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
|
@@ -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
|
+
}
|