This commit is contained in:
2026-08-07 17:36:15 +02:00
parent d1385baa45
commit 31a6764484
9 changed files with 75219 additions and 68 deletions

View File

@@ -22,6 +22,10 @@ FALLBACK_HEADER = (
SENSOR_CHANNELS = [2, 3, 4, 5]
# If no bytes at all arrive for this long, treat the device as hung (e.g. an
# I2C bus lockup freezing the Arduino) rather than looping forever in silence.
DEFAULT_STALL_TIMEOUT_S = 20.0
_FAN_PCT_RE = re.compile(r"\((\d+)%\)")
_FAN_PWM_RE = re.compile(r"^(\d+)/")
@@ -206,6 +210,11 @@ def enable_dryer_logging(ser, retries: int = 3) -> None:
print("WARN: did not see 'OK csv logging on' — continuing anyway", file=sys.stderr)
def log_notice(message: str) -> None:
stamp = datetime.now(timezone.utc).isoformat(timespec="seconds")
print(f"{stamp} {message}", file=sys.stderr)
def cmd_log(args: argparse.Namespace) -> int:
from dryer_tui import parse_status
@@ -214,6 +223,7 @@ def cmd_log(args: argparse.Namespace) -> int:
if out is None:
out = args.log_dir / f"dryer_{datetime.now():%Y%m%d_%H%M%S}.csv"
stall_timeout = args.stall_timeout
print(f"Logging {port} -> {out}", file=sys.stderr)
if args.auto_log_on:
print("Will send 'log on' after connect", file=sys.stderr)
@@ -223,6 +233,7 @@ def cmd_log(args: argparse.Namespace) -> int:
if args.auto_log_on:
enable_dryer_logging(ser)
last_activity = time.monotonic()
while True:
try:
raw = ser.readline()
@@ -230,10 +241,26 @@ def cmd_log(args: argparse.Namespace) -> int:
print(f"\nStopped ({session.row_count} rows).", file=sys.stderr)
session.close()
return 0
except Exception as exc:
log_notice(
f"ERROR: serial read failed ({exc}) — closing after "
f"{session.row_count} rows"
)
session.close()
return 1
if not raw:
if time.monotonic() - last_activity >= stall_timeout:
log_notice(
f"WARN: no data from {port} for {stall_timeout:.0f}s — "
f"device likely hung (e.g. I2C bus lockup on the Arduino) "
f"— closing after {session.row_count} rows"
)
session.close()
return 1
continue
last_activity = time.monotonic()
line = decode_line(raw)
parsed = parse_status(line)
if parsed:
@@ -277,6 +304,15 @@ def build_parser() -> argparse.ArgumentParser:
default=True,
help="Send 'log on' after connect in log mode (default: on)",
)
parser.add_argument(
"--stall-timeout",
type=float,
default=DEFAULT_STALL_TIMEOUT_S,
help=(
"log mode: seconds without any data before treating the device as "
f"hung and exiting (default: {DEFAULT_STALL_TIMEOUT_S:.0f})"
),
)
subparsers = parser.add_subparsers(dest="action")
log_p = subparsers.add_parser("log", help="Headless CSV capture", add_help=False)
@@ -285,6 +321,7 @@ def build_parser() -> argparse.ArgumentParser:
log_p.add_argument("-o", "--output", type=Path)
log_p.add_argument("--log-dir", type=Path, default=Path("logs"))
log_p.add_argument("--auto-log-on", action=argparse.BooleanOptionalAction, default=True)
log_p.add_argument("--stall-timeout", type=float, default=DEFAULT_STALL_TIMEOUT_S)
tui_p = subparsers.add_parser("tui", help="Interactive curses dashboard")
tui_p.add_argument("-p", "--port")