西门子S7通信指南.md 55 KB

西门子 S7 PLC 通信指南

基于 HslCommunication 库,提取自 YZWater3 项目,可直接复用于其他 .NET 项目。

目录


概述

本项目使用 HslCommunication 工业通信库与西门子 S7 系列 PLC 进行通信。

核心特性

  • 支持 S7-200 / S7-200Smart / S7-300 / S7-400 / S7-1200 / S7-1500
  • 异步读写,支持 Float / Bool / byte[] 等数据类型
  • 批量读取优化(连续地址合并)
  • 自动重连机制
  • 大端序/小端序自动转换

授权配置

HslCommunication 是商业授权库,使用前需要设置授权码。

授权方式

方式 说明
免费试用 不设置授权码,功能受限(随机断开、部分 API 不可用)
正式授权 购买授权码后通过 SetAuthorizationCode() 激活

设置授权码

必须在创建 PLC 通信对象之前调用,建议在程序启动入口(如 Main() 或 App.OnStartup())中调用:

using HslCommunication;

// 在程序启动时设置授权码(必须在使用任何 PLC 功能之前调用)
HslCommunication.Authorization.SetAuthorizationCode("你的授权码");

在 Avalonia 中的应用示例

// App.axaml.cs
public class App : Application
{
    public override void OnFrameworkInitializationCompleted()
    {
        // 激活 HslCommunication 授权(在所有 PLC 操作之前)
        HslCommunication.Authorization.SetAuthorizationCode(
            "c765bac7-2caf-446d-9619-f67e19d6624e");

        // 后续初始化...
        base.OnFrameworkInitializationCompleted();
    }
}

在 WPF / WinForms 中的应用示例

// Program.cs 或 App.xaml.cs
[STAThread]
static void Main()
{
    // 在 Application 启动前设置
    HslCommunication.Authorization.SetAuthorizationCode("你的授权码");

    App domain = new App();
    domain.InitializeComponent();
    domain.Run();
}

授权相关 API

// 检查授权状态
bool isAuthorized = HslCommunication.Authorization.IsAuthorized();

// 获取授权到期时间
DateTime expireDate = HslCommunication.Authorization.GetExpireDate();

// 获取授权设备数量限制
int deviceCount = HslCommunication.Authorization.GetDeviceCount();

未授权时的表现

  • 连接会随机断开(不定时丢失连接)
  • 部分高级功能不可用
  • 数据读取可能出现异常

注意:本项目使用的授权码 c765bac7-2caf-446d-9619-f67e19d6624e 仅限本项目使用。
其他项目请前往 HslCommunication 官网 购买授权。


依赖安装

NuGet 包

<PackageReference Include="HslCommunication" Version="11.8.2" />

安装命令

# Package Manager
Install-Package HslCommunication

# .NET CLI
dotnet add package HslCommunication

连接建立

基础连接类

using HslCommunication;
using HslCommunication.Profinet.Siemens;

public class SiemensPlcHelper
{
    private SiemensS7Net? _plc;
    private bool _isConnected;

    public bool IsConnected => _isConnected;

    /// <summary>
    /// 初始化并连接 PLC
    /// </summary>
    public async Task<bool> ConnectAsync(string ip, int port = 102, 
        SiemensPLCS series = SiemensPLCS.S200Smart)
    {
        _plc = new SiemensS7Net(series, ip)
        {
            Port = port,
            ConnectTimeOut = 3000,   // 连接超时 3秒
            ReceiveTimeOut = 3000    // 接收超时 3秒
        };

        var result = await _plc.ConnectServerAsync();
        _isConnected = result.IsSuccess;
        return _isConnected;
    }

    /// <summary>
    /// 断开连接
    /// </summary>
    public void Disconnect()
    {
        _plc?.ConnectClose();
        _isConnected = false;
    }

    /// <summary>
    /// 释放资源
    /// </summary>
    public void Dispose()
    {
        Disconnect();
        _plc?.Dispose();
        _plc = null;
    }
}

连接参数说明

参数 说明 默认值
ip PLC 的 IP 地址 必填
port 端口号 102
series PLC 系列 SiemensPLCS.S200Smart
ConnectTimeOut 连接超时(毫秒) 3000
ReceiveTimeOut 接收超时(毫秒) 3000

数据读取

读取浮点数(REAL)

/// <summary>
/// 读取浮点数 (4字节 REAL)
/// </summary>
public async Task<float> ReadFloatAsync(string address)
{
    if (!_isConnected || _plc == null) return 0f;
    
    var result = await _plc.ReadFloatAsync(address);  // 如 "VD100"
    return result.IsSuccess ? result.Content : 0f;
}

读取布尔值(位)

/// <summary>
/// 读取布尔值 (单个位)
/// </summary>
public async Task<bool> ReadBoolAsync(string address)
{
    if (!_isConnected || _plc == null) return false;
    
    var result = await _plc.ReadBoolAsync(address);   // 如 "M10.0"
    return result.IsSuccess ? result.Content : false;
}

批量读取字节(高性能)

/// <summary>
/// 批量读取字节数组
/// </summary>
public async Task<byte[]?> ReadBytesAsync(string address, ushort length)
{
    if (!_isConnected || _plc == null) return null;
    
    var result = await _plc.ReadAsync(address, length);  // 如 "V100", 20
    return result.IsSuccess ? result.Content : null;
}

// 使用示例:一次读取 5 个连续的浮点数
var bytes = await ReadBytesAsync("V100", 20);  // 从 V100 开始读 20 字节
if (bytes != null)
{
    var value1 = ParseFloat(bytes, 0);   // VD100
    var value2 = ParseFloat(bytes, 4);   // VD104
    var value3 = ParseFloat(bytes, 8);   // VD108
    var value4 = ParseFloat(bytes, 12);  // VD112
    var value5 = ParseFloat(bytes, 16);  // VD116
}

读取整数类型

/// <summary>
/// 读取整数(INT/WORD/DINT/DWORD)
/// </summary>
public async Task<int> ReadIntAsync(string address, string dataType = "INT")
{
    if (!_isConnected || _plc == null) return 0;
    
    ushort length = dataType switch
    {
        "INT" or "WORD" => 2,
        "DINT" or "DWORD" => 4,
        _ => 2
    };
    
    var result = await _plc.ReadAsync(address, length);
    if (!result.IsSuccess || result.Content == null) return 0;
    
    var bytes = result.Content;
    if (BitConverter.IsLittleEndian)
    {
        Array.Reverse(bytes);
    }
    
    return dataType switch
    {
        "INT" => BitConverter.ToInt16(bytes, 0),
        "WORD" => BitConverter.ToUInt16(bytes, 0),
        "DINT" => BitConverter.ToInt32(bytes, 0),
        "DWORD" => (int)BitConverter.ToUInt32(bytes, 0),
        _ => 0
    };
}

数据写入

写入浮点数

