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 // Communicate with the Scratch Device Manager through Socket.IO
window.ScratchDeviceManager = new (function () { 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 // 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 // work around https://github.com/socketio/socket.io-client/issues/812
function connectNamespace(namespace) { function connectNamespace(namespace) {
return io(self.deviceManagerHost + namespace, {forceNew: true}); return io(instance.deviceManagerHost + namespace, {forceNew: true});
} }
self.wedo2_list = function (callback) { function onClose(){
$.ajax(self.deviceManagerHost + '/wedo2/list', { for(var i=0; i<sockets.length; i++){
dataType: 'text', 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) { success: function (data, textStatus, jqXHR) {
var deviceList = JSON.parse(data); isConnected = true;
if (deviceList.constructor == Array) { if (data.constructor == Array) {
callback(deviceList); callback(data, ext_type, ext_name);
} }
},
error: function (jqXHR, textStatus, errorThrown) {
isConnected = false;
} }
}); });
}; };
// TODO: handle multiple devices // Attempt to open a device-specific socket connection to the Device Manager.
self.wedo2_open = function (deviceId, callback) { // This must call `callback` exactly once no matter what.
var socket = connectNamespace('/wedo2'); // The callback will receive a connected socket on success or `null` on failure.
var pluginDevice = new RawWeDo2(deviceId, socket); instance.socket_open = function (ext_name, deviceType, deviceId, callback) {
socket.on('deviceWasOpened', function (event) { function onDeviceWasOpened () {
callback(pluginDevice); // If this is the first event on this socket then respond with success.
}); if (clearOpenTimeout()) {
socket.emit('open', {deviceId: deviceId}); callback(socket);
};
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); 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 } function onTimeout () {
WeDo.setSensorHandler = function (handler) { // This will trigger `onDisconnect()`
setHandler('sensorChanged', handler); 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) { instance.isConnected = function () {
// TODO: resolve this ambiguity return isConnected;
setHandler('disconnect', handler);
setHandler('deviceWasClosed', handler);
}; };
}
})(); })();

View file

