Commit Graph
4114 Commits
Author SHA1 Message Date
agessaman e24e6feab5 fix(mqtt): count only failed starts, not rejected reconnects, in cf
reconnect() on a started client returns ESP_FAIL while an attempt is already
in progress. On a Heltec V3, a live reconfigure hit exactly that and the
attempt then connected, yet cf read 1. Count start_failures only when
connect() on a stopped client fails, and restore the attempt flag to its
prior value on any failed call so the in-progress attempt is still judged by
its own outcome.
2026-09-26 12:48:44 -07:00
agessaman 99a4b6fad0 fix(mqtt): count failed starts and ignore bounced sessions in cf
connect()/reconnect() returning an error started no attempt, so no event
arrived and the failure went uncounted -- the low-heap case the counter exists
for. Counted now in a bridge-task-only start_failures, summed into cf: and
mqtt_connect_failures.

A renewal or corrected-clock bounce whose softDisconnect() times out gets its
old session's DISCONNECTED after the new attempt raised the flag. That session
is still marked connected until the handler runs, so the gate now also
requires !connected. An ignored late CONNECTED clears the flag too.

cf: no longer depends on dc being non-zero. Comments corrected for the
PSRAM-fallback stack cost and heap_largest's sampling point; the worst-case
test now models 6 slots.
2026-09-26 12:25:53 -07:00
agessaman 2f6d139729 fix(mqtt): count connect failures from a flag set before the attempt
The Starting-state gate raced the event task: client_state becomes Starting
only after connect()/reconnect() returns, so a fast failure could deliver
DISCONNECTED first and go uncounted, while a deliberate stop of an attempt in
flight was counted as a failure. A per-slot flag is now raised before the call
and cleared by onConnect, a failed start, or stopSlotClient().

Also stops zero-valued connect_failures/slots_breaker from creating an empty
stats object.
2026-09-26 11:53:54 -07:00
agessaman 3230fb6afc feat(mqtt): count connect failures and report tripped breakers
Two gaps remained after the first connection-health commit.

A slot retrying into a heap that cannot satisfy its TLS allocation moved no
counter at all. disconnect_count rose, but it also rises for dropped live
sessions, so it could not separate "actively failing to connect" from "was
connected, dropped once". Adds a per-slot connect_failures, incremented in
onDisconnect when the event arrives while the client is Starting -- meaning
it ended an attempt that never reached onConnect. Late events from a stopped
or quarantined client, and soft-disconnects of a live session, are in other
states and are not miscounted.

A slot parked by the circuit breaker also looked identical to one still
retrying, despite being materially worse: the breaker only probes every 30
minutes, so recovery is far slower and an operator may need to intervene.

Surfaces both:
- `mqttN.diag` gains `cf:<n>` beside the existing `dc:<n>`.
- the status payload gains `mqtt_connect_failures` (summed across slots) and
  `mqtt_slots_breaker` (count of tripped slots), each emitted only when
  non-zero so a healthy board's payload does not grow.

The counter is what distinguishes a retrying slot from a quiet one: while a
slot is down its publish counters cannot move by definition, so this is the
only value that shows the device is still trying.

Both suggested by adversarial review.
2026-09-26 11:10:13 -07:00
agessaman 9512a68953 fix(mqtt): size the status buffer for real field maxima
The worst-case test added with the connection-health fields did not model
production maxima -- it used a 28-byte model and a 24-byte firmware string,
while MQTTBridge permits _origin[32], _device_id[65], _board_model[64],
_firmware_version[64] and derives a 63-byte client version. isValidName()
rejects [ ] / \ : , ? * but permits '"', so a 31-character node name of
quotes is legal and every quote escapes to two bytes.

Measured at those real maxima:

  without connection-health fields   762 bytes
  with connection-health fields      851 bytes
  buffer                             768 bytes

