Commit Graph
1515 Commits
Author SHA1 Message Date
agessaman b1ceaf01a8 fix(mqtt): require real SNTP completion before crediting a fallback server
The fallback configured a server, waited 500 ms, and accepted any plausible
system clock as proof that server had answered. It usually has not answered
that fast — and the device usually already holds valid time, from an earlier
sync or the RTC — so the first server in the list was credited unconditionally,
the walk stopped there, _last_ntp_sync was refreshed, and an unreachable host
was logged as the source. On the `set mqtt.ntp` validation path, where the
single-server walk exists so a typo fails fast, that reported a bad server as OK.

Poll sntp_get_sync_status() for SNTP_SYNC_STATUS_COMPLETED instead, which is the
layer's own statement that a packet arrived. The status is one-shot — reading
COMPLETED clears it — so a result left by an earlier sync would latch on the
first poll; clear it before the loop.

An implausible epoch after a completed sync now moves to the next server rather
than spinning out the remaining attempts against a server that has answered.
2026-08-14 19:40:48 -07:00
agessaman 2173794966 fix(mqtt): reconnect the NTP-corrected slot instead of no-opping on a live client
esp_mqtt_client_reconnect() is honoured only from MQTT_STATE_WAIT_RECONNECT, so
the post-correction path minted a fresh token, staged it, and then asked a
connected client to reconnect — a request esp-mqtt refuses. The slot kept running
on the token the clock correction had just proven stale, and recovery became
broker-driven rather than the clean reconnect this code intends.

Split the three states the path can find: a stopped or waiting client goes through
reconnectSlotClient() as before, and a live one has its transport closed first.
Only where the broker enforces exp, though — waev leaves live sessions alone past
expiry, so bouncing it would spend the 16 KiB contiguous handshake that the rest of
this branch exists to avoid.

Not a regression: the base branch called client->reconnect() directly at the same
site. On ESP32 the block is reachable from the WiFi-reconnect resync and the CLI
forced sync; the hourly refresh uses refreshNTP(), which does not carry it.
2026-08-14 18:41:20 -07:00
agessaman 6ffadd6e72 fix(mqtt): route both reconnect ladders through the stopped-client guard
reconnectSlotClient() checks isStarted() and calls connect() instead of
reconnect() when the client was stopped, but only the post-NTP stale-token
path used it. The ordinary backoff ladder and the circuit-breaker probe called
slot.client->reconnect() directly, and esp_mqtt_client_reconnect() is a no-op
on a client that is not started.

Two ways in. connect() sets _started only when esp_mqtt_client_start() returns
ESP_OK while setupSlot() sets initial_connect_done unconditionally, so a start
failure under heap pressure stranded the slot. More routinely, the WiFi-drop
handler calls disconnect() on every connected slot, which clears _started —
after that the ladder issued no-ops forever and the slot never came back.

Not caught by the soaks: the log line the guard prints can only come from the
NTP path, so a stranded slot and a slot that never entered the state produce
identical logs. Observed reconnects were broker-side drops with WiFi up, which
leave the client started.

The renewal-bounce path keeps its own isStarted() branch — it needs
softDisconnect(), which the helper does not do.
2026-08-14 09:49:16 -07:00
agessaman c0c823b6b0 fix(mqtt): reuse a still-valid JWT on ordinary reconnects
Every ordinary backoff reconnect and every circuit-breaker probe minted a
fresh JWT and re-applied credentials, with no check of whether the existing
token was still valid. setCredentials() always dirties the esp-mqtt config, so
reconnect() then called esp_mqtt_set_config() as well. On a flapping broker
that is a signing plus a configuration-copy cycle on every retry, and these
observers see ~38 genuine reconnects/day per slot.

The no-bounce renewal change (27bd05a1) only stopped the proactive renewal
from tearing down a live session; it left this retry path untouched, which is
why a soak shows renewals neither firing nor failing for hours while drops
continue — each reconnect silently re-mints and pushes the expiry out.

Reuse the credentials when their validity is provable and refresh them
otherwise. canReuseJwtForReconnect() lives with the other policy predicates so
it is host-testable, and it establishes current_time < token_expires_at before
subtracting: token_expires_at is unsigned, so an already-expired token would
otherwise wrap to ~4e9 seconds and read as valid for decades. The
>= kMinimumValidEpoch term also rejects the 0 that a failed renewal writes.

