Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions dosview/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,8 @@ def refresh_ports():
self.setLayout(layout)

def _open_eeprom_manager(self, read_addr: int, module_type: str = "detector"):
from .eeprom_schema import TOTAL_SIZE

def _log_eeprom(kind, message, data=None, *, full=False):
colors = {"read": "\x1b[32m", "write": "\x1b[33m", "info": "\x1b[36m"}
prefix = f"[EEPROM][{kind.upper()}]"
Expand All @@ -954,10 +956,10 @@ def _log_eeprom(kind, message, data=None, *, full=False):
if not self.i2c_thread or not self.i2c_thread.hw:
# Graceful fallback: demo mode without device
def read_device() -> bytes:
# Return empty 101-byte block (unprogrammed EEPROM = 0xFF)
# Return empty block of TOTAL_SIZE (unprogrammed EEPROM = 0xFF)
_log_eeprom("info", "I2C not connected; starting demo mode")
_log_eeprom("read", "Demo mode: returning synthetic 0xFF block", data=b'\xFF' * 16)
return b'\xFF' * 101
return b'\xFF' * TOTAL_SIZE
def write_device(blob: bytes) -> None:
_log_eeprom(
"write", f"Demo mode: would write {len(blob)} bytes", data=bytes(blob[:16])
Expand All @@ -977,7 +979,7 @@ def write_device(blob: bytes) -> None:
def read_device() -> bytes:
try:
hw.set_i2c_direction(to_usb=True)
_log_eeprom("read", f"Reading 101 bytes from EEPROM addr=0x{read_addr:02X}")
_log_eeprom("read", f"Reading {TOTAL_SIZE} bytes from EEPROM addr=0x{read_addr:02X}")

# Debug: read serial number
try:
Expand All @@ -987,7 +989,7 @@ def read_device() -> bytes:
print(f"Warning: Could not read EEPROM SN: {e}")

# Read EEPROM data via Airdos04Hardware
data = hw.read_eeprom(101, start_address=0, eeprom_address=read_addr)
data = hw.read_eeprom(TOTAL_SIZE, start_address=0, eeprom_address=read_addr)
_log_eeprom(
"read",
f"Total read {len(data)} bytes; sample={list(data[:8])}",
Expand Down
161 changes: 147 additions & 14 deletions dosview/eeprom_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .eeprom_schema import (
DeviceType,
EepromRecord,
KNOWN_DEVICES,
TOTAL_SIZE,
compute_crc32,
pack_record,
Expand Down Expand Up @@ -47,6 +48,9 @@ def __init__(
# sekce keV kalibrace (pouze detektor má kalibrační koeficienty).
self._module_type = module_type
self._widgets = {}
# Nastaveno v _select_device(), pokud obsah EEPROM neodpovídá žádnému
# zařízení z katalogu (xDOS_devices.yaml). Použito pro varování uživateli.
self._device_mismatch: Optional[str] = None
self._build_ui()

# UI construction -----------------------------------------------------
Expand Down Expand Up @@ -94,15 +98,23 @@ def _build_ui(self) -> None:
# === Hardware ===
form_layout.addRow(self._make_section_label("Hardware"))

# Verze a HW revize jsou kaskádové combo boxy: nabízejí pouze kombinace,
# které existují v katalogu (xDOS_devices.yaml). Změna typu přefiltruje
# dostupné verze, změna verze přefiltruje dostupné revize.
hw_layout = QtWidgets.QHBoxLayout()
self._widgets["device_version"] = self._make_int_field(minv=0, maxv=255)
self._widgets["hardware_revision"] = self._make_int_field(minv=0, maxv=255)
self._widgets["device_version"] = QtWidgets.QComboBox()
self._widgets["hardware_revision"] = QtWidgets.QComboBox()
hw_layout.addWidget(self._widgets["device_version"])
hw_layout.addWidget(QtWidgets.QLabel("rev"))
hw_layout.addWidget(self._widgets["hardware_revision"])
hw_layout.addStretch(1)
form_layout.addRow("Device version / HW revision:", hw_layout)

# Zapojení kaskády a počáteční naplnění nabídek pro výchozí typ.
self._widgets["device_type"].currentIndexChanged.connect(self._refresh_version_options)
self._widgets["device_version"].currentIndexChanged.connect(self._refresh_revision_options)
self._refresh_version_options()

self._widgets["device_identifier"] = self._make_line_edit(max_length=24)
form_layout.addRow("Device identifier (max 24 chars):", self._widgets["device_identifier"])

Expand Down Expand Up @@ -302,10 +314,119 @@ def _make_binary_field(self, bits: int = 32) -> QtWidgets.QLineEdit:

def _make_device_type_field(self) -> QtWidgets.QComboBox:
cb = QtWidgets.QComboBox()
for dt in DeviceType:
# Nabízíme pouze rodiny zařízení, které mají alespoň jedno zařízení
# v katalogu (xDOS_devices.yaml) - tedy ne UNKNOWN.
for dt in self._catalog_device_types():
cb.addItem(dt.name, int(dt))
return cb

@staticmethod
def _catalog_device_types() -> list:
"""Rodiny zařízení vyskytující se v katalogu, v pořadí prvního výskytu."""
seen: list = []
for d in KNOWN_DEVICES:
if d.device_type not in seen:
seen.append(d.device_type)
return seen

@staticmethod
def _sorted_revisions(revs: list) -> list:
"""Seřadí revize: None (žádná) první, pak písmena vzestupně."""
result: list = []
if any(r is None for r in revs):
result.append(None)
result.extend(sorted({r for r in revs if r is not None}))
return result

@staticmethod
def _revision_byte(rev) -> int:
"""Převede revizi z katalogu (písmeno nebo None) na uložený byte."""
return ord(rev) if rev else 0

@staticmethod
def _revision_label(rev_byte: int) -> str:
"""Čitelná podoba uloženého byte revize."""
if rev_byte == 0:
return "—"
if 65 <= rev_byte <= 90:
return chr(rev_byte)
return f"0x{rev_byte:02X}"

# Cascading device selection -----------------------------------------
def _refresh_version_options(self) -> None:
"""Naplní nabídku verzí podle vybraného typu zařízení."""
dt_val = self._widgets["device_type"].currentData()
version_cb = self._widgets["device_version"]
version_cb.blockSignals(True)
version_cb.clear()
versions = sorted(
{d.device_version for d in KNOWN_DEVICES if int(d.device_type) == dt_val}
)
for v in versions:
version_cb.addItem(f"{int(v):02d}", int(v))
version_cb.blockSignals(False)
self._refresh_revision_options()

def _refresh_revision_options(self) -> None:
"""Naplní nabídku revizí podle vybraného typu a verze."""
dt_val = self._widgets["device_type"].currentData()
ver = self._widgets["device_version"].currentData()
rev_cb = self._widgets["hardware_revision"]
rev_cb.blockSignals(True)
rev_cb.clear()
revs = [
d.hardware_revision
for d in KNOWN_DEVICES
if int(d.device_type) == dt_val and d.device_version == ver
]
for rev in self._sorted_revisions(revs):
rev_cb.addItem("—" if rev is None else str(rev), self._revision_byte(rev))
rev_cb.blockSignals(False)

def _select_device(self, device_type, device_version, hardware_revision) -> None:
"""Nastaví kaskádové combo boxy na danou kombinaci z EEPROM.

Pokud kombinace neexistuje v katalogu, nastaví self._device_mismatch
na varovný text a combo boxy ponechá na nejbližší platné hodnotě -
uživatel tak musí vybrat platné zařízení, než ho zapíše.
"""
w = self._widgets
self._device_mismatch = None
missing = []

type_idx = w["device_type"].findData(int(device_type))
if type_idx < 0:
name = device_type.name if isinstance(device_type, DeviceType) else str(device_type)
missing.append(f"device type {name}")
type_idx = 0
w["device_type"].blockSignals(True)
w["device_type"].setCurrentIndex(type_idx)
w["device_type"].blockSignals(False)
self._refresh_version_options()

ver_idx = w["device_version"].findData(int(device_version))
if ver_idx < 0:
missing.append(f"version {int(device_version)}")
ver_idx = 0
w["device_version"].blockSignals(True)
w["device_version"].setCurrentIndex(max(ver_idx, 0))
w["device_version"].blockSignals(False)
self._refresh_revision_options()

rev_byte = int(hardware_revision)
rev_idx = w["hardware_revision"].findData(rev_byte)
if rev_idx < 0:
missing.append(f"HW revision {self._revision_label(rev_byte)}")
rev_idx = 0
w["hardware_revision"].blockSignals(True)
w["hardware_revision"].setCurrentIndex(max(rev_idx, 0))
w["hardware_revision"].blockSignals(False)

if missing:
self._device_mismatch = (
"stored device not in catalog (" + ", ".join(missing) + ")"
)

def _format_timestamp(self, ts: int) -> str:
"""Formátuje Unix timestamp na čitelný čas."""
if ts <= 0:
Expand Down Expand Up @@ -336,13 +457,13 @@ def _populate(self, record: EepromRecord, skip_rtc_sync: bool = False) -> None:
w = self._widgets
w["format_version"].setValue(int(record.format_version))

idx = w["device_type"].findData(int(record.device_type))
if idx >= 0:
w["device_type"].setCurrentIndex(idx)
# Typ / verze / revize - kaskádové combo boxy omezené na katalog.
# _select_device nastaví _device_mismatch, pokud kombinace neexistuje.
self._select_device(
record.device_type, record.device_version, record.hardware_revision
)

w["crc32"].setText(f"0x{record.crc32:08X}")
w["device_version"].setValue(int(record.device_version))
w["hardware_revision"].setValue(int(record.hardware_revision))
w["device_identifier"].setText(record.device_identifier)

w["operating_modes"].setText(f"{record.operating_modes:016b}")
Expand Down Expand Up @@ -406,7 +527,13 @@ def _on_load_device(self) -> None:
# 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")
if self._device_mismatch:
self._set_status(
f"⚠️ Loaded from device — {self._device_mismatch}. "
"Select a valid device before writing."
)
else:
self._set_status("✅ Loaded from device")
except Exception as exc:
self._set_status(f"❌ Read error: {exc}")

Expand Down Expand Up @@ -451,9 +578,15 @@ def _on_load_file(self) -> None:
# 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)"
)
if self._device_mismatch:
self._set_status(
f"⚠️ Loaded from file {Path(path).name} — {self._device_mismatch}. "
"Select a valid device before writing."
)
else:
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}")

Expand All @@ -478,8 +611,8 @@ def _collect_record(self) -> EepromRecord:
try:
format_version = int(w["format_version"].value())
device_type = DeviceType(w["device_type"].currentData())
device_version = int(w["device_version"].value())
hardware_revision = int(w["hardware_revision"].value())
device_version = int(w["device_version"].currentData() or 0)
hardware_revision = int(w["hardware_revision"].currentData() or 0)
device_identifier = w["device_identifier"].text()

operating_modes_text = w["operating_modes"].text().strip() or "0"
Expand Down
Loading