So the 768-byte buffer already had only six bytes of headroom before this
work, and the four new fields overflow it. Overflow is silent:
serializeComplete() returns 0 and clears the buffer, and the bridge
publishes only a positive length, so a device would simply stop publishing
status with no error anywhere -- the same blindness the connection-health
fields exist to remove.

Raises STATUS_JSON_BUFFER_SIZE to 1024 and corrects the test to use the
actual maxima. Status serializes into the shared 2048-byte publish scratch
buffer, so this costs no extra RAM; only the PSRAM-allocation-failed stack
fallback grows by 256 bytes. A static_assert keeps the status ceiling within
the shared buffer.

Found by adversarial review of the preceding commit.
2026-09-26 11:09:21 -07:00
agessaman 2c60fee9d5 feat(mqtt): report slot connection health so an outage is visible
A slot that cannot establish TLS reported nothing. The per-slot counters
track publish successes and failures, and a slot that never connects never
attempts a publish -- so its ok count freezes and its error count stays at
zero for the entire outage. Observed on hardware: a slot sat dead for over
five hours while `sN=` showed 0 errors and the published status payload
carried no indication at all.

Two surfaces gain the missing signal:

- `get mqtt.stats` gains `down=<n>` (omitted when zero, like `filt=`) and
  marks each disconnected slot with a trailing `!`. The marker goes after
  the counts so existing "s(\d)=(\d+)/(\d+)" parsers keep matching.
- The published status payload gains `mqtt_slots_up`, `mqtt_slots_total`,
  `heap_largest`, and `mqtt_outage_secs`. These ride on every healthy slot's
  status message, because the slot that is down cannot report itself.

`mqtt_slots_up < mqtt_slots_total` is the condition to alert on.
`mqtt_outage_secs` is detail: it is present only when the outage has a known
start, so its absence does not imply health -- a slot that has never
connected since boot has no outage start time.

`heap_largest` is included because free heap alone does not predict a failed
connection. A slot needs one contiguous 16,384-byte block for its TLS record
buffers; measured immediately before an attempt this value predicted the
outcome in every observed case, while total free heap was 85 KB throughout a
multi-hour outage. A low value with every slot up is normal rather than a
fault -- established sessions already hold their buffers.

Publish-error semantics are unchanged: connect failures are not folded into
them.

Adds host coverage for the new fields, for the outage key's presence rule,
and for worst-case payload size. That last one matters because
serializeComplete() returns 0 and clears the buffer on overflow and the
bridge publishes only a positive length, so exceeding the 768-byte status
buffer would silently stop status publication -- the same blindness this
change exists to remove.
2026-09-26 11:05:40 -07:00
agessaman b3e059c461 Merge branch 'fix/mqtt-omit-unknown-snr-rssi' into observer-firmware-dev 2026-09-26 10:52:15 -07:00
agessamanandACETyr f9df11f33e fix(mqtt): omit SNR/RSSI instead of publishing placeholder values
The reconstruction-less fallback path in buildPacketJSON (queued packet
with no captured raw frame that cannot be re-serialized) published
fabricated SNR 12.5 / RSSI -65 that consumers could not distinguish
from real radio readings. It now passes NAN / MQTT_RSSI_UNKNOWN, and
buildPacketMessage omits the SNR/RSSI keys entirely when no measurement
exists, mirroring the existing score omission.

Ports the fix from ACETyr's PR #13 (0a037040) on mqtt-bridge-implementation.

