Annotated walkthrough · for review

eagle_auto.py

The Magic Lantern whip driver — 135 lines, written 2 September 2026. Every other family module on Eagle is a copy-forward of this file. It is where the locked mechanism came from, and it carries two scars in its own comments.

Family
magic_lantern
Vendor app
wl.smartled.rgb
Drives
3 whips
Write char
fff3
Source
Eagle, live 19:47
In git
eagle-lab/

1 · The thesis, in the docstring

6WHY THIS EXISTS (Zora, 2026-09-02):7  Every failing run on 09-02 called Device1.Connect() -- asking bluetoothd to establish the link.8  Measured: the link forms and the daemon aborts it at 0.7s; discovery never completes (tested to 120s).9  Ray's own banked maintain.py:166 never does that. It registers the MAC in the CONTROLLER's10  filter-accept-list (btmgmt add-device -a 2) and the HARDWARE completes the link the instant the11  device advertises -- the same mechanism a phone uses. D-Bus is then used ONLY for reads/writes.

Mechanism lines 9–11

This is the whole idea. We stop asking bluetoothd to connect and instead hand the MAC to the controller's filter-accept-list, then let the radio complete the link in hardware when the device next advertises. D-Bus is demoted to reads and writes only.

The prior art was already on the box — your own maintain.py:166. Line 9 says so explicitly.

Measurement line 8, line 12

PathResult
Device1.Connect()link forms, daemon aborts at 0.7 s
discoverynever completes, tested to 120 s
accept-list connect~1 s
services resolve~5 s strong · ~15 s weak
warm command~8 ms

2 · A threshold set from the worst case, on purpose

14⛔ RESOLVE TIMEOUT IS SET FROM THE WEAKEST DEVICE, NOT THE STRONGEST.15  E9 (~-32 dBm) resolves in ~5s; D2 (~-56 dBm) takes ~15s. A ceiling tuned on E9 makes D2 fail16  forever. See feedback_threshold_tuned_on_the_best_case_sample.27RESOLVE_TIMEOUT = 45.0     # generous ON PURPOSE -- see the header note

Mechanism line 27

45 s looks lazy and is not. The passenger whip sits 24 dB down on the driver whip and takes three times as long to resolve. Tune the ceiling on the strong one and the weak one never works again — and it fails in a way that looks like broken hardware rather than a bad constant.

Worth your eye: this is the same trap that got rack written off as dead hardware in August.

3 · The frames, and the one that is mandatory

33# ---- frames, byte-exact from wl.smartled.rgb (Magic Lantern) ------------------34def rgb(r, g, b):  return bytes([0x7E, 0x07, 0x05, 0x03, r & 255, g & 255, b & 255, 0x10, 0xEF])35def power(on):     return bytes([0x7E, 0x04, 0x04, 1 if on else 0, 0x00, 1 if on else 0, 0xFF, 0x00, 0xEF])36def melk_login():  return bytes([0x7E, 0x07, 0x83, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEF])

Mechanism line 33

Line 33 is where the app name has been recorded all along — wl.smartled.rgb. Every frame is byte-exact from the vendor app, not inferred. All three share the 0x7E … 0xEF envelope.

4 · First scar: shipped without the login

84# ⛔ 2026-09-02: THE MELK LOGIN IS MANDATORY AND I SHIPPED WITHOUT IT.85# My first manual run sent login -> power -> red -> green and Ray SAW the whip86# change. send_many() then defaulted login=False, so every UI command after that87# was ACCEPTED at GATT (rc=0) and silently IGNORED by the device. Ray saw nothing.88# ⭐ I made it fast by removing the thing that made it work -- and rc=0 hid it.89# Login belongs on the LINK, not on every write: send it once per new link so the90# warm path stays ~8 ms.

Scar lines 84–90

The manual run worked because it sent the login. The optimisation then defaulted login=False, and from that point every command was accepted at the GATT layer and thrown away by the device. The return code stayed 0 the whole time. Your eyes caught it; nothing in the software could have.

The fix is on line 89 and it is the right shape: login belongs to the link, not to the write, so it costs one round trip per connection and the warm path stays at 8 ms.

Pattern four families, one symptom

Magic Lantern needs melk_login. Grill needs the 2A… knock. Triones and Suparee each need a handshake. Four different unlock rituals, one identical failure: the write succeeds and the light does nothing.

5 · Second scar: a success check that could not fail

119def _write(cpath, payload):120    """⛔ 2026-09-02: busctl PRINTS 'Call failed: Not connected' AND EXITS 0.121    This function used to `return rc == 0` -- a success check that could not fail, in the one122    place it mattered. It reported OK for every write to a disconnected device, all afternoon,123    and Ray's eyes were the only thing that caught it. PARSE THE OUTPUT, NEVER THE EXIT CODE."""128    blob = (r.stdout or "") + (r.stderr or "")129    if r.returncode != 0 or "Call failed" in blob or "Error" in blob:

Scar lines 120–123

“A success check that could not fail, in the one place it mattered.”

busctl prints Call failed: Not connected and then exits 0. Checking rc == 0 therefore returns success for every write to a dead link. Line 129 is the fix: the exit code is still checked, but the output is parsed too, and either one can fail the write.

6 · The part that reflects how these links actually behave

107# ⛔ re-verify the link IMMEDIATELY before writing. "Linked 20 s ago" is not "linked108# now" -- these links do not reliably hold, and a write to a dropped link reports OK.109if not _prop(mac, "Connected"):110    out[k] = (False, "%s LINK DROPPED before write (not connected)" % k); continue

Mechanism lines 107–110

A freshness check immediately before the write, because a link that resolved twenty seconds ago may be gone. Cheap — one property read on the warm path.

This is the line the current question turns on. It treats currently connected as a precondition for sending — and the vendor app has no such precondition. It finds, sends, and lets go. The direction under discussion is subtraction, and this is the code it would subtract from.

7 · The write characteristic does not identify the family

Read across all six modules rather than taking this file alone. The tidy version of this table — one family, one characteristic — is not what the code says.

FamilyServiceWriteNotifyUnlock ritual
magic_lantern—fff3nonemelk_login
elk—fff3nonemelk_login
grillffe0ffe1none2A… knock
trionesffd5ffd9ffd4 in ffd0notify-gated
supareeservice000cffd9—handshake
novaae00ae01ae02none

Two collisions read from the modules, not from notes

Magic Lantern and elk share fff3 and the melk_login unlock — two different vendors, two different sets of hardware, identical transport and identical ritual. Triones and suparee share ffd9 but it lives in different services (ffd5 vs service000c).

So the characteristic alone tells you neither the family nor the protocol. Six families, five characteristics, two collisions.

This also explains the docstring bug. elk_control.py genuinely is this file with a different DEVICES dict — same characteristic, same login, same transport. A header reading “eagle_auto.py” never looked wrong to anyone editing it, because functionally it was not wrong. (Caught by Solace against the module; my first version of this table had elk's unlock as “none”.)

Why it still works suparee_control.py line 19

Because _char() resolves by UUID across the whole tree rather than walking a fixed service path — the module says so explicitly. That single choice, inherited from this file's line 59–66, is what lets suparee's ffd9 be found in a service nobody expected it in.

It is also a latent trap: a tree-wide UUID search on a device exposing fff3 twice would take whichever came first.

8 · What to look at first

For the review

Lines 9–11 — the mechanism, the one thing locked on 08-18.
Line 27 — the timeout, and why the weakest device sets it.
Lines 107–110 — the precondition that no vendor app has.
Lines 120–123 — the check that could not fail.