diff --git a/README.md b/README.md index c89e772..2acd40a 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ Other benefits: | **Heltec V3** | ESP32-S3 | SX1262 | OLED (SSD1306), 8MB flash | | **Heltec V4.2** | ESP32-S3 | SX1262 + GC1109 PA | OLED (SSD1306), 16MB flash, 2MB PSRAM | | **Heltec V4.3** | ESP32-S3 | SX1262 + KCT8103L PA | OLED (SSD1306), 16MB flash, 2MB PSRAM | +| **Heltec Wireless Tracker** | ESP32-S3 | SX1262 | ST7735R 160×80 TFT, UC6580 GPS | +| **LilyGo T-Beam v1.2** | ESP32 (PICO-D4) | SX1262 | AXP2101 PMU, GNSS, USB-UART CLI | ### Other @@ -204,7 +206,9 @@ Key Kconfig options (set in board configs or via `-D` flags): | `CONFIG_ZEPHCORE_MAX_CONTACTS` | 350 | Contact storage slots (companion) | | `CONFIG_ZEPHCORE_MAX_CHANNELS` | 40 | Channel slots (companion) | | `CONFIG_ZEPHCORE_BLE_PASSKEY` | 123456 | BLE pairing PIN | -| `CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC` | 300 | GPS fix interval (seconds) | +| `CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC` | 300 | Companion GPS duty interval between fixes (seconds); 0 = always-on | +| `CONFIG_ZEPHCORE_GPS_FIRST_FIX_TIMEOUT_SEC` | 300 | Cold-start window for the very first fix (longer to allow almanac download) | +| `CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC` | 172800 | Repeater/room-server GPS duty interval boot default (48 h); 0 = always-on | | `CONFIG_ZEPHCORE_WIFI_OTA` | n | WiFi AP + HTTP OTA updates (ESP32 repeaters, requires `--sysbuild`) | | `CONFIG_ZEPHCORE_REPEATER_UPLINK` | n | Repeater WiFi+MQTT uplink (ESP32) | | `CONFIG_ZEPHCORE_PACKET_LOGGING` | n | Arduino-compatible mesh packet logging | diff --git a/zephcore/ARCHITECTURE.md b/zephcore/ARCHITECTURE.md index 1b98875..f3726f2 100644 --- a/zephcore/ARCHITECTURE.md +++ b/zephcore/ARCHITECTURE.md @@ -429,8 +429,9 @@ Power: `powersaving on/off` - 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. +- Advertising always uses `BT_LE_ADV_OPT_USE_IDENTITY` — exposes the stable identity address even when privacy is enabled, preserving Android connect-from-app +- `CONFIG_BT_PRIVACY` **disabled** on nRF52840 / MG24: identity address is advertised directly; both iOS and Android work without RPA. Android's Flutter BLE plugin fails `connectGatt()` to RPA-advertised devices from app context. +- `CONFIG_BT_PRIVACY` **enabled** on ESP32-S3 (`boards/common/esp32_common.conf`): the Espressif controller's privacy-OFF Secure-Connections path produces a MIC failure against iOS (HCI disconnect `0x3d` at encryption start). Privacy ON keeps the controller on its working SC path. `USE_IDENTITY` advertising preserves Android compatibility. Do **not** remove `USE_IDENTITY` while ESP32 privacy is on. - 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 @@ -442,21 +443,50 @@ Power: `powersaving on/off` - **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) +- **BLE bonds**: NVS (`storage_partition`, 0xD0000 on nRF52) via Zephyr settings backend (≥1.16.2) - **Prefs**: 292-byte binary format, Arduino-compatible, field-by-field I/O (see §13) - **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 +**First-boot migration (3-way FS self-heal)** + +A marker file `/lfs/_zc_init` is written after the first clean ZephCore boot. On every subsequent boot it is present and the logic below is skipped. On first boot (marker absent), `main_companion.cpp` picks one of three paths before `bt_enable()` runs: + +1. **No prefs, or Arduino MeshCore prefs** → full LFS + NVS format. Arduino's `new_prefs` omits `node_lat`/`node_lon`, shifting `freq`/`sf`/`bw` by 16 bytes; `prefsLookLikeArduino()` detects this by range-checking those fields. Covers fresh installs and Arduino → ZephCore migrations. +2. **Valid ZephCore prefs + `/lfs/settings` present** → NVS-only erase (`formatNVSOnly()`). ZephCore ≤1.16.1 stored BLE bonds in `/lfs/settings` (file backend); ≤1.16.1 used 0xD0000 as app code, so bytes there may pass NVS sector validation and hang `settings_load()`. Identity/prefs/contacts are preserved; re-pairing is required. +3. **Valid ZephCore prefs + no `/lfs/settings`** → skip format entirely. NVS was already initialised by ZephCore ≥1.16.2; bonds survive the upgrade. + +`loadPrefs()` also range-checks `freq`/`sf`/`bw` after deserialisation and reverts to compile-time defaults on out-of-range values, so a misread Arduino prefs file never corrupts the radio config. + ### 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 +**Duty cycle vs always-on** + +`gps_wake_interval_ms` (initialised from `prefs.gps_interval`) controls the mode: + +- **Duty cycling** (`gps_wake_interval_ms > 0`): after acquiring 3 good fixes the GPS powers down; the state machine wakes it again after the configured standby interval. The fix callback fires and then the GPS sleeps. +- **Always-on** (`gps_wake_interval_ms == 0`): the GPS never powers down. `consecutive_good_fixes` is reset after each promotion so the 3-fix gate cycles continuously, streaming fresh positions. Flash writes and fix callbacks are rate-limited to once per `gps_acquire_timeout_ms` to avoid hammering storage. + +`gps_set_poll_interval_sec(0)` switches to always-on live; persisted via `prefs.gps_interval` (set by `set gps duty 0`). + +**Timeout split** + +Two separate timeouts apply to acquisition: + +- `CONFIG_ZEPHCORE_GPS_FIRST_FIX_TIMEOUT_SEC` (default 300s): the cold-start window used for the very first acquisition after `gps_enable()`. Longer to allow almanac download. +- `CONFIG_ZEPHCORE_GPS_FIX_TIMEOUT_SEC` (default 120s): the normal per-wake timeout for all subsequent acquisitions (warm start). + +**Repeater mode** + +Repeaters and room servers default to `CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC` (48 h) for GPS duty — GPS wakes only for a periodic time-sync fix (5-minute acquire window). The interval is now unified with companion via `prefs.gps_interval` and is configurable at runtime via `set gps duty `; persists across reboots. + ### 7.4 USB (`adapters/usb/`) - **CompanionUSB**: V3-framed CDC (little-endian 16-bit length prefix + payload) @@ -569,6 +599,8 @@ prj.conf (base: console; production defaults — LOG=n, ASSERT=n) | 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 | +| Heltec Wireless Tracker | ESP32-S3 | SX1262 | UC6580 | TFT 160x80 | - | - | - | 350 | +| LilyGo T-Beam v1.2 | ESP32 | SX1262 | gnss-nmea | - | - | - | - | 300 | | XIAO MG24 | EFR32MG24 | SX1262 | - | - | - | - | - | 350 | --- @@ -667,7 +699,7 @@ Over USB CDC: V3 framing: `[2B LE length] [1B opcode] [payload...]` | `/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) | +| `storage_partition` (NVS, 0xD0000 nRF52) | BLE bonds + Zephyr settings | NVS settings backend (≥1.16.2; old `/lfs/settings` file detected by self-heal) | ### Preferences Binary Layout (292 bytes) diff --git a/zephcore/adapters/datastore/ZephyrDataStore.cpp b/zephcore/adapters/datastore/ZephyrDataStore.cpp index d09fe87..3bfb74d 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.cpp +++ b/zephcore/adapters/datastore/ZephyrDataStore.cpp @@ -134,7 +134,7 @@ void ZephyrDataStore::begin() checkAdvBlobFile(); } -bool ZephyrDataStore::exists(const char *path) +bool ZephyrDataStore::exists(const char *path) const { struct fs_dirent ent; return fs_stat(path, &ent) == 0; @@ -145,7 +145,7 @@ bool ZephyrDataStore::removeFile(const char *path) return fs_unlink(path) == 0; } -bool ZephyrDataStore::openRead(const char *path, uint8_t *buf, size_t buf_sz, size_t &out_len) +bool ZephyrDataStore::openRead(const char *path, uint8_t *buf, size_t buf_sz, size_t &out_len) const { struct fs_file_t file; fs_file_t_init(&file); diff --git a/zephcore/adapters/datastore/ZephyrDataStore.h b/zephcore/adapters/datastore/ZephyrDataStore.h index 2ed18e0..18650ff 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.h +++ b/zephcore/adapters/datastore/ZephyrDataStore.h @@ -90,9 +90,9 @@ private: void checkAdvBlobFile(); void migrateToExternalFS(); - bool openRead(const char *path, uint8_t *buf, size_t buf_sz, size_t &out_len); + bool openRead(const char *path, uint8_t *buf, size_t buf_sz, size_t &out_len) const; bool atomicReplaceFile(const char *path, const uint8_t *buf, size_t len); - bool exists(const char *path); + bool exists(const char *path) const; bool removeFile(const char *path); bool copyFile(const char *src, const char *dst); }; diff --git a/zephcore/boards/example_board/README.md b/zephcore/boards/example_board/README.md index f3976c0..2a844c0 100644 --- a/zephcore/boards/example_board/README.md +++ b/zephcore/boards/example_board/README.md @@ -43,6 +43,7 @@ SWD flash: `west flash` (requires J-Link, pyocd, or nrfjprog connected). | Heltec V4.2 (GC1109 PA) | `west build -b heltec_wifi_lora32_v4/esp32s3/procpu zephcore` | `west flash` | | Heltec V4.3 (KCT8103L PA) | `west build -b heltec_wifi_lora32_v43/esp32s3/procpu zephcore` | `west flash` | | Heltec Wireless Tracker | `west build -b heltec_wireless_tracker/esp32s3/procpu zephcore` | `west flash` | +| LilyGo T-Beam v1.2 | `west build -b ttgo_tbeam/esp32/procpu zephcore` | `west flash` | **Heltec V3 console:** ZephCore routes console/shell to `uart0` on V3. Use the UART serial port for boot logs and CLI. @@ -51,6 +52,13 @@ unclear, check GPIO2's default pull: the V4.2 GC1109 PA has an internal pull-dow (GPIO2 reads LOW at boot), while the V4.3 KCT8103L PA has an internal pull-up (GPIO2 reads HIGH). Only difference in firmware: TX control pin GPIO46→GPIO5. +**LilyGo T-Beam v1.2:** Classic ESP32 (PICO-D4) board — several caveats apply: +- Upstream Zephyr DTS models the SX1276 variant; `board.overlay` overrides the radio node to SX1262 on the same SPI3 wiring. +- PICO-D4 rev 1.0 bootloops when the bootloader tries to enable QIO flash mode. `board.conf` forces DIO (`CONFIG_ESPTOOLPY_FLASHMODE_DIO=y`). Any new classic ESP32 board with PICO-D4 needs this. +- Classic ESP32 DRAM is much smaller than ESP32-S3. `board.conf` shrinks contacts/channels/queue (`MAX_CONTACTS=160`, `MAX_CHANNELS=8`, `OFFLINE_QUEUE_SIZE=128`) to fit. +- AXP2101 PMU manages LoRa and GPS power rails via Zephyr regulator + fuel-gauge drivers (auto-enabled from the upstream DTS PMU nodes). Add `CONFIG_FUEL_GAUGE=y` to boards that expose battery state-of-charge over I2C. +- Console/CLI are on `uart0` (onboard USB-UART). No native USB on classic ESP32. + ESP32 flash uses esptool over USB via `west flash`. Hold BOOT button if the device doesn't enter download mode automatically. @@ -67,6 +75,26 @@ west build -b /esp32s3/procpu zephcore --pristine --sysbuild -- \ west flash --esp-device COMX ``` +### SX127x Boards (loramac-node backend) + +ZephCore supports SX1272/SX1276/SX1278 via the loramac-node backend — a separate radio path from the native SX126x driver used by all other boards. The TTGO LoRa32 is the reference implementation: + +| Board | Build string | Flash | +|----------------|------------------------------------------------|--------------| +| TTGO LoRa32 | `west build -b ttgo_lora32/esp32/procpu zephcore` | `west flash` | + +SX127x boards require these `board.conf` overrides (the `zephcore_common.conf` default enables the native SX126x path): + +```kconfig +CONFIG_LORA_MODULE_BACKEND_NATIVE=n +CONFIG_LORA_MODULE_BACKEND_LORAMAC_NODE=y +CONFIG_ZEPHCORE_RADIO_SX127X=y +CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE=n # lora_recv_duty_cycle not implemented for SX127x +CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM=17 # PA_BOOST max without external PA +``` + +Classic ESP32 + PICO-D4 also need the DIO flash mode fix (see T-Beam note above). + **One-time setup required (all ESP32 boards):** ``` @@ -248,6 +276,9 @@ should ONLY contain settings that can't be inferred from hardware: CONFIG_HEAP_MEM_POOL_SIZE Override for large displays (>128x64) CONFIG_SEGGER_RTT_BUFFER_SIZE_UP Shrink RTT on RAM-tight boards CONFIG_ESPTOOLPY_FLASHSIZE_16MB ESP32 boards with 16MB flash + CONFIG_ESPTOOLPY_FLASHMODE_DIO=y Classic ESP32 PICO-D4 boards (bootloops otherwise) + CONFIG_ESPTOOLPY_FLASHMODE_QIO=n Companion to the DIO override above + CONFIG_FUEL_GAUGE=y Boards with AXP2101 or other I2C fuel gauge CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM Boards with external PA CONFIG_ZEPHCORE_MAX_TX_POWER_DBM Boards with external PA CONFIG_ZEPHCORE_APC Adaptive Power Control — OFF by default. @@ -331,16 +362,16 @@ Quick Reference: Wio-SX1262 XIAO Pin Mapping All XIAO boards use the same D-pin assignment for Wio-SX1262: - Signal | XIAO Pin | nRF52840 | nRF54L15 | MG24 | ESP32-C3 - -------|----------|-----------|-----------|-----------|-------- - DIO1 | D1 | P0.03 | P1.05 | PC01 | GPIO3 - RESET | D2 | P0.28 | P1.06 | PC02 | GPIO4 - BUSY | D3 | P0.05 | P1.07 | PC03 | GPIO5 - NSS | D4 | P0.04 | P1.10 | PC04 | GPIO6 - RXEN | D5 | P0.29 | P1.11 | PC05 | GPIO7 - SCK | D8 | P1.13 | P2.01 | PA03 | GPIO8 - MISO | D9 | P1.14 | P2.04 | PA04 | GPIO9 - MOSI | D10 | P1.15 | P2.02 | PA05 | GPIO10 + Signal | XIAO Pin | nRF52840 | nRF54L15 | MG24 | ESP32-C3 | ESP32-C6 | ESP32-S3 + -------|----------|-----------|-----------|-----------|-----------|-----------|---------- + DIO1 | D1 | P0.03 | P1.05 | PC01 | GPIO3 | GPIO1 | GPIO39 + RESET | D2 | P0.28 | P1.06 | PC02 | GPIO4 | GPIO2 | GPIO42 + BUSY | D3 | P0.05 | P1.07 | PC03 | GPIO5 | GPIO21 | GPIO40 + NSS | D4 | P0.04 | P1.10 | PC04 | GPIO6 | GPIO22 | GPIO41 + RXEN | D5 | P0.29 | P1.11 | PC05 | GPIO7 | GPIO23 | GPIO38 + SCK | D8 | P1.13 | P2.01 | PA03 | GPIO8 | GPIO19 | GPIO7 + MISO | D9 | P1.14 | P2.04 | PA04 | GPIO9 | GPIO20 | GPIO8 + MOSI | D10 | P1.15 | P2.02 | PA05 | GPIO10 | GPIO18 | GPIO9 Note: nRF52840 D-pin mapping varies by board (XIAO nRF52840 shown). RAK4631 has SX1262 integrated — different pinout entirely.