聚焦于使用 C# 通过 S7.NET 库实现与西门子 S7-1200 和 S7-200 SMART PLC 的通信,结合您之前的问题

聚焦于使用 C# 通过 S7.NET 库实现与西门子 S7-1200 和 S7-200 SMART PLC 的通信,结合您之前的问题(WebSocket、QUIC、TCP、UDP、MQTT、HTTP、串口、USB 等),我将详细讲解 S7.NET 通信的原理,提供完整的 C# 代码示例(包括 WinForm 客户端与 S7-1200/S7-200 SMART 通信),并确保内容与上下文相关。

所有代码基于 .NET 8,确保跨平台兼容性(Windows、Linux、macOS),并包含详细的中文解释。我会参考提供的搜索结果(如 S7NetPlus、Sharp7 等)以及 S7.NET 文档,确保准确性和实用性,尤其针对车牌识别物联网场景。


1. S7.NET 通讯原理

1.1 S7.NET 简介S7.NET(S7NetPlus)是一个开源的 .NET 库,用于通过以太网(TCP/IP)与西门子 S7 系列 PLC(包括 S7-200、S7-300、S7-400、S7-1200、S7-1500)通信。它通过 Profinet 协议与 PLC 交互,支持读写数据块(DB)、输入(I)、输出(Q)、标志位(M)等数据区域。

S7.NET 是纯 C# 实现,支持调试,适用于工业自动化、HMI、SCADA 和物联网应用。

1.2 通信原理S7.NET 通过以下步骤与 PLC 通信:

  1. 建立连接:
    • 使用 PLC 的 IP 地址、机架号(Rack)和槽号(Slot)初始化连接。
    • S7-1200/S7-200 SMART 默认机架/槽号为 0/0。
    • 连接基于 TCP 端口 102(ISO-on-TCP)。
  2. 数据读写:
    • 绝对寻址:直接访问 PLC 数据区域,如 DB1.DBX0.0(数据块 1 的位 0)、I0.0(输入 0.0)、Q0.0(输出 0.0)、M0.0(标志位 0.0)。
    • 数据类型:支持 Bit、Byte、Word、DWord、Int、Real 等。
    • 批量读写:使用 ReadBytes 或 WriteBytes 读写多个字节。
  3. 错误处理:
    • 检查连接状态和传输错误(如超时、缓冲区无效)。
    • 使用 OperationResult 或异常处理错误。
  4. 优化:
    • S7.NET 内部优化 PDU(协议数据单元)大小,减少通信开销。
    • 支持多线程,但需为每个线程创建独立的 Plc 实例。

1.3 S7-1200 和 S7-200 SMART 配置要求

  • S7-1200:
    • 在 TIA Portal 中配置:
      • 网络:确保 PLC 启用以太网(Profinet),设置静态 IP。
      • 数据块(DB):取消“优化块访问”(Optimized Block Access),否则需使用符号寻址。
      • CPU 保护:启用“允许来自远程对象的 PUT/GET 通信”。
    • 默认端口:102。
    • 机架/槽号:0/0。
  • S7-200 SMART:
    • 使用 STEP 7-Micro/WIN SMART 配置:
      • 网络:配置以太网模块(如 EM DE01),设置 IP。
      • 数据块:S7-200 SMART 使用 V 区(变量存储区)代替 DB。
      • 访问权限:确保允许远程访问。
    • 默认端口:102。
    • 机架/槽号:0/0。
  • 注意事项(搜索结果):
    • 确保 PLC 的固件版本兼容(S7-1200 固件 3.0/4.0 已验证)。

    • 检查 DB 配置,避免“无效缓冲区”错误。

    • S7-200 SMART 可能需要额外以太网模块(如 CP243-1)。

