diff --git a/doc/.gitignore b/doc/.gitignore new file mode 100644 index 0000000..88d85bf --- /dev/null +++ b/doc/.gitignore @@ -0,0 +1,217 @@ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml \ No newline at end of file diff --git a/doc/graph_widgets.py b/doc/graph_widgets.py new file mode 100644 index 0000000..124f47e --- /dev/null +++ b/doc/graph_widgets.py @@ -0,0 +1,562 @@ +# from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QLabel, QSlider, QLineEdit, QGridLayout, QPushButton, QCheckBox, QHBoxLayout, QInputDialog) +# from PyQt5.QtCore import pyqtSignal +# from PyQt5.QtGui import QDoubleValidator +import os +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QLabel, QSlider, QLineEdit, + QGridLayout, QPushButton, QCheckBox, QHBoxLayout, QInputDialog, QComboBox, QFileDialog) +from PyQt5.QtCore import pyqtSignal, Qt, QTimer +from PyQt5.QtGui import QDoubleValidator, QPixmap + +try: + from i18n import _ + import utils_ui + import utils + from widgets import EditRemarDialog +except Exception: + from COMTool.i18n import _ + from COMTool import utils_ui + from COMTool import utils + from COMTool.widgets import EditRemarDialog + +# from .graph_widget_metasenselite import Graph_MetaSenseLite +from .graph_widgets_base import Graph_Widget_Base +import pyqtgraph as pg +from struct import unpack, pack +import time + + +class Graph_Image(Graph_Widget_Base): + id = "image" + updateSignal = pyqtSignal(bytes) + startTimeoutSignal = pyqtSignal() + stopTimeoutSignal = pyqtSignal() + + def __init__(self, parent=None, hintSignal=lambda type, title, msg: None, + rmCallback=lambda widget: None, send=lambda x: None, config=None): + # 初始化配置,默认缩放倍数为10 + default = {"scale_factor": 10} + if config: + default.update(config) + super().__init__(parent, hintSignal, rmCallback, send, default, default) + + self.image_data = bytearray() + # 从配置获取缩放倍数 + self.scale_factor = self.config.get("scale_factor", 10) + + layout = QVBoxLayout() + self.setLayout(layout) + + self.imageLabel = QLabel("[ Waiting for image... ]") + self.imageLabel.setAlignment(Qt.AlignCenter) + layout.addWidget(self.imageLabel) + + self.clearBtn = QPushButton("Clear Image") + self.clearBtn.clicked.connect(self.clear) + layout.addWidget(self.clearBtn) + + self.updateSignal.connect(self.update_image) + + self.timeoutTimer = QTimer(self) + self.timeoutTimer.setInterval(2000) # 2 seconds timeout + self.timeoutTimer.timeout.connect(self.clear_buffer) + + self.startTimeoutSignal.connect(lambda: self.timeoutTimer.start()) + self.stopTimeoutSignal.emit() + + def clear(self): + self.image_data.clear() + self.imageLabel.clear() + self.imageLabel.setText("[ Cleared ]") + self.stopTimeoutSignal.emit() + + def clear_buffer(self): + self.image_data.clear() + self.imageLabel.setText("[ Timeout: buffer cleared ]") + self.stopTimeoutSignal.emit() + + def onData(self, data: bytes): + self.stopTimeoutSignal.emit() # Stop any running timer when data is received + self.image_data.extend(data) + + # Check for common image signatures + if self._is_complete_jpeg(self.image_data) or \ + self._is_complete_png(self.image_data) or \ + self._is_complete_bmp(self.image_data): + self.updateSignal.emit(bytes(self.image_data)) + self.image_data.clear() + else: + self.startTimeoutSignal.emit() # Restart timer only if not complete + + def _is_complete_jpeg(self, data): + return data[:2] == b'\xFF\xD8' and data[-2:] == b'\xFF\xD9' + + def _is_complete_png(self, data): + return data[:8] == b'\x89PNG\r\n\x1a\n' and b'IEND' in data[-20:] + + def _is_complete_bmp(self, data): + return data[:2] == b'BM' and len(data) >= int.from_bytes(data[2:6], 'little') + + def update_image(self, img_bytes: bytes): + pixmap = QPixmap() + if pixmap.loadFromData(img_bytes): + # 使用配置的缩放倍数进行缩放 + scaled_width = pixmap.width() * self.scale_factor + scaled_height = pixmap.height() * self.scale_factor + scaled = pixmap.scaled( + scaled_width, + scaled_height, + Qt.KeepAspectRatio, + Qt.SmoothTransformation + ) + self.imageLabel.setPixmap(scaled) + else: + self.imageLabel.setText("[ Failed to decode image ]") + +class Graph_Plot(Graph_Widget_Base): + updateSignal = pyqtSignal(dict) + id = "plot" + + def __init__(self, parent=None, hintSignal=lambda type, title, msg: None, rmCallback=lambda widget: None, send=lambda x: None, config=None): + default = { + "xRange": 10, + "xRangeEnable": True, + "header": "\\xAA\\xCC\\xEE\\xBB", + "binary_protocol": False, + } + super().__init__(parent, hintSignal=hintSignal, rmCallback=rmCallback, + send=send, config=config, defaultConfig=default) + self.headerBytes = utils.str_to_bytes( + self.config["header"], escape=True, encoding="utf-8") + self.layout = QGridLayout() + self.setLayout(self.layout) + self.plotWin = pg.GraphicsLayoutWidget() + self.plotWin.setMinimumHeight(200) + self.default_x = 0; + pg.setConfigOptions(antialias=True) + rmBtn = QPushButton(_("Remove")) + clearBtn = QPushButton(_("Clear")) + rangeLabel = QLabel(_("Range:")) + rangeConf = QLineEdit(str(self.config["xRange"])) + rangeEnable = QCheckBox(_("Enable")) + rangeEnable.setChecked(self.config["xRangeEnable"]) + self.binary_protocol = QCheckBox(_("Binary Protocol")) + headerLabel = QLabel(_("Header:")) + headerConf = QLineEdit(self.config["header"]) + self.headerBtn = QPushButton(_("Set")) + hint = _("Protocol: header + 1Byte name length + name + 8Bytes x(double) + 8Bytes y(double) + 1Byte sum\n" + "Protocol example code see help") + headerConf.setToolTip(hint) + self.binary_widgets = [headerLabel, headerConf, self.headerBtn] + self.binary_protocol.setToolTip(_('Check this if you want to use binary protocol, or use ASCII protocol(e.g. "$data1,10.0,200.0\\n"), more see help button on the top of window')) + self.show_binary_protocol_widgets(self.config["binary_protocol"]) + headerLabel.setToolTip(hint) + self.headerBtn.setToolTip(hint) + validator = QDoubleValidator() + rangeConf.setValidator(validator) + self.layout.addWidget(self.plotWin, 0, 0, 1, 3) + self.layout.addWidget(rmBtn, 1, 0, 1, 1) + self.layout.addWidget(clearBtn, 1, 2, 1, 1) + self.layout.addWidget(rangeLabel, 2, 0, 1, 1) + self.layout.addWidget(rangeConf, 2, 1, 1, 1) + self.layout.addWidget(rangeEnable, 2, 2, 1, 1) + self.layout.addWidget(self.binary_protocol, 3, 0, 1, 1) + self.layout.addWidget(headerLabel, 4, 0, 1, 1) + self.layout.addWidget(headerConf, 4, 1, 1, 1) + self.layout.addWidget(self.headerBtn, 4, 2, 1, 1) + self.resize(600, 400) + self.p = self.plotWin.addPlot(colspan=2) + # self.p.setLabel('bottom', 'x', '') + self.p.addLegend() + self.p.setXRange(0, self.config["xRange"]) + self.updateSignal.connect(self.update) + self.curves = {} + self.rawData = b'' + self.data = {} + self.builtinColors = [ + "#BD4B4B", + "#3BB273", + "#FFFFFA", + "#307473", + "#3C6997", + "#746D75", + "#228CDB", + "#824C71", + "#7768AE", + "#DC6BAD", + "#607d8b", + "#F18701", + "#912F40", + "#414288", + "#ED4D6E", + "#FFD29D", + "#B56576", + "#503B31", + "#93E1D8", + "#596157", + ] + self.notUsedColors = self.builtinColors.copy() + self.colors = { + + } + rangeConf.textChanged.connect(self.setRange) + rangeEnable.clicked.connect( + lambda: self.setEnableRange(rangeEnable.isChecked())) + self.headerBtn.clicked.connect( + lambda: self.setHeader(headerConf.text())) + rmBtn.clicked.connect(self.remove) + headerConf.textChanged.connect(self.headerChanged) + clearBtn.clicked.connect(self.clear) + self.binary_protocol.clicked.connect(self.en_binary_protocol) + + def remove(self): + self.rmCallback(self) + + def en_binary_protocol(self): + self.rawData = b'' + self.config["binary_protocol"] = self.binary_protocol.isChecked() + self.show_binary_protocol_widgets(self.config["binary_protocol"]) + + def show_binary_protocol_widgets(self, show=True): + for w in self.binary_widgets: + w.setVisible(show) + + def clear(self): + self.data = {} + self.curves = {} + self.notUsedColors = self.builtinColors.copy() + self.colors = {} + self.p.clear() + + def setRange(self, text): + if text: + self.config["xRange"] = float(text) + + def setEnableRange(self, en): + self.config["xRangeEnable"] = en + + def headerChanged(self, text): + if self.config["header"] != text: + self.headerBtn.setText(_("Set") + " *") + else: + self.headerBtn.setText(_("Set")) + + def setHeader(self, text): + if text: + try: + textBytes = utils.str_to_bytes( + text, escape=True, encoding="utf-8") + self.config["header"] = text + self.headerBytes = textBytes + self.headerBtn.setText(_("Set")) + except Exception: + self.hintSignal.emit("error", _("Error"), _("Format error")) + + def decodeData(self, data: bytes): + ''' + @data bytes, protocol: + | header(4B) | line name len(1B) | line name | x(8B) | y(8B) | sum(1B) | + | AA CC EE BB | 1~255 | roll | double | double | uint8 | + @return haveFrame, dict { + "name": { + "x": [], + "y": [] + } + } + ''' + # append data + self.rawData += data + # find header + header = self.headerBytes + idx = self.rawData.find(header) + if idx < 0: + return False, self.data + self.rawData = self.rawData[idx:] + # check data length + nameLen = unpack("B", self.rawData[len(header): len(header) + 1])[0] + frameLen = len(header) + nameLen + 18 # 5 + nameLen + 16 + 1 + if len(self.rawData) < frameLen: + return False, self.data + # get data + frame = self.rawData[:frameLen] + self.rawData = self.rawData[frameLen:] + _sum = frame[-1] + # check sum + if _sum != sum(frame[:frameLen - 1]) % 256: + return True, self.data + name = frame[len(header) + 1: len(header) + + 1 + nameLen].decode("utf-8") + x = unpack("d", frame[-17:-9])[0] + y = unpack("d", frame[-9:-1])[0] + if not name in self.data: + self.data[name] = { + "x": [x], + "y": [y] + } + else: + self.data[name]["x"].append(x) + self.data[name]["y"].append(y) + return True, self.data + + def decodeDataAscii(self, data: bytes): + ''' + @data bytes, protocol: + $[line name],[x],[y]<,checksum>\n + or $[line name],[y] + "$" means start of frame, "," means separator, + checksum is optional, checksum is sum of all bytes in frame except ",checksum". + e.g. + "$roll,2.0\n" + "$roll,1.0,2.0\n" + "$pitch,1.0,2.0\r\n" + "$pitch,1.0,2.0,179\n" , the 179 = sum(ord(c) for c in "$pitch,1.0,2.0") % 256 + @return haveFrame, dict { + "name": { + "x": [], + "y": [] + } + } + ''' + # append data + self.rawData += data + # find header + header = b"$" + end = b"\n" + idx = self.rawData.find(header) + if idx < 0: + return False, self.data + self.rawData = self.rawData[idx:] + # find \n + idx = self.rawData.find(end) + if idx < 0: + return False, self.data + # get data + frame = self.rawData[1:idx] # pitch,1.0,2.0 pitch,1.0,2.0,179 + self.rawData = self.rawData[idx + 1:] + items = frame.split(b",") + if len(items) != 2 and len(items) != 3 and len(items) != 4: + print("-- format error, frame item len(%s, should 3 or 4) error: %s" % (len(items), frame)) + return False, self.data + try: + if len(items) == 2: + x = float(self.default_x) + y = float(items[1]) + self.default_x += 1 + else: + x = float(items[1]) + y = float(items[2]) + except Exception: + print("-- x or y format error, frame: %s" % frame) + return False, self.data + # checksum + if len(items) == 4: + try: + _sum = int(items[3]) + except Exception: + print("-- checksum format error({} not int), frame: {}".format(items[3], frame)) + return False, self.data + # get last , index + idx = frame.rfind(b",") + if sum(header + frame[:idx]) % 256 != _sum: + print("-- checksum error") + return False, self.data + name = items[0].decode("utf-8") + if not name in self.data: + self.data[name] = { + "x": [x], + "y": [y] + } + else: + self.data[name]["x"].append(x) + self.data[name]["y"].append(y) + return True, self.data + + def pickColor(self, name: str): + if name in self.colors: + return self.colors[name] + else: + if not self.notUsedColors: + self.notUsedColors = self.builtinColors.copy() + color = self.notUsedColors.pop(0) + self.colors[name] = color + return color + + def update(self, data: dict): + for k, v in data.items(): + if not k in self.curves: + print("add curve:", k) + color = self.pickColor(k) + self.colors[k] = color + self.curves[k] = self.p.plot(pen=pg.mkPen(color=color, width=2), + name=k,) + # symbolBrush=color, symbolPen='w', symbol='o', symbolSize=1) + if self.config["xRangeEnable"] and self.config["xRange"] > 0: + self.p.setXRange( + v["x"][-1] - self.config["xRange"], v["x"][-1]) + self.curves[k].setData(x=v["x"], y=v["y"]) + + def onData(self, data: bytes): + while 1: + haveFrame, allData = self.decodeData(data) if self.config["binary_protocol"] else self.decodeDataAscii(data) + if not haveFrame: + break + data = b'' + self.updateSignal.emit(allData) + + +class Graph_Button(Graph_Widget_Base): + class Button(QWidget): + def __init__(self, rmCallback, addCallback, clickCallback, changeCallback, + value=("", "hello", "hello", "fa.circle", [])): + super().__init__() + if not value[0]: + self.id = str(int(time.time() * 1000)) + else: + self.id = value[0] + self.rmCallback = rmCallback + self.changeCallback = changeCallback + self.setProperty("class", "graphBtn") + layout = QHBoxLayout() + layoutSetting = QVBoxLayout() + self.setLayout(layout) + self.button = QPushButton(value[2]) + self.button.setProperty("class", "bigBtn") + self.value = value[2] + self.icon = value[3] + self.shortcut = value[4] + layout.addLayout(layoutSetting) + layout.addWidget(self.button) + self.addBtn = QPushButton() + self.rmBtn = QPushButton() + self.editBtn = QPushButton() + self.addBtn.setProperty("class", "smallBtn3") + self.rmBtn.setProperty("class", "smallBtn3") + self.editBtn.setProperty("class", "smallBtn3") + utils_ui.setButtonIcon(self.addBtn, "fa.plus") + utils_ui.setButtonIcon(self.rmBtn, "fa.minus") + utils_ui.setButtonIcon(self.editBtn, "fa.edit") + if self.icon: + utils_ui.setButtonIcon(self.button, self.icon) + if self.shortcut: + tip = self.getShortcutTip() + self.button.setToolTip(tip) + layoutSetting.addStretch() + layoutSetting.addWidget(self.rmBtn) + layoutSetting.addWidget(self.addBtn) + layoutSetting.addWidget(self.editBtn) + self.rmBtn.clicked.connect(self.onRm) + self.addBtn.clicked.connect(lambda: addCallback(self)) + self.editBtn.clicked.connect(self.editButton) + self.button.clicked.connect(lambda: clickCallback( + self, utils.str_to_bytes(self.value, escape=True, encoding="utf-8"))) + + def getInfo(self): + return [self.id, self.button.text(), self.value, self.icon, self.shortcut] + + def onRm(self): + utils_ui.clearButtonIcon(self.addBtn) + utils_ui.clearButtonIcon(self.rmBtn) + utils_ui.clearButtonIcon(self.editBtn) + utils_ui.clearButtonIcon(self.button) + self.rmCallback(self) + + def editButton(self): + ok, remark, value, icon, shortcut = EditRemarDialog( + self.button.text(), self.icon, shortcut=self.shortcut, value=self.value).exec() + if ok: + self.value = value + self.icon = icon + self.shortcut = shortcut + self.button.setText(remark) + if self.shortcut: + tip = self.getShortcutTip() + self.button.setToolTip(tip) + utils_ui.setButtonIcon(self.button, icon) + self.changeCallback(self) + + def getShortcutTip(self): + return _("Shortcut") + ": " + "+".join((name for v, name in self.shortcut)) + + id = "button" + + def __init__(self, parent=None, hintSignal=lambda type, title, msg: None, rmCallback=lambda widget: None, send=lambda x: None, config=None): + default = { + "items": [] + } + super().__init__(parent, hintSignal, rmCallback, send, config, default) + self.layout = QHBoxLayout() + self.setLayout(self.layout) + if not self.config["items"]: + button = Graph_Button.Button( + self.onRm, self.onAdd, self.onClick, self.onChange) + self.layout.addWidget(button) + self.buttons = [button] + self.config["items"] = [button.getInfo()] + else: + self.buttons = [] + for item in self.config["items"]: + button = Graph_Button.Button( + self.onRm, self.onAdd, self.onClick, self.onChange, value=item) + self.layout.addWidget(button) + self.buttons.append(button) + self.pressedKeys = [] + + def onRm(self, widget): + ''' + @widget Graph_Button.Button + ''' + item = widget.getInfo() + for item_ in self.config["items"]: + if item_[0] == item[0]: # id + self.config["items"].remove(item_) + break + self.buttons.remove(widget) + widget.setParent(None) + if not self.buttons: + self.rmCallback(self) + + def onAdd(self, widget): + ''' + @widget Graph_Button.Button + ''' + button = Graph_Button.Button( + self.onRm, self.onAdd, self.onClick, self.onChange) + self.layout.addWidget(button) + self.buttons.append(button) + self.config["items"].append(button.getInfo()) + + def onClick(self, widget, data: bytes): + self.send(data) + + def onChange(self, widget): + self.config["items"][self.buttons.index(widget)] = widget.getInfo() + + def onKeyPressEvent(self, event): + key = event.key() + self.pressedKeys.append(key) + for uid, name, value, icon, shortcut in self.config["items"]: + if not shortcut: + continue + if len(shortcut) == len(self.pressedKeys): + same = True + for i in range(len(shortcut)): + if shortcut[i][0] != self.pressedKeys[i]: + same = False + break + if same: + value = utils.str_to_bytes( + value, escape=True, encoding="utf-8") + self.send(value) + + def onKeyReleaseEvent(self, event): + key = event.key() + if key in self.pressedKeys: + self.pressedKeys.remove(key) + + +graphWidgets = { + Graph_Plot.id: Graph_Plot, + Graph_Button.id: Graph_Button, + Graph_Image.id: Graph_Image + # Graph_DragTof.id: Graph_DragTof, + # Graph_MetaSenseLite.id: Graph_MetaSenseLite, +} diff --git a/doc/hsjl_plugin_timer.py b/doc/hsjl_plugin_timer.py index 59170d1..44be9f0 100644 --- a/doc/hsjl_plugin_timer.py +++ b/doc/hsjl_plugin_timer.py @@ -30,8 +30,8 @@ except ImportError: class Plugin(Plugin_Base): - id = "hsjl_time" - name = _("hsjl_time") + id = "hsjl_timer" + name = _("hsjl_timer") showReceiveDataSignal = pyqtSignal(str) # 图片1的字节数据(已存在,提取为常量) IMAGE1_DATA = [ @@ -72,9 +72,11 @@ class Plugin(Plugin_Base): "version": 1, "customSendItems": [ { - "caption": "遥测", + "caption": "自检项", "icon": "fa5.play-circle", - "data": "5A 54 E6 EE 00 D4 02 5A FE" + "data": "5A 54 E1 EE 00 CF 02 5A FE" + #"data": "5A 54 E1 EE 01 00 00 00 01 15 00 55 00 05 55 57 58 80 00 00 00 00 00 00 00\n\ + #00 00 00 01 00 00 00 00 75 17 5A FE " }, { "caption": "图片传输", @@ -88,7 +90,7 @@ class Plugin(Plugin_Base): 4E 62 24 11 DE 82 35 D8 00 00 00 00 49 45 4E 44 AE 42 60 82 00 76 C4 5A FE" }, { - "caption": "总控", + "caption": "配置发送", "icon": "fa5.play-circle", "data": "5A 54 E3 EE AA 03 22 00 00 A0 06 5A FE" }, @@ -98,6 +100,8 @@ class Plugin(Plugin_Base): for k in default: if k not in self.config: self.config[k] = default[k] + self.config["customSendItems"] = default["customSendItems"] + print("初始化的customSendItems:", self.config["customSendItems"]) def createToggleButton(self): toggleBtn = QPushButton("开关: Off") @@ -119,31 +123,58 @@ class Plugin(Plugin_Base): return toggleBtn def createIdButton(self): - toggleBtn = QPushButton("id: 01") + # 定义ID映射:原始ID -> 显示文本(带标识) + id_display_map = { + "01": "01(主)", + "02": "02(从)", + "03": "03(从)" + } + default_id = "01" # 原始ID默认值 + # 按钮显示带标识的文本 + toggleBtn = QPushButton(f"id: {id_display_map[default_id]}") + # 存储原始ID(关键:组帧时用这个值) + toggleBtn.setProperty("originalId", default_id) + menu = QMenu(toggleBtn) - ids = ["01", "02", "03"] - def setId(text): - toggleBtn.setText(f"id: {text}") - print(f"id设为 {text}") + def setId(original_id): + # 更新显示文本(带标识) + toggleBtn.setText(f"id: {id_display_map[original_id]}") + # 更新存储的原始ID + toggleBtn.setProperty("originalId", original_id) + print(f"id设为 {original_id}") # 确认原始ID正确 - for id_text in ids: - menu.addAction(id_text).triggered.connect(lambda checked, t=id_text: setId(t)) + # 菜单显示带标识的文本,但传递原始ID + for original_id, display_text in id_display_map.items(): + menu.addAction(display_text).triggered.connect( + lambda checked, id=original_id: setId(id) + ) toggleBtn.setMenu(menu) return toggleBtn def createtoIdButton(self): - toggleBtn = QPushButton("目的节点id: 01") + # 目的节点ID同理处理 + id_display_map = { + "01": "01(主)", + "02": "02(从)", + "03": "03(从)" + } + default_id = "01" + toggleBtn = QPushButton(f"目的节点id: {id_display_map[default_id]}") + toggleBtn.setProperty("originalId", default_id) # 存储原始ID + menu = QMenu(toggleBtn) - ids = ["01", "02", "03"] - def setId(text): - toggleBtn.setText(f"目的节点id: {text}") - print(f"目的节点id设为 {text}") + def setId(original_id): + toggleBtn.setText(f"目的节点id: {id_display_map[original_id]}") + toggleBtn.setProperty("originalId", original_id) + print(f"目的节点id设为 {original_id}") - for id_text in ids: - menu.addAction(id_text).triggered.connect(lambda checked, t=id_text: setId(t)) + for original_id, display_text in id_display_map.items(): + menu.addAction(display_text).triggered.connect( + lambda checked, id=original_id: setId(id) + ) toggleBtn.setMenu(menu) return toggleBtn @@ -197,9 +228,11 @@ class Plugin(Plugin_Base): # ID字段,转为整数 try: - id_val = int(id_text.split(":")[1].strip(), 16) + # 直接获取存储的原始ID(如"01") + original_id = self.idButton.property("originalId") + id_val = int(original_id, 16) # 转为16进制整数 except: - id_val = 0x00 + id_val = 0x00 # 异常时默认值 frame.append(id_val) # 速率字段对应映射(参考原来速率的0x22等) @@ -240,9 +273,10 @@ class Plugin(Plugin_Base): # ID字段,转为整数 try: - id_val = int(toid_text.split(":")[1].strip(), 16) + original_toid = self.toidButton.property("originalId") + id_val = int(original_toid, 16) except: - id_val = 0x01 + id_val = 0x01 # 异常时默认值 frame.append(id_val) # 根据索引选择图片数据 @@ -381,19 +415,19 @@ class Plugin(Plugin_Base): self.mainWidget.addWidget(sendItemsOuterWrapper) # 网络状态标签初始化 - self.networkStatusLabel = QLabel("网络连接状态: 未连接") + self.networkStatusLabel = QLabel("节点状态: 未知") self.networkStatusLabel.setAlignment(Qt.AlignLeft) self.networkStatusLabel.setFont(QFont("Arial", 10)) - self.networkPhaseLabel = QLabel("网络运行阶段: 未知") + self.networkPhaseLabel = QLabel("天线温度: 未知") self.networkPhaseLabel.setAlignment(Qt.AlignLeft) self.networkPhaseLabel.setFont(QFont("Arial", 10)) - self.networktemLabel = QLabel("fpga温度: 未知") + self.networktemLabel = QLabel("电压状态: 未知") self.networktemLabel.setAlignment(Qt.AlignLeft) self.networktemLabel.setFont(QFont("Arial", 10)) - self.networkvoltageLabel = QLabel("电压监测: 未知") + self.networkvoltageLabel = QLabel("fgpa温度: 未知") self.networkvoltageLabel.setAlignment(Qt.AlignLeft) self.networkvoltageLabel.setFont(QFont("Arial", 10)) @@ -434,34 +468,32 @@ class Plugin(Plugin_Base): return itemWidget def onUiInitDone(self): - # 去掉自检项 - self.config["customSendItems"] = [item for item in self.config["customSendItems"] if item["caption"] != "自检"] # 清空旧布局 self.clearLayout(self.customSendItemsLayout) # 创建“开关”、“ID”、“时隙表”、“速率”按钮,供“总控”使用 self.toggleButton = self.createToggleButton() - self.customSendItemsLayout.addWidget(self.toggleButton, 1, 0) + self.customSendItemsLayout.addWidget(self.toggleButton, 1, 0, 1, 2) self.idButton = self.createIdButton() - self.customSendItemsLayout.addWidget(self.idButton, 1, 1) + self.customSendItemsLayout.addWidget(self.idButton, 0, 1) self.slotButton = self.createSlotButton() - self.customSendItemsLayout.addWidget(self.slotButton, 1, 2) + self.customSendItemsLayout.addWidget(self.slotButton, 0, 2) self.rateButton = self.createRateButton() - self.customSendItemsLayout.addWidget(self.rateButton, 1, 3) + self.customSendItemsLayout.addWidget(self.rateButton, 0, 0) self.toidButton = self.createtoIdButton() self.customSendItemsLayout.addWidget(self.toidButton, 0, 3) - # 第一行固定插入“遥测”、“图片传输”、“总控” + # 第一行固定插入“自检”、“图片传输”、“总控” for item in self.config["customSendItems"]: - if item["caption"] == "遥测": + if item["caption"] == "自检项": + print("找到自检项,准备添加到(0,0)") # 新增 w = self.createSendItemWidget(item) - self.customSendItemsLayout.addWidget(w, 0, 0) - + self.customSendItemsLayout.addWidget(w, 3, 0, 1, 2) elif item["caption"] == "图片传输": totalControlWidget = QWidget() layout = QHBoxLayout() @@ -545,16 +577,16 @@ class Plugin(Plugin_Base): layout.addWidget(label) layout.addWidget(sendBtn) - self.customSendItemsLayout.addWidget(totalControlWidget, 0, 1) + self.customSendItemsLayout.addWidget(totalControlWidget, 3, 2, 1, 2 ) - elif item["caption"] == "总控": + elif item["caption"] == "配置发送": totalControlWidget = QWidget() layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) totalControlWidget.setLayout(layout) sendBtn = QPushButton("") - label = QLabel("总控") + label = QLabel("配置发送") utils_ui.setButtonIcon(sendBtn, item.get("icon", "fa.send")) sendBtn.setProperty("class", "smallBtn") @@ -568,7 +600,7 @@ class Plugin(Plugin_Base): # 动态构造指令字符串S cmd_str = self.buildBindCommand(switch_text, id_text, rate_text, slot_text) - print(f"总控: {cmd_str}") + print(f"配置发送: {cmd_str}") data_bytes = self.parseSendData(cmd_str, encoding=self.configGlobal["encoding"], @@ -579,16 +611,13 @@ class Plugin(Plugin_Base): layout.addWidget(label) layout.addWidget(sendBtn) - self.customSendItemsLayout.addWidget(totalControlWidget, 0, 2) + self.customSendItemsLayout.addWidget(totalControlWidget, 1, 2, 1, 2) # 第三行放4个状态标签 self.customSendItemsLayout.addWidget(self.networkStatusLabel, 2, 0) self.customSendItemsLayout.addWidget(self.networkPhaseLabel, 2, 1) self.customSendItemsLayout.addWidget(self.networkvoltageLabel, 2, 2) self.customSendItemsLayout.addWidget(self.networktemLabel, 2, 3) - # 初始化结构体 - self.structStatusCheckRsp = Struct( - "6d}\n" - ret += f"发送数据计数: {payload[sysbusOffset + 2]:>6d}\n" - ret += f"遥测请求计数: {payload[sysbusOffset + 3]:>6d}\n" - - ret += "\n -- 链路层状态\n" - ret += f"时元号: {payload[linkOffset + 0]:>6d}\n" - ret += f"时隙号: {payload[linkOffset + 1]:>3d}\n" - - ret += "当前角色: " - if payload[linkOffset + 2] == 0xAA: - ret += f"簇首 (0x{payload[linkOffset + 2]:02X})\n" - elif payload[linkOffset + 2] == 0xCC: - ret += f"成员 (0x{payload[linkOffset + 2]:02X})\n" - else: - ret += f"【未知错误】(0x{payload[linkOffset + 2]:02X})\n" - - # 网络连接状态更新 - network_status = "未连接" - if payload[linkOffset + 3] == 0x11: - network_status = "已连接" - elif payload[linkOffset + 3] == 0x00: - network_status = "未连接" - else: - network_status = f"未知状态 (0x{payload[linkOffset + 3]:02X})" - - # 网络运行阶段更新 - phase = "未知" - if payload[linkOffset + 4] == 0x11: - phase = "建网阶段" - elif payload[linkOffset + 4] == 0x22: - phase = "业务通信" - else: - phase = f"未知阶段 (0x{payload[linkOffset + 4]:02X})" - - self.networkStatusLabel.setText(f"网络连接状态: {network_status}") - self.networkPhaseLabel.setText(f"网络运行阶段: {phase}") - - ret += f"重建/入网计数: {payload[linkOffset + 5]:>6d}\n" - ret += f"通信天线位号: {payload[linkOffset + 6]:>1d}\n" - - ret += f"建网帧发送计数: {payload[linkOffset + 7]:>6d}\n" - ret += f"建网帧接收计数: {payload[linkOffset + 8]:>6d}\n" - ret += f"建网帧正确计数: {payload[linkOffset + 9]:>6d}\n" - ret += f"应答帧发送计数: {payload[linkOffset + 10]:>6d}\n" - ret += f"应答帧接收计数: {payload[linkOffset + 11]:>6d}\n" - ret += f"应答帧正确计数: {payload[linkOffset + 12]:>6d}\n" - ret += f"分发帧发送计数: {payload[linkOffset + 13]:>6d}\n" - ret += f"分发帧接收计数: {payload[linkOffset + 14]:>6d}\n" - ret += f"分发帧正确计数: {payload[linkOffset + 15]:>6d}\n" - ret += f"网管帧发送计数: {payload[linkOffset + 16]:>6d}\n" - ret += f"网管帧接收计数: {payload[linkOffset + 17]:>6d}\n" - ret += f"网管帧正确计数: {payload[linkOffset + 18]:>6d}\n" - ret += f"RTT 帧发送计数: {payload[linkOffset + 19]:>6d}\n" - ret += f"RTT 帧接收计数: {payload[linkOffset + 20]:>6d}\n" - ret += f"RTT 帧正确计数: {payload[linkOffset + 21]:>6d}\n" - ret += f"业务帧发送计数: {payload[linkOffset + 22]:>6d}\n" - ret += f"业务帧接收计数: {payload[linkOffset + 23]:>6d}\n" - ret += f"业务帧正确计数: {payload[linkOffset + 24]:>6d}\n" - ret += f"时间偏差记录: {payload[linkOffset + 25]/1e2:.2f} us\n" - ret += f"发送数据 ID: {payload[linkOffset + 26]:>6d}\n" - ret += f"接收数据 ID: {payload[linkOffset + 27]:>6d}\n" - - ret += "\n -- 物理层状态\n" - ret += f"捕获计数: {payload[phyOffset + 0]:>6d}\n" - ret += f"帧同步计数: {payload[phyOffset + 1]:>6d}\n" - ret += f"译码计数: {payload[phyOffset + 2]:>6d}\n" - ret += f"CRC 正确计数:{payload[phyOffset + 3]:>6d}\n" - ret += f"多普勒估计: {payload[phyOffset + 4]:.2f} Hz\n" - ret += f"正确接收时刻: {payload[phyOffset + 5]/1e2:.2f} us\n" - ret += f"捕获峰均比: {payload[phyOffset + 6]:.2f}\n" - ret += f"捕获相位值: {payload[phyOffset + 7]:>3d}\n" - ret += f"捕获频偏通道: {payload[phyOffset + 8]:>3d}\n" - ret += f"帧同步峰均比: {payload[phyOffset + 9]:.2f}\n" - ret += f"帧同步频偏通道: {payload[phyOffset + 10]:>3d}\n" - ret += f"交织输入数据幅度统计:\t0x{payload[phyOffset + 11]:04X}\n" - ret += f"天线控制模式: {bin(payload[phyOffset + 10])[2:].zfill(8)}\n" - - ret += "\n -- 硬件状态\n" - voltage = f"FPGA 电压监测: {bin(payload[hwOffset+0])[2:].zfill(8)}\n" + hwOffset = 4 + ret += f"FPGA 电压监测: {bin(payload[hwOffset+0])[2:].zfill(8)}\n" ret += f"时钟锁定状态: {bin(payload[hwOffset+1])[2:].zfill(8)}\n" ret += f"AD 锁定状态: {bin(payload[hwOffset+2])[2:].zfill(8)}\n" ret += f"AD 温度 1: {payload[hwOffset+3]:>4d}\n" ret += f"AD 温度 2: {payload[hwOffset+4]:>4d}\n" - temp = f"FPGA 温度: {payload[hwOffset+5]:>4d}\n" + ret += f"FPGA 温度: {payload[hwOffset+5]:>4d}\n" + ret += f"天线-1 工作 PLL: {(payload[hwOffset+6] >> 15) & 0x01:>1d}\n" ret += f"天线-1 工作频率: {(payload[hwOffset+6] & 0x7FFF)+3700:>4d}\n" + print(payload[3]) ret += f"天线-2 工作 PLL: {(payload[hwOffset+7] >> 15) & 0x01:>1d}\n" ret += f"天线-2 工作频率: {(payload[hwOffset+7] & 0x7FFF)+3700:>4d}\n" + print(payload[3]) ret += f"天线-3 工作 PLL: {(payload[hwOffset+8] >> 15) & 0x01:>1d}\n" ret += f"天线-3 工作频率: {(payload[hwOffset+8] & 0x7FFF)+3700:>4d}\n" + print(payload[3]) ret += f"天线 PLL 锁定状态: {bin(payload[hwOffset+9])[2:].zfill(8)}\n" + print(payload[3]) ret += f"天线工作模式: {bin(payload[hwOffset+10])[2:].zfill(8)}\n" ret += f"天线温度: {payload[hwOffset+11]:>4d}\n" - print('+') - print(voltage) - print('_') - self.networkvoltageLabel.setText(f"电压监测: {voltage}") - self.networktemLabel.setText(f"fgpa温度: {temp}") + print(payload[3]) ret += "--------------------------------\n" + print(payload[3]) + print(network_status) + phase=payload[14] + temp=payload[9] + voltage=payload[4] + self.networkStatusLabel.setText(f"节点状态: {network_status}") + self.networkPhaseLabel.setText(f"天线温度: {phase}") + self.networkvoltageLabel.setText(f"电压状态: {voltage}") + self.networktemLabel.setText(f"fgpa温度: {temp}") return ret + '\n' + self.printFrame(data) + def evalimageTestRsp(self, data: bytes): print('“”“”“”“”“”“”“”“”“”“”“”“') try: @@ -879,22 +858,28 @@ class Plugin(Plugin_Base): ret += f"帧尾: {frame_tail.hex().upper()}\n" print('&&&&&') - # 图像显示逻辑(保留原始控件调用方式) - # 截取有效图像数据 + # 图像显示逻辑(添加缩放倍数控制) + # 1. 手动设置缩放倍数(可根据需要修改,例如放大2倍) + # 图像显示逻辑(修正后) if from_id == 0x01 or from_id == 0x02: # 图像通道 A → 左侧图像控件 if len(self.widgets) >= 1 and isinstance(self.widgets[0], graphWidgets["image"]): - self.widgets[0].onData(image_data) # 发送有效数据 + # 仅传递图像数据,缩放倍数已在控件初始化时通过config设置 + self.widgets[0].onData(image_data) else: self.showReceiveDataSignal.emit("\n### No valid image widget found (A1)\n") - print(from_id) elif from_id == 0x03: # 图像通道 B → 右侧图像控件 if len(self.widgets) >= 2 and isinstance(self.widgets[1], graphWidgets["image"]): - self.widgets[1].onData(image_data) # 发送有效数据 + # 仅传递图像数据 + self.widgets[1].onData(image_data) else: self.showReceiveDataSignal.emit("\n### No valid image widget found (A2)\n") + print(from_id) + print('+') + ret += "------------------------------------\n" + return ret + '\n' + self.printFrame(data) print(from_id) print('+') ret += "------------------------------------\n"