базово: +сделан lowlevel для кучи переменных (пока работает медленно) +сделан сохранение принимаемых значений в лог + gui терминалок подогнаны под один стиль плюс минус
505 lines
22 KiB
Python
505 lines
22 KiB
Python
"""
|
||
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_<name>'.
|
||
- 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('<no file>')
|
||
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_<ptr_type>' (для совместимости с VariableTableWidget)
|
||
var['return_type_pref']-> 't_<return_type>' (см. ниже) # не обяз.
|
||
|
||
Дополнительно корректируем 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 <xml>
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# ----------------------------------------------------------- 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_())
|