Widgets W1-W5 confirmed working in Cockpit v1.18.2; update README, add probe diagnostic and gitignore, remove obsolete html

This commit is contained in:
Grant 2026-06-23 14:20:26 +02:00
parent 12b5161436
commit 66f9bd7440
14 changed files with 98 additions and 1667 deletions

6
widgets/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
# Manual backup copies — never commit these
*[Cc]opy*.json
* - Copy.json
# Working drafts — superseded by dated final versions
w0_working.json

View File

@ -2,56 +2,85 @@
## Requirements
- **Cockpit native desktop app v1.17.0** (Windows `.exe` installer)
- The BlueOS browser extension version does NOT support DIY widgets
- Download from: https://github.com/bluerobotics/cockpit/releases/tag/v1.17.0
- **Cockpit native desktop app v1.18.2** (or compatible 1.18.x)
- The BlueOS browser extension ("Lite") does NOT support DIY widgets — the native desktop app is mandatory.
- Cockpit must connect directly to the RPi4/BlueOS IP over plain HTTP (not through an HTTPS reverse proxy).
- BlueOS Pirate Mode must be enabled each session (Settings → General).
## Network standard
Use direct IP addresses everywhere. Do NOT use `blueos.local` — mDNS/Avahi resolution is unreliable across subnets and over Tailscale.
| Host | IP |
|---|---|
| RPi4 (BlueOS gateway) | `192.168.1.100` |
| RPi5 (autonomy brain) | `192.168.1.101` |
## How to install a widget
1. Open the Cockpit native desktop app
2. Connect to the vehicle / VM (`100.84.141.120` for dev VM)
3. Enter edit mode (pencil icon, top right)
4. Click **Add Widget** at the bottom of the screen
5. Scroll right to find the `</>` DIY widget — drag it onto the view
6. Click the **gear icon** on the widget to open the editor
7. Click **Import** and select the `.json` file for the widget you want
8. The widget will load immediately
1. Open the Cockpit native desktop app.
2. Connect to the vehicle at `192.168.1.100` (RPi4/BlueOS) over HTTP.
3. Enter edit mode (pencil icon, top right).
4. Click **Add Widget** at the bottom of the screen.
5. Scroll right to find the `</>` DIY widget — drag it onto the view.
6. Click the **gear icon** on the widget to open the editor.
7. Click **Import** and select the `.json` file for the widget.
8. The widget loads immediately.
> **Note:** Widget files are `.json` format (not `.html`).
> Cockpit expects a JSON file with `html`, `css`, and `js` fields.
> Widget files are `.json` (not `.html`). Cockpit expects a JSON file with `html`, `css`, and `js` string fields.
## Data lake API (Cockpit v1.18.2 DIY widget scope)
- Read a single live value: `window.cockpit.getDataLakeVariableData(variableId)` — returns the live value. **This is the getter to use.**
- `getAllDataLakeVariablesInfo()` returns metadata only (id/name/type/description), not values.
- `getDataLakeValue()` does NOT exist — do not use.
- `listenToDatalakeVariable()` does NOT exist in DIY widget scope (it is the postMessage iframe External API only).
## Live data lake variables (published by cockpit_bridge)
External WebSocket variables carry the `external/` prefix.
| Variable | Source topic | Notes |
|---|---|---|
| `external/rov-failsafe` | /rov/failsafe | Failsafe state: 0=GREEN 1=AMBER 2=RED |
| `external/rov-ms` | /rov/mission/status | Mission state: 0=IDLE 1=RUNNING 2=PAUSED 3=COMPLETE 4=ABORTED |
| `external/rov-mp` | /rov/mission/status | Mission progress 0.01.0 |
| `external/rov-depth` | /rov/depth | Depth (m) |
| `external/rov-heading` | /mavros compass_hdg | Heading (deg) |
| `external/rov-voltage` | /mavros/battery | Battery voltage |
## Widget index
| File | Widget | Notes |
| File | Widget | Status | Reads |
|---|---|---|---|
| w0_data_lake_inspector.json | Data Lake Inspector | Diagnostic. Flashing/scroll issue OUTSTANDING. | All data lake variables |
| w1_system_health_indicator.json | System Health Indicator | CONFIRMED working | `external/rov-failsafe` |
| w2_mission_status.json | Mission Status | CONFIRMED working | `external/rov-ms`, `external/rov-mp` |
| w3_abort_button.json | Abort Button | UI CONFIRMED. Backend pending. | POST → FastAPI `/abort` |
| w4_mission_setup_button.json | Mission Setup Button | UI CONFIRMED. Backend pending. | `external/rov-ms`; opens `/setup` |
| w5_battery_return_budget.json | Return Budget | UI CONFIRMED. Live budget pending. | `SYS_STATUS/battery_remaining` + `external/rov-return-budget` |
Diagnostic widget (not production):
| File | Widget | Purpose |
|---|---|---|
| w0_data_lake_inspector.json | Data Lake Inspector | Diagnostic — shows all data lake variables. Not production. |
| w1_system_health_indicator.json | System Health Indicator | Traffic light GREEN/AMBER/RED. Reads `rov_failsafe` from data lake. |
| w2_mission_status.json | Mission Status | Progress bar + state. Reads `rov_mission_state` and `rov_mission_progress`. |
| w3_abort_button.json | Abort Button | Confirm dialog → POST /abort to FastAPI. Edit `FASTAPI_HOST` before use. |
| w4_mission_setup_button.json | Mission Setup Button | Opens setup page in new tab. Edit `SETUP_URL` before use. |
| w5_battery_return_budget.json | Return Budget | Headroom warning. Reads `SYS_STATUS/battery_remaining` (native) + `rov_return_budget`. |
| diagnostics/w_probe_api.json | API Probe | Enumerates `window.cockpit.*` methods and live-reads one variable. Used to confirm the data lake getter. Keep for future debugging. |
## Config values to edit before field use
## Backend dependencies (not yet built)
In `w3_abort_button.json` (line 1 of the JS section):
```
var FASTAPI_HOST = 'http://blueos.local:8081';
```
- **FastAPI service on RPi5:8081** — required by W3 (`POST /abort`) and W4 (`GET /setup`). Until built, W3's abort returns a network error (handled cleanly) and W4's setup page is blank.
- **`external/rov-return-budget`** — not yet published by cockpit_bridge. Until then W5 shows the budget as pending and battery as "not reported" under SITL (SITL reports -1).
In `w4_mission_setup_button.json` (line 1 of the JS section):
```
var SETUP_URL = 'http://blueos.local:8081/setup';
```
## Config values
Both values are correct for real hardware. For the dev VM, replace `blueos.local` with the VM Tailscale IP.
W3 and W4 target the RPi5 backend directly:
## Widget data dependencies
- W3 `W3_FASTAPI_HOST` = `http://192.168.1.101:8081`
- W4 `W4_SETUP_URL` = `http://192.168.1.101:8081/setup`
| Widget | Works without backend? | Requires |
|---|---|---|
| W0 Data Lake Inspector | Yes — shows live MAVLink data immediately | Vehicle connected |
| W1 System Health | Shows "waiting" until bridge runs | NAMED_VALUE bridge node |
| W2 Mission Status | Shows "waiting" until bridge runs | NAMED_VALUE bridge node |
| W3 Abort Button | Button visible, POST will fail | FastAPI backend container |
| W4 Setup Button | Button visible, opens 404 | Extension backend serving setup page |
| W5 Return Budget | Battery% shows live, budget shows 0% | FastAPI backend for return budget calc |
## Widget conventions (for new widgets)
- All element IDs and CSS classes are prefixed per-widget (e.g. `w1-`, `w2-`) to avoid collisions in the shared Cockpit renderer.
- All `document.getElementById` calls must be INSIDE functions, not at script load time (DOM timing).
- Avoid: `body {}` / `:root {}` globals, `@keyframes`, `position: fixed`, Unicode in JS strings, `async/await`. Use `var`, not `let`/`const`.
- A footer status line (debug) is present on data-reading widgets during validation; strip before final production.

File diff suppressed because one or more lines are too long

View File