Minting stays the default for every uncertain case — unsynced clock, missing or
insane expiry, empty token, or an expiry inside kJwtReconnectSafetyMarginSecs
(60 s), which covers the handshake itself.

Two paths still always mint, deliberately:

  - The circuit-breaker probe. It is the recovery of last resort for a slot
    that has already failed repeatedly, quite possibly on auth, and it runs
    once per 30 minutes — so a fresh token there costs nothing worth counting
    against keeping that path guaranteed-clean.
  - Any slot whose last error was a broker refusal. Before this change, minting
    on every retry accidentally recovered from server-side credential
    invalidation: key rotation, revocation, broker clock skew, or an audience
    change after a reconfigure. Reuse would have retried a rejected credential
    until it neared expiry — up to 24 h for every preset that leaves
    token_lifetime at the default. onError already detects
    MQTT_ERROR_TYPE_CONNECTION_REFUSED and only logged it; it now also sets a
    per-slot force-mint flag, cleared on a successful connect and wherever the
    credentials it referred to are blanked. The flag is volatile because the
    esp-mqtt callback sets it and the bridge loop consumes it.

The reconnect log line reports the decision and its outcome — REUSE, MINT with
a reason, and OK/FAILED for the mint — because a silently failed mint is the
case most likely to end in an auth refusal. It never prints the token.

Host tests cover the reuse boundary: exact margin, already-expired, expiry 0,
sub-epoch expiry, empty token, unsynced clock, and the force-mint override.
2026-08-14 09:47:40 -07:00
agessaman 88c824c175 docs(mqtt): correct what the WS buffer padding actually fixes
The comment claimed the (WS_BUFFER_SIZE + 1) padding makes an oversized
upgrade response "fail cleanly (Upgrade header not found)". Reading
release/v4.4 transport_ws.c against release/v5.3 shows it does not.

v4.4's response loop is

    } while (NULL == strstr(ws->buffer, "\r\n\r\n") && header_len < WS_BUFFER_SIZE);

so it also exits when the buffer fills without the terminator, and the code
then looks for "Sec-WebSocket-Accept:" and returns 0 if it is present. That
header appears early in a response, so an oversized header block yields a
BOGUS SUCCESS rather than a clean failure: the unread remainder stays queued
on the socket and is delivered as the first post-upgrade read, where the
deframer parses HTTP bytes as a WebSocket frame header.

Observed on hardware 2026-08-12 on a Heltec V4: a Cloudflare Page Shield CSP
report-uri header pushed the 101 response past the buffer, and the tail of
that header ("csp-reporting.cloudflare.com/cdn-cgi/script_monitor/report?")
reached the MQTT layer as payload, surfacing as
"Invalid MSG_TYPE response: 3" (0x35 = '5', high nibble 3).

The padding's real and only value is preventing the one-byte overflow of the
heap canary, which is still worth having. Narrow the comment to that claim and
record where the parser fix has to come from: IDF 5.2+ requires the "\r\n\r\n"
delimiter, memmoves the bytes following it, and fails cleanly when the buffer
fills. It cannot be patched here, since transport_ws.c ships precompiled in
libtcp_transport.a on Arduino 2.x.

No functional change.

(cherry picked from commit 9894e65e8ad961704d430a12a065baffda353f50)
2026-08-14 09:47:40 -07:00
agessaman f4ba55be7a fix(mqtt): stop bouncing a live waev session to renew its token
waev's operator confirmed on 2026-08-11 that their servers do not disconnect a
client when its JWT passes exp — a 60-minute token can hold a session open for
hours. The renewal path assumed the opposite, in as many words: the comment at
the bounce called the renewal buffer "the ONLY margin between 'device
re-authenticates' and 'broker enforces exp and FIN-closes the session
mid-stream' — observed on the waev preset".

