@@ -21,6 +21,9 @@ import { EventBus } from "../../components/eventBus";
2121import { ispConnected } from "../utils/connection.js" ;
2222import FC from "../fc" ;
2323
24+ const PORT_CHANGE_DEBOUNCE_MS = 500 ;
25+ const AUTO_DETECT_DELAY_MS = 1000 ;
26+
2427const firmware_flasher = {
2528 targets : null ,
2629 buildApi : new BuildApi ( ) ,
@@ -40,6 +43,55 @@ 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+ if ( STM32 . rebootMode || isFlashOnConnect ) {
54+ STM32 . rebootMode = 0 ;
55+ GUI . connect_lock = false ;
56+ firmware_flasher . startFlashing ( ) ;
57+ }
58+ } ,
59+ detectedSerialDevice : function ( ) {
60+ // Serial device connect should trigger auto-detect
61+ AutoDetect . verifyBoard ( ) ;
62+ } ,
63+ onPortChange : function ( port ) {
64+ // Debounce rapid port events
65+ if ( firmware_flasher . portChangeTimer ) {
66+ clearTimeout ( firmware_flasher . portChangeTimer ) ;
67+ firmware_flasher . portChangeTimer = null ;
68+ }
69+
70+ if ( GUI . connect_lock ) return ;
71+
72+ if ( port && port !== "0" && ! ( STM32 && STM32 . rebootMode ) ) {
73+ firmware_flasher . portChangeTimer = setTimeout ( ( ) => {
74+ firmware_flasher . portChangeTimer = null ;
75+ if ( ! GUI . connect_lock ) {
76+ AutoDetect . verifyBoard ( ) ;
77+ }
78+ } , PORT_CHANGE_DEBOUNCE_MS ) ;
79+ } else if ( ! port || port === "0" ) {
80+ if ( ! GUI . connect_lock ) {
81+ $ ( 'select[name="board"]' ) . val ( "0" ) . trigger ( "change" ) ;
82+ }
83+ }
84+ } ,
85+ onDeviceRemoved : function ( ) {
86+ // Avoid clearing when removal is expected during flashing/reboot
87+ if ( GUI . connect_lock || STM32 . rebootMode ) {
88+ return ;
89+ }
90+ $ ( 'select[name="board"]' ) . val ( "0" ) . trigger ( "change" ) ;
91+ firmware_flasher . clearBufferedFirmware ( ) ;
92+ } ,
93+ // Single debounce timer shared across instances
94+ portChangeTimer : null ,
4395} ;
4496
4597firmware_flasher . initialize = async function ( callback ) {
@@ -615,6 +667,10 @@ firmware_flasher.initialize = async function (callback) {
615667 self . filename = null ;
616668 }
617669
670+ // Expose the implementation to the module so module-scoped handlers that
671+ // may run before initialize() completes can call it safely.
672+ firmware_flasher . clearBufferedFirmware = clearBufferedFirmware ;
673+
618674 $ ( 'select[name="board"]' ) . select2 ( ) ;
619675 $ ( 'select[name="osdProtocols"]' ) . select2 ( ) ;
620676 $ ( 'select[name="radioProtocols"]' ) . select2 ( ) ;
@@ -742,20 +798,91 @@ firmware_flasher.initialize = async function (callback) {
742798 return output . join ( "" ) . split ( "\n" ) ;
743799 }
744800
745- function detectedUsbDevice ( device ) {
801+ firmware_flasher . detectedUsbDevice = function ( device ) {
746802 const isFlashOnConnect = $ ( "input.flash_on_connect" ) . is ( ":checked" ) ;
747803
748804 console . log ( `${ self . logHead } Detected USB device:` , device ) ;
749805 console . log ( `${ self . logHead } Reboot mode: %s, flash on connect` , STM32 . rebootMode , isFlashOnConnect ) ;
750806
807+ // If another operation is in progress, ignore port events (unless we're resuming from a reboot)
808+ if ( GUI . connect_lock && ! STM32 . rebootMode ) {
809+ console . log ( `${ self . logHead } Port event ignored due to active operation (connect_lock)` ) ;
810+ return ;
811+ }
812+
813+ // Proceed if we're resuming a reboot sequence or if flash-on-connect is enabled and no operation is active
751814 if ( STM32 . rebootMode || isFlashOnConnect ) {
815+ const wasReboot = ! ! STM32 . rebootMode ;
752816 STM32 . rebootMode = 0 ;
753- GUI . connect_lock = false ;
817+ // Only clear the global connect lock when we are resuming from a reboot
818+ // so we don't accidentally interrupt another active operation.
819+ if ( wasReboot ) {
820+ GUI . connect_lock = false ;
821+ }
754822 startFlashing ( ) ;
755823 }
756- }
824+ } ;
825+
826+ firmware_flasher . detectedSerialDevice = function ( ) {
827+ AutoDetect . verifyBoard ( ) ;
828+ } ;
829+
830+ firmware_flasher . onPortChange = function ( port ) {
831+ // Clear any pending debounce timer so rapid port events don't re-enter the handler.
832+ if ( firmware_flasher . portChangeTimer ) {
833+ clearTimeout ( firmware_flasher . portChangeTimer ) ;
834+ firmware_flasher . portChangeTimer = null ;
835+ }
836+
837+ console . log ( `${ self . logHead } Port changed to:` , port ) ;
838+
839+ if ( GUI . connect_lock ) {
840+ console . log ( `${ self . logHead } Port change ignored during active operation (connect_lock set)` ) ;
841+ return ;
842+ }
843+
844+ // Auto-detect board when port changes and we're on firmware flasher tab
845+ if ( port && port !== "0" && $ ( "input.flash_on_connect" ) . is ( ":checked" ) === false && ! STM32 . rebootMode ) {
846+ console . log ( `${ self . logHead } Auto-detecting board for port change (debounced)` ) ;
847+
848+ // Debounced verification: re-check connect lock when the timeout fires
849+ firmware_flasher . portChangeTimer = setTimeout ( ( ) => {
850+ firmware_flasher . portChangeTimer = null ;
851+ if ( GUI . connect_lock ) {
852+ console . log ( `${ self . logHead } Skipping auto-detect due to active operation at timeout` ) ;
853+ return ;
854+ }
855+ AutoDetect . verifyBoard ( ) ;
856+ } , PORT_CHANGE_DEBOUNCE_MS ) ; // Small delay to ensure port is ready
857+ } else if ( ! port || port === "0" ) {
858+ if ( ! GUI . connect_lock ) {
859+ console . log ( `${ self . logHead } Clearing board selection - no port selected` ) ;
860+ $ ( 'select[name="board"]' ) . val ( "0" ) . trigger ( "change" ) ;
861+ } else {
862+ console . log ( `${ self . logHead } Not clearing board selection because operation in progress` ) ;
863+ }
864+ }
865+ } ;
757866
758- EventBus . $on ( "port-handler:auto-select-usb-device" , detectedUsbDevice ) ;
867+ firmware_flasher . onDeviceRemoved = function ( devicePath ) {
868+ console . log ( `${ self . logHead } Device removed:` , devicePath ) ;
869+
870+ // If a flashing operation or reboot is in progress, the device may be
871+ // removed intentionally (it reboots into DFU). Don't clear selection or
872+ // buffered firmware in that case as it would prevent the flashing flow.
873+ if ( GUI . connect_lock || STM32 . rebootMode ) {
874+ console . log ( `${ self . logHead } Device removal during active operation/reboot; skipping clear` ) ;
875+ return ;
876+ }
877+
878+ $ ( 'select[name="board"]' ) . val ( "0" ) . trigger ( "change" ) ;
879+ clearBufferedFirmware ( ) ;
880+ } ;
881+
882+ EventBus . $on ( "port-handler:auto-select-usb-device" , firmware_flasher . detectedUsbDevice ) ;
883+ EventBus . $on ( "port-handler:auto-select-serial-device" , firmware_flasher . detectedSerialDevice ) ;
884+ EventBus . $on ( "ports-input:change" , firmware_flasher . onPortChange ) ;
885+ EventBus . $on ( "port-handler:device-removed" , firmware_flasher . onDeviceRemoved ) ;
759886
760887 async function saveFirmware ( ) {
761888 const fileType = self . firmware_type ;
@@ -1389,6 +1516,11 @@ firmware_flasher.initialize = async function (callback) {
13891516 }
13901517 }
13911518
1519+ // Expose the local startFlashing implementation to module callers/tests so
1520+ // module-scoped handlers can safely call firmware_flasher.startFlashing()
1521+ // even if those callers ran before initialize() completed.
1522+ firmware_flasher . startFlashing = startFlashing ;
1523+
13921524 $ ( "a.flash_firmware" ) . on ( "click" , async function ( ) {
13931525 if ( GUI . connect_lock ) {
13941526 return ;
@@ -1472,6 +1604,17 @@ firmware_flasher.initialize = async function (callback) {
14721604 $ ( "a.exit_dfu" ) . removeClass ( "disabled" ) ;
14731605 }
14741606
1607+ // Auto-detect board if drone is already connected when tab becomes active
1608+ if (
1609+ ( PortHandler . portAvailable && ! $ ( 'select[name="board"]' ) . val ( ) ) ||
1610+ $ ( 'select[name="board"]' ) . val ( ) === "0"
1611+ ) {
1612+ console . log ( `${ self . logHead } Auto-detecting board for already connected device` ) ;
1613+ setTimeout ( ( ) => {
1614+ AutoDetect . verifyBoard ( ) ;
1615+ } , AUTO_DETECT_DELAY_MS ) ; // Small delay to ensure tab is fully loaded
1616+ }
1617+
14751618 GUI . content_ready ( callback ) ;
14761619 }
14771620
@@ -1490,6 +1633,24 @@ firmware_flasher.cleanup = function (callback) {
14901633 $ ( document ) . unbind ( "keypress" ) ;
14911634 $ ( document ) . off ( "click" , "span.progressLabel a" ) ;
14921635
1636+ const cleanupHandler = ( evt , property ) => {
1637+ const handler = firmware_flasher [ property ] ;
1638+ if ( handler ) {
1639+ EventBus . $off ( evt , handler ) ;
1640+ }
1641+ } ;
1642+
1643+ cleanupHandler ( "port-handler:auto-select-usb-device" , "detectedUsbDevice" ) ;
1644+ cleanupHandler ( "port-handler:auto-select-serial-device" , "detectedSerialDevice" ) ;
1645+ cleanupHandler ( "ports-input:change" , "onPortChange" ) ;
1646+ cleanupHandler ( "port-handler:device-removed" , "onDeviceRemoved" ) ;
1647+
1648+ // Clear any pending debounce timer so it cannot fire after cleanup
1649+ if ( firmware_flasher . portChangeTimer ) {
1650+ clearTimeout ( firmware_flasher . portChangeTimer ) ;
1651+ firmware_flasher . portChangeTimer = null ;
1652+ }
1653+
14931654 if ( callback ) callback ( ) ;
14941655} ;
14951656
0 commit comments