501 lines
21 KiB
Python
501 lines
21 KiB
Python
"""
|
||
VariablesXML + get_all_vars_data
|
||
---------------------------------
|
||
Поддержка вложенных структур, указателей на структуры ("->"),
|
||
и многомерных массивов (индексация [i][j]...).
|
||
|
||
Требования пользователя:
|
||
- size (без индекса) = общий размер массива в байтах (НЕ измерение!).
|
||
- size1..sizeN = размеры измерений массива.
|
||
- В результирующем плоском списке (flattened) должны присутствовать ВСЕ промежуточные
|
||
пути: var, var[0], var[0][0], var[0][0].field, var[0][0].field->subfield, ...
|
||
- Аналогично для членов структур.
|
||
|
||
Пример желаемого формата:
|
||
project
|
||
project.adc
|
||
project.adc[0]
|
||
project.adc[0][0]
|
||
project.adc[0][0].bus
|
||
project.adc[0][0].bus->status
|
||
|
||
Данный модуль реализует:
|
||
- Разбор XML (parse) с извлечением размеров размерностей в поле `dims`.
|
||
- Генерацию плоского списка словарей `flattened()`.
|
||
- Построение иерархии словарей `get_all_vars_data()`.
|
||
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
import xml.etree.ElementTree as ET
|
||
from dataclasses import dataclass, field
|
||
from typing import List, Dict, Optional, Tuple, Any
|
||
|
||
import var_setup # ожидается split_path(...)
|
||
from generate_debug_vars import choose_type_map, type_map # используется для выбора карт типов
|
||
|
||
# --------------------------- константы ----------------------------
|
||
|
||
DATE_FIELD_SET = {"year", "month", "day", "hour", "minute"}
|
||
|
||
# --------------------------- dataclasses --------------------------
|
||
|
||
@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', ...
|
||
dims: Optional[List[int]] = None
|
||
|
||
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'
|
||
dims: Optional[List[int]] = None # полный список размеров [size1, size2, ...]
|
||
|
||
def base_address_hex(self) -> str:
|
||
return f"0x{self.address:06X}"
|
||
|
||
|
||
# --------------------------- класс парсера -----------------------
|
||
|
||
class VariablesXML:
|
||
"""
|
||
Читает XML и предоставляет методы:
|
||
- flattened(): плоский список всех путей.
|
||
- date_struct_candidates(): как раньше.
|
||
|
||
Правила формирования путей:
|
||
* Структурные поля: '.'
|
||
* Поля через указатель на структуру: '->'
|
||
* Массивы: [index] (каждое измерение).
|
||
"""
|
||
|
||
# предполагаемые размеры примитивов (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()
|
||
|
||
# ------------------ утилиты ------------------
|
||
|
||
@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 _is_union(t: str) -> bool:
|
||
if not t:
|
||
return False
|
||
low = t.strip()
|
||
return low.startswith('union ')
|
||
|
||
@staticmethod
|
||
def _strip_array_suffix(t: str) -> str:
|
||
t = t.strip()
|
||
while t.endswith('[]'):
|
||
t = t[:-2].strip()
|
||
return 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) -> None:
|
||
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: ET.Element, base_offset=0) -> 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')
|
||
|
||
abs_offset = base_offset + offset
|
||
|
||
|
||
# Собираем размеры, если есть
|
||
dims: List[int] = []
|
||
i = 1
|
||
while True:
|
||
size_key = f"size{i}"
|
||
size_val = elem.get(size_key)
|
||
if size_val is None:
|
||
break
|
||
parsed = self._parse_int_guess(size_val) # предполагается твоя функция парсинга int
|
||
if parsed is not None:
|
||
dims.append(parsed)
|
||
i += 1
|
||
|
||
node = MemberNode(
|
||
name=name,
|
||
offset=abs_offset,
|
||
type_str=t,
|
||
size=size,
|
||
kind=kind,
|
||
dims=dims if dims else None,
|
||
)
|
||
|
||
# Для детей
|
||
for ch in elem.findall('member'):
|
||
if kind == 'union':
|
||
# Для union детей НЕ добавляем их offset, просто передаём abs_offset
|
||
child = parse_member(ch, base_offset=abs_offset)
|
||
child.offset = abs_offset # выравниваем offset, игнорируем offset детей
|
||
else:
|
||
# Для struct/array суммируем offset нормально
|
||
child = parse_member(ch, base_offset=abs_offset)
|
||
node.children.append(child)
|
||
|
||
# Аналогично для pointee
|
||
pointee_elem = elem.find('pointee')
|
||
if pointee_elem is not None:
|
||
for ch in pointee_elem.findall('member'):
|
||
if kind == 'union':
|
||
child = parse_member(ch, base_offset=abs_offset)
|
||
child.offset = abs_offset
|
||
else:
|
||
child = parse_member(ch, base_offset=abs_offset)
|
||
node.children.append(child)
|
||
size_p = pointee_elem.get('size')
|
||
if size_p:
|
||
node.size = int(size_p, 16)
|
||
|
||
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')
|
||
|
||
dims: List[int] = []
|
||
i = 1
|
||
while True:
|
||
key = f'size{i}'
|
||
val = var.get(key)
|
||
if val is None:
|
||
break
|
||
parsed = self._parse_int_guess(val)
|
||
if parsed is not None:
|
||
dims.append(parsed)
|
||
i += 1
|
||
|
||
members = [parse_member(m) for m in var.findall('member')]
|
||
|
||
v = VariableNode(
|
||
name=name,
|
||
address=addr,
|
||
type_str=t,
|
||
size=size,
|
||
members=members,
|
||
kind=kind,
|
||
dims=dims if dims else None,
|
||
)
|
||
self.variables.append(v)
|
||
except FileNotFoundError:
|
||
self.variables = []
|
||
except ET.ParseError:
|
||
self.variables = []
|
||
|
||
# ------------------ helpers для flattened ---------------------
|
||
|
||
def _elem_size_bytes(self, total_size: Optional[int], dims: List[int], base_type: str, members: List[MemberNode]) -> int:
|
||
"""Оценка размера одного *листового* элемента (последнего измерения).
|
||
Если total_size и dims все известны — берём size / prod(dims).
|
||
Иначе — пробуем примитивный размер; иначе 1.
|
||
(Не учитываем выравнивание структур; при необходимости можно расширить.)
|
||
"""
|
||
if total_size is not None and dims:
|
||
prod = 1
|
||
for d in dims:
|
||
if d is None or d == 0:
|
||
prod = None
|
||
break
|
||
prod *= d
|
||
if prod and prod > 0:
|
||
return max(1, total_size // prod)
|
||
prim = self._guess_primitive_size(base_type)
|
||
if prim:
|
||
return prim
|
||
# Если структура и у неё есть size по детям? Пока fallback=1.
|
||
return 1
|
||
|
||
# ------------------ flattened ------------------
|
||
|
||
def flattened(self, max_array_elems: Optional[int] = None) -> List[Dict[str, Any]]:
|
||
"""Возвращает плоский список всех путей (каждый путь = dict).
|
||
Включает промежуточные узлы массивов (var[0], var[0][0], ...).
|
||
"""
|
||
out: List[Dict[str, Any]] = []
|
||
|
||
def mk(name: str, addr: Optional[int], type_str: str, size: Optional[int], kind: Optional[str], dims_for_node: Optional[List[int]]):
|
||
if 'Bender' in name:
|
||
a=1
|
||
out.append({
|
||
'name': name,
|
||
'address': addr,
|
||
'type': type_str,
|
||
'size': size,
|
||
'kind': kind,
|
||
'dims': dims_for_node[:] if dims_for_node else None,
|
||
})
|
||
|
||
def expand_members(prefix: str, base_addr: int, members: List[MemberNode], parent_is_ptr_struct: bool, parent_is_union: bool) -> None:
|
||
# Выбираем разделитель пути: '.' если обычный член, '->' если указатель на структуру
|
||
join = '->' if parent_is_ptr_struct else '.'
|
||
|
||
for m in members:
|
||
path_m = f"{prefix}{join}{m.name}" if prefix else m.name
|
||
is_union = m.kind == 'union' or parent_is_union
|
||
if is_union:
|
||
# Все поля union начинаются с одного адреса
|
||
addr_m = base_addr
|
||
else:
|
||
addr_m = base_addr + m.offset
|
||
|
||
dims = m.dims or []
|
||
|
||
mk(path_m, addr_m, m.type_str, m.size, m.kind, dims)
|
||
|
||
if m.kind == 'array' and dims:
|
||
base_t = self._strip_array_suffix(m.type_str)
|
||
elem_sz = m.size
|
||
|
||
# Для массива внутри структуры: первый уровень — '.' для доступа,
|
||
# внутри массива раскрываем по обычной логике с parent_is_ptr_struct=False
|
||
expand_dims(path_m, addr_m, dims, base_t, m.children, elem_sz, parent_is_ptr_struct=False)
|
||
else:
|
||
if m.children:
|
||
# Проверяем, является ли поле указателем на структуру
|
||
is_ptr = self._is_pointer_to_struct(m.type_str)
|
||
# Рекурсивно раскрываем дочерние поля, выбирая правильный разделитель
|
||
expand_members(path_m, addr_m, m.children, is_ptr, is_union)
|
||
|
||
|
||
def expand_dims(name: str, base_addr: int, dims: List[int], base_type: str, children: List[MemberNode], elem_size: int, parent_is_ptr_struct: bool) -> None:
|
||
prods: List[int] = []
|
||
acc = 1
|
||
for d in reversed(dims[1:]):
|
||
acc *= (d if d else 1)
|
||
prods.append(acc)
|
||
prods.reverse()
|
||
|
||
def rec(k: int, cur_name: str, cur_addr: int) -> None:
|
||
if k == len(dims):
|
||
# Листовой элемент массива
|
||
mk(cur_name, cur_addr, base_type, elem_size, None, None)
|
||
# Если элемент — структура или указатель на структуру, раскрываем вложения
|
||
if children and self._is_struct_or_union(base_type):
|
||
expand_members(cur_name, cur_addr, children, parent_is_ptr_struct=False, parent_is_union=self._is_union(base_type))
|
||
elif self._is_pointer_to_struct(base_type):
|
||
expand_members(cur_name, cur_addr, children, parent_is_ptr_struct=True, parent_is_union=self._is_union(base_type))
|
||
return
|
||
|
||
dim_sz = dims[k] or 0
|
||
if max_array_elems is not None:
|
||
dim_sz = min(dim_sz, max_array_elems)
|
||
|
||
stride = elem_size * prods[k] if k < len(prods) else elem_size
|
||
if len(dims) > 2:
|
||
a=1
|
||
for i in range(dim_sz):
|
||
child_name = f"{cur_name}[{i}]"
|
||
child_addr = (cur_addr + i * stride) if cur_addr is not None else None
|
||
remaining = dims[k+1:]
|
||
mk(child_name, child_addr, base_type + '[]' * len(remaining), stride if remaining else elem_size, 'array' if remaining else None, remaining)
|
||
rec(k + 1, child_name, child_addr)
|
||
|
||
rec(0, name, base_addr)
|
||
|
||
|
||
# --- цикл по топ‑левел переменным ---
|
||
for v in self.variables:
|
||
dims = v.dims or []
|
||
mk(v.name, v.address, v.type_str, v.size, v.kind, dims)
|
||
if (v.kind == 'array' or v.type_str.endswith('[]')) and dims:
|
||
base_t = self._strip_array_suffix(v.type_str)
|
||
elem_sz = v.size
|
||
expand_dims(v.name, v.address, dims, base_t, v.members, elem_sz, parent_is_ptr_struct=False)
|
||
else:
|
||
if v.members:
|
||
is_ptr = self._is_pointer_to_struct(v.type_str)
|
||
is_union = self._is_union(v.type_str)
|
||
expand_members(v.name, v.address, v.members, is_ptr, is_union)
|
||
|
||
return out
|
||
|
||
# -------------------- date candidates (как раньше) -------------
|
||
|
||
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
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# Построение иерархического дерева из flattened()
|
||
# ------------------------------------------------------------------
|
||
def get_all_vars_data(self) -> List[Dict[str, Any]]:
|
||
"""
|
||
Строит иерархию словарей из плоского списка переменных.
|
||
|
||
Каждый узел = {
|
||
'name': <полный путь>,
|
||
'address': <адрес или None>,
|
||
'type': <тип>,
|
||
'size': <байты>,
|
||
'kind': <'array' | ...>,
|
||
'dims': [size1, size2, ...] или None,
|
||
'children': [...список дочерних узлов]
|
||
}
|
||
|
||
Возвращает список корневых узлов (top-level переменных).
|
||
"""
|
||
flat_data = self.flattened(max_array_elems=None)
|
||
|
||
# Быстрое отображение имя -> узел (словарь с детьми)
|
||
all_nodes: Dict[str, Dict[str, Any]] = {}
|
||
for item in flat_data:
|
||
node = dict(item)
|
||
node['children'] = []
|
||
all_nodes[item['name']] = node
|
||
|
||
def _parent_struct_split(path: str) -> Optional[str]:
|
||
# Ищем последний '.' или '->' для определения родителя
|
||
dot_idx = path.rfind('.')
|
||
arrow_idx = path.rfind('->')
|
||
cut_idx = max(dot_idx, arrow_idx)
|
||
if cut_idx == -1:
|
||
return None
|
||
# '->' занимает 2 символа, нужно взять срез до начала '->'
|
||
if arrow_idx > dot_idx:
|
||
return path[:arrow_idx]
|
||
else:
|
||
return path[:dot_idx]
|
||
|
||
def find_parent(path: str) -> Optional[str]:
|
||
"""
|
||
Возвращает полный путь родителя, учитывая '.', '->' и индексы [] в конце.
|
||
|
||
Если путь заканчивается индексом [k], удаляет последний индекс и проверяет наличие родителя.
|
||
Иначе пытается найти последний сепаратор '.' или '->'.
|
||
"""
|
||
# Если есть trailing индекс в конце, убираем его
|
||
m = re.search(r'\[[0-9]+\]$', path)
|
||
if m:
|
||
base = path[:m.start()] # убираем последний [k]
|
||
# Если базовый путь есть в узлах, считаем его родителем
|
||
if base in all_nodes:
|
||
return base
|
||
# Иначе пытаемся найти родителя от базового пути
|
||
return _parent_struct_split(base)
|
||
else:
|
||
# Если нет индекса, просто ищем последний разделитель
|
||
return _parent_struct_split(path)
|
||
|
||
# Строим иерархию: parent -> children
|
||
roots: List[Dict[str, Any]] = []
|
||
for full_name, node in all_nodes.items():
|
||
parent_name = find_parent(full_name)
|
||
if parent_name and parent_name in all_nodes:
|
||
all_nodes[parent_name]['children'].append(node)
|
||
else:
|
||
roots.append(node)
|
||
|
||
# Рекурсивно сортируем детей по имени для порядка
|
||
def sort_nodes(nodes: List[Dict[str, Any]]):
|
||
nodes.sort(key=lambda n: n['name'])
|
||
for n in nodes:
|
||
if n['children']:
|
||
sort_nodes(n['children'])
|
||
|
||
sort_nodes(roots)
|
||
return roots
|