聚焦于使用 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 和物联网应用。
- GitHub 仓库:https://github.com/S7NetPlus/s7netplus[](https://github.com/S7NetPlus/s7netplus)
- 支持的 PLC:S7-200、S7-200 SMART、S7-300、S7-400、S7-1200、S7-1500。
- 协议:基于 TCP/IP 的 Profinet,无需 OPC 服务器或额外驱动。
- 数据类型:支持字节(Byte)、字(Word)、整数(Int)、浮点数(Real)、结构体等。
1.2 通信原理S7.NET 通过以下步骤与 PLC 通信:
- 建立连接:
- 使用 PLC 的 IP 地址、机架号(Rack)和槽号(Slot)初始化连接。
- S7-1200/S7-200 SMART 默认机架/槽号为 0/0。
- 连接基于 TCP 端口 102(ISO-on-TCP)。
- 数据读写:
- 绝对寻址:直接访问 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 读写多个字节。
- 错误处理:
- 检查连接状态和传输错误(如超时、缓冲区无效)。
- 使用 OperationResult 或异常处理错误。
- 优化:
- 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。
- 在 TIA Portal 中配置:
- S7-200 SMART:
- 使用 STEP 7-Micro/WIN SMART 配置:
- 网络:配置以太网模块(如 EM DE01),设置 IP。
- 数据块:S7-200 SMART 使用 V 区(变量存储区)代替 DB。
- 访问权限:确保允许远程访问。
- 默认端口:102。
- 机架/槽号:0/0。
- 使用 STEP 7-Micro/WIN SMART 配置:
- 注意事项(搜索结果):
- 确保 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 项目设置
- 创建 WinForm 项目:dotnet new winforms -n PlcCommunication.
- 安装 S7.NET 库:dotnet add package S7netplus.
- 配置 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)。
- S7-1200:在 TIA Portal 创建 DB1(取消优化块访问),包含:
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 端口。
测试建议:
- 配置 S7-1200(TIA Portal)和 S7-200 SMART(STEP 7-Micro/WIN),确保 IP 可达。
- 运行 WinForm 程序,测试单 PLC 通信和桥接。
- 运行 WebSocket 服务端和客户端,验证数据传输和安全机制。
- 使用 Wireshark(tcp.port == 102 或 tcp.port == 5001)捕获通信数据。
- 模拟网络中断,验证错误处理和重连机制。
如需更复杂实现(如多线程优化、MQTT 集成、QUIC 支持)或针对特定 PLC 型号的配置,请告知!