""" LowLevelSelectorWidget (refactored) ----------------------------------- Версия, использующая VariableTableWidget вместо самодельной таблицы selected_vars_table. Ключевые изменения: * Вместо QTableWidget с 6 колонками теперь встраивается VariableTableWidget (8 колонок: №, En, Name, Origin Type, Base Type, IQ Type, Return Type, Short Name). * Логика sync <-> self._all_available_vars перенесена в _on_var_table_changed() и _pull_from_var_table(). * Поддержка политики хранения типов: - ptr_type: строковое имя (без префикса `pt_`). - ptr_type_enum: числовой индекс (см. PT_ENUM_ORDER). - Для совместимости с VariableTableWidget: поле `pt_type` = 'pt_'. - IQ / Return: аналогично (`iq_type` / `iq_type_enum`, `return_type` / `return_type_enum`). * Функции получения выбранных переменных теперь читают данные из VariableTableWidget. * Убраны неиспользуемые методы, связанные с прежней таблицей (комбо‑боксы и т.п.). Как интегрировать: 1. Поместите этот файл рядом с module VariableTableWidget (см. импорт ниже). Если класс VariableTableWidget находится в том же файле — удалите строку импорта и используйте напрямую. 2. Убедитесь, что VariablesXML предоставляет методы get_all_vars_data() (list[dict]) и, при наличии, get_struct_map() -> dict[type_name -> dict[field_name -> field_type]]. Если такого метода нет, передаём пустой {} и автодополнение по структурам будет недоступно. 3. Отметьте переменные в VariableSelectorDialog (как и раньше) — он обновит self._all_available_vars. После закрытия диалога вызывается self._populate_var_table(). 4. Для чтения выбранных переменных используйте get_selected_variables_and_addresses(); она вернёт список словарей в унифицированном формате. Примечание о совместимости: VariableTableWidget работает с ключами `pt_type`, `iq_type`, `return_type` (строки с префиксами). Мы поддерживаем дублирование этих полей с «новыми» полями без префикса и enum‑значениями. """ from __future__ import annotations import sys import re import datetime from dataclasses import dataclass, field from typing import List, Dict, Optional, Tuple, Any from PySide2 import QtCore, QtGui from PySide2.QtWidgets import ( QWidget, QVBoxLayout, QPushButton, QLabel, QHBoxLayout, QFileDialog, QMessageBox, QMainWindow, QApplication, QSizePolicy, QSpinBox, QGroupBox, QSplitter, QFormLayout ) # Локальные импорты from path_hints import PathHints from generate_debug_vars import choose_type_map, type_map from var_selector_window import VariableSelectorDialog from allvars_xml_parser import VariablesXML # Импортируем готовую таблицу # ЗАМЕТКА: замените на реальное имя файла/модуля, если отличается. from var_table import VariableTableWidget, rows as VT_ROWS # noqa: F401 # ------------------------------------------------------------ Enumerations -- # Порядок фиксируем на основании предыдущей версии. При необходимости расширьте. PT_ENUM_ORDER = [ 'unknown','int8','int16','int32','int64', 'uint8','uint16','uint32','uint64','float', 'struct','union' ] IQ_ENUM_ORDER = [ 'iq_none','iq','iq1','iq2','iq3','iq4','iq5','iq6', 'iq7','iq8','iq9','iq10','iq11','iq12','iq13','iq14', 'iq15','iq16','iq17','iq18','iq19','iq20','iq21','iq22', 'iq23','iq24','iq25','iq26','iq27','iq28','iq29','iq30' ] PT_ENUM_VALUE: Dict[str, int] = {name: idx for idx, name in enumerate(PT_ENUM_ORDER)} IQ_ENUM_VALUE: Dict[str, int] = {name: idx for idx, name in enumerate(IQ_ENUM_ORDER)} PT_ENUM_NAME_FROM_VAL: Dict[int, str] = {v: k for k, v in PT_ENUM_VALUE.items()} IQ_ENUM_NAME_FROM_VAL: Dict[int, str] = {v: k for k, v in IQ_ENUM_VALUE.items()} # ------------------------------------------- Address / validation helpers -- HEX_ADDR_MASK = QtCore.QRegExp(r"0x[0-9A-Fa-f]{0,6}") class HexAddrValidator(QtGui.QRegExpValidator): def __init__(self, parent=None): super().__init__(HEX_ADDR_MASK, parent) @staticmethod def normalize(text: str) -> str: if not text: return '0x000000' try: val = int(text,16) except ValueError: return '0x000000' return f"0x{val & 0xFFFFFF:06X}" class LowLevelSelectorWidget(QWidget): variablePrepared = QtCore.Signal(dict) xmlLoaded = QtCore.Signal(str) def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle('LowLevel Variable Selector') self._xml: Optional[VariablesXML] = None self._paths: List[str] = [] self._path_info: Dict[str, Tuple[int, str]] = {} self._addr_index: Dict[int, Optional[str]] = {} self._hints = PathHints() self._all_available_vars: List[Dict[str, Any]] = [] self.dt = None self.flat_vars = None # --- NEW --- self.btn_read_once = QPushButton("Read Once") self.btn_start_polling = QPushButton("Start Polling") self.spin_interval = QSpinBox() self.spin_interval.setRange(50, 10000) self.spin_interval.setValue(500) self.spin_interval.setSuffix(" ms") self._build_ui() self._connect() def _build_ui(self): tab = QWidget() main_layout = QVBoxLayout(tab) # --- Variable Selector --- g_selector = QGroupBox("Variable Selector") selector_layout = QVBoxLayout(g_selector) form_selector = QFormLayout() # --- XML File chooser --- file_layout = QHBoxLayout() self.btn_load = QPushButton('Load XML...') self.lbl_file = QLabel('') self.lbl_file.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) file_layout.addWidget(self.btn_load) file_layout.addWidget(self.lbl_file, 1) form_selector.addRow("XML File:", file_layout) # --- Interval SpinBox --- self.spin_interval = QSpinBox() self.spin_interval.setRange(50, 10000) self.spin_interval.setValue(500) self.spin_interval.setSuffix(" ms") form_selector.addRow("Interval:", self.spin_interval) selector_layout.addLayout(form_selector) # --- Buttons --- self.btn_read_once = QPushButton("Read Once") self.btn_start_polling = QPushButton("Start Polling") btn_layout = QHBoxLayout() btn_layout.addWidget(self.btn_read_once) btn_layout.addWidget(self.btn_start_polling) selector_layout.addLayout(btn_layout) # --- Table --- g_table = QGroupBox("Table") table_layout = QVBoxLayout(g_table) self.btn_open_var_selector = QPushButton("Выбрать переменные...") table_layout.addWidget(self.btn_open_var_selector) self.var_table = VariableTableWidget(self, show_value_instead_of_shortname=1) self.var_table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) table_layout.addWidget(self.var_table) # --- Timestamp (moved here) --- self.lbl_timestamp = QLabel('Timestamp: -') table_layout.addWidget(self.lbl_timestamp) # --- Splitter (Selector + Table) --- v_split = QSplitter(QtCore.Qt.Vertical) v_split.addWidget(g_selector) v_split.addWidget(g_table) v_split.setStretchFactor(0, 1) v_split.setStretchFactor(1, 3) main_layout.addWidget(v_split) self.setLayout(main_layout) def _connect(self): self.btn_load.clicked.connect(self._on_load_xml) self.btn_open_var_selector.clicked.connect(self._on_open_variable_selector) # ------------------------------------------------------ XML loading ---- def _on_load_xml(self): path, _ = QFileDialog.getOpenFileName( self, 'Select variables XML', '', 'XML Files (*.xml);;All Files (*)') if not path: return try: self._xml = VariablesXML(path) self.flat_vars = {v['name']: v for v in self._xml.flattened()} # Получаем сырые данные по переменным self._all_available_vars = self._xml.get_all_vars_data() except Exception as e: QMessageBox.critical(self, 'Parse error', f'Ошибка парсинга:\n{e}') return self.lbl_file.setText(path) self.lbl_timestamp.setText(f'Timestamp: {self._xml.timestamp or "-"}') self._populate_internal_maps_from_all_vars() self._apply_timestamp_to_date() self.xmlLoaded.emit(path) self._log(f'Loaded {path}, variables={len(self._all_available_vars)})') def _apply_timestamp_to_date(self): if not (self._xml and self._xml.timestamp): return try: # Пример: "Sat Jul 19 15:27:59 2025" self.dt = datetime.datetime.strptime(self._xml.timestamp, "%a %b %d %H:%M:%S %Y") except Exception as e: print(f"Ошибка разбора timestamp '{self._xml.timestamp}': {e}") # ------------------------------------------ Variable selector dialog ---- def _on_open_variable_selector(self): if not self._xml: QMessageBox.warning(self, 'No XML', 'Сначала загрузите XML файл.') return dialog = VariableSelectorDialog( table=None, # не используем встроенную таблицу all_vars=self._all_available_vars, structs=None, # при необходимости подайте реальные структуры из XML typedefs=None, # ... xml_path=None, # по запросу пользователя xml_path = None parent=self ) if dialog.exec_() == dialog.Accepted: # Диалог обновил self._all_available_vars напрямую self._populate_internal_maps_from_all_vars() self._populate_var_table() self._log("Variable selection updated.") # ----------------------------------------------------- Populate table ---- def _populate_var_table(self): """Отобразить переменные (show_var == 'true') в VariableTableWidget.""" if not self._all_available_vars: self.var_table.setRowCount(0) return # Нормализуем все записи перед передачей таблице. for var in self._all_available_vars: self._normalize_var_record(var) # Карта структур для автодополнения (если VariablesXML предоставляет) try: structs_map = self._xml.get_struct_map() if self._xml else {} except AttributeError: structs_map = {} # populate() принимает: (vars_list, structs, on_change_callback) self.var_table.populate(self._all_available_vars, structs_map, self._on_var_table_changed) # -------------------------------------------------- Table change slot ---- def _on_var_table_changed(self, *args, **kwargs): # noqa: D401 (неиспользуемые) """Вызывается при любом изменении в VariableTableWidget. Читаем данные из таблицы, мержим в self._all_available_vars (по имени), пересобираем служебные индексы. """ updated = self.var_table.read_data() # list[dict] # создаём индекс по имени из master списка idx_by_name = {v.get('name'): v for v in self._all_available_vars if v.get('name')} for rec in updated: nm = rec.get('name') if not nm: continue dst = idx_by_name.get(nm) if not dst: # Новая запись; добавляем базовые поля dst = { 'name': nm, 'address': 0, 'file': '', 'extern': 'false', 'static': 'false', } self._all_available_vars.append(dst) idx_by_name[nm] = dst # перенести видимые поля dst['show_var'] = str(bool(rec.get('show_var'))).lower() dst['enable'] = str(bool(rec.get('enable'))).lower() dst['shortname']= rec.get('shortname', nm) dst['type'] = rec.get('type', dst.get('type','')) # типы (строковые, с префиксами) -> нормализуем pt_pref = rec.get('pt_type','pt_unknown') # 'pt_int16' iq_pref = rec.get('iq_type','t_iq_none') # 't_iq10' etc. rt_pref = rec.get('return_type', iq_pref) self._assign_types_from_prefixed(dst, pt_pref, iq_pref, rt_pref) # Пересобрать карты путей/адресов self._populate_internal_maps_from_all_vars() # --------------------------------- Normalize var record (public-ish) ---- def _normalize_var_record(self, var: Dict[str, Any]): """Унифицирует записи переменной. Требуемые поля после нормализации: var['ptr_type'] -> str (напр. 'int16') var['ptr_type_enum'] -> int var['iq_type'] -> str ('iq10') var['iq_type_enum'] -> int var['return_type'] -> str ('iq10') var['return_type_enum']-> int var['pt_type'] -> 'pt_' (для совместимости с VariableTableWidget) var['return_type_pref']-> 't_' (см. ниже) # не обяз. Дополнительно корректируем show_var/enable и адрес. """ # --- show_var / enable var['show_var'] = str(var.get('show_var', 'false')).lower() var['enable'] = str(var.get('enable', 'true')).lower() # --- address if not var.get('address'): var_name = var.get('name') # Ищем в self.flat_vars if hasattr(self, 'flat_vars') and isinstance(self.flat_vars, dict): flat_entry = self.flat_vars.get(var_name) if flat_entry and 'address' in flat_entry: var['address'] = flat_entry['address'] else: var['address'] = 0 else: var['address'] = 0 else: # Нормализация адреса (если строка типа '0x1234') try: if isinstance(var['address'], str): var['address'] = int(var['address'], 16) except ValueError: var['address'] = 0 # --- ptr_type (строка) name = None if isinstance(var.get('ptr_type'), str): name = var['ptr_type'] elif isinstance(var.get('ptr_type_name'), str): name = var['ptr_type_name'] elif isinstance(var.get('pt_type'), str): name = var['pt_type'].replace('pt_','') elif isinstance(var.get('ptr_type'), int): name = PT_ENUM_NAME_FROM_VAL.get(var['ptr_type'], 'unknown') else: name = self._map_type_to_ptr_enum(var.get('type')) val = PT_ENUM_VALUE.get(name, 0) var['ptr_type'] = name var['ptr_type_enum'] = val var['pt_type'] = f'pt_{name}' # ---------------------------------------------- prefixed assign helper ---- def _assign_types_from_prefixed(self, dst: Dict[str, Any], pt_pref: str, iq_pref: str, rt_pref: str): """Парсит строки вида 'pt_int16', 't_iq10' и записывает нормализованные поля.""" pt_name = pt_pref.replace('pt_','') if pt_pref else 'unknown' iq_name = iq_pref if iq_name.startswith('t_'): iq_name = iq_name[2:] rt_name = rt_pref if rt_name.startswith('t_'): rt_name = rt_name[2:] dst['ptr_type'] = pt_name dst['ptr_type_enum'] = PT_ENUM_VALUE.get(pt_name, 0) dst['pt_type'] = f'pt_{pt_name}' dst['iq_type'] = iq_name dst['iq_type_enum'] = IQ_ENUM_VALUE.get(iq_name, 0) dst['return_type'] = rt_name dst['return_type_enum'] = IQ_ENUM_VALUE.get(rt_name, dst['iq_type_enum']) dst['return_type_pref'] = f't_{rt_name}' # ------------------------------------------ Populate internal maps ---- def _populate_internal_maps_from_all_vars(self): self._path_info.clear() self._addr_index.clear() self._paths.clear() for var in self._all_available_vars: nm = var.get('name') tp = var.get('type') addr = var.get('address') if nm is None: continue if addr is None: addr = 0 var['address'] = 0 self._paths.append(nm) self._path_info[nm] = (addr, tp) if addr in self._addr_index: self._addr_index[addr] = None else: self._addr_index[addr] = nm # Обновим подсказки self._hints.set_paths([(p, self._path_info[p][1]) for p in self._paths]) # -------------------------------------------------- Public helpers ---- def get_selected_variables_and_addresses(self) -> List[Dict[str, Any]]: """Возвращает список выбранных переменных (show_var == true) с адресами и типами. Чтение из VariableTableWidget + подстановка адресов/прочих служебных полей из master списка. """ tbl_data = self.var_table.read_data() # список dict'ов в формате VariableTableWidget idx_by_name = {v.get('name'): v for v in self._all_available_vars if v.get('name')} out: List[Dict[str, Any]] = [] for rec in tbl_data: nm = rec.get('name') if not nm: continue src = idx_by_name.get(nm, {}) addr = src.get('address') if addr is None or addr == '' or addr == 0: src['address'] = self.flat_vars.get(nm, {}).get('address', 0) else: # если это строка "0x..." — конвертируем в int if isinstance(addr, str) and addr.startswith('0x'): try: src['address'] = int(addr, 16) except ValueError: src['address'] = self.flat_vars.get(nm, {}).get('address', 0) type_str = src.get('type', rec.get('type','N/A')) # нормализация типов tmp = dict(src) # copy src to preserve extra fields (file, extern, ...) self._assign_types_from_prefixed(tmp, rec.get('pt_type','pt_unknown'), rec.get('iq_type','t_iq_none'), rec.get('return_type', rec.get('iq_type','t_iq_none'))) tmp['show_var'] = str(bool(rec.get('show_var'))).lower() tmp['enable'] = str(bool(rec.get('enable'))).lower() tmp['name'] = nm tmp['address'] = addr tmp['type'] = type_str out.append(tmp) return out def get_datetime(self): return self.dt def set_variable_value(self, var_name: str, value: Any): # 1. Обновляем master-список переменных found = None for var in self._all_available_vars: if var.get('name') == var_name: var['value'] = value found = var break if not found: # Если переменной нет в списке, можно либо проигнорировать, либо добавить. return False # 2. Обновляем отображение в таблице self.var_table.populate(self._all_available_vars, {}, self._on_var_table_changed) return True # --------------- Address mapping / type mapping helpers --------------- def _map_type_to_ptr_enum(self, type_str: Optional[str]) -> str: if not type_str: return 'unknown' low = type_str.lower() token = low.replace('*',' ').replace('[',' ') return type_map.get(token, 'unknown').replace('pt_','') # ----------------------------------------------------------- Logging -- def _log(self, msg: str): print(f"[LowLevelSelectorWidget Log] {msg}") # --------------------------------------------------------------------------- # Тест‑прогоночка (ручной) -------------------------------------------------- # Запускать только вручную: python LowLevelSelectorWidget_refactored.py # --------------------------------------------------------------------------- # ----------------------------------------------------------- Demo window -- class _DemoWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle('LowLevel Selector Demo') self.selector = LowLevelSelectorWidget(self) self.setCentralWidget(self.selector) self.selector.variablePrepared.connect(self.on_var) def on_var(self, data: dict): print('Variable prepared ->', data) def closeEvent(self, ev): self.setCentralWidget(None) super().closeEvent(ev) # ----------------------------------------------------------------- main --- if __name__ == '__main__': app = QApplication(sys.argv) w = _DemoWindow() w.resize(640, 520) w.show() sys.exit(app.exec_())