Merge pull request #1763 from evhan55/extensions/disconnect-errors

Various fixes to extension disconnect errors
This commit is contained in:
Eric Rosenbaum 2019-01-18 16:58:45 -05:00 committed by GitHub
commit 0b251adace
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 182 additions and 62 deletions

View file

@ -558,6 +558,8 @@ class Runtime extends EventEmitter {
/**
* Event name for updating the available set of peripheral devices.
* This causes the peripheral connection modal to update a list of
* available peripherals.
* @const {string}
*/
static get PERIPHERAL_LIST_UPDATE () {
@ -566,14 +568,25 @@ class Runtime extends EventEmitter {
/**
* Event name for reporting that a peripheral has connected.
* This causes the status button in the blocks menu to indicate 'connected'.
* @const {string}
*/
static get PERIPHERAL_CONNECTED () {
return 'PERIPHERAL_CONNECTED';
}
/**
* Event name for reporting that a peripheral has been intentionally disconnected.
* This causes the status button in the blocks menu to indicate 'disconnected'.
* @const {string}
*/
static get PERIPHERAL_DISCONNECTED () {
return 'PERIPHERAL_DISCONNECTED';
}
/**
* Event name for reporting that a peripheral has encountered a request error.
* This causes the peripheral connection modal to switch to an error state.
* @const {string}
*/
static get PERIPHERAL_REQUEST_ERROR () {
@ -581,15 +594,17 @@ class Runtime extends EventEmitter {
}
/**
* Event name for reporting that a peripheral has encountered a disconnect error.
* Event name for reporting that a peripheral connection has been lost.
* This causes a 'peripheral connection lost' error alert to display.
* @const {string}
*/
static get PERIPHERAL_DISCONNECT_ERROR () {
return 'PERIPHERAL_DISCONNECT_ERROR';
static get PERIPHERAL_CONNECTION_LOST_ERROR () {
return 'PERIPHERAL_CONNECTION_LOST_ERROR';
}
/**
* Event name for reporting that a peripheral has not been discovered.
* This causes the peripheral connection modal to show a timeout state.
* @const {string}
*/
static get PERIPHERAL_SCAN_TIMEOUT () {

View file

@ -476,6 +476,7 @@ class EV3 {
this._bt = null;
this._runtime.registerPeripheralExtension(extensionId, this);
this.disconnect = this.disconnect.bind(this);
this._onConnect = this._onConnect.bind(this);
this._onMessage = this._onMessage.bind(this);
this._pollValues = this._pollValues.bind(this);
@ -561,7 +562,7 @@ class EV3 {
this._bt = new BT(this._runtime, this._extensionId, {
majorDeviceClass: 8,
minorDeviceClass: 1
}, this._onConnect, this._onMessage);
}, this._onConnect, this.disconnect, this._onMessage);
}
/**

View file

@ -25,8 +25,12 @@ const BLECommand = {
CMD_DISPLAY_LED: 0x82
};
// TODO: Needs comment
const BLETimeout = 4500; // TODO: might need tweaking based on how long the peripheral takes to start sending data
/**
* A time interval to wait (in milliseconds) before reporting to the BLE socket
* that data has stopped coming from the peripheral.
*/
const BLETimeout = 4500;
/**
* A time interval to wait (in milliseconds) while a block that sends a BLE message is running.
@ -34,6 +38,12 @@ const BLETimeout = 4500; // TODO: might need tweaking based on how long the peri
*/
const BLESendInterval = 100;
/**
* A string to report to the BLE socket when the micro:bit has stopped receiving data.
* @type {string}
*/
const BLEDataStoppedError = 'micro:bit extension stopped receiving data';
/**
* Enum for micro:bit protocol.
* https://github.com/LLK/scratch-microbit-firmware/blob/master/protocol.md
@ -212,7 +222,7 @@ class MicroBit {
filters: [
{services: [BLEUUID.service]}
]
}, this._onConnect);
}, this._onConnect, this.disconnect);
}
/**
@ -289,7 +299,10 @@ class MicroBit {
*/
_onConnect () {
this._ble.read(BLEUUID.service, BLEUUID.rxChar, true, this._onMessage);
this._timeoutID = window.setInterval(this.disconnect, BLETimeout);
this._timeoutID = window.setInterval(
() => this._ble.handleDisconnectError(BLEDataStoppedError),
BLETimeout
);
}
/**
@ -317,7 +330,10 @@ class MicroBit {
// cancel disconnect timeout and start a new one
window.clearInterval(this._timeoutID);
this._timeoutID = window.setInterval(this.disconnect, BLETimeout);
this._timeoutID = window.setInterval(
() => this._ble.handleDisconnectError(BLEDataStoppedError),
BLETimeout
);
}
/**

View file

@ -40,11 +40,18 @@ const BLEService = {
*/
const BLECharacteristic = {
ATTACHED_IO: '00001527-1212-efde-1523-785feabcd123',
LOW_VOLTAGE_ALERT: '00001528-1212-efde-1523-785feabcd123',
INPUT_VALUES: '00001560-1212-efde-1523-785feabcd123',
INPUT_COMMAND: '00001563-1212-efde-1523-785feabcd123',
OUTPUT_COMMAND: '00001565-1212-efde-1523-785feabcd123'
};
/**
* A time interval to wait (in milliseconds) in between battery check calls.
* @type {number}
*/
const BLEBatteryCheckInterval = 5000;
/**
* A time interval to wait (in milliseconds) while a block that sends a BLE message is running.
* @type {number}
@ -421,8 +428,17 @@ class WeDo2 {
*/
this._rateLimiter = new RateLimiter(BLESendRateMax);
/**
* An interval id for the battery check interval.
* @type {number}
* @private
*/
this._batteryLevelIntervalId = null;
this.disconnect = this.disconnect.bind(this);
this._onConnect = this._onConnect.bind(this);
this._onMessage = this._onMessage.bind(this);
this._checkBatteryLevel = this._checkBatteryLevel.bind(this);
}
/**
@ -578,7 +594,7 @@ class WeDo2 {
services: [BLEService.DEVICE_SERVICE]
}],
optionalServices: [BLEService.IO_SERVICE]
}, this._onConnect);
}, this._onConnect, this.disconnect);
}
/**
@ -606,6 +622,11 @@ class WeDo2 {
if (this._ble) {
this._ble.disconnect();
}
if (this._batteryLevelIntervalId) {
window.clearInterval(this._batteryLevelIntervalId);
this._batteryLevelIntervalId = null;
}
}
/**
@ -711,6 +732,7 @@ class WeDo2 {
BLECharacteristic.ATTACHED_IO,
this._onMessage
);
this._batteryLevelIntervalId = window.setInterval(this._checkBatteryLevel, BLEBatteryCheckInterval);
}
/**
@ -757,6 +779,19 @@ class WeDo2 {
}
}
/**
* Check the battery level on the WeDo 2.0. If the WeDo 2.0 has disconnected
* for some reason, the BLE socket will get an error back and automatically
* close the socket.
*/
_checkBatteryLevel () {
this._ble.read(
BLEService.DEVICE_SERVICE,
BLECharacteristic.LOW_VOLTAGE_ALERT,
false
);
}
/**
* Register a new sensor or motor connected at a port. Store the type of
* sensor or motor internally, and then register for notifications on input

View file

@ -11,23 +11,25 @@ class BLE extends JSONRPCWebSocket {
* @param {string} extensionId - the id of the extension using this socket.
* @param {object} peripheralOptions - the list of options for peripheral discovery.
* @param {object} connectCallback - a callback for connection.
* @param {object} disconnectCallback - a callback for disconnection.
*/
constructor (runtime, extensionId, peripheralOptions, connectCallback) {
constructor (runtime, extensionId, peripheralOptions, connectCallback, disconnectCallback = null) {
const ws = new WebSocket(ScratchLinkWebSocket);
super(ws);
this._ws = ws;
this._ws.onopen = this.requestPeripheral.bind(this); // only call request peripheral after socket opens
this._ws.onerror = this._sendRequestError.bind(this, 'ws onerror');
this._ws.onclose = this._sendDisconnectError.bind(this, 'ws onclose');
this._ws.onerror = this._handleRequestError.bind(this, 'ws onerror');
this._ws.onclose = this.handleDisconnectError.bind(this, 'ws onclose');
this._availablePeripherals = {};
this._connectCallback = connectCallback;
this._connected = false;
this._characteristicDidChangeCallback = null;
this._disconnectCallback = disconnectCallback;
this._discoverTimeoutID = null;
this._extensionId = extensionId;
this._peripheralOptions = peripheralOptions;
this._discoverTimeoutID = null;
this._runtime = runtime;
}
@ -41,10 +43,10 @@ class BLE extends JSONRPCWebSocket {
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}
this._discoverTimeoutID = window.setTimeout(this._sendDiscoverTimeout.bind(this), 15000);
this._discoverTimeoutID = window.setTimeout(this._handleDiscoverTimeout.bind(this), 15000);
this.sendRemoteRequest('discover', this._peripheralOptions)
.catch(e => {
this._sendRequestError(e);
this._handleRequestError(e);
});
}
// TODO: else?
@ -63,7 +65,7 @@ class BLE extends JSONRPCWebSocket {
this._connectCallback();
})
.catch(e => {
this._sendRequestError(e);
this._handleRequestError(e);
});
}
@ -71,10 +73,15 @@ class BLE extends JSONRPCWebSocket {
* Close the websocket.
*/
disconnect () {
if (!this._connected) return;
this._ws.close();
this._connected = false;
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}
this._runtime.emit(this._runtime.constructor.PERIPHERAL_DISCONNECTED);
}
/**
@ -99,7 +106,7 @@ class BLE extends JSONRPCWebSocket {
this._characteristicDidChangeCallback = onCharacteristicChanged;
return this.sendRemoteRequest('startNotifications', params)
.catch(e => {
this._sendDisconnectError(e);
this.handleDisconnectError(e);
});
}
@ -119,10 +126,12 @@ class BLE extends JSONRPCWebSocket {
if (optStartNotifications) {
params.startNotifications = true;
}
if (onCharacteristicChanged) {
this._characteristicDidChangeCallback = onCharacteristicChanged;
}
return this.sendRemoteRequest('read', params)
.catch(e => {
this._sendDisconnectError(e);
this.handleDisconnectError(e);
});
}
@ -145,7 +154,7 @@ class BLE extends JSONRPCWebSocket {
}
return this.sendRemoteRequest('write', params)
.catch(e => {
this._sendDisconnectError(e);
this.handleDisconnectError(e);
});
}
@ -168,14 +177,45 @@ class BLE extends JSONRPCWebSocket {
}
break;
case 'characteristicDidChange':
if (this._characteristicDidChangeCallback) {
this._characteristicDidChangeCallback(params.message);
}
break;
case 'ping':
return 42;
}
}
_sendRequestError (/* e */) {
/**
* Handle an error resulting from losing connection to a peripheral.
*
* This could be due to:
* - battery depletion
* - going out of bluetooth range
* - being powered down
*
* Disconnect the socket, and if the extension using this socket has a
* disconnect callback, call it. Finally, emit an error to the runtime.
*/
handleDisconnectError (/* e */) {
// log.error(`BLE error: ${JSON.stringify(e)}`);
if (!this._connected) return;
// TODO: Fix branching by splitting up cleanup/disconnect in extension
if (this._disconnectCallback) {
this._disconnectCallback(); // must call disconnect()
} else {
this.disconnect();
}
this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTION_LOST_ERROR, {
message: `Scratch lost connection to`,
extensionId: this._extensionId
});
}
_handleRequestError (/* e */) {
// log.error(`BLE error: ${JSON.stringify(e)}`);
this._runtime.emit(this._runtime.constructor.PERIPHERAL_REQUEST_ERROR, {
@ -184,20 +224,7 @@ class BLE extends JSONRPCWebSocket {
});
}
_sendDisconnectError (/* e */) {
// log.error(`BLE error: ${JSON.stringify(e)}`);
if (!this._connected) return;
this._connected = false;
this._runtime.emit(this._runtime.constructor.PERIPHERAL_DISCONNECT_ERROR, {
message: `Scratch lost connection to`,
extensionId: this._extensionId
});
}
_sendDiscoverTimeout () {
_handleDiscoverTimeout () {
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}

View file

@ -11,24 +11,26 @@ class BT extends JSONRPCWebSocket {
* @param {string} extensionId - the id of the extension using this socket.
* @param {object} peripheralOptions - the list of options for peripheral discovery.
* @param {object} connectCallback - a callback for connection.
* @param {object} disconnectCallback - a callback for disconnection.
* @param {object} messageCallback - a callback for message sending.
*/
constructor (runtime, extensionId, peripheralOptions, connectCallback, messageCallback) {
constructor (runtime, extensionId, peripheralOptions, connectCallback, disconnectCallback = null, messageCallback) {
const ws = new WebSocket(ScratchLinkWebSocket);
super(ws);
this._ws = ws;
this._ws.onopen = this.requestPeripheral.bind(this); // only call request peripheral after socket opens
this._ws.onerror = this._sendRequestError.bind(this, 'ws onerror');
this._ws.onclose = this._sendDisconnectError.bind(this, 'ws onclose');
this._ws.onerror = this._handleRequestError.bind(this, 'ws onerror');
this._ws.onclose = this.handleDisconnectError.bind(this, 'ws onclose');
this._availablePeripherals = {};
this._connectCallback = connectCallback;
this._connected = false;
this._characteristicDidChangeCallback = null;
this._disconnectCallback = disconnectCallback;
this._discoverTimeoutID = null;
this._extensionId = extensionId;
this._peripheralOptions = peripheralOptions;
this._discoverTimeoutID = null;
this._messageCallback = messageCallback;
this._runtime = runtime;
}
@ -43,10 +45,10 @@ class BT extends JSONRPCWebSocket {
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}
this._discoverTimeoutID = window.setTimeout(this._sendDiscoverTimeout.bind(this), 15000);
this._discoverTimeoutID = window.setTimeout(this._handleDiscoverTimeout.bind(this), 15000);
this.sendRemoteRequest('discover', this._peripheralOptions)
.catch(
e => this._sendRequestError(e)
e => this._handleRequestError(e)
);
}
// TODO: else?
@ -65,7 +67,7 @@ class BT extends JSONRPCWebSocket {
this._connectCallback();
})
.catch(e => {
this._sendRequestError(e);
this._handleRequestError(e);
});
}
@ -73,10 +75,15 @@ class BT extends JSONRPCWebSocket {
* Close the websocket.
*/
disconnect () {
if (!this._connected) return;
this._ws.close();
this._connected = false;
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}
this._runtime.emit(this._runtime.constructor.PERIPHERAL_DISCONNECTED);
}
/**
@ -89,7 +96,7 @@ class BT extends JSONRPCWebSocket {
sendMessage (options) {
return this.sendRemoteRequest('send', options)
.catch(e => {
this._sendDisconnectError(e);
this.handleDisconnectError(e);
});
}
@ -120,7 +127,36 @@ class BT extends JSONRPCWebSocket {
}
}
_sendRequestError (/* e */) {
/**
* Handle an error resulting from losing connection to a peripheral.
*
* This could be due to:
* - battery depletion
* - going out of bluetooth range
* - being powered down
*
* Disconnect the socket, and if the extension using this socket has a
* disconnect callback, call it. Finally, emit an error to the runtime.
*/
handleDisconnectError (/* e */) {
// log.error(`BT error: ${JSON.stringify(e)}`);
if (!this._connected) return;
// TODO: Fix branching by splitting up cleanup/disconnect in extension
if (this._disconnectCallback) {
this._disconnectCallback(); // must call disconnect()
} else {
this.disconnect();
}
this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTION_LOST_ERROR, {
message: `Scratch lost connection to`,
extensionId: this._extensionId
});
}
_handleRequestError (/* e */) {
// log.error(`BT error: ${JSON.stringify(e)}`);
this._runtime.emit(this._runtime.constructor.PERIPHERAL_REQUEST_ERROR, {
@ -129,20 +165,7 @@ class BT extends JSONRPCWebSocket {
});
}
_sendDisconnectError (/* e */) {
// log.error(`BT error: ${JSON.stringify(e)}`);
if (!this._connected) return;
this._connected = false;
this._runtime.emit(this._runtime.constructor.PERIPHERAL_DISCONNECT_ERROR, {
message: `Scratch lost connection to`,
extensionId: this._extensionId
});
}
_sendDiscoverTimeout () {
_handleDiscoverTimeout () {
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}

View file

@ -124,8 +124,11 @@ class VirtualMachine extends EventEmitter {
this.runtime.on(Runtime.PERIPHERAL_REQUEST_ERROR, () =>
this.emit(Runtime.PERIPHERAL_REQUEST_ERROR)
);
this.runtime.on(Runtime.PERIPHERAL_DISCONNECT_ERROR, data =>
this.emit(Runtime.PERIPHERAL_DISCONNECT_ERROR, data)
this.runtime.on(Runtime.PERIPHERAL_DISCONNECTED, () =>
this.emit(Runtime.PERIPHERAL_DISCONNECTED)
);
this.runtime.on(Runtime.PERIPHERAL_CONNECTION_LOST_ERROR, data =>
this.emit(Runtime.PERIPHERAL_CONNECTION_LOST_ERROR, data)
);
this.runtime.on(Runtime.PERIPHERAL_SCAN_TIMEOUT, () =>
this.emit(Runtime.PERIPHERAL_SCAN_TIMEOUT)