add the node manifest, dataflow, CI and docs
validate / validate (pull_request) Failing after 58m48s
validate / test (pull_request) Successful in 58m48s
validate / markdownlint (pull_request) Successful in 58m43s

dora-node.yml is the typed contract a consumer wires against, and the only one
they get: no source package is shared between node repositories. The jog,
request and ui ports stay untyped, because dora treats a port carrying
request_id as pattern-correlated and several Arrow schemas legitimately flow
over each — one per service, one per console message.

dataflow.yml runs the whole operator stack from checkouts: this node, the
WebSocket bridge and the arm.

web/ holds a placeholder page, not the real console. It exists so the dataflow
has something to serve and so the node can be checked end to end before the
Control section is ported across from the monolithic repo. It reads telemetry
and can take, release and stop; nothing else.
This commit is contained in:
2026-09-15 15:43:12 -05:00
parent 56b86699bd
commit 7250396a1f
6 changed files with 669 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>teleop — placeholder console</title>
<style>
:root { color-scheme: dark; }
body { margin: 0; padding: 24px; background: #15151c; color: #e6e6ef;
font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; }
h1 { font-size: 16px; margin: 0 0 4px; }
p.note { color: #8a8a9e; margin: 0 0 20px; max-width: 60ch; }
.card { background: #1d1d26; border: 1px solid #2c2c3a; border-radius: 8px;
padding: 16px; margin-bottom: 16px; max-width: 60ch; }
.row { display: flex; justify-content: space-between; gap: 16px; padding: 3px 0; }
.row span:last-child { color: #7fd1e8; }
button { background: #2c2c3a; color: #e6e6ef; border: 1px solid #3c3c4e;
border-radius: 6px; padding: 8px 14px; font: inherit; cursor: pointer; }
button:hover { background: #363648; }
button.stop { background: #5a1f27; border-color: #7d2b35; }
#link { color: #8a8a9e; }
</style>
</head>
<body>
<h1>teleop — placeholder console</h1>
<p class="note">
Not the real console. This page exists so the dataflow has something to serve and so the
node can be checked end to end before the Control section is ported across from the
monolithic repo. It reads telemetry and can take, release and stop — nothing else.
</p>
<div class="card" id="state"></div>
<div class="card">
<button onclick="send({type:'control', action:'take', preempt:true})">Take control</button>
<button onclick="send({type:'control', action:'release'})">Release</button>
<button class="stop" onclick="send({type:'estop'})">E-STOP</button>
<button onclick="send({type:'reset'})">Reset</button>
</div>
<div id="link"></div>
<script>
"use strict";
const KEYS = ["arm_state", "owner", "connected", "ready", "estop", "vacuum", "freedrive"];
let socket = null;
function send(payload) {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: "data", payload }));
}
}
function render(telemetry) {
const rows = KEYS.map((key) =>
`<div class="row"><span>${key}</span><span>${telemetry[key]}</span></div>`);
const pose = telemetry.position;
if (pose) {
const shown = ["x", "y", "z"].map((axis) => pose[axis].toFixed(1)).join(", ");
rows.push(`<div class="row"><span>position</span><span>${shown} mm</span></div>`);
}
document.getElementById("state").innerHTML = rows.join("");
}
function connect() {
// The socket is the bridge's port, not this page's — the page is served over
// HTTP on a different one.
const url = `ws://${location.hostname}:9000`;
document.getElementById("link").textContent = `socket: ${url}`;
socket = new WebSocket(url);
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type !== "data" || !message.payload) return;
if (message.payload.type === "telemetry") render(message.payload);
};
socket.onclose = () => {
document.getElementById("link").textContent = `socket: ${url} — closed, retrying`;
setTimeout(connect, 2000);
};
}
connect();
</script>
</body>
</html>