diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index e2015e8f..ab7a367b 100755
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -1441,6 +1441,21 @@
"cliReboot": {
"message": "CLI reboot detected"
},
+ "cliSaveToFileBtn": {
+ "message": "Save to File"
+ },
+ "cliSaveToFileFailed": {
+ "message": "Failed to save CLI output to file"
+ },
+ "cliSaveToFileAborted": {
+ "message": "Saving CLI output to file was aborted"
+ },
+ "cliSaveToFileCompleted": {
+ "message": "CLI output successfully saved to file"
+ },
+ "cliClearOutputHistoryBtn": {
+ "message": "Clear output history"
+ },
"loggingNote": {
"message": "Data will be logged in this tab only, leaving the tab will cancel logging and application will return to its normal \"configurator\" state.
You are free to select the global update period, data will be written into the log file every 1 second for performance reasons."
diff --git a/js/fc.js b/js/fc.js
index 6e8d5c26..ecf78b96 100644
--- a/js/fc.js
+++ b/js/fc.js
@@ -103,7 +103,8 @@ var FC = {
battery_profile: 0,
uid: [0, 0, 0],
accelerometerTrims: [0, 0],
- armingFlags: 0
+ armingFlags: 0,
+ name: ''
};
BF_CONFIG = {
@@ -567,7 +568,7 @@ var FC = {
features.push(
{bit: 28, group: 'esc-priority', name: 'PWM_OUTPUT_ENABLE', haveTip: true}
);
-
+
/*
* Transponder disabled until not implemented in firmware
*/
@@ -1068,8 +1069,8 @@ var FC = {
return {
0: "Land",
1: "Drop",
- 2: "RTH",
- 3: "Do Nothing",
+ 2: "RTH",
+ 3: "Do Nothing",
}
},
getRcMapLetters: function () {
diff --git a/js/helpers.js b/js/helpers.js
index 9207089d..ecf72651 100644
--- a/js/helpers.js
+++ b/js/helpers.js
@@ -12,4 +12,37 @@ function constrain(input, min, max) {
}
return input;
+}
+
+function zeroPad(value, width) {
+ value = "" + value;
+
+ while (value.length < width) {
+ value = "0" + value;
+ }
+
+ return value;
+}
+
+function generateFilename(prefix, suffix) {
+ var date = new Date();
+ var filename = prefix;
+
+ if (CONFIG) {
+ if (CONFIG.flightControllerIdentifier) {
+ filename = CONFIG.flightControllerIdentifier + '_' + filename;
+ }
+ if (CONFIG.name && CONFIG.name.trim() !== '') {
+ filename = filename + '_' + CONFIG.name.trim().replace(' ', '_');
+ }
+ }
+
+ filename = filename + '_' + date.getFullYear()
+ + zeroPad(date.getMonth() + 1, 2)
+ + zeroPad(date.getDate(), 2)
+ + '_' + zeroPad(date.getHours(), 2)
+ + zeroPad(date.getMinutes(), 2)
+ + zeroPad(date.getSeconds(), 2);
+
+ return filename + '.' + suffix;
}
\ No newline at end of file
diff --git a/js/msp/MSPHelper.js b/js/msp/MSPHelper.js
index f22d8be0..8b2c78c7 100644
--- a/js/msp/MSPHelper.js
+++ b/js/msp/MSPHelper.js
@@ -525,7 +525,7 @@ var mspHelper = (function (gui) {
break;
- case MSPCodes.MSP_SET_SERVO_MIX_RULE:
+ case MSPCodes.MSP_SET_SERVO_MIX_RULE:
console.log("Servo mix saved");
break;
@@ -1345,6 +1345,11 @@ var mspHelper = (function (gui) {
console.log('OSD char uploaded');
break;
case MSPCodes.MSP_NAME:
+ CONFIG.name = '';
+ var char;
+ while ((char = data.readU8()) !== null) {
+ CONFIG.name += String.fromCharCode(char);
+ }
break;
case MSPCodes.MSP_SET_NAME:
console.log("Craft name set");
@@ -1420,7 +1425,7 @@ var mspHelper = (function (gui) {
case MSPCodes.MSP2_INAV_SET_MC_BRAKING:
console.log('Braking config saved');
break;
-
+
default:
console.log('Unknown code detected: ' + dataHandler.code);
} else {
@@ -2186,7 +2191,7 @@ var mspHelper = (function (gui) {
// send one at a time, with index
var servoRule = SERVO_RULES.get()[servoIndex];
-
+
buffer.push(servoIndex);
buffer.push(servoRule.getTarget());
buffer.push(servoRule.getInput());
diff --git a/js/serial_backend.js b/js/serial_backend.js
index da2312da..2226d030 100755
--- a/js/serial_backend.js
+++ b/js/serial_backend.js
@@ -306,7 +306,12 @@ function onOpen(openInfo) {
googleAnalytics.sendEvent('Firmware', 'Variant', CONFIG.flightControllerIdentifier + ',' + CONFIG.flightControllerVersion);
GUI.log(chrome.i18n.getMessage('fcInfoReceived', [CONFIG.flightControllerIdentifier, CONFIG.flightControllerVersion]));
if (semver.gte(CONFIG.flightControllerVersion, CONFIGURATOR.firmwareVersionAccepted)) {
- onValidFirmware();
+ mspHelper.getCraftName(function(name) {
+ if (name) {
+ CONFIG.name = name;
+ }
+ onValidFirmware();
+ });
} else {
onInvalidFirmwareVersion();
}
diff --git a/src/css/tabs/cli.css b/src/css/tabs/cli.css
index 52597896..d7f1643d 100644
--- a/src/css/tabs/cli.css
+++ b/src/css/tabs/cli.css
@@ -3,7 +3,7 @@
}
.tab-cli .content_wrapper {
- height: calc(100% - 50px);
+ height: calc(100% - 92px);
}
.tab-cli p {
@@ -56,4 +56,19 @@
.tab-cli .window .wrapper {
white-space: pre-wrap;
+}
+
+.tab-cli .save {
+ color: white;
+}
+
+@media only screen and (max-width: 1055px) , only screen and (max-device-width: 1055px) {
+
+.tab-cli .content_wrapper {
+ height: calc(100% - 87px);
+}
+.tab-cli .content_toolbar {
+ margin-top: 5px;
+}
+
}
\ No newline at end of file
diff --git a/tabs/cli.html b/tabs/cli.html
index 19872413..9a533704 100644
--- a/tabs/cli.html
+++ b/tabs/cli.html
@@ -5,6 +5,7 @@
+
-
+
+
\ No newline at end of file
diff --git a/tabs/cli.js b/tabs/cli.js
index 82a3d894..0a7a62c7 100644
--- a/tabs/cli.js
+++ b/tabs/cli.js
@@ -1,11 +1,46 @@
'use strict';
/*global chrome*/
TABS.cli = {
- 'validateText': "",
- 'currentLine': "",
- 'sequenceElements': 0
+ lineDelayMs: 15,
+ profileSwitchDelayMs: 100,
+ outputHistory: "",
+ cliBuffer: ""
};
+function removePromptHash(promptText) {
+ return promptText.replace(/^# /, '');
+}
+
+function cliBufferCharsToDelete(command, buffer) {
+ var commonChars = 0;
+ for (var i = 0;i < buffer.length;i++) {
+ if (command[i] === buffer[i]) {
+ commonChars++;
+ } else {
+ break;
+ }
+ }
+
+ return buffer.length - commonChars;
+}
+
+function commandWithBackSpaces(command, buffer, noOfCharsToDelete) {
+ const backspace = String.fromCharCode(127);
+ return backspace.repeat(noOfCharsToDelete) + command.substring(buffer.length - noOfCharsToDelete, command.length);
+}
+
+function getCliCommand(command, cliBuffer) {
+ const buffer = removePromptHash(cliBuffer);
+ const bufferRegex = new RegExp('^' + buffer, 'g');
+ if (command.match(bufferRegex)) {
+ return command.replace(bufferRegex, '');
+ }
+
+ const noOfCharsToDelete = cliBufferCharsToDelete(command, buffer);
+
+ return commandWithBackSpaces(command, buffer, noOfCharsToDelete);
+}
+
TABS.cli.initialize = function (callback) {
var self = this;
@@ -14,12 +49,13 @@ TABS.cli.initialize = function (callback) {
googleAnalytics.sendAppView('CLI');
}
- /*
- * Flush MSP queue as well as all MSP registered callbacks
- */
+ // Flush MSP queue as well as all MSP registered callbacks
helper.mspQueue.flush();
MSP.callbacks_cleanup();
+ self.outputHistory = "";
+ self.cliBuffer = "";
+
$('#content').load("./tabs/cli.html", function () {
// translate to user-selected language
localize();
@@ -28,18 +64,101 @@ TABS.cli.initialize = function (callback) {
var textarea = $('.tab-cli textarea');
+ $('.tab-cli .save').click(function() {
+ var prefix = 'cli';
+ var suffix = 'txt';
+
+ var filename = generateFilename(prefix, suffix);
+
+ var accepts = [{
+ description: suffix.toUpperCase() + ' files', extensions: [suffix],
+ }];
+
+ chrome.fileSystem.chooseEntry({type: 'saveFile', suggestedName: filename, accepts: accepts}, function(entry) {
+ if (chrome.runtime.lastError) {
+ if (chrome.runtime.lastError.message === 'User cancelled') {
+ GUI.log(chrome.i18n.getMessage('cliSaveToFileAborted'));
+ } else {
+ GUI.log(chrome.i18n.getMessage('cliSaveToFileFailed'));
+ console.error(chrome.runtime.lastError.message);
+ }
+ return;
+ }
+
+ if (!entry) {
+ GUI.log(chrome.i18n.getMessage('cliSaveToFileAborted'));
+ return;
+ }
+
+ entry.createWriter(function (writer) {
+ writer.onerror = function (){
+ GUI.log(chrome.i18n.getMessage('cliSaveToFileFailed'));
+ };
+
+ writer.onwriteend = function () {
+ if (self.outputHistory.length > 0 && writer.length === 0) {
+ writer.write(new Blob([self.outputHistory], {type: 'text/plain'}));
+ } else {
+ GUI.log(chrome.i18n.getMessage('cliSaveToFileCompleted'));
+ }
+ };
+
+ writer.truncate(0);
+ }, function (){
+ GUI.log(chrome.i18n.getMessage('cliSaveToFileFailed'));
+ console.error('Failed to get file writer');
+ });
+ });
+ });
+
+$('.tab-cli .clear').click(function() {
+ self.outputHistory = "";
+ $('.tab-cli .window .wrapper').empty();
+ });
+
+ // Tab key detection must be on keydown,
+ // `keypress`/`keyup` happens too late, as `textarea` will have already lost focus.
+ textarea.keydown(function (event) {
+ const tabKeyCode = 9;
+ if (event.which == tabKeyCode) {
+ // prevent default tabbing behaviour
+ event.preventDefault();
+ const outString = textarea.val();
+ const lastCommand = outString.split("\n").pop();
+ const command = getCliCommand(lastCommand, self.cliBuffer);
+ if (command) {
+ self.sendAutoComplete(command);
+ textarea.val('');
+ }
+ }
+ });
+
textarea.keypress(function (event) {
- if (event.which == 13) { // enter
+ const enterKeyCode = 13;
+ if (event.which == enterKeyCode) {
event.preventDefault(); // prevent the adding of new line
var out_string = textarea.val();
- var out_arr = out_string.split("\n");
self.history.add(out_string.trim());
- var timeout_needle = 0;
- for (var i = 0; i < out_arr.length; i++) {
- self.sendSlowly(out_arr, i, timeout_needle++);
- }
+ var outputArray = out_string.split("\n");
+ Promise.reduce(outputArray, function(delay, line, index) {
+ return new Promise(function (resolve) {
+ helper.timeout.add('CLI_send_slowly', function () {
+ var processingDelay = self.lineDelayMs;
+ if (line.toLowerCase().startsWith('profile')) {
+ processingDelay = self.profileSwitchDelayMs;
+ }
+ const isLastCommand = outputArray.length === index + 1;
+ if (isLastCommand && self.cliBuffer) {
+ line = getCliCommand(line, self.cliBuffer);
+ }
+ self.sendLine(line, function () {
+ resolve(processingDelay);
+ });
+ }, delay)
+ })
+ }, 0);
textarea.val('');
}
@@ -84,29 +203,33 @@ TABS.cli.history.add = function (str) {
this.history.push(str);
this.index = this.history.length;
};
+
TABS.cli.history.prev = function () {
if (this.index > 0) this.index -= 1;
return this.history[this.index];
};
+
TABS.cli.history.next = function () {
if (this.index < this.history.length) this.index += 1;
return this.history[this.index - 1];
};
-TABS.cli.sendSlowly = function (out_arr, i, timeout_needle) {
- helper.timeout.add('CLI_send_slowly', function () {
- var bufferOut = new ArrayBuffer(out_arr[i].length + 1);
- var bufView = new Uint8Array(bufferOut);
+const backspaceCode = 8;
+const lineFeedCode = 10;
+const carriageReturnCode = 13;
- for (var c_key = 0; c_key < out_arr[i].length; c_key++) {
- bufView[c_key] = out_arr[i].charCodeAt(c_key);
- }
+function writeToOutput(text) {
+ $('.tab-cli .window .wrapper').append(text);
+ $('.tab-cli .window').scrollTop($('.tab-cli .window .wrapper').height());
+}
- bufView[out_arr[i].length] = 0x0D; // enter (\n)
+function writeLineToOutput(text) {
+ writeToOutput(text + "
");
+}
- serial.send(bufferOut);
- }, timeout_needle * 100);
-};
+function setPrompt(text) {
+ $('.tab-cli textarea').val(text);
+}
TABS.cli.read = function (readInfo) {
/* Some info about handling line feeds and carriage return
@@ -117,93 +240,106 @@ TABS.cli.read = function (readInfo) {
MAC only understands CR
Linux and Unix only understand LF
Windows understands (both) CRLF
- Chrome OS currenty unknown
+ Chrome OS currently unknown
*/
var data = new Uint8Array(readInfo.data),
- text = "";
+ validateText = "",
+ sequenceCharsToSkip = 0;
for (var i = 0; i < data.length; i++) {
- if (CONFIGURATOR.cliValid) {
- if (data[i] == 27 || this.sequenceElements > 0) { // ESC + other
- this.sequenceElements++;
+ const currentChar = String.fromCharCode(data[i]);
- // delete previous space
- if (this.sequenceElements == 1) {
- text = text.substring(0, text.length -1);
- }
-
- // Reset
- if (this.sequenceElements >= 5) {
- this.sequenceElements = 0;
- }
- }
-
- if (this.sequenceElements == 0) {
- switch (data[i]) {
- case 10: // line feed
- if (GUI.operating_system != "MacOS") {
- text += "
";
- }
- this.currentLine = "";
- break;
- case 13: // carriage return
- if (GUI.operating_system == "MacOS") {
- text += "
";
- }
- this.currentLine = "";
- break;
- case 60:
- text += '<';
- break;
- case 62:
- text += '>';
- break;
-
- default:
- text += String.fromCharCode(data[i]);
- this.currentLine += String.fromCharCode(data[i]);
- }
- }
- if (this.currentLine == 'Rebooting') {
- CONFIGURATOR.cliActive = false;
- CONFIGURATOR.cliValid = false;
- GUI.log(chrome.i18n.getMessage('cliReboot'));
- GUI.log(chrome.i18n.getMessage('deviceRebooting'));
- GUI.handleReconnect();
- }
- } else {
+ if (!CONFIGURATOR.cliValid) {
// try to catch part of valid CLI enter message
- this.validateText += String.fromCharCode(data[i]);
- text += String.fromCharCode(data[i]);
+ validateText += currentChar;
+ writeToOutput(currentChar);
+ continue;
}
+
+ const escapeSequenceCode = 27;
+ const escapeSequenceCharLength = 3;
+ if (data[i] == escapeSequenceCode && !sequenceCharsToSkip) { // ESC + other
+ sequenceCharsToSkip = escapeSequenceCharLength;
+ }
+
+ if (sequenceCharsToSkip) {
+ sequenceCharsToSkip--;
+ continue;
+ }
+
+ switch (data[i]) {
+ case lineFeedCode:
+ if (GUI.operating_system === "Windows") {
+ writeLineToOutput(this.cliBuffer);
+ this.cliBuffer = "";
+ }
+ break;
+ case carriageReturnCode:
+ if (GUI.operating_system !== "Windows") {
+ writeLineToOutput(this.cliBuffer);
+ this.cliBuffer = "";
+ }
+ break;
+ case 60:
+ this.cliBuffer += '<';
+ break;
+ case 62:
+ this.cliBuffer += '>';
+ break;
+ case backspaceCode:
+ this.cliBuffer = this.cliBuffer.slice(0, -1);
+ break;
+
+ default:
+ this.cliBuffer += currentChar;
+ }
+
+ this.outputHistory += currentChar;
+
+ if (this.cliBuffer == 'Rebooting') {
+ CONFIGURATOR.cliActive = false;
+ CONFIGURATOR.cliValid = false;
+ GUI.log(chrome.i18n.getMessage('cliReboot'));
+ GUI.log(chrome.i18n.getMessage('deviceRebooting'));
+ GUI.handleReconnect();
+ }
+
}
- if (!CONFIGURATOR.cliValid && this.validateText.indexOf('CLI') != -1) {
+ if (!CONFIGURATOR.cliValid && validateText.indexOf('CLI') !== -1) {
GUI.log(chrome.i18n.getMessage('cliEnter'));
CONFIGURATOR.cliValid = true;
- this.validateText = "";
+ validateText = "";
}
- $('.tab-cli .window .wrapper').append(text);
- $('.tab-cli .window').scrollTop($('.tab-cli .window .wrapper').height());
+ setPrompt(removePromptHash(this.cliBuffer));
+};
+
+TABS.cli.sendLine = function (line, callback) {
+ this.send(line + '\n', callback);
+};
+
+TABS.cli.sendAutoComplete = function (line, callback) {
+ this.send(line + '\t', callback);
+};
+
+TABS.cli.send = function (line, callback) {
+ var bufferOut = new ArrayBuffer(line.length);
+ var bufView = new Uint8Array(bufferOut);
+
+ for (var c_key = 0; c_key < line.length; c_key++) {
+ bufView[c_key] = line.charCodeAt(c_key);
+ }
+
+ serial.send(bufferOut, callback);
};
TABS.cli.cleanup = function (callback) {
- if (!CONFIGURATOR.connectionValid || !CONFIGURATOR.cliValid) {
+ if (!(CONFIGURATOR.connectionValid && CONFIGURATOR.cliValid && CONFIGURATOR.cliActive)) {
if (callback) callback();
return;
}
-
- var bufferOut = new ArrayBuffer(5);
- var bufView = new Uint8Array(bufferOut);
-
- bufView[0] = 0x65; // e
- bufView[1] = 0x78; // x
- bufView[2] = 0x69; // i
- bufView[3] = 0x74; // t
- bufView[4] = 0x0D; // enter
-
- serial.send(bufferOut, function (writeInfo) {
+ this.send(getCliCommand('exit\r', this.cliBuffer), function (writeInfo) {
// we could handle this "nicely", but this will do for now
// (another approach is however much more complicated):
// we can setup an interval asking for data lets say every 200ms, when data arrives, callback will be triggered and tab switched
diff --git a/tabs/configuration.js b/tabs/configuration.js
index 35108a8b..f7adfc6a 100644
--- a/tabs/configuration.js
+++ b/tabs/configuration.js
@@ -12,10 +12,14 @@ TABS.configuration.initialize = function (callback, scrollPosition) {
var craftName = null;
var loadCraftName = function(callback) {
- mspHelper.getCraftName(function(name) {
- craftName = name;
+ if (!CONFIG.name || CONFIG.name.trim() === '') {
+ mspHelper.getCraftName(function(name) {
+ craftName = name;
+ callback();
+ });
+ } else {
callback();
- });
+ }
};
var saveCraftName = function(callback) {
@@ -625,7 +629,7 @@ TABS.configuration.initialize = function (callback, scrollPosition) {
$attitudeFrequency.change(function () {
INAV_PID_CONFIG.attitudeTaskFrequency = $attitudeFrequency.val();
});
-
+
if (semver.gte(CONFIG.flightControllerVersion, "1.5.0")) {
var $sensorAcc = $('#sensor-acc'),