Files
WiFiManager/tools/capture-serial.py
T

96 lines
2.8 KiB
Python

#!/usr/bin/env python3
"""Passively retain serial evidence for a physical portal OTA upload.
The recorder deliberately opens the ESP USB-UART with both modem-control lines
inactive. It never resets the target: the calling runner has already flashed
and booted fixture A before this process attaches.
"""
import argparse
import os
import signal
import sys
import time
import serial
_stop_requested = False
def request_stop(_signum, _frame):
"""Let the read loop finish promptly after a normal runner cleanup."""
global _stop_requested
_stop_requested = True
def open_capture_port(port):
"""Open a UART without PySerial's default reset-causing line assertion."""
serial_port = serial.Serial(
port=None,
baudrate=115200,
timeout=0.25,
rtscts=False,
dsrdtr=False,
)
serial_port.dtr = False
serial_port.rts = False
serial_port.port = port
serial_port.open()
return serial_port
def write_ready(path):
"""Publish readiness only after the passive serial port is open."""
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
try:
os.fchmod(descriptor, 0o600)
os.write(descriptor, b"ready\n")
finally:
os.close(descriptor)
def capture(port, output, ready_file, should_stop=None):
"""Append serial bytes until terminated by the owning hardware runner."""
global _stop_requested
_stop_requested = False
if should_stop is None:
should_stop = lambda: _stop_requested
try:
with (
open_capture_port(port) as serial_port,
open(output, "ab", buffering=0) as output_file,
):
os.fchmod(output_file.fileno(), 0o600)
write_ready(ready_file)
while not should_stop():
data = serial_port.read(4096)
if data:
output_file.write(data)
else:
# PySerial normally blocks for its configured timeout. A
# broken backend or test double can return immediately;
# never let that turn a detached recorder into a CPU loop.
time.sleep(0.01)
except (OSError, serial.SerialException) as error:
print(f"Passive serial capture failed: {error}", file=sys.stderr)
return 1
return 0
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--port", required=True)
parser.add_argument("--output", required=True)
parser.add_argument("--ready-file", required=True)
args = parser.parse_args()
signal.signal(signal.SIGTERM, request_stop)
signal.signal(signal.SIGINT, request_stop)
return capture(args.port, args.output, args.ready_file)
if __name__ == "__main__":
sys.exit(main())