- 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)
This commit is contained in:
452
Python/Event_Compare/Event_Compare.py
Normal file
452
Python/Event_Compare/Event_Compare.py
Normal file
@@ -0,0 +1,452 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user