That premise made waev expensive, because waev is the only preset with a short
token_lifetime (3300 s; every other is 0, meaning the 24 h default). It was
therefore the only slot bouncing often: measured every ~47 minutes, about 30
times a day per device. And the bounce's re-handshake is where contiguous
internal DRAM goes — one renewal traced on hardware took the largest free block
from 27,124 to 16,372 B, below the 16,384 B mbedTLS inbound record buffer, after
which that slot could not re-handshake at all. The teardown and the credential
update cost nothing; the handshake costs everything.

So for a broker that leaves live sessions alone, refresh the credentials in place
and let the next genuine reconnect use them. That path already existed for the
"token renewed but old one still valid" case; this just stops treating imminent
expiry as a reason to tear down a healthy connection.

mqttPresetEnforcesTokenExp() defaults to true and is keyed by preset name rather
than a new struct field: adding a field would mean re-ordering a dozen positional
initialisers, where a mistake is silent, and the wrong default costs an outage
rather than a re-handshake. Custom and audience-only slots have no preset and are
treated as enforcing.

Our own logs already argued against the premise and we had not noticed: across 14
multi-device outages (10 hitting all four devices) the drops landed within ~3 s of
each other, on devices whose independent boot times gave them independent token
issue times. Independent expiries cannot align that tightly, so exp enforcement
was never a good explanation for them.

Unverified on hardware yet — the operator's statement is second-hand. Next: apply
to one board only and confirm the session survives past exp, that a later
reconnect still authenticates, and that the ~47-minute 27,124<->16,372
oscillation stops.

(cherry picked from commit 27bd05a17b9303b158feec7dab60af2fe128f5ce)
2026-08-14 09:47:40 -07:00
agessaman daec2e4edd fix(mqtt): address review — stopped clients, late allocation, fail-open map check
Three defects found reviewing the preceding commits.

1. reconnectSlotClient() stranded a STOPPED client, reintroducing the very bug
   this branch fixes. It only rebuilt when isStarted() was true and otherwise
   fell through to reconnect(), which is a documented no-op on a stopped client
   — so nothing restarted it, at any rung, including the breaker probe. The
   WiFi-transition teardown reaches exactly this state: it calls the hard
   disconnect(), clearing _started while initial_connect_done stays set, so
   after WiFi returned the slot could never come back. Now a stopped client is
   started with connect() before the rebuild/reuse decision is considered.

   The post-NTP credential refresh had the same exposure — it called
   reconnect() directly — so it now goes through the helper too, still reusing
   the transport since its fault is stale credentials, not the transport.

2. Allocating the neighbors buffer on first use let a stopped bridge allocate.
   A neighbour discovery started before a stop can complete after it, and
   neither caller rechecks bridge state, so requestPublishNeighbors() would
   allocate 4 KB after releaseRuntimeBuffers() had already run and strand
   _neighbors_publish_pending with no task to consume it. end() then returns
   early on !_initialized, retaining the buffer until a later begin/end or a
   reboot. Guarded on isRunning(), the same flag end() checks.

   The release/acquire handoff itself was confirmed sound: the allocation and
   copy precede the release store, and the task loop reads the pointer only
   after its acquire load, so a half-published pointer is not observable.

3. The post-link map check failed open, contradicting the fail-closed claim in
   its own commit message. A missing map, an unrecognised map format, or a
   partial archive list each warned and passed; and it hardcoded firmware.map
   while the post-action target used ${PROGNAME}, so a renamed program could
   inspect a stale or absent file and still succeed. All four now fail the
   build, and it requires every one of the four archives to appear rather than
   at least one.

Rebuilt Heltec_v3_repeater_observer_mqtt, Heltec_v3_repeater and
heltec_v4_repeater_observer_mqtt; the opt-in path still reports all 4 archives
linked from .mbedtls-4k/.

(cherry picked from commit 5b5f076e5e165997e8050f2be061c7c67340fcf7)
2026-08-14 09:47:40 -07:00
agessaman 8ee21d2c0f fix(mqtt): allocate the neighbors JSON buffer on first use, not at bridge start
allocateRuntimeBuffers() took NEIGHBORS_JSON_BUFFER_SIZE unconditionally on
every board built WITH_MQTT_NEIGHBORS, whether or not mqtt.neighbors was ever
turned on. On a non-PSRAM board that is 4 KB of internal DRAM held for the
bridge's lifetime by a node that may never publish a neighbours snapshot.

