scratchx/scratch_extensions/scratch_ext.js
Chris Willis-Ford 2c998d1ccc Call checkPolling() directly when plugin exists
When the plugin is already started we skip calling createDevicePlugin(),
which is fine except that we were relying on createDevicePlugin() to
call checkPolling() for us. We now call checkPolling() directly in those
cases where it was being skipped before.
2015-08-07 14:20:38 -07:00

337 lines
11 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 (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 && !pluginAvailable())
return { status: 0, msg: 'Missing browser plugin' };
return handlers[ext_name]._getStatus();
};
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 = {};
for (var 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;
}
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']) {
var 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 (!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;
dev.set_nonblocking(true);
//devices[ext_name][path] = self;
devices[ext_name] = self;
if (readyCallback) readyCallback(self);
});
};
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) {
try {
plugin.serial_open(self.id, opts, function (d) {
// dev.set_disconnect_handler(function () {
// self.close();
// handlers[ext_name]._deviceRemoved(self);
// });
// devices[ext_name][path] = self;
dev = d;
devices[ext_name] = self;
dev.set_error_handler(function (message) {
alert('Serial device error\n\nDevice: ' + id + '\nError: ' + message);
});
if (readyCallback) readyCallback(self);
});
}
catch (e) {
console.log('Error opening serial device ' + id + ': ' + e);
}
};
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);
};
}
})();