Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
1ae9d2a
RTC discovery: expose what the boot probe found
ptr727 Sep 17, 2026
2f5873e
Capture the reset cause at boot instead of consuming it
ptr727 Sep 18, 2026
e6a61a3
Add the `hw` CLI command: what hardware this firmware actually found
ptr727 Sep 18, 2026
06207ab
Document the `hw` command and what it will not infer
ptr727 Sep 18, 2026
6c3f0b1
Review round 1: stop three reports from claiming more than is known
ptr727 Sep 18, 2026
439277d
Review round 2: cover every I2C bus, and honour the documented usage …
ptr727 Sep 18, 2026
912670b
Document the I2C coverage guarantee and its one gap
ptr727 Sep 18, 2026
786b1a5
Review round 3: the empty-scan message claimed more than was known
ptr727 Sep 18, 2026
d84acfb
rak4631: declare the RTC descriptors so a fitted RAK12002 is used
ptr727 Sep 18, 2026
05c9c6b
Review round 4: reset per-candidate probe state on every probe
ptr727 Sep 18, 2026
1820b90
Hardware validation: two redundant fields in the summary
ptr727 Sep 18, 2026
b86ec0f
Review round 6: derive the bus string the way Zephyr names devices
ptr727 Sep 18, 2026
8dd7a83
Add the Heltec WiFi LoRa 32 V4-R8 board
ptr727 Sep 18, 2026
aecbc42
Companion CLI: mark a truncated reply instead of cutting it silently
ptr727 Sep 18, 2026
96bbb83
Review round 7: bound the resume marker, validate two snprintfs, fix …
ptr727 Sep 18, 2026
a897450
Review round 8: fix stale board identity in comments, and one I broke…
ptr727 Sep 18, 2026
50aa8b6
Review round 9: the summary was claiming a fact discovery never estab…
ptr727 Sep 18, 2026
7f93422
Review round 10: the docs contradicted themselves about scan coverage
ptr727 Sep 18, 2026
f1130d4
Review round 11: make the page-index ceiling match the comment explai…
ptr727 Sep 18, 2026
22b0f38
Review round 12: the scan could drop a present chip, under a comment …
ptr727 Sep 18, 2026
fe9b346
Restore the original line endings my edits had silently rewritten
ptr727 Sep 18, 2026
eed6c3a
Merge dev into work/hw-cli
ptr727 Oct 2, 2026
041cb82
hw: build against current dev, and drop a header-only scan line
ptr727 Oct 2, 2026
fcd0f20
hw: page to what the transport carries, and report only what is known
ptr727 Oct 2, 2026
1dd6dc8
rak4631: declare only the RAK12002 RTC, not every RTC candidate
ptr727 Oct 2, 2026
0aa6b62
hw: say autodiscovery is disabled only when something is declared
ptr727 Oct 2, 2026
24728a0
hw: move past a line too long for a whole page
ptr727 Oct 2, 2026
69001b7
hw rtc: absent only when nothing answered
ptr727 Oct 2, 2026
fa7dedd
docs: say what hw rtc's absent and present mean on each driver
ptr727 Oct 2, 2026
69ae19f
docs: correct where hw rtc's absent and present are ambiguous
ptr727 Oct 2, 2026
99f8cf8
rtc: reset the adopted chip with the rest of each probe's outcome
ptr727 Oct 2, 2026
4c8e906
hw: leave room for a local reply prefix in the companion frame
ptr727 Oct 2, 2026
581fe6c
rtc: do not adopt a chip on a status read that failed
ptr727 Oct 2, 2026
35ea3e2
Merge dev (upstream 5b77947) into work/hw-cli
ptr727 Oct 2, 2026
45729a5
hw i2c scan: stop a bus that times out instead of probing every address
ptr727 Oct 2, 2026
0aa174d
hw i2c scan: print what a stopped bus found before saying it stopped
ptr727 Oct 2, 2026
7a9d049
hw: dispatch only on the whole word, and say why a cut line sets no m…
ptr727 Oct 2, 2026
959cc96
hw: compile the report only where CommonCLI is, and say it is local o…
ptr727 Oct 2, 2026
a4fdb4a
hw gps: say a disabled GNSS node is disabled, not configured
ptr727 Oct 2, 2026
d0fd45f
hw gps: say the disabled check is the devicetree half of HAS_GPS_UART
ptr727 Oct 2, 2026
cf743ca
hw: mark a line longer than the render buffer as cut
ptr727 Oct 2, 2026
e1950de
hw: leave room for a local reply prefix on every role, not only compa…
ptr727 Oct 2, 2026
9f62d10
docs: list a bus that was not ready as an unprobed RTC case
ptr727 Oct 2, 2026
a097ab1
hw: answer a start index past the last line instead of an empty reply
ptr727 Oct 2, 2026
08c1ca4
Merge dev (upstream f9d01cf) into work/hw-cli
ptr727 Oct 2, 2026
515dd95
hw i2c scan: name every declaration at a shared address
ptr727 Oct 2, 2026
1dc07d6
Move the upstream defect fixes out of the hw CLI iteration branch
ptr727 Oct 3, 2026
7b090db
docs: say which RTC is adopted, and that hw sensors reads each sensor
ptr727 Oct 3, 2026
acbacfd
hw: make three header comments true of the code
ptr727 Oct 3, 2026
ff92086
hw: state the reply cap a caller passes, and drop a stale scan note
ptr727 Oct 3, 2026
ad70d15
docs: the RTC count with autodiscovery off, and the local prefix
ptr727 Oct 3, 2026
8585b87
Merge dev (108b96d) into work/hw-cli
ptr727 Oct 6, 2026
76a27a7
hw rtc: publish probe outcomes at the end; correct the merged docs
ptr727 Oct 6, 2026
14e6968
hw rtc: read discovery's outcome as one snapshot
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
8bcc79c
Merge upstream dev (75543d4) into work/hw-cli
ptr727 Oct 10, 2026
c167178
hw: probe the I2C scan by reading, and trim to upstream's rules
ptr727 Oct 10, 2026
06de0ca
hw: drop the remaining impossible guards, reuse toHex, one I2C predicate
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
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
Comment thread
ptr727 marked this conversation as resolved.
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