Boards without a charge-complete signal only infer "full" from voltage, and
a real pack rarely reads the full BATT_MAX_MILLIVOLTS (4.2V), so the plug icon
was effectively never shown. Treat "full" as a high band (>= 95%) so the plug
appears when the battery is charged rather than requiring an exact 100%.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Show a small lightning-bolt icon to the left of the battery indicator on
the ui-new home screen while the device is externally powered, and a plug
icon once the battery reads full. The bolt/plug sits beside the battery so
the fill bar stays clean and uninterrupted. When a buzzer is present, the
mute icon shifts one slot further left so the two never overlap.
Charging state is derived from board.isExternalPowered(), so this works on
any board that reports external power (e.g. the nRF52 VBUS-detect path).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The stop timeout bounded only the cooperative attempt. Its fallback tears down
clients from the calling task, and both helpers skipped the stop entirely when
the client reported not-connected — then deleted it anyway. That is the exact
shape of the dangerous case: a client whose DISCONNECTED callback has already
cleared its connected flag while it sits inside esp_mqtt_client_stop() reports
not-connected, so the stop was skipped precisely when it mattered and the
object was freed from under a live ESP-MQTT task.
Both helpers take a force flag, and only the dirty branch passes it. force is
deliberately not gated on connected(), and routes through the existing
forceStop() rather than disconnect(), whose wait for the DISCONNECTED event is
unbounded. Every other caller — the non-ESP32 release path, the Core-0
cooperative teardown, reconfigure, and the slot-cap path — keeps its current
behaviour byte for byte.
This does not make the path bounded. esp_mqtt_client_stop() waits on the
client's STOPPED_BIT with portMAX_DELAY on the pinned framework, so a client
wedged inside mbedTLS can still block the caller. The change trades a
free-under-a-live-task for a wait, which is the safer of the two failure modes.
Bounding it properly, and the surrounding destroy-while-still-stopping hazard,
need the ownership handoff reworked; that is tracked separately.
The preset-application loop in begin() only had a positive branch, so when
preferences named no preset the slot kept whatever the previous begin() left in
RAM. Nothing else resets it: teardownSlot() deliberately preserves enabled and
preset so a reconfigure can reuse the mbedTLS context, and the constructor
clears them exactly once. A bridge restart could therefore resurrect a broker
the operator had disabled and reconnect to it with the old credentials.
Config fields only. The client belongs to destroySlotClients() and the token
buffer to releaseSlotAuthToken(); clearing either here would strand a pointer
esp-mqtt still holds in its config.
Note the positive branch re-enabling a slot is correct and unchanged: a slot
capped off at startup is disabled in RAM only, with preferences still naming a
real preset, and the cap decision has to be re-made on each start.
Reachable via WebConfig, where a pending full restart discards the per-slot
restart mask, so a batch that disables a slot can end in a restart that never
applied the disable. A plain CLI set cannot reach it: restartBridgeSlot()
applies the change to the live bridge immediately, keeping RAM and preferences
in agreement. Verified on hardware as a non-regression check for that reason —
disable, restart, slot stays down; restore, restart, all slots return.
An unconfigured slot has an all-zero secret, so it matches null-key group traffic (a sender with an unset PSK). The zero-key MAC validates against the empty slot and the foreign message is delivered as if it belonged to that channel — every node with a free slot is a null-key sink. Skip empty slots.
ESP-IDF stores the mbedTLS stack error as a positive magnitude (it captures
-ret), so negating it before printing produced "mbedtls:-0xFFFF8100" instead
of "mbedtls:-0x7F00" for the record-buffer allocation failure. %04X is a
minimum width, so nothing masked it.
Normalisation moves into MQTTReplyFormat.h as mbedtlsErrorMagnitude() rather
than staying inline in the bridge: inline is why this survived, since the
existing test passes the already-correct magnitude straight into replyAppendf
and never exercised the caller. It accepts either sign so a later SDK storing
the real negative code still renders, and widens to int64_t before negating
because negating INT32_MIN is undefined behaviour.
MQTTReplyFormat.h also gains the stdint.h it was always missing: it compiled
only because MQTTBridge.cpp pulls stdint in via other headers, and the host
test includes the header standalone.
thinknode_m8: initial support
adds some functionalities to ui_task and epddisplay:
epd display,
- supports screen off, and clear screen
- supports forcing full refresh every N partial refreshes (off by default, configured to 60 for M8)
ui_task
- UI_SHOW_CLOCK flag to display clock on first screen
- rotary button can act like joystick (with regards to Enter key), defaults to old config for existing nodes
- screen off on long press when using rotary button (very handy for the watch)