Skip to content

Chapter 7: Common Data Model Bridge (CDMB)

In Chapter 4 we saw how the MW Services layer manages the hub's middleware concerns. Now we step into the control plane — the part of the SDK that actually talks to smart-home devices. The first stop is the Common Data Model Bridge, or CDMB.

What Does the CDMB Do?

Think of the CDMB as a mail sorting office. The cloud-side Agent sends a JSON command — "turn on the Z-Wave light in the living room" — and the CDMB's job is to:

  1. Open the envelope (parse the JSON).
  2. Read the address label (inspect the eNId prefix).
  3. Drop the letter into the right outgoing mailbag (route to the correct protocol plugin).

It never speaks Z-Wave or Zigbee itself; it only knows who speaks those languages and hands the work off.

Source orientation: All CDMB code lives under control/IoTSmartHomeDevice-CDMBCore/. Headers are in include/, the entry point is src/iotshd_cdmb_main.c, and the core logic is in src/core/iotshd_cdmb_svc.c.


The Routing Key: eNId Prefixes

Every managed device has an endpoint Node ID (eNId) that starts with a short protocol prefix. The CDMB uses a simple strncmp against the first three characters to decide where a command goes:

Prefix Protocol Compile Flag
zw. Z-Wave ZWAVE_SUPPORT_ENABLED
zb. Zigbee ZIGBEE_SUPPORT_ENABLED
mt. Matter MATTER_SUPPORT_ENABLED (reserved)

The lookup happens in iotmiCdmb_retrieveDeviceProtocol():

// src/core/iotshd_cdmb_svc.c
if (!strncmp(node_id->valuestring, "zw.", 3)) {
    protocol = PROTOCOL_ZWAVE;
} else if (!strncmp(node_id->valuestring, "zb.", 3)) {
    protocol = PROTOCOL_ZIGBEE;
} else if (!strncmp(node_id->valuestring, "mt.", 3)) {
    protocol = PROTOCOL_MATTER;
}

There is also a custom plugin path: if the payload contains a customProtocolClientName field instead of a standard prefix, the CDMB routes to PROTOCOL_CUSTOM. We will cover custom plugins in detail in Chapter 8.


JSON Message Format

Every request the Agent sends over IPC follows a standard envelope. The CDMB parses six required header fields before it ever looks at the payload:

{
  "mTp": "controlCommand",
  "ve":  "1.0.0",
  "t":   "1713100000.000",
  "mId": "msg-abc-123",
  "tId": "managed-thing-42",
  "cId": "client-007",
  "p":   { "eNId": "zw.5", "...": "..." }
}

These map to the iotmiCdmb_requestHeaderItem_t enum defined in include/iotshd_cdmb_common_types.h:

typedef enum {
  REQ_MESSAGETYPE = 0,  // "mTp"
  REQ_VERSION,          // "ve"
  REQ_TIMESTAMP,        // "t"
  REQ_MESSAGEID,        // "mId"
  REQ_MANAGEDTHINGID,   // "tId"
  REQ_CLIENTID          // "cId"
} iotmiCdmb_requestHeaderItem_t;

The header parser (iotmiCdmb_requestHeaderParser) rejects the request immediately if any of these six fields is missing — fail fast, no guessing.


How a Command Flows Through the CDMB

Here is the end-to-end journey of a single control command:

sequenceDiagram
    participant Agent
    participant IPC as IPC Framework
    participant CDMB
    participant Router as Protocol Router
    participant Plugin as Z-Wave Plugin

    Agent->>IPC: JSON command (async)
    IPC->>CDMB: iotmiCdmb_requestHandler()
    CDMB->>Router: parse eNId → "zw."
    Router->>Plugin: IotmiCdmbZWave_TaskHandler(task)
    Plugin-->>CDMB: response payload

Let's walk through each step in the code.

Step 1 — IPC Delivers the Request

The IPC framework calls iotmiCdmb_requestHandler() with command type IOTMI_IPC_SVC_SEND_REQ_TO_CDMB. This is the single entry point for all Agent-to-CDMB traffic (see Chapter 3 for how IPC dispatches commands).

// src/core/iotshd_cdmb_svc.c
iotmi_statusCode_t iotmiCdmb_requestHandler(
    IotmiIpc_Command cmd, uint32_t buf_len,
    const uint8_t *in_buf,
    uint32_t *ret_buf_len, uint8_t **ret_buf);

Step 2 — Parse and Create a Task

The handler calls iotmiCdmb_requestHeaderParser(), which extracts the six header fields and detaches the "p" (payload) object into a freshly allocated iotmiCdmb_task_t:

// include/iotshd_cdmb_common_types.h
typedef struct iotmiCdmb_task {
  char* reqHeaderItems[IOTMI_CDMB_REQ_HEADER_ITEM_NUM];
  cJSON* reqPayload;
  cJSON* rspPayload;
} iotmiCdmb_task_t;

