From 96496a0256ac85dbe72f36a2c0dea67671ea3db5 Mon Sep 17 00:00:00 2001 From: Razvalyaev Date: Mon, 21 Jul 2025 13:40:52 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B2=D1=81=D0=B5=20=D0=BD=D0=B5=D0=BF=D0=BB?= =?UTF-8?q?=D0=BE=D1=85=D0=BE=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=B5?= =?UTF-8?q?=D1=82.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit сейв перед попыткой улучшить lowlevel debug --- Src/path_hints.py | 35 ++++ Src/tms_debugvar_lowlevel.py | 157 ++++++++++------ Src/tms_debugvar_term.py | 335 ++++++++++++++++++++++++++--------- Src/var_selector_table.py | 18 +- Src/var_selector_window.py | 2 +- debug_tools.h | 1 + 6 files changed, 393 insertions(+), 155 deletions(-) diff --git a/Src/path_hints.py b/Src/path_hints.py index 74a9bc5..70179d1 100644 --- a/Src/path_hints.py +++ b/Src/path_hints.py @@ -92,6 +92,11 @@ class PathNode: def add_child(self, child: "PathNode") -> None: self.children[child.name] = child + def get_children(self) -> List["PathNode"]: + """ + Вернуть список дочерних узлов, отсортированных по имени. + """ + return sorted(self.children.values(), key=lambda n: n.name) class PathHints: """ @@ -173,6 +178,15 @@ class PathHints: def find_node(self, path: str) -> Optional[PathNode]: 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, @@ -226,6 +240,27 @@ class PathHints: if prefix_last == '' or prefix_last in child.name.lower(): res.append(child.full_path) 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 + # ------------ внутренние вспомогательные ------------ diff --git a/Src/tms_debugvar_lowlevel.py b/Src/tms_debugvar_lowlevel.py index cd99366..3a84669 100644 --- a/Src/tms_debugvar_lowlevel.py +++ b/Src/tms_debugvar_lowlevel.py @@ -7,7 +7,7 @@ LowLevelSelectorWidget (PySide2) * Построения плоского списка путей (имя/подпуть) с расчётом абсолютного адреса (base_address + offset) * Определения структур с полями даты (year, month, day, hour, minute) * Выбора переменной и (опционально) переменной даты / ручного ввода даты - * Выбора типов: ptr_type (pt_*), iq_type, return_type + * Выбора типов: ptr_type , iq_type, return_type * Форматирования адреса в виде 0x000000 (6 HEX) * Генерации словаря/кадра для последующей LowLevel-команды (не отправляет сам) @@ -17,7 +17,7 @@ LowLevelSelectorWidget (PySide2) { 'address': int, 'address_hex': str, # '0x....' - 'ptr_type': int, # значение enum pt_* + 'ptr_type': int, # значение enum * 'iq_type': int, 'return_type': int, 'datetime': { @@ -40,33 +40,23 @@ from dataclasses import dataclass, field from typing import List, Dict, Optional, Tuple from PySide2 import QtCore, QtGui, QtWidgets from path_hints import PathHints +from generate_debug_vars import choose_type_map, type_map # ------------------------------------------------------------ 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', -} +PTR_TYPE_MAP = type_map PT_ENUM_ORDER = [ - 'pt_unknown','pt_int8','pt_int16','pt_int32','pt_int64', - 'pt_uint8','pt_uint16','pt_uint32','pt_uint64','pt_float', - 'pt_struct','pt_union' + 'unknown','int8','int16','int32','int64', + 'uint8','uint16','uint32','uint64','float', + 'struct','union' ] IQ_ENUM_ORDER = [ - 't_iq_none','t_iq','t_iq1','t_iq2','t_iq3','t_iq4','t_iq5','t_iq6', - 't_iq7','t_iq8','t_iq9','t_iq10','t_iq11','t_iq12','t_iq13','t_iq14', - 't_iq15','t_iq16','t_iq17','t_iq18','t_iq19','t_iq20','t_iq21','t_iq22', - 't_iq23','t_iq24','t_iq25','t_iq26','t_iq27','t_iq28','t_iq29','t_iq30' + 'iq_none','iq','iq1','iq2','iq3','iq4','iq5','iq6', + 'iq7','iq8','iq9','iq10','iq11','iq12','iq13','iq14', + 'iq15','iq16','iq17','iq18','iq19','iq20','iq21','iq22', + 'iq23','iq24','iq25','iq26','iq27','iq28','iq29','iq30' ] # Для примера: маппинг имени enum -> числовое значение (индекс по порядку) @@ -133,6 +123,7 @@ class VariablesXML: self.path = path self.timestamp: str = '' self.variables: List[VariableNode] = [] + choose_type_map(0) self._parse() # ------------------ low helpers ------------------ @@ -242,23 +233,26 @@ class VariablesXML: 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 + count: Optional[int], + base_type: Optional[str], + node_children: Optional[List[MemberNode]]) -> int: + # 1) size_bytes/count if size_bytes and count and count > 0: - stride = size_bytes // count - if stride * count != size_bytes: - # округлённо вверх - stride = (size_bytes + count - 1) // count - if stride <= 0: - stride = 1 - return stride - # 2) размер примитива по типу + if size_bytes % count == 0: + stride = size_bytes // count + if stride <= 0: + stride = 1 + return stride + else: + # size не кратен count → скорее всего size = размер одного элемента + return max(size_bytes, 1) + + # 2) попытка по типу (примитив) if base_type: gs = self._guess_primitive_size(base_type) if gs: return gs + # 3) попытка по детям (структура) if node_children: min_off = min(ch.offset for ch in node_children) @@ -273,8 +267,10 @@ class VariablesXML: stride = max_end - min_off if stride > 0: return stride + return 1 + def expand_members(prefix_name: str, base_addr: int, members: List[MemberNode], @@ -393,6 +389,7 @@ class LowLevelSelectorWidget(QtWidgets.QWidget): self._paths = [] self._path_info = {} self._addr_index = {} + self._backspace_pressed = False self._hints = PathHints() self._build_ui() self._connect() @@ -417,7 +414,7 @@ class LowLevelSelectorWidget(QtWidgets.QWidget): # --- Search field for variable --- self.edit_var_search = QtWidgets.QLineEdit() - self.edit_var_search.setPlaceholderText("Введите имя/путь или адрес 0x......") + self.edit_var_search.setPlaceholderText("Введите имя переменной или адрес 0x......") form.addRow('Variable:', self.edit_var_search) # Popup list @@ -447,15 +444,15 @@ class LowLevelSelectorWidget(QtWidgets.QWidget): box.addWidget(QtWidgets.QLabel(label, alignment=QtCore.Qt.AlignHCenter)) box.addWidget(w) dt_row.addLayout(box) - form.addRow('Manual Date:', dt_row) + form.addRow('Compile Date:', dt_row) # Types self.cmb_ptr_type = QtWidgets.QComboBox(); self.cmb_ptr_type.addItems(PT_ENUM_ORDER) self.cmb_iq_type = QtWidgets.QComboBox(); self.cmb_iq_type.addItems(IQ_ENUM_ORDER) self.cmb_return_type = QtWidgets.QComboBox(); self.cmb_return_type.addItems(IQ_ENUM_ORDER) - form.addRow('ptr_type:', self.cmb_ptr_type) - form.addRow('iq_type:', self.cmb_iq_type) - form.addRow('return_type:', self.cmb_return_type) + form.addRow('Type:', self.cmb_ptr_type) + form.addRow('IQ Type:', self.cmb_iq_type) + form.addRow('Return IQ Type:', self.cmb_return_type) lay.addLayout(form) @@ -478,7 +475,16 @@ class LowLevelSelectorWidget(QtWidgets.QWidget): self.btn_prepare.clicked.connect(self._emit_variable) self.edit_var_search.textEdited.connect(self._on_var_search_edited) self.edit_var_search.returnPressed.connect(self._activate_current_popup_selection) + QtWidgets.QApplication.instance().focusChanged.connect(self._on_focus_changed) + + def focusOutEvent(self, event): + super().focusOutEvent(event) + self._hide_popup() + def _on_focus_changed(self, old, now): + # Если фокус ушёл из нашего виджета — скрываем подсказки + if now is None or not self.isAncestorOf(now): + self._hide_popup() # ---------------- XML Load ---------------- def _on_load_xml(self): path, _ = QtWidgets.QFileDialog.getOpenFileName( @@ -555,7 +561,7 @@ class LowLevelSelectorWidget(QtWidgets.QWidget): limit = 400 added = 0 for p in paths: - info = self._path_info.get(p) + info = self._path_info.get(str(p)) if not info: continue addr, t = info @@ -589,7 +595,13 @@ class LowLevelSelectorWidget(QtWidgets.QWidget): self._popup.hide() def _on_var_search_edited(self, text: str): + # Показываем подсказки при вводе текста, если не было Backspace (чтобы не добавлять разделитель) t = text.strip() + if self._backspace_pressed: + # При стирании не показываем автодополнение с разделителем + self._backspace_pressed = False + self._hide_popup() + return # адрес? if t.startswith("0x") and len(t) >= 3: @@ -608,13 +620,26 @@ class LowLevelSelectorWidget(QtWidgets.QWidget): 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() + path = idx.data(QtCore.Qt.UserRole + 1) + if not path: + return + + # Реализуем автодополнение по цепочке, пока подсказка имеет детей + current_path = path + + children = self._hints.get_children(str(current_path)) + if children: + current_path = self._hints.add_separator(current_path) + + self._set_current_variable(str(current_path)) # <-- здесь + + if not children: + self._hide_popup() + def _activate_current_popup_selection(self): if self._popup.isVisible(): @@ -629,28 +654,54 @@ class LowLevelSelectorWidget(QtWidgets.QWidget): 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): + key = ev.key() + if key in (QtCore.Qt.Key_Down, QtCore.Qt.Key_Up): if not self._popup.isVisible(): self._show_popup() else: - step = 1 if ev.key()==QtCore.Qt.Key_Down else -1 + step = 1 if key == QtCore.Qt.Key_Down else -1 cur = self._popup.currentIndex() row = cur.row() + step if row < 0: row = 0 if row >= self._model_filtered.rowCount(): - row = self._model_filtered.rowCount()-1 - self._popup.setCurrentIndex(self._model_filtered.index(row,0)) + row = self._model_filtered.rowCount() - 1 + self._popup.setCurrentIndex(self._model_filtered.index(row, 0)) return True - elif ev.key() == QtCore.Qt.Key_Escape: + + elif key == QtCore.Qt.Key_Escape: self._hide_popup() return True + + elif key == QtCore.Qt.Key_Backspace: + # Помечаем, что была нажата Backspace + self._backspace_pressed = True + return False # дальше обрабатываем обычным образом + + elif key == QtCore.Qt.Key_Space and (ev.modifiers() & QtCore.Qt.ControlModifier): + # Ctrl+Space — показать подсказки + text = self.edit_var_search.text() + suggestions = self._hints.suggest(text) + self._update_popup_model(suggestions) + self._show_popup() + return True + + else: + # Любая другая клавиша — сбрасываем Backspace-флаг + self._backspace_pressed = False + return super().eventFilter(obj, ev) + def _set_current_variable(self, path: str, from_address=False): + self.edit_var_search.setText(path) + self._update_popup_model(self._hints.suggest(path)) + self._show_popup() if path not in self._path_info: return addr, type_str = self._path_info[path] - self.edit_var_search.setText(path) + + # Разделитель добавляем только если не стираем (Backspace), и если уже не добавлен + # В этой функции не будем добавлять разделитель, добавляем его только при автодополнении выше self.edit_address.setText(f"0x{addr:06X}") ptr_enum_name = self._map_type_to_ptr_enum(type_str) self._select_combo_text(self.cmb_ptr_type, ptr_enum_name) @@ -664,13 +715,13 @@ class LowLevelSelectorWidget(QtWidgets.QWidget): def _map_type_to_ptr_enum(self, type_str: str) -> str: if not type_str: - return 'pt_unknown' + return 'unknown' low = type_str.lower() - token = low.replace('*',' ').replace('[',' ').split()[0] - return PTR_TYPE_MAP.get(token, 'pt_unknown') + token = low.replace('*',' ').replace('[',' ') + return PTR_TYPE_MAP.get(token, 'unknown') def _select_combo_text(self, combo: QtWidgets.QComboBox, text: str): - ix = combo.findText(text) + ix = combo.findText(text.replace('pt_', '')) if ix >= 0: combo.setCurrentIndex(ix) diff --git a/Src/tms_debugvar_term.py b/Src/tms_debugvar_term.py index 0cbf70f..c054a36 100644 --- a/Src/tms_debugvar_term.py +++ b/Src/tms_debugvar_term.py @@ -1,12 +1,36 @@ from PySide2 import QtCore, QtWidgets, QtSerialPort from tms_debugvar_lowlevel import LowLevelSelectorWidget import datetime +import time # ------------------------------- Константы протокола ------------------------ WATCH_SERVICE_BIT = 0x8000 DEBUG_OK = 0 # ожидаемый код успешного чтения SIGN_BIT_MASK = 0x80 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 --- def crc16_ibm(data: bytes, *, init=0xFFFF) -> int: """CRC16-IBM (aka CRC-16/ANSI, polynomial 0xA001 reflected).""" @@ -20,6 +44,26 @@ def crc16_ibm(data: bytes, *, init=0xFFFF) -> int: crc >>= 1 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): def __init__(self, title="", animationDuration=300, parent=None): super().__init__(parent) @@ -131,7 +175,7 @@ class DebugTerminalWidget(QtWidgets.QWidget): self.auto_crc_check = auto_crc_check self._drop_if_busy = drop_if_busy self._replace_if_busy = replace_if_busy - + self._last_txn_timestamp = 0 if iq_scaling is None: iq_scaling = {n: float(1 << n) for n in range(31)} iq_scaling[0] = 1.0 @@ -200,6 +244,22 @@ class DebugTerminalWidget(QtWidgets.QWidget): self.tabs = QtWidgets.QTabWidget() self._build_watch_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) # --- UART Log --- self.log_spoiler = Spoiler("UART Log", animationDuration=300, parent=self) @@ -211,71 +271,116 @@ class DebugTerminalWidget(QtWidgets.QWidget): log_layout.addWidget(self.txt_log) self.log_spoiler.setContentLayout(log_layout) - layout.addWidget(g_serial, 0) + layout.addWidget(g_serial) layout.addWidget(self.tabs, 1) - layout.addWidget(self.log_spoiler, 0) + layout.addWidget(g_control) + layout.addWidget(self.log_spoiler) layout.setStretch(layout.indexOf(g_serial), 0) layout.setStretch(layout.indexOf(self.tabs), 1) - layout.setStretch(layout.indexOf(self.log_spoiler), 0) + def _build_watch_tab(self): - # ... (код для вкладки Watch остаётся без изменений) tab = QtWidgets.QWidget() - vtab = QtWidgets.QVBoxLayout(tab) + main_layout = QtWidgets.QVBoxLayout(tab) - g_watch = QtWidgets.QGroupBox("Watch Variables") - grid = QtWidgets.QGridLayout(g_watch) - grid.setHorizontalSpacing(8) - grid.setVerticalSpacing(4) + # --- Variable Selector --- + g_selector = QtWidgets.QGroupBox("Variable Selector") + selector_layout = QtWidgets.QVBoxLayout(g_selector) + + form_selector = QtWidgets.QFormLayout() + h_layout = QtWidgets.QHBoxLayout() + + self.spin_index = QtWidgets.QSpinBox() + self.spin_index.setRange(0, 0x7FFF) + self.spin_index.setAccelerated(True) - 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.spin_count = QtWidgets.QSpinBox() + self.spin_count.setRange(1, 255) + self.spin_count.setValue(1) + + + + # Первая группа: Base Index + spin + checkbox + 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") - 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) + 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"]) + 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) + 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) - 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) + # --- Вертикальный сплиттер --- + 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) - vtab.addWidget(g_watch, 1) + main_layout.addWidget(v_split) self.tabs.addTab(tab, "Watch") + + table_layout.addWidget(self.tbl_values) + + def _build_lowlevel_tab(self): tab = QtWidgets.QWidget() main_layout = QtWidgets.QVBoxLayout(tab) - # --- Селектор переменной --- - self.ll_selector = LowLevelSelectorWidget(tab) - main_layout.addWidget(self.ll_selector, 1) # Даём ему растягиваться - - # --- Панель управления и статуса --- - g_controls = QtWidgets.QGroupBox("Controls & Status") - grid = QtWidgets.QGridLayout(g_controls) + # --- 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") @@ -283,40 +388,44 @@ class DebugTerminalWidget(QtWidgets.QWidget): 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.ll_val_rettype = QtWidgets.QLabel("-") - self.ll_val_raw = 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) - 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) - main_layout.setStretchFactor(g_controls, 0) # Не растягивать + # Поле 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") + def _connect_ui(self): # Watch self.btn_refresh.clicked.connect(self.set_available_ports) 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_poll.clicked.connect(self._toggle_polling) self.chk_hex_index.stateChanged.connect(self._toggle_index_base) @@ -327,8 +436,18 @@ class DebugTerminalWidget(QtWidgets.QWidget): self.btn_ll_read.clicked.connect(self.request_lowlevel_once) self.btn_ll_poll.clicked.connect(self._toggle_ll_polling) + 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 ---------------------------- - # ... (код без изменений) def set_available_ports(self): cur = self.cmb_port.currentText() self.cmb_port.blockSignals(True) @@ -419,20 +538,46 @@ class DebugTerminalWidget(QtWidgets.QWidget): idx = int(self.spin_index.value()) 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] + + # Запускаем стандартный запрос значений. Он автоматически обработает + # отсутствующую сервисную информацию (имена/типы) перед запросом данных. + #self.request_values() + def request_values(self): base = int(self.spin_index.value()) count = int(self.spin_count.value()) needed = [] - if self.chk_auto_service.isChecked(): - for i in range(base, base+count): - if i not in self._name_cache: - needed.append(i) + for i in range(base, base+count): + if i not in self._name_cache: + needed.append(i) if needed: 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._kick_service_queue() else: + self.set_status("read values...", "values") self._enqueue_or_start(base, service=False, varqnt=count) def request_lowlevel_once(self): @@ -451,6 +596,7 @@ class DebugTerminalWidget(QtWidgets.QWidget): frame = self._build_lowlevel_request(self._ll_current_var_info) meta = {'lowlevel': True} + self.set_status("read lowlevel...", "values") self._enqueue_raw(frame, meta) # -------------------------- SERVICE QUEUE FLOW ------------------------ @@ -496,7 +642,15 @@ class DebugTerminalWidget(QtWidgets.QWidget): return self._start_txn(frame, meta) - def _start_txn(self, frame: bytes, meta: dict): + 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 + self._busy = True self._txn_meta = meta self._rx_buf.clear() @@ -561,6 +715,7 @@ class DebugTerminalWidget(QtWidgets.QWidget): 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: @@ -648,6 +803,7 @@ class DebugTerminalWidget(QtWidgets.QWidget): self._log("[ERR] Service frame too short"); return self._check_crc(payload, crc_lo, crc_hi) adr, cmd, vhi, vlo, status, iq_raw, name_len = payload[:7] + status_desc = _decode_debug_status(status) index = self._clear_service_bit(vhi, vlo) if len(payload) < 7 + name_len: self._log("[ERR] Service name truncated"); return @@ -657,13 +813,10 @@ class DebugTerminalWidget(QtWidgets.QWidget): if status == DEBUG_OK: self._name_cache[index] = (status, iq_raw, name, is_signed, frac_bits) 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(''); 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}'") + + def _parse_data_frame(self, frame: bytes, *, error_mode: bool): # ... (код без изменений) payload = frame[:-4]; crc_lo, crc_hi = frame[-4], frame[-3] @@ -673,13 +826,22 @@ 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") if len(payload) < 8: self._log("[ERR] Error frame truncated"); return - err_hi, err_lo = payload[6:8]; bad_index = (err_hi << 8) | err_lo - self._log(f"[DATA] ERROR status={status} bad_index={bad_index}") + err_hi, err_lo = payload[6:8] + 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.valuesRead.emit(base, 0, [], [], [], []) return + if len(payload) < 6 + varqnt*2: self._log("[ERR] Data payload truncated"); return raw_vals = [] @@ -705,13 +867,10 @@ class DebugTerminalWidget(QtWidgets.QWidget): scaled_list.append(scaled); display_raw_list.append(value_int) self._populate_table(idx_list, name_list, iq_list, display_raw_list, scaled_list) 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(): _, 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]) else: - self.edit_single_value.setText("") 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}") @@ -727,14 +886,13 @@ class DebugTerminalWidget(QtWidgets.QWidget): addr2, addr1, addr0 = payload[3], payload[4], payload[5] 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) + self.ll_val_status.setText(f"0x{status:02X} ({status_desc})") if not success: self.ll_val_rettype.setText('-') - self.ll_val_raw.setText('-') - self.ll_val_scaled.setText('') - self.llValueRead.emit(addr24, status, 0, 0, float('nan')) - self._log(f"[LL] ERROR status=0x{status:02X} addr=0x{addr24:06X}") + self.ll_val_scaled.setText(f"") + self._log(f"[LL] ERROR status=0x{status:02X} ({status_desc}) addr=0x{addr24:06X}") return return_type = payload[6] @@ -749,7 +907,7 @@ class DebugTerminalWidget(QtWidgets.QWidget): else: value_int = raw16 - if self.chk_ll_raw.isChecked(): + if self.chk_raw.isChecked(): scale = 1.0 else: scale = self.iq_scaling.get(frac_bits, 1.0 / (1 << frac_bits)) # 1 / 2^N @@ -758,12 +916,21 @@ class DebugTerminalWidget(QtWidgets.QWidget): # Обновляем 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._log(f"[LL] OK addr=0x{addr24:06X} type=0x{return_type:02X} raw={value_int} scaled={scaled:.6g}") + 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"")) + self.tbl_values.setItem(0, 2, QtWidgets.QTableWidgetItem("-")) + self.tbl_values.setItem(0, 3, QtWidgets.QTableWidgetItem("-")) + self.tbl_values.setItem(0, 4, QtWidgets.QTableWidgetItem(""))\ + def _populate_table(self, idxs, names, iqs, raws, scaled): """ Быстрое массовое обновление таблицы значений. @@ -877,7 +1044,6 @@ class DebugTerminalWidget(QtWidgets.QWidget): # Сбрасываем старые значения self.ll_val_status.setText("-") self.ll_val_rettype.setText("-") - self.ll_val_raw.setText("-") self.ll_val_scaled.setText("-") # ------------------------------ HELPERS -------------------------------- @@ -894,12 +1060,12 @@ class DebugTerminalWidget(QtWidgets.QWidget): # Блокируем кнопки в зависимости от состояния 'busy' и 'polling' # Watch tab - can_use_watch = not busy and not self._polling - self.btn_read_service.setEnabled(can_use_watch) + can_use_watch = not busy and not (self._polling or self._ll_polling) + #self.btn_update_service.setEnabled(can_use_watch) self.btn_read_values.setEnabled(can_use_watch) # 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) def _on_serial_error(self, err): @@ -967,4 +1133,5 @@ if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) win = _DemoWindow(); win.show() + win.resize(640, 520) sys.exit(app.exec_()) diff --git a/Src/var_selector_table.py b/Src/var_selector_table.py index 83ab568..b67deb1 100644 --- a/Src/var_selector_table.py +++ b/Src/var_selector_table.py @@ -292,23 +292,7 @@ class VariableSelectWidget(QWidget): return suggestions def insert_completion(self, full_path: str): - """ - Пользователь выбрал подсказку (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 += '.' # обычный переход + text = self.hints.add_separator(full_path) if not self._bckspc_pressed: self.search_input.setText(text) self.search_input.setCursorPosition(len(text)) diff --git a/Src/var_selector_window.py b/Src/var_selector_window.py index 8eb3361..a4073db 100644 --- a/Src/var_selector_window.py +++ b/Src/var_selector_window.py @@ -304,7 +304,7 @@ class VariableSelectorDialog(QDialog): # Проверка пути к XML if not hasattr(self, 'xml_path') or not self.xml_path: from PySide2.QtWidgets import QMessageBox - QMessageBox.warning(self, "Ошибка", "Путь к XML не задан, невозможно обновить переменные.") + #QMessageBox.warning(self, "Ошибка", "Путь к XML не задан, невозможно обновить переменные.") return root, tree = myXML.safe_parse_xml(self.xml_path) diff --git a/debug_tools.h b/debug_tools.h index 48fec2b..3ad2983 100644 --- a/debug_tools.h +++ b/debug_tools.h @@ -53,6 +53,7 @@ #define DEBUG_ERR_ADDR_ALIGN (1<<3) | DEBUG_ERR #define DEBUG_ERR_INTERNAL (1<<4) | DEBUG_ERR #define DEBUG_ERR_DATATIME (1<<5) | DEBUG_ERR +#define DEBUG_ERR_RS (1<<6) | DEBUG_ERR