Link test: UDP echo sweep, soak and TCP each way, driven by the app
The tablet's app sockets are the only ones bound to the USB link, so the measurement has to run in the app. tool/linktest_server.py serves the Orin end and logs every result with its own TCP_INFO. Startable without touching the screen: am start ... --es linktest <label>. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
#!/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 "<id> <secs>\\n", gets zeros for <secs>
|
||||
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, secs = readline(conn).split()
|
||||
end = time.monotonic() + float(secs)
|
||||
sent = 0
|
||||
try:
|
||||
while time.monotonic() < end:
|
||||
sent += conn.send(CHUNK)
|
||||
except OSError as e:
|
||||
log({"id": test_id, "kind": "tcp_down_orin", "error": str(e)})
|
||||
log({"id": test_id, "kind": "tcp_down_orin", "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)
|
||||
Reference in New Issue
Block a user