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 ;
2015-07-23 15:45:28 -04:00
var isOffline = ( Scratch && Scratch . FlashApp && Scratch . FlashApp . ASobj &&
Scratch . FlashApp . ASobj . isOffline && Scratch . FlashApp . ASobj . isOffline ( ) ) ;
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
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 ( ) ;
}
}
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 ) {
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 ] ;
} ;
2015-07-23 15:45:28 -04:00
lib . canAccessDevices = function ( ) { return pluginAvailable ( ) ; } ;
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 ) ;
}
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 ) ;
}
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 ) {
if ( ! ( ext _name in handlers ) )
return { status : 0 , msg : 'Not loaded' } ;
2015-03-24 00:33:19 -07:00
2015-07-23 15:45:28 -04:00
if ( ext _name in deviceSpecs && ! pluginAvailable ( ) )
return { status : 0 , msg : 'Missing browser plugin' } ;
2015-03-24 00:33:19 -07:00
return handlers [ ext _name ] . _getStatus ( ) ;
} ;
2015-07-23 15:45:28 -04:00
lib . notify = function ( text ) {
if ( window . JSsetProjectBanner ) JSsetProjectBanner ( text ) ;
2015-03-24 00:33:19 -07:00
else alert ( text ) ;
} ;
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 ( ) {
2015-07-23 15:45:28 -04:00
for ( var extName in handlers )
2015-03-24 00:33:19 -07:00
handlers [ extName ] . _shutdown ( ) ;
handlers = { } ;
stopPolling ( ) ;
}
function checkDevices ( ) {
var awaitingSpecs = { } ;
2015-07-23 15:45:28 -04:00
for ( var ext _name in deviceSpecs )
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 ;
}
2015-07-23 15:45:28 -04:00
else if ( spec . type == 'serial' )
2015-03-24 00:33:19 -07:00
awaitingSpecs [ 'serial' ] = ext _name ;
}
2015-07-23 15:45:28 -04:00
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 ) ) ;
}
} ) ;
2015-03-24 00:33:19 -07:00
}
2015-07-23 15:45:28 -04:00
if ( awaitingSpecs [ 'serial' ] ) {
2015-03-24 00:33:19 -07:00
var ext _name = awaitingSpecs [ 'serial' ] ;
2015-07-23 15:45:28 -04:00
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
}
2015-07-23 15:45:28 -04:00
if ( ! shouldLookForDevices ( ) )
2015-03-24 00:33:19 -07:00
stopPolling ( ) ;
}
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 ( ) {
2015-07-23 15:45:28 -04:00
for ( var ext _name in deviceSpecs )
if ( ! devices [ ext _name ] )
2015-03-24 00:33:19 -07:00
return true ;
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 ) ;
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.
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
}
function hidDevice ( info , ext _name ) {
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 ;
dev . set _nonblocking ( true ) ;
//devices[ext_name][path] = self;
devices [ ext _name ] = self ;
2015-07-23 15:45:28 -04:00
2015-08-07 14:20:38 -07:00
if ( readyCallback ) readyCallback ( self ) ;
} ) ;
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
} ;
}
function serialDevice ( id , ext _name ) {
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 ) {
2015-03-24 00:33:19 -07:00
try {
2015-08-07 14:20:38 -07:00
plugin . serial _open ( self . id , opts , function ( d ) {
2015-07-23 15:45:28 -04:00
// dev.set_disconnect_handler(function () {
// self.close();
// handlers[ext_name]._deviceRemoved(self);
// });
2015-08-07 14:20:38 -07:00
// devices[ext_name][path] = self;
2015-07-23 15:45:28 -04:00
dev = d ;
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 ) {
alert ( 'Serial device error\n\nDevice: ' + id + '\nError: ' + message ) ;
} ) ;
2015-08-07 14:20:38 -07:00
if ( readyCallback ) readyCallback ( self ) ;
2015-07-23 15:45:28 -04:00
} ) ;
}
catch ( e ) {
console . log ( 'Error opening serial device ' + id + ': ' + e ) ;
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 ) ;
} ;
}
} ) ( ) ;