2016-08-02 00:44:18 -07:00
( 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 ;
} ;
2016-10-07 12:09:17 -07:00
// Each block must have a unique function name even if the implementation is identical.
ext . whenTilted = ext . isTilted ;
2016-08-02 00:44:18 -07:00
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 ] ,
2016-10-07 12:09:17 -07:00
[ 'h' , 'when tilted %m.tiltDirAny' , 'whenTilted' , strings . TILT _ANY ] ,
2016-08-02 00:44:18 -07:00
[ '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 ;
}
}
} ;
}
} ) ( { } ) ;