bmweb-cli 0.1.0

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.
Files changed (72) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +351 -0
  3. package/dist/bmweb.js +2790 -0
  4. package/package.json +53 -0
  5. package/runtime/core/bestvm/codec.js +285 -0
  6. package/runtime/core/bestvm/environment.js +116 -0
  7. package/runtime/core/bestvm/executor.js +1483 -0
  8. package/runtime/core/bestvm/index.js +52 -0
  9. package/runtime/core/bestvm/machine.js +491 -0
  10. package/runtime/core/bestvm/operands.js +356 -0
  11. package/runtime/core/bestvm/registers.js +152 -0
  12. package/runtime/core/bestvm/write-guard.js +111 -0
  13. package/runtime/core/ipofile/compile.js +364 -0
  14. package/runtime/core/ipofile/decls.js +187 -0
  15. package/runtime/core/ipofile/emit.js +708 -0
  16. package/runtime/core/ipofile/exec.js +164 -0
  17. package/runtime/core/ipofile/lex.js +243 -0
  18. package/runtime/core/ipofile/parse.js +550 -0
  19. package/runtime/core/ipofile/pool.js +404 -0
  20. package/runtime/core/ipofile/walk.js +431 -0
  21. package/runtime/core/ipovm/builtin-helpers.js +182 -0
  22. package/runtime/core/ipovm/builtins-api.js +610 -0
  23. package/runtime/core/ipovm/builtins-screen.js +493 -0
  24. package/runtime/core/ipovm/builtins-table.js +166 -0
  25. package/runtime/core/ipovm/builtins-text.js +166 -0
  26. package/runtime/core/ipovm/emissions.js +138 -0
  27. package/runtime/core/ipovm/hosts.js +191 -0
  28. package/runtime/core/ipovm/operators.js +229 -0
  29. package/runtime/core/ipovm/structures.js +250 -0
  30. package/runtime/core/ipovm/suspensions.js +241 -0
  31. package/runtime/core/ipovm/tape.js +206 -0
  32. package/runtime/core/ipovm/values.js +241 -0
  33. package/runtime/core/ipovm/vm.js +1166 -0
  34. package/runtime/core/translate.js +526 -0
  35. package/runtime/core/webshim/api-router.js +592 -0
  36. package/runtime/core/webshim/bus.js +95 -0
  37. package/runtime/core/webshim/coding.js +82 -0
  38. package/runtime/core/webshim/data-fetch.js +66 -0
  39. package/runtime/core/webshim/exchange.js +288 -0
  40. package/runtime/core/webshim/framing.js +331 -0
  41. package/runtime/core/webshim/install.js +30 -0
  42. package/runtime/core/webshim/job-runner.js +319 -0
  43. package/runtime/core/webshim/native-bus.js +108 -0
  44. package/runtime/core/webshim/timers.js +82 -0
  45. package/runtime/core/webshim/trace.js +205 -0
  46. package/runtime/core/webshim/transport-base.js +128 -0
  47. package/runtime/core/webshim/variant-resolver.js +249 -0
  48. package/runtime/core/webshim/web-serial-bus.js +734 -0
  49. package/runtime/home/bmweb-home.ips +76 -0
  50. package/runtime/home/bmweb.h +26 -0
  51. package/runtime/screens/activations.js +258 -0
  52. package/runtime/screens/garage/diff.js +331 -0
  53. package/runtime/screens/garage/share.js +276 -0
  54. package/runtime/screens/garage/store.js +547 -0
  55. package/runtime/screens/ipo-runtime/cells.js +176 -0
  56. package/runtime/screens/ipo-runtime/dialogs.js +254 -0
  57. package/runtime/screens/ipo-runtime/home.js +358 -0
  58. package/runtime/screens/ipo-runtime/open.js +393 -0
  59. package/runtime/screens/ipo-runtime/paint-grid.js +106 -0
  60. package/runtime/screens/ipo-runtime/paint-modern.js +424 -0
  61. package/runtime/screens/ipo-runtime/print.js +281 -0
  62. package/runtime/screens/ipo-runtime/program.js +1337 -0
  63. package/runtime/screens/ipo-runtime/protocol.js +464 -0
  64. package/runtime/screens/ipo-runtime/script-scan.js +225 -0
  65. package/runtime/screens/ipo-runtime/translate-sets.js +130 -0
  66. package/runtime/screens/ipo-runtime/ui.js +249 -0
  67. package/runtime/screens/ipo-runtime/wire-policy.js +113 -0
  68. package/runtime/screens/ir.js +324 -0
  69. package/runtime/screens/search/data.js +153 -0
  70. package/runtime/screens/search/match.js +285 -0
  71. package/runtime/screens/search/open.js +66 -0
  72. package/runtime/vendor/fflate.min.js +1 -0
