Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
94a4d16
Rebase the hw CLI onto current dev, dropping what has landed upstream
ptr727 Sep 19, 2026
3d6f1d6
Merge dev into hw-cli-only
ptr727 Oct 2, 2026
c7b77d6
hw: build against current dev, and drop a header-only scan line
ptr727 Oct 2, 2026
7165091
hw: page to what the transport carries, and report only what is known
ptr727 Oct 2, 2026
78b486a
hw: say autodiscovery is disabled only when something is declared
ptr727 Oct 2, 2026
7dbe59d
hw: move past a line too long for a whole page
ptr727 Oct 2, 2026
0057730
hw rtc: absent only when nothing answered
ptr727 Oct 2, 2026
1b95502
docs: say what hw rtc's absent and present mean on each driver
ptr727 Oct 2, 2026
5108d4d
docs: correct where hw rtc's absent and present are ambiguous
ptr727 Oct 2, 2026
75c5c52
rtc: reset the adopted chip with the rest of each probe's outcome
ptr727 Oct 2, 2026
5231627
hw: leave room for a local reply prefix in the companion frame
ptr727 Oct 2, 2026
0212c7a
rtc: do not adopt a chip on a status read that failed
ptr727 Oct 2, 2026
01a03b1
Merge dev (upstream 5b77947) into hw-cli-only
ptr727 Oct 2, 2026
4c2c112
hw i2c scan: stop a bus that times out instead of probing every address
ptr727 Oct 2, 2026
f2badf2
hw i2c scan: print what a stopped bus found before saying it stopped
ptr727 Oct 2, 2026
0f413ed
hw: dispatch only on the whole word, and say why a cut line sets no m…
ptr727 Oct 2, 2026
898ebaa
hw: compile the report only where CommonCLI is, and say it is local o…
ptr727 Oct 2, 2026
3f810fe
hw gps: say a disabled GNSS node is disabled, not configured
ptr727 Oct 2, 2026
755e28a
hw gps: say the disabled check is the devicetree half of HAS_GPS_UART
ptr727 Oct 2, 2026
4385a20
hw: mark a line longer than the render buffer as cut
ptr727 Oct 2, 2026
018b73c
hw: leave room for a local reply prefix on every role, not only compa…
ptr727 Oct 2, 2026
0fed09c
docs: list a bus that was not ready as an unprobed RTC case
ptr727 Oct 2, 2026
04c5459
hw: answer a start index past the last line instead of an empty reply
ptr727 Oct 2, 2026
19cd0da
Merge dev (upstream f9d01cf) into hw-cli-only
ptr727 Oct 2, 2026
47c0649
hw i2c scan: name every declaration at a shared address
ptr727 Oct 2, 2026
bf3e96a
Move the RTC status-read fix out of the hw CLI branch
ptr727 Oct 3, 2026
2612e66
docs: list the hw command in the architecture's CLI summary
ptr727 Oct 3, 2026
311ed79
docs: say which RTC is adopted, and that hw sensors reads each sensor
ptr727 Oct 3, 2026
e3bd329
hw: make three header comments true of the code
ptr727 Oct 3, 2026
e821439
hw: state the reply cap a caller passes, and drop a stale scan note
ptr727 Oct 3, 2026
cc54670
docs: the RTC count with autodiscovery off, and the local prefix
ptr727 Oct 3, 2026
29af49c
Merge dev (108b96d) into hw-cli-only
ptr727 Oct 6, 2026
db69172
rtc: clear the RV3028 12-hour bit at boot
ptr727 Oct 8, 2026
2eb8a8e
t096: shared PWM brightness for the heartbeat/TX LED (unpersisted)
Aug 26, 2026
bb49aa1
t096: fold LED brightness into set/get leds instead of a new command
Aug 26, 2026
c8c456b
t096: split leds.brightness from leds on|off (final Option 1 syntax)
Aug 27, 2026
4905191
wt2: shared PWM brightness for the status LED (heartbeat/TX/flashes)
Aug 27, 2026
ec2a07d
leds: persist brightness across reboots and firmware updates
Sep 4, 2026
50ca150
board: extend TX/RX LED pin-share arbitration to PWM-shared boards
Sep 4, 2026
0a15194
board: wire V4.3's existing PWM LED node into the brightness/mode system
Sep 4, 2026
c92c9ff
leds: default brightness 100%, matching pre-existing hardware behavior
Sep 4, 2026
f04546d
prefs: encode led_brightness too, not just decode; fix pinned test la…
Sep 30, 2026
263171b
tests: sync catalog.json labels with f8a4992 prefs size bump
Sep 30, 2026
ad2e17e
prefs: move led_brightness after wifi_pwd; enable PWM in board.conf
Oct 6, 2026
5425ce8
cli: restore cliOnOff() for "set leds"
Oct 6, 2026
fc0723a
led: one shared PWM write helper, shorter comments
Oct 6, 2026
e5e927e
leds: drop unused esp32s3_pwm_led.conf, fix stale WT2 comment
Oct 7, 2026
6b9d205
leds: do not reclaim a PWM-shared pin as plain GPIO in main()
Oct 8, 2026
c1d2185
Merge pull request #106 from ptr727/rv3028-boot-24h
liquidraver Oct 8, 2026
6acefe6
md updates
liquidraver Oct 8, 2026
119d124
Merge pull request #79 from bisbille/pr-led-brightness-pwm
liquidraver Oct 8, 2026
c60b5e1
pwm cleanup
liquidraver Oct 9, 2026
3518a4d
robot instructions update
liquidraver Oct 9, 2026
c69b3c6
md updates
liquidraver Oct 9, 2026
1b3d378
enable MCU temp for ESP
liquidraver Oct 9, 2026
5ac3156
observer lightsleep + companion opt-in
liquidraver Oct 9, 2026
12eba39
add erase and reboot to observers
liquidraver Oct 10, 2026
e00b128
observer TLS fix
liquidraver Oct 10, 2026
7dc00bb
west bump
liquidraver Oct 10, 2026
75543d4
enable lightsleep for all
liquidraver Oct 10, 2026
c72582b
Merge upstream dev (75543d4) into hw-cli-only
ptr727 Oct 10, 2026
8c800f6
hw: take work/hw-cli's rework (c167178, 06de0ca)
ptr727 Oct 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,4 @@ __pycache__/
.DS_Store
Thumbs.db
desktop.ini
zephcore/lib/zcpfs/SPEC.md
8 changes: 7 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ touch before writing code. Decisions are in `docs/adr/`.
## Invariants: do not change without an ADR