@ -1,344 +0,0 @@
<!DOCTYPE html>
<!--
============================================================
ARGONAUT 3 — DATA LAKE INSPECTOR
Widget 0 — Diagnostic tool (not a production widget)
============================================================
Purpose : Show all Cockpit data lake variables with live
updating values. Used during development to
discover what MAVLink data is available.
Import : Cockpit → Edit mode → Add DIY Widget →
gear icon → Import → select this file
Version : 1.1 (added scrolling, compact layout, fix)
============================================================
-->
<style>
/* ── Reset and root variables ──────────────────────────── */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg0: #09111a; /* darkest background */
--bg1: #0e1d2e; /* table row background */
--bg2: #162336; /* header / input background */
--border: #1e3050; /* subtle border */
--text0: #d8eeff; /* primary text */
--text1: #6a9bbf; /* secondary / dim text */
--accent: #00c8f0; /* highlight colour */
--green: #00e09a; /* value flash on update */
--amber: #ffb830; /* status warning */
--red: #ff3a5a; /* status error / stale */
--mono: 'Courier New', monospace;
}
/* ── Outer wrapper — fills the widget area ────────────── */
body {
background: var(--bg0);
color: var(--text0);
font-family: var(--mono);
font-size: 12px;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden; /* prevent body scroll — table scrolls */
}
/* ── Sticky header bar ─────────────────────────────────── */
#header {
background: var(--bg2);
border-bottom: 1px solid var(--border);
padding: 5px 8px;
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0; /* never shrink — always visible */
}
#header .title {
font-size: 11px;
font-weight: bold;
color: var(--accent);
white-space: nowrap;
}
/* Search / filter input */
#search {
flex: 1;
background: var(--bg0);
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text0);
font-family: var(--mono);
font-size: 11px;
padding: 2px 6px;
outline: none;
}
#search:focus {
border-color: var(--accent);
}
/* Live variable count */
#count {
font-size: 10px;
color: var(--text1);
white-space: nowrap;
}
/* Connection status dot */
#status {
font-size: 10px;
color: var(--amber);
white-space: nowrap;
}
#status.ok { color: var(--green); }
#status.err { color: var(--red); }
/* ── Scrollable table container ────────────────────────── */
#table-wrap {
flex: 1; /* take remaining height */
overflow-y: auto; /* THIS is what enables scrolling */
overflow-x: hidden;
}
/* Thin scrollbar styling */
#table-wrap::-webkit-scrollbar { width: 5px; }
#table-wrap::-webkit-scrollbar-track { background: var(--bg0); }
#table-wrap::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
/* ── Table ─────────────────────────────────────────────── */
table {
width: 100%;
border-collapse: collapse;
}
/* Sticky column headers scroll with table — not the page */
thead th {
position: sticky;
top: 0;
background: var(--bg2);
border-bottom: 1px solid var(--border);
color: var(--text1);
font-size: 10px;
font-weight: normal;
padding: 3px 6px;
text-align: left;
text-transform: uppercase;
letter-spacing: 0.05em;
z-index: 1;
}
/* Column widths */
thead th:nth-child(1) { width: 55%; } /* Variable name */
thead th:nth-child(2) { width: 12%; } /* Type */
thead th:nth-child(3) { width: 33%; } /* Value */
/* Rows */
tbody tr {
border-bottom: 1px solid var(--border);
transition: background 0.1s;
}
tbody tr:hover { background: var(--bg2); }
/* Flash green when a value changes */
tbody tr.flash td { color: var(--green); }
td {
padding: 3px 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Variable name column */
td:nth-child(1) {
color: var(--accent);
font-size: 11px;
}
/* Type column */
td:nth-child(2) {
color: var(--text1);
font-size: 10px;
}
/* Value column */
td:nth-child(3) {
color: var(--text0);
font-size: 11px;
}
/* Empty state message */
#empty {
padding: 20px;
text-align: center;
color: var(--text1);
font-size: 11px;
}
</style>
<body>
<!-- ── Header bar ──────────────────────────────────────── -->
<div id="header">
<span class="title">⬡ Data Lake</span>
<input id="search" type="text" placeholder="filter variables…" />
<span id="count">0 vars</span>
<span id="status">connecting…</span>
</div>
<!-- ── Scrollable table area ───────────────────────────── -->
<div id="table-wrap">
<table>
<thead>
<tr>
<th>Variable</th>
<th>Type</th>
<th>Value</th>
</tr>
</thead>
<tbody id="tbody">
<!-- Rows injected by JS -->
</tbody>
</table>
<div id="empty" style="display:none">No variables match the filter.</div>
</div>
</body>
<script>
// ── State ─────────────────────────────────────────────────
let vars = {}; // { variableName: { type, value } }
let filter = ''; // current search string (lowercased)
// ── DOM references ────────────────────────────────────────
const tbody = document.getElementById('tbody');
const countEl = document.getElementById('count');
const statusEl= document.getElementById('status');
const searchEl= document.getElementById('search');
const emptyEl = document.getElementById('empty');
// ── Format a raw value for display ───────────────────────
function fmt(v) {
if (v === null || v === undefined) return '—';
if (typeof v === 'number') {
// Integers shown as-is; floats to 4 decimal places
return Number.isInteger(v) ? String(v) : v.toFixed(4);
}
if (typeof v === 'boolean') return v ? 'true' : 'false';
if (typeof v === 'object') return JSON.stringify(v).slice(0, 60);
return String(v).slice(0, 80);
}
// ── Render the table ─────────────────────────────────────
// Only re-renders rows that changed (avoids scroll reset)
function render() {
const keys = Object.keys(vars).sort();
const filtered = keys.filter(k =>
!filter || k.toLowerCase().includes(filter)
);
// Show/hide empty state
emptyEl.style.display = filtered.length === 0 ? '' : 'none';
// Update count
countEl.textContent = `${filtered.length} / ${keys.length} vars`;
// Build a map of existing rows by variable name
const existingRows = {};
tbody.querySelectorAll('tr[data-key]').forEach(row => {
existingRows[row.dataset.key] = row;
});
// Remove rows no longer in filtered set
Object.keys(existingRows).forEach(k => {
if (!filtered.includes(k)) {
existingRows[k].remove();
delete existingRows[k];
}
});
// Add or update rows (in sorted order)
filtered.forEach((key, idx) => {
const { type, value } = vars[key];
let row = existingRows[key];
if (!row) {
// Create new row
row = document.createElement('tr');
row.dataset.key = key;
row.innerHTML = `<td title="${key}">${key}</td><td></td><td></td>`;
tbody.appendChild(row);
}
// Update type and value cells
const cells = row.cells;
const newType = type || '—';
const newValue = fmt(value);
// Flash green on value change
if (cells[2].textContent !== newValue) {
row.classList.add('flash');
setTimeout(() => row.classList.remove('flash'), 400);
}
cells[1].textContent = newType;
cells[2].textContent = newValue;
});
}
// ── Poll the data lake every 500ms ───────────────────────
// getAllDataLakeVariablesInfo() returns {name: {type, value}}
function poll() {
try {
// Guard: API may not be ready immediately on load
if (typeof window.cockpit === 'undefined' ||
typeof window.cockpit.getAllDataLakeVariablesInfo !== 'function') {
statusEl.className = 'err';
statusEl.textContent = 'API not ready';
return;
}
const info = window.cockpit.getAllDataLakeVariablesInfo();
if (!info || Object.keys(info).length === 0) {
statusEl.className = '';
statusEl.textContent = 'no data';
return;
}
// Update our state from the fresh snapshot
Object.entries(info).forEach(([key, meta]) => {
vars[key] = {
type: meta?.type ?? typeof meta?.value ?? '?',
value: meta?.value ?? meta,
};
});
statusEl.className = 'ok';
statusEl.textContent = 'live';
render();
} catch (err) {
statusEl.className = 'err';
statusEl.textContent = 'error';
console.error('[DataLake Inspector]', err);
}
}
// ── Search / filter handler ───────────────────────────────
searchEl.addEventListener('input', () => {
filter = searchEl.value.trim().toLowerCase();
render();
});
// ── Start polling ─────────────────────────────────────────
// Initial poll after 300ms (allows Cockpit API to initialise)
setTimeout(poll, 300);
setInterval(poll, 500);
</script>

View File

@ -1,288 +0,0 @@
<!DOCTYPE html>
<!--
============================================================
ARGONAUT 3 — SYSTEM HEALTH INDICATOR
Widget 1 — Primary safety status (always visible)
============================================================
Purpose : Display GREEN / AMBER / RED failsafe state as a
traffic-light row of three circles with a label.
Data : Reads NAMED_VALUE_INT "rov_failsafe" from the
Cockpit data lake. Values: 0=GREEN, 1=AMBER, 2=RED
(The ROS2 → MAVROS → NAMED_VALUE bridge must be
running for live data. Falls back to a "waiting"
state if the variable is absent.)
Import : Cockpit → Edit mode → Add DIY Widget →
gear icon → Import → select this file
Version : 1.0
============================================================
-->
<style>
/* ── Reset ───────────────────────────────────────────────── */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #09111a; /* widget background */
--text0: #d8eeff; /* primary label text */
--text1: #6a9bbf; /* secondary / dim text */
--green: #00e09a; /* GREEN state fill */
--amber: #ffb830; /* AMBER state fill */
--red: #ff3a5a; /* RED state fill */
--dim: #1a2d42; /* inactive circle fill */
--border: #1e3050; /* inactive circle border */
--mono: 'Courier New', monospace;
}
/* ── Full-height flex container ─────────────────────────── */
body {
background: var(--bg);
color: var(--text0);
font-family: var(--mono);
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
user-select: none;
}
/* ── Three-circle row ───────────────────────────────────── */
#circles {
display: flex;
gap: 12px;
align-items: center;
justify-content: center;
margin-bottom: 10px;
}
/* Each circle is a labelled flex column */
.circle-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
/* The circle itself */
.circle {
width: 36px;
height: 36px;
border-radius: 50%;
border: 2px solid var(--border);
background: var(--dim);
transition: background 0.3s, border-color 0.3s, box-shadow 0.3s;
}
/* Colour states — applied to .circle when active */
.circle.green {
background: var(--green);
border-color: var(--green);
box-shadow: 0 0 12px var(--green);
}
.circle.amber {
background: var(--amber);
border-color: var(--amber);
box-shadow: 0 0 12px var(--amber);
}
.circle.red {
background: var(--red);
border-color: var(--red);
box-shadow: 0 0 16px var(--red);
}
/* RED state flashing border animation */
@keyframes flash-red {
0%, 100% { box-shadow: 0 0 16px var(--red); }
50% { box-shadow: 0 0 32px var(--red), 0 0 8px #ff000088; }
}
.circle.red { animation: flash-red 1s ease-in-out infinite; }
/* Label below each circle */
.circle-label {
font-size: 9px;
color: var(--text1);
text-transform: uppercase;
letter-spacing: 0.08em;
}
/* Active label is brighter */
.circle-label.active { color: var(--text0); font-weight: bold; }
/* ── Status message line ────────────────────────────────── */
#message {
font-size: 11px;
color: var(--text1);
text-align: center;
max-width: 180px;
line-height: 1.4;
min-height: 16px; /* prevent layout shift when empty */
}
/* Colour the message text to match current state */
#message.green { color: var(--green); }
#message.amber { color: var(--amber); }
#message.red { color: var(--red); }
/* ── Small "last updated" footer ─────────────────────────── */
#footer {
margin-top: 6px;
font-size: 9px;
color: #2a4060; /* very dim — not important */
}
</style>
<body>
<!-- Three traffic-light circles -->
<div id="circles">
<div class="circle-wrap">
<div id="c-green" class="circle"></div>
<span id="l-green" class="circle-label">Green</span>
</div>
<div class="circle-wrap">
<div id="c-amber" class="circle"></div>
<span id="l-amber" class="circle-label">Amber</span>
</div>
<div class="circle-wrap">
<div id="c-red" class="circle"></div>
<span id="l-red" class="circle-label">Red</span>
</div>
</div>
<!-- Human-readable status message -->
<div id="message">Connecting…</div>
<!-- Update timestamp (very dim) -->
<div id="footer"></div>
</body>
<script>
// ── Config ────────────────────────────────────────────────
// Name of the data lake variable published by the ROS2 bridge
const VARIABLE = 'rov_failsafe';
// Poll interval in milliseconds
const POLL_MS = 500;
// State values
const STATE_GREEN = 0;
const STATE_AMBER = 1;
const STATE_RED = 2;
// Default messages for each state (used when no reason string present)
const MESSAGES = {
[STATE_GREEN]: 'Systems nominal',
[STATE_AMBER]: 'Parameter degraded — check system',
[STATE_RED]: 'CRITICAL — Safe action triggered',
};
// ── DOM references ────────────────────────────────────────
const cGreen = document.getElementById('c-green');
const cAmber = document.getElementById('c-amber');
const cRed = document.getElementById('c-red');
const lGreen = document.getElementById('l-green');
const lAmber = document.getElementById('l-amber');
const lRed = document.getElementById('l-red');
const msgEl = document.getElementById('message');
const footEl = document.getElementById('footer');
// ── Apply a state to the UI ───────────────────────────────
// state : 0 (GREEN) | 1 (AMBER) | 2 (RED) | null (waiting)
// reason: optional string to show in the message line
function applyState(state, reason) {
// Clear all active circle classes first
[cGreen, cAmber, cRed].forEach(c =>
c.classList.remove('green', 'amber', 'red')
);
// Clear all active label classes
[lGreen, lAmber, lRed].forEach(l =>
l.classList.remove('active')
);
// Remove all message colour classes
msgEl.classList.remove('green', 'amber', 'red');
if (state === STATE_GREEN) {
cGreen.classList.add('green');
lGreen.classList.add('active');
msgEl.classList.add('green');
msgEl.textContent = reason || MESSAGES[STATE_GREEN];
} else if (state === STATE_AMBER) {
cAmber.classList.add('amber');
lAmber.classList.add('active');
msgEl.classList.add('amber');
msgEl.textContent = reason || MESSAGES[STATE_AMBER];
} else if (state === STATE_RED) {
cRed.classList.add('red');
lRed.classList.add('active');
msgEl.classList.add('red');
msgEl.textContent = reason || MESSAGES[STATE_RED];
} else {
// No known state — waiting for data
msgEl.textContent = 'Waiting for data…';
}
}
// ── Poll the data lake ────────────────────────────────────
function poll() {
try {
// Guard: ensure Cockpit API is available
if (typeof window.cockpit === 'undefined' ||
typeof window.cockpit.getDataLakeValue !== 'function') {
applyState(null);
footEl.textContent = 'API not ready';
return;
}
// Read the primary state variable (integer 0/1/2)
const stateRaw = window.cockpit.getDataLakeValue(VARIABLE);
if (stateRaw === null || stateRaw === undefined) {
// Variable not yet in the data lake — bridge not running
applyState(null);
footEl.textContent = `Waiting for ${VARIABLE}`;
return;
}
const state = parseInt(stateRaw, 10);
// Optional: read a reason string if the bridge publishes one
// (published separately as NAMED_VALUE_FLOAT/rov_failsafe_reason)
// For now we use the default messages above.
const reason = null;
applyState(state, reason);
// Update timestamp footer
const now = new Date();
footEl.textContent = `Updated ${now.toLocaleTimeString()}`;
} catch (err) {
applyState(null);
footEl.textContent = 'Poll error — see console';
console.error('[Health Widget]', err);
}
}
// ── Start ─────────────────────────────────────────────────
// Initial delay gives the Cockpit API time to initialise
setTimeout(poll, 300);
setInterval(poll, POLL_MS);
</script>

