Skip to content

Commit a728ae7

Browse files
Attempt a workaround for the bad response issue
See microbit-foundation/python-editor-v3#89 Ultimately this needs a fix in DAPLink to clear the queue on reset but that won't have much impact on existing devices.
1 parent c01107d commit a728ae7

File tree

2 files changed

+90
-88
lines changed

2 files changed

+90
-88
lines changed

lib/usb-device-wrapper.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export class DAPWrapper {
9696
this.initialConnectionComplete = true;
9797
}
9898

99-
await this.daplink.connect();
99+
await this.connectDaplink();
100100
await this.cortexM.connect();
101101

102102
this.logging.event({
@@ -154,6 +154,90 @@ 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 is the response to our first DAP_INFO (from attempt 0).
182+
// We sent additional DAP_INFO commands in subsequent attempts while
183+
// reading stale responses - read out those responses too.
184+
for (let i = 0; i < attempt; i++) {
185+
await this.transport.read();
186+
}
187+
this.logging.log(
188+
`USB buffer drain: synchronized after ${attempt} stale response(s)`,
189+
);
190+
return;
191+
}
192+
this.logging.log(
193+
`USB buffer drain: discarded stale response 0x${responseBytes[0].toString(16)}`,
194+
);
195+
}
196+
197+
this.logging.log(
198+
"USB buffer drain: warning - could not fully synchronize after max attempts",
199+
);
200+
}
201+
202+
/**
203+
* Connect daplink, handling stale USB responses from a previous session.
204+
* See: https://github.com/microbit-foundation/python-editor-v3/issues/89
205+
*/
206+
private async connectDaplink(maxRetries = 3): Promise<void> {
207+
let lastError: Error | undefined;
208+
209+
for (let attempt = 0; attempt < maxRetries; attempt++) {
210+
try {
211+
if (attempt > 0) {
212+
this.logging.log(
213+
`Connection retry attempt ${attempt + 1}/${maxRetries}`,
214+
);
215+
await this.drainStaleResponses();
216+
}
217+
218+
await this.daplink.connect();
219+
return;
220+
} catch (e) {
221+
// https://github.com/ARMmbed/dapjs/blob/master/src/proxy/cmsis-dap.ts#L178
222+
if (e instanceof Error && /^Bad response for /.test(e.message)) {
223+
lastError = e;
224+
this.logging.log(`Bad response error during connect: ${e.message}`);
225+
226+
try {
227+
await this.transport.close();
228+
} catch {
229+
// Ignore close errors
230+
}
231+
await this.transport.open();
232+
continue;
233+
}
234+
throw e;
235+
}
236+
}
237+
238+
throw lastError || new Error("Connection failed after retries");
239+
}
240+
157241
async startSerial(listener: (data: string) => void): Promise<void> {
158242
const currentBaud = await this.daplink.getSerialBaudrate();
159243
if (currentBaud !== 115200) {

lib/usb.ts

Lines changed: 5 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -142,62 +142,6 @@ class MicrobitWebUSBConnectionImpl
142142
};
143143

144144
private flashing: boolean = false;
145-
private disconnectAfterFlash: boolean = false;
146-
private visibilityReconnect: boolean = false;
147-
private visibilityChangeListener = () => {
148-
if (document.visibilityState === "visible") {
149-
if (
150-
this.visibilityReconnect &&
151-
this.status !== ConnectionStatus.CONNECTED
152-
) {
153-
this.disconnectAfterFlash = false;
154-
this.visibilityReconnect = false;
155-
if (!this.flashing) {
156-
this.log("Reconnecting visible tab");
157-
this.connect();
158-
}
159-
}
160-
} else {
161-
if (!this.unloading && this.status === ConnectionStatus.CONNECTED) {
162-
if (!this.flashing) {
163-
this.log("Disconnecting hidden tab");
164-
this.disconnect().then(() => {
165-
this.visibilityReconnect = true;
166-
});
167-
} else {
168-
this.log("Scheduling disconnect of hidden tab for after flash");
169-
this.disconnectAfterFlash = true;
170-
}
171-
}
172-
}
173-
};
174-
175-
private unloading = false;
176-
177-
private beforeUnloadListener = () => {
178-
// If serial is in progress when the page unloads with V1 DAPLink 0254 or V2 0255
179-
// then it'll fail to reconnect with mismatched command/response errors.
180-
// Try hard to disconnect as a workaround.
181-
// https://github.com/microbit-foundation/python-editor-v3/issues/89
182-
this.unloading = true;
183-
this.stopSerialInternal();
184-
// The user might stay on the page if they have unsaved changes and there's another beforeunload listener.
185-
window.addEventListener(
186-
"focus",
187-
() => {
188-
const assumePageIsStayingOpenDelay = 1000;
189-
setTimeout(() => {
190-
if (this.status === ConnectionStatus.CONNECTED) {
191-
this.unloading = false;
192-
if (this.addedListeners.serialdata) {
193-
this.startSerialInternal();
194-
}
195-
}
196-
}, assumePageIsStayingOpenDelay);
197-
},
198-
{ once: true },
199-
);
200-
};
201145

202146
private logging: Logging;
203147
private deviceSelectionMode: DeviceSelectionMode;
@@ -221,30 +165,12 @@ class MicrobitWebUSBConnectionImpl
221165
if (navigator.usb) {
222166
navigator.usb.addEventListener("disconnect", this.handleDisconnect);
223167
}
224-
if (typeof window !== "undefined") {
225-
window.addEventListener("beforeunload", this.beforeUnloadListener);
226-
if (window.document) {
227-
window.document.addEventListener(
228-
"visibilitychange",
229-
this.visibilityChangeListener,
230-
);
231-
}
232-
}
233168
}
234169

235170
dispose() {
236171
if (navigator.usb) {
237172
navigator.usb.removeEventListener("disconnect", this.handleDisconnect);
238173
}
239-
if (typeof window !== "undefined") {
240-
window.removeEventListener("beforeunload", this.beforeUnloadListener);
241-
if (window.document) {
242-
window.document.removeEventListener(
243-
"visibilitychange",
244-
this.visibilityChangeListener,
245-
);
246-
}
247-
}
248174
}
249175

250176
setRequestDeviceExclusionFilters(exclusionFilters: USBDeviceFilter[]) {
@@ -331,18 +257,11 @@ class MicrobitWebUSBConnectionImpl
331257
} finally {
332258
progress(undefined, wasPartial);
333259

334-
if (this.disconnectAfterFlash) {
335-
this.log("Disconnecting after flash due to tab visibility");
336-
this.disconnectAfterFlash = false;
337-
await this.disconnect();
338-
this.visibilityReconnect = true;
339-
} else {
340-
if (this.addedListeners.serialdata) {
341-
this.log("Reinstating serial after flash");
342-
if (this.connection.daplink) {
343-
await this.connection.daplink.connect();
344-
await this.startSerialInternal();
345-
}
260+
if (this.addedListeners.serialdata) {
261+
this.log("Reinstating serial after flash");
262+
if (this.connection.daplink) {
263+
await this.connection.daplink.connect();
264+
await this.startSerialInternal();
346265
}
347266
}
348267
}
@@ -408,7 +327,6 @@ class MicrobitWebUSBConnectionImpl
408327

409328
private setStatus(newStatus: ConnectionStatus) {
410329
this.status = newStatus;
411-
this.visibilityReconnect = false;
412330
this.log("USB connection status " + newStatus);
413331
this.dispatchTypedEvent("status", new ConnectionStatusEvent(newStatus));
414332
}

0 commit comments

Comments
 (0)