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 CHANGED
@@ -1,11 +1,12 @@
1
1
  # bmweb-cli
2
2
 
3
3
  [BMWeb](https://bmweb.danner.ink/)'s tools as a command line. Read what an
4
- INPA `.IPO` script does, compile an `.IPS` / `.SRC` source, search every
5
- module the site ships for the key that does a thing, decode or compare the
6
- report links the app's Garage shares, and, with a K+DCAN cable, run jobs,
7
- read every fault memory of a car, and drive a module's INPA screens from
8
- the terminal.
4
+ INPA `.IPO` script does, compile an `.IPS` / `.SRC` source into a real
5
+ `.IPO`, search every module the site ships for the key that does a thing,
6
+ decode or compare the report links the app's Garage shares, list what a
7
+ module's SGBD declares (its jobs and their arguments, results and lookup
8
+ tables), and, with a K+DCAN cable, run jobs, read every fault memory of a
9
+ car, and drive a module's INPA screens from the terminal.
9
10
 
10
11
  The commands run the app's own code: the `.IPO` reader and the source
11
12
  compiler, the job search, the Garage report codec, the transport (framing,
@@ -28,9 +29,10 @@ Only the project's own code. Nothing BMW-derived ships with it: no scripts,
28
29
  no SGBDs, no fault database, no parts data. Everything the tool needs that
29
30
  it does not carry is fetched from the site (or `--api <url>`) and kept
30
31
  under `$XDG_CACHE_HOME/bmweb-cli/` (default `~/.cache/bmweb-cli/`) for a
31
- day: the job search index (about 2.5 MB), and, for the commands on the
32
- cable, the chassis archives the app itself loads (`api/chassis/E46.chassis`,
33
- about 20 MB for an E46), the group bytecode and the shared tables.
32
+ day: the job search index (about 2.5 MB), and, for the `sgbd` commands,
33
+ `job --info` and the commands on the cable, the chassis archives the app
34
+ itself loads (`api/chassis/E46.chassis`, about 20 MB for an E46), the
35
+ group bytecode and the shared tables.
34
36
  `--refresh` fetches again; when the site cannot be reached the cached copy
35
37
  is used and a warning says so.
36
38
 
@@ -97,7 +99,7 @@ m_main F10 Back DIAGNOSE_ENDE, (exit)
97
99
  m_main Shift+F10 Print (printscreen)
98
100
  ```
99
101
 
100
- ### `bmweb ipo compile <file.IPS> [-I dir]... [-o out]`
102
+ ### `bmweb ipo compile <file.IPS> [-I dir]... [-o out] [--exec]`
101
103
 
102
104
  Compile an INPA source. Includes are looked up beside the script and in
103
105
  each `-I` directory, by file name, case-insensitively, the way INPA's own
@@ -106,18 +108,22 @@ tooling finds them; a missing one is named rather than half-compiled.
106
108
  ```
107
109
  $ bmweb ipo compile MY_SCRIPT.IPS -I ~/INPA/SGDAT
108
110
  MY_SCRIPT.IPS: compiled 8 procedures (2 menus, 3 screens, 3 functions, 0 state machines), includes INPA.H
109
- wrote /home/me/MY_SCRIPT.ipoexec.json (the app's exec form; not INPA's binary .IPO)
111
+ wrote /home/me/MY_SCRIPT.IPO (44902 bytes, INPA's .IPO container)
110
112
 
111
113
  $ bmweb ipo compile MY_SCRIPT.IPS
112
114
  bmweb: MY_SCRIPT.IPS: missing include INPA.H (searched /home/me; pass -I <dir> with the INPA headers)
113
115
  ```
114
116
 
115
- What it writes is the form the app runs: the script's procedures as the
116
- token stream the runtime executes (`{procs, byid}`, the same shape the
117
- Script runner builds from a dropped file). The app's compiler emits that,
118
- not INPA's binary container, so the output is not a `.IPO` you could hand
119
- to INPA itself. The `.IPO` byte writer lives in the repository's Python
120
- tooling and is not part of this package.
117
+ What it writes is a real `.IPO`: INPA's own binary container, the same
118
+ block layout the shipped scripts use. The writer is the app's own, and it
119
+ is held to reproduction -- every script in a full INPA install decodes and
120
+ re-encodes to its own bytes exactly, so the bytes it writes for a source
121
+ are the bytes that format calls for.
122
+
123
+ `--exec` writes the app's exec form instead (`{procs, byid}` as JSON, the
124
+ token stream the runtime executes and the same shape the Script runner
125
+ builds from a dropped file), which is what this command wrote before it
126
+ could write the container.
121
127
 
122
128
  ### `bmweb search <query...> [--chassis E46] [--limit N] [--json]`
123
129
 
@@ -197,6 +203,88 @@ Answering changed:
197
203
  + new - cleared = still present ~ ident field changed
198
204
  ```
199
205
 
206
+ ### `bmweb sgbd jobs <sgbd> [--api url] [--refresh] [--json]`
207
+
208
+ Every job an SGBD declares, with the arguments it takes, the results it
209
+ returns, a comment, and whether the app's write gate would ask about it
210
+ before sending it. Nothing is sent and no port is opened: this is the
211
+ module's shipped declaration, the same one the app's Tool32 lists a job
212
+ from. A diagnostic group (`d_motor`, `d_0012`) is read from its own group
213
+ file rather than resolved on the wire, so it lists the job names and the
214
+ tables the group carries.
215
+
216
+ ```
217
+ $ bmweb sgbd jobs lws5
218
+ NAME ARGS RESULTS COMMENT WRITE
219
+ ------------------------- ----------------------------------------- ---------------------------------------------------------------------- ----------------------------------- -----
220
+ ABGLEICH_LESEN JOB_STATUS, ABGL_LRW_OFFSET, ABGL_LWS_ID, ABGL_FGSTNR, _TEL_ANTWORT OKAY, wenn fehlerfrei
221
+ ABGLEICH_SCHREIBEN JOB_STATUS, _TEL_ANTWORT OKAY, wenn fehlerfrei yes
222
+ ABGLEICH_VORGEBEN ABGL_LRW_OFFSET, ABGL_LWS_ID, ABGL_FGSTNR JOB_STATUS, _TEL_AN_SG, _TEL_ANTWORT OKAY, wenn fehlerfrei yes
223
+ CODIERUNG_LESEN BLOCK JOB_STATUS, COD_DATEN, _TEL_AN_SG, _TEL_ANTWORT OKAY, wenn fehlerfrei
224
+ FS_LESEN JOB_STATUS, F_HEX_CODE, F_ORT_NR, F_ORT_TEXT, F_HFK, F_ART_ANZ, +4 more OKAY, wenn fehlerfrei
225
+ FS_LOESCHEN JOB_STATUS, _TEL_ANTWORT OKAY, wenn fehlerfrei yes
226
+ ...
227
+
228
+ 24 jobs, 9 the app would ask about before sending
229
+ ```
230
+
231
+ The WRITE column is the app's classifier (`isWriteJob`), the same verdict
232
+ `bmweb job` gates on: a read token in the name wins, a write token makes a
233
+ write, and an unknown name is a write. A cell that lists only the first few
234
+ names says how many more there are; `--json` and `bmweb job <sgbd> <JOB>
235
+ --info` carry every one.
236
+
237
+ The site's export writes a job's results as `NAME : comment` and keeps no
238
+ comment above them, so COMMENT is the `JOB_COMMENT` result where the SGBD's
239
+ author wrote one and the first declared result's comment otherwise.
240
+
241
+ ### `bmweb sgbd tables <sgbd> [--api url] [--refresh] [--json]`
242
+
243
+ The lookup tables an SGBD carries for its own bytecode -- the ones its jobs
244
+ read with `tabset` to turn a byte into a word -- with the rows and columns
245
+ of each.
246
+
247
+ ```
248
+ $ bmweb sgbd tables ms450ds0
249
+ NAME ROWS COLS COLUMNS
250
+ ------------------------ ---- ---- ---------------------------------------------------
251
+ AUTHENTISIERUNG 4 2 AUTH_NR, AUTH_TEXT
252
+ BAUDRATE 7 3 NR, BAUD, BAUD_TEXT
253
+ BETRIEBSSTUNDENSTATUS 4 2 WERT, UWTEXT
254
+ BITS 72 4 NAME, BYTE, MASK, VALUE
255
+ CBSKENNUNG 16 3 NR, CBS_K, CBS_K_TEXT
256
+ ...
257
+
258
+ 53 tables (bmweb sgbd table ms450ds0 <NAME> prints one)
259
+ ```
260
+
261
+ These are the module's own tables. The four shared table files (`t_pcod`,
262
+ `t_scod`, `t_ausb`, `t_grtb`, which an SGBD reads with `tabsetex`) belong
263
+ to no one module -- every job VM is handed all of them -- so they are not
264
+ listed here, and a module is never said to carry them.
265
+
266
+ ### `bmweb sgbd table <sgbd> <NAME> [--api url] [--refresh] [--json]`
267
+
268
+ One table's rows.
269
+
270
+ ```
271
+ $ bmweb sgbd table ms450ds0 BAUDRATE
272
+ NR BAUD BAUD_TEXT
273
+ ---- -------- --------------------
274
+ 0x01 PC9600 Baudrate 9.6 kBaud
275
+ 0x02 PC19200 Baudrate 19.2 kBaud
276
+ 0x03 PC38400 Baudrate 38.4 kBaud
277
+ 0x04 PC57600 Baudrate 57.6 kBaud
278
+ 0x05 PC115200 Baudrate 115.2 kBaud
279
+ 0x06 SB Specific Baudrate
280
+ 0xXY -- unbekannte Baudrate
281
+
282
+ 7 rows in BAUDRATE
283
+ ```
284
+
285
+ The name is matched without regard to case, and a near miss is named:
286
+ `bmweb sgbd table lws5 BITS` answers `lws5 carries no table BITS`.
287
+
200
288
  ## Commands on the cable
201
289
 
202
290
  These need a K+DCAN cable (an FTDI cable, or a clone) on the car's OBD
@@ -218,11 +306,18 @@ own bytecode branches on. An FTDI cable wants a 1 ms latency timer (set for
218
306
  you on Linux; on macOS and Windows a driver setting), and an echo failure
219
307
  (IFH-0003) says so.
220
308
 
221
- **Tested against the app's fake car, pending a real K+DCAN run.** The
222
- transport is exercised end to end against a fake cable (the DS2 and
223
- BMW-FAST framing, the reopen, the DTR sequence, the echo), and `job`,
224
- `scan` and `tui` against the fake car the app's own runtime tests use. No
225
- real cable has been on this code yet.
309
+ **Run on a real car.** Since 0.1.4, `job`, `scan` and `tui` have been run
310
+ on an E46 over an FTDI K+DCAN cable on macOS. What that run fixed is in
311
+ `src/serial.ts`: on macOS a Node serial port never wakes on bytes an FTDI
312
+ cable sends back, so the reader polls the port's modem lines while it is
313
+ reading and picks the bytes up itself. The offline commands need no cable
314
+ and never open one.
315
+
316
+ Everything is still tested offline as well: the transport end to end
317
+ against a fake cable (the DS2 and BMW-FAST framing, the reopen, the DTR
318
+ sequence, the echo), and `job`, `scan` and `tui` against the fake car the
319
+ app's own runtime tests use, both directly and through a gateway on a real
320
+ socket. The gateway itself has not been on a car yet.
226
321
 
227
322
  ### `bmweb ports [--json]`
228
323
 
@@ -235,7 +330,116 @@ $ bmweb ports
235
330
  /dev/cu.usbserial-AB0JQ9XY FTDI 0403:6001 sn AB0JQ9XY
236
331
  ```
237
332
 
238
- ### `bmweb job <sgbd> <JOB> [arg] [--port p] [--yes] [--json]`
333
+ ### Gateway
334
+
335
+ One machine owns the cable; another drives it. `bmweb gateway` opens the
336
+ local port exactly as `job` does and serves it over a WebSocket, and
337
+ `--gateway` on `job`, `scan` and `tui` (and the app in a browser) hands
338
+ that socket to the same transport, which cannot tell it from a cable in
339
+ its own USB port.
340
+
341
+ Nothing about the wire moves. The framing, the checksums, the reopen for a
342
+ concept change, DTR as the K-line transmit enable, the echo dropped by
343
+ count, the timeouts measured to the first byte and every write
344
+ confirmation all run on the machine you typed the command on, because
345
+ that is where the bus is. The machine with the cable moves bytes and
346
+ modem-line states, and nothing else.
347
+
348
+ **It is a pipe, not a gate.** The gateway never sees a job name, only
349
+ bytes, so it cannot tell a read from a write and does not try. Anyone who
350
+ can reach the listening port can drive the car. It binds `127.0.0.1` by
351
+ default, which is this machine only; `--listen 0.0.0.0:6801` opens it to
352
+ whoever can route to the machine, and says so when it starts.
353
+
354
+ On the machine with the cable:
355
+
356
+ ```
357
+ $ bmweb gateway --listen 0.0.0.0:6801
358
+ gateway: /dev/cu.usbserial-AB0JQ9XY served at ws://0.0.0.0:6801
359
+ this is a byte pipe with no gate of its own: anyone who can reach this port can drive the car.
360
+ listening beyond this machine; the port is open to whoever can route to it
361
+ one client at a time; the cable is closed when the client goes.
362
+ client 192.168.1.24:52233 connected
363
+ client 192.168.1.24:52233 disconnected, cable closed
364
+ ^C
365
+ gateway: stopping, the cable is closed
366
+ ```
367
+
368
+ On the machine driving it:
369
+
370
+ ```
371
+ $ bmweb job ms450ds0 STATUS_LESEN --gateway 192.168.1.9:6801
372
+ ms450ds0 MS450DS0 STATUS_LESEN: 1 set
373
+
374
+ set 1
375
+ STAT_MOTORDREHZAHL_WERT 812.5
376
+ STAT_MOTORDREHZAHL_EINH 1/min
377
+ JOB_STATUS OKAY
378
+
379
+ $ bmweb job ms450ds0 FS_LOESCHEN --gateway 192.168.1.9:6801
380
+ FS_LOESCHEN on ms450ds0 is a write (it changes the module or drives something). Send it? [y/N] n
381
+ FS_LOESCHEN on ms450ds0: not sent (a write needs --yes or a y answer)
382
+ ```
383
+
384
+ The question is asked here, on the driving machine, and a `n` means the
385
+ bytes never leave it. `--gateway` takes `host:port`, a bare host (port
386
+ 6801 is assumed), or a full `ws://` / `wss://` URL. One client at a time:
387
+ a second connection is refused with a message, and the cable is closed
388
+ when the client goes, so the host machine can use its own port in
389
+ between.
390
+
391
+ #### From a browser
392
+
393
+ The app drives a gateway too. Set the address in Settings, under "Cable on
394
+ another machine", or open the page with `?gateway=ws://192.168.1.9:6801`,
395
+ which stores it. The topbar's cable chip then reads
396
+ `cable: gateway 192.168.1.9:6801 (/dev/cu.usbserial-AB0JQ9XY)`, so the car
397
+ never looks local when it is not.
398
+
399
+ Where this works is a browser rule, not ours: **a page served over http
400
+ (localhost, an offline export opened from a file, an offline copy served
401
+ on your own network) may open a plain `ws://` gateway. The hosted https
402
+ site may not** -- browsers block `ws://` from an https page as mixed
403
+ content, with no override. From https, only a `wss://` gateway works,
404
+ which means putting a TLS front end with a certificate the browser trusts
405
+ in front of it.
406
+
407
+ #### The protocol
408
+
409
+ JSON text frames for control, binary frames for the bytes the wire
410
+ carries, so a read crosses as one frame with no base64 and no copy.
411
+
412
+ | direction | frame | meaning |
413
+ | -------------- | ------ | ------------------------------------------------------------------------------------------- |
414
+ | client to host | text | `{"id":1,"op":"open","config":{"baudRate":9600,"dataBits":8,"stopBits":1,"parity":"even"}}` |
415
+ | client to host | text | `{"id":2,"op":"close"}` |
416
+ | client to host | text | `{"id":3,"op":"setSignals","signals":{"dataTerminalReady":true}}` |
417
+ | client to host | text | `{"id":4,"op":"getSignals"}` |
418
+ | client to host | binary | the bytes to write, exactly as given |
419
+ | host to client | text | `{"id":1,"ok":true}`, or `{"id":4,"ok":true,"signals":{...}}` |
420
+ | host to client | text | `{"id":2,"ok":false,"error":"..."}` |
421
+ | host to client | text | `{"event":"hello","port":"/dev/cu.usbserial-AB0JQ9XY","gateway":"bmweb"}` |
422
+ | host to client | text | `{"event":"writeFailed","error":"..."}` |
423
+ | host to client | binary | bytes as they arrive off the wire, streamed |
424
+
425
+ A write needs no reply: the wire has no acknowledgement to give, and the
426
+ transport never waited for one, so a round trip here would only lengthen
427
+ the DTR hold and lose the answer. A write that FAILS does travel, as an
428
+ unsolicited `writeFailed`, because a client that thinks a request went out
429
+ when it never left the cable reports a phantom fault against a healthy
430
+ module. Reads are streamed as the bytes arrive rather than gathered into
431
+ an answer, so a timeout still means time-to-first-byte on the driving
432
+ machine. `setSignals` keeps the lines it does not name, and keeps them
433
+ across the reopen a concept change makes, exactly as a local port does.
434
+ Every failure travels back as the same message text a local port would
435
+ have raised, so the app throws it verbatim and cannot tell the difference.
436
+
437
+ The WebSocket is RFC 6455 with no compression and no extensions, written
438
+ in the package itself (`src/ws.ts`) rather than pulled in, because the
439
+ package promises zero runtime dependencies and Node ships a WebSocket
440
+ client only from version 22 and no server at all.
441
+
442
+ ### `bmweb job <sgbd> <JOB> [arg] [--results a,b] [--info] [--port p] [--gateway h:p] [--yes] [--json]`
239
443
 
240
444
  One raw job on one module, like the app's Tool32: the SGBD's own bytecode
241
445
  runs in the job VM over the cable, inside its EDIABAS session
@@ -263,7 +467,51 @@ name wins, a write token makes a write, and an unknown name is a write.
263
467
  A write goes out only with `--yes` or a `y` on the terminal; without a
264
468
  terminal the answer is no. A write is never sent silently.
265
469
 
266
- ### `bmweb scan <chassis> [--port p] [--share] [--json]`
470
+ `--results` names the results to print, comma-separated and without regard
471
+ to case. The job still runs whole -- the flag narrows what is shown, not
472
+ what is asked of the module -- and `--json` is filtered the same way, so
473
+ the table and the JSON never disagree about what was read. A name that
474
+ matched nothing is a warning on stderr and the exit code stays 0.
475
+
476
+ ```
477
+ $ bmweb job ms450ds0 STATUS_LESEN --results stat_motordrehzahl_wert,JOB_STATUS
478
+ ms450ds0 MS450DS0 STATUS_LESEN: 1 set
479
+
480
+ set 1
481
+ STAT_MOTORDREHZAHL_WERT 812.5
482
+ JOB_STATUS OKAY
483
+ ```
484
+
485
+ `--info` prints what the SGBD declares about the job -- its arguments, its
486
+ results and their comments -- instead of running it. It opens no port and
487
+ sends nothing, so it works with the cable unplugged and with the car
488
+ elsewhere; a job argument beside it is refused rather than quietly ignored.
489
+
490
+ ```
491
+ $ bmweb job ms450ds0 AIF_LESEN --info
492
+ ms450ds0 AIF_LESEN
493
+
494
+ arguments (1)
495
+ AIF_NUMMER int ==0 : aktuelles AIF > 0 : Nummer des zu lesenden AIF default = 0 : aktuelles AIF
496
+
497
+ results (21)
498
+ AIF_ADRESSE_HIGH AIF Adresse des AIF, High-Word
499
+ AIF_ADRESSE_LOW AIF Adresse des AIF, Low-Word
500
+ AIF_FG_NR Fahrgestellnummer 7-stellig
501
+ AIF_FG_NR_LANG Fahrgestellnummer 17-stellig falls vorhanden, sonst 7-stellig
502
+ AIF_DATUM Datum der SG-Programmierung in der Form TT.MM.JJJJ
503
+ ...
504
+ JOB_STATUS OKAY, wenn fehlerfrei
505
+ ```
506
+
507
+ A job that writes is headed `[WRITE]` here too, and `bmweb sgbd jobs`
508
+ below lists every job of a module at once.
509
+
510
+ `--gateway <host:port>` runs the same job on a cable another machine is
511
+ serving (see [Gateway](#gateway) below). The write gate does not move: it
512
+ is asked here, on the machine you typed the command on.
513
+
514
+ ### `bmweb scan <chassis> [--port p] [--gateway h:p] [--share] [--json]`
267
515
 
268
516
  INPA's own whole-vehicle script (E46 E53 E65 E83 E85 E87 E89 E90 R50 R56):
269
517
  the script is opened, its fault-memory menu's read key is pressed, and what
@@ -295,7 +543,9 @@ Share: https://bmweb.danner.ink/#report/...
295
543
  The script's progress window (which module it is asking) goes to stderr;
296
544
  a key that would write is declined, a prompt is cancelled: the scan reads.
297
545
 
298
- ### `bmweb tui [<chassis> <sgbd>] [--port p] [--menu m_x]`
546
+ `--gateway <host:port>` scans a car on a cable another machine is serving.
547
+
548
+ ### `bmweb tui [<chassis> <sgbd>] [--port p] [--gateway h:p] [--menu m_x]`
299
549
 
300
550
  INPA's screens in the terminal. With a chassis and a module (SGBD or INPA
301
551
  code, or the chassis itself for its whole-vehicle script), that module's
@@ -315,7 +565,10 @@ cancels. The home starts with or without a cable.
315
565
 
316
566
  The TUI runs on the terminal's alternate screen (the buffer vim and htop
317
567
  use), so the shell's scrollback is never touched and quitting restores it;
318
- a screen redraws in place, only the lines that changed.
568
+ a screen redraws in place, only the lines that changed. A viewer longer
569
+ than the terminal (a fault protocol, a report) scrolls: Up/Down a line,
570
+ PgUp/PgDn a page, Home/End to either end, with a line under it saying
571
+ which rows are shown.
319
572
 
320
573
  Every dialog INPA opens is a prompt: a message waits for Enter, an input
321
574
  asks for the number (or hex, or text) within the declared range, the
@@ -332,6 +585,9 @@ performs.
332
585
  The module data the script needs (its `.IPO`, the SGBD bytecode, the
333
586
  tables) comes from the site's chassis archive, cached as described above.
334
587
 
588
+ `--gateway <host:port>` drives a cable another machine is serving. Every
589
+ confirmation still happens here, on your keyboard.
590
+
335
591
  ### Every command
336
592
 
337
593
  `--json` prints machine-readable output instead of a table. Errors are one
@@ -347,8 +603,15 @@ to `dist/bmweb.js` with esbuild and type-checks with `tsc`; `npm test` runs
347
603
  the `node:test` suites the build produced, every one of them offline: the
348
604
  serial tests drive the app's bus over a fake cable, the job, scan and tui
349
605
  tests drive the app's runtime against a fake car and a scripted terminal,
350
- with a module script written for the tests in INPA's language. The
351
- repository's `tools/check.sh` runs all of it.
606
+ with a module script written for the tests in INPA's language, and the
607
+ gateway tests run `job`, `scan` and `tui` a second time through a real
608
+ gateway on a port the OS picks. The repository's `tools/check.sh` runs all
609
+ of it.
610
+
611
+ Set `BMWEB_VERBOSE=1` to see what the app's runtime logs: the wire trace
612
+ the bus dumps after an error, each variant probe's verdict, the cable
613
+ events. It goes to stderr; without it the runtime is silent and a command's
614
+ output is only its own.
352
615
 
353
616
  ## License
354
617