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,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The modern skin: the same cycle's emissions grouped the way the
|
|
3
|
+
* script wrote them -- one row per LINE block -- and a menu's backdrop
|
|
4
|
+
* legend shown as the function-group tiles the app has always used. Nothing
|
|
5
|
+
* here decides what a line means: a row exists because the program painted
|
|
6
|
+
* it this cycle, keyed by the result the script bound.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* INPA's key-legend notation as a menu's backdrop prints it: "< F4 >
|
|
11
|
+
* Fehlerspeicher lesen", "< Shift > + < F6 > EWS". Group 1 = the Shift
|
|
12
|
+
* prefix, 2 = the key number, 3 = the caption.
|
|
13
|
+
* @type {RegExp}
|
|
14
|
+
*/
|
|
15
|
+
const IPO_KEY_LEGEND =
|
|
16
|
+
/^\s*(<\s*Shift\s*>\s*\+\s*)?<\s*F\s*(\d+)\s*>\s*(\S.*)$/i;
|
|
17
|
+
|
|
18
|
+
/** How many tiles enter with a stagger before the animation is skipped. */
|
|
19
|
+
const IPO_TILE_STAGGER = 20;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One row of the modern skin.
|
|
23
|
+
* @typedef {object} IpoRow
|
|
24
|
+
* @property {string} caption - the caption pieces joined
|
|
25
|
+
* @property {string[]} parts - the caption pieces as printed
|
|
26
|
+
* @property {IpoRowCell[]} cells - the value/lamp/gauge cells
|
|
27
|
+
* @property {string} trailer - text printed after the values (a unit)
|
|
28
|
+
* @property {boolean} band - first row of its logical line
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A cell of a modern row.
|
|
33
|
+
* @typedef {object} IpoRowCell
|
|
34
|
+
* @property {'text'|'value'|'lamp'|'gauge'} kind - what drew it
|
|
35
|
+
* @property {string|null} key - the result key
|
|
36
|
+
* @property {string} text - the text
|
|
37
|
+
* @property {string} [on] - a lamp's TrueText
|
|
38
|
+
* @property {string} [off] - a lamp's FalseText
|
|
39
|
+
* @property {IpoCellMeta|null} meta - the instrument's declaration
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A menu shown as tiles.
|
|
44
|
+
* @typedef {object} IpoMenuTiles
|
|
45
|
+
* @property {Array<{nr: number, shift: boolean, label: string, legend: string, cat: string}>} tiles - one per key
|
|
46
|
+
* @property {IpoRow[]} rest - the rows that were not legend lines
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The rows of the last cycle, one per LINE block (a LINE that prints several
|
|
51
|
+
* screen rows is several rows here too: INPA laid them out one under the
|
|
52
|
+
* other). Within a row the text elements before the first value are its
|
|
53
|
+
* caption, the value/lamp/gauge elements are its cells, text after a value
|
|
54
|
+
* is a unit or trailer. A row with only text is a note -- unless it was
|
|
55
|
+
* printed in two or more pieces at different columns, which is a caption
|
|
56
|
+
* and its value (ZKE5's info screen: "Rework program" at column 0, ":" at
|
|
57
|
+
* 33, the name at 35 -- copied into strings at startup and printed with
|
|
58
|
+
* ftextout). The first piece is the caption, a lone ":" is dropped, the
|
|
59
|
+
* rest are the value.
|
|
60
|
+
* @param {IpoProgram} p - the program (its lines and cells)
|
|
61
|
+
* @returns {IpoRow[]}
|
|
62
|
+
*/
|
|
63
|
+
function ipoLineRows(p) {
|
|
64
|
+
const rows = [];
|
|
65
|
+
for (const ln of p.lines || []) {
|
|
66
|
+
const els = (ln.elements || [])
|
|
67
|
+
.filter((el) => el.row != null && el.col != null)
|
|
68
|
+
.sort((a, b) => a.row - b.row || a.col - b.col);
|
|
69
|
+
if (!els.length) continue;
|
|
70
|
+
const cellOf = (el) => p.cells.get(`${el.row}:${el.col}`);
|
|
71
|
+
const byRow = new Map();
|
|
72
|
+
for (const el of els) {
|
|
73
|
+
if (!byRow.has(el.row)) byRow.set(el.row, []);
|
|
74
|
+
byRow.get(el.row).push(el);
|
|
75
|
+
}
|
|
76
|
+
let first = true;
|
|
77
|
+
const groups = [...byRow.values()];
|
|
78
|
+
for (let gi = 0; gi < groups.length; gi++) {
|
|
79
|
+
const group = groups[gi];
|
|
80
|
+
// INPA's status idiom: captions on one screen row, the lamps or bars
|
|
81
|
+
// under them on the next, column by column ("Klemme 15" at col 1 over
|
|
82
|
+
// its lamp at col 1, "Motor steht" at col 30 over its own). Read as
|
|
83
|
+
// rows those were a caption row with the second caption taken for a
|
|
84
|
+
// value, then an anonymous row of lamps. Pair each cell with the
|
|
85
|
+
// caption standing over it and emit one row per pair.
|
|
86
|
+
const paired = ipoPairCaptionRow(group, groups[gi + 1], cellOf);
|
|
87
|
+
if (paired) {
|
|
88
|
+
for (const r of paired) {
|
|
89
|
+
rows.push({ ...r, band: first });
|
|
90
|
+
first = false;
|
|
91
|
+
}
|
|
92
|
+
gi += 1; // the cell row is consumed
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const caption = [];
|
|
96
|
+
const cells = [];
|
|
97
|
+
const trailer = [];
|
|
98
|
+
for (const el of group) {
|
|
99
|
+
const c = cellOf(el);
|
|
100
|
+
const text = c ? c.text : '';
|
|
101
|
+
if (el.t === 'text') {
|
|
102
|
+
if (!text.trim()) continue;
|
|
103
|
+
(cells.length ? trailer : caption).push(text.trim());
|
|
104
|
+
} else {
|
|
105
|
+
cells.push({
|
|
106
|
+
kind: el.t,
|
|
107
|
+
key: el.key || null,
|
|
108
|
+
text,
|
|
109
|
+
on: el.on,
|
|
110
|
+
off: el.off,
|
|
111
|
+
meta: c ? c.meta : null,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!caption.length && !cells.length) continue;
|
|
116
|
+
if (
|
|
117
|
+
!cells.length &&
|
|
118
|
+
caption.length >= 2 &&
|
|
119
|
+
!IPO_KEY_LEGEND.test(caption[0])
|
|
120
|
+
) {
|
|
121
|
+
const rest = caption.slice(1).filter((t) => t !== ':');
|
|
122
|
+
if (rest.length) {
|
|
123
|
+
for (const t of rest)
|
|
124
|
+
cells.push({ kind: 'text', key: null, text: t, meta: null });
|
|
125
|
+
caption.length = 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
rows.push({
|
|
129
|
+
caption: caption.join(' '),
|
|
130
|
+
parts: caption,
|
|
131
|
+
cells,
|
|
132
|
+
trailer: trailer.join(' '),
|
|
133
|
+
band: first, // first row of its logical line
|
|
134
|
+
});
|
|
135
|
+
first = false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return rows;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* A caption row over a cell row, paired by column: each lamp/bar takes the
|
|
143
|
+
* nearest caption at or left of its column. Null when the two rows are not
|
|
144
|
+
* that shape (the top row has a non-text element, the bottom row has text,
|
|
145
|
+
* or two cells would share a caption -- a heading over several lamps stays
|
|
146
|
+
* a heading).
|
|
147
|
+
* @param {IpoElement[]} top - the elements of one screen row
|
|
148
|
+
* @param {IpoElement[]|undefined} bottom - the elements of the next row
|
|
149
|
+
* @param {(el: IpoElement) => IpoCell|undefined} cellOf - the painted cell for an element
|
|
150
|
+
* @returns {Array<{caption: string, parts: string[], cells: IpoRowCell[], trailer: string}>|null}
|
|
151
|
+
*/
|
|
152
|
+
function ipoPairCaptionRow(top, bottom, cellOf) {
|
|
153
|
+
if (!bottom || !bottom.length) return null;
|
|
154
|
+
const textOf = (el) => ((cellOf(el) || {}).text || '').trim();
|
|
155
|
+
// a caption is printed text, or a result printed as text that reads as a
|
|
156
|
+
// label (a measurement block prints STAT_MESSWERTn_TEXT over each bar)
|
|
157
|
+
const isLabel = (el) =>
|
|
158
|
+
el.t === 'text' ||
|
|
159
|
+
(el.t === 'value' &&
|
|
160
|
+
/[A-Za-z]/.test(textOf(el)) &&
|
|
161
|
+
!/^-?\d/.test(textOf(el)));
|
|
162
|
+
if (!top.every(isLabel)) return null;
|
|
163
|
+
// the row under it holds lamps or bars, plus printed units after them
|
|
164
|
+
if (!bottom.some((el) => el.t === 'lamp' || el.t === 'gauge')) return null;
|
|
165
|
+
if (
|
|
166
|
+
bottom.some((el) => el.t !== 'lamp' && el.t !== 'gauge' && el.t !== 'text')
|
|
167
|
+
)
|
|
168
|
+
return null;
|
|
169
|
+
const caps = top
|
|
170
|
+
.map((el) => ({ col: el.col, text: textOf(el) }))
|
|
171
|
+
.sort((a, b) => a.col - b.col);
|
|
172
|
+
if (!caps.length) return null;
|
|
173
|
+
const sorted = [...bottom].sort((a, b) => a.col - b.col);
|
|
174
|
+
const used = new Set();
|
|
175
|
+
const out = [];
|
|
176
|
+
for (const el of sorted) {
|
|
177
|
+
if (el.t === 'text') {
|
|
178
|
+
// a unit or note printed after a cell belongs to that cell
|
|
179
|
+
const t = textOf(el);
|
|
180
|
+
if (!out.length) return null;
|
|
181
|
+
if (t) {
|
|
182
|
+
const last = out[out.length - 1];
|
|
183
|
+
last.trailer = `${last.trailer} ${t}`.trim();
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
let cap = null;
|
|
188
|
+
for (const c of caps) if (c.col <= el.col + 1) cap = c;
|
|
189
|
+
if (!cap) cap = caps[0];
|
|
190
|
+
if (used.has(cap)) return null;
|
|
191
|
+
used.add(cap);
|
|
192
|
+
const c = cellOf(el);
|
|
193
|
+
out.push({
|
|
194
|
+
caption: cap.text,
|
|
195
|
+
parts: cap.text ? [cap.text] : [],
|
|
196
|
+
cells: [
|
|
197
|
+
{
|
|
198
|
+
kind: el.t,
|
|
199
|
+
key: el.key || null,
|
|
200
|
+
text: c ? c.text : '',
|
|
201
|
+
on: el.on,
|
|
202
|
+
off: el.off,
|
|
203
|
+
meta: c ? c.meta : null,
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
trailer: '',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A tile's category class from the SGBD job the key's body sends (a fixed
|
|
214
|
+
* vocabulary), never from the caption.
|
|
215
|
+
* @param {string|null|undefined} job - job name
|
|
216
|
+
* @returns {string}
|
|
217
|
+
*/
|
|
218
|
+
function ipoJobCategory(job) {
|
|
219
|
+
if (!job) return '';
|
|
220
|
+
if (/^(FS|IS|HS)_/i.test(job)) return 'gt-fault';
|
|
221
|
+
if (/^(STATUS|MESSWERT)/i.test(job)) return 'gt-live';
|
|
222
|
+
if (/^(STEUERN|START)/i.test(job)) return 'gt-act';
|
|
223
|
+
if (/IDENT|^INFO$/i.test(job)) return 'gt-info';
|
|
224
|
+
if (/COD|ADAPT|ABGLEICH/i.test(job)) return 'gt-code';
|
|
225
|
+
return '';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* nr -> the caption the current screen printed for that key in its legend.
|
|
230
|
+
* @param {IpoProgram} p - the program
|
|
231
|
+
* @param {string[]|null} [notes] - collects the caption pieces that are not legend lines
|
|
232
|
+
* @returns {Map<number, string>}
|
|
233
|
+
*/
|
|
234
|
+
function ipoLegendMap(p, notes) {
|
|
235
|
+
const legend = new Map();
|
|
236
|
+
for (const r of ipoLineRows(p)) {
|
|
237
|
+
for (const t of r.parts || []) {
|
|
238
|
+
const m = t.match(IPO_KEY_LEGEND);
|
|
239
|
+
if (!m) {
|
|
240
|
+
if (notes) notes.push(t);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const nr = Number(m[2]) + (m[1] ? IPO_SHIFT_BASE : 0);
|
|
244
|
+
if (!legend.has(nr)) legend.set(nr, m[3].trim());
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return legend;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* A menu's backdrop as tiles: one per ITEM the script declared, captioned
|
|
252
|
+
* by the ITEM's own short label, described by the legend line the screen
|
|
253
|
+
* printed for that key. A screen that prints no legend is a readout, drawn
|
|
254
|
+
* as rows (null). ZKE5's main screen prints BOTH -- the part number and
|
|
255
|
+
* build date above the legend -- so those rows stay rows (`rest`) and the
|
|
256
|
+
* legend becomes the tiles.
|
|
257
|
+
* @param {IpoProgram} p - the program
|
|
258
|
+
* @returns {IpoMenuTiles|null}
|
|
259
|
+
*/
|
|
260
|
+
function ipoMenuTiles(p) {
|
|
261
|
+
const rows = ipoLineRows(p);
|
|
262
|
+
const legend = ipoLegendMap(p, null);
|
|
263
|
+
if (!legend.size) return null;
|
|
264
|
+
const items = (p.items || []).filter((it) => !it.hidden || legend.has(it.nr));
|
|
265
|
+
if (!items.length || !items.some((it) => legend.has(it.nr))) return null;
|
|
266
|
+
const rest = rows.filter(
|
|
267
|
+
(r) => !(r.parts || []).some((t) => IPO_KEY_LEGEND.test(t))
|
|
268
|
+
);
|
|
269
|
+
const toks = p.exec && p.exec.procs ? p.exec.procs[p.menu] : null;
|
|
270
|
+
const tiles = items.map((it) => {
|
|
271
|
+
let cat = '';
|
|
272
|
+
if (toks && typeof irItemBodyJobs === 'function') {
|
|
273
|
+
const jobs = irItemBodyJobs(p.exec, toks, it.start, it.end) || [];
|
|
274
|
+
for (const j of jobs) {
|
|
275
|
+
cat = ipoJobCategory(j);
|
|
276
|
+
if (cat) break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
nr: it.nr,
|
|
281
|
+
shift: !!it.shift,
|
|
282
|
+
label: it.label || legend.get(it.nr) || '',
|
|
283
|
+
legend: legend.get(it.nr) || '',
|
|
284
|
+
cat: cat || 'gt-default',
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
return { tiles, rest };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Paint a menu's tiles. A backdrop screen can be frequent: the cycle
|
|
292
|
+
* repaints every tick. The tiles are rebuilt (and their entrance animation
|
|
293
|
+
* replayed) only when what they show changed, else the grid is left exactly
|
|
294
|
+
* as it is.
|
|
295
|
+
* @param {HTMLElement} gridEl - the screen container
|
|
296
|
+
* @param {IpoProgram} p - the program
|
|
297
|
+
* @param {IpoMenuTiles} menu - the tiles to draw
|
|
298
|
+
* @returns {void}
|
|
299
|
+
*/
|
|
300
|
+
function ipoPaintTiles(gridEl, p, menu) {
|
|
301
|
+
const sig = JSON.stringify([p.menu, menu.tiles, menu.rest]);
|
|
302
|
+
if (gridEl._ipoTilesSig === sig) return;
|
|
303
|
+
gridEl._ipoTilesSig = sig;
|
|
304
|
+
const notes = ipoRowsHtml(menu.rest);
|
|
305
|
+
const grid = document.createElement('div');
|
|
306
|
+
grid.className = 'group-grid stagger';
|
|
307
|
+
for (const t of menu.tiles) {
|
|
308
|
+
const tile = document.createElement('div');
|
|
309
|
+
tile.className = `group-tile ${t.cat}`;
|
|
310
|
+
const fk = `${t.shift ? 'Shift+' : ''}F${t.nr > IPO_SHIFT_BASE ? t.nr - IPO_SHIFT_BASE : t.nr}`;
|
|
311
|
+
const name = ipoText(t.label);
|
|
312
|
+
const desc = t.legend ? ipoText(t.legend) : '';
|
|
313
|
+
tile.innerHTML =
|
|
314
|
+
`<div class="group-header-row"><span class="group-fkey">${esc(fk)}</span></div>` +
|
|
315
|
+
`<div class="group-name">${esc(name)}</div>` +
|
|
316
|
+
`<div class="group-count">${esc(desc !== name ? desc : '')}</div>` +
|
|
317
|
+
`<div class="group-arrow">→</div>`;
|
|
318
|
+
tile.onclick = () => p.press(t.nr);
|
|
319
|
+
grid.appendChild(tile);
|
|
320
|
+
}
|
|
321
|
+
gridEl.classList.add('ipo-tiles');
|
|
322
|
+
gridEl.innerHTML = notes ? `<div class="ipo-notes">${notes}</div>` : '';
|
|
323
|
+
gridEl.appendChild(grid);
|
|
324
|
+
if (typeof stagger === 'function') stagger(grid, IPO_TILE_STAGGER);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Paint the modern skin: a menu's tiles when the screen printed a legend,
|
|
329
|
+
* else the LINE rows; a screen func that printed without LINE blocks (a
|
|
330
|
+
* userbox, a banner) falls back to its rows of text in order.
|
|
331
|
+
* @param {HTMLElement} gridEl - the screen container
|
|
332
|
+
* @param {IpoProgram} p - the program
|
|
333
|
+
* @returns {void}
|
|
334
|
+
*/
|
|
335
|
+
function ipoPaintLines(gridEl, p) {
|
|
336
|
+
const menu = ipoMenuTiles(p);
|
|
337
|
+
if (menu) {
|
|
338
|
+
ipoPaintTiles(gridEl, p, menu);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
gridEl._ipoTilesSig = null;
|
|
342
|
+
gridEl.classList.remove('ipo-tiles');
|
|
343
|
+
const rows = ipoLineRows(p);
|
|
344
|
+
if (!rows.length) {
|
|
345
|
+
const texts = [...p.cells.values()]
|
|
346
|
+
.filter((c) => c.text && c.text.trim())
|
|
347
|
+
.sort((a, b) => a.row - b.row || a.col - b.col);
|
|
348
|
+
if (!texts.length) {
|
|
349
|
+
gridEl.innerHTML = `<div class="ipo-empty">${esc(ipoText(p.title || ''))}</div>`;
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const byRow = new Map();
|
|
353
|
+
for (const c of texts) {
|
|
354
|
+
if (!byRow.has(c.row)) byRow.set(c.row, []);
|
|
355
|
+
byRow.get(c.row).push(ipoText(c.text).trim());
|
|
356
|
+
}
|
|
357
|
+
gridEl.innerHTML = [...byRow.values()]
|
|
358
|
+
.map(
|
|
359
|
+
(parts) =>
|
|
360
|
+
`<div class="ipo-line ipo-line-note">${esc(parts.join(' '))}</div>`
|
|
361
|
+
)
|
|
362
|
+
.join('');
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
gridEl.innerHTML = ipoRowsHtml(rows);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The modern rows' HTML (see ipoLineRows). A logical line's first row gets
|
|
370
|
+
* air above it (one LINE per fault); a row with no caption is the value
|
|
371
|
+
* itself (a fault's own header row).
|
|
372
|
+
* @param {IpoRow[]} rows - the rows
|
|
373
|
+
* @returns {string} HTML
|
|
374
|
+
*/
|
|
375
|
+
function ipoRowsHtml(rows) {
|
|
376
|
+
const html = [];
|
|
377
|
+
const capHtml = (parts) =>
|
|
378
|
+
parts
|
|
379
|
+
.map((t) => esc(ipoText(t).replace(/\s*[:=]\s*$/, '')))
|
|
380
|
+
.join('<span class="ipo-line-gap"></span>');
|
|
381
|
+
for (let i = 0; i < rows.length; i++) {
|
|
382
|
+
const r = rows[i];
|
|
383
|
+
const cap = capHtml(r.parts || [r.caption]);
|
|
384
|
+
const band = i > 0 && r.band ? ' ipo-band' : '';
|
|
385
|
+
if (!r.cells.length) {
|
|
386
|
+
html.push(`<div class="ipo-line ipo-line-note${band}">${cap}</div>`);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const cells = r.cells
|
|
390
|
+
.map((c) => {
|
|
391
|
+
const text = ipoText(c.text);
|
|
392
|
+
const key = c.key ? ` data-key="${esc(c.key)}"` : '';
|
|
393
|
+
if (c.kind === 'lamp') return ipoLampHtml(c);
|
|
394
|
+
if (c.kind === 'gauge') return ipoGaugeHtml(c);
|
|
395
|
+
return `<span class="ipo-line-val ipo-${c.kind}"${key}>${esc(text.trim())}</span>`;
|
|
396
|
+
})
|
|
397
|
+
.join('');
|
|
398
|
+
const unit = r.trailer
|
|
399
|
+
? `<span class="ipo-line-unit">${esc(ipoText(r.trailer))}</span>`
|
|
400
|
+
: '';
|
|
401
|
+
const nocap = r.parts && r.parts.length ? '' : ' ipo-nocap';
|
|
402
|
+
html.push(
|
|
403
|
+
`<div class="ipo-line${band}${nocap}">` +
|
|
404
|
+
`<span class="ipo-line-cap">${cap}</span>` +
|
|
405
|
+
`<span class="ipo-line-cells">${cells}${unit}</span>` +
|
|
406
|
+
`</div>`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
return html.join('');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
413
|
+
module.exports = {
|
|
414
|
+
IPO_KEY_LEGEND,
|
|
415
|
+
ipoLineRows,
|
|
416
|
+
ipoPairCaptionRow,
|
|
417
|
+
ipoJobCategory,
|
|
418
|
+
ipoLegendMap,
|
|
419
|
+
ipoMenuTiles,
|
|
420
|
+
ipoPaintTiles,
|
|
421
|
+
ipoPaintLines,
|
|
422
|
+
ipoRowsHtml,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file INPA's Print screen (the script's printscreen, the F9 key, Cmd/Ctrl+P
|
|
3
|
+
* on a module view) as a clean sheet: the module and menu named at the top,
|
|
4
|
+
* the screen as the car answered it, the keys the script offers, and no app
|
|
5
|
+
* chrome. In INPA mode the sheet is the 80-column text grid, lamps and bars
|
|
6
|
+
* spelled out; in modern mode it is the same rows as a caption / value /
|
|
7
|
+
* unit table. Built on core/print.js, so it paginates and prints
|
|
8
|
+
* black-on-white whatever theme the app wears.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Columns of INPA's virtual screen the printed grid keeps. */
|
|
12
|
+
const IPO_PRINT_COLS = 80;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One grid row as plain text: cells placed at their columns, a lamp as
|
|
16
|
+
* "[x] word" / "[ ] word", a bar as its reading followed by the declared
|
|
17
|
+
* scale.
|
|
18
|
+
* @param {Array<{col: number, text: string, kind: string, meta: IpoCellMeta|null}>} cells - the row's cells, column order
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
function ipoPrintRowText(cells) {
|
|
22
|
+
let out = '';
|
|
23
|
+
for (const c of cells) {
|
|
24
|
+
const gap = Math.max(c.col - out.length, out.length > 0 ? 1 : 0);
|
|
25
|
+
out += ' '.repeat(gap);
|
|
26
|
+
if (c.kind === 'lamp') {
|
|
27
|
+
out += `[${ipoLampOn(c) ? 'x' : ' '}] ${ipoText(c.text).trim()}`;
|
|
28
|
+
} else if (c.kind === 'gauge') {
|
|
29
|
+
out += `${ipoText(c.text).trim()}${ipoPrintScale(c.meta)}`;
|
|
30
|
+
} else {
|
|
31
|
+
out += c.text;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out.replace(/\s+$/, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A bar's declared scale for the sheet: " (min..max)" when it has one.
|
|
39
|
+
* @param {IpoCellMeta|null} m - the cell's declaration
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
function ipoPrintScale(m) {
|
|
43
|
+
if (
|
|
44
|
+
!m ||
|
|
45
|
+
m.min == null ||
|
|
46
|
+
m.max == null ||
|
|
47
|
+
!Number.isFinite(m.min) ||
|
|
48
|
+
!Number.isFinite(m.max)
|
|
49
|
+
)
|
|
50
|
+
return '';
|
|
51
|
+
return ` (${ipoScaleLabel(m.min)}..${ipoScaleLabel(m.max)})`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The INPA grid as text lines, top row to the last printed one, blank rows
|
|
56
|
+
* kept so the layout INPA chose survives on paper.
|
|
57
|
+
* @param {IpoProgram} p - the program
|
|
58
|
+
* @returns {string[]}
|
|
59
|
+
*/
|
|
60
|
+
function ipoPrintGridText(p) {
|
|
61
|
+
const byRow = ipoGridRows(p);
|
|
62
|
+
const rows = [...byRow.keys()].sort((a, b) => a - b);
|
|
63
|
+
if (!rows.length) return [];
|
|
64
|
+
const last = rows[rows.length - 1];
|
|
65
|
+
const out = [];
|
|
66
|
+
for (let r = 0; r <= last; r++) {
|
|
67
|
+
const cells = (byRow.get(r) || []).sort((a, b) => a.col - b.col);
|
|
68
|
+
out.push(cells.length ? ipoPrintRowText(cells) : '');
|
|
69
|
+
}
|
|
70
|
+
// trailing blank rows only lengthen the sheet
|
|
71
|
+
while (out.length && !out[out.length - 1]) out.pop();
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A modern-mode cell as text for the sheet.
|
|
77
|
+
* @param {{kind: string, text: string, meta: IpoCellMeta|null}} c - the cell
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
function ipoPrintCellText(c) {
|
|
81
|
+
const text = ipoText(c.text).trim();
|
|
82
|
+
if (c.kind === 'lamp') return `${ipoLampOn(c) ? '●' : '○'} ${text}`;
|
|
83
|
+
if (c.kind === 'gauge') return `${text}${ipoPrintScale(c.meta)}`;
|
|
84
|
+
return text;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The modern rows as a caption / value / unit table, notes spanning the
|
|
89
|
+
* width, a logical line's first row separated from the one before.
|
|
90
|
+
* @param {IpoRow[]} rows - the rows (see ipoLineRows)
|
|
91
|
+
* @returns {string} HTML
|
|
92
|
+
*/
|
|
93
|
+
function ipoPrintRowsHtml(rows) {
|
|
94
|
+
if (!rows.length) return '';
|
|
95
|
+
const body = rows
|
|
96
|
+
.map((r, i) => {
|
|
97
|
+
const cap = (r.parts || [r.caption])
|
|
98
|
+
.map((t) => ipoText(t).replace(/\s*[:=]\s*$/, ''))
|
|
99
|
+
.join(' ');
|
|
100
|
+
const band = i > 0 && r.band ? ' class="pr-band"' : '';
|
|
101
|
+
if (!r.cells.length) {
|
|
102
|
+
return `<tr${band}><td colspan="3" class="pr-note">${esc(cap)}</td></tr>`;
|
|
103
|
+
}
|
|
104
|
+
const vals = r.cells.map(ipoPrintCellText).filter(Boolean).join(' ');
|
|
105
|
+
return (
|
|
106
|
+
`<tr${band}><td>${esc(cap)}</td>` +
|
|
107
|
+
`<td class="pr-mono pr-num">${esc(vals)}</td>` +
|
|
108
|
+
`<td class="pr-unit">${esc(r.trailer ? ipoText(r.trailer) : '')}</td></tr>`
|
|
109
|
+
);
|
|
110
|
+
})
|
|
111
|
+
.join('');
|
|
112
|
+
return `<table class="pr-table pr-screen-table"><tbody>${body}</tbody></table>`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The keys the script offers on this menu, drawn as the F-key bar the app
|
|
117
|
+
* shows under the screen: ten slots F1..F10 with the short caption, a
|
|
118
|
+
* second row for the Shift keys when the menu has any, and the menu's own
|
|
119
|
+
* legend text under a caption where it says more. Empty slots stay as
|
|
120
|
+
* blank boxes, as on the bar.
|
|
121
|
+
* @param {IpoProgram} p - the program
|
|
122
|
+
* @returns {string} HTML, empty when the menu has no keys
|
|
123
|
+
*/
|
|
124
|
+
function ipoPrintKeysHtml(p, inpa) {
|
|
125
|
+
const items = (p.items || []).filter((it) => it.nr !== 20);
|
|
126
|
+
if (!items.length) return '';
|
|
127
|
+
if (inpa) return ipoPrintInpaKeysHtml(p, items);
|
|
128
|
+
const menu = ipoMenuTiles(p);
|
|
129
|
+
const legend = new Map();
|
|
130
|
+
for (const t of (menu && menu.tiles) || []) {
|
|
131
|
+
if (t.legend) legend.set(`${t.shift ? 's' : ''}${t.nr}`, t.legend);
|
|
132
|
+
}
|
|
133
|
+
const byKey = new Map(items.map((it) => [it.nr, it]));
|
|
134
|
+
const row = (shift) => {
|
|
135
|
+
let any = false;
|
|
136
|
+
const slots = [];
|
|
137
|
+
for (let k = 1; k <= 10; k++) {
|
|
138
|
+
const nr = shift ? IPO_SHIFT_BASE + k : k;
|
|
139
|
+
const it = byKey.get(nr);
|
|
140
|
+
const label = it ? ipoKeyLabel(p, it) : '';
|
|
141
|
+
if (label) any = true;
|
|
142
|
+
const lg = it ? legend.get(`${shift ? 's' : ''}${nr}`) : null;
|
|
143
|
+
const sub =
|
|
144
|
+
lg && ipoText(lg).trim() !== label.trim()
|
|
145
|
+
? `<span class="pr-fkey-s">${esc(ipoText(lg).trim())}</span>`
|
|
146
|
+
: '';
|
|
147
|
+
slots.push(
|
|
148
|
+
`<span class="pr-fkey${label ? '' : ' empty'}">` +
|
|
149
|
+
`<span class="pr-fkey-k">${shift ? '⇧' : ''}F${k}</span>` +
|
|
150
|
+
`<span class="pr-fkey-l">${esc(label)}</span>${sub}</span>`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return any ? `<div class="pr-fkeys">${slots.join('')}</div>` : '';
|
|
154
|
+
};
|
|
155
|
+
const plain = row(false);
|
|
156
|
+
const shifted = row(true);
|
|
157
|
+
if (!plain && !shifted) return '';
|
|
158
|
+
return `<div class="pr-keys">${plain}${shifted}</div>`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The print document for the module view as it stands: what printDoc()
|
|
163
|
+
* takes. Pure, so the harness can pin it without a printer.
|
|
164
|
+
* @param {IpoProgram} p - the program
|
|
165
|
+
* @param {EcuRecord} ecu - the module
|
|
166
|
+
* @param {boolean} inpa - INPA layout (the text grid) rather than the modern rows
|
|
167
|
+
* @returns {PrintDocOptions}
|
|
168
|
+
*/
|
|
169
|
+
/**
|
|
170
|
+
* The key bar the way the INPA layout draws it on screen: a row of key
|
|
171
|
+
* numbers over a row of flat boxes with the caption centred, and a Shift
|
|
172
|
+
* row below when the menu has shifted keys.
|
|
173
|
+
* @param {object} p - the running program
|
|
174
|
+
* @param {object[]} items - the menu's keys (Shift+F10 excluded)
|
|
175
|
+
* @returns {string} HTML
|
|
176
|
+
*/
|
|
177
|
+
function ipoPrintInpaKeysHtml(p, items) {
|
|
178
|
+
const byKey = new Map(items.map((it) => [it.nr, it]));
|
|
179
|
+
const row = (shift) => {
|
|
180
|
+
let any = false;
|
|
181
|
+
const nums = [];
|
|
182
|
+
const btns = [];
|
|
183
|
+
for (let k = 1; k <= 10; k++) {
|
|
184
|
+
const it = byKey.get(shift ? IPO_SHIFT_BASE + k : k);
|
|
185
|
+
const label = it ? ipoKeyLabel(p, it) : '';
|
|
186
|
+
if (label) any = true;
|
|
187
|
+
nums.push(`<span class="pr-ikey-num">${shift ? '⇧' : ''}F${k}</span>`);
|
|
188
|
+
btns.push(
|
|
189
|
+
`<span class="pr-ikey${label ? '' : ' empty'}">${esc(label)}</span>`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return any
|
|
193
|
+
? `<div class="pr-ikeys"><div class="pr-ikey-nums">${nums.join('')}</div>` +
|
|
194
|
+
`<div class="pr-ikey-btns">${btns.join('')}</div></div>`
|
|
195
|
+
: '';
|
|
196
|
+
};
|
|
197
|
+
const plain = row(false);
|
|
198
|
+
const shifted = row(true);
|
|
199
|
+
if (!plain && !shifted) return '';
|
|
200
|
+
return `<div class="pr-keys">${plain}${shifted}</div>`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function ipoPrintDocument(p, ecu, inpa) {
|
|
204
|
+
const title = ipoText(p.title || '') || p.menu || '';
|
|
205
|
+
const sections = [];
|
|
206
|
+
if (
|
|
207
|
+
p.view &&
|
|
208
|
+
p.view.report &&
|
|
209
|
+
typeof ipoProtocolPrintSections === 'function'
|
|
210
|
+
) {
|
|
211
|
+
// viewopen over a fault read: the scan report's tables, then INPA's text
|
|
212
|
+
sections.push(...ipoProtocolPrintSections(p.view));
|
|
213
|
+
} else if (p.view) {
|
|
214
|
+
// viewopen: the file the script wrote
|
|
215
|
+
sections.push({
|
|
216
|
+
html: `<pre class="pr-screen">${esc((p.view.lines || []).join('\n'))}</pre>`,
|
|
217
|
+
});
|
|
218
|
+
} else if (inpa) {
|
|
219
|
+
const lines = ipoPrintGridText(p);
|
|
220
|
+
if (lines.length) {
|
|
221
|
+
sections.push({
|
|
222
|
+
html: `<pre class="pr-screen">${esc(lines.join('\n'))}</pre>`,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
const menu = ipoMenuTiles(p);
|
|
227
|
+
const rows = menu && menu.tiles.length ? menu.rest : ipoLineRows(p);
|
|
228
|
+
const html = ipoPrintRowsHtml(rows);
|
|
229
|
+
if (html) sections.push({ html });
|
|
230
|
+
}
|
|
231
|
+
const keys = ipoPrintKeysHtml(p, inpa);
|
|
232
|
+
if (keys) sections.push({ html: keys, avoidBreak: true });
|
|
233
|
+
if (!sections.length) {
|
|
234
|
+
sections.push({
|
|
235
|
+
html: `<p class="pr-p">${esc(title || 'Nothing on screen yet.')}</p>`,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
const meta = [
|
|
239
|
+
ecu.chassis ? ['Chassis', String(ecu.chassis).toUpperCase()] : null,
|
|
240
|
+
['SGBD', `${ecu.sgbd}.prg`],
|
|
241
|
+
p.menu ? ['Menu', p.menu] : null,
|
|
242
|
+
p.screen ? ['Screen', p.screen] : null,
|
|
243
|
+
['Printed', new Date().toLocaleString()],
|
|
244
|
+
];
|
|
245
|
+
return {
|
|
246
|
+
title: ecu.label || ecu.sgbd,
|
|
247
|
+
subtitle: title,
|
|
248
|
+
meta,
|
|
249
|
+
sections,
|
|
250
|
+
landscape:
|
|
251
|
+
inpa && ipoPrintGridText(p).some((l) => l.length > IPO_PRINT_COLS),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* INPA's printscreen for the module view: the clean sheet when the print
|
|
257
|
+
* builder is loaded, the browser's own print otherwise.
|
|
258
|
+
* @param {IpoProgram} p - the program
|
|
259
|
+
* @param {EcuRecord} ecu - the module
|
|
260
|
+
* @param {boolean} inpa - INPA layout
|
|
261
|
+
* @returns {Promise<void>}
|
|
262
|
+
*/
|
|
263
|
+
function ipoPrintScreen(p, ecu, inpa) {
|
|
264
|
+
if (typeof printDoc === 'function') {
|
|
265
|
+
return printDoc(ipoPrintDocument(p, ecu, inpa));
|
|
266
|
+
}
|
|
267
|
+
if (typeof window !== 'undefined' && typeof window.print === 'function')
|
|
268
|
+
window.print();
|
|
269
|
+
return Promise.resolve();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
273
|
+
module.exports = {
|
|
274
|
+
IPO_PRINT_COLS,
|
|
275
|
+
ipoPrintGridText,
|
|
276
|
+
ipoPrintRowsHtml,
|
|
277
|
+
ipoPrintKeysHtml,
|
|
278
|
+
ipoPrintDocument,
|
|
279
|
+
ipoPrintScreen,
|
|
280
|
+
};
|
|
281
|
+
}
|