""" 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