Gating the existing allocation on the pref would not work: mqtt.neighbors is
read live by the mesh loop with no bridge restart, so enabling it at runtime
would find no buffer and silently publish nothing. Allocate on first use
instead, in requestPublishNeighbors(), which is reached only when something
actually wants to publish — periodic or a manual discovery.

Publishing the pointer across cores is safe with the existing handshake: the
allocation precedes the release store on _neighbors_publish_pending, and the
task loop reads the pointer only after its matching acquire load, so the
pointer cannot be observed half-published. A failed allocation drops that one
snapshot and retries on the next, rather than disabling neighbours for the
bridge's lifetime as the eager path did.

(cherry picked from commit e6da052a93f8765824d0fb4bd0c704ca3ed3d294)
2026-08-14 09:47:40 -07:00
agessaman 4c90db2199 fix(mqtt): renew JWT credentials without stopping the esp-mqtt client
The scheduled JWT bounce called PsychicMqttClient::disconnect(), which ends
with esp_mqtt_client_stop(). That ends the client task and returns its 6 KiB
stack to the heap at the moment the TLS teardown vacates two 16 KiB mbedTLS
record buffers, so the stack lands in that hole and the next handshake cannot
reuse it. On non-PSRAM boards the largest free block then ratchets down 16 KiB
at a time while total free heap stays flat.

Soak evidence from a Heltec V3 on 8d1a0eb3: 43 of 60 disconnects had no
preceding transport error, i.e. they were this proactive bounce rather than a
broker FIN, and two of the three max_alloc steps landed within 5 s of one.
Losing a whole TLS session later returned exactly 16,384 bytes of contiguity.

softDisconnect() closes the transport without the stop, so the task and its
stack stay put across the handshake. The bounce uses it plus reconnect(), and
falls back to connect() when the client really is stopped, since reconnect()
is a silent no-op in that state.

Also corrects a comment claiming the mbedTLS context survives a transport
close: only the esp-mqtt client object does.