The task struct is the CDMB's unit of work — it carries the request in and the response out.

Step 3 — Route by Message Type

Before protocol routing, the handler checks the messageType header. Discovery requests (D2C_HUB_START_LOCAL_DISCOVERY) go to iotmiCdmb_handleDiscoveryTask(); everything else goes to iotmiCdmb_handleTask():

if (strncmp(msg_type, "D2C_HUB_START_LOCAL_DISCOVERY",
            strlen("D2C_HUB_START_LOCAL_DISCOVERY")) == 0) {
    status = iotmiCdmb_handleDiscoveryTask(task);
} else {
    status = iotmiCdmb_handleTask(task);
}

Step 4 — Route to the Plugin

Inside iotmiCdmb_handleTask(), the protocol enum drives a switch statement. Each case is guarded by both a compile-time #ifdef and a runtime flag:

case PROTOCOL_ZWAVE:
  if (zwave_support_runtime_enabled) {
      status = IotmiCdmbZWave_TaskHandler(task);
  }
  break;

This two-layer guard means you can build the SDK with Z-Wave support but still disable it at launch via a command-line argument (disable-zwave).

Step 5 — Build and Return the Response

After the plugin populates task->rspPayload, the CDMB wraps it back into the standard envelope with iotmiCdmb_buildTaskResponse() and hands the buffer back to IPC.


Plugin Loading: Compile-Time Linking

Unlike a dynamic plugin system, the CDMB links protocol plugins as static libraries selected at build time. The CMakeLists.txt uses CMake flags to conditionally include each one:

# src/CMakeLists.txt (simplified)
if(${EN_ZWAVE_CONTROL})
  target_link_libraries(${PROJECT_NAME} PUBLIC
    iotmi_cdmb_zwave)
endif()

The three flags — EN_ZWAVE_CONTROL, EN_ZIGBEE_CONTROL, EN_MATTER_CONTROL — each pull in a corresponding static library (iotmi_cdmb_zwave, iotmi_cdmb_zigbee_plugin, iotmi_cdmb_matter). No dlopen, no plugin directories — just linker-level composition.


Service Lifecycle

The CDMB process starts in iotshd_cdmb_main.c and follows a clear five-step lifecycle:

  1. Daemonize (optional -B flag).
  2. Configure IPC URLs — async, sync, and publish sockets.
  3. Register iotmiCdmbSvc_start as the IPC feature callback.
  4. Start IPC — this triggers iotmiCdmbSvc_start(), which registers the command handler and initializes every enabled plugin.
  5. ShutdowniotmiCdmbSvc_stop() tears down plugins and deregisters handlers.

The service succeeds only if at least one plugin initializes. If every plugin fails, the whole CDMB exits with an error — there is no point running a bridge with nothing on the other side.


Event Publishing

Plugins don't just respond to commands — they also push unsolicited events (e.g., a sensor reporting a temperature change). The CDMB provides iotmiCdmb_publishEvent() for this:

// include/iotshd_cdmb_svc.h
void iotmiCdmb_publishEvent(
    IotmiIpc_EventId event_id,
    const uint8_t *data, uint32_t data_len);

Each event gets a monotonically increasing sequence number (protected by a mutex) so the Agent can detect gaps. The event data is wrapped in an iotmiCdmb_eventData_t struct that pairs the sequence with the payload string, then published over the IPC pub socket as IOTMI_IPC_EVENT_CDMB_REPORT.


Custom Plugin Path

For protocols beyond Z-Wave/Zigbee/Matter, the CDMB supports a custom plugin channel. Instead of matching an eNId prefix, it looks for a customProtocolClientName field in the payload. If found, the task is serialized and forwarded over a dedicated IPC pub socket to the external plugin process.

Custom plugins connect to the CDMB via a socket assignment handshake (IOTMI_IPC_SOCKET_INDEX_REQUEST) and maintain liveness with heartbeats (IOTMI_IPC_SOCKET_HEARTBEAT_REQUEST). We will explore this in Chapter 8.


Key Takeaways

Concept Detail
Routing 3-char eNId prefix → protocol enum → switch dispatch
Message format 6 required header fields + "p" payload
Plugin binding Static libraries, selected by CMake flags
Runtime control Plugins can be disabled via CLI args at launch
Events Sequence-numbered, published over IPC pub socket
Thread safety Mutexes guard service init and event sequence

Conclusion

The CDMB is the SDK's traffic controller — it never touches a radio or a wire, but every device command and event passes through it. Its design is deliberately thin: parse, route, respond. The real protocol work happens one layer deeper, inside the plugins.

In Chapter 8: Protocol Plugins, we will open up those plugin libraries and see how Z-Wave frames, Zigbee clusters, and custom protocols actually get translated into the common data model.