.NET 8 + Cursor 编写的工业 Web SCADA 系统

发布时间:2026/9/15 5:56:23

.NET 8 + Cursor 编写的工业 Web SCADA 系统 目录前言项目介绍项目功能项目特点项目技术项目代码项目效果项目源码总结前言工业现场的数据采集与监控说起来简单做起来坑不少。串口超时、寄存器地址错位、设备突然离线、数据刷新太快页面卡死、历史数据堆积成山——每个问题都能让人折腾好几天。这两年在折腾工业监控系统时一直想搞一套既能快速落地、又不用被商业组态软件绑架的方案。.NET 8 的 Blazor Server 给了我一个新思路用 C# 从头撸到尾从设备驱动到前端展示不用换语言不用搞前后端分离那一套复杂工程。把思路和实现整理出来希望能给同样在搞工业监控的同行一些参考。项目介绍一套基于 .NET 8 Blazor Server 开发的轻量级工业 Web SCADA 系统专门针对多泵站集中监控场景设计。整套系统涵盖了设备数据采集、实时监控、报警管理、历史存储和数据分析等核心环节。系统默认配置了一套四泵站标准点表模板71 个标签/站涵盖液位、频率、运行状态、控制模式等关键参数。开箱即用只要配好 Modbus TCP 设备地址就能在浏览器里看到泵站的实时运转情况。项目功能实时监控泵站总览 Dashboard在线/离线统计、报警数量、各站液位概览泵站详情页液位实时曲线、泵组启停控制、运行状态指示灯标签数据 100ms 级刷新操作响应基本感觉不到延迟报警管理高低限报警、设备离线报警自动触发报警列表支持按时间、级别、确认状态筛选报警确认操作带操作人记录历史数据标签值定时归档默认 10 秒一次支持按时间范围查询任意标签的历史曲线数据导出为 CSV 格式设备诊断每个设备的通讯状态实时可见提供 /api/status、/api/device-status 等诊断接口采集日志结构化输出排查问题不靠猜多客户端推送SignalR 实时广播外部客户端可订阅数据防抖机制避免高频推送压垮网络项目特点**全栈 C#**从 Modbus 驱动到 Web 界面一套技术栈从头写到尾维护成本低团队上手快配置驱动设备和点表全部走 JSON 配置新增泵站只需复制一份模板文件改改 IP 地址轻量存储SQLite 作为历史库部署省心数据量不大时完全够用采集与展示分离后台服务负责轮询和计算Blazor 组件只做渲染各司其职防抖推送不管是页面更新还是 SignalR 广播都做了 100ms 批量合并避免高频刷新把浏览器搞崩并行轮询多设备并行采集一个设备卡住不影响其他站的数据更新项目技术层级技术选型说明前端框架Blazor Server (InteractiveServer)服务端渲染实时数据通过 SignalR 推送通讯协议Modbus TCP (NModbus)主流 PLC 和 RTU 都支持兼容性好实时推送SignalRWebSocket 长连接延迟低数据存储EF Core SQLite轻量、免安装适合工控机部署日志Serilog结构化日志方便接入 ELK 或 Seq图表ECharts (CDN)液位趋势、历史曲线展示DI 容器.NET 8 原生 DI不用第三方容器够用项目分层结构IndustrialWebScada/ ├── config/ # 配置目录 │ ├── devices.json # 设备列表 │ └── device-templates/ # 点表模板 │ └── pump-station.json # 71 标签标准模板 ├── src/ │ ├── IndustrialWebScada.Core/ # 领域模型、接口、事件定义 │ ├── IndustrialWebScada.Application/ # 业务逻辑TagValueStore、AlarmEngine │ ├── IndustrialWebScada.Drivers/ # ModbusTcp、MQTT 驱动实现 │ ├── IndustrialWebScada.Infrastructure/ # EF Core SQLite 持久化 │ ├── IndustrialWebScada.BackgroundServices/ # 采集、推送、报警、归档服务 │ └── IndustrialWebScada.Web/ # Blazor Server 主程序 │ ├── Components/Pages/ # Dashboard、PumpStationList 等页面 │ ├── Components/Shared/ # PumpStationCard、WaterLevelTank 等组件 │ └── Program.cs # 入口与 DI 配置 └── tests/ └── IndustrialWebScada.Drivers.Tests/ # 驱动层单元测试项目代码数据采集服务核心循环public classDataCollectionService : BackgroundService { privatereadonly ITagValueStore _store; privatereadonly IEnumerableIDeviceDriver _drivers; privatereadonly ILoggerDataCollectionService _logger; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var tasks _drivers.Select(d CollectDeviceAsync(d, stoppingToken)); await Task.WhenAll(tasks); // 并行采集互不干扰 await Task.Delay(GetInterval(), stoppingToken); } } private async Task CollectDeviceAsync(IDeviceDriver driver, CancellationToken ct) { try { var tags await driver.ReadAllTagsAsync(ct); _store.Update(tags, DateTime.UtcNow); } catch (Exception ex) { _logger.Warning(ex, 采集失败 [{DeviceId}], driver.DeviceId); _store.MarkOffline(driver.DeviceId); } } }采集服务以 BackgroundService 形式跑在后台每个设备独立并行执行。某个设备超时或断线不会拖累其它设备的数据更新。防抖推送器100ms 批量合并public classTagValueNotifier : IDisposable { privatereadonly System.Timers.Timer _timer; privatereadonly Dictionarystring, TagValue _buffer new(); privatereadonlyobject _lock new(); privatereadonlyint _intervalMs 100; publicevent ActionIReadOnlyListTagValue OnNotified; public void Push(TagValue tagValue) { lock (_lock) { _buffer[tagValue.TagId] tagValue; // 同一标签只保留最新值 } } private void Flush() { ListTagValue snapshot; lock (_lock) { if (_buffer.Count 0) return; snapshot _buffer.Values.ToList(); _buffer.Clear(); } OnNotified?.Invoke(snapshot); } }页面组件订阅这个 Notifier收到批量数据后统一调用 StateHasChanged()。100ms 的窗口既能保证实时性又避免了逐条刷新带来的频繁渲染开销。Modbus 地址合并读取减少报文往返public async TaskDictionarystring, object ReadAllTagsAsync(CancellationToken ct) { var result new Dictionarystring, object(); var mergedRequests MergeAdjacentAddresses(_tagConfigs); foreach (var request in mergedRequests) { var data request.FunctionCode switch { 2 await _modbusMaster.ReadInputsAsync(unitId, request.Start, request.Length), 3 await _modbusMaster.ReadHoldingRegistersAsync(unitId, request.Start, request.Length), 4 await _modbusMaster.ReadInputRegistersAsync(unitId, request.Start, request.Length), _ thrownew NotSupportedException() }; // 按原始点表拆分数据 ParseAndFill(result, data, request); } return result; }点表里 71 个标签如果逐个读取需要发 71 次请求。这个合并逻辑会把地址连续的寄存器批量读取读 16 个保持寄存器只需 1 次请求效率直接拉满。项目效果主控台页面聚合了所有泵站的状态概览右上角显示在线/离线数量和未确认报警数中间区域以卡片形式展示各站的液位和运行泵数。泵站详情页是使用频率最高的界面。左侧是液位动态水位图中间是四台泵的运行状态灯和启停按钮底部是最近 30 分钟的液位趋势。控制指令下发后泵状态的变化在 1-2 秒内就能在界面上反映出来。报警页面按时间倒序排列未确认的报警高亮显示。点击确认按钮会记录操作人和确认时间方便事后追溯。历史查询页面支持选择时间范围和标签名称曲线图用 ECharts 渲染缩放和平移操作都很流畅。右侧提供 CSV 导出按钮方便导出数据做进一步分析。项目源码GitHubhttps://github.com/suwencjp/VibeCodingIndustrialWebScada总结回过头来看这套系统最大的价值在于把工业采集和 Web 监控打通了而且全程没离开 .NET 生态。当然这套方案也有明显的局限。Blazor Server 依赖长连接大规模部署时对服务器的 SignalR 连接数有压力。另外 SQLite 在高并发写入场景下性能一般如果点位超过 5000 个建议换成 PostgreSQL 或 InfluxDB。不过对于中小型泵站几十到几百个标签来说这套架构足够皮实好用了。引入地址
延伸阅读

