首页 / 资讯中心 / 文章详情

Python水质检测上位机:串口采集、SQLite存储与实时曲线

Python水质检测上位机:串口采集、SQLite存储与实时曲线 ★ FEATURED ARTICLE
简介水质检测.rar 面向环境科学、数据分析与机器学习初学者及科研人员围绕水质安全影响因素分析与预测提供从数据预处理到多模型对比的一套轻量实践材料。压缩包共10个文件以8个Python脚本为主辅以1份CSV水质参数数据和1份开题报告文档整体约221KB便于下载后在本地复现实验。CSV涵盖pH值、溶解氧、氨氮、浊度、电导率等常用指标脚本涉及数据清洗、缺失值与异常值处理、特征工程及时间序列处理并给出XGBoost、随机森林、LightGBM、KNN、AdaBoost等算法的对比实验代码可用于观察不同模型在水质预测任务中的表现差异。开题报告则帮助梳理研究背景、目标、方法选择与实验设计。已有195人学习适合用作课程设计、毕业设计前期的方案参考也可帮助环境监测人员理解机器学习在水质预测中的落地流程。1. 从一沓手写台账说起这套水质检测包能替掉哪些活去年帮人看一个小型水站的记录本A4 纸订成册每页写着日期、pH、浊度、谁测的。翻到中间有三天是空白问起来才知道那几天电极线松了读数一直跳索性就不记了。纸质台账最要命的地方不是写起来麻烦是它没有后悔药数据丢了就是丢了你想回头查换滤芯前后浊度有没有变化只能靠回忆。这套水质检测.rar 解决的就是这一段——把 pH、浊度、TDS、温度这些探头的读数自动采上来、存进本地库、画成能回看的曲线超限了给个提示而不是靠人隔两个小时跑过去抄一次。它适合三类人做课程设计或毕业设计、需要一套能跑通完整采集链路的同学手上已经有传感器、缺上位机软件的自搭水站玩家以及想把手工记录换成电子台账、又不想上云的小型站点。整套东西是单机形态不依赖服务器插上串口就能用这对没有运维的环境来说反而是优点。2. 资源构成与技术选型四层结构如何把传感器读数送到屏幕上先把这份包的整体骨架说清楚不然后面改代码会找不到落点。它按「采集 → 解析 → 存储 → 呈现」切成四层层与层之间用队列和函数调用隔开没有互相 import 成一团。这种切法不是什么架构美学是踩出来的传感器读数一旦和界面刷新写在一个循环里串口一卡窗口就假死任务管理器里只能强杀。2.1 目录里都有什么包解压之后大致是下面这个形状不同版本的文件名可能略有出入但职责划分基本一致水质检测/ ├── main.py # 入口读配置、装配线程、拉起窗口 ├── config.yaml # 串口参数、采样周期、告警阈值改这里不用动代码 ├── core/ │ ├── serial_reader.py # 串口线程只负责把字节流切成完整帧 │ ├── protocol.py # 帧解析、校验、单位换算 │ ├── repository.py # SQLite 建表与读写 │ └── alarm.py # 阈值判定、去抖、告警分发 ├── ui/ │ ├── main_window.py # 主窗口与菜单 │ ├── chart_view.py # 实时曲线控件 │ └── history_view.py # 历史查询与导出 ├── tools/ │ └── fake_source.py # 没有硬件时的模拟数据源 ├── data/ # 运行时生成 water.db ├── requirements.txt └── README.md文件/目录职责二次开发时改它的频率config.yaml串口、阈值、采样周期高几乎每次部署都改core/protocol.py帧格式与字段换算高换探头必改core/serial_reader.py字节流切帧、断线重连低逻辑稳定core/repository.py建表、批量写入、查询中加参数要加列ui/chart_view.py曲线刷新与坐标范围中加参数要加曲线tools/fake_source.py造数据联调必备低但很值得读提示先看config.yaml和protocol.py这两个文件基本就能判断这套包能不能直接对接你手上的硬件其他文件可以晚点再看。2.2 技术栈为什么是这几样上位机用 Python串口靠pyserial本地库用标准库自带的sqlite3界面走PyQt5加pyqtgraph画曲线导出报表用pandas最后PyInstaller打成一个 exe 丢给现场。这套组合的共同点是「装得少、部署轻」SQLite 不需要单独起服务一个文件就是一个库拷走就能备份pyqtgraph画几千个点的动态曲线比 matplotlib 顺得多后者更适合出静态图。有人会问为什么不上 MySQL 或者时序库。小型站点的现实是现场那台机器常年没人维护多一个服务就多一个半夜挂掉的可能。日均几千条记录的量级SQLite 开着 WAL 模式完全扛得住写入是毫秒级的事。真要上多站点集中管理那才轮到换存储这时候repository.py单独抽出来的价值就体现出来了——只改这一个文件。如果这份包是 Web 形态的变体Flask 起服务、前端用 ECharts 画图结构上只有ui/这一层被替换core/里的采集、解析、入库三块原样不动。判断方法很简单看requirements.txt里有没有 flask有就是 Web 版。2.3 一次采样从串口到曲线的完整链路传感器输出的是模拟电压或数字信号下位机做完 ADC 之后按约定打包成帧从串口吐给上位机。包里常见的一种帧定义是这样总共 19 字节固定长度偏移长度含义说明01帧头固定0xAA11帧头固定0x5521设备地址多探头时用来区分31数据类型0x01表示采样数据4–74设备时间戳秒级小端无符号整数8–92pH实际值 ×100有符号10–112浊度实际值 ×10单位 NTU12–132TDS单位 mg/L14–152温度实际值 ×100161状态位bit0 电极异常bit1 校准过期171校验和第 2 到 16 字节累加取低 8 位181帧尾固定0x0D固定长度帧的好处是切分简单找到帧头、数够 19 字节、验校验和不合格就丢。代价是扩展性差加一路参数就得改帧长这一点在第 6 章会专门处理。整条链路的时序是采集线程阻塞读串口 → 切出完整帧塞进queue.Queue→ 解析线程取出解析成 dict → 一个分支进写库线程一个分支进界面刷新。用队列而不是回调是为了让慢的那一端写库、画图不拖累快的那一端读串口。3. 核心链路拆解串口取帧、协议校验、入库与告警判定这一章是整套东西最值得细看的部分也是最容易翻车的地方。串口读取看着就几行代码实际上半帧、粘包、拔线重连这些情况一个都跑不掉写得不严实跑一晚上早上起来就会发现曲线断了一截。3.1 串口读取与断线重连采集线程只干一件事把字节流切成完整帧。它不做解析也不碰数据库这样即使解析逻辑改坏了采集这一层还是干净的。import queue import threading import time import serial class SerialReader(threading.Thread): 把串口字节流切成完整帧塞进队列不做任何业务解析 def __init__(self, port: str, baudrate: int, out_q: queue.Queue[bytes], frame_head: bytes b\xaa\x55, frame_size: int 19): super().__init__(daemonTrue, namefreader-{port}) self.port port self.baudrate baudrate self.out_q out_q self.frame_head frame_head self.frame_size frame_size self._buf bytearray() self._stop threading.Event() def stop(self): self._stop.set() def run(self): # 外层 while 负责重连内层 while 负责读数据拔线后回到外层 while not self._stop.is_set(): ser self._open() if ser is None: time.sleep(2) # 打不开就退避别死循环刷日志把磁盘写满 continue try: while not self._stop.is_set(): chunk ser.read(ser.in_waiting or 1) if chunk: self._feed(chunk) except (serial.SerialException, OSError): pass # USB 被拔掉走这里直接进 finally 重连 finally: ser.close() def _open(self): try: return serial.Serial(self.port, self.baudrate, timeout0.5) except serial.SerialException: return None def _feed(self, chunk: bytes): self._buf.extend(chunk) while True: idx self._buf.find(self.frame_head) if idx 0: self._buf.clear() # 没有帧头说明全是噪声直接丢 return if idx 0: del self._buf[:idx] # 帧头之前的脏数据不要 if len(self._buf) self.frame_size: return # 半帧留着等下一批数据 frame bytes(self._buf[:self.frame_size]) del self._buf[:self.frame_size] self.out_q.put(frame)几个参数和设计点值得说明。timeout0.5让read不会永久阻塞线程才能响应stop()否则退出程序时窗口关了线程还挂着。ser.in_waiting or 1是常见的省事写法缓冲区里有数据就一次读完没有就阻塞读 1 字节避免空转占满 CPU。daemonTrue保证主程序退出时不留孤儿线程但对写库线程不能这么干那个必须优雅停止并 commit不然最后一批数据会丢。真正容易出问题的是_buf的清理时机。噪声大的现场比如探头线跟变频器走一个线槽帧头之前可能堆着几百字节的垃圾必须在find之后立即删掉否则内存只涨不降。另外如果下位机发送的帧长不固定这里就要改成先读长度字节再判断固定帧长是最省事的形态。3.2 帧解析与校验和拿到完整帧之后才做校验顺序不能反。先验帧头帧尾再验校验和最后才解析字段——校验不过的帧直接返回None绝不进库。import struct def calc_checksum(frame: bytes) - int: 第 2 到 16 字节累加取低 8 位 return sum(frame[2:17]) 0xFF def parse_frame(frame: bytes) - dict | None: if len(frame) ! 19 or frame[0] ! 0xAA or frame[1] ! 0x55: return None if frame[18] ! 0x0D or calc_checksum(frame) ! frame[17]: return None # IhhhH 共 12 字节时间戳4 pH2 浊度2 TDS2 温度2 ts, ph, ntu, tds, temp struct.unpack(IhhhH, frame[4:16]) return { device_id: f{frame[2]:02X}, ts: ts, ph: round(ph / 100.0, 2), turbidity: round(ntu / 10.0, 1), tds: float(tds), temp: round(temp / 100.0, 2), status: frame[16], }struct的格式串是小端解析的核心。表示小端I是无符号 4 字节h是有符号 2 字节H是无符号 2 字节。pH 用有符号是因为有些探头在校准时会输出负值用无符号就直接变成 65000 多了。缩放系数÷100、÷10写在解析层而不是界面层这样库里存的永远是物理量导出报表时不用再换算一遍。校验和用累加而不是 CRC16是这类小数据包常见的取舍出错概率低、计算量小下位机几个时钟周期就算完。但如果现场干扰特别强累加校验会漏掉某些双比特翻转那就得换 CRC16客户端和服务端一起改。字段返回dict而不是自定义类是为了后面写库和画图都能直接按键取值也方便 JSON 序列化。状态位单独留着电极异常的时候可以在界面上直接标红不用等到读数超限才发现探头已经没浸在水里了。3.3 建表与批量入库每条采样单独 commit 是最常见的性能坑一天下来几万次磁盘同步树莓派上能把 SD 卡写废。正确做法是攒一批再写。CREATE TABLE IF NOT EXISTS sample ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER NOT NULL, -- 设备或采集时间秒级 device_id TEXT NOT NULL, ph REAL, turbidity REAL, tds REAL, temp REAL, status INTEGER NOT NULL DEFAULT 0, level INTEGER NOT NULL DEFAULT 0 -- 0 正常 1 预警 2 报警 ); CREATE INDEX IF NOT EXISTS idx_sample_ts ON sample (ts); CREATE INDEX IF NOT EXISTS idx_sample_dev_ts ON sample (device_id, ts);import queue import sqlite3 import threading DDL open(core/schema.sql, encodingutf-8).read() class SampleWriter(threading.Thread): 单写线程独占一个连接批量提交 def __init__(self, db_path: str, in_q: queue.Queue[dict], batch: int 20, flush_sec: float 1.0): super().__init__(daemonFalse, namedb-writer) self.db_path db_path self.in_q in_q self.batch batch self.flush_sec flush_sec self._stop threading.Event() def stop(self): self._stop.set() def run(self): conn sqlite3.connect(self.db_path) conn.executescript(DDL) conn.execute(PRAGMA journal_modeWAL) # 读写并发不互相阻塞 conn.execute(PRAGMA synchronousNORMAL) # 掉电最多丢最后一批速度翻倍 buf [] while not self._stop.is_set() or buf: try: buf.append(self.in_q.get(timeoutself.flush_sec)) except queue.Empty: pass if len(buf) self.batch or (buf and self.in_q.empty()): conn.executemany( INSERT INTO sample (ts, device_id, ph, turbidity, tds, temp, status, level) VALUES (?,?,?,?,?,?,?,?), [(r[ts], r[device_id], r[ph], r[turbidity], r[tds], r[temp], r[status], r[level]) for r in buf], ) conn.commit() buf.clear() conn.close()WAL模式是 SQLite 在「一边写一边查」场景下的关键开关它让读操作不被写事务阻塞历史查询界面刷新时不会卡住采集。synchronousNORMAL是拿一点掉电安全性换吞吐对水质监测这种数据来说可以接受——丢最后一两秒的记录比卡顿一整晚强。daemonFalse配合stop()是故意的程序退出时要先停采集线程、再停写库线程让缓冲区里的数据落盘。顺序反了最后一批就永远留在内存里了。批量大小 20 条、1 秒兜底是按常见 1 秒一条的采样率定的采样更快就把batch调大。3.4 阈值判定与去抖告警不能一超限就弹窗模拟量探头本身就有几百分之几的抖动pH 在 6.5 边界上下飘的时候一晚上能给你弹一百次。加个连续计数去抖问题基本消失。# config.yaml 里对应的规则 # rules: # ph: [6.5, 8.5] # turbidity: [0, 1.0] # tds: [0, 1000] # temp: [0, 40] def judge(sample: dict, rules: dict, state: dict, need: int 3) - int: 连续 need 次越界才升级任一次回正常立即复位 hit 0 for name, (lo, hi) in rules.items(): v sample.get(name) if v is None: continue if v lo or v hi: state[name] state.get(name, 0) 1 else: state[name] 0 if state[name] need: hit 1 if hit 0: return 0 return 1 if hit 1 else 2state是一个跨调用保留的字典每个参数一个计数器这是去抖的全部秘密。need3意味着连续 3 个采样点都越界才报按 1 秒一条算就是 3 秒延迟对水质这种变化缓慢的对象完全够用。如果某些参数要求更灵敏比如余氯单独给它配need1就行把规则从二元组改成三元组即可。判定结果写回level字段同时推一条消息给界面。这里不要再加一层「告警处理器类」两三个函数就够过度设计在这个体量的项目里只会让后来接手的人更懵。4. 界面与可视化实时曲线、历史回溯与指标限值表界面这一层的目标很明确让值班的人扫一眼就知道现在正不正常点两下就能查到上周三下午的数据。花哨的仪表盘不如一条清晰的曲线加一个会变色的状态灯。4.1 实时曲线的刷新方式曲线最容易踩的坑是把刷新逻辑写进采集线程里跨线程直接操作控件跑着跑着就崩。稳妥的做法是界面用QTimer定时去队列里取数据采集那边完全不知道界面的存在。from collections import deque import pyqtgraph as pg from PyQt5 import QtCore, QtWidgets class PhChart(pg.PlotWidget): def __init__(self, window: int 600, parentNone): super().__init__(parent) self.setLabel(left, pH) self.setLabel(bottom, 采样点) self.setYRange(0, 14) self.showGrid(xTrue, yTrue, alpha0.3) self.curve self.plot(penpg.mkPen(#2e86de, width2)) self.buf deque(maxlenwindow) # 定长内存不会一直涨 def push(self, value: float): self.buf.append(value) self.curve.setData(list(self.buf)) class MainWindow(QtWidgets.QMainWindow): def __init__(self, data_q, interval_ms: int 500): super().__init__() self.data_q data_q self.chart PhChart() self.setCentralWidget(self.chart) self.timer QtCore.QTimer(self) self.timer.timeout.connect(self.drain) self.timer.start(interval_ms) # 界面 2Hz 刷新采集再快也不影响 def drain(self): latest None while True: try: latest self.data_q.get_nowait() except Exception: break if latest is not None: self.chart.push(latest[ph])deque(maxlen600)是定长队列超出自动丢最老的跑一个月内存也不会涨。界面刷新频率500ms和采样频率1s解耦是这套结构最实在的好处采样调快了界面不卡采样断了界面也不崩只是曲线不动。drain里那个while True是故意把队列抽干只显示最后一条。如果只取一条队列会越积越多最后内存爆掉。判断队列积压还有个土办法在状态栏显示队列长度超过 100 就说明写库或解析跟不上了得查。Web 版本的对应位置是前端setInterval定时拉/api/latest后端从同一个队列里取逻辑完全一样只是把QTimer换成了浏览器定时器。4.2 历史查询与报表导出历史页面的核心是一条带时间范围条件的查询。索引建在ts上几百万行也是毫秒级。import pandas as pd def query_range(conn, start_ts: int, end_ts: int, device_id: str | None None): sql (SELECT ts, device_id, ph, turbidity, tds, temp, level FROM sample WHERE ts BETWEEN ? AND ?) args [start_ts, end_ts] if device_id: sql AND device_id ? args.append(device_id) sql ORDER BY ts ASC return pd.read_sql_query(sql, conn, paramsargs) def export_csv(df: pd.DataFrame, path: str): out df.copy() out[时间] pd.to_datetime(out[ts], units, utcTrue).dt.tz_convert(Asia/Shanghai) out out.drop(columns[ts]) out.to_csv(path, indexFalse, encodingutf-8-sig) # 带 BOMExcel 打开不乱码utf-8-sig这个细节坑过不少人不加 BOMExcel 双击打开中文列名就是乱码现场的人会以为数据坏了。utcTrue再tz_convert是为了处理跨时区的情况单站点可以简化成pd.to_datetime(df[ts], units)。查询结果直接给pandas是为了导出和统计日均值、最大值都方便但如果你的环境里不想装 pandas用csv模块手写循环也完全可行就是代码长一点。4.3 参数与限值对照表界面上的颜色和报警都依赖这张表改阈值只改配置不要散落在代码里参数常见参考限值单位超限最常见的原因pH6.5 – 8.5无量纲电极未校准、参比液干涸浊度≤ 1.0NTU探头窗口结垢、气泡附着TDS≤ 1000mg/L探头常数设置错、水温补偿缺失温度0 – 40℃探头脱落、线缆断芯电极状态位应为 0—校准过期、信号超量程注意表里的限值只是常见参考区间不同用途的水标准不一样正式使用前请按你所在场景的实际要求核对别直接照抄。5. 常见问题排查串口、毛刺、时间戳与打包的五个坑这套东西跑通不难难的是跑稳。下面五条基本覆盖了我在类似项目里遇到的大半问题按「现象 → 原因 → 解决」写照着对号入座就行。5.1 从零启动的四步先把环境跑起来再谈别的。没硬件的时候用包里的fake_source.py造数据能省掉一大半联调时间。# 1. 建虚拟环境别用系统 Python 直接装 python -m venv .venv source .venv/bin/activate # Windows 下是 .venv\Scripts\activate # 2. 装依赖 pip install -r requirements.txt # 3. 没有硬件起一个模拟数据源往虚拟串口里灌帧 # Windows 用 com0com 建一对虚拟串口Linux 用 socat socat -d -d pty,raw,echo0 pty,raw,echo0 python tools/fake_source.py --port /dev/pts/3 --interval 1.0 # 4. 把主程序指向另一端 python main.py --config config.yaml模拟数据源的核心就是按 3.2 节的帧格式反向打包造出带毛刺、带偶发超限的数据用来验证去抖和告警是否生效import random import struct import time import serial def build_frame(ph: float, ntu: float, tds: float, temp: float, addr: int 0x01) - bytes: body struct.pack(IhhhH, int(time.time()), int(ph * 100), int(ntu * 10), int(tds), int(temp * 100)) frame b\xAA\x55 bytes([addr, 0x01]) body b\x00 assert len(frame) 17, len(frame) return frame bytes([sum(frame[2:17]) 0xFF, 0x0D]) if __name__ __main__: ser serial.Serial(/dev/pts/4, 9600) while True: # 在 7.0 附近加噪声偶尔串到 9.0 触发一次告警 ph 7.0 random.gauss(0, 0.15) (2.0 if random.random() 0.02 else 0) ser.write(build_frame(ph, 0.3 random.random() * 0.5, 220, 24.5)) time.sleep(1)assert len(frame) 17这行别删改协议的时候它是第一道防线帧长算错了立刻报错比上位机那边收到一堆解析失败的None好查得多。5.2 五个高频坑的定位与处理坑一串口打不开提示 Permission denied 或拒绝访问。现象是程序一启动就退出日志里只有一句SerialException。原因是 Linux 下当前用户不在dialout组或者 Windows 下串口被另一个软件串口助手、厂商的上位机占着。解决Linux 执行sudo usermod -aG dialout $USER后重新登录Windows 先关掉其他串口工具设备管理器里看看是不是被识别成了两个 COM 口有些 CH340 驱动会这样。这个坑属于玄学范畴插不同的 USB 口表现还不一样建议在_open失败时把可用串口列表打出来from serial.tools import list_ports print([p.device for p in list_ports.comports()])坑二曲线毛刺pH 时不时跳到 1 或者 13。现象是数据里混着明显不合理的尖峰一天能有几十个。原因是模拟量探头共地没做好、信号线跟电源线捆在一起或者电极表面结垢导致接触电阻变化。解决硬件上把探头地和主板地单点连接、信号线用屏蔽线且屏蔽层单端接地软件上在解析层之后加一个中值滤波窗口取 5能滤掉绝大部分孤立尖峰比滑动平均更保边。别用平均平均会把真实的突变抹平反而漏掉污染事件。坑三时间戳错乱历史曲线的时间轴对不上。现象是查昨天下午的数据结果显示在凌晨或者两个探头的记录差了 8 小时。原因分两种一是ts用的是设备时间而设备没有 RTC、断电重启后从 0 开始二是存的是 UTC 秒展示时忘了做时区转换。解决入库前统一用采集端时间覆盖设备时间前提是采集机时间准建议开 NTP并在字段名上标明ts_utc展示层统一在一处做转换别每个界面各转一次那种写法必出不一致。坑四打包成 exe 之后一启动就闪退。现象是源码跑得好好的PyInstaller打完双击没反应。原因十有八九是相对路径源码里的open(config.yaml)在 PyInstaller 解压的临时目录里找不到文件。解决所有资源路径走统一入口——import sys from pathlib import Path def res_path(name: str) - Path: base Path(getattr(sys, _MEIPASS, Path(__file__).resolve().parent)) return base / name def data_path(name: str) - Path: 可写数据放 exe 同级的 data 目录不要放 _MEIPASS那是临时的 base Path(sys.executable).parent if getattr(sys, frozen, False) \ else Path(__file__).resolve().parent p base / data / name p.parent.mkdir(parentsTrue, exist_okTrue) return p配置文件和图标用res_path数据库和导出文件用data_path。这两个混用就会出现「配置读到了、数据库写完重启就没了」的诡异现象。坑五跑一段时间报 database is locked。现象是采集正常但历史查询偶尔报错或者写库线程卡住。原因是多个线程共用一个sqlite3连接或者事务没提交就开了新事务。解决写库线程独占一个连接3.3 节就是这么做的读操作各自开连接或者走连接池必须共用时加check_same_threadFalse并配一把锁但更推荐直接按线程分开。另外记得开 WAL很多人以为开了没用其实是开在了错误的连接上——PRAGMA是连接级的每个新连接都要执行一次。6. 二次开发给系统加一路新参数从协议表改到界面最常见的改造需求就是加探头比如再上一路余氯或者溶解氧。如果按前面的写法硬改你会同时动到解析函数、建表语句、插入语句、曲线控件、导出列五六个地方改漏一个就出问题。我一般会先把参数抽成一张注册表# core/params.py PARAMS { ph: {offset: 8, fmt: h, scale: 0.01, unit: , range: (0, 14)}, turbidity: {offset: 10, fmt: h, scale: 0.1, unit: NTU, range: (0, 1000)}, tds: {offset: 12, fmt: h, scale: 1.0, unit: mg/L, range: (0, 2000)}, temp: {offset: 14, fmt: H, scale: 0.01, unit: ℃, range: (0, 60)}, # 新增一路余氯改下位机帧长这里加一行其他地方基本不用动 # chlorine: {offset: 16, fmt: H, scale: 0.001, unit: mg/L, range: (0, 5)}, }解析函数改成遍历注册表struct.unpack_from按偏移取值加参数时帧长和校验范围跟着调一下就行。建表语句用 , .join(f{k} REAL for k in PARAMS)动态拼入库的INSERT同理用参数名列表拼占位符。前端按PARAMS的 key 循环生成曲线控件和 Y 轴标签加一路就自动多一条线。改完之后的验证顺序我固定成三步先跑fake_source.py造新参数的数据确认解析出来的值和造的值对得上差一个量级就是scale写错了再开历史页面确认新列进了表和导出文件最后拔一次串口看重连之后新参数是否还能继续写入。第三步最容易忘也最容易出问题——很多人的新参数只在启动时注册了一次重连后初始化没走字段就一直是空的。从那以后我每接一个新探头都先把它的量程、单位、零点偏移记进params.py再动别的地方绝不先在界面里手写一遍曲线。这个顺序反过来血泪经验就是界面能画出来但库里没存等要出报表的时候才发现历史数据缺一路那时候只能重采。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站