/// <summary>
/// 写入浮点数
/// </summary>
public async Task<bool> WriteFloatAsync(string address, float value)
{
    if (!_isConnected || _plc == null) return false;
    
    var result = await _plc.WriteAsync(address, value);  // 如 "VD100", 123.45f
    return result.IsSuccess;
}

写入布尔值

/// <summary>
/// 写入布尔值
/// </summary>
public async Task<bool> WriteBoolAsync(string address, bool value)
{
    if (!_isConnected || _plc == null) return false;
    
    var result = await _plc.WriteAsync(address, value);  // 如 "M10.0", true
    return result.IsSuccess;
}

写入整数

/// <summary>
/// 写入整数
/// </summary>
public async Task<bool> WriteIntAsync(string address, int value, string dataType = "INT")
{
    if (!_isConnected || _plc == null) return false;
    
    object data = dataType switch
    {
        "INT" => (short)value,
        "WORD" => (ushort)value,
        "DINT" => value,
        "DWORD" => (uint)value,
        _ => (short)value
    };
    
    var result = await _plc.WriteAsync(address, data);
    return result.IsSuccess;
}

字节序处理

重要:西门子 S7 PLC 使用 大端序(Big-Endian),而 Windows 使用 小端序(Little-Endian),读取字节后需要进行转换。

浮点数解析

/// <summary>
/// 从字节数组解析浮点数(S7 大端序 → 本地小端序)
/// </summary>
public static float ParseFloat(byte[] data, int offset)
{
    if (data == null || offset + 4 > data.Length) return 0f;
    
    if (BitConverter.IsLittleEndian)
    {
        // S7 是大端序,需要翻转字节
        var bytes = new byte[] { 
            data[offset + 3], data[offset + 2], 
            data[offset + 1], data[offset] 
        };
        return BitConverter.ToSingle(bytes, 0);
    }
    return BitConverter.ToSingle(data, offset);
}

位解析

/// <summary>
/// 从字节中提取特定位
/// </summary>
public static bool ParseBit(byte[] data, int byteOffset, int bitOffset)
{
    if (data == null || byteOffset >= data.Length) return false;
    return (data[byteOffset] & (1 << bitOffset)) != 0;
}

地址配置管理 PlcConfig

将所有 PLC 地址集中管理,支持从配置文件覆盖,避免硬编码散落各处。

using System.Text.Json;

/// <summary>
/// PLC 地址配置 - 所有地址映射集中管理,支持配置文件覆盖
/// </summary>
public static class PlcConfig
{
    // 地址覆盖表(从配置文件加载)
    private static Dictionary<string, string> _overrides = new();

    /// <summary>
    /// 从配置文件加载地址覆盖
    /// </summary>
    public static void LoadFromConfig(string configPath = "plc-addresses.json")
    {
        if (File.Exists(configPath))
        {
            var json = File.ReadAllText(configPath);
            _overrides = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new();
        }
    }

    /// <summary>
    /// 获取地址(优先使用配置覆盖值)
    /// </summary>
    private static string Addr(string tagName, string defaultAddr)
    {
        return _overrides.TryGetValue(tagName, out var addr) ? addr : defaultAddr;
    }

    // ═══ 读取地址 ═══

    // 水箱液位 (float, VD)
    public static string Tank1Level => Addr(nameof(Tank1Level), "VD100");
    public static string Tank2Level => Addr(nameof(Tank2Level), "VD104");
    public static string Tank3Level => Addr(nameof(Tank3Level), "VD108");
    public static string Tank4Level => Addr(nameof(Tank4Level), "VD112");

    // 流量 (float, VD)
    public static string InflowRate  => Addr(nameof(InflowRate),  "VD200");
    public static string OutflowRate => Addr(nameof(OutflowRate), "VD204");

    // 泵运行状态 (bool, Q)
    public const string Pump1Run = "Q0.0";
    public const string Pump2Run = "Q0.1";
    public const string Pump3Run = "Q0.2";
    public const string Pump4Run = "Q0.3";
    public const string Pump5Run = "Q0.4";

    // 泵故障 (bool, I)
    public const string Pump1Fault = "I0.0";
    public const string Pump2Fault = "I0.1";
    public const string Pump3Fault = "I0.2";
    public const string Pump4Fault = "I0.3";
    public const string Pump5Fault = "I0.4";

    // 泵频率 (float, VD)
    public static string Pump1Freq => Addr(nameof(Pump1Freq), "VD300");
    public static string Pump2Freq => Addr(nameof(Pump2Freq), "VD304");
    public static string Pump3Freq => Addr(nameof(Pump3Freq), "VD308");
    public static string Pump4Freq => Addr(nameof(Pump4Freq), "VD312");
    public static string Pump5Freq => Addr(nameof(Pump5Freq), "VD316");

    // 泵电流 (float, VD)
    public static string Pump1Current => Addr(nameof(Pump1Current), "VD320");
    public static string Pump2Current => Addr(nameof(Pump2Current), "VD324");
    public static string Pump3Current => Addr(nameof(Pump3Current), "VD328");
    public static string Pump4Current => Addr(nameof(Pump4Current), "VD332");
    public static string Pump5Current => Addr(nameof(Pump5Current), "VD336");

    // 风机 (bool)
    public const string Fan1Run  = "Q0.5";
    public const string Fan2Run  = "Q0.6";
    public const string Fan1Fault = "I0.5";
    public const string Fan2Fault = "I0.6";

    // 阀门位置 (int, VD)
    public static string Valve1Position => Addr(nameof(Valve1Position), "VD500");
    public static string Valve2Position => Addr(nameof(Valve2Position), "VD504");
    public static string Valve3Position => Addr(nameof(Valve3Position), "VD508");
    public static string Valve4Position => Addr(nameof(Valve4Position), "VD512");

    // 系统模式 (bool, M)
    public const string SystemMode = "M0.0";

    // ═══ 写入地址 ═══

    // 泵启停控制 (bool, Q)
    public const string Pump1Start = "Q0.0";
    public const string Pump2Start = "Q0.1";
    public const string Pump3Start = "Q0.2";
    public const string Pump4Start = "Q0.3";
    public const string Pump5Start = "Q0.4";

    // 风机启停 (bool, Q)
    public const string Fan1Start = "Q0.5";
    public const string Fan2Start = "Q0.6";

    // 阀门控制 (bool, Q)
    public const string Valve1Control = "Q1.0";
    public const string Valve2Control = "Q1.1";
    public const string Valve3Control = "Q1.2";
    public const string Valve4Control = "Q1.3";

    // 泵频率设定 (float, VD)
    public const string PumpFreqSetpoint = "VD600";

    // ═══ 辅助方法 ═══

    public static string GetPumpRunAddress(int index) => index switch
    {
        0 => Pump1Run, 1 => Pump2Run, 2 => Pump3Run, 3 => Pump4Run, 4 => Pump5Run,
        _ => throw new ArgumentOutOfRangeException(nameof(index))
    };

