espnow_conn_v2.Messages.gen_demi_god builds the
frame, espnow_msg.demigod_gen_* packs the payload, and espnow_conn_v2.Parser._demi_god
receives it.
Who can send one
There is no key and no role check on the wire — “demi-god” is a name for the sender side, not a credential (see security). Four senders exist in the image, and only the first is reachable in normal use. All rows Confirmed.Earlier versions of this page said the sender was a gated “demi daemon” role with a
demi_daemon_send API, and that send_pwr_off_cmd was how power-off was issued. Both are
wrong. demi_daemon_send is an LED task name (compass.dis:3950), enable_demi_daemon
is dead code behind a hardcoded False, send_pwr_off_cmd has no caller, and demi_god.py
itself contains only send_msg, manager and start — its qstr table has no
is_demi_daemon, enable_demi_daemon or demi_daemon_send at all. In 5.0.3 the phone’s
“update nearby devices” button is the one demi-god command a user can actually cause.manager requires a command id (A cmd_id must be provided for demi-god messages), turns
the LEDs over to itself (evt_led_override_off.clear(), crystal hot pink), optionally
resets the radio and re-adds FF:FF:FF:FF:FF:FF as an ESP-NOW peer, then broadcasts and
restores the LEDs. The BLE path passes reset_comms=False, so it skips the radio reset.
Confirmed (demi_god.dis, manager / send_msg).
Frame layout
gen_demi_god allocates bytearray(4) and writes SyncWord A7 74 + cat_id = 1 +
the caller’s cmd_id, then calls one espnow_msg.demigod_gen_* packer. Each packer first
extends the buffer with zero bytes and then struct.pack_intos from frame offset 4, so
the frame a Totem transmits is longer than the receiver’s TOTEM_MSG_MAP entry — the
tail is zero padding (and, for (1,0), variable-length strings). All Confirmed from
espnow_msg.dis.
The receiver’s format is unpacked from frame[2:], so its leading BB is the echoed
(cat_id, cmd_id) and a calcsize of n covers frame bytes 2 through n+1.
The
EXTENDED keys are total frame lengths, not schema ids: Parser.aread looks up
EXTENDED[(1,6)][len(frame)], which is why a (1,6) frame is decoded with the long format
only when it is exactly 29 bytes. An earlier version of this page called 29 and 5 “schema
ids”; they are byte counts.The other EXTENDED key for (1,6) is 5, and it is unreachable. Every other key in the
table is calcsize(fmt) + 2 (25→23, 59→57, 72→69, 29→27, 45→43), but <BBbbB also has
calcsize 5, so the short form would need a 7-byte frame; a 5-byte one makes
struct.unpack raise. demigod_gen_pwr_control always builds 29 bytes, so no Totem emits
either length. Confirmed from the EXTENDED literal (espnow_conn_v2.dis:916-923).(1,0) firmware update
buff.extend(bytes(12)), then struct.pack_into('<B7bHbb', buff, 4, …), then the four
strings back to back from offset 16, then an optional endpoint_id byte appended last.
Confirmed (espnow_msg.dis, demigod_gen_ota_update).
security_algo == 1 means the SSID and key were passed through f_lib.helpers.rot13
before packing, and the receiver runs convert_to_str(..., security_algo=frame[4]) to undo
it. Branch and version are always plain. ROT13 is a substitution, not encryption — the
credentials are recoverable from the frame by anyone. Confirmed (espnow_msg.dis
demigod_gen_ota_update; espnow_conn_v2.dis:8408-8480 _demi_god).
(1,2) navigation-logging rule
<iffHii at offset 4: rule_id, latitude, longitude, radius (H), nbt, eat
(both i). The packer returns False — and gen_demi_god then returns False — when
latitude, longitude, radius, nbt and eat are all falsy. The receiver does nothing
with this command: Parser._demi_god’s cmd == 2 branch jumps straight to the end of the
function. Confirmed (espnow_conn_v2.dis:8613-8617).
(1,5) find device
buff.extend(bytes(25)), then <6B at 4 (target MAC) and <3B8bhh4B at 10.
Confirmed (espnow_msg.dis, demigod_gen_find_device).
This is a filtered broadcast, not an address: every receiver tests itself against the
filters and, if it matches, blinks all its LEDs in the given colour for
duration_sec
(tasks.launch(blink_all_leds, task_name='demi_god_blink', …)). A device that fails any
filter logs Device disqualified from demigod command.
(1,6) power control
buff.extend(bytes(25)), then <bb5B at 4, <6B at 11 and <3i at 17. Confirmed
(espnow_msg.dis, demigod_gen_pwr_control).
boot_mode reaches Compass.power_control, which maps 1 → device_reboot(),
2 → device_reboot(is_soft_reboot=True), 3 → device_off() and ignores everything else
(compass.dis, power_control; log Power Control | Desired boot_mode: {}). Confirmed.
An earlier version of this page listed “power-mode change
(
Changing power mode from {} to: {})” as a (1,6) effect. That log belongs to
Compass.change_power_mode, the local eco/normal toggle, which nothing in the demi-god path
calls. (1,6) only reboots, soft-reboots or powers off.(1,7) venue code
buff.extend(bytes(8)), then <Bbb at offset 4: venue_code (B, default 0),
rssi_min (b, default −100), rssi_max (b, default 0). The remaining five bytes stay
zero. Confirmed (espnow_msg.dis, demigod_gen_venu_code).
The receiver stores config.venue_code and sets the device’s colour from it:
1 → color_id 0, 2 → 1, 3 → 10, 4 → 4, anything else → 12, then
tasks.launch(compassing.next_color). A frame carrying the code the device already has is
a no-op. Confirmed (espnow_conn_v2.dis:9007-9120).
An earlier version of this page said the venue code “scopes subsequent mesh / smart-group
behaviour”. It does not: the only readers of
config.venue_code in the image are this
handler and nav_logger, which records it alongside a log entry. Its visible effect on the
mesh is none; its visible effect on the device is the LED colour.What a receiver checks
Parser._demi_god gates per command — there is no gate shared by all of category 1.
All rows Confirmed from espnow_conn_v2.dis:8302-9122.
The RSSI floor is carried in the frame, chosen by the sender. It is not a fixed
threshold on the receiver: the dead
send_pwr_off_cmd and the dead SOS-switch handlers all
send −99, which any device that can hear the frame at all will pass. The one live sender,
the phone’s (2,5) button, sends −30.
evt_demigod_rx_done is not a network acknowledgement. (1,6) awaits it, clears it,
performs the action and sets it again — it is a local re-entrancy latch so two power
commands cannot overlap. evt_demigod_tx_done is the same latch on the sender side.
Confirmed (espnow_conn_v2.dis:8947-9005; compass.dis, daemon_hard_reset).
Transmit back-pressure (v5.0.3)
demi_god.py is byte-identical between v5.0.2 and v5.0.3, but the transport under it
changed: send_now (reached via enow_v2.broadcast → send) drops a broadcast locally,
counting it as tx_skipped, when _tx_free() is ≤ 0 — that is, within 2 s of an
ESP_ERR_ESPNOW_NO_MEM refusal or when 32 − 4 − pending_tx() is not positive. Each of the
send_count repetitions is checked on its own, so individual repetitions can be skipped
under load. Confirmed (espnow_conn_v2.dis, _tx_free / send_now).
Security considerations
- The link is unencrypted and unauthenticated. No PMK or LMK is ever programmed
(see ESP-NOW mesh), and the
application payload carries no signature, HMAC, nonce, challenge or token. The SyncWord
A7 74is shared by every ESP-NOW frame the firmware sends — a framing marker, not a secret. - Validation is SyncWord +
len ≥ 4inEspConn.recv, then a(cat, cmd)lookup and astruct.unpackover an exactly-sized slice inParser.aread— a minimum-length check, not an integrity check. There is no CRC or checksum on the frame. (Invalid Checksum value for: {}belongs to the u-blox UBX GNSS parser, not this path.) - The visible safeguards are the frame’s own RSSI window, the
(1,0)duplicate suppression and the(1,0)upright-device exemption. None of them identifies the sender, and the RSSI window is a sender-chosen field, so it constrains nothing an author does not want constrained. (1,0)can carry a WiFi SSID and key in the clear, optionally ROT13’d (security_algo = 1), andsave_network = 1makes the receiver write them touser-config.json. This is not hypothetical: it is exactly what the phone’s “update nearby devices” button sends — the owner’s own home WiFi credentials, ROT13’d, broadcast to every Totem within −30 dBm, with instructions to save them. ROT13 is a fixed letter rotation; it hides the credentials from nobody.(1,5)lets a sender select devices by boot count, failed-boot count and peer count, so a frame can be aimed at, for example, every device in range that has ever failed a boot — which is exactly whatdaemon_hw_errorsdoes.