diff --git a/zephcore/ARCHITECTURE.md b/zephcore/ARCHITECTURE.md new file mode 100644 index 0000000..fc0d4cd --- /dev/null +++ b/zephcore/ARCHITECTURE.md @@ -0,0 +1,653 @@ +# ZephCore Architecture Guide + +> Comprehensive developer reference for the ZephCore codebase — a Zephyr RTOS port of the Arduino MeshCore LoRa mesh networking firmware. + +--- + +## Table of Contents + +1. [Project Overview](#1-project-overview) +2. [Directory Structure](#2-directory-structure) +3. [Layer Architecture](#3-layer-architecture) +4. [Core Mesh Engine](#4-core-mesh-engine) +5. [Radio Subsystem](#5-radio-subsystem) +6. [Application Layer](#6-application-layer) +7. [Hardware Adapters](#7-hardware-adapters) +8. [UI Subsystem](#8-ui-subsystem) +9. [Build System](#9-build-system) +10. [Board Matrix](#10-board-matrix) +11. [Packet Format Reference](#11-packet-format-reference) +12. [BLE Protocol Reference](#12-ble-protocol-reference) +13. [Data Storage](#13-data-storage) +14. [Key Call Flows](#14-key-call-flows) + +--- + +## 1. Project Overview + +ZephCore is a LoRa mesh networking firmware running on Zephyr RTOS. It supports two device roles: + +- **Companion**: BLE-connected device paired with a phone app. Full contact/channel/message management. +- **Repeater**: Autonomous headless relay node. CLI administration via authenticated mesh connections or serial UART. + +Supported hardware: nRF52840, nRF54L15, ESP32-C3/C6/S3, EFR32MG24 — all with SX1262 or LR1110 LoRa radios. + +### Upstream Relationship + +ZephCore is a port of [Arduino MeshCore](https://github.com/rmendes76/MeshCore). The core mesh protocol (Mesh.cpp, Dispatcher.cpp, Packet.cpp, Identity.cpp, Utils.cpp) is shared code. Adapters (`adapters/`) bridge MeshCore's HAL interfaces to Zephyr APIs. Binary file formats (prefs, contacts, channels) are byte-compatible with Arduino MeshCore. + +--- + +## 2. Directory Structure + +``` +zephcore/ +├── src/ # Core mesh engine (shared with Arduino MeshCore) +│ ├── Mesh.cpp # Routing protocol: flood, direct, dedup, adverts +│ ├── Dispatcher.cpp # Packet queue, radio scheduling, CAD, duty cycle +│ ├── Packet.cpp # Packet serialization, hash, wire format +│ ├── Identity.cpp # Ed25519 key management, ECDH shared secrets +│ ├── Utils.cpp # AES-ECB encrypt, HMAC-SHA256, MAC +│ ├── StaticPoolPacketManager.cpp # Fixed-size packet pool (24 slots) +│ ├── main_companion.cpp # Companion mode entry point + event loop +│ └── main_repeater.cpp # Repeater mode entry point + event loop +│ +├── include/mesh/ # Core interfaces (shared with Arduino MeshCore) +│ ├── Mesh.h, Dispatcher.h, Packet.h, Identity.h, Utils.h +│ ├── MeshCore.h # Constants: key sizes, packet limits +│ ├── Radio.h # Abstract radio interface +│ ├── Board.h, Clock.h, RNG.h, RTC.h # HAL interfaces +│ ├── LoRaConfig.h # Default radio parameters +│ ├── RadioIncludes.h # Compile-time radio driver selection +│ ├── SimpleMeshTables.h # Hash-based packet deduplication +│ └── StaticPoolPacketManager.h # Fixed pool allocator +│ +├── adapters/ # Zephyr HAL implementations +│ ├── radio/ # LoRa radio drivers +│ │ ├── LoRaRadioBase.cpp/h # Shared TX/RX state machine, noise floor, AGC +│ │ ├── SX126xRadio.cpp/h # SX1262 adapter (native Zephyr driver) +│ │ ├── LR1110Radio.cpp/h # LR1110 adapter (patched Zephyr driver) +│ │ ├── radio_common.h # Shared radio types and constants +│ │ └── lr11xx/ # LR11xx low-level HAL (SPI, GPIO, Semtech SDK) +│ ├── ble/ZephyrBLE.cpp/h # BLE NUS service, pairing, TX congestion +│ ├── board/ZephyrBoard.cpp/h # Battery ADC, LEDs, reboot, bootloader +│ ├── clock/ # Millisecond uptime + software RTC +│ ├── datastore/ZephyrDataStore.cpp/h # LittleFS persistence +│ ├── gps/ZephyrGPSManager.cpp/h # GNSS state machine, power mgmt +│ ├── ota/wifi_ota.c/h # WiFi SoftAP + HTTP firmware upload +│ ├── rng/ZephyrRNG.cpp/h # Hardware CSPRNG with PRNG fallback +│ ├── sensors/ # I2C env sensors + power monitors +│ └── usb/ # USB CDC for companion + repeater +│ +├── app/ # Application layer +│ ├── CompanionMesh.cpp/h # Phone-connected companion logic +│ ├── RepeaterMesh.cpp/h # Autonomous repeater logic +│ └── RepeaterDataStore.cpp/h # Repeater-specific persistence paths +│ +├── helpers/ # Shared utilities +│ ├── BaseChatMesh.cpp/h # Contact/channel/message base class +│ ├── CommonCLI.cpp/h # Serial/mesh CLI command processor +│ ├── AdvertDataHelpers.cpp/h # Advertisement wire format encoder/decoder +│ ├── ClientACL.cpp/h # Authenticated client management +│ ├── TransportKeyStore.cpp/h # Region transport key cache +│ ├── RegionMap.cpp/h # Region-based flood filtering +│ ├── ContactInfo.h, ChannelDetails.h, NodePrefs.h # Data structures +│ ├── RateLimiter.h, IdentityStore.h, StatsFormatHelper.h +│ └── ui/ # Display, buzzer, input, pages, Doom game +│ +├── boards/ # Board definitions +│ ├── common/ # Shared configs, DTS includes, partition layouts +│ ├── nrf52840/ # RAK4631, WisMesh Tag, T1000-E, ThinkNode M1, etc. +│ ├── nrf54l/ # XIAO nRF54L15 +│ ├── esp32/ # LilyGo TLoRa C6, Station G2, XIAO ESP32-C3/C6 +│ └── mg24/ # XIAO MG24 +│ +├── patches/ # Zephyr tree modifications +│ ├── zephyr/ # Unified diffs (SX126x extensions, GNSS, blobs) +│ └── zephyr-new/ # New files (LR11xx Zephyr driver, DTS bindings) +│ +├── lib/ed25519/ # Vendored Ed25519 crypto library +├── tools/ # Formatter (flash erase) + LR1110 firmware updater +├── CMakeLists.txt # Build orchestration +├── Kconfig # All ZephCore configuration options +├── prj.conf # Base project config +└── west.yml # West manifest (Zephyr version pin) +``` + +--- + +## 3. Layer Architecture + +``` +┌─────────────────────────────────────────────────┐ +│ Phone App (BLE NUS) or Serial CLI (USB CDC) │ External +├─────────────────────────────────────────────────┤ +│ CompanionMesh / RepeaterMesh │ App Layer +│ ├── BaseChatMesh (contacts, channels, msgs) │ +│ ├── CommonCLI (command processor) │ +│ ├── ClientACL, RegionMap, TransportKeyStore │ +│ └── UI (display, buzzer, buttons) │ +├─────────────────────────────────────────────────┤ +│ mesh::Mesh │ Routing +│ ├── Flood routing (path hash accumulation) │ +│ ├── Direct routing (source-routed paths) │ +│ ├── Packet dedup (SimpleMeshTables) │ +│ └── Advert / ACK / Trace / Group dispatch │ +├─────────────────────────────────────────────────┤ +│ mesh::Dispatcher │ Scheduling +│ ├── TX/RX queue management │ +│ ├── CAD (channel activity detection) │ +│ ├── Duty cycle enforcement (EU ETSI) │ +│ ├── RX delay (score-based prioritization) │ +│ └── Maintenance (noise floor, AGC reset) │ +├─────────────────────────────────────────────────┤ +│ LoRaRadioBase │ Radio HAL +│ ├── SX126xRadio ──► Zephyr SX126x driver │ +│ └── LR1110Radio ──► Custom LR11xx driver │ +├─────────────────────────────────────────────────┤ +│ Zephyr RTOS (kernel, drivers, BLE, FS, USB) │ Platform +└─────────────────────────────────────────────────┘ +``` + +--- + +## 4. Core Mesh Engine + +### 4.1 Packet Lifecycle + +1. **Allocation**: `StaticPoolPacketManager::allocNew()` — fixed pool of 24 `Packet` objects (no heap) +2. **Creation**: `Mesh::createDatagram()`, `createAdvert()`, `createAck()`, etc. +3. **Queuing**: `Dispatcher::sendPacket()` → `PacketManager::queueOutbound()` with priority + scheduled time +4. **Transmission**: `Dispatcher::checkSend()` → CAD check → serialize → `radio->startSendRaw()` +5. **Release**: `PacketManager::free()` after TX complete or processing done + +### 4.2 Packet Structure + +``` +Wire format: + [header: 1B] [transport_codes: 0 or 4B] [path_len: 1B] [path: variable] [payload: variable] + +Header byte: + Bits 0-1: Route type (0=transport_flood, 1=flood, 2=direct, 3=transport_direct) + Bits 2-5: Payload type (0=REQ .. 15=RAW_CUSTOM) + Bits 6-7: Version (0=v1) + +Path_len byte: + Bits 0-5: Hash count (0-63 hops) + Bits 6-7: Hash size mode (0=1B, 1=2B, 2=3B, 3=reserved) +``` + +### 4.3 Payload Types + +| Type | Value | Description | +|------|-------|-------------| +| REQ | 0x00 | Encrypted request to peer | +| RESPONSE | 0x01 | Encrypted response from peer | +| TXT_MSG | 0x02 | Encrypted text message | +| ACK | 0x03 | 4-byte CRC acknowledgment | +| ADVERT | 0x04 | Signed identity advertisement | +| GRP_TXT | 0x05 | Group channel text message | +| GRP_DATA | 0x06 | Group channel data | +| ANON_REQ | 0x07 | Anonymous request (includes full pubkey) | +| PATH | 0x08 | Path return (source route exchange) | +| TRACE | 0x09 | Trace route | +| MULTIPART | 0x0A | Multi-ACK container | +| CONTROL | 0x0B | Control data (zero-hop) | +| RAW_CUSTOM | 0x0F | Raw custom data | + +### 4.4 Routing + +**Flood routing**: Packet has no destination path. Each relay node appends its identity hash to `path[]` and retransmits. Priority decreases with hop count. `allowPacketForward()` is the gatekeeper. + +**Direct routing**: Packet carries a source-routed `path[]`. Each relay node checks if the first path hash matches its own identity, removes itself, and forwards. Path is built from previous flood packets' accumulated hashes. + +**Deduplication**: `SimpleMeshTables` maintains a circular buffer of 128 packet hashes (8 bytes each, SHA-256 truncated) and 64 ACK CRCs. `hasSeen()` prevents duplicate processing and retransmission. + +### 4.5 Dispatcher Scheduling + +The Dispatcher runs a tight loop: + +``` +loop(): + 1. Check if current TX is complete → release packet, record airtime + 2. Process next inbound packet from queue (if scheduled time has passed) + 3. checkRecv(): Drain radio RX ring buffer + - Parse raw bytes into Packet + - Flood packets: compute RX delay based on score → defer or process immediately + - Direct packets: process immediately + 4. checkSend(): Check outbound queue + - CAD: if channel busy, retry with random backoff (120-480ms) + - Duty cycle: if exceeded, defer 5 seconds (admin packets exempt) + - Final LBT check right before TX (closes timing gap) + - Serialize and transmit +``` + +**RX Delay**: Flood packets are delayed based on signal quality. High-quality signals (high SNR, short packets) get shorter delays, allowing closer/better relays to retransmit first. Uses a lookup table approximation of `10^(0.85 - score*0.1) - 1` multiplied by airtime. + +**Duty Cycle**: Fixed 1-hour sliding window. Default 10%. Admin packets (REQ, RESPONSE, ANON_REQ, CONTROL) are exempt. + +### 4.6 Maintenance Loop + +Called every ~5 seconds from the main event loop: + +1. **Noise floor calibration**: EMA with alpha=1/8, jitter, threshold filtering, warmup +2. **RX mode watchdog**: Flags error if radio stuck outside RX for >8 seconds +3. **AGC reset**: Periodic warm sleep + recalibration (configurable interval, default off) + +### 4.7 Encryption + +- **Peer-to-peer**: ECDH shared secret (Curve25519) → AES-128-ECB encrypt → 2-byte HMAC-SHA256 MAC +- **Group channels**: SHA-256 of channel name → AES key +- **Advertisements**: Ed25519 signature over (pubkey + timestamp + app_data) +- **ACKs**: SHA-256(shared_secret + packet_hash) truncated to 4 bytes + +--- + +## 5. Radio Subsystem + +### 5.1 Class Hierarchy + +``` +mesh::Radio (abstract interface) + └── LoRaRadioBase (shared state machine, ring buffer, noise floor) + ├── SX126xRadio → Zephyr native SX126x driver + sx126x_ext.h + └── LR1110Radio → Custom lr11xx_lora.c driver + Semtech HAL +``` + +Compile-time selection via `CONFIG_ZEPHCORE_RADIO_LR1110` in `RadioIncludes.h`. + +### 5.2 LoRaRadioBase State Machine + +**TX Flow**: +1. `startSendRaw()` → cancel RX → configure TX → copy to buffer → async send → wake TX wait thread +2. TX wait thread blocks on semaphore, polls completion signal (5s timeout) +3. On DIO1 TX_DONE interrupt → signal raised → restart RX → update stats + +**RX Flow**: +1. `lora_recv_async()` with callback +2. ISR writes to 8-slot SPSC ring buffer (drops NEW packet on overflow) +3. Main thread drains via `recvRaw()` + +**Config Caching**: Avoids redundant `lora_config()` calls. Fast-path for TX↔RX transitions when only direction differs. + +### 5.3 Noise Floor EMA + +Algorithm in `triggerNoiseFloorCalibrate()`: +- Random 0-500ms jitter to break phase-lock with interference +- 4 RSSI samples per tick, take minimum +- Threshold filter: reject samples ≥ floor + 14dB (after 8-tick warmup) +- Periodic bypass: every 8th tick accepts unconditionally +- EMA: `floor += round_nearest((sample - floor) / 8)`, clamped to [-120, -50] dBm + +### 5.4 LR1110 Driver Errata Workarounds + +The custom `lr11xx_lora.c` driver handles several LR1110 firmware bugs: +- **CMD_ERROR IRQ**: Benign error flag on several write commands — cleared silently +- **RX buffer drift**: Buffer base shifts 4 bytes per packet → `clear_rxbuffer()` after every RX +- **Header error**: Can shift buffer pointer → standby before RX restart +- **DIO1 stuck HIGH**: 5-cycle detection → full hardware reset + recovery +- **RX duty cycle broken**: 23-40% packet loss → disabled, always continuous RX + +### 5.5 Default Radio Parameters + +| Parameter | Default | Notes | +|-----------|---------|-------| +| Frequency | 869.618 MHz | EU 869.4-869.65 MHz band (500mW ERP allowed) | +| Bandwidth | 62 kHz | | +| Spreading Factor | 8 | | +| Coding Rate | 4/8 | | +| Preamble | 16 symbols | | +| TX Power | 22 dBm | Clamped by `CONFIG_ZEPHCORE_MAX_TX_POWER_DBM` | + +--- + +## 6. Application Layer + +### 6.1 Class Hierarchy + +``` +mesh::Mesh +├── BaseChatMesh (contacts, channels, messages, connections) +│ └── CompanionMesh (BLE protocol, phone sync, offline queue, ACK tracking) +└── RepeaterMesh (ClientACL, RegionMap, CLI, rate limiting, neighbor tracking) +``` + +### 6.2 CompanionMesh + +Handles the binary BLE protocol with ~60 command opcodes. Key features: +- **Offline queue**: 16-frame circular buffer with peek/confirm pattern (survives BLE drops) +- **ACK tracking**: 8-slot table, computes expected ACK = SHA256(secret + hash)[0:4] +- **Contact iteration**: Streaming protocol with `lastmod` filtering for incremental sync +- **Lazy write batching**: Dirty contacts/channels flush after 5-second delay +- **Protocol versioning**: V2/V3 frame format negotiation with phone app +- **Ed25519 signing**: 3-phase flow (start→data→finish) for signing up to 8KB +- **Flood scope**: Transport key filtering for region-scoped sends + +### 6.3 RepeaterMesh + +Autonomous operation features: +- **Authentication**: Password-based login with timestamp replay protection (120s window) +- **Permission levels**: GUEST(0), READ_ONLY(1), READ_WRITE(2), ADMIN(3) +- **Region filtering**: `RegionMap` with transport key matching per flood packet +- **Rate limiting**: 4 requests per 120s (discovery), 4 per 180s (anonymous) +- **Neighbor tracking**: 16-slot table with RSSI/SNR/name/timestamp +- **Temporary radio params**: `tempradio` command with auto-revert timer + +### 6.4 CommonCLI Commands + +System: `ver`, `board`, `reboot`, `start dfu`, `start ota`, `erase` +Config: `set name/freq/radio/tx/flood.max/password/...`, corresponding getters +GPS: `gps on/off/setloc/advert` +Sensors: `sensor get/set/list` +Stats: `stats-core/stats-radio/stats-packets`, `clear stats` +Power: `powersaving on/off` + +--- + +## 7. Hardware Adapters + +### 7.1 BLE (`adapters/ble/`) + +- Nordic UART Service (NUS) with AUTHEN permissions (forces pairing) +- Passkey-based MITM pairing, runtime configurable PIN +- TX congestion control: queue (12 frames) + overflow buffer + retry + timeout watchdog +- Fast/slow advertising switching with post-disconnect flap prevention +- DLE (Data Length Extension) to 251 bytes +- Interface coexistence: BLE vs USB, one active at a time + +### 7.2 DataStore (`adapters/datastore/`) + +- **Internal**: LittleFS on flash (`/lfs`) +- **External**: Optional LittleFS on QSPI (`/ext`) with auto-migration +- **Prefs**: 93-byte binary format, Arduino-compatible, field-by-field I/O +- **Contacts**: 152-byte records, stored on external flash if available +- **Channels**: 68-byte records (4 pad + 32 name + 32 secret) +- **Blobs**: Fixed-size records with LRU eviction by timestamp + +### 7.3 GPS (`adapters/gps/`) + +- State machine: OFF → ACQUIRING → STANDBY (with warm standby on supported hardware) +- 3 consecutive good fixes (≥4 satellites) required before reporting +- Multi-constellation: GPS+GLONASS+Galileo+BeiDou with fallback +- T1000-E: Complex 6-GPIO power sequencing with VRTC preservation +- Repeater mode: 48-hour wake interval for time sync only +- GPS time blocks phone time sync for 2 hours after last fix + +### 7.4 USB (`adapters/usb/`) + +- **CompanionUSB**: V3-framed CDC (little-endian 16-bit length prefix + payload) +- **RepeaterUSB**: Minimal CDC with 1200-baud DFU touch detection +- Both share message queues with BLE adapter (transport-agnostic mesh layer) + +### 7.5 Board (`adapters/board/`) + +- Battery ADC with optional regulator-gated voltage divider, 8-sample average +- UF2 bootloader entry via GPREGRET magic (0x57 = UF2, 0xA8 = BLE DFU) +- TX LED bracketing for LoRa transmissions +- Bootloader version detection via flash memory scan + +--- + +## 8. UI Subsystem + +### 8.1 Architecture + +Event-driven, no dedicated thread. All UI work on Zephyr work queues. + +``` +Hardware buttons → Zephyr input subsystem → Longpress filter → Multi-tap filter + → ui_input_cb() → page navigation / action dispatch → schedule_render() + → render_work (50ms OLED / 200ms EPD debounce) → CFB framebuffer → display +``` + +### 8.2 Pages + +**Companion** (11 pages): Messages, Recent, Radio, Bluetooth, Advert, GPS, Buzzer, Sensors, Offgrid, DFU, Shutdown + +**Repeater** (3 pages): Status, Radio, Shutdown + +### 8.3 Multi-Tap Input + +Single button, up to 4 taps within 400ms window: +- 1 tap → Page next +- 2 taps → Flood advert +- 3 taps → Buzzer toggle +- 4 taps → GPS toggle (immediate, no delay) + +### 8.4 Buzzer + +Non-blocking RTTTL parser on dedicated work queue. Predefined melodies for startup, shutdown, messages, ACKs. 2-second safety watchdog auto-silences on work queue stall. + +### 8.5 Doom Easter Egg + +Wolf3D-style raycaster on OLED: textured walls, 2 enemy types, shooting, HUD. Bypasses CFB, writes directly to display. ~1.7KB RAM, ~5KB flash. Enabled via `CONFIG_ZEPHCORE_EASTER_EGG_DOOM`. Triple-press ENTER on Messages page to activate. + +--- + +## 9. Build System + +### 9.1 Config Layering + +``` +prj.conf (base: console, logging) + → boards/common/zephcore_common.conf (ALL boards: BLE, crypto, FS, LoRa, sensors) + → boards/common/_common.conf (nrf52/esp32/nrf54l/mg24 specifics) + → boards///board.conf (board-specific pins, features) + → [optional] repeater.conf, prod.conf (user extras, LAST = highest priority) +``` + +### 9.2 Key Kconfig Choices + +- **Role**: `ZEPHCORE_ROLE_COMPANION` (default) vs `ZEPHCORE_ROLE_REPEATER` +- **Radio**: `ZEPHCORE_RADIO_NATIVE` (SX126x, default) vs `ZEPHCORE_RADIO_LR1110` +- **Features**: Display, buzzer, buttons, multi-tap, Doom (auto-enabled from DT) + +### 9.3 Patches + +| Patch | Risk | Purpose | +|-------|------|---------| +| 0001-lora-lr11xx-build | LOW | Integrates LR11xx driver into Zephyr LoRa build | +| 0003-lora-sx126x-native | **HIGH** | ~400 lines: DIO1 work queue, errata workarounds, extension API | +| 0005-gnss-air530z-easy | MEDIUM | EASY ephemeris + removes PM (prevents deadlocks) | +| 0006-blobs-py | LOW | Fix `west blobs fetch` KeyError | + +### 9.4 Flash Partition Layouts + +**nRF52840 SD v6**: SoftDevice 152KB → App 696KB → LFS 128KB → UF2 48KB +**nRF52840 SD v7**: SoftDevice 156KB → App 692KB → LFS 128KB → UF2 48KB +**ESP32 (4MB)**: Boot + App → NVS 8KB → LFS 184KB +**nRF54L15**: MCUboot 64KB → App 1272KB → NVS 8KB → LFS 84KB + +--- + +## 10. Board Matrix + +| Board | SoC | Radio | GPS | Display | Buzzer | Buttons | QSPI | Max Contacts | +|-------|-----|-------|-----|---------|--------|---------|------|-------------| +| RAK4631 | nRF52840 | SX1262 | gnss-nmea | - | - | - | - | 350 | +| WisMesh Tag | nRF52840 | SX1262 | Air530Z | - | Yes | 1+multitap | - | 350 | +| T1000-E | nRF52840 | **LR1110** | AG3335 | - | Yes | 1+multitap | - | 350 | +| ThinkNode M1 | nRF52840 | SX1262 | Air530Z | EPD 200x200 | Yes | 2+multitap | 2MB | 510 | +| Wio Tracker L1 | nRF52840 | SX1262 | L76K | OLED 128x64 | Yes | 5-way joy | 2MB | 510 | +| Ikoka Nano 30dBm | nRF52840 | SX1262+PA | - | - | - | - | - | 350 | +| XIAO nRF54L15 | nRF54L15 | SX1262 | - | - | - | - | - | 450 | +| XIAO ESP32-C3 | ESP32-C3 | SX1262 | - | - | - | - | - | 300 | +| XIAO ESP32-C6 | ESP32-C6 | SX1262 | - | - | - | - | - | 300 | +| LilyGo TLoRa C6 | ESP32-C6 | SX1262 | - | - | - | - | - | 300 | +| Station G2 | ESP32-S3 | SX1262+PA | UART1 | OLED 128x64 | - | 1 button | - | 350 | +| XIAO MG24 | EFR32MG24 | SX1262 | - | - | - | - | - | 350 | + +--- + +## 11. Packet Format Reference + +### Wire Format + +``` +Byte 0: Header + [1:0] Route type: 0=transport_flood, 1=flood, 2=direct, 3=transport_direct + [5:2] Payload type (see table in §4.3) + [7:6] Version (0=v1) + +If transport route (bit 0 or both bits set): + Bytes 1-4: transport_codes[2] (2x uint16_t LE) + +Next byte: path_len + [5:0] Hash count (number of hops) + [7:6] Hash size mode (0→1B, 1→2B, 2→3B) + +Next N bytes: path[] (hash_count × hash_size bytes) + +Remaining bytes: payload (type-specific) +``` + +### Advert Payload + +``` +[32B pubkey] [4B timestamp LE] [64B Ed25519 signature] [0-32B app_data] + +app_data format (AdvertDataHelpers): + Byte 0: type(3:0) | flags(7:4) + flags: bit4=lat/lon, bit5=feat1, bit6=feat2, bit7=name + [optional 8B: lat(float) + lon(float)] + [optional 2B: features1] + [optional 2B: features2] + [remaining: name string] +``` + +### Encrypted Datagram (REQ/RESPONSE/TXT_MSG) + +``` +[1B dest_hash] [1B src_hash] [encrypted_payload + 2B MAC] + +encrypted_payload (after AES-128-ECB decrypt): + For TXT_MSG: [4B timestamp] [1B txt_type] [text...] + txt_type: 0=plain, 1=cli_data, 2=signed_plain +``` + +--- + +## 12. BLE Protocol Reference + +### Frame Format + +Raw binary over BLE NUS. Each frame: `[1B opcode] [payload...]` +Over USB CDC: V3 framing: `[2B LE length] [1B opcode] [payload...]` + +### Key Command Opcodes (phone → device) + +| Opcode | Name | Payload | +|--------|------|---------| +| 0x01 | CMD_SEND_TXT_MSG | contact_idx + text | +| 0x03 | CMD_GET_CONTACTS | [optional 4B lastmod filter] | +| 0x06 | CMD_GET_SELF_INFO | (none) | +| 0x07 | CMD_SET_SELF_INFO | type + name + lat + lon | +| 0x0B | CMD_GET_MSG_WAITING | (none) | +| 0x0C | CMD_CONFIRM_MSG | (none) | +| 0x11 | CMD_SET_PREF | pref_key + value | +| 0x12 | CMD_DEVICE_QUERY | (none) | +| 0x15 | CMD_SEND_SELF_ADVERT | (none) | +| 0x20 | CMD_NEGOTIATE_VER | target_version | + +### Push Notifications (device → phone, async) + +| Code | Name | +|------|------| +| 0x80 | PUSH_CODE_ADVERT | +| 0x82 | PUSH_CODE_SEND_CONFIRMED | +| 0x83 | PUSH_CODE_MSG_WAITING | +| 0x8A | PUSH_CODE_NEW_ADVERT | + +--- + +## 13. Data Storage + +### File Paths + +| Path | Content | Format | +|------|---------|--------| +| `/lfs/_main.id` | Node identity | 64B private key + 32B public key | +| `/lfs/new_prefs` | Preferences | 93B binary (Arduino-compatible) | +| `/lfs/contacts3` or `/ext/contacts3` | Contacts | 152B × N records | +| `/lfs/channels2` or `/ext/channels2` | Channels | 68B × N records | +| `/lfs/adv_blobs` or `/ext/adv_blobs` | Advert cache | Fixed-size blob records | +| `/lfs/repeater/acl` | Client ACL | 136B × N records | +| `/lfs/repeater/regions2` | Region map | Header + 164B × N entries | + +### Preferences Binary Layout (93 bytes) + +``` +Offset Size Field +0 4 airtime_factor (float) +4 32 node_name +36 4 (padding) +40 8 node_lat (double) +48 8 node_lon (double) +56 4 freq (float) +60 1 sf +61 1 cr +62 1 client_repeat +63 1 manual_add_contacts +64 4 bw (float) +... ... (see CommonCLI.cpp loadPrefs/savePrefs for full layout) +92 1 rx_boost (ZephCore extension) +``` + +--- + +## 14. Key Call Flows + +### 14.1 Receiving a LoRa Packet → Application + +``` +DIO1 interrupt → Zephyr lora driver → async RX callback + → LoRaRadioBase::rxCallbackStatic() → SPSC ring buffer write → _rx_cb() + → k_event_post(MESH_EVENT_LORA_RX) → main thread wakes + → Dispatcher::loop() → checkRecv() → drain ring buffer + → tryParsePacket() → score + airtime calc + → flood: calcRxDelay() → queue inbound or process immediately + → direct: process immediately + → Mesh::onRecvPacket() → decrypt → dispatch by type + → BaseChatMesh::onPeerDataRecv() → onMessageRecv() + → CompanionMesh: writeFrame() to phone or queueOfflineMessage() +``` + +### 14.2 Sending a Text Message + +``` +Phone sends CMD_SEND_TXT_MSG via BLE NUS + → CompanionMesh::handleProtocolFrame() + → BaseChatMesh::sendMessage(contact, text) + → composeMsgPacket(): ECDH secret → AES encrypt → MAC + → if contact has path: trySendDirect() + → else: sendFlood() + → Mesh::sendFlood() → mark seen → queue outbound + → Dispatcher::checkSend() → CAD check → duty cycle check → LBT → startSendRaw() +``` + +### 14.3 Repeater Forwarding a Packet + +``` +Dispatcher::checkRecv() → Mesh::onRecvPacket() + → flood packet, not for us + → routeRecvPacket() → allowPacketForward() + → RepeaterMesh checks: disable_fwd? flood_max? region filter? + → if allowed: append self hash to path, ACTION_RETRANSMIT_DELAYED + → re-queued outbound with priority = hop count +``` + +### 14.4 Noise Floor Calibration Cycle + +``` +main event loop (every 5s) → Dispatcher::maintenanceLoop() + → radio->triggerNoiseFloorCalibrate(threshold) + → guards: in RX? TX active? duty cycle? mid-receive? + → random 0-500ms jitter + → read 4 RSSI samples, take min + → first sample: seed directly + → warmup (<8 ticks): accept unconditionally + → periodic bypass (every 8th): accept unconditionally + → otherwise: reject if sample ≥ floor + 14dB + → EMA: floor += round((sample - floor) / 8) + → clamp [-120, -50] dBm +``` diff --git a/zephcore/adapters/usb/ZephyrCompanionUSB.cpp b/zephcore/adapters/usb/ZephyrCompanionUSB.cpp index 7111b6b..2668ad7 100644 --- a/zephcore/adapters/usb/ZephyrCompanionUSB.cpp +++ b/zephcore/adapters/usb/ZephyrCompanionUSB.cpp @@ -25,6 +25,7 @@ LOG_MODULE_REGISTER(zephcore_usb, CONFIG_ZEPHCORE_USB_LOG_LEVEL); #define USB_DTR_CHECK_MS 10000 /* USB DTR poll interval - 10s for power savings */ #define USB_RING_BUF_SIZE 512 /* USB RX ring buffer size */ +#define USB_FRAME_TIMEOUT_MS 2000 /* Partial frame timeout - reset parser after 2s of no completion */ /* USB CDC state */ static const struct device *usb_dev; @@ -33,6 +34,7 @@ static struct ring_buf usb_ring_buf; static uint8_t usb_rx_buf[MAX_FRAME_SIZE + 2]; /* +2 for length prefix */ static uint16_t usb_rx_idx; static uint16_t usb_frame_len; /* Expected frame length (0 = waiting for header) */ +static uint32_t usb_frame_start_time; /* Timestamp of first byte in current frame */ static bool usb_dtr_active; /* Pointers to mesh event infrastructure (set by init) */ @@ -83,12 +85,22 @@ static void usb_rx_work_fn(struct k_work *work) uint8_t byte; + /* Timeout partial frames — if we've been accumulating bytes for too long + * without completing a frame, reset the parser state */ + if (usb_rx_idx > 0 && (k_uptime_get_32() - usb_frame_start_time) > USB_FRAME_TIMEOUT_MS) { + LOG_WRN("usb_rx: partial frame timeout (idx=%u, expected=%u), resync", + usb_rx_idx, usb_frame_len); + usb_frame_len = 0; + usb_rx_idx = 0; + } + while (ring_buf_get(&usb_ring_buf, &byte, 1) == 1) { /* V3 framing: [len_lo][len_hi][payload...] */ if (usb_rx_idx == 0) { /* First byte of length */ usb_rx_buf[0] = byte; usb_rx_idx = 1; + usb_frame_start_time = k_uptime_get_32(); } else if (usb_rx_idx == 1) { /* Second byte of length */ usb_rx_buf[1] = byte; diff --git a/zephcore/app/CompanionMesh.h b/zephcore/app/CompanionMesh.h index 432dc64..706e93c 100644 --- a/zephcore/app/CompanionMesh.h +++ b/zephcore/app/CompanionMesh.h @@ -178,6 +178,20 @@ public: */ void cancelSyncPending() { _sync_pending = false; } + /** + * Free the Ed25519 signing buffer if allocated. + * Call on BLE disconnect to prevent 8KB leak when disconnect + * occurs between CMD_SIGN_START and CMD_SIGN_FINISH. + */ + void cleanupSignState() { + if (_sign_data) { + delete[] _sign_data; + _sign_data = nullptr; + } + _sign_data_len = 0; + _sign_data_capacity = 0; + } + /** * Get BLE device name for advertising. */ diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index bc74a9f..fa6c107 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -46,6 +46,11 @@ static T constrain(T value, T min_val, T max_val) { return value; } +/* Read exactly 'len' bytes; short/error read stops the chain via ok flag */ +static inline bool prefs_read(struct fs_file_t *f, void *dest, size_t len) { + return fs_read(f, dest, len) == (ssize_t)len; +} + void CommonCLI::loadPrefs(const char* path) { struct fs_file_t file; fs_file_t_init(&file); @@ -56,50 +61,58 @@ void CommonCLI::loadPrefs(const char* path) { } uint8_t pad[8]; + bool ok = true; - fs_read(&file, &_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0 - fs_read(&file, &_prefs->node_name, sizeof(_prefs->node_name)); // 4 - fs_read(&file, pad, 4); // 36 - fs_read(&file, &_prefs->node_lat, sizeof(_prefs->node_lat)); // 40 - fs_read(&file, &_prefs->node_lon, sizeof(_prefs->node_lon)); // 48 - fs_read(&file, &_prefs->password[0], sizeof(_prefs->password)); // 56 - fs_read(&file, &_prefs->freq, sizeof(_prefs->freq)); // 72 - fs_read(&file, &_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76 - fs_read(&file, &_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77 - fs_read(&file, &_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78 - fs_read(&file, pad, 1); // 79 - fs_read(&file, &_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80 - fs_read(&file, &_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84 - fs_read(&file, &_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88 - fs_read(&file, &_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104 - fs_read(&file, pad, 4); // 108 - fs_read(&file, &_prefs->sf, sizeof(_prefs->sf)); // 112 - fs_read(&file, &_prefs->cr, sizeof(_prefs->cr)); // 113 - fs_read(&file, &_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 - fs_read(&file, &_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115 - fs_read(&file, &_prefs->bw, sizeof(_prefs->bw)); // 116 - fs_read(&file, &_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120 - fs_read(&file, &_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121 - fs_read(&file, pad, 2); // 122 - fs_read(&file, &_prefs->flood_max, sizeof(_prefs->flood_max)); // 124 - fs_read(&file, &_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125 - fs_read(&file, &_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126 - fs_read(&file, pad, 1); // skip bridge_enabled // 127 - fs_read(&file, pad, 2); // skip bridge_delay // 128 - fs_read(&file, pad, 1); // skip bridge_pkt_src // 130 - fs_read(&file, pad, 4); // skip bridge_baud // 131 - fs_read(&file, pad, 1); // skip bridge_channel // 135 - fs_read(&file, pad, 16); // skip bridge_secret // 136 - fs_read(&file, &_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152 - fs_read(&file, pad, 3); // 153 - fs_read(&file, &_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156 - fs_read(&file, &_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 - fs_read(&file, &_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161 - fs_read(&file, &_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 - fs_read(&file, &_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 - fs_read(&file, _prefs->owner_info, sizeof(_prefs->owner_info)); // 170 - fs_read(&file, &_prefs->rx_boost, sizeof(_prefs->rx_boost)); // 290 - fs_read(&file, &_prefs->rx_duty_cycle, sizeof(_prefs->rx_duty_cycle)); // 291 + /* Read fields in Arduino-compatible binary order. + * On truncated file, short-circuit at first failure + * so remaining fields keep their default values. */ + ok = ok && prefs_read(&file, &_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0 + ok = ok && prefs_read(&file, &_prefs->node_name, sizeof(_prefs->node_name)); // 4 + ok = ok && prefs_read(&file, pad, 4); // 36 + ok = ok && prefs_read(&file, &_prefs->node_lat, sizeof(_prefs->node_lat)); // 40 + ok = ok && prefs_read(&file, &_prefs->node_lon, sizeof(_prefs->node_lon)); // 48 + ok = ok && prefs_read(&file, &_prefs->password[0], sizeof(_prefs->password)); // 56 + ok = ok && prefs_read(&file, &_prefs->freq, sizeof(_prefs->freq)); // 72 + ok = ok && prefs_read(&file, &_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76 + ok = ok && prefs_read(&file, &_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77 + ok = ok && prefs_read(&file, &_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78 + ok = ok && prefs_read(&file, pad, 1); // 79 + ok = ok && prefs_read(&file, &_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80 + ok = ok && prefs_read(&file, &_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84 + ok = ok && prefs_read(&file, &_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88 + ok = ok && prefs_read(&file, &_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104 + ok = ok && prefs_read(&file, pad, 4); // 108 + ok = ok && prefs_read(&file, &_prefs->sf, sizeof(_prefs->sf)); // 112 + ok = ok && prefs_read(&file, &_prefs->cr, sizeof(_prefs->cr)); // 113 + ok = ok && prefs_read(&file, &_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 + ok = ok && prefs_read(&file, &_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115 + ok = ok && prefs_read(&file, &_prefs->bw, sizeof(_prefs->bw)); // 116 + ok = ok && prefs_read(&file, &_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120 + ok = ok && prefs_read(&file, &_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121 + ok = ok && prefs_read(&file, pad, 2); // 122 + ok = ok && prefs_read(&file, &_prefs->flood_max, sizeof(_prefs->flood_max)); // 124 + ok = ok && prefs_read(&file, &_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125 + ok = ok && prefs_read(&file, &_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126 + ok = ok && prefs_read(&file, pad, 1); // skip bridge_enabled // 127 + ok = ok && prefs_read(&file, pad, 2); // skip bridge_delay // 128 + ok = ok && prefs_read(&file, pad, 1); // skip bridge_pkt_src // 130 + ok = ok && prefs_read(&file, pad, 4); // skip bridge_baud // 131 + ok = ok && prefs_read(&file, pad, 1); // skip bridge_channel // 135 + ok = ok && prefs_read(&file, pad, 16); // skip bridge_secret // 136 + ok = ok && prefs_read(&file, &_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152 + ok = ok && prefs_read(&file, pad, 3); // 153 + ok = ok && prefs_read(&file, &_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156 + ok = ok && prefs_read(&file, &_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 + ok = ok && prefs_read(&file, &_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161 + ok = ok && prefs_read(&file, &_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 + ok = ok && prefs_read(&file, &_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 + ok = ok && prefs_read(&file, _prefs->owner_info, sizeof(_prefs->owner_info)); // 170 + ok = ok && prefs_read(&file, &_prefs->rx_boost, sizeof(_prefs->rx_boost)); // 290 + ok = ok && prefs_read(&file, &_prefs->rx_duty_cycle, sizeof(_prefs->rx_duty_cycle)); // 291 + + if (!ok) { + LOG_WRN("Prefs file %s truncated, some fields use defaults", path); + } fs_close(&file); diff --git a/zephcore/helpers/RegionMap.cpp b/zephcore/helpers/RegionMap.cpp index b6b130c..07889d0 100644 --- a/zephcore/helpers/RegionMap.cpp +++ b/zephcore/helpers/RegionMap.cpp @@ -150,9 +150,10 @@ RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) { _store->getAutoKeyFor(region->id, region->name, keys[0]); num = 1; } else { // new: implicit auto hashtag region - char tmp[sizeof(region->name)]; + char tmp[sizeof(region->name) + 1]; tmp[0] = '#'; - strcpy(&tmp[1], region->name); + memcpy(&tmp[1], region->name, sizeof(region->name) - 1); + tmp[sizeof(region->name)] = '\0'; _store->getAutoKeyFor(region->id, tmp, keys[0]); num = 1; } diff --git a/zephcore/helpers/ui/ui_mesh_actions.cpp b/zephcore/helpers/ui/ui_mesh_actions.cpp index f46a0b2..6166361 100644 --- a/zephcore/helpers/ui/ui_mesh_actions.cpp +++ b/zephcore/helpers/ui/ui_mesh_actions.cpp @@ -47,10 +47,12 @@ static uint32_t s_mesh_event_ui_action; /* Atomic UI action flags — set from input thread, consumed in mesh event loop */ static atomic_t pending_ui_actions; -/* Pending state for toggle actions (pass the value to mesh thread) */ -static volatile bool pending_gps_enabled; -static volatile bool pending_buzzer_quiet; -static volatile bool pending_offgrid_enabled; +/* Pending state for toggle actions (pass the value to mesh thread). + * Written before atomic_or on pending_ui_actions, read after atomic_clear, + * so the atomic provides ordering. Using atomic_t for portability. */ +static atomic_t pending_gps_enabled; +static atomic_t pending_buzzer_quiet; +static atomic_t pending_offgrid_enabled; extern "C" void ui_mesh_actions_init(struct k_event *mesh_events, uint32_t mesh_event_ui_action, @@ -87,7 +89,7 @@ extern "C" void mesh_gps_set_enabled(bool enable) gps_enable(enable); /* Defer the flash write (savePrefs) to mesh thread */ - pending_gps_enabled = enable; + atomic_set(&pending_gps_enabled, enable ? 1 : 0); atomic_or(&pending_ui_actions, UI_ACTION_GPS_TOGGLE); k_event_post(s_mesh_events, s_mesh_event_ui_action); } @@ -100,7 +102,7 @@ extern "C" void mesh_ble_set_enabled(bool enable) extern "C" void mesh_set_buzzer_quiet(bool quiet) { /* Defer the flash write (savePrefs) to mesh thread */ - pending_buzzer_quiet = quiet; + atomic_set(&pending_buzzer_quiet, quiet ? 1 : 0); atomic_or(&pending_ui_actions, UI_ACTION_BUZZER_TOGGLE); k_event_post(s_mesh_events, s_mesh_event_ui_action); } @@ -108,7 +110,7 @@ extern "C" void mesh_set_buzzer_quiet(bool quiet) extern "C" void mesh_set_offgrid_mode(bool enable) { /* Defer the flash write (savePrefs) to mesh thread */ - pending_offgrid_enabled = enable; + atomic_set(&pending_offgrid_enabled, enable ? 1 : 0); atomic_or(&pending_ui_actions, UI_ACTION_OFFGRID_TOGGLE); k_event_post(s_mesh_events, s_mesh_event_ui_action); } @@ -178,23 +180,23 @@ extern "C" void mesh_handle_ui_actions(void) bool need_save = false; if (actions & UI_ACTION_GPS_TOGGLE) { - s_mesh->prefs.gps_enabled = - pending_gps_enabled ? 1 : 0; - LOG_INF("GPS %s (button)", pending_gps_enabled ? "on" : "off"); + bool gps_en = atomic_get(&pending_gps_enabled) != 0; + s_mesh->prefs.gps_enabled = gps_en ? 1 : 0; + LOG_INF("GPS %s (button)", gps_en ? "on" : "off"); need_save = true; } if (actions & UI_ACTION_BUZZER_TOGGLE) { - s_mesh->prefs.buzzer_quiet = - pending_buzzer_quiet ? 1 : 0; - LOG_INF("buzzer_quiet=%d (button)", pending_buzzer_quiet); + bool bq = atomic_get(&pending_buzzer_quiet) != 0; + s_mesh->prefs.buzzer_quiet = bq ? 1 : 0; + LOG_INF("buzzer_quiet=%d (button)", bq); need_save = true; } if (actions & UI_ACTION_OFFGRID_TOGGLE) { - s_mesh->prefs.client_repeat = - pending_offgrid_enabled ? 1 : 0; - LOG_INF("client_repeat=%d (button)", pending_offgrid_enabled); + bool og = atomic_get(&pending_offgrid_enabled) != 0; + s_mesh->prefs.client_repeat = og ? 1 : 0; + LOG_INF("client_repeat=%d (button)", og); need_save = true; } diff --git a/zephcore/src/Mesh.cpp b/zephcore/src/Mesh.cpp index 5cb23ad..12fd029 100644 --- a/zephcore/src/Mesh.cpp +++ b/zephcore/src/Mesh.cpp @@ -409,10 +409,12 @@ Packet *Mesh::createControlData(const uint8_t *data, size_t len) 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; @@ -434,10 +436,12 @@ void Mesh::sendFlood(Packet *packet, uint32_t delay_millis, uint8_t path_hash_si 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; diff --git a/zephcore/src/Packet.cpp b/zephcore/src/Packet.cpp index 3ee5e5e..87e6395 100644 --- a/zephcore/src/Packet.cpp +++ b/zephcore/src/Packet.cpp @@ -81,8 +81,10 @@ uint8_t Packet::writeTo(uint8_t dest[]) const bool Packet::readFrom(const uint8_t src[], uint8_t len) { uint8_t i = 0; + if (len < 2) return false; header = src[i++]; if (hasTransportCodes()) { + if (len < 6) return false; memcpy(&transport_codes[0], &src[i], 2); i += 2; memcpy(&transport_codes[1], &src[i], 2); i += 2; } else { diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index 31eea92..7dc8a62 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -156,6 +156,8 @@ static void ble_on_disconnected(void) /* Reset message sync — un-ACKed peeked message stays in queue * and will be re-sent on next CMD_SYNC_NEXT_MESSAGE. */ companion_mesh_ptr->cancelSyncPending(); + /* Free Ed25519 sign buffer if allocated mid-operation */ + companion_mesh_ptr->cleanupSignState(); #endif ui_notify(UI_EVENT_BLE_DISCONNECTED); }