diff --git a/locales/en/messages.json b/locales/en/messages.json index ce2a0f17..e80d0f88 100644 --- a/locales/en/messages.json +++ b/locales/en/messages.json @@ -52,6 +52,13 @@ "portsSelectManual": { "message": "Manual Selection" }, + "portsSelectVirtual": { + "message": "Virtual Mode (Experimental)", + "description": "Configure a Virtual Flight Controller without the need of a physical FC." + }, + "virtualMSPVersion": { + "message": "Virtual Firmware Version" + }, "portOverrideText": { "message": "Port:" }, diff --git a/src/css/main.css b/src/css/main.css index 05e3bfbe..02a1dc4f 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -224,6 +224,13 @@ input[type="number"]::-webkit-inner-spin-button { color: var(--subtleAccent); } +#firmware-virtual-option { + height: 76px; + width: 180px; + margin-right: 15px; + display: none; +} + #port-override-option { height: 76px; margin-top: 7px; diff --git a/src/js/VirtualFC.js b/src/js/VirtualFC.js new file mode 100644 index 00000000..9630ed8c --- /dev/null +++ b/src/js/VirtualFC.js @@ -0,0 +1,212 @@ +'use strict'; + +const VirtualFC = { + // these values are manufactured to unlock all the functionality of the configurator, they dont represent actual hardware + setVirtualConfig() { + const virtualFC = FC; + + virtualFC.resetState(); + + virtualFC.CONFIG.flightControllerVersion = "4.2.4"; + virtualFC.CONFIG.apiVersion = CONFIGURATOR.virtualApiVersion; + + virtualFC.FEATURE_CONFIG.features = new Features(FC.CONFIG); + virtualFC.FEATURE_CONFIG.features.setMask(0); + + virtualFC.BEEPER_CONFIG.beepers = new Beepers(FC.CONFIG); + virtualFC.BEEPER_CONFIG.dshotBeaconConditions = new Beepers(FC.CONFIG, [ "RX_LOST", "RX_SET" ]); + + virtualFC.MIXER_CONFIG.mixer = 3; + + virtualFC.MOTOR_DATA = Array.from({length: 8}); + virtualFC.MOTOR_3D_CONFIG = true; + virtualFC.MOTOR_CONFIG = { + minthrottle: 1070, + maxthrottle: 2000, + mincommand: 1000, + motor_count: 4, + motor_poles: 14, + use_dshot_telemetry: true, + use_esc_sensor: false, + }; + + virtualFC.SERVO_CONFIG = Array.from({length: 8}); + + for (let i = 0; i < virtualFC.SERVO_CONFIG.length; i++) { + virtualFC.SERVO_CONFIG[i] = { + middle: 1500, + min: 1000, + max: 2000, + indexOfChannelToForward: 255, + rate: 100, + reversedInputSources: 0, + }; + } + + virtualFC.ADJUSTMENT_RANGES = Array.from({length: 16}); + + for (let i = 0; i < virtualFC.ADJUSTMENT_RANGES.length; i++) { + virtualFC.ADJUSTMENT_RANGES[i] = { + slotIndex: 0, + auxChannelIndex: 0, + range: { + start: 900, + end: 900, + }, + adjustmentFunction: 0, + auxSwitchChannelIndex: 0, + }; + } + + virtualFC.SERIAL_CONFIG.ports = Array.from({length: 6}); + + virtualFC.SERIAL_CONFIG.ports[0] = { + identifier: 20, + auxChannelIndex: 0, + functions: ["MSP"], + msp_baudrate: 115200, + gps_baudrate: 57600, + telemetry_baudrate: "AUTO", + blackbox_baudrate: 115200, + }; + + for (let i = 1; i < virtualFC.SERIAL_CONFIG.ports.length; i++) { + virtualFC.SERIAL_CONFIG.ports[i] = { + identifier: i-1, + auxChannelIndex: 0, + functions: [], + msp_baudrate: 115200, + gps_baudrate: 57600, + telemetry_baudrate: "AUTO", + blackbox_baudrate: 115200, + }; + } + + virtualFC.LED_STRIP = Array.from({length: 256}); + + for (let i = 0; i < virtualFC.LED_STRIP.length; i++) { + virtualFC.LED_STRIP[i] = { + x: 0, + y: 0, + functions: ["c"], + color: 0, + directions: [], + parameters: 0, + }; + } + + virtualFC.ANALOG = { + voltage: 12, + mAhdrawn: 1200, + rssi: 100, + amperage: 3, + }; + + virtualFC.CONFIG.sampleRateHz = 12000; + virtualFC.PID_ADVANCED_CONFIG.pid_process_denom = 2; + + virtualFC.BLACKBOX.supported = true; + + virtualFC.VTX_CONFIG.vtx_type = 1; + + virtualFC.BATTERY_CONFIG = { + vbatmincellvoltage: 1, + vbatmaxcellvoltage: 4, + vbatwarningcellvoltage: 3, + capacity: 10000, + voltageMeterSource: 1, + currentMeterSource: 1, + }; + + virtualFC.BATTERY_STATE = { + cellCount: 10, + voltage: 20, + mAhDrawn: 1000, + amperage: 3, + }; + + virtualFC.DATAFLASH = { + ready: true, + supported: true, + sectors: 1024, + totalSize: 40000, + usedSize: 10000, + }; + + virtualFC.SDCARD = { + supported: true, + state: 1, + freeSizeKB: 1024, + totalSizeKB: 2048, + }; + + virtualFC.SENSOR_CONFIG = { + acc_hardware: 1, + baro_hardware: 1, + mag_hardware: 1, + }; + + virtualFC.RC = { + channels: Array.from({length: 16}), + active_channels: 16, + }; + for (let i = 0; i < virtualFC.RC.channels.length; i++) { + virtualFC.RC.channels[i] = 1500; + } + + // from https://github.com/betaflight/betaflight/blob/master/docs/Modes.md + virtualFC.AUX_CONFIG = ["ARM","ANGLE","HORIZON","ANTI GRAVITY","MAG","HEADFREE","HEADADJ","CAMSTAB","PASSTHRU","BEEPERON","LEDLOW","CALIB", + "OSD","TELEMETRY","SERVO1","SERVO2","SERVO3","BLACKBOX","FAILSAFE","AIRMODE","3D","FPV ANGLE MIX","BLACKBOX ERASE","CAMERA CONTROL 1", + "CAMERA CONTROL 2","CAMERA CONTROL 3","FLIP OVER AFTER CRASH","BOXPREARM","BEEP GPS SATELLITE COUNT","VTX PIT MODE","USER1","USER2", + "USER3","USER4","PID AUDIO","PARALYZE","GPS RESCUE","ACRO TRAINER","DISABLE VTX CONTROL","LAUNCH CONTROL"]; + FC.AUX_CONFIG_IDS = [0,1,2,4,5,6,7,8,12,13,15,17,19,20,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,39,40,41,42,43,44,45,46,47,48,49]; + + for (let i = 0; i < 16; i++) { + virtualFC.RXFAIL_CONFIG[i] = { + mode: 1, + value: 1500, + }; + } + + // 11 1111 (pass bitchecks) + virtualFC.CONFIG.activeSensors = 63; + }, + setupVirtualOSD(){ + const virtualOSD = OSD; + + virtualOSD.data.video_system = 1; + virtualOSD.data.unit_mode = 1; + + virtualOSD.virtualMode = { + itemPositions: Array.from({length: 60}), + statisticsState: [], + warningFlags: 0, + timerData: [], + }; + + virtualOSD.data.state = { + haveMax7456Configured: true, + haveOsdFeature: true, + haveMax7456FontDeviceConfigured: true, + isMax7456FontDeviceDetected: true, + haveSomeOsd: true, + }; + + virtualOSD.data.parameters = { + cameraFrameWidth: 30, + cameraFrameHeight: 30, + }; + + virtualOSD.data.osd_profiles = { + number: 3, + selected: 0, + }; + + virtualOSD.data.alarms = { + rssi: { display_name: i18n.getMessage('osdTimerAlarmOptionRssi'), value: 0 }, + cap: { display_name: i18n.getMessage('osdTimerAlarmOptionCapacity'), value: 0 }, + alt: { display_name: i18n.getMessage('osdTimerAlarmOptionAltitude'), value: 0 }, + time: { display_name: 'Minutes', value: 0 }, + }; + }, +}; diff --git a/src/js/backup_restore.js b/src/js/backup_restore.js index 1153648e..33b533ff 100644 --- a/src/js/backup_restore.js +++ b/src/js/backup_restore.js @@ -928,6 +928,14 @@ function configuration_restore(callback) { } } + if (CONFIGURATOR.virtualMode) { + FC.resetState(); + FC.CONFIG.apiVersion = CONFIGURATOR.virtualApiVersion; + + sensor_status(FC.CONFIG.activeSensors); + update_dataflash_global(); + } + upload(); } } diff --git a/src/js/data_storage.js b/src/js/data_storage.js index 54131d14..8eddc1c1 100644 --- a/src/js/data_storage.js +++ b/src/js/data_storage.js @@ -25,6 +25,8 @@ var CONFIGURATOR = { connectionValid: false, connectionValidCliOnly: false, + virtualMode: false, + virtualApiVersion: '0.0.1', cliActive: false, cliValid: false, gitChangesetId: 'unknown', diff --git a/src/js/main.js b/src/js/main.js index be571516..bc1205d9 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -98,7 +98,7 @@ function closeSerial() { // automatically close the port when application closes const connectionId = serial.connectionId; - if (connectionId && CONFIGURATOR.connectionValid) { + if (connectionId && CONFIGURATOR.connectionValid && !CONFIGURATOR.virtualMode) { // code below is handmade MSP message (without pretty JS wrapper), it behaves exactly like MSP.send_message // sending exit command just in case the cli tab was open. // reset motors to default (mincommand) diff --git a/src/js/msp.js b/src/js/msp.js index 6d52afde..9717f471 100644 --- a/src/js/msp.js +++ b/src/js/msp.js @@ -56,6 +56,10 @@ const MSP = { JUMBO_FRAME_SIZE_LIMIT: 255, read: function (readInfo) { + if (CONFIGURATOR.virtualMode) { + return; + } + const data = new Uint8Array(readInfo.data); for (const chunk of data) { @@ -310,6 +314,13 @@ const MSP = { return bufferOut; }, send_message: function (code, data, callback_sent, callback_msp, doCallbackOnError) { + if (CONFIGURATOR.virtualMode) { + if (callback_msp) { + callback_msp(); + } + return; + } + if (code === undefined) { return; } diff --git a/src/js/port_handler.js b/src/js/port_handler.js index c6973c54..29c8bf02 100644 --- a/src/js/port_handler.js +++ b/src/js/port_handler.js @@ -17,6 +17,9 @@ const PortHandler = new function () { PortHandler.initialize = function () { this.portPickerElement = $('div#port-picker #port'); + // fill dropdown with version numbers + generateVirtualApiVersions(); + // start listening, check after TIMEOUT_CHECK ms this.check(); }; @@ -72,6 +75,12 @@ PortHandler.check_usb_devices = function (callback) { data: {isDFU: true}, })); + self.portPickerElement.append($('', { + value: 'virtual', + text: i18n.getMessage('portsSelectVirtual'), + data: {isVirtual: true}, + })); + self.portPickerElement.append($('', { value: 'manual', text: i18n.getMessage('portsSelectManual'), @@ -213,6 +222,12 @@ PortHandler.updatePortSelect = function (ports) { })); } + this.portPickerElement.append($("", { + value: 'virtual', + text: i18n.getMessage('portsSelectVirtual'), + data: {isVirtual: true}, + })); + this.portPickerElement.append($("", { value: 'manual', text: i18n.getMessage('portsSelectManual'), diff --git a/src/js/serial.js b/src/js/serial.js index dfa548eb..c7d6dd8b 100644 --- a/src/js/serial.js +++ b/src/js/serial.js @@ -8,7 +8,7 @@ const serial = { bytesReceived: 0, bytesSent: 0, failed: 0, - connectionType: 'serial', // 'serial' or 'tcp' + connectionType: 'serial', // 'serial' or 'tcp' or 'virtual' connectionIP: '127.0.0.1', connectionPort: 5761, @@ -20,6 +20,8 @@ const serial = { const testUrl = path.match(/^tcp:\/\/([A-Za-z0-9\.-]+)(?:\:(\d+))?$/); if (testUrl) { self.connectTcp(testUrl[1], testUrl[2], options, callback); + } else if (path === 'virtual') { + self.connectVirtual(callback); } else { self.connectSerial(path, options, callback); } @@ -189,6 +191,21 @@ const serial = { } }); }, + connectVirtual: function (callback) { + const self = this; + self.connectionType = 'virtual'; + + if (!self.openCanceled) { + self.connected = true; + self.connectionId = 'virtual'; + self.bitrate = 115200; + self.bytesReceived = 0; + self.bytesSent = 0; + self.failed = 0; + + callback(); + } + }, disconnect: function (callback) { const self = this; self.connected = false; @@ -203,26 +220,32 @@ const serial = { for (let i = (self.onReceiveError.listeners.length - 1); i >= 0; i--) { self.onReceiveError.removeListener(self.onReceiveError.listeners[i]); } + if (self.connectionType !== 'virtual') { + if (self.connectionType === 'tcp') { + chrome.sockets.tcp.disconnect(self.connectionId, function () { + checkChromeRuntimeError(); + console.log(`${self.connectionType}: disconnecting socket.`); + }); + } - if (self.connectionType === 'tcp') { - chrome.sockets.tcp.disconnect(self.connectionId, function () { + const disconnectFn = (self.connectionType === 'serial') ? chrome.serial.disconnect : chrome.sockets.tcp.close; + disconnectFn(self.connectionId, function (result) { checkChromeRuntimeError(); - console.log(`${self.connectionType}: disconnecting socket.`); + + result = result || self.connectionType === 'tcp'; + console.log(`${self.connectionType}: ${result ? 'closed' : 'failed to close'} connection with ID: ${self.connectionId}, Sent: ${self.bytesSent} bytes, Received: ${self.bytesReceived} bytes`); + + self.connectionId = false; + self.bitrate = 0; + + if (callback) callback(result); }); - } - - const disconnectFn = (self.connectionType === 'serial') ? chrome.serial.disconnect : chrome.sockets.tcp.close; - disconnectFn(self.connectionId, function (result) { - checkChromeRuntimeError(); - - result = result || self.connectionType === 'tcp'; - console.log(`${self.connectionType}: ${result ? 'closed' : 'failed to close'} connection with ID: ${self.connectionId}, Sent: ${self.bytesSent} bytes, Received: ${self.bytesReceived} bytes`); - + } else { self.connectionId = false; - self.bitrate = 0; - - if (callback) callback(result); - }); + if (callback) { + callback(); + } + } } else { // connection wasn't opened, so we won't try to close anything // instead we will rise canceled flag which will prevent connect from continueing further after being canceled diff --git a/src/js/serial_backend.js b/src/js/serial_backend.js index 51eaf4cb..31e02755 100644 --- a/src/js/serial_backend.js +++ b/src/js/serial_backend.js @@ -12,6 +12,12 @@ function initializeSerialBackend() { else { $('#port-override-option').hide(); } + if (selected_port.data().isVirtual) { + $('#firmware-virtual-option').show(); + } + else { + $('#firmware-virtual-option').hide(); + } if (selected_port.data().isDFU) { $('select#baud').hide(); } @@ -67,7 +73,14 @@ function initializeSerialBackend() { $('div#port-picker #port, div#port-picker #baud, div#port-picker #delay').prop('disabled', true); $('div.connect_controls div.connect_state').text(i18n.getMessage('connecting')); - serial.connect(portName, {bitrate: selected_baud}, onOpen); + if (selectedPort.data().isVirtual) { + CONFIGURATOR.virtualMode = true; + CONFIGURATOR.virtualApiVersion = $('#firmware-version-dropdown :selected').val(); + + serial.connect('virtual', {}, onOpenVirtual); + } else { + serial.connect(portName, {bitrate: selected_baud}, onOpen); + } toggleStatus(); } else { @@ -206,6 +219,8 @@ function setConnectionTimeout() { function onOpen(openInfo) { if (openInfo) { + CONFIGURATOR.virtualMode = false; + // update connected_to GUI.connected_to = GUI.connecting_to; @@ -297,6 +312,23 @@ function onOpen(openInfo) { } } +function onOpenVirtual() { + GUI.connected_to = GUI.connecting_to; + GUI.connecting_to = false; + + CONFIGURATOR.connectionValid = true; + + mspHelper = new MspHelper(); + + VirtualFC.setVirtualConfig(); + + processBoardInfo(); + + update_dataflash_global(); + sensor_status(FC.CONFIG.activeSensors); + updateTabList(FC.FEATURE_CONFIG.features); +} + function abortConnect() { $('div#connectbutton div.connect_state').text(i18n.getMessage('connect')); $('div#connectbutton a.connect').removeClass('active'); diff --git a/src/js/tabs/gps.js b/src/js/tabs/gps.js index 11ed7e9d..bf5c970d 100644 --- a/src/js/tabs/gps.js +++ b/src/js/tabs/gps.js @@ -102,6 +102,8 @@ TABS.gps.initialize = function (callback) { } } + let frame = document.getElementById('map'); + // enable data pulling GUI.interval_add('gps_pull', function gps_update() { // avoid usage of the GPS commands until a GPS sensor is detected for targets that are compiled without GPS support. @@ -137,8 +139,6 @@ TABS.gps.initialize = function (callback) { } }); - let frame = document.getElementById('map'); - $('#zoom_in').click(function() { console.log('zoom in'); const message = { diff --git a/src/js/tabs/osd.js b/src/js/tabs/osd.js index 3d3cd107..7926e808 100644 --- a/src/js/tabs/osd.js +++ b/src/js/tabs/osd.js @@ -1867,6 +1867,11 @@ OSD.msp = { warningFlags |= (1 << i); } } + + if (CONFIGURATOR.virtualMode) { + OSD.virtualMode.warningFlags = warningFlags; + } + console.log(warningFlags); result.push16(warningFlags); if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_41)) { @@ -1882,17 +1887,24 @@ OSD.msp = { result.push8(OSD.data.parameters.cameraFrameHeight); } } - } return result; }, encodeLayout(displayItem) { + if (CONFIGURATOR.virtualMode) { + OSD.virtualMode.itemPositions[displayItem.index] = this.helpers.pack.position(displayItem); + } + const buffer = []; buffer.push8(displayItem.index); buffer.push16(this.helpers.pack.position(displayItem)); return buffer; }, encodeStatistics(statItem) { + if (CONFIGURATOR.virtualMode) { + OSD.virtualMode.statisticsState[statItem.index] = statItem.enabled; + } + const buffer = []; buffer.push8(statItem.index); buffer.push16(statItem.enabled); @@ -1900,10 +1912,49 @@ OSD.msp = { return buffer; }, encodeTimer(timer) { + if (CONFIGURATOR.virtualMode) { + OSD.virtualMode.timerData[timer.index] = {}; + OSD.virtualMode.timerData[timer.index].src = timer.src; + OSD.virtualMode.timerData[timer.index].precision = timer.precision; + OSD.virtualMode.timerData[timer.index].alarm = timer.alarm; + } + const buffer = [-2, timer.index]; buffer.push16(this.helpers.pack.timer(timer)); return buffer; }, + processOsdElements(data, itemPositions){ + // Now we have the number of profiles, process the OSD elements + for (const item of itemPositions) { + const j = data.displayItems.length; + let c; + let suffix; + let ignoreSize = false; + if (data.displayItems.length < OSD.constants.DISPLAY_FIELDS.length) { + c = OSD.constants.DISPLAY_FIELDS[j]; + } else { + c = OSD.constants.UNKNOWN_DISPLAY_FIELD; + suffix = (1 + data.displayItems.length - OSD.constants.DISPLAY_FIELDS.length).toString(); + ignoreSize = true; + } + data.displayItems.push($.extend({ + name: c.name, + text: suffix ? [c.text, suffix] : c.text, + desc: c.desc, + index: j, + draw_order: c.draw_order, + preview: suffix ? c.preview + suffix : c.preview, + ignoreSize, + }, this.helpers.unpack.position(item, c))); + } + + // Generate OSD element previews and positionable that are defined by a function + for (const item of data.displayItems) { + if (typeof (item.preview) === 'function') { + item.preview = item.preview(data); + } + } + }, // Currently only parses MSP_MAX_OSD responses, add a switch on payload.code if more codes are handled decode(payload) { const view = payload.data; @@ -2068,36 +2119,82 @@ OSD.msp = { d.parameters.cameraFrameHeight = view.readU8(); } - // Now we have the number of profiles, process the OSD elements - for (const item of itemsPositionsRead) { - const j = d.displayItems.length; - let c; - let suffix; - let ignoreSize = false; - if (d.displayItems.length < OSD.constants.DISPLAY_FIELDS.length) { - c = OSD.constants.DISPLAY_FIELDS[j]; - } else { - c = OSD.constants.UNKNOWN_DISPLAY_FIELD; - suffix = (1 + d.displayItems.length - OSD.constants.DISPLAY_FIELDS.length).toString(); - ignoreSize = true; + this.processOsdElements(d, itemsPositionsRead); + + OSD.updateDisplaySize(); + }, + decodeVirtual() { + const d = OSD.data; + + d.displayItems = []; + d.statItems = []; + d.warnings = []; + d.timers = []; + + if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_36)) { + // Parse statistics display enable + const expectedStatsCount = OSD.constants.STATISTIC_FIELDS.length; + + for (let i = 0; i < expectedStatsCount; i++) { + const v = OSD.virtualMode.statisticsState[i] ? 1 : 0; + + // Known statistics field + if (i < expectedStatsCount) { + const c = OSD.constants.STATISTIC_FIELDS[i]; + d.statItems.push({ + name: c.name, + text: c.text, + desc: c.desc, + index: i, + enabled: v === 1, + }); + + // Read all the data for any statistics we don't know about + } else { + const statisticNumber = i - expectedStatsCount + 1; + d.statItems.push({ + name: 'UNKNOWN', + text: ['osdTextStatUnknown', statisticNumber], + desc: 'osdDescStatUnknown', + index: i, + enabled: v === 1, + }); + } + } + + // Parse configurable timers + const expectedTimersCount = 3; + for (let i = 0; i < expectedTimersCount; i++) { + d.timers.push($.extend({ + index: i, + }, OSD.virtualMode.timerData[i])); + } + + // Parse enabled warnings + const warningCount = OSD.constants.WARNINGS.length; + const warningFlags = OSD.virtualMode.warningFlags; + + for (let i = 0; i < warningCount; i++) { + const enabled = (warningFlags & (1 << i)) !== 0; + + // Known warning field + if (i < warningCount) { + d.warnings.push($.extend(OSD.constants.WARNINGS[i], { enabled })); + + // Push Unknown Warning field + } else { + const warningNumber = i - warningCount + 1; + d.warnings.push({ + name: 'UNKNOWN', + text: ['osdWarningTextUnknown', warningNumber], + desc: 'osdWarningUnknown', + enabled, + }); + } } - d.displayItems.push($.extend({ - name: c.name, - text: suffix ? [c.text, suffix] : c.text, - desc: c.desc, - index: j, - draw_order: c.draw_order, - preview: suffix ? c.preview + suffix : c.preview, - ignoreSize, - }, this.helpers.unpack.position(item, c))); } - // Generate OSD element previews and positionable that are defined by a function - for (const item of d.displayItems) { - if (typeof (item.preview) === 'function') { - item.preview = item.preview(d); - } - } + this.processOsdElements(OSD.data, OSD.virtualMode.itemPositions); OSD.updateDisplaySize(); }, @@ -2235,6 +2332,10 @@ TABS.osd.initialize = function(callback) { GUI.active_tab = 'osd'; } + if (CONFIGURATOR.virtualMode) { + VirtualFC.setupVirtualOSD(); + } + $('#content').load("./tabs/osd.html", function() { // Prepare symbols depending on the version SYM.loadSymbols(); @@ -2325,7 +2426,11 @@ TABS.osd.initialize = function(callback) { OSD.chooseFields(); - OSD.msp.decode(info); + if (CONFIGURATOR.virtualMode) { + OSD.msp.decodeVirtual(); + } else { + OSD.msp.decode(info); + } if (OSD.data.state.haveMax7456FontDeviceConfigured && !OSD.data.state.isMax7456FontDeviceDetected) { $('.noOsdChipDetect').show(); diff --git a/src/js/tabs/setup.js b/src/js/tabs/setup.js index 7d38b553..4c87cff0 100644 --- a/src/js/tabs/setup.js +++ b/src/js/tabs/setup.js @@ -29,13 +29,20 @@ TABS.setup.initialize = function (callback) { // translate to user-selected language i18n.localizePage(); + const backupButton = $('#content .backup'); + if (semver.lt(FC.CONFIG.apiVersion, CONFIGURATOR.API_VERSION_MIN_SUPPORTED_BACKUP_RESTORE)) { - $('#content .backup').addClass('disabled'); + backupButton.addClass('disabled'); $('#content .restore').addClass('disabled'); GUI.log(i18n.getMessage('initialSetupBackupAndRestoreApiVersion', [FC.CONFIG.apiVersion, CONFIGURATOR.API_VERSION_MIN_SUPPORTED_BACKUP_RESTORE])); } + // saving and uploading an imaginary config to hardware is a bad idea + if (CONFIGURATOR.virtualMode) { + backupButton.addClass('disabled'); + } + // initialize 3D Model self.initModel(); @@ -157,7 +164,7 @@ TABS.setup.initialize = function (callback) { console.log(`YAW reset to 0 deg, fix: ${self.yaw_fix} deg`); }); - $('#content .backup').click(function () { + backupButton.click(function () { if ($(this).hasClass('disabled')) { return; } diff --git a/src/js/utils/common.js b/src/js/utils/common.js index bd4b6061..96168256 100644 --- a/src/js/utils/common.js +++ b/src/js/utils/common.js @@ -42,3 +42,34 @@ function bytesToSize(bytes) { } return false; } + +const majorFirmwareVersions = { + '1.43': '4.2.*', + '1.42': '4.1.*', + '1.41': '4.0.*', + '1.40': '3.5.*', + '1.39': '3.4.*', + '1.37': '3.3.0', + '1.36': '3.2.*', + '1.31': '3.1.0', +}; + +function generateVirtualApiVersions() { + const firmwareVersionDropdown = document.getElementById('firmware-version-dropdown'); + const max = semver.minor(CONFIGURATOR.API_VERSION_MAX_SUPPORTED); + + for (let i = max; i > 0; i--) { + const option = document.createElement("option"); + const verNum = `1.${i}`; + option.value = `${verNum}.0`; + option.text = `MSP: ${verNum} `; + + if (majorFirmwareVersions.hasOwnProperty(verNum)) { + option.text += ` | Firmware: ${majorFirmwareVersions[verNum]}`; + } else if (i === max) { + option.text += ` | Latest Firmware`; + } + + firmwareVersionDropdown.appendChild(option); + } +} diff --git a/src/main.html b/src/main.html index 6797919f..df0b38fd 100644 --- a/src/main.html +++ b/src/main.html @@ -76,6 +76,7 @@ + @@ -165,6 +166,11 @@ +