    public static string GetPumpFaultAddress(int index) => index switch
    {
        0 => Pump1Fault, 1 => Pump2Fault, 2 => Pump3Fault, 3 => Pump4Fault, 4 => Pump5Fault,
        _ => throw new ArgumentOutOfRangeException(nameof(index))
    };

    public static string GetPumpFreqAddress(int index) => index switch
    {
        0 => Pump1Freq, 1 => Pump2Freq, 2 => Pump3Freq, 3 => Pump4Freq, 4 => Pump5Freq,
        _ => throw new ArgumentOutOfRangeException(nameof(index))
    };

    public static string GetPumpCurrentAddress(int index) => index switch
    {
        0 => Pump1Current, 1 => Pump2Current, 2 => Pump3Current, 3 => Pump4Current, 4 => Pump5Current,
        _ => throw new ArgumentOutOfRangeException(nameof(index))
    };

    public static string GetValveControlAddress(int index) => index switch
    {
        0 => Valve1Control, 1 => Valve2Control, 2 => Valve3Control, 3 => Valve4Control,
        _ => throw new ArgumentOutOfRangeException(nameof(index))
    };

    public static string GetValvePositionAddress(int index) => index switch
    {
        0 => Valve1Position, 1 => Valve2Position, 2 => Valve3Position, 3 => Valve4Position,
        _ => throw new ArgumentOutOfRangeException(nameof(index))
    };
}

地址覆盖配置文件(plc-addresses.json)

{
    "Tank1Level": "VD100",
    "Tank2Level": "VD104",
    "InflowRate": "VD200",
    "OutflowRate": "VD204",
    "Pump1Freq": "VD300",
    "Pump2Freq": "VD304",
    "Pump1Current": "VD320",
    "Pump2Current": "VD324",
    "Valve1Position": "VD500",
    "Valve2Position": "VD504"
}

S7 地址解析器(批量读取优化)

将分散的地址自动分组为连续区间,减少 PLC 通信次数(从 N 次降为 G 次)。

using System.Text.RegularExpressions;

/// <summary>
/// 西门子 S7 地址解析工具
/// 支持格式: V/VB/VW/VD/M/MB/MW/MD/I/Q/IB/QB/IW/QW
/// </summary>
public static class S7AddressParser
{
    public enum AreaType
    {
        Input,     // I - 输入
        Output,    // Q - 输出
        Marker,    // M - 标志位
        Variable   // V - 变量存储区
    }

    public record ParsedAddress(
        AreaType Area,
        int ByteOffset,
        int BitIndex,
        int ByteLength,
        string OriginalAddress
    );

    /// <summary>
    /// 解析 S7 地址
    /// </summary>
    public static ParsedAddress? Parse(string address)
    {
        if (string.IsNullOrEmpty(address)) return null;

        address = address.Trim().ToUpper();

        var match = Regex.Match(address,
            @"^(I|Q|M|V|IB|QB|MB|VB|IW|QW|MW|VW|ID|QD|MD|VD)(\d+)(?:\.(\d))?$");
        if (!match.Success) return null;

        var prefix = match.Groups[1].Value;
        var byteOffset = int.Parse(match.Groups[2].Value);
        var bitIndex = match.Groups[3].Success ? int.Parse(match.Groups[3].Value) : -1;

        AreaType area;
        int length;

        switch (prefix)
        {
            case "I":  case "IB": area = AreaType.Input;    length = 1; break;
            case "IW":            area = AreaType.Input;    length = 2; break;
            case "ID":            area = AreaType.Input;    length = 4; break;
            case "Q":  case "QB": area = AreaType.Output;   length = 1; break;
            case "QW":            area = AreaType.Output;   length = 2; break;
            case "QD":            area = AreaType.Output;   length = 4; break;
            case "M":  case "MB": area = AreaType.Marker;   length = 1; break;
            case "MW":            area = AreaType.Marker;   length = 2; break;
            case "MD":            area = AreaType.Marker;   length = 4; break;
            case "V":  case "VB": area = AreaType.Variable; length = 1; break;
            case "VW":            area = AreaType.Variable; length = 2; break;
            case "VD":            area = AreaType.Variable; length = 4; break;
            default: return null;
        }

        return new ParsedAddress(area, byteOffset, bitIndex, length, address);
    }

    /// <summary>
    /// 地址组(用于批量读取)
    /// </summary>
    public record AddressGroup(
        AreaType Area,
        int StartByte,
        int TotalBytes,
        List<(string Key, int Offset)> Items
    );

    /// <summary>
    /// 将地址列表分组为连续的批量读取组
    /// </summary>
    /// <param name="addresses">地址列表 (key, address)</param>
    /// <param name="maxGroupBytes">单个批次的最大字节数(默认 200)</param>
    public static List<AddressGroup> GroupConsecutiveAddresses(
        List<(string Key, string Address)> addresses,
        int maxGroupBytes = 200)
    {
        var groups = new List<AddressGroup>();

        // 按区域分组
        var byArea = addresses
            .Select(a => (a.Key, Parsed: Parse(a.Address)))
            .Where(a => a.Parsed != null)
            .GroupBy(a => a.Parsed!.Area);

        foreach (var areaGroup in byArea)
        {
            var sorted = areaGroup
                .OrderBy(a => a.Parsed!.ByteOffset)
                .ToList();

            var currentGroup = new List<(string Key, int Offset)>();
            int groupStart = 0;
            int groupEnd = 0;

            foreach (var item in sorted)
            {
                var offset = item.Parsed!.ByteOffset;
                var length = item.Parsed.ByteLength;

                if (currentGroup.Count == 0)
                {
                    groupStart = offset;
                    groupEnd = offset + length;
                    currentGroup.Add((item.Key, offset - groupStart));
                }
                else if (offset + length <= groupStart + maxGroupBytes &&
                         offset <= groupEnd + 4) // 允许最多 4 字节间隔
                {
                    groupEnd = Math.Max(groupEnd, offset + length);
                    currentGroup.Add((item.Key, offset - groupStart));
                }
                else
                {
                    // 保存当前组,开始新组
                    if (currentGroup.Count > 0)
                    {
                        groups.Add(new AddressGroup(
                            areaGroup.Key,
                            groupStart,
                            groupEnd - groupStart,
                            currentGroup
                        ));
                    }
                    groupStart = offset;
                    groupEnd = offset + length;
                    currentGroup = new List<(string Key, int Offset)> { (item.Key, 0) };
                }
            }

            if (currentGroup.Count > 0)
            {
                groups.Add(new AddressGroup(
                    areaGroup.Key,
                    groupStart,
                    groupEnd - groupStart,
                    currentGroup
                ));
            }
        }

        return groups;
    }

    /// <summary>
    /// 将区域类型转换为可识别的地址前缀
    /// </summary>
    public static string GetAddressPrefix(AreaType area, int byteLength)
    {
        var prefix = area switch
        {
            AreaType.Input    => "I",
            AreaType.Output   => "Q",
            AreaType.Marker   => "M",
            AreaType.Variable => "V",
            _ => "V"
        };

        return byteLength switch
        {
            1 => prefix,
            2 => $"{prefix}W",
            4 => $"{prefix}D",
            _ => prefix
        };
    }
}

