From ac7a4358d6323e769eae034a7b9a49db3f6ef9ab Mon Sep 17 00:00:00 2001 From: Scott Lahteine Date: Thu, 5 Feb 2015 11:14:31 -0800 Subject: [PATCH] Add a responsive file uploader - Specify HTML5 - Allow drag and drop for loading configurations --- Marlin/configurator/css/configurator.css | 2 +- Marlin/configurator/index.html | 13 +- Marlin/configurator/js/binaryfileuploader.js | 79 +++++++++ Marlin/configurator/js/binarystring.js | 168 +++++++++++++++++++ Marlin/configurator/js/configurator.js | 128 +++++++++++--- 5 files changed, 359 insertions(+), 31 deletions(-) create mode 100644 Marlin/configurator/js/binaryfileuploader.js create mode 100644 Marlin/configurator/js/binarystring.js diff --git a/Marlin/configurator/css/configurator.css b/Marlin/configurator/css/configurator.css index 9c7b1be0a..993517415 100644 --- a/Marlin/configurator/css/configurator.css +++ b/Marlin/configurator/css/configurator.css @@ -15,7 +15,7 @@ label, input, select, textarea { display: block; float: left; margin: 1px 0; } label.newline, textarea { clear: both; } label { width: 130px; height: 1em; padding: 10px 480px 10px 1em; margin-right: -470px; text-align: right; } input[type="text"], select, .jstepper { margin: 0.75em 0 0; } -input[type="checkbox"], input[type="radio"] { margin: 1em 0 0; } +input[type="checkbox"], input[type="radio"], input[type="file"] { margin: 1em 0 0; } #config_form { display: block; background: #DDD; padding: 20px; color: #000; } /*#config_text, #config_adv_text { font-family: "Andale mono", monospace; clear: both; }*/ #config_text, #config_adv_text { height: 25em; overflow: auto; background-color: #FFF; color: #888; padding: 10px; } diff --git a/Marlin/configurator/index.html b/Marlin/configurator/index.html index aad22492a..06ab26d56 100644 --- a/Marlin/configurator/index.html +++ b/Marlin/configurator/index.html @@ -1,15 +1,18 @@ + - Marlin Configurator + Marlin Configurator + + -
+

Marlin Configurator 0.1a

Enter values in the form, get a Marlin configuration.
Will include a drop-down of known configurations.

    @@ -18,9 +21,9 @@
    - + -
    +
    @@ -52,6 +55,6 @@
    -
