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.
- package/LICENSE +674 -0
- package/README.md +351 -0
- package/dist/bmweb.js +2790 -0
- package/package.json +53 -0
- package/runtime/core/bestvm/codec.js +285 -0
- package/runtime/core/bestvm/environment.js +116 -0
- package/runtime/core/bestvm/executor.js +1483 -0
- package/runtime/core/bestvm/index.js +52 -0
- package/runtime/core/bestvm/machine.js +491 -0
- package/runtime/core/bestvm/operands.js +356 -0
- package/runtime/core/bestvm/registers.js +152 -0
- package/runtime/core/bestvm/write-guard.js +111 -0
- package/runtime/core/ipofile/compile.js +364 -0
- package/runtime/core/ipofile/decls.js +187 -0
- package/runtime/core/ipofile/emit.js +708 -0
- package/runtime/core/ipofile/exec.js +164 -0
- package/runtime/core/ipofile/lex.js +243 -0
- package/runtime/core/ipofile/parse.js +550 -0
- package/runtime/core/ipofile/pool.js +404 -0
- package/runtime/core/ipofile/walk.js +431 -0
- package/runtime/core/ipovm/builtin-helpers.js +182 -0
- package/runtime/core/ipovm/builtins-api.js +610 -0
- package/runtime/core/ipovm/builtins-screen.js +493 -0
- package/runtime/core/ipovm/builtins-table.js +166 -0
- package/runtime/core/ipovm/builtins-text.js +166 -0
- package/runtime/core/ipovm/emissions.js +138 -0
- package/runtime/core/ipovm/hosts.js +191 -0
- package/runtime/core/ipovm/operators.js +229 -0
- package/runtime/core/ipovm/structures.js +250 -0
- package/runtime/core/ipovm/suspensions.js +241 -0
- package/runtime/core/ipovm/tape.js +206 -0
- package/runtime/core/ipovm/values.js +241 -0
- package/runtime/core/ipovm/vm.js +1166 -0
- package/runtime/core/translate.js +526 -0
- package/runtime/core/webshim/api-router.js +592 -0
- package/runtime/core/webshim/bus.js +95 -0
- package/runtime/core/webshim/coding.js +82 -0
- package/runtime/core/webshim/data-fetch.js +66 -0
- package/runtime/core/webshim/exchange.js +288 -0
- package/runtime/core/webshim/framing.js +331 -0
- package/runtime/core/webshim/install.js +30 -0
- package/runtime/core/webshim/job-runner.js +319 -0
- package/runtime/core/webshim/native-bus.js +108 -0
- package/runtime/core/webshim/timers.js +82 -0
- package/runtime/core/webshim/trace.js +205 -0
- package/runtime/core/webshim/transport-base.js +128 -0
- package/runtime/core/webshim/variant-resolver.js +249 -0
- package/runtime/core/webshim/web-serial-bus.js +734 -0
- package/runtime/home/bmweb-home.ips +76 -0
- package/runtime/home/bmweb.h +26 -0
- package/runtime/screens/activations.js +258 -0
- package/runtime/screens/garage/diff.js +331 -0
- package/runtime/screens/garage/share.js +276 -0
- package/runtime/screens/garage/store.js +547 -0
- package/runtime/screens/ipo-runtime/cells.js +176 -0
- package/runtime/screens/ipo-runtime/dialogs.js +254 -0
- package/runtime/screens/ipo-runtime/home.js +358 -0
- package/runtime/screens/ipo-runtime/open.js +393 -0
- package/runtime/screens/ipo-runtime/paint-grid.js +106 -0
- package/runtime/screens/ipo-runtime/paint-modern.js +424 -0
- package/runtime/screens/ipo-runtime/print.js +281 -0
- package/runtime/screens/ipo-runtime/program.js +1337 -0
- package/runtime/screens/ipo-runtime/protocol.js +464 -0
- package/runtime/screens/ipo-runtime/script-scan.js +225 -0
- package/runtime/screens/ipo-runtime/translate-sets.js +130 -0
- package/runtime/screens/ipo-runtime/ui.js +249 -0
- package/runtime/screens/ipo-runtime/wire-policy.js +113 -0
- package/runtime/screens/ir.js +324 -0
- package/runtime/screens/search/data.js +153 -0
- package/runtime/screens/search/match.js +285 -0
- package/runtime/screens/search/open.js +66 -0
- package/runtime/vendor/fflate.min.js +1 -0
package/dist/bmweb.js
ADDED
|
@@ -0,0 +1,2790 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/args.ts
|
|
4
|
+
var CliError = class extends Error {
|
|
5
|
+
};
|
|
6
|
+
function parseArgs(argv, spec) {
|
|
7
|
+
const positional = [];
|
|
8
|
+
const flags = {};
|
|
9
|
+
const byAlias = /* @__PURE__ */ new Map();
|
|
10
|
+
for (const [name, s] of Object.entries(spec))
|
|
11
|
+
if (s.alias) byAlias.set(s.alias, name);
|
|
12
|
+
const set = (name, raw) => {
|
|
13
|
+
const s = spec[name];
|
|
14
|
+
if (!s) throw new CliError(`unknown option --${name}`);
|
|
15
|
+
if (s.kind === "bool") {
|
|
16
|
+
if (raw !== null) throw new CliError(`--${name} takes no value`);
|
|
17
|
+
flags[name] = true;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (raw === null) throw new CliError(`--${name} needs a value`);
|
|
21
|
+
if (s.kind === "number") {
|
|
22
|
+
const n = Number(raw);
|
|
23
|
+
if (!Number.isFinite(n)) throw new CliError(`--${name} needs a number`);
|
|
24
|
+
flags[name] = n;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (s.kind === "list") {
|
|
28
|
+
const list = flags[name] || [];
|
|
29
|
+
list.push(raw);
|
|
30
|
+
flags[name] = list;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
flags[name] = raw;
|
|
34
|
+
};
|
|
35
|
+
for (let i = 0; i < argv.length; i++) {
|
|
36
|
+
const a = argv[i];
|
|
37
|
+
if (a === "--") {
|
|
38
|
+
positional.push(...argv.slice(i + 1));
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
if (a.startsWith("--")) {
|
|
42
|
+
const eq = a.indexOf("=");
|
|
43
|
+
const name = eq >= 0 ? a.slice(2, eq) : a.slice(2);
|
|
44
|
+
const s = spec[name];
|
|
45
|
+
if (!s) throw new CliError(`unknown option --${name}`);
|
|
46
|
+
if (eq >= 0) set(name, a.slice(eq + 1));
|
|
47
|
+
else if (s.kind === "bool") set(name, null);
|
|
48
|
+
else {
|
|
49
|
+
const v = argv[i + 1];
|
|
50
|
+
if (v === void 0) throw new CliError(`--${name} needs a value`);
|
|
51
|
+
set(name, v);
|
|
52
|
+
i++;
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (a.length >= 2 && a[0] === "-" && a !== "-") {
|
|
57
|
+
const name = byAlias.get(a[1]);
|
|
58
|
+
if (!name) throw new CliError(`unknown option ${a.slice(0, 2)}`);
|
|
59
|
+
const s = spec[name];
|
|
60
|
+
if (a.length > 2) set(name, a.slice(2));
|
|
61
|
+
else if (s.kind === "bool") set(name, null);
|
|
62
|
+
else {
|
|
63
|
+
const v = argv[i + 1];
|
|
64
|
+
if (v === void 0) throw new CliError(`${a} needs a value`);
|
|
65
|
+
set(name, v);
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
positional.push(a);
|
|
71
|
+
}
|
|
72
|
+
return { positional, flags };
|
|
73
|
+
}
|
|
74
|
+
function helpLines(spec) {
|
|
75
|
+
const rows = Object.entries(spec).map(([name, s]) => {
|
|
76
|
+
const value = s.kind === "bool" ? "" : s.kind === "number" ? " N" : " <value>";
|
|
77
|
+
const left = `${s.alias ? `-${s.alias}, ` : " "}--${name}${value}`;
|
|
78
|
+
return [left, s.help || ""];
|
|
79
|
+
});
|
|
80
|
+
const w = Math.max(0, ...rows.map((r) => r[0].length));
|
|
81
|
+
return rows.map(([l, h]) => ` ${l.padEnd(w)} ${h}`.trimEnd());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/ipo.ts
|
|
85
|
+
import { readdirSync, readFileSync as readFileSync3, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
86
|
+
import { basename, dirname as dirname2, join as join4, resolve } from "node:path";
|
|
87
|
+
|
|
88
|
+
// src/table.ts
|
|
89
|
+
function formatTable(rows, header, indent = "") {
|
|
90
|
+
const all = header ? [header, ...rows] : rows;
|
|
91
|
+
const text = all.map((r) => r.map((c) => c === void 0 ? "" : String(c)));
|
|
92
|
+
const cols = Math.max(0, ...text.map((r) => r.length));
|
|
93
|
+
const widths = [];
|
|
94
|
+
for (let c = 0; c < cols; c++)
|
|
95
|
+
widths[c] = Math.max(0, ...text.map((r) => (r[c] || "").length));
|
|
96
|
+
const line = (r) => indent + r.map(
|
|
97
|
+
(cell, c) => c === cols - 1 ? cell : cell.padEnd(widths[c])
|
|
98
|
+
).join(" ").trimEnd();
|
|
99
|
+
const out = text.map(line);
|
|
100
|
+
if (header) {
|
|
101
|
+
const rule = widths.map((w) => "-".repeat(w));
|
|
102
|
+
out.splice(1, 0, line(rule));
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
function formatCount(n) {
|
|
107
|
+
return n.toLocaleString("en-US");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/runtime.ts
|
|
111
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
112
|
+
import { join as join3 } from "node:path";
|
|
113
|
+
import { fileURLToPath } from "node:url";
|
|
114
|
+
import { createContext, runInContext } from "node:vm";
|
|
115
|
+
|
|
116
|
+
// src/runtime-files.json
|
|
117
|
+
var runtime_files_default = [
|
|
118
|
+
"vendor/fflate.min.js",
|
|
119
|
+
"core/webshim/timers.js",
|
|
120
|
+
"core/webshim/trace.js",
|
|
121
|
+
"core/webshim/framing.js",
|
|
122
|
+
"core/webshim/exchange.js",
|
|
123
|
+
"core/webshim/transport-base.js",
|
|
124
|
+
"core/webshim/native-bus.js",
|
|
125
|
+
"core/webshim/web-serial-bus.js",
|
|
126
|
+
"core/webshim/bus.js",
|
|
127
|
+
"core/webshim/data-fetch.js",
|
|
128
|
+
"core/webshim/job-runner.js",
|
|
129
|
+
"core/webshim/variant-resolver.js",
|
|
130
|
+
"core/webshim/coding.js",
|
|
131
|
+
"core/webshim/api-router.js",
|
|
132
|
+
"core/webshim/install.js",
|
|
133
|
+
"core/bestvm/write-guard.js",
|
|
134
|
+
"core/bestvm/codec.js",
|
|
135
|
+
"core/bestvm/machine.js",
|
|
136
|
+
"core/bestvm/registers.js",
|
|
137
|
+
"core/bestvm/operands.js",
|
|
138
|
+
"core/bestvm/environment.js",
|
|
139
|
+
"core/bestvm/executor.js",
|
|
140
|
+
"core/bestvm/index.js",
|
|
141
|
+
"core/ipovm/values.js",
|
|
142
|
+
"core/ipovm/operators.js",
|
|
143
|
+
"core/ipovm/hosts.js",
|
|
144
|
+
"core/ipovm/emissions.js",
|
|
145
|
+
"core/ipovm/tape.js",
|
|
146
|
+
"core/ipovm/builtin-helpers.js",
|
|
147
|
+
"core/ipovm/builtins-screen.js",
|
|
148
|
+
"core/ipovm/builtins-api.js",
|
|
149
|
+
"core/ipovm/builtins-text.js",
|
|
150
|
+
"core/ipovm/builtins-table.js",
|
|
151
|
+
"core/ipovm/structures.js",
|
|
152
|
+
"core/ipovm/suspensions.js",
|
|
153
|
+
"core/ipovm/vm.js",
|
|
154
|
+
"core/ipofile/pool.js",
|
|
155
|
+
"core/ipofile/decls.js",
|
|
156
|
+
"core/ipofile/walk.js",
|
|
157
|
+
"core/ipofile/exec.js",
|
|
158
|
+
"core/ipofile/lex.js",
|
|
159
|
+
"core/ipofile/parse.js",
|
|
160
|
+
"core/ipofile/emit.js",
|
|
161
|
+
"core/ipofile/compile.js",
|
|
162
|
+
"core/translate.js",
|
|
163
|
+
"screens/ir.js",
|
|
164
|
+
"screens/ipo-runtime/script-scan.js",
|
|
165
|
+
"screens/ipo-runtime/translate-sets.js",
|
|
166
|
+
"screens/ipo-runtime/wire-policy.js",
|
|
167
|
+
"screens/ipo-runtime/program.js",
|
|
168
|
+
"screens/ipo-runtime/cells.js",
|
|
169
|
+
"screens/ipo-runtime/paint-modern.js",
|
|
170
|
+
"screens/ipo-runtime/paint-grid.js",
|
|
171
|
+
"screens/ipo-runtime/dialogs.js",
|
|
172
|
+
"screens/ipo-runtime/print.js",
|
|
173
|
+
"screens/ipo-runtime/protocol.js",
|
|
174
|
+
"screens/ipo-runtime/ui.js",
|
|
175
|
+
"screens/ipo-runtime/open.js",
|
|
176
|
+
"screens/ipo-runtime/home.js",
|
|
177
|
+
"home/bmweb-home.ips",
|
|
178
|
+
"home/bmweb.h",
|
|
179
|
+
"screens/activations.js",
|
|
180
|
+
"screens/garage/store.js",
|
|
181
|
+
"screens/garage/diff.js",
|
|
182
|
+
"screens/garage/share.js",
|
|
183
|
+
"screens/search/data.js",
|
|
184
|
+
"screens/search/match.js",
|
|
185
|
+
"screens/search/open.js"
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
// src/site.ts
|
|
189
|
+
import {
|
|
190
|
+
existsSync,
|
|
191
|
+
mkdirSync,
|
|
192
|
+
readFileSync,
|
|
193
|
+
statSync,
|
|
194
|
+
writeFileSync
|
|
195
|
+
} from "node:fs";
|
|
196
|
+
import { dirname, join as join2 } from "node:path";
|
|
197
|
+
|
|
198
|
+
// src/cache.ts
|
|
199
|
+
import { homedir } from "node:os";
|
|
200
|
+
import { join } from "node:path";
|
|
201
|
+
var CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
202
|
+
function cacheDir(env = process.env) {
|
|
203
|
+
const base = env.XDG_CACHE_HOME && env.XDG_CACHE_HOME.trim() ? env.XDG_CACHE_HOME : join(homedir(), ".cache");
|
|
204
|
+
return join(base, "bmweb-cli");
|
|
205
|
+
}
|
|
206
|
+
function indexCachePath(env = process.env) {
|
|
207
|
+
return join(cacheDir(env), "search-index.json.gz");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/site.ts
|
|
211
|
+
var DEFAULT_API = "https://bmweb.danner.ink/";
|
|
212
|
+
var config = {
|
|
213
|
+
base: DEFAULT_API,
|
|
214
|
+
refresh: false,
|
|
215
|
+
env: process.env,
|
|
216
|
+
fetchImpl: (...a) => fetch(...a)
|
|
217
|
+
};
|
|
218
|
+
function configureSite(opts) {
|
|
219
|
+
if (opts.base !== void 0) config.base = normalizeBase(opts.base);
|
|
220
|
+
if (opts.refresh !== void 0) config.refresh = opts.refresh;
|
|
221
|
+
if (opts.env !== void 0) config.env = opts.env;
|
|
222
|
+
if (opts.fetchImpl !== void 0) config.fetchImpl = opts.fetchImpl;
|
|
223
|
+
}
|
|
224
|
+
function normalizeBase(base) {
|
|
225
|
+
const s = String(base || "").trim();
|
|
226
|
+
return s.endsWith("/") ? s : `${s}/`;
|
|
227
|
+
}
|
|
228
|
+
function sitePath(input) {
|
|
229
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
230
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) return null;
|
|
231
|
+
return url.replace(/^\/+/, "");
|
|
232
|
+
}
|
|
233
|
+
function siteCachePath(rel, env = config.env) {
|
|
234
|
+
const clean = rel.split("?")[0];
|
|
235
|
+
return join2(cacheDir(env), "site", ...clean.split("/"));
|
|
236
|
+
}
|
|
237
|
+
async function siteGet(rel) {
|
|
238
|
+
const file = siteCachePath(rel);
|
|
239
|
+
const have = existsSync(file);
|
|
240
|
+
const fresh = have && Date.now() - statSync(file).mtimeMs < CACHE_MAX_AGE_MS;
|
|
241
|
+
if (have && fresh && !config.refresh)
|
|
242
|
+
return { status: 200, bytes: readFileSync(file), fromCache: true };
|
|
243
|
+
let status = 0;
|
|
244
|
+
let bytes = null;
|
|
245
|
+
try {
|
|
246
|
+
const r = await config.fetchImpl(`${config.base}${rel}`);
|
|
247
|
+
status = r.status;
|
|
248
|
+
if (r.ok) bytes = new Uint8Array(await r.arrayBuffer());
|
|
249
|
+
} catch {
|
|
250
|
+
status = 0;
|
|
251
|
+
}
|
|
252
|
+
if (bytes) {
|
|
253
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
254
|
+
writeFileSync(file, bytes);
|
|
255
|
+
return { status: 200, bytes, fromCache: false };
|
|
256
|
+
}
|
|
257
|
+
if (have && status !== 404)
|
|
258
|
+
return { status: 200, bytes: readFileSync(file), fromCache: true };
|
|
259
|
+
return { status: status || 503, bytes: null, fromCache: false };
|
|
260
|
+
}
|
|
261
|
+
async function siteFetch(input, init) {
|
|
262
|
+
const rel = sitePath(input);
|
|
263
|
+
if (rel === null) return config.fetchImpl(input, init);
|
|
264
|
+
const r = await siteGet(rel);
|
|
265
|
+
if (!r.bytes)
|
|
266
|
+
return new Response(JSON.stringify({ error: `${rel}: HTTP ${r.status}` }), {
|
|
267
|
+
status: r.status,
|
|
268
|
+
statusText: r.status === 404 ? "Not Found" : "Unavailable"
|
|
269
|
+
});
|
|
270
|
+
return new Response(r.bytes, { status: 200 });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/runtime.ts
|
|
274
|
+
var RUNTIME_DIR = fileURLToPath(
|
|
275
|
+
new URL("../runtime/", import.meta.url)
|
|
276
|
+
);
|
|
277
|
+
var loaded = null;
|
|
278
|
+
var sandboxRef = null;
|
|
279
|
+
var apiImpl = null;
|
|
280
|
+
function setApiImpl(fn) {
|
|
281
|
+
apiImpl = fn;
|
|
282
|
+
}
|
|
283
|
+
function runtimeGlobals() {
|
|
284
|
+
loadRuntime();
|
|
285
|
+
return sandboxRef;
|
|
286
|
+
}
|
|
287
|
+
function hostGlobals() {
|
|
288
|
+
const noop = () => {
|
|
289
|
+
};
|
|
290
|
+
const sandbox = {
|
|
291
|
+
// the scripts test `typeof window` and read window.<x>; the context's own
|
|
292
|
+
// global stands in, as it does in the page (self: fflate's UMD attaches
|
|
293
|
+
// its global to `self` when there is no CommonJS `exports`)
|
|
294
|
+
console,
|
|
295
|
+
// timers.js's bmwSleep falls back to setTimeout where there is no
|
|
296
|
+
// Worker; program.js schedules screen cycles and drains key presses
|
|
297
|
+
// through setTimeout; activations.js defers a session end a microtask
|
|
298
|
+
setTimeout,
|
|
299
|
+
clearTimeout,
|
|
300
|
+
setInterval,
|
|
301
|
+
clearInterval,
|
|
302
|
+
queueMicrotask,
|
|
303
|
+
// share.js: TextEncoder/TextDecoder, Blob, Response and the two
|
|
304
|
+
// compression streams encode and decode the link payload; btoa/atob do
|
|
305
|
+
// the base64url. api-router.js builds Responses and reads query strings;
|
|
306
|
+
// data-fetch.js decodes archive members. All are Node 18+ globals,
|
|
307
|
+
// handed into the fresh realm.
|
|
308
|
+
TextEncoder,
|
|
309
|
+
TextDecoder,
|
|
310
|
+
Blob,
|
|
311
|
+
Response,
|
|
312
|
+
Request,
|
|
313
|
+
URL,
|
|
314
|
+
URLSearchParams,
|
|
315
|
+
CompressionStream,
|
|
316
|
+
DecompressionStream,
|
|
317
|
+
btoa,
|
|
318
|
+
atob,
|
|
319
|
+
// WHAT THE APP READS ITS DATA THROUGH. install.js takes window.fetch as
|
|
320
|
+
// the real fetch and wraps it with the shim; every site-relative path
|
|
321
|
+
// the shim then asks for is served from the hosted site through the
|
|
322
|
+
// disk cache (site.ts), never from a local vendor tree.
|
|
323
|
+
fetch: siteFetch,
|
|
324
|
+
// web-serial-bus.js asks navigator.serial for the port; the CLI fills
|
|
325
|
+
// this in with the Node port once one is chosen (live.ts)
|
|
326
|
+
navigator: { serial: void 0 },
|
|
327
|
+
// activations.js registers a pagehide hook at load; the bus announces
|
|
328
|
+
// cable events; neither has a page here
|
|
329
|
+
addEventListener: noop,
|
|
330
|
+
removeEventListener: noop,
|
|
331
|
+
dispatchEvent: noop,
|
|
332
|
+
// ui.js writes the key caption into the document's style at render
|
|
333
|
+
// time and the harness stubs getElementById; neither draws anything
|
|
334
|
+
document: {
|
|
335
|
+
getElementById: () => null,
|
|
336
|
+
documentElement: { style: { setProperty: noop, removeProperty: noop } }
|
|
337
|
+
},
|
|
338
|
+
// the status bar the runtime writes to
|
|
339
|
+
sbLeft: { textContent: "" },
|
|
340
|
+
sbRight: { textContent: "" },
|
|
341
|
+
// HTML escaping, identity here: no DOM receives the text
|
|
342
|
+
esc: (s) => String(s == null ? "" : s),
|
|
343
|
+
// the F-key bar; the CLI's own UI adapter renders keys itself
|
|
344
|
+
setActions: noop,
|
|
345
|
+
// translate.js and translate-sets.js gate on the language setting
|
|
346
|
+
lang: () => "en",
|
|
347
|
+
// the fault dictionaries the fed results are translated through: the
|
|
348
|
+
// large generated ones are not in this package, so results stay as the
|
|
349
|
+
// module sent them (the app behaves the same in a build without them)
|
|
350
|
+
BMW_ENV_TEXT: {},
|
|
351
|
+
BMW_FAULT_PHRASES: {},
|
|
352
|
+
BMW_FAULT_DB: {},
|
|
353
|
+
scopedFaultDb: () => null,
|
|
354
|
+
// a chassis id's display name (core/core/ui.js); the id is fine here
|
|
355
|
+
dispChassis: (id) => String(id || ""),
|
|
356
|
+
// store.js persists through Settings and wire-policy.js reads the
|
|
357
|
+
// confirmActuators preference; the CLI stores nothing and confirms
|
|
358
|
+
// every actuator drive, so a read-only stand-in returning the default
|
|
359
|
+
// is exact
|
|
360
|
+
Settings: {
|
|
361
|
+
get: (_k, d) => d,
|
|
362
|
+
set: noop
|
|
363
|
+
},
|
|
364
|
+
// open.js's searchOpenHit assigns location.hash; api-router.js derives
|
|
365
|
+
// WEB_BASE from the pathname (empty: paths hang off the site root)
|
|
366
|
+
location: { hash: "", href: "", pathname: "/", search: "" },
|
|
367
|
+
// the engine client (core/core/api.js): a route in, its JSON out, the
|
|
368
|
+
// server's `error` field as the thrown message. Routed through apiImpl
|
|
369
|
+
// so a test can put a fake car behind the very same call.
|
|
370
|
+
api: async (path, opts) => {
|
|
371
|
+
if (apiImpl) return apiImpl(path, opts);
|
|
372
|
+
const f = sandbox.fetch;
|
|
373
|
+
const res = await f(path, opts);
|
|
374
|
+
if (!res.ok) {
|
|
375
|
+
const body = await res.json().catch(() => ({}));
|
|
376
|
+
throw new Error(body.error || res.statusText);
|
|
377
|
+
}
|
|
378
|
+
return res.json();
|
|
379
|
+
},
|
|
380
|
+
// the result-set projection (core/core/api.js): the engine's synthetic
|
|
381
|
+
// system set dropped when it leads
|
|
382
|
+
dataSets: (sets) => {
|
|
383
|
+
const list = sets || [];
|
|
384
|
+
const s = list[0];
|
|
385
|
+
const system = !!s && typeof s === "object" && ("SAETZE" in s || "JOBNAME" in s || "OBJECT" in s);
|
|
386
|
+
return list.length && system ? list.slice(1) : list;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
sandbox.window = sandbox;
|
|
390
|
+
sandbox.self = sandbox;
|
|
391
|
+
sandbox.globalThis = sandbox;
|
|
392
|
+
return sandbox;
|
|
393
|
+
}
|
|
394
|
+
function loadRuntime() {
|
|
395
|
+
if (loaded) return loaded;
|
|
396
|
+
const sandbox = hostGlobals();
|
|
397
|
+
const collected = {};
|
|
398
|
+
sandbox.module = {
|
|
399
|
+
get exports() {
|
|
400
|
+
return collected;
|
|
401
|
+
},
|
|
402
|
+
set exports(v) {
|
|
403
|
+
Object.assign(collected, v);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
const context = createContext(sandbox);
|
|
407
|
+
for (const rel of runtime_files_default) {
|
|
408
|
+
if (!rel.endsWith(".js")) continue;
|
|
409
|
+
const file = join3(RUNTIME_DIR, rel);
|
|
410
|
+
let code;
|
|
411
|
+
try {
|
|
412
|
+
code = readFileSync2(file, "utf8");
|
|
413
|
+
} catch {
|
|
414
|
+
throw new Error(
|
|
415
|
+
`runtime file missing: ${rel} (the package was built without scripts/sync-runtime.mjs)`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
runInContext(code, context, { filename: rel });
|
|
419
|
+
}
|
|
420
|
+
for (const name of LIFTED_GLOBALS) {
|
|
421
|
+
collected[name] = runInContext(
|
|
422
|
+
`typeof ${name} === 'undefined' ? undefined : ${name}`,
|
|
423
|
+
context
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
sandboxRef = sandbox;
|
|
427
|
+
loaded = collected;
|
|
428
|
+
return loaded;
|
|
429
|
+
}
|
|
430
|
+
var LIFTED_GLOBALS = [
|
|
431
|
+
"IPO_IDENT_ROWS",
|
|
432
|
+
"IPO_TICK_MS",
|
|
433
|
+
"webBus",
|
|
434
|
+
"withBusLock",
|
|
435
|
+
"webResolveVariant",
|
|
436
|
+
"webResolveVariantLast",
|
|
437
|
+
"isWriteJob",
|
|
438
|
+
"ipoProtocolReport",
|
|
439
|
+
// home.js's host object: the status builtin reads it by name, so the
|
|
440
|
+
// CLI hangs its own status on it rather than on the VM
|
|
441
|
+
"IPO_HOME_HOST"
|
|
442
|
+
];
|
|
443
|
+
|
|
444
|
+
// src/ipo.ts
|
|
445
|
+
var SHIFT_BASE = 10;
|
|
446
|
+
var JOB_NAME_RE = /^[A-Z][A-Z0-9_]{2,}$/;
|
|
447
|
+
var JOB_CALL_RE = /^(INP.?apiJob|INPAapiFsLesen|ApiJobFsLesenFAB)$/;
|
|
448
|
+
var ACTION_CALLS = /* @__PURE__ */ new Set([
|
|
449
|
+
"exit",
|
|
450
|
+
"printscreen",
|
|
451
|
+
"printfile",
|
|
452
|
+
"select",
|
|
453
|
+
"deselect",
|
|
454
|
+
"start",
|
|
455
|
+
"stop",
|
|
456
|
+
"control",
|
|
457
|
+
"messagebox"
|
|
458
|
+
]);
|
|
459
|
+
var MAX_CALL_DEPTH = 4;
|
|
460
|
+
function keyName(nr) {
|
|
461
|
+
return nr > SHIFT_BASE ? `Shift+F${nr - SHIFT_BASE}` : `F${nr}`;
|
|
462
|
+
}
|
|
463
|
+
function scanBody(exec, toks, start, end, R, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
464
|
+
const out = {
|
|
465
|
+
jobs: [],
|
|
466
|
+
screen: null,
|
|
467
|
+
frequent: false,
|
|
468
|
+
submenu: null,
|
|
469
|
+
scriptchange: null,
|
|
470
|
+
actions: [],
|
|
471
|
+
title: null
|
|
472
|
+
};
|
|
473
|
+
const byid = exec.byid || {};
|
|
474
|
+
let args = [];
|
|
475
|
+
const addJob = (j) => {
|
|
476
|
+
if (!out.jobs.includes(j)) out.jobs.push(j);
|
|
477
|
+
};
|
|
478
|
+
const strings = () => args.filter((t) => t.op === "const" && t.t === "s").map((t) => String(t.v));
|
|
479
|
+
const merge = (inner) => {
|
|
480
|
+
inner.jobs.forEach(addJob);
|
|
481
|
+
if (!out.screen && inner.screen) {
|
|
482
|
+
out.screen = inner.screen;
|
|
483
|
+
out.frequent = inner.frequent;
|
|
484
|
+
}
|
|
485
|
+
if (!out.submenu && inner.submenu) out.submenu = inner.submenu;
|
|
486
|
+
if (!out.scriptchange && inner.scriptchange)
|
|
487
|
+
out.scriptchange = inner.scriptchange;
|
|
488
|
+
if (!out.title && inner.title) out.title = inner.title;
|
|
489
|
+
for (const a of inner.actions)
|
|
490
|
+
if (!out.actions.includes(a)) out.actions.push(a);
|
|
491
|
+
};
|
|
492
|
+
for (let i = start; i < end && i < toks.length; i++) {
|
|
493
|
+
const t = toks[i];
|
|
494
|
+
if (t.op === "frame") {
|
|
495
|
+
args = [];
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (t.op === "const" || t.op === "var" || t.op === "procref") {
|
|
499
|
+
args.push(t);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (t.op === "calluser") {
|
|
503
|
+
const name2 = byid[`func:${t.n}`];
|
|
504
|
+
if (name2 && depth < MAX_CALL_DEPTH && !seen.has(name2)) {
|
|
505
|
+
const body = exec.procs[name2];
|
|
506
|
+
if (body) {
|
|
507
|
+
const next = new Set(seen);
|
|
508
|
+
next.add(name2);
|
|
509
|
+
merge(scanBody(exec, body, 0, body.length, R, depth + 1, next));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
args = [];
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (t.op !== "call") continue;
|
|
516
|
+
const name = t.name || "";
|
|
517
|
+
if (JOB_CALL_RE.test(name)) {
|
|
518
|
+
const s = strings();
|
|
519
|
+
let job;
|
|
520
|
+
if (/^INP.?apiJob$/.test(name)) {
|
|
521
|
+
const second = args[1];
|
|
522
|
+
if (second && second.op === "const" && second.t === "s")
|
|
523
|
+
job = String(second.v);
|
|
524
|
+
}
|
|
525
|
+
if (job === void 0) {
|
|
526
|
+
const first = args[0];
|
|
527
|
+
const sgbd = first && first.op === "const" && first.t === "s" ? String(first.v) : "";
|
|
528
|
+
job = s.find((v) => v !== sgbd && JOB_NAME_RE.test(v));
|
|
529
|
+
}
|
|
530
|
+
if (job && JOB_NAME_RE.test(job)) addJob(job);
|
|
531
|
+
} else if (name === "setscreen") {
|
|
532
|
+
const ref = args.find(
|
|
533
|
+
(a) => a.op === "procref" && a.kind === R.IPO_REF_SCREEN
|
|
534
|
+
);
|
|
535
|
+
const flag = args.find(
|
|
536
|
+
(a) => a.op === "const" && (a.t === "b" || a.t === "i")
|
|
537
|
+
);
|
|
538
|
+
const scr = ref ? byid[`screen:${ref.n}`] : void 0;
|
|
539
|
+
if (scr && !out.screen) {
|
|
540
|
+
out.screen = scr;
|
|
541
|
+
out.frequent = !!(flag && flag.v);
|
|
542
|
+
}
|
|
543
|
+
} else if (name === "setmenu") {
|
|
544
|
+
const ref = args.find(
|
|
545
|
+
(a) => a.op === "procref" && a.kind === R.IPO_REF_MENU
|
|
546
|
+
);
|
|
547
|
+
const menu = ref ? byid[`menu:${ref.n}`] : void 0;
|
|
548
|
+
if (menu && !out.submenu) out.submenu = menu;
|
|
549
|
+
} else if (name === "scriptchange") {
|
|
550
|
+
const s = strings();
|
|
551
|
+
if (s[0] && !out.scriptchange) out.scriptchange = s[0];
|
|
552
|
+
} else if (name === "setmenutitle" || name === "settitle") {
|
|
553
|
+
const s = strings();
|
|
554
|
+
if (s[0] && !out.title) out.title = s[0];
|
|
555
|
+
} else if (ACTION_CALLS.has(name)) {
|
|
556
|
+
if (!out.actions.includes(name)) out.actions.push(name);
|
|
557
|
+
}
|
|
558
|
+
args = [];
|
|
559
|
+
}
|
|
560
|
+
return out;
|
|
561
|
+
}
|
|
562
|
+
function scanScreen(exec, screen, R) {
|
|
563
|
+
const toks = screen ? exec.procs[screen] : void 0;
|
|
564
|
+
if (!toks) return null;
|
|
565
|
+
return scanBody(exec, toks, 0, toks.length, R);
|
|
566
|
+
}
|
|
567
|
+
function describeKey(exec, menu, item, R) {
|
|
568
|
+
const toks = exec.procs[menu] || [];
|
|
569
|
+
const own = scanBody(exec, toks, item.start, item.end, R);
|
|
570
|
+
const scr = scanScreen(exec, own.screen, R);
|
|
571
|
+
const jobs = [...own.jobs];
|
|
572
|
+
for (const j of scr ? scr.jobs : []) if (!jobs.includes(j)) jobs.push(j);
|
|
573
|
+
return {
|
|
574
|
+
menu,
|
|
575
|
+
nr: item.nr,
|
|
576
|
+
key: keyName(item.nr),
|
|
577
|
+
label: item.label,
|
|
578
|
+
hidden: item.hidden,
|
|
579
|
+
screen: own.screen,
|
|
580
|
+
screenTitle: scr ? scr.title : null,
|
|
581
|
+
frequent: own.frequent,
|
|
582
|
+
submenu: own.submenu,
|
|
583
|
+
scriptchange: own.scriptchange,
|
|
584
|
+
ownJobs: own.jobs,
|
|
585
|
+
screenJobs: scr ? scr.jobs : [],
|
|
586
|
+
jobs,
|
|
587
|
+
// the app's own verdict: the write classifier, minus the session
|
|
588
|
+
// plumbing every script sends without asking (wire-policy.js)
|
|
589
|
+
writes: jobs.filter((j) => R.ipoNeedsConfirm(j)),
|
|
590
|
+
actions: own.actions
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function describeMenu(exec, name, R) {
|
|
594
|
+
const toks = exec.procs[name] || [];
|
|
595
|
+
const items = R.ipoMenuItems(exec, name);
|
|
596
|
+
const firstItem = items.length ? items[0].start - 1 : toks.length;
|
|
597
|
+
const prologue = scanBody(exec, toks, 0, firstItem, R);
|
|
598
|
+
const shown = R.ipoScreenForMenu(exec, name);
|
|
599
|
+
return {
|
|
600
|
+
name,
|
|
601
|
+
title: prologue.title,
|
|
602
|
+
screen: shown ? shown.screen : prologue.screen,
|
|
603
|
+
keys: items.map((it) => describeKey(exec, name, it, R))
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function menuNames(exec) {
|
|
607
|
+
return Object.entries(exec.byid || {}).filter(([k]) => k.startsWith("menu:")).sort((a, b) => Number(a[0].split(":")[1]) - Number(b[0].split(":")[1])).map(([, v]) => v);
|
|
608
|
+
}
|
|
609
|
+
function describeScript(exec, source, R, includes = Array.from(exec.includes || [])) {
|
|
610
|
+
const inv = R.ipofInventory(exec);
|
|
611
|
+
const entry = exec.procs.inpainit ? "inpainit" : exec.procs.SgbdInpaCheck ? "SgbdInpaCheck" : null;
|
|
612
|
+
const entryScan = entry ? scanBody(exec, exec.procs[entry], 0, Infinity, R) : null;
|
|
613
|
+
return {
|
|
614
|
+
name: exec.ecu,
|
|
615
|
+
source,
|
|
616
|
+
form: exec.coding ? "NCS coding dispatcher (drives the coding host, not a diagnostic session)" : "INPA diagnostic script",
|
|
617
|
+
bytes: exec.bytes ?? null,
|
|
618
|
+
unknown: exec.unknown ?? 0,
|
|
619
|
+
entry,
|
|
620
|
+
rootMenu: entryScan ? entryScan.submenu : null,
|
|
621
|
+
rootScreen: entryScan ? entryScan.screen : null,
|
|
622
|
+
includes,
|
|
623
|
+
imports: Object.values(exec.imports || {}),
|
|
624
|
+
procedures: Object.keys(exec.procs).length,
|
|
625
|
+
functions: inv.funcs.length,
|
|
626
|
+
machines: inv.machines.length,
|
|
627
|
+
screens: inv.screens.length,
|
|
628
|
+
menus: menuNames(exec).map((m) => describeMenu(exec, m, R))
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function gatherIncludes(dirs, skip) {
|
|
632
|
+
const R = loadRuntime();
|
|
633
|
+
const files = {};
|
|
634
|
+
const own = resolve(skip);
|
|
635
|
+
for (const dir of dirs) {
|
|
636
|
+
let names;
|
|
637
|
+
try {
|
|
638
|
+
names = readdirSync(dir);
|
|
639
|
+
} catch {
|
|
640
|
+
throw new CliError(`include directory not found: ${dir}`);
|
|
641
|
+
}
|
|
642
|
+
for (const name of names.sort()) {
|
|
643
|
+
if (!R.ipofIsInclude(name)) continue;
|
|
644
|
+
const p = join4(dir, name);
|
|
645
|
+
if (resolve(p) === own) continue;
|
|
646
|
+
try {
|
|
647
|
+
if (!statSync2(p).isFile()) continue;
|
|
648
|
+
} catch {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
const key = name.toLowerCase();
|
|
652
|
+
if (!(key in files)) files[key] = readFileSync3(p, "latin1");
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return files;
|
|
656
|
+
}
|
|
657
|
+
function readScript(file, includeDirs) {
|
|
658
|
+
const R = loadRuntime();
|
|
659
|
+
const name = basename(file);
|
|
660
|
+
const stem = R.ipofStem(name);
|
|
661
|
+
let bytes;
|
|
662
|
+
try {
|
|
663
|
+
bytes = readFileSync3(file);
|
|
664
|
+
} catch {
|
|
665
|
+
throw new CliError(`cannot read ${file}`);
|
|
666
|
+
}
|
|
667
|
+
if (R.ipofIsCompiled(name)) {
|
|
668
|
+
let exec;
|
|
669
|
+
try {
|
|
670
|
+
exec = R.ipofDecodeExec(new Uint8Array(bytes), stem);
|
|
671
|
+
} catch (e) {
|
|
672
|
+
throw new CliError(String(e.message || e));
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
exec,
|
|
676
|
+
source: "compiled .IPO",
|
|
677
|
+
stem,
|
|
678
|
+
includes: Array.from(exec.includes || [])
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
if (!R.ipofIsSource(name))
|
|
682
|
+
throw new CliError(`${name}: not a script (.IPO) or a source (.IPS, .SRC)`);
|
|
683
|
+
const dirs = [dirname2(resolve(file)), ...includeDirs];
|
|
684
|
+
const files = gatherIncludes(dirs, file);
|
|
685
|
+
const r = R.ipofCompileSource(bytes.toString("latin1"), {
|
|
686
|
+
name: stem,
|
|
687
|
+
files
|
|
688
|
+
});
|
|
689
|
+
if (!r.ok || !r.exec) {
|
|
690
|
+
if (r.missing.length) {
|
|
691
|
+
throw new CliError(
|
|
692
|
+
`${name}: missing include${r.missing.length === 1 ? "" : "s"} ${r.missing.join(", ")} (searched ${dirs.join(", ")}; pass -I <dir> with the INPA headers)`
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
const first = r.errors[0];
|
|
696
|
+
const where = first && first.line ? `line ${first.line}: ` : "";
|
|
697
|
+
throw new CliError(
|
|
698
|
+
`${name}: ${where}${first ? first.text || first.message : "compile failed"}`
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
return {
|
|
702
|
+
exec: r.exec,
|
|
703
|
+
source: "compiled from source",
|
|
704
|
+
stem,
|
|
705
|
+
includes: Array.from(r.includes)
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
function ipoInfo(file, includeDirs, json) {
|
|
709
|
+
const R = loadRuntime();
|
|
710
|
+
const s = readScript(file, includeDirs);
|
|
711
|
+
const info = describeScript(s.exec, s.source, R, s.includes);
|
|
712
|
+
if (json) return [JSON.stringify(info, null, 2)];
|
|
713
|
+
const kv = [
|
|
714
|
+
["Script", `${info.name} (${info.source})`],
|
|
715
|
+
["Form", info.form],
|
|
716
|
+
[
|
|
717
|
+
"Entry",
|
|
718
|
+
info.entry ? `${info.entry}${info.rootMenu || info.rootScreen ? ` -> ${[info.rootMenu, info.rootScreen].filter(Boolean).join(" / ")}` : ""}` : "none (INPA starts at inpainit or SgbdInpaCheck; this script has neither)"
|
|
719
|
+
],
|
|
720
|
+
["Includes", info.includes.length ? info.includes.join(", ") : "none"],
|
|
721
|
+
[
|
|
722
|
+
"Procedures",
|
|
723
|
+
`${info.procedures} (${info.menus.length} menus, ${info.screens} screens, ${info.functions} functions, ${info.machines} state machines)`
|
|
724
|
+
],
|
|
725
|
+
["DLL imports", info.imports.length ? info.imports.join(", ") : "none"]
|
|
726
|
+
];
|
|
727
|
+
if (info.bytes != null) {
|
|
728
|
+
kv.push([
|
|
729
|
+
"Bytecode",
|
|
730
|
+
`${formatCount(info.bytes)} bytes` + (info.unknown ? `, ${formatCount(info.unknown)} undecoded` : "")
|
|
731
|
+
]);
|
|
732
|
+
}
|
|
733
|
+
const out = formatTable(kv);
|
|
734
|
+
for (const m of info.menus) {
|
|
735
|
+
out.push("");
|
|
736
|
+
out.push(...menuLines(m));
|
|
737
|
+
}
|
|
738
|
+
return out;
|
|
739
|
+
}
|
|
740
|
+
function menuLines(m) {
|
|
741
|
+
const head = [
|
|
742
|
+
m.name,
|
|
743
|
+
m.title ? `"${m.title}"` : "",
|
|
744
|
+
m.screen ? `(screen ${m.screen})` : ""
|
|
745
|
+
].filter(Boolean).join(" ");
|
|
746
|
+
const rows = m.keys.map((k) => [
|
|
747
|
+
k.key,
|
|
748
|
+
keyLabel(k),
|
|
749
|
+
keyTarget(k),
|
|
750
|
+
keyJobsText(k)
|
|
751
|
+
]);
|
|
752
|
+
return [head, ...formatTable(rows, void 0, " ")];
|
|
753
|
+
}
|
|
754
|
+
function keyLabel(k) {
|
|
755
|
+
return k.hidden ? "(no caption)" : k.label;
|
|
756
|
+
}
|
|
757
|
+
function keyTarget(k) {
|
|
758
|
+
const parts = [];
|
|
759
|
+
if (k.screen) parts.push(`${k.screen}${k.frequent ? " (cyclic)" : ""}`);
|
|
760
|
+
if (k.submenu) parts.push(`menu ${k.submenu}`);
|
|
761
|
+
if (k.scriptchange) parts.push(`script ${k.scriptchange}`);
|
|
762
|
+
return parts.join(", ");
|
|
763
|
+
}
|
|
764
|
+
function keyJobsText(k) {
|
|
765
|
+
const jobs = k.jobs.map((j) => k.writes.includes(j) ? `${j} [WRITE]` : j);
|
|
766
|
+
const acts = k.actions.filter((a) => a !== "messagebox");
|
|
767
|
+
return [...jobs, ...acts.map((a) => `(${a})`)].join(", ");
|
|
768
|
+
}
|
|
769
|
+
function ipoKeys(file, includeDirs, menu, json) {
|
|
770
|
+
const R = loadRuntime();
|
|
771
|
+
const s = readScript(file, includeDirs);
|
|
772
|
+
const names = menu ? [menu] : menuNames(s.exec);
|
|
773
|
+
if (menu && !(menu in s.exec.procs))
|
|
774
|
+
throw new CliError(
|
|
775
|
+
`${s.stem}: no menu ${menu} (menus: ${menuNames(s.exec).join(", ") || "none"})`
|
|
776
|
+
);
|
|
777
|
+
const menus = names.map((m) => describeMenu(s.exec, m, R));
|
|
778
|
+
if (json) return [JSON.stringify(menus, null, 2)];
|
|
779
|
+
const rows = [];
|
|
780
|
+
for (const m of menus)
|
|
781
|
+
for (const k of m.keys)
|
|
782
|
+
rows.push([
|
|
783
|
+
m.name,
|
|
784
|
+
k.key,
|
|
785
|
+
keyLabel(k),
|
|
786
|
+
keyTarget(k),
|
|
787
|
+
keyJobsText(k),
|
|
788
|
+
k.writes.length ? "yes" : ""
|
|
789
|
+
]);
|
|
790
|
+
if (!rows.length) return [`${s.stem}: no menu keys`];
|
|
791
|
+
return formatTable(rows, ["MENU", "KEY", "LABEL", "OPENS", "JOBS", "WRITES"]);
|
|
792
|
+
}
|
|
793
|
+
function ipoCompile(file, includeDirs, out) {
|
|
794
|
+
const R = loadRuntime();
|
|
795
|
+
const name = basename(file);
|
|
796
|
+
if (!R.ipofIsSource(name))
|
|
797
|
+
throw new CliError(`${name}: compile takes a .IPS or .SRC source`);
|
|
798
|
+
const s = readScript(file, includeDirs);
|
|
799
|
+
const target = out || join4(dirname2(resolve(file)), `${s.stem}.ipoexec.json`);
|
|
800
|
+
try {
|
|
801
|
+
writeFileSync2(target, JSON.stringify(s.exec));
|
|
802
|
+
} catch {
|
|
803
|
+
throw new CliError(`cannot write ${target}`);
|
|
804
|
+
}
|
|
805
|
+
const inv = R.ipofInventory(s.exec);
|
|
806
|
+
return [
|
|
807
|
+
`${name}: compiled ${Object.keys(s.exec.procs).length} procedures (${inv.menus.length} menus, ${inv.screens.length} screens, ${inv.funcs.length} functions, ${inv.machines.length} state machines)` + (s.includes.length ? `, includes ${s.includes.join(", ")}` : ""),
|
|
808
|
+
`wrote ${target} (the app's exec form; not INPA's binary .IPO)`
|
|
809
|
+
];
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// src/live.ts
|
|
813
|
+
import { createInterface } from "node:readline";
|
|
814
|
+
|
|
815
|
+
// src/serial.ts
|
|
816
|
+
import { readdirSync as readdirSync2 } from "node:fs";
|
|
817
|
+
var PORT_PATTERNS = [
|
|
818
|
+
/^cu\.usbserial/i,
|
|
819
|
+
/^cu\.SLAB/i,
|
|
820
|
+
/^cu\.wchusbserial/i,
|
|
821
|
+
/^ttyUSB/i,
|
|
822
|
+
/^ttyACM/i
|
|
823
|
+
];
|
|
824
|
+
var NodeSerialPort = class {
|
|
825
|
+
path;
|
|
826
|
+
opener;
|
|
827
|
+
binding = null;
|
|
828
|
+
/** chunks heard and not yet read */
|
|
829
|
+
chunks = [];
|
|
830
|
+
/** the one read waiting for bytes, when the queue is empty */
|
|
831
|
+
waiter = null;
|
|
832
|
+
/** the lines as last set, so a partial setSignals keeps the others */
|
|
833
|
+
lines = { dtr: false, rts: false, brk: false };
|
|
834
|
+
/** the wire trace sink, when the CLI wants one */
|
|
835
|
+
info;
|
|
836
|
+
/**
|
|
837
|
+
* @param path - the device path
|
|
838
|
+
* @param opener - how a binding is opened
|
|
839
|
+
* @param info - vendor and product ids, when known (portLabel shows them)
|
|
840
|
+
*/
|
|
841
|
+
constructor(path, opener, info = {}) {
|
|
842
|
+
this.path = path;
|
|
843
|
+
this.opener = opener;
|
|
844
|
+
this.info = info;
|
|
845
|
+
}
|
|
846
|
+
/** Is a binding open (Web Serial's SerialPort.connected). */
|
|
847
|
+
get connected() {
|
|
848
|
+
return !!this.binding;
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Open the device with the given settings.
|
|
852
|
+
* @param cfg - baud, bits, parity
|
|
853
|
+
*/
|
|
854
|
+
async open(cfg) {
|
|
855
|
+
if (this.binding) throw new Error(`${this.path} is already open`);
|
|
856
|
+
const b = await this.opener(this.path, cfg);
|
|
857
|
+
this.binding = b;
|
|
858
|
+
this.chunks = [];
|
|
859
|
+
b.onData((chunk) => this.push(chunk));
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Close the device: the binding goes, a waiting read is told `done`.
|
|
863
|
+
*/
|
|
864
|
+
async close() {
|
|
865
|
+
const b = this.binding;
|
|
866
|
+
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.chunks = [];
|
|
873
|
+
if (b) await b.close();
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Bytes the binding heard: to the waiting read, else queued.
|
|
877
|
+
* @param chunk - the bytes
|
|
878
|
+
*/
|
|
879
|
+
push(chunk) {
|
|
880
|
+
if (!chunk.length) return;
|
|
881
|
+
if (this.waiter) {
|
|
882
|
+
const w = this.waiter;
|
|
883
|
+
this.waiter = null;
|
|
884
|
+
w({ value: chunk, done: false });
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
this.chunks.push(chunk);
|
|
888
|
+
}
|
|
889
|
+
/** The readable side: getReader() hands back the one reader. */
|
|
890
|
+
get readable() {
|
|
891
|
+
return {
|
|
892
|
+
getReader: () => ({
|
|
893
|
+
read: () => this.read(),
|
|
894
|
+
cancel: async () => this.cancel(),
|
|
895
|
+
releaseLock: () => {
|
|
896
|
+
}
|
|
897
|
+
})
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
/** The writable side: getWriter() hands back the one writer. */
|
|
901
|
+
get writable() {
|
|
902
|
+
return {
|
|
903
|
+
getWriter: () => ({
|
|
904
|
+
write: (bytes) => this.write(bytes),
|
|
905
|
+
releaseLock: () => {
|
|
906
|
+
}
|
|
907
|
+
})
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* One read: the next chunk, or `done` when the port is closed.
|
|
912
|
+
* @returns the read result
|
|
913
|
+
*/
|
|
914
|
+
read() {
|
|
915
|
+
const next = this.chunks.shift();
|
|
916
|
+
if (next) return Promise.resolve({ value: next, done: false });
|
|
917
|
+
if (!this.binding) return Promise.resolve({ value: void 0, done: true });
|
|
918
|
+
return new Promise((resolve2) => {
|
|
919
|
+
this.waiter = resolve2;
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Cancel the reader: a waiting read is told `done`, buffered bytes go.
|
|
924
|
+
*/
|
|
925
|
+
cancel() {
|
|
926
|
+
this.chunks = [];
|
|
927
|
+
if (this.waiter) {
|
|
928
|
+
const w = this.waiter;
|
|
929
|
+
this.waiter = null;
|
|
930
|
+
w({ value: void 0, done: true });
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Write bytes. Resolves when the OS has them, not when they have left the
|
|
935
|
+
* wire: the bus then holds DTR for the telegram's own byte time, and
|
|
936
|
+
* waiting for transmission here would double that hold and lose the
|
|
937
|
+
* ECU's answer (the reference interface's DtrTimeCorrCom is 0.3 ms).
|
|
938
|
+
* @param bytes - the framed request
|
|
939
|
+
*/
|
|
940
|
+
async write(bytes) {
|
|
941
|
+
if (!this.binding) throw new Error(`${this.path} is not open`);
|
|
942
|
+
await this.binding.write(bytes);
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Drive the modem lines. A partial call keeps the lines it does not name,
|
|
946
|
+
* as Web Serial does; the binding always gets all three, because
|
|
947
|
+
* `serialport` would otherwise assert the ones left unsaid.
|
|
948
|
+
* @param s - the lines to set
|
|
949
|
+
*/
|
|
950
|
+
async setSignals(s) {
|
|
951
|
+
if (!this.binding) throw new Error(`${this.path} is not open`);
|
|
952
|
+
if (s.dataTerminalReady !== void 0)
|
|
953
|
+
this.lines.dtr = !!s.dataTerminalReady;
|
|
954
|
+
if (s.requestToSend !== void 0) this.lines.rts = !!s.requestToSend;
|
|
955
|
+
if (s.break !== void 0) this.lines.brk = !!s.break;
|
|
956
|
+
await this.binding.set({ ...this.lines });
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Read the modem lines (KL15 arrives on DSR or DCD of a K+DCAN cable).
|
|
960
|
+
* @returns the lines, or null when the binding cannot say
|
|
961
|
+
*/
|
|
962
|
+
async getSignals() {
|
|
963
|
+
if (!this.binding) return null;
|
|
964
|
+
const st = await this.binding.get();
|
|
965
|
+
if (!st) return null;
|
|
966
|
+
return {
|
|
967
|
+
dataSetReady: !!st.dsr,
|
|
968
|
+
dataCarrierDetect: !!st.dcd,
|
|
969
|
+
clearToSend: !!st.cts
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* The USB ids, for the bus's port label.
|
|
974
|
+
* @returns what is known
|
|
975
|
+
*/
|
|
976
|
+
getInfo() {
|
|
977
|
+
return this.info;
|
|
978
|
+
}
|
|
979
|
+
/** Web Serial's disconnect event; a pulled USB cable is not watched here. */
|
|
980
|
+
addEventListener() {
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
var serialportModule = null;
|
|
984
|
+
async function loadSerialport() {
|
|
985
|
+
if (serialportModule) return serialportModule;
|
|
986
|
+
try {
|
|
987
|
+
serialportModule = await import("serialport");
|
|
988
|
+
} catch {
|
|
989
|
+
throw new CliError(
|
|
990
|
+
"the serialport package is not installed; run: npm i -g serialport (or reinstall bmweb-cli with its optional dependencies)"
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
return serialportModule;
|
|
994
|
+
}
|
|
995
|
+
async function openSerialportBinding(path, cfg) {
|
|
996
|
+
const mod = await loadSerialport();
|
|
997
|
+
const port = new mod.SerialPort({
|
|
998
|
+
path,
|
|
999
|
+
baudRate: cfg.baudRate,
|
|
1000
|
+
dataBits: cfg.dataBits,
|
|
1001
|
+
stopBits: cfg.stopBits,
|
|
1002
|
+
parity: cfg.parity,
|
|
1003
|
+
autoOpen: false,
|
|
1004
|
+
// keep the lines where we leave them across close/open
|
|
1005
|
+
hupcl: false
|
|
1006
|
+
});
|
|
1007
|
+
await new Promise(
|
|
1008
|
+
(res, rej) => port.open(
|
|
1009
|
+
(e) => e ? rej(new CliError(`cannot open ${path}: ${e.message}`)) : res()
|
|
1010
|
+
)
|
|
1011
|
+
);
|
|
1012
|
+
const call = (fn) => new Promise((res, rej) => fn((e) => e ? rej(e) : res()));
|
|
1013
|
+
const binding = {
|
|
1014
|
+
write: (bytes) => call((cb) => port.write(Buffer.from(bytes), cb)),
|
|
1015
|
+
onData: (fn) => port.on("data", (b) => fn(new Uint8Array(b))),
|
|
1016
|
+
set: (s) => call(
|
|
1017
|
+
(cb) => port.set(
|
|
1018
|
+
{
|
|
1019
|
+
dtr: s.dtr,
|
|
1020
|
+
rts: s.rts,
|
|
1021
|
+
brk: s.brk,
|
|
1022
|
+
cts: false,
|
|
1023
|
+
dsr: false,
|
|
1024
|
+
...process.platform === "linux" ? { lowLatency: true } : {}
|
|
1025
|
+
},
|
|
1026
|
+
cb
|
|
1027
|
+
)
|
|
1028
|
+
),
|
|
1029
|
+
get: () => new Promise((res) => port.get((e, st) => res(e || !st ? null : st))),
|
|
1030
|
+
close: () => new Promise((res) => port.close(() => res()))
|
|
1031
|
+
};
|
|
1032
|
+
await binding.set({ dtr: false, rts: false, brk: false });
|
|
1033
|
+
return binding;
|
|
1034
|
+
}
|
|
1035
|
+
async function listPorts(devDir = "/dev") {
|
|
1036
|
+
const found = /* @__PURE__ */ new Map();
|
|
1037
|
+
try {
|
|
1038
|
+
for (const name of readdirSync2(devDir))
|
|
1039
|
+
if (PORT_PATTERNS.some((re) => re.test(name)))
|
|
1040
|
+
found.set(`${devDir}/${name}`, "");
|
|
1041
|
+
} catch {
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
const mod = await loadSerialport();
|
|
1045
|
+
for (const p of await mod.SerialPort.list()) {
|
|
1046
|
+
const base = p.path.split("/").pop() || p.path;
|
|
1047
|
+
if (!PORT_PATTERNS.some((re) => re.test(base))) continue;
|
|
1048
|
+
const detail = [
|
|
1049
|
+
p.manufacturer,
|
|
1050
|
+
p.vendorId && p.productId ? `${p.vendorId}:${p.productId}` : "",
|
|
1051
|
+
p.serialNumber ? `sn ${p.serialNumber}` : ""
|
|
1052
|
+
].filter(Boolean).join(" ");
|
|
1053
|
+
found.set(p.path, detail);
|
|
1054
|
+
}
|
|
1055
|
+
} catch {
|
|
1056
|
+
}
|
|
1057
|
+
return [...found.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([path, detail]) => ({ path, detail }));
|
|
1058
|
+
}
|
|
1059
|
+
function choosePort(wanted, candidates) {
|
|
1060
|
+
if (wanted) return wanted;
|
|
1061
|
+
if (candidates.length === 1) return candidates[0].path;
|
|
1062
|
+
if (!candidates.length)
|
|
1063
|
+
throw new CliError(
|
|
1064
|
+
"no K+DCAN cable found (looked for cu.usbserial*, cu.SLAB*, cu.wchusbserial*, ttyUSB*, ttyACM*); pass --port <device>"
|
|
1065
|
+
);
|
|
1066
|
+
throw new CliError(
|
|
1067
|
+
`several ports found; pass --port: ${candidates.map((c) => c.path).join(", ")}`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// src/live.ts
|
|
1072
|
+
var FTDI_HINT = "If this is an FTDI cable, set its latency timer to 1 ms (Linux: done for you; macOS: FTDI D2XX/driver setting; Windows: Device Manager, Port Settings, Advanced).";
|
|
1073
|
+
async function connectBus(opts = {}) {
|
|
1074
|
+
configureSite({
|
|
1075
|
+
...opts.api ? { base: opts.api } : {},
|
|
1076
|
+
refresh: !!opts.refresh
|
|
1077
|
+
});
|
|
1078
|
+
const R = loadRuntime();
|
|
1079
|
+
const g = runtimeGlobals();
|
|
1080
|
+
const candidates = opts.ports || await listPorts();
|
|
1081
|
+
const path = choosePort(opts.port, candidates);
|
|
1082
|
+
const opener = opts.opener || openSerialportBinding;
|
|
1083
|
+
const port = new NodeSerialPort(path, opener);
|
|
1084
|
+
g.navigator.serial = {
|
|
1085
|
+
requestPort: async () => port,
|
|
1086
|
+
getPorts: async () => []
|
|
1087
|
+
};
|
|
1088
|
+
let label;
|
|
1089
|
+
try {
|
|
1090
|
+
label = await R.webBus.connect();
|
|
1091
|
+
} catch (e) {
|
|
1092
|
+
throw new CliError(`cannot open ${path}: ${e.message}`);
|
|
1093
|
+
}
|
|
1094
|
+
return { R, label, path };
|
|
1095
|
+
}
|
|
1096
|
+
async function disconnectBus(R) {
|
|
1097
|
+
try {
|
|
1098
|
+
await R.withBusLock(async () => {
|
|
1099
|
+
});
|
|
1100
|
+
} catch {
|
|
1101
|
+
}
|
|
1102
|
+
if (R.webBus.connected) await R.webBus.disconnect();
|
|
1103
|
+
}
|
|
1104
|
+
async function portsCommand(json, ports) {
|
|
1105
|
+
const list = ports || await listPorts();
|
|
1106
|
+
if (json) return [JSON.stringify(list, null, 2)];
|
|
1107
|
+
if (!list.length)
|
|
1108
|
+
return [
|
|
1109
|
+
"no candidate ports (cu.usbserial*, cu.SLAB*, cu.wchusbserial*, ttyUSB*, ttyACM*)"
|
|
1110
|
+
];
|
|
1111
|
+
return formatTable(list.map((p) => [p.path, p.detail]));
|
|
1112
|
+
}
|
|
1113
|
+
async function askYesNo(question, io = {}) {
|
|
1114
|
+
const input = io.input || process.stdin;
|
|
1115
|
+
const output = io.output || process.stderr;
|
|
1116
|
+
if (!io.input && !process.stdin.isTTY) return false;
|
|
1117
|
+
const rl = createInterface({ input, output });
|
|
1118
|
+
const answer = await new Promise(
|
|
1119
|
+
(res) => rl.question(`${question} [y/N] `, res)
|
|
1120
|
+
);
|
|
1121
|
+
rl.close();
|
|
1122
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
1123
|
+
}
|
|
1124
|
+
async function jobCommand(sgbd, job, opts = {}) {
|
|
1125
|
+
const R = loadRuntime();
|
|
1126
|
+
const name = job.toUpperCase();
|
|
1127
|
+
const target = sgbd.toLowerCase();
|
|
1128
|
+
if (R.isWriteJob(name) && !opts.yes) {
|
|
1129
|
+
const ask = opts.confirm || askYesNo;
|
|
1130
|
+
const ok = await ask(
|
|
1131
|
+
`${name} on ${target} is a write (it changes the module or drives something). Send it?`
|
|
1132
|
+
);
|
|
1133
|
+
if (!ok)
|
|
1134
|
+
return [
|
|
1135
|
+
`${name} on ${target}: not sent (a write needs --yes or a y answer)`
|
|
1136
|
+
];
|
|
1137
|
+
}
|
|
1138
|
+
const api = opts.apiFn || runtimeGlobals().api;
|
|
1139
|
+
const q = opts.arg != null && opts.arg !== "" ? `?arg=${encodeURIComponent(opts.arg)}` : "";
|
|
1140
|
+
let d;
|
|
1141
|
+
try {
|
|
1142
|
+
d = await api(`/api/ecu/${target}/run/${encodeURIComponent(name)}${q}`, {
|
|
1143
|
+
method: "POST"
|
|
1144
|
+
});
|
|
1145
|
+
} catch (e) {
|
|
1146
|
+
const m = String(e.message || e);
|
|
1147
|
+
throw new CliError(
|
|
1148
|
+
`${name} on ${target}: ${m}${/IFH-0003/.test(m) ? `
|
|
1149
|
+
${FTDI_HINT}` : ""}`
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
if (opts.json) return [JSON.stringify(d, null, 2)];
|
|
1153
|
+
return formatAnswer(d);
|
|
1154
|
+
}
|
|
1155
|
+
function formatAnswer(d) {
|
|
1156
|
+
const sys = d.system || {};
|
|
1157
|
+
const out = [
|
|
1158
|
+
`${String(sys.OBJECT || "")} ${String(sys.VARIANTE || "")} ${String(sys.JOBNAME || d.job || "")}: ${(d.sets || []).length} set${(d.sets || []).length === 1 ? "" : "s"}`
|
|
1159
|
+
];
|
|
1160
|
+
(d.sets || []).forEach((set, i) => {
|
|
1161
|
+
out.push("", `set ${i + 1}`);
|
|
1162
|
+
const rows = Object.entries(set).filter(([k]) => !k.startsWith("_")).map(([k, v]) => [k, valueText(v)]);
|
|
1163
|
+
out.push(...formatTable(rows, void 0, " "));
|
|
1164
|
+
});
|
|
1165
|
+
return out;
|
|
1166
|
+
}
|
|
1167
|
+
function valueText(v) {
|
|
1168
|
+
if (v == null) return "";
|
|
1169
|
+
if (Array.isArray(v))
|
|
1170
|
+
return v.map(
|
|
1171
|
+
(b) => (Number(b) & 255).toString(16).toUpperCase().padStart(2, "0")
|
|
1172
|
+
).join("-");
|
|
1173
|
+
if (typeof v === "object") return JSON.stringify(v);
|
|
1174
|
+
return String(v);
|
|
1175
|
+
}
|
|
1176
|
+
async function openModule(chassis, sgbd, api = runtimeGlobals().api) {
|
|
1177
|
+
const cid = chassis.toUpperCase();
|
|
1178
|
+
const want = sgbd.toLowerCase();
|
|
1179
|
+
let ecu;
|
|
1180
|
+
if (want === cid.toLowerCase()) {
|
|
1181
|
+
ecu = {
|
|
1182
|
+
code: cid,
|
|
1183
|
+
sgbd: want,
|
|
1184
|
+
label: `INPA ${cid} script`,
|
|
1185
|
+
group: null,
|
|
1186
|
+
kind: "vehicle",
|
|
1187
|
+
chassis: cid
|
|
1188
|
+
};
|
|
1189
|
+
} else {
|
|
1190
|
+
let config2;
|
|
1191
|
+
try {
|
|
1192
|
+
config2 = await api(`/api/chassis/${cid}`);
|
|
1193
|
+
} catch (e) {
|
|
1194
|
+
throw new CliError(
|
|
1195
|
+
`no chassis ${cid} on the site (${e.message})`
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
const rows = [];
|
|
1199
|
+
for (const s of config2.sections || []) rows.push(...s.ecus || []);
|
|
1200
|
+
const row = rows.find((r) => String(r.sgbd).toLowerCase() === want) || rows.find((r) => String(r.code || "").toLowerCase() === want) || rows.find(
|
|
1201
|
+
(r) => (r.variants || []).some((v) => String(v).toLowerCase() === want)
|
|
1202
|
+
);
|
|
1203
|
+
if (!row)
|
|
1204
|
+
throw new CliError(
|
|
1205
|
+
`${cid} carries no module ${sgbd} (try: ${rows.slice(0, 12).map((r) => r.sgbd).join(", ")}${rows.length > 12 ? ", ..." : ""})`
|
|
1206
|
+
);
|
|
1207
|
+
ecu = {
|
|
1208
|
+
code: row.code || "",
|
|
1209
|
+
label: row.label || row.sgbd,
|
|
1210
|
+
sgbd: String(row.sgbd).toLowerCase(),
|
|
1211
|
+
group: row.group || null,
|
|
1212
|
+
chassis: cid
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
return prepareModule(ecu, api);
|
|
1216
|
+
}
|
|
1217
|
+
async function prepareModule(ecu, api = runtimeGlobals().api) {
|
|
1218
|
+
const R = loadRuntime();
|
|
1219
|
+
try {
|
|
1220
|
+
const idx = await api("/api/ecu-index.json");
|
|
1221
|
+
ecu._ipoKnownSgbds = new Set(
|
|
1222
|
+
Object.keys(idx || {}).map((k) => k.toLowerCase())
|
|
1223
|
+
);
|
|
1224
|
+
} catch {
|
|
1225
|
+
ecu._ipoKnownSgbds = /* @__PURE__ */ new Set();
|
|
1226
|
+
}
|
|
1227
|
+
await resolveGroupVariant(ecu, api, R);
|
|
1228
|
+
const codeHint = ecu.code ? `?code=${encodeURIComponent(ecu.code)}` : "";
|
|
1229
|
+
ecu._ir = await api(`/api/ecu/${ecu.sgbd}/ir${codeHint}`).catch(() => null);
|
|
1230
|
+
const menusOf = (ir) => Object.keys((ir || {}).menus || {}).length;
|
|
1231
|
+
if (!menusOf(ecu._ir) && ecu._sgbdBase && ecu._sgbdBase !== ecu.sgbd) {
|
|
1232
|
+
const base = await api(`/api/ecu/${ecu._sgbdBase}/ir${codeHint}`).catch(
|
|
1233
|
+
() => null
|
|
1234
|
+
);
|
|
1235
|
+
if (menusOf(base)) {
|
|
1236
|
+
ecu._ir = base;
|
|
1237
|
+
ecu._irFrom = ecu._sgbdBase;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
const script = String(ecu._irFrom || ecu.sgbd).toLowerCase();
|
|
1241
|
+
const exec = await R.irLiveExec(script);
|
|
1242
|
+
if (!exec || !exec.procs || !Object.keys(exec.procs).length)
|
|
1243
|
+
throw new CliError(
|
|
1244
|
+
`${script}: the site ships no runnable script for this module`
|
|
1245
|
+
);
|
|
1246
|
+
if (!(exec.procs.inpainit || exec.procs.SgbdInpaCheck))
|
|
1247
|
+
throw new CliError(
|
|
1248
|
+
`${script}: the script has no INPA entry point (inpainit)`
|
|
1249
|
+
);
|
|
1250
|
+
return { ecu, exec };
|
|
1251
|
+
}
|
|
1252
|
+
async function resolveGroupVariant(ecu, api, R) {
|
|
1253
|
+
const g = String(ecu.group || "").toLowerCase();
|
|
1254
|
+
ecu._variantSource = null;
|
|
1255
|
+
if (!g) {
|
|
1256
|
+
ecu._variantSource = "ungrouped";
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
let idx = null;
|
|
1260
|
+
try {
|
|
1261
|
+
idx = await api("/data/groups/index.json");
|
|
1262
|
+
} catch {
|
|
1263
|
+
idx = null;
|
|
1264
|
+
}
|
|
1265
|
+
if (!idx || !(idx.groups || []).includes(g)) {
|
|
1266
|
+
ecu._variantSource = "nogroup";
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
let v = null;
|
|
1270
|
+
try {
|
|
1271
|
+
v = await R.webResolveVariant(g);
|
|
1272
|
+
} catch {
|
|
1273
|
+
v = null;
|
|
1274
|
+
}
|
|
1275
|
+
if (!v) {
|
|
1276
|
+
ecu._variantSource = "unverified";
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
ecu._variant = v.toUpperCase();
|
|
1280
|
+
ecu._variantSource = "confirmed";
|
|
1281
|
+
if (v === String(ecu.sgbd).toLowerCase()) return;
|
|
1282
|
+
try {
|
|
1283
|
+
const jobs = await api(`/api/ecu/${v}/jobs`);
|
|
1284
|
+
if (!Array.isArray(jobs) || !jobs.length) return;
|
|
1285
|
+
} catch {
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
ecu._sgbdBase = ecu._sgbdBase || ecu.sgbd;
|
|
1289
|
+
ecu.sgbd = v;
|
|
1290
|
+
ecu._variantSource = "identified";
|
|
1291
|
+
}
|
|
1292
|
+
var SCAN_CHASSIS = [
|
|
1293
|
+
"E46",
|
|
1294
|
+
"E53",
|
|
1295
|
+
"E65",
|
|
1296
|
+
"E83",
|
|
1297
|
+
"E85",
|
|
1298
|
+
"E87",
|
|
1299
|
+
"E89",
|
|
1300
|
+
"E90",
|
|
1301
|
+
"R50",
|
|
1302
|
+
"R56"
|
|
1303
|
+
];
|
|
1304
|
+
|
|
1305
|
+
// src/faults.ts
|
|
1306
|
+
function hexDigits(v) {
|
|
1307
|
+
if (v == null || v === "") return "";
|
|
1308
|
+
const text = Array.isArray(v) ? v.map((b) => (Number(b) & 255).toString(16).padStart(2, "0")).join("") : String(v);
|
|
1309
|
+
return text.trim().toUpperCase().replace(/^0X/, "").replace(/[^0-9A-F]/g, "").slice(0, 4);
|
|
1310
|
+
}
|
|
1311
|
+
function leadingCode(text) {
|
|
1312
|
+
const m = /^([0-9A-F]{3,5})\b/i.exec(String(text || "").trim());
|
|
1313
|
+
return m ? m[1].toUpperCase() : "";
|
|
1314
|
+
}
|
|
1315
|
+
function faultCode(c) {
|
|
1316
|
+
const hex = hexDigits(c.F_HEX_CODE);
|
|
1317
|
+
if (hex) return hex;
|
|
1318
|
+
const lead = leadingCode(c.F_ORT_TEXT);
|
|
1319
|
+
if (lead) return lead;
|
|
1320
|
+
const nr = c.F_ORT_NR;
|
|
1321
|
+
if (nr != null && String(nr).trim() !== "") return String(nr).trim();
|
|
1322
|
+
return "";
|
|
1323
|
+
}
|
|
1324
|
+
function faultText(c, code) {
|
|
1325
|
+
const text = String(c.F_ORT_TEXT || "").trim();
|
|
1326
|
+
const lead = leadingCode(text);
|
|
1327
|
+
if (lead && lead === code) return text.slice(lead.length).trim();
|
|
1328
|
+
return text;
|
|
1329
|
+
}
|
|
1330
|
+
function faultCount(c) {
|
|
1331
|
+
const n = c.F_HFK ?? c.F_LZ;
|
|
1332
|
+
return n == null || n === "" ? "" : String(n);
|
|
1333
|
+
}
|
|
1334
|
+
function faultState(c) {
|
|
1335
|
+
const vt = String(c.F_VORHANDEN_TEXT || "").toLowerCase();
|
|
1336
|
+
if (!vt) return "";
|
|
1337
|
+
const says = /momentan vorhanden|currently present/.test(vt);
|
|
1338
|
+
const denies = /nicht vorhanden|not (currently )?present/.test(vt);
|
|
1339
|
+
return says && !denies ? "present" : "stored";
|
|
1340
|
+
}
|
|
1341
|
+
function faultRow(c) {
|
|
1342
|
+
const code = faultCode(c);
|
|
1343
|
+
return {
|
|
1344
|
+
code,
|
|
1345
|
+
text: faultText(c, code),
|
|
1346
|
+
count: faultCount(c),
|
|
1347
|
+
state: faultState(c)
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// src/report.ts
|
|
1352
|
+
function payloadOf(arg) {
|
|
1353
|
+
const s = String(arg || "").trim();
|
|
1354
|
+
const at = s.indexOf("#report/");
|
|
1355
|
+
if (at >= 0) return s.slice(at + "#report/".length);
|
|
1356
|
+
if (s.startsWith("report/")) return s.slice("report/".length);
|
|
1357
|
+
if (/^https?:\/\//i.test(s))
|
|
1358
|
+
throw new CliError("that link carries no #report/ fragment");
|
|
1359
|
+
return s;
|
|
1360
|
+
}
|
|
1361
|
+
async function decodeReport(arg) {
|
|
1362
|
+
const R = loadRuntime();
|
|
1363
|
+
const payload = payloadOf(arg);
|
|
1364
|
+
if (!payload) throw new CliError("no report payload given");
|
|
1365
|
+
const p = await R.garageShareDecode(payload);
|
|
1366
|
+
if (!p)
|
|
1367
|
+
throw new CliError(
|
|
1368
|
+
"this link does not carry a readable report (it may have been cut short when pasted)"
|
|
1369
|
+
);
|
|
1370
|
+
return p;
|
|
1371
|
+
}
|
|
1372
|
+
function scanOf(p) {
|
|
1373
|
+
const R = loadRuntime();
|
|
1374
|
+
return {
|
|
1375
|
+
kind: p.kind,
|
|
1376
|
+
at: p.at,
|
|
1377
|
+
chassis: p.chassis,
|
|
1378
|
+
report: p.report,
|
|
1379
|
+
summary: p.summary || R.garageScanSummary(p.report)
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
function moduleName(m) {
|
|
1383
|
+
return m.label || String(m.sgbd || "").toUpperCase();
|
|
1384
|
+
}
|
|
1385
|
+
function identRows(ident) {
|
|
1386
|
+
const R = loadRuntime();
|
|
1387
|
+
const rows = [];
|
|
1388
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1389
|
+
const val = (k) => ident[k] == null ? "" : String(ident[k]).trim();
|
|
1390
|
+
for (const [keys, cap] of R.IPO_IDENT_ROWS || []) {
|
|
1391
|
+
for (const k of keys) {
|
|
1392
|
+
seen.add(k);
|
|
1393
|
+
const v = val(k);
|
|
1394
|
+
if (v) rows.push([keys.length > 1 ? `${cap} (${k})` : cap, k, v]);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
for (const k of Object.keys(ident)) {
|
|
1398
|
+
if (seen.has(k) || k.startsWith("_")) continue;
|
|
1399
|
+
const v = val(k);
|
|
1400
|
+
if (v) rows.push([k, k, v]);
|
|
1401
|
+
}
|
|
1402
|
+
return rows;
|
|
1403
|
+
}
|
|
1404
|
+
function headerLines(p) {
|
|
1405
|
+
const R = loadRuntime();
|
|
1406
|
+
const sum = p.summary || R.garageScanSummary(p.report);
|
|
1407
|
+
const who = [p.label, p.chassis].filter(Boolean).join(" / ");
|
|
1408
|
+
const kind = p.kind === "ident" ? "identification" : "fault memories";
|
|
1409
|
+
return formatTable([
|
|
1410
|
+
["Report", `${kind}${who ? ` of ${who}` : ""}`],
|
|
1411
|
+
["Read", p.at || ""],
|
|
1412
|
+
[
|
|
1413
|
+
"Modules",
|
|
1414
|
+
`${sum.modules} read, ${sum.withFaults} with faults, ${sum.faults} fault${sum.faults === 1 ? "" : "s"}, ${sum.silent} silent`
|
|
1415
|
+
]
|
|
1416
|
+
]);
|
|
1417
|
+
}
|
|
1418
|
+
function faultLines(report) {
|
|
1419
|
+
const rows = [];
|
|
1420
|
+
for (const m of report.modules || []) {
|
|
1421
|
+
const codes = m.codes || [];
|
|
1422
|
+
if (!codes.length) {
|
|
1423
|
+
rows.push([moduleName(m), "", "no faults stored"]);
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
for (const c of codes) {
|
|
1427
|
+
const r = faultRow(c);
|
|
1428
|
+
rows.push([moduleName(m), r.code, r.text, r.count, r.state]);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
if (!rows.length) return ["no modules in this report"];
|
|
1432
|
+
return formatTable(rows, ["MODULE", "CODE", "TEXT", "COUNT", "STATE"]);
|
|
1433
|
+
}
|
|
1434
|
+
function identLines(report) {
|
|
1435
|
+
const rows = [];
|
|
1436
|
+
for (const m of report.modules || []) {
|
|
1437
|
+
const ident = m.ident;
|
|
1438
|
+
if (!ident || !Object.keys(ident).length) {
|
|
1439
|
+
rows.push([moduleName(m), "", "(no identification)"]);
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
for (const [cap, , v] of identRows(ident))
|
|
1443
|
+
rows.push([moduleName(m), cap, v]);
|
|
1444
|
+
}
|
|
1445
|
+
if (!rows.length) return ["no modules in this report"];
|
|
1446
|
+
return formatTable(rows, ["MODULE", "FIELD", "VALUE"]);
|
|
1447
|
+
}
|
|
1448
|
+
function silentLines(report) {
|
|
1449
|
+
const silent = report.silent || [];
|
|
1450
|
+
if (!silent.length) return [];
|
|
1451
|
+
return [
|
|
1452
|
+
"",
|
|
1453
|
+
`Silent (${silent.length}):`,
|
|
1454
|
+
...formatTable(
|
|
1455
|
+
silent.map((s) => [
|
|
1456
|
+
String(s.target || "").toUpperCase(),
|
|
1457
|
+
s.label || "",
|
|
1458
|
+
s.error || ""
|
|
1459
|
+
]),
|
|
1460
|
+
void 0,
|
|
1461
|
+
" "
|
|
1462
|
+
)
|
|
1463
|
+
];
|
|
1464
|
+
}
|
|
1465
|
+
async function reportShow(arg, json) {
|
|
1466
|
+
const p = await decodeReport(arg);
|
|
1467
|
+
if (json) return [JSON.stringify(p, null, 2)];
|
|
1468
|
+
return [...headerLines(p), "", ...reportBodyLines(p.report, p.kind)];
|
|
1469
|
+
}
|
|
1470
|
+
function reportBodyLines(report, kind) {
|
|
1471
|
+
const body = kind === "ident" ? identLines(report) : faultLines(report);
|
|
1472
|
+
return [...body, ...silentLines(report)];
|
|
1473
|
+
}
|
|
1474
|
+
function diffFaultRow(sign, c) {
|
|
1475
|
+
const r = faultRow(c);
|
|
1476
|
+
return [sign, r.code, r.text, r.count];
|
|
1477
|
+
}
|
|
1478
|
+
function diffJson(a, b, diff) {
|
|
1479
|
+
const R = loadRuntime();
|
|
1480
|
+
const strip = (p) => ({
|
|
1481
|
+
kind: p.kind,
|
|
1482
|
+
at: p.at,
|
|
1483
|
+
chassis: p.chassis,
|
|
1484
|
+
label: p.label
|
|
1485
|
+
});
|
|
1486
|
+
return {
|
|
1487
|
+
from: strip(a),
|
|
1488
|
+
to: strip(b),
|
|
1489
|
+
kind: diff.kind,
|
|
1490
|
+
counts: R.garageDiffCounts(diff),
|
|
1491
|
+
modules: diff.modules,
|
|
1492
|
+
silence: diff.silence
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
async function reportDiff(argA, argB, json) {
|
|
1496
|
+
const R = loadRuntime();
|
|
1497
|
+
const a = await decodeReport(argA);
|
|
1498
|
+
const b = await decodeReport(argB);
|
|
1499
|
+
const diff = R.garageDiffScans(scanOf(a), scanOf(b));
|
|
1500
|
+
if (json) return [JSON.stringify(diffJson(a, b, diff), null, 2)];
|
|
1501
|
+
const counts = R.garageDiffCounts(diff);
|
|
1502
|
+
const out = formatTable([
|
|
1503
|
+
["From", `${a.at || ""}${a.label ? ` ${a.label}` : ""}`],
|
|
1504
|
+
["To", `${b.at || ""}${b.label ? ` ${b.label}` : ""}`],
|
|
1505
|
+
[
|
|
1506
|
+
"Changes",
|
|
1507
|
+
`${counts.added} new, ${counts.cleared} cleared, ${counts.same} still present` + (diff.kind !== "faults" ? `, ${counts.fields} ident fields changed` : "") + `, ${counts.modules} module${counts.modules === 1 ? "" : "s"} changed`
|
|
1508
|
+
]
|
|
1509
|
+
]);
|
|
1510
|
+
for (const m of diff.modules) {
|
|
1511
|
+
out.push("");
|
|
1512
|
+
if (m.unread) {
|
|
1513
|
+
out.push(`${m.label} (not read in the newer report)`);
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
if (!m.changed) {
|
|
1517
|
+
out.push(
|
|
1518
|
+
`${m.label} unchanged` + (m.same.length ? ` (${m.same.length} still present)` : "")
|
|
1519
|
+
);
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
out.push(m.label);
|
|
1523
|
+
const rows = [];
|
|
1524
|
+
for (const c of m.added) rows.push(diffFaultRow("+", c));
|
|
1525
|
+
for (const c of m.cleared) rows.push(diffFaultRow("-", c));
|
|
1526
|
+
for (const c of m.same) rows.push(diffFaultRow("=", c));
|
|
1527
|
+
for (const f of m.fields)
|
|
1528
|
+
rows.push(["~", f.label, `${f.from || "(none)"} -> ${f.to || "(none)"}`]);
|
|
1529
|
+
out.push(...formatTable(rows, void 0, " "));
|
|
1530
|
+
}
|
|
1531
|
+
if (diff.silence.length) {
|
|
1532
|
+
out.push("", "Answering changed:");
|
|
1533
|
+
out.push(
|
|
1534
|
+
...formatTable(
|
|
1535
|
+
diff.silence.map((s) => [
|
|
1536
|
+
s.state === "silent" ? "silent now" : "answering now",
|
|
1537
|
+
s.label,
|
|
1538
|
+
s.error || ""
|
|
1539
|
+
]),
|
|
1540
|
+
void 0,
|
|
1541
|
+
" "
|
|
1542
|
+
)
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
out.push("", "+ new - cleared = still present ~ ident field changed");
|
|
1546
|
+
return out;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// src/scan.ts
|
|
1550
|
+
var VEHICLE_SCRIPTS = {
|
|
1551
|
+
E46: "e46",
|
|
1552
|
+
E53: "e53",
|
|
1553
|
+
E65: "e65",
|
|
1554
|
+
E83: "e83",
|
|
1555
|
+
E85: "e85",
|
|
1556
|
+
E87: "e87",
|
|
1557
|
+
E89: "e89_d_all",
|
|
1558
|
+
E90: "e90",
|
|
1559
|
+
R50: "r50",
|
|
1560
|
+
R56: "r56_all"
|
|
1561
|
+
};
|
|
1562
|
+
var FAULT_KEY_RE = /^(FS lesen|Fehlerspeicher lesen|Read (fault|error) memory)$/i;
|
|
1563
|
+
var FAULT_MENU = "m_fs";
|
|
1564
|
+
function headlessUi(progress) {
|
|
1565
|
+
let lastBox = "";
|
|
1566
|
+
return {
|
|
1567
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
1568
|
+
loadExec: async (sgbd) => loadRuntime().irLiveExec(sgbd),
|
|
1569
|
+
route: () => {
|
|
1570
|
+
},
|
|
1571
|
+
status: () => {
|
|
1572
|
+
},
|
|
1573
|
+
error: (_p, text) => progress(`error: ${text}`),
|
|
1574
|
+
message: async (title, body) => {
|
|
1575
|
+
progress(`${title}${body ? `: ${body}` : ""}`);
|
|
1576
|
+
},
|
|
1577
|
+
// a prompt cannot be answered headless: the key is abandoned
|
|
1578
|
+
askInput: async () => null,
|
|
1579
|
+
// a scan reads; a key that would write is declined, never sent
|
|
1580
|
+
confirmKey: async () => false,
|
|
1581
|
+
confirmWrite: async () => false,
|
|
1582
|
+
pickComponent: async () => null,
|
|
1583
|
+
pickLines: async () => null,
|
|
1584
|
+
saveFile: async () => null,
|
|
1585
|
+
writeFile: async () => {
|
|
1586
|
+
},
|
|
1587
|
+
printScreen: () => {
|
|
1588
|
+
},
|
|
1589
|
+
resolveScriptEcu: (from, script, exec) => loadRuntime().ipoResolveScriptEcu(from, script, exec),
|
|
1590
|
+
machineTick: async () => "tick",
|
|
1591
|
+
userbox: (_p, box) => {
|
|
1592
|
+
if (!box) return;
|
|
1593
|
+
const lines = box.lines || [];
|
|
1594
|
+
const text = lines.length ? String(lines[lines.length - 1]) : "";
|
|
1595
|
+
if (text && text !== lastBox) {
|
|
1596
|
+
lastBox = text;
|
|
1597
|
+
progress(text.trim());
|
|
1598
|
+
}
|
|
1599
|
+
},
|
|
1600
|
+
renderKeys: () => {
|
|
1601
|
+
},
|
|
1602
|
+
paint: () => {
|
|
1603
|
+
},
|
|
1604
|
+
left: () => {
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
async function scanCommand(chassis, opts = {}) {
|
|
1609
|
+
const R = loadRuntime();
|
|
1610
|
+
const cid = chassis.toUpperCase();
|
|
1611
|
+
if (opts.apiFn) setApiImpl(opts.apiFn);
|
|
1612
|
+
try {
|
|
1613
|
+
return await scanRun(R, cid, opts);
|
|
1614
|
+
} finally {
|
|
1615
|
+
if (opts.apiFn) setApiImpl(null);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
async function scanRun(R, cid, opts) {
|
|
1619
|
+
const progress = opts.progress || ((l) => process.stderr.write(`${l}
|
|
1620
|
+
`));
|
|
1621
|
+
const api = opts.apiFn || runtimeGlobals().api;
|
|
1622
|
+
let ecu;
|
|
1623
|
+
let exec;
|
|
1624
|
+
if (opts.exec) {
|
|
1625
|
+
ecu = opts.ecu || {
|
|
1626
|
+
code: cid,
|
|
1627
|
+
sgbd: cid.toLowerCase(),
|
|
1628
|
+
label: `INPA ${cid} script`,
|
|
1629
|
+
group: null,
|
|
1630
|
+
kind: "vehicle",
|
|
1631
|
+
chassis: cid
|
|
1632
|
+
};
|
|
1633
|
+
exec = opts.exec;
|
|
1634
|
+
} else {
|
|
1635
|
+
const script = VEHICLE_SCRIPTS[cid];
|
|
1636
|
+
if (!script)
|
|
1637
|
+
throw new CliError(
|
|
1638
|
+
`no whole-vehicle script for ${cid} (one ships for ${Object.keys(VEHICLE_SCRIPTS).join(" ")})`
|
|
1639
|
+
);
|
|
1640
|
+
({ ecu, exec } = await openModule(cid, script, api));
|
|
1641
|
+
ecu.kind = "vehicle";
|
|
1642
|
+
}
|
|
1643
|
+
const ui = headlessUi(progress);
|
|
1644
|
+
const p = new R.IpoProgram(ecu, exec, ui);
|
|
1645
|
+
const r = await p.start();
|
|
1646
|
+
if (p.noCable) throw new CliError("no cable connected");
|
|
1647
|
+
if (!r.ok) {
|
|
1648
|
+
const last = p.messages[p.messages.length - 1];
|
|
1649
|
+
throw new CliError(
|
|
1650
|
+
`${cid}: the script did not start (${r.reason || "stopped"})` + (last ? `: ${last.title}${last.body ? ` ${last.body}` : ""}` : "")
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
if (exec.procs[FAULT_MENU] && p.menu !== FAULT_MENU)
|
|
1654
|
+
await p.openMenu(FAULT_MENU);
|
|
1655
|
+
const key = p.items.find(
|
|
1656
|
+
(it) => FAULT_KEY_RE.test(String(it.label || it.legendLabel || "").trim())
|
|
1657
|
+
);
|
|
1658
|
+
if (!key) {
|
|
1659
|
+
const have = p.items.map((it) => `F${it.nr} ${it.label}`).join(", ");
|
|
1660
|
+
p.close();
|
|
1661
|
+
throw new CliError(
|
|
1662
|
+
`${cid}: no fault-memory read key on ${p.menu} (keys: ${have})`
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
progress(`${cid}: ${key.label} (F${key.nr})`);
|
|
1666
|
+
await p.press(key.nr);
|
|
1667
|
+
const report = p.view && p.view.report || R.ipoProtocolReport(p.wireReads, p.view && p.view.lines || []);
|
|
1668
|
+
await p.leaveModule();
|
|
1669
|
+
const summary = R.garageScanSummary(report);
|
|
1670
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1671
|
+
let link;
|
|
1672
|
+
if (opts.share) {
|
|
1673
|
+
const payload = await R.garageShareEncode(
|
|
1674
|
+
{ kind: report.kind, at, chassis: cid, report, summary },
|
|
1675
|
+
{ label: opts.label || "", chassis: cid }
|
|
1676
|
+
);
|
|
1677
|
+
link = `https://bmweb.danner.ink/#report/${payload}`;
|
|
1678
|
+
}
|
|
1679
|
+
if (opts.json) {
|
|
1680
|
+
return {
|
|
1681
|
+
report,
|
|
1682
|
+
link,
|
|
1683
|
+
lines: [
|
|
1684
|
+
JSON.stringify({ chassis: cid, at, summary, report, link }, null, 2)
|
|
1685
|
+
]
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
const lines = formatTable([
|
|
1689
|
+
["Scan", `${cid} fault memories (${ecu.sgbd}.ipo)`],
|
|
1690
|
+
["Read", at],
|
|
1691
|
+
[
|
|
1692
|
+
"Modules",
|
|
1693
|
+
`${summary.modules} answered, ${summary.withFaults} with faults, ${summary.faults} fault${summary.faults === 1 ? "" : "s"}, ${summary.silent} silent`
|
|
1694
|
+
]
|
|
1695
|
+
]);
|
|
1696
|
+
lines.push("", ...reportBodyLines(report, report.kind));
|
|
1697
|
+
if (link) lines.push("", `Share: ${link}`);
|
|
1698
|
+
return { report, lines, link };
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// src/search.ts
|
|
1702
|
+
import {
|
|
1703
|
+
existsSync as existsSync2,
|
|
1704
|
+
mkdirSync as mkdirSync2,
|
|
1705
|
+
readFileSync as readFileSync4,
|
|
1706
|
+
statSync as statSync3,
|
|
1707
|
+
writeFileSync as writeFileSync3
|
|
1708
|
+
} from "node:fs";
|
|
1709
|
+
import { gunzipSync } from "node:zlib";
|
|
1710
|
+
var SITE = "https://bmweb.danner.ink/";
|
|
1711
|
+
var INDEX_URL = `${SITE}api/search-index.json.gz`;
|
|
1712
|
+
var INDEX_MAX_AGE_MS = CACHE_MAX_AGE_MS;
|
|
1713
|
+
function parseIndex(gz) {
|
|
1714
|
+
const R = loadRuntime();
|
|
1715
|
+
let doc;
|
|
1716
|
+
try {
|
|
1717
|
+
doc = JSON.parse(gunzipSync(gz).toString("utf8"));
|
|
1718
|
+
} catch {
|
|
1719
|
+
throw new CliError("the search index is not readable (not gzipped JSON)");
|
|
1720
|
+
}
|
|
1721
|
+
if (!doc || doc.v !== R.SEARCH_INDEX_VERSION)
|
|
1722
|
+
throw new CliError(
|
|
1723
|
+
`the search index is format v${doc && doc.v}; this bmweb-cli reads v${R.SEARCH_INDEX_VERSION} (update the package)`
|
|
1724
|
+
);
|
|
1725
|
+
return doc;
|
|
1726
|
+
}
|
|
1727
|
+
async function loadIndex(opts = {}) {
|
|
1728
|
+
const env = opts.env || process.env;
|
|
1729
|
+
const file = indexCachePath(env);
|
|
1730
|
+
const warn = opts.warn || ((line) => console.error(line));
|
|
1731
|
+
const have = existsSync2(file);
|
|
1732
|
+
const fresh = have && Date.now() - statSync3(file).mtimeMs < INDEX_MAX_AGE_MS;
|
|
1733
|
+
if (have && fresh && !opts.refresh) return parseIndex(readFileSync4(file));
|
|
1734
|
+
const doFetch = opts.fetchImpl || fetch;
|
|
1735
|
+
let bytes = null;
|
|
1736
|
+
let why = "";
|
|
1737
|
+
try {
|
|
1738
|
+
const r = await doFetch(INDEX_URL);
|
|
1739
|
+
if (r.ok) bytes = new Uint8Array(await r.arrayBuffer());
|
|
1740
|
+
else why = `HTTP ${r.status}`;
|
|
1741
|
+
} catch (e) {
|
|
1742
|
+
why = String(e.message || e);
|
|
1743
|
+
}
|
|
1744
|
+
if (bytes) {
|
|
1745
|
+
const index = parseIndex(bytes);
|
|
1746
|
+
mkdirSync2(cacheDir(env), { recursive: true });
|
|
1747
|
+
writeFileSync3(file, bytes);
|
|
1748
|
+
return index;
|
|
1749
|
+
}
|
|
1750
|
+
if (have) {
|
|
1751
|
+
warn(
|
|
1752
|
+
`bmweb: could not refresh the search index (${why}); using the cached copy`
|
|
1753
|
+
);
|
|
1754
|
+
return parseIndex(readFileSync4(file));
|
|
1755
|
+
}
|
|
1756
|
+
throw new CliError(
|
|
1757
|
+
`could not fetch the search index from ${INDEX_URL} (${why})`
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
function hitLink(hit, chassis) {
|
|
1761
|
+
const R = loadRuntime();
|
|
1762
|
+
const route = R.searchHitRoute(hit, chassis);
|
|
1763
|
+
return route ? `${SITE}#${route}` : null;
|
|
1764
|
+
}
|
|
1765
|
+
function searchRows(result) {
|
|
1766
|
+
const rows = [];
|
|
1767
|
+
for (const g of result.groups)
|
|
1768
|
+
for (const m of g.modules)
|
|
1769
|
+
for (const h of m.hits) {
|
|
1770
|
+
const e = h.entry;
|
|
1771
|
+
rows.push({
|
|
1772
|
+
chassis: g.chassis,
|
|
1773
|
+
sgbd: m.module.sgbd,
|
|
1774
|
+
module: m.module.label,
|
|
1775
|
+
code: m.module.code,
|
|
1776
|
+
kind: e.t === "k" ? "key" : "screen",
|
|
1777
|
+
key: e.t === "k" && e.n != null ? keyText(e.n) : null,
|
|
1778
|
+
menu: e.t === "k" ? e.m || null : null,
|
|
1779
|
+
screen: e.s || null,
|
|
1780
|
+
label: h.label,
|
|
1781
|
+
title: h.sub,
|
|
1782
|
+
jobs: h.jobs,
|
|
1783
|
+
writes: !!e.w,
|
|
1784
|
+
link: hitLink(h, g.chassis),
|
|
1785
|
+
score: h.score
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
return rows;
|
|
1789
|
+
}
|
|
1790
|
+
function keyText(n) {
|
|
1791
|
+
return n > 10 ? `Shift+F${n - 10}` : `F${n}`;
|
|
1792
|
+
}
|
|
1793
|
+
function runSearch(query, opts) {
|
|
1794
|
+
const R = loadRuntime();
|
|
1795
|
+
if (!R.searchTerms(query).length)
|
|
1796
|
+
throw new CliError("the query needs at least two characters");
|
|
1797
|
+
const result = R.searchRun(opts.index, query, {
|
|
1798
|
+
chassis: opts.chassis,
|
|
1799
|
+
max: opts.limit
|
|
1800
|
+
});
|
|
1801
|
+
const rows = searchRows(result);
|
|
1802
|
+
if (opts.json)
|
|
1803
|
+
return [
|
|
1804
|
+
JSON.stringify(
|
|
1805
|
+
{ query, total: result.total, shown: result.shown, rows },
|
|
1806
|
+
null,
|
|
1807
|
+
2
|
|
1808
|
+
)
|
|
1809
|
+
];
|
|
1810
|
+
if (!rows.length) return [`no results for "${query}"`];
|
|
1811
|
+
const out = [];
|
|
1812
|
+
for (const g of result.groups) {
|
|
1813
|
+
out.push(
|
|
1814
|
+
g.chassis || "(modules no car carries; open them by name in the app)"
|
|
1815
|
+
);
|
|
1816
|
+
for (const m of g.modules) {
|
|
1817
|
+
const head = [
|
|
1818
|
+
m.module.sgbd,
|
|
1819
|
+
m.module.label,
|
|
1820
|
+
m.module.code ? `(${m.module.code})` : ""
|
|
1821
|
+
].filter(Boolean).join(" ");
|
|
1822
|
+
out.push(` ${head}`);
|
|
1823
|
+
const table = m.hits.map((h) => {
|
|
1824
|
+
const e = h.entry;
|
|
1825
|
+
const what = e.t === "k" && e.n != null ? keyText(e.n) : "screen";
|
|
1826
|
+
const label = h.label + (h.sub ? ` (${h.sub})` : "");
|
|
1827
|
+
const jobs = h.jobs.join(", ") + (e.w ? " [WRITE]" : "");
|
|
1828
|
+
const link = hitLink(h, g.chassis) || "(not openable)";
|
|
1829
|
+
return [what, label, jobs, link];
|
|
1830
|
+
});
|
|
1831
|
+
out.push(...formatTable(table, void 0, " "));
|
|
1832
|
+
}
|
|
1833
|
+
out.push("");
|
|
1834
|
+
}
|
|
1835
|
+
out.push(
|
|
1836
|
+
result.shown < result.total ? `${result.shown} of ${result.total} results shown (raise --limit for more)` : `${result.total} result${result.total === 1 ? "" : "s"}`
|
|
1837
|
+
);
|
|
1838
|
+
return out;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
// src/tui.ts
|
|
1842
|
+
import { writeFileSync as writeFileSync4 } from "node:fs";
|
|
1843
|
+
import { createInterface as createInterface2, emitKeypressEvents } from "node:readline";
|
|
1844
|
+
function terminalHomeHost(api, status) {
|
|
1845
|
+
return {
|
|
1846
|
+
chassis: async () => {
|
|
1847
|
+
const ids = await api("/api/chassis");
|
|
1848
|
+
return (ids || []).map((id) => ({ value: id, label: id }));
|
|
1849
|
+
},
|
|
1850
|
+
modules: async (chassis) => {
|
|
1851
|
+
const ch = await api(`/api/chassis/${encodeURIComponent(chassis)}`);
|
|
1852
|
+
const out = [];
|
|
1853
|
+
for (const sec of ch && ch.sections || [])
|
|
1854
|
+
for (const ecu of sec.ecus || [])
|
|
1855
|
+
if (ecu && ecu.sgbd)
|
|
1856
|
+
out.push({
|
|
1857
|
+
value: String(ecu.sgbd).toLowerCase(),
|
|
1858
|
+
label: ecu.label || ecu.code || ecu.sgbd,
|
|
1859
|
+
meta: `${sec.name || ""} ${ecu.sgbd}`
|
|
1860
|
+
});
|
|
1861
|
+
return out;
|
|
1862
|
+
},
|
|
1863
|
+
vehicle: async (chassis) => VEHICLE_SCRIPTS[String(chassis).toUpperCase()] || "",
|
|
1864
|
+
status
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
var SHIFTED_DIGITS = "!@#$%^&*()";
|
|
1868
|
+
var DIGITS = "1234567890";
|
|
1869
|
+
var GAUGE_WIDTH = 12;
|
|
1870
|
+
function keyToPress(k) {
|
|
1871
|
+
if (k.ctrl && k.name === "c") return "quit";
|
|
1872
|
+
if (k.name === "escape") return "back";
|
|
1873
|
+
if (k.ch === "q" || k.ch === "Q") return "quit";
|
|
1874
|
+
const f = /^f(\d{1,2})$/.exec(k.name);
|
|
1875
|
+
if (f) {
|
|
1876
|
+
const n = Number(f[1]);
|
|
1877
|
+
if (n >= 1 && n <= 10) return k.shift ? n + 10 : n;
|
|
1878
|
+
return null;
|
|
1879
|
+
}
|
|
1880
|
+
const d = DIGITS.indexOf(k.ch);
|
|
1881
|
+
if (k.ch && d >= 0) return d + 1;
|
|
1882
|
+
const s = SHIFTED_DIGITS.indexOf(k.ch);
|
|
1883
|
+
if (k.ch && s >= 0) return s + 11;
|
|
1884
|
+
return null;
|
|
1885
|
+
}
|
|
1886
|
+
function nodeTerminal() {
|
|
1887
|
+
const input = process.stdin;
|
|
1888
|
+
const output = process.stdout;
|
|
1889
|
+
emitKeypressEvents(input);
|
|
1890
|
+
const raw = (on) => {
|
|
1891
|
+
if (input.isTTY) input.setRawMode(on);
|
|
1892
|
+
};
|
|
1893
|
+
raw(true);
|
|
1894
|
+
input.resume();
|
|
1895
|
+
const subs = /* @__PURE__ */ new Set();
|
|
1896
|
+
let paused = false;
|
|
1897
|
+
input.on(
|
|
1898
|
+
"keypress",
|
|
1899
|
+
(str, key) => {
|
|
1900
|
+
if (paused) return;
|
|
1901
|
+
const k = {
|
|
1902
|
+
name: key && key.name || "",
|
|
1903
|
+
ch: str || "",
|
|
1904
|
+
shift: !!(key && key.shift),
|
|
1905
|
+
ctrl: !!(key && key.ctrl)
|
|
1906
|
+
};
|
|
1907
|
+
for (const f of subs) f(k);
|
|
1908
|
+
}
|
|
1909
|
+
);
|
|
1910
|
+
return {
|
|
1911
|
+
write: (s) => {
|
|
1912
|
+
output.write(s);
|
|
1913
|
+
},
|
|
1914
|
+
get columns() {
|
|
1915
|
+
return output.columns || 80;
|
|
1916
|
+
},
|
|
1917
|
+
get rows() {
|
|
1918
|
+
return output.rows || 24;
|
|
1919
|
+
},
|
|
1920
|
+
onKey(fn) {
|
|
1921
|
+
subs.add(fn);
|
|
1922
|
+
return () => {
|
|
1923
|
+
subs.delete(fn);
|
|
1924
|
+
};
|
|
1925
|
+
},
|
|
1926
|
+
async readLine(prompt) {
|
|
1927
|
+
paused = true;
|
|
1928
|
+
raw(false);
|
|
1929
|
+
const rl = createInterface2({ input, output, terminal: true });
|
|
1930
|
+
try {
|
|
1931
|
+
return await new Promise((res) => {
|
|
1932
|
+
rl.question(prompt, (a) => res(a));
|
|
1933
|
+
rl.on("close", () => res(null));
|
|
1934
|
+
});
|
|
1935
|
+
} finally {
|
|
1936
|
+
rl.close();
|
|
1937
|
+
raw(true);
|
|
1938
|
+
paused = false;
|
|
1939
|
+
}
|
|
1940
|
+
},
|
|
1941
|
+
close() {
|
|
1942
|
+
raw(false);
|
|
1943
|
+
input.pause();
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
var TuiUi = class {
|
|
1948
|
+
term;
|
|
1949
|
+
sent = [];
|
|
1950
|
+
statusText = "";
|
|
1951
|
+
progressText = "";
|
|
1952
|
+
stopRequested = false;
|
|
1953
|
+
writeKeys = /* @__PURE__ */ new Set();
|
|
1954
|
+
program = null;
|
|
1955
|
+
leftResolve = null;
|
|
1956
|
+
/** resolves once the program reports it left the module */
|
|
1957
|
+
leftPromise;
|
|
1958
|
+
R;
|
|
1959
|
+
/** the home script's host: what its picks list */
|
|
1960
|
+
home;
|
|
1961
|
+
/**
|
|
1962
|
+
* @param term - the terminal
|
|
1963
|
+
* @param R - the runtime (for the write verdict on key captions)
|
|
1964
|
+
* @param home - the home script's host, when the home may run
|
|
1965
|
+
*/
|
|
1966
|
+
constructor(term, R, home = null) {
|
|
1967
|
+
this.term = term;
|
|
1968
|
+
this.R = R;
|
|
1969
|
+
this.home = home;
|
|
1970
|
+
this.leftPromise = new Promise((res) => {
|
|
1971
|
+
this.leftResolve = res;
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* BMWeb's own picker (the home script's bmweb_pick): the host's list as
|
|
1976
|
+
* numbered rows; a number picks, anything else narrows the list to the
|
|
1977
|
+
* rows containing it, an empty answer cancels. A vehicle pick has one
|
|
1978
|
+
* answer and asks nothing.
|
|
1979
|
+
* @param _p - the program
|
|
1980
|
+
* @param step - the pick suspension
|
|
1981
|
+
* @returns the chosen value, or null for cancel
|
|
1982
|
+
*/
|
|
1983
|
+
async pickHome(_p, step) {
|
|
1984
|
+
if (!this.home) return null;
|
|
1985
|
+
if (step.what === "vehicle") return this.home.vehicle(step.arg);
|
|
1986
|
+
let options;
|
|
1987
|
+
try {
|
|
1988
|
+
options = step.what === "module" ? await this.home.modules(step.arg) : await this.home.chassis();
|
|
1989
|
+
} catch (e) {
|
|
1990
|
+
await this.message("No list", String(e.message || e));
|
|
1991
|
+
return null;
|
|
1992
|
+
}
|
|
1993
|
+
const title = step.what === "module" ? `Modules of ${step.arg}` : "Vehicles";
|
|
1994
|
+
let shown = options;
|
|
1995
|
+
for (; ; ) {
|
|
1996
|
+
const rows = shown.map(
|
|
1997
|
+
(o, i) => ` ${String(i + 1).padStart(3)}. ${o.label}${o.meta ? ` (${o.meta})` : ""}`
|
|
1998
|
+
).join("\n");
|
|
1999
|
+
const a = await this.term.readLine(
|
|
2000
|
+
`
|
|
2001
|
+
${title}${shown.length !== options.length ? ` (${shown.length} of ${options.length})` : ""}
|
|
2002
|
+
${rows}
|
|
2003
|
+
Number to open, text to filter, Enter to cancel: `
|
|
2004
|
+
);
|
|
2005
|
+
if (a == null || !a.trim()) return null;
|
|
2006
|
+
const n = Number(a.trim());
|
|
2007
|
+
if (Number.isInteger(n) && n >= 1 && n <= shown.length)
|
|
2008
|
+
return shown[n - 1].value;
|
|
2009
|
+
const q = a.trim().toLowerCase();
|
|
2010
|
+
const next = options.filter(
|
|
2011
|
+
(o) => `${o.label} ${o.meta || ""} ${o.value}`.toLowerCase().includes(q)
|
|
2012
|
+
);
|
|
2013
|
+
shown = next.length ? next : options;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
/** Note the program once it exists, for the key handler. */
|
|
2017
|
+
attach(p) {
|
|
2018
|
+
this.program = p;
|
|
2019
|
+
}
|
|
2020
|
+
/** Esc during a parked state machine: stop it at the next tick. */
|
|
2021
|
+
requestStop() {
|
|
2022
|
+
this.stopRequested = true;
|
|
2023
|
+
}
|
|
2024
|
+
sleep(ms) {
|
|
2025
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
2026
|
+
}
|
|
2027
|
+
loadExec(sgbd) {
|
|
2028
|
+
return this.R.irLiveExec(sgbd);
|
|
2029
|
+
}
|
|
2030
|
+
route() {
|
|
2031
|
+
}
|
|
2032
|
+
status(_p, text) {
|
|
2033
|
+
this.statusText = text;
|
|
2034
|
+
this.drawFooter();
|
|
2035
|
+
}
|
|
2036
|
+
error(_p, text) {
|
|
2037
|
+
this.statusText = `error: ${text}`;
|
|
2038
|
+
this.drawFooter();
|
|
2039
|
+
}
|
|
2040
|
+
async message(title, body) {
|
|
2041
|
+
await this.term.readLine(
|
|
2042
|
+
`
|
|
2043
|
+
${title}${body ? `
|
|
2044
|
+
${body}` : ""}
|
|
2045
|
+
[Enter to continue] `
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* INPA's prompts, answered on the keyboard with the meanings irAskInput
|
|
2050
|
+
* gives them: inputdigital is the script's two words and stores 1 for the
|
|
2051
|
+
* true word; the plain OK/Cancel box stores 0 for OK; inputhex takes a hex
|
|
2052
|
+
* string, input2text words, inputnum a real, the rest whole numbers within
|
|
2053
|
+
* the declared range. A cancel (empty answer, Esc) abandons the key.
|
|
2054
|
+
* @param step - the input suspension
|
|
2055
|
+
* @param label - the key's caption, when the script gave no title
|
|
2056
|
+
* @returns what resume() stores, or null for cancel
|
|
2057
|
+
*/
|
|
2058
|
+
async askInput(step, label) {
|
|
2059
|
+
const prompts = step.prompts || [];
|
|
2060
|
+
const refs = Math.max(1, Number(step.refs || 1));
|
|
2061
|
+
const name = String(step.name || "");
|
|
2062
|
+
const p0 = prompts[0] || label || "";
|
|
2063
|
+
const p1 = prompts[1] || "";
|
|
2064
|
+
if (name === "inputdigital") {
|
|
2065
|
+
const f = prompts[prompts.length - 2] || "OFF";
|
|
2066
|
+
const t = prompts[prompts.length - 1] || "ON";
|
|
2067
|
+
const a = await this.term.readLine(
|
|
2068
|
+
`
|
|
2069
|
+
${p0}
|
|
2070
|
+
${p1}
|
|
2071
|
+
[${t} = y, ${f} = n, cancel = Enter] `
|
|
2072
|
+
);
|
|
2073
|
+
if (a == null || !a.trim()) return null;
|
|
2074
|
+
return /^y/i.test(a.trim()) ? 1 : 0;
|
|
2075
|
+
}
|
|
2076
|
+
if (name === "builtin_3f" && prompts.length <= 2 && refs === 1) {
|
|
2077
|
+
const a = await this.term.readLine(
|
|
2078
|
+
`
|
|
2079
|
+
${p0}
|
|
2080
|
+
${p1}
|
|
2081
|
+
[OK = y, cancel = n] `
|
|
2082
|
+
);
|
|
2083
|
+
return a != null && /^y/i.test(a.trim()) ? 0 : null;
|
|
2084
|
+
}
|
|
2085
|
+
const hex = /hex/i.test(name);
|
|
2086
|
+
const text = /text/i.test(name);
|
|
2087
|
+
const real = name === "inputnum";
|
|
2088
|
+
const vals = [];
|
|
2089
|
+
for (let k = 0; k < refs; k++) {
|
|
2090
|
+
const cap = refs > 1 ? prompts[2 + k] || `${p0} (${k + 1}/${refs})` : p1;
|
|
2091
|
+
const range = step.lo != null && step.hi != null && !hex ? ` [${step.lo}..${step.hi}]` : "";
|
|
2092
|
+
const a = await this.term.readLine(`
|
|
2093
|
+
${p0}
|
|
2094
|
+
${cap}${range}: `);
|
|
2095
|
+
if (a == null) return null;
|
|
2096
|
+
if (text) {
|
|
2097
|
+
vals.push(String(a));
|
|
2098
|
+
continue;
|
|
2099
|
+
}
|
|
2100
|
+
if (!a.trim()) return null;
|
|
2101
|
+
if (hex) {
|
|
2102
|
+
vals.push(a.trim());
|
|
2103
|
+
continue;
|
|
2104
|
+
}
|
|
2105
|
+
const n = real ? Number(a) : Math.trunc(Number(a));
|
|
2106
|
+
if (!Number.isFinite(n)) return null;
|
|
2107
|
+
if (refs === 1 && step.lo != null && step.hi != null && (n < step.lo || n > step.hi))
|
|
2108
|
+
return null;
|
|
2109
|
+
vals.push(n);
|
|
2110
|
+
}
|
|
2111
|
+
return refs > 1 ? vals : vals[0];
|
|
2112
|
+
}
|
|
2113
|
+
async confirmKey(_p, it, jobs, writes) {
|
|
2114
|
+
const a = await this.term.readLine(
|
|
2115
|
+
`
|
|
2116
|
+
Run "${it.label || it.legendLabel || `F${it.nr}`}"? It can send ${jobs.join(", ")} and ${writes.join(", ")} write${writes.length === 1 ? "s" : ""} to the module. [y/N] `
|
|
2117
|
+
);
|
|
2118
|
+
return !!a && /^y(es)?$/i.test(a.trim());
|
|
2119
|
+
}
|
|
2120
|
+
async confirmWrite(_p, job, arg, ctx) {
|
|
2121
|
+
const every = String(ctx.scope || "").startsWith("screen:") ? " This screen sends it on every refresh; yes allows it while the screen is open." : "";
|
|
2122
|
+
const a = await this.term.readLine(
|
|
2123
|
+
`
|
|
2124
|
+
Send ${job}${arg ? ` ${arg}` : ""} to the module (${ctx.label})?${every} [y/N] `
|
|
2125
|
+
);
|
|
2126
|
+
return !!a && /^y(es)?$/i.test(a.trim());
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* INPA's togglelist: the active screen's LINE declarations, numbered;
|
|
2130
|
+
* one is picked and switched on or off (a tick list with the multiple
|
|
2131
|
+
* flag, every picked key ';'-joined).
|
|
2132
|
+
*/
|
|
2133
|
+
async pickComponent(p, step) {
|
|
2134
|
+
const rows = this.R.ipoScreenComponents(p.exec, p.screen).map((l, i2) => ({
|
|
2135
|
+
key: step.argnum ? String(i2 + 1) : String(l.keys).split(";")[0] || "",
|
|
2136
|
+
caption: l.label || String(l.keys).split(";")[0] || ""
|
|
2137
|
+
}));
|
|
2138
|
+
if (!rows.length) {
|
|
2139
|
+
await this.message(
|
|
2140
|
+
"No components to pick",
|
|
2141
|
+
"This screen lists no components."
|
|
2142
|
+
);
|
|
2143
|
+
return null;
|
|
2144
|
+
}
|
|
2145
|
+
const list = rows.map((r, i2) => ` ${i2 + 1}. ${r.caption} (${r.key})`).join("\n");
|
|
2146
|
+
if (step.multiple) {
|
|
2147
|
+
const a2 = await this.term.readLine(
|
|
2148
|
+
`
|
|
2149
|
+
${list}
|
|
2150
|
+
Components, comma-separated (Enter cancels): `
|
|
2151
|
+
);
|
|
2152
|
+
const picked = pickNumbers(a2, rows.length).map(
|
|
2153
|
+
(i2) => rows[i2]
|
|
2154
|
+
);
|
|
2155
|
+
if (!picked.length) return null;
|
|
2156
|
+
return { ort: picked.map((r) => r.key).join(";"), ein: 0 };
|
|
2157
|
+
}
|
|
2158
|
+
const a = await this.term.readLine(
|
|
2159
|
+
`
|
|
2160
|
+
${list}
|
|
2161
|
+
Component number (Enter cancels): `
|
|
2162
|
+
);
|
|
2163
|
+
const [i] = pickNumbers(a, rows.length);
|
|
2164
|
+
if (i == null) return null;
|
|
2165
|
+
const onOff = await this.term.readLine(`On or off? [on/off] `);
|
|
2166
|
+
if (onOff == null || !onOff.trim()) return null;
|
|
2167
|
+
return {
|
|
2168
|
+
ort: rows[i].key,
|
|
2169
|
+
ein: /^on/i.test(onOff.trim()) ? 0 : 1
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
/** INPA's Select: which named logical lines to keep on screen. */
|
|
2173
|
+
async pickLines(_p, names, multiple, _current, hints) {
|
|
2174
|
+
if (!names.length) {
|
|
2175
|
+
const where = (hints || []).map((h) => `${h.key} ${h.label} (${h.lines} lines)`).join(", ");
|
|
2176
|
+
await this.message(
|
|
2177
|
+
"Nothing to select here",
|
|
2178
|
+
where ? `Select works after one of: ${where}` : "This screen has no named lines."
|
|
2179
|
+
);
|
|
2180
|
+
return null;
|
|
2181
|
+
}
|
|
2182
|
+
const list = names.map((n, i) => ` ${i + 1}. ${n}`).join("\n");
|
|
2183
|
+
const a = await this.term.readLine(
|
|
2184
|
+
`
|
|
2185
|
+
${list}
|
|
2186
|
+
Lines to show${multiple ? ", comma-separated" : ""} (a = all, Enter cancels): `
|
|
2187
|
+
);
|
|
2188
|
+
if (a == null) return null;
|
|
2189
|
+
if (/^a(ll)?$/i.test(a.trim())) return [];
|
|
2190
|
+
const picked = pickNumbers(a, names.length).map((i) => names[i]);
|
|
2191
|
+
return picked.length ? picked : null;
|
|
2192
|
+
}
|
|
2193
|
+
/** INPA's save-as dialog: a file name, written when the body ends. */
|
|
2194
|
+
async saveFile() {
|
|
2195
|
+
const a = await this.term.readLine(`
|
|
2196
|
+
Save as [fault-memory.txt]: `);
|
|
2197
|
+
if (a == null) return null;
|
|
2198
|
+
return { name: a.trim() || "fault-memory.txt" };
|
|
2199
|
+
}
|
|
2200
|
+
async writeFile(_p, picked, lines) {
|
|
2201
|
+
writeFileSync4(picked.name, lines.join("\r\n") + "\r\n", "latin1");
|
|
2202
|
+
this.status(_p, `wrote ${picked.name}`);
|
|
2203
|
+
}
|
|
2204
|
+
printScreen(p) {
|
|
2205
|
+
this.term.write(`
|
|
2206
|
+
${this.gridLines(p).join("\n")}
|
|
2207
|
+
`);
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* The module a scriptchange names. From the home script it is the car's
|
|
2211
|
+
* own config row for the picked chassis (home.js ipoHomeEcuFor), prepared
|
|
2212
|
+
* the way opening it from the module list prepares it; from any other
|
|
2213
|
+
* script it is the module the car identifies (open.js).
|
|
2214
|
+
*/
|
|
2215
|
+
async resolveScriptEcu(from, script, exec) {
|
|
2216
|
+
if (from && from.sgbd === this.R.IPO_HOME_SGBD) {
|
|
2217
|
+
const row = await this.R.ipoHomeEcuFor(from, script);
|
|
2218
|
+
if (!row) return null;
|
|
2219
|
+
try {
|
|
2220
|
+
const api = runtimeGlobals().api;
|
|
2221
|
+
return (await prepareModule(row, api)).ecu;
|
|
2222
|
+
} catch {
|
|
2223
|
+
return row;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
return this.R.ipoResolveScriptEcu(from, script, exec);
|
|
2227
|
+
}
|
|
2228
|
+
/** A %STATE park: tick after IPO_TICK_MS, or stop when Esc was pressed. */
|
|
2229
|
+
async machineTick(_p, step) {
|
|
2230
|
+
this.progressText = `state ${String(step.name || "").replace(/^%/, "")} (Esc stops)`;
|
|
2231
|
+
this.drawFooter();
|
|
2232
|
+
await this.sleep(this.R.IPO_TICK_MS);
|
|
2233
|
+
if (this.stopRequested) {
|
|
2234
|
+
this.stopRequested = false;
|
|
2235
|
+
this.progressText = "";
|
|
2236
|
+
return "stop";
|
|
2237
|
+
}
|
|
2238
|
+
return "tick";
|
|
2239
|
+
}
|
|
2240
|
+
userbox(_p, box) {
|
|
2241
|
+
if (!box) {
|
|
2242
|
+
this.progressText = "";
|
|
2243
|
+
} else {
|
|
2244
|
+
const lines = box.lines || [];
|
|
2245
|
+
const text = lines.length ? String(lines[lines.length - 1]) : "";
|
|
2246
|
+
this.progressText = `${box.title ? `${box.title}: ` : ""}${text}`.trim();
|
|
2247
|
+
}
|
|
2248
|
+
this.drawFooter();
|
|
2249
|
+
}
|
|
2250
|
+
/** The F-key bar: the write verdict per key marks what will ask. */
|
|
2251
|
+
renderKeys(p) {
|
|
2252
|
+
this.writeKeys = /* @__PURE__ */ new Set();
|
|
2253
|
+
const jobsOf = runtimeGlobals().irItemBodyJobs;
|
|
2254
|
+
for (const it of p.items) {
|
|
2255
|
+
const jobs = jobsOf ? jobsOf(p.exec, p.exec.procs[p.menu || ""], it.start, it.end) : [];
|
|
2256
|
+
if (jobs.some((j) => this.R.ipoNeedsConfirm(j)))
|
|
2257
|
+
this.writeKeys.add(it.nr);
|
|
2258
|
+
}
|
|
2259
|
+
this.paint(p);
|
|
2260
|
+
}
|
|
2261
|
+
/** Redraw everything: title, grid, keys, footer. */
|
|
2262
|
+
paint(p) {
|
|
2263
|
+
const w = this.term.columns;
|
|
2264
|
+
const out = [];
|
|
2265
|
+
const title = `${p.ecu.label || p.ecu.sgbd} ${p.ecu.sgbd}.prg ${p.title || ""}`.trim();
|
|
2266
|
+
out.push(title.slice(0, w));
|
|
2267
|
+
out.push("-".repeat(Math.min(w, 78)));
|
|
2268
|
+
if (p.view) {
|
|
2269
|
+
out.push(...(p.view.lines || []).slice(0, this.term.rows - 8));
|
|
2270
|
+
} else {
|
|
2271
|
+
out.push(...this.gridLines(p));
|
|
2272
|
+
}
|
|
2273
|
+
out.push("");
|
|
2274
|
+
out.push(...this.keyLines(p));
|
|
2275
|
+
this.term.write(`\x1B[H\x1B[2J${out.join("\n")}
|
|
2276
|
+
`);
|
|
2277
|
+
this.drawFooter();
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* The grid as text rows: cells placed at their column, a lamp as a dot
|
|
2281
|
+
* and its word, a gauge as a bar and its number, the rest as its text.
|
|
2282
|
+
*/
|
|
2283
|
+
gridLines(p) {
|
|
2284
|
+
const w = this.term.columns;
|
|
2285
|
+
const rows = /* @__PURE__ */ new Map();
|
|
2286
|
+
for (const c of p.cells.values()) {
|
|
2287
|
+
if (!rows.has(c.row)) rows.set(c.row, []);
|
|
2288
|
+
rows.get(c.row).push(c);
|
|
2289
|
+
}
|
|
2290
|
+
const maxRow = rows.size ? Math.max(...rows.keys()) : -1;
|
|
2291
|
+
const out = [];
|
|
2292
|
+
for (let r = 0; r <= maxRow; r++) {
|
|
2293
|
+
const line = [];
|
|
2294
|
+
for (const c of (rows.get(r) || []).sort((a, b) => a.col - b.col)) {
|
|
2295
|
+
const text = cellText(c);
|
|
2296
|
+
while (line.length < c.col) line.push(" ");
|
|
2297
|
+
for (const ch of text) line.push(ch);
|
|
2298
|
+
}
|
|
2299
|
+
out.push(line.join("").replace(/\s+$/, "").slice(0, w));
|
|
2300
|
+
}
|
|
2301
|
+
return out;
|
|
2302
|
+
}
|
|
2303
|
+
/** The key bar: the plain bank on one line, the shifted on the next. */
|
|
2304
|
+
keyLines(p) {
|
|
2305
|
+
const shown = p.items.filter((it) => !it.hidden || !!it.legendLabel);
|
|
2306
|
+
const label = (it) => `${it.shift ? "S" : ""}F${it.shift ? it.nr - 10 : it.nr} ${it.label || it.legendLabel || ""}${this.writeKeys.has(it.nr) ? "*" : ""}`;
|
|
2307
|
+
const plain = shown.filter((it) => !it.shift).map(label);
|
|
2308
|
+
const shifted = shown.filter((it) => it.shift).map(label);
|
|
2309
|
+
const out = [];
|
|
2310
|
+
if (plain.length) out.push(plain.join(" "));
|
|
2311
|
+
if (shifted.length) out.push(shifted.join(" "));
|
|
2312
|
+
out.push(
|
|
2313
|
+
"(1..9,0 = F1..F10, shifted symbols = Shift+F, Esc = back, q = quit, * asks first)"
|
|
2314
|
+
);
|
|
2315
|
+
return out;
|
|
2316
|
+
}
|
|
2317
|
+
/** The two bottom lines: status and progress. */
|
|
2318
|
+
drawFooter() {
|
|
2319
|
+
this.term.write(
|
|
2320
|
+
`\x1B[s\x1B[${Math.max(1, this.term.rows - 1)};1H\x1B[K${this.statusText}
|
|
2321
|
+
\x1B[K${this.progressText}\x1B[u`
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
left() {
|
|
2325
|
+
if (this.leftResolve) this.leftResolve();
|
|
2326
|
+
}
|
|
2327
|
+
};
|
|
2328
|
+
function pickNumbers(a, n) {
|
|
2329
|
+
if (a == null) return [];
|
|
2330
|
+
return a.split(/[,\s]+/).map((s) => Number(s)).filter((i) => Number.isInteger(i) && i >= 1 && i <= n).map((i) => i - 1);
|
|
2331
|
+
}
|
|
2332
|
+
function cellText(c) {
|
|
2333
|
+
const text = String(c.text || "");
|
|
2334
|
+
if (c.kind === "lamp") {
|
|
2335
|
+
const word = text.trim();
|
|
2336
|
+
const on = c.meta && c.meta.on != null && word === String(c.meta.on) || /^(1|ein|on|an|ja|yes|aktiv|true)$/i.test(word);
|
|
2337
|
+
return `${on ? "(*)" : "( )"} ${word}`;
|
|
2338
|
+
}
|
|
2339
|
+
if (c.kind === "gauge" && c.meta && c.meta.min != null && c.meta.max != null) {
|
|
2340
|
+
const n = parseFloat(text);
|
|
2341
|
+
if (Number.isFinite(n)) {
|
|
2342
|
+
const span = c.meta.max - c.meta.min || 1;
|
|
2343
|
+
const fill = Math.round(
|
|
2344
|
+
Math.max(0, Math.min(1, (n - c.meta.min) / span)) * GAUGE_WIDTH
|
|
2345
|
+
);
|
|
2346
|
+
return `[${"#".repeat(fill)}${".".repeat(GAUGE_WIDTH - fill)}] ${text.trim()}`;
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
return text;
|
|
2350
|
+
}
|
|
2351
|
+
async function tuiCommand(chassis, sgbd, opts = {}) {
|
|
2352
|
+
const R = loadRuntime();
|
|
2353
|
+
const home = !chassis && !sgbd;
|
|
2354
|
+
if (opts.apiFn) setApiImpl(opts.apiFn);
|
|
2355
|
+
let cable = false;
|
|
2356
|
+
if (!opts.noBus) {
|
|
2357
|
+
if (home) {
|
|
2358
|
+
try {
|
|
2359
|
+
await connectBus(opts);
|
|
2360
|
+
cable = true;
|
|
2361
|
+
} catch (e) {
|
|
2362
|
+
if (!(e instanceof CliError)) throw e;
|
|
2363
|
+
}
|
|
2364
|
+
} else {
|
|
2365
|
+
await connectBus(opts);
|
|
2366
|
+
cable = true;
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
const api = opts.apiFn || runtimeGlobals().api;
|
|
2370
|
+
const host = terminalHomeHost(
|
|
2371
|
+
api,
|
|
2372
|
+
() => `bmweb-cli ${opts.version || ""} ${cable || opts.noBus && !home ? "cable connected" : "no cable"}`.trim()
|
|
2373
|
+
);
|
|
2374
|
+
Object.assign(R.IPO_HOME_HOST, host);
|
|
2375
|
+
const term = opts.term || nodeTerminal();
|
|
2376
|
+
const ui = new TuiUi(term, R, host);
|
|
2377
|
+
let p = null;
|
|
2378
|
+
try {
|
|
2379
|
+
let ecu;
|
|
2380
|
+
let exec;
|
|
2381
|
+
if (opts.exec) {
|
|
2382
|
+
ecu = opts.ecu || {
|
|
2383
|
+
sgbd: String(sgbd || "").toLowerCase(),
|
|
2384
|
+
label: String(sgbd || "").toUpperCase(),
|
|
2385
|
+
chassis: String(chassis || "").toUpperCase()
|
|
2386
|
+
};
|
|
2387
|
+
exec = opts.exec;
|
|
2388
|
+
} else if (home) {
|
|
2389
|
+
exec = R.ipoHomeExec();
|
|
2390
|
+
ecu = {
|
|
2391
|
+
sgbd: R.IPO_HOME_SGBD,
|
|
2392
|
+
code: "BMWEB",
|
|
2393
|
+
label: "BMWeb",
|
|
2394
|
+
_variant: R.IPO_HOME_SGBD.toUpperCase()
|
|
2395
|
+
};
|
|
2396
|
+
} else {
|
|
2397
|
+
({ ecu, exec } = await openModule(
|
|
2398
|
+
chassis,
|
|
2399
|
+
sgbd,
|
|
2400
|
+
opts.apiFn
|
|
2401
|
+
));
|
|
2402
|
+
}
|
|
2403
|
+
p = new R.IpoProgram(ecu, exec, ui);
|
|
2404
|
+
ui.attach(p);
|
|
2405
|
+
const r = await p.start();
|
|
2406
|
+
if (p.noCable) throw new CliError("no cable connected");
|
|
2407
|
+
if (!r.ok) {
|
|
2408
|
+
const last = p.messages[p.messages.length - 1];
|
|
2409
|
+
throw new CliError(
|
|
2410
|
+
`${ecu.sgbd}: the script did not start (${r.reason || "stopped"})` + (last ? `: ${last.title}${last.body ? ` ${last.body}` : ""}` : "")
|
|
2411
|
+
);
|
|
2412
|
+
}
|
|
2413
|
+
if (opts.menu) {
|
|
2414
|
+
if (!exec.procs[opts.menu])
|
|
2415
|
+
throw new CliError(`${ecu.sgbd}: no menu ${opts.menu}`);
|
|
2416
|
+
if (opts.menu !== p.menu) await p.openMenu(opts.menu);
|
|
2417
|
+
}
|
|
2418
|
+
const program = p;
|
|
2419
|
+
const unsubscribe = term.onKey((k) => {
|
|
2420
|
+
const what = keyToPress(k);
|
|
2421
|
+
if (what === null) return;
|
|
2422
|
+
if (what === "quit") {
|
|
2423
|
+
unsubscribe();
|
|
2424
|
+
program.leaveModule().catch(() => {
|
|
2425
|
+
});
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
if (what === "back") {
|
|
2429
|
+
ui.requestStop();
|
|
2430
|
+
program.back().catch(() => {
|
|
2431
|
+
});
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
program.press(what).catch(() => {
|
|
2435
|
+
});
|
|
2436
|
+
});
|
|
2437
|
+
await ui.leftPromise;
|
|
2438
|
+
unsubscribe();
|
|
2439
|
+
return { log: program.log, messages: program.messages };
|
|
2440
|
+
} finally {
|
|
2441
|
+
if (p && !p.closed) p.close();
|
|
2442
|
+
term.close();
|
|
2443
|
+
if (opts.apiFn) setApiImpl(null);
|
|
2444
|
+
if (!opts.noBus) await disconnectBus(R);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
// src/bmweb.ts
|
|
2449
|
+
var VERSION = true ? "0.1.0" : "0.0.0-dev";
|
|
2450
|
+
var INCLUDE = {
|
|
2451
|
+
include: {
|
|
2452
|
+
kind: "list",
|
|
2453
|
+
alias: "I",
|
|
2454
|
+
help: "a directory holding the INPA headers a source needs (repeatable)"
|
|
2455
|
+
}
|
|
2456
|
+
};
|
|
2457
|
+
var JSON_FLAG = {
|
|
2458
|
+
json: {
|
|
2459
|
+
kind: "bool",
|
|
2460
|
+
help: "print machine-readable JSON instead of a table"
|
|
2461
|
+
}
|
|
2462
|
+
};
|
|
2463
|
+
var LIVE = {
|
|
2464
|
+
port: {
|
|
2465
|
+
kind: "string",
|
|
2466
|
+
alias: "p",
|
|
2467
|
+
help: "the serial device (the single candidate when there is one)"
|
|
2468
|
+
},
|
|
2469
|
+
api: {
|
|
2470
|
+
kind: "string",
|
|
2471
|
+
help: "the site the module data comes from (default https://bmweb.danner.ink/)"
|
|
2472
|
+
},
|
|
2473
|
+
refresh: {
|
|
2474
|
+
kind: "bool",
|
|
2475
|
+
help: "fetch the module data again even when the cached copy is fresh"
|
|
2476
|
+
}
|
|
2477
|
+
};
|
|
2478
|
+
var COMMANDS = {
|
|
2479
|
+
"ipo info": {
|
|
2480
|
+
usage: "bmweb ipo info <file.IPO|file.IPS|file.SRC> [-I dir]... [--json]",
|
|
2481
|
+
summary: "what a script is: form, includes, entry, procedures, DLL imports, and every menu with its keys and jobs",
|
|
2482
|
+
flags: { ...INCLUDE, ...JSON_FLAG },
|
|
2483
|
+
async run(pos, flags) {
|
|
2484
|
+
const file = pos[0];
|
|
2485
|
+
if (!file || pos.length > 1)
|
|
2486
|
+
throw new CliError("usage: " + COMMANDS["ipo info"].usage);
|
|
2487
|
+
return ipoInfo(file, flags.include || [], !!flags.json);
|
|
2488
|
+
}
|
|
2489
|
+
},
|
|
2490
|
+
"ipo keys": {
|
|
2491
|
+
usage: "bmweb ipo keys <file.IPO|file.IPS|file.SRC> [--menu m_x] [-I dir]... [--json]",
|
|
2492
|
+
summary: "the F-keys of every menu (or one): the screen each opens, the jobs it sends, and which of them write",
|
|
2493
|
+
flags: {
|
|
2494
|
+
...INCLUDE,
|
|
2495
|
+
menu: { kind: "string", alias: "m", help: "only this menu procedure" },
|
|
2496
|
+
...JSON_FLAG
|
|
2497
|
+
},
|
|
2498
|
+
async run(pos, flags) {
|
|
2499
|
+
const file = pos[0];
|
|
2500
|
+
if (!file || pos.length > 1)
|
|
2501
|
+
throw new CliError("usage: " + COMMANDS["ipo keys"].usage);
|
|
2502
|
+
return ipoKeys(
|
|
2503
|
+
file,
|
|
2504
|
+
flags.include || [],
|
|
2505
|
+
flags.menu,
|
|
2506
|
+
!!flags.json
|
|
2507
|
+
);
|
|
2508
|
+
}
|
|
2509
|
+
},
|
|
2510
|
+
"ipo compile": {
|
|
2511
|
+
usage: "bmweb ipo compile <file.IPS|file.SRC> [-I dir]... [-o out.ipoexec.json]",
|
|
2512
|
+
summary: "compile an INPA source into the app's exec form (JSON); missing includes are named",
|
|
2513
|
+
flags: {
|
|
2514
|
+
...INCLUDE,
|
|
2515
|
+
out: {
|
|
2516
|
+
kind: "string",
|
|
2517
|
+
alias: "o",
|
|
2518
|
+
help: "where to write (default: <stem>.ipoexec.json beside the source)"
|
|
2519
|
+
}
|
|
2520
|
+
},
|
|
2521
|
+
async run(pos, flags) {
|
|
2522
|
+
const file = pos[0];
|
|
2523
|
+
if (!file || pos.length > 1)
|
|
2524
|
+
throw new CliError(
|
|
2525
|
+
"usage: " + COMMANDS["ipo compile"].usage
|
|
2526
|
+
);
|
|
2527
|
+
return ipoCompile(
|
|
2528
|
+
file,
|
|
2529
|
+
flags.include || [],
|
|
2530
|
+
flags.out
|
|
2531
|
+
);
|
|
2532
|
+
}
|
|
2533
|
+
},
|
|
2534
|
+
search: {
|
|
2535
|
+
usage: "bmweb search <query...> [--chassis E46] [--limit N] [--refresh] [--json]",
|
|
2536
|
+
summary: "find the INPA key or screen that does a thing, across every module the site ships, with a deep link per hit",
|
|
2537
|
+
flags: {
|
|
2538
|
+
chassis: {
|
|
2539
|
+
kind: "string",
|
|
2540
|
+
alias: "c",
|
|
2541
|
+
help: "only modules this chassis carries"
|
|
2542
|
+
},
|
|
2543
|
+
limit: { kind: "number", alias: "n", help: "rows to show (default 50)" },
|
|
2544
|
+
refresh: {
|
|
2545
|
+
kind: "bool",
|
|
2546
|
+
help: "fetch the index again even when the cached copy is fresh"
|
|
2547
|
+
},
|
|
2548
|
+
...JSON_FLAG
|
|
2549
|
+
},
|
|
2550
|
+
async run(pos, flags) {
|
|
2551
|
+
const query = pos.join(" ").trim();
|
|
2552
|
+
if (!query)
|
|
2553
|
+
throw new CliError("usage: " + COMMANDS.search.usage);
|
|
2554
|
+
const index = await loadIndex({ refresh: !!flags.refresh });
|
|
2555
|
+
return runSearch(query, {
|
|
2556
|
+
chassis: flags.chassis,
|
|
2557
|
+
limit: flags.limit || 50,
|
|
2558
|
+
json: !!flags.json,
|
|
2559
|
+
index
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2562
|
+
},
|
|
2563
|
+
"report show": {
|
|
2564
|
+
usage: "bmweb report show <link-or-payload> [--json]",
|
|
2565
|
+
summary: "decode a shared Garage report link: module, code, text, count per fault, plus the silent addresses",
|
|
2566
|
+
flags: { ...JSON_FLAG },
|
|
2567
|
+
async run(pos, flags) {
|
|
2568
|
+
const arg = pos[0];
|
|
2569
|
+
if (!arg || pos.length > 1)
|
|
2570
|
+
throw new CliError(
|
|
2571
|
+
"usage: " + COMMANDS["report show"].usage
|
|
2572
|
+
);
|
|
2573
|
+
return reportShow(arg, !!flags.json);
|
|
2574
|
+
}
|
|
2575
|
+
},
|
|
2576
|
+
"report diff": {
|
|
2577
|
+
usage: "bmweb report diff <link-a> <link-b> [--json]",
|
|
2578
|
+
summary: "what changed between two shared reports: new, cleared and still-present faults per module",
|
|
2579
|
+
flags: { ...JSON_FLAG },
|
|
2580
|
+
async run(pos, flags) {
|
|
2581
|
+
const [a, b] = pos;
|
|
2582
|
+
if (!a || !b || pos.length > 2)
|
|
2583
|
+
throw new CliError(
|
|
2584
|
+
"usage: " + COMMANDS["report diff"].usage
|
|
2585
|
+
);
|
|
2586
|
+
return reportDiff(a, b, !!flags.json);
|
|
2587
|
+
}
|
|
2588
|
+
},
|
|
2589
|
+
ports: {
|
|
2590
|
+
usage: "bmweb ports [--json]",
|
|
2591
|
+
summary: "the serial ports a K+DCAN cable shows up as (cu.usbserial*, cu.SLAB*, cu.wchusbserial*, ttyUSB*, ttyACM*)",
|
|
2592
|
+
flags: { ...JSON_FLAG },
|
|
2593
|
+
async run(pos, flags) {
|
|
2594
|
+
if (pos.length)
|
|
2595
|
+
throw new CliError("usage: " + COMMANDS.ports.usage);
|
|
2596
|
+
return portsCommand(!!flags.json);
|
|
2597
|
+
}
|
|
2598
|
+
},
|
|
2599
|
+
job: {
|
|
2600
|
+
usage: "bmweb job <sgbd> <JOB> [arg] [--port p] [--api url] [--yes] [--json]",
|
|
2601
|
+
summary: "one raw job on one module over the cable, like the app's Tool32; a write needs --yes or a y answer",
|
|
2602
|
+
flags: {
|
|
2603
|
+
...LIVE,
|
|
2604
|
+
yes: {
|
|
2605
|
+
kind: "bool",
|
|
2606
|
+
alias: "y",
|
|
2607
|
+
help: "consent to a write job on the command line"
|
|
2608
|
+
},
|
|
2609
|
+
...JSON_FLAG
|
|
2610
|
+
},
|
|
2611
|
+
async run(pos, flags) {
|
|
2612
|
+
const [sgbd, job, arg] = pos;
|
|
2613
|
+
if (!sgbd || !job || pos.length > 3)
|
|
2614
|
+
throw new CliError("usage: " + COMMANDS.job.usage);
|
|
2615
|
+
const { R } = await connectBus(liveOptions(flags));
|
|
2616
|
+
try {
|
|
2617
|
+
return await jobCommand(sgbd, job, {
|
|
2618
|
+
arg,
|
|
2619
|
+
yes: !!flags.yes,
|
|
2620
|
+
json: !!flags.json
|
|
2621
|
+
});
|
|
2622
|
+
} finally {
|
|
2623
|
+
await disconnectBus(R);
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
},
|
|
2627
|
+
scan: {
|
|
2628
|
+
usage: "bmweb scan <chassis> [--port p] [--api url] [--share] [--json]",
|
|
2629
|
+
summary: `INPA's whole-vehicle script over the cable (${SCAN_CHASSIS.join(" ")}): every fault memory as a report, --share adds a Garage link`,
|
|
2630
|
+
flags: {
|
|
2631
|
+
...LIVE,
|
|
2632
|
+
share: {
|
|
2633
|
+
kind: "bool",
|
|
2634
|
+
help: "print a Garage share link carrying the report"
|
|
2635
|
+
},
|
|
2636
|
+
label: {
|
|
2637
|
+
kind: "string",
|
|
2638
|
+
help: "the car name on the share link"
|
|
2639
|
+
},
|
|
2640
|
+
...JSON_FLAG
|
|
2641
|
+
},
|
|
2642
|
+
async run(pos, flags) {
|
|
2643
|
+
const chassis = pos[0];
|
|
2644
|
+
if (!chassis || pos.length > 1)
|
|
2645
|
+
throw new CliError("usage: " + COMMANDS.scan.usage);
|
|
2646
|
+
const { R } = await connectBus(liveOptions(flags));
|
|
2647
|
+
try {
|
|
2648
|
+
const r = await scanCommand(chassis, {
|
|
2649
|
+
share: !!flags.share,
|
|
2650
|
+
json: !!flags.json,
|
|
2651
|
+
label: flags.label
|
|
2652
|
+
});
|
|
2653
|
+
return r.lines;
|
|
2654
|
+
} finally {
|
|
2655
|
+
await disconnectBus(R);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
},
|
|
2659
|
+
tui: {
|
|
2660
|
+
usage: "bmweb tui [<chassis> <sgbd>] [--port p] [--api url] [--menu m_x]",
|
|
2661
|
+
summary: "INPA screens in the terminal: the app's home (pick a chassis and a module) with no arguments, else that module; F-keys on the number row, every write asked first, released on quit",
|
|
2662
|
+
flags: {
|
|
2663
|
+
...LIVE,
|
|
2664
|
+
menu: {
|
|
2665
|
+
kind: "string",
|
|
2666
|
+
alias: "m",
|
|
2667
|
+
help: "the menu procedure to open once the script is up"
|
|
2668
|
+
}
|
|
2669
|
+
},
|
|
2670
|
+
async run(pos, flags) {
|
|
2671
|
+
const [chassis, sgbd] = pos;
|
|
2672
|
+
if (pos.length === 1 || pos.length > 2)
|
|
2673
|
+
throw new CliError("usage: " + COMMANDS.tui.usage);
|
|
2674
|
+
const r = await tuiCommand(chassis, sgbd, {
|
|
2675
|
+
...liveOptions(flags),
|
|
2676
|
+
menu: flags.menu,
|
|
2677
|
+
version: VERSION
|
|
2678
|
+
});
|
|
2679
|
+
return [
|
|
2680
|
+
`${r.log.length} job${r.log.length === 1 ? "" : "s"} sent:`,
|
|
2681
|
+
...r.log.map(
|
|
2682
|
+
(l) => ` ${l.target} ${l.job}${l.arg ? ` ${l.arg}` : ""} ${l.status}`
|
|
2683
|
+
)
|
|
2684
|
+
];
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
};
|
|
2688
|
+
function liveOptions(flags) {
|
|
2689
|
+
return {
|
|
2690
|
+
port: flags.port,
|
|
2691
|
+
api: flags.api,
|
|
2692
|
+
refresh: !!flags.refresh
|
|
2693
|
+
};
|
|
2694
|
+
}
|
|
2695
|
+
function helpText() {
|
|
2696
|
+
const out = [
|
|
2697
|
+
`bmweb ${VERSION}: BMWeb's tools as a command line (https://bmweb.danner.ink/)`,
|
|
2698
|
+
"",
|
|
2699
|
+
"usage: bmweb <command> [options]",
|
|
2700
|
+
"",
|
|
2701
|
+
"commands:"
|
|
2702
|
+
];
|
|
2703
|
+
const names = Object.keys(COMMANDS);
|
|
2704
|
+
const w = Math.max(...names.map((n) => n.length));
|
|
2705
|
+
for (const n of names)
|
|
2706
|
+
out.push(` ${n.padEnd(w)} ${COMMANDS[n].summary}`);
|
|
2707
|
+
out.push(
|
|
2708
|
+
"",
|
|
2709
|
+
" bmweb <command> --help options of one command",
|
|
2710
|
+
" bmweb --version",
|
|
2711
|
+
"",
|
|
2712
|
+
"ports, job, scan and tui talk to the car over a K+DCAN cable (the serialport",
|
|
2713
|
+
"package, an optional dependency). Module data and the search index are",
|
|
2714
|
+
"fetched from the site and cached under $XDG_CACHE_HOME/bmweb-cli",
|
|
2715
|
+
"(default ~/.cache/bmweb-cli) for a day; nothing BMW-derived ships here."
|
|
2716
|
+
);
|
|
2717
|
+
return out;
|
|
2718
|
+
}
|
|
2719
|
+
function commandHelp(name) {
|
|
2720
|
+
const c = COMMANDS[name];
|
|
2721
|
+
return [
|
|
2722
|
+
`usage: ${c.usage}`,
|
|
2723
|
+
"",
|
|
2724
|
+
c.summary,
|
|
2725
|
+
"",
|
|
2726
|
+
"options:",
|
|
2727
|
+
...helpLines(c.flags)
|
|
2728
|
+
];
|
|
2729
|
+
}
|
|
2730
|
+
function resolveCommand(argv) {
|
|
2731
|
+
for (const n of [2, 1]) {
|
|
2732
|
+
const name = argv.slice(0, n).join(" ");
|
|
2733
|
+
if (COMMANDS[name]) return { name, rest: argv.slice(n) };
|
|
2734
|
+
}
|
|
2735
|
+
return null;
|
|
2736
|
+
}
|
|
2737
|
+
async function main(argv, out = (l) => process.stdout.write(l + "\n"), err = (l) => process.stderr.write(l + "\n")) {
|
|
2738
|
+
try {
|
|
2739
|
+
if (!argv.length || argv[0] === "--help" || argv[0] === "-h" || argv[0] === "help") {
|
|
2740
|
+
helpText().forEach(out);
|
|
2741
|
+
return 0;
|
|
2742
|
+
}
|
|
2743
|
+
if (argv[0] === "--version" || argv[0] === "-v") {
|
|
2744
|
+
out(VERSION);
|
|
2745
|
+
return 0;
|
|
2746
|
+
}
|
|
2747
|
+
const picked = resolveCommand(argv);
|
|
2748
|
+
if (!picked) {
|
|
2749
|
+
const group = argv[0];
|
|
2750
|
+
const members = Object.keys(COMMANDS).filter(
|
|
2751
|
+
(n) => n.startsWith(group + " ")
|
|
2752
|
+
);
|
|
2753
|
+
if (members.length) {
|
|
2754
|
+
members.forEach((n) => out(`usage: ${COMMANDS[n].usage}`));
|
|
2755
|
+
return 1;
|
|
2756
|
+
}
|
|
2757
|
+
throw new CliError(
|
|
2758
|
+
`unknown command "${argv.join(" ")}" (try bmweb --help)`
|
|
2759
|
+
);
|
|
2760
|
+
}
|
|
2761
|
+
if (picked.rest.includes("--help") || picked.rest.includes("-h")) {
|
|
2762
|
+
commandHelp(picked.name).forEach(out);
|
|
2763
|
+
return 0;
|
|
2764
|
+
}
|
|
2765
|
+
const c = COMMANDS[picked.name];
|
|
2766
|
+
const parsed = parseArgs(picked.rest, c.flags);
|
|
2767
|
+
const lines = await c.run(parsed.positional, parsed.flags);
|
|
2768
|
+
lines.forEach(out);
|
|
2769
|
+
return 0;
|
|
2770
|
+
} catch (e) {
|
|
2771
|
+
if (e instanceof CliError) {
|
|
2772
|
+
err(`bmweb: ${e.message}`);
|
|
2773
|
+
return 1;
|
|
2774
|
+
}
|
|
2775
|
+
err(`bmweb: unexpected error: ${e.stack || e}`);
|
|
2776
|
+
return 1;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
if (typeof process !== "undefined" && process.argv[1] && /bmweb(\.js)?$/.test(process.argv[1])) {
|
|
2780
|
+
main(process.argv.slice(2)).then((code) => {
|
|
2781
|
+
process.exitCode = code;
|
|
2782
|
+
});
|
|
2783
|
+
}
|
|
2784
|
+
export {
|
|
2785
|
+
VERSION,
|
|
2786
|
+
commandHelp,
|
|
2787
|
+
helpText,
|
|
2788
|
+
main,
|
|
2789
|
+
resolveCommand
|
|
2790
|
+
};
|