Commit Graph
1595 Commits
Author SHA1 Message Date
agessaman a129420804 fix(network): pre-merge review fixes
- Guard the boot-time link bootstrap to observer ESP32 builds. It ran
  unconditionally in MyMesh::begin(), breaking every non-observer
  repeater/room server build (ESP32 and nRF52).
- Rename NetworkInterface -> NetworkLink (class, accessor, files).
  Arduino-ESP32 3.x ships its own NetworkInterface class and header,
  which broke the ESP32-C6 builds. Drop WiFi.setAutoConnect(), a no-op
  on 2.x and removed in 3.x.
- Refresh stored Wi-Fi credentials every bridge tick so the STA
  reconnect loop picks up `set wifi.ssid` / `set wifi.pwd` without a
  reboot, as the bridge did before the link moved out of it. Skip
  reconnects while the SSID is empty.
- Restore the "WiFi connected: <ip>" / "WiFi disconnected: reason N"
  debug lines the bridge used to print.
- Alert on Ethernet only once it has held a lease this boot or when no
  Wi-Fi is configured; Wi-Fi-only installs of an Ethernet-preferred
  image keep Wi-Fi alerts instead of reporting "Ethernet down".
- Record wifi.setup_complete only for Ethernet LAN onboarding, so
  Wi-Fi builds keep the SSID-based first-boot portal rule.
- Use seq_cst for the route-switch lock/mutation flag handshake.
- Docs: SNMP RSSI sentinel is -127; describe link-return vs medium-switch
  reconnect behavior accurately; note runtime credential pickup.
- Test: unknown keys inside a known /mqtt.json group are ignored, which
  keeps wifi.setup_complete downgrade-safe.
2026-09-19 10:14:16 -07:00
agessaman 8ab6a988aa Merge branch 'observer-firmware-dev' into feat/abstract-networking 2026-09-19 09:23:32 -07:00
agessaman 77398bfc89 Merge upstream/dev into observer-firmware-dev 2026-09-17 21:19:23 -07:00
ripplebiz 0ec5ed5fe7 Merge pull request #3393 from jbrazio/fix/static-pool-hardening
Improve null checks and index validation in StaticPoolPacketManager
2026-09-16 12:26:29 +10:00
Und3r_1337 9d6d53a71a fix(config): allow digits in config keys so keys like gr_1hop round-trip 2026-09-16 01:01:14 +02:00
Huw Duddy 6b2c35449c Merge pull request #3390 from jbrazio/feature/is-gps-detected
Add isGPSDetected() capability probe to SensorManager
2026-09-14 22:41:37 +10:00
Huw Duddy 5f5f4958bf Merge pull request #3002 from entr0p1/feat/nrf52-pwrmgt-v2_3-prep1
nRF52840 Power Management Stage 1 (Boot Lock) - v2.3 preparation work
2026-09-14 21:26:16 +10:00
João Brázio f722794e50 Validate set af input to prevent out-of-range airtime factors 2026-09-10 09:17:37 +01:00
Liam Cottle 1751783931 Merge pull request #3357 from skandragon/feat/picow-wifi-ble
Add WiFi to RPI PicoW (and perhaps others)
2026-09-09 21:55:22 +12:00
taco 974f00ded0 fix: LR2021 to use correct macro for IRQ_DETECTED in startReceive() 2026-09-08 16:35:31 +10:00
Michael Graff b9fa450f52 Fix review findings on Pico W WiFi/BLE 2026-09-06 20:43:37 -05:00
Michael Graff 88cc014f55 Add BLE companion support for Pico W 2026-09-06 20:15:45 -05:00
Stephan Rodemeier 4bbe3bf62b Add BSmesh.de preset 2026-09-06 20:09:08 +02:00
Michael Graff d33fb4e9a7 Fix config parser aborting on an empty object
'custom:{}' (a DynamicConfigSerializer with nothing set) hits EXPECT_KEY
with a '}' and returns TOK_ERROR, so loadSerial stops there and silently
drops every property after it. Nothing follows 'custom' in NodePrefs
today, so it goes unnoticed until you add one.

