2015-03-24 00:33:19 -07:00
// scratch_ext.js
// Shane M. Clements, November 2013
// ScratchExtensions
//
// Scratch 2.0 extension manager which Scratch communicates with to initialize extensions and communicate with them.
// The extension manager also handles creating the browser plugin to enable access to HID and serial devices.
2015-07-23 15:45:28 -04:00
window . ScratchExtensions = new ( function ( ) {
2015-03-24 00:33:19 -07:00
var plugin = null ;
var handlers = { } ;
var blockDefs = { } ;
var menuDefs = { } ;
var deviceSpecs = { } ;
var devices = { } ;
var poller = null ;
var lib = this ;
2016-08-02 00:49:34 -07:00
var isOffline = Scratch && Scratch . FlashApp && Scratch . FlashApp . ASobj &&
Scratch . FlashApp . ASobj . isOffline && Scratch . FlashApp . ASobj . isOffline ( ) ;
2015-07-23 15:45:28 -04:00
var pluginAvailable = function ( ) {
return ! ! window . ArrayBuffer && ! ! (
isOffline ||
( window . ScratchPlugin && window . ScratchPlugin . isAvailable ( ) ) ||
( window . ScratchDeviceHost && window . ScratchDeviceHost . isAvailable ( ) )
) ;
} ;
lib . register = function ( name , descriptor , handler , deviceSpec ) {
if ( name in handlers ) {
console . log ( 'Scratch extension "' + name + '" already exists!' ) ;
2015-03-24 00:33:19 -07:00
return false ;
}
handlers [ name ] = handler ;
blockDefs [ name ] = descriptor . blocks ;
2015-07-23 15:45:28 -04:00
if ( descriptor . menus ) menuDefs [ name ] = descriptor . menus ;
if ( deviceSpec ) deviceSpecs [ name ] = deviceSpec ;
2015-03-24 00:33:19 -07:00
// Show the blocks in Scratch!
var extObj = {
extensionName : name ,
blockSpecs : descriptor . blocks ,
url : descriptor . url ,
menus : descriptor . menus ,
javascriptURL : loadingURL
} ;
Scratch . FlashApp . ASobj . ASloadExtension ( extObj ) ;
2015-08-07 14:20:38 -07:00
2016-08-02 00:49:34 -07:00
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 ( ) ;
}
}
2015-08-07 14:20:38 -07:00
2015-03-24 00:33:19 -07:00
return true ;
} ;
var loadingURL ;
2015-07-23 15:45:28 -04:00
lib . loadExternalJS = function ( url ) {
2015-03-24 00:33:19 -07:00
var scr = document . createElement ( "script" ) ;
scr . src = url ; // + "?ts=" + new Date().getTime();
loadingURL = url ;
document . getElementsByTagName ( "head" ) [ 0 ] . appendChild ( scr ) ;
} ;
2015-07-23 15:45:28 -04:00
lib . loadLocalJS = function ( code ) {
2015-03-24 00:33:19 -07:00
// Run the extension code in the global scope
try {
( new Function ( code ) ) ( ) ;
2015-07-23 15:45:28 -04:00
} catch ( e ) {
2015-03-24 00:33:19 -07:00
console . log ( e . stack . toString ( ) ) ;
}
} ;
2015-07-23 15:45:28 -04:00
lib . unregister = function ( name ) {
2016-08-02 00:49:34 -07:00
try {
handlers [ name ] . _shutdown ( ) ;
} catch ( e ) {
}
2015-03-24 00:33:19 -07:00
delete handlers [ name ] ;
delete blockDefs [ name ] ;
delete menuDefs [ name ] ;
delete deviceSpecs [ name ] ;
} ;
2016-08-02 00:49:34 -07:00
lib . canAccessDevices = function ( ) {
return pluginAvailable ( ) ;
} ;
2015-07-23 15:45:28 -04:00
lib . getReporter = function ( ext _name , reporter , args ) {
2015-03-24 00:33:19 -07:00
return handlers [ ext _name ] [ reporter ] . apply ( handlers [ ext _name ] , args ) ;
} ;
2015-07-23 15:45:28 -04:00
lib . getReporterAsync = function ( ext _name , reporter , args , job _id ) {
var callback = function ( retval ) {
2015-03-24 00:33:19 -07:00
Scratch . FlashApp . ASobj . ASextensionReporterDone ( ext _name , job _id , retval ) ;
2016-08-02 00:49:34 -07:00
} ;
2015-03-24 00:33:19 -07:00
args . push ( callback ) ;
handlers [ ext _name ] [ reporter ] . apply ( handlers [ ext _name ] , args ) ;
} ;
2015-07-23 15:45:28 -04:00
lib . getReporterForceAsync = function ( ext _name , reporter , args , job _id ) {
var retval = handlers [ ext _name ] [ reporter ] . apply ( handlers [ ext _name ] , args ) ;
Scratch . FlashApp . ASobj . ASextensionReporterDone ( ext _name , job _id , retval ) ;
} ;
lib . runCommand = function ( ext _name , command , args ) {
2015-03-24 00:33:19 -07:00
handlers [ ext _name ] [ command ] . apply ( handlers [ ext _name ] , args ) ;
} ;
2015-07-23 15:45:28 -04:00
lib . runAsync = function ( ext _name , command , args , job _id ) {
var callback = function ( ) {
2015-03-24 00:33:19 -07:00
Scratch . FlashApp . ASobj . ASextensionCallDone ( ext _name , job _id ) ;
2016-08-02 00:49:34 -07:00
} ;
2015-03-24 00:33:19 -07:00
args . push ( callback ) ;
handlers [ ext _name ] [ command ] . apply ( handlers [ ext _name ] , args ) ;
} ;
2015-07-23 15:45:28 -04:00
lib . getStatus = function ( ext _name ) {
2016-08-02 00:49:34 -07:00
if ( ! ( ext _name in handlers ) ) {
return { status : 0 , msg : 'Not loaded' } ;
}
2015-03-24 00:33:19 -07:00
2016-08-02 00:49:34 -07:00
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 ;
}
}
2015-03-24 00:33:19 -07:00
return handlers [ ext _name ] . _getStatus ( ) ;
} ;
2016-08-02 00:49:34 -07:00
lib . stop = function ( ext _name ) {
var ext = handlers [ ext _name ] ;
if ( ext . _stop ) {
ext . _stop ( ) ;
}
else if ( ext . resetAll ) { // old, undocumented call
ext . resetAll ( ) ;
}
} ;
2015-07-23 15:45:28 -04:00
lib . notify = function ( text ) {
2016-08-02 00:49:34 -07:00
if ( window . JSsetProjectBanner ) {
JSsetProjectBanner ( text ) ;
} else {
alert ( text ) ;
}
2015-03-24 00:33:19 -07:00
} ;
2015-07-23 15:45:28 -04:00
lib . resetPlugin = function ( ) {
2015-03-24 00:33:19 -07:00
if ( plugin && plugin . reset ) plugin . reset ( ) ;
shutdown ( ) ;
} ;
2015-07-23 15:45:28 -04:00
$ ( window ) . unload ( function ( e ) {
2015-03-24 00:33:19 -07:00
shutdown ( ) ;
} ) ;
function shutdown ( ) {
2016-08-02 00:49:34 -07:00
for ( var extName in handlers ) {
2015-03-24 00:33:19 -07:00
handlers [ extName ] . _shutdown ( ) ;
2016-08-02 00:49:34 -07:00
}
2015-03-24 00:33:19 -07:00
handlers = { } ;
stopPolling ( ) ;
}
function checkDevices ( ) {
var awaitingSpecs = { } ;
2016-08-02 00:49:34 -07:00
var ext _name ;
for ( ext _name in deviceSpecs ) {
2015-07-23 15:45:28 -04:00
if ( ! devices [ ext _name ] ) {
2015-03-24 00:33:19 -07:00
var spec = deviceSpecs [ ext _name ] ;
2015-07-23 15:45:28 -04:00
if ( spec . type == 'hid' ) {
if ( ! awaitingSpecs [ 'hid' ] ) awaitingSpecs [ 'hid' ] = { } ;
2015-03-24 00:33:19 -07:00
awaitingSpecs [ 'hid' ] [ spec . vendor + '_' + spec . product ] = ext _name ;
}
2016-08-02 00:49:34 -07:00
else if ( spec . type == 'serial' ) {
2015-03-24 00:33:19 -07:00
awaitingSpecs [ 'serial' ] = ext _name ;
2016-08-02 00:49:34 -07:00
}
else if ( spec . type == 'wedo2' ) {
awaitingSpecs [ 'wedo2' ] = ext _name ;
}
2015-03-24 00:33:19 -07:00
}
2016-08-02 00:49:34 -07:00
}
2015-03-24 00:33:19 -07:00
2016-08-02 00:49:34 -07:00
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 [ '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 ) ) ;
}
} ) ;
}
2015-03-24 00:33:19 -07:00
}
2016-08-02 00:49:34 -07:00
if ( ScratchDeviceManager && awaitingSpecs [ 'wedo2' ] ) {
ext _name = awaitingSpecs [ 'wedo2' ] ;
ScratchDeviceManager . wedo2 _list ( function ( deviceList ) {
for ( var i = 0 ; i < deviceList . length ; ++ i ) {
2016-08-11 18:08:48 -04:00
handlers [ ext _name ] . _deviceConnected ( new WeDo2Device ( deviceList [ i ] . id || deviceList [ i ] , ext _name ) ) ;
2015-07-23 15:45:28 -04:00
}
} ) ;
2015-03-24 00:33:19 -07:00
}
2016-08-02 00:49:34 -07:00
if ( ! shouldLookForDevices ( ) ) {
2015-03-24 00:33:19 -07:00
stopPolling ( ) ;
2016-08-02 00:49:34 -07:00
}
2015-03-24 00:33:19 -07:00
}
function checkPolling ( ) {
2015-07-23 15:45:28 -04:00
if ( poller || ! shouldLookForDevices ( ) ) return ;
2015-03-24 00:33:19 -07:00
poller = setInterval ( checkDevices , 500 ) ;
2015-07-23 15:45:28 -04:00
}
2015-03-24 00:33:19 -07:00
function stopPolling ( ) {
2015-07-23 15:45:28 -04:00
if ( poller ) clearInterval ( poller ) ;
2015-03-24 00:33:19 -07:00
poller = null ;
}
function shouldLookForDevices ( ) {
2016-08-02 00:49:34 -07:00
for ( var ext _name in deviceSpecs ) {
if ( ! devices [ ext _name ] ) {
2015-03-24 00:33:19 -07:00
return true ;
2016-08-02 00:49:34 -07:00
}
}
2015-03-24 00:33:19 -07:00
return false ;
}
function createDevicePlugin ( ) {
2015-07-23 15:45:28 -04:00
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 ) ;
} else {
// Not IE: try NPAPI
var pluginContainer = document . createElement ( 'div' ) ;
document . getElementById ( 'scratch' ) . parentNode . appendChild ( pluginContainer ) ;
2016-08-02 00:49:34 -07:00
pluginContainer . innerHTML =
'<object type="application/x-scratchdeviceplugin" width="1" height="1"> </object>' ;
2015-07-23 15:45:28 -04:00
plugin = pluginContainer . firstChild ;
}
// Talk to the actual plugin, but make it pretend to be asynchronous.
plugin = new window . ScratchPlugin . PluginWrapper ( plugin ) ;
}
2015-03-24 00:33:19 -07:00
2015-07-23 15:45:28 -04:00
// Wait a moment to access the plugin and claim any devices that plugins are
// interested in.
setTimeout ( checkPolling , 100 ) ;
2015-03-24 00:33:19 -07:00
}
2016-08-02 00:49:34 -07:00
function HidDevice ( info , ext _name ) {
2015-03-24 00:33:19 -07:00
var dev = null ;
var self = this ;
// TODO: add support for multiple devices per extension
//if(!(ext_name in devices)) devices[ext_name] = {};
this . id = info [ "path" ] ;
this . info = info ;
function disconnect ( ) {
2015-07-23 15:45:28 -04:00
setTimeout ( function ( ) {
2015-03-24 00:33:19 -07:00
self . close ( ) ;
handlers [ ext _name ] . _deviceRemoved ( self ) ;
} , 0 ) ;
}
2015-07-23 15:45:28 -04:00
this . open = function ( readyCallback ) {
2015-08-07 14:20:38 -07:00
plugin . hid _open ( self . id , function ( d ) {
dev = d ;
2016-08-02 00:49:34 -07:00
if ( dev ) {
devices [ ext _name ] = self ;
dev . set _nonblocking ( true ) ;
}
if ( readyCallback ) readyCallback ( d ? self : null ) ;
2015-08-07 14:20:38 -07:00
} ) ;
2015-03-24 00:33:19 -07:00
} ;
2015-07-23 15:45:28 -04:00
this . close = function ( ) {
if ( ! dev ) return ;
2015-03-24 00:33:19 -07:00
dev . close ( ) ;
delete devices [ ext _name ] ;
dev = null ;
checkPolling ( ) ;
} ;
2015-07-23 15:45:28 -04:00
this . write = function ( data , callback ) {
if ( ! dev ) return ;
dev . write ( data , function ( len ) {
if ( len < 0 ) disconnect ( ) ;
if ( callback ) callback ( len ) ;
} ) ;
2015-03-24 00:33:19 -07:00
} ;
2015-07-23 15:45:28 -04:00
this . read = function ( callback , len ) {
if ( ! dev ) return null ;
if ( ! len ) len = 65 ;
dev . read ( len , function ( data ) {
if ( data . byteLength == 0 ) disconnect ( ) ;
callback ( data ) ;
} ) ;
2015-03-24 00:33:19 -07:00
} ;
}
2016-08-02 00:49:34 -07:00
function SerialDevice ( id , ext _name ) {
2015-03-24 00:33:19 -07:00
var dev = null ;
var self = this ;
// TODO: add support for multiple devices per extension
//if(!(ext_name in devices)) devices[ext_name] = {};
this . id = id ;
2015-07-23 15:45:28 -04:00
this . open = function ( opts , readyCallback ) {
2016-08-02 00:49:34 -07:00
plugin . serial _open ( self . id , opts , function ( d ) {
dev = d ;
if ( dev ) {
2015-08-07 14:20:38 -07:00
devices [ ext _name ] = self ;
2015-07-23 15:45:28 -04:00
dev . set _error _handler ( function ( message ) {
2016-09-13 14:30:35 -04:00
console . log ( 'Serial device error\nDevice: ' + id + '\nError: ' + message ) ;
2015-07-23 15:45:28 -04:00
} ) ;
2016-08-02 00:49:34 -07:00
}
if ( readyCallback ) readyCallback ( d ? self : null ) ;
} ) ;
2015-03-24 00:33:19 -07:00
} ;
2015-07-23 15:45:28 -04:00
this . close = function ( ) {
if ( ! dev ) return ;
2015-03-24 00:33:19 -07:00
dev . close ( ) ;
delete devices [ ext _name ] ;
dev = null ;
checkPolling ( ) ;
} ;
2015-07-23 15:45:28 -04:00
this . send = function ( data ) {
if ( ! dev ) return ;
2015-03-24 00:33:19 -07:00
dev . send ( data ) ;
} ;
2015-07-23 15:45:28 -04:00
this . set _receive _handler = function ( handler ) {
if ( ! dev ) return ;
2015-03-24 00:33:19 -07:00
dev . set _receive _handler ( handler ) ;
} ;
}
2016-08-02 00:49:34 -07:00
// 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 ( ) ;
} ;
}
2015-03-24 00:33:19 -07:00
} ) ( ) ;