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:
parent
12b5161436
commit
66f9bd7440
6
widgets/.gitignore
vendored
Normal file
6
widgets/.gitignore
vendored
Normal 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
|
||||
@ -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.0–1.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.
|
||||
|
||||
7
widgets/diagnostics/w_probe_api.json
Normal file
7
widgets/diagnostics/w_probe_api.json
Normal file
File diff suppressed because one or more lines are too long
@ -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>
|
||||
@ -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>
|
||||
@ -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);"
|
||||
}
|
||||
@ -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>
|
||||
@ -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);"
|
||||
}
|
||||
@ -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>
|
||||
@ -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);"
|
||||
}
|
||||
|
||||
@ -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>
|
||||
@ -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);"
|
||||
}
|
||||
|
||||
@ -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.0–100.0 (%)
|
||||
rov_return_budget (FLOAT) 0.0–100.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 0–100
|
||||
const VAR_BUDGET = 'rov_return_budget'; // FLOAT 0–100
|
||||
// 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 0–100
|
||||
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>
|
||||
@ -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);"
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user