bmweb-cli 0.1.0 → 0.1.2
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/README.md +10 -6
- package/dist/bmweb.js +251 -90
- package/package.json +1 -1
- package/runtime/core/bestvm/index.js +1 -3
- package/runtime/core/bestvm/machine.js +3 -2
- package/runtime/core/bestvm/write-guard.js +3 -3
- package/runtime/core/ipovm/builtins-api.js +13 -0
- package/runtime/core/ipovm/builtins-screen.js +117 -1
- package/runtime/core/ipovm/builtins-table.js +7 -5
- package/runtime/core/ipovm/emissions.js +21 -0
- package/runtime/core/ipovm/suspensions.js +32 -2
- package/runtime/core/translate.js +3 -3
- package/runtime/core/webshim/transport-base.js +3 -3
- package/runtime/screens/ipo-runtime/print.js +80 -0
- package/runtime/screens/ipo-runtime/program.js +128 -0
- package/runtime/screens/ipo-runtime/ui.js +2 -0
package/README.md
CHANGED
|
@@ -309,15 +309,19 @@ status line and the script's progress window are the two bottom lines.
|
|
|
309
309
|
With no arguments it starts on the app's own home, an INPA script of the
|
|
310
310
|
project's own (`home/bmweb-home.ips`): F1 picks a chassis then a module,
|
|
311
311
|
F2 the chassis's whole-vehicle script, and `scriptchange` hands the screen
|
|
312
|
-
to that script.
|
|
313
|
-
|
|
314
|
-
cable.
|
|
312
|
+
to that script. A pick is a list the keyboard walks: Up/Down move the bar,
|
|
313
|
+
typing narrows the list to the rows containing the text, Enter picks, Esc
|
|
314
|
+
cancels. The home starts with or without a cable.
|
|
315
|
+
|
|
316
|
+
The TUI runs on the terminal's alternate screen (the buffer vim and htop
|
|
317
|
+
use), so the shell's scrollback is never touched and quitting restores it;
|
|
318
|
+
a screen redraws in place, only the lines that changed.
|
|
315
319
|
|
|
316
320
|
Every dialog INPA opens is a prompt: a message waits for Enter, an input
|
|
317
321
|
asks for the number (or hex, or text) within the declared range, the
|
|
318
|
-
two-word box takes y/n, the component picker (togglelist)
|
|
319
|
-
|
|
320
|
-
file name. **Every write is asked first**, exactly as the app asks: a key
|
|
322
|
+
two-word box takes y/n, the component picker (togglelist) and Select are
|
|
323
|
+
the same list picker (Space marks several where several may be picked),
|
|
324
|
+
save-as asks for a file name. **Every write is asked first**, exactly as the app asks: a key
|
|
321
325
|
whose body can send a write names the jobs and waits for y; a screen that
|
|
322
326
|
sends one on every refresh asks once for as long as it is open; n or
|
|
323
327
|
Enter abandons the key. On quit the leaving menu's Back job (the script's
|
package/dist/bmweb.js
CHANGED
|
@@ -1892,6 +1892,7 @@ function nodeTerminal() {
|
|
|
1892
1892
|
};
|
|
1893
1893
|
raw(true);
|
|
1894
1894
|
input.resume();
|
|
1895
|
+
output.write("\x1B[?1049h\x1B[H");
|
|
1895
1896
|
const subs = /* @__PURE__ */ new Set();
|
|
1896
1897
|
let paused = false;
|
|
1897
1898
|
input.on(
|
|
@@ -1923,6 +1924,12 @@ function nodeTerminal() {
|
|
|
1923
1924
|
subs.delete(fn);
|
|
1924
1925
|
};
|
|
1925
1926
|
},
|
|
1927
|
+
onResize(fn) {
|
|
1928
|
+
output.on("resize", fn);
|
|
1929
|
+
return () => {
|
|
1930
|
+
output.off("resize", fn);
|
|
1931
|
+
};
|
|
1932
|
+
},
|
|
1926
1933
|
async readLine(prompt) {
|
|
1927
1934
|
paused = true;
|
|
1928
1935
|
raw(false);
|
|
@@ -1939,6 +1946,7 @@ function nodeTerminal() {
|
|
|
1939
1946
|
}
|
|
1940
1947
|
},
|
|
1941
1948
|
close() {
|
|
1949
|
+
output.write("\x1B[?1049l");
|
|
1942
1950
|
raw(false);
|
|
1943
1951
|
input.pause();
|
|
1944
1952
|
}
|
|
@@ -1952,6 +1960,12 @@ var TuiUi = class {
|
|
|
1952
1960
|
stopRequested = false;
|
|
1953
1961
|
writeKeys = /* @__PURE__ */ new Set();
|
|
1954
1962
|
program = null;
|
|
1963
|
+
/** the lines of the frame on screen, when the cursor sits right below it */
|
|
1964
|
+
frame = [];
|
|
1965
|
+
/** a picker owns the keyboard: the program's key handler must stand back */
|
|
1966
|
+
modal = false;
|
|
1967
|
+
/** the terminal size the frame was drawn for; a change draws fresh */
|
|
1968
|
+
frameSize = "";
|
|
1955
1969
|
leftResolve = null;
|
|
1956
1970
|
/** resolves once the program reports it left the module */
|
|
1957
1971
|
leftPromise;
|
|
@@ -1970,6 +1984,16 @@ var TuiUi = class {
|
|
|
1970
1984
|
this.leftPromise = new Promise((res) => {
|
|
1971
1985
|
this.leftResolve = res;
|
|
1972
1986
|
});
|
|
1987
|
+
if (term.onResize) term.onResize(() => this.resized());
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* The terminal changed size: the frame on screen no longer fits its
|
|
1991
|
+
* rows, so it is erased (as far as the cursor can climb back over it)
|
|
1992
|
+
* and drawn again for the new size.
|
|
1993
|
+
*/
|
|
1994
|
+
resized() {
|
|
1995
|
+
this.frame = [];
|
|
1996
|
+
if (this.program) this.paint(this.program);
|
|
1973
1997
|
}
|
|
1974
1998
|
/**
|
|
1975
1999
|
* BMWeb's own picker (the home script's bmweb_pick): the host's list as
|
|
@@ -1991,32 +2015,133 @@ var TuiUi = class {
|
|
|
1991
2015
|
return null;
|
|
1992
2016
|
}
|
|
1993
2017
|
const title = step.what === "module" ? `Modules of ${step.arg}` : "Vehicles";
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2018
|
+
const picked = await this.pickList(title, options);
|
|
2019
|
+
return picked ? picked[0] : null;
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* The picker: a list the keyboard walks. Up/Down (and PageUp/PageDown)
|
|
2023
|
+
* move the bar, typing narrows the list to the rows containing the text,
|
|
2024
|
+
* Backspace widens it again, Enter picks the row under the bar (Space
|
|
2025
|
+
* marks a row when several may be picked, Enter then takes the marked
|
|
2026
|
+
* ones, or the bar's row when none is marked), Esc cancels. Drawn as the
|
|
2027
|
+
* frame, so it repaints in place like a screen.
|
|
2028
|
+
* @param title - what is being picked
|
|
2029
|
+
* @param options - the rows
|
|
2030
|
+
* @param multiple - whether several rows may be picked
|
|
2031
|
+
* @returns the picked values (one, unless multiple), or null for cancel
|
|
2032
|
+
*/
|
|
2033
|
+
async pickList(title, options, multiple = false) {
|
|
2034
|
+
let filter = "";
|
|
2035
|
+
let cursor = 0;
|
|
2036
|
+
let top = 0;
|
|
2037
|
+
const marked = /* @__PURE__ */ new Set();
|
|
2038
|
+
const shown = () => {
|
|
2039
|
+
const q = filter.toLowerCase();
|
|
2040
|
+
return q ? options.filter(
|
|
2011
2041
|
(o) => `${o.label} ${o.meta || ""} ${o.value}`.toLowerCase().includes(q)
|
|
2042
|
+
) : options;
|
|
2043
|
+
};
|
|
2044
|
+
const draw = () => {
|
|
2045
|
+
const rows = shown();
|
|
2046
|
+
const w = this.term.columns;
|
|
2047
|
+
const window = Math.max(3, this.term.rows - 7);
|
|
2048
|
+
if (cursor >= rows.length) cursor = Math.max(0, rows.length - 1);
|
|
2049
|
+
if (cursor < top) top = cursor;
|
|
2050
|
+
if (cursor >= top + window) top = cursor - window + 1;
|
|
2051
|
+
const count = rows.length === options.length ? `${options.length}` : `${rows.length} of ${options.length}`;
|
|
2052
|
+
const lines = [
|
|
2053
|
+
`${title} \x1B[2m(${count})\x1B[0m`,
|
|
2054
|
+
`\x1B[2mFilter:\x1B[0m ${filter}\x1B[7m \x1B[0m`,
|
|
2055
|
+
""
|
|
2056
|
+
];
|
|
2057
|
+
for (const o of rows.slice(top, top + window)) {
|
|
2058
|
+
const i = rows.indexOf(o);
|
|
2059
|
+
const mark = multiple ? marked.has(o.value) ? "[x] " : "[ ] " : "";
|
|
2060
|
+
const meta = o.meta ? ` ${o.meta}` : "";
|
|
2061
|
+
const text = `${mark}${o.label}${meta}`.slice(0, w - 4);
|
|
2062
|
+
lines.push(
|
|
2063
|
+
i === cursor ? `\x1B[7m > ${text.padEnd(w - 4)} \x1B[0m` : ` ${o.meta ? `${mark}${o.label}\x1B[2m${meta}\x1B[0m` : text}`
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
if (!rows.length) lines.push(" \x1B[2m(nothing matches)\x1B[0m");
|
|
2067
|
+
if (rows.length > top + window)
|
|
2068
|
+
lines.push(` \x1B[2m... ${rows.length - top - window} more\x1B[0m`);
|
|
2069
|
+
lines.push("");
|
|
2070
|
+
lines.push(
|
|
2071
|
+
`\x1B[2m\u2191\u2193 move type to filter Enter picks${multiple ? " Space marks" : ""} Esc cancels\x1B[0m`
|
|
2012
2072
|
);
|
|
2013
|
-
|
|
2073
|
+
this.flush(lines);
|
|
2074
|
+
};
|
|
2075
|
+
this.modal = true;
|
|
2076
|
+
try {
|
|
2077
|
+
return await new Promise((resolve2) => {
|
|
2078
|
+
const off = this.term.onKey((k) => {
|
|
2079
|
+
const rows = shown();
|
|
2080
|
+
const finish = (v) => {
|
|
2081
|
+
off();
|
|
2082
|
+
resolve2(v);
|
|
2083
|
+
};
|
|
2084
|
+
if (k.name === "escape" || k.ctrl && k.name === "c") {
|
|
2085
|
+
finish(null);
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
if (k.name === "return" || k.name === "enter") {
|
|
2089
|
+
if (multiple && marked.size) {
|
|
2090
|
+
finish(
|
|
2091
|
+
options.filter((o2) => marked.has(o2.value)).map((o2) => o2.value)
|
|
2092
|
+
);
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
const o = rows[cursor];
|
|
2096
|
+
finish(o ? [o.value] : null);
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
if (k.name === "up" || k.ctrl && k.name === "p")
|
|
2100
|
+
cursor = Math.max(0, cursor - 1);
|
|
2101
|
+
else if (k.name === "down" || k.ctrl && k.name === "n")
|
|
2102
|
+
cursor = Math.min(rows.length - 1, cursor + 1);
|
|
2103
|
+
else if (k.name === "pageup")
|
|
2104
|
+
cursor = Math.max(0, cursor - (this.term.rows - 7));
|
|
2105
|
+
else if (k.name === "pagedown")
|
|
2106
|
+
cursor = Math.min(rows.length - 1, cursor + (this.term.rows - 7));
|
|
2107
|
+
else if (k.name === "home") cursor = 0;
|
|
2108
|
+
else if (k.name === "end") cursor = Math.max(0, rows.length - 1);
|
|
2109
|
+
else if (k.name === "backspace") filter = filter.slice(0, -1);
|
|
2110
|
+
else if (multiple && k.name === "space") {
|
|
2111
|
+
const o = rows[cursor];
|
|
2112
|
+
if (o) {
|
|
2113
|
+
if (marked.has(o.value)) marked.delete(o.value);
|
|
2114
|
+
else marked.add(o.value);
|
|
2115
|
+
}
|
|
2116
|
+
} else if (k.ch && !k.ctrl && k.ch >= " " && k.ch !== "\x7F") {
|
|
2117
|
+
filter += k.ch;
|
|
2118
|
+
cursor = 0;
|
|
2119
|
+
} else return;
|
|
2120
|
+
draw();
|
|
2121
|
+
});
|
|
2122
|
+
draw();
|
|
2123
|
+
});
|
|
2124
|
+
} finally {
|
|
2125
|
+
this.modal = false;
|
|
2126
|
+
this.frame = [];
|
|
2127
|
+
if (this.program) this.paint(this.program);
|
|
2014
2128
|
}
|
|
2015
2129
|
}
|
|
2016
2130
|
/** Note the program once it exists, for the key handler. */
|
|
2017
2131
|
attach(p) {
|
|
2018
2132
|
this.program = p;
|
|
2019
2133
|
}
|
|
2134
|
+
/**
|
|
2135
|
+
* A typed prompt under the frame (readline, raw mode off). It moves the
|
|
2136
|
+
* cursor, so the frame is forgotten and the next paint clears the screen
|
|
2137
|
+
* and draws fresh.
|
|
2138
|
+
* @param prompt - the prompt text
|
|
2139
|
+
* @returns the answer, or null when cancelled
|
|
2140
|
+
*/
|
|
2141
|
+
async ask(prompt) {
|
|
2142
|
+
this.frame = [];
|
|
2143
|
+
return this.term.readLine(prompt);
|
|
2144
|
+
}
|
|
2020
2145
|
/** Esc during a parked state machine: stop it at the next tick. */
|
|
2021
2146
|
requestStop() {
|
|
2022
2147
|
this.stopRequested = true;
|
|
@@ -2038,7 +2163,7 @@ Number to open, text to filter, Enter to cancel: `
|
|
|
2038
2163
|
this.drawFooter();
|
|
2039
2164
|
}
|
|
2040
2165
|
async message(title, body) {
|
|
2041
|
-
await this.
|
|
2166
|
+
await this.ask(
|
|
2042
2167
|
`
|
|
2043
2168
|
${title}${body ? `
|
|
2044
2169
|
${body}` : ""}
|
|
@@ -2064,7 +2189,7 @@ ${body}` : ""}
|
|
|
2064
2189
|
if (name === "inputdigital") {
|
|
2065
2190
|
const f = prompts[prompts.length - 2] || "OFF";
|
|
2066
2191
|
const t = prompts[prompts.length - 1] || "ON";
|
|
2067
|
-
const a = await this.
|
|
2192
|
+
const a = await this.ask(
|
|
2068
2193
|
`
|
|
2069
2194
|
${p0}
|
|
2070
2195
|
${p1}
|
|
@@ -2074,12 +2199,10 @@ ${p1}
|
|
|
2074
2199
|
return /^y/i.test(a.trim()) ? 1 : 0;
|
|
2075
2200
|
}
|
|
2076
2201
|
if (name === "builtin_3f" && prompts.length <= 2 && refs === 1) {
|
|
2077
|
-
const a = await this.
|
|
2078
|
-
`
|
|
2202
|
+
const a = await this.ask(`
|
|
2079
2203
|
${p0}
|
|
2080
2204
|
${p1}
|
|
2081
|
-
[OK = y, cancel = n] `
|
|
2082
|
-
);
|
|
2205
|
+
[OK = y, cancel = n] `);
|
|
2083
2206
|
return a != null && /^y/i.test(a.trim()) ? 0 : null;
|
|
2084
2207
|
}
|
|
2085
2208
|
const hex = /hex/i.test(name);
|
|
@@ -2089,7 +2212,7 @@ ${p1}
|
|
|
2089
2212
|
for (let k = 0; k < refs; k++) {
|
|
2090
2213
|
const cap = refs > 1 ? prompts[2 + k] || `${p0} (${k + 1}/${refs})` : p1;
|
|
2091
2214
|
const range = step.lo != null && step.hi != null && !hex ? ` [${step.lo}..${step.hi}]` : "";
|
|
2092
|
-
const a = await this.
|
|
2215
|
+
const a = await this.ask(`
|
|
2093
2216
|
${p0}
|
|
2094
2217
|
${cap}${range}: `);
|
|
2095
2218
|
if (a == null) return null;
|
|
@@ -2111,7 +2234,7 @@ ${cap}${range}: `);
|
|
|
2111
2234
|
return refs > 1 ? vals : vals[0];
|
|
2112
2235
|
}
|
|
2113
2236
|
async confirmKey(_p, it, jobs, writes) {
|
|
2114
|
-
const a = await this.
|
|
2237
|
+
const a = await this.ask(
|
|
2115
2238
|
`
|
|
2116
2239
|
Run "${it.label || it.legendLabel || `F${it.nr}`}"? It can send ${jobs.join(", ")} and ${writes.join(", ")} write${writes.length === 1 ? "s" : ""} to the module. [y/N] `
|
|
2117
2240
|
);
|
|
@@ -2119,7 +2242,7 @@ Run "${it.label || it.legendLabel || `F${it.nr}`}"? It can send ${jobs.join(", "
|
|
|
2119
2242
|
}
|
|
2120
2243
|
async confirmWrite(_p, job, arg, ctx) {
|
|
2121
2244
|
const every = String(ctx.scope || "").startsWith("screen:") ? " This screen sends it on every refresh; yes allows it while the screen is open." : "";
|
|
2122
|
-
const a = await this.
|
|
2245
|
+
const a = await this.ask(
|
|
2123
2246
|
`
|
|
2124
2247
|
Send ${job}${arg ? ` ${arg}` : ""} to the module (${ctx.label})?${every} [y/N] `
|
|
2125
2248
|
);
|
|
@@ -2131,8 +2254,8 @@ Send ${job}${arg ? ` ${arg}` : ""} to the module (${ctx.label})?${every} [y/N] `
|
|
|
2131
2254
|
* flag, every picked key ';'-joined).
|
|
2132
2255
|
*/
|
|
2133
2256
|
async pickComponent(p, step) {
|
|
2134
|
-
const rows = this.R.ipoScreenComponents(p.exec, p.screen).map((l,
|
|
2135
|
-
key: step.argnum ? String(
|
|
2257
|
+
const rows = this.R.ipoScreenComponents(p.exec, p.screen).map((l, i) => ({
|
|
2258
|
+
key: step.argnum ? String(i + 1) : String(l.keys).split(";")[0] || "",
|
|
2136
2259
|
caption: l.label || String(l.keys).split(";")[0] || ""
|
|
2137
2260
|
}));
|
|
2138
2261
|
if (!rows.length) {
|
|
@@ -2142,32 +2265,27 @@ Send ${job}${arg ? ` ${arg}` : ""} to the module (${ctx.label})?${every} [y/N] `
|
|
|
2142
2265
|
);
|
|
2143
2266
|
return null;
|
|
2144
2267
|
}
|
|
2145
|
-
const
|
|
2268
|
+
const options = rows.map((r) => ({
|
|
2269
|
+
value: r.key,
|
|
2270
|
+
label: r.caption,
|
|
2271
|
+
meta: r.key !== r.caption ? r.key : ""
|
|
2272
|
+
}));
|
|
2146
2273
|
if (step.multiple) {
|
|
2147
|
-
const
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
)
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
`
|
|
2160
|
-
${list}
|
|
2161
|
-
Component number (Enter cancels): `
|
|
2274
|
+
const picked2 = await this.pickList("Components", options, true);
|
|
2275
|
+
if (!picked2 || !picked2.length) return null;
|
|
2276
|
+
return { ort: picked2.join(";"), ein: 0 };
|
|
2277
|
+
}
|
|
2278
|
+
const picked = await this.pickList("Component", options);
|
|
2279
|
+
if (!picked) return null;
|
|
2280
|
+
const onOff = await this.pickList(
|
|
2281
|
+
`${options.find((o) => o.value === picked[0])?.label || picked[0]}`,
|
|
2282
|
+
[
|
|
2283
|
+
{ value: "on", label: "On" },
|
|
2284
|
+
{ value: "off", label: "Off" }
|
|
2285
|
+
]
|
|
2162
2286
|
);
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
const onOff = await this.term.readLine(`On or off? [on/off] `);
|
|
2166
|
-
if (onOff == null || !onOff.trim()) return null;
|
|
2167
|
-
return {
|
|
2168
|
-
ort: rows[i].key,
|
|
2169
|
-
ein: /^on/i.test(onOff.trim()) ? 0 : 1
|
|
2170
|
-
};
|
|
2287
|
+
if (!onOff) return null;
|
|
2288
|
+
return { ort: picked[0], ein: onOff[0] === "on" ? 0 : 1 };
|
|
2171
2289
|
}
|
|
2172
2290
|
/** INPA's Select: which named logical lines to keep on screen. */
|
|
2173
2291
|
async pickLines(_p, names, multiple, _current, hints) {
|
|
@@ -2179,20 +2297,18 @@ Component number (Enter cancels): `
|
|
|
2179
2297
|
);
|
|
2180
2298
|
return null;
|
|
2181
2299
|
}
|
|
2182
|
-
const
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
);
|
|
2188
|
-
if (
|
|
2189
|
-
|
|
2190
|
-
const picked = pickNumbers(a, names.length).map((i) => names[i]);
|
|
2191
|
-
return picked.length ? picked : null;
|
|
2300
|
+
const options = [
|
|
2301
|
+
{ value: "\0all", label: "(all lines)" },
|
|
2302
|
+
...names.map((n) => ({ value: n, label: n }))
|
|
2303
|
+
];
|
|
2304
|
+
const picked = await this.pickList("Lines to show", options, multiple);
|
|
2305
|
+
if (!picked || !picked.length) return null;
|
|
2306
|
+
if (picked.includes("\0all")) return [];
|
|
2307
|
+
return picked;
|
|
2192
2308
|
}
|
|
2193
2309
|
/** INPA's save-as dialog: a file name, written when the body ends. */
|
|
2194
2310
|
async saveFile() {
|
|
2195
|
-
const a = await this.
|
|
2311
|
+
const a = await this.ask(`
|
|
2196
2312
|
Save as [fault-memory.txt]: `);
|
|
2197
2313
|
if (a == null) return null;
|
|
2198
2314
|
return { name: a.trim() || "fault-memory.txt" };
|
|
@@ -2202,9 +2318,9 @@ Save as [fault-memory.txt]: `);
|
|
|
2202
2318
|
this.status(_p, `wrote ${picked.name}`);
|
|
2203
2319
|
}
|
|
2204
2320
|
printScreen(p) {
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
`);
|
|
2321
|
+
const name = `bmweb-screen-${Date.now()}.txt`;
|
|
2322
|
+
writeFileSync4(name, this.gridLines(p).join("\n") + "\n", "utf8");
|
|
2323
|
+
this.status(p, `printed the screen to ${name}`);
|
|
2208
2324
|
}
|
|
2209
2325
|
/**
|
|
2210
2326
|
* The module a scriptchange names. From the home script it is the car's
|
|
@@ -2258,23 +2374,63 @@ ${this.gridLines(p).join("\n")}
|
|
|
2258
2374
|
}
|
|
2259
2375
|
this.paint(p);
|
|
2260
2376
|
}
|
|
2261
|
-
/** Redraw everything: title, grid, keys, footer. */
|
|
2377
|
+
/** Redraw everything: title, grid, keys, footer -- in place. */
|
|
2262
2378
|
paint(p) {
|
|
2379
|
+
this.flush(this.frameLines(p));
|
|
2380
|
+
}
|
|
2381
|
+
/**
|
|
2382
|
+
* The frame as lines: title, rule, the view or the grid, a blank, the
|
|
2383
|
+
* key bar, then the status and progress lines. Cut to the terminal's
|
|
2384
|
+
* width (a wrapped line would break the row count the redraw relies on)
|
|
2385
|
+
* and to its height, the body giving way first.
|
|
2386
|
+
* @param p - the program
|
|
2387
|
+
* @returns the lines, none wider than the terminal
|
|
2388
|
+
*/
|
|
2389
|
+
frameLines(p) {
|
|
2263
2390
|
const w = this.term.columns;
|
|
2264
|
-
const out = [];
|
|
2265
2391
|
const title = `${p.ecu.label || p.ecu.sgbd} ${p.ecu.sgbd}.prg ${p.title || ""}`.trim();
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2392
|
+
const head = [title, "-".repeat(Math.min(w, 78))];
|
|
2393
|
+
let body = p.view ? [...p.view.lines || []] : this.gridLines(p);
|
|
2394
|
+
const tail = ["", ...this.keyLines(p), this.statusText, this.progressText];
|
|
2395
|
+
const room = Math.max(1, this.term.rows - 1 - head.length - tail.length);
|
|
2396
|
+
if (body.length > room) body = body.filter((l, i) => l || body[i - 1]);
|
|
2397
|
+
if (body.length > room) {
|
|
2398
|
+
const hidden = body.length - (room - 1);
|
|
2399
|
+
body = [
|
|
2400
|
+
...body.slice(0, room - 1),
|
|
2401
|
+
`(${hidden} more rows: enlarge the terminal)`
|
|
2402
|
+
];
|
|
2272
2403
|
}
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2404
|
+
return [...head, ...body, ...tail].map(
|
|
2405
|
+
(l) => String(l).replace(/[\r\n]/g, " ").slice(0, w)
|
|
2406
|
+
);
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* Put a frame on the screen over the last one. The frame always starts
|
|
2410
|
+
* at the top-left of the alternate screen: go there, rewrite the lines
|
|
2411
|
+
* that differ (each erased to the end of the row), step over the ones
|
|
2412
|
+
* that match, and erase whatever the old frame had below the new one.
|
|
2413
|
+
* With no frame to build on (first paint, after a prompt, after a
|
|
2414
|
+
* resize) the screen is cleared first -- on the alternate screen that
|
|
2415
|
+
* costs no scrollback.
|
|
2416
|
+
* @param lines - the new frame
|
|
2417
|
+
*/
|
|
2418
|
+
flush(lines) {
|
|
2419
|
+
const size = `${this.term.columns}x${this.term.rows}`;
|
|
2420
|
+
if (size !== this.frameSize) this.frame = [];
|
|
2421
|
+
this.frameSize = size;
|
|
2422
|
+
const prev = this.frame;
|
|
2423
|
+
if (prev.length === lines.length && prev.every((l, i) => l === lines[i]))
|
|
2424
|
+
return;
|
|
2425
|
+
let s = prev.length ? "\x1B[H" : "\x1B[2J\x1B[H";
|
|
2426
|
+
lines.forEach((line, i) => {
|
|
2427
|
+
if (i < prev.length && prev[i] === line) s += "\x1B[B";
|
|
2428
|
+
else s += `\r${line}\x1B[K\r
|
|
2429
|
+
`;
|
|
2430
|
+
});
|
|
2431
|
+
if (lines.length < prev.length) s += "\r\x1B[J";
|
|
2432
|
+
this.term.write(s);
|
|
2433
|
+
this.frame = lines;
|
|
2278
2434
|
}
|
|
2279
2435
|
/**
|
|
2280
2436
|
* The grid as text rows: cells placed at their column, a lamp as a dot
|
|
@@ -2314,21 +2470,25 @@ ${this.gridLines(p).join("\n")}
|
|
|
2314
2470
|
);
|
|
2315
2471
|
return out;
|
|
2316
2472
|
}
|
|
2317
|
-
/**
|
|
2473
|
+
/**
|
|
2474
|
+
* The two bottom lines of the frame: status and progress. They are part
|
|
2475
|
+
* of the frame, so a change repaints it (which writes just those lines);
|
|
2476
|
+
* before a program exists the text is written on its own.
|
|
2477
|
+
*/
|
|
2318
2478
|
drawFooter() {
|
|
2319
|
-
this.
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2479
|
+
if (this.program) {
|
|
2480
|
+
this.paint(this.program);
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
this.frame = [];
|
|
2484
|
+
this.term.write(`${this.statusText}\r
|
|
2485
|
+
${this.progressText}\r
|
|
2486
|
+
`);
|
|
2323
2487
|
}
|
|
2324
2488
|
left() {
|
|
2325
2489
|
if (this.leftResolve) this.leftResolve();
|
|
2326
2490
|
}
|
|
2327
2491
|
};
|
|
2328
|
-
function pickNumbers(a, n) {
|
|
2329
|
-
if (a == null) return [];
|
|
2330
|
-
return a.split(/[,\s]+/).map((s) => Number(s)).filter((i) => Number.isInteger(i) && i >= 1 && i <= n).map((i) => i - 1);
|
|
2331
|
-
}
|
|
2332
2492
|
function cellText(c) {
|
|
2333
2493
|
const text = String(c.text || "");
|
|
2334
2494
|
if (c.kind === "lamp") {
|
|
@@ -2417,6 +2577,7 @@ async function tuiCommand(chassis, sgbd, opts = {}) {
|
|
|
2417
2577
|
}
|
|
2418
2578
|
const program = p;
|
|
2419
2579
|
const unsubscribe = term.onKey((k) => {
|
|
2580
|
+
if (ui.modal) return;
|
|
2420
2581
|
const what = keyToPress(k);
|
|
2421
2582
|
if (what === null) return;
|
|
2422
2583
|
if (what === "quit") {
|
|
@@ -2446,7 +2607,7 @@ async function tuiCommand(chassis, sgbd, opts = {}) {
|
|
|
2446
2607
|
}
|
|
2447
2608
|
|
|
2448
2609
|
// src/bmweb.ts
|
|
2449
|
-
var VERSION = true ? "0.1.
|
|
2610
|
+
var VERSION = true ? "0.1.2" : "0.0.0-dev";
|
|
2450
2611
|
var INCLUDE = {
|
|
2451
2612
|
include: {
|
|
2452
2613
|
kind: "list",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bmweb-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "BMWeb's tools as a command line: read INPA .IPO scripts, compile .IPS sources, search the corpus job index, decode and diff shared Garage reports, and, over a K+DCAN cable, run jobs, whole-car scans and INPA screens in the terminal.",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -41,9 +41,7 @@ if (typeof module !== 'undefined' && module.exports) {
|
|
|
41
41
|
JUMP_TESTS,
|
|
42
42
|
REG_BYTES,
|
|
43
43
|
isWriteJob,
|
|
44
|
-
// the classifier's parts, exported
|
|
45
|
-
// can compare each against its Python twin in
|
|
46
|
-
// tools/verify/sgbd_bulk_verify.py pattern-by-pattern
|
|
44
|
+
// the classifier's parts, exported for tests and tooling
|
|
47
45
|
READ_TOKEN,
|
|
48
46
|
CONFIG_READ_TOKEN,
|
|
49
47
|
WRITE_TOKEN,
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* strings); executing the program handles all of it, which is why EDIABAS is
|
|
7
7
|
* flawless. Input is tools/sgbd_code.py output (ops array, jumps as indices);
|
|
8
8
|
* telegram I/O is a callback, so one VM runs live cable / .sim / fixture.
|
|
9
|
-
* Semantics ported from
|
|
10
|
-
*
|
|
9
|
+
* Semantics ported from EdiabasLib (EdOperations.cs, EdiabasNet.cs); the
|
|
10
|
+
* engine's own result sets for 460 E46 jobs are the committed fixture
|
|
11
|
+
* data/sim-captures/vmfix.json that test_bestvm.js replays.
|
|
11
12
|
*
|
|
12
13
|
* THE REGISTER MODEL, which nothing else here makes sense without:
|
|
13
14
|
* B/I/L/A are VIEWS over one 32-byte array, LITTLE-endian within a view, so
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
* changed in the corpus -- but with INFO checked after writes (rule 3), a
|
|
36
36
|
* hypothetical SYSTEMCHECK_STOP_INFO must hit the write tier, not fall
|
|
37
37
|
* through to the INFO tier. Relaxing a WRITE token is the safe direction.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* This is the ONE classifier: the Python twin that the retired engine
|
|
39
|
+
* harness carried is gone, so there is no second answer to "is this a
|
|
40
|
+
* write?" to drift from.
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -444,6 +444,18 @@ function bViewopen(vm, stack) {
|
|
|
444
444
|
};
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
+
/**
|
|
448
|
+
* printfile(->ErrorCode, FileName, PrinterName, PrinterPort, ErrorMsgFlag):
|
|
449
|
+
* INPA prints the named file (the fault protocol a read wrote) on the
|
|
450
|
+
* printer. The print itself is the runtime's (suspensions.js 'printfile');
|
|
451
|
+
* here the error code is 0, printed or not, so the script never takes its
|
|
452
|
+
* "could not print" branch over a sheet the browser handles.
|
|
453
|
+
* @type {IpoBuiltin}
|
|
454
|
+
*/
|
|
455
|
+
function bPrintfile(vm, stack) {
|
|
456
|
+
storeOut(vm, stack, 0);
|
|
457
|
+
}
|
|
458
|
+
|
|
447
459
|
/**
|
|
448
460
|
* filewrite(text): append a line to the open file.
|
|
449
461
|
* @type {IpoBuiltin}
|
|
@@ -601,6 +613,7 @@ if (typeof module !== 'undefined' && module.exports) {
|
|
|
601
613
|
bInputDigital,
|
|
602
614
|
bFileopen,
|
|
603
615
|
bFileclose,
|
|
616
|
+
bPrintfile,
|
|
604
617
|
bFilewrite,
|
|
605
618
|
bFileread,
|
|
606
619
|
bSettimer,
|
|
@@ -13,6 +13,12 @@ const IPO_ANALOG_FMT_RE = /^(\d+)\.(\d+)$/;
|
|
|
13
13
|
/** toFixed()'s upper bound on decimals; a stray format never asks for more. */
|
|
14
14
|
const IPO_MAX_DECIMALS = 20;
|
|
15
15
|
|
|
16
|
+
/** Bytes per line of INPA's hexdump. */
|
|
17
|
+
const IPO_HEXDUMP_LINE = 16;
|
|
18
|
+
|
|
19
|
+
/** The fewest hex digits a hexdump address column shows. */
|
|
20
|
+
const IPO_HEXDUMP_ADDR_MIN = 4;
|
|
21
|
+
|
|
16
22
|
/**
|
|
17
23
|
* settitle / setmenutitle: the first argument is the title.
|
|
18
24
|
* @type {IpoBuiltin}
|
|
@@ -113,6 +119,38 @@ function bUserboxClear(vm) {
|
|
|
113
119
|
if (vm.onUserbox) vm.onUserbox(vm.userbox);
|
|
114
120
|
}
|
|
115
121
|
|
|
122
|
+
/**
|
|
123
|
+
* The foreground / background pair a colour builtin was handed: its last
|
|
124
|
+
* two integer arguments (userboxsetcolor leads with the box number).
|
|
125
|
+
* @param {IpoValue[]} stack - the call's arguments
|
|
126
|
+
* @returns {{fg: number, bk: number}|null} null when the call named no pair
|
|
127
|
+
*/
|
|
128
|
+
function ipoColorArgs(stack) {
|
|
129
|
+
const n = allInts(stack);
|
|
130
|
+
if (n.length < 2) return null;
|
|
131
|
+
return { fg: n[n.length - 2], bk: n[n.length - 1] };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* setcolor(FgColor, BkColor): the colour every text printed after it takes.
|
|
136
|
+
* Recorded on the run so a host that draws colour can honour it; the
|
|
137
|
+
* browser's skins keep their own palette.
|
|
138
|
+
* @type {IpoBuiltin}
|
|
139
|
+
*/
|
|
140
|
+
function bSetcolor(vm, stack) {
|
|
141
|
+
const c = ipoColorArgs(stack);
|
|
142
|
+
if (c) vm.out.color = c;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* userboxsetcolor(BoxNum, FgColor, BkColor): the progress window's colours.
|
|
147
|
+
* @type {IpoBuiltin}
|
|
148
|
+
*/
|
|
149
|
+
function bUserboxSetcolor(vm, stack) {
|
|
150
|
+
const c = ipoColorArgs(stack);
|
|
151
|
+
if (c && vm.userbox) vm.userbox.color = c;
|
|
152
|
+
}
|
|
153
|
+
|
|
116
154
|
/**
|
|
117
155
|
* userboxftextout(text, row, col ...): a line of the progress window. It is
|
|
118
156
|
* also a textout, so a body without a box paints it with the screen.
|
|
@@ -234,9 +272,69 @@ function bTextout(vm, stack) {
|
|
|
234
272
|
el.row = ints[0];
|
|
235
273
|
el.col = ints[1];
|
|
236
274
|
}
|
|
275
|
+
// the colour setcolor chose for what follows rides on the live cell so a
|
|
276
|
+
// host that draws colour (the terminal UI) can; offline the element stays
|
|
277
|
+
// byte-identical to the Python twin's
|
|
278
|
+
if (vm.wireJobs && vm.out.color) el.color = vm.out.color;
|
|
237
279
|
line.elements.push(el);
|
|
238
280
|
}
|
|
239
281
|
|
|
282
|
+
/**
|
|
283
|
+
* The lines of INPA's hexdump: one per 16 bytes, the address (the start
|
|
284
|
+
* address plus the line's offset, in hex, at least as wide as the start
|
|
285
|
+
* address was written) then the bytes as two hex digits each. A start
|
|
286
|
+
* address that is not hex counts from 0.
|
|
287
|
+
* @param {string} startAdr - the address the bytes were read from ("0x1000")
|
|
288
|
+
* @param {number[]} bytes - the bytes to show
|
|
289
|
+
* @returns {string[]}
|
|
290
|
+
*/
|
|
291
|
+
function ipoHexdumpLines(startAdr, bytes) {
|
|
292
|
+
const digits = String(startAdr || '')
|
|
293
|
+
.trim()
|
|
294
|
+
.replace(/^0x/i, '');
|
|
295
|
+
const base = /^[0-9a-f]+$/i.test(digits) ? parseInt(digits, 16) : 0;
|
|
296
|
+
const width = Math.max(IPO_HEXDUMP_ADDR_MIN, digits.length);
|
|
297
|
+
const out = [];
|
|
298
|
+
for (let i = 0; i < bytes.length; i += IPO_HEXDUMP_LINE) {
|
|
299
|
+
const addr = (base + i).toString(16).toUpperCase().padStart(width, '0');
|
|
300
|
+
const hex = bytes
|
|
301
|
+
.slice(i, i + IPO_HEXDUMP_LINE)
|
|
302
|
+
.map((b) => (b & 0xff).toString(16).toUpperCase().padStart(2, '0'));
|
|
303
|
+
out.push(`${addr} ${hex.join(' ')}`);
|
|
304
|
+
}
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* hexdump(StartAdr, numbytes, row, col): the bytes of the last
|
|
310
|
+
* INPAapiResultBinary painted as a hex table at (row, col), the way the
|
|
311
|
+
* Speicher-lesen screens show memory. The bytes come from the wire, so
|
|
312
|
+
* offline there is nothing to paint (the lift sees the call, not a table).
|
|
313
|
+
* Each line is printed as ftextout would print it, one row down from the
|
|
314
|
+
* last, so the grid, the sheet and Select treat it as text.
|
|
315
|
+
* @type {IpoBuiltin}
|
|
316
|
+
*/
|
|
317
|
+
function bHexdump(vm, stack) {
|
|
318
|
+
if (!vm.wireJobs) return;
|
|
319
|
+
const key = vm.globals.get('__pending_binary__');
|
|
320
|
+
if (!key || !vm.host || typeof vm.host.raw !== 'function') return;
|
|
321
|
+
const set = vm.globals.get('__pending_binary_set__');
|
|
322
|
+
const hex = ipoBinaryHex(
|
|
323
|
+
vm.host.raw(key, { set: set == null ? undefined : set })
|
|
324
|
+
);
|
|
325
|
+
if (!hex) return;
|
|
326
|
+
let bytes = (hex.match(/[0-9A-F]{2}/g) || []).map((h) => parseInt(h, 16));
|
|
327
|
+
const args = stack.filter((x) => !isRef(x));
|
|
328
|
+
const count = args.length > 1 ? Math.trunc(num(args[1])) : bytes.length;
|
|
329
|
+
if (count > 0) bytes = bytes.slice(0, count);
|
|
330
|
+
const row = args.length > 2 ? Math.trunc(num(args[2])) : 0;
|
|
331
|
+
const col = args.length > 3 ? Math.trunc(num(args[3])) : 0;
|
|
332
|
+
const startAdr = args.length ? asStr(args[0]) : '';
|
|
333
|
+
ipoHexdumpLines(startAdr, bytes).forEach((text, i) =>
|
|
334
|
+
bTextout(vm, [text, row + i, col])
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
240
338
|
/**
|
|
241
339
|
* The element a lamp or bar builtin declares, placed on the current line.
|
|
242
340
|
* Offline the row/col come from the int scan the Python twin uses (for
|
|
@@ -464,7 +562,20 @@ function bCallwin(vm, stack, item) {
|
|
|
464
562
|
}
|
|
465
563
|
|
|
466
564
|
/**
|
|
467
|
-
*
|
|
565
|
+
* viewclose(): INPA closes its viewer window. Live, the run says so and
|
|
566
|
+
* drops a view it opened earlier in the same body; a viewopen after it
|
|
567
|
+
* sets a new one, which wins (the runtime reads the view first, the close
|
|
568
|
+
* only when there is none). Offline nothing is lifted from a window.
|
|
569
|
+
* @type {IpoBuiltin}
|
|
570
|
+
*/
|
|
571
|
+
function bViewclose(vm) {
|
|
572
|
+
if (!vm.wireJobs) return;
|
|
573
|
+
vm.out.view = null;
|
|
574
|
+
vm.out.viewClose = true;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* A builtin with no effect on the model (window chrome, stop).
|
|
468
579
|
* @type {IpoBuiltin}
|
|
469
580
|
*/
|
|
470
581
|
function bNoop() {}
|
|
@@ -476,6 +587,11 @@ if (typeof module !== 'undefined' && module.exports) {
|
|
|
476
587
|
bSetmenu,
|
|
477
588
|
bSetscreen,
|
|
478
589
|
bTextout,
|
|
590
|
+
ipoHexdumpLines,
|
|
591
|
+
bHexdump,
|
|
592
|
+
bSetcolor,
|
|
593
|
+
bUserboxSetcolor,
|
|
594
|
+
bViewclose,
|
|
479
595
|
drawField,
|
|
480
596
|
bAnalogout,
|
|
481
597
|
bMultiAnalogout,
|
|
@@ -56,7 +56,7 @@ const BUILTINS = {
|
|
|
56
56
|
userboxopen: bUserboxOpen,
|
|
57
57
|
userboxclose: bUserboxClose,
|
|
58
58
|
viewopen: bViewopen,
|
|
59
|
-
viewclose:
|
|
59
|
+
viewclose: bViewclose,
|
|
60
60
|
setstate: bSetstate,
|
|
61
61
|
start: bSetstate,
|
|
62
62
|
select: bSelect,
|
|
@@ -81,8 +81,8 @@ const BUILTINS = {
|
|
|
81
81
|
fileclose: bFileclose,
|
|
82
82
|
filewrite: bFilewrite,
|
|
83
83
|
fileread: bFileread,
|
|
84
|
-
hexdump:
|
|
85
|
-
printfile:
|
|
84
|
+
hexdump: bHexdump,
|
|
85
|
+
printfile: bPrintfile,
|
|
86
86
|
setstatemachine: bNoop,
|
|
87
87
|
StrArrayCreate: bStrArrayCreate,
|
|
88
88
|
StrArrayDestroy: bNoop,
|
|
@@ -147,7 +147,8 @@ const BUILTINS = {
|
|
|
147
147
|
builtin_87: bUnavailable,
|
|
148
148
|
builtin_93: bUnavailable,
|
|
149
149
|
builtin_90: bStrArraySize, // string array length, out-param
|
|
150
|
-
builtin_1a:
|
|
150
|
+
builtin_1a: bSetcolor, // setcolor
|
|
151
|
+
setcolor: bSetcolor,
|
|
151
152
|
builtin_51: bBlankscreen, // blankscreen
|
|
152
153
|
blankscreen: bBlankscreen,
|
|
153
154
|
settimer: bSettimer,
|
|
@@ -158,7 +159,8 @@ const BUILTINS = {
|
|
|
158
159
|
bmweb_pick: bBmwebPick,
|
|
159
160
|
bmweb_status: bBmwebStatus,
|
|
160
161
|
builtin_57: bUserboxClear, // userboxclear
|
|
161
|
-
builtin_58:
|
|
162
|
+
builtin_58: bUserboxSetcolor, // userboxsetcolor
|
|
163
|
+
userboxsetcolor: bUserboxSetcolor,
|
|
162
164
|
};
|
|
163
165
|
|
|
164
166
|
if (typeof module !== 'undefined' && module.exports) {
|
|
@@ -24,6 +24,15 @@
|
|
|
24
24
|
* @property {string} [fmt] - analogout display format ("6.2")
|
|
25
25
|
* @property {string} [on] - digitalout's TrueText
|
|
26
26
|
* @property {string} [off] - digitalout's FalseText
|
|
27
|
+
* @property {IpoColor} [color] - the setcolor in force when a live run printed it
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A colour pair as INPA's setcolor / userboxsetcolor name it: two of
|
|
32
|
+
* INPA's palette indices. Recorded for hosts that draw colour.
|
|
33
|
+
* @typedef {object} IpoColor
|
|
34
|
+
* @property {number} fg - foreground index
|
|
35
|
+
* @property {number} bk - background index
|
|
27
36
|
*/
|
|
28
37
|
|
|
29
38
|
/**
|
|
@@ -111,6 +120,18 @@ class Emissions {
|
|
|
111
120
|
* @type {{path: string, lines: string[]}|null}
|
|
112
121
|
*/
|
|
113
122
|
this.view = null;
|
|
123
|
+
/**
|
|
124
|
+
* viewclose(): a live body closed INPA's viewer window. Read only when
|
|
125
|
+
* no viewopen in the same body left a view behind.
|
|
126
|
+
* @type {boolean}
|
|
127
|
+
*/
|
|
128
|
+
this.viewClose = false;
|
|
129
|
+
/**
|
|
130
|
+
* setcolor(fg, bk): the colour in force for the text printed after it,
|
|
131
|
+
* null until a body sets one.
|
|
132
|
+
* @type {IpoColor|null}
|
|
133
|
+
*/
|
|
134
|
+
this.color = null;
|
|
114
135
|
/**
|
|
115
136
|
* setscreen's second argument: TRUE = a frequent screen, re-run its cycle
|
|
116
137
|
* while it is current (INPA's WM_TIMER loop); null = no setscreen.
|
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
* says why; `out` is the emissions so far (a parked machine's drawn screen
|
|
13
13
|
* IS the picker).
|
|
14
14
|
* @typedef {object} IpoStep
|
|
15
|
-
* @property {'done'|'yield'|'job'|'wait'|'input'|'message'|'toggle'|'print'|'select'|'exit'} kind -
|
|
15
|
+
* @property {'done'|'yield'|'job'|'wait'|'input'|'message'|'toggle'|'print'|'printfile'|'select'|'exit'} kind -
|
|
16
16
|
* done: the proc finished; yield: parked at a %STATE; job: a wire job to
|
|
17
17
|
* run and feed back; wait: a timed wartezeit; input: an INPA prompt;
|
|
18
18
|
* message: a blocking messagebox; toggle: the component picker; print:
|
|
19
|
-
* printscreen;
|
|
19
|
+
* printscreen; printfile: a written file to print; select: INPA's line
|
|
20
|
+
* filter; exit: the script ended itself
|
|
20
21
|
* @property {Emissions} [out] - emissions so far
|
|
22
|
+
* @property {string} [file] - printfile / fsread: the file's name
|
|
21
23
|
* @property {string} [name] - yield: the state label
|
|
22
24
|
* @property {string} [job] - job: the job name
|
|
23
25
|
* @property {string|null} [sgbd] - job: the SGBD the script addressed
|
|
@@ -46,7 +48,9 @@ const IPO_SUSPEND_KINDS = new Set([
|
|
|
46
48
|
'message',
|
|
47
49
|
'toggle',
|
|
48
50
|
'pick', // bmweb_pick: the host lists, the user chooses (home script)
|
|
51
|
+
'fsread', // INPAapiFsLesen: the renderer reads the fault memory and writes the file
|
|
49
52
|
'print',
|
|
53
|
+
'printfile', // printfile: the renderer prints a file the body wrote
|
|
50
54
|
'select',
|
|
51
55
|
'exit',
|
|
52
56
|
]);
|
|
@@ -184,6 +188,15 @@ function ipoDriveBuiltin(vm, t, stack) {
|
|
|
184
188
|
if (vm.wireJobs && name === 'printscreen') {
|
|
185
189
|
return { kind: 'print', out: vm.out };
|
|
186
190
|
}
|
|
191
|
+
// INPA's printfile(->rc, file, printer, port, flag): the protocol file a
|
|
192
|
+
// read wrote goes to the printer. The builtin answers rc = 0 first (the
|
|
193
|
+
// script's own error branch must not fire), then the renderer prints the
|
|
194
|
+
// file's lines from the VM's files. Offline only the rc is stored.
|
|
195
|
+
if (vm.wireJobs && name === 'printfile') {
|
|
196
|
+
vm._builtin(t, stack, null);
|
|
197
|
+
const strs = stack.filter((x) => !isRef(x)).map((x) => asStr(x));
|
|
198
|
+
return { kind: 'printfile', file: strs[0] || '', out: vm.out };
|
|
199
|
+
}
|
|
187
200
|
// A LIVE togglelist is INPA's component picker: park until the renderer
|
|
188
201
|
// hands back the pick ({ort, ein}); resume re-runs the builtin with it.
|
|
189
202
|
// togglelist(MultipleSelectFlag, ArgNumFlag, ->ApiToggleString)
|
|
@@ -196,6 +209,23 @@ function ipoDriveBuiltin(vm, t, stack) {
|
|
|
196
209
|
out: vm.out,
|
|
197
210
|
};
|
|
198
211
|
}
|
|
212
|
+
// INPAapiFsLesen(sgbd, file): INPA's API reads the module's fault memory
|
|
213
|
+
// (FS_LESEN, then the detail of every entry) and writes the protocol file
|
|
214
|
+
// the script then viewopen()s. 161 shipped scripts read faults only this
|
|
215
|
+
// way. Live, the renderer does that read and hands the file back; offline
|
|
216
|
+
// it stays a noop (nothing to lift from the API's own work).
|
|
217
|
+
if (
|
|
218
|
+
vm.wireJobs &&
|
|
219
|
+
(name === 'INPAapiFsLesen' || name === 'INPAapiFsLesen2')
|
|
220
|
+
) {
|
|
221
|
+
const strs = stack.filter((x) => !isRef(x)).map((x) => asStr(x));
|
|
222
|
+
return {
|
|
223
|
+
kind: 'fsread',
|
|
224
|
+
sgbd: strs[0] || '',
|
|
225
|
+
file: strs[1] || 'na_fs.tmp',
|
|
226
|
+
out: vm.out,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
199
229
|
// BMWeb's own picker: bmweb_pick("chassis"|"module"|"vehicle", arg,
|
|
200
230
|
// ->choice). Park like a togglelist; the renderer asks the host for the
|
|
201
231
|
// list and resumes with the choice, which the re-run builtin stores.
|
|
@@ -178,9 +178,9 @@ const PCODE_MAP = {
|
|
|
178
178
|
'27C2': 'P2562',
|
|
179
179
|
'27C4': 'P2564',
|
|
180
180
|
};
|
|
181
|
-
// Flatten an EDIABAS result value to the
|
|
182
|
-
//
|
|
183
|
-
//
|
|
181
|
+
// Flatten an EDIABAS result value to the text EDIABAS itself would print:
|
|
182
|
+
// byte arrays become dashed hex ("27-DA"), everything else its plain string.
|
|
183
|
+
// The web VM returns live typed values --
|
|
184
184
|
// `ergy` (binary) emits a byte Array, `ergi`/`ergb`/... emit numbers -- so
|
|
185
185
|
// screens that only ever saw the native path's strings funnel through here.
|
|
186
186
|
// An empty binary result ([]) is truthy but must read as "no code", which the
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
* here): SerialProxy.cs is the byte-mover behind NativeSerialBus (open/write/
|
|
21
21
|
* readAvailable/close/flush). Bytes cross that bridge as a JSON int[]
|
|
22
22
|
* (BmacwBridge.cs AsNumberArray), NOT base64 -- base64 corrupted the
|
|
23
|
-
* echo/checksum.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* echo/checksum. The former C# engine (deleted with its InpaMac.Api server) was
|
|
24
|
+
* reference for what JS reimplemented, not a transport, and was never part
|
|
25
|
+
* of this interface.
|
|
26
26
|
*/
|
|
27
27
|
/* exported SerialTransportBase */
|
|
28
28
|
|
|
@@ -269,6 +269,84 @@ function ipoPrintScreen(p, ecu, inpa) {
|
|
|
269
269
|
return Promise.resolve();
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Whether a printfile names the file the viewer is showing: the same path,
|
|
274
|
+
* or the same text (the API's fault read and a save-as can leave the
|
|
275
|
+
* script holding a different name for the same protocol).
|
|
276
|
+
* @param {IpoProgram} p - the program
|
|
277
|
+
* @param {string} name - the file name printfile passed
|
|
278
|
+
* @param {string[]} lines - the file's lines
|
|
279
|
+
* @returns {boolean}
|
|
280
|
+
*/
|
|
281
|
+
function ipoPrintFileIsView(p, name, lines) {
|
|
282
|
+
const v = p.view;
|
|
283
|
+
if (!v) return false;
|
|
284
|
+
if (v.path && v.path === name) return true;
|
|
285
|
+
const shown = v.lines || [];
|
|
286
|
+
return (
|
|
287
|
+
shown.length > 0 &&
|
|
288
|
+
shown.length === lines.length &&
|
|
289
|
+
shown.every((l, i) => l === lines[i])
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The print document for INPA's printfile: the fault protocol as the
|
|
295
|
+
* report's tables plus INPA's text when the file is the one the viewer
|
|
296
|
+
* shows (what printscreen prints over that view), else the file's lines
|
|
297
|
+
* as a monospace sheet. Pure, so the harness can pin it.
|
|
298
|
+
* @param {IpoProgram} p - the program
|
|
299
|
+
* @param {EcuRecord} ecu - the module
|
|
300
|
+
* @param {string} name - the file's name
|
|
301
|
+
* @param {string[]} lines - the file's lines
|
|
302
|
+
* @returns {PrintDocOptions}
|
|
303
|
+
*/
|
|
304
|
+
function ipoPrintFileDocument(p, ecu, name, lines) {
|
|
305
|
+
const isView = ipoPrintFileIsView(p, name, lines);
|
|
306
|
+
const sections = [];
|
|
307
|
+
if (
|
|
308
|
+
isView &&
|
|
309
|
+
p.view.report &&
|
|
310
|
+
typeof ipoProtocolPrintSections === 'function'
|
|
311
|
+
) {
|
|
312
|
+
sections.push(...ipoProtocolPrintSections(p.view));
|
|
313
|
+
} else {
|
|
314
|
+
sections.push({
|
|
315
|
+
html: `<pre class="pr-screen">${esc((lines || []).join('\n'))}</pre>`,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
const subtitle =
|
|
319
|
+
isView && p.view.title ? ipoText(p.view.title) : ipoText(p.title || '');
|
|
320
|
+
return {
|
|
321
|
+
title: ecu.label || ecu.sgbd,
|
|
322
|
+
subtitle: subtitle || name,
|
|
323
|
+
meta: [
|
|
324
|
+
ecu.chassis ? ['Chassis', String(ecu.chassis).toUpperCase()] : null,
|
|
325
|
+
['SGBD', `${ecu.sgbd}.prg`],
|
|
326
|
+
['File', name],
|
|
327
|
+
['Printed', new Date().toLocaleString()],
|
|
328
|
+
],
|
|
329
|
+
sections,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* INPA's printfile for the module view: the clean sheet when the print
|
|
335
|
+
* builder is loaded, nothing otherwise (the browser's own print would show
|
|
336
|
+
* the screen, not the file).
|
|
337
|
+
* @param {IpoProgram} p - the program
|
|
338
|
+
* @param {EcuRecord} ecu - the module
|
|
339
|
+
* @param {string} name - the file's name
|
|
340
|
+
* @param {string[]} lines - the file's lines
|
|
341
|
+
* @returns {Promise<void>}
|
|
342
|
+
*/
|
|
343
|
+
function ipoPrintFile(p, ecu, name, lines) {
|
|
344
|
+
if (typeof printDoc === 'function') {
|
|
345
|
+
return printDoc(ipoPrintFileDocument(p, ecu, name, lines));
|
|
346
|
+
}
|
|
347
|
+
return Promise.resolve();
|
|
348
|
+
}
|
|
349
|
+
|
|
272
350
|
if (typeof module !== 'undefined' && module.exports) {
|
|
273
351
|
module.exports = {
|
|
274
352
|
IPO_PRINT_COLS,
|
|
@@ -277,5 +355,7 @@ if (typeof module !== 'undefined' && module.exports) {
|
|
|
277
355
|
ipoPrintKeysHtml,
|
|
278
356
|
ipoPrintDocument,
|
|
279
357
|
ipoPrintScreen,
|
|
358
|
+
ipoPrintFileDocument,
|
|
359
|
+
ipoPrintFile,
|
|
280
360
|
};
|
|
281
361
|
}
|
|
@@ -103,6 +103,8 @@ const IPO_SILENT_RE = /IFH-0009|IFH-0019|no answer/i;
|
|
|
103
103
|
* @property {(p: IpoProgram, step: IpoStep) => Promise<IpoPick|null>} pickComponent - the togglelist picker
|
|
104
104
|
* @property {(p: IpoProgram, names: string[], multiple: boolean, current: Set<string>|null, hints?: Array<{key: string, label: string, lines: number}>) => Promise<string[]|null>} pickLines - INPA's Select
|
|
105
105
|
* @property {(p: IpoProgram) => void} printScreen - INPA's printscreen
|
|
106
|
+
* @property {(p: IpoProgram, name: string, lines: string[]) => void} [printFile] -
|
|
107
|
+
* INPA's printfile: a file the script wrote, by name, as its lines
|
|
106
108
|
* @property {(ecu: EcuRecord, script: string, exec: IpoExec) => Promise<EcuRecord|null>} [resolveScriptEcu] -
|
|
107
109
|
* the module a scriptchange target addresses, identified by the car
|
|
108
110
|
* @property {(p: IpoProgram, step: IpoStep, guards: Set<number>) => Promise<'tick'|'press'|'stop'>} machineTick - a %STATE park
|
|
@@ -173,6 +175,8 @@ class IpoProgram {
|
|
|
173
175
|
this.script = null;
|
|
174
176
|
/** @type {string|null} the menu the entry set (the route's root) */
|
|
175
177
|
this.rootMenu = null;
|
|
178
|
+
/** @type {string|null} the screen the entry set on the root menu */
|
|
179
|
+
this.rootScreen = null;
|
|
176
180
|
this.answered = false; // INITIALISIERUNG returned any result
|
|
177
181
|
this.silent = false; // INITIALISIERUNG got no answer at all
|
|
178
182
|
this.noCable = false; // no adapter: the script cannot ask the car
|
|
@@ -276,6 +280,14 @@ class IpoProgram {
|
|
|
276
280
|
this.messages.push({ title: step.title, body: step.body });
|
|
277
281
|
await this.ui.message(step.title, step.body);
|
|
278
282
|
step = vm.resume();
|
|
283
|
+
} else if (step.kind === 'fsread') {
|
|
284
|
+
// INPA's API fault read: FS_LESEN and every entry's detail on the
|
|
285
|
+
// wire, then the protocol file the script goes on to viewopen
|
|
286
|
+
const lines = await this.apiFaultRead(step.sgbd, step.file, ctx);
|
|
287
|
+
if (lines == null) return { done: false, cancelled: true };
|
|
288
|
+
vm.files.set(step.file, lines);
|
|
289
|
+
vm.lastWritten = step.file;
|
|
290
|
+
step = vm.resume();
|
|
279
291
|
} else if (step.kind === 'pick') {
|
|
280
292
|
// BMWeb's own picker (the home script): the host lists chassis,
|
|
281
293
|
// modules or the whole-vehicle script; a cancel leaves the body
|
|
@@ -298,6 +310,9 @@ class IpoProgram {
|
|
|
298
310
|
} else if (step.kind === 'print') {
|
|
299
311
|
this.ui.printScreen(this);
|
|
300
312
|
step = vm.resume();
|
|
313
|
+
} else if (step.kind === 'printfile') {
|
|
314
|
+
this.printFile(step.file);
|
|
315
|
+
step = vm.resume();
|
|
301
316
|
} else if (step.kind === 'select') {
|
|
302
317
|
// INPA's Select: which of the screen's named logical lines to show.
|
|
303
318
|
// A screen with none still opens the box (INPA shows an empty
|
|
@@ -341,6 +356,23 @@ class IpoProgram {
|
|
|
341
356
|
return { done: true, exit: !!(vm.out && vm.out.exit) };
|
|
342
357
|
}
|
|
343
358
|
|
|
359
|
+
/**
|
|
360
|
+
* INPA's printfile: the named file's lines go to the UI's printer. The
|
|
361
|
+
* name is what the script computed; when the VM holds no file by it the
|
|
362
|
+
* last one written stands in, as viewopen's does (the API's fault read
|
|
363
|
+
* names its file for the script, and a save-as may have renamed it).
|
|
364
|
+
* @param {string} name - the file name the script passed
|
|
365
|
+
* @returns {void}
|
|
366
|
+
*/
|
|
367
|
+
printFile(name) {
|
|
368
|
+
if (typeof this.ui.printFile !== 'function' || !this.vm) return;
|
|
369
|
+
const { files, lastWritten } = this.vm;
|
|
370
|
+
let path = name;
|
|
371
|
+
if (!files.has(path) && lastWritten && files.has(lastWritten))
|
|
372
|
+
path = lastWritten;
|
|
373
|
+
this.ui.printFile(this, path, [...(files.get(path) || [])]);
|
|
374
|
+
}
|
|
375
|
+
|
|
344
376
|
/**
|
|
345
377
|
* Select's choice: the names of the logical lines to show, or null for
|
|
346
378
|
* all. The current screen is repainted through it; a screen change keeps
|
|
@@ -406,6 +438,79 @@ class IpoProgram {
|
|
|
406
438
|
* @param {IpoRunContext} ctx - what is running
|
|
407
439
|
* @returns {Promise<IpoFeed|null>} the feed, or null when the user declined
|
|
408
440
|
*/
|
|
441
|
+
/**
|
|
442
|
+
* What INPAapiFsLesen does inside INPA: read the module's fault memory
|
|
443
|
+
* (FS_LESEN), then the detail of every stored fault (FS_LESEN_DETAIL by
|
|
444
|
+
* its location number), and write the protocol file. The reads go through
|
|
445
|
+
* runJob, so wireReads carries them and the viewopen that follows draws
|
|
446
|
+
* the report joined with the fault lookup, as the whole-vehicle protocol
|
|
447
|
+
* does; the lines are INPA's own layout for the plain-text view.
|
|
448
|
+
* @param {string} sgbd - the module the script names
|
|
449
|
+
* @param {string} file - the file the script will viewopen
|
|
450
|
+
* @param {object} ctx - the body's context (scope, label)
|
|
451
|
+
* @returns {Promise<string[]|null>} the file's lines, or null when cancelled
|
|
452
|
+
*/
|
|
453
|
+
async apiFaultRead(sgbd, file, ctx) {
|
|
454
|
+
const fed = await this.runJob(sgbd, 'FS_LESEN', null, ctx);
|
|
455
|
+
if (fed == null) return null;
|
|
456
|
+
const sets = (fed.sets || []).slice(1);
|
|
457
|
+
const faults = sets.filter(
|
|
458
|
+
(s) => s && (s.F_ORT_NR != null || s.F_HEX_CODE)
|
|
459
|
+
);
|
|
460
|
+
const name = String(sgbd || this.ecu.sgbd || '').toUpperCase();
|
|
461
|
+
const lines = [name, ''];
|
|
462
|
+
if (String(fed.get('JOB_STATUS') || 'OKAY') !== 'OKAY') {
|
|
463
|
+
lines.push(`Fehlerspeicher lesen: ${fed.get('JOB_STATUS')}`);
|
|
464
|
+
return lines;
|
|
465
|
+
}
|
|
466
|
+
if (!faults.length) {
|
|
467
|
+
lines.push('Kein Fehler im Fehlerspeicher');
|
|
468
|
+
return lines;
|
|
469
|
+
}
|
|
470
|
+
lines.push(
|
|
471
|
+
`${faults.length} Fehler im Fehlerspeicher`.replace(
|
|
472
|
+
/^1 Fehler/,
|
|
473
|
+
'1 Fehler'
|
|
474
|
+
),
|
|
475
|
+
''
|
|
476
|
+
);
|
|
477
|
+
for (const f of faults) {
|
|
478
|
+
const nr = f.F_ORT_NR != null ? String(f.F_ORT_NR) : '';
|
|
479
|
+
let det = f;
|
|
480
|
+
if (nr) {
|
|
481
|
+
// the detail pass, as INPA's API makes it; a module without the job
|
|
482
|
+
// answers with an error and the memory's own fields stand
|
|
483
|
+
const d = await this.runJob(sgbd, 'FS_LESEN_DETAIL', nr, ctx);
|
|
484
|
+
if (d == null) return null;
|
|
485
|
+
const ds = (d.sets || []).slice(1).find((s) => s && s.F_ORT_NR != null);
|
|
486
|
+
if (ds) det = Object.assign({}, f, ds);
|
|
487
|
+
}
|
|
488
|
+
const hex =
|
|
489
|
+
typeof hexText === 'function'
|
|
490
|
+
? hexText(det.F_HEX_CODE)
|
|
491
|
+
: String(det.F_HEX_CODE || '');
|
|
492
|
+
lines.push(
|
|
493
|
+
`${nr}${hex ? ` (${hex})` : ''} ${det.F_ORT_TEXT || ''}`.trim()
|
|
494
|
+
);
|
|
495
|
+
const bits = [];
|
|
496
|
+
if (det.F_ART1_TEXT) bits.push(`Fehlerart: ${det.F_ART1_TEXT}`);
|
|
497
|
+
if (det.F_HFK != null) bits.push(`Häufigkeit: ${det.F_HFK}`);
|
|
498
|
+
if (det.F_LZ != null) bits.push(`Logistikzähler: ${det.F_LZ}`);
|
|
499
|
+
if (det.F_VORHANDEN_TEXT) bits.push(det.F_VORHANDEN_TEXT);
|
|
500
|
+
if (bits.length) lines.push(` ${bits.join(' ')}`);
|
|
501
|
+
const n = Number(det.F_UW_ANZ) || 0;
|
|
502
|
+
for (let i = 1; i <= n; i++) {
|
|
503
|
+
const t = det[`F_UW${i}_TEXT`];
|
|
504
|
+
if (t == null) continue;
|
|
505
|
+
const v = det[`F_UW${i}_WERT`];
|
|
506
|
+
const u = det[`F_UW${i}_EINH`];
|
|
507
|
+
lines.push(` ${t}: ${v != null ? v : ''}${u ? ` ${u}` : ''}`);
|
|
508
|
+
}
|
|
509
|
+
lines.push('');
|
|
510
|
+
}
|
|
511
|
+
return lines;
|
|
512
|
+
}
|
|
513
|
+
|
|
409
514
|
async runJob(sgbd, job, arg, ctx) {
|
|
410
515
|
const target = ipoWireTarget(this.ecu, sgbd);
|
|
411
516
|
const entry = !!(ctx && (ctx.scope === 'entry' || ctx.scope === 'exit'));
|
|
@@ -561,6 +666,10 @@ class IpoProgram {
|
|
|
561
666
|
this.menu = out.menu || null;
|
|
562
667
|
this.screen = out.screen || null;
|
|
563
668
|
this.frequent = !!out.screenFrequent;
|
|
669
|
+
// the script's own front: the menu and screen its entry set. Esc on
|
|
670
|
+
// that menu first returns to that screen (see back()).
|
|
671
|
+
this.rootMenu = this.menu;
|
|
672
|
+
this.rootScreen = this.screen;
|
|
564
673
|
this.title = out.title || null;
|
|
565
674
|
return { ok: true };
|
|
566
675
|
}
|
|
@@ -1232,6 +1341,10 @@ class IpoProgram {
|
|
|
1232
1341
|
const rep = ipoProtocolReport(this.wireReads, out.view.lines);
|
|
1233
1342
|
this.view.report = rep.modules.length ? rep : null;
|
|
1234
1343
|
}
|
|
1344
|
+
} else if (out.viewClose) {
|
|
1345
|
+
// viewclose without a viewopen after it: INPA's viewer window is
|
|
1346
|
+
// gone, the screen behind it shows again
|
|
1347
|
+
this.view = null;
|
|
1235
1348
|
}
|
|
1236
1349
|
// the body painted (userbox text, a result line): show it with the screen
|
|
1237
1350
|
this.takeCells(out);
|
|
@@ -1281,6 +1394,21 @@ class IpoProgram {
|
|
|
1281
1394
|
this.queued = 'back';
|
|
1282
1395
|
return;
|
|
1283
1396
|
}
|
|
1397
|
+
// On the root menu, a key like Ident or Code only swaps the screen
|
|
1398
|
+
// (setscreen without setmenu) and the menu's own F10 is "End", which
|
|
1399
|
+
// leaves the module. Esc there first returns to the screen the entry
|
|
1400
|
+
// set, so one Esc undoes the screen and the next one leaves -- INPA's
|
|
1401
|
+
// main menu never strands the user on a sub-screen.
|
|
1402
|
+
if (
|
|
1403
|
+
this.menu &&
|
|
1404
|
+
this.menu === this.rootMenu &&
|
|
1405
|
+
this.rootScreen &&
|
|
1406
|
+
this.screen !== this.rootScreen &&
|
|
1407
|
+
this.exec.procs[this.rootScreen]
|
|
1408
|
+
) {
|
|
1409
|
+
await this.showScreen(this.rootScreen, false);
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1284
1412
|
if (this.items.some((it) => it.nr === IPO_BACK_KEY))
|
|
1285
1413
|
return this.press(IPO_BACK_KEY);
|
|
1286
1414
|
return this.leaveModule();
|
|
@@ -165,6 +165,8 @@ function ipoMakeUi(ecu, container, back) {
|
|
|
165
165
|
ipoPickLines(names, multiple, current, hints),
|
|
166
166
|
// INPA's printscreen: the module view as a clean sheet (print.js)
|
|
167
167
|
printScreen: (p) => ipoPrintScreen(p, p.ecu || ecu, inpa),
|
|
168
|
+
// INPA's printfile: the protocol file the script wrote, as a sheet
|
|
169
|
+
printFile: (p, name, lines) => ipoPrintFile(p, p.ecu || ecu, name, lines),
|
|
168
170
|
resolveScriptEcu: (from, script, exec) =>
|
|
169
171
|
// the home script names a module by its SGBD after a chassis pick:
|
|
170
172
|
// that module is the car's own record, not a wire-resolved variant
|