S7Sharp 1.0.3
dotnet add package S7Sharp --version 1.0.3
NuGet\Install-Package S7Sharp -Version 1.0.3
<PackageReference Include="S7Sharp" Version="1.0.3" />
<PackageVersion Include="S7Sharp" Version="1.0.3" />
<PackageReference Include="S7Sharp" />
paket add S7Sharp --version 1.0.3
#r "nuget: S7Sharp, 1.0.3"
#:package S7Sharp@1.0.3
#addin nuget:?package=S7Sharp&version=1.0.3
#tool nuget:?package=S7Sharp&version=1.0.3
S7Sharp
基于 HslCommunication 的 Siemens S7 PLC 强类型数据映射库。通过表达式树(Expression Tree)在运行时编译高性能的字节解析器和序列化器,将 PLC DB 块数据直接映射到 C# class,避免逐字段手工 ReadBool / ReadFloat / ReadString 的重复编码。
动机
HslCommunication 提供了内置的 SiemensS7Net.Read<T> 泛型方法用于自动映射,但在实际使用中存在两个核心问题:
- Bug —
Read<T>在处理某些数据类型(如 Siemens STRING)时存在行为偏差,导致解析结果不可靠 - 效率 —
Read<T>每次调用都走反射路径,在高频读写场景下开销显著
S7Sharp 用运行时表达式树编译替代反射:首次访问时生成等同于手写代码的 IL,后续调用零反射,且完全掌控解析逻辑。
安装
dotnet add package S7Sharp
HslCommunication 作为依赖会自动安装。
快速开始
1. 定义数据模型
用 [HslDeviceAddress] 标注每个属性在 PLC DB 块中的地址:
using HslCommunication.Reflection;
using S7Sharp;
[WarmUp]
public class DeviceInfo
{
// 位访问 — 格式 DB{num}.{byteOffset}.{bitPos}
[HslDeviceAddress("DB90.0.0")]
public bool EmergencyStop { get; set; }
[HslDeviceAddress("DB90.0.1")]
public bool Running { get; set; }
// 数值类型 — Big-endian,自动匹配字节宽度
[HslDeviceAddress("DB90.2.0")]
public float Frequency { get; set; }
[HslDeviceAddress("DB90.6")]
public short SpeedSetpoint { get; set; }
[HslDeviceAddress("DB90.8")]
public int TotalRuntime { get; set; }
[HslDeviceAddress("DB90.12")]
public double Energy { get; set; }
// byte[] — attr.Length 指定长度,默认 256
[HslDeviceAddress("DB90.20", 32)]
public byte[] RawData { get; set; }
// string — Siemens STRING (ISO-8859-1),attr.Length 指定总字节数(含2字节头部),默认 256
[HslDeviceAddress("DB90.52", 30)]
public string Name { get; set; } = string.Empty;
}
地址格式:DB{num}.{byteOffset} 或 DB{num}.{byteOffset}.{bitPos}
2. 定义嵌套类型
复杂类型(class/struct)属性可以自动参与映射,无需在父类型上重复标注地址:
// 嵌套类型:内部属性标注 [HslDeviceAddress] 即可
[WarmUp]
public class AccountConfig
{
[HslDeviceAddress("DB90.10", 30)]
public string Address { get; set; } = string.Empty;
[HslDeviceAddress("DB90.40", 30)]
public string Key { get; set; } = string.Empty;
}
[WarmUp]
public class DeviceInfo
{
[HslDeviceAddress("DB90.0.0")]
public bool Running { get; set; }
// 无需 [HslDeviceAddress] — 框架自动从 AccountConfig 内部属性推导地址和字节宽度
public AccountConfig Account { get; set; }
[HslDeviceAddress("DB90.70")]
public short SpeedSetpoint { get; set; }
}
S7Sharp 会自动发现 Account 是嵌套类型,从 AccountConfig 内部属性反推出地址范围(DB90.10~DB90.69,60 字节),并在一次 PLC 读取中同时解析所有层级的数据。
注意:同一嵌套类型在同一父类型中出现多次时,必须显式标注
[HslDeviceAddress],否则框架无法区分地址。
3. 字段长度配置(可选)
string 和 byte[] 需要通过 [HslDeviceAddress] 的第二个参数 Length 指定 PLC 中的字节范围。如果希望将长度与代码解耦,可以通过 S7FieldConfig 从 JSON 配置文件加载:
s7fieldconfig.json:
{
"DeviceInfo": {
"Name": 30,
"FirmwareData": 16
},
"AccountConfig": {
"Address": 30,
"Key": 30
}
}
格式为 "类型名": { "属性名": 长度 },类型名用简单名称(不含命名空间)。
.csproj 配置(复制到输出目录):
<ItemGroup>
<None Update="s7fieldconfig.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
代码加载:
using S7Sharp;
// ① 加载配置
S7FieldConfig.LoadJson("s7fieldconfig.json");
// ② 预热(此时 TypeMeta 静态构造读取配置)
client.WarmUp<DeviceInfo>();
顺序不能乱:
LoadJson → WarmUp → ReadStruct。TypeMeta<T>是静态类,构造只执行一次,配置晚于 WarmUp 不会生效。
配置文件中的长度会覆盖 [HslDeviceAddress] 的 Length 参数。也可以用代码方式设置:
S7FieldConfig.Set<DeviceInfo>(d => d.Name, 30);
S7FieldConfig.Set(typeof(DeviceInfo), "FirmwareData", 16);
此时 [HslDeviceAddress] 就可以省略 Length 参数:
[HslDeviceAddress("DB90.52")] // 无需 Length,由配置提供
public string Name { get; set; } = string.Empty;
4. 启动时预热
using HslCommunication.Profinet.Siemens;
using System.Reflection;
var client = new SiemensS7Net(SiemensPLCS.S1200, "192.168.1.10");
client.ConnectServer();
// 预热单个类型(如果使用了 S7FieldConfig,必须先加载配置)
client.WarmUp<DeviceInfo>();
// 或扫描当前程序集中所有带 [WarmUp] 标记的类型并预编译
client.WarmUpAssembly(Assembly.GetExecutingAssembly());
WarmUp 会在首次调用前完成反射扫描和表达式树编译,避免第一次 ReadStruct 时的冷启动延迟。
5. 读取数据
var info = client.ReadStruct<DeviceInfo>();
Console.WriteLine($"急停: {info.Running}, 频率: {info.Frequency:F2} Hz");
Console.WriteLine($"账户地址: {info.Account.Address}, Key: {info.Account.Key}");
失败行为:
ReadStruct在 PLC 读取失败时返回一个默认构造的空对象(属性保持类型默认值),不抛异常;ReadField读取失败时静默跳过该字段;WriteStruct采用读-改-写,若整块读取失败则放弃写入。
6. 写回数据
单字段写回:
info.Frequency += 0.5f;
info.SpeedSetpoint = 100;
client.WriteField(info, d => d.Frequency);
client.WriteField(info, d => d.SpeedSetpoint);
按值写回(无需对象实例,v1.0.2+):
// 直接写值到 PLC,无需先读取或构造对象
client.WriteField<DeviceInfo>(d => d.Frequency, 55.5f);
client.WriteField<DeviceInfo>(d => d.SpeedSetpoint, 100);
client.WriteField<DeviceInfo>(d => d.Heartbeat, true, 40); // 支持字节偏移
client.WriteField<DeviceInfo>(d => d.Name, "S7Sharp"); // 支持 string / byte[] / 嵌套类型
单字段读取(设值到对象):
// 从 PLC 读取单个字段并设值到对象
client.ReadField(info, d => d.Frequency);
client.ReadField(info, d => d.Name);
单字段读取(直接返回值,v1.0.3+):
// 无需先构造对象,读取单个字段并直接返回其值
float freq = client.ReadField<DeviceInfo, float>(d => d.Frequency);
bool running = client.ReadField<DeviceInfo, bool>(d => d.Running);
string name = client.ReadField<DeviceInfo, string>(d => d.Name);
整块写回(读-改-写模式):
info.Frequency -= 0.5f;
info.Name = "新名称";
client.WriteStruct(info);
7. 同构多实例与字节偏移
当同一个 DB 块中存在多组同构数据区时(如多台电机、多个配方区),可通过 byteOffset 参数指定 DB 块内的起始字节偏移,无需为每个实例复制类型定义:
// 假设 DB20 中从字节 0 和字节 40 起各有一组相同的 MotorData
var motor1 = client.ReadStruct<MotorData>(); // DB20.0 起点
var motor2 = client.ReadStruct<MotorData>(40); // DB20.40 起点
client.ReadField(motor2, d => d.Speed, 40); // 读 DB20.40 的 Speed 字段
client.WriteField(motor2, d => d.SpeedSetpoint, 40); // 写 DB20.40 的 SpeedSetpoint
client.WriteStruct(motor2, 40); // 读-改-写 DB20.40 整块
偏移语义统一为 DB 块内字节偏移:
ReadStruct/WriteStruct:作用于结构体基址MinByte上ReadField/WriteField:作用于字段自身的字节地址;bit 字段的偏移加在字节部分,位号不变
所有参数默认 0,不传时行为与旧版本完全一致。负偏移导致地址越界时会抛出 ArgumentOutOfRangeException。
8. 嵌套类型的多实例
同一嵌套类型在父类型中出现多次时(如多组账户配置),通过 [S7Offset] 声明各自的字节偏移来区分数据区。偏移相对嵌套类型自身的起始地址(MinByte),内部字段相对布局不变:
[WarmUp]
public class DeviceInfo
{
// AccountConfig 默认区域(DB90.10 起)
public AccountConfig AccountA { get; set; }
// 同一嵌套类型第二实例:整体平移 90 字节 → DB90.100 起
// (内部字段相对布局不变:Address→DB90.100,Key→DB90.130)
[S7Offset(90)]
public AccountConfig AccountB { get; set; }
[HslDeviceAddress("DB90.70")]
public short SpeedSetpoint { get; set; }
}
var info = client.ReadStruct<DeviceInfo>(); // 一次读取,两个 Account 全部自动映射
client.ReadField(info, d => d.AccountB); // 读 DB90.100 数据区
client.WriteField(info, d => d.AccountB); // 写 DB90.100 数据区
client.WriteStruct(info); // 整块读-改-写,两个 Account 一起回写
注意:同一嵌套类型出现多次且未标注
[S7Offset]时,会在TypeMeta初始化阶段抛出InvalidOperationException,避免字段被静默丢弃。多个实例的数据区不要重叠,实例占用的实际宽度取决于其内部字段长度。
支持的数据类型
| C# 类型 | 地址格式 | 字节数 | 说明 |
|---|---|---|---|
bool |
DB90.0.3 |
1 bit | 位读取(read-modify-write 安全) |
short |
DB90.2 |
2 | Big-endian |
ushort |
DB90.2 |
2 | Big-endian |
int |
DB90.4 |
4 | Big-endian |
uint |
DB90.4 |
4 | Big-endian |
float |
DB90.2.0 |
4 | Big-endian (IEEE 754) |
double |
DB90.8 |
8 | Big-endian (IEEE 754) |
byte[] |
DB90.16 |
取决于 attr.Length |
原始字节块,attr.Length 或 S7FieldConfig 指定长度,默认 256 |
string |
DB90.10 |
取决于 attr.Length |
Siemens STRING (ISO-8859-1),attr.Length 或 S7FieldConfig 指定总字节数(2 字节头部 + 内容),默认 256 |
| 嵌套 class | 自动推导 | 自动推导 | 嵌套类型的属性标注 [HslDeviceAddress] 即可,父类型无需重复标注 |
工作原理
首次调用 ReadStruct<T> / WriteStruct<T>
→ 反射扫描 T 的 [HslDeviceAddress] 属性 + 自动发现嵌套类型
→ 编译表达式树为 Func<byte[], T> / Action<byte[], T>
→ 生成零反射包装器(ParserFunc / SerializerFunc)供嵌套类型调用
→ 缓存编译结果
后续调用
→ 直接使用编译好的委托(≈ 手写代码性能,无反射、无 DynamicInvoke)
编译后的代码等价于极致的直写字节解析循环,无任何反射和分支开销。嵌套类型的解析/序列化也通过预编译的包装委托实现零反射调用。
约束
- 所有属性必须引用同一个 DB 块(同一 struct 仅能映射单个 DB)
- 支持
bool/short/ushort/int/uint/float/double/byte[]/string/ 嵌套 class 十种数据类型 - string 格式为 Siemens
STRING(ISO-8859-1),非WSTRING - 属性必须可写(
CanWrite = true) - 同一嵌套类型在同一父类型中多次出现时,必须用
[S7Offset]指定字节偏移区分数据区
API 参考
SiemensS7NetExtensions
| 方法 | 说明 |
|---|---|
WarmUp<T>(this SiemensS7Net) |
预热单个类型,提前编译表达式树和初始化 S7 协议层 |
WarmUpAssembly(this SiemensS7Net, Assembly) |
扫描 Assembly 中所有 [WarmUp] 类型并预热 |
ReadStruct<T>(this SiemensS7Net, int byteOffset = 0) |
读取整个 DB 块并映射为强类型对象,可指定 DB 内字节偏移 |
ReadField<T, TValue>(this SiemensS7Net, T, Expression<Func<T, TValue>>, int byteOffset = 0) |
从 PLC 读取单个字段并设值到对象,可指定字节偏移 |
ReadField<T, TValue>(this SiemensS7Net, Expression<Func<T, TValue>>, int byteOffset = 0) |
读取单个字段并直接返回其值(v1.0.3+),可指定字节偏移 |
WriteField<T, TValue>(this SiemensS7Net, T, Expression<Func<T, TValue>>, int byteOffset = 0) |
写回单个字段到 PLC,可指定字节偏移 |
WriteField<T, TValue>(this SiemensS7Net, Expression<Func<T, TValue>>, TValue value, int byteOffset = 0) |
按值写回单个字段,无需对象实例,可指定字节偏移 |
WriteStruct<T>(this SiemensS7Net, T, int byteOffset = 0) |
读-改-写整块数据(安全覆盖,不误伤相邻位),可指定字节偏移 |
WarmUpAttribute
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class WarmUpAttribute : Attribute { }
标记需要预热的 class 类型,配合 WarmUpAssembly() 批量编译。
S7OffsetAttribute
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class S7OffsetAttribute : Attribute
{
public int ByteOffset { get; }
public S7OffsetAttribute(int byteOffset = 0);
}
标注在嵌套类型属性上,声明其相对嵌套类型起始地址(MinByte)的字节偏移。同一嵌套类型在父类型中出现多次时用于区分数据区。
S7FieldConfig
| 方法 | 说明 |
|---|---|
Set<T>(Expression<Func<T, object>>, int) |
强类型方式设置字段长度 |
Set(Type, string, int) |
直接按类型和属性名设置字段长度 |
LoadJson(string) |
从 JSON 文件加载字段长度配置 |
LoadJson(Stream) |
从 JSON 流加载字段长度配置 |
Clear() |
清除所有配置 |
SiemensStringHelper
| 方法 | 说明 |
|---|---|
DecodeString(byte[]) |
解析 Siemens STRING 字节数组 |
DecodeString(byte[], int, int) |
从指定偏移解析(零分配) |
EncodeString(string?, int) |
将字符串编码为 Siemens STRING 字节数组 |
EncodeStringInto(string?, int, byte[], int) |
编码并写入目标 buffer 指定偏移 |
DecodeWString(byte[]) |
解析 Siemens WSTRING (UTF-16BE) |
许可
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- HslCommunication (>= 12.9.2)
- Newtonsoft.Json (>= 13.0.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.