Server roles (slice 7)
- CommonCLI split into upstream's shape; region CLI moved into CommonCLI;
ClientACL, RegionMap, TransportKeyStore and both server meshes in
upstream shape over an Arduino compat shim (helpers/compat: Arduino.h,
Stream/Print, Serial, SHA256, FILESYSTEM/File over Zephyr fs)
- room server processAck port; `get acl` prints as upstream
- room: default name Room-XXXX, display/input rotation, live radio UI refresh
Event loops (slice 7b)
- repeater and room server share server_main_common with a ServerRole
descriptor; shared event bits in src/mesh_events.h
- room server deadline-driven (idle wakes 2.2/s -> 0.08/s); companion
loop() runs on housekeeping (closes the lazy contact-flush gap)
- `get loop.wakes` in debug builds (CONFIG_ZEPHCORE_LOOP_WAKE_STATS)
Companion transports and WiFi (slice 8)
- upstream MultiSerialInterface: BLE, USB/UART and TCP are each a
BaseSerialInterface, every connected transport gets every frame;
CompanionSerial holds a lossless reply; shared frame_txq for BLE/TCP
- SerialCompanionTransport and LinuxTCPTransport replaced by
TcpCompanionTransport; ZephyrBLE split (gatt layout, DFU)
- WiFi companion (upstream wifi.ssid/pwd/enabled CLI, TCP port 5000) on
boards declaring `capabilities: wifi` in zephcore.yml: the PSRAM
ESP32-S3 boards, plus Heltec V3 (140 contacts), Wireless Tracker (130)
and both C6 boards (280); ESP32-S3 without PSRAM gets bounded WiFi
buffers and a measured 72 KB heap; PSRAM boards' internal heap 72 KB
- TCP queues: own depth (ZEPHCORE_TCP_QUEUE_SIZE=8), RX waits instead
of dropping
- RAM: companion ClientACL dropped; main/BT-TX/TCP stacks sized from
measured peaks (-7 KB nRF companions, -9 KB ESP32 companions)
- ESP32 WiFi stopped working under ~200 frames/s unicast. Fixed by:
patches/zephyr/0017 never block the WiFi task in the RX callback;
patches/zephyr/0018 lock shared_multi_heap (heap corruption with the
WiFi heap in PSRAM); patches/modules/hal_espressif/0001 backport of
hal_espressif af97fb02df (PSRAM realloc)
Storage and prefs (slice 9)
- prefs are now prefs.json through upstream's ConfigSerializer (ported
verbatim), with upstream's key names for shared fields and ZephCore's
own under "zc"; on first boot the binary new_prefs / repeater prefs is
migrated and kept, so a downgrade still boots with the old settings
- helpers/PrefsCodec: both legacy binary layouts (companion 272 B,
server 311 B) as Zephyr-free codecs; sanitizeNodePrefs is the only
validator
- fix: server prefs could be replaced by a truncated file when a write
failed; identity, prefs and channel writes go through one checked
atomic replace (ZephyrFsUtil)
- fix: a corrupt cad_auto byte turned adaptive CAD off instead of the
default on
- fix: a deliberate 300 s GPS duty on a repeater was rewritten to 48 h
on every boot (that migration now runs on legacy files only)
- one identity loader for all roles (IdentityFile); an unusable key
file is always parked as .bad before regenerating
- server store no longer stamps repeater defaults on room server and
observer; companion first-boot volume policy moved into the store
(adoptVolume)
- ZephyrDataStore 1435 -> ~850 lines
- LoRaRadioBase -> LoRaRadio (final); SX126x/SX127x/LR1110/LR2021Radio
replaced by const LoRaRadioOps tables (radio_ops_<family>.cpp), one
linked per build; mains construct the radio in one line
- lr20xx: vendored Semtech SDK + HAL moved beside the driver (no app-tree
sources), DIO1 handler split (CCN 52 -> 10), zc_lora_timing.h adopted,
dead exports removed, header rewritten, comment diet (image-identical)
- semtech,lr2021 uses the lr1110 property names (irq-gpios, tcxo-voltage,
rfsw-*), compiled config byte-identical
- correct the stale "LR2021 is not CAD-probed" claim
- CadController (stats, window, re-anchor, visits, staircase, safety,
starvation relax) and NoiseFloorEstimator split out of LoRaRadioBase
behind a 4-call CadHw interface; Zephyr-free, host unit tests added
- prefs mandatory: setPrefs() before begin(), LoRaConfig fallbacks and the
companion/observer placeholder prefs removed; LoRaConfig is the one
defaults source (invalid-prefs fallback now uses the board TX default)
- TX-wait thread stack owned by the base; reconfigureWithParams removed
- driver extension headers on the public include path
(zephyr/drivers/lora/*.h); patch 0003 include updated
- comment diet (image-identical); rationale moved to devdocs LLD 03
- Mesh, Dispatcher, Packet, packet pool and the core headers rebuilt from
Arduino MeshCore's text; ZephCore changes re-applied, each fenced
`// ZEPHCORE:` (diff vs upstream: 4540 -> 1147 lines)
- upstream header layout; MainBoard gains handleCommand/loop and the other
upstream members; RNG::nextInt back in Utils.cpp
- packet logging moved to the roles' log hooks (helpers/PacketLog.h)
- zero-hop direct ACKs deduplicated and length-checked as upstream
- RadioIncludes.h -> src/, LoRaConfig.h -> helpers/
- .editorconfig: shared files keep upstream formatting
Verified: 10 role builds, per-function disassembly review, bench DM/ACK
regression (T1000-E, Wio L1, XIAO S3 on 1.17.4) incl. via live repeater.
main_repeater.cpp and main_room_server.cpp applied the GPS duty interval
~55 lines before data_store.loadPrefs() ran, so getNodePrefs()->gps_interval
still held the initNodePrefs() value of 300 s.
That 300 is the *companion* default. loadPrefs() is not a pure read on these
roles: its first-boot path is the only place the 48 h repeater/room-server
default is established, and it persists that to flash so later code can treat
flash as authoritative. A stored value is likewise only in the struct after
loadPrefs() returns. Reading gps_interval before that call therefore cannot
see either the role default or the operator's setting -- only the companion
constant.
Two consequences, on every boot:
- A stock GPS-equipped repeater duty-cycled its GPS every 5 minutes instead
of every 48 hours, ~576x more wakeups than intended. On battery nodes this
is a straight power regression.
- Any operator value never reached the hardware. "set gps duty 0" (always-on)
persisted to flash correctly, and `get gps duty` reports the manager's
now-effective value, so it answered 300 -- readable as a stale default
rather than as the setting being ignored.
The GPS manager itself is correct: gps_start_acquiring(), the fix-done
callback, gps_enable() and the fresh-fix request all guard standby on
gps_duty_cycling(), so the always-on invariant holds once the manager is
actually told the interval is zero.
Same ordering class as the two bugs already documented in these files -- radio
frequency binding and the LED master switch -- where CLI readback looked right
and the hardware disagreed. main_companion.cpp has always had the correct
order (loadPrefs() at 1461, gps_set_poll_interval_sec() at 1616) and is
unaffected.
Move the interval and the mode switch that starts acquisition to just after
loadPrefs(). Callback registration stays where it was: it has no prefs
dependency and must precede acquisition.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two hardware-found fixes after on-air testing on the GAT562 kit:
- Add a 500ms push timer in main_room_server.cpp so the post-sync
engine advances at its intended cadence. ZephCore is event-driven
(no Arduino superloop), so without this the engine only ran on the
5s housekeeping tick — posts dripped out every ~5s and transmits
bunched up, causing timeouts/resends.
- Lower the post-sync hold from upstream's conservative defaults
(POST_SYNC_DELAY 6s -> 2s, PUSH_NOTIFY 2000ms -> 1000ms). These are
server-side timing only (no wire-format change), and take measured
delivery from ~6-7s down to ~2s.
Verified on hardware (910.525/62.5/SF7/CR8, two clients): normal-pace
messages deliver in ~2s with clean ACKs. Rapid-fire bursts can still
drop out-of-order messages via the per-client timestamp replay check
(unchanged from upstream) — left as-is to stay upstream-compatible.
Add the MeshCore Room Server role to ZephCore — a store-and-forward
shared message room. Clients log in with an admin or guest password
and post messages; the server pushes each new post to all other
logged-in clients (round-robin, per-client sync cursor, ACK + retry,
3-strike eviction).
Ported from upstream MeshCore's simple_room_server, structured as a
near-clone of RepeaterMesh so it reuses the proven ACL, region
filtering, CLI, adverts and telemetry; the post buffer + push engine
are the only net-new pieces:
- RoomServerMesh: PostInfo ring (MAX_UNSYNCED_POSTS=32), addPost,
pushPostToClient, getUnsyncedCount, processAck, onAckRecv, the
loop() push driver; room login (onAnonDataRecv parses the
sync_since cursor); posts/admin-CLI/keep-alive (onPeerDataRecv);
ADV_TYPE_ROOM advert; ServerStats wire layout; disable_fwd=1.
- main_room_server.cpp: event-loop entry (USB serial CLI, no BLE).
- Kconfig: ZEPHCORE_ROLE_ROOM_SERVER + ZEPHCORE_MAX_UNSYNCED_POSTS.
- CMakeLists role gating; boards/common/room_server.conf.
Post frame, SHA-256 ACK and login-reply layouts match upstream for
MeshCore app compatibility. Builds for gat562_30s
(FLASH 30.95% / RAM 32.46%). Not yet hardware-tested.