批量读取使用示例

// 待读取的地址列表
var addresses = new List<(string key, string addr)>
{
    ("Tank1Level", "VD100"),
    ("Tank2Level", "VD104"),
    ("Tank3Level", "VD108"),
    ("Tank4Level", "VD112"),
    ("InflowRate", "VD200"),
    ("OutflowRate", "VD204"),
};

// 自动分组
var groups = S7AddressParser.GroupConsecutiveAddresses(addresses);
// 结果:
//   组1: VD100-VD112 (4个地址, 16字节) → 一次读取
//   组2: VD200-VD204 (2个地址, 8字节)  → 一次读取
// 原需 6 次通信 → 优化为 2 次

// 逐组批量读取
var results = new Dictionary<string, float>();
foreach (var group in groups)
{
    var prefix = S7AddressParser.GetAddressPrefix(group.Area, group.TotalBytes switch
    {
        >= 4 => 4, >= 2 => 2, _ => 1
    });
    var batchAddr = $"{prefix}{group.StartByte}";

    var bytes = await plc.ReadBytesAsync(batchAddr, (ushort)group.TotalBytes);
    if (bytes == null) continue;

    foreach (var (key, offset) in group.Items)
    {
        if (offset + 4 <= bytes.Length)
            results[key] = ParseFloat(bytes, offset);
    }
}

数据模型 PlcDataModel

内存数据快照,UI 通过此模型绑定显示数据。

using System.Collections.Concurrent;
using CommunityToolkit.Mvvm.ComponentModel;

/// <summary>
/// PLC 数据模型 - 内存数据快照
/// </summary>
public partial class PlcDataModel : ObservableObject
{
    // ═══ 水箱液位 ═══
    [ObservableProperty] private float _tank1Level;
    [ObservableProperty] private float _tank2Level;
    [ObservableProperty] private float _tank3Level;
    [ObservableProperty] private float _tank4Level;

    // ═══ 流量 ═══
    [ObservableProperty] private float _inflowRate;
    [ObservableProperty] private float _outflowRate;
    [ObservableProperty] private float _flowDelta;

    // ═══ 泵状态 (0-4) ═══
    [ObservableProperty] private bool _pump1Running;
    [ObservableProperty] private bool _pump2Running;
    [ObservableProperty] private bool _pump3Running;
    [ObservableProperty] private bool _pump4Running;
    [ObservableProperty] private bool _pump5Running;

    // 泵故障
    [ObservableProperty] private bool _pump1Fault;
    [ObservableProperty] private bool _pump2Fault;
    [ObservableProperty] private bool _pump3Fault;
    [ObservableProperty] private bool _pump4Fault;
    [ObservableProperty] private bool _pump5Fault;

    // 泵频率
    [ObservableProperty] private float _pump1Freq;
    [ObservableProperty] private float _pump2Freq;
    [ObservableProperty] private float _pump3Freq;
    [ObservableProperty] private float _pump4Freq;
    [ObservableProperty] private float _pump5Freq;

    // 泵电流
    [ObservableProperty] private float _pump1Current;
    [ObservableProperty] private float _pump2Current;
    [ObservableProperty] private float _pump3Current;
    [ObservableProperty] private float _pump4Current;
    [ObservableProperty] private float _pump5Current;

    // ═══ 风机状态 ═══
    [ObservableProperty] private bool _fan1Running;
    [ObservableProperty] private bool _fan2Running;
    [ObservableProperty] private bool _fan1Fault;
    [ObservableProperty] private bool _fan2Fault;

    // ═══ 阀门位置 (0-100, 0=全关, 100=全开) ═══
    [ObservableProperty] private int _valve1Position;
    [ObservableProperty] private int _valve2Position;
    [ObservableProperty] private int _valve3Position;
    [ObservableProperty] private int _valve4Position;

    // ═══ 系统状态 ═══
    [ObservableProperty] private bool _isAutoMode;

    // ═══ 数据质量 ═══
    [ObservableProperty] private DateTime _lastReadTime;
    [ObservableProperty] private bool _isDataValid;
    [ObservableProperty] private bool _isPlcConnected;

    // ═══ 动态寄存器(由项目配置驱动) ═══
    /// <summary>模拟量动态寄存器(名称 → 值)</summary>
    public ConcurrentDictionary<string, float> AnalogRegisters { get; } = new();

    /// <summary>数字量动态寄存器(名称 → 值)</summary>
    public ConcurrentDictionary<string, bool> DigitalRegisters { get; } = new();

    public float GetAnalogRegister(string key, float defaultValue = 0)
        => AnalogRegisters.TryGetValue(key, out var value) ? value : defaultValue;

    public bool GetDigitalRegister(string key, bool defaultValue = false)
        => DigitalRegisters.TryGetValue(key, out var value) ? value : defaultValue;

    // ═══ 计算属性 ═══
    public int RunningPumpCount => (Pump1Running ? 1 : 0) + (Pump2Running ? 1 : 0) +
                                   (Pump3Running ? 1 : 0) + (Pump4Running ? 1 : 0) +
                                   (Pump5Running ? 1 : 0);

    public bool HasAnyFault => Pump1Fault || Pump2Fault || Pump3Fault || Pump4Fault ||
                               Pump5Fault || Fan1Fault || Fan2Fault;
}

轮询服务模板(完整版)

包含:并发控制、死区过滤、降级模式、指数退避重连、批量读取优化。

using System.Collections.Concurrent;
using HslCommunication;
using HslCommunication.Profinet.Siemens;
using Microsoft.Extensions.Logging;

public class PlcPollingService : IDisposable
{
    private SiemensS7Net? _plc;
    private bool _isConnected;
    private CancellationTokenSource? _cts;
    private Task? _pollingTask;
    private readonly PlcDataModel _data = new();

    // PLC 读取并发限制(防止同时发起过多请求导致 PLC 过载)
    private static readonly SemaphoreSlim _plcReadSemaphore = new(4, 4);

    private readonly ILogger? _logger;

    // ═══ 配置 ═══
    public int PollingIntervalMs { get; set; } = 1000;
    public float DeadbandPercent { get; set; } = 0.5f;
    public int MaxReconnectAttempts { get; set; } = 10;
    public int ReconnectBaseDelayMs { get; set; } = 1000;

    // ═══ 事件 ═══
    public event Action<PlcDataModel>? DataUpdated;
    public event Action<bool>? ConnectionStateChanged;
    public event Action<string>? ErrorOccurred;

    public PlcDataModel Data => _data;
    public bool IsConnected => _isConnected;

    public PlcPollingService(ILogger? logger = null)
    {
        _logger = logger;
    }

    /// <summary>启动轮询</summary>
    public void Start(string ip, int port = 102, SiemensPLCS series = SiemensPLCS.S200Smart)
    {
        if (_pollingTask != null && !_pollingTask.IsCompleted) return;

        _cts = new CancellationTokenSource();
        _pollingTask = Task.Run(() => PollingLoopAsync(ip, port, series, _cts.Token));
    }

