Live updates
Live updates are how an app learns that a device changed without polling. Each node subscribes itself when it is created, applies incoming updates to its own parameters, and re-broadcasts them so the app can react. ESPRMNeoSubscriptionManager decides which channel serves each node.
Most apps need only one call:
import { ESPRMNeoEventType } from "@espressif/rainmaker-neo-base-sdk";
await user.connectMQTT();
user.subscribe(ESPRMNeoEventType.nodeUpdates, (update) => {
console.log(update.nodeId, update.source, update.payload);
});
Everything below is what happens underneath, and how to change it.
Connect and disconnect
await user.connectMQTT();
await user.disconnectMQTT();
connectMQTT() returns Promise<boolean>. It disconnects any existing session first, runs the credential chain, derives a client ID of the form user:<email-or-phone>:<session>, and connects. The client ID format is not cosmetic; the IoT policy scopes connection rights to that prefix, so it cannot be chosen freely.
disconnectMQTT() returns Promise<void> and is a no-op when MQTT was never initialised or is already disconnected. It throws only if the underlying disconnect fails.
logout() disconnects for you and resets the MQTT session state internally, so an explicit call is needed only when you want to drop the connection while staying signed in; backgrounding the app, for instance. Reconnect with connectMQTT(); node subscriptions re-establish through the manager.
Retry the connect
connectMQTT() has no internal retry. Because it runs the credential chain first, a transient failure anywhere along it; a 500 from the role-exchange endpoint, a dropped request on a flaky network; rejects the whole call. Nothing retries on your behalf, so a single blip at launch leaves the app with no live updates for the rest of the session.
Wrap it with bounded retries and back-off:
async function connectWithRetries(user, attempts = 3) {
let lastError;
for (let attempt = 1; attempt <= attempts; attempt++) {
try {
return await user.connectMQTT();
} catch (error) {
lastError = error;
if (attempt < attempts) {
await new Promise((r) => setTimeout(r, 1500 * attempt));
}
}
}
throw lastError;
}
Cache the resulting promise per user so two call sites cannot open two connections, and drop the cache entry if every attempt failed; otherwise a permanently-rejected promise is what every later caller gets.
What happens automatically
Configuring an MQTT adaptor registers the SDK's MQTT subscription channel and makes it the default channel order. Constructing a node then registers it with the MQTT orchestrator, subscribes it through the manager, and issues a warm read so the node's current parameter values arrive right after subscribing.
So by the time you have a node and a connected MQTT session, its parameters track the device. Each update the node receives is applied to its devices, services and config, written to the local cache, and pushed onto a process-wide bus that the nodeUpdates event reads.
If the cloud reports that the node's configuration version changed, the node re-syncs itself, which is how a firmware update that adds a parameter appears without app intervention. Note that re-syncing rebuilds the device and parameter instances; see Node configuration.
What an update looks like
ESPNodeUpdateData:
| Field | Contents |
|---|---|
nodeId | Node the update is for |
source | Channel that delivered it, "mqtt" today |
eventType | Always the params-changed event |
payload | { deviceName: { paramId: value } } |
metadata | Channel-specific, carries the shadow for MQTT |
The exported NODE_PARAMS_CHANGED_EVENT constant is the eventType value, for code that switches on it.
Updates are partial. A payload carries only what changed, so merge rather than replace when you keep your own copy of state.
Channels
A channel is one delivery mechanism. The manager holds registered channels, an order, and picks per node: it walks the effective order, keeps the channels that both exist and report supportsNode(node), and tries each until one subscribes successfully.
Reach the manager through the base class; never construct one:
const manager = ESPRMNeoBase.subscriptionManager;
console.log(manager.getRegisteredChannels());
console.log(manager.getGlobalChannelOrder());
| Method | Purpose |
|---|---|
registerChannel(channel, autoInitialize?) | Add a channel |
unregisterChannel(channelId) | Dispose and remove one |
setGlobalChannelOrder(channelIds) | Set the default order |
getGlobalChannelOrder() | Read the default order |
getRegisteredChannels() | List channel IDs |
getEffectiveChannelOrder(node) | Order that applies to a node |
getAvailableChannelsForNode(node) | Channels that would be tried |
subscribeToNode(node, callback) | Subscribe one node |
subscribeToAllNodes(nodes, callback) | Subscribe many |
unsubscribeFromNode(nodeId, callback?) | Detach one or all subscribers |
dispose() | Tear everything down |
registerChannel() throws when a channel with that ID is already registered. setGlobalChannelOrder() does not throw on unknown IDs; it warns, because an order is often set before the channel it names has registered.
subscribeToAllNodes() does not reject when individual nodes fail; it logs the failures and resolves. Check the nodes themselves if you need to know.
Per-node channel order
A node can override the global order:
node.setSubscriptionChannelOrder(["my-channel", "mqtt"]);
node.getSubscriptionChannelOrder();
node.clearSubscriptionChannelOrder();
getSubscriptionChannelOrder() returns the effective order; the node's own if set, otherwise the global one. An empty array is treated as unset and falls back to the global order rather than leaving the node with nothing.
Unsubscribing
await ESPRMNeoBase.subscriptionManager.unsubscribeFromNode(nodeId, myCallback);
await ESPRMNeoBase.subscriptionManager.unsubscribeFromNode(nodeId);
Passing a callback detaches just that subscriber and leaves the node's others receiving updates. Omitting it removes every subscriber and clears the node's MQTT shadow binding, so the next subscribe re-registers against the node's current shadow, which is what you want after group membership changed the node's shadow topic.
For a node you are discarding, node.unsubscribeFromMqttUpdates() is the simpler call.
Nothing unsubscribes a node when it is garbage-collected. Orchestrator registrations and MQTT topic subscriptions stay behind, so a screen that repeatedly fetches and drops nodes leaks subscriptions. Call node.unsubscribeFromMqttUpdates() on teardown.
Diagnosing "no available channels"
subscribeToNode() throws when nothing can serve the node, and the message names the effective order, the registered channels, which IDs in the order are unregistered, and which are registered but do not support this node. Read it before guessing; it distinguishes a typo in the order from a missing adaptor.
The usual cause is no MQTT adaptor: without one no channel is registered, so the order is empty.
Related
- Events — the
nodeUpdatesevent this feeds - Device control — confirming a write from an update
- Adaptors — the MQTT adaptor this needs
- Node configuration — what a version change re-syncs
- MQTT user reference — the topics behind the MQTT channel