c38a7b0fd8
New files:
main.py - PedalApp: boots all subsystems in order,
wires MIDI/footswitch callbacks, graceful
teardown reverses boot order
src/system/config.py - YAML config loader with deep-merge
(separated to avoid hardware deps)
src/system/services.py - systemd unit generator for pedal.service
+ multi-fx-pedal.target
scripts/install_service.sh - copies project, creates venv, installs
+ enables service units
tests/test_integration.py - 41 tests: boot, routing, display sync,
teardown, systemd content, CLI, edge cases
Modified:
tests/conftest.py - add project root to sys.path
221 lines
6.8 KiB
Python
Executable File
221 lines
6.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Standalone MIDI test tool — loopback test, clock monitor, and CC debug.
|
|
|
|
Tests MIDI UART and/or USB ports without requiring the full pedal
|
|
application. Works headless (no display needed).
|
|
|
|
Usage:
|
|
# Default (UART on /dev/ttyAMA0)
|
|
python scripts/midi_test.py
|
|
|
|
# Custom UART port
|
|
python scripts/midi_test.py --uart /dev/ttyUSB0
|
|
|
|
# USB-MIDI only
|
|
python scripts/midi_test.py --no-uart --usb
|
|
|
|
# Display clock BPM in real-time
|
|
python scripts/midi_test.py --clock-monitor
|
|
|
|
# Check all available MIDI ports (discovery mode)
|
|
python scripts/midi_test.py --discover
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import logging
|
|
import sys
|
|
import time
|
|
from typing import NoReturn
|
|
|
|
# ── Add src to path ─────────────────────────────────────────────────
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
|
|
|
|
from midi.handler import (
|
|
MIDIHandler,
|
|
UARTMIDI,
|
|
USBMIDI,
|
|
CC_EXPRESSION,
|
|
CC_VOLUME,
|
|
)
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
datefmt="%H:%M:%S",
|
|
)
|
|
logger = logging.getLogger("midi-test")
|
|
|
|
|
|
def discover_ports() -> None:
|
|
"""List available MIDI ports without opening them."""
|
|
print("=" * 50)
|
|
print("MIDI Port Discovery")
|
|
print("=" * 50)
|
|
|
|
# USBMIDI port listing
|
|
try:
|
|
import rtmidi # type: ignore[import-untyped]
|
|
|
|
midi_in = rtmidi.MidiIn()
|
|
midi_out = rtmidi.MidiOut()
|
|
midi_in.ignore_types(timing=True, sysex=False)
|
|
|
|
ports_in = midi_in.get_ports()
|
|
ports_out = midi_out.get_ports()
|
|
|
|
print(f"\nUSB-MIDI Input Ports ({len(ports_in)}):")
|
|
for i, p in enumerate(ports_in):
|
|
print(f" {i}: {p}")
|
|
|
|
print(f"\nUSB-MIDI Output Ports ({len(ports_out)}):")
|
|
for i, p in enumerate(ports_out):
|
|
print(f" {i}: {p}")
|
|
|
|
midi_in.close_port()
|
|
except ImportError:
|
|
print("\npython-rtmidi not installed — cannot scan USB ports")
|
|
|
|
# UART port check
|
|
print("\nUART MIDI Ports:")
|
|
for port in ["/dev/ttyAMA0", "/dev/ttyUSB0", "/dev/ttyS0"]:
|
|
p = Path(port)
|
|
if p.exists():
|
|
print(f" {port} — EXISTS")
|
|
else:
|
|
print(f" {port} — not found")
|
|
|
|
print()
|
|
print("On Raspberry Pi, also check:")
|
|
print(" /dev/serial0 (symlink to active UART)")
|
|
print(" dmesg | grep tty")
|
|
print(" dtoverlay=disable-bt (if using UART for MIDI)")
|
|
print(" core_freq=250 (for stable UART baud rate)")
|
|
|
|
|
|
def clock_monitor(handler: MIDIHandler) -> None:
|
|
"""Monitor MIDI clock BPM in real-time."""
|
|
|
|
def on_bpm(bpm: float) -> None:
|
|
logger.info("MIDI Clock BPM: %.1f", bpm)
|
|
|
|
handler.set_clock_callback(on_bpm)
|
|
logger.info("Clock monitor active — send MIDI clock to see BPM")
|
|
|
|
|
|
def dispatch_example(handler: MIDIHandler) -> None:
|
|
"""Subscribe to all MIDI events and print them."""
|
|
|
|
def on_pc(channel: int, program: int) -> None:
|
|
logger.info(" PC: ch=%d → program=%d", channel, program)
|
|
|
|
def on_cc(cc_number: int, value: int, channel: int) -> None:
|
|
logger.info(" CC: ch=%d, cc=%d → %d", channel, cc_number, value)
|
|
|
|
def on_note(note: int, velocity: int, channel: int) -> None:
|
|
logger.info(" Note: ch=%d, note=%d, vel=%d (on=%s)",
|
|
channel, note, velocity, velocity > 0)
|
|
|
|
def on_learn(mapping: object) -> None:
|
|
logger.info(" MIDI Learned: %s", mapping)
|
|
|
|
handler.set_pc_callback(on_pc)
|
|
handler.register_cc(CC_EXPRESSION, lambda v, c: on_cc(CC_EXPRESSION, v, c))
|
|
handler.register_cc(CC_VOLUME, lambda v, c: on_cc(CC_VOLUME, v, c))
|
|
handler.set_note_callback(on_note)
|
|
handler.set_midi_learn_callback(on_learn)
|
|
|
|
|
|
def send_test_messages(handler: MIDIHandler) -> None:
|
|
"""Send test MIDI messages out all open ports."""
|
|
logger.info("Sending test MIDI messages...")
|
|
|
|
handler.send(MIDIHandler.send_note_on(60, velocity=100))
|
|
time.sleep(0.1)
|
|
handler.send(MIDIHandler.send_note_off(60))
|
|
time.sleep(0.1)
|
|
handler.send(MIDIHandler.send_cc(7, 100))
|
|
time.sleep(0.1)
|
|
handler.send(MIDIHandler.send_pc(1))
|
|
time.sleep(0.1)
|
|
handler.send(MIDIHandler.send_pitch_bend(8192))
|
|
|
|
logger.info("Test messages sent (5 messages)")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(
|
|
description="MIDI test tool for Pi Multi-FX Pedal",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog=__doc__,
|
|
)
|
|
parser.add_argument("--uart", default="/dev/ttyAMA0",
|
|
help="UART device path (default: /dev/ttyAMA0)")
|
|
parser.add_argument("--no-uart", action="store_true",
|
|
help="Skip UART MIDI")
|
|
parser.add_argument("--no-usb", action="store_true",
|
|
help="Skip USB MIDI")
|
|
parser.add_argument("--usb-port", default="",
|
|
help="Filter for USB-MIDI port name")
|
|
parser.add_argument("--clock-monitor", action="store_true",
|
|
help="Display MIDI clock BPM in real-time")
|
|
parser.add_argument("--discover", action="store_true",
|
|
help="Discover available MIDI ports and exit")
|
|
parser.add_argument("--send-test", action="store_true",
|
|
help="Send test MIDI messages after startup")
|
|
parser.add_argument("--verbose", "-v", action="store_true",
|
|
help="Enable debug logging")
|
|
args = parser.parse_args()
|
|
|
|
if args.verbose:
|
|
logging.getLogger("midi").setLevel(logging.DEBUG)
|
|
|
|
if args.discover:
|
|
discover_ports()
|
|
return
|
|
|
|
handler = MIDIHandler()
|
|
|
|
uart_port = None if args.no_uart else args.uart
|
|
usb_enabled = not args.no_usb
|
|
|
|
print("=" * 50)
|
|
print("Pi Multi-FX Pedal — MIDI Test Tool")
|
|
print("=" * 50)
|
|
print(f" UART MIDI: {uart_port or 'disabled'}")
|
|
print(f" USB MIDI: {'enabled' if usb_enabled else 'disabled'}")
|
|
print(f" Clock mon: {'yes' if args.clock_monitor else 'no'}")
|
|
print(f" Send test: {'yes' if args.send_test else 'no'}")
|
|
print("=" * 50)
|
|
|
|
if args.clock_monitor:
|
|
clock_monitor(handler)
|
|
|
|
dispatch_example(handler)
|
|
|
|
print("\nStarting MIDI handler...")
|
|
handler.start(uart_port=uart_port, usb=usb_enabled, usb_port_name=args.usb_port)
|
|
|
|
print(f"\nActive interfaces: {handler.interface_names or 'none (software-only)'}")
|
|
|
|
if args.send_test:
|
|
send_test_messages(handler)
|
|
|
|
print("\nListening for MIDI messages...")
|
|
print("Press Ctrl+C to stop.\n")
|
|
|
|
try:
|
|
while True:
|
|
time.sleep(0.1)
|
|
except KeyboardInterrupt:
|
|
print("\nShutting down...")
|
|
finally:
|
|
handler.stop()
|
|
print("MIDI handler stopped.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |