-
Notifications
You must be signed in to change notification settings - Fork 2
Device Inclusion
Inclusion is the secure onboarding process by which a fresh device joins a RadioMesh network coordinated by a hub. It performs mutual authentication, exchanges a network-wide encryption key, and persists enough state on the device that it auto-rejoins after reboot without re-running the handshake.
The protocol is inspired by the inclusion sequences used by Zigbee, Z-Wave, and LoRaWAN, adapted for low-power LoRa meshes.
The two sides of the handshake are:
- A hub (
MeshDeviceType::HUB) — broadcasts inclusion availability and validates joining devices. - A standard device (
MeshDeviceType::STANDARD) — listens for hub broadcasts and requests to join.
Inclusion is fully driven by the framework. The application controls only two things: when the hub enters inclusion mode, and how it reacts to inclusion progress callbacks.
The hub and joining device exchange five message types (topics 0x06–0x0A) in a fixed order:
| Step | Direction | Message | Topic | Encrypted? | Payload |
|---|---|---|---|---|---|
| 1 | Hub → broadcast | INCLUDE_OPEN |
0x08 | No | Hub announces it is accepting devices |
| 2 | Device → hub | INCLUDE_REQUEST |
0x06 | No | Device public key + initial counter |
| 3 | Hub → device | INCLUDE_RESPONSE |
0x07 | Partial | Hub public key + ECDH-encrypted network key + nonce |
| 4 | Device → hub | INCLUDE_CONFIRM |
0x09 | Yes (net key) | Encrypted nonce (proves key decryption) |
| 5 | Hub → device | INCLUDE_SUCCESS |
0x0A | Yes (net key) | Inclusion completed |
Steps 1 and 2 must be unencrypted: the device does not yet hold the network key, and the hub does not yet know which device is asking to join. Step 3 marks the cryptographic handoff — from that point on, all messages between hub and device use the network key.
The flow is documented in source at src/framework/device/inc/InclusionController.h:14-21.
-
NOT_INCLUDED— Device has no network key. Can only send inclusion messages. This is the state of any fresh device. -
INCLUSION_PENDING— A handshake is in progress. -
INCLUDED— Device holds a valid network key and can send/receive application messages.
Query the current state with device->isIncluded().
-
NORMAL— Hub does not accept new devices. Existing included devices continue to operate normally. -
INCLUSION— Hub is broadcastingINCLUDE_OPENand ready to onboard new devices.
device = builder.start()
.withLoraRadio(radioParams)
.withSecureMessaging(securityParams)
.withRxPacketCallback(onPacketReceived)
.build("MyDevice", device_id, MeshDeviceType::STANDARD);No additional builder calls are needed. The device automatically listens for INCLUDE_OPEN once it is built and setup() is called.
device = builder.start()
.withLoraRadio(radioParams)
.withWifiAccessPoint(apParams)
.withDevicePortal(portalParams)
.build("MyHub", hub_id, MeshDeviceType::HUB);The hub typically pairs inclusion with withDevicePortal so an operator can toggle inclusion mode from a web UI (see examples/DeviceInclusion/MiniHub/).
The application controls inclusion through two IDevice methods:
// Hub-only: start/stop broadcasting INCLUDE_OPEN.
int enableInclusionMode(bool enable);
// Both: check current network membership.
bool isIncluded() const;
// Both: erase persisted inclusion state, returning the device to NOT_INCLUDED.
int factoryReset();Standard devices need no runtime call — joining happens automatically when a hub is in inclusion mode and the device receives INCLUDE_OPEN.
A common hub pattern is to call enableInclusionMode(false) automatically after a successful inclusion to limit the join window (see MiniHub.ino).
-
ECDH-style key exchange. Each side generates a fresh keypair per session. The device sends its public key in
INCLUDE_REQUEST; the hub uses it to encrypt the network key inINCLUDE_RESPONSE. Only the holder of the matching device private key can decrypt the network key. -
Plaintext bootstrap window.
INCLUDE_OPENandINCLUDE_REQUESTare unavoidably unencrypted — neither party has a shared secret yet. This window is bounded by the session timeout and validated by the nonce check in step 4. -
Per-session nonce. The hub includes a 4-byte random nonce in
INCLUDE_RESPONSE. The device must echo this nonce, encrypted with the freshly-received network key, inINCLUDE_CONFIRM. This proves the device actually decrypted the network key rather than replaying a captured exchange. -
MIC authentication. When
SecurityMethod::AESis in use, every encrypted packet carries a Message Integrity Code that the receiver validates before accepting the payload. Tampered or replayed packets are dropped silently. - Network key scope. Once inclusion completes, the device uses the network key for all application traffic. The device's per-session keypair is no longer needed and is not used for routine messages.
Inclusion state is persisted to flash so a rebooted device re-enters the network without re-running the handshake. The InclusionController stores four items under short keys:
| Key | Contents |
|---|---|
is |
Inclusion state |
mc |
Message counter (replay protection) |
pk |
Device private key |
hk |
Hub public key |
On startup, loadAndApplyNetworkKey() restores the device's crypto context from these values. If they are intact, the device is immediately INCLUDED; if missing or corrupt, it falls back to NOT_INCLUDED and listens for the next inclusion broadcast.
To force a fresh inclusion (e.g. moving a device to a different network), call device->factoryReset(), which wipes these keys.
-
Session timeout: 60 seconds. Each handshake step has a fixed window. If the peer does not respond in time, the protocol state resets to
IDLEand the device returns toNOT_INCLUDED(or, on the hub, stays in inclusion mode awaiting another device). - Total timeout: 60 seconds. A complete handshake must finish within this window.
-
Auto factory reset (application policy). The
StandardDeviceexample demonstrates a 5-minute fallback: if the device fails to join withinFACTORY_RESET_TIMEOUT, it callsfactoryReset()and restarts inclusion from a clean slate. This is application-level policy, not framework-enforced. -
Silent drops. Packets that fail MIC validation, replay-counter checks, or topic-vs-state validation (e.g. a
NOT_INCLUDEDdevice trying to send application data) are dropped without notification. Use the packet callback for visibility.