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.
Files changed (72) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +351 -0
  3. package/dist/bmweb.js +2790 -0
  4. package/package.json +53 -0
  5. package/runtime/core/bestvm/codec.js +285 -0
  6. package/runtime/core/bestvm/environment.js +116 -0
  7. package/runtime/core/bestvm/executor.js +1483 -0
  8. package/runtime/core/bestvm/index.js +52 -0
  9. package/runtime/core/bestvm/machine.js +491 -0
  10. package/runtime/core/bestvm/operands.js +356 -0
  11. package/runtime/core/bestvm/registers.js +152 -0
  12. package/runtime/core/bestvm/write-guard.js +111 -0
  13. package/runtime/core/ipofile/compile.js +364 -0
  14. package/runtime/core/ipofile/decls.js +187 -0
  15. package/runtime/core/ipofile/emit.js +708 -0
  16. package/runtime/core/ipofile/exec.js +164 -0
  17. package/runtime/core/ipofile/lex.js +243 -0
  18. package/runtime/core/ipofile/parse.js +550 -0
  19. package/runtime/core/ipofile/pool.js +404 -0
  20. package/runtime/core/ipofile/walk.js +431 -0
  21. package/runtime/core/ipovm/builtin-helpers.js +182 -0
  22. package/runtime/core/ipovm/builtins-api.js +610 -0
  23. package/runtime/core/ipovm/builtins-screen.js +493 -0
  24. package/runtime/core/ipovm/builtins-table.js +166 -0
  25. package/runtime/core/ipovm/builtins-text.js +166 -0
  26. package/runtime/core/ipovm/emissions.js +138 -0
  27. package/runtime/core/ipovm/hosts.js +191 -0
  28. package/runtime/core/ipovm/operators.js +229 -0
  29. package/runtime/core/ipovm/structures.js +250 -0
  30. package/runtime/core/ipovm/suspensions.js +241 -0
  31. package/runtime/core/ipovm/tape.js +206 -0
  32. package/runtime/core/ipovm/values.js +241 -0
  33. package/runtime/core/ipovm/vm.js +1166 -0
  34. package/runtime/core/translate.js +526 -0
  35. package/runtime/core/webshim/api-router.js +592 -0
  36. package/runtime/core/webshim/bus.js +95 -0
  37. package/runtime/core/webshim/coding.js +82 -0
  38. package/runtime/core/webshim/data-fetch.js +66 -0
  39. package/runtime/core/webshim/exchange.js +288 -0
  40. package/runtime/core/webshim/framing.js +331 -0
  41. package/runtime/core/webshim/install.js +30 -0
  42. package/runtime/core/webshim/job-runner.js +319 -0
  43. package/runtime/core/webshim/native-bus.js +108 -0
  44. package/runtime/core/webshim/timers.js +82 -0
  45. package/runtime/core/webshim/trace.js +205 -0
  46. package/runtime/core/webshim/transport-base.js +128 -0
  47. package/runtime/core/webshim/variant-resolver.js +249 -0
  48. package/runtime/core/webshim/web-serial-bus.js +734 -0
  49. package/runtime/home/bmweb-home.ips +76 -0
  50. package/runtime/home/bmweb.h +26 -0
  51. package/runtime/screens/activations.js +258 -0
  52. package/runtime/screens/garage/diff.js +331 -0
  53. package/runtime/screens/garage/share.js +276 -0
  54. package/runtime/screens/garage/store.js +547 -0
  55. package/runtime/screens/ipo-runtime/cells.js +176 -0
  56. package/runtime/screens/ipo-runtime/dialogs.js +254 -0
  57. package/runtime/screens/ipo-runtime/home.js +358 -0
  58. package/runtime/screens/ipo-runtime/open.js +393 -0
  59. package/runtime/screens/ipo-runtime/paint-grid.js +106 -0
  60. package/runtime/screens/ipo-runtime/paint-modern.js +424 -0
  61. package/runtime/screens/ipo-runtime/print.js +281 -0
  62. package/runtime/screens/ipo-runtime/program.js +1337 -0
  63. package/runtime/screens/ipo-runtime/protocol.js +464 -0
  64. package/runtime/screens/ipo-runtime/script-scan.js +225 -0
  65. package/runtime/screens/ipo-runtime/translate-sets.js +130 -0
  66. package/runtime/screens/ipo-runtime/ui.js +249 -0
  67. package/runtime/screens/ipo-runtime/wire-policy.js +113 -0
  68. package/runtime/screens/ir.js +324 -0
  69. package/runtime/screens/search/data.js +153 -0
  70. package/runtime/screens/search/match.js +285 -0
  71. package/runtime/screens/search/open.js +66 -0
  72. 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
+ }