diff --git a/android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.java b/android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.java new file mode 100644 index 00000000000..3b63b4b122d --- /dev/null +++ b/android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.java @@ -0,0 +1,327 @@ +package betaflight.configurator.protocols.tcp; + +import android.util.Base64; +import android.util.Log; +import com.getcapacitor.JSObject; +import com.getcapacitor.Plugin; +import com.getcapacitor.PluginCall; +import com.getcapacitor.PluginMethod; +import com.getcapacitor.annotation.CapacitorPlugin; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Capacitor plugin that provides raw TCP socket functionality with thread safety, + * robust resource management, and comprehensive error handling. + */ +@CapacitorPlugin(name = "BetaflightTcp") +public class BetaflightTcpPlugin extends Plugin { + private static final String TAG = "BetaflightTcp"; + + // Error messages + private static final String ERROR_IP_REQUIRED = "IP address is required"; + private static final String ERROR_INVALID_PORT = "Invalid port number"; + private static final String ERROR_ALREADY_CONNECTED = "Already connected; please disconnect first"; + private static final String ERROR_NOT_CONNECTED = "Not connected to any server"; + private static final String ERROR_DATA_REQUIRED = "Data is required"; + private static final String ERROR_CONNECTION_LOST = "Connection lost"; + private static final String ERROR_CONNECTION_CLOSED = "Connection closed by peer"; + + // Connection settings + private static final int DEFAULT_TIMEOUT_MS = 30_000; + private static final int MIN_PORT = 1; + private static final int MAX_PORT = 65535; + + private enum ConnectionState { + DISCONNECTED, + CONNECTING, + CONNECTED, + DISCONNECTING, + ERROR + } + + // Thread-safe state and locks + private final AtomicReference state = new AtomicReference<>(ConnectionState.DISCONNECTED); + private final ReentrantLock socketLock = new ReentrantLock(); + private final ReentrantLock writerLock = new ReentrantLock(); + + private Socket socket; + private InputStream input; + private OutputStream output; + private Thread readerThread; + private volatile boolean readerRunning = false; + + @PluginMethod + public void connect(final PluginCall call) { + call.setKeepAlive(true); + final String ip = call.getString("ip"); + + Integer portObj = call.getInt("port"); + final int port = (portObj != null) ? portObj : -1; + + if (ip == null || ip.isEmpty()) { + call.reject(ERROR_IP_REQUIRED); + call.setKeepAlive(false); + return; + } + + if (!compareAndSetState(ConnectionState.DISCONNECTED, ConnectionState.CONNECTING)) { + call.reject(ERROR_ALREADY_CONNECTED); + call.setKeepAlive(false); + return; + } + + + new Thread(() -> { + socketLock.lock(); + try { + socket = new Socket(); + InetSocketAddress address = new InetSocketAddress(ip, port); + socket.connect(address, DEFAULT_TIMEOUT_MS); + socket.setSoTimeout(DEFAULT_TIMEOUT_MS); + + input = socket.getInputStream(); + output = socket.getOutputStream(); + + state.set(ConnectionState.CONNECTED); + JSObject result = new JSObject(); + result.put("success", true); + call.resolve(result); + Log.d(TAG, "Connected to " + ip + (port != -1 ? (":" + port) : "")); + + startReaderThread(); + } catch (Exception e) { + state.set(ConnectionState.ERROR); + closeResourcesInternal(); + state.set(ConnectionState.DISCONNECTED); + call.reject("Connection failed: " + e.getMessage()); + Log.e(TAG, "Connection failed", e); + } finally { + socketLock.unlock(); + call.setKeepAlive(false); + } + }).start(); + } + + @PluginMethod + public void send(final PluginCall call) { + String data = call.getString("data"); + if (data == null || data.isEmpty()) { + call.reject(ERROR_DATA_REQUIRED); + return; + } + if (state.get() != ConnectionState.CONNECTED) { + call.reject(ERROR_NOT_CONNECTED); + return; + } + call.setKeepAlive(true); + + new Thread(() -> { + writerLock.lock(); + try { + if (output == null || state.get() != ConnectionState.CONNECTED) { + call.reject(ERROR_CONNECTION_LOST); + return; + } + byte[] payload = Base64.decode(data, Base64.NO_WRAP); + output.write(payload); + output.flush(); + + JSObject result = new JSObject(); + result.put("success", true); + call.resolve(result); + Log.d(TAG, "Sent " + payload.length + " bytes"); + } catch (Exception e) { + handleCommunicationError(e, "Send failed", call); + } finally { + writerLock.unlock(); + call.setKeepAlive(false); + } + }).start(); + } + + @PluginMethod + public void receive(final PluginCall call) { + // Deprecated by continuous reader (Task 2) + JSObject result = new JSObject(); + result.put("data", ""); + call.reject("Continuous read active. Listen for 'dataReceived' events instead."); + } + + @PluginMethod + public void disconnect(final PluginCall call) { + ConnectionState current = state.get(); + if (current == ConnectionState.DISCONNECTED) { + JSObject result = new JSObject(); + result.put("success", true); + call.resolve(result); + return; + } + if (!compareAndSetState(current, ConnectionState.DISCONNECTING)) { + call.reject("Invalid state for disconnect: " + current); + return; + } + call.setKeepAlive(true); + + new Thread(() -> { + socketLock.lock(); + try { + closeResourcesInternal(); + state.set(ConnectionState.DISCONNECTED); + JSObject result = new JSObject(); + result.put("success", true); + call.resolve(result); + Log.d(TAG, "Disconnected successfully"); + } catch (Exception e) { + state.set(ConnectionState.ERROR); + // Ensure cleanup completes even on error + try { + closeResourcesInternal(); + } catch (Exception ce) { + Log.e(TAG, "Cleanup error during disconnect", ce); + } + call.reject("Disconnect failed: " + e.getMessage()); + Log.e(TAG, "Disconnect failed", e); + // Reset to a clean disconnected state after handling error + state.set(ConnectionState.DISCONNECTED); + } finally { + socketLock.unlock(); + call.setKeepAlive(false); + } + }).start(); + } + + @PluginMethod + public void getStatus(final PluginCall call) { + JSObject result = new JSObject(); + result.put("connected", state.get() == ConnectionState.CONNECTED); + result.put("state", state.get().toString()); + call.resolve(result); + } + + @Override + protected void handleOnDestroy() { + socketLock.lock(); + try { + state.set(ConnectionState.DISCONNECTING); + closeResourcesInternal(); + state.set(ConnectionState.DISCONNECTED); + } catch (Exception e) { + Log.e(TAG, "Error cleaning up resources on destroy", e); + } finally { + socketLock.unlock(); + } + super.handleOnDestroy(); + } + + private void startReaderThread() { + if (readerThread != null && readerThread.isAlive()) return; + readerRunning = true; + readerThread = new Thread(() -> { + Log.d(TAG, "Reader thread started"); + try { + byte[] buf = new byte[4096]; + while (readerRunning && state.get() == ConnectionState.CONNECTED && input != null) { + int read = input.read(buf); + if (read == -1) { + notifyDisconnectFromPeer(); + break; + } + if (read > 0) { + byte[] chunk = Arrays.copyOf(buf, read); + String b64 = Base64.encodeToString(chunk, Base64.NO_WRAP); + JSObject payload = new JSObject(); + payload.put("data", b64); + notifyListeners("dataReceived", payload); + } + } + } catch (Exception e) { + if (readerRunning) { + Log.e(TAG, "Reader thread error", e); + JSObject err = new JSObject(); + err.put("error", e.getMessage()); + notifyListeners("dataReceivedError", err); + handleCommunicationError(e, "Receive failed", null); + } + } finally { + Log.d(TAG, "Reader thread stopped"); + } + }, "SocketReaderThread"); + readerThread.start(); + } + + private void notifyDisconnectFromPeer() { + Log.d(TAG, "Peer closed connection"); + JSObject evt = new JSObject(); + evt.put("reason", "peer_closed"); + notifyListeners("connectionClosed", evt); + socketLock.lock(); + try { + state.set(ConnectionState.ERROR); + closeResourcesInternal(); + state.set(ConnectionState.DISCONNECTED); + } finally { + socketLock.unlock(); + } + } + + private void stopReaderThread() { + readerRunning = false; + if (readerThread != null) { + try { + readerThread.interrupt(); + readerThread.join(500); + } catch (InterruptedException ignored) {} + readerThread = null; + } + } + + private void closeResourcesInternal() { + stopReaderThread(); + if (input != null) { try { input.close(); } catch (IOException e) { Log.e(TAG, "Error closing input stream", e); } finally { input = null; } } + if (output != null) { try { output.close(); } catch (IOException e) { Log.e(TAG, "Error closing output stream", e); } finally { output = null; } } + if (socket != null) { try { socket.close(); } catch (IOException e) { Log.e(TAG, "Error closing socket", e); } finally { socket = null; } } + } + + private void handleCommunicationError(Exception error, String message, PluginCall call) { + socketLock.lock(); + try { + state.set(ConnectionState.ERROR); + closeResourcesInternal(); + state.set(ConnectionState.DISCONNECTED); + + String fullMsg = message + ": " + (error != null ? error.getMessage() : "unknown error"); + if (call != null) { + call.reject(fullMsg); + } else { + // No PluginCall available (e.g., background reader thread). Log the error. + Log.e(TAG, fullMsg, error); + // Optionally notify listeners (commented to avoid duplicate notifications): + // JSObject err = new JSObject(); + // err.put("error", fullMsg); + // notifyListeners("socketError", err); + } + Log.e(TAG, message, error); + } finally { + socketLock.unlock(); + } + } + + private boolean compareAndSetState(ConnectionState expected, ConnectionState newState) { + return state.compareAndSet(expected, newState); + } + + private String truncateForLog(String data) { + if (data == null) return "null"; + final int maxLen = 100; + if (data.length() <= maxLen) return data; + return data.substring(0, maxLen) + "... (" + data.length() + " chars)"; + } +} diff --git a/locales/en/messages.json b/locales/en/messages.json index ba6c71a66ed..95a3d9cf180 100755 --- a/locales/en/messages.json +++ b/locales/en/messages.json @@ -1420,6 +1420,9 @@ "configurationFeatures": { "message": "Other Features" }, + "configurationFeatureHelp": { + "message": "Help" + }, "configurationReceiver": { "message": "Receiver" }, @@ -1450,8 +1453,14 @@ "configurationSpiRxHelp": { "message": "Note: The SPI RX provider will only work if the required hardware is on board or connected to an SPI bus." }, - "configurationOtherFeaturesHelp": { - "message": "Note: Not all features are supported by all flight controllers. If you enable a specific feature, and it is disabled after you hit 'Save and Reboot', it means that this feature is not supported on your board." + "configurationActiveImu": { + "message": "Active IMU" + }, + "configurationSensors": { + "message": "Sensor Configuration" + }, + "configurationSensorsHelp": { + "message": "Configure additional sensors and advanced features." }, "configurationMagAlignmentHelp": { "message": "The magnetometer alignment is the orientation of the magnetometer sensor on the flight controller board. The default is usually correct, but if you have a custom build or a flight controller with a different magnetometer orientation, you may need to adjust this setting." @@ -1540,6 +1549,30 @@ "message": "$t(configurationBoardAlignmentYaw.message)", "description": "Don't translate!!!" }, + "configurationGyro1AlignmentRoll": { + "message": "$t(configurationBoardAlignmentRoll.message)", + "description": "Don't translate!!!" + }, + "configurationGyro1AlignmentPitch": { + "message": "$t(configurationBoardAlignmentPitch.message)", + "description": "Don't translate!!!" + }, + "configurationGyro1AlignmentYaw": { + "message": "$t(configurationBoardAlignmentYaw.message)", + "description": "Don't translate!!!" + }, + "configurationGyro2AlignmentRoll": { + "message": "$t(configurationBoardAlignmentRoll.message)", + "description": "Don't translate!!!" + }, + "configurationGyro2AlignmentPitch": { + "message": "$t(configurationBoardAlignmentPitch.message)", + "description": "Don't translate!!!" + }, + "configurationGyro2AlignmentYaw": { + "message": "$t(configurationBoardAlignmentYaw.message)", + "description": "Don't translate!!!" + }, "configurationSensorAlignmentGyro": { "message": "GYRO Alignment" }, @@ -1576,6 +1609,10 @@ "configurationSensorAlignmentDefaultOption": { "message": "Default" }, + "configurationSensorAlignmentCustom": { + "message": "Custom" + }, + "configurationAccelTrims": { "message": "Accelerometer Trim" }, @@ -1606,6 +1643,9 @@ "configurationGyroCalOnFirstArm": { "message": "Calibrate Gyro on first arm" }, + "configurationGyroCalOnFirstArmHelp": { + "message": "When enabled, the gyroscope will be calibrated on the first arm after power on. This can help reduce gyro drift during flight." + }, "configurationMotorIdle": { "message": "Motor Idle (%)" }, @@ -7113,7 +7153,8 @@ "message": "Sends ESC data to the FC via DShot telemetry. Required by RPM Filtering and dynamic idle.

Note: Requires a compatible ESC with appropriate firmware, eg JESC, Jazzmac, BLHeli-32.", "description": "Description of the Bidirectional DShot feature of the ESC/Motor" }, - "configurationGyroSyncDenom": { + + "configurationGyroFrequency": { "message": "Gyro update frequency" }, "configurationPidProcessDenom": { diff --git a/src/components/tabs/ConfigurationTab.vue b/src/components/tabs/ConfigurationTab.vue new file mode 100644 index 00000000000..b0d025e193e --- /dev/null +++ b/src/components/tabs/ConfigurationTab.vue @@ -0,0 +1,1580 @@ + + + + + diff --git a/src/css/tabs/configuration.less b/src/css/tabs/configuration.less index 27a62dd8f73..aebdc895310 100644 --- a/src/css/tabs/configuration.less +++ b/src/css/tabs/configuration.less @@ -27,8 +27,12 @@ gap: 0.5rem; .sensor_align_inputs { display: flex; + align-items: center; label { white-space: nowrap; + display: flex; + align-items: center; + gap: 0.5rem; } } } @@ -75,8 +79,16 @@ padding-right: 0; } - thead { - display: none; + .visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; } .beeper-controls { diff --git a/src/js/fc.js b/src/js/fc.js index e761a5ec0de..1a906b69a8e 100644 --- a/src/js/fc.js +++ b/src/js/fc.js @@ -479,8 +479,20 @@ const FC = { align_mag: 0, gyro_detection_flags: 0, gyro_to_use: 0, + gyro_1_align: 0, gyro_2_align: 0, + gyro_1_align_roll: 0, + gyro_1_align_pitch: 0, + gyro_1_align_yaw: 0, + gyro_2_align_roll: 0, + gyro_2_align_pitch: 0, + gyro_2_align_yaw: 0, + gyro_align: [], // API 1.47+ + gyro_enable_mask: 0, // API 1.47+ + gyro_align_roll: [], // API 1.47+ + gyro_align_pitch: [], // API 1.47+ + gyro_align_yaw: [], // API 1.47+ mag_align_roll: 0, mag_align_pitch: 0, mag_align_yaw: 0, diff --git a/src/js/gui.js b/src/js/gui.js index e68cc5ef76f..83735863cb9 100644 --- a/src/js/gui.js +++ b/src/js/gui.js @@ -255,6 +255,7 @@ class GuiControl { const COLOR_SWITCHERY_SECOND = "var(--switcherysecond)"; $(".togglesmall").each(function (index, elem) { + if ($(elem).next(".switchery").length) return; const switchery = new Switchery(elem, { size: "small", color: COLOR_ACCENT, @@ -267,6 +268,7 @@ class GuiControl { }); $(".toggle").each(function (index, elem) { + if ($(elem).next(".switchery").length) return; const switchery = new Switchery(elem, { color: COLOR_ACCENT, secondaryColor: COLOR_SWITCHERY_SECOND, @@ -278,6 +280,7 @@ class GuiControl { }); $(".togglemedium").each(function (index, elem) { + if ($(elem).next(".switchery").length) return; const switchery = new Switchery(elem, { className: "switcherymid", color: COLOR_ACCENT, diff --git a/src/js/main.js b/src/js/main.js index 847c2023c6b..306c65046dc 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -13,7 +13,7 @@ import CliAutoComplete from "./CliAutoComplete.js"; import DarkTheme, { setDarkTheme } from "./DarkTheme.js"; import { isExpertModeEnabled } from "./utils/isExpertModeEnabled.js"; import { updateTabList } from "./utils/updateTabList.js"; -import { mountVueTab } from "./vue_tab_mounter.js"; +import { mountVueTab, unmountVueTab } from "./vue_tab_mounter.js"; import * as THREE from "three"; import NotificationManager from "./utils/notifications.js"; @@ -27,12 +27,6 @@ import("./msp/debug/msp_debug_tools.js") console.warn("Failed to load MSP debug tools:", err); }); -if (typeof String.prototype.replaceAll === "undefined") { - String.prototype.replaceAll = function (match, replace) { - return this.replace(new RegExp(match, "g"), () => replace); - }; -} - $(document).ready(function () { appReady(); }); @@ -215,6 +209,7 @@ function startProcess() { // detach listeners and remove element data const content = $("#content"); + unmountVueTab(); content.empty(); // display loading screen @@ -290,9 +285,7 @@ function startProcess() { import("./tabs/setup_osd").then(({ setup_osd }) => setup_osd.initialize(content_ready)); break; case "configuration": - import("./tabs/configuration").then(({ configuration }) => - configuration.initialize(content_ready), - ); + mountVueTab("configuration", content_ready); break; case "pid_tuning": import("./tabs/pid_tuning").then(({ pid_tuning }) => pid_tuning.initialize(content_ready)); diff --git a/src/js/msp/MSPHelper.js b/src/js/msp/MSPHelper.js index 0d442aa42d8..0e261938981 100644 --- a/src/js/msp/MSPHelper.js +++ b/src/js/msp/MSPHelper.js @@ -639,14 +639,42 @@ MspHelper.prototype.process_data = function (dataHandler) { FC.SENSOR_ALIGNMENT.gyro_detection_flags = data.readU8(); if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47)) { - FC.SENSOR_ALIGNMENT.gyro_enable_mask = data.readU8(); // replacing gyro_to_use - FC.SENSOR_ALIGNMENT.mag_align_roll = data.read16() / 10; - FC.SENSOR_ALIGNMENT.mag_align_pitch = data.read16() / 10; - FC.SENSOR_ALIGNMENT.mag_align_yaw = data.read16() / 10; + FC.SENSOR_ALIGNMENT.gyro_enable_mask = data.readU8(); + + // Initialize arrays + FC.SENSOR_ALIGNMENT.gyro_align = []; + FC.SENSOR_ALIGNMENT.gyro_align_roll = []; + FC.SENSOR_ALIGNMENT.gyro_align_pitch = []; + FC.SENSOR_ALIGNMENT.gyro_align_yaw = []; + + // Read 8 gyros + for (let i = 0; i < 8; i++) { + FC.SENSOR_ALIGNMENT.gyro_align.push(data.readU8()); + FC.SENSOR_ALIGNMENT.gyro_align_roll.push(data.read16() / 10); + FC.SENSOR_ALIGNMENT.gyro_align_pitch.push(data.read16() / 10); + FC.SENSOR_ALIGNMENT.gyro_align_yaw.push(data.read16() / 10); + } + + // Read Mag alignment if data remains + if (data.byteLength - data.offset >= 6) { + FC.SENSOR_ALIGNMENT.mag_align_roll = data.read16() / 10; + FC.SENSOR_ALIGNMENT.mag_align_pitch = data.read16() / 10; + FC.SENSOR_ALIGNMENT.mag_align_yaw = data.read16() / 10; + } } else { FC.SENSOR_ALIGNMENT.gyro_to_use = data.readU8(); FC.SENSOR_ALIGNMENT.gyro_1_align = data.readU8(); FC.SENSOR_ALIGNMENT.gyro_2_align = data.readU8(); + + // Check if we have enough data for custom alignment values (2 gyros * 3 axes * 2 bytes = 12 bytes) + if (data.byteLength - data.offset >= 12) { + FC.SENSOR_ALIGNMENT.gyro_1_align_roll = data.read16() / 10; + FC.SENSOR_ALIGNMENT.gyro_1_align_pitch = data.read16() / 10; + FC.SENSOR_ALIGNMENT.gyro_1_align_yaw = data.read16() / 10; + FC.SENSOR_ALIGNMENT.gyro_2_align_roll = data.read16() / 10; + FC.SENSOR_ALIGNMENT.gyro_2_align_pitch = data.read16() / 10; + FC.SENSOR_ALIGNMENT.gyro_2_align_yaw = data.read16() / 10; + } } break; @@ -2112,8 +2140,17 @@ MspHelper.prototype.crunch = function (code, modifierCode = undefined) { .push8(FC.SENSOR_ALIGNMENT.align_mag); if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47)) { + buffer.push8(FC.SENSOR_ALIGNMENT.gyro_enable_mask); + + for (let i = 0; i < 8; i++) { + buffer + .push8(FC.SENSOR_ALIGNMENT.gyro_align[i]) + .push16(FC.SENSOR_ALIGNMENT.gyro_align_roll[i] * 10) + .push16(FC.SENSOR_ALIGNMENT.gyro_align_pitch[i] * 10) + .push16(FC.SENSOR_ALIGNMENT.gyro_align_yaw[i] * 10); + } + buffer - .push8(FC.SENSOR_ALIGNMENT.gyro_enable_mask) // replacing gyro_to_use .push16(FC.SENSOR_ALIGNMENT.mag_align_roll * 10) .push16(FC.SENSOR_ALIGNMENT.mag_align_pitch * 10) .push16(FC.SENSOR_ALIGNMENT.mag_align_yaw * 10); @@ -2121,7 +2158,13 @@ MspHelper.prototype.crunch = function (code, modifierCode = undefined) { buffer .push8(FC.SENSOR_ALIGNMENT.gyro_to_use) .push8(FC.SENSOR_ALIGNMENT.gyro_1_align) - .push8(FC.SENSOR_ALIGNMENT.gyro_2_align); + .push8(FC.SENSOR_ALIGNMENT.gyro_2_align) + .push16(FC.SENSOR_ALIGNMENT.gyro_1_align_roll * 10) + .push16(FC.SENSOR_ALIGNMENT.gyro_1_align_pitch * 10) + .push16(FC.SENSOR_ALIGNMENT.gyro_1_align_yaw * 10) + .push16(FC.SENSOR_ALIGNMENT.gyro_2_align_roll * 10) + .push16(FC.SENSOR_ALIGNMENT.gyro_2_align_pitch * 10) + .push16(FC.SENSOR_ALIGNMENT.gyro_2_align_yaw * 10); } break; diff --git a/src/js/serial_backend.js b/src/js/serial_backend.js index 603ae772c27..33177031765 100644 --- a/src/js/serial_backend.js +++ b/src/js/serial_backend.js @@ -29,8 +29,12 @@ import { ispConnected } from "./utils/connection"; const logHead = "[SERIAL-BACKEND]"; +export const REBOOT_CONNECT_MAX_TIME_MS = 60000; +export const REBOOT_GRACE_PERIOD_MS = 1500; + let mspHelper; let connectionTimestamp = null; +let rebootTimestamp = 0; let liveDataRefreshTimerId = false; let isConnected = false; @@ -63,7 +67,7 @@ export function initializeSerialBackend() { PortHandler.portPicker.autoConnect && !isCliOnlyMode() && (connectionTimestamp === null || connectionTimestamp > 0)) || - Date.now() - GUI.reboot_timestamp <= GUI.REBOOT_CONNECT_MAX_TIME_MS + Date.now() - rebootTimestamp <= REBOOT_CONNECT_MAX_TIME_MS ) { connectDisconnect(); } @@ -124,13 +128,6 @@ function connectDisconnect() { return; } - // When rebooting, adhere to the auto-connect setting - const REBOOT_GRACE_PERIOD_MS = 2000; - if (!PortHandler.portPicker.autoConnect && Date.now() - GUI.reboot_timestamp < REBOOT_GRACE_PERIOD_MS) { - console.log(`${logHead} Rebooting, not connecting`); - return; - } - const portName = selectedPort === "manual" ? PortHandler.portPicker.portOverride : selectedPort; console.log(`${logHead} Connecting to: ${portName}`); @@ -605,7 +602,6 @@ function finishOpen() { onConnect(); GUI.selectDefaultTabWhenConnected(); - GUI.reboot_timestamp = 0; } function connectCli() { @@ -787,3 +783,140 @@ function startLiveDataRefreshTimer() { clearLiveDataRefreshTimer(); liveDataRefreshTimerId = setInterval(update_live_status, 250); } + +export function reinitializeConnection() { + if (CONFIGURATOR.virtualMode) { + connectDisconnect(); + if (PortHandler.portPicker.autoConnect) { + return setTimeout(function () { + $("a.connection_button__link").trigger("click"); + }, 500); + } + } + + const currentPort = PortHandler.portPicker.selectedPort; + + rebootTimestamp = Date.now(); + + // Send reboot command to the flight controller + MSP.send_message(MSPCodes.MSP_SET_REBOOT, false, false); + + // Force connection invalid to ensure reboot dialog waits for reconnection + CONFIGURATOR.connectionValid = false; + + if (currentPort.startsWith("bluetooth") || currentPort === "manual") { + if (!PortHandler.portPicker.autoConnect) { + return setTimeout(function () { + $("a.connection_button__link").trigger("click"); + }, REBOOT_GRACE_PERIOD_MS); + } + } + + // Show reboot progress modal + showRebootDialog(); +} + +function showRebootDialog() { + gui_log(i18n.getMessage("deviceRebooting")); + + // Show reboot progress modal + const rebootDialog = document.getElementById("rebootProgressDialog") || createRebootProgressDialog(); + rebootDialog.querySelector(".reboot-progress-bar").style.width = "0%"; + rebootDialog.querySelector(".reboot-status").textContent = i18n.getMessage("rebootFlightController"); + rebootDialog.showModal(); + + // Update progress during reboot + let progress = 0; + const progressInterval = setInterval(() => { + progress += 5; + if (progress <= 100) { + rebootDialog.querySelector(".reboot-progress-bar").style.width = `${progress}%`; + } + }, 100); + + // Check for successful connection every 100ms with a timeout + const connectionCheckInterval = setInterval(() => { + const connectionCheckTimeoutReached = Date.now() - rebootTimestamp > REBOOT_CONNECT_MAX_TIME_MS; + const noSerialReconnect = !PortHandler.portPicker.autoConnect && PortHandler.portAvailable; + + if (CONFIGURATOR.connectionValid || connectionCheckTimeoutReached || noSerialReconnect) { + clearInterval(connectionCheckInterval); + clearInterval(progressInterval); + + rebootDialog.querySelector(".reboot-progress-bar").style.width = "100%"; + rebootDialog.querySelector(".reboot-status").textContent = i18n.getMessage("rebootFlightControllerReady"); + + // Close the dialog after showing "ready" message briefly + setTimeout(() => { + rebootDialog.close(); + }, 1000); + + if (connectionCheckTimeoutReached) { + console.log(`${logHead} Reboot timeout reached`); + } else { + gui_log(i18n.getMessage("deviceReady")); + } + } + }, 100); + + // Helper function to create the reboot dialog if it doesn't exist + function createRebootProgressDialog() { + const dialog = document.createElement("dialog"); + dialog.id = "rebootProgressDialog"; + dialog.className = "dialogReboot"; + + dialog.innerHTML = ` +
+
${i18n.getMessage("rebootFlightController")}
+
+
+
+
+ `; + + document.body.appendChild(dialog); + + // Add styles if not already defined + if (!document.getElementById("rebootProgressStyle")) { + const style = document.createElement("style"); + style.id = "rebootProgressStyle"; + style.textContent = ` + .dialogReboot { + border: 1px solid #3f4241; + border-radius: 5px; + background-color: #2d3233; + color: #fff; + padding: 20px; + max-width: 400px; + } + .reboot-progress-container { + width: 100%; + background-color: #424546; + border-radius: 3px; + margin: 15px 0 5px; + height: 10px; + } + .reboot-progress-bar { + height: 100%; + background-color: #ffbb00; + border-radius: 3px; + transition: width 0.1s ease-in-out; + width: 0%; + } + .reboot-status { + text-align: center; + margin: 10px 0; + } + `; + document.head.appendChild(style); + } + + return dialog; + } +} + +/** + * Re-initializes the connection, typically after a reboot or configuration change. + * Exposed as a public API for Vue components. + */ +GUI.reinitializeConnection = reinitializeConnection; diff --git a/src/js/tabs/configuration.js b/src/js/tabs/configuration.js deleted file mode 100644 index 3f938f04d52..00000000000 --- a/src/js/tabs/configuration.js +++ /dev/null @@ -1,710 +0,0 @@ -import semver from "semver"; -import { i18n } from "../localization"; -import GUI, { TABS } from "../gui"; -import { tracking } from "../Analytics"; -import { mspHelper } from "../msp/MSPHelper"; -import FC from "../fc"; -import MSP from "../msp"; -import MSPCodes from "../msp/MSPCodes"; -import { API_VERSION_1_45, API_VERSION_1_46, API_VERSION_1_47 } from "../data_storage"; -import { updateTabList } from "../utils/updateTabList"; -import $ from "jquery"; -import { have_sensor } from "../sensor_helpers"; -import { sensorTypes } from "../sensor_types"; -import { gui_log } from "../gui_log"; - -const configuration = { - analyticsChanges: {}, -}; - -const SENSOR_ALIGNMENTS = [ - "CW 0°", - "CW 90°", - "CW 180°", - "CW 270°", - "CW 0° flip", - "CW 90° flip", - "CW 180° flip", - "CW 270° flip", - "Custom", -]; - -const MAX_GYROS = 8; // Maximum number of gyros supported - -configuration.initialize = function (callback) { - const self = this; - - if (GUI.active_tab != "configuration") { - GUI.active_tab = "configuration"; - GUI.configuration_loaded = true; - } - - function load_serial_config() { - mspHelper.loadSerialConfig(load_config); - } - - function load_config() { - Promise.resolve(true) - .then(() => MSP.promise(MSPCodes.MSP_FEATURE_CONFIG)) - .then(() => MSP.promise(MSPCodes.MSP_BEEPER_CONFIG)) - .then(() => MSP.promise(MSPCodes.MSP_BOARD_ALIGNMENT_CONFIG)) - .then(() => MSP.promise(MSPCodes.MSP_ACC_TRIM)) - .then(() => MSP.promise(MSPCodes.MSP_ARMING_CONFIG)) - .then(() => MSP.promise(MSPCodes.MSP_RC_DEADBAND)) - .then(() => MSP.promise(MSPCodes.MSP_SENSOR_CONFIG)) - .then(() => MSP.promise(MSPCodes.MSP_SENSOR_ALIGNMENT)) - .then(() => - semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_45) - ? MSP.promise(MSPCodes.MSP_NAME) - : Promise.resolve(true), - ) - .then(() => - semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45) - ? MSP.promise(MSPCodes.MSP2_GET_TEXT, mspHelper.crunch(MSPCodes.MSP2_GET_TEXT, MSPCodes.CRAFT_NAME)) - : Promise.resolve(true), - ) - .then(() => MSP.promise(MSPCodes.MSP_RX_CONFIG)) - .then(() => - semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45) - ? MSP.promise(MSPCodes.MSP2_GET_TEXT, mspHelper.crunch(MSPCodes.MSP2_GET_TEXT, MSPCodes.PILOT_NAME)) - : Promise.resolve(true), - ) - .then(() => MSP.promise(MSPCodes.MSP_ADVANCED_CONFIG)) - .then(() => - semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_46) && FC.checkBuildOption("USE_MAG") - ? MSP.promise(MSPCodes.MSP_COMPASS_CONFIG) - : Promise.resolve(true), - ) - .then(() => - semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47) - ? MSP.promise(MSPCodes.MSP2_GYRO_SENSOR) - : Promise.resolve(true), - ) - .then(() => load_html()); - } - - function load_html() { - $("#content").load("./tabs/configuration.html", process_html); - } - - load_serial_config(); - - function process_html() { - self.analyticsChanges = {}; - - const features_e = $(".tab-configuration .features"); - - FC.FEATURE_CONFIG.features.generateElements(features_e); - - // Dshot Beeper - const dshotBeeper_e = $(".tab-configuration .dshotbeeper"); - const dshotBeeperBeaconTone = $("select.dshotBeeperBeaconTone"); - const dshotBeaconCondition_e = $("tbody.dshotBeaconConditions"); - const dshotBeaconSwitch_e = $("tr.dshotBeaconSwitch"); - - for (let i = 1; i <= 5; i++) { - dshotBeeperBeaconTone.append(``); - } - dshotBeeper_e.show(); - - dshotBeeperBeaconTone.change(function () { - FC.BEEPER_CONFIG.dshotBeaconTone = dshotBeeperBeaconTone.val(); - }); - - dshotBeeperBeaconTone.val(FC.BEEPER_CONFIG.dshotBeaconTone); - - const template = $(".beepers .beeper-template"); - dshotBeaconSwitch_e.hide(); - FC.BEEPER_CONFIG.dshotBeaconConditions.generateElements(template, dshotBeaconCondition_e); - - $("input.condition", dshotBeaconCondition_e).change(function () { - const element = $(this); - FC.BEEPER_CONFIG.dshotBeaconConditions.updateData(element); - }); - - // DShot Beeper toggle all buttons - $(".dshot-beeper-enable-all").click(function () { - $("input.condition", dshotBeaconCondition_e).each(function () { - $(this).prop("checked", true).trigger("change"); - }); - }); - - $(".dshot-beeper-disable-all").click(function () { - $("input.condition", dshotBeaconCondition_e).each(function () { - $(this).prop("checked", false).trigger("change"); - }); - }); - - // Analog Beeper - const destination = $(".beepers .beeper-configuration"); - const beeper_e = $(".tab-configuration .beepers"); - - FC.BEEPER_CONFIG.beepers.generateElements(template, destination); - - // translate to user-selected language - i18n.localizePage(); - - // Gyro and PID update - const gyroTextElement = $("input.gyroFrequency"); - const gyroSelectElement = $("select.gyroSyncDenom"); - const pidSelectElement = $("select.pidProcessDenom"); - - function addDenomOption(element, denom, baseFreq) { - let denomDescription; - if (baseFreq === 0) { - denomDescription = i18n.getMessage("configurationSpeedPidNoGyro", { value: denom }); - } else { - denomDescription = i18n.getMessage("configurationKHzUnitLabel", { - value: (baseFreq / denom).toFixed(2), - }); - } - element.append(``); - } - - const updateGyroDenomReadOnly = function (gyroFrequency) { - gyroSelectElement.hide(); - - let gyroContent; - if (gyroFrequency === 0) { - gyroContent = i18n.getMessage("configurationSpeedGyroNoGyro"); - } else { - gyroContent = i18n.getMessage("configurationKHzUnitLabel", { - value: (gyroFrequency / 1000).toFixed(2), - }); - } - gyroTextElement.val(gyroContent); - }; - - $("div.gyroUse32kHz").hide(); - - updateGyroDenomReadOnly(FC.CONFIG.sampleRateHz); - - gyroSelectElement.val(FC.PID_ADVANCED_CONFIG.gyro_sync_denom); - - $(".systemconfigNote").html(i18n.getMessage("configurationLoopTimeHelp")); - - gyroSelectElement - .change(function () { - const originalPidDenom = parseInt(pidSelectElement.val()); - const pidBaseFreq = FC.CONFIG.sampleRateHz / 1000; - const MAX_DENOM = 8; - - pidSelectElement.empty(); - - for (let denom = 1; denom <= MAX_DENOM; denom++) { - addDenomOption(pidSelectElement, denom, pidBaseFreq); - } - - pidSelectElement.val(originalPidDenom); - }) - .change(); - - pidSelectElement.val(FC.PID_ADVANCED_CONFIG.pid_process_denom); - - $('input[id="accHardwareSwitch"]').prop("checked", FC.SENSOR_CONFIG.acc_hardware !== 1); - $('input[id="baroHardwareSwitch"]').prop("checked", FC.SENSOR_CONFIG.baro_hardware !== 1); - $('input[id="magHardwareSwitch"]').prop("checked", FC.SENSOR_CONFIG.mag_hardware !== 1); - - // Only show these sections for supported FW - if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)) { - $('input[name="craftName"]').val(FC.CONFIG.craftName); - $('input[name="pilotName"]').val(FC.CONFIG.pilotName); - } else { - $('input[name="craftName"]').val(FC.CONFIG.name); - $(".pilotName").hide(); - } - - $('input[name="fpvCamAngleDegrees"]').val(FC.RX_CONFIG.fpvCamAngleDegrees); - - // fill board alignment - $('input[name="board_align_roll"]').val(FC.BOARD_ALIGNMENT_CONFIG.roll); - $('input[name="board_align_pitch"]').val(FC.BOARD_ALIGNMENT_CONFIG.pitch); - $('input[name="board_align_yaw"]').val(FC.BOARD_ALIGNMENT_CONFIG.yaw); - - // fill accel trims - $('input[name="roll"]').val(FC.CONFIG.accelerometerTrims[1]); - $('input[name="pitch"]').val(FC.CONFIG.accelerometerTrims[0]); - - $('input[id="configurationSmallAngle"]').val(FC.ARMING_CONFIG.small_angle); - - if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_46)) { - $('input[id="configurationGyroCalOnFirstArm"]').prop( - "checked", - FC.ARMING_CONFIG.gyro_cal_on_first_arm === 1, - ); - - if (FC.FEATURE_CONFIG.features.isEnabled("MOTOR_STOP")) { - $('input[id="configurationAutoDisarmDelay"]').val(FC.ARMING_CONFIG.auto_disarm_delay); - } else { - $('input[id="configurationAutoDisarmDelay"]').parent().parent().hide(); - } - } else { - $('input[id="configurationGyroCalOnFirstArm"]').parent().parent().hide(); - $('input[id="configurationAutoDisarmDelay"]').parent().parent().hide(); - } - - // Multi gyro handling for newer firmware - if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47)) { - // Hide deprecated gyro_box - $(".tab-configuration .gyro_box").parent().hide(); - - // Define gyro detection flags - const GYRO_DETECTION_FLAGS = { DETECTED_DUAL_GYROS: 1 << 7 }; - let gyroCount = 0; - // Determine how many gyros are detected (bit count) - for (let i = 0; i < MAX_GYROS; i++) { - GYRO_DETECTION_FLAGS[`DETECTED_GYRO_${i + 1}`] = 1 << i; - if ((FC.SENSOR_ALIGNMENT.gyro_detection_flags & (1 << i)) !== 0) { - gyroCount++; - } - } - - // Initialize gyro_enable_mask if needed - if (FC.SENSOR_ALIGNMENT.gyro_enable_mask === undefined) { - FC.SENSOR_ALIGNMENT.gyro_enable_mask = (1 << MAX_GYROS) - 1; // All enabled by default - } else if (FC.SENSOR_ALIGNMENT.gyro_enable_mask === 0) { - // Safety check: if somehow all gyros are disabled, enable the first one - FC.SENSOR_ALIGNMENT.gyro_enable_mask = 1; - } - - if (gyroCount > 1) { - // Track which gyros are detected - const detected_gyros = []; - - function createGyroBox(gyroIndex, container) { - // Create a new gyro alignment div - const gyroBox = $(`
`); - const gyroRow = $(`
`); - const gyroDetected = sensorTypes().gyro.elements[FC.GYRO_SENSOR.gyro_hardware[gyroIndex]]; - - const enableCheck = $(`
- -
`); - - gyroRow.append(enableCheck); - gyroBox.append(gyroRow); - - // Add the box to the container - container.append(gyroBox); - - // Initialize the enable checkbox - enableCheck - .find("input") - .prop("checked", (FC.SENSOR_ALIGNMENT.gyro_enable_mask & (1 << gyroIndex)) !== 0); - - // Add handler for enable/disable checkbox - enableCheck.find("input").on("change", function () { - const checked = $(this).is(":checked"); - - if (checked) { - // Enabling a gyro is always fine - FC.SENSOR_ALIGNMENT.gyro_enable_mask |= 1 << gyroIndex; - } else { - // Safety check: prevent disabling all gyros - const newMask = FC.SENSOR_ALIGNMENT.gyro_enable_mask & ~(1 << gyroIndex); - if (newMask === 0) { - // Prevent the action - keep the checkbox checked - $(this).prop("checked", true); - - // Show an error message to the user - gui_log(i18n.getMessage("configurationGyroRequiredWarning")); - return; - } - - // It's safe to disable this gyro - FC.SENSOR_ALIGNMENT.gyro_enable_mask = newMask; - } - - self.analyticsChanges[`Gyro${gyroIndex + 1}Enable`] = checked; - }); - } - - // For each possible gyro - for (let i = 0; i < MAX_GYROS; i++) { - detected_gyros[i] = - (FC.SENSOR_ALIGNMENT.gyro_detection_flags & GYRO_DETECTION_FLAGS[`DETECTED_GYRO_${i + 1}`]) != - 0; - - // If gyro is detected, create UI for it - if (detected_gyros[i]) { - createGyroBox(i, $(".tab-configuration .gyro_enable_configuration")); - } - } - - // Only show not found message if no gyros are detected - $(".gyro_notfound").toggle(!detected_gyros.some((detected) => detected)); - } else { - // Hide the gyro container if not needed - $(".tab-configuration .gyro_enable_box").parent().hide(); - } - } else { - // Hide the gyro enable box introduced in 1.47 - $(".tab-configuration .gyro_enable_box").parent().hide(); - - // Original code for older firmware versions remains unchanged - const orientation_gyro_to_use_e = $("select.gyro_to_use"); - const orientation_gyro_1_align_e = $("select.gyro_1_align"); - const orientation_gyro_2_align_e = $("select.gyro_2_align"); - - const GYRO_DETECTION_FLAGS = { - DETECTED_GYRO_1: 1 << 0, - DETECTED_GYRO_2: 1 << 1, - DETECTED_DUAL_GYROS: 1 << 7, - }; - - const detected_gyro_1 = - (FC.SENSOR_ALIGNMENT.gyro_detection_flags & GYRO_DETECTION_FLAGS.DETECTED_GYRO_1) != 0; - const detected_gyro_2 = - (FC.SENSOR_ALIGNMENT.gyro_detection_flags & GYRO_DETECTION_FLAGS.DETECTED_GYRO_2) != 0; - const detected_dual_gyros = - (FC.SENSOR_ALIGNMENT.gyro_detection_flags & GYRO_DETECTION_FLAGS.DETECTED_DUAL_GYROS) != 0; - - if (detected_gyro_1) { - orientation_gyro_to_use_e.append( - ``, - ); - } - if (detected_gyro_2) { - orientation_gyro_to_use_e.append( - ``, - ); - } - if (detected_dual_gyros) { - orientation_gyro_to_use_e.append( - ``, - ); - } - - for (let i = 0; i < SENSOR_ALIGNMENTS.length; i++) { - orientation_gyro_1_align_e.append(``); - orientation_gyro_2_align_e.append(``); - } - - orientation_gyro_to_use_e.val(FC.SENSOR_ALIGNMENT.gyro_to_use); - orientation_gyro_1_align_e.val(FC.SENSOR_ALIGNMENT.gyro_1_align); - orientation_gyro_2_align_e.val(FC.SENSOR_ALIGNMENT.gyro_2_align); - - $(".gyro_alignment_inputs_first").toggle(detected_gyro_1); - $(".gyro_alignment_inputs_second").toggle(detected_gyro_2); - $(".gyro_alignment_inputs_selection").toggle(detected_gyro_1 || detected_gyro_2); - $(".gyro_alignment_inputs_notfound").toggle(!detected_gyro_1 && !detected_gyro_2); - - // Keep original handlers - orientation_gyro_1_align_e.on("change", function () { - const value = parseInt($(this).val()); - - if (value !== FC.SENSOR_ALIGNMENT.gyro_1_align) { - const newValue = $(this).find("option:selected").text(); - self.analyticsChanges["Gyro1Alignment"] = newValue; - } - FC.SENSOR_ALIGNMENT.gyro_1_align = value; - }); - - orientation_gyro_2_align_e.on("change", function () { - const value = parseInt($(this).val()); - - if (value !== FC.SENSOR_ALIGNMENT.gyro_2_align) { - const newValue = $(this).find("option:selected").text(); - self.analyticsChanges["Gyro2Alignment"] = newValue; - } - FC.SENSOR_ALIGNMENT.gyro_2_align = value; - }); - } - - // Magnetometer - const orientation_mag_e = $("select.mag_align"); - - const hasMag = - have_sensor(FC.CONFIG.activeSensors, "mag") && semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_46); - - if (hasMag) { - $('input[name="mag_declination"]').val(FC.COMPASS_CONFIG.mag_declination.toFixed(1)); - } else { - $("div.mag_declination").parent().parent().hide(); - } - - for (let i = 0; i < SENSOR_ALIGNMENTS.length; i++) { - orientation_mag_e.append(``); - } - - orientation_mag_e.val(FC.SENSOR_ALIGNMENT.align_mag); - - function toggleMagCustomAlignmentInputs() { - // Toggle custom alignment visibility based on current value - const index = parseInt(orientation_mag_e.val()) - 1; - const isCustom = SENSOR_ALIGNMENTS[index] === "Custom"; - $(".mag_align_inputs").toggle(isCustom); - } - - orientation_mag_e.change(function () { - let value = parseInt($(this).val()); - - let newValue = undefined; - if (value !== FC.SENSOR_ALIGNMENT.align_mag) { - newValue = $(this).find("option:selected").text(); - } - self.analyticsChanges["MagAlignment"] = newValue; - - FC.SENSOR_ALIGNMENT.align_mag = value; - - toggleMagCustomAlignmentInputs(); - }); - - if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47)) { - $('input[name="mag_align_roll"]').val(FC.SENSOR_ALIGNMENT.mag_align_roll); - $('input[name="mag_align_pitch"]').val(FC.SENSOR_ALIGNMENT.mag_align_pitch); - $('input[name="mag_align_yaw"]').val(FC.SENSOR_ALIGNMENT.mag_align_yaw); - - toggleMagCustomAlignmentInputs(); - } else { - $(".tab-configuration .gyro_align_box").hide(); - $(".tab-configuration .mag_align_box").hide(); - } - - // Range finder - if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47)) { - const rangeFinderType_e = $("select.rangefinderType"); - const sonarElements = sensorTypes().sonar.elements; - - for (let i = 0; i < sonarElements.length; i++) { - rangeFinderType_e.append(``); - } - - rangeFinderType_e.val(FC.SENSOR_CONFIG.sonar_hardware); - } else { - $(".tab-configuration .rangefinder").parent().hide(); - } - - // Optical flow sensor - if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47)) { - const opticalflowType_e = $("select.opticalflowType"); - const opticalflowElements = sensorTypes().opticalflow.elements; - - for (let i = 0; i < opticalflowElements.length; i++) { - opticalflowType_e.append(``); - } - - opticalflowType_e.val(FC.SENSOR_CONFIG.opticalflow_hardware); - } else { - $(".tab-configuration .opticalflow").parent().hide(); - } - - // UI hooks - - $("input.feature", features_e).change(function () { - const element = $(this); - - FC.FEATURE_CONFIG.features.updateData(element); - updateTabList(FC.FEATURE_CONFIG.features); - }); - - $('input[id="accHardwareSwitch"]') - .change(function () { - const checked = $(this).is(":checked"); - $(".accelNeeded").toggle(checked); - }) - .change(); - - $(features_e) - .filter("select") - .change(function () { - const element = $(this); - - FC.FEATURE_CONFIG.features.updateData(element); - updateTabList(FC.FEATURE_CONFIG.features); - }); - - $("input.condition", beeper_e).change(function () { - const element = $(this); - FC.BEEPER_CONFIG.beepers.updateData(element); - }); - - // Analog Beeper toggle all buttons - $(".beeper-enable-all").click(function () { - $("input.condition", beeper_e).each(function () { - $(this).prop("checked", true).trigger("change"); - }); - }); - - $(".beeper-disable-all").click(function () { - $("input.condition", beeper_e).each(function () { - $(this).prop("checked", false).trigger("change"); - }); - }); - - function save_config() { - // Define all configuration operations to execute - const saveOperations = [ - { code: MSPCodes.MSP_SET_FEATURE_CONFIG }, - { code: MSPCodes.MSP_SET_BEEPER_CONFIG }, - { code: MSPCodes.MSP_SET_BOARD_ALIGNMENT_CONFIG }, - { code: MSPCodes.MSP_SET_RC_DEADBAND }, - { code: MSPCodes.MSP_SET_SENSOR_ALIGNMENT }, - { code: MSPCodes.MSP_SET_ADVANCED_CONFIG }, - { code: MSPCodes.MSP_SET_ACC_TRIM }, - { code: MSPCodes.MSP_SET_ARMING_CONFIG }, - { code: MSPCodes.MSP_SET_SENSOR_CONFIG }, - { - condition: () => semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45), - code: MSPCodes.MSP2_SET_TEXT, - extraParams: MSPCodes.CRAFT_NAME, - fallback: { code: MSPCodes.MSP_SET_NAME }, - }, - { - condition: () => semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45), - code: MSPCodes.MSP2_SET_TEXT, - extraParams: MSPCodes.PILOT_NAME, - }, - { code: MSPCodes.MSP_SET_RX_CONFIG }, - { - condition: () => semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_46), - code: MSPCodes.MSP_SET_COMPASS_CONFIG, - }, - ]; - - // Start with a resolved promise - let saveChain = Promise.resolve(true); - - // Build the promise chain - saveOperations.forEach((operation) => { - saveChain = saveChain.then(() => { - // Skip if operation has a condition that returns false - if (operation.condition && !operation.condition()) { - if (operation.fallback) { - // Use fallback operation if provided - return MSP.promise( - operation.fallback.code, - operation.fallback.extraParams - ? mspHelper.crunch(operation.fallback.code, operation.fallback.extraParams) - : mspHelper.crunch(operation.fallback.code), - ); - } - return Promise.resolve(true); - } - - // Execute the operation - return MSP.promise( - operation.code, - operation.extraParams - ? mspHelper.crunch(operation.code, operation.extraParams) - : mspHelper.crunch(operation.code), - ); - }); - }); - - // Complete the chain with final write - return saveChain.then(() => mspHelper.writeConfiguration(true)); - } - - $("a.save").on("click", function () { - // gather data that doesn't have automatic change event bound - FC.BOARD_ALIGNMENT_CONFIG.roll = parseInt($('input[name="board_align_roll"]').val()); - FC.BOARD_ALIGNMENT_CONFIG.pitch = parseInt($('input[name="board_align_pitch"]').val()); - FC.BOARD_ALIGNMENT_CONFIG.yaw = parseInt($('input[name="board_align_yaw"]').val()); - - if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47)) { - FC.SENSOR_ALIGNMENT.gyro_enable_mask = 0; - - // Gather the enable/disable state for all gyros - for (let i = 0; i < MAX_GYROS; i++) { - // Check if the checkbox is checked for this gyro - if ($(`#gyro_${i + 1}_enable`).is(":checked")) { - // Set the bit in the mask - FC.SENSOR_ALIGNMENT.gyro_enable_mask |= 1 << i; - } - // If not checked, the bit stays 0 as we initialized the mask to 0 - } - - FC.SENSOR_ALIGNMENT.mag_align_roll = parseInt($('input[name="mag_align_roll"]').val()); - FC.SENSOR_ALIGNMENT.mag_align_pitch = parseInt($('input[name="mag_align_pitch"]').val()); - FC.SENSOR_ALIGNMENT.mag_align_yaw = parseInt($('input[name="mag_align_yaw"]').val()); - } else { - FC.SENSOR_ALIGNMENT.gyro_1_align = parseInt($("select.gyro_1_align").val()); - FC.SENSOR_ALIGNMENT.gyro_2_align = parseInt($("select.gyro_2_align").val()); - FC.SENSOR_ALIGNMENT.gyro_to_use = parseInt($("select.gyro_to_use").val()); - } - - FC.CONFIG.accelerometerTrims[1] = parseInt($('input[name="roll"]').val()); - FC.CONFIG.accelerometerTrims[0] = parseInt($('input[name="pitch"]').val()); - - if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_46)) { - FC.ARMING_CONFIG.gyro_cal_on_first_arm = $('input[id="configurationGyroCalOnFirstArm"]').is(":checked") - ? 1 - : 0; - // only update auto_disarm_delay if MOTOR_STOP is enabled - if (FC.FEATURE_CONFIG.features.isEnabled("MOTOR_STOP")) { - FC.ARMING_CONFIG.auto_disarm_delay = parseInt($('input[id="configurationAutoDisarmDelay"]').val()); - } - } - - // declination added first in #3676 - if (hasMag) { - FC.COMPASS_CONFIG.mag_declination = $('input[name="mag_declination"]').val(); - } - - if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47)) { - FC.SENSOR_CONFIG.sonar_hardware = $("select.rangefinderType").val(); - FC.SENSOR_CONFIG.opticalflow_hardware = $("select.opticalflowType").val(); - } - - FC.ARMING_CONFIG.small_angle = parseInt($('input[id="configurationSmallAngle"]').val()); - - FC.PID_ADVANCED_CONFIG.gyro_sync_denom = parseInt(gyroSelectElement.val()); - - const value = parseInt(pidSelectElement.val()); - - if (value !== FC.PID_ADVANCED_CONFIG.pid_process_denom) { - const newFrequency = pidSelectElement.find("option:selected").text(); - self.analyticsChanges["PIDLoopSettings"] = `denominator: ${value} | frequency: ${newFrequency}`; - } else { - self.analyticsChanges["PIDLoopSettings"] = undefined; - } - - FC.PID_ADVANCED_CONFIG.pid_process_denom = value; - - FC.RX_CONFIG.fpvCamAngleDegrees = parseInt($('input[name="fpvCamAngleDegrees"]').val()); - - tracking.sendSaveAndChangeEvents( - tracking.EVENT_CATEGORIES.FLIGHT_CONTROLLER, - self.analyticsChanges, - "configuration", - ); - self.analyticsChanges = {}; - - // fill some data - FC.SENSOR_CONFIG.acc_hardware = $('input[id="accHardwareSwitch"]').is(":checked") ? 0 : 1; - FC.SENSOR_CONFIG.baro_hardware = $('input[id="baroHardwareSwitch"]').is(":checked") ? 0 : 1; - FC.SENSOR_CONFIG.mag_hardware = $('input[id="magHardwareSwitch"]').is(":checked") ? 0 : 1; - - if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)) { - FC.CONFIG.craftName = $('input[name="craftName"]').val().trim(); - FC.CONFIG.pilotName = $('input[name="pilotName"]').val().trim(); - } else { - FC.CONFIG.name = $('input[name="craftName"]').val().trim(); - } - - mspHelper.sendSerialConfig(save_config); - }); - - // status data pulled via separate timer with static speed - GUI.interval_add( - "status_pull", - function () { - MSP.send_message(MSPCodes.MSP_STATUS); - }, - 250, - true, - ); - GUI.content_ready(callback); - } -}; - -configuration.cleanup = function (callback) { - if (callback) callback(); -}; - -TABS.configuration = configuration; -export { configuration }; diff --git a/src/js/vue_components.js b/src/js/vue_components.js index 16def8d66c3..39e4540e014 100644 --- a/src/js/vue_components.js +++ b/src/js/vue_components.js @@ -9,6 +9,7 @@ import LandingTab from "../components/tabs/LandingTab.vue"; import OptionsTab from "../components/tabs/OptionsTab.vue"; import PortsTab from "../components/tabs/PortsTab.vue"; import ServosTab from "../components/tabs/ServosTab.vue"; +import ConfigurationTab from "../components/tabs/ConfigurationTab.vue"; // Registry of Vue tab components - used by main.js for dynamic mounting export const VueTabComponents = { @@ -17,6 +18,7 @@ export const VueTabComponents = { options: OptionsTab, ports: PortsTab, servos: ServosTab, + configuration: ConfigurationTab, }; // Create a Vue plugin that registers all components globally @@ -34,5 +36,6 @@ export const BetaflightComponents = { app.component("OptionsTab", OptionsTab); app.component("PortsTab", PortsTab); app.component("ServosTab", ServosTab); + app.component("ConfigurationTab", ConfigurationTab); }, }; diff --git a/src/js/vue_tab_mounter.js b/src/js/vue_tab_mounter.js index 3a70eeadff8..0dd6b5e3222 100644 --- a/src/js/vue_tab_mounter.js +++ b/src/js/vue_tab_mounter.js @@ -63,6 +63,9 @@ export function mountVueTab(tabName, contentReadyCallback) { currentTabApp.provide("betaflightModel", globalThis.vm); } + // Set active tab for legacy compatibility + GUI.active_tab = tabName; + // Mount to content currentTabApp.mount(contentEl); diff --git a/src/tabs/configuration.html b/src/tabs/configuration.html deleted file mode 100644 index 2362cb8cddc..00000000000 --- a/src/tabs/configuration.html +++ /dev/null @@ -1,458 +0,0 @@ -
-
-
-
- -
-
-

-
- -
- -
- -
-
-
-
-
-
-
-

-
-
- - - -
-
- - -
-
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
- - -
-
-
-
-
-
- -
-
-
- -
-
-
-
- -
-
-
-
-
-
- -
-
-
- - -
-
-
-
-
-
-
- -
-
-
- -
-
-
-
- -
- -
-
-
- - -
-
-
-
-
-
-
-

-
- - - - - - - - - - - -
-
-
-
- -
-
- -
- -
-
-
-
-
-
-
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
- - - -
-
-
-
-
-
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- - - -
-
-
-
-
-
- - - - -
-
-
- -
-
- - - -
-
-
-
-
-
-
- -
-
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
- -
-
-
- - - -
-
-
-
-
-
-
- - -
-
-
- - - -
-
-
-
-
-
-
- - -
-
-
- - - -
-
-
-
-
-
- -
-
- -
-
-
- -
-
-
-
-
-
-
- - - - - - - - - -
-
-
- -
- -
-
-
-
- -
- -
-
-
- - -
- - - - -
-
-
-
-
- -
-
-
-
-
-
-
- - -
- - - - - - - - - -
-
-
-
-
-
-
- - -
-
- -
-
- -
-