Skip to content

Observer firmware - #3360

Open
benocd wants to merge 524 commits into
meshcore-dev:mainfrom
benocd:observer-firmware
Open

Observer firmware#3360
benocd wants to merge 524 commits into
meshcore-dev:mainfrom
benocd:observer-firmware

Conversation

@benocd

@benocd benocd commented Sep 5, 2026

Copy link
Copy Markdown

build for Heltec Wireless Stick v2.1

agessaman and others added 30 commits June 23, 2026 12:55
Enhanced the firmware versioning system by appending a build number
suffix when available, allowing for better tracking of published builds.
This change improves the OTA update process by providing clearer
versioning information in the embedded firmware string.
Improved the otaFromManifestImpl method to handle dry run scenarios
by allowing HTTP fetches without TLS, reducing heap usage on no-PSRAM
boards. This change ensures better compatibility and reliability during
OTA checks while maintaining security for actual updates.
Updated the otaFromManifestImpl method to enhance the display of
available and current firmware versions. The changes ensure that
the short commit hash is included in the status messages,
providing clearer versioning information during OTA checks.
Implemented functionality to generate and compare partition-table
signatures during OTA updates. This enhancement ensures that the
target build's partition layout matches the device's actual layout,
improving the reliability of OTA updates and preventing issues
related to partition changes.
Updated the build-observer-firmwares workflow to exclude .partsig files
when uploading release assets. This change prevents issues with the
slim-manifest generator and ensures that only relevant firmware files
are published, improving the integrity of the release process.
Implemented new commands for configuring and diagnosing NTP server
settings in the MQTT bridge. Users can now set a custom NTP server
and probe connectivity to configured servers. This enhancement
improves time synchronization reliability for JWT authentication
and provides better diagnostics for NTP connectivity issues.
Remove unused MQTTMessageBuilder members (getPacketTypeString,
formatTimestamp/Time/Date stubs, JSON_BUFFER_SIZE constant) for a
small flash saving with no behavior change.

Replace the per-byte snprintf("%02X") in bytesToHex with a nibble
lookup table, avoiding a format-string parse up to ~512x per publish
on the MQTT task. Output is byte-for-byte identical uppercase hex.
feat(mqtt): add rflab.io WebSocket broker preset
Improved error handling in the MQTT client to log specific reasons
for connection refusals, including detailed return codes. This change
ensures that users are informed of authentication issues and server
availability problems, enhancing the debugging experience.
Refactored the CommonCLI class to separate observer-related command
handling into CommonCLI_Observer.cpp. This change improves code
organization and maintainability by isolating MQTT, WiFi, and other
observer-specific commands from the main CLI logic.
Refactored the handling of observer-related settings by moving them from
NodePrefs to a new MQTTPrefs structure. This change centralizes MQTT,
WiFi, timezone, SNMP, and alert configurations, improving code organization
and maintainability. The new structure allows for better separation of
concerns and prepares the codebase for future enhancements.
…ation

Updated MQTT configuration to succeed when port is defined in mqttN.server but not explicitly set in mqttN.port. Updated documentation to specify that when a full
URL with a scheme is provided, the port setting is optional. Added an
example for local development using a plain WebSocket URL, enhancing
clarity for users configuring custom MQTT brokers.
CoreComms is a map, analyzer, and mesh health platform created by EastMe.sh
The NodePrefs->MQTTPrefs split (2eb41ba) left the promised one-time
migration of the old /com_prefs trailing block unimplemented, so users
upgrading an observer node silently lost SNMP, radio-watchdog, and
fault-alert configuration (alerts reset to off; PSK/hashtag/region wiped).

