news 2026/9/13 15:00:29

C#上位机串口Modbus温湿度采集与MySQL存储实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#上位机串口Modbus温湿度采集与MySQL存储实战

简介:一套基于Modbus协议的上位机串口通信与MySQL存储的完整工程方案,面向工业监控、环境数据采集及QT上位机开发者,解决温湿度数据实时采集、动态曲线展示与历史存储查询等问题。压缩包共15个文件,包含5个C++源文件(负责业务逻辑与通信)、4个头文件(声明接口与数据结构)、4个QT界面文件(可调整显示布局)以及工程配置与用户设置文件,整体仅24KB,结构紧凑便于阅读。工程覆盖串口参数配置、Modbus读写命令封装、MySQL建表与插入查询、实时曲线绘制等关键模块,并设计了按时间段检索历史数据的功能,代码量精炼且逻辑完整,可直接编译运行或在此基础上二次扩展。目前已有197人学习下载,适合具备一定C++/QT基础、需要快速搭建同类监控系统的开发者参考。

1. 上位机、串口、Modbus 与 MySQL 组成的温湿度采集链路

很多做上位机开发的人,第一次接触 Modbus 就是在温湿度采集场景里。下位机是带 RS-485 或 RS-232 的温湿度变送器,上位机用 C# 写窗体程序,通过串口每隔几秒读一次寄存器,再把数据写进 MySQL。整条链路拆开其实是三段:串口要保证帧不丢不乱,Modbus RTU 要处理 CRC 与寄存器地址映射,MySQL 要设计成能存一年以上的时序数据还不卡。动态曲线只是最上面一层表现。下面按这三段展开,从协议解析、表结构设计、曲线联动一路到历史回放与排错,适合正在做设备监控、产线数据采集上位机,或者被客户要求留三年历史数据的工程师。

2. 串口上跑 Modbus RTU:从报文结构到温湿度寄存器的解析实现

2.1 Modbus RTU 帧结构与时序:为什么 CRC 必须自己算

Modbus RTU 是主从问答式协议,上位机永远作为主机发起请求,温湿度变送器作为从机被动应答。一条读请求固定 8 字节:从站地址 1 字节、功能码 1 字节、起始寄存器地址 2 字节、寄存器数量 2 字节、CRC16 校验 2 字节。从站地址决定总线上哪个设备回答,功能码区分读什么类型的寄存器,CRC 则是整个报文里最容易抄错的部分。

字节位置内容长度说明调试示例
0从站地址1拨码开关或模块配置0x01
1功能码10x03 读保持寄存器,0x04 读输入寄存器0x04
2-3起始地址2寄存器地址,高字节在前0x0101
4-5寄存器数量2本次一次读几个寄存器0x0002
6-7CRC162低字节在前计算得到

温湿度变送器的寄存器地址随厂家而异,常见做法是把温度放在 0x0101、湿度放在 0x0102,用 0x04 功能码读输入寄存器。也有不少模组把数据放到保持寄存器里,这时要把功能码从 0x04 换成 0x03,地址不变。很多人在这一步栽跟头:用 Modbus 调试工具能读到数据,换成上位机就超时,往往就是功能码和寄存器类型没对上。RTU 还有个容易被忽略的时序要求:报文之间至少要空闲 3.5 个字符时间,9600 波特率下大约是 4 毫秒,这个间隔串口驱动会负责,但连续高频查询时如果间隔太短,从站会把两个请求粘成一帧。

2.2 用 C# 构建读请求:从站地址、功能码与寄存器地址对齐

2.2.1 读请求代码与 CRC 校验实现

CRC16 在 .NET 的串口 API 里不会自动生成,必须自己算。标准 CRC-16/MODBUS 的算法是初始值 0xFFFF,把帧里每个字节异或进去,再对每个 bit 按多项式 0xA001 移位处理。

