все неплохо работает.

сейв перед попыткой улучшить lowlevel debug
This commit is contained in:
Razvalyaev 2025-07-21 13:40:52 +03:00
parent f89aff1b1c
commit 96496a0256
6 changed files with 393 additions and 155 deletions

View File

@ -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
# ------------ внутренние вспомогательные ------------

View File

@ -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)

View File

@ -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('<err>'); self.lbl_iq.setText('-')
self._log(f"[SERVICE] idx={index} status={status} iq_raw=0x{iq_raw:02X} sign={'S' if is_signed else 'U'} frac={frac_bits} name='{name}'")
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('<ERROR>')
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"<ERROR:{status_desc}>")
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"<ERROR:{desc}>"))
self.tbl_values.setItem(0, 2, QtWidgets.QTableWidgetItem("-"))
self.tbl_values.setItem(0, 3, QtWidgets.QTableWidgetItem("-"))
self.tbl_values.setItem(0, 4, QtWidgets.QTableWidgetItem("<ERROR>"))\
def _populate_table(self, idxs, names, iqs, raws, scaled):
"""
Быстрое массовое обновление таблицы значений.
@ -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_())

View File

@ -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))

View File

@ -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)

View File

@ -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