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,249 @@
1
+ /**
2
+ * @file Group -> variant resolution over the live bus.
3
+ *
4
+ * Which SGBD is this ECU? EDIABAS answers with GROUP files: d_00a4 probes
5
+ * diagnostic address 0xA4, decodes the ident answer, and reports VARIANTE
6
+ * ("MRS4"), which IS the SGBD name to load. The groups in data/groups/ are
7
+ * the same VM bytecode as any job (tools/export/sgbd_export.py); the newer
8
+ * dialect additionally resolves through the t_grtb assignment table
9
+ * (variants.json), reached by tabset/tabsetex "ZuordnungsTabelle".
10
+ * ResolveSgbdFile in the reference engine does exactly this: run the
11
+ * group's IDENTIFIKATION, read result VARIANTE from the first data set.
12
+ */
13
+ /* exported forgetResolvedVariants, webResolveVariantLast, webResolveVariant */
14
+
15
+ /**
16
+ * Why the last webResolveVariant call answered what it did.
17
+ * @typedef {object} ResolveDiag
18
+ * @property {string} group - The group name (lowercased).
19
+ * @property {'no-probe-shipped'|'silent-recently'|'probe-error'|'bus-silent'|'resolved'|'answered-but-unmatched'} path -
20
+ * Which exit the resolver took.
21
+ * @property {string} [variant] - The resolved SGBD name.
22
+ * @property {string} [error] - The probe's error text.
23
+ * @property {number} [empty] - Telegrams that got no usable answer.
24
+ * @property {number} [real] - Telegrams that were answered.
25
+ * @property {number} [sets] - Result sets the probe produced.
26
+ */
27
+
28
+ /**
29
+ * name -> Promise<code|null>; the PROMISE is cached so two concurrent
30
+ * resolves of the same group fetch once.
31
+ * @type {Map<string, Promise<any|null>>}
32
+ */
33
+ const groupCodeCache = new Map();
34
+ /** @type {Promise<any|null>|null} */
35
+ let groupVariantsPromise = null;
36
+ /**
37
+ * group -> variant. Successful resolutions only, per session: an ECU that
38
+ * did not answer may be a module that was busy, so a re-sweep asks again.
39
+ * @type {Map<string, string>}
40
+ */
41
+ const groupVariantCache = new Map();
42
+ /**
43
+ * Groups that answered NOTHING, by the time they were last probed. A
44
+ * whole-vehicle script asks each group two or three jobs in a row (INFO,
45
+ * FS_LESEN); an address that is not fitted must not eat a full probe
46
+ * timeout for each of them. Short-lived on purpose: the next module the
47
+ * user plugs in, or the ignition coming on, must be found again.
48
+ * @type {Map<string, number>}
49
+ */
50
+ const groupMissCache = new Map();
51
+ /** how long a silent address stays silent without re-probing (ms) */
52
+ const GROUP_MISS_TTL_MS = 15000;
53
+
54
+ /**
55
+ * Load a group's VM bytecode from data/groups/<name>.json.gz, once.
56
+ * @param {string} name - The group name (any case).
57
+ * @returns {Promise<any|null>} The exported program, or null.
58
+ */
59
+ function loadGroupCode(name) {
60
+ const key = String(name).toLowerCase();
61
+ if (!groupCodeCache.has(key)) {
62
+ groupCodeCache.set(key, webFetchGz(`data/groups/${key}.json.gz`));
63
+ }
64
+ return groupCodeCache.get(key);
65
+ }
66
+
67
+ /**
68
+ * Load the t_grtb assignment table (variants.json), once.
69
+ * @returns {Promise<{table?: string, rows: any[]}|null>}
70
+ */
71
+ function loadGroupVariants() {
72
+ if (!groupVariantsPromise) {
73
+ groupVariantsPromise = webFetchJson('data/groups/variants.json');
74
+ }
75
+ return groupVariantsPromise;
76
+ }
77
+
78
+ /**
79
+ * Forget the resolved variants: they are facts about the CAR on the other
80
+ * end of the cable, and the next connection may be a different one.
81
+ */
82
+ function forgetResolvedVariants() {
83
+ groupVariantCache.clear();
84
+ groupMissCache.clear();
85
+ }
86
+
87
+ /**
88
+ * The tables a group probe runs against: the group's OWN tables (tabset
89
+ * dialect: d_0032 reaches its embedded ZuordnungsTabelle copy with a plain
90
+ * `tabset`), plus t_grtb for the `tabsetex "ZuordnungsTabelle", "t_grtb"`
91
+ * dialect. Each side also stands in for the other defensively: exports that
92
+ * predate local tables get the t_grtb master under the local name (its keys
93
+ * embed the address, so the same rows match), and a missing variants.json
94
+ * falls back to the group's local copies as t_grtb -- but where both exist,
95
+ * the real source wins (local copy for tabset, the t_grtb dump for
96
+ * tabsetex), which is what the engine reads in each case.
97
+ * @param {any} code - The group's exported program.
98
+ * @param {{table?: string, rows: any[]}|null} variants - variants.json.
99
+ * @returns {{tables: Record<string, any>, extTables: Record<string, any>}}
100
+ */
101
+ function groupProbeTables(code, variants) {
102
+ const tables = Object.assign({}, code.tables || {});
103
+ const extTables = { t_grtb: Object.assign({}, code.tables || {}) };
104
+ if (variants && Array.isArray(variants.rows)) {
105
+ const tname = variants.table || 'ZuordnungsTabelle';
106
+ extTables.t_grtb[tname] = variants.rows;
107
+ const hasLocal = Object.keys(tables).some(
108
+ (k) => k.toUpperCase() === tname.toUpperCase()
109
+ );
110
+ if (!hasLocal) tables[tname] = variants.rows;
111
+ }
112
+ return { tables, extTables };
113
+ }
114
+
115
+ /**
116
+ * Why the last webResolveVariant call answered null -- the gate screens
117
+ * read this so a failed probe reports WHICH way it failed instead of a
118
+ * generic "no answer". Five exits share that null, and on a live car they
119
+ * mean completely different things (silent bus vs. answered-but-unmatched).
120
+ * @type {ResolveDiag|null}
121
+ */
122
+ let _lastResolve = null;
123
+
124
+ /**
125
+ * The diagnosis of the most recent resolve.
126
+ * @returns {ResolveDiag|null}
127
+ */
128
+ function webResolveVariantLast() {
129
+ return _lastResolve;
130
+ }
131
+
132
+ /**
133
+ * Record how a resolve ended: for webResolveVariantLast, the console, and
134
+ * the session journal (a beta Report must say WHY a module was not
135
+ * identified, not just that inpainit stopped afterwards).
136
+ * @param {string} key - The group name (lowercased).
137
+ * @param {ResolveDiag['path']} path - Which exit was taken.
138
+ * @param {Partial<ResolveDiag>} [extra] - Counters, variant or error text.
139
+ */
140
+ function noteResolve(key, path, extra) {
141
+ _lastResolve = { group: key, path, ...extra };
142
+ console.info(`[variant] ${key}: ${path}`, extra || '');
143
+ if (typeof Journal !== 'undefined' && Journal.log) {
144
+ Journal.log(
145
+ 'variant',
146
+ `${key}: ${path}` + (extra ? ' ' + JSON.stringify(extra) : '')
147
+ );
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Resolve one group to a concrete variant over the live bus: run the
153
+ * group's IDENTIFIKATION exactly the way webRunJob drives a job (passes
154
+ * with memoised answers over webBus.exchange), and return the VARIANTE it
155
+ * reports, LOWERCASED -- that is the SGBD name every loader here expects.
156
+ * IDENTIFIKATION is a read (IDENT is a strong read token), so no
157
+ * allowWrites is involved.
158
+ *
159
+ * A GROUP PROBES SEVERAL PROTOCOLS AT ONE ADDRESS, AND SILENCE ON ONE OF
160
+ * THEM IS A NORMAL STEP, NOT THE END OF THE JOB. d_0012 opens with a DS2
161
+ * frame (12 04 00), then falls back to KWP2000* (B8 12 F1 02 1A 80). An MS45
162
+ * ignores the first and answers the second -- verified against EDIABAS's own
163
+ * ifh.trc on a real E46, which logs SetError EDIABAS_IFH_0009 on the DS2
164
+ * probe and keeps going. The bytecode branches on the answer's LENGTH
165
+ * (slen), so an empty answer is what carries it to the next telegram.
166
+ * Letting that rejection escape returned "no variant" for a DME that was
167
+ * answering perfectly, and the sweep drew it as "not installed" -- hiding
168
+ * ten real stored faults. driveJobOverBus applies the same rule for both.
169
+ * @param {string} groupName - The group (d_xxxx) to probe, any case.
170
+ * @returns {Promise<string|null>} The lowercased SGBD name, or null when
171
+ * nothing answered, the answer matched no variant, or the group could not
172
+ * be loaded; never a made-up name.
173
+ */
174
+ async function webResolveVariant(groupName) {
175
+ const key = String(groupName).toLowerCase();
176
+ if (groupVariantCache.has(key)) return groupVariantCache.get(key);
177
+ const missedAt = groupMissCache.get(key);
178
+ if (missedAt != null && Date.now() - missedAt < GROUP_MISS_TTL_MS) {
179
+ noteResolve(key, 'silent-recently');
180
+ return null;
181
+ }
182
+ const code = await loadGroupCode(key);
183
+ if (!code || !code.jobs || code.jobs.IDENTIFIKATION === undefined) {
184
+ noteResolve(key, 'no-probe-shipped');
185
+ return null;
186
+ }
187
+ const variants = await loadGroupVariants();
188
+ const { tables, extTables } = groupProbeTables(code, variants);
189
+
190
+ let sets;
191
+ const tally = newTally();
192
+ try {
193
+ const r = await driveJobOverBus(
194
+ (send, now) =>
195
+ new Best2Vm(code, {
196
+ tables,
197
+ extTables,
198
+ args: '',
199
+ // Group probing walks diagnostic addresses to find out WHICH module
200
+ // answers, so it must never transmit anything that changes one. It
201
+ // only ever needs idents, and this stays refused on purpose.
202
+ allowWrites: false,
203
+ now,
204
+ send,
205
+ }),
206
+ 'IDENTIFIKATION',
207
+ '',
208
+ tally
209
+ );
210
+ sets = r.sets;
211
+ } catch (e) {
212
+ // A job-level failure (bad bytecode, unusable answer) means the address
213
+ // did not identify. Absence of an ANSWER is handled inside the drive.
214
+ noteResolve(key, 'probe-error', {
215
+ error: String((e && e.message) || e),
216
+ empty: tally.empty,
217
+ real: tally.real,
218
+ });
219
+ groupMissCache.set(key, Date.now());
220
+ return null;
221
+ }
222
+ // Nothing on this address answered anything: the module is genuinely not
223
+ // there. Distinguished from a resolution that ran but matched no variant,
224
+ // which is a shipped-tables problem rather than a silent bus.
225
+ if (tally.empty && !tally.real) {
226
+ noteResolve(key, 'bus-silent', { empty: tally.empty });
227
+ groupMissCache.set(key, Date.now());
228
+ return null;
229
+ }
230
+ for (const s of sets || []) {
231
+ if (typeof s.VARIANTE === 'string' && s.VARIANTE) {
232
+ const v = s.VARIANTE.toLowerCase();
233
+ noteResolve(key, 'resolved', {
234
+ variant: v,
235
+ empty: tally.empty,
236
+ real: tally.real,
237
+ });
238
+ groupVariantCache.set(key, v);
239
+ return v;
240
+ }
241
+ }
242
+ noteResolve(key, 'answered-but-unmatched', {
243
+ empty: tally.empty,
244
+ real: tally.real,
245
+ sets: (sets || []).length,
246
+ });
247
+ groupMissCache.set(key, Date.now());
248
+ return null;
249
+ }