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,547 @@
1
+ /**
2
+ * @file The Garage's persistence: the cars the user owns and the whole-car
3
+ * scans kept against each. Pure data, no DOM -- the screens draw what these
4
+ * return, and the node harness drives them directly.
5
+ *
6
+ * Two keys sit beside the app's other settings (core/core/settings.js writes
7
+ * the whole map to localStorage on every set):
8
+ *
9
+ * bmweb.garage.cars GarageCar[] the saved vehicles
10
+ * bmweb.garage.scans {[carId]: GarageScan[]} newest first, capped
11
+ *
12
+ * Scans are kept per car rather than in one flat list so dropping a car drops
13
+ * its history in one write, and so the cap is per car -- a car scanned every
14
+ * week never pushes another car's history out.
15
+ */
16
+
17
+ /* exported
18
+ GARAGE_CARS_KEY, GARAGE_SCANS_KEY, GARAGE_SCAN_CAP,
19
+ garageCars, garageCar, garageAddCar, garageUpdateCar, garageRemoveCar,
20
+ garageCarLabel, garageFindByVin, garageScans, garageScan, garageAddScan,
21
+ garageRemoveScan, garageScanSummary, garageCarFromHit, garageNewId,
22
+ garageScanFor, garageScanTargetClear, garageScanTarget,
23
+ garagePrevSameKind, garageScanPair */
24
+
25
+ /** Settings key holding the saved cars. */
26
+ const GARAGE_CARS_KEY = 'bmweb.garage.cars';
27
+
28
+ /** Settings key holding the per-car scan history. */
29
+ const GARAGE_SCANS_KEY = 'bmweb.garage.scans';
30
+
31
+ /**
32
+ * How many scans are kept per car. The oldest is dropped past this. Reports
33
+ * carry every fault of every module, so the whole history has to stay inside
34
+ * localStorage's few megabytes.
35
+ */
36
+ const GARAGE_SCAN_CAP = 50;
37
+
38
+ /** Characters at the end of a BMW VIN that identify the car (production number). */
39
+ const GARAGE_VIN_TAIL = 7;
40
+
41
+ /**
42
+ * One car in the garage.
43
+ * @typedef {object} GarageCar
44
+ * @property {string} id - stable local id; scans are keyed by it
45
+ * @property {string} [vin] - full VIN or the 7-character production number, upper-case
46
+ * @property {string} chassis - chassis code (E46, ...)
47
+ * @property {string} label - what the lists show
48
+ * @property {string} added - ISO timestamp
49
+ * @property {string} [notes] - the owner's own note
50
+ * @property {string} [model] - model name, when a VIN decoded it
51
+ * @property {string} [body] - body code
52
+ * @property {string} [motor] - engine code
53
+ * @property {string} [prod] - build date, YYYYMM(DD)
54
+ */
55
+
56
+ /**
57
+ * One stored whole-car read. `report` is the live view's own report object
58
+ * (screens/ipo-runtime/protocol.js builds it), kept as-is so the history view
59
+ * can hand it straight back to that file's renderer.
60
+ * @typedef {object} GarageScan
61
+ * @property {string} id - stable local id
62
+ * @property {string} carId - the car it belongs to
63
+ * @property {string} at - ISO timestamp of the read
64
+ * @property {'faults'|'ident'} kind
65
+ * @property {string} [chassis] - the chassis it was read on
66
+ * @property {object} report - the IpoProtocolReport as stored
67
+ * @property {string[]} [lines] - INPA's own protocol text
68
+ * @property {GarageScanSummary} summary - counts, so a list needs no report walk
69
+ */
70
+
71
+ /**
72
+ * The counts a history row shows.
73
+ * @typedef {object} GarageScanSummary
74
+ * @property {number} modules - modules that answered
75
+ * @property {number} withFaults - of those, how many had stored faults
76
+ * @property {number} faults - total stored faults
77
+ * @property {number} silent - addresses that did not answer
78
+ */
79
+
80
+ /**
81
+ * Read a JSON value from Settings, guarding the shape the caller expects.
82
+ * A hand-edited or half-written value must not take the screen down, so
83
+ * anything of the wrong shape reads as empty.
84
+ * @param {string} key - the Settings key
85
+ * @param {*} fallback - value when unset or the wrong shape
86
+ * @param {(v: *) => boolean} shapeOk - shape guard
87
+ * @returns {*}
88
+ */
89
+ function garageRead(key, fallback, shapeOk) {
90
+ if (typeof Settings !== 'object' || !Settings || !Settings.get)
91
+ return fallback;
92
+ const v = Settings.get(key, fallback);
93
+ return shapeOk(v) ? v : fallback;
94
+ }
95
+
96
+ /**
97
+ * Write a JSON value to Settings.
98
+ * @param {string} key - the Settings key
99
+ * @param {*} val - the value
100
+ * @returns {void}
101
+ */
102
+ function garageWrite(key, val) {
103
+ if (typeof Settings === 'object' && Settings && Settings.set)
104
+ Settings.set(key, val);
105
+ }
106
+
107
+ /**
108
+ * A short, collision-free local id. Time-prefixed so ids sort by creation
109
+ * even when two are minted in the same millisecond.
110
+ * @returns {string}
111
+ */
112
+ function garageNewId() {
113
+ return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
114
+ }
115
+
116
+ /**
117
+ * Every saved car, in the order they were added to the front (newest first).
118
+ * @returns {GarageCar[]}
119
+ */
120
+ function garageCars() {
121
+ return garageRead(GARAGE_CARS_KEY, [], Array.isArray).slice();
122
+ }
123
+
124
+ /**
125
+ * One saved car by id.
126
+ * @param {string} id - the car id
127
+ * @returns {GarageCar|null}
128
+ */
129
+ function garageCar(id) {
130
+ return garageCars().find((c) => c.id === id) || null;
131
+ }
132
+
133
+ /**
134
+ * A car's display name: the owner's label, else what the VIN decoded to,
135
+ * else the bare chassis.
136
+ * @param {GarageCar} car - the car
137
+ * @returns {string}
138
+ */
139
+ function garageCarLabel(car) {
140
+ if (!car) return '';
141
+ if (car.label) return car.label;
142
+ const disp =
143
+ typeof dispChassis === 'function'
144
+ ? dispChassis(car.chassis)
145
+ : car.chassis || '';
146
+ return [disp, car.model].filter(Boolean).join(' ') || car.vin || 'Vehicle';
147
+ }
148
+
149
+ /**
150
+ * The car a VIN belongs to. BMW VINs are matched on the 7-character
151
+ * production number at the end, so a car saved by its short code still
152
+ * matches the full VIN a module reports (and the other way round).
153
+ * @param {string} vin - a VIN or production number
154
+ * @returns {GarageCar|null}
155
+ */
156
+ function garageFindByVin(vin) {
157
+ const v = String(vin || '')
158
+ .trim()
159
+ .toUpperCase()
160
+ .replace(/\s/g, '');
161
+ if (v.length < GARAGE_VIN_TAIL) return null;
162
+ const tail = v.slice(-GARAGE_VIN_TAIL);
163
+ return (
164
+ garageCars().find((c) => {
165
+ const cv = String(c.vin || '')
166
+ .trim()
167
+ .toUpperCase();
168
+ return (
169
+ cv.length >= GARAGE_VIN_TAIL && cv.slice(-GARAGE_VIN_TAIL) === tail
170
+ );
171
+ }) || null
172
+ );
173
+ }
174
+
175
+ /**
176
+ * Add a car, or fold it into the one already holding that VIN. Returns the
177
+ * stored car either way, so a caller can navigate straight to it.
178
+ * @param {Partial<GarageCar>} entry - at least a chassis, usually a VIN too
179
+ * @returns {GarageCar} the stored car
180
+ */
181
+ function garageAddCar(entry) {
182
+ const e = entry || {};
183
+ const vin = String(e.vin || '')
184
+ .trim()
185
+ .toUpperCase()
186
+ .replace(/\s/g, '');
187
+ const existing = vin ? garageFindByVin(vin) : null;
188
+ if (existing) {
189
+ // same car re-saved: keep its id (and its scans) and refresh what decoded.
190
+ // The longer VIN wins -- a car first saved by the 7-character production
191
+ // number gains the full number when a read reports it, and never loses
192
+ // the full one to a later short match.
193
+ const prev = String(existing.vin || '');
194
+ return garageUpdateCar(existing.id, {
195
+ chassis: e.chassis || existing.chassis,
196
+ label: e.label || existing.label,
197
+ model: e.model || existing.model,
198
+ body: e.body || existing.body,
199
+ motor: e.motor || existing.motor,
200
+ prod: e.prod || existing.prod,
201
+ vin: vin.length >= prev.length ? vin || prev : prev,
202
+ });
203
+ }
204
+ /** @type {GarageCar} */
205
+ const car = {
206
+ id: garageNewId(),
207
+ chassis: String(e.chassis || '').toUpperCase(),
208
+ label: String(e.label || ''),
209
+ added: new Date().toISOString(),
210
+ };
211
+ if (vin) car.vin = vin;
212
+ if (e.notes) car.notes = String(e.notes);
213
+ for (const k of ['model', 'body', 'motor', 'prod'])
214
+ if (e[k]) car[k] = String(e[k]);
215
+ if (!car.label) car.label = garageCarLabel(car);
216
+ garageWrite(GARAGE_CARS_KEY, [car, ...garageCars()]);
217
+ return car;
218
+ }
219
+
220
+ /**
221
+ * Change a saved car's fields.
222
+ * @param {string} id - the car id
223
+ * @param {Partial<GarageCar>} patch - fields to set; undefined values are ignored
224
+ * @returns {GarageCar|null} the updated car
225
+ */
226
+ function garageUpdateCar(id, patch) {
227
+ const list = garageCars();
228
+ const i = list.findIndex((c) => c.id === id);
229
+ if (i < 0) return null;
230
+ const next = { ...list[i] };
231
+ for (const [k, v] of Object.entries(patch || {}))
232
+ if (v !== undefined && k !== 'id') next[k] = v;
233
+ list[i] = next;
234
+ garageWrite(GARAGE_CARS_KEY, list);
235
+ return next;
236
+ }
237
+
238
+ /**
239
+ * Drop a car and every scan kept against it.
240
+ * @param {string} id - the car id
241
+ * @returns {void}
242
+ */
243
+ function garageRemoveCar(id) {
244
+ garageWrite(
245
+ GARAGE_CARS_KEY,
246
+ garageCars().filter((c) => c.id !== id)
247
+ );
248
+ const all = garageAllScans();
249
+ if (id in all) {
250
+ delete all[id];
251
+ garageWrite(GARAGE_SCANS_KEY, all);
252
+ }
253
+ }
254
+
255
+ /**
256
+ * The whole scan map, car id -> scans.
257
+ * @returns {Object<string, GarageScan[]>}
258
+ */
259
+ function garageAllScans() {
260
+ return garageRead(
261
+ GARAGE_SCANS_KEY,
262
+ {},
263
+ (v) => !!v && typeof v === 'object' && !Array.isArray(v)
264
+ );
265
+ }
266
+
267
+ /**
268
+ * A car's scans, newest first.
269
+ * @param {string} carId - the car id
270
+ * @returns {GarageScan[]}
271
+ */
272
+ function garageScans(carId) {
273
+ const list = garageAllScans()[carId];
274
+ return Array.isArray(list) ? list.slice() : [];
275
+ }
276
+
277
+ /**
278
+ * One stored scan.
279
+ * @param {string} carId - the car id
280
+ * @param {string} scanId - the scan id
281
+ * @returns {GarageScan|null}
282
+ */
283
+ function garageScan(carId, scanId) {
284
+ return garageScans(carId).find((s) => s.id === scanId) || null;
285
+ }
286
+
287
+ /**
288
+ * Count what a report holds, for the history rows.
289
+ * @param {object} report - an IpoProtocolReport
290
+ * @returns {GarageScanSummary}
291
+ */
292
+ function garageScanSummary(report) {
293
+ const mods = (report && report.modules) || [];
294
+ const withFaults = mods.filter((m) => (m.codes || []).length);
295
+ return {
296
+ modules: mods.length,
297
+ withFaults: withFaults.length,
298
+ faults: withFaults.reduce((n, m) => n + m.codes.length, 0),
299
+ silent: ((report && report.silent) || []).length,
300
+ };
301
+ }
302
+
303
+ /**
304
+ * One fault, ready to be stored as JSON.
305
+ *
306
+ * F_HEX_CODE arrives from the web VM as a byte array. JSON.stringify turns a
307
+ * typed array into {"0":39,"1":218}, which hexText() then cannot read -- the
308
+ * reloaded fault would draw as "[object Object]". Flattening it here to the
309
+ * same dashed string the native path produces keeps a reloaded scan
310
+ * byte-identical to a live one, and costs less space than the byte map.
311
+ * @param {object} code - an FS_LESEN entry
312
+ * @returns {object} the entry as stored
313
+ */
314
+ function garageCompactCode(code) {
315
+ const out = { ...code };
316
+ if (out.F_HEX_CODE != null && typeof out.F_HEX_CODE !== 'string') {
317
+ const flat =
318
+ typeof hexText === 'function'
319
+ ? hexText(out.F_HEX_CODE)
320
+ : String(out.F_HEX_CODE);
321
+ if (flat) out.F_HEX_CODE = flat;
322
+ else delete out.F_HEX_CODE;
323
+ }
324
+ return out;
325
+ }
326
+
327
+ /**
328
+ * Strip a report down to what the history needs to redraw and diff it. The
329
+ * live report carries whatever the wire returned; only the fields the
330
+ * renderer and the diff read are kept, so a long history stays inside
331
+ * localStorage.
332
+ * @param {object} report - the live IpoProtocolReport
333
+ * @returns {object} the stored report
334
+ */
335
+ function garageCompactReport(report) {
336
+ const r = report || {};
337
+ return {
338
+ kind: r.kind === 'ident' ? 'ident' : 'faults',
339
+ modules: (r.modules || []).map((m) => {
340
+ /** @type {object} */
341
+ const out = {
342
+ sgbd: m.sgbd,
343
+ via: m.via,
344
+ label: m.label,
345
+ codes: (m.codes || []).map(garageCompactCode),
346
+ };
347
+ if (m.ident) out.ident = { ...m.ident };
348
+ return out;
349
+ }),
350
+ silent: (r.silent || []).map((s) => ({
351
+ target: s.target,
352
+ label: s.label,
353
+ error: s.error,
354
+ })),
355
+ // the viewer's own toggle, never persisted as "on": a reopened scan
356
+ // always shows the table first
357
+ showText: false,
358
+ };
359
+ }
360
+
361
+ /**
362
+ * Keep a scan against a car, dropping the oldest past the cap.
363
+ * @param {string} carId - the car it belongs to
364
+ * @param {object} view - the program view ({report, lines}) or a bare report
365
+ * @param {{chassis?: string, at?: string}} [meta] - what the live view knows
366
+ * @returns {GarageScan|null} the stored scan, or null when there is no report
367
+ */
368
+ function garageAddScan(carId, view, meta) {
369
+ const v = view || {};
370
+ const report = v.report || (v.modules ? v : null);
371
+ if (!carId || !report || !(report.modules || []).length) return null;
372
+ const m = meta || {};
373
+ const compact = garageCompactReport(report);
374
+ /** @type {GarageScan} */
375
+ const scan = {
376
+ id: garageNewId(),
377
+ carId,
378
+ at: m.at || new Date().toISOString(),
379
+ kind: compact.kind,
380
+ report: compact,
381
+ summary: garageScanSummary(compact),
382
+ };
383
+ if (m.chassis) scan.chassis = String(m.chassis).toUpperCase();
384
+ if (Array.isArray(v.lines) && v.lines.length) scan.lines = v.lines.slice();
385
+ const all = garageAllScans();
386
+ const list = Array.isArray(all[carId]) ? all[carId] : [];
387
+ all[carId] = [scan, ...list].slice(0, GARAGE_SCAN_CAP);
388
+ garageWrite(GARAGE_SCANS_KEY, all);
389
+ return scan;
390
+ }
391
+
392
+ /**
393
+ * Drop one scan.
394
+ * @param {string} carId - the car id
395
+ * @param {string} scanId - the scan id
396
+ * @returns {void}
397
+ */
398
+ function garageRemoveScan(carId, scanId) {
399
+ const all = garageAllScans();
400
+ if (!Array.isArray(all[carId])) return;
401
+ all[carId] = all[carId].filter((s) => s.id !== scanId);
402
+ garageWrite(GARAGE_SCANS_KEY, all);
403
+ }
404
+
405
+ /**
406
+ * Turn a decoded VIN (the shared decoder's EtkVinHit) into a garage entry.
407
+ * @param {object} hit - the decoded VIN
408
+ * @returns {Partial<GarageCar>}
409
+ */
410
+ function garageCarFromHit(hit) {
411
+ const h = hit || {};
412
+ const disp =
413
+ typeof dispChassis === 'function' && h.chassis
414
+ ? dispChassis(h.chassis)
415
+ : h.chassis || '';
416
+ return {
417
+ vin: h.vin,
418
+ chassis: h.chassis,
419
+ label: [disp, h.model].filter(Boolean).join(' '),
420
+ model: h.model,
421
+ body: h.body,
422
+ motor: h.motor,
423
+ prod: h.prod,
424
+ };
425
+ }
426
+
427
+ // ---- which scans may be compared ------------------------------------------------
428
+
429
+ /**
430
+ * The older scan of the same kind before one: a fault scan is compared with
431
+ * the fault scan before it, an identification read with the identification
432
+ * read before it -- a fault list against an ident table is not a comparison.
433
+ * @param {GarageScan[]} scans - a car's scans, newest first
434
+ * @param {string} scanId - the scan to look back from
435
+ * @returns {GarageScan|null}
436
+ */
437
+ function garagePrevSameKind(scans, scanId) {
438
+ const i = (scans || []).findIndex((s) => s.id === scanId);
439
+ if (i < 0) return null;
440
+ const kind = scans[i].kind;
441
+ for (let j = i + 1; j < scans.length; j++)
442
+ if (scans[j].kind === kind) return scans[j];
443
+ return null;
444
+ }
445
+
446
+ /**
447
+ * The newest pair of scans of one kind: the latest scan and the one of its
448
+ * kind before it, else the latest scan of the other kind and its
449
+ * predecessor. Null when no two scans share a kind.
450
+ * @param {GarageScan[]} scans - a car's scans, newest first
451
+ * @returns {{from: GarageScan, to: GarageScan}|null}
452
+ */
453
+ function garageScanPair(scans) {
454
+ for (const to of scans || []) {
455
+ const from = garagePrevSameKind(scans, to.id);
456
+ if (from) return { from, to };
457
+ }
458
+ return null;
459
+ }
460
+
461
+ // ---- the car a scan launched from the garage belongs to ----------------------
462
+
463
+ /**
464
+ * How long a scan started from a car's page stays filed against that car
465
+ * (a whole-car read on a cold cable can take minutes; a day is generous).
466
+ */
467
+ const GARAGE_TARGET_TTL_MS = 24 * 60 * 60 * 1000;
468
+
469
+ /**
470
+ * The car whose page launched the read now running, if any. In memory only:
471
+ * it lives exactly as long as the page the scan runs in.
472
+ * @type {{carId: string, at: number}|null}
473
+ */
474
+ let garageTarget = null;
475
+
476
+ /**
477
+ * File the next saved scan against this car: the garage's own Fault scan /
478
+ * Identification buttons call it before opening the whole-car script.
479
+ * @param {string} carId - the car
480
+ * @returns {void}
481
+ */
482
+ function garageScanFor(carId) {
483
+ garageTarget = carId ? { carId: String(carId), at: Date.now() } : null;
484
+ }
485
+
486
+ /**
487
+ * Forget the pending target (after a save took it).
488
+ * @returns {void}
489
+ */
490
+ function garageScanTargetClear() {
491
+ garageTarget = null;
492
+ }
493
+
494
+ /**
495
+ * The car a just-finished read should be filed against without asking: the
496
+ * one whose page launched it, provided the read did not identify itself as
497
+ * a different car. A VIN the read found must match the car's own when the
498
+ * car has one; a car saved without a VIN accepts whatever the read found.
499
+ * @param {string} [vin] - the VIN the read identified, if any
500
+ * @returns {GarageCar|null}
501
+ */
502
+ function garageScanTarget(vin) {
503
+ if (!garageTarget) return null;
504
+ if (Date.now() - garageTarget.at > GARAGE_TARGET_TTL_MS) {
505
+ garageTarget = null;
506
+ return null;
507
+ }
508
+ const car = garageCar(garageTarget.carId);
509
+ if (!car) {
510
+ garageTarget = null;
511
+ return null;
512
+ }
513
+ if (vin && car.vin) {
514
+ const a = String(vin).toUpperCase().slice(-GARAGE_VIN_TAIL);
515
+ const b = String(car.vin).toUpperCase().slice(-GARAGE_VIN_TAIL);
516
+ if (a !== b) return null;
517
+ }
518
+ return car;
519
+ }
520
+
521
+ if (typeof module !== 'undefined' && module.exports) {
522
+ module.exports = {
523
+ GARAGE_CARS_KEY,
524
+ GARAGE_SCANS_KEY,
525
+ GARAGE_SCAN_CAP,
526
+ garageCars,
527
+ garageCar,
528
+ garageAddCar,
529
+ garageUpdateCar,
530
+ garageRemoveCar,
531
+ garageCarLabel,
532
+ garageFindByVin,
533
+ garageScans,
534
+ garageScan,
535
+ garageAddScan,
536
+ garageRemoveScan,
537
+ garageScanSummary,
538
+ garageCompactReport,
539
+ garageCarFromHit,
540
+ garageNewId,
541
+ garageScanFor,
542
+ garageScanTargetClear,
543
+ garageScanTarget,
544
+ garagePrevSameKind,
545
+ garageScanPair,
546
+ };
547
+ }