.NET 8.0 工业物联网实战:基于 S7netplus 封装高可用西门子 PLC 通信类

一、 核心组件与依赖安装

在 .NET 8.0 环境中,我们选择开源社区最稳定、使用最广泛的 S7netplus

通过 NuGet 包管理器或控制台安装:

bash 复制代码
dotnet add package S7netplus

二、 配置文件与选项模式 (Options Pattern)

在现代 .NET 开发中,硬编码是大忌。我们使用 Options 模式来管理 PLC 的连接参数。

1. S7PlcOptions.cs (配置实体类)

csharp 复制代码
using S7.Net;

namespace IIoT.PlcIntegration;

/// <summary>
/// PLC 连接配置选项
/// </summary>
public class S7PlcOptions
{
    /// <summary>
    /// 配置文件中的节点名称
    /// </summary>
    public const string Position = "S7PlcConfig";

    /// <summary>
    /// CPU 类型 (例如: S71200, S71500, S7300)
    /// 默认使用 S7-1500
    /// </summary>
    public CpuType Cpu { get; set; } = CpuType.S71500;

    /// <summary>
    /// PLC 的 IP 地址
    /// </summary>
    public string IpAddress { get; set; } = "127.0.0.1";

    /// <summary>
    /// 机架号 (对于 S7-1200/1500 通常为 0)
    /// </summary>
    public short Rack { get; set; } = 0;

    /// <summary>
    /// 插槽号 (对于 S7-1200/1500 通常为 1,S7-300 通常为 2)
    /// </summary>
    public short Slot { get; set; } = 1;

    /// <summary>
    /// 连接超时时间(毫秒)
    /// </summary>
    public int TimeoutMs { get; set; } = 3000;
}

三、 接口抽象定义

遵循依赖倒置原则(DIP),我们先定义清晰的接口。这不仅方便后续通过依赖注入使用,也极其方便单元测试(Mock)。

2. IS7PlcService.cs (接口定义)

csharp 复制代码
using S7.Net;

namespace IIoT.PlcIntegration;

/// <summary>
/// S7 PLC 通信服务接口
/// </summary>
public interface IS7PlcService
{
    /// <summary>
    /// 获取当前是否处于连接状态
    /// </summary>
    bool IsConnected { get; }

    /// <summary>
    /// 泛型读取方法 (适用于读取单个变量,如 DB1.DBD4)
    /// </summary>
    /// <typeparam name="T">期望转换的数据类型 (如 float, short, bool)</typeparam>
    /// <param name="variable">PLC 变量地址</param>
    /// <param name="cancellationToken">取消令牌</param>
    /// <returns>读取成功返回对应类型的值,失败返回默认值</returns>
    Task<T?> ReadAsync<T>(string variable, CancellationToken cancellationToken = default);

    /// <summary>
    /// 写入单个变量
    /// </summary>
    /// <param name="variable">PLC 变量地址</param>
    /// <param name="value">要写入的值</param>
    /// <param name="cancellationToken">取消令牌</param>
    /// <returns>是否写入成功</returns>
    Task<bool> WriteAsync(string variable, object value, CancellationToken cancellationToken = default);

    /// <summary>
    /// 批量读取连续的字节 (高性能推荐用法)
    /// 工业现场为了节约带宽和降低延迟,通常一次性读取整个 DB 块的字节数组,然后在 C# 中解析
    /// </summary>
    /// <param name="dataType">数据块类型 (如 DataType.DataBlock)</param>
    /// <param name="db">DB 块号</param>
    /// <param name="startByteAdr">起始字节偏移量</param>
    /// <param name="count">读取的字节长度</param>
    /// <param name="cancellationToken">取消令牌</param>
    /// <returns>字节数组,失败返回 null</returns>
    Task<byte[]?> ReadBytesAsync(DataType dataType, int db, int startByteAdr, int count, CancellationToken cancellationToken = default);
}

四、 核心服务实现(灵魂代码)

这是整篇文章的核心所在。需要向读者强调:工控通信的命门在于并发控制和异常处理。这里使用了 C# 12 的主构造函数(Primary Constructors)。

