fix tui
This commit is contained in:
@@ -75,13 +75,14 @@ static const float SPREAD_EMA_ALPHA = 0.45f;
|
|||||||
|
|
||||||
// PID auto-tune (relay method) — run with: autotune 45
|
// PID auto-tune (relay method) — run with: autotune 45
|
||||||
static const float AUTOTUNE_HYSTERESIS_C = 0.4f;
|
static const float AUTOTUNE_HYSTERESIS_C = 0.4f;
|
||||||
static const float AUTOTUNE_PREHEAT_BAND_C = 5.0f;
|
static const float AUTOTUNE_PREHEAT_BAND_C = 3.0f;
|
||||||
static const float AUTOTUNE_PREHEAT_DUTY = 100.0f;
|
static const float AUTOTUNE_PREHEAT_DUTY = 100.0f;
|
||||||
static const uint8_t AUTOTUNE_PREHEAT_FAN_PWM = 70; // low fan for entire autotune
|
static const uint8_t AUTOTUNE_PREHEAT_FAN_PWM = 0; // fan off — maximize heat-up
|
||||||
static const uint8_t AUTOTUNE_CYCLES_REQUIRED = 6;
|
static const uint8_t AUTOTUNE_CYCLES_REQUIRED = 5;
|
||||||
static const uint32_t AUTOTUNE_PREHEAT_TIMEOUT_MS = 1200000UL; // 20 min
|
static const uint32_t AUTOTUNE_PREHEAT_TIMEOUT_MS = 1200000UL; // 20 min
|
||||||
|
static const uint32_t AUTOTUNE_RELAY_STALL_MS = 1500000UL; // 25 min in relay, 0 cycles
|
||||||
static const uint32_t AUTOTUNE_SESSION_TIMEOUT_MS = 3600000UL; // 60 min total
|
static const uint32_t AUTOTUNE_SESSION_TIMEOUT_MS = 3600000UL; // 60 min total
|
||||||
static const uint32_t AUTOTUNE_RELAY_PERIOD_MAX_MS = 2400000UL; // count periods up to 40 min
|
static const uint32_t AUTOTUNE_RELAY_PERIOD_MAX_MS = 2400000UL;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Timing
|
// Timing
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public:
|
|||||||
spreadSamples_(0),
|
spreadSamples_(0),
|
||||||
cycleCount_(0),
|
cycleCount_(0),
|
||||||
aboveSetpoint_(false),
|
aboveSetpoint_(false),
|
||||||
|
useMaxSensorPv_(false),
|
||||||
sessionStartMs_(0),
|
sessionStartMs_(0),
|
||||||
phaseStartMs_(0),
|
phaseStartMs_(0),
|
||||||
resultKp_(PID_KP),
|
resultKp_(PID_KP),
|
||||||
@@ -47,12 +48,14 @@ public:
|
|||||||
|
|
||||||
float preheatTargetC() const { return setpointC_ - AUTOTUNE_PREHEAT_BAND_C; }
|
float preheatTargetC() const { return setpointC_ - AUTOTUNE_PREHEAT_BAND_C; }
|
||||||
|
|
||||||
|
bool usesMaxSensor() const { return useMaxSensorPv_; }
|
||||||
|
|
||||||
const char *phaseName() const {
|
const char *phaseName() const {
|
||||||
switch (phase_) {
|
switch (phase_) {
|
||||||
case Phase::Preheat:
|
case Phase::Preheat:
|
||||||
return "preheat";
|
return "preheat";
|
||||||
case Phase::Relay:
|
case Phase::Relay:
|
||||||
return "relay";
|
return useMaxSensorPv_ ? "relay-max" : "relay-avg";
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -124,8 +127,8 @@ public:
|
|||||||
fail(F("autotune: abort — preheat timeout"));
|
fail(F("autotune: abort — preheat timeout"));
|
||||||
return phase_;
|
return phase_;
|
||||||
}
|
}
|
||||||
if (avgTempC >= preheatTargetC()) {
|
if (avgTempC >= preheatTargetC() || maxTempC >= setpointC_ - 2.0f) {
|
||||||
enterRelay(avgTempC, nowMs);
|
enterRelay(avgTempC, maxTempC, spreadC, nowMs);
|
||||||
} else {
|
} else {
|
||||||
heaterDutyOut = AUTOTUNE_PREHEAT_DUTY;
|
heaterDutyOut = AUTOTUNE_PREHEAT_DUTY;
|
||||||
}
|
}
|
||||||
@@ -137,27 +140,44 @@ public:
|
|||||||
return phase_;
|
return phase_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nowMs - sessionStartMs_ > AUTOTUNE_SESSION_TIMEOUT_MS) {
|
||||||
|
fail(F("autotune: abort — session timeout"));
|
||||||
|
return phase_;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cycleCount_ == 0 && nowMs - phaseStartMs_ > AUTOTUNE_RELAY_STALL_MS) {
|
||||||
|
Serial.print(F("autotune: relay stalled — avg "));
|
||||||
|
Serial.print(avgTempC, 1);
|
||||||
|
Serial.print(F("C max "));
|
||||||
|
Serial.print(maxTempC, 1);
|
||||||
|
Serial.println(F("C (spread too large for avg to cross setpoint?)"));
|
||||||
|
fail(F("autotune: abort — no oscillation"));
|
||||||
|
return phase_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float pv = useMaxSensorPv_ ? maxTempC : avgTempC;
|
||||||
|
|
||||||
spreadSum_ += spreadC;
|
spreadSum_ += spreadC;
|
||||||
++spreadSamples_;
|
++spreadSamples_;
|
||||||
|
|
||||||
if (avgTempC > peakSinceCross_) {
|
if (pv > peakSinceCross_) {
|
||||||
peakSinceCross_ = avgTempC;
|
peakSinceCross_ = pv;
|
||||||
}
|
}
|
||||||
if (avgTempC < valleySinceCross_) {
|
if (pv < valleySinceCross_) {
|
||||||
valleySinceCross_ = avgTempC;
|
valleySinceCross_ = pv;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool heatOn = false;
|
bool heatOn = false;
|
||||||
if (avgTempC <= relayLow_) {
|
if (pv <= relayLow_) {
|
||||||
heatOn = true;
|
heatOn = true;
|
||||||
} else if (avgTempC >= relayHigh_) {
|
} else if (pv >= relayHigh_) {
|
||||||
heatOn = false;
|
heatOn = false;
|
||||||
} else {
|
} else {
|
||||||
heatOn = !aboveSetpoint_;
|
heatOn = !aboveSetpoint_;
|
||||||
}
|
}
|
||||||
heaterDutyOut = heatOn ? 100.0f : 0.0f;
|
heaterDutyOut = heatOn ? 100.0f : 0.0f;
|
||||||
|
|
||||||
const bool nowAbove = avgTempC >= setpointC_;
|
const bool nowAbove = pv >= setpointC_;
|
||||||
if (nowAbove != aboveSetpoint_) {
|
if (nowAbove != aboveSetpoint_) {
|
||||||
onSetpointCrossing(nowMs);
|
onSetpointCrossing(nowMs);
|
||||||
aboveSetpoint_ = nowAbove;
|
aboveSetpoint_ = nowAbove;
|
||||||
@@ -167,14 +187,18 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void enterRelay(float avgTempC, uint32_t nowMs) {
|
void enterRelay(float avgTempC, float maxTempC, float spreadC, uint32_t nowMs) {
|
||||||
phase_ = Phase::Relay;
|
phase_ = Phase::Relay;
|
||||||
phaseStartMs_ = nowMs;
|
phaseStartMs_ = nowMs;
|
||||||
aboveSetpoint_ = avgTempC >= setpointC_;
|
useMaxSensorPv_ = spreadC > GOOD_SPREAD_C;
|
||||||
peakSinceCross_ = avgTempC;
|
const float pv = useMaxSensorPv_ ? maxTempC : avgTempC;
|
||||||
valleySinceCross_ = avgTempC;
|
aboveSetpoint_ = pv >= setpointC_;
|
||||||
|
peakSinceCross_ = pv;
|
||||||
|
valleySinceCross_ = pv;
|
||||||
lastCrossMs_ = 0;
|
lastCrossMs_ = 0;
|
||||||
Serial.print(F("autotune: relay test started ("));
|
Serial.print(F("autotune: relay "));
|
||||||
|
Serial.print(useMaxSensorPv_ ? F("max-sensor") : F("avg"));
|
||||||
|
Serial.print(F(" ("));
|
||||||
Serial.print((nowMs - sessionStartMs_) / 1000UL);
|
Serial.print((nowMs - sessionStartMs_) / 1000UL);
|
||||||
Serial.println(F("s preheat)"));
|
Serial.println(F("s preheat)"));
|
||||||
}
|
}
|
||||||
@@ -191,6 +215,7 @@ private:
|
|||||||
spreadSamples_ = 0;
|
spreadSamples_ = 0;
|
||||||
cycleCount_ = 0;
|
cycleCount_ = 0;
|
||||||
aboveSetpoint_ = false;
|
aboveSetpoint_ = false;
|
||||||
|
useMaxSensorPv_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void onSetpointCrossing(uint32_t nowMs) {
|
void onSetpointCrossing(uint32_t nowMs) {
|
||||||
@@ -295,6 +320,7 @@ private:
|
|||||||
uint16_t spreadSamples_;
|
uint16_t spreadSamples_;
|
||||||
uint8_t cycleCount_;
|
uint8_t cycleCount_;
|
||||||
bool aboveSetpoint_;
|
bool aboveSetpoint_;
|
||||||
|
bool useMaxSensorPv_;
|
||||||
uint32_t sessionStartMs_;
|
uint32_t sessionStartMs_;
|
||||||
uint32_t phaseStartMs_;
|
uint32_t phaseStartMs_;
|
||||||
float resultKp_;
|
float resultKp_;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@@ -19,6 +20,118 @@ FALLBACK_HEADER = (
|
|||||||
"fan_pct,cutoff,failsafe,ch2_t,ch2_h,ch3_t,ch3_h,ch4_t,ch4_h,ch5_t,ch5_h"
|
"fan_pct,cutoff,failsafe,ch2_t,ch2_h,ch3_t,ch3_h,ch4_t,ch4_h,ch5_t,ch5_h"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SENSOR_CHANNELS = [2, 3, 4, 5]
|
||||||
|
|
||||||
|
_FAN_PCT_RE = re.compile(r"\((\d+)%\)")
|
||||||
|
_FAN_PWM_RE = re.compile(r"^(\d+)/")
|
||||||
|
|
||||||
|
|
||||||
|
def fan_pct_from_status(fan: str) -> int:
|
||||||
|
match = _FAN_PCT_RE.search(fan)
|
||||||
|
if match:
|
||||||
|
return int(match.group(1))
|
||||||
|
match = _FAN_PWM_RE.match(fan)
|
||||||
|
if match:
|
||||||
|
return (int(match.group(1)) * 100) // 255
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def target_c_from_status(target: str) -> str:
|
||||||
|
if target.startswith("idle"):
|
||||||
|
return "0.0"
|
||||||
|
if target.endswith("C"):
|
||||||
|
return target[:-1]
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
|
def build_csv_payload_from_status(data: dict, ms: int | None = None) -> str:
|
||||||
|
if ms is None:
|
||||||
|
ms = int(time.time() * 1000)
|
||||||
|
|
||||||
|
sensors = {ch: (temp, hum) for ch, temp, hum in data.get("sensor_list", [])}
|
||||||
|
parts = [
|
||||||
|
str(ms),
|
||||||
|
target_c_from_status(data["target"]),
|
||||||
|
data["avg"],
|
||||||
|
data["min"],
|
||||||
|
data["max"],
|
||||||
|
data["spread"],
|
||||||
|
data["heatlim"],
|
||||||
|
data["heater"],
|
||||||
|
str(fan_pct_from_status(data.get("fan", "0"))),
|
||||||
|
"1" if data.get("cutoff_active") == "YES" else "0",
|
||||||
|
"1" if data.get("failsafe") == "YES" else "0",
|
||||||
|
]
|
||||||
|
for ch in SENSOR_CHANNELS:
|
||||||
|
if str(ch) in sensors:
|
||||||
|
temp, hum = sensors[str(ch)]
|
||||||
|
if temp == "ERR":
|
||||||
|
parts.extend(["", ""])
|
||||||
|
else:
|
||||||
|
parts.extend([temp, hum])
|
||||||
|
else:
|
||||||
|
parts.extend(["", ""])
|
||||||
|
return ",".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
class CsvSession:
|
||||||
|
"""Deferred CSV writer — no empty file until the first row lands."""
|
||||||
|
|
||||||
|
def __init__(self, path: Path):
|
||||||
|
self.path = path
|
||||||
|
self._fh = None
|
||||||
|
self._header_written = False
|
||||||
|
self.row_count = 0
|
||||||
|
|
||||||
|
def _ensure_open(self) -> None:
|
||||||
|
if self._fh is None:
|
||||||
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._fh = self.path.open("w", encoding="utf-8")
|
||||||
|
|
||||||
|
def _write_header(self) -> None:
|
||||||
|
if not self._header_written:
|
||||||
|
self._ensure_open()
|
||||||
|
assert self._fh is not None
|
||||||
|
self._fh.write(FALLBACK_HEADER + "\n")
|
||||||
|
self._header_written = True
|
||||||
|
|
||||||
|
def write_device_line(self, line: str) -> None:
|
||||||
|
if line.startswith("csv_hdr,"):
|
||||||
|
self._ensure_open()
|
||||||
|
assert self._fh is not None
|
||||||
|
device_header = line[len("csv_hdr,") :]
|
||||||
|
self._fh.write("wall_time," + device_header + "\n")
|
||||||
|
self._header_written = True
|
||||||
|
self._fh.flush()
|
||||||
|
return
|
||||||
|
if not line.startswith("csv,"):
|
||||||
|
return
|
||||||
|
self._write_header()
|
||||||
|
assert self._fh is not None
|
||||||
|
wall_time = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||||
|
self._fh.write(wall_time + "," + line[len("csv,") :] + "\n")
|
||||||
|
self._fh.flush()
|
||||||
|
self.row_count += 1
|
||||||
|
|
||||||
|
def write_status(self, data: dict) -> None:
|
||||||
|
self._write_header()
|
||||||
|
assert self._fh is not None
|
||||||
|
wall_time = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||||
|
payload = build_csv_payload_from_status(data)
|
||||||
|
self._fh.write(wall_time + "," + payload + "\n")
|
||||||
|
self._fh.flush()
|
||||||
|
self.row_count += 1
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
if self._fh is not None:
|
||||||
|
self._fh.close()
|
||||||
|
self._fh = None
|
||||||
|
if self.row_count == 0 and self.path.exists():
|
||||||
|
try:
|
||||||
|
self.path.unlink()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def detect_serial_port() -> str | None:
|
def detect_serial_port() -> str | None:
|
||||||
by_id = Path("/dev/serial/by-id")
|
by_id = Path("/dev/serial/by-id")
|
||||||
@@ -93,36 +206,20 @@ def enable_dryer_logging(ser, retries: int = 3) -> None:
|
|||||||
print("WARN: did not see 'OK csv logging on' — continuing anyway", file=sys.stderr)
|
print("WARN: did not see 'OK csv logging on' — continuing anyway", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
def write_csv_row(fh, line: str, header_written: list[bool]) -> None:
|
|
||||||
if line.startswith("csv_hdr,"):
|
|
||||||
device_header = line[len("csv_hdr,") :]
|
|
||||||
fh.write("wall_time," + device_header + "\n")
|
|
||||||
header_written[0] = True
|
|
||||||
fh.flush()
|
|
||||||
return
|
|
||||||
if not line.startswith("csv,"):
|
|
||||||
return
|
|
||||||
if not header_written[0]:
|
|
||||||
fh.write(FALLBACK_HEADER + "\n")
|
|
||||||
header_written[0] = True
|
|
||||||
wall_time = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
||||||
fh.write(wall_time + "," + line[len("csv,") :] + "\n")
|
|
||||||
fh.flush()
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_log(args: argparse.Namespace) -> int:
|
def cmd_log(args: argparse.Namespace) -> int:
|
||||||
|
from dryer_tui import parse_status
|
||||||
|
|
||||||
port = resolve_port(args.port)
|
port = resolve_port(args.port)
|
||||||
out = args.output
|
out = args.output
|
||||||
if out is None:
|
if out is None:
|
||||||
out = args.log_dir / f"dryer_{datetime.now():%Y%m%d_%H%M%S}.csv"
|
out = args.log_dir / f"dryer_{datetime.now():%Y%m%d_%H%M%S}.csv"
|
||||||
out.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
print(f"Logging {port} -> {out}", file=sys.stderr)
|
print(f"Logging {port} -> {out}", file=sys.stderr)
|
||||||
if args.auto_log_on:
|
if args.auto_log_on:
|
||||||
print("Will send 'log on' after connect", file=sys.stderr)
|
print("Will send 'log on' after connect", file=sys.stderr)
|
||||||
|
|
||||||
header_written = False
|
session = CsvSession(out)
|
||||||
with open_serial(port, args.baud) as ser, out.open("w", encoding="utf-8") as fh:
|
with open_serial(port, args.baud) as ser:
|
||||||
if args.auto_log_on:
|
if args.auto_log_on:
|
||||||
enable_dryer_logging(ser)
|
enable_dryer_logging(ser)
|
||||||
|
|
||||||
@@ -130,32 +227,27 @@ def cmd_log(args: argparse.Namespace) -> int:
|
|||||||
try:
|
try:
|
||||||
raw = ser.readline()
|
raw = ser.readline()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nStopped.", file=sys.stderr)
|
print(f"\nStopped ({session.row_count} rows).", file=sys.stderr)
|
||||||
|
session.close()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if not raw:
|
if not raw:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
line = decode_line(raw)
|
line = decode_line(raw)
|
||||||
if not line.startswith("csv_hdr,") and not line.startswith("csv,"):
|
parsed = parse_status(line)
|
||||||
if line:
|
if parsed:
|
||||||
|
session.write_status(parsed)
|
||||||
print(line)
|
print(line)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if line.startswith("csv_hdr,"):
|
if line.startswith("csv_hdr,") or line.startswith("csv,"):
|
||||||
device_header = line[len("csv_hdr,") :]
|
session.write_device_line(line)
|
||||||
fh.write("wall_time," + device_header + "\n")
|
|
||||||
header_written = True
|
|
||||||
fh.flush()
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not header_written:
|
if line:
|
||||||
fh.write(FALLBACK_HEADER + "\n")
|
print(line)
|
||||||
header_written = True
|
return 0
|
||||||
|
|
||||||
wall_time = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
||||||
fh.write(wall_time + "," + line[len("csv,") :] + "\n")
|
|
||||||
fh.flush()
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_tui(args: argparse.Namespace) -> int:
|
def cmd_tui(args: argparse.Namespace) -> int:
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ from datetime import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from capture_csv import (
|
from capture_csv import (
|
||||||
|
CsvSession,
|
||||||
decode_line,
|
decode_line,
|
||||||
write_csv_row,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
PRESETS: list[tuple[str, float]] = [
|
PRESETS: list[tuple[str, float]] = [
|
||||||
@@ -52,7 +52,7 @@ STATUS_RE = re.compile(
|
|||||||
SENSOR_RE = re.compile(r"ch(\d+):([\d.]+)C/(\d+)%|ch(\d+):ERR")
|
SENSOR_RE = re.compile(r"ch(\d+):([\d.]+)C/(\d+)%|ch(\d+):ERR")
|
||||||
|
|
||||||
AUTOTUNE_MODE_RE = re.compile(
|
AUTOTUNE_MODE_RE = re.compile(
|
||||||
r"autotune/(?P<phase>\w+) (?P<elapsed>\d+)s (?P<cycles>\d+/\d+)cyc pre>=(?P<pre>\d+)C"
|
r"autotune/(?P<phase>[\w-]+) (?P<elapsed>\d+)s (?P<cycles>\d+/\d+)cyc pre>=(?P<pre>\d+)C"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -155,8 +155,7 @@ class SerialWorker:
|
|||||||
self.state = state
|
self.state = state
|
||||||
self.lock = lock
|
self.lock = lock
|
||||||
self.stop = threading.Event()
|
self.stop = threading.Event()
|
||||||
self._log_fh = None
|
self._csv: CsvSession | None = None
|
||||||
self._header_written = [False]
|
|
||||||
self._thread: threading.Thread | None = None
|
self._thread: threading.Thread | None = None
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
@@ -171,9 +170,9 @@ class SerialWorker:
|
|||||||
self.stop.set()
|
self.stop.set()
|
||||||
if self._thread is not None:
|
if self._thread is not None:
|
||||||
self._thread.join(timeout=1.5)
|
self._thread.join(timeout=1.5)
|
||||||
if self._log_fh is not None:
|
if self._csv is not None:
|
||||||
self._log_fh.close()
|
self._csv.close()
|
||||||
self._log_fh = None
|
self._csv = None
|
||||||
|
|
||||||
def send(self, command: str) -> None:
|
def send(self, command: str) -> None:
|
||||||
if self.ser is None:
|
if self.ser is None:
|
||||||
@@ -186,19 +185,21 @@ class SerialWorker:
|
|||||||
if enabled and not self.state.csv_logging:
|
if enabled and not self.state.csv_logging:
|
||||||
log_dir.mkdir(parents=True, exist_ok=True)
|
log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
path = log_dir / f"dryer_{datetime.now():%Y%m%d_%H%M%S}.csv"
|
path = log_dir / f"dryer_{datetime.now():%Y%m%d_%H%M%S}.csv"
|
||||||
self._log_fh = path.open("w", encoding="utf-8")
|
self._csv = CsvSession(path)
|
||||||
self._header_written = [False]
|
|
||||||
self.state.csv_path = path
|
self.state.csv_path = path
|
||||||
self.state.csv_logging = True
|
self.state.csv_logging = True
|
||||||
self.state.messages.append(f"CSV -> {path.name}")
|
self.state.messages.append(f"CSV -> {path.name} (on status)")
|
||||||
self.send("log on")
|
self.send("log on")
|
||||||
elif not enabled and self.state.csv_logging:
|
elif not enabled and self.state.csv_logging:
|
||||||
self.send("log off")
|
self.send("log off")
|
||||||
self.state.csv_logging = False
|
self.state.csv_logging = False
|
||||||
self.state.csv_path = None
|
self.state.csv_path = None
|
||||||
if self._log_fh is not None:
|
if self._csv is not None:
|
||||||
self._log_fh.close()
|
rows = self._csv.row_count
|
||||||
self._log_fh = None
|
self._csv.close()
|
||||||
|
self._csv = None
|
||||||
|
self.state.messages.append(f"CSV logging off ({rows} rows)")
|
||||||
|
else:
|
||||||
self.state.messages.append("CSV logging off")
|
self.state.messages.append("CSV logging off")
|
||||||
|
|
||||||
def _note(self, line: str) -> None:
|
def _note(self, line: str) -> None:
|
||||||
@@ -221,14 +222,14 @@ class SerialWorker:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if line.startswith("csv,") or line.startswith("csv_hdr,"):
|
if line.startswith("csv,") or line.startswith("csv_hdr,"):
|
||||||
if self._log_fh is not None:
|
|
||||||
write_csv_row(self._log_fh, line, self._header_written)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
parsed = parse_status(line)
|
parsed = parse_status(line)
|
||||||
if parsed:
|
if parsed:
|
||||||
with self.lock:
|
with self.lock:
|
||||||
apply_status(self.state, parsed)
|
apply_status(self.state, parsed)
|
||||||
|
if self._csv is not None:
|
||||||
|
self._csv.write_status(parsed)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if line.startswith("target="):
|
if line.startswith("target="):
|
||||||
|
|||||||
Reference in New Issue
Block a user