Also include stdlib.h, which Arduino.h was providing on-device but not
in the native test build.
2026-09-05 23:15:13 -05:00
Michael Graff 0b2c47a932 Add WiFi companion support for Pico W
SerialWifiInterface has no ESP32-specific code, so move it to
helpers/wifi and reuse it on RP2040. Guard the ESP32-only WiFi
event/auto-reconnect calls and poll link state on RP2040 instead.
2026-09-05 23:15:13 -05:00
João Brázio 8a418faf93 Add isGPSDetected() capability probe to SensorManager
Lets applications know whether a GPS was actually detected
(EnvironmentSensorManager reports its gps_detected state), e.g. to
hide GPS-dependent UI when no receiver is attached.
2026-09-04 21:04:20 +01:00
João Brázio 1d5283de76 Improve null checks and index validation in StaticPoolPacketManager 2026-09-04 21:03:06 +01:00
agessaman a0188bd174 fix(network): harden Ethernet-preferred lifecycle 2026-09-04 08:42:46 -07:00
agessaman d9db3e8e6a fix(network): harden automatic link transitions 2026-09-03 20:47:11 -07:00
agessaman fc528ae640 fix(network): set ethernet hostname before startup 2026-09-03 20:47:00 -07:00
agessaman 0c65fc79db feat(network): implement hostname configuration for automatic interfaces 2026-09-03 17:16:30 -07:00
agessaman 88b57db2c6 feat(mqtt): initialize shared network event runtime for Ethernet 2026-09-03 15:38:13 -07:00
agessaman 3e3eec8d5d feat(mqtt): add link diagnostics and improve network logging 2026-09-02 21:37:30 -07:00
agessaman 366b086713 feat: add automatic network failover 2026-09-01 21:18:22 -07:00
agessaman 03c0ad81fe feat: abstract MQTT network transport 2026-09-01 16:31:28 -07:00
Adam Gessaman da6bab3e83 Merge upstream/dev into observer-firmware-dev 2026-08-28 19:45:59 -07:00
agessaman 970ae395a8 fix(display): harden CHSC6X polling and long-gap activity accounting
Addresses a Codex review of the preceding three commits.

Touch could disable itself for the whole session. begin() latched _present
from a single address probe, but this controller NACKs whenever it has nothing
to report, so an idle probe at boot was indistinguishable from absent
hardware. The probe is now diagnostics only; checkTap() polls regardless, and
a NACK costs one quiet bus cycle.

Touch no longer mutates shared bus state. Wrapping the read in
setTimeOut()/restore was an unsynchronised global write, and the MQTT task
drives the same Wire through AutoDiscoverRTCClock in its NTP fallback, so it
could inherit the short timeout. The address probe alone removes the
ESP_ERR_TIMEOUT stalls that motivated it, and it also ran before the timeout
was installed, so the transaction most exposed to a wedged bus was unprotected
anyway.

A single failed read could fake a release. The debounce window was 40 ms
against a 50 ms poll, so a state change was confirmed by the very next sample
and one NACK mid-touch produced a release followed by a second tap. It is now
80 ms - two consecutive consistent reads.

RadioActivityWindow froze after a gap longer than ~24.8 days. tick() treated
any delta past the signed halfway mark as an out-of-order timestamp, so a node
left unserviced that long kept a month-old packet in the 20-minute window and
reported it as recently received. A backwards step is now only believed when
it is small, which is what an out-of-order reading between two call sites
actually looks like.

Not changed: a downgrade that then saves prefs drops the display group, since
/mqtt.json is rewritten from the older serializer's known schema. That is
inherent to every appended field in this format, and bumping the JSON version
would be worse - older firmware would reject the file rather than ignore one
group. Documented instead.
2026-08-28 14:55:44 -07:00
agessaman ca17c2980c feat(display): add runtime display.flip for panel orientation
`set display.flip 0|1` (also off/on) turns the panel 180 degrees from its
compiled DISPLAY_ROTATION, persisted in MQTTPrefs alongside display.timeout
and applied live without a reboot.

