S7Sharp 1.0.3

dotnet add package S7Sharp --version 1.0.3
                    
NuGet\Install-Package S7Sharp -Version 1.0.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="S7Sharp" Version="1.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="S7Sharp" Version="1.0.3" />
                    
Directory.Packages.props
<PackageReference Include="S7Sharp" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add S7Sharp --version 1.0.3
                    
#r "nuget: S7Sharp, 1.0.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package S7Sharp@1.0.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=S7Sharp&version=1.0.3
                    
Install as a Cake Addin
#tool nuget:?package=S7Sharp&version=1.0.3
                    
Install as a Cake Tool

S7Sharp

基于 HslCommunication 的 Siemens S7 PLC 强类型数据映射库。通过表达式树(Expression Tree)在运行时编译高性能的字节解析器和序列化器,将 PLC DB 块数据直接映射到 C# class,避免逐字段手工 ReadBool / ReadFloat / ReadString 的重复编码。

动机

HslCommunication 提供了内置的 SiemensS7Net.Read<T> 泛型方法用于自动映射,但在实际使用中存在两个核心问题:

  1. BugRead<T> 在处理某些数据类型(如 Siemens STRING)时存在行为偏差,导致解析结果不可靠
  2. 效率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. 字段长度配置(可选)

stringbyte[] 需要通过 [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 → ReadStructTypeMeta<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.LengthS7FieldConfig 指定长度,默认 256
string DB90.10 取决于 attr.Length Siemens STRING (ISO-8859-1),attr.LengthS7FieldConfig 指定总字节数(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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.3 66 9/1/2026
1.0.2 66 9/1/2026
1.0.1 80 8/25/2026
1.0.0 114 8/2/2026