diff --git a/dosview/__init__.py b/dosview/__init__.py index 2390360..c0307d4 100644 --- a/dosview/__init__.py +++ b/dosview/__init__.py @@ -87,23 +87,40 @@ def plot(self, data): plot_evolution.plot(time_axis[window_size-1:], rolling_avg, pen=pen) ev_data = self.data[2] - plot_spectrum.plot(range(len(ev_data)), ev_data, + plot_spectrum.plot(range(len(ev_data)), ev_data, pen="r", symbol='x', symbolPen = 'g', symbolBrush = 0.2, name = "Energy") plot_spectrum.setLabel("left", "Total count per channel", units="#") plot_spectrum.setLabel("bottom", "Channel", units="#") - # np_metadata = data[4] - - # print("METADATA") - # print(np_metadata[:,0]/60) - # print(np_metadata[:,6]) - # plot_evolution.plot(np_metadata[:,0]/60, np_metadata[:,6], pen="b", symbol='p', symbolPen='b', symbolBrush=0.1, name="Pressure") - - plot_spectrum.setLogMode(x=True, y=True) plot_spectrum.showGrid(x=True, y=True) + if len(self.data) > 4 and self.data[4]: + telemetry = self.data[4] + plot_telemetry = self.addPlot(row=2, col=0) + plot_telemetry.showGrid(x=True, y=True) + plot_telemetry.setLabel("bottom", "Time", units="min") + plot_telemetry.addLegend() + + colors = { + "temperature_0": (220, 50, 50), + "humidity_0": (50, 100, 220), + "temperature_1": (220, 130, 50), + "humidity_1": (50, 180, 220), + "temperature_2": (180, 50, 180), + "pressure_3": (50, 200, 100), + "voltage": (240, 240, 50), + "current": (200, 50, 200), + "capacity_remaining": (100, 220, 150), + "capacity_full": (150, 150, 150), + "temperature": (220, 100, 100), + } + for key, (t, vals) in telemetry.items(): + pen = pg.mkPen(color=colors.get(key, (200, 200, 200)), width=2) + line = plot_telemetry.plot(t / 60, vals, pen=pen, name=key) + self.telemetry_lines[key] = line + print("PLOT DURATION ... ", time.time()-start_time) def telemetry_toggle(self, key, value): @@ -804,6 +821,11 @@ def initUI(self): self.upload_file_button.setMaximumHeight(20) self.upload_file_button.clicked.connect(lambda: UploadFileDialog().exec_()) + self.export_csv_button = QPushButton("Export spectrum") + self.export_csv_button.setMaximumHeight(20) + self.export_csv_button.clicked.connect(self.export_spectrum_csv) + self.export_csv_button.setEnabled(False) + log_view_widget = QWidget() self.left_panel = QSplitter(Qt.Vertical) @@ -814,6 +836,7 @@ def initUI(self): vb = QHBoxLayout() vb.addWidget(self.open_img_view_button) vb.addWidget(self.upload_file_button) + vb.addWidget(self.export_csv_button) self.left_panel.setLayout(vb) self.logView_splitter = QSplitter(Qt.Horizontal) @@ -843,8 +866,9 @@ def start_data_loading(self): self.load_data_thread.start() def on_data_loaded(self, data): - self.data = data # TODO>.. tohle do budoucna zrusit a nahradit tridou parseru.. + self.data = data # TODO>.. tohle do budoucna zrusit a nahradit tridou parseru.. print("Data are fully loaded...") + self.export_csv_button.setEnabled(True) self.plot_canvas.plot(data) print("After plot data canvas") @@ -888,12 +912,28 @@ def add_properties_to_tree(item, properties): def open_spectrogram_view(self): - matrix = self.data[-1] #TODO .. tohle predelat na nejakou tridu pro parserovani + matrix = self.data[3] # metadata dict (spectrogram view) w = DataSpectrumView(self) w.show() w.plot_data(matrix) + def export_spectrum_csv(self): + path, _ = QFileDialog.getSaveFileName( + self, "Export spectrum", "", "CSV files (*.csv)" + ) + if not path: + return + if not path.endswith(".csv"): + path += ".csv" + import csv + hist = self.data[2] + with open(path, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(["channel", "counts"]) + for ch, cnt in enumerate(hist): + writer.writerow([ch, int(cnt)]) + class UploadFileDialog(QDialog): def __init__(self, parent=None): diff --git a/dosview/parsers.py b/dosview/parsers.py index a050d08..e9a5691 100644 --- a/dosview/parsers.py +++ b/dosview/parsers.py @@ -29,38 +29,45 @@ def parse(self): # pragma: no cover - concrete classes implement raise NotImplementedError -class Airdos04CLogParser(BaseLogParser): - """Parser for AIRDOS04C log files.""" +class AirdosV2LogParser(BaseLogParser): + """Parser for AIRDOS log files using data format version 2.x.""" @staticmethod def detect(file_path: str | Path) -> bool: with open(file_path, "r") as f: for line in f: - if line.startswith("$DOS") and "AIRDOS04C" in line: - return True + if line.startswith("$DOS"): + parts = line.strip().split(",") + # parts[2] = fw-version; MAJOR.MINOR encodes the data format version + if len(parts) > 2 and parts[2].startswith("2."): + return True return False def parse(self): start_time = time.time() - print("AIRDOS04C parser start") + print("AIRDOS v2 parser start") metadata = { "log_runs_count": 0, "log_device_info": {}, "log_info": {}, } - hist = np.zeros(1024, dtype=int) + hist = np.zeros(65536, dtype=int) total_counts = 0 sums: List[int] = [] time_axis: List[float] = [] inside_run = False current_hist = None current_counts = 0 + device_type = "unknown" + env_records: List[Tuple[float, ...]] = [] + batt_records: List[Tuple[float, ...]] = [] with open(self.file_path, "r") as file: for line in file: parts = line.strip().split(",") match parts[0]: case "$DOS": + device_type = parts[1] if len(parts) > 1 else "unknown" metadata["log_device_info"]["DOS"] = { "type": parts[0], "hw-model": parts[1], @@ -83,8 +90,8 @@ def parse(self): current_counts += 1 case "$STOP": if inside_run: - if len(parts) > 4: - for idx, val in enumerate(parts[4:]): + if len(parts) > 5: + for idx, val in enumerate(parts[5:]): try: current_hist[idx] += int(val) except ValueError: @@ -95,6 +102,35 @@ def parse(self): time_axis.append(float(parts[2])) inside_run = False current_hist = None + case "$ENV": + # $ENV,count,tm.tm_s100,T1,H1,T2,H2,T_MS5611,P_MS5611 + if len(parts) >= 9: + try: + env_records.append(( + float(parts[2]), # time + float(parts[3]), # T1 (SHT31 primary) + float(parts[4]), # H1 + float(parts[5]), # T2 (SHT31 secondary) + float(parts[6]), # H2 + float(parts[7]), # T_MS5611 + float(parts[8]), # P_MS5611 + )) + except ValueError: + pass + case "$BATT": + # $BATT,count,tm.tm_s100,voltage_mV,current_mA,remaining_mAh,full_mAh,temp_C + if len(parts) >= 8: + try: + batt_records.append(( + float(parts[2]), # time + float(parts[3]), # voltage_mV + float(parts[4]), # current_mA + float(parts[5]), # remaining_mAh + float(parts[6]), # full_charge_mAh + float(parts[7]), # temperature_C + )) + except ValueError: + pass case _: continue @@ -102,10 +138,31 @@ def parse(self): metadata["log_info"]["events_total"] = int(total_counts) metadata["log_info"]["log_type_version"] = "2.0" metadata["log_info"]["log_type"] = "xDOS_SPECTRAL" - metadata["log_info"]["detector_type"] = "AIRDOS04C" - print("Parsed AIRDOS04C format in", time.time() - start_time, "s") + metadata["log_info"]["detector_type"] = device_type + + telemetry: dict = {} + if env_records: + ea = np.array(env_records) + telemetry["temperature_0"] = (ea[:, 0], ea[:, 1]) + telemetry["humidity_0"] = (ea[:, 0], ea[:, 2]) + telemetry["temperature_1"] = (ea[:, 0], ea[:, 3]) + telemetry["humidity_1"] = (ea[:, 0], ea[:, 4]) + telemetry["temperature_2"] = (ea[:, 0], ea[:, 5]) + telemetry["pressure_3"] = (ea[:, 0], ea[:, 6]) + if batt_records: + ba = np.array(batt_records) + telemetry["voltage"] = (ba[:, 0], ba[:, 1]) + telemetry["current"] = (ba[:, 0], ba[:, 2]) + telemetry["capacity_remaining"] = (ba[:, 0], ba[:, 3]) + telemetry["capacity_full"] = (ba[:, 0], ba[:, 4]) + telemetry["temperature"] = (ba[:, 0], ba[:, 5]) + + print("Parsed AIRDOS v2 format in", time.time() - start_time, "s") + return [np.array(time_axis), np.array(sums), hist, metadata, telemetry] + - return [np.array(time_axis), np.array(sums), hist, metadata] +# Backwards-compatible alias +Airdos04CLogParser = AirdosV2LogParser class OldLogParser(BaseLogParser): @@ -203,7 +260,7 @@ def parse(self): return [time_column, sums, hist, metadata] -LOG_PARSERS: Sequence[type[BaseLogParser]] = [Airdos04CLogParser, OldLogParser] +LOG_PARSERS: Sequence[type[BaseLogParser]] = [AirdosV2LogParser, OldLogParser] def get_parser_for_file(file_path: str | Path) -> BaseLogParser: @@ -220,7 +277,8 @@ def parse_file(file_path: str | Path): __all__ = [ "BaseLogParser", - "Airdos04CLogParser", + "AirdosV2LogParser", + "Airdos04CLogParser", # backwards-compatible alias "OldLogParser", "get_parser_for_file", "parse_file",