更多相关文章

2026/9/5 22:31:57

嵌入式低功耗设计:PCGPIO寄存器与时钟门控协同管理详解

1. 电源管理寄存器:嵌入式低功耗设计的基石在嵌入式系统,尤其是电池供电的物联网设备、便携式医疗仪器或远程传感器节点中,功耗是决定产品成败的关键指标之一。作为一名长期奋战在一线的嵌入式工程师,我见过太多项目初期对功耗“不…

2026/9/10 10:15:19

AM335x PRCM模块实战:从时钟管理到低功耗设计

1. 从手册到实战:理解AM335x PRCM模块的核心价值如果你正在基于TI的AM335x系列处理器(比如经典的BeagleBone Black)进行嵌入式开发,那么“电源、复位和时钟管理”这个模块,也就是PRCM,绝对是你绕不开的核心…

2026/9/15 5:51:35

SPOOLing技术:独占设备变共享设备

128: SPOOLing技术:独占设备变共享设备 想象一下,公司只有一台打印机,但有50个员工都要打印文件。如果每个人都要等到自己的文件打完才能离开,效率得多低? 更麻烦的是,打印机是独占设备——同一时刻只能服务一个任务。那怎么让所有人都觉得"打印机随时可以用"…

2026/9/15 5:51:35

Flutter+OpenHarmony跨端司机推荐模块开发实践

