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