简介:这份资源是面向自动化、机械设计制造及仪器仪表相关专业学生与工程技术人员的毕业论文文档,聚焦加油站信息化管理中的数据采集与状态监控系统设计,可帮助读者理解上位机、数据采集器、加油机与液位仪之间的协同架构,以及从人工记录向信息化管理过渡的完整思路。压缩包内共1个docx文件,约193KB,内容为完整的毕业论文正文,涵盖摘要、系统总体方案、数据采集器设计与实现、上位机数据采集程序、加油站监控程序及通信协议设计等章节,并配有中英文摘要与关键词。文中重点讨论了数据采集器稳定性、通信速度与可靠性要求,以及通过动态库适配不同加油机协议的模块化程序设计方法,对油罐库存实时监控与安全泄漏检测也有涉及。目前已有106人学习下载,适合需要参考同类课题结构、撰写毕业论文或了解加油站监控系统实现路径的读者。
1. 加油站数据采集与状态监控系统:从液位仪到上位机的一条完整链路
一辆油罐车卸完油,站长打开电脑,想确认 3 号罐现在到底有多少升 92 号汽油。这个看似简单的动作,背后要串起液位仪、加油机、控制主板、通信网关和一台常年不关机的上位机。加油站数据采集与状态监控系统要解决的,就是把分散在罐区、加油岛、配电间的数据统一收上来,实时判断液位、温度、水位、加油量是否正常,并在异常时第一时间报警。它适合做工业数据采集的工程师、做 SCADA 上位机开发的程序员,以及需要给油站做数字化改造的集成商。热搜里常出现的 SCADA、上位机、液位仪、数据采集这几个词,正好对应这条链路的四个关键环节。下面我按自己实际搭过的一套方案,把选型、通信、组态、入库和排错讲清楚,新手能照着复现,熟手能看到参数边界和踩坑点。
2. 加油站数据采集的现场对象与通信选型:先搞清楚采什么、怎么采
2.1 液位仪、加油机、控制主板三类数据源的区别
加油站现场的数据源大致分三类,通信方式和采集频率完全不同,混在一起设计是很多项目翻车的起点。
第一类是液位仪,装在油罐顶部,负责测量油高、水位、温度,部分型号还能算体积和密度。它通常通过 RS-485 或 RS-232 串口输出,协议多为厂家私有协议或标准 Modbus RTU。采集频率不需要太高,5 到 30 秒一次足够,因为液位变化本身是缓慢过程。
第二类是加油机,每把枪的加油量、金额、升数由加油机主板产生。它一般通过厂家提供的通信板卡输出,常见的是 RS-485 或以太网,协议有私有协议,也有支持 Modbus 的型号。这个数据要求实时性高,每笔加油交易都要准确抓到,不能丢单。
第三类是控制主板或 PLC,负责配电、照明、潜油泵等设备状态。这类通常用 Modbus TCP 或 Modbus RTU,采集开关量、电流、电压等。频率可以低一些,10 到 60 秒一次。
选型时先确认每类设备的物理接口和协议,再决定用串口服务器还是直接走网口。常见做法是液位仪和加油机走 RS-485 总线,通过串口服务器转成以太网,统一进上位机。注意一条 485 总线上挂的设备不要超过 32 个,距离超过 800 米要加中继器。
2.2 用 Python 写一个 Modbus RTU 液位仪采集最小示例
下面这段代码是我在本地验证液位仪通信时最常用的最小示例,用 pymodbus 库读保持寄存器。实际项目里我会把它封装成采集线程,这里先跑通单次读取。
from pymodbus.client import ModbusSerialClient import time # 配置串口参数,液位仪常见为 9600 8N1 client = ModbusSerialClient( port='COM3', # Windows 下是 COMx,Linux 下是 /dev/ttyUSB0 baudrate=9600, # 波特率必须和液位仪一致 bytesize=8, parity='N', stopbits=1, timeout=1 # 超时 1 秒,现场干扰大时可放宽到 2 秒 ) # 连接串口 if not client.connect(): print('串口连接失败,检查线序和端口号') exit() # 液位仪从站地址假设为 1,读取 0x0000 开始的 4 个寄存器 # 不同厂家寄存器地址不同,必须查对应手册 try: rr = client.read_holding_registers(address=0, count=4, slave=1) if rr.isError(): print('读取错误,检查从站地址和寄存器地址') else: # 假设寄存器 0 是油高,单位 mm,需要除以 10 得到 cm oil_height = rr.registers[0] / 10.0 # 寄存器 1 是水位,单位 mm water_height = rr.registers[1] / 10.0 # 寄存器 2 是温度,单位 0.1 摄氏度 temperature = rr.registers[2] / 10.0 print(f'油高: {oil_height} cm, 水位: {water_height} cm, 温度: {temperature} ℃') except Exception as e: print(f'采集异常: {e}') finally: client.close()这段代码的逻辑是:先建立串口连接,再按从站地址和寄存器地址读取原始数据,最后按厂家手册的缩放系数换算成工程值。参数说明里最关键的是baudrate、parity和slave,这三个只要有一个不对,读出来就是乱码或超时。寄存器地址和缩放系数必须查液位仪手册,不同品牌差异很大,不要凭经验猜。实际部署时我会把这段逻辑放进一个循环,每 10 秒采集一次,并把结果写入本地缓存,防止网络中断丢数据。
2.3 加油机数据采集的协议适配与轮询策略
加油机比液位仪麻烦,因为很多型号不直接支持标准 Modbus,而是用厂家私有协议。常见做法是找厂家要通信协议文档,或者用厂家提供的通信板卡,板卡会把加油数据转成标准协议输出。
如果加油机支持 Modbus RTU,轮询策略要注意:不要用太短的轮询间隔,否则会影响加油机主板正常出油。我一般设 200 到 500 毫秒轮询一次,只读交易状态和当前枪号,交易完成后读一次完整数据。如果加油机是私有协议,通常需要厂家提供 DLL 或协议说明,用 C# 或 Python 按帧格式解析。
这里有个血泪经验:加油机通信口和液位仪通信口不要挂在同一条 485 总线上。加油机通信数据量大、实时性高,液位仪是慢速设备,混在一起容易互相干扰,表现为液位仪偶尔超时、加油数据偶尔丢帧。分开两条总线,各自接串口服务器,上位机开两个采集线程,稳定性会好很多。
3. 上位机与 SCADA 组态:把采集到的数据变成能看的画面
3.1 上位机选型:组态软件还是自研 C#/Qt 程序
采集层跑通后,下一步是把数据展示出来。这里有两个方向:用现成 SCADA 组态软件,或者自己写上位机。
组态软件比如组态王、中控 SCADA、国产 SCADA 平台,优点是开发快,拖拽控件就能画出油罐、加油机、管线的组态图,报警、趋势、报表都是现成的。缺点是授权费用不低,定制化能力有限,遇到特殊协议或特殊算法时不好扩展。
自研上位机用 C# WinForm/WPF 或 Qt,优点是灵活,想怎么画就怎么画,想接什么数据库就接什么数据库。缺点是开发周期长,报警、历史存储、权限管理这些都要自己写。热搜里常出现的 C# 上位机通用框架、Qt 上位机通信,说的就是这个方向。
我的建议是:如果项目周期短、预算够,优先用组态软件,把精力放在通信和现场调试上。如果项目需要深度定制,比如要和油站已有的 ERP 对接、要做复杂的库存预测,那就自研。很多集成商的做法是混合:用组态软件做画面和报警,用自研程序做数据转发和业务逻辑。
3.2 用组态王或中控 SCADA 画一张油罐组态图的关键步骤
以组态软件为例,画一张油罐监控画面的步骤大致如下。不同软件菜单名称略有差异,但逻辑相通。
第一步,建工程并配置设备驱动。在设备管理中新增 Modbus RTU 或 Modbus TCP 驱动,填入串口服务器 IP、端口、从站地址。如果是 Modbus TCP,端口一般是 502。
第二步,建数据词典。把液位仪的油高、水位、温度,加油机的枪号、升数、金额,控制主板的开关状态,逐个定义成变量。变量名建议用英文加下划线,比如tank1_oil_height,方便后续脚本引用。
第三步,画组态画面。用矩形和圆弧画出油罐轮廓,用填充属性绑定油高变量,填充高度按油高比例变化。加油机可以用图片或简单图形表示,旁边放文本框显示当前升数和金额。
第四步,配置报警。给油高设上下限,比如超过 90% 高报,低于 10% 低报;给水位设高报,超过 50mm 报警。报警输出可以弹窗、变色、写数据库。
第五步,配置历史趋势。把油高、温度、水位加到趋势曲线里,采样周期设 1 分钟,存到组态软件自带的历史库或外部 SQL 数据库。
这里有个注意点:组态软件的填充动画通常要求变量是 0 到 100 的百分比,而液位仪输出的是毫米或厘米,需要在数据词典里做线性变换。变换公式写错,画面就会显示满罐或空罐,现场调试时容易被吓一跳。
3.3 自研上位机用 C# 做实时数据刷新和报警的骨架
如果选择自研,下面是一个 C# 上位机数据刷新和报警的简化骨架,用 WinForm 加定时器实现。
using System; using System.Windows.Forms; using System.IO.Ports; public partial class MainForm : Form { private SerialPort _serialPort; private Timer _pollTimer; private double _oilHeight; private const double HighAlarm = 90.0; // 高报阈值 90% private const double LowAlarm = 10.0; // 低报阈值 10% public MainForm() { InitializeComponent(); // 初始化串口,参数与液位仪一致 _serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One); _serialPort.ReadTimeout = 1000; _serialPort.Open(); // 定时器每 2 秒轮询一次 _pollTimer = new Timer(); _pollTimer.Interval = 2000; _pollTimer.Tick += PollTimer_Tick; _pollTimer.Start(); } private void PollTimer_Tick(object sender, EventArgs e) { try { // 发送 Modbus RTU 读寄存器请求帧,这里省略帧构造细节 byte[] request = BuildModbusRequest(1, 0, 4); _serialPort.Write(request, 0, request.Length); // 读取响应,按协议解析出油高 byte[] buffer = new byte[256]; int len = _serialPort.Read(buffer, 0, buffer.Length); _oilHeight = ParseOilHeight(buffer, len); // 更新界面,注意跨线程调用要用 Invoke this.Invoke(new Action(() => { lblOilHeight.Text = _oilHeight.ToString("F1") + " cm"; // 报警判断 if (_oilHeight >= HighAlarm) { lblAlarm.Text = "高液位报警"; lblAlarm.ForeColor = System.Drawing.Color.Red; } else if (_oilHeight <= LowAlarm) { lblAlarm.Text = "低液位报警"; lblAlarm.ForeColor = System.Drawing.Color.Red; } else { lblAlarm.Text = "正常"; lblAlarm.ForeColor = System.Drawing.Color.Green; } })); } catch (TimeoutException) { // 超时记录日志,不弹窗,避免频繁打扰 Log("采集超时"); } catch (Exception ex) { Log("采集异常: " + ex.Message); } } private byte[] BuildModbusRequest(byte slave, ushort start, ushort count) { // 实际项目按 Modbus RTU 帧格式构造,含 CRC 校验 // 这里只做示意,完整实现需计算 CRC16 return new byte[] { slave, 0x03, (byte)(start >> 8), (byte)start, (byte)(count >> 8), (byte)count, 0x00, 0x00 }; } private double ParseOilHeight(byte[] buffer, int len) { // 按液位仪协议解析,假设第 3、4 字节是油高原始值 if (len < 5) return 0; int raw = (buffer[3] << 8) | buffer[4]; return raw / 10.0; } private void Log(string msg) { // 写本地日志文件,便于事后排查 System.IO.File.AppendAllText("collect.log", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " " + msg + Environment.NewLine); } }这段代码的关键点有三个:一是串口参数必须和液位仪一致;二是定时器轮询间隔不要设太短,2 秒对液位采集足够;三是跨线程更新界面必须用Invoke,否则会抛异常。参数说明里HighAlarm和LowAlarm按油罐实际安全容量设定,一般高报 90%、低报 10%,但不同油站要求不同,要按现场规定调整。实际项目里我会把采集和界面分开,采集放后台线程,界面只负责显示,避免界面卡顿影响采集。
4. 数据存储、报警联动与远程监控:让系统真正可用
4.1 用 SQL Server 或 MySQL 存历史数据的表结构设计
采集和显示跑通后,数据要存下来,否则只能看实时值,没法查历史、做报表。数据库选 SQL Server 或 MySQL 都行,油站这种规模用 MySQL 足够。
核心表设计三张:实时表、历史表、报警表。
实时表只存每个测点的最新值,字段包括测点编号、测点名称、当前值、单位、更新时间。这张表数据量小,上位机每次采集后更新。
历史表存时序数据,字段包括自增 ID、测点编号、测点值、采集时间。这张表增长快,建议按天或按月分区,或者定期归档。采集周期 1 分钟的话,一个油站 20 个测点,一天约 2.8 万条,一年约 1000 万条,MySQL 单表扛得住,但查询要加时间索引。
报警表存报警记录,字段包括报警 ID、测点编号、报警类型、报警值、报警时间、确认时间、确认人。报警产生时插入一条,确认时更新确认字段。
下面是一个建表 SQL 示例。
-- 实时数据表 CREATE TABLE realtime_data ( point_id VARCHAR(32) PRIMARY KEY, point_name VARCHAR(64), current_value DECIMAL(10,2), unit VARCHAR(16), update_time DATETIME ); -- 历史数据表,按采集时间建索引 CREATE TABLE history_data ( id BIGINT AUTO_INCREMENT PRIMARY KEY, point_id VARCHAR(32), point_value DECIMAL(10,2), collect_time DATETIME, INDEX idx_point_time (point_id, collect_time) ); -- 报警记录表 CREATE TABLE alarm_record ( alarm_id BIGINT AUTO_INCREMENT PRIMARY KEY, point_id VARCHAR(32), alarm_type VARCHAR(32), alarm_value DECIMAL(10,2), alarm_time DATETIME, confirm_time DATETIME NULL, confirm_user VARCHAR(32) NULL );参数说明:DECIMAL(10,2)表示总共 10 位、小数 2 位,对液位和温度够用。idx_point_time索引是为了按测点加时间范围查询时走索引,不然历史查询会越来越慢。实际项目里我会再加一张班次表,把加油量和班次关联,方便做交接班报表。
4.2 报警联动:声光报警、短信通知和自动断泵的逻辑
报警不能只在上位机上弹个窗,现场没人盯着屏幕。常见做法是三级联动。
第一级是上位机声光报警,用音箱放报警音,屏幕弹窗变色。这个最简单,但依赖有人在场。
第二级是短信或语音通知,通过短信猫或云短信接口发给站长和值班人员。注意短信接口要有重试机制,发送失败要记录,不能因为短信通道故障导致报警丢失。
第三级是自动断泵,高液位报警时自动停止潜油泵,防止溢罐。这个逻辑要慎重,必须加延时确认,比如液位连续 3 次超过高报阈值才断泵,避免液位波动误动作。断泵输出一般通过 PLC 或继电器板卡实现,上位机写一个输出点,硬件执行。
这里有个后悔药:报警阈值不要设得太灵敏,液位在卸油时会剧烈波动,如果一超就报,值班人员会被频繁打扰,最后干脆把报警关掉。我一般设两级阈值,高报 90% 提醒,高高报 95% 才断泵,中间留缓冲。
4.3 远程监控的组网方式和数据同步注意点
油站通常分布在各地,总部要看各站数据,就需要远程监控。常见组网方式有两种。
一种是油站上位机通过有线宽带或 4G 路由器把数据推到总部服务器。上位机本地存一份,同时按分钟级增量同步到总部数据库。这种方式对网络要求低,断网时本地继续采集,恢复后补传。
另一种是总部直接通过专线或组网设备访问油站上位机,实时读取。这种方式实时性好,但依赖网络稳定,断网时总部就瞎了。
我一般用第一种,本地优先,远程同步。同步时注意两点:一是时间戳要用油站本地时间,不要用总部时间,否则历史数据时间会乱;二是增量同步要记录最后同步 ID,避免重复插入。断网补传时按时间范围查本地历史表,批量插入总部库,插入前先按测点加时间做去重。
5. 避坑与排查:加油站数据采集系统最常见的 5 个翻车现场
5.1 液位仪读数跳变或恒定不变
现象:上位机显示的油高一会儿正常,一会儿跳到 0 或满量程,或者一直不变。
原因:串口线屏蔽层没接地,现场变频器或潜油泵干扰;或者寄存器地址读错,读到了保留寄存器;或者缩放系数用错,把原始值当工程值。
解决:先查线,485 线要用双绞屏蔽线,屏蔽层单端接地。再用串口调试工具单独读液位仪,确认原始值是否稳定。如果原始值稳定但上位机跳变,检查解析代码的字节序和缩放系数。最后在采集程序里加滑动平均滤波,连续 3 次取中间值,能压掉大部分毛刺。
5.2 加油机丢单或重复计数
现象:某笔加油交易没抓到,或者同一笔交易被记了两次。
原因:轮询间隔太长,交易完成后数据被覆盖;或者交易状态判断逻辑有误,把进行中的交易当成完成;或者网络中断导致数据重传。
解决:加油机轮询间隔不要超过 500 毫秒,交易完成后立即读一次完整数据。交易状态要按厂家协议判断,通常有明确的完成标志位。上位机记录每笔交易的唯一流水号,入库前先查重。网络中断时本地缓存,恢复后按流水号去重补传。
5.3 组态画面数据不刷新或显示问号
现象:组态软件画面上油罐填充不动,或者数值显示问号。
原因:数据词典里变量类型设错,比如把模拟量设成开关量;或者设备驱动没连上,采集线程没启动;或者填充动画绑定的变量不是百分比。
解决:先看组态软件的设备状态,确认驱动在线。再查数据词典,模拟量要设成浮点或整型,不要设成离散。填充动画绑定前先做线性变换,把油高换算成 0 到 100 的百分比。如果还不行,用组态软件自带的调试工具看变量实时值,确认采集层有没有数据上来。
5.4 数据库写入越来越慢
现象:系统跑几个月后,历史查询变慢,写入也变慢。
原因:历史表没建索引,或者索引建了但查询没用上;或者单表数据量太大,没做分区或归档。
解决:历史表必须建(point_id, collect_time)联合索引。查询时条件要按索引顺序写,先测点后时间。数据量超过 1000 万条后,按月归档,把老数据移到历史归档表,主表只留最近 3 个月。MySQL 可以用分区表,按时间范围分区,查询时自动裁剪。
5.5 远程同步数据时间错乱
现象:总部看到的数据时间比油站本地时间差几个小时,或者顺序乱了。
原因:油站上位机和总部服务器时区设置不一致;或者同步时用了总部服务器时间而不是油站采集时间;或者断网补传时批量插入顺序错乱。
解决:所有设备统一用东八区时间,油站上位机开 NTP 对时。同步时数据带油站本地采集时间戳,总部按这个时间戳入库,不要用NOW()。补传时按采集时间排序后插入,插入前按测点加时间去重。如果时区实在统一不了,数据库存 UTC 时间,显示时再转本地时区。
6. 进阶技巧:用采集数据做油罐库存预测和异常用油识别
系统跑稳之后,采集的历史数据就不只是用来看的,可以做两件有价值的事:库存预测和异常用油识别。
库存预测的思路很简单:用过去 7 天同一时段的油高变化,算平均消耗速度,再结合当前油高,预测还能用多少小时。下面是一个 Python 示例,从数据库读历史数据,算消耗速度并预测。
import pymysql from datetime import datetime, timedelta # 连接数据库 conn = pymysql.connect(host='localhost', user='root', password='password', database='gas_station') cursor = conn.cursor() # 查过去 7 天同一测点的油高历史,按时间排序 point_id = 'tank1_oil_height' seven_days_ago = datetime.now() - timedelta(days=7) sql = """SELECT collect_time, point_value FROM history_data WHERE point_id = %s AND collect_time >= %s ORDER BY collect_time ASC""" cursor.execute(sql, (point_id, seven_days_ago)) rows = cursor.fetchall() if len(rows) < 2: print('历史数据不足,无法预测') else: # 算总消耗量和总时间,得到平均消耗速度 first_time, first_value = rows[0] last_time, last_value = rows[-1] total_hours = (last_time - first_time).total_seconds() / 3600.0 total_consumed = first_value - last_value if total_hours > 0 and total_consumed > 0: speed = total_consumed / total_hours # 单位 cm/小时 # 当前油高从实时表读 cursor.execute("SELECT current_value FROM realtime_data WHERE point_id = %s", (point_id,)) current = cursor.fetchone()[0] # 假设低液位报警线是 10 cm,算还能用多久 remaining = (current - 10.0) / speed if speed > 0 else 0 print(f'当前油高 {current} cm,平均消耗 {speed:.2f} cm/小时,预计 {remaining:.1f} 小时后到低报线') else: print('消耗数据异常,检查是否有卸油或数据缺失') cursor.close() conn.close()这段代码的逻辑是:取最近 7 天油高数据,用首尾差值算平均消耗速度,再用当前油高减报警线,除以速度得到剩余可用时间。参数说明里seven_days_ago可以按实际调整,油站销量稳定的话 3 天也够,销量波动大就用 14 天。注意如果这 7 天内有卸油,首尾差值会偏小甚至为负,所以实际项目里我会先剔除卸油时段的数据,只取两次卸油之间的消耗段。
异常用油识别更有意思。正常加油时,油高下降是平滑的,每次加油量对应油高下降一个固定比例。如果某段时间油高下降明显快于加油量对应的下降,可能存在异常损耗。做法是:把加油机的每笔交易升数累加,换算成油高下降理论值,再和液位仪实际下降值对比,偏差超过 3% 就标记异常,人工核查。这个功能不需要额外硬件,只用已有的采集数据就能做,很多油站老板对这个很感兴趣。
我自己做这类项目最大的习惯是:现场调试时一定带一个串口调试工具和一个笔记本,先把每个设备的原始数据读出来确认,再往上位机接。不要一上来就调上位机,否则通信层和显示层的问题混在一起,排查起来非常痛苦。另外,采集程序一定要写日志,每个设备的每次采集结果都记下来,出问题时看日志比猜快得多。希望帮到你。
本文还有配套的精品资源,点击获取