- 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
113 lines
4.2 KiB
Python
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())
|