Pieter ViljoenandClaude Opus 5 bb84f54191 drive every gps control line with logical gpio semantics
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>
2026-09-19 10:10:30 -07:00
2026-09-03 14:46:26 +02:00
2026-09-18 21:25:19 +02:00
2026-07-21 15:04:25 +02:00
2026-09-15 13:50:50 +02:00
2026-07-21 14:43:39 +02:00
2026-09-18 21:25:19 +02:00
2026-09-18 21:25:19 +02:00
2026-09-14 09:14:46 +02:00
2026-08-24 21:43:40 +02:00
2026-09-15 13:50:50 +02:00

ZephCore — MeshCore for Zephyr RTOS ZephCore logo

A port of MeshCore LoRa mesh firmware from Arduino to Zephyr RTOS. Aiming for full protocol compatibility with the original Arduino firmware and the MeshCore mobile apps.

Why Zephyr?

The Arduino version uses a loop(). This port replaces that with Zephyr's event-driven primitives (k_event_wait, k_poll, k_msgq), so the CPU sleeps in WFI (Wait For Interrupt) between events.

Other benefits:

  • Proper driver model -- LoRa, GNSS, display, sensors, and BLE all use Zephyr subsystem drivers rather than Arduino libraries
  • Hierarchical build configuration -- board-specific settings compose cleanly via Kconfig and devicetree overlays
  • DFU support -- generates Arduino compatible zip packages for OTA updates and UF2 binaries for drag-and-drop flashing
  • Back and forth compatible -- Adapted to softdevice and adafruit's bootloader, so no bootloader re-flashing required.

Supported Boards

nRF52840, ESP32, nRF54L15, MG24, and STM32WL boards, covering SX126x, LR1110, LR2021, and SX127x radios. ZephCore also runs as a native Linux process on SBCs (Femtofox / Luckfox Pico Mini, Raspberry Pi + RAK6421 HAT) with a real SX1262 on SPI/GPIO and the companion app connecting over TCP — see LINUX_NATIVE.md.

For the full board list with exact west build -b strings, radios, and hardware notes, see the supported boards list and the Board Porting Guide.

Device Roles

  • Companion (default) -- connects to MeshCore mobile apps via BLE/USB. Contacts, channels, offline message queue.
  • Repeater -- forwards packets, configured via USB serial CLI. See the Repeater CLI Command Reference for all available commands.
  • Room Server -- store-and-forward shared message room (a "BBS"). Clients log in with an admin or guest password and post messages; the server pushes each new post to every other logged-in client. No BLE; configured via the same USB serial CLI as the repeater.
  • Observer (ESP32 only) -- listen-only node that publishes received LoRa packets to MQTT over WiFi STA. Configured at runtime via serial CLI.

Building

Prerequisites: Zephyr SDK >=1.0.1 (!) and west installed (required by Zephyr 4.4.0, which is pinned in west.yml).

Optional: adafruit-nrfutil to allow DFU zip generation for OTA updates on nRF52.

# Initialize workspace (first time only)
cd <cloned folder>
west init -l zephcore
west update

# Companion (production — default, no extra conf needed)
west build -b wio_tracker_l1 zephcore --pristine

# Companion (debug logging)
west build -b wio_tracker_l1 zephcore --pristine -- \
  -DEXTRA_CONF_FILE="boards/common/debug.conf"

# Repeater
west build -b rak4631 zephcore --pristine -- \
  -DEXTRA_CONF_FILE="boards/common/repeater.conf"

# Repeater (debug logging)
west build -b rak4631 zephcore --pristine -- \
  -DEXTRA_CONF_FILE="boards/common/repeater.conf;boards/common/debug.conf"

# Repeater with packet logging (clean RAW/RX/TX lines only, no debug spam)
west build -b rak4631 zephcore --pristine -- \
  -DEXTRA_CONF_FILE="boards/common/repeater.conf;boards/common/packet_logging.conf"

# ESP32 repeater + WiFi AP HTTP OTA (requires sysbuild for MCUboot)
west build -b xiao_esp32s3/esp32s3/procpu zephcore --pristine --sysbuild -- \
  -DEXTRA_CONF_FILE="boards/common/repeater.conf;boards/common/wifi_ota.conf"

# Room Server (store-and-forward BBS, USB CLI)
west build -b rak4631 zephcore --pristine -- \
  -DEXTRA_CONF_FILE="boards/common/room_server.conf"

# Observer (ESP32, listen-only WiFi+MQTT)
west build -b xiao_esp32c3 zephcore --pristine -- \
  -DEXTRA_CONF_FILE="boards/common/observer.conf"

# Formatter (factory-reset utility)
west build -b wio_tracker_l1 zephcore/tools/formatter --pristine

# BLE debug logging (debug.conf enables logging; the flag raises the BLE adapter to DBG)
west build -b rak4631 zephcore --pristine -- \
  -DEXTRA_CONF_FILE="boards/common/debug.conf" -DCONFIG_ZEPHCORE_BLE_LOG_LEVEL_DBG=y

