Commit Graph
3506 Commits
Author SHA1 Message Date
agessaman 17e68ecc45 Merge branch 'webconfig' into observer-firmware-dev 2026-07-19 15:49:40 -07:00
agessaman 25bfac0e93 fix: remove duplicate declarations left by the upstream merge
The 2026-07-19 upstream merge auto-merged two additions into the same class
bodies without conflicting, producing duplicate declarations:

  examples/simple_room_server/MyMesh.h  - getCADEnabled()
  variants/heltec_v4/HeltecV4Board.h    - setLoRaFemLnaEnabled(),
                                          canControlLoRaFemLna(),
                                          isLoRaFemLnaEnabled()

Same class of breakage as the RadioLibWrapper::_cad_enabled and
MyMesh::getCADEnabled() duplicates already fixed in the merge commit. These
survived because the merge was validated with the two prescribed MQTT smoke
builds, and neither of them compiles simple_room_server or the heltec_v4
variant - so CI was the first thing to touch the broken files.

Now verified across ALL 32 observer envs locally, not a sample: 32/32 build.
2026-07-19 15:49:27 -07:00
agessaman 4ff118ce52 fix(prefs): stop spurious VFS error on every prefs save
Saving observer prefs logged an ESP32 error line on every save:

  [E][vfs_api.cpp:182] remove(): /mqtt_prefs.tmp does not exists or is directory

MQTTPrefsFileStore::begin() cleared a stale transaction with an unconditional
remove("/mqtt_prefs.tmp"). On the normal path there is no stale tmp - commit()
renames it away - so the remove always failed and the ESP32 VFS layer logged it
at [E] level. The save itself succeeded; the noise just reads as a fault in the
serial log at exactly the moment an operator is watching a config change.

Guard each remove on exists(), at all four sites: begin() and abort() for both
the /mqtt_prefs and /com_prefs stores. Semantics are unchanged - a genuinely
stale tmp is still cleared, and a failure to clear it still aborts the
transaction - it just stops issuing a syscall that can only fail.

Fixed here on webconfig (the 1.16.0-based line that carries the atomic store)
so it flows to flex with the rest of that work. NOT applicable to
mqtt-bridge-implementation-flex today: flex has no .tmp/rename handling at all,
so the code path does not exist there.

Verified: Heltec_v3_repeater_observer_mqtt builds; hardware confirmation of the
silenced log pending.
2026-07-19 15:45:24 -07:00
agessaman f1390afa71 ci(beta): drop self-referencing env redeclarations
The build step redeclared OTA_MANIFEST_BASE_URL and OTA_CHANNEL_TAG as
${{ env.X }}, referencing the very variables it was setting. Workflow-level
env: is already inherited by every step, so this was redundant; had the
self-reference resolved empty it would have silently blanked the channel and
produced firmware with no manifest base. The verify step would have caught it,
but the risk is unnecessary.
2026-07-19 15:14:26 -07:00
agessaman 8daa86096d ci(beta): trigger on push to observer-firmware-dev
Dispatch-only does not work in this repo. The fork's default branch is `dev`
(an upstream mirror carrying none of the observer workflows), and GitHub only
surfaces workflow_dispatch for workflows present on the DEFAULT branch — so the
beta workflow would never have appeared in the Actions UI and could not have
been run at all.

Adding fork-specific workflows to `dev` would pollute the upstream mirror and
conflict on every upstream sync, so the push trigger is the right mechanism: it
runs from the file on the pushed branch, which is exactly how the production
observer workflow already works.

workflow_dispatch is retained (harmless, and starts working if the default
branch ever changes). paths-ignore mirrors production so docs/CI-only commits
do not rebuild firmware.

Trade-off now explicit in the file: every push to observer-firmware-dev
publishes a dev build. Stage on a side branch and fast-forward when you intend
to release.
2026-07-19 15:07:10 -07:00
agessaman 5f027f1a9c docs: rename branch to observer-firmware-dev; document release channels
The branch was named as a one-off dated merge (merge/upstream-dev-20260719),
but it is actually the standing development line: upstream merges land here and
the dev/beta firmware channel is built from it. Rename accordingly and reframe
the handoff so future upstream merges land ON this branch rather than spawning a
new dated branch each time.