1.4 与上下文的联系

  • TCP(前文):S7.NET 使用 TCP 端口 102,与 TCP 的三次握手和可靠性相关。
  • UDP:S7.NET 不使用 UDP,依赖 TCP 的有序传输。
  • MQTT:S7.NET 可与 MQTT 结合,将 PLC 数据发布到 MQTT Broker(如车牌数据)。
  • QUIC:QUIC 可作为 WebSocket 的传输层,间接支持 PLC 数据传输。
  • WebSocket(前文):S7.NET 读取的 PLC 数据可通过 WebSocket(wss://)传输到 Web 客户端。
  • HTTP:PLC 数据可通过 HTTP API 暴露,S7.NET 提供数据源。
  • ICMP:Ping 测试 PLC 的 IP 连通性。
  • 串口/USB:S7-200 SMART 支持串口(PPI),但 S7.NET 仅支持以太网。
  • 车牌识别(前文):S7.NET 读取车牌识别设备的 PLC 数据(如传感器状态),通过 WebSocket/MQTT 传输到服务器。

1.5 跨平台注意事项

  • .NET 8:S7.NET 兼容 .NET 8,支持 Windows、Linux、macOS。
  • NuGet 包:安装 S7netplus(Install-Package S7netplus)。
  • 网络:确保 PC 和 PLC 在同一网段,防火墙开放 TCP 102 端口。
  • 调试:使用 TIA Portal 或 STEP 7-Micro/WIN 监控 PLC 数据。

2. C# 实现 S7-1200 和 S7-200 SMART PLC 通讯

2.1 原理使用 S7.NET 实现 C# 与 S7-1200/S7-200 SMART 的通信:

  • 服务端:PLC 作为服务器,监听 TCP 102 端口。
  • 客户端:C# 程序使用 S7.NET 发起连接,读写数据。
  • 功能:
    • 连接:初始化 Plc 对象,连接 PLC。
    • 读写:访问 DB(S7-1200)或 V 区(S7-200 SMART)。
    • 错误处理:捕获连接和读写异常。
    • 车牌识别:读取 PLC 中的车牌数据(如字符串、浮点数),通过 WebSocket 传输到前端。
  • 库:S7.NET(S7netplus),无需 Siemens 官方 SDK 或 OPC 服务器。

2.2 代码示例以下是一个完整的 WinForm 程序,连接 S7-1200 和 S7-200 SMART,读取和写入车牌数据,支持实时显示。

2.2.1 项目设置

  1. 创建 WinForm 项目:dotnet new winforms -n PlcCommunication.
  2. 安装 S7.NET 库:dotnet add package S7netplus.
  3. 配置 PLC:
    • S7-1200:在 TIA Portal 创建 DB1(取消优化块访问),包含:
      • DB1.DBB0(Byte,状态标志)。
      • DB1.DBD4(Real,车牌识别置信度)。
      • DB1.DBB10(String,车牌号,长度 10)。
    • S7-200 SMART:在 STEP 7-Micro/WIN 创建 V 区:
      • VB0(Byte,状态标志)。
      • VD4(Real,置信度)。
      • VB10(String,车牌号,长度 10)。

2.2.2 WinForm 客户端代码csharp

using S7.Net;
using System;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace PlcCommunication
{
    public partial class Form1 : Form
    {
        private Plc plc;
        private TextBox textBoxIp, textBoxDb, textBoxLog;
        private Button btnConnect, btnRead, btnWrite;
        private Timer timer;

        public Form1()
        {
            InitializeComponent();
            InitializeUI();
        }

        private void InitializeUI()
        {
            textBoxIp = new TextBox { Left = 20, Top = 20, Width = 200, Text = "192.168.0.1" };
            textBoxDb = new TextBox { Left = 20, Top = 60, Width = 200, Text = "1" }; // DB1 或 V 区
            btnConnect = new Button { Text = "连接 PLC", Left = 20, Top = 100, Width = 100 };
            btnRead = new Button { Text = "读取数据", Left = 130, Top = 100, Width = 100 };
            btnWrite = new Button { Text = "写入车牌", Left = 240, Top = 100, Width = 100 };
            textBoxLog = new TextBox { Left = 20, Top = 140, Width = 400, Height = 200, Multiline = true, ScrollBars = ScrollBars.Vertical };

            Controls.AddRange(new Control[] { textBoxIp, textBoxDb, btnConnect, btnRead, btnWrite, textBoxLog });

            btnConnect.Click += BtnConnect_Click;
            btnRead.Click += BtnRead_Click;
            btnWrite.Click += BtnWrite_Click;

            // 定时读取
            timer = new Timer { Interval = 1000 };
            timer.Tick += async (s, e) => await ReadPlcDataAsync();
        }

        private async void BtnConnect_Click(object sender, EventArgs e)
        {
            try
            {
                if (plc == null || !plc.IsConnected)
                {
                    // 初始化 PLC(S7-1200 或 S7-200 SMART)
                    plc = new Plc(CpuType.S71200, textBoxIp.Text, 0, 0); // S7-200 SMART 也用 S71200
                    await Task.Run(() => plc.Open());
                    if (plc.IsConnected)
                    {
                        btnConnect.Text = "断开 PLC";
                        textBoxLog.AppendText($"已连接到 PLC: {textBoxIp.Text}\r\n");
                        timer.Start();
                    }
                    else
                    {
                        textBoxLog.AppendText("连接失败\r\n");
                    }
                }
                else
                {
                    await Task.Run(() => plc.Close());
                    plc = null;
                    btnConnect.Text = "连接 PLC";
                    textBoxLog.AppendText("已断开 PLC\r\n");
                    timer.Stop();
                }
            }
            catch (Exception ex)
            {
                textBoxLog.AppendText($"连接错误: {ex.Message}\r\n");
            }
        }

        private async void BtnRead_Click(object sender, EventArgs e)
        {
            await ReadPlcDataAsync();
        }

        private async Task ReadPlcDataAsync()
        {
            if (plc?.IsConnected != true)
            {
                textBoxLog.AppendText("请先连接 PLC\r\n");
                return;
            }

            try
            {
                int dbNumber = int.Parse(textBoxDb.Text);
                // 读取数据:状态(Byte)、置信度(Real)、车牌号(String)
                var status = await Task.Run(() => plc.Read(DataType.DataBlock, dbNumber, 0, VarType.Byte, 1));
                var confidence = await Task.Run(() => plc.Read(DataType.DataBlock, dbNumber, 4, VarType.Real, 1));
                var plateNumber = await Task.Run(() => plc.Read(DataType.DataBlock, dbNumber, 10, VarType.String, 10));

                textBoxLog.AppendText($"读取数据 - 状态: {Convert.ToByte(status)}," +
                    $" 置信度: {Convert.ToSingle(confidence):F2}, 车牌号: {plateNumber}\r\n");
            }
            catch (Exception ex)
            {
                textBoxLog.AppendText($"读取错误: {ex.Message}\r\n");
            }
        }

        private async void BtnWrite_Click(object sender, EventArgs e)
        {
            if (plc?.IsConnected != true)
            {
                textBoxLog.AppendText("请先连接 PLC\r\n");
                return;
            }

            try
            {
                int dbNumber = int.Parse(textBoxDb.Text);
                // 写入数据:状态(Byte)、置信度(Real)、车牌号(String)
                byte status = 1; // 示例状态
                float confidence = 0.95f; // 示例置信度
                string plateNumber = "ABC123"; // 示例车牌号

                await Task.Run(() =>
                {
                    plc.Write(DataType.DataBlock, dbNumber, 0, status);
                    plc.Write(DataType.DataBlock, dbNumber, 4, confidence);
                    plc.Write(DataType.DataBlock, dbNumber, 10, plateNumber);
                });

                textBoxLog.AppendText($"写入数据 - 状态: {status}, 置信度: {confidence:F2}, 车牌号: {plateNumber}\r\n");
            }
            catch (Exception ex)
            {
                textBoxLog.AppendText($"写入错误: {ex.Message}\r\n");
            }
        }

        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            timer?.Stop();
            plc?.Close();
            base.OnFormClosing(e);
        }
    }

    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

2.3 解释

  • 项目设置:
    • 创建 WinForm 项目,安装 S7netplus(dotnet add package S7netplus)。
    • 确保 PLC IP 可达(Ping 测试),防火墙开放 TCP 102 端口。
  • 界面:
    • IP 输入框:输入 PLC IP(如 192.168.0.1)。
    • DB 输入框:输入数据块编号(S7-1200 为 DB1,S7-200 SMART 为 1 表示 V 区)。
    • 按钮:连接/断开 PLC、读取数据、写入车牌。
    • 日志:显示连接状态和读写结果。
  • 功能:
    • 连接:初始化 Plc 对象,连接 S7-1200/S7-200 SMART(CpuType.S71200,机架/槽号 0/0)。
    • 读取:每秒读取 DB1/V 区的状态(Byte)、置信度(Real)、车牌号(String)。
    • 写入:写入示例数据(状态、置信度、车牌号)。
    • 定时器:每秒自动读取数据,模拟实时监控。
  • 错误处理:
    • 捕获连接异常(如 IP 不可达)、读写异常(如无效缓冲区)。
    • 搜索结果提到“无效缓冲区”错误,需确保 DB 取消优化访问。

  • 测试:
    • 配置 S7-1200(TIA Portal)或 S7-200 SMART(STEP 7-Micro/WIN)。
    • 运行程序,输入 PLC IP 和 DB 号,点击“连接 PLC”。
    • 点击“读取数据”或“写入车牌”,观察日志。
    • 使用 TIA Portal/STEP 7-Micro/WIN 监控 DB/V 区数据变化。

3. S7-1200 和 S7-200 SMART 之间的通信(通过 C# 桥接)

3.1 原理S7-1200 和 S7-200 SMART 均支持 Profinet,但它们之间无法直接通信(无内置 S7 通信协议支持)。C# 程序可作为中间桥接:

  • C# 客户端:分别连接两个 PLC,读取一个 PLC 的数据,写入另一个 PLC。
  • 数据交换:通过 S7.NET 读写 DB/V 区,同步数据。
  • 场景:车牌识别数据从 S7-1200(主控 PLC)传输到 S7-200 SMART(辅助设备)。
  • 安全机制:
    • 使用 WebSocket(wss://)传输数据到前端,确保加密(参考前文)。
    • 验证 PLC 访问权限,避免未授权访问。

3.2 代码示例以下是 WinForm 程序,桥接 S7-1200 和 S7-200 SMART,同步车牌数据。

3.2.1 代码csharp

using S7.Net;
using System;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace PlcBridge
{
    public partial class Form1 : Form
    {
        private Plc plc1200, plc200Smart;
        private TextBox textBoxIp1200, textBoxIp200Smart, textBoxDb1200, textBoxDb200Smart, textBoxLog;
        private Button btnConnect, btnSync;
        private Timer timer;

        public Form1()
        {
            InitializeComponent();
            InitializeUI();
        }

        private void InitializeUI()
        {
            textBoxIp1200 = new TextBox { Left = 20, Top = 20, Width = 200, Text = "192.168.0.1" }; // S7-1200 IP
            textBoxIp200Smart = new TextBox { Left = 20, Top = 60, Width = 200, Text = "192.168.0.2" }; // S7-200 SMART IP
            textBoxDb1200 = new TextBox { Left = 20, Top = 100, Width = 200, Text = "1" }; // S7-1200 DB1
            textBoxDb200Smart = new TextBox { Left = 20, Top = 140, Width = 200, Text = "1" }; // S7-200 SMART V 区
            btnConnect = new Button { Text = "连接 PLC", Left = 20, Top = 180, Width = 100 };
            btnSync = new Button { Text = "同步数据", Left = 130, Top = 180, Width = 100 };
            textBoxLog = new TextBox { Left = 20, Top = 220, Width = 400, Height = 200, Multiline = true, ScrollBars = ScrollBars.Vertical };

            Controls.AddRange(new Control[] { textBoxIp1200, textBoxIp200Smart, textBoxDb1200, textBoxDb200Smart, btnConnect, btnSync, textBoxLog });

            btnConnect.Click += BtnConnect_Click;
            btnSync.Click += BtnSync_Click;

            // 定时同步
            timer = new Timer { Interval = 2000 };
            timer.Tick += async (s, e) => await SyncPlcDataAsync();
        }

        private async void BtnConnect_Click(object sender, EventArgs e)
        {
            try
            {
                if (plc1200 == null || !plc1200.IsConnected)
                {
                    plc1200 = new Plc(CpuType.S71200, textBoxIp1200.Text, 0, 0);
                    await Task.Run(() => plc1200.Open());
                    if (plc1200.IsConnected)
                        textBoxLog.AppendText($"已连接到 S7-1200: {textBoxIp1200.Text}\r\n");
                    else
                        textBoxLog.AppendText("S7-1200 连接失败\r\n");
                }

                if (plc200Smart == null || !plc200Smart.IsConnected)
                {
                    plc200Smart = new Plc(CpuType.S71200, textBoxIp200Smart.Text, 0, 0);
                    await Task.Run(() => plc200Smart.Open());
                    if (plc200Smart.IsConnected)
                        textBoxLog.AppendText($"已连接到 S7-200 SMART: {textBoxIp200Smart.Text}\r\n");
                    else
                        textBoxLog.AppendText("S7-200 SMART 连接失败\r\n");
                }

                if (plc1200?.IsConnected == true && plc200Smart?.IsConnected == true)
                {
                    btnConnect.Text = "断开 PLC";
                    timer.Start();
                }
                else
                {
                    btnConnect.Text = "连接 PLC";
                    timer.Stop();
                }
            }
            catch (Exception ex)
            {
                textBoxLog.AppendText($"连接错误: {ex.Message}\r\n");
            }
        }

        private async void BtnSync_Click(object sender, EventArgs e)
        {
            await SyncPlcDataAsync();
        }

        private async Task SyncPlcDataAsync()
        {
            if (plc1200?.IsConnected != true || plc200Smart?.IsConnected != true)
            {
                textBoxLog.AppendText("请先连接两个 PLC\r\n");
                return;
            }

            try
            {
                int db1200 = int.Parse(textBoxDb1200.Text);
                int db200Smart = int.Parse(textBoxDb200Smart.Text);

                // 从 S7-1200 读取数据
                var status = await Task.Run(() => plc1200.Read(DataType.DataBlock, db1200, 0, VarType.Byte, 1));
                var confidence = await Task.Run(() => plc1200.Read(DataType.DataBlock, db1200, 4, VarType.Real, 1));
                var plateNumber = await Task.Run(() => plc1200.Read(DataType.DataBlock, db1200, 10, VarType.String, 10));

                // 写入 S7-200 SMART
                await Task.Run(() =>
                {
                    plc200Smart.Write(DataType.DataBlock, db200Smart, 0, status);
                    plc200Smart.Write(DataType.DataBlock, db200Smart, 4, confidence);
                    plc200Smart.Write(DataType.DataBlock, db200Smart, 10, plateNumber);
                });

                textBoxLog.AppendText($"同步数据 - 状态: {Convert.ToByte(status)}, " +
                    $"置信度: {Convert.ToSingle(confidence):F2}, 车牌号: {plateNumber}\r\n");
            }
            catch (Exception ex)
            {
                textBoxLog.AppendText($"同步错误: {ex.Message}\r\n");
            }
        }

        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            timer?.Stop();
            plc1200?.Close();
            plc200Smart?.Close();
            base.OnFormClosing(e);
        }
    }

    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

3.3 解释

  • 界面:
    • 输入框:S7-1200 IP(192.168.0.1)、S7-200 SMART IP(192.168.0.2)、DB 号(1)。
    • 按钮:连接/断开 PLC、同步数据。
    • 日志:显示连接状态和同步结果。
  • 功能:
    • 连接:分别连接 S7-1200 和 S7-200 SMART,验证连接状态。
    • 同步:从 S7-1200 的 DB1 读取状态、置信度、车牌号,写入 S7-200 SMART 的 V 区。
    • 定时器:每 2 秒自动同步数据,模拟实时桥接。
  • 错误处理:
    • 捕获连接异常、读写异常。
    • 搜索结果提到 S7-200 SMART 连接问题,需确保以太网模块(如 CP243-1)正确配置。

  • 测试:
    • 配置两个 PLC(S7-1200 DB1,S7-200 SMART V 区)。
    • 运行程序,输入 IP 和 DB 号,点击“连接 PLC”。
    • 点击“同步数据”,观察日志和 PLC 数据变化。
    • 使用 TIA Portal/STEP 7-Micro/WIN 验证 S7-200 SMART 的 V 区数据。

4. 结合 WebSocket 传输 PLC 数据(安全机制)

4.1 原理将 S7.NET 读取的 PLC 数据通过 WebSocket(wss://)传输到前端,结合前文的安全机制:

  • TLS:使用 wss:// 加密通信。
  • JWT 认证:验证客户端身份。
  • Origin 检查:限制连接来源。
  • 心跳:Ping/Pong 保持连接。
  • 场景:S7-1200 采集车牌数据,C# 桥接到 S7-200 SMART,并通过 WebSocket 广播到 Web 客户端。

4.2 代码示例以下是扩展的 WinForm 程序,结合 S7.NET 和 WebSocket,传输车牌数据。

4.2.1 服务端(ASP.NET Core,WebSocket 和 S7.NET)csharp

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using S7.Net;
using System;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace PlcWebSocketServer
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>().UseKestrel(options =>
                    {
                        options.ListenAnyIP(5001, listenOptions =>
                        {
                            listenOptions.UseHttps("certificate.pfx", "password"); // SSL 证书
                        });
                    });
                });
    }

    public class Startup
    {
        private readonly Plc plc1200 = new Plc(CpuType.S71200, "192.168.0.1", 0, 0);
        private readonly ConcurrentDictionary<string, WebSocket> clients = new();

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseWebSockets();
            app.Use(async (context, next) =>
            {
                if (context.Request.Path == "/ws/plate")
                {
                    if (context.WebSockets.IsWebSocketRequest)
                    {
                        string origin = context.Request.Headers["Origin"];
                        if (origin != "http://localhost" && origin != "https://localhost")
                        {
                            context.Response.StatusCode = 403;
                            await context.Response.WriteAsync("无效的 Origin");
                            return;
                        }

                        using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
                        string clientId = Guid.NewGuid().ToString();
                        clients.TryAdd(clientId, webSocket);
                        await HandleWebSocketAsync(context, webSocket, clientId);
                    }
                    else
                    {
                        context.Response.StatusCode = 400;
                    }
                }
                else
                {
                    await next();
                }
            });

            // 定时读取 PLC 数据并广播
            _ = Task.Run(async () =>
            {
                try
                {
                    plc1200.Open();
                    while (true)
                    {
                        if (plc1200.IsConnected)
                        {
                            var status = plc1200.Read(DataType.DataBlock, 1, 0, VarType.Byte, 1);
                            var confidence = plc1200.Read(DataType.DataBlock, 1, 4, VarType.Real, 1);
                            var plateNumber = plc1200.Read(DataType.DataBlock, 1, 10, VarType.String, 10);

                            var plateData = new
                            {
                                PlateNumber = plateNumber.ToString(),
                                Confidence = Convert.ToSingle(confidence),
                                Status = Convert.ToByte(status),
                                Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
                            };
                            string json = JsonSerializer.Serialize(plateData);
                            await BroadcastMessageAsync(json);
                        }
                        await Task.Delay(2000);
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"PLC 错误: {ex.Message}");
                }
            });
        }

        private async Task HandleWebSocketAsync(HttpContext context, WebSocket webSocket, string clientId)
        {
            var buffer = new byte[1024 * 4];
            try
            {
                while (webSocket.State == WebSocketState.Open)
                {
                    var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
                    if (result.MessageType == WebSocketMessageType.Text)
                    {
                        string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
                        await BroadcastMessageAsync(message);
                    }
                    else if (result.MessageType == WebSocketMessageType.Close)
                    {
                        clients.TryRemove(clientId, out _);
                        await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "关闭连接", CancellationToken.None);
                        break;
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"WebSocket 错误: {ex.Message}");
            }
            finally
            {
                clients.TryRemove(clientId, out _);
            }
        }

        private async Task BroadcastMessageAsync(string message)
        {
            byte[] buffer = Encoding.UTF8.GetBytes(message);
            foreach (var client in clients)
            {
                if (client.Value.State == WebSocketState.Open)
                {
                    await client.Value.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
                }
            }
            Console.WriteLine($"广播消息: {message}");
        }
    }
}

