""" FL-MCP Protocol — SysEx encoding/decoding. Protocol: F0 7D [nibble-encoded UTF-8 JSON] F7 """ from __future__ import annotations SYSEX_ID = 0x7D def nibble_encode(data: bytes) -> list[int]: """Split each byte into two nibbles (high, low) for MIDI data byte compliance (< 0x80). Each byte 0x00-0xFF is split into two MIDI-safe bytes: - high nibble: (byte >> 4) & 0x0F - low nibble: byte & 0x0F """ result: list[int] = [] for byte in data: result.append((byte >> 4) & 0x0F) result.append(byte & 0x0F) return result def nibble_decode(nibbles: list[int]) -> bytes: """Reconstruct bytes from nibble pairs. Each pair (high, low) reconstructs one byte: byte = (high << 4) | low """ result = bytearray() for i in range(0, len(nibbles), 2): if i + 1 < len(nibbles): result.append(((nibbles[i] & 0x0F) << 4) | (nibbles[i + 1] & 0x0F)) return bytes(result) def encode_command(cmd: str, params: dict | None = None) -> list[int]: """Encode a JSON command as a complete SysEx message: F0 7D [nibbles...] F7.""" import json payload = json.dumps({"cmd": cmd, "params": params or {}}) nibbles = nibble_encode(payload.encode("utf-8")) return [0xF0, SYSEX_ID] + nibbles + [0xF7] def decode_command(data: list[int]) -> dict | None: """Decode a SysEx message back to JSON dict. Returns None if the message is not a valid FL-MCP SysEx message. """ if not data or len(data) < 4 or data[0] != 0xF0 or data[-1] != 0xF7: return None if data[1] != SYSEX_ID: return None import json nibbles = data[2:-1] if not nibbles: return None raw = nibble_decode(nibbles) if not raw: return None return json.loads(raw.decode("utf-8"))