safety commit
This commit is contained in:
147
scripts/fan_test.py
Normal file
147
scripts/fan_test.py
Normal file
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cycle fan speeds on the dryer for wiring / PWM verification.
|
||||
|
||||
Uses the firmware `fan test <pwm>` command (heater stays off). Sends `target 0`
|
||||
first so the thermal loop is idle.
|
||||
|
||||
Example:
|
||||
./fan_test.py
|
||||
./fan_test.py --pct 30 50 100 --interval 3 --loop
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
from capture_csv import decode_line, open_serial, resolve_port
|
||||
|
||||
OK_RE = re.compile(r"^OK ")
|
||||
ERR_RE = re.compile(r"^ERR ")
|
||||
FAN_STATUS_RE = re.compile(r"fan=(\d+)/255\((\d+)%\)")
|
||||
|
||||
|
||||
def pct_to_pwm(pct: int) -> int:
|
||||
if pct < 0 or pct > 100:
|
||||
raise ValueError(f"fan percent must be 0-100, got {pct}")
|
||||
return round(pct * 255 / 100)
|
||||
|
||||
|
||||
def send_command(ser, command: str, timeout: float = 2.0) -> list[str]:
|
||||
ser.write((command.strip() + "\n").encode("utf-8"))
|
||||
ser.flush()
|
||||
lines: list[str] = []
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
raw = ser.readline()
|
||||
if not raw:
|
||||
continue
|
||||
line = decode_line(raw)
|
||||
if not line:
|
||||
continue
|
||||
lines.append(line)
|
||||
if OK_RE.match(line) or ERR_RE.match(line):
|
||||
break
|
||||
return lines
|
||||
|
||||
|
||||
def drain_status(ser, duration: float) -> str | None:
|
||||
"""Read serial for `duration` seconds; return last status fan field if seen."""
|
||||
fan_field: str | None = None
|
||||
deadline = time.monotonic() + duration
|
||||
while time.monotonic() < deadline:
|
||||
raw = ser.readline()
|
||||
if not raw:
|
||||
continue
|
||||
line = decode_line(raw)
|
||||
if not line or line.startswith("csv"):
|
||||
continue
|
||||
match = FAN_STATUS_RE.search(line)
|
||||
if match:
|
||||
fan_field = f"{match.group(1)}/255 ({match.group(2)}%)"
|
||||
elif line.startswith("target="):
|
||||
print(f" status: {line}", flush=True)
|
||||
return fan_field
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Cycle fan PWM to verify fan control")
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--port",
|
||||
help="Serial port (default: auto-detect)",
|
||||
)
|
||||
parser.add_argument("-b", "--baud", type=int, default=115200)
|
||||
parser.add_argument(
|
||||
"--pct",
|
||||
type=int,
|
||||
nargs="+",
|
||||
default=[0, 30, 100, 200, 255],
|
||||
metavar="PCT",
|
||||
help="Fan speeds in percent (default: 0 30 100 200 255)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--interval",
|
||||
type=float,
|
||||
default=5.0,
|
||||
metavar="SEC",
|
||||
help="Seconds to hold each step (default: 5)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--loop",
|
||||
action="store_true",
|
||||
help="Repeat the sequence until Ctrl+C",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
port = resolve_port(args.port)
|
||||
sequence = [(pct, pct_to_pwm(pct)) for pct in args.pct]
|
||||
|
||||
print(f"Port: {port}", file=sys.stderr)
|
||||
print(
|
||||
f"Sequence: {' -> '.join(str(p) + '%' for p, _ in sequence)} "
|
||||
f"every {args.interval:g}s (heater off)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print("Ctrl+C to stop\n", file=sys.stderr)
|
||||
|
||||
interrupted = False
|
||||
with open_serial(port, args.baud) as ser:
|
||||
ser.reset_input_buffer()
|
||||
|
||||
lines = send_command(ser, "target 0")
|
||||
for line in lines:
|
||||
print(line, flush=True)
|
||||
if any(ERR_RE.match(line) for line in lines):
|
||||
return 1
|
||||
|
||||
try:
|
||||
while True:
|
||||
for pct, pwm in sequence:
|
||||
print(f">>> fan test {pwm} ({pct}%)", flush=True)
|
||||
lines = send_command(ser, f"fan test {pwm}")
|
||||
for line in lines:
|
||||
print(f" {line}", flush=True)
|
||||
if any(ERR_RE.match(line) for line in lines):
|
||||
return 1
|
||||
|
||||
reported = drain_status(ser, args.interval)
|
||||
if reported:
|
||||
print(f" reported fan={reported}", flush=True)
|
||||
|
||||
if not args.loop:
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
interrupted = True
|
||||
print("\nInterrupted", file=sys.stderr)
|
||||
finally:
|
||||
print(">>> fan test 0 (stop)", flush=True)
|
||||
send_command(ser, "fan test 0")
|
||||
|
||||
return 130 if interrupted else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user