@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.
- package/dist/cjs/ESPRMNeoBase.js +1 -1
- package/dist/cjs/index.js +11 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/proto/rmaker_local_ctrl.js +273 -0
- package/dist/cjs/proto/rmaker_local_ctrl.js.map +1 -0
- package/dist/cjs/services/ESPTransport/ESPDiscovery/ESPDiscoveryManager.js +5 -4
- package/dist/cjs/services/ESPTransport/ESPDiscovery/ESPDiscoveryManager.js.map +1 -1
- package/dist/cjs/services/ESPTransport/ESPLocalControlTransport.js +183 -116
- package/dist/cjs/services/ESPTransport/ESPLocalControlTransport.js.map +1 -1
- package/dist/cjs/services/ESPTransport/LocalControlSession.js +62 -0
- package/dist/cjs/services/ESPTransport/LocalControlSession.js.map +1 -0
- package/dist/cjs/types/transport.js +24 -0
- package/dist/cjs/types/transport.js.map +1 -1
- package/dist/cjs/utils/constants.js +70 -4
- package/dist/cjs/utils/constants.js.map +1 -1
- package/dist/cjs/utils/eventSubscriptionUtils.js +48 -4
- package/dist/cjs/utils/eventSubscriptionUtils.js.map +1 -1
- package/dist/esm/ESPRMNeoBase.js +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/proto/rmaker_local_ctrl.js +271 -0
- package/dist/esm/proto/rmaker_local_ctrl.js.map +1 -0
- package/dist/esm/services/ESPTransport/ESPDiscovery/ESPDiscoveryManager.js +5 -4
- package/dist/esm/services/ESPTransport/ESPDiscovery/ESPDiscoveryManager.js.map +1 -1
- package/dist/esm/services/ESPTransport/ESPLocalControlTransport.js +184 -117
- package/dist/esm/services/ESPTransport/ESPLocalControlTransport.js.map +1 -1
- package/dist/esm/services/ESPTransport/LocalControlSession.js +59 -0
- package/dist/esm/services/ESPTransport/LocalControlSession.js.map +1 -0
- package/dist/esm/types/transport.js +24 -1
- package/dist/esm/types/transport.js.map +1 -1
- package/dist/esm/utils/constants.js +65 -5
- package/dist/esm/utils/constants.js.map +1 -1
- package/dist/esm/utils/eventSubscriptionUtils.js +49 -5
- package/dist/esm/utils/eventSubscriptionUtils.js.map +1 -1
- package/dist/types/ESPRMNeoBase.d.ts +1 -1
- package/dist/types/proto/rmaker_local_ctrl.d.ts +137 -0
- package/dist/types/services/ESPTransport/ESPDiscovery/ESPDiscoveryManager.d.ts +4 -3
- package/dist/types/services/ESPTransport/ESPLocalControlTransport.d.ts +70 -23
- package/dist/types/services/ESPTransport/LocalControlSession.d.ts +24 -0
- package/dist/types/types/discovery.d.ts +1 -1
- package/dist/types/types/localControl.d.ts +26 -1
- package/dist/types/types/transport.d.ts +24 -2
- package/dist/types/utils/baseUtils.d.ts +1 -1
- package/dist/types/utils/constants.d.ts +65 -5
- package/dist/types/utils/eventSubscriptionUtils.d.ts +19 -3
- package/package.json +1 -1
- package/dist/cjs/proto/constants.js +0 -24
- package/dist/cjs/proto/constants.js.map +0 -1
- package/dist/cjs/proto/esp_local_ctrl.js +0 -787
- package/dist/cjs/proto/esp_local_ctrl.js.map +0 -1
- package/dist/esm/proto/constants.js +0 -24
- package/dist/esm/proto/constants.js.map +0 -1
- package/dist/esm/proto/esp_local_ctrl.js +0 -758
- package/dist/esm/proto/esp_local_ctrl.js.map +0 -1
- package/dist/types/proto/constants.d.ts +0 -15
- package/dist/types/proto/esp_local_ctrl.d.ts +0 -283
|
@@ -1,156 +1,223 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
await this.ensureConnected(
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
75
|
-
await this.ensureConnected(
|
|
76
|
-
return this.
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
// ──
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
/**
|
|
645
|
-
|
|
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
|
-
/**
|
|
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: {
|
|
50
|
+
metadata: {
|
|
51
|
+
baseUrl: info.baseUrl,
|
|
52
|
+
protocol: transport.ESPLocalControlProtocol.rmakerLocalCtrl,
|
|
53
|
+
...(capabilities && { capabilities }),
|
|
54
|
+
},
|
|
19
55
|
},
|
|
20
56
|
};
|
|
21
57
|
}
|
|
22
|
-
/**
|
|
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) =>
|
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/esm/ESPRMNeoBase.js
CHANGED
|
@@ -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 `
|
|
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.
|