-
-
Notifications
You must be signed in to change notification settings - Fork 1k
feat: Migrate Configuration Tab to Vue 3 #4727
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Erics1337
wants to merge
39
commits into
betaflight:master
Choose a base branch
from
Erics1337:vue-tab-config
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,058
−1,188
Open
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
570d3eb
feat: convert configuration tab to vue and rebase from master
Erics1337 b2fce43
fix: Vue Migration: Servos & Serial (#4725)
Erics1337 9a4ad42
fix(ConfigurationTab): Resolve CodeRabbit review comments
Erics1337 7d1d1c4
fix: features configuration table to fix styling issues and fix loca…
Erics1337 7c07d39
chore: remove legacy configuration tab HTML and JS files
Erics1337 14fa3f1
Resolve conflict from rebase
Erics1337 49235b5
Add custom Capacitor plugin-socket for raw TCP support (#4471)
haslinghuis 80278fa
fix(config-tab): improve table accessibility and fix headers
Erics1337 08381fd
feat: Introduce 'Other Features' translation and help icon, and refin…
Erics1337 519a2cb
fix coderabbit comments
Erics1337 e4fb57f
Update src/components/tabs/ConfigurationTab.vue
Erics1337 b723dbf
refactor: Make sensor alignment display conditional on API version, a…
Erics1337 151c269
fix: Conditionally display legacy sensor alignment options based on A…
Erics1337 421d1c4
fix: Show Sensor Alignment box and fix magnetometer dropdown
Erics1337 9575f09
Address comments: Add custom magnetic alignment options, update gyro …
Erics1337 50cc3f9
Address coderabbit comments: Implement custom roll/pitch/yaw alignmen…
Erics1337 e0e183f
feat: Remove custom gyro alignment angle inputs and associated data h…
Erics1337 a5a5de3
address coderabbit comments: correct feature list rendering, ensure a…
Erics1337 15a6d45
address coderabbit comments: Update gyro frequency display key and co…
Erics1337 f3e727b
address coderabbit comments: add custom roll, pitch, and yaw alignmen…
Erics1337 e63ca72
address more coderabbit comments: Correct configuration help text ren…
Erics1337 665ab3f
fix: Rename "Other Features" UI section to "Sensor Configuration", up…
Erics1337 813e42a
refactor: Separate magnetometer alignment section, update gyro alignm…
Erics1337 9f6a99e
feat: Implement multi-gyro array support in backend to align with PR …
Erics1337 562335d
refactor: Update gyro alignment properties from arrays to distinct gy…
Erics1337 17c6a9e
fix: import TABS from gui module
Erics1337 a551469
fix: validate remaining data length before reading magnetometer align…
Erics1337 8b35c89
fix: Remove redundant `key` attributes from `<tbody>` elements and pr…
Erics1337 f734a61
Resolve conflict: Remove MainActivity.java
Erics1337 2008d7b
fix: remove duplicate ServosTab import and component registrations
Erics1337 1e62119
fix: Add reboot connection timeout and grace period constants, and a …
Erics1337 6af7970
refactor(locales): cleanup gyro keys and remove unused config strings
Erics1337 6fe675c
fix(msp): ensure custom gyro alignment persists for legacy API
Erics1337 51dc9e7
fix(ui): improve configuration tab robustness and rendering
Erics1337 b49dde9
Update src/js/msp/MSPHelper.js
Erics1337 098df4a
feat: Implement Multi-Gyro support for API 1.47+
Erics1337 dd0abbb
revert
Erics1337 52d316e
Fix Configuration Tab IMU display and revert backend changes
Erics1337 03dac7d
Fix multi-gyro display for API 1.47+
Erics1337 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
327 changes: 327 additions & 0 deletions
327
android/app/src/main/java/betaflight/configurator/protocols/tcp/BetaflightTcpPlugin.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<ConnectionState> 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)"; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Port validation is incomplete - negative port will cause InetSocketAddress exception.
Line 67 sets
port = -1whenportObjis null, but there's no subsequent validation to reject invalid ports before attempting connection on line 86.InetSocketAddresswill throwIllegalArgumentExceptionfor ports outside 0-65535.🔎 Proposed fix to validate port before connection
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 (port < MIN_PORT || port > MAX_PORT) { + call.reject(ERROR_INVALID_PORT); + call.setKeepAlive(false); + return; + } if (!compareAndSetState(ConnectionState.DISCONNECTED, ConnectionState.CONNECTING)) {🤖 Prompt for AI Agents