Update scratch_extensions, support Device Manager

This commit is contained in:
Christopher Willis-Ford 2016-08-02 00:49:34 -07:00
parent 66d51d6814
commit 20eb5c804b
7 changed files with 505 additions and 268 deletions

View file

@ -680,6 +680,7 @@
<script type="text/javascript" src="scratch_extensions/scratch_proxies.js" defer></script>
<script type="text/javascript" src="scratch_extensions/scratch_plugin.js" defer></script>
<script type="text/javascript" src="scratch_extensions/scratch_nmh.js" defer></script>
<script type="text/javascript" src="scratch_extensions/scratch_deviceManager.js" defer></script>
<script type="text/javascript" src="scratch_extensions/scratch_ext.js" defer></script>
</body>
</html>

View file

@ -4,7 +4,7 @@
//
// This is an extension for development and testing of the Scratch Javascript Extension API.
(function(ext) {
(function (ext) {
var device = null;
var rawData = null;
@ -30,24 +30,24 @@
'resistance-D': 0
};
ext.resetAll = function(){};
// Hats / triggers
ext.whenSensorConnected = function(which) {
ext.whenSensorConnected = function (which) {
return getSensorPressed(which);
};
ext.whenSensorPass = function(which, sign, level) {
ext.whenSensorPass = function (which, sign, level) {
if (sign == '<') return getSensor(which) < level;
return getSensor(which) > level;
};
// Reporters
ext.sensorPressed = function(which) {
ext.sensorPressed = function (which) {
return getSensorPressed(which);
};
ext.sensor = function(which) { return getSensor(which); };
ext.sensor = function (which) {
return getSensor(which);
};
// Private logic
function getSensorPressed(which) {
@ -65,6 +65,7 @@
}
var inputArray = [];
function processData() {
var bytes = new Uint8Array(rawData);
@ -74,10 +75,10 @@
// Right now there's no guarantee that our 18 bytes start at the beginning of a message.
// Maybe we should treat the data as a stream of 2-byte packets instead of 18-byte packets.
// That way we could just check the high bit of each byte to verify that we're aligned.
for(var i=0; i<9; ++i) {
var hb = bytes[i*2] & 127;
for (var i = 0; i < 9; ++i) {
var hb = bytes[i * 2] & 127;
var channel = hb >> 3;
var lb = bytes[i*2+1] & 127;
var lb = bytes[i * 2 + 1] & 127;
inputArray[channel] = ((hb & 7) << 7) + lb;
}
@ -87,17 +88,17 @@
watchdog = null;
}
for(var name in inputs) {
for (var name in inputs) {
var v = inputArray[channels[name]];
if(name == 'light') {
if (name == 'light') {
v = (v < 25) ? 100 - v : Math.round((1023 - v) * (75 / 998));
}
else if(name == 'sound') {
else if (name == 'sound') {
//empirically tested noise sensor floor
v = Math.max(0, v - 18)
v = (v < 50) ? v / 2 :
v = Math.max(0, v - 18);
v = (v < 50) ? v / 2 :
//noise ceiling
25 + Math.min(75, Math.round((v - 50) * (75 / 580)));
25 + Math.min(75, Math.round((v - 50) * (75 / 580)));
}
else {
v = (100 * v) / 1023;
@ -110,22 +111,22 @@
rawData = null;
}
function appendBuffer( buffer1, buffer2 ) {
var tmp = new Uint8Array( buffer1.byteLength + buffer2.byteLength );
tmp.set( new Uint8Array( buffer1 ), 0 );
tmp.set( new Uint8Array( buffer2 ), buffer1.byteLength );
function appendBuffer(buffer1, buffer2) {
var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
tmp.set(new Uint8Array(buffer1), 0);
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
return tmp.buffer;
}
// Extension API interactions
var potentialDevices = [];
ext._deviceConnected = function(dev) {
ext._deviceConnected = function (dev) {
potentialDevices.push(dev);
if (!device) {
tryNextDevice();
}
}
};
function tryNextDevice() {
// If potentialDevices is empty, device will be undefined.
@ -133,24 +134,28 @@
device = potentialDevices.shift();
if (device) {
device.open({ stopBits: 0, bitRate: 38400, ctsFlowControl: 0 }, deviceOpened);
device.open({stopBits: 0, bitRate: 38400, ctsFlowControl: 0}, deviceOpened);
}
}
var poller = null;
var watchdog = null;
function deviceOpened(dev) {
if (!dev) {
// Opening the port failed.
tryNextDevice();
return;
}
device.set_receive_handler(function(data) {
device.set_receive_handler(function (data) {
//console.log('Received: ' + data.byteLength);
if(!rawData || rawData.byteLength == 18) rawData = new Uint8Array(data);
else rawData = appendBuffer(rawData, data);
if (!rawData || rawData.byteLength == 18) {
rawData = new Uint8Array(data);
} else {
rawData = appendBuffer(rawData, data);
}
if(rawData.byteLength >= 18) {
if (rawData.byteLength >= 18) {
//console.log(rawData);
processData();
//device.send(pingCmd.buffer);
@ -160,10 +165,10 @@
// Tell the PicoBoard to send a input data every 50ms
var pingCmd = new Uint8Array(1);
pingCmd[0] = 1;
poller = setInterval(function() {
poller = setInterval(function () {
device.send(pingCmd.buffer);
}, 50);
watchdog = setTimeout(function() {
watchdog = setTimeout(function () {
// This device didn't get good data in time, so give up on it. Clean up and then move on.
// If we get good data then we'll terminate this watchdog.
clearInterval(poller);
@ -173,26 +178,26 @@
device = null;
tryNextDevice();
}, 250);
};
ext._deviceRemoved = function(dev) {
if(device != dev) return;
if(poller) poller = clearInterval(poller);
device = null;
};
ext._shutdown = function() {
if(device) device.close();
if(poller) poller = clearInterval(poller);
device = null;
};
ext._getStatus = function() {
if(!device) return {status: 1, msg: 'PicoBoard disconnected'};
if(watchdog) return {status: 1, msg: 'Probing for PicoBoard'};
return {status: 2, msg: 'PicoBoard connected'};
}
ext._deviceRemoved = function (dev) {
if (device != dev) return;
if (poller) poller = clearInterval(poller);
device = null;
};
ext._shutdown = function () {
if (device) device.close();
if (poller) poller = clearInterval(poller);
device = null;
};
ext._getStatus = function () {
if (!device) return {status: 1, msg: 'PicoBoard disconnected'};
if (watchdog) return {status: 1, msg: 'Probing for PicoBoard'};
return {status: 2, msg: 'PicoBoard connected'};
};
var descriptor = {
blocks: [
['h', 'when %m.booleanSensor', 'whenSensorConnected', 'button pressed'],

View file

@ -0,0 +1,91 @@
// Communicate with the Scratch Device Manager through Socket.IO
window.ScratchDeviceManager = new (function () {
var self = this;
// device-manager.scratch.mit.edu = 127.0.0.1
self.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});
}
self.wedo2_list = function (callback) {
$.ajax(self.deviceManagerHost + '/wedo2/list', {
dataType: 'text',
success: function (data, textStatus, jqXHR) {
var deviceList = JSON.parse(data);
if (deviceList.constructor == Array) {
callback(deviceList);
}
}
});
};
// 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);
}
}
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);
};
}
})();

View file

@ -14,8 +14,8 @@ window.ScratchExtensions = new (function () {
var poller = null;
var lib = this;
var isOffline = (Scratch && Scratch.FlashApp && Scratch.FlashApp.ASobj &&
Scratch.FlashApp.ASobj.isOffline && Scratch.FlashApp.ASobj.isOffline());
var isOffline = Scratch && Scratch.FlashApp && Scratch.FlashApp.ASobj &&
Scratch.FlashApp.ASobj.isOffline && Scratch.FlashApp.ASobj.isOffline();
var pluginAvailable = function () {
return !!window.ArrayBuffer && !!(
isOffline ||
@ -45,20 +45,23 @@ window.ScratchExtensions = new (function () {
};
Scratch.FlashApp.ASobj.ASloadExtension(extObj);
if (deviceSpec) {
if (!plugin) {
if (pluginAvailable()) {
// createDevicePlugin() will eventually call checkPolling() if it succeeds
setTimeout(createDevicePlugin, 10);
} else if (window.ScratchPlugin.useActiveX) {
JSsetProjectBanner('Sorry, your version of Internet Explorer is not supported. Please upgrade to version 10 or 11.');
}
}
else {
// Second hardware-using project in the same tab
checkPolling();
}
}
if (deviceSpec) {
if (!plugin) {
if (pluginAvailable()) {
// createDevicePlugin() will eventually call checkPolling() if it succeeds
setTimeout(createDevicePlugin, 10);
} else if (ScratchDeviceManager) {
// No plugin is NBD if we're using the SDM
checkPolling();
} else if (window.ScratchPlugin.useActiveX) {
JSsetProjectBanner('Sorry, your version of Internet Explorer is not supported. Please upgrade to version 10 or 11.');
}
}
else {
// Second hardware-using project in the same tab
checkPolling();
}
}
return true;
};
@ -81,14 +84,20 @@ window.ScratchExtensions = new (function () {
};
lib.unregister = function (name) {
try { handlers[name]._shutdown(); } catch (e) { }
try {
handlers[name]._shutdown();
} catch (e) {
}
delete handlers[name];
delete blockDefs[name];
delete menuDefs[name];
delete deviceSpecs[name];
};
lib.canAccessDevices = function () { return pluginAvailable(); };
lib.canAccessDevices = function () {
return pluginAvailable();
};
lib.getReporter = function (ext_name, reporter, args) {
return handlers[ext_name][reporter].apply(handlers[ext_name], args);
};
@ -96,7 +105,7 @@ window.ScratchExtensions = new (function () {
lib.getReporterAsync = function (ext_name, reporter, args, job_id) {
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);
};
@ -113,24 +122,50 @@ window.ScratchExtensions = new (function () {
lib.runAsync = function (ext_name, command, args, job_id) {
var callback = function () {
Scratch.FlashApp.ASobj.ASextensionCallDone(ext_name, job_id);
}
};
args.push(callback);
handlers[ext_name][command].apply(handlers[ext_name], args);
};
lib.getStatus = function (ext_name) {
if (!(ext_name in handlers))
return { status: 0, msg: 'Not loaded' };
if (!(ext_name in handlers)) {
return {status: 0, msg: 'Not loaded'};
}
if (ext_name in deviceSpecs && !pluginAvailable())
return { status: 0, msg: 'Missing browser plugin' };
if (ext_name in deviceSpecs) {
switch (deviceSpecs[ext_name].type) {
case 'wedo2':
if (!ScratchDeviceManager) {
return {status: 0, msg: 'Missing Scratch Device Manager'};
}
break;
default:
if (!pluginAvailable()) {
return {status: 0, msg: 'Missing browser plugin'};
}
break;
}
}
return handlers[ext_name]._getStatus();
};
lib.stop = function (ext_name) {
var ext = handlers[ext_name];
if (ext._stop) {
ext._stop();
}
else if (ext.resetAll) { // old, undocumented call
ext.resetAll();
}
};
lib.notify = function (text) {
if (window.JSsetProjectBanner) JSsetProjectBanner(text);
else alert(text);
if (window.JSsetProjectBanner) {
JSsetProjectBanner(text);
} else {
alert(text);
}
};
lib.resetPlugin = function () {
@ -143,47 +178,67 @@ window.ScratchExtensions = new (function () {
});
function shutdown() {
for (var extName in handlers)
for (var extName in handlers) {
handlers[extName]._shutdown();
}
handlers = {};
stopPolling();
}
function checkDevices() {
var awaitingSpecs = {};
for (var ext_name in deviceSpecs)
var ext_name;
for (ext_name in deviceSpecs) {
if (!devices[ext_name]) {
var spec = deviceSpecs[ext_name];
if (spec.type == 'hid') {
if (!awaitingSpecs['hid']) awaitingSpecs['hid'] = {};
awaitingSpecs['hid'][spec.vendor + '_' + spec.product] = ext_name;
}
else if (spec.type == 'serial')
else if (spec.type == 'serial') {
awaitingSpecs['serial'] = ext_name;
}
else if (spec.type == 'wedo2') {
awaitingSpecs['wedo2'] = ext_name;
}
}
}
if (plugin) {
if (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));
}
}
});
}
if (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));
if (awaitingSpecs['serial']) {
ext_name = awaitingSpecs['serial'];
plugin.serial_list(function (deviceList) {
for (var i = 0; i < deviceList.length; i++) {
handlers[ext_name]._deviceConnected(new SerialDevice(deviceList[i], ext_name));
}
});
}
}
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, ext_name));
}
});
}
if (awaitingSpecs['serial']) {
var ext_name = awaitingSpecs['serial'];
plugin.serial_list(function (deviceList) {
for (var i = 0; i < deviceList.length; i++) {
handlers[ext_name]._deviceConnected(new serialDevice(deviceList[i], ext_name));
}
});
}
if (!shouldLookForDevices())
if (!shouldLookForDevices()) {
stopPolling();
}
}
function checkPolling() {
@ -198,9 +253,11 @@ window.ScratchExtensions = new (function () {
}
function shouldLookForDevices() {
for (var ext_name in deviceSpecs)
if (!devices[ext_name])
for (var ext_name in deviceSpecs) {
if (!devices[ext_name]) {
return true;
}
}
return false;
}
@ -223,7 +280,8 @@ window.ScratchExtensions = new (function () {
// 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>';
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.
@ -235,7 +293,7 @@ window.ScratchExtensions = new (function () {
setTimeout(checkPolling, 100);
}
function hidDevice(info, ext_name) {
function HidDevice(info, ext_name) {
var dev = null;
var self = this;
@ -255,11 +313,11 @@ window.ScratchExtensions = new (function () {
this.open = function (readyCallback) {
plugin.hid_open(self.id, function (d) {
dev = d;
dev.set_nonblocking(true);
//devices[ext_name][path] = self;
devices[ext_name] = self;
if (readyCallback) readyCallback(self);
if (dev) {
devices[ext_name] = self;
dev.set_nonblocking(true);
}
if (readyCallback) readyCallback(d ? self : null);
});
};
this.close = function () {
@ -287,7 +345,7 @@ window.ScratchExtensions = new (function () {
};
}
function serialDevice(id, ext_name) {
function SerialDevice(id, ext_name) {
var dev = null;
var self = this;
@ -296,26 +354,16 @@ window.ScratchExtensions = new (function () {
this.id = id;
this.open = function (opts, readyCallback) {
try {
plugin.serial_open(self.id, opts, function (d) {
// dev.set_disconnect_handler(function () {
// self.close();
// handlers[ext_name]._deviceRemoved(self);
// });
// devices[ext_name][path] = self;
dev = d;
plugin.serial_open(self.id, opts, function (d) {
dev = d;
if (dev) {
devices[ext_name] = self;
dev.set_error_handler(function (message) {
alert('Serial device error\n\nDevice: ' + id + '\nError: ' + message);
});
if (readyCallback) readyCallback(self);
});
}
catch (e) {
console.log('Error opening serial device ' + id + ': ' + e);
}
}
if (readyCallback) readyCallback(d ? self : null);
});
};
this.close = function () {
if (!dev) return;
@ -334,4 +382,74 @@ window.ScratchExtensions = new (function () {
dev.set_receive_handler(handler);
};
}
// TODO: create a base class for these device classes so that we can share common code
function WeDo2Device(id, ext_name) {
var dev = null;
var self = this;
this.id = id;
function disconnect() {
setTimeout(function () {
self.close();
handlers[ext_name]._deviceRemoved(self);
}, 0);
}
this.open = function(readyCallback) {
ScratchDeviceManager.wedo2_open(self.id, function(d) {
dev = d;
if (dev) {
devices[ext_name] = self;
dev.setDeviceWasClosedHandler(disconnect);
}
if (readyCallback) readyCallback(d ? self : null);
});
};
this.close = function() {
if (!dev) return;
dev.close();
delete devices[ext_name];
dev = null;
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`.
// Sensor names include `tilt` and `distance`.
this.set_sensor_handler = function(handler) {
if (!dev) return;
dev.setSensorHandler(handler);
};
// Starts motor at given power, 0-100. Use negative power for reverse.
this.set_motor_on = function(motorIndex, power) {
dev.setMotorOn(motorIndex, power);
};
// Applies active braking.
this.set_motor_brake = function(motorIndex) {
dev.setMotorBrake(motorIndex);
};
// Turns motor off. Depending on power and load, the motor will drift to a stop.
this.set_motor_off = function(motorIndex) {
dev.setMotorOff(motorIndex);
};
// Sets the RGB LED color. The RGB color should be specified in 0xRRGGBB format.
this.set_led = function(rgb) {
dev.setLED(rgb);
};
this.play_tone = function(tone, durationMs) {
dev.playTone(tone, durationMs);
};
this.stop_tone = function() {
dev.stopTone();
};
}
})();

View file

@ -54,6 +54,7 @@ window.ScratchDeviceHost = new (function () {
};
var pendingCallbacks = {};
function sendMessage(message, callback) {
var callbackToken = (callNumber++).toString();
pendingCallbacks[callbackToken] = callback;

View file

@ -71,7 +71,7 @@ window.ScratchPlugin = new (function () {
self.close = function () {
device.close();
};
};
}
function SerialWrapper(device) {
var self = this;
@ -92,5 +92,5 @@ window.ScratchPlugin = new (function () {
self.set_error_handler = function (callback) {
device.set_error_handler(callback);
};
};
}
})();

View file

@ -4,7 +4,7 @@
//
// This is an extension for development and testing of the Scratch Javascript Extension API.
(function(ext) {
(function (ext) {
var device = null;
var rawData = null;
@ -22,127 +22,140 @@
var weDoTilt = 0;
// Commands
ext.motorOnFor = function(motor, time, callback) {
ext.motorOnFor = function (motor, time, callback) {
//ext.allMotorsOn();
ext.motorOn(motor);
ext.motorOn(motor);
setTimeout(function() {
ext.motorOff(motor);
setTimeout(function () {
ext.motorOff(motor);
//callback();
if (typeof callback=="function") callback();
if (typeof callback == "function") callback();
}, 1000 * time);
};
ext.motorOn = function(motor) {
switch(motor) {
case "motor":
ext.allMotorsOn('m');
break;
case "motor A":
setMotorOn('m', 0, true);
break;
case "motor B":
setMotorOn('m', 1, true);
break;
case "lights":
ext.allMotorsOn('l');
break;
default:
ext.allMotorsOn('a');
}
ext.motorOn = function (motor) {
switch (motor) {
case "motor":
ext.allMotorsOn('m');
break;
case "motor A":
setMotorOn('m', 0, true);
break;
case "motor B":
setMotorOn('m', 1, true);
break;
case "lights":
ext.allMotorsOn('l');
break;
default:
ext.allMotorsOn('a');
}
};
ext.allMotorsOn = function(type) {
ext.allMotorsOn = function (type) {
setMotorOn(type, 0, true);
setMotorOn(type, 1, true);
};
ext.motorOff = function(motor) {
switch(motor) {
case "motor":
ext.allMotorsOff('m');
break;
case "motor A":
setMotorOn('m', 0, false);
break;
case "motor B":
setMotorOn('m', 1, false);
break;
case "lights":
ext.allMotorsOff('l');
break;
default:
ext.allMotorsOff('a');
}
ext.motorOff = function (motor) {
switch (motor) {
case "motor":
ext.allMotorsOff('m');
break;
case "motor A":
setMotorOn('m', 0, false);
break;
case "motor B":
setMotorOn('m', 1, false);
break;
case "lights":
ext.allMotorsOff('l');
break;
default:
ext.allMotorsOff('a');
}
};
ext.resetAll = function() {
ext.allMotorsOff('a');
ext._stop = function () {
ext.allMotorsOff('a');
};
ext.allMotorsOff = function(type) {
ext.allMotorsOff = function (type) {
setMotorOn(type, 0, false);
setMotorOn(type, 1, false);
};
ext.startMotorPower = function(motor, power) {
switch(motor) {
case "motor":
setMotorPower('m', 0, power);
setMotorPower('m', 1, power);
setMotorOn('m', 0, true);
setMotorOn('m', 1, true);
break;
case "motor A":
setMotorPower('m', 0, power);
setMotorOn('m', 0, true);
break;
case "motor B":
setMotorPower('m', 1, power);
setMotorOn('m', 1, true);
break;
case "lights":
setMotorPower('l', 0, power);
setMotorPower('l', 1, power);
setMotorOn('l', 0, true);
setMotorOn('l', 1, true);
break;
default:
setMotorPower('a', 0, power);
setMotorPower('a', 1, power);
setMotorOn('a', 0, true);
setMotorOn('a', 1, true);
}
ext.startMotorPower = function (motor, power) {
switch (motor) {
case "motor":
setMotorPower('m', 0, power);
setMotorPower('m', 1, power);
setMotorOn('m', 0, true);
setMotorOn('m', 1, true);
break;
case "motor A":
setMotorPower('m', 0, power);
setMotorOn('m', 0, true);
break;
case "motor B":
setMotorPower('m', 1, power);
setMotorOn('m', 1, true);
break;
case "lights":
setMotorPower('l', 0, power);
setMotorPower('l', 1, power);
setMotorOn('l', 0, true);
setMotorOn('l', 1, true);
break;
default:
setMotorPower('a', 0, power);
setMotorPower('a', 1, power);
setMotorOn('a', 0, true);
setMotorOn('a', 1, true);
}
};
ext.setMotorDirection = function(motor, s) {
ext.setMotorDirection = function (motor, s) {
var dir;
if('this way' == s) dir = 1;
else if('that way' == s) dir = -1;
else if('reverse' == s) dir = 0;
else return;
if ('this way' == s) {
dir = 1;
} else if ('that way' == s) {
dir = -1;
} else if ('reverse' == s) {
dir = 0;
} else {
return;
}
switch(motor) {
case "motor A":
setMotorDirection(0, dir);
break;
case "motor B":
setMotorDirection(1, dir);
break;
default:
setMotorDirection(0, dir);
setMotorDirection(1, dir);
}
switch (motor) {
case "motor A":
setMotorDirection(0, dir);
break;
case "motor B":
setMotorDirection(1, dir);
break;
default:
setMotorDirection(0, dir);
setMotorDirection(1, dir);
}
};
// Hat blocks
ext.whenDistance = function(s, dist) { return device!=null && ('<' == s ? (getDistance() < dist) : (getDistance() > dist)); };
ext.whenTilt = function(s, tilt) { return device!=null && ('=' == s ? (getTilt() == tilt) : (getTilt() != tilt)); };
ext.whenDistance = function (s, dist) {
return device != null && ('<' == s ? (getDistance() < dist) : (getDistance() > dist));
};
ext.whenTilt = function (s, tilt) {
return device != null && ('=' == s ? (getTilt() == tilt) : (getTilt() != tilt));
};
//ext.whenDistanceLessThan = function(dist) { return device!=null && getDistance() < dist; };
//ext.whenTiltIs = function(tilt) { return device!=null && getTilt() == tilt; };
// Reporters
ext.getDistance = function() { return getDistance(); };
ext.getTilt = function() { return getTilt(); };
ext.getDistance = function () {
return getDistance();
};
ext.getTilt = function () {
return getTilt();
};
// Internal logic
function setMotorDirection(motorID, dir) {
@ -152,7 +165,7 @@
if ((dir == -1) || (dir == 1)) motor.dir = dir;
if (dir == 0) motor.dir = -motor.dir; // reverse
if (motor.isOn) sendMotorState();
};
}
function setMotorOn(type, motorID, flag) {
var motor = getMotor(type, motorID);
@ -161,7 +174,7 @@
motor.isOn = (flag == true);
if (wasOn) checkForMotorsOff();
sendMotorState();
};
}
function setMotorPower(type, motorID, pwr) {
// Pwr: 0..100
@ -172,9 +185,11 @@
if (motor.power > 0) motor.isOn = true;
if (wasOn) checkForMotorsOff();
sendMotorState();
};
}
var wedoCommand = new Uint8Array(9);
wedoCommand[1] = 0x40;
var wedoCommand = new Uint8Array(9); wedoCommand[1] = 0x40;
function sendMotorState() {
if (device) {
// Each motor is controlled by a signed byte whose sign determines the direction and absolute value the power
@ -194,29 +209,29 @@
}
function getMotor(type, motorID) {
if(rawData && okayToReadIDs()) {
var s = new Uint8Array(rawData);
id0 = s[3];
id1 = s[5];
}
//console.log(id0);
//console.log(id0.toString(2));
//console.log(id1);
//console.log(id1.toString(2));
//console.log();
if (rawData && okayToReadIDs()) {
var s = new Uint8Array(rawData);
id0 = s[3];
id1 = s[5];
}
//console.log(id0);
//console.log(id0.toString(2));
//console.log(id1);
//console.log(id1.toString(2));
//console.log();
if ((motorID == 0) && isMotor(type, id0)) return motors[0];
if ((motorID == 1) && isMotor(type, id1)) return motors[1];
return null;
}
function isMotor(type, id) {
switch (type) {
case 'm': // motor
return (234 <= id) && (id <= 246);
case 'l': // light
return (200 <= id) && (id <= 205);
}
return ((234 <= id) && (id <= 246)) || ((200 <= id) && (id <= 205));
switch (type) {
case 'm': // motor
return (234 <= id) && (id <= 246);
case 'l': // light
return (200 <= id) && (id <= 205);
}
return ((234 <= id) && (id <= 246)) || ((200 <= id) && (id <= 205));
}
function checkForMotorsOff() {
@ -246,21 +261,27 @@
weDoDistance = Math.max(0, Math.min(weDoDistance, 100));
}
if ((28 <= id) && (id <= 47)) { // tilt sensor
if (rawValue < 49) weDoTilt = 3;
else if (rawValue < 100) weDoTilt = 2;
else if (rawValue < 154) weDoTilt = 0;
else if (rawValue < 205) weDoTilt = 1;
else weDoTilt = 4;
if (rawValue < 49) {
weDoTilt = 3;
} else if (rawValue < 100) {
weDoTilt = 2;
} else if (rawValue < 154) {
weDoTilt = 0;
} else if (rawValue < 205) {
weDoTilt = 1;
} else {
weDoTilt = 4;
}
}
}
function getDistance() {
if(rawData) processData();
if (rawData) processData();
return weDoDistance;
}
function getTilt() {
if(rawData) processData();
if (rawData) processData();
return weDoTilt;
}
@ -279,58 +300,58 @@
}
var poller = null;
ext._deviceConnected = function(dev) {
if(device) return;
ext._deviceConnected = function (dev) {
if (device) return;
device = dev;
device.open();
poller = setInterval(function() {
device.read(function(data) {
poller = setInterval(function () {
device.read(function (data) {
rawData = data;
});
}, 20);
};
ext._deviceRemoved = function(dev) {
if(device != dev) return;
if(poller) poller = clearInterval(poller);
ext._deviceRemoved = function (dev) {
if (device != dev) return;
if (poller) poller = clearInterval(poller);
device = null;
};
ext._shutdown = function() {
ext._shutdown = function () {
setMotorOn('a', 0, false);
setMotorOn('a', 1, false);
if(poller) poller = clearInterval(poller);
if(device) device.close();
if (poller) poller = clearInterval(poller);
if (device) device.close();
device = null;
};
ext._getStatus = function() {
if(!device) return {status: 1, msg: 'LEGO WeDo disconnected'};
ext._getStatus = function () {
if (!device) return {status: 1, msg: 'LEGO WeDo disconnected'};
return {status: 2, msg: ' LEGO WeDo connected'};
}
};
var descriptor = {
blocks: [
['w', 'turn %m.motor on for %n secs', 'motorOnFor', 'motor',1],
[' ', 'turn %m.motor on', 'motorOn', 'motor'],
[' ', 'turn %m.motor off', 'motorOff', 'motor'],
[' ', 'set %m.motor power to %n', 'startMotorPower', 'motor',100],
[' ', 'set %m.motor2 direction to %m.motorDirection','setMotorDirection','motor','this way'],
['h', 'when distance %m.lessMore %n', 'whenDistance', '<', 20],
['h', 'when tilt %m.eNe %n', 'whenTilt', '=', 1],
['r', 'distance', 'getDistance'],
['r', 'tilt', 'getTilt']
['w', 'turn %m.motor on for %n secs', 'motorOnFor', 'motor', 1],
[' ', 'turn %m.motor on', 'motorOn', 'motor'],
[' ', 'turn %m.motor off', 'motorOff', 'motor'],
[' ', 'set %m.motor power to %n', 'startMotorPower', 'motor', 100],
[' ', 'set %m.motor2 direction to %m.motorDirection', 'setMotorDirection', 'motor', 'this way'],
['h', 'when distance %m.lessMore %n', 'whenDistance', '<', 20],
['h', 'when tilt %m.eNe %n', 'whenTilt', '=', 1],
['r', 'distance', 'getDistance'],
['r', 'tilt', 'getTilt']
],
menus: {
motor: ['motor', 'motor A', 'motor B', 'lights', 'everything'],
motor2: ['motor', 'motor A', 'motor B', 'all motors'],
motor2: ['motor', 'motor A', 'motor B', 'all motors'],
motorDirection: ['this way', 'that way', 'reverse'],
lessMore: ['<', '>'],
eNe: ['=','not =']
eNe: ['=', 'not =']
},
url: '/info/help/studio/tips/ext/LEGO WeDo/'
url: '/info/help/studio/tips/ext/LEGO WeDo/'
};
ScratchExtensions.register('LEGO WeDo', descriptor, ext, {type: 'hid', vendor:1684, product:3});
ScratchExtensions.register('LEGO WeDo', descriptor, ext, {type: 'hid', vendor: 1684, product: 3});
})({});