опять кууууча всего:
базово доделаны терминалки до более менее итогового состояния
This commit is contained in:
		
							parent
							
								
									e99de603e6
								
							
						
					
					
						commit
						502046091c
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -6,3 +6,4 @@
 | 
			
		||||
/DebugVarEdit_GUI.dist
 | 
			
		||||
/DebugVarEdit_GUI.onefile-build
 | 
			
		||||
/parse_xml/build/
 | 
			
		||||
/parse_xml/Src/__pycache__/
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								DebugVarEdit.exe
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								DebugVarEdit.exe
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							@ -191,14 +191,39 @@ class VarEditor(QWidget):
 | 
			
		||||
 | 
			
		||||
        self.setLayout(layout)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def open_terminal(self, target):
 | 
			
		||||
        target = target.lower()
 | 
			
		||||
        
 | 
			
		||||
        if target == "tms":
 | 
			
		||||
            self.terminal_widget = _DemoWindow()  # _DemoWindow наследует QWidget
 | 
			
		||||
            self.terminal_widget.show()
 | 
			
		||||
            exe_name = "DebugVarTerminal.exe"
 | 
			
		||||
            # Путь к exe в текущей директории запуска программы
 | 
			
		||||
            exe_path = os.path.join(os.getcwd(), exe_name)
 | 
			
		||||
            
 | 
			
		||||
            if not os.path.isfile(exe_path):
 | 
			
		||||
                # Файл не найден — попросим пользователя выбрать путь к exe
 | 
			
		||||
                msg = QMessageBox()
 | 
			
		||||
                msg.setIcon(QMessageBox.Warning)
 | 
			
		||||
                msg.setWindowTitle("Файл не найден")
 | 
			
		||||
                msg.setText(f"Файл {exe_name} не найден в текущей папке.\nВыберите путь к {exe_name}.")
 | 
			
		||||
                msg.exec_()
 | 
			
		||||
                
 | 
			
		||||
                # Открываем диалог выбора файла
 | 
			
		||||
                selected_path, _ = QFileDialog.getOpenFileName(
 | 
			
		||||
                    None, "Выберите файл " + exe_name, os.getcwd(), "Executable Files (*.exe)"
 | 
			
		||||
                )
 | 
			
		||||
                
 | 
			
		||||
                if not selected_path:
 | 
			
		||||
                    # Пользователь отменил выбор — ничего не делаем
 | 
			
		||||
                    return
 | 
			
		||||
                
 | 
			
		||||
                exe_path = selected_path
 | 
			
		||||
            
 | 
			
		||||
            # Запускаем exe (отдельное окно терминала)
 | 
			
		||||
            subprocess.Popen([exe_path], creationflags=subprocess.CREATE_NEW_CONSOLE)
 | 
			
		||||
            
 | 
			
		||||
        elif target == "modbus":
 | 
			
		||||
            a=1
 | 
			
		||||
            a = 1
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
    def on_target_selected(self, target):
 | 
			
		||||
 | 
			
		||||
@ -1,34 +1,57 @@
 | 
			
		||||
"""
 | 
			
		||||
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 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
 | 
			
		||||
from typing import List, Dict, Optional, Tuple, Any
 | 
			
		||||
 | 
			
		||||
DATE_FIELD_SET = {'year','month','day','hour','minute'}
 | 
			
		||||
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)
 | 
			
		||||
    # --- новые, но необязательные (совместимость) ---
 | 
			
		||||
    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 (число элементов в массиве)
 | 
			
		||||
    dims: Optional[List[int]] = None
 | 
			
		||||
 | 
			
		||||
    def is_date_struct(self) -> bool:
 | 
			
		||||
        if not self.children:
 | 
			
		||||
@ -44,51 +67,57 @@ class VariableNode:
 | 
			
		||||
    type_str: str
 | 
			
		||||
    size: Optional[int]
 | 
			
		||||
    members: List[MemberNode] = field(default_factory=list)
 | 
			
		||||
    # --- новые, но необязательные ---
 | 
			
		||||
    # --- доп.поля ---
 | 
			
		||||
    kind: Optional[str] = None                # 'array'
 | 
			
		||||
    count: Optional[int] = None         # size1
 | 
			
		||||
    dims: Optional[List[int]] = None          # полный список размеров [size1, size2, ...]
 | 
			
		||||
 | 
			
		||||
    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 '.'
 | 
			
		||||
    Читает XML и предоставляет методы:
 | 
			
		||||
      - flattened(): плоский список всех путей.
 | 
			
		||||
      - date_struct_candidates(): как раньше.
 | 
			
		||||
 | 
			
		||||
    Правила формирования путей:
 | 
			
		||||
      * Структурные поля: '.'
 | 
			
		||||
      * Поля через указатель на структуру: '->'
 | 
			
		||||
      * Массивы: [index] (каждое измерение).
 | 
			
		||||
    """
 | 
			
		||||
    # assumed primitive sizes (for STM/MCU: int=2)
 | 
			
		||||
 | 
			
		||||
    # предполагаемые размеры примитивов (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,
 | 
			
		||||
        '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.timestamp: str = ""
 | 
			
		||||
        self.variables: List[VariableNode] = []
 | 
			
		||||
        choose_type_map(0)
 | 
			
		||||
        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')):
 | 
			
		||||
        if txt.startswith(('0x', '0X')):
 | 
			
		||||
            return int(txt, 16)
 | 
			
		||||
        # если в строке есть буквы A-F → возможно hex
 | 
			
		||||
        # если в строке есть A-F, попробуем hex
 | 
			
		||||
        if any(c in 'abcdefABCDEF' for c in txt):
 | 
			
		||||
            try:
 | 
			
		||||
                return int(txt, 16)
 | 
			
		||||
