Every observer build now carries both channel URLs, so checking for a URL's
presence or absence can no longer tell prod from beta (and the old checks would
reject every build). Read the ota-base-* tags from every .bin in out/ instead
of a single ELF, with URLs shared by build.sh and both workflows.
Use a bounded uzlib-derived block encoder with the existing strict tinf decoder, qualify infrastructure roles, budget temporary RAM, and cache running-image metadata. Add self-serve CLI and policy, USB host-loss sleep grace, hardware test harnesses, regression tests, and CI coverage.
Keep Companion and Terminal Chat profiles unchanged; retain full-sensor and reduced-sensor OTA builds. Add compact crypto and OTA names, qualified display asset compression, font license packaging, and scope regression tests.
Keep the graceful final-ACK TCP close and optional FEM control guards from
pixitha's browser compatibility fixes. Replace Brotli negotiation and duplicate
page assets with one gzip representation compressed by Google Zopfli 0.4.3
using 1,000 iterations. Compress the bootstrap, captive success, compact OTA,
and static WiFi setup pages with the same validated build cache.
Honor gzip quality values and wildcard precedence. Validate compressed OTA
page evidence in the firmware capability gate, and add native and Python
regressions plus CI coverage. Remove the unused Brotli build dependency.
Validation: 54 native tests, 56 Python/browser checks, full V4.3 build and
verified application flash. Actual V4 WiFi responses matched the generated
gzip assets; 30 repeat downloads completed without reboot. Chromium loaded
the setup and settings pages without script or transfer errors. Physical
Android testing is pending because the Pi detects no phone over USB or mDNS.
Integrate PowerSaving-v17 through a27e78e4 (six new commits).
Use its 10-minute awake / 30-minute asleep companion GPS profile and
separate requested GPS state from receiver power in CLI diagnostics.
Include its nRF52 sensor debug-console settle delay and debug option.
Resolve overlapping GPS v2 changes using the current sensor callbacks,
which already implement live power-saving transitions, user-off control,
telemetry acquisition, wrap-safe sleep/wake timers and retained Airoha
RTC backup. Preserve preference persistence, telemetry cache/holds and
UART/shared-rail ownership safeguards across all four GPS drivers.
Add a compiled production GPS status regression test and run it in CI.
Validated 23 native GPS/power tests, preference and nRF52 pin contracts,
and firmware builds for T1000-E BLE Companion, MeshTracker X1 USB
Companion, Heltec Tracker Repeater, Mesh Solar Room and RAK4631 Sensor.
Integrate compatible observer board, preset, WebConfig, display, CI, and policy updates. Preserve the checkout's newer MQTT binary preferences and WiFi bridge runtime where the forked implementations conflict. Keep ThinkNode observer networking WiFi-based until the Ethernet transport is wired into that runtime.
Two gaps found while tracing a ThinkNode M7 report of a missing OTA
manifest. The M7 itself was healthy in both channels; these are what the
investigation turned up around it.
Pass --prune to gen-slim-manifests.py in both workflows. The generator only
ever added, so an env that stopped being built kept a manifest pointing at a
release asset the KEEP_BUILDS=2 prune later deleted — a 404 for `ota update`
on any node still running it. The flasher-side commit adds the flag along
with an empty-input guard and --prune-limit, which fails the run rather than
pruning when a build looks like it silently lost envs.
Add "Verify production channel is baked in", which production lacked while
beta has had it from the start. The manifest base is a compile-time -D, so a
build that lost it or picked up the other channel's is invisible until a node
runs `ota check` — and the failure mode is production hardware OTA-ing itself
onto beta. Checked against real published binaries: passes the production M7
build, rejects the beta one.
OTA_MANIFEST_BASE_URL is now stated explicitly in the production env: block,
equal to build.sh's default, so the verify step asserts against the value the
build was actually handed instead of a second hardcoded copy that could drift.
Neither change triggers a build: .github/** is in both workflows' paths-ignore.
The preset table is fleet state, not a build detail. A slot's preset is stored
in /mqtt.json by name, and firmware that does not know a name does not merely
ignore it: MQTTPrefsSerializer repairs it to "none" and CommonCLI writes the
repaired file back to flash. A node that rolls back to a channel missing a
preset therefore loses that slot permanently, and re-upgrading does not bring
it back.
The parity gate compared preset names only, deliberately allowing URL, CA and
credential drift. It now requires src/helpers/MQTTPresets.h to be byte-identical
between the channels, which also catches that drift — the two channels are meant
to dial the same brokers.
That is only workable if the file holds no channel-specific code, so
mqttPresetEnforcesTokenExp() moves to the new MQTTPresetPolicy.h. It was the
sole difference between the two channels' copies, and with it moved they match
exactly today. Policy keyed off the table belongs there from now on; the table
itself stays pure data.
The older name-only comparison stays available without --exact for ad-hoc use,
and the checker's self-test now covers both modes, including that --exact
rejects a config-only change the name check waves through.
Roll back failed password and advert changes, propagate identity-store results, and report unsaved common and observer settings. Correct observer filter persistence and KISS OTA capability claims. Add fault-injection and packaging regressions.
Read identities up to three times before allowing replacement, publish only complete reads, and require new startup keys to be saved with bounded retries.
Use authoritative replay-file metadata throughout persistence and clock recovery. Validate setperm input before narrowing or mutating roles, and add production-path regression coverage.