View File

@ -1,7 +1,7 @@
{
"html": "<div id=\"circles\">\n <div class=\"circle-wrap\">\n <div id=\"c-green\" class=\"circle\"></div>\n <span id=\"l-green\" class=\"circle-label\">Green</span>\n </div>\n <div class=\"circle-wrap\">\n <div id=\"c-amber\" class=\"circle\"></div>\n <span id=\"l-amber\" class=\"circle-label\">Amber</span>\n </div>\n <div class=\"circle-wrap\">\n <div id=\"c-red\" class=\"circle\"></div>\n <span id=\"l-red\" class=\"circle-label\">Red</span>\n </div>\n</div>\n<div id=\"message\">Connecting...</div>\n<div id=\"footer\">--</div>",
"html": "<div id=\"w1-circles\">\n <div class=\"w1-circle-wrap\">\n <div id=\"w1-c-green\" class=\"w1-circle\"></div>\n <span id=\"w1-l-green\" class=\"w1-circle-label\">Green</span>\n </div>\n <div class=\"w1-circle-wrap\">\n <div id=\"w1-c-amber\" class=\"w1-circle\"></div>\n <span id=\"w1-l-amber\" class=\"w1-circle-label\">Amber</span>\n </div>\n <div class=\"w1-circle-wrap\">\n <div id=\"w1-c-red\" class=\"w1-circle\"></div>\n <span id=\"w1-l-red\" class=\"w1-circle-label\">Red</span>\n </div>\n</div>\n<div id=\"w1-message\">Connecting...</div>\n<div id=\"w1-footer\">--</div>",
"css": ".circle-wrap { display: inline-flex; flex-direction: column; align-items: center; gap: 4px; margin: 0 6px; }\n.circle { width: 36px; height: 36px; border-radius: 50%; border: 2px solid #1e3050; background: #1a2d42; }\n.circle.green { background: #00e09a; border-color: #00e09a; box-shadow: 0 0 12px #00e09a; }\n.circle.amber { background: #ffb830; border-color: #ffb830; box-shadow: 0 0 12px #ffb830; }\n.circle.red { background: #ff3a5a; border-color: #ff3a5a; box-shadow: 0 0 16px #ff3a5a; }\n.circle-label { font-size: 9px; color: #6a9bbf; text-transform: uppercase; letter-spacing: 0.08em; font-family: monospace; }\n.circle-label.active { color: #d8eeff; font-weight: bold; }\n#circles { display: flex; align-items: center; justify-content: center; margin-bottom: 10px; }\n#message { font-size: 11px; color: #6a9bbf; text-align: center; max-width: 180px; line-height: 1.4; font-family: monospace; }\n#message.green { color: #00e09a; }\n#message.amber { color: #ffb830; }\n#message.red { color: #ff3a5a; }\n#footer { margin-top: 6px; font-size: 9px; color: #2a4060; font-family: monospace; }",
"css": ".w1-circle-wrap { display: inline-flex; flex-direction: column; align-items: center; gap: 4px; margin: 0 6px; } .w1-circle { width: 36px; height: 36px; border-radius: 50%; border: 2px solid #1e3050; background: #1a2d42; } .w1-circle.green { background: #00e09a; border-color: #00e09a; box-shadow: 0 0 12px #00e09a; } .w1-circle.amber { background: #ffb830; border-color: #ffb830; box-shadow: 0 0 12px #ffb830; } .w1-circle.red { background: #ff3a5a; border-color: #ff3a5a; box-shadow: 0 0 16px #ff3a5a; } .w1-circle-label { font-size: 9px; color: #6a9bbf; text-transform: uppercase; letter-spacing: 0.08em; font-family: monospace; } .w1-circle-label.active { color: #d8eeff; font-weight: bold; } #w1-circles { display: flex; align-items: center; justify-content: center; margin-bottom: 10px; } #w1-message { font-size: 11px; color: #6a9bbf; text-align: center; max-width: 180px; line-height: 1.4; font-family: monospace; } #w1-message.green { color: #00e09a; } #w1-message.amber { color: #ffb830; } #w1-message.red { color: #ff3a5a; } #w1-footer { margin-top: 6px; font-size: 9px; color: #2a4060; font-family: monospace; }",
"js": "var VARIABLE = 'rov_failsa';\nvar POLL_MS = 500;\n\nvar cGreen = document.getElementById('c-green');\nvar cAmber = document.getElementById('c-amber');\nvar cRed = document.getElementById('c-red');\nvar lGreen = document.getElementById('l-green');\nvar lAmber = document.getElementById('l-amber');\nvar lRed = document.getElementById('l-red');\nvar msgEl = document.getElementById('message');\nvar footEl = document.getElementById('footer');\n\nfunction clearAll() {\n cGreen.className = 'circle';\n cAmber.className = 'circle';\n cRed.className = 'circle';\n lGreen.className = 'circle-label';\n lAmber.className = 'circle-label';\n lRed.className = 'circle-label';\n msgEl.className = '';\n}\n\nfunction applyState(state) {\n clearAll();\n if (state === 0) {\n cGreen.className = 'circle green';\n lGreen.className = 'circle-label active';\n msgEl.className = 'green';\n msgEl.textContent = 'Systems nominal';\n } else if (state === 1) {\n cAmber.className = 'circle amber';\n lAmber.className = 'circle-label active';\n msgEl.className = 'amber';\n msgEl.textContent = 'Parameter degraded';\n } else if (state === 2) {\n cRed.className = 'circle red';\n lRed.className = 'circle-label active';\n msgEl.className = 'red';\n msgEl.textContent = 'CRITICAL';\n } else {\n msgEl.textContent = 'Waiting for data...';\n }\n}\n\nfunction poll() {\n try {\n if (typeof window.cockpit === 'undefined') {\n footEl.textContent = 'API not ready';\n return;\n }\n var raw = window.cockpit.getDataLakeValue(VARIABLE);\n if (raw === null || raw === undefined) {\n applyState(-1);\n footEl.textContent = 'Waiting for ' + VARIABLE;\n return;\n }\n applyState(parseInt(raw, 10));\n footEl.textContent = 'OK';\n } catch(err) {\n footEl.textContent = 'Error';\n }\n}\n\nsetTimeout(poll, 300);\nsetInterval(poll, POLL_MS);"
"js": "// W1 -- System Health Indicator\n// Reads external/rov-failsafe from Cockpit data lake.\n// Getter confirmed: window.cockpit.getDataLakeVariableData(id)\n// State: 0=GREEN 1=AMBER 2=RED null=waiting\nvar W1_VAR = 'external/rov-failsafe';\nvar W1_POLL_MS = 500;\n\nfunction w1ClearAll() {\n document.getElementById('w1-c-green').className = 'w1-circle';\n document.getElementById('w1-c-amber').className = 'w1-circle';\n document.getElementById('w1-c-red').className = 'w1-circle';\n document.getElementById('w1-l-green').className = 'w1-circle-label';\n document.getElementById('w1-l-amber').className = 'w1-circle-label';\n document.getElementById('w1-l-red').className = 'w1-circle-label';\n document.getElementById('w1-message').className = '';\n}\n\nfunction w1ApplyState(state) {\n w1ClearAll();\n var msgEl = document.getElementById('w1-message');\n if (state === 0) {\n document.getElementById('w1-c-green').className = 'w1-circle green';\n document.getElementById('w1-l-green').className = 'w1-circle-label active';\n msgEl.className = 'green';\n msgEl.textContent = 'Systems nominal';\n } else if (state === 1) {\n document.getElementById('w1-c-amber').className = 'w1-circle amber';\n document.getElementById('w1-l-amber').className = 'w1-circle-label active';\n msgEl.className = 'amber';\n msgEl.textContent = 'Parameter degraded';\n } else if (state === 2) {\n document.getElementById('w1-c-red').className = 'w1-circle red';\n document.getElementById('w1-l-red').className = 'w1-circle-label active';\n msgEl.className = 'red';\n msgEl.textContent = 'CRITICAL';\n } else {\n msgEl.textContent = 'Waiting for data...';\n }\n}\n\nfunction w1Poll() {\n var footEl = document.getElementById('w1-footer');\n try {\n if (typeof window.cockpit === 'undefined') {\n footEl.textContent = 'API not ready';\n return;\n }\n var raw = window.cockpit.getDataLakeVariableData(W1_VAR);\n if (raw === null || raw === undefined) {\n w1ApplyState(-1);\n footEl.textContent = 'Waiting for ' + W1_VAR;\n return;\n }\n w1ApplyState(parseInt(raw, 10));\n footEl.textContent = 'OK';\n } catch(err) {\n footEl.textContent = 'Error: ' + err.message;\n }\n}\n\nsetTimeout(w1Poll, 300);\nsetInterval(w1Poll, W1_POLL_MS);"
}

View File

