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
Christopher Willis-Ford 47d9063829 Update scratch_extensions for upcoming scratchr2
This corresponds to the upcoming October 20th release
2016-10-07 12:09:17 -07:00

615 lines
20 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);
};
if(handlers[ext_name]._getStatus().status != 2){
callback(false);
}
else{
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 'ble':
case 'wedo2':
if (!(ScratchDeviceManager && ScratchDeviceManager.isConnected())) {
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 {
awaitingSpecs[spec.type] = ext_name;
}
}
}
for (var specType in awaitingSpecs) {
if (!awaitingSpecs.hasOwnProperty(specType)) continue;
if (plugin && specType == 'hid') {
var awaitingHid = awaitingSpecs['hid'];
plugin.hid_list(function (deviceList) {
for (var i = 0; i < deviceList.length; i++) {
var deviceID = deviceList[i]["vendor_id"] + '_' + deviceList[i]["product_id"];
var hid_ext_name = awaitingHid[deviceID];
if (hid_ext_name) {
handlers[hid_ext_name]._deviceConnected(new HidDevice(deviceList[i], hid_ext_name));
}
}
});
}
else if (plugin && specType == '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));
}
});
}
else if (ScratchDeviceManager) {
ext_name = awaitingSpecs[specType];
ScratchDeviceManager.device_list(specType, ext_name, deviceSpecs[ext_name], deviceListCallback);
}
}
if (!shouldLookForDevices()) {
stopPolling();
}
}
function deviceListCallback(deviceList, ext_type, ext_name) {
for (var i = 0; i < deviceList.length; ++i) {
var deviceConstructor = Devices[ext_type];
var deviceId = deviceList[i].id || deviceList[i];
var device = new deviceConstructor(deviceId, ext_type, ext_name);
handlers[ext_name]._deviceConnected(device);
}
}
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;
try {
// 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);
}
}
catch (e) {
console.error('Error creating plugin or wrapper:', e);
plugin = null;
}
// 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);
};
}
function WeDo2Device(id, ext_type, ext_name) {
var dev = null;
this.ext_type = ext_type;
this.ext_name = ext_name;
var self = this;
this.id = id;
function RawWeDo2(deviceId, socket) {
var WeDo = this;
var eventHandlers = {};
WeDo.close = function() {
socket.close();
};
WeDo.setMotorOn = function(motorIndex, power) {
socket.emit('motorOn', {motorIndex:motorIndex, power:power});
};
WeDo.setMotorOff = function(motorIndex) {
socket.emit('motorOff', {motorIndex:motorIndex});
};
WeDo.setMotorBrake = function(motorIndex) {
socket.emit('motorBrake', {motorIndex:motorIndex});
};
WeDo.setLED = function(rgb) {
socket.emit('setLED', {rgb:rgb});
};
WeDo.playTone = function(tone, durationMs) {
socket.emit('playTone', {tone:tone, ms:durationMs});
};
WeDo.stopTone = function() {
socket.emit('stopTone');
};
function setHandler(eventName, handler) {
if (eventHandlers.hasOwnProperty(eventName)) {
var oldHandler = eventHandlers[eventName];
if (oldHandler) {
socket.removeListener(eventName, oldHandler);
}
}
if (handler) {
socket.on(eventName, handler);
}
eventHandlers[eventName] = handler;
}
// function handler(event) { access event.sensorName and event.sensorValue }
WeDo.setSensorHandler = function (handler) {
setHandler('sensorChanged', handler);
};
WeDo.setDeviceWasClosedHandler = function (handler) {
// TODO: resolve this ambiguity
setHandler('disconnect', handler);
setHandler('deviceWasClosed', handler);
};
}
function disconnect() {
setTimeout(function () {
self.close();
handlers[ext_name]._deviceRemoved(self);
}, 0);
}
this.is_open = function() {
return !!dev;
};
this.open = function(readyCallback) {
ScratchDeviceManager.socket_open(self.ext_name, self.ext_type, self.id, function(socket) {
if (socket) {
dev = new RawWeDo2(self.id, socket);
devices[ext_name] = self;
dev.setDeviceWasClosedHandler(disconnect);
}
else {
dev = null;
disconnect();
}
if (readyCallback) {
readyCallback(dev ? self : null);
}
});
};
this.close = function() {
if (!dev) return;
dev.close();
delete devices[ext_name];
dev = null;
checkPolling();
};
// 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();
};
}
function BleDevice(id, ext_type, ext_name) {
var self = this;
this.ext_name = ext_name;
this.ext_type = ext_type;
this.socket = null;
this.id = id;
var onActions = [];
var onceActions = [];
function disconnect() {
setTimeout(function () {
self.close();
handlers[self.ext_name]._deviceRemoved(self);
}, 0);
}
this.emit = function(action, data){
if(self.socket){
self.socket.emit(action, data);
}
return !!self.socket;
};
this.on = function(action, callback){
if(self.is_open()){
self.socket.on(action, callback);
}
else{
onActions.push([action, callback]);
}
};
this.once = function(action, callback){
if(self.is_open()){
self.socket.once(action, callback);
}
else{
onceActions.push([action, callback]);
}
};
this.open = function(readyCallback) {
ScratchDeviceManager.socket_open(self.ext_name, ext_type, self.id, function(s) {
self.socket = s;
if (self.socket) {
devices[self.ext_name] = self;
onActions.forEach(function(element){
self.socket.on(element[0], element[1]);
});
onceActions.forEach(function(element){
self.socket.once(element[0], element[1]);
});
self.socket.on('disconnect', disconnect);
self.socket.on('deviceWasClosed', disconnect);
}
if (readyCallback) readyCallback(self.socket ? self : null);
});
};
this.close = function() {
if (!self.socket) return;
self.socket.close();
delete devices[self.ext_name];
self.socket = null;
checkPolling();
};
this.is_open = function() {
return !!self.socket;
};
}
Devices = {ble: BleDevice, wedo2: WeDo2Device};
})();