- Byte compatibility with Arduino MeshCore on the air and on the companion protocol; `prefs.json` format.
- Settings persist in `prefs.json` only. The legacy binary prefs files are frozen: no new fields in
`PrefsCodec`, no change to its size constants or to the pinned offsets and lengths in its tests.
- The wall clock only moves forward.
- Mesh, GPS state and UI run on the main thread; loops are event-driven, no polling.
- No heap in the packet path. Companion builds are RAM-bound.
Expand All @@ -48,10 +50,14 @@ touch before writing code. Decisions are in `docs/adr/`.
## Mechanics

- Branch from `dev`, target `dev`, rebase instead of merging.
- After a rebase, re-read the whole diff against `dev`: a branch older than the code around it can undo newer
work without a conflict.
- Tabs for indentation, except the upstream-ported files.
- Build with `--pristine` when the board or role changes; ESP32 S3/C-series repeaters need `--sysbuild`.
- Host tests: `python3 tests/run.py run --profile quick` (Linux or WSL). A new case needs a
`tests/catalog.json` entry.
`tests/catalog.json` entry. A failing pinned test is a finding: do not edit the pinned values to make it pass.
- PR CI runs the host tests only. Build every board you touched and one you did not, and run each role you
changed. Releases build from `zephcore.yml` alone, so nothing may depend on a hand-passed fragment.
- New board: `zephcore.yml` without `release:`, then `python zephcore/scripts/board_manifest.py check` and
`... docs`.
- Docs in the same PR: a clause in `docs/ARCHITECTURE.md` when behaviour, an interface or a limit changes;
Expand Down
38 changes: 31 additions & 7 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,7 @@ Handles the binary BLE protocol with ~50 command opcodes. Key features:
- **ACK tracking**: 8-slot table, computes expected ACK = SHA256(secret + hash)[0:4]
- **Contact iteration**: Streaming protocol with `lastmod` filtering for incremental sync. The dump advances only
while a client is connected and every connected transport has room; it is cancelled when the last client leaves
- **Lazy write batching**: Dirty contacts/channels flush after 5-second delay
- **Lazy write batching**: dirty contacts and channels are written 5 s after the last change, so a burst (a contacts import) is written once; a contact change that only refreshes liveness waits up to an hour (`LAZY_WRITE_LIVENESS_MS`)
- **Protocol versioning**: V2/V3 frame format negotiation with phone app
- **Ed25519 signing**: 3-phase flow (start→data→finish) for signing up to 8KB
- **Flood scope**: Transport key filtering for region-scoped sends
Expand Down Expand Up @@ -738,6 +738,7 @@ Listen-only node (ESP32 only): receives LoRa packets and publishes them to an MQ
### 6.6 CommonCLI Commands

