базово: +сделан lowlevel для кучи переменных (пока работает медленно) +сделан сохранение принимаемых значений в лог + gui терминалок подогнаны под один стиль плюс минус
439 lines
20 KiB
Python
439 lines
20 KiB
Python
|
||
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
|
||
|