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. * 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} * @const {string}
*/ */
static get PERIPHERAL_LIST_UPDATE () { static get PERIPHERAL_LIST_UPDATE () {
@ -566,14 +568,25 @@ class Runtime extends EventEmitter {
/** /**
* Event name for reporting that a peripheral has connected. * Event name for reporting that a peripheral has connected.
* This causes the status button in the blocks menu to indicate 'connected'.
* @const {string} * @const {string}
*/ */
static get PERIPHERAL_CONNECTED () { static get PERIPHERAL_CONNECTED () {
return '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. * 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} * @const {string}
*/ */
static get PERIPHERAL_REQUEST_ERROR () { 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} * @const {string}
*/ */
static get PERIPHERAL_DISCONNECT_ERROR () { static get PERIPHERAL_CONNECTION_LOST_ERROR () {
return 'PERIPHERAL_DISCONNECT_ERROR'; return 'PERIPHERAL_CONNECTION_LOST_ERROR';
} }
/** /**
* Event name for reporting that a peripheral has not been discovered. * Event name for reporting that a peripheral has not been discovered.
* This causes the peripheral connection modal to show a timeout state.
* @const {string} * @const {string}
*/ */
static get PERIPHERAL_SCAN_TIMEOUT () { static get PERIPHERAL_SCAN_TIMEOUT () {

View file

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

View file

@ -25,8 +25,12 @@ const BLECommand = {
CMD_DISPLAY_LED: 0x82 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. * 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; 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. * Enum for micro:bit protocol.
* https://github.com/LLK/scratch-microbit-firmware/blob/master/protocol.md * https://github.com/LLK/scratch-microbit-firmware/blob/master/protocol.md
@ -212,7 +222,7 @@ class MicroBit {
filters: [ filters: [
{services: [BLEUUID.service]} {services: [BLEUUID.service]}
] ]
}, this._onConnect); }, this._onConnect, this.disconnect);
} }
/** /**
@ -289,7 +299,10 @@ class MicroBit {
*/ */
_onConnect () { _onConnect () {
this._ble.read(BLEUUID.service, BLEUUID.rxChar, true, this._onMessage); 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 // cancel disconnect timeout and start a new one
window.clearInterval(this._timeoutID); 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 = { const BLECharacteristic = {
ATTACHED_IO: '00001527-1212-efde-1523-785feabcd123', ATTACHED_IO: '00001527-1212-efde-1523-785feabcd123',
LOW_VOLTAGE_ALERT: '00001528-1212-efde-1523-785feabcd123',
INPUT_VALUES: '00001560-1212-efde-1523-785feabcd123', INPUT_VALUES: '00001560-1212-efde-1523-785feabcd123',
INPUT_COMMAND: '00001563-1212-efde-1523-785feabcd123', INPUT_COMMAND: '00001563-1212-efde-1523-785feabcd123',
OUTPUT_COMMAND: '00001565-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. * A time interval to wait (in milliseconds) while a block that sends a BLE message is running.
* @type {number} * @type {number}
@ -421,8 +428,17 @@ class WeDo2 {
*/ */
this._rateLimiter = new RateLimiter(BLESendRateMax); 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._onConnect = this._onConnect.bind(this);
this._onMessage = this._onMessage.bind(this); this._onMessage = this._onMessage.bind(this);
this._checkBatteryLevel = this._checkBatteryLevel.bind(this);
} }
/** /**
@ -578,7 +594,7 @@ class WeDo2 {
services: [BLEService.DEVICE_SERVICE] services: [BLEService.DEVICE_SERVICE]
}], }],
optionalServices: [BLEService.IO_SERVICE] optionalServices: [BLEService.IO_SERVICE]
}, this._onConnect); }, this._onConnect, this.disconnect);
} }
/** /**
@ -606,6 +622,11 @@ class WeDo2 {
if (this._ble) { if (this._ble) {
this._ble.disconnect(); this._ble.disconnect();
} }
if (this._batteryLevelIntervalId) {
window.clearInterval(this._batteryLevelIntervalId);
this._batteryLevelIntervalId = null;
}
} }
/** /**
@ -711,6 +732,7 @@ class WeDo2 {
BLECharacteristic.ATTACHED_IO, BLECharacteristic.ATTACHED_IO,
this._onMessage 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 * 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 * 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 {string} extensionId - the id of the extension using this socket.
* @param {object} peripheralOptions - the list of options for peripheral discovery. * @param {object} peripheralOptions - the list of options for peripheral discovery.
* @param {object} connectCallback - a callback for connection. * @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); const ws = new WebSocket(ScratchLinkWebSocket);
super(ws); super(ws);
this._ws = ws; this._ws = ws;
this._ws.onopen = this.requestPeripheral.bind(this); // only call request peripheral after socket opens 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.onerror = this._handleRequestError.bind(this, 'ws onerror');
this._ws.onclose = this._sendDisconnectError.bind(this, 'ws onclose'); this._ws.onclose = this.handleDisconnectError.bind(this, 'ws onclose');
this._availablePeripherals = {}; this._availablePeripherals = {};
this._connectCallback = connectCallback; this._connectCallback = connectCallback;
this._connected = false; this._connected = false;
this._characteristicDidChangeCallback = null; this._characteristicDidChangeCallback = null;
this._disconnectCallback = disconnectCallback;
this._discoverTimeoutID = null;
this._extensionId = extensionId; this._extensionId = extensionId;
this._peripheralOptions = peripheralOptions; this._peripheralOptions = peripheralOptions;
this._discoverTimeoutID = null;
this._runtime = runtime; this._runtime = runtime;
} }
@ -41,10 +43,10 @@ class BLE extends JSONRPCWebSocket {
if (this._discoverTimeoutID) { if (this._discoverTimeoutID) {
window.clearTimeout(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) this.sendRemoteRequest('discover', this._peripheralOptions)
.catch(e => { .catch(e => {
this._sendRequestError(e); this._handleRequestError(e);
}); });
} }
// TODO: else? // TODO: else?
@ -63,7 +65,7 @@ class BLE extends JSONRPCWebSocket {
this._connectCallback(); this._connectCallback();
}) })
.catch(e => { .catch(e => {
this._sendRequestError(e); this._handleRequestError(e);
}); });
} }
@ -71,10 +73,15 @@ class BLE extends JSONRPCWebSocket {
* Close the websocket. * Close the websocket.
*/ */
disconnect () { disconnect () {
if (!this._connected) return;
this._ws.close(); this._ws.close();
this._connected = false;
if (this._discoverTimeoutID) { if (this._discoverTimeoutID) {
window.clearTimeout(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; this._characteristicDidChangeCallback = onCharacteristicChanged;
return this.sendRemoteRequest('startNotifications', params) return this.sendRemoteRequest('startNotifications', params)
.catch(e => { .catch(e => {
this._sendDisconnectError(e); this.handleDisconnectError(e);
}); });
} }
@ -119,10 +126,12 @@ class BLE extends JSONRPCWebSocket {
if (optStartNotifications) { if (optStartNotifications) {
params.startNotifications = true; params.startNotifications = true;
} }
this._characteristicDidChangeCallback = onCharacteristicChanged; if (onCharacteristicChanged) {
this._characteristicDidChangeCallback = onCharacteristicChanged;
}
return this.sendRemoteRequest('read', params) return this.sendRemoteRequest('read', params)
.catch(e => { .catch(e => {
this._sendDisconnectError(e); this.handleDisconnectError(e);
}); });
} }
@ -145,7 +154,7 @@ class BLE extends JSONRPCWebSocket {
} }
return this.sendRemoteRequest('write', params) return this.sendRemoteRequest('write', params)
.catch(e => { .catch(e => {
this._sendDisconnectError(e); this.handleDisconnectError(e);
}); });
} }
@ -168,14 +177,45 @@ class BLE extends JSONRPCWebSocket {
} }
break; break;
case 'characteristicDidChange': case 'characteristicDidChange':
this._characteristicDidChangeCallback(params.message); if (this._characteristicDidChangeCallback) {
this._characteristicDidChangeCallback(params.message);
}
break; break;
case 'ping': case 'ping':
return 42; 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)}`); // log.error(`BLE error: ${JSON.stringify(e)}`);
this._runtime.emit(this._runtime.constructor.PERIPHERAL_REQUEST_ERROR, { this._runtime.emit(this._runtime.constructor.PERIPHERAL_REQUEST_ERROR, {
@ -184,20 +224,7 @@ class BLE extends JSONRPCWebSocket {
}); });
} }
_sendDisconnectError (/* e */) { _handleDiscoverTimeout () {
// 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 () {
if (this._discoverTimeoutID) { if (this._discoverTimeoutID) {
window.clearTimeout(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 {string} extensionId - the id of the extension using this socket.
* @param {object} peripheralOptions - the list of options for peripheral discovery. * @param {object} peripheralOptions - the list of options for peripheral discovery.
* @param {object} connectCallback - a callback for connection. * @param {object} connectCallback - a callback for connection.
* @param {object} disconnectCallback - a callback for disconnection.
* @param {object} messageCallback - a callback for message sending. * @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); const ws = new WebSocket(ScratchLinkWebSocket);
super(ws); super(ws);
this._ws = ws; this._ws = ws;
this._ws.onopen = this.requestPeripheral.bind(this); // only call request peripheral after socket opens 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.onerror = this._handleRequestError.bind(this, 'ws onerror');
this._ws.onclose = this._sendDisconnectError.bind(this, 'ws onclose'); this._ws.onclose = this.handleDisconnectError.bind(this, 'ws onclose');
this._availablePeripherals = {}; this._availablePeripherals = {};
this._connectCallback = connectCallback; this._connectCallback = connectCallback;
this._connected = false; this._connected = false;
this._characteristicDidChangeCallback = null; this._characteristicDidChangeCallback = null;
this._disconnectCallback = disconnectCallback;
this._discoverTimeoutID = null;
this._extensionId = extensionId; this._extensionId = extensionId;
this._peripheralOptions = peripheralOptions; this._peripheralOptions = peripheralOptions;
this._discoverTimeoutID = null;
this._messageCallback = messageCallback; this._messageCallback = messageCallback;
this._runtime = runtime; this._runtime = runtime;
} }
@ -43,10 +45,10 @@ class BT extends JSONRPCWebSocket {
if (this._discoverTimeoutID) { if (this._discoverTimeoutID) {
window.clearTimeout(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) this.sendRemoteRequest('discover', this._peripheralOptions)
.catch( .catch(
e => this._sendRequestError(e) e => this._handleRequestError(e)
); );
} }
// TODO: else? // TODO: else?
@ -65,7 +67,7 @@ class BT extends JSONRPCWebSocket {
this._connectCallback(); this._connectCallback();
}) })
.catch(e => { .catch(e => {
this._sendRequestError(e); this._handleRequestError(e);
}); });
} }
@ -73,10 +75,15 @@ class BT extends JSONRPCWebSocket {
* Close the websocket. * Close the websocket.
*/ */
disconnect () { disconnect () {
if (!this._connected) return;
this._ws.close(); this._ws.close();
this._connected = false;
if (this._discoverTimeoutID) { if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID); window.clearTimeout(this._discoverTimeoutID);
} }
this._runtime.emit(this._runtime.constructor.PERIPHERAL_DISCONNECTED);
} }
/** /**
@ -89,7 +96,7 @@ class BT extends JSONRPCWebSocket {
sendMessage (options) { sendMessage (options) {
return this.sendRemoteRequest('send', options) return this.sendRemoteRequest('send', options)
.catch(e => { .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)}`); // log.error(`BT error: ${JSON.stringify(e)}`);
this._runtime.emit(this._runtime.constructor.PERIPHERAL_REQUEST_ERROR, { this._runtime.emit(this._runtime.constructor.PERIPHERAL_REQUEST_ERROR, {
@ -129,20 +165,7 @@ class BT extends JSONRPCWebSocket {
}); });
} }
_sendDisconnectError (/* e */) { _handleDiscoverTimeout () {
// 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 () {
if (this._discoverTimeoutID) { if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID); window.clearTimeout(this._discoverTimeoutID);
} }

View file

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