From 1bccbb873faa54fa465237262bb50a2cae3d8718 Mon Sep 17 00:00:00 2001 From: YuanHongbin <975559679@qq.com> Date: Tue, 1 Sep 2026 11:43:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=AE=BE=E5=A4=87=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E4=B8=8E=E5=8D=87=E7=BA=A7=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=8F=82=E6=95=B0=E4=BF=A1=E6=81=AF=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复点击设备升级闪退问题 - 升级弹窗MAC提示改为81-00-00-06-00-00,进度条颜色不再随失焦变化 - 新增设备查询(culs)/设备删除(cudel)功能 - 设备列表显示Laser/FPO/LpCfg参数 - 设备参数信息区重新排布,显示通讯单元读取间隔 - 说明书同步更新 --- gateway_config.py | 962 +++++++++++++++++++++++++++++++-------------- generate_manual.py | 139 ++----- 2 files changed, 691 insertions(+), 410 deletions(-) diff --git a/gateway_config.py b/gateway_config.py index a34aa88..9a30be1 100644 --- a/gateway_config.py +++ b/gateway_config.py @@ -7,16 +7,18 @@ import sys import os import re import time +import struct +import zlib import threading -from datetime import datetime from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel, QComboBox, QPushButton, QLineEdit, QTextEdit, - QMessageBox, QGridLayout, QSplitter + QMessageBox, QGridLayout, QFileDialog, QDialog, QProgressBar, + QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView ) -from PyQt5.QtCore import Qt, QThread, pyqtSignal, QRegExp -from PyQt5.QtGui import QFont, QColor, QPalette, QRegExpValidator, QIntValidator, QIcon +from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer +from PyQt5.QtGui import QFont, QColor, QPalette, QIcon import serial import serial.tools.list_ports @@ -28,8 +30,54 @@ def resource_path(relative_path): return os.path.join(os.path.abspath(os.path.dirname(__file__)), relative_path) +# ── 传感器通用串口升级协议 V1.1 ───────────────────────────── +FRAME_HEADER = 0x7D +UPG_PACKET_DATA = 200 # 每包数据长度上限 +CMD_ANNOUNCE = 0x81 # 主机->从机:下发APP请求 +CMD_ACK = 0x01 # 从机->主机:应答(Response=0x14) +CMD_REQ_PKG = 0x02 # 从机->主机:请求包序号 +CMD_SEND_PKG = 0x82 # 主机->从机:下发包 +PKG_DATALEN_MAX = 200 + + +def crc16_modbus(data): + """MODBUS CRC16,多项式0x8005(反射),返回16位整数。""" + crc = 0xFFFF + for b in data: + crc ^= b + for _ in range(8): + if crc & 1: + crc = (crc >> 1) ^ 0xA001 + else: + crc >>= 1 + return crc + + +def build_frame(dev_mac, cmd, payload): + """0x7D + DevMac(6) + Cmd + PayloadLen + Payload + CRC16(小端)。""" + body = bytes([FRAME_HEADER]) + bytes(dev_mac) + bytes([cmd, len(payload)]) + bytes(payload) + crc = crc16_modbus(body) + return body + struct.pack(' bytes(6),无效返回 None。""" + if not s: + return None + parts = s.strip().replace('-', ':').split(':') + parts = [p for p in parts if p] + if len(parts) != 6: + return None + try: + return bytes(int(p, 16) for p in parts) + except ValueError: + return None + + + class SerialThread(QThread): data_received = pyqtSignal(str) + data_received_raw = pyqtSignal(bytes) def __init__(self, ser): super().__init__() @@ -47,6 +95,7 @@ class SerialThread(QThread): except Exception: text = data.decode('gbk', errors='replace') self.data_received.emit(text) + self.data_received_raw.emit(data) else: time.sleep(0.01) except Exception: @@ -64,15 +113,16 @@ class GatewayConfigTool(QMainWindow): self.serial_thread = None self._recv_buffer = "" self._para_mode = False - self._cur_comm = "Cat1" # 当前通讯方式: Cat1 或 Eth + self.upgrade_dialog = None + self.dev_dialog = None self.init_ui() self.refresh_ports() def init_ui(self): self.setWindowTitle("网关配置工具") self.setWindowIcon(QIcon(resource_path("物联网网关.png"))) - self.setMinimumSize(1000, 850) - self.resize(1000, 850) + self.setMinimumSize(940, 640) + self.resize(1000, 700) central = QWidget() self.setCentralWidget(central) @@ -110,222 +160,23 @@ class GatewayConfigTool(QMainWindow): main_layout.addWidget(conn_group) - # === 配置区 + 日志区(上下布局)=== - splitter = QSplitter(Qt.Vertical) + # === 参数配置区 === + config_group = QGroupBox("参数配置") + config_group_layout = QVBoxLayout(config_group) + config_group_layout.setSpacing(4) - # -- 上方:配置区 -- - config_widget = QWidget() - config_layout = QVBoxLayout(config_widget) - config_layout.setContentsMargins(0, 0, 0, 0) - config_layout.setSpacing(2) - - # 基本指令 + 通讯方式(一行) - top_row = QHBoxLayout() - top_row.setSpacing(4) - - self.btn_para = QPushButton("查看配置(para)") - self.btn_para.clicked.connect(self.on_para_clicked) - top_row.addWidget(self.btn_para) - - self.btn_cat1 = QPushButton("Cat1(comm cat1)") - self.btn_cat1.clicked.connect(lambda: self.set_comm_mode("Cat1")) - top_row.addWidget(self.btn_cat1) - - self.btn_eth = QPushButton("ETH(comm eth)") - self.btn_eth.clicked.connect(lambda: self.set_comm_mode("Eth")) - top_row.addWidget(self.btn_eth) - - top_row.addStretch() - config_layout.addLayout(top_row) - - # 当前配置显示 - info_group = QGroupBox("当前网关配置") - info_grid = QGridLayout(info_group) - info_grid.setSpacing(4) - - def _bold_label(color="#1565C0"): - lb = QLabel("-") - lb.setStyleSheet(f"font-weight:bold; color:{color};") - return lb - - row = 0 - info_grid.addWidget(QLabel("软件版本:"), row, 0) - self.label_sw_ver = _bold_label() - info_grid.addWidget(self.label_sw_ver, row, 1) - info_grid.addWidget(QLabel("硬件版本:"), row, 2) - self.label_hw_ver = _bold_label() - info_grid.addWidget(self.label_hw_ver, row, 3) - - row = 1 - info_grid.addWidget(QLabel("电池:"), row, 0) - self.label_battery = _bold_label() - info_grid.addWidget(self.label_battery, row, 1) - info_grid.addWidget(QLabel("注册状态:"), row, 2) - self.label_reg_status = _bold_label() - info_grid.addWidget(self.label_reg_status, row, 3) - - row = 2 - info_grid.addWidget(QLabel("网关MAC:"), row, 0) - self.label_gw_mac = _bold_label() - info_grid.addWidget(self.label_gw_mac, row, 1, 1, 3) - - row = 3 - info_grid.addWidget(QLabel("通讯方式:"), row, 0) - self.label_cur_comm = _bold_label() - info_grid.addWidget(self.label_cur_comm, row, 1) - info_grid.addWidget(QLabel("读取间隔:"), row, 2) - self.label_read_interval = _bold_label() - info_grid.addWidget(self.label_read_interval, row, 3) - - row = 4 - info_grid.addWidget(QLabel("Cat1 IMEI:"), row, 0) - self.label_imei = _bold_label() - info_grid.addWidget(self.label_imei, row, 1, 1, 3) - - row = 5 - info_grid.addWidget(QLabel("Cat1 SIM:"), row, 0) - self.label_sim = _bold_label() - info_grid.addWidget(self.label_sim, row, 1, 1, 3) - - row = 6 - info_grid.addWidget(QLabel("Lora频率:"), row, 0) - self.label_cur_lora_fc = _bold_label() - info_grid.addWidget(self.label_cur_lora_fc, row, 1) - - # Eth参数(紧凑两列) - eth_info_group = QGroupBox("ETH网络参数") - eth_info_grid = QGridLayout(eth_info_group) - eth_info_grid.setSpacing(4) - - row = 0 - eth_info_grid.addWidget(QLabel("网关IP:"), row, 0) - self.label_cur_eth_ip = _bold_label() - eth_info_grid.addWidget(self.label_cur_eth_ip, row, 1) - eth_info_grid.addWidget(QLabel("网关端口:"), row, 2) - self.label_cur_eth_port = _bold_label() - eth_info_grid.addWidget(self.label_cur_eth_port, row, 3) - - row = 1 - eth_info_grid.addWidget(QLabel("子网掩码:"), row, 0) - self.label_cur_eth_mask = _bold_label() - eth_info_grid.addWidget(self.label_cur_eth_mask, row, 1) - eth_info_grid.addWidget(QLabel("服务器网关:"), row, 2) - self.label_cur_eth_gw = _bold_label() - eth_info_grid.addWidget(self.label_cur_eth_gw, row, 3) - - row = 2 - eth_info_grid.addWidget(QLabel("服务器IP:"), row, 0) - self.label_cur_eth_dip = _bold_label() - eth_info_grid.addWidget(self.label_cur_eth_dip, row, 1) - eth_info_grid.addWidget(QLabel("服务器端口:"), row, 2) - self.label_cur_eth_dport = _bold_label() - eth_info_grid.addWidget(self.label_cur_eth_dport, row, 3) - - row = 3 - eth_info_grid.addWidget(QLabel("IP模式:"), row, 0) - self.label_cur_eth_ipmode = _bold_label() - eth_info_grid.addWidget(self.label_cur_eth_ipmode, row, 1) - eth_info_grid.addWidget(QLabel("工作模式:"), row, 2) - self.label_cur_eth_workmode = _bold_label() - eth_info_grid.addWidget(self.label_cur_eth_workmode, row, 3) - - # 低功耗配置 - misc_group = QGroupBox("低功耗配置") - misc_grid = QGridLayout(misc_group) - misc_grid.setSpacing(2) - misc_grid.setContentsMargins(10, 4, 4, 4) - - row = 0 - misc_grid.addWidget(QLabel("采集间隔:"), row, 0) - self.label_lpcfg_ci = _bold_label() - misc_grid.addWidget(self.label_lpcfg_ci, row, 1) - misc_grid.addWidget(QLabel("上报间隔:"), row, 2) - self.label_lpcfg_ri = _bold_label() - misc_grid.addWidget(self.label_lpcfg_ri, row, 3) - - row = 1 - misc_grid.addWidget(QLabel("紧急上报间隔:"), row, 0) - self.label_lpcfg_ei = _bold_label() - misc_grid.addWidget(self.label_lpcfg_ei, row, 1) - misc_grid.addWidget(QLabel("紧急上报时长:"), row, 2) - self.label_lpcfg_et = _bold_label() - misc_grid.addWidget(self.label_lpcfg_et, row, 3) - - config_layout.addWidget(info_group) - config_layout.addWidget(eth_info_group) - config_layout.addWidget(misc_group) - - # Eth参数(紧凑两列) - eth_group = QGroupBox("ETH参数配置(需先切换到eth模式)") - eth_grid = QGridLayout(eth_group) - eth_grid.setSpacing(4) - - ip_validator = QRegExpValidator(QRegExp(r"^(\d{1,3}\.){3}\d{1,3}$")) - - eth_grid.addWidget(QLabel("网关IP:"), 0, 0) - self.edit_eth_ip = QLineEdit("192.168.1.101") - self.edit_eth_ip.setValidator(ip_validator) - eth_grid.addWidget(self.edit_eth_ip, 0, 1) - self.btn_eth_ip = QPushButton("发送") - self.btn_eth_ip.setFixedWidth(50) - self.btn_eth_ip.clicked.connect(lambda: self.send_param("eth ip", self.edit_eth_ip)) - eth_grid.addWidget(self.btn_eth_ip, 0, 2) - - eth_grid.addWidget(QLabel("网关端口:"), 0, 3) - self.edit_eth_port = QLineEdit("10000") - self.edit_eth_port.setValidator(QIntValidator(1, 65535)) - eth_grid.addWidget(self.edit_eth_port, 0, 4) - self.btn_eth_port = QPushButton("发送") - self.btn_eth_port.setFixedWidth(50) - self.btn_eth_port.clicked.connect(lambda: self.send_param("eth port", self.edit_eth_port)) - eth_grid.addWidget(self.btn_eth_port, 0, 5) - - eth_grid.addWidget(QLabel("服务器网关:"), 1, 0) - self.edit_eth_gw = QLineEdit("192.168.1.1") - self.edit_eth_gw.setValidator(ip_validator) - eth_grid.addWidget(self.edit_eth_gw, 1, 1) - self.btn_eth_gw = QPushButton("发送") - self.btn_eth_gw.setFixedWidth(50) - self.btn_eth_gw.clicked.connect(lambda: self.send_param("eth gw", self.edit_eth_gw)) - eth_grid.addWidget(self.btn_eth_gw, 1, 2) - - eth_grid.addWidget(QLabel("服务器IP:"), 1, 3) - self.edit_eth_dip = QLineEdit("192.168.1.100") - self.edit_eth_dip.setValidator(ip_validator) - eth_grid.addWidget(self.edit_eth_dip, 1, 4) - self.btn_eth_dip = QPushButton("发送") - self.btn_eth_dip.setFixedWidth(50) - self.btn_eth_dip.clicked.connect(lambda: self.send_param("eth dip", self.edit_eth_dip)) - eth_grid.addWidget(self.btn_eth_dip, 1, 5) - - eth_grid.addWidget(QLabel("服务器端口:"), 2, 0) - self.edit_eth_dport = QLineEdit("8080") - self.edit_eth_dport.setValidator(QIntValidator(1, 65535)) - eth_grid.addWidget(self.edit_eth_dport, 2, 1) - self.btn_eth_dport = QPushButton("发送") - self.btn_eth_dport.setFixedWidth(50) - self.btn_eth_dport.clicked.connect(lambda: self.send_param("eth dport", self.edit_eth_dport)) - eth_grid.addWidget(self.btn_eth_dport, 2, 2) - - self.btn_eth_apply = QPushButton("应用ETH配置 (eth apply)") - self.btn_eth_apply.setStyleSheet("background-color:#4CAF50; color:white; font-weight:bold; padding:4px;") - self.btn_eth_apply.clicked.connect(self.send_eth_apply) - eth_grid.addWidget(self.btn_eth_apply, 2, 3, 1, 3) - - config_layout.addWidget(eth_group) - - # Lora信道配置(独立区块,类似ETH参数) - lora_group = QGroupBox("Lora信道配置") + # Lora 参数(子分组) + lora_group = QGroupBox("Lora 参数") lora_layout = QHBoxLayout(lora_group) lora_layout.setSpacing(6) - lora_layout.addWidget(QLabel("信道:")) + lora_layout.addWidget(QLabel("信道频率(fc):")) self.combo_lora_fc = QComboBox() self.combo_lora_fc.setFixedWidth(200) lora_freqs = [] fc_hz = 410100000 ch_idx = 1 - while fc_hz < 525000000: + while fc_hz < 493000000: mhz = fc_hz / 1000000 lora_freqs.append((str(fc_hz), f"信道{ch_idx}: {mhz:.1f}MHz")) fc_hz += 200000 @@ -345,17 +196,100 @@ class GatewayConfigTool(QMainWindow): self.btn_lora_fc.clicked.connect(self.on_lora_send) lora_layout.addWidget(self.btn_lora_fc) + lora_hint = QLabel("设置LoRa通信信道频率") + lora_hint.setStyleSheet("color:#909090;") + lora_layout.addWidget(lora_hint) lora_layout.addStretch() - config_layout.addWidget(lora_group) + config_group_layout.addWidget(lora_group) - # -- 下方:日志区 + 自定义指令 -- - log_widget = QWidget() - log_layout = QHBoxLayout(log_widget) - log_layout.setContentsMargins(0, 0, 0, 0) - log_layout.setSpacing(4) + # 查看配置按钮 + 提示 + btn_row = QHBoxLayout() + btn_row.setSpacing(6) + self.btn_para = QPushButton("查看配置(para)") + self.btn_para.setFixedWidth(130) + self.btn_para.clicked.connect(self.on_para_clicked) + btn_row.addWidget(self.btn_para) + + self.btn_upgrade = QPushButton("设备升级") + self.btn_upgrade.setFixedWidth(100) + self.btn_upgrade.setStyleSheet("background-color:#1976D2; color:white; font-weight:bold;") + self.btn_upgrade.clicked.connect(self.on_upgrade_clicked) + btn_row.addWidget(self.btn_upgrade) + + self.btn_dev_mgmt = QPushButton("设备管理") + self.btn_dev_mgmt.setFixedWidth(100) + self.btn_dev_mgmt.setStyleSheet("background-color:#388E3C; color:white; font-weight:bold;") + self.btn_dev_mgmt.clicked.connect(self.on_dev_mgmt_clicked) + btn_row.addWidget(self.btn_dev_mgmt) + + para_hint = QLabel("配置后可通过para命令验证是否成功") + para_hint.setStyleSheet("color:#909090;") + btn_row.addWidget(para_hint) + btn_row.addStretch() + config_group_layout.addLayout(btn_row) + + main_layout.addWidget(config_group) + + # === 设备参数信息区 === + info_group = QGroupBox("设备参数信息") + info_grid = QGridLayout(info_group) + info_grid.setSpacing(8) + info_grid.setContentsMargins(12, 8, 12, 8) + + def _val_label(color="#757575"): + lb = QLabel("-") + lb.setStyleSheet(f"color:{color};") + return lb + + row = 0 + info_grid.addWidget(QLabel("软件版本:"), row, 0) + self.label_sw_ver = _val_label() + info_grid.addWidget(self.label_sw_ver, row, 1) + info_grid.addWidget(QLabel("硬件版本:"), row, 2) + self.label_hw_ver = _val_label() + info_grid.addWidget(self.label_hw_ver, row, 3) + + row = 1 + info_grid.addWidget(QLabel("电池电量:"), row, 0) + self.label_battery = _val_label() + info_grid.addWidget(self.label_battery, row, 1) + info_grid.addWidget(QLabel("注册状态:"), row, 2) + self.label_reg_status = _val_label() + info_grid.addWidget(self.label_reg_status, row, 3) + + row = 2 + info_grid.addWidget(QLabel("通讯方式:"), row, 0) + self.label_cur_comm = _val_label() + info_grid.addWidget(self.label_cur_comm, row, 1) + info_grid.addWidget(QLabel("Lora频率:"), row, 2) + self.label_cur_lora_fc = _val_label() + info_grid.addWidget(self.label_cur_lora_fc, row, 3) + + row = 3 + info_grid.addWidget(QLabel("网关MAC:"), row, 0) + self.label_gw_mac = _val_label() + info_grid.addWidget(self.label_gw_mac, row, 1) + info_grid.addWidget(QLabel("通讯单元读取间隔:"), row, 2) + self.label_comm_interval = _val_label() + info_grid.addWidget(self.label_comm_interval, row, 3) + + row = 4 + info_grid.addWidget(QLabel("Cat1 IMEI:"), row, 0) + self.label_imei = _val_label() + info_grid.addWidget(self.label_imei, row, 1) + info_grid.addWidget(QLabel("Cat1 SIM:"), row, 2) + self.label_sim = _val_label() + info_grid.addWidget(self.label_sim, row, 3) + + main_layout.addWidget(info_group) + + # === 下方:通讯日志 + 自定义指令 === + bottom_widget = QWidget() + bottom_layout = QHBoxLayout(bottom_widget) + bottom_layout.setContentsMargins(0, 0, 0, 0) + bottom_layout.setSpacing(4) - # 左侧:通讯日志 log_group = QGroupBox("通讯日志") log_inner = QVBoxLayout(log_group) @@ -374,7 +308,6 @@ class GatewayConfigTool(QMainWindow): log_btn_layout.addStretch() log_inner.addLayout(log_btn_layout) - # 右侧:自定义指令 custom_group = QGroupBox("自定义指令") custom_inner = QVBoxLayout(custom_group) custom_inner.setSpacing(6) @@ -391,14 +324,10 @@ class GatewayConfigTool(QMainWindow): custom_inner.addWidget(self.btn_custom_send) custom_inner.addStretch() - log_layout.addWidget(log_group, 4) - log_layout.addWidget(custom_group, 1) + bottom_layout.addWidget(log_group, 4) + bottom_layout.addWidget(custom_group, 1) - splitter.addWidget(config_widget) - splitter.addWidget(log_widget) - splitter.setSizes([280, 220]) - - main_layout.addWidget(splitter) + main_layout.addWidget(bottom_widget, 1) self.statusBar().showMessage("就绪 - 请选择串口并连接") def refresh_ports(self): @@ -438,6 +367,7 @@ class GatewayConfigTool(QMainWindow): ) self.serial_thread = SerialThread(self.serial_port) self.serial_thread.data_received.connect(self.on_data_received) + self.serial_thread.data_received_raw.connect(self.on_upgrade_data) self.serial_thread.start() self.btn_connect.setText("关闭串口") @@ -481,26 +411,46 @@ class GatewayConfigTool(QMainWindow): except Exception as e: self.log_send(f"[发送错误] {e}") - def set_comm_mode(self, mode): - self._cur_comm = mode - self.send_command(f"comm {mode.lower()}") + def send_raw(self, data): + if not self.serial_port or not self.serial_port.is_open: + QMessageBox.warning(self, "错误", "请先打开串口!") + return False + try: + self.serial_port.write(data) + self.log_send(f">>> {data.hex(' ').upper()}") + return True + except Exception as e: + self.log_send(f"[发送错误] {e}") + return False - def send_param(self, prefix, edit_widget): - if prefix.startswith("eth") and self._cur_comm != "Eth": - QMessageBox.warning(self, "提示", "当前为Cat1通讯模式,请先切换到ETH模式!") - return - value = edit_widget.text().strip() - if not value: - QMessageBox.warning(self, "错误", "参数值不能为空!") - return - cmd = f"{prefix} {value}" - self.send_command(cmd) + # ── 设备升级 ───────────────────────────────────────── + def on_upgrade_clicked(self): + if self.upgrade_dialog is None: + self.upgrade_dialog = UpgradeDialog(self) + else: + self.upgrade_dialog._reset_ui() + self.upgrade_dialog.show() + self.upgrade_dialog.raise_() + self.upgrade_dialog.activateWindow() - def send_eth_apply(self): - if self._cur_comm != "Eth": - QMessageBox.warning(self, "提示", "当前为Cat1通讯模式,请先切换到ETH模式!") + def upgrade_active(self): + return self.upgrade_dialog is not None and self.upgrade_dialog.isUpgrading() + + # ── 设备管理(查询/删除) ──────────────────────────── + def on_dev_mgmt_clicked(self): + if not self.serial_port or not self.serial_port.is_open: + QMessageBox.warning(self, "错误", "请先打开串口!") return - self.send_command("eth apply") + if self.dev_dialog is None: + self.dev_dialog = DeviceDialog(self) + self.dev_dialog.show() + self.dev_dialog.raise_() + self.dev_dialog.activateWindow() + self.dev_dialog.query() + + def on_upgrade_data(self, data): + if self.upgrade_active(): + self.upgrade_dialog.feed_data(data) def send_custom_command(self): cmd = self.edit_custom.text().strip() @@ -510,8 +460,12 @@ class GatewayConfigTool(QMainWindow): self.edit_custom.clear() def on_data_received(self, text): + if self.upgrade_active(): + return self._recv_buffer += text self.log_recv(text) + if self.dev_dialog is not None and self.dev_dialog.isVisible(): + self.dev_dialog.feed_text(text) if self._para_mode: self._try_parse_para() @@ -556,17 +510,16 @@ class GatewayConfigTool(QMainWindow): _update(self.label_gw_mac, val) elif key == 'Battery': _update(self.label_battery, val) - elif key == 'CommInf': + elif key == 'Much': _update(self.label_cur_comm, val) - self._cur_comm = val elif key == 'Cat1 IMEI': _update(self.label_imei, val) elif key == 'Cat1 SIM': _update(self.label_sim, val) elif key == 'CommUnitReadInterval': - _update(self.label_read_interval, f"{val}s") - elif key == 'Register Status': - status_map = {"0": "未注册", "1": "已注册"} + _update(self.label_comm_interval, f"{val} s") + elif key == 'MuchReg': + status_map = {"true": "已注册", "false": "未注册", "null": "未注册"} _update(self.label_reg_status, status_map.get(val, val)) # --- LTENet (Cat1) section --- elif key == 'DestAddr' and 'Cat1' in section: @@ -577,28 +530,6 @@ class GatewayConfigTool(QMainWindow): pass elif key == 'LocalPort' and 'Cat1' in section: pass - # --- EthNet section --- - elif key == 'IpMode': - _update(self.label_cur_eth_ipmode, val) - elif key == 'LocalIP': - _update(self.label_cur_eth_ip, val) - self.edit_eth_ip.setText(val) - elif key == 'LocalPort' and 'Eth' in section: - _update(self.label_cur_eth_port, val) - self.edit_eth_port.setText(val) - elif key == 'WorkMode': - _update(self.label_cur_eth_workmode, val) - elif key == 'SubnetMask': - _update(self.label_cur_eth_mask, val) - elif key == 'Gateway': - _update(self.label_cur_eth_gw, val) - self.edit_eth_gw.setText(val) - elif key == 'DestIP': - _update(self.label_cur_eth_dip, val) - self.edit_eth_dip.setText(val) - elif key == 'DestPort' and 'Eth' in section: - _update(self.label_cur_eth_dport, val) - self.edit_eth_dport.setText(val) # --- Lora --- elif key == 'Lora': fc_m = re.search(r'fc\s+(\d+)', val) @@ -608,42 +539,485 @@ class GatewayConfigTool(QMainWindow): idx = self.combo_lora_fc.findData(fc_val) if idx >= 0: self.combo_lora_fc.setCurrentIndex(idx) - elif key == 'LpCfg': - def _conv_unit(u): - if u == 'sec': return '秒' - if u == 'min': return '分钟' - return u - ci_m = re.search(r'CI:(\d+)(\w+)', val) - ri_m = re.search(r'RI:(\d+)(\w+)', val) - ei_m = re.search(r'EI:(\d+)(\w+)', val) - et_m = re.search(r'ET:(\d+)(\w+)', val) - if ci_m: - _update(self.label_lpcfg_ci, f"{ci_m.group(1)}{_conv_unit(ci_m.group(2))}") - if ri_m: - _update(self.label_lpcfg_ri, f"{ri_m.group(1)}{_conv_unit(ri_m.group(2))}") - if ei_m: - _update(self.label_lpcfg_ei, f"{ei_m.group(1)}{_conv_unit(ei_m.group(2))}") - if et_m: - _update(self.label_lpcfg_et, f"{et_m.group(1)}{_conv_unit(et_m.group(2))}") self._para_mode = False self._recv_buffer = "" self.statusBar().showMessage("配置读取完成") def log_send(self, text): - timestamp = datetime.now().strftime("%H:%M:%S") - self.text_log.append(f'[{timestamp}] {text}') + self.text_log.append(f'{text}') def log_recv(self, text): - timestamp = datetime.now().strftime("%H:%M:%S") clean = text.replace('\r', '').replace('\n', '
') - self.text_log.append(f'[{timestamp}] <<< {clean}') + self.text_log.append(f'<<< {clean}') def closeEvent(self, event): self.close_serial() event.accept() +class UpgradeDialog(QDialog): + """设备升级对话框:参照通用串口升级协议V1.1。""" + + def __init__(self, main_window): + super().__init__(main_window) + self.main = main_window + self.setWindowTitle("设备升级 - 通用串口升级协议V1.1") + self.setModal(False) + self.resize(720, 520) + + self.running = False # 升级进行中 + self._mac = None + self._fw_data = b"" + self._pkg_num = 0 + self._sent = set() + self._buf = b"" + self._timer = QTimer() + self._timer.setSingleShot(True) + self._timer.timeout.connect(self._on_timeout) + + root = QVBoxLayout(self) + root.setContentsMargins(8, 8, 8, 8) + root.setSpacing(6) + + # ── 升级设置 ── + cfg = QGroupBox("升级设置") + cfg_layout = QGridLayout(cfg) + cfg_layout.setSpacing(6) + + cfg_layout.addWidget(QLabel("固件文件(.bin):"), 0, 0) + self.fw_edit = QLineEdit() + self.fw_edit.setPlaceholderText("选择固件文件...") + cfg_layout.addWidget(self.fw_edit, 0, 1) + btn_browse = QPushButton("浏览") + btn_browse.clicked.connect(self._choose_file) + cfg_layout.addWidget(btn_browse, 0, 2) + + cfg_layout.addWidget(QLabel("设备MAC:"), 1, 0) + default_mac = main_window.label_gw_mac.text() + self.mac_edit = QLineEdit(default_mac if default_mac != "-" else "") + self.mac_edit.setPlaceholderText("81-00-00-06-00-00") + cfg_layout.addWidget(self.mac_edit, 1, 1) + mac_hint = QLabel("6字节从机MAC地址") + mac_hint.setStyleSheet("color:#909090;") + cfg_layout.addWidget(mac_hint, 1, 2) + + btn_row = QHBoxLayout() + self.start_btn = QPushButton("开始升级") + self.start_btn.clicked.connect(self._start) + btn_row.addWidget(self.start_btn) + btn_close = QPushButton("关闭") + btn_close.clicked.connect(self.close) + btn_row.addWidget(btn_close) + btn_row.addStretch() + cfg_layout.addLayout(btn_row, 2, 0, 1, 3) + + self.progress = QProgressBar() + self.progress.setRange(0, 100) + self.progress.setValue(0) + # 固定进度条颜色,避免窗口失活(点击弹窗外)时变色 + self.progress.setStyleSheet(""" + QProgressBar { + border: 1px solid #B0B0B0; + border-radius: 3px; + background: #E0E0E0; + text-align: center; + } + QProgressBar::chunk { + background-color: #1976D2; + } + """) + cfg_layout.addWidget(self.progress, 3, 0, 1, 2) + self.status_lbl = QLabel("就绪") + cfg_layout.addWidget(self.status_lbl, 3, 2) + + root.addWidget(cfg) + + # ── 升级日志 ── + log_group = QGroupBox("升级日志") + log_layout = QVBoxLayout(log_group) + self.log_text = QTextEdit() + self.log_text.setReadOnly(True) + self.log_text.setFont(QFont("Consolas", 9)) + log_btn_row = QHBoxLayout() + btn_clear_log = QPushButton("清空日志") + btn_clear_log.clicked.connect(self.log_text.clear) + log_btn_row.addStretch() + log_btn_row.addWidget(btn_clear_log) + log_layout.addLayout(log_btn_row) + log_layout.addWidget(self.log_text) + root.addWidget(log_group, 1) + + def isUpgrading(self): + return self.running + + def _reset_ui(self): + """重置弹窗 UI 到初始状态,避免关闭后重新打开时界面卡住。""" + self.running = False + self._timer.stop() + self._buf = b"" + self._sent.clear() + self._fw_data = b"" + self._mac = None + self._pkg_num = 0 + self.start_btn.setEnabled(True) + self.progress.setValue(0) + self.status_lbl.setText("就绪") + + def _choose_file(self): + path, _ = QFileDialog.getOpenFileName( + self, "选择固件文件", "", "固件文件 (*.bin);;所有文件 (*)" + ) + if path: + self.fw_edit.setText(path) + + def _log(self, msg, tag="info"): + color = {"ok": "#2E7D32", "err": "#D32F2F", "info": "#1565C0"}.get(tag, "#000000") + self.log_text.append(f'{msg}') + + def _log_debug(self, data): + if not data: + return + if any(0x20 <= b <= 0x7E for b in data): + text = data.decode('utf-8', errors='replace').strip('\x00\r\n ') + if text: + self._log(f"[设备] {text}") + + def _start(self): + if self.running: + return + path = self.fw_edit.text().strip() + mac = parse_mac(self.mac_edit.text()) + if not path or not os.path.exists(path): + QMessageBox.warning(self, "提示", "请先选择固件文件") + return + if mac is None: + QMessageBox.warning(self, "提示", "设备MAC格式错误,应为 81-00-00-06-00-00") + return + if not self.main.serial_port or not self.main.serial_port.is_open: + QMessageBox.warning(self, "提示", "请先打开串口") + return + try: + with open(path, 'rb') as f: + fw = f.read() + except Exception as e: + QMessageBox.critical(self, "错误", f"读取固件失败: {e}") + return + if not fw: + QMessageBox.warning(self, "提示", "固件文件为空") + return + + self._mac = mac + self._fw_data = fw + self._pkg_num = (len(fw) + UPG_PACKET_DATA - 1) // UPG_PACKET_DATA + self._sent.clear() + self._buf = b"" + self.running = True + self.start_btn.setEnabled(False) + self.progress.setValue(0) + self.status_lbl.setText("等待握手...") + + app_crc = zlib.crc32(fw) & 0xFFFFFFFF + payload = struct.pack('= 11: + idx = self._buf.find(bytes([FRAME_HEADER])) + if idx < 0: + # 无帧头,保留末尾字节以防跨包帧头 + self._buf = self._buf[-1:] + return None + if idx > 0: + self._log_debug(self._buf[:idx]) + self._buf = self._buf[idx:] + plen = self._buf[8] + total = 11 + plen + if len(self._buf) < total: + return None + frame = self._buf[:total] + cmd = frame[7] + payload = frame[9:9 + plen] + if crc16_modbus(frame[:-2]) == (frame[-2] | (frame[-1] << 8)): + self._buf = self._buf[total:] + return (cmd, payload) + # 误判的帧头,丢弃继续寻找 + self._buf = self._buf[1:] + return None + + def _handle(self, cmd, payload): + self._timer.start(5000) + if cmd == CMD_ACK: + self._log("收到从机应答(0x01)", "ok") + self.status_lbl.setText("设备就绪") + return + if cmd == CMD_REQ_PKG: + if len(payload) < 4: + self._log("从机请求包帧格式错误", "err") + return + idx, total = struct.unpack('= self._pkg_num: + self._log(f"从机请求包序号越界({idx}),视为完成", "ok") + self._finish() + return + start = idx * UPG_PACKET_DATA + data = self._fw_data[start:start + UPG_PACKET_DATA] + p = struct.pack('= self._pkg_num: + self._finish() + return + self._log(f"收到其他帧 Cmd={cmd:#04x}", "err") + + def _finish(self): + self.running = False + self._timer.stop() + self.start_btn.setEnabled(True) + if len(self._sent) >= self._pkg_num: + self._log(f"升级完成 (已发送全部 {len(self._sent)} 包)", "ok") + self.progress.setValue(100) + self.status_lbl.setText("完成") + else: + self._log(f"升级未完成,已发送 {len(self._sent)}/{self._pkg_num} 包", "err") + + def _on_timeout(self): + if not self.running: + return + self.running = False + self._timer.stop() + self.start_btn.setEnabled(True) + self._log("升级超时(5秒无响应)", "err") + self.status_lbl.setText("超时") + + def closeEvent(self, event): + self._reset_ui() + event.accept() + + +class DeviceDialog(QDialog): + """设备管理对话框:通过 culs 查询已注册通讯单元,cudel 删除设备。""" + + def __init__(self, main_window): + super().__init__(main_window) + self.main = main_window + self.setWindowTitle("设备管理 - 查询/删除") + self.setModal(False) + self.resize(860, 520) + + self._buf = "" + self._pending_del = None # 等待删除结果的MAC,"all"表示删除全部 + self._quiet = QTimer(self) + self._quiet.setSingleShot(True) + self._quiet.timeout.connect(self._on_quiet) + + root = QVBoxLayout(self) + root.setContentsMargins(8, 8, 8, 8) + root.setSpacing(6) + + btn_row = QHBoxLayout() + self.btn_refresh = QPushButton("刷新查询") + self.btn_refresh.clicked.connect(self.query) + btn_row.addWidget(self.btn_refresh) + self.btn_del_all = QPushButton("删除全部设备") + self.btn_del_all.setStyleSheet("color:#D32F2F; font-weight:bold;") + self.btn_del_all.clicked.connect(self._del_all) + btn_row.addWidget(self.btn_del_all) + btn_close = QPushButton("关闭") + btn_close.clicked.connect(self.close) + btn_row.addWidget(btn_close) + btn_row.addStretch() + self.status_lbl = QLabel("就绪") + btn_row.addWidget(self.status_lbl) + root.addLayout(btn_row) + + self.table = QTableWidget(0, 11) + self.table.setHorizontalHeaderLabels( + ["DevType", "设备MAC", "传感器类型", "Laser", "电量", + "RSSI", "SNR", "FPO", "LpCfg", "在线", "操作"]) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) + self.table.horizontalHeader().setStretchLastSection(True) + self.table.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.table.setSelectionMode(QAbstractItemView.SingleSelection) + self.table.verticalHeader().setVisible(False) + root.addWidget(self.table, 1) + + def _serial_ok(self): + if not self.main.serial_port or not self.main.serial_port.is_open: + QMessageBox.warning(self, "错误", "请先打开串口!") + return False + return True + + # ── 查询 ── + def query(self): + if not self._serial_ok(): + return + self._buf = "" + self._pending_del = None + self._quiet.stop() + self.status_lbl.setText("查询中...") + self.main.send_command("culs") + self._quiet.start(800) + + def feed_text(self, text): + self._buf += text + self._quiet.start(600) + + def _on_quiet(self): + if self._pending_del is not None: + buf = self._buf + if "deleted" in buf.lower(): + self.status_lbl.setText(f"删除成功 ({self._pending_del})") + elif "not registered" in buf: + self.status_lbl.setText("删除失败:设备未注册") + elif "Para Error" in buf: + self.status_lbl.setText("删除失败:指令参数错误") + else: + self.status_lbl.setText("删除失败:设备无响应") + self._pending_del = None + self.query() + return + self._parse_devices() + + def _parse_devices(self): + devices = [] + cur = None + for raw in self._buf.replace('\r', '').split('\n'): + line = raw.strip() + # Lora通讯单元:DevType: XXXX, SensorNum: N + m = re.match(r'DevType:\s*([0-9A-Fa-f]{4}),\s*SensorNum:\s*(\d+)', line) + if m: + cur = {"type": m.group(1).upper(), "builtin": False, + "macs": [], "types": [], "bat": "", "rssi": "", "snr": "", "online": ""} + devices.append(cur) + continue + # RS485内置通讯单元:DevType: XXXX(单独一行) + m = re.match(r'DevType:\s*([0-9A-Fa-f]{4})$', line) + if m: + cur = {"type": m.group(1).upper(), "builtin": True, + "macs": [], "types": [], "bat": "", "rssi": "", "snr": "", "online": ""} + devices.append(cur) + continue + if cur is None: + continue + m = re.match(r'DevMac:\s*([0-9A-Fa-f]{2}(?:-[0-9A-Fa-f]{2}){5})', line) + if m: + cur["macs"].append(("Master", m.group(1).upper())) + continue + m = re.match( + r'(Master|Slave)\s*Mac:\s*([0-9A-Fa-f]{2}(?:-[0-9A-Fa-f]{2}){5})' + r'(?:\s*Type:\s*([0-9A-Fa-f]{4}))?' + r'(?:\s*Laser:\s*(On|Off))?', line) + if m: + cur["macs"].append((m.group(1), m.group(2).upper())) + if m.group(3): + cur["types"].append(m.group(3).upper()) + if m.group(4): + cur["laser"] = m.group(4) + continue + m = re.match(r'Bat:(\d+)%\s*Rssi:(-?\d+)\s*Snr:(-?\d+)', line) + if m: + cur["bat"], cur["rssi"], cur["snr"] = m.group(1), m.group(2), m.group(3) + continue + # FPO Start:08:00, Dur:8h, End:16:00, FullPower + m = re.match(r'FPO\s+Start:(\d+):00,\s*Dur:(\d+)h,\s*End:(\d+):00,\s*(\w+)', line) + if m: + cur["fpo"] = f"{m.group(1)}:00-{m.group(3)}:00 {m.group(2)}h {m.group(4)}" + continue + # LpCfg: CI:300sec, RI:20min, EI:2min, ET:20min + m = re.match(r'LpCfg:\s*CI:(\d+)sec,\s*RI:(\d+)min,\s*EI:(\d+)min,\s*ET:(\d+)min', line) + if m: + cur["lpcfg"] = (f"CI:{m.group(1)}s RI:{m.group(2)}m " + f"EI:{m.group(3)}m ET:{m.group(4)}m") + continue + m = re.match(r'Online:(Yes|No)', line) + if m: + cur["online"] = m.group(1) + self._fill_table(devices) + self._buf = "" + + def _fill_table(self, devices): + self.table.setRowCount(0) + for dev in devices: + row = self.table.rowCount() + self.table.insertRow(row) + type_str = dev["type"] + ("(内置)" if dev["builtin"] else "") + macs = "\n".join(f"{role}: {mac}" for role, mac in dev["macs"]) + types = " ".join(dev["types"]) if dev["types"] else "-" + online = dev["online"] or "-" + vals = [type_str, macs or "-", types, + dev.get("laser", "") or "-", + f'{dev["bat"]}%' if dev["bat"] else "-", + dev["rssi"] or "-", dev["snr"] or "-", + dev.get("fpo", "") or "-", + dev.get("lpcfg", "") or "-", + online] + for col, v in enumerate(vals): + item = QTableWidgetItem(v) + if col == 9 and online in ("Yes", "No"): + item.setForeground(Qt.green if online == "Yes" else Qt.red) + self.table.setItem(row, col, item) + master = next((mac for role, mac in dev["macs"] if role == "Master"), None) + if master and not dev["builtin"]: + btn = QPushButton("删除") + btn.setStyleSheet("color:#D32F2F;") + btn.clicked.connect(lambda _, m=master: self._del_one(m)) + self.table.setCellWidget(row, 10, btn) + else: + self.table.setItem(row, 10, QTableWidgetItem("-" if dev["builtin"] else "")) + self.status_lbl.setText(f"共 {len(devices)} 个设备") + + # ── 删除 ── + def _del_one(self, mac): + if not self._serial_ok(): + return + ret = QMessageBox.question(self, "确认删除", f"确定删除设备 {mac} 吗?", + QMessageBox.Yes | QMessageBox.No) + if ret != QMessageBox.Yes: + return + self._buf = "" + self._pending_del = mac + self.status_lbl.setText(f"正在删除 {mac} ...") + self.main.send_command(f"cudel {mac}") + self._quiet.start(800) + + def _del_all(self): + if not self._serial_ok(): + return + ret = QMessageBox.question(self, "确认删除", "确定删除全部已注册设备吗?", + QMessageBox.Yes | QMessageBox.No) + if ret != QMessageBox.Yes: + return + self._buf = "" + self._pending_del = "all" + self.status_lbl.setText("正在删除全部设备 ...") + self.main.send_command("cudel all") + self._quiet.start(800) + + def closeEvent(self, event): + self._quiet.stop() + event.accept() + + def main(): app = QApplication(sys.argv) app.setStyle("Fusion") diff --git a/generate_manual.py b/generate_manual.py index 27cbfab..d360bf5 100644 --- a/generate_manual.py +++ b/generate_manual.py @@ -184,12 +184,9 @@ toc_items = [ ("3", "功能详解"), (" 3.1", "串口连接"), (" 3.2", "查看配置(para)"), - (" 3.3", "通讯方式切换"), - (" 3.4", "ETH网络参数配置"), - (" 3.5", "Lora信道配置"), - (" 3.6", "低功耗配置"), - (" 3.7", "自定义指令"), - (" 3.8", "通讯日志"), + (" 3.3", "Lora信道配置"), + (" 3.4", "自定义指令"), + (" 3.5", "通讯日志"), ("4", "指令参考"), ("5", "常见问题(FAQ)"), ("6", "注意事项"), @@ -213,10 +210,7 @@ features = [ ["功能模块", "说明"], ["串口通信", "通过USB转串口连接网关设备,波特率500000,实时收发指令"], ["配置读取", "一键读取网关全部参数,包括软件/硬件版本、MAC地址、电池状态、注册状态等"], - ["通讯方式切换", "支持Cat1(4G)和ETH(以太网)两种通讯方式的切换"], - ["ETH参数配置", "配置以太网相关参数:IP地址、端口、子网掩码、网关、服务器地址等"], - ["Lora信道配置", "选择并设置Lora通信信道频率(410.1MHz ~ 524.9MHz)"], - ["低功耗配置", "查看采集间隔、上报间隔、紧急上报等低功耗参数"], + ["Lora信道配置", "选择并设置Lora通信信道频率(410.1MHz ~ 492.9MHz)"], ["自定义指令", "支持手动输入任意指令与网关交互"], ["通讯日志", "实时显示发送和接收的通讯数据,便于调试"], ] @@ -235,15 +229,11 @@ story.append(make_table(["项目", "要求"], req_rows, col_widths=[35 * mm, 115 story.append(heading2("1.3 软件界面总览")) story.append(body("软件界面自上而下分为以下几个区域:")) ui_rows = [ - [f"{LQ}串口连接{RQ}区", "选择串口、刷新端口列表、打开/关闭串口连接"], - [f"基本指令区", f"查看配置、切换通讯方式(Cat1/ETH)的快捷按钮"], - [f"当前网关配置", f"显示网关读取到的各项参数(版本、MAC、电池、注册状态等)"], - [f"ETH网络参数", f"显示当前ETH网络参数(IP、端口、掩码、服务器地址等)"], - [f"低功耗配置", f"显示采集间隔、上报间隔等低功耗相关参数"], - [f"ETH参数配置", f"输入并发送ETH相关配置参数"], - [f"Lora信道配置", f"选择Lora信道并发送"], - [f"通讯日志", f"实时显示收发数据(蓝色=发送,橙色=接收)"], - [f"自定义指令", f"手动输入任意指令发送"], + [f"{LQ}串口连接{RQ}区", "选择串口、刷新端口列表、打开/关闭串口"], + [f"{LQ}参数配置{RQ}区", f"设置LoRa信道频率、{LQ}查看配置(para){RQ}、{LQ}设备升级{RQ}按钮"], + [f"{LQ}设备参数信息{RQ}区", f"显示网关读取到的各项参数(版本、MAC、电池、注册状态等)"], + [f"{LQ}通讯日志{RQ}区", f"实时显示收发数据(蓝色=发送,橙色=接收)"], + [f"{LQ}自定义指令{RQ}区", f"手动输入任意指令发送"], ] story.append(make_table(["区域", "功能说明"], ui_rows, col_widths=[35 * mm, 115 * mm])) @@ -302,10 +292,9 @@ para_rows = [ ["软件版本", "SoftWare V", "网关固件版本号"], ["硬件版本", "HardWare V", "网关硬件版本号"], ["网关MAC", "GateWay MAC", "网关唯一MAC地址"], - ["电池", "Battery", "当前电池电量"], - ["注册状态", "Register Status", "0=未注册,1=已注册"], - ["通讯方式", "CommInf", "当前通讯方式(Cat1/Eth)"], - ["读取间隔", "CommUnitReadInterval", "通讯单元读取间隔(秒)"], + ["电池电量", "Battery", "当前电池电量"], + ["注册状态", "MuchReg", "true=已注册,false/null=未注册"], + ["通讯方式", "Much", "当前主通讯方式(eth/cat1/lora)"], ["Cat1 IMEI", "Cat1 IMEI", "4G模块IMEI号"], ["Cat1 SIM", "Cat1 SIM", "SIM卡信息"], ["Lora频率", "Lora fc", "当前Lora信道频率"], @@ -313,55 +302,10 @@ para_rows = [ story.append(make_table(["显示项", "指令关键字", "说明"], para_rows, col_widths=[30 * mm, 45 * mm, 75 * mm])) # 3.3 -story.append(heading2("3.3 通讯方式切换")) -story.append(body("网关支持两种通讯方式,可通过快捷按钮切换:")) -comm_rows = [ - ["Cat1(comm cat1)", "comm cat1", "切换到4G/Cat1通讯模式"], - ["ETH(comm eth)", "comm eth", "切换到以太网通讯模式"], -] -story.append(make_table(["按钮", "发送指令", "说明"], comm_rows, col_widths=[40 * mm, 35 * mm, 75 * mm])) -story.append(Spacer(1, 2 * mm)) -story.append(note(f"切换通讯方式后,需要重新点击{LQ}查看配置{RQ}刷新当前状态。ETH参数配置必须在ETH模式下进行。")) - -story.append(PageBreak()) - -# 3.4 -story.append(heading2("3.4 ETH网络参数配置")) +story.append(heading2("3.3 Lora信道配置")) story.append(body( - f"ETH参数配置区域用于设置网关以太网相关的网络参数。必须先切换到ETH模式才能发送ETH相关指令。" -)) -story.append(Spacer(1, 2 * mm)) - -eth_rows = [ - ["网关IP", "eth ip", "192.168.1.101", "网关本地IP地址"], - ["网关端口", "eth port", "10000", "网关本地端口号(1-65535)"], - ["服务器网关", "eth gw", "192.168.1.1", "网络网关地址"], - ["服务器IP", "eth dip", "192.168.1.100", "远端服务器IP地址"], - ["服务器端口", "eth dport", "8080", "远端服务器端口号(1-65535)"], -] -story.append(make_table( - ["参数名称", "指令前缀", "默认值", "说明"], - eth_rows, col_widths=[28 * mm, 25 * mm, 35 * mm, 62 * mm] -)) -story.append(Spacer(1, 3 * mm)) - -story.append(heading3("ETH配置流程")) -eth_steps = [ - f"点击ETH(comm eth)按钮切换到以太网模式;", - "在各输入框中填入目标参数值;", - "逐项点击发送按钮,将参数写入网关;", - f"最后点击应用ETH配置(eth apply)按钮,使配置生效。", -] -for i, s in enumerate(eth_steps, 1): - story.append(Paragraph(f"{i}. {s}", style_body)) -story.append(Spacer(1, 2 * mm)) -story.append(note(f"仅发送参数而未点击{LQ}应用ETH配置{RQ},参数不会生效。IP地址格式为 x.x.x.x,端口范围为1-65535。")) - -# 3.5 -story.append(heading2("3.5 Lora信道配置")) -story.append(body( - "Lora信道配置用于设置网关的Lora无线通信频率。信道列表覆盖410.1MHz至524.9MHz范围," - "频率间隔0.2MHz,共575个信道可供选择。" + "Lora信道配置用于设置网关的Lora无线通信频率。信道列表覆盖410.1MHz至492.9MHz范围," + "频率间隔0.2MHz,共415个信道可供选择。" )) story.append(Spacer(1, 2 * mm)) @@ -376,31 +320,14 @@ story.append(Spacer(1, 2 * mm)) lora_range = [ ["起始频率", "410.1 MHz(信道1)"], - ["终止频率", "524.9 MHz(信道575)"], + ["终止频率", "492.9 MHz(信道415)"], ["频率间隔", "0.2 MHz(200 kHz)"], ["指令格式", "lora fc [频率值,单位Hz]"], ] story.append(make_table(["项目", "值"], lora_range, col_widths=[35 * mm, 115 * mm])) -# 3.6 -story.append(heading2("3.6 低功耗配置")) -story.append(body( - f"低功耗配置显示网关的低功耗运行参数,这些参数通过{LQ}查看配置(para){RQ}指令读取,在{LQ}低功耗配置{RQ}分组框中显示:" -)) -lp_rows = [ - ["采集间隔(CI)", "传感器数据采集的时间间隔"], - ["上报间隔(RI)", "正常数据上报的时间间隔"], - ["紧急上报间隔(EI)", "紧急状态下的数据上报间隔"], - ["紧急上报时长(ET)", "紧急上报模式持续时长"], -] -story.append(make_table(["参数", "说明"], lp_rows, col_widths=[40 * mm, 110 * mm])) -story.append(Spacer(1, 2 * mm)) -story.append(note("低功耗参数单位可能为秒(sec)或分钟(min),以网关返回的实际值为准。")) - -story.append(PageBreak()) - -# 3.7 -story.append(heading2("3.7 自定义指令")) +# 3.4 +story.append(heading2("3.4 自定义指令")) story.append(body( "自定义指令区域允许用户手动输入任意指令与网关交互,适用于高级调试和特殊配置场景。" )) @@ -414,12 +341,12 @@ custom_steps = [ for i, s in enumerate(custom_steps, 1): story.append(Paragraph(f"{i}. {s}", style_body)) -# 3.8 -story.append(heading2("3.8 通讯日志")) +# 3.5 +story.append(heading2("3.5 通讯日志")) story.append(body("通讯日志区域实时显示与网关的通讯数据,采用深色背景方便阅读:")) log_rows = [ - ["蓝色文字", "发送的指令(>>> 前缀)", "包含时间戳和发送的指令内容"], - ["橙色文字", "接收的数据(<<< 前缀)", "包含时间戳和网关返回的原始数据"], + ["蓝色文字", "发送的指令(>>> 前缀)", "发送的指令内容"], + ["橙色文字", "接收的数据(<<< 前缀)", "网关返回的原始数据"], ] story.append(make_table(["颜色", "类型", "说明"], log_rows, col_widths=[25 * mm, 45 * mm, 80 * mm])) story.append(Spacer(1, 2 * mm)) @@ -435,14 +362,6 @@ story.append(Spacer(1, 2 * mm)) cmd_rows = [ ["para", "读取网关全部配置参数", "查看配置(para)"], - ["comm cat1", "切换到Cat1通讯模式", "Cat1(comm cat1)"], - ["comm eth", "切换到ETH通讯模式", "ETH(comm eth)"], - ["eth ip <IP>", "设置网关本地IP地址", "ETH参数配置"], - ["eth port <端口>", "设置网关本地端口号", "ETH参数配置"], - ["eth gw <IP>", "设置网络网关地址", "ETH参数配置"], - ["eth dip <IP>", "设置远端服务器IP", "ETH参数配置"], - ["eth dport <端口>", "设置远端服务器端口", "ETH参数配置"], - ["eth apply", "应用ETH配置参数", "应用ETH配置"], ["lora fc <频率>", "设置Lora信道频率(Hz)", "Lora信道配置"], ] story.append(make_table( @@ -465,16 +384,7 @@ faqs = [ ("Q2:发送指令后没有回复?", f"1)确认串口已成功打开(按钮显示红色{LQ}关闭串口{RQ});2)检查串口线RX/TX是否接反;" "3)确认网关设备已上电且正常运行;4)尝试重新连接串口。"), - (f"Q3:ETH参数发送提示{LQ}请先切换到ETH模式{RQ}?", - f"ETH参数配置必须在ETH通讯模式下进行。请先点击{LQ}ETH(comm eth){RQ}按钮切换模式," - "再进行ETH参数的发送。"), - ("Q4:修改ETH参数后未生效?", - f"修改ETH参数后,需要点击{LQ}应用ETH配置(eth apply){RQ}按钮,参数才会真正生效。" - "仅发送参数值而未应用,网关不会更新配置。"), - ("Q5:如何确认当前网关的通讯方式?", - f"点击{LQ}查看配置(para){RQ}按钮,在{LQ}当前网关配置{RQ}区域查看{LQ}通讯方式{RQ}字段," - "显示为Cat1或Eth。"), - ("Q6:Lora信道列表很长,如何快速选择?", + ("Q3:Lora信道列表很长,如何快速选择?", "Lora信道下拉框支持鼠标滚轮滚动选择,也可以点击下拉后直接滚动浏览。" "信道编号和频率一一对应,选择后点击发送即可。"), ] @@ -490,11 +400,8 @@ story.append(heading1("6 注意事项")) story.append(hr()) warnings = [ - "通讯模式切换:在进行ETH参数配置前,务必先切换到ETH通讯模式,否则参数发送将被拒绝。", - f"ETH配置应用:修改ETH网络参数后,必须点击{LQ}应用ETH配置{RQ}按钮才能使参数生效。", "串口独占:同一串口在同一时间只能被一个程序打开,请确保使用前关闭其他可能占用串口的软件。", "波特率固定:本工具固定使用500000波特率,与网关固件匹配,请勿尝试修改。", - "参数格式:IP地址需符合标准格式(如192.168.1.101),端口号范围为1-65535,否则发送可能失败。", f"数据备份:修改配置前建议先通过{LQ}查看配置{RQ}记录当前参数,以便需要时恢复。", "断电保护:配置修改后请确认网关已保存参数,避免在参数写入过程中断电。", "驱动安装:首次使用需安装USB转串口芯片驱动(如CH340、CP2102等),具体型号取决于使用的转接线。",