Commit Graph
3931 Commits
Author SHA1 Message Date
agessaman dfdcc25b50 fix(wifi): one power-save mapping for CLI, startup and reconnect
`set wifi.powersave min` stored 0 and applied WIFI_PS_MIN_MODEM, but the
bridge's post-reconnect mapping read the same 0 as WIFI_PS_NONE. A node
explicitly configured for minimum modem sleep therefore ran with power save off
after its first reconnect, while `get wifi.powersave` still reported min. The
stored default is already 1 (`none`), so nothing here changes the product
default — it stops an operator's explicit choice from being reinterpreted.

Move the value/name/mode table into WifiPowerSavePolicy (pure, host-tested) and
use it from the CLI setter and getter, the web config snapshot and the bridge.
The bridge now also applies the mode when it finds the STA already associated at
start — that path has no connect transition to carry the setting, so a bridge
restart used to leave whatever mode was set before.
2026-09-09 14:44:14 -07:00
agessaman d8ffae3230 fix(dispatcher): pick radio-watchdog activity by age, not largest timestamp
The watchdog took max(last_recv, last_irq, last_tx) of three raw millis()
timestamps. Around the 32-bit wrap a transmit stamped in the upper half of the
range is numerically larger than every fresh post-wrap receive, so it kept
winning for up to ~24 days: the watchdog measured silence from that stale
transmit, concluded the radio had gone deaf, and reset a radio that was
receiving normally — once per watchdog interval, until the old value stopped
winning.

Extract the decision into RadioWatchdog (pure, host-testable). It compares each
timestamp only against its own previous value, so any change is fresh activity,
and stamps it against a monotonic 64-bit clock extended from millis(). Evidence
older than one 32-bit cycle now stays old instead of aliasing back to "recent",
which the previous unsigned-subtraction age could not do either.

Covered in test/test_radio_watchdog: pre-wrap TX with continuous post-wrap RX,
genuine silence across the wrap, silence past a full millis() cycle, no activity
since boot, disabled watchdog, TX-only traffic, and a stalled mesh loop.
2026-09-09 14:42:39 -07:00
Adam Gessaman 4d9ad8722f Merge pull request #51 from stphnrdmr/sr-add-bsmesh
Add BSmesh.de preset
2026-09-06 18:20:05 -07:00
Stephan Rodemeier e28bf78b7f Add BSmesh.de preset 2026-09-06 20:09:08 +02:00
Adam Gessaman 3bacd9edcb Merge pull request #50 from agessaman/feat/station-g3-r2-slot
feat(station-g3): add second RF slot (r2) build targets
2026-09-05 14:27:23 -07:00
agessaman d8d72b7833 feat(station-g3): add second RF slot (r2) build targets
The motherboard has two RF daughterboard slots on separate GPIOs, mirrored by
its "LNA P" / "LNA S" jumpers, but the variant only ever pinned slot 1.

The shared configuration moves into [Station_G3_ESP32_common], with the SPI and
LNA lines split into per-slot sections. [Station_G3_ESP32] (slot 1) and
[Station_G3_ESP32_r2] (slot 2) each compose common + their own pins, so neither
set of -D flags has to shadow the other on the compile line - appending an
override would work only by last-wins, and esp32_base's -w hides the resulting
redefinition warning. Every pre-existing env keeps its name and resolves to
byte-identical options; verified by diffing pio project config --json-output.

PA PL1 is one board-level jumper shared by both slots, so P_PA1_EN stays in the
common section along with the TX power calibration.

Adds r2 twins of the repeater, room server and both observer envs, each an exact
mirror of its slot-1 sibling apart from the base it extends.

Slot 2 pins come from on-hardware testing in agessaman/MeshCore#49 and are not
documented publicly; the vendor wiki serves no content to fetchers and Meshtastic
implements slot 1 only.

Known limitation, unchanged from that PR: GPIO 42 and 43 fall outside the
ESP32-S3 RTC GPIO range (0-21), so the rtc_gpio_hold_en() calls in
ESP32Board::enterDeepSleep() and StationG3Board::powerOff() return
ESP_ERR_INVALID_ARG on slot 2 and the NSS and LNA pins are not latched through
deep sleep. Fixing it means gpio_hold_en() plus gpio_deep_sleep_hold_en() in
shared code.
2026-09-05 12:14:29 -07:00
Adam Gessaman 3eec2d2825 Merge pull request #48 from agessaman/merge/upstream-dev-20260828
Merge/upstream dev 20260828
2026-08-28 21:23:07 -07:00
Adam Gessaman 99a7e99a2e Merge upstream/dev into observer-firmware-dev 2026-08-28 19:45:59 -07:00
agessaman 7ea615dabd docs: document display.timeout and display.flip
Adds both settings to docs/cli_commands.md in the existing format, and records
the R8 dashboard, display controls and the two hardware fixes in the changelog.

