bmweb-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +351 -0
  3. package/dist/bmweb.js +2790 -0
  4. package/package.json +53 -0
  5. package/runtime/core/bestvm/codec.js +285 -0
  6. package/runtime/core/bestvm/environment.js +116 -0
  7. package/runtime/core/bestvm/executor.js +1483 -0
  8. package/runtime/core/bestvm/index.js +52 -0
  9. package/runtime/core/bestvm/machine.js +491 -0
  10. package/runtime/core/bestvm/operands.js +356 -0
  11. package/runtime/core/bestvm/registers.js +152 -0
  12. package/runtime/core/bestvm/write-guard.js +111 -0
  13. package/runtime/core/ipofile/compile.js +364 -0
  14. package/runtime/core/ipofile/decls.js +187 -0
  15. package/runtime/core/ipofile/emit.js +708 -0
  16. package/runtime/core/ipofile/exec.js +164 -0
  17. package/runtime/core/ipofile/lex.js +243 -0
  18. package/runtime/core/ipofile/parse.js +550 -0
  19. package/runtime/core/ipofile/pool.js +404 -0
  20. package/runtime/core/ipofile/walk.js +431 -0
  21. package/runtime/core/ipovm/builtin-helpers.js +182 -0
  22. package/runtime/core/ipovm/builtins-api.js +610 -0
  23. package/runtime/core/ipovm/builtins-screen.js +493 -0
  24. package/runtime/core/ipovm/builtins-table.js +166 -0
  25. package/runtime/core/ipovm/builtins-text.js +166 -0
  26. package/runtime/core/ipovm/emissions.js +138 -0
  27. package/runtime/core/ipovm/hosts.js +191 -0
  28. package/runtime/core/ipovm/operators.js +229 -0
  29. package/runtime/core/ipovm/structures.js +250 -0
  30. package/runtime/core/ipovm/suspensions.js +241 -0
  31. package/runtime/core/ipovm/tape.js +206 -0
  32. package/runtime/core/ipovm/values.js +241 -0
  33. package/runtime/core/ipovm/vm.js +1166 -0
  34. package/runtime/core/translate.js +526 -0
  35. package/runtime/core/webshim/api-router.js +592 -0
  36. package/runtime/core/webshim/bus.js +95 -0
  37. package/runtime/core/webshim/coding.js +82 -0
  38. package/runtime/core/webshim/data-fetch.js +66 -0
  39. package/runtime/core/webshim/exchange.js +288 -0
  40. package/runtime/core/webshim/framing.js +331 -0
  41. package/runtime/core/webshim/install.js +30 -0
  42. package/runtime/core/webshim/job-runner.js +319 -0
  43. package/runtime/core/webshim/native-bus.js +108 -0
  44. package/runtime/core/webshim/timers.js +82 -0
  45. package/runtime/core/webshim/trace.js +205 -0
  46. package/runtime/core/webshim/transport-base.js +128 -0
  47. package/runtime/core/webshim/variant-resolver.js +249 -0
  48. package/runtime/core/webshim/web-serial-bus.js +734 -0
  49. package/runtime/home/bmweb-home.ips +76 -0
  50. package/runtime/home/bmweb.h +26 -0
  51. package/runtime/screens/activations.js +258 -0
  52. package/runtime/screens/garage/diff.js +331 -0
  53. package/runtime/screens/garage/share.js +276 -0
  54. package/runtime/screens/garage/store.js +547 -0
  55. package/runtime/screens/ipo-runtime/cells.js +176 -0
  56. package/runtime/screens/ipo-runtime/dialogs.js +254 -0
  57. package/runtime/screens/ipo-runtime/home.js +358 -0
  58. package/runtime/screens/ipo-runtime/open.js +393 -0
  59. package/runtime/screens/ipo-runtime/paint-grid.js +106 -0
  60. package/runtime/screens/ipo-runtime/paint-modern.js +424 -0
  61. package/runtime/screens/ipo-runtime/print.js +281 -0
  62. package/runtime/screens/ipo-runtime/program.js +1337 -0
  63. package/runtime/screens/ipo-runtime/protocol.js +464 -0
  64. package/runtime/screens/ipo-runtime/script-scan.js +225 -0
  65. package/runtime/screens/ipo-runtime/translate-sets.js +130 -0
  66. package/runtime/screens/ipo-runtime/ui.js +249 -0
  67. package/runtime/screens/ipo-runtime/wire-policy.js +113 -0
  68. package/runtime/screens/ir.js +324 -0
  69. package/runtime/screens/search/data.js +153 -0
  70. package/runtime/screens/search/match.js +285 -0
  71. package/runtime/screens/search/open.js +66 -0
  72. package/runtime/vendor/fflate.min.js +1 -0