(cherry picked from commit 10cf5cf48fb009e751e25b37fcc1f3d1256ddbbc)
2026-08-14 09:47:40 -07:00
agessaman 6c0ae4d249 merge: upstream/dev into observer-firmware-dev (v1.17.1)
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.
2026-08-14 08:09:07 -07:00
Scott Powell fcb9bdf209 * combine radio's entropy with CC310 RNG 2026-08-14 17:46:43 +10:00
ripplebiz b09cb27a1f Merge pull request #3106 from ViezeVingertjes/fix/scoped-reply-routing
Fix replies dropped when flood.max.unscoped is low
2026-08-13 15:00:37 +10:00
agessaman ced0eb3780 merge: upstream/dev into observer-firmware-dev
Brings in the external FEM gain preferences (fem_txgain, PR #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.
2026-08-12 07:19:19 -07:00
ripplebiz e9edfc8e1a Merge pull request #3188 from oltaco/heltec-t096-pin-fix
Fix Heltec T096 Tx issue
2026-08-12 23:00:31 +10:00
taco f43e1cde89 Revert "t096 compiler layout workaround"
This reverts commit 9c60f9696e.
2026-08-12 22:28:36 +10:00
ripplebiz a4ab7f0e59 Merge pull request #3137 from agessaman/feat/station-g3-fem-prefs
Station G3: Expose, persist, and apply FEM gain preferences
2026-08-12 16:11:23 +10:00
ripplebiz 1cb308b694 Merge pull request #3182 from oltaco/t096-tx-workaround
Workaround for Heltec T096 transmit issue
2026-08-12 14:49:32 +10:00
taco 9c60f9696e t096 compiler layout workaround
see https://github.com/meshcore-dev/MeshCore/issues/3151
2026-08-12 14:04:50 +10:00
ripplebiz e027263d1e Merge pull request #3154 from fdlamotte/nrf52_hwcrypto_init
nrf52: call nRFCrypto.begin()/end() only once
2026-08-12 12:14:21 +10:00
ripplebiz fbef2ff0dc Merge pull request #3164 from weebl2000/fix-tbeam-s3-display
Fix display issues on T-beam Supreme S3
2026-08-11 14:39:39 +10:00
agessaman 28dd599fa8 fix(observer): recover T-Beam Supreme display startup 2026-08-10 12:59:02 -07:00
agessaman e2aa7b98f9 feat(companion_radio): add external FEM gain preferences for RX and TX for companions
Introduced consistent preferences for external LoRa FEM RX and TX gain settings in NodePrefs. Updated companion MyMesh to apply these settings during initialization and transmission. Added unit tests to verify the round-trip serialization of these new preferences.
2026-08-10 11:59:26 -07:00
Rastislav Vysoky 3138ad99ba move RadioLibWrapper noise_floor level message under special debug flag 2026-08-10 16:56:56 +02:00
Wessel Nieboer 32bd6d48d6 Fix T-beam Supreme S3 display 2026-08-10 13:39:16 +02:00
Huw Duddy f6c25e6a25 Merge pull request #3158 from mepholic/bug/fix-rxgain-setting-post-agc-reset
Fix RX Boosted Gain setting after AGC reset is triggered
2026-08-10 17:31:38 +10:00
Dan Theisen 6f491f3033 RX Boosted Gain was set back to the compiled in value rather than the current/flash value set by the user when the ACG reset is triggered. This commit fixes this. 2026-08-09 18:11:35 -07:00
Florent ecb8c94560 nrf52: call nRFCrypto.begin()/end() only once 2026-08-09 14:46:23 -04:00
agessaman 95f326954b merge: perf/memory-savings into observer-firmware-dev
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.
2026-08-09 09:33:20 -07:00
agessaman f9968ac1a5 merge: upstream/dev into observer-firmware-dev (v1.17.0)
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.
2026-08-09 06:32:53 -07:00
taco ce62c8b5d7 LR2021: add PREAMBLE_DETECTED bit and IRQ timeout logic 2026-08-09 21:41:22 +10:00
Adam Gessaman 2125917f15 Merge pull request #43 from agessaman/feat/webconfig-cli
Feat/webconfig cli
2026-08-08 17:20:24 -07:00
agessaman 6cfdc61baf fix(webconfig): define the in-class constants out of line
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.
2026-08-08 17:13:00 -07:00
agessaman da37b6eb2d feat(mqtt): add idahomesh broker preset
Keep built-in preset names in parity with observer-firmware after #41.
2026-08-08 15:19:05 -07:00
agessaman 8abe26ba7b fix(webconfig): stop the CLI reading secrets, and enforce the setup password
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.
2026-08-08 14:05:02 -07:00
agessaman 20826dcccf feat(webconfig): trim the displayed version to base, build and channel
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.
2026-08-08 09:01:29 -07:00
agessaman c831e599ec fix(webconfig): tighten CLI failure detection, reboot deferral and refusals
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.
2026-08-08 08:40:49 -07:00
agessaman d532e4ea86 fix(webconfig): correct reply classification and the missing MyMesh commands
Two things hardware turned up.

The whole terminal came back red. The endpoint decided a command had succeeded
by testing its reply for an "OK" prefix — the convention the config batch relies
on, and a safe one there because every allowlisted setter uses it. The CLI
reaches the whole surface, where success has no single shape: setters answer
"OK...", getters answer "> value", `erase` answers "File system erase: OK". Only
failure is uniform ("Err", "ERR:", "Error:"), so that is what the CLI now tests
for.

Colour was the visible half. The other half was worse: _batch_all_ok went false
the moment a sequence contained a `get`, so a script ending in `reboot` was told
some commands had failed and the reboot was withheld.

Replies are green now and red means the node said no, which is what red should
have meant all along. The "> " a getter prefixes its value with is dropped for
display — on the serial console it sets the value apart, here it collides with
the prompt glyph that means "you typed this". The mock emits that marker too;
had it done so from the start, this would have shown up before the flash.

Second: discover.neighbors and discover.scopes did not autocomplete, because
MyMesh::handleCommand intercepts a few commands before delegating to CommonCLI
and the table was built by reading CommonCLI alone. setperm, `get acl` and
`shutdown` were missing for the same reason.

The audit could not have caught that: it drove every command the table offered
and checked the mock answered, which only finds gaps in one direction. It now
also reads the command literals the firmware dispatches on — across CommonCLI,
CommonCLI_Observer and MyMesh — and fails on any the table does not offer. That
check found `shutdown` immediately.

122 commands, all answered, none missing. 22 batch + 14 keys tests pass.
2026-08-08 08:14:54 -07:00
agessaman d7109c185c feat(webconfig): implement /api/cli on the device
The terminal has been driving the mock since it was built. This is the firmware
side, so it works on hardware.

Same 202 + reqid + poll contract as a config save, for the same reason:
CommonCLI touches prefs, the radio and the filesystem, none of which may be
reached from the async_tcp task. Commands go into the deferred slot and tick()
drains them on the loop task. Unlike a save this is not allowlisted — reaching
what the serial console reaches is the point, and execCommand() already passes
sender_timestamp 0, so the terminal gets exactly the serial console's
privilege. Authentication is the boundary, as it is there.

The CLI shares the config batch's slot rather than owning a second MAX_BATCH
array: both drain on the loop task, both are single-slot, and a duplicate would
cost ~8 KB of permanently resident RAM. Sharing also makes a save and a CLI run
mutually exclusive, which they must be. Each reader checks the kind, so neither
can serve the other's results.

Three things the mock could not have taught us:

  - Board::reboot() does not return, so a drained `reboot` would take the node
    down before the client read a single result. It is answered rather than
    executed, and the batch arms the existing deferred-reboot path once the
    results have been read — withheld if any command failed, exactly as a save
    withholds one. clkreboot/poweroff/ota update do real work on the way down
    and cannot be faked, so they still drop the connection; the UI warns first.
  - `password <new>` echoes the new password in its reply. The config path
    already scrubbed that by key; a CLI entry has no key, so it is matched on
    the command. CLI commands are also kept out of the serial log entirely —
    the browser session and the serial console are different audiences.
  - MAX_BATCH is 24, not the 64 the page assumed. It is reported as
    status.max_cmds instead of hardcoded, so the cap cannot drift.

Results stream and page (kCliResultPage = 8), and "done" means the client has
been handed every result, not merely that execution finished — otherwise a
client that stops polling at "done" loses the last page. Commands are never
echoed back: they may carry a secret, and the client matches by index.

New decisions live in WebConfigBatch.h with the rest, covered by three host
tests. Builds clean for heltec_v4_repeater_observer_mqtt; 22 batch + 14 keys
tests pass; the CLI audit reports 119/119 against the updated mock.
2026-08-07 23:16:38 -07:00
agessaman 3b11540e15 feat(mqtt): add gomesh to broker presets table 2026-08-07 20:54:56 -07:00
Martynas Zilinskas 2436229591 Update MQTT preset for atvirastinklas to use GTS_ROOT_R4 certificate 2026-08-07 18:07:41 +03:00
Martynas Zilinskas da7b51f181 Add new MQTT preset for atvirastinklas. 2026-08-07 17:53:58 +03:00
agessaman 23066573e5 fix(station-g3): expose FEM gain preferences 2026-08-06 15:29:14 -07:00
Hacuchino-hash 3abe415cf7 Add RGB LED and haptic notifications for SenseCAP MeshTracker X1 2026-08-06 07:27:05 -05:00
agessaman 73faa30cfa feat(mqtt): increase built-in MQTT preset count to 31 2026-08-05 19:58:47 -07:00
Josh Clarke c0adb67033 Add new MQTT presets for okimesh 2026-08-05 22:37:03 -04:00
ripplebiz 84e3a67bd7 Merge pull request #3115 from oltaco/meshnology_w12
Add support for Meshnology W12 and LR2021 Wrapper
2026-08-05 15:04:00 +10:00
taco 9d4f93806d LR2021: auto-LDRO for side detectors 2026-08-05 14:23:32 +10:00
taco 36e77671a2 LR2021: fix for hardware cad and side detectors 2026-08-05 14:23:32 +10:00
taco 696a82d7c2 LR2021 multi-SF support (side detectors) 2026-08-05 14:23:32 +10:00
taco 7cc16366cb add Meshnology W12 support and LR2021 wrapper 2026-08-05 14:23:29 +10:00