- Added bunch of screens, fonts and images - Added script to read out frame buffer (function currently disabled in Firmware)
452 lines
16 KiB
Python
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() |