This repository has been archived on 2025-05-05. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
scratchx/scratch_extensions/scratch_ext.js

456 lines
15 KiB
JavaScript
Raw Normal View History

// 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 || 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;
// 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) {
console.log('Serial device error\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();
};
}
})();