Files
app-flutter/tool/linktest_server.py
T
mikkeli 6b5fef41c4
CI / Analyze & Test (push) Successful in 10s
CI / Debug APK (push) Successful in 25s
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
2026-10-11 15:55:52 +09:00

124 lines
3.7 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 "<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)