сделана бета терминалка для опроса переменных
This commit is contained in:
parent
5be6343c33
commit
c94a7e711c
@ -1,72 +1,96 @@
|
|||||||
"""
|
|
||||||
PySide2 Serial Debug Terminal Widget
|
|
||||||
=====================================
|
|
||||||
|
|
||||||
A small, embeddable terminal-style widget for talking to your MCU debug monitor
|
|
||||||
via the simple protocol you described:
|
|
||||||
|
|
||||||
Request (host -> target):
|
|
||||||
0x0A 0x44 <DbgNumbHigh> <DbgNumbLow>
|
|
||||||
* In <DbgNumbHigh>, the MSB (bit15 of the 16‑bit dbg number) selects read type:
|
|
||||||
0 = read value, 1 = read name.
|
|
||||||
* Remaining 15 bits = variable index (0..0x7FFF).
|
|
||||||
|
|
||||||
Response (target -> host): built per your WatchVar() C function. Layout:
|
|
||||||
[0] addr_recive (target address / echo)
|
|
||||||
[1] CMD_RS232_WATCH (command echo, e.g., 0x44)
|
|
||||||
[2] status (0x00 = OK, 0xFF = error; else vendor‑defined)
|
|
||||||
[3] IQType (enum: 0=int, 1=IQ1, 2=IQ2, ...)
|
|
||||||
if ReadName == 1:
|
|
||||||
[4..N] DebugVarName_t raw bytes (fixed length, default 32, configurable)
|
|
||||||
else (ReadName == 0):
|
|
||||||
[4] dataLo (LSB of 16‑bit value)
|
|
||||||
[5] dataHi (MSB of 16‑bit value)
|
|
||||||
[...next] crcLo crcHi 0x00 0x00 (CRC16‑IBM over the bytes preceding CRC)
|
|
||||||
|
|
||||||
NOTE: The C code shows a 32‑bit signed Data but only transmits the low 16 bits.
|
|
||||||
This widget decodes the 16‑bit word and sign‑extends to 32 when converting IQ,
|
|
||||||
keeping behavior close to the firmware sample. Adjust in subclass if needed.
|
|
||||||
|
|
||||||
Features
|
|
||||||
--------
|
|
||||||
- COM port selection (auto‑populate) & open/close.
|
|
||||||
- Variable index entry (0..0x7FFF) via spin box (hex display optional).
|
|
||||||
- Buttons: "Read Name", "Read Value".
|
|
||||||
- Raw vs Formatted output checkbox: when unchecked, IQ scaling -> float.
|
|
||||||
- Displays: Name, Value, IQ type.
|
|
||||||
- UART log window (timestamped hex + ASCII, TX/RX tagged).
|
|
||||||
- CRC16‑IBM check; bad CRC flagged in log.
|
|
||||||
- Signals for integration: nameRead(index, status, iq, nameStr), valueRead(index, status, iq, raw16, floatVal).
|
|
||||||
- Non‑blocking I/O via QSerialPort (QtSerialPort) — *preferred* since you are on Windows and already in Qt/PySide2. No extra threads needed.
|
|
||||||
(If you strongly prefer pyserial, see commented alt implementation at bottom.)
|
|
||||||
|
|
||||||
Tested Python target: 3.7 w/ PySide2.
|
|
||||||
|
|
||||||
Integration
|
|
||||||
-----------
|
|
||||||
Instantiate `DebugTerminalWidget(parent=None)` and embed into your main window or layout.
|
|
||||||
Call `set_available_ports()` periodically or on refresh to re‑enumerate COM ports.
|
|
||||||
|
|
||||||
Customization knobs (constructor args):
|
|
||||||
- cmd_byte: defaults to 0x44 (as in your request).
|
|
||||||
- start_byte: defaults to 0x0A.
|
|
||||||
- name_field_len: bytes to read when name requested (default 32; set to len(DebugVarName_t)).
|
|
||||||
- iq_scaling: dict mapping iq enum -> divisor (float). default: {0:1.0,1:2.0,2:4.0,3:8.0,... up to 15:2**n}.
|
|
||||||
- signed: treat 16‑bit value as signed (default True). If False, unsigned.
|
|
||||||
|
|
||||||
Limitations / TODO hooks
|
|
||||||
------------------------
|
|
||||||
- If firmware later sends full 32‑bit Data, override `_parse_value_payload()`.
|
|
||||||
- Multi‑frame or streaming modes not supported (only single WatchVar replies).
|
|
||||||
- Timeout & retry basic; expand as needed.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import struct
|
import struct
|
||||||
import datetime
|
import datetime
|
||||||
from PySide2 import QtCore, QtWidgets, QtSerialPort
|
from PySide2 import QtCore, QtWidgets, QtSerialPort
|
||||||
from collections import deque
|
from PySide2.QtCore import QTimer
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- CRC util ---
|
||||||
|
def crc16_ibm(data: bytes, *, init=0xFFFF) -> int:
|
||||||
|
"""CRC16-IBM (aka CRC-16/ANSI, polynomial 0xA001 reflected)."""
|
||||||
|
crc = init
|
||||||
|
for b in data:
|
||||||
|
crc ^= b
|
||||||
|
for _ in range(8):
|
||||||
|
if crc & 1:
|
||||||
|
crc = (crc >> 1) ^ 0xA001
|
||||||
|
else:
|
||||||
|
crc >>= 1
|
||||||
|
return crc & 0xFFFF
|
||||||
|
|
||||||
|
class Spoiler(QtWidgets.QWidget):
|
||||||
|
def __init__(self, title="", animationDuration=300, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._animationDuration = animationDuration
|
||||||
|
|
||||||
|
# --- Toggle button ---
|
||||||
|
self.toggleButton = QtWidgets.QToolButton(self)
|
||||||
|
self.toggleButton.setStyleSheet("QToolButton { border: none; }")
|
||||||
|
self.toggleButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
|
||||||
|
self.toggleButton.setArrowType(QtCore.Qt.RightArrow)
|
||||||
|
self.toggleButton.setText(title)
|
||||||
|
self.toggleButton.setCheckable(True)
|
||||||
|
|
||||||
|
# --- Header line ---
|
||||||
|
self.headerLine = QtWidgets.QFrame(self)
|
||||||
|
self.headerLine.setFrameShape(QtWidgets.QFrame.HLine)
|
||||||
|
self.headerLine.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||||
|
|
||||||
|
# --- Content area ---
|
||||||
|
self.contentArea = QtWidgets.QScrollArea(self)
|
||||||
|
self.contentArea.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||||
|
self.contentArea.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||||
|
self.contentArea.setWidgetResizable(True)
|
||||||
|
self._contentWidget = QtWidgets.QWidget()
|
||||||
|
self.contentArea.setWidget(self._contentWidget)
|
||||||
|
self.contentArea.setMaximumHeight(0)
|
||||||
|
|
||||||
|
# --- Анимация только по контенту ---
|
||||||
|
self._ani_content = QtCore.QPropertyAnimation(self.contentArea, b"maximumHeight")
|
||||||
|
self._ani_content.setDuration(animationDuration)
|
||||||
|
self._ani_content.setEasingCurve(QtCore.QEasingCurve.InOutCubic)
|
||||||
|
|
||||||
|
# Следим за шагами анимации → обновляем родителя
|
||||||
|
self._ani_content.valueChanged.connect(self._adjust_parent_size)
|
||||||
|
|
||||||
|
# --- Layout ---
|
||||||
|
self.mainLayout = QtWidgets.QGridLayout(self)
|
||||||
|
self.mainLayout.setVerticalSpacing(0)
|
||||||
|
self.mainLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.mainLayout.addWidget(self.toggleButton, 0, 0, 1, 1)
|
||||||
|
self.mainLayout.addWidget(self.headerLine, 0, 1, 1, 1)
|
||||||
|
self.mainLayout.addWidget(self.contentArea, 1, 0, 1, 2)
|
||||||
|
|
||||||
|
# --- Signals ---
|
||||||
|
self.toggleButton.clicked.connect(self._on_toggled)
|
||||||
|
|
||||||
|
def setContentLayout(self, contentLayout):
|
||||||
|
old = self._contentWidget.layout()
|
||||||
|
if old:
|
||||||
|
QtWidgets.QWidget().setLayout(old)
|
||||||
|
self._contentWidget.setLayout(contentLayout)
|
||||||
|
|
||||||
|
def _adjust_parent_size(self, *_):
|
||||||
|
top = self.window()
|
||||||
|
if top:
|
||||||
|
size = top.size()
|
||||||
|
size.setHeight(top.sizeHint().height()) # берём новую высоту
|
||||||
|
top.resize(size) # ширина остаётся прежней
|
||||||
|
|
||||||
|
def _on_toggled(self, checked: bool):
|
||||||
|
self.toggleButton.setArrowType(QtCore.Qt.DownArrow if checked else QtCore.Qt.RightArrow)
|
||||||
|
|
||||||
|
contentHeight = self._contentWidget.sizeHint().height()
|
||||||
|
self._ani_content.stop()
|
||||||
|
self._ani_content.setStartValue(self.contentArea.maximumHeight())
|
||||||
|
self._ani_content.setEndValue(contentHeight if checked else 0)
|
||||||
|
|
||||||
|
# --- Фиксируем ширину на время анимации ---
|
||||||
|
w = self.width()
|
||||||
|
self.setFixedWidth(w)
|
||||||
|
self._ani_content.finished.connect(lambda: self.setMaximumWidth(16777215)) # сброс фикса
|
||||||
|
|
||||||
|
self._ani_content.start()
|
||||||
|
|
||||||
class DebugTerminalWidget(QtWidgets.QWidget):
|
class DebugTerminalWidget(QtWidgets.QWidget):
|
||||||
nameRead = QtCore.Signal(int, int, int, str) # index, status, iq, name
|
nameRead = QtCore.Signal(int, int, int, str) # index, status, iq, name
|
||||||
@ -83,7 +107,9 @@ class DebugTerminalWidget(QtWidgets.QWidget):
|
|||||||
signed=True,
|
signed=True,
|
||||||
iq_scaling=None,
|
iq_scaling=None,
|
||||||
read_timeout_ms=200,
|
read_timeout_ms=200,
|
||||||
auto_crc_check=True):
|
auto_crc_check=True,
|
||||||
|
drop_if_busy=False,
|
||||||
|
replace_if_busy=True):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.start_byte = start_byte
|
self.start_byte = start_byte
|
||||||
self.cmd_byte = cmd_byte
|
self.cmd_byte = cmd_byte
|
||||||
@ -91,25 +117,43 @@ class DebugTerminalWidget(QtWidgets.QWidget):
|
|||||||
self.signed = signed
|
self.signed = signed
|
||||||
self.read_timeout_ms = read_timeout_ms
|
self.read_timeout_ms = read_timeout_ms
|
||||||
self.auto_crc_check = auto_crc_check
|
self.auto_crc_check = auto_crc_check
|
||||||
self._busy = False
|
|
||||||
self._queue = deque()
|
# lockstep policy flags
|
||||||
|
self._drop_if_busy = drop_if_busy
|
||||||
|
self._replace_if_busy = replace_if_busy
|
||||||
|
|
||||||
if iq_scaling is None:
|
if iq_scaling is None:
|
||||||
iq_scaling = {n: float(1 << n) for n in range(16)}
|
iq_scaling = {n: float(1 << n) for n in range(16)}
|
||||||
iq_scaling[0] = 1.0
|
iq_scaling[0] = 1.0
|
||||||
self.iq_scaling = iq_scaling
|
self.iq_scaling = iq_scaling
|
||||||
|
|
||||||
# Serial port
|
# Serial port ---------------------------------------------------------
|
||||||
self.serial = QtSerialPort.QSerialPort(self)
|
self.serial = QtSerialPort.QSerialPort(self)
|
||||||
self.serial.setBaudRate(115200)
|
self.serial.setBaudRate(115200)
|
||||||
self.serial.readyRead.connect(self._on_ready_read)
|
self.serial.readyRead.connect(self._on_ready_read)
|
||||||
self.serial.errorOccurred.connect(self._on_serial_error)
|
self.serial.errorOccurred.connect(self._on_serial_error)
|
||||||
|
self._index_change_timer = QtCore.QTimer(self)
|
||||||
|
self._index_change_timer.setSingleShot(True)
|
||||||
|
self._index_change_timer.timeout.connect(self._on_index_change_timeout)
|
||||||
|
self._index_change_delay_ms = 200 # задержка перед отправкой запроса
|
||||||
|
|
||||||
|
# RX state ------------------------------------------------------------
|
||||||
self._rx_buf = bytearray()
|
self._rx_buf = bytearray()
|
||||||
self._waiting_name = False
|
self._waiting_name = False
|
||||||
self._expected_min_len = 0
|
self._expected_min_len = 0
|
||||||
|
self._expected_exact_len = None # if known exactly
|
||||||
|
|
||||||
# Timer for polling
|
# Lockstep tx/rx state ------------------------------------------------
|
||||||
|
self._busy = False # True => запрос отправлен, ждём ответ/таймаут
|
||||||
|
self._pending_cmd = None # (frame, is_name, index) ожидающий отправки
|
||||||
|
self._active_index = None # индекс текущего запроса (для сигналов)
|
||||||
|
|
||||||
|
# Timer for per-transaction timeout ----------------------------------
|
||||||
|
self._txn_timer = QtCore.QTimer(self)
|
||||||
|
self._txn_timer.setSingleShot(True)
|
||||||
|
self._txn_timer.timeout.connect(self._on_txn_timeout)
|
||||||
|
|
||||||
|
# Polling timer -------------------------------------------------------
|
||||||
self._poll_timer = QtCore.QTimer(self)
|
self._poll_timer = QtCore.QTimer(self)
|
||||||
self._poll_timer.timeout.connect(self._on_poll_timeout)
|
self._poll_timer.timeout.connect(self._on_poll_timeout)
|
||||||
self._polling = False
|
self._polling = False
|
||||||
@ -205,19 +249,27 @@ class DebugTerminalWidget(QtWidgets.QWidget):
|
|||||||
var_layout.addWidget(self.chk_raw, 5, 2)
|
var_layout.addWidget(self.chk_raw, 5, 2)
|
||||||
|
|
||||||
main_layout.addWidget(var_group)
|
main_layout.addWidget(var_group)
|
||||||
|
|
||||||
|
# --- Collapsible UART Log ---
|
||||||
|
self.log_spoiler = Spoiler("UART Log", animationDuration=300, parent=self)
|
||||||
|
|
||||||
# --- UART Log Group ---
|
log_layout = QtWidgets.QVBoxLayout()
|
||||||
log_group = QtWidgets.QGroupBox("UART Log")
|
|
||||||
log_layout = QtWidgets.QVBoxLayout(log_group)
|
|
||||||
|
|
||||||
self.txt_log = QtWidgets.QTextEdit()
|
self.txt_log = QtWidgets.QTextEdit()
|
||||||
self.txt_log.setReadOnly(True)
|
self.txt_log.setReadOnly(True)
|
||||||
self.txt_log.setFontFamily("Courier")
|
self.txt_log.setFontFamily("Courier")
|
||||||
log_layout.addWidget(self.txt_log)
|
log_layout.addWidget(self.txt_log)
|
||||||
|
|
||||||
main_layout.addWidget(log_group, 1)
|
self.log_spoiler.setContentLayout(log_layout)
|
||||||
|
main_layout.addWidget(self.log_spoiler, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def _toggle_log_panel(self, checked):
|
||||||
|
if checked:
|
||||||
|
self.toggle_log_btn.setArrowType(QtCore.Qt.DownArrow)
|
||||||
|
self.log_panel.show()
|
||||||
|
else:
|
||||||
|
self.toggle_log_btn.setArrowType(QtCore.Qt.RightArrow)
|
||||||
|
self.log_panel.hide()
|
||||||
# ----------------------------------------------------------- Port mgmt ---
|
# ----------------------------------------------------------- Port mgmt ---
|
||||||
def set_available_ports(self):
|
def set_available_ports(self):
|
||||||
"""Enumerate COM ports and repopulate combo box."""
|
"""Enumerate COM ports and repopulate combo box."""
|
||||||
@ -255,7 +307,7 @@ class DebugTerminalWidget(QtWidgets.QWidget):
|
|||||||
self.btn_open.setText("Close")
|
self.btn_open.setText("Close")
|
||||||
self._log(f"[PORT] Opened {port_name} @ {baud}")
|
self._log(f"[PORT] Opened {port_name} @ {baud}")
|
||||||
self.portOpened.emit(port_name)
|
self.portOpened.emit(port_name)
|
||||||
|
|
||||||
# --------------------------------------------------------- Frame build ---
|
# --------------------------------------------------------- Frame build ---
|
||||||
def _build_request(self, index: int, read_name: bool) -> bytes:
|
def _build_request(self, index: int, read_name: bool) -> bytes:
|
||||||
if read_name:
|
if read_name:
|
||||||
@ -265,37 +317,75 @@ class DebugTerminalWidget(QtWidgets.QWidget):
|
|||||||
hi = (dbg >> 8) & 0xFF
|
hi = (dbg >> 8) & 0xFF
|
||||||
lo = dbg & 0xFF
|
lo = dbg & 0xFF
|
||||||
return bytes([self.start_byte & 0xFF, self.cmd_byte & 0xFF, hi, lo])
|
return bytes([self.start_byte & 0xFF, self.cmd_byte & 0xFF, hi, lo])
|
||||||
|
|
||||||
|
# ------------------------------- PUBLIC API (safe single outstanding) ---
|
||||||
def request_name(self):
|
def request_name(self):
|
||||||
index = int(self.spin_index.value())
|
self._issue_command(is_name=True)
|
||||||
frame = self._build_request(index, True)
|
|
||||||
self._enqueue_command(frame, is_name=True)
|
|
||||||
|
|
||||||
def request_value(self):
|
def request_value(self):
|
||||||
|
self._issue_command(is_name=False)
|
||||||
|
|
||||||
|
def _issue_command(self, *, is_name: bool):
|
||||||
index = int(self.spin_index.value())
|
index = int(self.spin_index.value())
|
||||||
frame = self._build_request(index, False)
|
frame = self._build_request(index, is_name)
|
||||||
self._enqueue_command(frame, is_name=False)
|
|
||||||
|
if self._busy:
|
||||||
def _enqueue_command(self, frame: bytes, is_name: bool):
|
if self._drop_if_busy:
|
||||||
self._queue.append((frame, is_name))
|
self._log("[LOCKSTEP] Busy -> drop new request")
|
||||||
if not self._busy:
|
return
|
||||||
self._process_next_command()
|
if self._replace_if_busy:
|
||||||
|
self._pending_cmd = (frame, is_name, index)
|
||||||
def _process_next_command(self):
|
self._log("[LOCKSTEP] Busy -> replaced pending request")
|
||||||
if not self._queue:
|
else:
|
||||||
|
# queue disabled; ignore
|
||||||
|
self._log("[LOCKSTEP] Busy -> ignore (no replace)")
|
||||||
return
|
return
|
||||||
frame, is_name = self._queue.popleft()
|
|
||||||
|
# idle -> send immediately
|
||||||
|
self._start_transaction(frame, is_name, index)
|
||||||
|
|
||||||
|
# ------------------------------------------------------ TXN lifecycle ---
|
||||||
|
def _start_transaction(self, frame: bytes, is_name: bool, index: int):
|
||||||
|
"""Mark busy, compute expected length, send frame, start timeout."""
|
||||||
self._busy = True
|
self._busy = True
|
||||||
|
self._active_index = index
|
||||||
self._waiting_name = is_name
|
self._waiting_name = is_name
|
||||||
|
# Expected minimal len: hdr[4] + payload(name/val) + crc/trailer[4]
|
||||||
if is_name:
|
if is_name:
|
||||||
self._expected_min_len = 4 + self.name_field_len + 4
|
self._expected_min_len = 4 + self.name_field_len + 4
|
||||||
else:
|
else:
|
||||||
self._expected_min_len = 4 + 2 + 4
|
self._expected_min_len = 4 + 2 + 4
|
||||||
|
self._expected_exact_len = self._expected_min_len # protocol fixed-size now
|
||||||
|
self._rx_buf.clear()
|
||||||
|
self._set_ui_busy(True)
|
||||||
self._send(frame)
|
self._send(frame)
|
||||||
|
self._txn_timer.start(self.read_timeout_ms)
|
||||||
|
|
||||||
def _finish_command(self):
|
def _end_transaction(self):
|
||||||
|
"""Common exit path after parse or timeout."""
|
||||||
|
self._txn_timer.stop()
|
||||||
self._busy = False
|
self._busy = False
|
||||||
self._process_next_command()
|
self._active_index = None
|
||||||
|
self._expected_min_len = 0
|
||||||
|
self._expected_exact_len = None
|
||||||
|
self._rx_buf.clear()
|
||||||
|
self._set_ui_busy(False)
|
||||||
|
# if we have pending -> fire it now
|
||||||
|
if self._pending_cmd is not None:
|
||||||
|
frame, is_name, index = self._pending_cmd
|
||||||
|
self._pending_cmd = None
|
||||||
|
# start immediately (no recursion issues; single-shot via singleShot)
|
||||||
|
QtCore.QTimer.singleShot(0, lambda f=frame, n=is_name, i=index: self._start_transaction(f, n, i))
|
||||||
|
|
||||||
|
def _on_txn_timeout(self):
|
||||||
|
if not self._busy:
|
||||||
|
return
|
||||||
|
self._log("[TIMEOUT] Response not received in time; aborting transaction")
|
||||||
|
# log any garbage that came in
|
||||||
|
if self._rx_buf:
|
||||||
|
self._log_frame(bytes(self._rx_buf), tx=False)
|
||||||
|
self._end_transaction()
|
||||||
|
|
||||||
# --------------------------------------------------------------- TX/RX ---
|
# --------------------------------------------------------------- TX/RX ---
|
||||||
def _send(self, data: bytes):
|
def _send(self, data: bytes):
|
||||||
n = self.serial.write(data)
|
n = self.serial.write(data)
|
||||||
@ -303,41 +393,49 @@ class DebugTerminalWidget(QtWidgets.QWidget):
|
|||||||
self._log(f"[ERR] Write incomplete: {n}/{len(data)}")
|
self._log(f"[ERR] Write incomplete: {n}/{len(data)}")
|
||||||
self.txBytes.emit(data)
|
self.txBytes.emit(data)
|
||||||
self._log_frame(data, tx=True)
|
self._log_frame(data, tx=True)
|
||||||
# start a timeout timer to clear buffer if no response
|
|
||||||
QtCore.QTimer.singleShot(self.read_timeout_ms, self._check_timeout)
|
|
||||||
|
|
||||||
def _check_timeout(self):
|
|
||||||
self._busy = False
|
|
||||||
data = self.serial.readAll()
|
|
||||||
chunk = bytes(data)
|
|
||||||
if chunk:
|
|
||||||
self._log_frame(chunk, tx=False) # <-- логируем каждую порцию
|
|
||||||
self._rx_buf.extend(chunk)
|
|
||||||
if self._expected_min_len and len(self._rx_buf) >= self._expected_min_len:
|
|
||||||
self._log("[TIMEOUT] No complete response")
|
|
||||||
self._rx_buf.clear()
|
|
||||||
self._expected_min_len = 0
|
|
||||||
self._finish_command()
|
|
||||||
|
|
||||||
def _on_ready_read(self):
|
def _on_ready_read(self):
|
||||||
|
if not self._busy:
|
||||||
|
# unexpected data while idle -> just log & drop
|
||||||
|
chunk = self.serial.readAll().data()
|
||||||
|
if chunk:
|
||||||
|
self._log("[WARN] RX while idle -> ignored")
|
||||||
|
self._log_frame(chunk, tx=False)
|
||||||
|
return
|
||||||
|
|
||||||
self._rx_buf.extend(self.serial.readAll().data())
|
self._rx_buf.extend(self.serial.readAll().data())
|
||||||
# if we know the minimum expected length, test
|
|
||||||
if self._expected_min_len and len(self._rx_buf) >= self._expected_min_len:
|
# If exact length known and reached -> parse immediately (no wait for timeout)
|
||||||
# parse frame
|
if self._expected_exact_len is not None and len(self._rx_buf) >= self._expected_exact_len:
|
||||||
frame = bytes(self._rx_buf)
|
frame = bytes(self._rx_buf[:self._expected_exact_len])
|
||||||
self._rx_buf.clear()
|
# log rx; if extra bytes remain we'll keep them for next txn (unlikely)
|
||||||
self._expected_min_len = 0
|
|
||||||
self.rxBytes.emit(frame)
|
self.rxBytes.emit(frame)
|
||||||
self._log_frame(frame, tx=False)
|
self._log_frame(frame, tx=False)
|
||||||
self._parse_response(frame)
|
self._parse_response(frame)
|
||||||
|
# discard everything consumed
|
||||||
self._finish_command()
|
del self._rx_buf[:self._expected_exact_len]
|
||||||
|
if self._rx_buf:
|
||||||
|
self._log("[WARN] Extra RX bytes after frame -> stash for next txn")
|
||||||
|
self._end_transaction()
|
||||||
|
return
|
||||||
|
|
||||||
|
# If only min len known: check >= min -> try parse; else keep waiting (timer still running)
|
||||||
|
if self._expected_min_len and len(self._rx_buf) >= self._expected_min_len:
|
||||||
|
frame = bytes(self._rx_buf)
|
||||||
|
self.rxBytes.emit(frame)
|
||||||
|
self._log_frame(frame, tx=False)
|
||||||
|
self._parse_response(frame)
|
||||||
|
self._end_transaction()
|
||||||
|
return
|
||||||
|
# else: wait for more data or timeout
|
||||||
|
|
||||||
def _on_serial_error(self, err):
|
def _on_serial_error(self, err):
|
||||||
if err == QtSerialPort.QSerialPort.NoError:
|
if err == QtSerialPort.QSerialPort.NoError:
|
||||||
return
|
return
|
||||||
self._log(f"[SERIAL ERR] {self.serial.errorString()} ({err})")
|
self._log(f"[SERIAL ERR] {self.serial.errorString()} ({err})")
|
||||||
|
# treat as txn failure if busy
|
||||||
|
if self._busy:
|
||||||
|
self._end_transaction()
|
||||||
# ------------------------------------------------------------- Parsing ---
|
# ------------------------------------------------------------- Parsing ---
|
||||||
def _parse_response(self, frame: bytes):
|
def _parse_response(self, frame: bytes):
|
||||||
# basic length check
|
# basic length check
|
||||||
@ -373,6 +471,8 @@ class DebugTerminalWidget(QtWidgets.QWidget):
|
|||||||
if cmd != self.cmd_byte:
|
if cmd != self.cmd_byte:
|
||||||
self._log(f"[WARN] Unexpected cmd 0x{cmd:02X}")
|
self._log(f"[WARN] Unexpected cmd 0x{cmd:02X}")
|
||||||
|
|
||||||
|
index = self._active_index if self._active_index is not None else self.spin_index.value()
|
||||||
|
|
||||||
if self._waiting_name:
|
if self._waiting_name:
|
||||||
name_bytes = payload[4:4 + self.name_field_len]
|
name_bytes = payload[4:4 + self.name_field_len]
|
||||||
# stop at first NUL
|
# stop at first NUL
|
||||||
@ -383,12 +483,22 @@ class DebugTerminalWidget(QtWidgets.QWidget):
|
|||||||
name_str = name_bytes.decode(errors="replace")
|
name_str = name_bytes.decode(errors="replace")
|
||||||
self.edit_name.setText(name_str)
|
self.edit_name.setText(name_str)
|
||||||
self.lbl_iq.setText(str(iq))
|
self.lbl_iq.setText(str(iq))
|
||||||
self.nameRead.emit(self.spin_index.value(), status, iq, name_str)
|
self.nameRead.emit(index, status, iq, name_str)
|
||||||
else:
|
else:
|
||||||
|
# Чтение значения
|
||||||
|
if status != 0:
|
||||||
|
# Ошибка чтения переменной — считаем её недействительной
|
||||||
|
self.edit_value.setText("INVALID")
|
||||||
|
self.edit_value.setStyleSheet("color: red; font-weight: bold;")
|
||||||
|
self.lbl_iq.setText("-")
|
||||||
|
self.valueRead.emit(index, status, iq, 0, float('nan'))
|
||||||
|
self._log(f"[ERR] Variable at index {index} invalid, status={status}")
|
||||||
|
return
|
||||||
|
|
||||||
raw_lo = payload[4] if len(payload) > 4 else 0
|
raw_lo = payload[4] if len(payload) > 4 else 0
|
||||||
raw_hi = payload[5] if len(payload) > 5 else 0
|
raw_hi = payload[5] if len(payload) > 5 else 0
|
||||||
raw16 = (raw_hi << 8) | raw_lo
|
raw16 = (raw_hi << 8) | raw_lo
|
||||||
if self.signed and raw16 & 0x8000:
|
if self.signed and (raw16 & 0x8000):
|
||||||
raw_signed = raw16 - 0x10000
|
raw_signed = raw16 - 0x10000
|
||||||
else:
|
else:
|
||||||
raw_signed = raw16
|
raw_signed = raw16
|
||||||
@ -401,9 +511,10 @@ class DebugTerminalWidget(QtWidgets.QWidget):
|
|||||||
disp = f"{float_val:.6g}" # compact
|
disp = f"{float_val:.6g}" # compact
|
||||||
self.edit_value.setText(disp)
|
self.edit_value.setText(disp)
|
||||||
self.lbl_iq.setText(str(iq))
|
self.lbl_iq.setText(str(iq))
|
||||||
self.valueRead.emit(self.spin_index.value(), status, iq, raw_signed, float_val)
|
self.valueRead.emit(index, status, iq, raw_signed, float_val)
|
||||||
|
|
||||||
# -------------------------------------------------------------- Helpers ---
|
# -------------------------------------------------------------- Helpers ---
|
||||||
|
|
||||||
def _toggle_index_base(self, state):
|
def _toggle_index_base(self, state):
|
||||||
val = self.spin_index.value()
|
val = self.spin_index.value()
|
||||||
if state == QtCore.Qt.Checked:
|
if state == QtCore.Qt.Checked:
|
||||||
@ -417,33 +528,60 @@ class DebugTerminalWidget(QtWidgets.QWidget):
|
|||||||
|
|
||||||
def _on_index_changed(self, new_index: int):
|
def _on_index_changed(self, new_index: int):
|
||||||
if self._polling:
|
if self._polling:
|
||||||
# В режиме polling при изменении индекса автоматически запрашиваем имя
|
self._index_change_timer.start(self._index_change_delay_ms)
|
||||||
self.request_name()
|
|
||||||
|
def _on_index_change_timeout(self):
|
||||||
|
# Здесь запускаем запрос имени или значения по новому индексу
|
||||||
|
if self._polling:
|
||||||
|
# если включён polling — можно просто перезапустить опрос с новым индексом
|
||||||
|
# например:
|
||||||
|
self._restart_polling_cycle()
|
||||||
|
|
||||||
|
def _restart_polling_cycle(self):
|
||||||
|
# Прервать текущую транзакцию (если есть)
|
||||||
|
if self._busy:
|
||||||
|
# Если занято — запустить таймер, который через немного проверит снова
|
||||||
|
# Можно, например, использовать QTimer.singleShot (если PyQt/PySide)
|
||||||
|
QTimer.singleShot(10, self._restart_polling_cycle) # через 100 мс повторить попытку
|
||||||
|
return
|
||||||
|
# можно отправить запрос имени, если нужно
|
||||||
|
self.request_name()
|
||||||
|
# Запустить следующий запрос
|
||||||
|
self._on_poll_timeout()
|
||||||
|
|
||||||
|
def _set_polling_ui(self, polling: bool):
|
||||||
|
# Если polling == True -> блокируем кнопки Read/Write, иначе разблокируем
|
||||||
|
self.btn_read_name.setDisabled(polling)
|
||||||
|
self.btn_read_value.setDisabled(polling)
|
||||||
|
|
||||||
def _toggle_polling(self):
|
def _toggle_polling(self):
|
||||||
if self._polling:
|
if self._polling:
|
||||||
self._poll_timer.stop()
|
self._poll_timer.stop()
|
||||||
self._polling = False
|
self._polling = False
|
||||||
self.btn_poll.setText("Start Polling")
|
self.btn_poll.setText("Start Polling")
|
||||||
self._log("[POLL] Polling stopped")
|
self._set_polling_ui(False)
|
||||||
|
self._log("[POLL] Stopped")
|
||||||
else:
|
else:
|
||||||
if not self.serial.isOpen():
|
|
||||||
self._log("[WARN] Port not open. Cannot start polling.")
|
|
||||||
return
|
|
||||||
interval = self.spin_interval.value()
|
interval = self.spin_interval.value()
|
||||||
self._poll_timer.start(interval)
|
self._poll_timer.start(interval)
|
||||||
self._polling = True
|
self._polling = True
|
||||||
self.btn_poll.setText("Stop Polling")
|
self.btn_poll.setText("Stop Polling")
|
||||||
self._log(f"[POLL] Polling started with interval {interval} ms")
|
self._set_polling_ui(True)
|
||||||
self.request_name()
|
self._log(f"[POLL] Started, interval {interval} ms")
|
||||||
self._poll_once() # immediate first poll
|
|
||||||
|
|
||||||
def _on_poll_timeout(self):
|
def _on_poll_timeout(self):
|
||||||
self._poll_once()
|
self._poll_once()
|
||||||
|
|
||||||
def _poll_once(self):
|
def _poll_once(self):
|
||||||
if self._polling and self.serial.isOpen():
|
if self._polling and self.serial.isOpen() and not self._busy:
|
||||||
self.request_value()
|
self.request_value()
|
||||||
|
# если busy -> просто пропускаем тик; не ставим pending, чтобы не накапливать очередь
|
||||||
|
|
||||||
|
def _set_ui_busy(self, busy: bool):
|
||||||
|
'''self.btn_read_name.setEnabled(not busy)
|
||||||
|
self.btn_read_value.setEnabled(not busy)
|
||||||
|
# Не запрещаем Stop Polling, иначе нельзя прервать зависший запрос
|
||||||
|
self.spin_index.setEnabled(not busy)'''
|
||||||
|
|
||||||
def _log(self, msg: str):
|
def _log(self, msg: str):
|
||||||
ts = datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
ts = datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||||
@ -457,25 +595,11 @@ class DebugTerminalWidget(QtWidgets.QWidget):
|
|||||||
self._log(f"[{dir_tag}] {hex_bytes} |{ascii_bytes}|")
|
self._log(f"[{dir_tag}] {hex_bytes} |{ascii_bytes}|")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- CRC util ---
|
|
||||||
def crc16_ibm(data: bytes, *, init=0xFFFF) -> int:
|
|
||||||
"""CRC16-IBM (aka CRC-16/ANSI, polynomial 0xA001 reflected)."""
|
|
||||||
crc = init
|
|
||||||
for b in data:
|
|
||||||
crc ^= b
|
|
||||||
for _ in range(8):
|
|
||||||
if crc & 1:
|
|
||||||
crc = (crc >> 1) ^ 0xA001
|
|
||||||
else:
|
|
||||||
crc >>= 1
|
|
||||||
return crc & 0xFFFF
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------- Demo harness ---
|
# ---------------------------------------------------------- Demo harness ---
|
||||||
class _DemoWindow(QtWidgets.QMainWindow):
|
class _DemoWindow(QtWidgets.QMainWindow):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.setWindowTitle("Debug Terminal Demo")
|
self.setWindowTitle("DebugVar Terminal")
|
||||||
self.term = DebugTerminalWidget(self)
|
self.term = DebugTerminalWidget(self)
|
||||||
self.setCentralWidget(self.term)
|
self.setCentralWidget(self.term)
|
||||||
# connect sample signals -> print
|
# connect sample signals -> print
|
||||||
@ -493,7 +617,5 @@ class _DemoWindow(QtWidgets.QMainWindow):
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = QtWidgets.QApplication(sys.argv)
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
win = _DemoWindow()
|
win = _DemoWindow()
|
||||||
win.resize(600, 500)
|
|
||||||
win.show()
|
win.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
|
@ -249,9 +249,9 @@ static int convertDebugVarToIQx(DebugVar_t *var, int32_t *ret_var)
|
|||||||
else
|
else
|
||||||
iq_final64 = iq_united64 >> (-shift);
|
iq_final64 = iq_united64 >> (-shift);
|
||||||
|
|
||||||
// Проверяем переполнение int32_t
|
// // Проверяем переполнение int32_t
|
||||||
if (iq_final64 > 2147483647 || iq_final64 < -2147483648)
|
// if (iq_final64 > 2147483647 || iq_final64 < -2147483648)
|
||||||
return 3; // переполнение
|
// return 3; // переполнение
|
||||||
|
|
||||||
*ret_var = (int32_t)iq_final64;
|
*ret_var = (int32_t)iq_final64;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user