2 Commits

Author SHA1 Message Date
Razvalyaev
788ad19464 кууууча всего по терминалке, надо резгребать и структурировать
базово:
+сделан lowlevel для кучи переменных (пока работает медленно)
+сделан сохранение принимаемых значений в лог
+ gui терминалок подогнаны под один стиль плюс минус
2025-07-22 18:05:12 +03:00
Razvalyaev
96496a0256 все неплохо работает.
сейв перед попыткой улучшить lowlevel debug
2025-07-21 13:40:52 +03:00
10 changed files with 1629 additions and 887 deletions

BIN
DebugTools.rar Normal file

Binary file not shown.

438
Src/allvars_xml_parser.py Normal file
View File

@@ -0,0 +1,438 @@
from __future__ import annotations
import sys
import re
import xml.etree.ElementTree as ET
import var_setup
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
from PySide2.QtWidgets import (
QDialog, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QPushButton,
QLineEdit, QLabel, QHeaderView, QCompleter, QCheckBox, QHBoxLayout, QSizePolicy,
QTableWidget, QTableWidgetItem, QFileDialog, QWidget, QMessageBox, QApplication, QMainWindow
)
from PySide2 import QtCore, QtGui
from path_hints import PathHints
from generate_debug_vars import choose_type_map, type_map
from var_selector_window import VariableSelectorDialog
from typing import List, Tuple, Optional, Dict, Any, Set
DATE_FIELD_SET = {'year','month','day','hour','minute'}
@dataclass
class MemberNode:
name: str
offset: int = 0
type_str: str = ''
size: Optional[int] = None
children: List['MemberNode'] = field(default_factory=list)
# --- новые, но необязательные (совместимость) ---
kind: Optional[str] = None # 'array', 'union', ...
count: Optional[int] = None # size1 (число элементов в массиве)
def is_date_struct(self) -> bool:
if not self.children:
return False
child_names = {c.name for c in self.children}
return DATE_FIELD_SET.issubset(child_names)
@dataclass
class VariableNode:
name: str
address: int
type_str: str
size: Optional[int]
members: List[MemberNode] = field(default_factory=list)
# --- новые, но необязательные ---
kind: Optional[str] = None # 'array'
count: Optional[int] = None # size1
def base_address_hex(self) -> str:
return f"0x{self.address:06X}"
# --------------------------- XML Parser ----------------------------
class VariablesXML:
"""
Reads your XML and outputs a flat list of paths:
- Arrays -> name[i], multilevel -> name[i][j]
- Pointer to struct -> children via '->'
- Regular struct -> children via '.'
"""
# assumed primitive sizes (for STM/MCU: int=2)
_PRIM_SIZE = {
'char':1, 'signed char':1, 'unsigned char':1, 'uint8_t':1, 'int8_t':1,
'short':2, 'short int':2, 'signed short':2, 'unsigned short':2,
'uint16_t':2, 'int16_t':2,
'int':2, 'signed int':2, 'unsigned int':2,
'long':4, 'unsigned long':4, 'int32_t':4, 'uint32_t':4,
'float':4,
'long long':8, 'unsigned long long':8, 'int64_t':8, 'uint64_t':8, 'double':8,
}
def __init__(self, path: str):
self.path = path
self.timestamp: str = ''
self.variables: List[VariableNode] = []
choose_type_map(0)
self._parse()
# ------------------ low helpers ------------------
@staticmethod
def _parse_int_guess(txt: Optional[str]) -> Optional[int]:
if not txt:
return None
txt = txt.strip()
if txt.startswith(('0x','0X')):
return int(txt, 16)
# если в строке есть буквы A-F → возможно hex
if any(c in 'abcdefABCDEF' for c in txt):
try:
return int(txt, 16)
except ValueError:
pass
try:
return int(txt, 10)
except ValueError:
return None
@staticmethod
def _is_pointer_to_struct(t: str) -> bool:
if not t:
return False
low = t.replace('\t',' ').replace('\n',' ')
return 'struct ' in low and '*' in low
@staticmethod
def _is_struct_or_union(t: str) -> bool:
if not t:
return False
low = t.strip()
return low.startswith('struct ') or low.startswith('union ')
@staticmethod
def _strip_array_suffix(t: str) -> str:
return t[:-2].strip() if t.endswith('[]') else t
def _guess_primitive_size(self, type_str: str) -> Optional[int]:
if not type_str:
return None
base = type_str
for tok in ('volatile','const'):
base = base.replace(tok, '')
base = base.replace('*',' ')
base = base.replace('[',' ').replace(']',' ')
base = ' '.join(base.split()).strip()
return self._PRIM_SIZE.get(base)
# ------------------ XML read ------------------
def _parse(self):
try:
tree = ET.parse(self.path)
root = tree.getroot()
ts = root.find('timestamp')
self.timestamp = ts.text.strip() if ts is not None and ts.text else ''
def parse_member(elem) -> MemberNode:
name = elem.get('name','')
offset = int(elem.get('offset','0'),16) if elem.get('offset') else 0
t = elem.get('type','') or ''
size_attr = elem.get('size')
size = int(size_attr,16) if size_attr else None
kind = elem.get('kind')
size1_attr = elem.get('size1')
count = None
if size1_attr:
count = self._parse_int_guess(size1_attr)
node = MemberNode(name=name, offset=offset, type_str=t, size=size,
kind=kind, count=count)
for ch in elem.findall('member'):
node.children.append(parse_member(ch))
return node
for var in root.findall('variable'):
addr = int(var.get('address','0'),16)
name = var.get('name','')
t = var.get('type','') or ''
size_attr = var.get('size')
size = int(size_attr,16) if size_attr else None
kind = var.get('kind')
size1_attr = var.get('size1')
count = None
if size1_attr:
count = self._parse_int_guess(size1_attr)
members = [parse_member(m) for m in var.findall('member')]
self.variables.append(
VariableNode(name=name, address=addr, type_str=t, size=size,
members=members, kind=kind, count=count)
)
except FileNotFoundError:
self.variables = []
except ET.ParseError:
self.variables = []
# ------------------ flatten (expanded) ------------------
def flattened(self,
max_array_elems: Optional[int] = None
) -> List[Dict[str, Any]]:
"""
Returns a list of dictionaries with full data for variables and their expanded members.
Each dictionary contains: 'name', 'address', 'type', 'size', 'kind', 'count'.
max_array_elems: limit unfolding of large arrays (None = all).
"""
out: List[Dict[str, Any]] = []
def get_dict(name: str, address: int, type_str: str, size: Optional[int], kind: Optional[str], count: Optional[int]) -> Dict[str, Any]:
"""Helper to create the output dictionary format."""
return {
'name': name,
'address': address,
'type': type_str,
'size': size,
'kind': kind,
'count': count
}
def compute_stride(size_bytes: Optional[int],
count: Optional[int],
base_type: Optional[str],
node_children: Optional[List[MemberNode]]) -> int:
"""Calculates the stride (size of one element) for arrays."""
# 1) size_bytes/count
if size_bytes and count and count > 0:
if size_bytes % count == 0:
stride = size_bytes // count
if stride <= 0:
stride = 1
return stride
else:
# size not divisible by count → most likely size = size of one element
return max(size_bytes, 1)
# 2) attempt by type (primitive)
if base_type:
gs = self._guess_primitive_size(base_type)
if gs:
return gs
# 3) attempt by children (structure)
if node_children:
if not node_children:
return 1
min_off = min(ch.offset for ch in node_children)
max_end = min_off
for ch in node_children:
sz = ch.size
if not sz:
sz = self._guess_primitive_size(ch.type_str) or 1
end = ch.offset + sz
if end > max_end:
max_end = end
stride = max_end - min_off
if stride > 0:
return stride
return 1
def expand_members(prefix_name: str,
base_addr: int,
members: List[MemberNode],
parent_is_ptr_struct: bool):
"""
Recursively expands members of structs/unions or pointed-to structs.
parent_is_ptr_struct: if True, connection is '->' otherwise '.'
"""
join = '->' if parent_is_ptr_struct else '.'
for m in members:
path_m = f"{prefix_name}{join}{m.name}" if prefix_name else m.name
addr_m = base_addr + m.offset
out.append(get_dict(path_m, addr_m, m.type_str, m.size, m.kind, m.count))
# array?
if (m.kind == 'array') or m.type_str.endswith('[]'):
count = m.count
if count is None:
count = 0
if count <= 0:
continue
base_t = self._strip_array_suffix(m.type_str)
stride = compute_stride(m.size, count, base_t, m.children if m.children else None)
limit = count if max_array_elems is None else min(count, max_array_elems)
for i in range(limit):
path_i = f"{path_m}[{i}]"
addr_i = addr_m + i*stride
# Determine kind for array element based on its base type
elem_kind = None
if self._is_struct_or_union(base_t):
elem_kind = 'struct' # or 'union' depending on `base_t` prefix
elif self._guess_primitive_size(base_t):
elem_kind = 'primitive'
# For array elements, 'size' is the stride (size of one element), 'count' is None.
out.append(get_dict(path_i, addr_i, base_t, stride, elem_kind, None))
# array element: if structure / union → unfold fields
if m.children and self._is_struct_or_union(base_t):
expand_members(path_i, addr_i, m.children, parent_is_ptr_struct=False)
# array element: if pointer to structure
elif self._is_pointer_to_struct(base_t):
# usually no children in XML for these, but if present — use them
expand_members(path_i, addr_i, m.children, parent_is_ptr_struct=True)
continue
# not an array, but has children (e.g., struct/union)
if m.children:
is_ptr_struct = self._is_pointer_to_struct(m.type_str)
expand_members(path_m, addr_m, m.children, parent_is_ptr_struct=is_ptr_struct)
# --- top-level variables ---
for v in self.variables:
out.append(get_dict(v.name, v.address, v.type_str, v.size, v.kind, v.count))
# top-level array?
if (v.kind == 'array') or v.type_str.endswith('[]'):
count = v.count
if count is None:
count = 0
if count > 0:
base_t = self._strip_array_suffix(v.type_str)
stride = compute_stride(v.size, count, base_t, v.members if v.members else None)
limit = count if max_array_elems is None else min(count, max_array_elems)
for i in range(limit):
p = f"{v.name}[{i}]"
a = v.address + i*stride
# Determine kind for array element
elem_kind = None
if self._is_struct_or_union(base_t):
elem_kind = 'struct' # or 'union'
elif self._guess_primitive_size(base_t):
elem_kind = 'primitive'
out.append(get_dict(p, a, base_t, stride, elem_kind, None))
# array of structs?
if v.members and self._is_struct_or_union(base_t):
expand_members(p, a, v.members, parent_is_ptr_struct=False)
# array of pointers to structs?
elif self._is_pointer_to_struct(base_t):
expand_members(p, a, v.members, parent_is_ptr_struct=True)
continue
# top-level not an array, but has members
if v.members:
is_ptr_struct = self._is_pointer_to_struct(v.type_str)
expand_members(v.name, v.address, v.members, parent_is_ptr_struct=is_ptr_struct)
return out
# -------------------- date candidates (as it was) --------------------
def date_struct_candidates(self) -> List[Tuple[str,int]]:
cands = []
for v in self.variables:
# top level (if all date fields are present)
direct_names = {mm.name for mm in v.members}
if DATE_FIELD_SET.issubset(direct_names):
cands.append((v.name, v.address))
# check first-level members
for m in v.members:
if m.is_date_struct():
cands.append((f"{v.name}.{m.name}", v.address + m.offset))
return cands
def get_all_vars_data(self) -> List[Dict[str, Any]]:
"""
Возвращает вложенную структуру словарей с полными данными для всех переменных и их развернутых членов.
Каждый словарь представляет узел в иерархии и содержит:
'name' (полный путь), 'address', 'size', 'type', 'kind', 'count', и 'children' (если есть).
Логика определения родительского пути теперь использует `split_path` для анализа структуры пути.
"""
flat_data = self.flattened(max_array_elems=None)
root_nodes: List[Dict[str, Any]] = []
all_nodes_map: Dict[str, Dict[str, Any]] = {}
for item in flat_data:
node_dict = {**item, 'children': []}
all_nodes_map[item['name']] = node_dict
# Вспомогательная функция для определения полного пути родителя с использованием split_path
def get_parent_path_using_split(full_path: str) -> Optional[str]:
# 1. Используем split_path для получения компонентов пути.
components = var_setup.split_path(full_path)
# Если нет компонентов или только один (верхний уровень, не массивный элемент)
if not components or len(components) == 1:
# Если компонент один и это не индекс массива (например, "project" или "my_var")
# тогда у него нет родителя в этой иерархии.
# Если это был бы "my_array[0]" -> components=['my_array', '[0]'], len=2
if len(components) == 1 and not components[0].startswith('['):
return None
elif len(components) == 2 and components[-1].startswith('['): # like "my_array[0]"
return components[0] # Return "my_array" as parent
else: # Edge cases or malformed, treat as root
return None
# 2. Определяем, как отрезать "хвост" из оригинальной строки `full_path`, чтобы получить родителя.
# Эта логика остаётся похожей на предыдущую, так как `split_path` не включает разделители
# и мы должны получить точную строку родительского пути.
# Находим индекс последнего разделителя '.' или '->'
last_dot_idx = full_path.rfind('.')
last_arrow_idx = full_path.rfind('->')
effective_last_sep_idx = -1
if last_dot_idx > last_arrow_idx:
effective_last_sep_idx = last_dot_idx
elif last_arrow_idx != -1:
effective_last_sep_idx = last_arrow_idx
# Находим начало последнего суффикса массива (e.g., '[0]') в оригинальной строке
array_suffix_match = re.search(r'(\[[^\]]*\])+$', full_path)
array_suffix_start_idx = -1
if array_suffix_match:
array_suffix_start_idx = array_suffix_match.start()
# Логика определения родителя:
# - Если есть суффикс массива, и он находится после последнего разделителя (или разделителей нет),
# то родитель - это часть до суффикса массива. (e.g., 'project.adc[0]' -> 'project.adc')
# - Иначе, если есть разделитель, родитель - это часть до последнего разделителя. (e.g., 'project.adc.bus' -> 'project.adc')
# - Иначе (ни разделителей, ни суффиксов), это корневой элемент.
if array_suffix_start_idx != -1 and (array_suffix_start_idx > effective_last_sep_idx):
return full_path[:array_suffix_start_idx]
elif effective_last_sep_idx != -1:
return full_path[:effective_last_sep_idx]
else:
return None # Корневой элемент без явного родителя
# Основная логика get_all_vars_data
# Заполнение связей "родитель-потомок"
for item_name, node_dict in all_nodes_map.items():
parent_name = get_parent_path_using_split(item_name) # Используем новую вспомогательную функцию
if parent_name and parent_name in all_nodes_map:
all_nodes_map[parent_name]['children'].append(node_dict)
else:
root_nodes.append(node_dict)
# Сортируем корневые узлы и их детей рекурсивно по имени
def sort_nodes(nodes_list: List[Dict[str, Any]]):
nodes_list.sort(key=lambda x: x['name'])
for node in nodes_list:
if node['children']:
sort_nodes(node['children'])
sort_nodes(root_nodes)
return root_nodes

