Compare commits
6 Commits
v1.1
...
f89aff1b1c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f89aff1b1c | ||
|
|
6830743477 | ||
|
|
171f176d63 | ||
|
|
f2c4b7b3cd | ||
|
|
c94a7e711c | ||
|
|
5be6343c33 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@
|
||||
/build_temp
|
||||
/DebugVarEdit_GUI.build
|
||||
/DebugVarEdit_GUI.dist
|
||||
/DebugVarEdit_GUI.onefile-build
|
||||
/DebugVarEdit_GUI.onefile-build
|
||||
/parse_xml/build/
|
||||
|
||||
BIN
DebugVarEdit.exe
BIN
DebugVarEdit.exe
Binary file not shown.
@@ -7,6 +7,7 @@ import subprocess
|
||||
import lxml.etree as ET
|
||||
from generate_debug_vars import type_map, choose_type_map
|
||||
from enum import IntEnum
|
||||
from tms_debugvar_term import _DemoWindow
|
||||
import threading
|
||||
from generate_debug_vars import run_generate
|
||||
import var_setup
|
||||
@@ -17,6 +18,7 @@ import scan_vars
|
||||
import myXML
|
||||
import time
|
||||
|
||||
|
||||
from PySide2.QtWidgets import (
|
||||
QApplication, QWidget, QTableWidget, QTableWidgetItem,
|
||||
QCheckBox, QComboBox, QLineEdit, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||
@@ -148,7 +150,18 @@ class VarEditor(QWidget):
|
||||
self.target_menu.addAction(self.action_tms)
|
||||
self.target_menu.addAction(self.action_stm)
|
||||
|
||||
self.terminal_menu = QMenu("Открыть Терминал", menubar)
|
||||
|
||||
self.action_terminal_tms = QAction("TMS DemoTerminal", self)
|
||||
self.action_terminal_modbus = QAction("Modbus DemoTerminal", self)
|
||||
self.action_terminal_tms.triggered.connect(lambda: self.open_terminal("TMS"))
|
||||
self.action_terminal_modbus.triggered.connect(lambda: self.open_terminal("MODBUS"))
|
||||
|
||||
self.terminal_menu.addAction(self.action_terminal_tms)
|
||||
#self.terminal_menu.addAction(self.action_terminal_modbus)
|
||||
|
||||
menubar.addMenu(self.target_menu)
|
||||
menubar.addMenu(self.terminal_menu)
|
||||
|
||||
# Кнопка сохранения
|
||||
btn_save = QPushButton(build_title)
|
||||
@@ -178,7 +191,16 @@ class VarEditor(QWidget):
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def open_terminal(self, target):
|
||||
target = target.lower()
|
||||
|
||||
if target == "tms":
|
||||
self.terminal_widget = _DemoWindow() # _DemoWindow наследует QWidget
|
||||
self.terminal_widget.show()
|
||||
elif target == "modbus":
|
||||
a=1
|
||||
|
||||
|
||||
def on_target_selected(self, target):
|
||||
self.target_menu.setTitle(f'МК: {target}')
|
||||
self.settings.setValue("mcu_choosen", target)
|
||||
|
||||
252
Src/path_hints.py
Normal file
252
Src/path_hints.py
Normal file
@@ -0,0 +1,252 @@
|
||||
# path_hints.py
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import re
|
||||
|
||||
|
||||
# ---------------------- tokenization helpers ----------------------
|
||||
|
||||
def split_path_tokens(path: str) -> List[str]:
|
||||
"""
|
||||
Разбивает строку пути на логические части:
|
||||
'foo[2].bar[1]->baz' -> ['foo', '[2]', 'bar', '[1]', 'baz']
|
||||
Аналог твоей split_path(), но оставлена как чистая функция.
|
||||
"""
|
||||
tokens: List[str] = []
|
||||
token = ''
|
||||
i = 0
|
||||
L = len(path)
|
||||
while i < L:
|
||||
c = path[i]
|
||||
# '->'
|
||||
if c == '-' and i + 1 < L and path[i:i+2] == '->':
|
||||
if token:
|
||||
tokens.append(token)
|
||||
token = ''
|
||||
i += 2
|
||||
continue
|
||||
# одиночный '-' в конце
|
||||
if c == '-' and i == L - 1:
|
||||
i += 1
|
||||
continue
|
||||
# '.'
|
||||
if c == '.':
|
||||
if token:
|
||||
tokens.append(token)
|
||||
token = ''
|
||||
i += 1
|
||||
continue
|
||||
# '[' ... ']'
|
||||
if c == '[':
|
||||
if token:
|
||||
tokens.append(token)
|
||||
token = ''
|
||||
idx = ''
|
||||
while i < L and path[i] != ']':
|
||||
idx += path[i]
|
||||
i += 1
|
||||
if i < L and path[i] == ']':
|
||||
idx += ']'
|
||||
i += 1
|
||||
tokens.append(idx)
|
||||
continue
|
||||
# обычный символ
|
||||
token += c
|
||||
i += 1
|
||||
if token:
|
||||
tokens.append(token)
|
||||
return tokens
|
||||
|
||||
|
||||
def canonical_key(path: str) -> str:
|
||||
"""
|
||||
Преобразует путь к канонической форме для индекса / поиска:
|
||||
- '->' -> '.'
|
||||
- '[' -> '.['
|
||||
- lower()
|
||||
"""
|
||||
p = path.replace('->', '.')
|
||||
p = p.replace('[', '.[')
|
||||
return p.lower()
|
||||
|
||||
|
||||
# ---------------------- индекс узлов ----------------------
|
||||
|
||||
@dataclass
|
||||
class PathNode:
|
||||
"""
|
||||
Узел в логическом дереве путей.
|
||||
Храним:
|
||||
- собственное имя (локальное, напр. 'controller' или '[3]')
|
||||
- полный путь (оригинальный, как его должен видеть пользователь)
|
||||
- тип (опционально; widget может хранить отдельно)
|
||||
- дети
|
||||
"""
|
||||
name: str
|
||||
full_path: str
|
||||
type_str: str = ''
|
||||
children: Dict[str, "PathNode"] = field(default_factory=dict)
|
||||
|
||||
def add_child(self, child: "PathNode") -> None:
|
||||
self.children[child.name] = child
|
||||
|
||||
|
||||
class PathHints:
|
||||
"""
|
||||
Движок автоподсказок / completion.
|
||||
Работает с плоским списком ПОЛНЫХ имён (как показываются пользователю).
|
||||
Сам восстанавливает иерархию и выдаёт подсказки по текущему вводу.
|
||||
Qt-независим.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._paths: List[str] = []
|
||||
self._types: Dict[str, str] = {} # full_path -> type_str (опционально)
|
||||
self._index: Dict[str, PathNode] = {} # canonical full path -> node
|
||||
self._root_children: Dict[str, PathNode] = {} # top-level по первому токену
|
||||
|
||||
# ------------ Подаём данные ------------
|
||||
def set_paths(self,
|
||||
paths: List[Tuple[str, Optional[str]]]
|
||||
) -> None:
|
||||
"""
|
||||
paths: список кортежей (full_path, type_str|None).
|
||||
Пример: ('project.controller.read.errors.bit.status_er0', 'unsigned int')
|
||||
Поля могут содержать '->' и индексы, т.е. строки в пользовательском формате.
|
||||
|
||||
NOTE: порядок не важен; дерево строится автоматически.
|
||||
"""
|
||||
self._paths = []
|
||||
self._types.clear()
|
||||
self._index.clear()
|
||||
self._root_children.clear()
|
||||
|
||||
for p, t in paths:
|
||||
if t is None:
|
||||
t = ''
|
||||
self._add_path(p, t)
|
||||
|
||||
def _add_path(self, full_path: str, type_str: str) -> None:
|
||||
self._paths.append(full_path)
|
||||
self._types[full_path] = type_str
|
||||
|
||||
toks = split_path_tokens(full_path)
|
||||
if not toks:
|
||||
return
|
||||
|
||||
cur_dict = self._root_children
|
||||
cur_full = ''
|
||||
parent_node: Optional[PathNode] = None
|
||||
|
||||
for i, tok in enumerate(toks):
|
||||
# Собираем ПОЛНЫЙ путь
|
||||
if cur_full == '':
|
||||
cur_full = tok
|
||||
else:
|
||||
if tok.startswith('['):
|
||||
cur_full += tok
|
||||
else:
|
||||
cur_full += '.' + tok
|
||||
|
||||
# Если узел уже есть
|
||||
node = cur_dict.get(tok)
|
||||
if node is None:
|
||||
# --- ВАЖНО: full_path = cur_full ---
|
||||
node = PathNode(name=tok, full_path=cur_full)
|
||||
cur_dict[tok] = node
|
||||
|
||||
# Регистрируем все узлы, включая промежуточные
|
||||
self._index[canonical_key(cur_full)] = node
|
||||
|
||||
parent_node = node
|
||||
cur_dict = node.children
|
||||
|
||||
# В последний узел добавляем тип
|
||||
if parent_node:
|
||||
parent_node.type_str = type_str
|
||||
|
||||
|
||||
# ------------ Поиск узла ------------
|
||||
|
||||
def find_node(self, path: str) -> Optional[PathNode]:
|
||||
return self._index.get(canonical_key(path))
|
||||
|
||||
# ------------ Подсказки ------------
|
||||
|
||||
def suggest(self,
|
||||
text: str,
|
||||
*,
|
||||
include_partial: bool = True
|
||||
) -> List[str]:
|
||||
"""
|
||||
Вернёт список *полных имён узлов*, подходящих под ввод.
|
||||
Правила (упрощённо, повторяя твою update_completions()):
|
||||
- Если текст пуст → top-level.
|
||||
- Если заканчивается на '.' или '->' или '[' → вернуть детей текущего узла.
|
||||
- Иначе → фильтр по последнему фрагменту (prefix substring match).
|
||||
"""
|
||||
text = text or ''
|
||||
stripped = text.strip()
|
||||
|
||||
# пусто: top-level
|
||||
if stripped == '':
|
||||
return sorted(self._root_full_names())
|
||||
|
||||
# Завершение по разделителю?
|
||||
if stripped.endswith('.') or stripped.endswith('->') or stripped.endswith('['):
|
||||
base = stripped[:-1] if stripped.endswith('[') else stripped.rstrip('.').rstrip('>').rstrip('-')
|
||||
node = self.find_node(base)
|
||||
if node:
|
||||
return self._children_full_names(node)
|
||||
# не нашли базу — ничего
|
||||
return []
|
||||
|
||||
# иначе: обычный поиск по последней части
|
||||
toks = split_path_tokens(stripped)
|
||||
prefix_last = toks[-1].lower() if toks else ''
|
||||
parent_toks = toks[:-1]
|
||||
|
||||
if not parent_toks:
|
||||
# фильтр top-level
|
||||
res = []
|
||||
for name, node in self._root_children.items():
|
||||
if prefix_last == '' or prefix_last in name.lower():
|
||||
res.append(node.full_path)
|
||||
return sorted(res)
|
||||
|
||||
# есть родитель
|
||||
parent_path = self._join_tokens(parent_toks)
|
||||
parent_node = self.find_node(parent_path)
|
||||
if not parent_node:
|
||||
return []
|
||||
res = []
|
||||
for child in parent_node.children.values():
|
||||
if prefix_last == '' or prefix_last in child.name.lower():
|
||||
res.append(child.full_path)
|
||||
return sorted(res)
|
||||
|
||||
# ------------ внутренние вспомогательные ------------
|
||||
|
||||
def _root_full_names(self) -> List[str]:
|
||||
return [node.full_path for node in self._root_children.values()]
|
||||
|
||||
def _children_full_names(self, node: PathNode) -> List[str]:
|
||||
return [ch.full_path for ch in node.children.values()]
|
||||
|
||||
@staticmethod
|
||||
def _join_tokens(tokens: List[str]) -> str:
|
||||
"""
|
||||
Собираем путь обратно. Для внутренних нужд (поиск), формат не критичен —
|
||||
всё равно canonical_key() нормализует.
|
||||
"""
|
||||
if not tokens:
|
||||
return ''
|
||||
out = tokens[0]
|
||||
for t in tokens[1:]:
|
||||
if t.startswith('['):
|
||||
out += t
|
||||
else:
|
||||
out += '.' + t
|
||||
return out
|
||||
739
Src/tms_debugvar_lowlevel.py
Normal file
739
Src/tms_debugvar_lowlevel.py
Normal file
@@ -0,0 +1,739 @@
|
||||
"""
|
||||
LowLevelSelectorWidget (PySide2)
|
||||
--------------------------------
|
||||
Виджет для:
|
||||
* Выбора XML файла с описанием переменных (как в примере пользователя)
|
||||
* Парсинга всех <variable> и их вложенных <member>
|
||||
* Построения плоского списка путей (имя/подпуть) с расчётом абсолютного адреса (base_address + offset)
|
||||
* Определения структур с полями даты (year, month, day, hour, minute)
|
||||
* Выбора переменной и (опционально) переменной даты / ручного ввода даты
|
||||
* Выбора типов: ptr_type (pt_*), iq_type, return_type
|
||||
* Форматирования адреса в виде 0x000000 (6 HEX)
|
||||
* Генерации словаря/кадра для последующей LowLevel-команды (не отправляет сам)
|
||||
|
||||
Интеграция:
|
||||
* Подключите сигнал variablePrepared(dict) к функции, формирующей и отправляющей пакет.
|
||||
* Содержимое dict:
|
||||
{
|
||||
'address': int,
|
||||
'address_hex': str, # '0x....'
|
||||
'ptr_type': int, # значение enum pt_*
|
||||
'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
|
||||
|
||||
# ------------------------------------------------------------ Enumerations --
|
||||
# Сопоставление строк из XML типу ptr_type (адаптируйте под реальный проект)
|
||||
PTR_TYPE_MAP = {
|
||||
'int8': 'pt_int8', 'signed char': 'pt_int8', 'char': 'pt_int8',
|
||||
'int16': 'pt_int16', 'short': 'pt_int16', 'int': 'pt_int16',
|
||||
'int32': 'pt_int32', 'long': 'pt_int32',
|
||||
'int64': 'pt_int64', 'long long': 'pt_int64',
|
||||
'uint8': 'pt_uint8', 'unsigned char': 'pt_uint8',
|
||||
'uint16': 'pt_uint16', 'unsigned short': 'pt_uint16', 'unsigned int': 'pt_uint16',
|
||||
'uint32': 'pt_uint32', 'unsigned long': 'pt_uint32',
|
||||
'uint64': 'pt_uint64', 'unsigned long long': 'pt_uint64',
|
||||
'float': 'pt_float', 'floatf': 'pt_float',
|
||||
'struct': 'pt_struct', 'union': 'pt_union',
|
||||
}
|
||||
|
||||
PT_ENUM_ORDER = [
|
||||
'pt_unknown','pt_int8','pt_int16','pt_int32','pt_int64',
|
||||
'pt_uint8','pt_uint16','pt_uint32','pt_uint64','pt_float',
|
||||
'pt_struct','pt_union'
|
||||
]
|
||||
|
||||
IQ_ENUM_ORDER = [
|
||||
't_iq_none','t_iq','t_iq1','t_iq2','t_iq3','t_iq4','t_iq5','t_iq6',
|
||||
't_iq7','t_iq8','t_iq9','t_iq10','t_iq11','t_iq12','t_iq13','t_iq14',
|
||||
't_iq15','t_iq16','t_iq17','t_iq18','t_iq19','t_iq20','t_iq21','t_iq22',
|
||||
't_iq23','t_iq24','t_iq25','t_iq26','t_iq27','t_iq28','t_iq29','t_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] = []
|
||||
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/count
|
||||
if size_bytes and count and count > 0:
|
||||
stride = size_bytes // count
|
||||
if stride * count != size_bytes:
|
||||
# округлённо вверх
|
||||
stride = (size_bytes + count - 1) // count
|
||||
if stride <= 0:
|
||||
stride = 1
|
||||
return stride
|
||||
# 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._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('Manual 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('ptr_type:', self.cmb_ptr_type)
|
||||
form.addRow('iq_type:', self.cmb_iq_type)
|
||||
form.addRow('return_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)
|
||||
|
||||
# ---------------- 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(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):
|
||||
t = text.strip()
|
||||
|
||||
# адрес?
|
||||
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 path:
|
||||
self._set_current_variable(path)
|
||||
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:
|
||||
if ev.key() in (QtCore.Qt.Key_Down, QtCore.Qt.Key_Up):
|
||||
if not self._popup.isVisible():
|
||||
self._show_popup()
|
||||
else:
|
||||
step = 1 if ev.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 ev.key() == QtCore.Qt.Key_Escape:
|
||||
self._hide_popup()
|
||||
return True
|
||||
return super().eventFilter(obj, ev)
|
||||
|
||||
def _set_current_variable(self, path: str, from_address=False):
|
||||
if path not in self._path_info:
|
||||
return
|
||||
addr, type_str = self._path_info[path]
|
||||
self.edit_var_search.setText(path)
|
||||
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 'pt_unknown'
|
||||
low = type_str.lower()
|
||||
token = low.replace('*',' ').replace('[',' ').split()[0]
|
||||
return PTR_TYPE_MAP.get(token, 'pt_unknown')
|
||||
|
||||
def _select_combo_text(self, combo: QtWidgets.QComboBox, text: str):
|
||||
ix = combo.findText(text)
|
||||
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_())
|
||||
970
Src/tms_debugvar_term.py
Normal file
970
Src/tms_debugvar_term.py
Normal file
@@ -0,0 +1,970 @@
|
||||
from PySide2 import QtCore, QtWidgets, QtSerialPort
|
||||
from tms_debugvar_lowlevel import LowLevelSelectorWidget
|
||||
import datetime
|
||||
|
||||
# ------------------------------- Константы протокола ------------------------
|
||||
WATCH_SERVICE_BIT = 0x8000
|
||||
DEBUG_OK = 0 # ожидаемый код успешного чтения
|
||||
SIGN_BIT_MASK = 0x80
|
||||
FRAC_MASK_FULL = 0x7F # если используем 7 бит дробной части
|
||||
# ---------------------------------------------------------------- CRC util ---
|
||||
def crc16_ibm(data: bytes, *, init=0xFFFF) -> int:
|
||||
"""CRC16-IBM (aka CRC-16/ANSI, polynomial 0xA001 reflected)."""
|
||||
crc = init
|
||||
for b in data:
|
||||
crc ^= b
|
||||
for _ in range(8):
|
||||
if crc & 1:
|
||||
crc = (crc >> 1) ^ 0xA001
|
||||
else:
|
||||
crc >>= 1
|
||||
return crc & 0xFFFF
|
||||
|
||||
class Spoiler(QtWidgets.QWidget):
|
||||
def __init__(self, title="", animationDuration=300, parent=None):
|
||||
super().__init__(parent)
|
||||
self._animationDuration = animationDuration
|
||||
self.state = False
|
||||
|
||||
# --- Toggle button ---
|
||||
self.toggleButton = QtWidgets.QToolButton(self)
|
||||
self.toggleButton.setStyleSheet("QToolButton { border: none; }")
|
||||
self.toggleButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
|
||||
self.toggleButton.setArrowType(QtCore.Qt.RightArrow)
|
||||
self.toggleButton.setText(title)
|
||||
self.toggleButton.setCheckable(True)
|
||||
|
||||
# --- Header line ---
|
||||
self.headerLine = QtWidgets.QFrame(self)
|
||||
self.headerLine.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
self.headerLine.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
|
||||
# --- Content area ---
|
||||
self.contentArea = QtWidgets.QScrollArea(self)
|
||||
self.contentArea.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
self.contentArea.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||
self.contentArea.setWidgetResizable(True)
|
||||
self._contentWidget = QtWidgets.QWidget()
|
||||
self.contentArea.setWidget(self._contentWidget)
|
||||
self.contentArea.setMaximumHeight(0)
|
||||
|
||||
# --- Анимация только по контенту ---
|
||||
self._ani_content = QtCore.QPropertyAnimation(self.contentArea, b"maximumHeight")
|
||||
self._ani_content.setDuration(animationDuration)
|
||||
self._ani_content.setEasingCurve(QtCore.QEasingCurve.InOutCubic)
|
||||
|
||||
# Следим за шагами анимации → обновляем родителя
|
||||
self._ani_content.valueChanged.connect(self._adjust_parent_size)
|
||||
|
||||
# --- Layout ---
|
||||
self.mainLayout = QtWidgets.QGridLayout(self)
|
||||
self.mainLayout.setVerticalSpacing(0)
|
||||
self.mainLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.mainLayout.addWidget(self.toggleButton, 0, 0, 1, 1)
|
||||
self.mainLayout.addWidget(self.headerLine, 0, 1, 1, 1)
|
||||
self.mainLayout.addWidget(self.contentArea, 1, 0, 1, 2)
|
||||
|
||||
# --- Signals ---
|
||||
self.toggleButton.clicked.connect(self._on_toggled)
|
||||
|
||||
def setContentLayout(self, contentLayout):
|
||||
old = self._contentWidget.layout()
|
||||
if old:
|
||||
QtWidgets.QWidget().setLayout(old)
|
||||
self._contentWidget.setLayout(contentLayout)
|
||||
|
||||
def getState(self):
|
||||
return self.state
|
||||
|
||||
def _adjust_parent_size(self, *_):
|
||||
top = self.window()
|
||||
if top:
|
||||
size = top.size()
|
||||
size.setHeight(top.sizeHint().height()) # берём новую высоту
|
||||
top.resize(size) # ширина остаётся прежней
|
||||
|
||||
def _on_toggled(self, checked: bool):
|
||||
self.state = checked
|
||||
self.toggleButton.setArrowType(QtCore.Qt.DownArrow if checked else QtCore.Qt.RightArrow)
|
||||
|
||||
contentHeight = self._contentWidget.sizeHint().height()
|
||||
self._ani_content.stop()
|
||||
self._ani_content.setStartValue(self.contentArea.maximumHeight())
|
||||
self._ani_content.setEndValue(contentHeight if checked else 0)
|
||||
|
||||
# --- Фиксируем ширину на время анимации ---
|
||||
w = self.width()
|
||||
self.setFixedWidth(w)
|
||||
self._ani_content.finished.connect(lambda: self.setMaximumWidth(16777215)) # сброс фикса
|
||||
|
||||
self._ani_content.start()
|
||||
|
||||
|
||||
# --------------------------- DebugTerminalWidget ---------------------------
|
||||
class DebugTerminalWidget(QtWidgets.QWidget):
|
||||
# Существующие сигналы (Watch)
|
||||
nameRead = QtCore.Signal(int, int, int, str)
|
||||
valueRead = QtCore.Signal(int, int, int, int, float)
|
||||
valuesRead = QtCore.Signal(int, int, list, list, list, list)
|
||||
# Новые сигналы (LowLevel)
|
||||
llValueRead = QtCore.Signal(int, int, int, int, float) # addr, status, rettype_raw, raw16_signed, scaled
|
||||
|
||||
portOpened = QtCore.Signal(str)
|
||||
portClosed = QtCore.Signal(str)
|
||||
txBytes = QtCore.Signal(bytes)
|
||||
rxBytes = QtCore.Signal(bytes)
|
||||
|
||||
def __init__(self, parent=None, *,
|
||||
start_byte=0x0A,
|
||||
cmd_byte=0x46,
|
||||
cmd_lowlevel=0x47,
|
||||
iq_scaling=None,
|
||||
read_timeout_ms=250,
|
||||
auto_crc_check=True,
|
||||
drop_if_busy=False,
|
||||
replace_if_busy=True):
|
||||
super().__init__(parent)
|
||||
self.device_addr = start_byte
|
||||
self.cmd_byte = cmd_byte
|
||||
self.cmd_lowlevel = cmd_lowlevel
|
||||
self.read_timeout_ms = read_timeout_ms
|
||||
self.auto_crc_check = auto_crc_check
|
||||
self._drop_if_busy = drop_if_busy
|
||||
self._replace_if_busy = replace_if_busy
|
||||
|
||||
if iq_scaling is None:
|
||||
iq_scaling = {n: float(1 << n) for n in range(31)}
|
||||
iq_scaling[0] = 1.0
|
||||
self.iq_scaling = iq_scaling
|
||||
|
||||
# Serial
|
||||
self.serial = QtSerialPort.QSerialPort(self)
|
||||
self.serial.setBaudRate(115200)
|
||||
self.serial.readyRead.connect(self._on_ready_read)
|
||||
self.serial.errorOccurred.connect(self._on_serial_error)
|
||||
|
||||
# State
|
||||
self._rx_buf = bytearray()
|
||||
self._busy = False
|
||||
self._pending_cmd = None # (frame, meta)
|
||||
self._txn_meta = None # {'service':bool,'index':int,'varqnt':int,'chain':...,'lowlevel':bool}
|
||||
|
||||
self._txn_timer = QtCore.QTimer(self)
|
||||
self._txn_timer.setSingleShot(True)
|
||||
self._txn_timer.timeout.connect(self._on_txn_timeout)
|
||||
|
||||
# Watch polling
|
||||
self._poll_timer = QtCore.QTimer(self)
|
||||
self._poll_timer.timeout.connect(self._on_poll_timeout)
|
||||
self._polling = False
|
||||
|
||||
# LowLevel polling
|
||||
self._ll_poll_timer = QtCore.QTimer(self)
|
||||
self._ll_poll_timer.timeout.connect(self._on_ll_poll_timeout)
|
||||
self._ll_polling = False
|
||||
self._ll_current_var_info = None # Хранит инфо о выбранной LL переменной
|
||||
|
||||
# Кэш: index -> (status, iq, name, is_signed, frac_bits)
|
||||
self._name_cache = {}
|
||||
|
||||
# Очередь service индексов
|
||||
self._service_queue = []
|
||||
self._pending_data_after_services = None # (base, count)
|
||||
|
||||
self._build_ui()
|
||||
self._connect_ui()
|
||||
self.set_available_ports()
|
||||
|
||||
# ------------------------------ UI ----------------------------------
|
||||
def _build_ui(self):
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
|
||||
# --- Serial group ---
|
||||
g_serial = QtWidgets.QGroupBox("Serial Port")
|
||||
hs = QtWidgets.QHBoxLayout(g_serial)
|
||||
self.cmb_port = QtWidgets.QComboBox()
|
||||
self.btn_refresh = QtWidgets.QPushButton("Refresh")
|
||||
self.cmb_baud = QtWidgets.QComboBox()
|
||||
self.cmb_baud.addItems(["9600","19200","38400","57600","115200","230400"])
|
||||
self.cmb_baud.setCurrentText("115200")
|
||||
self.btn_open = QtWidgets.QPushButton("Open")
|
||||
hs.addWidget(QtWidgets.QLabel("Port:"))
|
||||
hs.addWidget(self.cmb_port, 1)
|
||||
hs.addWidget(self.btn_refresh)
|
||||
hs.addSpacing(10)
|
||||
hs.addWidget(QtWidgets.QLabel("Baud:"))
|
||||
hs.addWidget(self.cmb_baud)
|
||||
hs.addWidget(self.btn_open)
|
||||
|
||||
# --- TabWidget ---
|
||||
self.tabs = QtWidgets.QTabWidget()
|
||||
self._build_watch_tab()
|
||||
self._build_lowlevel_tab() # <-- Вызываем новый метод
|
||||
|
||||
# --- UART Log ---
|
||||
self.log_spoiler = Spoiler("UART Log", animationDuration=300, parent=self)
|
||||
self.log_spoiler.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
|
||||
QtWidgets.QSizePolicy.Minimum)
|
||||
log_layout = QtWidgets.QVBoxLayout()
|
||||
self.txt_log = QtWidgets.QTextEdit(); self.txt_log.setReadOnly(True)
|
||||
self.txt_log.setFontFamily("Courier")
|
||||
log_layout.addWidget(self.txt_log)
|
||||
self.log_spoiler.setContentLayout(log_layout)
|
||||
|
||||
layout.addWidget(g_serial, 0)
|
||||
layout.addWidget(self.tabs, 1)
|
||||
layout.addWidget(self.log_spoiler, 0)
|
||||
layout.setStretch(layout.indexOf(g_serial), 0)
|
||||
layout.setStretch(layout.indexOf(self.tabs), 1)
|
||||
layout.setStretch(layout.indexOf(self.log_spoiler), 0)
|
||||
|
||||
def _build_watch_tab(self):
|
||||
# ... (код для вкладки Watch остаётся без изменений)
|
||||
tab = QtWidgets.QWidget()
|
||||
vtab = QtWidgets.QVBoxLayout(tab)
|
||||
|
||||
g_watch = QtWidgets.QGroupBox("Watch Variables")
|
||||
grid = QtWidgets.QGridLayout(g_watch)
|
||||
grid.setHorizontalSpacing(8)
|
||||
grid.setVerticalSpacing(4)
|
||||
|
||||
self.spin_index = QtWidgets.QSpinBox(); self.spin_index.setRange(0, 0x7FFF); self.spin_index.setAccelerated(True)
|
||||
self.chk_hex_index = QtWidgets.QCheckBox("Hex")
|
||||
self.spin_count = QtWidgets.QSpinBox(); self.spin_count.setRange(1,255); self.spin_count.setValue(1)
|
||||
self.btn_read_service = QtWidgets.QPushButton("Read Name/Type")
|
||||
self.btn_read_values = QtWidgets.QPushButton("Read Value(s)")
|
||||
self.btn_poll = QtWidgets.QPushButton("Start Polling")
|
||||
self.spin_interval = QtWidgets.QSpinBox(); self.spin_interval.setRange(50,10000); self.spin_interval.setValue(500); self.spin_interval.setSuffix(" ms")
|
||||
self.chk_auto_service = QtWidgets.QCheckBox("Auto service before values if miss cache"); self.chk_auto_service.setChecked(True)
|
||||
self.chk_raw = QtWidgets.QCheckBox("Raw (no IQ scaling)")
|
||||
self.lbl_name = QtWidgets.QLineEdit(); self.lbl_name.setReadOnly(True)
|
||||
self.lbl_iq = QtWidgets.QLabel("-")
|
||||
self.edit_single_value = QtWidgets.QLineEdit(); self.edit_single_value.setReadOnly(True)
|
||||
|
||||
self.tbl_values = QtWidgets.QTableWidget(0, 5)
|
||||
self.tbl_values.setHorizontalHeaderLabels(["Index","Name","IQ","Raw","Scaled"])
|
||||
hh = self.tbl_values.horizontalHeader()
|
||||
hh.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
|
||||
hh.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
|
||||
hh.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
|
||||
hh.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
|
||||
hh.setSectionResizeMode(4, QtWidgets.QHeaderView.Stretch)
|
||||
self.tbl_values.verticalHeader().setVisible(False)
|
||||
|
||||
r = 0
|
||||
grid.addWidget(QtWidgets.QLabel("Base Index:"), r, 0); grid.addWidget(self.spin_index, r, 1); grid.addWidget(self.chk_hex_index, r, 2); r+=1
|
||||
grid.addWidget(QtWidgets.QLabel("Count:"), r, 0); grid.addWidget(self.spin_count, r, 1); r+=1
|
||||
grid.addWidget(self.btn_read_service, r, 0); grid.addWidget(self.btn_read_values, r, 1); grid.addWidget(self.btn_poll, r, 2); r+=1
|
||||
grid.addWidget(QtWidgets.QLabel("Interval:"), r, 0); grid.addWidget(self.spin_interval, r, 1); grid.addWidget(self.chk_auto_service, r, 2); r+=1
|
||||
grid.addWidget(QtWidgets.QLabel("Name:"), r, 0); grid.addWidget(self.lbl_name, r, 1, 1, 2); r+=1
|
||||
grid.addWidget(QtWidgets.QLabel("IQ:"), r, 0); grid.addWidget(self.lbl_iq, r, 1); grid.addWidget(self.chk_raw, r, 2); r+=1
|
||||
grid.addWidget(QtWidgets.QLabel("Single:"), r, 0); grid.addWidget(self.edit_single_value, r, 1, 1, 2); r+=1
|
||||
grid.addWidget(QtWidgets.QLabel("Array Values:"), r, 0); r+=1
|
||||
grid.addWidget(self.tbl_values, r, 0, 1, 3); grid.setRowStretch(r, 1)
|
||||
|
||||
vtab.addWidget(g_watch, 1)
|
||||
self.tabs.addTab(tab, "Watch")
|
||||
|
||||
def _build_lowlevel_tab(self):
|
||||
tab = QtWidgets.QWidget()
|
||||
main_layout = QtWidgets.QVBoxLayout(tab)
|
||||
|
||||
# --- Селектор переменной ---
|
||||
self.ll_selector = LowLevelSelectorWidget(tab)
|
||||
main_layout.addWidget(self.ll_selector, 1) # Даём ему растягиваться
|
||||
|
||||
# --- Панель управления и статуса ---
|
||||
g_controls = QtWidgets.QGroupBox("Controls & Status")
|
||||
grid = QtWidgets.QGridLayout(g_controls)
|
||||
|
||||
self.btn_ll_read = QtWidgets.QPushButton("Read Once")
|
||||
self.btn_ll_poll = QtWidgets.QPushButton("Start Polling")
|
||||
self.spin_ll_interval = QtWidgets.QSpinBox()
|
||||
self.spin_ll_interval.setRange(50, 10000)
|
||||
self.spin_ll_interval.setValue(500)
|
||||
self.spin_ll_interval.setSuffix(" ms")
|
||||
self.chk_ll_raw = QtWidgets.QCheckBox("Raw (no IQ scaling)")
|
||||
|
||||
self.ll_val_status = QtWidgets.QLabel("-")
|
||||
self.ll_val_rettype = QtWidgets.QLabel("-")
|
||||
self.ll_val_raw = QtWidgets.QLabel("-")
|
||||
self.ll_val_scaled = QtWidgets.QLabel("-")
|
||||
|
||||
# Размещение виджетов
|
||||
grid.addWidget(self.btn_ll_read, 0, 0)
|
||||
grid.addWidget(self.btn_ll_poll, 0, 1)
|
||||
grid.addWidget(QtWidgets.QLabel("Interval:"), 1, 0)
|
||||
grid.addWidget(self.spin_ll_interval, 1, 1)
|
||||
grid.addWidget(self.chk_ll_raw, 0, 2, 2, 1) # Растянем на 2 строки
|
||||
|
||||
# Результаты в виде формы
|
||||
form_layout = QtWidgets.QFormLayout()
|
||||
form_layout.addRow("Status:", self.ll_val_status)
|
||||
form_layout.addRow("Return Type:", self.ll_val_rettype)
|
||||
form_layout.addRow("Raw Value:", self.ll_val_raw)
|
||||
form_layout.addRow("Scaled Value:", self.ll_val_scaled)
|
||||
|
||||
grid.addLayout(form_layout, 2, 0, 1, 3)
|
||||
grid.setColumnStretch(2, 1)
|
||||
|
||||
main_layout.addWidget(g_controls)
|
||||
main_layout.setStretchFactor(g_controls, 0) # Не растягивать
|
||||
|
||||
self.tabs.addTab(tab, "LowLevel")
|
||||
|
||||
def _connect_ui(self):
|
||||
# Watch
|
||||
self.btn_refresh.clicked.connect(self.set_available_ports)
|
||||
self.btn_open.clicked.connect(self._open_close_port)
|
||||
self.btn_read_service.clicked.connect(self.request_service_single)
|
||||
self.btn_read_values.clicked.connect(self.request_values)
|
||||
self.btn_poll.clicked.connect(self._toggle_polling)
|
||||
self.chk_hex_index.stateChanged.connect(self._toggle_index_base)
|
||||
|
||||
# LowLevel (новые и переделанные)
|
||||
self.ll_selector.variablePrepared.connect(self._on_ll_variable_prepared)
|
||||
self.ll_selector.xmlLoaded.connect(lambda p: self._log(f"[LL] XML loaded: {p}"))
|
||||
self.btn_ll_read.clicked.connect(self.request_lowlevel_once)
|
||||
self.btn_ll_poll.clicked.connect(self._toggle_ll_polling)
|
||||
|
||||
# ----------------------------- SERIAL MGMT ----------------------------
|
||||
# ... (код без изменений)
|
||||
def set_available_ports(self):
|
||||
cur = self.cmb_port.currentText()
|
||||
self.cmb_port.blockSignals(True)
|
||||
self.cmb_port.clear()
|
||||
for info in QtSerialPort.QSerialPortInfo.availablePorts():
|
||||
self.cmb_port.addItem(info.portName())
|
||||
if cur:
|
||||
ix = self.cmb_port.findText(cur)
|
||||
if ix >= 0:
|
||||
self.cmb_port.setCurrentIndex(ix)
|
||||
self.cmb_port.blockSignals(False)
|
||||
|
||||
def _open_close_port(self):
|
||||
if self.serial.isOpen():
|
||||
name = self.serial.portName()
|
||||
self.serial.close()
|
||||
self.btn_open.setText("Open")
|
||||
self._log(f"[PORT] Closed {name}")
|
||||
self.portClosed.emit(name)
|
||||
return
|
||||
port = self.cmb_port.currentText()
|
||||
if not port:
|
||||
self._log("[ERR] No port selected")
|
||||
return
|
||||
self.serial.setPortName(port)
|
||||
self.serial.setBaudRate(int(self.cmb_baud.currentText()))
|
||||
if not self.serial.open(QtCore.QIODevice.ReadWrite):
|
||||
self._log(f"[ERR] Open fail {port}: {self.serial.errorString()}")
|
||||
return
|
||||
self.btn_open.setText("Close")
|
||||
self._log(f"[PORT] Opened {port}")
|
||||
self.portOpened.emit(port)
|
||||
|
||||
# ---------------------------- FRAME BUILD -----------------------------
|
||||
def _build_request(self, index: int, *, service: bool, varqnt: int) -> bytes:
|
||||
dbg = index & 0x7FFF
|
||||
if service:
|
||||
dbg |= WATCH_SERVICE_BIT
|
||||
hi = (dbg >> 8) & 0xFF
|
||||
lo = dbg & 0xFF
|
||||
q = varqnt & 0xFF
|
||||
payload = bytes([self.device_addr & 0xFF, self.cmd_byte & 0xFF, hi, lo, q])
|
||||
crc = crc16_ibm(payload)
|
||||
return payload + bytes([crc & 0xFF, (crc >> 8) & 0xFF])
|
||||
|
||||
def _build_lowlevel_request(self, var_info: dict) -> bytes:
|
||||
# Формат: [adr][cmd_lowlevel][year_hi][year_lo][month][day][hour][minute][addr2][addr1][addr0][pt_type][iq_type][return_type]
|
||||
# Пытаемся получить время из переданной информации
|
||||
dt_info = var_info.get('datetime')
|
||||
|
||||
if dt_info:
|
||||
# Используем время из var_info
|
||||
year = dt_info.get('year', 2000)
|
||||
month = dt_info.get('month', 1)
|
||||
day = dt_info.get('day', 1)
|
||||
hour = dt_info.get('hour', 0)
|
||||
minute = dt_info.get('minute', 0)
|
||||
self._log("[LL] Using time from selector.")
|
||||
else:
|
||||
# Если в var_info времени нет, используем текущее системное время (старое поведение)
|
||||
now = QtCore.QDateTime.currentDateTime()
|
||||
year = now.date().year()
|
||||
month = now.date().month()
|
||||
day = now.date().day()
|
||||
hour = now.time().hour()
|
||||
minute = now.time().minute()
|
||||
self._log("[LL] Fallback to current system time.")
|
||||
|
||||
addr = var_info.get('address', 0)
|
||||
addr2 = (addr >> 16) & 0xFF
|
||||
addr1 = (addr >> 8) & 0xFF
|
||||
addr0 = addr & 0xFF
|
||||
pt_type = var_info.get('ptr_type', 0) & 0xFF
|
||||
iq_type = var_info.get('iq_type', 0) & 0xFF
|
||||
ret_type = var_info.get('return_type', 0) & 0xFF
|
||||
|
||||
frame_wo_crc = bytes([
|
||||
self.device_addr & 0xFF, self.cmd_lowlevel & 0xFF,
|
||||
(year >> 8) & 0xFF, year & 0xFF,
|
||||
month & 0xFF, day & 0xFF, hour & 0xFF, minute & 0xFF,
|
||||
addr2, addr1, addr0, pt_type, iq_type, ret_type
|
||||
])
|
||||
crc = crc16_ibm(frame_wo_crc)
|
||||
return frame_wo_crc + bytes([crc & 0xFF, (crc >> 8) & 0xFF])
|
||||
|
||||
# ----------------------------- PUBLIC API -----------------------------
|
||||
def request_service_single(self):
|
||||
idx = int(self.spin_index.value())
|
||||
self._enqueue_or_start(idx, service=True, varqnt=0)
|
||||
|
||||
def request_values(self):
|
||||
base = int(self.spin_index.value())
|
||||
count = int(self.spin_count.value())
|
||||
needed = []
|
||||
if self.chk_auto_service.isChecked():
|
||||
for i in range(base, base+count):
|
||||
if i not in self._name_cache:
|
||||
needed.append(i)
|
||||
if needed:
|
||||
self._service_queue = needed[:]
|
||||
self._pending_data_after_services = (base, count)
|
||||
self._log(f"[AUTO] Need service for {len(needed)} indices: {needed}")
|
||||
self._kick_service_queue()
|
||||
else:
|
||||
self._enqueue_or_start(base, service=False, varqnt=count)
|
||||
|
||||
def request_lowlevel_once(self):
|
||||
"""Запрашивает чтение выбранной LowLevel переменной."""
|
||||
if not self.serial.isOpen():
|
||||
self._log("[LL] Port is not open.")
|
||||
return
|
||||
if self._busy:
|
||||
self._log("[LL] Busy, request dropped.")
|
||||
return
|
||||
if not self._ll_current_var_info:
|
||||
self._log("[LL] No variable selected!")
|
||||
if self._ll_polling: # Если поллинг активен, но переменная пропала - стоп
|
||||
self._toggle_ll_polling()
|
||||
return
|
||||
|
||||
frame = self._build_lowlevel_request(self._ll_current_var_info)
|
||||
meta = {'lowlevel': True}
|
||||
self._enqueue_raw(frame, meta)
|
||||
|
||||
# -------------------------- SERVICE QUEUE FLOW ------------------------
|
||||
# ... (код без изменений)
|
||||
def _kick_service_queue(self):
|
||||
if self._busy:
|
||||
return
|
||||
if self._service_queue:
|
||||
nxt = self._service_queue.pop(0)
|
||||
self._enqueue_or_start(nxt, service=True, varqnt=0, queue_mode=True)
|
||||
elif self._pending_data_after_services:
|
||||
base, count = self._pending_data_after_services
|
||||
self._pending_data_after_services = None
|
||||
self._enqueue_or_start(base, service=False, varqnt=count)
|
||||
|
||||
# ------------------------ TRANSACTION SCHEDULER -----------------------
|
||||
# ... (код без изменений)
|
||||
def _enqueue_raw(self, frame: bytes, meta: dict):
|
||||
if self._busy:
|
||||
if self._drop_if_busy and not self._replace_if_busy:
|
||||
self._log("[LOCKSTEP] Busy -> drop")
|
||||
return
|
||||
if self._replace_if_busy:
|
||||
self._pending_cmd = (frame, meta)
|
||||
self._log("[LOCKSTEP] Busy -> replaced pending")
|
||||
else:
|
||||
self._log("[LOCKSTEP] Busy -> ignore")
|
||||
return
|
||||
self._start_txn(frame, meta)
|
||||
|
||||
def _enqueue_or_start(self, index, service: bool, varqnt: int, chain_after=None, queue_mode=False):
|
||||
frame = self._build_request(index, service=service, varqnt=varqnt)
|
||||
meta = {'service': service, 'index': index, 'varqnt': varqnt, 'chain': chain_after, 'queue_mode': queue_mode, 'lowlevel': False}
|
||||
if self._busy:
|
||||
if self._drop_if_busy and not self._replace_if_busy:
|
||||
self._log("[LOCKSTEP] Busy -> drop")
|
||||
return
|
||||
if self._replace_if_busy:
|
||||
self._pending_cmd = (frame, meta)
|
||||
self._log("[LOCKSTEP] Busy -> replaced pending")
|
||||
else:
|
||||
self._log("[LOCKSTEP] Busy -> ignore")
|
||||
return
|
||||
self._start_txn(frame, meta)
|
||||
|
||||
def _start_txn(self, frame: bytes, meta: dict):
|
||||
self._busy = True
|
||||
self._txn_meta = meta
|
||||
self._rx_buf.clear()
|
||||
self._set_ui_busy(True)
|
||||
self._send(frame)
|
||||
self._txn_timer.start(self.read_timeout_ms)
|
||||
|
||||
def _end_txn(self):
|
||||
self._txn_timer.stop()
|
||||
queue_mode = False
|
||||
chain = None
|
||||
meta = self._txn_meta
|
||||
if meta:
|
||||
queue_mode = meta.get('queue_mode', False)
|
||||
chain = meta.get('chain')
|
||||
self._txn_meta = None
|
||||
self._busy = False
|
||||
self._rx_buf.clear()
|
||||
self._set_ui_busy(False)
|
||||
|
||||
if chain:
|
||||
base, serv, q = chain
|
||||
self._enqueue_or_start(base, service=serv, varqnt=q)
|
||||
return
|
||||
if self._pending_cmd is not None:
|
||||
frame, meta = self._pending_cmd; self._pending_cmd = None
|
||||
QtCore.QTimer.singleShot(0, lambda f=frame,m=meta: self._start_txn(f,m))
|
||||
return
|
||||
if queue_mode:
|
||||
QtCore.QTimer.singleShot(0, self._kick_service_queue)
|
||||
return
|
||||
|
||||
def _on_txn_timeout(self):
|
||||
if not self._busy: return
|
||||
is_ll = self._txn_meta.get('lowlevel', False) if self._txn_meta else False
|
||||
log_prefix = "[LL TIMEOUT]" if is_ll else "[TIMEOUT]"
|
||||
self._log(f"{log_prefix} No response")
|
||||
if self._rx_buf:
|
||||
self._log_frame(bytes(self._rx_buf), tx=False)
|
||||
self._end_txn()
|
||||
|
||||
# ------------------------------- TX/RX ---------------------------------
|
||||
# ... (код без изменений)
|
||||
def _send(self, data: bytes):
|
||||
w = self.serial.write(data)
|
||||
if w != len(data):
|
||||
self._log(f"[ERR] Write short {w}/{len(data)}")
|
||||
self.txBytes.emit(data)
|
||||
self._log_frame(data, tx=True)
|
||||
|
||||
def _on_ready_read(self):
|
||||
self._rx_buf.extend(self.serial.readAll().data())
|
||||
if not self._busy:
|
||||
if self._rx_buf:
|
||||
self._log("[WARN] Data while idle -> drop")
|
||||
self._log_frame(bytes(self._rx_buf), tx=False)
|
||||
self._rx_buf.clear()
|
||||
return
|
||||
self._try_parse()
|
||||
|
||||
# ------------------------------- PARSING -------------------------------
|
||||
def _try_parse(self):
|
||||
if not self._txn_meta:
|
||||
return
|
||||
if self._txn_meta.get('lowlevel', False):
|
||||
self._try_parse_lowlevel()
|
||||
else:
|
||||
self._try_parse_watch()
|
||||
|
||||
def _try_parse_watch(self):
|
||||
# ... (код без изменений)
|
||||
service = self._txn_meta['service']
|
||||
buf = self._rx_buf
|
||||
trailer_len = 4
|
||||
if service:
|
||||
if len(buf) < 7 + trailer_len:
|
||||
return
|
||||
name_len = buf[6]
|
||||
expected = 7 + name_len + trailer_len
|
||||
if len(buf) < expected:
|
||||
return
|
||||
frame = bytes(buf[:expected]); del buf[:expected]
|
||||
self.rxBytes.emit(frame); self._log_frame(frame, tx=False)
|
||||
self._parse_service_frame(frame)
|
||||
self._end_txn()
|
||||
else:
|
||||
if len(buf) < 6 + trailer_len:
|
||||
return
|
||||
varqnt = buf[4]; status = buf[5]
|
||||
if status != DEBUG_OK:
|
||||
expected = 8 + trailer_len
|
||||
if len(buf) < expected: return
|
||||
frame = bytes(buf[:expected]); del buf[:expected]
|
||||
self.rxBytes.emit(frame); self._log_frame(frame, tx=False)
|
||||
self._parse_data_frame(frame, error_mode=True)
|
||||
self._end_txn()
|
||||
else:
|
||||
expected = 6 + varqnt*2 + trailer_len
|
||||
if len(buf) < expected: return
|
||||
frame = bytes(buf[:expected]); del buf[:expected]
|
||||
self.rxBytes.emit(frame); self._log_frame(frame, tx=False)
|
||||
self._parse_data_frame(frame, error_mode=False)
|
||||
self._end_txn()
|
||||
|
||||
def _try_parse_lowlevel(self):
|
||||
# Ожидаемая длина: Успех=13, Ошибка=10
|
||||
buf = self._rx_buf
|
||||
if len(buf) < 10: # Минимальная длина (ошибка)
|
||||
return
|
||||
|
||||
# Проверяем, что ответ для нас
|
||||
if buf[1] != self.cmd_lowlevel:
|
||||
self._log("[LL] Unexpected cmd in lowlevel parser, flushing.")
|
||||
self._log_frame(bytes(self._rx_buf), tx=False)
|
||||
self._rx_buf.clear()
|
||||
# Не завершаем транзакцию, ждём таймаута
|
||||
return
|
||||
|
||||
status = buf[2]
|
||||
expected_len = 13 if status == DEBUG_OK else 10
|
||||
|
||||
if len(buf) >= expected_len:
|
||||
frame = bytes(buf[:expected_len])
|
||||
del buf[:expected_len]
|
||||
self.rxBytes.emit(frame)
|
||||
self._log_frame(frame, tx=False)
|
||||
self._parse_lowlevel_frame(frame, success=(status == DEBUG_OK))
|
||||
self._end_txn()
|
||||
|
||||
def _check_crc(self, payload: bytes, crc_lo: int, crc_hi: int):
|
||||
if not self.auto_crc_check:
|
||||
return True
|
||||
crc_rx = (crc_hi << 8) | crc_lo
|
||||
crc_calc = crc16_ibm(payload)
|
||||
if crc_calc != crc_rx:
|
||||
self._log(f"[CRC FAIL] calc=0x{crc_calc:04X} rx=0x{crc_rx:04X}")
|
||||
return False
|
||||
self._log("[CRC OK]")
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _clear_service_bit(vhi, vlo):
|
||||
return ((vhi & 0x7F) << 8) | vlo
|
||||
|
||||
def _parse_service_frame(self, frame: bytes):
|
||||
# ... (код без изменений)
|
||||
payload = frame[:-4]; crc_lo, crc_hi = frame[-4], frame[-3]
|
||||
if len(payload) < 7:
|
||||
self._log("[ERR] Service frame too short"); return
|
||||
self._check_crc(payload, crc_lo, crc_hi)
|
||||
adr, cmd, vhi, vlo, status, iq_raw, name_len = payload[:7]
|
||||
index = self._clear_service_bit(vhi, vlo)
|
||||
if len(payload) < 7 + name_len:
|
||||
self._log("[ERR] Service name truncated"); return
|
||||
name_bytes = payload[7:7+name_len]; name = name_bytes.decode(errors='replace')
|
||||
is_signed = (iq_raw & SIGN_BIT_MASK) != 0
|
||||
frac_bits = iq_raw & FRAC_MASK_FULL
|
||||
if status == DEBUG_OK:
|
||||
self._name_cache[index] = (status, iq_raw, name, is_signed, frac_bits)
|
||||
self.nameRead.emit(index, status, iq_raw, name)
|
||||
if self.spin_count.value() == 1 and index == self.spin_index.value():
|
||||
if status == DEBUG_OK:
|
||||
self.lbl_name.setText(name); self.lbl_iq.setText(f"{frac_bits}{'s' if is_signed else 'u'}")
|
||||
else:
|
||||
self.lbl_name.setText('<err>'); self.lbl_iq.setText('-')
|
||||
self._log(f"[SERVICE] idx={index} status={status} iq_raw=0x{iq_raw:02X} sign={'S' if is_signed else 'U'} frac={frac_bits} name='{name}'")
|
||||
|
||||
def _parse_data_frame(self, frame: bytes, *, error_mode: bool):
|
||||
# ... (код без изменений)
|
||||
payload = frame[:-4]; crc_lo, crc_hi = frame[-4], frame[-3]
|
||||
if len(payload) < 6:
|
||||
self._log("[ERR] Data frame too short"); return
|
||||
self._check_crc(payload, crc_lo, crc_hi)
|
||||
adr, cmd, vhi, vlo, varqnt, status = payload[:6]
|
||||
base = self._clear_service_bit(vhi, vlo)
|
||||
if error_mode:
|
||||
if len(payload) < 8:
|
||||
self._log("[ERR] Error frame truncated"); return
|
||||
err_hi, err_lo = payload[6:8]; bad_index = (err_hi << 8) | err_lo
|
||||
self._log(f"[DATA] ERROR status={status} bad_index={bad_index}")
|
||||
self.valueRead.emit(bad_index, status, 0, 0, float('nan'))
|
||||
self.valuesRead.emit(base, 0, [], [], [], [])
|
||||
return
|
||||
if len(payload) < 6 + varqnt*2:
|
||||
self._log("[ERR] Data payload truncated"); return
|
||||
raw_vals = []
|
||||
pos = 6
|
||||
for _ in range(varqnt):
|
||||
hi = payload[pos]; lo = payload[pos+1]; pos += 2
|
||||
raw16 = (hi << 8) | lo
|
||||
raw_vals.append(raw16)
|
||||
idx_list = []; iq_list = []; name_list = []; scaled_list = []; display_raw_list = []
|
||||
for ofs, raw16 in enumerate(raw_vals):
|
||||
idx = base + ofs
|
||||
status_i, iq_raw, name_i, is_signed, frac_bits = self._name_cache.get(idx, (DEBUG_OK, 0, '', False, 0))
|
||||
if is_signed and (raw16 & 0x8000):
|
||||
value_int = raw16 - 0x10000
|
||||
else:
|
||||
value_int = raw16
|
||||
if self.chk_raw.isChecked():
|
||||
scale = 1.0
|
||||
else:
|
||||
scale = self.iq_scaling.get(frac_bits, 1.0 / (1 << frac_bits))
|
||||
scaled = float(value_int) / scale if frac_bits > 0 else float(value_int)
|
||||
idx_list.append(idx); iq_list.append(iq_raw); name_list.append(name_i)
|
||||
scaled_list.append(scaled); display_raw_list.append(value_int)
|
||||
self._populate_table(idx_list, name_list, iq_list, display_raw_list, scaled_list)
|
||||
if varqnt == 1:
|
||||
self.edit_single_value.setText(str(display_raw_list[0]) if self.chk_raw.isChecked() else f"{scaled_list[0]:.6g}")
|
||||
if idx_list[0] == self.spin_index.value():
|
||||
_, iq_raw0, name0, is_signed0, frac0 = self._name_cache.get(idx_list[0], (DEBUG_OK, 0, '', False, 0))
|
||||
self.lbl_name.setText(name0); self.lbl_iq.setText(f"{frac0}{'s' if is_signed0 else 'u'}")
|
||||
self.valueRead.emit(idx_list[0], status, iq_list[0], display_raw_list[0], scaled_list[0])
|
||||
else:
|
||||
self.edit_single_value.setText("")
|
||||
self.valuesRead.emit(base, varqnt, idx_list, iq_list, display_raw_list, scaled_list)
|
||||
self._log(f"[DATA] base={base} q={varqnt} values={[f'{v:.6g}' for v in scaled_list] if not self.chk_raw.isChecked() else raw_vals}")
|
||||
|
||||
def _parse_lowlevel_frame(self, frame: bytes, success: bool):
|
||||
payload_len = 9 if success else 6
|
||||
crc_pos = payload_len
|
||||
payload = frame[:payload_len]
|
||||
crc_lo, crc_hi = frame[crc_pos], frame[crc_pos+1]
|
||||
|
||||
self._check_crc(payload, crc_lo, crc_hi)
|
||||
|
||||
status = payload[2]
|
||||
addr2, addr1, addr0 = payload[3], payload[4], payload[5]
|
||||
addr24 = (addr2 << 16) | (addr1 << 8) | addr0
|
||||
|
||||
self.ll_val_status.setText(f"0x{status:02X} ({'OK' if status == DEBUG_OK else 'ERR'})")
|
||||
|
||||
if not success:
|
||||
self.ll_val_rettype.setText('-')
|
||||
self.ll_val_raw.setText('-')
|
||||
self.ll_val_scaled.setText('<ERROR>')
|
||||
self.llValueRead.emit(addr24, status, 0, 0, float('nan'))
|
||||
self._log(f"[LL] ERROR status=0x{status:02X} addr=0x{addr24:06X}")
|
||||
return
|
||||
|
||||
return_type = payload[6]
|
||||
data_hi, data_lo = payload[7], payload[8]
|
||||
raw16 = (data_hi << 8) | data_lo
|
||||
|
||||
is_signed = (return_type & SIGN_BIT_MASK) != 0
|
||||
frac_bits = return_type & FRAC_MASK_FULL
|
||||
|
||||
if is_signed and (raw16 & 0x8000):
|
||||
value_int = raw16 - 0x10000
|
||||
else:
|
||||
value_int = raw16
|
||||
|
||||
if self.chk_ll_raw.isChecked():
|
||||
scale = 1.0
|
||||
else:
|
||||
scale = self.iq_scaling.get(frac_bits, 1.0 / (1 << frac_bits)) # 1 / 2^N
|
||||
|
||||
scaled = float(value_int) / scale
|
||||
|
||||
# Обновляем UI
|
||||
self.ll_val_rettype.setText(f"0x{return_type:02X} ({frac_bits}{'s' if is_signed else 'u'})")
|
||||
self.ll_val_raw.setText(str(value_int))
|
||||
self.ll_val_scaled.setText(f"{scaled:.6g}")
|
||||
|
||||
self.llValueRead.emit(addr24, status, return_type, value_int, scaled)
|
||||
self._log(f"[LL] OK addr=0x{addr24:06X} type=0x{return_type:02X} raw={value_int} scaled={scaled:.6g}")
|
||||
|
||||
def _populate_table(self, idxs, names, iqs, raws, scaled):
|
||||
"""
|
||||
Быстрое массовое обновление таблицы значений.
|
||||
- Не пересоздаём QTableWidgetItem при каждом вызове: обновляем текст.
|
||||
- Блокируем сортировку, сигналы и обновления на время заполнения.
|
||||
- Предвычисляем отображаемые строки (особенно формат scaled).
|
||||
"""
|
||||
tbl = self.tbl_values
|
||||
n = len(idxs)
|
||||
|
||||
# Заморозка UI на время массового обновления
|
||||
prev_sorting = tbl.isSortingEnabled()
|
||||
tbl.setSortingEnabled(False)
|
||||
tbl.blockSignals(True)
|
||||
tbl.setUpdatesEnabled(False)
|
||||
|
||||
# Подготовка размера
|
||||
if tbl.rowCount() != n:
|
||||
tbl.setRowCount(n)
|
||||
|
||||
# Предварительно решаем: показывать сырые или масштабированные значения
|
||||
show_raw = self.chk_raw.isChecked()
|
||||
|
||||
# Готовим строки (ускоряет при больших объёмах)
|
||||
# str() заранее, чтобы не повторять в цикле
|
||||
idx_strs = [str(v) for v in idxs]
|
||||
raw_strs = [str(v) for v in raws]
|
||||
scaled_strs = raw_strs if show_raw else [f"{v:.6g}" for v in scaled]
|
||||
|
||||
# Флаги необновляемых ячеек (только выбор/просмотр)
|
||||
flags_ro = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
|
||||
|
||||
# Локальный шорткат для быстрой установки текста в ячейку
|
||||
def _set_text(row, col, text):
|
||||
item = tbl.item(row, col)
|
||||
if item is None:
|
||||
item = QtWidgets.QTableWidgetItem(text)
|
||||
item.setFlags(flags_ro)
|
||||
tbl.setItem(row, col, item)
|
||||
else:
|
||||
# обновим текст только при изменении (немного экономит на больших данных)
|
||||
if item.text() != text:
|
||||
item.setText(text)
|
||||
if item.flags() != flags_ro:
|
||||
item.setFlags(flags_ro)
|
||||
|
||||
# Основной цикл
|
||||
for row in range(n):
|
||||
iq_raw = iqs[row]
|
||||
is_signed = (iq_raw & SIGN_BIT_MASK) != 0
|
||||
frac_bits = iq_raw & FRAC_MASK_FULL
|
||||
iq_disp = f"{frac_bits}{'s' if is_signed else 'u'}"
|
||||
|
||||
_set_text(row, 0, idx_strs[row])
|
||||
_set_text(row, 1, names[row])
|
||||
_set_text(row, 2, iq_disp)
|
||||
_set_text(row, 3, raw_strs[row])
|
||||
_set_text(row, 4, scaled_strs[row])
|
||||
|
||||
# Разморозка
|
||||
tbl.blockSignals(False)
|
||||
tbl.setUpdatesEnabled(True)
|
||||
tbl.setSortingEnabled(prev_sorting)
|
||||
tbl.viewport().update()
|
||||
|
||||
|
||||
# ------------------------------ POLLING --------------------------------
|
||||
def _toggle_polling(self):
|
||||
if self._polling:
|
||||
self._poll_timer.stop()
|
||||
self._polling = False
|
||||
self.btn_poll.setText("Start Polling")
|
||||
self._log("[POLL] Stopped")
|
||||
else:
|
||||
interval = self.spin_interval.value()
|
||||
self._poll_timer.start(interval)
|
||||
self._polling = True
|
||||
self.btn_poll.setText("Stop Polling")
|
||||
self._log(f"[POLL] Started interval={interval}ms")
|
||||
self._set_ui_busy(False) # Обновить доступность кнопок
|
||||
|
||||
def _on_poll_timeout(self):
|
||||
self.request_values()
|
||||
|
||||
def _toggle_ll_polling(self):
|
||||
"""Включает и выключает поллинг для LowLevel вкладки."""
|
||||
if self._ll_polling:
|
||||
self._ll_poll_timer.stop()
|
||||
self._ll_polling = False
|
||||
self.btn_ll_poll.setText("Start Polling")
|
||||
self._log("[LL POLL] Stopped")
|
||||
else:
|
||||
if not self._ll_current_var_info:
|
||||
self._log("[LL POLL] Cannot start: no variable selected.")
|
||||
return
|
||||
interval = self.spin_ll_interval.value()
|
||||
self._ll_poll_timer.start(interval)
|
||||
self._ll_polling = True
|
||||
self.btn_ll_poll.setText("Stop Polling")
|
||||
self._log(f"[LL POLL] Started interval={interval}ms")
|
||||
self._set_ui_busy(False) # Обновить доступность кнопок
|
||||
|
||||
def _on_ll_poll_timeout(self):
|
||||
"""Слот таймера поллинга для LowLevel."""
|
||||
self.request_lowlevel_once()
|
||||
|
||||
def _on_ll_variable_prepared(self, var_info: dict):
|
||||
"""Срабатывает при выборе переменной в селекторе."""
|
||||
self._ll_current_var_info = var_info
|
||||
self._log(f"[LL] Selected variable '{var_info['path']}' @ {var_info['address_hex']}")
|
||||
# Сбрасываем старые значения
|
||||
self.ll_val_status.setText("-")
|
||||
self.ll_val_rettype.setText("-")
|
||||
self.ll_val_raw.setText("-")
|
||||
self.ll_val_scaled.setText("-")
|
||||
|
||||
# ------------------------------ HELPERS --------------------------------
|
||||
def _toggle_index_base(self, st):
|
||||
# ... (код без изменений)
|
||||
val = self.spin_index.value()
|
||||
if st == QtCore.Qt.Checked:
|
||||
self.spin_index.setDisplayIntegerBase(16); self.spin_index.setPrefix("0x")
|
||||
else:
|
||||
self.spin_index.setDisplayIntegerBase(10); self.spin_index.setPrefix("")
|
||||
self.spin_index.setValue(val)
|
||||
|
||||
def _set_ui_busy(self, busy: bool):
|
||||
# Блокируем кнопки в зависимости от состояния 'busy' и 'polling'
|
||||
|
||||
# Watch tab
|
||||
can_use_watch = not busy and not self._polling
|
||||
self.btn_read_service.setEnabled(can_use_watch)
|
||||
self.btn_read_values.setEnabled(can_use_watch)
|
||||
|
||||
# LowLevel tab
|
||||
can_use_ll = not busy and not self._ll_polling
|
||||
self.btn_ll_read.setEnabled(can_use_ll)
|
||||
|
||||
def _on_serial_error(self, err):
|
||||
# ... (код без изменений)
|
||||
if err == QtSerialPort.QSerialPort.NoError: return
|
||||
self._log(f"[SERIAL ERR] {self.serial.errorString()} ({err})")
|
||||
if self._busy: self._end_txn()
|
||||
|
||||
# ------------------------------ LOGGING --------------------------------
|
||||
def _log(self, msg: str):
|
||||
# ... (код без изменений)
|
||||
if not self.log_spoiler.getState():
|
||||
return
|
||||
ts = datetime.datetime.now().strftime('%H:%M:%S.%f')[:-3]
|
||||
self.txt_log.append(f"{ts} {msg}")
|
||||
|
||||
def _log_frame(self, data: bytes, *, tx: bool):
|
||||
# ... (код без изменений)
|
||||
if not self.log_spoiler.getState():
|
||||
return
|
||||
tag = 'TX' if tx else 'RX'
|
||||
hexs = ' '.join(f"{b:02X}" for b in data)
|
||||
ascii_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data)
|
||||
self._log(f"[{tag}] {hexs} |{ascii_part}|")
|
||||
|
||||
|
||||
# ---------------------------------------------------------- Demo harness ---
|
||||
class _DemoWindow(QtWidgets.QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("DebugVar Terminal")
|
||||
self.term = DebugTerminalWidget(self)
|
||||
self.setCentralWidget(self.term)
|
||||
self.term.nameRead.connect(self._on_name)
|
||||
self.term.valueRead.connect(self._on_value)
|
||||
self.term.llValueRead.connect(self._on_ll_value)
|
||||
|
||||
def _on_name(self, index, status, iq, name):
|
||||
return
|
||||
print(f"Name idx={index} status={status} iq={iq} name='{name}'")
|
||||
|
||||
def _on_value(self, index, status, iq, raw16, floatVal):
|
||||
return
|
||||
print(f"Value idx={index} status={status} iq={iq} raw={raw16} val={floatVal}")
|
||||
|
||||
def _on_ll_value(self, addr, status, rettype_raw, raw16, scaled):
|
||||
return
|
||||
print(f"LL addr=0x{addr:06X} status={status} type=0x{rettype_raw:02X} raw={raw16} scaled={scaled}")
|
||||
|
||||
def format_address(addr_text: str) -> str:
|
||||
try:
|
||||
value = int(addr_text, 16)
|
||||
except ValueError:
|
||||
value = 0
|
||||
return f"0x{value:06X}"
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.setCentralWidget(None)
|
||||
if self.term:
|
||||
self.term.deleteLater(); self.term = None
|
||||
super().closeEvent(event)
|
||||
|
||||
# ------------------------------- Demo --------------------------------------
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
win = _DemoWindow(); win.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -1,86 +1,64 @@
|
||||
import re
|
||||
# variable_select_widget.py
|
||||
import pickle
|
||||
import hashlib
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from PySide2.QtWidgets import (
|
||||
QWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QLineEdit,
|
||||
QWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QLineEdit,
|
||||
QHeaderView, QCompleter
|
||||
)
|
||||
from PySide2.QtGui import QKeyEvent
|
||||
from PySide2.QtCore import Qt, QStringListModel
|
||||
import pickle
|
||||
import time
|
||||
import hashlib
|
||||
|
||||
from path_hints import PathHints, canonical_key, split_path_tokens
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# utils
|
||||
# ------------------------------------------------------------------
|
||||
def compute_vars_hash(vars_list):
|
||||
return hashlib.sha1(pickle.dumps(vars_list)).hexdigest()
|
||||
|
||||
# Вспомогательные функции, которые теперь будут использоваться виджетом
|
||||
def split_path(path):
|
||||
"""
|
||||
Разбивает путь на компоненты:
|
||||
- 'foo[2].bar[1]->baz' → ['foo', '[2]', 'bar', '[1]', 'baz']
|
||||
Если видит '-' в конце строки (без '>' после) — обрезает этот '-'
|
||||
"""
|
||||
tokens = []
|
||||
token = ''
|
||||
i = 0
|
||||
length = len(path)
|
||||
while i < length:
|
||||
c = path[i]
|
||||
# Разделители: '->' и '.'
|
||||
if c == '-' and i + 1 < length and path[i:i+2] == '->':
|
||||
if token:
|
||||
tokens.append(token)
|
||||
token = ''
|
||||
i += 2
|
||||
continue
|
||||
elif c == '-' and i == length - 1:
|
||||
# '-' на конце строки без '>' после — просто пропускаем его
|
||||
i += 1
|
||||
continue
|
||||
elif c == '.':
|
||||
if token:
|
||||
tokens.append(token)
|
||||
token = ''
|
||||
i += 1
|
||||
continue
|
||||
elif c == '[':
|
||||
if token:
|
||||
tokens.append(token)
|
||||
token = ''
|
||||
idx = ''
|
||||
while i < length and path[i] != ']':
|
||||
idx += path[i]
|
||||
i += 1
|
||||
if i < length and path[i] == ']':
|
||||
idx += ']'
|
||||
i += 1
|
||||
tokens.append(idx)
|
||||
continue
|
||||
else:
|
||||
token += c
|
||||
i += 1
|
||||
if token:
|
||||
tokens.append(token)
|
||||
return tokens
|
||||
|
||||
|
||||
def is_lazy_item(item):
|
||||
def is_lazy_item(item: QTreeWidgetItem) -> bool:
|
||||
return item.childCount() == 1 and item.child(0).text(0) == 'lazy_marker'
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# VariableSelectWidget
|
||||
# ------------------------------------------------------------------
|
||||
class VariableSelectWidget(QWidget):
|
||||
"""
|
||||
Виджет выбора переменных с деревом + строкой поиска + автодополнением.
|
||||
Подсказки полностью через PathHints.
|
||||
ВАЖНО: ожидается, что в данных (vars_list) каждое var['name'] — ПОЛНЫЙ ПУТЬ
|
||||
(например: 'project.adc.status'), даже внутри children.
|
||||
"""
|
||||
|
||||
ROLE_NAME = Qt.UserRole # локальный хвост (display)
|
||||
ROLE_VAR_DICT = Qt.UserRole + 100 # исходный dict
|
||||
ROLE_FULLPATH = Qt.UserRole + 200 # полный путь
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.expanded_vars = []
|
||||
self.node_index = {}
|
||||
self.is_autocomplete_on = True # <--- ДОБАВИТЬ ЭТУ СТРОКУ
|
||||
self._bckspc_pressed = False
|
||||
self.manual_completion_active = False
|
||||
self._vars_hash = None
|
||||
|
||||
# --- UI Элементы ---
|
||||
# данные
|
||||
self.expanded_vars: List[Dict[str, Any]] = []
|
||||
self.is_autocomplete_on = True
|
||||
self.manual_completion_active = False
|
||||
self._bckspc_pressed = False
|
||||
self._vars_hash: Optional[str] = None
|
||||
|
||||
# индекс: canonical_full_path -> item
|
||||
self._item_by_canon: Dict[str, QTreeWidgetItem] = {}
|
||||
|
||||
# подсказки
|
||||
self.hints = PathHints()
|
||||
|
||||
# --- UI ---
|
||||
self.search_input = QLineEdit(self)
|
||||
self.search_input.setPlaceholderText("Поиск...")
|
||||
|
||||
|
||||
self.tree = QTreeWidget(self)
|
||||
self.tree.setHeaderLabels(["Имя переменной", "Тип"])
|
||||
self.tree.setSelectionMode(QTreeWidget.ExtendedSelection)
|
||||
@@ -97,32 +75,58 @@ class VariableSelectWidget(QWidget):
|
||||
self.completer.setCaseSensitivity(Qt.CaseInsensitive)
|
||||
self.completer.setFilterMode(Qt.MatchContains)
|
||||
self.completer.setWidget(self.search_input)
|
||||
self.completer.activated[str].connect(self.insert_completion)
|
||||
|
||||
# --- Layout ---
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(self.search_input)
|
||||
layout.addWidget(self.tree)
|
||||
|
||||
# --- Соединения ---
|
||||
#self.search_input.textChanged.connect(self.on_search_text_changed)
|
||||
self.search_input.textChanged.connect(lambda text: self.on_search_text_changed(text))
|
||||
# layout
|
||||
lay = QVBoxLayout(self)
|
||||
lay.setContentsMargins(0, 0, 0, 0)
|
||||
lay.addWidget(self.search_input)
|
||||
lay.addWidget(self.tree)
|
||||
|
||||
# signals
|
||||
self.search_input.textChanged.connect(self.on_search_text_changed)
|
||||
self.search_input.installEventFilter(self)
|
||||
self.completer.activated[str].connect(lambda text: self.insert_completion(text))
|
||||
|
||||
# --- Публичные методы для управления виджетом снаружи ---
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# public api
|
||||
# ------------------------------------------------------------------
|
||||
def set_autocomplete(self, enabled: bool):
|
||||
"""Включает или выключает режим автодополнения."""
|
||||
self.is_autocomplete_on = enabled
|
||||
|
||||
def set_data(self, vars_list):
|
||||
"""Основной метод для загрузки данных в виджет."""
|
||||
def set_data(self, vars_list: List[Dict[str, Any]]):
|
||||
"""
|
||||
Загружаем список переменных (формат: см. класс docstring).
|
||||
"""
|
||||
# deepcopy
|
||||
self.expanded_vars = pickle.loads(pickle.dumps(vars_list, protocol=pickle.HIGHEST_PROTOCOL))
|
||||
# self.build_completion_list() # Если нужна полная перестройка списка
|
||||
self.populate_tree()
|
||||
|
||||
# rebuild hints из полного списка узлов (каждый узел уже с full_path)
|
||||
self._rebuild_hints_from_vars(self.expanded_vars)
|
||||
|
||||
# rebuild tree
|
||||
self.populate_tree(self.expanded_vars)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# hints builder: дети уже содержат ПОЛНЫЙ ПУТЬ
|
||||
# ------------------------------------------------------------------
|
||||
def _rebuild_hints_from_vars(self, vars_list: List[Dict[str, Any]]):
|
||||
paths: List[tuple] = []
|
||||
|
||||
def walk(node: Dict[str, Any]):
|
||||
full = node.get('name', '')
|
||||
if full:
|
||||
paths.append((full, node.get('type')))
|
||||
for ch in node.get('children', []) or []:
|
||||
walk(ch)
|
||||
|
||||
for v in vars_list:
|
||||
walk(v)
|
||||
|
||||
self.hints.set_paths(paths)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# tree building
|
||||
# ------------------------------------------------------------------
|
||||
def populate_tree(self, vars_list=None):
|
||||
if vars_list is None:
|
||||
vars_list = self.expanded_vars
|
||||
@@ -130,477 +134,311 @@ class VariableSelectWidget(QWidget):
|
||||
new_hash = compute_vars_hash(vars_list)
|
||||
if self._vars_hash == new_hash:
|
||||
return
|
||||
|
||||
self._vars_hash = new_hash
|
||||
|
||||
self.tree.setUpdatesEnabled(False)
|
||||
self.tree.blockSignals(True)
|
||||
self.tree.clear()
|
||||
self.node_index.clear()
|
||||
self._item_by_canon.clear()
|
||||
|
||||
for var in vars_list:
|
||||
self.add_tree_item_lazy(None, var)
|
||||
# построим top-level из входного списка: определяем по глубине токенов
|
||||
# (vars_list может содержать и глубокие узлы; выберем корни = те, чей full_path не имеет родителя в списке)
|
||||
full_to_node = {v['name']: v for v in vars_list}
|
||||
# но safer: просто добавляем все как top-level, если ты уже передаёшь только корни.
|
||||
# Если в твоих данных vars_list == корни, просто сделаем:
|
||||
for v in vars_list:
|
||||
self._add_tree_item_lazy(None, v)
|
||||
|
||||
self.tree.setUpdatesEnabled(True)
|
||||
self.tree.blockSignals(False)
|
||||
|
||||
header = self.tree.header()
|
||||
header.setSectionResizeMode(QHeaderView.Interactive)
|
||||
header.setSectionResizeMode(1, QHeaderView.Stretch)
|
||||
self.tree.setColumnWidth(0, 400)
|
||||
|
||||
def on_item_expanded(self, item):
|
||||
|
||||
def on_item_expanded(self, item: QTreeWidgetItem):
|
||||
if is_lazy_item(item):
|
||||
item.removeChild(item.child(0))
|
||||
var = item.data(0, Qt.UserRole + 100)
|
||||
var = item.data(0, self.ROLE_VAR_DICT)
|
||||
if var:
|
||||
for child_var in var.get('children', []):
|
||||
self.add_tree_item_lazy(item, child_var)
|
||||
for ch in var.get('children', []) or []:
|
||||
self._add_tree_item_lazy(item, ch)
|
||||
|
||||
|
||||
def get_full_item_name(self, item):
|
||||
fullname = item.text(0)
|
||||
# Заменяем '->' на '.'
|
||||
fullname = fullname.replace('->', '.')
|
||||
fullname = fullname.replace('[', '.[')
|
||||
return fullname
|
||||
|
||||
def add_tree_item_lazy(self, parent, var):
|
||||
name = var['name']
|
||||
# ------------------------------------------------------------------
|
||||
# item creation (var['name'] — ПОЛНЫЙ ПУТЬ)
|
||||
# ------------------------------------------------------------------
|
||||
def _add_tree_item_lazy(self, parent: Optional[QTreeWidgetItem], var: Dict[str, Any]):
|
||||
full_path = var.get('name', '')
|
||||
type_str = var.get('type', '')
|
||||
item = QTreeWidgetItem([name, type_str])
|
||||
item.setData(0, Qt.UserRole, name)
|
||||
full_name = self.get_full_item_name(item)
|
||||
self.node_index[full_name.lower()] = item
|
||||
|
||||
# здесь оставляем полный путь для отображения
|
||||
item = QTreeWidgetItem([full_path, type_str])
|
||||
item.setData(0, self.ROLE_NAME, full_path) # теперь ROLE_NAME = полный путь
|
||||
item.setData(0, self.ROLE_VAR_DICT, var)
|
||||
item.setData(0, self.ROLE_FULLPATH, full_path)
|
||||
|
||||
if "(bitfield:" in type_str:
|
||||
item.setDisabled(True)
|
||||
self.set_tool(item, "Битовые поля недоступны для выбора")
|
||||
self._set_tool(item, "Битовые поля недоступны для выбора")
|
||||
|
||||
# метаданные
|
||||
for i, attr in enumerate(['file', 'extern', 'static']):
|
||||
item.setData(0, Qt.UserRole + 1 + i, var.get(attr))
|
||||
|
||||
# в дерево
|
||||
if parent is None:
|
||||
self.tree.addTopLevelItem(item)
|
||||
else:
|
||||
parent.addChild(item)
|
||||
|
||||
# Если есть дети — добавляем заглушку (чтобы можно было раскрыть)
|
||||
# lazy children
|
||||
if var.get('children'):
|
||||
dummy = QTreeWidgetItem(["lazy_marker"])
|
||||
item.addChild(dummy)
|
||||
|
||||
# Кэшируем детей для подгрузки по событию
|
||||
item.setData(0, Qt.UserRole + 100, var) # Сохраняем var целиком
|
||||
# индекс
|
||||
self._item_by_canon[canonical_key(full_path)] = item
|
||||
|
||||
|
||||
def show_matching_path(self, item, path_parts, level=0):
|
||||
node_name = item.text(0).lower()
|
||||
node_parts = split_path(node_name)
|
||||
@staticmethod
|
||||
def _tail_token(full_path: str) -> str:
|
||||
toks = split_path_tokens(full_path)
|
||||
return toks[-1] if toks else full_path
|
||||
|
||||
if 'project' in node_name:
|
||||
a = 1
|
||||
# ------------------------------------------------------------------
|
||||
# filtering
|
||||
# ------------------------------------------------------------------
|
||||
def filter_tree(self):
|
||||
"""
|
||||
Быстрый фильтр:
|
||||
- без разделителей → substring по ЛОКАЛЬНОМУ имени top-level
|
||||
- с разделителями → структурный (по токенам full_path)
|
||||
"""
|
||||
text = (self.search_input.text() or '').strip()
|
||||
low = text.lower()
|
||||
parts = split_path_tokens(low) if low else []
|
||||
|
||||
# простой режим (нет ., ->, [):
|
||||
if low and all(x not in low for x in ('.', '->', '[')):
|
||||
for i in range(self.tree.topLevelItemCount()):
|
||||
it = self.tree.topLevelItem(i)
|
||||
full = (it.data(0, self.ROLE_FULLPATH) or '').lower()
|
||||
it.setHidden(low not in full)
|
||||
return
|
||||
|
||||
# структурный
|
||||
for i in range(self.tree.topLevelItemCount()):
|
||||
it = self.tree.topLevelItem(i)
|
||||
self._show_matching_path(it, parts, 0)
|
||||
|
||||
def _show_matching_path(self, item: QTreeWidgetItem, path_parts: List[str], level: int = 0):
|
||||
"""
|
||||
Сравниваем введённый путь (разбитый на токены) с ПОЛНЫМ ПУТЁМ узла.
|
||||
Алгоритм: берём полный путь узла, разбиваем в токены, берём уровень level,
|
||||
и сравниваем с соответствующим токеном path_parts[level].
|
||||
"""
|
||||
full = (item.data(0, self.ROLE_FULLPATH) or '').lower()
|
||||
node_parts = split_path_tokens(full)
|
||||
|
||||
if level >= len(path_parts):
|
||||
# Путь полностью пройден — показываем только этот узел (без раскрытия всех детей)
|
||||
item.setHidden(False)
|
||||
item.setExpanded(False)
|
||||
return True
|
||||
|
||||
if level >= len(node_parts):
|
||||
# Уровень поиска больше длины пути узла — скрываем
|
||||
item.setHidden(False)
|
||||
item.setHidden(True)
|
||||
return False
|
||||
|
||||
search_part = path_parts[level]
|
||||
node_part = node_parts[level]
|
||||
|
||||
if search_part == node_part:
|
||||
# Точное совпадение — показываем узел, идём вглубь только по совпадениям
|
||||
item.setHidden(False)
|
||||
matched_any = False
|
||||
self.on_item_expanded(item)
|
||||
for i in range(item.childCount()):
|
||||
child = item.child(i)
|
||||
if self.show_matching_path(child, path_parts, level + 1):
|
||||
ch = item.child(i)
|
||||
if self._show_matching_path(ch, path_parts, level + 1):
|
||||
matched_any = True
|
||||
item.setExpanded(matched_any)
|
||||
return matched_any or item.childCount() == 0
|
||||
|
||||
elif node_part.startswith(search_part):
|
||||
# Неполное совпадение — показываем только этот узел, детей скрываем, не раскрываем
|
||||
item.setHidden(False)
|
||||
item.setExpanded(False)
|
||||
return True
|
||||
|
||||
elif search_part in node_part and (level == len(path_parts)-1):
|
||||
# Неполное совпадение — показываем только этот узел, детей скрываем, не раскрываем
|
||||
|
||||
elif search_part in node_part and (level == len(path_parts) - 1):
|
||||
item.setHidden(False)
|
||||
item.setExpanded(False)
|
||||
return True
|
||||
|
||||
else:
|
||||
# Несовпадение — скрываем
|
||||
item.setHidden(True)
|
||||
return False
|
||||
|
||||
|
||||
def filter_tree(self):
|
||||
text = self.search_input.text().strip().lower()
|
||||
path_parts = split_path(text) if text else []
|
||||
|
||||
if '.' not in text and '->' not in text and '[' not in text and text != '':
|
||||
for i in range(self.tree.topLevelItemCount()):
|
||||
item = self.tree.topLevelItem(i)
|
||||
name = item.text(0).lower()
|
||||
if text in name:
|
||||
item.setHidden(False)
|
||||
# Не сбрасываем expanded, чтобы можно было раскрывать вручную
|
||||
else:
|
||||
item.setHidden(True)
|
||||
else:
|
||||
for i in range(self.tree.topLevelItemCount()):
|
||||
item = self.tree.topLevelItem(i)
|
||||
self.show_matching_path(item, path_parts, 0)
|
||||
|
||||
|
||||
|
||||
def find_node_by_path(self, root_vars, path_list):
|
||||
current_level = root_vars
|
||||
node = None
|
||||
for part in path_list:
|
||||
node = None
|
||||
for var in current_level:
|
||||
if var['name'] == part:
|
||||
node = var
|
||||
break
|
||||
if node is None:
|
||||
return None
|
||||
current_level = node.get('children', [])
|
||||
return node
|
||||
|
||||
|
||||
def update_completions(self, text=None):
|
||||
# ------------------------------------------------------------------
|
||||
# completions (ONLY PathHints)
|
||||
# ------------------------------------------------------------------
|
||||
def update_completions(self, text: Optional[str] = None) -> List[str]:
|
||||
if text is None:
|
||||
text = self.search_input.text().strip()
|
||||
text = self.search_input.text()
|
||||
suggestions = self.hints.suggest(text)
|
||||
self.completer.setModel(QStringListModel(suggestions))
|
||||
if suggestions:
|
||||
self.completer.complete()
|
||||
else:
|
||||
text = text.strip()
|
||||
self.completer.popup().hide()
|
||||
return suggestions
|
||||
|
||||
normalized_text = text.replace('->', '.')
|
||||
parts = split_path(text)
|
||||
path_parts = parts[:-1] if parts else []
|
||||
prefix = parts[-1].lower() if parts else ''
|
||||
ends_with_sep = text.endswith('.') or text.endswith('->') or text.endswith('[')
|
||||
is_index_suggestion = text.endswith('[')
|
||||
def insert_completion(self, full_path: str):
|
||||
"""
|
||||
Пользователь выбрал подсказку (full_path).
|
||||
Если у узла есть дети и пользователь не поставил разделитель —
|
||||
добавим '.'. Для массивного токена ('[0]') → добавим '.' тоже.
|
||||
(Позже допилим '->' при наличии метаданных.)
|
||||
"""
|
||||
node = self.hints.find_node(full_path)
|
||||
text = full_path
|
||||
|
||||
completions = []
|
||||
|
||||
def find_exact_node(parts):
|
||||
if not parts:
|
||||
return None
|
||||
fullname = parts[0]
|
||||
for p in parts[1:]:
|
||||
fullname += '.' + p
|
||||
return self.node_index.get(fullname.lower())
|
||||
|
||||
if is_index_suggestion:
|
||||
base_text = text[:-1] # убираем '['
|
||||
parent_node = self.find_node_by_fullname(base_text)
|
||||
if not parent_node:
|
||||
base_text_clean = re.sub(r'\[\d+\]$', '', base_text)
|
||||
parent_node = self.find_node_by_fullname(base_text_clean)
|
||||
if parent_node:
|
||||
seen = set()
|
||||
for i in range(parent_node.childCount()):
|
||||
child = parent_node.child(i)
|
||||
if child.isHidden():
|
||||
continue
|
||||
cname = child.text(0)
|
||||
m = re.match(rf'^{re.escape(base_text)}\[(\d+)\]$', cname)
|
||||
if m and cname not in seen:
|
||||
completions.append(cname)
|
||||
seen.add(cname)
|
||||
self.completer.setModel(QStringListModel(completions))
|
||||
return completions
|
||||
|
||||
if ends_with_sep:
|
||||
node = self.find_node_by_fullname(text[:-1])
|
||||
if node:
|
||||
for i in range(node.childCount()):
|
||||
child = node.child(i)
|
||||
if child.isHidden():
|
||||
continue
|
||||
completions.append(child.text(0))
|
||||
elif not path_parts:
|
||||
# Первый уровень — только если имя начинается с prefix
|
||||
for i in range(self.tree.topLevelItemCount()):
|
||||
item = self.tree.topLevelItem(i)
|
||||
if item.isHidden():
|
||||
continue
|
||||
name = item.text(0)
|
||||
if name.lower().startswith(prefix):
|
||||
completions.append(name)
|
||||
else:
|
||||
node = find_exact_node(path_parts)
|
||||
if node:
|
||||
for i in range(node.childCount()):
|
||||
child = node.child(i)
|
||||
if child.isHidden():
|
||||
continue
|
||||
name = child.text(0)
|
||||
name_parts = child.data(0, Qt.UserRole + 10)
|
||||
if name_parts is None:
|
||||
name_parts = split_path(name)
|
||||
child.setData(0, Qt.UserRole + 10, name_parts)
|
||||
if not name_parts:
|
||||
continue
|
||||
last_part = name_parts[-1].lower()
|
||||
if prefix == '' or prefix in last_part: # ← строго startswith
|
||||
completions.append(name)
|
||||
|
||||
self.completer.setModel(QStringListModel(completions))
|
||||
self.completer.complete()
|
||||
return completions
|
||||
|
||||
|
||||
# Функция для поиска узла с полным именем
|
||||
def find_node_by_fullname(self, name):
|
||||
if name is None:
|
||||
return None
|
||||
normalized_name = name.replace('->', '.').lower()
|
||||
normalized_name = normalized_name.replace('[', '.[').lower()
|
||||
return self.node_index.get(normalized_name)
|
||||
|
||||
def insert_completion(self, text):
|
||||
node = self.find_node_by_fullname(text)
|
||||
if node and node.childCount() > 0 and not (text.endswith('.') or text.endswith('->') or text.endswith('[')):
|
||||
# Определяем разделитель по имени первого ребёнка
|
||||
child_name = node.child(0).text(0)
|
||||
if child_name.startswith(text + '->'):
|
||||
text += '->'
|
||||
elif child_name.startswith(text + '.'):
|
||||
text += '.'
|
||||
elif '[' in child_name:
|
||||
text += '[' # для массивов
|
||||
if node and node.children and not (
|
||||
text.endswith('.') or text.endswith('->') or text.endswith('[')
|
||||
):
|
||||
first_child = next(iter(node.children.values()))
|
||||
if first_child.name.startswith('['):
|
||||
text += '[' # пользователь сразу начнёт ввод индекса
|
||||
else:
|
||||
text += '.' # fallback
|
||||
|
||||
if not self._bckspc_pressed:
|
||||
self.search_input.setText(text)
|
||||
self.search_input.setCursorPosition(len(text))
|
||||
|
||||
self.run_completions(text)
|
||||
else:
|
||||
text += '.' # обычный переход
|
||||
if not self._bckspc_pressed:
|
||||
self.search_input.setText(text)
|
||||
self.search_input.setCursorPosition(len(text))
|
||||
self.run_completions(text)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# events
|
||||
# ------------------------------------------------------------------
|
||||
def eventFilter(self, obj, event):
|
||||
if obj == self.search_input and isinstance(event, QKeyEvent):
|
||||
if event.key() == Qt.Key_Space and event.modifiers() & Qt.ControlModifier:
|
||||
self.manual_completion_active = True
|
||||
text = self.search_input.text().strip()
|
||||
self.run_completions(text)
|
||||
self.run_completions(self.search_input.text())
|
||||
elif event.key() == Qt.Key_Escape:
|
||||
# Esc — выключаем ручной режим и скрываем подсказки, если autocomplete выключен
|
||||
if not self.is_autocomplete_on:
|
||||
self.manual_completion_active = False
|
||||
self.completer.popup().hide()
|
||||
return True
|
||||
|
||||
if event.key() == Qt.Key_Backspace:
|
||||
self._bckspc_pressed = True
|
||||
self._bckspc_pressed = True
|
||||
else:
|
||||
self._bckspc_pressed = False
|
||||
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
def run_completions(self, text):
|
||||
completions = self.update_completions(text)
|
||||
|
||||
if not self.is_autocomplete_on and self._bckspc_pressed:
|
||||
text = text[:-1]
|
||||
|
||||
if len(completions) == 1 and completions[0].lower() == text.lower():
|
||||
# Найдем узел с таким именем
|
||||
def find_exact_item(name):
|
||||
stack = [self.tree.topLevelItem(i) for i in range(self.tree.topLevelItemCount())]
|
||||
while stack:
|
||||
node = stack.pop()
|
||||
if node.text(0).lower() == name.lower():
|
||||
return node
|
||||
for i in range(node.childCount()):
|
||||
stack.append(node.child(i))
|
||||
return None
|
||||
def run_completions(self, text: str):
|
||||
if not self.is_autocomplete_on and not self.manual_completion_active:
|
||||
self.completer.popup().hide()
|
||||
return
|
||||
self.update_completions(text)
|
||||
|
||||
node = find_exact_item(completions[0])
|
||||
if node and node.childCount() > 0:
|
||||
# Используем первую подсказку, чтобы определить нужный разделитель
|
||||
completions = self.update_completions(text + '.')
|
||||
if not completions:
|
||||
return
|
||||
suggestion = completions[0]
|
||||
|
||||
# Ищем, какой символ идёт после текущего текста
|
||||
separator = '.'
|
||||
if suggestion.startswith(text):
|
||||
rest = suggestion[len(text):]
|
||||
if rest.startswith(text + '->'):
|
||||
separator += '->'
|
||||
elif rest.startswith(text + '.'):
|
||||
separator += '.'
|
||||
elif '[' in rest:
|
||||
separator += '[' # для массивов
|
||||
else:
|
||||
separator += '.' # fallback
|
||||
|
||||
if not self._bckspc_pressed:
|
||||
self.search_input.setText(text + separator)
|
||||
completions = self.update_completions(text)
|
||||
self.completer.setModel(QStringListModel(completions))
|
||||
self.completer.complete()
|
||||
return True
|
||||
|
||||
# Иначе просто показываем подсказки
|
||||
self.completer.setModel(QStringListModel(completions))
|
||||
if completions:
|
||||
self.completer.complete()
|
||||
return True
|
||||
|
||||
def on_search_text_changed(self, text):
|
||||
sender_widget = self.sender()
|
||||
sender_name = sender_widget.objectName() if sender_widget else "Unknown Sender"
|
||||
|
||||
self.completer.setWidget(self.search_input)
|
||||
def on_search_text_changed(self, text: str):
|
||||
self.completer.setWidget(self.search_input)
|
||||
self.filter_tree()
|
||||
if text == None:
|
||||
text = self.search_input.text().strip()
|
||||
if text is None:
|
||||
text = self.search_input.text()
|
||||
if self.is_autocomplete_on:
|
||||
self.run_completions(text)
|
||||
else:
|
||||
# Если выключено, показываем подсказки только если флаг ручного вызова True
|
||||
if self.manual_completion_active:
|
||||
self.run_completions(text)
|
||||
else:
|
||||
self.completer.popup().hide()
|
||||
|
||||
|
||||
def focusInEvent(self, event):
|
||||
if self.completer.widget() != self.search_input:
|
||||
self.completer.setWidget(self.search_input)
|
||||
super().focusInEvent(event)
|
||||
|
||||
def _custom_focus_in_event(self, event):
|
||||
# Принудительно установить виджет для completer при получении фокуса
|
||||
if self.completer.widget() != self.search_input:
|
||||
self.completer.setWidget(self.search_input)
|
||||
super(QLineEdit, self.search_input).focusInEvent(event) # Вызвать оригинальный обработчик
|
||||
|
||||
|
||||
def build_completion_list(self):
|
||||
completions = []
|
||||
|
||||
def recurse(var, prefix=''):
|
||||
fullname = f"{prefix}.{var['name']}" if prefix else var['name']
|
||||
completions.append(fullname)
|
||||
for child in var.get('children', []):
|
||||
recurse(child, fullname)
|
||||
|
||||
for v in self.expanded_vars:
|
||||
recurse(v)
|
||||
self.all_completions = completions
|
||||
|
||||
def set_tool(self, item, text):
|
||||
item.setToolTip(0, text)
|
||||
item.setToolTip(1, text)
|
||||
|
||||
def get_all_items(self):
|
||||
"""Возвращает все конечные (leaf) элементы, исключая битовые поля и элементы с детьми (реальными)."""
|
||||
def collect_leaf_items(parent):
|
||||
leaf_items = []
|
||||
for i in range(parent.childCount()):
|
||||
child = parent.child(i)
|
||||
if child.isHidden():
|
||||
continue
|
||||
|
||||
# Если есть заглушка — раскрываем
|
||||
self.on_item_expanded(child)
|
||||
|
||||
if child.childCount() == 0:
|
||||
item_type = child.text(1)
|
||||
if item_type and 'bitfield' in str(item_type).lower():
|
||||
continue
|
||||
leaf_items.append(child)
|
||||
else:
|
||||
leaf_items.extend(collect_leaf_items(child))
|
||||
return leaf_items
|
||||
|
||||
all_leaf_items = []
|
||||
for i in range(self.tree.topLevelItemCount()):
|
||||
top = self.tree.topLevelItem(i)
|
||||
|
||||
# Раскрываем lazy, если надо
|
||||
self.on_item_expanded(top)
|
||||
|
||||
if top.childCount() == 0:
|
||||
item_type = top.text(1)
|
||||
if item_type and 'bitfield' in str(item_type).lower():
|
||||
continue
|
||||
all_leaf_items.append(top)
|
||||
else:
|
||||
all_leaf_items.extend(collect_leaf_items(top))
|
||||
return all_leaf_items
|
||||
|
||||
|
||||
|
||||
def _get_internal_selected_items(self):
|
||||
"""Возвращает выделенные элементы и всех их потомков, включая lazy."""
|
||||
selected = self.tree.selectedItems()
|
||||
all_items = []
|
||||
|
||||
def collect_children(item):
|
||||
# Раскрываем при необходимости
|
||||
# Раскрываем lazy, если надо
|
||||
self.on_item_expanded(item)
|
||||
|
||||
items = [item]
|
||||
for i in range(item.childCount()):
|
||||
child = item.child(i)
|
||||
items.extend(collect_children(child))
|
||||
return items
|
||||
|
||||
for item in selected:
|
||||
all_items.extend(collect_children(item))
|
||||
|
||||
return all_items
|
||||
|
||||
def get_selected_items(self):
|
||||
"""Возвращает только конечные (leaf) выделенные элементы, исключая bitfield."""
|
||||
selected = self.tree.selectedItems()
|
||||
leaf_items = []
|
||||
for item in selected:
|
||||
# Раскрываем lazy, если надо
|
||||
self.on_item_expanded(item)
|
||||
|
||||
# Если у узла нет видимых/выделенных детей — он лист
|
||||
if all(item.child(i).isHidden() or not item.child(i).isSelected() for i in range(item.childCount())):
|
||||
item_type = item.data(0, Qt.UserRole)
|
||||
if item_type and 'bitfield' in str(item_type).lower():
|
||||
continue
|
||||
leaf_items.append(item)
|
||||
return leaf_items
|
||||
|
||||
|
||||
def get_all_var_names(self):
|
||||
"""Возвращает имена всех конечных (leaf) переменных, исключая битовые поля и группы."""
|
||||
return [item.text(0) for item in self.get_all_items() if item.text(0)]
|
||||
|
||||
|
||||
def _get_internal_selected_var_names(self):
|
||||
"""Возвращает имена выделенных переменных."""
|
||||
return [item.text(0) for item in self._get_internal_selected_items() if item.text(0)]
|
||||
|
||||
|
||||
def get_selected_var_names(self):
|
||||
"""Возвращает имена только конечных (leaf) переменных из выделенных."""
|
||||
return [item.text(0) for item in self.get_selected_items() if item.text(0)]
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.completer.setWidget(None)
|
||||
self.completer.deleteLater()
|
||||
super().closeEvent(event)
|
||||
super().closeEvent(event)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# lookup by full path
|
||||
# ------------------------------------------------------------------
|
||||
def find_item_by_fullpath(self, path: str) -> Optional[QTreeWidgetItem]:
|
||||
return self._item_by_canon.get(canonical_key(path))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# tooltips
|
||||
# ------------------------------------------------------------------
|
||||
def _set_tool(self, item: QTreeWidgetItem, text: str):
|
||||
item.setToolTip(0, text)
|
||||
item.setToolTip(1, text)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# selection helpers
|
||||
# ------------------------------------------------------------------
|
||||
def get_all_items(self):
|
||||
"""Все leaf-узлы (подгружаем lazy)."""
|
||||
def collect_leaf(parent):
|
||||
leaves = []
|
||||
for i in range(parent.childCount()):
|
||||
ch = parent.child(i)
|
||||
if ch.isHidden():
|
||||
continue
|
||||
self.on_item_expanded(ch)
|
||||
if ch.childCount() == 0:
|
||||
t = ch.text(1)
|
||||
if t and 'bitfield' in t.lower():
|
||||
continue
|
||||
leaves.append(ch)
|
||||
else:
|
||||
leaves.extend(collect_leaf(ch))
|
||||
return leaves
|
||||
|
||||
out = []
|
||||
for i in range(self.tree.topLevelItemCount()):
|
||||
top = self.tree.topLevelItem(i)
|
||||
self.on_item_expanded(top)
|
||||
if top.childCount() == 0:
|
||||
t = top.text(1)
|
||||
if t and 'bitfield' in t.lower():
|
||||
continue
|
||||
out.append(top)
|
||||
else:
|
||||
out.extend(collect_leaf(top))
|
||||
return out
|
||||
|
||||
def _get_internal_selected_items(self):
|
||||
selected = self.tree.selectedItems()
|
||||
all_items = []
|
||||
def collect(item):
|
||||
self.on_item_expanded(item)
|
||||
res = [item]
|
||||
for i in range(item.childCount()):
|
||||
res.extend(collect(item.child(i)))
|
||||
return res
|
||||
for it in selected:
|
||||
all_items.extend(collect(it))
|
||||
return all_items
|
||||
|
||||
def get_selected_items(self):
|
||||
selected = self.tree.selectedItems()
|
||||
leaves = []
|
||||
for it in selected:
|
||||
self.on_item_expanded(it)
|
||||
if all(it.child(i).isHidden() or not it.child(i).isSelected() for i in range(it.childCount())):
|
||||
t = it.data(0, self.ROLE_NAME)
|
||||
if t and isinstance(t, str) and 'bitfield' in t.lower():
|
||||
continue
|
||||
leaves.append(it)
|
||||
return leaves
|
||||
|
||||
def get_all_var_names(self):
|
||||
return [it.data(0, self.ROLE_FULLPATH) for it in self.get_all_items() if it.data(0, self.ROLE_FULLPATH)]
|
||||
|
||||
def _get_internal_selected_var_names(self):
|
||||
return [it.data(0, self.ROLE_FULLPATH) for it in self._get_internal_selected_items() if it.data(0, self.ROLE_FULLPATH)]
|
||||
|
||||
def get_selected_var_names(self):
|
||||
return [it.data(0, self.ROLE_FULLPATH) for it in self.get_selected_items() if it.data(0, self.ROLE_FULLPATH)]
|
||||
|
||||
@@ -253,6 +253,8 @@ class VariableTableWidget(QTableWidget):
|
||||
ret_combo = CtrlScrollComboBox()
|
||||
ret_combo.addItems(self.iq_types)
|
||||
value = var['return_type'].replace('t_', '')
|
||||
if value not in self.iq_types:
|
||||
ret_combo.addItem(value)
|
||||
ret_combo.setCurrentText(value)
|
||||
ret_combo.currentTextChanged.connect(on_change_callback)
|
||||
ret_combo.setStyleSheet(style_with_padding)
|
||||
@@ -375,10 +377,9 @@ class VariableTableWidget(QTableWidget):
|
||||
combo.clear()
|
||||
|
||||
combo.addItems(allowed_items)
|
||||
if current in allowed_items:
|
||||
combo.setCurrentText(current)
|
||||
else:
|
||||
combo.setCurrentIndex(0)
|
||||
if current not in allowed_items:
|
||||
combo.addItem(current)
|
||||
combo.setCurrentText(current)
|
||||
combo.blockSignals(False)
|
||||
|
||||
|
||||
|
||||
253
debug_tools.c
253
debug_tools.c
@@ -9,6 +9,21 @@ DebugLowLevel_t debug_ll = DEBUG_LOWLEVEL_INIT; ///<
|
||||
|
||||
static int getDebugVar(DebugVar_t *var, int32_t *int_var, float *float_var);
|
||||
static int convertDebugVarToIQx(DebugVar_t *var, int32_t *ret_var);
|
||||
static int iqTypeToQ(DebugVarIQType_t t);
|
||||
static int is_addr_in_allowed_ranges(uint32_t addr_val, const AddrRange_t *ranges, int count);
|
||||
|
||||
/**
|
||||
* @brief Ìàññèâ äîïóñòèìûõ äèàïàçîíîâ àäðåñîâ äëÿ îòëàäî÷íîãî ÷òåíèÿ
|
||||
*
|
||||
* Âêëþ÷àåò â ñåáÿ íàáîð äèàïàçîíîâ ïàìÿòè, ðàçðåø¸ííûõ äëÿ äîñòóïà
|
||||
* ôóíêöèåé Debug_LowLevel_ReadVar.
|
||||
*/
|
||||
static const AddrRange_t debug_allowed_ranges[] = ALLOWED_ADDRESS_RANGES;
|
||||
/**
|
||||
* @brief Êîëè÷åñòâî ýëåìåíòîâ â ìàññèâå debug_allowed_ranges
|
||||
*/
|
||||
static const int debug_allowed_ranges_count = sizeof(debug_allowed_ranges) / sizeof(debug_allowed_ranges[0]);
|
||||
|
||||
|
||||
///////////////////////////----EXAPLE-----//////////////////////////////
|
||||
int var_numb = 1; ///< Ïðèìåð ïåðåìåííîé äëÿ îòëàäêè
|
||||
@@ -24,8 +39,9 @@ DateTime_t ext_date = {2025, 11, 07, 16, 50}; ///<
|
||||
*/
|
||||
void Debug_Test_Example(void)
|
||||
{
|
||||
return;
|
||||
result = Debug_ReadVar(var_numb, &return_var);
|
||||
result = Debug_ReadVarName(var_numb, var_name);
|
||||
result = Debug_ReadVarName(var_numb, var_name, 0);
|
||||
|
||||
|
||||
if(Debug_LowLevel_Initialize(&ext_date) == 0)
|
||||
@@ -43,19 +59,79 @@ void Debug_Test_Example(void)
|
||||
*/
|
||||
int Debug_ReadVar(int var_ind, int32_t *return_32b)
|
||||
{
|
||||
if(return_32b == NULL)
|
||||
return 1;
|
||||
int32_t tmp_var;
|
||||
|
||||
if(return_32b == NULL)
|
||||
return DEBUG_ERR_INTERNAL;
|
||||
if (var_ind >= DebugVar_Qnt)
|
||||
return 1;
|
||||
return DEBUG_ERR_VAR_NUMB;
|
||||
if((dbg_vars[var_ind].ptr_type == pt_struct) || (dbg_vars[var_ind].ptr_type == pt_union) ||
|
||||
(dbg_vars[var_ind].ptr_type == pt_unknown))
|
||||
return 1;
|
||||
return DEBUG_ERR_INVALID_VAR;
|
||||
|
||||
return convertDebugVarToIQx(&dbg_vars[var_ind], return_32b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ×èòàåò âîçâðàùàåìûé òèï (IQ) ïåðåìåííîé ïî èíäåêñó.
|
||||
* @param var_ind – èíäåêñ ïåðåìåííîé.
|
||||
* @param vartype – óêàçàòåëü äëÿ âîçâðàòà òèïà.
|
||||
* @return int – 0: óñïåõ, 1: îøèáêà.
|
||||
* @details Èñïîëüçóåòñÿ äëÿ ÷òåíèÿ âîçâðàùàåìîãî òèïà (IQ) ïåðåìåííûõ ïî èõ èíäåêñó.
|
||||
*/
|
||||
int Debug_ReadVarReturnType(int var_ind, int *vartype)
|
||||
{
|
||||
int rettype;
|
||||
if(vartype == NULL)
|
||||
return DEBUG_ERR_INTERNAL;
|
||||
if (var_ind >= DebugVar_Qnt)
|
||||
return DEBUG_ERR_VAR_NUMB;
|
||||
if((dbg_vars[var_ind].ptr_type == pt_struct) || (dbg_vars[var_ind].ptr_type == pt_union) ||
|
||||
(dbg_vars[var_ind].ptr_type == pt_unknown))
|
||||
return DEBUG_ERR_INVALID_VAR;
|
||||
|
||||
*vartype = iqTypeToQ(dbg_vars[var_ind].return_type);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ×èòàåò òèï ïåðåìåííîé ïî èíäåêñó.
|
||||
* @param var_ind – èíäåêñ ïåðåìåííîé.
|
||||
* @param vartype – óêàçàòåëü äëÿ âîçâðàòà òèïà.
|
||||
* @return int – 0: óñïåõ, 1: îøèáêà.
|
||||
* @details Èñïîëüçóåòñÿ äëÿ ÷òåíèÿ òèïà ïåðåìåííûõ ïî èõ èíäåêñó.
|
||||
*/
|
||||
int Debug_ReadVarType(int var_ind, int *vartype)
|
||||
{
|
||||
int rettype;
|
||||
if(vartype == NULL)
|
||||
return DEBUG_ERR_INTERNAL;
|
||||
if (var_ind >= DebugVar_Qnt)
|
||||
return DEBUG_ERR_VAR_NUMB;
|
||||
if((dbg_vars[var_ind].ptr_type == pt_struct) || (dbg_vars[var_ind].ptr_type == pt_union) ||
|
||||
(dbg_vars[var_ind].ptr_type == pt_unknown))
|
||||
return DEBUG_ERR_INVALID_VAR;
|
||||
|
||||
*vartype = dbg_vars[var_ind].ptr_type;
|
||||
|
||||
switch(dbg_vars[var_ind].ptr_type)
|
||||
{
|
||||
case pt_int8:
|
||||
case pt_int16:
|
||||
case pt_int32:
|
||||
case pt_float:
|
||||
*vartype = dbg_vars[var_ind].ptr_type | DEBUG_SIGNED_VAR;
|
||||
break;
|
||||
|
||||
default:
|
||||
*vartype = dbg_vars[var_ind].ptr_type;
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ×èòàåò èìÿ ïåðåìåííîé ïî èíäåêñó.
|
||||
* @param var_ind – èíäåêñ ïåðåìåííîé.
|
||||
@@ -63,21 +139,26 @@ int Debug_ReadVar(int var_ind, int32_t *return_32b)
|
||||
* @return int – 0: óñïåõ, 1: îøèáêà.
|
||||
* @details Êîïèðóåò èìÿ ïåðåìåííîé â ïðåäîñòàâëåííûé áóôåð.
|
||||
*/
|
||||
int Debug_ReadVarName(int var_ind, DebugVarName_t name_ptr)
|
||||
int Debug_ReadVarName(int var_ind, DebugVarName_t name_ptr, int *length)
|
||||
{
|
||||
int i;
|
||||
|
||||
if(name_ptr == NULL)
|
||||
return 1;
|
||||
return DEBUG_ERR_INTERNAL;
|
||||
|
||||
if (var_ind >= DebugVar_Qnt)
|
||||
return 1;
|
||||
return DEBUG_ERR_VAR_NUMB;
|
||||
|
||||
int i;
|
||||
// Êîïèðîâàíèå ñ çàùèòîé îò ïåðåïîëíåíèÿ è ÿâíîé îñòàíîâêîé ïî '\0'
|
||||
for (i = 0; i < sizeof(dbg_vars[var_ind].name); i++)
|
||||
{
|
||||
name_ptr[i] = dbg_vars[var_ind].name[i];
|
||||
if (dbg_vars[var_ind].name[i] == '\0')
|
||||
{
|
||||
if(length != NULL)
|
||||
*length = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Ãàðàíòèðîâàííîå çàâåðøåíèå ñòðîêè (íà ñëó÷àé, åñëè â var->name íå áûëî '\0')
|
||||
name_ptr[sizeof(dbg_vars[var_ind].name) - 1] = '\0';
|
||||
@@ -85,7 +166,6 @@ int Debug_ReadVarName(int var_ind, DebugVarName_t name_ptr)
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief ×èòàåò çíà÷åíèå ïåðåìåííîé îòëàäêè ñ íèæíåãî óðîâíÿ.
|
||||
* @param return_32b – óêàçàòåëü íà ïåðåìåííóþ, êóäà çàïèñûâàåòñÿ ðåçóëüòàò.
|
||||
@@ -94,26 +174,17 @@ int Debug_ReadVarName(int var_ind, DebugVarName_t name_ptr)
|
||||
*/
|
||||
int Debug_LowLevel_ReadVar(int32_t *return_32b)
|
||||
{
|
||||
if (return_32b == NULL)
|
||||
return 1;
|
||||
if (debug_ll.isVerified == 0)
|
||||
return 1;
|
||||
|
||||
uint8_t *addr = debug_ll.dbg_var.Ptr;
|
||||
uint32_t addr_val = (uint32_t)addr;
|
||||
|
||||
// Ðàçðåø¸ííûå äèàïàçîíû ïàìÿòè (èç .cmd ôàéëà)
|
||||
if (!(
|
||||
(addr_val <= 0x0007FF) || // RAMM0 + RAMM1
|
||||
(addr_val >= 0x008120 && addr_val <= 0x009FFC) || // L0 + L1 SARAM
|
||||
(addr_val >= 0x3F8000 && addr_val <= 0x3F9FFF) || // PRAMH0 + DRAMH0
|
||||
(addr_val >= 0x3FF000 && addr_val <= 0x3FFFFF) || // BOOTROM + RESET
|
||||
(addr_val >= 0x080002 && addr_val <= 0x09FFFF) || // RAMEX1
|
||||
(addr_val >= 0x0F0000 && addr_val <= 0x0FFEFF) || // RAMEX4
|
||||
(addr_val >= 0x100002 && addr_val <= 0x103FFF) || // RAMEX0 + RAMEX2 + RAMEX01
|
||||
(addr_val >= 0x102000 && addr_val <= 0x103FFF) // RAMEX2
|
||||
)) {
|
||||
return 2; // Çàïðåù¸ííûé àäðåñ — íåëüçÿ ÷èòàòü
|
||||
if (return_32b == NULL)
|
||||
return DEBUG_ERR_INTERNAL;
|
||||
if (debug_ll.isVerified == 0)
|
||||
return DEBUG_ERR_DATATIME;
|
||||
|
||||
if (is_addr_in_allowed_ranges(addr_val, debug_allowed_ranges, debug_allowed_ranges_count) != 0)
|
||||
{
|
||||
return DEBUG_ERR_ADDR; // Çàïðåù¸ííûé àäðåñ — íåëüçÿ ÷èòàòü
|
||||
}
|
||||
|
||||
return convertDebugVarToIQx(&debug_ll.dbg_var, return_32b);
|
||||
@@ -128,7 +199,7 @@ int Debug_LowLevel_ReadVar(int32_t *return_32b)
|
||||
int Debug_LowLevel_Initialize(DateTime_t* external_date)
|
||||
{
|
||||
if (external_date == NULL) {
|
||||
return -1;
|
||||
return DEBUG_ERR_INTERNAL;
|
||||
}
|
||||
|
||||
|
||||
@@ -144,10 +215,64 @@ int Debug_LowLevel_Initialize(DateTime_t* external_date)
|
||||
}
|
||||
debug_ll.isVerified = 0;
|
||||
|
||||
return 1; // Íå ñîâïàëî
|
||||
return DEBUG_ERR_DATATIME; // Íå ñîâïàëî
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief ×èòàåò âîçâðàùàåìûé òèï (IQ) íèçêîóðîâíåíî çàäàííîé ïåðåìåííîé.
|
||||
* @param var_ind – èíäåêñ ïåðåìåííîé.
|
||||
* @param vartype – óêàçàòåëü äëÿ âîçâðàòà òèïà.
|
||||
* @return int – 0: óñïåõ, 1: îøèáêà.
|
||||
* @details Èñïîëüçóåòñÿ äëÿ ÷òåíèÿ âîçâðàùàåìîãî òèïà (IQ) ïåðåìåííûõ ïî èõ èíäåêñó.
|
||||
*/
|
||||
int Debug_LowLevel_ReadVarReturnType(int *vartype)
|
||||
{
|
||||
int rettype;
|
||||
if(vartype == NULL)
|
||||
return DEBUG_ERR_INTERNAL;
|
||||
if((debug_ll.dbg_var.ptr_type == pt_struct) || (debug_ll.dbg_var.ptr_type == pt_union) ||
|
||||
(debug_ll.dbg_var.ptr_type == pt_unknown))
|
||||
return DEBUG_ERR_INVALID_VAR;
|
||||
|
||||
*vartype = iqTypeToQ(debug_ll.dbg_var.return_type);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ×èòàåò òèï íèçêîóðîâíåíî çàäàííîé ïåðåìåííîé.
|
||||
* @param var_ind – èíäåêñ ïåðåìåííîé.
|
||||
* @param vartype – óêàçàòåëü äëÿ âîçâðàòà òèïà.
|
||||
* @return int – 0: óñïåõ, 1: îøèáêà.
|
||||
*/
|
||||
int Debug_LowLevel_ReadVarType(int *vartype)
|
||||
{
|
||||
int rettype;
|
||||
if(vartype == NULL)
|
||||
return DEBUG_ERR_INTERNAL;
|
||||
if((debug_ll.dbg_var.ptr_type == pt_struct) || (debug_ll.dbg_var.ptr_type == pt_union) ||
|
||||
(debug_ll.dbg_var.ptr_type == pt_unknown))
|
||||
return DEBUG_ERR_INVALID_VAR;
|
||||
|
||||
*vartype = debug_ll.dbg_var.ptr_type;
|
||||
|
||||
switch(debug_ll.dbg_var.ptr_type)
|
||||
{
|
||||
case pt_int8:
|
||||
case pt_int16:
|
||||
case pt_int32:
|
||||
case pt_float:
|
||||
*vartype = debug_ll.dbg_var.ptr_type | DEBUG_SIGNED_VAR;
|
||||
break;
|
||||
|
||||
default:
|
||||
*vartype = debug_ll.dbg_var.ptr_type;
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -167,7 +292,7 @@ static int iqTypeToQ(DebugVarIQType_t t)
|
||||
else if (t >= t_iq1 && t <= t_iq30)
|
||||
return (int)t - (int)t_iq1 + 1; // íàïðèìåð t_iq1 -> 1, t_iq2 -> 2 è ò.ä.
|
||||
else
|
||||
return -1; // îøèáêà
|
||||
return 0; // îøèáêà
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,26 +305,25 @@ static int iqTypeToQ(DebugVarIQType_t t)
|
||||
static int convertDebugVarToIQx(DebugVar_t *var, int32_t *ret_var)
|
||||
{
|
||||
int32_t iq_numb, iq_united, iq_final;
|
||||
int64_t iq_united64 = 0;
|
||||
int64_t iq_final64 = 0;
|
||||
int status;
|
||||
|
||||
float float_numb;
|
||||
|
||||
if(getDebugVar(var, &iq_numb, &float_numb) != 0)
|
||||
return 1;
|
||||
status = getDebugVar(var, &iq_numb, &float_numb);
|
||||
if(status != 0)
|
||||
return status;
|
||||
|
||||
int src_q = iqTypeToQ(var->iq_type);
|
||||
int dst_q = iqTypeToQ(var->return_type);
|
||||
|
||||
if (src_q < 0 || dst_q < 0)
|
||||
return 2; // íåïðàâèëüíûé ôîðìàò
|
||||
|
||||
int64_t iq_united64 = 0;
|
||||
int64_t iq_final64 = 0;
|
||||
|
||||
// Êîíâåðòàöèÿ ê GLOBAL_Q (64-áèò)
|
||||
if (var->iq_type == t_iq_none) {
|
||||
if (var->ptr_type == pt_float) {
|
||||
// float_numb óìíîæàåì íà 2^GLOBAL_Q
|
||||
// Ðåçóëüòàò ïðèâîäèì ê 64 áèòà
|
||||
iq_united64 = (int64_t)(float_numb * (1 << GLOBAL_Q));
|
||||
iq_united64 = (int64_t)(float_numb * ((uint32_t)1 << GLOBAL_Q));
|
||||
} else {
|
||||
iq_united64 = ((int64_t)iq_numb) << GLOBAL_Q;
|
||||
}
|
||||
@@ -222,11 +346,7 @@ static int convertDebugVarToIQx(DebugVar_t *var, int32_t *ret_var)
|
||||
else
|
||||
iq_final64 = iq_united64 >> (-shift);
|
||||
|
||||
// Ïðîâåðÿåì ïåðåïîëíåíèå int32_t
|
||||
if (iq_final64 > 2147483647 || iq_final64 < -2147483648)
|
||||
return 3; // ïåðåïîëíåíèå
|
||||
|
||||
*ret_var = (uint32_t)iq_final64;
|
||||
*ret_var = (int32_t)iq_final64;
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -242,52 +362,55 @@ static int convertDebugVarToIQx(DebugVar_t *var, int32_t *ret_var)
|
||||
*/
|
||||
static int getDebugVar(DebugVar_t *var, int32_t *int_var, float *float_var)
|
||||
{
|
||||
if (!var || !int_var || !float_var || !var->Ptr)
|
||||
return 1; // îøèáêà: null óêàçàòåëü
|
||||
|
||||
uint8_t *addr = var->Ptr;
|
||||
uint32_t addr_val = (uint32_t)addr;
|
||||
|
||||
if (!var || !int_var || !float_var || !var->Ptr)
|
||||
return DEBUG_ERR_INTERNAL; // îøèáêà: null óêàçàòåëü
|
||||
|
||||
switch (var->ptr_type)
|
||||
{
|
||||
case pt_int8: // 8 áèò
|
||||
if ((addr_val & ALIGN_8BIT) != 0) // ïðîâåðÿåì âûðàâíèâàíèå
|
||||
return 1; // îøèáêà âûðàâíèâàíèÿ
|
||||
return DEBUG_ERR_ADDR_ALIGN; // îøèáêà âûðàâíèâàíèÿ
|
||||
*int_var = *((volatile int8_t *)addr);
|
||||
break;
|
||||
case pt_uint8:
|
||||
if ((addr_val & ALIGN_8BIT) != 0) // ïðîâåðÿåì âûðàâíèâàíèå
|
||||
return 1; // îøèáêà âûðàâíèâàíèÿ
|
||||
return DEBUG_ERR_ADDR_ALIGN; // îøèáêà âûðàâíèâàíèÿ
|
||||
*int_var = *((volatile uint8_t *)addr);
|
||||
break;
|
||||
|
||||
case pt_int16: // 16 áèò (int)
|
||||
if ((addr_val & ALIGN_16BIT) != 0) // ïðîâåðÿåì âûðàâíèâàíèå
|
||||
return 2; // îøèáêà âûðàâíèâàíèÿ
|
||||
return DEBUG_ERR_ADDR_ALIGN; // îøèáêà âûðàâíèâàíèÿ
|
||||
*int_var = *((volatile int16_t *)addr);
|
||||
break;
|
||||
case pt_uint16:
|
||||
if ((addr_val & ALIGN_16BIT) != 0) // ïðîâåðÿåì âûðàâíèâàíèå
|
||||
return 2; // îøèáêà âûðàâíèâàíèÿ
|
||||
return DEBUG_ERR_ADDR_ALIGN; // îøèáêà âûðàâíèâàíèÿ
|
||||
*int_var = *((volatile uint16_t *)addr);
|
||||
break;
|
||||
|
||||
case pt_int32: // 32 áèò
|
||||
if ((addr_val & ALIGN_32BIT) != 0) // ïðîâåðÿåì âûðàâíèâàíèå
|
||||
return 3; // îøèáêà âûðàâíèâàíèÿ
|
||||
return DEBUG_ERR_ADDR_ALIGN; // îøèáêà âûðàâíèâàíèÿ
|
||||
*int_var = *((volatile int32_t *)addr);
|
||||
break;
|
||||
case pt_uint32:
|
||||
if ((addr_val & ALIGN_32BIT) != 0) // ïðîâåðÿåì âûðàâíèâàíèå
|
||||
return 3; // îøèáêà âûðàâíèâàíèÿ
|
||||
return DEBUG_ERR_ADDR_ALIGN; // îøèáêà âûðàâíèâàíèÿ
|
||||
*int_var = *((volatile uint32_t *)addr);
|
||||
break;
|
||||
|
||||
case pt_float: // float (4 áàéòà)
|
||||
if ((addr_val & ALIGN_FLOAT) != 0) // ïðîâåðêà âûðàâíèâàíèÿ
|
||||
return 4; // îøèáêà âûðàâíèâàíèÿ
|
||||
return DEBUG_ERR_ADDR_ALIGN; // îøèáêà âûðàâíèâàíèÿ
|
||||
*float_var = *((volatile float *)addr);
|
||||
break;
|
||||
|
||||
default:
|
||||
return 1; // íåïîääåðæèâàåìûé òèï
|
||||
return DEBUG_ERR_INVALID_VAR; // íåïîääåðæèâàåìûé òèï
|
||||
// äëÿ óêàçàòåëåé è ìàññèâîâ íå ïîääåðæèâàåòñÿ ÷òåíèå
|
||||
// case pt_ptr_int8:
|
||||
// case pt_ptr_int16:
|
||||
@@ -306,3 +429,23 @@ static int getDebugVar(DebugVar_t *var, int32_t *int_var, float *float_var)
|
||||
return 0; // óñïåõ
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief Ïðîâåðÿåò, âõîäèò ëè àäðåñ â îäèí èç äîïóñòèìûõ äèàïàçîíîâ
|
||||
*
|
||||
* @param addr_val - Çíà÷åíèå àäðåñà äëÿ ïðîâåðêè
|
||||
* @param ranges - Óêàçàòåëü íà ìàññèâ äèàïàçîíîâ AddrRange_t
|
||||
* @param count - Êîëè÷åñòâî äèàïàçîíîâ â ìàññèâå
|
||||
* @return 0 åñëè àäðåñ íàõîäèòñÿ â îäíîì èç äèàïàçîíîâ, èíà÷å 1
|
||||
*/
|
||||
static int is_addr_in_allowed_ranges(uint32_t addr_val, const AddrRange_t *ranges, int count)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
if (addr_val >= ranges[i].start && addr_val <= ranges[i].end) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
|
||||
|
||||
|
||||
#define ALLOWED_ADDRESS_RANGES { \
|
||||
{0x000000, 0x0007FF}, \
|
||||
{0x008120, 0x009FFC}, \
|
||||
{0x3F8000, 0x3F9FFF}, \
|
||||
{0x3FF000, 0x3FFFFF}, \
|
||||
{0x080002, 0x09FFFF}, \
|
||||
{0x0F0000, 0x0FFEFF}, \
|
||||
{0x100002, 0x103FFF}, \
|
||||
{0x102000, 0x103FFF} \
|
||||
}
|
||||
|
||||
|
||||
#if UINT8_MAX // Åñëè åñòü òèï 8 áèò - çí÷à÷èò àäðåñàöèÿ ïî 8 áèò
|
||||
|
||||
#define ALIGN_8BIT 0x0 ///< Âûðàâíèâàíèå áåç îãðàíè÷åíèé (ëþáîé àäðåñ)
|
||||
@@ -31,6 +43,18 @@
|
||||
#define NULL 0
|
||||
#endif
|
||||
|
||||
#define DEBUG_SIGNED_VAR (1<<7)
|
||||
|
||||
#define DEBUG_OK (0)
|
||||
#define DEBUG_ERR (1<<7)
|
||||
#define DEBUG_ERR_VAR_NUMB (1<<0) | DEBUG_ERR
|
||||
#define DEBUG_ERR_INVALID_VAR (1<<1) | DEBUG_ERR
|
||||
#define DEBUG_ERR_ADDR (1<<2) | DEBUG_ERR
|
||||
#define DEBUG_ERR_ADDR_ALIGN (1<<3) | DEBUG_ERR
|
||||
#define DEBUG_ERR_INTERNAL (1<<4) | DEBUG_ERR
|
||||
#define DEBUG_ERR_DATATIME (1<<5) | DEBUG_ERR
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @brief Òèï äàííûõ, íà êîòîðûé óêàçûâàåò óêàçàòåëü ïåðåìåííîé îòëàäêè.
|
||||
@@ -129,6 +153,13 @@ typedef struct {
|
||||
uint8_t minute; ///< Ìèíóòû (0-59)
|
||||
} DateTime_t;
|
||||
|
||||
/**
|
||||
* @brief Ñòðóêòóðà, îïèñûâàþùàÿ äèàïàçîí àäðåñîâ ïàìÿòè.
|
||||
*/
|
||||
typedef struct {
|
||||
uint32_t start; ///< Íà÷àëüíûé àäðåñ äèàïàçîíà
|
||||
uint32_t end; ///< Êîíå÷íûé àäðåñ äèàïàçîíà (âêëþ÷èòåëüíî)
|
||||
} AddrRange_t;
|
||||
/**
|
||||
* @brief Ñòðóêòóðà íèæíåãî óðîâíÿ îòëàäêè.
|
||||
*/
|
||||
@@ -159,10 +190,19 @@ void Debug_Test_Example(void);
|
||||
/* ×èòàåò çíà÷åíèå ïåðåìåííîé ïî èíäåêñó */
|
||||
int Debug_ReadVar(int var_ind, int32_t *return_long);
|
||||
/* ×èòàåò èìÿ ïåðåìåííîé ïî èíäåêñó */
|
||||
int Debug_ReadVarName(int var_ind, DebugVarName_t name_ptr);
|
||||
int Debug_ReadVarName(int var_ind, DebugVarName_t name_ptr, int *length);
|
||||
/* ×èòàåò âîçâðàùàåìûé òèï (IQ) ïåðåìåííîé ïî èíäåêñó */
|
||||
int Debug_ReadVarReturnType(int var_ind, int *vartype);
|
||||
/* ×èòàåò òèï ïåðåìåííîé ïî èíäåêñó */
|
||||
int Debug_ReadVarType(int var_ind, int *vartype);
|
||||
|
||||
|
||||
/* ×èòàåò çíà÷åíèå ïåðåìåííîé ñ íèæíåãî óðîâíÿ */
|
||||
int Debug_LowLevel_ReadVar(int32_t *return_long);
|
||||
/* Èíèöèàëèçèðóåò îòëàäêó íèæíåãî óðîâíÿ */
|
||||
int Debug_LowLevel_Initialize(DateTime_t *external_date);
|
||||
|
||||
/* ×èòàåò âîçâðàùàåìûé òèï (IQ) íèçêîóðîâíåíî çàäàííîé ïåðåìåííîé */
|
||||
int Debug_LowLevel_ReadVarReturnType(int *vartype);
|
||||
/* ×èòàåò òèï íèçêîóðîâíåíî çàäàííîé ïåðåìåííîé.*/
|
||||
int Debug_LowLevel_ReadVarType(int *vartype);
|
||||
#endif //DEBUG_TOOLS
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
#include "debug_tools.h"
|
||||
|
||||
|
||||
// Инклюды для доступа к переменным
|
||||
#include "bender.h"
|
||||
|
||||
// Экстерны для доступа к переменным
|
||||
extern int ADC0finishAddr;
|
||||
|
||||
|
||||
// Определение массива с указателями на переменные для отладки
|
||||
int DebugVar_Qnt = 5;
|
||||
#pragma DATA_SECTION(dbg_vars,".dbgvar_info")
|
||||
// pointer_type iq_type return_iq_type short_name
|
||||
DebugVar_t dbg_vars[] = {\
|
||||
{(uint8_t *)&freqTerm, pt_float, t_iq_none, t_iq10, "freqT" }, \
|
||||
{(uint8_t *)&ADC_sf[0][0], pt_int16, t_iq_none, t_iq_none, "ADC_sf00" }, \
|
||||
{(uint8_t *)&ADC_sf[0][1], pt_int16, t_iq_none, t_iq_none, "ADC_sf01" }, \
|
||||
{(uint8_t *)&ADC_sf[0][2], pt_int16, t_iq_none, t_iq_none, "ADC_sf02" }, \
|
||||
{(uint8_t *)&ADC_sf[0][3], pt_int16, t_iq_none, t_iq_none, "ADC_sf03" }, \
|
||||
{(uint8_t *)&Bender[0].KOhms, pt_uint16, t_iq, t_iq10, "Bend0.KOhm" }, \
|
||||
{(uint8_t *)&Bender[0].Times, pt_uint16, t_iq_none, t_iq_none, "Bend0.Time" }, \
|
||||
};
|
||||
352
parse_xml/Src/parse_xml.py
Normal file
352
parse_xml/Src/parse_xml.py
Normal file
@@ -0,0 +1,352 @@
|
||||
# pyinstaller --onefile --distpath . --workpath ./build --specpath ./build parse_xml.py
|
||||
# python -m nuitka --standalone --onefile --output-dir=./build parse_xml.py
|
||||
import xml.etree.ElementTree as ET
|
||||
import xml.dom.minidom
|
||||
import sys
|
||||
import os
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: python simplify_dwarf.py <input.xml> <info.txt> [output.xml]")
|
||||
sys.exit(1)
|
||||
|
||||
input_path = sys.argv[1]
|
||||
info_path = sys.argv[2]
|
||||
|
||||
if len(sys.argv) >= 4:
|
||||
output_path = sys.argv[3]
|
||||
else:
|
||||
input_dir = os.path.dirname(os.path.abspath(input_path))
|
||||
output_path = os.path.join(input_dir, "simplified.xml")
|
||||
|
||||
tree = ET.parse(input_path)
|
||||
root = tree.getroot()
|
||||
|
||||
def extract_timestamp(info_path):
|
||||
with open(info_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
if "Time Stamp:" in line:
|
||||
parts = line.split("Time Stamp:")
|
||||
if len(parts) > 1:
|
||||
timestamp = parts[1].strip()
|
||||
return timestamp
|
||||
|
||||
|
||||
|
||||
die_by_id = {die.attrib.get("id"): die for die in root.iter("die") if "id" in die.attrib}
|
||||
|
||||
def get_attr(die, attr_type):
|
||||
for attr in die.findall("attribute"):
|
||||
type_elem = attr.find("type")
|
||||
if type_elem is not None and type_elem.text == attr_type:
|
||||
return attr.find("value")
|
||||
return None
|
||||
|
||||
def get_die_size(die):
|
||||
"""Вернуть размер DIE в байтах из атрибута DW_AT_byte_size."""
|
||||
for attr in die.findall("attribute"):
|
||||
type_elem = attr.find("type")
|
||||
if type_elem is not None and type_elem.text == "DW_AT_byte_size":
|
||||
const_elem = attr.find("value/const")
|
||||
if const_elem is not None:
|
||||
return int(const_elem.text, 0)
|
||||
return None
|
||||
|
||||
def resolve_type_die(type_id):
|
||||
"""Получить DIE типа, разрешая typedef, const и volatile."""
|
||||
visited = set()
|
||||
while type_id and type_id not in visited:
|
||||
visited.add(type_id)
|
||||
die = die_by_id.get(type_id)
|
||||
if die is None:
|
||||
return None
|
||||
tag = die.findtext("tag")
|
||||
if tag in ("DW_TAG_volatile_type", "DW_TAG_const_type", "DW_TAG_typedef", "DW_TAG_TI_far_type"):
|
||||
ref = get_attr(die, "DW_AT_type")
|
||||
if ref is not None and ref.find("ref") is not None:
|
||||
type_id = ref.find("ref").attrib.get("idref")
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return die
|
||||
return None
|
||||
|
||||
# Словарь для простых базовых типов по тегам (пример)
|
||||
base_types_map = {
|
||||
"DW_TAG_base_type": lambda die: die.find("attribute[@type='DW_AT_name']/value/string").text if die.find("attribute[@type='DW_AT_name']/value/string") is not None else "unknown",
|
||||
"DW_TAG_structure_type": lambda die: "struct",
|
||||
"DW_TAG_union_type": lambda die: "union",
|
||||
"DW_TAG_pointer_type": lambda die: "pointer",
|
||||
"DW_TAG_array_type": lambda die: "array",
|
||||
}
|
||||
|
||||
def get_type_name(type_id):
|
||||
die = resolve_type_die(type_id)
|
||||
if die is None:
|
||||
return "unknown"
|
||||
|
||||
tag = die.findtext("tag")
|
||||
|
||||
if tag == "DW_TAG_pointer_type":
|
||||
ref = get_attr(die, "DW_AT_type")
|
||||
if ref is not None and ref.find("ref") is not None:
|
||||
pointee_id = ref.find("ref").attrib.get("idref")
|
||||
name = get_type_name(pointee_id)
|
||||
return name + "*" if name != "unknown" else name
|
||||
else:
|
||||
return "void*"
|
||||
|
||||
elif tag == "DW_TAG_base_type":
|
||||
name_attr = get_attr(die, "DW_AT_name")
|
||||
if name_attr is not None:
|
||||
return name_attr.findtext("string")
|
||||
else:
|
||||
return "base_type_unknown"
|
||||
|
||||
elif tag == "DW_TAG_structure_type":
|
||||
name_attr = get_attr(die, "DW_AT_name")
|
||||
name = name_attr.findtext("string") if name_attr is not None else "anonymous_struct"
|
||||
return f"struct {name}"
|
||||
|
||||
elif tag == "DW_TAG_union_type":
|
||||
name_attr = get_attr(die, "DW_AT_name")
|
||||
name = name_attr.findtext("string") if name_attr is not None else "anonymous_union"
|
||||
return f"union {name}"
|
||||
|
||||
elif tag == "DW_TAG_array_type":
|
||||
ref = get_attr(die, "DW_AT_type")
|
||||
if ref is not None and ref.find("ref") is not None:
|
||||
element_type_id = ref.find("ref").attrib.get("idref")
|
||||
element_type_name = get_type_name(element_type_id)
|
||||
return f"{element_type_name}[]"
|
||||
else:
|
||||
return "array[]"
|
||||
|
||||
# Добавляем поддержку enum
|
||||
elif tag == "DW_TAG_enumeration_type":
|
||||
name_attr = get_attr(die, "DW_AT_name")
|
||||
name = name_attr.findtext("string") if name_attr is not None else "anonymous_enum"
|
||||
return f"enum {name}"
|
||||
|
||||
else:
|
||||
return "unknown"
|
||||
|
||||
def parse_offset(offset_text):
|
||||
if offset_text and offset_text.startswith("DW_OP_plus_uconst "):
|
||||
return int(offset_text.split()[-1], 0)
|
||||
return 0
|
||||
|
||||
|
||||
def get_array_dimensions(array_die):
|
||||
"""Рекурсивно получить размеры всех измерений массива из DIE с тегом DW_TAG_array_type."""
|
||||
dims = []
|
||||
|
||||
# Ищем размер текущего измерения
|
||||
# Размер может быть в DW_AT_upper_bound, либо вычисляться из DW_AT_byte_size и типа элемента
|
||||
# Но часто в DWARF размер указывается через дочерние die с тегом DW_TAG_subrange_type
|
||||
|
||||
subrange = None
|
||||
for child in array_die.findall("die"):
|
||||
if child.findtext("tag") == "DW_TAG_subrange_type":
|
||||
subrange = child
|
||||
break
|
||||
|
||||
dim_size = None
|
||||
if subrange is not None:
|
||||
# Ищем атрибут DW_AT_upper_bound
|
||||
ub_attr = get_attr(subrange, "DW_AT_upper_bound")
|
||||
if ub_attr is not None:
|
||||
val = ub_attr.find("value/const")
|
||||
if val is not None:
|
||||
# Размер измерения равен верхней границе + 1 (т.к. верхняя граница индексируется с 0)
|
||||
dim_size = int(val.text, 0) + 1
|
||||
|
||||
if dim_size is None:
|
||||
# Если размер не нашли, попробуем вычислить через общий размер / размер элемента
|
||||
arr_size = get_die_size(array_die)
|
||||
element_type_ref = get_attr(array_die, "DW_AT_type")
|
||||
if element_type_ref is not None and element_type_ref.find("ref") is not None:
|
||||
element_type_id = element_type_ref.find("ref").attrib.get("idref")
|
||||
element_type_die = resolve_type_die(element_type_id)
|
||||
elem_size = get_die_size(element_type_die) if element_type_die is not None else None
|
||||
|
||||
if arr_size is not None and elem_size:
|
||||
dim_size = arr_size // elem_size
|
||||
|
||||
if dim_size is None:
|
||||
dim_size = 0 # Неизвестно
|
||||
|
||||
dims.append(dim_size)
|
||||
|
||||
# Рекурсивно проверяем, если элемент типа тоже массив (многомерный)
|
||||
element_type_ref = get_attr(array_die, "DW_AT_type")
|
||||
if element_type_ref is not None and element_type_ref.find("ref") is not None:
|
||||
element_type_id = element_type_ref.find("ref").attrib.get("idref")
|
||||
element_type_die = resolve_type_die(element_type_id)
|
||||
if element_type_die is not None and element_type_die.findtext("tag") == "DW_TAG_array_type":
|
||||
dims.extend(get_array_dimensions(element_type_die))
|
||||
|
||||
return dims
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def handle_array_type(member_elem, resolved_type, offset=0):
|
||||
dims = get_array_dimensions(resolved_type)
|
||||
|
||||
# Получаем элементарный тип массива (наибольший элемент в цепочке массивов)
|
||||
def get_base_element_type(die):
|
||||
ref = get_attr(die, "DW_AT_type")
|
||||
if ref is not None and ref.find("ref") is not None:
|
||||
type_id = ref.find("ref").attrib.get("idref")
|
||||
type_die = resolve_type_die(type_id)
|
||||
if type_die is not None and type_die.findtext("tag") == "DW_TAG_array_type":
|
||||
return get_base_element_type(type_die)
|
||||
else:
|
||||
return type_die
|
||||
return None
|
||||
|
||||
element_type_die = get_base_element_type(resolved_type)
|
||||
element_type_name = get_type_name(element_type_die.attrib.get("id")) if element_type_die is not None else "unknown"
|
||||
|
||||
# Формируем строку типа с нужным количеством []
|
||||
type_with_array = element_type_name + "[]" * len(dims)
|
||||
member_elem.set("type", type_with_array)
|
||||
|
||||
# Размер всего массива
|
||||
arr_size = get_die_size(resolved_type)
|
||||
if arr_size is not None:
|
||||
member_elem.set("size", str(arr_size))
|
||||
|
||||
# Добавляем атрибуты size1, size2, ...
|
||||
for i, dim in enumerate(dims, 1):
|
||||
member_elem.set(f"size{i}", str(dim))
|
||||
|
||||
member_elem.set("kind", "array")
|
||||
|
||||
# Если базовый элемент - структура, рекурсивно добавляем её члены
|
||||
if element_type_die is not None and element_type_die.findtext("tag") == "DW_TAG_structure_type":
|
||||
add_members_recursive(member_elem, element_type_die, offset)
|
||||
|
||||
|
||||
|
||||
|
||||
def add_members_recursive(parent_elem, struct_die, base_offset=0):
|
||||
tag = struct_die.findtext("tag")
|
||||
is_union = tag == "DW_TAG_union_type"
|
||||
|
||||
|
||||
# Получаем размер структуры/объединения
|
||||
size = get_die_size(struct_die)
|
||||
if size is not None:
|
||||
parent_elem.set("size", hex(size))
|
||||
|
||||
|
||||
for member in struct_die.findall("die"):
|
||||
if member.findtext("tag") != "DW_TAG_member":
|
||||
continue
|
||||
|
||||
name_attr = get_attr(member, "DW_AT_name")
|
||||
offset_attr = get_attr(member, "DW_AT_data_member_location")
|
||||
type_attr = get_attr(member, "DW_AT_type")
|
||||
|
||||
if name_attr is None or offset_attr is None or type_attr is None:
|
||||
continue
|
||||
|
||||
name = name_attr.findtext("string")
|
||||
offset_text = offset_attr.findtext("block")
|
||||
offset = parse_offset(offset_text) + base_offset
|
||||
type_id = type_attr.find("ref").attrib.get("idref")
|
||||
resolved_type = resolve_type_die(type_id)
|
||||
type_name = get_type_name(type_id)
|
||||
|
||||
if type_name == "unknown":
|
||||
continue
|
||||
|
||||
member_elem = ET.SubElement(
|
||||
parent_elem, "member", name=name, offset=hex(offset), type=type_name
|
||||
)
|
||||
|
||||
if is_union:
|
||||
member_elem.set("kind", "union")
|
||||
|
||||
if resolved_type is not None:
|
||||
subtag = resolved_type.findtext("tag")
|
||||
|
||||
# Обработка массива
|
||||
if subtag == "DW_TAG_array_type":
|
||||
handle_array_type(member_elem, resolved_type, offset)
|
||||
# Обработка структур и объединений
|
||||
elif subtag in ("DW_TAG_structure_type", "DW_TAG_union_type"):
|
||||
member_elem.set("type", type_name)
|
||||
add_members_recursive(member_elem, resolved_type, offset)
|
||||
else:
|
||||
member_elem.set("type", type_name)
|
||||
|
||||
|
||||
output_root = ET.Element("variables")
|
||||
for die in root.iter("die"):
|
||||
if die.findtext("tag") != "DW_TAG_variable":
|
||||
continue
|
||||
|
||||
name_attr = get_attr(die, "DW_AT_name")
|
||||
addr_attr = get_attr(die, "DW_AT_location")
|
||||
type_attr = get_attr(die, "DW_AT_type")
|
||||
|
||||
if name_attr is None or addr_attr is None or type_attr is None:
|
||||
continue
|
||||
|
||||
name = name_attr.findtext("string")
|
||||
|
||||
# Пропускаем переменные с '$' в имени
|
||||
if "$" in name:
|
||||
continue
|
||||
|
||||
addr_text = addr_attr.findtext("block")
|
||||
if not addr_text or not addr_text.startswith("DW_OP_addr "):
|
||||
continue
|
||||
|
||||
addr = int(addr_text.split()[-1], 0)
|
||||
type_id = type_attr.find("ref").attrib.get("idref")
|
||||
resolved_type = resolve_type_die(type_id)
|
||||
type_name = get_type_name(type_id)
|
||||
# Пропускаем переменные, находящиеся в памяти периферии
|
||||
if 0x800 <= addr < 0x8000:
|
||||
continue
|
||||
|
||||
# Проверка на DW_TAG_subroutine_type - пропускаем такие переменные
|
||||
if type_name == "unknown":
|
||||
continue
|
||||
|
||||
var_elem = ET.SubElement(output_root, "variable", name=name, address=hex(addr), type=type_name)
|
||||
if resolved_type is not None:
|
||||
tag = resolved_type.findtext("tag")
|
||||
|
||||
if tag == "DW_TAG_array_type":
|
||||
handle_array_type(var_elem, resolved_type)
|
||||
|
||||
elif tag in ("DW_TAG_structure_type", "DW_TAG_union_type"):
|
||||
add_members_recursive(var_elem, resolved_type)
|
||||
|
||||
|
||||
timestamp = extract_timestamp(info_path)
|
||||
|
||||
# Создаём новый элемент <timestamp> с текстом timestamp
|
||||
timestamp_elem = ET.Element("timestamp")
|
||||
timestamp_elem.text = timestamp
|
||||
|
||||
# Вставляем тег timestamp в начало (или куда хочешь)
|
||||
output_root.insert(0, timestamp_elem) # В начало списка дочерних элементов
|
||||
|
||||
# Красивый вывод
|
||||
|
||||
rough_string = ET.tostring(output_root, encoding="utf-8")
|
||||
reparsed = xml.dom.minidom.parseString(rough_string)
|
||||
pretty_xml = reparsed.toprettyxml(indent=" ")
|
||||
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write(pretty_xml)
|
||||
|
||||
os.remove(input_path)
|
||||
os.remove(info_path)
|
||||
print(f"Simplified and formatted XML saved to: {output_path}")
|
||||
BIN
parse_xml/parse_xml.exe
Normal file
BIN
parse_xml/parse_xml.exe
Normal file
Binary file not shown.
Reference in New Issue
Block a user