joeylib2/scripts/lib/serialStreamPeer.py
2026-10-13 14:01:46 -05:00

126 lines
4.2 KiB
Python
Executable file

#!/usr/bin/env python3
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
"""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))