Refactor for hardware extensions (#1555)

* Beginning refactor: renaming 'device' to 'peripheral', shortening function names, reordering functions, etc.

* Continuing refactoring: renaming some functions to be more verbose in the runtime, adding JSDocs, etc.

* Changing 'device' to 'peripheral', etc.

* Changing 'session' to 'socket'.

* Fixing EV3 menus and menu arg validation, reordering functions, etc.

* Add _send, add some references to documentation, etc.

* Factored out _outputCommand and _inputCommand, renamed some enums, etc.

* Fixed _outputCommand, some other minor cleanup.

* Make _outputCommand and _inputCommand public.

* Added TODO.

* Renamed BLE UUID enums to be clearer.

* Change WeDo2 in comments to WeDo 2.0, etc.

* Changed some WeDo2Motor command names, cleaned up some JSDocs.

* Beginning a major EV3 refactor.

* WeDo2 formatting and comment changes.

* Motor refactoring in EV3: motorTurnClockwise and motorTurnCounterClockwise initial working state.

* Add reminders to possibly cast motor menu args in WeDo2.

* Continue to move motor commands in EV3 to EV3Motor class, don't create new EV3Motor on every poll cycle, etc.

* Factoring EV3 polling value commands, etc.

* Fixing EV3 motor power, position and button pressed, and some commenting, etc.

* Move EV3 motor position parsing to EV3Motor class, move directCommand and directCompoundCommand functions, some commenting, etc.

* Changed WeDo2 motor label enum name.

* Removed some EV3 motor functions that aren't needed, changed menu label enum names, moved some opcodes up to enums.

* Fixing comments and documentation.

* Some commenting.

* Adding further documentation and references to PDFs, changed reply check to be safer, etc.

* Some comment changes.

* Moving some functions around in EV3 and WeDo2 to match.

* Commenting, etc.

* Some renaming of session, etc.

* Fix stopAllMotors in EV3.

* Fixing clearing of motors in EV3.

* Some comment changes.

* Change runtime .extensions/registerExtension to .peripheralExtensions/registerPeripheralExtension.

* Renaming outputCommand/inputCommand to generateOutputCommand/generateInputCommand, etc.

* Moved motorCommandIDs to EV3Motor class, renamed directCommand to generateCommand, etc.

* Adding a reminder to rename something.

* JSDoc fix in EV3Motor class.

* Fixing microbit function name.

* Adding a todo item.

* Changing Ev3 menu formats to be backwards compatible, moving a BLE function up.

* Fixing EV3 ports again, and button pressed returning a boolean.

* Fixing menu value to be a string in EV3.
This commit is contained in:
Evelyn Eastmond 2018-09-07 17:01:23 -04:00 committed by Eric Rosenbaum
parent b41423bdfa
commit ec432e3b2f
9 changed files with 1476 additions and 1234 deletions

View file

@ -264,7 +264,10 @@ class Runtime extends EventEmitter {
video: new Video(this) video: new Video(this)
}; };
this.extensionDevices = {}; /**
* A list of extensions, used to manage hardware connection.
*/
this.peripheralExtensions = {};
/** /**
* A runtime profiler that records timed events for later playback to * A runtime profiler that records timed events for later playback to
@ -928,32 +931,56 @@ class Runtime extends EventEmitter {
(result, categoryInfo) => result.concat(categoryInfo.blocks.map(blockInfo => blockInfo.json)), []); (result, categoryInfo) => result.concat(categoryInfo.blocks.map(blockInfo => blockInfo.json)), []);
} }
registerExtensionDevice (extensionId, device) { /**
this.extensionDevices[extensionId] = device; * Register an extension that communications with a hardware peripheral by id,
* to have access to it and its peripheral functions in the future.
* @param {string} extensionId - the id of the extension.
* @param {object} extension - the extension to register.
*/
registerPeripheralExtension (extensionId, extension) {
this.peripheralExtensions[extensionId] = extension;
} }
startDeviceScan (extensionId) { /**
if (this.extensionDevices[extensionId]) { * Tell the specified extension to scan for a peripheral.
this.extensionDevices[extensionId].startDeviceScan(); * @param {string} extensionId - the id of the extension.
*/
scanForPeripheral (extensionId) {
if (this.peripheralExtensions[extensionId]) {
this.peripheralExtensions[extensionId].scan();
} }
} }
connectToPeripheral (extensionId, peripheralId) { /**
if (this.extensionDevices[extensionId]) { * Connect to the extension's specified peripheral.
this.extensionDevices[extensionId].connectDevice(peripheralId); * @param {string} extensionId - the id of the extension.
* @param {number} peripheralId - the id of the peripheral.
*/
connectPeripheral (extensionId, peripheralId) {
if (this.peripheralExtensions[extensionId]) {
this.peripheralExtensions[extensionId].connect(peripheralId);
} }
} }
disconnectExtensionSession (extensionId) { /**
if (this.extensionDevices[extensionId]) { * Disconnect from the extension's connected peripheral.
this.extensionDevices[extensionId].disconnectSession(); * @param {string} extensionId - the id of the extension.
*/
disconnectPeripheral (extensionId) {
if (this.peripheralExtensions[extensionId]) {
this.peripheralExtensions[extensionId].disconnect();
} }
} }
/**
* Returns whether the extension has a currently connected peripheral.
* @param {string} extensionId - the id of the extension.
* @return {boolean} - whether the extension has a connected peripheral.
*/
getPeripheralIsConnected (extensionId) { getPeripheralIsConnected (extensionId) {
let isConnected = false; let isConnected = false;
if (this.extensionDevices[extensionId]) { if (this.peripheralExtensions[extensionId]) {
isConnected = this.extensionDevices[extensionId].getPeripheralIsConnected(); isConnected = this.peripheralExtensions[extensionId].isConnected();
} }
return isConnected; return isConnected;
} }

