Without ROOM_PASSWORD the guest password defaults to empty, and a client
sending a blank password is granted read+write access (can post). Every
other board's room_server env sets ROOM_PASSWORD; the ThinkNode M6 and M1
were the only two shipping an open room. Default them to "hello" to match
the rest of the tree; operators can still change it at runtime with
'set guest.password'.
next_check and next_gps_update stored a future millis() value in a signed
long and compared with a naive '>'. After the ~24.8-day millis() sign flip
the deadline sits above the wrapped millis(), so the block never runs again
and GPS->RTC time-sync (and the location cache refresh) stall permanently
until reboot. Switch to unsigned deadlines with the wrap-safe signed-
difference compare '(long)(millis() - deadline) > 0', matching the idiom in
Dispatcher::millisHasNowPassed.
Also: reorder the MicroNMEALocationProvider ctor init-list to declaration
order (silences -Wreorder) and drop the always-true 'if (_claims > 0)' guard
in claim() (claim() always runs after _claims++, so it is >= 1).
The M6 is the only board whose simple_repeater env ships with MESH_DEBUG=1
and GPS_NMEA_DEBUG=1 enabled. MESH_DEBUG=1 adds a 5-second boot delay
(examples/simple_repeater/main.cpp) plus verbose per-packet serial prints on
the hot RX/TX path; GPS_NMEA_DEBUG=1 echoes every GPS UART character to the
serial console (MicroNMEALocationProvider.h). Every sibling repeater env
(M1, M3, t1000-e, RAK, Heltec) ships these off, and the M6 room_server env
in this same file already has them commented. Comment them out to match.
External watchdog support was added (but never merged) for MeshTower V1. The update's V2's #defines to follow the conventions set there and piggy back off the support already written for V1.
The host-side build/verify/inspect/serve CLI now lives as a standalone Rust
project at https://github.com/vk496/motatool. Remove the in-tree C++ copy
(tools/motatool/) and repoint the docs (ota_protocol.md, ota_user_guide.md,
tools/mota/README.md) at the standalone repo.
No cross-dependency either direction: MeshCore's firmware build never invoked
motatool (only tools/mota/ Python glue runs in the build), and motatool depends
only on the shared .mota wire spec in docs/ota_protocol.md. tools/mota/
(pio_endf.py EndF hook, motalib.py reference lib, gen_targets.py) is unchanged.
The T096 is an nRF52840 with the same S140 v6 SoftDevice and flash geometry as
the RAK4631, so it can reuse the shared rak4631_hw OTA recipe (ENABLE_OTA +
OtaFlashLayout_nrf52.h — APP_BASE 0x26000, staging ceiling 0xD4000, all
static_asserts satisfied by the stock nrf52840_s140_v6[_extrafs] ldscripts).
With the OTAFIX bootloader (oltaco PR #42 adds the T096 board) a node applies
in place; without it the node still discovers/relays/serves OTA and merely
fetch-refuses apply — so a T096 is a useful OTA relay/seeder regardless.
MOTA_HW_ID='"Heltec_t096"' tags the EndF trailer so a .mota built for other
hardware is refused. Propagates to every T096 role via ${Heltec_t096.*}.
Verified: repeater + companion_radio_usb build clean, 14 OTA objects each,
EndF stamped (hw='Heltec_t096', v1.17.0).
Second-channel pipeline for mqtt-observer-plus, mirroring
build-observer-firmwares.yml with full channel isolation:
- rolling tag observer-mqtt-plus-latest (never shared: per-tag asset pruning
would cross-delete on a shared tag)
- FIRMWARE_VERSION v1.16.0-plus, separate build counter
- OTA_MANIFEST_BASE overridden to /v-plus at build time so device pull-OTA
can never hop a plus node back onto the stable channel (env build flags
append after ini flags, so the override wins — verified in the binary)
Not enabled: workflow_dispatch only (push trigger staged in comments), and
the flasher-repo sync is gated behind SYNC_FLASHER=false with a fail-closed
guard so a premature enable cannot clobber the stable config.json. The
header documents the flasher-side prerequisites (channel-aware
update-firmware.py, SPA channel picker, v-plus manifests, plus changelog).
Manual dispatch already works for publishing hand-flashable test builds to
the release page.
The observer firmware workflow depends on three build.sh behaviors that the
keymindCascade merge dropped:
- embedded-version tagging: observer builds report
v<base>[.<FIRMWARE_BUILD_NUMBER>]-observer-<hash> via ver/MQTT/SNMP while
asset filenames stay <env>-v<base>-<hash>
- -DOTA_VARIANT injection (selects the slim per-variant manifest for pull-OTA)
- per-env .partsig emission (OTA partition-compatibility gate)
Re-applied inside the keymind build_firmware/collect_esp32_artifacts
structure. Verified by building Heltec_v3_repeater_observer_mqtt with the CI
flags set and checking the embedded version/URL strings in firmware.bin.
Integrates Mike Carper's transmission-reliability work (direct retries with
SNR-adaptive backoff and CR, flood retry controls, alt-path replies, flood
channel gates) plus its upstream/dev base (as of 2026-07-06) on top of the
observer stack. The flex branch is untouched; this branch is the experimental
integration line.
Conflict-resolution decisions, for future re-syncs (git rerere is enabled and
has recorded these):
- NodePrefs adopts keymind/upstream member order + retry/flood tail. Member
order is in-memory only: /com_prefs stays field-by-field with the same
canonical file order both sides already share through offset 294; keymind
appends the retry tail at 295+ (new canonical size 676). Flex fleet files
load unchanged; retry fields default via direct_retry_prefs_magic.
- loadPrefsInt keeps the fork's legacy MQTT-gap recovery but moves its
detection boundary from 'extra > 5' to 'extra > LEGACY_MQTT_GAP_3SLOT (864)'
so keymind-size tails take the normal read path. COM_PREFS_TAIL_BYTES
retired.
- Dispatcher/RadioLibWrappers: fork watchdog additions + keymind TX overrides
and CAD busy counter are unioned; RxReservePacketManager::queueOutbound
follows upstream's new bool return (false when shedding).
- CommonCLI: fork observer dispatch + versioned /mqtt_prefs machinery kept;
keymind CLI (retry/flood/radioat) taken; duplicate CAD/FEM handlers and
sanitise lines deduped (kept keymind placements to minimize future diff).
- MyMesh (repeater/room): fork alerter/OTA/observer wiring kept; keymind
scheduled-radio system replaces the old pending_* temp-radio members;
applySavedRadioParams() replaces raw setParams at startup.
- ESP32Board: fork manifest-OTA + keymind powerOff/deepSleep and stopOTAUpdate
unioned; startOTAUpdate keeps fork's STA-aware IP with keymind's idempotency
guard.
- build.sh taken wholesale from keymind (fleet builds use GH Actions).
Verified: Heltec_v3_repeater_observer_mqtt, Heltec_v3_repeater,
heltec_v4_repeater_observer_mqtt, Heltec_v3_room_server_observer_mqtt build;
host migration harness (13 MQTT + non-MQTT scenarios) passes with updated
expectations (676-byte /com_prefs; odd-size files normalize on save, not
load); native unit tests 13/13.
Merges three stacked PRs in one push so the rolling release only builds
the final state:
#20 — Complete observer settings migration and upstream-seam cleanup
- Implement the missing one-time /com_prefs -> /mqtt_prefs observer
settings migration (silent data loss on upgrade before this).
- Harden /mqtt_prefs into a versioned format (magic + version +
payload_len header); legacy headerless layouts detected by size and
migrated once. Drop vestigial _legacy_* fields.
- Deployed flex layout pinned by static_assert (Legacy6SlotMQTTPrefs ==
2904 bytes); verified with the host migration harness and on-device
(Heltec V4: presets, WiFi, and origin survive upgrade and reboot).
#21 — Restore upstream duty-cycle enforcement and CAD
- Restore Dispatcher/StaticPoolPacketManager verbatim from upstream/dev
(token-bucket duty cycle, CAD interface); re-apply the MQTT radio
watchdog as pure additions behind WITH_MQTT_BRIDGE.
- Restore cad_enabled and radio_fem_rxgain persistence at upstream's
/com_prefs offsets (byte-parity with upstream, tail 3 -> 5 bytes).
- Add RxReservePacketManager for observer builds: priority-aware shed +
30s stale expiry so heavy throttling can't park the packet pool and
make the node un-administrable.
- Device-confirmed on Heltec V4.2: throttling holds at set dutycycle 1
on a busy mesh, CAD persists across boots, capture continues at full
rate under throttle.
#22 — Drive LoRa FEM RX gain from radio_fem_rxgain
- MainBoard gains setLoRaFemLnaEnabled/canControlLoRaFemLna/
isLoRaFemLnaEnabled; wired for heltec_v4, heltec_t096, and
tracker_v2. radio.fem.rxgain CLI gated on board capability.
- Persisted pref applied at app startup; defaults ON (upstream parity),
so LNA-capable boards gain RX boost on upgrade.
- Guard path device-confirmed on V4.2 (GC1109, PA-only: reports
unsupported). LNA toggle on V4.3/KCT8103L pending hardware.
Multi-hour soak on the full stack: no memory issues or regressions.
Duty-cycle throttling, CAD-under-load, capture decoupling, remote admin under
throttle, and radio-watchdog fire/recover all device-confirmed on a Heltec
V4.2 (busy mesh + off-frequency bench). Notes the watchdog observability
gotchas found during testing: silent on release builds (check stats-radio-diag
err_flags bit 8) and armed only after first radio activity.
Device testing at 'set dutycycle 1' on a busy mesh showed the node becoming
un-administrable within ~2 minutes: the shed policy dropped its own CLI
responses along with repeats, and parked retransmissions (which never expire)
absorbed every budget refill.
RxReservePacketManager now sheds by priority below the RX reserve — only
pri > 1 outbound (multi-hop flood repeats, adverts, trace) is refused, so the
node's own responses/ACKs (pri 0) and login/PATH replies (pri 1) still queue;
below an emergency floor (reserve/2) everything is shed to protect capture.
Queued packets untransmitted 30 s past their scheduled time are expired at
dequeue via a pointer-keyed age table (the pool is a fixed set of packets, so
pool_size slots cover every key). Under normal load the queue drains in
milliseconds and neither policy triggers.
Load-testing the restored token bucket at 'set dutycycle 1' showed MQTT
capture dropping to exactly the TX rate. Queued retransmissions hold static-
pool packets with no expiry, so throttling parks the whole pool in the send
queue; Dispatcher::checkRecv() then discards received packets before logRx()
ever feeds the bridge — each completed TX frees exactly one packet for
exactly one more RX.
Observer builds now use RxReservePacketManager (fork-owned header): once the
free pool drops below a quarter of the pool, outbound packets are refused and
freed, so RX allocation and MQTT capture continue at full rate while the node
sheds repeat load it has no TX budget for anyway. Non-observer builds keep
upstream pool behavior via the same factory; StaticPoolPacketManager stays
byte-identical to upstream.