package/README.md ADDED
@@ -0,0 +1,351 @@
1
+ # bmweb-cli
2
+
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.
9
+
10
+ The commands run the app's own code: the `.IPO` reader and the source
11
+ compiler, the job search, the Garage report codec, the transport (framing,
12
+ the K-line exchange, the Web Serial bus), the BEST2 job VM, the `.IPO` VM
13
+ and the program that runs a module's script are the same files the site
14
+ loads, copied into the package at build time. There is one implementation,
15
+ and this is a second way to reach it.
16
+
17
+ ```
18
+ npm i -g bmweb-cli
19
+ bmweb --help
20
+ ```
21
+
22
+ Node 20 or later. One optional dependency, `serialport`, used only by the
23
+ commands that talk to the car; if it did not install, `npm i -g serialport`.
24
+
25
+ ## What is in the package
26
+
27
+ Only the project's own code. Nothing BMW-derived ships with it: no scripts,
28
+ no SGBDs, no fault database, no parts data. Everything the tool needs that
29
+ it does not carry is fetched from the site (or `--api <url>`) and kept
30
+ 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.
34
+ `--refresh` fetches again; when the site cannot be reached the cached copy
35
+ is used and a warning says so.
36
+
37
+ ## Offline commands
38
+
39
+ ### `bmweb ipo info <file>`
40
+
41
+ What a script is: form, entry point, includes, procedure counts, DLL
42
+ imports, and every menu with its keys, the screen each opens and the jobs it
43
+ sends. Takes a compiled `.IPO`, or an `.IPS` / `.SRC` source (compiled
44
+ first; pass `-I <dir>` with the INPA headers it includes).
45
+
46
+ ```
47
+ $ bmweb ipo info PROBE.IPS -I inc
48
+ Script PROBE (compiled from source)
49
+ Form INPA diagnostic script
50
+ Entry inpainit -> m_main / s_main
51
+ Includes PROBE.H
52
+ Procedures 8 (2 menus, 3 screens, 3 functions, 0 state machines)
53
+ DLL imports none
54
+
55
+ m_main "Main" (screen s_main)
56
+ F1 Ident s_ident IDENT
57
+ F2 Fault memory s_fs (cyclic) FS_LESEN, FS_LESEN_DETAIL
58
+ F3 Clear faults FS_LOESCHEN [WRITE]
59
+ F4 Helper IDENT
60
+ F5 (no caption) menu m_sub
61
+ F10 Back DIAGNOSE_ENDE, (exit)
62
+ Shift+F10 Print (printscreen)
63
+
64
+ m_sub
65
+ F1 Sub key STATUS_LESEN
66
+ ```
67
+
68
+ The jobs are read from the script without running it: the constants each
69
+ key's body pushes before its `INPAapiJob` calls, plus what the screen it
70
+ opens sends from its own `LINE` blocks, following helper functions the key
71
+ calls. `[WRITE]` marks a job the app would ask about before sending (the
72
+ write classifier's verdict, minus the session plumbing every script sends).
73
+ A job whose name the script builds at run time is not shown.
74
+
75
+ The package carries the app's own home script (`runtime/home/bmweb-home.ips`
76
+ and its `bmweb.h`, both written by the project), which reads like any other:
77
+
78
+ ```
79
+ $ bmweb ipo info "$(npm root -g)/bmweb-cli/runtime/home/bmweb-home.ips" \
80
+ -I "$(npm root -g)/bmweb-cli/runtime/home"
81
+ ```
82
+
83
+ ### `bmweb ipo keys <file> [--menu m_x]`
84
+
85
+ The same keys as one table across every menu, or one menu.
86
+
87
+ ```
88
+ $ bmweb ipo keys PROBE.IPS -I inc --menu m_main
89
+ MENU KEY LABEL OPENS JOBS WRITES
90
+ ------ --------- ------------ ------------- ------------------------- ------
91
+ m_main F1 Ident s_ident IDENT
92
+ m_main F2 Fault memory s_fs (cyclic) FS_LESEN, FS_LESEN_DETAIL
93
+ m_main F3 Clear faults FS_LOESCHEN [WRITE] yes
94
+ m_main F4 Helper IDENT
95
+ m_main F5 (no caption) menu m_sub
96
+ m_main F10 Back DIAGNOSE_ENDE, (exit)
97
+ m_main Shift+F10 Print (printscreen)
98
+ ```
99
+
100
+ ### `bmweb ipo compile <file.IPS> [-I dir]... [-o out]`
101
+
102
+ Compile an INPA source. Includes are looked up beside the script and in
103
+ each `-I` directory, by file name, case-insensitively, the way INPA's own
104
+ tooling finds them; a missing one is named rather than half-compiled.
105
+
106
+ ```
107
+ $ bmweb ipo compile MY_SCRIPT.IPS -I ~/INPA/SGDAT
108
+ 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)
110
+
111
+ $ bmweb ipo compile MY_SCRIPT.IPS
112
+ bmweb: MY_SCRIPT.IPS: missing include INPA.H (searched /home/me; pass -I <dir> with the INPA headers)
113
+ ```
114
+
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.
121
+
122
+ ### `bmweb search <query...> [--chassis E46] [--limit N] [--json]`
123
+
124
+ The corpus job search: every INPA key and screen in every module the site
125
+ ships, matched on what it is called (in German and in English) and the
126
+ jobs it sends. All words must match. Results are grouped by chassis, then
127
+ module, and every hit carries the deep link that opens it on the site.
128
+
129
+ ```
130
+ $ bmweb search clear adaptation --chassis E46 --limit 4
131
+ E46
132
+ ms410ds0 MS 41.0 (ms410ds0)
133
+ F7 clear adaptation values [WRITE] https://bmweb.danner.ink/#car/E46/ms410ds0/m_fehler
134
+ ms410ds1 MS 41.0 (ms410ds1)
135
+ F7 clear adaptation values [WRITE] https://bmweb.danner.ink/#car/E46/ms410ds1/m_fehler
136
+ ms410ds2 MS 41.0 (ms410ds2)
137
+ F7 clear adaptation values [WRITE] https://bmweb.danner.ink/#car/E46/ms410ds2/m_fehler
138
+ ms450ds0 MS45 (ms450ds0)
139
+ F8 Clear selected adaptation values https://bmweb.danner.ink/#car/E46/ms450ds0/m_main/s_ada_loe
140
+
141
+ 4 of 5 results shown (raise --limit for more)
142
+ ```
143
+
144
+ ### `bmweb report show <link-or-payload> [--json]`
145
+
146
+ Decode a link the Garage's Share button made (`...#report/<payload>`; the
147
+ bare payload is accepted too) and print the report: one row per fault with
148
+ its module, code, text, occurrence count and whether it was present at the
149
+ read, plus the addresses that stayed silent. An identification report
150
+ prints each module's ident fields with the app's captions.
151
+
152
+ ```
153
+ $ bmweb report show 'https://bmweb.danner.ink/#report/q1WpV...'
154
+ Report fault memories of E46 325i / E46
155
+ Read 2026-09-01T10:00:00Z
156
+ Modules 2 read, 1 with faults, 2 faults, 1 silent
157
+
158
+ MODULE CODE TEXT COUNT STATE
159
+ -------- ---- ---------------------------- ----- -------
160
+ DME MS45 27C3 DMTL pump current too high 3 present
161
+ DME MS45 120 Lambda sensor heater, bank 1 1 stored
162
+ IHKA no faults stored
163
+
164
+ Silent (1):
165
+ D_0044 EGS no answer
166
+ ```
167
+
168
+ The code is the DTC from `F_HEX_CODE` when the module reports one, else the
169
+ DTC the fault text leads with, else the location number, which is how the
170
+ Garage itself identifies a fault when comparing reads. The link carries no
171
+ VIN, and nothing is fetched: the report is in the link.
172
+
173
+ ### `bmweb report diff <link-a> <link-b> [--json]`
174
+
175
+ What changed between two shared reports, older first, using the app's own
176
+ comparison: new, cleared and still-present faults per module, ident fields
177
+ that moved, and modules that went silent or started answering. A fault
178
+ present in both reads is the same fault whatever its status byte or counter
179
+ did; a module the newer read has no record of is unread, not cleared.
180
+
181
+ ```
182
+ $ bmweb report diff "$A" "$B"
183
+ From 2026-09-01T10:00:00Z E46 325i
184
+ To 2026-09-08T12:00:00Z E46 325i
185
+ Changes 1 new, 1 cleared, 1 still present, 1 module changed
186
+
187
+ DME MS45
188
+ + 2A0B Camshaft sensor, inlet 1
189
+ - 120 Lambda sensor heater, bank 1 1
190
+ = 27C3 DMTL pump current too high 5
191
+
192
+ IHKA unchanged
193
+
194
+ Answering changed:
195
+ answering now EGS
196
+
197
+ + new - cleared = still present ~ ident field changed
198
+ ```
199
+
200
+ ## Commands on the cable
201
+
202
+ These need a K+DCAN cable (an FTDI cable, or a clone) on the car's OBD
203
+ port, and the `serialport` package. The port is the single candidate when
204
+ there is one; otherwise name it with `--port`.
205
+
206
+ The wire is the app's own transport, run unchanged over a Node port with
207
+ the Web Serial API's shape. What that carries over, verified by the app on
208
+ a real E46 with an FTDI cable on a Mac: the port opens 115200 8N1 and is
209
+ reopened 9600 8E1 for a DS2 or KWP2000 module on its own; DTR idles high
210
+ for BMW-FAST and D-CAN and low on every K-line concept, RTS is never
211
+ raised, and on the K line DTR is held for exactly the telegram's byte time
212
+ as the transmit enable; the ISO 9141 slow init is bit-banged on the break
213
+ line; the echo a wired K line returns is dropped by count; every timeout is
214
+ time-to-first-byte and comes from the SGBD's own communication parameters;
215
+ a read that ran out of time is resumed, never abandoned, so no byte is
216
+ lost; a silent address is the ECU's answer of zero bytes, which the SGBD's
217
+ own bytecode branches on. An FTDI cable wants a 1 ms latency timer (set for
218
+ you on Linux; on macOS and Windows a driver setting), and an echo failure
219
+ (IFH-0003) says so.
220
+
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.
226
+
227
+ ### `bmweb ports [--json]`
228
+
229
+ The serial ports a K+DCAN cable shows up as: `cu.usbserial*`, `cu.SLAB*`,
230
+ `cu.wchusbserial*`, `ttyUSB*`, `ttyACM*`, with the vendor detail when
231
+ `serialport` is installed. Needs no package to list.
232
+
233
+ ```
234
+ $ bmweb ports
235
+ /dev/cu.usbserial-AB0JQ9XY FTDI 0403:6001 sn AB0JQ9XY
236
+ ```
237
+
238
+ ### `bmweb job <sgbd> <JOB> [arg] [--port p] [--yes] [--json]`
239
+
240
+ One raw job on one module, like the app's Tool32: the SGBD's own bytecode
241
+ runs in the job VM over the cable, inside its EDIABAS session
242
+ (INITIALISIERUNG once, the communication parameters carried across jobs,
243
+ ENDE when another module is loaded), and the result sets print as tables.
244
+ A group name (`D_MOTOR`, `D_0012`) is resolved on the wire to the variant
245
+ that answers, as the app resolves it.
246
+
247
+ ```
248
+ $ bmweb job ms450ds0 STATUS_LESEN
249
+ ms450ds0 MS450DS0 STATUS_LESEN: 1 set
250
+
251
+ set 1
252
+ STAT_MOTORDREHZAHL_WERT 812.5
253
+ STAT_MOTORDREHZAHL_EINH 1/min
254
+ JOB_STATUS OKAY
255
+
256
+ $ bmweb job ms450ds0 FS_LOESCHEN
257
+ FS_LOESCHEN on ms450ds0 is a write (it changes the module or drives something). Send it? [y/N] n
258
+ FS_LOESCHEN on ms450ds0: not sent (a write needs --yes or a y answer)
259
+ ```
260
+
261
+ The write gate is the app's classifier (`isWriteJob`): a read token in the
262
+ name wins, a write token makes a write, and an unknown name is a write.
263
+ A write goes out only with `--yes` or a `y` on the terminal; without a
264
+ terminal the answer is no. A write is never sent silently.
265
+
266
+ ### `bmweb scan <chassis> [--port p] [--share] [--json]`
267
+
268
+ INPA's own whole-vehicle script (E46 E53 E65 E83 E85 E87 E89 E90 R50 R56):
269
+ the script is opened, its fault-memory menu's read key is pressed, and what
270
+ it put on the wire is folded into the same report the app's Garage keeps,
271
+ one module per address that answered, the silent ones listed. `--share`
272
+ prints a link carrying the report, the same link the Garage's Share button
273
+ makes, which `bmweb report show` and the site both open.
274
+
275
+ ```
276
+ $ bmweb scan E46 --share
277
+ E46: FS lesen (F1)
278
+ Engine
279
+ ...
280
+ Scan E46 fault memories (e46.ipo)
281
+ Read 2026-09-09T08:26:15.221Z
282
+ Modules 14 answered, 2 with faults, 3 faults, 5 silent
283
+
284
+ MODULE CODE TEXT COUNT STATE
285
+ -------- ---- -------------------------- ----- -------
286
+ ms450ds0 27C3 DMTL pump current too high 3 present
287
+ ...
288
+ Silent (5):
289
+ D_0044 D_0044 ERROR_NO_ANSWER
290
+ ...
291
+
292
+ Share: https://bmweb.danner.ink/#report/...
293
+ ```
294
+
295
+ The script's progress window (which module it is asking) goes to stderr;
296
+ a key that would write is declined, a prompt is cancelled: the scan reads.
297
+
298
+ ### `bmweb tui [<chassis> <sgbd>] [--port p] [--menu m_x]`
299
+
300
+ INPA's screens in the terminal. With a chassis and a module (SGBD or INPA
301
+ code, or the chassis itself for its whole-vehicle script), that module's
302
+ script runs the way the app runs it: its entry identifies the module, its
303
+ root menu's keys are on the number row (1..9 and 0 for F1..F10, the
304
+ shifted symbols `! @ # $ % ^ & * ( )` and Shift+F1..F10 for the shifted
305
+ bank), Esc is the script's own Back, q quits. The screen is INPA's grid
306
+ redrawn in place, a lamp as `(*) word`, a bar as `[####....] value`; the
307
+ status line and the script's progress window are the two bottom lines.
308
+
309
+ With no arguments it starts on the app's own home, an INPA script of the
310
+ project's own (`home/bmweb-home.ips`): F1 picks a chassis then a module,
311
+ F2 the chassis's whole-vehicle script, and `scriptchange` hands the screen
312
+ to that script. The picks are numbered lists on the terminal; typing text
313
+ filters, a number opens, Enter cancels. The home starts with or without a
314
+ cable.
315
+
316
+ Every dialog INPA opens is a prompt: a message waits for Enter, an input
317
+ asks for the number (or hex, or text) within the declared range, the
318
+ two-word box takes y/n, the component picker (togglelist) lists the
319
+ screen's lines by number, Select lists the named lines, save-as asks for a
320
+ file name. **Every write is asked first**, exactly as the app asks: a key
321
+ whose body can send a write names the jobs and waits for y; a screen that
322
+ sends one on every refresh asks once for as long as it is open; n or
323
+ Enter abandons the key. On quit the leaving menu's Back job (the script's
324
+ own release of whatever it energised) goes to the module, then the
325
+ script's `inpaexit` (its DIAGNOSE_ENDE), the same release-on-leave the app
326
+ performs.
327
+
328
+ The module data the script needs (its `.IPO`, the SGBD bytecode, the
329
+ tables) comes from the site's chassis archive, cached as described above.
330
+
331
+ ### Every command
332
+
333
+ `--json` prints machine-readable output instead of a table. Errors are one
334
+ line on stderr and exit code 1. `bmweb <command> --help` lists a command's
335
+ options; `bmweb --version` prints the version.
336
+
337
+ ## Developing
338
+
339
+ The package lives in `cli/` of the [BMWeb repository](https://github.com/dader34/BMWeb).
340
+ `npm run build` copies the app files it runs into `runtime/` (the list is
341
+ `src/runtime-files.json`, in the app's load order), bundles `src/bmweb.ts`
342
+ to `dist/bmweb.js` with esbuild and type-checks with `tsc`; `npm test` runs
343
+ the `node:test` suites the build produced, every one of them offline: the
344
+ serial tests drive the app's bus over a fake cable, the job, scan and tui
345
+ tests drive the app's runtime against a fake car and a scripted terminal,
346
+ with a module script written for the tests in INPA's language. The
347
+ repository's `tools/check.sh` runs all of it.
348
+
349
+ ## License
350
+
351
+ GPL-3.0, as the repository is. See `LICENSE`.