S7Sharp 1.1.2

dotnet add package S7Sharp --version 1.1.2
                    
NuGet\Install-Package S7Sharp -Version 1.1.2
                    
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.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="S7Sharp" Version="1.1.2" />
                    
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.1.2
                    
#r "nuget: S7Sharp, 1.1.2"
                    
#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.1.2
                    
#: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.1.2
                    
Install as a Cake Addin
#tool nuget:?package=S7Sharp&version=1.1.2
                    
Install as a Cake Tool

S7Sharp

面向西门子 S7 系列 PLC 的 .NET 8 通信库:自研 S7 协议栈(TPKT / COTP / S7comm)。

  • 目标框架:net8.0(C# 12)
  • 根命名空间:S7Sharp.Siemens
  • 依赖:包元数据的 dependencies 当前为空(零第三方依赖)—— 协议内核、传输层、对象映射自研,配置反序列化走 BCL System.Text.Json;日后引入第三方组件会出现在该处
  • 分发形式:以 NuGet 包(二进制)形式提供;源码仓库不对外公开,因此本文不涉及从源码构建,包内也不含源码/符号服务器

安装

dotnet add package S7Sharp

包内含 lib/net8.0/S7Sharp.dll 与 XML 文档(S7Sharp.xml,供 IntelliSense),调试符号已嵌入 DLL。


快速开始

using S7Sharp.Siemens;

using var plc = new SiemensS7Net
{
    IpAddress = "192.168.0.10",
    Port      = 102,     // 默认 102
    Rack      = 0,       // 默认 0
    Slot      = 1,       // S7-1200/1500 通常为 1
    ConnectTimeoutMs = 10000,   // 可选:连接超时,默认 10000
    IoTimeoutMs      = 5000     // 可选:单次请求收发超时,默认 5000
};

S7Result connected = plc.ConnectServer();
if (!connected.IsSuccess)
{
    Console.WriteLine($"连接失败:{connected.Message} (code={connected.ErrorCode})");
    return;
}

// 读 4 个字节
S7Result<byte[]> raw = plc.Read("DB1.DBB0", 4);

// 读一个位
S7Result<bool> bit = plc.ReadBool("DB1.DBX0.3");

// 写字符串(自动按 PLC 声明的最大长度加长度头)
S7Result written = plc.Write("DB1.DBB10", "HELLO");

plc.ConnectClose();

每个方法都有同步版与 *Async 异步版(异步版接受 CancellationToken)。 SiemensS7Net 是 IDisposable,Dispose() 会关闭通道;ConnectClose() 幂等,可显式提前关闭。 只读运行时属性:IsConnected(通道是否已连接并就绪)、NegotiatedPdu(协商到的 PDU 长度,0 = 尚未协商;分包预算 = 本值 − 28)。


地址文法

解析器 S7VarAddressParser 只接受实测过的形态,未在真机/靶机上验证过的形态一律解析失败(本地失败、零网络流量):

区域 形态 示例
数据块 DB<号>.DBX<字节>.<位> / DB<号>.DBB<字节> / DBW / DBD DB1.DBX0.1、DB1.DBW2
标志位 M M<字节>[.位]、MB/MW/MD<字节> M100、M100.1、MB100
输入 I I<字节>[.位]、IB/IW/ID<字节> I0.0、IW0
输出 Q Q<字节>[.位]、QB/QW/QD<字节> Q0.1、QW0
  • DB 号范围 1..65535,位号 0..7;协议地址字段是 3 字节位地址 ⇒ 上限 0xFFFFFF(等价于字节偏移上限 0x1FFFFF)。
  • 尺寸标记(DB 的 DBB/DBW/DBD,M/I/Q 的 B/W/D 后缀)不改变地址换算(协议地址字段恒为位地址,DB1.DBW2 → 0x000010);写 DBB 只是可读性标记。
  • 不支持 V 区、定时器 T、计数器 C(未实测,不做猜测)。

原语读写

能力 同步 异步
读字节 Read(address, ushort length) ReadAsync
写字节 Write(address, byte[]) WriteAsync
读单个位 ReadBool(address) ReadBoolAsync
读多个位 ReadBool(address, ushort length) ReadBoolArrayAsync
写单个位 Write(address, bool) WriteBitAsync
写多个位 Write(address, bool[]) WriteBitArrayAsync
读字符串(S7 感知,两帧) ReadString(address) ReadStringAsync
读字符串(固定长度,去头) ReadString(address, ushort length) ReadStringAsync
写字符串 Write(address, string) WriteStringAsync
读/写 WSTRING ReadWString / WriteWString ReadWStringAsync / WriteWStringAsync
读/写 DATE_AND_TIME ReadDateTime / Write(address, DateTime) ReadDateTimeAsync / WriteDateTimeAsync
读/写 DTL ReadDTLDataTime / WriteDTLTime ReadDtlAsync / WriteDtlAsync
读/写 DATE ReadDate / WriteDate ReadDateAsync / WriteDateAsync
多地址批量读 Read(string[], ushort[]) ReadMultipleAsync
读订货号 ReadOrderNumber() ReadOrderNumberAsync

要点:

  • 字符串:读返回纯内容(不含 2 字节长度头),与写对称;写会先试探读 2 字节取 PLC 声明的 maxLen(读到 0 按 254 处理),超长拒绝而非截断。
  • 位数组写:只支持单字节内的位(起点位号 + 位数 ≤ 8),走 read-modify-write,未覆盖的高位保持原值;跨字节写会被明确拒绝。
  • 多地址读:多个项进同一请求,返回值为按入参顺序拼接的一段连续字节,调用方按 length 自行切分。
  • 分包:按协商 PDU 自动分包(NegotiatedPdu),无需调用方干预。
  • 断线语义:一次请求开始前若发现通道已断,自动重连一次,失败即返回失败 —— 无重试、无退避(避免写路径上的重复写入风险)。

标量数值读写(P9-1)

不经 POCO 映射,按地址直接读 / 写 S7 的基本数值类型(一律大端;浮点位保真 —— NaN 载荷与 -0.0 原样往返)。

// 单值
client.Write("DB1.DBD0", 1234);                    // DINT
int count = client.ReadInt32("DB1.DBD0").Content;

client.Write("DB1.DBD4", 12.5f);                   // REAL
float torque = client.ReadFloat("DB1.DBD4").Content;

// 数组:length 是**元素个数**
client.Write("DB1.DBD8", new long[] { 1, 2, 3 });  // LINT[]
long[] values = client.ReadInt64("DB1.DBD8", 3).Content;

// 异步面与同步面成对
S7Result<ushort> flags = await client.ReadUInt16Async("DB1.DBW20");
S7 宽 读(单值 / 数组) 写(重载) CLR
INT 2 ReadInt16(addr) / ReadInt16(addr, len) Write(addr, short) / Write(addr, short[]) short
WORD 2 ReadUInt16 Write(addr, ushort) ushort
DINT 4 ReadInt32 Write(addr, int) int
DWORD 4 ReadUInt32 Write(addr, uint) uint
LINT 8 ReadInt64 Write(addr, long) long
ULINT 8 ReadUInt64 Write(addr, ulong) ulong
REAL 4 ReadFloat Write(addr, float) float
LREAL 8 ReadDouble Write(addr, double) double

异步面:读用 ReadXxxAsync / ReadXxxArrayAsync,写用同名重载 WriteAsync(addr, value, ct)。

语义要点

  • 数组 length = 元素个数;length == 0 或空数组 / null ⇒ 本地失败 LengthOutOfRange,不发帧
  • 单次请求 = length × 宽度 字节,超过 65535 ⇒ 本地失败 LengthOutOfRange(不内部分批,见 I62)
  • 位地址(DB1.DBX0.1 / M100.1)一律拒绝 AddressFormat —— 协议里位访问走位传输,与字节传输不是同一语义
  • 尺寸标记(DBB / DBW / DBD)不参与偏移换算,也不校验与目标类型宽度是否一致
  • 解码与对象映射面同源(BinaryPrimitives 大端原语),两面对同址同类型逐值相等

升级注意(1.1.1 起,源码级破坏)

写面新增了 8 个 Write(string, T[]) 重载,于是用集合表达式/数组字面量传参的调用会变成二义性:

client.Write("M100", [1, 2, 3]);                    // ❌ 编译失败:byte[] / short[] / int[] 都匹配
client.Write("M100", new byte[] { 1, 2, 3 });       // ✅ 显式类型即可

已存变量(byte[] data)与新类型的显式数组(new short[] { … })不受影响。

对象映射(POCO ↔ DB 块)

按 [S7Address] 标注把 DB 块字节与强类型对象双向映射,映射器由表达式树编译,首次访问类型时编译一次并缓存。

using S7Sharp.Siemens;

public sealed class DeviceInfo
{
    [S7Address("DB90.0")]      public bool   Running   { get; set; }  // DB90.DBX0.0
    [S7Address("DB90.2")]      public short  Temperature { get; set; }
    [S7Address("DB90.4")]      public float  Pressure   { get; set; }
    [S7Address("DB90.8")]      public double Totalizer  { get; set; }
    [S7Address("DB90.16", 10)] public string Name       { get; set; }
}

// 预热(可选):提前完成表达式编译与 JIT
plc.WarmUp<DeviceInfo>();

// 整块读(一帧)
S7Result<DeviceInfo> info = plc.ReadStruct<DeviceInfo>();

// 整块写(读-改-写两帧)
S7Result ok = plc.WriteStruct(info.Content);

// 字段级读写
plc.ReadField(info.Content, d => d.Pressure);
plc.WriteField(info.Content, d => d.Pressure, 1.25f);

扩展方法(SiemensS7NetExtensions):WarmUp<T> / WarmUpAssembly(Assembly) / ReadStruct<T> / WriteStruct<T> / ReadField / WriteField(后两者各有"带对象实例"与"直接给值"两个重载,均可附加 byteOffset 用于同一 DB 内的同构结构多实例,详见下文「偏移量用法」)。

  • 支持字段类型:bool、short、ushort、int、uint、long、ulong、float、double、string、byte[]、嵌套类(自动推导或按 [S7Offset] 声明偏移)。
  • 数值布局一律大端;STRING 按 Latin-1 逐字节解码。
  • 映射实体必须是可变 class + 无参构造 + 可写属性(反射映射通道要求),不要写成 record / init / required。
  • 约束(违反即在类型首次访问抛出,不产生任何报文):所有字段须属同一个 DB;同一嵌套类型出现多次时每个属性都须带 [S7Offset]。异常发生在泛型类型的静态构造里,调用方看到的是被 CLR 包一层的 TypeInitializationException(内层才是 InvalidOperationException);要按 bool 判定请用 TryGetLayout(见下文「运行期类型的映射面」)。
  • string / byte[] 的长度只来自 [S7Address].Length(缺省 256)。P4-2 起的 S7FieldConfig(可在运行期覆盖长度)已随 P5-1 退场 —— 长度是标注期 / 生成期已知量,而类型的映射元数据只构建一次,"运行期改长度"只能靠重生类型(理由见 P5-1 专项设计 §2.4)。

类型的映射元数据只在首次访问该类型时构建一次(TypeMeta<T> 静态构造),之后不再重读任何配置。

已知的不对称行为(既有语义,原样保留):整块写时 STRING 的 maxLen = 标注宽度 - 2,而字段级写走底层"试探读 → 按 254 写头"。


配置驱动的动态类型(P5-1)

不预写 class,按 JSON 在运行期生成类型:

{
  "description": "整批配置的说明(可选)",
  "types": [
    { "name": "Head", "dbNumber": 90, "description": "子结构:双轴头",
      "fields": [ { "name": "A", "type": "int32", "address": "0" } ] },
    { "name": "DynCfg", "dbNumber": 90,
      "fields": [
        { "name": "Temp",  "type": "float",  "address": "6",  "description": "油温" },
        { "name": "Name",  "type": "string", "address": "26", "length": 20 },
        { "name": "Head1", "type": "Head",   "offset": 0 }
      ] }
  ]
}
using S7DynTypeSet set = S7DynTypes.Register("dynconfig.json");
set.TryGetType("DynCfg", out Type dynType);   // 逻辑名 → 生成的类型
  • 地址只写块内位置("0" / 位字段 "0.0";P6-3 起):DB 号只来自类型级 dbNumber,生成时拼成完整地址 [S7Address("DB90.0")] —— 故调用面 / 协议面看到的地址一字未变(S7VarAddressParser 拿到的仍是 DB90.6 这种形态),改 dbNumber 就是整体搬家、字段不用动。
  • 旧写法不再合法(P6-3 起,破坏性变更):"address": "DB90.6" 会被拒,消息里给出改写建议(请改写为「6」)—— 本次刻意不引入 schema 版本号,兼容性由这条消息承担。
  • 严格校验:非法配置(未知类型字符 / 地址文法错(含仍带 DB 前缀的旧写法)/ 逻辑名重复 / 循环嵌套 / 多实例嵌套缺 offset / 未知键……)在 Register 时立即抛出 S7DynConfigException,不做静默跳过。键集固定:顶层只认 types / description,类型级只认 name / dbNumber / fields / description,字段级只认 name / type / address / length / offset / description —— 多一个未知键、或缺少必填项(顶层 types、类型级 name)都会报错,拼错的键名不会被静默忽略。
  • 逻辑名是配置与解析的唯一键,与生成的 CLR 类型名解耦;注册即查重 —— 同一逻辑名不允许两个活着的批次(热更请先 Dispose 旧句柄)。
  • Dispose 句柄 = 丢弃本批次引用;调用侧不得缓存旧的 Type / 映射面编译出的委托 / T 的实例,否则本批次仍被钉住而无法回收。
  • 把运行期类型喂给映射面走非泛型桥入口(与泛型版同一实现路径、异常类型一致):
using S7DynTypeSet set = S7DynTypes.Register("dynconfig.json");
set.TryGetType("DynCfg", out Type dynType);

S7Result<object> read = plc.ReadStruct(dynType);        // 读:整块读一帧
object obj = read.Content;
object boxed = obj;
S7Result ok = plc.WriteStruct(boxed);                    // 写:读-改-写两帧
  • 描述字段(可选,纯元数据):配置的顶层 / 类型级 / 字段级都可写 description,生成时分别落成 BCL [Description],挂在批次程序集 / 生成的类型 / 生成的属性上:set.Assembly.GetCustomAttribute<DescriptionAttribute>()?.Description · type.GetCustomAttribute<DescriptionAttribute>()?.Description · type.GetProperty("Temp").GetCustomAttribute<DescriptionAttribute>()?.Description(需 using System.ComponentModel;)。它不参与地址与宽度推导,映射面只认 [S7Address] / [S7Offset],故不受影响;省略 / 空串 / 全空白视为未提供。
  • 热更流程:旧句柄.Dispose() → Register(新配置) → TryGetType → 桥入口。热更后旧类型的动态程序集可被回收(映射缓存改 ConditionalWeakTable,不再强引用类型)—— 前提是调用侧没有缓存旧 Type / 委托 / 实例。
  • 支持的类型字符集:bool(地址须带位号,形如 0.0)/ int16 / uint16 / int32 / uint32 / int64 / uint64 / float / double / string / bytes / 另一逻辑名(嵌套,可带 offset)。
  • 不支持 NativeAOT:生成的类型是运行期动态代码(AssemblyBuilder),AOT 场景下不可用。

保存之前先只读校验:不生成类型、不注册、不占用逻辑名,一次把问题列全。

S7DynValidationReport report = S7DynTypes.Validate("dynconfig.json");

if (!report.IsValid)
    foreach (S7DynValidationIssue issue in report.Issues)      // 错误:该配置注册不了
        Console.WriteLine($"[{issue.LogicalName}/{issue.FieldName}] {issue.Message}");

foreach (S7DynValidationIssue warn in report.Warnings)         // 告警:不拦任何动作
    Console.WriteLine(warn.Message);

IReadOnlyList<string> kinds = S7DynTypes.SupportedFieldTypes;  // type 的取值域,供下拉
  • Validate 与 Register 走同一份判定(同一遍历顺序)⇒ Issues 的第一条与 Register 会抛出的那条逐字相同;Register 仍是 fail-fast(首个非告警问题即抛 S7DynConfigException)。
  • Issue.Message 含"来源 + 逻辑名 + 字段名 + 原因"四要素;LogicalName / FieldName 另给结构化定位,界面可直接跳到对应字段行,无需解析字符串。
  • 告警不拦任何动作:不影响 IsValid、不拦 Register,也不塞进 Issues。三条判定(字节区间重叠 / 同位重复 / 位 × 字节混用)都可能是故意的 —— PLC 侧确有"同一段字节按两种类型读"(双视图)的合法用法,做成错误会拒收既有配置。
  • SupportedFieldTypes 只描述取值域(与内部类型表同一份);某字段是否合法仍由 Validate 判定。
  • Validate 与 Register 都接受文件路径或 Stream 重载。

偏移量用法

两套偏移各管一段、互不相干,别混用:

手段 写在哪 作用域 何时确定
[S7Offset(n)] 嵌套类属性上 该嵌套实例整体挪到「嵌套类型起始地址 + n」 类型设计时(元数据)
byteOffset 参数 调用时的可选末位参数(int byteOffset = 0) 这一次调用整体平移 每次调用时(不入缓存)

一、[S7Offset]:同一嵌套类型的多个实例

public class PocoNested                       // 嵌套类型自身的相对布局
{
    [S7Address("DB100.0")] public short A { get; set; }
    [S7Address("DB100.2")] public int   B { get; set; }
}

public class PocoTwoHeads
{
    [S7Offset(0)] public PocoNested Head1 { get; set; }   // A→DB100.0,B→DB100.2
    [S7Offset(8)] public PocoNested Head2 { get; set; }   // A→DB100.8,B→DB100.10
}
  • 同一嵌套类型出现两次及以上时,每个属性都必须带 [S7Offset](缺一个即在类型首次访问抛 TypeInitializationException,内层为 InvalidOperationException;不产生任何报文)。
  • [S7Offset(n)] 的语义是「嵌套类型起始地址 + n」,不是在该属性上重新排布;嵌套内部的相对间距保持不变(上例 Head2.B 仍紧跟 A 后 2 字节)。
  • 只出现一次时 [S7Offset] 可省略;写 [S7Offset(0)] 与不写等价,写非 0 值即整体平移。

二、byteOffset 参数:调用时的整体平移

ReadStruct<T> / WriteStruct<T> / ReadField(两个重载)/ WriteField(两个重载)都有此参数,位置在最后一个:

// DeviceInfo 标注从 DB90.DBB0 起,长度 26 字节;第二个实例按 stride 32 摆放
S7Result<DeviceInfo> second = plc.ReadStruct<DeviceInfo>(32);   // 读 DB90.DBB32 起
plc.WriteStruct(second.Content, 32);                            // 写同一段

plc.WriteField(second.Content, d => d.Temperature, 32);         // DB90.DBB34(自身字节地址 2 + 32)
plc.WriteField(second.Content, d => d.Running,     32);         // DB90.DBX32.0(位字段:位号不变)
S7Result<float> p = plc.ReadField<DeviceInfo, float>(d => d.Pressure, 32);
  • 整块:起始地址 = DB<号>.DBB<最小字节地址 + byteOffset>,读取长度恒为该类型的字节跨度(TotalBytes,与偏移无关)→ 第 N 个实例传 stride × N,stride 由你自己按 PLC 侧布局给(通常 ≥ TotalBytes 并含对齐)。
  • 字段级:偏移加在该字段自身的字节地址上;位字段走 DBX<字节 + byteOffset>.<原位号> —— 只移字节部分、位号不变。
  • 偏移只作用于本地生成的地址字符串,不改 [S7Address] 元数据;对象内各字段的相对布局不变。
  • 偏移是调用级的,不会被缓存进类型元数据,同一类型可一次传 0、一次传 32 混着用。
  • 偏移使地址变负(如 最小字节地址 + byteOffset < 0)时抛 ArgumentOutOfRangeException,属本地失败,不发帧。

运行期类型的映射面

ReadStruct<T> / WriteStruct<T> 要求编译期就知道类型。手里只有 Type(例如 S7DynTypes 生成的那种)时走这一组非泛型入口 —— 地址与宽度只来自映射面本身,调用方不必按配置重算一遍(那是第二份规则):

能力 方法
取布局只读快照 TryGetLayout(Type, out S7StructLayout)
整块读原始字节 ReadStructBytes(Type, byteOffset = 0)
整块读 → 对象 ReadStruct(Type, byteOffset = 0)
整块写 ← 对象 WriteStruct(object, byteOffset = 0)
按属性名写回该字段 WriteField(object, propertyName, byteOffset = 0)
字段级读写(表达式) 泛型 ReadField<T, TValue> / WriteField<T, TValue>(见上文「对象映射」)

S7StructLayout(一个类型的布局):ReadAddress(整块基址,可直接喂 Read)· DbNumber · MinByte · TotalBytes(整块跨度 = 整块读长度)· Fields。 S7FieldLayout(每个字段一条):Name · Address · OffsetInBlock(相对整块起点)· ByteWidth · IsBit / BitIndex(位号,非位字段为 -1)· Nested(子结构布局,逐层展开用;非子结构为 null)。

if (SiemensS7NetExtensions.TryGetLayout(dynType, out S7StructLayout layout))
{
    Console.WriteLine($"{layout.ReadAddress}:{layout.TotalBytes} 字节,DB{layout.DbNumber}");
    foreach (S7FieldLayout f in layout.Fields)
        Console.WriteLine($"{f.Name} @ {f.Address} offset={f.OffsetInBlock} width={f.ByteWidth}"
                          + (f.IsBit ? $" bit={f.BitIndex}" : ""));
}

S7Result<byte[]> raw = plc.ReadStructBytes(dynType);   // 与 ReadStruct(Type) 同一基址与长度
  • TryGetLayout 不抛:不可映射(无可读字段 / 跨 DB 混用 / 多实例嵌套缺 [S7Offset])返回 false,type 为 null 才抛。
  • ReadStructBytes 的理由是"要原始字节":ReadStruct 只给解好的对象,而逐字节比对(写前 / 写后)需要字节。
  • WriteField(obj, 属性名) 写的是对象里此刻的值 —— 接口上无法指定"写什么值";要指定值请用泛型 WriteField(obj, d => d.X, value)。
  • 子字段偏移的参照系:实例起点 ≡ 子类型的基准(MinByte)⇒ 叶子在父块里的偏移 = 父字段 OffsetInBlock + 子字段 OffsetInBlock,不再补 MinByte;多层同样逐层相加。

离线解码:字节 → 对象 / 字段值

S7StructParser 是 ReadStructBytes 的反函数 —— 把整块字节解成对象、或取出某个字段的值,全程不碰设备、不发帧:

// 整块字节:来自 ReadStructBytes,或来自数据变化监控事件
S7Result<byte[]> raw = plc.ReadStructBytes(dynType);

if (S7StructParser.TryParse(dynType, raw.Content, out object obj, out string why))
    Console.WriteLine(obj.GetType().Name);

if (S7StructParser.TryReadField(dynType, raw.Content, "Head1.Temp", out object temp, out why))
    Console.WriteLine($"油温 = {temp}");
  • 值只来自映射面:内部走的就是 ReadStruct 用的同一个解码器(不是第二份解码规则)⇒ 与 ReadStruct 同值。
  • 点路径文法:. 分隔 · 属性名大小写敏感(ordinal)· 逐层走 S7FieldLayout.Nested · 不解析 Address 串;叶子是子结构时失败(并提示继续往里展开到基本类型字段)。它与数据变化监控事件里的字段名同一条文法,故"事件报出的字段名"可以直接当路径用。
  • 失败语义:bool + out error(人可读的原因),不抛业务异常;只有 type == null 才抛 ArgumentNullException(编程错误)。
  • 块长度:必须 ≥ 该类型的 S7StructLayout.TotalBytes(更长允许,多出来的字节不参与解码)。这条检查不多余 —— 解码器对越界字段静默跳过,不先查长度的话,"块短了"会表现成"某个字段凭空是默认值"。

按点路径写:算清"写哪一层"

S7 线上没有"按叶子写" —— 写一个叶子必然是把整个实例的字节读-改-写。S7StructWriter 把"布局 + 点路径"算成"写哪一层、目标在哪儿",产物是 S7FieldTarget;它本身不写设备(真正的落笔是写"最外层字段"):

// dynType = 生成的类型;layout = TryGetLayout 的快照;obj = ReadStruct(Type) / TryParse 读回来的实例
if (S7StructWriter.TryResolve(dynType, layout, "Head1.Temp", out S7FieldTarget target, out string why))
{
    // ① 读:拿当前值(通常来自 ReadStruct(Type) 或 ReadStructBytes,也可从实例里取)
    S7StructWriter.TryGetValue(target, obj, out object current, out _);

    // ② 改:就地改叶子(改的是读回来的那个实例)
    S7StructWriter.TrySetValue(target, obj, 42.5f, out _);

    // ③ 写:落到设备上的粒度 = 最外层字段
    plc.WriteField(obj, target.Outer.Name);
}

S7FieldTarget 给出判据需要的输入事实(调用方不必自己重算布局数学):

成员 含义
FieldPath / LeafType 路径原文 / 叶子的 CLR 类型(值转换的类型来源)
Outer 真正被写的那一层(顶层字段 = 该字段本身;路径写 = 最外层实例)
Leaf / Chain / OffsetInBlock 目标叶子 / 从外到内的属性链 / 叶子在整块里的字节偏移
IsNested 是否多层(写范围 ⊃ 目标范围 ⇒ "顺带改了整个实例")
IsBit / BitIndex / ByteWidth 位事实与宽度(位字段宽度按 1 计)
TargetLo / TargetHi 目标区间(位字段 = 所在字节;非位字段 = 叶子自己的字节区间)
WriteLo / WriteHi 写区间(顶层字段 = 叶子;路径写 = 最外层实例)
Address / LeafProperty 展示用地址(DB90.DBB6 / DB90.DBX0.3)/ 叶子属性
  • 三个方法全部 bool + out error,纯离线:TryResolve / TryGetValue 不改任何对象,TrySetValue 只改传进去的那个实例;设备上的落笔仍由调用方用 WriteField 完成。type / layout / target 为 null 才抛(编程错误)。
  • 与读侧共用同一套点路径文法(大小写 ordinal · 逐层走 Nested · 不解析 Address 串)⇒ TryReadField 取得到的路径,TryResolve 也定位得到。
  • AddressOf(dbNumber, byteInDb, leaf) 可单独调用 —— 拼子字段的展示串时用它(地址格式只此一份)。
  • 叶子是子结构时 TryResolve 失败(整体赋值不支持):要么继续往里展开,要么改用 bytes 字段。

结果模型与错误码

所有对外方法不抛业务异常(PLC / 协议 / 传输侧的失败一律走结果模型),统一返回 S7Result / S7Result<T>: 编程错误仍会抛:null 参数 ⇒ ArgumentNullException / ArgumentException,偏移使地址为负 ⇒ ArgumentOutOfRangeException,类型不可映射 ⇒ TypeInitializationException(各节分别注明)。

S7Result<byte[]> r = plc.Read("DB1.DBB0", 4);
if (r.IsSuccess) { /* r.Content */ }
else             { /* r.Message, r.ErrorCode */ }

ErrorCode 约定(S7ErrorCodes):

值 含义
0 成功
负值 本地失败:-1 地址格式 / -2 地址越界 / -3 长度非法 / -4 超 PDU 预算 / -5 响应结构非法 / -6 连接失败 / -7 超时 / -8 通道关闭 / -9 帧结构非法
正值 PLC 返回的原始 return code,原样透传

已知边界

  • 不做 T / C 区与 V 区寻址(未实测)。
  • 不做 PLC 型号族的握手帧变体:本库只有显式 Rack / Slot,没有 SiemensPLCS(S300/S400/S200Smart)枚举 —— 不同系列的默认槽号、src TSAP、握手帧差异未实现。
  • ReadOrderNumber() 是"照既有行为实现":从响应完整帧的固定偏移 [71, 91) 取 20 字节 ASCII,未逐字段解析 SZL 结构;换 PLC 型号/固件时该偏移是否仍成立,本库不作保证。
  • 未接入真实 PLC 验证(内部以自研 S7TestServer 靶机做端到端测试,该靶机不随包分发);上生产前请在目标机型上做一次端到端校验。

许可证

MIT —— NuGet 包以 license 元数据(MIT)声明,条款全文见 https://licenses.nuget.org/MIT。

本包仅以二进制形式分发:源码仓库不对外公开,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.
  • net8.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on S7Sharp:

Package Downloads
S7Sharp.Watch

S7Sharp 的数据变化监控库:按每个定义自己的刷新间隔周期读一块数据(定义来源可以是 C# 类,也可以是 JSON 配置生成的运行期类型), 与缓存按字节比较 —— 没有变化则零动作,有变化则更新缓存并以 event 通知(带差异字节与变化字段名)。 只读、进程内、零第三方依赖;事件与读取解耦(独立队列,每定义合并只留最新一条)。 未接真实 PLC 验证(现场校验请以目标机型为准)。

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.2 104 10/21/2026
1.1.1 141 9/28/2026
1.1.0 86 9/21/2026