The display.flip entry calls out that it is persisted config which survives a
firmware update, since a node still carrying flip=1 from testing looks exactly
like a firmware whose orientation was never fixed.
2026-08-28 16:06:16 -07:00
agessaman 445eaf8342 chore(display): log the persisted display.flip state at boot
display.flip lives in /mqtt.json, so it survives a reflash and is invisible
while someone is chasing a wrong orientation - a node still carrying flip=1
from testing looks exactly like a firmware that was never fixed. Boot now
reports "Display: flip off" or "flip on (rotated 180)".
2026-08-28 15:54:16 -07:00
agessaman 06656d4308 fix(display): default the R8 portrait panels to DISPLAY_ROTATION=0
Rotation 2 was consistently upside down on the Expansion Kit V2 panel, so 0
becomes the compiled default for both portrait observer targets.

display.flip is unchanged and still defaults to off: the compiled constant
should be the correct orientation, with the setting reserved for a board
mounted the other way up. A node already carrying `display.flip 1` from
testing needs `set display.flip 0` after this.

Landscape targets are untouched - they take the driver's own DISPLAY_ROTATION
default of 3.
2026-08-28 15:45:19 -07:00
agessaman 3e7e0322e5 fix(display): make the USER button click immediate, 3 s hold to power off
Power-off now needs a 3 second hold; any shorter press toggles the display.
MomentaryButton reports a CLICK for any release short of its threshold, so
that single value defines both.

The button also felt unreliable - "a brief press doesn't wake it, more often
than not". MomentaryButton's multi-click detection withholds a CLICK for
MULTI_CLICK_WINDOW_MS (280 ms) after release, and folds a second press
arriving inside that window into a DOUBLE_CLICK. Since the handler only acts
on CLICK, an impatient second press produced nothing at all: press, see
nothing, press again, still nothing. Multi-click is now off for these targets,
so CLICK fires on release.

Both settings are build flags defaulted in variants/heltec_v4_r8/target.cpp
and overridden only on the two TFT observer bases, because the companion
builds share this user_btn and do use double/triple click.

DISPLAY_TOUCH_DEBUG additionally logs which input caused a toggle
("Display: button -> on"), so any remaining flake can be attributed to the
button or to a spurious touch read rather than guessed at.
2026-08-28 15:37:09 -07:00
agessaman 018d68063f 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 f3c81b559b 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 86c4849e55 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 fcd92e985f 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 fc361ca94b feat(display): add R8 portrait observer layouts 2026-08-28 09:21:38 -07:00
agessaman 85fd9b09be fix(boards): update Heltec V4 R8 TFT LEDA control to active HIGH 2026-08-28 07:44:16 -07:00
Scott Powell 65650bb192 New TXT_TYPE_CLI_COMMAND (3) 2026-08-28 13:12:59 +10:00
liamcottle e3d0f9fcaa fix cad cli command on companion 2026-08-27 19:05:18 +12: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 9ab13158cb * fix for RPI Picow 2026-08-25 19:04:32 +10:00
Scott Powell 6dad3d5ab4 * companion: fix for setSpreadFactor(). "set cad ..." and "board" now implemented. 2026-08-24 20:44:17 +10:00
Scott Powell 8ccc9928a8 * DynamicConfigSerializer fixes, and unit tests 2026-08-24 19:42:55 +10:00
Liam Cottle 12998cba89 Merge pull request #3275 from liamcottle/gps/rak3401
Fix PA on RAK3401 when GPS missing
2026-08-24 19:52:10 +12: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
Huw Duddy b2499907c2 Merge pull request #3290 from liamcottle/feature/ui-no-hibernate
add UI_NO_HIBERNATE build flag to disable hibernate screen
2026-08-24 13:40:47 +10:00
liamcottle 7dc2d54818 add UI_NO_HIBERNATE build flag to disable hibernate screen on wio tracker l1 2026-08-24 13:45:19 +12:00
agessaman 4a58d8076c fix(boards): initialize Heltec V4 R8 TFT 2026-08-23 16:34:27 -07:00
agessaman c5d987df70 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 a1cf5bd806 * refactored 'FEM' commands for HeltecTrackerV2 & HeltecV4 2026-08-24 02:16:18 +10: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 52306bfe0f legacy queueMessage() case, eg. repeater replies 2026-08-23 20:07:28 +10:00
Scott Powell f7c568e3d9 onCommandDataRecv() use '>' prefix for CLI replies. 2026-08-23 20:03:49 +10:00
Scott Powell 21179760d3 "Unknown command" replies 2026-08-23 19:35:43 +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
ripplebiz 9d7cee6639 Merge pull request #3268 from liamcottle/rak/gps
Add support for RAK12500 and RAK12501 GPS on RAK3401
2026-08-22 14:35:21 +10:00
liamcottle 0b652b5629 add support for rak12500 and rak12501 gps on rak3401 2026-08-22 16:16:32 +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 0cc6fdfea6 Add Heltec V4 R8 FEM LNA controls 2026-08-20 21:48:47 -07:00
agessaman 8fc0733299 fix(examples): add MQTT teardown delay before OTA allocation 2026-08-20 21:13:58 -07:00
Adam Gessaman 2358b750ae feat(mqtt): add Heltec V4 R8 configurations for MQTT observer and room server 2026-08-20 20:50:22 -07:00
agessaman 0a3b50bcf9 test(alert): add WiFi fault policy host tests 2026-08-20 19:46:58 -07:00
agessaman 3666cb6da9 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