223
Src/csv_logger.py Normal file
View File

@@ -0,0 +1,223 @@
import csv
import numbers
import time
from datetime import datetime
from PySide2 import QtWidgets
class CsvLogger:
"""
Логгер, совместимый по формату с C-реализацией CSV_AddTitlesLine / CSV_AddLogLine.
Публичный API сохранён:
set_titles(varnames)
set_value(timestamp, varname, varvalue)
select_file(parent=None) -> bool
write_to_csv()
Использование:
1) set_titles([...])
2) многократно set_value(ts, name, value)
3) select_file() (по желанию)
4) write_to_csv()
"""
def __init__(self, filename="log.csv", delimiter=';'):
self._filename = filename
self._delimiter = delimiter
# Пользовательские заголовки
self.variable_names_ordered = []
# Полные заголовки CSV (Ticks(X), Ticks(Y), Time(Y), ...)
self.headers = ['t'] # до вызова set_titles placeholder
# Данные: {timestamp_key: {varname: value, ...}}
# timestamp_key = то, что передано в set_value (float/int/etc)
self.data_rows = {}
# Внутренние структуры для генерации CSV-формата С
self._row_wall_dt = {} # {timestamp_key: datetime при первой записи}
self._base_ts = None # timestamp_key первой строки (число)
self._base_ts_val = 0.0 # float значение первой строки (для delta)
self._tick_x_start = 0 # начальный тик (можно менять вручную при необходимости)
# ---- Свойства ----
@property
def filename(self):
return self._filename
# ---- Публичные методы ----
def set_titles(self, varnames):
"""
Устанавливает имена переменных.
Формирует полные заголовки CSV в формате С-лога.
"""
if not isinstance(varnames, list):
raise TypeError("Varnames must be a list of strings.")
if not all(isinstance(name, str) for name in varnames):
raise ValueError("All variable names must be strings.")
self.variable_names_ordered = varnames
self.headers = ["Ticks(X)", "Ticks(Y)", "Time(Y)"] + self.variable_names_ordered
# Сброс данных (структура изменилась)
self.data_rows.clear()
self._row_wall_dt.clear()
self._base_ts = None
self._base_ts_val = 0.0
def set_value(self, timestamp, varname, varvalue):
"""
Установить ОДНО значение в ОДНУ колонку для заданного timestampа.
timestamp — float секунд с эпохи (time.time()).
"""
if varname not in self.variable_names_ordered:
return # игнор, как у тебя было
# Новая строка?
if timestamp not in self.data_rows:
# Инициализируем поля переменных значением None
self.data_rows[timestamp] = {vn: None for vn in self.variable_names_ordered}
# Дата/время строки из ПЕРЕДАННОГО timestamp (а не datetime.now()!)
try:
ts_float = float(timestamp)
except Exception:
# если какая-то дичь прилетела, пусть будет 0 (эпоха) чтобы не упасть
ts_float = 0.0
self._row_wall_dt[timestamp] = datetime.fromtimestamp(ts_float)
# База для расчёта Ticks(Y) — первая строка
if self._base_ts is None:
self._base_ts = timestamp
self._base_ts_val = ts_float
# Записываем значение
self.data_rows[timestamp][varname] = varvalue
def select_file(self, parent=None) -> bool:
"""
Диалог выбора файла.
"""
options = QtWidgets.QFileDialog.Options()
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
parent,
"Сохранить данные CSV",
self._filename,
"CSV Files (*.csv);;All Files (*)",
options=options
)
if filename:
if not filename.lower().endswith('.csv'):
filename += '.csv'
self._filename = filename
return True
else:
return False
def write_to_csv(self):
"""
Формирует CSV в формате C:
Ticks(X);Ticks(Y);Time(Y);Var1;Var2;...
0;0,000000;22/07/2025 13:45:12:0123;...;...
Правила значений:
- Тик X: автоинкремент от 0 (или self._tick_x_start) по порядку сортировки timestamp.
- Ticks(Y): дельта (секунды,микросекунды) между текущим timestamp и первым timestamp.
- Time(Y): wallclock строки (datetime.now() при первом появлении timestamp).
- Значение < 0 -> пустая ячейка (как if(raw_data[i] >= 0) else ;)
- None -> пустая ячейка.
"""
if len(self.headers) <= 3: # только служебные поля без переменных
print("Ошибка: Заголовки не установлены или не содержат переменных. Вызовите set_titles() перед записью.")
return
if not self._filename:
print("Ошибка: Имя файла не определено. select_file() или задайте при инициализации.")
return
if not self.data_rows:
print("Предупреждение: Нет данных для записи.")
# всё равно создадим файл с одними заголовками
try:
with open(self._filename, 'w', newline='', encoding='utf-8') as csvfile:
# QUOTE_NONE + escapechar для чистого формата без кавычек (как в С-строке)
writer = csv.writer(
csvfile,
delimiter=self._delimiter,
quoting=csv.QUOTE_NONE,
escapechar='\\',
lineterminator='\r\n'
)
# Пишем заголовки
writer.writerow(self.headers)
if self.data_rows:
sorted_ts = sorted(self.data_rows.keys(), key=self._ts_sort_key)
# убедимся, что база была зафиксирована
if self._base_ts is None:
self._base_ts = sorted_ts[0]
self._base_ts_val = self._coerce_ts_to_float(self._base_ts)
tick_x = self._tick_x_start
for ts in sorted_ts:
row_dict = self.data_rows[ts]
# delta по timestamp
cur_ts_val = self._coerce_ts_to_float(ts)
delta_us = int(round((cur_ts_val - self._base_ts_val) * 1_000_000))
if delta_us < 0:
delta_us = 0 # защита
seconds = delta_us // 1_000_000
micros = delta_us % 1_000_000
# wallclock строки
dt = self._row_wall_dt.get(ts, datetime.now())
# Формат DD/MM/YYYY HH:MM:SS:мммм (4 цифры ms, как в C: us/1000)
time_str = dt.strftime("%d/%m/%Y %H:%M:%S") + f":{dt.microsecond // 1000:04d}"
# Значения
row_vals = []
for vn in self.variable_names_ordered:
v = row_dict.get(vn)
if v is None:
row_vals.append("") # нет данных
else:
# если числовое и <0 -> пусто (как в C: если raw_data[i] >= 0 else ;)
if isinstance(v, numbers.Number) and v < 0:
row_vals.append("")
else:
row_vals.append(v)
csv_row = [tick_x, f"{seconds},{micros:06d}", time_str] + row_vals
writer.writerow(csv_row)
tick_x += 1
print(f"Данные успешно записаны в '{self._filename}'")
except Exception as e:
print(f"Ошибка при записи в файл '{self._filename}': {e}")
# ---- Вспомогательные ----
def _coerce_ts_to_float(self, ts):
"""
Пробуем привести переданный timestamp к float.
Разрешаем int/float/str, остальное -> индекс по порядку (0).
"""
if isinstance(ts, numbers.Number):
return float(ts)
try:
return float(ts)
except Exception:
# fallback: нечисловой ключ -> используем порядковый индекс
# (таких почти не должно быть, но на всякий)
return 0.0
def _ts_sort_key(self, ts):
"""
Ключ сортировки timestampов — сначала попытка float, потом str.
"""
if isinstance(ts, numbers.Number):
return (0, float(ts))
try:
return (0, float(ts))
except Exception:
return (1, str(ts))

View File