Adds a "Branch and Release Channels" section with the full production vs
dev/beta separation table (branch, workflow, release tag, manifest base,
download host, flasher config, embedded version), and restates why both channels
share FIRMWARE_VERSION: the OTA logic treats a differing base as "always an
update", so channels must separate by manifest URL, never by base version.

Also corrects the beta workflow's dispatch-only rationale, which cited a
short-lived branch name that no longer applies. Dispatch-only still stands, for
the better reason: publishing firmware that real nodes pull over the air should
be explicit, not a side effect of every commit to a dev branch.
2026-07-19 14:52:58 -07:00
agessaman c51e7ea426 ci(beta): tag version as beta-dev and publish config-beta.json
- OTA_CHANNEL_TAG is now 'beta-dev', so the embedded version carries the
  channel AND its provenance: v1.16.0.N-observer-beta-dev-<hash>. This channel
  is built from the upstream-dev-merged line, so 'dev' is visible in `ver`,
  the MQTT firmware_version, and SNMP rather than inferred from a branch name.
  Verified on a real build; OTA version parsing is unaffected.

- config-beta.json is now written into the flasher checkout and committed,
  reversing the earlier ephemeral approach: the flasher SPA loads it directly
  for ?config=config-beta, so it has to be served. It is still DERIVED from
  config.json on every beta build rather than hand-maintained, so the dev/beta
  device list cannot drift from production.
2026-07-19 14:50:23 -07:00
agessaman 5094ece3bf ci: add beta release channel workflow
Publishes a parallel observer firmware channel that cannot cross-contaminate
production. Manual dispatch only, so the branch is chosen in the Actions UI
rather than hardcoded here.

Channel separation (each of these is load-bearing, not cosmetic):
- OTA_MANIFEST_BASE_URL -> beta nodes only ever read beta manifests. This is
  the one that actually keeps devices on-channel.
- Separate RELEASE_TAG: the publish step prunes all but KEEP_BUILDS hashes
  WITHIN its tag, so a shared tag would make each channel delete the other's
  assets.
- Separate build counter: shared counters would interleave and make OTA's
  "N behind" comparison meaningless.
- Separate staticPath via a derived config-beta.json.

FIRMWARE_VERSION deliberately matches production: the OTA logic treats a
different base version as "always an update", so channels must be separated by
manifest URL, not base version. OTA_CHANNEL_TAG marks the embedded version
instead (v1.16.0.N-observer-beta-<hash>) so `ver` identifies the channel.

config-beta.json is derived per build rather than committed - a checked-in copy
would be a 56-entry duplicate of config.json that goes stale as devices are
added. Deriving keeps the beta device list identical by construction.

Two verify steps fail the build rather than publish firmware that would OTA
itself onto production: one checks the beta URL is baked into a binary (and the
production URL is not), one checks the generated manifests use the beta host.

Production's changelog and docs sync steps are omitted - those rewrite site-wide
content the production channel owns. The flasher commit is scoped to the beta
manifest dir and counter for the same reason.