1. 项目概述:跨端司机推荐模块的技术价值在当代出行服务生态中,"司机推荐"功能已经从简单的列表展示演变为融合实时数据、用户画像和设备适配的智能交互界面。这个看似简单的UI模块实际上需要处理多源数据整合、跨终端适配和即时交互三大核心挑…

2026/9/15 5:51:35

缓冲技术:解决速度不匹配的“中间人“

127: 缓冲技术:解决速度不匹配的"中间人" 你有没有注意过一个现象:在电脑上看在线视频,有时候会先"加载一下",然后才流畅播放?那个"加载"的过程,就是在建立缓冲区。 计算机世界里,不同设备、不同程序之间的速度差异巨大。缓冲技术就是用…

2026/9/15 5:51:35

解决.NET iOS AOT编译模块加载失败问题

1. 问题现象与背景解析遇到"Failed to load AOT module xxx while running in aot-only mode"错误时,通常是在.NET iOS应用开发中使用AOT(Ahead-Of-Time)编译模式时出现的典型问题。这个错误的核心在于运行时无法加载预期的预编译模…

2026/9/15 5:51:35

I/O管理:输入输出如何协调

126: I/O管理:输入输出如何协调 你敲一下键盘,屏幕上立刻出现一个字符。这个看似瞬间的动作背后,操作系统做了一大堆协调工作。 键盘、鼠标、显示器、硬盘、打印机、网卡……这些设备速度差异巨大,工作方式也完全不同。操作系统需要一套统一的机制来管理它们,这就是I/O管…

2026/9/15 5:46:35

SMB共享流量分析实战:Wireshark提取与修复传输载荷

我最近在靶场里完整走通了一条链路:目标内网里开着一台 Windows 主机的 SMB 共享,共享目录里躺着一个 pcap 流量包,看起来像攻击者顺手留下的“分析素材”。我的任务就是通过 SMB 把这个包拉回来,用 Wireshark 逐层翻记录&#xf…

2026/9/15 4:54:30

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/15 0:01:16

AI英语单词APP开发:自适应学习算法与移动端优化实践

1. 项目概述 作为一名在移动应用开发领域摸爬滚打多年的老手,我最近完成了一个AI英语单词APP的开发项目。这个项目将传统单词记忆方法与现代AI技术相结合,打造了一款能够智能适应不同用户学习习惯的英语学习工具。 市面上大多数单词APP都存在一个通病&a…

2026/9/15 0:01:16

Flutter与OpenHarmony结合开发手语学习APP实战

1. 项目背景与核心价值作为一名同时接触过Flutter和OpenHarmony的开发者,最近我完成了一个基于Flutter for OpenHarmony的手语学习APP实战项目。这个项目最大的特点在于实现了跨平台框架与国产操作系统深度结合的创新实践——用Flutter开发的应用能完美运行在OpenHa…

2026/9/15 0:01:16

六个月成为机器人工程师:从ROS2到SLAM的实战路径

1. 六个月的紧迫感从哪来:先搞清楚你要成为哪种机器人工程师说实话,六个月的期限并不是一个宽松的时间线。市面上任何一本正经的机器人学教材都超过五百页,ROS2的官方文档可以翻到你怀疑人生,再加上ABB、KUKA这些工业机器人厂家动…

2026/9/14 11:59:31

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/14 13:53:59

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/14 11:22:57

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码