Adding 2 to the compiled rotation rather than setting an absolute value keeps
portrait portrait and landscape landscape, so the DisplayViewport geometry
never changes with it and the setting cannot produce a nonsensical mix.
DisplayDriver gains a defaulted no-op setFlipped(), so no other display
driver is affected.

The compiled rotation was verified identical across `pio run` and `build.sh`
on two machines (movi a11, 2 at the setRotation call site), yet the panel read
upside down for one tester and upright for another - which is what a board
mounted either way up looks like. No single compiled constant satisfies both,
so orientation becomes a setting rather than another rebuild.

Runtime-only, like display_timeout_secs: LegacyV1MQTTPrefs and the frozen
binary payload sizes are untouched, and the JSON group is an append that older
firmware skips.
2026-08-28 14:37:03 -07:00
agessaman 11d7551ccd fix(display): correct R8 touch decode, display wake and power-off
Four defects found by hardware testing of the Expansion Kit V2.

Touch never registered. The panel's controller does not use the point-count
encoding the reference CHSC6X drivers document: byte 0 reads 0x00 idle and
0x1F while a finger is down, so testing for a count of 1 never fired. A
partly-failed read leaves 0xFF, which must not count as a press either, so
the test is now != 0x00 && != 0xFF.

Touch polling could stall the UI loop for ~1 s at a time. The controller
NACKs its address whenever it has nothing to report, and calling requestFrom()
unconditionally logged a bus error on every 50 ms poll and, once the bus
wedged, burned a full ESP_ERR_TIMEOUT inside loop(). Probe the address first,
which reports the same NACK quietly, and bound the read with setTimeOut().

The display could not be woken once it blanked; only RST brought it back.
turnOn() re-ran the whole display.init(), which re-enters SPI setup, spends
~500 ms in Adafruit's reset delays and pulses GPIO 21 - the line shared with
TP_RST, so it reset the touch controller on every wake. Since turnOff() no
longer parks that line low, the panel stays configured while dark and waking
is just the backlight. Toggling also clears the refresh deadline so the
current frame is drawn immediately instead of the stale one.

Power-off rebooted instead of staying off. powerOff() went through
enterDeepSleep(), which always arms an ext1 wake on P_LORA_DIO_1; a deep-sleep
wake is a full reboot, so a node in live traffic restarted within seconds of
showing "Turning OFF". It now disables every wake source, so the node stays
down until RST or a power cycle.

Note that the display off/on cycle had never been exercised on this board
before: observer builds pinned AUTO_OFF_MILLIS=0, so the panel never blanked
until display.timeout made it a runtime setting.
2026-08-28 14:26:52 -07:00
agessaman 5d56267176 feat(display): add R8 observer TFT dashboard, touch toggle and display.timeout
Replace the sparse Heltec V4 R8 observer home screen with a padded dark
analytics dashboard, add manual display control, and make blanking a runtime
setting.

Dashboard (DISPLAY_ACTIVITY_DASHBOARD, the four R8 TFT observer envs):

- RadioActivityWindow: 20 one-minute buckets of valid RX packets, no heap.
  The caller's 32-bit millis() is extended to a monotonic 64-bit clock, so
  nothing downstream has a rollover case; an always-on node passes 2^32 ms
  after ~49.7 days, which would otherwise re-enter warm-up and divide 20
  minutes of traffic by seconds. Rates use 19 whole minutes plus the elapsed
  part of the current one rather than a fixed 1200 s.
- ObserverDashboard: header, radio strip, headline totals, a 20-bar
  packets-per-minute graph and RF/status footers, with separate portrait and
  landscape layouts. A text row is a fixed 16 px, which is 3.2 logical units
  in portrait but 4.27 in landscape, so one shared grid would overlap.
  Text is trimmed by character budget, not measured width: getTextWidth()
  reports an over-long string at the portrait driver's fallback scale, so
  DisplayDriver::drawTextEllipsized() under-trims and the row renders at half
  height.
