@@ -96,7 +96,7 @@ export class DAPWrapper {
9696 this . initialConnectionComplete = true ;
9797 }
9898
99- await this . daplink . connect ( ) ;
99+ await this . connectWithRetry ( ) ;
100100 await this . cortexM . connect ( ) ;
101101
102102 this . logging . event ( {
@@ -154,6 +154,83 @@ export class DAPWrapper {
154154 throw lastError ;
155155 }
156156
157+ /**
158+ * Drain any stale responses from the USB buffer.
159+ * Sends a known command (DAP_INFO) and keeps reading until we get the
160+ * matching response. This recovers from the state where a previous session
161+ * was closed mid-serial-read, leaving stale responses in the device's
162+ * USB buffer that break subsequent connections.
163+ *
164+ * See: https://github.com/microbit-foundation/python-editor-v3/issues/89
165+ */
166+ private async drainStaleResponses ( ) : Promise < void > {
167+ // DAPLink's DAP_PACKET_COUNT is 5-8 for micro:bit variants.
168+ // In practice, only 1-2 stale responses are typical (from interrupted serial read).
169+ // Use a value slightly above the max buffer size as a safety margin.
170+ const maxAttempts = 10 ;
171+
172+ for ( let attempt = 0 ; attempt < maxAttempts ; attempt ++ ) {
173+ // DAP_INFO is a safe, read-only command that always succeeds
174+ const packet = [ DapCmd . DAP_INFO , 0x01 ] ;
175+ await this . transport . write ( Uint8Array . from ( packet ) . buffer ) ;
176+
177+ const response = await this . transport . read ( ) ;
178+ const responseBytes = new Uint8Array ( response . buffer ) ;
179+
180+ if ( responseBytes [ 0 ] === DapCmd . DAP_INFO ) {
181+ this . logging . log ( "USB buffer drain: synchronized" ) ;
182+ return ;
183+ }
184+ this . logging . log (
185+ `USB buffer drain: discarded stale response 0x${ responseBytes [ 0 ] . toString ( 16 ) } , expected 0x${ DapCmd . DAP_INFO . toString ( 16 ) } ` ,
186+ ) ;
187+ }
188+
189+ this . logging . log (
190+ "USB buffer drain: warning - could not fully synchronize after max attempts" ,
191+ ) ;
192+ }
193+
194+ /**
195+ * Connect to daplink with retry logic for stale response recovery.
196+ * If a "Bad response" error occurs (indicating stale USB data from a
197+ * previous session), this method will drain the stale responses and retry.
198+ */
199+ private async connectWithRetry ( maxRetries = 3 ) : Promise < void > {
200+ let lastError : Error | undefined ;
201+
202+ for ( let attempt = 0 ; attempt < maxRetries ; attempt ++ ) {
203+ try {
204+ if ( attempt > 0 ) {
205+ this . logging . log (
206+ `Connection retry attempt ${ attempt + 1 } /${ maxRetries } ` ,
207+ ) ;
208+ await this . drainStaleResponses ( ) ;
209+ }
210+
211+ await this . daplink . connect ( ) ;
212+ return ;
213+ } catch ( e ) {
214+ // https://github.com/ARMmbed/dapjs/blob/master/src/proxy/cmsis-dap.ts#L178
215+ if ( e instanceof Error && / ^ B a d r e s p o n s e f o r / . test ( e . message ) ) {
216+ lastError = e ;
217+ this . logging . log ( `Bad response error during connect: ${ e . message } ` ) ;
218+
219+ try {
220+ await this . transport . close ( ) ;
221+ } catch {
222+ // Ignore close errors
223+ }
224+ await this . transport . open ( ) ;
225+ continue ;
226+ }
227+ throw e ;
228+ }
229+ }
230+
231+ throw lastError || new Error ( "Connection failed after retries" ) ;
232+ }
233+
157234 async startSerial ( listener : ( data : string ) => void ) : Promise < void > {
158235 const currentBaud = await this . daplink . getSerialBaudrate ( ) ;
159236 if ( currentBaud !== 115200 ) {
0 commit comments