# SUB Control A deliberately small, low-bandwidth MAVLink controller for bench-testing an ArduSub vehicle over a USB serial connection. It sends no parameter-list request and no telemetry-stream request. ![SUB Control interface](docs/sub-control-interface.png) The desktop window uses a local HTML/CSS/JavaScript interface rendered by the Windows WebView. Python remains the only launcher and owns the MAVLink worker, arming, logging, output limiting, and parameter writes. It does not run a web server, open a browser tab, or require an internet connection. The WebView is frameless and uses the compact title bar drawn by the interface. The vehicle rail includes a local STL viewer: choose **Load STL** to start with an orthographic top-view silhouette, then drag to orbit or use the mouse wheel to zoom. A cached STL copy is stored locally under the ignored `runtime/` directory so the model can be restored on the next launch. STEP/STP CAD solids must first be tessellated/exported as STL because the embedded renderer intentionally does not include a full CAD kernel. Binary and ASCII STL faces are preserved in full; large assemblies are not rendered by skipping triangles. Imports are limited to 30 MB to keep startup and interactive orbiting responsive. ## Safety first - Remove propellers for initial tests and restrain the vehicle. - Verify the ArduSub frame and `SERVOx_FUNCTION`, motor direction, ESC range, leak/failsafe, and emergency-stop arrangements before arming. - The application starts disarmed and limits pilot demand to 20% by default. Once armed, control movement is sent directly with no additional gate. - Closing or disconnecting sends neutral controls and a disarm request, but a physical power cutoff is still required. The controls are pilot-demand axes, not direct PWM values. ArduSub performs the frame mixing and generates the configured ESC outputs. For ArduSub, `MANUAL_CONTROL.z = 500` is neutral vertical thrust. ## Run on Windows 1. Install Python 3.11 or newer. A virtual environment installs packages, but it does not install Python itself. 2. Open PowerShell in this folder and run `./run.ps1` (or right-click it and choose **Run with PowerShell**). 3. Select the autopilot's COM port, then click **Connect**. Baud and control-rate settings are under the small `•••` link button (normally 57600 baud / 20 Hz). 4. Confirm `ArduSub` and a live heartbeat are shown. 5. Click **Set MANUAL**, then **ARM** only when the bench is safe. 6. Move the controls. Once armed, no additional enable button is needed. `run.ps1` automatically creates a private `.venv` on the first run, installs the versions declared in `requirements.txt`, and starts SUB Control. Later runs reuse the same environment and update missing dependencies when necessary. The `.venv` directory is intentionally ignored by Git: virtual environments contain machine-specific executables and can be recreated from `requirements.txt`. ## Repository layout The Gitea repository is named `argo`. `SUB Control` is a normal directory inside that repository containing this application. A Gitea **Project** is a planning board for issues and tasks; source code is not attached to a Project board. Git tracks the folder automatically because its files are committed on the repository's `main` branch. Keyboard controls for SimpleROV-3: W/S forward/back, A/D yaw left/right, Q/E alternative yaw control, and R/F rise/dive. Releasing movement keys eases their controls back to neutral. ## Three-thruster limitation The common ArduSub `SimpleROV-3` arrangement controls forward, yaw, and depth. It cannot independently control lateral, roll, or pitch. Accordingly, the main pad is mapped to forward/yaw, while the vertical slider controls depth thrust. ## Link behavior - MAVLink GCS heartbeat: 1 Hz - `MANUAL_CONTROL`: selectable 10 or 20 Hz (default 20 Hz). Lower rates are intentionally excluded because they make short return ramps become abrupt steps at the flight controller. - No full parameter download - On connection, inherited bulk telemetry streams are stopped. Only `SYS_STATUS` at 1 Hz and `SERVO_OUTPUT_RAW` / `RC_CHANNELS` at 2 Hz are requested, in addition to the normal heartbeat and event-driven status text. - Arm/disarm uses `MAV_CMD_COMPONENT_ARM_DISARM` and displays `COMMAND_ACK` - Link health is based on received autopilot heartbeats - Battery voltage is read from `SYS_STATUS` at 1 Hz and shown as pack voltage, estimated per-cell voltage, and a voltage-only percentage. Cell count, capacity, C rating, full/empty cell voltages, and warning percentage are configurable and saved locally in `battery_settings.json`. Capacity consumed cannot be measured without a current sensor. - Servo configuration scans `SERVO1_FUNCTION` through `SERVO16_FUNCTION` and ArduSub's `MOT_1_DIRECTION` through `MOT_12_DIRECTION` one at a time, waiting for each response and retrying missed replies. A selected output can be assigned to Motor 1–12 or set unassigned. Motor direction is toggled with the corresponding `MOT_n_DIRECTION` value while disarmed; `SERVOx_REVERSED` is intentionally not used for Motor functions. It never downloads the full parameter list. The diagnostic scan also reads `SERVO1`–`SERVO3` MIN/TRIM/MAX and ArduSub's joystick gain values into the local session log; these values remain read-only in the application. - Every session creates a local text log under `logs/`, including operator actions, outgoing manual-control values and commands, acknowledgements, status text, and all received MAVLink messages. This does not use ArduPilot's onboard logger or add any link traffic. The car-style virtual joystick maps only to forward/reverse and continuous differential turning; it never sends lateral motion. The joystick and vertical slider ease back to center over 400 ms after mouse/key release, so neither can be left commanding motion. Focus loss, stale telemetry, disconnect, and disarm clear commanded inputs without easing. This is a neutral-command request, **not proof that motors have stopped** when the communication link is unavailable. In addition, transmitted pilot demands have a small 4% input deadband and a 0.25-second full-range slew limit. This makes both acceleration and deceleration arrive at the flight controller as a sequence of stable intermediate values. Forward and yaw are normalized as a SimpleROV-3 motor pair before the output limit is applied, preventing their mixer contributions from adding beyond the selected horizontal limit. ArduPilot may ignore manual input when `MAV_GCS_SYSID` does not match this application's system ID (default 255), or when `MAV_OPTIONS` does not allow MAVLink input. Those parameters must already be correctly configured. ## Engage, confirmation, and interruption handling The compact window opens at 1100 × 650. Empty sidebar areas drag the window; the STL viewer, buttons, and sliders are separate interactive regions. Output limit and Motor setup sit below the main controls. Recent activity retains the last three events, oldest at the top and newest emphasized at the bottom. Routine hold instructions are kept in tooltips; pending confirmations and safety warnings still appear above the controls. The full-width text control displays `engage` / `disengage`; disengage sends the same immediate disarm request described below. - Hold **ENGAGE** for 1.4 seconds (or hold F2). Early release, leaving the button, interrupted touch, and focus loss cancel the hold without sending arm. - **ENGAGING…** means a request is pending. A send result or accepted command ACK alone does not prove that the vehicle armed: a fresh autopilot heartbeat reporting the requested state is the final confirmation. A heartbeat can confirm state even if the ACK packet was lost. - **DISARM** is one tap or F3, with no long hold. While confirmation is pending, **DISARMING…** stays red and driving is disabled. Disarm can also interrupt a pending Engage request. No forced-arm/disarm bypass is used. - Confirmation timeout: 5 seconds in the backend (6-second UI fallback). Rejection and timeout remain visible. Late heartbeat updates still determine the displayed state; the UI never substitutes an animation state for telemetry. - A heartbeat older than 3 seconds is stale. A missing bridge reply after 1.5 seconds shows **STATE UNKNOWN**, preserving the last-known state without claiming disarm. Battery readings older than 5 seconds are marked stale. - The web frontend renews an input lease locally every 250 ms. If it stops for 1 second, the worker clears stored demands and sends neutral at its configured control rate. This adds no MAVLink stream or parameter download. Focus loss clears both captured touch pointers and queued input; reconnect never replays the old joystick position. Normal release still eases back to center. - Link loss cannot deliver a stop over a broken tether. Verify the vehicle's own failsafe and use independent physical power isolation for bench tests. ### Regression checks (no hardware) Run from this project folder after setting up the Python environment: ```powershell .venv\Scripts\python.exe -m unittest discover -s tests -v node --test tests/control_ui.test.cjs ``` Node is needed only for the frontend regression tests, not to run the app. The tests use mocked transport and synthetic pointer events, including independent joystick/vertical pointers. Validate physical multi-touch on the intended screen and FC failsafes on a restrained, propeller-free setup before powered operation.