8 Commits

Author SHA1 Message Date
Razvalyaev
788ad19464 кууууча всего по терминалке, надо резгребать и структурировать
базово:
+сделан lowlevel для кучи переменных (пока работает медленно)
+сделан сохранение принимаемых значений в лог
+ gui терминалок подогнаны под один стиль плюс минус
2025-07-22 18:05:12 +03:00
Razvalyaev
96496a0256 все неплохо работает.
сейв перед попыткой улучшить lowlevel debug
2025-07-21 13:40:52 +03:00
Razvalyaev
f89aff1b1c Улучшена скорость полла Watch (переделано формирование таблицы) 2025-07-19 18:17:00 +03:00
Razvalyaev
6830743477 Начата работа над lowlevel терминалкой (по адресам из xml) 2025-07-19 18:01:40 +03:00
Razvalyaev
171f176d63 добавлен exe для парса всех переменных из .out 2025-07-19 11:36:32 +03:00
Razvalyaev
f2c4b7b3cd структурирован код debug_tools
доработана демо-терминалка для считывания tms переменных и встроена в DebugVarEdit
2025-07-19 10:56:46 +03:00
Razvalyaev
c94a7e711c сделана бета терминалка для опроса переменных 2025-07-18 17:43:23 +03:00
Razvalyaev
5be6343c33 бета терминалка для опроса 2025-07-18 16:47:18 +03:00
17 changed files with 3662 additions and 585 deletions

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@
/DebugVarEdit_GUI.build /DebugVarEdit_GUI.build
/DebugVarEdit_GUI.dist /DebugVarEdit_GUI.dist
/DebugVarEdit_GUI.onefile-build /DebugVarEdit_GUI.onefile-build
/parse_xml/build/

BIN
DebugTools.rar Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -7,6 +7,7 @@ import subprocess
import lxml.etree as ET import lxml.etree as ET
from generate_debug_vars import type_map, choose_type_map from generate_debug_vars import type_map, choose_type_map
from enum import IntEnum from enum import IntEnum
from tms_debugvar_term import _DemoWindow
import threading import threading
from generate_debug_vars import run_generate from generate_debug_vars import run_generate
import var_setup import var_setup
@@ -17,6 +18,7 @@ import scan_vars
import myXML import myXML
import time import time
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QApplication, QWidget, QTableWidget, QTableWidgetItem, QApplication, QWidget, QTableWidget, QTableWidgetItem,
QCheckBox, QComboBox, QLineEdit, QVBoxLayout, QHBoxLayout, QPushButton, 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_tms)
self.target_menu.addAction(self.action_stm) 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.target_menu)
menubar.addMenu(self.terminal_menu)
# Кнопка сохранения # Кнопка сохранения
btn_save = QPushButton(build_title) btn_save = QPushButton(build_title)
@@ -178,6 +191,15 @@ class VarEditor(QWidget):
self.setLayout(layout) 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): def on_target_selected(self, target):
self.target_menu.setTitle(f'МК: {target}') self.target_menu.setTitle(f'МК: {target}')

438
Src/allvars_xml_parser.py Normal file
View File

@@ -0,0 +1,438 @@
from __future__ import annotations
import sys
import re
import xml.etree.ElementTree as ET
import var_setup
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
from PySide2.QtWidgets import (
QDialog, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QPushButton,
QLineEdit, QLabel, QHeaderView, QCompleter, QCheckBox, QHBoxLayout, QSizePolicy,
QTableWidget, QTableWidgetItem, QFileDialog, QWidget, QMessageBox, QApplication, QMainWindow
)
from PySide2 import QtCore, QtGui
from path_hints import PathHints
from generate_debug_vars import choose_type_map, type_map
from var_selector_window import VariableSelectorDialog
from typing import List, Tuple, Optional, Dict, Any, Set
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:
"""
Reads your XML and outputs a flat list of paths:
- Arrays -> name[i], multilevel -> name[i][j]
- Pointer to struct -> children via '->'
- Regular struct -> children via '.'
"""
# assumed primitive sizes (for STM/MCU: int=2)
_PRIM_SIZE = {
'char':1, 'signed char':1, 'unsigned char':1, 'uint8_t':1, 'int8_t':1,
'short':2, 'short int':2, 'signed short':2, 'unsigned short':2,
'uint16_t':2, 'int16_t':2,
'int':2, 'signed int':2, 'unsigned int':2,
'long':4, 'unsigned long':4, 'int32_t':4, 'uint32_t':4,
'float':4,
'long long':8, 'unsigned long long':8, 'int64_t':8, 'uint64_t':8, 'double':8,
}
def __init__(self, path: str):
self.path = path
self.timestamp: str = ''
self.variables: List[VariableNode] = []
choose_type_map(0)
self._parse()
# ------------------ low helpers ------------------
@staticmethod
def _parse_int_guess(txt: Optional[str]) -> Optional[int]:
if not txt:
return None
txt = txt.strip()
if txt.startswith(('0x','0X')):
return int(txt, 16)
# если в строке есть буквы A-F → возможно hex
if any(c in 'abcdefABCDEF' for c in txt):
try:
return int(txt, 16)
except ValueError:
pass
try:
return int(txt, 10)
except ValueError:
return None
@staticmethod
def _is_pointer_to_struct(t: str) -> bool:
if not t:
return False
low = t.replace('\t',' ').replace('\n',' ')
return 'struct ' in low and '*' in low
@staticmethod
def _is_struct_or_union(t: str) -> bool:
if not t:
return False
low = t.strip()
return low.startswith('struct ') or low.startswith('union ')
@staticmethod
def _strip_array_suffix(t: str) -> str:
return t[:-2].strip() if t.endswith('[]') else t
def _guess_primitive_size(self, type_str: str) -> Optional[int]:
if not type_str:
return None
base = type_str
for tok in ('volatile','const'):
base = base.replace(tok, '')
base = base.replace('*',' ')
base = base.replace('[',' ').replace(']',' ')
base = ' '.join(base.split()).strip()
return self._PRIM_SIZE.get(base)
# ------------------ XML read ------------------
def _parse(self):
try:
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)
)
except FileNotFoundError:
self.variables = []
except ET.ParseError:
self.variables = []
# ------------------ flatten (expanded) ------------------
def flattened(self,
max_array_elems: Optional[int] = None
) -> List[Dict[str, Any]]:
"""
Returns a list of dictionaries with full data for variables and their expanded members.
Each dictionary contains: 'name', 'address', 'type', 'size', 'kind', 'count'.
max_array_elems: limit unfolding of large arrays (None = all).
"""
out: List[Dict[str, Any]] = []
def get_dict(name: str, address: int, type_str: str, size: Optional[int], kind: Optional[str], count: Optional[int]) -> Dict[str, Any]:
"""Helper to create the output dictionary format."""
return {
'name': name,
'address': address,
'type': type_str,
'size': size,
'kind': kind,
'count': count
}
def compute_stride(size_bytes: Optional[int],
count: Optional[int],
base_type: Optional[str],
node_children: Optional[List[MemberNode]]) -> int:
"""Calculates the stride (size of one element) for arrays."""
# 1) size_bytes/count
if size_bytes and count and count > 0:
if size_bytes % count == 0:
stride = size_bytes // count
if stride <= 0:
stride = 1
return stride
else:
# size not divisible by count → most likely size = size of one element
return max(size_bytes, 1)
# 2) attempt by type (primitive)
if base_type:
gs = self._guess_primitive_size(base_type)
if gs:
return gs
# 3) attempt by children (structure)
if node_children:
if not node_children:
return 1
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):
"""
Recursively expands members of structs/unions or pointed-to structs.
parent_is_ptr_struct: if True, connection is '->' otherwise '.'
"""
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
out.append(get_dict(path_m, addr_m, m.type_str, m.size, m.kind, m.count))
# array?
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
# Determine kind for array element based on its base type
elem_kind = None
if self._is_struct_or_union(base_t):
elem_kind = 'struct' # or 'union' depending on `base_t` prefix
elif self._guess_primitive_size(base_t):
elem_kind = 'primitive'
# For array elements, 'size' is the stride (size of one element), 'count' is None.
out.append(get_dict(path_i, addr_i, base_t, stride, elem_kind, None))
# array element: if structure / union → unfold fields
if m.children and self._is_struct_or_union(base_t):
expand_members(path_i, addr_i, m.children, parent_is_ptr_struct=False)
# array element: if pointer to structure
elif self._is_pointer_to_struct(base_t):
# usually no children in XML for these, but if present — use them
expand_members(path_i, addr_i, m.children, parent_is_ptr_struct=True)
continue
# not an array, but has children (e.g., struct/union)
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 variables ---
for v in self.variables:
out.append(get_dict(v.name, v.address, v.type_str, v.size, v.kind, v.count))
# top-level array?
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
# Determine kind for array element
elem_kind = None
if self._is_struct_or_union(base_t):
elem_kind = 'struct' # or 'union'
elif self._guess_primitive_size(base_t):
elem_kind = 'primitive'
out.append(get_dict(p, a, base_t, stride, elem_kind, None))
# array of structs?
if v.members and self._is_struct_or_union(base_t):
expand_members(p, a, v.members, parent_is_ptr_struct=False)
# array of pointers to structs?
elif self._is_pointer_to_struct(base_t):
expand_members(p, a, v.members, parent_is_ptr_struct=True)
continue
# top-level not an array, but has members
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 (as it was) --------------------
def date_struct_candidates(self) -> List[Tuple[str,int]]:
cands = []
for v in self.variables:
# top level (if all date fields are present)
direct_names = {mm.name for mm in v.members}
if DATE_FIELD_SET.issubset(direct_names):
cands.append((v.name, v.address))
# check first-level members
for m in v.members:
if m.is_date_struct():
cands.append((f"{v.name}.{m.name}", v.address + m.offset))
return cands
def get_all_vars_data(self) -> List[Dict[str, Any]]:
"""
Возвращает вложенную структуру словарей с полными данными для всех переменных и их развернутых членов.
Каждый словарь представляет узел в иерархии и содержит:
'name' (полный путь), 'address', 'size', 'type', 'kind', 'count', и 'children' (если есть).
Логика определения родительского пути теперь использует `split_path` для анализа структуры пути.
"""
flat_data = self.flattened(max_array_elems=None)
root_nodes: List[Dict[str, Any]] = []
all_nodes_map: Dict[str, Dict[str, Any]] = {}
for item in flat_data:
node_dict = {**item, 'children': []}
all_nodes_map[item['name']] = node_dict
# Вспомогательная функция для определения полного пути родителя с использованием split_path
def get_parent_path_using_split(full_path: str) -> Optional[str]:
# 1. Используем split_path для получения компонентов пути.
components = var_setup.split_path(full_path)
# Если нет компонентов или только один (верхний уровень, не массивный элемент)
if not components or len(components) == 1:
# Если компонент один и это не индекс массива (например, "project" или "my_var")
# тогда у него нет родителя в этой иерархии.
# Если это был бы "my_array[0]" -> components=['my_array', '[0]'], len=2
if len(components) == 1 and not components[0].startswith('['):
return None
elif len(components) == 2 and components[-1].startswith('['): # like "my_array[0]"
return components[0] # Return "my_array" as parent
else: # Edge cases or malformed, treat as root
return None
# 2. Определяем, как отрезать "хвост" из оригинальной строки `full_path`, чтобы получить родителя.
# Эта логика остаётся похожей на предыдущую, так как `split_path` не включает разделители
# и мы должны получить точную строку родительского пути.
# Находим индекс последнего разделителя '.' или '->'
last_dot_idx = full_path.rfind('.')
last_arrow_idx = full_path.rfind('->')
effective_last_sep_idx = -1
if last_dot_idx > last_arrow_idx:
effective_last_sep_idx = last_dot_idx
elif last_arrow_idx != -1:
effective_last_sep_idx = last_arrow_idx
# Находим начало последнего суффикса массива (e.g., '[0]') в оригинальной строке
array_suffix_match = re.search(r'(\[[^\]]*\])+$', full_path)
array_suffix_start_idx = -1
if array_suffix_match:
array_suffix_start_idx = array_suffix_match.start()
# Логика определения родителя:
# - Если есть суффикс массива, и он находится после последнего разделителя (или разделителей нет),
# то родитель - это часть до суффикса массива. (e.g., 'project.adc[0]' -> 'project.adc')
# - Иначе, если есть разделитель, родитель - это часть до последнего разделителя. (e.g., 'project.adc.bus' -> 'project.adc')
# - Иначе (ни разделителей, ни суффиксов), это корневой элемент.
if array_suffix_start_idx != -1 and (array_suffix_start_idx > effective_last_sep_idx):
return full_path[:array_suffix_start_idx]
elif effective_last_sep_idx != -1:
return full_path[:effective_last_sep_idx]
else:
return None # Корневой элемент без явного родителя
# Основная логика get_all_vars_data
# Заполнение связей "родитель-потомок"
for item_name, node_dict in all_nodes_map.items():
parent_name = get_parent_path_using_split(item_name) # Используем новую вспомогательную функцию
if parent_name and parent_name in all_nodes_map:
all_nodes_map[parent_name]['children'].append(node_dict)
else:
root_nodes.append(node_dict)
# Сортируем корневые узлы и их детей рекурсивно по имени
def sort_nodes(nodes_list: List[Dict[str, Any]]):
nodes_list.sort(key=lambda x: x['name'])
for node in nodes_list:
if node['children']:
sort_nodes(node['children'])
sort_nodes(root_nodes)
return root_nodes

