Files
midi2motor/test_midi.py
devdesk 89857a1769 Add serial PWM control and Streamlit GUI
- src/main.cpp: Add serial command interface (A/B/C values, s=status, 0=off)
- src/main.cpp: Serial control disables MIDI timeout for persistent values
- serial_control.py: Streamlit GUI with sliders and quick action buttons
- serial_control.py: Filter out ttyS* ports, prioritize USB serial
- test_midi.py: Update for new CC control mapping (CC 20/21/22)
- README.md: Complete project documentation
2026-01-28 00:50:59 +02:00

269 lines
8.6 KiB
Python
Executable File

#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "mido",
# "python-rtmidi",
# ]
# ///
"""
Test MIDI receiver on Arduino with CC and Note control.
CC Mapping (Arduino pins 9, 10, 11):
- CC 20 -> PWM Pin 1 (pin 9)
- CC 21 -> PWM Pin 2 (pin 10)
- CC 22 -> PWM Pin 3 (pin 11)
Note Mapping (optional, enabled in Arduino):
- Note 60 (C4) -> PWM Pin 1
- Note 62 (D4) -> PWM Pin 2
- Note 64 (E4) -> PWM Pin 3
"""
import mido
import time
import argparse
import math
# CC numbers matching Arduino configuration
CC_PWM_1 = 20
CC_PWM_2 = 21
CC_PWM_3 = 22
# Note numbers matching Arduino configuration
NOTE_CH1 = 60 # C4
NOTE_CH2 = 62 # D4
NOTE_CH3 = 64 # E4
def find_midi_port():
"""Find a suitable MIDI output port."""
outputs = mido.get_output_names()
print("Available MIDI outputs:")
for i, name in enumerate(outputs):
print(f" {i}: {name}")
if not outputs:
return None
# Find CH345 or use first available
for name in outputs:
if "CH345" in name or "MIDI" in name:
return name
return outputs[0]
def demo_cc_sweep(output, duration=5.0):
"""Sweep all three CC channels from 0 to 127 and back."""
print("\n--- CC Sweep Demo ---")
print("Ramping all channels 0->127->0")
steps = 128
delay = duration / (steps * 2)
# Ramp up
for val in range(0, 128):
for cc in [CC_PWM_1, CC_PWM_2, CC_PWM_3]:
msg = mido.Message('control_change', control=cc, value=val, channel=0)
output.send(msg)
if val % 16 == 0:
print(f" CC value: {val}")
time.sleep(delay)
# Ramp down
for val in range(127, -1, -1):
for cc in [CC_PWM_1, CC_PWM_2, CC_PWM_3]:
msg = mido.Message('control_change', control=cc, value=val, channel=0)
output.send(msg)
if val % 16 == 0:
print(f" CC value: {val}")
time.sleep(delay)
print("Sweep complete.\n")
def demo_cc_individual(output):
"""Test each CC channel individually."""
print("\n--- Individual CC Channel Test ---")
channels = [
(CC_PWM_1, "PWM Pin 1 (pin 9)"),
(CC_PWM_2, "PWM Pin 2 (pin 10)"),
(CC_PWM_3, "PWM Pin 3 (pin 11)"),
]
for cc, name in channels:
print(f"Testing CC {cc} - {name}")
# Turn on
msg = mido.Message('control_change', control=cc, value=127, channel=0)
output.send(msg)
print(f" CC {cc} = 127")
time.sleep(1.0)
# Turn off
msg = mido.Message('control_change', control=cc, value=0, channel=0)
output.send(msg)
print(f" CC {cc} = 0")
time.sleep(0.5)
print("Individual test complete.\n")
def demo_cc_wave(output, duration=10.0):
"""Create a sine wave pattern on all channels with phase offset."""
print("\n--- CC Sine Wave Demo ---")
print(f"Running for {duration} seconds (Ctrl+C to stop early)")
start = time.time()
freq = 0.5 # Hz
try:
while time.time() - start < duration:
t = time.time() - start
# Phase-shifted sine waves for each channel
val1 = int(63.5 + 63.5 * math.sin(2 * math.pi * freq * t))
val2 = int(63.5 + 63.5 * math.sin(2 * math.pi * freq * t + 2 * math.pi / 3))
val3 = int(63.5 + 63.5 * math.sin(2 * math.pi * freq * t + 4 * math.pi / 3))
output.send(mido.Message('control_change', control=CC_PWM_1, value=val1, channel=0))
output.send(mido.Message('control_change', control=CC_PWM_2, value=val2, channel=0))
output.send(mido.Message('control_change', control=CC_PWM_3, value=val3, channel=0))
time.sleep(0.02)
except KeyboardInterrupt:
pass
# Turn off all
for cc in [CC_PWM_1, CC_PWM_2, CC_PWM_3]:
output.send(mido.Message('control_change', control=cc, value=0, channel=0))
print("Wave demo complete.\n")
def demo_notes(output):
"""Test note-based control (legacy mode)."""
print("\n--- Note Control Demo ---")
print("Testing notes: C4 (60), D4 (62), E4 (64)")
notes = [
(NOTE_CH1, "C4 - PWM Pin 1"),
(NOTE_CH2, "D4 - PWM Pin 2"),
(NOTE_CH3, "E4 - PWM Pin 3"),
]
for note, name in notes:
print(f" Note ON: {name} vel=100")
output.send(mido.Message('note_on', note=note, velocity=100, channel=0))
time.sleep(0.5)
print(f" Note OFF: {name}")
output.send(mido.Message('note_off', note=note, velocity=0, channel=0))
time.sleep(0.3)
print("Note demo complete.\n")
def interactive_mode(output):
"""Interactive CC control mode."""
print("\n--- Interactive Mode ---")
print("Commands:")
print(" 1/2/3 <value> - Set CC channel 1/2/3 to value (0-127)")
print(" all <value> - Set all channels to value")
print(" sweep - Run sweep demo")
print(" wave - Run wave demo")
print(" off - Turn all channels off")
print(" q/quit - Exit")
print()
try:
while True:
cmd = input("> ").strip().lower()
if cmd in ('q', 'quit', 'exit'):
break
elif cmd == 'off':
for cc in [CC_PWM_1, CC_PWM_2, CC_PWM_3]:
output.send(mido.Message('control_change', control=cc, value=0, channel=0))
print("All channels off")
elif cmd == 'sweep':
demo_cc_sweep(output)
elif cmd == 'wave':
demo_cc_wave(output)
elif cmd.startswith('all '):
try:
val = int(cmd.split()[1])
val = max(0, min(127, val))
for cc in [CC_PWM_1, CC_PWM_2, CC_PWM_3]:
output.send(mido.Message('control_change', control=cc, value=val, channel=0))
print(f"All channels set to {val}")
except (ValueError, IndexError):
print("Usage: all <value>")
elif cmd[0] in '123' and ' ' in cmd:
try:
ch = int(cmd[0])
val = int(cmd.split()[1])
val = max(0, min(127, val))
cc = [CC_PWM_1, CC_PWM_2, CC_PWM_3][ch - 1]
output.send(mido.Message('control_change', control=cc, value=val, channel=0))
print(f"Channel {ch} (CC {cc}) set to {val}")
except (ValueError, IndexError):
print("Usage: 1/2/3 <value>")
elif cmd:
print("Unknown command. Type 'q' to quit.")
except (KeyboardInterrupt, EOFError):
pass
# Turn off on exit
for cc in [CC_PWM_1, CC_PWM_2, CC_PWM_3]:
output.send(mido.Message('control_change', control=cc, value=0, channel=0))
print("\nExiting interactive mode.")
def main():
parser = argparse.ArgumentParser(description="Test MIDI CC control for Arduino PWM")
parser.add_argument('--mode', '-m', choices=['sweep', 'individual', 'wave', 'notes', 'interactive', 'all'],
default='all', help='Demo mode to run (default: all)')
parser.add_argument('--duration', '-d', type=float, default=5.0,
help='Duration for sweep/wave demos (default: 5.0)')
args = parser.parse_args()
port_name = find_midi_port()
if port_name is None:
print("No MIDI output ports found!")
return
print(f"\nUsing: {port_name}")
print("Press Ctrl+C to stop\n")
with mido.open_output(port_name) as output:
try:
if args.mode == 'interactive':
interactive_mode(output)
elif args.mode == 'sweep':
demo_cc_sweep(output, args.duration)
elif args.mode == 'individual':
demo_cc_individual(output)
elif args.mode == 'wave':
demo_cc_wave(output, args.duration)
elif args.mode == 'notes':
demo_notes(output)
else: # all
demo_cc_individual(output)
demo_cc_sweep(output, args.duration)
demo_notes(output)
print("All demos complete!")
except KeyboardInterrupt:
# Ensure all outputs are off
for cc in [CC_PWM_1, CC_PWM_2, CC_PWM_3]:
output.send(mido.Message('control_change', control=cc, value=0, channel=0))
print("\nStopped. All channels turned off.")
if __name__ == "__main__":
main()