@@ -92,6 +92,11 @@ class PathNode:
def add_child(self, child: "PathNode") -> None: def add_child(self, child: "PathNode") -> None:
self.children[child.name] = child self.children[child.name] = child
def get_children(self) -> List["PathNode"]:
"""
Вернуть список дочерних узлов, отсортированных по имени.
"""
return sorted(self.children.values(), key=lambda n: n.name)
class PathHints: class PathHints:
""" """
@@ -173,6 +178,15 @@ class PathHints:
def find_node(self, path: str) -> Optional[PathNode]: def find_node(self, path: str) -> Optional[PathNode]:
return self._index.get(canonical_key(path)) return self._index.get(canonical_key(path))
def get_children(self, full_path: str) -> List[PathNode]:
"""
Вернуть список дочерних узлов PathNode для заданного полного пути.
Если узел не найден — вернуть пустой список.
"""
node = self.find_node(full_path)
if node is None:
return []
return node.get_children()
# ------------ Подсказки ------------ # ------------ Подсказки ------------
def suggest(self, def suggest(self,
@@ -227,6 +241,27 @@ class PathHints:
res.append(child.full_path) res.append(child.full_path)
return sorted(res) return sorted(res)
def add_separator(self, full_path: str) -> str:
"""
Возвращает full_path с добавленным разделителем ('.' или '['),
если у узла есть дети и пользователь ещё не поставил разделитель.
Если первый ребёнок — массивный токен ('[0]') → добавляем '['.
Позже можно допилить '->' для указателей.
"""
node = self.find_node(full_path)
text = full_path
if node and node.children and not (
text.endswith('.') or text.endswith('->') or text.endswith('[')
):
first_child = next(iter(node.children.values()))
if first_child.name.startswith('['):
text += '[' # сразу начинаем индекс
else:
text += '.' # обычный переход
return text
# ------------ внутренние вспомогательные ------------ # ------------ внутренние вспомогательные ------------
def _root_full_names(self) -> List[str]: def _root_full_names(self) -> List[str]:

View File

@@ -1,368 +1,71 @@
""" """
LowLevelSelectorWidget (PySide2) LowLevelSelectorWidget (refactored)
-------------------------------- -----------------------------------
Виджет для: Версия, использующая VariableTableWidget вместо самодельной таблицы selected_vars_table.
* Выбора XML файла с описанием переменных (как в примере пользователя)
* Парсинга всех <variable> и их вложенных <member>
* Построения плоского списка путей (имя/подпуть) с расчётом абсолютного адреса (base_address + offset)
* Определения структур с полями даты (year, month, day, hour, minute)
* Выбора переменной и (опционально) переменной даты / ручного ввода даты
* Выбора типов: ptr_type (pt_*), iq_type, return_type
* Форматирования адреса в виде 0x000000 (6 HEX)
* Генерации словаря/кадра для последующей LowLevel-команды (не отправляет сам)
Интеграция: Ключевые изменения:
* Подключите сигнал variablePrepared(dict) к функции, формирующей и отправляющей пакет. * Вместо QTableWidget с 6 колонками теперь встраивается VariableTableWidget (8 колонок: №, En, Name, Origin Type, Base Type, IQ Type, Return Type, Short Name).
* Содержимое dict: * Логика sync <-> self._all_available_vars перенесена в _on_var_table_changed() и _pull_from_var_table().
{ * Поддержка политики хранения типов:
'address': int, - ptr_type: строковое имя (без префикса `pt_`).
'address_hex': str, # '0x....' - ptr_type_enum: числовой индекс (см. PT_ENUM_ORDER).
'ptr_type': int, # значение enum pt_* - Для совместимости с VariableTableWidget: поле `pt_type` = 'pt_<name>'.
'iq_type': int, - IQ / Return: аналогично (`iq_type` / `iq_type_enum`, `return_type` / `return_type_enum`).
'return_type': int, * Функции получения выбранных переменных теперь читают данные из VariableTableWidget.
'datetime': { * Убраны неиспользуемые методы, связанные с прежней таблицей (комбо‑боксы и т.п.).
'year': int,
'month': int, Как интегрировать:
'day': int, 1. Поместите этот файл рядом с module VariableTableWidget (см. импорт ниже). Если класс VariableTableWidget находится в том же файле — удалите строку импорта и используйте напрямую.
'hour': int, 2. Убедитесь, что VariablesXML предоставляет методы get_all_vars_data() (list[dict]) и, при наличии, get_struct_map() -> dict[type_name -> dict[field_name -> field_type]]. Если такого метода нет, передаём пустой {} и автодополнение по структурам будет недоступно.
'minute': int, 3. Отметьте переменные в VariableSelectorDialog (как и раньше) — он обновит self._all_available_vars. После закрытия диалога вызывается self._populate_var_table().
}, 4. Для чтения выбранных переменных используйте get_selected_variables_and_addresses(); она вернёт список словарей в унифицированном формате.
'path': str, # полный путь переменной
'type_string': str, # строка типа из XML Примечание о совместимости: VariableTableWidget работает с ключами `pt_type`, `iq_type`, `return_type` (строки с префиксами). Мы поддерживаем дублирование этих полей с «новыми» полями без префикса и enumзначениями.
}
Зависимости: только PySide2 и стандартная библиотека.
""" """
from __future__ import annotations from __future__ import annotations
import sys import sys
import xml.etree.ElementTree as ET import re
import datetime
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple from typing import List, Dict, Optional, Tuple, Any
from PySide2 import QtCore, QtGui, QtWidgets
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 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 -- # ------------------------------------------------------------ Enumerations --
# Сопоставление строк из XML типу ptr_type (адаптируйте под реальный проект) # Порядок фиксируем на основании предыдущей версии. При необходимости расширьте.
PTR_TYPE_MAP = {
'int8': 'pt_int8', 'signed char': 'pt_int8', 'char': 'pt_int8',
'int16': 'pt_int16', 'short': 'pt_int16', 'int': 'pt_int16',
'int32': 'pt_int32', 'long': 'pt_int32',
'int64': 'pt_int64', 'long long': 'pt_int64',
'uint8': 'pt_uint8', 'unsigned char': 'pt_uint8',
'uint16': 'pt_uint16', 'unsigned short': 'pt_uint16', 'unsigned int': 'pt_uint16',
'uint32': 'pt_uint32', 'unsigned long': 'pt_uint32',
'uint64': 'pt_uint64', 'unsigned long long': 'pt_uint64',
'float': 'pt_float', 'floatf': 'pt_float',
'struct': 'pt_struct', 'union': 'pt_union',
}
PT_ENUM_ORDER = [ PT_ENUM_ORDER = [
'pt_unknown','pt_int8','pt_int16','pt_int32','pt_int64', 'unknown','int8','int16','int32','int64',
'pt_uint8','pt_uint16','pt_uint32','pt_uint64','pt_float', 'uint8','uint16','uint32','uint64','float',
'pt_struct','pt_union' 'struct','union'
] ]
IQ_ENUM_ORDER = [ IQ_ENUM_ORDER = [
't_iq_none','t_iq','t_iq1','t_iq2','t_iq3','t_iq4','t_iq5','t_iq6', 'iq_none','iq','iq1','iq2','iq3','iq4','iq5','iq6',
't_iq7','t_iq8','t_iq9','t_iq10','t_iq11','t_iq12','t_iq13','t_iq14', 'iq7','iq8','iq9','iq10','iq11','iq12','iq13','iq14',
't_iq15','t_iq16','t_iq17','t_iq18','t_iq19','t_iq20','t_iq21','t_iq22', 'iq15','iq16','iq17','iq18','iq19','iq20','iq21','iq22',
't_iq23','t_iq24','t_iq25','t_iq26','t_iq27','t_iq28','t_iq29','t_iq30' 'iq23','iq24','iq25','iq26','iq27','iq28','iq29','iq30'
] ]
# Для примера: маппинг имени enum -> числовое значение (индекс по порядку) PT_ENUM_VALUE: Dict[str, int] = {name: idx for idx, name in enumerate(PT_ENUM_ORDER)}
PT_ENUM_VALUE = {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)}
IQ_ENUM_VALUE = {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()}
# -------------------------------------------------------------- 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)
@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] = []
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/count
if size_bytes and count and count > 0:
stride = size_bytes // count
if stride * count != size_bytes:
# округлённо вверх
stride = (size_bytes + count - 1) // count
if stride <= 0:
stride = 1
return stride
# 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 -- # ------------------------------------------- Address / validation helpers --
@@ -381,341 +84,403 @@ class HexAddrValidator(QtGui.QRegExpValidator):
return '0x000000' return '0x000000'
return f"0x{val & 0xFFFFFF:06X}" return f"0x{val & 0xFFFFFF:06X}"
# --------------------------------------------------------- Main Widget ----
class LowLevelSelectorWidget(QtWidgets.QWidget): class LowLevelSelectorWidget(QWidget):
variablePrepared = QtCore.Signal(dict) variablePrepared = QtCore.Signal(dict)
xmlLoaded = QtCore.Signal(str) xmlLoaded = QtCore.Signal(str)
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle('LowLevel Variable Selector') self.setWindowTitle('LowLevel Variable Selector')
self._xml: Optional[VariablesXML] = None self._xml: Optional[VariablesXML] = None
self._paths = [] self._paths: List[str] = []
self._path_info = {} self._path_info: Dict[str, Tuple[int, str]] = {}
self._addr_index = {} self._addr_index: Dict[int, Optional[str]] = {}
self._hints = PathHints() 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._build_ui()
self._connect() self._connect()
def _build_ui(self): def _build_ui(self):
lay = QtWidgets.QVBoxLayout(self) tab = QWidget()
main_layout = QVBoxLayout(tab)
# --- File chooser --- # --- Variable Selector ---
file_box = QtWidgets.QHBoxLayout() g_selector = QGroupBox("Variable Selector")
self.btn_load = QtWidgets.QPushButton('Load XML...') selector_layout = QVBoxLayout(g_selector)
self.lbl_file = QtWidgets.QLabel('<no file>')
form_selector = QFormLayout()
# --- XML File chooser ---
file_layout = QHBoxLayout()
self.btn_load = QPushButton('Load XML...')
self.lbl_file = QLabel('<no file>')
self.lbl_file.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) self.lbl_file.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
file_box.addWidget(self.btn_load) file_layout.addWidget(self.btn_load)
file_box.addWidget(self.lbl_file, 1) file_layout.addWidget(self.lbl_file, 1)
lay.addLayout(file_box) form_selector.addRow("XML File:", file_layout)
self.lbl_timestamp = QtWidgets.QLabel('Timestamp: -') # --- Interval SpinBox ---
lay.addWidget(self.lbl_timestamp) 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 --- # --- Buttons ---
self.edit_var_search = QtWidgets.QLineEdit() self.btn_read_once = QPushButton("Read Once")
self.edit_var_search.setPlaceholderText("Введите имя/путь или адрес 0x......") self.btn_start_polling = QPushButton("Start Polling")
form.addRow('Variable:', self.edit_var_search) btn_layout = QHBoxLayout()
btn_layout.addWidget(self.btn_read_once)
btn_layout.addWidget(self.btn_start_polling)
selector_layout.addLayout(btn_layout)
# Popup list # --- Table ---
self._popup = QtWidgets.QListView() g_table = QGroupBox("Table")
self._popup.setWindowFlags(QtCore.Qt.ToolTip) table_layout = QVBoxLayout(g_table)
self._popup.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) self.btn_open_var_selector = QPushButton("Выбрать переменные...")
self._popup.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) table_layout.addWidget(self.btn_open_var_selector)
self._popup.clicked.connect(self._on_popup_clicked) self.var_table = VariableTableWidget(self, show_value_instead_of_shortname=1)
self._model_all = QtGui.QStandardItemModel(self) self.var_table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self._model_filtered = QtGui.QStandardItemModel(self) table_layout.addWidget(self.var_table)
# Address # --- Timestamp (moved here) ---
self.edit_address = QtWidgets.QLineEdit('0x000000') self.lbl_timestamp = QLabel('Timestamp: -')
self.edit_address.setValidator(HexAddrValidator(self)) table_layout.addWidget(self.lbl_timestamp)
self.edit_address.setMaximumWidth(120)
form.addRow('Address:', self.edit_address)
# Manual date spins # --- Splitter (Selector + Table) ---
dt_row = QtWidgets.QHBoxLayout() v_split = QSplitter(QtCore.Qt.Vertical)
self.spin_year = QtWidgets.QSpinBox(); self.spin_year.setRange(2000, 2100); self.spin_year.setValue(2025) v_split.addWidget(g_selector)
self.spin_month = QtWidgets.QSpinBox(); self.spin_month.setRange(1,12) v_split.addWidget(g_table)
self.spin_day = QtWidgets.QSpinBox(); self.spin_day.setRange(1,31) v_split.setStretchFactor(0, 1)
self.spin_hour = QtWidgets.QSpinBox(); self.spin_hour.setRange(0,23) v_split.setStretchFactor(1, 3)
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('Manual Date:', dt_row)
# Types main_layout.addWidget(v_split)
self.cmb_ptr_type = QtWidgets.QComboBox(); self.cmb_ptr_type.addItems(PT_ENUM_ORDER) self.setLayout(main_layout)
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('ptr_type:', self.cmb_ptr_type)
form.addRow('iq_type:', self.cmb_iq_type)
form.addRow('return_type:', self.cmb_return_type)
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): def _connect(self):
self.btn_load.clicked.connect(self._on_load_xml) self.btn_load.clicked.connect(self._on_load_xml)
self.edit_address.editingFinished.connect(self._normalize_address) self.btn_open_var_selector.clicked.connect(self._on_open_variable_selector)
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)
# ---------------- XML Load ---------------- # ------------------------------------------------------ XML loading ----
def _on_load_xml(self): def _on_load_xml(self):
path, _ = QtWidgets.QFileDialog.getOpenFileName( path, _ = QFileDialog.getOpenFileName(
self, 'Select variables XML', '', 'XML Files (*.xml);;All Files (*)') self, 'Select variables XML', '', 'XML Files (*.xml);;All Files (*)')
if not path: if not path:
return return
try: try:
self._xml = VariablesXML(path) 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: except Exception as e:
QtWidgets.QMessageBox.critical(self, 'Parse error', f'Ошибка парсинга:\n{e}') QMessageBox.critical(self, 'Parse error', f'Ошибка парсинга:\n{e}')
return return
self.lbl_file.setText(path) self.lbl_file.setText(path)
self.lbl_timestamp.setText(f'Timestamp: {self._xml.timestamp or "-"}') 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._apply_timestamp_to_date()
self.xmlLoaded.emit(path) 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): def _apply_timestamp_to_date(self):
if not self._xml.timestamp: if not (self._xml and self._xml.timestamp):
return return
import datetime
try: try:
# Пример: "Sat Jul 19 15:27:59 2025" # Пример: "Sat Jul 19 15:27:59 2025"
dt = datetime.datetime.strptime(self._xml.timestamp, "%a %b %d %H:%M:%S %Y") self.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)
except Exception as e: except Exception as e:
print(f"Ошибка разбора timestamp '{self._xml.timestamp}': {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: if not self._xml:
QMessageBox.warning(self, 'No XML', 'Сначала загрузите XML файл.')
return return
flat = self._xml.flattened()
# flat: [(path, addr, type_str), ...]
self._paths = [] dialog = VariableSelectorDialog(
self._path_info = {} table=None, # не используем встроенную таблицу
self._addr_index = {} all_vars=self._all_available_vars,
self._model_all.clear() # держим «сырой» полный список (можно не показывать) structs=None, # при необходимости подайте реальные структуры из XML
self._model_filtered.clear() # текущие подсказки 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.")
# индексирование # ----------------------------------------------------- Populate table ----
for path, addr, t in flat: def _populate_var_table(self):
self._paths.append(path) """Отобразить переменные (show_var == 'true') в VariableTableWidget."""
self._path_info[path] = (addr, t) 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_<ptr_type>' (для совместимости с VariableTableWidget)
var['return_type_pref']-> 't_<return_type>' (см. ниже) # не обяз.
Дополнительно корректируем 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: if addr in self._addr_index:
self._addr_index[addr] = None self._addr_index[addr] = None
else: 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]) self._hints.set_paths([(p, self._path_info[p][1]) for p in self._paths])
# начальное состояние попапа (пустой ввод → top-level) # -------------------------------------------------- Public helpers ----
self._update_popup_model(self._hints.suggest('')) 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 --------------- out: List[Dict[str, Any]] = []
def _update_popup_model(self, paths: List[str]): for rec in tbl_data:
"""Обновляет модель попапа списком путей (full paths).""" nm = rec.get('name')
self._model_filtered.clear() if not nm:
limit = 400
added = 0
for p in paths:
info = self._path_info.get(p)
if not info:
continue continue
addr, t = info src = idx_by_name.get(nm, {})
it = QtGui.QStandardItem(f"{p} [{addr:06X}]") addr = src.get('address')
it.setData(p, QtCore.Qt.UserRole+1) if addr is None or addr == '' or addr == 0:
it.setData(addr, QtCore.Qt.UserRole+2) src['address'] = self.flat_vars.get(nm, {}).get('address', 0)
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):
t = text.strip()
# адрес?
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 path:
self._set_current_variable(path)
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:
if ev.key() in (QtCore.Qt.Key_Down, QtCore.Qt.Key_Up):
if not self._popup.isVisible():
self._show_popup()
else: else:
step = 1 if ev.key()==QtCore.Qt.Key_Down else -1 # если это строка "0x..." — конвертируем в int
cur = self._popup.currentIndex() if isinstance(addr, str) and addr.startswith('0x'):
row = cur.row() + step try:
if row < 0: row = 0 src['address'] = int(addr, 16)
if row >= self._model_filtered.rowCount(): except ValueError:
row = self._model_filtered.rowCount()-1 src['address'] = self.flat_vars.get(nm, {}).get('address', 0)
self._popup.setCurrentIndex(self._model_filtered.index(row,0)) type_str = src.get('type', rec.get('type','N/A'))
# нормализация типов
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
if not found:
# Если переменной нет в списке, можно либо проигнорировать, либо добавить.
return False
# 2. Обновляем отображение в таблице
self.var_table.populate(self._all_available_vars, {}, self._on_var_table_changed)
return True return True
elif ev.key() == QtCore.Qt.Key_Escape:
self._hide_popup()
return True
return super().eventFilter(obj, ev)
def _set_current_variable(self, path: str, from_address=False): # --------------- Address mapping / type mapping helpers ---------------
if path not in self._path_info:
return
addr, type_str = self._path_info[path]
self.edit_var_search.setText(path)
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}")
# --------------- Date struct / address / helpers ---------------
def _normalize_address(self): def _map_type_to_ptr_enum(self, type_str: Optional[str]) -> str:
self.edit_address.setText(HexAddrValidator.normalize(self.edit_address.text()))
def _map_type_to_ptr_enum(self, type_str: str) -> str:
if not type_str: if not type_str:
return 'pt_unknown' return 'unknown'
low = type_str.lower() low = type_str.lower()
token = low.replace('*',' ').replace('[',' ').split()[0] token = low.replace('*',' ').replace('[',' ')
return PTR_TYPE_MAP.get(token, 'pt_unknown') return type_map.get(token, 'unknown').replace('pt_','')
def _select_combo_text(self, combo: QtWidgets.QComboBox, text: str):
ix = combo.findText(text)
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)
# ----------------------------------------------------------- Logging --
def _log(self, msg: str): def _log(self, msg: str):
self.txt_info.appendPlainText(msg) print(f"[LowLevelSelectorWidget Log] {msg}")
# ---------------------------------------------------------------------------
# Тест‑прогоночка (ручной) --------------------------------------------------
# Запускать только вручную: python LowLevelSelectorWidget_refactored.py <xml>
# ---------------------------------------------------------------------------
# ----------------------------------------------------------- Demo window -- # ----------------------------------------------------------- Demo window --
class _DemoWindow(QtWidgets.QMainWindow): class _DemoWindow(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setWindowTitle('LowLevel Selector Demo') self.setWindowTitle('LowLevel Selector Demo')
@@ -732,7 +497,7 @@ class _DemoWindow(QtWidgets.QMainWindow):
# ----------------------------------------------------------------- main --- # ----------------------------------------------------------------- main ---
if __name__ == '__main__': if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv) app = QApplication(sys.argv)
w = _DemoWindow() w = _DemoWindow()
w.resize(640, 520) w.resize(640, 520)
w.show() w.show()