3. S7PlcService.cs (核心实现)

csharp 复制代码
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using S7.Net;

namespace IIoT.PlcIntegration;

/// <summary>
/// 高可用 S7 PLC 服务实现
/// </summary>
/// <remarks>
/// 设计要点:
/// 1. 使用 SemaphoreSlim 保证线程安全,防止多线程同时发起 TCP 请求导致 S7 协议报文错乱。
/// 2. 采用惰性连接 (Lazy Connection) 与断线自愈机制,读写前自动检查并重建连接。
/// 3. 捕获底层异常时主动释放底层 Socket,避免死锁或半连接状态。
/// </remarks>
public class S7PlcService(IOptions<S7PlcOptions> options, ILogger<S7PlcService> logger) : IS7PlcService, IDisposable
{
    private readonly S7PlcOptions _options = options.Value;
    private Plc? _plc;
    
    // 【核心组件】信号量,只允许 1 个线程同时进入底层读写,防止串线
    private readonly SemaphoreSlim _semaphore = new(1, 1);

    // 判断内部 PLC 对象是否实例化且处于连接状态
    public bool IsConnected => _plc is { IsConnected: true };

    /// <summary>
    /// 内部核心方法:确保连接就绪。
    /// 注意:调用此方法前,必须确保已经获得了 _semaphore 锁!
    /// </summary>
    private async Task<bool> EnsureConnectionAsync(CancellationToken cancellationToken)
    {
        // 如果已经连接,直接返回,避免重复开销
        if (IsConnected) return true;

        try
        {
            // 彻底清理旧的连接实例,防止内存泄漏或 Socket 端口占用
            _plc?.Close();
            
            // 重新实例化 PLC 对象
            _plc = new Plc(_options.Cpu, _options.IpAddress, _options.Rack, _options.Slot);
            _plc.ReadTimeout = _options.TimeoutMs;
            _plc.WriteTimeout = _options.TimeoutMs;

            // 异步发起 TCP 连接
            await _plc.OpenAsync(cancellationToken);
            
            logger.LogInformation("成功连接到西门子 PLC [{IpAddress}]. Rack:{Rack}, Slot:{Slot}", 
                _options.IpAddress, _options.Rack, _options.Slot);
                
            return true;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "连接西门子 PLC [{IpAddress}] 失败!请检查网络状态或机架/插槽配置。", _options.IpAddress);
            return false;
        }
    }

    public async Task<T?> ReadAsync<T>(string variable, CancellationToken cancellationToken = default)
    {
        // 等待进入临界区
        await _semaphore.WaitAsync(cancellationToken);
        try
        {
            // 检查并建立连接,如果连接失败直接返回默认值
            if (!await EnsureConnectionAsync(cancellationToken)) return default;

            // 调用底层读取
            var result = await _plc!.ReadAsync(variable, cancellationToken);
            
            if (result == null) return default;

            // 类型转换处理:处理底层返回类型与泛型 T 不完全一致的情况
            return result is T typedResult ? typedResult : (T)Convert.ChangeType(result, typeof(T));
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "读取 PLC 变量 {Variable} 异常。将强制断开连接触发下一次重连。", variable);
            // 【防御性编程】一旦发生异常,立刻关闭当前连接,强迫下次请求重建连接
            _plc?.Close(); 
            return default;
        }
        finally
        {
            // 无论成功失败,必须释放锁
            _semaphore.Release();
        }
    }

    public async Task<bool> WriteAsync(string variable, object value, CancellationToken cancellationToken = default)
    {
        await _semaphore.WaitAsync(cancellationToken);
        try
        {
            if (!await EnsureConnectionAsync(cancellationToken)) return false;

            await _plc!.WriteAsync(variable, value, cancellationToken);
            logger.LogDebug("成功向变量 {Variable} 写入值: {Value}", variable, value);
            return true;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "写入 PLC 变量 {Variable} 异常!", variable);
            _plc?.Close();
            return false;
        }
        finally
        {
            _semaphore.Release();
        }
    }

    public async Task<byte[]?> ReadBytesAsync(DataType dataType, int db, int startByteAdr, int count, CancellationToken cancellationToken = default)
    {
        await _semaphore.WaitAsync(cancellationToken);
        try
        {
            if (!await EnsureConnectionAsync(cancellationToken)) return null;

            return await _plc!.ReadBytesAsync(dataType, db, startByteAdr, count, cancellationToken);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "批量读取 PLC 字节数据失败。DB:{Db}, 起始地址:{Start}, 长度:{Count}", db, startByteAdr, count);
            _plc?.Close();
            return null;
        }
        finally
        {
            _semaphore.Release();
        }
    }

    /// <summary>
    /// 释放非托管资源
    /// </summary>
    public void Dispose()
    {
        _plc?.Close();
        _semaphore.Dispose();
        GC.SuppressFinalize(this);
    }
}

