Bridge nodes
A bridge node is an ordinary RainMaker Neo node that additionally proxies cloud connectivity for a set of child devices that have no MQTT connection of their own — Zigbee bulbs, BLE-mesh sensors, a proprietary RF network.
Each child appears in the cloud as its own Thing with its own node configuration and its own shadow, so from an app's point of view they are normal devices. Under the hood, cloud-side IoT Rules rewrite all child traffic onto a bridge-owned namespace and the bridge multiplexes it over one MQTT connection.
Most products do not need this. If your device talks to the cloud directly, you want Data model, not this page.
Enabling it
CONFIG_RMNG_BRIDGE_ENABLED=y
CONFIG_RMNG_BRIDGE_MAX_CHILDREN=128 # range 1–400
CONFIG_RMNG_BRIDGE_MAX_CHILDREN sizes a static slot pool — the memory is reserved up front, and esp_rmaker_bridge_add_child() fails once the pool is exhausted. Size it for your product, not optimistically.
Two related settings shift when the bridge is enabled, and you should leave them shifted:
| Option | Bridge default | Why |
|---|---|---|
CONFIG_RMAKER_WORK_QUEUE_TASK_QUEUE_SIZE | 256 instead of 64 | Set it to at least twice your child count — every child operation goes through the work queue |
CONFIG_ESP_RMAKER_MQTT_ENABLE_BUDGETING | n instead of y | A bridge with any real number of children would exhaust the budget immediately |
What the SDK does and does not do
The SDK is deliberately agnostic about how you bridge. It knows nothing about Zigbee, BLE-mesh, or whatever RF protocol you use — that is your protocol stack's job.
The SDK provides:
- Child Thing lifecycle on the cloud — add and remove
- Reachability marking
- A per-child
esp_rmaker_node_tyou populate with the standard node APIs - Per-child node-configuration reporting
- Group-info propagation for children
You provide:
- The bridge-side protocol: discovery, pairing, transport
- The mapping from bridge-protocol devices to virtual devices under the child node
- Reconciliation when the cloud and your local view disagree
Adding a child
Everything is asynchronous and event-driven. esp_rmaker_bridge_add_child() always does a cloud round trip:
esp_rmaker_bridge_add_child("bulb01", /* child_suffix */
"0x00124B0012345678" /* bridge_local_id */);
| Argument | Rules |
|---|---|
child_suffix | The suffix of the child's thing name, after the <parent>-- prefix. Must match [A-Za-z0-9_]{1,32} — no hyphens. |
bridge_local_id | Your protocol's identifier for the child, e.g. a Zigbee EUI-64. This is the idempotency key. |
The cloud is authoritative for idempotency: re-issuing the call with a bridge_local_id it has seen before returns the same child Thing name instead of creating a duplicate. That is what makes bridge reboots safe — replay your child list on every boot and let the cloud deduplicate.
The outcome arrives as an event:
| Event | Payload |
|---|---|
RMAKER_EVENT_BRIDGE_CHILD_ADDED | esp_rmaker_event_bridge_child_added_t * — child handle, cloud-assigned thing name, your bridge_local_id |
RMAKER_EVENT_BRIDGE_CHILD_ADD_FAILED | esp_rmaker_event_bridge_child_failed_t * — requested suffix, bridge_local_id, and an error string (or "timeout") |
Populating a child
Each child owns its own node handle. Once you have the ADDED event, populate it exactly as you would the self node, then commit:
case RMAKER_EVENT_BRIDGE_CHILD_ADDED: {
esp_rmaker_event_bridge_child_added_t *ev = data;
esp_rmaker_node_t *child_node = esp_rmaker_bridge_child_node(ev->child);
esp_rmaker_node_info_t info = {
.name = "Kitchen Bulb", .type = "lightbulb",
.fw_version = "1.0.0", .model = "acme-zb-bulb",
};
esp_rmaker_node_fill_with_info(child_node, &info);
esp_rmaker_device_t *dev = esp_rmaker_lightbulb_device_create("Light", NULL, false);
esp_rmaker_node_add_device(child_node, dev);
esp_rmaker_bridge_child_commit_devices(ev->child);
break;
}
esp_rmaker_bridge_child_commit_devices() builds the child's node-config slice, hashes it, and publishes setNodeConfig on the child's topic only if the checksum changed — so committing on every boot is cheap. esp_rmaker_report_node_config_for_child() does the same thing if you want to report without a device change.
Inbound cloud writes addressed to the child are dispatched to those virtual devices by the SDK's normal parameter-write path. There is no per-child callback — register write callbacks on the virtual devices as usual.
For outbound reporting, mark the child's parameters with esp_rmaker_state_mark_for_update() just as you would for the self node.
Reachability vs removal
Two different situations, and confusing them is the most common bridge bug:
| Situation | Call |
|---|---|
| The child is still paired but temporarily unreachable — out of range, battery asleep | esp_rmaker_bridge_child_mark_online(child, false) |
| The child has been unpaired and should no longer exist in the cloud | esp_rmaker_bridge_remove_child(child) |
mark_online queues an iparams shadow update ({state:{reported:{online: <bool>}}}) so apps show the child as offline. Removal is permanent.
Removal is optimistic
esp_rmaker_bridge_remove_child() posts RMAKER_EVENT_BRIDGE_CHILD_REMOVED as soon as the removeChild publish succeeds — before the cloud's bridgeAck arrives. If the cloud subsequently fails the removal, you get RMAKER_EVENT_BRIDGE_CHILD_REMOVE_FAILED as well.
So a failed removal shows up as both events: REMOVED (local teardown already happened) and REMOVE_FAILED (the cloud disagrees). Reconciling is your job — typically by re-issuing add_child with the same bridge_local_id if the child must stay registered.
The child handle is invalid after REMOVED fires. Do not use it.
Introspection
| API | Returns |
|---|---|
esp_rmaker_bridge_child_thing_name(child) | Cloud-assigned thing name; valid until the child is removed |
esp_rmaker_bridge_child_bridge_local_id(child) | The bridge_local_id the child was registered with |
esp_rmaker_bridge_child_node(child) | The child's node handle |
All strings are owned by the SDK — do not free them. Handle pointers are reused when a slot is reallocated, so never cache a handle past its REMOVED event.
Group membership
Children can belong to groups independently of the bridge. When the cloud updates a child's group or subgroup, the SDK posts:
| Event | Payload |
|---|---|
RMAKER_EVENT_BRIDGE_CHILD_GROUP_INFO_UPDATED | esp_rmaker_event_bridge_child_group_info_t * — child handle and the new group-info string (<primary>[-<sg>-...]) |
The group-info string is part of the child's MQTT topics, so the SDK handles the retopic itself. You only need this event if your application cares about group membership.
Design notes
Everything is asynchronous. No bridge call blocks on the cloud. Drive your bridge from the event loop; don't try to serialise it.
Replay on boot. Re-issue add_child for every child you know about after RMAKER_EVENT_CORE_STARTED. Idempotency on bridge_local_id makes this safe and is the intended pattern.
One MQTT connection is the point, and the constraint. All children share the bridge's connection, so per-connection AWS IoT limits (100 publishes per second, 50 subscriptions) apply to the whole family. See Cloud specifications → limits.
Related
- Callbacks and events — the event loop the bridge is driven from
- Data model — populating a child node
- Cloud specifications → Node registration — the
bridgecapability on a registered node - CLI batch flow — registering a node with the
bridgecapability