View File

@@ -1,12 +1,36 @@
from PySide2 import QtCore, QtWidgets, QtSerialPort from PySide2 import QtCore, QtWidgets, QtSerialPort
from tms_debugvar_lowlevel import LowLevelSelectorWidget from tms_debugvar_lowlevel import LowLevelSelectorWidget
import datetime import datetime
import time
from csv_logger import CsvLogger
# ------------------------------- Константы протокола ------------------------ # ------------------------------- Константы протокола ------------------------
WATCH_SERVICE_BIT = 0x8000 WATCH_SERVICE_BIT = 0x8000
DEBUG_OK = 0 # ожидаемый код успешного чтения DEBUG_OK = 0 # ожидаемый код успешного чтения
SIGN_BIT_MASK = 0x80 SIGN_BIT_MASK = 0x80
FRAC_MASK_FULL = 0x7F # если используем 7 бит дробной части FRAC_MASK_FULL = 0x7F # если используем 7 бит дробной части
# --- Debug status codes (из прошивки) ---
DEBUG_OK = 0x00
DEBUG_ERR = 0x80 # общий флаг ошибки (старший бит)
DEBUG_ERR_VAR_NUMB = DEBUG_ERR | (1 << 0)
DEBUG_ERR_INVALID_VAR = DEBUG_ERR | (1 << 1)
DEBUG_ERR_ADDR = DEBUG_ERR | (1 << 2)
DEBUG_ERR_ADDR_ALIGN = DEBUG_ERR | (1 << 3)
DEBUG_ERR_INTERNAL = DEBUG_ERR | (1 << 4)
DEBUG_ERR_DATATIME = DEBUG_ERR | (1 << 5)
DEBUG_ERR_RS = DEBUG_ERR | (1 << 5)
# для декодирования по битам
_DEBUG_ERR_BITS = (
(1 << 0, "Invalid Variable Index"),
(1 << 1, "Invalid Variable"),
(1 << 2, "Invalid Address"),
(1 << 3, "Invalid Address Align"),
(1 << 4, "Internal Code Error"),
(1 << 5, "Invalid Data or Time"),
(1 << 6, "Error with RS"),
)
# ---------------------------------------------------------------- CRC util --- # ---------------------------------------------------------------- CRC util ---
def crc16_ibm(data: bytes, *, init=0xFFFF) -> int: def crc16_ibm(data: bytes, *, init=0xFFFF) -> int:
"""CRC16-IBM (aka CRC-16/ANSI, polynomial 0xA001 reflected).""" """CRC16-IBM (aka CRC-16/ANSI, polynomial 0xA001 reflected)."""
@@ -20,6 +44,26 @@ def crc16_ibm(data: bytes, *, init=0xFFFF) -> int:
crc >>= 1 crc >>= 1
return crc & 0xFFFF return crc & 0xFFFF
def _decode_debug_status(status: int) -> str:
"""Преобразует код статуса прошивки в строку.
Возвращает 'OK' или перечисление битов через '|'.
Не зависит от того, WATCH или LowLevel.
"""
if status == DEBUG_OK:
return "OK"
parts = []
if status & DEBUG_ERR:
for mask, name in _DEBUG_ERR_BITS:
if status & mask:
parts.append(name)
if not parts: # старший бит есть, но ни один из известных младших не выставлен
parts.append("ERR")
else:
# Неожиданно: статус !=0, но бит DEBUG_ERR не стоит
parts.append(f"0x{status:02X}")
return "|".join(parts)
class Spoiler(QtWidgets.QWidget): class Spoiler(QtWidgets.QWidget):
def __init__(self, title="", animationDuration=300, parent=None): def __init__(self, title="", animationDuration=300, parent=None):
super().__init__(parent) super().__init__(parent)
@@ -131,7 +175,8 @@ class DebugTerminalWidget(QtWidgets.QWidget):
self.auto_crc_check = auto_crc_check self.auto_crc_check = auto_crc_check
self._drop_if_busy = drop_if_busy self._drop_if_busy = drop_if_busy
self._replace_if_busy = replace_if_busy self._replace_if_busy = replace_if_busy
self._last_txn_timestamp = 0
self._ll_polling_active = False
if iq_scaling is None: if iq_scaling is None:
iq_scaling = {n: float(1 << n) for n in range(31)} iq_scaling = {n: float(1 << n) for n in range(31)}
iq_scaling[0] = 1.0 iq_scaling[0] = 1.0
@@ -162,7 +207,13 @@ class DebugTerminalWidget(QtWidgets.QWidget):
self._ll_poll_timer = QtCore.QTimer(self) self._ll_poll_timer = QtCore.QTimer(self)
self._ll_poll_timer.timeout.connect(self._on_ll_poll_timeout) self._ll_poll_timer.timeout.connect(self._on_ll_poll_timeout)
self._ll_polling = False 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) # Кэш: index -> (status, iq, name, is_signed, frac_bits)
self._name_cache = {} self._name_cache = {}
@@ -200,6 +251,49 @@ class DebugTerminalWidget(QtWidgets.QWidget):
self.tabs = QtWidgets.QTabWidget() self.tabs = QtWidgets.QTabWidget()
self._build_watch_tab() self._build_watch_tab()
self._build_lowlevel_tab() # <-- Вызываем новый метод self._build_lowlevel_tab() # <-- Вызываем новый метод
g_control = QtWidgets.QGroupBox("Control / Status")
control_layout = QtWidgets.QHBoxLayout(g_control) # Используем QHBoxLayout
# Форма для статусов слева
form_control = QtWidgets.QFormLayout()
self.lbl_status = QtWidgets.QLabel("Idle")
self.lbl_status.setStyleSheet("font-weight: bold; color: grey;")
form_control.addRow("Status:", self.lbl_status)
self.lbl_actual_interval = QtWidgets.QLabel("-")
form_control.addRow("Actual Interval:", self.lbl_actual_interval)
control_layout.addLayout(form_control, 1) # Растягиваем форму
# Галочка Raw справа
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 --- # --- UART Log ---
self.log_spoiler = Spoiler("UART Log", animationDuration=300, parent=self) self.log_spoiler = Spoiler("UART Log", animationDuration=300, parent=self)
@@ -211,112 +305,116 @@ class DebugTerminalWidget(QtWidgets.QWidget):
log_layout.addWidget(self.txt_log) log_layout.addWidget(self.txt_log)
self.log_spoiler.setContentLayout(log_layout) self.log_spoiler.setContentLayout(log_layout)
layout.addWidget(g_serial, 0) layout.addWidget(g_serial)
layout.addWidget(self.tabs, 1) layout.addWidget(self.tabs, 1)
layout.addWidget(self.log_spoiler, 0) 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(g_serial), 0)
layout.setStretch(layout.indexOf(self.tabs), 1) layout.setStretch(layout.indexOf(self.tabs), 1)
layout.setStretch(layout.indexOf(self.log_spoiler), 0)
def _build_watch_tab(self): def _build_watch_tab(self):
# ... (код для вкладки Watch остаётся без изменений)
tab = QtWidgets.QWidget()
vtab = QtWidgets.QVBoxLayout(tab)
g_watch = QtWidgets.QGroupBox("Watch Variables")
grid = QtWidgets.QGridLayout(g_watch)
grid.setHorizontalSpacing(8)
grid.setVerticalSpacing(4)
self.spin_index = QtWidgets.QSpinBox(); self.spin_index.setRange(0, 0x7FFF); self.spin_index.setAccelerated(True)
self.chk_hex_index = QtWidgets.QCheckBox("Hex")
self.spin_count = QtWidgets.QSpinBox(); self.spin_count.setRange(1,255); self.spin_count.setValue(1)
self.btn_read_service = QtWidgets.QPushButton("Read Name/Type")
self.btn_read_values = QtWidgets.QPushButton("Read Value(s)")
self.btn_poll = QtWidgets.QPushButton("Start Polling")
self.spin_interval = QtWidgets.QSpinBox(); self.spin_interval.setRange(50,10000); self.spin_interval.setValue(500); self.spin_interval.setSuffix(" ms")
self.chk_auto_service = QtWidgets.QCheckBox("Auto service before values if miss cache"); self.chk_auto_service.setChecked(True)
self.chk_raw = QtWidgets.QCheckBox("Raw (no IQ scaling)")
self.lbl_name = QtWidgets.QLineEdit(); self.lbl_name.setReadOnly(True)
self.lbl_iq = QtWidgets.QLabel("-")
self.edit_single_value = QtWidgets.QLineEdit(); self.edit_single_value.setReadOnly(True)
self.tbl_values = QtWidgets.QTableWidget(0, 5)
self.tbl_values.setHorizontalHeaderLabels(["Index","Name","IQ","Raw","Scaled"])
hh = self.tbl_values.horizontalHeader()
hh.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
hh.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
hh.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
hh.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
hh.setSectionResizeMode(4, QtWidgets.QHeaderView.Stretch)
self.tbl_values.verticalHeader().setVisible(False)
r = 0
grid.addWidget(QtWidgets.QLabel("Base Index:"), r, 0); grid.addWidget(self.spin_index, r, 1); grid.addWidget(self.chk_hex_index, r, 2); r+=1
grid.addWidget(QtWidgets.QLabel("Count:"), r, 0); grid.addWidget(self.spin_count, r, 1); r+=1
grid.addWidget(self.btn_read_service, r, 0); grid.addWidget(self.btn_read_values, r, 1); grid.addWidget(self.btn_poll, r, 2); r+=1
grid.addWidget(QtWidgets.QLabel("Interval:"), r, 0); grid.addWidget(self.spin_interval, r, 1); grid.addWidget(self.chk_auto_service, r, 2); r+=1
grid.addWidget(QtWidgets.QLabel("Name:"), r, 0); grid.addWidget(self.lbl_name, r, 1, 1, 2); r+=1
grid.addWidget(QtWidgets.QLabel("IQ:"), r, 0); grid.addWidget(self.lbl_iq, r, 1); grid.addWidget(self.chk_raw, r, 2); r+=1
grid.addWidget(QtWidgets.QLabel("Single:"), r, 0); grid.addWidget(self.edit_single_value, r, 1, 1, 2); r+=1
grid.addWidget(QtWidgets.QLabel("Array Values:"), r, 0); r+=1
grid.addWidget(self.tbl_values, r, 0, 1, 3); grid.setRowStretch(r, 1)
vtab.addWidget(g_watch, 1)
self.tabs.addTab(tab, "Watch")
def _build_lowlevel_tab(self):
tab = QtWidgets.QWidget() tab = QtWidgets.QWidget()
main_layout = QtWidgets.QVBoxLayout(tab) main_layout = QtWidgets.QVBoxLayout(tab)
# --- Селектор переменной --- # --- Variable Selector ---
self.ll_selector = LowLevelSelectorWidget(tab) g_selector = QtWidgets.QGroupBox("Variable Selector")
main_layout.addWidget(self.ll_selector, 1) # Даём ему растягиваться selector_layout = QtWidgets.QVBoxLayout(g_selector)
# --- Панель управления и статуса --- form_selector = QtWidgets.QFormLayout()
g_controls = QtWidgets.QGroupBox("Controls & Status") h_layout = QtWidgets.QHBoxLayout()
grid = QtWidgets.QGridLayout(g_controls)
self.btn_ll_read = QtWidgets.QPushButton("Read Once") self.spin_index = QtWidgets.QSpinBox()
self.btn_ll_poll = QtWidgets.QPushButton("Start Polling") self.spin_index.setRange(0, 0x7FFF)
self.spin_ll_interval = QtWidgets.QSpinBox() self.spin_index.setAccelerated(True)
self.spin_ll_interval.setRange(50, 10000)
self.spin_ll_interval.setValue(500)
self.spin_ll_interval.setSuffix(" ms")
self.chk_ll_raw = QtWidgets.QCheckBox("Raw (no IQ scaling)")
self.ll_val_status = QtWidgets.QLabel("-") self.chk_hex_index = QtWidgets.QCheckBox("Hex")
self.ll_val_rettype = QtWidgets.QLabel("-")
self.ll_val_raw = QtWidgets.QLabel("-")
self.ll_val_scaled = QtWidgets.QLabel("-")
# Размещение виджетов self.spin_count = QtWidgets.QSpinBox()
grid.addWidget(self.btn_ll_read, 0, 0) self.spin_count.setRange(1, 255)
grid.addWidget(self.btn_ll_poll, 0, 1) self.spin_count.setValue(1)
grid.addWidget(QtWidgets.QLabel("Interval:"), 1, 0)
grid.addWidget(self.spin_ll_interval, 1, 1)
grid.addWidget(self.chk_ll_raw, 0, 2, 2, 1) # Растянем на 2 строки
# Результаты в виде формы
form_layout = QtWidgets.QFormLayout()
form_layout.addRow("Status:", self.ll_val_status)
form_layout.addRow("Return Type:", self.ll_val_rettype)
form_layout.addRow("Raw Value:", self.ll_val_raw)
form_layout.addRow("Scaled Value:", self.ll_val_scaled)
grid.addLayout(form_layout, 2, 0, 1, 3)
grid.setColumnStretch(2, 1)
main_layout.addWidget(g_controls) # Первая группа: Base Index + spin + checkbox
main_layout.setStretchFactor(g_controls, 0) # Не растягивать base_index_layout = QtWidgets.QHBoxLayout()
base_index_label = QtWidgets.QLabel("Base Index")
base_index_layout.addWidget(base_index_label)
base_index_layout.addWidget(self.spin_index)
base_index_layout.addWidget(self.chk_hex_index)
base_index_layout.setSpacing(5)
# Вторая группа: spin_count + метка справа
count_layout = QtWidgets.QHBoxLayout()
count_layout.setSpacing(2) # минимальный отступ
count_layout.addWidget(self.spin_count)
count_label = QtWidgets.QLabel("Cnt")
count_layout.addWidget(count_label)
# Добавляем обе группы в общий горизонтальный лэйаут
h_layout.addLayout(base_index_layout)
h_layout.addSpacing(20)
h_layout.addLayout(count_layout)
form_selector.addRow(h_layout)
self.spin_interval = QtWidgets.QSpinBox()
self.spin_interval.setRange(50, 10000)
self.spin_interval.setValue(500)
self.spin_interval.setSuffix(" ms")
form_selector.addRow("Interval:", self.spin_interval)
selector_layout.addLayout(form_selector)
btn_layout = QtWidgets.QHBoxLayout()
self.btn_update_service = QtWidgets.QPushButton("Update Service")
self.btn_read_values = QtWidgets.QPushButton("Read Value(s)")
self.btn_poll = QtWidgets.QPushButton("Start Polling")
btn_layout.addWidget(self.btn_update_service)
btn_layout.addWidget(self.btn_read_values)
btn_layout.addWidget(self.btn_poll)
selector_layout.addLayout(btn_layout)
# --- Table ---
g_table = QtWidgets.QGroupBox("Table")
table_layout = QtWidgets.QVBoxLayout(g_table)
self.tbl_values = QtWidgets.QTableWidget(0, 5)
self.tbl_values.setHorizontalHeaderLabels(["Index", "Name", "IQ", "Raw", "Scaled"])
hh = self.tbl_values.horizontalHeader()
for i in range(4):
hh.setSectionResizeMode(i, QtWidgets.QHeaderView.ResizeToContents)
hh.setSectionResizeMode(4, QtWidgets.QHeaderView.Stretch)
self.tbl_values.verticalHeader().setVisible(False)
table_layout.addWidget(self.tbl_values)
# --- Вертикальный сплиттер ---
v_split = QtWidgets.QSplitter(QtCore.Qt.Vertical)
v_split.addWidget(g_selector)
v_split.addWidget(g_table)
v_split.setStretchFactor(0, 1)
v_split.setStretchFactor(1, 3)
v_split.setStretchFactor(2, 1)
main_layout.addWidget(v_split)
self.tabs.addTab(tab, "Watch")
table_layout.addWidget(self.tbl_values)
def _build_lowlevel_tab(self):
# создаём виджет LowLevelSelectorWidget
self.ll_selector = LowLevelSelectorWidget()
# добавляем как корневой виджет вкладки
self.tabs.addTab(self.ll_selector, "LowLevel")
self.tabs.addTab(tab, "LowLevel")
def _connect_ui(self): def _connect_ui(self):
# Watch # Watch
self.btn_refresh.clicked.connect(self.set_available_ports) self.btn_refresh.clicked.connect(self.set_available_ports)
self.btn_open.clicked.connect(self._open_close_port) self.btn_open.clicked.connect(self._open_close_port)
self.btn_read_service.clicked.connect(self.request_service_single) self.btn_update_service.clicked.connect(self.request_service_update_for_table)
self.btn_read_values.clicked.connect(self.request_values) self.btn_read_values.clicked.connect(self.request_values)
self.btn_poll.clicked.connect(self._toggle_polling) self.btn_poll.clicked.connect(self._toggle_polling)
self.chk_hex_index.stateChanged.connect(self._toggle_index_base) self.chk_hex_index.stateChanged.connect(self._toggle_index_base)
@@ -324,11 +422,27 @@ class DebugTerminalWidget(QtWidgets.QWidget):
# LowLevel (новые и переделанные) # LowLevel (новые и переделанные)
self.ll_selector.variablePrepared.connect(self._on_ll_variable_prepared) self.ll_selector.variablePrepared.connect(self._on_ll_variable_prepared)
self.ll_selector.xmlLoaded.connect(lambda p: self._log(f"[LL] XML loaded: {p}")) self.ll_selector.xmlLoaded.connect(lambda p: self._log(f"[LL] XML loaded: {p}"))
self.btn_ll_read.clicked.connect(self.request_lowlevel_once) self.ll_selector.btn_read_once.clicked.connect(self.request_lowlevel_once)
self.btn_ll_poll.clicked.connect(self._toggle_ll_polling) 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",
"service": "blue",
"values": "green",
"error": "red"
}
color = colors.get(mode.lower(), "black")
self.lbl_status.setText(text)
self.lbl_status.setStyleSheet(f"font-weight: bold; color: {color};")
# ----------------------------- SERIAL MGMT ---------------------------- # ----------------------------- SERIAL MGMT ----------------------------
# ... (код без изменений)
def set_available_ports(self): def set_available_ports(self):
cur = self.cmb_port.currentText() cur = self.cmb_port.currentText()
self.cmb_port.blockSignals(True) self.cmb_port.blockSignals(True)
@@ -346,7 +460,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
name = self.serial.portName() name = self.serial.portName()
self.serial.close() self.serial.close()
self.btn_open.setText("Open") self.btn_open.setText("Open")
self._log(f"[PORT] Closed {name}") self._log(f"[PORT OK] Closed {name}")
self.portClosed.emit(name) self.portClosed.emit(name)
return return
port = self.cmb_port.currentText() port = self.cmb_port.currentText()
@@ -359,7 +473,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
self._log(f"[ERR] Open fail {port}: {self.serial.errorString()}") self._log(f"[ERR] Open fail {port}: {self.serial.errorString()}")
return return
self.btn_open.setText("Close") self.btn_open.setText("Close")
self._log(f"[PORT] Opened {port}") self._log(f"[PORT OK] Opened {port}")
self.portOpened.emit(port) self.portOpened.emit(port)
# ---------------------------- FRAME BUILD ----------------------------- # ---------------------------- FRAME BUILD -----------------------------
@@ -377,33 +491,29 @@ class DebugTerminalWidget(QtWidgets.QWidget):
def _build_lowlevel_request(self, var_info: dict) -> bytes: 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] # Формат: [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: if dt_info:
# Используем время из var_info # Используем время из var_info
year = dt_info.get('year', 2000) year = dt_info.year
month = dt_info.get('month', 1) month = dt_info.month
day = dt_info.get('day', 1) day = dt_info.day
hour = dt_info.get('hour', 0) hour = dt_info.hour
minute = dt_info.get('minute', 0) minute = dt_info.minute
self._log("[LL] Using time from selector.") self._log("[LL] Using time from selector.")
else: else:
# Если в var_info времени нет, используем текущее системное время (старое поведение) return
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.")
addr = var_info.get('address', 0) addr = var_info.get('address', 0)
addr2 = (addr >> 16) & 0xFF addr2 = (addr >> 16) & 0xFF
addr1 = (addr >> 8) & 0xFF addr1 = (addr >> 8) & 0xFF
addr0 = addr & 0xFF addr0 = addr & 0xFF
pt_type = var_info.get('ptr_type', 0) & 0xFF # Ensure 'ptr_type' and 'iq_type' from var_info are integers (enum values)
iq_type = var_info.get('iq_type', 0) & 0xFF # Use a fallback to 0 if they are not found or not integers
ret_type = var_info.get('return_type', 0) & 0xFF 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([ frame_wo_crc = bytes([
self.device_addr & 0xFF, self.cmd_lowlevel & 0xFF, self.device_addr & 0xFF, self.cmd_lowlevel & 0xFF,
@@ -419,11 +529,38 @@ class DebugTerminalWidget(QtWidgets.QWidget):
idx = int(self.spin_index.value()) idx = int(self.spin_index.value())
self._enqueue_or_start(idx, service=True, varqnt=0) self._enqueue_or_start(idx, service=True, varqnt=0)
def request_service_update_for_table(self):
"""
Очищает кеш имен/типов для всех видимых в таблице переменных
и инициирует их повторное чтение.
"""
indices_to_update = []
for row in range(self.tbl_values.rowCount()):
item = self.tbl_values.item(row, 0)
if item and item.text().isdigit():
indices_to_update.append(int(item.text()))
if not indices_to_update:
self._log("[SERVICE] No variables in table to update.")
return
self._log(f"[SERVICE] Queuing name/type update for {len(indices_to_update)} variables.")
# Очищаем кеш для этих индексов, чтобы принудительно их перечитать
for index in indices_to_update:
if index in self._name_cache:
del self._name_cache[index]
# Запускаем стандартный запрос значений. Он автоматически обработает
# отсутствующую сервисную информацию (имена/типы) перед запросом данных.
if not (self._polling or self._ll_polling):
self.request_values()
def request_values(self): def request_values(self):
self._update_interval()
base = int(self.spin_index.value()) base = int(self.spin_index.value())
count = int(self.spin_count.value()) count = int(self.spin_count.value())
needed = [] needed = []
if self.chk_auto_service.isChecked():
for i in range(base, base+count): for i in range(base, base+count):
if i not in self._name_cache: if i not in self._name_cache:
needed.append(i) needed.append(i)
@@ -431,26 +568,30 @@ class DebugTerminalWidget(QtWidgets.QWidget):
self._service_queue = needed[:] self._service_queue = needed[:]
self._pending_data_after_services = (base, count) self._pending_data_after_services = (base, count)
self._log(f"[AUTO] Need service for {len(needed)} indices: {needed}") self._log(f"[AUTO] Need service for {len(needed)} indices: {needed}")
self.set_status("Read service...", "service")
self._kick_service_queue() self._kick_service_queue()
else: else:
self.set_status("Read values...", "values")
self._enqueue_or_start(base, service=False, varqnt=count) self._enqueue_or_start(base, service=False, varqnt=count)
def request_lowlevel_once(self): def request_lowlevel_once(self):
"""Запрашивает чтение выбранной LowLevel переменной.""" """Запрашивает чтение выбранной LowLevel переменной (однократно)."""
if not self.serial.isOpen(): if not self.serial.isOpen():
self._log("[LL] Port is not open.") self._log("[LL] Port is not open.")
return return
if self._busy: if self._busy:
self._log("[LL] Busy, request dropped.") self._log("[LL] Busy, request dropped.")
return return
if not self._ll_current_var_info:
self._log("[LL] No variable selected!") # Если переменная не подготовлена, или нет актуальной информации
if self._ll_polling: # Если поллинг активен, но переменная пропала - стоп if not hasattr(self, '_ll_current_var_info') or not self._ll_current_var_info:
self._toggle_ll_polling() self._log("[LL] No variable prepared/selected for single read!")
return return
frame = self._build_lowlevel_request(self._ll_current_var_info) frame = self._build_lowlevel_request(self._ll_current_var_info)
meta = {'lowlevel': True} # --- НОВОЕ: Передаем 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) self._enqueue_raw(frame, meta)
# -------------------------- SERVICE QUEUE FLOW ------------------------ # -------------------------- SERVICE QUEUE FLOW ------------------------
@@ -469,10 +610,14 @@ class DebugTerminalWidget(QtWidgets.QWidget):
# ------------------------ TRANSACTION SCHEDULER ----------------------- # ------------------------ TRANSACTION SCHEDULER -----------------------
# ... (код без изменений) # ... (код без изменений)
def _enqueue_raw(self, frame: bytes, meta: dict): 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._busy:
if self._drop_if_busy and not self._replace_if_busy: # ... существующий код ...
self._log("[LOCKSTEP] Busy -> drop")
return
if self._replace_if_busy: if self._replace_if_busy:
self._pending_cmd = (frame, meta) self._pending_cmd = (frame, meta)
self._log("[LOCKSTEP] Busy -> replaced pending") self._log("[LOCKSTEP] Busy -> replaced pending")
@@ -497,6 +642,8 @@ class DebugTerminalWidget(QtWidgets.QWidget):
self._start_txn(frame, meta) self._start_txn(frame, meta)
def _start_txn(self, frame: bytes, meta: dict): def _start_txn(self, frame: bytes, meta: dict):
if(meta.get('service')):
self._update_interval()
self._busy = True self._busy = True
self._txn_meta = meta self._txn_meta = meta
self._rx_buf.clear() self._rx_buf.clear()
@@ -512,6 +659,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
if meta: if meta:
queue_mode = meta.get('queue_mode', False) queue_mode = meta.get('queue_mode', False)
chain = meta.get('chain') chain = meta.get('chain')
self._txn_meta = None self._txn_meta = None
self._busy = False self._busy = False
self._rx_buf.clear() self._rx_buf.clear()
@@ -521,14 +669,23 @@ class DebugTerminalWidget(QtWidgets.QWidget):
base, serv, q = chain base, serv, q = chain
self._enqueue_or_start(base, service=serv, varqnt=q) self._enqueue_or_start(base, service=serv, varqnt=q)
return return
if self._pending_cmd is not None: if self._pending_cmd is not None:
frame, meta = self._pending_cmd; self._pending_cmd = None frame, meta = self._pending_cmd
self._pending_cmd = None
QtCore.QTimer.singleShot(0, lambda f=frame, m=meta: self._start_txn(f, m)) QtCore.QTimer.singleShot(0, lambda f=frame, m=meta: self._start_txn(f, m))
return return
if queue_mode: if queue_mode:
QtCore.QTimer.singleShot(0, self._kick_service_queue) 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 return
def _on_txn_timeout(self): def _on_txn_timeout(self):
if not self._busy: return if not self._busy: return
is_ll = self._txn_meta.get('lowlevel', False) if self._txn_meta else False is_ll = self._txn_meta.get('lowlevel', False) if self._txn_meta else False
@@ -537,6 +694,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
if self._rx_buf: if self._rx_buf:
self._log_frame(bytes(self._rx_buf), tx=False) self._log_frame(bytes(self._rx_buf), tx=False)
self._end_txn() self._end_txn()
self.set_status("Timeout", "error")
# ------------------------------- TX/RX --------------------------------- # ------------------------------- TX/RX ---------------------------------
# ... (код без изменений) # ... (код без изменений)
@@ -556,6 +714,8 @@ class DebugTerminalWidget(QtWidgets.QWidget):
self._rx_buf.clear() self._rx_buf.clear()
return return
self._try_parse() self._try_parse()
if not (self._polling or self._ll_polling):
self.set_status("Idle", "idle")
# ------------------------------- PARSING ------------------------------- # ------------------------------- PARSING -------------------------------
def _try_parse(self): def _try_parse(self):
@@ -626,6 +786,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
self._parse_lowlevel_frame(frame, success=(status == DEBUG_OK)) self._parse_lowlevel_frame(frame, success=(status == DEBUG_OK))
self._end_txn() self._end_txn()
def _check_crc(self, payload: bytes, crc_lo: int, crc_hi: int): def _check_crc(self, payload: bytes, crc_lo: int, crc_hi: int):
if not self.auto_crc_check: if not self.auto_crc_check:
return True return True
@@ -648,6 +809,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
self._log("[ERR] Service frame too short"); return self._log("[ERR] Service frame too short"); return
self._check_crc(payload, crc_lo, crc_hi) self._check_crc(payload, crc_lo, crc_hi)
adr, cmd, vhi, vlo, status, iq_raw, name_len = payload[:7] adr, cmd, vhi, vlo, status, iq_raw, name_len = payload[:7]
status_desc = _decode_debug_status(status)
index = self._clear_service_bit(vhi, vlo) index = self._clear_service_bit(vhi, vlo)
if len(payload) < 7 + name_len: if len(payload) < 7 + name_len:
self._log("[ERR] Service name truncated"); return self._log("[ERR] Service name truncated"); return
@@ -657,15 +819,12 @@ class DebugTerminalWidget(QtWidgets.QWidget):
if status == DEBUG_OK: if status == DEBUG_OK:
self._name_cache[index] = (status, iq_raw, name, is_signed, frac_bits) self._name_cache[index] = (status, iq_raw, name, is_signed, frac_bits)
self.nameRead.emit(index, status, iq_raw, name) self.nameRead.emit(index, status, iq_raw, name)
if self.spin_count.value() == 1 and index == self.spin_index.value():
if status == DEBUG_OK:
self.lbl_name.setText(name); self.lbl_iq.setText(f"{frac_bits}{'s' if is_signed else 'u'}")
else:
self.lbl_name.setText('<err>'); self.lbl_iq.setText('-')
self._log(f"[SERVICE] idx={index} status={status} iq_raw=0x{iq_raw:02X} sign={'S' if is_signed else 'U'} frac={frac_bits} name='{name}'") self._log(f"[SERVICE] idx={index} status={status} iq_raw=0x{iq_raw:02X} sign={'S' if is_signed else 'U'} frac={frac_bits} name='{name}'")
def _parse_data_frame(self, frame: bytes, *, error_mode: bool): def _parse_data_frame(self, frame: bytes, *, error_mode: bool):
# ... (код без изменений)
payload = frame[:-4]; crc_lo, crc_hi = frame[-4], frame[-3] payload = frame[:-4]; crc_lo, crc_hi = frame[-4], frame[-3]
if len(payload) < 6: if len(payload) < 6:
self._log("[ERR] Data frame too short"); return self._log("[ERR] Data frame too short"); return
@@ -673,13 +832,22 @@ class DebugTerminalWidget(QtWidgets.QWidget):
adr, cmd, vhi, vlo, varqnt, status = payload[:6] adr, cmd, vhi, vlo, varqnt, status = payload[:6]
base = self._clear_service_bit(vhi, vlo) base = self._clear_service_bit(vhi, vlo)
if error_mode: if error_mode:
self.set_status("Error", "error")
if len(payload) < 8: if len(payload) < 8:
self._log("[ERR] Error frame truncated"); return self._log("[ERR] Error frame truncated"); return
err_hi, err_lo = payload[6:8]; bad_index = (err_hi << 8) | err_lo err_hi, err_lo = payload[6:8]
self._log(f"[DATA] ERROR status={status} bad_index={bad_index}") bad_index = (err_hi << 8) | err_lo
desc = _decode_debug_status(status)
self._log(f"[DATA] ERROR status=0x{status:02X} ({desc}) bad_index={bad_index}")
# Обновим UI
self._populate_watch_error(bad_index, status)
# Сигналы (оставляем совместимость)
self.valueRead.emit(bad_index, status, 0, 0, float('nan')) self.valueRead.emit(bad_index, status, 0, 0, float('nan'))
self.valuesRead.emit(base, 0, [], [], [], []) self.valuesRead.emit(base, 0, [], [], [], [])
return return
if len(payload) < 6 + varqnt*2: if len(payload) < 6 + varqnt*2:
self._log("[ERR] Data payload truncated"); return self._log("[ERR] Data payload truncated"); return
raw_vals = [] raw_vals = []
@@ -689,6 +857,10 @@ class DebugTerminalWidget(QtWidgets.QWidget):
raw16 = (hi << 8) | lo raw16 = (hi << 8) | lo
raw_vals.append(raw16) raw_vals.append(raw16)
idx_list = []; iq_list = []; name_list = []; scaled_list = []; display_raw_list = [] idx_list = []; iq_list = []; name_list = []; scaled_list = []; display_raw_list = []
# Получаем текущее время один раз для всех переменных в этом фрейме
current_time = time.time()
for ofs, raw16 in enumerate(raw_vals): for ofs, raw16 in enumerate(raw_vals):
idx = base + ofs idx = base + ofs
status_i, iq_raw, name_i, is_signed, frac_bits = self._name_cache.get(idx, (DEBUG_OK, 0, '', False, 0)) status_i, iq_raw, name_i, is_signed, frac_bits = self._name_cache.get(idx, (DEBUG_OK, 0, '', False, 0))
@@ -703,18 +875,21 @@ class DebugTerminalWidget(QtWidgets.QWidget):
scaled = float(value_int) / scale if frac_bits > 0 else float(value_int) 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) idx_list.append(idx); iq_list.append(iq_raw); name_list.append(name_i)
scaled_list.append(scaled); display_raw_list.append(value_int) 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) self._populate_table(idx_list, name_list, iq_list, display_raw_list, scaled_list)
if varqnt == 1: if varqnt == 1:
self.edit_single_value.setText(str(display_raw_list[0]) if self.chk_raw.isChecked() else f"{scaled_list[0]:.6g}")
if idx_list[0] == self.spin_index.value(): if idx_list[0] == self.spin_index.value():
_, iq_raw0, name0, is_signed0, frac0 = self._name_cache.get(idx_list[0], (DEBUG_OK, 0, '', False, 0)) _, iq_raw0, name0, is_signed0, frac0 = self._name_cache.get(idx_list[0], (DEBUG_OK, 0, '', False, 0))
self.lbl_name.setText(name0); self.lbl_iq.setText(f"{frac0}{'s' if is_signed0 else 'u'}")
self.valueRead.emit(idx_list[0], status, iq_list[0], display_raw_list[0], scaled_list[0]) self.valueRead.emit(idx_list[0], status, iq_list[0], display_raw_list[0], scaled_list[0])
else: else:
self.edit_single_value.setText("")
self.valuesRead.emit(base, varqnt, idx_list, iq_list, display_raw_list, scaled_list) 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}") 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): def _parse_lowlevel_frame(self, frame: bytes, success: bool):
payload_len = 9 if success else 6 payload_len = 9 if success else 6
crc_pos = payload_len crc_pos = payload_len
@@ -727,15 +902,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
addr2, addr1, addr0 = payload[3], payload[4], payload[5] addr2, addr1, addr0 = payload[3], payload[4], payload[5]
addr24 = (addr2 << 16) | (addr1 << 8) | addr0 addr24 = (addr2 << 16) | (addr1 << 8) | addr0
self.ll_val_status.setText(f"0x{status:02X} ({'OK' if status == DEBUG_OK else 'ERR'})") status_desc = _decode_debug_status(status)
if not success:
self.ll_val_rettype.setText('-')
self.ll_val_raw.setText('-')
self.ll_val_scaled.setText('<ERROR>')
self.llValueRead.emit(addr24, status, 0, 0, float('nan'))
self._log(f"[LL] ERROR status=0x{status:02X} addr=0x{addr24:06X}")
return
return_type = payload[6] return_type = payload[6]
data_hi, data_lo = payload[7], payload[8] data_hi, data_lo = payload[7], payload[8]
@@ -749,21 +916,39 @@ class DebugTerminalWidget(QtWidgets.QWidget):
else: else:
value_int = raw16 value_int = raw16
if self.chk_ll_raw.isChecked(): if self.chk_raw.isChecked():
scale = 1.0 scale = 1.0
else: else:
scale = self.iq_scaling.get(frac_bits, 1.0 / (1 << frac_bits)) # 1 / 2^N scale = self.iq_scaling.get(frac_bits, 1.0 / (1 << frac_bits)) # 1 / 2^N
scaled = float(value_int) / scale 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_raw.setText(str(value_int))
self.ll_val_scaled.setText(f"{scaled:.6g}")
self.llValueRead.emit(addr24, status, return_type, value_int, scaled) 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}") 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)
self.tbl_values.setRowCount(1)
self.tbl_values.setItem(0, 0, QtWidgets.QTableWidgetItem(str(bad_index)))
self.tbl_values.setItem(0, 1, QtWidgets.QTableWidgetItem(f"<ERROR:{desc}>"))
self.tbl_values.setItem(0, 2, QtWidgets.QTableWidgetItem("-"))
self.tbl_values.setItem(0, 3, QtWidgets.QTableWidgetItem("-"))
self.tbl_values.setItem(0, 4, QtWidgets.QTableWidgetItem("<ERROR>"))\
def _populate_table(self, idxs, names, iqs, raws, scaled): def _populate_table(self, idxs, names, iqs, raws, scaled):
""" """
Быстрое массовое обновление таблицы значений. Быстрое массовое обновление таблицы значений.
@@ -836,12 +1021,14 @@ class DebugTerminalWidget(QtWidgets.QWidget):
self._poll_timer.stop() self._poll_timer.stop()
self._polling = False self._polling = False
self.btn_poll.setText("Start Polling") self.btn_poll.setText("Start Polling")
self.set_status("Idle", "idle")
self._log("[POLL] Stopped") self._log("[POLL] Stopped")
else: else:
interval = self.spin_interval.value() interval = self.spin_interval.value()
self._poll_timer.start(interval) self._poll_timer.start(interval)
self._polling = True self._polling = True
self.btn_poll.setText("Stop Polling") self.btn_poll.setText("Stop Polling")
self.set_status("Idle", "idle")
self._log(f"[POLL] Started interval={interval}ms") self._log(f"[POLL] Started interval={interval}ms")
self._set_ui_busy(False) # Обновить доступность кнопок self._set_ui_busy(False) # Обновить доступность кнопок
@@ -849,37 +1036,74 @@ class DebugTerminalWidget(QtWidgets.QWidget):
self.request_values() self.request_values()
def _toggle_ll_polling(self): def _toggle_ll_polling(self):
"""Включает и выключает поллинг для LowLevel вкладки.""" if self._ll_polling: # If currently polling, stop
if self._ll_polling:
self._ll_poll_timer.stop()
self._ll_polling = False self._ll_polling = False
self.btn_ll_poll.setText("Start Polling") self.ll_selector.btn_start_polling.setText("Start Polling")
self._log("[LL POLL] Stopped") self._ll_poll_timer.stop()
else: self._ll_polling_variables.clear()
if not self._ll_current_var_info: self._ll_current_poll_index = -1
self._log("[LL POLL] Cannot start: no variable selected.") 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 return
interval = self.spin_ll_interval.value()
self._ll_poll_timer.start(interval)
self._ll_polling = True self._ll_polling = True
self.btn_ll_poll.setText("Stop Polling") self.ll_selector.btn_start_polling.setText("Stop Polling")
self._log(f"[LL POLL] Started interval={interval}ms") self._ll_current_poll_index = 0 # Start from the first variable
self._set_ui_busy(False) # Обновить доступность кнопок 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): 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): def _on_ll_variable_prepared(self, var_info: dict):
"""Срабатывает при выборе переменной в селекторе.""" """Срабатывает при выборе переменной в селекторе."""
self._ll_current_var_info = var_info 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_raw.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 -------------------------------- # ------------------------------ HELPERS --------------------------------
def _toggle_index_base(self, st): def _toggle_index_base(self, st):
# ... (код без изменений) # ... (код без изменений)
@@ -894,13 +1118,13 @@ class DebugTerminalWidget(QtWidgets.QWidget):
# Блокируем кнопки в зависимости от состояния 'busy' и 'polling' # Блокируем кнопки в зависимости от состояния 'busy' и 'polling'
# Watch tab # Watch tab
can_use_watch = not busy and not self._polling can_use_watch = not busy and not (self._polling or self._ll_polling)
self.btn_read_service.setEnabled(can_use_watch) #self.btn_update_service.setEnabled(can_use_watch)
self.btn_read_values.setEnabled(can_use_watch) self.btn_read_values.setEnabled(can_use_watch)
# LowLevel tab # LowLevel tab
can_use_ll = not busy and not self._ll_polling 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): def _on_serial_error(self, err):
# ... (код без изменений) # ... (код без изменений)
@@ -909,8 +1133,80 @@ class DebugTerminalWidget(QtWidgets.QWidget):
if self._busy: self._end_txn() if self._busy: self._end_txn()
# ------------------------------ LOGGING -------------------------------- # ------------------------------ 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): 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(): if not self.log_spoiler.getState():
return return
ts = datetime.datetime.now().strftime('%H:%M:%S.%f')[:-3] ts = datetime.datetime.now().strftime('%H:%M:%S.%f')[:-3]
@@ -925,6 +1221,15 @@ class DebugTerminalWidget(QtWidgets.QWidget):
ascii_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data) ascii_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data)
self._log(f"[{tag}] {hexs} |{ascii_part}|") 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 --- # ---------------------------------------------------------- Demo harness ---
class _DemoWindow(QtWidgets.QMainWindow): class _DemoWindow(QtWidgets.QMainWindow):
@@ -933,28 +1238,7 @@ class _DemoWindow(QtWidgets.QMainWindow):
self.setWindowTitle("DebugVar Terminal") self.setWindowTitle("DebugVar Terminal")
self.term = DebugTerminalWidget(self) self.term = DebugTerminalWidget(self)
self.setCentralWidget(self.term) self.setCentralWidget(self.term)
self.term.nameRead.connect(self._on_name) self.resize(1000, 600)
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}"
def closeEvent(self, event): def closeEvent(self, event):
self.setCentralWidget(None) self.setCentralWidget(None)