Co-Authored-By: ACETyr <35745290+ACETyr@users.noreply.github.com>
2026-09-26 10:46:11 -07:00
agessaman 16fe47cd0e Merge branch 'feat/observer-more-esp32-boards' into observer-firmware-dev 2026-09-26 10:08:18 -07:00
agessaman 9d115ad912 feat(observer): add dev-preview observer envs for 11 more ESP32-S3 boards
Repeater + room-server observer builds for Heltec RC32 (with and without
display), Wireless Tracker v1.0, Wireless Paper, Vision Master E213/E290,
Meshnology W12, LilyGo T-ETH Elite (WiFi only), T3S3 SX1276, Xiao S3 and
Ebyte EoRa-S3. The 4 MB boards (T3S3 SX1276, EoRa-S3) use min_spiffs and
omit SNMP; non-PSRAM boards opt into MQTT_NEIGHBORS_WITHOUT_PSRAM.
2026-09-26 10:03:48 -07:00
agessaman 7e39cac254 Merge upstream/dev into observer-firmware-dev
Picks up 35 upstream commits through 988d017a: board loop() hook,
sensor SUBSCRIBE/telemetry push, companion MyMesh::Listener UI refactor,
agc.reset.interval widened to uint16, LR1121 DC-DC, ThinkNode M4 and
Muzi Base boards, M7 companion WiFi.
2026-09-26 09:32:09 -07:00
Liam Cottle 988d017abb Merge pull request #3497 from meshcore-dev/companion-telem-push
Companion Telemetry push support
2026-09-26 20:51:39 +12:00
Liam Cottle 618b2ef467 Merge pull request #3500 from liamcottle/board/wio-tracker-l1-1watt
disable hibernate ui on 1watt wio tracker variant
2026-09-25 16:57:59 +12:00
liamcottle aa1c46e1db disable hibernate ui on 1watt wio tracker variant 2026-09-25 16:55:17 +12:00
Scott Powell b204f9a114 * changed onUnhandledResponse() to return true if handled/consumed
* unhandled responses (eg. push telemetry) now sent to app as PUSH_CODE_BINARY_RESPONSE frames
2026-09-24 13:49:57 +10:00
Liam Cottle c423dece36 Merge pull request #3490 from liamcottle/fix/heltec-ct62
Fix Heltec CT62 boot loop
2026-09-23 17:48:04 +12:00
liamcottle c13b312188 fix heltec ct62 boot loop 2026-09-23 15:04:51 +12:00
Scott Powell 69f3022a5e Merge commit '140178728b34c821b5169665c1f6f5ede2b9f543' into dev 2026-09-22 18:02:33 +10:00
Scott Powell 560fb847d5 missing board.loop() in companion, room server, kiss modem. 2026-09-22 18:01:15 +10:00
ripplebiz 140178728b Merge pull request #3427 from Dom4n/feat/lr1110-dcdc-dev
Enable DC/DC for LR1110 radios
2026-09-22 17:57:09 +10:00
ripplebiz a76d511ff4 Merge pull request #3131 from liekmarflow/feature/board-api-hooks
Add board loop hook for periodic work
2026-09-22 16:43:18 +10:00
Wolfram Keil 61be70849a Add board loop hook for periodic work 2026-09-22 08:01:48 +02:00
ripplebiz 5e9d80df38 Merge pull request #3464 from lbibass/lr1121_dcdc
LR1121 DC-DC & Misc Updates
2026-09-22 15:12:56 +10:00
ripplebiz 7faf87682c Merge pull request #3469 from Blake-Latchford/issue/3409/constrain-agc-interval
Constrain `agc.reset.interval` and allow intervals > 255s
2026-09-22 15:03:03 +10:00
Blake Latchford 0c3fe356d0 Constrain agc.reset.interval and allow intervals > 255s 2026-09-21 12:26:36 -04:00
fdlamotte be371634d0 Merge pull request #3468 from weebl2000/thinknodem4_fixes
Thinknode m4 - don't touch crystal and OTA name
2026-09-21 11:09:55 -04:00
Wessel Nieboer b5787757a7 Thinknode m4 - don't touch crystal and OTA name 2026-09-21 15:55:44 +02:00
fdlamotte f4ae72dc12 Merge pull request #3436 from Blake-Latchford/wio-e5-fix
WIO-E5 Fix: Pin Radiolab for stm32 and replace ltoa
2026-09-21 05:50:12 -04:00
ripplebiz 65737d48f4 Merge pull request #3402 from meshcore-dev/sensor-improvements
Sensor improvements
2026-09-21 19:03:18 +10:00
Ev Lbibass 84f1e11d29 update structure of LR1121 to match LR1110 files. Implement DC-DC on LR1121 radio. saves about 30% power consumption at idle. 2026-09-20 20:38:05 -04:00
fdlamotte f8f9bd4daf Merge pull request #3463 from fdlamotte/thinknode_m4
Thinknode m4
Doing the merge now as it doesn't touch anything outside its own variant dir and its board file ;)
2026-09-20 16:22:47 -04:00
Florent 8bf072a6f0 M4: Handle poweroff 2026-09-20 16:13:15 -04:00
Florent 0a15c487d8 m4: inital port 2026-09-20 15:25:18 -04:00
Blake Latchford 12a8c8d4d4 Add missing include, and revert removal of lota 2026-09-20 15:16:33 -04:00
Liam Cottle 0886259701 Merge pull request #3108 from tjdownes/muzi-base
Add Muzi Base board support (Duo + Uno + SuperIO)
2026-09-20 22:56:32 +12:00
TJ Downes 114cf9f4fc Use the default BLE TX power on Muzi Base
Drop BLE_TX_POWER=0 from the companion envs so they use the default of
4 dBm, like most other variants. The lower power was carried over from
the original Base Duo PR and real-world range is better at the default.
2026-09-20 03:46:25 -07:00
ripplebiz b37e7dd955 Merge pull request #3431 from meshcore-dev/abstract-ui-overhaul
Abstract UI overhaul
2026-09-20 18:08:29 +10:00
TJ Downes 798ce636df Address review feedback on Muzi Base support
- rename the board class and files to MuziBaseBoard
- uppercase the build flags (MUZI_BASE, MUZI_BASE_SUPERIO)
- name the superIO envs muzi_base_{duo,uno}_superIO_companion_radio_ble so
  build.sh picks them up
