122 lines
4 KiB
Python
Executable file
122 lines
4 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""serialStreamPeer.py - host end of a both-directions serial stream gate.
|
|
|
|
The guest (examples/sermouse) transmits an endless stream and checks an
|
|
endless stream from us, both byte-exact:
|
|
|
|
byte k = (k ^ (k >> 8) ^ mask) & 0xFF
|
|
|
|
with mask GUEST_MASK for the guest's stream and PEER_MASK for ours (the same
|
|
formula and masks as streamByte() in examples/sermouse/sermouse.c). A lost or
|
|
repeated byte throws every byte after it off its expected value.
|
|
|
|
This peer listens (the emulator's null_modem connects out), checks the guest's
|
|
stream in order, and answers it: our stream is paced byte for byte on the
|
|
guest's, never more than WINDOW bytes ahead of what has come back. So when the
|
|
guest pauses its stream the line goes quiet both ways, and once the guest has
|
|
been silent for QUIET_SECONDS the counts are recorded as a quiet snapshot
|
|
"sent:received" -- at that moment every byte either side sent has arrived, so a
|
|
harness that reads the guest's own counts while it is paused can require them
|
|
to equal one of these exactly.
|
|
|
|
serialStreamPeer.py PORT WALL MIN_BYTES
|
|
|
|
Runs until the emulator closes the link or WALL seconds pass, then prints one
|
|
line, STREAM PASS or STREAM FAIL, with the counts and the quiet snapshots. PASS
|
|
means the guest's stream arrived with no bad byte and at least MIN_BYTES long.
|
|
Exit 0 on pass, 1 on fail.
|
|
"""
|
|
|
|
import select
|
|
import sys
|
|
import time
|
|
|
|
from serialLoopPeer import acceptOne
|
|
|
|
GUEST_MASK = 0xA5
|
|
PEER_MASK = 0x00
|
|
WINDOW = 256 # our bytes allowed ahead of the guest's
|
|
QUIET_SECONDS = 0.25 # guest silence that counts as a pause
|
|
POLL_SECONDS = 0.05
|
|
READ_SIZE = 4096
|
|
BYTE_MASK = 0xFF
|
|
BYTE_BITS = 8
|
|
NO_BAD_INDEX = -1
|
|
|
|
|
|
def streamByte(index, mask):
|
|
return (index ^ (index >> BYTE_BITS) ^ mask) & BYTE_MASK
|
|
|
|
|
|
def run(conn, wall, start):
|
|
received = 0
|
|
sent = 0
|
|
bad = 0
|
|
firstBad = NO_BAD_INDEX
|
|
lastRx = 0.0
|
|
quietArmed = False
|
|
snapshots = []
|
|
closed = False
|
|
|
|
conn.setblocking(False)
|
|
while time.time() - start < wall:
|
|
# Our stream starts once the guest's has: it is open and listening.
|
|
want = received + WINDOW - sent if received > 0 else 0
|
|
writers = [conn] if want > 0 else []
|
|
readable, writable, _ = select.select([conn], writers, [], POLL_SECONDS)
|
|
now = time.time()
|
|
if readable:
|
|
try:
|
|
data = conn.recv(READ_SIZE)
|
|
except OSError:
|
|
data = b""
|
|
if not data:
|
|
closed = True
|
|
break
|
|
for value in data:
|
|
if value != streamByte(received, GUEST_MASK):
|
|
if firstBad == NO_BAD_INDEX:
|
|
firstBad = received
|
|
bad += 1
|
|
received += 1
|
|
lastRx = now
|
|
quietArmed = True
|
|
if writable and want > 0:
|
|
chunk = bytes(streamByte(sent + i, PEER_MASK) for i in range(want))
|
|
try:
|
|
sent += conn.send(chunk)
|
|
except OSError:
|
|
pass
|
|
if quietArmed and now - lastRx >= QUIET_SECONDS:
|
|
snapshots.append("%d:%d" % (sent, received))
|
|
quietArmed = False
|
|
return received, sent, bad, firstBad, snapshots, closed
|
|
|
|
|
|
def main(argv):
|
|
if len(argv) != 4:
|
|
sys.stderr.write(__doc__)
|
|
return 2
|
|
port = int(argv[1])
|
|
wall = float(argv[2])
|
|
minBytes = int(argv[3])
|
|
start = time.time()
|
|
|
|
print("STREAM listening on 127.0.0.1:%d" % port, flush=True)
|
|
conn = acceptOne(port, wall)
|
|
if conn is None:
|
|
print("STREAM FAIL: the emulator never connected")
|
|
return 1
|
|
received, sent, bad, firstBad, snapshots, closed = run(conn, wall, start)
|
|
conn.close()
|
|
|
|
ok = bad == 0 and received >= minBytes
|
|
print("STREAM %s received=%d sent=%d bad=%d firstBad=%d quiet=%s end=%s" %
|
|
("PASS" if ok else "FAIL", received, sent, bad, firstBad,
|
|
",".join(snapshots) if snapshots else "none",
|
|
"closed" if closed else "wall"))
|
|
return 0 if ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|