@espressif/rainmaker-neo-base-sdk 1.0.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 (117) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +40 -4
  3. package/dist/cjs/ESPDevice.js +6 -0
  4. package/dist/cjs/ESPDevice.js.map +1 -1
  5. package/dist/cjs/ESPRMNeoBase.js +1 -1
  6. package/dist/cjs/ESPRMNeoNode.js +33 -14
  7. package/dist/cjs/ESPRMNeoNode.js.map +1 -1
  8. package/dist/cjs/index.js +14 -0
  9. package/dist/cjs/index.js.map +1 -1
  10. package/dist/cjs/methods/ESPDevice/Provision.js +7 -0
  11. package/dist/cjs/methods/ESPDevice/Provision.js.map +1 -1
  12. package/dist/cjs/methods/ESPDevice/ResetWifiStatus.js +26 -0
  13. package/dist/cjs/methods/ESPDevice/ResetWifiStatus.js.map +1 -0
  14. package/dist/cjs/methods/ESPDevice/RetryNetworkCredentials.js +46 -0
  15. package/dist/cjs/methods/ESPDevice/RetryNetworkCredentials.js.map +1 -0
  16. package/dist/cjs/methods/ESPRMNeoNode/Sync.js +4 -1
  17. package/dist/cjs/methods/ESPRMNeoNode/Sync.js.map +1 -1
  18. package/dist/cjs/methods/ESPRMNeoUser/MQTT/AssumeRole.js +8 -9
  19. package/dist/cjs/methods/ESPRMNeoUser/MQTT/AssumeRole.js.map +1 -1
  20. package/dist/cjs/proto/esp_rmaker_chal_resp.js +5 -0
  21. package/dist/cjs/proto/esp_rmaker_chal_resp.js.map +1 -1
  22. package/dist/cjs/proto/rmaker_local_ctrl.js +273 -0
  23. package/dist/cjs/proto/rmaker_local_ctrl.js.map +1 -0
  24. package/dist/cjs/services/ESPRMNeoMqtt/ESPRMNeoMqtt.js +13 -0
  25. package/dist/cjs/services/ESPRMNeoMqtt/ESPRMNeoMqtt.js.map +1 -1
  26. package/dist/cjs/services/ESPTransport/ESPDiscovery/ESPDiscoveryManager.js +5 -4
  27. package/dist/cjs/services/ESPTransport/ESPDiscovery/ESPDiscoveryManager.js.map +1 -1
  28. package/dist/cjs/services/ESPTransport/ESPLocalControlTransport.js +183 -116
  29. package/dist/cjs/services/ESPTransport/ESPLocalControlTransport.js.map +1 -1
  30. package/dist/cjs/services/ESPTransport/LocalControlSession.js +62 -0
  31. package/dist/cjs/services/ESPTransport/LocalControlSession.js.map +1 -0
  32. package/dist/cjs/services/NodeMQTTOrchestrator.js +2 -5
  33. package/dist/cjs/services/NodeMQTTOrchestrator.js.map +1 -1
  34. package/dist/cjs/types/transport.js +24 -0
  35. package/dist/cjs/types/transport.js.map +1 -1
  36. package/dist/cjs/utils/constants.js +80 -4
  37. package/dist/cjs/utils/constants.js.map +1 -1
  38. package/dist/cjs/utils/eventSubscriptionUtils.js +48 -4
  39. package/dist/cjs/utils/eventSubscriptionUtils.js.map +1 -1
  40. package/dist/cjs/utils/nodeUtils.js +12 -1
  41. package/dist/cjs/utils/nodeUtils.js.map +1 -1
  42. package/dist/cjs/utils/waitForNodeOnline.js +7 -6
  43. package/dist/cjs/utils/waitForNodeOnline.js.map +1 -1
  44. package/dist/esm/ESPDevice.js +6 -0
  45. package/dist/esm/ESPDevice.js.map +1 -1
  46. package/dist/esm/ESPRMNeoBase.js +1 -1
  47. package/dist/esm/ESPRMNeoNode.js +33 -14
  48. package/dist/esm/ESPRMNeoNode.js.map +1 -1
  49. package/dist/esm/index.js +4 -2
  50. package/dist/esm/index.js.map +1 -1
  51. package/dist/esm/methods/ESPDevice/Provision.js +7 -0
  52. package/dist/esm/methods/ESPDevice/Provision.js.map +1 -1
  53. package/dist/esm/methods/ESPDevice/ResetWifiStatus.js +24 -0
  54. package/dist/esm/methods/ESPDevice/ResetWifiStatus.js.map +1 -0
  55. package/dist/esm/methods/ESPDevice/RetryNetworkCredentials.js +44 -0
  56. package/dist/esm/methods/ESPDevice/RetryNetworkCredentials.js.map +1 -0
  57. package/dist/esm/methods/ESPRMNeoNode/Sync.js +4 -1
  58. package/dist/esm/methods/ESPRMNeoNode/Sync.js.map +1 -1
  59. package/dist/esm/methods/ESPRMNeoUser/MQTT/AssumeRole.js +9 -10
  60. package/dist/esm/methods/ESPRMNeoUser/MQTT/AssumeRole.js.map +1 -1
  61. package/dist/esm/proto/esp_rmaker_chal_resp.js +5 -0
  62. package/dist/esm/proto/esp_rmaker_chal_resp.js.map +1 -1
  63. package/dist/esm/proto/rmaker_local_ctrl.js +271 -0
  64. package/dist/esm/proto/rmaker_local_ctrl.js.map +1 -0
  65. package/dist/esm/services/ESPRMNeoMqtt/ESPRMNeoMqtt.js +13 -0
  66. package/dist/esm/services/ESPRMNeoMqtt/ESPRMNeoMqtt.js.map +1 -1
  67. package/dist/esm/services/ESPTransport/ESPDiscovery/ESPDiscoveryManager.js +5 -4
  68. package/dist/esm/services/ESPTransport/ESPDiscovery/ESPDiscoveryManager.js.map +1 -1
  69. package/dist/esm/services/ESPTransport/ESPLocalControlTransport.js +184 -117
  70. package/dist/esm/services/ESPTransport/ESPLocalControlTransport.js.map +1 -1
  71. package/dist/esm/services/ESPTransport/LocalControlSession.js +59 -0
  72. package/dist/esm/services/ESPTransport/LocalControlSession.js.map +1 -0
  73. package/dist/esm/services/NodeMQTTOrchestrator.js +2 -5
  74. package/dist/esm/services/NodeMQTTOrchestrator.js.map +1 -1
  75. package/dist/esm/types/transport.js +24 -1
  76. package/dist/esm/types/transport.js.map +1 -1
  77. package/dist/esm/utils/constants.js +74 -5
  78. package/dist/esm/utils/constants.js.map +1 -1
  79. package/dist/esm/utils/eventSubscriptionUtils.js +49 -5
  80. package/dist/esm/utils/eventSubscriptionUtils.js.map +1 -1
  81. package/dist/esm/utils/nodeUtils.js +12 -1
  82. package/dist/esm/utils/nodeUtils.js.map +1 -1
  83. package/dist/esm/utils/waitForNodeOnline.js +7 -6
  84. package/dist/esm/utils/waitForNodeOnline.js.map +1 -1
  85. package/dist/types/ESPDevice.d.ts +10 -0
  86. package/dist/types/ESPRMNeoBase.d.ts +1 -1
  87. package/dist/types/ESPRMNeoNode.d.ts +17 -5
  88. package/dist/types/methods/ESPDevice/ResetWifiStatus.d.ts +18 -0
  89. package/dist/types/methods/ESPDevice/RetryNetworkCredentials.d.ts +22 -0
  90. package/dist/types/methods/ESPDevice/index.d.ts +2 -0
  91. package/dist/types/methods/ESPRMNeoUser/MQTT/AssumeRole.d.ts +25 -15
  92. package/dist/types/proto/rmaker_local_ctrl.d.ts +137 -0
  93. package/dist/types/services/ESPRMNeoMqtt/ESPRMNeoMqtt.d.ts +10 -0
  94. package/dist/types/services/ESPTransport/ESPDiscovery/ESPDiscoveryManager.d.ts +4 -3
  95. package/dist/types/services/ESPTransport/ESPLocalControlTransport.d.ts +70 -23
  96. package/dist/types/services/ESPTransport/LocalControlSession.d.ts +24 -0
  97. package/dist/types/services/NodeMQTTOrchestrator.d.ts +2 -2
  98. package/dist/types/services/interfaces/MQTTTransport.d.ts +10 -0
  99. package/dist/types/types/discovery.d.ts +1 -1
  100. package/dist/types/types/localControl.d.ts +26 -1
  101. package/dist/types/types/provision.d.ts +10 -0
  102. package/dist/types/types/transport.d.ts +24 -2
  103. package/dist/types/utils/baseUtils.d.ts +1 -1
  104. package/dist/types/utils/constants.d.ts +74 -5
  105. package/dist/types/utils/eventSubscriptionUtils.d.ts +19 -3
  106. package/dist/types/utils/waitForNodeOnline.d.ts +9 -3
  107. package/package.json +1 -1
  108. package/dist/cjs/proto/constants.js +0 -24
  109. package/dist/cjs/proto/constants.js.map +0 -1
  110. package/dist/cjs/proto/esp_local_ctrl.js +0 -787
  111. package/dist/cjs/proto/esp_local_ctrl.js.map +0 -1
  112. package/dist/esm/proto/constants.js +0 -24
  113. package/dist/esm/proto/constants.js.map +0 -1
  114. package/dist/esm/proto/esp_local_ctrl.js +0 -758
  115. package/dist/esm/proto/esp_local_ctrl.js.map +0 -1
  116. package/dist/types/proto/constants.d.ts +0 -15
  117. package/dist/types/proto/esp_local_ctrl.d.ts +0 -283
