кууууча всего по терминалке, надо резгребать и структурировать
базово: +сделан lowlevel для кучи переменных (пока работает медленно) +сделан сохранение принимаемых значений в лог + gui терминалок подогнаны под один стиль плюс минус
This commit is contained in:
		
							parent
							
								
									96496a0256
								
							
						
					
					
						commit
						788ad19464
					
				
							
								
								
									
										
											BIN
										
									
								
								DebugTools.rar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								DebugTools.rar
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										438
									
								
								Src/allvars_xml_parser.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										438
									
								
								Src/allvars_xml_parser.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,438 @@
 | 
			
		||||
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
import sys
 | 
			
		||||
import re
 | 
			
		||||
import xml.etree.ElementTree as ET
 | 
			
		||||
import var_setup
 | 
			
		||||
from dataclasses import dataclass, field
 | 
			
		||||
from typing import List, Dict, Optional, Tuple
 | 
			
		||||
from PySide2.QtWidgets import (
 | 
			
		||||
    QDialog, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QPushButton,
 | 
			
		||||
    QLineEdit, QLabel, QHeaderView, QCompleter, QCheckBox, QHBoxLayout, QSizePolicy,
 | 
			
		||||
    QTableWidget, QTableWidgetItem, QFileDialog, QWidget, QMessageBox, QApplication, QMainWindow
 | 
			
		||||
)
 | 
			
		||||
from PySide2 import QtCore, QtGui
 | 
			
		||||
from path_hints import PathHints
 | 
			
		||||
from generate_debug_vars import choose_type_map, type_map
 | 
			
		||||
from var_selector_window import VariableSelectorDialog
 | 
			
		||||
from typing import List, Tuple, Optional, Dict, Any, Set
 | 
			
		||||
 | 
			
		||||
DATE_FIELD_SET = {'year','month','day','hour','minute'}
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class MemberNode:
 | 
			
		||||
    name: str
 | 
			
		||||
    offset: int = 0
 | 
			
		||||
    type_str: str = ''
 | 
			
		||||
    size: Optional[int] = None
 | 
			
		||||
    children: List['MemberNode'] = field(default_factory=list)
 | 
			
		||||
    # --- новые, но необязательные (совместимость) ---
 | 
			
		||||
    kind: Optional[str] = None          # 'array', 'union', ...
 | 
			
		||||
    count: Optional[int] = None         # size1 (число элементов в массиве)
 | 
			
		||||
 | 
			
		||||
    def is_date_struct(self) -> bool:
 | 
			
		||||
        if not self.children:
 | 
			
		||||
            return False
 | 
			
		||||
        child_names = {c.name for c in self.children}
 | 
			
		||||
        return DATE_FIELD_SET.issubset(child_names)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class VariableNode:
 | 
			
		||||
    name: str
 | 
			
		||||
    address: int
 | 
			
		||||
    type_str: str
 | 
			
		||||
    size: Optional[int]
 | 
			
		||||
    members: List[MemberNode] = field(default_factory=list)
 | 
			
		||||
    # --- новые, но необязательные ---
 | 
			
		||||
    kind: Optional[str] = None          # 'array'
 | 
			
		||||
    count: Optional[int] = None         # size1
 | 
			
		||||
 | 
			
		||||
    def base_address_hex(self) -> str:
 | 
			
		||||
        return f"0x{self.address:06X}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# --------------------------- XML Parser ----------------------------
 | 
			
		||||
 | 
			
		||||
