diff --git a/scratch_extensions/scratch_deviceManager.js b/scratch_extensions/scratch_deviceManager.js index a50a7e2..18fe165 100644 --- a/scratch_extensions/scratch_deviceManager.js +++ b/scratch_extensions/scratch_deviceManager.js @@ -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}); + } + + function onClose(){ + for(var i=0; i= 0) { + sockets.splice(socketIndex, 1); + } + // If this is the first event on this socket then respond with failure. + if (clearOpenTimeout()) { + callback(null); + } + } - WeDo.stopTone = function() { - socket.emit('stopTone'); - }; + function onTimeout () { + // This will trigger `onDisconnect()` + socket.disconnect(); + } - function setHandler(eventName, handler) { - if (eventHandlers.hasOwnProperty(eventName)) { - var oldHandler = eventHandlers[eventName]; - if (oldHandler) { - socket.removeListener(eventName, oldHandler); - } + // 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; } - if (handler) { - socket.on(eventName, handler); + else { + return false; } - eventHandlers[eventName] = handler; } - // function handler(event) { access event.sensorName and event.sensorValue } - WeDo.setSensorHandler = function (handler) { - setHandler('sensorChanged', handler); - }; + var socket = connectNamespace('/' + deviceType); + sockets.push(socket); - WeDo.setDeviceWasClosedHandler = function (handler) { - // TODO: resolve this ambiguity - setHandler('disconnect', handler); - setHandler('deviceWasClosed', handler); - }; - } + 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; + }; })(); diff --git a/scratch_extensions/scratch_ext.js b/scratch_extensions/scratch_ext.js index 75d0fd9..c9cc950 100644 --- a/scratch_extensions/scratch_ext.js +++ b/scratch_extensions/scratch_ext.js @@ -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 = - ' '; - 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 = + ' '; + 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}; })(); diff --git a/scratch_extensions/scratch_nmh.js b/scratch_extensions/scratch_nmh.js index 551a56f..fb771bd 100644 --- a/scratch_extensions/scratch_nmh.js +++ b/scratch_extensions/scratch_nmh.js @@ -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; diff --git a/scratch_extensions/wedo2Extension.js b/scratch_extensions/wedo2Extension.js index c5c4c30..32eb1f1 100644 --- a/scratch_extensions/wedo2Extension.js +++ b/scratch_extensions/wedo2Extension.js @@ -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]