Skip to content

Commit 9728474

Browse files
committed
Improve events
1 parent 8c10a46 commit 9728474

File tree

8 files changed

+526
-14
lines changed

8 files changed

+526
-14
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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,67 @@ 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 = this.checkIsNeedBatchWrite();
238+
239+
// Attempt to obtain writer/reader if available
240+
try {
241+
if (this.port?.writable && !this.writer) {
242+
this.writer = this.port.writable.getWriter();
243+
}
244+
} catch (e) {
245+
console.warn(`${logHead} Could not get writer from already-open port:`, e);
246+
}
247+
248+
try {
249+
if (this.port?.readable && !this.reader) {
250+
this.reader = this.port.readable.getReader();
251+
}
252+
} catch (e) {
253+
console.warn(`${logHead} Could not get reader from already-open port:`, e);
254+
}
255+
256+
if (connectionInfo) {
257+
this.connected = true;
258+
this.connectionId = path;
259+
this.bitrate = options.baudRate;
260+
this.bytesReceived = 0;
261+
this.bytesSent = 0;
262+
this.failed = 0;
263+
this.openRequested = false;
264+
265+
this.port.addEventListener("disconnect", this.handleDisconnect);
266+
this.addEventListener("receive", this.handleReceiveBytes);
267+
268+
console.log(`${logHead} Recovered already-open serial port, ID: ${this.connectionId}`);
269+
this.dispatchEvent(new CustomEvent("connect", { detail: connectionInfo }));
270+
271+
// Start read loop if not already running
272+
if (!this.reading) {
273+
this.reading = true;
274+
this.readLoop();
275+
}
276+
277+
return true;
278+
}
279+
} catch (e) {
280+
console.warn(`${logHead} Failed to recover already-open port:`, e);
281+
}
282+
}
283+
223284
this.openRequested = false;
224285
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
225286
return false;

src/js/serial.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,47 @@ 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+
200+
// Wait for either a 'connect' or 'disconnect' event dispatched
201+
// at the Serial level, then re-evaluate state.
202+
await new Promise((resolve) => {
203+
const onConnect = () => {
204+
cleanup();
205+
resolve(true);
206+
};
207+
const onDisconnect = () => {
208+
cleanup();
209+
resolve(false);
210+
};
211+
const cleanup = () => {
212+
try {
213+
this.removeEventListener("connect", onConnect);
214+
this.removeEventListener("disconnect", onDisconnect);
215+
} catch (e) {
216+
/* ignore */
217+
}
218+
};
219+
220+
this.addEventListener("connect", onConnect, { once: true });
221+
this.addEventListener("disconnect", onDisconnect, { once: true });
222+
});
223+
224+
// After the in-flight attempt finished, if we're already connected to
225+
// the requested port, return success. Otherwise, fall-through and
226+
// attempt to connect now that the protocol is no longer in the
227+
// 'openRequested' state.
228+
const connectedPort = this._protocol.getConnectedPort?.();
229+
if (this._protocol.connected && connectedPort && connectedPort.path === path) {
230+
console.log(`${this.logHead} Already connected to the requested port after waiting`);
231+
return true;
232+
}
233+
}
234+
194235
console.log(`${this.logHead} Connecting to port:`, path, "with options:", options);
195236
return this._protocol.connect(path, options, callback);
196237
}

src/js/tabs/firmware_flasher.js

Lines changed: 172 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import { EventBus } from "../../components/eventBus";
2121
import { ispConnected } from "../utils/connection.js";
2222
import FC from "../fc";
2323

