Solar Debugger:一个轻量易扩展的上位机调试助手
在我以往从事的项目中,一个系统可能有几十甚至几百个参数,为了便于系统调试,我制作一个可以适配多个项目、简单的、专注于数据写入和显示的上位机:Solar Debugger。
gitee:https://gitee.com/lsksp/solar_-debugger
发布版本百度网盘链接: https://pan.baidu.com/s/1Cfo5AZWSh1g5_Qf7VgOTiQ?pwd=iwys 提取码: iwys
发布版本gitee链接:https://gitee.com/lsksp/solar_-debugger/releases/tag/V1.0.0
一、项目简介
Solar Debugger 是一款基于 Qt6/C++ 开发的跨平台上位机调试工具。工具主打轻量化、易扩展、零代码适配,用户仅需修改 JSON 配置文件,即可快速完成设备参数定义、界面布局与通讯协议适配,无需改动源码、无需重新编译。
本工具面向嵌入式、电机控制、机器人开发等设备调试场景,专注设备通讯交互、参数监控与曲线分析。区别于传统硬编码上位机,本工具实现参数定义、UI 布局、通讯协议完全解耦,所有参数的名称、类型、量程、单位、分组及UI样式均由配置文件动态生成,大幅提升设备调试与项目适配效率。
| 项目 | 版本 / 说明 |
|---|---|
| 语言标准 | C++17 |
| GUI 框架 | Qt 6.5 及以上(Core、Widgets、SerialPort、PrintSupport) |
| 构建系统 | CMake ≥ 3.19 |
| 编译器 | MSVC 2022 / MinGW 13+ / GCC 11+ / Clang 14+ |
| 第三方库 | QCustomPlot |
| 设计工具 | Qt Creator |
二、功能描述
视频演示:Solar Debugger:一个轻量易扩展的上位机调试助手
2.1 下行数据
软件页面上半部分包含系统配置、参数配置 两个可自定义标签页,所有标签页的名称、数量、展示顺序,均可通过 __paramsConfig.json 配置文件自定义,无需修改源码。
标签页内支持自定义下发参数控件,如模式选择、参数增益等。用户手动输入参数数值后,点击下发按钮,即可按照预设协议格式向下位机发送数据,所有下发参数的名称、数量、排列顺序均由配置文件动态定义。
界面中央独立设置开关指令区域,和下发参数类似,可由用户定义。该类指令为预设开关量,无需手动输入数值,点击即可发送固定格式指令数据。指令的名称、数量、排序同样支持配置文件自定义适配。
2.2 上行数据(下位机上传至上位机)
上位机上行数据展示界面分为三大区域:左上波形图像显示区、左下功能控制区、右侧参数通道列表区,所有功能均基于配置文件动态适配,灵活度高。
2.2.1 右侧通道列表
通道列表会动态展示配置文件中注册的所有上行参数,包含参数实时数值、波形显示开关等信息。为避免无效数据干扰、提升调试效率,参数不会默认展示,仅当下位机上传对应有效数据后,通道列表才会动态刷新显示,可搭配上行控制功能联动使用。
2.2.2 左上波形显示区
支持多维缩放操作,适配不同数据观测需求:纵向坐标轴侧滚动鼠标滚轮可实现纵向缩放,横向坐标轴侧滚动滚轮可实现横向缩放,坐标轴外侧区域滚动滚轮可实现波形整体缩放,操作方式贴合常规调试工具使用习惯。
2.2.3 左下功能控制区
集成全套波形调试工具,核心功能包含:自定义FIFO数据缓存上限、波形自动适配、数据接收与波形刷新暂停、清空历史波形数据、缓存数据导出CSV文件、光标显示/隐藏切换,满足日常数据观测、记录、调试需求。
2.2.4 上行控制核心功能
该功能可读取配置文件中注册的全部上行参数,用户可自主勾选需要采集的参数,确认发送后,上位机将推送ID=0 的上行调控特殊帧 。下位机接收响应后,会主动上传ID=1 的批量上传特殊帧,一次性回传所有勾选的参数数据,大幅提升批量数据采集效率。
常规数据上传规则:下位机可使用自定义参数ID作为帧ID,单帧数据段可携带该ID对应的多组参数数据。为保留特殊帧预留位,用户自定义参数帧ID必须大于24,≤24的ID为系统预留特殊帧专用。
项目配套提供下位机适配函数库,适配逻辑与上位机一致,开发者仅需注册对应参数,即可直接调用封装好的帧构建、响应、编解码相关函数,快速完成下位机协议适配,具体使用方法详见文档第六章。
三、配置文件说明
上位机所有上下行参数、UI布局、控件属性与通讯映射关系,均由**__paramsConfig.json** 文件统一配置。项目无需硬编码参数,修改配置文件即可完成设备适配与界面自定义。
配置文件分为两大核心节点:
- meta:配置文件基础信息,包含项目描述、版本号等通用标识。
- parameters:参数配置数组,所有上下行通讯参数、UI控件属性均在此定义。
完整可用的配置示例如下:
jsonc
{
"meta": {
"description": "Solar Debugger Configuration",
"version": "1.0"
},
"parameters": [
[30, "急停", "急停", "Uint8", 0, 1.0, 0.0, "", 1, 0, 1, "开关指令", 2],
[258, "d轴电流", "d轴电流", "Int16", 0, 0.01, 0.0, "A", 0, -1000, 1000, "上行数据", 0],
[258, "q轴电流", "q轴电流", "Int16", 2, 0.01, 0.0, "A", 0, -1000, 1000, "上行数据", 0],
[259, "温度", "温度", "Int32", 0, 0.01, 0.0, "℃", 0, -1000, 1000, "上行数据", 0],
[260, "速度", "速度", "Int16", 0, 0.01, 0.0, "rpm", 0, -500, 500, "上行数据", 0],
[261, "电量", "电量", "Uint8", 0, 0.1, 0.0, "", 0, 0, 100, "上行数据", 0],
[512, "模式选择", "模式选择", "Uint8", 0, 1.0, 0.0, "", 4, 1, 16, "系统配置", 1],
[513, "增益", "增益", "Float", 0, 1.0, 0.0, "LSB", 1.0, 0.001, 10.0, "系统配置", 1],
[500, "电流环Kp", "电流环比例增益", "Float", 0, 1.0, 0.0, "", 1.0, 0.0, 100.0, "参数配置", 1],
[501, "电流环Ki", "电流环积分增益", "Float", 0, 1.0, 0.0, "", 10.0, 0.0, 1000.0, "参数配置", 1]
]
}
parameters 数组内每个元素为固定13位参数配置数组,字段顺序、含义固定,完整定义如下:
text
[ID, 名称, 描述, 数据类型, 位偏移量, 比例, 偏置, 单位, 默认值, 最小值, 最大值, 分类, ui显示类型]
| 序号 | 字段 | 类型 | 说明 |
|---|---|---|---|
| 0 | ID | int | 数据段的唯一 ID(必须大于24),对应通讯帧中的 frameId。下行参数 一个 ID 对应一个参数;上行参数 一个 ID 可对应多个参数(通过 位偏移量 区分) |
| 1 | 名称 | string | UI 上显示的参数名 |
| 2 | 描述 | string | 对参数作用的详细介绍 |
| 3 | 数据类型 | string | Uint8 / Int8 / Uint16 / Int16 / Uint32 / Int32 / Float |
| 4 | 位偏移量 | int | 参数在帧 data 段中的起始字节偏移 |
| 5 | 比例 | float | 缩放因子:物理值 = 原始值 × 比例 + 偏置 |
| 6 | 偏置 | float | 偏移量 |
| 7 | 单位 | string | 物理单位(如 A、r/min、℃),可留空 |
| 8 | 默认值 | float | UI 初始化时显示的值 |
| 9 | 最小值 | float | UI 输入校验下限 |
| 10 | 最大值 | float | UI 输入校验上限 |
| 11 | 分类 | string | UI 分组标签,相同分类的参数会自动归到同一个 Tab 页 |
| 12 | ui显示类型 | int | 0 = 只读数值显示;1 = 可写数值输入框;2 = 开关按钮 |
3.2 下行数据配置规范
下行数据为上位机下发至下位机的控制参数,为简化协议逻辑、提升传输效率,统一遵循以下规范:
- ID唯一绑定:单个通讯ID仅对应一个下行参数,映射关系唯一。
- 无数据缩放:下行参数比例固定为1、偏置固定为0,帧原始数据与物理值完全一致,支持直接指针操作,无需类型转换。
- 双UI下发模式:支持两类可下发控件,底层通讯逻辑完全一致,仅展示形式不同。UI类型1为数值输入框,手动输入数值后下发;UI类型2为功能按钮,点击直接发送默认值,适用于急停、启停等开关指令
3.3 上行数据配置规范
上行数据为下位机上传至上位机的监测参数,支持单ID多参数绑定,提供两种传输模式,适配不同调试场景,且下位机上传数据无需上位机回复响应帧。
- 单ID多参数绑定:同一个通讯ID可绑定多个监测参数,通过字节偏移量区分不同参数,节省帧ID资源。
- 普通单ID上传:以参数自定义ID为帧ID,单帧仅传输该ID对应的一组参数数据,适用于单点实时监测。
- 批量统一上传:固定帧ID为1,单帧可聚合多个自定义ID的参数数据,配合上位机上行控制功能,实现按需批量采集,高效调试多通道数据
四、软件架构
Solar_Debugger/
├── CMakeLists.txt
├── main.cpp
├── MainWindow.cpp / .h # 主窗口装配
├── CommunicationManager.cpp / .h # 通讯管理(子线程)
├── __paramsConfig.json # 用户配置文件,需要复制到可执行exe文件同目录
├── Solar_Debugger.rc # Windows 资源文件
├── ReadMe.md
├── ui/ # UI 层
│ ├── BaseConfigPage.* # 通用参数配置页(表格 + 输入框)
│ ├── ConfigTabwidget.* # 多分类 Tab 容器
│ ├── ConnectionWidgetBase.h # 连接配置对话框基类
│ ├── SerialConnectionWidget.* # 串口连接页面
│ ├── Switchwidget.* # 开关按钮组
│ ├── CurvePlotWidget.* # 实时曲线绘图(基于 QCustomPlot)
│ ├── UplinkControlDialog.* # 上行调控对话框(勾选 ID 下发 frameId=0)
│ └── Logwidget.* # 通讯日志面板
├── src/
│ ├── core/ # 数据核心层
│ │ ├── Parameter.h # 参数描述结构
│ │ ├── ParameterRegistry.* # 参数注册表
│ │ ├── DataCacheLayer.* # 数据缓存与节流
│ │ └── RingBuffer.h # 环形缓冲
│ ├── protocol/ # 协议层
│ │ ├── ProtocolCodec.h # 抽象编解码接口
│ │ └── SerialCodec.* # 串口协议实现
│ ├── communication/ # 通讯驱动层
│ │ ├── AbstractCommunicationDriver.h
│ │ └── SerialDriver.*
│ └── utils/
├── device_library/ # 下位机调用库(纯 C 实现)
│ ├── serial_protocol.* # 串口帧协议(打包、解析、CRC16)
│ ├── param_table.* # 参数管理引擎(注册、校验、上传)
│ └── param_manager.* # 应用层示例(参数定义、回调处理)
├── qcustomplot/ # 第三方绘图库源码
├── res/ # 资源文件(可替换自己的qss文件),需要复制到可执行exe文件同目录
└── build/ # 构建输出目录
Solar Debugger 采用 分层 + 抽象接口 的设计,便于后续替换通讯方式与协议而无需改动上层 UI。整个系统自上而下分为四层,数据流向如下:
UI层 ←→ 核心层(参数注册表+数据缓存) ←→ 协议层(编解码) ←→ 通讯层(串口驱动)
- UI层 (
ui/):负责界面展示与用户交互,包括参数配置页(BaseConfigPage)、Tab容器(ConfigTabWidget)、实时曲线绘图(CurvePlotWidget)、日志面板(LogWidget)等。所有UI均由__paramsConfig.json驱动自动生成,无需硬编码。 - 核心层 (
src/core/):ParameterRegistry(单例)在启动时从 JSON 加载全部参数定义,供 UI 和协议层查询;DataCacheLayer对高频接收数据做节流降采样,避免 UI 刷新过载。 - 协议层 (
src/protocol/):ProtocolCodec是纯虚接口(encode/decode),当前由SerialCodec实现自定义串口帧协议。替换协议只需新增一个ProtocolCodec子类。 - 通讯层 (
src/communication/):AbstractCommunicationDriver定义了打开/关闭/下发等纯虚接口,当前由SerialDriver实现串口收发。CommunicationManager将驱动运行在独立子线程中,通过信号槽与主线程通信。
五、通讯协议
当前默认采用 自定义串口帧协议,结构清晰、自带校验、支持粘包处理。协议的基本格式如下:
| 字段名 | 长度(字节) | 数据内容 | 说明 |
|---|---|---|---|
| 帧头 (SOF) | 2 | 0x55, 0xAA |
固定起始标志 |
| 帧计数 | 1 | cnt |
每发送一帧自增 1,用于检测丢包或乱序(溢出后自动归零) |
| 帧 ID | 2 | frameId |
参数 ID(沿用 CAN ID 概念),小端模式(低字节在前) |
| 数据长度 | 2 | len |
数据字段的长度,小端模式 |
| 数据 | N | data |
实际传输的参数数据,长度由上一字段决定 |
| CRC16 | 2 | crc |
Modbus CRC16,小端模式。校验范围:从"版本"到"数据" |
| 帧尾 (EOF) | 2 | 0xAA, 0x55 |
固定结束标志 |
帧ID段和数据段是必要的,帧ID段用于判定数据对应的参数,数据段是数据的载体。对于用户定义的帧ID必须大于24,这是因为对于小于等于24的ID会用于一些特殊帧(响应帧、控制帧、批量上传帧)
5.2 数据打包规则
- 物理值与原始字节之间的转换遵循公式:
physical = raw * scale + offsetValue - 多字节整型按 小端模式 打包与解析
- 一个
frameId(一次下行帧)可携带多个上行参数,参数在帧内通过offset字段区分字节位置
5.3 特殊帧
5.3.1 上行调控帧(frameId = 0)
frameId = 0 是保留的上行调控帧,上位机专属控制帧,无对应业务参数,作用是主动指定下位机需要上传的参数ID列表,实现按需采集、筛选数据,减少无效传输。
上行调控帧不对应参数表中的任何 ID,固定ID为0,用于上位机主动告诉下位机"我需要看哪些上行数据"。数据段格式:
| 帧ID段(2字节) | 数据段(2×N 字节) |
|---|---|
| 0 | 按顺序排列用户勾选的 ID号,每个 ID 占 2 字节,小端打包。 |
如下例子所示,若我想要获取温度和速度信息,选择后发送。
则下位机将发送下述十六进制源码到下位机:
tex
55 AA 00 00 00 04 00 03 01 04 01 99 F0 AA 55
对源码解析如下:
5.3.2 批量上传帧(frameId = 1)
依赖上行调控帧生效,是下位机的批量数据应答帧。下位机严格按照上行调控帧携带的ID顺序,将对应参数数据依次填入数据段,单帧可完成多参数批量上传,大幅提升数据传输效率。
若上位机未提前下发上行调控帧、无对应ID排序记录,收到该帧后会提示"未注册批量上传配置"。
相较于传统单ID绑定多参数的上传方式,该模式支持动态筛选采集参数,适配复杂控制系统上百级参数的调试场景,灵活性更强。
5.3.3 响应帧(frameId = 2)
下位机应答专用帧,用于确认上位机所有下行指令(普通参数、特殊帧)是否执行成功。
数据段格式 :固定2字节,存储被应答的目标帧ID,告知上位机对应指令已正常接收处理。
超时机制:上位机下发指令后,100ms内未收到下位机响应帧,判定为通讯异常并打印日志报错。
| 帧ID段(2字节) | 数据段(2 字节) |
|---|---|
| 2 | 需要响应的帧ID |
六、下位机调用库
为了方便下位机(如 STM32)快速接入 Solar Debugger,工程的 device_library/ 文件夹提供了一套纯 C 实现的参数通讯库,无需依赖任何第三方库。
6.1 库文件概览
| 文件 | 模块 | 功能 |
|---|---|---|
serial_protocol.h/.c |
串口帧协议 | 帧打包、帧解析、CRC16 校验 |
param_table.h/.c |
参数管理引擎 | 参数注册、范围校验、数据写入、批量/单次上传 |
param_manager.h/.c |
应用层示例 | 具体参数定义、UART 回调处理、周期上传逻辑 |
三个模块的依赖关系:param_manager → param_table → serial_protocol,自上而下调用。
6.2 serial_protocol --- 串口帧协议
负责将原始字节流解析为帧结构,或将帧数据打包为字节流。解析时采用零拷贝 方式,payload 指针直接指向接收缓冲区。
对外接口:
| 函数 | 作用 |
|---|---|
protocol_parse_frame(rx_buf, size, frame) |
从接收缓冲区中解析一帧,校验帧头、帧尾、CRC16,成功返回 true |
protocol_pack_frame(frame_id, data, data_len, out_buf, out_len) |
将 frameId + 数据段打包为完整协议帧,自动填充帧头、帧计数、CRC、帧尾 |
param_table_pack_response(responded_id, out_buf, out_len) |
打包响应帧(frameId=2),用于应答上位机的下行指令 |
crc16_modbus(data, len) |
计算 Modbus CRC16 校验值 |
6.3 param_table --- 参数管理引擎
通用的参数注册表,将整数 ID 映射到应用变量,支持多数据类型、缩放因子、范围校验和批量上传。最多可注册 32 个参数条目。
核心概念:
- scale(缩放因子):上行传输时,浮点变量会除以 scale 转为整数发送;上位机收到后乘以 scale 还原物理值
- flags :
PARAM_FLAG_FLOAT_TO_INT(变量是 float,传输时转整数)或PARAM_FLAG_DIRECT_INT(直接拷贝原始字节) - offset(偏移):同一 ID 下多个参数通过 offset 区分字节位置(如 d 轴/q 轴电流共享 ID=258)
对外接口:
| 函数 | 作用 |
|---|---|
param_table_init(tbl, min_id) |
初始化参数表,设置最小合法 ID |
param_table_register(tbl, id, offset, type, var_ptr, scale, flags, min, max) |
注册一个参数条目,绑定 ID 与应用变量 |
param_table_process(tbl, id, data, len) |
处理接收到的下行数据:校验范围 → 写入绑定变量 → 置修改标志 |
param_table_set_upload_ids(tbl, ids, count) |
设置批量上传的 ID 列表(由上行调控帧触发) |
param_table_batch_upload(tbl, out_buf, out_len) |
将所有注册的上传 ID 打包为一帧(frameId=1) |
param_table_upload_single(tbl, id, out_buf, out_len) |
将单个 ID 的数据打包为一帧 |
param_table_clear_modified(tbl) |
清除所有参数的修改标志 |
6.4 param_manager --- 应用层示例
这是面向具体项目的应用层文件,演示如何使用上述两个模块。开发者可直接参考此文件进行改造。
对外接口:
| 函数 | 作用 |
|---|---|
param_init() |
初始化参数表并注册所有上下行参数 |
param_handle(id, data, len) |
处理接收到的帧:id ≥ 24 时写入参数并回复 ACK;id = 0 时配置批量上传列表 |
param_upload() |
周期调用,将所有上行参数打包发送(需替换其中的模拟数据为真实传感器数据) |
6.5 使用示例讲解
以 param_manager.c 为例,分三步说明库的使用方式:
第一步:初始化参数(param_init)
c
/**
* 初始化参数管理器,注册所有参数条目
*/
void param_init(void)
{
/* 初始化参数表,设置最小ID */
param_table_init(&g_param_tbl, MIN_PARAM_ID);
/**************** 上行数据 ***************/
/* 注册电流参数(ID258,不同偏移) */
param_table_register(&g_param_tbl, PARAM_ID_CURRENT, 0, DT_INT16,
&d_current_f, 0.01f, 1, -1000, 1000);
param_table_register(&g_param_tbl, PARAM_ID_CURRENT, 2, DT_INT16,
&q_current_f, 0.01f, 1, -1000, 1000);
/* 注册温度、速度、电池参数 */
param_table_register(&g_param_tbl, PARAM_ID_TEMPERATURE, 0, DT_INT32,
&temperature_f, 0.01f, 1, -1000, 1000);
param_table_register(&g_param_tbl, PARAM_ID_SPEED, 0, DT_INT16,
&speed_f, 0.01f, 1, -500, 500);
param_table_register(&g_param_tbl, PARAM_ID_BATTERY, 0, DT_UINT8,
&battery_f, 0.1f, 1, 0, 100);
/**************** 下行数据 ***************/
/* 注册系统参数 */
param_table_register(&g_param_tbl, PARAM_ID_EMERGENCY, 0, DT_UINT8,
&emergency, 1.0f, 2, 0, 1);
param_table_register(&g_param_tbl, PARAM_ID_MODE, 0, DT_UINT8,
&mode, 1.0f, 2, 1, 16);
param_table_register(&g_param_tbl, PARAM_ID_GAIN, 0, DT_FLOAT,
&gain, 1.0f, 2, 0.001f, 10.0f);
}
每个 register 调用将一个 ID 绑定到一个全局变量,后续收发数据时库会自动读写该变量。
第二步:接收处理(UART 回调 → param_handle)
c
/**
* 处理接收到的参数帧
* @note 接收到调控帧后调用,处理参数写入和批量上传ID设置。
* 直接在接收到数据帧后调用发送响应帧,一般不建议在中断直接调用全局变量、占用硬件资源等。
* 但考虑仅在上位机下发数据时调用,程序此时应处于调试或等待指令阶段,一般不会影响外部任务。
* 且发送函数内部会等待发送完成,因此暂时不考虑中断安全问题。
*/
void param_handle(uint16_t id, uint8_t *data, uint16_t len)
{
if (id >= MIN_PARAM_ID) {
/* 处理普通参数写入 */
param_table_process(&g_param_tbl, id, data, len);
/* 打包响应帧 */
param_table_pack_response(id, send_buf, &send_len);
/* 等待串口发送完成并发送数据 */
while (uart3_is_busy()) {
/* 等待发送完成 */
}
debug_send(send_buf, send_len);
}
else if (id == PARAM_ID_CONTROL) /* 检测到上行控制帧 */
{
/* 处理系统参数:设置批量上传ID列表 */
param_table_set_upload_ids(&g_param_tbl, (uint16_t *)data, len / 2);
/* 打包响应帧 */
param_table_pack_response(id, send_buf, &send_len);
while (uart3_is_busy()) {
/* 等待发送完成 */
}
debug_send(send_buf, send_len);
/* 清除发送标志,允许上传 */
send_flag = 0;
}
}
param_handle 内部逻辑:
id ≥ 24:调用param_table_process将数据写入对应变量,然后发送 ACK 响应帧id = 0:调用param_table_set_upload_ids配置批量上传列表
第三步:周期上传(param_upload)
c
/**
* 上传参数(由应用层周期调用)
*/
void param_upload(void)
{
static uint32_t tick = 0; /* 模拟时间计数器 */
float sin_val = 0.0f;
tick++;
/* 计算正弦值,周期约为 100 个 tick */
sin_val = sinf((float)tick * 0.0628f); /* 0.0628 ≈ 2π/100 */
/* 模拟 d_current_f:正弦波,范围 -10.0 ~ 10.0 */
d_current_f = sin_val * 10.0f;
/* 模拟 q_current_f:余弦波(正弦偏移90度),范围 -10.0 ~ 10.0 */
q_current_f = sinf((float)tick * 0.0628f + 1.5708f) * 10.0f;
/* 模拟 temperature_f:锯齿波,范围 0 ~ 100,周期 100 tick */
temperature_f = (float)(tick % 100);
/* 模拟 speed_f:三角波,范围 -500.0 ~ 500.0 */
speed_f = (float)((tick % 200) - 100) * 5.0f;
/* 模拟 battery_f:反向锯齿波,范围 0 ~ 100 */
battery_f = 100.0f - ((float)(tick % 200) * 0.5f);
/* 批量上传 */
param_table_batch_upload(&g_param_tbl, send_buf, &send_len);
/* 单参数上传ID为PARAM_ID_CURRENT的数据 */
// param_table_upload_single(&g_param_tbl, PARAM_ID_CURRENT, send_buf, &send_len);
/* 串口发送数据 */
debug_send(send_buf, send_len);
}
在定时器中断或主循环中周期调用即可,库会自动按注册的 ID 顺序打包数据。
6.6 快速接入步骤
- 将
serial_protocol.h/.c和param_table.h/.c加入你的工程 - 参考
param_manager.h/.c,定义自己的全局变量和参数 ID - 在初始化函数中调用
param_table_register()注册所有参数 - 在 UART 接收回调中调用
protocol_parse_frame()+param_handle() - 在定时任务中调用
param_upload(),将模拟数据替换为真实传感器读数
七、License
本项目以开源方式发布,欢迎学习、研究、二次开发使用。具体协议见 LICENSE(如未附带则默认遵循 MIT 协议)。
八、致谢
- Qt Project --- 跨平台 GUI 框架
- QCustomPlot --- 优秀的 Qt 绘图库
如果这个项目对你有帮助,欢迎 ⭐ Star 支持;问题与建议请提 Issue,PR 欢迎。
QQ:2981218238
邮箱:lsk0358@163.com