# Security Mesh Node — Architecture & Development Guide ## Project Structure ``` security-new/ ├── platformio.ini # PlatformIO config (targets, libs, flags) ├── src/ │ ├── main.cpp # Main firmware — SecurityMesh class + loop │ ├── target.h # Pin mappings, radio params, platform defs │ └── variants/ │ └── promicro/ # ProMicro-specific files ├── boards/ # Custom linker scripts ├── scripts/ # Build helper scripts ├── .pio/ # Build output (ignored) ``` All MeshCore library lives at `../MeshCore/`. --- ## How Radio Mesh Works ### Low-level Layer (RadioLib) - `radio_module` (SX1262) — raw LoRa send/receive. - `radio_driver` (CustomSX1262Wrapper) — RadioLib wrapper. Provides `getCurrentRSSI()`, `setParams()`, `setTxPower()`. ### Mesh Layer (MeshCore) - `mesh::Mesh` — main class that manages flooding, routing, encryption, channel storage. - `mesh::Dispatcher` — base of Mesh (packet scheduling, radio control). - `mesh::GroupChannel` — channel with `hash[1]` and `secret[32]`. - `mesh::Packet` — a mesh packet. Obtained from `PacketManager` (pool of 16 slots). ### Lifecycle ``` 1. radio_module.begin() — init SX1262 2. radio_driver.setParams(...) — freq, BW, SF, CR 3. Mesh.begin() — starts listening 4. Mesh.loop() loop() — pumps radio ISR, processes incoming 5. onGroupDataRecv() callback — called when encrypted group msg arrives ``` --- ## SecurityMesh Class ### Channel Derivation (`setup_channel()`) ``` SHA256("1234") → 32 bytes key mesh_channel.secret = key[0..15] (first 16 bytes) mesh_channel.hash = SHA256(secret)[0..0] (first 1 byte, PATH_HASH_SIZE=1) ``` ### Critical Virtual Overrides #### `searchChannelsByHash()` **WHY IT'S NEEDED:** `Mesh::loop()` calls `searchChannelsByHash()` to match incoming packet hashes against stored channels. The base implementation returns 0 (no matches). Without this override, **every incoming group message is silently dropped** and `onGroupDataRecv` is never called. ```cpp int searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel channels[], int max_matches) override { if (max_matches > 0 && channel_ready && memcmp(hash, mesh_channel.hash, sizeof(mesh_channel.hash)) == 0) { channels[0] = mesh_channel; return 1; // found 1 match } return 0; } ``` `sizeof(mesh_channel.hash)` = `PATH_HASH_SIZE` = 1 byte. #### `onGroupDataRecv()` Called for each decrypted group message that matched a channel. Format: `[4 bytes ts][1 byte type][text]`. Type byte must be 0 for text. ### Sending (`send_text()`) ```cpp void send_text(const char* text) { uint8_t buf[256]; buf[0..3] = timestamp (4 bytes) buf[4] = 0 (type: text) buf[5..] = text Packet* pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, mesh_channel, buf, 5 + text_len); sendFlood(pkt, 0, 1); // 1 hop } ``` `sendFlood` with `path_hash_size=1` means 0-hop (immediate neighbors only). Increase to flood across the mesh. --- ## Command Processing All commands are processed in `onGroupDataRecv()`: ### Preprocessing 1. **Trim trailing spaces** — `"status "` → `"status"` 2. **Lowercase** — `"Status"` → `"status"` 3. **Strip prefix** — `"Security: status"` → `"status"` (anything before `": "`) 4. **Trim leading spaces** ### Command Table | Command | Action | Response | |---------|--------|----------| | `help` | list commands | `help:...` | | `ohrana on` | arm, relay1 ON | `ohrana on ok` | | `ohrana off` | disarm, relay1 OFF | `ohrana off ok` | | `relay1 on` | relay1 ON (direct) | `relay1 on ok` | | `relay1 off` | relay1 OFF (direct) | `relay1 off ok` | | `relay2 on` | relay2 ON | `relay2 on ok` | | `relay2 off` | relay2 OFF | `relay2 off ok` | | `status` | report state | `status:armed/off temp:±X.XC hall:trig/ok motion:trig/ok` | | `ver` | build date | `build:Mon DD YYYY HH:MM:SS` | | `scan` | noise floor | `noise:-XXXdBm` | | `security_conf ` | change channel | `channel:` | ### Auto-Sent Messages - **Startup:** `poweron Mon DD YYYY HH:MM:SS` — sent 10s after boot. - **Hall alert:** `alert:hall` — when armed and reed opens. - **Motion alert:** `alert:motion` — when armed and PIR triggers. Cooldown: 10 minutes between alerts (ALERT_COOLDOWN_MS). --- ## Hardware | Pin (nRF52) | Pin (ProMicro) | Function | |-------------|----------------|----------| | 30 | 13 | NSS (LoRa SPI CS) | | 29 | 12 | SCK | | 31 | 14 | MOSI | | 2 | 15 | MISO | | 7 | 11 | DIO1 (LoRa IRQ) | | 6 | 10 | RST | | 10 | 16 | BUSY | | — | 21 | POWER_EN (ProMicro only) | | 12 | 0 | REED (hall sensor) | | 11 | 1 | PIR (motion) | | 3 | 2 | DS18B20 (temp) | | 4 | 3 | RELAY_1 | | 5 | 4 | RELAY_2 | | 13 | 22 | LED_STATUS | **Reference:** `target.h` lines 25–114. --- ## Radio Config | Param | Value | |-------|-------| | Frequency | 868.731018 MHz | | Bandwidth | 62.5 kHz | | Spreading Factor | 7 | | Coding Rate | 7 (4/7) | | TX Power | 22 dBm | | TCXO voltage | 1.8V | | Current limit | 140 mA | | RX Boosted Gain | on | | DIO2 as RF switch | on | `PATH_HASH_MODE=0` → `PATH_HASH_SIZE=1` (minimal hash, maximum throughput). --- ## Build & Upload ```bash # Build pio run -e promicro_security # Upload (via nrfutil) pio run -e promicro_security -t upload # Monitor pio device monitor -b 115200 ``` Current resource usage: RAM 8%, Flash 36.5%. --- ## Common Pitfalls & Debugging ### `onGroupDataRecv` not called despite LED blink The LED blink (3 fast flashes) is the first thing in `onGroupDataRecv`. If LED doesn't blink: - Check `searchChannelsByHash()` override — it's required. - Check hash match: both sides must derive the same `secret` and `hash` from passphrase. - Check radio config matches (freq, BW, SF, CR). ### No response despite LED blinking - Command was received (LED blinked) but processing or send failed. - **Test with simple response** — if `send_text("ok")` fails but `send_text("relay2 on ok")` works, check response length/SNPRINTF. - `sendFlood` with `path_hash_size=1` sends only to neighbors. If response doesn't arrive at the remote app, check app's mesh routing or increase path_hash_size. ### `send_text()` doesn't actually send - `createGroupDatagram` may return NULL if packet pool is full (16 slots). - `rtc_clock.getCurrentTime()` returning bogus data (VolatileRTCClock starts from 0, fine). ### Adding a new command 1. Add `else if` block in `onGroupDataRecv()` after line 168. 2. Use `strcmp(cmd, "command") == 0` for exact match. 3. Call `send_text("response")` to reply. 4. Update the `help` string. ### Config persistent storage - `InternalFS` (LittleFS) on nRF52 internal flash. - `SecurityConfig`: armed + relay2 state saved as binary struct. - File: `/security.cfg`, magic `0x53454355`. --- ## Key Files in MeshCore Library | File | What it provides | |------|-----------------| | `MeshCore/src/Mesh.h` | `Mesh`, `GroupChannel`, virtual methods | | `MeshCore/src/MeshCore.h` | Constants (`PUB_KEY_SIZE`, `PATH_HASH_SIZE`, etc.) | | `MeshCore/src/Dispatcher.h` | `Dispatcher`, packet scheduling | | `MeshCore/src/helpers/SimpleMeshTables.h` | Duplicate packet detection | | `MeshCore/src/helpers/StaticPoolPacketManager.h` | Packet memory pool | | `MeshCore/src/helpers/ChannelDetails.h` | Channel helper structs | | `MeshCore/src/helpers/NRF52Board.h` | nRF52 board abstraction | --- ## Things That Won't Work - **BLE** — not included. No `BLESerialInterface`, no `BaseChatMesh`. - **`BaseChatMesh`** — not used. SecurityMesh inherits directly from `mesh::Mesh`. - **Serial commands** — not processed. Serial is for debug output only. - **`onPeerDataRecv`** — not used. Group messaging only, no direct peer messages.