- Six per-row signatures computed from what is actually drawn, so only the
  rows whose pixels changed repaint. No startFrame(), no whole-screen clear.
  Link state moved out of the full-frame signature, so a DHCP renewal or WiFi
  flap repaints one footer row instead of the panel.
- Dark theme by retuning the UIColor statics at runtime, which needs no
  display-driver edit and carries boot, setup, reboot and power-off with it.

Touch and button (DISPLAY_TOUCH_TOGGLE):

- CHSC6X at I2C 0x2E, polled; TP_INT is unusable (optional R13, and GPIO 43
  is U0TXD). The point-count byte is tested against a valid count, never
  against non-zero: an idle read returns 0xFF, which reads as a finger held
  down forever and latches the tap detector after one event.
- turnOff() no longer parks PIN_TFT_RST low on this board. GPIO 21 is a
  shared LCD_RST/TP_RST net, so doing that held the touch controller in
  reset for as long as the display was off. Verified against Heltec's
  expansion-board and mainboard schematics and the V4-R8 datasheet pinout,
  which also correct the pin comment in HeltecV4R8Board.cpp.
- The USER button click now toggles the display too; it previously did
  nothing whenever the display was already on.

display.timeout:

- `set display.timeout <secs>` / `get display.timeout`, 0 = stay on, 60 s
  default, 3600 max. Read live, so a change applies without a reboot and
  restarts the countdown rather than firing on the old deadline.
- Stored in MQTTPrefs (/mqtt.json), keeping NodePrefs aligned with upstream.
  Runtime-only: LegacyV1MQTTPrefs and the four frozen binary payload sizes
  are unchanged. No JSON format-version bump - the loader skips keys no
  def() claims, so older firmware reads newer files and this firmware reads
  older ones with the default applied. Both directions are covered by tests.
- Joins the observer atomic-setter contract, so a failed save rolls the live
  value back instead of only claiming to.

New periodic work uses a wrap-safe deadline check; `millis() >= deadline`
fires every loop for a whole interval before each rollover.

Adds test_radio_activity_window, test_observer_dashboard (driving the real
renderer against a recording DisplayDriver in both orientation profiles) and
test_touch_tap_detector. 440 native cases pass.
2026-08-28 13:39:16 -07:00
agessaman 0d2c926a07 feat(display): add R8 portrait observer layouts 2026-08-28 09:21:38 -07:00
Liam Cottle 1c3248902a Merge pull request #3298 from meshcore-dev/companion-cli
Companion CLI
2026-08-27 18:17:10 +12:00
Scott Powell 8ccc9928a8 * DynamicConfigSerializer fixes, and unit tests 2026-08-24 19:42:55 +10:00
Scott Powell 845242f4aa * prefs, custom dirty state 2026-08-24 17:22:15 +10:00
Scott Powell 5a162ff4c6 * new DynamicConfigSerializer
* board/variant KeyValueStore now can write to 'custom' object in Json prefs
2026-08-24 17:12:56 +10:00
agessaman 910e6ab03d fix(boards): initialize Heltec V4 R8 TFT 2026-08-23 16:34:27 -07:00
agessaman 104143bba6 fix(boards): reset Heltec V4 R8 TFT on GPIO 21
GPIO 21 is the Expansion Kit V2 panel reset, not touch reset.
Leaving RST unwired left the ST7789 blank after a bogus touch pulse.
2026-08-23 15:42:52 -07:00
Scott Powell 47b0b7b3b8 * T096, and Station G3: refactoring the 'FEM' prefs to variant-specific code
* CommonCLI: 'FEM' commands removed
* introduced static no-op attachDynamicPrefs() for all boards
2026-08-24 01:50:22 +10:00
Scott Powell e485d01dcb support for CMD_SEND_TXT_MSG and new TXT_TYPE_CLI_COMMAND 2026-08-23 22:49:45 +10:00
Scott Powell 2c0ace2519 * new TXT_TYPE_CLI_COMMAND (3) 2026-08-23 22:04:05 +10:00
Scott Powell b4f7e941fa * CommonRadioPrefs refactors done 2026-08-23 17:30:30 +10:00
Scott Powell 41588d8052 * adding new CommonRadioPrefs
* refactor: moving various radio CLI handling to CommonRadioPrefs
2026-08-23 14:45:15 +10:00
liamcottle ab4c33826d don't toggle io pins on rak3401 when probing gps 2026-08-23 14:14:45 +12:00
Scott Powell afb969ccca * initial wiring/routing of CLI commands to companion (either via app/serial interface, or remotely via txt message) 2026-08-21 20:55:57 +10:00
agessaman 822020b339 test(alert): add WiFi fault policy host tests 2026-08-20 19:46:58 -07:00
agessaman e3ecae8a94 fix(mqtt): preserve WiFi outage state for alerts 2026-08-20 19:46:44 -07:00
ripplebiz e0031870f6 Merge pull request #3233 from entr0p1/fix/centralise-max-clients
Fix duplicate MAX_CLIENTS definition
2026-08-20 14:09:30 +10:00
ripplebiz c0f30d7304 Merge pull request #3234 from agessaman/fix/sensors-bosch-alt-i2c-address
Fix I2C address claiming and probe both BMP/BME addresses
2026-08-20 14:01:49 +10:00
agessaman 539ae72bca fix(mqtt): stop a wedged client before deleting it on the dirty-stop path
The stop timeout bounded only the cooperative attempt. Its fallback tears down
clients from the calling task, and both helpers skipped the stop entirely when
the client reported not-connected — then deleted it anyway. That is the exact
shape of the dangerous case: a client whose DISCONNECTED callback has already
cleared its connected flag while it sits inside esp_mqtt_client_stop() reports
not-connected, so the stop was skipped precisely when it mattered and the
object was freed from under a live ESP-MQTT task.

