Unity直连西门子PLC:基于S7.Net开源方案实现工业仿真与数据通信
1. 项目概述与核心价值最近在做一个工业仿真项目需要让Unity构建的PC端或Web端应用能够实时读取和写入西门子S7-1200/1500系列PLC的数据。这听起来像是典型的数字孪生或上位机监控场景。市面上方案不少但要么是商业方案价格不菲要么是封装好的黑盒出了问题无从下手。经过一番调研和折腾我最终选择了一条完全开源、透明且可控的技术路线Unity S7.Net TIA Portal S7-PLCSIM Advanced。这个组合拳打下来不仅实现了需求更让我对整个工业通信的底层逻辑有了更深的理解。如果你也在为Unity与西门子PLC的通信头疼或者想自己搭建一个轻量级的仿真测试环境这篇记录或许能帮你少走很多弯路。简单来说这个项目的核心目标是在Unity中不依赖任何昂贵的中间件或OPC服务器直接通过S7协议与西门子PLC无论是真实的还是仿真的进行数据交换。这尤其适合用于产品前期的功能验证、操作员培训系统开发或者对实时性要求不是极端苛刻的监控可视化场景。整个方案的成本几乎为零除了你已有的Unity和TIA Portal授权并且所有代码和配置对你都是完全开放的。2. 技术栈选型与深度解析为什么是这几个工具的组合这背后是经过深思熟虑的。市面上Unity连接PLC的常见做法是使用OPC UA这固然是标准但引入OPC UA服务器如KEPServerEX增加了复杂度、成本和单点故障。而S7.Net这个开源库直接实现了西门子私有的S7协议基于ISO-on-TCP端口102相当于在应用层绕过了OPC直连PLC。这对于追求轻量化、可控化的项目来说是绝佳选择。2.1 S7.Net开源库的利与弊S7.Net是一个用C#编写的开源库托管在GitHub上。它的最大优势是纯粹和直接。你不需要在PLC侧安装任何额外的软件或授权只需要知道PLC的IP地址、机架号、槽号以及你要访问的数据块DB地址就可以进行读写。注意S7协议是西门子的私有协议这意味着S7.Net是通过逆向工程实现的。虽然它非常稳定广泛应用于各种开源SCADA和上位机项目中但在极其严苛、涉及人身安全或高价值生产的场景下使用前仍需进行充分的测试和评估。对于仿真、培训和一般监控它完全够用。它的工作原理是封装了S7协议的报文构建和解析。当你调用Read(DataType dataType, int db, int startByteAdr, int count)方法时库会帮你构建一个符合S7规范的读取请求报文发送到PLC的102端口并解析返回的报文将原始的字节流转换成C#中的变量如int, float, bool数组等。2.2 TIA Portal V16不仅仅是编程软件博图TIA Portal在这里扮演了两个核心角色。第一它是PLC的编程和组态环境我们需要用它来编写PLC程序并定义好与Unity通信的数据接口通常是一个或多个数据块DB。第二它的“在线与诊断”功能至关重要。当通信出现问题时我们可以在这里监控PLC的实时状态、查看数据块的实际值这是排查故障的黄金窗口。2.3 S7-PLCSIM Advanced V3.0仿真环境的革命这是整个方案能跑通的关键一环。普通的S7-PLCSIM只能仿真PLC的逻辑其网络接口是虚拟的外部软件如Unity无法通过真实网络与其通信。而S7-PLCSIM Advanced则不同它会在你的电脑上创建一个虚拟的以太网适配器并绑定一个虚拟的PLC实例。这个虚拟PLC拥有真实的IP地址能够响应来自同一台电脑甚至同一局域网内其他电脑的S7协议请求。这意味着你可以在没有真实PLC硬件的情况下完整地开发和测试你的Unity通信程序。V3.0版本相比之前更稳定对S7-1500系列的支持也更完善。3. 环境搭建与核心配置实战理论说再多不如动手搭一遍。下面是我一步步搭建这个仿真测试环境的详细过程其中包含了不少官方文档里不会提的“坑点”。3.1 软件安装与顺序安装顺序很重要乱序可能导致软件间无法正确识别。首先安装TIA Portal V16。确保安装时选择了所有需要的组件包括STEP 7 Professional用于S7-1200/1500编程。然后安装S7-PLCSIM Advanced V3.0。安装过程中它会自动检测已安装的TIA Portal版本并进行集成。安装完成后务必以管理员身份运行一次它的控制台S7-PLCSIM Advanced V3.0 Console完成虚拟网卡的初始化。Unity环境我使用的是Unity 2021.3 LTS版本。这个版本比较稳定对.NET Standard 2.1支持良好兼容S7.Net库。3.2 在TIA Portal中创建仿真PLC项目打开TIA Portal新建一个项目添加一个S7-1500系列的CPU例如CPU 1511-1 PN。关键步骤在于硬件组态的网络配置进入设备视图选中CPU上的PN/IE接口。在属性窗口中为它分配一个IP地址例如192.168.10.10子网掩码255.255.255.0。这个IP将是Unity要连接的地址。新建一个全局数据块用于通信。例如创建DB1并在这个DB内定义你的变量。为了测试我定义了以下变量StartButton(Bool 地址0.0) 启动按钮由Unity写入。RunningStatus(Bool 地址0.1) 运行状态由PLC写入Unity读取。SetTemperature(Real 地址2.0) 设定温度由Unity写入。ActualTemperature(Real 地址6.0) 实际温度由PLC写入Unity读取。ProductionCount(Int 地址10.0) 产量计数由PLC写入。实操心得地址规划要清晰。Bool类型按位计算一个Byte8位可以存8个Bool。下一个变量从新的字节开始比如Real浮点数占4个字节所以ActualTemperature从地址6.0开始2.0的SetTemperature占用了2.0, 2.1, 2.2, 2.3四个字节。提前画个内存映射图会省去很多调试时的混乱。3.3 配置S7-PLCSIM Advanced并启动虚拟PLC这是最容易出错的地方。打开S7-PLCSIM Advanced控制台。点击“Create new PLC instance”。在配置中选择与你TIA项目中对应的CPU型号如CPU 1511-1 PN V3.0。在“Network”设置中关键一步将“Adapter”选择为“PLCSIM Virtual Eth. Adapter”。这个虚拟网卡是安装Advanced版本时创建的。IP地址设置必须与TIA项目中硬件组态的IP完全一致192.168.10.10。点击“Start”启动虚拟PLC实例。此时在TIA Portal中你应该可以通过“下载到设备”将程序下载到这个虚拟PLC中。在下载时选择目标设备为“PN/IE”并搜索设备你应该能看到一个IP为192.168.10.10的“S7-PLCSIM Virtual PLC”选中它并下载。下载完成后将虚拟PLC切换到“RUN”模式。3.4 在Unity中集成S7.Net从GitHub下载S7.Net的源码或直接获取其编译好的DLLS7netplus.dll。我推荐使用源码以便于调试和了解内部机制。在Unity项目中创建一个Plugins文件夹如果不存在。将S7netplus.dll放入其中。由于该库依赖System.Net.Sockets等在Unity的Player Settings中确保“Api Compatibility Level”设置为“.NET Standard 2.1”或“.NET Framework”后者兼容性更好。创建一个C#脚本例如S7Communicator.cs开始编写通信逻辑。4. Unity端通信核心代码实现与详解下面是一个最精简但功能完整的Unity端通信管理器代码框架我加了大量注释来说明每个步骤的意图和注意事项。using S7.Net; using System; using System.Net; using UnityEngine; public class S7Communicator : MonoBehaviour { // PLC连接参数可在Inspector中配置 [Header(PLC Connection Settings)] public string plcIP 192.168.10.10; public int rack 0; // 机架号对于S7-1500虚拟PLC通常是0 public int slot 1; // 槽号对于S7-1500通常是1 public int port 102; // S7协议标准端口 // 数据块定义与TIA Portal中DB1的变量对应 [Header(Data Block Mapping (DB1))] public bool startButton; // 对应 DB1.DBX0.0 public bool runningStatus; // 对应 DB1.DBX0.1 public float setTemperature; // 对应 DB1.DBD2 public float actualTemperature; // 对应 DB1.DBD6 public int productionCount; // 对应 DB1.DBW10 private Plc plc; // S7.Net的核心PLC对象 private bool isConnected false; void Start() { InitializePLCConnection(); } void InitializePLCConnection() { // 创建Plc对象CpuType.S71500 指定了PLC系列这会影响一些内部协议细节 plc new Plc(CpuType.S71500, plcIP, rack, slot); plc.Port port; Debug.Log($尝试连接PLC: {plcIP}); try { // Open()是同步方法在Unity主线程调用可能会卡顿对于复杂项目建议用异步或放到协程中 plc.Open(); isConnected plc.IsConnected; if (isConnected) { Debug.Log(PLC连接成功); // 开始周期性的数据读写循环 InvokeRepeating(nameof(ReadWriteDataCycle), 0f, 0.2f); // 每200ms循环一次 } else { Debug.LogError(PLC连接失败但未抛出异常。); } } catch (Exception ex) { Debug.LogError($连接PLC时发生异常: {ex.Message}); isConnected false; } } void ReadWriteDataCycle() { if (!isConnected || plc null) return; try { // --- 读取PLC数据 (DB1) --- // 读取多个变量时批量读取效率远高于单个读取 runningStatus (bool)plc.Read(DataType.DataBlock, 1, 0, VarType.Bit, 1); // DB1.DBX0.1 actualTemperature (float)plc.Read(DataType.DataBlock, 1, 6, VarType.Real, 4); // DB1.DBD6 productionCount (int)plc.Read(DataType.DataBlock, 1, 10, VarType.Int, 2); // DB1.DBW10 // --- 向PLC写入数据 (DB1) --- // 只有需要改变的值才写入避免不必要的通信负载 plc.Write(DataType.DataBlock, 1, 0, VarType.Bit, 1, startButton); // 写入 DB1.DBX0.0 plc.Write(DataType.DataBlock, 1, 2, VarType.Real, 4, setTemperature); // 写入 DB1.DBD2 // 可以在Unity中更新UI或触发事件 // UpdateUI(); } catch (Exception ex) { Debug.LogWarning($读写数据时出错: {ex.Message}); // 这里可以加入重连逻辑 AttemptReconnect(); } } // 示例由UI按钮触发的方法 public void OnStartButtonPressed() { startButton true; // 注意这里只是改变了C#变量的值真正的写入发生在ReadWriteDataCycle中。 // 如果要求立即写入可以在这里单独调用一次plc.Write。 plc.Write(DataType.DataBlock, 1, 0, VarType.Bit, 1, startButton); } void AttemptReconnect() { // 简单的重连逻辑 if (plc ! null) { plc.Close(); } isConnected false; Debug.Log(尝试重新连接...); InitializePLCConnection(); } void OnDestroy() { // 务必在程序退出或对象销毁时关闭连接释放资源 if (plc ! null plc.IsConnected) { CancelInvoke(nameof(ReadWriteDataCycle)); plc.Close(); Debug.Log(PLC连接已关闭。); } } }4.1 关键参数解析与避坑指南CpuType 必须选对。S7-1200用S71200S7-1500用S71500。选错可能导致连接失败或数据解析错误。Rack和Slot 对于S7-1500机架号Rack通常为0槽号Slot通常为1。你可以在TIA Portal的在线诊断中查看PLC的模块状态确认准确的槽号。数据类型与地址 这是最容易出错的地方。S7.Net中的VarType需要与TIA Portal中变量的数据类型严格对应。Bool-VarType.Bit 地址格式为字节地址.位地址(如 0.1)。Int-VarType.Int 占2个字节。Real-VarType.Real 占4个字节。DInt-VarType.DInt 占4个字节。批量读写 在ReadWriteDataCycle中我演示了分开读写。但实际上S7.Net支持一次性读取一个连续的数据区域。例如可以将DB1中从字节0开始的一段数据全部读回然后在C#中手动解析。这能显著减少通信次数提升效率。对于写入如果多个变量需要同时生效防止PLC看到中间状态也应使用批量写入。5. 通信调试与问题排查全记录即便按照上述步骤操作第一次也难免遇到问题。下面是我遇到过的典型问题及解决方法整理成了排查清单。5.1 连接失败PlcException: Error on connecting...这是最常见的问题原因多在网络层面。问题现象可能原因排查步骤与解决方案根本连不上超时1. IP地址错误。2. 防火墙阻止了端口102。3. S7-PLCSIM Advanced未启动或配置错误。1.Ping测试在命令提示符输入ping 192.168.10.10。如果不通检查IP配置。2.关闭防火墙临时关闭Windows防火墙或为端口102添加入站规则。3.检查虚拟PLC确认S7-PLCSIM Advanced控制台中对应实例的IP正确且状态为“Running”。在TIA Portal中尝试“在线访问”看能否搜索到该PLC。连接被拒绝1. 机架/槽号错误。2. PLC处于STOP模式。3. PLC的通信权限未开启。1. 核对TIA Portal中CPU的属性确认机架和槽号。2. 将虚拟PLC切换到RUN模式。3. 在TIA Portal的CPU属性中找到“防护与安全”-“连接机制”勾选“允许来自远程对象的PUT/GET通信访问”。这一步至关重要很多连接问题都是因为它没勾选5.2 数据读写错误能连接但读回来是0或错误值问题现象可能原因排查步骤与解决方案读取值始终为0或默认值1. DB块编号错误。2. 变量地址计算错误。3. PLC程序未将值写入该DB地址。1. 确认Unity代码中的DataBlock编号如1与TIA Portal中的DB编号一致。2.使用TIA Portal在线监控这是最强大的调试工具。在线打开DB1查看你定义的变量地址如ActualTemperature的地址是否为DB1.DBD6并确认这些地址有数值在变化。3. 在PLC程序中确保有逻辑如一个不断自增的定时器在向ActualTemperature等变量写入数据用于测试。写入PLC不生效1. 写入的地址被PLC程序立即覆盖。2. 写入的数据类型不匹配。1. 在线监控DB1看Unity写入的瞬间值是否出现又被PLC程序改掉。如果是需要协调双方的读写逻辑避免冲突。2. 仔细检查plc.Write方法中的VarType和字节长度确保与PLC中变量定义完全匹配。读取时抛出类型转换异常C#端数据类型与VarType或读取的字节长度不匹配。例如用(float)plc.Read(... VarType.Real, 4)读取一个Real长度必须是4。如果PLC中定义的是Int这里却用VarType.Real去解析必然出错。对照TIA Portal中的数据类型表逐一核对。5.3 性能与稳定性优化心得通信频率InvokeRepeating的间隔不宜过短。工业通信不是游戏循环200ms-500ms的周期对于大多数监控场景已经足够。过高的频率会增加PLC的通信处理负荷和网络流量。异常处理与重连工业现场网络可能波动。代码中必须有健壮的异常处理try-catch和重连机制如AttemptReconnect。避免一次通信失败导致整个程序卡死。异步操作上述示例是同步读写在数据量大或网络慢时会阻塞Unity主线程导致画面卡顿。对于正式项目强烈建议将S7.Net的读写操作放在单独的线程或使用async/await进行异步调用。S7.Net自身也提供了ReadAsync和WriteAsync方法。数据打包尽量减少通信次数。例如将所有需要读取的变量集中在一个连续的DB区域一次读取一个字节数组byte[]然后在C#端解析。写入亦然。这能极大提升效率。6. 从PC到WebGL的部署挑战与解决方案项目初期在PC上运行良好但当我们尝试将Unity项目构建为WebGL并部署到浏览器中运行时遇到了最大的挑战S7.Net库无法在WebGL环境下工作。原因在于S7.Net底层依赖于System.Net.Sockets命名空间这是完整的.NET框架功能。而Unity WebGL的运行时是基于Mono/WASM的出于安全和沙箱限制不支持直接的TCP Socket通信。浏览器不允许一个网页随意访问本地网络中的其他IP地址即PLC的IP这会引发严重的跨域和安全问题。6.1 WebGL通信架构重构为了解决这个问题我们必须引入一个中间层——一个后端服务器。架构从Unity(WebGL) --直接S7协议-- PLC变为Unity(WebGL) --WebSocket/HTTP-- 后端服务器 --S7协议-- PLC。后端服务器 可以使用任何你熟悉的技术栈如Node.js (JavaScript), Python (Flask/Django), ASP.NET Core (C#)等。它的核心职责有两个作为一个WebSocket服务器或提供RESTful API接收来自浏览器中Unity WebGL应用的请求。使用S7.Net库或其他语言的S7客户端库如python-snap7与真实的或仿真的PLC进行通信。Unity WebGL客户端 不再包含S7.Net。通信逻辑改为通过UnityWebRequestHTTP或第三方WebSocket库如NativeWebSocket与后端服务器交互发送“读取DB1”或“写入DB1某地址”的指令。6.2 一个简单的Node.js后端示例这里给出一个极简的Node.js Express node-snap7Node.js的S7客户端库的方案片段// server.js const express require(express); const Snap7 require(node-snap7); const app express(); app.use(express.json()); // 用于解析JSON请求体 const plc new Snap7.S7Client(); // 连接到仿真PLC function connectToPLC() { plc.ConnectTo(192.168.10.10, 0, 1, (err) { if (err) console.error(PLC连接失败:, err); else console.log(PLC连接成功); }); } connectToPLC(); // 提供读取数据的API app.get(/api/read/:db/:start/:size, (req, res) { const {db, start, size} req.params; plc.DBRead(parseInt(db), parseInt(start), parseInt(size), (err, data) { if (err) { res.status(500).json({error: err.message}); } else { // 将Buffer数据转换为JSON可传输的格式如Base64 res.json({data: data.toString(base64)}); } }); }); // 提供写入数据的API app.post(/api/write/:db/:start, (req, res) { const {db, start} req.params; const buffer Buffer.from(req.body.data, base64); // 从前端接收Base64数据 plc.DBWrite(parseInt(db), parseInt(start), buffer, (err) { if (err) { res.status(500).json({error: err.message}); } else { res.json({success: true}); } }); }); app.listen(3000, () console.log(后端服务器运行在 http://localhost:3000));6.3 Unity WebGL端的对应修改在Unity中你需要移除所有S7.Net的引用和代码改为使用UnityWebRequest// 在WebGL构建中使用的通信管理器 public class WebGLCommManager : MonoBehaviour { private string serverUrl http://localhost:3000; // 你的后端服务器地址 IEnumerator ReadDataFromServer() { string url ${serverUrl}/api/read/1/0/20; // 读取DB1从字节0开始读20个字节 using (UnityWebRequest request UnityWebRequest.Get(url)) { yield return request.SendWebRequest(); if (request.result UnityWebRequest.Result.Success) { string json request.downloadHandler.text; // 解析JSON将Base64数据转换回字节数组再按约定格式解析出各个变量 // ... 解析逻辑 ... // actualTemperature ParseFloatFromBuffer(buffer, 6); // productionCount ParseIntFromBuffer(buffer, 10); } } } IEnumerator WriteDataToServer(byte[] dataToWrite) { string base64Data Convert.ToBase64String(dataToWrite); string url ${serverUrl}/api/write/1/0; // 写入DB1从字节0开始 using (UnityWebRequest request UnityWebRequest.Post(url, )) { byte[] bodyRaw System.Text.Encoding.UTF8.GetBytes(${{\data\: \{base64Data}\}}); request.uploadHandler new UploadHandlerRaw(bodyRaw); request.downloadHandler new DownloadHandlerBuffer(); request.SetRequestHeader(Content-Type, application/json); yield return request.SendWebRequest(); // ... 处理响应 ... } } }6.4 WebGL部署的额外注意事项跨域问题CORS 如果你的Unity WebGL页面和后端服务器不在同一个域名和端口下浏览器会阻止请求。你必须在后端服务器如上面的Node.js例子中设置CORS头允许前端页面的域名进行访问。网络延迟 通信链变长了浏览器-服务器-PLC-服务器-浏览器延迟会增加。设计交互时要考虑这个因素避免需要极快响应的操作。服务器部署 最终这个后端服务器需要部署在一台能够同时访问到PLC网络和互联网或企业内网的机器上。云服务器通常无法直接访问本地PLC网络所以这种方案更适合部署在本地或工厂内网环境中。这套从PC直连到WebGL间接连接的完整方案虽然增加了后端开发的复杂度但它解锁了WebGL部署的能力使得通过浏览器进行远程、跨平台的工业仿真与监控成为了可能。整个探索过程从协议理解、环境搭建、代码编写到问题排查是一次对工业自动化与现代应用开发如何结合非常有益的实践。