
简介这份资源是面向自动化、机械设计制造及仪器仪表相关专业学生与工程技术人员的毕业设计文档围绕加油站数据采集与状态监控系统展开重点解决传统人工记录加油交易、人工测量油罐库存所导致的效率低、易出错、无法实时监控与泄漏检测等问题。压缩包内仅含1个docx文件约193KB为完整的论文正文涵盖摘要、系统总体方案、数据采集器设计与实现、上位机数据采集程序、加油站监控程序及通信协议设计等章节并配有中英文摘要与关键词。内容以软件研制过程为主线采用模块化结构数据采集程序通过动态库与加油机、液位仪通信便于适配不同加油机协议读者可据此了解上位机与数据采集器之间的数据流转、接口转换思路及系统稳定性、通信速度、可靠性等设计要点。目前已有106人学习适合需要参考同类课题结构、撰写毕业论文或搭建加油站信息化监控方案初稿的读者。1. 加油站数据采集与状态监控系统从液位仪到上位机的一条完整链路一辆油罐车卸完油站长在办公室里刷新一下界面六个储罐的液位、温度、水位、油品体积全部实时跳动低于安全库存自动弹提醒卸油前后数据自动生成报表——这套东西听起来像大厂方案其实用一台工控机加一套上位机软件就能搭起来。加油站数据采集与状态监控系统要解决的核心问题就三件事把液位仪、加油机、测漏传感器这些下位设备的数据稳定读上来把数据在本地存住并做状态判断再把状态用组态画面和报警推给站长和区域管理员。它适合连锁油站的信息化负责人、做工业上位机开发的工程师、以及想从 SCADA 方向切入能源零售场景的技术团队。整套系统的技术底座是数据采集加状态监控落地形态通常是一台跑上位机软件的边缘网关向下走 RS-485/Modbus 或 TCP 私有协议对接液位仪向上走 MQTT 或 HTTP 把汇总数据送到中心平台。下面按选型、采集、存储、监控、避坑、进阶六段拆开讲每一步都给到能直接抄的参数和代码。2. 加油站现场的设备拓扑与通信选型为什么先定协议再定软件2.1 液位仪、加油机、测漏仪各自说什么协议加油站现场的下位设备大致分三类协议差异很大选型阶段必须先把这三类的通信方式摸清楚否则上位机写到一半发现协议对不上返工成本极高。液位仪是数据采集的核心主流品牌如 OPW、Veeder-Root、国内多数磁致伸缩液位仪对外基本都提供 RS-485 半双工接口跑 Modbus RTU 或厂商私有协议。Modbus RTU 的好处是寄存器地址公开、调试工具多坏处是不同厂商的寄存器映射不统一浮点数高低字节顺序经常要试。私有协议则相反地址不公开但数据打包规整通常需要厂商提供协议文档或 DLL。加油机的情况更复杂。老站用的是机械计量的税控加油机只有脉冲输出需要额外的采集板把脉冲转成 RS-485 或 CAN 信号新站用的是带通信接口的智能加油机直接走 TCP 或 RS-485 上报每笔加油记录。测漏仪一般是独立设备走 RS-485 或干接点数据量小但报警优先级最高。设备类型常见接口常见协议采集频率数据量级液位仪RS-485 / RS-232Modbus RTU / 私有1~5 秒每罐 10~20 个寄存器智能加油机TCP / RS-485私有 / Modbus事件触发每笔 20~50 字节测漏仪RS-485 / 干接点Modbus / 开关量5~30 秒几个状态位环境传感器RS-485 / 4-20mAModbus RTU10~60 秒2~4 个模拟量选型结论很直接如果现场液位仪支持 Modbus RTU优先用 Modbus上位机侧用现成的 Modbus 库开发量最小如果只支持私有协议必须拿到协议文档并在合同里写明厂商需提供调试支持。加油机如果只有脉冲输出建议单独加一块脉冲采集板不要试图从税控口直接读数据。2.2 上位机软件选型组态王、国产 SCADA 还是自研 C# 上位机这是整个项目最容易纠结的地方。三条路线各有适用场景选错了不是做不出来而是维护成本会失控。组态王、力控这类传统组态软件的优势是画面拖拽快、报警和报表功能现成、现场电工也能改。缺点是授权按点数收费液位仪加加油机加传感器轻松上百点授权成本不低而且私有协议驱动往往要额外买或自己写遇到非标协议一样要写脚本。适合点位不多、预算有限、后期不想自己维护代码的站。国产 SCADA 平台如中控、亚控的新一代产品在组态基础上加了数据上云和 Web 发布能力适合连锁油站做集中监控。但部署重、对服务器有要求单站场景有点杀鸡用牛刀。自研 C# 上位机是这几年越来越多团队选的路。C# 在工控上位机领域生态成熟串口、TCP、Modbus 库齐全WPF 或 WinForm 做界面快后期想加 MQTT 上云、想接自定义报表都灵活。代价是前期开发量大需要有人懂协议、懂多线程、懂界面。适合有开发能力、点位多、要做定制化报表和连锁管理的团队。我一般建议单站、点位少于 50、没有开发人员选组态王连锁站、要做集中平台、有开发资源选自研 C# 上位机。中间态可以用组态软件做本地画面自研一个采集服务专门负责协议对接和数据上云两者用 OPC 或数据库对接。2.3 网络与边缘网关的部署位置上位机跑在哪直接决定布线和后期运维难度。常见做法是在加油站便利店或站长室放一台工控机或无风扇边缘网关通过 RS-485 总线把液位仪、测漏仪串起来加油机走网线或另一路 485。工控机双网口一个口接现场设备网段一个口接加油站办公网或 4G 路由上云。这里有个容易忽略的点RS-485 总线的手拉手拓扑和终端电阻。液位仪和测漏仪如果挂在同一条 485 总线上波特率要统一终端电阻要在总线两端各接一个 120 欧。现场经常出现通信时好时坏八成是拓扑做成了星形或者终端电阻没接。工控机侧建议用带隔离的 485 转换器加油站环境电磁干扰不小非隔离模块用久了容易烧口。3. 用 C# 上位机把液位仪数据读上来Modbus RTU 采集的最小实现3.1 串口参数与 Modbus 寄存器映射的确认方法动手写代码之前先用调试工具把通信跑通。推荐用 Modbus Poll 或类似的 Modbus 主站调试工具按液位仪手册设置串口参数。典型参数是波特率 9600、数据位 8、停止位 1、无校验但磁致伸缩液位仪也有用 19200 或 38400 的以手册为准。从站地址一般是 1 到 8对应不同罐或不同设备。确认寄存器映射时重点看三样液位、温度、水位分别对应哪个寄存器地址数据类型是 16 位整数还是 32 位浮点浮点的话高低字顺序是什么。很多液位仪手册写的是“寄存器 40001 开始”实际编程时地址要减 1 变成 0。浮点数如果读出来是乱码先把高低字对调试试这是最常见的坑。# 用 modpoll 命令行工具快速验证液位仪通信 # 参数说明 # -m rtu 使用 Modbus RTU 模式 # -a 1 从站地址为 1 # -b 9600 波特率 9600 # -p none 无校验 # -d 8 数据位 8 # -s 1 停止位 1 # -r 1 起始寄存器地址 1对应 40002 # -c 10 连续读 10 个寄存器 # -t 4:float 按 32 位浮点解析字顺序 4高字在前 modpoll -m rtu -a 1 -b 9600 -p none -d 8 -s 1 -r 1 -c 10 -t 4:float /dev/ttyUSB0这条命令能读出数据说明串口参数和寄存器映射基本对了。如果读出来全是 0 或超时先查接线 A/B 是否接反再查从站地址和波特率。命令行工具验证通过后再把这套参数搬到 C# 代码里。3.2 C# 采集服务的核心代码与多线程处理C# 侧用 NModbus 或 FluentModbus 这类库配合 SerialPort 类。核心结构是一个采集服务类内部起一个后台线程或定时器按固定周期轮询每个从站把读到的原始寄存器解析成工程量再写入本地缓存或数据库。下面是一个最小可用的采集循环。using System; using System.IO.Ports; using System.Threading; using NModbus; public class TankCollector { private readonly SerialPort _port; private readonly IModbusSerialMaster _master; private readonly byte _slaveId; private Timer _timer; public TankCollector(string portName, byte slaveId) { _port new SerialPort(portName, 9600, Parity.None, 8, StopBits.One) { ReadTimeout 1000, WriteTimeout 1000 }; _port.Open(); var factory new ModbusFactory(); _master factory.CreateRtuMaster(_port); _master.Transport.ReadTimeout 1000; _slaveId slaveId; } public void Start() { // 每 2 秒采集一次避免总线负载过高 _timer new Timer(Collect, null, 0, 2000); } private void Collect(object state) { try { // 读 10 个保持寄存器起始地址 0 ushort[] regs _master.ReadHoldingRegisters(_slaveId, 0, 10); // 解析液位寄存器 0-1 组成 32 位浮点高字在前 float level ModbusFloat(regs[0], regs[1]); // 解析温度寄存器 2-3 float temp ModbusFloat(regs[2], regs[3]); // 解析水位寄存器 4-5 float water ModbusFloat(regs[4], regs[5]); // 写入本地缓存供界面和上云服务读取 TankCache.Update(_slaveId, level, temp, water); } catch (Exception ex) { // 采集失败记录日志不中断循环 Logger.Warn($从站 {_slaveId} 采集失败: {ex.Message}); } } private static float ModbusFloat(ushort high, ushort low) { byte[] bytes new byte[4]; // 高字在前低字在后 bytes[0] (byte)(high 8); bytes[1] (byte)(high 0xFF); bytes[2] (byte)(low 8); bytes[3] (byte)(low 0xFF); Array.Reverse(bytes); // 如果字节序不对调整这里 return BitConverter.ToSingle(bytes, 0); } }这段代码的关键点有三个。第一采集放在 Timer 回调里周期 2 秒不要用 while(true) 死循环否则界面线程和采集线程容易打架。第二每次采集都包在 try-catch 里单次失败只记日志不退出现场通信偶发超时是常态。第三浮点解析单独抽成方法高低字和字节序的调整集中在一处换品牌液位仪时只改这里。参数方面采集周期不是越短越好。RS-485 总线是半双工多个从站轮询时周期太短会导致总线冲突和超时。一般液位仪 2 到 5 秒一次足够测漏仪可以 10 秒一次加油机走事件触发不参与轮询。如果从站多把周期拉长到 5 秒或者按从站分组错开轮询。3.3 采集异常的重试与断线恢复策略现场通信不可能一直稳定采集服务必须能扛住偶发超时和设备断电。常见做法是每个从站维护一个失败计数器连续失败 3 次标记为离线界面变灰并报警恢复成功一次就清零。重试不要在同一周期内立刻重试容易把总线堵死放到下一个周期自然重试即可。断线恢复还要考虑串口本身。USB 转 485 模块偶尔会掉线表现为 SerialPort 打开但读写全超时。这种情况需要检测连续失败次数超过阈值后主动关闭串口再重新打开。下面是一个简单的恢复逻辑片段。private int _failCount 0; private const int OfflineThreshold 3; private void Collect(object state) { try { ushort[] regs _master.ReadHoldingRegisters(_slaveId, 0, 10); _failCount 0; TankCache.SetOnline(_slaveId, true); // ... 解析和缓存 } catch (Exception ex) { _failCount; if (_failCount OfflineThreshold) { TankCache.SetOnline(_slaveId, false); Logger.Warn($从站 {_slaveId} 连续失败 {_failCount} 次标记离线); // 连续失败超过 10 次尝试重开串口 if (_failCount 10) { ReopenPort(); } } } } private void ReopenPort() { try { _port.Close(); Thread.Sleep(500); _port.Open(); _failCount 0; Logger.Info(串口已重新打开); } catch (Exception ex) { Logger.Error($串口重开失败: {ex.Message}); } }这套逻辑不复杂但能挡掉现场八成的通信抖动。注意重开串口要加延时立刻重开往往还是失败。另外如果多个从站共用一条总线一个从站掉线不应该影响其他从站的采集所以每个从站的失败计数要独立。4. 状态监控与报警从数据缓存到组态画面和上云4.1 本地数据缓存与状态判断逻辑采集上来的数据不能直接怼到界面上中间要有一层缓存和状态判断。缓存的作用是解耦采集线程和界面线程界面按自己的刷新频率读缓存不直接碰串口。状态判断则负责把原始液位转换成业务状态正常、低液位、高液位、水位超标、温度异常、设备离线。状态判断的阈值一般来自业务规则比如液位低于罐容 20% 报低液位水位超过 50mm 报水位报警温度超过 40 度报异常。这些阈值不要硬编码在代码里放到配置文件或数据库现场调整不用重新编译。下面是一个状态判断的示例。public enum TankStatus { Normal, LowLevel, HighLevel, WaterAlarm, TempAlarm, Offline } public static TankStatus Evaluate(float level, float water, float temp, bool online) { if (!online) return TankStatus.Offline; if (water 50f) return TankStatus.WaterAlarm; if (temp 40f || temp -10f) return TankStatus.TempAlarm; if (level 20f) return TankStatus.LowLevel; if (level 95f) return TankStatus.HighLevel; return TankStatus.Normal; }状态判断的顺序有讲究。离线优先级最高因为数据不可信水位和温度报警优先于液位因为这两个直接关系安全和油品质量液位高低放在最后。这个顺序如果搞反了会出现水位超标但界面显示正常的尴尬情况。4.2 组态画面与报警推送的实现方式如果走组态王路线画面拖拽即可报警用内置的报警窗口配置报警限值和声音。如果走自研 C# 路线界面用 WPF 或 WinForm罐体用自定义控件画液位用矩形高度或弧形表示颜色随状态变化。报警推送本地用声音加弹窗远程用 MQTT 发到中心平台再由平台推送给站长手机。上云的数据格式建议统一成 JSON字段包括站号、罐号、液位、温度、水位、状态、时间戳。MQTT 主题按站号分层比如station/{stationId}/tank/{tankId}/status中心平台订阅通配主题即可。上报频率本地状态变化时立即上报正常状态每 30 秒或 1 分钟心跳一次避免流量浪费。// 上云上报示例用 MQTTnet var message new MqttApplicationMessageBuilder() .WithTopic($station/{stationId}/tank/{tankId}/status) .WithPayload(JsonSerializer.Serialize(new { stationId, tankId, level, temp, water, status status.ToString(), ts DateTimeOffset.UtcNow.ToUnixTimeSeconds() })) .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) .Build(); await _mqttClient.PublishAsync(message);QoS 用 AtLeastOnce 就够加油站数据丢一两条心跳不影响业务但报警数据建议用 ExactlyOnce 或本地先落库再上报保证不丢。上云链路要有断线重连和本地缓存补传网络恢复后把断网期间的数据补上去。4.3 历史数据存储与报表生成历史数据存本地数据库SQLite 适合单站数据量大了换 MySQL 或 PostgreSQL。表结构按罐和时间建索引字段包括时间戳、站号、罐号、液位、温度、水位、状态。存储频率不用跟采集频率一致采集 2 秒一次存储可以 1 分钟一条报警事件单独存事件表。报表常见的有日报、月报、卸油记录、报警统计。日报按罐统计期初库存、期末库存、进油量、出油量、报警次数。卸油记录通过液位突变检测自动生成卸油开始和结束时间、卸油量、卸油前后液位都记下来。这些报表用 SQL 聚合查询加模板导出 Excel 即可不需要复杂的报表引擎。-- 日报查询示例按罐统计当日进出油和报警 SELECT tank_id, MIN(level) AS min_level, MAX(level) AS max_level, MAX(level) - MIN(level) AS level_change, SUM(CASE WHEN status LowLevel THEN 1 ELSE 0 END) AS low_alarm_count, SUM(CASE WHEN status WaterAlarm THEN 1 ELSE 0 END) AS water_alarm_count FROM tank_history WHERE station_id ? AND ts ? AND ts ? GROUP BY tank_id;索引建在(station_id, ts)上查询按站和时间范围走索引数据量到千万级也能秒出。历史数据保留策略按业务定一般原始数据保留 3 到 6 个月聚合后的日报月报长期保留。5. 加油站数据采集与状态监控的避坑清单现场踩过的五条血泪经验5.1 液位仪浮点数读出来是乱码现象Modbus 读回来的寄存器值转成浮点后是几百万或几亿的离谱数字或者一直是 0。原因浮点数的字顺序或字节序和代码假设的不一致不同品牌液位仪有的高字在前有的低字在前字节序也有大小端之分。解决先用调试工具读原始寄存器手动按四种组合高字前/低字前 × 大端/小端换算找到能对上实际液位的组合再改代码里的解析方法。不要凭手册猜手册经常写错。5.2 RS-485 总线通信时好时坏现象采集大部分时间正常偶尔连续超时过一会儿又自己恢复。原因总线拓扑做成了星形而不是手拉手或者终端电阻没接或者 485 转换器非隔离受干扰。解决检查接线是否手拉手总线两端各接 120 欧终端电阻换带隔离的 485 转换器波特率如果高于 19200 可以降到 9600 试试。加油站变频设备多干扰源要远离信号线。5.3 采集线程和界面线程抢资源导致界面卡死现象界面点一下卡几秒或者采集频率一高界面就无响应。原因采集和界面在同一个线程或者缓存没有加锁导致读写冲突。解决采集放独立线程或 Timer界面通过缓存读数据缓存用 ConcurrentDictionary 或加锁的普通字典。界面刷新频率控制在 1 秒一次不要跟采集频率绑定。5.4 上云数据重复或丢失现象中心平台看到同一时间点两条数据或者断网恢复后数据缺一段。原因MQTT QoS 设置不当或者断网期间数据没落本地缓存。解决上报前先写本地待发表上报成功后标记已发断网恢复后扫描待发表补传。QoS 用 AtLeastOnce中心平台按站号加时间戳做去重。5.5 液位仪和测漏仪挂同一条总线互相干扰现象单独接液位仪正常接上测漏仪后两个都通信不稳定。原因两个设备波特率或校验方式不一致或者其中一个设备响应慢拖垮总线。解决查两个设备的手册统一波特率和校验如果无法统一分两条总线分别接不同的串口。测漏仪报警优先级高建议单独一路不要和液位仪混挂。6. 从单站到连锁把采集服务做成可复制的边缘节点单站跑通之后下一步一定是连锁复制。这时候最大的变化不是技术而是部署和运维方式。我一般会把采集服务做成一个自包含的边缘节点程序配置文件描述本站有哪些设备、什么协议、什么地址程序启动后按配置加载驱动不写死任何站-specific 的逻辑。新站上线只需要改配置文件不用重新编译。配置用 JSON 或 YAML结构大致是设备列表加每个设备的协议参数和寄存器映射。下面是一个配置示例。{ stationId: SZ001, devices: [ { type: tank, name: 1号罐, protocol: modbus-rtu, port: COM3, slaveId: 1, baudRate: 9600, registers: { level: { address: 0, type: float32, wordOrder: high }, temp: { address: 2, type: float32, wordOrder: high }, water: { address: 4, type: float32, wordOrder: high } }, thresholds: { lowLevel: 20, highLevel: 95, waterAlarm: 50, tempHigh: 40 } } ], cloud: { mqttBroker: tcp://your-broker:1883, topicPrefix: station/SZ001, heartbeatInterval: 60 } }这套配置驱动的做法好处是新增站点零代码改动坏处是配置本身要版本管理改错了会导致采集异常。我的习惯是配置文件纳入 Git每次上线前用校验脚本检查必填字段和地址范围避免手误。验证一个边缘节点是否健康我一般看四个指标采集成功率应该大于 99%、平均采集延迟小于 1 秒、上云心跳间隔稳定性抖动小于 5 秒、本地待发表积压量应该长期为 0。这四个指标在节点本地做个简单页面展示或者随心跳一起上报中心平台就能看到每个站的健康度。最后说一个我自己的教训。早期做第一个站的时候我把所有逻辑写在一个大程序里采集、界面、上云、报表全耦合在一起结果第二个站要改一个寄存器地址我得重新编译整个程序还不敢保证不影响其他功能。后来拆成采集服务、缓存服务、上云服务、界面四个独立进程用本地 socket 或共享内存通信改一个不影响其他部署也灵活。这个重构花了两周但后面复制十个站省下的时间远不止两周。如果你正准备做第一个站建议一开始就按可复制的边缘节点来设计别图快写成一坨。希望帮到你。本文还有配套的精品资源点击获取