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,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Entry point: open the live program for a module, and the one-driver
|
|
3
|
+
* rule that pauses it while a remote helper has the cable. This is the last
|
|
4
|
+
* piece of screens/ipo-runtime/ in load order and exports the runtime's
|
|
5
|
+
* public API.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** @type {IpoProgram|null} the program whose view is showing */
|
|
9
|
+
let _ipoCurrent = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The current program left its view (its UI adapter reports it).
|
|
13
|
+
* @returns {void}
|
|
14
|
+
*/
|
|
15
|
+
function ipoProgramLeft() {
|
|
16
|
+
_ipoCurrent = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* ONE DRIVER AT A TIME. While the owner has admitted a helper, the helper's
|
|
21
|
+
* runtime is what runs on the cable. The owner's own module screens would
|
|
22
|
+
* cycle their jobs on the same K-line every tick and the helper's requests
|
|
23
|
+
* would queue behind them, seconds at a time; so an owner opening a module
|
|
24
|
+
* during a live share sees a notice instead, and a share being admitted
|
|
25
|
+
* closes whatever the owner had running.
|
|
26
|
+
* @returns {boolean}
|
|
27
|
+
*/
|
|
28
|
+
function ipoRemoteDriving() {
|
|
29
|
+
return (
|
|
30
|
+
typeof Remote !== 'undefined' &&
|
|
31
|
+
Remote &&
|
|
32
|
+
Remote.role === 'owner' &&
|
|
33
|
+
!!Remote.accepted
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Close the owner's running program because a helper was admitted.
|
|
39
|
+
* @returns {void}
|
|
40
|
+
*/
|
|
41
|
+
function ipoPauseForRemote() {
|
|
42
|
+
if (_ipoCurrent) {
|
|
43
|
+
_ipoCurrent.close();
|
|
44
|
+
_ipoCurrent = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Show a notice in the module container with only a Back key.
|
|
50
|
+
* @param {HTMLElement} container - the view
|
|
51
|
+
* @param {string} html - the notice
|
|
52
|
+
* @param {string} status - the status-bar text
|
|
53
|
+
* @param {() => void} back - leave the module view
|
|
54
|
+
* @returns {void}
|
|
55
|
+
*/
|
|
56
|
+
function ipoNotice(container, html, status, back) {
|
|
57
|
+
container.className = 'results-panel';
|
|
58
|
+
container.innerHTML = html;
|
|
59
|
+
sbLeft.textContent = status;
|
|
60
|
+
setActions([ipoBackAction(() => back())]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The "module did not identify itself" screen: the script's own last
|
|
65
|
+
* message when it stopped itself, else the generic explanation, plus WHY
|
|
66
|
+
* inpainit had nothing better than the SGBD filename to check -- the group
|
|
67
|
+
* probe's own verdict (bus-silent, probe-error, ...) is the actionable half
|
|
68
|
+
* of this screen, so say it instead of leaving a self-contradictory "'SM46'
|
|
69
|
+
* not found, found 'SM46'".
|
|
70
|
+
* @param {object} ecu - the module
|
|
71
|
+
* @param {IpoProgram} program - the program that failed to start
|
|
72
|
+
* @returns {string} HTML
|
|
73
|
+
*/
|
|
74
|
+
function ipoStoppedHtml(ecu, program) {
|
|
75
|
+
const m = (program.messages || []).slice(-1)[0];
|
|
76
|
+
const rd =
|
|
77
|
+
typeof webResolveVariantLast === 'function'
|
|
78
|
+
? webResolveVariantLast()
|
|
79
|
+
: null;
|
|
80
|
+
const g = String(ecu.group || '').toLowerCase();
|
|
81
|
+
const why =
|
|
82
|
+
rd && g && rd.group === g && rd.path !== 'resolved'
|
|
83
|
+
? `<div style="margin-top:14px;font-size:12px;color:var(--ink-faint)">` +
|
|
84
|
+
`Variant probe ${esc(g)}: <b>${esc(rd.path)}</b>` +
|
|
85
|
+
(rd.empty != null || rd.real != null
|
|
86
|
+
? ` (${Number(rd.real || 0)} answered, ${Number(rd.empty || 0)} silent)`
|
|
87
|
+
: '') +
|
|
88
|
+
(rd.error ? ` — ${esc(String(rd.error))}` : '') +
|
|
89
|
+
`. The car did not name this module, so the script checked the ` +
|
|
90
|
+
`SGBD filename instead. Ignition on, reopen the module.</div>`
|
|
91
|
+
: '';
|
|
92
|
+
return (
|
|
93
|
+
`<div class="empty"><div class="empty-big" style="color:var(--amber)">` +
|
|
94
|
+
`${esc(m ? ipoText(m.title) : `${ecu.label} is not answering`)}</div>` +
|
|
95
|
+
`<div>${esc(
|
|
96
|
+
m
|
|
97
|
+
? ipoText(m.body || '')
|
|
98
|
+
: 'The cable is connected, but this module did not identify itself. It may not be fitted to this car, or the ignition may need to be on.'
|
|
99
|
+
)}</div>${why}</div>`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Which shipped SGBDs a script may address explicitly, cached on the module.
|
|
105
|
+
* @param {object} ecu - the module
|
|
106
|
+
* @returns {Promise<void>}
|
|
107
|
+
*/
|
|
108
|
+
async function ipoLoadKnownSgbds(ecu) {
|
|
109
|
+
if (ecu._ipoKnownSgbds) return;
|
|
110
|
+
try {
|
|
111
|
+
const idx = await fetch('api/ecu-index.json').then((r) =>
|
|
112
|
+
r.ok ? r.json() : null
|
|
113
|
+
);
|
|
114
|
+
ecu._ipoKnownSgbds = new Set(
|
|
115
|
+
Object.keys(idx || {}).map((k) => k.toLowerCase())
|
|
116
|
+
);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
ecu._ipoKnownSgbds = new Set();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Open the live program for a module. Returns true when it took the view
|
|
124
|
+
* (even if the script stopped itself: the reason is shown), false when this
|
|
125
|
+
* module cannot run live (no exec) so the caller falls back.
|
|
126
|
+
* @param {object} ecu - the module
|
|
127
|
+
* @param {HTMLElement} container - where the view is drawn
|
|
128
|
+
* @param {() => void} back - leave the module view
|
|
129
|
+
* @param {string|null} [openMenu] - a menu to open (a deep link)
|
|
130
|
+
* @param {string|null} [openScreen] - the screen to show on that menu
|
|
131
|
+
* @param {RegExp|string|null} [pressKey] - a read key to press on arrival, by its caption
|
|
132
|
+
* @returns {Promise<boolean>}
|
|
133
|
+
*/
|
|
134
|
+
/** The shipped group -> identifiable variants map, fetched once. */
|
|
135
|
+
let _ipoVariantsByGroupP = null;
|
|
136
|
+
function ipoVariantsByGroup() {
|
|
137
|
+
return (_ipoVariantsByGroupP ??= fetch('data/groups/variants-by-group.json')
|
|
138
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
139
|
+
.catch(() => null));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The variant names a script's entry accepts: the string constants of its
|
|
144
|
+
* inpainit that name a shipped SGBD ("B_SM46_3", "EASY_E_B").
|
|
145
|
+
* @param {IpoExec} exec - the script
|
|
146
|
+
* @param {Set<string>} known - every shipped SGBD, lowercased
|
|
147
|
+
* @returns {string[]} lowercased
|
|
148
|
+
*/
|
|
149
|
+
function ipoScriptVariants(exec, known) {
|
|
150
|
+
const toks =
|
|
151
|
+
(exec.procs && (exec.procs.inpainit || exec.procs.SgbdInpaCheck)) || [];
|
|
152
|
+
const out = [];
|
|
153
|
+
for (const t of toks) {
|
|
154
|
+
if (t.op !== 'const' || typeof t.v !== 'string') continue;
|
|
155
|
+
const v = t.v.trim().toLowerCase();
|
|
156
|
+
if (v && known.has(v) && !out.includes(v)) out.push(v);
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The module a scriptchange target addresses. The new script's inpainit
|
|
163
|
+
* names the variants it accepts; the group whose IDENTIFIKATION can name
|
|
164
|
+
* one of them is asked live, and the car's answer is the SGBD the program
|
|
165
|
+
* talks to from then on. With no group to ask, the first shipped variant
|
|
166
|
+
* the script names is taken.
|
|
167
|
+
* @param {EcuRecord} ecu - the module the view opened
|
|
168
|
+
* @param {string} script - the script the key named, lowercased
|
|
169
|
+
* @param {IpoExec} exec - that script
|
|
170
|
+
* @returns {Promise<EcuRecord|null>} the target, null when nothing answered
|
|
171
|
+
*/
|
|
172
|
+
async function ipoResolveScriptEcu(ecu, script, exec) {
|
|
173
|
+
const known = ecu._ipoKnownSgbds || new Set();
|
|
174
|
+
const wants = ipoScriptVariants(exec, known);
|
|
175
|
+
const byGroup = (await ipoVariantsByGroup()) || {};
|
|
176
|
+
let group = null;
|
|
177
|
+
let best = 0;
|
|
178
|
+
for (const [g, list] of Object.entries(byGroup)) {
|
|
179
|
+
const hits = (list || []).filter((v) =>
|
|
180
|
+
wants.includes(String(v).toLowerCase())
|
|
181
|
+
).length;
|
|
182
|
+
if (hits > best) {
|
|
183
|
+
best = hits;
|
|
184
|
+
group = g;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
let sgbd = null;
|
|
188
|
+
if (group && typeof webResolveVariant === 'function') {
|
|
189
|
+
try {
|
|
190
|
+
sgbd = await webResolveVariant(group);
|
|
191
|
+
} catch (e) {
|
|
192
|
+
sgbd = null;
|
|
193
|
+
}
|
|
194
|
+
if (!sgbd) return null; // the group asked and nothing answered
|
|
195
|
+
} else if (wants.length) {
|
|
196
|
+
sgbd = wants[0];
|
|
197
|
+
} else if (known.has(script)) {
|
|
198
|
+
sgbd = script;
|
|
199
|
+
}
|
|
200
|
+
if (!sgbd) return null;
|
|
201
|
+
return {
|
|
202
|
+
...ecu,
|
|
203
|
+
code: script,
|
|
204
|
+
sgbd: String(sgbd).toLowerCase(),
|
|
205
|
+
group: group ? group.toUpperCase() : ecu.group,
|
|
206
|
+
_variant: String(sgbd).toUpperCase(),
|
|
207
|
+
_irFrom: script,
|
|
208
|
+
_sgbdBase: undefined,
|
|
209
|
+
_scriptChangeOf: ecu.sgbd,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function ipoProgramOpen(
|
|
214
|
+
ecu,
|
|
215
|
+
container,
|
|
216
|
+
back,
|
|
217
|
+
openMenu,
|
|
218
|
+
openScreen,
|
|
219
|
+
pressKey
|
|
220
|
+
) {
|
|
221
|
+
if (typeof IpoVm === 'undefined' || typeof FeedHost === 'undefined')
|
|
222
|
+
return false;
|
|
223
|
+
if (ipoRemoteDriving()) {
|
|
224
|
+
ipoNotice(
|
|
225
|
+
container,
|
|
226
|
+
`<div class="empty"><div class="empty-big" style="color:var(--amber)">A helper is driving your car</div>` +
|
|
227
|
+
`<div>Your own module screens stay off while the remote session is live, so the helper's reads are not queued behind them. End the session to use this module yourself.</div></div>`,
|
|
228
|
+
`${ecu.sgbd}.prg · remote session live`,
|
|
229
|
+
back
|
|
230
|
+
);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (typeof irLiveExec !== 'function' || typeof irExecSgbd !== 'function')
|
|
234
|
+
return false;
|
|
235
|
+
const exec = await irLiveExec(irExecSgbd(ecu));
|
|
236
|
+
if (!exec || !exec.procs || !Object.keys(exec.procs).length) return false;
|
|
237
|
+
if (!(exec.procs.inpainit || exec.procs.SgbdInpaCheck)) return false;
|
|
238
|
+
if (_ipoCurrent) _ipoCurrent.close();
|
|
239
|
+
if (ecu._ir && typeof irUseTranslations === 'function')
|
|
240
|
+
irUseTranslations(ecu._ir);
|
|
241
|
+
await ipoLoadKnownSgbds(ecu);
|
|
242
|
+
// the fault dictionaries the fed results are translated through
|
|
243
|
+
// (faultdb.js: large, injected on demand, absent from a build that opted
|
|
244
|
+
// out of the fault tables -- then the ECU's German is what shows)
|
|
245
|
+
if (ipoTranslating() && typeof loadFaultDb === 'function') {
|
|
246
|
+
try {
|
|
247
|
+
await loadFaultDb();
|
|
248
|
+
} catch (e) {
|
|
249
|
+
/* results stay as sent */
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const ui = ipoMakeUi(ecu, container, back);
|
|
253
|
+
const program = new IpoProgram(ecu, exec, ui);
|
|
254
|
+
_ipoCurrent = program;
|
|
255
|
+
sbLeft.textContent = `${ecu.sgbd}.prg · starting`;
|
|
256
|
+
const r = await program.start();
|
|
257
|
+
// No adapter at all: the entry jobs could not reach the car. Some scripts
|
|
258
|
+
// tolerate a failed INITIALISIERUNG and still open their root menu, which
|
|
259
|
+
// would read as an offline view of a module nothing has talked to -- so
|
|
260
|
+
// the gate fires whether or not the script carried on.
|
|
261
|
+
if (program.noCable) {
|
|
262
|
+
program.close();
|
|
263
|
+
_ipoCurrent = null;
|
|
264
|
+
ipoNotice(
|
|
265
|
+
container,
|
|
266
|
+
errorBlock('no cable connected'),
|
|
267
|
+
`${ecu.sgbd}.prg · no cable`,
|
|
268
|
+
back
|
|
269
|
+
);
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
if (!r.ok) {
|
|
273
|
+
_ipoCurrent = null;
|
|
274
|
+
if (program.silent || r.reason === 'stopped') {
|
|
275
|
+
ipoNotice(
|
|
276
|
+
container,
|
|
277
|
+
ipoStoppedHtml(ecu, program),
|
|
278
|
+
`${ecu.sgbd}.prg · ${program.silent ? 'no response' : 'stopped'}`,
|
|
279
|
+
back
|
|
280
|
+
);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
if (r.reason === 'cancelled') {
|
|
284
|
+
back();
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
// the script itself failed before its root menu opened: an app error,
|
|
288
|
+
// not the car -- say which, there is no other renderer to fall back to
|
|
289
|
+
ipoNotice(
|
|
290
|
+
container,
|
|
291
|
+
errorBlock(
|
|
292
|
+
`vm error: INPA's script for ${ecu.sgbd} did not start (${r.reason})`
|
|
293
|
+
),
|
|
294
|
+
`${ecu.sgbd}.prg · failed`,
|
|
295
|
+
back
|
|
296
|
+
);
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
// A DEEP LINK LANDS; IT DOES NOT PRESS. openMenu runs a menu's prologue and
|
|
300
|
+
// shows a screen -- both of which read -- but it never runs an ITEM body, so
|
|
301
|
+
// a link into an activation menu cannot drive an actuator on arrival. That
|
|
302
|
+
// is the whole reason a search result carries the menu and screen NAMES
|
|
303
|
+
// rather than the key to press.
|
|
304
|
+
const wantScreen = openScreen && exec.procs[openScreen] ? openScreen : null;
|
|
305
|
+
// a link that names the screen but not its menu (a search result for the
|
|
306
|
+
// screen itself) lands on the menu that shows that screen, so the F-keys
|
|
307
|
+
// are the screen's own and not the entry menu's
|
|
308
|
+
let landMenu = openMenu && exec.procs[openMenu] ? openMenu : null;
|
|
309
|
+
if (wantScreen) {
|
|
310
|
+
// A key result names the menu the key SITS on and the screen it opens;
|
|
311
|
+
// that screen belongs to the menu the key switches to (its item body
|
|
312
|
+
// is setscreen then setmenu), so landing on the named menu with that
|
|
313
|
+
// screen would put the parent's jobs on the F-keys. Land on the owner
|
|
314
|
+
// unless the named menu shows this screen itself.
|
|
315
|
+
const shownHere = landMenu ? ipoScreenForMenu(exec, landMenu) : null;
|
|
316
|
+
if (!(shownHere && shownHere.screen === wantScreen)) {
|
|
317
|
+
const owner = ipoMenuForScreen(exec, wantScreen);
|
|
318
|
+
if (owner) landMenu = owner;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (landMenu && landMenu !== program.menu) {
|
|
322
|
+
await program.openMenu(landMenu, wantScreen ? { screen: wantScreen } : {});
|
|
323
|
+
}
|
|
324
|
+
// A MENU PROLOGUE'S OWN setscreen OUTRANKS opts.screen, by design: the
|
|
325
|
+
// script's choice is what a normal keypress must land on. A link naming a
|
|
326
|
+
// screen is the one case where the user's choice is more specific than the
|
|
327
|
+
// script's default backdrop, so it is applied afterwards rather than by
|
|
328
|
+
// weakening that rule for every other caller.
|
|
329
|
+
if (wantScreen && wantScreen !== program.screen) {
|
|
330
|
+
await program.showScreen(wantScreen, false);
|
|
331
|
+
}
|
|
332
|
+
// A deep link lands; it does not press. The one exception is a caller
|
|
333
|
+
// asking for a named READ key by its caption (the Garage's Fault scan /
|
|
334
|
+
// Identification buttons): that key is pressed for the user exactly as
|
|
335
|
+
// their own press would be, so a key that writes still goes through the
|
|
336
|
+
// runtime's confirmation.
|
|
337
|
+
if (pressKey) {
|
|
338
|
+
const re =
|
|
339
|
+
pressKey instanceof RegExp
|
|
340
|
+
? pressKey
|
|
341
|
+
: new RegExp(
|
|
342
|
+
`^${String(pressKey).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`,
|
|
343
|
+
'i'
|
|
344
|
+
);
|
|
345
|
+
const it = (program.items || []).find((x) =>
|
|
346
|
+
re.test(String(x.label || '').trim())
|
|
347
|
+
);
|
|
348
|
+
if (it) program.press(it.nr).catch(() => {});
|
|
349
|
+
}
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (typeof window !== 'undefined') {
|
|
354
|
+
window.ipoProgramOpen = ipoProgramOpen;
|
|
355
|
+
window.ipoResolveScriptEcu = ipoResolveScriptEcu;
|
|
356
|
+
window.ipoPauseForRemote = ipoPauseForRemote;
|
|
357
|
+
// Cmd/Ctrl+P on an open module view prints its sheet (core/print.js asks)
|
|
358
|
+
window.ipoPrintAvailable = () => !!(_ipoCurrent && !_ipoCurrent.closed);
|
|
359
|
+
window.ipoPrintCurrent = () =>
|
|
360
|
+
_ipoCurrent
|
|
361
|
+
? ipoPrintScreen(
|
|
362
|
+
_ipoCurrent,
|
|
363
|
+
_ipoCurrent.ecu,
|
|
364
|
+
typeof inpaMode === 'function' && inpaMode()
|
|
365
|
+
)
|
|
366
|
+
: null;
|
|
367
|
+
window.IpoProgram = IpoProgram;
|
|
368
|
+
window.ipoMenuItems = ipoMenuItems;
|
|
369
|
+
window.ipoWireTarget = ipoWireTarget;
|
|
370
|
+
window.ipoScreenForMenu = ipoScreenForMenu;
|
|
371
|
+
window.ipoNeedsConfirm = ipoNeedsConfirm;
|
|
372
|
+
window.ipoMakeUi = ipoMakeUi;
|
|
373
|
+
}
|
|
374
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
375
|
+
module.exports = {
|
|
376
|
+
ipoNeedsConfirm,
|
|
377
|
+
IpoProgram,
|
|
378
|
+
ipoMenuItems,
|
|
379
|
+
ipoWireTarget,
|
|
380
|
+
ipoProgramOpen,
|
|
381
|
+
ipoResolveScriptEcu,
|
|
382
|
+
ipoScriptVariants,
|
|
383
|
+
ipoMakeUi,
|
|
384
|
+
ipoLineRows,
|
|
385
|
+
ipoMenuTiles,
|
|
386
|
+
ipoScreenComponents,
|
|
387
|
+
ipoLampHtml,
|
|
388
|
+
ipoGaugeHtml,
|
|
389
|
+
ipoScreenLineNames,
|
|
390
|
+
ipoProgramLeft,
|
|
391
|
+
ipoPauseForRemote,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file INPA mode: the canvas. One DOM row per screen row, each cell placed
|
|
3
|
+
* at its column (padding with spaces), captions and values marked, lamps and
|
|
4
|
+
* bars drawn inline at the column the script gave them.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** A logical line at least this tall (a fault entry) gets air above it. */
|
|
8
|
+
const IPO_BAND_MIN_ROWS = 3;
|
|
9
|
+
|
|
10
|
+
/** Columns a lamp's dot and spacing take beyond its word. */
|
|
11
|
+
const IPO_LAMP_EXTRA_COLS = 2;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The cells of the grid by row, translated and width-kept. INPA's virtual
|
|
15
|
+
* screen has as many logical lines as the script prints (a four-fault
|
|
16
|
+
* freeze-frame list runs past 30 rows and INPA scrolls); the page scrolls
|
|
17
|
+
* the same way, so nothing is cut off. A translated cell keeps the width
|
|
18
|
+
* the script gave the German, so the columns INPA laid out stay put
|
|
19
|
+
* ("Motordrehzahl" and its value at column 40 -> "Engine speed" padded to
|
|
20
|
+
* the same width). A longer English pushes only its own row right.
|
|
21
|
+
* @param {IpoProgram} p - the program
|
|
22
|
+
* @returns {Map<number, Array<{col: number, text: string, kind: string, key: string|null, meta: IpoCellMeta|null}>>}
|
|
23
|
+
*/
|
|
24
|
+
function ipoGridRows(p) {
|
|
25
|
+
const byRow = new Map();
|
|
26
|
+
for (const c of p.cells.values()) {
|
|
27
|
+
const r = Number(c.row),
|
|
28
|
+
col = Number(c.col);
|
|
29
|
+
if (!(r >= 0) || !(col >= 0)) continue;
|
|
30
|
+
let text = ipoText(c.text);
|
|
31
|
+
if (!text) continue;
|
|
32
|
+
if (text !== c.text && text.length < c.text.length)
|
|
33
|
+
text = text.padEnd(c.text.length);
|
|
34
|
+
if (!byRow.has(r)) byRow.set(r, []);
|
|
35
|
+
byRow.get(r).push({ col, text, kind: c.kind, key: c.key, meta: c.meta });
|
|
36
|
+
}
|
|
37
|
+
return byRow;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* One screen row's HTML: its cells in column order, a cell that starts
|
|
42
|
+
* before the previous one ended still getting one space so overlapping
|
|
43
|
+
* draws never merge into one word.
|
|
44
|
+
* @param {Array<{col: number, text: string, kind: string, key: string|null, meta: IpoCellMeta|null}>} cells - the row's cells
|
|
45
|
+
* @returns {string} HTML
|
|
46
|
+
*/
|
|
47
|
+
function ipoGridRowHtml(cells) {
|
|
48
|
+
let out = '';
|
|
49
|
+
let at = 0;
|
|
50
|
+
for (const c of cells) {
|
|
51
|
+
const gap = Math.max(c.col - at, at > 0 ? 1 : 0);
|
|
52
|
+
out += ' '.repeat(gap);
|
|
53
|
+
if (c.kind === 'lamp') {
|
|
54
|
+
out += ipoLampHtml(c);
|
|
55
|
+
at = c.col + c.text.length + IPO_LAMP_EXTRA_COLS;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (c.kind === 'gauge') {
|
|
59
|
+
out += ipoGaugeHtml(c);
|
|
60
|
+
at = c.col + IPO_GAUGE_COLS + c.text.length;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const cls = c.kind === 'value' ? 'ipo-val' : 'ipo-cap';
|
|
64
|
+
out += `<span class="${cls}"${c.key ? ` data-key="${esc(c.key)}"` : ''}>${esc(c.text)}</span>`;
|
|
65
|
+
at = c.col + c.text.length;
|
|
66
|
+
}
|
|
67
|
+
// a value the script padded to its column width ends in blanks that
|
|
68
|
+
// would only widen the row
|
|
69
|
+
return out.replace(/\s+$/, '');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Paint the INPA grid into the screen container.
|
|
74
|
+
* @param {HTMLElement} gridEl - the screen container
|
|
75
|
+
* @param {IpoProgram} p - the program
|
|
76
|
+
* @returns {void}
|
|
77
|
+
*/
|
|
78
|
+
function ipoPaintGrid(gridEl, p) {
|
|
79
|
+
const byRow = ipoGridRows(p);
|
|
80
|
+
const rows = [...byRow.keys()].sort((a, b) => a - b);
|
|
81
|
+
if (!rows.length) {
|
|
82
|
+
gridEl.innerHTML = `<div class="ipo-empty">${esc(ipoText(p.title || ''))}</div>`;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const last = rows[rows.length - 1];
|
|
86
|
+
const html = [];
|
|
87
|
+
for (let r = 0; r <= last; r++) {
|
|
88
|
+
const cells = (byRow.get(r) || []).sort((a, b) => a.col - b.col);
|
|
89
|
+
// a logical line's first row gets a little air above it (the fault
|
|
90
|
+
// list is one LINE per entry); the top of the screen needs none
|
|
91
|
+
const band =
|
|
92
|
+
r > 0 && p.bandTops && (p.bandTops.get(r) || 0) >= IPO_BAND_MIN_ROWS
|
|
93
|
+
? ' ipo-band'
|
|
94
|
+
: '';
|
|
95
|
+
if (!cells.length) {
|
|
96
|
+
html.push(`<div class="ipo-row${band}"> </div>`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
html.push(`<div class="ipo-row${band}">${ipoGridRowHtml(cells)}</div>`);
|
|
100
|
+
}
|
|
101
|
+
gridEl.innerHTML = html.join('');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
105
|
+
module.exports = { ipoPaintGrid };
|
|
106
|
+
}
|