bmweb-cli 0.1.2 → 0.1.4

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 (2) hide show
  1. package/dist/bmweb.js +75 -19
  2. package/package.json +1 -1
package/dist/bmweb.js CHANGED
@@ -814,6 +814,7 @@ import { createInterface } from "node:readline";
814
814
 
815
815
  // src/serial.ts
816
816
  import { readdirSync as readdirSync2 } from "node:fs";
817
+ var RX_KICK_MS = 4;
817
818
  var PORT_PATTERNS = [
818
819
  /^cu\.usbserial/i,
819
820
  /^cu\.SLAB/i,
@@ -827,8 +828,12 @@ var NodeSerialPort = class {
827
828
  binding = null;
828
829
  /** chunks heard and not yet read */
829
830
  chunks = [];
830
- /** the one read waiting for bytes, when the queue is empty */
831
- waiter = null;
831
+ /** reads waiting for bytes, oldest first, when the queue is empty */
832
+ waiters = [];
833
+ /** the receive kick, running while a read waits (see startKick) */
834
+ kick = null;
835
+ /** a kick ioctl in flight, so they never pile up */
836
+ kicking = false;
832
837
  /** the lines as last set, so a partial setSignals keeps the others */
833
838
  lines = { dtr: false, rts: false, brk: false };
834
839
  /** the wire trace sink, when the CLI wants one */
@@ -864,23 +869,25 @@ var NodeSerialPort = class {
864
869
  async close() {
865
870
  const b = this.binding;
866
871
  this.binding = null;
867
- if (this.waiter) {
868
- const w = this.waiter;
869
- this.waiter = null;
870
- w({ value: void 0, done: true });
871
- }
872
+ this.stopKick();
873
+ this.wakeAll();
872
874
  this.chunks = [];
873
875
  if (b) await b.close();
874
876
  }
877
+ /** Tell every waiting read the port is done, and forget them. */
878
+ wakeAll() {
879
+ const ws = this.waiters;
880
+ this.waiters = [];
881
+ for (const w of ws) w({ value: void 0, done: true });
882
+ }
875
883
  /**
876
884
  * Bytes the binding heard: to the waiting read, else queued.
877
885
  * @param chunk - the bytes
878
886
  */
879
887
  push(chunk) {
880
888
  if (!chunk.length) return;
881
- if (this.waiter) {
882
- const w = this.waiter;
883
- this.waiter = null;
889
+ const w = this.waiters.shift();
890
+ if (w) {
884
891
  w({ value: chunk, done: false });
885
892
  return;
886
893
  }
@@ -916,19 +923,54 @@ var NodeSerialPort = class {
916
923
  if (next) return Promise.resolve({ value: next, done: false });
917
924
  if (!this.binding) return Promise.resolve({ value: void 0, done: true });
918
925
  return new Promise((resolve2) => {
919
- this.waiter = resolve2;
926
+ this.waiters.push(resolve2);
927
+ this.startKick();
920
928
  });
921
929
  }
930
+ /**
931
+ * Make the driver deliver what it has heard.
932
+ *
933
+ * THE BUG THIS FIXES. On macOS the built-in FTDI driver does not wake the
934
+ * reader when bytes arrive: with a read armed and the process simply
935
+ * waiting, an ECU's answer sat in the driver until some OTHER call touched
936
+ * the device (the next write, a modem-line change, close), and only then
937
+ * came out -- measured on a real car as 0 bytes for the first exchange
938
+ * after open and every later exchange delivering the PREVIOUS one's bytes
939
+ * at its start. Polling the modem lines (a TIOCMGET, no wire traffic)
940
+ * every few milliseconds while a read waits makes each answer arrive
941
+ * within the poll interval, first exchange included. The interval never
942
+ * holds the process open and stops itself once no read is waiting.
943
+ */
944
+ startKick() {
945
+ if (this.kick) return;
946
+ const tick = () => {
947
+ const b = this.binding;
948
+ if (!b || !this.waiters.length) {
949
+ this.stopKick();
950
+ return;
951
+ }
952
+ if (this.kicking) return;
953
+ this.kicking = true;
954
+ b.get().catch(() => null).then(() => {
955
+ this.kicking = false;
956
+ });
957
+ };
958
+ this.kick = setInterval(tick, RX_KICK_MS);
959
+ if (typeof this.kick === "object" && "unref" in this.kick)
960
+ this.kick.unref();
961
+ }
962
+ /** Stop the receive kick. */
963
+ stopKick() {
964
+ if (!this.kick) return;
965
+ clearInterval(this.kick);
966
+ this.kick = null;
967
+ }
922
968
  /**
923
969
  * Cancel the reader: a waiting read is told `done`, buffered bytes go.
924
970
  */
925
971
  cancel() {
926
972
  this.chunks = [];
927
- if (this.waiter) {
928
- const w = this.waiter;
929
- this.waiter = null;
930
- w({ value: void 0, done: true });
931
- }
973
+ this.wakeAll();
932
974
  }
933
975
  /**
934
976
  * Write bytes. Resolves when the OS has them, not when they have left the
@@ -1867,6 +1909,7 @@ function terminalHomeHost(api, status) {
1867
1909
  var SHIFTED_DIGITS = "!@#$%^&*()";
1868
1910
  var DIGITS = "1234567890";
1869
1911
  var GAUGE_WIDTH = 12;
1912
+ var QUIT_MS = 4e3;
1870
1913
  function keyToPress(k) {
1871
1914
  if (k.ctrl && k.name === "c") return "quit";
1872
1915
  if (k.name === "escape") return "back";
@@ -1941,6 +1984,7 @@ function nodeTerminal() {
1941
1984
  });
1942
1985
  } finally {
1943
1986
  rl.close();
1987
+ input.resume();
1944
1988
  raw(true);
1945
1989
  paused = false;
1946
1990
  }
@@ -2576,16 +2620,27 @@ async function tuiCommand(chassis, sgbd, opts = {}) {
2576
2620
  if (opts.menu !== p.menu) await p.openMenu(opts.menu);
2577
2621
  }
2578
2622
  const program = p;
2623
+ let leaving = false;
2579
2624
  const unsubscribe = term.onKey((k) => {
2580
2625
  if (ui.modal) return;
2581
2626
  const what = keyToPress(k);
2582
2627
  if (what === null) return;
2583
2628
  if (what === "quit") {
2584
- unsubscribe();
2585
- program.leaveModule().catch(() => {
2629
+ if (leaving) {
2630
+ if (k.ctrl && !opts.term) {
2631
+ term.close();
2632
+ process.exit(130);
2633
+ }
2634
+ return;
2635
+ }
2636
+ leaving = true;
2637
+ const done = program.leaveModule().catch(() => {
2586
2638
  });
2639
+ const late = new Promise((r2) => setTimeout(r2, QUIT_MS));
2640
+ Promise.race([done, late]).then(() => ui.left());
2587
2641
  return;
2588
2642
  }
2643
+ if (leaving) return;
2589
2644
  if (what === "back") {
2590
2645
  ui.requestStop();
2591
2646
  program.back().catch(() => {
@@ -2607,7 +2662,7 @@ async function tuiCommand(chassis, sgbd, opts = {}) {
2607
2662
  }
2608
2663
 
2609
2664
  // src/bmweb.ts
2610
- var VERSION = true ? "0.1.2" : "0.0.0-dev";
2665
+ var VERSION = true ? "0.1.4" : "0.0.0-dev";
2611
2666
  var INCLUDE = {
2612
2667
  include: {
2613
2668
  kind: "list",
@@ -2940,6 +2995,7 @@ async function main(argv, out = (l) => process.stdout.write(l + "\n"), err = (l)
2940
2995
  if (typeof process !== "undefined" && process.argv[1] && /bmweb(\.js)?$/.test(process.argv[1])) {
2941
2996
  main(process.argv.slice(2)).then((code) => {
2942
2997
  process.exitCode = code;
2998
+ process.stdout.write("", () => process.exit(code));
2943
2999
  });
2944
3000
  }
2945
3001
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bmweb-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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",