Files
RP2350_MIDI_Lighter/Python/Event_Compare/Event_Compare.py
Chris 89c875e38f - First complete version of firmware. Currently being tested in the rehearsal room
- Added bunch of screens, fonts and images
 - Added script to read out frame buffer (function currently disabled in Firmware)
2025-10-26 20:57:58 +01:00

452 lines
16 KiB
Python

#!/usr/bin/env python3
"""
MIDI Test Validator
Compares MIDI events from USB MIDI device with firmware event counters
"""
import serial
import time
import rtmidi
import random
# Serial port configuration - adjust COM port as needed
FIRMWARE_PORT = "COM9" # Firmware USB serial port
BAUD_RATE = 115200
# MIDI device name to look for
MIDI_DEVICE_NAME = "Midilink" # Will search for devices containing this string
# MIDI Octave to filter (0-10, or None for all octaves)
MIDI_OCTAVE = 1 # Only count notes in octave 4 (middle C is C4)
# MIDI Channel (0-15, where 0 = channel 1)
MIDI_CHANNEL = 0
# Test data generation settings
TEST_NUM_EVENTS = 200000 # Total number of Note ON events to generate
TEST_MIN_DELAY_MS = 5 # Minimum delay between events (ms)
TEST_MAX_DELAY_MS = 30 # Maximum delay between events (ms)
TEST_MAX_OVERLAPPING = 300 # Maximum number of overlapping notes
# Debug output
DEBUG_ENABLED = False # Set to False to disable debug output
# MIDI definitions
MIDI_NOTE_ON = 0x90
MIDI_NOTE_OFF = 0x80
class MidiEventCounter:
def __init__(self):
self.red_on = 0
self.red_off = 0
self.green_on = 0
self.green_off = 0
self.blue_on = 0
self.blue_off = 0
def add_event(self, note, velocity, is_note_on, debug=False):
# Calculate octave (MIDI note 0-127, octave = note // 12, where C4 = MIDI note 60)
# MIDI octaves: C-1=0-11, C0=12-23, C1=24-35, C2=36-47, C3=48-59, C4=60-71, etc.
octave = note // 12 - 1 # Adjust so C4 = octave 4
note_in_octave = note % 12
if debug:
note_names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
note_name = f"{note_names[note_in_octave]}{octave}"
print(f"[DEBUG] Note {note} = {note_name} (Octave: {octave}, Note in octave: {note_in_octave})")
# Check if octave matches (if MIDI_OCTAVE is set)
if MIDI_OCTAVE is not None and octave != MIDI_OCTAVE:
if debug:
print(f"[DEBUG] -> Ignoring: Wrong octave (expected {MIDI_OCTAVE}, got {octave})")
return
# Determine color based on note (adjust to your config)
# Assuming C=Red, D=Green, E=Blue
color_mapped = None
if note_in_octave in [0, 1]: # C, C# = Red
if is_note_on:
self.red_on += 1
else:
self.red_off += 1
color_mapped = "RED"
elif note_in_octave in [2, 3]: # D, D# = Green
if is_note_on:
self.green_on += 1
else:
self.green_off += 1
color_mapped = "GREEN"
elif note_in_octave in [4, 5]: # E, F = Blue
if is_note_on:
self.blue_on += 1
else:
self.blue_off += 1
color_mapped = "BLUE"
else:
color_mapped = "NONE (ignored)"
if debug:
event_type = "ON " if is_note_on else "OFF"
print(f"[DEBUG] Mapped Note {note} -> {color_mapped} {event_type}")
def print_summary(self):
print("\nMIDI Device Event Counts:")
print(f" Red - ON: {self.red_on:6d}, OFF: {self.red_off:6d}")
print(f" Green - ON: {self.green_on:6d}, OFF: {self.green_off:6d}")
print(f" Blue - ON: {self.blue_on:6d}, OFF: {self.blue_off:6d}")
def midi_callback(message, data):
"""Callback for MIDI input - called when MIDI messages are received"""
event_counter = data
midi_message = message[0]
if len(midi_message) >= 3:
status = midi_message[0]
note = midi_message[1]
velocity = midi_message[2]
# Check for Note On or Note Off
if (status & 0xF0) == MIDI_NOTE_ON or (status & 0xF0) == MIDI_NOTE_OFF:
channel = status & 0x0F
is_note_on = ((status & 0xF0) == MIDI_NOTE_ON and velocity > 0)
event_type = "Note ON" if is_note_on else "Note OFF"
if DEBUG_ENABLED:
print(f"[MIDI] {event_type}, Channel: {channel}, Note: {note}, Velocity: {velocity}")
event_counter.add_event(note, velocity, is_note_on, debug=DEBUG_ENABLED)
def find_midi_device(device_name):
"""Find MIDI input device by name"""
midiin = rtmidi.MidiIn()
ports = midiin.get_ports()
print(f"\nAvailable MIDI Input devices:")
for i, port in enumerate(ports):
print(f" [{i}] {port}")
# Search for device containing the name
for i, port in enumerate(ports):
if device_name.lower() in port.lower():
print(f"\n✓ Found '{device_name}' input at port {i}: {port}")
return i
print(f"\n✗ Could not find MIDI input device containing '{device_name}'")
return None
def find_midi_output_device(device_name):
"""Find MIDI output device by name"""
midiout = rtmidi.MidiOut()
ports = midiout.get_ports()
print(f"\nAvailable MIDI Output devices:")
for i, port in enumerate(ports):
print(f" [{i}] {port}")
# Search for device containing the name
for i, port in enumerate(ports):
if device_name.lower() in port.lower():
print(f"\n✓ Found '{device_name}' output at port {i}: {port}")
return i
print(f"\n✗ Could not find MIDI output device containing '{device_name}'")
return None
def generate_test_data(midiout):
"""Generate random MIDI test data with overlapping notes"""
# Notes to use: C, C#, D, D#, E, F (Red, Green, Blue colors with alternates)
# Calculate MIDI note numbers for the configured octave
base_note = (MIDI_OCTAVE + 1) * 12 # Base note for the octave
notes = {
'C': base_note + 0, # Red
'C#': base_note + 1, # Red alternate
'D': base_note + 2, # Green
'D#': base_note + 3, # Green alternate
'E': base_note + 4, # Blue
'F': base_note + 5 # Blue alternate
}
note_names = list(notes.keys())
note_values = list(notes.values())
print(f"\nGenerating {TEST_NUM_EVENTS} random MIDI events...")
print(f"Octave: {MIDI_OCTAVE}, Channel: {MIDI_CHANNEL + 1}")
print(f"Using notes: {', '.join([f'{name}({notes[name]})' for name in note_names])}")
print("="*50 + "\n")
active_notes = [] # Track currently active (on but not yet off) notes
event_queue = [] # Queue of all events to send
# Generate NOTE ON events
for i in range(TEST_NUM_EVENTS):
note_idx = random.randint(0, len(note_values) - 1)
note = note_values[note_idx]
velocity = random.randint(64, 127) # Random velocity
event_queue.append({
'type': 'on',
'note': note,
'note_name': note_names[note_idx],
'velocity': velocity
})
active_notes.append(note)
# Randomly send some NOTE OFF events if we have overlapping notes
while len(active_notes) >= TEST_MAX_OVERLAPPING:
off_note = active_notes.pop(0)
off_note_name = note_names[note_values.index(off_note)]
event_queue.append({
'type': 'off',
'note': off_note,
'note_name': off_note_name,
'velocity': 0
})
# Random chance to send an OFF event even if under max
if len(active_notes) > 0 and random.random() < 0.4:
off_note = active_notes.pop(random.randint(0, len(active_notes) - 1))
off_note_name = note_names[note_values.index(off_note)]
event_queue.append({
'type': 'off',
'note': off_note,
'note_name': off_note_name,
'velocity': 0
})
# Send remaining NOTE OFF events
while len(active_notes) > 0:
off_note = active_notes.pop(0)
off_note_name = note_names[note_values.index(off_note)]
event_queue.append({
'type': 'off',
'note': off_note,
'note_name': off_note_name,
'velocity': 0
})
# Send all events with random delays
total_events = len(event_queue)
for idx, event in enumerate(event_queue):
if event['type'] == 'on':
message = [MIDI_NOTE_ON | MIDI_CHANNEL, event['note'], event['velocity']]
if DEBUG_ENABLED:
print(f"→ Note ON: {event['note_name']:3s} (MIDI {event['note']:3d}), Velocity: {event['velocity']:3d}")
else:
message = [MIDI_NOTE_OFF | MIDI_CHANNEL, event['note'], event['velocity']]
if DEBUG_ENABLED:
print(f"← Note OFF: {event['note_name']:3s} (MIDI {event['note']:3d})")
midiout.send_message(message)
# Show progress bar (only if debug is disabled to avoid cluttering output)
if not DEBUG_ENABLED:
progress = (idx + 1) / total_events
bar_length = 40
filled_length = int(bar_length * progress)
bar = '' * filled_length + '' * (bar_length - filled_length)
print(f'\rProgress: [{bar}] {idx + 1}/{total_events} events ({progress*100:.1f}%)', end='', flush=True)
# Random delay before next event
delay_ms = random.randint(TEST_MIN_DELAY_MS, TEST_MAX_DELAY_MS)
time.sleep(delay_ms / 1000.0)
if not DEBUG_ENABLED:
print() # New line after progress bar
print("\n" + "="*50)
print("✓ Test data generation complete")
print("="*50)
def read_firmware_counters(ser_fw):
"""Send 'a' command and read firmware event counters"""
# Clear input buffer
ser_fw.reset_input_buffer()
# Send command
ser_fw.write(b'a\r')
time.sleep(0.2)
# Read all available data
response = ser_fw.read(ser_fw.in_waiting).decode('ascii')
# Split by \r to get individual lines
lines = response.split('\r')
counters = {}
colors = ['red', 'green', 'blue']
line_idx = 0
for color in colors:
if line_idx < len(lines):
line = lines[line_idx].strip()
if ';' in line:
parts = line.split(';')
on_count = int(parts[0].strip())
off_count = int(parts[1].strip())
counters[color] = {'on': on_count, 'off': off_count}
else:
print(f"Warning: Could not parse line for {color}: {line}")
counters[color] = {'on': 0, 'off': 0}
line_idx += 1
else:
print(f"Warning: No data for {color}")
counters[color] = {'on': 0, 'off': 0}
return counters
def compare_results(midi_counts, firmware_counts):
"""Compare MIDI device and firmware event counts"""
print("\nFirmware Event Counts:")
print(f" Red - ON: {firmware_counts['red']['on']:6d}, OFF: {firmware_counts['red']['off']:6d}")
print(f" Green - ON: {firmware_counts['green']['on']:6d}, OFF: {firmware_counts['green']['off']:6d}")
print(f" Blue - ON: {firmware_counts['blue']['on']:6d}, OFF: {firmware_counts['blue']['off']:6d}")
print("\n" + "="*50)
print("COMPARISON RESULTS:")
print("="*50)
all_match = True
# Compare Red
if midi_counts.red_on == firmware_counts['red']['on'] and \
midi_counts.red_off == firmware_counts['red']['off']:
print("Red: PASS ✓")
else:
print(f"Red: FAIL ✗ (MIDI: {midi_counts.red_on}/{midi_counts.red_off}, "
f"Firmware: {firmware_counts['red']['on']}/{firmware_counts['red']['off']})")
all_match = False
# Compare Green
if midi_counts.green_on == firmware_counts['green']['on'] and \
midi_counts.green_off == firmware_counts['green']['off']:
print("Green: PASS ✓")
else:
print(f"Green: FAIL ✗ (MIDI: {midi_counts.green_on}/{midi_counts.green_off}, "
f"Firmware: {firmware_counts['green']['on']}/{firmware_counts['green']['off']})")
all_match = False
# Compare Blue
if midi_counts.blue_on == firmware_counts['blue']['on'] and \
midi_counts.blue_off == firmware_counts['blue']['off']:
print("Blue: PASS ✓")
else:
print(f"Blue: FAIL ✗ (MIDI: {midi_counts.blue_on}/{midi_counts.blue_off}, "
f"Firmware: {firmware_counts['blue']['on']}/{firmware_counts['blue']['off']})")
all_match = False
print("="*50)
if all_match:
print("✓ ALL TESTS PASSED")
else:
print("✗ SOME TESTS FAILED")
return all_match
def main():
print("MIDI Test Validator")
print("="*50)
print("\nSelect mode:")
print(" 1. Manual mode (record MIDI events manually)")
print(" 2. Auto-test mode (send random test data)")
mode = input("\nEnter mode (1 or 2): ").strip()
auto_test = (mode == '2')
# Find and open MIDI input device
midi_in_port = find_midi_device(MIDI_DEVICE_NAME)
if midi_in_port is None:
print("\nPlease check:")
print(" 1. MIDI device is connected")
print(" 2. MIDI_DEVICE_NAME variable matches your device")
return
midiin = rtmidi.MidiIn()
event_counter = MidiEventCounter()
# Set callback and open input port
midiin.set_callback(midi_callback, data=event_counter)
midiin.open_port(midi_in_port)
print(f"✓ Opened MIDI input port")
# Find and open MIDI output device (for auto-test mode)
midiout = None
if auto_test:
midi_out_port = find_midi_output_device(MIDI_DEVICE_NAME)
if midi_out_port is None:
print("\nCould not find MIDI output device for auto-test mode")
midiin.close_port()
return
midiout = rtmidi.MidiOut()
midiout.open_port(midi_out_port)
print(f"✓ Opened MIDI output port")
# Open firmware serial port
try:
ser_firmware = serial.Serial(FIRMWARE_PORT, BAUD_RATE, timeout=1)
print(f"✓ Connected to firmware on {FIRMWARE_PORT}")
except serial.SerialException as e:
print(f"Error opening serial port: {e}")
midiin.close_port()
if midiout:
midiout.close_port()
return
# Clear firmware counters at startup
print("\nClearing firmware counters...")
read_firmware_counters(ser_firmware) # This reads and resets the counters
print("✓ Firmware counters cleared")
if MIDI_OCTAVE is not None:
print(f"\nConfiguration: Filtering for MIDI Octave {MIDI_OCTAVE} only")
else:
print(f"\nConfiguration: Accepting all MIDI octaves")
print(f"Debug output: {'ENABLED' if DEBUG_ENABLED else 'DISABLED'}")
if auto_test:
# Auto-test mode: send test data
print("\n" + "="*50)
print("AUTO-TEST MODE")
print("="*50)
time.sleep(0.5) # Small delay to ensure everything is ready
generate_test_data(midiout)
# Give some time for last events to be processed
time.sleep(0.5)
else:
# Manual mode: wait for user input
print("\n" + "="*50)
print("MANUAL MODE - Recording MIDI events...")
print("Press ENTER to stop recording and compare results")
print("="*50 + "\n")
try:
input() # Wait for user to press enter
except KeyboardInterrupt:
print("\n\nStopped by user (Ctrl+C)")
# Display results
event_counter.print_summary()
# Read firmware counters
print("\nReading firmware counters...")
firmware_counts = read_firmware_counters(ser_firmware)
# Compare results
compare_results(event_counter, firmware_counts)
# Close ports
midiin.close_port()
if midiout:
midiout.close_port()
ser_firmware.close()
print("\nPorts closed.")
if __name__ == "__main__":
main()