@ -1,230 +0,0 @@
<!DOCTYPE html>
<!--
============================================================
ARGONAUT 3 — MISSION STATUS
Widget 2
============================================================
Purpose : Show the current mission state, progress bar, and
active waypoint during autonomous operation.
Data : Reads two NAMED_VALUE variables from the Cockpit
data lake (published by the ROS2 → MAVLink bridge):
rov_mission_state (INT) 0=IDLE, 1=RUNNING,
2=PAUSED, 3=COMPLETE,
4=ABORTED
rov_mission_progress (FLOAT) 0.0 to 1.0
Import : Cockpit → Edit mode → Add DIY Widget →
gear icon → Import → select this file
Note : Shows "--" placeholders when bridge is not running.
Version : 1.0
============================================================
-->
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #09111a;
--bg2: #0e1d2e;
--border: #1e3050;
--text0: #d8eeff;
--text1: #6a9bbf;
--green: #00e09a;
--amber: #ffb830;
--red: #ff3a5a;
--blue: #00c8f0;
--dim: #1a2d42;
--mono: 'Courier New', monospace;
}
body {
background: var(--bg);
color: var(--text0);
font-family: var(--mono);
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 10px 14px;
gap: 8px;
}
/* ── State label (IDLE / RUNNING / etc.) ────────────────── */
#state-label {
font-size: 18px;
font-weight: bold;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text1); /* overridden per state */
}
#state-label.idle { color: var(--text1); }
#state-label.running { color: var(--green); }
#state-label.paused { color: var(--amber); }
#state-label.complete { color: var(--blue); }
#state-label.aborted { color: var(--red); }
#state-label.waiting { color: var(--dim); }
/* ── Progress bar ─────────────────────────────────────────── */
#bar-wrap {
width: 100%;
height: 8px;
background: var(--dim);
border-radius: 4px;
border: 1px solid var(--border);
overflow: hidden;
}
/* The filled portion */
#bar-fill {
height: 100%;
width: 0%; /* set dynamically via JS */
border-radius: 4px;
background: var(--green); /* overridden per state */
transition: width 0.5s ease, background 0.3s;
}
/* ── Percentage label ────────────────────────────────────── */
#pct-label {
font-size: 11px;
color: var(--text1);
align-self: flex-end;
margin-top: -4px;
}
/* ── Waypoint info line ──────────────────────────────────── */
#waypoint {
font-size: 10px;
color: var(--text1);
text-align: center;
}
/* ── Data source footer ──────────────────────────────────── */
#footer {
font-size: 9px;
color: #1e3050;
margin-top: 2px;
}
</style>
<body>
<div id="state-label" class="waiting"></div>
<div id="bar-wrap">
<div id="bar-fill"></div>
</div>
<div id="pct-label">—%</div>
<div id="waypoint">Waypoint: —</div>
<div id="footer">Waiting for rov_mission_state…</div>
</body>
<script>
// ── Config ────────────────────────────────────────────────
const VAR_STATE = 'rov_mission_state'; // INT 0-4
const VAR_PROGRESS = 'rov_mission_progress'; // FLOAT 0.0-1.0
// Future: VAR_WAYPOINT could be a string variable once
// Cockpit supports NAMED_VALUE_STR. For now the waypoint
// label is derived from state + progress.
const POLL_MS = 500;
// State constants matching the MissionStatus ROS2 message
const STATE = {
0: 'IDLE',
1: 'RUNNING',
2: 'PAUSED',
3: 'COMPLETE',
4: 'ABORTED',
};
// CSS class and bar colour per state
const STATE_META = {
0: { cls: 'idle', barColor: '#1a2d42' }, /* dim — no mission */
1: { cls: 'running', barColor: '#00e09a' }, /* green — active */
2: { cls: 'paused', barColor: '#ffb830' }, /* amber — paused */
3: { cls: 'complete', barColor: '#00c8f0' }, /* blue — done */
4: { cls: 'aborted', barColor: '#ff3a5a' }, /* red — aborted */
};
// ── DOM references ────────────────────────────────────────
const stateLabelEl = document.getElementById('state-label');
const barFillEl = document.getElementById('bar-fill');
const pctLabelEl = document.getElementById('pct-label');
const waypointEl = document.getElementById('waypoint');
const footerEl = document.getElementById('footer');
// ── Render ────────────────────────────────────────────────
function render(state, progress) {
const stateStr = STATE[state] ?? '?';
const meta = STATE_META[state] ?? { cls: 'waiting', barColor: '#1a2d42' };
const pct = Math.round((progress ?? 0) * 100);
// State label
stateLabelEl.className = meta.cls;
stateLabelEl.textContent = stateStr;
// Progress bar width + colour
barFillEl.style.width = `${pct}%`;
barFillEl.style.background = meta.barColor;
// Percentage text — suppress when idle or complete/aborted at 0
pctLabelEl.textContent = (state === 1 || state === 2)
? `${pct}%`
: (state === 3 ? '100%' : '—%');
// Waypoint info — simple derivation until a string variable is available
if (state === 1 || state === 2) {
waypointEl.textContent = `Progress ${pct}% complete`;
} else if (state === 3) {
waypointEl.textContent = 'Mission complete';
} else if (state === 4) {
waypointEl.textContent = 'Mission aborted — vehicle holding';
} else {
waypointEl.textContent = 'No mission active';
}
footerEl.textContent = `Updated ${new Date().toLocaleTimeString()}`;
}
// ── Poll the data lake ────────────────────────────────────
function poll() {
try {
if (typeof window.cockpit === 'undefined' ||
typeof window.cockpit.getDataLakeValue !== 'function') {
stateLabelEl.className = 'waiting';
stateLabelEl.textContent = '—';
footerEl.textContent = 'Cockpit API not ready';
return;
}
const stateRaw = window.cockpit.getDataLakeValue(VAR_STATE);
const progressRaw = window.cockpit.getDataLakeValue(VAR_PROGRESS);
if (stateRaw === null || stateRaw === undefined) {
stateLabelEl.className = 'waiting';
stateLabelEl.textContent = '—';
footerEl.textContent = `Waiting for ${VAR_STATE}`;
return;
}
const state = parseInt(stateRaw, 10);
const progress = parseFloat(progressRaw ?? 0);
render(state, progress);
} catch (err) {
stateLabelEl.className = 'waiting';
stateLabelEl.textContent = 'ERR';
footerEl.textContent = 'Poll error — see console';
console.error('[Mission Status Widget]', err);
}
}
// ── Start ─────────────────────────────────────────────────
setTimeout(poll, 300);
setInterval(poll, POLL_MS);
</script>

View File

@ -1,7 +1,7 @@
{
"html": "<div id=\"state-label\">--</div>\n<div id=\"bar-wrap\"><div id=\"bar-fill\"></div></div>\n<div id=\"pct-label\">--%</div>\n<div id=\"waypoint\">No mission active</div>\n<div id=\"footer\">Waiting for data</div>",
"html": "<div id=\"w2-state-label\">--</div>\n<div id=\"w2-bar-wrap\"><div id=\"w2-bar-fill\"></div></div>\n<div id=\"w2-pct-label\">--%</div>\n<div id=\"w2-waypoint\">No mission active</div>\n<div id=\"w2-footer\">Waiting for data</div>",
"css": "#state-label {\n font-size: 18px;\n font-weight: bold;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: #6a9bbf;\n text-align: center;\n font-family: monospace;\n}\n#state-label.idle { color: #6a9bbf; }\n#state-label.running { color: #00e09a; }\n#state-label.paused { color: #ffb830; }\n#state-label.complete { color: #00c8f0; }\n#state-label.aborted { color: #ff3a5a; }\n#bar-wrap {\n width: 100%;\n height: 8px;\n background: #1a2d42;\n border-radius: 4px;\n border: 1px solid #1e3050;\n overflow: hidden;\n margin: 6px 0;\n}\n#bar-fill {\n height: 100%;\n width: 0%;\n border-radius: 4px;\n background: #00e09a;\n transition: width 0.5s ease, background 0.3s;\n}\n#pct-label {\n font-size: 11px;\n color: #6a9bbf;\n text-align: right;\n width: 100%;\n font-family: monospace;\n}\n#waypoint {\n font-size: 10px;\n color: #6a9bbf;\n text-align: center;\n font-family: monospace;\n margin-top: 4px;\n}\n#footer {\n font-size: 9px;\n color: #2a4060;\n font-family: monospace;\n margin-top: 4px;\n}",
"css": "#w2-state-label { font-size: 18px; font-weight: bold; letter-spacing: 0.12em; text-transform: uppercase; color: #6a9bbf; text-align: center; font-family: monospace; } #w2-state-label.idle { color: #6a9bbf; } #w2-state-label.running { color: #00e09a; } #w2-state-label.paused { color: #ffb830; } #w2-state-label.complete { color: #00c8f0; } #w2-state-label.aborted { color: #ff3a5a; } #w2-bar-wrap { width: 100%; height: 8px; background: #1a2d42; border-radius: 4px; border: 1px solid #1e3050; overflow: hidden; margin: 6px 0; } #w2-bar-fill { height: 100%; width: 0%; border-radius: 4px; background: #00e09a; transition: width 0.5s ease, background 0.3s; } #w2-pct-label { font-size: 11px; color: #6a9bbf; text-align: right; width: 100%; font-family: monospace; } #w2-waypoint { font-size: 10px; color: #6a9bbf; text-align: center; font-family: monospace; margin-top: 4px; } #w2-footer { font-size: 9px; color: #2a4060; font-family: monospace; margin-top: 4px; }",
"js": "var VAR_STATE = 'external/rov-ms';\nvar VAR_PROGRESS = 'external/rov-mp';\nvar POLL_MS = 500;\n\nvar STATE_NAMES = {0:'IDLE', 1:'RUNNING', 2:'PAUSED', 3:'COMPLETE', 4:'ABORTED'};\nvar STATE_CLASS = {0:'idle', 1:'running', 2:'paused', 3:'complete', 4:'aborted'};\nvar STATE_COLOR = {0:'#1a2d42', 1:'#00e09a', 2:'#ffb830', 3:'#00c8f0', 4:'#ff3a5a'};\n\nvar stateLabelEl = document.getElementById('state-label');\nvar barFillEl = document.getElementById('bar-fill');\nvar pctLabelEl = document.getElementById('pct-label');\nvar waypointEl = document.getElementById('waypoint');\nvar footerEl = document.getElementById('footer');\n\nfunction render(state, progress) {\n var pct = Math.round((progress || 0) * 100);\n stateLabelEl.className = STATE_CLASS[state] || '';\n stateLabelEl.textContent = STATE_NAMES[state] || '?';\n barFillEl.style.width = pct + '%';\n barFillEl.style.background = STATE_COLOR[state] || '#1a2d42';\n pctLabelEl.textContent = (state === 1 || state === 2) ? pct + '%' : (state === 3 ? '100%' : '--%');\n if (state === 1 || state === 2) {\n waypointEl.textContent = 'Progress ' + pct + '% complete';\n } else if (state === 3) {\n waypointEl.textContent = 'Mission complete';\n } else if (state === 4) {\n waypointEl.textContent = 'Mission aborted - vehicle holding';\n } else {\n waypointEl.textContent = 'No mission active';\n }\n footerEl.textContent = 'Updated ' + new Date().toLocaleTimeString();\n}\n\nfunction poll() {\n try {\n if (typeof window.cockpit === 'undefined') { footerEl.textContent = 'API not ready'; return; }\n var raw = window.cockpit.getDataLakeValue(VAR_STATE);\n if (raw === null || raw === undefined) { footerEl.textContent = 'Waiting for ' + VAR_STATE; return; }\n var prog = window.cockpit.getDataLakeValue(VAR_PROGRESS);\n render(parseInt(raw, 10), parseFloat(prog || 0));\n } catch(err) { footerEl.textContent = 'Error'; }\n}\n\nsetTimeout(poll, 300);\nsetInterval(poll, POLL_MS);"
"js": "// W2 -- Mission Status\n// Reads external/rov-ms (state) and external/rov-mp (progress 0.0-1.0)\n// Getter confirmed: window.cockpit.getDataLakeVariableData(id)\n// State: 0=IDLE 1=RUNNING 2=PAUSED 3=COMPLETE 4=ABORTED\nvar W2_VAR_STATE = 'external/rov-ms';\nvar W2_VAR_PROGRESS = 'external/rov-mp';\nvar W2_POLL_MS = 500;\n\nvar W2_STATE_NAMES = {0:'IDLE', 1:'RUNNING', 2:'PAUSED', 3:'COMPLETE', 4:'ABORTED'};\nvar W2_STATE_CLASS = {0:'idle', 1:'running', 2:'paused', 3:'complete', 4:'aborted'};\nvar W2_STATE_COLOR = {0:'#1a2d42', 1:'#00e09a', 2:'#ffb830', 3:'#00c8f0', 4:'#ff3a5a'};\n\nfunction w2Render(state, progress) {\n var stateLabelEl = document.getElementById('w2-state-label');\n var barFillEl = document.getElementById('w2-bar-fill');\n var pctLabelEl = document.getElementById('w2-pct-label');\n var waypointEl = document.getElementById('w2-waypoint');\n var footerEl = document.getElementById('w2-footer');\n var pct = Math.round((progress || 0) * 100);\n stateLabelEl.className = W2_STATE_CLASS[state] || '';\n stateLabelEl.textContent = W2_STATE_NAMES[state] || '?';\n barFillEl.style.width = pct + '%';\n barFillEl.style.background = W2_STATE_COLOR[state] || '#1a2d42';\n pctLabelEl.textContent = (state === 1 || state === 2) ? pct + '%' : (state === 3 ? '100%' : '--%');\n if (state === 1 || state === 2) {\n waypointEl.textContent = 'Progress ' + pct + '% complete';\n } else if (state === 3) {\n waypointEl.textContent = 'Mission complete';\n } else if (state === 4) {\n waypointEl.textContent = 'Mission aborted - vehicle holding';\n } else {\n waypointEl.textContent = 'No mission active';\n }\n footerEl.textContent = 'Updated ' + new Date().toLocaleTimeString();\n}\n\nfunction w2Poll() {\n var footerEl = document.getElementById('w2-footer');\n try {\n if (typeof window.cockpit === 'undefined') { footerEl.textContent = 'API not ready'; return; }\n var raw = window.cockpit.getDataLakeVariableData(W2_VAR_STATE);\n if (raw === null || raw === undefined) { footerEl.textContent = 'Waiting for ' + W2_VAR_STATE; return; }\n var prog = window.cockpit.getDataLakeVariableData(W2_VAR_PROGRESS);\n w2Render(parseInt(raw, 10), parseFloat(prog || 0));\n } catch(err) { footerEl.textContent = 'Error: ' + err.message; }\n}\n\nsetTimeout(w2Poll, 300);\nsetInterval(w2Poll, W2_POLL_MS);"
}

