Commit Graph
4106 Commits
Author SHA1 Message Date
agessaman 91d9db9c7f Merge branch 'feat/observer-more-esp32-boards' into observer-firmware-dev 2026-09-26 10:08:18 -07:00
agessaman 33f1b1096d 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 6baffa753c 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 5c0da89117 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 8dc81b7447 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 2c057b0fc6 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 3d2d0b5da6 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 4dca3688ea 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 9feed6ed18 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 0dca31f5c3 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 f412ed1b4b 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
agessaman 1bfbbf4bd8 build(mqtt): make the preset table byte-identical across channels
The preset table is fleet state, not a build detail. A slot's preset is stored
in /mqtt.json by name, and firmware that does not know a name does not merely
ignore it: MQTTPrefsSerializer repairs it to "none" and CommonCLI writes the
repaired file back to flash. A node that rolls back to a channel missing a
preset therefore loses that slot permanently, and re-upgrading does not bring
it back.

The parity gate compared preset names only, deliberately allowing URL, CA and
credential drift. It now requires src/helpers/MQTTPresets.h to be byte-identical
between the channels, which also catches that drift — the two channels are meant
to dial the same brokers.

That is only workable if the file holds no channel-specific code, so
mqttPresetEnforcesTokenExp() moves to the new MQTTPresetPolicy.h. It was the
sole difference between the two channels' copies, and with it moved they match
exactly today. Policy keyed off the table belongs there from now on; the table
itself stays pure data.

The older name-only comparison stays available without --exact for ad-hoc use,
and the checker's self-test now covers both modes, including that --exact
rejects a config-only change the name check waves through.
2026-09-19 16:46:55 -07:00
Adam Gessaman 01c0131a59 Merge pull request #53 from agessaman/feat/abstract-networking
Abstract the observer network link; Ethernet-preferred ThinkNode M7 observers
2026-09-19 16:31:59 -07:00
Adam Gessaman bba1425a7a Merge pull request #54 from agessaman/fix/mqtt-review-2026-09-09
MQTT client ownership and review fixes (F01–F04, F06–F08, F11–F14)
2026-09-19 16:31:44 -07:00
agessaman da6406e92f fix(network): rebuild the DHCP hostname when the node is renamed
The hostname was built once in begin() from the node name, so `set name` left
the node advertising its old name to DHCP until the next reflash. It is now
rebuilt on rename through a new CommonCLICallbacks::onNodeNameChanged() hook,
and begin() shares the same helper.

What this cannot do is rename a live lease. IDF 4.4 states that a hostname
changed after the interface is up "would only be reflected once the interface
restarts/reconnects", and Arduino 2.x's WiFi.setHostname() does not touch a
running netif at all — it writes a static string that the netif reads at
bring-up. Forcing the issue would mean bouncing the link, which costs every
MQTT slot a reconnect; that is not a reasonable price for a rename, so the new
name lands at the next reconnect or boot instead.

Only automatic (Ethernet-preferred) links carry a hostname today, exactly as
before. Giving plain Wi-Fi observers one would change the DHCP identity of the
existing fleet and is a separate decision.
2026-09-19 15:57:45 -07:00
agessaman c781862362 fix(network): keep the DNS snapshot typed and on the lwIP thread
Follow-up to the DNS restore, from a second review round.

lwIP's DNS functions belong to the TCP/IP thread, and both halves were called
from elsewhere: the Ethernet snapshot from the Arduino event task, the Wi-Fi
snapshot and every restore from the MQTT task. Both now go through
esp_netif_tcpip_exec(), which is also what serializes the snapshot itself, so
it no longer needs atomics. Neither caller is the TCP/IP thread, so the call
cannot deadlock on itself.

The snapshot keeps the whole ip_addr_t instead of an IPv4 word. These builds
compile lwIP with IPv6 enabled, so reinterpreting a v6 server as v4 would have
installed four meaningless bytes as a resolver. RDNSS is disabled here, so a v6
server should not arise today; storing the tagged value means it stays correct
if that changes.