223
Src/csv_logger.py Normal file
View File

@@ -0,0 +1,223 @@
import csv
import numbers
import time
from datetime import datetime
from PySide2 import QtWidgets
class CsvLogger:
"""
Логгер, совместимый по формату с C-реализацией CSV_AddTitlesLine / CSV_AddLogLine.
Публичный API сохранён:
set_titles(varnames)
set_value(timestamp, varname, varvalue)
select_file(parent=None) -> bool
write_to_csv()
Использование:
1) set_titles([...])
2) многократно set_value(ts, name, value)
3) select_file() (по желанию)
4) write_to_csv()
"""
def __init__(self, filename="log.csv", delimiter=';'):
self._filename = filename
self._delimiter = delimiter
# Пользовательские заголовки
self.variable_names_ordered = []
# Полные заголовки CSV (Ticks(X), Ticks(Y), Time(Y), ...)
self.headers = ['t'] # до вызова set_titles placeholder
# Данные: {timestamp_key: {varname: value, ...}}
# timestamp_key = то, что передано в set_value (float/int/etc)
self.data_rows = {}
# Внутренние структуры для генерации CSV-формата С
self._row_wall_dt = {} # {timestamp_key: datetime при первой записи}
self._base_ts = None # timestamp_key первой строки (число)
self._base_ts_val = 0.0 # float значение первой строки (для delta)
self._tick_x_start = 0 # начальный тик (можно менять вручную при необходимости)
# ---- Свойства ----
@property
def filename(self):
return self._filename
# ---- Публичные методы ----
def set_titles(self, varnames):
"""
Устанавливает имена переменных.
Формирует полные заголовки CSV в формате С-лога.
"""
if not isinstance(varnames, list):
raise TypeError("Varnames must be a list of strings.")
if not all(isinstance(name, str) for name in varnames):
raise ValueError("All variable names must be strings.")
self.variable_names_ordered = varnames
self.headers = ["Ticks(X)", "Ticks(Y)", "Time(Y)"] + self.variable_names_ordered
# Сброс данных (структура изменилась)
self.data_rows.clear()
self._row_wall_dt.clear()
self._base_ts = None
self._base_ts_val = 0.0
def set_value(self, timestamp, varname, varvalue):
"""
Установить ОДНО значение в ОДНУ колонку для заданного timestampа.
timestamp — float секунд с эпохи (time.time()).
"""
if varname not in self.variable_names_ordered:
return # игнор, как у тебя было
# Новая строка?
if timestamp not in self.data_rows:
# Инициализируем поля переменных значением None
self.data_rows[timestamp] = {vn: None for vn in self.variable_names_ordered}
# Дата/время строки из ПЕРЕДАННОГО timestamp (а не datetime.now()!)
try:
ts_float = float(timestamp)
except Exception:
# если какая-то дичь прилетела, пусть будет 0 (эпоха) чтобы не упасть
ts_float = 0.0
self._row_wall_dt[timestamp] = datetime.fromtimestamp(ts_float)
# База для расчёта Ticks(Y) — первая строка
if self._base_ts is None:
self._base_ts = timestamp
self._base_ts_val = ts_float
# Записываем значение
self.data_rows[timestamp][varname] = varvalue
def select_file(self, parent=None) -> bool:
"""
Диалог выбора файла.
"""
options = QtWidgets.QFileDialog.Options()
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
parent,
"Сохранить данные CSV",
self._filename,
"CSV Files (*.csv);;All Files (*)",
options=options
)
if filename:
if not filename.lower().endswith('.csv'):
filename += '.csv'
self._filename = filename
return True
else:
return False
def write_to_csv(self):
"""
Формирует CSV в формате C:
Ticks(X);Ticks(Y);Time(Y);Var1;Var2;...
0;0,000000;22/07/2025 13:45:12:0123;...;...
Правила значений:
- Тик X: автоинкремент от 0 (или self._tick_x_start) по порядку сортировки timestamp.
- Ticks(Y): дельта (секунды,микросекунды) между текущим timestamp и первым timestamp.
- Time(Y): wallclock строки (datetime.now() при первом появлении timestamp).
- Значение < 0 -> пустая ячейка (как if(raw_data[i] >= 0) else ;)
- None -> пустая ячейка.
"""
if len(self.headers) <= 3: # только служебные поля без переменных
print("Ошибка: Заголовки не установлены или не содержат переменных. Вызовите set_titles() перед записью.")
return
if not self._filename:
print("Ошибка: Имя файла не определено. select_file() или задайте при инициализации.")
return
if not self.data_rows:
print("Предупреждение: Нет данных для записи.")
# всё равно создадим файл с одними заголовками
try:
with open(self._filename, 'w', newline='', encoding='utf-8') as csvfile:
# QUOTE_NONE + escapechar для чистого формата без кавычек (как в С-строке)
writer = csv.writer(
csvfile,
delimiter=self._delimiter,
quoting=csv.QUOTE_NONE,
escapechar='\\',
lineterminator='\r\n'
)
# Пишем заголовки
writer.writerow(self.headers)
if self.data_rows:
sorted_ts = sorted(self.data_rows.keys(), key=self._ts_sort_key)
# убедимся, что база была зафиксирована
if self._base_ts is None:
self._base_ts = sorted_ts[0]
self._base_ts_val = self._coerce_ts_to_float(self._base_ts)
tick_x = self._tick_x_start
for ts in sorted_ts:
row_dict = self.data_rows[ts]
# delta по timestamp
cur_ts_val = self._coerce_ts_to_float(ts)
delta_us = int(round((cur_ts_val - self._base_ts_val) * 1_000_000))
if delta_us < 0:
delta_us = 0 # защита
seconds = delta_us // 1_000_000
micros = delta_us % 1_000_000
# wallclock строки
dt = self._row_wall_dt.get(ts, datetime.now())
# Формат DD/MM/YYYY HH:MM:SS:мммм (4 цифры ms, как в C: us/1000)
time_str = dt.strftime("%d/%m/%Y %H:%M:%S") + f":{dt.microsecond // 1000:04d}"
# Значения
row_vals = []
for vn in self.variable_names_ordered:
v = row_dict.get(vn)
if v is None:
row_vals.append("") # нет данных
else:
# если числовое и <0 -> пусто (как в C: если raw_data[i] >= 0 else ;)
if isinstance(v, numbers.Number) and v < 0:
row_vals.append("")
else:
row_vals.append(v)
csv_row = [tick_x, f"{seconds},{micros:06d}", time_str] + row_vals
writer.writerow(csv_row)
tick_x += 1
print(f"Данные успешно записаны в '{self._filename}'")
except Exception as e:
print(f"Ошибка при записи в файл '{self._filename}': {e}")
# ---- Вспомогательные ----
def _coerce_ts_to_float(self, ts):
"""
Пробуем привести переданный timestamp к float.
Разрешаем int/float/str, остальное -> индекс по порядку (0).
"""
if isinstance(ts, numbers.Number):
return float(ts)
try:
return float(ts)
except Exception:
# fallback: нечисловой ключ -> используем порядковый индекс
# (таких почти не должно быть, но на всякий)
return 0.0
def _ts_sort_key(self, ts):
"""
Ключ сортировки timestampов — сначала попытка float, потом str.
"""
if isinstance(ts, numbers.Number):
return (0, float(ts))
try:
return (0, float(ts))
except Exception:
return (1, str(ts))