System: `ver`, `board`, `reboot`, `start dfu`, `start ota`, `erase`
Hardware: `hw`, `hw board/rtc/i2c [scan]/gps/sensors/all` — see `CLI_commands.md`
Config: `set name/freq/radio/tx/flood.max/password/...`, corresponding getters
System (power): `poweroff`/`shutdown`, `get pwrmgt.support/source/bootreason/bootmv`
GPS: `gps`, `gps on/off/sync/setloc/advert`, `get gps`, `set gps duty <sec>`
Expand Down Expand Up @@ -838,6 +839,8 @@ Repeaters and room servers default to `CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC
went away is reported as a DTR drop in three cases: DTR low, VBUS removed, and a bus reset. The reset case exists
because the CDC class keeps the old line state across a reset, so nothing else says that the port the host had open
is gone (a hub resetting upstream leaves VBUS up). Bus suspend, resume and reset also reach a second callback.
While a host is on the bus (from reset, configuration or resume until suspend or VBUS loss) the module blocks
ESP32 light sleep: sleep stops the USB clock and the host loses the device without being told.
- **`ZephyrCompanionUSB`** (companion): the wired companion transport, over the CDC port or a plain UART
(`zephcore,companion-uart`). Framing: `<` (to the node) or `>` (from it), a little-endian 16-bit length, the payload;
a first byte that is printable and not `<` starts the text CLI instead. It has its own receive queue and an
Expand All @@ -850,17 +853,17 @@ Repeaters and room servers default to `CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC

### 7.5 Board (`adapters/board/`)

- Battery ADC with optional regulator-gated voltage divider, 8-sample average (boards with `zephyr,user` ADC node; MG24 has no battery divider, ADC disabled)
- Battery ADC with optional regulator-gated voltage divider, 8-sample average (boards with `zephyr,user` ADC node; MG24 has no battery divider, ADC disabled). A board with a fuel gauge (AXP2101) reads voltage and charge from it instead; one with an nPM1300 charger (XIAO nRF54LM20A) reads the voltage from the PMIC's own ADC through the sensor API and takes the percentage from the discharge curve
- UF2 bootloader entry via GPREGRET magic (0x57 = UF2, 0xA8 = BLE DFU)
- TX LED bracketing for LoRa transmissions (gated by the LED master switch below)
- Bootloader version detection via flash memory scan
- Battery reading cached 10 s for every caller (UI, telemetry, stats, alerts), under a mutex
- `powerOff()` (CLI `poweroff`/`shutdown`, UI power-off, low-battery shutdown all end in `zephcore_power_off()`, `board/zephyr_poweroff.c`): UI hook (heartbeat, display), GPS/sensor/buzzer rails off, LoRa held in reset, sw0 SENSE armed (nRF), power latch released, System OFF
- Upstream's pwrmgt getters: reset cause from `boot_info`, the last fatal error from `fatal_reboot.c`'s `__noinit` record (reason, pc, lr, thread; resolve pc with `addr2line` on the same build), shutdown reason from the `/lfs/shutdn` marker, battery voltage at boot

