Skip to main content

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.

Advanced feature

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:

OptionBridge defaultWhy
CONFIG_RMAKER_WORK_QUEUE_TASK_QUEUE_SIZE256 instead of 64Set it to at least twice your child count — every child operation goes through the work queue
CONFIG_ESP_RMAKER_MQTT_ENABLE_BUDGETINGn instead of yA 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_t you 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 */);
ArgumentRules
child_suffixThe suffix of the child's thing name, after the <parent>-- prefix. Must match [A-Za-z0-9_]{1,32}no hyphens.
bridge_local_idYour 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:

EventPayload
RMAKER_EVENT_BRIDGE_CHILD_ADDEDesp_rmaker_event_bridge_child_added_t * — child handle, cloud-assigned thing name, your bridge_local_id
RMAKER_EVENT_BRIDGE_CHILD_ADD_FAILEDesp_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:

SituationCall
The child is still paired but temporarily unreachable — out of range, battery asleepesp_rmaker_bridge_child_mark_online(child, false)
The child has been unpaired and should no longer exist in the cloudesp_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

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

EventPayload
RMAKER_EVENT_BRIDGE_CHILD_GROUP_INFO_UPDATEDesp_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.