@ -103,7 +132,7 @@ class VariablesXML:
 | 
			
		||||
    def _is_pointer_to_struct(t: str) -> bool:
 | 
			
		||||
        if not t:
 | 
			
		||||
            return False
 | 
			
		||||
        low = t.replace('\t',' ').replace('\n',' ')
 | 
			
		||||
        low = t.replace('\t', ' ').replace('\n', ' ')
 | 
			
		||||
        return 'struct ' in low and '*' in low
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
@ -113,24 +142,34 @@ class VariablesXML:
 | 
			
		||||
        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:
 | 
			
		||||
        return t[:-2].strip() if t.endswith('[]') else t
 | 
			
		||||
        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'):
 | 
			
		||||
        for tok in ('volatile', 'const'):
 | 
			
		||||
            base = base.replace(tok, '')
 | 
			
		||||
        base = base.replace('*',' ')
 | 
			
		||||
        base = base.replace('[',' ').replace(']',' ')
 | 
			
		||||
        base = base.replace('*', ' ')
 | 
			
		||||
        base = base.replace('[', ' ').replace(']', ' ')
 | 
			
		||||
        base = ' '.join(base.split()).strip()
 | 
			
		||||
        return self._PRIM_SIZE.get(base)
 | 
			
		||||
 | 
			
		||||
    # ------------------ XML read ------------------
 | 
			
		||||
 | 
			
		||||
    def _parse(self):
 | 
			
		||||
    def _parse(self) -> None:
 | 
			
		||||
        try:
 | 
			
		||||
            tree = ET.parse(self.path)
 | 
			
		||||
            root = tree.getroot()
 | 
			
		||||
@ -138,204 +177,235 @@ class VariablesXML:
 | 
			
		||||
            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 ''
 | 
			
		||||
            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
 | 
			
		||||
                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)
 | 
			
		||||
 | 
			
		||||
                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'):
 | 
			
		||||
                    node.children.append(parse_member(ch))
 | 
			
		||||
                    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
 | 
			
		||||
                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)
 | 
			
		||||
 | 
			
		||||
                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')]
 | 
			
		||||
                self.variables.append(
 | 
			
		||||
                    VariableNode(name=name, address=addr, type_str=t, size=size,
 | 
			
		||||
                                 members=members, kind=kind, count=count)
 | 
			
		||||
 | 
			
		||||
                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 = []
 | 
			
		||||
 | 
			
		||||
    # ------------------ flatten (expanded) ------------------
 | 
			
		||||
    # ------------------ helpers для flattened ---------------------
 | 
			
		||||
 | 
			
		||||
    def flattened(self,
 | 
			
		||||
                  max_array_elems: Optional[int] = None
 | 
			
		||||
                  ) -> List[Dict[str, Any]]:
 | 
			
		||||
    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.
 | 
			
		||||
        (Не учитываем выравнивание структур; при необходимости можно расширить.)
 | 
			
		||||
        """
 | 
			
		||||
        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).
 | 
			
		||||
        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 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 {
 | 
			
		||||
        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': address,
 | 
			
		||||
                'address': addr,
 | 
			
		||||
                'type': type_str,
 | 
			
		||||
                'size': size,
 | 
			
		||||
                'kind': kind,
 | 
			
		||||
                'count': count
 | 
			
		||||
            }
 | 
			
		||||
                'dims': dims_for_node[:] if dims_for_node else None,
 | 
			
		||||
            })
 | 
			
		||||
            
 | 
			
		||||
        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 '.'
 | 
			
		||||
            """
 | 
			
		||||
        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_name}{join}{m.name}" if prefix_name else m.name
 | 
			
		||||
                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
 | 
			
		||||
                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
 | 
			
		||||
                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)
 | 
			
		||||
                    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'
 | 
			
		||||
                    elem_sz = m.size
 | 
			
		||||
 | 
			
		||||
                        # 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)
 | 
			
		||||
                    # Для массива внутри структуры: первый уровень — '.' для доступа,
 | 
			
		||||
                    # внутри массива раскрываем по обычной логике с 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_struct = self._is_pointer_to_struct(m.type_str)
 | 
			
		||||
                    expand_members(path_m, addr_m, m.children, parent_is_ptr_struct=is_ptr_struct)
 | 
			
		||||
                        # Проверяем, является ли поле указателем на структуру
 | 
			
		||||
                        is_ptr = self._is_pointer_to_struct(m.type_str)
 | 
			
		||||
                        # Рекурсивно раскрываем дочерние поля, выбирая правильный разделитель
 | 
			
		||||
                        expand_members(path_m, addr_m, m.children, is_ptr, is_union)
 | 
			
		||||
 | 
			
		||||
        # --- top-level variables ---
 | 
			
		||||
 | 
			
		||||
        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:
 | 
			
		||||
            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:
 | 
			
		||||
            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)
 | 
			
		||||
                    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
 | 
			
		||||
                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_struct = self._is_pointer_to_struct(v.type_str)
 | 
			
		||||
                expand_members(v.name, v.address, v.members, parent_is_ptr_struct=is_ptr_struct)
 | 
			
		||||
                    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 (as it was) --------------------
 | 
			
		||||
    # -------------------- date candidates (как раньше) -------------
 | 
			
		||||
 | 
			
		||||
    def date_struct_candidates(self) -> List[Tuple[str,int]]:
 | 
			
		||||
    def date_struct_candidates(self) -> List[Tuple[str, int]]:
 | 
			
		||||
        cands = []
 | 
			
		||||
        for v in self.variables:
 | 
			
		||||
            # top level (if all date fields are present)
 | 
			
		||||
