Skip to content

Commit 45422a5

Browse files
committed
Improve events
1 parent 8c10a46 commit 45422a5

File tree

10 files changed

+631
-28
lines changed

10 files changed

+631
-28
lines changed

src/js/msp/MSPHelper.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1779,13 +1779,17 @@ MspHelper.prototype.process_data = function (dataHandler) {
17791779

17801780
// remove object from array
17811781
dataHandler.callbacks.splice(i, 1);
1782-
if (!crcError) {
1783-
// fire callback
1784-
if (callback) {
1782+
// Always invoke callback and pass crcError flag. Previously callbacks were skipped
1783+
// when crcError was true; tests and callers expect the callback to still be notified.
1784+
if (callback) {
1785+
try {
17851786
callback({ command: code, data: data, length: data.byteLength, crcError: crcError });
1787+
} catch (e) {
1788+
console.error(`callback for code ${code} threw:`, e);
17861789
}
1787-
} else {
1788-
console.warn(`code: ${code} - crc failed. No callback`);
1790+
}
1791+
if (crcError) {
1792+
console.warn(`code: ${code} - callback invoked despite crc failure`);
17891793
}
17901794
}
17911795
}

src/js/protocols/WebSerial.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,90 @@ class WebSerial extends EventTarget {
220220
}
221221
} catch (error) {
222222
console.error(`${logHead} Error connecting:`, error);
223+
224+
// If the port is already open (InvalidStateError) we can attempt to
225+
// recover by attaching to the already-open port. Some browser
226+
// implementations throw when open() is called concurrently but the
227+
// underlying port is already usable. Try to initialize reader/writer
228+
// and proceed as a successful connection.
229+
if (
230+
error &&
231+
(error.name === "InvalidStateError" ||
232+
(typeof error.message === "string" && error.message.includes("already open")))
233+
) {
234+
try {
235+
const connectionInfo = this.port?.getInfo ? this.port.getInfo() : null;
236+
this.connectionInfo = connectionInfo;
237+
this.isNeedBatchWrite = connectionInfo ? this.checkIsNeedBatchWrite() : false;
238+
// Attempt to obtain writer/reader if available
239+
try {
240+
if (this.port?.writable && !this.writer) {
241+
this.writer = this.port.writable.getWriter();
242+
}
243+
} catch (e) {
244+
console.warn(`${logHead} Could not get writer from already-open port:`, e);
245+
}
246+
247+
try {
248+
if (this.port?.readable && !this.reader) {
249+
this.reader = this.port.readable.getReader();
250+
}
251+
} catch (e) {
252+
console.warn(`${logHead} Could not get reader from already-open port:`, e);
253+
}
254+
255+
if (connectionInfo) {
256+
if (!this.writer || !this.reader) {
257+
console.warn(`${logHead} Recovered port missing reader/writer`);
258+
// Release partial writer/reader locks before aborting recovery
259+
if (this.writer) {
260+
try {
261+
this.writer.releaseLock();
262+
} catch (releaseErr) {
263+
console.warn(`${logHead} Failed to release partial writer lock:`, releaseErr);
264+
}
265+
this.writer = null;
266+
}
267+
if (this.reader) {
268+
try {
269+
this.reader.releaseLock?.();
270+
} catch (releaseErr) {
271+
console.warn(`${logHead} Failed to release partial reader lock:`, releaseErr);
272+
}
273+
this.reader = null;
274+
}
275+
// Reset request state and signal failure to callers instead of throwing
276+
this.openRequested = false;
277+
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
278+
return false;
279+
}
280+
this.connected = true;
281+
this.connectionId = path;
282+
this.bitrate = options.baudRate;
283+
this.bytesReceived = 0;
284+
this.bytesSent = 0;
285+
this.failed = 0;
286+
this.openRequested = false;
287+
288+
this.port.addEventListener("disconnect", this.handleDisconnect);
289+
this.addEventListener("receive", this.handleReceiveBytes);
290+
291+
console.log(`${logHead} Recovered already-open serial port, ID: ${this.connectionId}`);
292+
this.dispatchEvent(new CustomEvent("connect", { detail: connectionInfo }));
293+
294+
// Start read loop if not already running
295+
if (!this.reading) {
296+
this.reading = true;
297+
this.readLoop();
298+
}
299+
300+
return true;
301+
}
302+
} catch (e) {
303+
console.warn(`${logHead} Failed to recover already-open port:`, e);
304+
}
305+
}
306+
223307
this.openRequested = false;
224308
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
225309
return false;

src/js/serial.js

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,70 @@ class Serial extends EventTarget {
191191
return this._protocol.connect(path, options, callback);
192192
}
193193

194+
// If a connect/open is already in progress on the protocol, wait for it
195+
// to finish instead of returning immediately. This avoids race
196+
// conditions while allowing callers to await connection completion.
197+
if (this._protocol.openRequested) {
198+
console.warn(`${this.logHead} Protocol connection already in progress, waiting for completion`);
199+
// Wait for either a 'connect' or 'disconnect' event dispatched
200+
// at the Serial level, then re-evaluate state.
201+
const waited = await new Promise((resolve) => {
202+
const onConnect = () => {
203+
cleanup();
204+
resolve(true);
205+
};
206+
const onDisconnect = () => {
207+
cleanup();
208+
resolve(false);
209+
};
210+
const cleanup = () => {
211+
try {
212+
this.removeEventListener("connect", onConnect);
213+
this.removeEventListener("disconnect", onDisconnect);
214+
} catch {
215+
/* ignore */
216+
}
217+
};
218+
const timer = setTimeout(() => {
219+
cleanup();
220+
resolve(null);
221+
}, 5000);
222+
const wrap =
223+
(fn) =>
224+
(...args) => {
225+
clearTimeout(timer);
226+
fn(...args);
227+
};
228+
this.addEventListener("connect", wrap(onConnect), { once: true });
229+
this.addEventListener("disconnect", wrap(onDisconnect), { once: true });
230+
});
231+
232+
// Optional: if timed out, re-evaluate state to proceed safely
233+
if (waited === null && this._protocol.connected && this._protocol.getConnectedPort?.()?.path === path) {
234+
return true;
235+
}
236+
237+
// After the in-flight attempt finished, if we're already connected to
238+
// the requested port, return success. Otherwise, fall-through and
239+
// attempt to connect now that the protocol is no longer in the
240+
// 'openRequested' state.
241+
const connectedPort = this._protocol.getConnectedPort?.();
242+
if (this._protocol.connected && connectedPort?.path === path) {
243+
console.log(`${this.logHead} Already connected to the requested port after waiting`);
244+
return true;
245+
}
246+
247+
// If connected to a different port after waiting, disconnect first
248+
if (this._protocol.connected && connectedPort?.path !== path) {
249+
console.log(`${this.logHead} Connected to different port after waiting, disconnecting first`);
250+
const success = await this.disconnect();
251+
if (!success) {
252+
console.error(`${this.logHead} Failed to disconnect before reconnecting`);
253+
return false;
254+
}
255+
}
256+
}
257+
194258
console.log(`${this.logHead} Connecting to port:`, path, "with options:", options);
195259
return this._protocol.connect(path, options, callback);
196260
}
@@ -228,7 +292,9 @@ class Serial extends EventTarget {
228292
if (callback) {
229293
callback(false);
230294
}
231-
return Promise.resolve(false);
295+
// This function is async, so returning a plain value resolves the
296+
// promise with that value. No need to wrap with Promise.resolve().
297+
return false;
232298
}
233299
}
234300

0 commit comments

Comments
 (0)