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

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

View File

@ -7,7 +7,7 @@ LowLevelSelectorWidget (PySide2)
* Построения плоского списка путей (имя/подпуть) с расчётом абсолютного адреса (base_address + offset) * Построения плоского списка путей (имя/подпуть) с расчётом абсолютного адреса (base_address + offset)
* Определения структур с полями даты (year, month, day, hour, minute) * Определения структур с полями даты (year, month, day, hour, minute)
* Выбора переменной и (опционально) переменной даты / ручного ввода даты * Выбора переменной и (опционально) переменной даты / ручного ввода даты
* Выбора типов: ptr_type (pt_*), iq_type, return_type * Выбора типов: ptr_type , iq_type, return_type
* Форматирования адреса в виде 0x000000 (6 HEX) * Форматирования адреса в виде 0x000000 (6 HEX)
* Генерации словаря/кадра для последующей LowLevel-команды (не отправляет сам) * Генерации словаря/кадра для последующей LowLevel-команды (не отправляет сам)
@ -17,7 +17,7 @@ LowLevelSelectorWidget (PySide2)
{ {
'address': int, 'address': int,
'address_hex': str, # '0x....' 'address_hex': str, # '0x....'
'ptr_type': int, # значение enum pt_* 'ptr_type': int, # значение enum *
'iq_type': int, 'iq_type': int,
'return_type': int, 'return_type': int,
'datetime': { 'datetime': {
@ -40,33 +40,23 @@ from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple from typing import List, Dict, Optional, Tuple
from PySide2 import QtCore, QtGui, QtWidgets from PySide2 import QtCore, QtGui, QtWidgets
from path_hints import PathHints from path_hints import PathHints
from generate_debug_vars import choose_type_map, type_map
# ------------------------------------------------------------ Enumerations -- # ------------------------------------------------------------ Enumerations --
# Сопоставление строк из XML типу ptr_type (адаптируйте под реальный проект) # Сопоставление строк из XML типу ptr_type (адаптируйте под реальный проект)
PTR_TYPE_MAP = { PTR_TYPE_MAP = type_map
'int8': 'pt_int8', 'signed char': 'pt_int8', 'char': 'pt_int8',
'int16': 'pt_int16', 'short': 'pt_int16', 'int': 'pt_int16',
'int32': 'pt_int32', 'long': 'pt_int32',
'int64': 'pt_int64', 'long long': 'pt_int64',
'uint8': 'pt_uint8', 'unsigned char': 'pt_uint8',
'uint16': 'pt_uint16', 'unsigned short': 'pt_uint16', 'unsigned int': 'pt_uint16',
'uint32': 'pt_uint32', 'unsigned long': 'pt_uint32',
'uint64': 'pt_uint64', 'unsigned long long': 'pt_uint64',
'float': 'pt_float', 'floatf': 'pt_float',
'struct': 'pt_struct', 'union': 'pt_union',
}
PT_ENUM_ORDER = [ PT_ENUM_ORDER = [
'pt_unknown','pt_int8','pt_int16','pt_int32','pt_int64', 'unknown','int8','int16','int32','int64',
'pt_uint8','pt_uint16','pt_uint32','pt_uint64','pt_float', 'uint8','uint16','uint32','uint64','float',
'pt_struct','pt_union' 'struct','union'
] ]
IQ_ENUM_ORDER = [ IQ_ENUM_ORDER = [
't_iq_none','t_iq','t_iq1','t_iq2','t_iq3','t_iq4','t_iq5','t_iq6', 'iq_none','iq','iq1','iq2','iq3','iq4','iq5','iq6',
't_iq7','t_iq8','t_iq9','t_iq10','t_iq11','t_iq12','t_iq13','t_iq14', 'iq7','iq8','iq9','iq10','iq11','iq12','iq13','iq14',
't_iq15','t_iq16','t_iq17','t_iq18','t_iq19','t_iq20','t_iq21','t_iq22', 'iq15','iq16','iq17','iq18','iq19','iq20','iq21','iq22',
't_iq23','t_iq24','t_iq25','t_iq26','t_iq27','t_iq28','t_iq29','t_iq30' 'iq23','iq24','iq25','iq26','iq27','iq28','iq29','iq30'
] ]
# Для примера: маппинг имени enum -> числовое значение (индекс по порядку) # Для примера: маппинг имени enum -> числовое значение (индекс по порядку)
@ -133,6 +123,7 @@ class VariablesXML:
self.path = path self.path = path
self.timestamp: str = '' self.timestamp: str = ''
self.variables: List[VariableNode] = [] self.variables: List[VariableNode] = []
choose_type_map(0)
self._parse() self._parse()
# ------------------ low helpers ------------------ # ------------------ low helpers ------------------
@ -242,23 +233,26 @@ class VariablesXML:
out.append((path, addr, t)) out.append((path, addr, t))
def compute_stride(size_bytes: Optional[int], def compute_stride(size_bytes: Optional[int],
count: Optional[int], count: Optional[int],
base_type: Optional[str], base_type: Optional[str],
node_children: Optional[List[MemberNode]]) -> int: node_children: Optional[List[MemberNode]]) -> int:
# 1) пробуем size/count # 1) size_bytes/count
if size_bytes and count and count > 0: if size_bytes and count and count > 0:
stride = size_bytes // count if size_bytes % count == 0:
if stride * count != size_bytes: stride = size_bytes // count
# округлённо вверх if stride <= 0:
stride = (size_bytes + count - 1) // count stride = 1
if stride <= 0: return stride
stride = 1 else:
return stride # size не кратен count → скорее всего size = размер одного элемента
# 2) размер примитива по типу return max(size_bytes, 1)
# 2) попытка по типу (примитив)
if base_type: if base_type:
gs = self._guess_primitive_size(base_type) gs = self._guess_primitive_size(base_type)
if gs: if gs:
return gs return gs
# 3) попытка по детям (структура) # 3) попытка по детям (структура)
if node_children: if node_children:
min_off = min(ch.offset for ch in node_children) min_off = min(ch.offset for ch in node_children)
@ -273,8 +267,10 @@ class VariablesXML:
stride = max_end - min_off stride = max_end - min_off
if stride > 0: if stride > 0:
return stride return stride
return 1 return 1
def expand_members(prefix_name: str, def expand_members(prefix_name: str,
base_addr: int, base_addr: int,
members: List[MemberNode], members: List[MemberNode],
@ -393,6 +389,7 @@ class LowLevelSelectorWidget(QtWidgets.QWidget):
self._paths = [] self._paths = []
self._path_info = {} self._path_info = {}
self._addr_index = {} self._addr_index = {}
self._backspace_pressed = False
self._hints = PathHints() self._hints = PathHints()
self._build_ui() self._build_ui()
self._connect() self._connect()
@ -417,7 +414,7 @@ class LowLevelSelectorWidget(QtWidgets.QWidget):
# --- Search field for variable --- # --- Search field for variable ---
self.edit_var_search = QtWidgets.QLineEdit() 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) form.addRow('Variable:', self.edit_var_search)
# Popup list # Popup list
@ -447,15 +444,15 @@ class LowLevelSelectorWidget(QtWidgets.QWidget):
box.addWidget(QtWidgets.QLabel(label, alignment=QtCore.Qt.AlignHCenter)) box.addWidget(QtWidgets.QLabel(label, alignment=QtCore.Qt.AlignHCenter))
box.addWidget(w) box.addWidget(w)
dt_row.addLayout(box) dt_row.addLayout(box)
form.addRow('Manual Date:', dt_row) form.addRow('Compile Date:', dt_row)
# Types # Types
self.cmb_ptr_type = QtWidgets.QComboBox(); self.cmb_ptr_type.addItems(PT_ENUM_ORDER) 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_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) self.cmb_return_type = QtWidgets.QComboBox(); self.cmb_return_type.addItems(IQ_ENUM_ORDER)
form.addRow('ptr_type:', self.cmb_ptr_type) form.addRow('Type:', self.cmb_ptr_type)
form.addRow('iq_type:', self.cmb_iq_type) form.addRow('IQ Type:', self.cmb_iq_type)
form.addRow('return_type:', self.cmb_return_type) form.addRow('Return IQ Type:', self.cmb_return_type)
lay.addLayout(form) lay.addLayout(form)
@ -478,7 +475,16 @@ class LowLevelSelectorWidget(QtWidgets.QWidget):
self.btn_prepare.clicked.connect(self._emit_variable) self.btn_prepare.clicked.connect(self._emit_variable)
self.edit_var_search.textEdited.connect(self._on_var_search_edited) self.edit_var_search.textEdited.connect(self._on_var_search_edited)
self.edit_var_search.returnPressed.connect(self._activate_current_popup_selection) 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 ---------------- # ---------------- XML Load ----------------
def _on_load_xml(self): def _on_load_xml(self):
path, _ = QtWidgets.QFileDialog.getOpenFileName( path, _ = QtWidgets.QFileDialog.getOpenFileName(
@ -555,7 +561,7 @@ class LowLevelSelectorWidget(QtWidgets.QWidget):
limit = 400 limit = 400
added = 0 added = 0
for p in paths: for p in paths:
info = self._path_info.get(p) info = self._path_info.get(str(p))
if not info: if not info:
continue continue
addr, t = info addr, t = info
@ -589,7 +595,13 @@ class LowLevelSelectorWidget(QtWidgets.QWidget):
self._popup.hide() self._popup.hide()
def _on_var_search_edited(self, text: str): def _on_var_search_edited(self, text: str):
# Показываем подсказки при вводе текста, если не было Backspace (чтобы не добавлять разделитель)
t = text.strip() t = text.strip()
if self._backspace_pressed:
# При стирании не показываем автодополнение с разделителем
self._backspace_pressed = False
self._hide_popup()
return
# адрес? # адрес?
if t.startswith("0x") and len(t) >= 3: if t.startswith("0x") and len(t) >= 3:
@ -608,13 +620,26 @@ class LowLevelSelectorWidget(QtWidgets.QWidget):
self._update_popup_model(suggestions) self._update_popup_model(suggestions)
self._show_popup() self._show_popup()
def _on_popup_clicked(self, idx: QtCore.QModelIndex): def _on_popup_clicked(self, idx: QtCore.QModelIndex):
if not idx.isValid(): if not idx.isValid():
return return
path = idx.data(QtCore.Qt.UserRole+1) path = idx.data(QtCore.Qt.UserRole + 1)
if path: if not path:
self._set_current_variable(path) return
self._hide_popup()
# Реализуем автодополнение по цепочке, пока подсказка имеет детей
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): def _activate_current_popup_selection(self):
if self._popup.isVisible(): if self._popup.isVisible():
@ -629,28 +654,54 @@ class LowLevelSelectorWidget(QtWidgets.QWidget):
def eventFilter(self, obj, ev): def eventFilter(self, obj, ev):
if obj is self.edit_var_search and ev.type() == QtCore.QEvent.KeyPress: 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(): if not self._popup.isVisible():
self._show_popup() self._show_popup()
else: 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() cur = self._popup.currentIndex()
row = cur.row() + step row = cur.row() + step
if row < 0: row = 0 if row < 0: row = 0
if row >= self._model_filtered.rowCount(): if row >= self._model_filtered.rowCount():
row = self._model_filtered.rowCount()-1 row = self._model_filtered.rowCount() - 1
self._popup.setCurrentIndex(self._model_filtered.index(row,0)) self._popup.setCurrentIndex(self._model_filtered.index(row, 0))
return True return True
elif ev.key() == QtCore.Qt.Key_Escape:
elif key == QtCore.Qt.Key_Escape:
self._hide_popup() self._hide_popup()
return True 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) return super().eventFilter(obj, ev)
def _set_current_variable(self, path: str, from_address=False): 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: if path not in self._path_info:
return return
addr, type_str = self._path_info[path] addr, type_str = self._path_info[path]
self.edit_var_search.setText(path)
# Разделитель добавляем только если не стираем (Backspace), и если уже не добавлен
# В этой функции не будем добавлять разделитель, добавляем его только при автодополнении выше
self.edit_address.setText(f"0x{addr:06X}") self.edit_address.setText(f"0x{addr:06X}")
ptr_enum_name = self._map_type_to_ptr_enum(type_str) ptr_enum_name = self._map_type_to_ptr_enum(type_str)
self._select_combo_text(self.cmb_ptr_type, ptr_enum_name) 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: def _map_type_to_ptr_enum(self, type_str: str) -> str:
if not type_str: if not type_str:
return 'pt_unknown' return 'unknown'
low = type_str.lower() low = type_str.lower()
token = low.replace('*',' ').replace('[',' ').split()[0] token = low.replace('*',' ').replace('[',' ')
return PTR_TYPE_MAP.get(token, 'pt_unknown') return PTR_TYPE_MAP.get(token, 'unknown')
def _select_combo_text(self, combo: QtWidgets.QComboBox, text: str): def _select_combo_text(self, combo: QtWidgets.QComboBox, text: str):
ix = combo.findText(text) ix = combo.findText(text.replace('pt_', ''))
if ix >= 0: if ix >= 0:
combo.setCurrentIndex(ix) combo.setCurrentIndex(ix)

View File

@ -1,12 +1,36 @@
from PySide2 import QtCore, QtWidgets, QtSerialPort from PySide2 import QtCore, QtWidgets, QtSerialPort
from tms_debugvar_lowlevel import LowLevelSelectorWidget from tms_debugvar_lowlevel import LowLevelSelectorWidget
import datetime import datetime
import time
# ------------------------------- Константы протокола ------------------------ # ------------------------------- Константы протокола ------------------------
WATCH_SERVICE_BIT = 0x8000 WATCH_SERVICE_BIT = 0x8000
DEBUG_OK = 0 # ожидаемый код успешного чтения DEBUG_OK = 0 # ожидаемый код успешного чтения
SIGN_BIT_MASK = 0x80 SIGN_BIT_MASK = 0x80
FRAC_MASK_FULL = 0x7F # если используем 7 бит дробной части FRAC_MASK_FULL = 0x7F # если используем 7 бит дробной части
# --- Debug status codes (из прошивки) ---
DEBUG_OK = 0x00
DEBUG_ERR = 0x80 # общий флаг ошибки (старший бит)
DEBUG_ERR_VAR_NUMB = DEBUG_ERR | (1 << 0)
DEBUG_ERR_INVALID_VAR = DEBUG_ERR | (1 << 1)
DEBUG_ERR_ADDR = DEBUG_ERR | (1 << 2)
DEBUG_ERR_ADDR_ALIGN = DEBUG_ERR | (1 << 3)
DEBUG_ERR_INTERNAL = DEBUG_ERR | (1 << 4)
DEBUG_ERR_DATATIME = DEBUG_ERR | (1 << 5)
DEBUG_ERR_RS = DEBUG_ERR | (1 << 5)
# для декодирования по битам
_DEBUG_ERR_BITS = (
(1 << 0, "Invalid Variable Index"),
(1 << 1, "Invalid Variable"),
(1 << 2, "Invalid Address"),
(1 << 3, "Invalid Address Align"),
(1 << 4, "Internal Code Error"),
(1 << 5, "Invalid Data or Time"),
(1 << 6, "Error with RS"),
)
# ---------------------------------------------------------------- CRC util --- # ---------------------------------------------------------------- CRC util ---
def crc16_ibm(data: bytes, *, init=0xFFFF) -> int: def crc16_ibm(data: bytes, *, init=0xFFFF) -> int:
"""CRC16-IBM (aka CRC-16/ANSI, polynomial 0xA001 reflected).""" """CRC16-IBM (aka CRC-16/ANSI, polynomial 0xA001 reflected)."""
@ -20,6 +44,26 @@ def crc16_ibm(data: bytes, *, init=0xFFFF) -> int:
crc >>= 1 crc >>= 1
return crc & 0xFFFF return crc & 0xFFFF
def _decode_debug_status(status: int) -> str:
"""Преобразует код статуса прошивки в строку.
Возвращает 'OK' или перечисление битов через '|'.
Не зависит от того, WATCH или LowLevel.
"""
if status == DEBUG_OK:
return "OK"
parts = []
if status & DEBUG_ERR:
for mask, name in _DEBUG_ERR_BITS:
if status & mask:
parts.append(name)
if not parts: # старший бит есть, но ни один из известных младших не выставлен
parts.append("ERR")
else:
# Неожиданно: статус !=0, но бит DEBUG_ERR не стоит
parts.append(f"0x{status:02X}")
return "|".join(parts)
class Spoiler(QtWidgets.QWidget): class Spoiler(QtWidgets.QWidget):
def __init__(self, title="", animationDuration=300, parent=None): def __init__(self, title="", animationDuration=300, parent=None):
super().__init__(parent) super().__init__(parent)
@ -131,7 +175,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
self.auto_crc_check = auto_crc_check self.auto_crc_check = auto_crc_check
self._drop_if_busy = drop_if_busy self._drop_if_busy = drop_if_busy
self._replace_if_busy = replace_if_busy self._replace_if_busy = replace_if_busy
self._last_txn_timestamp = 0
if iq_scaling is None: if iq_scaling is None:
iq_scaling = {n: float(1 << n) for n in range(31)} iq_scaling = {n: float(1 << n) for n in range(31)}
iq_scaling[0] = 1.0 iq_scaling[0] = 1.0
@ -200,6 +244,22 @@ class DebugTerminalWidget(QtWidgets.QWidget):
self.tabs = QtWidgets.QTabWidget() self.tabs = QtWidgets.QTabWidget()
self._build_watch_tab() self._build_watch_tab()
self._build_lowlevel_tab() # <-- Вызываем новый метод self._build_lowlevel_tab() # <-- Вызываем новый метод
g_control = QtWidgets.QGroupBox("Control / Status")
control_layout = QtWidgets.QHBoxLayout(g_control) # Используем QHBoxLayout
# Форма для статусов слева
form_control = QtWidgets.QFormLayout()
self.lbl_status = QtWidgets.QLabel("Idle")
self.lbl_status.setStyleSheet("font-weight: bold; color: grey;")
form_control.addRow("Status:", self.lbl_status)
self.lbl_actual_interval = QtWidgets.QLabel("-")
form_control.addRow("Actual Interval:", self.lbl_actual_interval)
control_layout.addLayout(form_control, 1) # Растягиваем форму
# Галочка Raw справа
self.chk_raw = QtWidgets.QCheckBox("Raw (no IQ scaling)")
control_layout.addWidget(self.chk_raw)
# --- UART Log --- # --- UART Log ---
self.log_spoiler = Spoiler("UART Log", animationDuration=300, parent=self) self.log_spoiler = Spoiler("UART Log", animationDuration=300, parent=self)
@ -211,71 +271,116 @@ class DebugTerminalWidget(QtWidgets.QWidget):
log_layout.addWidget(self.txt_log) log_layout.addWidget(self.txt_log)
self.log_spoiler.setContentLayout(log_layout) self.log_spoiler.setContentLayout(log_layout)
layout.addWidget(g_serial, 0) layout.addWidget(g_serial)
layout.addWidget(self.tabs, 1) layout.addWidget(self.tabs, 1)
layout.addWidget(self.log_spoiler, 0) layout.addWidget(g_control)
layout.addWidget(self.log_spoiler)
layout.setStretch(layout.indexOf(g_serial), 0) layout.setStretch(layout.indexOf(g_serial), 0)
layout.setStretch(layout.indexOf(self.tabs), 1) layout.setStretch(layout.indexOf(self.tabs), 1)
layout.setStretch(layout.indexOf(self.log_spoiler), 0)
def _build_watch_tab(self): def _build_watch_tab(self):
# ... (код для вкладки Watch остаётся без изменений)
tab = QtWidgets.QWidget() tab = QtWidgets.QWidget()
vtab = QtWidgets.QVBoxLayout(tab) main_layout = QtWidgets.QVBoxLayout(tab)
g_watch = QtWidgets.QGroupBox("Watch Variables") # --- Variable Selector ---
grid = QtWidgets.QGridLayout(g_watch) g_selector = QtWidgets.QGroupBox("Variable Selector")
grid.setHorizontalSpacing(8) selector_layout = QtWidgets.QVBoxLayout(g_selector)
grid.setVerticalSpacing(4)
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.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_read_values = QtWidgets.QPushButton("Read Value(s)")
self.btn_poll = QtWidgets.QPushButton("Start Polling") 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") btn_layout.addWidget(self.btn_update_service)
self.chk_auto_service = QtWidgets.QCheckBox("Auto service before values if miss cache"); self.chk_auto_service.setChecked(True) btn_layout.addWidget(self.btn_read_values)
self.chk_raw = QtWidgets.QCheckBox("Raw (no IQ scaling)") btn_layout.addWidget(self.btn_poll)
self.lbl_name = QtWidgets.QLineEdit(); self.lbl_name.setReadOnly(True) selector_layout.addLayout(btn_layout)
self.lbl_iq = QtWidgets.QLabel("-")
self.edit_single_value = QtWidgets.QLineEdit(); self.edit_single_value.setReadOnly(True)
# --- Table ---
g_table = QtWidgets.QGroupBox("Table")
table_layout = QtWidgets.QVBoxLayout(g_table)
self.tbl_values = QtWidgets.QTableWidget(0, 5) 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 = self.tbl_values.horizontalHeader()
hh.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents) for i in range(4):
hh.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents) hh.setSectionResizeMode(i, QtWidgets.QHeaderView.ResizeToContents)
hh.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
hh.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
hh.setSectionResizeMode(4, QtWidgets.QHeaderView.Stretch) hh.setSectionResizeMode(4, QtWidgets.QHeaderView.Stretch)
self.tbl_values.verticalHeader().setVisible(False) 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 v_split = QtWidgets.QSplitter(QtCore.Qt.Vertical)
grid.addWidget(QtWidgets.QLabel("Count:"), r, 0); grid.addWidget(self.spin_count, r, 1); r+=1 v_split.addWidget(g_selector)
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 v_split.addWidget(g_table)
grid.addWidget(QtWidgets.QLabel("Interval:"), r, 0); grid.addWidget(self.spin_interval, r, 1); grid.addWidget(self.chk_auto_service, r, 2); r+=1 v_split.setStretchFactor(0, 1)
grid.addWidget(QtWidgets.QLabel("Name:"), r, 0); grid.addWidget(self.lbl_name, r, 1, 1, 2); r+=1 v_split.setStretchFactor(1, 3)
grid.addWidget(QtWidgets.QLabel("IQ:"), r, 0); grid.addWidget(self.lbl_iq, r, 1); grid.addWidget(self.chk_raw, r, 2); r+=1 v_split.setStretchFactor(2, 1)
grid.addWidget(QtWidgets.QLabel("Single:"), r, 0); grid.addWidget(self.edit_single_value, r, 1, 1, 2); r+=1
grid.addWidget(QtWidgets.QLabel("Array Values:"), r, 0); r+=1
grid.addWidget(self.tbl_values, r, 0, 1, 3); grid.setRowStretch(r, 1)
vtab.addWidget(g_watch, 1) main_layout.addWidget(v_split)
self.tabs.addTab(tab, "Watch") self.tabs.addTab(tab, "Watch")
table_layout.addWidget(self.tbl_values)
def _build_lowlevel_tab(self): def _build_lowlevel_tab(self):
tab = QtWidgets.QWidget() tab = QtWidgets.QWidget()
main_layout = QtWidgets.QVBoxLayout(tab) main_layout = QtWidgets.QVBoxLayout(tab)
# --- Селектор переменной --- # --- GroupBox для селектора переменной ---
self.ll_selector = LowLevelSelectorWidget(tab) group_var_selector = QtWidgets.QGroupBox("Variable Selector", tab)
main_layout.addWidget(self.ll_selector, 1) # Даём ему растягиваться var_selector_layout = QtWidgets.QVBoxLayout(group_var_selector)
self.ll_selector = LowLevelSelectorWidget(group_var_selector)
# --- Панель управления и статуса --- var_selector_layout.addWidget(self.ll_selector)
g_controls = QtWidgets.QGroupBox("Controls & Status")
grid = QtWidgets.QGridLayout(g_controls) # --- 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_read = QtWidgets.QPushButton("Read Once")
self.btn_ll_poll = QtWidgets.QPushButton("Start Polling") 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.setRange(50, 10000)
self.spin_ll_interval.setValue(500) self.spin_ll_interval.setValue(500)
self.spin_ll_interval.setSuffix(" ms") 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_status = QtWidgets.QLabel("-")
self.ll_val_rettype = QtWidgets.QLabel("-") self.ll_val_rettype = QtWidgets.QLabel("-")
self.ll_val_raw = QtWidgets.QLabel("-")
self.ll_val_scaled = QtWidgets.QLabel("-") self.ll_val_scaled = QtWidgets.QLabel("-")
# Размещение виджетов # Размещение виджетов в grid
grid.addWidget(self.btn_ll_read, 0, 0) grid.addWidget(self.btn_ll_read, 0, 0)
grid.addWidget(self.btn_ll_poll, 0, 1) grid.addWidget(self.btn_ll_poll, 0, 1)
grid.addWidget(QtWidgets.QLabel("Interval:"), 1, 0) grid.addWidget(QtWidgets.QLabel("Interval:"), 1, 0)
grid.addWidget(self.spin_ll_interval, 1, 1) 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 = QtWidgets.QFormLayout()
form_layout.addRow("Status:", self.ll_val_status) form_layout.addRow("Status:", self.ll_val_status)
form_layout.addRow("Return Type:", self.ll_val_rettype) 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) form_layout.addRow("Scaled Value:", self.ll_val_scaled)
# Поле Raw Value убрано
grid.addLayout(form_layout, 2, 0, 1, 3)
grid.setColumnStretch(2, 1)
main_layout.addWidget(g_controls)
main_layout.setStretchFactor(g_controls, 0) # Не растягивать
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") self.tabs.addTab(tab, "LowLevel")
def _connect_ui(self): def _connect_ui(self):
# Watch # Watch
self.btn_refresh.clicked.connect(self.set_available_ports) self.btn_refresh.clicked.connect(self.set_available_ports)
self.btn_open.clicked.connect(self._open_close_port) self.btn_open.clicked.connect(self._open_close_port)
self.btn_read_service.clicked.connect(self.request_service_single) self.btn_update_service.clicked.connect(self.request_service_update_for_table)
self.btn_read_values.clicked.connect(self.request_values) self.btn_read_values.clicked.connect(self.request_values)
self.btn_poll.clicked.connect(self._toggle_polling) self.btn_poll.clicked.connect(self._toggle_polling)
self.chk_hex_index.stateChanged.connect(self._toggle_index_base) self.chk_hex_index.stateChanged.connect(self._toggle_index_base)
@ -327,8 +436,18 @@ class DebugTerminalWidget(QtWidgets.QWidget):
self.btn_ll_read.clicked.connect(self.request_lowlevel_once) self.btn_ll_read.clicked.connect(self.request_lowlevel_once)
self.btn_ll_poll.clicked.connect(self._toggle_ll_polling) 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 ---------------------------- # ----------------------------- SERIAL MGMT ----------------------------
# ... (код без изменений)
def set_available_ports(self): def set_available_ports(self):
cur = self.cmb_port.currentText() cur = self.cmb_port.currentText()
self.cmb_port.blockSignals(True) self.cmb_port.blockSignals(True)
@ -419,20 +538,46 @@ class DebugTerminalWidget(QtWidgets.QWidget):
idx = int(self.spin_index.value()) idx = int(self.spin_index.value())
self._enqueue_or_start(idx, service=True, varqnt=0) self._enqueue_or_start(idx, service=True, varqnt=0)
def request_service_update_for_table(self):
"""
Очищает кеш имен/типов для всех видимых в таблице переменных
и инициирует их повторное чтение.
"""
indices_to_update = []
for row in range(self.tbl_values.rowCount()):
item = self.tbl_values.item(row, 0)
if item and item.text().isdigit():
indices_to_update.append(int(item.text()))
if not indices_to_update:
self._log("[SERVICE] No variables in table to update.")
return
self._log(f"[SERVICE] Queuing name/type update for {len(indices_to_update)} variables.")
# Очищаем кеш для этих индексов, чтобы принудительно их перечитать
for index in indices_to_update:
if index in self._name_cache:
del self._name_cache[index]
# Запускаем стандартный запрос значений. Он автоматически обработает
# отсутствующую сервисную информацию (имена/типы) перед запросом данных.
#self.request_values()
def request_values(self): def request_values(self):
base = int(self.spin_index.value()) base = int(self.spin_index.value())
count = int(self.spin_count.value()) count = int(self.spin_count.value())
needed = [] needed = []
if self.chk_auto_service.isChecked(): for i in range(base, base+count):
for i in range(base, base+count): if i not in self._name_cache:
if i not in self._name_cache: needed.append(i)
needed.append(i)
if needed: if needed:
self._service_queue = needed[:] self._service_queue = needed[:]
self._pending_data_after_services = (base, count) self._pending_data_after_services = (base, count)
self._log(f"[AUTO] Need service for {len(needed)} indices: {needed}") self._log(f"[AUTO] Need service for {len(needed)} indices: {needed}")
self.set_status("read service...", "service")
self._kick_service_queue() self._kick_service_queue()
else: else:
self.set_status("read values...", "values")
self._enqueue_or_start(base, service=False, varqnt=count) self._enqueue_or_start(base, service=False, varqnt=count)
def request_lowlevel_once(self): def request_lowlevel_once(self):
@ -451,6 +596,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
frame = self._build_lowlevel_request(self._ll_current_var_info) frame = self._build_lowlevel_request(self._ll_current_var_info)
meta = {'lowlevel': True} meta = {'lowlevel': True}
self.set_status("read lowlevel...", "values")
self._enqueue_raw(frame, meta) self._enqueue_raw(frame, meta)
# -------------------------- SERVICE QUEUE FLOW ------------------------ # -------------------------- SERVICE QUEUE FLOW ------------------------
@ -496,7 +642,15 @@ class DebugTerminalWidget(QtWidgets.QWidget):
return return
self._start_txn(frame, meta) self._start_txn(frame, meta)
def _start_txn(self, frame: bytes, meta: dict): def _start_txn(self, frame: bytes, meta: dict):
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._busy = True
self._txn_meta = meta self._txn_meta = meta
self._rx_buf.clear() self._rx_buf.clear()
@ -561,6 +715,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
def _try_parse(self): def _try_parse(self):
if not self._txn_meta: if not self._txn_meta:
return return
self.set_status("IDLE", "idle")
if self._txn_meta.get('lowlevel', False): if self._txn_meta.get('lowlevel', False):
self._try_parse_lowlevel() self._try_parse_lowlevel()
else: else:
@ -648,6 +803,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
self._log("[ERR] Service frame too short"); return self._log("[ERR] Service frame too short"); return
self._check_crc(payload, crc_lo, crc_hi) self._check_crc(payload, crc_lo, crc_hi)
adr, cmd, vhi, vlo, status, iq_raw, name_len = payload[:7] adr, cmd, vhi, vlo, status, iq_raw, name_len = payload[:7]
status_desc = _decode_debug_status(status)
index = self._clear_service_bit(vhi, vlo) index = self._clear_service_bit(vhi, vlo)
if len(payload) < 7 + name_len: if len(payload) < 7 + name_len:
self._log("[ERR] Service name truncated"); return self._log("[ERR] Service name truncated"); return
@ -657,13 +813,10 @@ class DebugTerminalWidget(QtWidgets.QWidget):
if status == DEBUG_OK: if status == DEBUG_OK:
self._name_cache[index] = (status, iq_raw, name, is_signed, frac_bits) self._name_cache[index] = (status, iq_raw, name, is_signed, frac_bits)
self.nameRead.emit(index, status, iq_raw, name) self.nameRead.emit(index, status, iq_raw, name)
if self.spin_count.value() == 1 and index == self.spin_index.value():
if status == DEBUG_OK:
self.lbl_name.setText(name); self.lbl_iq.setText(f"{frac_bits}{'s' if is_signed else 'u'}")
else:
self.lbl_name.setText('<err>'); self.lbl_iq.setText('-')
self._log(f"[SERVICE] idx={index} status={status} iq_raw=0x{iq_raw:02X} sign={'S' if is_signed else 'U'} frac={frac_bits} name='{name}'") self._log(f"[SERVICE] idx={index} status={status} iq_raw=0x{iq_raw:02X} sign={'S' if is_signed else 'U'} frac={frac_bits} name='{name}'")
def _parse_data_frame(self, frame: bytes, *, error_mode: bool): def _parse_data_frame(self, frame: bytes, *, error_mode: bool):
# ... (код без изменений) # ... (код без изменений)
payload = frame[:-4]; crc_lo, crc_hi = frame[-4], frame[-3] payload = frame[:-4]; crc_lo, crc_hi = frame[-4], frame[-3]
@ -673,13 +826,22 @@ class DebugTerminalWidget(QtWidgets.QWidget):
adr, cmd, vhi, vlo, varqnt, status = payload[:6] adr, cmd, vhi, vlo, varqnt, status = payload[:6]
base = self._clear_service_bit(vhi, vlo) base = self._clear_service_bit(vhi, vlo)
if error_mode: if error_mode:
self.set_status("error", "error")
if len(payload) < 8: if len(payload) < 8:
self._log("[ERR] Error frame truncated"); return self._log("[ERR] Error frame truncated"); return
err_hi, err_lo = payload[6:8]; bad_index = (err_hi << 8) | err_lo err_hi, err_lo = payload[6:8]
self._log(f"[DATA] ERROR status={status} bad_index={bad_index}") bad_index = (err_hi << 8) | err_lo
desc = _decode_debug_status(status)
self._log(f"[DATA] ERROR status=0x{status:02X} ({desc}) bad_index={bad_index}")
# Обновим UI
self._populate_watch_error(bad_index, status)
# Сигналы (оставляем совместимость)
self.valueRead.emit(bad_index, status, 0, 0, float('nan')) self.valueRead.emit(bad_index, status, 0, 0, float('nan'))
self.valuesRead.emit(base, 0, [], [], [], []) self.valuesRead.emit(base, 0, [], [], [], [])
return return
if len(payload) < 6 + varqnt*2: if len(payload) < 6 + varqnt*2:
self._log("[ERR] Data payload truncated"); return self._log("[ERR] Data payload truncated"); return
raw_vals = [] raw_vals = []
@ -705,13 +867,10 @@ class DebugTerminalWidget(QtWidgets.QWidget):
scaled_list.append(scaled); display_raw_list.append(value_int) scaled_list.append(scaled); display_raw_list.append(value_int)
self._populate_table(idx_list, name_list, iq_list, display_raw_list, scaled_list) self._populate_table(idx_list, name_list, iq_list, display_raw_list, scaled_list)
if varqnt == 1: if varqnt == 1:
self.edit_single_value.setText(str(display_raw_list[0]) if self.chk_raw.isChecked() else f"{scaled_list[0]:.6g}")
if idx_list[0] == self.spin_index.value(): if idx_list[0] == self.spin_index.value():
_, iq_raw0, name0, is_signed0, frac0 = self._name_cache.get(idx_list[0], (DEBUG_OK, 0, '', False, 0)) _, iq_raw0, name0, is_signed0, frac0 = self._name_cache.get(idx_list[0], (DEBUG_OK, 0, '', False, 0))
self.lbl_name.setText(name0); self.lbl_iq.setText(f"{frac0}{'s' if is_signed0 else 'u'}")
self.valueRead.emit(idx_list[0], status, iq_list[0], display_raw_list[0], scaled_list[0]) self.valueRead.emit(idx_list[0], status, iq_list[0], display_raw_list[0], scaled_list[0])
else: else:
self.edit_single_value.setText("")
self.valuesRead.emit(base, varqnt, idx_list, iq_list, display_raw_list, scaled_list) self.valuesRead.emit(base, varqnt, idx_list, iq_list, display_raw_list, scaled_list)
self._log(f"[DATA] base={base} q={varqnt} values={[f'{v:.6g}' for v in scaled_list] if not self.chk_raw.isChecked() else raw_vals}") self._log(f"[DATA] base={base} q={varqnt} values={[f'{v:.6g}' for v in scaled_list] if not self.chk_raw.isChecked() else raw_vals}")
@ -727,14 +886,13 @@ class DebugTerminalWidget(QtWidgets.QWidget):
addr2, addr1, addr0 = payload[3], payload[4], payload[5] addr2, addr1, addr0 = payload[3], payload[4], payload[5]
addr24 = (addr2 << 16) | (addr1 << 8) | addr0 addr24 = (addr2 << 16) | (addr1 << 8) | addr0
self.ll_val_status.setText(f"0x{status:02X} ({'OK' if status == DEBUG_OK else 'ERR'})") status_desc = _decode_debug_status(status)
self.ll_val_status.setText(f"0x{status:02X} ({status_desc})")
if not success: if not success:
self.ll_val_rettype.setText('-') self.ll_val_rettype.setText('-')
self.ll_val_raw.setText('-') self.ll_val_scaled.setText(f"<ERROR:{status_desc}>")
self.ll_val_scaled.setText('<ERROR>') self._log(f"[LL] ERROR status=0x{status:02X} ({status_desc}) addr=0x{addr24:06X}")
self.llValueRead.emit(addr24, status, 0, 0, float('nan'))
self._log(f"[LL] ERROR status=0x{status:02X} addr=0x{addr24:06X}")
return return
return_type = payload[6] return_type = payload[6]
@ -749,7 +907,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
else: else:
value_int = raw16 value_int = raw16
if self.chk_ll_raw.isChecked(): if self.chk_raw.isChecked():
scale = 1.0 scale = 1.0
else: else:
scale = self.iq_scaling.get(frac_bits, 1.0 / (1 << frac_bits)) # 1 / 2^N scale = self.iq_scaling.get(frac_bits, 1.0 / (1 << frac_bits)) # 1 / 2^N
@ -758,12 +916,21 @@ class DebugTerminalWidget(QtWidgets.QWidget):
# Обновляем UI # Обновляем UI
self.ll_val_rettype.setText(f"0x{return_type:02X} ({frac_bits}{'s' if is_signed else 'u'})") 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.ll_val_scaled.setText(f"{scaled:.6g}")
self.llValueRead.emit(addr24, status, return_type, value_int, scaled) self.llValueRead.emit(addr24, status, return_type, value_int, scaled)
self._log(f"[LL] OK addr=0x{addr24:06X} type=0x{return_type:02X} raw={value_int} scaled={scaled:.6g}") self._log(f"[LL] OK addr=0x{addr24:06X} type=0x{return_type:02X} raw={value_int} scaled={scaled:.6g}")
def _populate_watch_error(self, bad_index: int, status: int):
"""Отобразить строку ошибки при неудачном ответе WATCH."""
desc = _decode_debug_status(status)
self.tbl_values.setRowCount(1)
self.tbl_values.setItem(0, 0, QtWidgets.QTableWidgetItem(str(bad_index)))
self.tbl_values.setItem(0, 1, QtWidgets.QTableWidgetItem(f"<ERROR:{desc}>"))
self.tbl_values.setItem(0, 2, QtWidgets.QTableWidgetItem("-"))
self.tbl_values.setItem(0, 3, QtWidgets.QTableWidgetItem("-"))
self.tbl_values.setItem(0, 4, QtWidgets.QTableWidgetItem("<ERROR>"))\
def _populate_table(self, idxs, names, iqs, raws, scaled): def _populate_table(self, idxs, names, iqs, raws, scaled):
""" """
Быстрое массовое обновление таблицы значений. Быстрое массовое обновление таблицы значений.
@ -877,7 +1044,6 @@ class DebugTerminalWidget(QtWidgets.QWidget):
# Сбрасываем старые значения # Сбрасываем старые значения
self.ll_val_status.setText("-") self.ll_val_status.setText("-")
self.ll_val_rettype.setText("-") self.ll_val_rettype.setText("-")
self.ll_val_raw.setText("-")
self.ll_val_scaled.setText("-") self.ll_val_scaled.setText("-")
# ------------------------------ HELPERS -------------------------------- # ------------------------------ HELPERS --------------------------------
@ -894,12 +1060,12 @@ class DebugTerminalWidget(QtWidgets.QWidget):
# Блокируем кнопки в зависимости от состояния 'busy' и 'polling' # Блокируем кнопки в зависимости от состояния 'busy' и 'polling'
# Watch tab # Watch tab
can_use_watch = not busy and not self._polling can_use_watch = not busy and not (self._polling or self._ll_polling)
self.btn_read_service.setEnabled(can_use_watch) #self.btn_update_service.setEnabled(can_use_watch)
self.btn_read_values.setEnabled(can_use_watch) self.btn_read_values.setEnabled(can_use_watch)
# LowLevel tab # LowLevel tab
can_use_ll = not busy and not self._ll_polling can_use_ll = not busy and not (self._ll_polling or self._polling)
self.btn_ll_read.setEnabled(can_use_ll) self.btn_ll_read.setEnabled(can_use_ll)
def _on_serial_error(self, err): def _on_serial_error(self, err):
@ -967,4 +1133,5 @@ if __name__ == "__main__":
import sys import sys
app = QtWidgets.QApplication(sys.argv) app = QtWidgets.QApplication(sys.argv)
win = _DemoWindow(); win.show() win = _DemoWindow(); win.show()
win.resize(640, 520)
sys.exit(app.exec_()) sys.exit(app.exec_())

View File

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

View File

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

View File

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