    /// <summary>停止轮询</summary>
    public void Stop()
    {
        _cts?.Cancel();
        _pollingTask?.Wait(TimeSpan.FromSeconds(5));
        _cts?.Dispose();
        _cts = null;
        _pollingTask = null;
        Disconnect();
    }

    private void Disconnect()
    {
        _plc?.ConnectClose();
        _plc?.Dispose();
        _plc = null;
        _isConnected = false;
    }

    /// <summary>轮询主循环</summary>
    private async Task PollingLoopAsync(string ip, int port, SiemensPLCS series, CancellationToken ct)
    {
        var reconnectAttempts = 0;
        var isInDegradedMode = false;
        const int degradedModeRetryIntervalMs = 30000;

        while (!ct.IsCancellationRequested)
        {
            try
            {
                // 1. 确保连接
                if (!_isConnected)
                {
                    var connected = await TryConnectWithRetryAsync(ip, port, series, reconnectAttempts, ct);
                    
                    if (!connected)
                    {
                        reconnectAttempts++;

                        // 进入或保持在降级模式
                        if (reconnectAttempts >= MaxReconnectAttempts)
                        {
                            if (!isInDegradedMode)
                            {
                                isInDegradedMode = true;
                                ErrorOccurred?.Invoke(
                                    $"PLC 连接失败,已重试 {MaxReconnectAttempts} 次,进入降级模式(每 30 秒重试)");
                            }
                            await Task.Delay(degradedModeRetryIntervalMs, ct);
                            continue;
                        }

                        // 正常模式:指数退避
                        var delayMs = ReconnectBaseDelayMs * Math.Min(reconnectAttempts, 5);
                        await Task.Delay(delayMs, ct);
                        continue;
                    }

                    // 连接成功
                    if (isInDegradedMode)
                    {
                        isInDegradedMode = false;
                        ErrorOccurred?.Invoke("PLC 连接已恢复");
                    }
                    reconnectAttempts = 0;
                }

                // 2. 读取数据
                await ReadAllDataAsync(ct);

                // 3. 检测连接状态变化
                ConnectionStateChanged?.Invoke(_isConnected);

                // 4. 数据更新事件
                DataUpdated?.Invoke(_data);

                // 5. 等待下一周期
                var nextDelay = isInDegradedMode ? degradedModeRetryIntervalMs : PollingIntervalMs;
                await Task.Delay(nextDelay, ct);
            }
            catch (OperationCanceledException) { break; }
            catch (Exception ex)
            {
                _logger?.LogError(ex, "轮询异常");
                ErrorOccurred?.Invoke($"轮询异常: {ex.Message}");
                if (!_isConnected) _data.IsPlcConnected = false;
                await Task.Delay(PollingIntervalMs * 2, ct);
            }
        }
    }

    /// <summary>尝试连接(带重试)</summary>
    private async Task<bool> TryConnectWithRetryAsync(
        string ip, int port, SiemensPLCS series, int attempt, CancellationToken ct)
    {
        for (int i = 0; i <= attempt && i < 3; i++)
        {
            if (ct.IsCancellationRequested) return false;

            var connected = await ConnectAsync(ip, port, series);
            if (connected)
            {
                _data.IsPlcConnected = true;
                ConnectionStateChanged?.Invoke(true);
                return true;
            }

            if (i < attempt)
                await Task.Delay(ReconnectBaseDelayMs * (i + 1), ct);
        }
        return false;
    }

    private async Task<bool> ConnectAsync(string ip, int port, SiemensPLCS series)
    {
        try
        {
            _plc = new SiemensS7Net(series, ip)
            {
                Port = port,
                ConnectTimeOut = 3000,
                ReceiveTimeOut = 3000
            };

            var result = await _plc.ConnectServerAsync();
            _isConnected = result.IsSuccess;
            
            if (_isConnected)
                _logger?.LogInformation("PLC 连接成功: {Ip}:{Port}", ip, port);
            else
                _logger?.LogWarning("PLC 连接失败: {Message}", result.Message);

            return _isConnected;
        }
        catch (Exception ex)
        {
            _logger?.LogError(ex, "PLC 连接异常");
            return false;
        }
    }

    /// <summary>批量读取所有 PLC 数据</summary>
    private async Task ReadAllDataAsync(CancellationToken ct)
    {
        if (ct.IsCancellationRequested) return;

        // 并行发起所有批量读取
        var tankTask  = ReadBytesAsync(PlcConfig.Tank1Level, 16);  // 4 floats: 水箱液位
        var flowTask  = ReadBytesAsync(PlcConfig.InflowRate, 8);   // 2 floats: 流量
        var freqTask  = ReadBytesAsync(PlcConfig.Pump1Freq, 20);   // 5 floats: 泵频率
        var qTask     = ReadBytesAsync("Q0", 1);                   // 1 byte: 泵/风机运行状态
        var iTask     = ReadBytesAsync("I0", 1);                   // 1 byte: 泵/风机故障状态
        var mTask     = ReadBytesAsync("M0", 1);                   // 1 byte: 系统模式

        await Task.WhenAll(tankTask, flowTask, freqTask, qTask, iTask, mTask);

        if (ct.IsCancellationRequested) return;

        var tankData = tankTask.Result;
        var flowData = flowTask.Result;
        var freqData = freqTask.Result;
        var qData    = qTask.Result;
        var iData    = iTask.Result;
        var mData    = mTask.Result;

        // 任一批量读取失败则跳过本周期
        if (tankData == null || flowData == null || freqData == null ||
            qData == null || iData == null || mData == null)
        {
            _data.IsDataValid = false;
            _data.IsPlcConnected = _isConnected;
            return;
        }

        // 解析水箱液位 (VD100-VD112, 4 floats)
        var tank1 = ParseFloat(tankData, 0);
        var tank2 = ParseFloat(tankData, 4);
        var tank3 = ParseFloat(tankData, 8);
        var tank4 = ParseFloat(tankData, 12);

        // 解析流量 (VD200-VD204, 2 floats)
        var inflow  = ParseFloat(flowData, 0);
        var outflow = ParseFloat(flowData, 4);

        // 解析泵频率 (VD300-VD316, 5 floats)
        var f1 = ParseFloat(freqData, 0);
        var f2 = ParseFloat(freqData, 4);
        var f3 = ParseFloat(freqData, 8);
        var f4 = ParseFloat(freqData, 12);
        var f5 = ParseFloat(freqData, 16);

        // 解析 Q0 字节: bit 0-4 = 泵运行, bit 5-6 = 风机运行
        var p1   = ParseBit(qData, 0, 0);
        var p2   = ParseBit(qData, 0, 1);
        var p3   = ParseBit(qData, 0, 2);
        var p4   = ParseBit(qData, 0, 3);
        var p5   = ParseBit(qData, 0, 4);
        var fan1 = ParseBit(qData, 0, 5);
        var fan2 = ParseBit(qData, 0, 6);

        // 解析 I0 字节: bit 0-4 = 泵故障, bit 5-6 = 风机故障
        var pf1   = ParseBit(iData, 0, 0);
        var pf2   = ParseBit(iData, 0, 1);
        var pf3   = ParseBit(iData, 0, 2);
        var pf4   = ParseBit(iData, 0, 3);
        var pf5   = ParseBit(iData, 0, 4);
        var fanf1 = ParseBit(iData, 0, 5);
        var fanf2 = ParseBit(iData, 0, 6);

        // 解析 M0 字节: bit 0 = 系统模式
        var autoMode = ParseBit(mData, 0, 0);

        // 应用死区过滤后更新数据
        ApplyWithDeadband(_data.Tank1Level, tank1, v => _data.Tank1Level = v);
        ApplyWithDeadband(_data.Tank2Level, tank2, v => _data.Tank2Level = v);
        ApplyWithDeadband(_data.Tank3Level, tank3, v => _data.Tank3Level = v);
        ApplyWithDeadband(_data.Tank4Level, tank4, v => _data.Tank4Level = v);
        ApplyWithDeadband(_data.InflowRate, inflow,   v => _data.InflowRate = v);
        ApplyWithDeadband(_data.OutflowRate, outflow, v => _data.OutflowRate = v);

        _data.Pump1Running = p1;  _data.Pump2Running = p2;  _data.Pump3Running = p3;
        _data.Pump4Running = p4;  _data.Pump5Running = p5;

        _data.Pump1Fault = pf1;  _data.Pump2Fault = pf2;  _data.Pump3Fault = pf3;
        _data.Pump4Fault = pf4;  _data.Pump5Fault = pf5;

        _data.Pump1Freq = f1;  _data.Pump2Freq = f2;  _data.Pump3Freq = f3;
        _data.Pump4Freq = f4;  _data.Pump5Freq = f5;

        _data.Fan1Running = fan1;  _data.Fan2Running = fan2;
        _data.Fan1Fault = fanf1;   _data.Fan2Fault = fanf2;

        _data.IsAutoMode = autoMode;
        _data.LastReadTime = DateTime.Now;
        _data.IsDataValid = true;
        _data.IsPlcConnected = _isConnected;
        _data.FlowDelta = _data.InflowRate - _data.OutflowRate;
    }