File diff suppressed because one or more lines are too long

View file

@ -3,7 +3,7 @@ const BlockType = require('../../extension-support/block-type');
const log = require('../../util/log'); const log = require('../../util/log');
const cast = require('../../util/cast'); const cast = require('../../util/cast');
const formatMessage = require('format-message'); const formatMessage = require('format-message');
const BLESession = require('../../io/bleSession'); const BLE = require('../../io/ble');
const Base64Util = require('../../util/base64-util'); const Base64Util = require('../../util/base64-util');
/** /**
@ -25,7 +25,8 @@ const BLECommand = {
CMD_DISPLAY_LED: 0x82 CMD_DISPLAY_LED: 0x82
}; };
const BLETimeout = 4500; // TODO: might need tweaking based on how long the device takes to start sending data // 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) 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.
@ -46,7 +47,7 @@ const BLEUUID = {
}; };
/** /**
* Manage communication with a MicroBit device over a Scrath Link client socket. * Manage communication with a MicroBit peripheral over a Scrath Link client socket.
*/ */
class MicroBit { class MicroBit {
@ -65,12 +66,12 @@ class MicroBit {
this._runtime = runtime; this._runtime = runtime;
/** /**
* The BluetoothLowEnergy connection session for reading/writing device data. * The BluetoothLowEnergy connection socket for reading/writing peripheral data.
* @type {BLESession} * @type {BLE}
* @private * @private
*/ */
this._ble = null; this._ble = null;
this._runtime.registerExtensionDevice(extensionId, this); this._runtime.registerPeripheralExtension(extensionId, this);
/** /**
* The most recently received value for each sensor. * The most recently received value for each sensor.
@ -116,7 +117,7 @@ class MicroBit {
this._timeoutID = null; this._timeoutID = null;
/** /**
* A flag that is true while we are busy sending data to the BLE session. * A flag that is true while we are busy sending data to the BLE socket.
* @type {boolean} * @type {boolean}
* @private * @private
*/ */
@ -127,60 +128,30 @@ class MicroBit {
* true for a long time. * true for a long time.
*/ */
this._busyTimeoutID = null; this._busyTimeoutID = null;
}
// TODO: keep here? this.disconnect = this.disconnect.bind(this);
/** this._onConnect = this._onConnect.bind(this);
* Called by the runtime when user wants to scan for a device. this._onMessage = this._onMessage.bind(this);
*/
startDeviceScan () {
this._ble = new BLESession(this._runtime, {
filters: [
{services: [BLEUUID.service]}
]
}, this._onSessionConnect.bind(this));
}
// TODO: keep here?
/**
* Called by the runtime when user wants to connect to a certain device.
* @param {number} id - the id of the device to connect to.
*/
connectDevice (id) {
this._ble.connectDevice(id);
}
disconnectSession () {
window.clearInterval(this._timeoutID);
this._ble.disconnectSession();
}
getPeripheralIsConnected () {
let connected = false;
if (this._ble) {
connected = this._ble.getPeripheralIsConnected();
}
return connected;
} }
/** /**
* @param {string} text - the text to display. * @param {string} text - the text to display.
* @return {Promise} - a Promise that resolves when writing to device. * @return {Promise} - a Promise that resolves when writing to peripheral.
*/ */
displayText (text) { displayText (text) {
const output = new Uint8Array(text.length); const output = new Uint8Array(text.length);
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);
} }
return this._writeSessionData(BLECommand.CMD_DISPLAY_TEXT, output); return this.send(BLECommand.CMD_DISPLAY_TEXT, output);
} }
/** /**
* @param {Uint8Array} matrix - the matrix to display. * @param {Uint8Array} matrix - the matrix to display.
* @return {Promise} - a Promise that resolves when writing to device. * @return {Promise} - a Promise that resolves when writing to peripheral.
*/ */
displayMatrix (matrix) { displayMatrix (matrix) {
return this._writeSessionData(BLECommand.CMD_DISPLAY_LED, matrix); return this.send(BLECommand.CMD_DISPLAY_LED, matrix);
} }
/** /**
@ -226,20 +197,87 @@ class MicroBit {
} }
/** /**
* @param {number} pin - the pin to check touch state. * Called by the runtime when user wants to scan for a peripheral.
* @return {number} - the latest value received for the touch pin states.
*/ */
_checkPinState (pin) { scan () {
return this._sensors.touchPins[pin]; this._ble = new BLE(this._runtime, {
filters: [
{services: [BLEUUID.service]}
]
}, this._onConnect);
} }
/** /**
* Starts reading data from device after BLE has connected to it. * Called by the runtime when user wants to connect to a certain peripheral.
* @param {number} id - the id of the peripheral to connect to.
*/ */
_onSessionConnect () { connect (id) {
const callback = this._processSessionData.bind(this); this._ble.connectPeripheral(id);
this._ble.read(BLEUUID.service, BLEUUID.rxChar, true, callback); }
this._timeoutID = window.setInterval(this.disconnectSession.bind(this), BLETimeout);
/**
* Disconnect from the micro:bit.
*/
disconnect () {
window.clearInterval(this._timeoutID);
this._ble.disconnect();
}
/**
* Return true if connected to the micro:bit.
* @return {boolean} - whether the micro:bit is connected.
*/
isConnected () {
let connected = false;
if (this._ble) {
connected = this._ble.isConnected();
}
return connected;
}
/**
* Send a message to the peripheral BLE socket.
* @param {number} command - the BLE command hex.
* @param {Uint8Array} message - the message to write
*/
send (command, message) {
if (!this.isConnected()) return;
if (this._busy) return;
// Set a busy flag so that while we are sending a message and waiting for
// the response, additional messages are ignored.
this._busy = true;
// Set a timeout after which to reset the busy flag. This is used in case
// a BLE message was sent for which we never received a response, because
// e.g. the peripheral was turned off after the message was sent. We reset
// the busy flag after a while so that it is possible to try again later.
this._busyTimeoutID = window.setTimeout(() => {
this._busy = false;
}, 5000);
const output = new Uint8Array(message.length + 1);
output[0] = command; // attach command to beginning of message
for (let i = 0; i < message.length; i++) {
output[i + 1] = message[i];
}
const data = Base64Util.uint8ArrayToBase64(output);
this._ble.write(BLEUUID.service, BLEUUID.txChar, data, 'base64', true).then(
() => {
this._busy = false;
window.clearTimeout(this._busyTimeoutID);
}
);
}
/**
* Starts reading data from peripheral after BLE has connected to it.
* @private
*/
_onConnect () {
this._ble.read(BLEUUID.service, BLEUUID.rxChar, true, this._onMessage);
this._timeoutID = window.setInterval(this.disconnect, BLETimeout);
} }
/** /**
@ -247,7 +285,7 @@ class MicroBit {
* @param {object} base64 - the incoming BLE data. * @param {object} base64 - the incoming BLE data.
* @private * @private
*/ */
_processSessionData (base64) { _onMessage (base64) {
// parse data // parse data
const data = Base64Util.base64ToUint8Array(base64); const data = Base64Util.base64ToUint8Array(base64);
@ -267,44 +305,16 @@ 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.disconnectSession.bind(this), BLETimeout); this._timeoutID = window.setInterval(this.disconnect, BLETimeout);
} }
/** /**
* Send a message to the device BLE session. * @param {number} pin - the pin to check touch state.
* @param {number} command - the BLE command hex. * @return {number} - the latest value received for the touch pin states.
* @param {Uint8Array} message - the message to write
* @private * @private
*/ */
_writeSessionData (command, message) { _checkPinState (pin) {
if (!this.getPeripheralIsConnected()) return; return this._sensors.touchPins[pin];
if (this._busy) return;
// Set a busy flag so that while we are sending a message and waiting for
// the response, additional messages are ignored.
this._busy = true;
// Set a timeout after which to reset the busy flag. This is used in case
// a BLE message was sent for which we never received a response, because
// e.g. the device was turned off after the message was sent. We reset
// the busy flag after a while so that it is possible to try again later.
this._busyTimeoutID = window.setTimeout(() => {
this._busy = false;
}, 5000);
const output = new Uint8Array(message.length + 1);
output[0] = command; // attach command to beginning of message
for (let i = 0; i < message.length; i++) {
output[i + 1] = message[i];
}
const data = Base64Util.uint8ArrayToBase64(output);
this._ble.write(BLEUUID.service, BLEUUID.txChar, data, 'base64', true).then(
() => {
this._busy = false;
window.clearTimeout(this._busyTimeoutID);
}
);
} }
} }
@ -354,7 +364,7 @@ const PinState = {
}; };
/** /**
* Scratch 3.0 blocks to interact with a MicroBit device. * Scratch 3.0 blocks to interact with a MicroBit peripheral.
*/ */
class Scratch3MicroBitBlocks { class Scratch3MicroBitBlocks {
@ -527,8 +537,8 @@ class Scratch3MicroBitBlocks {
*/ */
this.runtime = runtime; this.runtime = runtime;
// Create a new MicroBit device instance // Create a new MicroBit peripheral instance
this._device = new MicroBit(this.runtime, Scratch3MicroBitBlocks.EXTENSION_ID); this._peripheral = new MicroBit(this.runtime, Scratch3MicroBitBlocks.EXTENSION_ID);
} }
/** /**
@ -725,11 +735,11 @@ class Scratch3MicroBitBlocks {
*/ */
whenButtonPressed (args) { whenButtonPressed (args) {
if (args.BTN === 'any') { if (args.BTN === 'any') {
return this._device.buttonA | this._device.buttonB; return this._peripheral.buttonA | this._peripheral.buttonB;
} else if (args.BTN === 'A') { } else if (args.BTN === 'A') {
return this._device.buttonA; return this._peripheral.buttonA;
} else if (args.BTN === 'B') { } else if (args.BTN === 'B') {
return this._device.buttonB; return this._peripheral.buttonB;
} }
return false; return false;
} }
@ -741,11 +751,11 @@ class Scratch3MicroBitBlocks {
*/ */
isButtonPressed (args) { isButtonPressed (args) {
if (args.BTN === 'any') { if (args.BTN === 'any') {
return this._device.buttonA | this._device.buttonB; return this._peripheral.buttonA | this._peripheral.buttonB;
} else if (args.BTN === 'A') { } else if (args.BTN === 'A') {
return this._device.buttonA; return this._peripheral.buttonA;
} else if (args.BTN === 'B') { } else if (args.BTN === 'B') {
return this._device.buttonB; return this._peripheral.buttonB;
} }
return false; return false;
} }
@ -758,11 +768,11 @@ class Scratch3MicroBitBlocks {
whenGesture (args) { whenGesture (args) {
const gesture = cast.toString(args.GESTURE); const gesture = cast.toString(args.GESTURE);
if (gesture === 'moved') { if (gesture === 'moved') {
return (this._device.gestureState >> 2) & 1; return (this._peripheral.gestureState >> 2) & 1;
} else if (gesture === 'shaken') { } else if (gesture === 'shaken') {
return this._device.gestureState & 1; return this._peripheral.gestureState & 1;
} else if (gesture === 'jumped') { } else if (gesture === 'jumped') {
return (this._device.gestureState >> 1) & 1; return (this._peripheral.gestureState >> 1) & 1;
} }
return false; return false;
} }
@ -780,12 +790,12 @@ class Scratch3MicroBitBlocks {
}; };
const hex = symbol.split('').reduce(reducer, 0); const hex = symbol.split('').reduce(reducer, 0);
if (hex !== null) { if (hex !== null) {
this._device.ledMatrixState[0] = hex & 0x1F; this._peripheral.ledMatrixState[0] = hex & 0x1F;
this._device.ledMatrixState[1] = (hex >> 5) & 0x1F; this._peripheral.ledMatrixState[1] = (hex >> 5) & 0x1F;
this._device.ledMatrixState[2] = (hex >> 10) & 0x1F; this._peripheral.ledMatrixState[2] = (hex >> 10) & 0x1F;
this._device.ledMatrixState[3] = (hex >> 15) & 0x1F; this._peripheral.ledMatrixState[3] = (hex >> 15) & 0x1F;
this._device.ledMatrixState[4] = (hex >> 20) & 0x1F; this._peripheral.ledMatrixState[4] = (hex >> 20) & 0x1F;
this._device.displayMatrix(this._device.ledMatrixState); this._peripheral.displayMatrix(this._peripheral.ledMatrixState);
} }
return new Promise(resolve => { return new Promise(resolve => {
@ -803,7 +813,7 @@ class Scratch3MicroBitBlocks {
*/ */
displayText (args) { displayText (args) {
const text = String(args.TEXT).substring(0, 19); const text = String(args.TEXT).substring(0, 19);
if (text.length > 0) this._device.displayText(text); if (text.length > 0) this._peripheral.displayText(text);
return new Promise(resolve => { return new Promise(resolve => {
setTimeout(() => { setTimeout(() => {
@ -818,9 +828,9 @@ class Scratch3MicroBitBlocks {
*/ */
displayClear () { displayClear () {
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
this._device.ledMatrixState[i] = 0; this._peripheral.ledMatrixState[i] = 0;
} }
this._device.displayMatrix(this._device.ledMatrixState); this._peripheral.displayMatrix(this._peripheral.ledMatrixState);
return new Promise(resolve => { return new Promise(resolve => {
setTimeout(() => { setTimeout(() => {
@ -868,8 +878,8 @@ class Scratch3MicroBitBlocks {
_isTilted (direction) { _isTilted (direction) {
switch (direction) { switch (direction) {
case TiltDirection.ANY: case TiltDirection.ANY:
return (Math.abs(this._device.tiltX / 10) >= Scratch3MicroBitBlocks.TILT_THRESHOLD) || return (Math.abs(this._peripheral.tiltX / 10) >= Scratch3MicroBitBlocks.TILT_THRESHOLD) ||
(Math.abs(this._device.tiltY / 10) >= Scratch3MicroBitBlocks.TILT_THRESHOLD); (Math.abs(this._peripheral.tiltY / 10) >= Scratch3MicroBitBlocks.TILT_THRESHOLD);
default: default:
return this._getTiltAngle(direction) >= Scratch3MicroBitBlocks.TILT_THRESHOLD; return this._getTiltAngle(direction) >= Scratch3MicroBitBlocks.TILT_THRESHOLD;
} }
@ -884,13 +894,13 @@ class Scratch3MicroBitBlocks {
_getTiltAngle (direction) { _getTiltAngle (direction) {
switch (direction) { switch (direction) {
case TiltDirection.FRONT: case TiltDirection.FRONT:
return Math.round(this._device.tiltY / -10); return Math.round(this._peripheral.tiltY / -10);
case TiltDirection.BACK: case TiltDirection.BACK:
return Math.round(this._device.tiltY / 10); return Math.round(this._peripheral.tiltY / 10);
case TiltDirection.LEFT: case TiltDirection.LEFT:
return Math.round(this._device.tiltX / -10); return Math.round(this._peripheral.tiltX / -10);
case TiltDirection.RIGHT: case TiltDirection.RIGHT:
return Math.round(this._device.tiltX / 10); return Math.round(this._peripheral.tiltX / 10);
default: default:
log.warn(`Unknown tilt direction in _getTiltAngle: ${direction}`); log.warn(`Unknown tilt direction in _getTiltAngle: ${direction}`);
} }
@ -905,7 +915,7 @@ class Scratch3MicroBitBlocks {
const pin = parseInt(args.PIN, 10); const pin = parseInt(args.PIN, 10);
if (isNaN(pin)) return; if (isNaN(pin)) return;
if (pin < 0 || pin > 2) return false; if (pin < 0 || pin > 2) return false;
return this._device._checkPinState(pin); return this._peripheral._checkPinState(pin);
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -1,22 +1,22 @@
const JSONRPCWebSocket = require('../util/jsonrpc-web-socket'); const JSONRPCWebSocket = require('../util/jsonrpc-web-socket');
// const log = require('../util/log');
const ScratchLinkWebSocket = 'wss://device-manager.scratch.mit.edu:20110/scratch/ble'; const ScratchLinkWebSocket = 'wss://device-manager.scratch.mit.edu:20110/scratch/ble';
// const log = require('../util/log');
class BLESession extends JSONRPCWebSocket { class BLE extends JSONRPCWebSocket {
/** /**
* A BLE device session object. It handles connecting, over web sockets, to * A BLE peripheral socket object. It handles connecting, over web sockets, to
* BLE devices, and reading and writing data to them. * BLE peripherals, and reading and writing data to them.
* @param {Runtime} runtime - the Runtime for sending/receiving GUI update events. * @param {Runtime} runtime - the Runtime for sending/receiving GUI update events.
* @param {object} deviceOptions - the list of options for device 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.
*/ */
constructor (runtime, deviceOptions, connectCallback) { constructor (runtime, peripheralOptions, connectCallback) {
const ws = new WebSocket(ScratchLinkWebSocket); const ws = new WebSocket(ScratchLinkWebSocket);
super(ws); super(ws);
this._ws = ws; this._ws = ws;
this._ws.onopen = this.requestDevice.bind(this); // only call request device after socket opens this._ws.onopen = this.requestPeripheral.bind(this); // only call request peripheral after socket opens
this._ws.onerror = this._sendError.bind(this, 'ws onerror'); this._ws.onerror = this._sendError.bind(this, 'ws onerror');
this._ws.onclose = this._sendError.bind(this, 'ws onclose'); this._ws.onclose = this._sendError.bind(this, 'ws onclose');
@ -24,21 +24,23 @@ class BLESession extends JSONRPCWebSocket {
this._connectCallback = connectCallback; this._connectCallback = connectCallback;
this._connected = false; this._connected = false;
this._characteristicDidChangeCallback = null; this._characteristicDidChangeCallback = null;
this._deviceOptions = deviceOptions; this._peripheralOptions = peripheralOptions;
this._discoverTimeoutID = null; this._discoverTimeoutID = null;
this._runtime = runtime; this._runtime = runtime;
} }
/** /**
* Request connection to the device. * Request connection to the peripheral.
* If the web socket is not yet open, request when the socket promise resolves. * If the web socket is not yet open, request when the socket promise resolves.
*/ */
requestDevice () { requestPeripheral () {
if (this._ws.readyState === 1) { // is this needed since it's only called on ws.onopen? if (this._ws.readyState === 1) { // is this needed since it's only called on ws.onopen?
this._availablePeripherals = {}; this._availablePeripherals = {};
this._discoverTimeoutID = window.setTimeout(this._sendDiscoverTimeout.bind(this), 15000); this._discoverTimeoutID = window.setTimeout(this._sendDiscoverTimeout.bind(this), 15000);
this.sendRemoteRequest('discover', this._deviceOptions) this.sendRemoteRequest('discover', this._peripheralOptions)
.catch(e => this._sendError(e)); // never reached? .catch(e => {
this._sendError(e);
}); // never reached?
} }
// TODO: else? // TODO: else?
} }
@ -48,7 +50,7 @@ class BLESession extends JSONRPCWebSocket {
* callback if connection is successful. * callback if connection is successful.
* @param {number} id - the id of the peripheral to connect to * @param {number} id - the id of the peripheral to connect to
*/ */
connectDevice (id) { connectPeripheral (id) {
this.sendRemoteRequest('connect', {peripheralId: id}) this.sendRemoteRequest('connect', {peripheralId: id})
.then(() => { .then(() => {
this._connected = true; this._connected = true;
@ -63,7 +65,7 @@ class BLESession extends JSONRPCWebSocket {
/** /**
* Close the websocket. * Close the websocket.
*/ */
disconnectSession () { disconnect () {
this._ws.close(); this._ws.close();
this._connected = false; this._connected = false;
} }
@ -71,37 +73,10 @@ class BLESession extends JSONRPCWebSocket {
/** /**
* @return {bool} whether the peripheral is connected. * @return {bool} whether the peripheral is connected.
*/ */
getPeripheralIsConnected () { isConnected () {
return this._connected; return this._connected;
} }
/**
* 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) {
// TODO: window?
window.clearTimeout(this._discoverTimeoutID);
}
break;
case 'characteristicDidChange':
this._characteristicDidChangeCallback(params.message);
break;
case 'ping':
return 42;
}
}
/** /**
* Start receiving notifications from the specified ble service. * Start receiving notifications from the specified ble service.
* @param {number} serviceId - the ble service to read. * @param {number} serviceId - the ble service to read.
@ -167,9 +142,36 @@ class BLESession extends JSONRPCWebSocket {
}); });
} }
/**
* 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) {
// TODO: window?
window.clearTimeout(this._discoverTimeoutID);
}
break;
case 'characteristicDidChange':
this._characteristicDidChangeCallback(params.message);
break;
case 'ping':
return 42;
}
}
_sendError (/* e */) { _sendError (/* e */) {
this.disconnectSession(); this.disconnect();
// log.error(`BLESession error: ${JSON.stringify(e)}`); // log.error(`BLE error: ${JSON.stringify(e)}`);
this._runtime.emit(this._runtime.constructor.PERIPHERAL_ERROR); this._runtime.emit(this._runtime.constructor.PERIPHERAL_ERROR);
} }
@ -178,4 +180,4 @@ class BLESession extends JSONRPCWebSocket {
} }
} }
module.exports = BLESession; module.exports = BLE;

View file

@ -1,23 +1,23 @@
const JSONRPCWebSocket = require('../util/jsonrpc-web-socket'); const JSONRPCWebSocket = require('../util/jsonrpc-web-socket');
// const log = require('../util/log');
const ScratchLinkWebSocket = 'wss://device-manager.scratch.mit.edu:20110/scratch/bt'; const ScratchLinkWebSocket = 'wss://device-manager.scratch.mit.edu:20110/scratch/bt';
// const log = require('../util/log');
class BTSession extends JSONRPCWebSocket { class BT extends JSONRPCWebSocket {
/** /**
* A BT device session object. It handles connecting, over web sockets, to * A BT peripheral socket object. It handles connecting, over web sockets, to
* BT devices, and reading and writing data to them. * BT peripherals, and reading and writing data to them.
* @param {Runtime} runtime - the Runtime for sending/receiving GUI update events. * @param {Runtime} runtime - the Runtime for sending/receiving GUI update events.
* @param {object} deviceOptions - the list of options for device 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} messageCallback - a callback for message sending. * @param {object} messageCallback - a callback for message sending.
*/ */
constructor (runtime, deviceOptions, connectCallback, messageCallback) { constructor (runtime, peripheralOptions, connectCallback, 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.requestDevice.bind(this); // only call request device after socket opens this._ws.onopen = this.requestPeripheral.bind(this); // only call request peripheral after socket opens
this._ws.onerror = this._sendError.bind(this, 'ws onerror'); this._ws.onerror = this._sendError.bind(this, 'ws onerror');
this._ws.onclose = this._sendError.bind(this, 'ws onclose'); this._ws.onclose = this._sendError.bind(this, 'ws onclose');
@ -25,21 +25,21 @@ class BTSession extends JSONRPCWebSocket {
this._connectCallback = connectCallback; this._connectCallback = connectCallback;
this._connected = false; this._connected = false;
this._characteristicDidChangeCallback = null; this._characteristicDidChangeCallback = null;
this._deviceOptions = deviceOptions; this._peripheralOptions = peripheralOptions;
this._discoverTimeoutID = null; this._discoverTimeoutID = null;
this._messageCallback = messageCallback; this._messageCallback = messageCallback;
this._runtime = runtime; this._runtime = runtime;
} }
/** /**
* Request connection to the device. * Request connection to the peripheral.
* If the web socket is not yet open, request when the socket promise resolves. * If the web socket is not yet open, request when the socket promise resolves.
*/ */
requestDevice () { requestPeripheral () {
if (this._ws.readyState === 1) { // is this needed since it's only called on ws.onopen? if (this._ws.readyState === 1) { // is this needed since it's only called on ws.onopen?
this._availablePeripherals = {}; this._availablePeripherals = {};
this._discoverTimeoutID = window.setTimeout(this._sendDiscoverTimeout.bind(this), 15000); this._discoverTimeoutID = window.setTimeout(this._sendDiscoverTimeout.bind(this), 15000);
this.sendRemoteRequest('discover', this._deviceOptions) this.sendRemoteRequest('discover', this._peripheralOptions)
.catch(e => this._sendError(e)); // never reached? .catch(e => this._sendError(e)); // never reached?
} }
// TODO: else? // TODO: else?
@ -50,7 +50,7 @@ class BTSession extends JSONRPCWebSocket {
* callback if connection is successful. * callback if connection is successful.
* @param {number} id - the id of the peripheral to connect to * @param {number} id - the id of the peripheral to connect to
*/ */
connectDevice (id) { connectPeripheral (id) {
this.sendRemoteRequest('connect', {peripheralId: id}) this.sendRemoteRequest('connect', {peripheralId: id})
.then(() => { .then(() => {
this._connected = true; this._connected = true;
@ -65,7 +65,7 @@ class BTSession extends JSONRPCWebSocket {
/** /**
* Close the websocket. * Close the websocket.
*/ */
disconnectSession () { disconnect () {
this._ws.close(); this._ws.close();
this._connected = false; this._connected = false;
} }
@ -73,7 +73,7 @@ class BTSession extends JSONRPCWebSocket {
/** /**
* @return {bool} whether the peripheral is connected. * @return {bool} whether the peripheral is connected.
*/ */
getPeripheralIsConnected () { isConnected () {
return this._connected; return this._connected;
} }
@ -113,8 +113,8 @@ class BTSession extends JSONRPCWebSocket {
} }
_sendError (/* e */) { _sendError (/* e */) {
this.disconnectSession(); this.disconnect();
// log.error(`BTSession error: ${JSON.stringify(e)}`); // log.error(`BT error: ${JSON.stringify(e)}`);
this._runtime.emit(this._runtime.constructor.PERIPHERAL_ERROR); this._runtime.emit(this._runtime.constructor.PERIPHERAL_ERROR);
} }
@ -123,4 +123,4 @@ class BTSession extends JSONRPCWebSocket {
} }
} }
module.exports = BTSession; module.exports = BT;

View file

@ -105,7 +105,6 @@ class VirtualMachine extends EventEmitter {
this.runtime.on(Runtime.BLOCKSINFO_UPDATE, blocksInfo => { this.runtime.on(Runtime.BLOCKSINFO_UPDATE, blocksInfo => {
this.emit(Runtime.BLOCKSINFO_UPDATE, blocksInfo); this.emit(Runtime.BLOCKSINFO_UPDATE, blocksInfo);
}); });
this.runtime.on(Runtime.PERIPHERAL_LIST_UPDATE, info => { this.runtime.on(Runtime.PERIPHERAL_LIST_UPDATE, info => {
this.emit(Runtime.PERIPHERAL_LIST_UPDATE, info); this.emit(Runtime.PERIPHERAL_LIST_UPDATE, info);
}); });
@ -213,18 +212,36 @@ class VirtualMachine extends EventEmitter {
this.runtime.ioDevices.video.setProvider(videoProvider); this.runtime.ioDevices.video.setProvider(videoProvider);
} }
startDeviceScan (extensionId) { /**
this.runtime.startDeviceScan(extensionId); * Tell the specified extension to scan for a peripheral.
* @param {string} extensionId - the id of the extension.
*/
scanForPeripheral (extensionId) {
this.runtime.scanForPeripheral(extensionId);
} }
connectToPeripheral (extensionId, peripheralId) { /**
this.runtime.connectToPeripheral(extensionId, peripheralId); * Connect to the extension's specified peripheral.
* @param {string} extensionId - the id of the extension.
* @param {number} peripheralId - the id of the peripheral.
*/
connectPeripheral (extensionId, peripheralId) {
this.runtime.connectPeripheral(extensionId, peripheralId);
} }
disconnectExtensionSession (extensionId) { /**
this.runtime.disconnectExtensionSession(extensionId); * Disconnect from the extension's connected peripheral.
* @param {string} extensionId - the id of the extension.
*/
disconnectPeripheral (extensionId) {
this.runtime.disconnectPeripheral(extensionId);
} }
/**
* Returns whether the extension has a currently connected peripheral.
* @param {string} extensionId - the id of the extension.
* @return {boolean} - whether the extension has a connected peripheral.
*/
getPeripheralIsConnected (extensionId) { getPeripheralIsConnected (extensionId) {
return this.runtime.getPeripheralIsConnected(extensionId); return this.runtime.getPeripheralIsConnected(extensionId);
} }

View file

@ -9,7 +9,7 @@ test('waitForSocket', t => {
t.end(); t.end();
}); });
test('requestDevice', t => { test('requestPeripheral', t => {
t.end(); t.end();
}); });

View file

@ -5,11 +5,11 @@ test('constructor', t => {
t.end(); t.end();
}); });
test('requestDevice', t => { test('requestPeripheral', t => {
t.end(); t.end();
}); });
test('connectDevice', t => { test('connectPeripheral', t => {
t.end(); t.end();
}); });