camview: tell the client when the camera is unavailable
The server sends "camera unavailable" after 3 s without a frame and "camera ok" when frames come; the app shows the notice instead of a stale frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
This commit is contained in:
+19
-2
@@ -28,6 +28,9 @@ class CamView extends ChangeNotifier {
|
|||||||
final _delays = <double>[];
|
final _delays = <double>[];
|
||||||
String lastReply = '';
|
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 _window = <(int, int)>[]; // (arrival µs, bytes) over the last second
|
||||||
final _clock = Stopwatch()..start();
|
final _clock = Stopwatch()..start();
|
||||||
|
|
||||||
@@ -55,6 +58,7 @@ class CamView extends ChangeNotifier {
|
|||||||
timeout: const Duration(seconds: 3),
|
timeout: const Duration(seconds: 3),
|
||||||
);
|
);
|
||||||
_s = s;
|
_s = s;
|
||||||
|
available = null;
|
||||||
status = 'connected';
|
status = 'connected';
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
await _read(s);
|
await _read(s);
|
||||||
@@ -63,6 +67,7 @@ class CamView extends ChangeNotifier {
|
|||||||
status = '$e'.split('\n').first;
|
status = '$e'.split('\n').first;
|
||||||
}
|
}
|
||||||
_s = null;
|
_s = null;
|
||||||
|
frame = null; // a frame from a closed connection is not live
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
if (_running) await Future<void>.delayed(const Duration(seconds: 1));
|
if (_running) await Future<void>.delayed(const Duration(seconds: 1));
|
||||||
}
|
}
|
||||||
@@ -92,7 +97,7 @@ class CamView extends ChangeNotifier {
|
|||||||
final bytes = Uint8List.sublistView(data, off + 16, off + 16 + body);
|
final bytes = Uint8List.sublistView(data, off + 16, off + 16 + body);
|
||||||
off += 16 + body;
|
off += 16 + body;
|
||||||
if (len == 0) {
|
if (len == 0) {
|
||||||
lastReply = utf8.decode(bytes);
|
_onText(utf8.decode(bytes));
|
||||||
} else {
|
} else {
|
||||||
_onFrame(Uint8List.fromList(bytes), sentAt);
|
_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) {
|
void _onFrame(Uint8List jpeg, double sentAt) {
|
||||||
final now = _clock.elapsedMicroseconds;
|
final now = _clock.elapsedMicroseconds;
|
||||||
_window.add((now, jpeg.length));
|
_window.add((now, jpeg.length));
|
||||||
@@ -163,7 +178,9 @@ class _CamViewPageState extends State<CamViewPage> {
|
|||||||
child: f == null
|
child: f == null
|
||||||
? Center(
|
? Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
cam.status,
|
cam.available == false
|
||||||
|
? 'Camera unavailable on the Orin'
|
||||||
|
: cam.status,
|
||||||
style: const TextStyle(color: Colors.white70),
|
style: const TextStyle(color: Colors.white70),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
+23
-9
@@ -4,9 +4,12 @@
|
|||||||
Streams the USB camera's own MJPEG frames, unmodified, to any client on 7710/tcp:
|
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]
|
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>"
|
client → server, lines: "exposure auto" | "exposure <100 µs units>" | "gain <0-240>"
|
||||||
→ one reply, "ok <state>" or "err <reason>"
|
→ one text reply, "ok <state>" or "err <reason>"
|
||||||
|
|
||||||
|
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
|
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
|
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
|
SO_MAX_PACING_RATE = 47
|
||||||
CONTROLS = {"exposure", "gain"}
|
CONTROLS = {"exposure", "gain"}
|
||||||
|
UNAVAILABLE_S = 3.0
|
||||||
|
|
||||||
|
|
||||||
class Latest:
|
class Latest:
|
||||||
@@ -39,7 +43,7 @@ class Latest:
|
|||||||
self.frame = (seq, t, jpeg)
|
self.frame = (seq, t, jpeg)
|
||||||
self.cond.notify_all()
|
self.cond.notify_all()
|
||||||
|
|
||||||
def after(self, seq, timeout=5.0):
|
def after(self, seq, timeout):
|
||||||
with self.cond:
|
with self.cond:
|
||||||
self.cond.wait_for(lambda: self.frame and self.frame[0] > seq, timeout)
|
self.cond.wait_for(lambda: self.frame and self.frame[0] > seq, timeout)
|
||||||
return self.frame
|
return self.frame
|
||||||
@@ -106,7 +110,13 @@ def serve_client(conn, peer, dev, latest, pace):
|
|||||||
conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||||
alive = threading.Event()
|
alive = threading.Event()
|
||||||
alive.set()
|
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():
|
def reader():
|
||||||
buf = b""
|
buf = b""
|
||||||
@@ -120,10 +130,7 @@ def serve_client(conn, peer, dev, latest, pace):
|
|||||||
line, buf = buf.split(b"\n", 1)
|
line, buf = buf.split(b"\n", 1)
|
||||||
reply = control(dev, line.decode(errors="replace").strip())
|
reply = control(dev, line.decode(errors="replace").strip())
|
||||||
print(f"{peer}: {line.decode(errors='replace').strip()} -> {reply}", flush=True)
|
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.
|
send_text(reply)
|
||||||
msg = reply.encode()
|
|
||||||
with send:
|
|
||||||
conn.sendall(struct.pack("!IdI", 0, time.time(), len(msg)) + msg)
|
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
alive.clear()
|
alive.clear()
|
||||||
@@ -131,11 +138,18 @@ def serve_client(conn, peer, dev, latest, pace):
|
|||||||
threading.Thread(target=reader, daemon=True).start()
|
threading.Thread(target=reader, daemon=True).start()
|
||||||
seq = 0
|
seq = 0
|
||||||
sent = 0
|
sent = 0
|
||||||
|
available = None # unknown until the first frame or the first timeout
|
||||||
try:
|
try:
|
||||||
while alive.is_set():
|
while alive.is_set():
|
||||||
f = latest.after(seq)
|
f = latest.after(seq, UNAVAILABLE_S)
|
||||||
if not f or f[0] <= seq:
|
if not f or f[0] <= seq:
|
||||||
|
if available is not False:
|
||||||
|
available = False
|
||||||
|
send_text("camera unavailable")
|
||||||
continue
|
continue
|
||||||
|
if available is not True:
|
||||||
|
available = True
|
||||||
|
send_text("camera ok")
|
||||||
seq, t, jpeg = f
|
seq, t, jpeg = f
|
||||||
with send:
|
with send:
|
||||||
conn.sendall(struct.pack("!IdI", len(jpeg), t, seq) + jpeg)
|
conn.sendall(struct.pack("!IdI", len(jpeg), t, seq) + jpeg)
|
||||||
|
|||||||
Reference in New Issue
Block a user