# 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 1. [Project Overview](#1-project-overview) 2. [Directory Structure](#2-directory-structure) 3. [Layer Architecture](#3-layer-architecture) 4. [Core Mesh Engine](#4-core-mesh-engine) 5. [Radio Subsystem](#5-radio-subsystem) 6. [Application Layer](#6-application-layer) 7. [Hardware Adapters](#7-hardware-adapters) 8. [UI Subsystem](#8-ui-subsystem) 9. [Build System](#9-build-system) 10. [Board Matrix](#10-board-matrix) 11. [Packet Format Reference](#11-packet-format-reference) 12. [BLE Protocol Reference](#12-ble-protocol-reference) 13. [Data Storage](#13-data-storage) 14. [Key Call Flows](#14-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](https://github.com/rmendes76/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 (24 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 1. **Allocation**: `StaticPoolPacketManager::allocNew()` — fixed pool of 24 `Packet` objects (no heap) 2. **Creation**: `Mesh::createDatagram()`, `createAdvert()`, `createAck()`, etc. 3. **Queuing**: `Dispatcher::sendPacket()` → `PacketManager::queueOutbound()` with priority + scheduled time 4. **Transmission**: `Dispatcher::checkSend()` → CAD check → serialize → `radio->startSendRaw()` 5. **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: 1. **Noise floor calibration**: EMA with alpha=1/8, jitter, threshold filtering, warmup 2. **RX mode watchdog**: Flags error if radio stuck outside RX for >8 seconds 3. **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**: 1. `startSendRaw()` → cancel RX → configure TX → copy to buffer → async send → wake TX wait thread 2. TX wait thread blocks on semaphore, polls completion signal (5s timeout) 3. On DIO1 TX_DONE interrupt → signal raised → restart RX → update stats **RX Flow**: 1. `lora_recv_async()` with callback 2. ISR writes to 8-slot SPSC ring buffer (drops NEW packet on overflow) 3. 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 `lastmod` filtering 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**: `RegionMap` with 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**: `tempradio` command 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/_common.conf (nrf52/esp32/nrf54l/mg24 specifics) → boards///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) vs `ZEPHCORE_ROLE_REPEATER` - **Radio**: `ZEPHCORE_RADIO_NATIVE` (SX126x, default) vs `ZEPHCORE_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 ```