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,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Concept-aware framing: which checksum, which length rule and which
|
|
3
|
+
* port settings each EDIABAS concept uses.
|
|
4
|
+
*
|
|
5
|
+
* The SGBD's telegram EXCLUDES its trailing checksum -- appending it is the
|
|
6
|
+
* interface's job (learned the hard way: a request one byte short is silently
|
|
7
|
+
* discarded by a real ECU). Framing per concept, from each job's xsetpar
|
|
8
|
+
* CommParameter {concept, baud, timeout}, verified against data/sim-captures:
|
|
9
|
+
*
|
|
10
|
+
* 1/5/6 DS2 family 9600 8E1 XOR checksum answer[1] = total length
|
|
11
|
+
* 0x10D KWP2000* 9600 8E1 sum8 answer[3] + 5 = total
|
|
12
|
+
* 0x10F BMW-FAST 115200 8N1 sum8 header short/long form
|
|
13
|
+
* 0x110 D-CAN 115200 8N1 sum8 (CAN cable, BMW-FAST serial)
|
|
14
|
+
* 0x10C ISO 9141 10400 8N1 sum8 after a 5-baud slow init
|
|
15
|
+
*
|
|
16
|
+
* The wire is described by the SGBD's set_communication_pars and nothing
|
|
17
|
+
* else. A telegram with no CommParameter behind it has no baud, no checksum
|
|
18
|
+
* rule and no length rule -- EDIABAS refuses it (IFH-0056), so do we, rather
|
|
19
|
+
* than assume BMW-FAST and sign a DS2 request with the wrong checksum.
|
|
20
|
+
*/
|
|
21
|
+
/* exported KDCAN, UTILITY_NOMINAL_MV, KLINE_DEFAULT_BAUD, conceptOf, isDs2, isIso9141, isKline, isBmwFast, assertReachable, ISO9141_INIT_ADDR, ISO9141_BAUD, ifhError, withChecksum, frameTotal, verifyChecksum, portConfig */
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The wire parameters an SGBD declares with xsetpar (Best2Vm.decodeCommParams),
|
|
25
|
+
* carried beside every telegram so the transport can frame, sign and pace it.
|
|
26
|
+
* @typedef {object} CommParams
|
|
27
|
+
* @property {number} concept - EDIABAS concept: 1/5/6 DS2, 0x10C ISO 9141,
|
|
28
|
+
* 0x10D KWP2000*, 0x10F BMW-FAST, 0x110 D-CAN.
|
|
29
|
+
* @property {number} [baud] - The rate the SGBD names (CommParameter[1]).
|
|
30
|
+
* @property {number|null} [timeout] - ParTimeoutStd: ms the ECU may take to
|
|
31
|
+
* START answering.
|
|
32
|
+
* @property {number|null} [regen] - ParRegenTime: ms of quiet the ECU needs
|
|
33
|
+
* after its answer before the next request.
|
|
34
|
+
* @property {number|null} [telEnd] - ParTimeoutTelEnd (inter-byte silence).
|
|
35
|
+
* @property {number|null} [timeoutNr78] - ParTimeoutNr78: ms the ECU may say
|
|
36
|
+
* "busy" (7F xx 78) between polls.
|
|
37
|
+
* @property {number[]} [answerLen] - The DS2 xawlen rule: [offset|length, add].
|
|
38
|
+
* @property {number} [waitMs] - A `wait` the SGBD issued before this telegram.
|
|
39
|
+
* @property {number[]} [params] - The raw CommParameter words.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Web Serial / native port settings.
|
|
44
|
+
* @typedef {object} PortConfig
|
|
45
|
+
* @property {number} baudRate - Bits per second.
|
|
46
|
+
* @property {number} dataBits - Always 8 here.
|
|
47
|
+
* @property {number} stopBits - Always 1 here.
|
|
48
|
+
* @property {'none'|'even'} parity - 8N1 for BMW-FAST/ISO 9141, 8E1 on the K line.
|
|
49
|
+
* @property {boolean} [dtr] - The IDLE level of DTR for this concept: high on
|
|
50
|
+
* BMW-FAST/D-CAN, low on every K-line concept (see portConfig).
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* K+DCAN over Web Serial. Default until a job's SGBD declares its own via
|
|
55
|
+
* xsetpar: BMW-FAST 115200 8N1 (the USB cable's default concept), DTR high.
|
|
56
|
+
* @type {PortConfig}
|
|
57
|
+
*/
|
|
58
|
+
const KDCAN = {
|
|
59
|
+
baudRate: 115200,
|
|
60
|
+
dataBits: 8,
|
|
61
|
+
stopBits: 1,
|
|
62
|
+
parity: 'none',
|
|
63
|
+
dtr: true,
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* UTILITY.PRG's own number, read out of its BEST/2 bytecode: UTILITY /
|
|
67
|
+
* INTERFACE substitutes 12000 mV when the interface cannot measure at all,
|
|
68
|
+
* and STATUS_UBATT / STATUS_ZUENDUNG both compare the adapter's sense reading
|
|
69
|
+
* against 10000 mV (`comp L0, 10000` / `jae`), so the nominal value reads as
|
|
70
|
+
* "on".
|
|
71
|
+
*/
|
|
72
|
+
const UTILITY_NOMINAL_MV = 12000;
|
|
73
|
+
/** The K-line rate EDIABAS falls back to when an SGBD names none (8E1). */
|
|
74
|
+
const KLINE_DEFAULT_BAUD = 9600;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The concept a telegram rides on, or IFH-0056 when the SGBD never set one.
|
|
78
|
+
* @param {CommParams|null|undefined} comm - The telegram's wire parameters.
|
|
79
|
+
* @returns {number} The concept id.
|
|
80
|
+
* @throws {Error} IFH-0056 when no CommParameter precedes the telegram.
|
|
81
|
+
*/
|
|
82
|
+
const conceptOf = (comm) => {
|
|
83
|
+
if (!(comm && comm.concept)) {
|
|
84
|
+
throw ifhError('IFH-0056', 'no CommParameter set before the telegram');
|
|
85
|
+
}
|
|
86
|
+
return comm.concept;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Is this a DS2-family concept (1, 5, 6)?
|
|
90
|
+
* @param {number} c - The concept id.
|
|
91
|
+
* @returns {boolean}
|
|
92
|
+
*/
|
|
93
|
+
const isDs2 = (c) => c === 1 || c === 5 || c === 6;
|
|
94
|
+
/**
|
|
95
|
+
* ISO 9141-2: the module sleeps until a 5-baud address byte wakes it.
|
|
96
|
+
* @param {number} c - The concept id.
|
|
97
|
+
* @returns {boolean}
|
|
98
|
+
*/
|
|
99
|
+
const isIso9141 = (c) => c === 0x10c;
|
|
100
|
+
/**
|
|
101
|
+
* Concepts that ride the K line and therefore need DTR as transmit enable.
|
|
102
|
+
* DS2 and KWP2000* are K-line; BMW-FAST/D-CAN (0x10F/0x110) are not.
|
|
103
|
+
* @param {number} c - The concept id.
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
106
|
+
const isKline = (c) => isDs2(c) || c === 0x10d;
|
|
107
|
+
/**
|
|
108
|
+
* BMW-FAST (0x10F) and D-CAN (0x110): the two concepts on which the reference
|
|
109
|
+
* interface holds DTR HIGH for the whole session on a plain FTDI/COM cable
|
|
110
|
+
* (`stateDtr = HasAdapterEcho`, set in exactly these two cases and nowhere
|
|
111
|
+
* else). Every other concept idles with DTR low.
|
|
112
|
+
* @param {number} c - The concept id.
|
|
113
|
+
* @returns {boolean}
|
|
114
|
+
*/
|
|
115
|
+
const isBmwFast = (c) => c === 0x10f || c === 0x110;
|
|
116
|
+
/**
|
|
117
|
+
* Concepts that only the old BMW ADS interface can drive: concept 1, concept
|
|
118
|
+
* 2 (ISO 9141 / KWP1281 5-baud) and concept 3. They need the L line on OBD
|
|
119
|
+
* pin 20 and ADS-style line control; the reference interface refuses all
|
|
120
|
+
* three on any echoing adapter ("only with ADS adapter", IFH-0006), and a
|
|
121
|
+
* K+DCAN cable is one. The 76 SGBDs that declare them are the early E31,
|
|
122
|
+
* E34, E36, E38 and E39 modules (Motronic 1.7 to 5.2.1, DDE 2.1, ZF EGS,
|
|
123
|
+
* the first ABS and IHKA). Refusing here, with the reason, beats signing a
|
|
124
|
+
* request the cable can never deliver and reporting "no answer".
|
|
125
|
+
*/
|
|
126
|
+
const ADS_ONLY_CONCEPTS = new Set([1, 2, 3]);
|
|
127
|
+
/**
|
|
128
|
+
* Refuse a concept this cable cannot physically reach, before anything is
|
|
129
|
+
* configured or written.
|
|
130
|
+
* @param {CommParams|null|undefined} comm - The telegram's wire parameters.
|
|
131
|
+
* @throws {Error} IFH-0006 for an ADS-only concept.
|
|
132
|
+
*/
|
|
133
|
+
function assertReachable(comm) {
|
|
134
|
+
const c = conceptOf(comm);
|
|
135
|
+
if (ADS_ONLY_CONCEPTS.has(c)) {
|
|
136
|
+
throw ifhError(
|
|
137
|
+
'IFH-0006',
|
|
138
|
+
`concept ${c} needs the ADS interface (L line, OBD pin 20); ` +
|
|
139
|
+
'a K+DCAN cable cannot reach this module'
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Verified on a real E46 (M54 / MS45): the DME answers the ISO 9141 generic
|
|
145
|
+
* tester address at 10400 baud, NOT its own KWP address at 9600. Sending a
|
|
146
|
+
* job to an unwoken module gets silence, which surfaced as IFH-0009 "no
|
|
147
|
+
* response" and looked for all the world like a wiring fault.
|
|
148
|
+
*/
|
|
149
|
+
const ISO9141_INIT_ADDR = 0x33;
|
|
150
|
+
/**
|
|
151
|
+
* The rate an E46 K-line module actually answers on after the slow or fast
|
|
152
|
+
* init -- 9600 (either parity) stays silent even after a successful wake.
|
|
153
|
+
*/
|
|
154
|
+
const ISO9141_BAUD = 10400;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Build an interface failure carrying its EDIABAS IFH identity, so
|
|
158
|
+
* explainError and a user comparing to real INPA see the same code (IFH-0009
|
|
159
|
+
* no answer, -0003 line/echo, -0019 truncated). SGBD-level ERROR_ECU_* stay
|
|
160
|
+
* the jobs' business.
|
|
161
|
+
* @param {string} code - The IFH code, e.g. 'IFH-0009'.
|
|
162
|
+
* @param {string} message - What went wrong, in plain words.
|
|
163
|
+
* @returns {Error & {ifh: string}} The error, tagged with `ifh`.
|
|
164
|
+
*/
|
|
165
|
+
function ifhError(code, message) {
|
|
166
|
+
const e = new Error(`${code}: ${message}`);
|
|
167
|
+
e.ifh = code;
|
|
168
|
+
return e;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Which EDIABAS transmit function a concept runs, and therefore its checksum
|
|
173
|
+
* and its answer-length rule (the reference interface's `switch (concept)`
|
|
174
|
+
* that sets ParTransmitFunc):
|
|
175
|
+
* 1, 5, 6 TransDs2 XOR length from xawlen (TelLengthDs2)
|
|
176
|
+
* 0x10D KWP2000* TransKwp2000S XOR byte[3] + 4 (TelLengthKwp2000S)
|
|
177
|
+
* 0x10B/0x10C/0x10F TransKwp2000Bmw/ sum TelLengthBmwFast
|
|
178
|
+
* 0x110 D-CAN TransBmwFast
|
|
179
|
+
* Nothing here looks at the first byte of a frame to decide -- 0xB8 is just
|
|
180
|
+
* the tester address KWP2000* and BMW-FAST both use.
|
|
181
|
+
*/
|
|
182
|
+
const XOR_CONCEPTS = new Set([1, 5, 6, 0x10d]);
|
|
183
|
+
/**
|
|
184
|
+
* The reference interface's per-concept CommAnswerLen seed, used when an
|
|
185
|
+
* SGBD never issues xawlen: [-o, k] = byte at offset o plus k.
|
|
186
|
+
* @type {Record<number, [number, number]>}
|
|
187
|
+
*/
|
|
188
|
+
const DS_ANSWER_LEN_DEFAULT = { 1: [-2, 0], 5: [-1, 0], 6: [-1, 0] };
|
|
189
|
+
/** Concepts whose checksum is the 8-bit sum (see XOR_CONCEPTS). */
|
|
190
|
+
const SUM_CONCEPTS = new Set([0x10b, 0x10c, 0x10f, 0x110]);
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The checksum a concept puts after its telegram.
|
|
194
|
+
* @param {ArrayLike<number>} bytes - The telegram without its checksum.
|
|
195
|
+
* @param {number} c - The concept id.
|
|
196
|
+
* @returns {number} XOR or sum8 of the bytes.
|
|
197
|
+
* @throws {Error} IFH-0018 for a concept this interface cannot sign.
|
|
198
|
+
*/
|
|
199
|
+
function checksumOf(bytes, c) {
|
|
200
|
+
let sum = 0;
|
|
201
|
+
if (XOR_CONCEPTS.has(c)) {
|
|
202
|
+
for (const b of bytes) sum ^= b;
|
|
203
|
+
return sum;
|
|
204
|
+
}
|
|
205
|
+
if (SUM_CONCEPTS.has(c)) {
|
|
206
|
+
for (const b of bytes) sum = (sum + b) & 0xff;
|
|
207
|
+
return sum;
|
|
208
|
+
}
|
|
209
|
+
throw ifhError(
|
|
210
|
+
'IFH-0018',
|
|
211
|
+
`concept 0x${c.toString(16)} is not supported on this interface`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Append the concept's checksum to an outgoing telegram.
|
|
217
|
+
* @param {ArrayLike<number>} out - The request as the SGBD built it.
|
|
218
|
+
* @param {CommParams} comm - Its wire parameters.
|
|
219
|
+
* @returns {number[]} The framed request, ready for the wire.
|
|
220
|
+
*/
|
|
221
|
+
function withChecksum(out, comm) {
|
|
222
|
+
return [...out, checksumOf(out, conceptOf(comm))];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Total frame length INCLUDING the checksum byte, or null while too few
|
|
227
|
+
* bytes are in to know. Each rule is its EDIABAS TelLength* + 1.
|
|
228
|
+
* @param {number[]} buf - The bytes received so far.
|
|
229
|
+
* @param {CommParams} comm - The wire parameters of the request.
|
|
230
|
+
* @returns {number|null} The frame length, or null while undecidable.
|
|
231
|
+
* @throws {Error} IFH-0018 for an unsupported concept or a DS2 job with no
|
|
232
|
+
* answer-length rule at all.
|
|
233
|
+
*/
|
|
234
|
+
function frameTotal(buf, comm) {
|
|
235
|
+
const c = conceptOf(comm);
|
|
236
|
+
if (XOR_CONCEPTS.has(c) && c !== 0x10d) {
|
|
237
|
+
// DS2: the rule the SGBD declared with xawlen (TelLengthDs2), or, when it
|
|
238
|
+
// never did, EDIABAS's own default for the concept -- every xsetpar seeds
|
|
239
|
+
// CommAnswerLen per concept (DS1/DS2 = [-1, 0] "byte 1 is the total
|
|
240
|
+
// length", concept 1 = [-2, 0]) and xawlen only overrides it. The E46
|
|
241
|
+
// steering-angle group d_0057 is 99 ops and never calls xawlen; refusing
|
|
242
|
+
// its exchange here made the resolver log "bus-silent" for a sensor that
|
|
243
|
+
// answers every time, and the scan printed "not installed".
|
|
244
|
+
let al = comm && comm.answerLen;
|
|
245
|
+
if (!al || !al.length) al = DS_ANSWER_LEN_DEFAULT[c];
|
|
246
|
+
if (!al) {
|
|
247
|
+
throw ifhError(
|
|
248
|
+
'IFH-0018',
|
|
249
|
+
'DS2 answer length not set by the SGBD (xawlen)'
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
if (al[0] > 0) return al[0];
|
|
253
|
+
const off = -al[0];
|
|
254
|
+
return buf.length > off ? buf[off] + (al[1] || 0) : null;
|
|
255
|
+
}
|
|
256
|
+
// KWP2000* (0x10d): these answers always carry the B8 F1 12 address header,
|
|
257
|
+
// so byte 0 (0xB8) is NOT a BMW-FAST length byte -- the length is byte 3,
|
|
258
|
+
// and the frame is byte3 + 4 header + 1 checksum. Verified against a real
|
|
259
|
+
// MS45 EDIABAS trace: byte3 0x1F -> 36 bytes, 0x41 -> 70, 0xFF -> 260. (Do
|
|
260
|
+
// NOT apply TelLengthBmwFast's byte0-&-0x3F rule here: 0xB8 & 0x3F = 56
|
|
261
|
+
// would force every answer to 60 bytes.)
|
|
262
|
+
if (c === 0x10d) return buf.length >= 4 ? buf[3] + 4 + 1 : null;
|
|
263
|
+
if (SUM_CONCEPTS.has(c)) {
|
|
264
|
+
// TelLengthBmwFast -- length in the low 6 bits of byte 0, with byte 3 (or
|
|
265
|
+
// bytes 4-5) as the long-form fallback.
|
|
266
|
+
if (!buf.length) return null;
|
|
267
|
+
const short = buf[0] & 0x3f;
|
|
268
|
+
if (short) return short + 3 + 1;
|
|
269
|
+
if (buf.length < 4) return null;
|
|
270
|
+
if (buf[3] === 0)
|
|
271
|
+
return buf.length >= 6 ? (buf[4] << 8) + buf[5] + 6 + 1 : null;
|
|
272
|
+
return buf[3] + 4 + 1;
|
|
273
|
+
}
|
|
274
|
+
throw ifhError(
|
|
275
|
+
'IFH-0018',
|
|
276
|
+
`concept 0x${c.toString(16)} is not supported on this interface`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Check a complete frame's trailing checksum against its concept's rule.
|
|
282
|
+
* @param {number[]} frame - The whole answer including its checksum byte.
|
|
283
|
+
* @param {CommParams} comm - The wire parameters of the request.
|
|
284
|
+
* @throws {Error} IFH-0019 on a mismatch.
|
|
285
|
+
*/
|
|
286
|
+
function verifyChecksum(frame, comm) {
|
|
287
|
+
const want = checksumOf(frame.slice(0, -1), conceptOf(comm));
|
|
288
|
+
if (want !== frame[frame.length - 1]) {
|
|
289
|
+
throw ifhError('IFH-0019', 'answer checksum mismatch');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The port settings a concept's telegrams travel on.
|
|
295
|
+
* @param {CommParams} comm - The telegram's wire parameters.
|
|
296
|
+
* @returns {PortConfig} Baud, bits and parity for the port.
|
|
297
|
+
*/
|
|
298
|
+
function portConfig(comm) {
|
|
299
|
+
const c = conceptOf(comm);
|
|
300
|
+
if (isIso9141(c)) {
|
|
301
|
+
// 8N1 after the handshake -- the init itself is bit-banged, not framed.
|
|
302
|
+
// DTR idles low: the reference raises the idle level for BMW-FAST and
|
|
303
|
+
// D-CAN only, never for 0x10C.
|
|
304
|
+
return {
|
|
305
|
+
baudRate: (comm && comm.baud) || ISO9141_BAUD,
|
|
306
|
+
dataBits: 8,
|
|
307
|
+
stopBits: 1,
|
|
308
|
+
parity: 'none',
|
|
309
|
+
dtr: false,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
if (isKline(c)) {
|
|
313
|
+
// DS2 and KWP2000* are 8E1 at the rate the SGBD names (concept 6 in the
|
|
314
|
+
// reference interface: parity = Even, baudRate = CommParameter[1]). An
|
|
315
|
+
// earlier 10400 8N1 override here came from an ISO 9141 experiment and
|
|
316
|
+
// does not belong on these concepts. DTR idles LOW: held high it keeps
|
|
317
|
+
// the cable transmitting and the answer is lost (real E46, MS45).
|
|
318
|
+
return {
|
|
319
|
+
baudRate: (comm && comm.baud) || KLINE_DEFAULT_BAUD,
|
|
320
|
+
dataBits: 8,
|
|
321
|
+
stopBits: 1,
|
|
322
|
+
parity: 'even',
|
|
323
|
+
dtr: false,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
// BMW-FAST / D-CAN: 115200 8N1 with DTR held HIGH for the session, the way
|
|
327
|
+
// the reference drives a plain cable (stateDtr = HasAdapterEcho). Dropping
|
|
328
|
+
// it after a K-line probe and leaving it there is the one thing this
|
|
329
|
+
// transport did differently from the reference on an E60/E65/E90 bus.
|
|
330
|
+
return { ...KDCAN, dtr: isBmwFast(c) };
|
|
331
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The shim's composition root: lock the bus, publish the public
|
|
3
|
+
* surface on window, and install the fetch shim. Loads last, after every
|
|
4
|
+
* other core/webshim/ piece.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Forget everything known about the car on the other end of the cable:
|
|
9
|
+
* the EDIABAS sessions (without ENDE -- the wire is already going away) and
|
|
10
|
+
* the resolved variants. Run before the bus disconnects.
|
|
11
|
+
*/
|
|
12
|
+
function forgetCar() {
|
|
13
|
+
forgetSessions();
|
|
14
|
+
forgetResolvedVariants();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
lockBus(webBus, forgetCar);
|
|
18
|
+
|
|
19
|
+
if (typeof window !== 'undefined') {
|
|
20
|
+
window.webBus = webBus;
|
|
21
|
+
window.installWebShim = installWebShim;
|
|
22
|
+
// group -> variant resolution, for the sweep screen: which SGBD answers
|
|
23
|
+
// at this diagnostic address? (lowercased SGBD name, or null)
|
|
24
|
+
window.webResolveVariant = webResolveVariant;
|
|
25
|
+
window.webResolveVariantLast = webResolveVariantLast;
|
|
26
|
+
// The explicitly-confirmed coding write path (the UI's "code this module"
|
|
27
|
+
// action). Gated on opts.confirmed and its own re-read proof; see coding.js.
|
|
28
|
+
window.webWriteCoding = webWriteCoding;
|
|
29
|
+
installWebShim();
|
|
30
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file EDIABAS's session model and the job runner: run a job the way the
|
|
3
|
+
* server's /run endpoint did, but in the VM, over the live bus.
|
|
4
|
+
*/
|
|
5
|
+
/* exported sessionFor, switchSession, forgetSessions, newTally, driveJobOverBus, webRunJob */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* How many telegram exchanges a single job may need before it is declared
|
|
9
|
+
* stuck (one VM replay pass per exchange).
|
|
10
|
+
*/
|
|
11
|
+
const MAX_JOB_PASSES = 64;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The VM's send callback: synchronous, returns a memoised answer or throws
|
|
15
|
+
* the needAnswer sentinel to unwind the pass.
|
|
16
|
+
* @callback SendFn
|
|
17
|
+
* @param {ArrayLike<number>} out - The request without its checksum.
|
|
18
|
+
* @param {CommParams} comm - Its wire parameters.
|
|
19
|
+
* @returns {number[]} The answer bytes.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One SGBD's session state.
|
|
24
|
+
* @typedef {object} Session
|
|
25
|
+
* @property {Map<string, any>} shared - shmset data carried across jobs.
|
|
26
|
+
* @property {boolean} inited - Has INITIALISIERUNG run.
|
|
27
|
+
* @property {CommParams|null} comm - The wire parameters xsetpar left behind.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What a VM job produced once every telegram it needed had been exchanged.
|
|
32
|
+
* @typedef {object} DriveResult
|
|
33
|
+
* @property {object[]} sets - The result sets.
|
|
34
|
+
* @property {Best2Vm} vm - The VM instance that completed the job.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* How the wire answered a job's telegrams, kept OUTSIDE the drive so the
|
|
39
|
+
* counts survive a pass that throws (the resolver reports them beside the
|
|
40
|
+
* error).
|
|
41
|
+
* @typedef {object} AnswerTally
|
|
42
|
+
* @property {number} empty - Telegrams the wire could not answer.
|
|
43
|
+
* @property {number} real - Telegrams that came back with bytes.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A fresh, zeroed answer tally.
|
|
48
|
+
* @returns {AnswerTally}
|
|
49
|
+
*/
|
|
50
|
+
function newTally() {
|
|
51
|
+
return { empty: 0, real: 0 };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* EDIABAS's session model, which a fresh-VM-per-job does not have:
|
|
56
|
+
* INITIALISIERUNG runs ONCE when an SGBD is loaded, shared data (shmset)
|
|
57
|
+
* persists across that SGBD's jobs, and ENDE runs when it is unloaded.
|
|
58
|
+
* MS450 hands its AIF block from init to later jobs exactly this way.
|
|
59
|
+
* The session also carries COMM: xsetpar lives in INITIALISIERUNG, so a
|
|
60
|
+
* later job's fresh VM never executes it -- without the carry, every
|
|
61
|
+
* ordinary job transmitted with default BMW-FAST framing and every K-line
|
|
62
|
+
* module got 115200 8N1 line noise.
|
|
63
|
+
* Keyed by SGBD; switching ECUs ends the previous session.
|
|
64
|
+
* @type {Map<string, Session>}
|
|
65
|
+
*/
|
|
66
|
+
const sessions = new Map();
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The session for an SGBD, created empty on first sight.
|
|
70
|
+
* @param {string} sgbd - The SGBD name (any case).
|
|
71
|
+
* @returns {Session}
|
|
72
|
+
*/
|
|
73
|
+
function sessionFor(sgbd) {
|
|
74
|
+
const key = String(sgbd).toLowerCase();
|
|
75
|
+
let s = sessions.get(key);
|
|
76
|
+
if (!s) {
|
|
77
|
+
s = { shared: new Map(), inited: false, comm: null };
|
|
78
|
+
sessions.set(key, s);
|
|
79
|
+
}
|
|
80
|
+
return s;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Run ENDE for a session being dropped. Fire-and-forget: the answer does
|
|
85
|
+
* not matter, but the ECU is entitled to the notification.
|
|
86
|
+
* @param {string} sgbd - The SGBD whose session ends.
|
|
87
|
+
*/
|
|
88
|
+
async function endSession(sgbd) {
|
|
89
|
+
const key = String(sgbd).toLowerCase();
|
|
90
|
+
const s = sessions.get(key);
|
|
91
|
+
if (!s || !s.inited) {
|
|
92
|
+
sessions.delete(key);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
sessions.delete(key);
|
|
96
|
+
try {
|
|
97
|
+
const code = await webFetchJson(`data/job-code/${key}.json`);
|
|
98
|
+
if (code && code.jobs && code.jobs.ENDE !== undefined) {
|
|
99
|
+
await webRunJob(sgbd, 'ENDE', null, {
|
|
100
|
+
noInit: true,
|
|
101
|
+
shared: s.shared,
|
|
102
|
+
comm: s.comm,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
/* the session is over either way */
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The currently-loaded SGBD (lowercased). EDIABAS holds one at a time;
|
|
112
|
+
* switching ends the old session so its ENDE runs while the bus is still up.
|
|
113
|
+
* @type {string|null}
|
|
114
|
+
*/
|
|
115
|
+
let loadedSgbd = null;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Make `sgbd` the loaded SGBD, ending the previous one's session first.
|
|
119
|
+
*
|
|
120
|
+
* NOTE this must NOT run between an SGBD's session init and its jobs: those
|
|
121
|
+
* are the same session. Clearing there let the job's BMW-FAST concept
|
|
122
|
+
* become the session concept, the port reopened at 115200, and the wake
|
|
123
|
+
* performed at 10400 was undone before the telegram went out.
|
|
124
|
+
* @param {string} sgbd - The SGBD about to run.
|
|
125
|
+
*/
|
|
126
|
+
async function switchSession(sgbd) {
|
|
127
|
+
const key = String(sgbd).toLowerCase();
|
|
128
|
+
if (loadedSgbd === key) return;
|
|
129
|
+
const prev = loadedSgbd;
|
|
130
|
+
loadedSgbd = key;
|
|
131
|
+
// Only a REAL switch clears the wire state. The early return above already
|
|
132
|
+
// skipped the no-op case, so reaching here means a different ECU -- which
|
|
133
|
+
// may live on a different wire, so the remembered concept and the wake
|
|
134
|
+
// that went with it do not carry over.
|
|
135
|
+
if (prev) {
|
|
136
|
+
await endSession(prev);
|
|
137
|
+
webBus.sessionConcept = null;
|
|
138
|
+
webBus.inited = null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Forget every session and the loaded SGBD, without running ENDE. Called
|
|
144
|
+
* when the cable goes away: the wire is already gone.
|
|
145
|
+
*/
|
|
146
|
+
function forgetSessions() {
|
|
147
|
+
sessions.clear();
|
|
148
|
+
loadedSgbd = null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The sentinel a send() throws to unwind a VM pass whose answer is not yet
|
|
153
|
+
* known. bestvm's INITIALISIERUNG handling rethrows it instead of swallowing
|
|
154
|
+
* it -- an init whose telegrams were never fetched used to "succeed" having
|
|
155
|
+
* sent nothing.
|
|
156
|
+
* @returns {Error & {needAnswer: true}}
|
|
157
|
+
*/
|
|
158
|
+
function needAnswerError() {
|
|
159
|
+
const need = new Error('__need_answer__');
|
|
160
|
+
need.needAnswer = true;
|
|
161
|
+
return need;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Did this wire error mean the telegram got nothing usable -- silence or a
|
|
166
|
+
* bad echo -- as opposed to a lying wire?
|
|
167
|
+
*
|
|
168
|
+
* A SILENT ECU IS AN ANSWER OF ZERO BYTES, NOT A DEAD JOB. The VM sets
|
|
169
|
+
* f.zero from the answer's length and the bytecode branches on it. Tracing
|
|
170
|
+
* EDIABAS proved the SGBD relies on exactly that: ms450ds0's INITIALISIERUNG
|
|
171
|
+
* holds two telegrams as constants, sends "82 12 F1 1A 80", gets IFH-0009 --
|
|
172
|
+
* and carries on to send "B8 12 F1 02 1A 80", which the ECU answers. Throwing
|
|
173
|
+
* here killed the job on the first telegram, so the second was never tried
|
|
174
|
+
* and a perfectly reachable DME looked silent.
|
|
175
|
+
*
|
|
176
|
+
* IFH-0009 (silence) and IFH-0003 (the echo did not come back cleanly) both
|
|
177
|
+
* mean THIS TELEGRAM GOT NOTHING USABLE. The SGBD's fallback branches on the
|
|
178
|
+
* answer's length via `slen`, so both must arrive as an empty answer or the
|
|
179
|
+
* bytecode never reaches its second telegram. A half-duplex K line genuinely
|
|
180
|
+
* produces both: an ECU that ignores a framing answers with silence, and the
|
|
181
|
+
* stray leftover byte that follows makes the NEXT echo compare fail.
|
|
182
|
+
* Treating only the timeout as "no answer" left the app dying on whichever
|
|
183
|
+
* of the two happened to occur first.
|
|
184
|
+
*
|
|
185
|
+
* Only those two are swallowed. A damaged frame, a checksum failure or a
|
|
186
|
+
* bus-level fault still throws: those mean the wire is lying, and continuing
|
|
187
|
+
* would decode garbage.
|
|
188
|
+
* @param {any} err - What webBus.exchange threw.
|
|
189
|
+
* @returns {boolean} True for IFH-0009 / IFH-0003.
|
|
190
|
+
*/
|
|
191
|
+
function isNoUsableAnswer(err) {
|
|
192
|
+
return !!(err && (err.ifh === 'IFH-0009' || err.ifh === 'IFH-0003'));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Drive one VM job over the bus in passes.
|
|
197
|
+
*
|
|
198
|
+
* send() is synchronous but the wire is async, so the VM runs in passes:
|
|
199
|
+
* each pass runs until a send whose answer we lack, which we fetch, memoise,
|
|
200
|
+
* then retry from the top (the VM is deterministic, so replay is safe). The
|
|
201
|
+
* memo is keyed by request bytes AND occurrence index: a job that sends the
|
|
202
|
+
* same telegram twice (clear-then-verify) must get the second answer, not a
|
|
203
|
+
* replay. One clock serves all passes -- a time that ticked between passes
|
|
204
|
+
* would change the request bytes, miss the memo, and re-transmit an
|
|
205
|
+
* already-sent telegram.
|
|
206
|
+
*
|
|
207
|
+
* REMEMBER THAT THE WIRE FAILED. The empty answer is what the SGBD's
|
|
208
|
+
* fallback needs, but a job whose telegrams ALL came back empty has not read
|
|
209
|
+
* the car -- it has read nothing. The tally lets the caller say so: without
|
|
210
|
+
* it the fault screen rendered "No stored faults / clean fault memory" for a
|
|
211
|
+
* DME holding nine real faults, which is the worst thing a diagnostic tool
|
|
212
|
+
* can say.
|
|
213
|
+
* @param {(send: SendFn, now: Date) => Best2Vm} buildVm - Builds a fresh VM
|
|
214
|
+
* for one pass around the given send callback and clock.
|
|
215
|
+
* @param {string} job - The job name.
|
|
216
|
+
* @param {string} arg - The job's argument string.
|
|
217
|
+
* @param {AnswerTally} tally - Updated in place as telegrams are answered.
|
|
218
|
+
* @returns {Promise<DriveResult>} The result sets and the finished VM.
|
|
219
|
+
* @throws {Error} A VM error from the job itself, a wire error that is not
|
|
220
|
+
* silence, or 'did not settle' after MAX_JOB_PASSES exchanges.
|
|
221
|
+
*/
|
|
222
|
+
async function driveJobOverBus(buildVm, job, arg, tally) {
|
|
223
|
+
const answers = new Map();
|
|
224
|
+
const jobNow = new Date();
|
|
225
|
+
for (let attempt = 0; attempt < MAX_JOB_PASSES; attempt++) {
|
|
226
|
+
let missing = null;
|
|
227
|
+
let sendSeq = 0;
|
|
228
|
+
const send = (out, comm) => {
|
|
229
|
+
const key = `${sendSeq++}:${Array.from(out)}`;
|
|
230
|
+
if (answers.has(key)) return answers.get(key);
|
|
231
|
+
// Carry the wire parameters along with the request: the exchange
|
|
232
|
+
// below needs the concept to frame, checksum and pace it. Unwind this
|
|
233
|
+
// pass: nothing sensible to return, and continuing would decode
|
|
234
|
+
// garbage.
|
|
235
|
+
missing = { key, out: Array.from(out), comm };
|
|
236
|
+
throw needAnswerError();
|
|
237
|
+
};
|
|
238
|
+
const vm = buildVm(send, jobNow);
|
|
239
|
+
try {
|
|
240
|
+
const sets = vm.run(job, arg);
|
|
241
|
+
return { sets, vm };
|
|
242
|
+
} catch (e) {
|
|
243
|
+
// Only the needAnswer sentinel may turn into a wire exchange. A real
|
|
244
|
+
// VM error thrown in the same pass must surface as itself, not be
|
|
245
|
+
// recycled into "did not settle".
|
|
246
|
+
if (!missing || !e.needAnswer) throw e;
|
|
247
|
+
let answer;
|
|
248
|
+
try {
|
|
249
|
+
answer = await webBus.exchange(missing.out, missing.comm);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
if (!isNoUsableAnswer(err)) throw err;
|
|
252
|
+
answer = [];
|
|
253
|
+
tally.empty++;
|
|
254
|
+
}
|
|
255
|
+
if (answer && answer.length) tally.real++;
|
|
256
|
+
answers.set(missing.key, answer);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
throw new Error(
|
|
260
|
+
`job did not settle after ${MAX_JOB_PASSES} telegram exchanges`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Run a job on an SGBD over the live bus, inside its EDIABAS session.
|
|
266
|
+
* @param {string} sgbd - The SGBD name.
|
|
267
|
+
* @param {string} job - The job name.
|
|
268
|
+
* @param {string|null} arg - The argument string (null for none).
|
|
269
|
+
* @param {{shared?: Map<string, any>, comm?: CommParams|null, noInit?: boolean}} [opts] -
|
|
270
|
+
* A detached session to run in (endSession's ENDE uses this).
|
|
271
|
+
* @returns {Promise<{sets: object[]}>} The job's result sets.
|
|
272
|
+
* @throws {Error} When no job code is shipped, when the ECU answered nothing
|
|
273
|
+
* at all (IFH-0009), or whatever the job or wire threw.
|
|
274
|
+
*/
|
|
275
|
+
async function webRunJob(sgbd, job, arg, opts = {}) {
|
|
276
|
+
const code = await webFetchJson(`data/job-code/${sgbd.toLowerCase()}.json`);
|
|
277
|
+
if (!code) throw new Error(`no job code shipped for ${sgbd}`);
|
|
278
|
+
const sharedTables = await loadSharedTables();
|
|
279
|
+
const tables =
|
|
280
|
+
(await webFetchJson(`data/sgbd-tables/${sgbd.toLowerCase()}.json`)) || {};
|
|
281
|
+
const session = opts.shared
|
|
282
|
+
? { shared: opts.shared, inited: true, comm: opts.comm || null }
|
|
283
|
+
: sessionFor(sgbd);
|
|
284
|
+
const argText = arg == null ? '' : String(arg);
|
|
285
|
+
const tally = newTally();
|
|
286
|
+
|
|
287
|
+
const { sets, vm } = await driveJobOverBus(
|
|
288
|
+
(send, now) =>
|
|
289
|
+
new Best2Vm(code, {
|
|
290
|
+
tables,
|
|
291
|
+
extTables: sharedTables,
|
|
292
|
+
args: argText,
|
|
293
|
+
// Writes permitted -- see the note on Best2Vm.allowWrites. This is
|
|
294
|
+
// the main job runner, so it is what lets an actuator test reach the
|
|
295
|
+
// wire.
|
|
296
|
+
allowWrites: true,
|
|
297
|
+
shared: session.shared,
|
|
298
|
+
inited: session.inited,
|
|
299
|
+
comm: session.comm,
|
|
300
|
+
now,
|
|
301
|
+
send,
|
|
302
|
+
}),
|
|
303
|
+
job,
|
|
304
|
+
argText,
|
|
305
|
+
tally
|
|
306
|
+
);
|
|
307
|
+
session.inited = true;
|
|
308
|
+
session.comm = vm.comm || session.comm;
|
|
309
|
+
// A job that transmitted and was answered by NOTHING did not read the
|
|
310
|
+
// car. Saying so is the only honest outcome: the alternative is a
|
|
311
|
+
// "clean fault memory" that is really a dead wire.
|
|
312
|
+
if (tally.empty && !tally.real) {
|
|
313
|
+
throw ifhError(
|
|
314
|
+
'IFH-0009',
|
|
315
|
+
'the ECU did not answer any telegram in this job'
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
return { sets };
|
|
319
|
+
}
|