Chapter 10: LPW Provisioner
In Chapter 9, we saw how the Device Agent represents a single managed device. But how does that device get onto the hub's network in the first place? That's the job of the LPW (Low-Power Wireless) Provisioner — the onboarding coordinator that bridges cloud provisioning commands with local radio operations.
This is the final chapter of our walkthrough. By the end, you'll understand the complete data flow from cloud to radio chip.
10.1 What the LPW Provisioner Does
The Provisioner sits between two worlds:
- Cloud side: receives onboarding commands via MQTT (see Chapter 5)
- Radio side: talks to Zigbee, Z-Wave, WiFi, and Matter radios through plugins
Its core job: when the cloud says "onboard this Zigbee sensor," the Provisioner picks the right radio plugin, feeds it the authentication material, opens the network, and reports back when the device joins.
📁 Key directories: -
provisioning/IoTSmartHomeDevice-LPWProvisionerCore/— C++ core -provisioning/IotManagedIntegrationsHubSDK-LPWProvisionerCore-C/— Pure C core -provisioning/plugins/— Protocol-specific plugins
10.2 The RadioInterface Abstraction
The Provisioner uses the Strategy pattern — one abstract interface, multiple protocol implementations. Let's look at the C++ version first.
iotshd_provisioner_radio_interface.h defines the base class:
class IotshdLPWProvisionerRadioInterface {
public:
virtual iotmi_statusCode_t Init() = 0;
virtual iotmi_statusCode_t AddAuthMaterial(
IotshAuthMaterial_t auth_material) = 0;
virtual iotmi_statusCode_t SetNetworkMode(
IOTSHD_NETWORK_MODE mode) = 0;
virtual iotmi_statusCode_t RemoveNode(int nodeId) = 0;
virtual void SetCallback(Callback callback) = 0;
// ... more virtual methods
};
Every radio plugin implements this same interface. The Provisioner doesn't care which protocol — it just calls Init(), AddAuthMaterial(), and SetNetworkMode() on whichever plugin matches the request.
Supported Radio Types
iotshd_provisioner_radio_structs.h enumerates the five supported radios:
enum IOTSHD_RADIO_INTERFACE {
IOTSHD_RADIO_INTERFACE_ZWAVE = 0,
IOTSHD_RADIO_INTERFACE_ZIGBEE = 1,
IOTSHD_RADIO_INTERFACE_MATTER = 2,
IOTSHD_RADIO_INTERFACE_WIFI = 3,
IOTSHD_RADIO_INTERFACE_CUSTOM = 4
};
Authentication Materials
Each protocol has its own credential type, unified through a union:
typedef union {
struct IotshdZwaveDsk zwave_dsk; // 47-char DSK
struct IotshdZigbeeInstallCode zigbee_install_code; // MAC + code
struct IotshdWifiAuth wifi_auth; // SN + UPC + expiry
} IotshAuthMaterial_t;
The cloud sends the right credential type for each protocol, and the Provisioner routes it to the matching plugin.
10.3 Network Modes
The Provisioner controls when and how devices can join via network modes:
enum IOTSHD_NETWORK_MODE {
IOTSHD_SMARTHOME_NETWORK_CLOSED,
IOTSHD_SMARTHOME_NETWORK_OPEN_FOR_ZERO_TOUCH,
IOTSHD_SMARTHOME_NETWORK_OPEN_FOR_LEGACY_JOIN,
IOTSHD_SMARTHOME_NETWORK_OPEN_FOR_REMOVE,
};
| Mode | Zigbee Behavior | Z-Wave Behavior |
|---|---|---|
CLOSED |
No joins allowed | No joins allowed |
ZERO_TOUCH |
Install-code commissioning (long-lived) | SmartStart with known DSK |
LEGACY_JOIN |
Open commissioning (~30s timeout) | NWI mode |
OPEN_FOR_REMOVE |
Not valid | NWE exclusion mode |
Zero-touch mode is the default — the hub can stay in this mode for months, only allowing pre-authorized devices to join.
10.4 The Observer/Callback Pattern
Plugins report events back to the Provisioner through callbacks. In C++:
Events include things like DEVICE_ADDED, DEVICE_REMOVED, NETWORK_READY, and SCAN_COMPLETE. The full list lives in iotshd_provisioner_radio_structs.h:
enum IOTSHD_RADIO_EVENTS {
IOTSHD_RADIO_EVENT_DEVICE_ADDED,
IOTSHD_RADIO_EVENT_DEVICE_REMOVED,
IOTSHD_RADIO_EVENT_NETWORK_READY,
IOTSHD_RADIO_EVENT_SCAN_COMPLETE,
// ... 20+ event types
};
When a Zigbee sensor successfully joins, the plugin fires DEVICE_ADDED. The Provisioner catches it and notifies the cloud.
10.5 The Message Queue
Internally, the Provisioner uses an async message queue to decouple cloud commands from radio operations. This is defined in iotmi_provisioner_message.h.
Message Types
There are 23+ message types covering the full provisioning lifecycle:
typedef enum {
IOTMI_MSG_TYPE_START_RADIO_ZIGBEE,
IOTMI_MSG_TYPE_START_RADIO_ZWAVE,
IOTMI_MSG_TYPE_FETCH_AUTH_MATERIAL,
IOTMI_MSG_TYPE_ADD_ONE_AUTH_MATERIAL,
IOTMI_MSG_TYPE_SET_NETWORK_MODE,
IOTMI_MSG_TYPE_REMOVE_NODE,
IOTMI_MSG_TYPE_CLOUD_MESSAGE,
// ... and more
} IotmiMessageType_t;
Message Structure
Each message carries a type and an optional payload:
Typed Payloads
Different message types use different payload structs. For example, adding auth material:
typedef struct {
IotmiRadioInterface_t interface;
IotmiAuthMaterial_t auth_material;
} IotmiAddOneAuthMaterialPayload_t;
The queue processes messages sequentially, ensuring radio operations don't collide. When a cloud command arrives via MQTT, it becomes a message on this queue.
10.6 Plugin Lifecycle
Each plugin goes through a well-defined lifecycle tracked by status:
enum IOTSHD_PLUGIN_STATUS {
IOTSHD_PLUGIN_STATUS_ERROR,
IOTSHD_PLUGIN_STATUS_INITIALIZED,
IOTSHD_PLUGIN_STATUS_RUNNING,
IOTSHD_PLUGIN_STATUS_READY,
IOTSHD_PLUGIN_STATUS_STOPPED
};
The typical flow:
- Provisioner starts → sends
IOTMI_MSG_TYPE_START_RADIO_ZIGBEE - Plugin
Init()called → initializes middleware and radio hardware - Status →
READY→ plugin can accept commands - Cloud sends auth material → queued as
ADD_ONE_AUTH_MATERIAL - Network mode set → plugin opens the radio for joining
- Device joins → plugin fires
DEVICE_ADDEDcallback
10.7 Zigbee Onboarding: A Concrete Example
Let's trace a Zigbee sensor being onboarded end-to-end.
📁
plugins/IoTSmartHomeDevice-Zigbee-Provisioner-Plugin/
The Zigbee Plugin Class
iotshd_provisioner_plugin_zigbee.hpp shows the concrete implementation:
class ZigbeeRadioInterface
: public IotshdLPWProvisionerRadioInterface {
public:
iotmi_statusCode_t Init() override;
iotmi_statusCode_t AddAuthMaterial(
IotshAuthMaterial_t authMaterial) override;
iotmi_statusCode_t SetNetworkMode(
IOTSHD_NETWORK_MODE mode) override;
// ... all virtual methods implemented
};
Init: Starting the Radio
From iotshd_provisioner_plugin_zigbee.cpp:
iotmi_statusCode_t ZigbeeRadioInterface::Init() {
iotmi_statusCode_t aceStatus =
iotshdZigbeeInit(reinterpret_cast<void*>(&m_callback));
m_status = (aceStatus == IOTMI_STATUS_OK)
? IOTSHD_PLUGIN_STATUS_READY
: IOTSHD_PLUGIN_STATUS_ERROR;
return aceStatus;
}
The plugin initializes the Zigbee middleware, passing its callback so it can receive radio events.
The Onboarding Sequence
sequenceDiagram
participant Cloud
participant Provisioner
participant MsgQueue
participant ZigbeePlugin
participant Radio
Cloud->>Provisioner: MQTT: Add install code
Provisioner->>MsgQueue: ADD_ONE_AUTH_MATERIAL
MsgQueue->>ZigbeePlugin: AddAuthMaterial(mac, code)
ZigbeePlugin->>Radio: Write install code to radio
Radio-->>ZigbeePlugin: DEVICE_ADDED event
ZigbeePlugin-->>Provisioner: Callback(DEVICE_ADDED)
Provisioner-->>Cloud: MQTT: Device onboarded
Here's what happens at each step:
- The cloud sends an install code (MAC address + Zigbee install code) via MQTT (Chapter 5)
- The Provisioner creates an
ADD_ONE_AUTH_MATERIALmessage withIOTMI_RADIO_INTERFACE_ZIGBEE - The message queue dispatches to the Zigbee plugin's
AddAuthMaterial() - The plugin writes the install code to the Zigbee radio via the middleware API
- When a device with that MAC joins, the radio fires
DEVICE_ADDED - The callback propagates up to the Provisioner, which reports success to the cloud
10.8 C++ vs C: Two Flavors, Same Pattern
The SDK provides both C++ and pure C implementations of the Provisioner core. Here's how they compare.
C++ Version: Virtual Classes
// Abstract interface — virtual methods
class IotshdLPWProvisionerRadioInterface {
virtual iotmi_statusCode_t Init() = 0;
virtual iotmi_statusCode_t AddAuthMaterial(
IotshAuthMaterial_t auth) = 0;
};
Plugins inherit and override:
class ZigbeeRadioInterface
: public IotshdLPWProvisionerRadioInterface {
iotmi_statusCode_t Init() override;
};
C Version: Function-Pointer Vtable
iotmi_provisioner_radio_interface.h defines a struct of function pointers:
typedef struct IotmiRadioPluginOps {
iotmi_statusCode_t (*init)(void* context);
iotmi_statusCode_t (*add_auth_material)(
void* context, const IotmiAuthMaterial_t* auth);
iotmi_statusCode_t (*remove_node)(
void* context, int node_id);
void (*destroy)(void* context);
// ... same operations as C++
} IotmiRadioPluginOps_t;
Plugins register their function table:
Side-by-Side Comparison
| Aspect | C++ | C |
|---|---|---|
| Polymorphism | virtual + inheritance |
Function-pointer struct |
| Plugin binding | Subclass the interface | Fill in IotmiRadioPluginOps_t |
| Callback | std::function<void(int, void*)> |
void (*)(int, void*, void*) |
| Memory | RAII / destructors | Manual create/destroy |
| Namespace | IotSmartHomeDevice::LPWProvisionerCore |
iotmi_provisioner_* prefix |
The C version adds a void* user_data parameter to callbacks — since C lacks closures, this is how context gets passed through.
10.9 Other Plugins at a Glance
Z-Wave Plugin
📁
plugins/IoTSmartHomeDevice-Zwave-Provisioner-Plugin/
Uses a 47-character DSK (Device Specific Key) for authentication. Supports SmartStart (zero-touch) and NWI/NWE modes. Includes a TimerManager for handling network mode timeouts.
WiFi Plugin
📁
plugins/IoTSmartHomeDevice-WiFi-Provisioner-Plugin/
The most complex plugin — manages SoftAP, TLS, SOCKS5 proxy, and credential exchange. Uses serial number + UPC + expiration timestamp for auth. Has dedicated sub-managers under managers/ for each concern.
Each plugin follows the same RadioInterface contract, so the Provisioner treats them identically.
10.10 How It All Connects
Let's zoom out and see where the Provisioner fits in the full SDK architecture:
- Cloud sends a provisioning command via MQTT
- MQTT Proxy (Chapter 5) receives the message on the hub
- IPC (Chapter 3) routes it to the Provisioner process
- Provisioner parses the command, creates a message on its internal queue
- Message Queue dispatches to the correct radio plugin
- Plugin talks to the radio hardware via middleware
- Radio event fires when device joins → callback chain propagates up
- Provisioner notifies the cloud via MQTT that onboarding succeeded
- Hub Onboarding (Chapter 6) completes the device registration
Tutorial Conclusion: The Full Picture
Congratulations — you've walked through the entire IoTMI Hub SDK, from the outermost cloud connection down to the radio chip. Let's recap the complete data flow:
☁️ AWS IoT Core
↕ MQTT (Ch 5)
🔌 MQTT Proxy — TLS connection to cloud
↕ IPC (Ch 3)
🧠 Hub Core — command routing, state management
↕ Internal message queue
📡 LPW Provisioner — onboarding coordinator (Ch 10)
↕ RadioInterface (Strategy pattern)
📻 Protocol Plugins — Zigbee, Z-Wave, WiFi, Matter
↕ Middleware APIs
⚡ Radio Hardware — the actual wireless chips
Key Design Patterns We Encountered
| Pattern | Where | Why |
|---|---|---|
| Strategy/Plugin | RadioInterface | Support multiple protocols with one interface |
| Observer/Callback | Radio events | Async notification from hardware to software |
| Message Queue | Provisioner internals | Decouple cloud commands from radio timing |
| Vtable (C) | IotmiRadioPluginOps_t |
Polymorphism without C++ |
| Proxy | MQTT Proxy | Bridge cloud ↔ local communication |
| Agent | Device Agent (Ch 9) | Represent each device as an autonomous entity |
What You've Learned
- Chapters 1–2: Project structure and build system
- Chapter 3: IPC — how processes talk to each other on the hub
- Chapter 4: Configuration and capability modeling
- Chapter 5: MQTT Proxy — the cloud connection
- Chapter 6: Hub onboarding — registering the hub itself
- Chapters 7–8: Device modeling and capability mapping
- Chapter 9: Device Agent — per-device lifecycle management
- Chapter 10: LPW Provisioner — bringing devices onto the network
The SDK is designed so that adding a new radio protocol means implementing one interface (RadioInterface) and registering it as a plugin. Everything else — cloud communication, message routing, event handling — is already wired up.
Happy building! 🚀