24+
const PORT_CHANGE_DEBOUNCE_MS = 500;
25+
const AUTO_DETECT_DELAY_MS = 1000;
26+
2427
const firmware_flasher = {
2528
targets: null,
2629
buildApi: new BuildApi(),
@@ -40,6 +43,62 @@ const firmware_flasher = {
4043
// Properties to preserve firmware state during flashing
4144
preFlashingMessage: null,
4245
preFlashingMessageType: null,
46+
// Minimal module-scoped handler references for cleanup to use
47+
// Module-level handler implementations so tests and early callers can
48+
// invoke them before `initialize()` binds the full DOM-aware versions.
49+
detectedUsbDevice: function () {
50+
// If a reboot-resume or flash-on-connect flow is active, we may need to
51+
// resume flashing. Otherwise USB attaches don't auto-detect a board.
52+
const isFlashOnConnect = $("input.flash_on_connect").is(":checked") || false;
53+
54+
// If another connect/operation is in progress, don't interfere.
55+
if (GUI.connect_lock) return;
56+
57+
// If we are resuming from a reboot, clear the connect lock and resume
58+
// flashing. For flash-on-connect, only start flashing when not locked.
59+
if (STM32.rebootMode || isFlashOnConnect) {
60+
const wasReboot = !!STM32.rebootMode;
61+
STM32.rebootMode = 0;
62+
// Only clear the global connect lock when we are resuming from a reboot
63+
// so we don't accidentally interrupt another active operation.
64+
if (wasReboot) {
65+
GUI.connect_lock = false;
66+
}
67+
firmware_flasher?.startFlashing();
68+
}
69+
},
70+
detectedSerialDevice: function () {
71+
// Serial device connect should trigger auto-detect
72+
AutoDetect.verifyBoard();
73+
},
74+
onPortChange: function (port) {
75+
// Debounce rapid port events
76+
if (firmware_flasher.portChangeTimer) {
77+
clearTimeout(firmware_flasher.portChangeTimer);
78+
firmware_flasher.portChangeTimer = null;
79+
}
80+
81+
if (GUI.connect_lock) return;
82+
83+
if (port && port !== "0" && !STM32?.rebootMode) {
84+
firmware_flasher.portChangeTimer = setTimeout(() => {
85+
firmware_flasher.portChangeTimer = null;
86+
AutoDetect.verifyBoard();
87+
}, PORT_CHANGE_DEBOUNCE_MS);
88+
} else if (!port || port === "0") {
89+
$('select[name="board"]').val("0").trigger("change");
90+
}
91+
},
92+
onDeviceRemoved: function () {
93+
// Avoid clearing when removal is expected during flashing/reboot
94+
if (GUI.connect_lock || STM32.rebootMode) {
95+
return;
96+
}
97+
$('select[name="board"]').val("0").trigger("change");
98+
firmware_flasher?.clearBufferedFirmware();
99+
},
100+
// Single debounce timer shared across instances
101+
portChangeTimer: null,
43102
};
44103

45104
firmware_flasher.initialize = async function (callback) {
@@ -615,6 +674,10 @@ firmware_flasher.initialize = async function (callback) {
615674
self.filename = null;
616675
}
617676

677+
// Expose the implementation to the module so module-scoped handlers that
678+
// may run before initialize() completes can call it safely.
679+
firmware_flasher.clearBufferedFirmware = clearBufferedFirmware;
680+
618681
$('select[name="board"]').select2();
619682
$('select[name="osdProtocols"]').select2();
620683
$('select[name="radioProtocols"]').select2();
@@ -742,20 +805,91 @@ firmware_flasher.initialize = async function (callback) {
742805
return output.join("").split("\n");
743806
}
744807

745-
function detectedUsbDevice(device) {
808+
firmware_flasher.detectedUsbDevice = function (device) {
746809
const isFlashOnConnect = $("input.flash_on_connect").is(":checked");
747810

748811
console.log(`${self.logHead} Detected USB device:`, device);
749812
console.log(`${self.logHead} Reboot mode: %s, flash on connect`, STM32.rebootMode, isFlashOnConnect);
750813

814+
// If another operation is in progress, ignore port events (unless we're resuming from a reboot)
815+
if (GUI.connect_lock && !STM32.rebootMode) {
816+
console.log(`${self.logHead} Port event ignored due to active operation (connect_lock)`);
817+
return;
818+
}
819+
820+
// Proceed if we're resuming a reboot sequence or if flash-on-connect is enabled and no operation is active
751821
if (STM32.rebootMode || isFlashOnConnect) {
822+
const wasReboot = !!STM32.rebootMode;
752823
STM32.rebootMode = 0;
753-
GUI.connect_lock = false;
824+
// Only clear the global connect lock when we are resuming from a reboot
825+
// so we don't accidentally interrupt another active operation.
826+
if (wasReboot) {
827+
GUI.connect_lock = false;
828+
}
754829
startFlashing();
755830
}
756-
}
831+
};
832+
833+
firmware_flasher.detectedSerialDevice = function () {
834+
AutoDetect.verifyBoard();
835+
};
757836

758-
EventBus.$on("port-handler:auto-select-usb-device", detectedUsbDevice);
837+
firmware_flasher.onPortChange = function (port) {
838+
// Clear any pending debounce timer so rapid port events don't re-enter the handler.
839+
if (firmware_flasher.portChangeTimer) {
840+
clearTimeout(firmware_flasher.portChangeTimer);
841+
firmware_flasher.portChangeTimer = null;
842+
}
843+
844+
console.log(`${self.logHead} Port changed to:`, port);
845+
846+
if (GUI.connect_lock) {
847+
console.log(`${self.logHead} Port change ignored during active operation (connect_lock set)`);
848+
return;
849+
}
850+
851+
// Auto-detect board when port changes and we're on firmware flasher tab
852+
if (port && port !== "0" && $("input.flash_on_connect").is(":checked") === false && !STM32.rebootMode) {
853+
console.log(`${self.logHead} Auto-detecting board for port change (debounced)`);
854+
855+
// Debounced verification: re-check connect lock when the timeout fires
856+
firmware_flasher.portChangeTimer = setTimeout(() => {
857+
firmware_flasher.portChangeTimer = null;
858+
if (GUI.connect_lock) {
859+
console.log(`${self.logHead} Skipping auto-detect due to active operation at timeout`);
860+
return;
861+
}
862+
AutoDetect.verifyBoard();
863+
}, PORT_CHANGE_DEBOUNCE_MS); // Small delay to ensure port is ready
864+
} else if (!port || port === "0") {
865+
if (!GUI.connect_lock) {
866+
console.log(`${self.logHead} Clearing board selection - no port selected`);
867+
$('select[name="board"]').val("0").trigger("change");
868+
} else {
869+
console.log(`${self.logHead} Not clearing board selection because operation in progress`);
870+
}
871+
}
872+
};
873+
874+
firmware_flasher.onDeviceRemoved = function (devicePath) {
875+
console.log(`${self.logHead} Device removed:`, devicePath);
876+
877+
// If a flashing operation or reboot is in progress, the device may be
878+
// removed intentionally (it reboots into DFU). Don't clear selection or
879+
// buffered firmware in that case as it would prevent the flashing flow.
880+
if (GUI.connect_lock || STM32.rebootMode) {
881+
console.log(`${self.logHead} Device removal during active operation/reboot; skipping clear`);
882+
return;
883+
}
884+
885+
$('select[name="board"]').val("0").trigger("change");
886+
clearBufferedFirmware();
887+
};
888+
889+
EventBus.$on("port-handler:auto-select-usb-device", firmware_flasher.detectedUsbDevice);
890+
EventBus.$on("port-handler:auto-select-serial-device", firmware_flasher.detectedSerialDevice);
891+
EventBus.$on("ports-input:change", firmware_flasher.onPortChange);
892+
EventBus.$on("port-handler:device-removed", firmware_flasher.onDeviceRemoved);
759893

760894
async function saveFirmware() {
761895
const fileType = self.firmware_type;
@@ -1389,6 +1523,11 @@ firmware_flasher.initialize = async function (callback) {
13891523
}
13901524
}
13911525

1526+
// Expose the local startFlashing implementation to module callers/tests so
1527+
// module-scoped handlers can safely call firmware_flasher.startFlashing()
1528+
// even if those callers ran before initialize() completed.
1529+
firmware_flasher.startFlashing = startFlashing;
1530+
13921531
$("a.flash_firmware").on("click", async function () {
13931532
if (GUI.connect_lock) {
13941533
return;
@@ -1472,6 +1611,17 @@ firmware_flasher.initialize = async function (callback) {
14721611
$("a.exit_dfu").removeClass("disabled");
14731612
}
14741613

1614+
// Auto-detect board if drone is already connected when tab becomes active
1615+
if (
1616+
(PortHandler.portAvailable && !$('select[name="board"]').val()) ||
1617+
$('select[name="board"]').val() === "0"
1618+
) {
1619+
console.log(`${self.logHead} Auto-detecting board for already connected device`);
1620+
setTimeout(() => {
1621+
AutoDetect.verifyBoard();
1622+
}, AUTO_DETECT_DELAY_MS); // Small delay to ensure tab is fully loaded
1623+
}
1624+
14751625
GUI.content_ready(callback);
14761626
}
14771627

@@ -1490,6 +1640,24 @@ firmware_flasher.cleanup = function (callback) {
14901640
$(document).unbind("keypress");
14911641
$(document).off("click", "span.progressLabel a");
14921642

1643+
const cleanupHandler = (evt, property) => {
1644+
const handler = firmware_flasher[property];
1645+
if (handler) {
1646+
EventBus.$off(evt, handler);
1647+
}
1648+
};
1649+
1650+
cleanupHandler("port-handler:auto-select-usb-device", "detectedUsbDevice");
1651+
cleanupHandler("port-handler:auto-select-serial-device", "detectedSerialDevice");
1652+
cleanupHandler("ports-input:change", "onPortChange");
1653+
cleanupHandler("port-handler:device-removed", "onDeviceRemoved");
1654+
1655+
// Clear any pending debounce timer so it cannot fire after cleanup
1656+
if (firmware_flasher.portChangeTimer) {
1657+
clearTimeout(firmware_flasher.portChangeTimer);
1658+
firmware_flasher.portChangeTimer = null;
1659+
}
1660+
14931661
if (callback) callback();
14941662
};
14951663

0 commit comments

Comments
 (0)