mirror of
https://github.com/scratchfoundation/scratchx.git
synced 2024-11-24 08:38:03 -05:00
Update scratch_extensions
for upcoming scratchr2
This corresponds to the upcoming October 20th release
This commit is contained in:
parent
ed239eb0f0
commit
47d9063829
4 changed files with 300 additions and 127 deletions
|
@ -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);
|
||||
}
|
||||
eventHandlers[eventName] = handler;
|
||||
}
|
||||
|
||||
// function handler(event) { access event.sensorName and event.sensorValue }
|
||||
WeDo.setSensorHandler = function (handler) {
|
||||
setHandler('sensorChanged', 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);
|
||||
}
|
||||
}
|
||||
|
||||
WeDo.setDeviceWasClosedHandler = function (handler) {
|
||||
// TODO: resolve this ambiguity
|
||||
setHandler('disconnect', handler);
|
||||
setHandler('deviceWasClosed', 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});
|
||||
};
|
||||
|
||||
instance.isConnected = function () {
|
||||
return isConnected;
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -106,8 +106,13 @@ window.ScratchExtensions = new (function () {
|
|||
var callback = function (retval) {
|
||||
Scratch.FlashApp.ASobj.ASextensionReporterDone(ext_name, job_id, retval);
|
||||
};
|
||||
args.push(callback);
|
||||
handlers[ext_name][reporter].apply(handlers[ext_name], args);
|
||||
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 () {
|
|||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
});
|
||||
else if (ScratchDeviceManager) {
|
||||
ext_name = awaitingSpecs[specType];
|
||||
ScratchDeviceManager.device_list(specType, ext_name, deviceSpecs[ext_name], deviceListCallback);
|
||||
}
|
||||
}
|
||||
|
||||
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,27 +274,33 @@ window.ScratchExtensions = new (function () {
|
|||
function createDevicePlugin() {
|
||||
if (plugin) return;
|
||||
|
||||
// TODO: delegate more of this to the other files
|
||||
if (isOffline) {
|
||||
// Talk to the AIR Native Extension through the offline editor's plugin emulation.
|
||||
plugin = Scratch.FlashApp.ASobj.getPlugin();
|
||||
} else if (window.ScratchDeviceHost && window.ScratchDeviceHost.isAvailable()) {
|
||||
// Talk to the Native Messaging Host through a Chrome extension.
|
||||
plugin = window.ScratchDeviceHost;
|
||||
} else {
|
||||
if (window.ScratchPlugin.useActiveX) {
|
||||
// we must be on IE or similar
|
||||
plugin = new ActiveXObject(window.ScratchPlugin.axObjectName);
|
||||
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.
|
||||
plugin = Scratch.FlashApp.ASobj.getPlugin();
|
||||
} else if (window.ScratchDeviceHost && window.ScratchDeviceHost.isAvailable()) {
|
||||
// Talk to the Native Messaging Host through a Chrome extension.
|
||||
plugin = window.ScratchDeviceHost;
|
||||
} else {
|
||||
// Not IE: try NPAPI
|
||||
var pluginContainer = document.createElement('div');
|
||||
document.getElementById('scratch').parentNode.appendChild(pluginContainer);
|
||||
pluginContainer.innerHTML =
|
||||
'<object type="application/x-scratchdeviceplugin" width="1" height="1"> </object>';
|
||||
plugin = pluginContainer.firstChild;
|
||||
if (window.ScratchPlugin.useActiveX) {
|
||||
// we must be on IE or similar
|
||||
plugin = new ActiveXObject(window.ScratchPlugin.axObjectName);
|
||||
} else {
|
||||
// Not IE: try NPAPI
|
||||
var pluginContainer = document.createElement('div');
|
||||
document.getElementById('scratch').parentNode.appendChild(pluginContainer);
|
||||
pluginContainer.innerHTML =
|
||||
'<object type="application/x-scratchdeviceplugin" width="1" height="1"> </object>';
|
||||
plugin = pluginContainer.firstChild;
|
||||
}
|
||||
// Talk to the actual plugin, but make it pretend to be asynchronous.
|
||||
plugin = new window.ScratchPlugin.PluginWrapper(plugin);
|
||||
}
|
||||
// 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
|
||||
|
@ -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};
|
||||
})();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in a new issue