+
diff --git a/Marlin/configurator/js/binaryfileuploader.js b/Marlin/configurator/js/binaryfileuploader.js new file mode 100644 index 000000000..0a1f38f3d --- /dev/null +++ b/Marlin/configurator/js/binaryfileuploader.js @@ -0,0 +1,79 @@ +function BinaryFileUploader(o) { + this.options = null; + + + this._defaultOptions = { + element: null, // HTML file element + onFileLoad: function(file) { + console.log(file.toString()); + } + }; + + + this._init = function(o) { + if (!this.hasFileUploaderSupport()) return; + + this._verifyDependencies(); + + this.options = this._mergeObjects(this._defaultOptions, o); + this._verifyOptions(); + + this.addFileChangeListener(); + } + + + this.hasFileUploaderSupport = function() { + return !!(window.File && window.FileReader && window.FileList && window.Blob); + } + + this.addFileChangeListener = function() { + this.options.element.addEventListener( + 'change', + this._bind(this, this.onFileChange) + ); + } + + this.onFileChange = function(e) { + // TODO accept multiple files + var file = e.target.files[0], + reader = new FileReader(); + + reader.onload = this._bind(this, this.onFileLoad); + reader.readAsBinaryString(file); + } + + this.onFileLoad = function(e) { + var content = e.target.result, + string = new BinaryString(content); + this.options.onFileLoad(string); + } + + + this._mergeObjects = function(starting, override) { + var merged = starting; + for (key in override) merged[key] = override[key]; + + return merged; + } + + this._verifyOptions = function() { + if (!(this.options.element && this.options.element.type && this.options.element.type === 'file')) { + throw 'Invalid element param in options. Must be a file upload DOM element'; + } + + if (typeof this.options.onFileLoad !== 'function') { + throw 'Invalid onFileLoad param in options. Must be a function'; + } + } + + this._verifyDependencies = function() { + if (!window.BinaryString) throw 'BinaryString is missing. Check that you\'ve correctly included it'; + } + + // helper function for binding methods to objects + this._bind = function(object, method) { + return function() {return method.apply(object, arguments);}; + } + + this._init(o); +} diff --git a/Marlin/configurator/js/binarystring.js b/Marlin/configurator/js/binarystring.js new file mode 100644 index 000000000..06af64fe2 --- /dev/null +++ b/Marlin/configurator/js/binarystring.js @@ -0,0 +1,168 @@ +function BinaryString(source) { + this._source = null; + this._bytes = []; + this._pos = 0; + this._length = 0; + + this._init = function(source) { + this._source = source; + this._bytes = this._stringToBytes(this._source); + this._length = this._bytes.length; + } + + this.current = function() {return this._pos;} + + this.rewind = function() {return this.jump(0);} + this.end = function() {return this.jump(this.length() - 1);} + this.next = function() {return this.jump(this.current() + 1);} + this.prev = function() {return this.jump(this.current() - 1);} + + this.jump = function(pos) { + if (pos < 0 || pos >= this.length()) return false; + + this._pos = pos; + return true; + } + + this.readByte = function(pos) { + pos = (typeof pos == 'number') ? pos : this.current(); + return this.readBytes(1, pos)[0]; + } + + this.readBytes = function(length, pos) { + length = length || 1; + pos = (typeof pos == 'number') ? pos : this.current(); + + if (pos > this.length() || + pos < 0 || + length <= 0 || + pos + length > this.length() || + pos + length < 0 + ) { + return false; + } + + var bytes = []; + + for (var i = pos; i < pos + length; i++) { + bytes.push(this._bytes[i]); + } + + return bytes; + } + + this.length = function() {return this._length;} + + this.toString = function() { + var string = '', + length = this.length(); + + for (var i = 0; i < length; i++) { + string += String.fromCharCode(this.readByte(i)); + } + + return string; + } + + this.toUtf8 = function() { + var inc = 0, + string = '', + length = this.length(); + + // determine if first 3 characters are the BOM + // then skip them in output if so + if (length >= 3 && + this.readByte(0) === 0xEF && + this.readByte(1) === 0xBB && + this.readByte(2) === 0xBF + ) { + inc = 3; + } + + for (; inc < length; inc++) { + var byte1 = this.readByte(inc), + byte2 = 0, + byte3 = 0, + byte4 = 0, + code1 = 0, + code2 = 0, + point = 0; + + switch (true) { + // single byte character; same as ascii + case (byte1 < 0x80): + code1 = byte1; + break; + + // 2 byte character + case (byte1 >= 0xC2 && byte1 < 0xE0): + byte2 = this.readByte(++inc); + + code1 = ((byte1 & 0x1F) << 6) + + (byte2 & 0x3F); + break; + + // 3 byte character + case (byte1 >= 0xE0 && byte1 < 0xF0): + byte2 = this.readByte(++inc); + byte3 = this.readByte(++inc); + + code1 = ((byte1 & 0xFF) << 12) + + ((byte2 & 0x3F) << 6) + + (byte3 & 0x3F); + break; + + // 4 byte character + case (byte1 >= 0xF0 && byte1 < 0xF5): + byte2 = this.readByte(++inc); + byte3 = this.readByte(++inc); + byte4 = this.readByte(++inc); + + point = ((byte1 & 0x07) << 18) + + ((byte2 & 0x3F) << 12) + + ((byte3 & 0x3F) << 6) + + (byte4 & 0x3F) + point -= 0x10000; + + code1 = (point >> 10) + 0xD800; + code2 = (point & 0x3FF) + 0xDC00; + break; + + default: + throw 'Invalid byte ' + this._byteToString(byte1) + ' whilst converting to UTF-8'; + break; + } + + string += (code2) ? String.fromCharCode(code1, code2) + : String.fromCharCode(code1); + } + + return string; + } + + this.toArray = function() {return this.readBytes(this.length() - 1, 0);} + + + this._stringToBytes = function(str) { + var bytes = [], + chr = 0; + + for (var i = 0; i < str.length; i++) { + chr = str.charCodeAt(i); + bytes.push(chr & 0xFF); + } + + return bytes; + } + + this._byteToString = function(byte) { + var asString = byte.toString(16).toUpperCase(); + while (asString.length < 2) { + asString = '0' + asString; + } + + return '0x' + asString; + } + + this._init(source); +} diff --git a/Marlin/configurator/js/configurator.js b/Marlin/configurator/js/configurator.js index ebb80bc59..4764c6ebe 100644 --- a/Marlin/configurator/js/configurator.js +++ b/Marlin/configurator/js/configurator.js @@ -55,6 +55,9 @@ var configuratorApp = (function(){ // private variables and functions go here var self, pi2 = Math.PI * 2, + boards_file = 'boards.h', + config_file = 'Configuration.h', + config_adv_file = 'Configuration_adv.h', $config = $('#config_text'), $config_adv = $('#config_adv_text'), boards_list = {}, @@ -67,37 +70,106 @@ var configuratorApp = (function(){ init: function() { self = this; // a 'this' for use when 'this' is something else + // Make a droppable file uploader + var $uploader = $('#file-upload'); + var fileUploader = new BinaryFileUploader({ + element: $uploader[0], + onFileLoad: function(file) { console.log(this); self.handleFileLoad(file, $uploader); } + }); + + if (!fileUploader.hasFileUploaderSupport()) alert('Your browser doesn\'t support the file reading API'); + // Read boards.h boards_list = {}; - $.get(marlin_config + "/boards.h", function(txt) { - // Get all the boards and save them into an object - var r, findDef = new RegExp('[ \\t]*#define[ \\t]+(BOARD_[^ \\t]+)[ \\t]+(\\d+)[ \\t]*(//[ \\t]*)?(.+)?', 'gm'); - while((r = findDef.exec(txt)) !== null) { - boards_list[r[1]] = r[2].prePad(3, '  ') + " — " + r[4].replace(/\).*/, ')'); - } + + var errFunc = function(jqXHR, textStatus, errorThrown) { + alert('Failed to load '+this.url+'. Try the file field.'); + }; + + $.ajax({ + url: marlin_config+'/'+boards_file, + type: 'GET', + async: false, + cache: false, + success: function(txt) { + // Get all the boards and save them into an object + self.initBoardsFromText(txt); + }, + error: errFunc }); // Read Configuration.h - $.get(marlin_config+"/Configuration.h", function(txt) { - // File contents into the textarea - $config.text(txt); - // Get all the thermistors and save them into an object - var r, s, findDef = new RegExp('(//.*\n)+\\s+(#define[ \\t]+TEMP_SENSOR_0)', 'g'); - r = findDef.exec(txt); - findDef = new RegExp('^//[ \\t]*([-\\d]+)[ \\t]+is[ \\t]+(.*)[ \\t]*$', 'gm'); - while((s = findDef.exec(r[0])) !== null) { - therms_list[s[1]] = s[1].prePad(4, '  ') + " — " + s[2]; - } + $.ajax({ + url: marlin_config+'/'+config_file, + type: 'GET', + async: false, + cache: false, + success: function(txt) { + // File contents into the textarea + $config.text(txt); + self.initThermistorsFromText(txt); + }, + error: errFunc }); // Read Configuration.h - $.get(marlin_config+"/Configuration_adv.h", function(txt) { - $config_adv.text(txt); - self.setupConfigForm(); + $.ajax({ + url: marlin_config+'/'+config_adv_file, + type: 'GET', + async: false, + cache: false, + success: function(txt) { + // File contents into the textarea + $config_adv.text(txt); + self.setupConfigForm(); + }, + error: errFunc }); }, + initBoardsFromText: function(txt) { + boards_list = {}; + var r, findDef = new RegExp('[ \\t]*#define[ \\t]+(BOARD_[^ \\t]+)[ \\t]+(\\d+)[ \\t]*(//[ \\t]*)?(.+)?', 'gm'); + while((r = findDef.exec(txt)) !== null) { + boards_list[r[1]] = r[2].prePad(3, '  ') + " — " + r[4].replace(/\).*/, ')'); + } + }, + + initThermistorsFromText: function(txt) { + // Get all the thermistors and save them into an object + var r, s, findDef = new RegExp('(//.*\n)+\\s+(#define[ \\t]+TEMP_SENSOR_0)', 'g'); + r = findDef.exec(txt); + findDef = new RegExp('^//[ \\t]*([-\\d]+)[ \\t]+is[ \\t]+(.*)[ \\t]*$', 'gm'); + while((s = findDef.exec(r[0])) !== null) { + therms_list[s[1]] = s[1].prePad(4, '  ') + " — " + s[2]; + } + }, + + handleFileLoad: function(file, $uploader) { + file += ''; + var filename = $uploader.val().replace(/.*[\/\\](.*)$/, '$1'); + switch(filename) { + case config_file: + $config.text(file); + this.initThermistorsFromText(file); + this.refreshConfigForm(); + break; + case config_adv_file: + $config_adv.text(file); + this.refreshConfigForm(); + break; + case boards_file: + this.initBoardsFromText(file); + $('#MOTHERBOARD').html('').addOptions(boards_list); + this.initField('MOTHERBOARD'); + break; + default: + console.log("Can't parse "+filename); + break; + } + }, + setupConfigForm: function() { // Modify form fields and make the form responsive. // As values change on the form, we could update the @@ -128,6 +200,17 @@ var configuratorApp = (function(){ ); }); + $('#SERIAL_PORT').addOptions([0,1,2,3,4,5,6,7]); + $('#BAUDRATE').addOptions([2400,9600,19200,38400,57600,115200,250000]); + $('#MOTHERBOARD').addOptions(boards_list); + $('#EXTRUDERS').addOptions([1,2,3,4]); + $('#POWER_SUPPLY').addOptions({'1':'ATX','2':'Xbox 360'}); + + this.refreshConfigForm(); + }, + + refreshConfigForm: function() { + /** * For now I'm manually creating these references * but I should be able to parse Configuration.h @@ -140,30 +223,25 @@ var configuratorApp = (function(){ * Then we only need to specify exceptions to * standard behavior, (which is to add a text field) */ - $('#SERIAL_PORT').addOptions([0,1,2,3,4,5,6,7]); this.initField('SERIAL_PORT'); - $('#BAUDRATE').addOptions([2400,9600,19200,38400,57600,115200,250000]); this.initField('BAUDRATE'); this.initField('BTENABLED'); - $('#MOTHERBOARD').addOptions(boards_list); this.initField('MOTHERBOARD'); this.initField('CUSTOM_MENDEL_NAME'); this.initField('MACHINE_UUID'); - $('#EXTRUDERS').addOptions([1,2,3,4]); this.initField('EXTRUDERS'); - $('#POWER_SUPPLY').addOptions({'1':'ATX','2':'Xbox 360'}); this.initField('POWER_SUPPLY'); this.initField('PS_DEFAULT_OFF'); - $('#TEMP_SENSOR_0, #TEMP_SENSOR_1, #TEMP_SENSOR_2, #TEMP_SENSOR_BED').addOptions(therms_list); + $('#TEMP_SENSOR_0, #TEMP_SENSOR_1, #TEMP_SENSOR_2, #TEMP_SENSOR_BED').html('').addOptions(therms_list); this.initField('TEMP_SENSOR_0'); this.initField('TEMP_SENSOR_1'); this.initField('TEMP_SENSOR_2');