mirror of
https://github.com/scratchfoundation/scratchx.git
synced 2024-11-25 17:18:21 -05:00
2c998d1ccc
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.
337 lines
11 KiB
JavaScript
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);
|
|
};
|
|
}
|
|
})();
|