Skip to content

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++:

using Callback = std::function<void(int event, void *payload)>;

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:

typedef struct {
  IotmiMessageType_t type;
  char* payload;
  size_t payload_size;
} IotmiMessage_t;

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:

  1. Provisioner starts → sends IOTMI_MSG_TYPE_START_RADIO_ZIGBEE
  2. Plugin Init() called → initializes middleware and radio hardware
  3. Status → READY → plugin can accept commands
  4. Cloud sends auth material → queued as ADD_ONE_AUTH_MATERIAL
  5. Network mode set → plugin opens the radio for joining
  6. Device joins → plugin fires DEVICE_ADDED callback

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:

  1. The cloud sends an install code (MAC address + Zigbee install code) via MQTT (Chapter 5)
  2. The Provisioner creates an ADD_ONE_AUTH_MATERIAL message with IOTMI_RADIO_INTERFACE_ZIGBEE
  3. The message queue dispatches to the Zigbee plugin's AddAuthMaterial()
  4. The plugin writes the install code to the Zigbee radio via the middleware API
  5. When a device with that MAC joins, the radio fires DEVICE_ADDED
  6. 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:

iotmi_provisioner_radio_interface_register_plugin(
    radio_interface, &my_ops, my_context);

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:

  1. Cloud sends a provisioning command via MQTT
  2. MQTT Proxy (Chapter 5) receives the message on the hub
  3. IPC (Chapter 3) routes it to the Provisioner process
  4. Provisioner parses the command, creates a message on its internal queue
  5. Message Queue dispatches to the correct radio plugin
  6. Plugin talks to the radio hardware via middleware
  7. Radio event fires when device joins → callback chain propagates up
  8. Provisioner notifies the cloud via MQTT that onboarding succeeded
  9. 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! 🚀