4.2.2 客户端(WinForm,接收 WebSocket 数据)csharp

using System;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace PlcWebSocketClient
{
    public partial class Form1 : Form
    {
        private ClientWebSocket client;
        private TextBox textBoxAddress, textBoxLog;
        private Button btnConnect;

        public Form1()
        {
            InitializeComponent();
            InitializeUI();
        }

        private void InitializeUI()
        {
            textBoxAddress = new TextBox { Left = 20, Top = 20, Width = 200, Text = "wss://localhost:5001/ws/plate" };
            btnConnect = new Button { Text = "连接 WebSocket", Left = 20, Top = 60, Width = 150 };
            textBoxLog = new TextBox { Left = 20, Top = 100, Width = 400, Height = 200, Multiline = true, ScrollBars = ScrollBars.Vertical };

            Controls.AddRange(new Control[] { textBoxAddress, btnConnect, textBoxLog });

            btnConnect.Click += BtnConnect_Click;
        }

        private async void BtnConnect_Click(object sender, EventArgs e)
        {
            try
            {
                if (client == null || client.State != WebSocketState.Open)
                {
                    client = new ClientWebSocket();
                    client.Options.AddSubProtocol("websocket");
                    client.Options.ClientCertificates = new System.Security.Cryptography.X509Certificates.X509CertificateCollection();
                    await client.ConnectAsync(new Uri(textBoxAddress.Text), CancellationToken.None);
                    btnConnect.Text = "断开 WebSocket";
                    textBoxLog.AppendText($"已连接到 WebSocket: {textBoxAddress.Text}\r\n");

                    _ = Task.Run(() => ReceiveMessagesAsync(client));
                }
                else
                {
                    await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "客户端关闭", CancellationToken.None);
                    client = null;
                    btnConnect.Text = "连接 WebSocket";
                    textBoxLog.AppendText("已断开 WebSocket\r\n");
                }
            }
            catch (Exception ex)
            {
                textBoxLog.AppendText($"WebSocket 错误: {ex.Message}\r\n");
            }
        }

        private async Task ReceiveMessagesAsync(ClientWebSocket client)
        {
            var buffer = new byte[1024 * 4];
            try
            {
                while (client.State == WebSocketState.Open)
                {
                    var result = await client.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
                    if (result.MessageType == WebSocketMessageType.Text)
                    {
                        string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
                        try
                        {
                            var plateData = JsonSerializer.Deserialize<PlateData>(message);
                            textBoxLog.Invoke((Action)(() => textBoxLog.AppendText(
                                $"收到车牌: {plateData.PlateNumber} - 置信度: {plateData.Confidence:F2} - " +
                                $"状态: {plateData.Status} - 时间: {plateData.Timestamp}\r\n")));
                        }
                        catch
                        {
                            textBoxLog.Invoke((Action)(() => textBoxLog.AppendText($"收到消息: {message}\r\n")));
                        }
                    }
                    else if (result.MessageType == WebSocketMessageType.Close)
                    {
                        await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "服务端关闭", CancellationToken.None);
                        break;
                    }
                }
            }
            catch (Exception ex)
            {
                textBoxLog.Invoke((Action)(() => textBoxLog.AppendText($"接收错误: {ex.Message}\r\n")));
            }
        }

        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            client?.CloseAsync(WebSocketCloseStatus.NormalClosure, "窗体关闭", CancellationToken.None).GetAwaiter().GetResult();
            base.OnFormClosing(e);
        }
    }

    public class PlateData
    {
        public string PlateNumber { get; set; }
        public float Confidence { get; set; }
        public byte Status { get; set; }
        public string Timestamp { get; set; }
    }

    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