    /// <summary>
    /// 带并发控制的字节读取
    /// </summary>
    private async Task<byte[]?> ReadBytesAsync(string address, ushort length)
    {
        if (!_isConnected || _plc == null) return null;

        await _plcReadSemaphore.WaitAsync();
        try
        {
            var result = await _plc.ReadAsync(address, length);
            if (!result.IsSuccess)
            {
                _isConnected = false;
                return null;
            }
            return result.Content;
        }
        catch
        {
            _isConnected = false;
            return null;
        }
        finally
        {
            _plcReadSemaphore.Release();
        }
    }

    /// <summary>
    /// 死区过滤:只有变化超过阈值才更新(减少 UI 无效刷新)
    /// </summary>
    private void ApplyWithDeadband(float currentValue, float newValue, Action<float> setter)
    {
        if (Math.Abs(currentValue) < 0.001f && Math.Abs(newValue) < 0.001f)
            return; // 都是零值,跳过

        var threshold = Math.Abs(currentValue) * DeadbandPercent / 100.0f;
        if (Math.Abs(newValue - currentValue) > threshold || currentValue == 0)
        {
            setter(newValue);
        }
    }

    private static float ParseFloat(byte[] data, int offset)
    {
        if (data == null || offset + 4 > data.Length) return 0f;
        if (BitConverter.IsLittleEndian)
        {
            var bytes = new byte[] { 
                data[offset + 3], data[offset + 2], 
                data[offset + 1], data[offset] 
            };
            return BitConverter.ToSingle(bytes, 0);
        }
        return BitConverter.ToSingle(data, offset);
    }

    private static bool ParseBit(byte[] data, int byteOffset, int bitOffset)
    {
        if (data == null || byteOffset >= data.Length) return false;
        return (data[byteOffset] & (1 << bitOffset)) != 0;
    }

    public void Dispose() => Stop();
}

关键机制说明

机制 实现方式 说明
并发控制 SemaphoreSlim(4, 4) 限制同时发起的 PLC 读取请求数,防止 PLC 过载
死区过滤 ApplyWithDeadband() 数据变化小于阈值(默认 0.5%)时不更新,减少 UI 无效刷新
降级模式 isInDegradedMode 连接失败超过 MaxReconnectAttempts 次后进入,重试间隔从 1s 延长到 30s
指数退避 ReconnectBaseDelayMs * attempts 重连间隔逐步增加,上限 30s,避免频繁冲击 PLC
并行批量读取 Task.WhenAll() 多个地址段并行发起读取,充分利用网络带宽

地址格式参考

存储区域

区域 说明 地址前缀
V 变量存储区(Variable) V / VB / VW / VD
M 标志位(Marker) M / MB / MW / MD
I 输入映像区(Input) I / IB / IW / ID
Q 输出映像区(Output) Q / QB / QW / QD

数据类型与地址格式

数据类型 字节长度 地址格式示例 说明
Bool(位) 1 bit M10.0, Q0.5 字节偏移.位索引
Byte 1 VB100, MB10 单字节
Word/INT 2 VW100, MW10 16位整数
DWord/REAL 4 VD100, MD10 32位浮点数
DWord/DINT 4 VD100, MD10 32位整数

常用地址示例

// 浮点数读取
"VD100"   // V区第100字节开始的双字(4字节浮点数)
"MD50"    // M区第50字节开始的双字

// 整数读取
"VW100"   // V区第100字节开始的字(2字节)
"MW10"    // M区第10字节开始的字

// 布尔值读取
"M10.0"   // M区第10字节的第0位
"Q0.5"    // Q区第0字节的第5位
"V100.3"  // V区第100字节的第3位

// 字节读取
"VB100"   // V区第100字节
"MB10"    // M区第10字节

配置文件结构

项目使用 JSON 配置文件管理 PLC 连接参数,支持运行时修改。

配置文件示例(yzwater-config.json)

{
    "PlcIp": "192.168.2.100",
    "PlcPort": 102,
    "AutoConnect": true,
    "PlcProtocol": "SiemensS7",

    "LevelHighAlarm": 80.0,
    "LevelLowAlarm": 20.0,
    "FlowHighAlarm": 100.0,
    "FlowLowAlarm": 5.0,

    "PlcAddresses": {
        "Tank1Level": "VD100",
        "Tank2Level": "VD104",
        "InflowRate": "VD200",
        "OutflowRate": "VD204",
        "Pump1Freq": "VD300",
        "Pump1Current": "VD320"
    }
}

配置模型类

using System.Text.Json;
using System.Text.Json.Serialization;

