diff --git a/lib/camview.dart b/lib/camview.dart index 8a2987f..f878c3c 100644 --- a/lib/camview.dart +++ b/lib/camview.dart @@ -28,6 +28,9 @@ class CamView extends ChangeNotifier { final _delays = []; String lastReply = ''; + /// Whether the Orin has the camera: null until it says, then its last notice. + bool? available; + final _window = <(int, int)>[]; // (arrival µs, bytes) over the last second final _clock = Stopwatch()..start(); @@ -55,6 +58,7 @@ class CamView extends ChangeNotifier { timeout: const Duration(seconds: 3), ); _s = s; + available = null; status = 'connected'; notifyListeners(); await _read(s); @@ -63,6 +67,7 @@ class CamView extends ChangeNotifier { status = '$e'.split('\n').first; } _s = null; + frame = null; // a frame from a closed connection is not live notifyListeners(); if (_running) await Future.delayed(const Duration(seconds: 1)); } @@ -92,7 +97,7 @@ class CamView extends ChangeNotifier { final bytes = Uint8List.sublistView(data, off + 16, off + 16 + body); off += 16 + body; if (len == 0) { - lastReply = utf8.decode(bytes); + _onText(utf8.decode(bytes)); } else { _onFrame(Uint8List.fromList(bytes), sentAt); } @@ -101,6 +106,16 @@ class CamView extends ChangeNotifier { } } + void _onText(String text) { + if (text == 'camera unavailable' || text == 'camera ok') { + available = text == 'camera ok'; + if (available == false) frame = null; // never show a stale frame as live + } else { + lastReply = text; + } + notifyListeners(); + } + void _onFrame(Uint8List jpeg, double sentAt) { final now = _clock.elapsedMicroseconds; _window.add((now, jpeg.length)); @@ -163,7 +178,9 @@ class _CamViewPageState extends State { child: f == null ? Center( child: Text( - cam.status, + cam.available == false + ? 'Camera unavailable on the Orin' + : cam.status, style: const TextStyle(color: Colors.white70), ), ) diff --git a/tool/camview_server.py b/tool/camview_server.py index 39b77f4..922f5aa 100644 --- a/tool/camview_server.py +++ b/tool/camview_server.py @@ -4,9 +4,12 @@ Streams the USB camera's own MJPEG frames, unmodified, to any client on 7710/tcp: server → client, per frame: [u32 length][f64 capture time, unix s][u32 seq][JPEG] - per reply: [u32 0][f64 time][u32 text length][UTF-8 text] + per text: [u32 0][f64 time][u32 text length][UTF-8 text] client → server, lines: "exposure auto" | "exposure <100 µs units>" | "gain <0-240>" - → one reply, "ok " or "err " + → one text reply, "ok " or "err " + +Texts also carry availability: "camera unavailable" when no frame has come for +UNAVAILABLE_S (none yet, or capture stopped), "camera ok" when frames resume. Latest-wins: each client gets the newest frame when it is ready for one, never a queue of old ones. Sockets are paced (SO_MAX_PACING_RATE, project-yukart decision @@ -25,6 +28,7 @@ import time SO_MAX_PACING_RATE = 47 CONTROLS = {"exposure", "gain"} +UNAVAILABLE_S = 3.0 class Latest: @@ -39,7 +43,7 @@ class Latest: self.frame = (seq, t, jpeg) self.cond.notify_all() - def after(self, seq, timeout=5.0): + def after(self, seq, timeout): with self.cond: self.cond.wait_for(lambda: self.frame and self.frame[0] > seq, timeout) return self.frame @@ -106,7 +110,13 @@ def serve_client(conn, peer, dev, latest, pace): conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) alive = threading.Event() alive.set() - send = threading.Lock() # frames and replies share the socket; never interleave them + send = threading.Lock() # frames and texts share the socket; never interleave them + + def send_text(text): + # Texts go in-band as a frame with length 0 and the text after the header. + msg = text.encode() + with send: + conn.sendall(struct.pack("!IdI", 0, time.time(), len(msg)) + msg) def reader(): buf = b"" @@ -120,10 +130,7 @@ def serve_client(conn, peer, dev, latest, pace): line, buf = buf.split(b"\n", 1) reply = control(dev, line.decode(errors="replace").strip()) print(f"{peer}: {line.decode(errors='replace').strip()} -> {reply}", flush=True) - # Replies go in-band as a frame with length 0 and the text after the header. - msg = reply.encode() - with send: - conn.sendall(struct.pack("!IdI", 0, time.time(), len(msg)) + msg) + send_text(reply) except OSError: pass alive.clear() @@ -131,11 +138,18 @@ def serve_client(conn, peer, dev, latest, pace): threading.Thread(target=reader, daemon=True).start() seq = 0 sent = 0 + available = None # unknown until the first frame or the first timeout try: while alive.is_set(): - f = latest.after(seq) + f = latest.after(seq, UNAVAILABLE_S) if not f or f[0] <= seq: + if available is not False: + available = False + send_text("camera unavailable") continue + if available is not True: + available = True + send_text("camera ok") seq, t, jpeg = f with send: conn.sendall(struct.pack("!IdI", len(jpeg), t, seq) + jpeg)