# 西门子 S7 PLC 通信指南
> 基于 HslCommunication 库,提取自 YZWater3 项目,可直接复用于其他 .NET 项目。
## 目录
- [概述](#概述)
- [授权配置](#授权配置)
- [依赖安装](#依赖安装)
- [连接建立](#连接建立)
- [数据读取](#数据读取)
- [数据写入](#数据写入)
- [字节序处理](#字节序处理)
- [地址配置管理 PlcConfig](#地址配置管理-plcconfig)
- [S7 地址解析器(批量读取优化)](#s7-地址解析器批量读取优化)
- [数据模型 PlcDataModel](#数据模型-plcdatamodel)
- [轮询服务模板(完整版)](#轮询服务模板完整版)
- [地址格式参考](#地址格式参考)
- [配置文件结构](#配置文件结构)
- [项目文件结构](#项目文件结构)
- [使用示例](#使用示例)
- [支持的 PLC 系列](#支持的-plc-系列)
- [常见问题](#常见问题)
- [许可证](#许可证)
---
## 概述
本项目使用 **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()`)中调用:
```csharp
using HslCommunication;
// 在程序启动时设置授权码(必须在使用任何 PLC 功能之前调用)
HslCommunication.Authorization.SetAuthorizationCode("你的授权码");
```
### 在 Avalonia 中的应用示例
```csharp
// 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 中的应用示例
```csharp
// Program.cs 或 App.xaml.cs
[STAThread]
static void Main()
{
// 在 Application 启动前设置
HslCommunication.Authorization.SetAuthorizationCode("你的授权码");
App domain = new App();
domain.InitializeComponent();
domain.Run();
}
```
### 授权相关 API
```csharp
// 检查授权状态
bool isAuthorized = HslCommunication.Authorization.IsAuthorized();
// 获取授权到期时间
DateTime expireDate = HslCommunication.Authorization.GetExpireDate();
// 获取授权设备数量限制
int deviceCount = HslCommunication.Authorization.GetDeviceCount();
```
### 未授权时的表现
- 连接会随机断开(不定时丢失连接)
- 部分高级功能不可用
- 数据读取可能出现异常
> **注意**:本项目使用的授权码 `c765bac7-2caf-446d-9619-f67e19d6624e` 仅限本项目使用。
> 其他项目请前往 [HslCommunication 官网](https://www.hslcommunication.cn/) 购买授权。
---
## 依赖安装
### NuGet 包
```xml
```
### 安装命令
```bash
# Package Manager
Install-Package HslCommunication
# .NET CLI
dotnet add package HslCommunication
```
---
## 连接建立
### 基础连接类
```csharp
using HslCommunication;
using HslCommunication.Profinet.Siemens;
public class SiemensPlcHelper
{
private SiemensS7Net? _plc;
private bool _isConnected;
public bool IsConnected => _isConnected;
///
/// 初始化并连接 PLC
///
public async Task 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;
}
///
/// 断开连接
///
public void Disconnect()
{
_plc?.ConnectClose();
_isConnected = false;
}
///
/// 释放资源
///
public void Dispose()
{
Disconnect();
_plc?.Dispose();
_plc = null;
}
}
```
### 连接参数说明
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `ip` | PLC 的 IP 地址 | 必填 |
| `port` | 端口号 | `102` |
| `series` | PLC 系列 | `SiemensPLCS.S200Smart` |
| `ConnectTimeOut` | 连接超时(毫秒) | `3000` |
| `ReceiveTimeOut` | 接收超时(毫秒) | `3000` |
---
## 数据读取
### 读取浮点数(REAL)
```csharp
///
/// 读取浮点数 (4字节 REAL)
///
public async Task ReadFloatAsync(string address)
{
if (!_isConnected || _plc == null) return 0f;
var result = await _plc.ReadFloatAsync(address); // 如 "VD100"
return result.IsSuccess ? result.Content : 0f;
}
```
### 读取布尔值(位)
```csharp
///
/// 读取布尔值 (单个位)
///
public async Task ReadBoolAsync(string address)
{
if (!_isConnected || _plc == null) return false;
var result = await _plc.ReadBoolAsync(address); // 如 "M10.0"
return result.IsSuccess ? result.Content : false;
}
```
### 批量读取字节(高性能)
```csharp
///
/// 批量读取字节数组
///
public async Task 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
}
```
### 读取整数类型
```csharp
///
/// 读取整数(INT/WORD/DINT/DWORD)
///
public async Task 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
};
}
```
---
## 数据写入
### 写入浮点数
```csharp
///
/// 写入浮点数
///
public async Task WriteFloatAsync(string address, float value)
{
if (!_isConnected || _plc == null) return false;
var result = await _plc.WriteAsync(address, value); // 如 "VD100", 123.45f
return result.IsSuccess;
}
```
### 写入布尔值
```csharp
///
/// 写入布尔值
///
public async Task WriteBoolAsync(string address, bool value)
{
if (!_isConnected || _plc == null) return false;
var result = await _plc.WriteAsync(address, value); // 如 "M10.0", true
return result.IsSuccess;
}
```
### 写入整数
```csharp
///
/// 写入整数
///
public async Task 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)**,读取字节后需要进行转换。
### 浮点数解析
```csharp
///
/// 从字节数组解析浮点数(S7 大端序 → 本地小端序)
///
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);
}
```
### 位解析
```csharp
///
/// 从字节中提取特定位
///
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 地址集中管理,支持从配置文件覆盖,避免硬编码散落各处。
```csharp
using System.Text.Json;
///
/// PLC 地址配置 - 所有地址映射集中管理,支持配置文件覆盖
///
public static class PlcConfig
{
// 地址覆盖表(从配置文件加载)
private static Dictionary _overrides = new();
///
/// 从配置文件加载地址覆盖
///
public static void LoadFromConfig(string configPath = "plc-addresses.json")
{
if (File.Exists(configPath))
{
var json = File.ReadAllText(configPath);
_overrides = JsonSerializer.Deserialize>(json) ?? new();
}
}
///
/// 获取地址(优先使用配置覆盖值)
///
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)
```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 次)。
```csharp
using System.Text.RegularExpressions;
///
/// 西门子 S7 地址解析工具
/// 支持格式: V/VB/VW/VD/M/MB/MW/MD/I/Q/IB/QB/IW/QW
///
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
);
///
/// 解析 S7 地址
///
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);
}
///
/// 地址组(用于批量读取)
///
public record AddressGroup(
AreaType Area,
int StartByte,
int TotalBytes,
List<(string Key, int Offset)> Items
);
///
/// 将地址列表分组为连续的批量读取组
///
/// 地址列表 (key, address)
/// 单个批次的最大字节数(默认 200)
public static List GroupConsecutiveAddresses(
List<(string Key, string Address)> addresses,
int maxGroupBytes = 200)
{
var groups = new List();
// 按区域分组
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;
}
///
/// 将区域类型转换为可识别的地址前缀
///
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
};
}
}
```
### 批量读取使用示例
```csharp
// 待读取的地址列表
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();
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 通过此模型绑定显示数据。
```csharp
using System.Collections.Concurrent;
using CommunityToolkit.Mvvm.ComponentModel;
///
/// PLC 数据模型 - 内存数据快照
///
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;
// ═══ 动态寄存器(由项目配置驱动) ═══
/// 模拟量动态寄存器(名称 → 值)
public ConcurrentDictionary AnalogRegisters { get; } = new();
/// 数字量动态寄存器(名称 → 值)
public ConcurrentDictionary 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;
}
```
---
## 轮询服务模板(完整版)
包含:**并发控制**、**死区过滤**、**降级模式**、**指数退避重连**、**批量读取优化**。
```csharp
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? DataUpdated;
public event Action? ConnectionStateChanged;
public event Action? ErrorOccurred;
public PlcDataModel Data => _data;
public bool IsConnected => _isConnected;
public PlcPollingService(ILogger? logger = null)
{
_logger = logger;
}
/// 启动轮询
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));
}
/// 停止轮询
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;
}
/// 轮询主循环
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);
}
}
}
/// 尝试连接(带重试)
private async Task 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 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;
}
}
/// 批量读取所有 PLC 数据
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;
}
///
/// 带并发控制的字节读取
///
private async Task 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();
}
}
///
/// 死区过滤:只有变化超过阈值才更新(减少 UI 无效刷新)
///
private void ApplyWithDeadband(float currentValue, float newValue, Action 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位整数 |
### 常用地址示例
```csharp
// 浮点数读取
"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)
```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"
}
}
```
### 配置模型类
```csharp
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? 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(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);
}
}
```
### 配置加载顺序
```csharp
// 在程序启动时按顺序加载:
// 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)
```xml
net8.0
enable
enable
```
---
## 使用示例
### 基础使用
```csharp
// 创建连接
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();
```
### 使用轮询服务
```csharp
// 创建轮询服务
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 接口
```csharp
using HslCommunication;
///
/// PLC 驱动接口 - 统一的 PLC 通信抽象
///
public interface IPlcDriver : IDisposable
{
string Name { get; }
bool IsConnected { get; }
Task ConnectAsync();
void Disconnect();
Task> ReadBytesAsync(string address, ushort length);
Task> ReadFloatAsync(string address);
Task> ReadBoolAsync(string address);
Task WriteAsync(string address, byte[] value);
Task WriteFloatAsync(string address, float value);
Task WriteBoolAsync(string address, bool value);
}
///
/// PLC 协议类型
///
public enum PlcProtocol
{
SiemensS7, // 西门子 S7
MitsubishiMC, // 三菱 MC
OmronFins, // 欧姆龙 Fins
ModbusTcp, // Modbus TCP
AllenBradley // AB EtherNet/IP
}
```
### SiemensS7Driver 实现
```csharp
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 ConnectAsync()
{
var result = await _plc.ConnectServerAsync();
_isConnected = result.IsSuccess;
return result;
}
public void Disconnect()
{
_plc.ConnectClose();
_isConnected = false;
}
public async Task> ReadBytesAsync(string address, ushort length)
=> await _plc.ReadAsync(address, length);
public async Task> ReadFloatAsync(string address)
=> await _plc.ReadFloatAsync(address);
public async Task> ReadBoolAsync(string address)
=> await _plc.ReadBoolAsync(address);
public async Task WriteAsync(string address, byte[] value)
=> await _plc.WriteAsync(address, value);
public async Task WriteFloatAsync(string address, float value)
=> await _plc.WriteAsync(address, value);
public async Task WriteBoolAsync(string address, bool value)
=> await _plc.WriteAsync(address, value);
public void Dispose()
{
if (_disposed) return;
_disposed = true;
Disconnect();
_plc.Dispose();
}
}
```
### PlcDriverFactory 工厂
```csharp
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 系列
```csharp
// 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)
```csharp
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();
}
```
### 日志使用示例
```csharp
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 依赖
```xml
```
### 启动代码
```csharp
// 程序入口(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());
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 库请遵循其官方许可证。