mirror of
https://github.com/alexhopeoconnor/WiFiManager.git
synced 2026-10-04 02:48:13 +10:00
130 lines
4.2 KiB
Python
130 lines
4.2 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
|
|
IMMEDIATE_EMPTY_READ_SECONDS = 0.01
|
|
EMPTY_READ_BACKOFF_INITIAL_SECONDS = 0.01
|
|
EMPTY_READ_BACKOFF_MAX_SECONDS = 0.25
|
|
|
|
|
|
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,
|
|
deadline_seconds=None,
|
|
clock=time.monotonic,
|
|
sleep=time.sleep,
|
|
):
|
|
"""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)
|
|
deadline = None
|
|
if deadline_seconds is not None:
|
|
deadline = clock() + deadline_seconds
|
|
empty_read_backoff = EMPTY_READ_BACKOFF_INITIAL_SECONDS
|
|
while not should_stop():
|
|
if deadline is not None and clock() >= deadline:
|
|
print("Passive serial capture reached its deadline.", file=sys.stderr)
|
|
return 2
|
|
read_started = clock()
|
|
data = serial_port.read(4096)
|
|
if data:
|
|
empty_read_backoff = EMPTY_READ_BACKOFF_INITIAL_SECONDS
|
|
output_file.write(data)
|
|
continue
|
|
|
|
# A real serial port blocks for its configured timeout. A
|
|
# broken or detached backend can return an empty read
|
|
# immediately. Back off only in that pathological case, and
|
|
# reset after real data, so the recorder cannot become a
|
|
# CPU-bound loop without penalising normal serial timeouts.
|
|
if clock() - read_started < IMMEDIATE_EMPTY_READ_SECONDS:
|
|
sleep(empty_read_backoff)
|
|
empty_read_backoff = min(
|
|
empty_read_backoff * 2,
|
|
EMPTY_READ_BACKOFF_MAX_SECONDS,
|
|
)
|
|
else:
|
|
empty_read_backoff = EMPTY_READ_BACKOFF_INITIAL_SECONDS
|
|
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)
|
|
parser.add_argument("--deadline-seconds", type=float)
|
|
args = parser.parse_args()
|
|
|
|
if args.deadline_seconds is not None and args.deadline_seconds <= 0:
|
|
parser.error("--deadline-seconds must be greater than zero")
|
|
|
|
signal.signal(signal.SIGTERM, request_stop)
|
|
signal.signal(signal.SIGINT, request_stop)
|
|
return capture(args.port, args.output, args.ready_file, deadline_seconds=args.deadline_seconds)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|