mirror of
https://github.com/scratchfoundation/scratchx.git
synced 2024-11-25 09:08:28 -05:00
287 lines
9.1 KiB
JavaScript
287 lines
9.1 KiB
JavaScript
|
// 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.
|
||
|
window.ScratchExtensions = new (function(){
|
||
|
var pluginName = 'Scratch Device Plugin'; // will be 'Scratch Plugin for Devices'
|
||
|
var pluginAvailable = (window.ActiveXObject || !!navigator.plugins[pluginName]) && !!window.ArrayBuffer;
|
||
|
var plugin = null;
|
||
|
var handlers = {};
|
||
|
var blockDefs = {};
|
||
|
var menuDefs = {};
|
||
|
var deviceSpecs = {};
|
||
|
var devices = {};
|
||
|
var poller = null;
|
||
|
var lib = this;
|
||
|
|
||
|
lib.register = function(name, descriptor, handler, deviceSpec) {
|
||
|
if(name in handlers) {
|
||
|
console.log('Scratch extension "'+name+'" already exists!');
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if(deviceSpec) {
|
||
|
if(!pluginAvailable && window.ActiveXObject) {
|
||
|
JSsetProjectBanner('Sorry, your version of Internet Explorer is not supported. Please upgrade to version 10 or 11.');
|
||
|
}
|
||
|
if(pluginAvailable && !plugin) setTimeout(createDevicePlugin, 10);
|
||
|
|
||
|
// Wait a moment to access the plugin and claim any devices that plugins are
|
||
|
// interested in.
|
||
|
setTimeout(checkPolling, 100);
|
||
|
}
|
||
|
|
||
|
handlers[name] = handler;
|
||
|
blockDefs[name] = descriptor.blocks;
|
||
|
if(descriptor.menus) menuDefs[name] = descriptor.menus;
|
||
|
if(deviceSpec) deviceSpecs[name] = deviceSpec;
|
||
|
|
||
|
// 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);
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
var loadingURL;
|
||
|
lib.loadExternalJS = function(url) {
|
||
|
var scr = document.createElement("script");
|
||
|
scr.src = url;// + "?ts=" + new Date().getTime();
|
||
|
loadingURL = url;
|
||
|
document.getElementsByTagName("head")[0].appendChild(scr);
|
||
|
};
|
||
|
|
||
|
lib.loadLocalJS = function(code) {
|
||
|
// Run the extension code in the global scope
|
||
|
try {
|
||
|
(new Function(code))();
|
||
|
} catch(e) {
|
||
|
console.log(e.stack.toString());
|
||
|
}
|
||
|
};
|
||
|
|
||
|
lib.unregister = function(name) {
|
||
|
try { handlers[name]._shutdown(); } catch(e){}
|
||
|
delete handlers[name];
|
||
|
delete blockDefs[name];
|
||
|
delete menuDefs[name];
|
||
|
delete deviceSpecs[name];
|
||
|
};
|
||
|
|
||
|
lib.canAccessDevices = function() { return pluginAvailable; };
|
||
|
lib.getReporter = function(ext_name, reporter, args) {
|
||
|
return handlers[ext_name][reporter].apply(handlers[ext_name], args);
|
||
|
};
|
||
|
|
||
|
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);
|
||
|
};
|
||
|
|
||
|
lib.runCommand = function(ext_name, command, args) {
|
||
|
handlers[ext_name][command].apply(handlers[ext_name], args);
|
||
|
};
|
||
|
|
||
|
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 deviceSpecs && !pluginAvailable)
|
||
|
return {status: 0, msg: 'Missing browser plugin'};
|
||
|
|
||
|
return handlers[ext_name]._getStatus();
|
||
|
};
|
||
|
|
||
|
lib.notify = function(text) {
|
||
|
if(window.JSsetProjectBanner) JSsetProjectBanner(text);
|
||
|
else alert(text);
|
||
|
};
|
||
|
|
||
|
lib.resetPlugin = function() {
|
||
|
if (plugin && plugin.reset) plugin.reset();
|
||
|
shutdown();
|
||
|
};
|
||
|
|
||
|
$(window).unload(function(e) {
|
||
|
shutdown();
|
||
|
});
|
||
|
|
||
|
function shutdown() {
|
||
|
for(var extName in handlers)
|
||
|
handlers[extName]._shutdown();
|
||
|
handlers = {};
|
||
|
stopPolling();
|
||
|
}
|
||
|
|
||
|
function checkDevices() {
|
||
|
var awaitingSpecs = {};
|
||
|
for(var 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')
|
||
|
awaitingSpecs['serial'] = ext_name;
|
||
|
}
|
||
|
|
||
|
if(awaitingSpecs['hid']) {
|
||
|
var deviceList = plugin.hid_list();
|
||
|
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']) {
|
||
|
var ext_name = awaitingSpecs['serial'];
|
||
|
var deviceList = plugin.serial_list();
|
||
|
for (var i = 0; i < deviceList.length; i++) {
|
||
|
handlers[ext_name]._deviceConnected(new serialDevice(deviceList[i], ext_name));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(!shouldLookForDevices())
|
||
|
stopPolling();
|
||
|
}
|
||
|
|
||
|
function checkPolling() {
|
||
|
if(poller || !shouldLookForDevices()) return;
|
||
|
|
||
|
poller = setInterval(checkDevices, 500);
|
||
|
}
|
||
|
|
||
|
function stopPolling() {
|
||
|
if(poller) clearInterval(poller);
|
||
|
poller = null;
|
||
|
}
|
||
|
|
||
|
function shouldLookForDevices() {
|
||
|
for(var ext_name in deviceSpecs)
|
||
|
if(!devices[ext_name])
|
||
|
return true;
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
function createDevicePlugin() {
|
||
|
if(plugin) return;
|
||
|
|
||
|
var pluginContainer = document.createElement('div');
|
||
|
document.getElementById('scratch').parentNode.appendChild(pluginContainer);
|
||
|
pluginContainer.innerHTML = '<object type="application/x-scratchdeviceplugin" width="1" height="1" codebase="/scratchr2/static/ext/ScratchDevicePlugin.cab"> </object>';
|
||
|
plugin = pluginContainer.firstChild;
|
||
|
}
|
||
|
|
||
|
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() {
|
||
|
setTimeout(function(){
|
||
|
self.close();
|
||
|
handlers[ext_name]._deviceRemoved(self);
|
||
|
}, 0);
|
||
|
}
|
||
|
|
||
|
this.open = function() {
|
||
|
try {
|
||
|
dev = plugin.hid_open(this.id);
|
||
|
if(window.ActiveXObject) dev = dev(this.id);
|
||
|
dev.set_nonblocking(true);
|
||
|
//devices[ext_name][path] = this;
|
||
|
devices[ext_name] = this;
|
||
|
}
|
||
|
catch(e) {}
|
||
|
};
|
||
|
this.close = function() {
|
||
|
if(!dev) return;
|
||
|
dev.close();
|
||
|
delete devices[ext_name];
|
||
|
dev = null;
|
||
|
|
||
|
checkPolling();
|
||
|
};
|
||
|
this.write = function(data) {
|
||
|
if(!dev) return;
|
||
|
var len = dev.write(data);
|
||
|
if(window.ActiveXObject) len = len(data);
|
||
|
if(len < 0) disconnect();
|
||
|
return len;
|
||
|
};
|
||
|
this.read = function(len) {
|
||
|
if(!dev) return null;
|
||
|
if(!len) len = 65;
|
||
|
var data = dev.read(len);
|
||
|
if(window.ActiveXObject) data = data(len);
|
||
|
if(data.byteLength == 0) disconnect();
|
||
|
return data;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
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;
|
||
|
this.open = function(opts) {
|
||
|
try {
|
||
|
dev = plugin.serial_open(this.id, opts);
|
||
|
// dev.set_disconnect_handler(function() {
|
||
|
// self.close();
|
||
|
// handlers[ext_name]._deviceRemoved(self);
|
||
|
// });
|
||
|
//devices[ext_name][path] = this;
|
||
|
devices[ext_name] = this;
|
||
|
}
|
||
|
catch(e) {}
|
||
|
};
|
||
|
this.close = function() {
|
||
|
if(!dev) return;
|
||
|
dev.close();
|
||
|
delete devices[ext_name];
|
||
|
dev = null;
|
||
|
|
||
|
checkPolling();
|
||
|
};
|
||
|
this.send = function(data) {
|
||
|
if(!dev) return;
|
||
|
dev.send(data);
|
||
|
};
|
||
|
this.set_receive_handler = function(handler) {
|
||
|
if(!dev) return;
|
||
|
dev.set_receive_handler(handler);
|
||
|
};
|
||
|
}
|
||
|
})();
|