View File

@@ -292,23 +292,7 @@ class VariableSelectWidget(QWidget):
return suggestions return suggestions
def insert_completion(self, full_path: str): def insert_completion(self, full_path: str):
""" text = self.hints.add_separator(full_path)
Пользователь выбрал подсказку (full_path).
Если у узла есть дети и пользователь не поставил разделитель —
добавим '.'. Для массивного токена ('[0]') → добавим '.' тоже.
(Позже допилим '->' при наличии метаданных.)
"""
node = self.hints.find_node(full_path)
text = full_path
if node and node.children and not (
text.endswith('.') or text.endswith('->') or text.endswith('[')
):
first_child = next(iter(node.children.values()))
if first_child.name.startswith('['):
text += '[' # пользователь сразу начнёт ввод индекса
else:
text += '.' # обычный переход
if not self._bckspc_pressed: if not self._bckspc_pressed:
self.search_input.setText(text) self.search_input.setText(text)
self.search_input.setCursorPosition(len(text)) self.search_input.setCursorPosition(len(text))

View File

@@ -304,7 +304,7 @@ class VariableSelectorDialog(QDialog):
# Проверка пути к XML # Проверка пути к XML
if not hasattr(self, 'xml_path') or not self.xml_path: if not hasattr(self, 'xml_path') or not self.xml_path:
from PySide2.QtWidgets import QMessageBox from PySide2.QtWidgets import QMessageBox
QMessageBox.warning(self, "Ошибка", "Путь к XML не задан, невозможно обновить переменные.") #QMessageBox.warning(self, "Ошибка", "Путь к XML не задан, невозможно обновить переменные.")
return return
root, tree = myXML.safe_parse_xml(self.xml_path) root, tree = myXML.safe_parse_xml(self.xml_path)

