""" Phase 0: SysEx Loopback Validation Test. Tests whether MIDI SysEx messages can travel through the Windows MIDI Services loopback port "FL_MCP" from the MCP server to the FL Studio controller script. Usage: python test_sysex_loopback.py Requires: - FL Studio running with FL_MCP controller script loaded - Windows MIDI Services loopback ports "FL_MCP 0" (input) and "FL_MCP 1" (output) """ from __future__ import annotations import sys import time try: import mido except ImportError: print("ERROR: mido not installed. Run: pip install mido") sys.exit(1) def send_sysex_raw(output, data): """Send raw SysEx bytes via mido.""" msg = mido.Message("sysex", data=bytes(data)) output.send(msg) def test_simple_sysex(): """Test 1: Send F0 7D 48 69 F7 ("Hi") and expect it back.""" print("\n=== Test 1: Simple SysEx Echo ===") output_name = None input_name = None for name in mido.get_output_names(): if "FL_MCP" in name and "1" in name: output_name = name break for name in mido.get_input_names(): if "FL_MCP" in name and "0" in name: input_name = name break if not output_name: print("FAIL: FL_MCP output port (FL_MCP 1) not found") print("Available outputs:", mido.get_output_names()) return False if not input_name: print("FAIL: FL_MCP input port (FL_MCP 0) not found") print("Available inputs:", mido.get_input_names()) return False print(f"Output: {output_name}") print(f"Input: {input_name}") try: output = mido.open_output(output_name) input_port = mido.open_input(input_name) except Exception as e: print(f"FAIL: Cannot open ports: {e}") return False # Send "Hi" ping: F0 7D 48 69 F7 # (0x48='H', 0x69='i') ping_data = [0xF0, 0x7D, 0x48, 0x69, 0xF7] print(f"Sending: {' '.join(f'{b:02X}' for b in ping_data)}") send_sysex_raw(output, ping_data[1:-1]) # mido adds F0/F7 print("Waiting 5s for echo...") timeout = 5.0 start = time.time() received = None while time.time() - start < timeout: msg = input_port.receive(timeout=0.1) if msg and msg.type == "sysex": received = list(msg.bytes()) break output.close() input_port.close() if received: print(f"Received: {' '.join(f'{b:02X}' for b in received)}") if received[0] == 0xF0 and received[-1] == 0xF7 and received[1] == 0x7D: print("PASS: Simple SysEx loopback works!") return True print(f"UNKNOWN: Received but unexpected bytes: {received}") return False else: print("FAIL: No message received within timeout") return False def test_json_command(): """Test 2: Send a proper JSON command via nibble encoding.""" print("\n=== Test 2: JSON Command SysEx ===") import json output_name = None input_name = None for name in mido.get_output_names(): if "FL_MCP" in name and "1" in name: output_name = name break for name in mido.get_input_names(): if "FL_MCP" in name and "0" in name: input_name = name break if not output_name or not input_name: print("SKIP: FL_MCP ports not available for JSON test") return False try: output = mido.open_output(output_name) input_port = mido.open_input(input_name) except Exception as e: print(f"SKIP: Cannot open ports: {e}") return False # Encode ping command: {"cmd":"ping","params":{"ts":1}} payload = json.dumps({"cmd": "ping", "params": {"ts": 1}}).encode("utf-8") print(f"JSON payload: {payload}") # Nibble encode nibbles = [] for byte in payload: nibbles.append((byte >> 4) & 0x0F) nibbles.append(byte & 0x0F) sysex_data = [0xF0, 0x7D] + nibbles + [0xF7] print(f"SysEx: {' '.join(f'{b:02X}' for b in sysex_data)}") send_sysex_raw(output, sysex_data[1:-1]) print("Waiting 5s for response...") timeout = 5.0 start = time.time() received = None while time.time() - start < timeout: msg = input_port.receive(timeout=0.1) if msg and msg.type == "sysex": received = list(msg.bytes()) break output.close() input_port.close() if received: print(f"Received: {' '.join(f'{b:02X}' for b in received)}") # Decode nibbles if received[1] == 0x7D: nibble_data = received[2:-1] result = bytearray() for i in range(0, len(nibble_data), 2): if i + 1 < len(nibble_data): result.append(((nibble_data[i] & 0x0F) << 4) | (nibble_data[i + 1] & 0x0F)) decoded = result.decode("utf-8") print(f"Decoded JSON: {decoded}") print("PASS: JSON command roundtrip works!") return True else: print("FAIL: No response within timeout") return False def list_ports(): """List all available MIDI ports.""" print("\n=== Available MIDI Ports ===") print("Inputs:") for p in mido.get_input_names(): print(f" {p}") print("Outputs:") for p in mido.get_output_names(): print(f" {p}") if __name__ == "__main__": print("FL Studio MCP — SysEx Loopback Validation") print("=========================================") list_ports() test1 = test_simple_sysex() test2 = test_json_command() print("\n=== Summary ===") print(f"Simple SysEx: {'PASS' if test1 else 'FAIL'}") print(f"JSON Command: {'PASS' if test2 else 'FAIL'}") if test1 and test2: print("\nSysEx loopback is functional. Phase 1-4 can proceed.") sys.exit(0) else: print("\nSysEx loopback FAILED. Phase 1-4 ABORTED — pivot required.") sys.exit(1)