View File

@ -1,303 +0,0 @@
<!DOCTYPE html>
<!--
============================================================
ARGONAUT 3 — ABORT BUTTON
Widget 3
============================================================
Purpose : Send an abort command to the mission executor via
a confirm dialog → POST to the FastAPI backend.
Safety : The confirm step is non-negotiable. A single press
shows a dialog. A second press (confirm) fires the
POST. The dialog auto-dismisses after 10 seconds
if not confirmed, to prevent accidental arming.
Endpoint: POST http://<FASTAPI_HOST>/abort
(FastAPI publishes true to /rov/mission/abort)
Config : Edit FASTAPI_HOST below to match your deployment.
During dev: the BlueOS VM IP + port 8081
In field: http://blueos.local:8081
Import : Cockpit → Edit mode → Add DIY Widget →
gear icon → Import → select this file
Version : 1.0
============================================================
-->
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #09111a;
--bg2: #160a0f; /* red-tinted background */
--border: #3a1020;
--text0: #d8eeff;
--text1: #8fb3d4;
--red: #ff3a5a;
--red-dim: #7a1a28;
--green: #00e09a;
--amber: #ffb830;
--mono: 'Courier New', monospace;
}
body {
background: var(--bg);
color: var(--text0);
font-family: var(--mono);
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 12px;
gap: 10px;
}
/* ── Main abort button ───────────────────────────────────── */
#abort-btn {
width: 100%;
padding: 14px 0;
background: var(--red-dim);
border: 2px solid var(--red);
border-radius: 6px;
color: var(--red);
font-family: var(--mono);
font-size: 15px;
font-weight: bold;
letter-spacing: 0.15em;
text-transform: uppercase;
cursor: pointer;
transition: background 0.15s, box-shadow 0.15s;
}
#abort-btn:hover {
background: #2a0c18;
box-shadow: 0 0 16px var(--red);
}
#abort-btn:active {
background: var(--red);
color: #fff;
}
/* Button disabled state (after abort sent) */
#abort-btn:disabled {
background: #1a0a10;
border-color: #3a1020;
color: var(--red-dim);
cursor: not-allowed;
box-shadow: none;
}
/* ── Confirm dialog overlay ──────────────────────────────── */
#confirm-overlay {
display: none; /* shown when confirm needed */
position: fixed;
inset: 0;
background: rgba(0,0,0,0.7);
align-items: center;
justify-content: center;
z-index: 10;
}
/* Show when active */
#confirm-overlay.active { display: flex; }
/* Dialog box */
#confirm-box {
background: #100510;
border: 2px solid var(--red);
border-radius: 8px;
padding: 18px;
max-width: 220px;
text-align: center;
box-shadow: 0 0 30px var(--red);
}
#confirm-box p {
font-size: 12px;
color: var(--text0);
line-height: 1.5;
margin-bottom: 14px;
}
#confirm-box .btn-row {
display: flex;
gap: 8px;
justify-content: center;
}
/* Confirm (yes) button */
#confirm-yes {
flex: 1;
padding: 8px;
background: var(--red);
border: none;
border-radius: 4px;
color: #fff;
font-family: var(--mono);
font-size: 12px;
font-weight: bold;
cursor: pointer;
letter-spacing: 0.08em;
}
#confirm-yes:hover { background: #ff6080; }
/* Cancel (no) button */
#confirm-no {
flex: 1;
padding: 8px;
background: transparent;
border: 1px solid var(--text1);
border-radius: 4px;
color: var(--text1);
font-family: var(--mono);
font-size: 12px;
cursor: pointer;
}
#confirm-no:hover { border-color: var(--text0); color: var(--text0); }
/* Auto-dismiss countdown */
#countdown {
font-size: 10px;
color: var(--red-dim);
margin-top: 8px;
}
/* ── Status message ──────────────────────────────────────── */
#status {
font-size: 10px;
color: var(--text1);
text-align: center;
min-height: 14px;
}
#status.ok { color: var(--green); }
#status.err { color: var(--red); }
</style>
<body>
<!-- Main button -->
<button id="abort-btn">⚠ ABORT MISSION</button>
<!-- Status message below the button -->
<div id="status">Standby</div>
<!-- Confirm dialog overlay (hidden until button pressed) -->
<div id="confirm-overlay">
<div id="confirm-box">
<p>Abort mission?<br>Vehicle will hold position<br>and await instruction.</p>
<div class="btn-row">
<button id="confirm-yes">ABORT</button>
<button id="confirm-no">Cancel</button>
</div>
<div id="countdown"></div>
</div>
</div>
</body>
<script>
// ── Config ────────────────────────────────────────────────
// Change this to match your FastAPI deployment:
// Dev: http://192.168.122.89:8081 (BlueOS VM IP)
// Field: http://blueos.local:8081
const FASTAPI_HOST = 'http://blueos.local:8081';
// Seconds before the confirm dialog auto-dismisses
const AUTO_DISMISS_SEC = 10;
// ── DOM references ────────────────────────────────────────
const abortBtn = document.getElementById('abort-btn');
const overlay = document.getElementById('confirm-overlay');
const confirmYes = document.getElementById('confirm-yes');
const confirmNo = document.getElementById('confirm-no');
const countdownEl = document.getElementById('countdown');
const statusEl = document.getElementById('status');
// ── Auto-dismiss timer handle ─────────────────────────────
let dismissTimer = null;
let countdownVal = AUTO_DISMISS_SEC;
// ── Show the confirm dialog ───────────────────────────────
function showConfirm() {
overlay.classList.add('active');
countdownVal = AUTO_DISMISS_SEC;
countdownEl.textContent = `Auto-cancel in ${countdownVal}s`;
// Countdown tick — dismiss automatically if ignored
dismissTimer = setInterval(() => {
countdownVal--;
countdownEl.textContent = `Auto-cancel in ${countdownVal}s`;
if (countdownVal <= 0) hideConfirm();
}, 1000);
}
// ── Hide the confirm dialog ───────────────────────────────
function hideConfirm() {
overlay.classList.remove('active');
clearInterval(dismissTimer);
dismissTimer = null;
countdownEl.textContent = '';
}
// ── Send the abort command ────────────────────────────────
async function sendAbort() {
hideConfirm();
abortBtn.disabled = true;
statusEl.className = '';
statusEl.textContent = 'Sending abort…';
try {
const res = await fetch(`${FASTAPI_HOST}/abort`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ abort: true }),
// Short timeout — we don't want to hang
signal: AbortSignal.timeout(5000),
});
if (res.ok) {
statusEl.className = 'ok';
statusEl.textContent = 'Abort sent — vehicle holding';
} else {
statusEl.className = 'err';
statusEl.textContent = `Server error: ${res.status}`;
abortBtn.disabled = false; // allow retry
}
} catch (err) {
statusEl.className = 'err';
statusEl.textContent = 'Network error — check connection';
abortBtn.disabled = false; // allow retry
console.error('[Abort Widget]', err);
}
}
// ── Event listeners ───────────────────────────────────────
// First press — show dialog
abortBtn.addEventListener('click', () => {
if (!abortBtn.disabled) showConfirm();
});
// Confirm — fire abort
confirmYes.addEventListener('click', sendAbort);
// Cancel — dismiss dialog
confirmNo.addEventListener('click', hideConfirm);
// ── Reset button after 30s (allows re-abort if needed) ───
// In real operations the button stays disabled until the
// vehicle is verified safe and a manual reset is done.
// Uncomment the block below to allow automatic re-enable:
/*
function resetButton() {
setTimeout(() => {
abortBtn.disabled = false;
statusEl.className = '';
statusEl.textContent = 'Standby';
}, 30000);
}
*/
</script>

View File

