feat(iomodbus): device settings migrate to the grouped settings list (spec 5.5)
This commit is contained in:
@@ -1,26 +1,105 @@
|
||||
"""Device Settings grid (legacy Grid(0)): Property | Value, editable where writable."""
|
||||
"""Device Settings as the §5.5 grouped settings list (replaces the legacy Grid(0)).
|
||||
|
||||
Structure comes from the selected device's catalog rows: one group headed by the
|
||||
device description (the catalog defines no semantic groups — inventing them would
|
||||
invent meaning), then one row per catalog setting in catalog order. Editability is
|
||||
per-register (``spec.writable``); read-only rows render the RO chip. Pick-list
|
||||
registers edit as §5.5 choice dropdowns whose stored raw IS the catalog *label* —
|
||||
``controller.write_cell`` already maps a label through ``codecs.selection`` to the
|
||||
numeric register value, so the wire path is unchanged. Live poll updates flow through
|
||||
``SettingsList.set_value`` (which never clobbers an open editor); the forced re-read
|
||||
after a write is the value confirmation, as on the DA-12/DA-07 station tabs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from PySide6.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from cim_suite.core.export import Cell, Sheet
|
||||
from cim_suite.core.ui.copy_menu import attach_copy_menu
|
||||
from cim_suite.core.ui.kit import SettingsList
|
||||
from cim_suite.core.ui.theme import Space
|
||||
|
||||
from ..domain.models import GRID_SETTINGS, RegisterCell
|
||||
from .. import help as H
|
||||
from .register_grid import RegisterGrid
|
||||
from ..protocol.codecs import parse_picklist
|
||||
|
||||
# §8: constraint text moves out of label parens into the faint hint.
|
||||
_PAREN = re.compile(r"\s*\(([^)]*)\)")
|
||||
|
||||
HEADERS = ["Property", "Value"] # export header row
|
||||
|
||||
|
||||
class SettingsTab(RegisterGrid):
|
||||
class SettingsTab(QWidget):
|
||||
GRID = GRID_SETTINGS
|
||||
|
||||
def __init__(self, controller) -> None:
|
||||
super().__init__(controller, ["Property", "Value"])
|
||||
self.set_column_help(H.SETTINGS_COLUMNS)
|
||||
super().__init__()
|
||||
self._ctrl = controller
|
||||
self._cells: dict[str, RegisterCell] = {} # key -> writable register cell
|
||||
self._nrows = 0
|
||||
|
||||
def _collect(self):
|
||||
rows: list[list[str]] = []
|
||||
row_cells: list[list[RegisterCell | None]] = []
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(Space.LG, Space.LG, Space.LG, Space.LG)
|
||||
layout.setSpacing(Space.MD)
|
||||
self.settings_list = SettingsList(self)
|
||||
self.settings_list.settingEdited.connect(self._on_setting_edited)
|
||||
layout.addWidget(self.settings_list)
|
||||
|
||||
controller.deviceSelected.connect(self.rebuild)
|
||||
controller.cellUpdated.connect(self._on_cell)
|
||||
attach_copy_menu(self.settings_list.table)
|
||||
self.rebuild()
|
||||
|
||||
# --- structure / values ------------------------------------------------------
|
||||
def rebuild(self) -> None:
|
||||
self.settings_list.clear()
|
||||
self._cells = {}
|
||||
state = self._ctrl.state
|
||||
self._nrows = 0 if state is None else len(state.settings)
|
||||
if state is None:
|
||||
return
|
||||
self.settings_list.add_group(state.dev.description)
|
||||
for r, srow in enumerate(state.settings):
|
||||
self._add_setting_row(str(r), srow)
|
||||
|
||||
def _add_setting_row(self, key: str, srow) -> None:
|
||||
match = _PAREN.search(srow.label)
|
||||
hint = match.group(1).strip() if match else ""
|
||||
label = _PAREN.sub("", srow.label).strip() or srow.label
|
||||
cell = srow.cell
|
||||
if cell is None or not cell.spec.writable:
|
||||
self.settings_list.add_setting(key, label, hint=hint, read_only=True)
|
||||
elif cell.spec.picklist:
|
||||
labels = [lab for _v, lab in parse_picklist(cell.spec.picklist)]
|
||||
# raw == display label: write_cell maps the label via codecs.selection.
|
||||
self.settings_list.add_choice(key, label, {lab: lab for lab in labels}, hint=hint)
|
||||
self._cells[key] = cell
|
||||
else:
|
||||
self.settings_list.add_setting(key, label, hint=hint)
|
||||
self._cells[key] = cell
|
||||
if cell is not None:
|
||||
self.settings_list.set_value(key, cell.value)
|
||||
|
||||
def _on_cell(self, grid: int, row: int, col: int, value: str) -> None:
|
||||
if grid != GRID_SETTINGS or col != 1 or row >= self._nrows:
|
||||
return
|
||||
self.settings_list.set_value(str(row), value)
|
||||
|
||||
# --- edits -------------------------------------------------------------------
|
||||
def _on_setting_edited(self, key: str, raw: str) -> None:
|
||||
cell = self._cells.get(key)
|
||||
if cell is not None:
|
||||
self._ctrl.write_cell(cell, raw)
|
||||
|
||||
# --- export ------------------------------------------------------------------
|
||||
def export_sheet(self) -> Sheet | None:
|
||||
state = self._ctrl.state
|
||||
rows: list[list[Cell]] = []
|
||||
if state is not None:
|
||||
rows.append([Cell(state.dev.description), Cell("")]) # group header row
|
||||
for srow in state.settings:
|
||||
value = srow.cell.value if srow.cell is not None else ""
|
||||
rows.append([srow.label, value])
|
||||
row_cells.append([None, srow.cell]) # col 0 label, col 1 value
|
||||
return rows, row_cells
|
||||
rows.append([Cell(srow.label), Cell(value)])
|
||||
return Sheet(title="", headers=list(HEADERS), rows=rows)
|
||||
|
||||
95
tests/iomodbus/test_settings_list_tab.py
Normal file
95
tests/iomodbus/test_settings_list_tab.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""Device Settings on the §5.5 SettingsList: group, RO, choices, live values."""
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
from cim_suite.core.ui.kit.settings_list import CHOICES_ROLE, GROUP_ROLE, RO_ROLE
|
||||
from cim_suite.modules.iomodbus.domain.controller import ModbusController
|
||||
from cim_suite.modules.iomodbus.transport.simulator import SimulatedModbusBus
|
||||
from cim_suite.modules.iomodbus.ui.main_window import MainWindow
|
||||
|
||||
|
||||
def _select(catalog, desc="CS-05 (new)"):
|
||||
sim = SimulatedModbusBus(catalog)
|
||||
ctrl = ModbusController(catalog)
|
||||
ctrl.attach(sim)
|
||||
ctrl.start()
|
||||
ctrl.scan(1, 2)
|
||||
for _ in range(40):
|
||||
ctrl._tick()
|
||||
dev = next(d for d in ctrl.devices if d.description == desc)
|
||||
ctrl.select_device(dev)
|
||||
for _ in range(300):
|
||||
ctrl._tick()
|
||||
return ctrl
|
||||
|
||||
|
||||
def test_group_header_is_the_device_description(qtbot, catalog):
|
||||
ctrl = _select(catalog)
|
||||
win = MainWindow(ctrl)
|
||||
qtbot.addWidget(win)
|
||||
t = win.settings_tab.settings_list.table
|
||||
assert t.item(0, 0).data(GROUP_ROLE)
|
||||
assert t.item(0, 0).text() == "CS-05 (new)"
|
||||
|
||||
|
||||
def test_read_only_register_renders_ro(qtbot, catalog):
|
||||
ctrl = _select(catalog)
|
||||
win = MainWindow(ctrl)
|
||||
qtbot.addWidget(win)
|
||||
tab = win.settings_tab
|
||||
ro_row = next(
|
||||
i for i, s in enumerate(ctrl.state.settings)
|
||||
if s.cell is not None and not s.cell.spec.writable
|
||||
)
|
||||
entry = tab.settings_list._rows[str(ro_row)]
|
||||
value = tab.settings_list.table.item(entry.row, 1)
|
||||
assert value.data(RO_ROLE)
|
||||
assert not (value.flags() & Qt.ItemFlag.ItemIsEditable)
|
||||
|
||||
|
||||
def test_edit_writes_through_write_cell(qtbot, catalog):
|
||||
ctrl = _select(catalog)
|
||||
win = MainWindow(ctrl)
|
||||
qtbot.addWidget(win)
|
||||
tab = win.settings_tab
|
||||
addr_row = next(i for i, s in enumerate(ctrl.state.settings) if s.label == "Address")
|
||||
entry = tab.settings_list._rows[str(addr_row)]
|
||||
tab.settings_list.table.item(entry.row, 1).setText("9") # itemChanged → settingEdited
|
||||
for _ in range(300):
|
||||
ctrl._tick()
|
||||
assert ctrl.state.settings[addr_row].cell.value == "9"
|
||||
|
||||
|
||||
def test_picklist_register_is_a_choice_row(qtbot, catalog):
|
||||
ctrl = _select(catalog)
|
||||
win = MainWindow(ctrl)
|
||||
qtbot.addWidget(win)
|
||||
tab = win.settings_tab
|
||||
# CS-05 (new) carries writable pick-list registers (Baud Rate, Power Mode).
|
||||
pick_rows = [
|
||||
i for i, s in enumerate(ctrl.state.settings)
|
||||
if s.cell is not None and s.cell.spec.writable and s.cell.spec.picklist
|
||||
]
|
||||
assert pick_rows, "CS-05 (new) should expose writable pick-list settings"
|
||||
entry = tab.settings_list._rows[str(pick_rows[0])]
|
||||
item = tab.settings_list.table.item(entry.row, 1)
|
||||
assert item.data(CHOICES_ROLE) # dropdown labels present; raw == label by design
|
||||
|
||||
|
||||
def test_live_poll_updates_flow_into_values(qtbot, catalog):
|
||||
ctrl = _select(catalog)
|
||||
win = MainWindow(ctrl)
|
||||
qtbot.addWidget(win)
|
||||
tab = win.settings_tab
|
||||
addr_row = next(i for i, s in enumerate(ctrl.state.settings) if s.label == "Address")
|
||||
ctrl.cellUpdated.emit(0, addr_row, 1, "42") # GRID_SETTINGS == 0
|
||||
assert tab.settings_list.value(str(addr_row)) == "42"
|
||||
|
||||
|
||||
def test_export_sheet_snapshots_all_rows(qtbot, catalog):
|
||||
ctrl = _select(catalog)
|
||||
win = MainWindow(ctrl)
|
||||
qtbot.addWidget(win)
|
||||
sheet = win.settings_tab.export_sheet()
|
||||
assert sheet.headers == ["Property", "Value"]
|
||||
assert len(sheet.rows) == len(ctrl.state.settings) + 1 # + group header row
|
||||
@@ -66,15 +66,16 @@ def test_select_populates_channels_grid(qtbot, catalog):
|
||||
assert tab.table.item(0, value_col).text() == "20.0"
|
||||
|
||||
|
||||
def test_settings_grid_editable_writes(qtbot, catalog):
|
||||
def test_settings_list_editable_writes(qtbot, catalog):
|
||||
ctrl, _ = _wired(catalog)
|
||||
win = MainWindow(ctrl)
|
||||
qtbot.addWidget(win)
|
||||
_scan_select(ctrl)
|
||||
tab = win.settings_tab
|
||||
# Find the "Address" row (writable) and edit it.
|
||||
# Find the "Address" row (writable) and edit it via the settings list.
|
||||
addr_row = next(i for i, s in enumerate(ctrl.state.settings) if s.label == "Address")
|
||||
tab.table.item(addr_row, 1).setText("9")
|
||||
entry = tab.settings_list._rows[str(addr_row)]
|
||||
tab.settings_list.table.item(entry.row, 1).setText("9")
|
||||
_pump(ctrl, 300)
|
||||
assert ctrl.state.settings[addr_row].cell.value == "9"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user