@ -349,90 +419,82 @@ class VariablesXML:
 | 
			
		||||
        return cands
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    # ------------------------------------------------------------------
 | 
			
		||||
    # Построение иерархического дерева из flattened()
 | 
			
		||||
    # ------------------------------------------------------------------
 | 
			
		||||
    def get_all_vars_data(self) -> List[Dict[str, Any]]:
 | 
			
		||||
        """
 | 
			
		||||
        Возвращает вложенную структуру словарей с полными данными для всех переменных и их развернутых членов.
 | 
			
		||||
        Каждый словарь представляет узел в иерархии и содержит:
 | 
			
		||||
        'name' (полный путь), 'address', 'size', 'type', 'kind', 'count', и 'children' (если есть).
 | 
			
		||||
        Логика определения родительского пути теперь использует `split_path` для анализа структуры пути.
 | 
			
		||||
        Строит иерархию словарей из плоского списка переменных.
 | 
			
		||||
 | 
			
		||||
        Каждый узел = {
 | 
			
		||||
            'name': <полный путь>,
 | 
			
		||||
            'address': <адрес или None>,
 | 
			
		||||
            'type': <тип>,
 | 
			
		||||
            'size': <байты>,
 | 
			
		||||
            'kind': <'array' | ...>,
 | 
			
		||||
            'dims': [size1, size2, ...] или None,
 | 
			
		||||
            'children': [...список дочерних узлов]
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Возвращает список корневых узлов (top-level переменных).
 | 
			
		||||
        """
 | 
			
		||||
        flat_data = self.flattened(max_array_elems=None)
 | 
			
		||||
 | 
			
		||||
        root_nodes: List[Dict[str, Any]] = []
 | 
			
		||||
        all_nodes_map: Dict[str, Dict[str, Any]] = {}
 | 
			
		||||
 | 
			
		||||
        # Быстрое отображение имя -> узел (словарь с детьми)
 | 
			
		||||
        all_nodes: Dict[str, Dict[str, Any]] = {}
 | 
			
		||||
        for item in flat_data:
 | 
			
		||||
            node_dict = {**item, 'children': []}
 | 
			
		||||
            all_nodes_map[item['name']] = node_dict
 | 
			
		||||
            node = dict(item)
 | 
			
		||||
            node['children'] = []
 | 
			
		||||
            all_nodes[item['name']] = node
 | 
			
		||||
 | 
			
		||||
        # Вспомогательная функция для определения полного пути родителя с использованием 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('['):
 | 
			
		||||
        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
 | 
			
		||||
                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]
 | 
			
		||||
            # '->' занимает 2 символа, нужно взять срез до начала '->'
 | 
			
		||||
            if arrow_idx > dot_idx:
 | 
			
		||||
                return path[:arrow_idx]
 | 
			
		||||
            else:
 | 
			
		||||
                return None # Корневой элемент без явного родителя
 | 
			
		||||
                return path[:dot_idx]
 | 
			
		||||
 | 
			
		||||
        # Основная логика get_all_vars_data
 | 
			
		||||
        def find_parent(path: str) -> Optional[str]:
 | 
			
		||||
            """
 | 
			
		||||
            Возвращает полный путь родителя, учитывая '.', '->' и индексы [] в конце.
 | 
			
		||||
 | 
			
		||||
        # Заполнение связей "родитель-потомок"
 | 
			
		||||
        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)
 | 
			
		||||
            Если путь заканчивается индексом [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:
 | 
			
		||||
                root_nodes.append(node_dict)
 | 
			
		||||
                # Если нет индекса, просто ищем последний разделитель
 | 
			
		||||
                return _parent_struct_split(path)
 | 
			
		||||
 | 
			
		||||
        # Сортируем корневые узлы и их детей рекурсивно по имени
 | 
			
		||||
        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'])
 | 
			
		||||
        # Строим иерархию: 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)
 | 
			
		||||
 | 
			
		||||
        sort_nodes(root_nodes)
 | 
			
		||||
 | 
			
		||||
        return root_nodes
 | 
			
		||||
        # Рекурсивно сортируем детей по имени для порядка
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
@ -12,10 +12,12 @@ from PyInstaller.utils.hooks import collect_data_files
 | 
			
		||||
 | 
			
		||||
# === Конфигурация ===
 | 
			
		||||
USE_NUITKA = True  # True — сборка через Nuitka, False — через PyInstaller
 | 
			
		||||
MAIN_SCRIPT_NAME = "tms_debugvar_term"
 | 
			
		||||
OUTPUT_NAME = "DebugVarTerminal"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SRC_PATH = Path("./Src/")
 | 
			
		||||
SCRIPT_PATH = SRC_PATH / "DebugVarEdit_GUI.py"
 | 
			
		||||
OUTPUT_NAME = "DebugVarEdit"
 | 
			
		||||
SCRIPT_PATH = SRC_PATH / (MAIN_SCRIPT_NAME + ".py")
 | 
			
		||||
 | 
			
		||||
DIST_PATH = Path("./").resolve()
 | 
			
		||||
WORK_PATH = Path("./build_temp").resolve()
 | 
			
		||||
@ -26,9 +28,9 @@ ICON_ICO_PATH = SRC_PATH / "icon.ico"
 | 
			
		||||
TEMP_FOLDERS = [
 | 
			
		||||
    "build_temp",
 | 
			
		||||
    "__pycache__",
 | 
			
		||||
    "DebugVarEdit_GUI.build",
 | 
			
		||||
    "DebugVarEdit_GUI.onefile-build",
 | 
			
		||||
    "DebugVarEdit_GUI.dist"
 | 
			
		||||
    MAIN_SCRIPT_NAME + ".build",
 | 
			
		||||
    MAIN_SCRIPT_NAME + ".onefile-build",
 | 
			
		||||
    MAIN_SCRIPT_NAME + ".dist"
 | 
			
		||||
]
 | 
			
		||||
# === Пути к DLL и прочим зависимостям ===
 | 
			
		||||
LIBS = {
 | 
			
		||||
 | 
			
		||||
@ -221,3 +221,4 @@ class CsvLogger:
 | 
			
		||||
            return (0, float(ts))
 | 
			
		||||
        except Exception:
 | 
			
		||||
            return (1, str(ts))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -59,6 +59,47 @@ def split_path_tokens(path: str) -> List[str]:
 | 
			
		||||
        tokens.append(token)
 | 
			
		||||
    return tokens
 | 
			
		||||
 | 
			
		||||
def split_path_tokens_with_spans(path: str) -> List[Tuple[str, int, int]]:
 | 
			
		||||
    """
 | 
			
		||||
    Возвращает список кортежей (токен, start_pos, end_pos)
 | 
			
		||||
    Токены — так же, как в split_path_tokens, но с позициями в исходной строке.
 | 
			
		||||
    """
 | 
			
		||||
    tokens = []
 | 
			
		||||
    i = 0
 | 
			
		||||
    L = len(path)
 | 
			
		||||
    while i < L:
 | 
			
		||||
        c = path[i]
 | 
			
		||||
        start = i
 | 
			
		||||
        # '->'
 | 
			
		||||
        if c == '-' and i + 1 < L and path[i:i+2] == '->':
 | 
			
		||||
            tokens.append(('->', start, start + 2))
 | 
			
		||||
            i += 2
 | 
			
		||||
            continue
 | 
			
		||||
        if c == '.':
 | 
			
		||||
            tokens.append(('.', start, start + 1))
 | 
			
		||||
            i += 1
 | 
			
		||||
            continue
 | 
			
		||||
        if c == '[':
 | 
			
		||||
            # захватим весь индекс с ']'
 | 
			
		||||
            j = i
 | 
			
		||||
            while j < L and path[j] != ']':
 | 
			
		||||
                j += 1
 | 
			
		||||
            if j < L and path[j] == ']':
 | 
			
		||||
                j += 1
 | 
			
		||||
            tokens.append((path[i:j], i, j))
 | 
			
		||||
            i = j
 | 
			
		||||
            continue
 | 
			
		||||
        # иначе - обычное имя (до точки, стрелки или скобок)
 | 
			
		||||
        j = i
 | 
			
		||||
        while j < L and path[j] not in ['.', '-', '[']:
 | 
			
		||||
            if path[j] == '-' and j + 1 < L and path[j:j+2] == '->':
 | 
			
		||||
                break
 | 
			
		||||
            j += 1
 | 
			
		||||
        tokens.append((path[i:j], i, j))
 | 
			
		||||
        i = j
 | 
			
		||||
    # фильтруем из списка токены-разделители '.' и '->' чтобы оставить только логические части
 | 
			
		||||
    filtered = [t for t in tokens if t[0] not in ['.', '->']]
 | 
			
		||||
    return filtered
 | 
			
		||||
 | 
			
		||||
def canonical_key(path: str) -> str:
 | 
			
		||||
    """
 | 
			
		||||