@ -1,7 +1,7 @@
{
"html": "<button id=\"abort-btn\">ABORT MISSION</button>\n<div id=\"status\">Standby</div>\n<div id=\"confirm-box\" style=\"display:none\">\n <p id=\"confirm-text\">Abort mission? Vehicle will hold position.</p>\n <div id=\"btn-row\">\n <button id=\"confirm-yes\">CONFIRM ABORT</button>\n <button id=\"confirm-no\">Cancel</button>\n </div>\n <div id=\"countdown\"></div>\n</div>",
"html": "<button id=\"w3-abort-btn\">ABORT MISSION</button>\n<div id=\"w3-status\">Standby</div>\n<div id=\"w3-confirm-box\" style=\"display:none\">\n <p id=\"w3-confirm-text\">Abort mission? Vehicle will hold position.</p>\n <div id=\"w3-btn-row\">\n <button id=\"w3-confirm-yes\">CONFIRM ABORT</button>\n <button id=\"w3-confirm-no\">Cancel</button>\n </div>\n <div id=\"w3-countdown\"></div>\n</div>",
"css": "#abort-btn {\n width: 100%;\n padding: 14px 0;\n background: #2a0c18;\n border: 2px solid #ff3a5a;\n border-radius: 6px;\n color: #ff3a5a;\n font-family: monospace;\n font-size: 14px;\n font-weight: bold;\n letter-spacing: 0.15em;\n text-transform: uppercase;\n cursor: pointer;\n}\n#abort-btn:hover { background: #3a1020; box-shadow: 0 0 16px #ff3a5a; }\n#abort-btn:disabled { background: #1a0a10; border-color: #3a1020; color: #3a1020; cursor: not-allowed; }\n#status {\n font-size: 10px;\n color: #6a9bbf;\n text-align: center;\n margin-top: 8px;\n font-family: monospace;\n min-height: 14px;\n}\n#status.ok { color: #00e09a; }\n#status.err { color: #ff3a5a; }\n#confirm-box {\n margin-top: 10px;\n background: #100510;\n border: 2px solid #ff3a5a;\n border-radius: 8px;\n padding: 12px;\n text-align: center;\n box-shadow: 0 0 20px #ff3a5a;\n}\n#confirm-text {\n font-size: 11px;\n color: #d8eeff;\n line-height: 1.5;\n margin-bottom: 10px;\n font-family: monospace;\n}\n#btn-row { display: flex; gap: 8px; justify-content: center; }\n#confirm-yes {\n flex: 1;\n padding: 8px;\n background: #ff3a5a;\n border: none;\n border-radius: 4px;\n color: #fff;\n font-family: monospace;\n font-size: 11px;\n font-weight: bold;\n cursor: pointer;\n}\n#confirm-yes:hover { background: #ff6080; }\n#confirm-no {\n flex: 1;\n padding: 8px;\n background: transparent;\n border: 1px solid #6a9bbf;\n border-radius: 4px;\n color: #6a9bbf;\n font-family: monospace;\n font-size: 11px;\n cursor: pointer;\n}\n#confirm-no:hover { color: #d8eeff; border-color: #d8eeff; }\n#countdown { font-size: 10px; color: #7a1a28; margin-top: 6px; font-family: monospace; }",
"css": "#w3-abort-btn { width: 100%; padding: 14px 0; background: #2a0c18; border: 2px solid #ff3a5a; border-radius: 6px; color: #ff3a5a; font-family: monospace; font-size: 14px; font-weight: bold; letter-spacing: 0.15em; text-transform: uppercase; cursor: pointer; } #w3-abort-btn:hover { background: #3a1020; box-shadow: 0 0 16px #ff3a5a; } #w3-abort-btn:disabled { background: #1a0a10; border-color: #3a1020; color: #3a1020; cursor: not-allowed; } #w3-status { font-size: 10px; color: #6a9bbf; text-align: center; margin-top: 8px; font-family: monospace; min-height: 14px; } #w3-status.ok { color: #00e09a; } #w3-status.err { color: #ff3a5a; } #w3-confirm-box { margin-top: 10px; background: #100510; border: 2px solid #ff3a5a; border-radius: 8px; padding: 12px; text-align: center; box-shadow: 0 0 20px #ff3a5a; } #w3-confirm-text { font-size: 11px; color: #d8eeff; line-height: 1.5; margin-bottom: 10px; font-family: monospace; } #w3-btn-row { display: flex; gap: 8px; justify-content: center; } #w3-confirm-yes { flex: 1; padding: 8px; background: #ff3a5a; border: none; border-radius: 4px; color: #fff; font-family: monospace; font-size: 11px; font-weight: bold; cursor: pointer; } #w3-confirm-yes:hover { background: #ff6080; } #w3-confirm-no { flex: 1; padding: 8px; background: transparent; border: 1px solid #6a9bbf; border-radius: 4px; color: #6a9bbf; font-family: monospace; font-size: 11px; cursor: pointer; } #w3-confirm-no:hover { color: #d8eeff; border-color: #d8eeff; } #w3-countdown { font-size: 10px; color: #7a1a28; margin-top: 6px; font-family: monospace; }",
"js": "var FASTAPI_HOST = 'http://blueos.local:8081';\nvar AUTO_DISMISS_SEC = 10;\n\nvar abortBtn = document.getElementById('abort-btn');\nvar statusEl = document.getElementById('status');\nvar confirmBox = document.getElementById('confirm-box');\nvar confirmYes = document.getElementById('confirm-yes');\nvar confirmNo = document.getElementById('confirm-no');\nvar countdownEl= document.getElementById('countdown');\n\nvar dismissTimer = null;\nvar countdownVal = AUTO_DISMISS_SEC;\n\nfunction showConfirm() {\n confirmBox.style.display = 'block';\n countdownVal = AUTO_DISMISS_SEC;\n countdownEl.textContent = 'Auto-cancel in ' + countdownVal + 's';\n dismissTimer = setInterval(function() {\n countdownVal--;\n countdownEl.textContent = 'Auto-cancel in ' + countdownVal + 's';\n if (countdownVal <= 0) hideConfirm();\n }, 1000);\n}\n\nfunction hideConfirm() {\n confirmBox.style.display = 'none';\n clearInterval(dismissTimer);\n dismissTimer = null;\n countdownEl.textContent = '';\n}\n\nfunction sendAbort() {\n hideConfirm();\n abortBtn.disabled = true;\n statusEl.className = '';\n statusEl.textContent = 'Sending abort...';\n fetch(FASTAPI_HOST + '/abort', {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({abort: true})\n })\n .then(function(res) {\n if (res.ok) {\n statusEl.className = 'ok';\n statusEl.textContent = 'Abort sent - vehicle holding';\n } else {\n statusEl.className = 'err';\n statusEl.textContent = 'Server error: ' + res.status;\n abortBtn.disabled = false;\n }\n })\n .catch(function(err) {\n statusEl.className = 'err';\n statusEl.textContent = 'Network error - check connection';\n abortBtn.disabled = false;\n console.error('[Abort]', err);\n });\n}\n\nabortBtn.addEventListener('click', function() {\n if (!abortBtn.disabled) showConfirm();\n});\nconfirmYes.addEventListener('click', sendAbort);\nconfirmNo.addEventListener('click', hideConfirm);"
"js": "// W3 -- Abort Button\n// Confirm dialog with 10s auto-dismiss, then POST to FastAPI /abort\n// No data lake reads needed -- action widget only\n// FASTAPI_HOST: update before field deployment\nvar W3_FASTAPI_HOST = 'http://192.168.1.101:8081';\nvar W3_AUTO_DISMISS_SEC = 10;\nvar w3DismissTimer = null;\nvar w3CountdownVal = W3_AUTO_DISMISS_SEC;\n\nfunction w3ShowConfirm() {\n document.getElementById('w3-confirm-box').style.display = 'block';\n w3CountdownVal = W3_AUTO_DISMISS_SEC;\n document.getElementById('w3-countdown').textContent = 'Auto-cancel in ' + w3CountdownVal + 's';\n w3DismissTimer = setInterval(function() {\n w3CountdownVal--;\n document.getElementById('w3-countdown').textContent = 'Auto-cancel in ' + w3CountdownVal + 's';\n if (w3CountdownVal <= 0) { w3HideConfirm(); }\n }, 1000);\n}\n\nfunction w3HideConfirm() {\n document.getElementById('w3-confirm-box').style.display = 'none';\n clearInterval(w3DismissTimer);\n w3DismissTimer = null;\n document.getElementById('w3-countdown').textContent = '';\n}\n\nfunction w3SendAbort() {\n w3HideConfirm();\n document.getElementById('w3-abort-btn').disabled = true;\n var statusEl = document.getElementById('w3-status');\n statusEl.className = '';\n statusEl.textContent = 'Sending abort...';\n fetch(W3_FASTAPI_HOST + '/abort', {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({abort: true})\n })\n .then(function(res) {\n if (res.ok) {\n statusEl.className = 'ok';\n statusEl.textContent = 'Abort sent - vehicle holding';\n } else {\n statusEl.className = 'err';\n statusEl.textContent = 'Server error: ' + res.status;\n document.getElementById('w3-abort-btn').disabled = false;\n }\n })\n .catch(function(err) {\n statusEl.className = 'err';\n statusEl.textContent = 'Network error - check connection';\n document.getElementById('w3-abort-btn').disabled = false;\n });\n}\n\n// Attach event listeners inside setTimeout so DOM is ready\nsetTimeout(function() {\n document.getElementById('w3-abort-btn').addEventListener('click', function() {\n if (!document.getElementById('w3-abort-btn').disabled) { w3ShowConfirm(); }\n });\n document.getElementById('w3-confirm-yes').addEventListener('click', w3SendAbort);\n document.getElementById('w3-confirm-no').addEventListener('click', w3HideConfirm);\n}, 100);"
}

View File

@ -1,176 +0,0 @@
<!DOCTYPE html>
<!--
============================================================
ARGONAUT 3 — MISSION SETUP BUTTON
Widget 4
============================================================
Purpose : Open the mission setup page in a new browser tab
without leaving the Cockpit dive view.
Visual : Subdued (dim) when a mission is active (running or
paused) to indicate it is available but not the
current priority. Full brightness when IDLE —
prompts the operator to configure before diving.
Data : Reads rov_mission_state to detect active mission.
Action : window.open() to setup page URL.
Config : Edit SETUP_URL below to match your deployment.
Import : Cockpit → Edit mode → Add DIY Widget →
gear icon → Import → select this file
Version : 1.0
============================================================
-->
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #09111a;
--border: #1e3050;
--text0: #d8eeff;
--text1: #6a9bbf;
--accent: #00c8f0;
--dim: #1a2d42;
--amber: #ffb830;
--mono: 'Courier New', monospace;
}
body {
background: var(--bg);
color: var(--text0);
font-family: var(--mono);
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 10px;
gap: 6px;
}
/* ── Setup button ────────────────────────────────────────── */
#setup-btn {
width: 100%;
padding: 10px 0;
background: transparent;
border: 1px solid var(--accent);
border-radius: 5px;
color: var(--accent);
font-family: var(--mono);
font-size: 12px;
font-weight: bold;
letter-spacing: 0.1em;
text-transform: uppercase;
cursor: pointer;
transition: background 0.2s, border-color 0.2s, color 0.2s, opacity 0.3s;
}
#setup-btn:hover {
background: rgba(0,200,240,0.1);
box-shadow: 0 0 10px rgba(0,200,240,0.3);
}
/* Subdued state when mission is active */
#setup-btn.subdued {
border-color: var(--dim);
color: var(--text1);
opacity: 0.5;
}
#setup-btn.subdued:hover {
opacity: 0.75;
background: transparent;
box-shadow: none;
}
/* ── Status line ─────────────────────────────────────────── */
#status {
font-size: 9px;
color: var(--text1);
text-align: center;
}
#status.active-warn { color: var(--amber); }
</style>
<body>
<button id="setup-btn">⚙ Mission Setup</button>
<div id="status"></div>
</body>
<script>
// ── Config ────────────────────────────────────────────────
// URL for the mission setup page served by the extension backend
const SETUP_URL = 'http://blueos.local:8081/setup';
// Data lake variable for mission state (to detect active mission)
const VAR_STATE = 'rov_mission_state';
const POLL_MS = 1000; // less frequent poll — state changes slowly
// Mission state values (matches rov_interfaces/MissionStatus)
const STATE_IDLE = 0;
const STATE_RUNNING = 1;
const STATE_PAUSED = 2;
const STATE_COMPLETE = 3;
const STATE_ABORTED = 4;
// ── DOM references ────────────────────────────────────────
const setupBtn = document.getElementById('setup-btn');
const statusEl = document.getElementById('status');
// ── Button click — open setup page ───────────────────────
setupBtn.addEventListener('click', () => {
window.open(SETUP_URL, '_blank', 'noopener,noreferrer');
});
// ── Poll mission state to control button appearance ───────
function poll() {
try {
if (typeof window.cockpit === 'undefined' ||
typeof window.cockpit.getDataLakeValue !== 'function') {
// API not ready — show neutral state
setupBtn.classList.remove('subdued');
statusEl.className = '';
statusEl.textContent = 'Connecting…';
return;
}
const stateRaw = window.cockpit.getDataLakeValue(VAR_STATE);
if (stateRaw === null || stateRaw === undefined) {
// Bridge not running — show button normally (no mission context)
setupBtn.classList.remove('subdued');
statusEl.className = '';
statusEl.textContent = 'No mission loaded';
return;
}
const state = parseInt(stateRaw, 10);
if (state === STATE_RUNNING || state === STATE_PAUSED) {
// Mission in progress — subdue the button (still accessible)
setupBtn.classList.add('subdued');
statusEl.className = 'active-warn';
statusEl.textContent = state === STATE_RUNNING
? 'Mission running — setup changes not recommended'
: 'Mission paused — setup changes not recommended';
} else {
// IDLE / COMPLETE / ABORTED — full brightness
setupBtn.classList.remove('subdued');
statusEl.className = '';
statusEl.textContent = state === STATE_IDLE
? 'No mission loaded — configure before diving'
: 'Ready to configure next mission';
}
} catch (err) {
console.error('[Setup Button Widget]', err);
}
}
// ── Start ─────────────────────────────────────────────────
setTimeout(poll, 300);
setInterval(poll, POLL_MS);
</script>

View File

