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
This commit is contained in:
Executable
+221
@@ -0,0 +1,221 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user