Merge pull request #2182 from paulkaplan/scratch-link-socket

Initial prototype of configurable scratch link socket
This commit is contained in:
Paul Kaplan 2019-06-04 10:11:22 -04:00 committed by GitHub
commit 6b04f62863
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 170 additions and 91 deletions

View file

@ -16,6 +16,7 @@ const maybeFormatMessage = require('../util/maybe-format-message');
const StageLayering = require('./stage-layering');
const Variable = require('./variable');
const xmlEscape = require('../util/xml-escape');
const ScratchLinkWebSocket = require('../util/scratch-link-websocket');
// Virtual I/O devices.
const Clock = require('../io/clock');
@ -1289,6 +1290,34 @@ class Runtime extends EventEmitter {
(result, categoryInfo) => result.concat(categoryInfo.blocks.map(blockInfo => blockInfo.json)), []);
}
/**
* Get a scratch link socket.
* @param {string} type Either BLE or BT
* @returns {ScratchLinkSocket} The scratch link socket.
*/
getScratchLinkSocket (type) {
const factory = this._linkSocketFactory || this._defaultScratchLinkSocketFactory;
return factory(type);
}
/**
* Configure how ScratchLink sockets are created. Factory must consume a "type" parameter
* either BT or BLE.
* @param {Function} factory The new factory for creating ScratchLink sockets.
*/
configureScratchLinkSocketFactory (factory) {
this._linkSocketFactory = factory;
}
/**
* The default scratch link socket creator, using websockets to the installed device manager.
* @param {string} type Either BLE or BT
* @returns {ScratchLinkSocket} The new scratch link socket (a WebSocket object)
*/
_defaultScratchLinkSocketFactory (type) {
return new ScratchLinkWebSocket(type);
}
/**
* Register an extension that communications with a hardware peripheral by id,
* to have access to it and its peripheral functions in the future.

View file

@ -1,8 +1,6 @@
const JSONRPCWebSocket = require('../util/jsonrpc-web-socket');
const ScratchLinkWebSocket = 'wss://device-manager.scratch.mit.edu:20110/scratch/ble';
// const log = require('../util/log');
const JSONRPC = require('../util/jsonrpc');
class BLE extends JSONRPCWebSocket {
class BLE extends JSONRPC {
/**
* A BLE peripheral socket object. It handles connecting, over web sockets, to
@ -14,13 +12,15 @@ class BLE extends JSONRPCWebSocket {
* @param {object} disconnectCallback - a callback for disconnection.
*/
constructor (runtime, extensionId, peripheralOptions, connectCallback, disconnectCallback = null) {
const ws = new WebSocket(ScratchLinkWebSocket);
super(ws);
super();
this._ws = ws;
this._ws.onopen = this.requestPeripheral.bind(this); // only call request peripheral after socket opens
this._ws.onerror = this._handleRequestError.bind(this, 'ws onerror');
this._ws.onclose = this.handleDisconnectError.bind(this, 'ws onclose');
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;
@ -31,6 +31,8 @@ class BLE extends JSONRPCWebSocket {
this._extensionId = extensionId;
this._peripheralOptions = peripheralOptions;
this._runtime = runtime;
this._socket.open();
}
/**
@ -38,18 +40,15 @@ class BLE extends JSONRPCWebSocket {
* If the web socket is not yet open, request when the socket promise resolves.
*/
requestPeripheral () {
if (this._ws.readyState === 1) { // is this needed since it's only called on ws.onopen?
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);
});
this._availablePeripherals = {};
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}
// TODO: else?
this._discoverTimeoutID = window.setTimeout(this._handleDiscoverTimeout.bind(this), 15000);
this.sendRemoteRequest('discover', this._peripheralOptions)
.catch(e => {
this._handleRequestError(e);
});
}
/**
@ -73,14 +72,14 @@ class BLE extends JSONRPCWebSocket {
* Close the websocket.
*/
disconnect () {
if (this._ws.readyState === this._ws.OPEN) {
this._ws.close();
}
if (this._connected) {
this._connected = false;
}
if (this._socket.isOpen()) {
this._socket.close();
}
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}

View file