Both helpers take a force flag, and only the dirty branch passes it. force is
deliberately not gated on connected(), and routes through the existing
forceStop() rather than disconnect(), whose wait for the DISCONNECTED event is
unbounded. Every other caller — the non-ESP32 release path, the Core-0
cooperative teardown, reconfigure, and the slot-cap path — keeps its current
behaviour byte for byte.

This does not make the path bounded. esp_mqtt_client_stop() waits on the
client's STOPPED_BIT with portMAX_DELAY on the pinned framework, so a client
wedged inside mbedTLS can still block the caller. The change trades a
free-under-a-live-task for a wait, which is the safer of the two failure modes.
Bounding it properly, and the surrounding destroy-while-still-stopping hazard,
need the ownership handoff reworked; that is tracked separately.
2026-08-19 08:38:00 -07:00
agessaman bd91ff80ea fix(mqtt): clear a slot that preferences say is disabled on bridge start
The preset-application loop in begin() only had a positive branch, so when
preferences named no preset the slot kept whatever the previous begin() left in
RAM. Nothing else resets it: teardownSlot() deliberately preserves enabled and
preset so a reconfigure can reuse the mbedTLS context, and the constructor
clears them exactly once. A bridge restart could therefore resurrect a broker
the operator had disabled and reconnect to it with the old credentials.

Config fields only. The client belongs to destroySlotClients() and the token
buffer to releaseSlotAuthToken(); clearing either here would strand a pointer
esp-mqtt still holds in its config.

Note the positive branch re-enabling a slot is correct and unchanged: a slot
capped off at startup is disabled in RAM only, with preferences still naming a
real preset, and the cap decision has to be re-made on each start.

Reachable via WebConfig, where a pending full restart discards the per-slot
restart mask, so a batch that disables a slot can end in a restart that never
applied the disable. A plain CLI set cannot reach it: restartBridgeSlot()
applies the change to the live bridge immediately, keeping RAM and preferences
in agreement. Verified on hardware as a non-regression check for that reason —
disable, restart, slot stays down; restore, restart, all slots return.
2026-08-19 08:38:00 -07:00