27 KiB
ZephCore Architecture Guide
Comprehensive developer reference for the ZephCore codebase — a Zephyr RTOS port of the Arduino MeshCore LoRa mesh networking firmware.
Table of Contents
- Project Overview
- Directory Structure
- Layer Architecture
- Core Mesh Engine
- Radio Subsystem
- Application Layer
- Hardware Adapters
- UI Subsystem
- Build System
- Board Matrix
- Packet Format Reference
- BLE Protocol Reference
- Data Storage
- Key Call Flows
1. Project Overview
ZephCore is a LoRa mesh networking firmware running on Zephyr RTOS. It supports two device roles:
- Companion: BLE-connected device paired with a phone app. Full contact/channel/message management.
- Repeater: Autonomous headless relay node. CLI administration via authenticated mesh connections or serial UART.
Supported hardware: nRF52840, nRF54L15, ESP32-C3/C6/S3, EFR32MG24 — all with SX1262 or LR1110 LoRa radios.
Upstream Relationship
ZephCore is a port of Arduino MeshCore. The core mesh protocol (Mesh.cpp, Dispatcher.cpp, Packet.cpp, Identity.cpp, Utils.cpp) is shared code. Adapters (adapters/) bridge MeshCore's HAL interfaces to Zephyr APIs. Binary file formats (prefs, contacts, channels) are byte-compatible with Arduino MeshCore.
2. Directory Structure
zephcore/
├── src/ # Core mesh engine (shared with Arduino MeshCore)
│ ├── Mesh.cpp # Routing protocol: flood, direct, dedup, adverts
│ ├── Dispatcher.cpp # Packet queue, radio scheduling, CAD, duty cycle
│ ├── Packet.cpp # Packet serialization, hash, wire format
│ ├── Identity.cpp # Ed25519 key management, ECDH shared secrets
│ ├── Utils.cpp # AES-ECB encrypt, HMAC-SHA256, MAC
│ ├── StaticPoolPacketManager.cpp # Fixed-size packet pool (32 slots)
│ ├── main_companion.cpp # Companion mode entry point + event loop
│ └── main_repeater.cpp # Repeater mode entry point + event loop
│
├── include/mesh/ # Core interfaces (shared with Arduino MeshCore)
│ ├── Mesh.h, Dispatcher.h, Packet.h, Identity.h, Utils.h
│ ├── MeshCore.h # Constants: key sizes, packet limits
│ ├── Radio.h # Abstract radio interface
│ ├── Board.h, Clock.h, RNG.h, RTC.h # HAL interfaces
│ ├── LoRaConfig.h # Default radio parameters
│ ├── RadioIncludes.h # Compile-time radio driver selection
│ ├── SimpleMeshTables.h # Hash-based packet deduplication
│ └── StaticPoolPacketManager.h # Fixed pool allocator
│
├── adapters/ # Zephyr HAL implementations
│ ├── radio/ # LoRa radio drivers
│ │ ├── LoRaRadioBase.cpp/h # Shared TX/RX state machine, noise floor, AGC
│ │ ├── SX126xRadio.cpp/h # SX1262 adapter (native Zephyr driver)
│ │ ├── LR1110Radio.cpp/h # LR1110 adapter (patched Zephyr driver)
│ │ ├── radio_common.h # Shared radio types and constants
│ │ └── lr11xx/ # LR11xx low-level HAL (SPI, GPIO, Semtech SDK)
│ ├── ble/ZephyrBLE.cpp/h # BLE NUS service, pairing, TX congestion
│ ├── board/ZephyrBoard.cpp/h # Battery ADC, LEDs, reboot, bootloader
│ ├── clock/ # Millisecond uptime + software RTC
│ ├── datastore/ZephyrDataStore.cpp/h # LittleFS persistence
│ ├── gps/ZephyrGPSManager.cpp/h # GNSS state machine, power mgmt
│ ├── ota/wifi_ota.c/h # WiFi SoftAP + HTTP firmware upload
│ ├── rng/ZephyrRNG.cpp/h # Hardware CSPRNG with PRNG fallback
│ ├── sensors/ # I2C env sensors + power monitors
│ └── usb/ # USB CDC for companion + repeater
│
├── app/ # Application layer
│ ├── CompanionMesh.cpp/h # Phone-connected companion logic
│ ├── RepeaterMesh.cpp/h # Autonomous repeater logic
│ └── RepeaterDataStore.cpp/h # Repeater-specific persistence paths
│
├── helpers/ # Shared utilities
│ ├── BaseChatMesh.cpp/h # Contact/channel/message base class
│ ├── CommonCLI.cpp/h # Serial/mesh CLI command processor
│ ├── AdvertDataHelpers.cpp/h # Advertisement wire format encoder/decoder
│ ├── ClientACL.cpp/h # Authenticated client management
│ ├── TransportKeyStore.cpp/h # Region transport key cache
│ ├── RegionMap.cpp/h # Region-based flood filtering
│ ├── ContactInfo.h, ChannelDetails.h, NodePrefs.h # Data structures
│ ├── RateLimiter.h, IdentityStore.h, StatsFormatHelper.h
│ └── ui/ # Display, buzzer, input, pages, Doom game
│
├── boards/ # Board definitions
│ ├── common/ # Shared configs, DTS includes, partition layouts
│ ├── nrf52840/ # RAK4631, WisMesh Tag, T1000-E, ThinkNode M1, etc.
│ ├── nrf54l/ # XIAO nRF54L15
│ ├── esp32/ # LilyGo TLoRa C6, Station G2, XIAO ESP32-C3/C6
│ └── mg24/ # XIAO MG24
│
├── patches/ # Zephyr tree modifications
│ ├── zephyr/ # Unified diffs (SX126x extensions, GNSS, blobs)
│ └── zephyr-new/ # New files (LR11xx Zephyr driver, DTS bindings)
│
├── lib/ed25519/ # Vendored Ed25519 crypto library
├── tools/ # Formatter (flash erase) + LR1110 firmware updater
├── CMakeLists.txt # Build orchestration
├── Kconfig # All ZephCore configuration options
├── prj.conf # Base project config
└── west.yml # West manifest (Zephyr version pin)
3. Layer Architecture
┌─────────────────────────────────────────────────┐
│ Phone App (BLE NUS) or Serial CLI (USB CDC) │ External
├─────────────────────────────────────────────────┤
│ CompanionMesh / RepeaterMesh │ App Layer
│ ├── BaseChatMesh (contacts, channels, msgs) │
│ ├── CommonCLI (command processor) │
│ ├── ClientACL, RegionMap, TransportKeyStore │
│ └── UI (display, buzzer, buttons) │
├─────────────────────────────────────────────────┤
│ mesh::Mesh │ Routing
│ ├── Flood routing (path hash accumulation) │
│ ├── Direct routing (source-routed paths) │
│ ├── Packet dedup (SimpleMeshTables) │
│ └── Advert / ACK / Trace / Group dispatch │
├─────────────────────────────────────────────────┤
│ mesh::Dispatcher │ Scheduling
│ ├── TX/RX queue management │
│ ├── CAD (channel activity detection) │
│ ├── Duty cycle enforcement (EU ETSI) │
│ ├── RX delay (score-based prioritization) │
│ └── Maintenance (noise floor, AGC reset) │
├─────────────────────────────────────────────────┤
│ LoRaRadioBase │ Radio HAL
│ ├── SX126xRadio ──► Zephyr SX126x driver │
│ └── LR1110Radio ──► Custom LR11xx driver │
├─────────────────────────────────────────────────┤
│ Zephyr RTOS (kernel, drivers, BLE, FS, USB) │ Platform
└─────────────────────────────────────────────────┘
4. Core Mesh Engine
4.1 Packet Lifecycle
- Allocation:
StaticPoolPacketManager::allocNew()— fixed pool of 32Packetobjects (no heap) - Creation:
Mesh::createDatagram(),createAdvert(),createAck(), etc. - Queuing:
Dispatcher::sendPacket()→PacketManager::queueOutbound()with priority + scheduled time - Transmission:
Dispatcher::checkSend()→ CAD check → serialize →radio->startSendRaw() - Release:
PacketManager::free()after TX complete or processing done
4.2 Packet Structure
Wire format:
[header: 1B] [transport_codes: 0 or 4B] [path_len: 1B] [path: variable] [payload: variable]
Header byte:
Bits 0-1: Route type (0=transport_flood, 1=flood, 2=direct, 3=transport_direct)
Bits 2-5: Payload type (0=REQ .. 15=RAW_CUSTOM)
Bits 6-7: Version (0=v1)
Path_len byte:
Bits 0-5: Hash count (0-63 hops)
Bits 6-7: Hash size mode (0=1B, 1=2B, 2=3B, 3=reserved)
4.3 Payload Types
| Type | Value | Description |
|---|---|---|
| REQ | 0x00 | Encrypted request to peer |
| RESPONSE | 0x01 | Encrypted response from peer |
| TXT_MSG | 0x02 | Encrypted text message |
| ACK | 0x03 | 4-byte CRC acknowledgment |
| ADVERT | 0x04 | Signed identity advertisement |
| GRP_TXT | 0x05 | Group channel text message |
| GRP_DATA | 0x06 | Group channel data |
| ANON_REQ | 0x07 | Anonymous request (includes full pubkey) |
| PATH | 0x08 | Path return (source route exchange) |
| TRACE | 0x09 | Trace route |
| MULTIPART | 0x0A | Multi-ACK container |
| CONTROL | 0x0B | Control data (zero-hop) |
| RAW_CUSTOM | 0x0F | Raw custom data |
4.4 Routing
Flood routing: Packet has no destination path. Each relay node appends its identity hash to path[] and retransmits. Priority decreases with hop count. allowPacketForward() is the gatekeeper.
Direct routing: Packet carries a source-routed path[]. Each relay node checks if the first path hash matches its own identity, removes itself, and forwards. Path is built from previous flood packets' accumulated hashes.
Deduplication: SimpleMeshTables maintains a circular buffer of 128 packet hashes (8 bytes each, SHA-256 truncated) and 64 ACK CRCs. hasSeen() prevents duplicate processing and retransmission.
4.5 Dispatcher Scheduling
The Dispatcher runs a tight loop:
loop():
1. Check if current TX is complete → release packet, record airtime
2. Process next inbound packet from queue (if scheduled time has passed)
3. checkRecv(): Drain radio RX ring buffer
- Parse raw bytes into Packet
- Flood packets: compute RX delay based on score → defer or process immediately
- Direct packets: process immediately
4. checkSend(): Check outbound queue
- CAD: if channel busy, retry with random backoff (120-480ms)
- Duty cycle: if exceeded, defer 5 seconds (admin packets exempt)
- Final LBT check right before TX (closes timing gap)
- Serialize and transmit
RX Delay: Flood packets are delayed based on signal quality. High-quality signals (high SNR, short packets) get shorter delays, allowing closer/better relays to retransmit first. Uses a lookup table approximation of 10^(0.85 - score*0.1) - 1 multiplied by airtime.
Duty Cycle: Fixed 1-hour sliding window. Default 10%. Admin packets (REQ, RESPONSE, ANON_REQ, CONTROL) are exempt.
4.6 Maintenance Loop
Called every ~5 seconds from the main event loop:
- Noise floor calibration: EMA with alpha=1/8, jitter, threshold filtering, warmup
- RX mode watchdog: Flags error if radio stuck outside RX for >8 seconds
- AGC reset: Periodic warm sleep + recalibration (configurable interval, default off)
4.7 Encryption
- Peer-to-peer: ECDH shared secret (Curve25519) → AES-128-ECB encrypt → 2-byte HMAC-SHA256 MAC
- Group channels: SHA-256 of channel name → AES key
- Advertisements: Ed25519 signature over (pubkey + timestamp + app_data)
- ACKs: SHA-256(shared_secret + packet_hash) truncated to 4 bytes
5. Radio Subsystem
5.1 Class Hierarchy
mesh::Radio (abstract interface)
└── LoRaRadioBase (shared state machine, ring buffer, noise floor)
├── SX126xRadio → Zephyr native SX126x driver + sx126x_ext.h
└── LR1110Radio → Custom lr11xx_lora.c driver + Semtech HAL
Compile-time selection via CONFIG_ZEPHCORE_RADIO_LR1110 in RadioIncludes.h.
5.2 LoRaRadioBase State Machine
TX Flow:
startSendRaw()→ cancel RX → configure TX → copy to buffer → async send → wake TX wait thread- TX wait thread blocks on semaphore, polls completion signal (5s timeout)
- On DIO1 TX_DONE interrupt → signal raised → restart RX → update stats
RX Flow:
lora_recv_async()with callback- ISR writes to 8-slot SPSC ring buffer (drops NEW packet on overflow)
- Main thread drains via
recvRaw()
Config Caching: Avoids redundant lora_config() calls. Fast-path for TX↔RX transitions when only direction differs.
5.3 Noise Floor EMA
Algorithm in triggerNoiseFloorCalibrate():
- Random 0-500ms jitter to break phase-lock with interference
- 4 RSSI samples per tick, take minimum
- Threshold filter: reject samples ≥ floor + 14dB (after 8-tick warmup)
- Periodic bypass: every 8th tick accepts unconditionally
- EMA:
floor += round_nearest((sample - floor) / 8), clamped to [-120, -50] dBm
5.4 LR1110 Driver Errata Workarounds
The custom lr11xx_lora.c driver handles several LR1110 firmware bugs:
- CMD_ERROR IRQ: Benign error flag on several write commands — cleared silently
- RX buffer drift: Buffer base shifts 4 bytes per packet →
clear_rxbuffer()after every RX - Header error: Can shift buffer pointer → standby before RX restart
- DIO1 stuck HIGH: 5-cycle detection → full hardware reset + recovery
- RX duty cycle broken: 23-40% packet loss → disabled, always continuous RX
5.5 Default Radio Parameters
| Parameter | Default | Notes |
|---|---|---|
| Frequency | 869.618 MHz | EU 869.4-869.65 MHz band (500mW ERP allowed) |
| Bandwidth | 62 kHz | |
| Spreading Factor | 8 | |
| Coding Rate | 4/8 | |
| Preamble | 16 symbols | |
| TX Power | 22 dBm | Clamped by CONFIG_ZEPHCORE_MAX_TX_POWER_DBM |
6. Application Layer
6.1 Class Hierarchy
mesh::Mesh
├── BaseChatMesh (contacts, channels, messages, connections)
│ └── CompanionMesh (BLE protocol, phone sync, offline queue, ACK tracking)
└── RepeaterMesh (ClientACL, RegionMap, CLI, rate limiting, neighbor tracking)
6.2 CompanionMesh
Handles the binary BLE protocol with ~60 command opcodes. Key features:
- Offline queue: 16-frame circular buffer with peek/confirm pattern (survives BLE drops)
- ACK tracking: 8-slot table, computes expected ACK = SHA256(secret + hash)[0:4]
- Contact iteration: Streaming protocol with
lastmodfiltering for incremental sync - Lazy write batching: Dirty contacts/channels flush after 5-second delay
- 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
6.3 RepeaterMesh
Autonomous operation features:
- Authentication: Password-based login with timestamp replay protection (120s window)
- Permission levels: GUEST(0), READ_ONLY(1), READ_WRITE(2), ADMIN(3)
- Region filtering:
RegionMapwith transport key matching per flood packet - Rate limiting: 4 requests per 120s (discovery), 4 per 180s (anonymous)
- Neighbor tracking: 16-slot table with RSSI/SNR/name/timestamp
- Temporary radio params:
tempradiocommand with auto-revert timer
6.4 CommonCLI Commands
System: ver, board, reboot, start dfu, start ota, erase
Config: set name/freq/radio/tx/flood.max/password/..., corresponding getters
GPS: gps on/off/setloc/advert
Sensors: sensor get/set/list
Stats: stats-core/stats-radio/stats-packets, clear stats
Power: powersaving on/off
7. Hardware Adapters
7.1 BLE (adapters/ble/)
- Nordic UART Service (NUS) with AUTHEN permissions (forces pairing)
- Passkey-based MITM pairing, runtime configurable PIN
- TX congestion control: queue (12 frames) + overflow buffer + retry + timeout watchdog
- Fast/slow advertising switching with post-disconnect flap prevention
- DLE (Data Length Extension) to 251 bytes
- Interface coexistence: BLE vs USB, one active at a time
7.2 DataStore (adapters/datastore/)
- Internal: LittleFS on flash (
/lfs) - External: Optional LittleFS on QSPI (
/ext) with auto-migration - Prefs: 93-byte binary format, Arduino-compatible, field-by-field I/O
- Contacts: 152-byte records, stored on external flash if available
- Channels: 68-byte records (4 pad + 32 name + 32 secret)
- Blobs: Fixed-size records with LRU eviction by timestamp
7.3 GPS (adapters/gps/)
- State machine: OFF → ACQUIRING → STANDBY (with warm standby on supported hardware)
- 3 consecutive good fixes (≥4 satellites) required before reporting
- Multi-constellation: GPS+GLONASS+Galileo+BeiDou with fallback
- T1000-E: Complex 6-GPIO power sequencing with VRTC preservation
- Repeater mode: 48-hour wake interval for time sync only
- GPS time blocks phone time sync for 2 hours after last fix
7.4 USB (adapters/usb/)
- CompanionUSB: V3-framed CDC (little-endian 16-bit length prefix + payload)
- RepeaterUSB: Minimal CDC with 1200-baud DFU touch detection
- Both share message queues with BLE adapter (transport-agnostic mesh layer)
7.5 Board (adapters/board/)
- Battery ADC with optional regulator-gated voltage divider, 8-sample average
- UF2 bootloader entry via GPREGRET magic (0x57 = UF2, 0xA8 = BLE DFU)
- TX LED bracketing for LoRa transmissions
- Bootloader version detection via flash memory scan
8. UI Subsystem
8.1 Architecture
Event-driven, no dedicated thread. All UI work on Zephyr work queues.
Hardware buttons → Zephyr input subsystem → Longpress filter → Multi-tap filter
→ ui_input_cb() → page navigation / action dispatch → schedule_render()
→ render_work (50ms OLED / 200ms EPD debounce) → CFB framebuffer → display
8.2 Pages
Companion (11 pages): Messages, Recent, Radio, Bluetooth, Advert, GPS, Buzzer, Sensors, Offgrid, DFU, Shutdown
Repeater (3 pages): Status, Radio, Shutdown
8.3 Multi-Tap Input
Single button, up to 4 taps within 400ms window:
- 1 tap → Page next
- 2 taps → Flood advert
- 3 taps → Buzzer toggle
- 4 taps → GPS toggle (immediate, no delay)
8.4 Buzzer
Non-blocking RTTTL parser on dedicated work queue. Predefined melodies for startup, shutdown, messages, ACKs. 2-second safety watchdog auto-silences on work queue stall.
8.5 Doom Easter Egg
Wolf3D-style raycaster on OLED: textured walls, 2 enemy types, shooting, HUD. Bypasses CFB, writes directly to display. ~1.7KB RAM, ~5KB flash. Enabled via CONFIG_ZEPHCORE_EASTER_EGG_DOOM. Triple-press ENTER on Messages page to activate.
9. Build System
9.1 Config Layering
prj.conf (base: console, logging)
→ boards/common/zephcore_common.conf (ALL boards: BLE, crypto, FS, LoRa, sensors)
→ boards/common/<platform>_common.conf (nrf52/esp32/nrf54l/mg24 specifics)
→ boards/<mcu>/<board>/board.conf (board-specific pins, features)
→ [optional] repeater.conf, prod.conf (user extras, LAST = highest priority)
9.2 Key Kconfig Choices
- Role:
ZEPHCORE_ROLE_COMPANION(default) vsZEPHCORE_ROLE_REPEATER - Radio:
ZEPHCORE_RADIO_NATIVE(SX126x, default) vsZEPHCORE_RADIO_LR1110 - Features: Display, buzzer, buttons, multi-tap, Doom (auto-enabled from DT)
9.3 Patches
| Patch | Risk | Purpose |
|---|---|---|
| 0001-lora-lr11xx-build | LOW | Integrates LR11xx driver into Zephyr LoRa build |
| 0003-lora-sx126x-native | HIGH | ~400 lines: DIO1 work queue, errata workarounds, extension API |
| 0005-gnss-air530z-easy | MEDIUM | EASY ephemeris + removes PM (prevents deadlocks) |
| 0006-blobs-py | LOW | Fix west blobs fetch KeyError |
9.4 Flash Partition Layouts
nRF52840 SD v6: SoftDevice 152KB → App 696KB → LFS 128KB → UF2 48KB nRF52840 SD v7: SoftDevice 156KB → App 692KB → LFS 128KB → UF2 48KB ESP32 (4MB): Boot + App → NVS 8KB → LFS 184KB nRF54L15: MCUboot 64KB → App 1272KB → NVS 8KB → LFS 84KB
10. Board Matrix
| Board | SoC | Radio | GPS | Display | Buzzer | Buttons | QSPI | Max Contacts |
|---|---|---|---|---|---|---|---|---|
| RAK4631 | nRF52840 | SX1262 | gnss-nmea | - | - | - | - | 350 |
| WisMesh Tag | nRF52840 | SX1262 | Air530Z | - | Yes | 1+multitap | - | 350 |
| T1000-E | nRF52840 | LR1110 | AG3335 | - | Yes | 1+multitap | - | 350 |
| ThinkNode M1 | nRF52840 | SX1262 | Air530Z | EPD 200x200 | Yes | 2+multitap | 2MB | 510 |
| Wio Tracker L1 | nRF52840 | SX1262 | L76K | OLED 128x64 | Yes | 5-way joy | 2MB | 510 |
| Ikoka Nano 30dBm | nRF52840 | SX1262+PA | - | - | - | - | - | 350 |
| XIAO nRF54L15 | nRF54L15 | SX1262 | - | - | - | - | - | 450 |
| XIAO ESP32-C3 | ESP32-C3 | SX1262 | - | - | - | - | - | 300 |
| XIAO ESP32-C6 | ESP32-C6 | SX1262 | - | - | - | - | - | 300 |
| LilyGo TLoRa C6 | ESP32-C6 | SX1262 | - | - | - | - | - | 300 |
| Station G2 | ESP32-S3 | SX1262+PA | UART1 | OLED 128x64 | - | 1 button | - | 350 |
| XIAO MG24 | EFR32MG24 | SX1262 | - | - | - | - | - | 350 |
11. Packet Format Reference
Wire Format
Byte 0: Header
[1:0] Route type: 0=transport_flood, 1=flood, 2=direct, 3=transport_direct
[5:2] Payload type (see table in §4.3)
[7:6] Version (0=v1)
If transport route (bit 0 or both bits set):
Bytes 1-4: transport_codes[2] (2x uint16_t LE)
Next byte: path_len
[5:0] Hash count (number of hops)
[7:6] Hash size mode (0→1B, 1→2B, 2→3B)
Next N bytes: path[] (hash_count × hash_size bytes)
Remaining bytes: payload (type-specific)
Advert Payload
[32B pubkey] [4B timestamp LE] [64B Ed25519 signature] [0-32B app_data]
app_data format (AdvertDataHelpers):
Byte 0: type(3:0) | flags(7:4)
flags: bit4=lat/lon, bit5=feat1, bit6=feat2, bit7=name
[optional 8B: lat(float) + lon(float)]
[optional 2B: features1]
[optional 2B: features2]
[remaining: name string]
Encrypted Datagram (REQ/RESPONSE/TXT_MSG)
[1B dest_hash] [1B src_hash] [encrypted_payload + 2B MAC]
encrypted_payload (after AES-128-ECB decrypt):
For TXT_MSG: [4B timestamp] [1B txt_type] [text...]
txt_type: 0=plain, 1=cli_data, 2=signed_plain
12. BLE Protocol Reference
Frame Format
Raw binary over BLE NUS. Each frame: [1B opcode] [payload...]
Over USB CDC: V3 framing: [2B LE length] [1B opcode] [payload...]
Key Command Opcodes (phone → device)
| Opcode | Name | Payload |
|---|---|---|
| 0x01 | CMD_SEND_TXT_MSG | contact_idx + text |
| 0x03 | CMD_GET_CONTACTS | [optional 4B lastmod filter] |
| 0x06 | CMD_GET_SELF_INFO | (none) |
| 0x07 | CMD_SET_SELF_INFO | type + name + lat + lon |
| 0x0B | CMD_GET_MSG_WAITING | (none) |
| 0x0C | CMD_CONFIRM_MSG | (none) |
| 0x11 | CMD_SET_PREF | pref_key + value |
| 0x12 | CMD_DEVICE_QUERY | (none) |
| 0x15 | CMD_SEND_SELF_ADVERT | (none) |
| 0x20 | CMD_NEGOTIATE_VER | target_version |
Push Notifications (device → phone, async)
| Code | Name |
|---|---|
| 0x80 | PUSH_CODE_ADVERT |
| 0x82 | PUSH_CODE_SEND_CONFIRMED |
| 0x83 | PUSH_CODE_MSG_WAITING |
| 0x8A | PUSH_CODE_NEW_ADVERT |
13. Data Storage
File Paths
| Path | Content | Format |
|---|---|---|
/lfs/_main.id |
Node identity | 64B private key + 32B public key |
/lfs/new_prefs |
Preferences | 93B binary (Arduino-compatible) |
/lfs/contacts3 or /ext/contacts3 |
Contacts | 152B × N records |
/lfs/channels2 or /ext/channels2 |
Channels | 68B × N records |
/lfs/adv_blobs or /ext/adv_blobs |
Advert cache | Fixed-size blob records |
/lfs/repeater/acl |
Client ACL | 136B × N records |
/lfs/repeater/regions2 |
Region map | Header + 164B × N entries |
Preferences Binary Layout (93 bytes)
Offset Size Field
0 4 airtime_factor (float)
4 32 node_name
36 4 (padding)
40 8 node_lat (double)
48 8 node_lon (double)
56 4 freq (float)
60 1 sf
61 1 cr
62 1 client_repeat
63 1 manual_add_contacts
64 4 bw (float)
... ... (see CommonCLI.cpp loadPrefs/savePrefs for full layout)
92 1 rx_boost (ZephCore extension)
14. Key Call Flows
14.1 Receiving a LoRa Packet → Application
DIO1 interrupt → Zephyr lora driver → async RX callback
→ LoRaRadioBase::rxCallbackStatic() → SPSC ring buffer write → _rx_cb()
→ k_event_post(MESH_EVENT_LORA_RX) → main thread wakes
→ Dispatcher::loop() → checkRecv() → drain ring buffer
→ tryParsePacket() → score + airtime calc
→ flood: calcRxDelay() → queue inbound or process immediately
→ direct: process immediately
→ Mesh::onRecvPacket() → decrypt → dispatch by type
→ BaseChatMesh::onPeerDataRecv() → onMessageRecv()
→ CompanionMesh: writeFrame() to phone or queueOfflineMessage()
14.2 Sending a Text Message
Phone sends CMD_SEND_TXT_MSG via BLE NUS
→ CompanionMesh::handleProtocolFrame()
→ BaseChatMesh::sendMessage(contact, text)
→ composeMsgPacket(): ECDH secret → AES encrypt → MAC
→ if contact has path: trySendDirect()
→ else: sendFlood()
→ Mesh::sendFlood() → mark seen → queue outbound
→ Dispatcher::checkSend() → CAD check → duty cycle check → LBT → startSendRaw()
14.3 Repeater Forwarding a Packet
Dispatcher::checkRecv() → Mesh::onRecvPacket()
→ flood packet, not for us
→ routeRecvPacket() → allowPacketForward()
→ RepeaterMesh checks: disable_fwd? flood_max? region filter?
→ if allowed: append self hash to path, ACTION_RETRANSMIT_DELAYED
→ re-queued outbound with priority = hop count
14.4 Noise Floor Calibration Cycle
main event loop (every 5s) → Dispatcher::maintenanceLoop()
→ radio->triggerNoiseFloorCalibrate(threshold)
→ guards: in RX? TX active? duty cycle? mid-receive?
→ random 0-500ms jitter
→ read 4 RSSI samples, take min
→ first sample: seed directly
→ warmup (<8 ticks): accept unconditionally
→ periodic bypass (every 8th): accept unconditionally
→ otherwise: reject if sample ≥ floor + 14dB
→ EMA: floor += round((sample - floor) / 8)
→ clamp [-120, -50] dBm