bmweb-cli 0.1.0 → 0.1.1
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/dist/bmweb.js +116 -37
- 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/dist/bmweb.js
CHANGED
|
@@ -1923,6 +1923,12 @@ function nodeTerminal() {
|
|
|
1923
1923
|
subs.delete(fn);
|
|
1924
1924
|
};
|
|
1925
1925
|
},
|
|
1926
|
+
onResize(fn) {
|
|
1927
|
+
output.on("resize", fn);
|
|
1928
|
+
return () => {
|
|
1929
|
+
output.off("resize", fn);
|
|
1930
|
+
};
|
|
1931
|
+
},
|
|
1926
1932
|
async readLine(prompt) {
|
|
1927
1933
|
paused = true;
|
|
1928
1934
|
raw(false);
|
|
@@ -1952,6 +1958,10 @@ var TuiUi = class {
|
|
|
1952
1958
|
stopRequested = false;
|
|
1953
1959
|
writeKeys = /* @__PURE__ */ new Set();
|
|
1954
1960
|
program = null;
|
|
1961
|
+
/** the lines of the frame on screen, when the cursor sits right below it */
|
|
1962
|
+
frame = [];
|
|
1963
|
+
/** the terminal size the frame was drawn for; a change draws fresh */
|
|
1964
|
+
frameSize = "";
|
|
1955
1965
|
leftResolve = null;
|
|
1956
1966
|
/** resolves once the program reports it left the module */
|
|
1957
1967
|
leftPromise;
|
|
@@ -1970,6 +1980,19 @@ var TuiUi = class {
|
|
|
1970
1980
|
this.leftPromise = new Promise((res) => {
|
|
1971
1981
|
this.leftResolve = res;
|
|
1972
1982
|
});
|
|
1983
|
+
if (term.onResize) term.onResize(() => this.resized());
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* The terminal changed size: the frame on screen no longer fits its
|
|
1987
|
+
* rows, so it is erased (as far as the cursor can climb back over it)
|
|
1988
|
+
* and drawn again for the new size.
|
|
1989
|
+
*/
|
|
1990
|
+
resized() {
|
|
1991
|
+
if (this.frame.length) {
|
|
1992
|
+
this.term.write(`\x1B[${this.frame.length}A\r\x1B[J`);
|
|
1993
|
+
this.frame = [];
|
|
1994
|
+
}
|
|
1995
|
+
if (this.program) this.paint(this.program);
|
|
1973
1996
|
}
|
|
1974
1997
|
/**
|
|
1975
1998
|
* BMWeb's own picker (the home script's bmweb_pick): the host's list as
|
|
@@ -1996,7 +2019,7 @@ var TuiUi = class {
|
|
|
1996
2019
|
const rows = shown.map(
|
|
1997
2020
|
(o, i) => ` ${String(i + 1).padStart(3)}. ${o.label}${o.meta ? ` (${o.meta})` : ""}`
|
|
1998
2021
|
).join("\n");
|
|
1999
|
-
const a = await this.
|
|
2022
|
+
const a = await this.ask(
|
|
2000
2023
|
`
|
|
2001
2024
|
${title}${shown.length !== options.length ? ` (${shown.length} of ${options.length})` : ""}
|
|
2002
2025
|
${rows}
|
|
@@ -2017,6 +2040,18 @@ Number to open, text to filter, Enter to cancel: `
|
|
|
2017
2040
|
attach(p) {
|
|
2018
2041
|
this.program = p;
|
|
2019
2042
|
}
|
|
2043
|
+
/**
|
|
2044
|
+
* A prompt on the terminal. It scrolls the frame away from under the
|
|
2045
|
+
* cursor, so the frame is forgotten and the next paint draws fresh below
|
|
2046
|
+
* the answer (the prompt and its answer stay in the transcript, the way a
|
|
2047
|
+
* tool call does).
|
|
2048
|
+
* @param prompt - the prompt text
|
|
2049
|
+
* @returns the answer, or null when cancelled
|
|
2050
|
+
*/
|
|
2051
|
+
async ask(prompt) {
|
|
2052
|
+
this.frame = [];
|
|
2053
|
+
return this.term.readLine(prompt);
|
|
2054
|
+
}
|
|
2020
2055
|
/** Esc during a parked state machine: stop it at the next tick. */
|
|
2021
2056
|
requestStop() {
|
|
2022
2057
|
this.stopRequested = true;
|
|
@@ -2038,7 +2073,7 @@ Number to open, text to filter, Enter to cancel: `
|
|
|
2038
2073
|
this.drawFooter();
|
|
2039
2074
|
}
|
|
2040
2075
|
async message(title, body) {
|
|
2041
|
-
await this.
|
|
2076
|
+
await this.ask(
|
|
2042
2077
|
`
|
|
2043
2078
|
${title}${body ? `
|
|
2044
2079
|
${body}` : ""}
|
|
@@ -2064,7 +2099,7 @@ ${body}` : ""}
|
|
|
2064
2099
|
if (name === "inputdigital") {
|
|
2065
2100
|
const f = prompts[prompts.length - 2] || "OFF";
|
|
2066
2101
|
const t = prompts[prompts.length - 1] || "ON";
|
|
2067
|
-
const a = await this.
|
|
2102
|
+
const a = await this.ask(
|
|
2068
2103
|
`
|
|
2069
2104
|
${p0}
|
|
2070
2105
|
${p1}
|
|
@@ -2074,12 +2109,10 @@ ${p1}
|
|
|
2074
2109
|
return /^y/i.test(a.trim()) ? 1 : 0;
|
|
2075
2110
|
}
|
|
2076
2111
|
if (name === "builtin_3f" && prompts.length <= 2 && refs === 1) {
|
|
2077
|
-
const a = await this.
|
|
2078
|
-
`
|
|
2112
|
+
const a = await this.ask(`
|
|
2079
2113
|
${p0}
|
|
2080
2114
|
${p1}
|
|
2081
|
-
[OK = y, cancel = n] `
|
|
2082
|
-
);
|
|
2115
|
+
[OK = y, cancel = n] `);
|
|
2083
2116
|
return a != null && /^y/i.test(a.trim()) ? 0 : null;
|
|
2084
2117
|
}
|
|
2085
2118
|
const hex = /hex/i.test(name);
|
|
@@ -2089,7 +2122,7 @@ ${p1}
|
|
|
2089
2122
|
for (let k = 0; k < refs; k++) {
|
|
2090
2123
|
const cap = refs > 1 ? prompts[2 + k] || `${p0} (${k + 1}/${refs})` : p1;
|
|
2091
2124
|
const range = step.lo != null && step.hi != null && !hex ? ` [${step.lo}..${step.hi}]` : "";
|
|
2092
|
-
const a = await this.
|
|
2125
|
+
const a = await this.ask(`
|
|
2093
2126
|
${p0}
|
|
2094
2127
|
${cap}${range}: `);
|
|
2095
2128
|
if (a == null) return null;
|
|
@@ -2111,7 +2144,7 @@ ${cap}${range}: `);
|
|
|
2111
2144
|
return refs > 1 ? vals : vals[0];
|
|
2112
2145
|
}
|
|
2113
2146
|
async confirmKey(_p, it, jobs, writes) {
|
|
2114
|
-
const a = await this.
|
|
2147
|
+
const a = await this.ask(
|
|
2115
2148
|
`
|
|
2116
2149
|
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
2150
|
);
|
|
@@ -2119,7 +2152,7 @@ Run "${it.label || it.legendLabel || `F${it.nr}`}"? It can send ${jobs.join(", "
|
|
|
2119
2152
|
}
|
|
2120
2153
|
async confirmWrite(_p, job, arg, ctx) {
|
|
2121
2154
|
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.
|
|
2155
|
+
const a = await this.ask(
|
|
2123
2156
|
`
|
|
2124
2157
|
Send ${job}${arg ? ` ${arg}` : ""} to the module (${ctx.label})?${every} [y/N] `
|
|
2125
2158
|
);
|
|
@@ -2144,7 +2177,7 @@ Send ${job}${arg ? ` ${arg}` : ""} to the module (${ctx.label})?${every} [y/N] `
|
|
|
2144
2177
|
}
|
|
2145
2178
|
const list = rows.map((r, i2) => ` ${i2 + 1}. ${r.caption} (${r.key})`).join("\n");
|
|
2146
2179
|
if (step.multiple) {
|
|
2147
|
-
const a2 = await this.
|
|
2180
|
+
const a2 = await this.ask(
|
|
2148
2181
|
`
|
|
2149
2182
|
${list}
|
|
2150
2183
|
Components, comma-separated (Enter cancels): `
|
|
@@ -2155,14 +2188,12 @@ Components, comma-separated (Enter cancels): `
|
|
|
2155
2188
|
if (!picked.length) return null;
|
|
2156
2189
|
return { ort: picked.map((r) => r.key).join(";"), ein: 0 };
|
|
2157
2190
|
}
|
|
2158
|
-
const a = await this.
|
|
2159
|
-
`
|
|
2191
|
+
const a = await this.ask(`
|
|
2160
2192
|
${list}
|
|
2161
|
-
Component number (Enter cancels): `
|
|
2162
|
-
);
|
|
2193
|
+
Component number (Enter cancels): `);
|
|
2163
2194
|
const [i] = pickNumbers(a, rows.length);
|
|
2164
2195
|
if (i == null) return null;
|
|
2165
|
-
const onOff = await this.
|
|
2196
|
+
const onOff = await this.ask(`On or off? [on/off] `);
|
|
2166
2197
|
if (onOff == null || !onOff.trim()) return null;
|
|
2167
2198
|
return {
|
|
2168
2199
|
ort: rows[i].key,
|
|
@@ -2180,7 +2211,7 @@ Component number (Enter cancels): `
|
|
|
2180
2211
|
return null;
|
|
2181
2212
|
}
|
|
2182
2213
|
const list = names.map((n, i) => ` ${i + 1}. ${n}`).join("\n");
|
|
2183
|
-
const a = await this.
|
|
2214
|
+
const a = await this.ask(
|
|
2184
2215
|
`
|
|
2185
2216
|
${list}
|
|
2186
2217
|
Lines to show${multiple ? ", comma-separated" : ""} (a = all, Enter cancels): `
|
|
@@ -2192,7 +2223,7 @@ Lines to show${multiple ? ", comma-separated" : ""} (a = all, Enter cancels): `
|
|
|
2192
2223
|
}
|
|
2193
2224
|
/** INPA's save-as dialog: a file name, written when the body ends. */
|
|
2194
2225
|
async saveFile() {
|
|
2195
|
-
const a = await this.
|
|
2226
|
+
const a = await this.ask(`
|
|
2196
2227
|
Save as [fault-memory.txt]: `);
|
|
2197
2228
|
if (a == null) return null;
|
|
2198
2229
|
return { name: a.trim() || "fault-memory.txt" };
|
|
@@ -2205,6 +2236,7 @@ Save as [fault-memory.txt]: `);
|
|
|
2205
2236
|
this.term.write(`
|
|
2206
2237
|
${this.gridLines(p).join("\n")}
|
|
2207
2238
|
`);
|
|
2239
|
+
this.frame = [];
|
|
2208
2240
|
}
|
|
2209
2241
|
/**
|
|
2210
2242
|
* The module a scriptchange names. From the home script it is the car's
|
|
@@ -2258,23 +2290,62 @@ ${this.gridLines(p).join("\n")}
|
|
|
2258
2290
|
}
|
|
2259
2291
|
this.paint(p);
|
|
2260
2292
|
}
|
|
2261
|
-
/** Redraw everything: title, grid, keys, footer. */
|
|
2293
|
+
/** Redraw everything: title, grid, keys, footer -- in place. */
|
|
2262
2294
|
paint(p) {
|
|
2295
|
+
this.flush(this.frameLines(p));
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* The frame as lines: title, rule, the view or the grid, a blank, the
|
|
2299
|
+
* key bar, then the status and progress lines. Cut to the terminal's
|
|
2300
|
+
* width (a wrapped line would break the row count the redraw relies on)
|
|
2301
|
+
* and to its height, the body giving way first.
|
|
2302
|
+
* @param p - the program
|
|
2303
|
+
* @returns the lines, none wider than the terminal
|
|
2304
|
+
*/
|
|
2305
|
+
frameLines(p) {
|
|
2263
2306
|
const w = this.term.columns;
|
|
2264
|
-
const out = [];
|
|
2265
2307
|
const title = `${p.ecu.label || p.ecu.sgbd} ${p.ecu.sgbd}.prg ${p.title || ""}`.trim();
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2308
|
+
const head = [title, "-".repeat(Math.min(w, 78))];
|
|
2309
|
+
let body = p.view ? [...p.view.lines || []] : this.gridLines(p);
|
|
2310
|
+
const tail = ["", ...this.keyLines(p), this.statusText, this.progressText];
|
|
2311
|
+
const room = Math.max(1, this.term.rows - 1 - head.length - tail.length);
|
|
2312
|
+
if (body.length > room) body = body.filter((l, i) => l || body[i - 1]);
|
|
2313
|
+
if (body.length > room) {
|
|
2314
|
+
const hidden = body.length - (room - 1);
|
|
2315
|
+
body = [
|
|
2316
|
+
...body.slice(0, room - 1),
|
|
2317
|
+
`(${hidden} more rows: enlarge the terminal)`
|
|
2318
|
+
];
|
|
2272
2319
|
}
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2320
|
+
return [...head, ...body, ...tail].map(
|
|
2321
|
+
(l) => String(l).replace(/[\r\n]/g, " ").slice(0, w)
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
/**
|
|
2325
|
+
* Put a frame on the terminal over the last one. With a frame on screen
|
|
2326
|
+
* the cursor is on the line under it: go up to its top, rewrite the lines
|
|
2327
|
+
* that differ (each erased to the end of the row), step over the ones
|
|
2328
|
+
* that match, and erase whatever the old frame had below the new one.
|
|
2329
|
+
* Without one (first paint, after a prompt, after a resize) the frame is
|
|
2330
|
+
* written where the cursor is.
|
|
2331
|
+
* @param lines - the new frame
|
|
2332
|
+
*/
|
|
2333
|
+
flush(lines) {
|
|
2334
|
+
const size = `${this.term.columns}x${this.term.rows}`;
|
|
2335
|
+
if (size !== this.frameSize) this.frame = [];
|
|
2336
|
+
this.frameSize = size;
|
|
2337
|
+
const prev = this.frame;
|
|
2338
|
+
if (prev.length === lines.length && prev.every((l, i) => l === lines[i]))
|
|
2339
|
+
return;
|
|
2340
|
+
let s = prev.length ? `\x1B[${prev.length}A` : "";
|
|
2341
|
+
lines.forEach((line, i) => {
|
|
2342
|
+
if (i < prev.length && prev[i] === line) s += "\x1B[B";
|
|
2343
|
+
else s += `\r${line}\x1B[K\r
|
|
2344
|
+
`;
|
|
2345
|
+
});
|
|
2346
|
+
if (lines.length < prev.length) s += "\r\x1B[J";
|
|
2347
|
+
if (s) this.term.write(s);
|
|
2348
|
+
this.frame = lines;
|
|
2278
2349
|
}
|
|
2279
2350
|
/**
|
|
2280
2351
|
* The grid as text rows: cells placed at their column, a lamp as a dot
|
|
@@ -2314,12 +2385,20 @@ ${this.gridLines(p).join("\n")}
|
|
|
2314
2385
|
);
|
|
2315
2386
|
return out;
|
|
2316
2387
|
}
|
|
2317
|
-
/**
|
|
2388
|
+
/**
|
|
2389
|
+
* The two bottom lines of the frame: status and progress. They are part
|
|
2390
|
+
* of the frame, so a change repaints it (which writes just those lines);
|
|
2391
|
+
* before a program exists the text is written on its own.
|
|
2392
|
+
*/
|
|
2318
2393
|
drawFooter() {
|
|
2319
|
-
this.
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2394
|
+
if (this.program) {
|
|
2395
|
+
this.paint(this.program);
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
this.frame = [];
|
|
2399
|
+
this.term.write(`${this.statusText}\r
|
|
2400
|
+
${this.progressText}\r
|
|
2401
|
+
`);
|
|
2323
2402
|
}
|
|
2324
2403
|
left() {
|
|
2325
2404
|
if (this.leftResolve) this.leftResolve();
|
|
@@ -2446,7 +2525,7 @@ async function tuiCommand(chassis, sgbd, opts = {}) {
|
|
|
2446
2525
|
}
|
|
2447
2526
|
|
|
2448
2527
|
// src/bmweb.ts
|
|
2449
|
-
var VERSION = true ? "0.1.
|
|
2528
|
+
var VERSION = true ? "0.1.1" : "0.0.0-dev";
|
|
2450
2529
|
var INCLUDE = {
|
|
2451
2530
|
include: {
|
|
2452
2531
|
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.1",
|
|
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
|