Output binaries are in build/zephyr/ -- .hex, .uf2, and DFU .zip as applicable.

Platform notes:

  • ESP32 boards require west blobs fetch hal_espressif once before the first build.
  • ESP32 default builds use CONFIG_ESP_SIMPLE_BOOT (no MCUboot) -- west flash writes a self-contained zephyr.bin. The wifi_ota.conf overlay is the exception: it requires MCUboot, so add --sysbuild.
  • MG24 requires west blobs fetch hal_silabs and pyocd (west flash --runner pyocd).
  • nRF54L15 requires --no-sysbuild (no MCUboot support yet).
  • Heltec V3 routes console/shell to uart0 -- use the UART serial port for logs/CLI.

Always use --pristine when switching boards or roles.

Architecture Overview

Mobile App  <--BLE (NUS)--> [ Companion ]  <--LoRa-->  Mesh Network
                                  |
                            k_event_wait()
                           /      |       \
                    LORA_RX   LORA_TX_DONE  BLE_RX

All code paths are event-driven. The CPU sleeps in WFI between events.

  • LoRa RX: Zephyr driver callback enqueues to a ring buffer and signals the mesh event loop
  • LoRa TX: A dedicated thread blocks on k_poll(), restarts RX on completion, then notifies the mesh loop
  • BLE: NUS write handler enqueues to k_msgq and signals the mesh loop; TX uses bt_gatt_notify_cb() chaining
  • USB: CDC-ACM with V3 binary framing protocol, frame timeout recovery
  • Main loop: k_event_wait() blocks until work arrives; housekeeping runs every 5s

Key Differences from Arduino

Arduino Zephyr
Idle behavior Cooperative loop; CPU busy-waits unless board.sleep() called explicitly k_event_wait(K_FOREVER) yields to idle thread → WFI between events
LoRa TX completion ISR sets flag, polled in loop() via isSendComplete() ISR signals k_poll_signal, dedicated thread blocks on k_poll()
BLE transport Platform-specific (ESP-IDF BLE, Adafruit nRF52 lib) Unified bt_gatt API across all SoCs
LoRa driver RadioLib (userspace SPI bit-bang) Zephyr subsystem driver (DTS-configured, kernel-managed SPI)
Configuration platformio.ini + variant.h per board Kconfig + devicetree overlays, hierarchical config inheritance
Threading Single loop() + ISRs Explicit threads (main mesh, TX wait) + system work queue

Adaptive Contention Window (ZephCore-only)

Arduino MeshCore uses three static delay knobs (txdelay, rxdelay, direct.txdelay) that add the same retransmit jitter regardless of local conditions. In a linear chain of repeaters where each only hears its neighbor, this adds latency for zero benefit. In dense areas with 50+ neighbors, the same value may be too low to avoid collisions.

ZephCore replaces all three with a self-tuning system based on observed retransmit contention:

  1. Dupe counting: When a node retransmits a flood packet, it counts how many times it hears that same packet retransmitted by neighbors within a 10-second window. This is a direct measurement of local contention -- 0 dupes means a quiet linear chain, 15+ means a dense cluster.

  2. EMA-based delay sizing: Dupe counts feed into a rolling exponential moving average. This drives a sqrt-curve delay factor for future retransmits: near-zero delay in sparse areas, scaling up in dense ones. At ~15 dupes (moderate density), the factor matches the old Arduino default of 0.5. Jitter is double-capped at min(2000ms, 6·airtime) for repeaters.

  3. Reactive per-packet backoff: When a node is waiting to retransmit and hears a neighbor retransmit the same packet, it pushes its own TX back by a random amount (up to backoff.multiplier × airtime). This is real-time CSMA -- you hear the channel being used for your packet, so you defer. Capped at min(2000ms, 12·airtime) total reactive extension per packet.

Companion-originated floods use a smaller spread (up to min(1000ms, 3·airtime)) so user messages feel responsive while still avoiding collisions with nearby repeaters that may be in TX/RX.

Direct packets (routed, single next-hop) use minimal fixed jitter instead of adaptive delay, since only the next hop retransmits them.

The old txdelay, rxdelay, and direct.txdelay commands are still accepted for binary compatibility with Arduino prefs but are ignored -- the system is fully adaptive.

CLI commands:

  • get txdelay -- shows adaptive status: contention estimate and current flood delay factor
  • get/set backoff.multiplier -- per-dupe reactive backoff multiplier (default 0.2, range 0.0-2.0). Set to 0 to disable reactive backoff (EMA window still works). Higher values allow more per-packet deferral in dense areas.

Compatibility: Purely local behavior, no wire protocol changes. Works alongside Arduino MeshCore repeaters -- their retransmits are counted as dupes just the same.

Power Saving

  • LoRa RX duty cycle: chip-autonomous receive windowing (sniff mode) reduces LoRa RX current from ~10-15mA to ~3-5mA. Off by default; toggle at runtime with set rxduty on/off (SX126x only — unsupported on LR1110 due to a mid-preamble lock issue, and on SX127x). Window timing is auto-sized per SF/BW/preamble from the SX126x datasheet constraints.
  • Production by default: No logging, no asserts, reboot-on-fatal. Add debug.conf to enable logging.
  • GPIO-gated GPS: Powered on only during fix acquisition

