S7Sharp 1.1.2
dotnet add package S7Sharp --version 1.1.2
NuGet\Install-Package S7Sharp -Version 1.1.2
<PackageReference Include="S7Sharp" Version="1.1.2" />
<PackageVersion Include="S7Sharp" Version="1.1.2" />
<PackageReference Include="S7Sharp" />
paket add S7Sharp --version 1.1.2
#r "nuget: S7Sharp, 1.1.2"
#:package S7Sharp@1.1.2
#addin nuget:?package=S7Sharp&version=1.1.2
#tool nuget:?package=S7Sharp&version=1.1.2
S7Sharp
面向西门子 S7 系列 PLC 的 .NET 8 通信库:自研 S7 协议栈(TPKT / COTP / S7comm)。
- 目标框架:
net8.0(C# 12) - 根命名空间:
S7Sharp.Siemens - 依赖:包元数据的
dependencies当前为空(零第三方依赖)—— 协议内核、传输层、对象映射自研,配置反序列化走 BCLSystem.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 | 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
- 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.