static ushort ComputeCrc16(byte[] data, int length) { ushort crc = 0xFFFF; // CRC-16/MODBUS 初始值 for (int i = 0; i < length; i++) { crc ^= data[i]; for (int bit = 0; bit < 8; bit++) { crc = (crc & 0x0001) != 0 ? (ushort)((crc >> 1) ^ 0xA001) // 多项式 0xA001 : (ushort)(crc >> 1); } } return crc; }

这段函数从字节 0 到 length-1 逐步计算,返回的是 16 位无符号整数。注意返回值不能直接强转成 byte,发送时要拆成两个字节,并且低字节在前。

static byte[] BuildReadRequest(byte slaveId, ushort startRegister, ushort registerCount) { byte[] frame = new byte[8]; frame[0] = slaveId; frame[1] = 0x04; // 读输入寄存器;对保持寄存器改 0x03 frame[2] = (byte)(startRegister >> 8); // 地址高字节在前 frame[3] = (byte)(startRegister & 0xFF); frame[4] = (byte)(registerCount >> 8); frame[5] = (byte)(registerCount & 0xFF); ushort crc = ComputeCrc16(frame, 6); frame[6] = (byte)(crc & 0xFF); // CRC 低字节在前 frame[7] = (byte)(crc >> 8); return frame; }

startRegister 就是 0x0101 这种具体寄存器地址,registerCount 是要读的寄存器个数。如果温度和湿度各占一个寄存器,count 传 2,一次请求拿回 4 字节数据,比发两次请求省一半交互时间。请求帧通过 SerialPort.Write 发出去,前提是初始化参数已经配对:数据位 8、停止位 1、无校验(即 8N1)是绝大多数温湿度变送器的默认组合,波特率出厂多为 9600,与下位机不一致的后果是收不到任何响应。

2.2.2 响应帧解析与小数位还原

从站的正常响应是 9 字节:地址 1、功能码 1、字节数 1、数据 4、CRC 2。温度和湿度在小端机器上的拼装方式和手册里的“高字节在前”相反,需要手动把两个字节拼成 16 位整数。

static bool TryParseResponse(byte[] resp, byte slaveId, out double temp, out double hum) { temp = 0; hum = 0; if (resp.Length < 9) return false; // 长度 9 为正常帧,5 为异常帧 if (resp[0] != slaveId) return false; if (resp[1] == 0x84) return false; // 异常功能码:最高位置 1 if (resp[1] != 0x04) return false; ushort crcRecv = (ushort)(resp[resp.Length - 2] | (resp[resp.Length - 1] << 8)); if (ComputeCrc16(resp, resp.Length - 2) != crcRecv) return false; short rawTemp = (short)((resp[3] << 8) | resp[4]); // 温度寄存器,高字节在前 short rawHum = (short)((resp[5] << 8) | resp[6]); // 湿度寄存器 temp = rawTemp / 10.0; hum = rawHum / 10.0; return true; }

响应里的数值多数按 0.1 精度放大存储,2551 表示 25.5 摄氏度,所以解析完要除以 10。用 short 而不是 int 是为了保留符号位,冷库的负温度靠这个符号位还原。CRC 比较必须在拼值之前做,否则脏帧会把温度显示成异常大数,甚至把故障数据写进历史库。

提示:寄存器数量一变,正常响应长度就变,计算方式是5 + 2 * registerCount。代码里按读 2 个寄存器固定 9 字节处理,改成读 4 个寄存器时记得同步调整长度判断。

2.3 用 Modbus Slave 模拟温湿度从站,不接硬件也能联调

手头没有传感器时,用 Modbus Slave 这类从站模拟器加上一对虚拟串口,就能把整条通信链路跑起来。虚拟串口软件会把 COM3 和 COM4 组成一对,上位机打开 COM3,Modbus Slave 监听 COM4,两边就像用一条串口线连着。在 Slave 里设置从站地址为 1,在 0x0101 和 0x0102 两个输入寄存器里分别填入 255 和 520,上位机每隔一秒读一次,显示区应该能看到 25.5℃ 和 52.0%RH 跟随变化。这一步把协议解析和硬件调试隔离,先证明上位机程序没问题,再去查现场的接线、干扰和波特率。想反过来验证上位机的请求字节对不对,可以把 Modbus Poll 这类主机工具挂在同一对虚拟串口的另一端,直接对照请求帧的十六进制内容。读寄存器类型必须和功能码对应:Slave 里填输入寄存器,上位机就要用 0x04 读;填保持寄存器,上位机则必须用 0x03,两边类型不一致,调试界面上全是超时异常。

3. MySQL 历史数据表设计与批量写入策略

3.1 时序数据表结构:不设外键,按时间精确到毫秒

温湿度数据是典型的时序数据,和业务表不同,它几乎只做追加写入和时间范围查询,很少更新单行。表结构设计要按这个读写特点来。

CREATE TABLE temp_hum ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, device_id VARCHAR(32) NOT NULL COMMENT '从站地址或设备编号', temperature DECIMAL(5,2) NOT NULL COMMENT '温度,单位℃', humidity DECIMAL(5,2) NOT NULL COMMENT '湿度,单位%RH', sample_time DATETIME(3) NOT NULL COMMENT '采集时间,毫秒精度', raw_data VARCHAR(64) DEFAULT NULL COMMENT '原始寄存器字节,排障用', PRIMARY KEY (id), KEY idx_device_time (device_id, sample_time), KEY idx_time (sample_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='温湿度历史表';

字段类型上有两个容易被忽略的点。第一,temperature 用 DECIMAL(5,2) 而不用 FLOAT。浮点类型在 MySQL 里存 25.5 看着没问题,但做 AVG、SUM 聚合时会出现 25.499999 这类尾巴,DECIMAL 是定点数,聚合结果能和界面显示值对上。第二,sample_time 用 DATETIME(3) 而不是 DATETIME,采样周期到 1 秒以内时,毫秒能区分同一秒内多条记录,排序更可靠,也不会因为相邻记录时间完全相同,把曲线画成竖直跳变。

这张表不建任何外键,也不和设备档案表做 JOIN。理由很直白:时序表的写入要快,外键约束带来额外开销,而且历史查询通常只需要 device_id 和时间范围两个条件,索引已经够用。设备名、安装位置这类静态信息放到另一张表,查询时在应用层合并结果,比数据库里做 JOIN 压力小。

3.2 采样周期、数据量与按月份分区

表结构确定后要估算数据量。假设采样周期 1 秒,一天产生 86400 条记录,按单条约 60 字节算,单日约 5MB,一个月 150 万条左右,一年接近 3150 万条。这个量级对 MySQL 单表不是不能跑,但查询会明显变慢,备份和清理也不好做。常见做法是按月做 RANGE 分区,注意 InnoDB 分区表的唯一键必须包含分区键,所以建表时主键要写成 (id, sample_time)。

CREATE TABLE temp_hum_part ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, device_id VARCHAR(32) NOT NULL, temperature DECIMAL(5,2) NOT NULL, humidity DECIMAL(5,2) NOT NULL, sample_time DATETIME(3) NOT NULL, raw_data VARCHAR(64) DEFAULT NULL, PRIMARY KEY (id, sample_time), KEY idx_device_time (device_id, sample_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 PARTITION BY RANGE COLUMNS(sample_time) ( PARTITION p202501 VALUES LESS THAN ('2025-02-01'), PARTITION p202502 VALUES LESS THAN ('2025-03-01'), PARTITION p202503 VALUES LESS THAN ('2025-04-01'), PARTITION pMax VALUES LESS THAN (MAXVALUE) );

分区以后,删除过期数据直接ALTER TABLE temp_hum_part DROP PARTITION p202503;,秒级完成,不需要发出几百万行的 DELETE。查询时 MySQL 自动做分区裁剪,只扫描命中时间段的分区。已经在线运行的单表想改成分区表,常规做法是新建分区表、INSERT 迁移、业务切表,避免直接 ALTER TABLE 触发长时间锁表和主键重建。注意最后的 pMax 分区要保留,否则时间超出分区范围的数据会写入失败。

3.3 批量写入与连接池配置:从一条一条插到每 2 秒一批

3.3.1 批量 INSERT 代码

上位机采集是高频写入,每条都开连接再 COMMIT 一次,每秒几十条数据时 MySQL 很容易到连接上限。常见做法是采集线程只解析数据,把记录放进内存队列,每 2 秒取一批做批量 INSERT。

async Task BatchInsertAsync(List<(string dev, double t, double h, DateTime dt)> rows) { if (rows.Count == 0) return; var values = new StringBuilder(); for (int i = 0; i < rows.Count; i++) { if (i > 0) values.Append(','); var r = rows[i]; values.Append($"('{r.dev}','{r.t:0.00}','{r.h:0.00}','{r.dt:yyyy-MM-dd HH:mm:ss.fff}')"); } using var conn = new MySqlConnection(connectionString); await conn.OpenAsync(); using var tx = await conn.BeginTransactionAsync(); string sql = "INSERT INTO temp_hum(device_id,temperature,humidity,sample_time) VALUES " + values.ToString(); using var cmd = new MySqlCommand(sql, conn, tx) { CommandTimeout = 10 }; await cmd.ExecuteNonQueryAsync(); await tx.CommitAsync(); }

这是千万行级数据量下最简单的批量写法:一条 SQL 插入所有行,事务包住保证要么全成功要么全失败。温度、湿度用0.00格式化成两位小数再放进 SQL,避免系统区域设置把小数点变成逗号导致语法错误。CommandTimeout 设 10 秒,数据库短暂不可用时宁可这次失败,也不能让采集队列越积越大。批量行数控制在 100 到 500 条之间比较合适,太大会超过 max_allowed_packet 默认值。

注意:批量 SQL 的 VALUES 顺序必须和 INSERT 目标列严格一致。临时加字段后最常出现的问题不是 SQL 报错,而是数值错位。

3.3.2 连接串参数与常见误区

connectionString 里值得逐个确认的参数有下面几个。

参数建议值说明
Server数据库地址上位机本机部署用 127.0.0.1
Poolingtrue连接池默认开启,不要手工关闭
Max Pool Size202 秒一批写入,20 足够
ConnectionTimeout5数据库不可达时快速失败,避免 UI 卡顿
SslModeNone仅限内网采集场景,公网部署必须按合规要求配置
Charsetutf8mb4与表字符集一致,避免中文设备名乱码

常见误区是把连接串写在循环里,每次都 new MySqlConnection。局部 using 本身没问题,问题在于频繁开关连接会让连接池反复收缩,表现为运行半小时后偶发超时。稳妥做法是让 BatchInsertAsync 里的连接实例在整个程序生命周期内被复用,或者至少配置 Min Pool Size=2 减少收缩抖动。

4. 实时温湿度显示与动态变化曲线

4.1 串口数据进入 UI 的线程模型:生产者-消费者

C# 上位机里 SerialPort.DataReceived 事件运行在串口后台线程,绝对不能在这个事件里直接给 TextBox、Chart 控件赋值。WinForms 里这样做会抛跨线程异常,WPF 里会随机闪烁或直接崩溃。正确处理是把事件当生产者,把解析好的数据点放进线程安全队列,UI 的窗体计时器作为消费者定时取出。

BlockingCollection<(double temp, double hum, DateTime ts)> dataQueue = new(4096); void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { var sp = (SerialPort)sender; int available = sp.BytesToRead; byte[] buffer = new byte[available]; sp.Read(buffer, 0, available); receiveBuffer.AddRange(buffer); // 当前按读 2 个寄存器、响应 9 字节切分 while (receiveBuffer.Count >= 9) { byte[] frame = receiveBuffer.Take(9).ToArray(); receiveBuffer.RemoveRange(0, 9); if (TryParseResponse(frame, slaveId, out double temp, out double hum)) { if (!dataQueue.TryAdd((temp, hum, DateTime.Now))) { dataQueue.TryTake(out _); // 队列满时丢最旧 dataQueue.TryAdd((temp, hum, DateTime.Now)); } } } }

TryParseResponse 直接复用第 2 章的解析逻辑,但串口底层可能把一个完整帧拆成两次到达,所以用累积缓冲区,只有凑够 9 字节并校验 CRC 通过才认为拿到完整帧。队列容量 4096,按 1 秒采样足够;满时先丢最旧再入队,避免上位机长时间卡顿导致内存上涨。UI 侧用 System.Windows.Forms.Timer,间隔 100 到 500 毫秒,从队列批量取数据。

private void UiTimer_Tick(object sender, EventArgs e) { while (dataQueue.TryTake(out var point)) { AppendToChart(point.ts, point.temp, point.hum); dbBuffer.Add(point); } if (dbBuffer.Count >= 100 || (dbBuffer.Count > 0 && DateTime.UtcNow - lastDbFlushUtc > TimeSpan.FromSeconds(2))) { var snapshot = dbBuffer.ToList(); dbBuffer.Clear(); lastDbFlushUtc = DateTime.UtcNow; _ = Task.Run(() => BatchInsertAsync(snapshot)); } }

100 毫秒轮询在 UI 线程上消耗极小。这个模型把串口接收、界面显示、MySQL 写入三个环节完全解耦,任何一环慢都不会阻塞串口读取,这是上位机长时间运行不卡的基础。界面刷新率由计时器决定,而不是由传感器采集速度决定,调速度时只改计时器间隔即可。

4.2 Chart 控件画双轴动态曲线:滑动窗口与坐标自适应

实时曲线推荐用 WinForms 自带的 Chart 控件。.NET Framework 工程里直接在工具箱引用 System.Windows.Forms.DataVisualization,.NET 6 及以上要从 NuGet 安装对应包。图表里加两个 Series:

系列名ChartType对应轴单位
温度LineAxisY 主 Y 轴
湿度LineAxisY2 次 Y 轴%RH

湿度范围通常在 0 到 100,温度可能是 -20 到 60,两个量级不同。把湿度挂到次 Y 轴,两条曲线都能撑满各自高度,否则湿度会被压成一条接近直线的平线。开双轴和鼠标缩放都在窗体 Load 里设置一次。

this.chart1.ChartAreas[0].CursorX.IsUserEnabled = true; this.chart1.ChartAreas[0].CursorX.IsUserSelectionEnabled = true; this.chart1.Series["温度"].YAxisType = AxisType.Primary; this.chart1.Series["湿度"].YAxisType = AxisType.Secondary;

追加数据点时,动态曲线最核心的问题是老点什么时候移出视线。只 AddXY 不清理,跑一天后图上有几万个点,CPU 和内存双双失控。常见做法是限制最多显示最近 600 个点,超出就从索引 0 开始移除。

const int maxPointsOnChart = 600; void AppendToChart(DateTime ts, double temp, double hum) { pointCount++; chart1.Series["温度"].Points.AddXY(ts, temp); chart1.Series["湿度"].Points.AddXY(ts, hum); while (pointCount > maxPointsOnChart) { chart1.Series["温度"].Points.RemoveAt(0); chart1.Series["湿度"].Points.RemoveAt(0); pointCount--; } chart1.ChartAreas[0].RecalculateAxesScale(); }

RecalculateAxesScale 会在移除后重新计算坐标范围,不必自己维护 X 轴最小值。600 个点对应 1 秒采样下的 10 分钟窗口,是一个便于观察变化的时间窗。补充一个环境问题:VS2019 里建的 C# 上位机工程,目标框架如果是 net5.0 或 net6.0,VS2015 打不开;要跨版本共用,把目标框架降到 .NET Framework 4.6.2 再重新生成解决方案。

4.3 显示刷新频率、曲线采样点数与缓冲区上限的配合

串口接收、界面显示、数据库写入三者频率要错开,不能绑在同一个心跳上。

环节频率理由
串口读取与帧解析DataReceived 触发串口来数据就要收,否则缓冲区溢出
曲线刷新100-500 ms 一次 Timer太快浪费 UI 线程,太慢曲线卡顿
MySQL 批量写入每 2 秒或积满 100 条减少提交次数,降低连接压力

这三组参数之间没有必须严格相等的关系。界面可以只显示最近 600 个点,数据库则永远全量保存。曲线上的点不代表历史数据的全部,只代表屏幕目前容纳的窗口。MySQL 写入缓冲的容量要比曲线窗口大得多,建议至少能容纳 2 个小时的数据,这样数据库短暂不可用时,恢复后还能把积压补写进去。

5. 历史数据查询与回放:把 MySQL 数据还原成曲线

5.1 原始数据查询与分页:先按时间过滤再排序

历史数据查询的目的不外乎两个:在表格里看明细,或者把某段时间重新画成曲线。最常写的查询是按设备和时间过滤。

SELECT device_id, temperature, humidity, sample_time FROM temp_hum WHERE device_id = '01' AND sample_time BETWEEN '2025-06-01 00:00:00' AND '2025-06-01 23:59:59' ORDER BY sample_time LIMIT 5000;

这里有两个容易踩的性能点。第一,ORDER BY 配合 LIMIT 时尽量用联合索引 idx_device_time,让排序直接走索引避免文件排序。第二,翻页不要用LIMIT 400000, 5000这种大偏移,MySQL 会先读 40 万行再丢弃前 40 万行,越翻越慢。正确做法是记录上一页最后一条的 sample_time,下一页用WHERE sample_time > 上次值继续。对温湿度历史表,一次拉回超过 5000 条也没有意义,界面上 5000 个点挤在一起什么也看不出来,精确时间段加限量数据才是历史表的正确打开方式。

5.2 按小时/按天聚合统计:避免一年数据全量拉取

要回答“过去三十天每天的最高温度是多少”这类问题,应该让数据库先聚合,而不是把所有原始记录拉回上位机在内存里算。

SELECT DATE_FORMAT(sample_time, '%Y-%m-%d') AS stat_day, ROUND(AVG(temperature), 2) AS avg_temp, ROUND(MAX(temperature), 2) AS max_temp, ROUND(MIN(temperature), 2) AS min_temp, ROUND(AVG(humidity), 2) AS avg_hum FROM temp_hum WHERE sample_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY stat_day ORDER BY stat_day;

这个查询的核心是让 MySQL 把一个月的数据压缩成 30 行,传输量和显示量都大幅降低。代价是 DATE_FORMAT 包裹了 sample_time 后,索引无法用于分组排序。这条 SQL 成为报表高频查询时,可以加一列 date_int(如 20250601)并在应用层同步维护,配合普通索引速度差距明显。MySQL 8.0 的函数索引也能解决,但会增大写入开销,先量化再决定。这类统计不需要 MySQL 存储过程,一条 SELECT 就能完成,存储过程只会让上位机和数据维护之间的沟通成本变高。

5.3 回放实现:定时器逐点推进 + DataGridView 联动高亮

历史回放的常见做法是把查询结果放进内存列表,界面计时器每 500 毫秒往曲线里推一个点,同时把表格当前行选中并滚动到可见位置。

List<TempHumRecord> history; int playIndex = 0; void PlaybackTimer_Tick(object sender, EventArgs e) { if (playIndex >= history.Count) { playbackTimer.Stop(); return; } var row = history[playIndex++]; AppendToChart(row.SampleTime, row.Temperature, row.Humidity); grid.Rows[playIndex - 1].Selected = true; if (grid.FirstDisplayedScrollingRowIndex < playIndex - 1) grid.FirstDisplayedScrollingRowIndex = playIndex - 1; }

回放时间和真实时间的比例由计时器间隔控制,500 毫秒推一个点适合观察变化,想快进就每次推 5 个点,进度条提供倍速选择。AppendToChart 里 600 个点数上限对回放同样生效,长历史回放到后半段时,曲线窗口会自动滑动,这正是“最近 600 个点”窗口语义的复用。表格高亮行号要在历史查询结果里维护,不能拿数据库自增 id 当表格行号用。走到这一步,历史数据查询已经从“能查到”变成了“能演示”,整条链路的功能闭环就完整了。

6. 串口、Modbus、MySQL 联合排错与运行期验证

6.1 常用故障现象与对应处理思路

现象原因处理
串口打开失败CH340 驱动未装好,或端口被其他程序占用设备管理器确认 COM 口存在,先关闭占用程序
串口烧写失败上位机持续占用端口,下载器无法独占烧写前退出上位机监听,下位机重新上电
请求总是超时波特率、从站地址、寄存器类型不匹配用串口调试助手或 Modbus 调试工具逐项核对
温度显示异常大数响应帧没先做 CRC 和长度校验先校验再拼值,按5 + 2 * count计算帧长
写 MySQL 偶发超时Max Pool Size 太小,或端口被系统策略拦截按第 3 章配置连接池,确认采集机到数据库的 3306 连通性
历史曲线有空洞采集进程重启期间采样丢失用 6.2 的 SQL 定位缺口,补断点续采逻辑

排错保持先后顺序:先确认串口层通,再确认 Modbus 能读到寄存器,最后查 MySQL 写入。顺序颠倒会浪费大量时间,CRC 还没算对就去排查数据库连接串是最常见的无效操作。

6.2 用一条 SQL 验证写入链路是否完整

判断采集系统隔夜运行是否正常,不需要盯着界面看一夜。用一条聚合 SQL 就能评估昨晚的写入完整性。

SELECT COUNT(*) AS actual_rows FROM temp_hum WHERE sample_time >= '2025-06-01 00:00:00' AND sample_time < '2025-06-02 00:00:00' AND device_id = '01';

如果配置是 1 秒采样一次,实际行数应该是 86400,允许一次重启导致的少量丢失。偏差过大时再按小时分组定位缺失时间段。用 MySQL Workbench 跑这条 SQL 并看执行计划,比命令行直观,也能确认索引有没有被正确使用。

6.3 长期运行的验收手段

最后一个经验是:上位机程序不要只在开发机上跑两小时就上线。至少做一次 24 小时连续运行,用 6.2 的 SQL 检查写入量,再观察内存占用是否随运行时间持续上涨,确认队列没有在没人注意时偷偷打满。用串口调试助手挂在边上观察是否有持续的解码异常,异常帧率应该低于千分之一。再把下位机断电再上电模拟现场断电,验证上位机在串口重连后能继续采集,不需要人工重启。把“下位机断电再上电”写进运维手册,现场人员照着做一次,比任何说明都管用。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 14:57:40

微信小程序地图打卡开发实战:从云函数到真机调试

简介&#xff1a;面向微信小程序开发学习者的“滴滴打卡”源码包&#xff0c;适合正在准备毕业设计或期末大作业的学生参考。资源以原生小程序结构组织&#xff0c;包含完整的前端页面、逻辑与样式代码&#xff0c;涵盖打卡类应用常见的功能模块与交互流程&#xff0c;可直接运…

作者头像 李华
网站建设 2026/9/13 14:56:34

Spring Boot轻量CRM骨架:JPA+Thymeleaf实战解析

简介&#xff1a;本资源是一套基于Java与HTML实现的轻量级CRM客户关系管理系统源码&#xff0c;面向Java初学者、Web开发入门者及中小企业信息化建设人员&#xff0c;聚焦客户信息采集、分类管理、交互记录与基础数据分析等核心需求&#xff0c;助力快速理解企业级客户管理系统…

作者头像 李华
网站建设 2026/9/13 14:56:06

Archon 核心概念详解:Workflow、Node、Command 与隔离机制

Archon 核心概念详解&#xff1a;Workflow、Node、Command 与隔离机制 【免费下载链接】Archon The first open-source harness builder for AI coding. Make AI coding deterministic and repeatable. 项目地址: https://gitcode.com/GitHub_Trending/archon3/Archon A…

作者头像 李华