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,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file ENGLISH FOR WHAT A JOB RETURNS. The script reads F_ORT_TEXT,
|
|
3
|
+
* F_SYMPTOM_TEXT, F_READY_TEXT... and glues them into its own lines before
|
|
4
|
+
* it paints, so by paint time there is one string nobody can split; the swap
|
|
5
|
+
* has to happen on the result values as they are fed to the VM. Exact
|
|
6
|
+
* dictionary lookups only (phraseText: the fault phrase tables and the
|
|
7
|
+
* generated fault-location dictionary), plus the fault-code lookup the fault
|
|
8
|
+
* screen uses for a location whose text has no entry but whose number the
|
|
9
|
+
* ECU's own codespace knows. A string without an entry is fed exactly as the
|
|
10
|
+
* ECU sent it; Original mode feeds everything untouched. Captions the .IPO
|
|
11
|
+
* prints itself are not this: irLabel handles those at paint.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** A P-code text: "P1128 " + the fault location + " - " + the fault type. */
|
|
15
|
+
const IPO_PCODE_RE = /^(P[0-9A-F]{4}\s+)([\s\S]*)$/i;
|
|
16
|
+
|
|
17
|
+
/** The result keys of a fault's freeze-frame (Umwelt) labels and values. */
|
|
18
|
+
const IPO_ENV_TEXT_KEY_RE = /^F_(UW|FF)\d*_TEXT$/;
|
|
19
|
+
const IPO_ENV_VALUE_KEY_RE = /^F_(UW|FF)\d*_WERT$/;
|
|
20
|
+
|
|
21
|
+
/** The result keys of a fault's P-code text. */
|
|
22
|
+
const IPO_PCODE_KEY_RE = /^F_PCODE7?_TEXT$/;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether fed results are translated: the dictionaries are loaded and the
|
|
26
|
+
* language setting is not Original.
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
function ipoTranslating() {
|
|
30
|
+
return (
|
|
31
|
+
typeof phraseText === 'function' &&
|
|
32
|
+
typeof lang === 'function' &&
|
|
33
|
+
lang() !== 'orig'
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* F_ORT_TEXT: the phrase dictionary, else the code. The code is the one the
|
|
39
|
+
* text leads with ("27DA BSD-Generator", kept as a prefix because the ECU
|
|
40
|
+
* wrote it there) or F_HEX_CODE, else the 16-bit F_ORT_NR when a codespace
|
|
41
|
+
* knows it whole (a DTC, not a location+detail word). The ECU's own
|
|
42
|
+
* codespace outranks the flat DB: 27C3 is the oil-level sensor on the E46
|
|
43
|
+
* MS45 and something else elsewhere.
|
|
44
|
+
* @param {Record<string, string>} set - the result set the text belongs to
|
|
45
|
+
* @param {string} text - the F_ORT_TEXT value
|
|
46
|
+
* @param {string} sgbd - the SGBD that answered (its codespace)
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
function ipoLocationText(set, text, sgbd) {
|
|
50
|
+
const hit = phraseText(text);
|
|
51
|
+
if (hit !== text) return hit;
|
|
52
|
+
const own = typeof scopedFaultDb === 'function' ? scopedFaultDb(sgbd) : null;
|
|
53
|
+
const flat = (typeof window !== 'undefined' && window.BMW_FAULT_DB) || null;
|
|
54
|
+
if (!own && !flat) return text;
|
|
55
|
+
const codes = [];
|
|
56
|
+
const led =
|
|
57
|
+
typeof bmwCode === 'function' ? bmwCode(text, set.F_HEX_CODE) : null;
|
|
58
|
+
if (led) codes.push({ code: led, flatOk: true });
|
|
59
|
+
const full = typeof ortNrFull === 'function' ? ortNrFull(set.F_ORT_NR) : null;
|
|
60
|
+
if (full) codes.push({ code: full, flatOk: !own });
|
|
61
|
+
for (const { code, flatOk } of codes) {
|
|
62
|
+
const name = (own && own[code]) || (flatOk && flat && flat[code]) || null;
|
|
63
|
+
if (!name) continue;
|
|
64
|
+
return new RegExp(`^${code}\\b`, 'i').test(String(text).trim())
|
|
65
|
+
? `${code} ${name}`
|
|
66
|
+
: name;
|
|
67
|
+
}
|
|
68
|
+
return text;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* F_PCODE_TEXT, translated part by part: the code prefix is kept, the fault
|
|
73
|
+
* location (FO) and the fault type (FA) are each a dictionary string of
|
|
74
|
+
* their own; a part with no entry stays German.
|
|
75
|
+
* @param {string} v - the value
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
function ipoPcodeText(v) {
|
|
79
|
+
const whole = phraseText(v);
|
|
80
|
+
if (whole !== v) return whole;
|
|
81
|
+
const m = String(v).match(IPO_PCODE_RE);
|
|
82
|
+
const code = m ? m[1] : '';
|
|
83
|
+
const rest = m ? m[2] : String(v);
|
|
84
|
+
const parts = rest.split(' - ');
|
|
85
|
+
if (parts.length < 2) return v;
|
|
86
|
+
const en = parts.map((x) => phraseText(x.trim()));
|
|
87
|
+
if (en.every((x, i) => x === parts[i].trim())) return v;
|
|
88
|
+
return code + en.join(' - ');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Freeze-frame (Umwelt) labels and their enum values have their own curated
|
|
93
|
+
* dictionary (envLabel over envmap.js): "(Motor) - Öltemperatur" -> "Engine
|
|
94
|
+
* oil temperature", "0 ES - Motor steht" -> "0 ES - engine stopped".
|
|
95
|
+
* @param {string} v - the value
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
function ipoEnvText(v) {
|
|
99
|
+
const e = typeof envLabel === 'function' ? envLabel(v) : v;
|
|
100
|
+
return e !== v ? e : phraseText(v);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* One result set with its *_TEXT strings in English where a dictionary
|
|
105
|
+
* carries them; every other value, and every string without an entry, as
|
|
106
|
+
* sent. A new object: the wire's answer is not rewritten in place.
|
|
107
|
+
* @param {Record<string, *>} set - a result set
|
|
108
|
+
* @param {string} sgbd - the SGBD that answered
|
|
109
|
+
* @returns {Record<string, *>}
|
|
110
|
+
*/
|
|
111
|
+
function ipoTranslateSet(set, sgbd) {
|
|
112
|
+
const out = {};
|
|
113
|
+
for (const [k, v] of Object.entries(set || {})) {
|
|
114
|
+
let t = v;
|
|
115
|
+
if (typeof v === 'string') {
|
|
116
|
+
if (k === 'F_ORT_TEXT') t = ipoLocationText(set, v, sgbd);
|
|
117
|
+
else if (IPO_PCODE_KEY_RE.test(k)) t = ipoPcodeText(v);
|
|
118
|
+
else if (IPO_ENV_TEXT_KEY_RE.test(k)) t = ipoEnvText(v);
|
|
119
|
+
else if (IPO_ENV_VALUE_KEY_RE.test(k) && /[A-Za-z]/.test(v))
|
|
120
|
+
t = ipoEnvText(v); // an enum value, not a number
|
|
121
|
+
else if (/_TEXT$/.test(k)) t = ipoEnvText(v); // MESSWERTBLOCK rows carry FUMWELTTEXTE labels
|
|
122
|
+
}
|
|
123
|
+
out[k] = t;
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
129
|
+
module.exports = { ipoTranslating, ipoTranslateSet };
|
|
130
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The UI adapter: maps the program's state onto the app's grammar --
|
|
3
|
+
* the F-key bar, a title, the painted grid (INPA mode) or per-LINE rows
|
|
4
|
+
* (modern), the app's own dialogs for INPA's prompts, and the status line.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The Back action for a menu without its own F10 (kind 'back' answers Esc).
|
|
9
|
+
* @param {() => void} fn - what Back does
|
|
10
|
+
* @returns {object} an action-bar entry
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* The caption over the key bar: the running script's menu title
|
|
14
|
+
* (setmenutitle), the way INPA shows it; empty restores "Select menu".
|
|
15
|
+
* @param {string} title
|
|
16
|
+
* @returns {void}
|
|
17
|
+
*/
|
|
18
|
+
function ipoSetKeysCaption(title) {
|
|
19
|
+
if (typeof document === 'undefined') return;
|
|
20
|
+
const st = document.documentElement.style;
|
|
21
|
+
if (title && title.trim())
|
|
22
|
+
st.setProperty('--fkeys-caption', JSON.stringify(title));
|
|
23
|
+
else st.removeProperty('--fkeys-caption');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ipoBackAction(fn) {
|
|
27
|
+
return { key: 'Escape', keyLabel: 'Esc', label: 'Back', kind: 'back', fn };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build the UI adapter for a module's container.
|
|
32
|
+
* @param {object} ecu - the module (sgbd, label, chassis)
|
|
33
|
+
* @param {HTMLElement} container - where the view is drawn
|
|
34
|
+
* @param {() => void} back - leave the module view
|
|
35
|
+
* @returns {IpoUi}
|
|
36
|
+
*/
|
|
37
|
+
function ipoMakeUi(ecu, container, back) {
|
|
38
|
+
const inpa = typeof inpaMode === 'function' && inpaMode();
|
|
39
|
+
let gridEl = null,
|
|
40
|
+
statusEl = null,
|
|
41
|
+
machineEl = null;
|
|
42
|
+
/** INPA's progress window while a body has one open (progressDialog) */
|
|
43
|
+
let progress = null;
|
|
44
|
+
/** the viewopen file currently in the DOM (paint keeps it across cycles) */
|
|
45
|
+
let paintedView = null;
|
|
46
|
+
|
|
47
|
+
const build = () => {
|
|
48
|
+
container.className = inpa ? 'ipo-view ipo-inpa' : 'ipo-view results-panel';
|
|
49
|
+
container.innerHTML =
|
|
50
|
+
`<div class="ipo-screen"></div>` +
|
|
51
|
+
`<div class="ipo-machine" hidden></div>` +
|
|
52
|
+
`<div class="ipo-status mono"></div>`;
|
|
53
|
+
gridEl = container.querySelector('.ipo-screen');
|
|
54
|
+
machineEl = container.querySelector('.ipo-machine');
|
|
55
|
+
statusEl = container.querySelector('.ipo-status');
|
|
56
|
+
};
|
|
57
|
+
build();
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The F-key bar: the script's plain keys, its shifted bank, and the
|
|
61
|
+
* script's own F10 as Back (kind 'back' answers Esc too). Only a menu
|
|
62
|
+
* WITHOUT one gets the app's Esc Back -- adding it beside an F10 spilled
|
|
63
|
+
* it into the first empty slot, and SM46's captionless F3 (read coding
|
|
64
|
+
* data) read "Back".
|
|
65
|
+
* @param {IpoProgram} p - the program
|
|
66
|
+
* @returns {void}
|
|
67
|
+
*/
|
|
68
|
+
const renderKeys = (p) => {
|
|
69
|
+
const shown = (it) => !it.hidden || !!it.legendLabel;
|
|
70
|
+
const plain = p.items.filter((it) => !it.shift && shown(it));
|
|
71
|
+
const shifted = p.items.filter((it) => it.shift && shown(it));
|
|
72
|
+
const asAction = (it) => {
|
|
73
|
+
const n = it.shift ? it.nr - IPO_SHIFT_BASE : it.nr;
|
|
74
|
+
return {
|
|
75
|
+
key: n === 20 ? null : String(n % 10),
|
|
76
|
+
keyLabel: it.shift ? `⇧F${it.nr - IPO_SHIFT_BASE}` : `F${it.nr}`,
|
|
77
|
+
label: ipoKeyLabel(p, it),
|
|
78
|
+
kind: it.nr === IPO_BACK_KEY ? 'back' : undefined,
|
|
79
|
+
fn: () => p.press(it.nr),
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
const acts = plain.map(asAction);
|
|
83
|
+
if (!plain.some((it) => it.nr === IPO_BACK_KEY)) {
|
|
84
|
+
acts.push(ipoBackAction(() => p.back()));
|
|
85
|
+
}
|
|
86
|
+
const put = () =>
|
|
87
|
+
setActions(acts, shifted.length ? shifted.map(asAction) : undefined);
|
|
88
|
+
if (typeof setActions === 'function') {
|
|
89
|
+
if (typeof keepActivationsDuring === 'function')
|
|
90
|
+
keepActivationsDuring(put);
|
|
91
|
+
else put();
|
|
92
|
+
}
|
|
93
|
+
ipoSetKeysCaption(ipoText(p.title || ''));
|
|
94
|
+
if (machineEl) machineEl.hidden = true;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** @type {IpoUi} */
|
|
98
|
+
const ui = {
|
|
99
|
+
// the bus's worker-backed timer: a hidden tab's setTimeout is throttled
|
|
100
|
+
// to once a second, which stretched every scripted wait and screen tick
|
|
101
|
+
sleep: (ms) =>
|
|
102
|
+
typeof bmwSleep === 'function'
|
|
103
|
+
? bmwSleep(ms)
|
|
104
|
+
: new Promise((r) => setTimeout(r, ms)),
|
|
105
|
+
loadExec: async (sgbd) => {
|
|
106
|
+
try {
|
|
107
|
+
return typeof irLiveExec === 'function' ? await irLiveExec(sgbd) : null;
|
|
108
|
+
} catch (e) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
route: (p) => {
|
|
113
|
+
if (typeof routeSetCar === 'function' && ecu.chassis && ecu.sgbd) {
|
|
114
|
+
const rootMenu = p.rootMenu || (p.rootMenu = p.menu);
|
|
115
|
+
routeSetCar(ecu.chassis, ecu.sgbd, p.menu === rootMenu ? null : p.menu);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
status: (p, text) => {
|
|
119
|
+
if (statusEl) statusEl.textContent = text;
|
|
120
|
+
sbLeft.textContent = `${(p.ecu || ecu).sgbd}.prg · ${p.menu || ''} · ${text}`;
|
|
121
|
+
},
|
|
122
|
+
error: (p, text) => {
|
|
123
|
+
if (statusEl) statusEl.textContent = `error: ${text}`;
|
|
124
|
+
sbLeft.textContent = `${ecu.sgbd}.prg · ${text}`;
|
|
125
|
+
},
|
|
126
|
+
message: (title, body) =>
|
|
127
|
+
messageDialog({
|
|
128
|
+
title: esc(ipoText(title)),
|
|
129
|
+
body: esc(ipoText(body || '')),
|
|
130
|
+
}),
|
|
131
|
+
askInput: (step, label) => irAskInput(step, label),
|
|
132
|
+
confirmKey: (p, it, jobs, writes) =>
|
|
133
|
+
confirmDialog({
|
|
134
|
+
title: `${esc(ipoKeyLabel(p, it))} on ${esc(ecu.label)}?`,
|
|
135
|
+
body:
|
|
136
|
+
`Runs INPA's own key script live; it can send ` +
|
|
137
|
+
`<span class="mono">${jobs.map(esc).join(' · ')}</span>.` +
|
|
138
|
+
`<br><br>The argument is computed by the script, exactly as INPA ` +
|
|
139
|
+
`computes it.`,
|
|
140
|
+
confirmLabel: 'Run',
|
|
141
|
+
danger: writes.length > 0,
|
|
142
|
+
}),
|
|
143
|
+
confirmWrite: (p, job, arg, ctx) =>
|
|
144
|
+
confirmDialog({
|
|
145
|
+
title: `Send ${esc(job)} on ${esc(ecu.label)}?`,
|
|
146
|
+
body:
|
|
147
|
+
`The ${esc(ctx && ctx.label ? ctx.label : 'script')} wants to send ` +
|
|
148
|
+
`<span class="mono">${esc(job)}${arg ? ' ' + esc(arg) : ''}</span>` +
|
|
149
|
+
` to the module.` +
|
|
150
|
+
(ctx && String(ctx.scope || '').startsWith('screen:')
|
|
151
|
+
? ` This screen sends it on every refresh; confirming allows it ` +
|
|
152
|
+
`for as long as the screen is open.`
|
|
153
|
+
: ''),
|
|
154
|
+
confirmLabel: 'Send',
|
|
155
|
+
danger: true,
|
|
156
|
+
}),
|
|
157
|
+
pickComponent: (p, step) => ipoPickComponent(p, step),
|
|
158
|
+
// BMWeb's own picker (the home script): the host's lists in a dialog
|
|
159
|
+
pickHome: (p, step) => ipoPickHome(p, step),
|
|
160
|
+
// INPA's save-as dialog: the browser's own picker where it has one
|
|
161
|
+
// (Chrome, Edge), else a name for a download
|
|
162
|
+
saveFile: (p, step) => ipoSaveFilePick(p, step),
|
|
163
|
+
writeFile: (p, picked, lines) => ipoSaveFileWrite(picked, lines),
|
|
164
|
+
pickLines: (p, names, multiple, current, hints) =>
|
|
165
|
+
ipoPickLines(names, multiple, current, hints),
|
|
166
|
+
// INPA's printscreen: the module view as a clean sheet (print.js)
|
|
167
|
+
printScreen: (p) => ipoPrintScreen(p, p.ecu || ecu, inpa),
|
|
168
|
+
resolveScriptEcu: (from, script, exec) =>
|
|
169
|
+
// the home script names a module by its SGBD after a chassis pick:
|
|
170
|
+
// that module is the car's own record, not a wire-resolved variant
|
|
171
|
+
from && from.sgbd === IPO_HOME_SGBD
|
|
172
|
+
? ipoHomeEcuFor(from, script)
|
|
173
|
+
: ipoResolveScriptEcu(from, script, exec),
|
|
174
|
+
machineTick: (p, step, guards) => ipoMachineTick(machineEl, step, guards),
|
|
175
|
+
// INPA's progress window: a popup with the title, the line the script
|
|
176
|
+
// wrote last (the module it is asking right now) and Cancel, which ends
|
|
177
|
+
// the body between two jobs
|
|
178
|
+
userbox: (p, box) => {
|
|
179
|
+
if (!box) {
|
|
180
|
+
if (progress) progress.close();
|
|
181
|
+
progress = null;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const lines = box.lines || [];
|
|
185
|
+
const text = lines.length ? ipoText(lines[lines.length - 1]) : '';
|
|
186
|
+
if (!progress) {
|
|
187
|
+
progress = progressDialog({
|
|
188
|
+
title: esc(ipoText(box.title || '')),
|
|
189
|
+
text,
|
|
190
|
+
onCancel: () => {
|
|
191
|
+
progress = null;
|
|
192
|
+
p.cancel();
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
} else {
|
|
196
|
+
progress.update(text);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
renderKeys,
|
|
200
|
+
paint: (p) => {
|
|
201
|
+
if (!gridEl) return;
|
|
202
|
+
ipoSetKeysCaption(ipoText(p.title || ''));
|
|
203
|
+
// viewopen: INPA's viewer window with the file the script wrote. The
|
|
204
|
+
// menu's screen cycle keeps painting behind it; the same file stays
|
|
205
|
+
// in the DOM so the reader's scroll position survives each cycle.
|
|
206
|
+
if (p.view) {
|
|
207
|
+
if (p.view !== paintedView) {
|
|
208
|
+
paintedView = p.view;
|
|
209
|
+
if (p.view.report && typeof ipoProtocolRender === 'function') {
|
|
210
|
+
const drawn = p.view;
|
|
211
|
+
// the whole-car read is finished: show what each fault captured,
|
|
212
|
+
// then offer to keep the report. Deferred until the renderer has
|
|
213
|
+
// built the rows and the bar these attach to.
|
|
214
|
+
Promise.resolve(ipoProtocolRender(gridEl, p)).then(async () => {
|
|
215
|
+
if (p.view !== drawn) return;
|
|
216
|
+
if (typeof garageAttachEnv === 'function')
|
|
217
|
+
await garageAttachEnv(gridEl, drawn.report, {
|
|
218
|
+
screensFor:
|
|
219
|
+
typeof garageScreensFor === 'function'
|
|
220
|
+
? garageScreensFor
|
|
221
|
+
: null,
|
|
222
|
+
});
|
|
223
|
+
if (p.view === drawn && typeof garageOfferSave === 'function')
|
|
224
|
+
garageOfferSave(gridEl, drawn, p.ecu);
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
gridEl.innerHTML = `<pre class="ipo-protocol mono">${esc(
|
|
228
|
+
(p.view.lines || []).join('\n')
|
|
229
|
+
)}</pre>`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
paintedView = null;
|
|
235
|
+
if (inpa) ipoPaintGrid(gridEl, p);
|
|
236
|
+
else ipoPaintLines(gridEl, p);
|
|
237
|
+
},
|
|
238
|
+
left: () => {
|
|
239
|
+
ipoSetKeysCaption('');
|
|
240
|
+
ipoProgramLeft();
|
|
241
|
+
if (typeof back === 'function') back();
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
return ui;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
248
|
+
module.exports = { ipoBackAction, ipoMakeUi };
|
|
249
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The runtime's wire policy: which SGBD a script's job goes to, and
|
|
3
|
+
* which jobs are a USER ACTION that must be confirmed before they are sent.
|
|
4
|
+
*
|
|
5
|
+
* SAFETY. Confirm before any key whose body can send a job the write
|
|
6
|
+
* classifier flags; a screen's own writes are confirmed once per screen.
|
|
7
|
+
* Release on leave is the script's own Back ITEM (that is how INPA releases
|
|
8
|
+
* what it energised) plus `inpaexit` on module exit; the Back job is also
|
|
9
|
+
* registered with registerMenuLeave so a route change still sends it. Reads
|
|
10
|
+
* never prompt. Jobs go to the SGBD the car identified.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The session plumbing INPA sends without asking anyone: the write
|
|
15
|
+
* classifier is default-deny (it guards the raw job route, where an unknown
|
|
16
|
+
* name must never reach the car unasked), and inside a running script most
|
|
17
|
+
* of what it flags is the session INITIALISIERUNG, the DIAGNOSE_AUFRECHT
|
|
18
|
+
* keep-alive, the closing DIAGNOSE_ENDE -- prompting for each turns opening
|
|
19
|
+
* a module into a wall of dialogs. What is worth confirming is an ACTUATOR
|
|
20
|
+
* COMMAND: a job that energises something, writes, resets or clears -- what
|
|
21
|
+
* INPA itself warns about. So the script's own plumbing goes silently and
|
|
22
|
+
* everything else the classifier flags still asks (an unknown write stays
|
|
23
|
+
* guarded).
|
|
24
|
+
* @type {RegExp}
|
|
25
|
+
*/
|
|
26
|
+
const IPO_PLUMBING =
|
|
27
|
+
/^(INITIALISIERUNG|IDENT|IDENT_\w+|INFO|DIAGNOSE_(AUFRECHT|ENDE|MODE)|ENDE)$/i;
|
|
28
|
+
|
|
29
|
+
/** a group SGBD's name: D_ + the diagnostic address or a family (D_MOTOR) */
|
|
30
|
+
const IPO_GROUP_SGBD_RE = /^d_[a-z0-9_]+$/;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The wire target for a job the script addresses. The script's own variable
|
|
34
|
+
* holds INPA's dispatch LIST ("IHKA46,IHKA46_2,IHKA46_3") until inpainit
|
|
35
|
+
* stores the resolved variant; the car's identified SGBD is what we talk to.
|
|
36
|
+
* A script that names ANOTHER shipped module explicitly (a DME screen asking
|
|
37
|
+
* the EGS) keeps that name.
|
|
38
|
+
* @param {object} ecu - the module ({sgbd, _sgbdBase, _ipoKnownSgbds})
|
|
39
|
+
* @param {string|null} sgbd - the SGBD the script named
|
|
40
|
+
* @returns {string} the SGBD to send to, lower-case
|
|
41
|
+
*/
|
|
42
|
+
function ipoWireTarget(ecu, sgbd) {
|
|
43
|
+
const mine = String((ecu && ecu.sgbd) || '').toLowerCase();
|
|
44
|
+
const s = String(sgbd || '')
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.trim();
|
|
47
|
+
if (!s || s.includes(',') || s === mine) return mine;
|
|
48
|
+
const base = String((ecu && ecu._sgbdBase) || '').toLowerCase();
|
|
49
|
+
if (s === base) return mine;
|
|
50
|
+
const known = ecu && ecu._ipoKnownSgbds;
|
|
51
|
+
if (known && known.has(s)) return s;
|
|
52
|
+
// a group SGBD (D_0044, D_MOTOR): a script addresses a module by its group
|
|
53
|
+
// and lets EDIABAS find the variant on the wire -- the whole-vehicle
|
|
54
|
+
// scripts do it for every module, a module's script for its own startup
|
|
55
|
+
// (IHKA46 goes through D_005B) or a neighbour (GS30 asks D_0044). The
|
|
56
|
+
// shim's run route resolves the group the same way EDIABAS does and
|
|
57
|
+
// caches the answer, so the group stays the target (api-router.js).
|
|
58
|
+
if (IPO_GROUP_SGBD_RE.test(s)) return s;
|
|
59
|
+
return mine;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Does sending this job need the user's confirmation: a write by the
|
|
64
|
+
* classifier's verdict that is not the script's own session plumbing.
|
|
65
|
+
* @param {string|null|undefined} job - job name
|
|
66
|
+
* @returns {boolean}
|
|
67
|
+
*/
|
|
68
|
+
function ipoNeedsConfirm(job) {
|
|
69
|
+
const n = String(job || '').trim();
|
|
70
|
+
if (!n) return false;
|
|
71
|
+
if (typeof isWriteJob === 'function' && !isWriteJob(n)) return false;
|
|
72
|
+
if (IPO_PLUMBING.test(n)) return false;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Writes that change the module for good, whatever the setting says: coding,
|
|
78
|
+
* flashing, EEPROM/adaptation writes, resets, clears. An actuator drive is
|
|
79
|
+
* not one of these (STEUERN_*, START_/STOP_*, AKTIVIER*): it energises a
|
|
80
|
+
* component for the moment and releases when the key or screen is left.
|
|
81
|
+
* @type {RegExp}
|
|
82
|
+
*/
|
|
83
|
+
const IPO_PERMANENT_WRITE_RE = new RegExp(
|
|
84
|
+
'(SCHREIB|LOESCH|FLASH|PROGRAMMIER|RESET|CODIER|WRITE|DOWNLOAD|UPLOAD' +
|
|
85
|
+
'|ABGLEICH|ADAPTION|ANLERN|TEACH|CLEAR|SETZEN|(?:\\b|_)SET(?:\\b|_)|EINSTELL|TILGUNG' +
|
|
86
|
+
'|AUTHENTIS|SLEEP|WAKEUP|POWER_?DOWN)',
|
|
87
|
+
'i'
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Does this write still ask, given Settings 'confirmActuators'? "Send
|
|
92
|
+
* immediately (like INPA)" turns the prompt off for actuator drives only; a
|
|
93
|
+
* permanent write asks whatever the setting says (the README's promise).
|
|
94
|
+
* @param {string} job - job name
|
|
95
|
+
* @returns {boolean} true when the prompt must be shown
|
|
96
|
+
*/
|
|
97
|
+
function ipoConfirmWanted(job) {
|
|
98
|
+
const n = String(job || '').trim();
|
|
99
|
+
if (IPO_PERMANENT_WRITE_RE.test(n)) return true;
|
|
100
|
+
if (typeof Settings === 'undefined' || !Settings || !Settings.get)
|
|
101
|
+
return true;
|
|
102
|
+
return Settings.get('confirmActuators', 'on') !== 'off';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
106
|
+
module.exports = {
|
|
107
|
+
IPO_PLUMBING,
|
|
108
|
+
IPO_PERMANENT_WRITE_RE,
|
|
109
|
+
ipoWireTarget,
|
|
110
|
+
ipoNeedsConfirm,
|
|
111
|
+
ipoConfirmWanted,
|
|
112
|
+
};
|
|
113
|
+
}
|