@ -1,7 +1,7 @@
{
"html": "<button id=\"setup-btn\">MISSION SETUP</button>\n<div id=\"status\">--</div>",
"html": "<button id=\"w4-setup-btn\">MISSION SETUP</button>\n<div id=\"w4-status\">--</div>",
"css": "#setup-btn {\n width: 100%;\n padding: 10px 0;\n background: transparent;\n border: 1px solid #00c8f0;\n border-radius: 5px;\n color: #00c8f0;\n font-family: monospace;\n font-size: 12px;\n font-weight: bold;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n cursor: pointer;\n transition: background 0.2s, opacity 0.3s;\n}\n#setup-btn:hover { background: rgba(0,200,240,0.1); }\n#setup-btn.subdued { border-color: #1a2d42; color: #6a9bbf; opacity: 0.5; }\n#setup-btn.subdued:hover { opacity: 0.75; background: transparent; }\n#status {\n font-size: 9px;\n color: #6a9bbf;\n text-align: center;\n margin-top: 6px;\n font-family: monospace;\n}\n#status.warn { color: #ffb830; }",
"css": "#w4-setup-btn { width: 100%; padding: 10px 0; background: transparent; border: 1px solid #00c8f0; border-radius: 5px; color: #00c8f0; font-family: monospace; font-size: 12px; font-weight: bold; letter-spacing: 0.1em; text-transform: uppercase; cursor: pointer; transition: background 0.2s, opacity 0.3s; } #w4-setup-btn:hover { background: rgba(0,200,240,0.1); } #w4-setup-btn.subdued { border-color: #1a2d42; color: #6a9bbf; opacity: 0.5; } #w4-setup-btn.subdued:hover { opacity: 0.75; background: transparent; } #w4-status { font-size: 9px; color: #6a9bbf; text-align: center; margin-top: 6px; font-family: monospace; } #w4-status.warn { color: #ffb830; }",
"js": "var SETUP_URL = 'http://blueos.local:8081/setup';\nvar VAR_STATE = 'rov_mission_state';\nvar POLL_MS = 1000;\n\nvar setupBtn = document.getElementById('setup-btn');\nvar statusEl = document.getElementById('status');\n\nsetupBtn.addEventListener('click', function() {\n window.open(SETUP_URL, '_blank');\n});\n\nfunction poll() {\n try {\n if (typeof window.cockpit === 'undefined') {\n statusEl.className = '';\n statusEl.textContent = 'Connecting...';\n return;\n }\n var raw = window.cockpit.getDataLakeValue(VAR_STATE);\n if (raw === null || raw === undefined) {\n setupBtn.className = '';\n statusEl.className = '';\n statusEl.textContent = 'No mission loaded';\n return;\n }\n var state = parseInt(raw, 10);\n if (state === 1 || state === 2) {\n setupBtn.className = 'subdued';\n statusEl.className = 'warn';\n statusEl.textContent = state === 1 ? 'Mission running' : 'Mission paused';\n } else {\n setupBtn.className = '';\n statusEl.className = '';\n statusEl.textContent = state === 0 ? 'Configure before diving' : 'Ready for next mission';\n }\n } catch(err) { console.error('[SetupBtn]', err); }\n}\n\nsetTimeout(poll, 300);\nsetInterval(poll, POLL_MS);"
"js": "// W4 -- Mission Setup Button\n// Opens setup page in new tab. Dims when mission is RUNNING or PAUSED.\n// Reads external/rov-ms for mission state.\n// Getter confirmed: window.cockpit.getDataLakeVariableData(id)\n// SETUP_URL: update before field deployment\nvar W4_SETUP_URL = 'http://192.168.1.101:8081/setup';\nvar W4_VAR_STATE = 'external/rov-ms';\nvar W4_POLL_MS = 1000;\n\nfunction w4Poll() {\n var setupBtn = document.getElementById('w4-setup-btn');\n var statusEl = document.getElementById('w4-status');\n try {\n if (typeof window.cockpit === 'undefined') {\n statusEl.className = '';\n statusEl.textContent = 'Connecting...';\n return;\n }\n var raw = window.cockpit.getDataLakeVariableData(W4_VAR_STATE);\n if (raw === null || raw === undefined) {\n setupBtn.className = '';\n statusEl.className = '';\n statusEl.textContent = 'No mission loaded';\n return;\n }\n var state = parseInt(raw, 10);\n if (state === 1 || state === 2) {\n setupBtn.className = 'subdued';\n statusEl.className = 'warn';\n statusEl.textContent = state === 1 ? 'Mission running' : 'Mission paused';\n } else {\n setupBtn.className = '';\n statusEl.className = '';\n statusEl.textContent = state === 0 ? 'Configure before diving' : 'Ready for next mission';\n }\n } catch(err) { statusEl.textContent = 'Error'; }\n}\n\n// Attach click listener inside setTimeout so DOM is ready\nsetTimeout(function() {\n document.getElementById('w4-setup-btn').addEventListener('click', function() {\n window.open(W4_SETUP_URL, '_blank');\n });\n}, 100);\n\nsetTimeout(w4Poll, 300);\nsetInterval(w4Poll, W4_POLL_MS);"
}

View File

@ -1,270 +0,0 @@
<!DOCTYPE html>
<!--
============================================================
ARGONAUT 3 — BATTERY / RETURN BUDGET
Widget 5
============================================================
Purpose : Show battery percentage together with the dynamic
return budget. Highlights amber when headroom
(battery return budget) drops below 15%.
Data : Reads two NAMED_VALUE variables from the data lake:
rov_battery_pct (FLOAT) 0.0100.0 (%)
rov_return_budget (FLOAT) 0.0100.0 (%)
(published by FastAPI backend based on depth,
distance, and thruster history)
Falls back to MAVROS native battery variable if
rov_battery_pct is not yet available.
Import : Cockpit → Edit mode → Add DIY Widget →
gear icon → Import → select this file
Version : 1.0
============================================================
-->
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #09111a;
--bg2: #0e1d2e;
--border: #1e3050;
--text0: #d8eeff;
--text1: #6a9bbf;
--green: #00e09a;
--amber: #ffb830;
--red: #ff3a5a;
--dim: #1a2d42;
--mono: 'Courier New', monospace;
}
body {
background: var(--bg);
color: var(--text0);
font-family: var(--mono);
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 10px 14px;
gap: 6px;
}
/* ── Battery icon + percentage ───────────────────────────── */
#batt-row {
display: flex;
align-items: center;
gap: 10px;
}
/* SVG battery icon housing */
#batt-icon {
position: relative;
width: 44px;
height: 22px;
border: 2px solid var(--text1);
border-radius: 3px;
}
/* Battery terminal nub on right */
#batt-icon::after {
content: '';
position: absolute;
right: -7px;
top: 50%;
transform: translateY(-50%);
width: 5px;
height: 10px;
background: var(--text1);
border-radius: 0 2px 2px 0;
}
/* Fill bar inside the icon */
#batt-fill {
position: absolute;
left: 1px;
top: 1px;
bottom: 1px;
width: 80%; /* set by JS */
border-radius: 1px;
background: var(--green);
transition: width 0.5s ease, background 0.3s;
}
/* Large percentage number */
#batt-pct {
font-size: 28px;
font-weight: bold;
color: var(--green);
transition: color 0.3s;
min-width: 56px;
text-align: right;
}
#batt-pct.amber { color: var(--amber); }
#batt-pct.red { color: var(--red); }
/* ── Return budget line ──────────────────────────────────── */
#budget-row {
display: flex;
align-items: center;
gap: 6px;
font-size: 10px;
color: var(--text1);
}
#budget-val {
color: var(--text0);
font-weight: bold;
}
#budget-val.warn { color: var(--amber); }
/* ── Headroom indicator ──────────────────────────────────── */
#headroom-row {
font-size: 10px;
color: var(--text1);
}
#headroom-row.warn { color: var(--amber); }
#headroom-row.crit { color: var(--red); }
/* ── Footer ──────────────────────────────────────────────── */
#footer {
font-size: 9px;
color: #1e3050;
margin-top: 2px;
}
</style>
<body>
<!-- Battery visual + percentage -->
<div id="batt-row">
<div id="batt-icon">
<div id="batt-fill"></div>
</div>
<div id="batt-pct">--%</div>
</div>
<!-- Return budget required -->
<div id="budget-row">
Return budget: <span id="budget-val">--%</span> required
</div>
<!-- Available headroom -->
<div id="headroom-row" id="headroom">Headroom: --</div>
<!-- Data source status -->
<div id="footer">Waiting for rov_battery_pct…</div>
</body>
<script>
// ── Config ────────────────────────────────────────────────
const VAR_BATTERY = 'rov_battery_pct'; // FLOAT 0100
const VAR_BUDGET = 'rov_return_budget'; // FLOAT 0100
// Fallback MAVROS native battery variable (different name in data lake)
const VAR_MAVROS_BATT = 'BATTERY_STATUS/voltages/0'; // adjust if needed
// Thresholds (percentage points of headroom)
const WARN_HEADROOM = 15; // < 15% headroom amber
const CRIT_HEADROOM = 5; // < 5% headroom red
const POLL_MS = 1000;
// ── DOM references ────────────────────────────────────────
const battFill = document.getElementById('batt-fill');
const battPct = document.getElementById('batt-pct');
const budgetVal = document.getElementById('budget-val');
const headroomRow = document.getElementById('headroom-row');
const footerEl = document.getElementById('footer');
// ── Render ────────────────────────────────────────────────
function render(battPctVal, budgetPctVal) {
// Clamp to 0100
const batt = Math.max(0, Math.min(100, battPctVal ?? 0));
const budget = Math.max(0, Math.min(100, budgetPctVal ?? 0));
const margin = batt - budget;
// Battery icon fill width (percentage of inner width)
battFill.style.width = `${batt}%`;
// Battery icon fill colour
let fillColor;
if (batt > 40) {
fillColor = '#00e09a'; /* green */
} else if (batt > 20) {
fillColor = '#ffb830'; /* amber */
} else {
fillColor = '#ff3a5a'; /* red */
}
battFill.style.background = fillColor;
// Percentage text + class
battPct.textContent = `${Math.round(batt)}%`;
battPct.className = batt > 40 ? '' : batt > 20 ? 'amber' : 'red';
// Return budget value
budgetVal.textContent = `${Math.round(budget)}%`;
// Headroom and warning state
headroomRow.textContent = `Headroom: ${Math.round(margin)}%`;
if (margin < CRIT_HEADROOM) {
headroomRow.className = 'crit';
budgetVal.className = 'warn';
} else if (margin < WARN_HEADROOM) {
headroomRow.className = 'warn';
budgetVal.className = 'warn';
} else {
headroomRow.className = '';
budgetVal.className = '';
}
footerEl.textContent = `Updated ${new Date().toLocaleTimeString()}`;
}
// ── Poll ──────────────────────────────────────────────────
function poll() {
try {
if (typeof window.cockpit === 'undefined' ||
typeof window.cockpit.getDataLakeValue !== 'function') {
footerEl.textContent = 'Cockpit API not ready';
return;
}
// Try primary variable first; fall back to MAVROS native
let battRaw = window.cockpit.getDataLakeValue(VAR_BATTERY);
let source = VAR_BATTERY;
if (battRaw === null || battRaw === undefined) {
battRaw = window.cockpit.getDataLakeValue(VAR_MAVROS_BATT);
source = VAR_MAVROS_BATT;
}
if (battRaw === null || battRaw === undefined) {
footerEl.textContent = 'Waiting for battery data…';
return;
}
const budgetRaw = window.cockpit.getDataLakeValue(VAR_BUDGET);
const budget = (budgetRaw !== null && budgetRaw !== undefined)
? parseFloat(budgetRaw)
: 0; // 0 if budget not yet calculated
render(parseFloat(battRaw), budget);
// Note source in footer so we know which variable was used
if (source !== VAR_BATTERY) {
footerEl.textContent += ` (via ${source})`;
}
} catch (err) {
footerEl.textContent = 'Poll error — see console';
console.error('[Battery Widget]', err);
}
}
// ── Start ─────────────────────────────────────────────────
setTimeout(poll, 300);
setInterval(poll, POLL_MS);
</script>