loadPrefsInt now detects an old-format /com_prefs by its size, skips the
legacy zero-filled MQTT gap (6-slot or 3-slot era), and recovers the
trailing observer block into a LegacyObserverTail (reusing the old
firmware's byte291/292 heuristic and per-field availability guards).
loadMQTTPrefs applies those values when the loaded /mqtt_prefs predates
the appended observer fields, and both files are rewritten once in the
current layout. rx_boosted_gain/flood_max_* are also recovered from the
correct offsets (previously read from inside the old gap and reset).

Verified with a host-side harness that round-trips the real old-firmware
savePrefs (from 2eb41ba^) through the new load path across upgrade,
fresh-install, upstream-format, 3-slot-era, truncated, and legacy-variant
cases, plus a non-MQTT-build variant. Updates the stale migration comments
and documents the CommonCLI_Observer seam in MQTT_IMPLEMENTATION.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	src/helpers/bridges/MQTTBridge.cpp
The Files section (source layout, upstream integration seams, and
on-device settings migration) is developer-facing detail that sat near
the top of the otherwise user-facing MQTT_IMPLEMENTATION.md. Move it to
a new MQTT_INTERNALS.md and link it from the bottom of the main doc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…l fields

The /mqtt_prefs format was detected purely by struct size, which is fragile
(size collisions across eras) and forced the vestigial `_legacy_*` fields to
be retained forever to preserve byte offsets. With ~several thousand observer
devices deployed from the rolling `observer-mqtt-latest` flasher, this is the
last safe moment to fix it before the observer-settings split ships.

/mqtt_prefs now leads with an 8-byte MQTTPrefsHeader (magic {0xF5,'M','Q','P'},
version, payload_len) followed by the raw payload. The magic's non-ASCII lead
byte cannot collide with a legacy file (whose payload starts with the
mqtt_origin string), so versioned and headerless files are cleanly separable.
An unrecognized (newer) version leaves the file untouched and falls back to
defaults rather than misreading it.

MQTTPrefs is compacted: the six `_legacy_*` fields are removed. Every deployed
headerless layout — pre-slot (OldMQTTPrefs), 3-slot (ThreeSlotMQTTPrefs), and
the shipped 6-slot flex layout (new Legacy6SlotMQTTPrefs) — is field-copied
into the compact struct and re-saved with the header once, on first boot.
Future fields append to the payload and stay backward compatible.

Verified with a host harness that generates a byte-exact deployed flex
/mqtt_prefs (the flex MQTTPrefs is confirmed identical to Legacy6SlotMQTTPrefs,
2904 bytes) and round-trips it through the new load path, plus pre-slot/3-slot
migration, unknown-version safety, magic/origin collision-safety, and the
combined /com_prefs observer-tail recovery. Observer + room-server + no-MQTT
ESP32 targets build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Commit 22eb9b8 ("Revert 'Merge remote-tracking branch origin/dev...'")
reverted an entire upstream merge to escape a bad merge state, dropping
860 lines across 66 files. Among the collateral never reconciled on a
later re-merge was eb4fa03's token-bucket duty-cycle enforcement — the
mechanism that keeps nodes under a configured airtime budget (and EU
868 MHz nodes under the legally-mandated duty cycle). The fork had fallen
back to fixed per-packet spacing (getAirtimeBudgetFactor reverted to 2.0),
losing the windowed enforcement.

This was never an intentional design choice, so restoring it re-aligns the
fork with upstream and REDUCES the merge-conflict surface: Dispatcher.{h,cpp}
now diverge from upstream by watchdog additions only (77 insertions, 0
deletions) instead of rewriting checkSend()/loop().

Restored from upstream: updateTxBudget/tx_budget_ms/duty_cycle_window_ms/
getRemainingTxBudget/getDutyCycleWindowMs and the windowed budget logic in
Dispatcher; getOutboundTotal() and the 0xFFFFFFFF count-all sentinel in
StaticPoolPacketManager; the getOutboundTotal() call in StatsFormatHelper.
Re-applied the fork's MQTT radio-watchdog on top as pure additions
(#ifdef WITH_MQTT_BRIDGE), keeping formatRadioDiag.

Stored airtime_factor settings keep their meaning: fork's t*factor spacing
and upstream's 1/(1+factor) windowed budget yield the same steady-state
duty cycle; upstream additionally allows short bursts within the window.

Phase 2 (CAD / radio_fem_rxgain, which touch NodePrefs persistence and
per-board FEM wiring) is documented in RESTORE_UPSTREAM_NOTES.md, not done
here. Builds: Heltec_v3 observer + plain repeater. NEEDS ON-DEVICE
duty-cycle validation before merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ream parity)

Continues restoring features dropped by the 22eb9b8 revert. Both were
upstream-tested code, not intentional fork removals.

CAD (hardware Channel Activity Detection / listen-before-talk before TX),
fully restored and functional:
- NodePrefs.cad_enabled + `set cad on/off` / `get cad` CLI (default off)
- RadioLibWrapper: _cad_enabled + setCADEnabled() + the scanChannel()/CAD
  branch in isChannelActive() (Phase 1 already restored the Dispatcher hook)
- getCADEnabled() overrides in the repeater/room/sensor apps (return the
  pref) and companion (always on, matching upstream)

radio_fem_rxgain: the NodePrefs field + /com_prefs persistence are restored
here at upstream's exact offsets (293 fem, 294 cad), which makes /com_prefs
byte-identical to upstream through the tail. The field is persisted and
defaults on (upstream default), but the per-board LNA *driving* + the
`radio.fem.rxgain` CLI are deferred to the FEM-hardware change (they depend
on board methods and want per-board bench testing).

The new-format /com_prefs tail grows from 3 to 5 bytes; the old-format
detection threshold and the host migration harness are updated accordingly
(all scenarios pass, incl. the non-MQTT-build variant).

Builds: Heltec_v3 repeater, repeater-observer, room-observer. (sensor /
plain-room / companion fail only on the pre-existing Timezone.h include
issue, unrelated to these changes.)
Completes the FEM RX-gain restoration begun in the CAD/prefs change, which
persisted radio_fem_rxgain but didn't yet drive the hardware. Also dropped
by the 22eb9b8 revert; restored to match upstream.

- MainBoard: setLoRaFemLnaEnabled()/canControlLoRaFemLna()/isLoRaFemLnaEnabled()
  virtuals (default: can't control — non-FEM boards report unsupported)
- heltec_v4: board overrides driving loRaFEMControl; LoRaFEMControl gains the
  isLNAEnabled() getter (it already tracked lna_enabled and drove the FEM)
- CLI: `set radio.fem.rxgain on/off` / `get radio.fem.rxgain` (guarded by
  canControlLoRaFemLna, so it reports "unsupported" on non-FEM boards)
- app startup applies the persisted pref: board.setLoRaFemLnaEnabled(
  _prefs.radio_fem_rxgain), beside setRxBoostedGainMode

Default is ON (upstream), so on FEM boards the LNA is enabled after upgrade —
a real reception behavior change to confirm on hardware. The other FEM
variants (heltec_t096/tower_v2/tracker_v2) need the same small board-override
addition; until then `radio.fem.rxgain` reports unsupported there (no
regression — status quo).

Builds: heltec_v4 repeater-observer + room-observer (FEM board), Heltec_v3
repeater (non-FEM, base virtuals no-op). NEEDS on-device validation on a
Heltec V4.
…ayouts

The unknown-version path kept defaults at boot but any later savePrefs()
(every CLI set command) rewrote /mqtt_prefs as v1 with defaults, destroying
the newer config after a firmware downgrade. Latch _mqtt_prefs_hold when an
unsupported version is seen and refuse to write while it is set — checked
before the NRF52/STM32 open path, which deletes the file first.

Also pin the frozen legacy /mqtt_prefs layouts (472/1464/2904 bytes + 8-byte
header) with static_asserts so every target build re-verifies the deployed
fleet's file offsets, and null-check _obs in AlertReporter::onLoop.
…efs tail size

performChannelScan was restored as protected non-virtual but upstream declares
it public virtual — match upstream verbatim so the hunk disappears from the
merge surface. Also replace the bare 'extra > 5' tail threshold with
COM_PREFS_TAIL_BYTES, tied by comment to the trailing writes in savePrefs(),
so the next upstream field append updates one named constant.
…rrides

companion_radio has its own NodePrefs without radio_fem_rxgain (matching
upstream, which doesn't wire companion either), so the startup call added in
e905451 broke every companion build:
  MyMesh.cpp:973: error: 'struct NodePrefs' has no member named 'radio_fem_rxgain'
It went unnoticed behind the pre-existing companion build failures.

Also port upstream's FEM LNA overrides for heltec_t096 and heltec_tracker_v2
verbatim (board overrides + isLNAEnabled/const getters), so all three boards
upstream wires now match instead of reporting unsupported. Both variant files
compile; those targets still fail overall on the pre-existing non-observer
MQTT-source build-config issue (unchanged with this work stashed).
… the pool

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.
… stale expiry

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.
agessaman and others added 30 commits August 8, 2026 08:40
…fusals

Five findings from review, all confirmed against the source.

Failure classification (P2). Testing replies for an "Err" prefix passed five
other shapes off as success: "Unknown command", "unknown config: x", "??: x",
"Can't find GPS", "(ERR: clock cannot go backwards)" and "File system erase:
Err". They rendered green, and worse, left _batch_all_ok true — so a queued
reboot went ahead after commands that had failed, defeating the gate entirely.

Rather than lengthen one guess, the two questions are now asked separately,
each erring safe:

  - colour asks "does this look like a failure", against every shape CommonCLI
    actually emits, enumerated in WebConfigBatch.h and pinned by a host test
    that uses the literal strings. Getting this wrong is cosmetic.
  - the reboot gate asks something narrower and answerable: "did every setting
    I asked for take". Only `set`/`password` gate it, and only on the "OK"
    prefix every setter keeps. Diagnostics no longer gate a reboot at all, so a
    harmless `memory` cannot strand one and no guess is made about "> value".

Reboot deferral (P2). CommonCLI dispatches on a six-byte prefix, so `reboot
now` and `rebooted` reach Board::reboot() too. Matching exactly meant those
variants skipped both the confirmation and the deferral and took the node down
mid-drain — the precise failure deferral exists to prevent. Both sides now
anchor the way the firmware dispatches, and the UI's risk matcher with them.

Three commands the portal cannot honestly serve are refused at POST with a
reason, and dropped from autocomplete, instead of running and lying:

  - `start ota` builds a second AsyncWebServer on port 80 with no bind check
    and answers "Started" regardless; the portal already holds that port, so it
    could only leak the allocation and inhibit sleep.
  - `clock sync` takes its time from the caller's timestamp, which a web
    request has none of, so CommonCLI always rejected it. `time <epoch>` works
    and remains offered.
  - bare `log` and `get acl` write their real output to Serial and hand back a
    stub the terminal showed as success; `log` also streams a whole file from
    the loop task, stalling the mesh and radio while it does.

The mock now emits the same failure shapes it used to fake as successes, so
these are reproducible off-hardware. 24 batch + 14 keys tests pass; audit
reports 119/119 answered, 0 missing, 4/4 refused with a reason.
The console showed "v1.16.0" — the version was there but buried in the header
line beside role and board, and on the build under test it genuinely had no more
to show: `pio run` never goes through build.sh, so no build number, no commit,
no OTA config.

The banner now prints FIRMWARE_VERSION whole and on its own line. Nothing was
truncating it; build.sh composes base[.build][-observer][-channel]-hash, so a CI
build already carries the published build number as a 4th component and the
commit as the trailing token — the two things that actually identify a build.

It also names the channel, which the version string encodes but does not spell
out (OTA_CHANNEL_TAG=beta-dev -> "-observer-beta-dev-"):

  v1.16.0.5-observer-beta-dev-a1b2c3d   (dev channel)
  v1.16.0.5-observer-beta-a1b2c3d       (beta channel)
  v1.16.0.5-observer-a1b2c3d            (release channel)
  v1.16.0                               (local build — not from CI, OTA not
                                         configured)

That last one earns its wording: build.sh deliberately leaves OTA_MANIFEST_BASE
undefined on local builds so such a node cannot update itself, and nothing about
a bare version number says so.

The mock reports a build.sh-shaped version now (--fw-version switches channel),
and `ver` answers from the same string /api/status does, as both do on-device.
The full embedded string is v1.16.0.5-observer-beta-dev-a1b2c3d. The -observer
tag is on every observer build and the commit is for machines, so neither tells
a person anything at a glance. Both the page header and the console banner now
show base + published build number + channel, paired with the build date the
way `ver` pairs them:

  v1.16.0.5-dev (6 Jun 2026)
  v1.16.0.5 (6 Jun 2026)

`ver` still prints the whole string, commit included, for when that is what you
need. The channel suffix follows the release filenames rather than the embedded
tag — build.sh writes FILENAME_CHANNEL_TAG "-dev" for the same builds it tags
"-observer-beta-dev" internally, so "-dev" is the name these already carry.

Carrying the build date meant /api/status had to report it; WebConfigServer now
takes FIRMWARE_BUILD_DATE alongside FIRMWARE_VERSION, from the same defines
`ver` reads.

A local build has neither build number nor channel to show, so the fact worth
knowing about it moves to the second line: "local build, OTA not configured".
build.sh deliberately leaves OTA_MANIFEST_BASE undefined there, and a bare
version number gives no hint that the node cannot update itself.
…assword

Two findings from review, both real, both mine.

The CLI could read secrets the portal has never exposed. CommonCLI splits its
surface by CALLER, not by command: a serial caller (sender_timestamp 0, physical
access) reads secrets in plaintext, a remote one gets "******** (serial only)".
Its own comments say so — "Serial only (WiFi creds grant LAN access); remote
sees set/unset". execCommand passes 0, which is what makes `erase`, `stats-*`
and `set freq` reachable at all, and with it the terminal inherited the serial
console's plaintext answers for an HTTP request: `get prv.key` returned this
node's identity, `get wifi.pwd` the operator's network.

Worse in setup mode, which authenticates by proximity to an open AP — and `start
webconfig ap` can be run on an already-configured node, so the secrets are real
by then, not blank.

I had reasoned that the AP was the trust boundary either way because the wizard
can already rewrite these. That conflated two capabilities: replacing a WiFi
password does not reveal the current one, and replacing an identity does not
reveal the existing private key. /api/config has always masked these on read
(wcIsSecretKey); the CLI simply broke that rule. Now only the READ is masked —
the command surface stays whole — in CommonCLI's own words, keeping the
set/unset signal that is the useful part.

Onboarding could also skip the mandatory password. handleConfigPost refuses to
arm a reboot during initial setup without one; the CLI only warned in the
browser, which a pasted script or a direct POST ignores, so a node could reboot
onto the LAN still holding the factory credential. Same rule now applies at
POST. It is satisfied by a `password` command anywhere in the session rather
than only in the same request, so the natural two-step console flow still works
— the form batch always sends both together and never needed that memory.

wcIsSecretReadCommand lives in WebConfigKeys.h beside the rest of the secret
classification, pinned by three host tests: what must be masked, what must not,
and that only reads are touched. 17 keys + 24 batch tests pass; the audit checks
a masked read round-trips as masked.
Station G3 observer envs were missing OTA_MANIFEST_BASE, so ota check
reported online OTA unsupported even though CI publishes the manifests.
…manifest-base

fix(station-g3): enable online OTA for observer builds
Keep built-in preset names in parity with observer-firmware after meshcore-dev#41.
Adding build_date to the WebConfigServer constructor broke every
*_room_server_observer_mqtt target: simple_room_server constructs the portal as
well, and only simple_repeater was updated. Nothing caught it because every
build run to that point had been a repeater target.
A worktree checked out inside the repo, caught by a `git add -A` and recorded
as a gitlink. Same problem df07083 fixed for .build-wt-028a5dca, and the same
fix: untrack it and widen the ignore rule, which only covered .build-wt-*.

It is not harmless. The gitlink pointed at a different commit than the worktree
had checked out, so `git status` was permanently dirty — which is how it nearly
got re-committed here, and it makes CI checkouts warn about a submodule path
with no .gitmodules entry.

The worktree directory is gone; branch fix/station-g3-ota-manifest-base and its
commit are untouched. That branch still carries one commit not on prod —
5000391, which adds OTA_MANIFEST_BASE to the station_g3 variant. It belongs on
prod, whose build.sh does not yet inject that flag, and must NOT come to dev,
where build.sh does and an .ini declaration cannot be overridden (SCons reorders
-U/-D) — it would pin dev and beta builds to the production manifest.
LilyGo_TLora_V2_1_1_6 observer builds failed to link:

  undefined reference to `WebConfigServer::MAX_BATCH'
    in handleStatus and handleCliPost

An in-class initialiser is only a declaration under C++11, which is what the
xtensa-esp32 toolchain builds with. Every previous use of MAX_BATCH was a
comparison, which reads the value and needs no symbol. Reporting it as
status.max_cmds, and naming it in the "too many commands" error, passes it to
ArduinoJson — which takes `const T&` — and binding a reference odr-uses it.

It linked on most targets because the compiler folded the reference away, and
failed on the ones where it did not. A cast at the two call sites would have
silenced it just as narrowly; defining the symbols is what stops the next use
from depending on the same luck. MAX_BODY and STOP_WARN_MS get the same
treatment for the same reason, before they are the next to be passed by
reference.
The flasher's /webconfig page frames webui/index.html — the real page this
firmware serves — and runs it against a browser simulator, so visitors drive the
actual product rather than looking at screenshots of it. That only stays true if
the page travels with the docs, so it joins the .md files this workflow already
syncs, and the flasher's build-webconfig-demo.py re-injects the simulator.

Triggers on webui/index.html alongside the existing markdown paths. Still gated
on observer-firmware, so nothing publishes until the portal CLI reaches the
release channel at v1.17.0; the flasher carries a hand-copied page until then.
Picks up upstream MeshCore 1.17.0 and the dev commits that followed it.

Notable upstream content:
- 1.17.0 version/build-date bump in the example MyMesh headers.
- anon_req hardening: reply_path_len is now uint8_t with an isValidPathLen()
  bounds check and a 0xFF sentinel; reply_path_hash_size is gone.
- LR2021 support (Meshnology W12, Seeed MeshTracker X1) incl. side detectors,
  multi-SF and the new `extra.sf` get/set CLI.
- CustomLFS 0.2.2 -> 0.2.3 (GD25Q64C support).
- kiss_modem envs for several nRF52 variants; ThinkNode M6 GPS/flash fixes.

Conflicts resolved:
- platformio.ini: took upstream's CustomLFS 0.2.3, kept our lib_ignore for
  the vendored PsychicMqttClient.
- CommonCLI.h: kept both upstream's USE_LR2021 configSideDetectors() hook and
  our fault-alert callbacks.

NodePrefs gains upstream's extra_sf[4], which is not registered with
ConfigSerializer, so /prefs.json layout is unchanged.

Verified: 275/275 native host tests pass; Heltec_v3_repeater_observer_mqtt,
Heltec_v3_repeater and heltec_v4_repeater_observer_mqtt all build.
Both channels move together: the beta workflow's FIRMWARE_VERSION must equal
production's, since a node compares its embedded base against its own channel's
manifest and a mismatch would read as permanently behind.

Resets the per-base build counter to N=1 on both channels. Nodes still on
v1.16.0.N take the diff_base branch in ESP32Board's OTA comparison ('different
base version is always an update'), so the counter going backwards does not
strand them; the build-number arithmetic only runs when the base matches.

The flasher repo's config.json version key still needs the matching bump.
The planned wholesale dev -> prod merge at upstream MeshCore 1.17.0. Production
now adopts the dev lineage in full; the two release channels stay separated by
workflow, not by branch content.

Resolution: prod's tree is now identical to observer-firmware-dev. Prod's 23
observer commits were sibling hand-ports of features dev implements more fully
(neighbors, max-active-slots, retain policy, MeshRank routing, SNMP, deferred
OTA), so dev's implementation wins throughout. Verified no prod-only content was
lost: MQTT broker presets are at parity (34 both sides), and the sensor_base
build flags that prod added for Heltec/RAK3112/Xiao S3 WIO are all present.

Three files auto-merged into an inconsistent mix and were forced to dev:
- examples/simple_repeater/MyMesh.h: kept BOTH lineages' WITH_MQTT_NEIGHBORS
  blocks, redeclaring neighbor_discover[] and declaring sendAnonRegionsReq()
  twice with different return types. Would not have compiled.
- src/helpers/MQTTDefaults.h: duplicate neighbors default assignments (dev
  already sets them further down).
- .wt-station-g3-prod: stray tracked build worktree, untracked on dev and
  covered by dev's .gitignore '.wt-*/' rule.

Prod's station-g3 OTA hotfix (5000391) hardcoded the production manifest URL
in build_flags; dropped in favour of dev's env, since build.sh already defaults
OTA_MANIFEST_BASE to the production URL when the beta workflow does not override
it. Verified on the built binary.

Channels after this merge:
- observer-firmware      -> v1.17.0, tag observer-mqtt-latest, manifest /v
- observer-firmware-dev  -> v1.17.0, tag observer-mqtt-beta-latest, manifest /beta/v
The beta workflow now also lives on prod but only triggers on the dev branch.
Nine commits reducing the MQTT bridge's internal-DRAM footprint, plus four
fixes that rode with them (invalid path encodings, stale-JWT scan after a
clock correction, setup-retry interval measured from the failure, retried
setup consuming the reconnect allowance).

Touches no prefs surface -- nothing in NodePrefs, MQTTPrefs, or
ConfigSerializer -- so /prefs.json layout is unaffected and there is no
fleet config risk.

Soak evidence: every soak branch already contained this work in full. Device 1
has run it 69.6 h with 325,852 publishes, 0 errors and 0 reboots. The caveat
worth carrying: that long-duration evidence is all on the reduced-TLS
framework (OUT_CONTENT_LEN 4096). Device 3 is now soaking it on the stock
framework, which is where the allocation-ordering interaction with the full
16 KiB record buffers actually gets exercised.
…isplay-117

fix(observer): recover T-Beam Supreme display startup
Documents why the largest allocatable block on a non-PSRAM observer walks down in
16 KiB steps at every TLS reconnect, and what to do about it.

The framework builds mbedTLS with the symmetric buffer configuration
(CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN 16384), so each broker slot holds two 16 KiB
record buffers in internal DRAM and two slots cost 64 KiB on a board with about
80 KiB free. Confirmed against the sdkconfig the Heltec env actually links, not
the IDF 5.3 libs package that belongs to the esp32c6 env.

Rebuilding only the mbedTLS archives with an asymmetric outbound buffer of 4 KiB
is ABI-safe, because in_buf/out_buf are heap pointers sized in ssl_setup() and no
public struct embeds a CONTENT_LEN-sized array. Dropping KEEP_PEER_CERTIFICATE is
not, since it changes mbedtls_ssl_session layout, so it is excluded.

Also records two traps worth not rediscovering: esp32-arduino-lib-builder's
release/v4.4 clones its dependencies at master and no longer resolves, and
pointing platform_packages at a file:// framework copy overwrites the shared
package for every other project on the machine.
Brings in the external FEM gain preferences (fem_txgain, PR meshcore-dev#3137 plus the
companion-side port), the AGC reset rxgain fix, the LR2021 preamble/IRQ
timeout logic, and assorted variant fixes (T096, T-Echo Card TCXO, promicro
pinmap, minewsemi, R1 Neo).

Conflict resolutions:

- SH1106Display: both sides fixed T-Beam Supreme startup independently. Kept
  our _initialized guard and DISPLAY_ADDRESS_ALT override, took upstream's
  SA0-pair fallback and its unconditional display.begin() so the frame buffer
  is allocated even when no panel answers.
- MyMesh/SensorMesh/CommonCLI: took upstream's fem_txgain default and wiring,
  kept our comments and the observer-side prefs layout.

Also fixes CustomLLCC68Wrapper, which upstream missed when sx126xResetAGC
gained its rx_boost_gain parameter. No variant builds that wrapper today, so
neither tree failed to compile.
Picks up upstream MeshCore 1.17.1.

Notable upstream content:
- 1.17.1 version/build-date bump in the example MyMesh headers.
- nRF52: combine radio entropy with CC310 RNG.
- Companion FEM prefs: load/save of fem_ properties commented out until they
  can be set from the client.
- Scoped reply routing: replies no longer dropped when flood.max.unscoped is
  low (RoutingPolicy + unit tests).
- nRF52 unused-pin sweep (T1, T-Echo Lite, MeshPocket).

No conflicts.
Both channels move together so a node comparing its embedded base against
its own channel's manifest does not read as permanently behind.

Resets the per-base build counter to N=1 on both channels. Nodes still on
v1.17.0.N take the diff_base branch in ESP32Board's OTA comparison, so the
counter going backwards does not strand them.
Promotes the v1.17.1 upstream sync and observer-firmware-dev work onto the
production channel.

Conflict in SH1106Display.cpp: kept the observer-firmware-dev begin() that
always runs Adafruit init and falls back across the 0x3C/0x3D SA0 pair,
rather than the earlier production DISPLAY_ADDRESS_ALT-only probe.
The rolling-release body is what the flasher dropdown serves as changelog.
Drop the stale v1.16.0 experimental blurb and describe the current observer
surface: MeshCore 1.17.1, web config, in-channel OTA, and neighbors.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants