led-panel/scripts/led-panel-ble.py
Salty 71515ef517 Initial: panel docs, BLE tools, glove firmware notes
- LED panel BLE protocol (partial RE from glove firmware)
- Panel MAC: E0:6E:41:94:39:70, BLE name: YS623B101069L
- led-panel-ble.py: scan, slot switch, cycle tool
- ble-capture-only.py: GATT server capture proxy
- Glove firmware docs (ESP32 WROOM, 4-button controller)
- Slot switch command doesn't work — capture needed
2026-04-01 17:46:23 -04:00

113 lines
4.2 KiB
Python

#!/usr/bin/env python3
"""
LED Panel BLE Slot Switcher
Connects to Amusing LED panel via BLE and switches between saved program slots.
Protocol reverse-engineered from /home/seb/glove/lib/Device/led/Led.cpp
"""
import asyncio
import sys
from bleak import BleakScanner, BleakClient
# BLE UUIDs (Microchip transparent UART)
SERVICE_UUID = "49535343-fe7d-4ae5-8fa9-9fafd205e455"
WRITE_UUID = "49535343-8841-43f4-a8d4-ecbe34729bb3"
# Base command for slot switching
BASE_CMD = bytearray([
0xAA, 0x55, 0xFF, 0xFF, 0x0E, 0x00, 0x01, 0x00,
0xC1, 0x02, 0x18, 0x06, 0x02, 0x3C, 0x00, 0x01,
0xFF, 0x7F, 0xAA, 0x05
])
def make_slot_cmd(slot_id):
"""Build slot switch command. slot_id starts at 61 (base)."""
cmd = bytearray(BASE_CMD)
offset = slot_id - 61
cmd[13] = 0x3C + offset # byte 13
cmd[18] = 0xAA + offset # byte 18
return cmd
async def scan():
"""Scan for BLE devices, look for LED panels."""
print("Scanning for BLE devices (10s)...")
devices = await BleakScanner.discover(timeout=10)
candidates = []
for d in devices:
name = d.name or ""
# Show everything with a name, highlight likely LED panels
if name:
marker = " <<<" if any(k in name.lower() for k in ["led", "amus", "lux", "panel", "pixel"]) else ""
rssi = getattr(d, 'rssi', None) or (d.details.get('props', {}).get('RSSI', '?') if hasattr(d, 'details') and isinstance(d.details, dict) else '?')
print(f" {d.address} RSSI:{rssi} {name}{marker}")
candidates.append(d)
if not candidates:
print("No named devices found. Panel might need to be in pairing mode.")
return candidates
async def connect_and_switch(address, slot_id):
"""Connect to panel and switch to given slot."""
print(f"Connecting to {address}...")
async with BleakClient(address, timeout=15) as client:
print(f"Connected: {client.is_connected}")
# List services for debugging
for svc in client.services:
if svc.uuid == SERVICE_UUID:
print(f" Found LED service: {svc.uuid}")
for char in svc.characteristics:
props = ",".join(char.properties)
print(f" Char {char.uuid} [{props}]")
cmd = make_slot_cmd(slot_id)
print(f"Switching to slot {slot_id}: {cmd.hex(' ')}")
await client.write_gatt_char(WRITE_UUID, cmd, response=False)
print("Done!")
async def cycle_slots(address, start=61, end=65, delay_sec=3):
"""Cycle through slots to identify which animation is where."""
print(f"Connecting to {address} for slot cycling...")
async with BleakClient(address, timeout=15) as client:
print(f"Connected: {client.is_connected}")
for slot in range(start, end + 1):
cmd = make_slot_cmd(slot)
print(f" Slot {slot}: {cmd.hex(' ')}")
await client.write_gatt_char(WRITE_UUID, cmd, response=False)
if slot < end:
print(f" Waiting {delay_sec}s...")
await asyncio.sleep(delay_sec)
print("Cycle complete!")
async def main():
if len(sys.argv) < 2:
print("Usage:")
print(" python3 led-panel-ble.py scan")
print(" python3 led-panel-ble.py slot <address> <slot_id>")
print(" python3 led-panel-ble.py cycle <address> [start] [end] [delay]")
print()
print("Examples:")
print(" python3 led-panel-ble.py scan")
print(" python3 led-panel-ble.py slot AA:BB:CC:DD:EE:FF 61")
print(" python3 led-panel-ble.py cycle AA:BB:CC:DD:EE:FF 61 65 3")
return
cmd = sys.argv[1]
if cmd == "scan":
await scan()
elif cmd == "slot" and len(sys.argv) >= 4:
addr = sys.argv[2]
slot = int(sys.argv[3])
await connect_and_switch(addr, slot)
elif cmd == "cycle" and len(sys.argv) >= 3:
addr = sys.argv[2]
start = int(sys.argv[3]) if len(sys.argv) > 3 else 61
end = int(sys.argv[4]) if len(sys.argv) > 4 else 65
delay = int(sys.argv[5]) if len(sys.argv) > 5 else 3
await cycle_slots(addr, start, end, delay)
else:
print("Invalid args. Run without args for usage.")
if __name__ == "__main__":
asyncio.run(main())