From ba4b86752d64f6eb771d8b4c26f8bc9cd4dc925b Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:24:12 +0200 Subject: [PATCH] acw: airtime-scale jitter caps, companion surroundings awareness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - flood retransmit jitter now capped at min(2000ms, 6·airtime) instead of fixed 2000ms — spreads tighter at SF7, unchanged at SF8 - reactive per-dupe backoff cap now min(2000ms, 12·airtime), keeps semantic of "push past ~12 relay slots" - contention ring 16 → 24 for 50-neighbor hilltops - companions passively track heard floods (warms EMA without forwarding) and spread their own TX by up to min(1000ms, 3·airtime), hopefully fixing repeaters missing companion's first transmission config cleanup: - move BLE TX buffer bumps (ACL_TX=12 etc.) from zephcore_common.conf to esp32_common.conf — the Espressif blob needs them, nRF doesn't, and the bumps were overflowing nRF52840 RAM - remove CONFIG_ZEPHCORE_MAX_CONTACTS=510 overrides from 5 nRF52840 companion boards; Kconfig default of 350 fits with comfortable margin (wio prod: 91% → 79% RAM) --- zephcore/ARCHITECTURE.md | 1390 ++++++++-------- zephcore/app/CompanionMesh.cpp | 26 +- zephcore/app/CompanionMesh.h | 7 + zephcore/app/RepeaterMesh.cpp | 12 +- zephcore/boards/common/esp32_common.conf | 9 + zephcore/boards/common/prod.conf | 44 +- zephcore/boards/common/zephcore_common.conf | 11 +- .../boards/nrf52840/sensecap_solar/board.conf | 2 - .../boards/nrf52840/thinknode_m1/board.conf | 57 +- .../boards/nrf52840/thinknode_m6/board.conf | 3 - .../boards/nrf52840/wio_tracker_l1/board.conf | 41 +- .../boards/nrf52840/xiao_nrf52840/board.conf | 2 - zephcore/include/mesh/ContentionTracker.h | 160 +- zephcore/include/mesh/Mesh.h | 208 +-- zephcore/src/ContentionTracker.cpp | 325 ++-- zephcore/src/Mesh.cpp | 1442 +++++++++-------- zephcore/west.yml | 34 +- 17 files changed, 1909 insertions(+), 1864 deletions(-) diff --git a/zephcore/ARCHITECTURE.md b/zephcore/ARCHITECTURE.md index bb29665..34ada2a 100644 --- a/zephcore/ARCHITECTURE.md +++ b/zephcore/ARCHITECTURE.md @@ -1,691 +1,699 @@ -# 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 (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 - -1. **Allocation**: `StaticPoolPacketManager::allocNew()` — fixed pool of 32 `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 Adaptive Contention Window - -Replaces Arduino MeshCore's static `txdelay`/`rxdelay` with two complementary mechanisms: - -**EMA Delay Factor (proactive)** - -`ContentionTracker` measures observed duplicates per retransmitted packet using a 16-entry ring buffer. Each entry tracks a packet (identified by FNV-1a hash) and records how many dupes arrive within a 10-second observation window. When the window closes, the entry is finalized and an EMA is updated with alpha = 1/8. The resulting estimate feeds the delay factor formula: - -``` -factor = 0.05 + 0.116 * sqrt(est) -``` - -Capped at 2.0. During warmup (fewer than 4 finalized entries), factor defaults to 0.5. Sparse nodes converge toward near-zero delay; dense nodes get proportionally higher delay. The factor scales the flood TX delay computed by `calcRxDelay()`. - -**Per-Dupe Reactive Backoff** - -When a duplicate of a pending outbound packet is heard, TX is rescheduled to `now + backoff_multiplier * airtime`. Each dupe triggers a full delay (not diminishing). Cumulative reactive extension is hard-capped at 2000 ms per packet; after the cap, CAD handles remaining channel activity. `backoff_multiplier` is configurable via `set backoff.multiplier X` (range 0.0–2.0). - -**Direct Packets** - -Direct (source-routed) packets bypass adaptive scaling entirely. They use minimal fixed jitter: `20 + rand(0, airtime / 10)` ms. - -**CLI** - -- `get txdelay` — shows current adaptive state (EMA estimate, delay factor, backoff multiplier). -- `set backoff.multiplier X` — controls per-dupe reactive delay (0.0–2.0). -- `txdelay`, `rxdelay`, `direct.txdelay` — accepted for prefs compatibility but ignored at runtime. - -**ContentionTracker Resource Usage** - -~172 bytes RAM. 16-entry ring buffer, FNV-1a packet hash, 10-second observation window, EMA with alpha = 1/8. - -### 4.8 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 on CCC + RX (forces pairing) -- Passkey-based MITM pairing (SC + MITM + Bonding), runtime configurable PIN via `app_passkey` callback -- DisplayOnly IO capability — phone enters passkey displayed on device / known to user -- Advertising with public identity address (no RPA) — required for Android Flutter BLE app compatibility -- `CONFIG_BT_PRIVACY` disabled: Android's Flutter BLE plugin fails to `connectGatt()` to RPA-advertised devices from app context; iOS and Android system BT settings handle RPA fine but the MeshCore app doesn't. Arduino MeshCore also uses public addresses. -- Pairing triggered reactively: phone hits ATT error 0x05 on secured attribute → initiates SMP pairing (Apple Accessory Design Guidelines §55 compliant — no proactive Security Request) -- 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 -- Debug: `boards/common/ble_debug.conf` overlay enables DBG on bt_smp/att/gatt/conn - -### 7.2 DataStore (`adapters/datastore/`) - -- **Internal**: LittleFS on flash (`/lfs`), 256-byte cache for reduced flash I/O -- **External**: Optional LittleFS on QSPI (`/ext`) with auto-migration -- **BLE bonds**: File-based settings on LittleFS (`/lfs/settings/`) — all platforms (no NVS) -- **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 (boards with `zephyr,user` ADC node; MG24 has no battery divider, ADC disabled) -- 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 Platform Notes - -- **nRF52840**: Zephyr open-source BLE controller, UF2 bootloader, partial flash erase for BLE coexistence -- **nRF54L15**: Same BLE controller as nRF52, CMSIS-DAP via SAMD11 bridge, no native USB -- **ESP32**: Espressif proprietary BLE blob, 32KB heap, asserts disabled (blob IRQ false positives) -- **EFR32MG24**: SiLabs proprietary BLE blob, 32KB heap, SEMAILBOX enabled for hardware TRNG/crypto entropy, ADC disabled (no battery divider), CMSIS-DAP via onboard SAMD11 - -### 9.4 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.5 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 → LFS 192KB -**ESP32-S3 (16MB)**: Boot + App → LFS 384KB -**nRF54L15**: MCUboot 64KB → App 1272KB → LFS 92KB -**EFR32MG24**: MCUboot 48KB → App 1344KB → LFS 144KB - ---- - -## 10. Board Matrix - -| Board | SoC | Radio | GPS | Display | Buzzer | Buttons | QSPI | Max Contacts | -|-------|-----|-------|-----|---------|--------|---------|------|-------------| -| RAK4631 | nRF52840 | SX1262 | gnss-nmea | - | - | - | - | 350 | -| RAK3401 1W | nRF52840 | SX1262+SKY66122 (30dBm) | gnss-nmea (opt) | - | - | - | - | 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 | -| `/lfs/settings/` | BLE bonds + Zephyr settings | File-based settings (all platforms) | - -### Preferences Binary Layout (292 bytes) - -Field-by-field serialization (NOT raw struct dump). See `memory/prefs-format.md` for full layout, -or `zephcore/helpers/CommonCLI.cpp` `loadPrefs()` for the authoritative source. - -Key ranges: name(4-36), radio(72-119), adaptive-delay(80-111, ignored at runtime), -Arduino-bridge(127-151, read+discarded), GPS(156-161), owner_info(170-290), rx_boost/duty(290-291). - ---- - -## 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 -``` +# 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 (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 + +1. **Allocation**: `StaticPoolPacketManager::allocNew()` — fixed pool of 32 `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 Adaptive Contention Window + +Replaces Arduino MeshCore's static `txdelay`/`rxdelay` with three complementary mechanisms. + +**EMA Delay Factor (proactive)** + +`ContentionTracker` measures observed duplicates per retransmitted packet using a **24-entry ring buffer** (sized for ~50-neighbor hilltop topologies with multiple concurrent in-flight floods). Each entry tracks a packet (identified by FNV-1a hash) and records how many dupes arrive within a 10-second observation window. When the window closes, the entry is finalized and an EMA is updated with alpha = 1/8. The resulting estimate feeds the delay factor formula: + +``` +factor = 0.05 + 0.170 * sqrt(est) +``` + +Capped at 2.0. During warmup (fewer than 4 finalized entries), factor defaults to 0.5. Sparse nodes converge toward near-zero delay; dense nodes get proportionally higher delay. + +The flood retransmit jitter window is `5·airtime·factor` clamped by **two ceilings**: +- Airtime-scaled: `6·airtime` — keeps SF7/narrow-BW configs from wasting time in oversized windows. +- Absolute: `2000ms` — bounds per-hop latency in dense areas even when airtime is large. + +**Per-Dupe Reactive Backoff** + +When a duplicate of a pending outbound packet is heard, TX is rescheduled to `now + backoff_multiplier * airtime`. Each dupe triggers a full delay (not diminishing). Cumulative reactive extension is capped at `min(2000ms, 12·airtime)` per packet; after the cap, CAD handles remaining channel activity. `backoff_multiplier` is configurable via `set backoff.multiplier X` (range 0.0–2.0). + +**Initial-Flood Jitter (companion-only)** + +Companions don't retransmit floods, but they observe mesh contention and need to spread their *originated* transmissions to avoid colliding with repeaters still busy in TX/RX. `Mesh::passivelyTrackFloods()` (overridden to `true` on `CompanionMesh`) registers every first-hearing of a flood with the ContentionTracker, so the EMA warms up even without forwarding. `Mesh::getInitialFloodJitter(packet)` is added to the caller-supplied delay in both `sendFlood` overloads; on companion this is `rand(0, min(1000ms, 3·airtime, 5·airtime·factor))` — half the repeater's ceilings. Repeaters keep the default 0 (no double-jitter on forwards). + +**Direct Packets** + +Direct (source-routed) packets bypass adaptive scaling entirely. They use minimal fixed jitter: `20 + rand(0, airtime / 10)` ms. + +**CLI** + +- `get txdelay` — shows current adaptive state (EMA estimate, delay factor, backoff multiplier). +- `set backoff.multiplier X` — controls per-dupe reactive delay (0.0–2.0). +- `txdelay`, `rxdelay`, `direct.txdelay` — accepted for prefs compatibility but ignored at runtime. + +**ContentionTracker Resource Usage** + +~260 bytes RAM (24-entry ring buffer × ~16B/entry + state). FNV-1a packet hash, 10-second observation window, EMA with alpha = 1/8. + +### 4.8 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 on CCC + RX (forces pairing) +- Passkey-based MITM pairing (SC + MITM + Bonding), runtime configurable PIN via `app_passkey` callback +- DisplayOnly IO capability — phone enters passkey displayed on device / known to user +- Advertising with public identity address (no RPA) — required for Android Flutter BLE app compatibility +- `CONFIG_BT_PRIVACY` disabled: Android's Flutter BLE plugin fails to `connectGatt()` to RPA-advertised devices from app context; iOS and Android system BT settings handle RPA fine but the MeshCore app doesn't. Arduino MeshCore also uses public addresses. +- Pairing triggered reactively: phone hits ATT error 0x05 on secured attribute → initiates SMP pairing (Apple Accessory Design Guidelines §55 compliant — no proactive Security Request) +- 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 +- Debug: `boards/common/ble_debug.conf` overlay enables DBG on bt_smp/att/gatt/conn + +### 7.2 DataStore (`adapters/datastore/`) + +- **Internal**: LittleFS on flash (`/lfs`), 256-byte cache for reduced flash I/O +- **External**: Optional LittleFS on QSPI (`/ext`) with auto-migration +- **BLE bonds**: File-based settings on LittleFS (`/lfs/settings/`) — all platforms (no NVS) +- **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 (boards with `zephyr,user` ADC node; MG24 has no battery divider, ADC disabled) +- 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 Platform Notes + +- **nRF52840**: Zephyr open-source BLE controller, UF2 bootloader, partial flash erase for BLE coexistence +- **nRF54L15**: Same BLE controller as nRF52, CMSIS-DAP via SAMD11 bridge, no native USB +- **ESP32**: Espressif proprietary BLE blob, 32KB heap, asserts disabled (blob IRQ false positives) +- **EFR32MG24**: SiLabs proprietary BLE blob, 32KB heap, SEMAILBOX enabled for hardware TRNG/crypto entropy, ADC disabled (no battery divider), CMSIS-DAP via onboard SAMD11 + +### 9.4 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.5 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 → LFS 192KB +**ESP32-S3 (16MB)**: Boot + App → LFS 384KB +**nRF54L15**: MCUboot 64KB → App 1272KB → LFS 92KB +**EFR32MG24**: MCUboot 48KB → App 1344KB → LFS 144KB + +--- + +## 10. Board Matrix + +| Board | SoC | Radio | GPS | Display | Buzzer | Buttons | QSPI | Max Contacts | +|-------|-----|-------|-----|---------|--------|---------|------|-------------| +| RAK4631 | nRF52840 | SX1262 | gnss-nmea | - | - | - | - | 350 | +| RAK3401 1W | nRF52840 | SX1262+SKY66122 (30dBm) | gnss-nmea (opt) | - | - | - | - | 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 | +| `/lfs/settings/` | BLE bonds + Zephyr settings | File-based settings (all platforms) | + +### Preferences Binary Layout (292 bytes) + +Field-by-field serialization (NOT raw struct dump). See `memory/prefs-format.md` for full layout, +or `zephcore/helpers/CommonCLI.cpp` `loadPrefs()` for the authoritative source. + +Key ranges: name(4-36), radio(72-119), adaptive-delay(80-111, ignored at runtime), +Arduino-bridge(127-151, read+discarded), GPS(156-161), owner_info(170-290), rx_boost/duty(290-291). + +--- + +## 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 +``` diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index fa6198c..8c489be 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -1173,10 +1173,13 @@ void CompanionMesh::onRawDataRecv(mesh::Packet *packet) uint32_t CompanionMesh::getRetransmitDelay(const mesh::Packet *packet) { float factor = getContentionTracker().getFloodDelayFactor(); - uint32_t t = (uint32_t)(_radio->getEstAirtimeFor( - packet->getPathByteLen() + packet->payload_len + 2) * factor); - uint32_t max_jitter = 5 * t; - /* Cap jitter to 2000ms to avoid excessive latency in very dense areas. + uint32_t airtime = _radio->getEstAirtimeFor( + packet->getPathByteLen() + packet->payload_len + 2); + uint32_t max_jitter = (uint32_t)(5 * airtime * factor); + /* Airtime-scaled ceiling: never exceed ~6 airtimes of spread. */ + uint32_t airtime_cap = 6 * airtime; + if (max_jitter > airtime_cap) max_jitter = airtime_cap; + /* Absolute cap: avoid excessive latency in very dense areas. * Reactive backoff will fine-tune further if needed. */ if (max_jitter > 2000) max_jitter = 2000; /* Floor: give downstream nodes time to finish RX processing @@ -1193,6 +1196,21 @@ uint32_t CompanionMesh::getDirectRetransmitDelay(const mesh::Packet *packet) return 20 + getRNG()->nextInt(0, t / 10 + 1); } +uint32_t CompanionMesh::getInitialFloodJitter(const mesh::Packet *packet) +{ + float factor = getContentionTracker().getFloodDelayFactor(); + uint32_t airtime = _radio->getEstAirtimeFor( + packet->getPathByteLen() + packet->payload_len + 2); + uint32_t max_jitter = (uint32_t)(5 * airtime * factor); + /* Companion spreads less aggressively than a repeater: half the + * airtime ceiling, and a tighter absolute cap (1000ms vs 2000ms). */ + uint32_t airtime_cap = 3 * airtime; + if (max_jitter > airtime_cap) max_jitter = airtime_cap; + if (max_jitter > 1000) max_jitter = 1000; + if (max_jitter == 0) return 0; + return getRNG()->nextInt(0, max_jitter + 1); +} + uint8_t CompanionMesh::getDutyCyclePercent() const { return (uint8_t)prefs.airtime_factor; diff --git a/zephcore/app/CompanionMesh.h b/zephcore/app/CompanionMesh.h index 2fdd887..ff72a06 100644 --- a/zephcore/app/CompanionMesh.h +++ b/zephcore/app/CompanionMesh.h @@ -261,6 +261,13 @@ protected: /* Dispatcher tuning (uses prefs) */ uint32_t getRetransmitDelay(const mesh::Packet *packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override; + + /* Companion doesn't forward, but needs surroundings awareness so its + * initial TX spreads with local contention. Enables passive EMA + * warming and adaptive initial-flood jitter. */ + bool passivelyTrackFloods() const override { return true; } + uint32_t getInitialFloodJitter(const mesh::Packet *packet) override; + uint8_t getDutyCyclePercent() const override; uint8_t getExtraAckTransmitCount() const override; diff --git a/zephcore/app/RepeaterMesh.cpp b/zephcore/app/RepeaterMesh.cpp index 2463497..892f93f 100644 --- a/zephcore/app/RepeaterMesh.cpp +++ b/zephcore/app/RepeaterMesh.cpp @@ -522,10 +522,14 @@ void RepeaterMesh::logTxFail(mesh::Packet* pkt, int len) { uint32_t RepeaterMesh::getRetransmitDelay(const mesh::Packet* packet) { float factor = getContentionTracker().getFloodDelayFactor(); - uint32_t t = (uint32_t)(_radio->getEstAirtimeFor( - packet->getPathByteLen() + packet->payload_len + 2) * factor); - uint32_t max_jitter = 5 * t; - /* Cap jitter to 2000ms to avoid excessive latency in very dense areas. + uint32_t airtime = _radio->getEstAirtimeFor( + packet->getPathByteLen() + packet->payload_len + 2); + uint32_t max_jitter = (uint32_t)(5 * airtime * factor); + /* Airtime-scaled ceiling: never exceed ~6 airtimes of spread + * (keeps SF7/narrow-BW configs from wasting time in oversized jitter windows). */ + uint32_t airtime_cap = 6 * airtime; + if (max_jitter > airtime_cap) max_jitter = airtime_cap; + /* Absolute cap: avoid excessive latency in very dense areas. * Reactive backoff will fine-tune further if needed. */ if (max_jitter > 2000) max_jitter = 2000; /* Floor: give downstream nodes time to finish RX processing diff --git a/zephcore/boards/common/esp32_common.conf b/zephcore/boards/common/esp32_common.conf index c564985..fac9540 100644 --- a/zephcore/boards/common/esp32_common.conf +++ b/zephcore/boards/common/esp32_common.conf @@ -15,6 +15,15 @@ CONFIG_BT_PRIVACY=y # BLE thread stacks — ESP32 software BLE controller needs larger stacks CONFIG_BT_TX_PROCESSOR_STACK_SIZE=2048 +# BLE TX buffers — the Espressif BLE controller blob bursts more aggressively +# than the nRF softdevice and will deadlock the system workqueue if ACL TX +# buffers run dry (bt_hci_cmd_alloc(K_FOREVER) blocks the cooperative syswq). +# EVT_RX must strictly exceed ACL_TX (enforced by BUILD_ASSERT in buf.h). +CONFIG_BT_BUF_ACL_TX_COUNT=12 +CONFIG_BT_L2CAP_TX_BUF_COUNT=12 +CONFIG_BT_CONN_TX_MAX=12 +CONFIG_BT_BUF_EVT_RX_COUNT=14 + # ========== Heap ========== # ESP32 BLE stack requires larger heap (override zephcore_common 2KB default) CONFIG_HEAP_MEM_POOL_SIZE=32768 diff --git a/zephcore/boards/common/prod.conf b/zephcore/boards/common/prod.conf index 3c0d7b6..7ff951a 100644 --- a/zephcore/boards/common/prod.conf +++ b/zephcore/boards/common/prod.conf @@ -1,22 +1,22 @@ -# Production Config - Common to ALL boards (Companion & Repeater) -# Build: west build -b zephcore --pristine -- \ -# -DEXTRA_CONF_FILE="boards/common/prod.conf" -# -# Disables logging for smaller binary and lower power consumption. -# USB CDC still works for CLI commands on repeaters. - -# Disable logging subsystem (saves ~62KB flash, ~2-5mA power) -CONFIG_LOG=n - -# Disable asserts (saves flash, removes CMake warning) -CONFIG_ASSERT=n - -# Disable SEGGER RTT (saves ~4KB RAM ring buffer + init code) -CONFIG_USE_SEGGER_RTT=n - -# Disable thread name strings (saves flash — only useful for debug) -CONFIG_THREAD_NAME=n - -# Max contacts: Kconfig default is 350 (safe for 100KB ExtraFS). -# Do NOT set here — EXTRA_CONF_FILE overrides board.conf, which would -# prevent boards with more storage (Wio QSPI=510) from raising the limit. +# Production Config - Common to ALL boards (Companion & Repeater) +# Build: west build -b zephcore --pristine -- \ +# -DEXTRA_CONF_FILE="boards/common/prod.conf" +# +# Disables logging for smaller binary and lower power consumption. +# USB CDC still works for CLI commands on repeaters. + +# Disable logging subsystem (saves ~62KB flash, ~2-5mA power) +CONFIG_LOG=n + +# Disable asserts (saves flash, removes CMake warning) +CONFIG_ASSERT=n + +# Disable SEGGER RTT (saves ~4KB RAM ring buffer + init code) +CONFIG_USE_SEGGER_RTT=n + +# Disable thread name strings (saves flash — only useful for debug) +CONFIG_THREAD_NAME=n + +# Max contacts: Kconfig default is 350 — fits nRF52840 RAM with ~10% headroom +# and is safe for 100KB ExtraFS. RAM-limited boards (xiao_nrf54l15=450, +# xiao_esp32c3=300) override in their own board.conf. Do not set here. diff --git a/zephcore/boards/common/zephcore_common.conf b/zephcore/boards/common/zephcore_common.conf index 5cc3f4e..3e10932 100644 --- a/zephcore/boards/common/zephcore_common.conf +++ b/zephcore/boards/common/zephcore_common.conf @@ -93,12 +93,11 @@ CONFIG_BT_BUF_ACL_RX_SIZE=251 CONFIG_BT_BUF_ACL_TX_SIZE=251 CONFIG_BT_L2CAP_TX_MTU=247 -# BLE TX buffers — default 3 is too low, causes system workqueue deadlock -# when BLE activity bursts (DLE + param update + ATT) exhaust all buffers -# and bt_hci_cmd_alloc(K_FOREVER) blocks the cooperative syswq forever. -CONFIG_BT_BUF_ACL_TX_COUNT=12 -CONFIG_BT_L2CAP_TX_BUF_COUNT=12 -CONFIG_BT_CONN_TX_MAX=12 +# BLE TX buffers — platform-specific. ESP32 gets a large bump in +# esp32_common.conf to prevent syswq deadlock during BLE bursts (DLE + +# param update + ATT exhausting buffers and bt_hci_cmd_alloc(K_FOREVER) +# blocking the cooperative syswq). nRF52 uses Zephyr defaults — RAM is +# tight on nRF52840 and the nRF controller rarely deadlocks. # BLE RX thread stack — default 1200 too small for ATT handlers + logging + asserts CONFIG_BT_RX_STACK_SIZE=2048 diff --git a/zephcore/boards/nrf52840/sensecap_solar/board.conf b/zephcore/boards/nrf52840/sensecap_solar/board.conf index e185741..539de3c 100644 --- a/zephcore/boards/nrf52840/sensecap_solar/board.conf +++ b/zephcore/boards/nrf52840/sensecap_solar/board.conf @@ -10,5 +10,3 @@ CONFIG_ZEPHCORE_BOARD_NAME="SenseCAP Solar" CONFIG_BT_DIS_MODEL_NUMBER_STR="Seeed SenseCAP Solar" CONFIG_ZEPHCORE_SD_FWID=0x0123 - -CONFIG_ZEPHCORE_MAX_CONTACTS=510 diff --git a/zephcore/boards/nrf52840/thinknode_m1/board.conf b/zephcore/boards/nrf52840/thinknode_m1/board.conf index dc602e1..c86e144 100644 --- a/zephcore/boards/nrf52840/thinknode_m1/board.conf +++ b/zephcore/boards/nrf52840/thinknode_m1/board.conf @@ -1,30 +1,27 @@ -# Elecrow ThinkNode M1 (nRF52840 + SX1262 + SSD1681 EPD) -# Board-specific configuration -# -# Hardware: -# - nRF52840 SoC, SX1262 LoRa (22dBm), 1.54" SSD1681 e-paper (200x200) -# - GPS module on UART0, MX25R1635F 2MB QSPI flash -# - Battery ADC on AIN2 (P0.04), 150K+150K divider -# - Buzzer on P0.06, two buttons, GPS hardware switch -# - LEDs: GREEN=P1.04, BLUE=P0.14 - -# Board identification (matches Arduino MeshCore variant name) -CONFIG_ZEPHCORE_BOARD_NAME="ThinkNode M1" - -# Device Information Service model name -CONFIG_BT_DIS_MODEL_NUMBER_STR="Elecrow ThinkNode-M1" - -# SoftDevice firmware ID (ThinkNode M1 bootloader uses S140 v6) -CONFIG_ZEPHCORE_SD_FWID=0x00B6 - -# Contacts — QSPI (2MB) provides ample storage for more contacts -CONFIG_ZEPHCORE_MAX_CONTACTS=510 - -# ========== RAM budget ========== -# SSD1681 200x200 CFB framebuffer needs 5000 bytes from k_malloc. -# Default heap (2048) is sized for SSD1306 128x64 (1024 bytes). -CONFIG_HEAP_MEM_POOL_SIZE=6144 - -# Shrink RTT buffer to free RAM (4096 → 1024, saves 3KB). -# Only used during J-Link debugging, 1KB is Zephyr's default. -CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=1024 +# Elecrow ThinkNode M1 (nRF52840 + SX1262 + SSD1681 EPD) +# Board-specific configuration +# +# Hardware: +# - nRF52840 SoC, SX1262 LoRa (22dBm), 1.54" SSD1681 e-paper (200x200) +# - GPS module on UART0, MX25R1635F 2MB QSPI flash +# - Battery ADC on AIN2 (P0.04), 150K+150K divider +# - Buzzer on P0.06, two buttons, GPS hardware switch +# - LEDs: GREEN=P1.04, BLUE=P0.14 + +# Board identification (matches Arduino MeshCore variant name) +CONFIG_ZEPHCORE_BOARD_NAME="ThinkNode M1" + +# Device Information Service model name +CONFIG_BT_DIS_MODEL_NUMBER_STR="Elecrow ThinkNode-M1" + +# SoftDevice firmware ID (ThinkNode M1 bootloader uses S140 v6) +CONFIG_ZEPHCORE_SD_FWID=0x00B6 + +# ========== RAM budget ========== +# SSD1681 200x200 CFB framebuffer needs 5000 bytes from k_malloc. +# Default heap (2048) is sized for SSD1306 128x64 (1024 bytes). +CONFIG_HEAP_MEM_POOL_SIZE=6144 + +# Shrink RTT buffer to free RAM (4096 → 1024, saves 3KB). +# Only used during J-Link debugging, 1KB is Zephyr's default. +CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=1024 diff --git a/zephcore/boards/nrf52840/thinknode_m6/board.conf b/zephcore/boards/nrf52840/thinknode_m6/board.conf index cf50bd6..3d1e0d5 100644 --- a/zephcore/boards/nrf52840/thinknode_m6/board.conf +++ b/zephcore/boards/nrf52840/thinknode_m6/board.conf @@ -18,6 +18,3 @@ CONFIG_BT_DIS_MODEL_NUMBER_STR="Elecrow ThinkNode-M6" # SoftDevice firmware ID (ThinkNode M6 bootloader uses S140 v6, same as M1/M3) CONFIG_ZEPHCORE_SD_FWID=0x00B6 - -# Contacts — QSPI (2MB) provides ample storage for more contacts -CONFIG_ZEPHCORE_MAX_CONTACTS=510 diff --git a/zephcore/boards/nrf52840/wio_tracker_l1/board.conf b/zephcore/boards/nrf52840/wio_tracker_l1/board.conf index be0dba8..7b6b426 100644 --- a/zephcore/boards/nrf52840/wio_tracker_l1/board.conf +++ b/zephcore/boards/nrf52840/wio_tracker_l1/board.conf @@ -1,22 +1,19 @@ -# Wio Tracker L1 (nRF52840 + SX1262) -# Board-specific configuration - pins and unique features only -# -# Hardware: -# - SX1262 LoRa on SPI0 (P1.14 CS, P1.07 RST, P1.10 BUSY, P0.07 DIO1, P1.08 RXEN) -# - L76KB GPS on UART0 (P0.26/P0.27) -# - P25Q16H 2MB QSPI flash -# - Battery ADC on AIN7 (P0.31), enable on P0.04 -# - SH1106 OLED on I2C0 (P0.05/P0.06, addr 0x3D) -# - Joystick + user button + buzzer (P1.00) - -# Board identification (matches Arduino variant name) -CONFIG_ZEPHCORE_BOARD_NAME="Wio Tracker L1" - -# Device Information Service model name -CONFIG_BT_DIS_MODEL_NUMBER_STR="Wio Tracker L1" - -# SoftDevice firmware ID (s140 v7.3.0 — Seeed bootloader) -CONFIG_ZEPHCORE_SD_FWID=0x0123 - -# Contacts — QSPI (2MB) provides ample storage for more contacts -CONFIG_ZEPHCORE_MAX_CONTACTS=510 +# Wio Tracker L1 (nRF52840 + SX1262) +# Board-specific configuration - pins and unique features only +# +# Hardware: +# - SX1262 LoRa on SPI0 (P1.14 CS, P1.07 RST, P1.10 BUSY, P0.07 DIO1, P1.08 RXEN) +# - L76KB GPS on UART0 (P0.26/P0.27) +# - P25Q16H 2MB QSPI flash +# - Battery ADC on AIN7 (P0.31), enable on P0.04 +# - SH1106 OLED on I2C0 (P0.05/P0.06, addr 0x3D) +# - Joystick + user button + buzzer (P1.00) + +# Board identification (matches Arduino variant name) +CONFIG_ZEPHCORE_BOARD_NAME="Wio Tracker L1" + +# Device Information Service model name +CONFIG_BT_DIS_MODEL_NUMBER_STR="Wio Tracker L1" + +# SoftDevice firmware ID (s140 v7.3.0 — Seeed bootloader) +CONFIG_ZEPHCORE_SD_FWID=0x0123 diff --git a/zephcore/boards/nrf52840/xiao_nrf52840/board.conf b/zephcore/boards/nrf52840/xiao_nrf52840/board.conf index b7c978b..9103fc0 100644 --- a/zephcore/boards/nrf52840/xiao_nrf52840/board.conf +++ b/zephcore/boards/nrf52840/xiao_nrf52840/board.conf @@ -7,8 +7,6 @@ CONFIG_BT_DIS_MODEL_NUMBER_STR="Seeed XIAO nRF52840" CONFIG_ZEPHCORE_SD_FWID=0x0123 -CONFIG_ZEPHCORE_MAX_CONTACTS=510 - # Default debug (logging.conf) sends LOG_* to RTT only. Plain XIAO is usually # flashed/debugged over USB without an SWD J-Link — enable UART backend so # LOG_* appears on the CDC ACM serial port (same as zephyr,console). diff --git a/zephcore/include/mesh/ContentionTracker.h b/zephcore/include/mesh/ContentionTracker.h index 0f45ede..5fb8571 100644 --- a/zephcore/include/mesh/ContentionTracker.h +++ b/zephcore/include/mesh/ContentionTracker.h @@ -1,80 +1,80 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Adaptive Contention Window — EMA-based flood retransmit delay - * - * Counts neighbor retransmit dupes within a 10s window per packet. - * Dupe counts feed a rolling EMA that drives an adaptive delay factor. - */ - -#pragma once - -#include - -namespace mesh { - -class Packet; - -class ContentionTracker { -public: - ContentionTracker(); - - /* FNV-1a 32-bit hash for ring buffer correlation (not dedup SHA256). */ - static uint32_t computePacketHash32(const Packet *pkt); - - void trackRetransmit(uint32_t hash32, uint32_t now_ms); - - /* Returns true if packet matched a tracked retransmit (dupe recorded). */ - bool recordDupeIfTracked(uint32_t hash32, uint32_t now_ms); - - /* Returns backoff_multiplier * airtime, clamped by remaining headroom. - * Returns 0 when hard cap reached or backoff disabled. */ - uint16_t getReactiveHeadroom(uint32_t hash32, uint32_t airtime_ms) const; - - void addReactiveExtension(uint32_t hash32, uint16_t added_ms); - - /* Finalize expired entries into EMA. */ - void tick(uint32_t now_ms); - - float getContentionEstimate() const; - - /* sqrt curve: MIN_FLOOD_FACTOR + FLOOD_SCALE * sqrt(est), cap 2.0. - * Returns 0.5 during warmup. */ - float getFloodDelayFactor() const; - - bool isWarmedUp() const { return _finalized_count >= WARMUP_PACKETS; } - - void setBackoffMultiplier(float m) { _backoff_multiplier = m; } - float getBackoffMultiplier() const { return _backoff_multiplier; } - -private: - static constexpr int RING_SIZE = 16; /* max concurrent tracked retransmits */ - static constexpr uint32_t WINDOW_MS = 10000; /* dupe observation window; covers SF12 2-hop */ - static constexpr int EMA_SHIFT = 3; /* alpha = 1/8 */ - static constexpr int WARMUP_PACKETS = 4; /* min samples before EMA is trusted */ - static constexpr float MIN_FLOOD_FACTOR = 0.05f; /* floor: near-zero delay in quiet networks */ - static constexpr float FLOOD_SCALE = 0.170f; /* (0.5 - 0.05) / sqrt(15) */ - static constexpr float MAX_FLOOD_FACTOR = 2.0f; /* ceiling: 2x base airtime */ - static constexpr float DEFAULT_BACKOFF_MULT = 0.5f; /* half-airtime per dupe heard */ - static constexpr uint32_t REACTIVE_HARD_CAP_MS = 2000; /* max cumulative reactive extension */ - static constexpr uint32_t STALE_MS = 300000; /* 5 min: reset EMA if no traffic */ - - struct Entry { - uint32_t hash32; - uint32_t first_seen_ms; - uint8_t dupe_count; - uint16_t reactive_added_ms; - bool active; - }; - - Entry _ring[RING_SIZE]; - int _next_idx; - uint32_t _ema_x256; - int _finalized_count; - uint32_t _last_retransmit_ms; - float _backoff_multiplier; - - void finalizeEntry(int idx); - int findEntry(uint32_t hash32) const; -}; - -} /* namespace mesh */ +/* + * SPDX-License-Identifier: Apache-2.0 + * Adaptive Contention Window — EMA-based flood retransmit delay + * + * Counts neighbor retransmit dupes within a 10s window per packet. + * Dupe counts feed a rolling EMA that drives an adaptive delay factor. + */ + +#pragma once + +#include + +namespace mesh { + +class Packet; + +class ContentionTracker { +public: + ContentionTracker(); + + /* FNV-1a 32-bit hash for ring buffer correlation (not dedup SHA256). */ + static uint32_t computePacketHash32(const Packet *pkt); + + void trackRetransmit(uint32_t hash32, uint32_t now_ms); + + /* Returns true if packet matched a tracked retransmit (dupe recorded). */ + bool recordDupeIfTracked(uint32_t hash32, uint32_t now_ms); + + /* Returns backoff_multiplier * airtime, clamped by remaining headroom. + * Returns 0 when hard cap reached or backoff disabled. */ + uint16_t getReactiveHeadroom(uint32_t hash32, uint32_t airtime_ms) const; + + void addReactiveExtension(uint32_t hash32, uint16_t added_ms); + + /* Finalize expired entries into EMA. */ + void tick(uint32_t now_ms); + + float getContentionEstimate() const; + + /* sqrt curve: MIN_FLOOD_FACTOR + FLOOD_SCALE * sqrt(est), cap 2.0. + * Returns 0.5 during warmup. */ + float getFloodDelayFactor() const; + + bool isWarmedUp() const { return _finalized_count >= WARMUP_PACKETS; } + + void setBackoffMultiplier(float m) { _backoff_multiplier = m; } + float getBackoffMultiplier() const { return _backoff_multiplier; } + +private: + static constexpr int RING_SIZE = 24; /* max concurrent tracked retransmits */ + static constexpr uint32_t WINDOW_MS = 10000; /* dupe observation window; covers SF12 2-hop */ + static constexpr int EMA_SHIFT = 3; /* alpha = 1/8 */ + static constexpr int WARMUP_PACKETS = 4; /* min samples before EMA is trusted */ + static constexpr float MIN_FLOOD_FACTOR = 0.05f; /* floor: near-zero delay in quiet networks */ + static constexpr float FLOOD_SCALE = 0.170f; /* (0.5 - 0.05) / sqrt(15) */ + static constexpr float MAX_FLOOD_FACTOR = 2.0f; /* ceiling: 2x base airtime */ + static constexpr float DEFAULT_BACKOFF_MULT = 0.5f; /* half-airtime per dupe heard */ + static constexpr uint32_t REACTIVE_HARD_CAP_MS = 2000; /* max cumulative reactive extension */ + static constexpr uint32_t STALE_MS = 300000; /* 5 min: reset EMA if no traffic */ + + struct Entry { + uint32_t hash32; + uint32_t first_seen_ms; + uint8_t dupe_count; + uint16_t reactive_added_ms; + bool active; + }; + + Entry _ring[RING_SIZE]; + int _next_idx; + uint32_t _ema_x256; + int _finalized_count; + uint32_t _last_retransmit_ms; + float _backoff_multiplier; + + void finalizeEntry(int idx); + int findEntry(uint32_t hash32) const; +}; + +} /* namespace mesh */ diff --git a/zephcore/include/mesh/Mesh.h b/zephcore/include/mesh/Mesh.h index aaaf328..c34e432 100644 --- a/zephcore/include/mesh/Mesh.h +++ b/zephcore/include/mesh/Mesh.h @@ -1,101 +1,107 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * ZephCore Mesh - routing protocol layer - */ - -#pragma once - -#include -#include -#ifdef CONFIG_ZEPHCORE_APC -#include -#endif -#include - -namespace mesh { - -struct GroupChannel { - uint8_t hash[PATH_HASH_SIZE]; - uint8_t secret[PUB_KEY_SIZE]; -}; - -class MeshTables { -public: - virtual bool hasSeen(const Packet *packet) = 0; - virtual void clear(const Packet *packet) = 0; -}; - -class Mesh : public Dispatcher { - RNG *_rng; - RTCClock *_rtc; - MeshTables *_tables; - - void removeSelfFromPath(Packet *packet); - void routeDirectRecvAcks(Packet *packet, uint32_t delay_millis); - DispatcherAction forwardMultipartDirect(Packet *pkt); - -protected: - ContentionTracker _contention; - ContentionTracker& getContentionTracker() { return _contention; } - const ContentionTracker& getContentionTracker() const { return _contention; } -#ifdef CONFIG_ZEPHCORE_APC - PowerController _power_ctrl; - PowerController& getPowerController() { return _power_ctrl; } - const PowerController& getPowerController() const { return _power_ctrl; } -#endif - void extendPendingRetransmit(uint32_t hash32); - - DispatcherAction onRecvPacket(Packet *pkt) override; - virtual uint32_t getCADFailRetryDelay() const override; - virtual DispatcherAction routeRecvPacket(Packet *packet); - virtual bool filterRecvFloodPacket(Packet *packet) { return false; } - virtual bool allowPacketForward(const Packet *packet); - virtual uint32_t getRetransmitDelay(const Packet *packet); - virtual uint32_t getDirectRetransmitDelay(const Packet *packet) { return 0; } - virtual uint8_t getExtraAckTransmitCount() const { return 0; } - virtual int searchPeersByHash(const uint8_t *hash) { (void)hash; return 0; } - virtual void getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) { (void)dest_secret; (void)peer_idx; } - virtual void onPeerDataRecv(Packet *packet, uint8_t type, int sender_idx, const uint8_t *secret, uint8_t *data, size_t len) { (void)packet; (void)type; (void)sender_idx; (void)secret; (void)data; (void)len; } - virtual void onTraceRecv(Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) { (void)packet; (void)tag; (void)auth_code; (void)flags; (void)path_snrs; (void)path_hashes; (void)path_len; } - virtual bool onPeerPathRecv(Packet *packet, int sender_idx, const uint8_t *secret, uint8_t *path, uint8_t path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) { (void)packet; (void)sender_idx; (void)secret; (void)path; (void)path_len; (void)extra_type; (void)extra; (void)extra_len; return false; } - virtual void onAdvertRecv(Packet *packet, const Identity &id, uint32_t timestamp, const uint8_t *app_data, size_t app_data_len) { (void)packet; (void)id; (void)timestamp; (void)app_data; (void)app_data_len; } - virtual void onAnonDataRecv(Packet *packet, const uint8_t *secret, const Identity &sender, uint8_t *data, size_t len) { (void)packet; (void)secret; (void)sender; (void)data; (void)len; } - virtual void onPathRecv(Packet *packet, Identity &sender, uint8_t *path, uint8_t path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) { (void)packet; (void)sender; (void)path; (void)path_len; (void)extra_type; (void)extra; (void)extra_len; } - virtual void onControlDataRecv(Packet *packet) { (void)packet; } - virtual void onRawDataRecv(Packet *packet) { (void)packet; } - virtual int searchChannelsByHash(const uint8_t *hash, GroupChannel channels[], int max_matches) { (void)hash; (void)channels; (void)max_matches; return 0; } - virtual void onGroupDataRecv(Packet *packet, uint8_t type, const GroupChannel &channel, uint8_t *data, size_t len) { (void)packet; (void)type; (void)channel; (void)data; (void)len; } - virtual void onAckRecv(Packet *packet, uint32_t ack_crc) { (void)packet; (void)ack_crc; } - -public: - Mesh(Radio &radio, MillisecondClock &ms, RNG &rng, RTCClock &rtc, PacketManager &mgr, MeshTables &tables); - void begin(); - void loop(); - void maintenanceLoop(); - - LocalIdentity self_id; - - RNG *getRNG() const { return _rng; } - RTCClock *getRTCClock() const { return _rtc; } - MeshTables *getTables() const { return _tables; } - - Packet *createAdvert(const LocalIdentity &id, const uint8_t *app_data = nullptr, size_t app_data_len = 0); - Packet *createAck(uint32_t ack_crc); - Packet *createMultiAck(uint32_t ack_crc, uint8_t remaining); - Packet *createControlData(const uint8_t *data, size_t len); - Packet *createDatagram(uint8_t type, const Identity &dest, const uint8_t *secret, const uint8_t *data, size_t len); - Packet *createAnonDatagram(uint8_t type, const LocalIdentity &sender, const Identity &dest, const uint8_t *secret, const uint8_t *data, size_t data_len); - Packet *createGroupDatagram(uint8_t type, const GroupChannel &channel, const uint8_t *data, size_t data_len); - Packet *createPathReturn(const Identity &dest, const uint8_t *secret, const uint8_t *path, uint8_t path_len, uint8_t extra_type, const uint8_t *extra, size_t extra_len); - Packet *createPathReturn(const uint8_t *dest_hash, const uint8_t *secret, const uint8_t *path, uint8_t path_len, uint8_t extra_type, const uint8_t *extra, size_t extra_len); - Packet *createRawData(const uint8_t *data, size_t len); - Packet *createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags = 0); - - void sendFlood(Packet *packet, uint32_t delay_millis = 0, uint8_t path_hash_size = 1); - void sendFlood(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis = 0, uint8_t path_hash_size = 1); - void sendDirect(Packet *packet, const uint8_t *path, uint8_t path_len, uint32_t delay_millis = 0); - void sendZeroHop(Packet *packet, uint32_t delay_millis = 0); - void sendZeroHop(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis = 0); -}; - -} /* namespace mesh */ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore Mesh - routing protocol layer + */ + +#pragma once + +#include +#include +#ifdef CONFIG_ZEPHCORE_APC +#include +#endif +#include + +namespace mesh { + +struct GroupChannel { + uint8_t hash[PATH_HASH_SIZE]; + uint8_t secret[PUB_KEY_SIZE]; +}; + +class MeshTables { +public: + virtual bool hasSeen(const Packet *packet) = 0; + virtual void clear(const Packet *packet) = 0; +}; + +class Mesh : public Dispatcher { + RNG *_rng; + RTCClock *_rtc; + MeshTables *_tables; + + void removeSelfFromPath(Packet *packet); + void routeDirectRecvAcks(Packet *packet, uint32_t delay_millis); + DispatcherAction forwardMultipartDirect(Packet *pkt); + +protected: + ContentionTracker _contention; + ContentionTracker& getContentionTracker() { return _contention; } + const ContentionTracker& getContentionTracker() const { return _contention; } +#ifdef CONFIG_ZEPHCORE_APC + PowerController _power_ctrl; + PowerController& getPowerController() { return _power_ctrl; } + const PowerController& getPowerController() const { return _power_ctrl; } +#endif + void extendPendingRetransmit(uint32_t hash32); + + DispatcherAction onRecvPacket(Packet *pkt) override; + virtual uint32_t getCADFailRetryDelay() const override; + virtual DispatcherAction routeRecvPacket(Packet *packet); + virtual bool filterRecvFloodPacket(Packet *packet) { return false; } + virtual bool allowPacketForward(const Packet *packet); + virtual uint32_t getRetransmitDelay(const Packet *packet); + virtual uint32_t getDirectRetransmitDelay(const Packet *packet) { return 0; } + /* Passive contention tracking: if true, track heard floods we don't forward + * (warms the contention EMA on nodes that don't relay, e.g. companions). */ + virtual bool passivelyTrackFloods() const { return false; } + /* Added to caller-supplied delay on every sendFlood. Default 0 (repeater + * behavior). Companion overrides to spread its initial TX adaptively. */ + virtual uint32_t getInitialFloodJitter(const Packet *packet) { (void)packet; return 0; } + virtual uint8_t getExtraAckTransmitCount() const { return 0; } + virtual int searchPeersByHash(const uint8_t *hash) { (void)hash; return 0; } + virtual void getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) { (void)dest_secret; (void)peer_idx; } + virtual void onPeerDataRecv(Packet *packet, uint8_t type, int sender_idx, const uint8_t *secret, uint8_t *data, size_t len) { (void)packet; (void)type; (void)sender_idx; (void)secret; (void)data; (void)len; } + virtual void onTraceRecv(Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) { (void)packet; (void)tag; (void)auth_code; (void)flags; (void)path_snrs; (void)path_hashes; (void)path_len; } + virtual bool onPeerPathRecv(Packet *packet, int sender_idx, const uint8_t *secret, uint8_t *path, uint8_t path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) { (void)packet; (void)sender_idx; (void)secret; (void)path; (void)path_len; (void)extra_type; (void)extra; (void)extra_len; return false; } + virtual void onAdvertRecv(Packet *packet, const Identity &id, uint32_t timestamp, const uint8_t *app_data, size_t app_data_len) { (void)packet; (void)id; (void)timestamp; (void)app_data; (void)app_data_len; } + virtual void onAnonDataRecv(Packet *packet, const uint8_t *secret, const Identity &sender, uint8_t *data, size_t len) { (void)packet; (void)secret; (void)sender; (void)data; (void)len; } + virtual void onPathRecv(Packet *packet, Identity &sender, uint8_t *path, uint8_t path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) { (void)packet; (void)sender; (void)path; (void)path_len; (void)extra_type; (void)extra; (void)extra_len; } + virtual void onControlDataRecv(Packet *packet) { (void)packet; } + virtual void onRawDataRecv(Packet *packet) { (void)packet; } + virtual int searchChannelsByHash(const uint8_t *hash, GroupChannel channels[], int max_matches) { (void)hash; (void)channels; (void)max_matches; return 0; } + virtual void onGroupDataRecv(Packet *packet, uint8_t type, const GroupChannel &channel, uint8_t *data, size_t len) { (void)packet; (void)type; (void)channel; (void)data; (void)len; } + virtual void onAckRecv(Packet *packet, uint32_t ack_crc) { (void)packet; (void)ack_crc; } + +public: + Mesh(Radio &radio, MillisecondClock &ms, RNG &rng, RTCClock &rtc, PacketManager &mgr, MeshTables &tables); + void begin(); + void loop(); + void maintenanceLoop(); + + LocalIdentity self_id; + + RNG *getRNG() const { return _rng; } + RTCClock *getRTCClock() const { return _rtc; } + MeshTables *getTables() const { return _tables; } + + Packet *createAdvert(const LocalIdentity &id, const uint8_t *app_data = nullptr, size_t app_data_len = 0); + Packet *createAck(uint32_t ack_crc); + Packet *createMultiAck(uint32_t ack_crc, uint8_t remaining); + Packet *createControlData(const uint8_t *data, size_t len); + Packet *createDatagram(uint8_t type, const Identity &dest, const uint8_t *secret, const uint8_t *data, size_t len); + Packet *createAnonDatagram(uint8_t type, const LocalIdentity &sender, const Identity &dest, const uint8_t *secret, const uint8_t *data, size_t data_len); + Packet *createGroupDatagram(uint8_t type, const GroupChannel &channel, const uint8_t *data, size_t data_len); + Packet *createPathReturn(const Identity &dest, const uint8_t *secret, const uint8_t *path, uint8_t path_len, uint8_t extra_type, const uint8_t *extra, size_t extra_len); + Packet *createPathReturn(const uint8_t *dest_hash, const uint8_t *secret, const uint8_t *path, uint8_t path_len, uint8_t extra_type, const uint8_t *extra, size_t extra_len); + Packet *createRawData(const uint8_t *data, size_t len); + Packet *createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags = 0); + + void sendFlood(Packet *packet, uint32_t delay_millis = 0, uint8_t path_hash_size = 1); + void sendFlood(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis = 0, uint8_t path_hash_size = 1); + void sendDirect(Packet *packet, const uint8_t *path, uint8_t path_len, uint32_t delay_millis = 0); + void sendZeroHop(Packet *packet, uint32_t delay_millis = 0); + void sendZeroHop(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis = 0); +}; + +} /* namespace mesh */ diff --git a/zephcore/src/ContentionTracker.cpp b/zephcore/src/ContentionTracker.cpp index 6012089..c89a4d6 100644 --- a/zephcore/src/ContentionTracker.cpp +++ b/zephcore/src/ContentionTracker.cpp @@ -1,161 +1,164 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Adaptive Contention Window — dupe-counting based delay estimation - */ - -#include -#include -#include -#include - -namespace mesh { - -ContentionTracker::ContentionTracker() - : _next_idx(0), _ema_x256(0), _finalized_count(0), - _last_retransmit_ms(0), _backoff_multiplier(DEFAULT_BACKOFF_MULT) -{ - memset(_ring, 0, sizeof(_ring)); -} - -/* FNV-1a over payload_type + first 8 payload bytes */ -uint32_t ContentionTracker::computePacketHash32(const Packet *pkt) -{ - uint32_t h = 0x811c9dc5u; /* FNV-1a offset basis */ - uint8_t t = pkt->getPayloadType(); - h = (h ^ t) * 0x01000193u; - int n = pkt->payload_len < 8 ? pkt->payload_len : 8; - for (int i = 0; i < n; i++) { - h = (h ^ pkt->payload[i]) * 0x01000193u; - } - return h; -} - -int ContentionTracker::findEntry(uint32_t hash32) const -{ - for (int i = 0; i < RING_SIZE; i++) { - if (_ring[i].active && _ring[i].hash32 == hash32) { - return i; - } - } - return -1; -} - -void ContentionTracker::finalizeEntry(int idx) -{ - if (!_ring[idx].active) return; - - uint32_t sample_x256 = (uint32_t)_ring[idx].dupe_count << 8; - - int32_t diff = (int32_t)sample_x256 - (int32_t)_ema_x256; - - if (_finalized_count < WARMUP_PACKETS) { - /* Warmup: seed EMA with fast convergence */ - if (_finalized_count == 0) { - _ema_x256 = sample_x256; - } else { - _ema_x256 = (uint32_t)((int32_t)_ema_x256 + (diff >> 1)); - } - } else { - _ema_x256 = (uint32_t)((int32_t)_ema_x256 + (diff >> EMA_SHIFT)); - } - - _finalized_count++; - _ring[idx].active = false; -} - -void ContentionTracker::trackRetransmit(uint32_t hash32, uint32_t now_ms) -{ - _last_retransmit_ms = now_ms; - - /* Evict oldest if ring slot occupied */ - if (_ring[_next_idx].active) { - finalizeEntry(_next_idx); - } - - Entry &e = _ring[_next_idx]; - e.hash32 = hash32; - e.first_seen_ms = now_ms; - e.dupe_count = 0; - e.reactive_added_ms = 0; - e.active = true; - - _next_idx = (_next_idx + 1) % RING_SIZE; -} - -bool ContentionTracker::recordDupeIfTracked(uint32_t hash32, uint32_t now_ms) -{ - int idx = findEntry(hash32); - if (idx < 0) return false; - - Entry &e = _ring[idx]; - - if (now_ms - e.first_seen_ms > WINDOW_MS) { - finalizeEntry(idx); - return false; - } - - if (e.dupe_count < 255) { - e.dupe_count++; - } - return true; -} - -uint16_t ContentionTracker::getReactiveHeadroom(uint32_t hash32, uint32_t airtime_ms) const -{ - int idx = findEntry(hash32); - if (idx < 0) return 0; - - uint32_t per_dupe = (uint32_t)(_backoff_multiplier * (float)airtime_ms); - if (per_dupe == 0) return 0; - - /* Hard cap: REACTIVE_HARD_CAP_MS total extension per packet */ - if (_ring[idx].reactive_added_ms >= REACTIVE_HARD_CAP_MS) return 0; - - uint32_t remaining = REACTIVE_HARD_CAP_MS - _ring[idx].reactive_added_ms; - if (per_dupe > remaining) per_dupe = remaining; - return per_dupe > 0xFFFF ? 0xFFFF : (uint16_t)per_dupe; -} - -void ContentionTracker::addReactiveExtension(uint32_t hash32, uint16_t added_ms) -{ - int idx = findEntry(hash32); - if (idx < 0) return; - - uint32_t total = (uint32_t)_ring[idx].reactive_added_ms + added_ms; - _ring[idx].reactive_added_ms = total > 0xFFFF ? 0xFFFF : (uint16_t)total; -} - -void ContentionTracker::tick(uint32_t now_ms) -{ - for (int i = 0; i < RING_SIZE; i++) { - if (_ring[i].active && now_ms - _ring[i].first_seen_ms > WINDOW_MS) { - finalizeEntry(i); - } - } - - /* Decay EMA toward 0 if no retransmit in STALE_MS */ - if (_last_retransmit_ms != 0 && now_ms - _last_retransmit_ms > STALE_MS) { - if (_ema_x256 > 0) { - _ema_x256 -= _ema_x256 >> EMA_SHIFT; - } - } -} - -float ContentionTracker::getContentionEstimate() const -{ - return (float)_ema_x256 / 256.0f; -} - -float ContentionTracker::getFloodDelayFactor() const -{ - if (!isWarmedUp()) return 0.5f; /* conservative default before warmup */ - - float est = getContentionEstimate(); - if (est <= 0.0f) return MIN_FLOOD_FACTOR; - - float factor = MIN_FLOOD_FACTOR + FLOOD_SCALE * sqrtf(est); - if (factor > MAX_FLOOD_FACTOR) factor = MAX_FLOOD_FACTOR; - return factor; -} - -} /* namespace mesh */ +/* + * SPDX-License-Identifier: Apache-2.0 + * Adaptive Contention Window — dupe-counting based delay estimation + */ + +#include +#include +#include +#include + +namespace mesh { + +ContentionTracker::ContentionTracker() + : _next_idx(0), _ema_x256(0), _finalized_count(0), + _last_retransmit_ms(0), _backoff_multiplier(DEFAULT_BACKOFF_MULT) +{ + memset(_ring, 0, sizeof(_ring)); +} + +/* FNV-1a over payload_type + first 8 payload bytes */ +uint32_t ContentionTracker::computePacketHash32(const Packet *pkt) +{ + uint32_t h = 0x811c9dc5u; /* FNV-1a offset basis */ + uint8_t t = pkt->getPayloadType(); + h = (h ^ t) * 0x01000193u; + int n = pkt->payload_len < 8 ? pkt->payload_len : 8; + for (int i = 0; i < n; i++) { + h = (h ^ pkt->payload[i]) * 0x01000193u; + } + return h; +} + +int ContentionTracker::findEntry(uint32_t hash32) const +{ + for (int i = 0; i < RING_SIZE; i++) { + if (_ring[i].active && _ring[i].hash32 == hash32) { + return i; + } + } + return -1; +} + +void ContentionTracker::finalizeEntry(int idx) +{ + if (!_ring[idx].active) return; + + uint32_t sample_x256 = (uint32_t)_ring[idx].dupe_count << 8; + + int32_t diff = (int32_t)sample_x256 - (int32_t)_ema_x256; + + if (_finalized_count < WARMUP_PACKETS) { + /* Warmup: seed EMA with fast convergence */ + if (_finalized_count == 0) { + _ema_x256 = sample_x256; + } else { + _ema_x256 = (uint32_t)((int32_t)_ema_x256 + (diff >> 1)); + } + } else { + _ema_x256 = (uint32_t)((int32_t)_ema_x256 + (diff >> EMA_SHIFT)); + } + + _finalized_count++; + _ring[idx].active = false; +} + +void ContentionTracker::trackRetransmit(uint32_t hash32, uint32_t now_ms) +{ + _last_retransmit_ms = now_ms; + + /* Evict oldest if ring slot occupied */ + if (_ring[_next_idx].active) { + finalizeEntry(_next_idx); + } + + Entry &e = _ring[_next_idx]; + e.hash32 = hash32; + e.first_seen_ms = now_ms; + e.dupe_count = 0; + e.reactive_added_ms = 0; + e.active = true; + + _next_idx = (_next_idx + 1) % RING_SIZE; +} + +bool ContentionTracker::recordDupeIfTracked(uint32_t hash32, uint32_t now_ms) +{ + int idx = findEntry(hash32); + if (idx < 0) return false; + + Entry &e = _ring[idx]; + + if (now_ms - e.first_seen_ms > WINDOW_MS) { + finalizeEntry(idx); + return false; + } + + if (e.dupe_count < 255) { + e.dupe_count++; + } + return true; +} + +uint16_t ContentionTracker::getReactiveHeadroom(uint32_t hash32, uint32_t airtime_ms) const +{ + int idx = findEntry(hash32); + if (idx < 0) return 0; + + uint32_t per_dupe = (uint32_t)(_backoff_multiplier * (float)airtime_ms); + if (per_dupe == 0) return 0; + + /* Effective cap: ~12 relay-slots (airtime-scaled), absolute ceiling REACTIVE_HARD_CAP_MS */ + uint32_t effective_cap = 12 * airtime_ms; + if (effective_cap > REACTIVE_HARD_CAP_MS) effective_cap = REACTIVE_HARD_CAP_MS; + + if (_ring[idx].reactive_added_ms >= effective_cap) return 0; + + uint32_t remaining = effective_cap - _ring[idx].reactive_added_ms; + if (per_dupe > remaining) per_dupe = remaining; + return per_dupe > 0xFFFF ? 0xFFFF : (uint16_t)per_dupe; +} + +void ContentionTracker::addReactiveExtension(uint32_t hash32, uint16_t added_ms) +{ + int idx = findEntry(hash32); + if (idx < 0) return; + + uint32_t total = (uint32_t)_ring[idx].reactive_added_ms + added_ms; + _ring[idx].reactive_added_ms = total > 0xFFFF ? 0xFFFF : (uint16_t)total; +} + +void ContentionTracker::tick(uint32_t now_ms) +{ + for (int i = 0; i < RING_SIZE; i++) { + if (_ring[i].active && now_ms - _ring[i].first_seen_ms > WINDOW_MS) { + finalizeEntry(i); + } + } + + /* Decay EMA toward 0 if no retransmit in STALE_MS */ + if (_last_retransmit_ms != 0 && now_ms - _last_retransmit_ms > STALE_MS) { + if (_ema_x256 > 0) { + _ema_x256 -= _ema_x256 >> EMA_SHIFT; + } + } +} + +float ContentionTracker::getContentionEstimate() const +{ + return (float)_ema_x256 / 256.0f; +} + +float ContentionTracker::getFloodDelayFactor() const +{ + if (!isWarmedUp()) return 0.5f; /* conservative default before warmup */ + + float est = getContentionEstimate(); + if (est <= 0.0f) return MIN_FLOOD_FACTOR; + + float factor = MIN_FLOOD_FACTOR + FLOOD_SCALE * sqrtf(est); + if (factor > MAX_FLOOD_FACTOR) factor = MAX_FLOOD_FACTOR; + return factor; +} + +} /* namespace mesh */ diff --git a/zephcore/src/Mesh.cpp b/zephcore/src/Mesh.cpp index 23cac45..67225a9 100644 --- a/zephcore/src/Mesh.cpp +++ b/zephcore/src/Mesh.cpp @@ -1,719 +1,723 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * ZephCore Mesh - minimal port for Phase 5 - */ - -#include -#include -#include - -#include -LOG_MODULE_REGISTER(zephcore_mesh, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); - -namespace mesh { - -Mesh::Mesh(Radio &radio, MillisecondClock &ms, RNG &rng, RTCClock &rtc, PacketManager &mgr, MeshTables &tables) - : Dispatcher(radio, ms, mgr), _rng(&rng), _rtc(&rtc), _tables(&tables) -{ -} - -void Mesh::begin() -{ - Dispatcher::begin(); -} - -void Mesh::loop() -{ - Dispatcher::loop(); -} - -void Mesh::maintenanceLoop() -{ - Dispatcher::maintenanceLoop(); - uint32_t now = (uint32_t)_ms->getMillis(); - _contention.tick(now); -#ifdef CONFIG_ZEPHCORE_APC - _power_ctrl.tick(now); - _radio->setTxPowerReduction(_power_ctrl.getPowerReduction()); -#endif -} - -void Mesh::extendPendingRetransmit(uint32_t hash32) -{ - uint32_t now = (uint32_t)_ms->getMillis(); - int total = _mgr->getOutboundTotal(); - for (int i = 0; i < total; i++) { - Packet *pkt = _mgr->getOutboundByIdx(i); - if (pkt && pkt->isRouteFlood() - && ContentionTracker::computePacketHash32(pkt) == hash32) { - uint32_t airtime = _radio->getEstAirtimeFor(pkt->getRawLength()); - uint16_t delay = _contention.getReactiveHeadroom(hash32, airtime); - if (delay == 0) break; - /* Reschedule from NOW: heard a dupe, defer by one - * backoff_multiplier × airtime window per dupe. */ - _mgr->rescheduleOutbound(i, now + delay); - _contention.addReactiveExtension(hash32, delay); - notifyTxQueued(delay); - break; - } - } -} - -bool Mesh::allowPacketForward(const Packet *packet) -{ - (void)packet; - return false; -} - -uint32_t Mesh::getRetransmitDelay(const Packet *packet) -{ - uint32_t t = (_radio->getEstAirtimeFor(packet->getRawLength()) * 52 / 50) / 2; - return _rng->nextInt(0, 5) * t; -} - -uint32_t Mesh::getCADFailRetryDelay() const -{ - return _rng->nextInt(1, 4) * 120; -} - -void Mesh::removeSelfFromPath(Packet *pkt) -{ - pkt->setPathHashCount(pkt->getPathHashCount() - 1); // decrement the count - - uint8_t sz = pkt->getPathHashSize(); - for (int k = 0; k < pkt->getPathHashCount()*sz; k += sz) { // shuffle path by 1 'entry' - memcpy(&pkt->path[k], &pkt->path[k + sz], sz); - } -} - -DispatcherAction Mesh::routeRecvPacket(Packet *packet) -{ - uint8_t n = packet->getPathHashCount(); - if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit() - && (n + 1)*packet->getPathHashSize() <= MAX_PATH_SIZE && allowPacketForward(packet)) { - // append this node's hash to 'path' - self_id.copyHashTo(&packet->path[n * packet->getPathHashSize()], packet->getPathHashSize()); - packet->setPathHashCount(n + 1); - uint32_t h = ContentionTracker::computePacketHash32(packet); - _contention.trackRetransmit(h, (uint32_t)_ms->getMillis()); -#ifdef CONFIG_ZEPHCORE_APC - _power_ctrl.trackTransmit(h, (uint32_t)_ms->getMillis()); -#endif - uint32_t d = getRetransmitDelay(packet); - return ACTION_RETRANSMIT_DELAYED(packet->getPathHashCount(), d); // give priority to closer sources - } - return ACTION_RELEASE; -} - -DispatcherAction Mesh::forwardMultipartDirect(Packet *pkt) -{ - uint8_t remaining = pkt->payload[0] >> 4; - uint8_t type = pkt->payload[0] & 0x0F; - if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { - Packet tmp; - tmp.header = pkt->header; - tmp.path_len = Packet::copyPath(tmp.path, pkt->path, pkt->path_len); - tmp.payload_len = pkt->payload_len - 1; - memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); - if (!_tables->hasSeen(&tmp)) { - removeSelfFromPath(&tmp); - routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); - } - } - return ACTION_RELEASE; -} - -void Mesh::routeDirectRecvAcks(Packet *packet, uint32_t delay_millis) -{ - if (!packet->isMarkedDoNotRetransmit()) { - uint32_t crc; - memcpy(&crc, packet->payload, 4); - Packet *a2 = createAck(crc); - if (a2) { - a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len); - a2->header &= ~PH_ROUTE_MASK; - a2->header |= ROUTE_TYPE_DIRECT; - sendPacket(a2, 0, delay_millis); - } - } -} - -DispatcherAction Mesh::onRecvPacket(Packet *pkt) -{ - // Handle direct TRACE packets - if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { - if (pkt->path_len < MAX_PATH_SIZE) { - int i = 0; - uint32_t trace_tag; - memcpy(&trace_tag, &pkt->payload[i], 4); i += 4; - uint32_t auth_code; - memcpy(&auth_code, &pkt->payload[i], 4); i += 4; - uint8_t flags = pkt->payload[i++]; - uint8_t path_sz = flags & 0x03; - - uint8_t len = pkt->payload_len - i; - uint8_t offset = pkt->path_len << path_sz; - if (offset >= len) { - onTraceRecv(pkt, trace_tag, auth_code, flags, pkt->path, &pkt->payload[i], len); - } else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->hasSeen(pkt)) { - pkt->path[pkt->path_len++] = (int8_t)(pkt->getSNR() * 4); - uint32_t d = getDirectRetransmitDelay(pkt); - return ACTION_RETRANSMIT_DELAYED(5, d); - } - } - return ACTION_RELEASE; - } - - // Handle direct CONTROL packets (zero-hop only) - if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_CONTROL && (pkt->payload[0] & 0x80) != 0) { - if (pkt->getPathHashCount() == 0) { - onControlDataRecv(pkt); - } - return ACTION_RELEASE; - } - - // Handle direct zero-hop ACKs (path_len=0) - if (pkt->isRouteDirect() && pkt->getPathHashCount() == 0 && pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { - uint32_t ack_crc; - memcpy(&ack_crc, pkt->payload, 4); - onAckRecv(pkt, ack_crc); - return ACTION_RELEASE; - } - - if (pkt->isRouteDirect() && pkt->getPathHashCount() > 0) { - if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { - uint32_t ack_crc; - memcpy(&ack_crc, pkt->payload, 4); - onAckRecv(pkt, ack_crc); - } - if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) && allowPacketForward(pkt)) { - if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { - return forwardMultipartDirect(pkt); - } - if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { - if (!_tables->hasSeen(pkt)) { - removeSelfFromPath(pkt); - routeDirectRecvAcks(pkt, 0); - } - return ACTION_RELEASE; - } - if (!_tables->hasSeen(pkt)) { - removeSelfFromPath(pkt); - return ACTION_RETRANSMIT_DELAYED(0, getDirectRetransmitDelay(pkt)); - } - } - return ACTION_RELEASE; - } - - if (pkt->isRouteFlood() && filterRecvFloodPacket(pkt)) return ACTION_RELEASE; - - /* Record dupes for contention tracking + reactive backoff */ - if (pkt->isRouteFlood()) { - uint32_t h = ContentionTracker::computePacketHash32(pkt); -#ifdef CONFIG_ZEPHCORE_APC - uint8_t first_hop = (pkt->getPathHashCount() > 0) ? pkt->path[0] : 0; - _power_ctrl.recordEcho(h, pkt->_snr, first_hop, (uint32_t)_ms->getMillis()); -#endif - if (_contention.recordDupeIfTracked(h, (uint32_t)_ms->getMillis())) { - extendPendingRetransmit(h); - } - } - - DispatcherAction action = ACTION_RELEASE; - - switch (pkt->getPayloadType()) { - case PAYLOAD_TYPE_ACK: { - uint32_t ack_crc; - memcpy(&ack_crc, pkt->payload, 4); - if (!_tables->hasSeen(pkt)) { - onAckRecv(pkt, ack_crc); - action = routeRecvPacket(pkt); - } - break; - } - case PAYLOAD_TYPE_PATH: - case PAYLOAD_TYPE_REQ: - case PAYLOAD_TYPE_RESPONSE: - case PAYLOAD_TYPE_TXT_MSG: { - int i = 0; - uint8_t dest_hash = pkt->payload[i++]; - uint8_t src_hash = pkt->payload[i++]; - - uint8_t *macAndData = &pkt->payload[i]; - if (i + CIPHER_MAC_SIZE >= (int)pkt->payload_len) { - LOG_WRN("onRecvPacket: incomplete packet (i=%d, payload_len=%d)", i, pkt->payload_len); - } else if (!_tables->hasSeen(pkt)) { - if (self_id.isHashMatch(&dest_hash)) { - int num = searchPeersByHash(&src_hash); - bool found = false; - for (int j = 0; j < num; j++) { - uint8_t secret[PUB_KEY_SIZE]; - getPeerSharedSecret(secret, j); - - uint8_t data[MAX_PACKET_PAYLOAD]; - int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i); - if (len > 0) { - if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) { - int k = 0; - uint8_t path_len = data[k++]; - uint8_t hash_size = (path_len >> 6) + 1; - uint8_t hash_count = path_len & 63; - uint8_t *path = &data[k]; k += hash_size*hash_count; - uint8_t extra_type = data[k++] & 0x0F; - uint8_t *extra = &data[k]; - uint8_t extra_len = len - k; - if (onPeerPathRecv(pkt, j, secret, path, path_len, extra_type, extra, extra_len)) { - if (pkt->isRouteFlood()) { - Packet *rpath = createPathReturn(&src_hash, secret, pkt->path, pkt->path_len, 0, nullptr, 0); - if (rpath) sendDirect(rpath, path, path_len, 500); - } - } - } else { - onPeerDataRecv(pkt, pkt->getPayloadType(), j, secret, data, len); - } - found = true; - break; - } - } - if (found) { - pkt->markDoNotRetransmit(); - } else { - LOG_WRN("onRecvPacket: no peer could decrypt message"); - } - } - action = routeRecvPacket(pkt); - } - break; - } - case PAYLOAD_TYPE_ANON_REQ: { - int i = 0; - uint8_t dest_hash = pkt->payload[i++]; - uint8_t *sender_pub_key = &pkt->payload[i]; i += PUB_KEY_SIZE; - - uint8_t *macAndData = &pkt->payload[i]; - if (i + 2 >= (int)pkt->payload_len) { - // incomplete packet - } else if (!_tables->hasSeen(pkt)) { - if (self_id.isHashMatch(&dest_hash)) { - Identity sender(sender_pub_key); - uint8_t secret[PUB_KEY_SIZE]; - self_id.calcSharedSecret(secret, sender); - - uint8_t data[MAX_PACKET_PAYLOAD]; - int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i); - if (len > 0) { - onAnonDataRecv(pkt, secret, sender, data, len); - pkt->markDoNotRetransmit(); - } - } - action = routeRecvPacket(pkt); - } - break; - } - case PAYLOAD_TYPE_GRP_DATA: - case PAYLOAD_TYPE_GRP_TXT: { - int i = 0; - uint8_t channel_hash = pkt->payload[i++]; - - uint8_t *macAndData = &pkt->payload[i]; - if (i + 2 >= (int)pkt->payload_len) { - // incomplete packet - } else if (!_tables->hasSeen(pkt)) { - GroupChannel channels[4]; - int num = searchChannelsByHash(&channel_hash, channels, 4); - for (int j = 0; j < num; j++) { - uint8_t data[MAX_PACKET_PAYLOAD]; - int len = Utils::MACThenDecrypt(channels[j].secret, data, macAndData, pkt->payload_len - i); - if (len > 0) { - onGroupDataRecv(pkt, pkt->getPayloadType(), channels[j], data, len); - break; - } - } - action = routeRecvPacket(pkt); - } - break; - } - case PAYLOAD_TYPE_ADVERT: { - int i = 0; - Identity id; - memcpy(id.pub_key, &pkt->payload[i], PUB_KEY_SIZE); - i += PUB_KEY_SIZE; - uint32_t timestamp; - memcpy(×tamp, &pkt->payload[i], 4); - i += 4; - const uint8_t *signature = &pkt->payload[i]; - i += SIGNATURE_SIZE; - if (i <= (int)pkt->payload_len && !self_id.matches(id.pub_key) && !_tables->hasSeen(pkt)) { - uint8_t *app_data = (uint8_t *)&pkt->payload[i]; - size_t app_data_len = pkt->payload_len - (size_t)i; - if (app_data_len > MAX_ADVERT_DATA_SIZE) app_data_len = MAX_ADVERT_DATA_SIZE; - uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE]; - int msg_len = 0; - memcpy(&message[msg_len], id.pub_key, PUB_KEY_SIZE); msg_len += PUB_KEY_SIZE; - memcpy(&message[msg_len], ×tamp, 4); msg_len += 4; - memcpy(&message[msg_len], app_data, app_data_len); msg_len += app_data_len; - if (id.verify(signature, message, msg_len)) { - onAdvertRecv(pkt, id, timestamp, app_data, app_data_len); - action = routeRecvPacket(pkt); - } - } - break; - } - case PAYLOAD_TYPE_RAW_CUSTOM: - if (pkt->isRouteDirect() && !_tables->hasSeen(pkt)) { - onRawDataRecv(pkt); - } - break; - case PAYLOAD_TYPE_MULTIPART: - if (pkt->payload_len > 2) { - /* uint8_t remaining = pkt->payload[0] >> 4; */ /* Reserved for future multipart support */ - uint8_t type = pkt->payload[0] & 0x0F; - - if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { - Packet tmp; - tmp.header = pkt->header; - tmp.path_len = Packet::copyPath(tmp.path, pkt->path, pkt->path_len); - tmp.payload_len = pkt->payload_len - 1; - memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); - - if (!_tables->hasSeen(&tmp)) { - uint32_t ack_crc; - memcpy(&ack_crc, tmp.payload, 4); - onAckRecv(&tmp, ack_crc); - } - } - } - break; - default: - break; - } - return action; -} - -Packet *Mesh::createAdvert(const LocalIdentity &id, const uint8_t *app_data, size_t app_data_len) -{ - if (app_data_len > MAX_ADVERT_DATA_SIZE) return nullptr; - - Packet *packet = obtainNewPacket(); - if (packet == nullptr) return nullptr; - - packet->header = (PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT); - int len = 0; - memcpy(&packet->payload[len], id.pub_key, PUB_KEY_SIZE); - len += PUB_KEY_SIZE; - uint32_t emitted_timestamp = _rtc->getCurrentTime(); - memcpy(&packet->payload[len], &emitted_timestamp, 4); - len += 4; - uint8_t *signature = &packet->payload[len]; - len += SIGNATURE_SIZE; - if (app_data && app_data_len > 0) { - memcpy(&packet->payload[len], app_data, app_data_len); - len += (int)app_data_len; - } - packet->payload_len = len; - - uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE]; - int msg_len = 0; - memcpy(&message[msg_len], id.pub_key, PUB_KEY_SIZE); msg_len += PUB_KEY_SIZE; - memcpy(&message[msg_len], &emitted_timestamp, 4); msg_len += 4; - if (app_data && app_data_len > 0) { - memcpy(&message[msg_len], app_data, app_data_len); msg_len += (int)app_data_len; - } - id.sign(signature, message, msg_len); - return packet; -} - -Packet *Mesh::createAck(uint32_t ack_crc) -{ - Packet *packet = obtainNewPacket(); - if (packet == nullptr) return nullptr; - packet->header = (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT); - memcpy(packet->payload, &ack_crc, 4); - packet->payload_len = 4; - return packet; -} - -Packet *Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) -{ - Packet *packet = obtainNewPacket(); - if (packet == nullptr) return nullptr; - packet->header = (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT); - packet->payload[0] = (remaining << 4) | PAYLOAD_TYPE_ACK; - memcpy(&packet->payload[1], &ack_crc, 4); - packet->payload_len = 5; - return packet; -} - -Packet *Mesh::createControlData(const uint8_t *data, size_t len) -{ - if (len > sizeof(Packet::payload)) return nullptr; - Packet *packet = obtainNewPacket(); - if (packet == nullptr) return nullptr; - packet->header = (PAYLOAD_TYPE_CONTROL << PH_TYPE_SHIFT); - memcpy(packet->payload, data, len); - packet->payload_len = (uint16_t)len; - return packet; -} - -void Mesh::sendFlood(Packet *packet, uint32_t delay_millis, uint8_t path_hash_size) -{ - if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { - releasePacket(packet); - return; - } - if (path_hash_size == 0 || path_hash_size > 3) { - LOG_WRN("sendFlood: invalid path_hash_size"); - releasePacket(packet); - return; - } - packet->header &= ~PH_ROUTE_MASK; - packet->header |= ROUTE_TYPE_FLOOD; - packet->setPathHashSizeAndCount(path_hash_size, 0); - _tables->hasSeen(packet); -#ifdef CONFIG_ZEPHCORE_APC - { - uint32_t h = ContentionTracker::computePacketHash32(packet); - _power_ctrl.trackTransmit(h, (uint32_t)_ms->getMillis()); - } -#endif - - uint8_t pri; - if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { - pri = 2; - } else if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) { - pri = 3; - } else { - pri = 1; - } - sendPacket(packet, pri, delay_millis); -} - -void Mesh::sendFlood(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis, uint8_t path_hash_size) -{ - if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { - releasePacket(packet); - return; - } - if (path_hash_size == 0 || path_hash_size > 3) { - LOG_WRN("sendFlood: invalid path_hash_size"); - releasePacket(packet); - return; - } - packet->header &= ~PH_ROUTE_MASK; - packet->header |= ROUTE_TYPE_TRANSPORT_FLOOD; - packet->transport_codes[0] = transport_codes[0]; - packet->transport_codes[1] = transport_codes[1]; - packet->setPathHashSizeAndCount(path_hash_size, 0); - _tables->hasSeen(packet); -#ifdef CONFIG_ZEPHCORE_APC - { - uint32_t h = ContentionTracker::computePacketHash32(packet); - _power_ctrl.trackTransmit(h, (uint32_t)_ms->getMillis()); - } -#endif - - uint8_t pri; - if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { - pri = 2; - } else if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) { - pri = 3; - } else { - pri = 1; - } - sendPacket(packet, pri, delay_millis); -} - -void Mesh::sendDirect(Packet *packet, const uint8_t *path, uint8_t path_len, uint32_t delay_millis) -{ - packet->header &= ~PH_ROUTE_MASK; - packet->header |= ROUTE_TYPE_DIRECT; - - uint8_t pri; - if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { - /* For TRACE packets, path is appended to end of PAYLOAD (used for SNRs) */ - memcpy(&packet->payload[packet->payload_len], path, path_len); - packet->payload_len += path_len; - packet->path_len = 0; - pri = 5; - } else { - packet->path_len = Packet::copyPath(packet->path, path, path_len); - if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { - pri = 1; - } else { - pri = 0; - } - } - - _tables->hasSeen(packet); - sendPacket(packet, pri, delay_millis); -} - -void Mesh::sendZeroHop(Packet *packet, uint32_t delay_millis) -{ - packet->header &= ~PH_ROUTE_MASK; - packet->header |= ROUTE_TYPE_DIRECT; - packet->path_len = 0; - _tables->hasSeen(packet); - sendPacket(packet, 0, delay_millis); -} - -void Mesh::sendZeroHop(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis) -{ - packet->header &= ~PH_ROUTE_MASK; - packet->header |= ROUTE_TYPE_TRANSPORT_DIRECT; - packet->transport_codes[0] = transport_codes[0]; - packet->transport_codes[1] = transport_codes[1]; - packet->path_len = 0; - _tables->hasSeen(packet); - sendPacket(packet, 0, delay_millis); -} - -#define MAX_COMBINED_PATH (MAX_PACKET_PAYLOAD - 2 - CIPHER_BLOCK_SIZE) - -Packet *Mesh::createPathReturn(const Identity &dest, const uint8_t *secret, const uint8_t *path, uint8_t path_len, - uint8_t extra_type, const uint8_t *extra, size_t extra_len) -{ - uint8_t dest_hash[PATH_HASH_SIZE]; - dest.copyHashTo(dest_hash); - return createPathReturn(dest_hash, secret, path, path_len, extra_type, extra, extra_len); -} - -Packet *Mesh::createPathReturn(const uint8_t *dest_hash, const uint8_t *secret, const uint8_t *path, uint8_t path_len, - uint8_t extra_type, const uint8_t *extra, size_t extra_len) -{ - uint8_t path_hash_size = (path_len >> 6) + 1; - uint8_t path_hash_count = path_len & 63; - - if (path_hash_count*path_hash_size + extra_len + 5 > MAX_COMBINED_PATH) return nullptr; - - Packet *packet = obtainNewPacket(); - if (packet == nullptr) return nullptr; - - packet->header = (PAYLOAD_TYPE_PATH << PH_TYPE_SHIFT); - - int len = 0; - memcpy(&packet->payload[len], dest_hash, PATH_HASH_SIZE); len += PATH_HASH_SIZE; - len += self_id.copyHashTo(&packet->payload[len]); - - { - int data_len = 0; - uint8_t data[MAX_PACKET_PAYLOAD]; - - data[data_len++] = path_len; - memcpy(&data[data_len], path, path_hash_count*path_hash_size); data_len += path_hash_count*path_hash_size; - if (extra_len > 0) { - data[data_len++] = extra_type; - memcpy(&data[data_len], extra, extra_len); data_len += extra_len; - } else { - data[data_len++] = 0xFF; // dummy payload type - _rng->random(&data[data_len], 4); data_len += 4; - } - - len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); - } - - packet->payload_len = len; - return packet; -} - -Packet *Mesh::createDatagram(uint8_t type, const Identity &dest, const uint8_t *secret, const uint8_t *data, size_t data_len) -{ - if (type == PAYLOAD_TYPE_TXT_MSG || type == PAYLOAD_TYPE_REQ || type == PAYLOAD_TYPE_RESPONSE) { - if (data_len + CIPHER_MAC_SIZE + CIPHER_BLOCK_SIZE - 1 > MAX_PACKET_PAYLOAD) { - LOG_WRN("createDatagram: data too large"); - return nullptr; - } - } else { - LOG_WRN("createDatagram: unsupported type %d", type); - return nullptr; - } - - Packet *packet = obtainNewPacket(); - if (packet == nullptr) { - LOG_ERR("createDatagram: packet alloc failed"); - return nullptr; - } - - packet->header = (type << PH_TYPE_SHIFT); - - int len = 0; - len += dest.copyHashTo(&packet->payload[len]); - len += self_id.copyHashTo(&packet->payload[len]); - len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); - - packet->payload_len = len; - return packet; -} - -Packet *Mesh::createAnonDatagram(uint8_t type, const LocalIdentity &sender, const Identity &dest, - const uint8_t *secret, const uint8_t *data, size_t data_len) -{ - if (type == PAYLOAD_TYPE_ANON_REQ) { - if (data_len + 1 + PUB_KEY_SIZE + CIPHER_BLOCK_SIZE - 1 > MAX_PACKET_PAYLOAD) return nullptr; - } else { - return nullptr; - } - - Packet *packet = obtainNewPacket(); - if (packet == nullptr) return nullptr; - - packet->header = (type << PH_TYPE_SHIFT); - - int len = 0; - if (type == PAYLOAD_TYPE_ANON_REQ) { - len += dest.copyHashTo(&packet->payload[len]); - memcpy(&packet->payload[len], sender.pub_key, PUB_KEY_SIZE); len += PUB_KEY_SIZE; - } - len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); - - packet->payload_len = len; - return packet; -} - -Packet *Mesh::createGroupDatagram(uint8_t type, const GroupChannel &channel, const uint8_t *data, size_t data_len) -{ - if (!(type == PAYLOAD_TYPE_GRP_TXT || type == PAYLOAD_TYPE_GRP_DATA)) return nullptr; - if (data_len + 1 + CIPHER_BLOCK_SIZE - 1 > MAX_PACKET_PAYLOAD) return nullptr; - - Packet *packet = obtainNewPacket(); - if (packet == nullptr) return nullptr; - - packet->header = (type << PH_TYPE_SHIFT); - - int len = 0; - memcpy(&packet->payload[len], channel.hash, PATH_HASH_SIZE); len += PATH_HASH_SIZE; - len += Utils::encryptThenMAC(channel.secret, &packet->payload[len], data, data_len); - - packet->payload_len = len; - return packet; -} - -Packet *Mesh::createRawData(const uint8_t *data, size_t len) -{ - if (len > sizeof(Packet::payload)) return nullptr; - - Packet *packet = obtainNewPacket(); - if (packet == nullptr) return nullptr; - - packet->header = (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT); - memcpy(packet->payload, data, len); - packet->payload_len = (uint16_t)len; - - return packet; -} - -Packet *Mesh::createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags) -{ - Packet *packet = obtainNewPacket(); - if (packet == nullptr) return nullptr; - - packet->header = (PAYLOAD_TYPE_TRACE << PH_TYPE_SHIFT); - memcpy(packet->payload, &tag, 4); - memcpy(&packet->payload[4], &auth_code, 4); - packet->payload[8] = flags; - packet->payload_len = 9; - - return packet; -} - -} /* namespace mesh */ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore Mesh - minimal port for Phase 5 + */ + +#include +#include +#include + +#include +LOG_MODULE_REGISTER(zephcore_mesh, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); + +namespace mesh { + +Mesh::Mesh(Radio &radio, MillisecondClock &ms, RNG &rng, RTCClock &rtc, PacketManager &mgr, MeshTables &tables) + : Dispatcher(radio, ms, mgr), _rng(&rng), _rtc(&rtc), _tables(&tables) +{ +} + +void Mesh::begin() +{ + Dispatcher::begin(); +} + +void Mesh::loop() +{ + Dispatcher::loop(); +} + +void Mesh::maintenanceLoop() +{ + Dispatcher::maintenanceLoop(); + uint32_t now = (uint32_t)_ms->getMillis(); + _contention.tick(now); +#ifdef CONFIG_ZEPHCORE_APC + _power_ctrl.tick(now); + _radio->setTxPowerReduction(_power_ctrl.getPowerReduction()); +#endif +} + +void Mesh::extendPendingRetransmit(uint32_t hash32) +{ + uint32_t now = (uint32_t)_ms->getMillis(); + int total = _mgr->getOutboundTotal(); + for (int i = 0; i < total; i++) { + Packet *pkt = _mgr->getOutboundByIdx(i); + if (pkt && pkt->isRouteFlood() + && ContentionTracker::computePacketHash32(pkt) == hash32) { + uint32_t airtime = _radio->getEstAirtimeFor(pkt->getRawLength()); + uint16_t delay = _contention.getReactiveHeadroom(hash32, airtime); + if (delay == 0) break; + /* Reschedule from NOW: heard a dupe, defer by one + * backoff_multiplier × airtime window per dupe. */ + _mgr->rescheduleOutbound(i, now + delay); + _contention.addReactiveExtension(hash32, delay); + notifyTxQueued(delay); + break; + } + } +} + +bool Mesh::allowPacketForward(const Packet *packet) +{ + (void)packet; + return false; +} + +uint32_t Mesh::getRetransmitDelay(const Packet *packet) +{ + uint32_t t = (_radio->getEstAirtimeFor(packet->getRawLength()) * 52 / 50) / 2; + return _rng->nextInt(0, 5) * t; +} + +uint32_t Mesh::getCADFailRetryDelay() const +{ + return _rng->nextInt(1, 4) * 120; +} + +void Mesh::removeSelfFromPath(Packet *pkt) +{ + pkt->setPathHashCount(pkt->getPathHashCount() - 1); // decrement the count + + uint8_t sz = pkt->getPathHashSize(); + for (int k = 0; k < pkt->getPathHashCount()*sz; k += sz) { // shuffle path by 1 'entry' + memcpy(&pkt->path[k], &pkt->path[k + sz], sz); + } +} + +DispatcherAction Mesh::routeRecvPacket(Packet *packet) +{ + uint8_t n = packet->getPathHashCount(); + if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit() + && (n + 1)*packet->getPathHashSize() <= MAX_PATH_SIZE && allowPacketForward(packet)) { + // append this node's hash to 'path' + self_id.copyHashTo(&packet->path[n * packet->getPathHashSize()], packet->getPathHashSize()); + packet->setPathHashCount(n + 1); + uint32_t h = ContentionTracker::computePacketHash32(packet); + _contention.trackRetransmit(h, (uint32_t)_ms->getMillis()); +#ifdef CONFIG_ZEPHCORE_APC + _power_ctrl.trackTransmit(h, (uint32_t)_ms->getMillis()); +#endif + uint32_t d = getRetransmitDelay(packet); + return ACTION_RETRANSMIT_DELAYED(packet->getPathHashCount(), d); // give priority to closer sources + } + return ACTION_RELEASE; +} + +DispatcherAction Mesh::forwardMultipartDirect(Packet *pkt) +{ + uint8_t remaining = pkt->payload[0] >> 4; + uint8_t type = pkt->payload[0] & 0x0F; + if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { + Packet tmp; + tmp.header = pkt->header; + tmp.path_len = Packet::copyPath(tmp.path, pkt->path, pkt->path_len); + tmp.payload_len = pkt->payload_len - 1; + memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); + if (!_tables->hasSeen(&tmp)) { + removeSelfFromPath(&tmp); + routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); + } + } + return ACTION_RELEASE; +} + +void Mesh::routeDirectRecvAcks(Packet *packet, uint32_t delay_millis) +{ + if (!packet->isMarkedDoNotRetransmit()) { + uint32_t crc; + memcpy(&crc, packet->payload, 4); + Packet *a2 = createAck(crc); + if (a2) { + a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len); + a2->header &= ~PH_ROUTE_MASK; + a2->header |= ROUTE_TYPE_DIRECT; + sendPacket(a2, 0, delay_millis); + } + } +} + +DispatcherAction Mesh::onRecvPacket(Packet *pkt) +{ + // Handle direct TRACE packets + if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { + if (pkt->path_len < MAX_PATH_SIZE) { + int i = 0; + uint32_t trace_tag; + memcpy(&trace_tag, &pkt->payload[i], 4); i += 4; + uint32_t auth_code; + memcpy(&auth_code, &pkt->payload[i], 4); i += 4; + uint8_t flags = pkt->payload[i++]; + uint8_t path_sz = flags & 0x03; + + uint8_t len = pkt->payload_len - i; + uint8_t offset = pkt->path_len << path_sz; + if (offset >= len) { + onTraceRecv(pkt, trace_tag, auth_code, flags, pkt->path, &pkt->payload[i], len); + } else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->hasSeen(pkt)) { + pkt->path[pkt->path_len++] = (int8_t)(pkt->getSNR() * 4); + uint32_t d = getDirectRetransmitDelay(pkt); + return ACTION_RETRANSMIT_DELAYED(5, d); + } + } + return ACTION_RELEASE; + } + + // Handle direct CONTROL packets (zero-hop only) + if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_CONTROL && (pkt->payload[0] & 0x80) != 0) { + if (pkt->getPathHashCount() == 0) { + onControlDataRecv(pkt); + } + return ACTION_RELEASE; + } + + // Handle direct zero-hop ACKs (path_len=0) + if (pkt->isRouteDirect() && pkt->getPathHashCount() == 0 && pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { + uint32_t ack_crc; + memcpy(&ack_crc, pkt->payload, 4); + onAckRecv(pkt, ack_crc); + return ACTION_RELEASE; + } + + if (pkt->isRouteDirect() && pkt->getPathHashCount() > 0) { + if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { + uint32_t ack_crc; + memcpy(&ack_crc, pkt->payload, 4); + onAckRecv(pkt, ack_crc); + } + if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) && allowPacketForward(pkt)) { + if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { + return forwardMultipartDirect(pkt); + } + if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { + if (!_tables->hasSeen(pkt)) { + removeSelfFromPath(pkt); + routeDirectRecvAcks(pkt, 0); + } + return ACTION_RELEASE; + } + if (!_tables->hasSeen(pkt)) { + removeSelfFromPath(pkt); + return ACTION_RETRANSMIT_DELAYED(0, getDirectRetransmitDelay(pkt)); + } + } + return ACTION_RELEASE; + } + + if (pkt->isRouteFlood() && filterRecvFloodPacket(pkt)) return ACTION_RELEASE; + + /* Record dupes for contention tracking + reactive backoff */ + if (pkt->isRouteFlood()) { + uint32_t h = ContentionTracker::computePacketHash32(pkt); +#ifdef CONFIG_ZEPHCORE_APC + uint8_t first_hop = (pkt->getPathHashCount() > 0) ? pkt->path[0] : 0; + _power_ctrl.recordEcho(h, pkt->_snr, first_hop, (uint32_t)_ms->getMillis()); +#endif + if (_contention.recordDupeIfTracked(h, (uint32_t)_ms->getMillis())) { + extendPendingRetransmit(h); + } else if (passivelyTrackFloods()) { + /* First hearing of a flood we won't forward — track it so the + * EMA reflects local contention (companion-side awareness). */ + _contention.trackRetransmit(h, (uint32_t)_ms->getMillis()); + } + } + + DispatcherAction action = ACTION_RELEASE; + + switch (pkt->getPayloadType()) { + case PAYLOAD_TYPE_ACK: { + uint32_t ack_crc; + memcpy(&ack_crc, pkt->payload, 4); + if (!_tables->hasSeen(pkt)) { + onAckRecv(pkt, ack_crc); + action = routeRecvPacket(pkt); + } + break; + } + case PAYLOAD_TYPE_PATH: + case PAYLOAD_TYPE_REQ: + case PAYLOAD_TYPE_RESPONSE: + case PAYLOAD_TYPE_TXT_MSG: { + int i = 0; + uint8_t dest_hash = pkt->payload[i++]; + uint8_t src_hash = pkt->payload[i++]; + + uint8_t *macAndData = &pkt->payload[i]; + if (i + CIPHER_MAC_SIZE >= (int)pkt->payload_len) { + LOG_WRN("onRecvPacket: incomplete packet (i=%d, payload_len=%d)", i, pkt->payload_len); + } else if (!_tables->hasSeen(pkt)) { + if (self_id.isHashMatch(&dest_hash)) { + int num = searchPeersByHash(&src_hash); + bool found = false; + for (int j = 0; j < num; j++) { + uint8_t secret[PUB_KEY_SIZE]; + getPeerSharedSecret(secret, j); + + uint8_t data[MAX_PACKET_PAYLOAD]; + int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i); + if (len > 0) { + if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) { + int k = 0; + uint8_t path_len = data[k++]; + uint8_t hash_size = (path_len >> 6) + 1; + uint8_t hash_count = path_len & 63; + uint8_t *path = &data[k]; k += hash_size*hash_count; + uint8_t extra_type = data[k++] & 0x0F; + uint8_t *extra = &data[k]; + uint8_t extra_len = len - k; + if (onPeerPathRecv(pkt, j, secret, path, path_len, extra_type, extra, extra_len)) { + if (pkt->isRouteFlood()) { + Packet *rpath = createPathReturn(&src_hash, secret, pkt->path, pkt->path_len, 0, nullptr, 0); + if (rpath) sendDirect(rpath, path, path_len, 500); + } + } + } else { + onPeerDataRecv(pkt, pkt->getPayloadType(), j, secret, data, len); + } + found = true; + break; + } + } + if (found) { + pkt->markDoNotRetransmit(); + } else { + LOG_WRN("onRecvPacket: no peer could decrypt message"); + } + } + action = routeRecvPacket(pkt); + } + break; + } + case PAYLOAD_TYPE_ANON_REQ: { + int i = 0; + uint8_t dest_hash = pkt->payload[i++]; + uint8_t *sender_pub_key = &pkt->payload[i]; i += PUB_KEY_SIZE; + + uint8_t *macAndData = &pkt->payload[i]; + if (i + 2 >= (int)pkt->payload_len) { + // incomplete packet + } else if (!_tables->hasSeen(pkt)) { + if (self_id.isHashMatch(&dest_hash)) { + Identity sender(sender_pub_key); + uint8_t secret[PUB_KEY_SIZE]; + self_id.calcSharedSecret(secret, sender); + + uint8_t data[MAX_PACKET_PAYLOAD]; + int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i); + if (len > 0) { + onAnonDataRecv(pkt, secret, sender, data, len); + pkt->markDoNotRetransmit(); + } + } + action = routeRecvPacket(pkt); + } + break; + } + case PAYLOAD_TYPE_GRP_DATA: + case PAYLOAD_TYPE_GRP_TXT: { + int i = 0; + uint8_t channel_hash = pkt->payload[i++]; + + uint8_t *macAndData = &pkt->payload[i]; + if (i + 2 >= (int)pkt->payload_len) { + // incomplete packet + } else if (!_tables->hasSeen(pkt)) { + GroupChannel channels[4]; + int num = searchChannelsByHash(&channel_hash, channels, 4); + for (int j = 0; j < num; j++) { + uint8_t data[MAX_PACKET_PAYLOAD]; + int len = Utils::MACThenDecrypt(channels[j].secret, data, macAndData, pkt->payload_len - i); + if (len > 0) { + onGroupDataRecv(pkt, pkt->getPayloadType(), channels[j], data, len); + break; + } + } + action = routeRecvPacket(pkt); + } + break; + } + case PAYLOAD_TYPE_ADVERT: { + int i = 0; + Identity id; + memcpy(id.pub_key, &pkt->payload[i], PUB_KEY_SIZE); + i += PUB_KEY_SIZE; + uint32_t timestamp; + memcpy(×tamp, &pkt->payload[i], 4); + i += 4; + const uint8_t *signature = &pkt->payload[i]; + i += SIGNATURE_SIZE; + if (i <= (int)pkt->payload_len && !self_id.matches(id.pub_key) && !_tables->hasSeen(pkt)) { + uint8_t *app_data = (uint8_t *)&pkt->payload[i]; + size_t app_data_len = pkt->payload_len - (size_t)i; + if (app_data_len > MAX_ADVERT_DATA_SIZE) app_data_len = MAX_ADVERT_DATA_SIZE; + uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE]; + int msg_len = 0; + memcpy(&message[msg_len], id.pub_key, PUB_KEY_SIZE); msg_len += PUB_KEY_SIZE; + memcpy(&message[msg_len], ×tamp, 4); msg_len += 4; + memcpy(&message[msg_len], app_data, app_data_len); msg_len += app_data_len; + if (id.verify(signature, message, msg_len)) { + onAdvertRecv(pkt, id, timestamp, app_data, app_data_len); + action = routeRecvPacket(pkt); + } + } + break; + } + case PAYLOAD_TYPE_RAW_CUSTOM: + if (pkt->isRouteDirect() && !_tables->hasSeen(pkt)) { + onRawDataRecv(pkt); + } + break; + case PAYLOAD_TYPE_MULTIPART: + if (pkt->payload_len > 2) { + /* uint8_t remaining = pkt->payload[0] >> 4; */ /* Reserved for future multipart support */ + uint8_t type = pkt->payload[0] & 0x0F; + + if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { + Packet tmp; + tmp.header = pkt->header; + tmp.path_len = Packet::copyPath(tmp.path, pkt->path, pkt->path_len); + tmp.payload_len = pkt->payload_len - 1; + memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); + + if (!_tables->hasSeen(&tmp)) { + uint32_t ack_crc; + memcpy(&ack_crc, tmp.payload, 4); + onAckRecv(&tmp, ack_crc); + } + } + } + break; + default: + break; + } + return action; +} + +Packet *Mesh::createAdvert(const LocalIdentity &id, const uint8_t *app_data, size_t app_data_len) +{ + if (app_data_len > MAX_ADVERT_DATA_SIZE) return nullptr; + + Packet *packet = obtainNewPacket(); + if (packet == nullptr) return nullptr; + + packet->header = (PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT); + int len = 0; + memcpy(&packet->payload[len], id.pub_key, PUB_KEY_SIZE); + len += PUB_KEY_SIZE; + uint32_t emitted_timestamp = _rtc->getCurrentTime(); + memcpy(&packet->payload[len], &emitted_timestamp, 4); + len += 4; + uint8_t *signature = &packet->payload[len]; + len += SIGNATURE_SIZE; + if (app_data && app_data_len > 0) { + memcpy(&packet->payload[len], app_data, app_data_len); + len += (int)app_data_len; + } + packet->payload_len = len; + + uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE]; + int msg_len = 0; + memcpy(&message[msg_len], id.pub_key, PUB_KEY_SIZE); msg_len += PUB_KEY_SIZE; + memcpy(&message[msg_len], &emitted_timestamp, 4); msg_len += 4; + if (app_data && app_data_len > 0) { + memcpy(&message[msg_len], app_data, app_data_len); msg_len += (int)app_data_len; + } + id.sign(signature, message, msg_len); + return packet; +} + +Packet *Mesh::createAck(uint32_t ack_crc) +{ + Packet *packet = obtainNewPacket(); + if (packet == nullptr) return nullptr; + packet->header = (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT); + memcpy(packet->payload, &ack_crc, 4); + packet->payload_len = 4; + return packet; +} + +Packet *Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) +{ + Packet *packet = obtainNewPacket(); + if (packet == nullptr) return nullptr; + packet->header = (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT); + packet->payload[0] = (remaining << 4) | PAYLOAD_TYPE_ACK; + memcpy(&packet->payload[1], &ack_crc, 4); + packet->payload_len = 5; + return packet; +} + +Packet *Mesh::createControlData(const uint8_t *data, size_t len) +{ + if (len > sizeof(Packet::payload)) return nullptr; + Packet *packet = obtainNewPacket(); + if (packet == nullptr) return nullptr; + packet->header = (PAYLOAD_TYPE_CONTROL << PH_TYPE_SHIFT); + memcpy(packet->payload, data, len); + packet->payload_len = (uint16_t)len; + return packet; +} + +void Mesh::sendFlood(Packet *packet, uint32_t delay_millis, uint8_t path_hash_size) +{ + if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { + releasePacket(packet); + return; + } + if (path_hash_size == 0 || path_hash_size > 3) { + LOG_WRN("sendFlood: invalid path_hash_size"); + releasePacket(packet); + return; + } + packet->header &= ~PH_ROUTE_MASK; + packet->header |= ROUTE_TYPE_FLOOD; + packet->setPathHashSizeAndCount(path_hash_size, 0); + _tables->hasSeen(packet); +#ifdef CONFIG_ZEPHCORE_APC + { + uint32_t h = ContentionTracker::computePacketHash32(packet); + _power_ctrl.trackTransmit(h, (uint32_t)_ms->getMillis()); + } +#endif + + uint8_t pri; + if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { + pri = 2; + } else if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) { + pri = 3; + } else { + pri = 1; + } + sendPacket(packet, pri, delay_millis + getInitialFloodJitter(packet)); +} + +void Mesh::sendFlood(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis, uint8_t path_hash_size) +{ + if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { + releasePacket(packet); + return; + } + if (path_hash_size == 0 || path_hash_size > 3) { + LOG_WRN("sendFlood: invalid path_hash_size"); + releasePacket(packet); + return; + } + packet->header &= ~PH_ROUTE_MASK; + packet->header |= ROUTE_TYPE_TRANSPORT_FLOOD; + packet->transport_codes[0] = transport_codes[0]; + packet->transport_codes[1] = transport_codes[1]; + packet->setPathHashSizeAndCount(path_hash_size, 0); + _tables->hasSeen(packet); +#ifdef CONFIG_ZEPHCORE_APC + { + uint32_t h = ContentionTracker::computePacketHash32(packet); + _power_ctrl.trackTransmit(h, (uint32_t)_ms->getMillis()); + } +#endif + + uint8_t pri; + if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { + pri = 2; + } else if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) { + pri = 3; + } else { + pri = 1; + } + sendPacket(packet, pri, delay_millis + getInitialFloodJitter(packet)); +} + +void Mesh::sendDirect(Packet *packet, const uint8_t *path, uint8_t path_len, uint32_t delay_millis) +{ + packet->header &= ~PH_ROUTE_MASK; + packet->header |= ROUTE_TYPE_DIRECT; + + uint8_t pri; + if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { + /* For TRACE packets, path is appended to end of PAYLOAD (used for SNRs) */ + memcpy(&packet->payload[packet->payload_len], path, path_len); + packet->payload_len += path_len; + packet->path_len = 0; + pri = 5; + } else { + packet->path_len = Packet::copyPath(packet->path, path, path_len); + if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { + pri = 1; + } else { + pri = 0; + } + } + + _tables->hasSeen(packet); + sendPacket(packet, pri, delay_millis); +} + +void Mesh::sendZeroHop(Packet *packet, uint32_t delay_millis) +{ + packet->header &= ~PH_ROUTE_MASK; + packet->header |= ROUTE_TYPE_DIRECT; + packet->path_len = 0; + _tables->hasSeen(packet); + sendPacket(packet, 0, delay_millis); +} + +void Mesh::sendZeroHop(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis) +{ + packet->header &= ~PH_ROUTE_MASK; + packet->header |= ROUTE_TYPE_TRANSPORT_DIRECT; + packet->transport_codes[0] = transport_codes[0]; + packet->transport_codes[1] = transport_codes[1]; + packet->path_len = 0; + _tables->hasSeen(packet); + sendPacket(packet, 0, delay_millis); +} + +#define MAX_COMBINED_PATH (MAX_PACKET_PAYLOAD - 2 - CIPHER_BLOCK_SIZE) + +Packet *Mesh::createPathReturn(const Identity &dest, const uint8_t *secret, const uint8_t *path, uint8_t path_len, + uint8_t extra_type, const uint8_t *extra, size_t extra_len) +{ + uint8_t dest_hash[PATH_HASH_SIZE]; + dest.copyHashTo(dest_hash); + return createPathReturn(dest_hash, secret, path, path_len, extra_type, extra, extra_len); +} + +Packet *Mesh::createPathReturn(const uint8_t *dest_hash, const uint8_t *secret, const uint8_t *path, uint8_t path_len, + uint8_t extra_type, const uint8_t *extra, size_t extra_len) +{ + uint8_t path_hash_size = (path_len >> 6) + 1; + uint8_t path_hash_count = path_len & 63; + + if (path_hash_count*path_hash_size + extra_len + 5 > MAX_COMBINED_PATH) return nullptr; + + Packet *packet = obtainNewPacket(); + if (packet == nullptr) return nullptr; + + packet->header = (PAYLOAD_TYPE_PATH << PH_TYPE_SHIFT); + + int len = 0; + memcpy(&packet->payload[len], dest_hash, PATH_HASH_SIZE); len += PATH_HASH_SIZE; + len += self_id.copyHashTo(&packet->payload[len]); + + { + int data_len = 0; + uint8_t data[MAX_PACKET_PAYLOAD]; + + data[data_len++] = path_len; + memcpy(&data[data_len], path, path_hash_count*path_hash_size); data_len += path_hash_count*path_hash_size; + if (extra_len > 0) { + data[data_len++] = extra_type; + memcpy(&data[data_len], extra, extra_len); data_len += extra_len; + } else { + data[data_len++] = 0xFF; // dummy payload type + _rng->random(&data[data_len], 4); data_len += 4; + } + + len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); + } + + packet->payload_len = len; + return packet; +} + +Packet *Mesh::createDatagram(uint8_t type, const Identity &dest, const uint8_t *secret, const uint8_t *data, size_t data_len) +{ + if (type == PAYLOAD_TYPE_TXT_MSG || type == PAYLOAD_TYPE_REQ || type == PAYLOAD_TYPE_RESPONSE) { + if (data_len + CIPHER_MAC_SIZE + CIPHER_BLOCK_SIZE - 1 > MAX_PACKET_PAYLOAD) { + LOG_WRN("createDatagram: data too large"); + return nullptr; + } + } else { + LOG_WRN("createDatagram: unsupported type %d", type); + return nullptr; + } + + Packet *packet = obtainNewPacket(); + if (packet == nullptr) { + LOG_ERR("createDatagram: packet alloc failed"); + return nullptr; + } + + packet->header = (type << PH_TYPE_SHIFT); + + int len = 0; + len += dest.copyHashTo(&packet->payload[len]); + len += self_id.copyHashTo(&packet->payload[len]); + len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); + + packet->payload_len = len; + return packet; +} + +Packet *Mesh::createAnonDatagram(uint8_t type, const LocalIdentity &sender, const Identity &dest, + const uint8_t *secret, const uint8_t *data, size_t data_len) +{ + if (type == PAYLOAD_TYPE_ANON_REQ) { + if (data_len + 1 + PUB_KEY_SIZE + CIPHER_BLOCK_SIZE - 1 > MAX_PACKET_PAYLOAD) return nullptr; + } else { + return nullptr; + } + + Packet *packet = obtainNewPacket(); + if (packet == nullptr) return nullptr; + + packet->header = (type << PH_TYPE_SHIFT); + + int len = 0; + if (type == PAYLOAD_TYPE_ANON_REQ) { + len += dest.copyHashTo(&packet->payload[len]); + memcpy(&packet->payload[len], sender.pub_key, PUB_KEY_SIZE); len += PUB_KEY_SIZE; + } + len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); + + packet->payload_len = len; + return packet; +} + +Packet *Mesh::createGroupDatagram(uint8_t type, const GroupChannel &channel, const uint8_t *data, size_t data_len) +{ + if (!(type == PAYLOAD_TYPE_GRP_TXT || type == PAYLOAD_TYPE_GRP_DATA)) return nullptr; + if (data_len + 1 + CIPHER_BLOCK_SIZE - 1 > MAX_PACKET_PAYLOAD) return nullptr; + + Packet *packet = obtainNewPacket(); + if (packet == nullptr) return nullptr; + + packet->header = (type << PH_TYPE_SHIFT); + + int len = 0; + memcpy(&packet->payload[len], channel.hash, PATH_HASH_SIZE); len += PATH_HASH_SIZE; + len += Utils::encryptThenMAC(channel.secret, &packet->payload[len], data, data_len); + + packet->payload_len = len; + return packet; +} + +Packet *Mesh::createRawData(const uint8_t *data, size_t len) +{ + if (len > sizeof(Packet::payload)) return nullptr; + + Packet *packet = obtainNewPacket(); + if (packet == nullptr) return nullptr; + + packet->header = (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT); + memcpy(packet->payload, data, len); + packet->payload_len = (uint16_t)len; + + return packet; +} + +Packet *Mesh::createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags) +{ + Packet *packet = obtainNewPacket(); + if (packet == nullptr) return nullptr; + + packet->header = (PAYLOAD_TYPE_TRACE << PH_TYPE_SHIFT); + memcpy(packet->payload, &tag, 4); + memcpy(&packet->payload[4], &auth_code, 4); + packet->payload[8] = flags; + packet->payload_len = 9; + + return packet; +} + +} /* namespace mesh */ diff --git a/zephcore/west.yml b/zephcore/west.yml index 879a6b8..2069f03 100644 --- a/zephcore/west.yml +++ b/zephcore/west.yml @@ -1,17 +1,17 @@ -manifest: - remotes: - - name: zephyrproject-rtos - url-base: https://github.com/zephyrproject-rtos - - projects: - - name: hal_espressif - remote: zephyrproject-rtos - revision: b7953b8019361d09e613f7011d2ccc41b984d087 - path: modules/hal/espressif - - name: zephyr - remote: zephyrproject-rtos - revision: 684c9e8f32e4373a21098559f748f06915f950c9 - import: true - - self: - path: zephcore +manifest: + remotes: + - name: zephyrproject-rtos + url-base: https://github.com/zephyrproject-rtos + + projects: + - name: hal_espressif + remote: zephyrproject-rtos + revision: b7953b8019361d09e613f7011d2ccc41b984d087 + path: modules/hal/espressif + - name: zephyr + remote: zephyrproject-rtos + revision: 684c9e8f32e4373a21098559f748f06915f950c9 + import: true + + self: + path: zephcore