455 lines
15 KiB
JavaScript
455 lines
15 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 plugin = null;
|
|
var handlers = {};
|
|
var blockDefs = {};
|
|
var menuDefs = {};
|
|
var deviceSpecs = {};
|
|
var devices = {};
|
|
var poller = null;
|
|
var lib = this;
|
|
|
|
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!');
|
|
return false;
|
|
}
|
|
|
|
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);
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
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.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) {
|
|
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) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
return handlers[ext_name]._getStatus();
|
|
};
|
|
|
|
lib.stop = function (ext_name) {
|
|
var ext = handlers[ext_name];
|
|
if (ext._stop) {
|
|
ext._stop();
|
|
}
|
|
else if (ext.resetAll) { // old, undocumented call
|
|
ext.resetAll();
|
|
}
|
|
};
|
|
|
|
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 = {};
|
|
var ext_name;
|
|
for (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;
|
|
}
|
|
else if (spec.type == 'wedo2') {
|
|
awaitingSpecs['wedo2'] = ext_name;
|
|
}
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
if (ScratchDeviceManager && awaitingSpecs['wedo2']) {
|
|
ext_name = awaitingSpecs['wedo2'];
|
|
ScratchDeviceManager.wedo2_list(function(deviceList) {
|
|
for (var i = 0; i < deviceList.length; ++i) {
|
|
handlers[ext_name]._deviceConnected(new WeDo2Device(deviceList[i].id, 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;
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Wait a moment to access the plugin and claim any devices that plugins are
|
|
// interested in.
|
|
setTimeout(checkPolling, 100);
|
|
}
|
|
|
|
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 (readyCallback) {
|
|
plugin.hid_open(self.id, function (d) {
|
|
dev = d;
|
|
if (dev) {
|
|
devices[ext_name] = self;
|
|
dev.set_nonblocking(true);
|
|
}
|
|
if (readyCallback) readyCallback(d ? self : null);
|
|
});
|
|
};
|
|
this.close = function () {
|
|
if (!dev) return;
|
|
dev.close();
|
|
delete devices[ext_name];
|
|
dev = null;
|
|
|
|
checkPolling();
|
|
};
|
|
this.write = function (data, callback) {
|
|
if (!dev) return;
|
|
dev.write(data, function (len) {
|
|
if (len < 0) disconnect();
|
|
if (callback) callback(len);
|
|
});
|
|
};
|
|
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);
|
|
});
|
|
};
|
|
}
|
|
|
|
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, readyCallback) {
|
|
plugin.serial_open(self.id, opts, function (d) {
|
|
dev = d;
|
|
if (dev) {
|
|
devices[ext_name] = self;
|
|
dev.set_error_handler(function (message) {
|
|
alert('Serial device error\n\nDevice: ' + id + '\nError: ' + message);
|
|
});
|
|
}
|
|
if (readyCallback) readyCallback(d ? self : null);
|
|
});
|
|
};
|
|
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);
|
|
};
|
|
}
|
|
|
|
// 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();
|
|
};
|
|
}
|
|
})();
|