@espressif/rainmaker-neo-base-sdk 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/cjs/ESPRMNeoBase.js +1 -1
  2. package/dist/cjs/index.js +11 -0
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/proto/rmaker_local_ctrl.js +273 -0
  5. package/dist/cjs/proto/rmaker_local_ctrl.js.map +1 -0
  6. package/dist/cjs/services/ESPTransport/ESPDiscovery/ESPDiscoveryManager.js +5 -4
  7. package/dist/cjs/services/ESPTransport/ESPDiscovery/ESPDiscoveryManager.js.map +1 -1
  8. package/dist/cjs/services/ESPTransport/ESPLocalControlTransport.js +183 -116
  9. package/dist/cjs/services/ESPTransport/ESPLocalControlTransport.js.map +1 -1
  10. package/dist/cjs/services/ESPTransport/LocalControlSession.js +62 -0
  11. package/dist/cjs/services/ESPTransport/LocalControlSession.js.map +1 -0
  12. package/dist/cjs/types/transport.js +24 -0
  13. package/dist/cjs/types/transport.js.map +1 -1
  14. package/dist/cjs/utils/constants.js +70 -4
  15. package/dist/cjs/utils/constants.js.map +1 -1
  16. package/dist/cjs/utils/eventSubscriptionUtils.js +48 -4
  17. package/dist/cjs/utils/eventSubscriptionUtils.js.map +1 -1
  18. package/dist/esm/ESPRMNeoBase.js +1 -1
  19. package/dist/esm/index.js +2 -2
  20. package/dist/esm/proto/rmaker_local_ctrl.js +271 -0
  21. package/dist/esm/proto/rmaker_local_ctrl.js.map +1 -0
  22. package/dist/esm/services/ESPTransport/ESPDiscovery/ESPDiscoveryManager.js +5 -4
  23. package/dist/esm/services/ESPTransport/ESPDiscovery/ESPDiscoveryManager.js.map +1 -1
  24. package/dist/esm/services/ESPTransport/ESPLocalControlTransport.js +184 -117
  25. package/dist/esm/services/ESPTransport/ESPLocalControlTransport.js.map +1 -1
  26. package/dist/esm/services/ESPTransport/LocalControlSession.js +59 -0
  27. package/dist/esm/services/ESPTransport/LocalControlSession.js.map +1 -0
  28. package/dist/esm/types/transport.js +24 -1
  29. package/dist/esm/types/transport.js.map +1 -1
  30. package/dist/esm/utils/constants.js +65 -5
  31. package/dist/esm/utils/constants.js.map +1 -1
  32. package/dist/esm/utils/eventSubscriptionUtils.js +49 -5
  33. package/dist/esm/utils/eventSubscriptionUtils.js.map +1 -1
  34. package/dist/types/ESPRMNeoBase.d.ts +1 -1
  35. package/dist/types/proto/rmaker_local_ctrl.d.ts +137 -0
  36. package/dist/types/services/ESPTransport/ESPDiscovery/ESPDiscoveryManager.d.ts +4 -3
  37. package/dist/types/services/ESPTransport/ESPLocalControlTransport.d.ts +70 -23
  38. package/dist/types/services/ESPTransport/LocalControlSession.d.ts +24 -0
  39. package/dist/types/types/discovery.d.ts +1 -1
  40. package/dist/types/types/localControl.d.ts +26 -1
  41. package/dist/types/types/transport.d.ts +24 -2
  42. package/dist/types/utils/baseUtils.d.ts +1 -1
  43. package/dist/types/utils/constants.d.ts +65 -5
  44. package/dist/types/utils/eventSubscriptionUtils.d.ts +19 -3
  45. package/package.json +1 -1
  46. package/dist/cjs/proto/constants.js +0 -24
  47. package/dist/cjs/proto/constants.js.map +0 -1
  48. package/dist/cjs/proto/esp_local_ctrl.js +0 -787
  49. package/dist/cjs/proto/esp_local_ctrl.js.map +0 -1
  50. package/dist/esm/proto/constants.js +0 -24
  51. package/dist/esm/proto/constants.js.map +0 -1
  52. package/dist/esm/proto/esp_local_ctrl.js +0 -758
  53. package/dist/esm/proto/esp_local_ctrl.js.map +0 -1
  54. package/dist/types/proto/constants.d.ts +0 -15
  55. package/dist/types/proto/esp_local_ctrl.d.ts +0 -283
@@ -1,156 +1,223 @@
1
1
  'use strict';
2
2
 
3
- var ESPRMNeoBase = require('../../ESPRMNeoBase.js');
4
- var constants$1 = require('../../proto/constants.js');
5
- var esp_local_ctrl = require('../../proto/esp_local_ctrl.js');
3
+ var rmaker_local_ctrl = require('../../proto/rmaker_local_ctrl.js');
4
+ var transport = require('../../types/transport.js');
6
5
  var constants = require('../../utils/constants.js');