@@ -0,0 +1,464 @@
1
+ /**
2
+ * @file The whole-vehicle protocol as data. A script's "read every module"
3
+ * key puts FS_LESEN on the wire per group and writes a text file; the
4
+ * runtime also kept every answer. This joins those answers with the app's
5
+ * fault dictionaries (names, P-codes, the lookup), draws them the way the
6
+ * module error scan draws its result, and prints them like its report.
7
+ * INPA's own text stays one toggle away, and is what F9 prints below the
8
+ * table.
9
+ */
10
+
11
+ /** the fault-memory reads a whole-vehicle script sends per module */
12
+ const IPO_FAULT_READ_RE = /^(FS|IS)_LESEN$/;
13
+ /** the per-fault detail read the same script sends afterwards */
14
+ const IPO_FAULT_DETAIL_RE = /^(FS|IS)_LESEN_DETAIL$/;
15
+ /** the identification read the same script sends per module (F2 Ident) */
16
+ const IPO_IDENT_READ_RE = /^IDENT$/;
17
+ /** the module info read that precedes it (REVISION, ECU) */
18
+ const IPO_INFO_READ_RE = /^INFO$/;
19
+ /**
20
+ * The identification fields the script prints, in its order, with the
21
+ * caption each row gets. A pair of keys is one row (week / year).
22
+ * @type {Array<[string[], string]>}
23
+ */
24
+ const IPO_IDENT_ROWS = [
25
+ [['VARIANTE'], 'Variant'],
26
+ [['REVISION'], 'Version'],
27
+ [['ID_BMW_NR'], 'BMW part number'],
28
+ [['ID_HW_NR'], 'Hardware number'],
29
+ [['ID_SW_NR'], 'Software number'],
30
+ [['ID_LIEF_TEXT'], 'Supplier'],
31
+ [['ID_LIEF_NR'], 'Supplier number'],
32
+ [['ID_COD_INDEX'], 'Coding index'],
33
+ [['ID_VAR_INDEX'], 'Variant index'],
34
+ [['ID_DIAG_INDEX'], 'Diagnosis index'],
35
+ [['ID_BUS_INDEX'], 'Bus index'],
36
+ [['ID_DATUM_KW', 'ID_DATUM_JAHR'], 'Build date (week/year)'],
37
+ [['FG_NR'], 'Chassis number'],
38
+ [['COD_AE_INDEX'], 'Coding data change index'],
39
+ ];
40
+ /** the protocol's overview line: "MRS4 2 Airbag ..." / "D_009C * Cabrio ..." */
41
+ const IPO_PROTOCOL_OVERVIEW_RE = /^(\S+)\s+(\d+|\*)\s+(\S.*)$/;
42
+
43
+ /**
44
+ * @typedef {object} IpoWireRead
45
+ * @property {string} target - the SGBD the script named (a group, or a variant)
46
+ * @property {string} [variant] - the SGBD that answered (VARIANTE)
47
+ * @property {string} job
48
+ * @property {string|null} [arg]
49
+ * @property {object[]} [sets] - the job's data sets
50
+ * @property {string} [error] - the failure, when nothing answered
51
+ */
52
+
53
+ /**
54
+ * @typedef {object} IpoProtocolModule
55
+ * @property {string} sgbd - the variant that answered, lower-case
56
+ * @property {string} via - the group it was reached through
57
+ * @property {string} label - the module's name (the script's, or the app's)
58
+ * @property {object[]} codes - FS_LESEN entries, detail merged in
59
+ * @property {object} [ident] - the IDENT answer (and INFO's REVISION) for an ident read
60
+ */
61
+
62
+ /**
63
+ * @typedef {object} IpoProtocolReport
64
+ * @property {'faults'|'ident'} kind - what the read was: fault memories, or identifications
65
+ * @property {IpoProtocolModule[]} modules - every module that answered, script order
66
+ * @property {{target: string, label: string, error: string}[]} silent - addresses that did not
67
+ * @property {boolean} showText - the viewer is on INPA's text, not the table
68
+ */
69
+
70
+ /**
71
+ * Fold what the read put on the wire into one record per module.
72
+ * @param {IpoWireRead[]} reads - the body's wire log
73
+ * @param {string[]} [lines] - the protocol's text (its overview names the modules)
74
+ * @returns {IpoProtocolReport}
75
+ */
76
+ function ipoProtocolReport(reads, lines) {
77
+ const names = ipoProtocolNames(lines);
78
+ const modules = new Map();
79
+ const silent = new Map();
80
+ let kind = 'faults';
81
+ const first = (sets) => (sets || []).find((s) => Object.keys(s).length) || {};
82
+ /** INFO answers by module: the script asks INFO before IDENT */
83
+ const revisions = new Map();
84
+ for (const r of reads || []) {
85
+ const job = String(r.job || '').toUpperCase();
86
+ if (r.error) {
87
+ if (
88
+ (IPO_FAULT_READ_RE.test(job) || IPO_IDENT_READ_RE.test(job)) &&
89
+ !silent.has(r.target)
90
+ )
91
+ silent.set(r.target, r.error);
92
+ continue;
93
+ }
94
+ const sgbd = String(r.variant || r.target).toLowerCase();
95
+ if (IPO_IDENT_READ_RE.test(job)) {
96
+ // F2 Ident: one IDENT per group, its answer is the module's record
97
+ kind = 'ident';
98
+ silent.delete(r.target);
99
+ if (!modules.has(sgbd)) {
100
+ modules.set(sgbd, {
101
+ sgbd,
102
+ via: String(r.target).toLowerCase(),
103
+ label:
104
+ names.get(sgbd) ||
105
+ names.get(String(r.target).toLowerCase()) ||
106
+ sgbd,
107
+ codes: [],
108
+ ident: { REVISION: revisions.get(sgbd), ...first(r.sets) },
109
+ });
110
+ }
111
+ } else if (IPO_INFO_READ_RE.test(job)) {
112
+ // INFO runs first: its REVISION is the version the script prints
113
+ const rev = first(r.sets).REVISION;
114
+ if (rev != null) revisions.set(sgbd, rev);
115
+ const m = modules.get(sgbd);
116
+ if (m && m.ident && rev != null && m.ident.REVISION == null)
117
+ m.ident.REVISION = rev;
118
+ } else if (IPO_FAULT_READ_RE.test(job)) {
119
+ silent.delete(r.target);
120
+ const codes = (r.sets || []).filter((c) => c.F_HEX_CODE || c.F_ORT_NR);
121
+ const m = modules.get(sgbd);
122
+ if (!m) {
123
+ modules.set(sgbd, {
124
+ sgbd,
125
+ via: String(r.target).toLowerCase(),
126
+ label:
127
+ names.get(sgbd) ||
128
+ names.get(String(r.target).toLowerCase()) ||
129
+ sgbd,
130
+ codes,
131
+ });
132
+ } else if (codes.length && !m.codes.length) {
133
+ m.codes = codes;
134
+ }
135
+ } else if (IPO_FAULT_DETAIL_RE.test(job)) {
136
+ const m = modules.get(sgbd);
137
+ if (!m) continue;
138
+ const nr = ipoProtocolFaultNr(r.arg);
139
+ const code = m.codes.find((c) => Number(c.F_ORT_NR) === nr);
140
+ const det = code ? ipoProtocolMatchDetail(r.sets, nr) : null;
141
+ if (det) {
142
+ // the detail's own hex/text never replace the memory's (wire.js
143
+ // keeps them the same way)
144
+ const { F_HEX_CODE, F_ORT_TEXT, ...rich } = det;
145
+ Object.assign(code, rich);
146
+ }
147
+ }
148
+ }
149
+ // a group the script asked again by its variant name is one module
150
+ return {
151
+ kind,
152
+ modules: [...modules.values()],
153
+ silent: [...silent.entries()].map(([target, error]) => ({
154
+ target,
155
+ label: names.get(target) || target.toUpperCase(),
156
+ error,
157
+ })),
158
+ showText: false,
159
+ };
160
+ }
161
+
162
+ /**
163
+ * The detail set for one fault: the set that names the number, else -- when
164
+ * no set names any number -- the one carrying a P-code or hex (the same
165
+ * rule the module error scan applies, sweep/wire.js matchDetail).
166
+ * @param {object[]} sets - the detail job's data sets
167
+ * @param {number} nr - the fault number asked about
168
+ * @returns {object|null}
169
+ */
170
+ function ipoProtocolMatchDetail(sets, nr) {
171
+ const list = sets || [];
172
+ const named = list.find((s) => Number(s.F_ORT_NR) === nr);
173
+ if (named) return named;
174
+ if (list.some((s) => s.F_ORT_NR != null)) return null;
175
+ return list.find((s) => s.F_PCODE_STRING || s.F_HEX_CODE) || null;
176
+ }
177
+
178
+ /**
179
+ * The fault number a detail read was asked about ("0x1F", "31").
180
+ * @param {string|null|undefined} arg
181
+ * @returns {number}
182
+ */
183
+ function ipoProtocolFaultNr(arg) {
184
+ const s = String(arg == null ? '' : arg).trim();
185
+ return /^0x/i.test(s) ? parseInt(s.slice(2), 16) : parseInt(s, 10);
186
+ }
187
+
188
+ /**
189
+ * Module names from the protocol's overview table, keyed by the SGBD or
190
+ * group name in lower case.
191
+ * @param {string[]} [lines]
192
+ * @returns {Map<string, string>}
193
+ */
194
+ function ipoProtocolNames(lines) {
195
+ const names = new Map();
196
+ for (const l of lines || []) {
197
+ const m = IPO_PROTOCOL_OVERVIEW_RE.exec(String(l).trim());
198
+ if (m && /^[A-Za-z0-9_]+$/.test(m[1]))
199
+ names.set(m[1].toLowerCase(), m[3].trim());
200
+ }
201
+ return names;
202
+ }
203
+
204
+ /**
205
+ * Draw the report into the module view: the scan's rows (one per module,
206
+ * fault detail rows under those with faults, "no response" for the silent
207
+ * ones) with a toggle to INPA's own text.
208
+ * @param {HTMLElement} el - the screen area
209
+ * @param {object} p - the running program (its view carries the report)
210
+ * @returns {Promise<void>}
211
+ */
212
+ async function ipoProtocolRender(el, p) {
213
+ const view = p.view;
214
+ const rep = view && view.report;
215
+ if (!rep) return;
216
+ const withFaults = rep.modules.filter((m) => m.codes.length);
217
+ const total = withFaults.reduce((n, m) => n + m.codes.length, 0);
218
+ const head =
219
+ rep.kind === 'ident'
220
+ ? `${rep.modules.length} module${rep.modules.length === 1 ? '' : 's'} answered · ` +
221
+ `${rep.silent.length} no response`
222
+ : `${withFaults.length} module${withFaults.length === 1 ? '' : 's'} with faults · ` +
223
+ `${total} fault${total === 1 ? '' : 's'} · ` +
224
+ `${rep.modules.length} read · ${rep.silent.length} no response`;
225
+ // the bar (counts + the toggle) stays whichever side is showing
226
+ el.innerHTML =
227
+ `<div class="quick-sweep ipo-protocol-report">` +
228
+ `<div class="quick-bar"><div class="quick-head">${esc(head)}</div>` +
229
+ `<div class="quick-bar-btns"><button class="btn ipo-protocol-toggle"></button></div></div>` +
230
+ `<div class="quick-rows"></div>` +
231
+ `<pre class="ipo-protocol mono" hidden></pre></div>`;
232
+ const rowsEl = el.querySelector('.quick-rows');
233
+ const pre = el.querySelector('.ipo-protocol');
234
+ const toggle = el.querySelector('.ipo-protocol-toggle');
235
+ pre.textContent = (view.lines || []).join('\n');
236
+ const side = () => {
237
+ rowsEl.hidden = !!rep.showText;
238
+ pre.hidden = !rep.showText;
239
+ toggle.textContent = rep.showText ? 'Report' : 'INPA text';
240
+ };
241
+ toggle.onclick = () => {
242
+ rep.showText = !rep.showText;
243
+ side();
244
+ };
245
+ side();
246
+ if (typeof loadFaultDb === 'function') await loadFaultDb();
247
+ if (p.view !== view) return; // the script moved on while the DB loaded
248
+ for (const m of rep.modules) {
249
+ const row = addSweepRow(rowsEl, ipoText(m.label || m.sgbd));
250
+ if (rep.kind === 'ident') {
251
+ // identification: the module answered; its record under it
252
+ row.classList.add('clean');
253
+ row.querySelector('.quick-status').textContent = m.sgbd;
254
+ ipoProtocolIdentRows(row, m);
255
+ ipoProtocolBetterLabel(m, row, null);
256
+ continue;
257
+ }
258
+ if (!m.codes.length) {
259
+ row.classList.add('clean');
260
+ row.querySelector('.quick-status').textContent = 'OK';
261
+ } else {
262
+ // the scan's row: the count and a Clear that runs FS_LOESCHEN on the
263
+ // module and re-reads it (sweep/rows.js clearModule)
264
+ row.classList.add('has-faults');
265
+ const f = { row, codes: m.codes, ecu: { sgbd: m.sgbd, label: m.label } };
266
+ setRowFaultStatus(f);
267
+ appendFaultDetailRows(row, m.codes, m.sgbd);
268
+ ipoProtocolLinkLookup(row.nextElementSibling, m);
269
+ ipoProtocolBetterLabel(m, row, f);
270
+ continue;
271
+ }
272
+ ipoProtocolBetterLabel(m, row, null);
273
+ }
274
+ for (const s of rep.silent) {
275
+ const row = addSweepRow(rowsEl, ipoText(s.label));
276
+ setRowNoResponse(row, 'no response');
277
+ }
278
+ }
279
+
280
+ /**
281
+ * The identification record under a module's row: the fields the script
282
+ * prints, in its order, only those the module answered.
283
+ * @param {HTMLElement} row - the module's row
284
+ * @param {IpoProtocolModule} m
285
+ * @returns {void}
286
+ */
287
+ function ipoProtocolIdentRows(row, m) {
288
+ const id = m.ident || {};
289
+ const val = (keys) =>
290
+ keys
291
+ .map((k) => (id[k] == null ? '' : String(id[k]).trim()))
292
+ .filter((v) => v && !v.startsWith('_'))
293
+ .join(' / ');
294
+ const items = IPO_IDENT_ROWS.map(([keys, cap]) => [cap, val(keys)]).filter(
295
+ ([, v]) => v
296
+ );
297
+ if (!items.length) return;
298
+ const wrap = document.createElement('div');
299
+ wrap.className = 'quick-detail';
300
+ wrap.innerHTML = items
301
+ .map(
302
+ ([cap, v]) =>
303
+ `<div class="quick-detail-row"><span class="quick-detail-code">${esc(cap)}</span>` +
304
+ `<span class="quick-detail-name mono">${esc(v)}</span></div>`
305
+ )
306
+ .join('');
307
+ row.insertAdjacentElement('afterend', wrap);
308
+ }
309
+
310
+ /**
311
+ * The app's own name for the variant (the chassis config's), once it is
312
+ * known: the script's German label is what the row shows until then.
313
+ * @param {IpoProtocolModule} m
314
+ * @param {HTMLElement} row
315
+ * @param {object|null} f - the row's clear record, whose label follows
316
+ * @returns {void}
317
+ */
318
+ function ipoProtocolBetterLabel(m, row, f) {
319
+ if (typeof variantLabel !== 'function') return;
320
+ variantLabel(m.sgbd)
321
+ .then((name) => {
322
+ if (name && row.isConnected) {
323
+ m.label = name;
324
+ if (f) f.ecu.label = name;
325
+ setRowLabel(row, name, `${m.sgbd} via ${m.via}`);
326
+ }
327
+ })
328
+ .catch(() => {});
329
+ }
330
+
331
+ /**
332
+ * Each fault detail row opens the Fault Lookup on that code.
333
+ * @param {HTMLElement|null} detailEl - the rows appendFaultDetailRows added
334
+ * @param {IpoProtocolModule} m
335
+ * @returns {void}
336
+ */
337
+ function ipoProtocolLinkLookup(detailEl, m) {
338
+ if (!detailEl || typeof setDtcParam !== 'function') return;
339
+ const rows = detailEl.querySelectorAll('.quick-detail-row');
340
+ rows.forEach((row, i) => {
341
+ const c = m.codes[i];
342
+ if (!c) return;
343
+ const ff = faultFields(c, m.sgbd);
344
+ row.classList.add('ipo-protocol-link');
345
+ row.title = 'Open in Fault Lookup';
346
+ row.onclick = () => {
347
+ setDtcParam(hexText(c.F_HEX_CODE) || ff.code, m.sgbd, ff.name);
348
+ if (typeof showLookup === 'function') showLookup();
349
+ };
350
+ });
351
+ }
352
+
353
+ /**
354
+ * The printed sheet's sections for a protocol: the scan report's tables per
355
+ * module with faults, the silent addresses, then INPA's own text.
356
+ * @param {object} view - the program's view ({lines, report})
357
+ * @returns {object[]} print sections (core/print.js)
358
+ */
359
+ function ipoProtocolPrintSections(view) {
360
+ const rep = view.report;
361
+ const sections = [];
362
+ const text = {
363
+ html: `<pre class="pr-screen">${esc((view.lines || []).join('\n'))}</pre>`,
364
+ };
365
+ if (rep.kind === 'ident') return ipoProtocolIdentPrint(rep, text);
366
+ // the scan report's table helpers (sweep/report.js) draw the tables; a
367
+ // page without them still prints INPA's text
368
+ if (
369
+ typeof faultColumns !== 'function' ||
370
+ typeof printFaultTable !== 'function'
371
+ )
372
+ return [text];
373
+ const withFaults = rep.modules.filter((m) => m.codes.length);
374
+ const columns = faultColumns();
375
+ if (!withFaults.length) {
376
+ sections.push(
377
+ printHtml(
378
+ `<p class="pr-p">No stored faults. ${rep.modules.length} module` +
379
+ `${rep.modules.length === 1 ? '' : 's'} read, ${rep.silent.length} did not answer.</p>`
380
+ )
381
+ );
382
+ }
383
+ for (const m of withFaults) {
384
+ sections.push(
385
+ printHeading(
386
+ `${ipoText(m.label)} · ${m.sgbd} · ` +
387
+ `${m.codes.length} fault${m.codes.length === 1 ? '' : 's'}`
388
+ )
389
+ );
390
+ sections.push({
391
+ html: printFaultTable(
392
+ { ecu: { sgbd: m.sgbd, label: ipoText(m.label) }, codes: m.codes },
393
+ columns
394
+ ),
395
+ });
396
+ }
397
+ if (rep.silent.length) {
398
+ sections.push(
399
+ printHtml(
400
+ `<p class="pr-p">No response: ${esc(
401
+ rep.silent.map((s) => ipoText(s.label)).join(', ')
402
+ )}</p>`
403
+ )
404
+ );
405
+ }
406
+ sections.push(printHeading("INPA's protocol"));
407
+ sections.push(text);
408
+ return sections;
409
+ }
410
+
411
+ /**
412
+ * The printed sheet for an identification read: one table of the modules
413
+ * that answered (part, hardware and software numbers, build week), the
414
+ * silent addresses, then INPA's text.
415
+ * @param {IpoProtocolReport} rep
416
+ * @param {object} text - the section with INPA's text
417
+ * @returns {object[]} print sections (core/print.js)
418
+ */
419
+ function ipoProtocolIdentPrint(rep, text) {
420
+ if (typeof printTable !== 'function') return [text];
421
+ const sections = [];
422
+ const t = printTable(
423
+ ['Module', 'SGBD', 'Part no.', 'HW', 'SW', 'Build wk/yr'],
424
+ rep.modules.map((m) => {
425
+ const id = m.ident || {};
426
+ const v = (k) => (id[k] == null ? '—' : String(id[k]));
427
+ return [
428
+ ipoText(m.label),
429
+ m.sgbd,
430
+ v('ID_BMW_NR'),
431
+ v('ID_HW_NR'),
432
+ v('ID_SW_NR'),
433
+ `${v('ID_DATUM_KW')}/${v('ID_DATUM_JAHR')}`,
434
+ ];
435
+ }),
436
+ ['', 'pr-code2', 'pr-code2', '', '', '']
437
+ );
438
+ t.avoidBreak = false;
439
+ sections.push(
440
+ printHeading(
441
+ `${rep.modules.length} module${rep.modules.length === 1 ? '' : 's'} answered`
442
+ ),
443
+ t
444
+ );
445
+ if (rep.silent.length)
446
+ sections.push(
447
+ printHtml(
448
+ `<p class="pr-p">No response: ${esc(
449
+ rep.silent.map((s) => ipoText(s.label)).join(', ')
450
+ )}</p>`
451
+ )
452
+ );
453
+ sections.push(printHeading("INPA's protocol"), text);
454
+ return sections;
455
+ }
456
+
457
+ if (typeof module !== 'undefined' && module.exports) {
458
+ module.exports = {
459
+ ipoProtocolReport,
460
+ ipoProtocolNames,
461
+ ipoProtocolFaultNr,
462
+ ipoProtocolPrintSections,
463
+ };
464
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * @file Static scans over a decoded script (the exec's procs), used by the
3
+ * runtime without executing anything: a menu's ITEM keys and prologue, the
4
+ * backdrop screen a menu is normally shown with, and a screen's component
5
+ * and logical-line declarations.
6
+ *
7
+ * Live .IPO runtime: the module view IS the running script. The derived IR
8
+ * (data/inpa-ir) approximated what a script would do and was corrected by
9
+ * heuristics; each heuristic was a place the guess differed from a run. This
10
+ * runtime runs the script instead: `inpainit` names the root menu and
11
+ * screen, a menu's ITEMs are the keys, a key press runs its body in the SAME
12
+ * VM the previous press left behind, a screen's LINE blocks paint cells and
13
+ * send their own jobs (keep-alives included), and a frequent screen re-runs
14
+ * its cycle on a timer -- INPA's own INIT -> LINE -> EXIT tick. Nothing
15
+ * above the VM decides what a key does.
16
+ */
17
+
18
+ /** F-key numbers above this are the shifted bank (Shift+F1 = 11). */
19
+ const IPO_SHIFT_BASE = 10;
20
+
21
+ /**
22
+ * One key of a menu: an ITEM token and where its body lies in the proc.
23
+ * @typedef {object} IpoMenuItem
24
+ * @property {number} nr - F-key number (11..20 = shifted)
25
+ * @property {string} label - the caption as the script wrote it
26
+ * @property {boolean} shift - a Shift+F key
27
+ * @property {boolean} hidden - no caption: INPA's bar shows it blank
28
+ * @property {number} start - first token index of the body
29
+ * @property {number} end - token index the body ends at (exclusive)
30
+ * @property {string} [legendLabel] - the caption the screen's legend gave a hidden key
31
+ */
32
+
33
+ /**
34
+ * ITEM tokens of a menu proc, in proc order.
35
+ * @param {object} exec - the decoded script ({procs, byid})
36
+ * @param {string} menuName - the menu proc
37
+ * @returns {IpoMenuItem[]}
38
+ */
39
+ function ipoMenuItems(exec, menuName) {
40
+ const toks = exec && exec.procs && exec.procs[menuName];
41
+ if (!Array.isArray(toks)) return [];
42
+ const out = [];
43
+ for (let i = 0; i < toks.length; i++) {
44
+ const t = toks[i];
45
+ if (t.op !== 'ITEM') continue;
46
+ let end = toks.length;
47
+ for (let j = i + 1; j < toks.length; j++) {
48
+ if (toks[j].op === 'ITEM' || toks[j].op === 'unk') {
49
+ end = j;
50
+ break;
51
+ }
52
+ }
53
+ out.push({
54
+ nr: t.nr,
55
+ label: t.label || '',
56
+ shift: t.nr > IPO_SHIFT_BASE,
57
+ hidden: !String(t.label || '').trim(),
58
+ start: i + 1,
59
+ end,
60
+ });
61
+ }
62
+ return out;
63
+ }
64
+
65
+ /**
66
+ * Where a menu proc's prologue ends (the first ITEM), or the proc end.
67
+ * @param {object} exec - the decoded script
68
+ * @param {string} menuName - the menu proc
69
+ * @returns {number} token index
70
+ */
71
+ function ipoPrologueEnd(exec, menuName) {
72
+ const toks = exec && exec.procs && exec.procs[menuName];
73
+ if (!Array.isArray(toks)) return 0;
74
+ const i = toks.findIndex((t) => t.op === 'ITEM');
75
+ return i < 0 ? toks.length : i;
76
+ }
77
+
78
+ /**
79
+ * The setscreen call in an ITEM body, walking back from a token: the screen
80
+ * ref and the frequent flag pushed before the call.
81
+ * @param {IpoToken[]} toks - the proc's tokens
82
+ * @param {number} j - index of the setscreen call
83
+ * @param {Record<string, string>} byid - the exec's byid table
84
+ * @returns {{screen: string, frequent: boolean}|null}
85
+ */
86
+ function ipoSetscreenArgs(toks, j, byid) {
87
+ let ref = null,
88
+ flag = false;
89
+ for (let k = j - 1; k >= 0 && toks[k].op !== 'frame'; k--) {
90
+ if (toks[k].op === 'procref' && toks[k].kind === IPO_REF_SCREEN)
91
+ ref = toks[k];
92
+ if (toks[k].op === 'const' && (toks[k].t === 'b' || toks[k].t === 'i'))
93
+ flag = !!toks[k].v;
94
+ }
95
+ const scr = ref && byid[`screen:${ref.n}`];
96
+ return scr ? { screen: scr, frequent: flag } : null;
97
+ }
98
+
99
+ /**
100
+ * The menu whose prologue shows a screen: the inverse of ipoScreenForMenu,
101
+ * for a link that names a screen but no menu (a search result for the
102
+ * screen itself). The screen's keys are the owning menu's, so landing on the
103
+ * screen under any other menu puts the wrong jobs on the F-keys.
104
+ * @param {object} exec - the decoded script
105
+ * @param {string} screen - the screen proc name
106
+ * @returns {string|null} the menu name, or null when no menu shows it
107
+ */
108
+ function ipoMenuForScreen(exec, screen) {
109
+ const byid = (exec && exec.byid) || {};
110
+ for (const [k, name] of Object.entries(byid)) {
111
+ if (!k.startsWith('menu:')) continue;
112
+ const shown = ipoScreenForMenu(exec, name); // {screen, frequent} or null
113
+ if (shown && shown.screen === screen) return name;
114
+ }
115
+ return null;
116
+ }
117
+
118
+ /**
119
+ * The backdrop a menu is normally shown with: the setscreen the key that
120
+ * opens it performs right before its setmenu. A deep link (the URL route)
121
+ * lands on the menu without pressing that key, so its screen is looked up
122
+ * here rather than left as whatever was current.
123
+ * @param {object} exec - the decoded script
124
+ * @param {string} menuName - the menu proc
125
+ * @returns {{screen: string, frequent: boolean}|null}
126
+ */
127
+ function ipoScreenForMenu(exec, menuName) {
128
+ const byid = (exec && exec.byid) || {};
129
+ const menuId = Object.entries(byid).find(
130
+ ([k, v]) => k.startsWith('menu:') && v === menuName
131
+ );
132
+ if (!menuId) return null;
133
+ const mid = Number(menuId[0].split(':')[1]);
134
+ for (const toks of Object.values(exec.procs || {})) {
135
+ for (let i = 0; i < toks.length; i++) {
136
+ const t = toks[i];
137
+ if (!(t.op === 'procref' && t.kind === IPO_REF_MENU && t.n === mid))
138
+ continue;
139
+ const nxt = toks[i + 1];
140
+ if (!(nxt && nxt.op === 'call' && nxt.name === 'setmenu')) continue;
141
+ // walk back within the same ITEM body for a setscreen
142
+ for (let j = i - 1; j >= 0 && toks[j].op !== 'ITEM'; j--) {
143
+ const c = toks[j];
144
+ if (c.op === 'call' && c.name === 'setscreen') {
145
+ const found = ipoSetscreenArgs(toks, j, byid);
146
+ if (found) return found;
147
+ }
148
+ }
149
+ }
150
+ }
151
+ return null;
152
+ }
153
+
154
+ /**
155
+ * The LINE declarations of a screen that carry a component key: INPA's
156
+ * togglelist offers exactly these.
157
+ * @param {object} exec - the decoded script
158
+ * @param {string|null} screen - the screen proc
159
+ * @returns {Array<{label: string, keys: string}>}
160
+ */
161
+ function ipoScreenComponents(exec, screen) {
162
+ const toks = exec && exec.procs && screen ? exec.procs[screen] : null;
163
+ if (!toks) return [];
164
+ return toks
165
+ .filter((t) => t.op === 'LINE' && t.keys)
166
+ .map((t) => ({ label: t.label || '', keys: String(t.keys) }));
167
+ }
168
+
169
+ /**
170
+ * The named logical lines of a screen: what INPA's Select offers.
171
+ * @param {object} exec - the decoded script
172
+ * @param {string|null} screen - the screen proc
173
+ * @returns {string[]}
174
+ */
175
+ function ipoScreenLineNames(exec, screen) {
176
+ const toks = exec && exec.procs && screen ? exec.procs[screen] : null;
177
+ if (!toks) return [];
178
+ const out = [];
179
+ for (const t of toks) {
180
+ if (t.op === 'LINE' && t.label && String(t.label).trim())
181
+ if (!out.includes(t.label)) out.push(t.label);
182
+ }
183
+ return out;
184
+ }
185
+
186
+ /**
187
+ * The keys of a menu whose body sets a screen that has named lines: what
188
+ * Select can filter once that key is pressed. Shown in the Select box when
189
+ * the current screen has nothing to choose from.
190
+ * @param {object} exec - the decoded script
191
+ * @param {string} menuName - the menu proc
192
+ * @param {IpoMenuItem[]} items - the menu's keys (ipoMenuItems)
193
+ * @returns {Array<{nr: number, shift: boolean, screen: string, lines: number}>}
194
+ */
195
+ function ipoSelectableKeys(exec, menuName, items) {
196
+ const toks = exec && exec.procs && exec.procs[menuName];
197
+ if (!Array.isArray(toks)) return [];
198
+ const byid = exec.byid || {};
199
+ const out = [];
200
+ for (const it of items || []) {
201
+ let screen = null;
202
+ for (let j = it.start; j < it.end; j++) {
203
+ if (toks[j].op === 'call' && toks[j].name === 'setscreen') {
204
+ const a = ipoSetscreenArgs(toks, j, byid);
205
+ if (a) screen = a.screen;
206
+ }
207
+ }
208
+ if (!screen) continue;
209
+ const lines = ipoScreenLineNames(exec, screen).length;
210
+ if (lines) out.push({ nr: it.nr, shift: !!it.shift, screen, lines });
211
+ }
212
+ return out;
213
+ }
214
+
215
+ if (typeof module !== 'undefined' && module.exports) {
216
+ module.exports = {
217
+ IPO_SHIFT_BASE,
218
+ ipoSelectableKeys,
219
+ ipoMenuItems,
220
+ ipoPrologueEnd,
221
+ ipoScreenForMenu,
222
+ ipoScreenComponents,
223
+ ipoScreenLineNames,
224
+ };
225
+ }