Skip to content

Commit 1c2a42f

Browse files
Check for serial connection lost (#213)
* Reconnect and reset micro:bit when connection is lost * Stop checking for connection when USB is unplugged
1 parent 3ef6c1f commit 1c2a42f

File tree

3 files changed

+103
-1
lines changed

3 files changed

+103
-1
lines changed

src/script/microbit-interfacing/MicrobitSerial.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* SPDX-License-Identifier: MIT
55
*/
66

7-
import { logError } from '../utils/logging';
7+
import { logError, logMessage } from '../utils/logging';
88
import MicrobitConnection, { DeviceRequestStates } from './MicrobitConnection';
99
import MicrobitUSB from './MicrobitUSB';
1010
import { onAccelerometerChange, onButtonChange } from './change-listeners';
@@ -16,13 +16,17 @@ import {
1616
stateOnFailedToConnect,
1717
stateOnReady,
1818
} from './state-updaters';
19+
import StaticConfiguration from '../../StaticConfiguration';
1920

2021
export class MicrobitSerial implements MicrobitConnection {
2122
private responseMap = new Map<
2223
number,
2324
(value: protocol.MessageResponse | PromiseLike<protocol.MessageResponse>) => void
2425
>();
2526

27+
// To avoid concurrent connect attempts
28+
private isConnecting: boolean = false;
29+
2630
// TODO: The radio frequency should be randomly generated once per session.
2731
// If we want a session to be restored (e.g. from local storage) and
2832
// the previously flashed micro:bits to continue working without
@@ -31,10 +35,18 @@ export class MicrobitSerial implements MicrobitConnection {
3135
// to configure the radio frequency for both micro:bits after they
3236
// are flashed, not just the radio bridge.
3337
private sessionRadioFrequency = 42;
38+
private connectionCheckIntervalId: ReturnType<typeof setInterval> | undefined;
39+
private lastReceivedMessageTimestamp: number | undefined;
3440

3541
constructor(private usb: MicrobitUSB) {}
3642

3743
async connect(...states: DeviceRequestStates[]): Promise<void> {
44+
logMessage('Serial connect', states);
45+
if (this.isConnecting) {
46+
logMessage('Skipping connect attempt when one is already in progress');
47+
return;
48+
}
49+
this.isConnecting = true;
3850
let unprocessedData = '';
3951
let previousButtonState = { A: 0, B: 0 };
4052

@@ -46,6 +58,8 @@ export class MicrobitSerial implements MicrobitConnection {
4658
const messages = protocol.splitMessages(unprocessedData + data);
4759
unprocessedData = messages.remainingInput;
4860
messages.messages.forEach(async msg => {
61+
this.lastReceivedMessageTimestamp = Date.now();
62+
4963
// Messages are either periodic sensor data or command/response
5064
const sensorData = protocol.processPeriodicMessage(msg);
5165
if (sensorData) {
@@ -80,6 +94,26 @@ export class MicrobitSerial implements MicrobitConnection {
8094
await this.handshake();
8195
stateOnConnected(DeviceRequestStates.INPUT);
8296

97+
// Check for USB being unplugged
98+
navigator.usb.addEventListener('disconnect', () => {
99+
logMessage('USB disconnected');
100+
this.stopConnectionCheck();
101+
});
102+
103+
// Check for connection lost
104+
if (this.connectionCheckIntervalId === undefined) {
105+
this.connectionCheckIntervalId = setInterval(async () => {
106+
const allowedTimeWithoutMessageInMs =
107+
StaticConfiguration.connectTimeoutDuration;
108+
if (
109+
this.lastReceivedMessageTimestamp &&
110+
Date.now() - this.lastReceivedMessageTimestamp > allowedTimeWithoutMessageInMs
111+
) {
112+
await this.handleReconnect();
113+
}
114+
}, 1000);
115+
}
116+
83117
// Set the radio frequency to a value unique to this session
84118
const radioFreqCommand = protocol.generateCmdRadioFrequency(
85119
this.sessionRadioFrequency,
@@ -104,26 +138,55 @@ export class MicrobitSerial implements MicrobitConnection {
104138

105139
stateOnAssigned(DeviceRequestStates.INPUT, this.usb.getModelNumber());
106140
stateOnReady(DeviceRequestStates.INPUT);
141+
logMessage('Serial successfully connected');
107142
} catch (e) {
108143
logError('Failed to initialise serial protocol', e);
109144
stateOnFailedToConnect(DeviceRequestStates.INPUT);
110145
await this.usb.stopSerial();
111146
throw e;
147+
} finally {
148+
this.isConnecting = false;
112149
}
113150
}
114151

115152
async disconnect(): Promise<void> {
153+
this.stopConnectionCheck();
116154
return this.disconnectInternal(true);
117155
}
118156

157+
private stopConnectionCheck() {
158+
clearInterval(this.connectionCheckIntervalId);
159+
this.connectionCheckIntervalId = undefined;
160+
this.lastReceivedMessageTimestamp = undefined;
161+
}
162+
119163
private async disconnectInternal(userDisconnect: boolean): Promise<void> {
120164
// We might want to send command to stop streaming here?
121165
this.responseMap.clear();
122166
await this.usb.stopSerial();
123167
stateOnDisconnected(DeviceRequestStates.INPUT, userDisconnect);
124168
}
125169

170+
async handleReconnect(): Promise<void> {
171+
if (this.isConnecting) {
172+
logMessage('Serial disconnect ignored... reconnect already in progress');
173+
return;
174+
}
175+
try {
176+
this.stopConnectionCheck();
177+
logMessage('Serial disconnected... automatically trying to reconnect');
178+
await this.usb.softwareReset();
179+
await this.usb.stopSerial();
180+
await this.reconnect();
181+
} catch (e) {
182+
logError('Serial connect triggered by disconnect listener failed', e);
183+
} finally {
184+
this.isConnecting = false;
185+
}
186+
}
187+
126188
async reconnect(): Promise<void> {
189+
logMessage('Serial reconnect');
127190
await this.connect(DeviceRequestStates.INPUT);
128191
}
129192

src/script/microbit-interfacing/MicrobitUSB.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { CortexM, DAPLink, WebUSB } from 'dapjs';
88
import MBSpecs from './MBSpecs';
99
import { HexType, getHexFileUrl } from './Microbits';
1010
import { logError } from '../utils/logging';
11+
import { CortexSpecialReg } from './constants';
1112

1213
const baudRate = 115200;
1314
const serialDelay = 5;
@@ -98,6 +99,27 @@ class MicrobitUSB {
9899
}
99100
}
100101

102+
/**
103+
* Resets the micro:bit in software by writing to NVIC_AIRCR.
104+
* Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/cortex/cortex.ts#L347
105+
*/
106+
public async softwareReset(): Promise<void> {
107+
const cortexM = new CortexM(this.transport);
108+
await cortexM.connect();
109+
await cortexM.writeMem32(
110+
CortexSpecialReg.NVIC_AIRCR,
111+
CortexSpecialReg.NVIC_AIRCR_VECTKEY | CortexSpecialReg.NVIC_AIRCR_SYSRESETREQ,
112+
);
113+
114+
// wait for the system to come out of reset
115+
let dhcsr = await cortexM.readMem32(CortexSpecialReg.DHCSR);
116+
117+
while ((dhcsr & CortexSpecialReg.S_RESET_ST) !== 0) {
118+
dhcsr = await cortexM.readMem32(CortexSpecialReg.DHCSR);
119+
}
120+
await cortexM.disconnect();
121+
}
122+
101123
/**
102124
* Flashes a .hex file to the micro:bit.
103125
* @param {string} hex The hex file to flash. (As a link)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* (c) 2023, Center for Computational Thinking and Design at Aarhus University and contributors
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
7+
export const CortexSpecialReg = {
8+
// Debug Halting Control and Status Register
9+
DHCSR: 0xe000edf0,
10+
S_RESET_ST: 1 << 25,
11+
12+
NVIC_AIRCR: 0xe000ed0c,
13+
NVIC_AIRCR_VECTKEY: 0x5fa << 16,
14+
NVIC_AIRCR_SYSRESETREQ: 1 << 2,
15+
16+
// Many more.
17+
};

0 commit comments

Comments
 (0)