287
Src/path_hints.py Normal file
View File

@@ -0,0 +1,287 @@
# 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
def get_children(self) -> List["PathNode"]:
"""
Вернуть список дочерних узлов, отсортированных по имени.
"""
return sorted(self.children.values(), key=lambda n: n.name)
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 get_children(self, full_path: str) -> List[PathNode]:
"""
Вернуть список дочерних узлов PathNode для заданного полного пути.
Если узел не найден — вернуть пустой список.
"""
node = self.find_node(full_path)
if node is None:
return []
return node.get_children()
# ------------ Подсказки ------------
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 add_separator(self, full_path: str) -> str:
"""
Возвращает full_path с добавленным разделителем ('.' или '['),
если у узла есть дети и пользователь ещё не поставил разделитель.
Если первый ребёнок — массивный токен ('[0]') → добавляем '['.
Позже можно допилить '->' для указателей.
"""
node = self.find_node(full_path)
text = full_path
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 += '.' # обычный переход
return text
# ------------ внутренние вспомогательные ------------
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

View File

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

1254
Src/tms_debugvar_term.py Normal file
View File

@@ -0,0 +1,1254 @@
from PySide2 import QtCore, QtWidgets, QtSerialPort
from tms_debugvar_lowlevel import LowLevelSelectorWidget
import datetime
import time
from csv_logger import CsvLogger
# ------------------------------- Константы протокола ------------------------
WATCH_SERVICE_BIT = 0x8000
DEBUG_OK = 0 # ожидаемый код успешного чтения
SIGN_BIT_MASK = 0x80
FRAC_MASK_FULL = 0x7F # если используем 7 бит дробной части
# --- Debug status codes (из прошивки) ---
DEBUG_OK = 0x00
DEBUG_ERR = 0x80 # общий флаг ошибки (старший бит)
DEBUG_ERR_VAR_NUMB = DEBUG_ERR | (1 << 0)
DEBUG_ERR_INVALID_VAR = DEBUG_ERR | (1 << 1)
DEBUG_ERR_ADDR = DEBUG_ERR | (1 << 2)
DEBUG_ERR_ADDR_ALIGN = DEBUG_ERR | (1 << 3)
DEBUG_ERR_INTERNAL = DEBUG_ERR | (1 << 4)
DEBUG_ERR_DATATIME = DEBUG_ERR | (1 << 5)
DEBUG_ERR_RS = DEBUG_ERR | (1 << 5)
# для декодирования по битам
_DEBUG_ERR_BITS = (
(1 << 0, "Invalid Variable Index"),
(1 << 1, "Invalid Variable"),
(1 << 2, "Invalid Address"),
(1 << 3, "Invalid Address Align"),
(1 << 4, "Internal Code Error"),
(1 << 5, "Invalid Data or Time"),
(1 << 6, "Error with RS"),
)
# ---------------------------------------------------------------- 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
def _decode_debug_status(status: int) -> str:
"""Преобразует код статуса прошивки в строку.
Возвращает 'OK' или перечисление битов через '|'.
Не зависит от того, WATCH или LowLevel.
"""
if status == DEBUG_OK:
return "OK"
parts = []
if status & DEBUG_ERR:
for mask, name in _DEBUG_ERR_BITS:
if status & mask:
parts.append(name)
if not parts: # старший бит есть, но ни один из известных младших не выставлен
parts.append("ERR")
else:
# Неожиданно: статус !=0, но бит DEBUG_ERR не стоит
parts.append(f"0x{status:02X}")
return "|".join(parts)
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
self._last_txn_timestamp = 0
self._ll_polling_active = False
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_polling_variables = [] # List of selected variables for polling
self._ll_current_poll_index = -1 # Index of the variable currently being polled in the _ll_polling_variables list
self._ll_current_var_info = []
self.csv_logger = CsvLogger()
self._csv_logging_active = False
self._last_csv_timestamp = 0 # Для отслеживания времени записи
# Кэш: 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() # <-- Вызываем новый метод
g_control = QtWidgets.QGroupBox("Control / Status")
control_layout = QtWidgets.QHBoxLayout(g_control) # Используем QHBoxLayout
# Форма для статусов слева
form_control = QtWidgets.QFormLayout()
self.lbl_status = QtWidgets.QLabel("Idle")
self.lbl_status.setStyleSheet("font-weight: bold; color: grey;")
form_control.addRow("Status:", self.lbl_status)
self.lbl_actual_interval = QtWidgets.QLabel("-")
form_control.addRow("Actual Interval:", self.lbl_actual_interval)
control_layout.addLayout(form_control, 1) # Растягиваем форму
# Галочка Raw справа
self.chk_raw = QtWidgets.QCheckBox("Raw (no IQ scaling)")
control_layout.addWidget(self.chk_raw)
# Создаем QGroupBox для группировки элементов управления CSV
self.csv_log_groupbox = QtWidgets.QGroupBox("CSV Logging")
csv_log_layout = QtWidgets.QVBoxLayout(self.csv_log_groupbox) # Передаем groupbox как родительский layout
# Элементы управления CSV
h_file_select = QtWidgets.QHBoxLayout()
self.btn_select_csv_file = QtWidgets.QPushButton("Выбрать файл CSV")
# Убедитесь, что self.csv_logger инициализирован где-то до этого момента
self.lbl_csv_filename = QtWidgets.QLabel(self.csv_logger.filename)
h_file_select.addWidget(self.btn_select_csv_file)
h_file_select.addWidget(self.lbl_csv_filename, 1)
csv_log_layout.addLayout(h_file_select)
h_control_buttons = QtWidgets.QHBoxLayout()
self.btn_start_csv_logging = QtWidgets.QPushButton("Начать запись в CSV")
self.btn_stop_csv_logging = QtWidgets.QPushButton("Остановить запись в CSV")
self.btn_save_csv_data = QtWidgets.QPushButton("Сохранить данные в CSV")
self.btn_stop_csv_logging.setEnabled(False) # По умолчанию остановлена
h_control_buttons.addWidget(self.btn_start_csv_logging)
h_control_buttons.addWidget(self.btn_stop_csv_logging)
h_control_buttons.addWidget(self.btn_save_csv_data)
csv_log_layout.addLayout(h_control_buttons)
# Добавляем QGroupBox в основной лейаут
# --- 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)
layout.addWidget(self.tabs, 1)
layout.addWidget(g_control)
layout.addWidget(self.csv_log_groupbox)
layout.addWidget(self.log_spoiler)
layout.setStretch(layout.indexOf(g_serial), 0)
layout.setStretch(layout.indexOf(self.tabs), 1)
def _build_watch_tab(self):
tab = QtWidgets.QWidget()
main_layout = QtWidgets.QVBoxLayout(tab)
# --- Variable Selector ---
g_selector = QtWidgets.QGroupBox("Variable Selector")
selector_layout = QtWidgets.QVBoxLayout(g_selector)
form_selector = QtWidgets.QFormLayout()
h_layout = QtWidgets.QHBoxLayout()
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)
# Первая группа: Base Index + spin + checkbox
base_index_layout = QtWidgets.QHBoxLayout()
base_index_label = QtWidgets.QLabel("Base Index")
base_index_layout.addWidget(base_index_label)
base_index_layout.addWidget(self.spin_index)
base_index_layout.addWidget(self.chk_hex_index)
base_index_layout.setSpacing(5)
# Вторая группа: spin_count + метка справа
count_layout = QtWidgets.QHBoxLayout()
count_layout.setSpacing(2) # минимальный отступ
count_layout.addWidget(self.spin_count)
count_label = QtWidgets.QLabel("Cnt")
count_layout.addWidget(count_label)
# Добавляем обе группы в общий горизонтальный лэйаут
h_layout.addLayout(base_index_layout)
h_layout.addSpacing(20)
h_layout.addLayout(count_layout)
form_selector.addRow(h_layout)
self.spin_interval = QtWidgets.QSpinBox()
self.spin_interval.setRange(50, 10000)
self.spin_interval.setValue(500)
self.spin_interval.setSuffix(" ms")
form_selector.addRow("Interval:", self.spin_interval)
selector_layout.addLayout(form_selector)
btn_layout = QtWidgets.QHBoxLayout()
self.btn_update_service = QtWidgets.QPushButton("Update Service")
self.btn_read_values = QtWidgets.QPushButton("Read Value(s)")
self.btn_poll = QtWidgets.QPushButton("Start Polling")
btn_layout.addWidget(self.btn_update_service)
btn_layout.addWidget(self.btn_read_values)
btn_layout.addWidget(self.btn_poll)
selector_layout.addLayout(btn_layout)
# --- Table ---
g_table = QtWidgets.QGroupBox("Table")
table_layout = QtWidgets.QVBoxLayout(g_table)
self.tbl_values = QtWidgets.QTableWidget(0, 5)
self.tbl_values.setHorizontalHeaderLabels(["Index", "Name", "IQ", "Raw", "Scaled"])
hh = self.tbl_values.horizontalHeader()
for i in range(4):
hh.setSectionResizeMode(i, QtWidgets.QHeaderView.ResizeToContents)
hh.setSectionResizeMode(4, QtWidgets.QHeaderView.Stretch)
self.tbl_values.verticalHeader().setVisible(False)
table_layout.addWidget(self.tbl_values)
# --- Вертикальный сплиттер ---
v_split = QtWidgets.QSplitter(QtCore.Qt.Vertical)
v_split.addWidget(g_selector)
v_split.addWidget(g_table)
v_split.setStretchFactor(0, 1)
v_split.setStretchFactor(1, 3)
v_split.setStretchFactor(2, 1)
main_layout.addWidget(v_split)
self.tabs.addTab(tab, "Watch")
table_layout.addWidget(self.tbl_values)
def _build_lowlevel_tab(self):
# создаём виджет LowLevelSelectorWidget
self.ll_selector = LowLevelSelectorWidget()
# добавляем как корневой виджет вкладки
self.tabs.addTab(self.ll_selector, "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_update_service.clicked.connect(self.request_service_update_for_table)
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.ll_selector.btn_read_once.clicked.connect(self.request_lowlevel_once)
self.ll_selector.btn_start_polling.clicked.connect(self._toggle_ll_polling)
# --- CSV Logging ---
self.btn_select_csv_file.clicked.connect(self._select_csv_file)
self.btn_start_csv_logging.clicked.connect(self._start_csv_logging)
self.btn_stop_csv_logging.clicked.connect(self._stop_csv_logging)
self.btn_save_csv_data.clicked.connect(self._save_csv_data)
def set_status(self, text: str, mode: str = "idle"):
colors = {
"idle": "gray",
"service": "blue",
"values": "green",
"error": "red"
}
color = colors.get(mode.lower(), "black")
self.lbl_status.setText(text)
self.lbl_status.setStyleSheet(f"font-weight: bold; color: {color};")
# ----------------------------- 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 OK] 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 OK] 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 = self.ll_selector.get_datetime()
if dt_info:
# Используем время из var_info
year = dt_info.year
month = dt_info.month
day = dt_info.day
hour = dt_info.hour
minute = dt_info.minute
self._log("[LL] Using time from selector.")
else:
return
addr = var_info.get('address', 0)
addr2 = (addr >> 16) & 0xFF
addr1 = (addr >> 8) & 0xFF
addr0 = addr & 0xFF
# Ensure 'ptr_type' and 'iq_type' from var_info are integers (enum values)
# Use a fallback to 0 if they are not found or not integers
pt_type = var_info.get('ptr_type_enum', 0) & 0xFF
iq_type = var_info.get('iq_type_enum', 0) & 0xFF
ret_type = var_info.get('return_type_enum', 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_service_update_for_table(self):
"""
Очищает кеш имен/типов для всех видимых в таблице переменных
и инициирует их повторное чтение.
"""
indices_to_update = []
for row in range(self.tbl_values.rowCount()):
item = self.tbl_values.item(row, 0)
if item and item.text().isdigit():
indices_to_update.append(int(item.text()))
if not indices_to_update:
self._log("[SERVICE] No variables in table to update.")
return
self._log(f"[SERVICE] Queuing name/type update for {len(indices_to_update)} variables.")
# Очищаем кеш для этих индексов, чтобы принудительно их перечитать
for index in indices_to_update:
if index in self._name_cache:
del self._name_cache[index]
# Запускаем стандартный запрос значений. Он автоматически обработает
# отсутствующую сервисную информацию (имена/типы) перед запросом данных.
if not (self._polling or self._ll_polling):
self.request_values()
def request_values(self):
self._update_interval()
base = int(self.spin_index.value())
count = int(self.spin_count.value())
needed = []
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.set_status("Read service...", "service")
self._kick_service_queue()
else:
self.set_status("Read values...", "values")
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 hasattr(self, '_ll_current_var_info') or not self._ll_current_var_info:
self._log("[LL] No variable prepared/selected for single read!")
return
frame = self._build_lowlevel_request(self._ll_current_var_info)
# --- НОВОЕ: Передаем ll_var_info в метаданные транзакции ---
meta = {'lowlevel': True, 'll_polling': False, 'll_var_info': self._ll_current_var_info}
self.set_status("Read lowlevel...", "values")
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):
# Добавляем ll_var_info, если это LL запрос
if meta.get('lowlevel', False) and 'll_var_info' not in meta:
# Это должно быть установлено вызывающим кодом, но для безопасности
# или если LL polling не передал var_info явно
meta['ll_var_info'] = self._ll_current_var_info # Используем last prepared var info for single shots
if self._busy:
# ... существующий код ...
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):
if(meta.get('service')):
self._update_interval()
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`, его убираем
# Если идёт LL polling — переходим сразу к следующей переменной
if self._ll_polling and (self._ll_poll_index < len(self._ll_polling_variables)):
self._process_next_ll_variable_in_cycle()
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()
self.set_status("Timeout", "error")
# ------------------------------- 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()
if not (self._polling or self._ll_polling):
self.set_status("Idle", "idle")
# ------------------------------- 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]
status_desc = _decode_debug_status(status)
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)
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:
self.set_status("Error", "error")
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
desc = _decode_debug_status(status)
self._log(f"[DATA] ERROR status=0x{status:02X} ({desc}) bad_index={bad_index}")
# Обновим UI
self._populate_watch_error(bad_index, status)
# Сигналы (оставляем совместимость)
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 = []
# Получаем текущее время один раз для всех переменных в этом фрейме
current_time = time.time()
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)
# --- Здесь записываем имя и значение в csv_logger ---
self.csv_logger.set_value(current_time, name_i, scaled)
self._populate_table(idx_list, name_list, iq_list, display_raw_list, scaled_list)
if varqnt == 1:
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.valueRead.emit(idx_list[0], status, iq_list[0], display_raw_list[0], scaled_list[0])
else:
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
status_desc = _decode_debug_status(status)
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_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
self.llValueRead.emit(addr24, status, return_type, value_int, scaled)
var_name = None
if self._ll_current_var_info.get("address") == addr24:
var_name = self._ll_current_var_info.get("name")
display_val = value_int if self.chk_raw.isChecked() else scaled
if var_name:
self.ll_selector.set_variable_value(var_name, display_val)
self._log(f"[LL] OK addr=0x{addr24:06X} type=0x{return_type:02X} raw={value_int} scaled={scaled:.6g}")
current_time = time.time() # Получаем текущее время
self.csv_logger.set_value(current_time, var_name, display_val)
def _populate_watch_error(self, bad_index: int, status: int):
"""Отобразить строку ошибки при неудачном ответе WATCH."""
desc = _decode_debug_status(status)
self.tbl_values.setRowCount(1)
self.tbl_values.setItem(0, 0, QtWidgets.QTableWidgetItem(str(bad_index)))
self.tbl_values.setItem(0, 1, QtWidgets.QTableWidgetItem(f"<ERROR:{desc}>"))
self.tbl_values.setItem(0, 2, QtWidgets.QTableWidgetItem("-"))
self.tbl_values.setItem(0, 3, QtWidgets.QTableWidgetItem("-"))
self.tbl_values.setItem(0, 4, QtWidgets.QTableWidgetItem("<ERROR>"))\
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.set_status("Idle", "idle")
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.set_status("Idle", "idle")
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):
if self._ll_polling: # If currently polling, stop
self._ll_polling = False
self.ll_selector.btn_start_polling.setText("Start Polling")
self._ll_poll_timer.stop()
self._ll_polling_variables.clear()
self._ll_current_poll_index = -1
self._log("[LL Polling] Stopped.")
else: # If not polling, start
# Get all selected variables from the LowLevelSelectorWidget
self._ll_polling_variables = self.ll_selector.get_selected_variables_and_addresses()
if not self._ll_polling_variables:
self._log("[LL] No variables selected for polling. Aborting.")
self.set_status("Error.", "error")
return
self._ll_polling = True
self.ll_selector.btn_start_polling.setText("Stop Polling")
self._ll_current_poll_index = 0 # Start from the first variable
self._log(f"[LL Polling] Started. Polling {len(self._ll_polling_variables)} variables.")
# Start the timer. It will trigger _on_ll_poll_timeout, which starts the cycle.
# The first cycle starts immediately, subsequent cycles wait for the interval.
self._ll_poll_timer.setInterval(self.ll_selector.spin_interval.value())
self._ll_poll_timer.start() # Start the timer for recurrent cycles
# Immediately kick off the first variable read of the first cycle
self._start_ll_cycle()
def _on_ll_poll_timeout(self):
"""Вызывается по таймеру для старта нового цикла."""
if self._ll_polling and not self._busy:
self._start_ll_cycle()
elif self._busy:
self._log("[LL Polling] Busy, skip cycle start.")
def _start_ll_cycle(self):
self._update_interval()
"""Запускает новый цикл опроса всех переменных."""
if not self._ll_polling or not self._ll_polling_variables:
return
self._ll_poll_index = 0
self._process_next_ll_variable_in_cycle()
def _on_ll_variable_prepared(self, var_info: dict):
"""Срабатывает при выборе переменной в селекторе."""
self._ll_current_var_info = var_info
def _process_next_ll_variable_in_cycle(self):
if not self._ll_polling: # Добавим проверку, чтобы избежать вызова, если LL polling отключен
return
if self._ll_poll_index < len(self._ll_polling_variables):
var_info = self._ll_polling_variables[self._ll_poll_index]
self._on_ll_variable_prepared(var_info)
self._ll_poll_index += 1
frame = self._build_lowlevel_request(var_info)
# --- НОВОЕ: Передаем var_info в метаданные транзакции для LL polling ---
meta = {'lowlevel': True, 'll_polling': True, 'll_var_info': var_info}
self.set_status(f"Polling LL: {var_info.get('name')}", "values")
self._enqueue_raw(frame, meta)
else:
# Цикл завершен, перезапускаем таймер для следующего полного цикла
self._ll_poll_index = 0
self._ll_poll_timer.start(self.ll_selector.spin_interval.value())
self.set_status("LL polling cycle done, waiting...", "idle")
# ------------------------------ 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 or self._ll_polling)
#self.btn_update_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 or self._polling)
self.ll_selector.btn_read_once.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 _select_csv_file(self):
"""Открывает диалог выбора файла для CSV и обновляет UI."""
if self.csv_logger.select_file(self): # Передаем self как parent для диалога
self.lbl_csv_filename.setText(self.csv_logger.filename)
self._log(f"CSV file set to: {self.csv_logger.filename}")
def _start_csv_logging(self):
"""Начинает запись данных в CSV. Устанавливает заголовки в зависимости от активной вкладки."""
if not self.serial.isOpen():
self._log("[CSV] Невозможно начать запись: COM порт не открыт.")
self.set_status("Port closed", "error")
return
# Определяем активную вкладку и устанавливаем заголовки
current_tab_index = self.tabs.currentIndex()
varnames_for_csv = []
if self.tabs.tabText(current_tab_index) == "Watch":
# Для вкладки Watch берем имена из кэша, если они есть, иначе используем Index_X
base_index = self.spin_index.value()
count = self.spin_count.value()
for i in range(base_index, base_index + count):
if i in self._name_cache and self._name_cache[i][2]: # status, iq_raw, name, is_signed, frac_bits
varnames_for_csv.append(self._name_cache[i][2])
else:
varnames_for_csv.append(f"Index_{i}")
self._log(f"[CSV] Начинается запись для Watch переменных: {varnames_for_csv}")
elif self.tabs.tabText(current_tab_index) == "LowLevel":
# Для вкладки LowLevel берем имена из ll_selector
selected_vars = self.ll_selector.get_selected_variables_and_addresses()
varnames_for_csv = [var['name'] for var in selected_vars if 'name' in var]
if not varnames_for_csv:
self._log("[CSV] Внимание: На вкладке LowLevel не выбраны переменные для записи.")
self._log(f"[CSV] Начинается запись для LowLevel переменных: {varnames_for_csv}")
else:
self._log("[CSV] Неизвестная активная вкладка. Невозможно определить заголовки CSV.")
return
if not varnames_for_csv:
self._log("[CSV] Нет переменных для записи в CSV. Запись не начата.")
return
self.csv_logger.set_titles(varnames_for_csv)
self._csv_logging_active = True
self.btn_start_csv_logging.setEnabled(False)
self.btn_stop_csv_logging.setEnabled(True)
self.set_status("CSV Logging ACTIVE", "values")
self._log("[CSV] Запись данных в CSV началась.")
def _stop_csv_logging(self):
"""Останавливает запись данных в CSV."""
self._csv_logging_active = False
self.btn_start_csv_logging.setEnabled(True)
self.btn_stop_csv_logging.setEnabled(False)
self.set_status("CSV Logging STOPPED", "idle")
self._log("[CSV] Запись данных в CSV остановлена.")
def _save_csv_data(self):
"""Сохраняет все собранные данные в CSV файл."""
if self._csv_logging_active:
self._log("[CSV] Запись активна. Сначала остановите запись.")
self.set_status("Stop logging first", "error")
return
self.csv_logger.write_to_csv()
self.set_status("CSV data saved", "idle")
def _log(self, msg: str):
# ... (код без изменений)
if 'ERR' in msg:
self.set_status(msg, 'error')
if 'OK' in msg:
self.set_status('Idle', 'idle')
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}|")
def _update_interval(self):
now = time.perf_counter()
if self._last_txn_timestamp is not None:
delta_ms = (now - self._last_txn_timestamp) * 1000
# Обновляем UI только если он уже создан
if hasattr(self, 'lbl_actual_interval'):
self.lbl_actual_interval.setText(f"{delta_ms:.1f} ms")
self._last_txn_timestamp = now
# ---------------------------------------------------------- Demo harness ---
class _DemoWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("DebugVar Terminal")
self.term = DebugTerminalWidget(self)
self.setCentralWidget(self.term)
self.resize(1000, 600)
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_())

View File

@@ -1,83 +1,61 @@
import re # variable_select_widget.py
import pickle
import hashlib
from typing import List, Dict, Any, Optional
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QLineEdit, QWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QLineEdit,
QHeaderView, QCompleter QHeaderView, QCompleter
) )
from PySide2.QtGui import QKeyEvent from PySide2.QtGui import QKeyEvent
from PySide2.QtCore import Qt, QStringListModel 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): def compute_vars_hash(vars_list):
return hashlib.sha1(pickle.dumps(vars_list)).hexdigest() 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: QTreeWidgetItem) -> bool:
def is_lazy_item(item):
return item.childCount() == 1 and item.child(0).text(0) == 'lazy_marker' return item.childCount() == 1 and item.child(0).text(0) == 'lazy_marker'
# ------------------------------------------------------------------
# VariableSelectWidget
# ------------------------------------------------------------------
class VariableSelectWidget(QWidget): 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): def __init__(self, parent=None):
super().__init__(parent) 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 = QLineEdit(self)
self.search_input.setPlaceholderText("Поиск...") self.search_input.setPlaceholderText("Поиск...")
@@ -97,32 +75,58 @@ class VariableSelectWidget(QWidget):
self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.completer.setCaseSensitivity(Qt.CaseInsensitive)
self.completer.setFilterMode(Qt.MatchContains) self.completer.setFilterMode(Qt.MatchContains)
self.completer.setWidget(self.search_input) self.completer.setWidget(self.search_input)
self.completer.activated[str].connect(self.insert_completion)
# --- Layout --- # layout
layout = QVBoxLayout(self) lay = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0) lay.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.search_input) lay.addWidget(self.search_input)
layout.addWidget(self.tree) lay.addWidget(self.tree)
# --- Соединения --- # signals
#self.search_input.textChanged.connect(self.on_search_text_changed) self.search_input.textChanged.connect(self.on_search_text_changed)
self.search_input.textChanged.connect(lambda text: self.on_search_text_changed(text))
self.search_input.installEventFilter(self) self.search_input.installEventFilter(self)
self.completer.activated[str].connect(lambda text: self.insert_completion(text))
# --- Публичные методы для управления виджетом снаружи ---
# ------------------------------------------------------------------
# public api
# ------------------------------------------------------------------
def set_autocomplete(self, enabled: bool): def set_autocomplete(self, enabled: bool):
"""Включает или выключает режим автодополнения."""
self.is_autocomplete_on = enabled 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.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): def populate_tree(self, vars_list=None):
if vars_list is None: if vars_list is None:
vars_list = self.expanded_vars vars_list = self.expanded_vars
@@ -130,351 +134,203 @@ class VariableSelectWidget(QWidget):
new_hash = compute_vars_hash(vars_list) new_hash = compute_vars_hash(vars_list)
if self._vars_hash == new_hash: if self._vars_hash == new_hash:
return return
self._vars_hash = new_hash self._vars_hash = new_hash
self.tree.setUpdatesEnabled(False) self.tree.setUpdatesEnabled(False)
self.tree.blockSignals(True) self.tree.blockSignals(True)
self.tree.clear() self.tree.clear()
self.node_index.clear() self._item_by_canon.clear()
for var in vars_list: # построим top-level из входного списка: определяем по глубине токенов
self.add_tree_item_lazy(None, var) # (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.setUpdatesEnabled(True)
self.tree.blockSignals(False) self.tree.blockSignals(False)
header = self.tree.header() header = self.tree.header()
header.setSectionResizeMode(QHeaderView.Interactive) header.setSectionResizeMode(QHeaderView.Interactive)
header.setSectionResizeMode(1, QHeaderView.Stretch) header.setSectionResizeMode(1, QHeaderView.Stretch)
self.tree.setColumnWidth(0, 400) self.tree.setColumnWidth(0, 400)
def on_item_expanded(self, item): def on_item_expanded(self, item: QTreeWidgetItem):
if is_lazy_item(item): if is_lazy_item(item):
item.removeChild(item.child(0)) item.removeChild(item.child(0))
var = item.data(0, Qt.UserRole + 100) var = item.data(0, self.ROLE_VAR_DICT)
if var: if var:
for child_var in var.get('children', []): for ch in var.get('children', []) or []:
self.add_tree_item_lazy(item, child_var) self._add_tree_item_lazy(item, ch)
# ------------------------------------------------------------------
def get_full_item_name(self, item): # item creation (var['name'] — ПОЛНЫЙ ПУТЬ)
fullname = item.text(0) # ------------------------------------------------------------------
# Заменяем '->' на '.' def _add_tree_item_lazy(self, parent: Optional[QTreeWidgetItem], var: Dict[str, Any]):
fullname = fullname.replace('->', '.') full_path = var.get('name', '')
fullname = fullname.replace('[', '.[')
return fullname
def add_tree_item_lazy(self, parent, var):
name = var['name']
type_str = var.get('type', '') type_str = var.get('type', '')
item = QTreeWidgetItem([name, type_str])
item.setData(0, Qt.UserRole, name) # здесь оставляем полный путь для отображения
full_name = self.get_full_item_name(item) item = QTreeWidgetItem([full_path, type_str])
self.node_index[full_name.lower()] = item 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: if "(bitfield:" in type_str:
item.setDisabled(True) item.setDisabled(True)
self.set_tool(item, "Битовые поля недоступны для выбора") self._set_tool(item, "Битовые поля недоступны для выбора")
# метаданные
for i, attr in enumerate(['file', 'extern', 'static']): for i, attr in enumerate(['file', 'extern', 'static']):
item.setData(0, Qt.UserRole + 1 + i, var.get(attr)) item.setData(0, Qt.UserRole + 1 + i, var.get(attr))
# в дерево
if parent is None: if parent is None:
self.tree.addTopLevelItem(item) self.tree.addTopLevelItem(item)
else: else:
parent.addChild(item) parent.addChild(item)
# Если есть дети — добавляем заглушку (чтобы можно было раскрыть) # lazy children
if var.get('children'): if var.get('children'):
dummy = QTreeWidgetItem(["lazy_marker"]) dummy = QTreeWidgetItem(["lazy_marker"])
item.addChild(dummy) item.addChild(dummy)
# Кэшируем детей для подгрузки по событию # индекс
item.setData(0, Qt.UserRole + 100, var) # Сохраняем var целиком self._item_by_canon[canonical_key(full_path)] = item
@staticmethod
def _tail_token(full_path: str) -> str:
toks = split_path_tokens(full_path)
return toks[-1] if toks else full_path
def show_matching_path(self, item, path_parts, level=0): # ------------------------------------------------------------------
node_name = item.text(0).lower() # filtering
node_parts = split_path(node_name) # ------------------------------------------------------------------
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 'project' in node_name: # простой режим (нет ., ->, [):
a = 1 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): if level >= len(path_parts):
# Путь полностью пройден — показываем только этот узел (без раскрытия всех детей)
item.setHidden(False) item.setHidden(False)
item.setExpanded(False) item.setExpanded(False)
return True return True
if level >= len(node_parts): if level >= len(node_parts):
# Уровень поиска больше длины пути узла — скрываем item.setHidden(True)
item.setHidden(False) return False
search_part = path_parts[level] search_part = path_parts[level]
node_part = node_parts[level] node_part = node_parts[level]
if search_part == node_part: if search_part == node_part:
# Точное совпадение — показываем узел, идём вглубь только по совпадениям
item.setHidden(False) item.setHidden(False)
matched_any = False matched_any = False
self.on_item_expanded(item) self.on_item_expanded(item)
for i in range(item.childCount()): for i in range(item.childCount()):
child = item.child(i) ch = item.child(i)
if self.show_matching_path(child, path_parts, level + 1): if self._show_matching_path(ch, path_parts, level + 1):
matched_any = True matched_any = True
item.setExpanded(matched_any) item.setExpanded(matched_any)
return matched_any or item.childCount() == 0 return matched_any or item.childCount() == 0
elif node_part.startswith(search_part): elif node_part.startswith(search_part):
# Неполное совпадение — показываем только этот узел, детей скрываем, не раскрываем
item.setHidden(False) item.setHidden(False)
item.setExpanded(False) item.setExpanded(False)
return True 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.setHidden(False)
item.setExpanded(False) item.setExpanded(False)
return True return True
else: else:
# Несовпадение — скрываем
item.setHidden(True) item.setHidden(True)
return False return False
# ------------------------------------------------------------------
def filter_tree(self): # completions (ONLY PathHints)
text = self.search_input.text().strip().lower() # ------------------------------------------------------------------
path_parts = split_path(text) if text else [] def update_completions(self, text: Optional[str] = None) -> List[str]:
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):
if text is None: if text is None:
text = self.search_input.text().strip() text = self.search_input.text()
else: suggestions = self.hints.suggest(text)
text = text.strip() self.completer.setModel(QStringListModel(suggestions))
if 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('[')
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() 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 += '[' # для массивов
else: else:
text += '.' # fallback self.completer.popup().hide()
return suggestions
def insert_completion(self, full_path: str):
text = self.hints.add_separator(full_path)
if not self._bckspc_pressed: if not self._bckspc_pressed:
self.search_input.setText(text) self.search_input.setText(text)
self.search_input.setCursorPosition(len(text)) self.search_input.setCursorPosition(len(text))
self.run_completions(text) self.run_completions(text)
else:
self.search_input.setText(text)
self.search_input.setCursorPosition(len(text))
# ------------------------------------------------------------------
# events
# ------------------------------------------------------------------
def eventFilter(self, obj, event): def eventFilter(self, obj, event):
if obj == self.search_input and isinstance(event, QKeyEvent): if obj == self.search_input and isinstance(event, QKeyEvent):
if event.key() == Qt.Key_Space and event.modifiers() & Qt.ControlModifier: if event.key() == Qt.Key_Space and event.modifiers() & Qt.ControlModifier:
self.manual_completion_active = True self.manual_completion_active = True
text = self.search_input.text().strip() self.run_completions(self.search_input.text())
self.run_completions(text)
elif event.key() == Qt.Key_Escape: elif event.key() == Qt.Key_Escape:
# Esc — выключаем ручной режим и скрываем подсказки, если autocomplete выключен
if not self.is_autocomplete_on: if not self.is_autocomplete_on:
self.manual_completion_active = False self.manual_completion_active = False
self.completer.popup().hide() self.completer.popup().hide()
return True return True
if event.key() == Qt.Key_Backspace: if event.key() == Qt.Key_Backspace:
self._bckspc_pressed = True self._bckspc_pressed = True
else: else:
self._bckspc_pressed = False self._bckspc_pressed = False
return super().eventFilter(obj, event) return super().eventFilter(obj, event)
def run_completions(self, text): def run_completions(self, text: str):
completions = self.update_completions(text) if not self.is_autocomplete_on and not self.manual_completion_active:
self.completer.popup().hide()
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
node = find_exact_item(completions[0])
if node and node.childCount() > 0:
# Используем первую подсказку, чтобы определить нужный разделитель
completions = self.update_completions(text + '.')
if not completions:
return return
suggestion = completions[0] self.update_completions(text)
# Ищем, какой символ идёт после текущего текста
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"
def on_search_text_changed(self, text: str):
self.completer.setWidget(self.search_input) self.completer.setWidget(self.search_input)
self.filter_tree() self.filter_tree()
if text == None: if text is None:
text = self.search_input.text().strip() text = self.search_input.text()
if self.is_autocomplete_on: if self.is_autocomplete_on:
self.run_completions(text) self.run_completions(text)
else: else:
# Если выключено, показываем подсказки только если флаг ручного вызова True
if self.manual_completion_active: if self.manual_completion_active:
self.run_completions(text) self.run_completions(text)
else: else:
@@ -485,122 +341,88 @@ class VariableSelectWidget(QWidget):
self.completer.setWidget(self.search_input) self.completer.setWidget(self.search_input)
super().focusInEvent(event) 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): def closeEvent(self, event):
self.completer.setWidget(None) self.completer.setWidget(None)
self.completer.deleteLater() 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)]

View File

@@ -304,7 +304,7 @@ class VariableSelectorDialog(QDialog):
# Проверка пути к XML # Проверка пути к XML
if not hasattr(self, 'xml_path') or not self.xml_path: if not hasattr(self, 'xml_path') or not self.xml_path:
from PySide2.QtWidgets import QMessageBox from PySide2.QtWidgets import QMessageBox
QMessageBox.warning(self, "Ошибка", "Путь к XML не задан, невозможно обновить переменные.") #QMessageBox.warning(self, "Ошибка", "Путь к XML не задан, невозможно обновить переменные.")
return return
root, tree = myXML.safe_parse_xml(self.xml_path) root, tree = myXML.safe_parse_xml(self.xml_path)

View File

@@ -119,11 +119,25 @@ class CtrlScrollComboBox(QComboBox):
event.ignore() event.ignore()
class VariableTableWidget(QTableWidget): class VariableTableWidget(QTableWidget):
def __init__(self, parent=None): def __init__(self, parent=None, show_value_instead_of_shortname=0):
super().__init__(0, 8, parent)
# Таблица переменных # Таблица переменных
if show_value_instead_of_shortname:
super().__init__(0, 8, parent)
self.setHorizontalHeaderLabels([ self.setHorizontalHeaderLabels([
'№', # новый столбец '№',
'En',
'Name',
'Origin Type',
'Base Type',
'IQ Type',
'Return Type',
'Value'
])
self._show_value = True
else:
super().__init__(0, 8, parent)
self.setHorizontalHeaderLabels([
'№',
'En', 'En',
'Name', 'Name',
'Origin Type', 'Origin Type',
@@ -132,71 +146,62 @@ class VariableTableWidget(QTableWidget):
'Return Type', 'Return Type',
'Short Name' 'Short Name'
]) ])
self._show_value = False
self.setEditTriggers(QAbstractItemView.AllEditTriggers) self.setEditTriggers(QAbstractItemView.AllEditTriggers)
self.var_list = [] self.var_list = []
# Инициализируем QSettings с именем организации и приложения
# QSettings
self.settings = QSettings("SET", "DebugVarEdit_VarTable") self.settings = QSettings("SET", "DebugVarEdit_VarTable")
# Восстанавливаем сохранённое состояние, если есть
shortsize = self.settings.value("shortname_size", True, type=int) shortsize = self.settings.value("shortname_size", True, type=int)
self._shortname_size = shortsize self._shortname_size = shortsize
self.type_options = list(dict.fromkeys(type_map.values())) self.type_options = list(dict.fromkeys(type_map.values()))
self.pt_types_all = [t.replace('pt_', '') for t in self.type_options] self.pt_types_all = [t.replace('pt_', '') for t in self.type_options]
self.iq_types_all = ['iq_none', 'iq'] + [f'iq{i}' for i in range(1, 31)] self.iq_types_all = ['iq_none', 'iq'] + [f'iq{i}' for i in range(1, 31)]
# Задаём базовые iq-типы (без префикса 'iq_')
self.iq_types = ['iq_none', 'iq', 'iq10', 'iq15', 'iq19', 'iq24'] self.iq_types = ['iq_none', 'iq', 'iq10', 'iq15', 'iq19', 'iq24']
# Фильтруем типы из type_map.values() исключая те, что содержат 'arr' или 'ptr' type_options = [t for t in dict.fromkeys(type_map.values()) if 'arr' not in t and 'ptr' not in t
type_options = [t for t in dict.fromkeys(type_map.values()) if 'arr' not in t and 'ptr' not in t and and 'struct' not in t and 'union' not in t and '64' not in t]
'struct' not in t and 'union' not in t and '64' not in t]
# Формируем display_type_options без префикса 'pt_'
self.pt_types = [t.replace('pt_', '') for t in type_options] self.pt_types = [t.replace('pt_', '') for t in type_options]
self._iq_type_filter = list(self.iq_types) # Текущий фильтр iq типов (по умолчанию все) self._iq_type_filter = list(self.iq_types)
self._pt_type_filter = list(self.pt_types) self._pt_type_filter = list(self.pt_types)
self._ret_type_filter = list(self.iq_types) self._ret_type_filter = list(self.iq_types)
header = self.horizontalHeader()
# Для остальных колонок — растяжение (Stretch), чтобы они заняли всю оставшуюся ширину
header = self.horizontalHeader()
for col in range(self.columnCount()): for col in range(self.columnCount()):
if col == self.columnCount() - 1: if col == self.columnCount() - 1:
header.setSectionResizeMode(col, QHeaderView.Stretch) header.setSectionResizeMode(col, QHeaderView.Stretch)
else: else:
header.setSectionResizeMode(col, QHeaderView.Interactive) header.setSectionResizeMode(col, QHeaderView.Interactive)
parent_widget = self.parentWidget()
# Сделаем колонки с номерами фиксированной ширины
self.setColumnWidth(rows.No, 30) self.setColumnWidth(rows.No, 30)
self.setColumnWidth(rows.include, 30) self.setColumnWidth(rows.include, 30)
self.setColumnWidth(rows.pt_type, 85) self.setColumnWidth(rows.pt_type, 85)
self.setColumnWidth(rows.iq_type, 85) self.setColumnWidth(rows.iq_type, 85)
self.setColumnWidth(rows.ret_type, 85) self.setColumnWidth(rows.ret_type, 85)
self.setColumnWidth(rows.name, 300) self.setColumnWidth(rows.name, 300)
self.setColumnWidth(rows.type, 100) self.setColumnWidth(rows.type, 100)
self._resizing = False self._resizing = False
self.horizontalHeader().sectionResized.connect(self.on_section_resized) self.horizontalHeader().sectionResized.connect(self.on_section_resized)
self.horizontalHeader().sectionClicked.connect(self.on_header_clicked) self.horizontalHeader().sectionClicked.connect(self.on_header_clicked)
def populate(self, vars_list, structs, on_change_callback): def populate(self, vars_list, structs, on_change_callback):
self.var_list = vars_list self.var_list = vars_list
self.setUpdatesEnabled(False)
self.blockSignals(True)
# --- ДО: удаляем отображение структур и union-переменных
for var in vars_list: for var in vars_list:
pt_type = var.get('pt_type', '') pt_type = var.get('pt_type', '')
if 'struct' in pt_type or 'union' in pt_type: if 'struct' in pt_type or 'union' in pt_type:
var['show_var'] = 'false' var['show_var'] = 'false'
var['enable'] = 'false' var['enable'] = 'false'
filtered_vars = [v for v in vars_list if v.get('show_var', 'false') == 'true'] filtered_vars = [v for v in vars_list if v.get('show_var', 'false') == 'true']
self.setRowCount(len(filtered_vars)) self.setRowCount(len(filtered_vars))
self.verticalHeader().setVisible(False) self.verticalHeader().setVisible(False)
style_with_padding = "padding-left: 5px; padding-right: 5px; font-size: 14pt; font-family: 'Segoe UI';" style_with_padding = "padding-left: 5px; padding-right: 5px; font-size: 14pt; font-family: 'Segoe UI';"
for row, var in enumerate(filtered_vars): for row, var in enumerate(filtered_vars):
# № # №
no_item = QTableWidgetItem(str(row)) no_item = QTableWidgetItem(str(row))
@@ -212,25 +217,21 @@ class VariableTableWidget(QTableWidget):
# Name # Name
name_edit = QLineEdit(var['name']) name_edit = QLineEdit(var['name'])
if var['type'] in structs:
completer = QCompleter(structs[var['type']].keys())
completer.setCaseSensitivity(Qt.CaseInsensitive)
name_edit.setCompleter(completer)
name_edit.textChanged.connect(on_change_callback) name_edit.textChanged.connect(on_change_callback)
name_edit.setStyleSheet(style_with_padding) name_edit.setStyleSheet(style_with_padding)
self.setCellWidget(row, rows.name, name_edit) self.setCellWidget(row, rows.name, name_edit)
# Origin Type (readonly) # Origin Type
origin_item = QTableWidgetItem(var.get('type', '')) origin_item = QTableWidgetItem(var.get('type', ''))
origin_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) origin_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
origin_item.setToolTip(var.get('type', '')) # Всплывающая подсказка origin_item.setToolTip(var.get('type', ''))
origin_item.setForeground(QBrush(Qt.black)) origin_item.setForeground(QBrush(Qt.black))
self.setItem(row, rows.type, origin_item) self.setItem(row, rows.type, origin_item)
# pt_type # pt_type
pt_combo = CtrlScrollComboBox() pt_combo = CtrlScrollComboBox()
pt_combo.addItems(self.pt_types) pt_combo.addItems(self.pt_types)
value = var['pt_type'].replace('pt_', '') value = var.get('pt_type', 'unknown').replace('pt_', '')
if value not in self.pt_types: if value not in self.pt_types:
pt_combo.addItem(value) pt_combo.addItem(value)
pt_combo.setCurrentText(value) pt_combo.setCurrentText(value)
@@ -241,7 +242,7 @@ class VariableTableWidget(QTableWidget):
# iq_type # iq_type
iq_combo = CtrlScrollComboBox() iq_combo = CtrlScrollComboBox()
iq_combo.addItems(self.iq_types) iq_combo.addItems(self.iq_types)
value = var['iq_type'].replace('t_', '') value = var.get('iq_type', 'iq_none').replace('t_', '')
if value not in self.iq_types: if value not in self.iq_types:
iq_combo.addItem(value) iq_combo.addItem(value)
iq_combo.setCurrentText(value) iq_combo.setCurrentText(value)
@@ -252,19 +253,32 @@ class VariableTableWidget(QTableWidget):
# return_type # return_type
ret_combo = CtrlScrollComboBox() ret_combo = CtrlScrollComboBox()
ret_combo.addItems(self.iq_types) ret_combo.addItems(self.iq_types)
value = var['return_type'].replace('t_', '') value = var.get('return_type', 'iq_none').replace('t_', '')
if value not in self.iq_types:
ret_combo.addItem(value)
ret_combo.setCurrentText(value) ret_combo.setCurrentText(value)
ret_combo.currentTextChanged.connect(on_change_callback) ret_combo.currentTextChanged.connect(on_change_callback)
ret_combo.setStyleSheet(style_with_padding) ret_combo.setStyleSheet(style_with_padding)
self.setCellWidget(row, rows.ret_type, ret_combo) self.setCellWidget(row, rows.ret_type, ret_combo)
# short_name # Последний столбец
if self._show_value:
val = var.get('value', '')
if val is None:
val = ''
val_edit = QLineEdit(str(val))
val_edit.textChanged.connect(on_change_callback)
val_edit.setStyleSheet(style_with_padding)
self.setCellWidget(row, rows.short_name, val_edit)
else:
short_name_val = var.get('shortname', var['name']) short_name_val = var.get('shortname', var['name'])
short_name_edit = QLineEdit(short_name_val) short_name_edit = QLineEdit(short_name_val)
short_name_edit.textChanged.connect(on_change_callback) short_name_edit.textChanged.connect(on_change_callback)
short_name_edit.setStyleSheet(style_with_padding) short_name_edit.setStyleSheet(style_with_padding)
self.setCellWidget(row, rows.short_name, short_name_edit) self.setCellWidget(row, rows.short_name, short_name_edit)
self.blockSignals(False)
self.setUpdatesEnabled(True)
self.check() self.check()
def check(self): def check(self):
@@ -375,10 +389,9 @@ class VariableTableWidget(QTableWidget):
combo.clear() combo.clear()
combo.addItems(allowed_items) combo.addItems(allowed_items)
if current in allowed_items: if current not in allowed_items:
combo.addItem(current)
combo.setCurrentText(current) combo.setCurrentText(current)
else:
combo.setCurrentIndex(0)
combo.blockSignals(False) combo.blockSignals(False)

View File

@@ -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 getDebugVar(DebugVar_t *var, int32_t *int_var, float *float_var);
static int convertDebugVarToIQx(DebugVar_t *var, int32_t *ret_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-----////////////////////////////// ///////////////////////////----EXAPLE-----//////////////////////////////
int var_numb = 1; ///< Ïðèìåð ïåðåìåííîé äëÿ îòëàäêè int var_numb = 1; ///< Ïðèìåð ïåðåìåííîé äëÿ îòëàäêè
@@ -24,8 +39,9 @@ DateTime_t ext_date = {2025, 11, 07, 16, 50}; ///<
*/ */
void Debug_Test_Example(void) void Debug_Test_Example(void)
{ {
return;
result = Debug_ReadVar(var_numb, &return_var); 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) 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) int Debug_ReadVar(int var_ind, int32_t *return_32b)
{ {
if(return_32b == NULL)
return 1;
int32_t tmp_var; int32_t tmp_var;
if(return_32b == NULL)
return DEBUG_ERR_INTERNAL;
if (var_ind >= DebugVar_Qnt) 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) || 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)) (dbg_vars[var_ind].ptr_type == pt_unknown))
return 1; return DEBUG_ERR_INVALID_VAR;
return convertDebugVarToIQx(&dbg_vars[var_ind], return_32b); 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 ×èòàåò èìÿ ïåðåìåííîé ïî èíäåêñó. * @brief ×èòàåò èìÿ ïåðåìåííîé ïî èíäåêñó.
* @param var_ind èíäåêñ ïåðåìåííîé. * @param var_ind èíäåêñ ïåðåìåííîé.
@@ -63,29 +139,33 @@ int Debug_ReadVar(int var_ind, int32_t *return_32b)
* @return int 0: óñïåõ, 1: îøèáêà. * @return int 0: óñïåõ, 1: îøèáêà.
* @details Êîïèðóåò èìÿ ïåðåìåííîé â ïðåäîñòàâëåííûé áóôåð. * @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) if(name_ptr == NULL)
return 1; return DEBUG_ERR_INTERNAL;
if (var_ind >= DebugVar_Qnt) if (var_ind >= DebugVar_Qnt)
return 1; return DEBUG_ERR_VAR_NUMB;
int i;
// Êîïèðîâàíèå ñ çàùèòîé îò ïåðåïîëíåíèÿ è ÿâíîé îñòàíîâêîé ïî '\0' // Êîïèðîâàíèå ñ çàùèòîé îò ïåðåïîëíåíèÿ è ÿâíîé îñòàíîâêîé ïî '\0'
for (i = 0; i < sizeof(dbg_vars[var_ind].name); i++) for (i = 0; i < sizeof(dbg_vars[var_ind].name); i++)
{ {
name_ptr[i] = dbg_vars[var_ind].name[i]; name_ptr[i] = dbg_vars[var_ind].name[i];
if (dbg_vars[var_ind].name[i] == '\0') if (dbg_vars[var_ind].name[i] == '\0')
{
if(length != NULL)
*length = i;
break; break;
} }
}
// Ãàðàíòèðîâàííîå çàâåðøåíèå ñòðîêè (íà ñëó÷àé, åñëè â var->name íå áûëî '\0') // Ãàðàíòèðîâàííîå çàâåðøåíèå ñòðîêè (íà ñëó÷àé, åñëè â var->name íå áûëî '\0')
name_ptr[sizeof(dbg_vars[var_ind].name) - 1] = '\0'; name_ptr[sizeof(dbg_vars[var_ind].name) - 1] = '\0';
return 0; return 0;
} }
/** /**
* @brief ×èòàåò çíà÷åíèå ïåðåìåííîé îòëàäêè ñ íèæíåãî óðîâíÿ. * @brief ×èòàåò çíà÷åíèå ïåðåìåííîé îòëàäêè ñ íèæíåãî óðîâíÿ.
* @param return_32b óêàçàòåëü íà ïåðåìåííóþ, êóäà çàïèñûâàåòñÿ ðåçóëüòàò. * @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) 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; uint8_t *addr = debug_ll.dbg_var.Ptr;
uint32_t addr_val = (uint32_t)addr; uint32_t addr_val = (uint32_t)addr;
// Ðàçðåø¸ííûå äèàïàçîíû ïàìÿòè (èç .cmd ôàéëà) if (return_32b == NULL)
if (!( return DEBUG_ERR_INTERNAL;
(addr_val <= 0x0007FF) || // RAMM0 + RAMM1 if (debug_ll.isVerified == 0)
(addr_val >= 0x008120 && addr_val <= 0x009FFC) || // L0 + L1 SARAM return DEBUG_ERR_DATATIME;
(addr_val >= 0x3F8000 && addr_val <= 0x3F9FFF) || // PRAMH0 + DRAMH0
(addr_val >= 0x3FF000 && addr_val <= 0x3FFFFF) || // BOOTROM + RESET if (is_addr_in_allowed_ranges(addr_val, debug_allowed_ranges, debug_allowed_ranges_count) != 0)
(addr_val >= 0x080002 && addr_val <= 0x09FFFF) || // RAMEX1 {
(addr_val >= 0x0F0000 && addr_val <= 0x0FFEFF) || // RAMEX4 return DEBUG_ERR_ADDR; // Çàïðåù¸ííûé àäðåñ — íåëüçÿ ÷èòàòü
(addr_val >= 0x100002 && addr_val <= 0x103FFF) || // RAMEX0 + RAMEX2 + RAMEX01
(addr_val >= 0x102000 && addr_val <= 0x103FFF) // RAMEX2
)) {
return 2; // Çàïðåù¸ííûé àäðåñ — íåëüçÿ ÷èòàòü
} }
return convertDebugVarToIQx(&debug_ll.dbg_var, return_32b); 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) int Debug_LowLevel_Initialize(DateTime_t* external_date)
{ {
if (external_date == NULL) { 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; 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) else if (t >= t_iq1 && t <= t_iq30)
return (int)t - (int)t_iq1 + 1; // íàïðèìåð t_iq1 -> 1, t_iq2 -> 2 è ò.ä. return (int)t - (int)t_iq1 + 1; // íàïðèìåð t_iq1 -> 1, t_iq2 -> 2 è ò.ä.
else 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) static int convertDebugVarToIQx(DebugVar_t *var, int32_t *ret_var)
{ {
int32_t iq_numb, iq_united, iq_final; int32_t iq_numb, iq_united, iq_final;
int64_t iq_united64 = 0;
int64_t iq_final64 = 0;
int status;
float float_numb; float float_numb;
if(getDebugVar(var, &iq_numb, &float_numb) != 0) status = getDebugVar(var, &iq_numb, &float_numb);
return 1; if(status != 0)
return status;
int src_q = iqTypeToQ(var->iq_type); int src_q = iqTypeToQ(var->iq_type);
int dst_q = iqTypeToQ(var->return_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-áèò) // Êîíâåðòàöèÿ ê GLOBAL_Q (64-áèò)
if (var->iq_type == t_iq_none) { if (var->iq_type == t_iq_none) {
if (var->ptr_type == pt_float) { if (var->ptr_type == pt_float) {
// float_numb óìíîæàåì íà 2^GLOBAL_Q // float_numb óìíîæàåì íà 2^GLOBAL_Q
// Ðåçóëüòàò ïðèâîäèì ê 64 áèòà // Ðåçóëüòàò ïðèâîäèì ê 64 áèòà
iq_united64 = (int64_t)(float_numb * (1 << GLOBAL_Q)); iq_united64 = (int64_t)(float_numb * ((uint32_t)1 << GLOBAL_Q));
} else { } else {
iq_united64 = ((int64_t)iq_numb) << GLOBAL_Q; iq_united64 = ((int64_t)iq_numb) << GLOBAL_Q;
} }
@@ -222,11 +346,7 @@ static int convertDebugVarToIQx(DebugVar_t *var, int32_t *ret_var)
else else
iq_final64 = iq_united64 >> (-shift); iq_final64 = iq_united64 >> (-shift);
// Ïðîâåðÿåì ïåðåïîëíåíèå int32_t *ret_var = (int32_t)iq_final64;
if (iq_final64 > 2147483647 || iq_final64 < -2147483648)
return 3; // ïåðåïîëíåíèå
*ret_var = (uint32_t)iq_final64;
} }
return 0; 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) 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; uint8_t *addr = var->Ptr;
uint32_t addr_val = (uint32_t)addr; uint32_t addr_val = (uint32_t)addr;
if (!var || !int_var || !float_var || !var->Ptr)
return DEBUG_ERR_INTERNAL; // îøèáêà: null óêàçàòåëü
switch (var->ptr_type) switch (var->ptr_type)
{ {
case pt_int8: // 8 áèò case pt_int8: // 8 áèò
if ((addr_val & ALIGN_8BIT) != 0) // ïðîâåðÿåì âûðàâíèâàíèå if ((addr_val & ALIGN_8BIT) != 0) // ïðîâåðÿåì âûðàâíèâàíèå
return 1; // îøèáêà âûðàâíèâàíèÿ return DEBUG_ERR_ADDR_ALIGN; // îøèáêà âûðàâíèâàíèÿ
*int_var = *((volatile int8_t *)addr); *int_var = *((volatile int8_t *)addr);
break;
case pt_uint8: case pt_uint8:
if ((addr_val & ALIGN_8BIT) != 0) // ïðîâåðÿåì âûðàâíèâàíèå if ((addr_val & ALIGN_8BIT) != 0) // ïðîâåðÿåì âûðàâíèâàíèå
return 1; // îøèáêà âûðàâíèâàíèÿ return DEBUG_ERR_ADDR_ALIGN; // îøèáêà âûðàâíèâàíèÿ
*int_var = *((volatile uint8_t *)addr); *int_var = *((volatile uint8_t *)addr);
break; break;
case pt_int16: // 16 áèò (int) case pt_int16: // 16 áèò (int)
if ((addr_val & ALIGN_16BIT) != 0) // ïðîâåðÿåì âûðàâíèâàíèå if ((addr_val & ALIGN_16BIT) != 0) // ïðîâåðÿåì âûðàâíèâàíèå
return 2; // îøèáêà âûðàâíèâàíèÿ return DEBUG_ERR_ADDR_ALIGN; // îøèáêà âûðàâíèâàíèÿ
*int_var = *((volatile int16_t *)addr); *int_var = *((volatile int16_t *)addr);
break;
case pt_uint16: case pt_uint16:
if ((addr_val & ALIGN_16BIT) != 0) // ïðîâåðÿåì âûðàâíèâàíèå if ((addr_val & ALIGN_16BIT) != 0) // ïðîâåðÿåì âûðàâíèâàíèå
return 2; // îøèáêà âûðàâíèâàíèÿ return DEBUG_ERR_ADDR_ALIGN; // îøèáêà âûðàâíèâàíèÿ
*int_var = *((volatile uint16_t *)addr); *int_var = *((volatile uint16_t *)addr);
break; break;
case pt_int32: // 32 áèò case pt_int32: // 32 áèò
if ((addr_val & ALIGN_32BIT) != 0) // ïðîâåðÿåì âûðàâíèâàíèå if ((addr_val & ALIGN_32BIT) != 0) // ïðîâåðÿåì âûðàâíèâàíèå
return 3; // îøèáêà âûðàâíèâàíèÿ return DEBUG_ERR_ADDR_ALIGN; // îøèáêà âûðàâíèâàíèÿ
*int_var = *((volatile int32_t *)addr); *int_var = *((volatile int32_t *)addr);
break;
case pt_uint32: case pt_uint32:
if ((addr_val & ALIGN_32BIT) != 0) // ïðîâåðÿåì âûðàâíèâàíèå if ((addr_val & ALIGN_32BIT) != 0) // ïðîâåðÿåì âûðàâíèâàíèå
return 3; // îøèáêà âûðàâíèâàíèÿ return DEBUG_ERR_ADDR_ALIGN; // îøèáêà âûðàâíèâàíèÿ
*int_var = *((volatile uint32_t *)addr); *int_var = *((volatile uint32_t *)addr);
break; break;
case pt_float: // float (4 áàéòà) case pt_float: // float (4 áàéòà)
if ((addr_val & ALIGN_FLOAT) != 0) // ïðîâåðêà âûðàâíèâàíèÿ if ((addr_val & ALIGN_FLOAT) != 0) // ïðîâåðêà âûðàâíèâàíèÿ
return 4; // îøèáêà âûðàâíèâàíèÿ return DEBUG_ERR_ADDR_ALIGN; // îøèáêà âûðàâíèâàíèÿ
*float_var = *((volatile float *)addr); *float_var = *((volatile float *)addr);
break; break;
default: default:
return 1; // íåïîääåðæèâàåìûé òèï return DEBUG_ERR_INVALID_VAR; // íåïîääåðæèâàåìûé òèï
// äëÿ óêàçàòåëåé è ìàññèâîâ íå ïîääåðæèâàåòñÿ ÷òåíèå // äëÿ óêàçàòåëåé è ìàññèâîâ íå ïîääåðæèâàåòñÿ ÷òåíèå
// case pt_ptr_int8: // case pt_ptr_int8:
// case pt_ptr_int16: // case pt_ptr_int16:
@@ -306,3 +429,23 @@ static int getDebugVar(DebugVar_t *var, int32_t *int_var, float *float_var)
return 0; // óñïåõ 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;
}

View File

@@ -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 бит #if UINT8_MAX // Если есть тип 8 бит - знчачит адресация по 8 бит
#define ALIGN_8BIT 0x0 ///< Выравнивание без ограничений (любой адрес) #define ALIGN_8BIT 0x0 ///< Выравнивание без ограничений (любой адрес)
@@ -31,6 +43,19 @@
#define NULL 0 #define NULL 0
#endif #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
#define DEBUG_ERR_RS (1<<6) | DEBUG_ERR
/** /**
* @brief Тип данных, на который указывает указатель переменной отладки. * @brief Тип данных, на который указывает указатель переменной отладки.
@@ -129,6 +154,13 @@ typedef struct {
uint8_t minute; ///< Минуты (0-59) uint8_t minute; ///< Минуты (0-59)
} DateTime_t; } DateTime_t;
/**
* @brief Ñòðóêòóðà, îïèñûâàþùàÿ äèàïàçîí àäðåñîâ ïàìÿòè.
*/
typedef struct {
uint32_t start; ///< Íà÷àëüíûé àäðåñ äèàïàçîíà
uint32_t end; ///< Êîíå÷íûé àäðåñ äèàïàçîíà (âêëþ÷èòåëüíî)
} AddrRange_t;
/** /**
* @brief Структура нижнего уровня отладки. * @brief Структура нижнего уровня отладки.
*/ */
@@ -159,10 +191,19 @@ void Debug_Test_Example(void);
/* Читает значение переменной по индексу */ /* Читает значение переменной по индексу */
int Debug_ReadVar(int var_ind, int32_t *return_long); 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_ReadVar(int32_t *return_long);
/* Инициализирует отладку нижнего уровня */ /* Инициализирует отладку нижнего уровня */
int Debug_LowLevel_Initialize(DateTime_t *external_date); int Debug_LowLevel_Initialize(DateTime_t *external_date);
/* ×èòàåò âîçâðàùàåìûé òèï (IQ) íèçêîóðîâíåíî çàäàííîé ïåðåìåííîé */
int Debug_LowLevel_ReadVarReturnType(int *vartype);
/* ×èòàåò òèï íèçêîóðîâíåíî çàäàííîé ïåðåìåííîé.*/
int Debug_LowLevel_ReadVarType(int *vartype);
#endif //DEBUG_TOOLS #endif //DEBUG_TOOLS

View File

@@ -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
View 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

Binary file not shown.