bmweb-cli 0.1.4 → 0.1.7
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/README.md +290 -27
- package/dist/bmweb.js +1591 -255
- package/package.json +1 -1
- package/runtime/core/ipofile/encode.js +701 -0
- package/runtime/core/ipofile/exec.js +155 -0
- package/runtime/core/webshim/web-serial-bus.js +59 -10
- package/runtime/screens/ipo-runtime/program.js +8 -2
|
@@ -6,7 +6,157 @@
|
|
|
6
6
|
* {ecu, procs, byid} shape. The runtime cannot tell a dropped file's exec
|
|
7
7
|
* from a shipped data/inpa-ir dump, which is the whole point -- a script the
|
|
8
8
|
* user supplies runs through the identical path, wire policy included.
|
|
9
|
+
*
|
|
10
|
+
* The token walk is a SCAN, not a parse: it hunts for declaration names and
|
|
11
|
+
* infers proc bounds from where the next one starts. That is what the VM does
|
|
12
|
+
* and what the shipped dumps were made with, so it stays. Alongside it,
|
|
13
|
+
* ipofReadContainer reads the file the way its own header says to -- a plain
|
|
14
|
+
* list of blocks -- and the result rides on the exec as `container` so
|
|
15
|
+
* ipofEncode can put every byte back exactly where it was, including the
|
|
16
|
+
* header fields no token has room for.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The block types whose payload is `size` 4-byte instruction words.
|
|
21
|
+
* @type {Object<number, boolean>}
|
|
22
|
+
*/
|
|
23
|
+
const IPOF_CODE_BLOCKS = {
|
|
24
|
+
0x01: true,
|
|
25
|
+
0x02: true,
|
|
26
|
+
0x03: true,
|
|
27
|
+
0x05: true,
|
|
28
|
+
0x21: true,
|
|
29
|
+
0x22: true,
|
|
30
|
+
0x23: true,
|
|
31
|
+
0x24: true,
|
|
32
|
+
0x25: true,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* How many bytes the constant pool's `size` entries occupy.
|
|
37
|
+
*
|
|
38
|
+
* The two dialects number their literals differently -- v1.x calls a string 04
|
|
39
|
+
* and an int 02, v5.x calls them 06 and 03 -- so the version picks the widths.
|
|
40
|
+
* Getting this wrong walks off the end of the pool and mis-frames every block
|
|
41
|
+
* after it, which is why the version is threaded in rather than guessed.
|
|
42
|
+
*
|
|
43
|
+
* @param {Uint8Array} data The file bytes.
|
|
44
|
+
* @param {number} at The first pool byte.
|
|
45
|
+
* @param {number} count How many entries the header declares.
|
|
46
|
+
* @param {number} verHi The container's major version.
|
|
47
|
+
* @returns {number} The pool's byte length.
|
|
9
48
|
*/
|
|
49
|
+
function ipofPoolLen(data, at, count, verHi) {
|
|
50
|
+
const width =
|
|
51
|
+
verHi === 1
|
|
52
|
+
? { 0x01: 1, 0x02: 2, 0x03: 4, 0x05: 8 }
|
|
53
|
+
: {
|
|
54
|
+
0x01: 1,
|
|
55
|
+
0x02: 1,
|
|
56
|
+
0x03: 2,
|
|
57
|
+
0x04: 4,
|
|
58
|
+
0x05: 8,
|
|
59
|
+
0x07: 4,
|
|
60
|
+
0x08: 4,
|
|
61
|
+
0x09: 4,
|
|
62
|
+
};
|
|
63
|
+
const strTag = verHi === 1 ? 0x04 : 0x06;
|
|
64
|
+
let i = at;
|
|
65
|
+
for (let k = 0; k < count; k += 1) {
|
|
66
|
+
if (i >= data.length) throw new Error('constant pool truncated');
|
|
67
|
+
const t = data[i];
|
|
68
|
+
i += 1;
|
|
69
|
+
if (t === strTag) {
|
|
70
|
+
const j = ipofFindNl(data, i, data.length);
|
|
71
|
+
if (j < 0) throw new Error('unterminated pool string');
|
|
72
|
+
i = j + 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const n = width[t];
|
|
76
|
+
if (n === undefined)
|
|
77
|
+
throw new Error(`unknown constant type 0x${t.toString(16)}`);
|
|
78
|
+
i += n;
|
|
79
|
+
}
|
|
80
|
+
return i - at;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Read the .IPO as the block container it is.
|
|
85
|
+
*
|
|
86
|
+
* Every block is self-delimiting -- its header names the payload length -- so
|
|
87
|
+
* the file tiles exactly, with no gaps and no padding. Reading it this way
|
|
88
|
+
* keeps the fields the token walk has nowhere to put: each block's marker, its
|
|
89
|
+
* id, and the type byte that says whether a screen's section is its SCREENFUNC
|
|
90
|
+
* or one of its LINEFUNCs.
|
|
91
|
+
*
|
|
92
|
+
* @param {Uint8Array} data The file bytes.
|
|
93
|
+
* @returns {{verHi: number, verLo: number, magic: string,
|
|
94
|
+
* blocks: Object[], globals: Uint8Array}|null} The container, or null when
|
|
95
|
+
* the bytes are not one.
|
|
96
|
+
*/
|
|
97
|
+
function ipofReadContainer(data) {
|
|
98
|
+
try {
|
|
99
|
+
if (data.length < 4) return null;
|
|
100
|
+
const verHi = data[0];
|
|
101
|
+
const verLo = data[1];
|
|
102
|
+
const m = ipofFindNl(data, 2, data.length);
|
|
103
|
+
if (m < 0) return null;
|
|
104
|
+
const magic = ipofLatin1(data, 2, m);
|
|
105
|
+
let i = m + 1;
|
|
106
|
+
const blocks = [];
|
|
107
|
+
let globals = new Uint8Array(0);
|
|
108
|
+
while (i < data.length) {
|
|
109
|
+
const type = data[i];
|
|
110
|
+
i += 1;
|
|
111
|
+
const n1 = ipofFindNl(data, i, data.length);
|
|
112
|
+
if (n1 < 0) return null;
|
|
113
|
+
const name = ipofLatin1(data, i, n1);
|
|
114
|
+
i = n1 + 1;
|
|
115
|
+
if (i + 4 > data.length) return null;
|
|
116
|
+
const id = ipofUint(data, i, 2);
|
|
117
|
+
const flags = ipofUint(data, i + 2, 2);
|
|
118
|
+
i += 4;
|
|
119
|
+
const n2 = ipofFindNl(data, i, data.length);
|
|
120
|
+
if (n2 < 0) return null;
|
|
121
|
+
const arg1 = ipofLatin1(data, i, n2);
|
|
122
|
+
i = n2 + 1;
|
|
123
|
+
const n3 = ipofFindNl(data, i, data.length);
|
|
124
|
+
if (n3 < 0) return null;
|
|
125
|
+
const arg2 = ipofLatin1(data, i, n3);
|
|
126
|
+
i = n3 + 1;
|
|
127
|
+
if (i + 3 > data.length) return null;
|
|
128
|
+
const marker = data[i];
|
|
129
|
+
const size = ipofUint(data, i + 1, 2);
|
|
130
|
+
i += 3;
|
|
131
|
+
let plen;
|
|
132
|
+
if (IPOF_CODE_BLOCKS[type]) plen = size * 4;
|
|
133
|
+
else if (type === 0x11) plen = size;
|
|
134
|
+
else if (type === 0x04) plen = size * 12;
|
|
135
|
+
else if (type === 0x12) plen = ipofPoolLen(data, i, size, verHi);
|
|
136
|
+
else plen = size;
|
|
137
|
+
if (i + plen > data.length) return null;
|
|
138
|
+
const payload = data.subarray(i, i + plen);
|
|
139
|
+
if (type === 0x11) globals = payload;
|
|
140
|
+
blocks.push({
|
|
141
|
+
type,
|
|
142
|
+
name,
|
|
143
|
+
id,
|
|
144
|
+
flags,
|
|
145
|
+
arg1,
|
|
146
|
+
arg2,
|
|
147
|
+
marker,
|
|
148
|
+
size,
|
|
149
|
+
payload,
|
|
150
|
+
});
|
|
151
|
+
i += plen;
|
|
152
|
+
}
|
|
153
|
+
return { verHi, verLo, magic, blocks, globals };
|
|
154
|
+
} catch (err) {
|
|
155
|
+
// a file that does not tile is not a container; the token scan still runs
|
|
156
|
+
void err;
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
10
160
|
|
|
11
161
|
/**
|
|
12
162
|
* Decode one .IPO into its runnable exec object.
|
|
@@ -68,6 +218,9 @@ function ipofDecodeExec(data, stem) {
|
|
|
68
218
|
imports: meta.imports,
|
|
69
219
|
unknown,
|
|
70
220
|
bytes,
|
|
221
|
+
// the file as its own header describes it, so it can be written back
|
|
222
|
+
// unchanged; null when the bytes do not tile as a block container
|
|
223
|
+
container: ipofReadContainer(data),
|
|
71
224
|
};
|
|
72
225
|
}
|
|
73
226
|
|
|
@@ -154,6 +307,8 @@ function ipofStem(name) {
|
|
|
154
307
|
if (typeof module !== 'undefined' && module.exports) {
|
|
155
308
|
module.exports = {
|
|
156
309
|
ipofDecodeExec,
|
|
310
|
+
ipofReadContainer,
|
|
311
|
+
ipofPoolLen,
|
|
157
312
|
ipofInventory,
|
|
158
313
|
ipofReadBytes,
|
|
159
314
|
ipofIsCompiled,
|
|
@@ -67,20 +67,43 @@ class WebSerialBus extends SerialTransportBase {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
70
|
+
* WHICH PORT this session drives: the one seam a gateway adds.
|
|
71
|
+
*
|
|
72
|
+
* With a gateway configured (Settings `gatewayUrl`, or `?gateway=` on the
|
|
73
|
+
* URL) the cable is on another machine, and the port is a socket to it
|
|
74
|
+
* with this same Web Serial surface. Everything below this line -- the
|
|
75
|
+
* framing, the line control, the reopens, the timeouts -- runs here
|
|
76
|
+
* either way and cannot tell the two apart, which is the whole point of
|
|
77
|
+
* putting the seam at the port rather than inside the transport.
|
|
78
|
+
* @returns {Promise<SerialPort>} The port to open.
|
|
79
|
+
* @throws {Error} When neither a gateway nor Web Serial can supply one.
|
|
75
80
|
*/
|
|
76
|
-
async
|
|
81
|
+
async _acquirePort() {
|
|
82
|
+
const url = typeof gatewaySetting === 'function' ? gatewaySetting() : '';
|
|
83
|
+
if (url) {
|
|
84
|
+
const remote = new GatewayPort(url);
|
|
85
|
+
await remote.dial();
|
|
86
|
+
return remote;
|
|
87
|
+
}
|
|
77
88
|
if (!('serial' in navigator)) {
|
|
78
89
|
throw new Error(
|
|
79
90
|
'This browser has no Web Serial. Use Chrome or Edge ' +
|
|
80
91
|
'(desktop), or the macOS app.'
|
|
81
92
|
);
|
|
82
93
|
}
|
|
83
|
-
|
|
94
|
+
return navigator.serial.requestPort();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Open a port the user picks. Must be called from a user gesture -- the
|
|
99
|
+
* browser will not show the port picker otherwise. app.js wires this to the
|
|
100
|
+
* "connect cable" control. A gateway needs no gesture (there is no picker
|
|
101
|
+
* to show), but it costs nothing to arrive through the same click.
|
|
102
|
+
* @returns {Promise<string>} The port label.
|
|
103
|
+
* @throws {Error} When no port can be acquired.
|
|
104
|
+
*/
|
|
105
|
+
async connect() {
|
|
106
|
+
this.port = await this._acquirePort();
|
|
84
107
|
await this.port.open(KDCAN);
|
|
85
108
|
this.config = KDCAN;
|
|
86
109
|
this.writer = this.port.writable.getWriter();
|
|
@@ -190,7 +213,23 @@ class WebSerialBus extends SerialTransportBase {
|
|
|
190
213
|
* the caller leaves the chip as "no cable".
|
|
191
214
|
*/
|
|
192
215
|
async reconnect() {
|
|
193
|
-
if (
|
|
216
|
+
if (this.connected) return null;
|
|
217
|
+
// A gateway has no permission to remember and no picker to skip: the
|
|
218
|
+
// socket either opens or it does not, so the silent path is simply the
|
|
219
|
+
// ordinary connect. A gateway that is not running stays "no cable",
|
|
220
|
+
// exactly as an unplugged cable does.
|
|
221
|
+
const gateway =
|
|
222
|
+
typeof gatewaySetting === 'function' ? gatewaySetting() : '';
|
|
223
|
+
if (gateway) {
|
|
224
|
+
try {
|
|
225
|
+
return await this.connect();
|
|
226
|
+
} catch (e) {
|
|
227
|
+
console.info(`[serial] the gateway did not answer: ${e.message}`);
|
|
228
|
+
this.port = null;
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (!('serial' in navigator)) return null;
|
|
194
233
|
let ports;
|
|
195
234
|
try {
|
|
196
235
|
ports = await navigator.serial.getPorts();
|
|
@@ -292,8 +331,14 @@ class WebSerialBus extends SerialTransportBase {
|
|
|
292
331
|
await this._reopenStreams(cfg);
|
|
293
332
|
}
|
|
294
333
|
|
|
295
|
-
/**
|
|
334
|
+
/**
|
|
335
|
+
* @returns {string} 'gateway host:port (device)' when the cable is on
|
|
336
|
+
* another machine, else 'USB vid:pid' when the port says, else
|
|
337
|
+
* 'serial'. The chip shows this, so a remote car reads as remote.
|
|
338
|
+
*/
|
|
296
339
|
portLabel() {
|
|
340
|
+
if (this.port && typeof this.port.label === 'function')
|
|
341
|
+
return this.port.label();
|
|
297
342
|
const i = this.port && this.port.getInfo ? this.port.getInfo() : {};
|
|
298
343
|
return i.usbVendorId
|
|
299
344
|
? `USB ${i.usbVendorId.toString(16)}:${(i.usbProductId || 0).toString(16)}`
|
|
@@ -303,11 +348,15 @@ class WebSerialBus extends SerialTransportBase {
|
|
|
303
348
|
/** Release the streams, close the port and forget the wire state. */
|
|
304
349
|
async disconnect() {
|
|
305
350
|
await this._releaseStreams();
|
|
351
|
+
const port = this.port;
|
|
306
352
|
try {
|
|
307
|
-
if (
|
|
353
|
+
if (port) await port.close();
|
|
308
354
|
} catch {
|
|
309
355
|
/* closing */
|
|
310
356
|
}
|
|
357
|
+
// a gateway's socket is dropped AFTER the remote cable is closed: the
|
|
358
|
+
// close travels over that very socket
|
|
359
|
+
if (port && typeof port.hangUp === 'function') port.hangUp();
|
|
311
360
|
this.port = this.reader = this.writer = null;
|
|
312
361
|
this._resetWireState();
|
|
313
362
|
}
|
|
@@ -775,8 +775,14 @@ class IpoProgram {
|
|
|
775
775
|
if (gen !== this.gen) return true;
|
|
776
776
|
if (out.title) this.title = out.title;
|
|
777
777
|
// the address bar follows the menu, so a link copied mid-script lands
|
|
778
|
-
// where the user is, not where the module was opened
|
|
779
|
-
|
|
778
|
+
// where the user is, not where the module was opened (a headless UI,
|
|
779
|
+
// the tree's live scan, owns no page and leaves the address alone)
|
|
780
|
+
if (
|
|
781
|
+
typeof routeSetCar === 'function' &&
|
|
782
|
+
this.ecu &&
|
|
783
|
+
this.ecu.chassis &&
|
|
784
|
+
!(this.ui && this.ui.headless)
|
|
785
|
+
)
|
|
780
786
|
routeSetCar(
|
|
781
787
|
this.ecu.chassis,
|
|
782
788
|
this.ecu.sgbd,
|