"""Paso 7: topología AR-NMEA-IO + asignación automática de I/O. Genera tarjetas + bindings físicos de cada sensor declarado en EquipmentModel.default_sensors. El integrador puede ajustar después en el editor de topología (Sprint 3+). """ from __future__ import annotations from PySide6.QtCore import Property, Signal from PySide6.QtWidgets import ( QHeaderView, QLabel, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget, QWizardPage, ) from vmssailor.core.equipment import Equipment from vmssailor.library import load_library from vmssailor.studio.designer.port_auto_assigner import ( AssignmentReport, auto_assign, ) from vmssailor.studio.designer.rule_engine import EquipmentProposal from vmssailor.studio.theme import C_FOG, mono_font, ui_font class Step07Topology(QWizardPage): """Vista previa de la topología auto-asignada.""" def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) self.setTitle("Paso 7 · Topología AR-NMEA-IO") self.setSubTitle( "Asignación automática de tarjetas y canales físicos. " "Capacidad por tarjeta: 10 DO · 5 DI · 1 RPM · 4 AI." ) layout = QVBoxLayout(self) layout.setSpacing(10) self._summary = QLabel("") self._summary.setFont(ui_font(11)) layout.addWidget(self._summary) self._table = QTableWidget(0, 4) self._table.setHorizontalHeaderLabels( ["Tarjeta", "Slot · Addr", "DO/DI/AI/RPM usados", "Ubicación"] ) self._table.horizontalHeader().setSectionResizeMode( 3, QHeaderView.ResizeMode.Stretch ) self._table.verticalHeader().setVisible(False) layout.addWidget(self._table, 1) self._warnings = QLabel("") self._warnings.setWordWrap(True) self._warnings.setFont(mono_font(9)) self._warnings.setStyleSheet(f"color: {C_FOG};") layout.addWidget(self._warnings) self._assignment: AssignmentReport | None = None from vmssailor.studio.wizard.wizard import F_ASSIGNMENT self.registerField(F_ASSIGNMENT, self, "assignment", "assignmentChanged") # Property API assignmentChanged = Signal() def get_assignment(self) -> AssignmentReport | None: return self._assignment def set_assignment(self, value: AssignmentReport) -> None: self._assignment = value self.assignmentChanged.emit() assignment = Property( object, fget=get_assignment, fset=set_assignment, notify=assignmentChanged ) def initializePage(self) -> None: from vmssailor.studio.wizard.wizard import F_PROPOSALS proposals: list[EquipmentProposal] = self.field(F_PROPOSALS) or [] if not proposals: self._summary.setText("Sin equipos. Vuelve al paso 5.") self._table.setRowCount(0) return # Materialize Equipment objects so we can run the assigner equipment_list: list[Equipment] = [] for idx, p in enumerate(proposals): from vmssailor.core.coords import ShipCoord equipment_list.append( Equipment( id=f"eq_{idx:03d}_{p.tag_prefix.lower()}", model_ref=p.model_ref, tag_prefix=p.tag_prefix, display_name=p.display_name, location=ShipCoord( x_pp=p.location_x_pp, y_cl=p.location_y_cl, z_bl=p.location_z_bl, ), system_id=p.system_id, ) ) # Cargar modelos de biblioteca try: lib = load_library() model_lookup = {m.id: m for m in lib.equipment_models} except Exception: model_lookup = {} report = auto_assign(equipment_list, model_lookup) self._assignment = report self._summary.setText( f"{len(report.cards)} tarjetas · " f"{len(report.tags)} tags asignados · " f"{report.n_skipped} sin tipo de señal · " f"bus: {report.bus.id if report.bus else '—'}" ) self._table.setRowCount(len(report.cards)) capacities: dict[str, dict[str, int]] = {} for t in report.tags: if t.physical_binding: cid = t.physical_binding.card_id ch = t.physical_binding.channel_type.value.upper() capacities.setdefault(cid, {"DO": 0, "DI": 0, "AI": 0, "RPM": 0}) capacities[cid][ch] = capacities[cid].get(ch, 0) + 1 for row, c in enumerate(report.cards): self._table.setItem(row, 0, QTableWidgetItem(c.id)) self._table.setItem(row, 1, QTableWidgetItem(f"slot {c.slot_number} · addr {c.modbus_address}")) cap = capacities.get(c.id, {}) usage = f"DO {cap.get('DO', 0)}/10 · DI {cap.get('DI', 0)}/5 · AI {cap.get('AI', 0)}/4 · RPM {cap.get('RPM', 0)}/1" self._table.setItem(row, 2, QTableWidgetItem(usage)) self._table.setItem(row, 3, QTableWidgetItem(c.physical_location)) self._table.resizeColumnsToContents() if report.warnings: self._warnings.setText("\n".join(report.warnings[:6])) else: self._warnings.setText("✓ Sin conflictos de capacidad detectados.") self.assignmentChanged.emit()