8f4e34d956
Orin -> tablet over NCM: paced streams up to 4 MB/s are clean; unpaced bursts and bulk hit multi-second holds that inflate the RTO to 10-16 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
151 lines
5.1 KiB
Python
151 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Orin side of the app's link test (lib/linktest.dart). Development tool, not a service.
|
|
|
|
The tablet's own sockets are the only ones bound to the USB link (`adb shell`
|
|
traffic goes over WiFi), so the app drives the test and this end only serves:
|
|
|
|
7791/tcp down: client sends one line, then reads until the server closes:
|
|
"<id> <secs>" zeros for <secs>
|
|
"<id> burst" then per line "<bytes>": exactly <bytes> zeros,
|
|
on the same connection until the client closes
|
|
"<id> paced <B/s> <secs>" 30 frames/s, each [u32 length][f64 send time][zeros],
|
|
length counting the whole frame
|
|
7792/tcp up: client sends "<id>\\n" then data until EOF; gets "<bytes>\\n" back
|
|
7793/udp echo
|
|
7794/tcp results: one JSON line per connection, appended to the results file
|
|
|
|
Every TCP test also logs this end's TCP_INFO (retransmits, RTT), which the app
|
|
cannot read. Run: python3 linktest_server.py [results.jsonl]
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import socket
|
|
import struct
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
RESULTS = os.path.expanduser(sys.argv[1] if len(sys.argv) > 1 else "~/linktest/results.jsonl")
|
|
CHUNK = b"\0" * 65536
|
|
lock = threading.Lock()
|
|
|
|
|
|
def log(rec):
|
|
rec = {"orin_ts": round(time.time(), 3), **rec}
|
|
with lock, open(RESULTS, "a") as f:
|
|
f.write(json.dumps(rec) + "\n")
|
|
print(json.dumps(rec), flush=True)
|
|
|
|
|
|
def tcp_info(sock):
|
|
# struct tcp_info: 8 bytes of u8 fields, then u32s; rtt is the 16th, total_retrans the 24th.
|
|
raw = sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_INFO, 104)
|
|
u = struct.unpack_from("8B24I", raw)
|
|
return {"retrans": u[8 + 23], "rtt_ms": u[8 + 15] / 1000, "rto_ms": u[8] / 1000}
|
|
|
|
|
|
def readline(conn):
|
|
buf = b""
|
|
while not buf.endswith(b"\n") and len(buf) < 256:
|
|
b = conn.recv(1)
|
|
if not b:
|
|
break
|
|
buf += b
|
|
return buf.decode().strip()
|
|
|
|
|
|
def down(conn, peer):
|
|
test_id, *args = readline(conn).split()
|
|
mode = args[0] if args[0] in ("burst", "paced") else "bulk"
|
|
sent = 0
|
|
try:
|
|
if mode == "burst":
|
|
# One warm connection, as an utterance would arrive on the open WebSocket.
|
|
while line := readline(conn):
|
|
left = int(line)
|
|
while left > 0:
|
|
n = conn.send(CHUNK[:min(left, len(CHUNK))])
|
|
left -= n
|
|
sent += n
|
|
elif mode == "paced":
|
|
rate, secs = float(args[1]), float(args[2])
|
|
size = max(12, int(rate / 30))
|
|
t0 = time.monotonic()
|
|
for i in range(int(secs * 30)):
|
|
# Pace on the schedule, not on the last send: a stall must not lower the offered rate.
|
|
delay = t0 + i / 30 - time.monotonic()
|
|
if delay > 0:
|
|
time.sleep(delay)
|
|
conn.sendall(struct.pack("!Id", size, time.time()) + bytes(size - 12))
|
|
sent += size
|
|
else:
|
|
end = time.monotonic() + float(args[0])
|
|
while time.monotonic() < end:
|
|
sent += conn.send(CHUNK)
|
|
except OSError as e:
|
|
log({"id": test_id, "kind": "tcp_down_orin", "mode": mode, "error": str(e)})
|
|
log({"id": test_id, "kind": "tcp_down_orin", "mode": mode, "args": args, "peer": peer, "bytes": sent,
|
|
**tcp_info(conn)})
|
|
|
|
|
|
def up(conn, peer):
|
|
test_id = readline(conn)
|
|
got = 0
|
|
t0 = time.monotonic()
|
|
while True:
|
|
b = conn.recv(65536)
|
|
if not b:
|
|
break
|
|
got += len(b)
|
|
secs = time.monotonic() - t0
|
|
conn.sendall(f"{got}\n".encode())
|
|
log({"id": test_id, "kind": "tcp_up_orin", "peer": peer, "bytes": got, "secs": round(secs, 3),
|
|
**tcp_info(conn)})
|
|
|
|
|
|
def results(conn, peer):
|
|
data = b""
|
|
while True:
|
|
b = conn.recv(65536)
|
|
if not b:
|
|
break
|
|
data += b
|
|
for line in data.decode().splitlines():
|
|
if line.strip():
|
|
log({"peer": peer, **json.loads(line)})
|
|
|
|
|
|
def serve_tcp(port, handler):
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
s.bind(("0.0.0.0", port))
|
|
s.listen(4)
|
|
while True:
|
|
conn, addr = s.accept()
|
|
|
|
def run(conn=conn, peer=addr[0]):
|
|
with conn:
|
|
try:
|
|
handler(conn, peer)
|
|
except Exception as e: # a broken test must not stop the server
|
|
print(f"{handler.__name__} {peer}: {e}", flush=True)
|
|
|
|
threading.Thread(target=run, daemon=True).start()
|
|
|
|
|
|
def serve_echo(port):
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
s.bind(("0.0.0.0", port))
|
|
while True:
|
|
data, addr = s.recvfrom(2048)
|
|
s.sendto(data, addr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
os.makedirs(os.path.dirname(RESULTS), exist_ok=True)
|
|
for port, h in ((7791, down), (7792, up), (7794, results)):
|
|
threading.Thread(target=serve_tcp, args=(port, h), daemon=True).start()
|
|
print(f"linktest server: tcp 7791 down, 7792 up, 7794 results; udp 7793 echo; -> {RESULTS}", flush=True)
|
|
serve_echo(7793)
|