Configuration

Key Kconfig options (set in board configs or via -D flags):

Option Default Description
CONFIG_ZEPHCORE_ROLE_COMPANION y BLE companion mode
CONFIG_ZEPHCORE_ROLE_REPEATER n USB CLI repeater mode
CONFIG_ZEPHCORE_ROLE_OBSERVER n Listen-only WiFi+MQTT mode (ESP32)
CONFIG_ZEPHCORE_RADIO_NATIVE y SX1261/SX1262/SX1268, LLCC68, STM32WL (Zephyr native sx126x driver)
CONFIG_ZEPHCORE_RADIO_LR1110 n LR1110/LR1120/LR1121 (custom driver)
CONFIG_ZEPHCORE_RADIO_LR2021 n LR2021 (custom driver)
CONFIG_ZEPHCORE_RADIO_SX127X n SX1272/SX1276/SX1278 (loramac-node backend)
CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE n RX duty cycle (sniff mode) boot default; runtime toggle via set rxduty on/off (SX126x only)
CONFIG_ZEPHCORE_APC y (compiled in, runtime OFF) Adaptive Power Control — enable at runtime via CLI
CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM 22 Initial TX power; lower for boards with external PA
CONFIG_ZEPHCORE_MAX_TX_POWER_DBM 22 Hard cap (radio adapter clamps above this)
CONFIG_ZEPHCORE_MAX_CONTACTS 350 Contact storage slots (companion)
CONFIG_ZEPHCORE_MAX_CHANNELS 40 Channel slots (companion)
CONFIG_ZEPHCORE_BLE_PASSKEY 123456 BLE pairing PIN
CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC 300 Companion GPS duty interval between fixes (seconds, 10–86400); always-on is a runtime setting (set gps duty 0)
CONFIG_ZEPHCORE_GPS_FIRST_FIX_TIMEOUT_SEC 300 Cold-start window for the very first fix (longer to allow almanac download)
CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC 172800 Repeater/room-server GPS duty interval boot default (48 h); 0 = always-on
CONFIG_ZEPHCORE_WIFI_OTA n WiFi AP + HTTP OTA updates (ESP32 repeaters, requires --sysbuild)
CONFIG_ZEPHCORE_REPEATER_UPLINK n Repeater WiFi+MQTT uplink (ESP32)
CONFIG_ZEPHCORE_PACKET_LOGGING n Arduino-compatible mesh packet logging
CONFIG_ZEPHCORE_HOUSEKEEPING_INTERVAL_MS 5000 Periodic maintenance interval

Project Structure

zephcore/
  src/              Core mesh engine (Mesh, Dispatcher, Packet, Identity, ContentionTracker)
  app/              Companion / Repeater / Room Server / Observer role implementations
  adapters/
    ble/            BLE NUS transport
    board/          GPIO, LED, power management
    clock/          Millisecond and RTC clocks
    datastore/      LittleFS filesystem wrapper
    gps/            GPS/GNSS drivers
    mqtt/           MQTT publisher (observer / repeater uplink)
    ota/            WiFi AP + HTTP firmware update server
    radio/          LoRa radio drivers (SX126x, LR1110, LR2021, SX127x)
    rng/            Random number generator
    sensors/        I2C sensor auto-detection
    transport/      TCP companion (native Linux) + serial companion (STM32WL)
    usb/            USB serial transport (CDC-ACM, V3 framing)
    wifi/           WiFi station client
  helpers/
    ui/             Shared UI plumbing (display, buzzer, multi-tap input)
    ui-button/      Single-button page UI
    ui-joystick/    5-way joystick UI (Wio Tracker L1)
  boards/
    nrf52840/       nRF52840 board overlays and configs
    esp32/          ESP32 (classic + C3/C6/S3) board overlays and configs
    nrf54l/         nRF54L15 board overlay and config
    mg24/           EFR32MG24 board overlay and config
    stm32wl/        Seeed LoRa-E5 board overlay and config
    linux_native/   native_sim presets (Femtofox, RAK6421)
    common/         Shared Kconfig fragments and devicetree includes
  lib/              Monocypher crypto library (Ed25519/X25519)
  patches/          Auto-applied patches to the Zephyr tree

License

MIT License — see LICENSE. Same license as the upstream MeshCore project, which this work relies heavily on (see the official meshcore repo).

A few vendored dependencies carry their own (compatible) licenses — see the notice at the bottom of LICENSE for details (Monocypher, Zephyr patches).

Logo by recrof, licensed under the WTFPL.

The comic below was stolen from https://www.twonks.co.uk/

TWONKS AI Design Logic

(FYI the whole project is 99,9% claude and cursor backed, relying heavily on the official meshcore repo and the work they do in it)

S
Description
No description provided
Readme MIT
397 MiB
Languages
C 68%
C++ 28.9%
CMake 1.8%
Python 0.9%
Shell 0.2%