diff --git a/dosview/__init__.py b/dosview/__init__.py index 87ecf7f..ace1395 100644 --- a/dosview/__init__.py +++ b/dosview/__init__.py @@ -230,6 +230,7 @@ def connectSlot(self, state=True, power_off=False): else: hid_interface_uart = hidDevice + if hid_interface_i2c is None or hid_interface_uart is None: self.loadingStateChanged.emit(False, "") self.errorOccurred.emit("AIRDOS device not found.\nPlease check that the device is connected via USB.") @@ -868,7 +869,7 @@ def refresh_ports(): layout.addWidget(splitter) self.setLayout(layout) - def _open_eeprom_manager(self, read_addr: int): + def _open_eeprom_manager(self, read_addr: int, module_type: str = "detector"): def _log_eeprom(kind, message, data=None, *, full=False): colors = {"read": "\x1b[32m", "write": "\x1b[33m", "info": "\x1b[36m"} prefix = f"[EEPROM][{kind.upper()}]" @@ -895,9 +896,18 @@ def write_device(blob: bytes) -> None: _log_eeprom( "write", f"Demo mode: would write {len(blob)} bytes", data=bytes(blob[:16]) ) + read_sn = None else: hw = self.i2c_thread.hw + # SN adresa podle typu modulu: + # detektor (USTSIPIN analogová deska) → an_eeprom_sn (0x5B) + # battery (BatDatUnit hlavní deska) → eeprom_sn (0x58) + if module_type == "detector": + sn_addr = hw.addr.an_eeprom_sn + else: + sn_addr = hw.addr.eeprom_sn + def read_device() -> bytes: try: hw.set_i2c_direction(to_usb=True) @@ -905,8 +915,8 @@ def read_device() -> bytes: # Debug: read serial number try: - sn = hw.read_serial_number(hw.addr.eeprom_sn) - print(f"EEPROM SN: {hex(sn)}") + sn = hw.read_serial_number(sn_addr) + print(f"EEPROM SN (addr=0x{sn_addr:02X}): {hex(sn)}") except Exception as e: print(f"Warning: Could not read EEPROM SN: {e}") @@ -936,6 +946,15 @@ def write_device(blob: bytes) -> None: finally: hw.set_i2c_direction(to_usb=False) + def read_sn() -> int: + # I2C směr už nastavuje volající (read_device drive). Zde to pro + # případ samostatného volání zajistíme explicitně. + try: + hw.set_i2c_direction(to_usb=True) + return hw.read_serial_number(sn_addr) + finally: + hw.set_i2c_direction(to_usb=False) + dlg = QDialog(self) dlg.setWindowTitle(f"EEPROM Manager (addr=0x{read_addr:02X})") v = QVBoxLayout(dlg) @@ -943,7 +962,9 @@ def write_device(blob: bytes) -> None: w = EepromManagerWidget( read_device=read_device, write_device=write_device, + read_sn=read_sn, io_context=self.i2c_thread, + module_type=module_type, ) v.addWidget(w) btn_close = QPushButton("Close") @@ -955,16 +976,16 @@ def write_device(blob: bytes) -> None: def open_eeprom_manager_detector(self): """Open EEPROM manager for the analogue board (USTSIPIN).""" if self.i2c_thread.hw: - self._open_eeprom_manager(self.i2c_thread.hw.addr.an_eeprom) + self._open_eeprom_manager(self.i2c_thread.hw.addr.an_eeprom, module_type="detector") else: - self._open_eeprom_manager(0x53) # fallback address + self._open_eeprom_manager(0x53, module_type="detector") # fallback address def open_eeprom_manager_battery(self): """Open EEPROM manager for the BatDatUnit.""" if self.i2c_thread.hw: - self._open_eeprom_manager(self.i2c_thread.hw.addr.eeprom) + self._open_eeprom_manager(self.i2c_thread.hw.addr.eeprom, module_type="battery") else: - self._open_eeprom_manager(0x50) # fallback address + self._open_eeprom_manager(0x50, module_type="battery") # fallback address def open_rtc_manager(self): """Open the RTC manager for detector clock management.""" diff --git a/dosview/eeprom_widget.py b/dosview/eeprom_widget.py index dfe13ba..571a140 100644 --- a/dosview/eeprom_widget.py +++ b/dosview/eeprom_widget.py @@ -8,7 +8,9 @@ from pathlib import Path from typing import Any, Callable, Optional -from PyQt5 import QtCore, QtWidgets +import struct + +from PyQt5 import QtCore, QtGui, QtWidgets from .eeprom_schema import ( DeviceType, @@ -23,6 +25,7 @@ IntReader = Callable[[], bytes] IntWriter = Callable[[bytes], None] +SNReader = Callable[[], int] class EepromManagerWidget(QtWidgets.QWidget): @@ -31,12 +34,18 @@ def __init__( parent: Optional[QtWidgets.QWidget] = None, read_device: Optional[IntReader] = None, write_device: Optional[IntWriter] = None, + read_sn: Optional[SNReader] = None, io_context: Optional[Any] = None, + module_type: str = "detector", ) -> None: super().__init__(parent) self._read_device = read_device self._write_device = write_device + self._read_sn = read_sn self._io_context = io_context + # Typ modulu ("detector" nebo "battery") - ovlivňuje, zda se zobrazí + # sekce keV kalibrace (pouze detektor má kalibrační koeficienty). + self._module_type = module_type self._widgets = {} self._build_ui() @@ -97,6 +106,14 @@ def _build_ui(self) -> None: self._widgets["device_identifier"] = self._make_line_edit(max_length=24) form_layout.addRow("Device identifier (max 24 chars):", self._widgets["device_identifier"]) + # Serial number (vyčtené z EEPROM SN, read-only) + self._widgets["serial_number"] = self._make_line_edit(readonly=True) + self._widgets["serial_number"].setPlaceholderText("—") + sn_font = self._widgets["serial_number"].font() + sn_font.setFamily("monospace") + self._widgets["serial_number"].setFont(sn_font) + form_layout.addRow("Serial number (SN):", self._widgets["serial_number"]) + # === Configuration === form_layout.addRow(self._make_section_label("Configuration")) @@ -106,51 +123,112 @@ def _build_ui(self) -> None: self._widgets["rtc_flags"] = self._make_binary_field(bits=8) form_layout.addRow("RTC flags (bin):", self._widgets["rtc_flags"]) - # === RTC history (most recent entry) === - form_layout.addRow(self._make_section_label("RTC History (entry 0 — most recent)")) - - rtc_info = QtWidgets.QLabel("Up to 5 RTC synchronisation entries are stored; editing affects entry 0 only.") - rtc_info.setStyleSheet("color: gray; font-size: 10px;") - rtc_info.setWordWrap(True) - form_layout.addRow("", rtc_info) - - self._widgets["rtc_init_ts"] = self._make_line_edit() - self._widgets["rtc_init_ts"].setPlaceholderText("Unix timestamp (s)") - form_layout.addRow("RTC init timestamp:", self._widgets["rtc_init_ts"]) - - self._widgets["rtc_init_ts_label"] = QtWidgets.QLabel("") - self._widgets["rtc_init_ts_label"].setStyleSheet("color: #666; font-style: italic;") - form_layout.addRow("", self._widgets["rtc_init_ts_label"]) - - self._widgets["rtc_ref_ts"] = self._make_line_edit() - self._widgets["rtc_ref_ts"].setPlaceholderText("Unix timestamp (s)") - form_layout.addRow("Reference timestamp:", self._widgets["rtc_ref_ts"]) - - self._widgets["rtc_ref_ts_label"] = QtWidgets.QLabel("") - self._widgets["rtc_ref_ts_label"].setStyleSheet("color: #666; font-style: italic;") - form_layout.addRow("", self._widgets["rtc_ref_ts_label"]) - - self._widgets["rtc_value_at_ref"] = self._make_line_edit() - self._widgets["rtc_value_at_ref"].setPlaceholderText("RTC seconds at reference") - form_layout.addRow("RTC value at reference:", self._widgets["rtc_value_at_ref"]) + # === RTC synchronization === + # RTC sync konstanty se zobrazují pouze u battery modulu (BatDatUnit + # obsahuje RTC čip). Detector (USTSIPIN) nemá RTC parametry. + if self._module_type == "battery": + form_layout.addRow(self._make_section_label("RTC Synchronization")) + + rtc_info = QtWidgets.QLabel("Timestamps for RTC clock synchronization") + rtc_info.setStyleSheet("color: gray; font-size: 10px;") + form_layout.addRow("", rtc_info) + + rtc_note = QtWidgets.QLabel( + "ℹ️ Usually managed via RTC Manager. Saved to file but skipped when loading from file." + ) + rtc_note.setStyleSheet("color: #b36b00; font-size: 10px; font-style: italic;") + rtc_note.setWordWrap(True) + form_layout.addRow("", rtc_note) + + # Init time (kdy RTC bylo na 0) + self._widgets["init_time"] = self._make_line_edit() + self._widgets["init_time"].setPlaceholderText("Unix timestamp (s)") + form_layout.addRow("Init time:", self._widgets["init_time"]) + + self._widgets["init_time_label"] = QtWidgets.QLabel("") + self._widgets["init_time_label"].setStyleSheet("color: #666; font-style: italic;") + form_layout.addRow("", self._widgets["init_time_label"]) + + # Sync time (čas poslední synchronizace) + self._widgets["sync_time"] = self._make_line_edit() + self._widgets["sync_time"].setPlaceholderText("Unix timestamp (s)") + form_layout.addRow("Sync time:", self._widgets["sync_time"]) + + self._widgets["sync_time_label"] = QtWidgets.QLabel("") + self._widgets["sync_time_label"].setStyleSheet("color: #666; font-style: italic;") + form_layout.addRow("", self._widgets["sync_time_label"]) + + # Sync RTC seconds (RTC value at synchronization) + self._widgets["sync_rtc_seconds"] = self._make_line_edit() + self._widgets["sync_rtc_seconds"].setPlaceholderText("RTC seconds at sync") + form_layout.addRow("Sync RTC seconds:", self._widgets["sync_rtc_seconds"]) + else: + # Detector nemá RTC parametry - vytvoříme skrytá pole pro zachování + # API kompatibility (populate/collect). Hodnoty se začtou z EEPROM + # a zapíšou zpět beze změny. + self._widgets["init_time"] = self._make_line_edit() + self._widgets["init_time"].setVisible(False) + self._widgets["init_time_label"] = QtWidgets.QLabel("") + self._widgets["init_time_label"].setVisible(False) + self._widgets["sync_time"] = self._make_line_edit() + self._widgets["sync_time"].setVisible(False) + self._widgets["sync_time_label"] = QtWidgets.QLabel("") + self._widgets["sync_time_label"].setVisible(False) + self._widgets["sync_rtc_seconds"] = self._make_line_edit() + self._widgets["sync_rtc_seconds"].setVisible(False) # === keV calibration === - form_layout.addRow(self._make_section_label("keV Calibration")) - - calib_info = QtWidgets.QLabel("Polynomial coefficients: keV = a₀ + a₁·ch + a₂·ch²") - calib_info.setStyleSheet("color: gray; font-size: 10px;") - form_layout.addRow("", calib_info) - - self._widgets["calibration_constants"] = [] - calib_labels = ["a₀ (offset) [keV]:", "a₁ (linear) [keV/ch]:", "a₂ (quadratic) [keV/ch²]:"] - for i, label in enumerate(calib_labels): - f = self._make_double_field() - self._widgets["calibration_constants"].append(f) - form_layout.addRow(label, f) - - self._widgets["calibration_version"] = self._make_line_edit() - self._widgets["calibration_version"].setPlaceholderText("Unix timestamp") - form_layout.addRow("Calibration version (timestamp):", self._widgets["calibration_version"]) + # Kalibrační koeficienty se zobrazují pouze u detektoru (ne u battery + # modulu). Hodnoty lze zadat ručně. + self._widgets["calib"] = [] + if self._module_type == "detector": + form_layout.addRow(self._make_section_label("keV Calibration")) + + calib_info = QtWidgets.QLabel("Polynomial coefficients: keV = a₀ + a₁·ch + a₂·ch²") + calib_info.setStyleSheet("color: gray; font-size: 10px;") + form_layout.addRow("", calib_info) + + calib_note = QtWidgets.QLabel( + "ℹ️ Stored as float32 (single precision, ~7 significant digits). " + "Scientific notation is supported (e.g. 1.5e-5)." + ) + calib_note.setStyleSheet("color: #666; font-size: 10px; font-style: italic;") + calib_note.setWordWrap(True) + form_layout.addRow("", calib_note) + + calib_labels = ["a₀ (offset) [keV]:", "a₁ (linear) [keV/ch]:", "a₂ (quadratic) [keV/ch²]:"] + for i, label in enumerate(calib_labels): + field = self._make_float_field() + self._widgets["calib"].append(field) + form_layout.addRow(label, field) + + # Řádek s calib_ts + tlačítkem "Now" pro vložení aktuálního + # Unix timestampu. + calib_ts_row = QtWidgets.QHBoxLayout() + self._widgets["calib_ts"] = self._make_line_edit() + self._widgets["calib_ts"].setPlaceholderText("Unix timestamp") + self.btn_calib_ts_now = QtWidgets.QPushButton("⏱️ Now") + self.btn_calib_ts_now.setToolTip("Set current Unix timestamp") + self.btn_calib_ts_now.clicked.connect(self._on_calib_ts_now) + calib_ts_row.addWidget(self._widgets["calib_ts"]) + calib_ts_row.addWidget(self.btn_calib_ts_now) + form_layout.addRow("Calibration timestamp:", calib_ts_row) + + self._widgets["calib_ts_label"] = QtWidgets.QLabel("") + self._widgets["calib_ts_label"].setStyleSheet("color: #666; font-style: italic;") + form_layout.addRow("", self._widgets["calib_ts_label"]) + else: + # Battery modul nemá kalibraci - vytvoříme skrytá pole, aby zůstala + # API kompatibilita (populate/collect) a hodnoty se zachovaly + # při čtení/zápisu z/do zařízení. + for _ in range(3): + field = self._make_float_field() + field.setVisible(False) + self._widgets["calib"].append(field) + self._widgets["calib_ts"] = self._make_line_edit() + self._widgets["calib_ts"].setVisible(False) + self._widgets["calib_ts_label"] = QtWidgets.QLabel("") + self._widgets["calib_ts_label"].setVisible(False) layout.addWidget(scroll) @@ -186,13 +264,32 @@ def _make_int_field(self, minv: int = 0, maxv: int = 0xFFFFFFFF) -> QtWidgets.QS sb.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) return sb - def _make_double_field(self) -> QtWidgets.QDoubleSpinBox: + def _make_double_field(self, readonly: bool = False) -> QtWidgets.QDoubleSpinBox: dsb = QtWidgets.QDoubleSpinBox() dsb.setRange(-1e9, 1e9) dsb.setDecimals(6) dsb.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) + if readonly: + dsb.setReadOnly(True) return dsb + def _make_float_field(self) -> QtWidgets.QLineEdit: + """Pole pro float32 hodnotu s podporou vědecké notace. + + Kalibrační koeficienty jsou v EEPROM uložené jako float32 (IEEE 754 + single precision, ~7 významných číslic). Textové pole umožňuje zadat + hodnoty jako 0.00001 i 1.5e-5. + """ + le = QtWidgets.QLineEdit() + le.setPlaceholderText("0.0 (e.g. 1.5e-5)") + validator = QtGui.QDoubleValidator() + validator.setNotation(QtGui.QDoubleValidator.ScientificNotation) + le.setValidator(validator) + font = le.font() + font.setFamily("monospace") + le.setFont(font) + return le + def _make_binary_field(self, bits: int = 32) -> QtWidgets.QLineEdit: """Pole pro binární hodnotu.""" le = QtWidgets.QLineEdit() @@ -220,8 +317,22 @@ def _format_timestamp(self, ts: int) -> str: except (OSError, ValueError): return "Neplatný čas" + @staticmethod + def _to_float32(value: float) -> float: + """Zaokrouhlí hodnotu na nejbližší float32 (IEEE 754 single).""" + return struct.unpack(" str: + """Formátuje float jako řetězec s přesností float32 (7 značných číslic).""" + f32 = cls._to_float32(value) + if f32 == 0.0: + return "0" + # 7 významných číslic pokrývá přesnost float32; %g odstraní trailing nuly. + return f"{f32:.7g}" + # Data population ----------------------------------------------------- - def _populate(self, record: EepromRecord) -> None: + def _populate(self, record: EepromRecord, skip_rtc_sync: bool = False) -> None: w = self._widgets w["format_version"].setValue(int(record.format_version)) @@ -237,18 +348,51 @@ def _populate(self, record: EepromRecord) -> None: w["operating_modes"].setText(f"{record.operating_modes:016b}") w["rtc_flags"].setText(f"{record.rtc_flags:08b}") - # RTC history entry 0 + # RTC synchronizace - samostatné položky + # Při načítání ze souboru se RTC sync položky přeskočí (jsou read-only + # a spárované s reálným zařízením - nepřepisovat ze souboru). init_ts, ref_ts, rtc_val = record.rtc_history[0] if record.rtc_history else (0, 0, 0) - w["rtc_init_ts"].setText(str(init_ts)) - w["rtc_init_ts_label"].setText(self._format_timestamp(init_ts)) - w["rtc_ref_ts"].setText(str(ref_ts)) - w["rtc_ref_ts_label"].setText(self._format_timestamp(ref_ts)) - w["rtc_value_at_ref"].setText(str(rtc_val)) - - for widget, val in zip(w["calibration_constants"], record.calibration_constants): - widget.setValue(float(val)) - - w["calibration_version"].setText(str(int(record.calibration_version))) + if not skip_rtc_sync: + w["init_time"].setText(str(init_ts)) + w["init_time_label"].setText(self._format_timestamp(init_ts)) + + w["sync_time"].setText(str(ref_ts)) + w["sync_time_label"].setText(self._format_timestamp(ref_ts)) + + w["sync_rtc_seconds"].setText(str(rtc_val)) + + # Kalibrační konstanty - uložené jako float32. Zobrazíme skutečnou + # float32 reprezentaci (round-trip přes struct 'f'), aby to, co uživatel + # vidí, odpovídalo tomu, co je v EEPROM. + for widget, val in zip(w["calib"], record.calibration_constants): + widget.setText(self._format_float32(val)) + + w["calib_ts"].setText(str(int(record.calibration_version))) + if "calib_ts_label" in w: + w["calib_ts_label"].setText(self._format_timestamp(int(record.calibration_version))) + + def _on_calib_ts_now(self) -> None: + """Vloží aktuální Unix timestamp do pole calib_ts.""" + import time + ts = int(time.time()) + self._widgets["calib_ts"].setText(str(ts)) + if "calib_ts_label" in self._widgets: + self._widgets["calib_ts_label"].setText(self._format_timestamp(ts)) + + def _update_serial_number(self) -> None: + """Vyčte SN z EEPROM SN čipu a zobrazí ho ve formuláři.""" + if not self._read_sn: + self._widgets["serial_number"].setText("— (SN reader not connected)") + return + try: + sn = self._read_sn() + # SN z AT24CS64 je 128-bit integer - zobrazit jako hex + if isinstance(sn, int): + self._widgets["serial_number"].setText(f"0x{sn:032X}") + else: + self._widgets["serial_number"].setText(str(sn)) + except Exception as exc: + self._widgets["serial_number"].setText(f"— (error: {exc})") # Handlers ------------------------------------------------------------ def _on_load_device(self) -> None: @@ -259,6 +403,8 @@ def _on_load_device(self) -> None: with LoadingContext(self, "Loading EEPROM", "Reading data from device..."): data = self._read_device() record = unpack_record(data, verify_crc=False) + # Současně s EEPROM obsahem vyčteme i SN z EEPROM SN čipu. + self._update_serial_number() self._populate(record) self._set_status("✅ Loaded from device") except Exception as exc: @@ -302,8 +448,12 @@ def _on_load_file(self) -> None: try: data = Path(path).read_bytes() record = unpack_record(data, verify_crc=False) - self._populate(record) - self._set_status(f"✅ Loaded from file {Path(path).name}") + # RTC sync konstanty se při načítání ze souboru přeskočí - + # zůstanou stávající hodnoty (navázané na reálný RTC zařízení). + self._populate(record, skip_rtc_sync=True) + self._set_status( + f"✅ Loaded from file {Path(path).name} (RTC sync skipped)" + ) except Exception as exc: self._set_status(f"❌ File load error: {exc}") @@ -337,14 +487,23 @@ def _collect_record(self) -> EepromRecord: rtc_flags_text = w["rtc_flags"].text().strip() or "0" rtc_flags = int(rtc_flags_text, 2) - - init_ts = int(w["rtc_init_ts"].text().strip() or "0", 0) - ref_ts = int(w["rtc_ref_ts"].text().strip() or "0", 0) - rtc_val = int(w["rtc_value_at_ref"].text().strip() or "0", 0) - - calib_vals = [float(sp.value()) for sp in w["calibration_constants"]] - calibration_version = int(w["calibration_version"].text() or "0", 0) - + # RTC synchronizace - samostatné položky + init_time_text = w["init_time"].text().strip() or "0" + init_time = int(init_time_text, 0) + + sync_time_text = w["sync_time"].text().strip() or "0" + sync_time = int(sync_time_text, 0) + + sync_rtc_seconds_text = w["sync_rtc_seconds"].text().strip() or "0" + sync_rtc_seconds = int(sync_rtc_seconds_text, 0) + + # Kalibrační konstanty - parsujeme z textových polí (float32). + calib_vals = [] + for le in w["calib"]: + txt = le.text().strip().replace(",", ".") or "0" + calib_vals.append(float(txt)) + calib_ts_text = w["calib_ts"].text() + calib_ts = int(calib_ts_text or "0", 0) except Exception as exc: raise ValueError(f"Invalid form value: {exc}") from exc @@ -356,9 +515,9 @@ def _collect_record(self) -> EepromRecord: device_identifier=device_identifier, operating_modes=operating_modes, rtc_flags=rtc_flags, - rtc_history=[(init_ts, ref_ts, rtc_val)] + [(0, 0, 0)] * 4, + rtc_history=[(init_time, sync_time, sync_rtc_seconds)] + [(0, 0, 0)] * 4, calibration_constants=tuple(calib_vals), - calibration_version=calibration_version, + calibration_version=calib_ts, ) payload = pack_record(record, with_crc=True) record.crc32 = compute_crc32(payload) @@ -424,4 +583,3 @@ def write_dev(blob: bytes): if __name__ == "__main__": _demo() - diff --git a/dosview/rtc_widget.py b/dosview/rtc_widget.py index 656eae9..8039b3b 100644 --- a/dosview/rtc_widget.py +++ b/dosview/rtc_widget.py @@ -327,7 +327,7 @@ def _on_reset_confirm(self) -> None: self._on_reset() def _on_reset(self) -> None: - """Provede reset RTC.""" + """Provede reset RTC a následně automaticky aplikuje SYNC.""" if not self._reset_rtc: self._set_status("❌ Není připojena funkce resetu") return @@ -335,11 +335,24 @@ def _on_reset(self) -> None: try: self._last_reset_time = self._reset_rtc() self._last_sync_time = self._last_reset_time # Reset je také sync - self._set_status("✅ RTC resetováno na nulu") self.rtc_reset.emit() - self._on_update() except Exception as e: self._set_status(f"❌ Chyba resetu: {e}") + return + + # Po resetu automaticky aplikovat SYNC, aby se do EEPROM zapsal + # kalibrační bod (sync_time, sync_rtc_seconds). + if self._sync_rtc: + try: + self._last_sync_time = self._sync_rtc() + self._set_status("✅ RTC resetováno a synchronizováno") + self.rtc_synced.emit() + except Exception as e: + self._set_status(f"⚠️ RTC resetováno, sync selhal: {e}") + else: + self._set_status("✅ RTC resetováno na nulu") + + self._on_update() def _set_status(self, msg: str) -> None: """Nastaví status zprávu."""