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,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The cells both skins draw: caption text through the exact-dictionary
|
|
3
|
+
* translation, and INPA's two instrument cells -- the lamp (digitalout) and
|
|
4
|
+
* the bar (analogout).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Words a lamp reads as "on" when its declared TrueText is unknown. */
|
|
8
|
+
const IPO_LAMP_ON_RE = /^(1|ein|on|an|ja|yes|aktiv|true)$/i;
|
|
9
|
+
|
|
10
|
+
/** The width a bar takes on the text grid, in columns. */
|
|
11
|
+
const IPO_GAUGE_COLS = 34;
|
|
12
|
+
|
|
13
|
+
/** A band edge within this many percent of a scale end collides with its number. */
|
|
14
|
+
const IPO_GAUGE_EDGE_MARGIN = 8;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A painted cell of the program's grid (see IpoProgram.takeCells).
|
|
18
|
+
* @typedef {object} IpoCell
|
|
19
|
+
* @property {number} row - absolute screen row
|
|
20
|
+
* @property {number} col - screen column
|
|
21
|
+
* @property {string} text - the cell's text
|
|
22
|
+
* @property {string|null} key - the result key it is bound to
|
|
23
|
+
* @property {'text'|'value'|'lamp'|'gauge'} kind - what drew it
|
|
24
|
+
* @property {IpoCellMeta|null} meta - what analogout/digitalout declared
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* What analogout/digitalout declared for a cell: the scale, the good band,
|
|
29
|
+
* the format, the two words -- the painter draws INPA's bar and lamp from it.
|
|
30
|
+
* @typedef {object} IpoCellMeta
|
|
31
|
+
* @property {number} [min] - scale minimum
|
|
32
|
+
* @property {number} [max] - scale maximum
|
|
33
|
+
* @property {number} [lo] - valid-band low edge
|
|
34
|
+
* @property {number} [hi] - valid-band high edge
|
|
35
|
+
* @property {string} [fmt] - display format
|
|
36
|
+
* @property {string} [on] - the lamp's TrueText
|
|
37
|
+
* @property {string} [off] - the lamp's FalseText
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A key's caption for the bar: its own label, else the legend's, translated.
|
|
42
|
+
* @param {IpoProgram} program - the running program (unused; kept for callers)
|
|
43
|
+
* @param {IpoMenuItem} it - the key
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
function ipoKeyLabel(program, it) {
|
|
47
|
+
const raw = it.label || it.legendLabel || '';
|
|
48
|
+
return irLabel(raw) || raw;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A printed string through the exact-dictionary translation; blank text
|
|
53
|
+
* passes through untouched so padding survives.
|
|
54
|
+
* @param {*} s - the text
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
function ipoText(s) {
|
|
58
|
+
const raw = String(s == null ? '' : s);
|
|
59
|
+
if (!raw.trim()) return raw;
|
|
60
|
+
return irLabel(raw) || raw;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A scale number for the bar's foot, rounded to two decimals.
|
|
65
|
+
* @param {number|null|undefined} v - the number
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
function ipoScaleLabel(v) {
|
|
69
|
+
if (v == null || !Number.isFinite(v)) return '';
|
|
70
|
+
const r = Math.round(v * 100) / 100;
|
|
71
|
+
return String(r);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Is a lamp lit? The word the script printed against its declared TrueText /
|
|
76
|
+
* FalseText, else the usual on-words.
|
|
77
|
+
* @param {{text: string, meta: IpoCellMeta|null}} c - the lamp cell
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
function ipoLampOn(c) {
|
|
81
|
+
const m = c.meta || {};
|
|
82
|
+
const word = String(c.text || '').trim();
|
|
83
|
+
return m.on != null && word && word === String(m.on).trim()
|
|
84
|
+
? true
|
|
85
|
+
: m.off != null && word === String(m.off).trim()
|
|
86
|
+
? false
|
|
87
|
+
: IPO_LAMP_ON_RE.test(word);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* digitalout(val, row, col, TrueText, FalseText) is a lamp: a filled circle
|
|
92
|
+
* with the TrueText beside it when the value is set, an empty circle with
|
|
93
|
+
* the FalseText otherwise.
|
|
94
|
+
* @param {IpoCell} c - the cell
|
|
95
|
+
* @returns {string} HTML
|
|
96
|
+
*/
|
|
97
|
+
function ipoLampHtml(c) {
|
|
98
|
+
const word = String(c.text || '').trim();
|
|
99
|
+
const on = ipoLampOn(c);
|
|
100
|
+
const key = c.key ? ` data-key="${esc(c.key)}"` : '';
|
|
101
|
+
return (
|
|
102
|
+
`<span class="ipo-lamp-cell"${key}>` +
|
|
103
|
+
`<span class="ipo-dot${on ? ' on' : ''}"></span>` +
|
|
104
|
+
`<span class="ipo-lamp-word">${esc(ipoText(word))}</span></span>`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* analogout(val, row, col, min, max, minvalid, maxvalid, fmt) is a bar: the
|
|
110
|
+
* declared scale min..max, the valid band green and the rest red, a black
|
|
111
|
+
* fill from the left up to the reading, the scale ends and band edges
|
|
112
|
+
* printed under it, the number beside it. With no declared scale, or a
|
|
113
|
+
* state word where a number was expected, the value shows as text.
|
|
114
|
+
* @param {IpoCell} c - the cell
|
|
115
|
+
* @returns {string} HTML
|
|
116
|
+
*/
|
|
117
|
+
function ipoGaugeHtml(c) {
|
|
118
|
+
const m = c.meta || {};
|
|
119
|
+
const text = String(c.text || '').trim();
|
|
120
|
+
const n = parseFloat(text);
|
|
121
|
+
const key = c.key ? ` data-key="${esc(c.key)}"` : '';
|
|
122
|
+
const hasScale =
|
|
123
|
+
m.min != null &&
|
|
124
|
+
m.max != null &&
|
|
125
|
+
Number.isFinite(m.min) &&
|
|
126
|
+
Number.isFinite(m.max);
|
|
127
|
+
if (!hasScale || Number.isNaN(n)) {
|
|
128
|
+
return `<span class="ipo-val ipo-gauge-val"${key}>${esc(text)}</span>`;
|
|
129
|
+
}
|
|
130
|
+
const span = m.max - m.min || 1;
|
|
131
|
+
const at = (v) => Math.max(0, Math.min(100, ((v - m.min) / span) * 100));
|
|
132
|
+
const pct = at(n).toFixed(1);
|
|
133
|
+
let bg = 'var(--gauge-ok)';
|
|
134
|
+
let edges = '';
|
|
135
|
+
if (
|
|
136
|
+
m.lo != null &&
|
|
137
|
+
m.hi != null &&
|
|
138
|
+
Number.isFinite(m.lo) &&
|
|
139
|
+
Number.isFinite(m.hi)
|
|
140
|
+
) {
|
|
141
|
+
const a = at(m.lo),
|
|
142
|
+
b = at(m.hi);
|
|
143
|
+
bg =
|
|
144
|
+
`linear-gradient(to right, var(--gauge-warn) 0 ${a.toFixed(1)}%, ` +
|
|
145
|
+
`var(--gauge-ok) ${a.toFixed(1)}% ${b.toFixed(1)}%, ` +
|
|
146
|
+
`var(--gauge-warn) ${b.toFixed(1)}% 100%)`;
|
|
147
|
+
for (const [v, pos] of [
|
|
148
|
+
[m.lo, a],
|
|
149
|
+
[m.hi, b],
|
|
150
|
+
]) {
|
|
151
|
+
if (pos > IPO_GAUGE_EDGE_MARGIN && pos < 100 - IPO_GAUGE_EDGE_MARGIN)
|
|
152
|
+
edges += `<span class="ipo-gauge-edge" style="left:${pos.toFixed(1)}%">${esc(ipoScaleLabel(v))}</span>`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return (
|
|
156
|
+
`<span class="ipo-gauge"${key}>` +
|
|
157
|
+
`<span class="ipo-gauge-bar">` +
|
|
158
|
+
`<span class="ipo-gauge-track" style="background:${bg}">` +
|
|
159
|
+
`<span class="ipo-gauge-fill" style="width:${pct}%"></span></span>` +
|
|
160
|
+
`<span class="ipo-gauge-foot"><span>${esc(ipoScaleLabel(m.min))}</span>${edges}` +
|
|
161
|
+
`<span>${esc(ipoScaleLabel(m.max))}</span></span></span>` +
|
|
162
|
+
`<span class="ipo-gauge-val">${esc(text)}</span></span>`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
167
|
+
module.exports = {
|
|
168
|
+
ipoLampOn,
|
|
169
|
+
IPO_GAUGE_COLS,
|
|
170
|
+
ipoKeyLabel,
|
|
171
|
+
ipoText,
|
|
172
|
+
ipoScaleLabel,
|
|
173
|
+
ipoLampHtml,
|
|
174
|
+
ipoGaugeHtml,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The app's dialogs for INPA's own: the togglelist component picker,
|
|
3
|
+
* the Select line picker, and the parked state machine's Continue/Stop
|
|
4
|
+
* control. (The input prompts are irAskInput in screens/ir.js.)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** A picker with more rows than this scrolls and says so. */
|
|
8
|
+
const IPO_PICK_LONG = 12;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* INPA's togglelist lists the ACTIVE screen's LINE declarations: the name is
|
|
12
|
+
* the LINE's label, the argument its key string (SHD46's s_steuern_digital:
|
|
13
|
+
* "Switch Sunroof Open" / SSHDA ...). With MultipleSelectFlag set (LSZ's
|
|
14
|
+
* in/output selection) it is a tick list and the script gets every picked
|
|
15
|
+
* key, ";"-joined, as one argument; with ArgNumFlag set it gets the lines'
|
|
16
|
+
* numbers instead of their keys.
|
|
17
|
+
* @param {IpoProgram} p - the program (its exec and current screen)
|
|
18
|
+
* @param {IpoStep} step - the toggle suspension
|
|
19
|
+
* @returns {Promise<IpoPick|null>} {ort, ein}, or null when cancelled
|
|
20
|
+
*/
|
|
21
|
+
async function ipoPickComponent(p, step) {
|
|
22
|
+
const multiple = !!(step && step.multiple);
|
|
23
|
+
const argnum = !!(step && step.argnum);
|
|
24
|
+
const rows = ipoScreenComponents(p.exec, p.screen).map((l, i) => ({
|
|
25
|
+
key: argnum ? String(i + 1) : String(l.keys).split(';')[0],
|
|
26
|
+
caption: ipoText(l.label || String(l.keys).split(';')[0]),
|
|
27
|
+
}));
|
|
28
|
+
if (!rows.length) {
|
|
29
|
+
await messageDialog({
|
|
30
|
+
title: 'No components to pick',
|
|
31
|
+
body: 'This screen lists no components for the picker.',
|
|
32
|
+
});
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return new Promise((resolveRaw) => {
|
|
36
|
+
// closing the modal fires onClose, which used to resolve null BEFORE
|
|
37
|
+
// the pick resolved: every pick read as cancelled
|
|
38
|
+
let settled = false;
|
|
39
|
+
const resolve = (v) => {
|
|
40
|
+
if (settled) return;
|
|
41
|
+
settled = true;
|
|
42
|
+
resolveRaw(v);
|
|
43
|
+
};
|
|
44
|
+
const type = multiple ? 'checkbox' : 'radio';
|
|
45
|
+
const actions = multiple
|
|
46
|
+
? `<button class="btn" data-x="cancel">Cancel</button>
|
|
47
|
+
<button class="btn primary" data-x="ok">Select</button>`
|
|
48
|
+
: `<button class="btn" data-x="cancel">Cancel</button>
|
|
49
|
+
<button class="btn" data-x="off">Off</button>
|
|
50
|
+
<button class="btn primary" data-x="on">On</button>`;
|
|
51
|
+
const long = rows.length > IPO_PICK_LONG;
|
|
52
|
+
const { overlay, close } = openModal(
|
|
53
|
+
`<div class="modal" role="dialog" aria-modal="true">
|
|
54
|
+
<div class="modal-title">${multiple ? 'Select components' : 'Select component'}</div>
|
|
55
|
+
<div class="modal-body ipo-pick${long ? ' ipo-pick-long' : ''}">${rows
|
|
56
|
+
.map(
|
|
57
|
+
(r, i) =>
|
|
58
|
+
`<label class="ipo-pick-row"><input type="${type}" name="ipo-pick" value="${i}"${!multiple && i === 0 ? ' checked' : ''}/> ` +
|
|
59
|
+
`<span>${esc(r.caption)}</span> <span class="mono ipo-pick-key">${esc(r.key)}</span></label>`
|
|
60
|
+
)
|
|
61
|
+
.join('')}</div>
|
|
62
|
+
${
|
|
63
|
+
long
|
|
64
|
+
? `<div class="ipo-pick-hint">${rows.length} components · scroll the list for more</div>`
|
|
65
|
+
: ''
|
|
66
|
+
}
|
|
67
|
+
<div class="modal-actions">${actions}</div></div>`,
|
|
68
|
+
{ onClose: () => resolve(null) }
|
|
69
|
+
);
|
|
70
|
+
overlay.querySelectorAll('[data-x]').forEach((b) => {
|
|
71
|
+
b.onclick = () => {
|
|
72
|
+
const x = b.dataset.x;
|
|
73
|
+
const picked = [
|
|
74
|
+
...overlay.querySelectorAll('input[name="ipo-pick"]:checked'),
|
|
75
|
+
].map((el) => rows[Number(el.value)]);
|
|
76
|
+
if (x === 'cancel' || !picked.length) resolve(null);
|
|
77
|
+
else if (multiple)
|
|
78
|
+
resolve({ ort: picked.map((r) => r.key).join(';'), ein: 0 });
|
|
79
|
+
else resolve({ ort: picked[0].key, ein: x === 'on' ? 0 : 1 });
|
|
80
|
+
close();
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* INPA's Select: tick the logical lines to keep on screen.
|
|
88
|
+
* @param {string[]} names - the screen's named logical lines
|
|
89
|
+
* @param {boolean} multiple - a tick list rather than one choice
|
|
90
|
+
* @param {Set<string>|null} current - the lines currently kept
|
|
91
|
+
* @returns {Promise<string[]|null>} the names to keep ([] = show all), or
|
|
92
|
+
* null when cancelled
|
|
93
|
+
*/
|
|
94
|
+
function ipoPickLines(names, multiple, current, hints) {
|
|
95
|
+
const where = (hints || []).length
|
|
96
|
+
? `<div class="ipo-pick-hint">On this menu, Select works after one of these keys:</div>` +
|
|
97
|
+
`<div class="ipo-pick-keys">${(hints || [])
|
|
98
|
+
.map(
|
|
99
|
+
(h) =>
|
|
100
|
+
`<span class="ipo-pick-key"><b>${esc(h.key)}</b> ${esc(h.label)} <span class="ipo-pick-n">${h.lines} lines</span></span>`
|
|
101
|
+
)
|
|
102
|
+
.join('')}</div>`
|
|
103
|
+
: '';
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
const { overlay, close } = openModal(
|
|
106
|
+
`<div class="modal" role="dialog" aria-modal="true">
|
|
107
|
+
<div class="modal-title">Select lines</div>
|
|
108
|
+
<div class="modal-body ipo-pick">${
|
|
109
|
+
names.length
|
|
110
|
+
? names
|
|
111
|
+
.map(
|
|
112
|
+
(n, i) =>
|
|
113
|
+
`<label class="ipo-pick-row"><input type="${multiple ? 'checkbox' : 'radio'}" name="ipo-lines" value="${i}"${
|
|
114
|
+
current ? (current.has(n) ? ' checked' : '') : ''
|
|
115
|
+
}/> <span>${esc(ipoText(n))}</span></label>`
|
|
116
|
+
)
|
|
117
|
+
.join('')
|
|
118
|
+
: `<div class="ipo-pick-hint">This screen has no lines to choose from. Select keeps only the entries you pick on a screen that lists several; Deselect shows them all again.</div>${where}`
|
|
119
|
+
}</div>
|
|
120
|
+
<div class="modal-actions">${
|
|
121
|
+
names.length
|
|
122
|
+
? `<button class="btn" data-x="cancel">Cancel</button>
|
|
123
|
+
<button class="btn" data-x="all">Show all</button>
|
|
124
|
+
<button class="btn primary" data-x="ok">Show selected</button>`
|
|
125
|
+
: `<button class="btn primary" data-x="cancel">Close</button>`
|
|
126
|
+
}</div></div>`,
|
|
127
|
+
{ onClose: () => resolve(null) }
|
|
128
|
+
);
|
|
129
|
+
let settled = false;
|
|
130
|
+
const done = (v) => {
|
|
131
|
+
if (settled) return;
|
|
132
|
+
settled = true;
|
|
133
|
+
resolve(v);
|
|
134
|
+
close();
|
|
135
|
+
};
|
|
136
|
+
overlay.querySelectorAll('[data-x]').forEach((b) => {
|
|
137
|
+
b.onclick = () => {
|
|
138
|
+
const x = b.dataset.x;
|
|
139
|
+
if (x === 'cancel') return done(null);
|
|
140
|
+
if (x === 'all') return done([]);
|
|
141
|
+
const picked = [
|
|
142
|
+
...overlay.querySelectorAll('input[name="ipo-lines"]:checked'),
|
|
143
|
+
].map((el) => names[Number(el.value)]);
|
|
144
|
+
done(picked);
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* A %STATE park: show the state's name with a Continue key when the parked
|
|
152
|
+
* segment tests a keypress flag, and a Stop key; tick on after
|
|
153
|
+
* IPO_TICK_MS on both the page clock and the worker clock (a hidden tab's
|
|
154
|
+
* setTimeout is throttled).
|
|
155
|
+
* @param {HTMLElement|null} machineEl - the machine strip (none: tick at once)
|
|
156
|
+
* @param {IpoStep} step - the yield suspension
|
|
157
|
+
* @param {Set<number>} guards - the keypress flags the segment tests
|
|
158
|
+
* @returns {Promise<'tick'|'press'|'stop'>}
|
|
159
|
+
*/
|
|
160
|
+
function ipoMachineTick(machineEl, step, guards) {
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
if (!machineEl) return resolve('tick');
|
|
163
|
+
machineEl.hidden = false;
|
|
164
|
+
const name = String(step.name || '').replace(/^%/, '');
|
|
165
|
+
machineEl.innerHTML =
|
|
166
|
+
`<span class="ipo-machine-state">${esc(name)}</span>` +
|
|
167
|
+
(guards && guards.size
|
|
168
|
+
? ` <button class="btn primary ipo-continue">Continue</button>`
|
|
169
|
+
: '') +
|
|
170
|
+
` <button class="btn ipo-stop">Stop</button>`;
|
|
171
|
+
let settled = false;
|
|
172
|
+
const done = (v) => {
|
|
173
|
+
if (settled) return;
|
|
174
|
+
settled = true;
|
|
175
|
+
clearTimeout(t);
|
|
176
|
+
// the park is over either way; the next park draws its own control
|
|
177
|
+
machineEl.hidden = true;
|
|
178
|
+
resolve(v);
|
|
179
|
+
};
|
|
180
|
+
const t = setTimeout(() => done('tick'), IPO_TICK_MS);
|
|
181
|
+
if (typeof bmwSleep === 'function')
|
|
182
|
+
bmwSleep(IPO_TICK_MS).then(() => done('tick'));
|
|
183
|
+
const c = machineEl.querySelector('.ipo-continue');
|
|
184
|
+
if (c) c.onclick = () => done('press');
|
|
185
|
+
machineEl.querySelector('.ipo-stop').onclick = () => {
|
|
186
|
+
machineEl.hidden = true;
|
|
187
|
+
done('stop');
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** what the save-as dialog suggests: the module's protocol as text */
|
|
193
|
+
const IPO_SAVE_SUGGESTED = 'fault-memory.txt';
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* INPA's save-as dialog. Chrome and Edge open the platform's own picker
|
|
197
|
+
* (File System Access); elsewhere a name is asked for and the file is
|
|
198
|
+
* downloaded when the script has written it.
|
|
199
|
+
* @param {object} p - the running program
|
|
200
|
+
* @param {{filter: string}} step - the 'file' suspension
|
|
201
|
+
* @returns {Promise<{name: string, handle?: object}|null>} null = cancelled
|
|
202
|
+
*/
|
|
203
|
+
async function ipoSaveFilePick(p, step) {
|
|
204
|
+
const suggested = IPO_SAVE_SUGGESTED;
|
|
205
|
+
if (typeof window.showSaveFilePicker === 'function') {
|
|
206
|
+
try {
|
|
207
|
+
const handle = await window.showSaveFilePicker({
|
|
208
|
+
suggestedName: suggested,
|
|
209
|
+
types: [{ description: 'Text', accept: { 'text/plain': ['.txt'] } }],
|
|
210
|
+
});
|
|
211
|
+
return { name: handle.name, handle };
|
|
212
|
+
} catch (e) {
|
|
213
|
+
return null; // AbortError: the user cancelled
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const name = await inputDialog({
|
|
217
|
+
title: 'Save as',
|
|
218
|
+
body: 'File name for the download.',
|
|
219
|
+
kind: 'text',
|
|
220
|
+
example: suggested,
|
|
221
|
+
confirmLabel: 'Save',
|
|
222
|
+
});
|
|
223
|
+
if (name == null || !String(name).trim()) return null;
|
|
224
|
+
return { name: String(name).trim() };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Write the lines the script produced to what the picker chose.
|
|
229
|
+
* @param {{name: string, handle?: object}} picked - the dialog's answer
|
|
230
|
+
* @param {string[]} lines - the file's lines
|
|
231
|
+
* @returns {Promise<void>}
|
|
232
|
+
*/
|
|
233
|
+
async function ipoSaveFileWrite(picked, lines) {
|
|
234
|
+
const text = lines.join('\r\n') + '\r\n';
|
|
235
|
+
if (picked.handle && typeof picked.handle.createWritable === 'function') {
|
|
236
|
+
const w = await picked.handle.createWritable();
|
|
237
|
+
await w.write(text);
|
|
238
|
+
await w.close();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const blob = new Blob([text], { type: 'text/plain' });
|
|
242
|
+
const url = URL.createObjectURL(blob);
|
|
243
|
+
const a = document.createElement('a');
|
|
244
|
+
a.href = url;
|
|
245
|
+
a.download = picked.name;
|
|
246
|
+
document.body.appendChild(a);
|
|
247
|
+
a.click();
|
|
248
|
+
a.remove();
|
|
249
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
253
|
+
module.exports = { ipoPickComponent, ipoPickLines, ipoMachineTick };
|
|
254
|
+
}
|