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,526 @@
|
|
|
1
|
+
// Exact-match English lookups for text the SGBD sends at runtime, plus the
|
|
2
|
+
// fault-code helpers. Pure lookup, no DOM, no word-level rewriting: a string
|
|
3
|
+
// is translated when a dictionary carries it whole, else it shows as BMW
|
|
4
|
+
// wrote it. Gated on Settings language (lang()==='orig' keeps German for
|
|
5
|
+
// EDIABAS-faithful mode). Captions from the .IPO scripts are NOT handled here
|
|
6
|
+
// -- each ECU carries its own map (data/inpa-i18n, irLabel in screens/ir.js).
|
|
7
|
+
/**
|
|
8
|
+
* Exact German -> English pairs for the fault texts the SGBD sends at runtime
|
|
9
|
+
* (symptom, fault-type, presence, warning lamp, readiness). Order matters
|
|
10
|
+
* where one phrase leads another.
|
|
11
|
+
* @type {Array<[string, string]>}
|
|
12
|
+
*/
|
|
13
|
+
const FAULT_PHRASES = [
|
|
14
|
+
// symptom (F_SYMPTOM_TEXT)
|
|
15
|
+
['kein Signal oder Wert', 'No signal or value'],
|
|
16
|
+
// fault-type (FA) texts as BMW composes them into F_PCODE_TEXT
|
|
17
|
+
// ("P1128 Motoroelniveausensor - kein Signal"): the part after " - "
|
|
18
|
+
['kein Signal', 'no signal'],
|
|
19
|
+
['System zu fett', 'system too rich'],
|
|
20
|
+
['System zu mager', 'system too lean'],
|
|
21
|
+
['Unterbrechung', 'open circuit'],
|
|
22
|
+
['Signal unplausibel', 'signal implausible'],
|
|
23
|
+
['Signal zu hoch', 'signal too high'],
|
|
24
|
+
['Signal zu niedrig', 'signal too low'],
|
|
25
|
+
['Signal oder Wert unterhalb Schwelle', 'Signal or value below threshold'],
|
|
26
|
+
['Signal oder Wert oberhalb Schwelle', 'Signal or value above threshold'],
|
|
27
|
+
['Signal oder Wert unplausibel', 'Signal or value implausible'],
|
|
28
|
+
['Kurzschluss nach Masse', 'Short circuit to ground'],
|
|
29
|
+
['Kurzschluss nach Plus', 'Short circuit to positive'],
|
|
30
|
+
['Kurzschluss nach Batterie', 'Short circuit to battery'],
|
|
31
|
+
['Leitungsunterbrechung', 'Open circuit'],
|
|
32
|
+
['mechanischer Fehler', 'Mechanical fault'],
|
|
33
|
+
['elektrischer Fehler', 'Electrical fault'],
|
|
34
|
+
// presence (F_VORHANDEN_TEXT)
|
|
35
|
+
[
|
|
36
|
+
'Fehler momentan nicht vorhanden, OBD-entprellt',
|
|
37
|
+
'Not currently present (OBD-confirmed)',
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
'Fehler momentan nicht vorhanden, nicht OBD-entprellt',
|
|
41
|
+
'Not currently present (not OBD-confirmed)',
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
'Fehler momentan vorhanden, noch nicht OBD-entprellt',
|
|
45
|
+
'Currently present (not yet OBD-confirmed)',
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
'Fehler momentan vorhanden, nicht OBD-entprellt',
|
|
49
|
+
'Currently present (not OBD-confirmed)',
|
|
50
|
+
],
|
|
51
|
+
[
|
|
52
|
+
'Fehler momentan vorhanden, OBD-entprellt',
|
|
53
|
+
'Currently present (OBD-confirmed)',
|
|
54
|
+
],
|
|
55
|
+
['Fehler momentan nicht vorhanden', 'Not currently present'],
|
|
56
|
+
['Fehler momentan vorhanden', 'Currently present'],
|
|
57
|
+
// warning lamp (F_WARNUNG_TEXT)
|
|
58
|
+
['Fehler verursacht kein Aufleuchten der Warnlampe (MIL)', 'No MIL'],
|
|
59
|
+
[
|
|
60
|
+
'Fehler wuerde das Aufleuchten der Warnlampe (MIL) verursachen',
|
|
61
|
+
'Would trigger MIL',
|
|
62
|
+
],
|
|
63
|
+
['Fehler verursacht das Aufleuchten der Warnlampe (MIL)', 'Triggers MIL'],
|
|
64
|
+
// readiness (F_READY_TEXT). The "noch nicht" variant must precede the
|
|
65
|
+
// plain "nicht" one: whichever is a leading match wins, and this phrase is
|
|
66
|
+
// "not YET met", not "not met".
|
|
67
|
+
['Testbedingungen noch nicht erfüllt', 'Test conditions not yet met'],
|
|
68
|
+
['Testbedingungen erfüllt', 'Test conditions met'],
|
|
69
|
+
['Testbedingungen nicht erfüllt', 'Test conditions not met'],
|
|
70
|
+
];
|
|
71
|
+
// Exact full-sentence translations for job-argument comments. Keyed on
|
|
72
|
+
// trimmed text.
|
|
73
|
+
/** Job-argument comment -> English, keyed on the trimmed German. @type {Object<string, string>} */
|
|
74
|
+
const ARG_PHRASES = {
|
|
75
|
+
'Als Argument wird ein vorgefuellter Binaerbuffer uebergeben':
|
|
76
|
+
'Pass a pre-built binary buffer as the argument',
|
|
77
|
+
'"ja" -> Funktionale Adresse 0xEF wird benutzt':
|
|
78
|
+
'"yes" -> use functional address 0xEF',
|
|
79
|
+
'0x????: Angabe eines einzelnen Fehlers': '0x????: a single fault',
|
|
80
|
+
'Zu übertragende Blocknummer (Zähler) bei langen Datenstreams':
|
|
81
|
+
'block number (counter) to transfer for long data streams',
|
|
82
|
+
"Wenn 'JA' wird der Messwertblock im SG gelöscht":
|
|
83
|
+
"'YES' clears the measurement block in the ECU",
|
|
84
|
+
'Abgleichdaten in folgendem Format':
|
|
85
|
+
'adjustment data in the following format',
|
|
86
|
+
'Auswahl eines Stellers (Pflicht)': 'select an actuator (required)',
|
|
87
|
+
'Auswahl eines Tests (Pflicht)': 'select a test (required)',
|
|
88
|
+
'Auswahl eines Tests': 'select a test',
|
|
89
|
+
'Nummer der auszulesenden Stützstellenkombination':
|
|
90
|
+
'number of the reference-point combination to read',
|
|
91
|
+
'Länge der folgenden Information wie die Antwort erhalten wird.':
|
|
92
|
+
'length of the following info on how the response is received.',
|
|
93
|
+
'ASCII-codiert Information wie die Antwort erhalten wird:':
|
|
94
|
+
'ASCII-coded info on how the response is received:',
|
|
95
|
+
'wird die Nummer des zu lesenden Fehlers im Fehlerspeicher uebergeben':
|
|
96
|
+
'pass the number of the fault to read from the fault memory',
|
|
97
|
+
'wird die Nummer des zu lesenden Fehlers uebergeben':
|
|
98
|
+
'pass the number of the fault to read',
|
|
99
|
+
'kleines x muss Charakter sein 0-9 oder A-Z':
|
|
100
|
+
'lowercase x must be a character 0-9 or A-Z',
|
|
101
|
+
'Dieser Job ist mit Passwort geschützt': 'This job is password protected',
|
|
102
|
+
'Wird nur bei Motoren mit 2 Bänken benötigt (M67TÜ)':
|
|
103
|
+
'only needed on engines with 2 banks (M67TU)',
|
|
104
|
+
'gibt einen absoluten Verstellwinkel an (0..180 Grd)':
|
|
105
|
+
'specifies an absolute adjustment angle (0..180 deg)',
|
|
106
|
+
'Dient nur zur Sicherheit, wird nicht': 'for safety only, is not',
|
|
107
|
+
'Länge des Individualisierungs Datenstream oder -streamstücks':
|
|
108
|
+
'length of the individualization data stream or stream piece',
|
|
109
|
+
'Individualdaten können via CAN oder MOST oder XY erreicht werden':
|
|
110
|
+
'individual data can be reached via CAN or MOST or XY',
|
|
111
|
+
'Individualdaten können via CAN oder MOST oder XY geschrieben werden':
|
|
112
|
+
'individual data can be written via CAN or MOST or XY',
|
|
113
|
+
'Übergabe im Format Messagenummern zB.: 00C0000D für N und V':
|
|
114
|
+
'pass as message numbers, e.g. 00C0000D for N and V',
|
|
115
|
+
'Einzelkerze rücksetzen: GLU1 ... GLU6 (... GLU8)':
|
|
116
|
+
'reset single glow plug: GLU1 ... GLU6 (... GLU8)',
|
|
117
|
+
'Wert der vorzugebenden Soll-Foerdermenge':
|
|
118
|
+
'value of the target delivery quantity to set',
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Exact-match phrase lookup for text that arrives at RUNTIME from the SGBD
|
|
122
|
+
// (fault symptom/location text, job-argument comments): the tables above,
|
|
123
|
+
// then the generated fault-location dictionary (faultdb.js). Text without an
|
|
124
|
+
// entry is returned as BMW wrote it -- there is no word-level rewriting.
|
|
125
|
+
// Captions the .IPO prints are translated by the per-ECU i18n map instead
|
|
126
|
+
// (irLabel, built from data/inpa-i18n/<ECU>.json).
|
|
127
|
+
/**
|
|
128
|
+
* Exact-match English for runtime SGBD text; the input unchanged when no
|
|
129
|
+
* dictionary carries it whole, or in Original mode.
|
|
130
|
+
* @param {string|number|null|undefined} text - Text from the ECU.
|
|
131
|
+
* @returns {string|number|null|undefined} The translation, or `text` as given.
|
|
132
|
+
*/
|
|
133
|
+
function phraseText(text) {
|
|
134
|
+
if (!text) return text;
|
|
135
|
+
if (typeof lang === 'function' && lang() === 'orig') return text;
|
|
136
|
+
// web VM results can be numbers (see bmwCode); only strings have .trim()
|
|
137
|
+
const trimmed = String(text).trim();
|
|
138
|
+
if (Object.prototype.hasOwnProperty.call(ARG_PHRASES, trimmed))
|
|
139
|
+
return ARG_PHRASES[trimmed];
|
|
140
|
+
// per-ECU fault-location text (SGBD FORTTEXTE tables, faultdb.js), variant-agnostic
|
|
141
|
+
if (typeof window !== 'undefined' && window.BMW_FAULT_PHRASES) {
|
|
142
|
+
const hit = window.BMW_FAULT_PHRASES[trimmed];
|
|
143
|
+
if (hit) return hit;
|
|
144
|
+
}
|
|
145
|
+
for (const [de, en] of FAULT_PHRASES) if (trimmed === de) return en;
|
|
146
|
+
return text;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Freeze-frame (Umwelt) field labels and enum values -- a pure lookup over the
|
|
150
|
+
// MAINTAINED dictionary window.BMW_ENV_TEXT (app/renderer/data/envmap.js,
|
|
151
|
+
// generated from tools/decompile/env_i18n_de.json). No word-munging: an entry
|
|
152
|
+
// is present with a curated English translation, or the German passes through
|
|
153
|
+
// unchanged. Keyed on the EXACT string, including any leading state code an
|
|
154
|
+
// enum value carries ("2 IS - Motor im Leerlauf"). Skipped in Original mode.
|
|
155
|
+
// Deliberately does NOT call phraseText -- that heuristic path never touches
|
|
156
|
+
// environment text.
|
|
157
|
+
/**
|
|
158
|
+
* English for a freeze-frame (Umwelt) field label or enum value, from the
|
|
159
|
+
* maintained dictionary; the German unchanged when absent or in Original mode.
|
|
160
|
+
* @param {string|null|undefined} text - The exact German string.
|
|
161
|
+
* @returns {string|null|undefined}
|
|
162
|
+
*/
|
|
163
|
+
function envLabel(text) {
|
|
164
|
+
if (lang() === 'orig' || !text) return text;
|
|
165
|
+
const s = String(text).trim();
|
|
166
|
+
const map = (typeof window !== 'undefined' && window.BMW_ENV_TEXT) || null;
|
|
167
|
+
if (map && Object.prototype.hasOwnProperty.call(map, s)) return map[s];
|
|
168
|
+
return text;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// BMW hex fault number (e.g. 27DA) -> OBD-II P-code. only real mappings; no
|
|
172
|
+
// fabricated codes.
|
|
173
|
+
/** Fallback BMW hex code -> SAE P-code, the few pinned by hand. @type {Object<string, string>} */
|
|
174
|
+
const PCODE_MAP = {
|
|
175
|
+
2761: 'P0410', // secondary air system
|
|
176
|
+
'27C3': 'P2563', // oil level sensor (thermal)
|
|
177
|
+
'27DA': 'P1734', // BSD bus / alternator comms (BMW-specific)
|
|
178
|
+
'27C2': 'P2562',
|
|
179
|
+
'27C4': 'P2564',
|
|
180
|
+
};
|
|
181
|
+
// Flatten an EDIABAS result value to the same text the native bridge produces
|
|
182
|
+
// (src/EdiabasMac/Diag.cs Format): byte arrays become dashed hex ("27-DA"),
|
|
183
|
+
// everything else its plain string. The web VM returns live typed values --
|
|
184
|
+
// `ergy` (binary) emits a byte Array, `ergi`/`ergb`/... emit numbers -- so
|
|
185
|
+
// screens that only ever saw the native path's strings funnel through here.
|
|
186
|
+
// An empty binary result ([]) is truthy but must read as "no code", which the
|
|
187
|
+
// join handles by yielding ''.
|
|
188
|
+
/**
|
|
189
|
+
* Flatten an EDIABAS result value to the native bridge's text form: byte
|
|
190
|
+
* arrays as dashed hex ("27-DA"), numbers as 4-digit hex, else the string.
|
|
191
|
+
* @param {any} v - A result value (string, number, byte array, or empty).
|
|
192
|
+
* @returns {string} The text, '' for empty/zero.
|
|
193
|
+
*/
|
|
194
|
+
function hexText(v) {
|
|
195
|
+
if (v == null || v === '') return '';
|
|
196
|
+
if (typeof v === 'string') return v;
|
|
197
|
+
// duck-typed, not `instanceof`: a typed array from another realm (worker,
|
|
198
|
+
// iframe) fails the instanceof check and would stringify as "39,218".
|
|
199
|
+
if (Array.isArray(v) || ArrayBuffer.isView(v))
|
|
200
|
+
return Array.from(v, (b) =>
|
|
201
|
+
(b & 0xff).toString(16).toUpperCase().padStart(2, '0')
|
|
202
|
+
).join('-');
|
|
203
|
+
if (typeof v === 'number')
|
|
204
|
+
return Number.isFinite(v) && v !== 0
|
|
205
|
+
? (v >>> 0).toString(16).toUpperCase().padStart(4, '0')
|
|
206
|
+
: '';
|
|
207
|
+
return String(v);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// BMW fault number = first token of F_ORT_TEXT ("27DA BSD-Generator" -> 27DA).
|
|
211
|
+
// GOTCHA: F_HEX_CODE is declared `binary` in every SGBD that has it. The native
|
|
212
|
+
// path flattens it first (Diag.Format -> BitConverter.ToString -> "27-DA", hence
|
|
213
|
+
// the dash strip below), but the web VM hands us the raw Uint8Array, which has
|
|
214
|
+
// no .replace. Most ECUs never reach here because their F_ORT_TEXT leads with
|
|
215
|
+
// the code; KOMBI's text is German-only, so it falls through to `hex`.
|
|
216
|
+
/**
|
|
217
|
+
* The BMW fault number for a fault: the leading hex token of F_ORT_TEXT, else
|
|
218
|
+
* the first two bytes of F_HEX_CODE.
|
|
219
|
+
* @param {string|null|undefined} loc - F_ORT_TEXT.
|
|
220
|
+
* @param {any} hex - F_HEX_CODE (string, number, or bytes).
|
|
221
|
+
* @returns {string|null} The uppercase code, or null when neither yields one.
|
|
222
|
+
*/
|
|
223
|
+
function bmwCode(loc, hex) {
|
|
224
|
+
const text = loc == null ? '' : String(loc);
|
|
225
|
+
if (text) {
|
|
226
|
+
const m = text.match(/^([0-9A-F]{3,5})\b/i);
|
|
227
|
+
if (m) return m[1].toUpperCase();
|
|
228
|
+
}
|
|
229
|
+
const h = hexText(hex);
|
|
230
|
+
return h ? h.replace(/-/g, '').slice(0, 4).toUpperCase() : null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// F_ORT_NR (BMW "Fehlerort") -> the LOCATION BYTE the SGBD FORTTEXTE table keys
|
|
234
|
+
// on (IHKA 0x1F, LWS 0x0B). For a 16-bit value (LWS 0x0B3F) the location is the
|
|
235
|
+
// HIGH byte; the low byte is symptom detail. EDIABAS gives it decimal ("2879");
|
|
236
|
+
// hex ("0x0B3F"/"1F") is accepted too. Returns two hex digits.
|
|
237
|
+
/**
|
|
238
|
+
* The FORTTEXTE location byte for an F_ORT_NR value, as two hex digits.
|
|
239
|
+
* @param {string|number|null|undefined} nr - F_ORT_NR (decimal or hex text).
|
|
240
|
+
* @returns {string|null} Two hex digits, the input as-is when unparseable, or null when empty.
|
|
241
|
+
*/
|
|
242
|
+
function ortNrCode(nr) {
|
|
243
|
+
if (nr == null) return null;
|
|
244
|
+
const s = String(nr).trim();
|
|
245
|
+
if (!s) return null;
|
|
246
|
+
let val = null;
|
|
247
|
+
let m =
|
|
248
|
+
s.match(/^0x([0-9A-Fa-f]+)$/) ||
|
|
249
|
+
s.match(/^([0-9A-Fa-f]*[A-Fa-f][0-9A-Fa-f]*)$/);
|
|
250
|
+
if (m) val = parseInt(m[1], 16);
|
|
251
|
+
else if (/^\d+$/.test(s)) val = parseInt(s, 10);
|
|
252
|
+
if (val == null || Number.isNaN(val)) return s; // unknown format: show as-is
|
|
253
|
+
const loc = val > 0xff ? (val >> 8) & 0xff : val; // high byte if 16-bit
|
|
254
|
+
return loc.toString(16).toUpperCase().padStart(2, '0');
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* The hand-pinned P-code for a fault, via {@link PCODE_MAP} only.
|
|
258
|
+
* @param {string|null|undefined} loc - F_ORT_TEXT.
|
|
259
|
+
* @param {any} hex - F_HEX_CODE.
|
|
260
|
+
* @returns {string|null}
|
|
261
|
+
*/
|
|
262
|
+
function pCode(loc, hex) {
|
|
263
|
+
const code = bmwCode(loc, hex);
|
|
264
|
+
return code && PCODE_MAP[code] ? PCODE_MAP[code] : null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// full 16-bit F_ORT_NR as 4-hex ("24002" -> "5DC2"), or null for single bytes.
|
|
268
|
+
// Lets the caller tell a real 2-byte DTC (DSC 5DC2, in the DB) from a text-scheme
|
|
269
|
+
// location+detail word (LWS 0B3F, not in the DB -> show the location byte).
|
|
270
|
+
/**
|
|
271
|
+
* The full 16-bit F_ORT_NR as four hex digits, or null for a single byte.
|
|
272
|
+
* @param {string|number|null|undefined} nr - F_ORT_NR (decimal or hex text).
|
|
273
|
+
* @returns {string|null}
|
|
274
|
+
*/
|
|
275
|
+
function ortNrFull(nr) {
|
|
276
|
+
if (nr == null) return null;
|
|
277
|
+
const s = String(nr).trim();
|
|
278
|
+
if (!s) return null;
|
|
279
|
+
let val = null;
|
|
280
|
+
let m =
|
|
281
|
+
s.match(/^0x([0-9A-Fa-f]+)$/) ||
|
|
282
|
+
s.match(/^([0-9A-Fa-f]*[A-Fa-f][0-9A-Fa-f]*)$/);
|
|
283
|
+
if (m) val = parseInt(m[1], 16);
|
|
284
|
+
else if (/^\d+$/.test(s)) val = parseInt(s, 10);
|
|
285
|
+
if (val == null || Number.isNaN(val) || val <= 0xff) return null;
|
|
286
|
+
return val.toString(16).toUpperCase().padStart(4, '0');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// P-code lookup backed by window.BMW_PCODES (BMW hex -> [SAE P-codes], primary
|
|
290
|
+
// first); PCODE_MAP is the fallback. Lazy-injected; fault screens warm it.
|
|
291
|
+
/** The in-flight pcodes.js load, so concurrent callers share one tag. @type {Promise<void>|null} */
|
|
292
|
+
let _pcodesPromise = null;
|
|
293
|
+
/**
|
|
294
|
+
* Load data/pcodes.js (sets window.BMW_PCODES) once; resolves even on failure.
|
|
295
|
+
* @returns {Promise<void>}
|
|
296
|
+
*/
|
|
297
|
+
function loadPcodes() {
|
|
298
|
+
if (typeof window === 'undefined') return Promise.resolve();
|
|
299
|
+
if (window.BMW_PCODES) return Promise.resolve();
|
|
300
|
+
if (_pcodesPromise) return _pcodesPromise;
|
|
301
|
+
_pcodesPromise = new Promise((resolve) => {
|
|
302
|
+
const s = document.createElement('script');
|
|
303
|
+
s.src = 'data/pcodes.js';
|
|
304
|
+
s.onload = () => resolve();
|
|
305
|
+
s.onerror = () => {
|
|
306
|
+
_pcodesPromise = null;
|
|
307
|
+
resolve();
|
|
308
|
+
};
|
|
309
|
+
document.head.appendChild(s);
|
|
310
|
+
});
|
|
311
|
+
return _pcodesPromise;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// rich ISTA fault metadata + service-info documents (decrypted DiagDocDb).
|
|
315
|
+
// Lazy-loaded: meta (14MB) warms with the fault screens; info (60MB) only on a
|
|
316
|
+
// fault detail panel.
|
|
317
|
+
//
|
|
318
|
+
// The large BMW-derived fault data (faultinfo/faultmeta/faultdb/faultindex) is
|
|
319
|
+
// NOT shipped in the repo -- it is BMW's copyrighted ISTA/EDIABAS text. It is
|
|
320
|
+
// hosted on the same Hugging Face dataset as the ETK data and loaded from there
|
|
321
|
+
// at runtime, with a local `data/` copy taking precedence when a build ships
|
|
322
|
+
// one (offline/desktop). Loading is a plain <script src> that sets a window
|
|
323
|
+
// global; cross-origin classic scripts load fine from HF.
|
|
324
|
+
/** Where the BMW-derived fault tables are hosted when a build ships none. */
|
|
325
|
+
const FAULT_HF_BASE =
|
|
326
|
+
'https://huggingface.co/datasets/CraigFf/bmweb-etk/resolve/main/faults/';
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Shared state of one lazy-loaded data script.
|
|
330
|
+
* @typedef {Object} LazyScriptHolder
|
|
331
|
+
* @property {Promise<void>|null} [p] - The in-flight load, if any.
|
|
332
|
+
*/
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Make a loader for a data script that sets a window global: local `src`
|
|
336
|
+
* first, then the hosted copy; resolves once either loads (or both fail).
|
|
337
|
+
* @param {string} src - Relative path (e.g. 'data/faultmeta.js').
|
|
338
|
+
* @param {string} ready - The window global the script sets.
|
|
339
|
+
* @param {LazyScriptHolder} holder - Where the in-flight promise is kept.
|
|
340
|
+
* @returns {() => Promise<void>} The loader.
|
|
341
|
+
*/
|
|
342
|
+
function _lazyScript(src, ready, holder) {
|
|
343
|
+
return function () {
|
|
344
|
+
if (typeof window === 'undefined') return Promise.resolve();
|
|
345
|
+
if (window[ready]) return Promise.resolve();
|
|
346
|
+
if (holder.p) return holder.p;
|
|
347
|
+
// basename for the HF fallback (src is like 'data/faultinfo.js')
|
|
348
|
+
const base = typeof WEB_BASE === 'string' ? WEB_BASE : '';
|
|
349
|
+
const file = src.split('/').pop();
|
|
350
|
+
const urls = [`${base}/${src}`, `${FAULT_HF_BASE}${file}`];
|
|
351
|
+
holder.p = new Promise((resolve) => {
|
|
352
|
+
let i = 0;
|
|
353
|
+
const tryNext = () => {
|
|
354
|
+
if (i >= urls.length) {
|
|
355
|
+
holder.p = null;
|
|
356
|
+
resolve();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const s = document.createElement('script');
|
|
360
|
+
s.src = urls[i++];
|
|
361
|
+
s.onload = () => resolve();
|
|
362
|
+
s.onerror = () => {
|
|
363
|
+
s.remove();
|
|
364
|
+
tryNext();
|
|
365
|
+
}; // local missing -> HF
|
|
366
|
+
document.head.appendChild(s);
|
|
367
|
+
};
|
|
368
|
+
tryNext();
|
|
369
|
+
});
|
|
370
|
+
return holder.p;
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
/** @type {LazyScriptHolder} */
|
|
374
|
+
const _metaHolder = {},
|
|
375
|
+
_infoHolder = {},
|
|
376
|
+
_codingHolder = {},
|
|
377
|
+
_datenHolder = {};
|
|
378
|
+
/** Load the ISTA fault metadata (window.BMW_FAULT_META, 14 MB). @type {() => Promise<void>} */
|
|
379
|
+
const loadFaultMeta = _lazyScript(
|
|
380
|
+
'data/faultmeta.js',
|
|
381
|
+
'BMW_FAULT_META',
|
|
382
|
+
_metaHolder
|
|
383
|
+
);
|
|
384
|
+
/** Load the service-info documents (window.BMW_FAULT_INFO, 60 MB). @type {() => Promise<void>} */
|
|
385
|
+
const loadFaultInfo = _lazyScript(
|
|
386
|
+
'data/faultinfo.js',
|
|
387
|
+
'BMW_FAULT_INFO',
|
|
388
|
+
_infoHolder
|
|
389
|
+
);
|
|
390
|
+
// what an ECU's coding values MEAN, for the SGBDs that name their own
|
|
391
|
+
/** Load the SGBD coding-value meanings (window.BMW_CODING_MAP). @type {() => Promise<void>} */
|
|
392
|
+
const loadCodingMap = _lazyScript(
|
|
393
|
+
'data/codingmap.js',
|
|
394
|
+
'BMW_CODING_MAP',
|
|
395
|
+
_codingHolder
|
|
396
|
+
);
|
|
397
|
+
// ...and from BMW's DATEN, for ECUs whose SGBD says nothing
|
|
398
|
+
/** Load BMW's DATEN coding map (window.BMW_DATEN_MAP). @type {() => Promise<void>} */
|
|
399
|
+
const loadDatenMap = _lazyScript(
|
|
400
|
+
'data/datenmap.js',
|
|
401
|
+
'BMW_DATEN_MAP',
|
|
402
|
+
_datenHolder
|
|
403
|
+
);
|
|
404
|
+
// which ECUs a car actually has: SGET rows + their AUFTRAGSAUSDRUCK predicate
|
|
405
|
+
/** @type {LazyScriptHolder} */
|
|
406
|
+
const _sgetHolder = {};
|
|
407
|
+
/** Load the SGET fitment rows (window.BMW_SGET). @type {() => Promise<void>} */
|
|
408
|
+
const loadSget = _lazyScript('data/sget.js', 'BMW_SGET', _sgetHolder);
|
|
409
|
+
// SGFAM (which ECU holds the vehicle order / the central coding key), AT
|
|
410
|
+
// (SA number -> equipment keywords) and ZST (ZCS key bits -> keywords)
|
|
411
|
+
/** @type {LazyScriptHolder} */
|
|
412
|
+
const _tablesHolder = {};
|
|
413
|
+
/** Load the SGFAM / AT / ZST tables (window.BMW_TABLES). @type {() => Promise<void>} */
|
|
414
|
+
const loadTables = _lazyScript('data/tables.js', 'BMW_TABLES', _tablesHolder);
|
|
415
|
+
// SA option numbers -> English names, dated (BMW reused the numbers)
|
|
416
|
+
/** @type {LazyScriptHolder} */
|
|
417
|
+
const _saNamesHolder = {};
|
|
418
|
+
/** Load the dated SA option names (window.BMW_SA_NAMES). @type {() => Promise<void>} */
|
|
419
|
+
const loadSaNames = _lazyScript(
|
|
420
|
+
'data/sanames.js',
|
|
421
|
+
'BMW_SA_NAMES',
|
|
422
|
+
_saNamesHolder
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
// per-ECU-variant records for a hex code: [{sgbd, name, info?}], or []. `info`
|
|
426
|
+
// indexes into BMW_FAULT_INFO[hex].
|
|
427
|
+
/**
|
|
428
|
+
* The per-ECU-variant records ISTA carries for a BMW hex code.
|
|
429
|
+
* @param {string|null|undefined} code - The hex code (with or without 0x).
|
|
430
|
+
* @returns {Array<{sgbd: string, name: string, info?: number}>} Empty when unknown or not loaded.
|
|
431
|
+
*/
|
|
432
|
+
function variantsForHex(code) {
|
|
433
|
+
if (!code) return [];
|
|
434
|
+
const c = String(code).replace(/^0x/i, '').toUpperCase();
|
|
435
|
+
const m = (typeof window !== 'undefined' && window.BMW_FAULT_META) || null;
|
|
436
|
+
return (m && m[c] && m[c].variants) || [];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// the service-info document for a hex code + variant info-index, or null.
|
|
440
|
+
/**
|
|
441
|
+
* The service-info document for a hex code and a variant's info index.
|
|
442
|
+
* @param {string|null|undefined} code - The hex code.
|
|
443
|
+
* @param {number|string|null|undefined} infoIdx - The variant's `info` index.
|
|
444
|
+
* @returns {any|null} The document, or null when absent or not loaded.
|
|
445
|
+
*/
|
|
446
|
+
function faultInfoFor(code, infoIdx) {
|
|
447
|
+
if (code == null || infoIdx == null) return null;
|
|
448
|
+
const c = String(code).replace(/^0x/i, '').toUpperCase();
|
|
449
|
+
const db = (typeof window !== 'undefined' && window.BMW_FAULT_INFO) || null;
|
|
450
|
+
const bucket = db && db[c];
|
|
451
|
+
return (bucket && bucket[String(infoIdx)]) || null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// all SAE P-codes for a BMW hex code ("27C3" -> ["P0456"], primary first), or [].
|
|
455
|
+
// Prefers ISTA meta, then the pcodes map, then the fallback.
|
|
456
|
+
/**
|
|
457
|
+
* Every SAE P-code for a BMW hex code, primary first.
|
|
458
|
+
* @param {string|null|undefined} code - The hex code.
|
|
459
|
+
* @returns {string[]} Empty when none is known.
|
|
460
|
+
*/
|
|
461
|
+
function pcodesForHex(code) {
|
|
462
|
+
if (!code) return [];
|
|
463
|
+
const c = String(code).replace(/^0x/i, '').toUpperCase();
|
|
464
|
+
const m = (typeof window !== 'undefined' && window.BMW_FAULT_META) || null;
|
|
465
|
+
if (m && m[c] && m[c].pcodes) return m[c].pcodes;
|
|
466
|
+
const db = (typeof window !== 'undefined' && window.BMW_PCODES) || null;
|
|
467
|
+
if (db && db[c]) return db[c];
|
|
468
|
+
if (PCODE_MAP[c]) return [PCODE_MAP[c]];
|
|
469
|
+
return [];
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// UNAMBIGUOUS offline P-code for a hex code, or null. Many BMW codes map to
|
|
473
|
+
// SEVERAL SAE P-codes gated by ECU variant; guessing the first misleads, so
|
|
474
|
+
// offline we return one ONLY when the code has exactly one. A live read's own
|
|
475
|
+
// F_PCODE_STRING is exact and always preferred.
|
|
476
|
+
/**
|
|
477
|
+
* The one unambiguous offline P-code for a hex code, or null when the code
|
|
478
|
+
* maps to several.
|
|
479
|
+
* @param {string|null|undefined} code - The hex code.
|
|
480
|
+
* @param {string} [sgbd] - The reading ECU (reserved for a variant-scoped lookup).
|
|
481
|
+
* @returns {string|null}
|
|
482
|
+
*/
|
|
483
|
+
function pcodeForHexSgbd(code, sgbd) {
|
|
484
|
+
if (!code) return null;
|
|
485
|
+
const list = pcodesForHex(code);
|
|
486
|
+
return list.length === 1 ? list[0] : null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// primary P-code for a bare BMW hex code ("27C3" -> "P0456"), or null.
|
|
490
|
+
/**
|
|
491
|
+
* The primary P-code for a BMW hex code.
|
|
492
|
+
* @param {string|null|undefined} code - The hex code.
|
|
493
|
+
* @returns {string|null}
|
|
494
|
+
*/
|
|
495
|
+
function pcodeForHex(code) {
|
|
496
|
+
const list = pcodesForHex(code);
|
|
497
|
+
return list.length ? list[0] : null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// reverse lookup for search: "P0456" -> "27C3", null if unknown. Built once from
|
|
501
|
+
// the richest source available (BMW_FAULT_META, then BMW_PCODES, then fallback).
|
|
502
|
+
/** P-code -> hex reverse map, and the source object it was built from. */
|
|
503
|
+
let _PCODE_REV = null,
|
|
504
|
+
_PCODE_REV_SRC = null;
|
|
505
|
+
/**
|
|
506
|
+
* Reverse lookup for search: an SAE P-code to the BMW hex code that carries it.
|
|
507
|
+
* @param {string} p - The P-code (any case).
|
|
508
|
+
* @returns {string|null} The hex code, or null when unknown.
|
|
509
|
+
*/
|
|
510
|
+
function hexForPcode(p) {
|
|
511
|
+
const meta = (typeof window !== 'undefined' && window.BMW_FAULT_META) || null;
|
|
512
|
+
const db = (typeof window !== 'undefined' && window.BMW_PCODES) || null;
|
|
513
|
+
const src = meta || db || PCODE_MAP;
|
|
514
|
+
if (_PCODE_REV_SRC !== src) {
|
|
515
|
+
_PCODE_REV = {};
|
|
516
|
+
_PCODE_REV_SRC = src;
|
|
517
|
+
for (const [h, v] of Object.entries(src)) {
|
|
518
|
+
const list = meta ? v.pcodes || [] : Array.isArray(v) ? v : [v];
|
|
519
|
+
for (const pc of list) {
|
|
520
|
+
const k = String(pc).toUpperCase();
|
|
521
|
+
if (!(k in _PCODE_REV)) _PCODE_REV[k] = h;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return _PCODE_REV[String(p).toUpperCase()] || null;
|
|
526
|
+
}
|