Update scratch_extensions for upcoming scratchr2

This corresponds to the upcoming October 20th release
This commit is contained in:
Christopher Willis-Ford 2016-10-07 12:09:17 -07:00
parent ed239eb0f0
commit 47d9063829
4 changed files with 300 additions and 127 deletions

View file

@ -1,91 +1,99 @@
// Communicate with the Scratch Device Manager through Socket.IO
window.ScratchDeviceManager = new (function () {
var self = this;
var instance = this;
var sockets = [];
// Assume it's OK until we find out otherwise
var isConnected = true;
// device-manager.scratch.mit.edu = 127.0.0.1
self.deviceManagerHost = 'https://device-manager.scratch.mit.edu:3030';
instance.deviceManagerHost = 'https://device-manager.scratch.mit.edu:3030';
// work around https://github.com/socketio/socket.io-client/issues/812
function connectNamespace(namespace) {
return io(self.deviceManagerHost + namespace, {forceNew: true});
return io(instance.deviceManagerHost + namespace, {forceNew: true});
}
self.wedo2_list = function (callback) {
$.ajax(self.deviceManagerHost + '/wedo2/list', {
dataType: 'text',
function onClose(){
for(var i=0; i<sockets.length; i++){
sockets[i].disconnect();
}
}
window.onbeforeunload = onClose;
instance.device_list = function (ext_type, ext_name, device_spec, callback) {
var url = instance.deviceManagerHost + '/' + ext_type + '/list';
var data = {
name: ext_name,
spec: device_spec
};
$.ajax(url, {
data: {data: JSON.stringify(data)},
dataType: 'json',
success: function (data, textStatus, jqXHR) {
var deviceList = JSON.parse(data);
if (deviceList.constructor == Array) {
callback(deviceList);
isConnected = true;
if (data.constructor == Array) {
callback(data, ext_type, ext_name);
}
},
error: function (jqXHR, textStatus, errorThrown) {
isConnected = false;
}
});
};
// TODO: handle multiple devices
self.wedo2_open = function (deviceId, callback) {
var socket = connectNamespace('/wedo2');
var pluginDevice = new RawWeDo2(deviceId, socket);
socket.on('deviceWasOpened', function (event) {
callback(pluginDevice);
});
socket.emit('open', {deviceId: deviceId});
};
function RawWeDo2(deviceId, socket) {
var WeDo = this;
var eventHandlers = {};
WeDo.close = function() {
socket.close();
};
WeDo.setMotorOn = function(motorIndex, power) {
socket.emit('motorOn', {motorIndex:motorIndex, power:power});
};
WeDo.setMotorOff = function(motorIndex) {
socket.emit('motorOff', {motorIndex:motorIndex});
};
WeDo.setMotorBrake = function(motorIndex) {
socket.emit('motorBrake', {motorIndex:motorIndex});
};
WeDo.setLED = function(rgb) {
socket.emit('setLED', {rgb:rgb});
};
WeDo.playTone = function(tone, durationMs) {
socket.emit('playTone', {tone:tone, ms:durationMs});
};
WeDo.stopTone = function() {
socket.emit('stopTone');
};
function setHandler(eventName, handler) {
if (eventHandlers.hasOwnProperty(eventName)) {
var oldHandler = eventHandlers[eventName];
if (oldHandler) {
socket.removeListener(eventName, oldHandler);
// Attempt to open a device-specific socket connection to the Device Manager.
// This must call `callback` exactly once no matter what.
// The callback will receive a connected socket on success or `null` on failure.
instance.socket_open = function (ext_name, deviceType, deviceId, callback) {
function onDeviceWasOpened () {
// If this is the first event on this socket then respond with success.
if (clearOpenTimeout()) {
callback(socket);
}
}
if (handler) {
socket.on(eventName, handler);
function onDisconnect () {
var socketIndex = sockets.indexOf(socket);
if (socketIndex >= 0) {
sockets.splice(socketIndex, 1);
}
// If this is the first event on this socket then respond with failure.
if (clearOpenTimeout()) {
callback(null);
}
eventHandlers[eventName] = handler;
}
// function handler(event) { access event.sensorName and event.sensorValue }
WeDo.setSensorHandler = function (handler) {
setHandler('sensorChanged', handler);
function onTimeout () {
// This will trigger `onDisconnect()`
socket.disconnect();
}
// If the timeout is still pending, clear it and return true. Otherwise, return false.
// Callers can use the return value to determine whether they are the first to respond on this socket.
function clearOpenTimeout () {
if (openTimeout !== null) {
clearTimeout(openTimeout);
openTimeout = null;
return true;
}
else {
return false;
}
}
var socket = connectNamespace('/' + deviceType);
sockets.push(socket);
socket.on('deviceWasOpened', onDeviceWasOpened);
socket.on('disconnect', onDisconnect);
var openTimeout = setTimeout(onTimeout, 10 * 1000);
socket.emit('open', {deviceId: deviceId, name: ext_name});
};
WeDo.setDeviceWasClosedHandler = function (handler) {
// TODO: resolve this ambiguity
setHandler('disconnect', handler);
setHandler('deviceWasClosed', handler);
instance.isConnected = function () {
return isConnected;
};
}
})();

View file

@ -106,8 +106,13 @@ window.ScratchExtensions = new (function () {
var callback = function (retval) {
Scratch.FlashApp.ASobj.ASextensionReporterDone(ext_name, job_id, retval);
};
if(handlers[ext_name]._getStatus().status != 2){
callback(false);
}
else{
args.push(callback);
handlers[ext_name][reporter].apply(handlers[ext_name], args);
}
};
lib.getReporterForceAsync = function (ext_name, reporter, args, job_id) {
@ -134,8 +139,9 @@ window.ScratchExtensions = new (function () {
if (ext_name in deviceSpecs) {
switch (deviceSpecs[ext_name].type) {
case 'ble':
case 'wedo2':
if (!ScratchDeviceManager) {
if (!(ScratchDeviceManager && ScratchDeviceManager.isConnected())) {
return {status: 0, msg: 'Missing Scratch Device Manager'};
}
break;
@ -195,29 +201,28 @@ window.ScratchExtensions = new (function () {
if (!awaitingSpecs['hid']) awaitingSpecs['hid'] = {};
awaitingSpecs['hid'][spec.vendor + '_' + spec.product] = ext_name;
}
else if (spec.type == 'serial') {
awaitingSpecs['serial'] = ext_name;
}
else if (spec.type == 'wedo2') {
awaitingSpecs['wedo2'] = ext_name;
else {
awaitingSpecs[spec.type] = ext_name;
}
}
}
if (plugin) {
if (awaitingSpecs['hid']) {
for (var specType in awaitingSpecs) {
if (!awaitingSpecs.hasOwnProperty(specType)) continue;
if (plugin && specType == 'hid') {
var awaitingHid = awaitingSpecs['hid'];
plugin.hid_list(function (deviceList) {
var hidList = awaitingSpecs['hid'];
for (var i = 0; i < deviceList.length; i++) {
var ext_name = hidList[deviceList[i]["vendor_id"] + '_' + deviceList[i]["product_id"]];
if (ext_name) {
handlers[ext_name]._deviceConnected(new HidDevice(deviceList[i], ext_name));
var deviceID = deviceList[i]["vendor_id"] + '_' + deviceList[i]["product_id"];
var hid_ext_name = awaitingHid[deviceID];
if (hid_ext_name) {
handlers[hid_ext_name]._deviceConnected(new HidDevice(deviceList[i], hid_ext_name));
}
}
});
}
if (awaitingSpecs['serial']) {
else if (plugin && specType == 'serial') {
ext_name = awaitingSpecs['serial'];
plugin.serial_list(function (deviceList) {
for (var i = 0; i < deviceList.length; i++) {
@ -225,15 +230,10 @@ window.ScratchExtensions = new (function () {
}
});
}
else if (ScratchDeviceManager) {
ext_name = awaitingSpecs[specType];
ScratchDeviceManager.device_list(specType, ext_name, deviceSpecs[ext_name], deviceListCallback);
}
if (ScratchDeviceManager && awaitingSpecs['wedo2']) {
ext_name = awaitingSpecs['wedo2'];
ScratchDeviceManager.wedo2_list(function(deviceList) {
for (var i = 0; i < deviceList.length; ++i) {
handlers[ext_name]._deviceConnected(new WeDo2Device(deviceList[i].id || deviceList[i], ext_name));
}
});
}
if (!shouldLookForDevices()) {
@ -241,6 +241,15 @@ window.ScratchExtensions = new (function () {
}
}
function deviceListCallback(deviceList, ext_type, ext_name) {
for (var i = 0; i < deviceList.length; ++i) {
var deviceConstructor = Devices[ext_type];
var deviceId = deviceList[i].id || deviceList[i];
var device = new deviceConstructor(deviceId, ext_type, ext_name);
handlers[ext_name]._deviceConnected(device);
}
}
function checkPolling() {
if (poller || !shouldLookForDevices()) return;
@ -265,6 +274,7 @@ window.ScratchExtensions = new (function () {
function createDevicePlugin() {
if (plugin) return;
try {
// TODO: delegate more of this to the other files
if (isOffline) {
// Talk to the AIR Native Extension through the offline editor's plugin emulation.
@ -287,6 +297,11 @@ window.ScratchExtensions = new (function () {
// Talk to the actual plugin, but make it pretend to be asynchronous.
plugin = new window.ScratchPlugin.PluginWrapper(plugin);
}
}
catch (e) {
console.error('Error creating plugin or wrapper:', e);
plugin = null;
}
// Wait a moment to access the plugin and claim any devices that plugins are
// interested in.
@ -383,13 +398,70 @@ window.ScratchExtensions = new (function () {
};
}
// TODO: create a base class for these device classes so that we can share common code
function WeDo2Device(id, ext_name) {
function WeDo2Device(id, ext_type, ext_name) {
var dev = null;
this.ext_type = ext_type;
this.ext_name = ext_name;
var self = this;
this.id = id;
function RawWeDo2(deviceId, socket) {
var WeDo = this;
var eventHandlers = {};
WeDo.close = function() {
socket.close();
};
WeDo.setMotorOn = function(motorIndex, power) {
socket.emit('motorOn', {motorIndex:motorIndex, power:power});
};
WeDo.setMotorOff = function(motorIndex) {
socket.emit('motorOff', {motorIndex:motorIndex});
};
WeDo.setMotorBrake = function(motorIndex) {
socket.emit('motorBrake', {motorIndex:motorIndex});
};
WeDo.setLED = function(rgb) {
socket.emit('setLED', {rgb:rgb});
};
WeDo.playTone = function(tone, durationMs) {
socket.emit('playTone', {tone:tone, ms:durationMs});
};
WeDo.stopTone = function() {
socket.emit('stopTone');
};
function setHandler(eventName, handler) {
if (eventHandlers.hasOwnProperty(eventName)) {
var oldHandler = eventHandlers[eventName];
if (oldHandler) {
socket.removeListener(eventName, oldHandler);
}
}
if (handler) {
socket.on(eventName, handler);
}
eventHandlers[eventName] = handler;
}
// function handler(event) { access event.sensorName and event.sensorValue }
WeDo.setSensorHandler = function (handler) {
setHandler('sensorChanged', handler);
};
WeDo.setDeviceWasClosedHandler = function (handler) {
// TODO: resolve this ambiguity
setHandler('disconnect', handler);
setHandler('deviceWasClosed', handler);
};
}
function disconnect() {
setTimeout(function () {
self.close();
@ -397,16 +469,27 @@ window.ScratchExtensions = new (function () {
}, 0);
}
this.is_open = function() {
return !!dev;
};
this.open = function(readyCallback) {
ScratchDeviceManager.wedo2_open(self.id, function(d) {
dev = d;
if (dev) {
ScratchDeviceManager.socket_open(self.ext_name, self.ext_type, self.id, function(socket) {
if (socket) {
dev = new RawWeDo2(self.id, socket);
devices[ext_name] = self;
dev.setDeviceWasClosedHandler(disconnect);
}
if (readyCallback) readyCallback(d ? self : null);
else {
dev = null;
disconnect();
}
if (readyCallback) {
readyCallback(dev ? self : null);
}
});
};
this.close = function() {
if (!dev) return;
dev.close();
@ -415,9 +498,6 @@ window.ScratchExtensions = new (function () {
checkPolling();
};
this.is_open = function() {
return !!dev;
};
// The `handler` should be a function like: function handler(event) {...}
// The `event` will contain properties called `sensorName` and `sensorValue`.
@ -452,4 +532,84 @@ window.ScratchExtensions = new (function () {
dev.stopTone();
};
}
function BleDevice(id, ext_type, ext_name) {
var self = this;
this.ext_name = ext_name;
this.ext_type = ext_type;
this.socket = null;
this.id = id;
var onActions = [];
var onceActions = [];
function disconnect() {
setTimeout(function () {
self.close();
handlers[self.ext_name]._deviceRemoved(self);
}, 0);
}
this.emit = function(action, data){
if(self.socket){
self.socket.emit(action, data);
}
return !!self.socket;
};
this.on = function(action, callback){
if(self.is_open()){
self.socket.on(action, callback);
}
else{
onActions.push([action, callback]);
}
};
this.once = function(action, callback){
if(self.is_open()){
self.socket.once(action, callback);
}
else{
onceActions.push([action, callback]);
}
};
this.open = function(readyCallback) {
ScratchDeviceManager.socket_open(self.ext_name, ext_type, self.id, function(s) {
self.socket = s;
if (self.socket) {
devices[self.ext_name] = self;
onActions.forEach(function(element){
self.socket.on(element[0], element[1]);
});
onceActions.forEach(function(element){
self.socket.once(element[0], element[1]);
});
self.socket.on('disconnect', disconnect);
self.socket.on('deviceWasClosed', disconnect);
}
if (readyCallback) readyCallback(self.socket ? self : null);
});
};
this.close = function() {
if (!self.socket) return;
self.socket.close();
delete devices[self.ext_name];
self.socket = null;
checkPolling();
};
this.is_open = function() {
return !!self.socket;
};
}
Devices = {ble: BleDevice, wedo2: WeDo2Device};
})();

View file

@ -7,7 +7,9 @@ window.ScratchDeviceHost = new (function () {
return isConnected;
};
if (!window.chrome) return;
if (!(window.chrome && window.chrome.runtime && window.chrome.runtime.connect)) {
return;
}
var extensionID = 'clmabinlolakdafkoajkfjjengcdmnpm';
var callNumber = 0;

View file

@ -148,6 +148,9 @@
return false;
};
// Each block must have a unique function name even if the implementation is identical.
ext.whenTilted = ext.isTilted;
ext.getTilt = function (tiltDir) {
var tiltValue;
switch(tiltDir) {
@ -332,7 +335,7 @@
[' ', 'set light color to %n', 'setLED', 50],
['w', 'play note %d.note for %n seconds', 'playNote', 60, 0.5],
['h', 'when distance %m.lessMore %n', 'whenDistance', strings.COMP_LESS, 50],
['h', 'when tilted %m.tiltDirAny', 'isTilted', strings.TILT_ANY],
['h', 'when tilted %m.tiltDirAny', 'whenTilted', strings.TILT_ANY],
['r', 'distance', 'getDistance'],
['b', 'tilted %m.tiltDirAny ?', 'isTilted', strings.TILT_ANY],
['r', 'tilt angle %m.tiltDir', 'getTilt', strings.TILT_UP]