Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 31612e5b95 | |||
| 4a6ff717c4 | |||
| 1d0b4b2fd1 | |||
| 67bd8ebf91 | |||
| 36baabeb5f | |||
| afa83653a3 | |||
| 03046499ee | |||
| 629a92ff41 | |||
| 2a84701e42 |
+141
-69
@@ -1,5 +1,10 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""生成边坡变形监测配置工具使用说明书(PDF格式)"""
|
# 生成配置工具使用说明书(PDF格式)
|
||||||
|
# 用法: python generate_manual.py narrow -> 生成窄带版说明书
|
||||||
|
# python generate_manual.py wide -> 生成宽带版说明书
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
from reportlab.lib.pagesizes import A4
|
from reportlab.lib.pagesizes import A4
|
||||||
from reportlab.lib.units import cm, mm
|
from reportlab.lib.units import cm, mm
|
||||||
from reportlab.lib.colors import HexColor, black, white
|
from reportlab.lib.colors import HexColor, black, white
|
||||||
@@ -11,7 +16,25 @@ from reportlab.platypus import (
|
|||||||
)
|
)
|
||||||
from reportlab.pdfbase import pdfmetrics
|
from reportlab.pdfbase import pdfmetrics
|
||||||
from reportlab.pdfbase.ttfonts import TTFont
|
from reportlab.pdfbase.ttfonts import TTFont
|
||||||
import os
|
|
||||||
|
# ── 版本与型号配置 ──
|
||||||
|
SOFTWARE_VERSION = "V1.0.0"
|
||||||
|
|
||||||
|
arg = sys.argv[1].lower() if len(sys.argv) > 1 else "narrow"
|
||||||
|
if arg in ("wide", "broadband", "宽带"):
|
||||||
|
EDITION = "宽带版"
|
||||||
|
FC_RANGE = "433.1MHz ~ 492.6MHz"
|
||||||
|
FC_CHANNELS = 120
|
||||||
|
FC_EXAMPLE = "433100000"
|
||||||
|
else:
|
||||||
|
EDITION = "窄带版"
|
||||||
|
FC_RANGE = "470.1MHz ~ 492.6MHz"
|
||||||
|
FC_CHANNELS = 46
|
||||||
|
FC_EXAMPLE = "470100000"
|
||||||
|
FC_STEP = "500kHz"
|
||||||
|
|
||||||
|
EXE_NAME = f"边坡变形监测-变形(位移)自动测量系统配置工具({EDITION} {SOFTWARE_VERSION}).exe"
|
||||||
|
PDF_NAME = f"边坡变形监测-变形(位移)自动测量系统配置工具使用说明书({EDITION} {SOFTWARE_VERSION}).pdf"
|
||||||
|
|
||||||
# 注册中文字体
|
# 注册中文字体
|
||||||
font_dir = os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts")
|
font_dir = os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts")
|
||||||
@@ -42,20 +65,20 @@ style_step = ParagraphStyle("Step", parent=style_body, leftIndent=20)
|
|||||||
style_warning = ParagraphStyle("Warning", parent=style_body, textColor=C_RED, fontName="SimHei")
|
style_warning = ParagraphStyle("Warning", parent=style_body, textColor=C_RED, fontName="SimHei")
|
||||||
style_q = ParagraphStyle("Q", fontName="SimHei", fontSize=11, leading=20, spaceBefore=10, spaceAfter=2)
|
style_q = ParagraphStyle("Q", fontName="SimHei", fontSize=11, leading=20, spaceBefore=10, spaceAfter=2)
|
||||||
style_a = ParagraphStyle("A", fontName="SimSun", fontSize=11, leading=20, leftIndent=20, spaceAfter=6)
|
style_a = ParagraphStyle("A", fontName="SimSun", fontSize=11, leading=20, leftIndent=20, spaceAfter=6)
|
||||||
|
style_th = ParagraphStyle("TH", fontName="SimHei", fontSize=11, leading=15, textColor=white)
|
||||||
|
style_td = ParagraphStyle("TD", fontName="SimSun", fontSize=10, leading=14)
|
||||||
|
|
||||||
def make_table(headers, rows, col_widths=None):
|
def make_table(headers, rows, col_widths=None):
|
||||||
data = [headers] + rows
|
# 单元格统一用 Paragraph 包装,超宽内容自动换行(字符串单元格会被截断)
|
||||||
|
from xml.sax.saxutils import escape as _esc
|
||||||
|
data = [[Paragraph(_esc(str(headers[0])), style_th)] + [Paragraph(_esc(str(c)), style_td) for c in headers[1:]]]
|
||||||
|
for r in rows:
|
||||||
|
data.append([Paragraph(_esc(str(c)), style_td) for c in r])
|
||||||
if col_widths is None:
|
if col_widths is None:
|
||||||
col_widths = [doc.width / len(headers)] * len(headers)
|
col_widths = [doc.width / len(headers)] * len(headers)
|
||||||
t = Table(data, colWidths=col_widths, repeatRows=1)
|
t = Table(data, colWidths=col_widths, repeatRows=1)
|
||||||
style_cmds = [
|
style_cmds = [
|
||||||
("BACKGROUND", (0, 0), (-1, 0), C_HEADER_BG),
|
("BACKGROUND", (0, 0), (-1, 0), C_HEADER_BG),
|
||||||
("TEXTCOLOR", (0, 0), (-1, 0), C_HEADER_FG),
|
|
||||||
("FONTNAME", (0, 0), (-1, 0), "SimHei"),
|
|
||||||
("FONTSIZE", (0, 0), (-1, 0), 11),
|
|
||||||
("FONTNAME", (0, 1), (-1, -1), "SimSun"),
|
|
||||||
("FONTSIZE", (0, 1), (-1, -1), 10),
|
|
||||||
("ALIGN", (0, 0), (-1, -1), "LEFT"),
|
|
||||||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||||
("GRID", (0, 0), (-1, -1), 0.5, HexColor("#CCCCCC")),
|
("GRID", (0, 0), (-1, -1), 0.5, HexColor("#CCCCCC")),
|
||||||
("TOPPADDING", (0, 0), (-1, -1), 4),
|
("TOPPADDING", (0, 0), (-1, -1), 4),
|
||||||
@@ -75,10 +98,7 @@ def step(num, text):
|
|||||||
return Paragraph(str(num) + ". " + text, style_step)
|
return Paragraph(str(num) + ". " + text, style_step)
|
||||||
|
|
||||||
# 构建文档
|
# 构建文档
|
||||||
output_path = os.path.join(
|
output_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), PDF_NAME)
|
||||||
os.path.dirname(os.path.abspath(__file__)),
|
|
||||||
"边坡变形监测配置工具使用说明书.pdf"
|
|
||||||
)
|
|
||||||
doc = SimpleDocTemplate(output_path, pagesize=A4,
|
doc = SimpleDocTemplate(output_path, pagesize=A4,
|
||||||
leftMargin=2.5*cm, rightMargin=2.5*cm,
|
leftMargin=2.5*cm, rightMargin=2.5*cm,
|
||||||
topMargin=2.5*cm, bottomMargin=2.5*cm)
|
topMargin=2.5*cm, bottomMargin=2.5*cm)
|
||||||
@@ -87,9 +107,9 @@ 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/>自动测量系统配置工具(" + EDITION + ")<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("版本 " + SOFTWARE_VERSION, style_cover_sub))
|
||||||
story.append(PageBreak())
|
story.append(PageBreak())
|
||||||
|
|
||||||
# 目录
|
# 目录
|
||||||
@@ -99,7 +119,7 @@ toc_items = [
|
|||||||
(True, "2 运行环境"),
|
(True, "2 运行环境"),
|
||||||
(True, "3 软件安装与启动"),
|
(True, "3 软件安装与启动"),
|
||||||
(False, " 3.1 安装"),
|
(False, " 3.1 安装"),
|
||||||
(False, " 3.2 ��动"),
|
(False, " 3.2 启动"),
|
||||||
(False, " 3.3 USB转串口驱动安装"),
|
(False, " 3.3 USB转串口驱动安装"),
|
||||||
(True, "4 软件界面说明"),
|
(True, "4 软件界面说明"),
|
||||||
(False, " 4.1 串口连接区域"),
|
(False, " 4.1 串口连接区域"),
|
||||||
@@ -111,7 +131,8 @@ toc_items = [
|
|||||||
(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 发送自定义指令"),
|
||||||
(True, "6 指令参考"),
|
(True, "6 指令参考"),
|
||||||
(True, "7 常见问题与故障排除"),
|
(True, "7 常见问题与故障排除"),
|
||||||
]
|
]
|
||||||
@@ -122,7 +143,7 @@ story.append(PageBreak())
|
|||||||
# 1 软件概述
|
# 1 软件概述
|
||||||
story.append(Paragraph("1 软件概述", style_h1))
|
story.append(Paragraph("1 软件概述", style_h1))
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
"边坡变形监测-变形(位移)自动测量系统配置工具是一款用于激光示踪传感器设备的网关参数配置软件。"
|
"边坡变形监测-变形(位移)自动测量系统配置工具(" + EDITION + ")是一款用于激光示踪传感器设备的网关参数配置软件。"
|
||||||
"软件通过RS485串口与传感器设备进行通信,支持查询设备参数、修改Lora无线通信频率、发送自定义指令等操作。",
|
"软件通过RS485串口与传感器设备进行通信,支持查询设备参数、修改Lora无线通信频率、发送自定义指令等操作。",
|
||||||
style_body
|
style_body
|
||||||
))
|
))
|
||||||
@@ -131,8 +152,11 @@ features = [
|
|||||||
"自动识别并列出本机可用串口",
|
"自动识别并列出本机可用串口",
|
||||||
"通过RS485串口连接激光示踪传感器设备",
|
"通过RS485串口连接激光示踪传感器设备",
|
||||||
"一键查询设备全部参数(软件版本、硬件版本、MAC地址、电池电量、注册状态等)",
|
"一键查询设备全部参数(软件版本、硬件版本、MAC地址、电池电量、注册状态等)",
|
||||||
"修改Lora无线通信信道频率(410.1MHz ~ 524.9MHz,步进200kHz)",
|
"修改Lora无线通信信道频率(" + FC_RANGE + ",步进" + FC_STEP + ",共" + str(FC_CHANNELS) + "个信道,信道0起始)",
|
||||||
|
"波特率支持修改(内置2400~115200常用波特率,也可手动输入)",
|
||||||
"查看设备低功耗配置参数(采集间隔、上报时间、紧急上报间隔/时长)",
|
"查看设备低功耗配置参数(采集间隔、上报时间、紧急上报间隔/时长)",
|
||||||
|
"注册状态自动转换为\"已注册/未注册\"显示",
|
||||||
|
"通过通用串口升级协议(V1.1)实现设备固件升级,支持进度显示与断口复用",
|
||||||
"支持发送自定义AT指令",
|
"支持发送自定义AT指令",
|
||||||
"通讯日志实时显示,方便调试",
|
"通讯日志实时显示,方便调试",
|
||||||
]
|
]
|
||||||
@@ -158,17 +182,17 @@ story.append(Paragraph("3 软件安装与启动", style_h1))
|
|||||||
|
|
||||||
story.append(Paragraph("3.1 安装", style_h2))
|
story.append(Paragraph("3.1 安装", style_h2))
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
'本软件为绿色免安装版本,无需安装过程。将"边坡变形监测-变形(位移)自动测量系统配置工具.exe"文件复制到电脑任意目录即可使用。',
|
"本软件为绿色免安装版本,无需安装过程。将\"" + EXE_NAME + "\"文件复制到电脑任意目录即可使用。",
|
||||||
style_body
|
style_body
|
||||||
))
|
))
|
||||||
|
|
||||||
story.append(Paragraph("3.2 启动", style_h2))
|
story.append(Paragraph("3.2 启动", style_h2))
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
'双击"边坡变形监测-变形(位移)自动测量系统配置工具.exe"文件即可启动软件。',
|
"双击\"" + EXE_NAME + "\"文件即可启动软件。窗口标题栏显示软件型号(" + EDITION + ")与版本号(" + SOFTWARE_VERSION + ")。",
|
||||||
style_body
|
style_body
|
||||||
))
|
))
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
'注意:如果Windows弹出"Windows已保护你的电脑"提示,请点击"更多信息"->"仍要运行"。',
|
"注意:如果Windows弹出\"Windows已保护你的电脑\"提示,请点击\"更多信息\"->\"仍要运行\"。",
|
||||||
style_body
|
style_body
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -185,7 +209,7 @@ story.append(make_table(
|
|||||||
))
|
))
|
||||||
story.append(Spacer(1, 6))
|
story.append(Spacer(1, 6))
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
'驱动安装成功后,将USB转RS485线插入电脑,在"设备管理器->端口(COM和LPT)"中应能看到对应的COM口编号。',
|
"驱动安装成功后,将USB转RS485线插入电脑,在\"设备管理器->端口(COM和LPT)\"中应能看到对应的COM口编号。",
|
||||||
style_body
|
style_body
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -198,27 +222,29 @@ story.append(Paragraph("位于界面顶部,用于选择和配置串口连接
|
|||||||
story.append(make_table(
|
story.append(make_table(
|
||||||
["项目", "说明", "默认值"],
|
["项目", "说明", "默认值"],
|
||||||
[
|
[
|
||||||
["串口选择", '下拉框选择可用串口,点击"刷新"按钮可重新扫描', "自动选择第一个"],
|
["串口选择", "下拉框选择可用串口(显示端口描述,同名端口自动追加COM号区分),点击\"刷新\"按钮可重新扫描", "自动选择第一个"],
|
||||||
["波特率", "串口通信速率", "9600 bps"],
|
["波特率", "串口通信速率,可编辑下拉框,内置2400/4800/9600/19200/38400/57600/115200,也可手动输入", "9600 bps"],
|
||||||
["数据位", "每帧数据位数", "8"],
|
["数据位", "固定为8,与设备通信协议一致(界面仅显示,不可修改)", "8"],
|
||||||
["停止位", "停止位位数", "1"],
|
["停止位", "固定为1,与设备通信协议一致(界面仅显示,不可修改)", "1"],
|
||||||
["校验", "奇偶校验方式", "None(无校验)"],
|
["校验", "固定为无校验,与设备通信协议一致(界面仅显示,不可修改)", "None(无校验)"],
|
||||||
["打开/关闭串口", "按钮切换串口连接状态", "—"],
|
["打开/关闭串口", "按钮切换串口连接状态;连接成功后串口下拉框锁定,关闭后恢复可选", "—"],
|
||||||
],
|
],
|
||||||
col_widths=[3.5*cm, 8.5*cm, 4*cm]
|
col_widths=[3.5*cm, 8.5*cm, 4*cm]
|
||||||
))
|
))
|
||||||
|
|
||||||
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通信频率,范围" + FC_RANGE + ",步进" + FC_STEP
|
||||||
'选择后点击"发送"按钮即可修改设备频率。',
|
+ "(共" + str(FC_CHANNELS) + "个信道,信道0起始,即信道0对应" + FC_RANGE.split(" ~ ")[0] + ")。"
|
||||||
|
"选择后点击\"发送\"按钮即可修改设备频率。",
|
||||||
style_bullet
|
style_bullet
|
||||||
))
|
))
|
||||||
story.append(Paragraph("查询当前配置:", style_body))
|
story.append(Paragraph("查询与升级按钮:", style_body))
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
'点击"查询当前配置(para)"按钮,向设备发送para指令,查询设备全部参数。查询结果将自动解析并显示在下方的参数信息区域。',
|
"点击\"查询当前配置(para)\"按钮,软件向设备发送para指令查询全部参数。"
|
||||||
|
"查询结果将自动解析并显示在下方参数信息区域。点击\"设备升级\"按钮可打开设备升级对话框。",
|
||||||
style_bullet
|
style_bullet
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -235,40 +261,52 @@ story.append(make_table(
|
|||||||
["通信方式", "设备当前通信方式(CommInf)"],
|
["通信方式", "设备当前通信方式(CommInf)"],
|
||||||
["设备标识", "设备唯一标识(DeviceSign)"],
|
["设备标识", "设备唯一标识(DeviceSign)"],
|
||||||
["网关MAC", "所连接网关的MAC地址"],
|
["网关MAC", "所连接网关的MAC地址"],
|
||||||
["注册状态", "设备在网关上的注册状态"],
|
["注册状态", "设备在网关上的注册状态,自动显示为\"已注册\"或\"未注册\""],
|
||||||
["Lora频率", "当前Lora通信频率"],
|
["Lora频率", "当前Lora通信频率"],
|
||||||
],
|
],
|
||||||
col_widths=[4*cm, 12*cm]
|
col_widths=[4*cm, 12*cm]
|
||||||
))
|
))
|
||||||
|
|
||||||
story.append(Paragraph("4.4 低功耗配置区域", style_h2))
|
story.append(Paragraph("4.4 低功耗配置区域", style_h2))
|
||||||
story.append(Paragraph("显示设备的低功耗运行参数:", style_body))
|
story.append(Paragraph("显示设备的低功耗运行参数(由para指令返回的LpCfg字段解析而来):", style_body))
|
||||||
story.append(make_table(
|
story.append(make_table(
|
||||||
["参数名称", "说明"],
|
["参数名称", "说明", "取值范围"],
|
||||||
[
|
[
|
||||||
["CI(采集间隔)", "传感器数据采集的时间间隔,单位:秒"],
|
["CI(采集间隔)", "传感器数据采集的时间间隔", "30~7200秒"],
|
||||||
["RI(上报时间)", "数据上报到网关的时间间隔,单位:分钟/时"],
|
["RI(上报时间)", "数据上报到网关的时间间隔", "5~720分钟"],
|
||||||
["EI(紧急上报间隔)", "紧急状态下数据上报的时间间隔,单位:分钟/时"],
|
["EI(紧急上报间隔)", "紧急状态下数据上报的时间间隔", "1~5分钟"],
|
||||||
["ET(紧急上报时长)", "紧急上报模式持续时间,单位:分钟/时"],
|
["ET(紧急上报时长)", "紧急上报模式持续时间", "10~120分钟"],
|
||||||
],
|
],
|
||||||
col_widths=[5*cm, 11*cm]
|
col_widths=[4.5*cm, 7.5*cm, 4*cm]
|
||||||
|
))
|
||||||
|
story.append(Spacer(1, 6))
|
||||||
|
story.append(Paragraph(
|
||||||
|
"显示格式说明:CI以\"秒\"显示;RI/EI/ET以\"分钟\"显示,当数值≥60分钟时自动换算为\"X时X分钟\"格式(例如90分钟显示为\"1时30分钟\")。",
|
||||||
|
style_body
|
||||||
))
|
))
|
||||||
|
|
||||||
story.append(Paragraph("4.5 通讯日志区域", style_h2))
|
story.append(Paragraph("4.5 通讯日志区域", 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(
|
||||||
["颜色", "含义"],
|
["颜色", "含义"],
|
||||||
[
|
[
|
||||||
["蓝色", "发送的指令"],
|
["蓝色", "发送的指令"],
|
||||||
["绿色", "接收到的��据"],
|
["绿色", "接收到的数据"],
|
||||||
["浅绿色", "系统提示信息"],
|
["浅绿色", "系统提示信息"],
|
||||||
["红色", "错误信息"],
|
["红色", "错误信息"],
|
||||||
],
|
],
|
||||||
col_widths=[4*cm, 12*cm]
|
col_widths=[4*cm, 12*cm]
|
||||||
))
|
))
|
||||||
story.append(Spacer(1, 6))
|
story.append(Spacer(1, 6))
|
||||||
story.append(Paragraph('自定义指令:右侧面板可输入自定义AT指令,点击"发送"或按Enter键发送。', style_body))
|
story.append(Paragraph(
|
||||||
|
"清空日志:点击日志窗口下方的\"清空日志\"按钮可清除当前全部日志内容,便于新一轮操作的观察。",
|
||||||
|
style_body
|
||||||
|
))
|
||||||
|
story.append(Paragraph("自定义指令:右侧面板可输入自定义指令,点击\"发送\"或按Enter键发送,发送成功后输入框自动清空。", style_body))
|
||||||
|
|
||||||
# 5 操作指南
|
# 5 操作指南
|
||||||
story.append(Paragraph("5 操作指南", style_h1))
|
story.append(Paragraph("5 操作指南", style_h1))
|
||||||
@@ -277,18 +315,18 @@ story.append(Paragraph("5.1 连接设备", style_h2))
|
|||||||
steps = [
|
steps = [
|
||||||
"将USB转RS485串口线连接到电脑USB接口。",
|
"将USB转RS485串口线连接到电脑USB接口。",
|
||||||
"将RS485线的A、B端分别连接到传感器设备的RS485接口(注意A接A、B接B)。",
|
"将RS485线的A、B端分别连接到传感器设备的RS485接口(注意A接A、B接B)。",
|
||||||
'启动软件,在"串口连接"区域点击"刷新"按钮扫描可用串口。',
|
"启动软件,在\"串口连接\"区域点击\"刷新\"按钮扫描可用串口。",
|
||||||
"在下拉框中选择对应的COM口。",
|
"在下拉框中选择对应的COM口。",
|
||||||
"确认波特率等参数与设备一致(默认9600, 8N1)。",
|
"确认波特率等参数与设备一致(默认9600, 8N1,波特率可直接修改)。",
|
||||||
'点击"打开串口"按钮,按钮文字变为"关闭串口"即表示连接成功。通讯日志中将显示连接信息。',
|
"点击\"打开串口\"按钮,按钮文字变为\"关闭串口\"即表示连接成功。通讯日志中将显示连接信息。",
|
||||||
]
|
]
|
||||||
for i, s in enumerate(steps, 1):
|
for i, s in enumerate(steps, 1):
|
||||||
story.append(step(i, s))
|
story.append(step(i, s))
|
||||||
|
|
||||||
story.append(Paragraph("5.2 查询设备参数", style_h2))
|
story.append(Paragraph("5.2 查询设备参数", style_h2))
|
||||||
steps = [
|
steps = [
|
||||||
'确认串口已成功连接(按钮显示"关闭串口")。',
|
"确认串口已成功连接(按钮显示\"关闭串口\")。",
|
||||||
'点击"查询当前配置(para)"按钮。',
|
"点击\"查询当前配置(para)\"按钮。",
|
||||||
"软件将发送para指令,设备返回参数信息后自动解析。",
|
"软件将发送para指令,设备返回参数信息后自动解析。",
|
||||||
"设备参数信息和低功耗配置区域将显示查询到的数值。",
|
"设备参数信息和低功耗配置区域将显示查询到的数值。",
|
||||||
"Lora频率下拉框也会同步更新为当前设备频率。",
|
"Lora频率下拉框也会同步更新为当前设备频率。",
|
||||||
@@ -299,9 +337,9 @@ for i, s in enumerate(steps, 1):
|
|||||||
story.append(Paragraph("5.3 修改Lora信道频率", style_h2))
|
story.append(Paragraph("5.3 修改Lora信道频率", style_h2))
|
||||||
steps = [
|
steps = [
|
||||||
"确认串口已成功连接。",
|
"确认串口已成功连接。",
|
||||||
'在"Lora 参数"区域的下拉框中选择目标信道频率。',
|
"在\"Lora 参数\"区域的下拉框中选择目标信道频率。",
|
||||||
'点击"发送"按钮,软件将发送"lora fc <频率值>"指令。',
|
"点击\"发送\"按钮,软件将发送\"lora fc <频率值>\"指令。",
|
||||||
'发送后建议点击"查询当前配置"验证修改是否成功。',
|
"发送后建议点击\"查询当前配置\"验证修改是否成功。",
|
||||||
]
|
]
|
||||||
for i, s in enumerate(steps, 1):
|
for i, s in enumerate(steps, 1):
|
||||||
story.append(step(i, s))
|
story.append(step(i, s))
|
||||||
@@ -310,11 +348,33 @@ story.append(Paragraph(
|
|||||||
style_warning
|
style_warning
|
||||||
))
|
))
|
||||||
|
|
||||||
story.append(Paragraph("5.4 发送自定义指令", style_h2))
|
story.append(Paragraph("5.4 设备升级", style_h2))
|
||||||
|
story.append(Paragraph("设备升级用于通过通用串口升级协议(V1.1)对设备固件进行升级。升级期间请保持设备供电正常。", style_body))
|
||||||
steps = [
|
steps = [
|
||||||
"确认串口已成功连接。",
|
"确认串口已成功连接。",
|
||||||
'在通讯日志区域右侧的"自定义指令"输入框中输入AT指令。',
|
"点击\"参数配置\"区域中的\"设备升级\"按钮,打开设备升级对话框。",
|
||||||
'点击"发送"按钮或按Enter键发送。',
|
"在对话框中点击\"浏览\"选择要更新的固件文件(.bin),并确认设备MAC地址(默认自动填充para查询到的本机MAC;格式为6字节横线分隔,例如 09-00-01-7F-FF-FF)。",
|
||||||
|
"点击\"开始升级\"按钮,软件复用当前串口连接并自动切换至9600bps升级波特率,向设备下发固件信息(包数、大小、CRC32校验值)。",
|
||||||
|
"设备校验通过后逐包请求数据,软件按请求逐包下发固件;进度条显示\"n/m 包\"传输进度,升级日志显示握手/下发/完成等过程信息。",
|
||||||
|
"升级完成后串口自动恢复原波特率,可在同一对话框再次点击\"开始升级\"重复升级,无需关闭重开。",
|
||||||
|
]
|
||||||
|
for i, s in enumerate(steps, 1):
|
||||||
|
story.append(step(i, s))
|
||||||
|
story.append(Spacer(1, 6))
|
||||||
|
story.append(Paragraph(
|
||||||
|
"注意:升级过程中请勿断开串口或断电,否则可能导致设备无法正常启动。若设备长时间无响应(约10秒),升级将自动停止并提示未完成,可检查接线后重试。",
|
||||||
|
style_warning
|
||||||
|
))
|
||||||
|
story.append(Paragraph(
|
||||||
|
"提示:升级期间设备输出的调试信息会以\"[设备]\"前缀转发到主界面通讯日志,可用于排查升级失败原因。",
|
||||||
|
style_body
|
||||||
|
))
|
||||||
|
|
||||||
|
story.append(Paragraph("5.5 发送自定义指令", style_h2))
|
||||||
|
steps = [
|
||||||
|
"确认串口已成功连接。",
|
||||||
|
"在通讯日志区域右侧的\"自定义指令\"输入框中输入AT指令。",
|
||||||
|
"点击\"发送\"按钮或按Enter键发送,发送成功后输入框自动清空。",
|
||||||
"通讯日志中将显示发送的指令和设备的回应。",
|
"通讯日志中将显示发送的指令和设备的回应。",
|
||||||
]
|
]
|
||||||
for i, s in enumerate(steps, 1):
|
for i, s in enumerate(steps, 1):
|
||||||
@@ -322,18 +382,20 @@ for i, s in enumerate(steps, 1):
|
|||||||
|
|
||||||
# 6 指令参考
|
# 6 指令参考
|
||||||
story.append(Paragraph("6 指令参考", style_h1))
|
story.append(Paragraph("6 指令参考", style_h1))
|
||||||
story.append(Paragraph("软件内置以下AT指令:", style_body))
|
story.append(Paragraph("软件界面涉及的指令如下:", style_body))
|
||||||
story.append(make_table(
|
story.append(make_table(
|
||||||
["指令", "功能", "示例"],
|
["指令", "功能", "对应操作"],
|
||||||
[
|
[
|
||||||
["para", "查询设备全部参数", "para"],
|
["para", "查询设备全部参数", "点击\"查询当前配置(para)\"按钮"],
|
||||||
["lora fc <频率值>", "设置Lora信道频率(单位Hz)", "lora fc 433100000"],
|
["lora fc <频率值>", "设置Lora信道频率(单位Hz)", "在\"Lora 参数\"下拉框中选择频率后点击\"发送\",范围" + FC_RANGE + ";示例:lora fc " + FC_EXAMPLE],
|
||||||
["(自定义指令)", "在自定义指令输入框中输入任意AT指令", "—"],
|
|
||||||
],
|
],
|
||||||
col_widths=[5*cm, 6*cm, 5*cm]
|
col_widths=[4.5*cm, 5.5*cm, 6*cm]
|
||||||
))
|
))
|
||||||
story.append(Spacer(1, 6))
|
story.append(Spacer(1, 6))
|
||||||
story.append(Paragraph("说明:指令以回车换行(\\r\\n)结尾自动发送,无需手动添加。", style_body))
|
story.append(Paragraph(
|
||||||
|
"说明:指令以回车换行(\\r\\n)结尾自动发送,无需手动添加。此外,可在自定义指令框中手动输入设备支持的其他指令,发送\"<指令> ?\"可查看该指令的在线帮助。",
|
||||||
|
style_body
|
||||||
|
))
|
||||||
|
|
||||||
# 7 常见问题
|
# 7 常见问题
|
||||||
story.append(Paragraph("7 常见问题与故障排除", style_h1))
|
story.append(Paragraph("7 常见问题与故障排除", style_h1))
|
||||||
@@ -343,8 +405,8 @@ qa_items = [
|
|||||||
"1. 检查USB转RS485线是否已插入电脑;\n"
|
"1. 检查USB转RS485线是否已插入电脑;\n"
|
||||||
"2. 检查设备管理器中是否出现对应的COM口;\n"
|
"2. 检查设备管理器中是否出现对应的COM口;\n"
|
||||||
"3. 确认USB转串口驱动已正确安装;\n"
|
"3. 确认USB转串口驱动已正确安装;\n"
|
||||||
'4. 点击"刷新"按钮重新扫描。'),
|
"4. 点击\"刷新\"按钮重新扫描。"),
|
||||||
('点击"打开串口"后提示连接失败',
|
("点击\"打开串口\"后提示连接失败",
|
||||||
"1. 确认该COM口未被其他软件占用(如串口助手、Keil等);\n"
|
"1. 确认该COM口未被其他软件占用(如串口助手、Keil等);\n"
|
||||||
"2. 尝试拔插USB线后重试;\n"
|
"2. 尝试拔插USB线后重试;\n"
|
||||||
"3. 尝试更换USB接口。"),
|
"3. 尝试更换USB接口。"),
|
||||||
@@ -357,14 +419,24 @@ qa_items = [
|
|||||||
"1. 确认传感器和网关使用相同的Lora信道频率;\n"
|
"1. 确认传感器和网关使用相同的Lora信道频率;\n"
|
||||||
"2. 修改频率后需等待设备重新连接;\n"
|
"2. 修改频率后需等待设备重新连接;\n"
|
||||||
"3. 可通过para命令验证当前频率设置。"),
|
"3. 可通过para命令验证当前频率设置。"),
|
||||||
('参数信息区域显示 "-"',
|
("参数信息区域显示 \"-\"",
|
||||||
"1. 表示尚未查询或未收到设备回应;\n"
|
"1. 表示尚未查询或未收到设备回应;\n"
|
||||||
'2. 请先点击"查询当前配置"按钮;\n'
|
"2. 请先点击\"查询当前配置\"按钮;\n"
|
||||||
"3. 检查串口连接是否正常。"),
|
"3. 检查串口连接是否正常。"),
|
||||||
('Windows提示"已保护你的电脑"无法运行',
|
("Windows提示\"已保护你的电脑\"无法运行",
|
||||||
"1. 点击更多信息;\n"
|
"1. 点击更多信息;\n"
|
||||||
"2. 点击仍要运行按钮;\n"
|
"2. 点击仍要运行按钮;\n"
|
||||||
"3. 此提示是因为exe未做代码签名,软件本身安全。"),
|
"3. 此提示是因为exe未做代码签名,软件本身安全。"),
|
||||||
|
("点击设备升级后无握手/升级失败",
|
||||||
|
"1. 确认设备MAC地址正确;\n"
|
||||||
|
"2. 确认固件文件为正确的.bin文件;\n"
|
||||||
|
"3. 确认串口连接正常、设备供电稳定;\n"
|
||||||
|
"4. 查看通讯日志中的设备调试信息辅助排查。"),
|
||||||
|
("升级进度长时间停留/中途停止",
|
||||||
|
"1. 软件在约10秒未收到设备响应时会自动停止升级,属正常保护机制;\n"
|
||||||
|
"2. 检查RS485接线是否松动、设备供电是否稳定;\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))
|
||||||
@@ -372,4 +444,4 @@ for q, a in qa_items:
|
|||||||
|
|
||||||
# 生成PDF
|
# 生成PDF
|
||||||
doc.build(story)
|
doc.build(story)
|
||||||
print("说明书已生成: " + output_path)
|
print("说明书已生成: " + output_path)
|
||||||
|
|||||||
+328
-15
@@ -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,8 +14,13 @@ 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
|
||||||
|
|
||||||
|
# 软件版本号
|
||||||
|
APP_VERSION = "V1.0.0"
|
||||||
|
|
||||||
|
|
||||||
def resource_path(relative_path):
|
def resource_path(relative_path):
|
||||||
if hasattr(sys, '_MEIPASS'):
|
if hasattr(sys, '_MEIPASS'):
|
||||||
@@ -24,8 +31,7 @@ def resource_path(relative_path):
|
|||||||
class LaserTracingConfigApp:
|
class LaserTracingConfigApp:
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
self.root = root
|
self.root = root
|
||||||
self.root.title("边坡变形监测-变形(位移)自动测量系统配置工具")
|
self.root.title(f"边坡变形监测-变形(位移)自动测量系统配置工具(宽带版 {APP_VERSION})")
|
||||||
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 +41,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 +67,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 +98,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 = 433100000
|
||||||
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("433100000", 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 +257,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 +275,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 +296,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 +324,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 +345,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 +368,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 +428,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)
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# PyInstaller 版本信息文件(宽带版)
|
||||||
|
|
||||||
|
VSVersionInfo(
|
||||||
|
ffi=FixedFileInfo(
|
||||||
|
filevers=(1, 0, 0, 0),
|
||||||
|
prodvers=(1, 0, 0, 0),
|
||||||
|
mask=0x3F,
|
||||||
|
flags=0x0,
|
||||||
|
OS=0x40004,
|
||||||
|
fileType=0x1,
|
||||||
|
subtype=0x0,
|
||||||
|
date=(0, 0),
|
||||||
|
),
|
||||||
|
kids=[
|
||||||
|
StringFileInfo(
|
||||||
|
[
|
||||||
|
StringTable(
|
||||||
|
"080404b0",
|
||||||
|
[
|
||||||
|
StringStruct("CompanyName", ""),
|
||||||
|
StringStruct("FileDescription", "边坡变形监测-变形(位移)自动测量系统配置工具(宽带版 V1.0.0)"),
|
||||||
|
StringStruct("FileVersion", "1.0.0.0"),
|
||||||
|
StringStruct("InternalName", "laser_tracing_config"),
|
||||||
|
StringStruct("LegalCopyright", ""),
|
||||||
|
StringStruct("OriginalFilename", "边坡变形监测-变形(位移)自动测量系统配置工具(宽带版 V1.0.0).exe"),
|
||||||
|
StringStruct("ProductName", "边坡变形监测-变形(位移)自动测量系统配置工具"),
|
||||||
|
StringStruct("ProductVersion", "1.0.0.0"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
VarFileInfo([VarStruct("Translation", [2052, 1200])]),
|
||||||
|
],
|
||||||
|
)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user