@@ -0,0 +1,273 @@
1
+ 'use strict';
2
+
3
+ /*
4
+ * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
5
+ *
6
+ * SPDX-License-Identifier: Apache-2.0
7
+ */
8
+ /**
9
+ * Codec for the `rmaker_local_ctrl` endpoint protocol, mirroring the firmware's
10
+ * `local_ctrl.proto` schema.
11
+ *
12
+ * Hand-rolled like {@link ClaimingProtoHelper} rather than generated: the
13
+ * schema is small, and generated `google-protobuf` modules cannot be pulled
14
+ * into a downstream Metro/RN bundle (see the note in `utils/export.ts`).
15
+ *
16
+ * Field numbers are the wire contract with deployed firmware — treat them as
17
+ * frozen:
18
+ * - `RMakerLocalCtrlPayload`: 1 = msg (varint), 10 = cmdGetData, 11 = respGetData
19
+ * - `CmdGetData`: 1 = DataType (varint), 2 = Offset (varint),
20
+ * 3 = Timestamp (varint), 4 = HasTimestamp (varint)
21
+ * - `RespGetData`: 1 = Status (varint), 2 = Buf
22
+ * - `PayloadBuf`: 1 = Offset (varint), 2 = Payload (bytes), 3 = TotalLen (varint)
23
+ *
24
+ * `Timestamp` / `HasTimestamp` are reserved for a future signed-response
25
+ * extension and are ignored by current firmware, so the encoder omits them.
26
+ *
27
+ * Only `get_params` / `get_config` use this schema; `set_params` carries raw
28
+ * JSON on the wire.
29
+ */
30
+ /**
31
+ * Status returned by the device for a data read.
32
+ */
33
+ exports.RMakerLocalCtrlStatus = void 0;
34
+ (function (RMakerLocalCtrlStatus) {
35
+ RMakerLocalCtrlStatus[RMakerLocalCtrlStatus["Success"] = 0] = "Success";
36
+ RMakerLocalCtrlStatus[RMakerLocalCtrlStatus["Fail"] = 1] = "Fail";
37
+ RMakerLocalCtrlStatus[RMakerLocalCtrlStatus["InvalidParam"] = 2] = "InvalidParam";
38
+ RMakerLocalCtrlStatus[RMakerLocalCtrlStatus["NoMemory"] = 3] = "NoMemory";
39
+ })(exports.RMakerLocalCtrlStatus || (exports.RMakerLocalCtrlStatus = {}));
40
+ /**
41
+ * Selects which document a read targets.
42
+ */
43
+ exports.RMakerLocalCtrlDataType = void 0;
44
+ (function (RMakerLocalCtrlDataType) {
45
+ /** The node's params JSON, as served by `get_params`. */
46
+ RMakerLocalCtrlDataType[RMakerLocalCtrlDataType["TypeParams"] = 0] = "TypeParams";
47
+ /** The node's config JSON, as served by `get_config`. */
48
+ RMakerLocalCtrlDataType[RMakerLocalCtrlDataType["TypeConfig"] = 1] = "TypeConfig";
49
+ })(exports.RMakerLocalCtrlDataType || (exports.RMakerLocalCtrlDataType = {}));
50
+ /**
51
+ * Message type discriminator carried on `RMakerLocalCtrlPayload.msg`.
52
+ */
53
+ exports.RMakerLocalCtrlMsgType = void 0;
54
+ (function (RMakerLocalCtrlMsgType) {
55
+ RMakerLocalCtrlMsgType[RMakerLocalCtrlMsgType["TypeCmdGetData"] = 0] = "TypeCmdGetData";
56
+ RMakerLocalCtrlMsgType[RMakerLocalCtrlMsgType["TypeRespGetData"] = 1] = "TypeRespGetData";
57
+ })(exports.RMakerLocalCtrlMsgType || (exports.RMakerLocalCtrlMsgType = {}));
58
+ /** Tag bytes written by the request encoder: `(fieldNumber << 3) | wireType`. */
59
+ const TAG_FIELD_1_VARINT = 0x08;
60
+ const TAG_FIELD_2_VARINT = 0x10;
61
+ const TAG_FIELD_10_BYTES = 0x52;
62
+ const WIRE_TYPE_VARINT = 0;
63
+ const WIRE_TYPE_BYTES = 2;
64
+ /**
65
+ * Encoder/decoder for `rmaker_local_ctrl` frames.
66
+ */
67
+ class RMakerLocalCtrlProtoHelper {
68
+ /**
69
+ * Builds a `CmdGetData` request for one fragment.
70
+ *
71
+ * An `offset` of 0 makes the device (re)generate and cache the document;
72
+ * subsequent offsets are served from that cache.
73
+ *
74
+ * @param dataType - Document to read.
75
+ * @param offset - Byte offset to read from.
76
+ * @returns The serialized request.
77
+ */
78
+ static createGetDataRequest(dataType, offset) {
79
+ const cmdGetData = this.concat([
80
+ // Both fields are written explicitly, including their proto3 zero values,
81
+ // matching how the reference clients frame this command.
82
+ new Uint8Array([TAG_FIELD_1_VARINT]),
83
+ this.encodeVarint(dataType),
84
+ new Uint8Array([TAG_FIELD_2_VARINT]),
85
+ this.encodeVarint(offset),
86
+ ]);
87
+ return this.concat([
88
+ new Uint8Array([TAG_FIELD_1_VARINT]),
89
+ this.encodeVarint(exports.RMakerLocalCtrlMsgType.TypeCmdGetData),
90
+ new Uint8Array([TAG_FIELD_10_BYTES]),
91
+ this.encodeVarint(cmdGetData.length),
92
+ cmdGetData,
93
+ ]);
94
+ }
95
+ /**
96
+ * Parses a `RespGetData` frame from the device.
97
+ *
98
+ * @param data - Raw response bytes.
99
+ * @returns The parsed payload. Absent fields keep their proto3 defaults.
100
+ */
101
+ static parseGetDataResponse(data) {
102
+ const result = {
103
+ msg: exports.RMakerLocalCtrlMsgType.TypeCmdGetData,
104
+ };
105
+ let index = 0;
106
+ while (index < data.length) {
107
+ const tag = data[index++];
108
+ const fieldNumber = tag >> 3;
109
+ const wireType = tag & 0x07;
110
+ if (fieldNumber === 1 && wireType === WIRE_TYPE_VARINT) {
111
+ const { value, newIndex } = this.readVarint(data, index);
112
+ result.msg = value;
113
+ index = newIndex;
114
+ continue;
115
+ }
116
+ if (fieldNumber === 11 && wireType === WIRE_TYPE_BYTES) {
117
+ const { value: length, newIndex } = this.readVarint(data, index);
118
+ result.respGetData = this.parseRespGetData(data.slice(newIndex, newIndex + length));
119
+ index = newIndex + length;
120
+ continue;
121
+ }
122
+ index = this.skipField(data, index, wireType);
123
+ }
124
+ return result;
125
+ }
126
+ /**
127
+ * Whether the device reported a successful read.
128
+ *
129
+ * @param response - Parsed response.
130
+ */
131
+ static isSuccess(response) {
132
+ return response.respGetData?.status === exports.RMakerLocalCtrlStatus.Success;
133
+ }
134
+ /**
135
+ * Returns the device's status as its enum name, for diagnostics.
136
+ *
137
+ * @param response - Parsed response.
138
+ * @returns The status name, or `"Unknown"` when absent.
139
+ */
140
+ static getStatus(response) {
141
+ const status = response.respGetData?.status;
142
+ return status === undefined
143
+ ? "Unknown"
144
+ : (exports.RMakerLocalCtrlStatus[status] ?? `Unknown(${status})`);
145
+ }
146
+ /**
147
+ * Offset the device answered with, which must match the requested offset.
148
+ *
149
+ * @param response - Parsed response.
150
+ */
151
+ static getOffset(response) {
152
+ return response.respGetData?.buf?.offset ?? 0;
153
+ }
154
+ /**
155
+ * Full document length reported by the device.
156
+ *
157
+ * @param response - Parsed response.
158
+ */
159
+ static getTotalLen(response) {
160
+ return response.respGetData?.buf?.totalLen ?? 0;
161
+ }
162
+ /**
163
+ * Fragment bytes carried by the response.
164
+ *
165
+ * @param response - Parsed response.
166
+ */
167
+ static getPayload(response) {
168
+ return response.respGetData?.buf?.payload ?? new Uint8Array(0);
169
+ }
170
+ // Private helper methods
171
+ static concat(parts) {
172
+ const totalLength = parts.reduce((sum, part) => sum + part.length, 0);
173
+ const result = new Uint8Array(totalLength);
174
+ let pos = 0;
175
+ for (const part of parts) {
176
+ result.set(part, pos);
177
+ pos += part.length;
178
+ }
179
+ return result;
180
+ }
181
+ static encodeVarint(value) {
182
+ const bytes = [];
183
+ let remaining = value;
184
+ while (remaining > 127) {
185
+ bytes.push((remaining & 0x7f) | 0x80);
186
+ remaining >>>= 7;
187
+ }
188
+ bytes.push(remaining & 0x7f);
189
+ return new Uint8Array(bytes);
190
+ }
191
+ static readVarint(data, index) {
192
+ let value = 0;
193
+ let shift = 0;
194
+ let byte;
195
+ do {
196
+ byte = data[index++];
197
+ value |= (byte & 0x7f) << shift;
198
+ shift += 7;
199
+ } while (byte & 0x80);
200
+ return { value, newIndex: index };
201
+ }
202
+ /** Advances past a field this codec does not read. */
203
+ static skipField(data, index, wireType) {
204
+ if (wireType === WIRE_TYPE_VARINT) {
205
+ return this.readVarint(data, index).newIndex;
206
+ }
207
+ if (wireType === WIRE_TYPE_BYTES) {
208
+ const { value: length, newIndex } = this.readVarint(data, index);
209
+ return newIndex + length;
210
+ }
211
+ return index + 1;
212
+ }
213
+ static parseRespGetData(data) {
214
+ // In proto3 an absent status defaults to 0, i.e. Success.
215
+ const result = { status: exports.RMakerLocalCtrlStatus.Success };
216
+ let index = 0;
217
+ while (index < data.length) {
218
+ const tag = data[index++];
219
+ const fieldNumber = tag >> 3;
220
+ const wireType = tag & 0x07;
221
+ if (fieldNumber === 1 && wireType === WIRE_TYPE_VARINT) {
222
+ const { value, newIndex } = this.readVarint(data, index);
223
+ result.status = value;
224
+ index = newIndex;
225
+ continue;
226
+ }
227
+ if (fieldNumber === 2 && wireType === WIRE_TYPE_BYTES) {
228
+ const { value: length, newIndex } = this.readVarint(data, index);
229
+ result.buf = this.parsePayloadBuf(data.slice(newIndex, newIndex + length));
230
+ index = newIndex + length;
231
+ continue;
232
+ }
233
+ index = this.skipField(data, index, wireType);
234
+ }
235
+ return result;
236
+ }
237
+ static parsePayloadBuf(data) {
238
+ const result = {
239
+ offset: 0,
240
+ payload: new Uint8Array(0),
241
+ totalLen: 0,
242
+ };
243
+ let index = 0;
244
+ while (index < data.length) {
245
+ const tag = data[index++];
246
+ const fieldNumber = tag >> 3;
247
+ const wireType = tag & 0x07;
248
+ if (fieldNumber === 1 && wireType === WIRE_TYPE_VARINT) {
249
+ const { value, newIndex } = this.readVarint(data, index);
250
+ result.offset = value;
251
+ index = newIndex;
252
+ continue;
253
+ }
254
+ if (fieldNumber === 2 && wireType === WIRE_TYPE_BYTES) {
255
+ const { value: length, newIndex } = this.readVarint(data, index);
256
+ result.payload = data.slice(newIndex, newIndex + length);
257
+ index = newIndex + length;
258
+ continue;
259
+ }
260
+ if (fieldNumber === 3 && wireType === WIRE_TYPE_VARINT) {
261
+ const { value, newIndex } = this.readVarint(data, index);
262
+ result.totalLen = value;
263
+ index = newIndex;
264
+ continue;
265
+ }
266
+ index = this.skipField(data, index, wireType);
267
+ }
268
+ return result;
269
+ }
270
+ }
271
+
272
+ exports.RMakerLocalCtrlProtoHelper = RMakerLocalCtrlProtoHelper;
273
+ //# sourceMappingURL=rmaker_local_ctrl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rmaker_local_ctrl.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -87,6 +87,19 @@ class ESPRMNeoMqtt {
87
87
  isConnected() {
88
88
  return this.#adapter.isConnected();
89
89
  }
