@robotical/raftjs 2.0.11 → 2.1.2
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 +10 -6
- package/dist/react-native/RaftChannelSimulated.js.map +1 -1
- package/dist/react-native/RaftChannelWebSerial.js +1 -1
- package/dist/react-native/RaftChannelWebSerial.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 +11 -1
- package/dist/react-native/RaftConnector.js +75 -9
- 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 +47 -2
- package/dist/react-native/RaftDeviceManager.js +696 -104
- 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 +0 -1
- package/dist/react-native/RaftFileHandler.js +61 -23
- package/dist/react-native/RaftFileHandler.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 +27 -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 +6 -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 +10 -6
- package/dist/web/RaftChannelSimulated.js.map +1 -1
- package/dist/web/RaftChannelWebSerial.js +1 -1
- package/dist/web/RaftChannelWebSerial.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 +11 -1
- package/dist/web/RaftConnector.js +75 -9
- 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 +47 -2
- package/dist/web/RaftDeviceManager.js +696 -104
- 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 +0 -1
- package/dist/web/RaftFileHandler.js +61 -23
- package/dist/web/RaftFileHandler.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 +27 -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 +6 -1
- package/dist/web/main.js.map +1 -1
- package/examples/dashboard/package.json +2 -2
- package/examples/dashboard/src/DeviceActionsForm.tsx +158 -8
- 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 +11 -6
- package/src/RaftChannelWebSerial.ts +1 -1
- package/src/RaftChannelWebSocket.ts +16 -2
- package/src/RaftConnector.ts +93 -15
- package/src/RaftCustomAttrHandler.ts +35 -45
- package/src/RaftDeviceInfo.ts +27 -0
- package/src/RaftDeviceManager.test.ts +164 -0
- package/src/RaftDeviceManager.ts +823 -121
- package/src/RaftDeviceMgrIF.ts +13 -2
- package/src/RaftDeviceStates.ts +49 -8
- package/src/RaftFileHandler.ts +69 -28
- 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 +34 -1
- package/src/RaftUpdateManager.ts +1 -1
- package/src/main.ts +3 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Web BLE Reconnect Recovery
|
|
2
|
+
|
|
3
|
+
Date: 2026-04-29
|
|
4
|
+
|
|
5
|
+
## Issue
|
|
6
|
+
|
|
7
|
+
The Axiom offline data logging UI can be refreshed while Web Bluetooth is still
|
|
8
|
+
connected. In that path the browser may report `gatt.connect()` success and then
|
|
9
|
+
disconnect during `getPrimaryService()`, producing an error like:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
GATT Server is disconnected. Cannot retrieve services. (Re)connect first with `device.gatt.connect`.
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Before this change, `RaftChannelBLE.web.ts` treated a failed primary-service
|
|
16
|
+
lookup as a terminal failure and returned `false` immediately. That meant a
|
|
17
|
+
transient reconnect race could fail the whole app-level connection attempt even
|
|
18
|
+
though the device was still running and still advertising.
|
|
19
|
+
|
|
20
|
+
The harsher path is a browser hard refresh while BLE is still connected. During
|
|
21
|
+
page unload, the normal app disconnect path cannot reliably complete because it
|
|
22
|
+
sends a graceful BLE command and waits asynchronously. If the page disappears
|
|
23
|
+
before that finishes, the browser can keep the GATT connection in a half-closed
|
|
24
|
+
state long enough for the next immediate reconnect to fail.
|
|
25
|
+
|
|
26
|
+
## Solution
|
|
27
|
+
|
|
28
|
+
`RaftChannelBLE.connect()` now keeps the existing connection retry loop active
|
|
29
|
+
for primary-service lookup failures:
|
|
30
|
+
|
|
31
|
+
- If no supported primary service is found and more connection attempts remain,
|
|
32
|
+
it disconnects any still-open GATT connection.
|
|
33
|
+
- It waits briefly before retrying.
|
|
34
|
+
- It only returns `false` after the final service lookup attempt fails.
|
|
35
|
+
|
|
36
|
+
This preserves the previous final failure behavior while allowing transient
|
|
37
|
+
Web Bluetooth/GATT cleanup races to recover inside the raftjs channel.
|
|
38
|
+
|
|
39
|
+
`RaftConnector` also now exposes `disconnectForPageUnload()`. It is deliberately
|
|
40
|
+
smaller than the normal `disconnect()` path:
|
|
41
|
+
|
|
42
|
+
- It disables automatic lost-connection retry.
|
|
43
|
+
- It detaches the current channel from the connector immediately.
|
|
44
|
+
- It starts the channel-level GATT disconnect without waiting for the normal
|
|
45
|
+
graceful BLE command sequence.
|
|
46
|
+
|
|
47
|
+
The Axiom app uses this from `beforeunload`/`pagehide` so a browser hard refresh
|
|
48
|
+
still starts Web Bluetooth cleanup before the page is replaced. Normal user
|
|
49
|
+
disconnects continue to use the existing graceful path.
|
|
50
|
+
|
|
51
|
+
## Validation
|
|
52
|
+
|
|
53
|
+
Validation was done from `Axiom-Experiment-App` against real Axiom hardware
|
|
54
|
+
(`Axiom009_adcf1e`) with firmware serial logs open on `/dev/cu.usbmodem2101`.
|
|
55
|
+
|
|
56
|
+
The diagnostic flow was:
|
|
57
|
+
|
|
58
|
+
1. Connect over Web Bluetooth.
|
|
59
|
+
2. Start Axiom offline data logging for LSM6DS at 1 Hz.
|
|
60
|
+
3. Hard-refresh the browser without issuing the app-level Disconnect command.
|
|
61
|
+
4. Immediately reconnect over Web Bluetooth.
|
|
62
|
+
5. Confirm the offline logger is still active and points to the same log file.
|
|
63
|
+
6. Stop the session and delete the generated e2e log.
|
|
64
|
+
|
|
65
|
+
The firmware logs showed no reboot or panic during the refresh/reconnect flow.
|
|
66
|
+
The logger stayed active across the browser refresh. The page-unload run showed
|
|
67
|
+
the Axiom app calling the immediate GATT disconnect path, the firmware observing
|
|
68
|
+
`BLE connection change isConn NO`, and reconnect reporting the same active log
|
|
69
|
+
with additional samples.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robotical/raftjs",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "Javascript/TS library for Raft library",
|
|
5
5
|
"main": "dist/web/main.js",
|
|
6
6
|
"types": "dist/web/main.d.ts",
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
"author": "Rob Dobson <rob@dobson.com>",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "https://github.com/
|
|
11
|
+
"url": "https://github.com/robotical/raftjs.git"
|
|
12
12
|
},
|
|
13
13
|
"bugs": {
|
|
14
|
-
"url": "https://github.com/
|
|
14
|
+
"url": "https://github.com/robotical/raftjs/issues"
|
|
15
15
|
},
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"keywords": [
|
|
@@ -31,21 +31,24 @@
|
|
|
31
31
|
"watch-all": "tsc -p tsconfig.json --watch & tsc -p tsconfig.react-native.json --watch"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
+
"@types/jest": "^30.0.0",
|
|
34
35
|
"@types/node": "^22.13.11",
|
|
36
|
+
"@types/text-encoding": "^0.0.40",
|
|
35
37
|
"@types/web-bluetooth": "^0.0.21",
|
|
36
38
|
"@typescript-eslint/eslint-plugin": "^8.27.0",
|
|
37
39
|
"eslint": "^9.23.0",
|
|
40
|
+
"jest": "^30.3.0",
|
|
38
41
|
"react-native-ble-plx": "^3.5.0",
|
|
39
|
-
"typescript": "^5.8.2",
|
|
40
42
|
"rimraf": "^6.0.1",
|
|
41
|
-
"
|
|
43
|
+
"ts-jest": "^29.4.6",
|
|
44
|
+
"typescript": "^5.8.2"
|
|
42
45
|
},
|
|
43
46
|
"dependencies": {
|
|
44
47
|
"isomorphic-ws": "^5.0.0",
|
|
45
48
|
"tslib": "^2.8.1"
|
|
46
49
|
},
|
|
47
50
|
"peerDependencies": {
|
|
48
|
-
"react-native
|
|
49
|
-
"react-native": "*"
|
|
51
|
+
"react-native": "*",
|
|
52
|
+
"react-native-ble-plx": "*"
|
|
50
53
|
}
|
|
51
54
|
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { tokenize, transpilePseudocodeToJs } from "./PseudocodeTranspiler";
|
|
2
|
+
import CustomAttrHandler from "./RaftCustomAttrHandler";
|
|
3
|
+
import { DeviceTypePollRespMetadata } from "./RaftDeviceInfo";
|
|
4
|
+
|
|
5
|
+
// ===== Pseudocode strings from DeviceTypeRecords.json =====
|
|
6
|
+
|
|
7
|
+
const PSEUDOCODE = {
|
|
8
|
+
max30101_fifo:
|
|
9
|
+
'int N=(buf[0]+32-buf[2])%32;int k=3;int i=0;while(i<N){out.Red=(buf[k]<<16)|(buf[k+1]<<8)|buf[k+2];out.IR=(buf[k+3]<<16)|(buf[k+4]<<8)|buf[k+5];k+=6;i++;next;}',
|
|
10
|
+
lsm6ds_fifo:
|
|
11
|
+
'int W=((buf[1]&0x0F)<<8)|buf[0];int P=((buf[3]&0x03)<<8)|buf[2];int skip=(6-P%6)%6;int N=(W-skip)/6;int maxN=(192-skip*2)/12;if(N>maxN){N=maxN;}if(N>16){N=16;}if(N<1){N=0;}int k=4+skip*2;int i=0;while(i<N){out.gx=(buf[k+1]<<8)|buf[k];out.gy=(buf[k+3]<<8)|buf[k+2];out.gz=(buf[k+5]<<8)|buf[k+4];out.ax=(buf[k+7]<<8)|buf[k+6];out.ay=(buf[k+9]<<8)|buf[k+8];out.az=(buf[k+11]<<8)|buf[k+10];k+=12;i++;next;}',
|
|
12
|
+
scd40_calc:
|
|
13
|
+
'out.CO2 = buf[0]; out.Temp = -45.0 + (175.0 * buf[1] / 65535.0); out.Humidity = (100.0 * buf[2] / 65535.0);',
|
|
14
|
+
gravity_o2_calc:
|
|
15
|
+
'float key = 20.9/120.0; float val = key * (buf[0] + (buf[1]/10.0) + (buf[2]/100.0)); out.oxygen = val;',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Helper to build a minimal DeviceTypePollRespMetadata
|
|
19
|
+
function makeMeta(numBytes: number, attrNames: string[], customName: string, pseudocode: string): DeviceTypePollRespMetadata {
|
|
20
|
+
return {
|
|
21
|
+
b: numBytes,
|
|
22
|
+
a: attrNames.map(n => ({ n, t: 'B' })),
|
|
23
|
+
c: { n: customName, c: pseudocode },
|
|
24
|
+
} as DeviceTypePollRespMetadata;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ===== Tokenizer tests =====
|
|
28
|
+
|
|
29
|
+
describe("tokenize", () => {
|
|
30
|
+
test("handles float literals before integers", () => {
|
|
31
|
+
const tokens = tokenize("20.9/120.0");
|
|
32
|
+
expect(tokens).toEqual([
|
|
33
|
+
{ type: "NUM_FLOAT", value: "20.9" },
|
|
34
|
+
{ type: "DIV_OP", value: "/" },
|
|
35
|
+
{ type: "NUM_FLOAT", value: "120.0" },
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("handles hex literals", () => {
|
|
40
|
+
const tokens = tokenize("buf[1]&0x0F");
|
|
41
|
+
expect(tokens).toEqual([
|
|
42
|
+
{ type: "ID", value: "buf" },
|
|
43
|
+
{ type: "LBRACK", value: "[" },
|
|
44
|
+
{ type: "NUM_INT", value: "1" },
|
|
45
|
+
{ type: "RBRACK", value: "]" },
|
|
46
|
+
{ type: "BITWISE_AND", value: "&" },
|
|
47
|
+
{ type: "HEX_NUM", value: "0x0F" },
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("handles int keyword without matching identifiers containing 'int'", () => {
|
|
52
|
+
const tokens = tokenize("int interval=5;");
|
|
53
|
+
expect(tokens[0]).toEqual({ type: "INT_KW", value: "int" });
|
|
54
|
+
// 'interval' should be a single ID, not "int" + "erval"
|
|
55
|
+
expect(tokens[1]).toEqual({ type: "ID", value: "interval" });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("handles multi-char operators before single-char", () => {
|
|
59
|
+
const tokens = tokenize("a<<b&&c||d==e!=f<=g>=h");
|
|
60
|
+
const types = tokens.map(t => t.type);
|
|
61
|
+
expect(types).toContain("LSHIFT");
|
|
62
|
+
expect(types).toContain("LOGICAL_AND");
|
|
63
|
+
expect(types).toContain("LOGICAL_OR");
|
|
64
|
+
expect(types).toContain("REL_OP"); // ==, !=, <=, >=
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("handles increment and decrement operators", () => {
|
|
68
|
+
const tokens = tokenize("i++;k--;");
|
|
69
|
+
expect(tokens).toEqual([
|
|
70
|
+
{ type: "ID", value: "i" },
|
|
71
|
+
{ type: "INC_OP", value: "++" },
|
|
72
|
+
{ type: "SEMI", value: ";" },
|
|
73
|
+
{ type: "ID", value: "k" },
|
|
74
|
+
{ type: "DEC_OP", value: "--" },
|
|
75
|
+
{ type: "SEMI", value: ";" },
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("handles dotted identifiers (out.X)", () => {
|
|
80
|
+
const tokens = tokenize("out.Red=val;");
|
|
81
|
+
expect(tokens[0]).toEqual({ type: "ID", value: "out.Red" });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("'if' falls through to ID token", () => {
|
|
85
|
+
const tokens = tokenize("if(N>16){N=16;}");
|
|
86
|
+
expect(tokens[0]).toEqual({ type: "ID", value: "if" });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("'while' is a keyword token", () => {
|
|
90
|
+
const tokens = tokenize("while(i<N){");
|
|
91
|
+
expect(tokens[0]).toEqual({ type: "WHILE", value: "while" });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("'next' is a keyword token", () => {
|
|
95
|
+
const tokens = tokenize("next;");
|
|
96
|
+
expect(tokens[0]).toEqual({ type: "NEXT", value: "next" });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("negative float literal tokenizes as SUB_OP + NUM_FLOAT", () => {
|
|
100
|
+
const tokens = tokenize("-45.0");
|
|
101
|
+
expect(tokens).toEqual([
|
|
102
|
+
{ type: "SUB_OP", value: "-" },
|
|
103
|
+
{ type: "NUM_FLOAT", value: "45.0" },
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("tokenizes all four pseudocode strings without errors", () => {
|
|
108
|
+
for (const [, code] of Object.entries(PSEUDOCODE)) {
|
|
109
|
+
const tokens = tokenize(code);
|
|
110
|
+
expect(tokens.length).toBeGreaterThan(0);
|
|
111
|
+
// Verify all characters are consumed by checking no unknown tokens
|
|
112
|
+
for (const t of tokens) {
|
|
113
|
+
expect(t.type).not.toBe("UNKNOWN");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ===== Transpiler output tests =====
|
|
120
|
+
|
|
121
|
+
describe("transpilePseudocodeToJs", () => {
|
|
122
|
+
test("replaces int with let and wraps in Math.trunc", () => {
|
|
123
|
+
const js = transpilePseudocodeToJs("int x=1;");
|
|
124
|
+
expect(js).toContain("let x=Math.trunc(1);");
|
|
125
|
+
expect(js).not.toContain("int ");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("replaces float with let without Math.trunc", () => {
|
|
129
|
+
const js = transpilePseudocodeToJs("float y=2.0;");
|
|
130
|
+
expect(js).toContain("let y=2.0;");
|
|
131
|
+
expect(js).not.toContain("float ");
|
|
132
|
+
expect(js).not.toMatch(/Math\.trunc/);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("Math.trunc wraps division in int declaration", () => {
|
|
136
|
+
const js = transpilePseudocodeToJs("int N=(W-skip)/6;");
|
|
137
|
+
expect(js).toContain("let N=Math.trunc((W-skip)/6);");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("removes next keyword", () => {
|
|
141
|
+
const js = transpilePseudocodeToJs("i++;next;");
|
|
142
|
+
expect(js).toContain("i++;");
|
|
143
|
+
// 'next' should be removed entirely; only the semicolons after i++ and after next remain
|
|
144
|
+
expect(js).not.toMatch(/\bnext\b/);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("includes Proxy preamble", () => {
|
|
148
|
+
const js = transpilePseudocodeToJs("out.x=1;");
|
|
149
|
+
expect(js).toContain("new Proxy");
|
|
150
|
+
expect(js).toContain("attrValues[p]");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("includes toInt16 helper", () => {
|
|
154
|
+
const js = transpilePseudocodeToJs("");
|
|
155
|
+
expect(js).toContain("function toInt16");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("preserves out.X assignments verbatim", () => {
|
|
159
|
+
const js = transpilePseudocodeToJs("out.Red=(buf[k]<<16);");
|
|
160
|
+
expect(js).toContain("out.Red=(buf[k]<<16);");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("preserves hex literals", () => {
|
|
164
|
+
const js = transpilePseudocodeToJs("int W=((buf[1]&0x0F)<<8)|buf[0];");
|
|
165
|
+
expect(js).toContain("0x0F");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("gravity_o2_calc transpiles correctly (float, no Math.trunc)", () => {
|
|
169
|
+
const js = transpilePseudocodeToJs(PSEUDOCODE.gravity_o2_calc);
|
|
170
|
+
expect(js).toContain("let key=20.9/120.0;");
|
|
171
|
+
expect(js).toContain("let val=key");
|
|
172
|
+
expect(js).toContain("out.oxygen=val;");
|
|
173
|
+
// float declarations should NOT be wrapped in Math.trunc
|
|
174
|
+
expect(js).not.toMatch(/Math\.trunc/);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("lsm6ds_fifo transpiles with Math.trunc for int divisions", () => {
|
|
178
|
+
const js = transpilePseudocodeToJs(PSEUDOCODE.lsm6ds_fifo);
|
|
179
|
+
expect(js).toContain("Math.trunc((W-skip)/6)");
|
|
180
|
+
expect(js).toContain("Math.trunc((192-skip*2)/12)");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ===== End-to-end execution tests via CustomAttrHandler =====
|
|
185
|
+
|
|
186
|
+
describe("CustomAttrHandler with transpiled pseudocode", () => {
|
|
187
|
+
|
|
188
|
+
test("max30101_fifo: decodes 2 samples", () => {
|
|
189
|
+
// buf[0] = write pointer = 5, buf[2] = read pointer = 3 → N = (5+32-3)%32 = 2
|
|
190
|
+
// Samples start at buf[3], 6 bytes each (3 for Red, 3 for IR)
|
|
191
|
+
const bufData = new Uint8Array([
|
|
192
|
+
5, 0, 3, // write ptr, unused, read ptr
|
|
193
|
+
0x01, 0x02, 0x03, // Red sample 0: (1<<16)|(2<<8)|3 = 66051
|
|
194
|
+
0x04, 0x05, 0x06, // IR sample 0: (4<<16)|(5<<8)|6 = 263430
|
|
195
|
+
0x10, 0x20, 0x30, // Red sample 1: (16<<16)|(32<<8)|48 = 1056816
|
|
196
|
+
0x40, 0x50, 0x60, // IR sample 1: (64<<16)|(80<<8)|96 = 4214880
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
const meta = makeMeta(bufData.length, ["Red", "IR"], "max30101_fifo", PSEUDOCODE.max30101_fifo);
|
|
200
|
+
const handler = new CustomAttrHandler();
|
|
201
|
+
const result = handler.handleAttr(meta, bufData, 0);
|
|
202
|
+
|
|
203
|
+
expect(result.length).toBe(2); // 2 attributes
|
|
204
|
+
expect(result[0]).toEqual([66051, 1056816]); // Red
|
|
205
|
+
expect(result[1]).toEqual([263430, 4214880]); // IR
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("max30101_fifo: N=0 produces empty arrays", () => {
|
|
209
|
+
// write ptr = read ptr → N = 0
|
|
210
|
+
const bufData = new Uint8Array([3, 0, 3]);
|
|
211
|
+
const meta = makeMeta(bufData.length, ["Red", "IR"], "max30101_fifo", PSEUDOCODE.max30101_fifo);
|
|
212
|
+
const handler = new CustomAttrHandler();
|
|
213
|
+
const result = handler.handleAttr(meta, bufData, 0);
|
|
214
|
+
|
|
215
|
+
expect(result[0]).toEqual([]); // Red
|
|
216
|
+
expect(result[1]).toEqual([]); // IR
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("lsm6ds_fifo: decodes 1 aligned sample (unsigned values)", () => {
|
|
220
|
+
// W = 6 words, P = 0 (aligned) → skip = 0, N = 1
|
|
221
|
+
// 4 bytes header + 12 bytes data = 16 bytes
|
|
222
|
+
const header = [6, 0, 0, 0]; // W=6, P=0
|
|
223
|
+
// gx=0x0100(256), gy=0x0200(512), gz=0x0300(768), ax=0x0400(1024), ay=0x0500(1280), az=0x0600(1536)
|
|
224
|
+
const sample = [0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06];
|
|
225
|
+
const bufData = new Uint8Array([...header, ...sample]);
|
|
226
|
+
|
|
227
|
+
const meta = makeMeta(bufData.length, ["gx", "gy", "gz", "ax", "ay", "az"], "lsm6ds_fifo", PSEUDOCODE.lsm6ds_fifo);
|
|
228
|
+
const handler = new CustomAttrHandler();
|
|
229
|
+
const result = handler.handleAttr(meta, bufData, 0);
|
|
230
|
+
|
|
231
|
+
// Custom handler produces unsigned values; sign conversion happens in RaftAttributeHandler
|
|
232
|
+
expect(result[0]).toEqual([256]); // gx
|
|
233
|
+
expect(result[1]).toEqual([512]); // gy
|
|
234
|
+
expect(result[2]).toEqual([768]); // gz
|
|
235
|
+
expect(result[3]).toEqual([1024]); // ax
|
|
236
|
+
expect(result[4]).toEqual([1280]); // ay
|
|
237
|
+
expect(result[5]).toEqual([1536]); // az
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("lsm6ds_fifo: integer division truncates (non-multiple word count)", () => {
|
|
241
|
+
// W = 7 words (not a multiple of 6), P = 0 → skip = 0, N = Math.trunc(7/6) = 1
|
|
242
|
+
// Without Math.trunc, N=1.166... would cause 2 iterations reading garbage
|
|
243
|
+
const header = [7, 0, 0, 0]; // W=7, P=0
|
|
244
|
+
const sample = [0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00];
|
|
245
|
+
// Only provide 4 header + 12 data bytes (enough for 1 sample, not 2)
|
|
246
|
+
const bufData = new Uint8Array([...header, ...sample]);
|
|
247
|
+
|
|
248
|
+
const meta = makeMeta(bufData.length, ["gx", "gy", "gz", "ax", "ay", "az"], "lsm6ds_fifo", PSEUDOCODE.lsm6ds_fifo);
|
|
249
|
+
const handler = new CustomAttrHandler();
|
|
250
|
+
const result = handler.handleAttr(meta, bufData, 0);
|
|
251
|
+
|
|
252
|
+
// Should decode exactly 1 sample (truncated division), not 2
|
|
253
|
+
expect(result[0]).toEqual([1]); // gx
|
|
254
|
+
expect(result[0].length).toBe(1);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("lsm6ds_fifo: clamps N to 16", () => {
|
|
258
|
+
// W = 200 words, P = 0 → N = 200/6 = 33, clamped to 16
|
|
259
|
+
// Need 4 header + 16*12 = 196 data bytes
|
|
260
|
+
const bufArr = new Array(200).fill(0);
|
|
261
|
+
bufArr[0] = 200; // W low byte
|
|
262
|
+
bufArr[1] = 0; // W high nibble
|
|
263
|
+
bufArr[2] = 0; // P low byte
|
|
264
|
+
bufArr[3] = 0; // P high byte
|
|
265
|
+
// Fill with recognizable data: each sample's gx low byte = sample index
|
|
266
|
+
for (let i = 0; i < 16; i++) {
|
|
267
|
+
bufArr[4 + i * 12] = i; // gx low byte
|
|
268
|
+
}
|
|
269
|
+
const bufData = new Uint8Array(bufArr);
|
|
270
|
+
|
|
271
|
+
const meta = makeMeta(bufData.length, ["gx", "gy", "gz", "ax", "ay", "az"], "lsm6ds_fifo", PSEUDOCODE.lsm6ds_fifo);
|
|
272
|
+
const handler = new CustomAttrHandler();
|
|
273
|
+
const result = handler.handleAttr(meta, bufData, 0);
|
|
274
|
+
|
|
275
|
+
expect(result[0].length).toBe(16); // gx has exactly 16 samples
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("scd40_calc: decodes CO2, Temp, Humidity", () => {
|
|
279
|
+
// buf[0] = CO2 raw, buf[1] = Temp raw, buf[2] = Humidity raw
|
|
280
|
+
// Using buf[0]=400 won't fit in single byte; the pseudocode treats buf values as numbers
|
|
281
|
+
// In real usage these would be pre-decoded multi-byte values packed into the buf
|
|
282
|
+
const bufData = new Uint8Array([100, 128, 200]);
|
|
283
|
+
|
|
284
|
+
const meta = makeMeta(bufData.length, ["CO2", "Temp", "Humidity"], "scd40_calc", PSEUDOCODE.scd40_calc);
|
|
285
|
+
const handler = new CustomAttrHandler();
|
|
286
|
+
const result = handler.handleAttr(meta, bufData, 0);
|
|
287
|
+
|
|
288
|
+
expect(result[0]).toEqual([100]); // CO2
|
|
289
|
+
expect(result[1][0]).toBeCloseTo(-45.0 + (175.0 * 128 / 65535.0), 5); // Temp
|
|
290
|
+
expect(result[2][0]).toBeCloseTo(100.0 * 200 / 65535.0, 5); // Humidity
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("gravity_o2_calc: decodes oxygen", () => {
|
|
294
|
+
// buf[0]=10, buf[1]=5, buf[2]=25 → val = (20.9/120) * (10 + 0.5 + 0.25) = 0.174166... * 10.75
|
|
295
|
+
const bufData = new Uint8Array([10, 5, 25]);
|
|
296
|
+
|
|
297
|
+
const meta = makeMeta(bufData.length, ["oxygen"], "gravity_o2_calc", PSEUDOCODE.gravity_o2_calc);
|
|
298
|
+
const handler = new CustomAttrHandler();
|
|
299
|
+
const result = handler.handleAttr(meta, bufData, 0);
|
|
300
|
+
|
|
301
|
+
const expected = (20.9 / 120.0) * (10 + 5 / 10.0 + 25 / 100.0);
|
|
302
|
+
expect(result[0][0]).toBeCloseTo(expected, 10);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("explicit j field takes priority over c field", () => {
|
|
306
|
+
const meta: DeviceTypePollRespMetadata = {
|
|
307
|
+
b: 1,
|
|
308
|
+
a: [{ n: "val", t: "B" }],
|
|
309
|
+
c: {
|
|
310
|
+
n: "test_fn",
|
|
311
|
+
c: "out.val = buf[0] * 2;", // pseudocode would multiply by 2
|
|
312
|
+
j: "attrValues['val'].push(buf[0] * 10);", // explicit JS multiplies by 10
|
|
313
|
+
},
|
|
314
|
+
} as DeviceTypePollRespMetadata;
|
|
315
|
+
|
|
316
|
+
const bufData = new Uint8Array([7]);
|
|
317
|
+
const handler = new CustomAttrHandler();
|
|
318
|
+
const result = handler.handleAttr(meta, bufData, 0);
|
|
319
|
+
|
|
320
|
+
expect(result[0]).toEqual([70]); // j field used → 7 * 10
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("no custom function returns empty attribute vectors", () => {
|
|
324
|
+
const meta: DeviceTypePollRespMetadata = {
|
|
325
|
+
b: 1,
|
|
326
|
+
a: [{ n: "val", t: "B" }],
|
|
327
|
+
} as DeviceTypePollRespMetadata;
|
|
328
|
+
|
|
329
|
+
const bufData = new Uint8Array([42]);
|
|
330
|
+
const handler = new CustomAttrHandler();
|
|
331
|
+
const result = handler.handleAttr(meta, bufData, 0);
|
|
332
|
+
|
|
333
|
+
expect(result.length).toBe(1);
|
|
334
|
+
expect(result[0]).toEqual([]); // No function, so no values pushed
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("caches compiled functions", () => {
|
|
338
|
+
const meta = makeMeta(1, ["val"], "cache_test", "out.val = buf[0];");
|
|
339
|
+
const handler = new CustomAttrHandler();
|
|
340
|
+
|
|
341
|
+
const buf1 = new Uint8Array([5]);
|
|
342
|
+
const buf2 = new Uint8Array([10]);
|
|
343
|
+
|
|
344
|
+
handler.handleAttr(meta, buf1, 0);
|
|
345
|
+
handler.handleAttr(meta, buf2, 0);
|
|
346
|
+
|
|
347
|
+
// Both calls should work correctly (second uses cached function)
|
|
348
|
+
const result = handler.handleAttr(meta, new Uint8Array([15]), 0);
|
|
349
|
+
expect(result[0]).toEqual([15]);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("uses available bytes when buffer is shorter than metadata length", () => {
|
|
353
|
+
const meta = makeMeta(10, ["val"], "test_short", "out.val = buf[0];");
|
|
354
|
+
const handler = new CustomAttrHandler();
|
|
355
|
+
const bufData = new Uint8Array([1, 2, 3]); // only 3 bytes, metadata length is 10
|
|
356
|
+
|
|
357
|
+
const result = handler.handleAttr(meta, bufData, 0);
|
|
358
|
+
expect(result).toEqual([[1]]);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("msgBufIdx offsets into the message buffer", () => {
|
|
362
|
+
// The handler slices from msgBufIdx, so buf[0] should be the byte at that offset
|
|
363
|
+
const meta = makeMeta(3, ["oxygen"], "gravity_o2_calc", PSEUDOCODE.gravity_o2_calc);
|
|
364
|
+
const handler = new CustomAttrHandler();
|
|
365
|
+
|
|
366
|
+
const fullBuf = new Uint8Array([0xFF, 0xFF, 10, 5, 25, 0xFF]); // data at offset 2
|
|
367
|
+
const result = handler.handleAttr(meta, fullBuf, 2);
|
|
368
|
+
|
|
369
|
+
const expected = (20.9 / 120.0) * (10 + 5 / 10.0 + 25 / 100.0);
|
|
370
|
+
expect(result[0][0]).toBeCloseTo(expected, 10);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
//
|
|
3
|
+
// PseudocodeTranspiler
|
|
4
|
+
// Transpiles Raft device pseudocode (from devInfoJson resp.c.c) to JavaScript function bodies
|
|
5
|
+
//
|
|
6
|
+
// Rob Dobson (C) 2024
|
|
7
|
+
//
|
|
8
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
9
|
+
|
|
10
|
+
export interface Token {
|
|
11
|
+
type: string;
|
|
12
|
+
value: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const TOKEN_SPEC: [string, RegExp][] = [
|
|
16
|
+
['FLOAT_KW', /float\b/],
|
|
17
|
+
['INT_KW', /int\b/],
|
|
18
|
+
['RETURN', /return\b/],
|
|
19
|
+
['WHILE', /while\b/],
|
|
20
|
+
['NEXT', /next\b/],
|
|
21
|
+
['HEX_NUM', /0x[0-9A-Fa-f]+/],
|
|
22
|
+
['NUM_FLOAT', /\d+\.\d*/],
|
|
23
|
+
['NUM_INT', /\d+/],
|
|
24
|
+
['ID', /[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*/],
|
|
25
|
+
['LSHIFT', /<</],
|
|
26
|
+
['RSHIFT', />>/],
|
|
27
|
+
['LOGICAL_AND', /&&/],
|
|
28
|
+
['LOGICAL_OR', /\|\|/],
|
|
29
|
+
['REL_OP', /==|!=|<=|>=|<|>/],
|
|
30
|
+
['INC_OP', /\+\+/],
|
|
31
|
+
['DEC_OP', /--/],
|
|
32
|
+
['ASSIGN', /=/],
|
|
33
|
+
['ADD_OP', /\+/],
|
|
34
|
+
['SUB_OP', /-/],
|
|
35
|
+
['MUL_OP', /\*/],
|
|
36
|
+
['DIV_OP', /\//],
|
|
37
|
+
['MOD_OP', /%/],
|
|
38
|
+
['BITWISE_AND', /&/],
|
|
39
|
+
['BITWISE_OR', /\|/],
|
|
40
|
+
['BITWISE_XOR', /\^/],
|
|
41
|
+
['BITWISE_NOT', /~/],
|
|
42
|
+
['LOGICAL_NOT', /!/],
|
|
43
|
+
['SEMI', /;/],
|
|
44
|
+
['COMMA', /,/],
|
|
45
|
+
['LPAREN', /\(/],
|
|
46
|
+
['RPAREN', /\)/],
|
|
47
|
+
['LBRACE', /\{/],
|
|
48
|
+
['RBRACE', /\}/],
|
|
49
|
+
['LBRACK', /\[/],
|
|
50
|
+
['RBRACK', /\]/],
|
|
51
|
+
['WS', /[ \t\n]+/],
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Build the combined regex once at module load
|
|
55
|
+
const COMBINED_RE = new RegExp(
|
|
56
|
+
TOKEN_SPEC.map(([name, re]) => `(?<${name}>${re.source})`).join('|'),
|
|
57
|
+
'g'
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
export function tokenize(code: string): Token[] {
|
|
61
|
+
// Reset lastIndex since we reuse the global regex
|
|
62
|
+
COMBINED_RE.lastIndex = 0;
|
|
63
|
+
const tokens: Token[] = [];
|
|
64
|
+
let match: RegExpExecArray | null;
|
|
65
|
+
while ((match = COMBINED_RE.exec(code)) !== null) {
|
|
66
|
+
for (const [name] of TOKEN_SPEC) {
|
|
67
|
+
if (match.groups![name] !== undefined) {
|
|
68
|
+
if (name !== 'WS') {
|
|
69
|
+
tokens.push({ type: name, value: match[0] });
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return tokens;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const PREAMBLE =
|
|
79
|
+
'const out = new Proxy({}, { set(_, p, v) { if (attrValues[p]) attrValues[p].push(v); return true; } });\n' +
|
|
80
|
+
'function toInt16(lo, hi) { const u = (hi << 8) | lo; return u & 0x8000 ? u - 0x10000 : u; }\n' +
|
|
81
|
+
'function toInt32(b0, b1, b2, b3) { return (b3 << 24) | (b2 << 16) | (b1 << 8) | b0; }\n';
|
|
82
|
+
|
|
83
|
+
export function transpilePseudocodeToJs(pseudocode: string): string {
|
|
84
|
+
const tokens = tokenize(pseudocode);
|
|
85
|
+
let js = PREAMBLE;
|
|
86
|
+
|
|
87
|
+
// Track int declarations so we can wrap the init expression in Math.trunc()
|
|
88
|
+
// to emulate C integer division semantics
|
|
89
|
+
let afterIntKw = false;
|
|
90
|
+
let wrappedInTrunc = false;
|
|
91
|
+
|
|
92
|
+
for (const token of tokens) {
|
|
93
|
+
switch (token.type) {
|
|
94
|
+
case 'INT_KW':
|
|
95
|
+
js += 'let ';
|
|
96
|
+
afterIntKw = true;
|
|
97
|
+
break;
|
|
98
|
+
case 'FLOAT_KW':
|
|
99
|
+
js += 'let ';
|
|
100
|
+
afterIntKw = false;
|
|
101
|
+
break;
|
|
102
|
+
case 'ASSIGN':
|
|
103
|
+
js += '=';
|
|
104
|
+
if (afterIntKw) {
|
|
105
|
+
js += 'Math.trunc(';
|
|
106
|
+
wrappedInTrunc = true;
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
case 'SEMI':
|
|
110
|
+
if (wrappedInTrunc) {
|
|
111
|
+
js += ')';
|
|
112
|
+
wrappedInTrunc = false;
|
|
113
|
+
}
|
|
114
|
+
js += ';';
|
|
115
|
+
afterIntKw = false;
|
|
116
|
+
break;
|
|
117
|
+
case 'NEXT':
|
|
118
|
+
// no-op in JS — samples are pushed to arrays via the out Proxy
|
|
119
|
+
break;
|
|
120
|
+
default:
|
|
121
|
+
js += token.value;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return js;
|
|
127
|
+
}
|