public class SystemConfig
{
    // PLC 连接
    public string PlcIp { get; set; } = "192.168.2.100";
    public int PlcPort { get; set; } = 102;
    public bool AutoConnect { get; set; } = true;
    public string PlcProtocol { get; set; } = "SiemensS7";

    // 报警阈值
    public float LevelHighAlarm { get; set; } = 80f;
    public float LevelLowAlarm { get; set; } = 20f;
    public float FlowHighAlarm { get; set; } = 100f;
    public float FlowLowAlarm { get; set; } = 5f;

    // 地址覆盖
    public Dictionary<string, string>? PlcAddresses { get; set; }
}

public static class ConfigService
{
    private static SystemConfig? _config;
    private static readonly string ConfigPath = "yzwater-config.json";

    public static SystemConfig GetConfig()
    {
        if (_config == null) Load();
        return _config ?? new SystemConfig();
    }

    public static void Load()
    {
        if (File.Exists(ConfigPath))
        {
            var json = File.ReadAllText(ConfigPath);
            _config = JsonSerializer.Deserialize<SystemConfig>(json);
        }
        _config ??= new SystemConfig();
    }

    public static void Save()
    {
        if (_config == null) return;
        var options = new JsonSerializerOptions { WriteIndented = true };
        var json = JsonSerializer.Serialize(_config, options);
        File.WriteAllText(ConfigPath, json);
    }
}

配置加载顺序

// 在程序启动时按顺序加载:

// 1. 加载主配置文件
ConfigService.Load();

// 2. 加载地址覆盖(覆盖默认硬编码地址)
PlcConfig.LoadFromConfig();

// 3. 初始化 PLC 服务
PlcService.Initialize();

项目文件结构

迁移时建议按以下结构组织文件:

YourProject/
├── Services/
│   ├── Plc/
│   │   ├── SiemensPlcHelper.cs          # 基础连接类
│   │   ├── PlcPollingService.cs         # 轮询服务(完整版)
│   │   ├── PlcConfig.cs                 # 地址配置管理
│   │   └── S7AddressParser.cs           # 地址解析器
│   │
│   ├── ConfigService.cs                 # 配置服务
│   └── NotificationService.cs           # 通知服务(可选)
│
├── Models/
│   ├── SystemConfig.cs                  # 配置模型
│   └── PlcDataModel.cs                  # PLC 数据模型
│
├── ViewModels/                          # MVVM 项目
│   └── MainViewModel.cs
│
├── Views/                               # MVVM 项目
│   └── MainWindow.axaml
│
├── yzwater-config.json                  # 运行时配置
├── plc-addresses.json                   # 地址覆盖(可选)
└── YourProject.csproj

NuGet 依赖(csproj)

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <!-- PLC 通信 -->
    <PackageReference Include="HslCommunication" Version="11.8.2" />
    
    <!-- MVVM(可选,WPF/Avalonia 项目) -->
    <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
    
    <!-- 日志 -->
    <PackageReference Include="Serilog" Version="4.0.1" />
    <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
    <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
    
    <!-- JSON 配置 -->
    <PackageReference Include="System.Text.Json" Version="8.0.4" />
  </ItemGroup>
</Project>

使用示例

基础使用

// 创建连接
var plc = new SiemensPlcHelper();
var connected = await plc.ConnectAsync("192.168.0.150", 102);

if (connected)
{
    // 读取浮点数
    var level = await plc.ReadFloatAsync("VD100");
    Console.WriteLine($"液位: {level:F2}");

    // 读取布尔值
    var pumpRun = await plc.ReadBoolAsync("M10.0");
    Console.WriteLine($"泵运行: {pumpRun}");

    // 写入浮点数
    await plc.WriteFloatAsync("VD200", 123.45f);

    // 写入布尔值
    await plc.WriteBoolAsync("M20.0", true);

    // 批量读取
    var bytes = await plc.ReadBytesAsync("V100", 20);
    if (bytes != null)
    {
        for (int i = 0; i < 5; i++)
        {
            var value = ParseFloat(bytes, i * 4);
            Console.WriteLine($"VD{100 + i * 4}: {value:F2}");
        }
    }
}

// 断开连接
plc.Disconnect();

使用轮询服务

// 创建轮询服务
var polling = new PlcPollingService();

// 订阅事件
polling.DataUpdated += data => 
{
    Console.WriteLine($"液位: {data.GetValueOrDefault("TankLevel"):F2}");
    Console.WriteLine($"流量: {data.GetValueOrDefault("FlowRate"):F2}");
};

polling.ConnectionStateChanged += connected =>
{
    Console.WriteLine(connected ? "PLC 已连接" : "PLC 已断开");
};

polling.ErrorOccurred += error =>
{
    Console.WriteLine($"错误: {error}");
};

// 启动(IP: 192.168.0.150, 端口: 102, S7-200Smart)
polling.Start("192.168.0.150", 102, SiemensPLCS.S200Smart);

// 程序退出时停止
Console.WriteLine("按任意键停止...");
Console.ReadKey();
polling.Stop();

多协议驱动抽象(可选扩展)

如果项目未来可能需要切换 PLC 品牌(三菱/欧姆龙/Modbus 等),可以使用统一的驱动接口抽象。

IPlcDriver 接口

using HslCommunication;

/// <summary>
/// PLC 驱动接口 - 统一的 PLC 通信抽象
/// </summary>
public interface IPlcDriver : IDisposable
{
    string Name { get; }
    bool IsConnected { get; }

    Task<OperateResult> ConnectAsync();
    void Disconnect();

    Task<OperateResult<byte[]>> ReadBytesAsync(string address, ushort length);
    Task<OperateResult<float>> ReadFloatAsync(string address);
    Task<OperateResult<bool>> ReadBoolAsync(string address);

    Task<OperateResult> WriteAsync(string address, byte[] value);
    Task<OperateResult> WriteFloatAsync(string address, float value);
    Task<OperateResult> WriteBoolAsync(string address, bool value);
}

/// <summary>
/// PLC 协议类型
/// </summary>
public enum PlcProtocol
{
    SiemensS7,       // 西门子 S7
    MitsubishiMC,    // 三菱 MC
    OmronFins,       // 欧姆龙 Fins
    ModbusTcp,       // Modbus TCP
    AllenBradley     // AB EtherNet/IP
}

SiemensS7Driver 实现

using HslCommunication;
using HslCommunication.Profinet.Siemens;

public class SiemensS7Driver : IPlcDriver
{
    private readonly SiemensS7Net _plc;
    private bool _disposed;
    private bool _isConnected;

    public string Name => "Siemens S7";
    public bool IsConnected => _isConnected && !_disposed;

    public SiemensS7Driver(SiemensPLCS plcSeries = SiemensPLCS.S200Smart)
    {
        _plc = new SiemensS7Net(plcSeries);
    }

    public void SetConnection(string ipAddress, int port = 102)
    {
        _plc.IpAddress = ipAddress;
        _plc.Port = port;
    }

