2015-03-24 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04: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 17:20:38 -04: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 03:33:19 -04:00
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
|
|
|
var loadingURL;
|
2015-07-23 15:45:28 -04:00
|
|
|
lib.loadExternalJS = function (url) {
|
2015-03-24 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04:00
|
|
|
else alert(text);
|
|
|
|
};
|
|
|
|
|
2015-07-23 15:45:28 -04:00
|
|
|
lib.resetPlugin = function () {
|
2015-03-24 03:33:19 -04:00
|
|
|
if (plugin && plugin.reset) plugin.reset();
|
|
|
|
shutdown();
|
|
|
|
};
|
|
|
|
|
2015-07-23 15:45:28 -04:00
|
|
|
$(window).unload(function (e) {
|
2015-03-24 03:33:19 -04:00
|
|
|
shutdown();
|
|
|
|
});
|
|
|
|
|
|
|
|
function shutdown() {
|
2015-07-23 15:45:28 -04:00
|
|
|
for (var extName in handlers)
|
2015-03-24 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04:00
|
|
|
}
|
|
|
|
|
2015-07-23 15:45:28 -04:00
|
|
|
if (awaitingSpecs['serial']) {
|
2015-03-24 03:33:19 -04: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 03:33:19 -04:00
|
|
|
}
|
|
|
|
|
2015-07-23 15:45:28 -04:00
|
|
|
if (!shouldLookForDevices())
|
2015-03-24 03:33:19 -04:00
|
|
|
stopPolling();
|
|
|
|
}
|
|
|
|
|
|
|
|
function checkPolling() {
|
2015-07-23 15:45:28 -04:00
|
|
|
if (poller || !shouldLookForDevices()) return;
|
2015-03-24 03:33:19 -04:00
|
|
|
|
|
|
|
poller = setInterval(checkDevices, 500);
|
2015-07-23 15:45:28 -04:00
|
|
|
}
|
2015-03-24 03:33:19 -04:00
|
|
|
|
|
|
|
function stopPolling() {
|
2015-07-23 15:45:28 -04:00
|
|
|
if (poller) clearInterval(poller);
|
2015-03-24 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04:00
|
|
|
self.close();
|
|
|
|
handlers[ext_name]._deviceRemoved(self);
|
|
|
|
}, 0);
|
|
|
|
}
|
|
|
|
|
2015-07-23 15:45:28 -04:00
|
|
|
this.open = function (readyCallback) {
|
2015-08-07 17:20:38 -04: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 17:20:38 -04:00
|
|
|
if (readyCallback) readyCallback(self);
|
|
|
|
});
|
2015-03-24 03:33:19 -04:00
|
|
|
};
|
2015-07-23 15:45:28 -04:00
|
|
|
this.close = function () {
|
|
|
|
if (!dev) return;
|
2015-03-24 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04: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 03:33:19 -04:00
|
|
|
try {
|
2015-08-07 17:20:38 -04: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 17:20:38 -04:00
|
|
|
// devices[ext_name][path] = self;
|
2015-07-23 15:45:28 -04:00
|
|
|
dev = d;
|
2015-08-07 17:20:38 -04: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 17:20:38 -04: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 03:33:19 -04:00
|
|
|
}
|
|
|
|
};
|
2015-07-23 15:45:28 -04:00
|
|
|
this.close = function () {
|
|
|
|
if (!dev) return;
|
2015-03-24 03:33:19 -04: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 03:33:19 -04:00
|
|
|
dev.send(data);
|
|
|
|
};
|
2015-07-23 15:45:28 -04:00
|
|
|
this.set_receive_handler = function (handler) {
|
|
|
|
if (!dev) return;
|
2015-03-24 03:33:19 -04:00
|
|
|
dev.set_receive_handler(handler);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
})();
|