⚠️ This repository is archived. Per-zone animation viaCOMMAND_INTERFACE(IID 60) was confirmed non-functional — the device silently ignores writes regardless of packet format. Active development has moved to magrgb-controller, which uses the reliable HAP lightbulb characteristics (IID 51–54). This repo is kept for protocol research reference only.
Full Python controller, SignalRGB integration, and technical protocol documentation for the Secretlab Magnus XL RGB Strip (co-developed with Nanoleaf, sold as MAGRGB).
- Device Identification
- The Reverse Engineering Journey
- Protocol Specification
- Initial Setup
- Script Reference
- SignalRGB Integration
- Architecture
- Files in This Repo
- Future Work
- Legal
| Field | Value |
|---|---|
| Product | Secretlab Magnus XL RGB Desk Strip |
| OEM | Nanoleaf (sold as MAGRGB) |
| BLE Advertised Name | Secretlab MAGRGB XXBJ |
| Protocol | HAP-BLE (Apple HomeKit Accessory Protocol over Bluetooth LE) |
| Power | USB |
After factory reset the device advertises on two simultaneous addresses — one per protocol:
| Address | Manufacturer ID | Protocol | Purpose |
|---|---|---|---|
XX:XX:XX:XX:XX:XX |
76 (Apple) |
HAP-BLE | HomeKit pairing + control |
YY:YY:YY:YY:YY:YY |
2059 (Nanoleaf) |
LTPDU | Nanoleaf app + Thread control |
Note: Both addresses change after each factory reset. Use
scan.pyto find the current ones.
| Service UUID | Purpose |
|---|---|
00001800-0000-1000-8000-00805f9b34fb |
Generic Access (device name) |
0000003e-0000-1000-8000-0026bb765291 |
HAP Accessory Information |
00000055-0000-1000-8000-0026bb765291 |
HAP Pairing (Pair-Setup 0x4C, Pair-Verify 0x4E) |
00000043-0000-1000-8000-0026bb765291 |
HAP Lightbulb — color control lives here |
00000701-0000-1000-8000-0026bb765291 |
Nanoleaf scene/effect service |
6d2ae1c4-9aea-11ea-bb37-0242ac130002 |
Nanoleaf LTPDU transport (encrypted) |
| Characteristic | HAP UUID | IID |
|---|---|---|
| On / Off | 00000025-...-0026bb765291 |
51 |
| Brightness (0–100) | 00000008-...-0026bb765291 |
52 |
| Hue (0–360°) | 00000013-...-0026bb765291 |
53 |
| Saturation (0–100%) | 0000002f-...-0026bb765291 |
54 |
The strip is controlled via the Nanoleaf app on Android/iOS, which meant the BLE traffic would tell us the protocol. The first step was an Android HCI snoop log capture.
What the snoop log showed:
The log captured encrypted variable-length packets to handles 0x008e and 0x0090 — the actual Nanoleaf app traffic to the MAGRGB, encrypted using X25519 + AES-CTR (Nanoleaf's LTPDU protocol) or HAP-BLE ChaCha20-Poly1305. Since the keys are ephemeral per-session, these packets cannot be decrypted from the capture alone.
A GATT service enumeration (see discover_services.py) revealed the device exposes both HAP-BLE and Nanoleaf LTPDU service trees simultaneously:
- HAP services (
0026bb765291UUID namespace) → Apple HomeKit protocol - LTPDU service (
6d2ae1c4-9aea-11ea-bb37-0242ac130002) → Nanoleaf proprietary protocol over Thread/CoAP
The device also advertises with two manufacturer IDs — Apple's (76) for HomeKit discovery and Nanoleaf's (2059) for the Nanoleaf app — on separate rotating BLE addresses.
This explains why aiohomekit's BLE scanner (which filters for Apple manufacturer ID 76) couldn't find the device during normal operation: it was paired and broadcasting an encrypted notification advertisement rather than a pairable one.
After factory resetting the device, the HAP address broadcasts a standard unencrypted HomeKit advertisement (manufacturer data starting with 0x06). aiohomekit's normal discovery still couldn't find it because the advertisement used Nanoleaf's advertisement address, not the Apple one.
Solution: Bypass aiohomekit's scanner-based discovery entirely. Use BleakScanner.find_device_by_address() to get the BLEDevice directly, then invoke aiohomekit's low-level drive_pairing_state_machine() directly with the HAP Pair-Setup characteristic (0x4C).
The SRP pairing uses the 8-digit HomeKit setup code printed on the device in XXX-XX-XXX format. The format matters — aiohomekit's check_pin_format rejects any other format.
Pairing flow:
1. BleakScanner.find_device_by_address(HAP_MAC)
2. AIOHomeKitBleakClient.connect()
3. drive_pairing_state_machine(PAIR_SETUP, perform_pair_setup_part1())
→ device returns SRP salt + public key
4. drive_pairing_state_machine(PAIR_SETUP, perform_pair_setup_part2(pin, uuid, salt, pubkey))
→ device returns long-term key pair (AccessoryLTPK, iOSDeviceLTSK, etc.)
5. Save pairing_data to pairing.json
With a valid pairing, HAP-BLE control works through aiohomekit's BlePairing.put_characteristics(). One compatibility issue: aiohomekit's BlePairing class expects to be driven by the full controller/scanner infrastructure, and crashes if _accessories_state is None when advertisement callbacks fire.
Fix: Pre-initialize _accessories_state with an empty AccessoriesState(Accessories(), 0, None, 0) immediately after loading the pairing.
Color is communicated in HSV space (not RGB), as HAP's Lightbulb service uses Hue + Saturation + Brightness as separate characteristics. RGB values from SignalRGB's WLED DRGB stream are converted with Python's colorsys.rgb_to_hsv().
HAP-BLE uses a standard Pair-Verify handshake (X25519 + Ed25519) after pairing to establish a ChaCha20-Poly1305 encrypted session. aiohomekit handles this transparently.
Control packets go through aiohomekit's put_characteristics([(aid, iid, value)]):
| Characteristic | IID | Type |
|---|---|---|
| On / Off | 51 |
boolean |
| Brightness | 52 |
int 0–100 |
| Hue | 53 |
int 0–360 |
| Saturation | 54 |
int 0–100 |
| COMMAND_INTERFACE | 60 |
bytes — Nanoleaf animation writes |
HAP has a discrete On/Off characteristic (IID 51). Sending RGB (0,0,0) does NOT turn off the light — you must write False to IID_ON. The bridge handles this automatically.
The bridge uses the Nanoleaf Animation Protocol via HAP COMMAND_INTERFACE (IID 60, UUID A28E1902) to achieve per-zone color control across 60 independent zones. This was reverse-engineered from the Nanoleaf Android app (me.nanoleaf.nanoleaf) using JADX.
The device exposes a Nanoleaf Animation Service (A18E6901-...) with several characteristics, but none of these are accessible via HAP — they are filtered out at the firmware level. The actual animation path was found in CommandCentreRepository.java:
// Line 6203 / 10806 — writes animation to COMMAND_INTERFACE, not ANIMATION_WRITE
new Ee.a(TlvType.DISPLAY_SCENE, scene.toByteArray(accessoryType)).formattedByteArray()The LTPDU service (6D2AE1C4-...) uses a separate Curve25519 + AES-CTR encrypted channel but animation commands are explicitly filtered out of LTPDU (n.java line 506). All animation traffic goes through the HAP session.
Every animation write to IID=60 uses this outer frame:
[cmd_hi, cmd_lo, len_hi, len_lo, ...TLV2 bytes...]
Where cmd_hi cmd_lo is the command type (DISPLAY_SCENE = 07 01), followed by a 2-byte big-endian length, followed by the TLV2 animation payload.
TLV2 payload = metaDataTlv + paletteTlv
MetaData TLV (tag=0x01, 1-byte length):
STRIPES (0x06): [sceneId, 0x06, transitTime, direction, segment]
FLOW (0x05): [sceneId, 0x05, transitTime, waitTime, direction, loopByte]
FADE (0x01): [sceneId, 0x01, transitTime, waitTime, loopByte]
Palette TLV (tag=0x02, 1-byte length, max 84 colors):
[numColors, c0_b2, c0_b1, c0_b0, ...]
Each color is 3-byte big-endian:
bit23 = repeat flag
bits22-14 = hue (0–360)
bits13-7 = sat (0–100)
bits6-0 = bri (0–100)
i = (repeat<<23) | (hue<<14) | (sat<<7) | bri
STRIPES divides the strip into equal segments, one color per segment. The segment parameter is the width of each segment as a percentage of the total strip length:
segment value |
Zones | Result |
|---|---|---|
| 33 | 3 | three equal thirds |
| 14 | ~7 | rainbow |
| 5 | 20 | coarse zones |
| 2 | 60 | production setting |
| 1 | 84 | finest (max without 2-byte palette) |
STRIPES and FLOW are only supported on SECRETLABS_LIGHT_STRIPS device type — confirmed working on the MAGRGB.
The bridge uses 60 zones (segment=2):
- 190-byte packet per frame — well within BLE MTU after HAP fragmentation
- Uses standard 1-byte TLV length (60 colors × 3 bytes + 1 = 181 bytes < 255)
- 123 SignalRGB pixels bucketed into 60 equal zones (~2 pixels per zone average)
- Visually indistinguishable from 84 or 123 zones at desk distance
# Packet structure (190 bytes total):
# [07 01] — DISPLAY_SCENE command
# [00 BC] — TLV2 length = 188
# [01 05 01 06 00 00 02] — MetaData TLV: STRIPES, transit=0, dir=0, seg=2
# [02 BD 3C ...] — Palette TLV: 60 colors × 3 bytes| File | Finding |
|---|---|
CommandCentreRepository.java |
Animation writes to COMMAND_INTERFACE (IID=60), not ANIMATION_WRITE (A18E6903) |
EndpointLookup.java |
Endpoints.CommandInterface is the animation endpoint |
n.java |
CommandInterface commands filtered OUT of LTPDU — confirms HAP-only path |
SimpleScene.java |
TLV2 wire format (metaDataTlv + paletteTlv) |
EffectType.java |
Effect byte values (STRIPES=6, FLOW=5, FADE=1, RANDOM=2, HIGHLIGHT=3) |
TlvType2.java / Tlv2.java |
Tag byte constants and 1-byte length encoding |
ze/C8489a.java |
LTPDU Curve25519 + AES-CTR crypto (not used for animation) |
- Python 3.9+
- Windows 10/11 with Bluetooth LE adapter
- The device factory reset (hold reset button ~10s until light flashes)
Tested with: Python 3.11, bleak 0.21, aiohomekit 3.2, SignalRGB 2.x, Windows 11 22H2+
pip install -r requirements.txtpython scan.pyLook for Secretlab MAGRGB XXBJ. Note both MAC addresses — you need the one with the Apple manufacturer ID for HAP pairing.
python scan_adv.pyThis shows raw advertisement data. The HAP address has Manufacturer: {76: '06...'}.
Edit the DEVICE_MAC constant in all four scripts with the Apple manufacturer ID address found in Step 1:
pair.pymagnus_wled_bridge.pytest.pydiscover_services.py
Each file has a # EDIT THIS comment above the constant.
python pair.py XXX-XX-XXXPass the 8-digit HomeKit setup code as a command-line argument. Format must be XXX-XX-XXX.
This creates pairing.json with your long-term keypair. Keep this file and never commit it — it contains your private HAP credentials. It is already listed in .gitignore.
python test.pyThe strip should cycle: RED → GREEN → BLUE → WHITE 50% → OFF.
| Script | Purpose | Usage |
|---|---|---|
scan.py |
List all nearby BLE devices | python scan.py |
scan_adv.py |
Show raw advertisement data for MAGRGB addresses | python scan_adv.py |
discover_services.py |
Enumerate GATT services and characteristics | python discover_services.py |
pair.py |
One-time HAP-BLE pairing, saves pairing.json |
python pair.py XXX-XX-XXX |
test.py |
Color cycle test — RED/GREEN/BLUE/WHITE/OFF | python test.py |
magnus_wled_bridge.py |
SignalRGB WLED bridge (run as Administrator) | python magnus_wled_bridge.py |
The strip is exposed to SignalRGB as a WLED device — no custom plugin needed.
Step 1 — Start the bridge (run as Administrator for port 80)
python magnus_wled_bridge.pyOutput:
WLED UDP on 127.0.0.2:21325
WLED HTTP on :80
Waiting for XX:XX:XX:XX:XX:XX...
Found: Secretlab MAGRGB XXBJ
HAP-BLE loop running.
Step 2 — Add in SignalRGB
- Open SignalRGB → Home → Lighting Services → WLED
- In "Discover WLED device by IP" enter
127.0.0.2and press Enter - SignalRGB calls
/json/infoon port 80, gets back"brand": "WLED"and adds the device - Click Link — "Magnus RGB Strip" is now on your canvas
Note: If you also run the Manka boom arm bridge, it runs on
127.0.0.1:80. The Magnus bridge runs on127.0.0.2:80— different loopback IPs, same port. SignalRGB discovers each by IP with no port suffix needed.
Step 3 — Assign an effect
Drag the Magnus RGB Strip block on your canvas and assign any effect.
- Open Task Scheduler → Create Task
- General: Name
Magnus BLE Bridge, check Run with highest privileges - Triggers: At startup, delay 30 seconds
- Actions: Start
python, argumentsC:\path\to\MAGRGB-controller\magnus_wled_bridge.py, start inC:\path\to\MAGRGB-controller - Save
╔═══════════════════════════════════════════════════════════════════╗
║ YOUR PC ║
║ (127.0.0.2) ║
║ ║
║ ┌─────────────────┐ ┌────────────────────────────────────┐ ║
║ │ SignalRGB │ │ magnus_wled_bridge.py │ ║
║ │ │ │ │ ║
║ │ Canvas effect │─────▶│ HTTP :80 (WLED discovery) │ ║
║ │ assigns color │ UDP │ UDP :21325 (DRGB color stream) │ ║
║ │ to MAGRGB │─────▶│ │ ║
║ └─────────────────┘ │ bucket 123px → 60 zones │ ║
║ │ RGB→HSV, STRIPES packet → IID=60 │ ║
║ │ HAP-BLE asyncio loop, max 10 Hz │ ║
║ └───────────────┬────────────────────┘ ║
║ │ HAP-BLE ║
║ │ (ChaCha20-Poly1305 ║
║ │ encrypted GATT) ║
╚════════════════════════════════════════════╪══════════════════════╝
│
┌────────▼────────┐
│ Secretlab │
│ MAGRGB │
│ HAP-BLE GATT │
└─────────────────┘
Key differences from raw GATT approaches:
- All BLE writes are encrypted (ChaCha20-Poly1305) — HAP session encryption
- Color is HSV not RGB — converted before each write
- On/Off is a separate characteristic — must be set explicitly, not inferred from black color
- Max ~10 Hz update rate (HAP-BLE round-trip is slower than raw GATT)
| File | Purpose |
|---|---|
pair.py |
One-time pairing — SRP pairing via HAP-BLE, saves pairing.json |
magnus_wled_bridge.py |
Main integration — WLED emulator + HAP-BLE bridge for SignalRGB |
test.py |
Color cycle test — verifies control works after pairing |
scan.py |
BLE scanner — lists all nearby devices with names and MACs |
scan_adv.py |
Advertisement scanner — shows raw manufacturer data for MAGRGB addresses |
discover_services.py |
GATT service enumerator — lists all services and characteristics |
compat.py |
Bleak 2.x compatibility shim used by bridge and test scripts |
requirements.txt |
Python dependencies with minimum version bounds |
pairing.json |
Your long-term keypair — generated by pair.py, gitignored (see pairing.json.example) |
pairing.json.example |
Schema reference for pairing.json with redacted placeholder values |
- Per-zone color control — IID 60 (
COMMAND_INTERFACE) packet format is correct per Nanoleaf APK source, but device ignores writes in practice (no error, no visual response). Bridge falls back to lightbulb IID 51–54 (single colour). Root cause unknown. - Auto-detect HAP MAC address on startup (handles address rotation after reset)
- Apple HomeKit re-integration alongside bridge (HAP supports up to 16 controllers)
- LTPDU integration — crypto is fully reverse-engineered (Curve25519 + AES-CTR); could enable firmware or non-animation commands. Animation itself goes via HAP, not LTPDU.
This project was developed for personal interoperability use with hardware the author owns. Reverse engineering for interoperability purposes is permitted under DMCA §1201(f) (US) and equivalent provisions in other jurisdictions.
Not affiliated with, endorsed by, or connected to Secretlab, Nanoleaf, or Apple. All trademarks are property of their respective owners.
Use at your own risk.