bmweb-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +351 -0
- package/dist/bmweb.js +2790 -0
- package/package.json +53 -0
- package/runtime/core/bestvm/codec.js +285 -0
- package/runtime/core/bestvm/environment.js +116 -0
- package/runtime/core/bestvm/executor.js +1483 -0
- package/runtime/core/bestvm/index.js +52 -0
- package/runtime/core/bestvm/machine.js +491 -0
- package/runtime/core/bestvm/operands.js +356 -0
- package/runtime/core/bestvm/registers.js +152 -0
- package/runtime/core/bestvm/write-guard.js +111 -0
- package/runtime/core/ipofile/compile.js +364 -0
- package/runtime/core/ipofile/decls.js +187 -0
- package/runtime/core/ipofile/emit.js +708 -0
- package/runtime/core/ipofile/exec.js +164 -0
- package/runtime/core/ipofile/lex.js +243 -0
- package/runtime/core/ipofile/parse.js +550 -0
- package/runtime/core/ipofile/pool.js +404 -0
- package/runtime/core/ipofile/walk.js +431 -0
- package/runtime/core/ipovm/builtin-helpers.js +182 -0
- package/runtime/core/ipovm/builtins-api.js +610 -0
- package/runtime/core/ipovm/builtins-screen.js +493 -0
- package/runtime/core/ipovm/builtins-table.js +166 -0
- package/runtime/core/ipovm/builtins-text.js +166 -0
- package/runtime/core/ipovm/emissions.js +138 -0
- package/runtime/core/ipovm/hosts.js +191 -0
- package/runtime/core/ipovm/operators.js +229 -0
- package/runtime/core/ipovm/structures.js +250 -0
- package/runtime/core/ipovm/suspensions.js +241 -0
- package/runtime/core/ipovm/tape.js +206 -0
- package/runtime/core/ipovm/values.js +241 -0
- package/runtime/core/ipovm/vm.js +1166 -0
- package/runtime/core/translate.js +526 -0
- package/runtime/core/webshim/api-router.js +592 -0
- package/runtime/core/webshim/bus.js +95 -0
- package/runtime/core/webshim/coding.js +82 -0
- package/runtime/core/webshim/data-fetch.js +66 -0
- package/runtime/core/webshim/exchange.js +288 -0
- package/runtime/core/webshim/framing.js +331 -0
- package/runtime/core/webshim/install.js +30 -0
- package/runtime/core/webshim/job-runner.js +319 -0
- package/runtime/core/webshim/native-bus.js +108 -0
- package/runtime/core/webshim/timers.js +82 -0
- package/runtime/core/webshim/trace.js +205 -0
- package/runtime/core/webshim/transport-base.js +128 -0
- package/runtime/core/webshim/variant-resolver.js +249 -0
- package/runtime/core/webshim/web-serial-bus.js +734 -0
- package/runtime/home/bmweb-home.ips +76 -0
- package/runtime/home/bmweb.h +26 -0
- package/runtime/screens/activations.js +258 -0
- package/runtime/screens/garage/diff.js +331 -0
- package/runtime/screens/garage/share.js +276 -0
- package/runtime/screens/garage/store.js +547 -0
- package/runtime/screens/ipo-runtime/cells.js +176 -0
- package/runtime/screens/ipo-runtime/dialogs.js +254 -0
- package/runtime/screens/ipo-runtime/home.js +358 -0
- package/runtime/screens/ipo-runtime/open.js +393 -0
- package/runtime/screens/ipo-runtime/paint-grid.js +106 -0
- package/runtime/screens/ipo-runtime/paint-modern.js +424 -0
- package/runtime/screens/ipo-runtime/print.js +281 -0
- package/runtime/screens/ipo-runtime/program.js +1337 -0
- package/runtime/screens/ipo-runtime/protocol.js +464 -0
- package/runtime/screens/ipo-runtime/script-scan.js +225 -0
- package/runtime/screens/ipo-runtime/translate-sets.js +130 -0
- package/runtime/screens/ipo-runtime/ui.js +249 -0
- package/runtime/screens/ipo-runtime/wire-policy.js +113 -0
- package/runtime/screens/ir.js +324 -0
- package/runtime/screens/search/data.js +153 -0
- package/runtime/screens/search/match.js +285 -0
- package/runtime/screens/search/open.js +66 -0
- package/runtime/vendor/fflate.min.js +1 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The ONE explicitly-coding write path.
|
|
3
|
+
*
|
|
4
|
+
* Everything else in this shim runs the VM read-mindedly (webRunJob, the
|
|
5
|
+
* probe with allowWrites:false). A coding write is different: it is the app's
|
|
6
|
+
* most safety-critical action, so it gets its own gate, its own
|
|
7
|
+
* confirmation, and prove-by-re-read.
|
|
8
|
+
*
|
|
9
|
+
* The write permission itself lives in coding-write.js, NOT here: that
|
|
10
|
+
* module is the only place that constructs the VM writeable, and it does so
|
|
11
|
+
* only after this function has checked opts.confirmed. Keeping the
|
|
12
|
+
* write-enabling VM construction out of the shim is deliberate -- webRunJob
|
|
13
|
+
* and every ordinary job run stay provably read-only (test_write_gate.js
|
|
14
|
+
* reads the shim and asserts what it builds).
|
|
15
|
+
*
|
|
16
|
+
* It runs over the SAME bus lock as reads (withBusLock), so a coding sequence
|
|
17
|
+
* cannot interleave with the topbar state poll or any job on the wire.
|
|
18
|
+
*/
|
|
19
|
+
/* exported webWriteCoding */
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Write coding data to a module through coding-write.js's confirmed path.
|
|
23
|
+
* @param {string} sgbd - The SGBD to code.
|
|
24
|
+
* @param {string} nettoHex - The netto coding bytes as hex.
|
|
25
|
+
* @param {{confirmed?: boolean, jobname?: string}} [opts] - `confirmed` must
|
|
26
|
+
* be true (the UI's explicit confirmation); `jobname` picks the dispatcher
|
|
27
|
+
* job when a derived dispatcher is shipped (default SG_CODIEREN).
|
|
28
|
+
* @returns {Promise<any>} Whatever writeCoding resolves with.
|
|
29
|
+
* @throws {Error} When coding-write.js is absent, the write is unconfirmed,
|
|
30
|
+
* no cable is connected, or no job code is shipped for the SGBD.
|
|
31
|
+
*/
|
|
32
|
+
async function webWriteCoding(sgbd, nettoHex, opts = {}) {
|
|
33
|
+
if (typeof window.writeCoding !== 'function') {
|
|
34
|
+
throw new Error('coding-write.js is not loaded');
|
|
35
|
+
}
|
|
36
|
+
if (!opts.confirmed) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
'coding write requires an explicit confirmation ' +
|
|
39
|
+
'(opts.confirmed) from the UI before it can transmit'
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (!webBus.connected) throw new Error('no cable connected');
|
|
43
|
+
|
|
44
|
+
const key = String(sgbd).toLowerCase();
|
|
45
|
+
// The SGBD program, its tables, and its job list -- the same sources the
|
|
46
|
+
// read path uses, so the strategy sees exactly the jobs this module exposes.
|
|
47
|
+
const code = await webFetchJson(`data/job-code/${key}.json`);
|
|
48
|
+
if (!code) throw new Error(`no job code shipped for ${sgbd}`);
|
|
49
|
+
const tables = (await webFetchJson(`data/sgbd-tables/${key}.json`)) || {};
|
|
50
|
+
|
|
51
|
+
// DISPATCHER PROGRAM (optional). When a derived A_<cabd> coding dispatcher
|
|
52
|
+
// is shipped for this module, writeCoding runs BMW's own dispatcher instead
|
|
53
|
+
// of the hand-sequenced strategy (see coding-write.js writeViaDispatch). The
|
|
54
|
+
// exec ships next to the module's job-code as <sgbd>.ipoexec.json with a
|
|
55
|
+
// "coding":true marker; opts.dataOrg carries the CABD word width. Absent, the
|
|
56
|
+
// strategy path runs unchanged, so this is additive and safe.
|
|
57
|
+
const dispatch = await webFetchJson(`data/coding-dispatch/${key}.json`);
|
|
58
|
+
const dispatchOk = dispatch && dispatch.coding && dispatch.procs;
|
|
59
|
+
|
|
60
|
+
// One SGBD is loaded at a time, exactly like a read: end the previous
|
|
61
|
+
// session (its ENDE) before this one initialises, then run the whole write
|
|
62
|
+
// sequence under the bus lock so nothing else touches the wire mid-coding.
|
|
63
|
+
await switchSession(sgbd);
|
|
64
|
+
const session = sessionFor(sgbd);
|
|
65
|
+
return withBusLock(() =>
|
|
66
|
+
window.writeCoding(sgbd, nettoHex, {
|
|
67
|
+
confirmed: true,
|
|
68
|
+
code,
|
|
69
|
+
tables,
|
|
70
|
+
jobs: code.jobs,
|
|
71
|
+
// the bus-locked wire, called from inside the lock we already hold --
|
|
72
|
+
// webBus.exchange re-enters withBusLock, which the promise chain
|
|
73
|
+
// serialises, so pass the RAW exchange to avoid queuing behind ourselves
|
|
74
|
+
exchange: (out, comm) => webBusRawExchange(out, comm),
|
|
75
|
+
session,
|
|
76
|
+
// the dispatcher program + its word width, only when shipped for this CABD
|
|
77
|
+
dispatch: dispatchOk ? dispatch : null,
|
|
78
|
+
dataOrg: dispatchOk ? dispatch.dataOrg || null : null,
|
|
79
|
+
jobname: dispatchOk ? opts.jobname || 'SG_CODIEREN' : undefined,
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Data loaders the job runner, resolver and coding path share: plain
|
|
3
|
+
* JSON, gzipped JSON, and the shared SGBD tables.
|
|
4
|
+
*
|
|
5
|
+
* These go through the global `fetch`, which install.js has replaced with the
|
|
6
|
+
* shim -- so `data/job-code/<sgbd>.json` and friends are answered from the
|
|
7
|
+
* cached ECU archives, not the network.
|
|
8
|
+
*/
|
|
9
|
+
/* exported webFetchJson, webFetchGz, loadSharedTables */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Fetch a JSON file, or null on any failure (404, bad JSON).
|
|
13
|
+
* @param {string} path - The app-relative path.
|
|
14
|
+
* @returns {Promise<any|null>} The parsed body, or null.
|
|
15
|
+
*/
|
|
16
|
+
async function webFetchJson(path) {
|
|
17
|
+
const r = await fetch(path);
|
|
18
|
+
return r.ok ? r.json().catch(() => null) : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Fetch a gzipped JSON file. data/groups files are gzipped JSON served
|
|
23
|
+
* as-is; a host that transparently content-decodes hands us plain JSON, so
|
|
24
|
+
* both are taken. Null on any failure.
|
|
25
|
+
* @param {string} path - The app-relative path.
|
|
26
|
+
* @returns {Promise<any|null>} The parsed body, or null.
|
|
27
|
+
*/
|
|
28
|
+
async function webFetchGz(path) {
|
|
29
|
+
try {
|
|
30
|
+
const r = await fetch(path);
|
|
31
|
+
if (!r.ok) return null;
|
|
32
|
+
const buf = new Uint8Array(await r.arrayBuffer());
|
|
33
|
+
const isGz = buf.length > 2 && buf[0] === 0x1f && buf[1] === 0x8b;
|
|
34
|
+
if (isGz && typeof fflate === 'undefined') {
|
|
35
|
+
throw new Error('fflate decompression library not loaded');
|
|
36
|
+
}
|
|
37
|
+
const text = new TextDecoder('utf-8').decode(
|
|
38
|
+
isGz ? fflate.gunzipSync(buf) : buf
|
|
39
|
+
);
|
|
40
|
+
return JSON.parse(text);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The shared table files, loaded once (the PROMISE is cached so concurrent
|
|
48
|
+
* callers fetch once).
|
|
49
|
+
* @type {Promise<Record<string, any>>|null}
|
|
50
|
+
*/
|
|
51
|
+
let sharedTablesPromise = null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The shared table files (t_pcod, t_scod, t_ausb, t_grtb): an SGBD reads
|
|
55
|
+
* them with `tabsetex <table>, <file>` -- ms450ds0's FS_LESEN_DETAIL looks
|
|
56
|
+
* its P-code up in t_pcod's PCodeTexte. Loaded once, given to every VM.
|
|
57
|
+
* @returns {Promise<Record<string, any>>} file name -> tables ({} when absent).
|
|
58
|
+
*/
|
|
59
|
+
function loadSharedTables() {
|
|
60
|
+
if (!sharedTablesPromise) {
|
|
61
|
+
sharedTablesPromise = webFetchGz('data/groups/shared-tables.json.gz')
|
|
62
|
+
.then((t) => (t && typeof t === 'object' ? t : {}))
|
|
63
|
+
.catch(() => ({}));
|
|
64
|
+
}
|
|
65
|
+
return sharedTablesPromise;
|
|
66
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file One request/answer exchange: echo handling, frame assembly,
|
|
3
|
+
* response-pending polling, bus pacing and the single retransmission.
|
|
4
|
+
*
|
|
5
|
+
* Shared by both transports, because the protocol does not change with the
|
|
6
|
+
* plumbing -- each transport only supplies exchangeRaw (write + read) and
|
|
7
|
+
* ensureConfig (port settings); everything else lives here.
|
|
8
|
+
*/
|
|
9
|
+
/* exported readFrame, runExchange */
|
|
10
|
+
|
|
11
|
+
/** How long readFrame idles between empty reads while waiting for bytes. */
|
|
12
|
+
const RX_POLL_MS = 4;
|
|
13
|
+
/**
|
|
14
|
+
* Once an answer has STARTED, the least it gets to finish. ParTimeoutStd is
|
|
15
|
+
* time-to-first-byte; a fault memory at 9600 baud needs ~250 ms of wire time
|
|
16
|
+
* after that and must not be cut off by a 500 ms answer timeout it already met.
|
|
17
|
+
*/
|
|
18
|
+
const FRAME_COMPLETE_MIN_MS = 3000;
|
|
19
|
+
/** The answer timeout when an SGBD's CommParameter names none. */
|
|
20
|
+
const DEFAULT_TIMEOUT_MS = 2000;
|
|
21
|
+
/** The most a `wait` in the SGBD may stall the bus before a write. */
|
|
22
|
+
const WAIT_CAP_MS = 5000;
|
|
23
|
+
/** ParRegenTime above this is a bogus blob, not a pacing rule. */
|
|
24
|
+
const REGEN_MAX_MS = 1000;
|
|
25
|
+
/** The floor for ParTimeoutNr78 when the SGBD names none (a slow routine). */
|
|
26
|
+
const NR78_MIN_MS = 5000;
|
|
27
|
+
/** How many "response pending" polls a stuck ECU gets before it fails. */
|
|
28
|
+
const MAX_PENDING_POLLS = 30;
|
|
29
|
+
/** Send once, retransmit once: EDIABAS's single-glitch cover. */
|
|
30
|
+
const EXCHANGE_ATTEMPTS = 2;
|
|
31
|
+
/** KWP negative response service id. */
|
|
32
|
+
const NEGATIVE_RESPONSE = 0x7f;
|
|
33
|
+
/** The negative-response code meaning "still working, ask again". */
|
|
34
|
+
const RESPONSE_PENDING = 0x78;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Pull whatever the port has right now.
|
|
38
|
+
* @callback PumpFn
|
|
39
|
+
* @returns {Promise<ArrayLike<number>|null|undefined>} Bytes, or nothing yet.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read one answer off the K line.
|
|
44
|
+
*
|
|
45
|
+
* The K line is HALF DUPLEX: one wire, so everything written is also heard
|
|
46
|
+
* back. Drop exactly as many bytes as were sent rather than pattern-matching
|
|
47
|
+
* the echo -- a request and its answer can legitimately share a prefix, and
|
|
48
|
+
* the reference interface drops by count for the same reason.
|
|
49
|
+
*
|
|
50
|
+
* A WIRED K LINE ALWAYS ECHOES. "Adapter echo" in the reference stack refers
|
|
51
|
+
* to a REMOTE adapter (Bluetooth/WiFi) that strips the echo for you; an FTDI
|
|
52
|
+
* cable on a half-duplex wire does not, so the echo is always here and is
|
|
53
|
+
* always dropped by count.
|
|
54
|
+
*
|
|
55
|
+
* @param {number[]|null} sent - The exact request written, or null when
|
|
56
|
+
* re-reading a continuation frame, which has no echo of its own.
|
|
57
|
+
* @param {number} timeoutMs - ParTimeoutStd: how long the ECU may take to
|
|
58
|
+
* START answering.
|
|
59
|
+
* @param {PumpFn} pump - Reads whatever the port has buffered.
|
|
60
|
+
* @param {CommParams} comm - The request's wire parameters (framing rules).
|
|
61
|
+
* @returns {Promise<number[]>} The complete, checksum-verified answer frame.
|
|
62
|
+
* @throws {Error} IFH-0003 when the echo never matches, IFH-0019 on a
|
|
63
|
+
* truncated or mis-signed frame, IFH-0009 on silence.
|
|
64
|
+
*/
|
|
65
|
+
async function readFrame(sent, timeoutMs, pump, comm) {
|
|
66
|
+
const buf = [];
|
|
67
|
+
const echoLen = sent ? sent.length : 0;
|
|
68
|
+
const deadline = Date.now() + timeoutMs;
|
|
69
|
+
|
|
70
|
+
// Where does `sent` start inside buf? -1 while it is not (yet) all here.
|
|
71
|
+
const findEcho = () => {
|
|
72
|
+
for (let start = 0; start + echoLen <= buf.length; start++) {
|
|
73
|
+
let ok = true;
|
|
74
|
+
for (let i = 0; i < echoLen; i++) {
|
|
75
|
+
if (buf[start + i] !== sent[i]) {
|
|
76
|
+
ok = false;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (ok) return start;
|
|
81
|
+
}
|
|
82
|
+
return -1;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// READ UNTIL THE ECHO IS ACTUALLY THERE, not merely until enough bytes have
|
|
86
|
+
// arrived. A half-duplex K line delivers the echo split across reads and
|
|
87
|
+
// often with a leftover byte in front of it -- "12 04 00 16" came back as
|
|
88
|
+
// "00 12 04 00" then "16" on the next read. Stopping at buf.length >=
|
|
89
|
+
// echoLen left the echo one byte short, the compare failed, and a healthy
|
|
90
|
+
// exchange was reported as IFH-0003.
|
|
91
|
+
let at = -1;
|
|
92
|
+
while (Date.now() < deadline) {
|
|
93
|
+
if (echoLen && (at = findEcho()) >= 0) break;
|
|
94
|
+
if (!echoLen && buf.length) break;
|
|
95
|
+
const got = await pump();
|
|
96
|
+
if (got && got.length) buf.push(...got);
|
|
97
|
+
else await bmwSleep(RX_POLL_MS);
|
|
98
|
+
}
|
|
99
|
+
if (echoLen && at < 0) {
|
|
100
|
+
throw ifhError(
|
|
101
|
+
'IFH-0003',
|
|
102
|
+
buf.length
|
|
103
|
+
? 'echo did not match the request (bus collision?)'
|
|
104
|
+
: 'no echo from the cable (is it connected to the car?)'
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
buf.splice(0, at >= 0 ? at + echoLen : 0); // drop leading noise AND the echo
|
|
108
|
+
// timeoutMs is ParTimeoutStd: how long the ECU may take to START answering.
|
|
109
|
+
// EDIABAS ends the frame on inter-byte silence (ParTimeoutTelEnd), not on
|
|
110
|
+
// that budget -- so once the first byte is here, a long answer at 9600 baud
|
|
111
|
+
// (a fault memory: ~250 ms of wire time) must not be cut off by a 500 ms
|
|
112
|
+
// answer timeout that it already met. Judge silence at timeoutMs; give a
|
|
113
|
+
// started frame its own completion budget.
|
|
114
|
+
const completionBudget = () =>
|
|
115
|
+
Date.now() + Math.max(timeoutMs, FRAME_COMPLETE_MIN_MS);
|
|
116
|
+
let frameDeadline = deadline;
|
|
117
|
+
let started = buf.length > 0;
|
|
118
|
+
if (started) frameDeadline = completionBudget();
|
|
119
|
+
while (Date.now() < frameDeadline) {
|
|
120
|
+
const total = frameTotal(buf, comm);
|
|
121
|
+
if (total !== null && buf.length >= total) {
|
|
122
|
+
const frame = buf.slice(0, total);
|
|
123
|
+
verifyChecksum(frame, comm);
|
|
124
|
+
return frame;
|
|
125
|
+
}
|
|
126
|
+
const got = await pump();
|
|
127
|
+
if (got && got.length) {
|
|
128
|
+
buf.push(...got);
|
|
129
|
+
if (!started) {
|
|
130
|
+
started = true;
|
|
131
|
+
frameDeadline = completionBudget();
|
|
132
|
+
}
|
|
133
|
+
} else await bmwSleep(RX_POLL_MS);
|
|
134
|
+
}
|
|
135
|
+
// A half-received frame is NOT an answer -- handing it to the VM decodes
|
|
136
|
+
// garbage. Distinguish it from silence so the error means something.
|
|
137
|
+
throw buf.length
|
|
138
|
+
? ifhError('IFH-0019', `incomplete answer from ECU (${buf.length} bytes)`)
|
|
139
|
+
: ifhError('IFH-0009', 'no answer from ECU (timeout)');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* "Response pending": an ECU that needs longer than its declared timeout
|
|
144
|
+
* answers 7F <service> 78 and keeps the request alive. EDIABAS waits for
|
|
145
|
+
* the real answer instead of failing; a flash or a long routine depends on
|
|
146
|
+
* it. The payload offset follows the same framing rules as frameTotal:
|
|
147
|
+
* DS2 puts it at 2, KWP2000* behind its 4-byte header, BMW-FAST at 3 for
|
|
148
|
+
* the short form and 4 for the long form (len byte at [3]) -- a long-frame
|
|
149
|
+
* 7F..78 sliced at 3 was returned to the VM as the final answer.
|
|
150
|
+
* @param {number[]} frame - A complete answer frame.
|
|
151
|
+
* @param {CommParams} comm - The request's wire parameters.
|
|
152
|
+
* @returns {boolean} True when the ECU asked for more time.
|
|
153
|
+
*/
|
|
154
|
+
function isResponsePending(frame, comm) {
|
|
155
|
+
const c = conceptOf(comm);
|
|
156
|
+
let body;
|
|
157
|
+
if (isDs2(c)) body = frame.slice(2);
|
|
158
|
+
else if (c === 0x10d) body = frame.slice(4);
|
|
159
|
+
else if (frame[0] === 0xb8) body = frame.slice(4);
|
|
160
|
+
else body = frame[0] & 0x3f ? frame.slice(3) : frame.slice(4);
|
|
161
|
+
return body[0] === NEGATIVE_RESPONSE && body[2] === RESPONSE_PENDING;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The quiet gap an SGBD demands between an answer and the next request.
|
|
166
|
+
* CommParameter dword 3 on the 0x1xx concepts (ParRegenTime); the DS2 case
|
|
167
|
+
* reads index 6 of its own 16-bit layout -- Best2Vm.decodeCommParams picks
|
|
168
|
+
* the concept's own index. Clamped, because a bogus blob must not stall the
|
|
169
|
+
* bus.
|
|
170
|
+
* @param {CommParams|null|undefined} comm - The telegram's wire parameters.
|
|
171
|
+
* @returns {number} Milliseconds of quiet, 0 when none or out of range.
|
|
172
|
+
*/
|
|
173
|
+
function regenTimeOf(comm) {
|
|
174
|
+
const raw = comm && comm.regen != null ? comm.regen : 0;
|
|
175
|
+
return raw > 0 && raw <= REGEN_MAX_MS ? raw : 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Honour the pacing an exchange owes the bus before its write: a `wait` the
|
|
180
|
+
* SGBD issued, and ParRegenTime measured from the ECU's last answer.
|
|
181
|
+
*
|
|
182
|
+
* ParRegenTime is a MANDATORY quiet gap between the ECU's last answer and the
|
|
183
|
+
* next request, measured from the response. The SGBD names it in its
|
|
184
|
+
* CommParameter -- 25 ms for this MS45 on concept 0x10D. Without it a
|
|
185
|
+
* telegram sent immediately after a reply is ignored, which is exactly what
|
|
186
|
+
* made a repeated FS_LESEN come back empty while the identical first one was
|
|
187
|
+
* answered.
|
|
188
|
+
* @param {Transport} bus - The transport, for its lastResponseAt.
|
|
189
|
+
* @param {CommParams} comm - The telegram's wire parameters.
|
|
190
|
+
* @returns {Promise<void>}
|
|
191
|
+
*/
|
|
192
|
+
async function paceBeforeWrite(bus, comm) {
|
|
193
|
+
// a `wait` in the SGBD paces the bus: honor it before writing
|
|
194
|
+
if (comm && comm.waitMs) {
|
|
195
|
+
await bmwSleep(Math.min(comm.waitMs, WAIT_CAP_MS));
|
|
196
|
+
}
|
|
197
|
+
const regenMs = regenTimeOf(comm);
|
|
198
|
+
if (regenMs && bus.lastResponseAt) {
|
|
199
|
+
const since = Date.now() - bus.lastResponseAt;
|
|
200
|
+
if (since < regenMs) {
|
|
201
|
+
await bmwSleep(regenMs - since);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* One request/answer exchange with per-concept retry. EDIABAS retransmits
|
|
208
|
+
* on a bad or missing answer (xreps); one retry covers the single-glitch
|
|
209
|
+
* case without hammering a dead bus.
|
|
210
|
+
*
|
|
211
|
+
* EVERY xsetpar RECONFIGURES THE WIRE. That is what the opcode is for, and
|
|
212
|
+
* EDIABAS honours each one: tracing this car showed it set concept 0x10F at
|
|
213
|
+
* 115200, send the short telegram, get silence, then set concept 0x10D at
|
|
214
|
+
* 9600 and send the long one -- which is answered. An earlier revision here
|
|
215
|
+
* pinned the wire to the FIRST concept of a session, so the second xsetpar
|
|
216
|
+
* was ignored, the B8 telegram went out at 115200 instead of 9600, and the
|
|
217
|
+
* DME never heard it. The VM had reached the right branch all along.
|
|
218
|
+
* sessionConcept is still tracked, but only so the K-line wake knows it is
|
|
219
|
+
* on a K-line module; it no longer overrides the telegram's own wire.
|
|
220
|
+
*
|
|
221
|
+
* NO FRAMING FALLBACK HERE. The SGBD owns that: tracing EDIABAS showed
|
|
222
|
+
* ms450ds0's INITIALISIERUNG hold BOTH telegrams as constants and try them
|
|
223
|
+
* in turn -- xsend "82 12 F1 1A 80" gets IFH-0009, the bytecode carries on,
|
|
224
|
+
* and xsend "B8 12 F1 02 1A 80" is answered. So a timed-out exchange must be
|
|
225
|
+
* reported to the VM, not retried behind its back with a rewritten telegram.
|
|
226
|
+
*
|
|
227
|
+
* @param {Transport} bus - The transport to drive.
|
|
228
|
+
* @param {ArrayLike<number>} out - The request without its checksum.
|
|
229
|
+
* @param {CommParams} comm - The request's wire parameters.
|
|
230
|
+
* @returns {Promise<number[]>} The answer frame.
|
|
231
|
+
* @throws {Error} The last IFH error when both attempts fail, or the first
|
|
232
|
+
* error that is not a garbled answer.
|
|
233
|
+
*/
|
|
234
|
+
async function runExchange(bus, out, comm) {
|
|
235
|
+
// An ADS-only concept (1, 2, 3) fails here with its reason, before the
|
|
236
|
+
// port is touched -- the reference refuses them the same way (IFH-0006).
|
|
237
|
+
assertReachable(comm);
|
|
238
|
+
bus.sessionConcept = conceptOf(comm);
|
|
239
|
+
await bus.ensureConfig(portConfig(comm));
|
|
240
|
+
const framed = withChecksum(out, comm);
|
|
241
|
+
const timeoutMs = (comm && comm.timeout) || DEFAULT_TIMEOUT_MS;
|
|
242
|
+
await paceBeforeWrite(bus, comm);
|
|
243
|
+
let lastErr;
|
|
244
|
+
for (let attempt = 0; attempt < EXCHANGE_ATTEMPTS; attempt++) {
|
|
245
|
+
try {
|
|
246
|
+
busTrace.add(
|
|
247
|
+
'tx',
|
|
248
|
+
framed,
|
|
249
|
+
`${attempt ? 'retransmit' : 'tx'} timeout=${timeoutMs}ms`
|
|
250
|
+
);
|
|
251
|
+
let frame = await bus.exchangeRaw(framed, timeoutMs, comm);
|
|
252
|
+
// keep reading while the ECU says "still working" -- bounded, so a
|
|
253
|
+
// stuck ECU still fails instead of hanging the screen
|
|
254
|
+
for (
|
|
255
|
+
let pending = 0;
|
|
256
|
+
pending < MAX_PENDING_POLLS && isResponsePending(frame, comm);
|
|
257
|
+
pending++
|
|
258
|
+
) {
|
|
259
|
+
// ParTimeoutNr78: how long the ECU may say "busy" between polls
|
|
260
|
+
frame = await bus.exchangeRaw(
|
|
261
|
+
null,
|
|
262
|
+
(comm && comm.timeoutNr78) || Math.max(timeoutMs, NR78_MIN_MS),
|
|
263
|
+
comm
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
busTrace.add('rx', frame, 'OK');
|
|
267
|
+
bus.lastResponseAt = Date.now();
|
|
268
|
+
return frame;
|
|
269
|
+
} catch (e) {
|
|
270
|
+
lastErr = e;
|
|
271
|
+
busTrace.add('err', null, `${e.ifh || ''} ${e.message}`.trim());
|
|
272
|
+
// Retransmit a GARBLED answer once (a K-line glitch), never a SILENT
|
|
273
|
+
// one: EDIABAS ships CommRepeats = 0, so a telegram nobody answers is
|
|
274
|
+
// sent exactly once and the bytecode moves on to its next protocol.
|
|
275
|
+
// Sending it twice doubled every probe step (ms450ds0's KWP2000* try
|
|
276
|
+
// before the BMW-FAST one that an MS45 actually answers).
|
|
277
|
+
// The error carries its IFH code; a garbled answer is IFH-0019
|
|
278
|
+
// (checksum / incomplete) or IFH-0003 (echo), silence is IFH-0009.
|
|
279
|
+
if (!(e && (e.ifh === 'IFH-0019' || e.ifh === 'IFH-0003'))) throw e;
|
|
280
|
+
// The reference retry is a PURE RETRANSMISSION: the same bytes with
|
|
281
|
+
// only the ParRegenTime wait, never a reinit. Re-arming the wake here
|
|
282
|
+
// made our retry a different, more disruptive operation than the one
|
|
283
|
+
// EDIABAS performs -- and on DS2 there is no wake to re-arm in the
|
|
284
|
+
// first place.
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
throw lastErr;
|
|
288
|
+
}
|