@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
package/dist/esm/index.js CHANGED
@@ -15,7 +15,7 @@ export { ESPDevice } from './ESPDevice.js';
15
15
  export { ESPDiscoveryProtocol, ESPRMNeoEventType } from './types/discovery.js';
16
16
  export { ESPClaimStatus, ESPConnectStatus, ESPProvResponseStatus, ESPProvisionStatus, ESPSecurity, ESPTransport } from './types/provision.js';
17
17
  export { resolveNodeId } from './types/subscription.js';
18
- export { DEFAULT_TRANSPORT_ORDER, ESPTransportMode } from './types/transport.js';
18
+ export { DEFAULT_LOCAL_CONTROL_PROTOCOL, DEFAULT_TRANSPORT_ORDER, ESPLocalControlProtocol, ESPTransportMode } from './types/transport.js';
19
19
  import './methods/ESPRMNeoAuth/ChangePassword.js';
20
20
  import './methods/ESPRMNeoAuth/ConfirmSignUp.js';
21
21
  import './methods/ESPRMNeoAuth/ForgotPassword.js';
@@ -111,7 +111,7 @@ export { buildGroupControlParamsTopic, buildRainMakerUserParamsTopic, buildShado
111
111
  export { coerceToFiniteNumber, coerceToNonEmptyString, coerceToString } from './utils/coerce.js';
112
112
  export { buildCanonicalQueryString, createAuthorizationHeader, generateIotDeviceGatewayMqttSignedUrl, generateSigV4AuthHeader } from './utils/awsSigv4Utils.js';
113
113
  export { buildMainApiUrl, buildUserCredentialsUrl, ensureValidIdToken, extractTemporaryCredentials, isExpiredTokenAuthError, parseCredentialsErrorMessage, postUserCredentials, refreshUserSession } from './utils/awsUtils.js';
114
- export { APICallValidationErrorCodes, APIEndpoints, APIOperations, APIPathV1, AWSCredentialsErrorMessages, AssumeRoleErrorMessages, AuthErrorCodes, AuthSuccessMessages, AutomationStatusValues, AutomationSuccessMessages, CLAIM_CSR_MAX_CHUNKS, CLAIM_DEVICE_CSR_KEYS, CLAIM_DEVICE_MAC_KEYS, CLAIM_MAC_HEX_LENGTHS, CLAIM_MAC_SEPARATORS, CLAIM_MAX_FRAGMENT_SIZE, ClaimCapabilities, ClaimCapabilityPolicies, ClaimEndpoints, ClaimErrorCodes, ClaimProgressMessages, ConfigErrorCodes, DEFAULT_REST_API_VERSION, ESPProvProgressMessages, ESPRMNeoErrorCodes, ESPRMNeoStorageKeys, ESPServiceParamType, ESPServiceType, Endpoint, ErrorLabels, GroupSuccessMessages, GroupUserAliases, HTTPMethods, IntegrationSuccessMessages, NodeSuccessMessages, NodeWarnMessages, ProtocolType, ProvErrorCodes, ProvisionType, SDK_VERSION, ScheduleErrorMessages, ScheduleSuccessMessages, ServiceType, SharingSuccessMessages, StatusMessage, StorageAdapterErrorCodes, StorageKeys, SubscriptionChannelIds, TokenErrorCodes, TriggerErrorMessages, TriggerSuccessMessages, ValidationErrorCodes } from './utils/constants.js';
114
+ export { APICallValidationErrorCodes, APIEndpoints, APIOperations, APIPathV1, AWSCredentialsErrorMessages, AssumeRoleErrorMessages, AuthErrorCodes, AuthSuccessMessages, AutomationStatusValues, AutomationSuccessMessages, CLAIM_CSR_MAX_CHUNKS, CLAIM_DEVICE_CSR_KEYS, CLAIM_DEVICE_MAC_KEYS, CLAIM_MAC_HEX_LENGTHS, CLAIM_MAC_SEPARATORS, CLAIM_MAX_FRAGMENT_SIZE, ClaimCapabilities, ClaimCapabilityPolicies, ClaimEndpoints, ClaimErrorCodes, ClaimProgressMessages, ConfigErrorCodes, DEFAULT_REST_API_VERSION, ESPProvProgressMessages, ESPRMNeoErrorCodes, ESPRMNeoStorageKeys, ESPServiceParamType, ESPServiceType, Endpoint, ErrorLabels, GroupSuccessMessages, GroupUserAliases, HTTPMethods, IntegrationSuccessMessages, NodeSuccessMessages, NodeWarnMessages, ProtocolType, ProvErrorCodes, ProvisionType, RMAKER_LOCAL_CTRL_FRAGMENT_SIZE, RMAKER_LOCAL_CTRL_VERSION_KEY, RMakerLocalCtrlCapability, RMakerLocalCtrlEndpoint, RMakerLocalCtrlSetParamsStatus, RMakerLocalCtrlTxtKey, SDK_VERSION, ScheduleErrorMessages, ScheduleSuccessMessages, ServiceType, SharingSuccessMessages, StatusMessage, StorageAdapterErrorCodes, StorageKeys, SubscriptionChannelIds, TokenErrorCodes, TriggerErrorMessages, TriggerSuccessMessages, ValidationErrorCodes } from './utils/constants.js';
115
115
  export { LogLevel, Logger } from './utils/logger.js';
116
116
  export { concurrentFetchPool } from './utils/mapLimit.js';
117
117
  export { EventEmitter } from './utils/eventEmitter.js';
@@ -0,0 +1,271 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
3
+ *
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /**
7
+ * Codec for the `rmaker_local_ctrl` endpoint protocol, mirroring the firmware's
8
+ * `local_ctrl.proto` schema.
9
+ *
10
+ * Hand-rolled like {@link ClaimingProtoHelper} rather than generated: the
11
+ * schema is small, and generated `google-protobuf` modules cannot be pulled
12
+ * into a downstream Metro/RN bundle (see the note in `utils/export.ts`).
13
+ *
14
+ * Field numbers are the wire contract with deployed firmware — treat them as
15
+ * frozen:
16
+ * - `RMakerLocalCtrlPayload`: 1 = msg (varint), 10 = cmdGetData, 11 = respGetData
17
+ * - `CmdGetData`: 1 = DataType (varint), 2 = Offset (varint),
18
+ * 3 = Timestamp (varint), 4 = HasTimestamp (varint)
19
+ * - `RespGetData`: 1 = Status (varint), 2 = Buf
20
+ * - `PayloadBuf`: 1 = Offset (varint), 2 = Payload (bytes), 3 = TotalLen (varint)
21
+ *
22
+ * `Timestamp` / `HasTimestamp` are reserved for a future signed-response
23
+ * extension and are ignored by current firmware, so the encoder omits them.
24
+ *
25
+ * Only `get_params` / `get_config` use this schema; `set_params` carries raw
26
+ * JSON on the wire.
27
+ */
28
+ /**
29
+ * Status returned by the device for a data read.
30
+ */
31
+ var RMakerLocalCtrlStatus;
32
+ (function (RMakerLocalCtrlStatus) {
33
+ RMakerLocalCtrlStatus[RMakerLocalCtrlStatus["Success"] = 0] = "Success";
34
+ RMakerLocalCtrlStatus[RMakerLocalCtrlStatus["Fail"] = 1] = "Fail";
35
+ RMakerLocalCtrlStatus[RMakerLocalCtrlStatus["InvalidParam"] = 2] = "InvalidParam";
36
+ RMakerLocalCtrlStatus[RMakerLocalCtrlStatus["NoMemory"] = 3] = "NoMemory";
37
+ })(RMakerLocalCtrlStatus || (RMakerLocalCtrlStatus = {}));
38
+ /**
39
+ * Selects which document a read targets.
40
+ */
41
+ var RMakerLocalCtrlDataType;
42
+ (function (RMakerLocalCtrlDataType) {
43
+ /** The node's params JSON, as served by `get_params`. */
44
+ RMakerLocalCtrlDataType[RMakerLocalCtrlDataType["TypeParams"] = 0] = "TypeParams";
45
+ /** The node's config JSON, as served by `get_config`. */
46
+ RMakerLocalCtrlDataType[RMakerLocalCtrlDataType["TypeConfig"] = 1] = "TypeConfig";
47
+ })(RMakerLocalCtrlDataType || (RMakerLocalCtrlDataType = {}));
48
+ /**
49
+ * Message type discriminator carried on `RMakerLocalCtrlPayload.msg`.
50
+ */
51
+ var RMakerLocalCtrlMsgType;
52
+ (function (RMakerLocalCtrlMsgType) {
53
+ RMakerLocalCtrlMsgType[RMakerLocalCtrlMsgType["TypeCmdGetData"] = 0] = "TypeCmdGetData";
54
+ RMakerLocalCtrlMsgType[RMakerLocalCtrlMsgType["TypeRespGetData"] = 1] = "TypeRespGetData";
55
+ })(RMakerLocalCtrlMsgType || (RMakerLocalCtrlMsgType = {}));
56
+ /** Tag bytes written by the request encoder: `(fieldNumber << 3) | wireType`. */
57
+ const TAG_FIELD_1_VARINT = 0x08;
58
+ const TAG_FIELD_2_VARINT = 0x10;
59
+ const TAG_FIELD_10_BYTES = 0x52;
60
+ const WIRE_TYPE_VARINT = 0;
61
+ const WIRE_TYPE_BYTES = 2;
62
+ /**
63
+ * Encoder/decoder for `rmaker_local_ctrl` frames.
64
+ */
65
+ class RMakerLocalCtrlProtoHelper {
66
+ /**
67
+ * Builds a `CmdGetData` request for one fragment.
68
+ *
69
+ * An `offset` of 0 makes the device (re)generate and cache the document;
70
+ * subsequent offsets are served from that cache.
71
+ *
72
+ * @param dataType - Document to read.
73
+ * @param offset - Byte offset to read from.
74
+ * @returns The serialized request.
75
+ */
76
+ static createGetDataRequest(dataType, offset) {
77
+ const cmdGetData = this.concat([
78
+ // Both fields are written explicitly, including their proto3 zero values,
79
+ // matching how the reference clients frame this command.
80
+ new Uint8Array([TAG_FIELD_1_VARINT]),
81
+ this.encodeVarint(dataType),
82
+ new Uint8Array([TAG_FIELD_2_VARINT]),
83
+ this.encodeVarint(offset),
84
+ ]);
85
+ return this.concat([
86
+ new Uint8Array([TAG_FIELD_1_VARINT]),
87
+ this.encodeVarint(RMakerLocalCtrlMsgType.TypeCmdGetData),
88
+ new Uint8Array([TAG_FIELD_10_BYTES]),
89
+ this.encodeVarint(cmdGetData.length),
90
+ cmdGetData,
91
+ ]);
92
+ }
93
+ /**
94
+ * Parses a `RespGetData` frame from the device.
95
+ *
96
+ * @param data - Raw response bytes.
97
+ * @returns The parsed payload. Absent fields keep their proto3 defaults.
98
+ */
99
+ static parseGetDataResponse(data) {
100
+ const result = {
101
+ msg: RMakerLocalCtrlMsgType.TypeCmdGetData,
102
+ };
103
+ let index = 0;
104
+ while (index < data.length) {
105
+ const tag = data[index++];
106
+ const fieldNumber = tag >> 3;
107
+ const wireType = tag & 0x07;
108
+ if (fieldNumber === 1 && wireType === WIRE_TYPE_VARINT) {
109
+ const { value, newIndex } = this.readVarint(data, index);
110
+ result.msg = value;
111
+ index = newIndex;
112
+ continue;
113
+ }
114
+ if (fieldNumber === 11 && wireType === WIRE_TYPE_BYTES) {
115
+ const { value: length, newIndex } = this.readVarint(data, index);
116
+ result.respGetData = this.parseRespGetData(data.slice(newIndex, newIndex + length));
117
+ index = newIndex + length;
118
+ continue;
119
+ }
120
+ index = this.skipField(data, index, wireType);
121
+ }
122
+ return result;
123
+ }
124
+ /**
125
+ * Whether the device reported a successful read.
126
+ *
127
+ * @param response - Parsed response.
128
+ */
129
+ static isSuccess(response) {
130
+ return response.respGetData?.status === RMakerLocalCtrlStatus.Success;
131
+ }
132
+ /**
133
+ * Returns the device's status as its enum name, for diagnostics.
134
+ *
135
+ * @param response - Parsed response.
136
+ * @returns The status name, or `"Unknown"` when absent.
137
+ */
138
+ static getStatus(response) {
139
+ const status = response.respGetData?.status;
140
+ return status === undefined
141
+ ? "Unknown"
142
+ : (RMakerLocalCtrlStatus[status] ?? `Unknown(${status})`);
143
+ }
144
+ /**
145
+ * Offset the device answered with, which must match the requested offset.
146
+ *
147
+ * @param response - Parsed response.
148
+ */
149
+ static getOffset(response) {
150
+ return response.respGetData?.buf?.offset ?? 0;
151
+ }
152
+ /**
153
+ * Full document length reported by the device.
154
+ *
155
+ * @param response - Parsed response.
156
+ */
157
+ static getTotalLen(response) {
158
+ return response.respGetData?.buf?.totalLen ?? 0;
159
+ }
160
+ /**
161
+ * Fragment bytes carried by the response.
162
+ *
163
+ * @param response - Parsed response.
164
+ */
165
+ static getPayload(response) {
166
+ return response.respGetData?.buf?.payload ?? new Uint8Array(0);
167
+ }
168
+ // Private helper methods
169
+ static concat(parts) {
170
+ const totalLength = parts.reduce((sum, part) => sum + part.length, 0);
171
+ const result = new Uint8Array(totalLength);
172
+ let pos = 0;
173
+ for (const part of parts) {
174
+ result.set(part, pos);
175
+ pos += part.length;
176
+ }
177
+ return result;
178
+ }
179
+ static encodeVarint(value) {
180
+ const bytes = [];
181
+ let remaining = value;
182
+ while (remaining > 127) {
183
+ bytes.push((remaining & 0x7f) | 0x80);
184
+ remaining >>>= 7;
185
+ }
186
+ bytes.push(remaining & 0x7f);
187
+ return new Uint8Array(bytes);
188
+ }
189
+ static readVarint(data, index) {
190
+ let value = 0;
191
+ let shift = 0;
192
+ let byte;
193
+ do {
194
+ byte = data[index++];
195
+ value |= (byte & 0x7f) << shift;
196
+ shift += 7;
197
+ } while (byte & 0x80);
198
+ return { value, newIndex: index };
199
+ }
200
+ /** Advances past a field this codec does not read. */
201
+ static skipField(data, index, wireType) {
202
+ if (wireType === WIRE_TYPE_VARINT) {
203
+ return this.readVarint(data, index).newIndex;
204
+ }
205
+ if (wireType === WIRE_TYPE_BYTES) {
206
+ const { value: length, newIndex } = this.readVarint(data, index);
207
+ return newIndex + length;
208
+ }
209
+ return index + 1;
210
+ }
211
+ static parseRespGetData(data) {
212
+ // In proto3 an absent status defaults to 0, i.e. Success.
213
+ const result = { status: RMakerLocalCtrlStatus.Success };
214
+ let index = 0;
215
+ while (index < data.length) {
216
+ const tag = data[index++];
217
+ const fieldNumber = tag >> 3;
218
+ const wireType = tag & 0x07;
219
+ if (fieldNumber === 1 && wireType === WIRE_TYPE_VARINT) {
220
+ const { value, newIndex } = this.readVarint(data, index);
221
+ result.status = value;
222
+ index = newIndex;
223
+ continue;
224
+ }
225
+ if (fieldNumber === 2 && wireType === WIRE_TYPE_BYTES) {
226
+ const { value: length, newIndex } = this.readVarint(data, index);
227
+ result.buf = this.parsePayloadBuf(data.slice(newIndex, newIndex + length));
228
+ index = newIndex + length;
229
+ continue;
230
+ }
231
+ index = this.skipField(data, index, wireType);
232
+ }
233
+ return result;
234
+ }
235
+ static parsePayloadBuf(data) {
236
+ const result = {
237
+ offset: 0,
238
+ payload: new Uint8Array(0),
239
+ totalLen: 0,
240
+ };
241
+ let index = 0;
242
+ while (index < data.length) {
243
+ const tag = data[index++];
244
+ const fieldNumber = tag >> 3;
245
+ const wireType = tag & 0x07;
246
+ if (fieldNumber === 1 && wireType === WIRE_TYPE_VARINT) {
247
+ const { value, newIndex } = this.readVarint(data, index);
248
+ result.offset = value;
249
+ index = newIndex;
250
+ continue;
251
+ }
252
+ if (fieldNumber === 2 && wireType === WIRE_TYPE_BYTES) {
253
+ const { value: length, newIndex } = this.readVarint(data, index);
254
+ result.payload = data.slice(newIndex, newIndex + length);
255
+ index = newIndex + length;
256
+ continue;
257
+ }
258
+ if (fieldNumber === 3 && wireType === WIRE_TYPE_VARINT) {
259
+ const { value, newIndex } = this.readVarint(data, index);
260
+ result.totalLen = value;
261
+ index = newIndex;
262
+ continue;
263
+ }
264
+ index = this.skipField(data, index, wireType);
265
+ }
266
+ return result;
267
+ }
268
+ }
269
+
270
+ export { RMakerLocalCtrlDataType, RMakerLocalCtrlMsgType, RMakerLocalCtrlProtoHelper, RMakerLocalCtrlStatus };
271
+ //# sourceMappingURL=rmaker_local_ctrl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rmaker_local_ctrl.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -12,9 +12,10 @@ import { ServiceType } from '../../../utils/constants.js';
12
12
  * adapter (see {@link ESPRMNeoBase.setLocalDiscoveryAdapter}).
13
13
  *
14
14
  * - With no discovery config, the default local protocol is used
15
- * (mDNS service `_esp_local_ctrl._tcp.` in the `local` domain).
16
- * - A custom {@link DiscoveryParamsInterface} can be supplied for other
17
- * discovery protocols.
15
+ * (mDNS service `_esp_rmaker_ctrl._tcp.` in the `local` domain — the
16
+ * service advertised by RainMaker Neo firmware).
17
+ * - A custom {@link DiscoveryParamsInterface} can be supplied to browse any
18
+ * other service type.
18
19
  */
19
20
  class ESPDiscoveryManager {
20
21
  /** Discovery parameters (service type / domain) passed to the adapter. */
@@ -29,7 +30,7 @@ class ESPDiscoveryManager {
29
30
  throw new Error("ESPLocalDiscoveryAdapter not set");
30
31
  }
31
32
  this.params = discoveryConfig || {
32
- serviceType: ServiceType.ESP_LOCAL_CTRL_TCP,
33
+ serviceType: ServiceType.ESP_RMAKER_LOCAL_CTRL_TCP,
33
34
  domain: ESPDiscoveryProtocol.local,
34
35
  };
35
36
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ESPDiscoveryManager.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"ESPDiscoveryManager.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -1,154 +1,221 @@
1
- import { ESPRMNeoBase } from '../../ESPRMNeoBase.js';
2
- import { Status } from '../../proto/constants.js';
3
- import { LocalCtrlMessage, LocalCtrlMsgType, CmdSetPropertyValues, PropertyValue, CmdGetPropertyCount, CmdGetPropertyValues } from '../../proto/esp_local_ctrl.js';
4
- import { Endpoint } from '../../utils/constants.js';
1
+ import { RMakerLocalCtrlDataType, RMakerLocalCtrlProtoHelper } from '../../proto/rmaker_local_ctrl.js';
2
+ import { ESPLocalControlProtocol } from '../../types/transport.js';
3
+ import { RMakerLocalCtrlEndpoint, RMakerLocalCtrlSetParamsStatus, RMAKER_LOCAL_CTRL_VERSION_KEY } from '../../utils/constants.js';
5
4
  import { uint8ArrayToBase64, base64ToUint8Array } from '../ESPRMNeoHelpers/TransformEncoding.js';
5
+ import { getLocalControlAdapter, ensureLocalControlSession } from './LocalControlSession.js';
6
6
 
7
7
  /*
8
8
  * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
9
9
  *
10
10
  * SPDX-License-Identifier: Apache-2.0
11
11
  */
12
- /** Property index of the writable "params" property exposed by RMNeo local-control firmware. */
13
- const LOCAL_CONTROL_PARAMS_INDEX = 1;
14
12
  /**
15
- * Built-in `local` transport. Communicates with the node over the LAN through
16
- * the app-supplied {@link ESPRMNeoBase.ESPLocalControlAdapter} using the
17
- * `esp_local_ctrl` protobuf protocol. Connection metadata (`baseUrl`,
18
- * `securityType`, `pop`, and `username` for sec2) is supplied via the transport
19
- * config by {@link delegatedTransportHandler}.
13
+ * Tail of the in-flight fragmented-read chain, per node id.
14
+ *
15
+ * Module-level on purpose: {@link delegatedTransportHandler} builds a fresh
16
+ * transport for every call, so a queue held on the instance would serialize
17
+ * nothing. Keyed by node, because the transfer cache is per device.
18
+ */
19
+ const nodeReadQueues = new Map();
20
+ /**
21
+ * Runs `read` after every fragmented read already queued for `nodeId`.
22
+ *
23
+ * A failed read must not wedge the queue, so the chain advances on rejection
24
+ * too, while the caller still sees the original rejection. The map entry is
25
+ * dropped once the chain drains, so a long-lived app doesn't retain one promise
26
+ * per node it has ever talked to.
27
+ *
28
+ * @param nodeId - Node whose reads are being serialized.
29
+ * @param read - Pull loop to run once the node is free.
30
+ */
31
+ function enqueueNodeRead(nodeId, read) {
32
+ const previous = nodeReadQueues.get(nodeId) ?? Promise.resolve();
33
+ const result = previous.then(read, read);
34
+ const tail = result.then(() => undefined, () => undefined);
35
+ nodeReadQueues.set(nodeId, tail);
36
+ void tail.then(() => {
37
+ // Only the current tail clears the entry; otherwise a newer read owns it.
38
+ if (nodeReadQueues.get(nodeId) === tail) {
39
+ nodeReadQueues.delete(nodeId);
40
+ }
41
+ });
42
+ return result;
43
+ }
44
+ /** Session endpoints of the `rmaker_local_ctrl` protocol, passed to the adapter. */
45
+ const RMAKER_SESSION_OPTIONS = {
46
+ protocol: ESPLocalControlProtocol.rmakerLocalCtrl,
47
+ sessionPath: RMakerLocalCtrlEndpoint.SESSION,
48
+ versionPath: RMakerLocalCtrlEndpoint.VERSION,
49
+ versionKey: RMAKER_LOCAL_CTRL_VERSION_KEY,
50
+ };
51
+ /**
52
+ * The built-in `local` transport, speaking the `rmaker_local_ctrl` endpoint
53
+ * protocol over the app-supplied {@link ESPRMNeoBase.ESPLocalControlAdapter}:
54
+ *
55
+ * - `set_params` carries the same raw JSON body as a cloud set-params call and
56
+ * answers `{"status":"success"}` / `{"status":"fail","description":…}`.
57
+ * - `get_params` / `get_config` exchange protobuf `CmdGetData`/`RespGetData` and
58
+ * are fragmented — the client pulls fixed-size chunks by offset until
59
+ * `TotalLen` is covered.
60
+ *
61
+ * Connection metadata (`baseUrl`, `securityType`, `pop`, and `username` for
62
+ * sec2) is supplied via the transport config by {@link
63
+ * delegatedTransportHandler}; security 0 is not offered by this protocol.
20
64
  */
21
65
  class ESPLocalControlTransport {
22
- payload;
23
66
  metadata;
24
- propertyInfo = {};
25
67
  constructor(transportConfig) {
26
68
  this.metadata = transportConfig.metadata ?? {};
27
69
  }
28
70
  get adapter() {
29
- const adapter = ESPRMNeoBase.getLocalControlAdapter();
30
- if (!adapter) {
31
- throw new Error("Local control adapter is not configured");
32
- }
33
- return adapter;
34
- }
35
- /**
36
- * Connects to the node, retrying on failure. RMNeo's adapter resolves on a
37
- * successful connection and rejects otherwise, so a resolved call is treated
38
- * as connected.
39
- */
40
- async connectWithRetry(nodeId, baseUrl, securityType, pop, username, maxRetries = 3) {
41
- let attempt = 0;
42
- let lastError;
43
- while (attempt < maxRetries) {
44
- try {
45
- await this.adapter.connect(nodeId, baseUrl, securityType, pop, username);
46
- return;
47
- }
48
- catch (error) {
49
- lastError = error;
50
- attempt += 1;
51
- }
52
- }
53
- const message = lastError instanceof Error ? lastError.message : String(lastError);
54
- throw new Error(`Failed to connect after ${maxRetries} attempts: ${message}`);
71
+ return getLocalControlAdapter();
55
72
  }
56
73
  async ensureConnected(nodeId) {
57
- const isConnected = await this.adapter.isConnected(nodeId);
58
- if (!isConnected) {
59
- await this.connectWithRetry(nodeId, this.metadata.baseUrl, this.metadata.securityType ?? 0, this.metadata.pop, this.metadata.username);
60
- }
74
+ await ensureLocalControlSession(this.adapter, nodeId, this.metadata, RMAKER_SESSION_OPTIONS);
61
75
  }
76
+ /**
77
+ * Applies params over `set_params`.
78
+ *
79
+ * @param payload - `{ node_id, payload }`, where `payload` is the
80
+ * `{ <deviceOrServiceName>: { <paramName>: value } }` map to write.
81
+ * @throws {Error} When the device reports a non-success status.
82
+ */
62
83
  async setParam(payload, _nodeRef) {
63
- this.payload = payload;
64
- await this.ensureConnected(this.payload?.node_id);
65
- const success = await this.setProperty(this.payload?.payload ?? {});
66
- if (!success) {
67
- throw new Error("Failed to set device params over local control");
68
- }
84
+ const nodeId = payload?.node_id;
85
+ await this.ensureConnected(nodeId);
86
+ const request = uint8ArrayToBase64(new TextEncoder().encode(JSON.stringify(payload?.payload ?? {})));
87
+ const response = await this.adapter.sendData(nodeId, RMakerLocalCtrlEndpoint.SET_PARAMS, request);
88
+ this.assertSetParamsAccepted(response);
69
89
  return { message: "Parameters updated successfully", statusCode: 200 };
70
90
  }
91
+ /**
92
+ * Reads the node's full params document over `get_params`.
93
+ *
94
+ * @param payload - `{ node_id }` identifying the node to read.
95
+ * @returns The params JSON, keyed by device/service name.
96
+ */
71
97
  async getParams(payload, _nodeRef) {
72
- this.payload = payload;
73
- await this.ensureConnected(this.payload?.node_id);
74
- return this.getPropertyInfo();
98
+ const nodeId = payload?.node_id;
99
+ await this.ensureConnected(nodeId);
100
+ return this.readJsonDocument(nodeId, RMakerLocalCtrlDataType.TypeParams, RMakerLocalCtrlEndpoint.GET_PARAMS);
75
101
  }
76
- // ── set ──────────────────────────────────────────────────────────────────
77
- async setProperty(json) {
78
- const request = this.buildSetPropertyRequest(json);
79
- const response = await this.adapter.sendData(this.payload?.node_id, Endpoint.LOCAL_CTRL, request);
80
- return this.processSetPropertyResponse(response);
81
- }
82
- buildSetPropertyRequest(json) {
83
- const message = new LocalCtrlMessage();
84
- message.msg = LocalCtrlMsgType.TypeCmdSetPropertyValues;
85
- const cmd = new CmdSetPropertyValues();
86
- const prop = new PropertyValue();
87
- prop.index = LOCAL_CONTROL_PARAMS_INDEX;
88
- prop.value = new TextEncoder().encode(JSON.stringify(json ?? {}));
89
- cmd.props.push(prop);
90
- message.cmd_set_prop_vals = cmd;
91
- return uint8ArrayToBase64(message.serialize());
92
- }
93
- processSetPropertyResponse(response) {
94
- const deserialized = LocalCtrlMessage.deserialize(base64ToUint8Array(response));
95
- return deserialized.resp_set_prop_vals.status === Status.Success;
102
+ /**
103
+ * Reads the node's config document over `get_config`. Not part of
104
+ * {@link ESPTransportInterface} — the node config normally comes from the
105
+ * cloud; this serves LAN-only flows.
106
+ *
107
+ * @param nodeId - Node to read from.
108
+ * @returns The node config JSON.
109
+ */
110
+ async getConfig(nodeId) {
111
+ await this.ensureConnected(nodeId);
112
+ return this.readJsonDocument(nodeId, RMakerLocalCtrlDataType.TypeConfig, RMakerLocalCtrlEndpoint.GET_CONFIG);
96
113
  }
97
- // ── get ──────────────────────────────────────────────────────────────────
98
- /** Fetches the property count, then reads each property value into propertyInfo. */
99
- async getPropertyInfo() {
100
- this.propertyInfo = {};
101
- const count = await this.fetchPropertyCount();
102
- for (let index = 0; index < count; index++) {
103
- await this.fetchPropertyValue(index);
114
+ // ── set ──────────────────────────────────────────────────────────────────
115
+ /**
116
+ * Validates a `set_params` raw-JSON response.
117
+ *
118
+ * @throws {Error} When the body is unparseable or reports a failure.
119
+ */
120
+ assertSetParamsAccepted(response) {
121
+ const text = new TextDecoder().decode(base64ToUint8Array(response ?? ""));
122
+ let parsed;
123
+ try {
124
+ parsed = JSON.parse(text);
104
125
  }
105
- return this.propertyInfo;
106
- }
107
- async fetchPropertyCount() {
108
- const response = await this.adapter.sendData(this.payload?.node_id, Endpoint.LOCAL_CTRL, this.buildGetPropertyCountRequest());
109
- return this.processGetPropertyCountResponse(response);
110
- }
111
- buildGetPropertyCountRequest() {
112
- const request = new LocalCtrlMessage();
113
- request.msg = LocalCtrlMsgType.TypeCmdGetPropertyCount;
114
- request.cmd_get_prop_count = new CmdGetPropertyCount();
115
- return uint8ArrayToBase64(request.serialize());
116
- }
117
- processGetPropertyCountResponse(response) {
118
- const deserialized = LocalCtrlMessage.deserialize(base64ToUint8Array(response));
119
- if (deserialized.resp_get_prop_count.status !== Status.Success) {
120
- throw new Error("Failed to retrieve property count from device");
126
+ catch {
127
+ throw new Error(`Unexpected set_params response from device: ${text || "<empty>"}`);
128
+ }
129
+ if (parsed?.status !== RMakerLocalCtrlSetParamsStatus.SUCCESS) {
130
+ throw new Error(parsed?.description ??
131
+ "Failed to set device params over local control (rmaker_local_ctrl)");
121
132
  }
122
- return deserialized.resp_get_prop_count.count;
123
133
  }
124
- async fetchPropertyValue(index) {
125
- const response = await this.adapter.sendData(this.payload?.node_id, Endpoint.LOCAL_CTRL, this.buildGetPropertyValueRequest(index));
126
- const property = this.processGetPropertyValueResponse(response);
127
- if (property) {
128
- this.propertyInfo[property.name] = property.value;
134
+ // ── get ──────────────────────────────────────────────────────────────────
135
+ /**
136
+ * Pulls a fragmented document and parses it as JSON, serialized against every
137
+ * other fragmented read of the same node.
138
+ *
139
+ * The device holds **one global transfer cache**, not one per session: an
140
+ * offset-0 request regenerates it and it is freed after the last fragment. So
141
+ * a `getParams()` racing a `getConfig()` on the same node would clobber the
142
+ * other — the second offset-0 regenerates the cache mid-transfer, and the
143
+ * first read's next fragment comes back `Fail` (or, worse, carries bytes from
144
+ * the wrong document). The queue makes that interleaving impossible rather
145
+ * than detecting it after the fact.
146
+ *
147
+ * @param nodeId - Node to read from.
148
+ * @param dataType - Which document to read (params or config).
149
+ * @param endpoint - Endpoint serving that document.
150
+ * @throws {Error} When the device reports a failure, the response is
151
+ * malformed, or a fragment makes no forward progress.
152
+ */
153
+ readJsonDocument(nodeId, dataType, endpoint) {
154
+ return enqueueNodeRead(nodeId, () => this.pullJsonDocument(nodeId, dataType, endpoint));
155
+ }
156
+ /** The client-pull loop itself; always reached via {@link readJsonDocument}. */
157
+ async pullJsonDocument(nodeId, dataType, endpoint) {
158
+ const fragments = [];
159
+ let offset = 0;
160
+ let totalLength = 0;
161
+ do {
162
+ const response = await this.adapter.sendData(nodeId, endpoint, this.buildGetDataRequest(dataType, offset));
163
+ const buf = this.processGetDataResponse(response, offset);
164
+ totalLength = buf.totalLength;
165
+ if (totalLength === 0) {
166
+ return {};
167
+ }
168
+ if (buf.payload.length === 0) {
169
+ // Without forward progress the pull loop would never terminate.
170
+ throw new Error(`Device returned an empty fragment at offset ${offset} of ${totalLength} on ${endpoint}`);
171
+ }
172
+ fragments.push(buf.payload);
173
+ offset += buf.payload.length;
174
+ } while (offset < totalLength);
175
+ if (offset > totalLength) {
176
+ throw new Error(`Fragmented read of ${endpoint} overran: got ${offset} bytes, expected ${totalLength}`);
129
177
  }
178
+ return this.parseJsonFragments(fragments, endpoint);
130
179
  }
131
- buildGetPropertyValueRequest(index) {
132
- const request = new LocalCtrlMessage();
133
- request.msg = LocalCtrlMsgType.TypeCmdGetPropertyValues;
134
- const cmd = new CmdGetPropertyValues();
135
- cmd.indices.push(index);
136
- request.cmd_get_prop_vals = cmd;
137
- return uint8ArrayToBase64(request.serialize());
180
+ buildGetDataRequest(dataType, offset) {
181
+ return uint8ArrayToBase64(RMakerLocalCtrlProtoHelper.createGetDataRequest(dataType, offset));
138
182
  }
139
- processGetPropertyValueResponse(response) {
140
- const deserialized = LocalCtrlMessage.deserialize(base64ToUint8Array(response));
141
- if (deserialized.resp_get_prop_vals.status !== Status.Success) {
142
- throw new Error("Failed to get property values from device response");
183
+ /**
184
+ * Parses one `RespGetData` and checks it answers the requested offset.
185
+ *
186
+ * @param response - Base64 protobuf response from the adapter.
187
+ * @param requestedOffset - Offset asked for, used to detect a desynced pull.
188
+ */
189
+ processGetDataResponse(response, requestedOffset) {
190
+ const parsed = RMakerLocalCtrlProtoHelper.parseGetDataResponse(base64ToUint8Array(response));
191
+ if (!RMakerLocalCtrlProtoHelper.isSuccess(parsed)) {
192
+ throw new Error(`Device rejected the local-control read (status ${RMakerLocalCtrlProtoHelper.getStatus(parsed)})`);
193
+ }
194
+ const fragmentOffset = RMakerLocalCtrlProtoHelper.getOffset(parsed);
195
+ if (fragmentOffset !== requestedOffset) {
196
+ throw new Error(`Device answered offset ${fragmentOffset}, expected ${requestedOffset}`);
143
197
  }
144
- const prop = deserialized.resp_get_prop_vals.props[0];
145
- if (!prop)
146
- return undefined;
147
198
  return {
148
- name: prop.name || "unknown",
149
- value: JSON.parse(new TextDecoder().decode(prop.value)),
199
+ payload: RMakerLocalCtrlProtoHelper.getPayload(parsed),
200
+ totalLength: RMakerLocalCtrlProtoHelper.getTotalLen(parsed),
150
201
  };
151
202
  }
203
+ /** Joins the pulled fragments and parses the result as JSON. */
204
+ parseJsonFragments(fragments, endpoint) {
205
+ const joined = new Uint8Array(fragments.reduce((length, fragment) => length + fragment.length, 0));
206
+ let position = 0;
207
+ for (const fragment of fragments) {
208
+ joined.set(fragment, position);
209
+ position += fragment.length;
210
+ }
211
+ const text = new TextDecoder().decode(joined);
212
+ try {
213
+ return JSON.parse(text);
214
+ }
215
+ catch {
216
+ throw new Error(`Device returned malformed JSON on ${endpoint}`);
217
+ }
218
+ }
152
219
  }
153
220
 
154
221
  export { ESPLocalControlTransport };