class VariablesXML:
 | 
			
		||||
    """
 | 
			
		||||
    Reads your XML and outputs a flat list of paths:
 | 
			
		||||
      - Arrays -> name[i], multilevel -> name[i][j]
 | 
			
		||||
      - Pointer to struct -> children via '->'
 | 
			
		||||
      - Regular struct -> children via '.'
 | 
			
		||||
    """
 | 
			
		||||
    # assumed primitive sizes (for STM/MCU: int=2)
 | 
			
		||||
    _PRIM_SIZE = {
 | 
			
		||||
        'char':1, 'signed char':1, 'unsigned char':1, 'uint8_t':1, 'int8_t':1,
 | 
			
		||||
        'short':2, 'short int':2, 'signed short':2, 'unsigned short':2,
 | 
			
		||||
        'uint16_t':2, 'int16_t':2,
 | 
			
		||||
        'int':2, 'signed int':2, 'unsigned int':2,
 | 
			
		||||
        'long':4, 'unsigned long':4, 'int32_t':4, 'uint32_t':4,
 | 
			
		||||
        'float':4,
 | 
			
		||||
        'long long':8, 'unsigned long long':8, 'int64_t':8, 'uint64_t':8, 'double':8,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def __init__(self, path: str):
 | 
			
		||||
        self.path = path
 | 
			
		||||
        self.timestamp: str = ''
 | 
			
		||||
        self.variables: List[VariableNode] = []
 | 
			
		||||
        choose_type_map(0)
 | 
			
		||||
        self._parse()
 | 
			
		||||
 | 
			
		||||
    # ------------------ low helpers ------------------
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _parse_int_guess(txt: Optional[str]) -> Optional[int]:
 | 
			
		||||
        if not txt:
 | 
			
		||||
            return None
 | 
			
		||||
        txt = txt.strip()
 | 
			
		||||
        if txt.startswith(('0x','0X')):
 | 
			
		||||
            return int(txt, 16)
 | 
			
		||||
        # если в строке есть буквы A-F → возможно hex
 | 
			
		||||
        if any(c in 'abcdefABCDEF' for c in txt):
 | 
			
		||||
            try:
 | 
			
		||||
                return int(txt, 16)
 | 
			
		||||
            except ValueError:
 | 
			
		||||
                pass
 | 
			
		||||
        try:
 | 
			
		||||
            return int(txt, 10)
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _is_pointer_to_struct(t: str) -> bool:
 | 
			
		||||
        if not t:
 | 
			
		||||
            return False
 | 
			
		||||
        low = t.replace('\t',' ').replace('\n',' ')
 | 
			
		||||
        return 'struct ' in low and '*' in low
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _is_struct_or_union(t: str) -> bool:
 | 
			
		||||
        if not t:
 | 
			
		||||
            return False
 | 
			
		||||
        low = t.strip()
 | 
			
		||||
        return low.startswith('struct ') or low.startswith('union ')
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _strip_array_suffix(t: str) -> str:
 | 
			
		||||
        return t[:-2].strip() if t.endswith('[]') else t
 | 
			
		||||
 | 
			
		||||
    def _guess_primitive_size(self, type_str: str) -> Optional[int]:
 | 
			
		||||
        if not type_str:
 | 
			
		||||
            return None
 | 
			
		||||
        base = type_str
 | 
			
		||||
        for tok in ('volatile','const'):
 | 
			
		||||
            base = base.replace(tok, '')
 | 
			
		||||
        base = base.replace('*',' ')
 | 
			
		||||
        base = base.replace('[',' ').replace(']',' ')
 | 
			
		||||
        base = ' '.join(base.split()).strip()
 | 
			
		||||
        return self._PRIM_SIZE.get(base)
 | 
			
		||||
 | 
			
		||||
    # ------------------ XML read ------------------
 | 
			
		||||
 | 
			
		||||
    def _parse(self):
 | 
			
		||||
        try:
 | 
			
		||||
            tree = ET.parse(self.path)
 | 
			
		||||
            root = tree.getroot()
 | 
			
		||||
 | 
			
		||||
            ts = root.find('timestamp')
 | 
			
		||||
            self.timestamp = ts.text.strip() if ts is not None and ts.text else ''
 | 
			
		||||
 | 
			
		||||
            def parse_member(elem) -> MemberNode:
 | 
			
		||||
                name = elem.get('name','')
 | 
			
		||||
                offset = int(elem.get('offset','0'),16) if elem.get('offset') else 0
 | 
			
		||||
                t = elem.get('type','') or ''
 | 
			
		||||
                size_attr = elem.get('size')
 | 
			
		||||
                size = int(size_attr,16) if size_attr else None
 | 
			
		||||
                kind = elem.get('kind')
 | 
			
		||||
                size1_attr = elem.get('size1')
 | 
			
		||||
                count = None
 | 
			
		||||
                if size1_attr:
 | 
			
		||||
                    count = self._parse_int_guess(size1_attr)
 | 
			
		||||
                node = MemberNode(name=name, offset=offset, type_str=t, size=size,
 | 
			
		||||
                                  kind=kind, count=count)
 | 
			
		||||
                for ch in elem.findall('member'):
 | 
			
		||||
                    node.children.append(parse_member(ch))
 | 
			
		||||
                return node
 | 
			
		||||
 | 
			
		||||
            for var in root.findall('variable'):
 | 
			
		||||
                addr = int(var.get('address','0'),16)
 | 
			
		||||
                name = var.get('name','')
 | 
			
		||||
                t = var.get('type','') or ''
 | 
			
		||||
                size_attr = var.get('size')
 | 
			
		||||
                size = int(size_attr,16) if size_attr else None
 | 
			
		||||
                kind = var.get('kind')
 | 
			
		||||
                size1_attr = var.get('size1')
 | 
			
		||||
                count = None
 | 
			
		||||
                if size1_attr:
 | 
			
		||||
                    count = self._parse_int_guess(size1_attr)
 | 
			
		||||
                members = [parse_member(m) for m in var.findall('member')]
 | 
			
		||||
                self.variables.append(
 | 
			
		||||
                    VariableNode(name=name, address=addr, type_str=t, size=size,
 | 
			
		||||
                                 members=members, kind=kind, count=count)
 | 
			
		||||
                )
 | 
			
		||||
        except FileNotFoundError:
 | 
			
		||||
            self.variables = []
 | 
			
		||||
        except ET.ParseError:
 | 
			
		||||
            self.variables = []
 | 
			
		||||
 | 
			
		||||
    # ------------------ flatten (expanded) ------------------
 | 
			
		||||
 | 
			
		||||
    def flattened(self,
 | 
			
		||||
                  max_array_elems: Optional[int] = None
 | 
			
		||||
                  ) -> List[Dict[str, Any]]:
 | 
			
		||||
        """
 | 
			
		||||
        Returns a list of dictionaries with full data for variables and their expanded members.
 | 
			
		||||
        Each dictionary contains: 'name', 'address', 'type', 'size', 'kind', 'count'.
 | 
			
		||||
        max_array_elems: limit unfolding of large arrays (None = all).
 | 
			
		||||
        """
 | 
			
		||||
        out: List[Dict[str, Any]] = []
 | 
			
		||||
 | 
			
		||||
        def get_dict(name: str, address: int, type_str: str, size: Optional[int], kind: Optional[str], count: Optional[int]) -> Dict[str, Any]:
 | 
			
		||||
            """Helper to create the output dictionary format."""
 | 
			
		||||
            return {
 | 
			
		||||
                'name': name,
 | 
			
		||||
                'address': address,
 | 
			
		||||
                'type': type_str,
 | 
			
		||||
                'size': size,
 | 
			
		||||
                'kind': kind,
 | 
			
		||||
                'count': count
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        def compute_stride(size_bytes: Optional[int],
 | 
			
		||||
                           count: Optional[int],
 | 
			
		||||
                           base_type: Optional[str],
 | 
			
		||||
                           node_children: Optional[List[MemberNode]]) -> int:
 | 
			
		||||
            """Calculates the stride (size of one element) for arrays."""
 | 
			
		||||
            # 1) size_bytes/count
 | 
			
		||||
            if size_bytes and count and count > 0:
 | 
			
		||||
                if size_bytes % count == 0:
 | 
			
		||||
                    stride = size_bytes // count
 | 
			
		||||
                    if stride <= 0:
 | 
			
		||||
                        stride = 1
 | 
			
		||||
                    return stride
 | 
			
		||||
                else:
 | 
			
		||||
                    # size not divisible by count → most likely size = size of one element
 | 
			
		||||
                    return max(size_bytes, 1)
 | 
			
		||||
 | 
			
		||||
            # 2) attempt by type (primitive)
 | 
			
		||||
            if base_type:
 | 
			
		||||
                gs = self._guess_primitive_size(base_type)
 | 
			
		||||
                if gs:
 | 
			
		||||
                    return gs
 | 
			
		||||
 | 
			
		||||
            # 3) attempt by children (structure)
 | 
			
		||||
            if node_children:
 | 
			
		||||
                if not node_children:
 | 
			
		||||
                    return 1
 | 
			
		||||
 | 
			
		||||
                min_off = min(ch.offset for ch in node_children)
 | 
			
		||||
                max_end = min_off
 | 
			
		||||
                for ch in node_children:
 | 
			
		||||
                    sz = ch.size
 | 
			
		||||
                    if not sz:
 | 
			
		||||
                        sz = self._guess_primitive_size(ch.type_str) or 1
 | 
			
		||||
                    end = ch.offset + sz
 | 
			
		||||
                    if end > max_end:
 | 
			
		||||
                        max_end = end
 | 
			
		||||
                stride = max_end - min_off
 | 
			
		||||
                if stride > 0:
 | 
			
		||||
                    return stride
 | 
			
		||||
 | 
			
		||||
            return 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        def expand_members(prefix_name: str,
 | 
			
		||||
                           base_addr: int,
 | 
			
		||||
                           members: List[MemberNode],
 | 
			
		||||
                           parent_is_ptr_struct: bool):
 | 
			
		||||
            """
 | 
			
		||||
            Recursively expands members of structs/unions or pointed-to structs.
 | 
			
		||||
            parent_is_ptr_struct: if True, connection is '->' otherwise '.'
 | 
			
		||||
            """
 | 
			
		||||
            join = '->' if parent_is_ptr_struct else '.'
 | 
			
		||||
            for m in members:
 | 
			
		||||
                path_m = f"{prefix_name}{join}{m.name}" if prefix_name else m.name
 | 
			
		||||
                addr_m = base_addr + m.offset
 | 
			
		||||
                out.append(get_dict(path_m, addr_m, m.type_str, m.size, m.kind, m.count))
 | 
			
		||||
 | 
			
		||||
                # array?
 | 
			
		||||
                if (m.kind == 'array') or m.type_str.endswith('[]'):
 | 
			
		||||
                    count = m.count
 | 
			
		||||
                    if count is None:
 | 
			
		||||
                        count = 0
 | 
			
		||||
                    if count <= 0:
 | 
			
		||||
                        continue
 | 
			
		||||
                    base_t = self._strip_array_suffix(m.type_str)
 | 
			
		||||
                    stride = compute_stride(m.size, count, base_t, m.children if m.children else None)
 | 
			
		||||
                    limit = count if max_array_elems is None else min(count, max_array_elems)
 | 
			
		||||
                    for i in range(limit):
 | 
			
		||||
                        path_i = f"{path_m}[{i}]"
 | 
			
		||||
                        addr_i = addr_m + i*stride
 | 
			
		||||
                        # Determine kind for array element based on its base type
 | 
			
		||||
                        elem_kind = None
 | 
			
		||||
                        if self._is_struct_or_union(base_t):
 | 
			
		||||
                            elem_kind = 'struct' # or 'union' depending on `base_t` prefix
 | 
			
		||||
                        elif self._guess_primitive_size(base_t):
 | 
			
		||||
                            elem_kind = 'primitive'
 | 
			
		||||
 | 
			
		||||
                        # For array elements, 'size' is the stride (size of one element), 'count' is None.
 | 
			
		||||
                        out.append(get_dict(path_i, addr_i, base_t, stride, elem_kind, None))
 | 
			
		||||
 | 
			
		||||
                        # array element: if structure / union → unfold fields
 | 
			
		||||
                        if m.children and self._is_struct_or_union(base_t):
 | 
			
		||||
                            expand_members(path_i, addr_i, m.children, parent_is_ptr_struct=False)
 | 
			
		||||
                        # array element: if pointer to structure
 | 
			
		||||
                        elif self._is_pointer_to_struct(base_t):
 | 
			
		||||
                            # usually no children in XML for these, but if present — use them
 | 
			
		||||
                            expand_members(path_i, addr_i, m.children, parent_is_ptr_struct=True)
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                # not an array, but has children (e.g., struct/union)
 | 
			
		||||
                if m.children:
 | 
			
		||||
                    is_ptr_struct = self._is_pointer_to_struct(m.type_str)
 | 
			
		||||
                    expand_members(path_m, addr_m, m.children, parent_is_ptr_struct=is_ptr_struct)
 | 
			
		||||
 | 
			
		||||
        # --- top-level variables ---
 | 
			
		||||
        for v in self.variables:
 | 
			
		||||
            out.append(get_dict(v.name, v.address, v.type_str, v.size, v.kind, v.count))
 | 
			
		||||
 | 
			
		||||
            # top-level array?
 | 
			
		||||
            if (v.kind == 'array') or v.type_str.endswith('[]'):
 | 
			
		||||
                count = v.count
 | 
			
		||||
                if count is None:
 | 
			
		||||
                    count = 0
 | 
			
		||||
                if count > 0:
 | 
			
		||||
                    base_t = self._strip_array_suffix(v.type_str)
 | 
			
		||||
                    stride = compute_stride(v.size, count, base_t, v.members if v.members else None)
 | 
			
		||||
                    limit = count if max_array_elems is None else min(count, max_array_elems)
 | 
			
		||||
                    for i in range(limit):
 | 
			
		||||
                        p = f"{v.name}[{i}]"
 | 
			
		||||
                        a = v.address + i*stride
 | 
			
		||||
                        # Determine kind for array element
 | 
			
		||||
                        elem_kind = None
 | 
			
		||||
                        if self._is_struct_or_union(base_t):
 | 
			
		||||
                            elem_kind = 'struct' # or 'union'
 | 
			
		||||
                        elif self._guess_primitive_size(base_t):
 | 
			
		||||
                            elem_kind = 'primitive'
 | 
			
		||||
 | 
			
		||||
                        out.append(get_dict(p, a, base_t, stride, elem_kind, None))
 | 
			
		||||
 | 
			
		||||
                        # array of structs?
 | 
			
		||||
                        if v.members and self._is_struct_or_union(base_t):
 | 
			
		||||
                            expand_members(p, a, v.members, parent_is_ptr_struct=False)
 | 
			
		||||
                        # array of pointers to structs?
 | 
			
		||||
                        elif self._is_pointer_to_struct(base_t):
 | 
			
		||||
                            expand_members(p, a, v.members, parent_is_ptr_struct=True)
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            # top-level not an array, but has members
 | 
			
		||||
            if v.members:
 | 
			
		||||
                is_ptr_struct = self._is_pointer_to_struct(v.type_str)
 | 
			
		||||
                expand_members(v.name, v.address, v.members, parent_is_ptr_struct=is_ptr_struct)
 | 
			
		||||
 | 
			
		||||
        return out
 | 
			
		||||
 | 
			
		||||
    # -------------------- date candidates (as it was) --------------------
 | 
			
		||||
 | 
			
		||||
    def date_struct_candidates(self) -> List[Tuple[str,int]]:
 | 
			
		||||
        cands = []
 | 
			
		||||
        for v in self.variables:
 | 
			
		||||
            # top level (if all date fields are present)
 | 
			
		||||
            direct_names = {mm.name for mm in v.members}
 | 
			
		||||
            if DATE_FIELD_SET.issubset(direct_names):
 | 
			
		||||
                cands.append((v.name, v.address))
 | 
			
		||||
            # check first-level members
 | 
			
		||||
            for m in v.members:
 | 
			
		||||
                if m.is_date_struct():
 | 
			
		||||
                    cands.append((f"{v.name}.{m.name}", v.address + m.offset))
 | 
			
		||||
        return cands
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def get_all_vars_data(self) -> List[Dict[str, Any]]:
 | 
			
		||||
        """
 | 
			
		||||
        Возвращает вложенную структуру словарей с полными данными для всех переменных и их развернутых членов.
 | 
			
		||||
        Каждый словарь представляет узел в иерархии и содержит:
 | 
			
		||||
        'name' (полный путь), 'address', 'size', 'type', 'kind', 'count', и 'children' (если есть).
 | 
			
		||||
        Логика определения родительского пути теперь использует `split_path` для анализа структуры пути.
 | 
			
		||||
        """
 | 
			
		||||
        flat_data = self.flattened(max_array_elems=None)
 | 
			
		||||
 | 
			
		||||
        root_nodes: List[Dict[str, Any]] = []
 | 
			
		||||
        all_nodes_map: Dict[str, Dict[str, Any]] = {}
 | 
			
		||||
 | 
			
		||||
        for item in flat_data:
 | 
			
		||||
            node_dict = {**item, 'children': []}
 | 
			
		||||
            all_nodes_map[item['name']] = node_dict
 | 
			
		||||
 | 
			
		||||
        # Вспомогательная функция для определения полного пути родителя с использованием split_path
 | 
			
		||||
        def get_parent_path_using_split(full_path: str) -> Optional[str]:
 | 
			
		||||
            # 1. Используем split_path для получения компонентов пути.
 | 
			
		||||
            components = var_setup.split_path(full_path)
 | 
			
		||||
            
 | 
			
		||||
            # Если нет компонентов или только один (верхний уровень, не массивный элемент)
 | 
			
		||||
            if not components or len(components) == 1:
 | 
			
		||||
                # Если компонент один и это не индекс массива (например, "project" или "my_var")
 | 
			
		||||
                # тогда у него нет родителя в этой иерархии.
 | 
			
		||||
                # Если это был бы "my_array[0]" -> components=['my_array', '[0]'], len=2
 | 
			
		||||
                if len(components) == 1 and not components[0].startswith('['):
 | 
			
		||||
                    return None
 | 
			
		||||
                elif len(components) == 2 and components[-1].startswith('['): # like "my_array[0]"
 | 
			
		||||
                    return components[0] # Return "my_array" as parent
 | 
			
		||||
                else: # Edge cases or malformed, treat as root
 | 
			
		||||
                    return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            # 2. Определяем, как отрезать "хвост" из оригинальной строки `full_path`, чтобы получить родителя.
 | 
			
		||||
            # Эта логика остаётся похожей на предыдущую, так как `split_path` не включает разделители
 | 
			
		||||
            # и мы должны получить точную строку родительского пути.
 | 
			
		||||
 | 
			
		||||
            # Находим индекс последнего разделителя '.' или '->'
 | 
			
		||||
            last_dot_idx = full_path.rfind('.')
 | 
			
		||||
            last_arrow_idx = full_path.rfind('->')
 | 
			
		||||
 | 
			
		||||
            effective_last_sep_idx = -1
 | 
			
		||||
            if last_dot_idx > last_arrow_idx:
 | 
			
		||||
                effective_last_sep_idx = last_dot_idx
 | 
			
		||||
            elif last_arrow_idx != -1:
 | 
			
		||||
                effective_last_sep_idx = last_arrow_idx
 | 
			
		||||
 | 
			
		||||
            # Находим начало последнего суффикса массива (e.g., '[0]') в оригинальной строке
 | 
			
		||||
            array_suffix_match = re.search(r'(\[[^\]]*\])+$', full_path)
 | 
			
		||||
            array_suffix_start_idx = -1
 | 
			
		||||
            if array_suffix_match:
 | 
			
		||||
                array_suffix_start_idx = array_suffix_match.start()
 | 
			
		||||
            
 | 
			
		||||
            # Логика определения родителя:
 | 
			
		||||
            # - Если есть суффикс массива, и он находится после последнего разделителя (или разделителей нет),
 | 
			
		||||
            #   то родитель - это часть до суффикса массива. (e.g., 'project.adc[0]' -> 'project.adc')
 | 
			
		||||
            # - Иначе, если есть разделитель, родитель - это часть до последнего разделителя. (e.g., 'project.adc.bus' -> 'project.adc')
 | 
			
		||||
            # - Иначе (ни разделителей, ни суффиксов), это корневой элемент.
 | 
			
		||||
            if array_suffix_start_idx != -1 and (array_suffix_start_idx > effective_last_sep_idx):
 | 
			
		||||
                return full_path[:array_suffix_start_idx]
 | 
			
		||||
            elif effective_last_sep_idx != -1:
 | 
			
		||||
                return full_path[:effective_last_sep_idx]
 | 
			
		||||
            else:
 | 
			
		||||
                return None # Корневой элемент без явного родителя
 | 
			
		||||
 | 
			
		||||
        # Основная логика get_all_vars_data
 | 
			
		||||
        
 | 
			
		||||
        # Заполнение связей "родитель-потомок"
 | 
			
		||||
        for item_name, node_dict in all_nodes_map.items():
 | 
			
		||||
            parent_name = get_parent_path_using_split(item_name) # Используем новую вспомогательную функцию
 | 
			
		||||
            if parent_name and parent_name in all_nodes_map:
 | 
			
		||||
                all_nodes_map[parent_name]['children'].append(node_dict)
 | 
			
		||||
            else:
 | 
			
		||||
                root_nodes.append(node_dict)
 | 
			
		||||
        
 | 
			
		||||
        # Сортируем корневые узлы и их детей рекурсивно по имени
 | 
			
		||||
        def sort_nodes(nodes_list: List[Dict[str, Any]]):
 | 
			
		||||
            nodes_list.sort(key=lambda x: x['name'])
 | 
			
		||||
            for node in nodes_list:
 | 
			
		||||
                if node['children']:
 | 
			
		||||
                    sort_nodes(node['children'])
 | 
			
		||||
        
 | 
			
		||||
        sort_nodes(root_nodes)
 | 
			
		||||
 | 
			
		||||
        return root_nodes
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										223
									
								
								Src/csv_logger.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								Src/csv_logger.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,223 @@
 | 
			
		||||
import csv
 | 
			
		||||
import numbers
 | 
			
		||||
import time
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from PySide2 import QtWidgets
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CsvLogger:
 | 
			
		||||
    """
 | 
			
		||||
    Логгер, совместимый по формату с C-реализацией CSV_AddTitlesLine / CSV_AddLogLine.
 | 
			
		||||
 | 
			
		||||
    Публичный API сохранён:
 | 
			
		||||
        set_titles(varnames)
 | 
			
		||||
        set_value(timestamp, varname, varvalue)
 | 
			
		||||
        select_file(parent=None) -> bool
 | 
			
		||||
        write_to_csv()
 | 
			
		||||
 | 
			
		||||
    Использование:
 | 
			
		||||
        1) set_titles([...])
 | 
			
		||||
        2) многократно set_value(ts, name, value)
 | 
			
		||||
        3) select_file() (по желанию)
 | 
			
		||||
        4) write_to_csv()
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, filename="log.csv", delimiter=';'):
 | 
			
		||||
        self._filename = filename
 | 
			
		||||
        self._delimiter = delimiter
 | 
			
		||||
 | 
			
		||||
        # Пользовательские заголовки
 | 
			
		||||
        self.variable_names_ordered = []
 | 
			
		||||
        # Полные заголовки CSV (Ticks(X), Ticks(Y), Time(Y), ...)
 | 
			
		||||
        self.headers = ['t']  # до вызова set_titles placeholder
 | 
			
		||||
 | 
			
		||||
        # Данные: {timestamp_key: {varname: value, ...}}
 | 
			
		||||
        # timestamp_key = то, что передано в set_value (float/int/etc)
 | 
			
		||||
        self.data_rows = {}
 | 
			
		||||
 | 
			
		||||
        # Внутренние структуры для генерации CSV-формата С
 | 
			
		||||
        self._row_wall_dt = {}      # {timestamp_key: datetime при первой записи}
 | 
			
		||||
        self._base_ts = None        # timestamp_key первой строки (число)
 | 
			
		||||
        self._base_ts_val = 0.0     # float значение первой строки (для delta)
 | 
			
		||||
        self._tick_x_start = 0      # начальный тик (можно менять вручную при необходимости)
 | 
			
		||||
 | 
			
		||||
    # ---- Свойства ----
 | 
			
		||||
    @property
 | 
			
		||||
    def filename(self):
 | 
			
		||||
        return self._filename
 | 
			
		||||
 | 
			
		||||
    # ---- Публичные методы ----
 | 
			
		||||
    def set_titles(self, varnames):
 | 
			
		||||
        """
 | 
			
		||||
        Устанавливает имена переменных.
 | 
			
		||||
        Формирует полные заголовки CSV в формате С-лога.
 | 
			
		||||
        """
 | 
			
		||||
        if not isinstance(varnames, list):
 | 
			
		||||
            raise TypeError("Varnames must be a list of strings.")
 | 
			
		||||
        if not all(isinstance(name, str) for name in varnames):
 | 
			
		||||
            raise ValueError("All variable names must be strings.")
 | 
			
		||||
 | 
			
		||||
        self.variable_names_ordered = varnames
 | 
			
		||||
        self.headers = ["Ticks(X)", "Ticks(Y)", "Time(Y)"] + self.variable_names_ordered
 | 
			
		||||
 | 
			
		||||
        # Сброс данных (структура изменилась)
 | 
			
		||||
        self.data_rows.clear()
 | 
			
		||||
        self._row_wall_dt.clear()
 | 
			
		||||
        self._base_ts = None
 | 
			
		||||
        self._base_ts_val = 0.0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def set_value(self, timestamp, varname, varvalue):
 | 
			
		||||
        """
 | 
			
		||||
        Установить ОДНО значение в ОДНУ колонку для заданного timestamp’а.
 | 
			
		||||
        timestamp — float секунд с эпохи (time.time()).
 | 
			
		||||
        """
 | 
			
		||||
        if varname not in self.variable_names_ordered:
 | 
			
		||||
            return  # игнор, как у тебя было
 | 
			
		||||
 | 
			
		||||
        # Новая строка?
 | 
			
		||||
        if timestamp not in self.data_rows:
 | 
			
		||||
            # Инициализируем поля переменных значением None
 | 
			
		||||
            self.data_rows[timestamp] = {vn: None for vn in self.variable_names_ordered}
 | 
			
		||||
 | 
			
		||||
            # Дата/время строки из ПЕРЕДАННОГО timestamp (а не datetime.now()!)
 | 
			
		||||
            try:
 | 
			
		||||
                ts_float = float(timestamp)
 | 
			
		||||
            except Exception:
 | 
			
		||||
                # если какая-то дичь прилетела, пусть будет 0 (эпоха) чтобы не упасть
 | 
			
		||||
                ts_float = 0.0
 | 
			
		||||
            self._row_wall_dt[timestamp] = datetime.fromtimestamp(ts_float)
 | 
			
		||||
 | 
			
		||||
            # База для расчёта Ticks(Y) — первая строка
 | 
			
		||||
            if self._base_ts is None:
 | 
			
		||||
                self._base_ts = timestamp
 | 
			
		||||
                self._base_ts_val = ts_float
 | 
			
		||||
 | 
			
		||||
        # Записываем значение
 | 
			
		||||
        self.data_rows[timestamp][varname] = varvalue
 | 
			
		||||
 | 
			
		||||
    def select_file(self, parent=None) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Диалог выбора файла.
 | 
			
		||||
        """
 | 
			
		||||
        options = QtWidgets.QFileDialog.Options()
 | 
			
		||||
        filename, _ = QtWidgets.QFileDialog.getSaveFileName(
 | 
			
		||||
            parent,
 | 
			
		||||
            "Сохранить данные CSV",
 | 
			
		||||
            self._filename,
 | 
			
		||||
            "CSV Files (*.csv);;All Files (*)",
 | 
			
		||||
            options=options
 | 
			
		||||
        )
 | 
			
		||||
        if filename:
 | 
			
		||||
            if not filename.lower().endswith('.csv'):
 | 
			
		||||
                filename += '.csv'
 | 
			
		||||
            self._filename = filename
 | 
			
		||||
            return True
 | 
			
		||||
        else:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
    def write_to_csv(self):
 | 
			
		||||
        """
 | 
			
		||||
        Формирует CSV в формате C:
 | 
			
		||||
            Ticks(X);Ticks(Y);Time(Y);Var1;Var2;...
 | 
			
		||||
            0;0,000000;22/07/2025 13:45:12:0123;...;...
 | 
			
		||||
 | 
			
		||||
        Правила значений:
 | 
			
		||||
            - Тик X: автоинкремент от 0 (или self._tick_x_start) по порядку сортировки timestamp.
 | 
			
		||||
            - Ticks(Y): дельта (секунды,микросекунды) между текущим timestamp и первым timestamp.
 | 
			
		||||
            - Time(Y): wallclock строки (datetime.now() при первом появлении timestamp).
 | 
			
		||||
            - Значение < 0 -> пустая ячейка (как if(raw_data[i] >= 0) else ;)
 | 
			
		||||
            - None -> пустая ячейка.
 | 
			
		||||
        """
 | 
			
		||||
        if len(self.headers) <= 3:  # только служебные поля без переменных
 | 
			
		||||
            print("Ошибка: Заголовки не установлены или не содержат переменных. Вызовите set_titles() перед записью.")
 | 
			
		||||
            return
 | 
			
		||||
        if not self._filename:
 | 
			
		||||
            print("Ошибка: Имя файла не определено. select_file() или задайте при инициализации.")
 | 
			
		||||
            return
 | 
			
		||||
        if not self.data_rows:
 | 
			
		||||
            print("Предупреждение: Нет данных для записи.")
 | 
			
		||||
            # всё равно создадим файл с одними заголовками
 | 
			
		||||
        try:
 | 
			
		||||
            with open(self._filename, 'w', newline='', encoding='utf-8') as csvfile:
 | 
			
		||||
                # QUOTE_NONE + escapechar для чистого формата без кавычек (как в С-строке)
 | 
			
		||||
                writer = csv.writer(
 | 
			
		||||
                    csvfile,
 | 
			
		||||
                    delimiter=self._delimiter,
 | 
			
		||||
                    quoting=csv.QUOTE_NONE,
 | 
			
		||||
                    escapechar='\\',
 | 
			
		||||
                    lineterminator='\r\n'
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                # Пишем заголовки
 | 
			
		||||
                writer.writerow(self.headers)
 | 
			
		||||
 | 
			
		||||
                if self.data_rows:
 | 
			
		||||
                    sorted_ts = sorted(self.data_rows.keys(), key=self._ts_sort_key)
 | 
			
		||||
                    # убедимся, что база была зафиксирована
 | 
			
		||||
                    if self._base_ts is None:
 | 
			
		||||
                        self._base_ts = sorted_ts[0]
 | 
			
		||||
                        self._base_ts_val = self._coerce_ts_to_float(self._base_ts)
 | 
			
		||||
 | 
			
		||||
                    tick_x = self._tick_x_start
 | 
			
		||||
                    for ts in sorted_ts:
 | 
			
		||||
                        row_dict = self.data_rows[ts]
 | 
			
		||||
                        # delta по timestamp
 | 
			
		||||
                        cur_ts_val = self._coerce_ts_to_float(ts)
 | 
			
		||||
                        delta_us = int(round((cur_ts_val - self._base_ts_val) * 1_000_000))
 | 
			
		||||
                        if delta_us < 0:
 | 
			
		||||
                            delta_us = 0  # защита
 | 
			
		||||
 | 
			
		||||
                        seconds = delta_us // 1_000_000
 | 
			
		||||
                        micros = delta_us % 1_000_000
 | 
			
		||||
 | 
			
		||||
                        # wallclock строки
 | 
			
		||||
                        dt = self._row_wall_dt.get(ts, datetime.now())
 | 
			
		||||
                        # Формат DD/MM/YYYY HH:MM:SS:мммм (4 цифры ms, как в C: us/1000)
 | 
			
		||||
                        time_str = dt.strftime("%d/%m/%Y %H:%M:%S") + f":{dt.microsecond // 1000:04d}"
 | 
			
		||||
 | 
			
		||||
                        # Значения
 | 
			
		||||
                        row_vals = []
 | 
			
		||||
                        for vn in self.variable_names_ordered:
 | 
			
		||||
                            v = row_dict.get(vn)
 | 
			
		||||
                            if v is None:
 | 
			
		||||
                                row_vals.append("")  # нет данных
 | 
			
		||||
                            else:
 | 
			
		||||
                                # если числовое и <0 -> пусто (как в C: если raw_data[i] >= 0 else ;)
 | 
			
		||||
                                if isinstance(v, numbers.Number) and v < 0:
 | 
			
		||||
                                    row_vals.append("")
 | 
			
		||||
                                else:
 | 
			
		||||
                                    row_vals.append(v)
 | 
			
		||||
 | 
			
		||||
                        csv_row = [tick_x, f"{seconds},{micros:06d}", time_str] + row_vals
 | 
			
		||||
                        writer.writerow(csv_row)
 | 
			
		||||
                        tick_x += 1
 | 
			
		||||
 | 
			
		||||
            print(f"Данные успешно записаны в '{self._filename}'")
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(f"Ошибка при записи в файл '{self._filename}': {e}")
 | 
			
		||||
 | 
			
		||||
    # ---- Вспомогательные ----
 | 
			
		||||
    def _coerce_ts_to_float(self, ts):
 | 
			
		||||
        """
 | 
			
		||||
        Пробуем привести переданный timestamp к float.
 | 
			
		||||
        Разрешаем int/float/str, остальное -> индекс по порядку (0).
 | 
			
		||||
        """
 | 
			
		||||
        if isinstance(ts, numbers.Number):
 | 
			
		||||
            return float(ts)
 | 
			
		||||
        try:
 | 
			
		||||
            return float(ts)
 | 
			
		||||
        except Exception:
 | 
			
		||||
            # fallback: нечисловой ключ -> используем порядковый индекс
 | 
			
		||||
            # (таких почти не должно быть, но на всякий)
 | 
			
		||||
            return 0.0
 | 
			
		||||
 | 
			
		||||
    def _ts_sort_key(self, ts):
 | 
			
		||||
        """
 | 
			
		||||
        Ключ сортировки timestamp’ов — сначала попытка float, потом str.
 | 
			
		||||
        """
 | 
			
		||||
        if isinstance(ts, numbers.Number):
 | 
			
		||||
            return (0, float(ts))
 | 
			
		||||
        try:
 | 
			
		||||
            return (0, float(ts))
 | 
			
		||||
        except Exception:
 | 
			
		||||
            return (1, str(ts))
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -2,7 +2,7 @@ from PySide2 import QtCore, QtWidgets, QtSerialPort
 | 
			
		||||
from tms_debugvar_lowlevel import LowLevelSelectorWidget
 | 
			
		||||
import datetime
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
from csv_logger import CsvLogger
 | 
			
		||||
# ------------------------------- Константы протокола ------------------------
 | 
			
		||||
WATCH_SERVICE_BIT = 0x8000
 | 
			
		||||
DEBUG_OK = 0  # ожидаемый код успешного чтения
 | 
			
		||||
@ -176,6 +176,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
        self._drop_if_busy = drop_if_busy
 | 
			
		||||
        self._replace_if_busy = replace_if_busy
 | 
			
		||||
        self._last_txn_timestamp = 0
 | 
			
		||||
        self._ll_polling_active = False
 | 
			
		||||
        if iq_scaling is None:
 | 
			
		||||
            iq_scaling = {n: float(1 << n) for n in range(31)}
 | 
			
		||||
            iq_scaling[0] = 1.0
 | 
			
		||||
@ -206,7 +207,13 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
        self._ll_poll_timer = QtCore.QTimer(self)
 | 
			
		||||
        self._ll_poll_timer.timeout.connect(self._on_ll_poll_timeout)
 | 
			
		||||
        self._ll_polling = False
 | 
			
		||||
        self._ll_current_var_info = None # Хранит инфо о выбранной LL переменной
 | 
			
		||||
        self._ll_polling_variables = [] # List of selected variables for polling
 | 
			
		||||
        self._ll_current_poll_index = -1 # Index of the variable currently being polled in the _ll_polling_variables list
 | 
			
		||||
        self._ll_current_var_info = []
 | 
			
		||||
 | 
			
		||||
        self.csv_logger = CsvLogger()
 | 
			
		||||
        self._csv_logging_active = False
 | 
			
		||||
        self._last_csv_timestamp = 0 # Для отслеживания времени записи
 | 
			
		||||
 | 
			
		||||
        # Кэш: index -> (status, iq, name, is_signed, frac_bits)
 | 
			
		||||
        self._name_cache = {}
 | 
			
		||||
@ -261,6 +268,33 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
        self.chk_raw = QtWidgets.QCheckBox("Raw (no IQ scaling)")
 | 
			
		||||
        control_layout.addWidget(self.chk_raw)
 | 
			
		||||
 | 
			
		||||
        # Создаем QGroupBox для группировки элементов управления CSV
 | 
			
		||||
        self.csv_log_groupbox = QtWidgets.QGroupBox("CSV Logging")
 | 
			
		||||
        csv_log_layout = QtWidgets.QVBoxLayout(self.csv_log_groupbox) # Передаем groupbox как родительский layout
 | 
			
		||||
 | 
			
		||||
        # Элементы управления CSV
 | 
			
		||||
        h_file_select = QtWidgets.QHBoxLayout()
 | 
			
		||||
        self.btn_select_csv_file = QtWidgets.QPushButton("Выбрать файл CSV")
 | 
			
		||||
        # Убедитесь, что self.csv_logger инициализирован где-то до этого момента
 | 
			
		||||
        self.lbl_csv_filename = QtWidgets.QLabel(self.csv_logger.filename)
 | 
			
		||||
        h_file_select.addWidget(self.btn_select_csv_file)
 | 
			
		||||
        h_file_select.addWidget(self.lbl_csv_filename, 1)
 | 
			
		||||
        csv_log_layout.addLayout(h_file_select)
 | 
			
		||||
 | 
			
		||||
        h_control_buttons = QtWidgets.QHBoxLayout()
 | 
			
		||||
        self.btn_start_csv_logging = QtWidgets.QPushButton("Начать запись в CSV")
 | 
			
		||||
        self.btn_stop_csv_logging = QtWidgets.QPushButton("Остановить запись в CSV")
 | 
			
		||||
        self.btn_save_csv_data = QtWidgets.QPushButton("Сохранить данные в CSV")
 | 
			
		||||
        
 | 
			
		||||
        self.btn_stop_csv_logging.setEnabled(False) # По умолчанию остановлена
 | 
			
		||||
 | 
			
		||||
        h_control_buttons.addWidget(self.btn_start_csv_logging)
 | 
			
		||||
        h_control_buttons.addWidget(self.btn_stop_csv_logging)
 | 
			
		||||
        h_control_buttons.addWidget(self.btn_save_csv_data)
 | 
			
		||||
        csv_log_layout.addLayout(h_control_buttons)
 | 
			
		||||
 | 
			
		||||
        # Добавляем QGroupBox в основной лейаут
 | 
			
		||||
 | 
			
		||||
        # --- UART Log ---
 | 
			
		||||
        self.log_spoiler = Spoiler("UART Log", animationDuration=300, parent=self)
 | 
			
		||||
        self.log_spoiler.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
 | 
			
		||||
@ -274,6 +308,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
        layout.addWidget(g_serial)
 | 
			
		||||
        layout.addWidget(self.tabs, 1)
 | 
			
		||||
        layout.addWidget(g_control)
 | 
			
		||||
        layout.addWidget(self.csv_log_groupbox) 
 | 
			
		||||
        layout.addWidget(self.log_spoiler)
 | 
			
		||||
        layout.setStretch(layout.indexOf(g_serial), 0)
 | 
			
		||||
        layout.setStretch(layout.indexOf(self.tabs), 1)
 | 
			
		||||
@ -369,56 +404,10 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    def _build_lowlevel_tab(self):
 | 
			
		||||
        tab = QtWidgets.QWidget()
 | 
			
		||||
        main_layout = QtWidgets.QVBoxLayout(tab)
 | 
			
		||||
 | 
			
		||||
        # --- GroupBox для селектора переменной ---
 | 
			
		||||
        group_var_selector = QtWidgets.QGroupBox("Variable Selector", tab)
 | 
			
		||||
        var_selector_layout = QtWidgets.QVBoxLayout(group_var_selector)
 | 
			
		||||
        self.ll_selector = LowLevelSelectorWidget(group_var_selector)
 | 
			
		||||
        var_selector_layout.addWidget(self.ll_selector)
 | 
			
		||||
        
 | 
			
		||||
        # --- GroupBox для панели управления чтением ---
 | 
			
		||||
        group_read_controls = QtWidgets.QGroupBox("Read Selected Variable", tab)
 | 
			
		||||
        grid = QtWidgets.QGridLayout(group_read_controls)
 | 
			
		||||
 | 
			
		||||
        self.btn_ll_read = QtWidgets.QPushButton("Read Once")
 | 
			
		||||
        self.btn_ll_poll = QtWidgets.QPushButton("Start Polling")
 | 
			
		||||
        self.spin_ll_interval = QtWidgets.QSpinBox()
 | 
			
		||||
        self.spin_ll_interval.setRange(50, 10000)
 | 
			
		||||
        self.spin_ll_interval.setValue(500)
 | 
			
		||||
        self.spin_ll_interval.setSuffix(" ms")
 | 
			
		||||
        
 | 
			
		||||
        # Поля для отображения результата
 | 
			
		||||
        self.ll_val_status = QtWidgets.QLabel("-")
 | 
			
		||||
        self.ll_val_rettype = QtWidgets.QLabel("-")
 | 
			
		||||
        self.ll_val_scaled = QtWidgets.QLabel("-")
 | 
			
		||||
 | 
			
		||||
        # Размещение виджетов в grid
 | 
			
		||||
        grid.addWidget(self.btn_ll_read, 0, 0)
 | 
			
		||||
        grid.addWidget(self.btn_ll_poll, 0, 1)
 | 
			
		||||
        grid.addWidget(QtWidgets.QLabel("Interval:"), 1, 0)
 | 
			
		||||
        grid.addWidget(self.spin_ll_interval, 1, 1)
 | 
			
		||||
 | 
			
		||||
        # Форма для результатов
 | 
			
		||||
        form_layout = QtWidgets.QFormLayout()
 | 
			
		||||
        form_layout.addRow("Status:", self.ll_val_status)
 | 
			
		||||
        form_layout.addRow("Return Type:", self.ll_val_rettype)
 | 
			
		||||
        form_layout.addRow("Scaled Value:", self.ll_val_scaled)
 | 
			
		||||
        # Поле Raw Value убрано
 | 
			
		||||
 | 
			
		||||
        grid.addLayout(form_layout, 2, 0, 1, 2) # Растягиваем на 2 колонки
 | 
			
		||||
        grid.setColumnStretch(1, 1)
 | 
			
		||||
 | 
			
		||||
        # Собираем layout вкладки
 | 
			
		||||
        v_split = QtWidgets.QSplitter(QtCore.Qt.Vertical, tab)
 | 
			
		||||
        v_split.addWidget(group_var_selector)
 | 
			
		||||
        v_split.addWidget(group_read_controls)
 | 
			
		||||
        v_split.setStretchFactor(0, 1) # Селектор растягивается
 | 
			
		||||
        v_split.setStretchFactor(1, 0) # Панель чтения - нет
 | 
			
		||||
        
 | 
			
		||||
        main_layout.addWidget(v_split)
 | 
			
		||||
        self.tabs.addTab(tab, "LowLevel")
 | 
			
		||||
        # создаём виджет LowLevelSelectorWidget
 | 
			
		||||
        self.ll_selector = LowLevelSelectorWidget()
 | 
			
		||||
        # добавляем как корневой виджет вкладки
 | 
			
		||||
        self.tabs.addTab(self.ll_selector, "LowLevel")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def _connect_ui(self):
 | 
			
		||||
@ -433,8 +422,14 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
        # 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.btn_ll_read.clicked.connect(self.request_lowlevel_once)
 | 
			
		||||
        self.btn_ll_poll.clicked.connect(self._toggle_ll_polling)
 | 
			
		||||
        self.ll_selector.btn_read_once.clicked.connect(self.request_lowlevel_once)
 | 
			
		||||
        self.ll_selector.btn_start_polling.clicked.connect(self._toggle_ll_polling)
 | 
			
		||||
 | 
			
		||||
        # --- CSV Logging ---
 | 
			
		||||
        self.btn_select_csv_file.clicked.connect(self._select_csv_file)
 | 
			
		||||
        self.btn_start_csv_logging.clicked.connect(self._start_csv_logging)
 | 
			
		||||
        self.btn_stop_csv_logging.clicked.connect(self._stop_csv_logging)
 | 
			
		||||
        self.btn_save_csv_data.clicked.connect(self._save_csv_data)
 | 
			
		||||
        
 | 
			
		||||
    def set_status(self, text: str, mode: str = "idle"):
 | 
			
		||||
        colors = {
 | 
			
		||||
@ -465,7 +460,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
            name = self.serial.portName()
 | 
			
		||||
            self.serial.close()
 | 
			
		||||
            self.btn_open.setText("Open")
 | 
			
		||||
            self._log(f"[PORT] Closed {name}")
 | 
			
		||||
            self._log(f"[PORT OK] Closed {name}")
 | 
			
		||||
            self.portClosed.emit(name)
 | 
			
		||||
            return
 | 
			
		||||
        port = self.cmb_port.currentText()
 | 
			
		||||
@ -478,7 +473,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
            self._log(f"[ERR] Open fail {port}: {self.serial.errorString()}")
 | 
			
		||||
            return
 | 
			
		||||
        self.btn_open.setText("Close")
 | 
			
		||||
        self._log(f"[PORT] Opened {port}")
 | 
			
		||||
        self._log(f"[PORT OK] Opened {port}")
 | 
			
		||||
        self.portOpened.emit(port)
 | 
			
		||||
 | 
			
		||||
    # ---------------------------- FRAME BUILD -----------------------------
 | 
			
		||||
@ -496,33 +491,29 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
    def _build_lowlevel_request(self, var_info: dict) -> bytes:
 | 
			
		||||
        # Формат: [adr][cmd_lowlevel][year_hi][year_lo][month][day][hour][minute][addr2][addr1][addr0][pt_type][iq_type][return_type]
 | 
			
		||||
        # Пытаемся получить время из переданной информации
 | 
			
		||||
        dt_info = var_info.get('datetime')
 | 
			
		||||
        dt_info = self.ll_selector.get_datetime()
 | 
			
		||||
    
 | 
			
		||||
        if dt_info:
 | 
			
		||||
            # Используем время из var_info
 | 
			
		||||
            year = dt_info.get('year', 2000)
 | 
			
		||||
            month = dt_info.get('month', 1)
 | 
			
		||||
            day = dt_info.get('day', 1)
 | 
			
		||||
            hour = dt_info.get('hour', 0)
 | 
			
		||||
            minute = dt_info.get('minute', 0)
 | 
			
		||||
            year = dt_info.year
 | 
			
		||||
            month = dt_info.month
 | 
			
		||||
            day = dt_info.day
 | 
			
		||||
            hour = dt_info.hour
 | 
			
		||||
            minute = dt_info.minute
 | 
			
		||||
            self._log("[LL] Using time from selector.")
 | 
			
		||||
        else:
 | 
			
		||||
            # Если в var_info времени нет, используем текущее системное время (старое поведение)
 | 
			
		||||
            now = QtCore.QDateTime.currentDateTime()
 | 
			
		||||
            year = now.date().year()
 | 
			
		||||
            month = now.date().month()
 | 
			
		||||
            day = now.date().day()
 | 
			
		||||
            hour = now.time().hour()
 | 
			
		||||
            minute = now.time().minute()
 | 
			
		||||
            self._log("[LL] Fallback to current system time.")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        addr = var_info.get('address', 0)
 | 
			
		||||
        addr2 = (addr >> 16) & 0xFF
 | 
			
		||||
        addr1 = (addr >> 8) & 0xFF
 | 
			
		||||
        addr0 = addr & 0xFF
 | 
			
		||||
        pt_type = var_info.get('ptr_type', 0) & 0xFF
 | 
			
		||||
        iq_type = var_info.get('iq_type', 0) & 0xFF
 | 
			
		||||
        ret_type = var_info.get('return_type', 0) & 0xFF
 | 
			
		||||
        # Ensure 'ptr_type' and 'iq_type' from var_info are integers (enum values)
 | 
			
		||||
        # Use a fallback to 0 if they are not found or not integers
 | 
			
		||||
        pt_type = var_info.get('ptr_type_enum', 0) & 0xFF 
 | 
			
		||||
        iq_type = var_info.get('iq_type_enum', 0) & 0xFF
 | 
			
		||||
        ret_type = var_info.get('return_type_enum', 0) & 0xFF
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        frame_wo_crc = bytes([
 | 
			
		||||
            self.device_addr & 0xFF, self.cmd_lowlevel & 0xFF,
 | 
			
		||||
@ -561,9 +552,12 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
            
 | 
			
		||||
            # Запускаем стандартный запрос значений. Он автоматически обработает
 | 
			
		||||
            # отсутствующую сервисную информацию (имена/типы) перед запросом данных.
 | 
			
		||||
            #self.request_values()
 | 
			
		||||
            if not (self._polling or self._ll_polling):
 | 
			
		||||
                self.request_values()
 | 
			
		||||
 | 
			
		||||
    def request_values(self):
 | 
			
		||||
        self._update_interval()
 | 
			
		||||
 | 
			
		||||
        base = int(self.spin_index.value())
 | 
			
		||||
        count = int(self.spin_count.value())
 | 
			
		||||
        needed = []
 | 
			
		||||
@ -574,29 +568,30 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
            self._service_queue = needed[:]
 | 
			
		||||
            self._pending_data_after_services = (base, count)
 | 
			
		||||
            self._log(f"[AUTO] Need service for {len(needed)} indices: {needed}")
 | 
			
		||||
            self.set_status("read service...", "service")
 | 
			
		||||
            self.set_status("Read service...", "service")
 | 
			
		||||
            self._kick_service_queue()
 | 
			
		||||
        else:
 | 
			
		||||
            self.set_status("read values...", "values")
 | 
			
		||||
            self.set_status("Read values...", "values")
 | 
			
		||||
            self._enqueue_or_start(base, service=False, varqnt=count)
 | 
			
		||||
 | 
			
		||||
    def request_lowlevel_once(self):
 | 
			
		||||
        """Запрашивает чтение выбранной LowLevel переменной."""
 | 
			
		||||
        """Запрашивает чтение выбранной LowLevel переменной (однократно)."""
 | 
			
		||||
        if not self.serial.isOpen():
 | 
			
		||||
            self._log("[LL] Port is not open.")
 | 
			
		||||
            return
 | 
			
		||||
        if self._busy:
 | 
			
		||||
            self._log("[LL] Busy, request dropped.")
 | 
			
		||||
            return
 | 
			
		||||
        if not self._ll_current_var_info:
 | 
			
		||||
            self._log("[LL] No variable selected!")
 | 
			
		||||
            if self._ll_polling: # Если поллинг активен, но переменная пропала - стоп
 | 
			
		||||
                self._toggle_ll_polling()
 | 
			
		||||
        
 | 
			
		||||
        # Если переменная не подготовлена, или нет актуальной информации
 | 
			
		||||
        if not hasattr(self, '_ll_current_var_info') or not self._ll_current_var_info:
 | 
			
		||||
            self._log("[LL] No variable prepared/selected for single read!")
 | 
			
		||||
            return
 | 
			
		||||
            
 | 
			
		||||
        frame = self._build_lowlevel_request(self._ll_current_var_info)
 | 
			
		||||
        meta = {'lowlevel': True}
 | 
			
		||||
        self.set_status("read lowlevel...", "values")
 | 
			
		||||
        # --- НОВОЕ: Передаем ll_var_info в метаданные транзакции ---
 | 
			
		||||
        meta = {'lowlevel': True, 'll_polling': False, 'll_var_info': self._ll_current_var_info} 
 | 
			
		||||
        self.set_status("Read lowlevel...", "values")
 | 
			
		||||
        self._enqueue_raw(frame, meta)
 | 
			
		||||
 | 
			
		||||
    # -------------------------- SERVICE QUEUE FLOW ------------------------
 | 
			
		||||
@ -615,10 +610,14 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
    # ------------------------ TRANSACTION SCHEDULER -----------------------
 | 
			
		||||
    # ... (код без изменений)
 | 
			
		||||
    def _enqueue_raw(self, frame: bytes, meta: dict):
 | 
			
		||||
        # Добавляем ll_var_info, если это LL запрос
 | 
			
		||||
        if meta.get('lowlevel', False) and 'll_var_info' not in meta:
 | 
			
		||||
            # Это должно быть установлено вызывающим кодом, но для безопасности
 | 
			
		||||
            # или если LL polling не передал var_info явно
 | 
			
		||||
            meta['ll_var_info'] = self._ll_current_var_info # Используем last prepared var info for single shots
 | 
			
		||||
 | 
			
		||||
        if self._busy:
 | 
			
		||||
            if self._drop_if_busy and not self._replace_if_busy:
 | 
			
		||||
                self._log("[LOCKSTEP] Busy -> drop")
 | 
			
		||||
                return
 | 
			
		||||
            # ... существующий код ...
 | 
			
		||||
            if self._replace_if_busy:
 | 
			
		||||
                self._pending_cmd = (frame, meta)
 | 
			
		||||
                self._log("[LOCKSTEP] Busy -> replaced pending")
 | 
			
		||||
@ -643,14 +642,8 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
        self._start_txn(frame, meta)
 | 
			
		||||
 | 
			
		||||
    def _start_txn(self, frame: bytes, meta: dict):  
 | 
			
		||||
        now = time.perf_counter()
 | 
			
		||||
        if self._last_txn_timestamp is not None:
 | 
			
		||||
            delta_ms = (now - self._last_txn_timestamp) * 1000
 | 
			
		||||
            # Обновляем UI только если он уже создан
 | 
			
		||||
            if hasattr(self, 'lbl_actual_interval'):
 | 
			
		||||
                self.lbl_actual_interval.setText(f"{delta_ms:.1f} ms")                
 | 
			
		||||
        self._last_txn_timestamp = now
 | 
			
		||||
 | 
			
		||||
        if(meta.get('service')):            
 | 
			
		||||
            self._update_interval()
 | 
			
		||||
        self._busy = True
 | 
			
		||||
        self._txn_meta = meta
 | 
			
		||||
        self._rx_buf.clear()
 | 
			
		||||
@ -666,6 +659,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
        if meta:
 | 
			
		||||
            queue_mode = meta.get('queue_mode', False)
 | 
			
		||||
            chain = meta.get('chain')
 | 
			
		||||
 | 
			
		||||
        self._txn_meta = None
 | 
			
		||||
        self._busy = False
 | 
			
		||||
        self._rx_buf.clear()
 | 
			
		||||
@ -675,14 +669,23 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
            base, serv, q = chain
 | 
			
		||||
            self._enqueue_or_start(base, service=serv, varqnt=q)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if self._pending_cmd is not None:
 | 
			
		||||
            frame, meta = self._pending_cmd; self._pending_cmd = None
 | 
			
		||||
            frame, meta = self._pending_cmd
 | 
			
		||||
            self._pending_cmd = None
 | 
			
		||||
            QtCore.QTimer.singleShot(0, lambda f=frame, m=meta: self._start_txn(f, m))
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if queue_mode:
 | 
			
		||||
            QtCore.QTimer.singleShot(0, self._kick_service_queue)
 | 
			
		||||
            # !!! Раньше тут было `return`, его убираем
 | 
			
		||||
 | 
			
		||||
        # Если идёт LL polling — переходим сразу к следующей переменной
 | 
			
		||||
        if self._ll_polling and (self._ll_poll_index < len(self._ll_polling_variables)):
 | 
			
		||||
            self._process_next_ll_variable_in_cycle()
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def _on_txn_timeout(self):
 | 
			
		||||
        if not self._busy: return
 | 
			
		||||
        is_ll = self._txn_meta.get('lowlevel', False) if self._txn_meta else False
 | 
			
		||||
@ -691,6 +694,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
        if self._rx_buf:
 | 
			
		||||
            self._log_frame(bytes(self._rx_buf), tx=False)
 | 
			
		||||
        self._end_txn()
 | 
			
		||||
        self.set_status("Timeout", "error")
 | 
			
		||||
 | 
			
		||||
    # ------------------------------- TX/RX ---------------------------------
 | 
			
		||||
    # ... (код без изменений)
 | 
			
		||||
@ -710,12 +714,13 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
                self._rx_buf.clear()
 | 
			
		||||
            return
 | 
			
		||||
        self._try_parse()
 | 
			
		||||
        if not (self._polling or self._ll_polling):
 | 
			
		||||
            self.set_status("Idle", "idle")
 | 
			
		||||
    
 | 
			
		||||
    # ------------------------------- PARSING -------------------------------
 | 
			
		||||
    def _try_parse(self):
 | 
			
		||||
        if not self._txn_meta:
 | 
			
		||||
            return
 | 
			
		||||
        self.set_status("IDLE", "idle")
 | 
			
		||||
        if self._txn_meta.get('lowlevel', False):
 | 
			
		||||
            self._try_parse_lowlevel()
 | 
			
		||||
        else:
 | 
			
		||||
@ -781,6 +786,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
            self._parse_lowlevel_frame(frame, success=(status == DEBUG_OK))
 | 
			
		||||
            self._end_txn()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def _check_crc(self, payload: bytes, crc_lo: int, crc_hi: int):
 | 
			
		||||
        if not self.auto_crc_check:
 | 
			
		||||
            return True
 | 
			
		||||
@ -817,8 +823,8 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def _parse_data_frame(self, frame: bytes, *, error_mode: bool):
 | 
			
		||||
        # ... (код без изменений)
 | 
			
		||||
        payload = frame[:-4]; crc_lo, crc_hi = frame[-4], frame[-3]
 | 
			
		||||
        if len(payload) < 6:
 | 
			
		||||
            self._log("[ERR] Data frame too short"); return
 | 
			
		||||
@ -826,7 +832,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
        adr, cmd, vhi, vlo, varqnt, status = payload[:6]
 | 
			
		||||
        base = self._clear_service_bit(vhi, vlo)
 | 
			
		||||
        if error_mode:
 | 
			
		||||
            self.set_status("error", "error")
 | 
			
		||||
            self.set_status("Error", "error")
 | 
			
		||||
            if len(payload) < 8:
 | 
			
		||||
                self._log("[ERR] Error frame truncated"); return
 | 
			
		||||
            err_hi, err_lo = payload[6:8]
 | 
			
		||||
@ -851,6 +857,10 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
            raw16 = (hi << 8) | lo
 | 
			
		||||
            raw_vals.append(raw16)
 | 
			
		||||
        idx_list = []; iq_list = []; name_list = []; scaled_list = []; display_raw_list = []
 | 
			
		||||
        
 | 
			
		||||
        # Получаем текущее время один раз для всех переменных в этом фрейме
 | 
			
		||||
        current_time = time.time() 
 | 
			
		||||
 | 
			
		||||
        for ofs, raw16 in enumerate(raw_vals):
 | 
			
		||||
            idx = base + ofs
 | 
			
		||||
            status_i, iq_raw, name_i, is_signed, frac_bits = self._name_cache.get(idx, (DEBUG_OK, 0, '', False, 0))
 | 
			
		||||
@ -865,6 +875,10 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
            scaled = float(value_int) / scale if frac_bits > 0 else float(value_int)
 | 
			
		||||
            idx_list.append(idx); iq_list.append(iq_raw); name_list.append(name_i)
 | 
			
		||||
            scaled_list.append(scaled); display_raw_list.append(value_int)
 | 
			
		||||
            
 | 
			
		||||
            # --- Здесь записываем имя и значение в csv_logger ---
 | 
			
		||||
            self.csv_logger.set_value(current_time, name_i, scaled)
 | 
			
		||||
 | 
			
		||||
        self._populate_table(idx_list, name_list, iq_list, display_raw_list, scaled_list)
 | 
			
		||||
        if varqnt == 1:
 | 
			
		||||
            if idx_list[0] == self.spin_index.value():
 | 
			
		||||
@ -874,6 +888,8 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
            self.valuesRead.emit(base, varqnt, idx_list, iq_list, display_raw_list, scaled_list)
 | 
			
		||||
        self._log(f"[DATA] base={base} q={varqnt} values={[f'{v:.6g}' for v in scaled_list] if not self.chk_raw.isChecked() else raw_vals}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def _parse_lowlevel_frame(self, frame: bytes, success: bool):
 | 
			
		||||
        payload_len = 9 if success else 6
 | 
			
		||||
        crc_pos = payload_len
 | 
			
		||||
@ -887,13 +903,6 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
        addr24 = (addr2 << 16) | (addr1 << 8) | addr0
 | 
			
		||||
 | 
			
		||||
        status_desc = _decode_debug_status(status)
 | 
			
		||||
        self.ll_val_status.setText(f"0x{status:02X} ({status_desc})")
 | 
			
		||||
        
 | 
			
		||||
        if not success:
 | 
			
		||||
            self.ll_val_rettype.setText('-')
 | 
			
		||||
            self.ll_val_scaled.setText(f"<ERROR:{status_desc}>")
 | 
			
		||||
            self._log(f"[LL] ERROR status=0x{status:02X} ({status_desc}) addr=0x{addr24:06X}")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        return_type = payload[6]
 | 
			
		||||
        data_hi, data_lo = payload[7], payload[8]
 | 
			
		||||
@ -914,13 +923,22 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
 | 
			
		||||
        scaled = float(value_int) / scale
 | 
			
		||||
        
 | 
			
		||||
        # Обновляем UI
 | 
			
		||||
        self.ll_val_rettype.setText(f"0x{return_type:02X} ({frac_bits}{'s' if is_signed else 'u'})")
 | 
			
		||||
        self.ll_val_scaled.setText(f"{scaled:.6g}")
 | 
			
		||||
        
 | 
			
		||||
        self.llValueRead.emit(addr24, status, return_type, value_int, scaled)
 | 
			
		||||
 | 
			
		||||
        var_name = None
 | 
			
		||||
        if self._ll_current_var_info.get("address") == addr24:
 | 
			
		||||
            var_name = self._ll_current_var_info.get("name")
 | 
			
		||||
        display_val = value_int if self.chk_raw.isChecked() else scaled
 | 
			
		||||
        if var_name:
 | 
			
		||||
            self.ll_selector.set_variable_value(var_name, display_val)
 | 
			
		||||
 | 
			
		||||
        self._log(f"[LL] OK addr=0x{addr24:06X} type=0x{return_type:02X} raw={value_int} scaled={scaled:.6g}")
 | 
			
		||||
 | 
			
		||||
        current_time = time.time() # Получаем текущее время
 | 
			
		||||
        self.csv_logger.set_value(current_time, var_name, display_val)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def _populate_watch_error(self, bad_index: int, status: int):
 | 
			
		||||
        """Отобразить строку ошибки при неудачном ответе WATCH."""
 | 
			
		||||
        desc = _decode_debug_status(status)
 | 
			
		||||
@ -1003,12 +1021,14 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
            self._poll_timer.stop()
 | 
			
		||||
            self._polling = False
 | 
			
		||||
            self.btn_poll.setText("Start Polling")
 | 
			
		||||
            self.set_status("Idle", "idle")
 | 
			
		||||
            self._log("[POLL] Stopped")
 | 
			
		||||
        else:
 | 
			
		||||
            interval = self.spin_interval.value()
 | 
			
		||||
            self._poll_timer.start(interval)
 | 
			
		||||
            self._polling = True
 | 
			
		||||
            self.btn_poll.setText("Stop Polling")
 | 
			
		||||
            self.set_status("Idle", "idle")
 | 
			
		||||
            self._log(f"[POLL] Started interval={interval}ms")
 | 
			
		||||
        self._set_ui_busy(False) # Обновить доступность кнопок
 | 
			
		||||
 | 
			
		||||
@ -1016,36 +1036,74 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
        self.request_values()
 | 
			
		||||
 | 
			
		||||
    def _toggle_ll_polling(self):
 | 
			
		||||
        """Включает и выключает поллинг для LowLevel вкладки."""
 | 
			
		||||
        if self._ll_polling:
 | 
			
		||||
            self._ll_poll_timer.stop()
 | 
			
		||||
        if self._ll_polling: # If currently polling, stop
 | 
			
		||||
            self._ll_polling = False
 | 
			
		||||
            self.btn_ll_poll.setText("Start Polling")
 | 
			
		||||
            self._log("[LL POLL] Stopped")
 | 
			
		||||
        else:
 | 
			
		||||
            if not self._ll_current_var_info:
 | 
			
		||||
                self._log("[LL POLL] Cannot start: no variable selected.")
 | 
			
		||||
            self.ll_selector.btn_start_polling.setText("Start Polling")
 | 
			
		||||
            self._ll_poll_timer.stop()
 | 
			
		||||
            self._ll_polling_variables.clear()
 | 
			
		||||
            self._ll_current_poll_index = -1
 | 
			
		||||
            self._log("[LL Polling] Stopped.")
 | 
			
		||||
        else: # If not polling, start
 | 
			
		||||
            # Get all selected variables from the LowLevelSelectorWidget
 | 
			
		||||
            self._ll_polling_variables = self.ll_selector.get_selected_variables_and_addresses()
 | 
			
		||||
            if not self._ll_polling_variables:
 | 
			
		||||
                self._log("[LL] No variables selected for polling. Aborting.")
 | 
			
		||||
                self.set_status("Error.", "error")
 | 
			
		||||
                return
 | 
			
		||||
            interval = self.spin_ll_interval.value()
 | 
			
		||||
            self._ll_poll_timer.start(interval)
 | 
			
		||||
 | 
			
		||||
            self._ll_polling = True
 | 
			
		||||
            self.btn_ll_poll.setText("Stop Polling")
 | 
			
		||||
            self._log(f"[LL POLL] Started interval={interval}ms")
 | 
			
		||||
        self._set_ui_busy(False) # Обновить доступность кнопок
 | 
			
		||||
            self.ll_selector.btn_start_polling.setText("Stop Polling")
 | 
			
		||||
            self._ll_current_poll_index = 0 # Start from the first variable
 | 
			
		||||
            self._log(f"[LL Polling] Started. Polling {len(self._ll_polling_variables)} variables.")
 | 
			
		||||
            
 | 
			
		||||
            # Start the timer. It will trigger _on_ll_poll_timeout, which starts the cycle.
 | 
			
		||||
            # The first cycle starts immediately, subsequent cycles wait for the interval.
 | 
			
		||||
            self._ll_poll_timer.setInterval(self.ll_selector.spin_interval.value())
 | 
			
		||||
            self._ll_poll_timer.start() # Start the timer for recurrent cycles
 | 
			
		||||
            
 | 
			
		||||
            # Immediately kick off the first variable read of the first cycle
 | 
			
		||||
            self._start_ll_cycle()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def _on_ll_poll_timeout(self):
 | 
			
		||||
        """Слот таймера поллинга для LowLevel."""
 | 
			
		||||
        self.request_lowlevel_once()
 | 
			
		||||
        """Вызывается по таймеру для старта нового цикла."""
 | 
			
		||||
        if self._ll_polling and not self._busy:
 | 
			
		||||
            self._start_ll_cycle()
 | 
			
		||||
        elif self._busy:
 | 
			
		||||
            self._log("[LL Polling] Busy, skip cycle start.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def _start_ll_cycle(self):
 | 
			
		||||
        self._update_interval()
 | 
			
		||||
 | 
			
		||||
        """Запускает новый цикл опроса всех переменных."""
 | 
			
		||||
        if not self._ll_polling or not self._ll_polling_variables:
 | 
			
		||||
            return
 | 
			
		||||
        self._ll_poll_index = 0
 | 
			
		||||
        self._process_next_ll_variable_in_cycle()
 | 
			
		||||
 | 
			
		||||
    def _on_ll_variable_prepared(self, var_info: dict):
 | 
			
		||||
        """Срабатывает при выборе переменной в селекторе."""
 | 
			
		||||
        self._ll_current_var_info = var_info
 | 
			
		||||
        self._log(f"[LL] Selected variable '{var_info['path']}' @ {var_info['address_hex']}")
 | 
			
		||||
        # Сбрасываем старые значения
 | 
			
		||||
        self.ll_val_status.setText("-")
 | 
			
		||||
        self.ll_val_rettype.setText("-")
 | 
			
		||||
        self.ll_val_scaled.setText("-")
 | 
			
		||||
 | 
			
		||||
    def _process_next_ll_variable_in_cycle(self):
 | 
			
		||||
        if not self._ll_polling: # Добавим проверку, чтобы избежать вызова, если LL polling отключен
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if self._ll_poll_index < len(self._ll_polling_variables):
 | 
			
		||||
            var_info = self._ll_polling_variables[self._ll_poll_index]
 | 
			
		||||
            self._on_ll_variable_prepared(var_info)
 | 
			
		||||
            self._ll_poll_index += 1
 | 
			
		||||
            frame = self._build_lowlevel_request(var_info)
 | 
			
		||||
            # --- НОВОЕ: Передаем var_info в метаданные транзакции для LL polling ---
 | 
			
		||||
            meta = {'lowlevel': True, 'll_polling': True, 'll_var_info': var_info} 
 | 
			
		||||
            self.set_status(f"Polling LL: {var_info.get('name')}", "values")
 | 
			
		||||
            self._enqueue_raw(frame, meta)
 | 
			
		||||
        else:
 | 
			
		||||
            # Цикл завершен, перезапускаем таймер для следующего полного цикла
 | 
			
		||||
            self._ll_poll_index = 0
 | 
			
		||||
            self._ll_poll_timer.start(self.ll_selector.spin_interval.value())
 | 
			
		||||
            self.set_status("LL polling cycle done, waiting...", "idle")
 | 
			
		||||
    # ------------------------------ HELPERS --------------------------------
 | 
			
		||||
    def _toggle_index_base(self, st):
 | 
			
		||||
        # ... (код без изменений)
 | 
			
		||||
@ -1066,7 +1124,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
        
 | 
			
		||||
        # LowLevel tab
 | 
			
		||||
        can_use_ll = not busy and not (self._ll_polling or self._polling)
 | 
			
		||||
        self.btn_ll_read.setEnabled(can_use_ll)
 | 
			
		||||
        self.ll_selector.btn_read_once.setEnabled(can_use_ll)
 | 
			
		||||
 | 
			
		||||
    def _on_serial_error(self, err):
 | 
			
		||||
        # ... (код без изменений)
 | 
			
		||||
@ -1075,8 +1133,80 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
        if self._busy: self._end_txn()
 | 
			
		||||
 | 
			
		||||
    # ------------------------------ LOGGING --------------------------------
 | 
			
		||||
    def _select_csv_file(self):
 | 
			
		||||
        """Открывает диалог выбора файла для CSV и обновляет UI."""
 | 
			
		||||
        if self.csv_logger.select_file(self): # Передаем self как parent для диалога
 | 
			
		||||
            self.lbl_csv_filename.setText(self.csv_logger.filename)
 | 
			
		||||
            self._log(f"CSV file set to: {self.csv_logger.filename}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def _start_csv_logging(self):
 | 
			
		||||
        """Начинает запись данных в CSV. Устанавливает заголовки в зависимости от активной вкладки."""
 | 
			
		||||
        if not self.serial.isOpen():
 | 
			
		||||
            self._log("[CSV] Невозможно начать запись: COM порт не открыт.")
 | 
			
		||||
            self.set_status("Port closed", "error")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # Определяем активную вкладку и устанавливаем заголовки
 | 
			
		||||
        current_tab_index = self.tabs.currentIndex()
 | 
			
		||||
        varnames_for_csv = []
 | 
			
		||||
 | 
			
		||||
        if self.tabs.tabText(current_tab_index) == "Watch":
 | 
			
		||||
            # Для вкладки Watch берем имена из кэша, если они есть, иначе используем Index_X
 | 
			
		||||
            base_index = self.spin_index.value()
 | 
			
		||||
            count = self.spin_count.value()
 | 
			
		||||
            for i in range(base_index, base_index + count):
 | 
			
		||||
                if i in self._name_cache and self._name_cache[i][2]: # status, iq_raw, name, is_signed, frac_bits
 | 
			
		||||
                    varnames_for_csv.append(self._name_cache[i][2])
 | 
			
		||||
                else:
 | 
			
		||||
                    varnames_for_csv.append(f"Index_{i}")
 | 
			
		||||
            self._log(f"[CSV] Начинается запись для Watch переменных: {varnames_for_csv}")
 | 
			
		||||
        elif self.tabs.tabText(current_tab_index) == "LowLevel":
 | 
			
		||||
            # Для вкладки LowLevel берем имена из ll_selector
 | 
			
		||||
            selected_vars = self.ll_selector.get_selected_variables_and_addresses()
 | 
			
		||||
            varnames_for_csv = [var['name'] for var in selected_vars if 'name' in var]
 | 
			
		||||
            if not varnames_for_csv:
 | 
			
		||||
                self._log("[CSV] Внимание: На вкладке LowLevel не выбраны переменные для записи.")
 | 
			
		||||
            self._log(f"[CSV] Начинается запись для LowLevel переменных: {varnames_for_csv}")
 | 
			
		||||
        else:
 | 
			
		||||
            self._log("[CSV] Неизвестная активная вкладка. Невозможно определить заголовки CSV.")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if not varnames_for_csv:
 | 
			
		||||
            self._log("[CSV] Нет переменных для записи в CSV. Запись не начата.")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        self.csv_logger.set_titles(varnames_for_csv)
 | 
			
		||||
        self._csv_logging_active = True
 | 
			
		||||
        self.btn_start_csv_logging.setEnabled(False)
 | 
			
		||||
        self.btn_stop_csv_logging.setEnabled(True)
 | 
			
		||||
        self.set_status("CSV Logging ACTIVE", "values")
 | 
			
		||||
        self._log("[CSV] Запись данных в CSV началась.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def _stop_csv_logging(self):
 | 
			
		||||
        """Останавливает запись данных в CSV."""
 | 
			
		||||
        self._csv_logging_active = False
 | 
			
		||||
        self.btn_start_csv_logging.setEnabled(True)
 | 
			
		||||
        self.btn_stop_csv_logging.setEnabled(False)
 | 
			
		||||
        self.set_status("CSV Logging STOPPED", "idle")
 | 
			
		||||
        self._log("[CSV] Запись данных в CSV остановлена.")
 | 
			
		||||
 | 
			
		||||
    def _save_csv_data(self):
 | 
			
		||||
        """Сохраняет все собранные данные в CSV файл."""
 | 
			
		||||
        if self._csv_logging_active:
 | 
			
		||||
            self._log("[CSV] Запись активна. Сначала остановите запись.")
 | 
			
		||||
            self.set_status("Stop logging first", "error")
 | 
			
		||||
            return
 | 
			
		||||
        self.csv_logger.write_to_csv()
 | 
			
		||||
        self.set_status("CSV data saved", "idle")
 | 
			
		||||
        
 | 
			
		||||
    def _log(self, msg: str):
 | 
			
		||||
        # ... (код без изменений)
 | 
			
		||||
        if 'ERR' in msg:
 | 
			
		||||
            self.set_status(msg, 'error')
 | 
			
		||||
        if 'OK' in msg:
 | 
			
		||||
            self.set_status('Idle', 'idle')
 | 
			
		||||
        if not self.log_spoiler.getState():
 | 
			
		||||
            return
 | 
			
		||||
        ts = datetime.datetime.now().strftime('%H:%M:%S.%f')[:-3]
 | 
			
		||||
@ -1091,6 +1221,15 @@ class DebugTerminalWidget(QtWidgets.QWidget):
 | 
			
		||||
        ascii_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data)
 | 
			
		||||
        self._log(f"[{tag}] {hexs} |{ascii_part}|")
 | 
			
		||||
 | 
			
		||||
    def _update_interval(self):        
 | 
			
		||||
        now = time.perf_counter()
 | 
			
		||||
        if self._last_txn_timestamp is not None:
 | 
			
		||||
            delta_ms = (now - self._last_txn_timestamp) * 1000
 | 
			
		||||
            # Обновляем UI только если он уже создан
 | 
			
		||||
            if hasattr(self, 'lbl_actual_interval'):
 | 
			
		||||
                self.lbl_actual_interval.setText(f"{delta_ms:.1f} ms")                
 | 
			
		||||
        self._last_txn_timestamp = now
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# ---------------------------------------------------------- Demo harness ---
 | 
			
		||||
class _DemoWindow(QtWidgets.QMainWindow):
 | 
			
		||||
@ -1099,28 +1238,7 @@ class _DemoWindow(QtWidgets.QMainWindow):
 | 
			
		||||
        self.setWindowTitle("DebugVar Terminal")
 | 
			
		||||
        self.term = DebugTerminalWidget(self)
 | 
			
		||||
        self.setCentralWidget(self.term)
 | 
			
		||||
        self.term.nameRead.connect(self._on_name)
 | 
			
		||||
        self.term.valueRead.connect(self._on_value)
 | 
			
		||||
        self.term.llValueRead.connect(self._on_ll_value)
 | 
			
		||||
 | 
			
		||||
    def _on_name(self, index, status, iq, name):
 | 
			
		||||
        return
 | 
			
		||||
        print(f"Name idx={index} status={status} iq={iq} name='{name}'")
 | 
			
		||||
 | 
			
		||||
    def _on_value(self, index, status, iq, raw16, floatVal):
 | 
			
		||||
        return
 | 
			
		||||
        print(f"Value idx={index} status={status} iq={iq} raw={raw16} val={floatVal}")
 | 
			
		||||
 | 
			
		||||
    def _on_ll_value(self, addr, status, rettype_raw, raw16, scaled):
 | 
			
		||||
        return
 | 
			
		||||
        print(f"LL addr=0x{addr:06X} status={status} type=0x{rettype_raw:02X} raw={raw16} scaled={scaled}")
 | 
			
		||||
 | 
			
		||||
    def format_address(addr_text: str) -> str:
 | 
			
		||||
        try:
 | 
			
		||||
            value = int(addr_text, 16)
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            value = 0
 | 
			
		||||
        return f"0x{value:06X}"
 | 
			
		||||
        self.resize(1000, 600)
 | 
			
		||||
        
 | 
			
		||||
    def closeEvent(self, event):
 | 
			
		||||
        self.setCentralWidget(None)
 | 
			
		||||
@ -1133,5 +1251,4 @@ if __name__ == "__main__":
 | 
			
		||||
    import sys
 | 
			
		||||
    app = QtWidgets.QApplication(sys.argv)
 | 
			
		||||
    win = _DemoWindow(); win.show()
 | 
			
		||||
    win.resize(640, 520)
 | 
			
		||||
    sys.exit(app.exec_())
 | 
			
		||||
 | 
			
		||||
@ -119,11 +119,25 @@ class CtrlScrollComboBox(QComboBox):
 | 
			
		||||
            event.ignore()
 | 
			
		||||
            
 | 
			
		||||
class VariableTableWidget(QTableWidget):
 | 
			
		||||
    def __init__(self, parent=None):
 | 
			
		||||
        super().__init__(0, 8, parent)
 | 
			
		||||
    def __init__(self, parent=None, show_value_instead_of_shortname=0):
 | 
			
		||||
        # Таблица переменных
 | 
			
		||||
        if show_value_instead_of_shortname:
 | 
			
		||||
            super().__init__(0, 8, parent)
 | 
			
		||||
            self.setHorizontalHeaderLabels([
 | 
			
		||||
            '№',             # новый столбец
 | 
			
		||||
                '№',
 | 
			
		||||
                'En',
 | 
			
		||||
                'Name',
 | 
			
		||||
                'Origin Type',
 | 
			
		||||
                'Base Type',
 | 
			
		||||
                'IQ Type',
 | 
			
		||||
                'Return Type',
 | 
			
		||||
                'Value'
 | 
			
		||||
            ])
 | 
			
		||||
            self._show_value = True
 | 
			
		||||
        else:
 | 
			
		||||
            super().__init__(0, 8, parent)
 | 
			
		||||
            self.setHorizontalHeaderLabels([
 | 
			
		||||
                '№',
 | 
			
		||||
                'En',
 | 
			
		||||
                'Name',
 | 
			
		||||
                'Origin Type',
 | 
			
		||||
@ -132,71 +146,62 @@ class VariableTableWidget(QTableWidget):
 | 
			
		||||
                'Return Type',
 | 
			
		||||
                'Short Name'
 | 
			
		||||
            ])
 | 
			
		||||
            self._show_value = False
 | 
			
		||||
        self.setEditTriggers(QAbstractItemView.AllEditTriggers)
 | 
			
		||||
        self.var_list = []
 | 
			
		||||
        # Инициализируем QSettings с именем организации и приложения
 | 
			
		||||
 | 
			
		||||
        # QSettings
 | 
			
		||||
        self.settings = QSettings("SET", "DebugVarEdit_VarTable")
 | 
			
		||||
        # Восстанавливаем сохранённое состояние, если есть
 | 
			
		||||
        shortsize = self.settings.value("shortname_size", True, type=int)
 | 
			
		||||
        self._shortname_size = shortsize
 | 
			
		||||
 | 
			
		||||
        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)]
 | 
			
		||||
        # Задаём базовые iq-типы (без префикса 'iq_')
 | 
			
		||||
        self.iq_types = ['iq_none', 'iq', 'iq10', 'iq15', 'iq19', 'iq24']
 | 
			
		||||
        # Фильтруем типы из type_map.values() исключая те, что содержат 'arr' или 'ptr'
 | 
			
		||||
        type_options = [t for t in dict.fromkeys(type_map.values()) if 'arr' not in t and 'ptr' not in t and
 | 
			
		||||
                        'struct' not in t and 'union' not in t and '64' not in t]
 | 
			
		||||
        # Формируем display_type_options без префикса 'pt_'
 | 
			
		||||
        type_options = [t for t in dict.fromkeys(type_map.values()) if 'arr' not in t and 'ptr' not in t
 | 
			
		||||
                        and 'struct' not in t and 'union' not in t and '64' not in t]
 | 
			
		||||
        self.pt_types = [t.replace('pt_', '') for t in type_options]
 | 
			
		||||
 | 
			
		||||
        self._iq_type_filter = list(self.iq_types)  # Текущий фильтр iq типов (по умолчанию все)
 | 
			
		||||
        self._iq_type_filter = list(self.iq_types)
 | 
			
		||||
        self._pt_type_filter = list(self.pt_types)
 | 
			
		||||
        self._ret_type_filter = list(self.iq_types)
 | 
			
		||||
        header = self.horizontalHeader()
 | 
			
		||||
        # Для остальных колонок — растяжение (Stretch), чтобы они заняли всю оставшуюся ширину
 | 
			
		||||
 | 
			
		||||
        header = self.horizontalHeader()
 | 
			
		||||
        for col in range(self.columnCount()):
 | 
			
		||||
            if col == self.columnCount() - 1:
 | 
			
		||||
                header.setSectionResizeMode(col, QHeaderView.Stretch)
 | 
			
		||||
            else:
 | 
			
		||||
                header.setSectionResizeMode(col, QHeaderView.Interactive)
 | 
			
		||||
 | 
			
		||||
        parent_widget = self.parentWidget()
 | 
			
		||||
        # Сделаем колонки с номерами фиксированной ширины
 | 
			
		||||
        self.setColumnWidth(rows.No, 30)
 | 
			
		||||
        self.setColumnWidth(rows.include, 30)
 | 
			
		||||
        self.setColumnWidth(rows.pt_type, 85)
 | 
			
		||||
        self.setColumnWidth(rows.iq_type, 85)
 | 
			
		||||
        self.setColumnWidth(rows.ret_type, 85)
 | 
			
		||||
 | 
			
		||||
        self.setColumnWidth(rows.name, 300)
 | 
			
		||||
        self.setColumnWidth(rows.type, 100)
 | 
			
		||||
 | 
			
		||||
        self._resizing = False
 | 
			
		||||
        self.horizontalHeader().sectionResized.connect(self.on_section_resized)
 | 
			
		||||
        self.horizontalHeader().sectionClicked.connect(self.on_header_clicked)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def populate(self, vars_list, structs, on_change_callback):
 | 
			
		||||
        self.var_list = vars_list
 | 
			
		||||
        self.setUpdatesEnabled(False)
 | 
			
		||||
        self.blockSignals(True)
 | 
			
		||||
 | 
			
		||||
        # --- ДО: удаляем отображение структур и union-переменных
 | 
			
		||||
        for var in vars_list:
 | 
			
		||||
            pt_type = var.get('pt_type', '')
 | 
			
		||||
            if 'struct' in pt_type or 'union' in pt_type:
 | 
			
		||||
                var['show_var'] = 'false'
 | 
			
		||||
                var['enable'] = 'false'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        filtered_vars = [v for v in vars_list if v.get('show_var', 'false') == 'true']
 | 
			
		||||
        self.setRowCount(len(filtered_vars))
 | 
			
		||||
        self.verticalHeader().setVisible(False)
 | 
			
		||||
        style_with_padding = "padding-left: 5px; padding-right: 5px; font-size: 14pt; font-family: 'Segoe UI';"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        for row, var in enumerate(filtered_vars):
 | 
			
		||||
            # №
 | 
			
		||||
            no_item = QTableWidgetItem(str(row))
 | 
			
		||||
@ -212,25 +217,21 @@ class VariableTableWidget(QTableWidget):
 | 
			
		||||
 | 
			
		||||
            # Name
 | 
			
		||||
            name_edit = QLineEdit(var['name'])
 | 
			
		||||
            if var['type'] in structs:
 | 
			
		||||
                completer = QCompleter(structs[var['type']].keys())
 | 
			
		||||
                completer.setCaseSensitivity(Qt.CaseInsensitive)
 | 
			
		||||
                name_edit.setCompleter(completer)
 | 
			
		||||
            name_edit.textChanged.connect(on_change_callback)
 | 
			
		||||
            name_edit.setStyleSheet(style_with_padding)
 | 
			
		||||
            self.setCellWidget(row, rows.name, name_edit)
 | 
			
		||||
 | 
			
		||||
            # Origin Type (readonly)
 | 
			
		||||
            # Origin Type
 | 
			
		||||
            origin_item = QTableWidgetItem(var.get('type', ''))
 | 
			
		||||
            origin_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
 | 
			
		||||
            origin_item.setToolTip(var.get('type', ''))  # Всплывающая подсказка
 | 
			
		||||
            origin_item.setToolTip(var.get('type', ''))
 | 
			
		||||
            origin_item.setForeground(QBrush(Qt.black))
 | 
			
		||||
            self.setItem(row, rows.type, origin_item)
 | 
			
		||||
 | 
			
		||||
            # pt_type
 | 
			
		||||
            pt_combo = CtrlScrollComboBox()
 | 
			
		||||
            pt_combo.addItems(self.pt_types)
 | 
			
		||||
            value = var['pt_type'].replace('pt_', '')
 | 
			
		||||
            value = var.get('pt_type', 'unknown').replace('pt_', '')
 | 
			
		||||
            if value not in self.pt_types:
 | 
			
		||||
                pt_combo.addItem(value)
 | 
			
		||||
            pt_combo.setCurrentText(value)
 | 
			
		||||
@ -241,7 +242,7 @@ class VariableTableWidget(QTableWidget):
 | 
			
		||||
            # iq_type
 | 
			
		||||
            iq_combo = CtrlScrollComboBox()
 | 
			
		||||
            iq_combo.addItems(self.iq_types)
 | 
			
		||||
            value = var['iq_type'].replace('t_', '')
 | 
			
		||||
            value = var.get('iq_type', 'iq_none').replace('t_', '')
 | 
			
		||||
            if value not in self.iq_types:
 | 
			
		||||
                iq_combo.addItem(value)
 | 
			
		||||
            iq_combo.setCurrentText(value)
 | 
			
		||||
@ -252,7 +253,7 @@ class VariableTableWidget(QTableWidget):
 | 
			
		||||
            # return_type
 | 
			
		||||
            ret_combo = CtrlScrollComboBox()
 | 
			
		||||
            ret_combo.addItems(self.iq_types)
 | 
			
		||||
            value = var['return_type'].replace('t_', '')
 | 
			
		||||
            value = var.get('return_type', 'iq_none').replace('t_', '')
 | 
			
		||||
            if value not in self.iq_types:
 | 
			
		||||
                ret_combo.addItem(value)
 | 
			
		||||
            ret_combo.setCurrentText(value)
 | 
			
		||||
@ -260,13 +261,24 @@ class VariableTableWidget(QTableWidget):
 | 
			
		||||
            ret_combo.setStyleSheet(style_with_padding)
 | 
			
		||||
            self.setCellWidget(row, rows.ret_type, ret_combo)
 | 
			
		||||
 | 
			
		||||
            # short_name
 | 
			
		||||
            # Последний столбец
 | 
			
		||||
            if self._show_value:
 | 
			
		||||
                val = var.get('value', '')
 | 
			
		||||
                if val is None:
 | 
			
		||||
                    val = ''
 | 
			
		||||
                val_edit = QLineEdit(str(val))
 | 
			
		||||
                val_edit.textChanged.connect(on_change_callback)
 | 
			
		||||
                val_edit.setStyleSheet(style_with_padding)
 | 
			
		||||
                self.setCellWidget(row, rows.short_name, val_edit)
 | 
			
		||||
            else:
 | 
			
		||||
                short_name_val = var.get('shortname', var['name'])
 | 
			
		||||
                short_name_edit = QLineEdit(short_name_val)
 | 
			
		||||
                short_name_edit.textChanged.connect(on_change_callback)
 | 
			
		||||
                short_name_edit.setStyleSheet(style_with_padding)
 | 
			
		||||
                self.setCellWidget(row, rows.short_name, short_name_edit)
 | 
			
		||||
 | 
			
		||||
        self.blockSignals(False)
 | 
			
		||||
        self.setUpdatesEnabled(True)
 | 
			
		||||
        self.check()
 | 
			
		||||
 | 
			
		||||
    def check(self):
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user