mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-01-25 17:09:50 -05:00
Extension/runtime protocol refactoring.
This commit is contained in:
parent
d778ddf00e
commit
730b798686
6 changed files with 158 additions and 207 deletions
|
@ -891,13 +891,13 @@ class Runtime extends EventEmitter {
|
||||||
|
|
||||||
startDeviceScan (extensionId) {
|
startDeviceScan (extensionId) {
|
||||||
if (this.extensionDevices[extensionId]) {
|
if (this.extensionDevices[extensionId]) {
|
||||||
this.extensionDevices[extensionId].startScan();
|
this.extensionDevices[extensionId].requestDevice();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connectToPeripheral (extensionId, peripheralId) {
|
connectToPeripheral (extensionId, peripheralId) {
|
||||||
if (this.extensionDevices[extensionId]) {
|
if (this.extensionDevices[extensionId]) {
|
||||||
this.extensionDevices[extensionId].connectToPeripheral(peripheralId);
|
this.extensionDevices[extensionId].connectDevice(peripheralId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
const ArgumentType = require('../../extension-support/argument-type');
|
const ArgumentType = require('../../extension-support/argument-type');
|
||||||
const BlockType = require('../../extension-support/block-type');
|
const BlockType = require('../../extension-support/block-type');
|
||||||
const log = require('../../util/log');
|
const log = require('../../util/log');
|
||||||
const ScratchBLE = require('../../io/scratchBLE');
|
const BLESession = require('../../io/bleSession');
|
||||||
const Base64Util = require('../../util/base64-util');
|
const Base64Util = require('../../util/base64-util');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -62,17 +62,15 @@ class MicroBit {
|
||||||
this._runtime = runtime;
|
this._runtime = runtime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ScratchBLE connection session for reading/writing device data.
|
* The BluetoothLowEnergy connection session for reading/writing device data.
|
||||||
* @type {ScratchBLE}
|
* @type {BLESession}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this._ble = new ScratchBLE(this._runtime, {
|
this._ble = new BLESession(this._runtime, extensionId, {
|
||||||
filters: [
|
filters: [
|
||||||
{services: [BLEUUID.service]}
|
{services: [BLEUUID.service]}
|
||||||
]
|
]
|
||||||
});
|
}, this._onSessionConnect.bind(this));
|
||||||
|
|
||||||
this._runtime.registerExtensionDevice(extensionId, this._ble);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The most recently received value for each sensor.
|
* The most recently received value for each sensor.
|
||||||
|
@ -119,14 +117,14 @@ class MicroBit {
|
||||||
for (let i = 0; i < text.length; i++) {
|
for (let i = 0; i < text.length; i++) {
|
||||||
output[i] = text.charCodeAt(i);
|
output[i] = text.charCodeAt(i);
|
||||||
}
|
}
|
||||||
this._writeBLE(BLECommand.CMD_DISPLAY_TEXT, output);
|
this._writeSessionData(BLECommand.CMD_DISPLAY_TEXT, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Uint8Array} matrix - the matrix to display.
|
* @param {Uint8Array} matrix - the matrix to display.
|
||||||
*/
|
*/
|
||||||
displayMatrix (matrix) {
|
displayMatrix (matrix) {
|
||||||
this._writeBLE(BLECommand.CMD_DISPLAY_LED, matrix);
|
this._writeSessionData(BLECommand.CMD_DISPLAY_LED, matrix);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -179,38 +177,20 @@ class MicroBit {
|
||||||
return this._sensors.touchPins[pin];
|
return this._sensors.touchPins[pin];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests connection to a device when BLE session is ready.
|
|
||||||
*/
|
|
||||||
// _onBLEReady () {
|
|
||||||
// this._ble.requestDevice({
|
|
||||||
// filters: [
|
|
||||||
// {services: [BLEUUID.service]}
|
|
||||||
// ]
|
|
||||||
// }, this._onBLEConnect.bind(this), this._onBLEError);
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts reading data from device after BLE has connected to it.
|
* Starts reading data from device after BLE has connected to it.
|
||||||
*/
|
*/
|
||||||
_onBLEConnect () {
|
_onSessionConnect () {
|
||||||
const callback = this._processBLEData.bind(this);
|
const callback = this._processSessionData.bind(this);
|
||||||
this._ble.read(BLEUUID.service, BLEUUID.rxChar, true, callback);
|
this._ble.read(BLEUUID.service, BLEUUID.rxChar, true, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} e - Error from BLE session.
|
|
||||||
*/
|
|
||||||
_onBLEError (e) {
|
|
||||||
log.error(`BLE error: ${e}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process the sensor data from the incoming BLE characteristic.
|
* Process the sensor data from the incoming BLE characteristic.
|
||||||
* @param {object} base64 - the incoming BLE data.
|
* @param {object} base64 - the incoming BLE data.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_processBLEData (base64) {
|
_processSessionData (base64) {
|
||||||
const data = Base64Util.base64ToUint8Array(base64);
|
const data = Base64Util.base64ToUint8Array(base64);
|
||||||
|
|
||||||
this._sensors.tiltX = data[1] | (data[0] << 8);
|
this._sensors.tiltX = data[1] | (data[0] << 8);
|
||||||
|
@ -234,7 +214,7 @@ class MicroBit {
|
||||||
* @param {Uint8Array} message - the message to write.
|
* @param {Uint8Array} message - the message to write.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_writeBLE (command, message) {
|
_writeSessionData (command, message) {
|
||||||
const output = new Uint8Array(message.length + 1);
|
const output = new Uint8Array(message.length + 1);
|
||||||
output[0] = command; // attach command to beginning of message
|
output[0] = command; // attach command to beginning of message
|
||||||
for (let i = 0; i < message.length; i++) {
|
for (let i = 0; i < message.length; i++) {
|
||||||
|
|
143
src/io/bleSession.js
Normal file
143
src/io/bleSession.js
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
const JSONRPCWebSocket = require('../util/jsonrpc-web-socket');
|
||||||
|
const ScratchLinkWebSocket = 'ws://localhost:20110/scratch/ble';
|
||||||
|
|
||||||
|
class BLESession extends JSONRPCWebSocket {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A BLE device session object. It handles connecting, over web sockets, to
|
||||||
|
* BLE devices, 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.
|
||||||
|
* @param {object} deviceOptions - the list of options for device discovery.
|
||||||
|
* @param {object} connectCallback - a callback for connection.
|
||||||
|
*/
|
||||||
|
constructor (runtime, extensionId, deviceOptions, connectCallback) {
|
||||||
|
const ws = new WebSocket(ScratchLinkWebSocket);
|
||||||
|
super(ws);
|
||||||
|
|
||||||
|
this._socketPromise = new Promise((resolve, reject) => {
|
||||||
|
this._ws.onopen = resolve;
|
||||||
|
this._ws.onerror = this._sendError(); // TODO: socket error?
|
||||||
|
});
|
||||||
|
|
||||||
|
this._availablePeripherals = {};
|
||||||
|
this._connectCallback = connectCallback;
|
||||||
|
this._characteristicDidChangeCallback = null;
|
||||||
|
this._deviceOptions = deviceOptions;
|
||||||
|
this._runtime = runtime;
|
||||||
|
this._ws = ws;
|
||||||
|
|
||||||
|
this._runtime.registerExtensionDevice(extensionId, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request connection to the device.
|
||||||
|
* If the web socket is not yet open, request when the socket promise resolves.
|
||||||
|
*/
|
||||||
|
requestDevice () {
|
||||||
|
// TODO: add timeout for 'no devices yet found' ?
|
||||||
|
if (this._ws.readyState === 1) {
|
||||||
|
this.sendRemoteRequest('pingMe') // TODO: remove pingMe when no longer needed
|
||||||
|
.then(() => this.sendRemoteRequest('discover', this._deviceOptions))
|
||||||
|
.catch(e => {
|
||||||
|
// TODO: what if discover doesn't initiate?
|
||||||
|
this._sendError(e);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Try again to connect to the websocket
|
||||||
|
this._socketPromise(this.sendRemoteRequest('pingMe') // TODO: remove pingMe when no longer needed
|
||||||
|
.then(() => this.sendRemoteRequest('discover', this._deviceOptions)))
|
||||||
|
.catch(e => {
|
||||||
|
// TODO: what if discover doesn't initiate?
|
||||||
|
this._sendError(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
|
||||||
|
*/
|
||||||
|
connectDevice (id) {
|
||||||
|
this.sendRemoteRequest('connect', {peripheralId: id})
|
||||||
|
.then(() => {
|
||||||
|
this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTED);
|
||||||
|
this._connectCallback();
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
// TODO: what if the peripheral loses power?
|
||||||
|
// TODO: what if tries to connect to an unknown peripheral id?
|
||||||
|
this._sendError(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) {
|
||||||
|
// TODO: does didReceiveCall receive any errors?
|
||||||
|
// TODO: Add peripheral 'undiscover' handling
|
||||||
|
switch (method) {
|
||||||
|
case 'didDiscoverPeripheral':
|
||||||
|
this._availablePeripherals[params.peripheralId] = params;
|
||||||
|
this._runtime.emit(
|
||||||
|
this._runtime.constructor.PERIPHERAL_LIST_UPDATE,
|
||||||
|
this._availablePeripherals
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'characteristicDidChange':
|
||||||
|
this._characteristicDidChangeCallback(params.message);
|
||||||
|
break;
|
||||||
|
case 'ping':
|
||||||
|
return 42;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start reading 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) {
|
||||||
|
const params = {
|
||||||
|
serviceId,
|
||||||
|
characteristicId
|
||||||
|
};
|
||||||
|
if (optStartNotifications) {
|
||||||
|
params.startNotifications = true;
|
||||||
|
}
|
||||||
|
this._characteristicDidChangeCallback = onCharacteristicChanged;
|
||||||
|
return this.sendRemoteRequest('read', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @return {Promise} - a promise from the remote send request.
|
||||||
|
*/
|
||||||
|
write (serviceId, characteristicId, message, encoding = null) {
|
||||||
|
const params = {serviceId, characteristicId, message};
|
||||||
|
if (encoding) {
|
||||||
|
params.encoding = encoding;
|
||||||
|
}
|
||||||
|
return this.sendRemoteRequest('write', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendError (e) {
|
||||||
|
console.log(`BLESession error ${e}`);
|
||||||
|
// are there different error types?
|
||||||
|
// this._runtime.emit(???????????????)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BLESession;
|
|
@ -1,8 +1,7 @@
|
||||||
const JSONRPCWebSocket = require('../util/jsonrpc');
|
const JSONRPCWebSocket = require('../util/jsonrpc');
|
||||||
|
|
||||||
const ScratchLinkWebSocket = 'ws://localhost:20110/scratch/bt';
|
const ScratchLinkWebSocket = 'ws://localhost:20110/scratch/bt';
|
||||||
|
|
||||||
class ScratchBT extends JSONRPCWebSocket {
|
class BTSession extends JSONRPCWebSocket {
|
||||||
constructor () {
|
constructor () {
|
||||||
super(new WebSocket(ScratchLinkWebSocket));
|
super(new WebSocket(ScratchLinkWebSocket));
|
||||||
}
|
}
|
||||||
|
@ -34,4 +33,4 @@ class ScratchBT extends JSONRPCWebSocket {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ScratchBT;
|
module.exports = BTSession;
|
|
@ -1,48 +0,0 @@
|
||||||
class PeripheralChooser {
|
|
||||||
|
|
||||||
get chosenPeripheralId () {
|
|
||||||
return this._chosenPeripheralId;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor (runtime) {
|
|
||||||
this._runtime = runtime;
|
|
||||||
this._availablePeripherals = {};
|
|
||||||
this._chosenPeripheralId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launches a GUI menu to choose a peripheral.
|
|
||||||
* @return {Promise} - chosen peripheral promise.
|
|
||||||
*/
|
|
||||||
choosePeripheral () {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// TODO: Temporary: should launch gui instead.
|
|
||||||
this._tempPeripheralChosenCallback = resolve;
|
|
||||||
this._tempPeripheralChosenReject = reject;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the peripheral ID to list of available peripherals.
|
|
||||||
* @param {object} info - the peripheral info object.
|
|
||||||
*/
|
|
||||||
addPeripheral (info) {
|
|
||||||
// Add a new peripheral, or if the id is already present, update it
|
|
||||||
this._availablePeripherals[info.peripheralId] = info;
|
|
||||||
|
|
||||||
const peripheralArray = Object.keys(this._availablePeripherals).map(id =>
|
|
||||||
this._availablePeripherals[id]
|
|
||||||
);
|
|
||||||
|
|
||||||
// @todo: sort peripherals by signal strength? or maybe not, so they don't jump around?
|
|
||||||
|
|
||||||
this._runtime.emit(this._runtime.constructor.PERIPHERAL_LIST_UPDATE, peripheralArray);
|
|
||||||
|
|
||||||
// TODO: Temporary: calls chosen callback on whatever peripherals are added.
|
|
||||||
// this._chosenPeripheralId = this._availablePeripherals[0];
|
|
||||||
// this._tempPeripheralChosenCallback(this._chosenPeripheralId);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = PeripheralChooser;
|
|
|
@ -1,123 +0,0 @@
|
||||||
const JSONRPCWebSocket = require('../util/jsonrpc-web-socket');
|
|
||||||
const PeripheralChooser = require('./peripheralChooser');
|
|
||||||
|
|
||||||
const ScratchLinkWebSocket = 'ws://localhost:20110/scratch/ble';
|
|
||||||
|
|
||||||
class ScratchBLE extends JSONRPCWebSocket {
|
|
||||||
constructor (runtime, deviceOptions) {
|
|
||||||
const ws = new WebSocket(ScratchLinkWebSocket);
|
|
||||||
super(ws);
|
|
||||||
|
|
||||||
this._socketPromise = new Promise((resolve, reject) => {
|
|
||||||
this._ws.onopen = resolve;
|
|
||||||
this._ws.onerror = reject;
|
|
||||||
});
|
|
||||||
|
|
||||||
this._runtime = runtime;
|
|
||||||
|
|
||||||
this._ws = ws;
|
|
||||||
this.peripheralChooser = new PeripheralChooser(this._runtime); // TODO: finalize gui connection
|
|
||||||
this._characteristicDidChange = null;
|
|
||||||
|
|
||||||
this._deviceOptions = deviceOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @todo handle websocket failed
|
|
||||||
startScan () {
|
|
||||||
console.log('BLE startScan', this._ws.readyState);
|
|
||||||
if (this._ws.readyState === 1) {
|
|
||||||
this.sendRemoteRequest('pingMe')
|
|
||||||
.then(() => this.requestDevice(this._deviceOptions));
|
|
||||||
} else {
|
|
||||||
// Try again to connect to the websocket
|
|
||||||
this._socketPromise(this.sendRemoteRequest('pingMe')
|
|
||||||
.then(() => this.requestDevice(this._deviceOptions)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connectToPeripheral (id) {
|
|
||||||
this.sendRemoteRequest(
|
|
||||||
'connect',
|
|
||||||
{peripheralId: id}
|
|
||||||
).then(() =>
|
|
||||||
this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTED)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request a device with the device options and optional gui options.
|
|
||||||
* @param {object} deviceOptions - list of device guiOptions.
|
|
||||||
* @param {object} onConnect - on connect callback.
|
|
||||||
* @param {object} onError - on error callbackk.
|
|
||||||
*/
|
|
||||||
requestDevice (deviceOptions, onConnect, onError) {
|
|
||||||
this.sendRemoteRequest('discover', deviceOptions)
|
|
||||||
.then(() => this.peripheralChooser.choosePeripheral()) // TODO: use gui options?
|
|
||||||
.then(id => this.sendRemoteRequest(
|
|
||||||
'connect',
|
|
||||||
{peripheralId: id}
|
|
||||||
))
|
|
||||||
.then(
|
|
||||||
onConnect,
|
|
||||||
onError
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
// TODO: Add peripheral 'undiscover' handling
|
|
||||||
switch (method) {
|
|
||||||
case 'didDiscoverPeripheral':
|
|
||||||
this.peripheralChooser.addPeripheral(params);
|
|
||||||
break;
|
|
||||||
case 'characteristicDidChange':
|
|
||||||
this._characteristicDidChange(params.message);
|
|
||||||
break;
|
|
||||||
case 'ping':
|
|
||||||
return 42;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start reading 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) {
|
|
||||||
const params = {
|
|
||||||
serviceId,
|
|
||||||
characteristicId
|
|
||||||
};
|
|
||||||
if (optStartNotifications) {
|
|
||||||
params.startNotifications = true;
|
|
||||||
}
|
|
||||||
this._characteristicDidChange = onCharacteristicChanged;
|
|
||||||
return this.sendRemoteRequest('read', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
* @return {Promise} - a promise from the remote send request.
|
|
||||||
*/
|
|
||||||
write (serviceId, characteristicId, message, encoding = null) {
|
|
||||||
const params = {serviceId, characteristicId, message};
|
|
||||||
if (encoding) {
|
|
||||||
params.encoding = encoding;
|
|
||||||
}
|
|
||||||
return this.sendRemoteRequest('write', params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = ScratchBLE;
|
|
Loading…
Reference in a new issue