@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,105 @@
|
|
|
1
|
+
# Devbin Backwards Compatibility
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
RaftJS is used by both Axiom and Cog. Axiom was already using the current devbin payload layout, but Cog firmware v1.9.5 is already in production and publishes device data using an older RaftCore record body layout. The app therefore has to keep supporting the newer Axiom/Cog format while remaining compatible with Cog v1.9.5 devices in the field.
|
|
6
|
+
|
|
7
|
+
The failure mode seen in Axiom Experiment App was:
|
|
8
|
+
|
|
9
|
+
- Cog connected successfully over BLE.
|
|
10
|
+
- The live preview received binary device messages.
|
|
11
|
+
- Accelerometer data did not appear.
|
|
12
|
+
- The browser logged malformed sample warnings and previously could surface errors such as `RangeError: Offset is outside the bounds of the DataView`.
|
|
13
|
+
|
|
14
|
+
The root cause was a format mismatch. RaftJS assumed that devbin records contained a device sequence byte followed by length-prefixed samples. Cog v1.9.5 sends fixed-size raw samples without the device sequence byte. When parsed as the newer format, the first timestamp byte was interpreted as a sample length, so the parser read the wrong boundaries and eventually tried to decode attributes past the end of a sample.
|
|
15
|
+
|
|
16
|
+
## Supported Record Layouts
|
|
17
|
+
|
|
18
|
+
### Current Format
|
|
19
|
+
|
|
20
|
+
Current devbin frames use:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
[msgType:2]
|
|
24
|
+
[devbin envelope: magic/version, topicIndex, envelopeSeq]
|
|
25
|
+
[recordLen:2]
|
|
26
|
+
[statusBus:1]
|
|
27
|
+
[address:4]
|
|
28
|
+
[devTypeIdx:2]
|
|
29
|
+
[deviceSeq:1]
|
|
30
|
+
[sampleLen:1][sampleData:sampleLen]...
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`sampleData` contains the poll-result timestamp followed by the device payload. This is the format used by current Axiom builds and newer Raft firmware.
|
|
34
|
+
|
|
35
|
+
### Cog v1.9.5 Legacy Format
|
|
36
|
+
|
|
37
|
+
Cog v1.9.5 uses the older record body:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
[msgType:2]
|
|
41
|
+
optional [devbin envelope]
|
|
42
|
+
[recordLen:2]
|
|
43
|
+
[statusBus:1]
|
|
44
|
+
[address:4]
|
|
45
|
+
[devTypeIdx:2]
|
|
46
|
+
[timestamp:2][payload:fixedSize]...
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
There is no per-device sequence byte and no per-sample length byte. Samples are decoded using the fixed payload size derived from the device type metadata.
|
|
50
|
+
|
|
51
|
+
In testing, Cog v1.9.5 was observed sending a hybrid shape: the newer `DB` devbin envelope was present, but each record still used the legacy raw sample body. The parser must therefore not infer the record body format from the envelope alone.
|
|
52
|
+
|
|
53
|
+
## RaftJS Parser Behavior
|
|
54
|
+
|
|
55
|
+
`DeviceManager.handleClientMsgBinary` now supports two record payload modes:
|
|
56
|
+
|
|
57
|
+
- `lengthPrefixed`: current records with `deviceSeq` and `[sampleLen][sampleData]`.
|
|
58
|
+
- `legacyRaw`: Cog v1.9.5 records with fixed-size `[timestamp][payload]` samples and no `deviceSeq`.
|
|
59
|
+
|
|
60
|
+
The parser first locates the record stream using the message prefix and optional devbin envelope. After it has the `devTypeIdx`, it fetches the device type info and validates the actual sample layout against the metadata. This lets it correctly identify the Cog v1.9.5 hybrid case where the frame has the current envelope but the record body is legacy raw.
|
|
61
|
+
|
|
62
|
+
Malformed samples are bounded to their record/sample range before decoding. If a sample cannot be decoded, RaftJS skips that sample and emits a throttled warning rather than throwing from `DataView`.
|
|
63
|
+
|
|
64
|
+
## Legacy Sample Size
|
|
65
|
+
|
|
66
|
+
For legacy raw records, the fixed sample size is:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
2-byte timestamp + payload size
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The payload size is normally derived from the sum of the attribute struct sizes in `resp.a`. If a custom response handler is used, or if the schema cannot be sized safely, RaftJS falls back to `resp.b`.
|
|
73
|
+
|
|
74
|
+
This schema-derived sizing is required for Cog v1.9.5 light sensor records because that firmware reports a doubled light payload size in metadata while the actual raw record contains one fixed payload matching the attribute schema.
|
|
75
|
+
|
|
76
|
+
## Direct Device Key Compatibility
|
|
77
|
+
|
|
78
|
+
Cog v1.9.5 publishes multiple direct-connected devices on bus `0`, address `0`. In the current key scheme this collapses to a single `0_0` device and causes metadata collisions, for example LightSensors and Power sharing the same key.
|
|
79
|
+
|
|
80
|
+
For legacy raw records only, RaftJS appends the device type index to direct bus/address zero records:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
0_0_<devTypeIdx>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This keeps legacy Cog direct devices distinct while preserving the existing key behavior for Axiom and newer length-prefixed records.
|
|
87
|
+
|
|
88
|
+
Command paths should use the stored `DeviceState.busName` and `DeviceState.deviceAddress`, not only parse the displayed device key. This avoids sending commands to an address such as `0_2` when the compatibility key is `0_0_2`.
|
|
89
|
+
|
|
90
|
+
## Verification
|
|
91
|
+
|
|
92
|
+
The compatibility behavior is covered by `src/RaftDeviceManager.test.ts`:
|
|
93
|
+
|
|
94
|
+
- current length-prefixed records decode correctly
|
|
95
|
+
- Cog v1.9.5 raw accelerometer records decode correctly
|
|
96
|
+
- Cog v1.9.5 raw records inside a devbin envelope decode correctly
|
|
97
|
+
- legacy direct devices with bus/address `0_0` stay distinct by device type index
|
|
98
|
+
|
|
99
|
+
The real-device validation used Axiom Experiment App:
|
|
100
|
+
|
|
101
|
+
- Cog v1.9.5 connected over BLE as `Robotical Cog`
|
|
102
|
+
- live `MXC400xXC` `ax`, `ay`, and `az` samples appeared in simple mode
|
|
103
|
+
- no page errors were observed during the live-preview watch
|
|
104
|
+
- an Axiom real-device connection still decoded live LSM6DS data, confirming the current length-prefixed path remained intact
|
|
105
|
+
|
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
# Pseudocode-to-JS Transpiler for Custom Attribute Decoding
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
When new Raft devices are added, their poll-response decoding logic is defined in DeviceTypeRecords.json via a compact pseudocode in the `resp.c.c` field. Today, `RaftCustomAttrHandler` handles these by:
|
|
6
|
+
|
|
7
|
+
1. **Hard-coded native handlers** — a chain of `if/else if` blocks keyed on `customFnDef.n` (e.g. `"max30101_fifo"`, `"lsm6ds_fifo"`, `"gravity_o2_calc"`). Every new device with custom decoding requires a raftjs code change and release.
|
|
8
|
+
|
|
9
|
+
2. **Pre-supplied JS via `customFnDef.j`** — an optional JavaScript string that is compiled with `new Function()` and cached. This works but requires the server or device-type record author to hand-write the JS in addition to the pseudocode.
|
|
10
|
+
|
|
11
|
+
The goal is to **automatically transpile the pseudocode (`customFnDef.c`) to JavaScript at runtime**, compile it via `new Function()`, and cache it — eliminating the need to update raftjs when new devices are added, and removing the need for authors to maintain a separate `j` field.
|
|
12
|
+
|
|
13
|
+
## Relationship to the Existing Python Tooling
|
|
14
|
+
|
|
15
|
+
The firmware build pipeline already contains two Python scripts in `RaftCore/scripts/`:
|
|
16
|
+
|
|
17
|
+
- **`PseudocodeHandler.py`** — a regex-based lexer and multi-target code generator (C++, Python, TypeScript) for the pseudocode language.
|
|
18
|
+
- **`DecodeGenerator.py`** — uses `PseudocodeHandler` to generate C++ decode functions (structs, extraction loops, timestamp handling) that are compiled into the firmware.
|
|
19
|
+
|
|
20
|
+
**These Python scripts remain as-is.** They are part of the firmware (ESP-IDF / C++) build and requiring Node.js on the firmware build platform would be an unnecessary dependency. The raftjs runtime transpiler described in this document is a separate, independent implementation targeting the same pseudocode language, but producing JavaScript instead of C++.
|
|
21
|
+
|
|
22
|
+
## Runtime Data Flow
|
|
23
|
+
|
|
24
|
+
The full `DeviceTypeRecords.json` is **not** available to raftjs at build time. Only the `devInfoJson` contents are delivered at runtime when a new device is detected by the firmware. The pseudocode lives inside `devInfoJson.resp.c.c`, so it is available to raftjs at the point a device is first seen:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Firmware detects device
|
|
28
|
+
→ sends devInfoJson to raftjs over BLE/WebSocket/Serial
|
|
29
|
+
→ raftjs parses into DeviceTypePollRespMetadata
|
|
30
|
+
→ resp.c.c contains the pseudocode string
|
|
31
|
+
→ transpile once → compile via new Function() → cache
|
|
32
|
+
→ all subsequent poll decodes use cached JIT'd function
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The transpilation + compilation cost is a one-time operation per device type. Subsequent poll responses use the cached function directly.
|
|
36
|
+
|
|
37
|
+
## Pseudocode Language Specification
|
|
38
|
+
|
|
39
|
+
The pseudocode is a minimal C-like language. Its grammar is defined by the tokenizer in `PseudocodeHandler.py` (in `RaftCore/scripts/`). The full token set:
|
|
40
|
+
|
|
41
|
+
| Token | Pattern | Notes |
|
|
42
|
+
|-------|---------|-------|
|
|
43
|
+
| `int` | keyword | Integer variable declaration |
|
|
44
|
+
| `float` | keyword | Float variable declaration |
|
|
45
|
+
| `return` | keyword | Return statement |
|
|
46
|
+
| `while` | keyword | While loop |
|
|
47
|
+
| `next` | keyword | Advance to next output record (loop iteration boundary) |
|
|
48
|
+
| `if` | — | Parsed as an `ID` token; passes through verbatim (valid JS keyword) |
|
|
49
|
+
| Identifiers | `[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*` | Includes dotted access like `out.gx`, `buf` |
|
|
50
|
+
| Integer literals | `\d+` | |
|
|
51
|
+
| Float literals | `\d+\.\d*` | e.g. `20.9`, `120.0` |
|
|
52
|
+
| Operators | `&&` `\|\|` `!` `<<` `>>` `&` `\|` `^` `~` `++` `--` `+` `-` `*` `/` `%` `==` `!=` `<=` `>=` `<` `>` `=` | Standard C operators |
|
|
53
|
+
| Delimiters | `;` `,` `(` `)` `{` `}` `[` `]` | |
|
|
54
|
+
|
|
55
|
+
### Semantic Conventions
|
|
56
|
+
|
|
57
|
+
- **`buf`** — the input byte array (a `Uint8Array` slice of the poll response)
|
|
58
|
+
- **`out.<name>`** — write an output attribute value. e.g. `out.gx = ...` pushes a value to the `gx` attribute vector
|
|
59
|
+
- **`next`** — marks the end of one output record in a multi-sample loop (e.g. FIFO reads). In JS this is a no-op since we just push to arrays.
|
|
60
|
+
- **`int` / `float`** — type declarations. In JS these become `let`.
|
|
61
|
+
|
|
62
|
+
### Real-World Examples
|
|
63
|
+
|
|
64
|
+
**max30101_fifo** (heart-rate sensor FIFO):
|
|
65
|
+
```
|
|
66
|
+
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;}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**lsm6ds_fifo** (6-axis IMU FIFO):
|
|
70
|
+
```
|
|
71
|
+
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;}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**scd40_calc** (CO2 sensor):
|
|
75
|
+
```
|
|
76
|
+
out.CO2 = buf[0]; out.Temp = -45.0 + (175.0 * buf[1] / 65535.0); out.Humidity = (100.0 * buf[2] / 65535.0);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**gravity_o2_calc** (oxygen sensor):
|
|
80
|
+
```
|
|
81
|
+
float key = 20.9/120.0; float val = key * (buf[0] + (buf[1]/10.0) + (buf[2]/100.0)); out.oxygen = val;
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Proposed Design
|
|
85
|
+
|
|
86
|
+
### Architecture Overview
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
DeviceTypeRecords.json
|
|
90
|
+
resp.c.c (pseudocode string)
|
|
91
|
+
│
|
|
92
|
+
▼
|
|
93
|
+
┌──────────────────────┐
|
|
94
|
+
│ PseudocodeTranspiler │ (new class in raftjs)
|
|
95
|
+
│ │
|
|
96
|
+
│ 1. Tokenize │ ─── regex-based lexer (port of PseudocodeHandler.py)
|
|
97
|
+
│ 2. Transform tokens │ ─── apply JS-specific substitutions
|
|
98
|
+
│ 3. Emit JS string │ ─── generate valid JS function body
|
|
99
|
+
└──────┬───────────────┘
|
|
100
|
+
│ JS source string
|
|
101
|
+
▼
|
|
102
|
+
┌──────────────────────┐
|
|
103
|
+
│ new Function(...) │ ─── V8/JSC/Hermes compiles & JITs
|
|
104
|
+
└──────┬───────────────┘
|
|
105
|
+
│ CustomAttrJsFn
|
|
106
|
+
▼
|
|
107
|
+
┌──────────────────────┐
|
|
108
|
+
│ CustomAttrHandler │ ─── caches compiled function, calls it for each poll
|
|
109
|
+
└──────────────────────┘
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Step 1: Port the Lexer to TypeScript
|
|
113
|
+
|
|
114
|
+
Port the `PseudocodeHandler.lexer()` method. This is a simple regex-based tokenizer. The TypeScript version corrects the token ordering issues found in the Python original (see Appendix A):
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// PseudocodeTranspiler.ts
|
|
118
|
+
|
|
119
|
+
interface Token {
|
|
120
|
+
type: string;
|
|
121
|
+
value: string | number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const TOKEN_SPEC: [string, RegExp][] = [
|
|
125
|
+
['FLOAT_KW', /float\b/],
|
|
126
|
+
['INT_KW', /int\b/],
|
|
127
|
+
['RETURN', /return\b/],
|
|
128
|
+
['WHILE', /while\b/],
|
|
129
|
+
['NEXT', /next\b/],
|
|
130
|
+
['HEX_NUM', /0x[0-9A-Fa-f]+/],
|
|
131
|
+
['NUM_FLOAT', /\d+\.\d*/],
|
|
132
|
+
['NUM_INT', /\d+/],
|
|
133
|
+
['ID', /[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*/],
|
|
134
|
+
['LSHIFT', /<</],
|
|
135
|
+
['RSHIFT', />>/],
|
|
136
|
+
['LOGICAL_AND', /&&/],
|
|
137
|
+
['LOGICAL_OR', /\|\|/],
|
|
138
|
+
['REL_OP', /==|!=|<=|>=|<|>/],
|
|
139
|
+
['INC_OP', /\+\+/],
|
|
140
|
+
['DEC_OP', /--/],
|
|
141
|
+
['ASSIGN', /=/],
|
|
142
|
+
['ADD_OP', /\+/],
|
|
143
|
+
['SUB_OP', /-/],
|
|
144
|
+
['MUL_OP', /\*/],
|
|
145
|
+
['DIV_OP', /\//],
|
|
146
|
+
['MOD_OP', /%/],
|
|
147
|
+
['BITWISE_AND', /&/],
|
|
148
|
+
['BITWISE_OR', /\|/],
|
|
149
|
+
['BITWISE_XOR', /\^/],
|
|
150
|
+
['BITWISE_NOT', /~/],
|
|
151
|
+
['LOGICAL_NOT', /!/],
|
|
152
|
+
['SEMI', /;/],
|
|
153
|
+
['COMMA', /,/],
|
|
154
|
+
['LPAREN', /\(/],
|
|
155
|
+
['RPAREN', /\)/],
|
|
156
|
+
['LBRACE', /\{/],
|
|
157
|
+
['RBRACE', /\}/],
|
|
158
|
+
['LBRACK', /\[/],
|
|
159
|
+
['RBRACK', /\]/],
|
|
160
|
+
['WS', /[ \t\n]+/],
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
function tokenize(code: string): Token[] {
|
|
164
|
+
const combined = new RegExp(
|
|
165
|
+
TOKEN_SPEC.map(([name, re]) => `(?<${name}>${re.source})`).join('|'),
|
|
166
|
+
'g'
|
|
167
|
+
);
|
|
168
|
+
const tokens: Token[] = [];
|
|
169
|
+
let match: RegExpExecArray | null;
|
|
170
|
+
while ((match = combined.exec(code)) !== null) {
|
|
171
|
+
for (const [name] of TOKEN_SPEC) {
|
|
172
|
+
if (match.groups![name] !== undefined) {
|
|
173
|
+
if (name !== 'WS') {
|
|
174
|
+
tokens.push({ type: name, value: match[0] });
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return tokens;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Key differences from the Python version (see Appendix A for details on the Python issues):
|
|
185
|
+
- `HEX_NUM` token added (matched before `NUM_INT`) so `0x0F` doesn't get split into `0` + `x0F` (Appendix A.2).
|
|
186
|
+
- `NUM_FLOAT` ordered before `NUM_INT` so float literals like `20.9` tokenize correctly (Appendix A.1).
|
|
187
|
+
- `if` is not a separate keyword — it falls through to `ID` and passes through verbatim, which is valid JS. Same as in the Python version.
|
|
188
|
+
- Token ordering is critical: multi-character operators (`<<`, `>>`, `&&`, `||`, `++`, `--`, `==`, etc.) must appear before their single-character counterparts.
|
|
189
|
+
- `INT_KW` / `FLOAT_KW` are handled as token types (not via regex substitution on values), avoiding the accidental matching of identifiers containing "int" or "float" (Appendix A.7).
|
|
190
|
+
|
|
191
|
+
### Step 2: Token Transformation & JS Code Generation
|
|
192
|
+
|
|
193
|
+
The key transformation is converting pseudocode semantics to the `CustomAttrJsFn` calling convention used by `handleAttr()`. The function receives these parameters:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
(buf: Uint8Array, attrValues: Record<string, number[]>, attrValueVecs: number[][],
|
|
197
|
+
pollRespMetadata: DeviceTypePollRespMetadata, msgBuffer: Uint8Array, msgBufIdx: number, numMsgBytes: number)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The transformations needed:
|
|
201
|
+
|
|
202
|
+
| Pseudocode | JS Output | Rationale |
|
|
203
|
+
|------------|-----------|-----------|
|
|
204
|
+
| `int x = ...` | `let x = ...` | JS `let` for local variables |
|
|
205
|
+
| `float x = ...` | `let x = ...` | JS `let` for local variables |
|
|
206
|
+
| `out.attrName = expr` | `attrValues["attrName"].push(expr)` | Push decoded value to the named attribute array |
|
|
207
|
+
| `next` | (empty / no-op) | In JS we just push to arrays; no struct pointer advancement needed |
|
|
208
|
+
| `buf[i]` | `buf[i]` | Direct — `buf` is already the sliced `Uint8Array` |
|
|
209
|
+
|
|
210
|
+
The `out.<name>` transformation is the most important. The Python `PseudocodeHandler.__main__` block already demonstrates this substitution for TypeScript mode:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
substitutions["out\.(.*)"] = "attrValues['\\1'].push(0); attrValues['\\1'][attrValues['\\1'].length-1] "
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
However, the push-then-overwrite pattern (`push(0)` then assign `[length-1]`) is awkward. A cleaner approach in the transpiler:
|
|
217
|
+
|
|
218
|
+
**Option A — Detect `out.X = expr;` as a complete statement and emit `attrValues["X"].push(expr);`**
|
|
219
|
+
|
|
220
|
+
This requires looking ahead from an `out.X` token to find the `=` and the expression up to the `;`, then wrapping it. This is more correct and avoids the double-write.
|
|
221
|
+
|
|
222
|
+
**Option B — Use a Proxy object** that intercepts property sets:
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
const out = new Proxy({}, {
|
|
226
|
+
set(_, prop, value) {
|
|
227
|
+
attrValues[prop].push(value);
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Then `out.gx = expr` naturally becomes `attrValues["gx"].push(expr)`. The pseudocode passes through almost verbatim — only `int`/`float` → `let` and `next` → `` substitutions are needed.
|
|
234
|
+
|
|
235
|
+
**Option B is strongly recommended** because:
|
|
236
|
+
- It avoids complex expression boundary detection
|
|
237
|
+
- The pseudocode passes through with minimal transformation
|
|
238
|
+
- The Proxy overhead is negligible since decoding runs per-poll (not in a tight inner loop of millions of iterations)
|
|
239
|
+
- It's significantly simpler to implement and maintain
|
|
240
|
+
|
|
241
|
+
### Step 3: JS Code Generation Function
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
function transpilePseudocodeToJs(pseudocode: string): string {
|
|
245
|
+
const tokens = tokenize(pseudocode);
|
|
246
|
+
let js = '';
|
|
247
|
+
|
|
248
|
+
// Preamble: create the out proxy
|
|
249
|
+
js += 'const out = new Proxy({}, { set(_, p, v) { if (attrValues[p]) attrValues[p].push(v); return true; } });\n';
|
|
250
|
+
|
|
251
|
+
for (const token of tokens) {
|
|
252
|
+
switch (token.type) {
|
|
253
|
+
case 'INT_KW':
|
|
254
|
+
case 'FLOAT_KW':
|
|
255
|
+
js += 'let ';
|
|
256
|
+
break;
|
|
257
|
+
case 'NEXT':
|
|
258
|
+
// no-op in JS — samples are pushed to arrays
|
|
259
|
+
break;
|
|
260
|
+
case 'SEMI':
|
|
261
|
+
js += ';\n';
|
|
262
|
+
break;
|
|
263
|
+
case 'LBRACE':
|
|
264
|
+
js += ' {\n';
|
|
265
|
+
break;
|
|
266
|
+
case 'RBRACE':
|
|
267
|
+
js += '}\n';
|
|
268
|
+
break;
|
|
269
|
+
default:
|
|
270
|
+
js += token.value;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return js;
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Step 4: Compilation and Caching in CustomAttrHandler
|
|
280
|
+
|
|
281
|
+
The compiled function is created with `new Function()` and cached, exactly as the existing `j` field handler does:
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
private getOrCompileFunction(customFnDef: CustomFunctionDefinition): CustomAttrJsFn | null {
|
|
285
|
+
// Prefer explicit JS if provided
|
|
286
|
+
let jsSource = customFnDef.j?.trim();
|
|
287
|
+
|
|
288
|
+
// Otherwise, transpile from pseudocode
|
|
289
|
+
if (!jsSource && customFnDef.c) {
|
|
290
|
+
jsSource = transpilePseudocodeToJs(customFnDef.c);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!jsSource) return null;
|
|
294
|
+
|
|
295
|
+
const cacheKey = `${customFnDef.n}::${jsSource}`;
|
|
296
|
+
const cached = this._jsFunctionCache.get(cacheKey);
|
|
297
|
+
if (cached) return cached;
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const fn = new Function(
|
|
301
|
+
'buf', 'attrValues', 'attrValueVecs',
|
|
302
|
+
'pollRespMetadata', 'msgBuffer', 'msgBufIdx', 'numMsgBytes',
|
|
303
|
+
jsSource
|
|
304
|
+
) as CustomAttrJsFn;
|
|
305
|
+
this._jsFunctionCache.set(cacheKey, fn);
|
|
306
|
+
return fn;
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.error(`Failed to compile function ${customFnDef.n}:`, err);
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Priority order:
|
|
315
|
+
1. `customFnDef.j` — explicit JS (existing mechanism, kept for backwards compat and for cases where the transpiler can't handle something)
|
|
316
|
+
2. `customFnDef.c` — pseudocode, transpiled to JS
|
|
317
|
+
3. Hard-coded native handlers — **deprecated**, kept temporarily as fallback
|
|
318
|
+
|
|
319
|
+
### Step 5: Simplify handleAttr()
|
|
320
|
+
|
|
321
|
+
Once the transpiler is in place, `handleAttr()` becomes:
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
public handleAttr(pollRespMetadata: DeviceTypePollRespMetadata, msgBuffer: Uint8Array, msgBufIdx: number): number[][] {
|
|
325
|
+
const numMsgBytes = pollRespMetadata.b;
|
|
326
|
+
const attrValueVecs: number[][] = [];
|
|
327
|
+
const attrValues: Record<string, number[]> = {};
|
|
328
|
+
|
|
329
|
+
for (let attrIdx = 0; attrIdx < pollRespMetadata.a.length; attrIdx++) {
|
|
330
|
+
attrValueVecs.push([]);
|
|
331
|
+
attrValues[pollRespMetadata.a[attrIdx].n] = attrValueVecs[attrIdx];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const customFnDef = pollRespMetadata.c;
|
|
335
|
+
if (!customFnDef) return attrValueVecs;
|
|
336
|
+
|
|
337
|
+
const buf = msgBuffer.slice(msgBufIdx, msgBufIdx + numMsgBytes);
|
|
338
|
+
if (buf.length < numMsgBytes) return [];
|
|
339
|
+
|
|
340
|
+
const fn = this.getOrCompileFunction(customFnDef);
|
|
341
|
+
if (!fn) return attrValueVecs;
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
fn(buf, attrValues, attrValueVecs, pollRespMetadata, msgBuffer, msgBufIdx, numMsgBytes);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
console.error(`CustomAttrHandler function ${customFnDef.n} execution failed`, err);
|
|
347
|
+
}
|
|
348
|
+
return attrValueVecs;
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
The entire `if/else if` chain for `max30101_fifo`, `lsm6ds_fifo`, `gravity_o2_calc` is removed.
|
|
353
|
+
|
|
354
|
+
## Signed Integer Handling
|
|
355
|
+
|
|
356
|
+
The pseudocode uses expressions like `(buf[k+1]<<8)|buf[k]` to reconstruct 16-bit values. In the current hard-coded `lsm6ds_fifo` handler, the `toInt16()` helper converts to signed. But the pseudocode itself doesn't encode signedness.
|
|
357
|
+
|
|
358
|
+
Options:
|
|
359
|
+
1. **Add a `toInt16()` / `toInt32()` helper to the function scope.** The transpiler preamble could inject it:
|
|
360
|
+
```javascript
|
|
361
|
+
function toInt16(lo, hi) { const u = (hi << 8) | lo; return u & 0x8000 ? u - 0x10000 : u; }
|
|
362
|
+
```
|
|
363
|
+
Then update the pseudocode to use `out.gx = toInt16(buf[k], buf[k+1])` instead of raw bit manipulation.
|
|
364
|
+
|
|
365
|
+
2. **Rely on the attribute `t` field.** The attribute metadata already declares signedness (e.g. `"t": "<h"` = signed little-endian int16). The caller of `handleAttr()` could apply sign extension post-hoc based on the type and bit width. This keeps the pseudocode simple but may need changes in `RaftAttributeHandler`.
|
|
366
|
+
|
|
367
|
+
3. **Accept that the current pseudocode produces unsigned values** and note that if sign-extension is needed, the pseudocode should use an explicit expression like `out.gx=((buf[k+1]<<8)|buf[k])<<16>>16;` (arithmetic right-shift sign-extends in JS).
|
|
368
|
+
|
|
369
|
+
**Recommendation:** Option 1 — inject a small set of helper functions into the transpiled function scope. This is the cleanest approach and requires no pseudocode changes. The transpiler preamble becomes:
|
|
370
|
+
|
|
371
|
+
```javascript
|
|
372
|
+
const out = new Proxy({}, { set(_, p, v) { if (attrValues[p]) attrValues[p].push(v); return true; } });
|
|
373
|
+
function toInt16(lo, hi) { const u = (hi << 8) | lo; return u & 0x8000 ? u - 0x10000 : u; }
|
|
374
|
+
function toInt32(b0, b1, b2, b3) { return (b3 << 24) | (b2 << 16) | (b1 << 8) | b0; }
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
For existing pseudocode that doesn't call `toInt16()`, values remain unsigned — which is the same as what the pseudocode literally says. If signedness matters, the pseudocode can be updated to use the helper, or new pseudocode for new devices can use it from day one.
|
|
378
|
+
|
|
379
|
+
## Security Considerations
|
|
380
|
+
|
|
381
|
+
`new Function()` is essentially `eval()`. Mitigations:
|
|
382
|
+
|
|
383
|
+
1. **Input is trusted** — the pseudocode comes from DeviceTypeRecords.json which is authored by device developers and delivered from the Raft device firmware or a trusted server. It is not user-supplied input from an untrusted source.
|
|
384
|
+
|
|
385
|
+
2. **Lexer whitelisting** — the tokenizer only recognizes a fixed set of tokens (identifiers, numbers, operators, keywords). Arbitrary strings like `fetch(...)`, `import(...)`, `require(...)`, or template literals cannot be constructed from these tokens. The lexer acts as a whitelist filter.
|
|
386
|
+
|
|
387
|
+
3. **No string literals** — the pseudocode language has no string literal token type. This prevents injection of arbitrary code via string concatenation.
|
|
388
|
+
|
|
389
|
+
4. **Scope isolation** — `new Function()` does not have access to the enclosing lexical scope (unlike `eval()`). The function only sees its explicit parameters plus globals.
|
|
390
|
+
|
|
391
|
+
5. **Cache key includes source** — if the pseudocode changes, a new function is compiled. Stale functions are not reused.
|
|
392
|
+
|
|
393
|
+
The existing `j` field mechanism already uses `new Function()` with the same trust model, so the transpiler does not introduce any new attack surface.
|
|
394
|
+
|
|
395
|
+
## Implementation Plan
|
|
396
|
+
|
|
397
|
+
### New File: `src/PseudocodeTranspiler.ts`
|
|
398
|
+
|
|
399
|
+
Contains:
|
|
400
|
+
- `Token` interface
|
|
401
|
+
- `tokenize(code: string): Token[]` — lexer
|
|
402
|
+
- `transpilePseudocodeToJs(pseudocode: string): string` — token transform + code generation
|
|
403
|
+
|
|
404
|
+
### Modified File: `src/RaftCustomAttrHandler.ts`
|
|
405
|
+
|
|
406
|
+
Changes:
|
|
407
|
+
- Import `transpilePseudocodeToJs`
|
|
408
|
+
- Modify `getOrCompileJsFunction()` → `getOrCompileFunction()` to try `customFnDef.c` transpilation when `customFnDef.j` is absent
|
|
409
|
+
- Remove the hard-coded `if/else if` chain from `handleAttr()` (or keep as a last-resort fallback behind a flag during migration)
|
|
410
|
+
- Simplify `handleAttr()` to always use the compiled function path
|
|
411
|
+
|
|
412
|
+
### New File: `src/PseudocodeTranspiler.test.ts`
|
|
413
|
+
|
|
414
|
+
Unit tests covering:
|
|
415
|
+
- Tokenizer output for each pseudocode example
|
|
416
|
+
- Transpiled JS output for each pseudocode example
|
|
417
|
+
- End-to-end: transpile → compile → execute with mock `buf` data and verify `attrValues` output
|
|
418
|
+
- All four known pseudocode strings (`max30101_fifo`, `lsm6ds_fifo`, `scd40_calc`, `gravity_o2_calc`)
|
|
419
|
+
|
|
420
|
+
### Migration Strategy
|
|
421
|
+
|
|
422
|
+
1. Implement transpiler and integrate into `getOrCompileFunction()`
|
|
423
|
+
2. Keep hard-coded handlers as fallback initially (try transpiled function first, fall back to hard-coded if transpilation fails)
|
|
424
|
+
3. Validate numeric equivalence for all four known custom functions using test data
|
|
425
|
+
4. Once validated, remove the hard-coded handlers entirely
|
|
426
|
+
5. The `j` field remains supported for edge cases where hand-written JS is preferable
|
|
427
|
+
|
|
428
|
+
## Transpiler Output Examples
|
|
429
|
+
|
|
430
|
+
### max30101_fifo
|
|
431
|
+
|
|
432
|
+
Input pseudocode:
|
|
433
|
+
```
|
|
434
|
+
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;}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Transpiled JS function body:
|
|
438
|
+
```javascript
|
|
439
|
+
const out = new Proxy({}, { set(_, p, v) { if (attrValues[p]) attrValues[p].push(v); return true; } });
|
|
440
|
+
let N=(buf[0]+32-buf[2])%32;
|
|
441
|
+
let k=3;
|
|
442
|
+
let i=0;
|
|
443
|
+
while (i<N) {
|
|
444
|
+
out.Red=(buf[k]<<16)|(buf[k+1]<<8)|buf[k+2];
|
|
445
|
+
out.IR=(buf[k+3]<<16)|(buf[k+4]<<8)|buf[k+5];
|
|
446
|
+
k+=6;
|
|
447
|
+
i++;
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### gravity_o2_calc
|
|
452
|
+
|
|
453
|
+
Input pseudocode:
|
|
454
|
+
```
|
|
455
|
+
float key = 20.9/120.0; float val = key * (buf[0] + (buf[1]/10.0) + (buf[2]/100.0)); out.oxygen = val;
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Transpiled JS function body:
|
|
459
|
+
```javascript
|
|
460
|
+
const out = new Proxy({}, { set(_, p, v) { if (attrValues[p]) attrValues[p].push(v); return true; } });
|
|
461
|
+
let key=20.9/120.0;
|
|
462
|
+
let val=key*(buf[0]+(buf[1]/10.0)+(buf[2]/100.0));
|
|
463
|
+
out.oxygen=val;
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
## Summary
|
|
467
|
+
|
|
468
|
+
| Aspect | Current | Proposed |
|
|
469
|
+
|--------|---------|----------|
|
|
470
|
+
| New device support | Requires raftjs code change | Automatic from `devInfoJson` at runtime |
|
|
471
|
+
| Decoding definition | Pseudocode (`c` field) + optional hand-written JS (`j` field) + hard-coded TS | Pseudocode auto-transpiled to JS at runtime; `j` field still supported as override |
|
|
472
|
+
| Performance | Native TS (hard-coded) or `new Function` (j field) | `new Function` (transpiled) — equivalent JIT performance |
|
|
473
|
+
| Maintenance | Three parallel implementations (pseudocode, C++ codegen, TS hard-code) | Two: pseudocode + auto-generated JS (C++ codegen remains for firmware) |
|
|
474
|
+
| Complexity | ~100 lines of hard-coded handlers growing with each device | ~80-line transpiler, stable regardless of device count |
|
|
475
|
+
| Build-time dependency | None (hard-coded) | None — transpilation happens at runtime when `devInfoJson` arrives |
|
|
476
|
+
| Python tooling | `PseudocodeHandler.py` + `DecodeGenerator.py` for firmware C++ codegen | Unchanged — firmware build pipeline is unaffected |
|
|
477
|
+
|
|
478
|
+
## Appendix A: Observations on the Python PseudocodeHandler / DecodeGenerator
|
|
479
|
+
|
|
480
|
+
The Python scripts work correctly for the current set of device types, but there are some issues and potential improvements worth noting. These are suggestions only — none are blocking for the raftjs transpiler work.
|
|
481
|
+
|
|
482
|
+
### A.1 Token Ordering Bug: `NUM_FLOAT` after `NUM_INT`
|
|
483
|
+
|
|
484
|
+
In `PseudocodeHandler.py`, the token list has:
|
|
485
|
+
|
|
486
|
+
```python
|
|
487
|
+
('NUM_INT', r'\d+'), # Integer number
|
|
488
|
+
('NUM_FLOAT', r'\d+\.\d*'), # Float number
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Because Python's `re.finditer` with alternation tries each alternative left-to-right and takes the first match, `NUM_INT` will always match the integer part of a float before `NUM_FLOAT` gets a chance. For example, `20.9` is tokenized as `NUM_INT(20)`, then `.` fails to match any token (silently skipped), then `NUM_INT(9)`.
|
|
492
|
+
|
|
493
|
+
This happens to work for the current pseudocode expressions like `20.9/120.0` because `20 / 120` in integer arithmetic followed by further operations still produces reasonable results in C++ codegen (where `int` division truncates). But it means the lexer never actually produces `NUM_FLOAT` tokens.
|
|
494
|
+
|
|
495
|
+
**Fix:** Move `NUM_FLOAT` before `NUM_INT` in the token list:
|
|
496
|
+
|
|
497
|
+
```python
|
|
498
|
+
('NUM_FLOAT', r'\d+\.\d*'), # Float number — must precede NUM_INT
|
|
499
|
+
('NUM_INT', r'\d+'), # Integer number
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### A.2 Hex Literal Handling
|
|
503
|
+
|
|
504
|
+
The pseudocode uses hex literals like `0x0F`, `0x03`, `0x60`. The lexer has no `HEX_NUM` token, so `0x0F` is tokenized as `NUM_INT(0)` + `ID(x0F)`. This works by accident in C++ output (it reconstructs `0x0F` from the concatenated tokens), but is fragile and would break if identifiers were processed differently.
|
|
505
|
+
|
|
506
|
+
**Fix:** Add a hex literal token before `NUM_INT`:
|
|
507
|
+
|
|
508
|
+
```python
|
|
509
|
+
('HEX_NUM', r'0x[0-9A-Fa-f]+'), # Hex number — must precede NUM_INT
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### A.3 Missing `if` / `else` Keywords
|
|
513
|
+
|
|
514
|
+
The pseudocode for `lsm6ds_fifo` uses `if` statements: `if(N>maxN){N=maxN;}`. There is no `IF` token in the lexer — `if` is matched as an `ID`. This works because all three code generators (C++, Python, TypeScript) pass `ID` tokens through verbatim, and `if` is a valid keyword in all three languages.
|
|
515
|
+
|
|
516
|
+
However, this means `if` doesn't get a trailing space in the generated C++/TypeScript like `while` does. The output is `if(N>maxN)` (no space — cosmetically fine but inconsistent). If readability of generated code matters, adding `IF` and `ELSE` keyword tokens would allow consistent formatting.
|
|
517
|
+
|
|
518
|
+
### A.4 `generate_python_code` — Potential Index Out of Range
|
|
519
|
+
|
|
520
|
+
In `generate_python_code()`, line:
|
|
521
|
+
|
|
522
|
+
```python
|
|
523
|
+
elif token_type == "ID" and tokens[i + 1][0] == "ASSIGN":
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
This accesses `tokens[i + 1]` without checking that `i + 1 < len(tokens)`. If an `ID` token is the last token, this will raise an `IndexError`. Not a problem with current pseudocode strings, but could bite if new pseudocode is added.
|
|
527
|
+
|
|
528
|
+
### A.5 `is_attr_type_signed` Operator Precedence
|
|
529
|
+
|
|
530
|
+
In `DecodeGenerator.py`:
|
|
531
|
+
|
|
532
|
+
```python
|
|
533
|
+
attrStr = attrType[1] if attrType[0] == ">" or attrType[0] == "<" and len(attrType) > 1 else attrType[0]
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Due to Python's operator precedence, `or` binds less tightly than `and`, so this is parsed as:
|
|
537
|
+
|
|
538
|
+
```python
|
|
539
|
+
attrStr = attrType[1] if (attrType[0] == ">" or (attrType[0] == "<" and len(attrType) > 1)) else attrType[0]
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
This means for `">"` (length 1), it will try `attrType[1]` and raise an `IndexError`. In practice this never happens because endianness-prefixed types are always 2+ chars (e.g. `">h"`) and single-char types like `"B"` or `"b"` don't start with `>` or `<`. But the logic doesn't match the intent. **Fix:**
|
|
543
|
+
|
|
544
|
+
```python
|
|
545
|
+
attrStr = attrType[1] if (attrType[0] == ">" or attrType[0] == "<") and len(attrType) > 1 else attrType[0]
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### A.6 Swapped Comments in `pystruct_map`
|
|
549
|
+
|
|
550
|
+
In `DecodeGenerator.py`:
|
|
551
|
+
|
|
552
|
+
```python
|
|
553
|
+
'b': ['int8_t', 'getUInt8AndInc'], # Signed byte
|
|
554
|
+
'B': ['uint8_t', 'getInt8AndInc'], # Unsigned byte
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
The extraction function names are swapped: `'b'` (signed) maps to `getUInt8AndInc` and `'B'` (unsigned) maps to `getInt8AndInc`. The comments are correct about the types, but the function names suggest the wrong signedness. This may be intentional (extract unsigned, then sign-extend later) or may be a subtle bug depending on how `getUInt8AndInc` / `getInt8AndInc` are implemented in the firmware.
|
|
558
|
+
|
|
559
|
+
### A.7 `generate_typescript_code` Doesn't Handle `INT`/`FLOAT` Keywords
|
|
560
|
+
|
|
561
|
+
The TypeScript code generator in `PseudocodeHandler.py` doesn't have cases for `INT` or `FLOAT` token types (unlike the C++ generator which outputs them as-is, or the Python generator which skips them). When run via the `__main__` block with `--lang typescript`, the substitutions dict maps `"int"` and `"float"` to `"let "`, but these are applied as regex substitutions on token *values* in the `else` branch — meaning they'd also match identifiers containing "int" or "float" (e.g. a variable named `interval` would become `letval`).
|
|
562
|
+
|
|
563
|
+
This is not a problem for the raftjs transpiler (which will have its own implementation), but the Python TypeScript generator should use token-type checks rather than value-based regex substitutions for keyword handling.
|