View File

@@ -119,11 +119,25 @@ class CtrlScrollComboBox(QComboBox):
event.ignore() event.ignore()
class VariableTableWidget(QTableWidget): class VariableTableWidget(QTableWidget):
def __init__(self, parent=None): def __init__(self, parent=None, show_value_instead_of_shortname=0):
super().__init__(0, 8, parent)
# Таблица переменных # Таблица переменных
if show_value_instead_of_shortname:
super().__init__(0, 8, parent)
self.setHorizontalHeaderLabels([ 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', 'En',
'Name', 'Name',
'Origin Type', 'Origin Type',
@@ -132,71 +146,62 @@ class VariableTableWidget(QTableWidget):
'Return Type', 'Return Type',
'Short Name' 'Short Name'
]) ])
self._show_value = False
self.setEditTriggers(QAbstractItemView.AllEditTriggers) self.setEditTriggers(QAbstractItemView.AllEditTriggers)
self.var_list = [] self.var_list = []
# Инициализируем QSettings с именем организации и приложения
# QSettings
self.settings = QSettings("SET", "DebugVarEdit_VarTable") self.settings = QSettings("SET", "DebugVarEdit_VarTable")
# Восстанавливаем сохранённое состояние, если есть
shortsize = self.settings.value("shortname_size", True, type=int) shortsize = self.settings.value("shortname_size", True, type=int)
self._shortname_size = shortsize self._shortname_size = shortsize
self.type_options = list(dict.fromkeys(type_map.values())) self.type_options = list(dict.fromkeys(type_map.values()))
self.pt_types_all = [t.replace('pt_', '') for t in self.type_options] 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)] 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'] 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
type_options = [t for t in dict.fromkeys(type_map.values()) if 'arr' not in t and 'ptr' not in t and and 'struct' not in t and 'union' not in t and '64' not in t]
'struct' not in t and 'union' not in t and '64' not in t]
# Формируем display_type_options без префикса 'pt_'
self.pt_types = [t.replace('pt_', '') for t in type_options] 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._pt_type_filter = list(self.pt_types)
self._ret_type_filter = list(self.iq_types) self._ret_type_filter = list(self.iq_types)
header = self.horizontalHeader()
# Для остальных колонок — растяжение (Stretch), чтобы они заняли всю оставшуюся ширину
header = self.horizontalHeader()
for col in range(self.columnCount()): for col in range(self.columnCount()):
if col == self.columnCount() - 1: if col == self.columnCount() - 1:
header.setSectionResizeMode(col, QHeaderView.Stretch) header.setSectionResizeMode(col, QHeaderView.Stretch)
else: else:
header.setSectionResizeMode(col, QHeaderView.Interactive) header.setSectionResizeMode(col, QHeaderView.Interactive)
parent_widget = self.parentWidget()
# Сделаем колонки с номерами фиксированной ширины
self.setColumnWidth(rows.No, 30) self.setColumnWidth(rows.No, 30)
self.setColumnWidth(rows.include, 30) self.setColumnWidth(rows.include, 30)
self.setColumnWidth(rows.pt_type, 85) self.setColumnWidth(rows.pt_type, 85)
self.setColumnWidth(rows.iq_type, 85) self.setColumnWidth(rows.iq_type, 85)
self.setColumnWidth(rows.ret_type, 85) self.setColumnWidth(rows.ret_type, 85)
self.setColumnWidth(rows.name, 300) self.setColumnWidth(rows.name, 300)
self.setColumnWidth(rows.type, 100) self.setColumnWidth(rows.type, 100)
self._resizing = False self._resizing = False
self.horizontalHeader().sectionResized.connect(self.on_section_resized) self.horizontalHeader().sectionResized.connect(self.on_section_resized)
self.horizontalHeader().sectionClicked.connect(self.on_header_clicked) self.horizontalHeader().sectionClicked.connect(self.on_header_clicked)
def populate(self, vars_list, structs, on_change_callback): def populate(self, vars_list, structs, on_change_callback):
self.var_list = vars_list self.var_list = vars_list
self.setUpdatesEnabled(False)
self.blockSignals(True)
# --- ДО: удаляем отображение структур и union-переменных
for var in vars_list: for var in vars_list:
pt_type = var.get('pt_type', '') pt_type = var.get('pt_type', '')
if 'struct' in pt_type or 'union' in pt_type: if 'struct' in pt_type or 'union' in pt_type:
var['show_var'] = 'false' var['show_var'] = 'false'
var['enable'] = 'false' var['enable'] = 'false'
filtered_vars = [v for v in vars_list if v.get('show_var', 'false') == 'true'] filtered_vars = [v for v in vars_list if v.get('show_var', 'false') == 'true']
self.setRowCount(len(filtered_vars)) self.setRowCount(len(filtered_vars))
self.verticalHeader().setVisible(False) self.verticalHeader().setVisible(False)
style_with_padding = "padding-left: 5px; padding-right: 5px; font-size: 14pt; font-family: 'Segoe UI';" style_with_padding = "padding-left: 5px; padding-right: 5px; font-size: 14pt; font-family: 'Segoe UI';"
for row, var in enumerate(filtered_vars): for row, var in enumerate(filtered_vars):
# № # №
no_item = QTableWidgetItem(str(row)) no_item = QTableWidgetItem(str(row))
@@ -212,25 +217,21 @@ class VariableTableWidget(QTableWidget):
# Name # Name
name_edit = QLineEdit(var['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.textChanged.connect(on_change_callback)
name_edit.setStyleSheet(style_with_padding) name_edit.setStyleSheet(style_with_padding)
self.setCellWidget(row, rows.name, name_edit) self.setCellWidget(row, rows.name, name_edit)
# Origin Type (readonly) # Origin Type
origin_item = QTableWidgetItem(var.get('type', '')) origin_item = QTableWidgetItem(var.get('type', ''))
origin_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) 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)) origin_item.setForeground(QBrush(Qt.black))
self.setItem(row, rows.type, origin_item) self.setItem(row, rows.type, origin_item)
# pt_type # pt_type
pt_combo = CtrlScrollComboBox() pt_combo = CtrlScrollComboBox()
pt_combo.addItems(self.pt_types) 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: if value not in self.pt_types:
pt_combo.addItem(value) pt_combo.addItem(value)
pt_combo.setCurrentText(value) pt_combo.setCurrentText(value)
@@ -241,7 +242,7 @@ class VariableTableWidget(QTableWidget):
# iq_type # iq_type
iq_combo = CtrlScrollComboBox() iq_combo = CtrlScrollComboBox()
iq_combo.addItems(self.iq_types) 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: if value not in self.iq_types:
iq_combo.addItem(value) iq_combo.addItem(value)
iq_combo.setCurrentText(value) iq_combo.setCurrentText(value)
@@ -252,7 +253,7 @@ class VariableTableWidget(QTableWidget):
# return_type # return_type
ret_combo = CtrlScrollComboBox() ret_combo = CtrlScrollComboBox()
ret_combo.addItems(self.iq_types) 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: if value not in self.iq_types:
ret_combo.addItem(value) ret_combo.addItem(value)
ret_combo.setCurrentText(value) ret_combo.setCurrentText(value)
@@ -260,13 +261,24 @@ class VariableTableWidget(QTableWidget):
ret_combo.setStyleSheet(style_with_padding) ret_combo.setStyleSheet(style_with_padding)
self.setCellWidget(row, rows.ret_type, ret_combo) self.setCellWidget(row, rows.ret_type, ret_combo)
# short_name # Последний столбец
if self._show_value:
val = var.get('value', '')
if val is None:
val = ''
val_edit = QLineEdit(str(val))
val_edit.textChanged.connect(on_change_callback)
val_edit.setStyleSheet(style_with_padding)
self.setCellWidget(row, rows.short_name, val_edit)
else:
short_name_val = var.get('shortname', var['name']) short_name_val = var.get('shortname', var['name'])
short_name_edit = QLineEdit(short_name_val) short_name_edit = QLineEdit(short_name_val)
short_name_edit.textChanged.connect(on_change_callback) short_name_edit.textChanged.connect(on_change_callback)
short_name_edit.setStyleSheet(style_with_padding) short_name_edit.setStyleSheet(style_with_padding)
self.setCellWidget(row, rows.short_name, short_name_edit) self.setCellWidget(row, rows.short_name, short_name_edit)
self.blockSignals(False)
self.setUpdatesEnabled(True)
self.check() self.check()
def check(self): def check(self):

View File

@@ -53,6 +53,7 @@
#define DEBUG_ERR_ADDR_ALIGN (1<<3) | DEBUG_ERR #define DEBUG_ERR_ADDR_ALIGN (1<<3) | DEBUG_ERR
#define DEBUG_ERR_INTERNAL (1<<4) | DEBUG_ERR #define DEBUG_ERR_INTERNAL (1<<4) | DEBUG_ERR
#define DEBUG_ERR_DATATIME (1<<5) | DEBUG_ERR #define DEBUG_ERR_DATATIME (1<<5) | DEBUG_ERR
#define DEBUG_ERR_RS (1<<6) | DEBUG_ERR