mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-23 14:32:59 -05:00
Merge pull request #1763 from evhan55/extensions/disconnect-errors
Various fixes to extension disconnect errors
This commit is contained in:
commit
0b251adace
7 changed files with 182 additions and 62 deletions
|
@ -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 () {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
69
src/io/bt.js
69
src/io/bt.js
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue