Skip to content

Commit fbf2fa5

Browse files
roman-dvorakclaude
andcommitted
Headless režim a verbose výstup pro vieweru, logování cloud dat z obou senzorů
- nový --headless / --no-gui běh bez Qt nad seriovou linkou - --verbose vypisuje hodnoty přijatých zpráv s časovou značkou - periodický souhrn (--stats-interval) v headless režimu - HDF5Logger rozšířen o /cloud{1,2} skupiny (TL/TR/BL/BR/center/Vdd/time) - cloud data se nyní loguji i z GUI módu Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 127d7f0 commit fbf2fa5

1 file changed

Lines changed: 258 additions & 2 deletions

File tree

sw/amsky01_viewer.py

Lines changed: 258 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
python amsky01_viewer.py --port /dev/ttyACM0 --baud 115200 --log --log-name AMSKY01
1616
python amsky01_viewer.py --replay AMSKY01_20260502_18.h5
1717
python amsky01_viewer.py --replay AMSKY01_20260502_18.h5 --replay-speed 5
18+
python amsky01_viewer.py --port /dev/ttyACM0 --headless --log --verbose
1819
"""
1920

2021
import argparse
@@ -458,6 +459,11 @@ def __init__(self, device_id: str, base_path: Path = None):
458459
self.light_gain = None
459460
self.light_expo = None
460461
self.light_time = None
462+
463+
# /cloud{1,2}/ — rohy + střed + Vdd
464+
for sid in (1, 2):
465+
for name in ("tl", "tr", "bl", "br", "center", "vdd", "time"):
466+
setattr(self, f"cloud{sid}_{name}", None)
461467

462468
def _get_current_hour(self):
463469
"""Vrací aktuální hodinu jako datetime objekt (UTC)."""
@@ -565,7 +571,26 @@ def _init_datasets(self):
565571
self.light_gain = light_grp["gain"]
566572
self.light_expo = light_grp["expo"]
567573
self.light_time = light_grp["time"]
568-
574+
575+
# Skupiny /cloud1 a /cloud2 — rohy, střed a Vdd
576+
for sid in (1, 2):
577+
gname = f"cloud{sid}"
578+
if gname not in f:
579+
grp = f.create_group(gname)
580+
for name in ("tl", "tr", "bl", "br", "center", "vdd"):
581+
setattr(self, f"cloud{sid}_{name}", grp.create_dataset(
582+
name, shape=(0,), maxshape=(None,),
583+
dtype=np.float32, chunks=(1024,)
584+
))
585+
setattr(self, f"cloud{sid}_time", grp.create_dataset(
586+
"time", shape=(0,), maxshape=(None,),
587+
dtype=np.int64, chunks=(1024,)
588+
))
589+
else:
590+
grp = f[gname]
591+
for name in ("tl", "tr", "bl", "br", "center", "vdd", "time"):
592+
setattr(self, f"cloud{sid}_{name}", grp[name])
593+
569594
def _get_timestamp_ns(self) -> int:
570595
"""Vrací aktuální Unix timestamp v nanosekundách."""
571596
return int(time.time() * 1e9)
@@ -628,6 +653,27 @@ def log_light(self, full: float, ir: float, gain: float, expo: float):
628653

629654
self.current_file.flush()
630655

656+
def log_cloud(self, sensor_id: int, tl: float, tr: float, bl: float,
657+
br: float, center: float, vdd: float = float('nan')):
658+
"""Zaloguje rohy/střed/Vdd pro daný senzor (1 nebo 2) do /cloud{N}."""
659+
self._ensure_file()
660+
661+
timestamp = self._get_timestamp_ns()
662+
values = {"tl": tl, "tr": tr, "bl": bl, "br": br,
663+
"center": center, "vdd": vdd}
664+
665+
time_ds = getattr(self, f"cloud{sensor_id}_time")
666+
n = time_ds.shape[0]
667+
time_ds.resize((n + 1,))
668+
time_ds[n] = timestamp
669+
670+
for name, value in values.items():
671+
ds = getattr(self, f"cloud{sensor_id}_{name}")
672+
ds.resize((n + 1,))
673+
ds[n] = value
674+
675+
self.current_file.flush()
676+
631677
def close(self):
632678
"""Zavře aktuální HDF5 soubor."""
633679
if self.current_file is not None:
@@ -661,6 +707,167 @@ def log_message(self, format, *args):
661707
pass
662708

663709

710+
class HeadlessRunner:
711+
"""Headless (no-GUI) běh nad seriovou linkou.
712+
713+
Parsuje příchozí $thrmap / $hygro / $light / $cloud_meta / $cloud, loguje
714+
do HDF5 (pokud zapnuto) a volitelně vypisuje hodnoty na stdout.
715+
"""
716+
717+
def __init__(self, ser, device_id="AMSKY01", log_path=".",
718+
enable_logging=True, verbose=False, sqm_zp=24.0,
719+
stats_interval=10.0):
720+
self.ser = ser
721+
self.verbose = verbose
722+
self.sqm_zp = sqm_zp
723+
self.stats_interval = stats_interval
724+
725+
self.line_buffer = ""
726+
self.logger = None
727+
if enable_logging:
728+
if not HDF5_AVAILABLE:
729+
print("VAROVÁNÍ: h5py není dostupné, logování vypnuto", file=sys.stderr)
730+
else:
731+
self.logger = HDF5Logger(device_id, Path(log_path or "."))
732+
print(f"HDF5 logování aktivní: {device_id} -> {log_path or '.'}")
733+
734+
# Poslední známé hodnoty pro logování
735+
self.last_ta = {1: None, 2: None}
736+
self.last_vdd = {1: float('nan'), 2: float('nan')}
737+
738+
# Počítadla pro periodický status
739+
self.counters = {"thrmap1": 0, "thrmap2": 0, "hygro": 0,
740+
"light": 0, "cloud1": 0, "cloud2": 0,
741+
"cloud_meta1": 0, "cloud_meta2": 0}
742+
self._stats_last = time.time()
743+
self._running = True
744+
745+
def _vlog(self, msg: str):
746+
if self.verbose:
747+
ts = datetime.now().strftime("%H:%M:%S")
748+
print(f"[{ts}] {msg}", flush=True)
749+
750+
def _print_stats(self):
751+
now = time.time()
752+
if now - self._stats_last < self.stats_interval:
753+
return
754+
dt = now - self._stats_last
755+
parts = [f"{k}={v}" for k, v in self.counters.items() if v > 0]
756+
log_state = ""
757+
if self.logger and self.logger.current_file:
758+
log_state = f" log={self.logger._get_filename(self.logger.current_hour)}"
759+
print(f"[stats {dt:.1f}s] " + ", ".join(parts) + log_state, flush=True)
760+
for k in self.counters:
761+
self.counters[k] = 0
762+
self._stats_last = now
763+
764+
def process_line(self, line: str):
765+
s = line.strip()
766+
if not s:
767+
return
768+
769+
if s.startswith("$thrmap,"):
770+
result = parse_thrmap(s)
771+
if result is None:
772+
return
773+
sensor_id, frame = result
774+
self.counters[f"thrmap{sensor_id}"] += 1
775+
self._vlog(f"thrmap S{sensor_id}: min={frame.min():.2f} "
776+
f"max={frame.max():.2f} mean={frame.mean():.2f} °C")
777+
if self.logger and self.last_ta[sensor_id] is not None:
778+
self.logger.log_sky(sensor_id, frame, self.last_ta[sensor_id])
779+
return
780+
781+
if s.startswith("$hygro,"):
782+
v = parse_hygro(s)
783+
if v is None:
784+
return
785+
t, rh, dew = v
786+
self.counters["hygro"] += 1
787+
self._vlog(f"hygro: T={t:.2f}°C RH={rh:.2f}% dew={dew:.2f}°C")
788+
if self.logger:
789+
self.logger.log_hygro(t, rh)
790+
return
791+
792+
if s.startswith("$light,"):
793+
v = parse_light(s)
794+
if v is None:
795+
return
796+
lux, full_raw, ir_raw, gain, itime, sqm_fw = v
797+
self.counters["light"] += 1
798+
try:
799+
gain_val = gain_str_to_float(gain)
800+
itime_val = float(str(itime).rstrip('ms'))
801+
sqm_calc = calculate_sqm(int(full_raw), int(ir_raw),
802+
itime_val, gain_val, self.sqm_zp)
803+
sqm_str = f"{sqm_calc:.2f}" if sqm_calc != float('inf') else "inf"
804+
except Exception:
805+
gain_val, itime_val, sqm_str = 1.0, 100.0, "n/a"
806+
self._vlog(f"light: full={full_raw:.0f} ir={ir_raw:.0f} "
807+
f"gain={gain} int={itime} lux={lux:.4f} sqm={sqm_str}")
808+
if self.logger:
809+
try:
810+
self.logger.log_light(full_raw, ir_raw, gain_val, itime_val)
811+
except Exception as e:
812+
print(f"chyba logování light: {e}", file=sys.stderr)
813+
return
814+
815+
if s.startswith("$cloud_meta,"):
816+
v = parse_cloud_meta(s)
817+
if v is None:
818+
return
819+
sensor_id, vdd, ta = v
820+
self.counters[f"cloud_meta{sensor_id}"] += 1
821+
self.last_ta[sensor_id] = ta
822+
self.last_vdd[sensor_id] = vdd
823+
self._vlog(f"cloud_meta S{sensor_id}: Vdd={vdd:.3f} V Ta={ta:.3f} °C")
824+
return
825+
826+
if s.startswith("$cloud,"):
827+
v = parse_cloud(s)
828+
if v is None:
829+
return
830+
sensor_id, tl, tr, bl, br, ctr = v
831+
self.counters[f"cloud{sensor_id}"] += 1
832+
self._vlog(f"cloud S{sensor_id}: TL={tl:.2f} TR={tr:.2f} "
833+
f"BL={bl:.2f} BR={br:.2f} C={ctr:.2f} °C")
834+
if self.logger:
835+
self.logger.log_cloud(sensor_id, tl, tr, bl, br, ctr,
836+
self.last_vdd[sensor_id])
837+
return
838+
839+
def run(self):
840+
"""Hlavní smyčka — čte ze seriové linky a zpracovává řádky."""
841+
if self.ser is None:
842+
print("Chyba: headless režim vyžaduje --port", file=sys.stderr)
843+
return 1
844+
845+
print(f"Headless mode: čtu z {self.ser.port} @ {self.ser.baudrate} Bd",
846+
flush=True)
847+
try:
848+
while self._running:
849+
try:
850+
waiting = self.ser.in_waiting
851+
if waiting:
852+
chunk = self.ser.read(waiting).decode(errors="ignore")
853+
self.line_buffer += chunk
854+
while "\n" in self.line_buffer:
855+
line, self.line_buffer = self.line_buffer.split("\n", 1)
856+
self.process_line(line)
857+
else:
858+
time.sleep(0.02)
859+
except serial.SerialException as e:
860+
print(f"Serial error: {e}", file=sys.stderr)
861+
return 2
862+
self._print_stats()
863+
except KeyboardInterrupt:
864+
print("\nUkončeno uživatelem (Ctrl+C)")
865+
finally:
866+
if self.logger:
867+
self.logger.close()
868+
return 0
869+
870+
664871
class ConfigDialog(QtWidgets.QDialog):
665872
"""Konfigurační dialog."""
666873

@@ -753,6 +960,9 @@ def __init__(self, ser=None, vmin=None, vmax=None, rotation=0.0, debug=False,
753960
# Pro logování sky dat potřebujeme poslední známou Ta
754961
self.last_ta1 = None
755962
self.last_ta2 = None
963+
# Poslední známá Vdd z $cloud_meta — připojí se k $cloud zápisu
964+
self.last_vdd1 = float('nan')
965+
self.last_vdd2 = float('nan')
756966

757967
# Režim zobrazení (raw vs calibrated)
758968
self.show_calibrated = False
@@ -1380,12 +1590,14 @@ def process_line(self, line: str):
13801590
self.lbl_vdd1.setText(f"{vdd:.3f}")
13811591
self.lbl_ta1.setText(f"{ta:.3f}")
13821592
self.last_ta1 = ta
1593+
self.last_vdd1 = vdd
13831594
self.current_data["mlx1"]["vdd"] = vdd
13841595
self.current_data["mlx1"]["ta"] = ta
13851596
elif sensor_id == 2:
13861597
self.lbl_vdd2.setText(f"{vdd:.3f}")
13871598
self.lbl_ta2.setText(f"{ta:.3f}")
13881599
self.last_ta2 = ta
1600+
self.last_vdd2 = vdd
13891601
self.current_data["mlx2"]["vdd"] = vdd
13901602
self.current_data["mlx2"]["ta"] = ta
13911603

@@ -1408,6 +1620,8 @@ def process_line(self, line: str):
14081620
self.current_data["cloud1"]["bl"] = bl
14091621
self.current_data["cloud1"]["br"] = br
14101622
self.current_data["cloud1"]["center"] = ctr
1623+
if self.logging_enabled and self.logger:
1624+
self.logger.log_cloud(1, tl, tr, bl, br, ctr, self.last_vdd1)
14111625
elif sensor_id == 2:
14121626
self.lbl_tl2.setText(f"{tl:.2f}")
14131627
self.lbl_tr2.setText(f"{tr:.2f}")
@@ -1419,7 +1633,9 @@ def process_line(self, line: str):
14191633
self.current_data["cloud2"]["bl"] = bl
14201634
self.current_data["cloud2"]["br"] = br
14211635
self.current_data["cloud2"]["center"] = ctr
1422-
1636+
if self.logging_enabled and self.logger:
1637+
self.logger.log_cloud(2, tl, tr, bl, br, ctr, self.last_vdd2)
1638+
14231639
self.update_data_table()
14241640
return
14251641

@@ -1740,9 +1956,49 @@ def main():
17401956
parser.add_argument("--log-name", type=str, default=None, help="ID zařízení pro názvy HDF5 souborů")
17411957
parser.add_argument("--log-path", type=str, default=None, help="cesta pro ukládání HDF5 souborů")
17421958
parser.add_argument("--sqm-zp", type=float, default=24.0, help="SQM kalibrační konstanta (výchozí: 24.0)")
1959+
parser.add_argument("--headless", "--no-gui", dest="headless", action="store_true",
1960+
help="běh bez GUI — pouze parsování a logování ze seriové linky")
1961+
parser.add_argument("--verbose", "-v", action="store_true",
1962+
help="vypisuj přijímané hodnoty na stdout")
1963+
parser.add_argument("--stats-interval", type=float, default=10.0,
1964+
help="interval [s] pro souhrnný status v headless režimu (default 10)")
17431965

17441966
args = parser.parse_args()
17451967

1968+
if args.headless:
1969+
if args.port is None:
1970+
print("Chyba: --headless vyžaduje --port", file=sys.stderr)
1971+
sys.exit(2)
1972+
try:
1973+
ser = serial.Serial(args.port, args.baud, timeout=0)
1974+
except serial.SerialException as e:
1975+
print(f"Nelze otevřít port {args.port}: {e}", file=sys.stderr)
1976+
sys.exit(1)
1977+
try:
1978+
ser.write(b"thrmap_on\n")
1979+
if args.verbose:
1980+
print("> thrmap_on")
1981+
except serial.SerialException:
1982+
pass
1983+
1984+
device_id = args.log_name or "AMSKY01"
1985+
log_path = args.log_path or "."
1986+
runner = HeadlessRunner(
1987+
ser,
1988+
device_id=device_id,
1989+
log_path=log_path,
1990+
enable_logging=args.log,
1991+
verbose=args.verbose,
1992+
sqm_zp=args.sqm_zp,
1993+
stats_interval=args.stats_interval,
1994+
)
1995+
rc = runner.run()
1996+
try:
1997+
ser.close()
1998+
except Exception:
1999+
pass
2000+
sys.exit(rc)
2001+
17462002
if args.port is None and args.replay is None:
17472003
# Spuštění bez argumentů – dialog pro výběr souboru se otevře po startu
17482004
print("Tip: zadejte --port nebo --replay, nebo použijte File > Open Replay File...")

0 commit comments

Comments
 (0)