Commit Graph
15 Commits
Author SHA1 Message Date
agessaman 0bfc281a91 docs(handoff): record the 2026-08-03 upstream merge
13 commits, 6 files, zero conflicts. Records the byte-identical smoke-build
baseline, the LR1110 RX-timeout fix and the global nRF52 CC310 crypto switch,
and the fact that the ESP32-only smoke pair could not see either.
2026-08-03 11:28:18 -07:00
agessaman 716f54d4d8 docs(handoff): scope the neighbors pool bug to the dev channel
The starvation needs ArduinoJson v7's fixed 4096-byte pool blocks plus dev's
custom budget-capped allocator. Production is still on v6 (MQTTMessageBuilder
uses createNestedArray, removed in 7.0), where DynamicJsonDocument(10240) is a
real compact slot pool needing ~3.8 KB for 50 entries, so prod is unaffected
and needs no hand-port.

Also notes that prod pins no ArduinoJson version at all.
2026-08-03 10:58:31 -07:00
agessaman 46ee60da90 docs(handoff): record non-PSRAM neighbors hardware verification
Bench run 2026-08-03 on a non-PSRAM ESP32-S3 observer at the 2-wss-slot
non-PSRAM maximum: periodic publish succeeded, min-ever free internal heap
53776 B, payload 1252 B of the 4096 B buffer.

Also records the pre-existing PSRAM pool-budget bug found while verifying,
and the two scenarios still untested on hardware: the >20-neighbour
truncation path, and a publish coinciding with a slot reconnect.
2026-08-03 10:54:43 -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 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 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 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
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 30278ac5c9 docs: add stability and testability handoff 2026-07-18 16:30:43 -07:00