Restore now writes every slot, empty ones included, so returning to a
one-server network cannot leave the other medium's second server behind as a
fallback that only fails slowly. A lease that carried no resolver at all is not
recorded, so a medium that has never had DNS leaves the current resolver alone
instead of wiping it.
2026-09-19 15:45:39 -07:00
agessaman 6e7f3cdb52 fix(network): bound link transitions and restore each medium's DNS
Two defects found by an adversarial review of the merge.

A link transition no longer stops a slot whose connect attempt is in flight.
Stopping one means esp_mqtt_client_stop(), which waits on the SDK's API mutex
and its task's stopped event with no bound; the client task notices only when
it returns from whatever transport call it is in. During teardown the bridge's
StopUnproven timeout contains that, but during a transition nothing does, so a
routine WiFi flap could freeze the sole MQTT worker for far longer than the 2 s
this path advertises, with the bridge's own stop handshake queued behind it.
The attempt is left to resolve instead: unlike a reconfigure, a transition
changes neither endpoint nor credentials, so an attempt that completes is
credited to the broker it actually reached and drops with the old route.

Failing back to Ethernet left the node using the WiFi network's DNS server.
lwIP keeps one global server list and IDF 4.4 has no per-interface retention,
so the medium that leased last owns DNS for every socket. Each link now
remembers the resolver its own DHCP lease installed and puts it back when it is
selected again; a medium that never held a lease leaves the current resolver
alone. Where the two networks are on different subnets this was a silent
outage: the link read as connected while every broker and NTP hostname failed
to resolve, until Ethernet's own DHCP renewal happened to fix it.
2026-09-19 15:31:51 -07:00
agessaman 25f26c3058 Merge fix/mqtt-review-2026-09-09 into feat/abstract-networking
#54 lands first, so the network abstraction is reworked on top of the
per-slot client ownership model instead of alongside it.

Two resolutions are semantic, not textual:

- Link-transition teardown goes through the ownership API. It called
  softDisconnect() on every started client, which bypassed client_state and
  ignored the typed result. A slot that is still Starting now gets a real
  stop: softDisconnect() returns immediately on a client that is not yet
  connected, so its attempt would otherwise complete against the old route and
  deliver a CONNECTED event indistinguishable from the new one's (F04). On a
  medium switch the old route can still be briefly usable, so that is not
  hypothetical. A connected client keeps the cheap bounded path, and a
  quarantined one is left alone.

- NetworkLink::applyPowerPrefs() adopts WifiPowerSavePolicy. Its local
  `2 ? MAX : NONE` mapping would have reintroduced F11 and read the new stored
  value 3 (explicit `min`) as none. It also applies the setting when the link
  starts already associated, which is the case the bridge used to cover.

The NTP probe from #54 keeps its validation and its one-attempt-per-server
bound, but resolves and gates on the selected link rather than on WiFi, so it
works on an Ethernet-preferred node.
2026-09-19 15:15:07 -07:00
agessaman 16562621a2 fix(mqtt): gate room-server OTA on an unproven stop; reap late acks
- The room server checked canFlashAfterStop() only when the bridge was
  running at OTA time. After a timed-out restart the bridge reads as
  stopped while its unacknowledged task may still own TLS/client state,
  so an OTA could erase and write flash under it. It now refuses while
  the stop is unproven, after first reaping any late ack. The repeater
  already gated unconditionally.
- MyMesh::loop() now reaps a late stop acknowledgement whenever it lands,
  releasing the withheld queue and buffers, and restarts only when a
  resume is pending and the bridge is enabled. Before, a bridge disabled
  during StopUnproven kept those resources until re-enabled or rebooted.
  pollLateStopAck() is public for this.
- The wrapper's destructor no longer stops an already-stopped client:
  destroySlotClients() had just stopped it, so every shutdown logged five
  spurious "esp_mqtt_client_stop failed: ESP_FAIL" errors.

Hardware (Heltec V4, 1 s test stop deadline): restart -> StopUnproven ->
`set bridge.enabled off`; the late ack was reaped ("releasing withheld
resources"), status read "not running", and `set bridge.enabled on`
started cleanly without a second release.
2026-09-19 14:22:00 -07:00