414 lines
12 KiB
JavaScript
414 lines
12 KiB
JavaScript
|
|
(function (ext) {
|
|
|
|
var device = null;
|
|
|
|
var motors = [
|
|
new Motor(0),
|
|
new Motor(1)
|
|
];
|
|
|
|
var sensors = {
|
|
tiltX: 0,
|
|
tiltY: 0,
|
|
distance: 0
|
|
};
|
|
|
|
ext.motorOnFor = function (motorId, time, callback) {
|
|
var milliseconds = 1000 * time;
|
|
// Tell each motor to turn on for `time`
|
|
forEachMotor(motorId, function (motor) {
|
|
motor.cancelMotorTimeout();
|
|
motor.setMotorOnFor(milliseconds);
|
|
});
|
|
// This block runs for a fixed amount of time, even if the motors end up getting interrupted by another block
|
|
setTimeout(function () {
|
|
if (callback) callback();
|
|
}, milliseconds);
|
|
};
|
|
|
|
ext.motorOn = function (motorId) {
|
|
forEachMotor(motorId, function (motor) {
|
|
motor.cancelMotorTimeout();
|
|
motor.setMotorOn();
|
|
});
|
|
};
|
|
|
|
ext.motorOff = function (motorId) {
|
|
forEachMotor(motorId, function (motor) {
|
|
motor.cancelMotorTimeout();
|
|
motor.startBraking();
|
|
});
|
|
};
|
|
|
|
ext.startMotorPower = function (motorId, power) {
|
|
power = Math.max(0, Math.min(power, 100));
|
|
forEachMotor(motorId, function (motor) {
|
|
motor.power = power;
|
|
});
|
|
ext.motorOn(motorId);
|
|
};
|
|
|
|
ext.setMotorDirection = function (motorId, direction) {
|
|
forEachMotor(motorId, function (motor) {
|
|
switch (direction) {
|
|
case strings.DIR_FORWARD:
|
|
motor.dir = 1;
|
|
break;
|
|
case strings.DIR_BACK:
|
|
motor.dir = -1;
|
|
break;
|
|
case strings.DIR_REV:
|
|
motor.dir = -motor.dir;
|
|
break;
|
|
default:
|
|
console.log('Unknown motor direction: ' + direction);
|
|
break;
|
|
}
|
|
if (motor.isOn) {
|
|
// change direction immediately, without altering power or timeout state
|
|
motor.setMotorOn();
|
|
}
|
|
});
|
|
};
|
|
|
|
ext.setLED = function (hue) {
|
|
if (device) {
|
|
// Change from [0,100] range to [0,360] range
|
|
hue = hue * 360 / 100;
|
|
|
|
var rgbArray = HSVToRGB(hue, 1, 1);
|
|
|
|
var r = Math.floor(rgbArray[0] * 255);
|
|
var g = Math.floor(rgbArray[1] * 255);
|
|
var b = Math.floor(rgbArray[2] * 255);
|
|
|
|
// Form hexadecimal number: 0xRRGGBB
|
|
var rgbNumber = (((r << 8) | g) << 8) | b;
|
|
|
|
device.set_led(rgbNumber);
|
|
}
|
|
};
|
|
|
|
ext.playNote = function (note, duration, callback) {
|
|
var durationMs = duration * 1000;
|
|
if (device) {
|
|
// TODO: offer music helpers to extensions:
|
|
// - convert beats to duration
|
|
// - convert note number to frequency
|
|
|
|
var tone = noteToTone(note);
|
|
device.play_tone(tone, durationMs);
|
|
}
|
|
// Keep disconnected behavior similar to connected behavior by delaying the callback even with no device.
|
|
setTimeout(callback, durationMs);
|
|
};
|
|
|
|
ext.stopNote = function () {
|
|
if (device) {
|
|
device.stop_tone();
|
|
}
|
|
};
|
|
|
|
ext.whenDistance = function (op, reference) {
|
|
if (device) {
|
|
switch (op) {
|
|
case strings.COMP_LESS:
|
|
return ext.getDistance() < reference;
|
|
case strings.COMP_MORE:
|
|
return ext.getDistance() > reference;
|
|
default:
|
|
console.log('Unknown operator in whenDistance: ' + op);
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
ext.getDistance = function () {
|
|
return device ? sensors.distance * 10 : 0;
|
|
};
|
|
|
|
ext.isTilted = function (tiltDirAny) {
|
|
if (device) {
|
|
var threshold = 15;
|
|
// TODO: share code with getTilt
|
|
switch(tiltDirAny) {
|
|
case strings.TILT_ANY:
|
|
return (Math.abs(sensors.tiltX) >= threshold) || (Math.abs(sensors.tiltY) >= threshold);
|
|
case strings.TILT_UP:
|
|
return -sensors.tiltY > threshold;
|
|
case strings.TILT_DOWN:
|
|
return sensors.tiltY > threshold;
|
|
case strings.TILT_LEFT:
|
|
return -sensors.tiltX > threshold;
|
|
case strings.TILT_RIGHT:
|
|
return sensors.tiltX > threshold;
|
|
}
|
|
}
|
|
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) {
|
|
case strings.TILT_UP:
|
|
tiltValue = -sensors.tiltY;
|
|
break;
|
|
case strings.TILT_DOWN:
|
|
tiltValue = sensors.tiltY;
|
|
break;
|
|
case strings.TILT_LEFT:
|
|
tiltValue = -sensors.tiltX;
|
|
break;
|
|
case strings.TILT_RIGHT:
|
|
tiltValue = sensors.tiltX;
|
|
break;
|
|
default:
|
|
console.log('Unknown tilt direction in getTilt: ' + tiltDir);
|
|
tiltValue = 0;
|
|
break;
|
|
}
|
|
return tiltValue;
|
|
};
|
|
|
|
function forEachMotor (motorId, motorFunction) {
|
|
var motorIndices;
|
|
switch (motorId) {
|
|
case strings.MOTOR_A:
|
|
motorIndices = [0];
|
|
break;
|
|
case strings.MOTOR_B:
|
|
motorIndices = [1];
|
|
break;
|
|
case strings.MOTOR_DEFAULT:
|
|
case strings.MOTOR_ALL:
|
|
motorIndices = [0, 1];
|
|
break;
|
|
default:
|
|
console.log('Invalid motor ID');
|
|
motorIndices = [];
|
|
break;
|
|
}
|
|
var numMotors = motorIndices.length;
|
|
for (var i = 0; i < numMotors; ++i) {
|
|
motorFunction(motors[motorIndices[i]]);
|
|
}
|
|
}
|
|
|
|
function onSensorChanged (event) {
|
|
sensors[event.sensorName] = event.sensorValue;
|
|
}
|
|
|
|
function clamp (val, min, max) {
|
|
return Math.max(min, Math.min(val, max));
|
|
}
|
|
|
|
// See https://en.wikipedia.org/wiki/HSL_and_HSV#From_HSV
|
|
// Returns an array of [R, G, B] where each component is in the range [0,1]
|
|
function HSVToRGB (hueDegrees, saturation, value) {
|
|
hueDegrees %= 360;
|
|
if (hueDegrees < 0) hueDegrees += 360;
|
|
saturation = clamp(saturation, 0, 1);
|
|
value = clamp(value, 0, 1);
|
|
|
|
var chroma = value * saturation;
|
|
var huePrime = hueDegrees / 60;
|
|
var x = chroma * (1 - Math.abs(huePrime % 2 - 1));
|
|
var rgb;
|
|
switch(Math.floor(huePrime)) {
|
|
case 0:
|
|
rgb = [chroma, x, 0];
|
|
break;
|
|
case 1:
|
|
rgb = [x, chroma, 0];
|
|
break;
|
|
case 2:
|
|
rgb = [0, chroma, x];
|
|
break;
|
|
case 3:
|
|
rgb = [0, x, chroma];
|
|
break;
|
|
case 4:
|
|
rgb = [x, 0, chroma];
|
|
break;
|
|
case 5:
|
|
rgb = [chroma, 0, x];
|
|
break;
|
|
}
|
|
|
|
var m = value - chroma;
|
|
rgb[0] += m;
|
|
rgb[1] += m;
|
|
rgb[2] += m;
|
|
|
|
return rgb;
|
|
}
|
|
|
|
function noteToTone (note) {
|
|
return 440 * Math.pow(2, (note - 69) / 12); // midi key 69 is A (440 Hz)
|
|
}
|
|
|
|
ext._deviceConnected = function (dev) {
|
|
if (device) return;
|
|
|
|
device = dev;
|
|
device.open(function (d) {
|
|
if (device == d) {
|
|
device.set_sensor_handler(onSensorChanged);
|
|
}
|
|
else if (d) {
|
|
console.log('Received open callback for wrong device');
|
|
}
|
|
else {
|
|
console.log('Opening device failed');
|
|
device = null;
|
|
}
|
|
});
|
|
};
|
|
|
|
ext._deviceRemoved = function (dev) {
|
|
if (device != dev) return;
|
|
device = null;
|
|
};
|
|
|
|
ext._stop = function () {
|
|
if (device) {
|
|
device.stop_tone();
|
|
forEachMotor(strings.MOTOR, function (motor) {
|
|
motor.cancelMotorTimeout();
|
|
motor.setMotorOff();
|
|
});
|
|
}
|
|
};
|
|
|
|
ext._shutdown = function () {
|
|
if (device) {
|
|
ext._stop();
|
|
device.close();
|
|
device = null;
|
|
}
|
|
};
|
|
|
|
ext._getStatus = function () {
|
|
if (device) {
|
|
if (device.is_open()) {
|
|
return {status: 2, msg: 'LEGO WeDo 2.0 connected'};
|
|
}
|
|
else {
|
|
return {status: 1, msg: 'LEGO WeDo 2.0 connecting...'};
|
|
}
|
|
}
|
|
else {
|
|
return {status: 1, msg: 'LEGO WeDo 2.0 disconnected'};
|
|
}
|
|
};
|
|
|
|
var strings = {
|
|
MOTOR_DEFAULT: 'motor',
|
|
MOTOR_A: 'motor A',
|
|
MOTOR_B: 'motor B',
|
|
MOTOR_ALL: 'all motors',
|
|
DIR_FORWARD: 'this way',
|
|
DIR_BACK: 'that way',
|
|
DIR_REV: 'reverse',
|
|
TILT_UP: 'up',
|
|
TILT_DOWN: 'down',
|
|
TILT_LEFT: 'left',
|
|
TILT_RIGHT: 'right',
|
|
TILT_ANY: 'any',
|
|
COMP_LESS: '<',
|
|
COMP_MORE: '>',
|
|
COMP_EQ: '=',
|
|
COMP_NEQ: 'not ='
|
|
};
|
|
|
|
var descriptor = {
|
|
blocks: [
|
|
['w', 'turn %m.motor on for %n secs', 'motorOnFor', strings.MOTOR_DEFAULT, 1],
|
|
[' ', 'turn %m.motor on', 'motorOn', strings.MOTOR_DEFAULT],
|
|
[' ', 'turn %m.motor off', 'motorOff', strings.MOTOR_DEFAULT],
|
|
[' ', 'set %m.motor power to %n', 'startMotorPower', strings.MOTOR_DEFAULT, 100],
|
|
[' ', 'set %m.motor direction to %m.motorDir', 'setMotorDirection', strings.MOTOR_DEFAULT, strings.DIR_FORWARD],
|
|
[' ', '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', 'whenTilted', strings.TILT_ANY],
|
|
['r', 'distance', 'getDistance'],
|
|
['b', 'tilted %m.tiltDirAny ?', 'isTilted', strings.TILT_ANY],
|
|
['r', 'tilt angle %m.tiltDir', 'getTilt', strings.TILT_UP]
|
|
],
|
|
menus: {
|
|
motor: [strings.MOTOR_DEFAULT, strings.MOTOR_A, strings.MOTOR_B, strings.MOTOR_ALL],
|
|
motorDir: [strings.DIR_FORWARD, strings.DIR_BACK, strings.DIR_REV],
|
|
tiltDir: [strings.TILT_UP, strings.TILT_DOWN, strings.TILT_LEFT, strings.TILT_RIGHT],
|
|
tiltDirAny: [strings.TILT_ANY, strings.TILT_UP, strings.TILT_DOWN, strings.TILT_LEFT, strings.TILT_RIGHT],
|
|
lessMore: [strings.COMP_LESS, strings.COMP_MORE],
|
|
eNe: [strings.COMP_EQ, strings.COMP_NEQ]
|
|
},
|
|
url: '/info/help/studio/tips/ext/LEGO WeDo 2/'
|
|
};
|
|
ScratchExtensions.register('LEGO WeDo 2.0', descriptor, ext, { type: 'wedo2' });
|
|
|
|
function Motor (motorIndex) {
|
|
var motor = this;
|
|
|
|
// Motor power: 0 to 100
|
|
motor.power = 100;
|
|
|
|
// Motor direction: 1 for "this way" or -1 for "that way"
|
|
motor.dir = 1;
|
|
|
|
// Is the motor currently on (not braking or drifting)?
|
|
motor.isOn = false;
|
|
|
|
// Pending timeout set by motorOnFor() or startBraking()
|
|
motor.pendingTimeoutId = null;
|
|
|
|
motor.setMotorOn = function () {
|
|
if (device) {
|
|
device.set_motor_on(motorIndex, motor.power * motor.dir);
|
|
motor.isOn = true;
|
|
}
|
|
};
|
|
|
|
motor.setMotorOnFor = function (milliseconds) {
|
|
if (device) {
|
|
motor.setMotorOn();
|
|
motor.pendingTimeoutId = setTimeout(motor.startBraking, milliseconds);
|
|
}
|
|
};
|
|
|
|
// Turn on the brake now, then turn the motor completely off in a bit to save battery
|
|
var motorBrakeTime = 1000; // milliseconds
|
|
motor.startBraking = function () {
|
|
if (device) {
|
|
device.set_motor_brake(motorIndex);
|
|
motor.isOn = false;
|
|
motor.pendingTimeoutId = setTimeout(motor.setMotorOff, motorBrakeTime);
|
|
}
|
|
};
|
|
|
|
// Turn the motor off and forget the timeout ID
|
|
motor.setMotorOff = function () {
|
|
if (device) {
|
|
device.set_motor_off(motorIndex);
|
|
motor.isOn = false;
|
|
|
|
motor.pendingTimeoutId = null;
|
|
}
|
|
};
|
|
|
|
// If there's a pending timeout (off/break or on/off/break sequence) for the given motor, cancel it
|
|
motor.cancelMotorTimeout = function () {
|
|
if (device) {
|
|
if (motor.pendingTimeoutId !== null) {
|
|
clearTimeout(motor.pendingTimeoutId);
|
|
motor.pendingTimeoutId = null;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
})({});
|