90
+ /**
91
+ * Registers a transport connection-status listener when the underlying
92
+ * adapter supports it; otherwise returns a no-op unsubscribe.
93
+ *
94
+ * @param callback - Invoked with `{ connected }` on status changes
95
+ * @returns Unsubscribe function
96
+ */
97
+ onConnectionStatusChange(callback) {
98
+ if (typeof this.#adapter.onConnectionStatusChange === "function") {
99
+ return this.#adapter.onConnectionStatusChange(callback);
100
+ }
101
+ return () => { };
102
+ }
90
103
  /**
91
104
  * Publish a message to a topic.
92
105
  *
@@ -1 +1 @@
1
- {"version":3,"file":"ESPRMNeoMqtt.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"ESPRMNeoMqtt.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -14,9 +14,10 @@ var constants = require('../../../utils/constants.js');
14
14
  * adapter (see {@link ESPRMNeoBase.setLocalDiscoveryAdapter}).
15
15
  *
16
16
  * - With no discovery config, the default local protocol is used
17
- * (mDNS service `_esp_local_ctrl._tcp.` in the `local` domain).
18
- * - A custom {@link DiscoveryParamsInterface} can be supplied for other
19
- * discovery protocols.
17
+ * (mDNS service `_esp_rmaker_ctrl._tcp.` in the `local` domain — the
18
+ * service advertised by RainMaker Neo firmware).
19
+ * - A custom {@link DiscoveryParamsInterface} can be supplied to browse any
20
+ * other service type.
20
21
  */
21
22
  class ESPDiscoveryManager {
22
23
  /** Discovery parameters (service type / domain) passed to the adapter. */
@@ -31,7 +32,7 @@ class ESPDiscoveryManager {
31
32
  throw new Error("ESPLocalDiscoveryAdapter not set");
32
33
  }
33
34
  this.params = discoveryConfig || {
34
- serviceType: constants.ServiceType.ESP_LOCAL_CTRL_TCP,
35
+ serviceType: constants.ServiceType.ESP_RMAKER_LOCAL_CTRL_TCP,
35
36
  domain: discovery.ESPDiscoveryProtocol.local,
36
37
  };
37
38
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ESPDiscoveryManager.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"ESPDiscoveryManager.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -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;