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.
@@ -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
- * Open a port the user picks. Must be called from a user gesture -- the
71
- * browser will not show the port picker otherwise. app.js wires this to the
72
- * "connect cable" control.
73
- * @returns {Promise<string>} The port label.
74
- * @throws {Error} When the browser has no Web Serial.
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 connect() {
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
- this.port = await navigator.serial.requestPort();
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 (!('serial' in navigator) || this.connected) return null;
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
- /** @returns {string} 'USB vid:pid' when the port says, else 'serial'. */
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 (this.port) await this.port.close();
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
- if (typeof routeSetCar === 'function' && this.ecu && this.ecu.chassis)
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,