bmweb-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +351 -0
  3. package/dist/bmweb.js +2790 -0
  4. package/package.json +53 -0
  5. package/runtime/core/bestvm/codec.js +285 -0
  6. package/runtime/core/bestvm/environment.js +116 -0
  7. package/runtime/core/bestvm/executor.js +1483 -0
  8. package/runtime/core/bestvm/index.js +52 -0
  9. package/runtime/core/bestvm/machine.js +491 -0
  10. package/runtime/core/bestvm/operands.js +356 -0
  11. package/runtime/core/bestvm/registers.js +152 -0
  12. package/runtime/core/bestvm/write-guard.js +111 -0
  13. package/runtime/core/ipofile/compile.js +364 -0
  14. package/runtime/core/ipofile/decls.js +187 -0
  15. package/runtime/core/ipofile/emit.js +708 -0
  16. package/runtime/core/ipofile/exec.js +164 -0
  17. package/runtime/core/ipofile/lex.js +243 -0
  18. package/runtime/core/ipofile/parse.js +550 -0
  19. package/runtime/core/ipofile/pool.js +404 -0
  20. package/runtime/core/ipofile/walk.js +431 -0
  21. package/runtime/core/ipovm/builtin-helpers.js +182 -0
  22. package/runtime/core/ipovm/builtins-api.js +610 -0
  23. package/runtime/core/ipovm/builtins-screen.js +493 -0
  24. package/runtime/core/ipovm/builtins-table.js +166 -0
  25. package/runtime/core/ipovm/builtins-text.js +166 -0
  26. package/runtime/core/ipovm/emissions.js +138 -0
  27. package/runtime/core/ipovm/hosts.js +191 -0
  28. package/runtime/core/ipovm/operators.js +229 -0
  29. package/runtime/core/ipovm/structures.js +250 -0
  30. package/runtime/core/ipovm/suspensions.js +241 -0
  31. package/runtime/core/ipovm/tape.js +206 -0
  32. package/runtime/core/ipovm/values.js +241 -0
  33. package/runtime/core/ipovm/vm.js +1166 -0
  34. package/runtime/core/translate.js +526 -0
  35. package/runtime/core/webshim/api-router.js +592 -0
  36. package/runtime/core/webshim/bus.js +95 -0
  37. package/runtime/core/webshim/coding.js +82 -0
  38. package/runtime/core/webshim/data-fetch.js +66 -0
  39. package/runtime/core/webshim/exchange.js +288 -0
  40. package/runtime/core/webshim/framing.js +331 -0
  41. package/runtime/core/webshim/install.js +30 -0
  42. package/runtime/core/webshim/job-runner.js +319 -0
  43. package/runtime/core/webshim/native-bus.js +108 -0
  44. package/runtime/core/webshim/timers.js +82 -0
  45. package/runtime/core/webshim/trace.js +205 -0
  46. package/runtime/core/webshim/transport-base.js +128 -0
  47. package/runtime/core/webshim/variant-resolver.js +249 -0
  48. package/runtime/core/webshim/web-serial-bus.js +734 -0
  49. package/runtime/home/bmweb-home.ips +76 -0
  50. package/runtime/home/bmweb.h +26 -0
  51. package/runtime/screens/activations.js +258 -0
  52. package/runtime/screens/garage/diff.js +331 -0
  53. package/runtime/screens/garage/share.js +276 -0
  54. package/runtime/screens/garage/store.js +547 -0
  55. package/runtime/screens/ipo-runtime/cells.js +176 -0
  56. package/runtime/screens/ipo-runtime/dialogs.js +254 -0
  57. package/runtime/screens/ipo-runtime/home.js +358 -0
  58. package/runtime/screens/ipo-runtime/open.js +393 -0
  59. package/runtime/screens/ipo-runtime/paint-grid.js +106 -0
  60. package/runtime/screens/ipo-runtime/paint-modern.js +424 -0
  61. package/runtime/screens/ipo-runtime/print.js +281 -0
  62. package/runtime/screens/ipo-runtime/program.js +1337 -0
  63. package/runtime/screens/ipo-runtime/protocol.js +464 -0
  64. package/runtime/screens/ipo-runtime/script-scan.js +225 -0
  65. package/runtime/screens/ipo-runtime/translate-sets.js +130 -0
  66. package/runtime/screens/ipo-runtime/ui.js +249 -0
  67. package/runtime/screens/ipo-runtime/wire-policy.js +113 -0
  68. package/runtime/screens/ir.js +324 -0
  69. package/runtime/screens/search/data.js +153 -0
  70. package/runtime/screens/search/match.js +285 -0
  71. package/runtime/screens/search/open.js +66 -0
  72. package/runtime/vendor/fflate.min.js +1 -0
@@ -0,0 +1,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
+ }