bmweb-cli 0.1.3 → 0.1.6
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 +9 -1
- package/dist/bmweb.js +158 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -315,7 +315,10 @@ cancels. The home starts with or without a cable.
|
|
|
315
315
|
|
|
316
316
|
The TUI runs on the terminal's alternate screen (the buffer vim and htop
|
|
317
317
|
use), so the shell's scrollback is never touched and quitting restores it;
|
|
318
|
-
a screen redraws in place, only the lines that changed.
|
|
318
|
+
a screen redraws in place, only the lines that changed. A viewer longer
|
|
319
|
+
than the terminal (a fault protocol, a report) scrolls: Up/Down a line,
|
|
320
|
+
PgUp/PgDn a page, Home/End to either end, with a line under it saying
|
|
321
|
+
which rows are shown.
|
|
319
322
|
|
|
320
323
|
Every dialog INPA opens is a prompt: a message waits for Enter, an input
|
|
321
324
|
asks for the number (or hex, or text) within the declared range, the
|
|
@@ -350,6 +353,11 @@ tests drive the app's runtime against a fake car and a scripted terminal,
|
|
|
350
353
|
with a module script written for the tests in INPA's language. The
|
|
351
354
|
repository's `tools/check.sh` runs all of it.
|
|
352
355
|
|
|
356
|
+
Set `BMWEB_VERBOSE=1` to see what the app's runtime logs: the wire trace
|
|
357
|
+
the bus dumps after an error, each variant probe's verdict, the cable
|
|
358
|
+
events. It goes to stderr; without it the runtime is silent and a command's
|
|
359
|
+
output is only its own.
|
|
360
|
+
|
|
353
361
|
## License
|
|
354
362
|
|
|
355
363
|
GPL-3.0, as the repository is. See `LICENSE`.
|
package/dist/bmweb.js
CHANGED
|
@@ -111,6 +111,7 @@ function formatCount(n) {
|
|
|
111
111
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
112
112
|
import { join as join3 } from "node:path";
|
|
113
113
|
import { fileURLToPath } from "node:url";
|
|
114
|
+
import { format, inspect } from "node:util";
|
|
114
115
|
import { createContext, runInContext } from "node:vm";
|
|
115
116
|
|
|
116
117
|
// src/runtime-files.json
|
|
@@ -284,14 +285,49 @@ function runtimeGlobals() {
|
|
|
284
285
|
loadRuntime();
|
|
285
286
|
return sandboxRef;
|
|
286
287
|
}
|
|
288
|
+
function runtimeConsole() {
|
|
289
|
+
const on = !!process.env.BMWEB_VERBOSE;
|
|
290
|
+
const say = (...a) => {
|
|
291
|
+
if (on) process.stderr.write(`${format(...a)}
|
|
292
|
+
`);
|
|
293
|
+
};
|
|
294
|
+
const table = (rows) => {
|
|
295
|
+
if (on)
|
|
296
|
+
process.stderr.write(
|
|
297
|
+
`${inspect(rows, { depth: 3, maxArrayLength: 200, breakLength: 160 })}
|
|
298
|
+
`
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
const noop = () => {
|
|
302
|
+
};
|
|
303
|
+
return {
|
|
304
|
+
log: say,
|
|
305
|
+
info: say,
|
|
306
|
+
warn: say,
|
|
307
|
+
error: say,
|
|
308
|
+
debug: say,
|
|
309
|
+
trace: say,
|
|
310
|
+
table,
|
|
311
|
+
group: say,
|
|
312
|
+
groupCollapsed: say,
|
|
313
|
+
groupEnd: noop,
|
|
314
|
+
time: noop,
|
|
315
|
+
timeEnd: noop,
|
|
316
|
+
assert: noop,
|
|
317
|
+
dir: table
|
|
318
|
+
};
|
|
319
|
+
}
|
|
287
320
|
function hostGlobals() {
|
|
288
321
|
const noop = () => {
|
|
289
322
|
};
|
|
290
323
|
const sandbox = {
|
|
291
|
-
// the scripts
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
|
|
324
|
+
// the app's scripts talk to the browser console: the bus dumps its wire
|
|
325
|
+
// trace after an IFH error (console.table in a collapsed group), the
|
|
326
|
+
// variant resolver notes every probe's verdict, the bus reports the
|
|
327
|
+
// cable. In a terminal those are noise between a command's own lines
|
|
328
|
+
// (a scan's table had the trace printed through it), so they go to
|
|
329
|
+
// stderr only when BMWEB_VERBOSE is set, and never to stdout
|
|
330
|
+
console: runtimeConsole(),
|
|
295
331
|
// timers.js's bmwSleep falls back to setTimeout where there is no
|
|
296
332
|
// Worker; program.js schedules screen cycles and drains key presses
|
|
297
333
|
// through setTimeout; activations.js defers a session end a microtask
|
|
@@ -814,6 +850,7 @@ import { createInterface } from "node:readline";
|
|
|
814
850
|
|
|
815
851
|
// src/serial.ts
|
|
816
852
|
import { readdirSync as readdirSync2 } from "node:fs";
|
|
853
|
+
var RX_KICK_MS = 4;
|
|
817
854
|
var PORT_PATTERNS = [
|
|
818
855
|
/^cu\.usbserial/i,
|
|
819
856
|
/^cu\.SLAB/i,
|
|
@@ -827,8 +864,12 @@ var NodeSerialPort = class {
|
|
|
827
864
|
binding = null;
|
|
828
865
|
/** chunks heard and not yet read */
|
|
829
866
|
chunks = [];
|
|
830
|
-
/**
|
|
831
|
-
|
|
867
|
+
/** reads waiting for bytes, oldest first, when the queue is empty */
|
|
868
|
+
waiters = [];
|
|
869
|
+
/** the receive kick, running while a read waits (see startKick) */
|
|
870
|
+
kick = null;
|
|
871
|
+
/** a kick ioctl in flight, so they never pile up */
|
|
872
|
+
kicking = false;
|
|
832
873
|
/** the lines as last set, so a partial setSignals keeps the others */
|
|
833
874
|
lines = { dtr: false, rts: false, brk: false };
|
|
834
875
|
/** the wire trace sink, when the CLI wants one */
|
|
@@ -864,23 +905,25 @@ var NodeSerialPort = class {
|
|
|
864
905
|
async close() {
|
|
865
906
|
const b = this.binding;
|
|
866
907
|
this.binding = null;
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
this.waiter = null;
|
|
870
|
-
w({ value: void 0, done: true });
|
|
871
|
-
}
|
|
908
|
+
this.stopKick();
|
|
909
|
+
this.wakeAll();
|
|
872
910
|
this.chunks = [];
|
|
873
911
|
if (b) await b.close();
|
|
874
912
|
}
|
|
913
|
+
/** Tell every waiting read the port is done, and forget them. */
|
|
914
|
+
wakeAll() {
|
|
915
|
+
const ws = this.waiters;
|
|
916
|
+
this.waiters = [];
|
|
917
|
+
for (const w of ws) w({ value: void 0, done: true });
|
|
918
|
+
}
|
|
875
919
|
/**
|
|
876
920
|
* Bytes the binding heard: to the waiting read, else queued.
|
|
877
921
|
* @param chunk - the bytes
|
|
878
922
|
*/
|
|
879
923
|
push(chunk) {
|
|
880
924
|
if (!chunk.length) return;
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
this.waiter = null;
|
|
925
|
+
const w = this.waiters.shift();
|
|
926
|
+
if (w) {
|
|
884
927
|
w({ value: chunk, done: false });
|
|
885
928
|
return;
|
|
886
929
|
}
|
|
@@ -916,19 +959,54 @@ var NodeSerialPort = class {
|
|
|
916
959
|
if (next) return Promise.resolve({ value: next, done: false });
|
|
917
960
|
if (!this.binding) return Promise.resolve({ value: void 0, done: true });
|
|
918
961
|
return new Promise((resolve2) => {
|
|
919
|
-
this.
|
|
962
|
+
this.waiters.push(resolve2);
|
|
963
|
+
this.startKick();
|
|
920
964
|
});
|
|
921
965
|
}
|
|
966
|
+
/**
|
|
967
|
+
* Make the driver deliver what it has heard.
|
|
968
|
+
*
|
|
969
|
+
* THE BUG THIS FIXES. On macOS the built-in FTDI driver does not wake the
|
|
970
|
+
* reader when bytes arrive: with a read armed and the process simply
|
|
971
|
+
* waiting, an ECU's answer sat in the driver until some OTHER call touched
|
|
972
|
+
* the device (the next write, a modem-line change, close), and only then
|
|
973
|
+
* came out -- measured on a real car as 0 bytes for the first exchange
|
|
974
|
+
* after open and every later exchange delivering the PREVIOUS one's bytes
|
|
975
|
+
* at its start. Polling the modem lines (a TIOCMGET, no wire traffic)
|
|
976
|
+
* every few milliseconds while a read waits makes each answer arrive
|
|
977
|
+
* within the poll interval, first exchange included. The interval never
|
|
978
|
+
* holds the process open and stops itself once no read is waiting.
|
|
979
|
+
*/
|
|
980
|
+
startKick() {
|
|
981
|
+
if (this.kick) return;
|
|
982
|
+
const tick = () => {
|
|
983
|
+
const b = this.binding;
|
|
984
|
+
if (!b || !this.waiters.length) {
|
|
985
|
+
this.stopKick();
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (this.kicking) return;
|
|
989
|
+
this.kicking = true;
|
|
990
|
+
b.get().catch(() => null).then(() => {
|
|
991
|
+
this.kicking = false;
|
|
992
|
+
});
|
|
993
|
+
};
|
|
994
|
+
this.kick = setInterval(tick, RX_KICK_MS);
|
|
995
|
+
if (typeof this.kick === "object" && "unref" in this.kick)
|
|
996
|
+
this.kick.unref();
|
|
997
|
+
}
|
|
998
|
+
/** Stop the receive kick. */
|
|
999
|
+
stopKick() {
|
|
1000
|
+
if (!this.kick) return;
|
|
1001
|
+
clearInterval(this.kick);
|
|
1002
|
+
this.kick = null;
|
|
1003
|
+
}
|
|
922
1004
|
/**
|
|
923
1005
|
* Cancel the reader: a waiting read is told `done`, buffered bytes go.
|
|
924
1006
|
*/
|
|
925
1007
|
cancel() {
|
|
926
1008
|
this.chunks = [];
|
|
927
|
-
|
|
928
|
-
const w = this.waiter;
|
|
929
|
-
this.waiter = null;
|
|
930
|
-
w({ value: void 0, done: true });
|
|
931
|
-
}
|
|
1009
|
+
this.wakeAll();
|
|
932
1010
|
}
|
|
933
1011
|
/**
|
|
934
1012
|
* Write bytes. Resolves when the OS has them, not when they have left the
|
|
@@ -1966,6 +2044,12 @@ var TuiUi = class {
|
|
|
1966
2044
|
frame = [];
|
|
1967
2045
|
/** a picker owns the keyboard: the program's key handler must stand back */
|
|
1968
2046
|
modal = false;
|
|
2047
|
+
/** the first line of the viewer shown (INPA's viewer scrolls; so does this) */
|
|
2048
|
+
viewTop = 0;
|
|
2049
|
+
/** the view the scroll position belongs to; a new view starts at the top */
|
|
2050
|
+
viewShown = null;
|
|
2051
|
+
/** how many viewer lines the last frame had room for */
|
|
2052
|
+
viewRoom = 0;
|
|
1969
2053
|
/** the terminal size the frame was drawn for; a change draws fresh */
|
|
1970
2054
|
frameSize = "";
|
|
1971
2055
|
leftResolve = null;
|
|
@@ -2380,6 +2464,32 @@ Save as [fault-memory.txt]: `);
|
|
|
2380
2464
|
paint(p) {
|
|
2381
2465
|
this.flush(this.frameLines(p));
|
|
2382
2466
|
}
|
|
2467
|
+
/**
|
|
2468
|
+
* Scroll the viewer by a key: a line, a page, or to an end. Nothing
|
|
2469
|
+
* happens without a viewer on screen.
|
|
2470
|
+
* @param how - 'up' | 'down' | 'pageup' | 'pagedown' | 'home' | 'end'
|
|
2471
|
+
* @returns whether the key was for the viewer
|
|
2472
|
+
*/
|
|
2473
|
+
scrollView(how) {
|
|
2474
|
+
const p = this.program;
|
|
2475
|
+
if (!p || !p.view) return false;
|
|
2476
|
+
const page = Math.max(1, this.viewRoom - 1);
|
|
2477
|
+
const n = (p.view.lines || []).length;
|
|
2478
|
+
const max = Math.max(0, n - this.viewRoom);
|
|
2479
|
+
const jump = {
|
|
2480
|
+
up: -1,
|
|
2481
|
+
down: 1,
|
|
2482
|
+
pageup: -page,
|
|
2483
|
+
pagedown: page,
|
|
2484
|
+
home: -n,
|
|
2485
|
+
end: n
|
|
2486
|
+
};
|
|
2487
|
+
const by = jump[how];
|
|
2488
|
+
if (by === void 0) return false;
|
|
2489
|
+
this.viewTop = Math.max(0, Math.min(max, this.viewTop + by));
|
|
2490
|
+
this.paint(p);
|
|
2491
|
+
return true;
|
|
2492
|
+
}
|
|
2383
2493
|
/**
|
|
2384
2494
|
* The frame as lines: title, rule, the view or the grid, a blank, the
|
|
2385
2495
|
* key bar, then the status and progress lines. Cut to the terminal's
|
|
@@ -2392,16 +2502,34 @@ Save as [fault-memory.txt]: `);
|
|
|
2392
2502
|
const w = this.term.columns;
|
|
2393
2503
|
const title = `${p.ecu.label || p.ecu.sgbd} ${p.ecu.sgbd}.prg ${p.title || ""}`.trim();
|
|
2394
2504
|
const head = [title, "-".repeat(Math.min(w, 78))];
|
|
2395
|
-
let body = p.view ? [...p.view.lines || []] : this.gridLines(p);
|
|
2396
2505
|
const tail = ["", ...this.keyLines(p), this.statusText, this.progressText];
|
|
2397
2506
|
const room = Math.max(1, this.term.rows - 1 - head.length - tail.length);
|
|
2398
|
-
|
|
2399
|
-
if (
|
|
2400
|
-
const
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2507
|
+
let body;
|
|
2508
|
+
if (p.view) {
|
|
2509
|
+
const lines = [...p.view.lines || []];
|
|
2510
|
+
if (p.view !== this.viewShown) {
|
|
2511
|
+
this.viewShown = p.view;
|
|
2512
|
+
this.viewTop = 0;
|
|
2513
|
+
}
|
|
2514
|
+
const fits = lines.length <= room;
|
|
2515
|
+
const window = fits ? room : room - 1;
|
|
2516
|
+
this.viewRoom = window;
|
|
2517
|
+
this.viewTop = Math.max(0, Math.min(this.viewTop, lines.length - window));
|
|
2518
|
+
body = lines.slice(this.viewTop, this.viewTop + window);
|
|
2519
|
+
if (!fits)
|
|
2520
|
+
body.push(
|
|
2521
|
+
`\x1B[2mrows ${this.viewTop + 1}-${this.viewTop + body.length} of ${lines.length} \u2191\u2193 PgUp PgDn Home End scroll\x1B[0m`
|
|
2522
|
+
);
|
|
2523
|
+
} else {
|
|
2524
|
+
body = this.gridLines(p);
|
|
2525
|
+
if (body.length > room) body = body.filter((l, i) => l || body[i - 1]);
|
|
2526
|
+
if (body.length > room) {
|
|
2527
|
+
const hidden = body.length - (room - 1);
|
|
2528
|
+
body = [
|
|
2529
|
+
...body.slice(0, room - 1),
|
|
2530
|
+
`(${hidden} more rows: enlarge the terminal)`
|
|
2531
|
+
];
|
|
2532
|
+
}
|
|
2405
2533
|
}
|
|
2406
2534
|
return [...head, ...body, ...tail].map(
|
|
2407
2535
|
(l) => String(l).replace(/[\r\n]/g, " ").slice(0, w)
|
|
@@ -2581,6 +2709,7 @@ async function tuiCommand(chassis, sgbd, opts = {}) {
|
|
|
2581
2709
|
let leaving = false;
|
|
2582
2710
|
const unsubscribe = term.onKey((k) => {
|
|
2583
2711
|
if (ui.modal) return;
|
|
2712
|
+
if (ui.scrollView(k.name)) return;
|
|
2584
2713
|
const what = keyToPress(k);
|
|
2585
2714
|
if (what === null) return;
|
|
2586
2715
|
if (what === "quit") {
|
|
@@ -2620,7 +2749,7 @@ async function tuiCommand(chassis, sgbd, opts = {}) {
|
|
|
2620
2749
|
}
|
|
2621
2750
|
|
|
2622
2751
|
// src/bmweb.ts
|
|
2623
|
-
var VERSION = true ? "0.1.
|
|
2752
|
+
var VERSION = true ? "0.1.6" : "0.0.0-dev";
|
|
2624
2753
|
var INCLUDE = {
|
|
2625
2754
|
include: {
|
|
2626
2755
|
kind: "list",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bmweb-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "BMWeb's tools as a command line: read INPA .IPO scripts, compile .IPS sources, search the corpus job index, decode and diff shared Garage reports, and, over a K+DCAN cable, run jobs, whole-car scans and INPA screens in the terminal.",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"type": "module",
|