diff --git a/DebugTools.rar b/DebugTools.rar new file mode 100644 index 0000000..38ae9db Binary files /dev/null and b/DebugTools.rar differ diff --git a/Src/allvars_xml_parser.py b/Src/allvars_xml_parser.py new file mode 100644 index 0000000..5a73000 --- /dev/null +++ b/Src/allvars_xml_parser.py @@ -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 + diff --git a/Src/csv_logger.py b/Src/csv_logger.py new file mode 100644 index 0000000..164bbe4 --- /dev/null +++ b/Src/csv_logger.py @@ -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)) diff --git a/Src/tms_debugvar_lowlevel.py b/Src/tms_debugvar_lowlevel.py index 3a84669..ddb3b43 100644 --- a/Src/tms_debugvar_lowlevel.py +++ b/Src/tms_debugvar_lowlevel.py @@ -1,51 +1,54 @@ """ -LowLevelSelectorWidget (PySide2) --------------------------------- -Виджет для: - * Выбора XML файла с описанием переменных (как в примере пользователя) - * Парсинга всех и их вложенных - * Построения плоского списка путей (имя/подпуть) с расчётом абсолютного адреса (base_address + offset) - * Определения структур с полями даты (year, month, day, hour, minute) - * Выбора переменной и (опционально) переменной даты / ручного ввода даты - * Выбора типов: ptr_type , iq_type, return_type - * Форматирования адреса в виде 0x000000 (6 HEX) - * Генерации словаря/кадра для последующей LowLevel-команды (не отправляет сам) +LowLevelSelectorWidget (refactored) +----------------------------------- +Версия, использующая VariableTableWidget вместо самодельной таблицы selected_vars_table. -Интеграция: - * Подключите сигнал variablePrepared(dict) к функции, формирующей и отправляющей пакет. - * Содержимое dict: - { - 'address': int, - 'address_hex': str, # '0x....' - 'ptr_type': int, # значение enum * - 'iq_type': int, - 'return_type': int, - 'datetime': { - 'year': int, - 'month': int, - 'day': int, - 'hour': int, - 'minute': int, - }, - 'path': str, # полный путь переменной - 'type_string': str, # строка типа из XML - } +Ключевые изменения: + * Вместо QTableWidget с 6 колонками теперь встраивается VariableTableWidget (8 колонок: №, En, Name, Origin Type, Base Type, IQ Type, Return Type, Short Name). + * Логика sync <-> self._all_available_vars перенесена в _on_var_table_changed() и _pull_from_var_table(). + * Поддержка политики хранения типов: + - ptr_type: строковое имя (без префикса `pt_`). + - ptr_type_enum: числовой индекс (см. PT_ENUM_ORDER). + - Для совместимости с VariableTableWidget: поле `pt_type` = 'pt_'. + - IQ / Return: аналогично (`iq_type` / `iq_type_enum`, `return_type` / `return_type_enum`). + * Функции получения выбранных переменных теперь читают данные из VariableTableWidget. + * Убраны неиспользуемые методы, связанные с прежней таблицей (комбо‑боксы и т.п.). + +Как интегрировать: + 1. Поместите этот файл рядом с module VariableTableWidget (см. импорт ниже). Если класс VariableTableWidget находится в том же файле — удалите строку импорта и используйте напрямую. + 2. Убедитесь, что VariablesXML предоставляет методы get_all_vars_data() (list[dict]) и, при наличии, get_struct_map() -> dict[type_name -> dict[field_name -> field_type]]. Если такого метода нет, передаём пустой {} и автодополнение по структурам будет недоступно. + 3. Отметьте переменные в VariableSelectorDialog (как и раньше) — он обновит self._all_available_vars. После закрытия диалога вызывается self._populate_var_table(). + 4. Для чтения выбранных переменных используйте get_selected_variables_and_addresses(); она вернёт список словарей в унифицированном формате. + +Примечание о совместимости: VariableTableWidget работает с ключами `pt_type`, `iq_type`, `return_type` (строки с префиксами). Мы поддерживаем дублирование этих полей с «новыми» полями без префикса и enum‑значениями. -Зависимости: только PySide2 и стандартная библиотека. """ from __future__ import annotations + import sys -import xml.etree.ElementTree as ET +import re +import datetime from dataclasses import dataclass, field -from typing import List, Dict, Optional, Tuple -from PySide2 import QtCore, QtGui, QtWidgets +from typing import List, Dict, Optional, Tuple, Any + +from PySide2 import QtCore, QtGui +from PySide2.QtWidgets import ( + QWidget, QVBoxLayout, QPushButton, QLabel, QHBoxLayout, QFileDialog, QMessageBox, + QMainWindow, QApplication, QSizePolicy, QSpinBox, QGroupBox, QSplitter, QFormLayout +) + +# Локальные импорты from path_hints import PathHints from generate_debug_vars import choose_type_map, type_map +from var_selector_window import VariableSelectorDialog +from allvars_xml_parser import VariablesXML + +# Импортируем готовую таблицу +# ЗАМЕТКА: замените на реальное имя файла/модуля, если отличается. +from var_table import VariableTableWidget, rows as VT_ROWS # noqa: F401 # ------------------------------------------------------------ Enumerations -- -# Сопоставление строк из XML типу ptr_type (адаптируйте под реальный проект) -PTR_TYPE_MAP = type_map - +# Порядок фиксируем на основании предыдущей версии. При необходимости расширьте. PT_ENUM_ORDER = [ 'unknown','int8','int16','int32','int64', 'uint8','uint16','uint32','uint64','float', @@ -59,308 +62,12 @@ IQ_ENUM_ORDER = [ 'iq23','iq24','iq25','iq26','iq27','iq28','iq29','iq30' ] -# Для примера: маппинг имени enum -> числовое значение (индекс по порядку) -PT_ENUM_VALUE = {name: idx for idx, name in enumerate(PT_ENUM_ORDER)} -IQ_ENUM_VALUE = {name: idx for idx, name in enumerate(IQ_ENUM_ORDER)} - -# -------------------------------------------------------------- Data types -- -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) +PT_ENUM_VALUE: Dict[str, int] = {name: idx for idx, name in enumerate(PT_ENUM_ORDER)} +IQ_ENUM_VALUE: Dict[str, int] = {name: idx for idx, name in enumerate(IQ_ENUM_ORDER)} +PT_ENUM_NAME_FROM_VAL: Dict[int, str] = {v: k for k, v in PT_ENUM_VALUE.items()} +IQ_ENUM_NAME_FROM_VAL: Dict[int, str] = {v: k for k, v in IQ_ENUM_VALUE.items()} -@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: - """ - Читает твой XML и выдаёт плоский список путей: - - Массивы -> name[i], многоуровневые -> name[i][j] - - Указатель на структуру -> дети через '->' - - Обычная структура -> дети через '.' - """ - # предположительные размеры примитивов (под 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): - 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) - ) - - # ------------------ flatten (expanded) ------------------ - - def flattened(self, - max_array_elems: Optional[int] = None - ) -> List[Tuple[str,int,str]]: - """ - Вернёт [(path, addr, type_str), ...]. - max_array_elems: ограничить разворачивание больших массивов (None = все). - """ - out: List[Tuple[str,int,str]] = [] - - def add(path: str, addr: int, t: str): - out.append((path, addr, t)) - - def compute_stride(size_bytes: Optional[int], - count: Optional[int], - base_type: Optional[str], - node_children: Optional[List[MemberNode]]) -> int: - # 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 не кратен count → скорее всего size = размер одного элемента - return max(size_bytes, 1) - - # 2) попытка по типу (примитив) - if base_type: - gs = self._guess_primitive_size(base_type) - if gs: - return gs - - # 3) попытка по детям (структура) - if node_children: - 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): - """ - Разворачиваем список members относительно базового адреса. - parent_is_ptr_struct: если True, то соединение '->' иначе '.' - """ - 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 - add(path_m, addr_m, m.type_str) - - # массив? - 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 - add(path_i, addr_i, base_t) - # элемент массива: если структура / union → раскроем поля - if m.children and self._is_struct_or_union(base_t): - expand_members(path_i, addr_i, m.children, parent_is_ptr_struct=False) - # элемент массива: если указатель на структуру - elif self._is_pointer_to_struct(base_t): - # у таких обычно нет children в XML, но если есть — используем - expand_members(path_i, addr_i, m.children, parent_is_ptr_struct=True) - continue - - # не массив - 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 --- - for v in self.variables: - add(v.name, v.address, v.type_str) - - # top-level массив? - 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 - add(p, a, base_t) - # массив структур? - if v.members and self._is_struct_or_union(base_t): - expand_members(p, a, v.members, parent_is_ptr_struct=False) - # массив указателей на структуры? - elif self._is_pointer_to_struct(base_t): - expand_members(p, a, v.members, parent_is_ptr_struct=True) - continue # к след. переменной - - # top-level не массив - 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 (как было) -------------------- - - def date_struct_candidates(self) -> List[Tuple[str,int]]: - cands = [] - for v in self.variables: - # верхний уровень (если есть все поля даты) - direct_names = {mm.name for mm in v.members} - if DATE_FIELD_SET.issubset(direct_names): - cands.append((v.name, v.address)) - # проверка членов первого уровня - for m in v.members: - if m.is_date_struct(): - cands.append((f"{v.name}.{m.name}", v.address + m.offset)) - return cands - - # ------------------------------------------- Address / validation helpers -- HEX_ADDR_MASK = QtCore.QRegExp(r"0x[0-9A-Fa-f]{0,6}") class HexAddrValidator(QtGui.QRegExpValidator): @@ -377,396 +84,403 @@ class HexAddrValidator(QtGui.QRegExpValidator): return '0x000000' return f"0x{val & 0xFFFFFF:06X}" -# --------------------------------------------------------- Main Widget ---- -class LowLevelSelectorWidget(QtWidgets.QWidget): + +class LowLevelSelectorWidget(QWidget): variablePrepared = QtCore.Signal(dict) xmlLoaded = QtCore.Signal(str) - def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle('LowLevel Variable Selector') self._xml: Optional[VariablesXML] = None - self._paths = [] - self._path_info = {} - self._addr_index = {} - self._backspace_pressed = False + self._paths: List[str] = [] + self._path_info: Dict[str, Tuple[int, str]] = {} + self._addr_index: Dict[int, Optional[str]] = {} self._hints = PathHints() + self._all_available_vars: List[Dict[str, Any]] = [] + self.dt = None + self.flat_vars = None + + # --- NEW --- + self.btn_read_once = QPushButton("Read Once") + self.btn_start_polling = QPushButton("Start Polling") + self.spin_interval = QSpinBox() + self.spin_interval.setRange(50, 10000) + self.spin_interval.setValue(500) + self.spin_interval.setSuffix(" ms") + self._build_ui() self._connect() - def _build_ui(self): - lay = QtWidgets.QVBoxLayout(self) + tab = QWidget() + main_layout = QVBoxLayout(tab) - # --- File chooser --- - file_box = QtWidgets.QHBoxLayout() - self.btn_load = QtWidgets.QPushButton('Load XML...') - self.lbl_file = QtWidgets.QLabel('') + # --- Variable Selector --- + g_selector = QGroupBox("Variable Selector") + selector_layout = QVBoxLayout(g_selector) + + form_selector = QFormLayout() + + # --- XML File chooser --- + file_layout = QHBoxLayout() + self.btn_load = QPushButton('Load XML...') + self.lbl_file = QLabel('') self.lbl_file.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) - file_box.addWidget(self.btn_load) - file_box.addWidget(self.lbl_file, 1) - lay.addLayout(file_box) + file_layout.addWidget(self.btn_load) + file_layout.addWidget(self.lbl_file, 1) + form_selector.addRow("XML File:", file_layout) - self.lbl_timestamp = QtWidgets.QLabel('Timestamp: -') - lay.addWidget(self.lbl_timestamp) + # --- Interval SpinBox --- + self.spin_interval = QSpinBox() + self.spin_interval.setRange(50, 10000) + self.spin_interval.setValue(500) + self.spin_interval.setSuffix(" ms") + form_selector.addRow("Interval:", self.spin_interval) - form = QtWidgets.QFormLayout() + selector_layout.addLayout(form_selector) - # --- Search field for variable --- - self.edit_var_search = QtWidgets.QLineEdit() - self.edit_var_search.setPlaceholderText("Введите имя переменной или адрес 0x......") - form.addRow('Variable:', self.edit_var_search) + # --- Buttons --- + self.btn_read_once = QPushButton("Read Once") + self.btn_start_polling = QPushButton("Start Polling") + btn_layout = QHBoxLayout() + btn_layout.addWidget(self.btn_read_once) + btn_layout.addWidget(self.btn_start_polling) + selector_layout.addLayout(btn_layout) - # Popup list - self._popup = QtWidgets.QListView() - self._popup.setWindowFlags(QtCore.Qt.ToolTip) - self._popup.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) - self._popup.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self._popup.clicked.connect(self._on_popup_clicked) - self._model_all = QtGui.QStandardItemModel(self) - self._model_filtered = QtGui.QStandardItemModel(self) + # --- Table --- + g_table = QGroupBox("Table") + table_layout = QVBoxLayout(g_table) + self.btn_open_var_selector = QPushButton("Выбрать переменные...") + table_layout.addWidget(self.btn_open_var_selector) + self.var_table = VariableTableWidget(self, show_value_instead_of_shortname=1) + self.var_table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + table_layout.addWidget(self.var_table) - # Address - self.edit_address = QtWidgets.QLineEdit('0x000000') - self.edit_address.setValidator(HexAddrValidator(self)) - self.edit_address.setMaximumWidth(120) - form.addRow('Address:', self.edit_address) + # --- Timestamp (moved here) --- + self.lbl_timestamp = QLabel('Timestamp: -') + table_layout.addWidget(self.lbl_timestamp) - # Manual date spins - dt_row = QtWidgets.QHBoxLayout() - self.spin_year = QtWidgets.QSpinBox(); self.spin_year.setRange(2000, 2100); self.spin_year.setValue(2025) - self.spin_month = QtWidgets.QSpinBox(); self.spin_month.setRange(1,12) - self.spin_day = QtWidgets.QSpinBox(); self.spin_day.setRange(1,31) - self.spin_hour = QtWidgets.QSpinBox(); self.spin_hour.setRange(0,23) - self.spin_minute = QtWidgets.QSpinBox(); self.spin_minute.setRange(0,59) - for w,label in [(self.spin_year,'Y'),(self.spin_month,'M'),(self.spin_day,'D'),(self.spin_hour,'h'),(self.spin_minute,'m')]: - box = QtWidgets.QVBoxLayout() - box.addWidget(QtWidgets.QLabel(label, alignment=QtCore.Qt.AlignHCenter)) - box.addWidget(w) - dt_row.addLayout(box) - form.addRow('Compile Date:', dt_row) + # --- Splitter (Selector + Table) --- + v_split = QSplitter(QtCore.Qt.Vertical) + v_split.addWidget(g_selector) + v_split.addWidget(g_table) + v_split.setStretchFactor(0, 1) + v_split.setStretchFactor(1, 3) - # Types - self.cmb_ptr_type = QtWidgets.QComboBox(); self.cmb_ptr_type.addItems(PT_ENUM_ORDER) - self.cmb_iq_type = QtWidgets.QComboBox(); self.cmb_iq_type.addItems(IQ_ENUM_ORDER) - self.cmb_return_type = QtWidgets.QComboBox(); self.cmb_return_type.addItems(IQ_ENUM_ORDER) - form.addRow('Type:', self.cmb_ptr_type) - form.addRow('IQ Type:', self.cmb_iq_type) - form.addRow('Return IQ Type:', self.cmb_return_type) + main_layout.addWidget(v_split) + self.setLayout(main_layout) - lay.addLayout(form) - self.btn_prepare = QtWidgets.QPushButton('Prepare Variable') - lay.addWidget(self.btn_prepare) - lay.addStretch(1) - - self.txt_info = QtWidgets.QPlainTextEdit() - self.txt_info.setReadOnly(True) - self.txt_info.setMaximumHeight(140) - lay.addWidget(QtWidgets.QLabel('Info:')) - lay.addWidget(self.txt_info) - - # Event filter for keyboard on search field - self.edit_var_search.installEventFilter(self) def _connect(self): self.btn_load.clicked.connect(self._on_load_xml) - self.edit_address.editingFinished.connect(self._normalize_address) - self.btn_prepare.clicked.connect(self._emit_variable) - self.edit_var_search.textEdited.connect(self._on_var_search_edited) - self.edit_var_search.returnPressed.connect(self._activate_current_popup_selection) - QtWidgets.QApplication.instance().focusChanged.connect(self._on_focus_changed) - - def focusOutEvent(self, event): - super().focusOutEvent(event) - self._hide_popup() + self.btn_open_var_selector.clicked.connect(self._on_open_variable_selector) - def _on_focus_changed(self, old, now): - # Если фокус ушёл из нашего виджета — скрываем подсказки - if now is None or not self.isAncestorOf(now): - self._hide_popup() - # ---------------- XML Load ---------------- + # ------------------------------------------------------ XML loading ---- def _on_load_xml(self): - path, _ = QtWidgets.QFileDialog.getOpenFileName( + path, _ = QFileDialog.getOpenFileName( self, 'Select variables XML', '', 'XML Files (*.xml);;All Files (*)') if not path: return try: self._xml = VariablesXML(path) + self.flat_vars = {v['name']: v for v in self._xml.flattened()} + # Получаем сырые данные по переменным + self._all_available_vars = self._xml.get_all_vars_data() except Exception as e: - QtWidgets.QMessageBox.critical(self, 'Parse error', f'Ошибка парсинга:\n{e}') + QMessageBox.critical(self, 'Parse error', f'Ошибка парсинга:\n{e}') return + self.lbl_file.setText(path) self.lbl_timestamp.setText(f'Timestamp: {self._xml.timestamp or "-"}') - self._populate_variables() + + self._populate_internal_maps_from_all_vars() self._apply_timestamp_to_date() self.xmlLoaded.emit(path) - self._log(f'Loaded {path}, variables={len(self._xml.variables)}') + self._log(f'Loaded {path}, variables={len(self._all_available_vars)})') + def _apply_timestamp_to_date(self): - if not self._xml.timestamp: + if not (self._xml and self._xml.timestamp): return - import datetime try: # Пример: "Sat Jul 19 15:27:59 2025" - dt = datetime.datetime.strptime(self._xml.timestamp, "%a %b %d %H:%M:%S %Y") - self.spin_year.setValue(dt.year) - self.spin_month.setValue(dt.month) - self.spin_day.setValue(dt.day) - self.spin_hour.setValue(dt.hour) - self.spin_minute.setValue(dt.minute) + self.dt = datetime.datetime.strptime(self._xml.timestamp, "%a %b %d %H:%M:%S %Y") except Exception as e: print(f"Ошибка разбора timestamp '{self._xml.timestamp}': {e}") - def _populate_variables(self): + # ------------------------------------------ Variable selector dialog ---- + def _on_open_variable_selector(self): if not self._xml: + QMessageBox.warning(self, 'No XML', 'Сначала загрузите XML файл.') return - flat = self._xml.flattened() - # flat: [(path, addr, type_str), ...] - self._paths = [] - self._path_info = {} - self._addr_index = {} - self._model_all.clear() # держим «сырой» полный список (можно не показывать) - self._model_filtered.clear() # текущие подсказки + dialog = VariableSelectorDialog( + table=None, # не используем встроенную таблицу + all_vars=self._all_available_vars, + structs=None, # при необходимости подайте реальные структуры из XML + typedefs=None, # ... + xml_path=None, # по запросу пользователя xml_path = None + parent=self + ) + if dialog.exec_() == dialog.Accepted: + # Диалог обновил self._all_available_vars напрямую + self._populate_internal_maps_from_all_vars() + self._populate_var_table() + self._log("Variable selection updated.") - # индексирование - for path, addr, t in flat: - self._paths.append(path) - self._path_info[path] = (addr, t) + # ----------------------------------------------------- Populate table ---- + def _populate_var_table(self): + """Отобразить переменные (show_var == 'true') в VariableTableWidget.""" + if not self._all_available_vars: + self.var_table.setRowCount(0) + return + + # Нормализуем все записи перед передачей таблице. + for var in self._all_available_vars: + self._normalize_var_record(var) + + # Карта структур для автодополнения (если VariablesXML предоставляет) + try: + structs_map = self._xml.get_struct_map() if self._xml else {} + except AttributeError: + structs_map = {} + + # populate() принимает: (vars_list, structs, on_change_callback) + self.var_table.populate(self._all_available_vars, structs_map, self._on_var_table_changed) + + # -------------------------------------------------- Table change slot ---- + def _on_var_table_changed(self, *args, **kwargs): # noqa: D401 (неиспользуемые) + """Вызывается при любом изменении в VariableTableWidget. + + Читаем данные из таблицы, мержим в self._all_available_vars (по имени), + пересобираем служебные индексы. + """ + updated = self.var_table.read_data() # list[dict] + + # создаём индекс по имени из master списка + idx_by_name = {v.get('name'): v for v in self._all_available_vars if v.get('name')} + + for rec in updated: + nm = rec.get('name') + if not nm: + continue + dst = idx_by_name.get(nm) + if not dst: + # Новая запись; добавляем базовые поля + dst = { + 'name': nm, + 'address': 0, + 'file': '', 'extern': 'false', 'static': 'false', + } + self._all_available_vars.append(dst) + idx_by_name[nm] = dst + + # перенести видимые поля + dst['show_var'] = str(bool(rec.get('show_var'))).lower() + dst['enable'] = str(bool(rec.get('enable'))).lower() + dst['shortname']= rec.get('shortname', nm) + dst['type'] = rec.get('type', dst.get('type','')) + + # типы (строковые, с префиксами) -> нормализуем + pt_pref = rec.get('pt_type','pt_unknown') # 'pt_int16' + iq_pref = rec.get('iq_type','t_iq_none') # 't_iq10' etc. + rt_pref = rec.get('return_type', iq_pref) + + self._assign_types_from_prefixed(dst, pt_pref, iq_pref, rt_pref) + + # Пересобрать карты путей/адресов + self._populate_internal_maps_from_all_vars() + + # --------------------------------- Normalize var record (public-ish) ---- + def _normalize_var_record(self, var: Dict[str, Any]): + """Унифицирует записи переменной. + + Требуемые поля после нормализации: + var['ptr_type'] -> str (напр. 'int16') + var['ptr_type_enum'] -> int + var['iq_type'] -> str ('iq10') + var['iq_type_enum'] -> int + var['return_type'] -> str ('iq10') + var['return_type_enum']-> int + var['pt_type'] -> 'pt_' (для совместимости с VariableTableWidget) + var['return_type_pref']-> 't_' (см. ниже) # не обяз. + + Дополнительно корректируем show_var/enable и адрес. + """ + # --- show_var / enable + var['show_var'] = str(var.get('show_var', 'false')).lower() + var['enable'] = str(var.get('enable', 'true')).lower() + + # --- address + if not var.get('address'): + var_name = var.get('name') + # Ищем в self.flat_vars + if hasattr(self, 'flat_vars') and isinstance(self.flat_vars, dict): + flat_entry = self.flat_vars.get(var_name) + if flat_entry and 'address' in flat_entry: + var['address'] = flat_entry['address'] + else: + var['address'] = 0 + else: + var['address'] = 0 + else: + # Нормализация адреса (если строка типа '0x1234') + try: + if isinstance(var['address'], str): + var['address'] = int(var['address'], 16) + except ValueError: + var['address'] = 0 + + + # --- ptr_type (строка) + name = None + if isinstance(var.get('ptr_type'), str): + name = var['ptr_type'] + elif isinstance(var.get('ptr_type_name'), str): + name = var['ptr_type_name'] + elif isinstance(var.get('pt_type'), str): + name = var['pt_type'].replace('pt_','') + elif isinstance(var.get('ptr_type'), int): + name = PT_ENUM_NAME_FROM_VAL.get(var['ptr_type'], 'unknown') + else: + name = self._map_type_to_ptr_enum(var.get('type')) + val = PT_ENUM_VALUE.get(name, 0) + var['ptr_type'] = name + var['ptr_type_enum'] = val + var['pt_type'] = f'pt_{name}' + + # ---------------------------------------------- prefixed assign helper ---- + def _assign_types_from_prefixed(self, dst: Dict[str, Any], pt_pref: str, iq_pref: str, rt_pref: str): + """Парсит строки вида 'pt_int16', 't_iq10' и записывает нормализованные поля.""" + pt_name = pt_pref.replace('pt_','') if pt_pref else 'unknown' + iq_name = iq_pref + if iq_name.startswith('t_'): + iq_name = iq_name[2:] + rt_name = rt_pref + if rt_name.startswith('t_'): + rt_name = rt_name[2:] + + dst['ptr_type'] = pt_name + dst['ptr_type_enum'] = PT_ENUM_VALUE.get(pt_name, 0) + dst['pt_type'] = f'pt_{pt_name}' + + dst['iq_type'] = iq_name + dst['iq_type_enum'] = IQ_ENUM_VALUE.get(iq_name, 0) + + dst['return_type'] = rt_name + dst['return_type_enum'] = IQ_ENUM_VALUE.get(rt_name, dst['iq_type_enum']) + dst['return_type_pref'] = f't_{rt_name}' + + # ------------------------------------------ Populate internal maps ---- + def _populate_internal_maps_from_all_vars(self): + self._path_info.clear() + self._addr_index.clear() + self._paths.clear() + + for var in self._all_available_vars: + nm = var.get('name') + tp = var.get('type') + addr = var.get('address') + if nm is None: + continue + if addr is None: + addr = 0 + var['address'] = 0 + self._paths.append(nm) + self._path_info[nm] = (addr, tp) if addr in self._addr_index: self._addr_index[addr] = None else: - self._addr_index[addr] = path + self._addr_index[addr] = nm - # наполняем «all» модель (необязательная, но пусть остаётся — не используем напрямую) - it = QtGui.QStandardItem(f"{path} [{addr:06X}]") - it.setData(path, QtCore.Qt.UserRole+1) - it.setData(addr, QtCore.Qt.UserRole+2) - it.setData(t, QtCore.Qt.UserRole+3) - self._model_all.appendRow(it) - - # построить подсказки + # Обновим подсказки self._hints.set_paths([(p, self._path_info[p][1]) for p in self._paths]) - # начальное состояние попапа (пустой ввод → top-level) - self._update_popup_model(self._hints.suggest('')) + # -------------------------------------------------- Public helpers ---- + def get_selected_variables_and_addresses(self) -> List[Dict[str, Any]]: + """Возвращает список выбранных переменных (show_var == true) с адресами и типами. - self._log(f"Variables loaded: {len(flat)}") + Чтение из VariableTableWidget + подстановка адресов/прочих служебных полей + из master списка. + """ + tbl_data = self.var_table.read_data() # список dict'ов в формате VariableTableWidget + idx_by_name = {v.get('name'): v for v in self._all_available_vars if v.get('name')} - # --------------- Search mechanics --------------- - def _update_popup_model(self, paths: List[str]): - """Обновляет модель попапа списком путей (full paths).""" - self._model_filtered.clear() - limit = 400 - added = 0 - for p in paths: - info = self._path_info.get(str(p)) - if not info: + out: List[Dict[str, Any]] = [] + for rec in tbl_data: + nm = rec.get('name') + if not nm: continue - addr, t = info - it = QtGui.QStandardItem(f"{p} [{addr:06X}]") - it.setData(p, QtCore.Qt.UserRole+1) - it.setData(addr, QtCore.Qt.UserRole+2) - it.setData(t, QtCore.Qt.UserRole+3) - self._model_filtered.appendRow(it) - added += 1 - if added >= limit: - break - if added >= limit: - extra = QtGui.QStandardItem("... (more results truncated)") - extra.setEnabled(False) - self._model_filtered.appendRow(extra) - - def _show_popup(self): - if self._model_filtered.rowCount() == 0: - self._popup.hide() - return - self._popup.setModel(self._model_filtered) - self._popup.setMinimumWidth(self.edit_var_search.width()) - pos = self.edit_var_search.mapToGlobal(QtCore.QPoint(0, self.edit_var_search.height())) - self._popup.move(pos) - self._popup.show() - self._popup.raise_() - self._popup.setFocus() - self._popup.setCurrentIndex(self._model_filtered.index(0,0)) - - def _hide_popup(self): - self._popup.hide() - - def _on_var_search_edited(self, text: str): - # Показываем подсказки при вводе текста, если не было Backspace (чтобы не добавлять разделитель) - t = text.strip() - if self._backspace_pressed: - # При стирании не показываем автодополнение с разделителем - self._backspace_pressed = False - self._hide_popup() - return - - # адрес? - if t.startswith("0x") and len(t) >= 3: - try: - addr = int(t, 16) - path = self._addr_index.get(addr) - if path: - self._set_current_variable(path, from_address=True) - self._hide_popup() - return - except ValueError: - pass - - # подсказки по имени - suggestions = self._hints.suggest(t) - self._update_popup_model(suggestions) - self._show_popup() - - - def _on_popup_clicked(self, idx: QtCore.QModelIndex): - if not idx.isValid(): - return - path = idx.data(QtCore.Qt.UserRole + 1) - if not path: - return - - # Реализуем автодополнение по цепочке, пока подсказка имеет детей - current_path = path - - children = self._hints.get_children(str(current_path)) - if children: - current_path = self._hints.add_separator(current_path) - - self._set_current_variable(str(current_path)) # <-- здесь - - if not children: - self._hide_popup() - - - def _activate_current_popup_selection(self): - if self._popup.isVisible(): - idx = self._popup.currentIndex() - if idx.isValid(): - self._on_popup_clicked(idx) - return - # Попытка прямого совпадения - path = self.edit_var_search.text().strip() - if path in self._path_info: - self._set_current_variable(path) - - def eventFilter(self, obj, ev): - if obj is self.edit_var_search and ev.type() == QtCore.QEvent.KeyPress: - key = ev.key() - if key in (QtCore.Qt.Key_Down, QtCore.Qt.Key_Up): - if not self._popup.isVisible(): - self._show_popup() - else: - step = 1 if key == QtCore.Qt.Key_Down else -1 - cur = self._popup.currentIndex() - row = cur.row() + step - if row < 0: row = 0 - if row >= self._model_filtered.rowCount(): - row = self._model_filtered.rowCount() - 1 - self._popup.setCurrentIndex(self._model_filtered.index(row, 0)) - return True - - elif key == QtCore.Qt.Key_Escape: - self._hide_popup() - return True - - elif key == QtCore.Qt.Key_Backspace: - # Помечаем, что была нажата Backspace - self._backspace_pressed = True - return False # дальше обрабатываем обычным образом - - elif key == QtCore.Qt.Key_Space and (ev.modifiers() & QtCore.Qt.ControlModifier): - # Ctrl+Space — показать подсказки - text = self.edit_var_search.text() - suggestions = self._hints.suggest(text) - self._update_popup_model(suggestions) - self._show_popup() - return True - + src = idx_by_name.get(nm, {}) + addr = src.get('address') + if addr is None or addr == '' or addr == 0: + src['address'] = self.flat_vars.get(nm, {}).get('address', 0) else: - # Любая другая клавиша — сбрасываем Backspace-флаг - self._backspace_pressed = False + # если это строка "0x..." — конвертируем в int + if isinstance(addr, str) and addr.startswith('0x'): + try: + src['address'] = int(addr, 16) + except ValueError: + src['address'] = self.flat_vars.get(nm, {}).get('address', 0) + type_str = src.get('type', rec.get('type','N/A')) - return super().eventFilter(obj, ev) + # нормализация типов + tmp = dict(src) # copy src to preserve extra fields (file, extern, ...) + self._assign_types_from_prefixed(tmp, + rec.get('pt_type','pt_unknown'), + rec.get('iq_type','t_iq_none'), + rec.get('return_type', rec.get('iq_type','t_iq_none'))) + tmp['show_var'] = str(bool(rec.get('show_var'))).lower() + tmp['enable'] = str(bool(rec.get('enable'))).lower() + tmp['name'] = nm + tmp['address'] = addr + tmp['type'] = type_str + out.append(tmp) + return out + def get_datetime(self): + return self.dt + + def set_variable_value(self, var_name: str, value: Any): + # 1. Обновляем master-список переменных + found = None + for var in self._all_available_vars: + if var.get('name') == var_name: + var['value'] = value + found = var + break - def _set_current_variable(self, path: str, from_address=False): - self.edit_var_search.setText(path) - self._update_popup_model(self._hints.suggest(path)) - self._show_popup() - if path not in self._path_info: - return - addr, type_str = self._path_info[path] + if not found: + # Если переменной нет в списке, можно либо проигнорировать, либо добавить. + return False - # Разделитель добавляем только если не стираем (Backspace), и если уже не добавлен - # В этой функции не будем добавлять разделитель, добавляем его только при автодополнении выше - self.edit_address.setText(f"0x{addr:06X}") - ptr_enum_name = self._map_type_to_ptr_enum(type_str) - self._select_combo_text(self.cmb_ptr_type, ptr_enum_name) - source = "ADDR" if from_address else "SEARCH" - self._log(f"[{source}] Selected {path} @0x{addr:06X} type={type_str} -> ptr={ptr_enum_name}") + # 2. Обновляем отображение в таблице + self.var_table.populate(self._all_available_vars, {}, self._on_var_table_changed) + return True - # --------------- Date struct / address / helpers --------------- + # --------------- Address mapping / type mapping helpers --------------- - def _normalize_address(self): - self.edit_address.setText(HexAddrValidator.normalize(self.edit_address.text())) - - def _map_type_to_ptr_enum(self, type_str: str) -> str: + + def _map_type_to_ptr_enum(self, type_str: Optional[str]) -> str: if not type_str: return 'unknown' low = type_str.lower() token = low.replace('*',' ').replace('[',' ') - return PTR_TYPE_MAP.get(token, 'unknown') - - def _select_combo_text(self, combo: QtWidgets.QComboBox, text: str): - ix = combo.findText(text.replace('pt_', '')) - if ix >= 0: - combo.setCurrentIndex(ix) - - def _collect_datetime(self) -> Dict[str,int]: - return { - 'year': self.spin_year.value(), - 'month': self.spin_month.value(), - 'day': self.spin_day.value(), - 'hour': self.spin_hour.value(), - 'minute': self.spin_minute.value(), - } - - def _emit_variable(self): - if not self._path_info: - QtWidgets.QMessageBox.warning(self, 'No XML', 'Сначала загрузите XML файл.') - return - path = self.edit_var_search.text().strip() - if path not in self._path_info: - QtWidgets.QMessageBox.warning(self, 'Variable', 'Переменная не выбрана / не найдена.') - return - addr, type_str = self._path_info[path] - ptr_type_name = self.cmb_ptr_type.currentText() - iq_type_name = self.cmb_iq_type.currentText() - ret_type_name = self.cmb_return_type.currentText() - out = { - 'address': addr, - 'address_hex': f"0x{addr:06X}", - 'ptr_type': PT_ENUM_VALUE.get(ptr_type_name, 0), - 'iq_type': IQ_ENUM_VALUE.get(iq_type_name, 0), - 'return_type': IQ_ENUM_VALUE.get(ret_type_name, 0), - 'datetime': self._collect_datetime(), - 'path': path, - 'type_string': type_str, - 'ptr_type_name': ptr_type_name, - 'iq_type_name': iq_type_name, - 'return_type_name': ret_type_name, - } - self._log(f"Prepared variable: {out}") - self.variablePrepared.emit(out) + return type_map.get(token, 'unknown').replace('pt_','') + # ----------------------------------------------------------- Logging -- def _log(self, msg: str): - self.txt_info.appendPlainText(msg) + print(f"[LowLevelSelectorWidget Log] {msg}") + + +# --------------------------------------------------------------------------- +# Тест‑прогоночка (ручной) -------------------------------------------------- +# Запускать только вручную: python LowLevelSelectorWidget_refactored.py +# --------------------------------------------------------------------------- # ----------------------------------------------------------- Demo window -- -class _DemoWindow(QtWidgets.QMainWindow): +class _DemoWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle('LowLevel Selector Demo') @@ -783,7 +497,7 @@ class _DemoWindow(QtWidgets.QMainWindow): # ----------------------------------------------------------------- main --- if __name__ == '__main__': - app = QtWidgets.QApplication(sys.argv) + app = QApplication(sys.argv) w = _DemoWindow() w.resize(640, 520) w.show() diff --git a/Src/tms_debugvar_term.py b/Src/tms_debugvar_term.py index c054a36..3b5c495 100644 --- a/Src/tms_debugvar_term.py +++ b/Src/tms_debugvar_term.py @@ -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): @@ -429,13 +418,19 @@ class DebugTerminalWidget(QtWidgets.QWidget): self.btn_read_values.clicked.connect(self.request_values) self.btn_poll.clicked.connect(self._toggle_polling) self.chk_hex_index.stateChanged.connect(self._toggle_index_base) - + # LowLevel (новые и переделанные) self.ll_selector.variablePrepared.connect(self._on_ll_variable_prepared) self.ll_selector.xmlLoaded.connect(lambda p: self._log(f"[LL] XML loaded: {p}")) - self.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 = { "idle": "gray", @@ -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") @@ -642,15 +641,9 @@ class DebugTerminalWidget(QtWidgets.QWidget): return 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 - + def _start_txn(self, frame: bytes, meta: dict): + 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 - QtCore.QTimer.singleShot(0, lambda f=frame,m=meta: self._start_txn(f,m)) + 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(): @@ -873,7 +887,9 @@ class DebugTerminalWidget(QtWidgets.QWidget): else: 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"") - 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,49 +1021,89 @@ 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) # Обновить доступность кнопок - def _on_poll_timeout(self): + def _on_poll_timeout(self): 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,29 +1238,8 @@ 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) if self.term: @@ -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_()) diff --git a/Src/var_table.py b/Src/var_table.py index e427bfb..f8fb2c5 100644 --- a/Src/var_table.py +++ b/Src/var_table.py @@ -119,84 +119,89 @@ 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): # Таблица переменных - self.setHorizontalHeaderLabels([ - '№', # новый столбец - 'En', - 'Name', - 'Origin Type', - 'Base Type', - 'IQ Type', - 'Return Type', - 'Short Name' - ]) + 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', + 'Base Type', + 'IQ Type', + '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 - 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) - + # Последний столбец + 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):