5 Commits

Author SHA1 Message Date
YuanHongbin 36baabeb5f 优化配置工具:波特率可编辑、移除激光与全功率时段配置、调整LoRa信道并规范注册状态显示 2026-09-02 10:48:49 +08:00
YuanHongbin afa83653a3 移除dch ch1切换逻辑并删除通用升级程序
- 查询当前配置直接发送para,不再预先发送dch ch1并等待切换回应
- 删除已不再使用的通用升级程序.exe(升级功能已集成到软件内)
2026-09-01 18:39:23 +08:00
YuanHongbin 03046499ee 更新使用说明书至V2.0并同步软件功能
- 界面区域调整:新增功率与激光配置区域说明,通讯日志调整说明
- 新增全功率时段配置、激光控制、设备升级操作说明
- 指令参考新增 fpocfg/laser 指令,去掉 dch ch1 内部切换描述
- Lora频率范围更新为410.1~492.9MHz
- 版本号升级为 V2.0
2026-08-25 15:42:35 +08:00
YuanHongbin 629a92ff41 新增设备升级功能并优化界面
- 集成传感器通用串口升级协议V1.1:帧头0x7D+DevMac(6)+Cmd+Len+Payload+MODBUS CRC16
- 新增"设备升级"对话框:选择固件、设备MAC、进度条,复用当前串口不断开连接
- 支持升级完成后在弹窗内再次升级
- 设备ASCII调试信息输出到主界面通讯日志,握手/下发/完成信息输出到升级弹窗
- 修复软件版本/硬件版本有时解析失败(等待完整分隔帧再解析)
- Lora信道频率下拉框范围改为410.1~492.9MHz
- 新增功率与激光参数显示及全功率时段/激光控制配置(与Lora布局一致)
- 查询当前配置时先发 dch ch1,等切换完成后再发 para
- 通讯日志去掉时间戳、参数值去除分隔逗号、窗口自动适配内容
- 附带升级协议文档与通用升级程序
2026-08-25 14:51:05 +08:00
YuanHongbin 2a84701e42 生成说明书 2026-08-19 10:13:23 +08:00
5 changed files with 417 additions and 28 deletions
+92 -13
View File
@@ -89,7 +89,7 @@ story = []
story.append(Spacer(1, 6*cm))
story.append(Paragraph("边坡变形监测-变形(位移)<br/>自动测量系统配置工具<br/>使用说明书", style_cover_title))
story.append(Spacer(1, 1.5*cm))
story.append(Paragraph("版本 V1.0", style_cover_sub))
story.append(Paragraph("版本 V2.0", style_cover_sub))
story.append(PageBreak())
# 目录
@@ -106,12 +106,16 @@ toc_items = [
(False, " 4.2 参数配置区域"),
(False, " 4.3 设备参数信息区域"),
(False, " 4.4 低功耗配置区域"),
(False, " 4.5 通讯日志区域"),
(False, " 4.5 功率与激光配置区域"),
(False, " 4.6 通讯日志区域"),
(True, "5 操作指南"),
(False, " 5.1 连接设备"),
(False, " 5.2 查询设备参数"),
(False, " 5.3 修改Lora信道频率"),
(False, " 5.4 发送自定义指令"),
(False, " 5.4 全功率时段配置"),
(False, " 5.5 激光控制"),
(False, " 5.6 设备升级"),
(False, " 5.7 发送自定义指令"),
(True, "6 指令参考"),
(True, "7 常见问题与故障排除"),
]
@@ -131,8 +135,11 @@ features = [
"自动识别并列出本机可用串口",
"通过RS485串口连接激光示踪传感器设备",
"一键查询设备全部参数(软件版本、硬件版本、MAC地址、电池电量、注册状态等)",
"修改Lora无线通信信道频率(410.1MHz ~ 524.9MHz,步进200kHz",
"修改Lora无线通信信道频率(410.1MHz ~ 492.9MHz,步进200kHz",
"查看设备低功耗配置参数(采集间隔、上报时间、紧急上报间隔/时长)",
"查看功率与激光参数,并配置全功率工作时间段",
"控制激光开关及设置自动关闭时间",
"通过通用串口升级协议(V1.1)实现设备固件升级",
"支持发送自定义AT指令",
"通讯日志实时显示,方便调试",
]
@@ -209,16 +216,28 @@ story.append(make_table(
))
story.append(Paragraph("4.2 参数配置区域", style_h2))
story.append(Paragraph("包含Lora参数配置和查询操作:", style_body))
story.append(Paragraph("包含Lora参数、全功率时段配置、激光控制以及查询/升级操作:", style_body))
story.append(Paragraph("Lora 参数:", style_body))
story.append(Paragraph(
"信道频率(fc):下拉选择Lora通信频率,范围410.1MHz ~ 524.9MHz,步进200kHz(共575个信道)。"
"信道频率(fc):下拉选择Lora通信频率,范围410.1MHz ~ 492.9MHz,步进200kHz(共415个信道)。"
'选择后点击"发送"按钮即可修改设备频率。',
style_bullet
))
story.append(Paragraph("查询当前配置:", style_body))
story.append(Paragraph("全功率时段配置:", style_body))
story.append(Paragraph(
'点击"查询当前配置(para)"按钮,向设备发送para指令,查询设备全部参数。查询结果将自动解析并显示在下方的参数信息区域。',
"设置全功率工作时间段:开始时间(0~23时)、持续时间(0~24时,0=关闭),"
'输入后点击"发送"按钮下发配置。',
style_bullet
))
story.append(Paragraph("激光控制:", style_body))
story.append(Paragraph(
'点击"打开"/"关闭"按钮控制激光开关;"自动关闭(min)"设置激光自动关闭时间(0=禁用),点击"设置"下发。',
style_bullet
))
story.append(Paragraph("查询与升级按钮:", style_body))
story.append(Paragraph(
'点击"查询当前配置(para)"按钮,软件向设备发送para指令查询全部参数。'
'查询结果将自动解析并显示在下方参数信息区域。点击"设备升级"按钮可打开设备升级对话框。',
style_bullet
))
@@ -254,14 +273,29 @@ story.append(make_table(
col_widths=[5*cm, 11*cm]
))
story.append(Paragraph("4.5 通讯日志区域", style_h2))
story.append(Paragraph("4.5 功率与激光配置区域", style_h2))
story.append(Paragraph("显示通过para指令查询到的功率与激光运行参数:", style_body))
story.append(make_table(
["参数名称", "说明"],
[
["开始时间", "全功率工作时间段的开始时刻"],
["结束时间", "全功率工作时间段的结束时刻"],
["全功率时长", "全功率工作持续时间"],
["当前模式", "当前工作模式(全功率/低功耗)"],
["激光状态", "激光当前开关状态(ON/OFF"],
["自动关闭", "激光自动关闭时间"],
],
col_widths=[5*cm, 11*cm]
))
story.append(Paragraph("4.6 通讯日志区域", style_h2))
story.append(Paragraph("界面底部,分为左右两部分:", style_body))
story.append(Paragraph("日志窗口:实时显示串口通讯内容,带时间戳。不同类型信息不同颜色区分:", style_body))
story.append(Paragraph("日志窗口:实时显示串口通讯内容。不同类型信息不同颜色区分:", style_body))
story.append(make_table(
["颜色", "含义"],
[
["蓝色", "发送的指令"],
["绿色", "接收到的"],
["绿色", "接收到的"],
["浅绿色", "系统提示信息"],
["红色", "错误信息"],
],
@@ -310,7 +344,44 @@ story.append(Paragraph(
style_warning
))
story.append(Paragraph("5.4 发送自定义指令", style_h2))
story.append(Paragraph("5.4 全功率时段配置", style_h2))
steps = [
"确认串口已成功连接。",
'"参数配置"区域的"全功率时段配置"中输入开始时间(0~23)和持续时间(0~240=关闭)。',
'点击"发送"按钮下发配置。',
'发送后建议点击"查询当前配置"验证配置是否生效。',
]
for i, s in enumerate(steps, 1):
story.append(step(i, s))
story.append(Paragraph("5.5 激光控制", style_h2))
steps = [
"确认串口已成功连接。",
'点击"打开""关闭"按钮控制激光开关。',
"如需要设置自动关闭时间,在\"自动关闭(min)\"框中输入分钟数(0=禁用),点击\"设置\"下发。",
'发送后建议点击"查询当前配置"验证激光状态。',
]
for i, s in enumerate(steps, 1):
story.append(step(i, s))
story.append(Paragraph("5.6 设备升级", style_h2))
story.append(Paragraph("设备升级用于通过通用串口升级协议对设备固件进行升级。升级期间请保持设备供电正常。", style_body))
steps = [
"确认串口已成功连接。",
'点击"参数配置"区域中的"设备升级"按钮,打开设备升级对话框。',
"在对话框中选择要更新的固件文件(.bin),并确认设备MAC地址(默认自动填充本机MAC)。",
'点击"开始升级"按钮,软件通过协议下发固件并向设备逐包传输。',
"握住安装、进度条显示传输进度;升级日志显示握手/下发/完成等过程信息。",
"升级完成后可在同一对话框再次点击\"开始升级\"重复升级,无需关闭重开。",
]
for i, s in enumerate(steps, 1):
story.append(step(i, s))
story.append(Paragraph(
"注意:升级过程中请勿断开串口或断电,否则可能导致设备无法正常启动。",
style_warning
))
story.append(Paragraph("5.7 发送自定义指令", style_h2))
steps = [
"确认串口已成功连接。",
'在通讯日志区域右侧的"自定义指令"输入框中输入AT指令。',
@@ -328,7 +399,10 @@ story.append(make_table(
[
["para", "查询设备全部参数", "para"],
["lora fc <频率值>", "设置Lora信道频率(单位Hz", "lora fc 433100000"],
["(自定义指令)", "在自定义指令输入框中输入任意AT指令", ""],
["fpocfg <开始> <持续>", "设置全功率工作时间段(时,0=关闭)", "fpocfg 8 2"],
["laser on / off", "打开/关闭激光", "laser on"],
["laser time <min>", "设置激光自动关闭时间(0=禁用)", "laser time 10"],
["(自定义指令)", "在自定义指令输入框中输入任意指令", ""],
],
col_widths=[5*cm, 6*cm, 5*cm]
))
@@ -365,6 +439,11 @@ qa_items = [
"1. 点击更多信息;\n"
"2. 点击仍要运行按钮;\n"
"3. 此提示是因为exe未做代码签名,软件本身安全。"),
("点击设备升级后无握手/升级失败",
"1. 确认设备MAC地址正确;\n"
"2. 确认固件文件为正确的.bin文件;\n"
"3. 确认串口连接正常、设备供电稳定;\n"
"4. 查看通讯日志中的设备调试信息辅助排查。"),
]
for q, a in qa_items:
story.append(Paragraph("Q: " + q, style_q))
+325 -15
View File
@@ -4,7 +4,9 @@
"""
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
from tkinter import ttk, scrolledtext, messagebox, filedialog
import struct
import binascii
import serial
import serial.tools.list_ports
import threading
@@ -12,6 +14,8 @@ import time
import re
import sys
import os
import shutil
import subprocess
from datetime import datetime
@@ -25,7 +29,6 @@ class LaserTracingConfigApp:
def __init__(self, root):
self.root = root
self.root.title("边坡变形监测-变形(位移)自动测量系统配置工具")
self.root.geometry("900x700")
self.root.resizable(True, True)
icon_path = resource_path("传感器.ico")
@@ -35,12 +38,17 @@ class LaserTracingConfigApp:
self.serial_port = None
self.serial_thread = None
self.running = False
self.upgrading = False
self.recv_buffer = ""
self._port_details = {}
self._build_ui()
self._refresh_ports()
# 根据内容自动调整窗口大小,刚好完整显示参数
self.root.update_idletasks()
self.root.geometry("")
# ─────────────────── UI构建 ───────────────────
def _build_ui(self):
@@ -56,7 +64,9 @@ class LaserTracingConfigApp:
ttk.Label(conn_frame, text="波特率:").grid(row=0, column=3, sticky=tk.W, padx=2)
self.baudrate_var = tk.StringVar(value="9600")
ttk.Label(conn_frame, textvariable=self.baudrate_var, width=8).grid(row=0, column=4, padx=2)
self.baudrate_combo = ttk.Combobox(conn_frame, textvariable=self.baudrate_var, width=8,
values=["2400", "4800", "9600", "19200", "38400", "57600", "115200"])
self.baudrate_combo.grid(row=0, column=4, padx=2)
ttk.Label(conn_frame, text="数据位:").grid(row=0, column=5, sticky=tk.W, padx=2)
self.databits_var = tk.StringVar(value="8")
@@ -85,25 +95,27 @@ class LaserTracingConfigApp:
self._fc_display_to_value = {}
self._fc_value_to_display = {}
fc_displays = []
freq = 410100000
ch = 1
while freq < 525000000:
freq = 470100000
ch = 0
while freq <= 492600000:
display = f"信道{ch}: {freq / 1000000:.1f}MHz"
fc_displays.append(display)
self._fc_display_to_value[display] = str(freq)
self._fc_value_to_display[str(freq)] = display
freq += 200000
freq += 500000
ch += 1
self.lora_fc_display_var = tk.StringVar(value=self._fc_value_to_display.get("433100000", fc_displays[0] if fc_displays else ""))
self.lora_fc_display_var = tk.StringVar(value=self._fc_value_to_display.get("470100000", fc_displays[0] if fc_displays else ""))
self.lora_fc_combo = ttk.Combobox(lora_frame, textvariable=self.lora_fc_display_var,
values=fc_displays, width=20, state="readonly")
self.lora_fc_combo.grid(row=0, column=1, padx=2)
ttk.Button(lora_frame, text="发送", command=self._send_lora_config).grid(row=0, column=3, padx=8)
ttk.Label(lora_frame, text="设置LoRa通信信道频率", foreground="gray").grid(row=0, column=4, padx=(0, 2), sticky=tk.W)
# 查询按钮行
query_frame = ttk.Frame(config_frame)
query_frame.pack(fill=tk.X, pady=4)
ttk.Button(query_frame, text="查询当前配置 (para)", command=self._send_para).pack(side=tk.LEFT, padx=4)
ttk.Button(query_frame, text="设备升级", command=self._launch_upgrade).pack(side=tk.LEFT, padx=4)
ttk.Label(query_frame, text="配置后可通过para命令验证是否成功",
foreground="gray").pack(side=tk.LEFT, padx=8)
@@ -242,6 +254,9 @@ class LaserTracingConfigApp:
except serial.SerialException as e:
messagebox.showerror("连接失败", str(e))
self._log(f"连接失败: {e}", "error")
except ValueError:
messagebox.showwarning("参数错误", "波特率必须为数字")
self._log("波特率必须为数字", "error")
def _disconnect(self):
self.running = False
@@ -257,6 +272,9 @@ class LaserTracingConfigApp:
def _recv_loop(self):
while self.running:
try:
if self.upgrading:
time.sleep(0.02)
continue
if self.serial_port and self.serial_port.is_open:
n = self.serial_port.in_waiting
if n > 0:
@@ -275,8 +293,9 @@ class LaserTracingConfigApp:
def _on_data_received(self, text):
self._log(text, "recv", newline=False)
# 检查是否收到完整para回应(以***结尾)
if "**********************************************************" in self.recv_buffer:
# 收到完整para回应:起始与结束各出现一次分隔符(****)后才解析
marker = "**********************************************************"
if self.recv_buffer.count(marker) >= 2:
self._parse_para_response(self.recv_buffer)
self.recv_buffer = ""
@@ -302,6 +321,13 @@ class LaserTracingConfigApp:
def _send_para(self):
self._send_command("para")
def _launch_upgrade(self):
"""打开设备升级对话框。复用当前已连接的串口,不断开连接。"""
if not self.serial_port or not self.serial_port.is_open:
messagebox.showwarning("警告", "请先打开串口")
return
UpgradeDialog(self, self.serial_port)
def _send_lora_config(self):
display = self.lora_fc_display_var.get().strip()
fc = self._fc_display_to_value.get(display)
@@ -316,8 +342,8 @@ class LaserTracingConfigApp:
lines = text.replace("\r\n", "\n").replace("\r", "\n").split("\n")
key_value_patterns = [
(r"[Ss]oft[Ww]are\s*[Vv]:\s*(\d+\.\d+)", "软件版本"),
(r"[Hh]ard[Ww]are\s*[Vv]:\s*(\d+\.\d+)", "硬件版本"),
(r"[Ss]oft[Ww]are\s*[Vv]:?\s*(\d+(?:\.\d+)*)", "软件版本"),
(r"[Hh]ard[Ww]are\s*[Vv]:?\s*(\d+(?:\.\d+)*)", "硬件版本"),
(r"Local MAC:\s*(.*)", "本机MAC"),
(r"Battery:\s*(.*)", "电池电量"),
(r"LocalType:\s*(.*)", "设备类型"),
@@ -339,6 +365,9 @@ class LaserTracingConfigApp:
# 更新参数显示
for key in self._para_order:
value = found.get(key, "-")
if key == "注册状态" and value != "-":
raw = value.strip().lower()
value = "已注册" if raw in ("1", "registered", "reg", "ok", "true", "yes", "success") else "未注册"
if key in self._para_labels:
self._para_labels[key].configure(text=value)
@@ -396,16 +425,297 @@ class LaserTracingConfigApp:
self.log_text.configure(state=tk.DISABLED)
def _log(self, message, tag="info", newline=True):
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
self.log_text.configure(state=tk.NORMAL)
if newline:
self.log_text.insert(tk.END, f"[{timestamp}] {message}\n", tag)
self.log_text.insert(tk.END, f"{message}\n", tag)
else:
self.log_text.insert(tk.END, f"[{timestamp}] {message}", tag)
self.log_text.insert(tk.END, f"{message}", tag)
self.log_text.see(tk.END)
self.log_text.configure(state=tk.DISABLED)
# ─────────────────── 通用串口升级协议(V1.1) ───────────────────
UPGRADE_HEADER = 0x7D
UPGRADE_PKG_SIZE = 200
def crc16_modbus(data):
"""MODBUS CRC16,多项式0x8005(反射0xA001),初值0xFFFF,低位在前。"""
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_upgrade_frame(devmac, cmd, payload):
"""按协议组帧:帧头(0x7D)+DevMac(6)+Cmd(1)+PayloadLen(1)+Payload+Check(2, 小端)。"""
body = bytes([UPGRADE_HEADER]) + devmac + bytes([cmd, len(payload)]) + payload
crc = crc16_modbus(body)
return body + bytes([crc & 0xFF, (crc >> 8) & 0xFF])
def parse_mac(mac_str):
"""从MAC字符串(如 09-00-01-7F-FF-FF)转为6字节bytes。"""
parts = mac_str.strip().replace("-", ":").replace(" ", "").split(":")
if len(parts) != 6:
return None
try:
return bytes(int(p, 16) for p in parts)
except ValueError:
return None
class UpgradeDialog:
PACKAGE_SIZE_LOG = 8
def __init__(self, app, serial_port):
self.app = app
self.serial_port = serial_port
self.port = serial_port.port
self.top = tk.Toplevel(app.root)
self.top.title("设备升级 - 通用串口升级协议V1.1")
self.top.geometry("720x500")
self.top.transient(app.root)
self.top.protocol("WM_DELETE_WINDOW", self._on_close)
self._orig_baud = None
self._orig_timeout = None
self._closed = False
self.running = False
# UI
cfg = ttk.LabelFrame(self.top, text="升级设置", padding=8)
cfg.pack(fill=tk.X, padx=8, pady=(8, 4))
ttk.Label(cfg, text="固件文件(.bin):").grid(row=0, column=0, sticky=tk.W, padx=2)
self.fw_var = tk.StringVar()
ttk.Entry(cfg, textvariable=self.fw_var, width=44).grid(row=0, column=1, padx=2)
ttk.Button(cfg, text="浏览", command=self._choose_file).grid(row=0, column=2, padx=2)
ttk.Label(cfg, text="设备MAC:").grid(row=1, column=0, sticky=tk.W, padx=2)
default_mac = app._para_labels["本机MAC"].cget("text")
self.mac_var = tk.StringVar(value=default_mac if default_mac != "-" else "")
ttk.Entry(cfg, textvariable=self.mac_var, width=24).grid(row=1, column=1, sticky=tk.W, padx=2)
ttk.Label(cfg, text="6字节从机MAC地址").grid(row=1, column=2, sticky=tk.W, padx=2)
self.start_btn = ttk.Button(cfg, text="开始升级", command=self._start)
self.start_btn.grid(row=2, column=1, sticky=tk.W, padx=2, pady=4)
ttk.Button(cfg, text="关闭", command=self._on_close).grid(row=2, column=2, sticky=tk.W, padx=2, pady=4)
self.progress = ttk.Progressbar(cfg, mode="determinate", maximum=100)
self.progress.grid(row=3, column=0, columnspan=3, sticky=tk.W + tk.E, padx=2, pady=(4, 0))
self.prog_var = tk.StringVar(value="就绪")
ttk.Label(cfg, textvariable=self.prog_var).grid(row=3, column=3, sticky=tk.W, padx=2)
log_frame = ttk.LabelFrame(self.top, text="升级日志", padding=4)
log_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=(4, 8))
self.log_text = scrolledtext.ScrolledText(log_frame, height=12, font=("Consolas", 8), state=tk.DISABLED)
self.log_text.tag_configure("ok", foreground="green")
self.log_text.tag_configure("err", foreground="red")
self.log_text.pack(fill=tk.BOTH, expand=True)
def _choose_file(self):
path = filedialog.askopenfilename(
title="选择固件文件",
filetypes=[("固件文件", "*.bin"), ("所有文件", "*.*")])
if path:
self.fw_var.set(path)
def _log(self, msg, tag="info"):
if self._closed:
return
tag = {"ok": "ok", "err": "err"}.get(tag, tag)
self.log_text.configure(state=tk.NORMAL)
self.log_text.insert(tk.END, f"{msg}\n", tag)
self.log_text.see(tk.END)
self.log_text.configure(state=tk.DISABLED)
def _on_close(self):
self.running = False
self._closed = True
self._restore_port()
if self.top.winfo_exists():
self.top.destroy()
def _restore_port(self):
# 恢复波特率/超时,并恢复主程序接收
if self.serial_port:
if self._orig_baud is not None:
try:
self.serial_port.baudrate = self._orig_baud
except Exception:
pass
if self._orig_timeout is not None:
try:
self.serial_port.timeout = self._orig_timeout
except Exception:
pass
self._orig_baud = None
self._orig_timeout = None
self.app.upgrading = False
def _start(self):
if self.running:
return
fw_path = self.fw_var.get().strip()
mac_bytes = parse_mac(self.mac_var.get())
if not fw_path or not os.path.exists(fw_path):
messagebox.showwarning("提示", "请先选择固件文件")
return
if not mac_bytes:
messagebox.showwarning("提示", "设备MAC格式错误,应为 09-00-01-7F-FF-FF")
return
try:
with open(fw_path, "rb") as f:
self.fw_data = f.read()
except Exception as e:
messagebox.showerror("错误", f"读取固件失败: {e}")
return
if not self.fw_data:
messagebox.showwarning("提示", "固件文件为空")
return
self.mac = mac_bytes
self.fw_size = len(self.fw_data)
self.package_num = (self.fw_size + UPGRADE_PKG_SIZE - 1) // UPGRADE_PKG_SIZE
self.pkg_size = UPGRADE_PKG_SIZE
self.running = True
self.start_btn.configure(state=tk.DISABLED)
self.progress.configure(maximum=100)
self.progress["value"] = 0
# 暂停主程序接收,调整串口至升级参数,复用同一连接
self.app.upgrading = True
self._orig_baud = self.serial_port.baudrate
self._orig_timeout = self.serial_port.timeout
self.serial_port.baudrate = 9600
self.serial_port.timeout = 0.2
threading.Thread(target=self._upgrade_worker, daemon=True).start()
def _send_frame(self, cmd, payload):
if not self.serial_port or not self.serial_port.is_open:
return False
frame = build_upgrade_frame(self.mac, cmd, payload)
self.serial_port.write(frame)
return True
def _read_frame(self, timeout=0.5):
"""读取一帧;自动跳过前置的ASCII调试输出,按CRC校验成功才算完整帧。"""
port = self.serial_port
if not port or not port.is_open:
return None
buf = getattr(self, "_read_buf", b"")
end = time.time() + timeout
while time.time() < end:
chunk = port.read(64)
if chunk:
buf += chunk
while True:
idx = buf.find(bytes([UPGRADE_HEADER]))
if idx < 0:
break
hdr = idx + 1
if len(buf) < hdr + 8: # 需:mac(6)+cmd(1)+len(1)
break
cmd = buf[hdr + 6]
plen = buf[hdr + 7]
total = hdr + 8 + plen + 2 # +payload+check
if len(buf) < total:
break
payload = buf[hdr + 8:hdr + 8 + plen]
check = buf[hdr + 8 + plen:hdr + 8 + plen + 2]
body = buf[idx:hdr + 8 + plen]
got_crc = check[0] | (check[1] << 8)
if got_crc == crc16_modbus(body):
# 设备ASCII调试信息 -> 输出到主界面通讯日志
prefix = buf[0:idx]
if any(0x20 <= b <= 0x7E for b in prefix):
dbg = prefix.decode("utf-8", errors="replace").strip("\x00\r\n ")
if dbg:
self.app.root.after(0, self.app._log, f"[设备] {dbg}", "recv")
self._read_buf = buf[total:]
return cmd, payload
# 误判的"帧头",丢弃继续搜索
buf = buf[idx + 1:]
if not chunk:
time.sleep(0.02)
self._read_buf = buf
return None
def _upgrade_worker(self):
try:
self._log(f"复用串口 {self.port} @9600,8,N,1 升级中...")
# 1. 下发APP信息包 (Cmd 0x81)
payload = struct.pack("<HI", self.package_num & 0xFFFF, self.fw_size)
payload += struct.pack("<I", binascii.crc32(self.fw_data) & 0xFFFFFFFF)
self._log(f"下发APP信息: 包数={self.package_num}, 大小={self.fw_size}字节")
self._send_frame(0x81, payload)
# 传感器会先打印调试信息,随后直接以 0x02 请求数据包;无需单独的 0x01 应答
self._log("等待设备握手后接收数据请求...")
sent = set()
stall = 0
while self.running:
frm = self._read_frame(1.5)
if frm is None:
stall += 1
if stall >= 6:
break
continue
stall = 0
cmd, payload = frm
if cmd == 0x02:
if len(payload) < 4:
continue
idx, total = struct.unpack("<HH", payload[0:4])
if idx >= self.package_num:
self._log("从机请求超出范围,视为完成", "ok")
break
start = idx * self.pkg_size
data = self.fw_data[start:start + self.pkg_size]
dl = len(data)
p = struct.pack("<HHB", idx, self.package_num & 0xFFFF, dl) + data
self._send_frame(0x82, p)
if idx not in sent:
sent.add(idx)
self.app.root.after(0, self._update_progress, len(sent))
elif cmd == 0x01:
self._log("收到从机应答(0x01)", "ok")
else:
self._log(f"收到其他帧 Cmd={cmd:#04x}", "err")
self.app.root.after(0, self._finish, len(sent))
except Exception as e:
self.app.root.after(0, self._error, f"升级出错: {e}")
finally:
self._restore_port()
def _update_progress(self, sent_pkgs):
if self._closed:
return
pct = sent_pkgs * 100.0 / self.package_num if self.package_num else 0
self.progress["value"] = pct
self.prog_var.set(f"{sent_pkgs}/{self.package_num}")
def _finish(self, sent_pkgs):
self.running = False
if sent_pkgs >= self.package_num:
self._log(f"升级完成 (已发送全部 {sent_pkgs} 包)", "ok")
self.prog_var.set("完成")
self.progress["value"] = 100
else:
self._log(f"升级未完成,已发送 {sent_pkgs}/{self.package_num}", "err")
self.start_btn.configure(state=tk.NORMAL)
def _error(self, msg):
self.running = False
self._log(msg, "err")
self.start_btn.configure(state=tk.NORMAL)
def main():
root = tk.Tk()
app = LaserTracingConfigApp(root)
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.