**Telemetry** (`REQ_TYPE_GET_TELEMETRY_DATA`, every role, upstream's shape): battery on channel 1, then `sensors.querySensors()` (`adapters/sensors/ZephyrSensorManager.cpp`): GPS position on channel 1 while the GPS is on and has a fix, each environment sensor and power-monitor channel found at boot on its own channel from 2 up (probe order), board-local analog sensors (T1000-E) on channel 1; then the MCU temperature on channel 1. Servers give guests battery + MCU temperature only and honour the requester's inverse permission mask, as upstream. Encoded by `helpers/compat/CayenneLPP.h`: upstream's library API and wire format, but values round to the nearest step where the library truncates.
**Telemetry** (`REQ_TYPE_GET_TELEMETRY_DATA`, every role, upstream's shape): battery on channel 1, then `sensors.querySensors()` (`adapters/sensors/ZephyrSensorManager.cpp`): GPS position on channel 1 while the GPS is on and has a fix, each environment sensor and power-monitor channel found at boot on its own channel from 2 up (probe order), board-local analog sensors (T1000-E) on channel 1; then the MCU temperature on channel 1 (the SoC's die sensor: the `die-temp0` alias, which is `coretemp` on ESP32-S3/C3/C6, else the Nordic nodelabel `temp`; omitted where there is none: classic ESP32, STM32WL, MG24). Servers give guests battery + MCU temperature only and honour the requester's inverse permission mask, as upstream. Encoded by `helpers/compat/CayenneLPP.h`: upstream's library API and wire format, but values round to the nearest step where the library truncates.

**Clock** (`adapters/clock/ZephyrRTCClock`): `setCurrentTime()` also writes the hardware RTC when the board has one (coalesced on the system work queue; a time outside 2000-2099 is not written), as upstream's `AutoDiscoverRTCClock` writes its chip; `seedCurrentTime()` is the boot restore, not written back. A chip is adopted at boot only once it is identified (`adapters/clock/ZephyrRTCDiscover.c`), because other parts can answer at the same address: a device is passed over when two reads each show a bit set that the chip's data sheet fixes at zero, or a time field out of range, and a time is restored only from a read in which every field is valid. The time is written in each chip's own encoding (the RX8130CE takes a one-hot weekday, `weekday-one-hot` in its descriptor), and an RV-3028 whose descriptor carries `rv3028-eeprom-config` (the RAK12002 on a RAK4631) has that configuration, its backup switchover, stored in the chip's EEPROM. Without a hardware RTC, the last time read is kept in no-init memory (RTC slow memory on ESP32) and restored after a reset that keeps RAM (reboot, crash, watchdog), as upstream's `ESP32RTCClock`; a power cycle starts from 1970 again.
**Clock** (`adapters/clock/ZephyrRTCClock`): `setCurrentTime()` also writes the hardware RTC when the board has one (coalesced on the system work queue; a time outside 2000-2099 is not written), as upstream's `AutoDiscoverRTCClock` writes its chip; `seedCurrentTime()` is the boot restore, not written back. A chip is adopted at boot only once it is identified (`adapters/clock/ZephyrRTCDiscover.c`), because other parts can answer at the same address: a device is passed over when two reads each show a bit set that the chip's data sheet fixes at zero, or a time field out of range, and a time is restored only from a read in which every field is valid. An RV-3028 cleanly identified in 12-hour mode has its `twelve-hour-bit` cleared first, so the chip converts its own hours. No time is taken from a cleanly identified RV-3028 whose `twelve-hour-bit` register cannot be read or reads FFh, or whose clear or re-read fails. The time is written in each chip's own encoding (the RX8130CE takes a one-hot weekday, `weekday-one-hot` in its descriptor), and an RV-3028 whose descriptor carries `rv3028-eeprom-config` (the RAK12002 on a RAK4631) has that configuration, its backup switchover, stored in the chip's EEPROM. `zephcore_rtc_snapshot()` gives `hw rtc` each candidate's outcome from the last probe run and the adopted chip, published together when the run ends. Without a hardware RTC, the last time read is kept in no-init memory (RTC slow memory on ESP32) and restored after a reset that keeps RAM (reboot, crash, watchdog), as upstream's `ESP32RTCClock`; a power cycle starts from 1970 again.

**LED master switch** (`helpers/led_gate.{c,h}`, `set leds on|off`, all roles): one process-wide
flag every LED driver consults — heartbeat and unread-message LEDs in `helpers/ui/ui_common.c`, the
Expand All @@ -871,18 +874,39 @@ hook so a CLI change also stops a lit heartbeat and refreshes the UI's LEDs page
`NodePrefs.leds_disabled` (companion offset 93; repeater offset 120, magic-encoded — see §13).
Does not cover the display backlight, which has its own UI brightness setting (`display_brightness`).

**LED brightness** (`set leds.brightness 0-100`): a board that aliases `heartbeat-pwm-led` /
`lora-tx-pwm-led` drives that LED through PWM (`zephcore_led_pwm_write()`), and the PWM alias wins over
`led0` / `lora-tx-led`. `CONFIG_ZEPHCORE_LED_PWM` follows the aliases: it selects `CONFIG_PWM` and stops
the server and observer mains from configuring `led0` as a GPIO, because that pin belongs to the PWM
driver. Persisted in `prefs.json` as `zc.leds_brightness`; it is not part of the legacy binary files.

### 7.6 WiFi / MQTT / TCP Transports

- **`adapters/wifi/ZephyrWiFiStation.c`**: WiFi STA client (ESP32) used by the observer, the repeater uplink and the WiFi companion
- **`adapters/mqtt/ZephyrMQTTPublisher.c`**: MQTT publisher for observed/uplinked packets
- **`adapters/wifi/ZephyrWiFiStation.c`**: WiFi STA client (ESP32) used by the observer, the repeater uplink and the WiFi companion.
The observer and the uplink run with the driver's WiFi power save (minimum modem sleep: the WiFi radio is off
between the access point's DTIM beacons); the WiFi companion requests power save off, as before. On ESP32
Comment thread
ptr727 marked this conversation as resolved.
light-sleep builds the station blocks SoC light sleep until the link is ready (DHCP, SNTP) and its client has
reported a session with `zc_wifi_station_session_up()`, and again whenever either is lost. With no such client
(the WiFi companion) the block stays for the life of the boot. While the SoC sleeps the WiFi MAC wakes it for
each beacon it listens to, and `helpers/pm_esp32_wake.c` refuses a sleep while the MAC's beacon timer is active.
On light-sleep companion builds the WiFi companion runs with power save too and reports a session at once, so
the station blocks sleep only while the link is not ready.
- **`adapters/mqtt/ZephyrMQTTPublisher.c`**: MQTT publisher for observed/uplinked packets. It reports its session to
the station: up at CONNACK, down when the session ends for any reason. Keepalive: PINGREQ once a quarter of
the 60 s keepalive is left, so a broker that enforces the keepalive without grace does not close an idle
session.
TLS is used without certificate verification, but the chain still has to be parsed: the uplink and observer
builds carry P-256 and P-384 with SHA-256 and SHA-384 for that.
- **`adapters/ota/wifi_ota.c`**: WiFi SoftAP + HTTP firmware upload to MCUboot slot1 (ESP32, requires `--sysbuild`)

Companion transports, as upstream's `companion_radio`: each is a `BaseSerialInterface`
(`adapters/transport/CompanionInterfaces.h`) registered in one `MultiSerialInterface`, so
BLE, USB/UART and TCP can all be connected at once; every connected client gets every
frame (a reply to one app also reaches the other).

- **`adapters/ble/ZephyrBLE.cpp`** (+ `ble_gatt_layout.cpp`, `ble_dfu.cpp`): BLE NUS
- **`adapters/ble/ZephyrBLE.cpp`** (+ `ble_gatt_layout.cpp`, `ble_dfu.cpp`): BLE NUS. Blocks ESP32 light sleep from
connect until the link is encrypted (pairing does not survive sleep); an encrypted link and advertising do not
block it.
- **`adapters/usb/ZephyrCompanionUSB.cpp`**: USB CDC-ACM, or a plain UART (`zephcore,companion-uart` chosen node; the only link on the Bluetooth-less LoRa-E5), with the text CLI
- **`adapters/transport/TcpCompanionTransport.c`**: TCP (port 5000, MeshCore `SerialWifiInterface` framing) on native Linux and on WiFi companions (`app/CompanionWifi.cpp`, `capabilities: wifi: true`)
- **`adapters/transport/frame_txq.c`**: the TX queue BLE and TCP share (congestion, overflow slot, lossless replies)
Expand Down
Loading
Loading