Also adds OTA_CHANNEL_TAG support to build.sh. Safe for OTA version parsing:
ota_parseVersion() reads to the first '-' and ota_extractHash() takes the token
after the last, so an extra tag between them changes neither. Verified on a
real build: v1.16.0.7-observer-beta-36831271.
2026-07-19 14:41:54 -07:00
agessaman 36831271fe build: make OTA_MANIFEST_BASE a build.sh-injected release channel
The observer fetches its manifest from <OTA_MANIFEST_BASE>/<OTA_VARIANT>.json,
so that URL IS the release channel. It was hardcoded to the production channel
in 28 identical places across variants/*/platformio.ini, which made a parallel
(e.g. beta) channel impossible: both channels build the same env names, so beta
devices would read the production manifest and silently flash themselves onto
production firmware.

Inject it from build.sh instead, symmetric with OTA_VARIANT (which no .ini
declares), defaulting to the production URL. Set OTA_MANIFEST_BASE_URL to
publish a parallel channel.

Removed from the .ini files rather than overridden: PLATFORMIO_BUILD_FLAGS
cannot reliably override a -D from build_flags, because SCons reorders -U/-D
and the -U can land after both -Ds, leaving the macro undefined. Verified
empirically before choosing this approach.

A default in a header was deliberately NOT added: leaving both macros undefined
on a plain 'pio run' is what keeps ESP32Board.cpp's 'ERR: OTA not configured
(build via build.sh)' guard firing, so locally built firmware is never OTA-armed.

Verified on Heltec_v3_repeater_observer_mqtt:
- plain 'pio run'      -> OTA disarmed, no manifest URL in the binary
- build.sh (default)   -> https://observer.gessaman.com/v
- OTA_MANIFEST_BASE_URL set -> beta URL only, production URL absent
2026-07-19 14:37:06 -07:00
agessaman 38066a5676 docs: re-baseline handoff after WebConfigBatch wiring and upstream merge
Roadmap status was stale in two ways: Phase 6 still described WebConfigBatch.h
as spec-only/not-wired, and there was no record of the upstream merge.

- Phase 6 / Current Baseline: record that WebConfigBatch is wired and
  load-bearing, document the two deliberate spec/caller asymmetries, and state
  plainly what is NOT verified (the POST/drain/result/reboot sequence over real
  HTTP) so the next person does not mistake green host tests for coverage.
- New "Upstream Merge Record" section: the /com_prefs finding (offset-addressed,
  so NodePrefs reordering is safe - with the warning not to generalize that to
  the versioned /mqtt_prefs), every conflict resolution, the setRxBoostedGain
  signature change, and the cost signal that the fork is the churn source and so
  merges should follow each phase rather than batch.
- Forward plan replaced with the actual remaining work in execution order.
- Merge discipline: note that Git resolved two duplicate declarations silently
  and only the compiler caught them.
2026-07-19 14:03:40 -07:00
agessaman 15e3400a28 merge: upstream/dev into webconfig (v1.16.0 base -> 2026-07-19)
First upstream merge since the 2026-06-06 base (191 upstream commits). 14 files
conflicted; resolutions below.

Fleet-critical check (Constraint 1): upstream reordered NodePrefs members
(rx_boosted_gain / path_hash_mode moved to the struct tail) but did NOT change
/com_prefs. Persistence is written field-by-field at explicit offsets, so member
order is in-memory only. Verified the fork's writeCommonPrefsImage() is
byte-identical to upstream's inline writer at every offset (79 pad, 121, 122,
290-294). No migration needed.

Resolutions:
- CommonCLI.h: kept the fork's NodePrefs (superset) and adopted upstream's
  setRxBoostedGain(bool)->bool signature change, which CommonCLI.cpp now uses to
  report unsupported. Corrected a stale comment claiming rx_boosted_gain lives at
  offset 79 (it is a pad; the field is at 290).
- CommonCLI.cpp: kept the fork's legacy /com_prefs migration and the extracted
  writeCommonPrefsImage() call.
- UITask.cpp: three-way merge - upstream's drawTextCentered + powering-off
  screen, plus the fork's WITH_WEBCONFIG portal/reboot screens.
- ESP32Board.cpp, MeshCore.h, platformio.ini: kept both sides (fork OTA additions
  alongside upstream powerOff/enterDeepSleep and Packet.cpp).
- MicroNMEALocationProvider.h: took upstream's claim/release and added the
  _claims member they depend on.
- MyMesh.cpp/.h (repeater + room server): kept the fork's superset defaults.
- Removed duplicate declarations auto-merge produced: RadioLibWrapper::_cad_enabled
  and MyMesh::getCADEnabled().

Verification: native suite 15/15 (incl. upstream's new test_mesh_tables), both
MQTT smoke builds green, ArduinoJson pin check passes. Hardware validation next.
2026-07-19 12:41:29 -07:00
agessaman cece565e11 refactor(webconfig): wire WebConfigBatch spec into WebConfigServer (Phase 6)
The pure batch/reboot/stop state machine in WebConfigBatch.h was host-tested
but not referenced by production, so the real logic in WebConfigServer.cpp was
untested and the two could drift silently.

Repoint the production decision points at the spec: POST classification and
replay-state naming, drain pacing/all-ok/finish, reboot scheduling and firing,
result classification, confirm-reboot arming, and stop gating. MAX_BATCH and
STOP_WARN_MS now alias kMaxBatch/kStopWarnMs so the constants cannot drift.

Behavior-preserving. Two asymmetries are deliberate and documented in the
header: finishRebootAt()'s 0 return must not be assigned unconditionally
(the manual /api/reboot route also owns _reboot_at and could be cancelled),
and classifyPost() is consulted in two phases because the change count is
only known after parsing, which must not precede the Replay/Busy answer.

Native suite (14 suites) and both MQTT smoke builds green.
2026-07-19 12:35:52 -07:00
agessaman c67d4022ec chore: gitignore worktree build directories
Worktree builds land in .build-wt-<hash>/ (177 MB in the current tree) and
were untracked but not ignored.
2026-07-19 12:35:52 -07:00
agessaman 0e7a07afff Merge remote-tracking branch 'fork/phase6/webconfig-batch-seam' into HEAD
# Conflicts:
#	STABILITY_TESTABILITY_HANDOFF.md
2026-07-19 11:10:17 -07:00
agessaman 4a6fe4e2be fix(mqtt): use ASCII hyphens in serial log strings
UTF-8 em-dashes (U+2014) inside MQTT_DEBUG_PRINTLN / CLI reply strings
render as mojibake ("aEUR"-style, e.g. cooperative stop garbles) on
consoles that don't decode UTF-8. Replace the em-dashes in printed
strings with ASCII '-'. Comments are intentionally left unchanged (they
never reach the console; rewriting them would be needless churn in a
merge-sensitive file).

Verified on V3: the stop message now emits pure ASCII -- zero non-ASCII
bytes in the boot+stop console capture.
2026-07-19 08:02:29 -07:00
agessaman f68a0fb9e7 feat(ota): broadcast OTA start/fail milestones on the alert channel
An operator who triggers `ota update` via remote management only sees the
command's reply; the actual OTA runs ~2.5s later and reboots on success,
so the start/fail notifications land outside the reply window. Mirror the
key OTA milestones onto the configured alert channel (in addition to the
Serial log):

- START ("OTA update starting") from beginDeferredOtaUpdate(), i.e. while
  the loop still runs -- a flood queued at the deferred fire could never
  transmit before the flash blocks the loop / reboots on success.
- FAIL ("OTA aborted: ...") at both abort points (teardown barrier
  withheld flashing; preflight/download error).

Success has no message: a successful flash reboots into the new image, so
the node returning on the new version is the signal.

New MyMesh::otaAlert() gates on the `alert on/off` master switch and rides
the configured alert scope (AlertReporter::sendText -> sendChannel ->
resolveAlertScope); no-op when alerts are off or no channel is set. Only
these start/fail milestones -- routine slot connect/disconnect is
unaffected (stays in AlertReporter's fault logic). Documented in ALERTS.md.
Both observer firmwares build.
2026-07-19 07:51:52 -07:00
agessaman 686a2f240a docs: note V4/PSRAM verification of slot-scaled stop timeout
3-slot PSRAM stop (V4) that force-timed-out at 8s pre-fix now logs
timeout 29000 ms and acks clean in ~16.4s -- confirms the slot-scaled
timeout on both memory paths (V3 non-PSRAM 2-slot + V4 PSRAM 3-slot).
2026-07-19 07:42:31 -07:00
agessaman 7767760fae fix(mqtt): slot-count-aware cooperative-stop timeout
Phase 0 hardware characterization showed real mbedTLS/wss client teardown
takes ~5-6s per connected slot, sequentially, so the flat 8s
MQTT_STOP_TIMEOUT_MS force-timed-out healthy multi-slot stops (2-slot
non-PSRAM ~11-12s, 3-slot PSRAM ~16s), which sets the dirty latch and
makes the OTA barrier withhold flashing -- multi-slot nodes could never
ota update.

Replace the flat constant with a slot-scaled budget computed per stop in
end(): 5s base + 8s per enabled slot (~1.5-2x headroom over measured),
applied via new MQTTLifecycle::Coordinator::setStopTimeoutMs() before
requestStop(). Headroom is nearly free: end() returns as soon as the task
acks (checked before the timeout ticks), so a larger bound only lengthens
the wait before force-killing a genuinely wedged task.

Hardware-verified on V3: a 2-slot stop that force-timed-out at 8s pre-fix
now logs "timeout 21000 ms" and acks clean in ~11.7s. Native suite +
both observer firmware builds green.
2026-07-19 07:34:27 -07:00
agessaman 29a7016e03 docs: record Phase 0/7 hardware characterization results
Hardware run of the outstanding Phase 0 (teardown timing) and Phase 7
(fault-injection) items on V3 (non-PSRAM, 1 wss slot) and V4 (PSRAM,
3 wss slots) observer nodes, both flashed with this branch.

Primary finding (release-gating): MQTT_STOP_TIMEOUT_MS=8000 is too
small. Per-wss-slot teardown is ~5-6s sequential, so healthy multi-slot
stops (2 slots at the non-PSRAM max, 3+ on PSRAM) exceed 8s, trip the
dirty/timeout fallback, and the OTA barrier withholds flashing -- so
multi-slot nodes could never ota update, and the forced path also stalls
the loop task ~15-27s. Recommend raising the timeout (slot-count-aware
preferred, e.g. 4s + 6s*active_slots) before Phase 5 ships.

Phase 7: no heap leak or crash across the representative fault-injection
matrix on either board (incl. repeated forced teardowns); non-PSRAM
reconfigure-churn fragmentation is bounded and fully reboot-recoverable;
PSRAM largest-block rock-stable. OTA barrier latch validated on hardware
in both clean and dirty states.
2026-07-19 07:18:16 -07:00
agessaman 3c170eb1a8 test(webconfig): pure WebConfigBatch state-machine spec + host tests (Phase 6)
The WebConfig POST/result/reboot/stop batch state machine was the largest
remaining Phase 6 coverage gap (all inline in WebConfigServer.cpp, coupled to
AsyncWebServer/ArduinoJson and untestable on host). Extract its decision + timing
CORE into a pure, dependency-free spec mirroring MQTTLifecycle.h:

- src/helpers/WebConfigBatch.h: classifyPost (replay/busy/accept/no-changes with
  the DONE-vs-PENDING reqid asymmetry), drain pacing (signed 25 ms gate, sticky
  all_ok, 30 s reboot fallback), result classification + arm-once 3 s reboot,
  signed-wrap-safe reboot-due / isRebootPending, and stop gating (finalize when
  refs==0, warn-once, never force teardown). Constants verbatim from the source.
- test/test_webconfig_batch/: full host coverage incl. exact boundaries and
  millis() rollover.

Spec-first, exactly like Phase 4's MQTTLifecycle.h: this is NOT yet wired into
WebConfigServer.cpp. That server is hardware-tuned (debugged against real iOS
captive-portal + HTTP-caching + route-ordering behavior), so making the spec
load-bearing is a deliberately separate, hardware-validated follow-up.

Faithfulness independently reviewed against WebConfigServer.cpp; native suite
green (14 dirs). No production behavior change.
2026-07-19 05:31:00 -07:00
agessaman b0cf29fb33 test(mqtt): extract + test remaining inline MQTT decision points (Phase 6)
Close the host-testable gaps named in Phase 6 of STABILITY_TESTABILITY_HANDOFF.md
by moving the last inline decision logic into the pure, host-tested policy seams:

- WiFi STA reconnect backoff: extract the inline ladder + wrap-safe timing from
  handleWiFiConnection() into MQTTConnectionPolicy::{wifiReconnectBackoffMs,
  wifiReconnectDue,nextWifiBackoffAttempt}. Behavior-preserving (elapsedMs is the
  wrap-safe form of the old ULONG_MAX branch); ladder/clamp/attempt-cap unchanged.
- Publication outcome pairing: name the (packet, raw) -> delivered contract as
  MQTTPacketQueuePolicy::queuedPacketPublished() and wire both queue-drain sites;
  partial success = completed, not retried.
- Freeze MQTTPublicationType enum values in a test (the bridge-side MQTTMessageType
  alignment is already enforced by a compile-time static_assert).

Adds host tests for all three (exact boundaries + millis() rollover). Native suite
green (13 dirs); non-PSRAM observer firmware smoke build compiles.

WebConfig batch/reboot/stop state-machine extraction and queue-orchestration
coverage remain open (tracked in the Phase 6 status).
2026-07-19 00:50:15 -07:00
agessaman 2e1a1410ce feat(mqtt): cooperative MQTT shutdown + OTA teardown barrier (Phase 5)
Wire the Phase 4 MQTTLifecycle state machine into MQTTBridge to replace the
blind vTaskDelete teardown that could kill the MQTT task mid-mbedTLS and then
free client buffers on a corrupted heap (the observed OTA teardown panic).

- MQTTBridge owns a MQTTLifecycle::Coordinator driven only by the loop task
  (Core 1) from begin()/end(); a nested LifecycleOps binds the host-tested Ops
  spec to FreeRTOS/PsychicMqttClient.
- end() requests a cooperative stop; the MQTT task (Core 0) tears down its own
  clients where the mbedTLS contexts live, acks via _stop_acked, and
  self-terminates. end() waits (bounded) for the ack, then frees queue/buffers.
- Bounded stop timeout -> reviewed fallback (force kill + Core-1 teardown) sets
  a dirty latch that withholds OTA flashing.
- begin() gains an idempotent double-call guard and syncs the Coordinator to
  Running.
- OTA teardown barrier: simple_repeater's deferred flash aborts/resumes unless
  end() reported a clean stop (canFlashAfterStop()).

Scope: minimal cooperative-shutdown unit. The volatile NTP/reconfigure handshake
replacement and the plain-data snapshot / consumer repointing (MQTT_OWNERSHIP.md
sections 1-3) are deferred. MQTT_STOP_TIMEOUT_MS is a Phase-0 placeholder pending
on-hardware characterization.

Native suite green (incl. test_mqtt_lifecycle); both observer firmware smoke
builds compile. Not yet hardware-validated (Phase 7 gate).
2026-07-19 00:25:37 -07:00
agessaman 9d5bd6f898 feat(mqtt): add Phase 4 lifecycle ownership + teardown test seams
Fork-owned, host-tested MQTT bridge lifecycle state machine and the narrow
dependency seam to drive it deterministically, plus the cross-core ownership
model. This is the Phase 4 "ownership and teardown test seams" safety net that
must land before the Phase 5 cooperative-shutdown refactor.

- src/helpers/MQTTLifecycle.h: pure state machine
  (Stopped->Starting->Running->StopRequested->Stopping->Stopped) + injected
  Ops seam (clock / task / resource owner / OTA barrier) + Coordinator with a
  bounded stop timeout. No Arduino/FreeRTOS/WiFi deps.
- test/test_mqtt_lifecycle/: 18 GoogleTest cases covering the phase's teardown
  matrix (stop during every activity, callback timing, duplicate/early stop,
  restart, timeout fallback, no-access-after-release) and the OTA-barrier
  scenarios.
- MQTT_OWNERSHIP.md: one owner per mutable runtime domain, current cross-core
  hazards with file:line references, target primitives, and Phase-0-pending
  (hardware-characterization) items.
- STABILITY_TESTABILITY_HANDOFF.md: Phase 4 status and verified premise
  refinements.

Scope: MQTTBridge.cpp is intentionally untouched. The production rewiring
(plain-data snapshot publication, volatile-handshake replacement, cooperative
shutdown) is deferred to Phase 5.
2026-07-18 20:31:33 -07:00
Adam Gessaman 1dfd2af56c Merge pull request #29 from agessaman/docs/rebaseline-stability-handoff
docs: re-baseline stability handoff and add stop-and-ask discipline
2026-07-18 19:32:28 -07:00
agessaman db11144916 docs: re-baseline stability handoff and add stop-and-ask discipline
The handoff plan had drifted from the branch. Phases 1-3 are already
implemented, so this updates the plan of record to match reality and adds
a change-control section governing how agents extend the work.

Re-baseline:
- Add a Roadmap Status table and an explicit forward-plan execution order
  (Phase 0 -> 4 -> 5 + OTA barrier -> 6 -> 7).
- Rewrite Current Baseline to include the landed CI, PSRAM buffer symmetry,
  and versioned /mqtt_prefs migration work.
- Mark Phases 1-3 Complete with their residuals (build-size gate + ASan
  pending; filesystem prefs adapter still in CommonCLI).
- Replace Phase 2's now-false premise (buffers are allocated in begin() /
  released in end() / reallocated on restart, per MQTTRuntimeBufferLifecycle).
- Annotate Phases 0/4/5/6/7 with verified status, including the on-demand
  getSlotStatusSnapshot() naming and the begin() double-call leak.
- Reframe the OTA teardown barrier as the fix for a known shipping heap panic.

Add "Change-Control Discipline (Stop-and-Ask)": agents must stop and ask
before implementing out-of-plan refactors or fixing newly discovered bugs,
report with evidence, and keep fixes single-purpose.
2026-07-18 19:26:33 -07:00
agessaman 1be09b9bd6 fix(mqtt): harden /mqtt_prefs migration (atomic durability, tests)
Rework the /mqtt_prefs load/save path so preference migrations are
crash-safe and, for the first time, unit-testable on the host.

Most of this is extraction. The multi-format migration that previously
lived inline in CommonCLI.cpp (and could only run on-device) is moved
into three dependency-free headers so it can be exercised without
Arduino, a filesystem, or the radio stack:

  - MQTTPrefsStorage.h  frozen layout structs for every /mqtt_prefs
                        format ever shipped, with static_asserts that
                        fail the build if any on-flash offset changes.
  - MQTTPrefsCodec.h    pure format classification, field-copy
                        migration, and plausibility validation.
  - MQTTPrefsAtomicStore.h  transactional writer plus the power-cut
                        upgrade gate, both host-testable.

New behavior, beyond the refactor:

  - Atomic writes: /mqtt_prefs is written to /mqtt_prefs.tmp, verified,
    then published with an atomic rename; the writer never removes the
    existing file. A failed or interrupted save leaves the current
    config intact.
  - Power-cut ordering: LegacyUpgradeGate guarantees /mqtt_prefs is
    durably committed before the legacy /com_prefs (or /node_prefs)
    carrying the observer tail is compacted or removed, so an
    interrupted two-file upgrade retries on the next boot without
    losing settings.
  - Corrupt, unsupported-version, and newer-than-known files are
    preserved and the device boots on in-RAM defaults, rather than
    overwriting a file this firmware cannot fully decode.
  - Headerless legacy formats are validated for plausibility before
    they are trusted and rewritten (raw prefs carry no checksum).

The full historical format matrix is migrated forward to the versioned
v1 layout: pre-slot (including pre-wifi-power), 3-slot (base and
token/topic tails), and headerless 6-slot (base, audience, rx, ntp).

Scope note: only /mqtt_prefs and the one-time /node_prefs -> /com_prefs
name migration use the atomic path. Ordinary /com_prefs saves remain a
direct rewrite, unchanged by this commit.

Tests: adds two host GoogleTest suites (pio test -e native).
  - test_mqtt_prefs_codec: format classification, migration fixtures,
    v1 header integrity, downgrade preservation.
  - test_mqtt_prefs_atomic_store: transactional writes, short-write
    detection, begin/finish/rename failure cleanup, original-file
    preservation.
2026-07-18 18:44:37 -07:00
agessaman b5deaf93fc fix(mqtt): restore PSRAM buffers after restart 2026-07-18 16:41:27 -07:00
agessaman 7b60ee704c ci: add MQTT firmware guardrails 2026-07-18 16:39:17 -07:00
agessaman 30278ac5c9 docs: add stability and testability handoff 2026-07-18 16:30:43 -07:00
agessaman 26a8a3cb58 feat(mqtt): refactor message building to use MQTTPayloadBuilder
Replace direct JSON construction in MQTTMessageBuilder with calls to
MQTTPayloadBuilder for building status, packet, and raw messages.
This change improves code maintainability and reduces duplication by
centralizing message formatting logic. Additionally, update platformio.ini
to include ArduinoJson dependency for JSON handling.
2026-07-18 15:47:11 -07:00
agessaman b7b3b4dcf1 feat(mqtt): enhance topic substitution overflow handling
Improve the mqttSubstituteTopic function to ensure that it reports
overflow when the output buffer is full, preventing silent truncation
of topics. Update related tests to verify the new behavior for
literal overflow and exact fit scenarios, enhancing robustness and
correctness of topic handling.
2026-07-18 14:55:21 -07:00
agessaman dc44311e49 feat(webconfig): validate request IDs and improve error handling
Add validation for request IDs in the web configuration server to ensure
they conform to the expected format. Enhance error responses for invalid
or unknown request IDs, improving the robustness of request handling.
Update the web UI to reflect these changes, ensuring that clients can
properly handle errors related to request ID mismatches.
2026-07-18 14:25:56 -07:00
agessaman cbbd01b620 feat(docs): add local testing instructions for MQTT functionality
Include detailed instructions for local testing of observer and WiFi
functionality without hardware. Document the use of a mock backend and
Wokwi ESP32-S3 simulation for easier development and testing.
Enhance the MQTT implementation documentation to improve developer
experience and facilitate testing workflows.
2026-07-18 13:19:27 -07:00
agessaman b7795720b2 feat(webconfig): improve request handling and server teardown logic
Enhance the WebConfigServer to track in-flight requests, ensuring that
the server is only freed once all requests have completed or a hard
timeout has been reached. This prevents crashes due to live connections
during server deletion. Update route handlers to log requests and
manage their lifecycle more effectively, improving stability and
performance of the web configuration portal.
2026-07-18 10:22:34 -07:00
agessaman a7c1cc631f feat(webconfig): add web configuration portal and MQTT enhancements
Introduce a web configuration portal for easier node management and
provisioning without serial CLI. Enhance MQTT functionality with
improved IATA code validation, dynamic slot management, and
background NTP synchronization. Update web UI elements for better
user experience and security notes regarding open AP usage.
2026-07-18 09:34:11 -07:00
ripplebiz 795989b918 Merge pull request #2966 from Quency-D/heltec-rc32
Add Heltec rc32 board
2026-07-17 23:23:29 +10:00
fdlamotte 4aec46b039 Merge pull request #2976 from fdlamotte/UITask_poweroff_radio_display
UI task: poweroff of radio and display moved to board
2026-07-17 08:35:06 -04:00
Florent c644720ea7 uitask: screen and radio poweroff moved to board 2026-07-17 08:29:31 -04:00
fdlamotte 8f5a393404 Merge pull request #2975 from fdlamotte/nrf52_boards--use-shutdownPeripherals-on-TInpulse-and-TEchoBoard
nrf52_variants: override shutdownPeripherals instead of powerOff for …
2026-07-17 08:12:35 -04:00
Florent 84d1e24338 nrf52_variants: override shutdownPeripherals instead of powerOff for TechoLite, TInpulse and TechoBoard (protect the gate) 2026-07-17 07:36:39 -04:00
fdlamotte 3a67685597 Merge pull request #2883 from fdlamotte/repeater_btn
repeater: poweroff on long press
2026-07-17 07:18:44 -04:00
fdlamotte 76c6e73fb6 Merge branch 'dev' into repeater_btn 2026-07-17 06:55:19 -04:00
fdlamotte 3eb5451d3e Merge pull request #2906 from fdlamotte/shutdown_peripherals
Proposal: Introduce shutdownPeripherals in NRF52Board to prepare for shutdown
2026-07-17 06:53:00 -04:00
Quency-D fd7b35f457 Move the rotation input to target.cpp 2026-07-17 15:50:25 +08:00
Quency-D b26f53a263 Avoid waking display from rotary input 2026-07-17 15:35:07 +08:00
ripplebiz 41f4d6bd91 Merge pull request #2560 from Quency-D/heltec-v4-r8
Add heltec-v4-r8 board
2026-07-17 13:54:45 +10:00
agessaman 639c07a414 feat(webconfig): enhance web configuration with flood and loop settings
Add new configuration options for flood traffic management and loop
detection in the web interface. This includes parameters for maximum
flood hops, maximum advert hops, and loop detection modes, improving
the control over mesh network behavior.
2026-07-16 18:35:21 -07:00
agessaman dfee21a0c9 feat(webconfig): implement web configuration portal for ESP32 2026-07-16 17:00:22 -07:00
Quency-D 61a8821d3c Optimize battery voltage reading 2026-07-16 17:45:00 +08:00
Quency-D a472cd7956 Delete duplicate pin definitions 2026-07-16 15:14:17 +08:00