- set MAX_CONTACTS to 350 like the other variants
- move the pin setup from initVariant() into MuziBaseBoard::begin()
2026-09-19 21:47:24 -07:00
Adam Gessaman 1025128622 Merge pull request #59 from agessaman/feat/webconfig-board-commands
feat(webconfig): let the portal discover board-specific CLI commands
2026-09-19 21:38:09 -07:00
Adam Gessaman 76a770d8f1 Merge pull request #58 from agessaman/fix/observer-hostname-on-rename
fix(network): rebuild the DHCP hostname when the node is renamed
2026-09-19 21:37:32 -07:00
agessaman fdf299c3d0 fix(webconfig): publish the board probe safely and test the gap
Codex review of the previous commit found two real problems.

The routes go live in startLanMode()/startSetupMode(), one tick before
probeBoardCommands() first runs, so a status request can land in between.
That made _board_cmds a genuine cross-task race despite the comment
claiming otherwise, and — worse — let /api/status answer "no board
commands" before the board had been asked. The page reads status once,
so a browser that loaded in that window hid working FEM controls for the
whole session.

The probe result is now published and read under _mux, and /api/status
OMITS board_cmds until the probe has run, which is not the same as
reporting an empty list. The page treats the absent field as "not known
yet" and re-polls with backoff; the audit waits for it the same way,
rather than auditing a board it only half knows.

Probing from the start path instead would close the window, but
Callbacks::execCommand is documented as loop-task-only and `start
webconfig` is itself mid-handleCommand, so that trades a race for
re-entrancy.

The audit never executed fan.lo or fan.hi, which have no getter and so no
round-trip: they were only ever matched as strings. They now run for real,
including the four rejection paths, and the mock grew --probe-delay so the
retry path is exercised rather than assumed.
2026-09-19 21:08:54 -07:00
agessaman a1f3e2c3d3 feat(webconfig): let the portal discover board-specific CLI commands
The FEM commands moved out of CommonCLI into Board::handleCommand(), and
the T-Beam 1W fan control was added there too, but the portal never caught
up: `radio.fem.txgain` and the fan commands were missing from the terminal
table entirely, while `radio.fem.rxgain` was offered on every board even
though a board with no hook now answers "??:" rather than "unsupported".

