Boundary v0: WebSocket with heartbeats, acked commands and mock state

One socket on :7700 carries hello, heartbeats both ways (5 s timeout), 5 Hz
state snapshots of envelopes, and idempotent commands. 'mark' is forwarded to
the recorder's control socket. --mock synthesises degrading state flagged as
mock in every envelope; without it a client gets hello and heartbeats only.
Peers outside loopback, RFC 1918 and the tailnet are refused. Deployed on the
car Orin as yukart-gw.service.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
This commit is contained in:
2026-10-09 20:19:05 +09:00
parent aaeb836f01
commit a1ed8d8fc5
11 changed files with 809 additions and 3 deletions
+58
View File
@@ -0,0 +1,58 @@
//! Fan-out to clients. Drop-only: each client has a bounded queue that loses its
//! oldest message when full, so a tablet with its screen off never stalls anything.
use std::collections::VecDeque;
use std::sync::{Arc, Condvar, Mutex};
use std::time::Duration;
const QUEUE: usize = 64;
pub struct Outbox {
q: Mutex<VecDeque<String>>,
cv: Condvar,
}
impl Outbox {
pub fn push(&self, msg: String) {
let mut q = self.q.lock().unwrap();
if q.len() >= QUEUE {
q.pop_front();
}
q.push_back(msg);
self.cv.notify_one();
}
/// Everything queued, waiting at most `wait` for something to arrive.
pub fn drain(&self, wait: Duration) -> Vec<String> {
let q = self.q.lock().unwrap();
let (mut q, _) = self.cv.wait_timeout_while(q, wait, |q| q.is_empty()).unwrap();
q.drain(..).collect()
}
}
#[derive(Default, Clone)]
pub struct Hub {
clients: Arc<Mutex<Vec<Arc<Outbox>>>>,
}
impl Hub {
pub fn join(&self) -> Arc<Outbox> {
let o = Arc::new(Outbox { q: Mutex::new(VecDeque::new()), cv: Condvar::new() });
self.clients.lock().unwrap().push(o.clone());
o
}
pub fn leave(&self, o: &Arc<Outbox>) {
self.clients.lock().unwrap().retain(|c| !Arc::ptr_eq(c, o));
}
pub fn broadcast(&self, msg: &str) {
for c in self.clients.lock().unwrap().iter() {
c.push(msg.to_string());
}
}
pub fn count(&self) -> usize {
self.clients.lock().unwrap().len()
}
}