mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-24 06:52:40 -05:00
256 lines
9 KiB
JavaScript
256 lines
9 KiB
JavaScript
const JSONRPC = require('../util/jsonrpc');
|
|
|
|
class BLE extends JSONRPC {
|
|
|
|
/**
|
|
* A BLE peripheral socket object. It handles connecting, over web sockets, to
|
|
* BLE peripherals, and reading and writing data to them.
|
|
* @param {Runtime} runtime - the Runtime for sending/receiving GUI update events.
|
|
* @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} resetCallback - a callback for resetting extension state.
|
|
*/
|
|
constructor (runtime, extensionId, peripheralOptions, connectCallback, resetCallback = null) {
|
|
super();
|
|
|
|
this._socket = runtime.getScratchLinkSocket('BLE');
|
|
this._socket.setOnOpen(this.requestPeripheral.bind(this));
|
|
this._socket.setOnClose(this.handleDisconnectError.bind(this));
|
|
this._socket.setOnError(this._handleRequestError.bind(this));
|
|
this._socket.setHandleMessage(this._handleMessage.bind(this));
|
|
|
|
this._sendMessage = this._socket.sendMessage.bind(this._socket);
|
|
|
|
this._availablePeripherals = {};
|
|
this._connectCallback = connectCallback;
|
|
this._connected = false;
|
|
this._characteristicDidChangeCallback = null;
|
|
this._resetCallback = resetCallback;
|
|
this._discoverTimeoutID = null;
|
|
this._extensionId = extensionId;
|
|
this._peripheralOptions = peripheralOptions;
|
|
this._runtime = runtime;
|
|
|
|
this._socket.open();
|
|
}
|
|
|
|
/**
|
|
* Request connection to the peripheral.
|
|
* If the web socket is not yet open, request when the socket promise resolves.
|
|
*/
|
|
requestPeripheral () {
|
|
this._availablePeripherals = {};
|
|
if (this._discoverTimeoutID) {
|
|
window.clearTimeout(this._discoverTimeoutID);
|
|
}
|
|
this._discoverTimeoutID = window.setTimeout(this._handleDiscoverTimeout.bind(this), 15000);
|
|
this.sendRemoteRequest('discover', this._peripheralOptions)
|
|
.catch(e => {
|
|
this._handleRequestError(e);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Try connecting to the input peripheral id, and then call the connect
|
|
* callback if connection is successful.
|
|
* @param {number} id - the id of the peripheral to connect to
|
|
*/
|
|
connectPeripheral (id) {
|
|
this.sendRemoteRequest('connect', {peripheralId: id})
|
|
.then(() => {
|
|
this._connected = true;
|
|
this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTED);
|
|
this._connectCallback();
|
|
})
|
|
.catch(e => {
|
|
this._handleRequestError(e);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Close the websocket.
|
|
*/
|
|
disconnect () {
|
|
if (this._connected) {
|
|
this._connected = false;
|
|
}
|
|
|
|
if (this._socket.isOpen()) {
|
|
this._socket.close();
|
|
}
|
|
|
|
if (this._discoverTimeoutID) {
|
|
window.clearTimeout(this._discoverTimeoutID);
|
|
}
|
|
|
|
// Sets connection status icon to orange
|
|
this._runtime.emit(this._runtime.constructor.PERIPHERAL_DISCONNECTED);
|
|
}
|
|
|
|
/**
|
|
* @return {bool} whether the peripheral is connected.
|
|
*/
|
|
isConnected () {
|
|
return this._connected;
|
|
}
|
|
|
|
/**
|
|
* Start receiving notifications from the specified ble service.
|
|
* @param {number} serviceId - the ble service to read.
|
|
* @param {number} characteristicId - the ble characteristic to get notifications from.
|
|
* @param {object} onCharacteristicChanged - callback for characteristic change notifications.
|
|
* @return {Promise} - a promise from the remote startNotifications request.
|
|
*/
|
|
startNotifications (serviceId, characteristicId, onCharacteristicChanged = null) {
|
|
const params = {
|
|
serviceId,
|
|
characteristicId
|
|
};
|
|
this._characteristicDidChangeCallback = onCharacteristicChanged;
|
|
return this.sendRemoteRequest('startNotifications', params)
|
|
.catch(e => {
|
|
this.handleDisconnectError(e);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Read from the specified ble service.
|
|
* @param {number} serviceId - the ble service to read.
|
|
* @param {number} characteristicId - the ble characteristic to read.
|
|
* @param {boolean} optStartNotifications - whether to start receiving characteristic change notifications.
|
|
* @param {object} onCharacteristicChanged - callback for characteristic change notifications.
|
|
* @return {Promise} - a promise from the remote read request.
|
|
*/
|
|
read (serviceId, characteristicId, optStartNotifications = false, onCharacteristicChanged = null) {
|
|
const params = {
|
|
serviceId,
|
|
characteristicId
|
|
};
|
|
if (optStartNotifications) {
|
|
params.startNotifications = true;
|
|
}
|
|
if (onCharacteristicChanged) {
|
|
this._characteristicDidChangeCallback = onCharacteristicChanged;
|
|
}
|
|
return this.sendRemoteRequest('read', params)
|
|
.catch(e => {
|
|
this.handleDisconnectError(e);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Write data to the specified ble service.
|
|
* @param {number} serviceId - the ble service to write.
|
|
* @param {number} characteristicId - the ble characteristic to write.
|
|
* @param {string} message - the message to send.
|
|
* @param {string} encoding - the message encoding type.
|
|
* @param {boolean} withResponse - if true, resolve after peripheral's response.
|
|
* @return {Promise} - a promise from the remote send request.
|
|
*/
|
|
write (serviceId, characteristicId, message, encoding = null, withResponse = null) {
|
|
const params = {serviceId, characteristicId, message};
|
|
if (encoding) {
|
|
params.encoding = encoding;
|
|
}
|
|
if (withResponse !== null) {
|
|
params.withResponse = withResponse;
|
|
}
|
|
return this.sendRemoteRequest('write', params)
|
|
.catch(e => {
|
|
this.handleDisconnectError(e);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle a received call from the socket.
|
|
* @param {string} method - a received method label.
|
|
* @param {object} params - a received list of parameters.
|
|
* @return {object} - optional return value.
|
|
*/
|
|
didReceiveCall (method, params) {
|
|
switch (method) {
|
|
case 'didDiscoverPeripheral':
|
|
this._availablePeripherals[params.peripheralId] = params;
|
|
this._runtime.emit(
|
|
this._runtime.constructor.PERIPHERAL_LIST_UPDATE,
|
|
this._availablePeripherals
|
|
);
|
|
if (this._discoverTimeoutID) {
|
|
window.clearTimeout(this._discoverTimeoutID);
|
|
}
|
|
break;
|
|
case 'userDidPickPeripheral':
|
|
this._availablePeripherals[params.peripheralId] = params;
|
|
this._runtime.emit(
|
|
this._runtime.constructor.USER_PICKED_PERIPHERAL,
|
|
this._availablePeripherals
|
|
);
|
|
if (this._discoverTimeoutID) {
|
|
window.clearTimeout(this._discoverTimeoutID);
|
|
}
|
|
break;
|
|
case 'userDidNotPickPeripheral':
|
|
this._runtime.emit(
|
|
this._runtime.constructor.PERIPHERAL_SCAN_TIMEOUT
|
|
);
|
|
if (this._discoverTimeoutID) {
|
|
window.clearTimeout(this._discoverTimeoutID);
|
|
}
|
|
break;
|
|
case 'characteristicDidChange':
|
|
if (this._characteristicDidChangeCallback) {
|
|
this._characteristicDidChangeCallback(params.message);
|
|
}
|
|
break;
|
|
case 'ping':
|
|
return 42;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* reset callback, call it. Finally, emit an error to the runtime.
|
|
*/
|
|
handleDisconnectError (/* e */) {
|
|
// log.error(`BLE error: ${JSON.stringify(e)}`);
|
|
|
|
if (!this._connected) return;
|
|
|
|
this.disconnect();
|
|
|
|
if (this._resetCallback) {
|
|
this._resetCallback();
|
|
}
|
|
|
|
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, {
|
|
message: `Scratch lost connection to`,
|
|
extensionId: this._extensionId
|
|
});
|
|
}
|
|
|
|
_handleDiscoverTimeout () {
|
|
if (this._discoverTimeoutID) {
|
|
window.clearTimeout(this._discoverTimeoutID);
|
|
}
|
|
this._runtime.emit(this._runtime.constructor.PERIPHERAL_SCAN_TIMEOUT);
|
|
}
|
|
}
|
|
|
|
module.exports = BLE;
|