@ -1,8 +1,6 @@
const JSONRPCWebSocket = require('../util/jsonrpc-web-socket');
const ScratchLinkWebSocket = 'wss://device-manager.scratch.mit.edu:20110/scratch/bt';
// const log = require('../util/log');
const JSONRPC = require('../util/jsonrpc');
class BT extends JSONRPCWebSocket {
class BT extends JSONRPC {
/**
* A BT peripheral socket object. It handles connecting, over web sockets, to
@ -15,13 +13,15 @@ class BT extends JSONRPCWebSocket {
* @param {object} messageCallback - a callback for message sending.
*/
constructor (runtime, extensionId, peripheralOptions, connectCallback, disconnectCallback = null, messageCallback) {
const ws = new WebSocket(ScratchLinkWebSocket);
super(ws);
super();
this._ws = ws;
this._ws.onopen = this.requestPeripheral.bind(this); // only call request peripheral after socket opens
this._ws.onerror = this._handleRequestError.bind(this, 'ws onerror');
this._ws.onclose = this.handleDisconnectError.bind(this, 'ws onclose');
this._socket = runtime.getScratchLinkSocket('BT');
this._socket.setOnOpen(this.requestPeripheral.bind(this));
this._socket.setOnError(this._handleRequestError.bind(this));
this._socket.setOnClose(this.handleDisconnectError.bind(this));
this._socket.setHandleMessage(this._handleMessage.bind(this));
this._sendMessage = this._socket.sendMessage.bind(this._socket);
this._availablePeripherals = {};
this._connectCallback = connectCallback;
@ -33,6 +33,8 @@ class BT extends JSONRPCWebSocket {
this._peripheralOptions = peripheralOptions;
this._messageCallback = messageCallback;
this._runtime = runtime;
this._socket.open();
}
/**
@ -40,18 +42,15 @@ class BT extends JSONRPCWebSocket {
* If the web socket is not yet open, request when the socket promise resolves.
*/
requestPeripheral () {
if (this._ws.readyState === 1) { // is this needed since it's only called on ws.onopen?
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)
);
this._availablePeripherals = {};
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}
// TODO: else?
this._discoverTimeoutID = window.setTimeout(this._handleDiscoverTimeout.bind(this), 15000);
this.sendRemoteRequest('discover', this._peripheralOptions)
.catch(
e => this._handleRequestError(e)
);
}
/**
@ -75,14 +74,14 @@ class BT extends JSONRPCWebSocket {
* Close the websocket.
*/
disconnect () {
if (this._ws.readyState === this._ws.OPEN) {
this._ws.close();
}
if (this._connected) {
this._connected = false;
}
if (this._socket.isOpen()) {
this._socket.close();
}
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}

View file

@ -1,40 +0,0 @@
const JSONRPC = require('./jsonrpc');
// const log = require('../util/log');
class JSONRPCWebSocket extends JSONRPC {
constructor (webSocket) {
super();
this._ws = webSocket;
this._ws.onmessage = e => this._onSocketMessage(e);
this._ws.onopen = e => this._onSocketOpen(e);
this._ws.onclose = e => this._onSocketClose(e);
this._ws.onerror = e => this._onSocketError(e);
}
dispose () {
this._ws.close();
this._ws = null;
}
_onSocketOpen () {
}
_onSocketClose () {
}
_onSocketError () {
}
_onSocketMessage (e) {
const json = JSON.parse(e.data);
this._handleMessage(json);
}
_sendMessage (message) {
const messageText = JSON.stringify(message);
this._ws.send(messageText);
}
}
module.exports = JSONRPCWebSocket;

View file

@ -0,0 +1,84 @@
/**
* This class provides a ScratchLinkSocket implementation using WebSockets,
* attempting to connect with the locally installed Scratch-Link.
*
* To connect with ScratchLink without WebSockets, you must implement all of the
* public methods in this class.
* - open()
* - close()
* - setOn[Open|Close|Error]
* - setHandleMessage
* - sendMessage(msgObj)
* - isOpen()
*/
class ScratchLinkWebSocket {
constructor (type) {
this._type = type;
this._onOpen = null;
this._onClose = null;
this._onError = null;
this._handleMessage = null;
this._ws = null;
}
open () {
switch (this._type) {
case 'BLE':
this._ws = new WebSocket('wss://device-manager.scratch.mit.edu:20110/scratch/ble');
break;
case 'BT':
this._ws = new WebSocket('wss://device-manager.scratch.mit.edu:20110/scratch/bt');
break;
default:
throw new Error(`Unknown ScratchLink socket Type: ${this._type}`);
}
if (this._onOpen && this._onClose && this._onError && this._handleMessage) {
this._ws.onopen = this._onOpen;
this._ws.onclose = this._onClose;
this._ws.onerror = this._onError;
} else {
throw new Error('Must set open, close, message and error handlers before calling open on the socket');
}
this._ws.onmessage = this._onMessage.bind(this);
}
close () {
this._ws.close();
this._ws = null;
}
sendMessage (message) {
const messageText = JSON.stringify(message);
this._ws.send(messageText);
}
setOnOpen (fn) {
this._onOpen = fn;
}
setOnClose (fn) {
this._onClose = fn;
}
setOnError (fn) {
this._onError = fn;
}
setHandleMessage (fn) {
this._handleMessage = fn;
}
isOpen () {
return this._ws && this._ws.readyState === this._ws.OPEN;
}
_onMessage (e) {
const json = JSON.parse(e.data);
this._handleMessage(json);
}
}
module.exports = ScratchLinkWebSocket;

View file

@ -1525,6 +1525,14 @@ class VirtualMachine extends EventEmitter {
}
return null;
}
/**
* Allow VM consumer to configure the ScratchLink socket creator.
* @param {Function} factory The custom ScratchLink socket factory.
*/
configureScratchLinkSocketFactory (factory) {
this.runtime.configureScratchLinkSocketFactory(factory);
}
}
module.exports = VirtualMachine;