@ -137,28 +178,19 @@ class PathHints:
 | 
			
		||||
        self._paths.append(full_path)
 | 
			
		||||
        self._types[full_path] = type_str
 | 
			
		||||
 | 
			
		||||
        toks = split_path_tokens(full_path)
 | 
			
		||||
        if not toks:
 | 
			
		||||
        tokens_spans = split_path_tokens_with_spans(full_path)
 | 
			
		||||
        if not tokens_spans:
 | 
			
		||||
            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
 | 
			
		||||
        for i, (tok, start, end) in enumerate(tokens_spans):
 | 
			
		||||
            cur_full = full_path[:end]  # подстрока с начала до конца токена включительно
 | 
			
		||||
            
 | 
			
		||||
            # Если узел уже есть
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -339,7 +339,7 @@ class LowLevelSelectorWidget(QWidget):
 | 
			
		||||
            name = var['ptr_type']
 | 
			
		||||
        elif isinstance(var.get('ptr_type_name'), str):
 | 
			
		||||
            name = var['ptr_type_name']
 | 
			
		||||
        elif isinstance(var.get('pt_type'), str):
 | 
			
		||||
        elif isinstance(var.get('pt_type'), str) and 'pt_' in var.get('pt_type'):
 | 
			
		||||
            name = var['pt_type'].replace('pt_','')
 | 
			
		||||
        elif isinstance(var.get('ptr_type'), int):
 | 
			
		||||
            name = PT_ENUM_NAME_FROM_VAL.get(var['ptr_type'], 'unknown')
 | 
			
		||||
@ -456,7 +456,7 @@ class LowLevelSelectorWidget(QWidget):
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        # 2. Обновляем отображение в таблице
 | 
			
		||||
        self.var_table.populate(self._all_available_vars, {}, self._on_var_table_changed)
 | 
			
		||||
        #self.var_table.populate(self._all_available_vars, {}, self._on_var_table_changed)
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    # --------------- Address mapping / type mapping helpers ---------------
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,8 @@
 | 
			
		||||
from PySide2 import QtCore, QtWidgets, QtSerialPort
 | 
			
		||||
from PySide2 import QtCore, QtWidgets, QtSerialPort, QtGui
 | 
			
		||||
from tms_debugvar_lowlevel import LowLevelSelectorWidget
 | 
			
		||||
import datetime
 | 
			
		||||
import time
 | 
			
		||||
import os
 | 
			
		||||
from csv_logger import CsvLogger
 | 
			
		||||
# ------------------------------- Константы протокола ------------------------
 | 
			
		||||
WATCH_SERVICE_BIT = 0x8000
 | 
			
		||||
@ -44,6 +45,19 @@ def crc16_ibm(data: bytes, *, init=0xFFFF) -> int:
 | 
			
		||||
                crc >>= 1
 | 
			
		||||
    return crc & 0xFFFF
 | 
			
		||||
 | 
			
		||||
def is_frozen():
 | 
			
		||||
    # Для Nuitka --onefile
 | 
			
		||||
    return getattr(sys, 'frozen', False)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_base_path():
 | 
			
		||||
    if is_frozen():
 | 
			
		||||
        # В Nuitka onefile распаковывается в папку с самим exe во временной директории
 | 
			
		||||
        return os.path.dirname(sys.executable)
 | 
			
		||||
    else:
 | 
			
		||||
        # Режим разработки
 | 
			
		||||
        return os.path.dirname(os.path.abspath(__file__))
 | 
			
		||||
    
 | 
			
		||||
