From 1f61e1ab3ef171f7897469c4e6d4a06fac1bd298 Mon Sep 17 00:00:00 2001 From: Chris Willis-Ford Date: Thu, 23 Jul 2015 15:45:28 -0400 Subject: [PATCH] Updated extension interface JS from main site This should allow communication with the latest version of the Scratch Device Plugin, including the Scratch Device Plugin for Chrome. --- index.html | 3 + scratch_extensions/scratch_ext.js | 254 +++++++++++++++----------- scratch_extensions/scratch_nmh.js | 184 +++++++++++++++++++ scratch_extensions/scratch_plugin.js | 96 ++++++++++ scratch_extensions/scratch_proxies.js | 91 +++++++++ 5 files changed, 524 insertions(+), 104 deletions(-) create mode 100644 scratch_extensions/scratch_nmh.js create mode 100644 scratch_extensions/scratch_plugin.js create mode 100644 scratch_extensions/scratch_proxies.js diff --git a/index.html b/index.html index 3335c08..7e960f7 100644 --- a/index.html +++ b/index.html @@ -285,6 +285,9 @@ + + + diff --git a/scratch_extensions/scratch_ext.js b/scratch_extensions/scratch_ext.js index b9dd563..517e196 100644 --- a/scratch_extensions/scratch_ext.js +++ b/scratch_extensions/scratch_ext.js @@ -4,9 +4,7 @@ // // 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 pluginName = 'Scratch Device Plugin'; // will be 'Scratch Plugin for Devices' - var pluginAvailable = (window.ActiveXObject || !!navigator.plugins[pluginName]) && !!window.ArrayBuffer; +window.ScratchExtensions = new (function () { var plugin = null; var handlers = {}; var blockDefs = {}; @@ -16,27 +14,34 @@ window.ScratchExtensions = new (function(){ var poller = null; var lib = this; - lib.register = function(name, descriptor, handler, deviceSpec) { - if(name in handlers) { - console.log('Scratch extension "'+name+'" already exists!'); + 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; } - if(deviceSpec) { - if(!pluginAvailable && window.ActiveXObject) { + if (deviceSpec && !plugin) { + if (pluginAvailable()) { + 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.'); } - if(pluginAvailable && !plugin) setTimeout(createDevicePlugin, 10); - - // Wait a moment to access the plugin and claim any devices that plugins are - // interested in. - setTimeout(checkPolling, 100); } handlers[name] = handler; blockDefs[name] = descriptor.blocks; - if(descriptor.menus) menuDefs[name] = descriptor.menus; - if(deviceSpec) deviceSpecs[name] = deviceSpec; + if (descriptor.menus) menuDefs[name] = descriptor.menus; + if (deviceSpec) deviceSpecs[name] = deviceSpec; // Show the blocks in Scratch! var extObj = { @@ -51,81 +56,86 @@ window.ScratchExtensions = new (function(){ }; var loadingURL; - lib.loadExternalJS = function(url) { + 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) { + lib.loadLocalJS = function (code) { // Run the extension code in the global scope try { (new Function(code))(); - } catch(e) { + } catch (e) { console.log(e.stack.toString()); } }; - lib.unregister = function(name) { - try { handlers[name]._shutdown(); } catch(e){} + 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) { + 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) { + 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.runCommand = function(ext_name, command, 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() { + 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'}; + 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'}; + 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); + lib.notify = function (text) { + if (window.JSsetProjectBanner) JSsetProjectBanner(text); else alert(text); }; - lib.resetPlugin = function() { + lib.resetPlugin = function () { if (plugin && plugin.reset) plugin.reset(); shutdown(); }; - $(window).unload(function(e) { + $(window).unload(function (e) { shutdown(); }); function shutdown() { - for(var extName in handlers) + for (var extName in handlers) handlers[extName]._shutdown(); handlers = {}; stopPolling(); @@ -133,65 +143,88 @@ window.ScratchExtensions = new (function(){ function checkDevices() { var awaitingSpecs = {}; - for(var ext_name in deviceSpecs) - if(!devices[ext_name]) { + for (var ext_name in deviceSpecs) + if (!devices[ext_name]) { var spec = deviceSpecs[ext_name]; - if(spec.type == 'hid') { - if(!awaitingSpecs['hid']) awaitingSpecs['hid'] = {}; + if (spec.type == 'hid') { + if (!awaitingSpecs['hid']) awaitingSpecs['hid'] = {}; awaitingSpecs['hid'][spec.vendor + '_' + spec.product] = ext_name; } - else if(spec.type == 'serial') + else if (spec.type == 'serial') awaitingSpecs['serial'] = ext_name; } - if(awaitingSpecs['hid']) { - var deviceList = plugin.hid_list(); - 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['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']) { + if (awaitingSpecs['serial']) { var ext_name = awaitingSpecs['serial']; - var deviceList = plugin.serial_list(); - for (var i = 0; i < deviceList.length; i++) { - handlers[ext_name]._deviceConnected(new serialDevice(deviceList[i], ext_name)); - } + 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()) + if (!shouldLookForDevices()) stopPolling(); } function checkPolling() { - if(poller || !shouldLookForDevices()) return; + if (poller || !shouldLookForDevices()) return; poller = setInterval(checkDevices, 500); - } + } function stopPolling() { - if(poller) clearInterval(poller); + if (poller) clearInterval(poller); poller = null; } function shouldLookForDevices() { - for(var ext_name in deviceSpecs) - if(!devices[ext_name]) + for (var ext_name in deviceSpecs) + if (!devices[ext_name]) return true; return false; } function createDevicePlugin() { - if(plugin) return; + if (plugin) return; - var pluginContainer = document.createElement('div'); - document.getElementById('scratch').parentNode.appendChild(pluginContainer); - pluginContainer.innerHTML = ' '; - plugin = pluginContainer.firstChild; + // 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 = ' '; + 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) { @@ -205,44 +238,47 @@ window.ScratchExtensions = new (function(){ this.info = info; function disconnect() { - setTimeout(function(){ + setTimeout(function () { self.close(); handlers[ext_name]._deviceRemoved(self); }, 0); } - this.open = function() { + this.open = function (readyCallback) { try { - dev = plugin.hid_open(this.id); - if(window.ActiveXObject) dev = dev(this.id); - dev.set_nonblocking(true); - //devices[ext_name][path] = this; - devices[ext_name] = this; + plugin.hid_open(this.id, function (d) { + dev = d; + dev.set_nonblocking(true); + //devices[ext_name][path] = this; + devices[ext_name] = this; + + if (readyCallback) readyCallback(this); + }); } - catch(e) {} + catch (e) { } }; - this.close = function() { - if(!dev) return; + this.close = function () { + if (!dev) return; dev.close(); delete devices[ext_name]; dev = null; checkPolling(); }; - this.write = function(data) { - if(!dev) return; - var len = dev.write(data); - if(window.ActiveXObject) len = len(data); - if(len < 0) disconnect(); - return len; + this.write = function (data, callback) { + if (!dev) return; + dev.write(data, function (len) { + if (len < 0) disconnect(); + if (callback) callback(len); + }); }; - this.read = function(len) { - if(!dev) return null; - if(!len) len = 65; - var data = dev.read(len); - if(window.ActiveXObject) data = data(len); - if(data.byteLength == 0) disconnect(); - return data; + 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); + }); }; } @@ -254,32 +290,42 @@ window.ScratchExtensions = new (function(){ //if(!(ext_name in devices)) devices[ext_name] = {}; this.id = id; - this.open = function(opts) { + this.open = function (opts, readyCallback) { try { - dev = plugin.serial_open(this.id, opts); -// dev.set_disconnect_handler(function() { -// self.close(); -// handlers[ext_name]._deviceRemoved(self); -// }); - //devices[ext_name][path] = this; - devices[ext_name] = this; + plugin.serial_open(this.id, opts, function (d) { +// dev.set_disconnect_handler(function () { +// self.close(); +// handlers[ext_name]._deviceRemoved(self); +// }); +// devices[ext_name][path] = this; + dev = d; + devices[ext_name] = this; + + dev.set_error_handler(function (message) { + alert('Serial device error\n\nDevice: ' + id + '\nError: ' + message); + }); + + if (readyCallback) readyCallback(this); + }); + } + catch (e) { + console.log('Error opening serial device ' + id + ': ' + e); } - catch(e) {} }; - this.close = function() { - if(!dev) return; + this.close = function () { + if (!dev) return; dev.close(); delete devices[ext_name]; dev = null; checkPolling(); }; - this.send = function(data) { - if(!dev) return; + this.send = function (data) { + if (!dev) return; dev.send(data); }; - this.set_receive_handler = function(handler) { - if(!dev) return; + this.set_receive_handler = function (handler) { + if (!dev) return; dev.set_receive_handler(handler); }; } diff --git a/scratch_extensions/scratch_nmh.js b/scratch_extensions/scratch_nmh.js new file mode 100644 index 0000000..08067fb --- /dev/null +++ b/scratch_extensions/scratch_nmh.js @@ -0,0 +1,184 @@ +// Communicate with the Scratch Native Messaging Host through an extension. +window.ScratchDeviceHost = new (function () { + var self = this; + var isConnected = false; + + self.isAvailable = function () { + return isConnected; + }; + + if (!window.chrome) return; + + var extensionID = 'clmabinlolakdafkoajkfjjengcdmnpm'; + var callNumber = 0; + var port = chrome.runtime.connect(extensionID); + console.assert(port, "Failed to create port"); + + var messageHandlers = {}; + port.onMessage.addListener(function (message) { + var messageName = message[0]; + if (messageName == "@") { + var callbackToken = message[1]; + var returnValue = message[2]; + var callback = pendingCallbacks[callbackToken]; + delete pendingCallbacks[callbackToken]; + if (callback) callback(returnValue); + } else { + var handler = messageHandlers[messageName]; + if (handler) { + handler(message); + } else { + console.log("SDH-Page: Unrecognized message " + message); + } + } + }); + + messageHandlers["serialRecv"] = function (message) { + var path = message[1]; + var data = message[2]; + + var device = serialDevices[path]; + if (device && device.receiveHandler) { + device.receiveHandler(data); + } + }; + + messageHandlers["serialError"] = function (message) { + var path = message[1]; + var errorMessage = message[2]; + + var device = serialDevices[path]; + if (device && device.errorHandler) { + device.errorHandler(errorMessage); + } + }; + + var pendingCallbacks = {}; + function sendMessage(message, callback) { + var callbackToken = (callNumber++).toString(); + pendingCallbacks[callbackToken] = callback; + port.postMessage([callbackToken, message]); + } + + sendMessage(["version"], function (version) { + isConnected = true; + }); + + self.hid_list = function (callback, opt_vendorID, opt_productID) { + var message = ["hid_list", opt_vendorID || 0, opt_productID || 0]; + sendMessage(message, function (deviceList) { + if (callback) callback(deviceList); + }); + }; + self.hid_open = function (path, callback) { + var message = ["hid_open_raw", path]; + sendMessage(message, function (result) { + var device; + if (result) { + device = new HidDevice(path); + ScratchProxies.AddHidProxies(device); + var claimMessage = ["claim", path]; + sendMessage(claimMessage); + } + if (callback) callback(device); + }); + }; + self.serial_list = function (callback) { + sendMessage(["serial_list"], function (deviceList) { + if (callback) callback(deviceList); + }); + }; + self.serial_open = function (path, opts, callback) { + var message = ["serial_open_raw", path]; + if (opts) message.push(opts); + sendMessage(message, function (result) { + var device; + if (result) { + device = new SerialDevice(path); + ScratchProxies.AddSerialProxies(device); + var claimMessage = ["claim", path]; + sendMessage(claimMessage); + } + if (callback) callback(device); + }); + }; + self.reset = function () { + sendMessage(["reset"]); + }; + self.version = function (callback) { + sendMessage(["version"], function (result) { + if (callback) callback(result); + }); + }; + + function HidDevice(path) { + var self = this; + + self.write_raw = function (arrayBuffer, callback) { + var message = ["write_raw", path, arrayBuffer]; + sendMessage(message, function (result) { + if (callback) callback(result); + }); + }; + self.send_feature_report_raw = function (arrayBuffer, callback) { + var message = ["send_feature_report_raw", path, arrayBuffer]; + sendMessage(message, function (result) { + if (callback) callback(result); + }); + }; + self.read_raw = function (size, callback) { + var message = ["read_raw", path, size]; + sendMessage(message, function (data) { + if (callback) callback(data); + }); + }; + self.get_feature_report_raw = function (size, callback) { + var message = ["get_feature_report_raw", path, size]; + sendMessage(message, function (data) { + if (callback) callback(data); + }); + }; + self.set_nonblocking = function (flag, callback) { + var message = ["set_nonblocking", path, flag]; + sendMessage(message, function (result) { + if (callback) callback(result); + }); + }; + self.close = function () { + sendMessage(["close", path]); + }; + } + + var serialDevices = {}; // path -> SerialDevice + function SerialDevice(path) { + var self = this; + + self.receiveHandler = undefined; + self.errorHandler = undefined; + + serialDevices[path] = self; + + self.send_raw = function (data) { + var message = ["serial_send_raw", path, data]; + sendMessage(message); + }; + self.close = function () { + var message = ["serial_close", path]; + sendMessage(message); + }; + self.is_open = function (callback) { + var message = ["serial_is_open", path]; + sendMessage(message, function (result) { + if (callback) callback(result); + }); + }; + self.set_receive_handler_raw = function (callback) { + self.receiveHandler = callback; + var message = ["serial_recv_start", path]; + sendMessage(message); + }; + self.set_error_handler = function (callback) { + self.errorHandler = callback; + }; + } +})(); diff --git a/scratch_extensions/scratch_plugin.js b/scratch_extensions/scratch_plugin.js new file mode 100644 index 0000000..3912bbf --- /dev/null +++ b/scratch_extensions/scratch_plugin.js @@ -0,0 +1,96 @@ +window.ScratchPlugin = new (function () { + var self = this; + + var pluginName = 'Scratch Device Plugin'; // will be 'Scratch Plugin for Devices' + self.useActiveX = window.hasOwnProperty('ActiveXObject'); + self.axObjectName = 'MITMediaLab.ScratchDevicePlugin'; // name of ActiveX object + self.isAvailable = function () { + return !!(self.useActiveX || navigator.plugins[pluginName]); + }; + + // These wrappers make the plugin act asynchronous, matching the API found in AIR and NMH. + // The one difference is that callbacks are triggered before the initiating call returns. + self.PluginWrapper = function (plugin) { + var self = this; + self.hid_list = function (callback, opt_vendorID, opt_productID) { + var deviceList = plugin.hid_list(opt_vendorID, opt_productID); + if (callback) callback(deviceList); + }; + self.hid_open = function (path, callback) { + var device = plugin.hid_open_raw(path); + if (device) { + device = new HidWrapper(device); + ScratchProxies.AddHidProxies(device); + } + if (callback) callback(device); + }; + self.serial_list = function (callback) { + var deviceList = plugin.serial_list(); + if (callback) callback(deviceList); + }; + self.serial_open = function (path, opts, callback) { + var device = plugin.serial_open_raw(path, opts); + if (device) { + device = new SerialWrapper(device); + ScratchProxies.AddSerialProxies(device); + } + if (callback) callback(device); + }; + self.reset = function () { + plugin.reset(); + }; + self.version = function (callback) { + var result = plugin.version(); + if (callback) callback(result); + }; + }; + + function HidWrapper(device) { + var self = this; + + self.write_raw = function (arrayBuffer, callback) { + var result = device.write_raw(arrayBuffer); + if (callback) callback(result); + }; + self.send_feature_report_raw = function (arrayBuffer, callback) { + var result = device.send_feature_report_raw(arrayBuffer); + if (callback) callback(result); + }; + self.read_raw = function (size, callback) { + var data = device.read_raw(size); + if (callback) callback(data); + }; + self.get_feature_report_raw = function (size, callback) { + var data = device.get_feature_report_raw(size); + if (callback) callback(data); + }; + self.set_nonblocking = function (flag, callback) { + var result = device.set_nonblocking(flag); + if (callback) callback(result); + }; + self.close = function () { + device.close(); + }; + }; + + function SerialWrapper(device) { + var self = this; + + self.send_raw = function (data) { + device.send_raw(data); + }; + self.close = function () { + device.close(); + }; + self.is_open = function (callback) { + var result = device.is_open(); + if (callback) callback(result); + }; + self.set_receive_handler_raw = function (callback) { + device.set_receive_handler_raw(callback); + }; + self.set_error_handler = function (callback) { + device.set_error_handler(callback); + }; + }; +})(); diff --git a/scratch_extensions/scratch_proxies.js b/scratch_extensions/scratch_proxies.js new file mode 100644 index 0000000..a11b782 --- /dev/null +++ b/scratch_extensions/scratch_proxies.js @@ -0,0 +1,91 @@ +// Extend hardware devices with some hardware-API-agnostic data conversion wrappers +window.ScratchProxies = new (function () { + var self = this; + var charsBase64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + + function ab_to_b64(arraybuffer) { + var bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = ''; + + for (i = 0; i < len; i += 3) { + base64 += charsBase64[bytes[i] >> 2]; + base64 += charsBase64[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64 += charsBase64[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += charsBase64[bytes[i + 2] & 63]; + } + + if ((len % 3) === 2) { + base64 = base64.substring(0, base64.length - 1) + '='; + } else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2) + '=='; + } + + return base64; + } + + function b64_to_ab(base64) { + var bufferLength = base64.length * 0.75, len = base64.length, i, p = 0, encoded1, encoded2, encoded3, encoded4; + if (base64[base64.length - 1] === '=') { + --bufferLength; + if (base64[base64.length - 2] === '=') { + --bufferLength; + } + } + + var arraybuffer = new ArrayBuffer(bufferLength), bytes = new Uint8Array(arraybuffer); + for (i = 0; i < len; i += 4) { + encoded1 = charsBase64.indexOf(base64[i]); + encoded2 = charsBase64.indexOf(base64[i + 1]); + encoded3 = charsBase64.indexOf(base64[i + 2]); + encoded4 = charsBase64.indexOf(base64[i + 3]); + bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); + bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); + bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); + } + + return arraybuffer; + } + + self.AddHidProxies = function (device) { + device.write = function (arrayBuffer, callback) { + var bufferBase64 = ab_to_b64(arrayBuffer); + device.write_raw(bufferBase64, callback); + }; + device.send_feature_report = function (arrayBuffer, callback) { + var bufferBase64 = ab_to_b64(arrayBuffer); + device.send_feature_report_raw(bufferBase64, callback); + }; + device.read = function (size, callback) { + device.read_raw(size, function (data) { + if (callback) { + data = b64_to_ab(data); + callback(data); + } + }); + }; + device.get_feature_report = function (size, callback) { + device.get_feature_report_raw(size, function (data) { + if (callback) { + data = b64_to_ab(data); + callback(data); + } + }); + }; + }; + + self.AddSerialProxies = function (device) { + device.send = function (arrayBuffer, callback) { + var bufferBase64 = ab_to_b64(arrayBuffer); + device.send_raw(bufferBase64, function (result) { + if (callback) callback(result); + }); + }; + device.set_receive_handler = function (callback) { + device.set_receive_handler_raw(function (data) { + if (callback) { + data = b64_to_ab(data); + callback(data); + } + }); + }; + }; +})();