Whether a node answers these is a property of the board, not of the build,
so the page cannot know from the firmware version. Ask the board instead:
probeBoardCommands() runs each candidate getter once on the loop task at
startup and keeps the ones that answer, which needs no per-variant list
because Board::handleCommand() already reports whether it handled a
command. /api/status names the survivors and the page hides everything
else, so adding a command to a variant means one entry in WC_BOARD_CMDS
rather than an edit per board.

The two FEM keys also become Radio-panel toggles, gated the same way;
their `set` reaches the board hook through the existing config batch, so
only the allowlist and /api/config needed to grow.

webconfig_cli_audit.py now scans variants/*/*Board.cpp for the boards that
actually build the portal, checks set-only keys in the reverse direction,
and verifies every gate is a command some board answers; the mock gained
--board-cmds so both shapes of board are testable. `stop ota` joins
NOT_OFFERED: `start ota` cannot run from the portal, so it has nothing to
stop.
2026-09-19 20:51:25 -07:00
Liam Cottle 4d9a292084 Merge pull request #3459 from liamcottle/board/thinknode-m7-wifi
Add wifi support to ThinkNode M7 BLE variant
2026-09-20 15:25:08 +12:00
liamcottle 4021a341f8 add wifi support to thinknode m7 ble variant 2026-09-20 15:11:21 +12:00
agessaman 8c8199a2ea ci(observer): prune stale manifests and verify the production OTA channel
Two gaps found while tracing a ThinkNode M7 report of a missing OTA
manifest. The M7 itself was healthy in both channels; these are what the
investigation turned up around it.

Pass --prune to gen-slim-manifests.py in both workflows. The generator only
ever added, so an env that stopped being built kept a manifest pointing at a
release asset the KEEP_BUILDS=2 prune later deleted — a 404 for `ota update`
on any node still running it. The flasher-side commit adds the flag along
with an empty-input guard and --prune-limit, which fails the run rather than
pruning when a build looks like it silently lost envs.

Add "Verify production channel is baked in", which production lacked while
beta has had it from the start. The manifest base is a compile-time -D, so a
build that lost it or picked up the other channel's is invisible until a node
runs `ota check` — and the failure mode is production hardware OTA-ing itself
onto beta. Checked against real published binaries: passes the production M7
build, rejects the beta one.

OTA_MANIFEST_BASE_URL is now stated explicitly in the production env: block,
equal to build.sh's default, so the verify step asserts against the value the
build was actually handed instead of a second hardcoded copy that could drift.

Neither change triggers a build: .github/** is in both workflows' paths-ignore.
2026-09-19 19:54:06 -07:00
Liam Cottle fac7bf5dd3 Merge pull request #3458 from liamcottle/cli/wifi-pwd
Allow fetching configured wifi password via companion cli
2026-09-20 14:34:48 +12:00
liamcottle 73afe3a0f5 allow fetching configured wifi password via companion cli 2026-09-20 14:22:13 +12:00
Adam Gessaman 2ff356b1fc Merge pull request #56 from agessaman/feat/marwoj-preset
Add marwoj MQTT broker preset (dev half)
2026-09-19 17:05:44 -07:00
MarekWoandClaude Opus 5 c32730ea33 Add marwoj MQTT broker preset
Adds a built-in preset for the Polish MeshCore community broker at
mqtt.marwoj.net: MQTT over TLS on port 8883, username/password auth
with credentials embedded in firmware, and the standard MeshCore topic
layout. The Let's Encrypt chain anchors at the existing ISRG_ROOT_X1
constant, so no new CA certificate is needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 17:01:23 -07:00
Adam Gessaman 71fff7d4ef Merge pull request #55 from agessaman/fix/mqtt-preset-file-parity
Share one preset table between channels; enforce it byte-for-byte
2026-09-19 17:01:05 -07:00