7
6
  var TransformEncoding = require('../ESPRMNeoHelpers/TransformEncoding.js');
7
+ var LocalControlSession = require('./LocalControlSession.js');
8
8
 
9
9
  /*
10
10
  * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
11
11
  *
12
12
  * SPDX-License-Identifier: Apache-2.0
13
13
  */
14
- /** Property index of the writable "params" property exposed by RMNeo local-control firmware. */
15
- const LOCAL_CONTROL_PARAMS_INDEX = 1;
16
14
  /**
17
- * Built-in `local` transport. Communicates with the node over the LAN through
18
- * the app-supplied {@link ESPRMNeoBase.ESPLocalControlAdapter} using the
19
- * `esp_local_ctrl` protobuf protocol. Connection metadata (`baseUrl`,
20
- * `securityType`, `pop`, and `username` for sec2) is supplied via the transport
21
- * config by {@link delegatedTransportHandler}.
15
+ * Tail of the in-flight fragmented-read chain, per node id.
16
+ *
17
+ * Module-level on purpose: {@link delegatedTransportHandler} builds a fresh
18
+ * transport for every call, so a queue held on the instance would serialize
19
+ * nothing. Keyed by node, because the transfer cache is per device.
20
+ */
21
+ const nodeReadQueues = new Map();
22
+ /**
23
+ * Runs `read` after every fragmented read already queued for `nodeId`.
24
+ *
25
+ * A failed read must not wedge the queue, so the chain advances on rejection
26
+ * too, while the caller still sees the original rejection. The map entry is
27
+ * dropped once the chain drains, so a long-lived app doesn't retain one promise
28
+ * per node it has ever talked to.
29
+ *
30
+ * @param nodeId - Node whose reads are being serialized.
31
+ * @param read - Pull loop to run once the node is free.
32
+ */
33
+ function enqueueNodeRead(nodeId, read) {
34
+ const previous = nodeReadQueues.get(nodeId) ?? Promise.resolve();
35
+ const result = previous.then(read, read);
36
+ const tail = result.then(() => undefined, () => undefined);
37
+ nodeReadQueues.set(nodeId, tail);
38
+ void tail.then(() => {
39
+ // Only the current tail clears the entry; otherwise a newer read owns it.
40
+ if (nodeReadQueues.get(nodeId) === tail) {
41
+ nodeReadQueues.delete(nodeId);
42
+ }
43
+ });
44
+ return result;
45
+ }
46
+ /** Session endpoints of the `rmaker_local_ctrl` protocol, passed to the adapter. */
47
+ const RMAKER_SESSION_OPTIONS = {
48
+ protocol: transport.ESPLocalControlProtocol.rmakerLocalCtrl,
49
+ sessionPath: constants.RMakerLocalCtrlEndpoint.SESSION,
50
+ versionPath: constants.RMakerLocalCtrlEndpoint.VERSION,
51
+ versionKey: constants.RMAKER_LOCAL_CTRL_VERSION_KEY,
52
+ };
53
+ /**
54
+ * The built-in `local` transport, speaking the `rmaker_local_ctrl` endpoint
55
+ * protocol over the app-supplied {@link ESPRMNeoBase.ESPLocalControlAdapter}:
56
+ *
57
+ * - `set_params` carries the same raw JSON body as a cloud set-params call and
58
+ * answers `{"status":"success"}` / `{"status":"fail","description":…}`.
59
+ * - `get_params` / `get_config` exchange protobuf `CmdGetData`/`RespGetData` and
60
+ * are fragmented — the client pulls fixed-size chunks by offset until
61
+ * `TotalLen` is covered.
62
+ *
63
+ * Connection metadata (`baseUrl`, `securityType`, `pop`, and `username` for
64
+ * sec2) is supplied via the transport config by {@link
65
+ * delegatedTransportHandler}; security 0 is not offered by this protocol.
22
66
  */
