7.7 KiB
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. ProvidesgetCurrentRSSI(),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 withhash[1]andsecret[32].mesh::Packet— a mesh packet. Obtained fromPacketManager(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.
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())
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
- Trim trailing spaces —
"status "→"status" - Lowercase —
"Status"→"status" - Strip prefix —
"Security: status"→"status"(anything before": ") - 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 <name> |
change channel | channel:<name> |
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
# 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
secretandhashfrom 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 butsend_text("relay2 on ok")works, check response length/SNPRINTF. sendFloodwithpath_hash_size=1sends 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
createGroupDatagrammay return NULL if packet pool is full (16 slots).rtc_clock.getCurrentTime()returning bogus data (VolatileRTCClock starts from 0, fine).
Adding a new command
- Add
else ifblock inonGroupDataRecv()after line 168. - Use
strcmp(cmd, "command") == 0for exact match. - Call
send_text("response")to reply. - Update the
helpstring.
Config persistent storage
InternalFS(LittleFS) on nRF52 internal flash.SecurityConfig: armed + relay2 state saved as binary struct.- File:
/security.cfg, magic0x53454355.
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, noBaseChatMesh. BaseChatMesh— not used. SecurityMesh inherits directly frommesh::Mesh.- Serial commands — not processed. Serial is for debug output only.
onPeerDataRecv— not used. Group messaging only, no direct peer messages.