晚上十一点,车间里的包装线停了。值班同事给我发微信说触摸屏上能看见故障,但具体是哪个气缸的感应器没到位,要等第二天有人进车间去查诊断信息。这种场景在非标设备、中小型产线上太常见了——触屏HMI就在设备旁边,人在哪儿都行,偏偏就是不能在设备旁边。
这套源码解决的就是这个问题:用C#写一个跑在手机上的组态软件,通过以太网直接和西门子S7-1200 PLC通信,把变量、指示灯、按钮、趋势图都做成可配置的组态画面。现场刷一下手机就能看到设备状态,也能做远程启停之类的操作。下面我结合这套C#全套源代码,从技术选型、代码架构、通信协议到实机调试,完整拆一遍整套设计思路。如果你是做上位机开发、设备调试、售后维护,或者刚接手类似项目想找个能跑的底子,这篇应该能帮你省不少时间。
1. 为什么要把组态软件搬进手机:先搞清楚这套源码解决了什么问题
1.1 传统上位机方案卡在哪里
做工业自动化的人对“组态软件”这四个字都不陌生,组态王、WinCC、力控、昆仑通态MCGS这些是PC端的老面孔。它们的典型使用方式是在一台工控机上画好工艺流程画面,把图形元素绑定到PLC里的变量地址,然后这台工控机就固定放在中控室或者设备电柜旁边。这有几个很现实的问题:
第一,地点被锁死。想在办公室看产线状态,要么拉一堆网线做远程桌面,要么跑一趟车间。设备临时报故障,摸出手机想看一眼具体情况,传统组态软件根本没有手机端能直接用的方案。
第二,报警通知是被动的。传统组态软件报警只在屏上弹窗、亮灯、响蜂鸣器,但人不在跟前就看不到。很多非标设备的售后问题都是半夜报警,第二天才知道,处理节奏被拖慢一整个晚上。
第三,开发节奏跟不上。传统组态软件做画面绑定,通常是在电脑上拖控件、配变量、下装运行。一旦要加一个监控点,或者调整按钮功能,往往得改工程、重新下装,对于需要频繁出差去现场改动的项目来说很麻烦。
1.2 手机组态和传统HMI在做同一件事,但思路不一样
这里要澄清一个概念:手机组态软件不是要替代WinCC这种大型SCADA系统,它做的是“轻量移动监控入口”这个事。核心区别在于,传统HMI的画面是“开发期固定下来的”,而手机组态APP的画面是“运行时按配置动态生成的”。
这背后的逻辑就是“组态”二字的本质:用一套预置的图形元素库,通过配置描述文件(经常是JSON、XML或者数据库表)告诉软件“界面上有什么控件、每个控件对应PLC哪个变量、什么条件下变什么颜色”。这跟用代码硬生生写死每一个页面截然不同——改画面不动代码,只要改配置文件就能重新生成界面,这是组态软件的灵魂所在。
这套C#源码走的也是同样的路线,只不过把运行环境从Windows换到了手机,把通信对象锁定为西门子S7-1200。
| 对比维度 | 传统PC组态软件 | 手机组态APP(这套源码) |
|---|---|---|
| 部署位置 | 工控机/触摸屏 | Android/iOS手机 |
| 画面生成 | 开发期固话,改图需重新下装 | 运行时解析配置,改配置即可 |
| 通信目标 | 多种PLC/设备 | 西门子S7-1200为主 |
| 实时性 | 高,适合密集监控 | 中等,满足移动巡检/报警确认 |
| 典型场景 | 中控室集中监控 | 移动巡检、售后远程支持、多站点查看 |
1.3 这套源码在项目中的准确位置
很多人在网上看到“C#全套源代码”这种标题,第一反应是下载下来就能直接连PLC。实际拿到手你会发现,它更像一个“可二次开发的半成品框架”。源码里把通信、配置解析、UI绑定这些骨架搭好了,你要做的是根据自己的PLC项目去填变量表、调整画面配置。
我看了这套代码后最大的感受是:它的价值不在于“点开就能用”,而在于给了你一条从C#到S7-1200、从桌面到移动端的完整技术路径。做上位机的人都知道,最花时间的不是写那几个读写指令,而是把通信层、配置层、界面层串起来时踩的那些坑。这套源码把坑先填了一部分。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型背后的逻辑:S7-1200、C#与通信协议为什么这么配
2.1 S7-1200在中小型设备里就是性价比之王
选西门子S7-1200作为目标机型不是偶然。这几年非标设备、小型产线、包装机械、环保设备上用S7-1200的比例非常高。它相比S7-200 SMART有更强的通信和数据处理能力,相比S7-300/1500又有价格优势,虽然定位是小型PLC,但配合通信模块在分布式设备上照样能撑起场面。
从软件开发角度,S7-1200有个特别友好的地方:它内置以太网口,支持西门子S7协议通信,也支持Modbus TCP,不需要额外加通信模块就能和上位机交换数据。CPU还自带一个网页服务器,工程调试时浏览器就能看变量状态。这套手机APP选它做监控对象,覆盖面足够大。
2.2 通信协议走哪条路:S7协议、Modbus TCP还是OPC UA
这是做前期技术选型必须先定的问题。S7-1200至少有三条常见路可以走,我在实际项目里都试过,差别还是挺明显的。
**S7协议(S7comm)**是西门子原生协议,直接走TCP 102端口。优点是数据类型覆盖完整,能直接读I/Q/M/DB/定时器/计数器,通信效率高,标签名字可以直接用类似“DB1.DBD0”“M2.0”这种西门子风格地址,和TIA Portal里的变量对应关系非常直观。缺点是不跨品牌,只适用于西门子生态。
Modbus TCP是通用协议,S7-1200在TIA Portal里启用Modbus TCP指令库就能用,跨品牌PLC都能通。但它本质上是以寄存器为单位的通信方式,布尔量、浮点数、32位整数都要自己做位拆解和字节顺序转换,程序里到处是ushort数组和Convert操作,代码写起来就没那么优雅了。
OPC UA是工业通信的大趋势,通信能力强、安全性好,但S7-1200上跑OPC UA受固件版本和许可证限制,手机端实现UA客户端也很重,对轻量APP来说性价比不高。
所以,像这套源码这样针对单个S7-1200做移动监控,最合理的方案就是直接用S7协议通信,省去很多中间转换。
| 通信方式 | 通信效率 | 开发成本 | 跨品牌 | 适用场景 |
|---|---|---|---|---|
| S7协议 | 高 | 低 | 不跨,仅西门子 | 单品牌PLC设备监控 |
| Modbus TCP | 中 | 中 | 支持 | 多品牌设备混合 |
| OPC UA | 中高 | 高 | 支持 | 系统级集成、大型SCADA |
2.3 用C#做手机APP:现实路线而不是理想路线
既然确认了S7协议,下一步就是怎么用C#把它跑在手机上。这里有个现实背景要讲清楚:工业软件圈子里面C#的存量太大了,从PC上位机、MES客户端到各种工具软件,大量代码是用C#写的。很多工程师手里的上位机代码就是基于Windows Forms或者WPF的,现在要做手机端,自然希望尽量复用C#的经验,而不是转头去学一套完全陌生的移动开发技术。
C#做手机APP的路线主要有三条:
Xamarin.Forms是老牌的跨平台方案,很多现成的工控源码都是用这个框架写的。虽然微软已经把它并入.NET MAUI生态,但存量项目非常多,得学会看。
.NET MAUI是Xamarin.Forms的继承者,统一了Android、iOS、Windows、macOS的UI层,C#和XAML的开发方式和WPF很像。对新项目来说,直接用.NET MAUI更合适,通信库、数据模型、业务逻辑这部分代码基本不用改,主要换的是UI层语法细节。
Blazor Hybrid是另一个思路,UI走Web技术栈,C#在底层跑逻辑。适合团队里前端能力强的场景,但工控UI用Blazor的相对少,参考资料也少一些。
HslCommunication是这套源码里最关键的第三方库。作者是国内的工业通信开发大神,专门帮C#开发者解决PLC通信问题。SiemensS7Net封装得非常干净,读Bool、读Int16、读字符串、写DB块都有现成方法,底层还会自动做报文组包和状态管理。比起自己从零写S7报文解析要省太多事了。
2.4 这种组合为什么适合你直接上手
C# + HslCommunication + S7-1200这套组合在工控圈里流行,根本原因就三个字:可维护。
对做项目的人来说,最怕的是买了一套源码回来,技术栈冷门到全公司没人看得懂。C#太普及了,即使做PLC的电气工程师也能看懂个大概;HslCommunication是国产库,中文文档和社区讨论多,出了问题查起来快;S7-1200就更不用说了,调试工具、示例程序遍地都是。这套组合是典型的“稳、熟、快”。
3. 源码的整体架构:这套手机组态软件是如何组织起来的
3.1 四个核心模块的职责划分
拿到手第一件事,先把整个解决方案的项目结构捋清楚。这类APP源码通常跑起来功能看着不复杂,但它代码组织得好不好,直接决定你二次开发时是省力还是想骂人。这套源码的模块边界比较清晰,我拆解下来主要有四层。
通信层:核心类是S7CommunicationService,封装了HslCommunication的SiemensS7Net。外部界面不直接碰PLC通信库,所有读写都通过这一层转发。这么做的原因是隔离变化——以后如果要把某个设备改成Modbus TCP,只要改这一层,不用动界面。
数据模型层:TagItem是单个监控点的数据结构,包含变量名、地址、数据类型、当前值、刷新时间、报警阈值这些信息;DeviceGroup是设备分组,一台S7-1200对应一个DeviceGroup,里面装了一堆TagItem。这个模型是整个组态配置的落点。
配置解析层:ConfigService负责读取配置文件,把JSON反序列化成DeviceGroup和TagItem集合。手机APP启动时先加载配置,再根据配置动态生成监控页面。
UI表现层:采用MVVM模式,View怎么显示由ViewModel里的TagItem集合驱动。列表项通过DataTemplate定义,状态变化通过INotifyPropertyChanged通知机制映射到界面颜色和文本。
3.2 项目目录与命名空间规划
源码的目录结构大致是下面这样的。我稍微做了整理,便于说明问题:
text复制S7MobileSCADA/
├─ App.xaml // 应用入口
├─ AppShell.xaml // 页面导航容器
├─ Models/
│ ├─ TagItem.cs // 监控点数据模型
│ ├─ DeviceGroup.cs // 设备分组模型
│ └─ AlarmRecord.cs // 报警记录模型
├─ Services/
│ ├─ S7CommunicationService.cs // S7通信封装(核心)
│ ├─ ConfigService.cs // 组态配置加载
│ └─ PollingService.cs // 轮询调度服务
├─ ViewModels/
│ ├─ MonitorViewModel.cs // 监控页ViewModel
│ └─ SettingViewModel.cs // 设置页ViewModel
├─ Views/
│ ├─ MonitorPage.xaml // 实时监控页面
│ └─ SettingPage.xaml // 连接设置页面
├─ Converters/
│ ├─ ValueToColorConverter.cs // 值转颜色
│ └─ BoolToTextConverter.cs // 布尔转文本
├─ Config/
│ └─ monitor_config.json // 组态配置文件
└─ Helpers/
└─ JsonConvertHelper.cs // JSON序列化辅助
你要是自己从头写过类似项目,会发现这个结构其实非常经典。通信服务、配置服务、轮询服务都是单例注册的,用依赖注入容器管理生命周期,页面之间通过Shell导航传参。这套结构的好处是每个类的职责很单一,你要调试通信问题就只看Services/S7CommunicationService.cs,不用满项目翻代码。
3.3 组态配置文件的JSON设计
组态软件的核心是“配置驱动界面”。这套源码里的monitor_config.json是灵魂文件,我摘一段简化后的示例:
json复制{
"device": {
"name": "包装线PLC",
"ip": "192.168.1.10",
"port": 102,
"plcType": "S1200",
"pollInterval": 500
},
"elements": [
{
"type": "lamp",
"tag": "M1.0",
"text": "运行中",
"colorOn": "#00C853",
"colorOff": "#BDBDBD"
},
{
"type": "button",
"tag": "M2.0",
"text": "启动",
"confirm": true
},
{
"type": "text",
"tag": "DB1.DBD0",
"format": "0.0",
"unit": "℃"
}
]
}
这段JSON描述了三类常见组态元素:指示灯(lamp)、按钮(button)、数值显示(text)。每个元素通过type字段决定控件模板,通过tag字段指定要读写的PLC地址,再通过若干UI属性描述显示样式。解析时把JSON反序列化成配置对象,UI层遍历配置对象集合,为每个元素创建对应的控件实例。
这样做带来的直接收益是:以后你要新增一个监控画面,根本不用重新编译APP。把新的JSON配置推到手机上,重启应用或者点击刷新,新画面就出来了。对设备制造商来说,这意味着同一套APP可以适配不同型号的设备,只需要随设备附带不同的配置文件即可。
3.4 核心数据流:轮询刷新与UI绑定的协作方式
把数据流理顺,整段代码就活起来了。这套源码的运行流程是固定周期循环:
- PollingService按pollInterval设定的间隔触发一次采集。
- 通信层把本次需要读取的所有地址批量发给S7-1200。
- PLC返回数据后,解析成Dictionary<string, object>,键是变量名,值是对应数据。
- 数据模型层用返回结果更新对应TagItem的Value属性,并触发PropertyChanged事件。
- UI层监听到值变化后,通过Converter把值转换成颜色或文本,界面自动刷新。
这个过程只要不在后台线程里直接操作UI控件,基本不会出现闪退。MVVM模式下更新的是ViewModel里的属性,UI绑定系统自己会处理线程调度,源码里这点处理得比较规范。
4. 关键代码拆解:从连接PLC到画面实时刷新的完整链路
4.1 建立S7-1200连接:参数和三个容易忽略的细节
通信层的连接建立代码大致是这样一个流程:
csharp复制public class S7CommunicationService
{
private SiemensS7Net _plc;
public bool Connect(string ip, int port = 102)
{
_plc = new SiemensS7Net(SiemensPlc.S1200, ip);
_plc.Port = port;
_plc.ConnectTimeOut = 3000;
_plc.OperationTimeout = 3000;
var result = _plc.ConnectServer();
return result.IsSuccess;
}
}
有三个细节特别容易踩坑:
第一个是PLC类型必须选对。SiemensPlc枚举里有一个S1200的选项,很多从S7-300时代转过来的老手习惯选S300或者S400,结果通信不上或者读出来的地址对不上。S7-1200和S7-300内部有些地址映射规则不一样,直接套用会出怪问题。
第二个是机架号和槽号。S7-1200的默认Rack一般是0、Slot一般是0,不需要像S7-300那样设置Rack=0、Slot=2。如果你从老项目里复制了一段创建实例的代码,带着Slot=2去连S7-1200,大概率连不上或者不稳定。HslCommunication的西门子接口里,S7-1200这个枚举已经处理好了默认机架槽号,不需要额外设置。
第三个是连接超时时间不要设得太短。手机在Wi-Fi网络下通信本身就有延迟波动,如果ConnectTimeOut设成几百毫秒,稍微有点网络抖动就直接报连接失败。实测在厂房Wi-Fi环境下,3秒是相对合理的起点。
4.2 批量读取与单点写入:通信层如何封装成统一服务
连接建立之后,最核心的就是读写了。很多初学的朋友最容易写出来的代码是一个标签一个标签地循环调用ReadBool、ReadInt16,这样能用,但绝对不是好写法。因为每次调用Read系列方法都要走一遍“组请求报文→发送→等待响应→解析响应”的完整往返,几十个变量轮一圈,耗时成倍上涨。
正确的做法是尽量使用批量读取。HslCommunication提供了Read(string address, ushort length)方法,可以读取连续的地址数据,再在本地按偏移量解析。不过要注意,S7协议并不是所有地址都支持任意跨区连续读取。在DB块内部连续地址范围内做批量读取是最稳的,而M区和I/Q区用按位读更实际。
把读写封装成统一服务后,页面代码就变得非常干净:
csharp复制public async Task<object> ReadTagAsync(TagItem tag)
{
try
{
switch (tag.DataType)
{
case "bool":
var boolResult = _plc.ReadBool(tag.Address);
return boolResult.IsSuccess ? boolResult.Content : null;
case "int16":
var int16Result = _plc.ReadInt16(tag.Address);
return int16Result.IsSuccess ? int16Result.Content : null;
case "float":
var floatResult = _plc.ReadFloat(tag.Address);
return floatResult.IsSuccess ? floatResult.Content : null;
default:
return null;
}
}
catch (Exception ex)
{
// 记录异常,返回null
return null;
}
}
写入操作则要根据变量类型走不同方法:
csharp复制public bool WriteTag(TagItem tag, object value)
{
try
{
switch (tag.DataType)
{
case "bool":
return _plc.Write(tag.Address, Convert.ToBoolean(value)).IsSuccess;
case "int16":
return _plc.Write(tag.Address, Convert.ToInt16(value)).IsSuccess;
case "float":
return _plc.Write(tag.Address, Convert.ToSingle(value)).IsSuccess;
default:
return false;
}
}
catch (Exception ex)
{
return false;
}
}
值得提醒的是:按钮这类“瞬时动作”型控件,写法和普通变量不一样。工业上位机里常见做法有两种,一种是按下按钮往M区写一个TRUE,PLC侧程序检测到这个TRUE后完成启动逻辑并把M区值复位;另一种是APP写入TRUE后延迟500毫秒再写入FALSE。第一种方案最可靠,建议优先与PLC程序配合。
4.3 后台轮询线程与UI线程的协作:防卡顿和防重入
轮询机制直接关系到APP的实际体验。这套源码里的PollingService使用的是异步循环方式,我这里把核心逻辑展开讲一下。
基础版可以用一个DisposableTimer来做,但更好的做法是使用async/await配合while循环,避免定时器回调重叠。核心点在于防重入:如果上一轮读取还没结束,下一轮定时又触发了,容易造成连接对象同时被多个线程使用,HslCommunication底层虽然做了部分线程安全处理,但主动防重入更稳妥。
csharp复制private SemaphoreSlim _pollLock = new SemaphoreSlim(1, 1);
private async Task PollingLoopAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
if (await _pollLock.WaitAsync(0)) // 立即返回,如果获取不到说明上一轮还在跑
{
try
{
await Task.Delay(_pollInterval);
await RefreshAllTagsAsync();
}
finally
{
_pollLock.Release();
}
}
else
{
await Task.Delay(100);
}
}
}
刷新UI部分要记住一个原则:任何涉及UI控件属性变化的操作,都要回到UI线程执行。MVVM绑定虽然帮我们解决了很多线程问题,但如果在后台线程里直接修改一个非线程安全的集合,照样会崩。源码里的做法是准备好一个核心的CollectionViewSource或者ObservableCollection,然后通过Dispatcher(或者MAUI里的MainThread.BeginInvokeOnMainThread)来更新数据源。我见过不少自己重写的版本在这里偷懒,最后都付出了闪退的代价。
4.4 把JSON组态翻译成界面控件的核心逻辑
UI层可能是最容易被低估的一块。很多人觉得组态不就是ListView绑定一下嘛,实际上真正做起来,控件不仅要创建出来,还要在运行过程中持续接收值更新、响应点击事件。
源码里的大致逻辑是:MonitorPage加载完成后,调用ConfigService读取配置文件,得到List
以指示灯为例,核心绑定可以简化成下面这样:
xml复制<Border>
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="{Binding Value, Converter={StaticResource BoolToBrushConverter}, ConverterParameter=#00C853|#BDBDBD}" />
</Style>
</Border.Style>
</Border>
BoolToBrushConverter的作用就是把布尔值映射成两种颜色。当PLC里M1.0为TRUE时,指示灯变绿;为FALSE时,变灰。这种绑定的好处是,轮询线程更新TagItem.Value之后,界面自动响应,中间不需要手动写一行刷新代码。
按钮元素则要额外处理点击事件。源码里按钮按下后,会先弹出确认对话框(对应JSON里的"confirm": true),确认后调用WriteTag方法写入对应地址。这个确认对话框不是可有可无的,尤其在做远程启动、远程急停复位这类操作时,误触一次就可能造成事故。
4.5 报警状态的颜色变化与本地记录
报警是监控类APP的刚需。这套源码的报警逻辑不复杂:每个TagItem可以配置上限下限,轮询更新值之后,把当前值和上下限做比较,越限则把IsAlarm属性置为TRUE,UI触发颜色变化和声音提示。
本地报警记录用SQLite存储是C#生态里很成熟的方案。sqlite-net-pcl这个库够轻量,和Xamarin/MAUI配合得很好。报警记录至少要存这些字段:报警时间、设备名、变量名、报警描述、报警值、解除时间。因为在手机APP端不适合做太复杂的历史趋势分析,本地记录的核心用途是事后追溯和给售后人员提供证据。
csharp复制public class AlarmRecord
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public DateTime AlarmTime { get; set; }
public string DeviceName { get; set; }
public string TagAddress { get; set; }
public string TagDescription { get; set; }
public string AlarmValue { get; set; }
public DateTime? ConfirmTime { get; set; }
}
5. 实机调试与落地过程中最容易踩的坑
5.1 手机连不上PLC?按这个顺序逐项排查
代码写完了,连上真机准备调试,最常见的现象就是“连接失败”。很多朋友第一反应是改代码,其实大部分情况下问题出在环境配置。
我把自己排查这个问题的顺序整理成了表格,你按顺序来,基本十分钟内能找到原因。
| 排查项 | 操作方式 | 原因 |
|---|---|---|
| 手机和PLC是否在同一网段 | 手机WiFi查看IP,和PLC的IP对比 | 不在同一网段根本不通 |
| PLC的IP是否填写正确 | 在TIA Portal的设备和网络里确认 | 填错一个数字就是另一个世界 |
| TIA Portal是否正在在线监控 | 关闭TIA Portal的在线功能 | STEP 7在线会占用PLC连接资源 |
| S7-1200是否允许PUT/GET通信 | CPU属性→保护与安全→连接机制→勾选“允许来自远程对象的PUT/GET通信访问” | 默认可能不允许,不勾选就拒绝上位机连接 |
| 电脑防火墙/手机安全软件 | 临时关闭测试一次 | 防火墙会拦掉TCP 102端口 |
| 连接超时设置是否过短 | ConnectTimeOut设为3000ms以上 | 厂房WiFi延迟高,超时太短必挂 |
这里要特别强调PUT/GET通信访问这个选项。S7-1200出于安全考虑,默认情况可能不开放远程PUT/GET通信,需要在TIA Portal里手动打开。很多第一次做S7-1200上位机通信的人都会在这里卡住,而HslCommunication报出来的错误提示不一定能直接看出来是权限问题。
5.2 S7-1200的连接资源限制:不是谁想连就能连
S7-1200虽然是以太网通信,但它同时能承载的HMI/OPC连接数是有限的。CPU型号不同数量有差异,有些入门级CPU可能只能同时支持三四个连接,而且TIA Portal在线监视也要占用一个连接槽位。这意味着,如果电脑开着TIA Portal在线,同时又有PC上位机连PLC,你再拿手机去连,很可能就报“资源不足”或者直接拒绝连接。
生产环境里要避免这个问题,就一条原则:APP使用长连接,不要每次都断开重连。HslCommunication的ConnectServer之后,把连接对象保存为单例,整个APP生命周期内复用,不要每个页面重新连一次。否则不仅影响自身,多个APP实例反复连接还会占满PLC的连接资源,直接影响生产。
5.3 轮询周期和批量读取:性能优化的两个突破口
手机APP监控PLC,性能瓶颈通常不在手机CPU,而在网络往返和PLC响应能力。S7-1200的性能虽然不错,但如果APP每一轮都单个变量逐个读,一次轮询可能要发几十个请求,PLC光是处理通信请求就要花不少时间,正常的设备逻辑执行反而受影响。
我实际使用下来的经验值如下,供你做参考:
| 监控点数 | 轮询周期 | 读取方式 |
|---|---|---|
| 1-10个 | 500ms-1s | 单点读取可以接受 |
| 10-50个 | 500ms-1s | 尽量在DB块内连续读取 |
| 50个以上 | 1s以上 | 建议分区域批量读取,避免单轮时间过长 |
另外,不要做什么“毫秒级刷新”这种无意义的事。手机端的人眼感知极限也就100ms左右,而PLC的主要任务是控制设备,不是伺候上位机。500ms的轮询周期对绝大多数设备监控场景已经绰绰有余,还能大幅降低PLC的通信负载。
5.4 断线重连与异常恢复:生产环境不能一断了之
厂房里的Wi-Fi环境和办公室完全不一样,AP漫游、信号遮挡、电磁干扰都会造成TCP连接断开。APP如果没有断线重连机制,设备断一次网就得重新打开APP,那这软件基本没法用。
断线重连的实现思路是:在轮询循环里检测读取结果,如果连续多次读取失败,判断为掉线,停止轮询,进入重连模式。重连采用指数退避策略,第一次等1秒,第二次2秒,第三次4秒,最大间隔不超过30秒。同时UI要明确显示“离线”状态,不能假装一切正常。
csharp复制private async Task TryReconnectAsync()
{
var retryDelay = 1000;
while (!_cancellationToken.IsCancellationRequested)
{
var result = _plc?.ConnectServer();
if (result != null && result.IsSuccess)
{
_isOnline = true;
RaiseStatusChanged();
return;
}
await Task.Delay(retryDelay);
retryDelay = Math.Min(retryDelay * 2, 30000);
}
}
这里有个细节:HslCommunication的连接对象在断线后,内部状态可能会变脏,重连前最干净的做法是new一个新的SiemensS7Net实例,用原来的IP和端口重新创建,而不是在旧对象上反复尝试ConnectServer。这一点很多人容易忽略。
5.5 安全性考虑:局域网直连与远程接入的取舍
手机APP直接连PLC,从安全角度讲是有讲究的。如果APP只用于厂房现场WiFi下的本地调试和点检,那S7协议直连没有问题,做好前面说的PUT/GET权限控制就行。但如果想让手机在外面通过公网访问车间里的PLC,直接把PLC的102端口映射到公网是非常危险的行为,等于把设备暴露在网络扫描器之下,分分钟被非法读写。
比较稳的做法是使用工业物联网网关或者支持云转发的通信设备。PLC通过网关接入云平台,手机APP连的是网关或者云服务,APP不直接访问PLC。这样做还有一个额外的好处——网关可以做数据缓存和访问控制,给PLC加了一层保护。
6. 从这套源码继续做下去的几个方向
如果你已经把这套源码跑通了,接下来想让它真正落地到项目里,我个人觉得有这几次方向值得优先做。
第一是补趋势曲线。现在只有实时数据刷新,长期变化趋势在场站类项目里是刚需。可以用OxyPlot这个C#图表库,把最近一小时、一天的温度、压力、电机电流画成曲线,配上SQLite的历史数据存储,体验基本就能接近商业组态软件了。
第二是完善报警推送。报警不能只在APP界面变颜色,要主动推消息。Android可以用本地通知加前台服务保持常驻;如果要做到微信、钉钉推送,就需要接一个消息服务端中转。考虑到都上C#了,直接用SignalR推给APP是性价比最高的方案。
第三是支持多设备切换。实际项目里一个调试工程师往往负责好几个设备,每个设备一套配置。可以把monitor_config.json做成配置列表,APP里做一个设备选择页,按设备加载不同配置。这个改动不难,但非常实用。
第四是加用户权限和操作日志。远程操作PLC是高风险动作,必须有权限控制。至少要做到:写操作需要登录验证,所有写操作记录日志(时间、用户、操作内容、结果)。这是设备安全的底线,别偷懒。
最后再说一个我自己在实际操作中的体会:这类手机监控APP最怕功能堆砌。有人觉得手机APP就要大而全,把PC组态那套复杂的配方管理、权限矩阵、多级报警全部搬到手机上,最后发现屏幕就那么大,操作繁琐,没人愿意用。我做了几个项目之后的结论是,手机端守住“查看状态、响应报警、紧急操作”这三个核心功能就够了,做得克制一点,维护成本低,现场用户的接受度反而高。
