@robotical/raftjs 2.1.0 → 2.1.3
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/devdocs/devbin-backwards-compatibility.md +105 -0
- package/devdocs/pseudocode-to-js-transpiler.md +563 -0
- package/dist/react-native/PseudocodeTranspiler.d.ts +6 -0
- package/dist/react-native/PseudocodeTranspiler.js +115 -0
- package/dist/react-native/PseudocodeTranspiler.js.map +1 -0
- package/dist/react-native/RaftAttributeHandler.d.ts +1 -1
- package/dist/react-native/RaftAttributeHandler.js +108 -32
- package/dist/react-native/RaftAttributeHandler.js.map +1 -1
- package/dist/react-native/RaftChannelBLE.web.d.ts +4 -0
- package/dist/react-native/RaftChannelBLE.web.js +59 -21
- package/dist/react-native/RaftChannelBLE.web.js.map +1 -1
- package/dist/react-native/RaftChannelSimulated.d.ts +1 -0
- package/dist/react-native/RaftChannelSimulated.js +9 -5
- package/dist/react-native/RaftChannelSimulated.js.map +1 -1
- package/dist/react-native/RaftChannelWebSocket.js +16 -1
- package/dist/react-native/RaftChannelWebSocket.js.map +1 -1
- package/dist/react-native/RaftConnector.d.ts +29 -1
- package/dist/react-native/RaftConnector.js +177 -11
- package/dist/react-native/RaftConnector.js.map +1 -1
- package/dist/react-native/RaftCustomAttrHandler.d.ts +2 -2
- package/dist/react-native/RaftCustomAttrHandler.js +32 -44
- package/dist/react-native/RaftCustomAttrHandler.js.map +1 -1
- package/dist/react-native/RaftDeviceInfo.d.ts +18 -0
- package/dist/react-native/RaftDeviceInfo.js +8 -0
- package/dist/react-native/RaftDeviceInfo.js.map +1 -1
- package/dist/react-native/RaftDeviceManager.d.ts +30 -3
- package/dist/react-native/RaftDeviceManager.js +618 -107
- package/dist/react-native/RaftDeviceManager.js.map +1 -1
- package/dist/react-native/RaftDeviceMgrIF.d.ts +11 -2
- package/dist/react-native/RaftDeviceStates.d.ts +27 -3
- package/dist/react-native/RaftDeviceStates.js +31 -6
- package/dist/react-native/RaftDeviceStates.js.map +1 -1
- package/dist/react-native/RaftFileHandler.d.ts +1 -1
- package/dist/react-native/RaftFileHandler.js +101 -34
- package/dist/react-native/RaftFileHandler.js.map +1 -1
- package/dist/react-native/RaftMicroPythonConsoleClient.d.ts +38 -0
- package/dist/react-native/RaftMicroPythonConsoleClient.js +45 -0
- package/dist/react-native/RaftMicroPythonConsoleClient.js.map +1 -0
- package/dist/react-native/RaftMsgHandler.d.ts +1 -1
- package/dist/react-native/RaftMsgHandler.js +6 -3
- package/dist/react-native/RaftMsgHandler.js.map +1 -1
- package/dist/react-native/RaftPublish.d.ts +2 -0
- package/dist/react-native/RaftPublish.js +81 -0
- package/dist/react-native/RaftPublish.js.map +1 -0
- package/dist/react-native/RaftStreamHandler.d.ts +11 -0
- package/dist/react-native/RaftStreamHandler.js +66 -0
- package/dist/react-native/RaftStreamHandler.js.map +1 -1
- package/dist/react-native/RaftStruct.d.ts +2 -2
- package/dist/react-native/RaftStruct.js +97 -26
- package/dist/react-native/RaftStruct.js.map +1 -1
- package/dist/react-native/RaftSystemType.d.ts +1 -0
- package/dist/react-native/RaftSystemUtils.d.ts +17 -1
- package/dist/react-native/RaftSystemUtils.js +51 -0
- package/dist/react-native/RaftSystemUtils.js.map +1 -1
- package/dist/react-native/RaftTimezone.d.ts +16 -0
- package/dist/react-native/RaftTimezone.js +153 -0
- package/dist/react-native/RaftTimezone.js.map +1 -0
- package/dist/react-native/RaftTypes.d.ts +46 -1
- package/dist/react-native/RaftTypes.js.map +1 -1
- package/dist/react-native/RaftUpdateManager.js +1 -1
- package/dist/react-native/RaftUpdateManager.js.map +1 -1
- package/dist/react-native/main.d.ts +3 -0
- package/dist/react-native/main.js +8 -1
- package/dist/react-native/main.js.map +1 -1
- package/dist/web/PseudocodeTranspiler.d.ts +6 -0
- package/dist/web/PseudocodeTranspiler.js +115 -0
- package/dist/web/PseudocodeTranspiler.js.map +1 -0
- package/dist/web/RaftAttributeHandler.d.ts +1 -1
- package/dist/web/RaftAttributeHandler.js +108 -32
- package/dist/web/RaftAttributeHandler.js.map +1 -1
- package/dist/web/RaftChannelBLE.web.d.ts +4 -0
- package/dist/web/RaftChannelBLE.web.js +59 -21
- package/dist/web/RaftChannelBLE.web.js.map +1 -1
- package/dist/web/RaftChannelSimulated.d.ts +1 -0
- package/dist/web/RaftChannelSimulated.js +9 -5
- package/dist/web/RaftChannelSimulated.js.map +1 -1
- package/dist/web/RaftChannelWebSocket.js +16 -1
- package/dist/web/RaftChannelWebSocket.js.map +1 -1
- package/dist/web/RaftConnector.d.ts +29 -1
- package/dist/web/RaftConnector.js +177 -11
- package/dist/web/RaftConnector.js.map +1 -1
- package/dist/web/RaftCustomAttrHandler.d.ts +2 -2
- package/dist/web/RaftCustomAttrHandler.js +32 -44
- package/dist/web/RaftCustomAttrHandler.js.map +1 -1
- package/dist/web/RaftDeviceInfo.d.ts +18 -0
- package/dist/web/RaftDeviceInfo.js +8 -0
- package/dist/web/RaftDeviceInfo.js.map +1 -1
- package/dist/web/RaftDeviceManager.d.ts +30 -3
- package/dist/web/RaftDeviceManager.js +618 -107
- package/dist/web/RaftDeviceManager.js.map +1 -1
- package/dist/web/RaftDeviceMgrIF.d.ts +11 -2
- package/dist/web/RaftDeviceStates.d.ts +27 -3
- package/dist/web/RaftDeviceStates.js +31 -6
- package/dist/web/RaftDeviceStates.js.map +1 -1
- package/dist/web/RaftFileHandler.d.ts +1 -1
- package/dist/web/RaftFileHandler.js +101 -34
- package/dist/web/RaftFileHandler.js.map +1 -1
- package/dist/web/RaftMicroPythonConsoleClient.d.ts +38 -0
- package/dist/web/RaftMicroPythonConsoleClient.js +45 -0
- package/dist/web/RaftMicroPythonConsoleClient.js.map +1 -0
- package/dist/web/RaftMsgHandler.d.ts +1 -1
- package/dist/web/RaftMsgHandler.js +6 -3
- package/dist/web/RaftMsgHandler.js.map +1 -1
- package/dist/web/RaftPublish.d.ts +2 -0
- package/dist/web/RaftPublish.js +81 -0
- package/dist/web/RaftPublish.js.map +1 -0
- package/dist/web/RaftStreamHandler.d.ts +11 -0
- package/dist/web/RaftStreamHandler.js +66 -0
- package/dist/web/RaftStreamHandler.js.map +1 -1
- package/dist/web/RaftStruct.d.ts +2 -2
- package/dist/web/RaftStruct.js +97 -26
- package/dist/web/RaftStruct.js.map +1 -1
- package/dist/web/RaftSystemType.d.ts +1 -0
- package/dist/web/RaftSystemUtils.d.ts +17 -1
- package/dist/web/RaftSystemUtils.js +51 -0
- package/dist/web/RaftSystemUtils.js.map +1 -1
- package/dist/web/RaftTimezone.d.ts +16 -0
- package/dist/web/RaftTimezone.js +153 -0
- package/dist/web/RaftTimezone.js.map +1 -0
- package/dist/web/RaftTypes.d.ts +46 -1
- package/dist/web/RaftTypes.js.map +1 -1
- package/dist/web/RaftUpdateManager.js +1 -1
- package/dist/web/RaftUpdateManager.js.map +1 -1
- package/dist/web/main.d.ts +3 -0
- package/dist/web/main.js +8 -1
- package/dist/web/main.js.map +1 -1
- package/examples/dashboard/package.json +2 -2
- package/examples/dashboard/src/DeviceActionsForm.tsx +177 -17
- package/examples/dashboard/src/DeviceLineChart.tsx +16 -3
- package/examples/dashboard/src/DevicePanel.tsx +92 -11
- package/examples/dashboard/src/DeviceSelectDialog.tsx +224 -0
- package/examples/dashboard/src/DeviceStatsPanel.tsx +76 -0
- package/examples/dashboard/src/DevicesPanel.tsx +11 -0
- package/examples/dashboard/src/LogConfigPanel.tsx +357 -0
- package/examples/dashboard/src/LogFilesPanel.tsx +200 -0
- package/examples/dashboard/src/LoggingPanel.tsx +264 -0
- package/examples/dashboard/src/Main.tsx +12 -2
- package/examples/dashboard/src/SettingsScreen.tsx +9 -4
- package/examples/dashboard/src/SystemTypeCog/CogStateInfo.ts +10 -3
- package/examples/dashboard/src/SystemTypeCog/SystemTypeCog.ts +37 -3
- package/examples/dashboard/src/SystemTypeGeneric/StateInfoGeneric.ts +10 -2
- package/examples/dashboard/src/SystemTypeGeneric/SystemTypeGeneric.ts +41 -7
- package/examples/dashboard/src/SystemTypeMarty/RICStateInfo.ts +34 -3
- package/examples/dashboard/src/styles.css +766 -1
- package/notes/web-ble-reconnect-retry.md +69 -0
- package/package.json +10 -7
- package/src/PseudocodeTranspiler.test.ts +372 -0
- package/src/PseudocodeTranspiler.ts +127 -0
- package/src/RaftAttributeHandler.ts +152 -76
- package/src/RaftChannelBLE.web.ts +62 -20
- package/src/RaftChannelSimulated.ts +10 -5
- package/src/RaftChannelWebSocket.ts +16 -2
- package/src/RaftConnector.ts +204 -17
- package/src/RaftCustomAttrHandler.ts +35 -45
- package/src/RaftDeviceInfo.ts +27 -0
- package/src/RaftDeviceManager.test.ts +164 -0
- package/src/RaftDeviceManager.ts +705 -127
- package/src/RaftDeviceMgrIF.ts +13 -2
- package/src/RaftDeviceStates.ts +49 -8
- package/src/RaftFileHandler.ts +112 -39
- package/src/RaftMicroPythonConsoleClient.ts +78 -0
- package/src/RaftMsgHandler.ts +8 -4
- package/src/RaftPublish.ts +92 -0
- package/src/RaftStreamHandler.ts +84 -1
- package/src/RaftStruct.test.ts +229 -0
- package/src/RaftStruct.ts +101 -37
- package/src/RaftSystemType.ts +1 -0
- package/src/RaftSystemUtils.ts +59 -0
- package/src/RaftTimezone.ts +151 -0
- package/src/RaftTypes.ts +57 -1
- package/src/RaftUpdateManager.ts +1 -1
- package/src/main.ts +3 -0
|
@@ -20,15 +20,16 @@ export default class AttributeHandler {
|
|
|
20
20
|
// Message timestamp size
|
|
21
21
|
private POLL_RESULT_TIMESTAMP_SIZE = 2;
|
|
22
22
|
private POLL_RESULT_WRAP_VALUE = this.POLL_RESULT_TIMESTAMP_SIZE === 2 ? 65536 : 4294967296;
|
|
23
|
-
private POLL_RESULT_RESOLUTION_US =
|
|
24
|
-
|
|
25
|
-
public processMsgAttrGroup(msgBuffer: Uint8Array, msgBufIdx: number, deviceTimeline: DeviceTimeline, pollRespMetadata: DeviceTypePollRespMetadata,
|
|
26
|
-
devAttrsState: DeviceAttributesState, maxDataPoints: number): number {
|
|
27
|
-
|
|
23
|
+
private POLL_RESULT_RESOLUTION_US = 100;
|
|
24
|
+
|
|
25
|
+
public processMsgAttrGroup(msgBuffer: Uint8Array, msgBufIdx: number, deviceTimeline: DeviceTimeline, pollRespMetadata: DeviceTypePollRespMetadata,
|
|
26
|
+
devAttrsState: DeviceAttributesState, maxDataPoints: number, msgEndIdx = msgBuffer.length): number {
|
|
27
|
+
|
|
28
28
|
// console.log(`processMsgAttrGroup msg ${msgHexStr} timestamp ${timestamp} origTimestamp ${origTimestamp} msgBufIdx ${msgBufIdx}`)
|
|
29
|
+
const boundedMsgEndIdx = Math.min(Math.max(msgEndIdx, msgBufIdx), msgBuffer.length);
|
|
29
30
|
|
|
30
31
|
// Extract msg timestamp
|
|
31
|
-
const { newBufIdx, timestampUs } = this.extractTimestampAndAdvanceIdx(msgBuffer, msgBufIdx, deviceTimeline);
|
|
32
|
+
const { newBufIdx, timestampUs } = this.extractTimestampAndAdvanceIdx(msgBuffer, msgBufIdx, deviceTimeline, boundedMsgEndIdx);
|
|
32
33
|
if (newBufIdx < 0)
|
|
33
34
|
return -1;
|
|
34
35
|
msgBufIdx = newBufIdx;
|
|
@@ -37,11 +38,38 @@ export default class AttributeHandler {
|
|
|
37
38
|
const msgDataStartIdx = msgBufIdx;
|
|
38
39
|
|
|
39
40
|
// New attribute values (in order as they appear in the attributes JSON)
|
|
40
|
-
let newAttrValues: number[][] = [];
|
|
41
|
+
let newAttrValues: (number | string)[][] = [];
|
|
41
42
|
if ("c" in pollRespMetadata) {
|
|
42
43
|
|
|
43
44
|
// Extract attribute values using custom handler
|
|
44
|
-
newAttrValues = this._customAttrHandler.handleAttr(pollRespMetadata, msgBuffer, msgBufIdx);
|
|
45
|
+
newAttrValues = this._customAttrHandler.handleAttr(pollRespMetadata, msgBuffer, msgBufIdx, boundedMsgEndIdx);
|
|
46
|
+
|
|
47
|
+
// Apply per-attribute transforms that the custom handler doesn't handle
|
|
48
|
+
for (let attrIdx = 0; attrIdx < pollRespMetadata.a.length && attrIdx < newAttrValues.length; attrIdx++) {
|
|
49
|
+
const attrDef = pollRespMetadata.a[attrIdx];
|
|
50
|
+
if (newAttrValues[attrIdx].length === 0) continue;
|
|
51
|
+
|
|
52
|
+
// Sign-extend values for signed types — the pseudocode produces unsigned
|
|
53
|
+
// values from bitwise operations, but the attribute type declares signedness
|
|
54
|
+
if (attrDef.t && isAttrTypeSigned(attrDef.t)) {
|
|
55
|
+
const byteWidth = structSizeOf(attrDef.t);
|
|
56
|
+
const signBit = 1 << (byteWidth * 8 - 1);
|
|
57
|
+
const range = signBit * 2;
|
|
58
|
+
newAttrValues[attrIdx] = newAttrValues[attrIdx].map(v => {
|
|
59
|
+
const n = v as number;
|
|
60
|
+
return (n & signBit) ? n - range : n;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if ("d" in attrDef && attrDef.d) {
|
|
65
|
+
const divisor = attrDef.d as number;
|
|
66
|
+
newAttrValues[attrIdx] = newAttrValues[attrIdx].map(v => (v as number) / divisor);
|
|
67
|
+
}
|
|
68
|
+
if ("a" in attrDef && attrDef.a !== undefined) {
|
|
69
|
+
const addend = attrDef.a as number;
|
|
70
|
+
newAttrValues[attrIdx] = newAttrValues[attrIdx].map(v => (v as number) + addend);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
45
73
|
|
|
46
74
|
} else {
|
|
47
75
|
|
|
@@ -61,16 +89,15 @@ export default class AttributeHandler {
|
|
|
61
89
|
// console.log(`RaftAttrHdlr.processMsgAttrGroup attr ${attrDef.n} msgBufIdx ${msgBufIdx} timestampUs ${timestampUs} attrDef ${JSON.stringify(attrDef)}`);
|
|
62
90
|
|
|
63
91
|
// Process the attribute
|
|
64
|
-
const { values, newMsgBufIdx } = this.processMsgAttribute(attrDef, msgBuffer, msgBufIdx, msgDataStartIdx);
|
|
92
|
+
const { values, newMsgBufIdx } = this.processMsgAttribute(attrDef, msgBuffer, msgBufIdx, msgDataStartIdx, boundedMsgEndIdx);
|
|
65
93
|
if (newMsgBufIdx < 0) {
|
|
66
|
-
|
|
67
|
-
continue;
|
|
94
|
+
return -1;
|
|
68
95
|
}
|
|
69
96
|
msgBufIdx = newMsgBufIdx;
|
|
70
97
|
newAttrValues.push(values);
|
|
71
98
|
}
|
|
72
99
|
}
|
|
73
|
-
|
|
100
|
+
|
|
74
101
|
// Number of bytes in group
|
|
75
102
|
let pollRespSizeBytes = msgBufIdx - msgDataStartIdx;
|
|
76
103
|
if (pollRespSizeBytes < pollRespMetadata.b) {
|
|
@@ -80,7 +107,7 @@ export default class AttributeHandler {
|
|
|
80
107
|
// Check if any attributes were added (in addition to timestamp)
|
|
81
108
|
if (newAttrValues.length === 0) {
|
|
82
109
|
console.warn(`DeviceManager msg attrGroup ${JSON.stringify(pollRespMetadata)} newAttrValues ${newAttrValues} is empty`);
|
|
83
|
-
return
|
|
110
|
+
return -1;
|
|
84
111
|
}
|
|
85
112
|
|
|
86
113
|
// All attributes must have the same number of new values
|
|
@@ -129,13 +156,56 @@ export default class AttributeHandler {
|
|
|
129
156
|
devAttrsState[attrDef.n].numNewValues = newAttrValues[attrIdx].length;
|
|
130
157
|
}
|
|
131
158
|
|
|
132
|
-
//
|
|
159
|
+
// --- Piecewise EMA timestamp reconstruction ---
|
|
160
|
+
// Track the last assigned sample timestamp and step forward by emaIntervalUs
|
|
161
|
+
// per sample. No T0 recomputation — avoids amplified batch-boundary jitter.
|
|
133
162
|
const timeIncUs: number = pollRespMetadata.us ? pollRespMetadata.us : 1000;
|
|
163
|
+
|
|
164
|
+
if (!deviceTimeline.emaCalibrated) {
|
|
165
|
+
// Cold start: anchor so that sample 0 gets the poll timestamp
|
|
166
|
+
deviceTimeline.emaIntervalUs = timeIncUs;
|
|
167
|
+
deviceTimeline.emaLastSampleTimeUs = timestampUs - deviceTimeline.emaIntervalUs;
|
|
168
|
+
deviceTimeline.emaPrevPollTimeUs = timestampUs;
|
|
169
|
+
deviceTimeline.emaCalibrated = true;
|
|
170
|
+
deviceTimeline.emaCalibrationPolls = 1;
|
|
171
|
+
} else {
|
|
172
|
+
// EMA interval update from poll-to-poll gap
|
|
173
|
+
if (numNewDataPoints > 1) {
|
|
174
|
+
const instantIntervalUs = (timestampUs - deviceTimeline.emaPrevPollTimeUs) / numNewDataPoints;
|
|
175
|
+
const alpha = deviceTimeline.emaCalibrationPolls < 20 ? 0.3 : 0.05;
|
|
176
|
+
deviceTimeline.emaIntervalUs = alpha * instantIntervalUs
|
|
177
|
+
+ (1.0 - alpha) * deviceTimeline.emaIntervalUs;
|
|
178
|
+
} else if (numNewDataPoints === 1) {
|
|
179
|
+
const instantIntervalUs = timestampUs - deviceTimeline.emaPrevPollTimeUs;
|
|
180
|
+
if (Number.isFinite(instantIntervalUs) && instantIntervalUs > 0) {
|
|
181
|
+
deviceTimeline.emaIntervalUs = instantIntervalUs;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
deviceTimeline.emaPrevPollTimeUs = timestampUs;
|
|
185
|
+
deviceTimeline.emaCalibrationPolls++;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Assign timestamps: each sample steps forward by emaIntervalUs
|
|
134
189
|
const timestampsUs = Array(numNewDataPoints).fill(0);
|
|
190
|
+
const lastTimeUs = deviceTimeline.timestampsUs.length > 0
|
|
191
|
+
? deviceTimeline.timestampsUs[deviceTimeline.timestampsUs.length - 1]
|
|
192
|
+
: -Infinity;
|
|
135
193
|
for (let i = 0; i < numNewDataPoints; i++) {
|
|
136
|
-
timestampsUs[i] =
|
|
194
|
+
timestampsUs[i] = numNewDataPoints === 1
|
|
195
|
+
? timestampUs
|
|
196
|
+
: deviceTimeline.emaLastSampleTimeUs + (i + 1) * deviceTimeline.emaIntervalUs;
|
|
197
|
+
// Ensure monotonically increasing timestamps
|
|
198
|
+
if (i === 0 && timestampsUs[0] <= lastTimeUs) {
|
|
199
|
+
timestampsUs[0] = lastTimeUs + 1;
|
|
200
|
+
} else if (i > 0 && timestampsUs[i] <= timestampsUs[i - 1]) {
|
|
201
|
+
timestampsUs[i] = timestampsUs[i - 1] + 1;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Advance the piecewise model cursor past all samples in this batch
|
|
205
|
+
if (deviceTimeline.emaCalibrated && numNewDataPoints > 0) {
|
|
206
|
+
deviceTimeline.emaLastSampleTimeUs = timestampsUs[timestampsUs.length - 1];
|
|
137
207
|
}
|
|
138
|
-
|
|
208
|
+
|
|
139
209
|
// Check if timeline points need to be discarded
|
|
140
210
|
const discardCount = Math.max(0, deviceTimeline.timestampsUs.length + timestampsUs.length - maxDataPoints);
|
|
141
211
|
if (discardCount > 0) {
|
|
@@ -144,6 +214,7 @@ export default class AttributeHandler {
|
|
|
144
214
|
|
|
145
215
|
// Add the new timestamps
|
|
146
216
|
deviceTimeline.timestampsUs.push(...timestampsUs);
|
|
217
|
+
deviceTimeline.totalSamplesAdded += numNewDataPoints;
|
|
147
218
|
|
|
148
219
|
// Validate attributes based on the vft field
|
|
149
220
|
this.validateAttributes(pollRespMetadata, devAttrsState, numNewDataPoints);
|
|
@@ -156,7 +227,7 @@ export default class AttributeHandler {
|
|
|
156
227
|
// Iterate through all attributes to find those with a vft field
|
|
157
228
|
for (let attrIdx = 0; attrIdx < pollRespMetadata.a.length; attrIdx++) {
|
|
158
229
|
const attrDef: DeviceTypeAttribute = pollRespMetadata.a[attrIdx];
|
|
159
|
-
|
|
230
|
+
|
|
160
231
|
// Check if this attribute has a vft field
|
|
161
232
|
if (!("vft" in attrDef) || !attrDef.vft) {
|
|
162
233
|
continue;
|
|
@@ -183,7 +254,7 @@ export default class AttributeHandler {
|
|
|
183
254
|
// Get the most recent values from both attributes
|
|
184
255
|
const numValues = currentAttr.values.length;
|
|
185
256
|
const startIdx = numValues - numNewDataPoints;
|
|
186
|
-
|
|
257
|
+
|
|
187
258
|
// Process each of the new values
|
|
188
259
|
for (let i = 0; i < numNewDataPoints; i++) {
|
|
189
260
|
const valueIdx = startIdx + i;
|
|
@@ -201,34 +272,39 @@ export default class AttributeHandler {
|
|
|
201
272
|
}
|
|
202
273
|
}
|
|
203
274
|
|
|
204
|
-
private processMsgAttribute(attrDef: DeviceTypeAttribute, msgBuffer: Uint8Array, msgBufIdx: number, msgDataStartIdx: number): { values: number[], newMsgBufIdx: number} {
|
|
275
|
+
private processMsgAttribute(attrDef: DeviceTypeAttribute, msgBuffer: Uint8Array, msgBufIdx: number, msgDataStartIdx: number, msgEndIdx: number): { values: (number | string)[], newMsgBufIdx: number} {
|
|
205
276
|
|
|
206
277
|
// Current field message string index
|
|
207
278
|
let curFieldBufIdx = msgBufIdx;
|
|
208
279
|
let attrUsesAbsPos = false;
|
|
280
|
+
const boundedMsgEndIdx = Math.min(Math.max(msgEndIdx, msgDataStartIdx), msgBuffer.length);
|
|
281
|
+
const attrTypesOnly = attrDef.t;
|
|
282
|
+
const numBytesConsumed = structSizeOf(attrTypesOnly);
|
|
209
283
|
|
|
210
284
|
// Check for "at" field which means absolute position in the buffer
|
|
211
285
|
if (attrDef.at !== undefined) {
|
|
212
286
|
// Handle both single value and array of byte positions
|
|
213
287
|
if (Array.isArray(attrDef.at)) {
|
|
214
288
|
// Create a new buffer for non-contiguous data extraction
|
|
215
|
-
const elemSize =
|
|
289
|
+
const elemSize = numBytesConsumed;
|
|
216
290
|
const bytesForType = new Uint8Array(elemSize);
|
|
217
|
-
|
|
291
|
+
|
|
218
292
|
// Zero out the buffer
|
|
219
293
|
bytesForType.fill(0);
|
|
220
|
-
|
|
294
|
+
|
|
221
295
|
// Copy bytes from the specified positions
|
|
222
296
|
for (let i = 0; i < attrDef.at.length && i < elemSize; i++) {
|
|
223
297
|
const sourceIdx = msgDataStartIdx + attrDef.at[i];
|
|
224
|
-
if (sourceIdx
|
|
225
|
-
|
|
298
|
+
if (sourceIdx >= boundedMsgEndIdx) {
|
|
299
|
+
return { values: [], newMsgBufIdx: -1 };
|
|
226
300
|
}
|
|
301
|
+
bytesForType[i] = msgBuffer[sourceIdx];
|
|
227
302
|
}
|
|
228
|
-
|
|
303
|
+
|
|
229
304
|
// Use this buffer for attribute extraction
|
|
230
305
|
msgBuffer = bytesForType;
|
|
231
306
|
curFieldBufIdx = 0;
|
|
307
|
+
msgEndIdx = bytesForType.length;
|
|
232
308
|
} else {
|
|
233
309
|
// Standard absolute position in the buffer
|
|
234
310
|
curFieldBufIdx = msgDataStartIdx + attrDef.at;
|
|
@@ -237,26 +313,25 @@ export default class AttributeHandler {
|
|
|
237
313
|
}
|
|
238
314
|
|
|
239
315
|
// Check if outside bounds of message
|
|
240
|
-
|
|
316
|
+
const attrEndIdx = curFieldBufIdx + numBytesConsumed;
|
|
317
|
+
const effectiveMsgEndIdx = Math.min(Math.max(msgEndIdx, curFieldBufIdx), msgBuffer.length);
|
|
318
|
+
if (curFieldBufIdx >= effectiveMsgEndIdx || attrEndIdx > effectiveMsgEndIdx) {
|
|
241
319
|
// console.warn(`DeviceManager msg outside bounds msgBuffer ${msgBuffer} attrName ${attrDef.n}`);
|
|
242
320
|
return { values: [], newMsgBufIdx: -1 };
|
|
243
321
|
}
|
|
244
322
|
|
|
245
|
-
// Attribute type
|
|
246
|
-
const attrTypesOnly = attrDef.t;
|
|
247
|
-
|
|
248
323
|
// Slice into buffer
|
|
249
|
-
const attrBuf = msgBuffer.slice(curFieldBufIdx);
|
|
250
|
-
|
|
324
|
+
const attrBuf = msgBuffer.slice(curFieldBufIdx, effectiveMsgEndIdx);
|
|
325
|
+
|
|
251
326
|
// Check if a mask is used and the value is signed
|
|
252
327
|
const maskOnSignedValue = "m" in attrDef && isAttrTypeSigned(attrTypesOnly);
|
|
253
328
|
|
|
254
329
|
// Extract the value using python-struct
|
|
255
330
|
const unpackValues = structUnpack(maskOnSignedValue ? attrTypesOnly.toUpperCase() : attrTypesOnly, attrBuf);
|
|
256
|
-
let attrValues = unpackValues as number[];
|
|
331
|
+
let attrValues = unpackValues as (number | string)[];
|
|
257
332
|
|
|
258
|
-
//
|
|
259
|
-
const
|
|
333
|
+
// Check if any values are strings (from 's' format) — skip numeric transforms for those
|
|
334
|
+
const hasStringValues = attrValues.some(v => typeof v === 'string');
|
|
260
335
|
|
|
261
336
|
// // Check if sign extendable mask specified on signed value
|
|
262
337
|
// if (mmSpecifiedOnSignedValue) {
|
|
@@ -270,82 +345,82 @@ export default class AttributeHandler {
|
|
|
270
345
|
// }
|
|
271
346
|
|
|
272
347
|
// Check for XOR mask
|
|
273
|
-
if ("x" in attrDef) {
|
|
348
|
+
if (!hasStringValues && "x" in attrDef) {
|
|
274
349
|
const mask = typeof attrDef.x === "string" ? parseInt(attrDef.x, 16) : attrDef.x as number;
|
|
275
|
-
attrValues = attrValues.map((value) => (value >>> 0) ^ mask);
|
|
350
|
+
attrValues = attrValues.map((value) => ((value as number) >>> 0) ^ mask);
|
|
276
351
|
}
|
|
277
|
-
|
|
352
|
+
|
|
278
353
|
// Check for AND mask
|
|
279
|
-
if ("m" in attrDef) {
|
|
354
|
+
if (!hasStringValues && "m" in attrDef) {
|
|
280
355
|
const mask = typeof attrDef.m === "string" ? parseInt(attrDef.m, 16) : attrDef.m as number;
|
|
281
|
-
attrValues = attrValues.map((value) => (maskOnSignedValue ? this.signExtend(value, mask) : (value >>> 0) & mask));
|
|
356
|
+
attrValues = attrValues.map((value) => (maskOnSignedValue ? this.signExtend(value as number, mask) : ((value as number) >>> 0) & mask));
|
|
282
357
|
}
|
|
283
358
|
|
|
284
359
|
// Check for a sign-bit
|
|
285
|
-
if ("sb" in attrDef) {
|
|
360
|
+
if (!hasStringValues && "sb" in attrDef) {
|
|
286
361
|
const signBitPos = attrDef.sb as number;
|
|
287
362
|
const signBitMask = 1 << signBitPos;
|
|
288
363
|
if ("ss" in attrDef) {
|
|
289
364
|
const signBitSubtract = attrDef.ss as number;
|
|
290
|
-
attrValues = attrValues.map((value) => (value & signBitMask) ? signBitSubtract - value : value);
|
|
365
|
+
attrValues = attrValues.map((value) => ((value as number) & signBitMask) ? signBitSubtract - (value as number) : value);
|
|
291
366
|
} else {
|
|
292
|
-
attrValues = attrValues.map((value) => (value & signBitMask) ? value - (signBitMask << 1) : value);
|
|
367
|
+
attrValues = attrValues.map((value) => ((value as number) & signBitMask) ? (value as number) - (signBitMask << 1) : value);
|
|
293
368
|
}
|
|
294
369
|
}
|
|
295
370
|
|
|
296
371
|
// Check for bit shift required
|
|
297
|
-
if ("s" in attrDef && attrDef.s) {
|
|
372
|
+
if (!hasStringValues && "s" in attrDef && attrDef.s) {
|
|
298
373
|
const bitshift = attrDef.s as number;
|
|
299
374
|
if (bitshift > 0) {
|
|
300
|
-
attrValues = attrValues.map((value) => (value >>> 0) >>> bitshift);
|
|
375
|
+
attrValues = attrValues.map((value) => ((value as number) >>> 0) >>> bitshift);
|
|
301
376
|
} else if (bitshift < 0) {
|
|
302
|
-
attrValues = attrValues.map((value) => (value >>> 0) << -bitshift);
|
|
377
|
+
attrValues = attrValues.map((value) => ((value as number) >>> 0) << -bitshift);
|
|
303
378
|
}
|
|
304
379
|
}
|
|
305
380
|
|
|
306
381
|
// Check for divisor
|
|
307
|
-
if ("d" in attrDef && attrDef.d) {
|
|
382
|
+
if (!hasStringValues && "d" in attrDef && attrDef.d) {
|
|
308
383
|
const divisor = attrDef.d as number;
|
|
309
|
-
attrValues = attrValues.map((value) => (value) / divisor);
|
|
384
|
+
attrValues = attrValues.map((value) => (value as number) / divisor);
|
|
310
385
|
}
|
|
311
386
|
|
|
312
387
|
// Check for value to add
|
|
313
|
-
if ("a" in attrDef && attrDef.a !== undefined) {
|
|
388
|
+
if (!hasStringValues && "a" in attrDef && attrDef.a !== undefined) {
|
|
314
389
|
const addValue = attrDef.a as number;
|
|
315
|
-
attrValues = attrValues.map((value) => (value) + addValue);
|
|
390
|
+
attrValues = attrValues.map((value) => (value as number) + addValue);
|
|
316
391
|
}
|
|
317
392
|
|
|
318
393
|
// Apply lookup table if defined
|
|
319
|
-
if ("lut" in attrDef && attrDef.lut !== undefined) {
|
|
394
|
+
if (!hasStringValues && "lut" in attrDef && attrDef.lut !== undefined) {
|
|
320
395
|
attrValues = attrValues.map((value): number => {
|
|
321
396
|
// Skip NaN values
|
|
322
|
-
if (isNaN(value)) {
|
|
323
|
-
return value;
|
|
397
|
+
if (isNaN(value as number)) {
|
|
398
|
+
return value as number;
|
|
324
399
|
}
|
|
325
400
|
|
|
326
401
|
// Search through the lookup table rows for a match
|
|
327
402
|
let defaultValue: number | null = null;
|
|
328
|
-
|
|
403
|
+
|
|
329
404
|
for (const row of attrDef.lut || []) {
|
|
330
405
|
// Empty string means default for unmatched values
|
|
331
406
|
if (row.r === "") {
|
|
332
407
|
defaultValue = row.v;
|
|
333
408
|
continue;
|
|
334
409
|
}
|
|
335
|
-
|
|
410
|
+
|
|
336
411
|
// Parse the range string
|
|
337
|
-
if (this.isValueInRangeString(value, row.r)) {
|
|
412
|
+
if (this.isValueInRangeString(value as number, row.r)) {
|
|
338
413
|
return row.v;
|
|
339
414
|
}
|
|
340
415
|
}
|
|
341
|
-
|
|
416
|
+
|
|
342
417
|
// If no match found but we have a default, use it
|
|
343
418
|
if (defaultValue !== null) {
|
|
344
419
|
return defaultValue;
|
|
345
420
|
}
|
|
346
|
-
|
|
421
|
+
|
|
347
422
|
// Otherwise keep the original value
|
|
348
|
-
return value;
|
|
423
|
+
return value as number;
|
|
349
424
|
});
|
|
350
425
|
}
|
|
351
426
|
|
|
@@ -363,38 +438,39 @@ export default class AttributeHandler {
|
|
|
363
438
|
// Return the value
|
|
364
439
|
return { values: attrValues, newMsgBufIdx: msgBufIdx };
|
|
365
440
|
}
|
|
366
|
-
|
|
441
|
+
|
|
367
442
|
private signExtend(value: number, mask: number): number {
|
|
368
443
|
const signBitMask = (mask + 1) >> 1;
|
|
369
444
|
const signBit = value & signBitMask;
|
|
370
|
-
|
|
445
|
+
|
|
371
446
|
if (signBit !== 0) { // If sign bit is set
|
|
372
447
|
const highBitsMask = ~mask & ~((mask + 1) >> 1);
|
|
373
448
|
value |= highBitsMask; // Apply the sign extension
|
|
374
449
|
}
|
|
375
|
-
|
|
450
|
+
|
|
376
451
|
return value;
|
|
377
452
|
}
|
|
378
453
|
|
|
379
|
-
private extractTimestampAndAdvanceIdx(msgBuffer: Uint8Array, msgBufIdx: number, timestampWrapHandler: DeviceTimeline):
|
|
454
|
+
private extractTimestampAndAdvanceIdx(msgBuffer: Uint8Array, msgBufIdx: number, timestampWrapHandler: DeviceTimeline, msgEndIdx = msgBuffer.length):
|
|
380
455
|
{ newBufIdx: number, timestampUs: number } {
|
|
456
|
+
const boundedMsgEndIdx = Math.min(Math.max(msgEndIdx, msgBufIdx), msgBuffer.length);
|
|
381
457
|
|
|
382
458
|
// Check there are enough bytes for the timestamp
|
|
383
|
-
if (msgBufIdx + this.POLL_RESULT_TIMESTAMP_SIZE >
|
|
459
|
+
if (msgBufIdx + this.POLL_RESULT_TIMESTAMP_SIZE > boundedMsgEndIdx) {
|
|
384
460
|
return { newBufIdx: -1, timestampUs: 0 };
|
|
385
461
|
}
|
|
386
462
|
|
|
387
463
|
// Use struct to extract the timestamp
|
|
388
464
|
const tsBuffer = msgBuffer.slice(msgBufIdx, msgBufIdx + this.POLL_RESULT_TIMESTAMP_SIZE);
|
|
389
465
|
let timestampUs: number;
|
|
390
|
-
if (this.POLL_RESULT_TIMESTAMP_SIZE === 2) {
|
|
466
|
+
if (this.POLL_RESULT_TIMESTAMP_SIZE === 2) {
|
|
391
467
|
timestampUs = structUnpack(">H", tsBuffer)[0] as number * this.POLL_RESULT_RESOLUTION_US;
|
|
392
468
|
} else {
|
|
393
469
|
timestampUs = structUnpack(">I", tsBuffer)[0] as number * this.POLL_RESULT_RESOLUTION_US;
|
|
394
470
|
}
|
|
395
471
|
|
|
396
472
|
// Check if time is before lastReportTimeMs by more than 100ms - in which case a wrap around occurred to add on the max value
|
|
397
|
-
if (timestampUs +
|
|
473
|
+
if (timestampUs + 10000 < timestampWrapHandler.lastReportTimestampUs ) {
|
|
398
474
|
timestampWrapHandler.reportTimestampOffsetUs += this.POLL_RESULT_WRAP_VALUE * this.POLL_RESULT_RESOLUTION_US;
|
|
399
475
|
}
|
|
400
476
|
timestampWrapHandler.lastReportTimestampUs = timestampUs;
|
|
@@ -413,38 +489,38 @@ export default class AttributeHandler {
|
|
|
413
489
|
private isValueInRangeString(value: number, rangeStr: string): boolean {
|
|
414
490
|
// Round to integer for comparison
|
|
415
491
|
const roundedValue = Math.round(value);
|
|
416
|
-
|
|
492
|
+
|
|
417
493
|
// Split the range string by commas
|
|
418
494
|
const parts = rangeStr.split(',');
|
|
419
|
-
|
|
495
|
+
|
|
420
496
|
for (const part of parts) {
|
|
421
497
|
// Check if it's a range (contains a hyphen)
|
|
422
498
|
if (part.includes('-')) {
|
|
423
499
|
const [startStr, endStr] = part.split('-');
|
|
424
|
-
|
|
500
|
+
|
|
425
501
|
// Handle hex values
|
|
426
|
-
const start = startStr.toLowerCase().startsWith('0x') ?
|
|
502
|
+
const start = startStr.toLowerCase().startsWith('0x') ?
|
|
427
503
|
parseInt(startStr, 16) : parseInt(startStr, 10);
|
|
428
|
-
const end = endStr.toLowerCase().startsWith('0x') ?
|
|
504
|
+
const end = endStr.toLowerCase().startsWith('0x') ?
|
|
429
505
|
parseInt(endStr, 16) : parseInt(endStr, 10);
|
|
430
|
-
|
|
506
|
+
|
|
431
507
|
if (!isNaN(start) && !isNaN(end) && roundedValue >= start && roundedValue <= end) {
|
|
432
508
|
return true;
|
|
433
509
|
}
|
|
434
|
-
}
|
|
510
|
+
}
|
|
435
511
|
// Check if it's a single value
|
|
436
512
|
else {
|
|
437
513
|
// Handle hex values
|
|
438
|
-
const partValue = part.toLowerCase().startsWith('0x') ?
|
|
514
|
+
const partValue = part.toLowerCase().startsWith('0x') ?
|
|
439
515
|
parseInt(part, 16) : parseInt(part, 10);
|
|
440
|
-
|
|
516
|
+
|
|
441
517
|
if (!isNaN(partValue) && roundedValue === partValue) {
|
|
442
518
|
return true;
|
|
443
519
|
}
|
|
444
520
|
}
|
|
445
521
|
}
|
|
446
|
-
|
|
522
|
+
|
|
447
523
|
return false;
|
|
448
524
|
}
|
|
449
525
|
|
|
450
|
-
}
|
|
526
|
+
}
|
|
@@ -41,6 +41,7 @@ export default class RaftChannelBLE implements RaftChannel {
|
|
|
41
41
|
// Connected flag and retries
|
|
42
42
|
private _isConnected = false;
|
|
43
43
|
private readonly _maxConnRetries = 3;
|
|
44
|
+
private readonly _connRetryDelayMs = 500;
|
|
44
45
|
|
|
45
46
|
// Event listener fn
|
|
46
47
|
private _eventListenerFn: ((event: Event) => void) | null = null;
|
|
@@ -49,6 +50,11 @@ export default class RaftChannelBLE implements RaftChannel {
|
|
|
49
50
|
private _requestedBatchAckSize = 10;
|
|
50
51
|
private _requestedFileBlockSize = 500;
|
|
51
52
|
|
|
53
|
+
// Max bytes per BLE write - messages larger than this are split into chunks.
|
|
54
|
+
// Web Bluetooth doesn't expose the negotiated MTU, so use a conservative
|
|
55
|
+
// default that works with BLE 4.2+ (ATT_MTU 251 → payload 244).
|
|
56
|
+
private _maxBleWriteSize = 244;
|
|
57
|
+
|
|
52
58
|
fhBatchAckSize(): number {
|
|
53
59
|
return this._requestedBatchAckSize;
|
|
54
60
|
}
|
|
@@ -115,6 +121,18 @@ export default class RaftChannelBLE implements RaftChannel {
|
|
|
115
121
|
return this._bleDevice || "";
|
|
116
122
|
}
|
|
117
123
|
|
|
124
|
+
private async disconnectGattBeforeRetry(): Promise<void> {
|
|
125
|
+
if (!this._bleDevice?.gatt?.connected) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
await this._bleDevice.gatt.disconnect();
|
|
131
|
+
} catch (error) {
|
|
132
|
+
RaftLog.warn(`RaftChannelBLE.connect - cannot disconnect before retry ${error}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
118
136
|
// Connect to a device
|
|
119
137
|
async connect(locator: string | object, _connectorOptions: ConnectorOptions): Promise<boolean> {
|
|
120
138
|
// RaftLog.debug(`Selected device: ${deviceID}`);
|
|
@@ -154,10 +172,19 @@ export default class RaftChannelBLE implements RaftChannel {
|
|
|
154
172
|
}
|
|
155
173
|
|
|
156
174
|
if (!service) {
|
|
175
|
+
if (connRetry === this._maxConnRetries - 1) {
|
|
176
|
+
RaftLog.warn(
|
|
177
|
+
`RaftChannelBLE.connect - cannot get primary service - attempt #${connRetry + 1} - giving up`
|
|
178
|
+
);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
157
182
|
RaftLog.warn(
|
|
158
|
-
`RaftChannelBLE.connect - cannot get primary service -
|
|
183
|
+
`RaftChannelBLE.connect - cannot get primary service - attempt #${connRetry + 1} - retrying`
|
|
159
184
|
);
|
|
160
|
-
|
|
185
|
+
await this.disconnectGattBeforeRetry();
|
|
186
|
+
await new Promise(resolve => setTimeout(resolve, this._connRetryDelayMs));
|
|
187
|
+
continue;
|
|
161
188
|
}
|
|
162
189
|
RaftLog.debug(
|
|
163
190
|
`RaftChannelBLE.connect - found service: ${service.uuid}`
|
|
@@ -286,7 +313,20 @@ export default class RaftChannelBLE implements RaftChannel {
|
|
|
286
313
|
}
|
|
287
314
|
}
|
|
288
315
|
|
|
289
|
-
//
|
|
316
|
+
// Write a single chunk to the BLE characteristic
|
|
317
|
+
private async _writeChunk(data: Uint8Array): Promise<void> {
|
|
318
|
+
if (!this._characteristicTx) return;
|
|
319
|
+
const bs = RaftUtils.toBufferSource(data);
|
|
320
|
+
if (this._characteristicTx.writeValueWithoutResponse) {
|
|
321
|
+
await this._characteristicTx.writeValueWithoutResponse(bs);
|
|
322
|
+
} else if (this._characteristicTx.writeValue) {
|
|
323
|
+
await this._characteristicTx.writeValue(bs);
|
|
324
|
+
} else if (this._characteristicTx.writeValueWithResponse) {
|
|
325
|
+
await this._characteristicTx.writeValueWithResponse(bs);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Send a message, chunking if it exceeds the BLE write size
|
|
290
330
|
async sendTxMsg(
|
|
291
331
|
msg: Uint8Array
|
|
292
332
|
// _sendWithResponse: boolean
|
|
@@ -304,16 +344,16 @@ export default class RaftChannelBLE implements RaftChannel {
|
|
|
304
344
|
}
|
|
305
345
|
this._msgTxTimeLast = Date.now();
|
|
306
346
|
|
|
307
|
-
// Write to the characteristic
|
|
347
|
+
// Write to the characteristic, chunking if necessary
|
|
308
348
|
try {
|
|
309
349
|
if (this._characteristicTx) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
350
|
+
if (msg.length <= this._maxBleWriteSize) {
|
|
351
|
+
await this._writeChunk(msg);
|
|
352
|
+
} else {
|
|
353
|
+
for (let offset = 0; offset < msg.length; offset += this._maxBleWriteSize) {
|
|
354
|
+
const chunk = msg.subarray(offset, Math.min(offset + this._maxBleWriteSize, msg.length));
|
|
355
|
+
await this._writeChunk(chunk);
|
|
356
|
+
}
|
|
317
357
|
}
|
|
318
358
|
}
|
|
319
359
|
break;
|
|
@@ -344,15 +384,17 @@ export default class RaftChannelBLE implements RaftChannel {
|
|
|
344
384
|
}
|
|
345
385
|
this._msgTxTimeLast = Date.now();
|
|
346
386
|
|
|
347
|
-
// Write to the characteristic
|
|
387
|
+
// Write to the characteristic, chunking if necessary
|
|
348
388
|
if (this._characteristicTx) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
389
|
+
if (msg.length <= this._maxBleWriteSize) {
|
|
390
|
+
this._writeChunk(msg);
|
|
391
|
+
} else {
|
|
392
|
+
(async () => {
|
|
393
|
+
for (let offset = 0; offset < msg.length; offset += this._maxBleWriteSize) {
|
|
394
|
+
const chunk = msg.subarray(offset, Math.min(offset + this._maxBleWriteSize, msg.length));
|
|
395
|
+
await this._writeChunk(chunk);
|
|
396
|
+
}
|
|
397
|
+
})();
|
|
356
398
|
}
|
|
357
399
|
return true;
|
|
358
400
|
}
|
|
@@ -371,4 +413,4 @@ export default class RaftChannelBLE implements RaftChannel {
|
|
|
371
413
|
return null as T;
|
|
372
414
|
}
|
|
373
415
|
|
|
374
|
-
}
|
|
416
|
+
}
|
|
@@ -23,6 +23,8 @@ interface SimulatedDeviceInfo {
|
|
|
23
23
|
|
|
24
24
|
export default class RaftChannelSimulated implements RaftChannel {
|
|
25
25
|
|
|
26
|
+
private readonly POLL_RESULT_TIMESTAMP_RESOLUTION_US = 100;
|
|
27
|
+
|
|
26
28
|
// Message handler
|
|
27
29
|
private _raftMsgHandler: RaftMsgHandler | null = null;
|
|
28
30
|
|
|
@@ -292,8 +294,10 @@ export default class RaftChannelSimulated implements RaftChannel {
|
|
|
292
294
|
const dataView = new DataView(dataBuffer);
|
|
293
295
|
let bytePos = 0;
|
|
294
296
|
|
|
295
|
-
// Add 16
|
|
296
|
-
|
|
297
|
+
// Add 16-bit big-endian timestamp ticks. Poll timestamps are decoded in
|
|
298
|
+
// 100us units by RaftAttributeHandler.
|
|
299
|
+
const timestampTicks = Math.floor((deviceTimeMs * 1000) / this.POLL_RESULT_TIMESTAMP_RESOLUTION_US) % 65536;
|
|
300
|
+
dataView.setUint16(bytePos, timestampTicks, false);
|
|
297
301
|
bytePos += 2;
|
|
298
302
|
|
|
299
303
|
const handledByCustomGenerator = this._fillCustomRawData(
|
|
@@ -357,9 +361,9 @@ export default class RaftChannelSimulated implements RaftChannel {
|
|
|
357
361
|
// Convert the buffer to a byte array
|
|
358
362
|
const dataBytes = new Uint8Array(dataBuffer);
|
|
359
363
|
|
|
360
|
-
// Create the JSON message structure
|
|
364
|
+
// Create the JSON message structure (bus key is numeric string to match firmware convention)
|
|
361
365
|
const message = {
|
|
362
|
-
"
|
|
366
|
+
"1": {
|
|
363
367
|
[deviceName]: {
|
|
364
368
|
"_t": deviceTypeInfo.type,
|
|
365
369
|
"_o": 1, // Device is online
|
|
@@ -464,7 +468,8 @@ export default class RaftChannelSimulated implements RaftChannel {
|
|
|
464
468
|
const hotspotRow = (Math.sin(hotspotPhase) + 1) * (rows - 1) / 2;
|
|
465
469
|
const hotspotCol = (Math.cos(hotspotPhase) + 1) * (cols - 1) / 2;
|
|
466
470
|
const hotspotAmplitude = 6;
|
|
467
|
-
const
|
|
471
|
+
const rawSigma = Math.max(rows, cols) / 3;
|
|
472
|
+
const sigma = Number.isFinite(rawSigma) ? rawSigma : 1;
|
|
468
473
|
|
|
469
474
|
for (let idx = 0; idx < repeatCount; idx++) {
|
|
470
475
|
const row = Math.floor(idx / cols);
|