五、 在项目中的注册与实战运用

appsettings.json 中配置 PLC 信息:

json 复制代码
{
  "S7PlcConfig": {
    "Cpu": "S71500",
    "IpAddress": "192.168.0.200",
    "Rack": 0,
    "Slot": 1,
    "TimeoutMs": 3000
  }
}

Program.cs 中注册服务(注册为单例,因为底层做了并发控制和重连,全应用共用一个实例即可,节约连接数):

csharp 复制代码
var builder = WebApplication.CreateBuilder(args);

// 1. 绑定配置到 IOptions
builder.Services.Configure<S7PlcOptions>(builder.Configuration.GetSection(S7PlcOptions.Position));

// 2. 注册为单例服务
builder.Services.AddSingleton<IS7PlcService, S7PlcService>();

// 3. 注册测试用的后台工作服务
builder.Services.AddHostedService<PlcDataCollectorWorker>();

var app = builder.Build();
app.Run();

编写一个后台采集服务 (PlcDataCollectorWorker.cs) 测试效果:

csharp 复制代码
using IIoT.PlcIntegration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

public class PlcDataCollectorWorker(IS7PlcService plcService, ILogger<PlcDataCollectorWorker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation("PLC 数据采集服务已启动...");

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                // 示例 1:读取简单的单变量 (DB1 块,偏移量 0 的双字转换为 Float)
                var temperature = await plcService.ReadAsync<float>("DB1.DBD0", stoppingToken);
                logger.LogInformation("当前设备温度: {Temp} ℃", temperature);

                // 示例 2:心跳写入 (将 M0.0 置为 true 告诉 PLC 上位机在线)
                await plcService.WriteAsync("M0.0", true, stoppingToken);
            }
            catch (Exception ex)
            {
                // 防止主循环崩溃
                logger.LogError(ex, "采集周期发生未处理异常");
            }

            // 间隔 1 秒采集一次
            await Task.Delay(1000, stoppingToken);
        }
    }
}
相关推荐
砍材农夫9 小时前
物联网 基于netty构建mqtt协议规范(发布/订阅模式)
java·开发语言·物联网·netty
銳昊城9 小时前
新大陆物联网竞赛经验谈
物联网·iot·新大陆物联网
振浩微433射频芯片9 小时前
工业环境下的“硬核”选择:如何科学评估国产433芯片的可靠性?
网络·人工智能·科技·单片机·物联网·学习
pingao14137810 小时前
供水排水燃气电力通信智慧井盖传感器_智慧市政管网监测设备
大数据·人工智能·物联网
2501_9139817810 小时前
智慧农业物联网应用方案指南:精准灌溉、土壤监测与数据驱动详解
物联网·智慧农业·农业物联网
靠谱品牌推荐官11 小时前
【高性能工程】每秒万次物联网数据高频握手:如何设计一套抗丢包的工业级小程序后端微服务架构?
物联网·小程序·架构
liguojun202511 小时前
软硬一体智慧场馆系统推荐——助力场馆数字化高效升级
java·大数据·人工智能·物联网·1024程序员节
CC1802539448611 小时前
深度对比:乐鑫 ESP32-C3-MINI-1U vs ESP32-C6-MINI-1U,哪款更适合您的 IoT 项目?
物联网·wifi6·esp32-c6