    public async Task<OperateResult> ConnectAsync()
    {
        var result = await _plc.ConnectServerAsync();
        _isConnected = result.IsSuccess;
        return result;
    }

    public void Disconnect()
    {
        _plc.ConnectClose();
        _isConnected = false;
    }

    public async Task<OperateResult<byte[]>> ReadBytesAsync(string address, ushort length)
        => await _plc.ReadAsync(address, length);

    public async Task<OperateResult<float>> ReadFloatAsync(string address)
        => await _plc.ReadFloatAsync(address);

    public async Task<OperateResult<bool>> ReadBoolAsync(string address)
        => await _plc.ReadBoolAsync(address);

    public async Task<OperateResult> WriteAsync(string address, byte[] value)
        => await _plc.WriteAsync(address, value);

    public async Task<OperateResult> WriteFloatAsync(string address, float value)
        => await _plc.WriteAsync(address, value);

    public async Task<OperateResult> WriteBoolAsync(string address, bool value)
        => await _plc.WriteAsync(address, value);

    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;
        Disconnect();
        _plc.Dispose();
    }
}

PlcDriverFactory 工厂

public static class PlcDriverFactory
{
    public static IPlcDriver Create(PlcProtocol protocol, string ipAddress, int port)
    {
        IPlcDriver driver = protocol switch
        {
            PlcProtocol.SiemensS7 => CreateSiemensS7(ipAddress, port),
            // 可扩展其他品牌...
            _ => throw new NotSupportedException($"不支持的 PLC 协议: {protocol}")
        };
        return driver;
    }

    private static SiemensS7Driver CreateSiemensS7(string ipAddress, int port)
    {
        var driver = new SiemensS7Driver(SiemensPLCS.S200Smart);
        driver.SetConnection(ipAddress, port);
        return driver;
    }

    public static int GetDefaultPort(PlcProtocol protocol) => protocol switch
    {
        PlcProtocol.SiemensS7     => 102,
        PlcProtocol.MitsubishiMC  => 6000,
        PlcProtocol.OmronFins     => 9600,
        PlcProtocol.ModbusTcp     => 502,
        PlcProtocol.AllenBradley  => 44818,
        _ => throw new NotSupportedException($"不支持的 PLC 协议: {protocol}")
    };
}

支持的 PLC 系列

枚举值 PLC 型号 说明
SiemensPLCS.S200 S7-200 老型号
SiemensPLCS.S200Smart S7-200 Smart 默认,常用
SiemensPLCS.S300 S7-300 中大型
SiemensPLCS.S400 S7-400 大型
SiemensPLCS.S1200 S7-1200 新型紧凑型
SiemensPLCS.S1500 S7-1500 新型高性能

指定 PLC 系列

// S7-1200
await plc.ConnectAsync("192.168.0.1", 102, SiemensPLCS.S1200);

// S7-1500
await plc.ConnectAsync("192.168.0.1", 102, SiemensPLCS.S1500);

// S7-300
await plc.ConnectAsync("192.168.0.1", 102, SiemensPLCS.S300);

常见问题

连接失败

  1. 检查 IP 地址和端口是否正确
  2. 确认 PLC 与 PC 网络连通(ping 测试)
  3. 检查 PLC 是否已启用 PUT/GET 通信
  4. 确认防火墙未阻止 102 端口

读取数据为 0

  1. 确认地址格式正确(如 VD100 而非 V100)
  2. 检查数据类型是否匹配(浮点数用 VD,整数用 VW)
  3. 检查字节序是否正确(使用 ParseFloat 方法)

读取数据异常

  1. 确认 PLC 系列选择正确
  2. 检查地址范围是否超出 PLC 实际配置
  3. 使用 Wireshark 抓包分析通信数据

批量读取失败

  1. 确认地址在同一区域内连续(不能跨区)
  2. 读取长度不要超过 PLC 限制(S7-200 最大 240 字节)
  3. 使用 S7AddressParser.GroupConsecutiveAddresses() 自动分组

轮询服务 CPU 占用高

  1. 适当增大 PollingIntervalMs(默认 1000ms)
  2. 增大 DeadbandPercent 减少 UI 刷新次数
  3. 使用批量读取替代多次单独读取

日志集成(Serilog)

using Serilog;

// 初始化日志(程序启动时调用一次)
public static void InitializeLogging()
{
    Log.Logger = new LoggerConfiguration()
        .MinimumLevel.Information()
        .WriteTo.Console()
        .WriteTo.File(
            path: "Logs/log-.txt",
            rollingInterval: RollingInterval.Day,
            outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
        .CreateLogger();
}

// 在程序退出时关闭
public static void CloseLogging()
{
    Log.CloseAndFlush();
}

日志使用示例

Log.Information("PLC 连接成功: {Ip}:{Port}", ip, port);
Log.Warning("PLC 连接失败: {Message}", result.Message);
Log.Error(ex, "读取 PLC 数据失败,地址: {Address}", address);
Log.Debug("批量读取优化: {Count} 个地址合并为 {Groups} 次读取", count, groups);

快速迁移清单

将以下文件复制到新项目即可完成迁移:

必需文件(6 个)

文件 功能 位置
SiemensPlcHelper.cs 基础连接类 连接建立章节
PlcConfig.cs 地址配置管理 地址配置管理章节
S7AddressParser.cs 地址解析 + 批量优化 S7 地址解析器章节
PlcDataModel.cs 数据模型 数据模型章节
PlcPollingService.cs 轮询服务(完整版) 轮询服务模板章节
ConfigService.cs + SystemConfig.cs 配置服务 配置文件结构章节

可选文件

文件 功能
IPlcDriver.cs 多协议驱动接口
SiemensS7Driver.cs S7 驱动封装
PlcDriverFactory.cs 驱动工厂
plc-addresses.json 地址覆盖配置

NuGet 依赖

<PackageReference Include="HslCommunication" Version="11.8.2" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="Serilog" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />

启动代码

// 程序入口(Program.cs 或 App.OnStartup)
public static async Task Main(string[] args)
{
    // 1. 初始化日志
    InitializeLogging();

    // 2. 设置 HslCommunication 授权
    HslCommunication.Authorization.SetAuthorizationCode("你的授权码");

    // 3. 加载配置
    ConfigService.Load();
    PlcConfig.LoadFromConfig();

    // 4. 启动轮询
    var polling = new PlcPollingService(Log.ForContext<PlcPollingService>());
    polling.DataUpdated += data => { /* 处理数据更新 */ };
    polling.ConnectionStateChanged += connected => { /* 处理连接状态 */ };
    polling.ErrorOccurred += error => { /* 处理错误 */ };

    var config = ConfigService.GetConfig();
    polling.Start(config.PlcIp, config.PlcPort);

    // 5. 程序退出时
    // polling.Stop();
    // ConfigService.Save();
    // Log.CloseAndFlush();
}

许可证

本文档基于 YZWater3 项目提取,仅供参考使用。HslCommunication 库请遵循其官方许可证。