The RX-pulse/pin-hold interlock (s_tx_lit/s_rx_pulse_off/
zephcore_led_radio_hold_pin) was gated by HAS_TX_LED alone, so it never
compiled in for any PWM-capable board (HAS_TX_LED_PWM). Left two real
gaps:
- T096 and Wireless Tracker V2 alias heartbeat and TX activity to the
SAME physical PWM LED (ZEPHCORE_LED_PIN_SHARED's intent exactly), but
had none of the arbitration that protects every plain-GPIO board with
the same wiring -- a transmit and a heartbeat tick could still clip
each other on the shared PWM channel.
- All PWM boards, RAK3401 included, had no onPacketReceived() blink at
all: "set leds.radio rx" (or "all") silently did nothing on the
activity LED, since that whole code path only existed under
HAS_TX_LED.
Fixed by widening every "#if HAS_TX_LED" guard around this machinery to
"#if HAS_TX_LED_PWM || HAS_TX_LED", and adding a small tx_led_write(bool)
helper (mirroring heartbeat_led_write() in helpers/ui/ui_common.c) so the
shared interlock logic doesn't need to know which path is compiled in --
only the final write differs.
ZEPHCORE_LED_PIN_SHARED itself gained a PWM branch: true when
heartbeat-pwm-led and lora-tx-pwm-led alias the same DT node (T096, WT2),
same as it already was for the GPIO led0/lora-tx-led case. RAK3401's two
PWM LEDs are on distinct nodes, so it correctly stays unshared -- no
arbitration overhead there, but it does now get the RX blink like every
other PWM board.
The heartbeat side (helpers/ui/ui_common.c) needed no change: it already
checks zephcore_led_radio_holds_pin() unconditionally through
heartbeat_led_write(), from the LED brightness persistence commit earlier
today.
Verified building for heltec_t096/nrf52840 (shared PWM LED),
heltec_wireless_tracker_v2/esp32s3 (shared PWM LED), and rak3401_1watt
(two independent PWM LEDs, confirms ZEPHCORE_LED_PIN_SHARED stays 0
there).
Single LED (P0.28) doubles as heartbeat, message flash, shutdown flash
and LoRa TX indicator, all sharing the same physical pin. Adds one PWM
channel on that pin (heartbeat-pwm-led/lora-tx-pwm-led aliases) and a
single 0-100 brightness knob (get/set led) covering every one of those
events at once, deliberately RAM-only: resets to a 10% default on every
reboot regardless of CLI changes made in a previous session. The
existing persisted on/off gate (set leds on|off) is untouched.
Boards without heartbeat-pwm-led/lora-tx-pwm-led aliases keep their
current plain-GPIO behavior unchanged.
ZephCore writes the year as two BCD digits with no century, as y % 100.
A time before 2000, accepted by a node whose clock had no time yet, was
stored as a year near the end of the century and restored at the next
boot above the 2025 floor: `time 900000000` (1998) came back as 2098.
rtc_time_block() now fails outside 2000-2099, and zephcore_rtc_save()
then logs and writes no time, for every chip; the chip keeps the time it
has. An RV3028 write retry stops if its run-on time leaves the range.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rtc_time_block() wrote the weekday as a number 0-6 (Sunday 0) for every
RTC. The RX8130CE's WEEK register (13h) is one-hot: Epson ETM50E-10
14.1.2, Table 15, p. 27, Sunday 01h to Saturday 40h, with no more than
one bit set. So each sync wrote Sunday as no day, Monday as Sunday, and
Wednesday, Friday and Saturday as two days at once. ZephCore never
reads the weekday back, so its time and date were unaffected.
A new boolean descriptor property, weekday-one-hot, is set on the
RX8130CE node in rtc-i2c.dtsi, and rtc_time_block() writes BIT(dow)
where it is set. That is the encoding of Zephyr's rtc_rx8130ce.c. The
DS3231, PCF8563 and RV3028 keep 0-6, as Zephyr's drivers for them
write it. The binding and comments no longer call the whole block BCD.
meshtracker_x1's YSN8900 sets it too. The board already uses the RX8900
register map for it, and the RX8900's WEEK register (03h) is one-hot:
Epson ETM45E-11 8.2.2, Table 15, p. 16. The YSN8900's own data sheet
has no register map, and its programming guide is not available.
The same defect and fix as meshcore-dev/MeshCore#3550.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The RAK4631 overlay declared no zephcore,rtc-i2c candidate, so a fitted
RAK12002 (an RV3028 at 0x52) was never probed or written, and every
power cut started the node at 1970. Declaring it is not enough: the
RV3028 ships with backup switchover disabled (EEPROM 37h BSM = 00), so a
power cut still loses the time.
The overlay now declares the RV3028 with the zero-mask rtc-i2c.dtsi uses
for it, and a new optional rv3028-eeprom-config descriptor property
holds (register, mask, value) triplets for its EEPROM configuration
registers 35h-37h. The RAK4631 sets direct switchover with the trickle
charger on (TCE, BSM = 01, TCR 3 kOhm), as the RV-3028-C7 Application
Manual (Rev. 1.4) 7.3 prescribes for a capacitor backup, FEDE set as
3.15.6 says it always should be, and CLKOUT off. The configuration has
to live in EEPROM: the chip reloads its configuration RAM from EEPROM at
power-on and every midnight (4.6.2).
The store runs only when a read identified the chip (RTC_FOUND, not an
adoption on a failed second read) and a BSF-checked time read just
before it showed no switchover. It follows 3.15.6 (BSM 00 or 10 for any
EEPROM access): EERD held, switchover off in RAM, each configured byte
compared against the EEPROM with a single-byte read and written only if
it differs, and a closing Refresh that restores the stored switchover
mode, read back on every boot. 36h, the factory trim, is never written.
Switchover is turned back on only once EEbusy reads 0; otherwise it
stays off until a retry or the daily refresh. A failed store sets the
config in RAM (also only once EEbusy reads 0). A failed or skipped store
is retried every 10 minutes, at most 3 times per boot, each retry
identifying the chip again first.
A switchover disables the chip's I2C mid-transfer (4.2), so at a node
with rv3028-eeprom-config the time is read at boot and written on each
sync with BSF cleared before and checked after. A read that saw a
switchover gives no time. A write sets year 00h first and the real year
last, so a power loss cutting it between its first and last byte leaves
year 2000 and the next boot takes no time from it, and a write not
confirmed is repeated every 5 s, up to 12 times, with the time run on.
All of the RV3028 code is compiled only when some node carries
rv3028-eeprom-config, so other boards, thinknode_m1 among them, build
the same RTC object as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rtc_probe() decided from one read whether the device at a declared
address was the RTC its descriptor expects: a valid BCD block including
hours and year, or the power-loss flag set or unreadable. That skipped
real RTCs whose year byte or hours another firmware left out of range
(a year byte of A0 or more; 12-hour mode reads PM hours with bit 5 set),
ruled nothing out by identity, and adopted an erased EEPROM through its
0xFF "status".
- An optional zero-mask descriptor property holds the time-block bits a
part's data sheet shows as 0. rtc-i2c.dtsi sets it for 0x68 (DS3231
19-5170 Rev 10 and DS3232 19-5337 Rev 5, Figure 1, p. 11; DS1307 Rev
3/15, Table 2, p. 8; 00h bit 7 left out, the DS1307's clock-halt bit),
the RV3028 (App Manual Rev 1.4, 3.2, p. 12) and the RX8130CE
(ETM50E-10, 13.2.1 Table 12, p. 22). The PCF8563 gets none: its data
sheet (Rev 11.1, Table 4, p. 10) marks unused bits "not relevant".
- One read rules a device out if a masked bit is set, or if seconds,
minutes, date or month are out of range while the power-loss flag does
not read as set. The year and hours are no longer identity checks.
- A device is passed over only when two reads each rule it out. A failed
first read means nothing is there; a failed second read does not count,
and with no clean read the chip is adopted for write-back but gives no
time. An all-0xFF second read rules the device out.
- An all-0xFF first read is skipped, and the first save probes once more,
since a real RTC can power up that way.
- A time is restored only from a clean read whose fields, hours and year
are all valid.
The decision is one function, rtc_identify(). The same rules, with a
few agreed differences, are in meshcore-dev/MeshCore#3544.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An nRF UARTE receiver starts at boot and never stops. The GPS UART
suspend in standby/off was limited to boards with GPS power control,
because suspending into a module that is still streaming NMEA could
hang the main thread waiting for RXTO (RAK3401, 1.16.6). Patch 0010
bounds that wait (4 ms, then the peripheral is disabled and the events
cleared) and resume re-arms from a clean state, so gps_power.cpp drops
the requirement: gat562_30s, muziworks_r1neo, rak3401_1watt, rak4631
and xiao_nrf54l15 now release the UART too.
Stress-tested with a temporary hook on the T1000-E, GPS streaming NMEA
at 115200: 900 suspend/resume cycles at random points of the stream,
no failure, slowest suspend 640 us (the bounded fallback never ran),
NMEA callbacks back at 1 Hz after every round.
xiao_nrf54l15 gets an optional gnss-nmea-generic on uart21 (D6/D7,
9600), as the RAK4631 and Heltec V3 have: a user-fitted GPS works on
the release image, and with nothing fitted the port, enabled upstream
and unused until now, no longer keeps its receiver armed. On the bench
unit: boots, GPS acquiring, 10 x gps off/on and two reboots clean.
Builds: rak4631 repeater and companion, rak3401_1watt, gat562_30s,
muziworks_r1neo, xiao_nrf54l15 repeater and companion.
ESP32 light sleep (devdocs/lld/13-power-management.md)
Light sleep on the S3 repeaters never worked as designed, for two
independent reasons:
- No S3 repeater ever slept. Every one carries WiFi OTA, the WiFi
driver starts WiFi at boot (NULL mode), and a started WiFi library
holds its modem PM lock. Measured on the XIAO: zero sleep entries
with WiFi compiled in. helpers/pm_esp32_wake.c now stops WiFi at
boot on PM builds; `start ota` and a station connect start it again
(verified: AP up, sleep blocked for the session, sleeping after
`stop ota`).
- DIO1 was not a wake source. GPIO_INT_WAKEUP means EXT1 in Zephyr's
ESP32 driver: RTC pads only, one trigger polarity for all pins on
the S3, and pads left on the RTC mux after every sleep. Since 1.17.1
pm_esp32.overlay also armed the active-low user button; gpio-keys
configures first, so the driver refused the active-high DIO1.
The new helper arms the radio IRQ line and the user button (sw0) as
per-pin level GPIO wakes (ESP-IDF gpio_wakeup_enable, any pad) for the
length of each sleep, masks their CPU interrupt while in level mode,
and re-raises the interrupt a sleep swallowed through status_w1ts, so
the radio's dio1_isr and gpio-keys run as usual. EXT1 is no longer
used: patch 0003 configures DIO1 as a plain input again and drops the
d7cc4cb pad-restore notifier (preamble section rewritten). Non-RTC
DIO1 boards (XIAO GPIO39, Station G2 GPIO48) can now light-sleep.
Each light sleep is also bounded by the next esp_timer alarm
(pm_policy_next_custom_ticks, CONFIG_PM_CUSTOM_TICKS_HOOK), as ESP-IDF
does; the BLE controller's modem-sleep wake timer lives there.
Light sleep isolates every pad that is not held, RTC or digital, while
the radio keeps receiving. Holds (ESP32_GPIO_SLEEP_HOLD_EN, inert
without PM) now live in each board's own files: NSS and RESET on all
PM-capable boards, VFEM_Ctrl (GPIO7) and Vext on V4/V4.3/V4 R8/WTv2,
CPS on V4, RXEN on the XIAO. The shared overlay keeps only &rtc_timer
and the GPIO wake source; the wrong RTC_PERIPH explanations are gone.
Also:
- `powersaving` (upstream's command and replies) gates light sleep;
light-sleep builds default to on, and a stored 0 without the new
pwr_sav_set marker (the field did nothing before) becomes on once.
`powersaving off` persists and keeps the node (and its USB) awake.
- `get pm`: asleep %, sleeps, wake causes, per-pin wake counts.
- The console window reopens on every user-button press.
- XIAO ESP32-S3: GPIO21 is both the kit's user button and the board's
led0; the heartbeat drove it low and every sleep woke as a button
press. led0 is deleted, the button gets a pull-up.
- Station G2: button active-low with pull-up, as upstream reads it
(was active-high). Untested on hardware.
Verified on the XIAO ESP32-S3 as a light-sleep repeater on the live
mesh (repeat off, 2 dBm) against a T1000-E on the same channel: awake
20 min, ratio of floods heard 0.95; asleep 30.5 min (96.6 % of
uptime), 119 vs 119, ratio 1.00. TX: sent == flood + direct. Heltec
boards: build-only (no unit on the bench).
Companion light sleep stays off by default; the esp_timer bound makes
it possible. Spike on the XIAO (pm_esp32.conf by hand, BLE only): a
held connection survives, 35 min with a poll every minute and no
miss, the SoC asleep ~91 % between connection events, LoRa RX 130 vs
~130 on the T1000-E. Pairing while asleep fails; USB CDC and UI still
need locks before companions can ship with it.
Flash wear (storage)
The companion marked contacts dirty on is_new, but upstream's is_new
means "not in contacts[]": auto-add declined the node (type filter,
hop limit, full table). Every advert from such a node forced a 5 s
full rewrite of an unchanged contacts3, while a contact that really
was just added only got the liveness deadline. onAdvertRecv now
snapshots the stored record first. is_new never dirties. An addition
or a changed name, type or position flushes in 5 s, and a plain
re-advert waits the liveness deadline, now 1 h instead of 10 min.
A new weak hook, zephcore_persist_before_off(), flushes the deferred
writes before every clean reboot and power-off. It is called from
zephcore_power_off(), the ZephyrBoard reboot, bootloader and BLE-OTA
paths, the UI save-and-restart, the joystick restart and wifi_ota.
Off the main thread it posts MESH_EVENT_PERSIST and waits up to 5 s.
Before this, only CMD_REBOOT flushed.
The repeater saved its ACL on every admin login. It now saves only
when the role or secret changed; the secret is the ECDH of two fixed
keys, so re-logins store nothing new. The room server likewise skips
the lazy write unless the role, sync_since or secret changed.
zephcore_prefs_json_save() compares the serialised JSON with the file
and skips an identical rewrite. Not tested on hardware.
Builds and tests: role_builds.sh 10/10; the ESP32 PM set on the
combined tree (five Heltec repeaters, V4 R8, Station G2, XIAO repeater
and companion) 9/9, repeaters at 69-71 % dram0; board_manifest check
clean; host tests quick 114/114. heltec_companion dram0 at 99.80 %
(776 B left, +88 B for both changes together).
- 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
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.
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>