def _decode_debug_status(status: int) -> str:
 | 
			
		||||
        """Преобразует код статуса прошивки в строку.
 | 
			
		||||
        Возвращает 'OK' или перечисление битов через '|'.
 | 
			
		||||
@ -97,9 +111,6 @@ class Spoiler(QtWidgets.QWidget):
 | 
			
		||||
        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)
 | 
			
		||||
@ -120,13 +131,6 @@ class Spoiler(QtWidgets.QWidget):
 | 
			
		||||
    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)
 | 
			
		||||
@ -135,12 +139,6 @@ class Spoiler(QtWidgets.QWidget):
 | 
			
		||||
        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()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -420,9 +418,8 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
        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_read_once.clicked.connect(self._start_ll_cycle)
 | 
			
		||||
        self.ll_selector.btn_start_polling.clicked.connect(self._toggle_ll_polling)
 | 
			
		||||
 | 
			
		||||
        # --- CSV Logging ---
 | 
			
		||||
@ -681,7 +678,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
            # !!! Раньше тут было `return`, его убираем
 | 
			
		||||
 | 
			
		||||
        # Если идёт LL polling — переходим сразу к следующей переменной
 | 
			
		||||
        if self._ll_polling and (self._ll_poll_index < len(self._ll_polling_variables)):
 | 
			
		||||
        if self._ll_polling:
 | 
			
		||||
            self._process_next_ll_variable_in_cycle()
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
@ -904,6 +901,13 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
 | 
			
		||||
        status_desc = _decode_debug_status(status)
 | 
			
		||||
 | 
			
		||||
        if not success:
 | 
			
		||||
            # Ошибка — в ответе нет ReturnType и данных, только статус
 | 
			
		||||
            self._log(f"[LL] ERROR addr=0x{addr24:06X} status=0x{status:02X} ({status_desc})")
 | 
			
		||||
            self.llValueRead.emit(addr24, status, None, None, None)
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        # Если success == True, продолжаем парсить ReturnType и данные
 | 
			
		||||
        return_type = payload[6]
 | 
			
		||||
        data_hi, data_lo = payload[7], payload[8]
 | 
			
		||||
        raw16 = (data_hi << 8) | data_lo
 | 
			
		||||
@ -923,7 +927,6 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
 | 
			
		||||
        scaled = float(value_int) / scale
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        self.llValueRead.emit(addr24, status, return_type, value_int, scaled)
 | 
			
		||||
 | 
			
		||||
        var_name = None
 | 
			
		||||
@ -935,7 +938,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
 | 
			
		||||
        self._log(f"[LL] OK addr=0x{addr24:06X} type=0x{return_type:02X} raw={value_int} scaled={scaled:.6g}")
 | 
			
		||||
 | 
			
		||||
        current_time = time.time() # Получаем текущее время
 | 
			
		||||
        current_time = time.time()
 | 
			
		||||
        self.csv_logger.set_value(current_time, var_name, display_val)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1030,6 +1033,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
            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):        
 | 
			
		||||
@ -1063,6 +1067,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
            
 | 
			
		||||
            # Immediately kick off the first variable read of the first cycle
 | 
			
		||||
            self._start_ll_cycle()
 | 
			
		||||
        self._set_ui_busy(False) # Обновить доступность кнопок
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def _on_ll_poll_timeout(self):
 | 
			
		||||
@ -1097,13 +1102,18 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
            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")
 | 
			
		||||
            # Получаем адрес переменной, предполагаем что ключ называется 'addr' или 'address'
 | 
			
		||||
            addr = var_info.get('addr') or var_info.get('address')
 | 
			
		||||
            if addr is not None:
 | 
			
		||||
                addr_str = f"0x{addr:06X}"
 | 
			
		||||
            else:
 | 
			
		||||
                addr_str = "addr unknown"
 | 
			
		||||
 | 
			
		||||
            self.set_status(f"Polling LL: {addr_str} {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")
 | 
			
		||||
            self.ll_selector._populate_var_table()
 | 
			
		||||
    # ------------------------------ HELPERS --------------------------------
 | 
			
		||||
    def _toggle_index_base(self, st):
 | 
			
		||||
        # ... (код без изменений)
 | 
			
		||||
@ -1180,7 +1190,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
        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.set_status("CSV Logging ACTIVATED", "service")
 | 
			
		||||
        self._log("[CSV] Запись данных в CSV началась.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1189,7 +1199,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
        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.set_status("CSV Logging STOPPED", "service")
 | 
			
		||||
        self._log("[CSV] Запись данных в CSV остановлена.")
 | 
			
		||||
 | 
			
		||||
    def _save_csv_data(self):
 | 
			
		||||
@ -1199,7 +1209,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
            self.set_status("Stop logging first", "error")
 | 
			
		||||
            return
 | 
			
		||||
        self.csv_logger.write_to_csv()
 | 
			
		||||
        self.set_status("CSV data saved", "idle")
 | 
			
		||||
        self.set_status("CSV data saved", "service")
 | 
			
		||||
        
 | 
			
		||||
    def _log(self, msg: str):
 | 
			
		||||
        # ... (код без изменений)
 | 
			
		||||
@ -1235,6 +1245,10 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
class _DemoWindow(QtWidgets.QMainWindow):
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        base_path = get_base_path()
 | 
			
		||||
        icon_path = os.path.join(base_path, "icon.ico")
 | 
			
		||||
        if os.path.exists(icon_path):
 | 
			
		||||
            self.setWindowIcon(QtGui.QIcon(icon_path))        
 | 
			
		||||
        self.setWindowTitle("DebugVar Terminal")
 | 
			
		||||
        self.term = DebugTerminalWidget(self)
 | 
			
		||||
        self.setCentralWidget(self.term)
 | 
			
		||||
 | 
			
		||||
@ -206,7 +206,7 @@ class VariableSelectorDialog(QDialog):
 | 
			
		||||
                    'enable': 'true', 
 | 
			
		||||
                    'shortname': name, 
 | 
			
		||||
                    'pt_type': '', 
 | 
			
		||||
                    'iq_type': '',
 | 
			
		||||
                    'iq_type': 't_iq_none',
 | 
			
		||||
                    'return_type': 't_iq_none', 
 | 
			
		||||
                    'file': file_val,
 | 
			
		||||
                    'extern': str(extern_val).lower() if extern_val else 'false',
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,8 @@ class SetSizeDialog(QDialog):
 | 
			
		||||
    """
 | 
			
		||||
    Диалоговое окно для выбора числового значения (размера).
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, parent=None, initial_value=10, min_value=1, max_value=50, title="Укажите размер короткого имени"):
 | 
			
		||||
    def __init__(self, parent=None, initial_value=10, min_value=1, max_value=50, title="Укажите размер короткого имени",
 | 
			
		||||
                 label_text="Количество символов:"):
 | 
			
		||||
        super().__init__(parent)
 | 
			
		||||
        self.setWindowTitle(title)
 | 
			
		||||
        self.setFixedSize(320, 120)  # Задаем фиксированный размер для аккуратного вида
 | 
			
		||||
