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,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Sharing a stored scan as a link that carries the report itself.
|
|
3
|
+
*
|
|
4
|
+
* Nothing is stored on a server: the compact report is deflated, base64url
|
|
5
|
+
* encoded and put in the URL fragment (#report/<payload>), so the link opens
|
|
6
|
+
* on any copy of the app, hosted or offline, with no car and no account. The
|
|
7
|
+
* VIN is left out of the payload; the car is named by its label and chassis.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/* exported garageSharePayload, garageShareEncode, garageShareDecode,
|
|
11
|
+
garageShareUrl, garageShareButton, showGarageSharedReport */
|
|
12
|
+
|
|
13
|
+
/** How long the Share button shows its "Copied" confirmation, ms. */
|
|
14
|
+
const GARAGE_COPY_FLASH_MS = 1600;
|
|
15
|
+
|
|
16
|
+
/** The payload format version, so a later reader can tell an older link. */
|
|
17
|
+
const GARAGE_SHARE_V = 1;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What a shared link carries: the report and enough to caption it. The VIN
|
|
21
|
+
* is deliberately absent, as it is in the beta reports.
|
|
22
|
+
* @param {GarageScan} scan - the stored scan
|
|
23
|
+
* @param {GarageCar} [car] - the car it belongs to, for its label
|
|
24
|
+
* @returns {{v: number, kind: string, at: string, chassis: string, label: string, report: object, summary: object}}
|
|
25
|
+
*/
|
|
26
|
+
function garageSharePayload(scan, car) {
|
|
27
|
+
const label =
|
|
28
|
+
typeof garageCarLabel === 'function' && car
|
|
29
|
+
? garageCarLabel(car)
|
|
30
|
+
: (car && car.label) || '';
|
|
31
|
+
return {
|
|
32
|
+
v: GARAGE_SHARE_V,
|
|
33
|
+
kind: scan.kind,
|
|
34
|
+
at: scan.at,
|
|
35
|
+
chassis: scan.chassis || (car && car.chassis) || '',
|
|
36
|
+
label,
|
|
37
|
+
report: scan.report,
|
|
38
|
+
summary: scan.summary,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Base64url of bytes (no padding, URL-safe alphabet).
|
|
44
|
+
* @param {Uint8Array} bytes - the bytes
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
function garageB64urlEncode(bytes) {
|
|
48
|
+
let bin = '';
|
|
49
|
+
for (let i = 0; i < bytes.length; i += 0x8000)
|
|
50
|
+
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
|
|
51
|
+
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Bytes of a base64url string.
|
|
56
|
+
* @param {string} s - the text
|
|
57
|
+
* @returns {Uint8Array}
|
|
58
|
+
*/
|
|
59
|
+
function garageB64urlDecode(s) {
|
|
60
|
+
const b64 = String(s).replace(/-/g, '+').replace(/_/g, '/');
|
|
61
|
+
const bin = atob(b64 + '='.repeat((4 - (b64.length % 4)) % 4));
|
|
62
|
+
const out = new Uint8Array(bin.length);
|
|
63
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Run bytes through a CompressionStream / DecompressionStream.
|
|
69
|
+
* @param {Uint8Array} bytes - the input
|
|
70
|
+
* @param {CompressionStream|DecompressionStream} stream - the transform
|
|
71
|
+
* @returns {Promise<Uint8Array>}
|
|
72
|
+
*/
|
|
73
|
+
async function garagePipe(bytes, stream) {
|
|
74
|
+
const res = new Response(new Blob([bytes]).stream().pipeThrough(stream));
|
|
75
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Encode a scan as the link payload: JSON, deflate-raw, base64url. A
|
|
80
|
+
* whole-car read compresses to a few KB, well inside what a browser and a
|
|
81
|
+
* forum post carry in a URL.
|
|
82
|
+
* @param {GarageScan} scan - the stored scan
|
|
83
|
+
* @param {GarageCar} [car] - the car it belongs to
|
|
84
|
+
* @returns {Promise<string>} the payload
|
|
85
|
+
*/
|
|
86
|
+
async function garageShareEncode(scan, car) {
|
|
87
|
+
const json = JSON.stringify(garageSharePayload(scan, car));
|
|
88
|
+
const raw = new TextEncoder().encode(json);
|
|
89
|
+
const packed = await garagePipe(raw, new CompressionStream('deflate-raw'));
|
|
90
|
+
return garageB64urlEncode(packed);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Decode a link payload back into what garageSharePayload built.
|
|
95
|
+
* @param {string} payload - the base64url text from the URL
|
|
96
|
+
* @returns {Promise<object|null>} the payload object, or null when unreadable
|
|
97
|
+
*/
|
|
98
|
+
async function garageShareDecode(payload) {
|
|
99
|
+
try {
|
|
100
|
+
const packed = garageB64urlDecode(payload);
|
|
101
|
+
const raw = await garagePipe(
|
|
102
|
+
packed,
|
|
103
|
+
new DecompressionStream('deflate-raw')
|
|
104
|
+
);
|
|
105
|
+
const obj = JSON.parse(new TextDecoder().decode(raw));
|
|
106
|
+
if (!obj || typeof obj !== 'object' || !obj.report) return null;
|
|
107
|
+
return obj;
|
|
108
|
+
} catch (e) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The shareable URL for a scan: this app's own address with the payload in
|
|
115
|
+
* the fragment, so it opens on the hosted site or an offline copy alike.
|
|
116
|
+
* @param {GarageScan} scan - the stored scan
|
|
117
|
+
* @param {GarageCar} [car] - the car it belongs to
|
|
118
|
+
* @param {string} [base] - the page address without a fragment (defaults to this page)
|
|
119
|
+
* @returns {Promise<string>}
|
|
120
|
+
*/
|
|
121
|
+
async function garageShareUrl(scan, car, base) {
|
|
122
|
+
const payload = await garageShareEncode(scan, car);
|
|
123
|
+
const here =
|
|
124
|
+
base != null
|
|
125
|
+
? base
|
|
126
|
+
: typeof location !== 'undefined'
|
|
127
|
+
? location.href.split('#')[0]
|
|
128
|
+
: '';
|
|
129
|
+
return `${here}#report/${payload}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Copy text to the clipboard, through the wiring app's helper when it is
|
|
134
|
+
* loaded (async clipboard with a textarea fallback for file://).
|
|
135
|
+
* @param {string} text - what to copy
|
|
136
|
+
* @returns {Promise<boolean>}
|
|
137
|
+
*/
|
|
138
|
+
async function garageCopyText(text) {
|
|
139
|
+
if (typeof wiringCopyText === 'function') return wiringCopyText(text);
|
|
140
|
+
try {
|
|
141
|
+
await navigator.clipboard.writeText(text);
|
|
142
|
+
return true;
|
|
143
|
+
} catch (e) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A Share button for a stored scan: copies the report link and flashes
|
|
150
|
+
* "Copied", exactly like the wiring app's Share.
|
|
151
|
+
* @param {GarageScan} scan - the stored scan
|
|
152
|
+
* @param {GarageCar} [car] - the car it belongs to
|
|
153
|
+
* @returns {HTMLButtonElement}
|
|
154
|
+
*/
|
|
155
|
+
function garageShareButton(scan, car) {
|
|
156
|
+
const btn = document.createElement('button');
|
|
157
|
+
btn.type = 'button';
|
|
158
|
+
btn.className = 'btn garage-share';
|
|
159
|
+
btn.textContent = 'Share';
|
|
160
|
+
btn.title = 'Copy a link that carries this report';
|
|
161
|
+
btn.onclick = async () => {
|
|
162
|
+
const url = await garageShareUrl(scan, car);
|
|
163
|
+
const ok = await garageCopyText(url);
|
|
164
|
+
const original = 'Share';
|
|
165
|
+
btn.textContent = ok ? '✓ Copied' : 'Copy failed';
|
|
166
|
+
btn.classList.toggle('copied', ok);
|
|
167
|
+
clearTimeout(btn._copyTimer);
|
|
168
|
+
btn._copyTimer = setTimeout(() => {
|
|
169
|
+
btn.textContent = original;
|
|
170
|
+
btn.classList.remove('copied');
|
|
171
|
+
}, GARAGE_COPY_FLASH_MS);
|
|
172
|
+
};
|
|
173
|
+
return btn;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* A report someone sent as a link: drawn by the live view's own renderer,
|
|
178
|
+
* with Save to garage so the recipient can keep it against a car of theirs.
|
|
179
|
+
* @param {string} payload - the base64url payload from #report/<payload>
|
|
180
|
+
* @returns {Promise<void>}
|
|
181
|
+
*/
|
|
182
|
+
async function showGarageSharedReport(payload) {
|
|
183
|
+
const p = await garageShareDecode(payload);
|
|
184
|
+
lastScreen = () => showGarageSharedReport(payload);
|
|
185
|
+
setCrumbs([
|
|
186
|
+
{ label: 'Vehicles', fn: showChassis },
|
|
187
|
+
{ label: 'Garage', fn: showGarage },
|
|
188
|
+
{ label: 'Shared report' },
|
|
189
|
+
]);
|
|
190
|
+
document.body.classList.add('apps-section');
|
|
191
|
+
sbLeft.textContent = 'shared report';
|
|
192
|
+
if (!p) {
|
|
193
|
+
view.innerHTML = head('Garage', 'Shared report', '');
|
|
194
|
+
view.appendChild(
|
|
195
|
+
errorBlock(
|
|
196
|
+
'This link does not carry a readable report. It may have been cut short when it was pasted.'
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
setActions([garageBackAction(showGarage)]);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const kindText = p.kind === 'ident' ? 'Identification' : 'Fault memories';
|
|
203
|
+
const who = [p.label, p.chassis && dispChassis(p.chassis)]
|
|
204
|
+
.filter(Boolean)
|
|
205
|
+
.join(' · ');
|
|
206
|
+
view.innerHTML = head(
|
|
207
|
+
'Garage',
|
|
208
|
+
'Shared report',
|
|
209
|
+
`${who ? `${who} · ` : ''}${kindText} · ${garageDateText(p.at)}`
|
|
210
|
+
);
|
|
211
|
+
const scan = {
|
|
212
|
+
kind: p.kind,
|
|
213
|
+
at: p.at,
|
|
214
|
+
chassis: p.chassis,
|
|
215
|
+
report: p.report,
|
|
216
|
+
summary: p.summary || garageScanSummary(p.report),
|
|
217
|
+
};
|
|
218
|
+
sbRight.textContent = garageScanCountsText(scan);
|
|
219
|
+
const body = document.createElement('div');
|
|
220
|
+
view.appendChild(body);
|
|
221
|
+
if (typeof ipoProtocolRender === 'function')
|
|
222
|
+
await ipoProtocolRender(body, garageViewFor(scan));
|
|
223
|
+
garageDisarmStoredReport(body);
|
|
224
|
+
if (typeof garageAttachEnv === 'function')
|
|
225
|
+
await garageAttachEnv(body, scan.report, {
|
|
226
|
+
screensFor:
|
|
227
|
+
typeof garageScreensFor === 'function' ? garageScreensFor : null,
|
|
228
|
+
});
|
|
229
|
+
// keep it: the report goes into the Garage against a car the recipient picks
|
|
230
|
+
const keep = async () => {
|
|
231
|
+
if (typeof garagePickCar !== 'function') return;
|
|
232
|
+
const car = await garagePickCar({ vin: '', chassis: p.chassis });
|
|
233
|
+
if (!car) return;
|
|
234
|
+
const kept = garageAddScan(
|
|
235
|
+
car.id,
|
|
236
|
+
{ report: p.report },
|
|
237
|
+
{
|
|
238
|
+
chassis: p.chassis,
|
|
239
|
+
at: p.at,
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
if (kept) showGarageScan(car.id, kept.id);
|
|
243
|
+
};
|
|
244
|
+
const bar = body.querySelector('.quick-bar-btns');
|
|
245
|
+
if (bar) {
|
|
246
|
+
const save = document.createElement('button');
|
|
247
|
+
save.className = 'btn garage-save';
|
|
248
|
+
save.textContent = 'Save to garage';
|
|
249
|
+
save.onclick = keep;
|
|
250
|
+
bar.appendChild(save);
|
|
251
|
+
}
|
|
252
|
+
setActions([
|
|
253
|
+
{ key: '1', label: 'Save to garage', fn: keep },
|
|
254
|
+
{
|
|
255
|
+
key: 'p',
|
|
256
|
+
label: 'Print',
|
|
257
|
+
kind: 'print',
|
|
258
|
+
fn: () =>
|
|
259
|
+
garagePrintScan(
|
|
260
|
+
{ label: p.label || 'Shared report', chassis: p.chassis },
|
|
261
|
+
scan
|
|
262
|
+
),
|
|
263
|
+
},
|
|
264
|
+
garageBackAction(showGarage),
|
|
265
|
+
]);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// node loads these pieces as modules; the browser gives them one shared scope
|
|
269
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
270
|
+
module.exports = {
|
|
271
|
+
garageSharePayload,
|
|
272
|
+
garageShareEncode,
|
|
273
|
+
garageShareDecode,
|
|
274
|
+
garageShareUrl,
|
|
275
|
+
};
|
|
276
|
+
}
|