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
2021import 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 ("\n Ukončeno uživatelem (Ctrl+C)" )
865+ finally :
866+ if self .logger :
867+ self .logger .close ()
868+ return 0
869+
870+
664871class 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