@ -74,7 +75,7 @@ class SetSizeDialog(QDialog):
 | 
			
		||||
 | 
			
		||||
        # Макет для ввода значения
 | 
			
		||||
        input_layout = QHBoxLayout()
 | 
			
		||||
        label = QLabel("Количество символов:", self)
 | 
			
		||||
        label = QLabel(label_text, self)
 | 
			
		||||
        
 | 
			
		||||
        self.spin_box = QSpinBox(self)
 | 
			
		||||
        self.spin_box.setRange(min_value, max_value)  # Устанавливаем диапазон допустимых значений
 | 
			
		||||
@ -155,6 +156,9 @@ class VariableTableWidget(QTableWidget):
 | 
			
		||||
        shortsize = self.settings.value("shortname_size", True, type=int)
 | 
			
		||||
        self._shortname_size = shortsize
 | 
			
		||||
 | 
			
		||||
        if(self._show_value):
 | 
			
		||||
            self._shortname_size = 3
 | 
			
		||||
 | 
			
		||||
        self.type_options = list(dict.fromkeys(type_map.values()))
 | 
			
		||||
        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)]
 | 
			
		||||
@ -262,11 +266,25 @@ class VariableTableWidget(QTableWidget):
 | 
			
		||||
            self.setCellWidget(row, rows.ret_type, ret_combo)
 | 
			
		||||
 | 
			
		||||
            # Последний столбец
 | 
			
		||||
            if self._show_value:
 | 
			
		||||
                if self._show_value:
 | 
			
		||||
                    val = var.get('value', '')
 | 
			
		||||
                    if val is None:
 | 
			
		||||
                        val = ''
 | 
			
		||||
                val_edit = QLineEdit(str(val))
 | 
			
		||||
                    else:
 | 
			
		||||
                        try:
 | 
			
		||||
                            f_val = float(val)
 | 
			
		||||
                            # Форматируем число с учетом self._shortname_size
 | 
			
		||||
                            if f_val.is_integer():
 | 
			
		||||
                                val = str(int(f_val))
 | 
			
		||||
                            else:
 | 
			
		||||
                                precision = getattr(self, "_shortname_size", 3)  # по умолчанию 3
 | 
			
		||||
                                val = f"{f_val:.{precision}f}"
 | 
			
		||||
                        except ValueError:
 | 
			
		||||
                            # Если значение не число (строка и т.п.), оставляем как есть
 | 
			
		||||
                            val = str(val)
 | 
			
		||||
 | 
			
		||||
                    val_edit = QLineEdit(val)
 | 
			
		||||
                    val_edit.textChanged.connect(on_change_callback)
 | 
			
		||||
                    val_edit.setStyleSheet(style_with_padding)
 | 
			
		||||
                    self.setCellWidget(row, rows.short_name, val_edit)
 | 
			
		||||
@ -310,6 +328,7 @@ class VariableTableWidget(QTableWidget):
 | 
			
		||||
                color = error_color
 | 
			
		||||
                tooltip = tooltip_missing
 | 
			
		||||
            elif long_shortname:                
 | 
			
		||||
                if not self._show_value:
 | 
			
		||||
                    color = warning_color
 | 
			
		||||
                    tooltip = tooltip_shortname
 | 
			
		||||
 | 
			
		||||
@ -365,9 +384,14 @@ class VariableTableWidget(QTableWidget):
 | 
			
		||||
                self.update_comboboxes({rows.ret_type: self._ret_type_filter})
 | 
			
		||||
                
 | 
			
		||||
        elif logicalIndex == rows.short_name:
 | 
			
		||||
            if self._show_value:
 | 
			
		||||
                dlg = SetSizeDialog(self, title="Укажите точность", label_text="Кол-во знаков после запятой", initial_value=3)
 | 
			
		||||
            else:
 | 
			
		||||
                dlg = SetSizeDialog(self)
 | 
			
		||||
 | 
			
		||||
            if dlg.exec_():
 | 
			
		||||
                self._shortname_size = dlg.get_selected_size()   
 | 
			
		||||
                if not self._show_value:
 | 
			
		||||
                    self.settings.setValue("shortname_size", self._shortname_size)             
 | 
			
		||||
                self.check()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,14 @@
 | 
			
		||||
# pyinstaller --onefile --distpath ./parse_xml --workpath ./build --specpath ./build parse_xml/Src/parse_xml.py
 | 
			
		||||
# python -m nuitka --standalone --onefile --output-dir=./build parse_xml/Src/parse_xml.py
 | 
			
		||||
# pyinstaller --onefile --distpath ./parse_xml --workpath ./parse_xml/build --specpath ./build parse_xml/Src/parse_xml.py
 | 
			
		||||
# python -m nuitka --standalone --onefile --output-dir=./parse_xml parse_xml/Src/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]")
 | 
			
		||||
    print("Usage: python parse_xml.exe <input.xml> <info.txt> [output.xml]")
 | 
			
		||||
    sys.exit(1)
 | 
			
		||||
 | 
			
		||||
input_path = sys.argv[1]
 | 
			
		||||
