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,734 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The Web Serial transport: a K+DCAN cable in a desktop browser, with
|
|
3
|
+
* the K-line line control (DTR transmit enable, fast init, ISO 9141 slow
|
|
4
|
+
* init) that only this bus can drive.
|
|
5
|
+
*/
|
|
6
|
+
/* exported WebSerialBus */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sentinel for a readSome() that ran out of time. A distinct object rather
|
|
10
|
+
* than null, so "no bytes yet" can never be confused with a real empty read.
|
|
11
|
+
*/
|
|
12
|
+
const TIMED_OUT = Symbol('timed-out');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The reference interface's DtrTimeCorrCom: how much longer than the
|
|
16
|
+
* telegram's byte time DTR stays up, in ms. No more -- DTR is the transmit
|
|
17
|
+
* enable, so holding it longer keeps the cable talking while the ECU answers
|
|
18
|
+
* and the reply is lost. An earlier +4 ms margin here produced a perfect echo
|
|
19
|
+
* and no answer.
|
|
20
|
+
*/
|
|
21
|
+
const DTR_TIME_CORR_MS = 0.3;
|
|
22
|
+
/** Fast init: the break is held until this long after it BEGAN. */
|
|
23
|
+
const FAST_INIT_BREAK_MS = 25;
|
|
24
|
+
/** Fast init: DTR drops this long after the break began (W4 is 25-50 ms). */
|
|
25
|
+
const FAST_INIT_DTR_MS = 50;
|
|
26
|
+
/** ISO 9141 slow init: one bit time at 5 baud. */
|
|
27
|
+
const SLOW_INIT_BIT_MS = 200;
|
|
28
|
+
/** ISO 9141 slow init: how long the 0x55 sync and two key bytes may take. */
|
|
29
|
+
const SLOW_INIT_SYNC_MS = 1200;
|
|
30
|
+
/** ISO 9141 slow init: the W4 pause before the tester's inverted key byte. */
|
|
31
|
+
const SLOW_INIT_W4_MS = 30;
|
|
32
|
+
/** ISO 9141 slow init: how long the ECU's address acknowledgement may take. */
|
|
33
|
+
const SLOW_INIT_ACK_MS = 400;
|
|
34
|
+
/** ISO 9141 sync pattern the ECU answers the 5-baud address with. */
|
|
35
|
+
const ISO9141_SYNC = 0x55;
|
|
36
|
+
/** drainBuffered: the most already-resolved reads it consumes in one go. */
|
|
37
|
+
const DRAIN_MAX_READS = 64;
|
|
38
|
+
/** drainBuffered: how long a pending read gets to prove it has settled. */
|
|
39
|
+
const DRAIN_PROBE_MS = 2;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The transport for a K+DCAN cable through the browser's Web Serial API.
|
|
43
|
+
* @extends SerialTransportBase
|
|
44
|
+
*/
|
|
45
|
+
class WebSerialBus extends SerialTransportBase {
|
|
46
|
+
constructor() {
|
|
47
|
+
super();
|
|
48
|
+
/** @type {SerialPort|null} */
|
|
49
|
+
this.port = null;
|
|
50
|
+
/** @type {ReadableStreamDefaultReader<Uint8Array>|null} */
|
|
51
|
+
this.reader = null;
|
|
52
|
+
/** @type {WritableStreamDefaultWriter<Uint8Array>|null} */
|
|
53
|
+
this.writer = null;
|
|
54
|
+
/** @type {PortConfig|null} */
|
|
55
|
+
this.config = null;
|
|
56
|
+
/**
|
|
57
|
+
* The one outstanding reader.read(), kept across a timed-out readSome so
|
|
58
|
+
* its bytes are never orphaned.
|
|
59
|
+
* @type {Promise<ReadableStreamReadResult<Uint8Array>>|null}
|
|
60
|
+
*/
|
|
61
|
+
this.pending = null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @returns {boolean} Is a port open. */
|
|
65
|
+
get connected() {
|
|
66
|
+
return !!this.port;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Open a port the user picks. Must be called from a user gesture -- the
|
|
71
|
+
* browser will not show the port picker otherwise. app.js wires this to the
|
|
72
|
+
* "connect cable" control.
|
|
73
|
+
* @returns {Promise<string>} The port label.
|
|
74
|
+
* @throws {Error} When the browser has no Web Serial.
|
|
75
|
+
*/
|
|
76
|
+
async connect() {
|
|
77
|
+
if (!('serial' in navigator)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
'This browser has no Web Serial. Use Chrome or Edge ' +
|
|
80
|
+
'(desktop), or the macOS app.'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
this.port = await navigator.serial.requestPort();
|
|
84
|
+
await this.port.open(KDCAN);
|
|
85
|
+
this.config = KDCAN;
|
|
86
|
+
this.writer = this.port.writable.getWriter();
|
|
87
|
+
this.reader = this.port.readable.getReader();
|
|
88
|
+
await this._applyIdleSignals(KDCAN);
|
|
89
|
+
this._resetWireState();
|
|
90
|
+
this._watchPort();
|
|
91
|
+
return this.portLabel();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Put the modem lines where the concept wants them for the whole session:
|
|
96
|
+
* DTR at the config's idle level, RTS always low.
|
|
97
|
+
*
|
|
98
|
+
* The reference interface does this in the same switch that picks baud and
|
|
99
|
+
* parity: on a plain FTDI/COM cable (an "echoing adapter" to it) DTR is
|
|
100
|
+
* held HIGH for BMW-FAST and D-CAN and LOW for every K-line concept, and
|
|
101
|
+
* RTS is never raised. What the browser leaves on the lines after open()
|
|
102
|
+
* is platform-dependent (Windows asserts both, WICG/serial#177), and a
|
|
103
|
+
* K-line exchange here ends with DTR dropped -- so without this step a
|
|
104
|
+
* BMW-FAST/D-CAN module was driven with DTR low after the first K-line
|
|
105
|
+
* probe of the session, which is the one way this transport differed
|
|
106
|
+
* from the reference on an E60/E65/E90 bus. Ports without setSignals
|
|
107
|
+
* (nothing to drive) are left alone.
|
|
108
|
+
* @param {PortConfig} cfg - The settings the session runs on.
|
|
109
|
+
* @returns {Promise<void>}
|
|
110
|
+
*/
|
|
111
|
+
async _applyIdleSignals(cfg) {
|
|
112
|
+
if (!this.port || !this.port.setSignals) return;
|
|
113
|
+
const dtr = !!(cfg && cfg.dtr);
|
|
114
|
+
await this.port.setSignals({
|
|
115
|
+
dataTerminalReady: dtr,
|
|
116
|
+
requestToSend: false,
|
|
117
|
+
});
|
|
118
|
+
busTrace.add('kline', null, `idle DTR=${dtr ? 'high' : 'low'} RTS=low`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Hear the cable being pulled. Web Serial fires `disconnect` on the port
|
|
123
|
+
* when its device goes away; until then a pulled cable looked connected
|
|
124
|
+
* (`this.port` was still set) and the chip said so until a reload. Drop
|
|
125
|
+
* the wire state and tell the app (a `bmweb-cable` event on window), then
|
|
126
|
+
* watch for the device coming back and reopen it silently.
|
|
127
|
+
* @returns {void}
|
|
128
|
+
*/
|
|
129
|
+
_watchPort() {
|
|
130
|
+
const port = this.port;
|
|
131
|
+
if (!port || typeof port.addEventListener !== 'function') return;
|
|
132
|
+
port.addEventListener('disconnect', () => this._portGone(port), {
|
|
133
|
+
once: true,
|
|
134
|
+
});
|
|
135
|
+
if (!this._replugWatched && typeof navigator !== 'undefined') {
|
|
136
|
+
const serial = navigator.serial;
|
|
137
|
+
if (serial && typeof serial.addEventListener === 'function') {
|
|
138
|
+
this._replugWatched = true;
|
|
139
|
+
serial.addEventListener('connect', () => {
|
|
140
|
+
if (this.connected) return;
|
|
141
|
+
this.reconnect()
|
|
142
|
+
.then((label) => {
|
|
143
|
+
if (label) this._announce(true);
|
|
144
|
+
})
|
|
145
|
+
.catch(() => {});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The port's device is gone: forget it without trying to close it (there
|
|
153
|
+
* is nothing to close), then announce.
|
|
154
|
+
* @param {SerialPort} port - the port that fired
|
|
155
|
+
* @returns {void}
|
|
156
|
+
*/
|
|
157
|
+
_portGone(port) {
|
|
158
|
+
if (this.port !== port) return; // already replaced
|
|
159
|
+
console.info('[serial] the cable was unplugged');
|
|
160
|
+
this._releaseStreams().catch(() => {});
|
|
161
|
+
this.port = this.reader = this.writer = null;
|
|
162
|
+
this._resetWireState();
|
|
163
|
+
this._announce(false);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Tell the app the cable state changed.
|
|
168
|
+
* @param {boolean} connected - whether a port is open now
|
|
169
|
+
* @returns {void}
|
|
170
|
+
*/
|
|
171
|
+
_announce(connected) {
|
|
172
|
+
if (typeof window === 'undefined' || !window.dispatchEvent) return;
|
|
173
|
+
try {
|
|
174
|
+
window.dispatchEvent(
|
|
175
|
+
new CustomEvent('bmweb-cable', { detail: { connected } })
|
|
176
|
+
);
|
|
177
|
+
} catch {
|
|
178
|
+
/* no CustomEvent: nothing to announce to */
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Reconnect WITHOUT a user gesture, on page load. Web Serial remembers a
|
|
184
|
+
* granted port across reloads (the permission survives; only the first
|
|
185
|
+
* requestPort() needs a click), so getPorts() returns it and open()
|
|
186
|
+
* succeeds silently. This is what keeps the cable "connected through a
|
|
187
|
+
* reload" -- the reopen-and-it-unlocks flow depends on it.
|
|
188
|
+
* @returns {Promise<string|null>} The label on success, or null when
|
|
189
|
+
* nothing was previously granted (first run, or the user revoked it) so
|
|
190
|
+
* the caller leaves the chip as "no cable".
|
|
191
|
+
*/
|
|
192
|
+
async reconnect() {
|
|
193
|
+
if (!('serial' in navigator) || this.connected) return null;
|
|
194
|
+
let ports;
|
|
195
|
+
try {
|
|
196
|
+
ports = await navigator.serial.getPorts();
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.info('[serial] getPorts() threw:', e.message);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
if (!ports.length) {
|
|
202
|
+
// The browser remembers a granted port PER ORIGIN, but the grant is lost
|
|
203
|
+
// if the origin changes, the device re-enumerated (some FTDI adapters do
|
|
204
|
+
// on replug), or the user cleared site permissions. Nothing to reopen
|
|
205
|
+
// silently -- the next connect() will ask once and it sticks again.
|
|
206
|
+
console.info(
|
|
207
|
+
'[serial] getPorts() returned no previously-granted port ' +
|
|
208
|
+
'(first run here, or the grant was lost) -- a one-time pick is needed'
|
|
209
|
+
);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
// Newer browsers expose SerialPort.connected = is the device physically
|
|
213
|
+
// present. Prefer a present one; a remembered-but-unplugged port would just
|
|
214
|
+
// fail to open. Fall back to the first if the flag is unavailable.
|
|
215
|
+
this.port = ports.find((p) => p.connected !== false) || ports[0];
|
|
216
|
+
try {
|
|
217
|
+
await this.port.open(KDCAN);
|
|
218
|
+
} catch (e) {
|
|
219
|
+
// The commonest cause is the port being held by another tab/app, or the
|
|
220
|
+
// device unplugged. Say which, rather than a silent "no cable".
|
|
221
|
+
console.info(
|
|
222
|
+
`[serial] reopen of a granted port failed: ${e.message} ` +
|
|
223
|
+
`(unplugged, or another tab/app holds it?)`
|
|
224
|
+
);
|
|
225
|
+
this.port = null;
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
this.config = KDCAN;
|
|
229
|
+
this.writer = this.port.writable.getWriter();
|
|
230
|
+
this.reader = this.port.readable.getReader();
|
|
231
|
+
await this._applyIdleSignals(KDCAN);
|
|
232
|
+
this._resetWireState();
|
|
233
|
+
this._watchPort();
|
|
234
|
+
console.info(
|
|
235
|
+
'[serial] reconnected to a previously-granted port, no picker'
|
|
236
|
+
);
|
|
237
|
+
return this.portLabel();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Give the port's streams back (cancel the reader, release both locks) so
|
|
242
|
+
* the port can be closed. Every failure is swallowed: the port is being
|
|
243
|
+
* closed or reopened either way.
|
|
244
|
+
*/
|
|
245
|
+
async _releaseStreams() {
|
|
246
|
+
try {
|
|
247
|
+
if (this.reader) {
|
|
248
|
+
await this.reader.cancel();
|
|
249
|
+
this.reader.releaseLock();
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
/* closing or reopening */
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
if (this.writer) this.writer.releaseLock();
|
|
256
|
+
} catch {
|
|
257
|
+
/* closing or reopening */
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Close the port and open it again on `cfg`, taking fresh stream locks.
|
|
263
|
+
* @param {PortConfig} cfg - The settings to reopen with.
|
|
264
|
+
*/
|
|
265
|
+
async _reopenStreams(cfg) {
|
|
266
|
+
await this.port.close();
|
|
267
|
+
await this.port.open(cfg);
|
|
268
|
+
this.config = cfg;
|
|
269
|
+
this.writer = this.port.writable.getWriter();
|
|
270
|
+
this.reader = this.port.readable.getReader();
|
|
271
|
+
// a reopened port comes back with whatever lines the platform asserts
|
|
272
|
+
await this._applyIdleSignals(cfg);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Close/reopen with a concept's wire settings. Reopening an already-
|
|
277
|
+
* granted port needs no user gesture, only the first requestPort() does.
|
|
278
|
+
* A change in the idle DTR level alone (BMW-FAST after a K-line probe, or
|
|
279
|
+
* back) does not drop the port: only the lines move.
|
|
280
|
+
* @param {PortConfig} cfg - The settings the next telegram needs.
|
|
281
|
+
*/
|
|
282
|
+
async ensureConfig(cfg) {
|
|
283
|
+
if (this._configUnchanged(cfg)) {
|
|
284
|
+
const want = !!(cfg && cfg.dtr);
|
|
285
|
+
if (!!(this.config && this.config.dtr) !== want) {
|
|
286
|
+
this.config = { ...this.config, dtr: want };
|
|
287
|
+
await this._applyIdleSignals(this.config);
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
await this._releaseStreams();
|
|
292
|
+
await this._reopenStreams(cfg);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** @returns {string} 'USB vid:pid' when the port says, else 'serial'. */
|
|
296
|
+
portLabel() {
|
|
297
|
+
const i = this.port && this.port.getInfo ? this.port.getInfo() : {};
|
|
298
|
+
return i.usbVendorId
|
|
299
|
+
? `USB ${i.usbVendorId.toString(16)}:${(i.usbProductId || 0).toString(16)}`
|
|
300
|
+
: 'serial';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Release the streams, close the port and forget the wire state. */
|
|
304
|
+
async disconnect() {
|
|
305
|
+
await this._releaseStreams();
|
|
306
|
+
try {
|
|
307
|
+
if (this.port) await this.port.close();
|
|
308
|
+
} catch {
|
|
309
|
+
/* closing */
|
|
310
|
+
}
|
|
311
|
+
this.port = this.reader = this.writer = null;
|
|
312
|
+
this._resetWireState();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* KL30/KL15, exactly the way INPA gets them on this same K+DCAN cable.
|
|
317
|
+
*
|
|
318
|
+
* The reference serial interface (plain serial, which is what an FTDI
|
|
319
|
+
* K+DCAN cable is) derives BOTH battery and ignition from ONE modem line,
|
|
320
|
+
* DSR: BatteryVoltage = IgnitionVoltage = DSR asserted ? nominal : 0. A
|
|
321
|
+
* genuine K+DCAN cable wires KL15 through to the DSR pin, so DSR asserted
|
|
322
|
+
* means the ignition is on (report the nominal voltage), DSR low means off.
|
|
323
|
+
* UTILITY's STATUS_UBATT / STATUS_ZUENDUNG then just compare that against
|
|
324
|
+
* 10000 mV. So the honest reading is the DSR line, not a fixed nominal --
|
|
325
|
+
* and Web Serial exposes it as getSignals().dataSetReady.
|
|
326
|
+
*
|
|
327
|
+
* The separate ignition-status read (the 82 F1 F1 FA FA command) exists
|
|
328
|
+
* ONLY for the BT/WiFi/ELM adapters; for a serial FTDI cable EDIABAS falls
|
|
329
|
+
* to the DSR path and both lamps track that one line together. So do we:
|
|
330
|
+
* no cable can tell battery-on-ignition-off apart on this hardware, and
|
|
331
|
+
* pretending it can would be the invention, not the fidelity.
|
|
332
|
+
*
|
|
333
|
+
* Only when the port cannot report signals at all do we fall back to the
|
|
334
|
+
* old nominal-on (a cable we cannot query is not evidence of ignition off).
|
|
335
|
+
* @returns {Promise<BusState>}
|
|
336
|
+
*/
|
|
337
|
+
async readState() {
|
|
338
|
+
if (!this.connected) return { battery: null, ignition: null };
|
|
339
|
+
const on = await this._ignitionAsserted();
|
|
340
|
+
if (on === null) {
|
|
341
|
+
// the port cannot report its signals: fall back to the nominal "on",
|
|
342
|
+
// the same thing EDIABAS shows for an interface it cannot sample.
|
|
343
|
+
return {
|
|
344
|
+
battery: UTILITY_NOMINAL_MV / 1000,
|
|
345
|
+
ignition: true,
|
|
346
|
+
derived: true,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
battery: on ? UTILITY_NOMINAL_MV / 1000 : null,
|
|
351
|
+
ignition: on,
|
|
352
|
+
sensed: true,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* KL15 arrives on a modem line of the K+DCAN cable. The reference interface
|
|
358
|
+
* reads DSR; on real FTDI clones the ignition drives DSR AND DCD together
|
|
359
|
+
* (both seen going true with the key on and false with it out on this
|
|
360
|
+
* hardware), while RI sits high regardless (stuck, even unplugged) and CTS
|
|
361
|
+
* stays low. So take ignition as asserted when EITHER DSR or DCD is high --
|
|
362
|
+
* covering both wirings -- and ignore RI/CTS, which carry no ignition here.
|
|
363
|
+
* @returns {Promise<boolean|null>} true = ignition on, false = off, null =
|
|
364
|
+
* the port reports no signals (caller falls back to "on").
|
|
365
|
+
*/
|
|
366
|
+
async _ignitionAsserted() {
|
|
367
|
+
if (!this.port || !this.port.getSignals) return null;
|
|
368
|
+
try {
|
|
369
|
+
const s = await this.port.getSignals();
|
|
370
|
+
if (!s) return null;
|
|
371
|
+
const dsr = typeof s.dataSetReady === 'boolean' ? s.dataSetReady : null;
|
|
372
|
+
const dcd =
|
|
373
|
+
typeof s.dataCarrierDetect === 'boolean' ? s.dataCarrierDetect : null;
|
|
374
|
+
if (dsr === null && dcd === null) return null;
|
|
375
|
+
return !!dsr || !!dcd;
|
|
376
|
+
} catch {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// exchange() is inherited from SerialTransportBase (identical for every
|
|
382
|
+
// transport -- it delegates to the shared runExchange).
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* BMW K-line fast init: a 25 ms break with DTR held, the wake a KWP2000
|
|
386
|
+
* module needs before it listens.
|
|
387
|
+
*
|
|
388
|
+
* THE THING THAT WAS MISSING. An E46 K-line module ignores every telegram
|
|
389
|
+
* until it is woken, and the wake is NOT the 2-second 5-baud ISO 9141 init
|
|
390
|
+
* -- it is a 25 ms break. The reference interface does exactly this:
|
|
391
|
+
*
|
|
392
|
+
* DTR on -> break 25 ms -> break off -> wait to 50 ms total -> DTR off
|
|
393
|
+
*
|
|
394
|
+
* Verified against a real E46 M54/MS45: with the wake the DME answers
|
|
395
|
+
* 82 12 F1 1A 80 with its ident string ("754472129001060300400..."), and
|
|
396
|
+
* without it every telegram at every baud and parity is met with silence.
|
|
397
|
+
* That silence is what surfaced as IFH-0009 and looked like a wiring fault.
|
|
398
|
+
* The wake runs at 10400 8N1, which is also where the answer comes back.
|
|
399
|
+
*
|
|
400
|
+
* DTR is this cable's transmit enable: the reference interface asserts it
|
|
401
|
+
* for the wake and for the duration of every telegram it writes.
|
|
402
|
+
*
|
|
403
|
+
* THE 50 ms IS MEASURED FROM THE START OF THE BREAK, not added after it.
|
|
404
|
+
* The reference takes one timestamp, holds the break until start+25 ms,
|
|
405
|
+
* releases it, then waits until start+50 ms and drops DTR -- so the
|
|
406
|
+
* telegram follows ~50 ms after the break BEGAN. Sleeping 25 then another
|
|
407
|
+
* 25 makes that 50 ms of sleep PLUS the four awaits' own latency, and the
|
|
408
|
+
* trace showed 61 ms from break to write. A module with a strict post-wake
|
|
409
|
+
* window (W4 is 25-50 ms) has stopped listening by then. Deadline-based, so
|
|
410
|
+
* the wall clock matches whatever the awaits cost.
|
|
411
|
+
*
|
|
412
|
+
* Currently no concept calls this (see exchangeRaw: DS2 is never broken,
|
|
413
|
+
* ISO 9141 slow-inits); it stays for the KWP2000 modules that need it.
|
|
414
|
+
* @param {CommParams} comm - The telegram's wire parameters (unused; kept
|
|
415
|
+
* for symmetry with slowInit).
|
|
416
|
+
* @throws {Error} IFH-0018 when the port cannot drive its signals.
|
|
417
|
+
*/
|
|
418
|
+
async fastInit(comm) {
|
|
419
|
+
if (!this.port.setSignals) {
|
|
420
|
+
throw ifhError(
|
|
421
|
+
'IFH-0018',
|
|
422
|
+
'this browser cannot drive the K line ' +
|
|
423
|
+
'(no setSignals); use the macOS app for this ECU'
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
const t0 = Date.now();
|
|
427
|
+
const until = async (ms) => {
|
|
428
|
+
const left = ms - (Date.now() - t0);
|
|
429
|
+
if (left > 0) await bmwSleep(left);
|
|
430
|
+
};
|
|
431
|
+
await this.port.setSignals({ dataTerminalReady: true, break: true });
|
|
432
|
+
await until(FAST_INIT_BREAK_MS);
|
|
433
|
+
await this.port.setSignals({ break: false });
|
|
434
|
+
await until(FAST_INIT_DTR_MS);
|
|
435
|
+
await this.port.setSignals({ dataTerminalReady: false });
|
|
436
|
+
busTrace.add('kline', null, `fastInit done in ${Date.now() - t0}ms`);
|
|
437
|
+
this.inited = true;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* ISO 9141-2 slow init. The module sleeps. Waking it means holding the K
|
|
442
|
+
* line low/high by hand at 5 BITS PER SECOND -- one start bit, eight
|
|
443
|
+
* address bits LSB first, one stop bit, 200 ms each, 2 seconds in total. No
|
|
444
|
+
* UART can frame that, so it is bit-banged with setSignals({break}) and the
|
|
445
|
+
* port is reopened afterwards to discard the framing garbage the break
|
|
446
|
+
* generates.
|
|
447
|
+
*
|
|
448
|
+
* The ECU then answers 0x55 (sync) and two key bytes. The tester echoes
|
|
449
|
+
* back the SECOND key byte inverted, and the ECU replies with the address
|
|
450
|
+
* inverted -- at which point the session is live and normal requests work.
|
|
451
|
+
*
|
|
452
|
+
* Proven against a real E46 M54/MS45:
|
|
453
|
+
* addr 0x33 @ 10400 -> 55 08 08, ack f7 cc, then
|
|
454
|
+
* mode01 pid00 -> 48 6b 12 41 00 bf 9f e8 91 (0x12 = the DME)
|
|
455
|
+
*
|
|
456
|
+
* Init is per-session: `this.inited` holds the concept it was done for, so
|
|
457
|
+
* a job run does not re-init on every exchange (each one costs 2+ seconds)
|
|
458
|
+
* but switching ECU or concept does.
|
|
459
|
+
* @param {CommParams} comm - The telegram's wire parameters (its baud).
|
|
460
|
+
* @returns {Promise<{keyBytes: number[], ack: number[]}>} The ECU's key
|
|
461
|
+
* bytes and whatever came back after the tester's inverted key.
|
|
462
|
+
* @throws {Error} IFH-0018 without setSignals; IFH-0009 when no sync came.
|
|
463
|
+
*/
|
|
464
|
+
async slowInit(comm) {
|
|
465
|
+
const baud = (comm && comm.baud) || ISO9141_BAUD;
|
|
466
|
+
const addr = ISO9141_INIT_ADDR;
|
|
467
|
+
/** @type {PortConfig} */
|
|
468
|
+
const cfg = { baudRate: baud, dataBits: 8, stopBits: 1, parity: 'none' };
|
|
469
|
+
|
|
470
|
+
// Break signalling needs the port open; parity/data bits are irrelevant
|
|
471
|
+
// while the line is driven by hand.
|
|
472
|
+
await this.ensureConfig(cfg);
|
|
473
|
+
if (!this.port.setSignals) {
|
|
474
|
+
throw ifhError(
|
|
475
|
+
'IFH-0018',
|
|
476
|
+
'this browser cannot bit-bang the K line ' +
|
|
477
|
+
'(no setSignals); use the macOS app for this ECU'
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// start bit (low), 8 data bits LSB first, stop bit (high) -- 200 ms each
|
|
482
|
+
const bits = [0];
|
|
483
|
+
for (let i = 0; i < 8; i++) bits.push((addr >> i) & 1);
|
|
484
|
+
bits.push(1);
|
|
485
|
+
for (const b of bits) {
|
|
486
|
+
await this.port.setSignals({ break: b === 0 });
|
|
487
|
+
await bmwSleep(SLOW_INIT_BIT_MS);
|
|
488
|
+
}
|
|
489
|
+
await this.port.setSignals({ break: false });
|
|
490
|
+
|
|
491
|
+
// Reopen so the break's framing errors are not read as data.
|
|
492
|
+
await this.reopen(cfg);
|
|
493
|
+
|
|
494
|
+
// 0x55 then two key bytes, within ~300 ms of the stop bit
|
|
495
|
+
const hdr = await this._readUpTo(3, Date.now() + SLOW_INIT_SYNC_MS);
|
|
496
|
+
const sync = hdr.indexOf(ISO9141_SYNC);
|
|
497
|
+
if (sync < 0 || hdr.length < sync + 3) {
|
|
498
|
+
throw ifhError(
|
|
499
|
+
'IFH-0009',
|
|
500
|
+
'the ECU did not answer the slow init ' +
|
|
501
|
+
'(no 0x55 sync). Ignition on, engine off?'
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
const kb2 = hdr[sync + 2];
|
|
505
|
+
|
|
506
|
+
// Tester sends ~KB2; the ECU replies ~addr. W4 is 25-50 ms.
|
|
507
|
+
await bmwSleep(SLOW_INIT_W4_MS);
|
|
508
|
+
await this.writer.write(new Uint8Array([~kb2 & 0xff]));
|
|
509
|
+
const ack = await this._readUpTo(2, Date.now() + SLOW_INIT_ACK_MS);
|
|
510
|
+
// The ack carries our own echo plus ~addr; a missing one is not fatal --
|
|
511
|
+
// the E46 answered f7 cc where only cc is the ECU's. Requests that follow
|
|
512
|
+
// are the real proof, so do not fail the session on a fussy ack.
|
|
513
|
+
this.inited = true;
|
|
514
|
+
return { keyBytes: [hdr[sync + 1], kb2], ack };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Collect up to `n` bytes before `deadline`, stopping early if the port
|
|
519
|
+
* closes. Used by the slow init's sync and ack reads.
|
|
520
|
+
* @param {number} n - How many bytes to wait for.
|
|
521
|
+
* @param {number} deadline - Absolute Date.now() cut-off.
|
|
522
|
+
* @returns {Promise<number[]>} Whatever arrived (possibly fewer than n).
|
|
523
|
+
*/
|
|
524
|
+
async _readUpTo(n, deadline) {
|
|
525
|
+
const got = [];
|
|
526
|
+
while (got.length < n && Date.now() < deadline) {
|
|
527
|
+
const { value, done } = await this.readSome(deadline);
|
|
528
|
+
if (done) break;
|
|
529
|
+
if (value && value.length) got.push(...value);
|
|
530
|
+
}
|
|
531
|
+
return got;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Close and reopen the port, dropping anything buffered. Used after the
|
|
536
|
+
* slow init, whose break signalling leaves framing errors in the stream.
|
|
537
|
+
* @param {PortConfig} cfg - The settings to reopen with.
|
|
538
|
+
*/
|
|
539
|
+
async reopen(cfg) {
|
|
540
|
+
await this._releaseStreams();
|
|
541
|
+
this.pending = null;
|
|
542
|
+
await this._reopenStreams(cfg);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* How long DTR stays up for a K-line write: exactly the telegram's byte
|
|
547
|
+
* time plus DtrTimeCorrCom, never less than 1 ms.
|
|
548
|
+
* @param {number[]} framed - The bytes about to be written.
|
|
549
|
+
* @returns {number} Whole milliseconds.
|
|
550
|
+
*/
|
|
551
|
+
_dtrHoldMs(framed) {
|
|
552
|
+
const bits = this.config && this.config.parity === 'none' ? 10 : 11;
|
|
553
|
+
const ms =
|
|
554
|
+
(framed.length * bits * 1000) /
|
|
555
|
+
((this.config && this.config.baudRate) || KLINE_DEFAULT_BAUD);
|
|
556
|
+
return Math.max(1, Math.round(ms + DTR_TIME_CORR_MS));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Write one framed request (waking the module first when its concept needs
|
|
561
|
+
* it, with DTR as transmit enable on the K line) and read its answer.
|
|
562
|
+
*
|
|
563
|
+
* WHICH concepts need waking at all: ISO 9141 sleeps until its 5-baud
|
|
564
|
+
* address arrives; DS2 does not, and BMW-FAST over a D-CAN cable does not.
|
|
565
|
+
* `inited` gates the one that does, and survives until the port is
|
|
566
|
+
* reopened (which is what clears it): an SGBD's session init can run on
|
|
567
|
+
* one concept while its jobs declare another, so keying this to the
|
|
568
|
+
* per-telegram concept would wake for the init and not for the job that
|
|
569
|
+
* follows.
|
|
570
|
+
*
|
|
571
|
+
* DS2 IS NOT WOKEN. The reference interface's fast-init wake has exactly
|
|
572
|
+
* ONE call site -- inside its KWP2000 transmit -- and its DS2 transmit
|
|
573
|
+
* contains no wake, no break, no 5-baud address at all; the concept-5/6
|
|
574
|
+
* setup marks the ECU connected outright so nothing can trigger one.
|
|
575
|
+
* We were sending a 25 ms break before the first telegram to every K-line
|
|
576
|
+
* address, DS2 included. That is 240 bit-times of dominant K line at 9600
|
|
577
|
+
* -- to a module that never expected a fast init it is either framing
|
|
578
|
+
* garbage to resync through, or, on a module that also speaks KWP2000, a
|
|
579
|
+
* genuine wake pattern that arms a different session in which a raw DS2
|
|
580
|
+
* telegram is not valid and gets dropped without reply. The E46 cluster
|
|
581
|
+
* (0x80) forgives it; the EGS (0x32) answered EDIABAS and never us.
|
|
582
|
+
* ISO 9141 genuinely does need its 5-baud address, so that stays.
|
|
583
|
+
*
|
|
584
|
+
* DTR IS THE TRANSMIT ENABLE on a K+DCAN cable, and it is what was
|
|
585
|
+
* missing. The reference interface raises it for the duration of every
|
|
586
|
+
* telegram it writes, and DS2 sets its "send with DTR" flag. Without it the
|
|
587
|
+
* bytes are framed correctly, leave the UART, and never reach the K line --
|
|
588
|
+
* which is exactly the silence that looked like a dead ECU.
|
|
589
|
+
* @param {number[]|null} framed - The request with its checksum, or null
|
|
590
|
+
* to read a continuation frame without writing.
|
|
591
|
+
* @param {number} timeoutMs - ParTimeoutStd for this read.
|
|
592
|
+
* @param {CommParams} comm - The request's wire parameters.
|
|
593
|
+
* @returns {Promise<number[]>} The answer frame.
|
|
594
|
+
*/
|
|
595
|
+
async exchangeRaw(framed, timeoutMs, comm) {
|
|
596
|
+
const concept = conceptOf(comm);
|
|
597
|
+
const kline = isKline(concept) || isKline(this.sessionConcept);
|
|
598
|
+
// Which ECU this telegram addresses (DS2: the first byte). Kept for the
|
|
599
|
+
// trace and for the ISO 9141 wake, which IS per module.
|
|
600
|
+
const addr = framed && framed.length ? framed[0] & 0xff : null;
|
|
601
|
+
busTrace.add(
|
|
602
|
+
'kline',
|
|
603
|
+
null,
|
|
604
|
+
`addr=0x${addr == null ? '??' : addr.toString(16)}` +
|
|
605
|
+
` concept=0x${concept.toString(16)} kline=${kline}` +
|
|
606
|
+
` session=${this.sessionConcept} inited=${this.inited}` +
|
|
607
|
+
` initedAddr=${this.initedAddr}` +
|
|
608
|
+
` cfg=${this.config && this.config.baudRate}/${this.config && this.config.parity}`
|
|
609
|
+
);
|
|
610
|
+
const wantsWake = isIso9141(concept) || isIso9141(this.sessionConcept);
|
|
611
|
+
if (framed && !this.inited && wantsWake) {
|
|
612
|
+
await this.slowInit(comm);
|
|
613
|
+
if (addr != null) this.initedAddr = addr;
|
|
614
|
+
}
|
|
615
|
+
if (framed) {
|
|
616
|
+
// Drain anything stale before a fresh write -- the same start-clean
|
|
617
|
+
// the native path gets from serialFlush(). A late answer from a
|
|
618
|
+
// timed-out exchange would otherwise be read as this request's echo,
|
|
619
|
+
// fail the compare, and cascade IFH-0003 until the stream happens to
|
|
620
|
+
// run dry. readSome reports {done:false, value:null} when it merely ran
|
|
621
|
+
// out of time (done means the PORT closed), so the drain stops on
|
|
622
|
+
// either, bounded so a chattering bus cannot spin forever.
|
|
623
|
+
// (K-line writes drain INSIDE the DTR window instead -- see below.)
|
|
624
|
+
if (!kline) await this.drainBuffered();
|
|
625
|
+
if (kline && this.port.setSignals) {
|
|
626
|
+
// DTR up, THEN drain, THEN write -- the order the reference uses
|
|
627
|
+
// (its input discard sits inside the DTR block, right before the
|
|
628
|
+
// write). Draining earlier let bytes arrive in the gap and be read
|
|
629
|
+
// as this request's echo.
|
|
630
|
+
await this.port.setSignals({ dataTerminalReady: true });
|
|
631
|
+
await this.drainBuffered();
|
|
632
|
+
}
|
|
633
|
+
await this.writer.write(new Uint8Array(framed));
|
|
634
|
+
if (kline && this.port.setSignals) {
|
|
635
|
+
const holdMs = this._dtrHoldMs(framed);
|
|
636
|
+
busTrace.add(
|
|
637
|
+
'kline',
|
|
638
|
+
null,
|
|
639
|
+
`DTR held ${holdMs}ms for ${framed.length}B` +
|
|
640
|
+
` @${this.config && this.config.baudRate}/${this.config && this.config.parity}`
|
|
641
|
+
);
|
|
642
|
+
await bmwSleep(holdMs);
|
|
643
|
+
await this.port.setSignals({ dataTerminalReady: false });
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const deadline = Date.now() + timeoutMs;
|
|
647
|
+
return readFrame(
|
|
648
|
+
framed,
|
|
649
|
+
timeoutMs,
|
|
650
|
+
async () => {
|
|
651
|
+
const { value, done } = await this.readSome(deadline);
|
|
652
|
+
return done ? null : value;
|
|
653
|
+
},
|
|
654
|
+
comm
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Drain what is already buffered WITHOUT arming a new read.
|
|
660
|
+
*
|
|
661
|
+
* THE BUG THIS FIXES: the old drain called readSome() with a 2 ms deadline.
|
|
662
|
+
* readSome keeps a timed-out read alive on this.pending (that is what stops
|
|
663
|
+
* bytes being lost), so the drain's last call left a live read armed. The
|
|
664
|
+
* write then went out and THAT read swallowed the first bytes of the echo --
|
|
665
|
+
* every answer arrived missing its head ("12 04 00 16" came back as
|
|
666
|
+
* "00 16"), which readFrame then failed to match.
|
|
667
|
+
*
|
|
668
|
+
* NEVER CREATE A READ HERE. An earlier version probed with
|
|
669
|
+
* this.reader.read() when nothing was outstanding; if the line was quiet
|
|
670
|
+
* that probe stayed armed, the telegram went out, and the probe swallowed
|
|
671
|
+
* the first bytes of the echo. The tell was unmistakable in a wire trace:
|
|
672
|
+
* the FIRST attempt of every exchange came back missing its head
|
|
673
|
+
* ("82 12 f1 1a 80 1f" as "12 f1 1a 80 1f") while the retry -- which found
|
|
674
|
+
* a pending read already in place and so created none -- was perfect.
|
|
675
|
+
*
|
|
676
|
+
* Only an ALREADY-OUTSTANDING read is consumed, and only while it keeps
|
|
677
|
+
* resolving immediately. A quiet line leaves this a no-op.
|
|
678
|
+
*/
|
|
679
|
+
async drainBuffered() {
|
|
680
|
+
for (let i = 0; i < DRAIN_MAX_READS && this.pending; i++) {
|
|
681
|
+
const settled = await Promise.race([
|
|
682
|
+
this.pending.then((r) => ({ hit: true, r })),
|
|
683
|
+
bmwSleep(DRAIN_PROBE_MS).then(() => ({ hit: false })),
|
|
684
|
+
]);
|
|
685
|
+
if (!settled.hit) return; // still outstanding: leave it be
|
|
686
|
+
const { value, done } = settled.r || {};
|
|
687
|
+
if (done || !value || !value.length) return;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Read with a deadline, WITHOUT losing bytes to an abandoned read.
|
|
693
|
+
*
|
|
694
|
+
* THE BUG THIS FIXES: racing reader.read() against a timeout and walking
|
|
695
|
+
* away leaves that read outstanding. Web Serial still delivers the next
|
|
696
|
+
* chunk to it, and because nothing held the promise those bytes were gone
|
|
697
|
+
* for good. The pre-write drain loop used to run with a 2 ms deadline and
|
|
698
|
+
* so ALWAYS ended by timing out -- meaning every exchange armed an orphaned
|
|
699
|
+
* read immediately before writing, which then swallowed the K-line echo.
|
|
700
|
+
* readFrame waited the full timeout for bytes already eaten and threw
|
|
701
|
+
* IFH-0003 "no echo from the cable" on a cable that echoes perfectly.
|
|
702
|
+
*
|
|
703
|
+
* Keeping the single outstanding read on `this.pending` and awaiting that
|
|
704
|
+
* same promise next time means a timed-out read is resumed, not discarded.
|
|
705
|
+
* @param {number} deadline - Absolute Date.now() cut-off for this call.
|
|
706
|
+
* @returns {Promise<{value: Uint8Array|null|undefined, done: boolean}>}
|
|
707
|
+
* The read result; {value:null, done:false} when it merely timed out
|
|
708
|
+
* (done:true is reserved for the port closing).
|
|
709
|
+
*/
|
|
710
|
+
async readSome(deadline) {
|
|
711
|
+
const ms = Math.max(1, deadline - Date.now());
|
|
712
|
+
if (!this.pending) {
|
|
713
|
+
// Tag the read so a resolved value can be told from a stale handle.
|
|
714
|
+
this.pending = this.reader.read().then(
|
|
715
|
+
(r) => {
|
|
716
|
+
this.pending = null;
|
|
717
|
+
return r;
|
|
718
|
+
},
|
|
719
|
+
(e) => {
|
|
720
|
+
this.pending = null;
|
|
721
|
+
throw e;
|
|
722
|
+
}
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
// a worker-timed race: a late wake resolves an orphaned promise, nothing
|
|
726
|
+
// else, so there is no timer to clear
|
|
727
|
+
const timeout = bmwSleep(ms).then(() => TIMED_OUT);
|
|
728
|
+
const r = await Promise.race([this.pending, timeout]);
|
|
729
|
+
// Timed out: the read stays on this.pending for the next call. Report
|
|
730
|
+
// "nothing yet" rather than done -- done means the port closed.
|
|
731
|
+
if (r === TIMED_OUT) return { value: null, done: false };
|
|
732
|
+
return r;
|
|
733
|
+
}
|
|
734
|
+
}
|