View File

@ -1,7 +1,7 @@
{
"html": "<div id=\"section-label\">RETURN BUDGET</div>\n<div class=\"data-row\">\n <span class=\"data-label\">Battery</span>\n <span class=\"data-value\" id=\"batt-val\">--%</span>\n</div>\n<div class=\"data-row\">\n <span class=\"data-label\">Required</span>\n <span class=\"data-value\" id=\"budget-val\">--%</span>\n</div>\n<div id=\"divider\"></div>\n<div class=\"data-row\">\n <span class=\"data-label\">Headroom</span>\n <span class=\"data-value\" id=\"headroom-val\">--</span>\n</div>\n<div id=\"bar-bg\"><div id=\"bar-fill\" style=\"width:0%\"></div></div>\n<div id=\"status-text\">Waiting for data</div>\n<div id=\"footer\">--</div>",
"html": "<div id=\"w5-section-label\">RETURN BUDGET</div>\n<div class=\"w5-data-row\">\n <span class=\"w5-data-label\">Battery</span>\n <span class=\"w5-data-value\" id=\"w5-batt-val\">--%</span>\n</div>\n<div class=\"w5-data-row\">\n <span class=\"w5-data-label\">Required</span>\n <span class=\"w5-data-value\" id=\"w5-budget-val\">--%</span>\n</div>\n<div id=\"w5-divider\"></div>\n<div class=\"w5-data-row\">\n <span class=\"w5-data-label\">Headroom</span>\n <span class=\"w5-data-value\" id=\"w5-headroom-val\">--</span>\n</div>\n<div id=\"w5-bar-bg\"><div id=\"w5-bar-fill\" style=\"width:0%\"></div></div>\n<div id=\"w5-status-text\">Waiting for data</div>\n<div id=\"w5-footer\">--</div>",
"css": "#section-label {\n font-size: 9px;\n color: #6a9bbf;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n font-family: monospace;\n width: 100%;\n margin-bottom: 4px;\n}\n.data-row {\n display: flex;\n width: 100%;\n justify-content: space-between;\n align-items: baseline;\n margin: 2px 0;\n}\n.data-label {\n font-size: 10px;\n color: #6a9bbf;\n font-family: monospace;\n}\n.data-value {\n font-size: 16px;\n font-weight: bold;\n color: #d8eeff;\n font-family: monospace;\n min-width: 48px;\n text-align: right;\n}\n.data-value.green { color: #00e09a; }\n.data-value.amber { color: #ffb830; }\n.data-value.red { color: #ff3a5a; }\n#divider {\n width: 100%;\n height: 1px;\n background: #1e3050;\n margin: 4px 0;\n}\n#bar-bg {\n width: 100%;\n height: 5px;\n background: #1a2d42;\n border-radius: 3px;\n overflow: hidden;\n margin: 3px 0;\n}\n#bar-fill {\n height: 100%;\n border-radius: 3px;\n background: #00e09a;\n transition: width 0.5s ease, background 0.3s;\n}\n#status-text {\n font-size: 10px;\n color: #6a9bbf;\n text-align: center;\n font-family: monospace;\n min-height: 12px;\n}\n#status-text.green { color: #00e09a; }\n#status-text.amber { color: #ffb830; }\n#status-text.red { color: #ff3a5a; }\n#footer {\n font-size: 9px;\n color: #1a2d42;\n font-family: monospace;\n margin-top: 2px;\n}",
"css": "#w5-section-label { font-size: 9px; color: #6a9bbf; text-transform: uppercase; letter-spacing: 0.1em; font-family: monospace; width: 100%; margin-bottom: 4px; } .w5-data-row { display: flex; width: 100%; justify-content: space-between; align-items: baseline; margin: 2px 0; } .w5-data-label { font-size: 10px; color: #6a9bbf; font-family: monospace; } .w5-data-value { font-size: 16px; font-weight: bold; color: #d8eeff; font-family: monospace; min-width: 48px; text-align: right; } .w5-data-value.green { color: #00e09a; } .w5-data-value.amber { color: #ffb830; } .w5-data-value.red { color: #ff3a5a; } #w5-divider { width: 100%; height: 1px; background: #1e3050; margin: 4px 0; } #w5-bar-bg { width: 100%; height: 5px; background: #1a2d42; border-radius: 3px; overflow: hidden; margin: 3px 0; } #w5-bar-fill { height: 100%; border-radius: 3px; background: #00e09a; transition: width 0.5s ease, background 0.3s; } #w5-status-text { font-size: 10px; color: #6a9bbf; text-align: center; font-family: monospace; min-height: 12px; } #w5-status-text.green { color: #00e09a; } #w5-status-text.amber { color: #ffb830; } #w5-status-text.red { color: #ff3a5a; } #w5-footer { font-size: 9px; color: #1a2d42; font-family: monospace; margin-top: 2px; }",
"js": "var VAR_BATTERY = 'SYS_STATUS/battery_remaining';\nvar VAR_BUDGET = 'rov_return_budget';\nvar WARN = 15;\nvar CRIT = 5;\nvar POLL_MS = 1000;\n\nvar battValEl = document.getElementById('batt-val');\nvar budgetValEl = document.getElementById('budget-val');\nvar headroomValEl = document.getElementById('headroom-val');\nvar barFillEl = document.getElementById('bar-fill');\nvar statusTextEl = document.getElementById('status-text');\nvar footerEl = document.getElementById('footer');\n\nfunction setColour(el, sev) {\n el.classList.remove('green', 'amber', 'red');\n if (sev) el.classList.add(sev);\n}\n\nfunction render(batt, budget, budgetLive) {\n batt = Math.round(batt);\n budget = Math.round(budget);\n var margin = batt - budget;\n var barPct = batt > 0 ? Math.min(100, Math.max(0, (margin / batt) * 100)) : 0;\n var battSev = batt > 40 ? 'green' : batt > 20 ? 'amber' : 'red';\n var sev, msg, barColor;\n if (margin < CRIT) {\n sev = 'red'; msg = 'CRITICAL - return now';\n barColor = '#ff3a5a';\n } else if (margin < WARN) {\n sev = 'amber'; msg = 'Low headroom - consider returning';\n barColor = '#ffb830';\n } else {\n sev = 'green'; msg = 'Sufficient headroom';\n barColor = '#00e09a';\n }\n battValEl.textContent = batt + '%';\n budgetValEl.textContent = budget + '%';\n headroomValEl.textContent= margin + '%';\n setColour(battValEl, battSev);\n setColour(headroomValEl, sev);\n setColour(statusTextEl, sev);\n barFillEl.style.width = barPct + '%';\n barFillEl.style.background = barColor;\n statusTextEl.textContent = msg;\n footerEl.textContent = budgetLive\n ? 'Updated ' + new Date().toLocaleTimeString()\n : 'Updated ' + new Date().toLocaleTimeString() + ' (budget pending)';\n}\n\nfunction poll() {\n try {\n if (typeof window.cockpit === 'undefined') { statusTextEl.textContent = 'API not ready'; return; }\n var battRaw = window.cockpit.getDataLakeValue(VAR_BATTERY);\n if (battRaw === null || battRaw === undefined) { statusTextEl.textContent = 'Waiting for battery data'; return; }\n var batt = parseInt(battRaw, 10);\n if (batt === -1) { statusTextEl.textContent = 'Battery % not reported by vehicle'; battValEl.textContent = '??%'; return; }\n var budgetRaw = window.cockpit.getDataLakeValue(VAR_BUDGET);\n var budgetLive = budgetRaw !== null && budgetRaw !== undefined;\n render(batt, budgetLive ? parseFloat(budgetRaw) : 0, budgetLive);\n } catch(err) { statusTextEl.textContent = 'Error'; console.error('[Budget]', err); }\n}\n\nsetTimeout(poll, 300);\nsetInterval(poll, POLL_MS);"
"js": "// W5 -- Return Budget\n// Reads battery % from SYS_STATUS/battery_remaining (native ArduSub).\n// Reads return budget from external/rov-return-budget (cockpit_bridge, future).\n// Getter confirmed: window.cockpit.getDataLakeVariableData(id)\n// Headroom = battery% - required%. AMBER<15% RED<5%\nvar W5_VAR_BATTERY = 'SYS_STATUS/battery_remaining';\nvar W5_VAR_BUDGET = 'external/rov-return-budget';\nvar W5_WARN = 15;\nvar W5_CRIT = 5;\nvar W5_POLL_MS = 1000;\n\nfunction w5SetColour(el, sev) {\n el.classList.remove('green', 'amber', 'red');\n if (sev) { el.classList.add(sev); }\n}\n\nfunction w5Render(batt, budget, budgetLive) {\n var battValEl = document.getElementById('w5-batt-val');\n var budgetValEl = document.getElementById('w5-budget-val');\n var headroomValEl = document.getElementById('w5-headroom-val');\n var barFillEl = document.getElementById('w5-bar-fill');\n var statusTextEl = document.getElementById('w5-status-text');\n var footerEl = document.getElementById('w5-footer');\n batt = Math.round(batt);\n budget = Math.round(budget);\n var margin = batt - budget;\n var barPct = batt > 0 ? Math.min(100, Math.max(0, (margin / batt) * 100)) : 0;\n var battSev = batt > 40 ? 'green' : batt > 20 ? 'amber' : 'red';\n var sev, msg, barColor;\n if (margin < W5_CRIT) {\n sev = 'red'; msg = 'CRITICAL - return now'; barColor = '#ff3a5a';\n } else if (margin < W5_WARN) {\n sev = 'amber'; msg = 'Low headroom - consider returning'; barColor = '#ffb830';\n } else {\n sev = 'green'; msg = 'Sufficient headroom'; barColor = '#00e09a';\n }\n battValEl.textContent = batt + '%';\n budgetValEl.textContent = budget + '%';\n headroomValEl.textContent = margin + '%';\n w5SetColour(battValEl, battSev);\n w5SetColour(headroomValEl, sev);\n w5SetColour(statusTextEl, sev);\n barFillEl.style.width = barPct + '%';\n barFillEl.style.background = barColor;\n statusTextEl.textContent = msg;\n footerEl.textContent = 'Updated ' + new Date().toLocaleTimeString() +\n (budgetLive ? '' : ' (budget pending)');\n}\n\nfunction w5Poll() {\n var statusTextEl = document.getElementById('w5-status-text');\n try {\n if (typeof window.cockpit === 'undefined') { statusTextEl.textContent = 'API not ready'; return; }\n var battRaw = window.cockpit.getDataLakeVariableData(W5_VAR_BATTERY);\n if (battRaw === null || battRaw === undefined) { statusTextEl.textContent = 'Waiting for battery data'; return; }\n var batt = parseInt(battRaw, 10);\n if (batt === -1) {\n statusTextEl.textContent = 'Battery % not reported by vehicle';\n document.getElementById('w5-batt-val').textContent = '??%';\n return;\n }\n var budgetRaw = window.cockpit.getDataLakeVariableData(W5_VAR_BUDGET);\n var budgetLive = budgetRaw !== null && budgetRaw !== undefined;\n w5Render(batt, budgetLive ? parseFloat(budgetRaw) : 0, budgetLive);\n } catch(err) { statusTextEl.textContent = 'Error: ' + err.message; }\n}\n\nsetTimeout(w5Poll, 300);\nsetInterval(w5Poll, W5_POLL_MS);"
}