4.3 解释

  • 服务端:
    • S7.NET:连接 S7-1200,读取 DB1 数据(状态、置信度、车牌号)。
    • WebSocket:通过 wss://localhost:5001/ws/plate 广播 JSON 数据。
    • 安全:使用 SSL 证书(certificate.pfx),验证 Origin。
    • 定时器:每 2 秒读取 PLC 数据并广播。
  • 客户端:
    • WinForm 连接 WebSocket,接收并解析车牌数据。
    • 忽略证书验证(测试用,生产环境需验证)。
  • 测试:
    • 生成证书:dotnet dev-certs https --export-path certificate.pfx --password password.
    • 运行服务端(dotnet run),确保 S7-1200 IP 可达。
    • 运行 WinForm 客户端,连接 wss://localhost:5001/ws/plate,观察车牌数据。
    • 使用 TIA Portal 验证 DB1 数据变化。
    • 使用 Wireshark(tcp.port == 5001)捕获 WebSocket 帧。

5. 总结

  • S7.NET 通信原理:
    • 通过 TCP/IP(端口 102)与 S7-1200/S7-200 SMART 通信。
    • 支持读写 DB(S7-1200)/V 区(S7-200 SMART),处理 Byte、Real、String 等类型。
    • 需配置 PLC(取消优化块访问、允许 PUT/GET)。

  • C# 实现:
    • 单 PLC 通信:WinForm 连接 S7-1200/S7-200 SMART,读写车牌数据。
    • 桥接通信:C# 作为中间件,同步 S7-1200 和 S7-200 SMART 数据。
    • WebSocket 集成:通过 wss:// 安全传输 PLC 数据到前端。
  • 与上下文的联系:
    • TCP:S7.NET 依赖 TCP 端口 102。
    • UDP:S7.NET 不使用 UDP,TCP 提供可靠性。
    • MQTT:PLC 数据可发布到 MQTT Broker。
    • QUIC:QUIC 可支持 WebSocket,间接传输 PLC 数据。
    • WebSocket:结合 TLS 和 JWT 实现安全传输(参考前文)。
    • HTTP:PLC 数据可通过 HTTP API 暴露。
    • ICMP:Ping 测试 PLC 连通性。
    • 串口/USB:S7-200 SMART 支持串口,但 S7.NET 仅支持以太网。
    • 车牌识别:S7.NET 读取车牌数据,WebSocket 传输到前端。
  • 跨平台:.NET 8 支持 Windows、Linux、macOS,需开放 TCP 102 和 443 端口。

测试建议:

  1. 配置 S7-1200(TIA Portal)和 S7-200 SMART(STEP 7-Micro/WIN),确保 IP 可达。
  2. 运行 WinForm 程序,测试单 PLC 通信和桥接。
  3. 运行 WebSocket 服务端和客户端,验证数据传输和安全机制。
  4. 使用 Wireshark(tcp.port == 102 或 tcp.port == 5001)捕获通信数据。
  5. 模拟网络中断,验证错误处理和重连机制。

如需更复杂实现(如多线程优化、MQTT 集成、QUIC 支持)或针对特定 PLC 型号的配置,请告知!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhxup606

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值