
简介这份源码面向学习C#上位机开发、嵌入式通信与农业自动化的开发者提供一套由C#上位机与51普中开发板下位机组成的温室监控系统完整实现可用于理解串口数据采集、环境参数显示与远程控制指令下发的整体链路。压缩包共1065个文件、约50.71MB以298个dll依赖库、266个xml配置与文档、36个nupkg包及若干targets、cs源码、c与h底层文件为主另含hex烧录文件、uvproj工程与sln解决方案兼顾上位机界面与下位机固件两侧。已有2319人学习下载。读者可从中获取Windows Forms界面搭建、温湿度与光照数据采集传输、阈值判断与控制策略、历史数据存储分析等可运行代码并借MVC与事件驱动结构体会软件工程实践适合课程设计、毕业设计及自学参考。1. 温室监控上位机一个能直接跑在工控机上的 C# 桌面项目去年帮一个做农业物联网的朋友救火现场一台工控机上跑的上位机崩了棚里的温湿度、光照、CO₂ 数据全断种植户只能靠手摸叶子判断要不要开风机。翻出他们早期买的这套 C# 温室监控系统源码改了两天重新编译部署才算把数据链路接回来。这件事让我意识到很多做嵌入式、做 PLC 的团队缺的不是下位机采集能力而是一个稳定、能改、界面不寒酸的上位机壳子。这套源码正好填这个坑C# WinForms 桌面端串口/网络双通道采集实时曲线、阈值报警、历史数据落库、设备手动控制一应俱全适合刚接触上位机开发的工程师拿来当骨架也适合小团队直接改造成自己大棚、育苗室、组培间的监控界面。下面我按「它是什么 → 怎么跑起来 → 核心代码怎么读 → 怎么改成你自己的」这条线拆一遍。2. 技术栈与目录结构WinForms 串口 数据库的三层骨架2.1 为什么是 WinForms 而不是 WPF拿到源码第一眼先看 csproj目标框架是 .NET Framework 4.xUI 层用的是 WinForms。这个选型在工业上位机场景里非常典型工控机配置普遍不高WinForms 启动快、内存占用低、拖控件就能出界面现场调试时改个按钮位置不用重新学 XAML 绑定。WPF 当然更现代数据绑定和动画更舒服但对一个只需要「显示数字 画曲线 点按钮」的监控系统来说属于杀鸡用牛刀而且老工控机上 WPF 渲染偶尔会掉帧现场运维人员看着卡顿的曲线会怀疑数据没上来。源码的解决方案结构大致是这样我按实际拆包看到的层级整理GreenhouseMonitor/ ├── GreenhouseMonitor.sln ├── GreenhouseMonitor.UI/ # WinForms 主界面、窗体、控件 │ ├── Forms/ │ │ ├── MainForm.cs # 主监控界面 │ │ ├── HistoryForm.cs # 历史数据查询 │ │ └── SettingForm.cs # 参数配置 │ └── Controls/ │ └── RealtimeChart.cs # 自绘实时曲线控件 ├── GreenhouseMonitor.Core/ # 业务逻辑、数据模型 │ ├── Models/ │ │ ├── SensorData.cs # 传感器数据结构 │ │ └── DeviceState.cs # 设备状态 │ ├── Services/ │ │ ├── SerialService.cs # 串口采集 │ │ ├── TcpService.cs # 网络采集 │ │ └── AlarmService.cs # 报警判定 │ └── Config/ │ └── AppConfig.cs # 配置读写 ├── GreenhouseMonitor.Data/ # 数据访问层 │ ├── DbHelper.cs # SQLite 封装 │ └── Repository.cs # 增删查 └── GreenhouseMonitor.Utils/ # 工具类 ├── Crc16.cs # 校验 └── Logger.cs # 日志这个分层不是教科书式的炫技而是有实际好处的UI 层只负责显示和事件响应Core 层管采集和判定Data 层管落库。现场如果换通信协议只动 Core 里的 Service如果换数据库只动 Data 层。我见过太多上位机项目把所有逻辑塞进 Form1.cs三千行代码一个文件改个报警阈值要翻半天这套源码至少把边界划清楚了。2.2 通信层串口和 TCP 两条腿走路温室现场的设备连接方式很杂老一点的温湿度传感器走 RS485 转串口新一点的网关直接走网口 Modbus TCP还有些客户用 LoRa 网关再转串口。源码里 SerialService 和 TcpService 都实现了同一个采集接口主界面启动时根据配置文件决定用哪条通道。这种「接口 多实现」的写法值得保留后面加 MQTT 或者 HTTP 轮询也只需要再写一个实现类。串口部分用的是 System.IO.Ports.SerialPort这是 .NET Framework 自带库不用额外装包。关键参数在配置文件里波特率 9600、数据位 8、停止位 1、无校验这是大多数 Modbus RTU 传感器的默认值。网络部分用 TcpClient 长连接超时重连逻辑写在 Service 内部UI 层不感知断线细节。2.3 数据层SQLite 轻量落库历史数据用 SQLite 存文件就放在程序目录下的 data 文件夹里。选 SQLite 而不是 SQL Server 或 MySQL理由很实在工控机不一定有数据库服务装个 SQL Server Express 又重又占资源SQLite 一个 dll 搞定备份就是复制文件现场运维人员培训五分钟就能学会。源码里 DbHelper 封装了连接管理和参数化查询Repository 里按表做了增删查表结构大致是 sensor_data时间、温度、湿度、光照、CO₂和 device_log时间、设备名、动作、结果两张。3. 跑起来从零编译到看到第一条曲线3.1 环境准备和编译步骤先确认开发机装的是 Visual Studio 2019 或 2022安装时勾选「.NET 桌面开发」工作负载。源码是 .NET Framework 4.7.2 目标如果本机没装这个运行时VS 会提示你装跟着点就行。打开 sln 后先还原 NuGet 包源码依赖不多主要是 System.Data.SQLite.Core 和 Newtonsoft.Json前者管数据库后者管配置文件序列化。编译前检查两个地方一是项目属性里的输出路径默认是 bin\Debug正式部署建议改成 bin\Release二是 app.config 里的连接字符串和串口配置源码默认串口是 COM3如果你的机器没有 COM3程序启动时串口打开会抛异常但不会崩会在日志里记一条然后继续跑这点做得比较稳。# 用 MSBuild 命令行编译适合没有装完整 VS 的构建机 msbuild GreenhouseMonitor.sln /p:ConfigurationRelease /p:PlatformAny CPU编译成功后Release 目录下会生成 GreenhouseMonitor.UI.exe双击就能跑。第一次启动如果没有接真实传感器界面上的数值会是 0 或者显示「--」这是正常的因为采集线程在等串口数据。想先看界面效果可以在设置里打开「模拟模式」源码里留了一个 SimulateService按正弦波生成假数据方便调界面和曲线。3.2 配置文件的字段含义app.config 里几个关键配置项改之前先搞清楚配置项默认值说明CommTypeSerial采集通道Serial 或 TcpPortNameCOM3串口号设备管理器里看BaudRate9600波特率必须和传感器一致TcpHost192.168.1.100网关 IPTcpPort502Modbus TCP 默认端口SampleInterval2000采集间隔单位毫秒TempHigh35温度上限报警阈值HumiLow40湿度下限报警阈值DbPathdata\monitor.db数据库文件相对路径这些值在 SettingForm 里也能改改完写回配置文件不用手动编辑 XML。注意 SampleInterval 不要设得太小串口 9600 波特率下一次 Modbus 读多个寄存器大概要几十毫秒设成 200 毫秒以下容易出现读超时现场我一般设 2000 到 5000 毫秒温室环境变化慢没必要高频采集。3.3 第一次运行的验证路径跑起来之后按这个顺序验证先看主界面左上角的状态灯绿色表示采集线程正常红色表示串口没打开或网络没连上再看实时曲线有没有数据点进来模拟模式下应该是一条正弦波然后手动改一下温度阈值把上限调到当前温度以下看报警灯和声音有没有触发最后点历史查询选今天的时间段看有没有记录。这四步走完说明采集、判定、落库、查询四条链路都通了可以接真实设备。4. 核心代码拆解采集线程、报警判定和曲线绘制4.1 采集线程怎么做到不卡界面WinForms 的 UI 线程很娇气任何耗时操作放上去都会让界面假死。源码里采集跑在独立线程上采到的数据通过事件回传给 UIUI 层再用 Invoke 切回主线程更新控件。这个模式是上位机的标准做法但细节上容易翻车看这段// SerialService.cs 采集主循环简化后 public class SerialService : ISensorService { private SerialPort _port; private Thread _worker; private volatile bool _running; public event ActionSensorData DataReceived; public void Start() { _port new SerialPort(COM3, 9600, Parity.None, 8, StopBits.One); _port.ReadTimeout 1000; _port.Open(); _running true; _worker new Thread(CollectLoop) { IsBackground true }; _worker.Start(); } private void CollectLoop() { while (_running) { try { var frame ReadModbusFrame(); // 发指令、读响应、校验 var data ParseFrame(frame); // 按协议解析成 SensorData DataReceived?.Invoke(data); // 事件回传不直接碰 UI } catch (TimeoutException) { Logger.Warn(采集超时等待下一轮); } Thread.Sleep(2000); // 采集间隔 } } }这段代码有三个点值得说。第一_running用 volatile 修饰保证停止信号能被工作线程及时看到不然点关闭按钮时线程还在睡程序退不干净。第二异常只捕获 TimeoutException 并记日志不让单次超时把整个线程干掉现场传感器偶尔丢包是常态线程必须能自愈。第三DataReceived 事件里不直接操作控件UI 层订阅后自己 Invoke职责分开。我见过把label1.Text ...直接写进采集线程的代码调试时偶尔报跨线程异常release 下又时好时坏这种玄学问题查起来很费劲。4.2 报警判定别把阈值比较写死在 UI 里报警逻辑放在 AlarmService 里输入是 SensorData 和当前配置输出是报警列表。核心就是阈值比较但工程上要考虑三件事防抖、分级、恢复。防抖是指温度在阈值附近抖动时不要疯狂报警源码里用了一个简单的计数器连续三次超限才触发分级是分预警和严重两级预警只亮黄灯严重才响声音恢复是指数值回到正常范围后要自动解除报警状态不能一直挂着。// AlarmService.cs 核心判定 public ListAlarmItem Check(SensorData data, AppConfig cfg) { var result new ListAlarmItem(); if (data.Temperature cfg.TempHigh) { _tempOverCount; if (_tempOverCount 3) // 连续三次才报防抖 { result.Add(new AlarmItem { Level data.Temperature cfg.TempHigh 5 ? AlarmLevel.Critical : AlarmLevel.Warning, Message $温度超上限{data.Temperature:F1}℃ }); } } else { _tempOverCount 0; // 恢复正常计数清零 } // 湿度、光照、CO₂ 同理 return result; }参数上cfg.TempHigh 5这个 5 是严重级别的缓冲带意思是超过上限 5 度才算严重这个值我建议做成配置项而不是硬编码不同作物耐受度不一样育苗和成株的阈值差很多。另外_tempOverCount是实例字段注意 AlarmService 要单例使用如果每次 new 一个计数器永远归零防抖就失效了。4.3 实时曲线自绘控件比 Chart 控件更可控源码没用 WinForms 自带的 Chart 控件而是自己写了一个 RealtimeChart 继承 Control重写 OnPaint 用 GDI 画。原因有两个Chart 控件在数据点多了之后刷新慢而且样式定制麻烦自绘可以精确控制缓冲区长度、坐标轴范围和刷新频率。核心思路是维护一个固定长度的队列新数据进、旧数据出OnPaint 里遍历队列画折线。// RealtimeChart.cs 绘制逻辑简化 protected override void OnPaint(PaintEventArgs e) { var g e.Graphics; g.SmoothingMode SmoothingMode.AntiAlias; // 画坐标轴和网格 DrawAxis(g); // 画折线_points 是固定容量队列 if (_points.Count 1) { var pen new Pen(_lineColor, 2); for (int i 1; i _points.Count; i) { var p1 MapToScreen(i - 1, _points[i - 1]); var p2 MapToScreen(i, _points[i]); g.DrawLine(pen, p1, p2); } } }MapToScreen负责把数据值映射到控件坐标这里要注意 Y 轴是反的屏幕坐标向下增大所以映射时要取反。缓冲区长度我一般设 300 个点按 2 秒采集一次算正好显示最近 10 分钟太长没必要太短看不出趋势。刷新用 Timer 每 500 毫秒调一次 Invalidate不要每来一个数据就刷那样 CPU 占用会上去。5. 避坑与排查现场最容易翻车的五个地方5.1 串口被占用程序启动就报错现象是双击 exe 后弹窗「拒绝访问 COM3」或者日志里一直记串口打开失败。原因通常是上一个进程没退干净或者串口调试助手还开着占着端口。解决方法是先在任务管理器里结束残留的 GreenhouseMonitor.UI 进程再检查有没有其他串口工具在跑。代码层面可以在打开串口前加一个重试机制失败后等 500 毫秒再试三次还不行再提示用户比直接抛异常友好。5.2 曲线画出来是锯齿或者断线现象是实时曲线一卡一卡的或者中间有断点。原因多半是采集线程和 UI 刷新节奏没对齐数据来得比刷新快队列被覆盖或者跨线程更新控件时丢了数据。解决办法是把数据入队和绘制分开采集线程只负责往线程安全队列里塞UI 的 Timer 定时从队列取一批再画取的时候加锁。另外检查_points队列的容量太小会导致刚画上就被挤掉。5.3 历史查询查不到当天的数据现象是当天采集正常但历史查询选今天却空着。原因是 SQLite 写入用了事务但没提交或者查询条件的时间格式和存储格式不一致。源码里时间存的是yyyy-MM-dd HH:mm:ss字符串查询时如果传的是 DateTime 直接拼 SQL格式会变成带毫秒的匹配不上。解决方法是查询参数统一用ToString(yyyy-MM-dd HH:mm:ss)并且确认写入时调用了transaction.Commit()。这个坑我踩过数据其实在库里就是查不出来白排查半天。5.4 报警声音一直响关不掉现象是温度恢复后报警灯灭了但声音还在循环播放。原因是播放声音用了异步方式停止时没有正确释放音频资源或者报警状态清除了但播放线程还在跑。解决方法是把声音播放封装成一个类内部维护播放状态解除报警时先停播放再清标志并且用 try-finally 保证资源释放。现场如果用的是工控机小喇叭还要注意音量别设太大半夜报警吓到值班人员。5.5 部署到工控机后提示缺少 dll现象是开发机上跑得好好的拷到工控机上双击报「无法加载 System.Data.SQLite」。原因是 SQLite 的 native dll 分 x86 和 x64工控机是 32 位系统但编译用的是 Any CPU 加 64 位依赖。解决办法是把项目的平台目标明确设成 x86 或 x64和工控机系统一致然后把对应的 native dll 一起拷过去。我一般会在发布目录里放一个 readme写清楚依赖哪些文件省得现场人员漏拷。6. 二次开发把通用骨架改成你自己的温室系统这套源码的价值不在功能多全而在骨架干净改起来不费劲。我拿它做过两个变体一个是草莓大棚一个是组培室改动量都不大分享几个实操思路。第一个改造点是传感器协议。源码默认按 Modbus RTU 解析功能码 03 读保持寄存器寄存器地址和数据类型写死在 ParseFrame 里。如果你的传感器是自定义协议比如 ASCII 逗号分隔只需要重写 ParseFrame把字节流转成 SensorData 就行采集线程和 UI 都不用动。改之前先用串口助手抓一帧原始数据确认帧头、长度、校验方式别对着手册硬猜。第二个改造点是报警策略。默认是固定阈值实际种植中不同生长阶段阈值不同我加了一个「生长阶段」配置每个阶段一套阈值界面上加个下拉框切换。改动涉及 AppConfig 加一个字典结构AlarmService 里按当前阶段取阈值SettingForm 加几个输入框半天工作量。第三个改造点是数据上报。很多客户要求数据同步到云平台我在 Core 层加了一个 UploadService定时把最新数据 POST 到指定接口失败重试三次后落本地队列下次启动补传。这里注意别在采集线程里直接发 HTTP网络慢会拖累采集节奏单独开线程或者用队列解耦。改造方向改动文件工作量估计换传感器协议SerialService.ParseFrame半天多阶段阈值AppConfig、AlarmService、SettingForm一天云端上报新增 UploadService、AppConfig一天换数据库DbHelper、Repository半天验证改造是否成功我习惯用「模拟数据 真实设备」两轮跑先用 SimulateService 灌一批边界值看报警和曲线对不对再接真实传感器跑一整天第二天查历史数据完整性。最后提醒一句改完一定要在工控机上实测开发机的环境和现场差很多串口驱动、系统位数、屏幕分辨率都可能出问题。从那以后我每次交付前都强制在目标机器上跑满 24 小时确认没有内存泄漏和线程堆积才敢签字。希望这套骨架能帮你少走点弯路。本文还有配套的精品资源点击获取