244 lines
8.1 KiB
Markdown
244 lines
8.1 KiB
Markdown
# 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` |
|
||
| `relay` | state of both relays | `relay:1on 2off` |
|
||
| `status` | report state | `status:armed/off temp:±X.XC hall:trig/ok motion:trig/ok bat:X.XXV/USB` |
|
||
| `ver` | build date | `build:Mon DD YYYY HH:MM:SS` |
|
||
| `scan` | noise floor | `noise:-XXXdBm` |
|
||
| `canal <name> <pass>` | change channel and reboot | `canal test 5674 ok` |
|
||
|
||
### Response Delay
|
||
|
||
All command responses are sent with a **5-second delay** (non-blocking, via `defer_response`).
|
||
The action (relay toggle, armed state change) happens immediately — only the reply text is deferred.
|
||
Alerts (`alert:hall`, `alert:motion`) and startup (`poweron`) are sent immediately without delay.
|
||
|
||
### 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.
|