@ -159,75 +161,6 @@ def parse_offset(offset_text):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_array_dimensions(array_die):
 | 
			
		||||
    """
 | 
			
		||||
    Собрать размеры всех измерений массива.
 | 
			
		||||
    Возвращает список размеров [dim0, dim1, ...] во внешне-внутреннем порядке.
 | 
			
		||||
    """
 | 
			
		||||
    dims = []
 | 
			
		||||
 | 
			
		||||
    # Ищем DW_TAG_subrange_type — для каждого измерения
 | 
			
		||||
    for child in array_die.findall("die"):
 | 
			
		||||
        if child.findtext("tag") != "DW_TAG_subrange_type":
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        dim_size = None
 | 
			
		||||
 | 
			
		||||
        # Попытка получить upper_bound
 | 
			
		||||
        ub_attr = get_attr(child, "DW_AT_upper_bound")
 | 
			
		||||
        if ub_attr is not None:
 | 
			
		||||
            val = ub_attr.find("value/const")
 | 
			
		||||
            if val is not None:
 | 
			
		||||
                try:
 | 
			
		||||
                    # В DWARF верхняя граница включительно, значит размер = upper_bound + 1
 | 
			
		||||
                    dim_size = int(val.text, 0) + 1
 | 
			
		||||
                except Exception:
 | 
			
		||||
                    pass
 | 
			
		||||
 | 
			
		||||
        # Если не получилось — попытаться из count
 | 
			
		||||
        if dim_size is None:
 | 
			
		||||
            ct_attr = get_attr(child, "DW_AT_count")
 | 
			
		||||
            if ct_attr is not None:
 | 
			
		||||
                val = ct_attr.find("value/const")
 | 
			
		||||
                if val is not None:
 | 
			
		||||
                    try:
 | 
			
		||||
                        dim_size = int(val.text, 0)
 | 
			
		||||
                    except Exception:
 | 
			
		||||
                        pass
 | 
			
		||||
 | 
			
		||||
        # Если ничего не найдено — ставим 0
 | 
			
		||||
        if dim_size is None:
 | 
			
		||||
            dim_size = 0
 | 
			
		||||
 | 
			
		||||
        dims.append(dim_size)
 | 
			
		||||
 | 
			
		||||
    # Если subrange не нашли (например, в случае typedef), попробуем через размер типа
 | 
			
		||||
    if not dims:
 | 
			
		||||
        arr_size = get_die_size(array_die)
 | 
			
		||||
        elem_size = None
 | 
			
		||||
        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")
 | 
			
		||||
            elem_die = resolve_type_die(element_type_id)
 | 
			
		||||
            if elem_die is not None:
 | 
			
		||||
                elem_size = get_die_size(elem_die)
 | 
			
		||||
                
 | 
			
		||||
        if arr_size is not None and elem_size:
 | 
			
		||||
            dims.append(arr_size // elem_size)
 | 
			
		||||
        else:
 | 
			
		||||
            dims.append(0)
 | 
			
		||||
 | 
			
		||||
    # Рекурсия — если элементный тип массива тоже массив, добавляем размеры вложенного
 | 
			
		||||
    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 get_base_type_die(array_die):
 | 
			
		||||
    """Спускаемся по цепочке DW_AT_type, пока не дойдем до не-массива (базового типа)."""
 | 
			
		||||
    current_die = array_die
 | 
			
		||||
@ -244,11 +177,98 @@ def get_base_type_die(array_die):
 | 
			
		||||
            return next_die
 | 
			
		||||
    return current_die
 | 
			
		||||
 | 
			
		||||
def get_array_dimensions(array_die):
 | 
			
		||||
    dims = []
 | 
			
		||||
    # Итерируем по всем DIE с тегом DW_TAG_subrange_type, потомки текущего массива
 | 
			
		||||
    for child in array_die.findall("die"):
 | 
			
		||||
        if child.findtext("tag") != "DW_TAG_subrange_type":
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        dim_size = None
 | 
			
		||||
        ub_attr = get_attr(child, "DW_AT_upper_bound")
 | 
			
		||||
        if ub_attr is not None:
 | 
			
		||||
            # Попробуем разные варианты получить значение upper_bound
 | 
			
		||||
            # 1) value/const
 | 
			
		||||
            val_const = ub_attr.find("const")
 | 
			
		||||
            if val_const is not None:
 | 
			
		||||
                try:
 | 
			
		||||
                    dim_size = int(val_const.text, 0) + 1
 | 
			
		||||
                    #print(f"[DEBUG] Found DW_AT_upper_bound const: {val_const.text}, size={dim_size}")
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    a=1#print(f"[WARN] Error parsing upper_bound const: {e}")
 | 
			
		||||
            else:
 | 
			
		||||
                # 2) value/block (DW_OP_constu / DW_OP_plus_uconst, etc.)
 | 
			
		||||
                val_block = ub_attr.find("block")
 | 
			
		||||
                if val_block is not None:
 | 
			
		||||
                    block_text = val_block.text
 | 
			
		||||
                    # Можно попытаться парсить DWARF expr (например DW_OP_plus_uconst 7)
 | 
			
		||||
                    if block_text and "DW_OP_plus_uconst" in block_text:
 | 
			
		||||
                        try:
 | 
			
		||||
                            parts = block_text.split()
 | 
			
		||||
                            val = int(parts[-1], 0)
 | 
			
		||||
                            dim_size = val + 1
 | 
			
		||||
                            #print(f"[DEBUG] Parsed upper_bound block: {val} + 1 = {dim_size}")
 | 
			
		||||
                        except Exception as e:
 | 
			
		||||
                            a=1#print(f"[WARN] Error parsing upper_bound block: {e}")
 | 
			
		||||
                    else:
 | 
			
		||||
                        a=1#print(f"[WARN] Unexpected DW_AT_upper_bound block content: {block_text}")
 | 
			
		||||
                else:
 | 
			
		||||
                    a=1#print(f"[WARN] DW_AT_upper_bound has no const or block value")
 | 
			
		||||
        
 | 
			
		||||
        if dim_size is None:
 | 
			
		||||
            # fallback по DW_AT_count — редко встречается
 | 
			
		||||
            ct_attr = get_attr(child, "DW_AT_count")
 | 
			
		||||
            if ct_attr is not None:
 | 
			
		||||
                val_const = ct_attr.find("value/const")
 | 
			
		||||
                if val_const is not None:
 | 
			
		||||
                    try:
 | 
			
		||||
                        dim_size = int(val_const.text, 0)
 | 
			
		||||
                        #print(f"[DEBUG] Found DW_AT_count: {dim_size}")
 | 
			
		||||
                    except Exception as e:
 | 
			
		||||
                        a=1#print(f"[WARN] Error parsing DW_AT_count const: {e}")
 | 
			
		||||
 | 
			
		||||
        if dim_size is None:
 | 
			
		||||
            print("[DEBUG] No dimension size found for this subrange, defaulting to 0")
 | 
			
		||||
            dim_size = 0
 | 
			
		||||
 | 
			
		||||
        dims.append(dim_size)
 | 
			
		||||
 | 
			
		||||
    # Если не нашли измерений — пытаемся вычислить размер массива по общему размеру
 | 
			
		||||
    if not dims:
 | 
			
		||||
        arr_size = get_die_size(array_die)
 | 
			
		||||
        elem_size = None
 | 
			
		||||
        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")
 | 
			
		||||
            elem_die = resolve_type_die(element_type_id)
 | 
			
		||||
            if elem_die is not None:
 | 
			
		||||
                elem_size = get_die_size(elem_die)
 | 
			
		||||
                #print(f"[DEBUG] Fallback: arr_size={arr_size}, elem_size={elem_size}")
 | 
			
		||||
 | 
			
		||||
        if arr_size is not None and elem_size:
 | 
			
		||||
            dim_calc = arr_size // elem_size
 | 
			
		||||
            dims.append(dim_calc)
 | 
			
		||||
            #print(f"[DEBUG] Calculated dimension size from total size: {dim_calc}")
 | 
			
		||||
        else:
 | 
			
		||||
            dims.append(0)
 | 
			
		||||
            print("[DEBUG] Could not calculate dimension size, set 0")
 | 
			
		||||
 | 
			
		||||
    # Рекурсивно обрабатываем вложенные массивы
 | 
			
		||||
    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))
 | 
			
		||||
 | 
			
		||||
    #print(f"[DEBUG] Array dimensions: {dims}")
 | 
			
		||||
    return dims
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def handle_array_type(member_elem, resolved_type, offset=0):
 | 
			
		||||
    dims = get_array_dimensions(resolved_type)
 | 
			
		||||
 | 
			
		||||
    # Определяем базовый тип (не массив)
 | 
			
		||||
    base_die = get_base_type_die(resolved_type)
 | 
			
		||||
    base_name = "unknown"
 | 
			
		||||
    base_size = None
 | 
			
		||||
