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
Kreg Hanning ed239eb0f0 Move serial errors to console
When a serial device is disconnected the user receives two
dialog messages indicating there was a serial device error.
This can cause confusion since the error message does not
clearly indicate what has happened.

Instead of alerting the user this will push the message to the console.
2016-09-13 14:30:35 -04:00

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 || 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();
};
}
})();