23
67
  class ESPLocalControlTransport {
24
- payload;
25
68
  metadata;
26
- propertyInfo = {};
27
69
  constructor(transportConfig) {
28
70
  this.metadata = transportConfig.metadata ?? {};
29
71
  }
30
72
  get adapter() {
31
- const adapter = ESPRMNeoBase.ESPRMNeoBase.getLocalControlAdapter();
32
- if (!adapter) {
33
- throw new Error("Local control adapter is not configured");
34
- }
35
- return adapter;
36
- }
37
- /**
38
- * Connects to the node, retrying on failure. RMNeo's adapter resolves on a
39
- * successful connection and rejects otherwise, so a resolved call is treated
40
- * as connected.
41
- */
42
- async connectWithRetry(nodeId, baseUrl, securityType, pop, username, maxRetries = 3) {
43
- let attempt = 0;
44
- let lastError;
45
- while (attempt < maxRetries) {
46
- try {
47
- await this.adapter.connect(nodeId, baseUrl, securityType, pop, username);
48
- return;
49
- }
50
- catch (error) {
51
- lastError = error;
52
- attempt += 1;
53
- }
54
- }
55
- const message = lastError instanceof Error ? lastError.message : String(lastError);
56
- throw new Error(`Failed to connect after ${maxRetries} attempts: ${message}`);
73
+ return LocalControlSession.getLocalControlAdapter();
57
74
  }
58
75
  async ensureConnected(nodeId) {
59
- const isConnected = await this.adapter.isConnected(nodeId);
60
- if (!isConnected) {
61
- await this.connectWithRetry(nodeId, this.metadata.baseUrl, this.metadata.securityType ?? 0, this.metadata.pop, this.metadata.username);
62
- }
76
+ await LocalControlSession.ensureLocalControlSession(this.adapter, nodeId, this.metadata, RMAKER_SESSION_OPTIONS);
63
77
  }
78
+ /**
79
+ * Applies params over `set_params`.
80
+ *
81
+ * @param payload - `{ node_id, payload }`, where `payload` is the
82
+ * `{ <deviceOrServiceName>: { <paramName>: value } }` map to write.
83
+ * @throws {Error} When the device reports a non-success status.
84
+ */
64
85
  async setParam(payload, _nodeRef) {
65
- this.payload = payload;
66
- await this.ensureConnected(this.payload?.node_id);
67
- const success = await this.setProperty(this.payload?.payload ?? {});
68
- if (!success) {
69
- throw new Error("Failed to set device params over local control");
70
- }
86
+ const nodeId = payload?.node_id;
87
+ await this.ensureConnected(nodeId);
88
+ const request = TransformEncoding.uint8ArrayToBase64(new TextEncoder().encode(JSON.stringify(payload?.payload ?? {})));
89
+ const response = await this.adapter.sendData(nodeId, constants.RMakerLocalCtrlEndpoint.SET_PARAMS, request);
90
+ this.assertSetParamsAccepted(response);
71
91
  return { message: "Parameters updated successfully", statusCode: 200 };
72
92
  }
93
+ /**
94
+ * Reads the node's full params document over `get_params`.
95
+ *
96
+ * @param payload - `{ node_id }` identifying the node to read.
97
+ * @returns The params JSON, keyed by device/service name.
98
+ */
73
99
  async getParams(payload, _nodeRef) {
74
- this.payload = payload;
75
- await this.ensureConnected(this.payload?.node_id);
76
- return this.getPropertyInfo();
100
+ const nodeId = payload?.node_id;
101
+ await this.ensureConnected(nodeId);
102
+ return this.readJsonDocument(nodeId, rmaker_local_ctrl.RMakerLocalCtrlDataType.TypeParams, constants.RMakerLocalCtrlEndpoint.GET_PARAMS);
77
103
  }
78
- // ── set ──────────────────────────────────────────────────────────────────
79
- async setProperty(json) {
80
- const request = this.buildSetPropertyRequest(json);
81
- const response = await this.adapter.sendData(this.payload?.node_id, constants.Endpoint.LOCAL_CTRL, request);
82
- return this.processSetPropertyResponse(response);
83
- }
84
- buildSetPropertyRequest(json) {
85
- const message = new esp_local_ctrl.LocalCtrlMessage();
86
- message.msg = esp_local_ctrl.LocalCtrlMsgType.TypeCmdSetPropertyValues;
87
- const cmd = new esp_local_ctrl.CmdSetPropertyValues();
88
- const prop = new esp_local_ctrl.PropertyValue();
89
- prop.index = LOCAL_CONTROL_PARAMS_INDEX;
90
- prop.value = new TextEncoder().encode(JSON.stringify(json ?? {}));
91
- cmd.props.push(prop);
92
- message.cmd_set_prop_vals = cmd;
93
- return TransformEncoding.uint8ArrayToBase64(message.serialize());
94
- }
95
- processSetPropertyResponse(response) {
96
- const deserialized = esp_local_ctrl.LocalCtrlMessage.deserialize(TransformEncoding.base64ToUint8Array(response));
97
- return deserialized.resp_set_prop_vals.status === constants$1.Status.Success;
104
+ /**
105
+ * Reads the node's config document over `get_config`. Not part of
106
+ * {@link ESPTransportInterface} — the node config normally comes from the
107
+ * cloud; this serves LAN-only flows.
108
+ *
109
+ * @param nodeId - Node to read from.
110
+ * @returns The node config JSON.
111
+ */
112
+ async getConfig(nodeId) {
113
+ await this.ensureConnected(nodeId);
114
+ return this.readJsonDocument(nodeId, rmaker_local_ctrl.RMakerLocalCtrlDataType.TypeConfig, constants.RMakerLocalCtrlEndpoint.GET_CONFIG);
98
115
  }
99
- // ── get ──────────────────────────────────────────────────────────────────
100
- /** Fetches the property count, then reads each property value into propertyInfo. */
101
- async getPropertyInfo() {
102
- this.propertyInfo = {};
103
- const count = await this.fetchPropertyCount();
104
- for (let index = 0; index < count; index++) {
105
- await this.fetchPropertyValue(index);
116
+ // ── set ──────────────────────────────────────────────────────────────────
117
+ /**
118
+ * Validates a `set_params` raw-JSON response.
119
+ *
120
+ * @throws {Error} When the body is unparseable or reports a failure.
121
+ */
122
+ assertSetParamsAccepted(response) {
123
+ const text = new TextDecoder().decode(TransformEncoding.base64ToUint8Array(response ?? ""));
124
+ let parsed;
125
+ try {
126
+ parsed = JSON.parse(text);
106
127
  }
107
- return this.propertyInfo;
108
- }
109
- async fetchPropertyCount() {
110
- const response = await this.adapter.sendData(this.payload?.node_id, constants.Endpoint.LOCAL_CTRL, this.buildGetPropertyCountRequest());
111
- return this.processGetPropertyCountResponse(response);
112
- }
113
- buildGetPropertyCountRequest() {
114
- const request = new esp_local_ctrl.LocalCtrlMessage();
115
- request.msg = esp_local_ctrl.LocalCtrlMsgType.TypeCmdGetPropertyCount;
116
- request.cmd_get_prop_count = new esp_local_ctrl.CmdGetPropertyCount();
117
- return TransformEncoding.uint8ArrayToBase64(request.serialize());
118
- }
119
- processGetPropertyCountResponse(response) {
120
- const deserialized = esp_local_ctrl.LocalCtrlMessage.deserialize(TransformEncoding.base64ToUint8Array(response));
121
- if (deserialized.resp_get_prop_count.status !== constants$1.Status.Success) {
122
- throw new Error("Failed to retrieve property count from device");
128
+ catch {
129
+ throw new Error(`Unexpected set_params response from device: ${text || "<empty>"}`);
130
+ }
131
+ if (parsed?.status !== constants.RMakerLocalCtrlSetParamsStatus.SUCCESS) {
132
+ throw new Error(parsed?.description ??
133
+ "Failed to set device params over local control (rmaker_local_ctrl)");
123
134
  }
124
- return deserialized.resp_get_prop_count.count;
125
135
  }
126
- async fetchPropertyValue(index) {
127
- const response = await this.adapter.sendData(this.payload?.node_id, constants.Endpoint.LOCAL_CTRL, this.buildGetPropertyValueRequest(index));
128
- const property = this.processGetPropertyValueResponse(response);
129
- if (property) {
130
- this.propertyInfo[property.name] = property.value;
136
+ // ── get ──────────────────────────────────────────────────────────────────
137
+ /**
138
+ * Pulls a fragmented document and parses it as JSON, serialized against every
139
+ * other fragmented read of the same node.
140
+ *
141
+ * The device holds **one global transfer cache**, not one per session: an
142
+ * offset-0 request regenerates it and it is freed after the last fragment. So
143
+ * a `getParams()` racing a `getConfig()` on the same node would clobber the
144
+ * other — the second offset-0 regenerates the cache mid-transfer, and the
145
+ * first read's next fragment comes back `Fail` (or, worse, carries bytes from
146
+ * the wrong document). The queue makes that interleaving impossible rather
147
+ * than detecting it after the fact.
148
+ *
149
+ * @param nodeId - Node to read from.
150
+ * @param dataType - Which document to read (params or config).
151
+ * @param endpoint - Endpoint serving that document.
152
+ * @throws {Error} When the device reports a failure, the response is
153
+ * malformed, or a fragment makes no forward progress.
154
+ */
155
+ readJsonDocument(nodeId, dataType, endpoint) {
156
+ return enqueueNodeRead(nodeId, () => this.pullJsonDocument(nodeId, dataType, endpoint));
157
+ }
158
+ /** The client-pull loop itself; always reached via {@link readJsonDocument}. */
159
+ async pullJsonDocument(nodeId, dataType, endpoint) {
160
+ const fragments = [];
161
+ let offset = 0;
162
+ let totalLength = 0;
163
+ do {
164
+ const response = await this.adapter.sendData(nodeId, endpoint, this.buildGetDataRequest(dataType, offset));
165
+ const buf = this.processGetDataResponse(response, offset);
166
+ totalLength = buf.totalLength;
167
+ if (totalLength === 0) {
168
+ return {};
169
+ }
170
+ if (buf.payload.length === 0) {
171
+ // Without forward progress the pull loop would never terminate.
172
+ throw new Error(`Device returned an empty fragment at offset ${offset} of ${totalLength} on ${endpoint}`);
173
+ }
174
+ fragments.push(buf.payload);
175
+ offset += buf.payload.length;
176
+ } while (offset < totalLength);
177
+ if (offset > totalLength) {
178
+ throw new Error(`Fragmented read of ${endpoint} overran: got ${offset} bytes, expected ${totalLength}`);
131
179
  }
180
+ return this.parseJsonFragments(fragments, endpoint);
132
181
  }
133
- buildGetPropertyValueRequest(index) {
134
- const request = new esp_local_ctrl.LocalCtrlMessage();
135
- request.msg = esp_local_ctrl.LocalCtrlMsgType.TypeCmdGetPropertyValues;
136
- const cmd = new esp_local_ctrl.CmdGetPropertyValues();
137
- cmd.indices.push(index);
138
- request.cmd_get_prop_vals = cmd;
139
- return TransformEncoding.uint8ArrayToBase64(request.serialize());
182
+ buildGetDataRequest(dataType, offset) {
183
+ return TransformEncoding.uint8ArrayToBase64(rmaker_local_ctrl.RMakerLocalCtrlProtoHelper.createGetDataRequest(dataType, offset));
140
184
  }
141
- processGetPropertyValueResponse(response) {
142
- const deserialized = esp_local_ctrl.LocalCtrlMessage.deserialize(TransformEncoding.base64ToUint8Array(response));
143
- if (deserialized.resp_get_prop_vals.status !== constants$1.Status.Success) {
144
- throw new Error("Failed to get property values from device response");
185
+ /**
186
+ * Parses one `RespGetData` and checks it answers the requested offset.
187
+ *
188
+ * @param response - Base64 protobuf response from the adapter.
189
+ * @param requestedOffset - Offset asked for, used to detect a desynced pull.
190
+ */
191
+ processGetDataResponse(response, requestedOffset) {
192
+ const parsed = rmaker_local_ctrl.RMakerLocalCtrlProtoHelper.parseGetDataResponse(TransformEncoding.base64ToUint8Array(response));
193
+ if (!rmaker_local_ctrl.RMakerLocalCtrlProtoHelper.isSuccess(parsed)) {
194
+ throw new Error(`Device rejected the local-control read (status ${rmaker_local_ctrl.RMakerLocalCtrlProtoHelper.getStatus(parsed)})`);
195
+ }
196
+ const fragmentOffset = rmaker_local_ctrl.RMakerLocalCtrlProtoHelper.getOffset(parsed);
197
+ if (fragmentOffset !== requestedOffset) {
198
+ throw new Error(`Device answered offset ${fragmentOffset}, expected ${requestedOffset}`);
145
199
  }
146
- const prop = deserialized.resp_get_prop_vals.props[0];
147
- if (!prop)
148
- return undefined;
149
200
  return {
150
- name: prop.name || "unknown",
151
- value: JSON.parse(new TextDecoder().decode(prop.value)),
201
+ payload: rmaker_local_ctrl.RMakerLocalCtrlProtoHelper.getPayload(parsed),
202
+ totalLength: rmaker_local_ctrl.RMakerLocalCtrlProtoHelper.getTotalLen(parsed),
152
203
  };
153
204
  }
205
+ /** Joins the pulled fragments and parses the result as JSON. */
206
+ parseJsonFragments(fragments, endpoint) {
207
+ const joined = new Uint8Array(fragments.reduce((length, fragment) => length + fragment.length, 0));
208
+ let position = 0;
209
+ for (const fragment of fragments) {
210
+ joined.set(fragment, position);
211
+ position += fragment.length;
212
+ }
213
+ const text = new TextDecoder().decode(joined);
214
+ try {
215
+ return JSON.parse(text);
216
+ }
217
+ catch {
218
+ throw new Error(`Device returned malformed JSON on ${endpoint}`);
219
+ }
220
+ }
154
221
  }
155
222
 
156
223
  exports.ESPLocalControlTransport = ESPLocalControlTransport;
@@ -1 +1 @@
1
- {"version":3,"file":"ESPLocalControlTransport.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"ESPLocalControlTransport.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ var ESPRMNeoBase = require('../../ESPRMNeoBase.js');
4
+
5
+ /*
6
+ * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
7
+ *
8
+ * SPDX-License-Identifier: Apache-2.0
9
+ */
10
+ /** Attempts made before a local-control handshake is reported as failed. */
11
+ const DEFAULT_CONNECT_RETRIES = 3;
12
+ /**
13
+ * Returns the app-supplied local control adapter.
14
+ *
15
+ * @throws {Error} When no adapter was registered on {@link ESPRMNeoBase}.
16
+ */
17
+ function getLocalControlAdapter() {
18
+ const adapter = ESPRMNeoBase.ESPRMNeoBase.getLocalControlAdapter();
19
+ if (!adapter) {
20
+ throw new Error("Local control adapter is not configured");
21
+ }
22
+ return adapter;
23
+ }
24
+ /**
25
+ * Ensures a usable local-control session for `nodeId`, connecting (with
26
+ * retries) when the adapter reports none.
27
+ *
28
+ * The adapter resolves on a successful handshake and rejects otherwise, so a
29
+ * resolved `connect` is treated as connected.
30
+ *
31
+ * @param adapter - Adapter to drive.
32
+ * @param nodeId - Node to connect to.
33
+ * @param metadata - Local transport metadata (`baseUrl`, `securityType`, `pop`,
34
+ * `username`).
35
+ * @param options - Session endpoints for the protocol in use; omitted for the
36
+ * adapter's built-in default paths.
37
+ * @param maxRetries - Connection attempts before failing.
38
+ * @throws {Error} When every connection attempt fails.
39
+ */
40
+ async function ensureLocalControlSession(adapter, nodeId, metadata, options, maxRetries = DEFAULT_CONNECT_RETRIES) {
41
+ if (await adapter.isConnected(nodeId)) {
42
+ return;
43
+ }
44
+ let attempt = 0;
45
+ let lastError;
46
+ while (attempt < maxRetries) {
47
+ try {
48
+ await adapter.connect(nodeId, metadata.baseUrl, metadata.securityType ?? 0, metadata.pop, metadata.username, options);
49
+ return;
50
+ }
51
+ catch (error) {
52
+ lastError = error;
53
+ attempt += 1;
54
+ }
55
+ }
56
+ const message = lastError instanceof Error ? lastError.message : String(lastError);
57
+ throw new Error(`Failed to connect after ${maxRetries} attempts: ${message}`);
58
+ }
59
+
60
+ exports.ensureLocalControlSession = ensureLocalControlSession;
61
+ exports.getLocalControlAdapter = getLocalControlAdapter;
62
+ //# sourceMappingURL=LocalControlSession.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalControlSession.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -19,6 +19,29 @@ exports.ESPTransportMode = void 0;
19
19
  /** Communication is routed through MQTT (RainMaker Neo cloud: AWS shadow). */
20
20
  ESPTransportMode["mqtt"] = "mqtt";
21
21
  })(exports.ESPTransportMode || (exports.ESPTransportMode = {}));
22
+ /**
23
+ * Wire protocol spoken by the `local` transport. Both protocols run over a
24
+ * protocomm session on the node's LAN HTTP server; they differ in endpoint
25
+ * names and message encoding.
26
+ *
27
+ * Carried on the local transport config's `metadata.protocol`, set by local
28
+ * discovery from the mDNS service type that produced the hit.
29
+ */
30
+ exports.ESPLocalControlProtocol = void 0;
31
+ (function (ESPLocalControlProtocol) {
32
+ /**
33
+ * RainMaker Neo protocol (`rmaker_local_ctrl/*` session plus
34
+ * `get_params`/`set_params`/`get_config`), advertised as
35
+ * `_esp_rmaker_ctrl._tcp`. Default for this SDK.
36
+ */
37
+ ESPLocalControlProtocol["rmakerLocalCtrl"] = "rmaker_local_ctrl";
38
+ })(exports.ESPLocalControlProtocol || (exports.ESPLocalControlProtocol = {}));
39
+ /**
40
+ * Protocol assumed when a local transport config carries no explicit
41
+ * `metadata.protocol` — for example a LAN transport restored from a client-side
42
+ * registry rather than a fresh discovery hit.
43
+ */
44
+ const DEFAULT_LOCAL_CONTROL_PROTOCOL = exports.ESPLocalControlProtocol.rmakerLocalCtrl;
22
45
  /**
23
46
  * Default transport priority order used when neither the node nor the SDK base
24
47
  * has an explicit order configured: local control first, MQTT (cloud) fallback.
@@ -28,5 +51,6 @@ const DEFAULT_TRANSPORT_ORDER = [
28
51
  exports.ESPTransportMode.mqtt,
29
52
  ];
30
53
 
54
+ exports.DEFAULT_LOCAL_CONTROL_PROTOCOL = DEFAULT_LOCAL_CONTROL_PROTOCOL;
31
55
  exports.DEFAULT_TRANSPORT_ORDER = DEFAULT_TRANSPORT_ORDER;
32
56
  //# sourceMappingURL=transport.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"transport.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"transport.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -475,8 +475,6 @@ const ProvisionType = {
475
475
  * An object containing endpoint paths.
476
476
  */
477
477
  const Endpoint = {
478
- /** The endpoint for local control. */
479
- LOCAL_CTRL: "esp_local_ctrl/control",
480
478
  /** The endpoint for cloud user association. */
481
479
  CLOUD_USER_ASSOCIATION: "cloud_user_assoc",
482
480
  /** The endpoint for challenge-response and get-node-id protocol with the device. */
@@ -484,6 +482,64 @@ const Endpoint = {
484
482
  /** The endpoint for assisted claiming. */
485
483
  RM_CLAIM: "rmaker_claim",
486
484
  };
485
+ /**
486
+ * Endpoint paths of the `rmaker_local_ctrl` protocol served by RainMaker Neo
487
+ * firmware (see the firmware's local-control endpoint protocol spec).
488
+ *
489
+ * All data endpoints inherit the security of the session established on
490
+ * {@link RMakerLocalCtrlEndpoint.SESSION}.
491
+ */
492
+ const RMakerLocalCtrlEndpoint = {
493
+ /** Protocomm session-security endpoint (SEC1 with/without PoP, or SEC2). */
494
+ SESSION: "rmaker_local_ctrl/session",
495
+ /** POST any payload; responds with the service info JSON (`sec_ver`, `cap`, …). */
496
+ VERSION: "rmaker_local_ctrl/version",
497
+ /** Reads the params JSON — protobuf `CmdGetData`, fragmented response. */
498
+ GET_PARAMS: "get_params",
499
+ /** Reads the node config JSON — protobuf `CmdGetData`, fragmented response. */
500
+ GET_CONFIG: "get_config",
501
+ /** Writes params — raw JSON request and response (no protobuf). */
502
+ SET_PARAMS: "set_params",
503
+ };
504
+ /**
505
+ * Root key of the JSON served by {@link RMakerLocalCtrlEndpoint.VERSION}, i.e.
506
+ * `{"rmaker_local_ctrl": {"ver": …, "sec_ver": …, "cap": [...]}}`. Passed to the
507
+ * local-control adapter so the native layer probes the right scheme version.
508
+ */
509
+ const RMAKER_LOCAL_CTRL_VERSION_KEY = "rmaker_local_ctrl";
510
+ /**
511
+ * Maximum fragment size (bytes) the firmware serves per `RespGetData`. Reads
512
+ * loop with `Offset += Payload.length` until `TotalLen` is reached; this value
513
+ * is informational (the device dictates the actual fragment length).
514
+ */
515
+ const RMAKER_LOCAL_CTRL_FRAGMENT_SIZE = 200;
516
+ /**
517
+ * `status` values in a `set_params` raw-JSON response.
518
+ */
519
+ const RMakerLocalCtrlSetParamsStatus = {
520
+ SUCCESS: "success",
521
+ FAIL: "fail",
522
+ };
523
+ /**
524
+ * mDNS TXT record keys advertised by the `_esp_rmaker_ctrl._tcp` service.
525
+ */
526
+ const RMakerLocalCtrlTxtKey = {
527
+ /** Node ID (also the service instance name and hostname). */
528
+ NODE_ID: "node_id",
529
+ /** Comma-separated active capabilities — see {@link RMakerLocalCtrlCapability}. */
530
+ CAP: "cap",
531
+ };
532
+ /**
533
+ * Capability tokens found in the `cap` TXT record. A node advertising only
534
+ * `ch_resp` is reachable for on-network user-node association but *not* for
535
+ * param control, so it must not be registered as a local control transport.
536
+ */
537
+ const RMakerLocalCtrlCapability = {
538
+ /** Params/config endpoints are registered. */
539
+ LOCAL_CTRL: "local_ctrl",
540
+ /** Challenge-response (on-network user-node association) is registered. */
541
+ CH_RESP: "ch_resp",
542
+ };
487
543
  /**
488
544
  * Assisted-claiming REST paths, on the deployment's main API and SigV4-signed
489
545
  * like every other `/v1/*` route.
@@ -641,8 +697,12 @@ const ProvErrorCodes = {
641
697
  * @enum {string}
642
698
  */
643
699
  const ServiceType = {
644
- /** Represents the ESP local control TCP service type. */
645
- ESP_LOCAL_CTRL_TCP: "_esp_local_ctrl._tcp.",
700
+ /**
701
+ * Service type advertised by RainMaker Neo firmware. A single instance serves
702
+ * the `rmaker_local_ctrl` endpoints; the `cap` TXT record says which endpoint
703
+ * sets are active. This is the default for local discovery in this SDK.
704
+ */
705
+ ESP_RMAKER_LOCAL_CTRL_TCP: "_esp_rmaker_ctrl._tcp.",
646
706
  };
647
707
  /**
648
708
  * An object containing protocol types.
@@ -887,6 +947,12 @@ exports.NodeWarnMessages = NodeWarnMessages;
887
947
  exports.ProtocolType = ProtocolType;
888
948
  exports.ProvErrorCodes = ProvErrorCodes;
889
949
  exports.ProvisionType = ProvisionType;
950
+ exports.RMAKER_LOCAL_CTRL_FRAGMENT_SIZE = RMAKER_LOCAL_CTRL_FRAGMENT_SIZE;
951
+ exports.RMAKER_LOCAL_CTRL_VERSION_KEY = RMAKER_LOCAL_CTRL_VERSION_KEY;
952
+ exports.RMakerLocalCtrlCapability = RMakerLocalCtrlCapability;
953
+ exports.RMakerLocalCtrlEndpoint = RMakerLocalCtrlEndpoint;
954
+ exports.RMakerLocalCtrlSetParamsStatus = RMakerLocalCtrlSetParamsStatus;
955
+ exports.RMakerLocalCtrlTxtKey = RMakerLocalCtrlTxtKey;
890
956
  exports.SDK_VERSION = SDK_VERSION;
891
957
  exports.ScheduleErrorMessages = ScheduleErrorMessages;
892
958
  exports.ScheduleSuccessMessages = ScheduleSuccessMessages;
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"constants.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -3,26 +3,70 @@
3
3
  var ESPDiscoveryManager = require('../services/ESPTransport/ESPDiscovery/ESPDiscoveryManager.js');
4
4
  var NodeUpdatesBus = require('../services/NodeUpdatesBus.js');
5
5
  var transport = require('../types/transport.js');
6
+ var constants = require('./constants.js');
6
7
 
7
8
  /*
8
9
  * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
9
10
  *
10
11
  * SPDX-License-Identifier: Apache-2.0
11
12
  */
12
- /** Maps a default local-discovery hit into the client-facing payload shape. */
13
+ /**
14
+ * Splits the mDNS `cap` TXT record (`"local_ctrl,ch_resp"`) into tokens.
15
+ * Returns `undefined` when the record is absent, which older firmware omits.
16
+ */
17
+ function parseCapabilities(txt) {
18
+ const raw = txt?.[constants.RMakerLocalCtrlTxtKey.CAP];
19
+ if (typeof raw !== "string")
20
+ return undefined;
21
+ const capabilities = raw
22
+ .split(",")
23
+ .map((capability) => capability.trim())
24
+ .filter(Boolean);
25
+ return capabilities.length ? capabilities : undefined;
26
+ }
27
+ /**
28
+ * Maps a default local-discovery hit into the client-facing payload shape.
29
+ *
30
+ * The `_esp_rmaker_ctrl._tcp` instance is also advertised by nodes that
31
+ * only serve challenge-response (on-network user-node association). Those are
32
+ * not reachable for param control, so a hit whose `cap` TXT record excludes
33
+ * `local_ctrl` maps to `undefined` and is dropped rather than registered as a
34
+ * local transport. A hit with no `cap` record is treated as control-capable.
35
+ *
36
+ * @param info - Raw adapter result (`nodeId`, `baseUrl`, and `txt` when the
37
+ * platform resolved TXT records).
38
+ * @returns The payload to deliver, or `undefined` to skip this hit.
39
+ */
13
40
  function toDiscoveredNodeData(info) {
41
+ const capabilities = parseCapabilities(info.txt);
42
+ if (capabilities &&
43
+ !capabilities.includes(constants.RMakerLocalCtrlCapability.LOCAL_CTRL)) {
44
+ return undefined;
45
+ }
14
46
  return {
15
47
  nodeId: info.nodeId,
16
48
  transportDetails: {
17
49
  type: transport.ESPTransportMode.local,
18
- metadata: { baseUrl: info.baseUrl },
50
+ metadata: {
51
+ baseUrl: info.baseUrl,
52
+ protocol: transport.ESPLocalControlProtocol.rmakerLocalCtrl,
53
+ ...(capabilities && { capabilities }),
54
+ },
19
55
  },
20
56
  };
21
57
  }
22
- /** Starts LAN discovery; each hit is mapped then passed to `onDiscovered`. */
58
+ /**
59
+ * Starts LAN discovery; each hit is mapped then passed to `onDiscovered`.
60
+ * Hits that are not control-capable (see {@link toDiscoveredNodeData}) are
61
+ * skipped.
62
+ */
23
63
  function startLocalDiscovery(onDiscovered) {
24
64
  const manager = new ESPDiscoveryManager.ESPDiscoveryManager();
25
- manager.startDiscovery((info) => onDiscovered(toDiscoveredNodeData(info)));
65
+ manager.startDiscovery((info) => {
66
+ const data = toDiscoveredNodeData(info);
67
+ if (data)
68
+ onDiscovered(data);
69
+ });
26
70
  return { stop: () => manager.stopDiscovery() };
27
71
  }
28
72
  /** Forwards process-wide node param updates to `onUpdate`. */
@@ -1 +1 @@
1
- {"version":3,"file":"eventSubscriptionUtils.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"eventSubscriptionUtils.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -183,7 +183,7 @@ class ESPRMNeoBase {
183
183
  }
184
184
  /**
185
185
  * Sets the local control adapter used to talk to nodes over the LAN via
186
- * the `esp_local_ctrl` protocol.
186
+ * the `rmaker_local_ctrl` protocol.
187
187
  *
188
188
  * @param adapter - Adapter implementing {@link ESPLocalControlAdapterInterface}.
189
189
  * @throws {Error} If the SDK is not initialized or the adapter is invalid.