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,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
|
+
}
|