@@ -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,84 @@ const firmware_flasher = {
4043 // Properties to preserve firmware state during flashing
4144 preFlashingMessage : null ,
4245 preFlashingMessageType : null ,
46+ // Single debounce timer shared across instances
47+ portChangeTimer : null ,
48+ logHead : "[FIRMWARE_FLASHER]" ,
49+ // Event handlers to allow removal on tab change
50+ detectedUsbDevice : function ( device ) {
51+ const isFlashOnConnect = $ ( "input.flash_on_connect" ) . is ( ":checked" ) ;
52+
53+ console . log ( `${ this . logHead } Detected USB device:` , device ) ;
54+ console . log ( `${ this . logHead } Reboot mode: %s, flash on connect` , STM32 . rebootMode , isFlashOnConnect ) ;
55+
56+ // If another operation is in progress, ignore port events (unless we're resuming from a reboot)
57+ if ( GUI . connect_lock && ! STM32 . rebootMode ) {
58+ console . log ( `${ this . logHead } Port event ignored due to active operation (connect_lock)` ) ;
59+ return ;
60+ }
61+
62+ // Proceed if we're resuming a reboot sequence or if flash-on-connect is enabled and no operation is active
63+ if ( STM32 . rebootMode || isFlashOnConnect ) {
64+ const wasReboot = ! ! STM32 . rebootMode ;
65+ STM32 . rebootMode = 0 ;
66+ // Only clear the global connect lock when we are resuming from a reboot
67+ // so we don't accidentally interrupt another active operation.
68+ if ( wasReboot ) {
69+ GUI . connect_lock = false ;
70+ }
71+ this . startFlashing ?. ( ) ;
72+ }
73+ } ,
74+ detectedSerialDevice : function ( ) {
75+ AutoDetect . verifyBoard ( ) ;
76+ } ,
77+ onPortChange : function ( port ) {
78+ // Clear any pending debounce timer so rapid port events don't re-enter the handler.
79+ if ( firmware_flasher . portChangeTimer ) {
80+ clearTimeout ( firmware_flasher . portChangeTimer ) ;
81+ firmware_flasher . portChangeTimer = null ;
82+ }
83+
84+ console . log ( `${ self . logHead } Port changed to:` , port ) ;
85+
86+ if ( GUI . connect_lock ) {
87+ console . log ( `${ this . logHead } Port change ignored during active operation (connect_lock set)` ) ;
88+ return ;
89+ }
90+
91+ // Auto-detect board when port changes and we're on firmware flasher tab
92+ if ( port && port !== "0" && $ ( "input.flash_on_connect" ) . is ( ":checked" ) === false && ! STM32 . rebootMode ) {
93+ console . log ( `${ self . logHead } Auto-detecting board for port change (debounced)` ) ;
94+
95+ // Debounced verification: re-check connect lock when the timeout fires
96+ firmware_flasher . portChangeTimer = setTimeout ( ( ) => {
97+ firmware_flasher . portChangeTimer = null ;
98+ if ( GUI . connect_lock ) {
99+ console . log ( `${ this . logHead } Skipping auto-detect due to active operation at timeout` ) ;
100+ return ;
101+ }
102+ AutoDetect . verifyBoard ( ) ;
103+ } , PORT_CHANGE_DEBOUNCE_MS ) ;
104+ } else if ( ! port || port === "0" ) {
105+ if ( ! GUI . connect_lock ) {
106+ console . log ( `${ this . logHead } Clearing board selection - no port selected` ) ;
107+ $ ( 'select[name="board"]' ) . val ( "0" ) . trigger ( "change" ) ;
108+ } else {
109+ console . log ( `${ this . logHead } Not clearing board selection because operation in progress` ) ;
110+ }
111+ }
112+ } ,
113+ onDeviceRemoved : function ( devicePath ) {
114+ console . log ( `${ this . logHead } Device removed:` , devicePath ) ;
115+
116+ // Avoid clearing when removal is expected during flashing/reboot
117+ if ( GUI . connect_lock || STM32 . rebootMode ) {
118+ return ;
119+ }
120+
121+ $ ( 'select[name="board"]' ) . val ( "0" ) . trigger ( "change" ) ;
122+ this . clearBufferedFirmware ?. ( ) ;
123+ } ,
43124} ;
44125
45126firmware_flasher . initialize = async function ( callback ) {
@@ -60,8 +141,6 @@ firmware_flasher.initialize = async function (callback) {
60141 self . intel_hex = undefined ;
61142 self . parsed_hex = undefined ;
62143
63- self . logHead = "[FIRMWARE_FLASHER]" ;
64-
65144 function getExtension ( key ) {
66145 if ( ! key ) {
67146 return undefined ;
@@ -742,20 +821,10 @@ firmware_flasher.initialize = async function (callback) {
742821 return output . join ( "" ) . split ( "\n" ) ;
743822 }
744823
745- function detectedUsbDevice ( device ) {
746- const isFlashOnConnect = $ ( "input.flash_on_connect" ) . is ( ":checked" ) ;
747-
748- console . log ( `${ self . logHead } Detected USB device:` , device ) ;
749- console . log ( `${ self . logHead } Reboot mode: %s, flash on connect` , STM32 . rebootMode , isFlashOnConnect ) ;
750-
751- if ( STM32 . rebootMode || isFlashOnConnect ) {
752- STM32 . rebootMode = 0 ;
753- GUI . connect_lock = false ;
754- startFlashing ( ) ;
755- }
756- }
757-
758- EventBus . $on ( "port-handler:auto-select-usb-device" , detectedUsbDevice ) ;
824+ EventBus . $on ( "port-handler:auto-select-usb-device" , firmware_flasher . detectedUsbDevice ) ;
825+ EventBus . $on ( "port-handler:auto-select-serial-device" , firmware_flasher . detectedSerialDevice ) ;
826+ EventBus . $on ( "ports-input:change" , firmware_flasher . onPortChange ) ;
827+ EventBus . $on ( "port-handler:device-removed" , firmware_flasher . onDeviceRemoved ) ;
759828
760829 async function saveFirmware ( ) {
761830 const fileType = self . firmware_type ;
@@ -1389,6 +1458,12 @@ firmware_flasher.initialize = async function (callback) {
13891458 }
13901459 }
13911460
1461+ // Expose the local startFlashing implementation to module callers/tests so
1462+ // module-scoped handlers can safely call firmware_flasher.startFlashing()
1463+ // even if those callers ran before initialize() completed.
1464+ firmware_flasher . startFlashing = startFlashing ;
1465+ firmware_flasher . clearBufferedFirmware = clearBufferedFirmware ;
1466+
13921467 $ ( "a.flash_firmware" ) . on ( "click" , async function ( ) {
13931468 if ( GUI . connect_lock ) {
13941469 return ;
@@ -1472,6 +1547,17 @@ firmware_flasher.initialize = async function (callback) {
14721547 $ ( "a.exit_dfu" ) . removeClass ( "disabled" ) ;
14731548 }
14741549
1550+ // Auto-detect board if drone is already connected when tab becomes active
1551+ if (
1552+ ( PortHandler . portAvailable && ! $ ( 'select[name="board"]' ) . val ( ) ) ||
1553+ $ ( 'select[name="board"]' ) . val ( ) === "0"
1554+ ) {
1555+ console . log ( `${ self . logHead } Auto-detecting board for already connected device` ) ;
1556+ setTimeout ( ( ) => {
1557+ AutoDetect . verifyBoard ( ) ;
1558+ } , AUTO_DETECT_DELAY_MS ) ;
1559+ }
1560+
14751561 GUI . content_ready ( callback ) ;
14761562 }
14771563
@@ -1490,6 +1576,24 @@ firmware_flasher.cleanup = function (callback) {
14901576 $ ( document ) . unbind ( "keypress" ) ;
14911577 $ ( document ) . off ( "click" , "span.progressLabel a" ) ;
14921578
1579+ const cleanupHandler = ( evt , property ) => {
1580+ const handler = firmware_flasher [ property ] ;
1581+ if ( handler ) {
1582+ EventBus . $off ( evt , handler ) ;
1583+ }
1584+ } ;
1585+
1586+ cleanupHandler ( "port-handler:auto-select-usb-device" , "detectedUsbDevice" ) ;
1587+ cleanupHandler ( "port-handler:auto-select-serial-device" , "detectedSerialDevice" ) ;
1588+ cleanupHandler ( "ports-input:change" , "onPortChange" ) ;
1589+ cleanupHandler ( "port-handler:device-removed" , "onDeviceRemoved" ) ;
1590+
1591+ // Clear any pending debounce timer so it cannot fire after cleanup
1592+ if ( firmware_flasher . portChangeTimer ) {
1593+ clearTimeout ( firmware_flasher . portChangeTimer ) ;
1594+ firmware_flasher . portChangeTimer = null ;
1595+ }
1596+
14931597 if ( callback ) callback ( ) ;
14941598} ;
14951599
0 commit comments