diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 7275ff4eab..969a1a0adb 100755
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -102,7 +102,9 @@
"tabLogging": {
"message": "Logging"
},
-
+ "tabDataflash": {
+ "message": "Dataflash"
+ },
"tabAdjustments": {
"message": "Adjustments"
},
@@ -846,6 +848,45 @@
"message": "Automatically loaded previous log file: $1"
},
+ "dataflashNote": {
+ "message": "Blackbox flight logs can be recorded to your flight controller's onboard dataflash chip."
+ },
+ "dataflashNotSupportedNote": {
+ "message": "Your flight controller does not have a compatible dataflash chip available."
+ },
+ "dataflashButtonSaveFile": {
+ "message": "Save flash to file..."
+ },
+ "dataflashButtonErase": {
+ "message": "Erase flash"
+ },
+ "dataflashConfirmEraseTitle": {
+ "message": "Confirm dataflash erase"
+ },
+ "dataflashConfirmEraseNote": {
+ "message": "This will erase any Blackbox logs or other data contained in the dataflash which will take about 20 seconds, are you sure?"
+ },
+ "dataflashSavingTitle": {
+ "message": "Saving dataflash to file"
+ },
+ "dataflashSavingNote": {
+ "message": "Saving could take several minutes, please wait."
+ },
+ "dataflashSavingNoteAfter": {
+ "message": "Save completed! Press \"Ok\" to continue."
+ },
+ "dataflashButtonSaveCancel": {
+ "message": "Cancel"
+ },
+ "dataflashButtonSaveDismiss": {
+ "message": "Ok"
+ },
+ "dataflashButtonEraseConfirm": {
+ "message": "Yes, erase dataflash"
+ },
+ "dataflashButtonEraseCancel": {
+ "message": "Cancel"
+ },
"firmwareFlasherReleaseSummaryHead": {
"message": "Release info"
},
diff --git a/js/data_storage.js b/js/data_storage.js
index 31be24de8f..343a96583f 100755
--- a/js/data_storage.js
+++ b/js/data_storage.js
@@ -149,3 +149,10 @@ var MISC = {
vbatmaxcellvoltage: 0,
vbatwarningcellvoltage: 0
};
+
+var DATAFLASH = {
+ ready: false,
+ sectors: 0,
+ totalSize: 0,
+ usedSize: 0
+};
diff --git a/js/gui.js b/js/gui.js
index f6007a9b58..89fab7d761 100644
--- a/js/gui.js
+++ b/js/gui.js
@@ -25,6 +25,7 @@ var GUI_control = function () {
'gps',
'led_strip',
'logging',
+ 'dataflash',
'modes',
'motors',
'pid_tuning',
diff --git a/js/msp.js b/js/msp.js
index d772710e31..1fea8bb3e8 100644
--- a/js/msp.js
+++ b/js/msp.js
@@ -22,6 +22,9 @@ var MSP_codes = {
MSP_SONAR: 58,
MSP_PID_CONTROLLER: 59,
MSP_SET_PID_CONTROLLER: 60,
+ MSP_DATAFLASH_SUMMARY: 70,
+ MSP_DATAFLASH_READ: 71,
+ MSP_DATAFLASH_ERASE: 72,
// Multiwii MSP commands
MSP_IDENT: 100,
@@ -671,8 +674,26 @@ var MSP = {
case MSP_codes.MSP_SET_LED_STRIP_CONFIG:
console.log('Led strip config saved');
break;
-
-
+ case MSP_codes.MSP_DATAFLASH_SUMMARY:
+ if (data.byteLength >= 13) {
+ DATAFLASH.ready = (data.getUint8(0) & 1) != 0;
+ DATAFLASH.sectors = data.getUint32(1, 1);
+ DATAFLASH.totalSize = data.getUint32(5, 1);
+ DATAFLASH.usedSize = data.getUint32(9, 1);
+ } else {
+ // Firmware version too old to support MSP_DATAFLASH_SUMMARY
+ DATAFLASH.ready = false;
+ DATAFLASH.sectors = 0;
+ DATAFLASH.totalSize = 0;
+ DATAFLASH.usedSize = 0;
+ }
+ break;
+ case MSP_codes.MSP_DATAFLASH_READ:
+ // No-op, let callback handle it
+ break;
+ case MSP_codes.MSP_DATAFLASH_ERASE:
+ console.log("Data flash erase begun...");
+ break;
case MSP_codes.MSP_SET_MODE_RANGE:
console.log('Mode range saved');
break;
@@ -798,6 +819,9 @@ var MSP = {
}
};
+/**
+ * Encode the request body for the MSP request with the given code and return it as an array of bytes.
+ */
MSP.crunch = function (code) {
var buffer = [];
@@ -959,6 +983,27 @@ MSP.crunch = function (code) {
return buffer;
};
+/**
+ * Send a request to read a block of data from the dataflash at the given address and pass that address and a dataview
+ * of the returned data to the given callback (or null for the data if an error occured).
+ */
+MSP.dataflashRead = function(address, onDataCallback) {
+ MSP.send_message(MSP_codes.MSP_DATAFLASH_READ, [address & 0xFF, (address >> 8) & 0xFF, (address >> 16) & 0xFF, (address >> 24) & 0xFF],
+ false, function(response) {
+ var chunkAddress = response.data.getUint32(0, 1);
+
+ // Verify that the address of the memory returned matches what the caller asked for
+ if (chunkAddress == address) {
+ /* Strip that address off the front of the reply and deliver it separately so the caller doesn't have to
+ * figure out the reply format:
+ */
+ onDataCallback(address, new DataView(response.data.buffer, response.data.byteOffset + 4, response.data.buffer.byteLength - 4));
+ } else {
+ // Report error
+ onDataCallback(address, null);
+ }
+ });
+} ;
MSP.sendModeRanges = function(onCompleteCallback) {
var nextFunction = send_next_mode_range;
diff --git a/main.css b/main.css
index cc8918d537..c066167726 100755
--- a/main.css
+++ b/main.css
@@ -353,3 +353,14 @@ input[type="number"]::-webkit-inner-spin-button {
text-align: center;
font-weight: bold;
}
+
+dialog {
+ background-color: white;
+ padding: 1em;
+ height: auto;
+ margin: auto auto;
+ position: absolute;
+ width: 50%;
+ border-radius: 5px;
+ border: 1px solid silver;
+}
\ No newline at end of file
diff --git a/main.html b/main.html
index 3ffd777881..432ef9c89e 100755
--- a/main.html
+++ b/main.html
@@ -23,6 +23,7 @@
+
@@ -70,6 +71,7 @@
+
@@ -148,6 +150,7 @@
+
diff --git a/main.js b/main.js
index 374f4cdc24..fa3d54b656 100755
--- a/main.js
+++ b/main.js
@@ -153,6 +153,9 @@ $(document).ready(function () {
case 'logging':
TABS.logging.initialize(content_ready);
break;
+ case 'dataflash':
+ TABS.dataflash.initialize(content_ready);
+ break;
case 'cli':
TABS.cli.initialize(content_ready);
break;
diff --git a/resources/motor_order/airplane.svg b/resources/motor_order/airplane.svg
index 7a6a596aee..a97ea0f741 100644
--- a/resources/motor_order/airplane.svg
+++ b/resources/motor_order/airplane.svg
@@ -1,73 +1,73 @@
-
-
-
+
+
+
diff --git a/resources/motor_order/atail_quad.svg b/resources/motor_order/atail_quad.svg
index 74641eb83e..63b93848c7 100644
--- a/resources/motor_order/atail_quad.svg
+++ b/resources/motor_order/atail_quad.svg
@@ -1,44 +1,44 @@
-
-
-
+
+
+
diff --git a/resources/motor_order/bicopter.svg b/resources/motor_order/bicopter.svg
index 95b2ca75f6..6d866dc7d2 100644
--- a/resources/motor_order/bicopter.svg
+++ b/resources/motor_order/bicopter.svg
@@ -1,52 +1,52 @@
-
-
-
+
+
+
diff --git a/resources/motor_order/flying_wing.svg b/resources/motor_order/flying_wing.svg
index 638f120d9a..4636539898 100644
--- a/resources/motor_order/flying_wing.svg
+++ b/resources/motor_order/flying_wing.svg
@@ -1,55 +1,55 @@
-
-
-
+
+
+
diff --git a/resources/motor_order/hex_p.svg b/resources/motor_order/hex_p.svg
index 03830b8dc6..eab82b7c83 100644
--- a/resources/motor_order/hex_p.svg
+++ b/resources/motor_order/hex_p.svg
@@ -1,58 +1,58 @@
-
-
-
+
+
+
diff --git a/resources/motor_order/hex_x.svg b/resources/motor_order/hex_x.svg
index ae28e35732..4e3a02d782 100644
--- a/resources/motor_order/hex_x.svg
+++ b/resources/motor_order/hex_x.svg
@@ -1,58 +1,58 @@
-
-
-
+
+
+
diff --git a/resources/motor_order/octo_flat_p.svg b/resources/motor_order/octo_flat_p.svg
index 7dc6460ecd..5b880729c6 100644
--- a/resources/motor_order/octo_flat_p.svg
+++ b/resources/motor_order/octo_flat_p.svg
@@ -1,72 +1,72 @@
-
-
-
+
+
+
diff --git a/resources/motor_order/octo_flat_x.svg b/resources/motor_order/octo_flat_x.svg
index 4031ab796d..9493a4b284 100644
--- a/resources/motor_order/octo_flat_x.svg
+++ b/resources/motor_order/octo_flat_x.svg
@@ -1,72 +1,72 @@
-
-
-
+
+
+
diff --git a/resources/motor_order/octo_x8.svg b/resources/motor_order/octo_x8.svg
index 236b3cf269..8e28d09573 100644
--- a/resources/motor_order/octo_x8.svg
+++ b/resources/motor_order/octo_x8.svg
@@ -1,76 +1,76 @@
-
-
-
+
+
+
diff --git a/resources/motor_order/quad_p.svg b/resources/motor_order/quad_p.svg
index 7424e1b359..25609e84ec 100644
--- a/resources/motor_order/quad_p.svg
+++ b/resources/motor_order/quad_p.svg
@@ -1,44 +1,44 @@
-
-
-
+
+
+
diff --git a/resources/motor_order/quad_x.svg b/resources/motor_order/quad_x.svg
index de045e366d..7ae53a0bdc 100644
--- a/resources/motor_order/quad_x.svg
+++ b/resources/motor_order/quad_x.svg
@@ -1,44 +1,44 @@
-
-
-
+
+
+
diff --git a/resources/motor_order/tri.svg b/resources/motor_order/tri.svg
index 8f30f934a5..5ff940c8f7 100644
--- a/resources/motor_order/tri.svg
+++ b/resources/motor_order/tri.svg
@@ -1,51 +1,51 @@
-
-
-
+
+
+
diff --git a/resources/motor_order/vtail_quad.svg b/resources/motor_order/vtail_quad.svg
index 654fa68695..6ec31ef664 100644
--- a/resources/motor_order/vtail_quad.svg
+++ b/resources/motor_order/vtail_quad.svg
@@ -1,44 +1,44 @@
-
-
-
+
+
+
diff --git a/resources/motor_order/y4.svg b/resources/motor_order/y4.svg
index 6edfcfa2ca..5f7a539fa2 100644
--- a/resources/motor_order/y4.svg
+++ b/resources/motor_order/y4.svg
@@ -1,45 +1,45 @@
-
-
-
+
+
+
diff --git a/resources/motor_order/y6.svg b/resources/motor_order/y6.svg
index ef1ee8013d..b830dd46b4 100644
--- a/resources/motor_order/y6.svg
+++ b/resources/motor_order/y6.svg
@@ -1,61 +1,61 @@
-
-
-
+
+
+
diff --git a/tabs/dataflash.css b/tabs/dataflash.css
new file mode 100644
index 0000000000..a6885d53a8
--- /dev/null
+++ b/tabs/dataflash.css
@@ -0,0 +1,192 @@
+.tab-dataflash .info {
+ margin: 0 0 10px 0;
+ position: relative;
+}
+.tab-dataflash .info .progressLabel {
+ position: absolute;
+
+ width: 100%;
+ height: 26px;
+
+ top: 0;
+ left: 0;
+
+ text-align: center;
+ line-height: 24px;
+
+ color: white;
+ font-weight: bold;
+
+ /* text-shadow: 1px 0px 2px rgba(0, 0, 0, 0.9);*/
+}
+.tab-dataflash .note {
+ padding: 5px;
+ border: 1px dashed silver;
+ margin-bottom: 8px;
+}
+.tab-dataflash .properties {
+ margin-top: 10px;
+}
+.tab-dataflash .dataflash-info {
+ overflow:hidden;
+}
+.tab-dataflash .dataflash-info dt {
+ float: left;
+ width: 12em;
+ height: 20px;
+ line-height: 20px;
+
+ font-weight: bold;
+}
+.tab-dataflash .dataflash-info dd {
+ display: block;
+ height: 20px;
+ line-height: 20px;
+}
+.tab-dataflash .speed {
+ margin-top: 5px;
+ width: 80px;
+
+ border: 1px solid silver;
+}
+.tab-dataflash .info {
+ margin-top: 10px;
+}
+.tab-dataflash .info dt {
+ float: left;
+ width: 120px;
+ height: 20px;
+ line-height: 20px;
+
+ font-weight: bold;
+}
+.tab-dataflash .info dd {
+ display: block;
+ margin-left: 130px;
+ height: 20px;
+ line-height: 20px;
+}
+.tab-dataflash .buttons {
+ width: calc(100% - 20px);
+
+ position: absolute;
+ bottom: 10px;
+}
+.tab-dataflash .buttons a {
+ display: block;
+ float: right;
+
+ margin-left: 10px;
+
+ height: 28px;
+ line-height: 28px;
+
+ padding: 0 15px 0 15px;
+
+ text-align: center;
+ font-weight: bold;
+
+ border: 1px solid silver;
+ background-color: #ececec;
+}
+.tab-dataflash .buttons a:hover {
+ background-color: #dedcdc;
+}
+.tab-dataflash .buttons a.disabled {
+ cursor: default;
+ color: #999;
+ pointer-events: none;
+}
+.tab-dataflash .dataflash-progress {
+ display: none;
+}
+.tab-dataflash .dataflash-contents {
+ margin:9px 16px;
+
+ border:1px solid silver;
+ background-color:#eee;
+
+ display:flex;
+ flex-direction:row;
+ flex-wrap:nowrap;
+ justify-content:flex-start;
+
+ border-radius:6px;
+}
+.tab-dataflash .dataflash-contents li {
+ height:26px;
+ position:relative;
+}
+.tab-dataflash .dataflash-contents li div {
+ position:absolute;
+ top:26px;
+ margin-top:4px;
+ text-align:center;
+ left: 0;
+ right: 0;
+}
+.tab-dataflash .dataflash-used {
+ background-color:#bcf;
+}
+.tab-dataflash progress::-webkit-progress-bar {
+ height:24px;
+ background-color:#eee;
+}
+.tab-dataflash progress::-webkit-progress-value {
+ background-color:#bcf;
+}
+
+.tab-dataflash dialog {
+ width:40em;
+}
+.tab-dataflash dialog .buttons {
+ position:static;
+ margin-top: 2em;
+ overflow: hidden;
+ width:auto;
+}
+.tab-dataflash dialog h3 {
+ margin-bottom: 0.5em;
+}
+
+.dataflash-confirm-erase .dataflash-erase-progress {
+ height:125px;
+ display:none;
+}
+.dataflash-confirm-erase.erasing .dataflash-erase-progress {
+ display:block;
+}
+.dataflash-confirm-erase.erasing h3,
+.dataflash-confirm-erase.erasing .erase-flash-confirm,
+.dataflash-confirm-erase.erasing .dataflash-confirm-erase-note {
+ display:none;
+}
+
+.tab-dataflash progress {
+ display:block;
+ width:100%;
+ margin:1em 0;
+}
+
+.dataflash-saving .dataflash-saving-after {
+ display:none;
+}
+.dataflash-saving.done .dataflash-saving-before {
+ display:none;
+}
+.dataflash-saving.done .dataflash-saving-after {
+ display:block;
+}
+
+.require-dataflash {
+ display:none;
+}
+.tab-dataflash.supported .require-dataflash {
+ display:block;
+}
+.require-no-dataflash {
+ display:block;
+}
+.tab-dataflash.supported .require-no-dataflash {
+ display:none;
+}
\ No newline at end of file
diff --git a/tabs/dataflash.html b/tabs/dataflash.html
new file mode 100644
index 0000000000..a3a391a471
--- /dev/null
+++ b/tabs/dataflash.html
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
Dataflash contents
+
+ -
+
+ Used space
+
+
+ -
+
+ Free space
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tabs/dataflash.js b/tabs/dataflash.js
new file mode 100644
index 0000000000..90980c5c7e
--- /dev/null
+++ b/tabs/dataflash.js
@@ -0,0 +1,240 @@
+'use strict';
+
+TABS.dataflash = {};
+TABS.dataflash.initialize = function (callback) {
+ var
+ self = this,
+ saveCancelled, eraseCancelled;
+
+ if (GUI.active_tab != 'dataflash') {
+ GUI.active_tab = 'dataflash';
+ googleAnalytics.sendAppView('dataflash');
+ }
+
+ var
+ requested_properties = [],
+ samples = 0,
+ requests = 0,
+ log_buffer = [];
+
+ if (CONFIGURATOR.connectionValid) {
+ MSP.send_message(MSP_codes.MSP_DATAFLASH_SUMMARY, false, false, function() {
+ $('#content').load("./tabs/dataflash.html", function() {
+ create_html();
+ });
+ });
+ }
+
+ function formatFilesize(bytes) {
+ if (bytes < 1024) {
+ return bytes + "B";
+ }
+
+ var kilobytes = bytes / 1024;
+
+ if (kilobytes < 1024) {
+ return Math.round(kilobytes) + "kB";
+ }
+
+ var megabytes = kilobytes / 1024;
+
+ return megabytes.toFixed(1) + "MB";
+ }
+
+ function update_html() {
+ if (DATAFLASH.usedSize > 0) {
+ $(".tab-dataflash .dataflash-used").css({
+ width: (DATAFLASH.usedSize / DATAFLASH.totalSize * 100) + "%",
+ display: 'block'
+ });
+
+ $(".tab-dataflash .dataflash-used div").text('Used space ' + formatFilesize(DATAFLASH.usedSize));
+ } else {
+ $(".tab-dataflash .dataflash-used").css({
+ display: 'none'
+ });
+ }
+
+ if (DATAFLASH.totalSize - DATAFLASH.usedSize > 0) {
+ $(".tab-dataflash .dataflash-free").css({
+ width: ((DATAFLASH.totalSize - DATAFLASH.usedSize) / DATAFLASH.totalSize * 100) + "%",
+ display: 'block'
+ });
+ $(".tab-dataflash .dataflash-free div").text('Free space ' + formatFilesize(DATAFLASH.totalSize - DATAFLASH.usedSize));
+ } else {
+ $(".tab-dataflash .dataflash-free").css({
+ display: 'none'
+ });
+ }
+
+ $(".tab-dataflash a.erase-flash, .tab-dataflash a.save-flash").toggleClass("disabled", DATAFLASH.usedSize == 0);
+ }
+
+ function create_html() {
+ var
+ supportsDataflash = DATAFLASH.totalSize > 0;
+
+ // translate to user-selected language
+ localize();
+
+ $(".tab-dataflash").toggleClass("supported", supportsDataflash);
+
+ if (supportsDataflash) {
+ // UI hooks
+ $('.tab-dataflash a.erase-flash').click(ask_to_erase_flash);
+
+ $('.tab-dataflash a.erase-flash-confirm').click(flash_erase);
+ $('.tab-dataflash a.erase-flash-cancel').click(flash_erase_cancel);
+
+ $('.tab-dataflash a.save-flash').click(flash_save_begin);
+ $('.tab-dataflash a.save-flash-cancel').click(flash_save_cancel);
+ $('.tab-dataflash a.save-flash-dismiss').click(dismiss_saving_dialog);
+
+ update_html();
+ }
+
+ if (callback) callback();
+ }
+
+ // IO related methods
+ function zeroPad(value, width) {
+ value = "" + value;
+
+ while (value.length < width) {
+ value = "0" + value;
+ }
+
+ return value;
+ }
+
+ function flash_save_cancel() {
+ saveCancelled = true;
+ }
+
+ function show_saving_dialog() {
+ $(".dataflash-saving progress").attr("value", 0);
+ saveCancelled = false;
+ $(".dataflash-saving").removeClass("done");
+
+ $(".dataflash-saving")[0].showModal();
+ }
+
+ function dismiss_saving_dialog() {
+ $(".dataflash-saving")[0].close();
+ }
+
+ function mark_saving_dialog_done() {
+ $(".dataflash-saving").addClass("done");
+ }
+
+ function flash_save_begin() {
+ var
+ maxBytes = DATAFLASH.usedSize;
+
+ if (GUI.connected_to) {
+ prepare_file(function(fileWriter) {
+ var
+ nextAddress = 0;
+
+ show_saving_dialog();
+
+ function onChunkRead(chunkAddress, chunkDataView) {
+ // If we didn't get a zero-byte chunk (indicating end-of-file), request more
+ if (chunkDataView.byteLength > 0) {
+ nextAddress += chunkDataView.byteLength;
+
+ $(".dataflash-saving progress").attr("value", nextAddress / maxBytes * 100);
+
+ var
+ blob = new Blob([chunkDataView]);
+
+ fileWriter.write(blob);
+
+ if (saveCancelled || nextAddress >= maxBytes) {
+ if (saveCancelled) {
+ dismiss_saving_dialog();
+ } else {
+ mark_saving_dialog_done();
+ }
+ } else {
+ MSP.dataflashRead(nextAddress, onChunkRead);
+ }
+ } else {
+ mark_saving_dialog_done();
+ }
+ }
+
+ MSP.dataflashRead(nextAddress, onChunkRead);
+ });
+ }
+ }
+
+ function prepare_file(onComplete) {
+ var
+ date = new Date(),
+ filename = 'blackbox_log_' + date.getFullYear() + '-' + zeroPad(date.getMonth() + 1, 2) + '-'
+ + zeroPad(date.getDate(), 2) + '_' + zeroPad(date.getHours(), 2) + zeroPad(date.getMinutes(), 2)
+ + zeroPad(date.getSeconds(), 2);
+
+ chrome.fileSystem.chooseEntry({type: 'saveFile', suggestedName: filename,
+ accepts: [{extensions: ['TXT']}]}, function(fileEntry) {
+ if (!fileEntry) {
+ console.log('No file selected');
+ return;
+ }
+
+ // echo/console log path specified
+ chrome.fileSystem.getDisplayPath(fileEntry, function(path) {
+ console.log('Dataflash dump file path: ' + path);
+ });
+
+ fileEntry.createWriter(function (fileWriter) {
+ fileWriter.onerror = function (e) {
+ console.error(e);
+
+ // stop logging if the procedure was/is still running
+ };
+
+ onComplete(fileWriter);
+ }, function (e) {
+ // File is not readable or does not exist!
+ console.error(e);
+ });
+ });
+ }
+
+ function ask_to_erase_flash() {
+ eraseCancelled = false;
+ $(".dataflash-confirm-erase").removeClass('erasing');
+
+ $(".dataflash-confirm-erase")[0].showModal();
+ }
+
+ function poll_for_erase_completion() {
+ MSP.send_message(MSP_codes.MSP_DATAFLASH_SUMMARY, false, false, function() {
+ update_html();
+ if (!eraseCancelled) {
+ if (DATAFLASH.ready) {
+ $(".dataflash-confirm-erase")[0].close();
+ } else {
+ setTimeout(poll_for_erase_completion, 500);
+ }
+ }
+ });
+ }
+
+ function flash_erase() {
+ $(".dataflash-confirm-erase").addClass('erasing');
+
+ MSP.send_message(MSP_codes.MSP_DATAFLASH_ERASE, false, false, poll_for_erase_completion);
+ }
+
+ function flash_erase_cancel() {
+ eraseCancelled = true;
+ $(".dataflash-confirm-erase")[0].close();
+ }
+};
+
+TABS.dataflash.cleanup = function (callback) {
+ if (callback) callback();
+};
\ No newline at end of file