@ -106,8 +106,13 @@ window.ScratchExtensions = new (function () {
var callback = function (retval) { var callback = function (retval) {
Scratch.FlashApp.ASobj.ASextensionReporterDone(ext_name, job_id, retval); Scratch.FlashApp.ASobj.ASextensionReporterDone(ext_name, job_id, retval);
}; };
if(handlers[ext_name]._getStatus().status != 2){
callback(false);
}
else{
args.push(callback); args.push(callback);
handlers[ext_name][reporter].apply(handlers[ext_name], args); handlers[ext_name][reporter].apply(handlers[ext_name], args);
}
}; };
lib.getReporterForceAsync = function (ext_name, reporter, args, job_id) { lib.getReporterForceAsync = function (ext_name, reporter, args, job_id) {
@ -134,8 +139,9 @@ window.ScratchExtensions = new (function () {
if (ext_name in deviceSpecs) { if (ext_name in deviceSpecs) {
switch (deviceSpecs[ext_name].type) { switch (deviceSpecs[ext_name].type) {
case 'ble':
case 'wedo2': case 'wedo2':
if (!ScratchDeviceManager) { if (!(ScratchDeviceManager && ScratchDeviceManager.isConnected())) {
return {status: 0, msg: 'Missing Scratch Device Manager'}; return {status: 0, msg: 'Missing Scratch Device Manager'};
} }
break; break;
@ -195,29 +201,28 @@ window.ScratchExtensions = new (function () {
if (!awaitingSpecs['hid']) awaitingSpecs['hid'] = {}; if (!awaitingSpecs['hid']) awaitingSpecs['hid'] = {};
awaitingSpecs['hid'][spec.vendor + '_' + spec.product] = ext_name; awaitingSpecs['hid'][spec.vendor + '_' + spec.product] = ext_name;
} }
else if (spec.type == 'serial') { else {
awaitingSpecs['serial'] = ext_name; awaitingSpecs[spec.type] = ext_name;
}
else if (spec.type == 'wedo2') {
awaitingSpecs['wedo2'] = ext_name;
} }
} }
} }
if (plugin) { for (var specType in awaitingSpecs) {
if (awaitingSpecs['hid']) { if (!awaitingSpecs.hasOwnProperty(specType)) continue;
if (plugin && specType == 'hid') {
var awaitingHid = awaitingSpecs['hid'];
plugin.hid_list(function (deviceList) { plugin.hid_list(function (deviceList) {
var hidList = awaitingSpecs['hid'];
for (var i = 0; i < deviceList.length; i++) { for (var i = 0; i < deviceList.length; i++) {
var ext_name = hidList[deviceList[i]["vendor_id"] + '_' + deviceList[i]["product_id"]]; var deviceID = deviceList[i]["vendor_id"] + '_' + deviceList[i]["product_id"];
if (ext_name) { var hid_ext_name = awaitingHid[deviceID];
handlers[ext_name]._deviceConnected(new HidDevice(deviceList[i], ext_name)); if (hid_ext_name) {
handlers[hid_ext_name]._deviceConnected(new HidDevice(deviceList[i], hid_ext_name));
} }
} }
}); });
} }
else if (plugin && specType == 'serial') {
if (awaitingSpecs['serial']) {
ext_name = awaitingSpecs['serial']; ext_name = awaitingSpecs['serial'];
plugin.serial_list(function (deviceList) { plugin.serial_list(function (deviceList) {
for (var i = 0; i < deviceList.length; i++) { 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()) { 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() { function checkPolling() {
if (poller || !shouldLookForDevices()) return; if (poller || !shouldLookForDevices()) return;
@ -265,6 +274,7 @@ window.ScratchExtensions = new (function () {
function createDevicePlugin() { function createDevicePlugin() {
if (plugin) return; if (plugin) return;
try {
// TODO: delegate more of this to the other files // TODO: delegate more of this to the other files
if (isOffline) { if (isOffline) {
// Talk to the AIR Native Extension through the offline editor's plugin emulation. // 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. // Talk to the actual plugin, but make it pretend to be asynchronous.
plugin = new window.ScratchPlugin.PluginWrapper(plugin); 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 // Wait a moment to access the plugin and claim any devices that plugins are
// interested in. // 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_type, ext_name) {
function WeDo2Device(id, ext_name) {
var dev = null; var dev = null;
this.ext_type = ext_type;
this.ext_name = ext_name;
var self = this; var self = this;
this.id = id; 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() { function disconnect() {
setTimeout(function () { setTimeout(function () {
self.close(); self.close();
@ -397,16 +469,27 @@ window.ScratchExtensions = new (function () {
}, 0); }, 0);
} }
this.is_open = function() {
return !!dev;
};
this.open = function(readyCallback) { this.open = function(readyCallback) {
ScratchDeviceManager.wedo2_open(self.id, function(d) { ScratchDeviceManager.socket_open(self.ext_name, self.ext_type, self.id, function(socket) {
dev = d; if (socket) {
if (dev) { dev = new RawWeDo2(self.id, socket);
devices[ext_name] = self; devices[ext_name] = self;
dev.setDeviceWasClosedHandler(disconnect); dev.setDeviceWasClosedHandler(disconnect);
} }
if (readyCallback) readyCallback(d ? self : null); else {
dev = null;
disconnect();
}
if (readyCallback) {
readyCallback(dev ? self : null);
}
}); });
}; };
this.close = function() { this.close = function() {
if (!dev) return; if (!dev) return;
dev.close(); dev.close();
@ -415,9 +498,6 @@ window.ScratchExtensions = new (function () {
checkPolling(); checkPolling();
}; };
this.is_open = function() {
return !!dev;
};
// The `handler` should be a function like: function handler(event) {...} // The `handler` should be a function like: function handler(event) {...}
// The `event` will contain properties called `sensorName` and `sensorValue`. // The `event` will contain properties called `sensorName` and `sensorValue`.
@ -452,4 +532,84 @@ window.ScratchExtensions = new (function () {
dev.stopTone(); 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; return isConnected;
}; };
if (!window.chrome) return; if (!(window.chrome && window.chrome.runtime && window.chrome.runtime.connect)) {
return;
}
var extensionID = 'clmabinlolakdafkoajkfjjengcdmnpm'; var extensionID = 'clmabinlolakdafkoajkfjjengcdmnpm';
var callNumber = 0; var callNumber = 0;

View file

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