#!/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))