@ -259,10 +279,10 @@ def handle_array_type(member_elem, resolved_type, offset=0):
 | 
			
		||||
            base_size = get_die_size(base_die)
 | 
			
		||||
        else:
 | 
			
		||||
            base_name = get_type_name(base_die.attrib.get("id", ""))
 | 
			
		||||
    #print(f"[DEBUG] Base type name: {base_name}, base size: {base_size}")
 | 
			
		||||
 | 
			
		||||
    member_elem.set("type", base_name + "[]" * len(dims))
 | 
			
		||||
 | 
			
		||||
    # Вычисляем общий размер массива — произведение размеров * размер базового элемента
 | 
			
		||||
    if base_size is None:
 | 
			
		||||
        base_size = 0
 | 
			
		||||
 | 
			
		||||
@ -270,31 +290,34 @@ def handle_array_type(member_elem, resolved_type, offset=0):
 | 
			
		||||
    for d in dims:
 | 
			
		||||
        if d == 0:
 | 
			
		||||
            total_elements = 0
 | 
			
		||||
            print(f"[WARN] Dimension size is zero, setting total elements to 0")
 | 
			
		||||
            break
 | 
			
		||||
        total_elements *= d
 | 
			
		||||
 | 
			
		||||
    total_size = total_elements * base_size if base_size is not None else 0
 | 
			
		||||
    if total_size:
 | 
			
		||||
        member_elem.set("size", str(total_size))
 | 
			
		||||
        member_elem.set("size", str(base_size if base_size is not None else 1))
 | 
			
		||||
    else:
 | 
			
		||||
        # fallback: если не удалось, можно попробовать get_die_size
 | 
			
		||||
        arr_size = get_die_size(resolved_type)
 | 
			
		||||
        if arr_size:
 | 
			
		||||
            member_elem.set("size", str(arr_size))
 | 
			
		||||
            #print(f"[DEBUG] Used fallback size from resolved_type: {arr_size}")
 | 
			
		||||
        else:
 | 
			
		||||
            print(f"[WARN] Could not determine total size for array")
 | 
			
		||||
 | 
			
		||||
    # Записываем размеры измерений size1, size2 ...
 | 
			
		||||
    for i, dim in enumerate(dims, 1):
 | 
			
		||||
        member_elem.set(f"size{i}", str(dim))
 | 
			
		||||
        #print(f"[DEBUG] Setting size{i} = {dim}")
 | 
			
		||||
 | 
			
		||||
    member_elem.set("kind", "array")
 | 
			
		||||
 | 
			
		||||
    # Если элемент базового типа — структура, разворачиваем её поля
 | 
			
		||||
    if base_die is not None and base_die.findtext("tag") == "DW_TAG_structure_type":
 | 
			
		||||
        add_members_recursive(member_elem, base_die, offset)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add_members_recursive(parent_elem, struct_die, base_offset=0):
 | 
			
		||||
    is_union = struct_die.findtext("tag") == "DW_TAG_union_type"
 | 
			
		||||
    size = get_die_size(struct_die)
 | 
			
		||||
@ -330,6 +353,20 @@ def add_members_recursive(parent_elem, struct_die, base_offset=0):
 | 
			
		||||
            elif tag in ("DW_TAG_structure_type", "DW_TAG_union_type"):
 | 
			
		||||
                member_elem.set("type", type_name)
 | 
			
		||||
                add_members_recursive(member_elem, resolved_type, offset)
 | 
			
		||||
            elif tag == "DW_TAG_pointer_type":
 | 
			
		||||
                # Проверяем тип, на который указывает указатель
 | 
			
		||||
                pointee_ref = get_attr(resolved_type, "DW_AT_type")
 | 
			
		||||
                if pointee_ref is not None and pointee_ref.find("ref") is not None:
 | 
			
		||||
                    pointee_id = pointee_ref.find("ref").attrib.get("idref")
 | 
			
		||||
                    pointee_die = resolve_type_die(pointee_id)
 | 
			
		||||
                    if pointee_die is not None:
 | 
			
		||||
                        pointee_tag = pointee_die.findtext("tag")
 | 
			
		||||
                        if pointee_tag in ("DW_TAG_structure_type", "DW_TAG_union_type"):
 | 
			
		||||
                            # Добавляем подэлементы для структуры, на которую указывает указатель
 | 
			
		||||
                            pointer_elem = ET.SubElement(member_elem, "pointee", type=get_type_name(pointee_id))
 | 
			
		||||
                            add_members_recursive(pointer_elem, pointee_die, 0)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
output_root = ET.Element("variables")
 | 
			
		||||
for die in root.iter("die"):
 | 
			
		||||
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
		Loading…
	
		Reference in New Issue
	
	Block a user