- retire 0018 (upstream 67151dfaf73 locks the shared multi-heap) and the
hal_espressif PSRAM-realloc backport (in the new hal pin)
- 0001: add LR1110/LR2021 to LORA_MODULE_BACKEND_NATIVE's depends on;
upstream 1263266b615 gated it on its own radios and LR boards silently
fell to loramac-node
- ble: build the persisted gatt_peer settings key ourselves in the old
format; bt_addr_le_to_str() changed to "P:AA:.." and would have orphaned
every key and sent every bonded phone a Service Changed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- boot_info: label the ESP32 reset reasons Zephyr's hwinfo driver maps to 0
(USB-Serial-JTAG/JTAG reset -> DEBUG, power glitch -> BROWNOUT, CPU lockup,
eFuse -> HARDWARE); get pwrmgt.bootreason no longer says Unknown after a flash
- mqtt: the uplink credentials struct moves to adapters/mqtt/uplink_creds.h,
so the adapter no longer includes an app header (layout unchanged: the
struct is persisted raw)
- gps: one off-levels routine for standby/off and System OFF; the sleep line
stays asserted in standby and drops at System OFF, as upstream's
sleep_gps/stop_gps/powerOff; System OFF now asserts reset before cutting
GPS_EN
- doom: sprites solid with a one-pixel black halo, boss drawn 1.5x on the
floor; the L2 boss was invisible from the spawn on 1-bit panels
- admin screen: telemetry via upstream LPPDataHelpers (ported; fixed its
1-byte humidity size), every field, per-channel, signed current
(helpers/ui/telemetry_text)
- battery %: both UIs show ZephyrBoard::getBattPercent() (discharge
curve / fuel gauge) instead of a linear 3.0-4.2 V estimate
- app/PowerPolicy: low-battery auto-shutdown + v-contact alert +
autoshutdown CLI on every companion (headless too); under 2000 mV
counts as no battery; default 3200 mV, stored 3300 migrated once
- one time_sync.c for both UIs; ui_push_radio_state() for all roles
- repeater admin screen in its own file; handleInput split per mode
- dead UI code removed; companion help fits one reply frame
- gps: servers honour gps_enabled at boot (was lost on reboot; zc.gps_set
upgrades older prefs to on once); sleep lock only while acquiring;
gps off works on boards without a power line (UART sleep); UI toggles
and fixes handled on the main thread; configure only after a good init;
observer parks its GPS; one interval clamp (clampGpsInterval); one
nav-mode knob (room servers on Air530Z now stationary); GNSS UARTE from DT
- gps: split into ZephyrGPSManager / gps_power / gps_module_cfg
(+gps_internal.h); dead driver branches, gps_get_diag, sats_* removed
- clock: ZephyrRTCClock writes the hardware RTC itself (clkreboot now
resets it); MESH_EVENT_RTC_SAVE gone
- power: one power-off path (zephcore_power_off, ZephyrBoard::powerOff),
poweroff/shutdown CLI, get pwrmgt.*, crash record kept across reboot,
battery reading cached 10 s, fuel gauge on the current API
- telemetry: upstream SensorManager + CayenneLPP (rounded to nearest),
one builder for all roles; per-sensor channels, MCU temp always,
guest gating and inverse mask on servers; sensor list/get/set work;
BME280/BMP280 also probed at 0x77
- CLI: gps in upstream's form, get gps detail, gps sync
- doom: each turn shrank the view vectors (cos = 1 - ROT/8, scale
~0.993, plus fixed-point truncation), so the level seemed to grow
and after ~10 turns a zero wall height was divided by: usage fault,
reboot. The view is renormalised to unit length after every turn;
the ray step and wall height are clamped
- doom: the L1 demon spawned inside a wall tile, so it could not be shot
and the portal never opened; it now mirrors the imp at (11,4)
- doom: the 60 s UI lock engaged mid-game, so the exit tap went to the
lock screen; the Doom tick now keeps the lock and auto-off timers
alive. The user button no longer also fires (joystick press does)
- doom: sprites keep a brightness floor at distance (L2 boss is still
hard to see; open)
- get mem (MEM_STATS builds): walk threads with k_thread_foreach_unlocked;
the stack scan under the thread-list spinlock held IRQs off long
enough to trip the BLE controller's radio-ISR latency assert
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
- BaseChatMesh/ContactInfo/ChannelDetails restored to upstream text, ZephCore
changes fenced; added to the Shared-file list
- upstream names: handleCmdFrame, writeOKFrame/writeErrFrame,
addToOfflineQueue, queueMessage; dead dequeue removed
- handleCmdFrame split into handleCmd<Name>() in upstream order;
CMD_SEND_PATH_DISCOVERY -> _REQ
- CompanionMesh::handleCommand as the single CLI entry point; over-the-air
TXT_TYPE_CLI_COMMAND from flag-0x10 contacts (upstream command set only)
- V-contact moved to app/VContact.cpp, protocol constants to
app/CompanionProtocol.h; comment diet (image-identical)
- 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
- compile Semtech lr11xx_driver v2.7.0 from modules/lib/lora-basics-modem
(driver layer only) instead of the vendored fork; HAL moves next to the
driver (+abort_blocking_cmd); driver no longer builds app-tree sources
- split lr11xx_dio1_work_handler into per-IRQ helpers (CCN 46 -> 12)
- zc_lora_timing.h: preamble grace, payload deadline, 24-bit saturation,
shared with sx126x (patch 0003) and host-tested; sx126x CAD_RX Rx bound
now saturates instead of wrapping (SF12/BW7.81)
- semtech,lr1110 binding includes upstream lr11xx-base; boards use
upstream property names (irq-gpios, tcxo-voltage, rfsw-*)
- drop dead lr11xx_get_random/get_chip_temp_c; correct the IRQ-clear
invariant wording; comment diet (image-identical)
- 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.
- boards/**/zephcore.yml: one manifest per board/preset (release roles,
variants, catalog metadata, capabilities); scripts/board_manifest.py
validates and serves them to build.sh, gen_provider_catalog.py and
docs/supported_boards.md
- cmake/zephcore_board.cmake: board dir -> platform, shared by app and
sysbuild; removes the board-name regex lists and 7 separate globs
- conf chain: exact-name role/debug classification checked against
Kconfig, idempotent across non-pristine reconfigures, version string
single-sourced (BT DIS rev generated), day-granular build epoch
- Doom easter egg: Kconfig default for the wio_tracker_l1 companion
instead of a build.sh-only flag
- catalog: maker for the LR2021 EVK (KeyError before); CI installs PyYAML
- user -DEXTRA_DTC_OVERLAY_FILE overlays now take precedence over derived overlays
Verified: identical .config, DTS and source lists for 97 board/role
configs; identical images for 4 reference builds; identical build.sh
command/artifact set and catalog content.
hwinfo_get_reset_cause() reads a latched register. It was read inline in
main_companion.cpp and cleared there, so the repeater, room server and
observer never read it and never cleared it: their flags carried into the next
boot, and whichever role read first defined what any later reader would see.
helpers/boot_info.c reads it once in a SYS_INIT at POST_KERNEL, logs it,
stores it and clears the register. That runs before main() on every role.
zephcore_boot_reset_cause() hands out the stored copy and reports false where
the platform has no support, which a caller can distinguish from a cause of 0.
The clear's return is discarded deliberately: ESP32 has no
hwinfo_clear_reset_cause() implementation, so the weak stub returns -ENOSYS.
The capture is still correct there; only the next boot's reading is not.
Logging in boot_info_init() rather than only in the consuming role matters
because the clear now happens everywhere. Without it a repeater that rebooted
on WATCHDOG or LOCKUP would have the evidence cleared and recorded nowhere,
where before it survived in the register for whoever read it over SWD.
main_companion.cpp reads the stored copy and renders through a table with one
row per RESET_* bit, so a cause with no label is no longer logged as bare hex.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gps_power_control() mixed the two GPIO conventions on the same pins. It
configured lines with GPIO_OUTPUT_HIGH / GPIO_OUTPUT_LOW, which Zephyr defines
as physical levels, then drove the same lines with gpio_pin_set_dt(), which is
logical. On a line declared GPIO_ACTIVE_LOW those are opposite, so the first
call asserted the reverse of every call after it.
Two lines are actually broken by this.
gps-enable, on the boards declaring it active-low. Most of them reach the block
named for the T1000-E rather than the "simple boards" path, because
HAS_T1000_GPS_CONTROL is (HAS_GPS_VRTC || HAS_GPS_RESET || HAS_GPS_SLEEP) and a
bare gps-reset alias is enough to route a board there. That block drives the
enable physical HIGH unconditionally on every power-on, and power-off lands on
physical HIGH too, so the line is de-asserted in both states and the module is
never powered at all.
gps-reset, on the six boards declaring it active-low: heltec_t096, heltec_t1,
heltec_t114, thinknode_m1, heltec_wifi_lora32_v4 and _v43. The documented
power-on sequence is "assert reset, hold 10 ms, release", written as
GPIO_OUTPUT_HIGH then gpio_pin_set_dt(0). On an active-low line both of those
are physical HIGH, so the pulse never asserts and the module starts
uncontrolled -- the condition the power-off comment says this code prevents.
heltec_wifi_lora32_v4's overlay documented the no-op rather than fixing it;
that comment is updated here, with _v43's copy.
vrtc, sleep, rtcint and resetb are converted too. Every board declaring them is
active-high, so those change nothing today; they are converted so the root
cause does not survive in four more places.
Converting is safe by construction. gpio_pin_configure() flips the init level
only when both GPIO_OUTPUT_INIT_LOGICAL and GPIO_ACTIVE_LOW are set, then
strips INIT_LOGICAL, so on an active-high pin GPIO_OUTPUT_ACTIVE produces a
flags word byte-identical to GPIO_OUTPUT_HIGH. Behaviour changes only where a
line is declared active-low, which is only where it is currently wrong.
gps_power_off_for_shutdown() had the same inversion and it mattered most there:
on the active-low boards the old physical LOW asserted the enable, powering the
GPS on as the device entered System OFF rather than powering it down. Its
header comment also described a narrower scope than the function has, naming
only the power enable where it configures vrtc, reset, sleep, rtcint and
resetb as well; that is corrected here.
Comments that described a level this change now drives logically are reworded
to asserted/de-asserted. Comments stating genuine electrical facts, such as a
pin floating LOW without its pull-up, are left as they are, since rewording
those would make them wrong.
Verified on hardware on a Heltec V4-R8, whose gps-enable is active-low: with
this change the boot log reports "GPS power GPIO configured, set ACTIVE" and
the L76K streams NMEA (210 sentences in 35 s), where without it the log reports
"set HIGH" and the module never powers on. Also built for heltec_wifi_lora32_v4
and _v43 (active-low enable and reset), thinknode_m1 and heltec_t114
(active-low reset) and t1000_e (active-high throughout).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The V4-R8 is an ESP32-S3R8 with 8 MB octal PSRAM, where V4 and V4.3 are S3R2
with 2 MB quad. Octal PSRAM claims GPIO33-37, which moves Vext 36->40, the LED
35->46 and VGNSS_Ctrl 34->42, and drops ADC_Ctrl and GNSS_RST. CTX binds as
lna-bypass-gpios rather than tx-enable-gpios, so the board is a fourth one
where set radio.fem.rxgain works; the CLI reference and the register-patch help
text say so.
The allowlist change is needed for the board to be added correctly. CMake
MATCHES is a substring search, so the existing heltec_wifi_lora32_v4 entry also
matches heltec_wifi_lora32_v4_r8 and the new board would get CONFIG_PM=y
without anyone deciding it should, on a path pm_esp32.conf marks as not yet
validated on hardware. Anchoring preserves the five boards that were intended
to match, including heltec_wireless_tracker_v2, which previously qualified only
by substring and is now listed explicitly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>