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(Spacer(1, 6*cm))
story.append(Paragraph("边坡变形监测-变形(位移)<br/>自动测量系统配置工具<br/>使用说明书", style_cover_title)) story.append(Paragraph("边坡变形监测-变形(位移)<br/>自动测量系统配置工具<br/>使用说明书", style_cover_title))
story.append(Spacer(1, 1.5*cm)) 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()) story.append(PageBreak())
# 目录 # 目录
@@ -106,12 +106,16 @@ toc_items = [
(False, " 4.2 参数配置区域"), (False, " 4.2 参数配置区域"),
(False, " 4.3 设备参数信息区域"), (False, " 4.3 设备参数信息区域"),
(False, " 4.4 低功耗配置区域"), (False, " 4.4 低功耗配置区域"),
(False, " 4.5 通讯日志区域"), (False, " 4.5 功率与激光配置区域"),
(False, " 4.6 通讯日志区域"),
(True, "5 操作指南"), (True, "5 操作指南"),
(False, " 5.1 连接设备"), (False, " 5.1 连接设备"),
(False, " 5.2 查询设备参数"), (False, " 5.2 查询设备参数"),
(False, " 5.3 修改Lora信道频率"), (False, " 5.3 修改Lora信道频率"),
(False, " 5.4 发送自定义指令"), (False, " 5.4 全功率时段配置"),
(False, " 5.5 激光控制"),
(False, " 5.6 设备升级"),
(False, " 5.7 发送自定义指令"),
(True, "6 指令参考"), (True, "6 指令参考"),
(True, "7 常见问题与故障排除"), (True, "7 常见问题与故障排除"),
] ]
@@ -131,8 +135,11 @@ features = [
"自动识别并列出本机可用串口", "自动识别并列出本机可用串口",
"通过RS485串口连接激光示踪传感器设备", "通过RS485串口连接激光示踪传感器设备",
"一键查询设备全部参数(软件版本、硬件版本、MAC地址、电池电量、注册状态等)", "一键查询设备全部参数(软件版本、硬件版本、MAC地址、电池电量、注册状态等)",
"修改Lora无线通信信道频率(410.1MHz ~ 524.9MHz,步进200kHz", "修改Lora无线通信信道频率(410.1MHz ~ 492.9MHz,步进200kHz",
"查看设备低功耗配置参数(采集间隔、上报时间、紧急上报间隔/时长)", "查看设备低功耗配置参数(采集间隔、上报时间、紧急上报间隔/时长)",
"查看功率与激光参数,并配置全功率工作时间段",
"控制激光开关及设置自动关闭时间",
"通过通用串口升级协议(V1.1)实现设备固件升级",
"支持发送自定义AT指令", "支持发送自定义AT指令",
"通讯日志实时显示,方便调试", "通讯日志实时显示,方便调试",
] ]
@@ -209,16 +216,28 @@ story.append(make_table(
)) ))
story.append(Paragraph("4.2 参数配置区域", style_h2)) 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("Lora 参数:", style_body))
story.append(Paragraph( story.append(Paragraph(
"信道频率(fc):下拉选择Lora通信频率,范围410.1MHz ~ 524.9MHz,步进200kHz(共575个信道)。" "信道频率(fc):下拉选择Lora通信频率,范围410.1MHz ~ 492.9MHz,步进200kHz(共415个信道)。"
'选择后点击"发送"按钮即可修改设备频率。', '选择后点击"发送"按钮即可修改设备频率。',
style_bullet style_bullet
)) ))
story.append(Paragraph("查询当前配置:", style_body)) story.append(Paragraph("全功率时段配置:", style_body))
story.append(Paragraph( 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 style_bullet
)) ))
@@ -254,14 +273,29 @@ story.append(make_table(
col_widths=[5*cm, 11*cm] 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(Paragraph("日志窗口:实时显示串口通讯内容。不同类型信息不同颜色区分:", style_body))
story.append(make_table( story.append(make_table(
["颜色", "含义"], ["颜色", "含义"],
[ [
["蓝色", "发送的指令"], ["蓝色", "发送的指令"],
["绿色", "接收到的"], ["绿色", "接收到的"],
["浅绿色", "系统提示信息"], ["浅绿色", "系统提示信息"],
["红色", "错误信息"], ["红色", "错误信息"],
], ],
@@ -310,7 +344,44 @@ story.append(Paragraph(
style_warning 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 = [ steps = [
"确认串口已成功连接。", "确认串口已成功连接。",
'在通讯日志区域右侧的"自定义指令"输入框中输入AT指令。', '在通讯日志区域右侧的"自定义指令"输入框中输入AT指令。',
@@ -328,7 +399,10 @@ story.append(make_table(
[ [
["para", "查询设备全部参数", "para"], ["para", "查询设备全部参数", "para"],
["lora fc <频率值>", "设置Lora信道频率(单位Hz", "lora fc 433100000"], ["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] col_widths=[5*cm, 6*cm, 5*cm]
)) ))
@@ -365,6 +439,11 @@ qa_items = [
"1. 点击更多信息;\n" "1. 点击更多信息;\n"
"2. 点击仍要运行按钮;\n" "2. 点击仍要运行按钮;\n"
"3. 此提示是因为exe未做代码签名,软件本身安全。"), "3. 此提示是因为exe未做代码签名,软件本身安全。"),
("点击设备升级后无握手/升级失败",
"1. 确认设备MAC地址正确;\n"
"2. 确认固件文件为正确的.bin文件;\n"
"3. 确认串口连接正常、设备供电稳定;\n"
"4. 查看通讯日志中的设备调试信息辅助排查。"),
] ]
for q, a in qa_items: for q, a in qa_items:
story.append(Paragraph("Q: " + q, style_q)) story.append(Paragraph("Q: " + q, style_q))
+325 -15
View File
@@ -4,7 +4,9 @@
""" """
import tkinter as tk 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
import serial.tools.list_ports import serial.tools.list_ports
import threading import threading
@@ -12,6 +14,8 @@ import time
import re import re
import sys import sys
import os import os
import shutil
import subprocess
from datetime import datetime from datetime import datetime
@@ -25,7 +29,6 @@ class LaserTracingConfigApp:
def __init__(self, root): def __init__(self, root):
self.root = root self.root = root
self.root.title("边坡变形监测-变形(位移)自动测量系统配置工具") self.root.title("边坡变形监测-变形(位移)自动测量系统配置工具")
self.root.geometry("900x700")
self.root.resizable(True, True) self.root.resizable(True, True)
icon_path = resource_path("传感器.ico") icon_path = resource_path("传感器.ico")
@@ -35,12 +38,17 @@ class LaserTracingConfigApp:
self.serial_port = None self.serial_port = None
self.serial_thread = None self.serial_thread = None
self.running = False self.running = False
self.upgrading = False
self.recv_buffer = "" self.recv_buffer = ""
self._port_details = {} self._port_details = {}
self._build_ui() self._build_ui()
self._refresh_ports() self._refresh_ports()
# 根据内容自动调整窗口大小,刚好完整显示参数
self.root.update_idletasks()
self.root.geometry("")
# ─────────────────── UI构建 ─────────────────── # ─────────────────── UI构建 ───────────────────
def _build_ui(self): 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) ttk.Label(conn_frame, text="波特率:").grid(row=0, column=3, sticky=tk.W, padx=2)
self.baudrate_var = tk.StringVar(value="9600") 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) ttk.Label(conn_frame, text="数据位:").grid(row=0, column=5, sticky=tk.W, padx=2)
self.databits_var = tk.StringVar(value="8") self.databits_var = tk.StringVar(value="8")
@@ -85,25 +95,27 @@ class LaserTracingConfigApp:
self._fc_display_to_value = {} self._fc_display_to_value = {}
self._fc_value_to_display = {} self._fc_value_to_display = {}
fc_displays = [] fc_displays = []
freq = 410100000 freq = 470100000
ch = 1 ch = 0
while freq < 525000000: while freq <= 492600000:
display = f"信道{ch}: {freq / 1000000:.1f}MHz" display = f"信道{ch}: {freq / 1000000:.1f}MHz"
fc_displays.append(display) fc_displays.append(display)
self._fc_display_to_value[display] = str(freq) self._fc_display_to_value[display] = str(freq)
self._fc_value_to_display[str(freq)] = display self._fc_value_to_display[str(freq)] = display
freq += 200000 freq += 500000
ch += 1 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, self.lora_fc_combo = ttk.Combobox(lora_frame, textvariable=self.lora_fc_display_var,
values=fc_displays, width=20, state="readonly") values=fc_displays, width=20, state="readonly")
self.lora_fc_combo.grid(row=0, column=1, padx=2) 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.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 = ttk.Frame(config_frame)
query_frame.pack(fill=tk.X, pady=4) 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="查询当前配置 (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命令验证是否成功", ttk.Label(query_frame, text="配置后可通过para命令验证是否成功",
foreground="gray").pack(side=tk.LEFT, padx=8) foreground="gray").pack(side=tk.LEFT, padx=8)
@@ -242,6 +254,9 @@ class LaserTracingConfigApp:
except serial.SerialException as e: except serial.SerialException as e:
messagebox.showerror("连接失败", str(e)) messagebox.showerror("连接失败", str(e))
self._log(f"连接失败: {e}", "error") self._log(f"连接失败: {e}", "error")
except ValueError:
messagebox.showwarning("参数错误", "波特率必须为数字")
self._log("波特率必须为数字", "error")
def _disconnect(self): def _disconnect(self):
self.running = False self.running = False
@@ -257,6 +272,9 @@ class LaserTracingConfigApp:
def _recv_loop(self): def _recv_loop(self):
while self.running: while self.running:
try: try:
if self.upgrading:
time.sleep(0.02)
continue
if self.serial_port and self.serial_port.is_open: if self.serial_port and self.serial_port.is_open:
n = self.serial_port.in_waiting n = self.serial_port.in_waiting
if n > 0: if n > 0:
@@ -275,8 +293,9 @@ class LaserTracingConfigApp:
def _on_data_received(self, text): def _on_data_received(self, text):
self._log(text, "recv", newline=False) self._log(text, "recv", newline=False)
# 检查是否收到完整para回应(以***结尾) # 收到完整para回应:起始与结束各出现一次分隔符(****)后才解析
if "**********************************************************" in self.recv_buffer: marker = "**********************************************************"
if self.recv_buffer.count(marker) >= 2:
self._parse_para_response(self.recv_buffer) self._parse_para_response(self.recv_buffer)
self.recv_buffer = "" self.recv_buffer = ""
@@ -302,6 +321,13 @@ class LaserTracingConfigApp:
def _send_para(self): def _send_para(self):
self._send_command("para") 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): def _send_lora_config(self):
display = self.lora_fc_display_var.get().strip() display = self.lora_fc_display_var.get().strip()
fc = self._fc_display_to_value.get(display) 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") lines = text.replace("\r\n", "\n").replace("\r", "\n").split("\n")
key_value_patterns = [ key_value_patterns = [
(r"[Ss]oft[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"[Hh]ard[Ww]are\s*[Vv]:?\s*(\d+(?:\.\d+)*)", "硬件版本"),
(r"Local MAC:\s*(.*)", "本机MAC"), (r"Local MAC:\s*(.*)", "本机MAC"),
(r"Battery:\s*(.*)", "电池电量"), (r"Battery:\s*(.*)", "电池电量"),
(r"LocalType:\s*(.*)", "设备类型"), (r"LocalType:\s*(.*)", "设备类型"),
@@ -339,6 +365,9 @@ class LaserTracingConfigApp:
# 更新参数显示 # 更新参数显示
for key in self._para_order: for key in self._para_order:
value = found.get(key, "-") 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: if key in self._para_labels:
self._para_labels[key].configure(text=value) self._para_labels[key].configure(text=value)
@@ -396,16 +425,297 @@ class LaserTracingConfigApp:
self.log_text.configure(state=tk.DISABLED) self.log_text.configure(state=tk.DISABLED)
def _log(self, message, tag="info", newline=True): def _log(self, message, tag="info", newline=True):
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
self.log_text.configure(state=tk.NORMAL) self.log_text.configure(state=tk.NORMAL)
if newline: if newline:
self.log_text.insert(tk.END, f"[{timestamp}] {message}\n", tag) self.log_text.insert(tk.END, f"{message}\n", tag)
else: 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.see(tk.END)
self.log_text.configure(state=tk.DISABLED) 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(): def main():
root = tk.Tk() root = tk.Tk()
app = LaserTracingConfigApp(root) app = LaserTracingConfigApp(root)
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.