Files
shawn c38a7b0fd8 Add main entry point + systemd services + integration tests
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
2026-06-07 23:39:50 -04:00

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()