debugVarTool/Src/tms_debugvar_lowlevel.py
Razvalyaev 96496a0256 все неплохо работает.
сейв перед попыткой улучшить lowlevel debug
2025-07-21 13:40:52 +03:00

791 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
LowLevelSelectorWidget (PySide2)
--------------------------------
Виджет для:
* Выбора XML файла с описанием переменных (как в примере пользователя)
* Парсинга всех <variable> и их вложенных <member>
* Построения плоского списка путей (имя/подпуть) с расчётом абсолютного адреса (base_address + offset)
* Определения структур с полями даты (year, month, day, hour, minute)
* Выбора переменной и (опционально) переменной даты / ручного ввода даты
* Выбора типов: ptr_type , iq_type, return_type
* Форматирования адреса в виде 0x000000 (6 HEX)
* Генерации словаря/кадра для последующей LowLevel-команды (не отправляет сам)
Интеграция:
* Подключите сигнал variablePrepared(dict) к функции, формирующей и отправляющей пакет.
* Содержимое dict:
{
'address': int,
'address_hex': str, # '0x....'
'ptr_type': int, # значение enum *
'iq_type': int,
'return_type': int,
'datetime': {
'year': int,
'month': int,
'day': int,
'hour': int,
'minute': int,
},
'path': str, # полный путь переменной
'type_string': str, # строка типа из XML
}
Зависимости: только PySide2 и стандартная библиотека.
"""
from __future__ import annotations
import sys
import xml.etree.ElementTree as ET
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
from PySide2 import QtCore, QtGui, QtWidgets
from path_hints import PathHints
from generate_debug_vars import choose_type_map, type_map
# ------------------------------------------------------------ Enumerations --
# Сопоставление строк из XML типу ptr_type (адаптируйте под реальный проект)
PTR_TYPE_MAP = type_map
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'
]
# Для примера: маппинг имени enum -> числовое значение (индекс по порядку)
PT_ENUM_VALUE = {name: idx for idx, name in enumerate(PT_ENUM_ORDER)}
IQ_ENUM_VALUE = {name: idx for idx, name in enumerate(IQ_ENUM_ORDER)}
# -------------------------------------------------------------- Data types --
DATE_FIELD_SET = {'year','month','day','hour','minute'}
@dataclass
class MemberNode:
name: str
offset: int = 0
type_str: str = ''
size: Optional[int] = None
children: List['MemberNode'] = field(default_factory=list)
# --- новые, но необязательные (совместимость) ---
kind: Optional[str] = None # 'array', 'union', ...
count: Optional[int] = None # size1 (число элементов в массиве)
def is_date_struct(self) -> bool:
if not self.children:
return False
child_names = {c.name for c in self.children}
return DATE_FIELD_SET.issubset(child_names)
@dataclass
class VariableNode:
name: str
address: int
type_str: str
size: Optional[int]
members: List[MemberNode] = field(default_factory=list)
# --- новые, но необязательные ---
kind: Optional[str] = None # 'array'
count: Optional[int] = None # size1
def base_address_hex(self) -> str:
return f"0x{self.address:06X}"
# --------------------------- XML Parser ----------------------------
class VariablesXML:
"""
Читает твой XML и выдаёт плоский список путей:
- Массивы -> name[i], многоуровневые -> name[i][j]
- Указатель на структуру -> дети через '->'
- Обычная структура -> дети через '.'
"""
# предположительные размеры примитивов (под STM/MCU: int=2)
_PRIM_SIZE = {
'char':1, 'signed char':1, 'unsigned char':1, 'uint8_t':1, 'int8_t':1,
'short':2, 'short int':2, 'signed short':2, 'unsigned short':2,
'uint16_t':2, 'int16_t':2,
'int':2, 'signed int':2, 'unsigned int':2,
'long':4, 'unsigned long':4, 'int32_t':4, 'uint32_t':4,
'float':4,
'long long':8, 'unsigned long long':8, 'int64_t':8, 'uint64_t':8, 'double':8,
}
def __init__(self, path: str):
self.path = path
self.timestamp: str = ''
self.variables: List[VariableNode] = []
choose_type_map(0)
self._parse()
# ------------------ low helpers ------------------
@staticmethod
def _parse_int_guess(txt: Optional[str]) -> Optional[int]:
if not txt:
return None
txt = txt.strip()
if txt.startswith(('0x','0X')):
return int(txt, 16)
# если в строке есть буквы A-F → возможно hex
if any(c in 'abcdefABCDEF' for c in txt):
try:
return int(txt, 16)
except ValueError:
pass
try:
return int(txt, 10)
except ValueError:
return None
@staticmethod
def _is_pointer_to_struct(t: str) -> bool:
if not t:
return False
low = t.replace('\t',' ').replace('\n',' ')
return 'struct ' in low and '*' in low
@staticmethod
def _is_struct_or_union(t: str) -> bool:
if not t:
return False
low = t.strip()
return low.startswith('struct ') or low.startswith('union ')
@staticmethod
def _strip_array_suffix(t: str) -> str:
return t[:-2].strip() if t.endswith('[]') else t
def _guess_primitive_size(self, type_str: str) -> Optional[int]:
if not type_str:
return None
base = type_str
for tok in ('volatile','const'):
base = base.replace(tok, '')
base = base.replace('*',' ')
base = base.replace('[',' ').replace(']',' ')
base = ' '.join(base.split()).strip()
return self._PRIM_SIZE.get(base)
# ------------------ XML read ------------------
def _parse(self):
tree = ET.parse(self.path)
root = tree.getroot()
ts = root.find('timestamp')
self.timestamp = ts.text.strip() if ts is not None and ts.text else ''
def parse_member(elem) -> MemberNode:
name = elem.get('name','')
offset = int(elem.get('offset','0'),16) if elem.get('offset') else 0
t = elem.get('type','') or ''
size_attr = elem.get('size')
size = int(size_attr,16) if size_attr else None
kind = elem.get('kind')
size1_attr = elem.get('size1')
count = None
if size1_attr:
count = self._parse_int_guess(size1_attr)
node = MemberNode(name=name, offset=offset, type_str=t, size=size,
kind=kind, count=count)
for ch in elem.findall('member'):
node.children.append(parse_member(ch))
return node
for var in root.findall('variable'):
addr = int(var.get('address','0'),16)
name = var.get('name','')
t = var.get('type','') or ''
size_attr = var.get('size')
size = int(size_attr,16) if size_attr else None
kind = var.get('kind')
size1_attr = var.get('size1')
count = None
if size1_attr:
count = self._parse_int_guess(size1_attr)
members = [parse_member(m) for m in var.findall('member')]
self.variables.append(
VariableNode(name=name, address=addr, type_str=t, size=size,
members=members, kind=kind, count=count)
)
# ------------------ flatten (expanded) ------------------
def flattened(self,
max_array_elems: Optional[int] = None
) -> List[Tuple[str,int,str]]:
"""
Вернёт [(path, addr, type_str), ...].
max_array_elems: ограничить разворачивание больших массивов (None = все).
"""
out: List[Tuple[str,int,str]] = []
def add(path: str, addr: int, t: str):
out.append((path, addr, t))
def compute_stride(size_bytes: Optional[int],
count: Optional[int],
base_type: Optional[str],
node_children: Optional[List[MemberNode]]) -> int:
# 1) size_bytes/count
if size_bytes and count and count > 0:
if size_bytes % count == 0:
stride = size_bytes // count
if stride <= 0:
stride = 1
return stride
else:
# size не кратен count → скорее всего size = размер одного элемента
return max(size_bytes, 1)
# 2) попытка по типу (примитив)
if base_type:
gs = self._guess_primitive_size(base_type)
if gs:
return gs
# 3) попытка по детям (структура)
if node_children:
min_off = min(ch.offset for ch in node_children)
max_end = min_off
for ch in node_children:
sz = ch.size
if not sz:
sz = self._guess_primitive_size(ch.type_str) or 1
end = ch.offset + sz
if end > max_end:
max_end = end
stride = max_end - min_off
if stride > 0:
return stride
return 1
def expand_members(prefix_name: str,
base_addr: int,
members: List[MemberNode],
parent_is_ptr_struct: bool):
"""
Разворачиваем список members относительно базового адреса.
parent_is_ptr_struct: если True, то соединение '->' иначе '.'
"""
join = '->' if parent_is_ptr_struct else '.'
for m in members:
path_m = f"{prefix_name}{join}{m.name}" if prefix_name else m.name
addr_m = base_addr + m.offset
add(path_m, addr_m, m.type_str)
# массив?
if (m.kind == 'array') or m.type_str.endswith('[]'):
count = m.count
if count is None:
count = 0 # неизвестно → не разворачиваем
if count <= 0:
continue
base_t = self._strip_array_suffix(m.type_str)
stride = compute_stride(m.size, count, base_t, m.children if m.children else None)
limit = count if max_array_elems is None else min(count, max_array_elems)
for i in range(limit):
path_i = f"{path_m}[{i}]"
addr_i = addr_m + i*stride
add(path_i, addr_i, base_t)
# элемент массива: если структура / union → раскроем поля
if m.children and self._is_struct_or_union(base_t):
expand_members(path_i, addr_i, m.children, parent_is_ptr_struct=False)
# элемент массива: если указатель на структуру
elif self._is_pointer_to_struct(base_t):
# у таких обычно нет children в XML, но если есть — используем
expand_members(path_i, addr_i, m.children, parent_is_ptr_struct=True)
continue
# не массив
if m.children:
is_ptr_struct = self._is_pointer_to_struct(m.type_str)
expand_members(path_m, addr_m, m.children, parent_is_ptr_struct=is_ptr_struct)
# --- top-level ---
for v in self.variables:
add(v.name, v.address, v.type_str)
# top-level массив?
if (v.kind == 'array') or v.type_str.endswith('[]'):
count = v.count
if count is None:
count = 0
if count > 0:
base_t = self._strip_array_suffix(v.type_str)
stride = compute_stride(v.size, count, base_t, v.members if v.members else None)
limit = count if max_array_elems is None else min(count, max_array_elems)
for i in range(limit):
p = f"{v.name}[{i}]"
a = v.address + i*stride
add(p, a, base_t)
# массив структур?
if v.members and self._is_struct_or_union(base_t):
expand_members(p, a, v.members, parent_is_ptr_struct=False)
# массив указателей на структуры?
elif self._is_pointer_to_struct(base_t):
expand_members(p, a, v.members, parent_is_ptr_struct=True)
continue # к след. переменной
# top-level не массив
if v.members:
is_ptr_struct = self._is_pointer_to_struct(v.type_str)
expand_members(v.name, v.address, v.members, parent_is_ptr_struct=is_ptr_struct)
return out
# -------------------- date candidates (как было) --------------------
def date_struct_candidates(self) -> List[Tuple[str,int]]:
cands = []
for v in self.variables:
# верхний уровень (если есть все поля даты)
direct_names = {mm.name for mm in v.members}
if DATE_FIELD_SET.issubset(direct_names):
cands.append((v.name, v.address))
# проверка членов первого уровня
for m in v.members:
if m.is_date_struct():
cands.append((f"{v.name}.{m.name}", v.address + m.offset))
return cands
# ------------------------------------------- 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}"
# --------------------------------------------------------- Main Widget ----
class LowLevelSelectorWidget(QtWidgets.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 = []
self._path_info = {}
self._addr_index = {}
self._backspace_pressed = False
self._hints = PathHints()
self._build_ui()
self._connect()
def _build_ui(self):
lay = QtWidgets.QVBoxLayout(self)
# --- File chooser ---
file_box = QtWidgets.QHBoxLayout()
self.btn_load = QtWidgets.QPushButton('Load XML...')
self.lbl_file = QtWidgets.QLabel('<no file>')
self.lbl_file.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
file_box.addWidget(self.btn_load)
file_box.addWidget(self.lbl_file, 1)
lay.addLayout(file_box)
self.lbl_timestamp = QtWidgets.QLabel('Timestamp: -')
lay.addWidget(self.lbl_timestamp)
form = QtWidgets.QFormLayout()
# --- Search field for variable ---
self.edit_var_search = QtWidgets.QLineEdit()
self.edit_var_search.setPlaceholderText("Введите имя переменной или адрес 0x......")
form.addRow('Variable:', self.edit_var_search)
# Popup list
self._popup = QtWidgets.QListView()
self._popup.setWindowFlags(QtCore.Qt.ToolTip)
self._popup.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self._popup.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self._popup.clicked.connect(self._on_popup_clicked)
self._model_all = QtGui.QStandardItemModel(self)
self._model_filtered = QtGui.QStandardItemModel(self)
# Address
self.edit_address = QtWidgets.QLineEdit('0x000000')
self.edit_address.setValidator(HexAddrValidator(self))
self.edit_address.setMaximumWidth(120)
form.addRow('Address:', self.edit_address)
# Manual date spins
dt_row = QtWidgets.QHBoxLayout()
self.spin_year = QtWidgets.QSpinBox(); self.spin_year.setRange(2000, 2100); self.spin_year.setValue(2025)
self.spin_month = QtWidgets.QSpinBox(); self.spin_month.setRange(1,12)
self.spin_day = QtWidgets.QSpinBox(); self.spin_day.setRange(1,31)
self.spin_hour = QtWidgets.QSpinBox(); self.spin_hour.setRange(0,23)
self.spin_minute = QtWidgets.QSpinBox(); self.spin_minute.setRange(0,59)
for w,label in [(self.spin_year,'Y'),(self.spin_month,'M'),(self.spin_day,'D'),(self.spin_hour,'h'),(self.spin_minute,'m')]:
box = QtWidgets.QVBoxLayout()
box.addWidget(QtWidgets.QLabel(label, alignment=QtCore.Qt.AlignHCenter))
box.addWidget(w)
dt_row.addLayout(box)
form.addRow('Compile Date:', dt_row)
# Types
self.cmb_ptr_type = QtWidgets.QComboBox(); self.cmb_ptr_type.addItems(PT_ENUM_ORDER)
self.cmb_iq_type = QtWidgets.QComboBox(); self.cmb_iq_type.addItems(IQ_ENUM_ORDER)
self.cmb_return_type = QtWidgets.QComboBox(); self.cmb_return_type.addItems(IQ_ENUM_ORDER)
form.addRow('Type:', self.cmb_ptr_type)
form.addRow('IQ Type:', self.cmb_iq_type)
form.addRow('Return IQ Type:', self.cmb_return_type)
lay.addLayout(form)
self.btn_prepare = QtWidgets.QPushButton('Prepare Variable')
lay.addWidget(self.btn_prepare)
lay.addStretch(1)
self.txt_info = QtWidgets.QPlainTextEdit()
self.txt_info.setReadOnly(True)
self.txt_info.setMaximumHeight(140)
lay.addWidget(QtWidgets.QLabel('Info:'))
lay.addWidget(self.txt_info)
# Event filter for keyboard on search field
self.edit_var_search.installEventFilter(self)
def _connect(self):
self.btn_load.clicked.connect(self._on_load_xml)
self.edit_address.editingFinished.connect(self._normalize_address)
self.btn_prepare.clicked.connect(self._emit_variable)
self.edit_var_search.textEdited.connect(self._on_var_search_edited)
self.edit_var_search.returnPressed.connect(self._activate_current_popup_selection)
QtWidgets.QApplication.instance().focusChanged.connect(self._on_focus_changed)
def focusOutEvent(self, event):
super().focusOutEvent(event)
self._hide_popup()
def _on_focus_changed(self, old, now):
# Если фокус ушёл из нашего виджета — скрываем подсказки
if now is None or not self.isAncestorOf(now):
self._hide_popup()
# ---------------- XML Load ----------------
def _on_load_xml(self):
path, _ = QtWidgets.QFileDialog.getOpenFileName(
self, 'Select variables XML', '', 'XML Files (*.xml);;All Files (*)')
if not path:
return
try:
self._xml = VariablesXML(path)
except Exception as e:
QtWidgets.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_variables()
self._apply_timestamp_to_date()
self.xmlLoaded.emit(path)
self._log(f'Loaded {path}, variables={len(self._xml.variables)}')
def _apply_timestamp_to_date(self):
if not self._xml.timestamp:
return
import datetime
try:
# Пример: "Sat Jul 19 15:27:59 2025"
dt = datetime.datetime.strptime(self._xml.timestamp, "%a %b %d %H:%M:%S %Y")
self.spin_year.setValue(dt.year)
self.spin_month.setValue(dt.month)
self.spin_day.setValue(dt.day)
self.spin_hour.setValue(dt.hour)
self.spin_minute.setValue(dt.minute)
except Exception as e:
print(f"Ошибка разбора timestamp '{self._xml.timestamp}': {e}")
def _populate_variables(self):
if not self._xml:
return
flat = self._xml.flattened()
# flat: [(path, addr, type_str), ...]
self._paths = []
self._path_info = {}
self._addr_index = {}
self._model_all.clear() # держим «сырой» полный список (можно не показывать)
self._model_filtered.clear() # текущие подсказки
# индексирование
for path, addr, t in flat:
self._paths.append(path)
self._path_info[path] = (addr, t)
if addr in self._addr_index:
self._addr_index[addr] = None
else:
self._addr_index[addr] = path
# наполняем «all» модель (необязательная, но пусть остаётся — не используем напрямую)
it = QtGui.QStandardItem(f"{path} [{addr:06X}]")
it.setData(path, QtCore.Qt.UserRole+1)
it.setData(addr, QtCore.Qt.UserRole+2)
it.setData(t, QtCore.Qt.UserRole+3)
self._model_all.appendRow(it)
# построить подсказки
self._hints.set_paths([(p, self._path_info[p][1]) for p in self._paths])
# начальное состояние попапа (пустой ввод → top-level)
self._update_popup_model(self._hints.suggest(''))
self._log(f"Variables loaded: {len(flat)}")
# --------------- Search mechanics ---------------
def _update_popup_model(self, paths: List[str]):
"""Обновляет модель попапа списком путей (full paths)."""
self._model_filtered.clear()
limit = 400
added = 0
for p in paths:
info = self._path_info.get(str(p))
if not info:
continue
addr, t = info
it = QtGui.QStandardItem(f"{p} [{addr:06X}]")
it.setData(p, QtCore.Qt.UserRole+1)
it.setData(addr, QtCore.Qt.UserRole+2)
it.setData(t, QtCore.Qt.UserRole+3)
self._model_filtered.appendRow(it)
added += 1
if added >= limit:
break
if added >= limit:
extra = QtGui.QStandardItem("... (more results truncated)")
extra.setEnabled(False)
self._model_filtered.appendRow(extra)
def _show_popup(self):
if self._model_filtered.rowCount() == 0:
self._popup.hide()
return
self._popup.setModel(self._model_filtered)
self._popup.setMinimumWidth(self.edit_var_search.width())
pos = self.edit_var_search.mapToGlobal(QtCore.QPoint(0, self.edit_var_search.height()))
self._popup.move(pos)
self._popup.show()
self._popup.raise_()
self._popup.setFocus()
self._popup.setCurrentIndex(self._model_filtered.index(0,0))
def _hide_popup(self):
self._popup.hide()
def _on_var_search_edited(self, text: str):
# Показываем подсказки при вводе текста, если не было Backspace (чтобы не добавлять разделитель)
t = text.strip()
if self._backspace_pressed:
# При стирании не показываем автодополнение с разделителем
self._backspace_pressed = False
self._hide_popup()
return
# адрес?
if t.startswith("0x") and len(t) >= 3:
try:
addr = int(t, 16)
path = self._addr_index.get(addr)
if path:
self._set_current_variable(path, from_address=True)
self._hide_popup()
return
except ValueError:
pass
# подсказки по имени
suggestions = self._hints.suggest(t)
self._update_popup_model(suggestions)
self._show_popup()
def _on_popup_clicked(self, idx: QtCore.QModelIndex):
if not idx.isValid():
return
path = idx.data(QtCore.Qt.UserRole + 1)
if not path:
return
# Реализуем автодополнение по цепочке, пока подсказка имеет детей
current_path = path
children = self._hints.get_children(str(current_path))
if children:
current_path = self._hints.add_separator(current_path)
self._set_current_variable(str(current_path)) # <-- здесь
if not children:
self._hide_popup()
def _activate_current_popup_selection(self):
if self._popup.isVisible():
idx = self._popup.currentIndex()
if idx.isValid():
self._on_popup_clicked(idx)
return
# Попытка прямого совпадения
path = self.edit_var_search.text().strip()
if path in self._path_info:
self._set_current_variable(path)
def eventFilter(self, obj, ev):
if obj is self.edit_var_search and ev.type() == QtCore.QEvent.KeyPress:
key = ev.key()
if key in (QtCore.Qt.Key_Down, QtCore.Qt.Key_Up):
if not self._popup.isVisible():
self._show_popup()
else:
step = 1 if key == QtCore.Qt.Key_Down else -1
cur = self._popup.currentIndex()
row = cur.row() + step
if row < 0: row = 0
if row >= self._model_filtered.rowCount():
row = self._model_filtered.rowCount() - 1
self._popup.setCurrentIndex(self._model_filtered.index(row, 0))
return True
elif key == QtCore.Qt.Key_Escape:
self._hide_popup()
return True
elif key == QtCore.Qt.Key_Backspace:
# Помечаем, что была нажата Backspace
self._backspace_pressed = True
return False # дальше обрабатываем обычным образом
elif key == QtCore.Qt.Key_Space and (ev.modifiers() & QtCore.Qt.ControlModifier):
# Ctrl+Space — показать подсказки
text = self.edit_var_search.text()
suggestions = self._hints.suggest(text)
self._update_popup_model(suggestions)
self._show_popup()
return True
else:
# Любая другая клавиша — сбрасываем Backspace-флаг
self._backspace_pressed = False
return super().eventFilter(obj, ev)
def _set_current_variable(self, path: str, from_address=False):
self.edit_var_search.setText(path)
self._update_popup_model(self._hints.suggest(path))
self._show_popup()
if path not in self._path_info:
return
addr, type_str = self._path_info[path]
# Разделитель добавляем только если не стираем (Backspace), и если уже не добавлен
# В этой функции не будем добавлять разделитель, добавляем его только при автодополнении выше
self.edit_address.setText(f"0x{addr:06X}")
ptr_enum_name = self._map_type_to_ptr_enum(type_str)
self._select_combo_text(self.cmb_ptr_type, ptr_enum_name)
source = "ADDR" if from_address else "SEARCH"
self._log(f"[{source}] Selected {path} @0x{addr:06X} type={type_str} -> ptr={ptr_enum_name}")
# --------------- Date struct / address / helpers ---------------
def _normalize_address(self):
self.edit_address.setText(HexAddrValidator.normalize(self.edit_address.text()))
def _map_type_to_ptr_enum(self, type_str: str) -> str:
if not type_str:
return 'unknown'
low = type_str.lower()
token = low.replace('*',' ').replace('[',' ')
return PTR_TYPE_MAP.get(token, 'unknown')
def _select_combo_text(self, combo: QtWidgets.QComboBox, text: str):
ix = combo.findText(text.replace('pt_', ''))
if ix >= 0:
combo.setCurrentIndex(ix)
def _collect_datetime(self) -> Dict[str,int]:
return {
'year': self.spin_year.value(),
'month': self.spin_month.value(),
'day': self.spin_day.value(),
'hour': self.spin_hour.value(),
'minute': self.spin_minute.value(),
}
def _emit_variable(self):
if not self._path_info:
QtWidgets.QMessageBox.warning(self, 'No XML', 'Сначала загрузите XML файл.')
return
path = self.edit_var_search.text().strip()
if path not in self._path_info:
QtWidgets.QMessageBox.warning(self, 'Variable', 'Переменная не выбрана / не найдена.')
return
addr, type_str = self._path_info[path]
ptr_type_name = self.cmb_ptr_type.currentText()
iq_type_name = self.cmb_iq_type.currentText()
ret_type_name = self.cmb_return_type.currentText()
out = {
'address': addr,
'address_hex': f"0x{addr:06X}",
'ptr_type': PT_ENUM_VALUE.get(ptr_type_name, 0),
'iq_type': IQ_ENUM_VALUE.get(iq_type_name, 0),
'return_type': IQ_ENUM_VALUE.get(ret_type_name, 0),
'datetime': self._collect_datetime(),
'path': path,
'type_string': type_str,
'ptr_type_name': ptr_type_name,
'iq_type_name': iq_type_name,
'return_type_name': ret_type_name,
}
self._log(f"Prepared variable: {out}")
self.variablePrepared.emit(out)
def _log(self, msg: str):
self.txt_info.appendPlainText(msg)
# ----------------------------------------------------------- Demo window --
class _DemoWindow(QtWidgets.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 = QtWidgets.QApplication(sys.argv)
w = _DemoWindow()
w.resize(640, 520)
w.show()
sys.exit(app.exec_())