Skip to content

joesdu/EasilyNET.Drivers.S7Plus

Repository files navigation

EasilyNET.Drivers.S7Plus

通用的西门子 S7-1200 / S7-1500 符号寻址采集驱动,基于 S7CommPlus 协议,支持 TLS 1.3 加密通信与符号(变量名)读写。纯托管实现,不依赖原生 OpenSSL DLL,Windows / Linux 行为一致。

A general-purpose communication driver for Siemens S7-1200 / S7-1500 PLCs over the S7CommPlus protocol. Symbolic (tag-name) read/write with fully-managed TLS 1.3 — no native OpenSSL dependency.

演示视频


1. 概述与适用范围

  • 支持型号:S7-1200、S7-1500(含固件 V2.0 及以上、启用了优化访问的设备)。
  • 寻址方式符号寻址。地址直接填 PLC 中的变量名(如 DataBlock_1.Temperature),无需 rack/slot、无需手工计算 DB 偏移量。
  • 加密通信:S7CommPlus 在 IsoOnTCP 之上叠加 TLS 1.3。TLS 层使用纯托管的 BouncyCastleOrg.BouncyCastle.Cryptography),支持 S7CommPlus 合法化所需的 RFC 5705 密钥导出。
  • 端口:固定使用 ISO-on-TCP 端口 102(无需也无法另指定端口)。
  • 目标框架net10.0 / net11.0

⚠️ 不支持:标准(非优化)数据块的偏移量寻址、S7-300/S7-400、S7-200/S7-200 Smart。


2. PLC 端前置配置(重要)

在 TIA Portal 中必须完成以下设置,否则连接会在 TLS 或合法化阶段失败:

  1. 启用安全通信:CPU 属性 → “防护与安全” → “连接机制”,确认启用 安全的 PG/PC 及 HMI 通信(S7CommPlus 走的是这条加密通道)。
  2. 数据块使用优化访问:需要采集的 DB 属性中勾选 “优化的块访问”(默认即为优化)。本驱动只能读取优化 DB。
  3. 访问级别 / 密码
    • 设为 完全访问(无保护) 时,用户名/密码可留空。
    • 若设置了 HMI/读写访问密码,则需在选项中填入对应用户名与密码(通过 OMS 合法化完成认证)。
  4. 下载到 PLC 后配置才生效。

3. 安装

dotnet add package EasilyNET.Drivers.S7Plus

4. 快速开始

本客户端为异步优先(async-first):连接/读/写/断开均为 Task 异步方法,并支持 CancellationToken

using EasilyNET.Drivers.S7Plus;

// logger 可选;传 null 则不输出日志。推荐 await using 以异步释放
await using var client = new S7PlusClient(
    host: "192.168.0.1",
    options: new S7PlusClientOptions
    {
        Username = "",       // 无密码保护时留空
        Password = "",
        TimeoutMs = 5000
    },
    logger: null);

// 连接(异步)。Read/Write 在未连接时也会自动尝试连接。
if (!await client.ConnectAsync())
{
    Console.WriteLine("连接失败");
    return;
}

// 批量读取:按 PLC 中每个符号的真实类型解码为字符串
foreach (var v in await client.ReadAsync("DB_1.Temperature", "DB_1.Motor.Speed", "DB_1.Buffer[5]"))
{
    Console.WriteLine($"{v.Symbol} = {(v.IsGood ? v.Value : "<bad>")} @ {v.Timestamp}");
}

// 写入:按解析到的真实类型编码
await client.WriteAsync("DB_1.Setpoint", "42");
await client.WriteAsync(new Dictionary<string, string>
{
    ["DB_1.Enable"]    = "1",              // Bool
    ["DB_1.Name"]      = "Hello",          // String / WString
    ["DB_1.StartTime"] = "2026-07-02 08:30:00", // Date / DateAndTime / DTL / LDT
    ["DB_1.Delay"]     = "1s500ms"         // Time / LTime / S5Time(也可写纯数字)
});

// 带取消令牌
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
var values = await client.ReadAsync(["DB_1.Temperature"], cts.Token);

公开 API

成员 说明
new S7PlusClient(host, options?, logger?) 创建客户端;options 含用户名/密码/超时,logger 为可选 ILogger
Task<bool> ConnectAsync(ct = default) 异步连接,已连接返回 true
Task DisconnectAsync(ct = default) 异步断开并释放底层连接
bool Connected 当前是否已连接
Task<IReadOnlyList<S7TagValue>> ReadAsync(params string[] symbols) 异步批量读取;未连接时自动连接
Task<IReadOnlyList<S7TagValue>> ReadAsync(IEnumerable<string> symbols, ct = default) 同上,带取消令牌。等全部读完才返回
Task ReadStreamAsync(IEnumerable<string> symbols, Action<IReadOnlyList<S7TagValue>> onBatch, ct = default) 流式读取:每批读完立即回调,不等整轮读完
Task<bool> WriteAsync(string symbol, string value, ct = default) 异步写单点
Task<bool> WriteAsync(IEnumerable<KeyValuePair<string,string>> writes, ct = default) 异步批量写
ValueTask DisposeAsync() 异步释放(仅 IAsyncDisposable,请用 await using

S7TagValueSymbol(符号)、Value(解码后的字符串,失败为 null)、IsGood(是否成功)、Timestamp(读取时间)。

S7PlusClientOptionsUsernamePasswordTimeoutMs(缺省 5000)、ReadBatchSize(缺省 0 = 用 PLC 协商值,详见 §7)。

ReadAsync 的结果数可能少于输入符号数,两种原因:符号在 PLC 中解析不到,或读到中途通信失败(此时返回已读到的部分)。故不能用结果为空判定通信中断,请改看 Connected

流式读取 ReadStreamAsync

点位很多时 ReadAsync 要等全部读完才返回(7000 点实测约 21 秒)。ReadStreamAsync 每读完一次协议往返的量(批大小 = PLC 协商下发的每请求点数上限,实测 S7-1200 为 50)就立即回调,首批数据的到达时间 ≈ 一次往返(约 165ms),与整轮耗时解耦

await client.ReadStreamAsync(symbols, batch =>
{
    // 回调在读取循环内同步触发;勿做耗时操作,否则会拖慢后续批次的读取节奏
    foreach (var v in batch) { /* 推给上层:事件 / Channel / 入库 */ }
}, ct);

流式只交付真正读到的点:解析不到的符号不属于任何成功批次,不会出现在任何一批中。需要“未读到的点也返回空值”的语义,请用 ReadAsync 并自行比对输入集合。

关于异步的说明:本驱动为自上而下的真异步实现——从底层 socket 收发(Socket.*Async)、TLS 密文泵、ISO 帧组装,到 S7CommPlus 请求-响应等待,全链路均为真正的 await无后台线程、无忙等待轮询、无 Task.Run 包装。接收由单个异步“接收泵”驱动,响应经异步信号量交付,CancellationToken 贯穿全程。所有 I/O 通过同一异步信号量串行化,可从多处并发 await(自动排队),避免同一连接上的并发请求导致协议序列号错乱。


5. 点表地址(变量名)格式

地址 = PLC 中的符号路径。驱动在首次读/写该点时按需解析为内部访问序列并缓存。

<块名>.<成员名>[.<子成员名>...]            普通数据块 / 结构体 / UDT 成员
<块名>.<数组名>[<下标>]                    一维数组元素
<块名>.<数组名>[<下标1>,<下标2>,...]       多维数组元素
"<含特殊字符的块名>"."<含特殊字符的成员>"   名称含空格/点号时用英文双引号包裹该层
类型 示例
数据块成员 DB_1.TemperatureDB_1.Speed
结构体 / UDT 嵌套 DB_1.Motor.Current
含特殊字符的名称 "My DB"."My Tag"
一维 / 多维数组元素 DB_1.Buffer[5]DB_1.Matrix[2,3]
功能块实例 DB 成员 FB1_DB.Speed
工艺对象成员 PID_Compact_1.Setpoint

名称需与 TIA Portal 中完全一致。解析失败会在日志中提示“not found in PLC”。

兼容上位机/组态软件的完整路径(自动剥离前缀)

很多组态软件(如 Kepware)导出的地址带 设备名 + 标签组 前缀,例如 PLC_1.Blocks.DB1.test1。驱动会自动逐级剥离前缀后再解析:先用完整地址尝试,PLC 不识别就去掉最前面一段重试,直到命中。因此可直接填写完整路径,也可填原生地址(DB1.test1)。

  • 不依赖前缀固定为 PLC_1 / Blocks——通道名、标签组名任意都可。
  • 块名含特殊字符需用双引号时("My DB".tag),从引号处起视为块名,不再剥离。

数据类型(自动识别,无需配置)

驱动在解析符号时从 PLC 的类型信息中获取每个变量的真实类型,读取按真实类型解码,写入按真实类型编码。支持:Bool、Byte/USInt/SInt、Word/Int/UInt、DWord/DInt/UDInt、LWord/LInt/ULInt、Real、LReal、Char/WChar、String/WString、Date、Time/LTime、TimeOfDay/LTOD、DateAndTime/DTL/LDT 及上述类型的数组。

读取取值输出格式

类别 输出形式 示例
布尔 Bool 1 / 0 1
整数 十进制 1231
浮点 Real / LReal 小数点固定为 .(不受区域影响) 3.14
字符串 / 字符 纯文本 Hello / A
日期/日期时间 Date、DateAndTime、DTL、LDT yyyy-MM-dd HH:mm:ss 2026-06-23 12:34:56
时刻 TimeOfDay / LTOD HH:mm:ss[.fff] 12:34:56
时长 Time / LTime / S5Time 可读时长(LTime 带 LT# 前缀,S5Time 归一为毫秒/秒) 1d_02h_30mLT#1s_500ms9990ms
数组 元素以英文逗号拼接 10,20,30

6. 写值说明(重要)

可写类型:Bool、Byte、USInt、SInt、Char、WChar、Word、Int、UInt、DWord、DInt、UDInt、LWord、LInt、ULInt、Real、LReal、String、WString,以及日期/时间全系:Date、DateAndTime、DTL、LDT、TimeOfDay、LTOD、Time、LTime、S5Time。写入按解析到的真实类型编码。

标量与字符串

类型 写入字符串
Bool 1 / 0 / true(其它视为 false
整数 十进制,如 1231-5(超范围写入失败并记录日志)
Real / LReal 3.14(小数点用 .
Char / WChar 单字符,取首字符
String / WString 纯文本(按变量声明的最大长度校验,超长则跳过)

日期 / 时间

写入格式与读取输出保持一致,支持“读出 → 写回”的往返。

类型 写入字符串 示例
Date yyyy-MM-dd(可带时间,时间部分忽略) 2026-07-02
DateAndTime yyyy-MM-dd HH:mm:ss[.fff] 2026-07-02 08:30:00.250
DTL yyyy-MM-dd HH:mm:ss[.fffffffff] 2026-07-02 08:30:00.123456789
LDT yyyy-MM-dd HH:mm:ss 2026-07-02 08:30:00
TimeOfDay HH:mm:ss[.fff] 08:30:00.250
LTOD HH:mm:ss[.fffffffff] 08:30:00.000000001
Time 纯整数(毫秒)或时长字面量 15001s500ms-2h1d2h3m4s5ms
LTime 纯整数(纳秒)或时长字面量 250LT#1s500ms1us
S5Time 纯整数(毫秒)或时长字面量(自动选时基) 9990ms2s
  • 时长字面量支持单位 d/h/m/s/ms/us/ns、可选正负号、_ 分隔与 T#/LT#/S5T# 前缀;含无法识别的残留(如 1s5x)会被拒绝并跳过,不会误写。
  • DTL 特别说明:DTL 写入需要正确的“接口时间戳”,该值只能从一次实际读取中获得。驱动在写入 DTL 前会自动预读一次该变量以取得时间戳(同一连接内每个 DTL 变量仅预读一次);若预读失败,该 DTL 写入项会被安全跳过而非发出错误的包。

暂不支持写入(会记录“Unsupported write data type”并跳过,不会误写):Pointer、Any、Struct 以及所有数组类型

哪些点“写了不生效”:由 PLC 程序/系统逻辑每周期覆盖的变量(如定时器实例的输出位 .Q、功能块实例的输出参数 FB_DB.OUT),与上位机 RO/RW 标记无关——WriteValues 返回 0 但值很快被覆盖;要外部控制应改写其输入参数或预设值而非输出。


7. 大点表读取慢?先看这里(重要)

点位上千后读取会明显变慢。以下均为 S7-1200 + 7000 点的现场实测结论,可省掉一轮踩坑。

慢的根因是「串行往返次数」,不是代码

PLC 在连接时协商下发 TagsPerReadRequestMax(实测 S7-1200 给 50),协议层据此把点表切块、逐块串行往返:

7000 点 ÷ 50 = 140 次往返 × 每次约 165ms ≈ 21 秒

这是唯一的大头。该值会在连接时以 Information 级打进日志,先确认它再谈优化:

S7CommPlusConnection - SystemLimits: TagsPerReadRequestMax=50, ..., SubscriptionMemoryMax=102400, PlcAttributesMax=400

这个分块大小由 PLC 说了算:协议层一律使用协商值,不做任何猜测。

ReadBatchSize:只能把批调小

ReadBatchSize(缺省 0 = 用协商值)控制每批读取点数,它同时决定单次发往 PLC 的请求点数、ReadStreamAsync 的交付粒度,以及写入抢占的最长等待。

留空/0  → 批 = 协商值(50) → 每批恰好一次往返 → 首批 165ms,整轮 21 秒   ← 推荐
填 25   → 批 = 25         → 请求装不满      → 首批 ~80ms,整轮翻到 ~42 秒
填 100  → 被钳制回 50(协商值是 PLC 硬上限,超出会被拒绝),并记一条 Warning

缺省值已是最优解:批 = 协商值时每批恰好一次往返,首批延迟与整轮耗时同时最优。调小只在两种情况下有意义:PLC 处理满额请求吃力,或需要更细的流式交付粒度(用整轮变慢去换)。不确定就留空。

单连接上加多线程无效,只会把连接搞坏

S7CommPlus 要求同一连接任一时刻至多一个在途请求。并发发请求会导致响应认领错乱,随即被协议完整性校验强制断连。本客户端的 I/O 信号量是协议要求,不是可有可无的锁。

开多个客户端实例同样解决不了:PLC 会饱和

连接数 整轮(7000点) 单次往返(50点) 启动握手 吞吐
1 21.5 秒 165 ms 5 秒 326 点/秒
2 ~12 秒 ~165 ms 11 秒 ~590 点/秒
4 15.0 秒 22 秒 468 点/秒
8 13.5 秒 ~725 ms 44 秒 519 点/秒

8 条连接时每条都慢了 4.5 倍,8 倍并发只换来约 1.6 倍净吞吐 —— 不是并行,是排队分片。

吞吐硬上限 ≈ 520~590 点/秒(与连接数无关)
→ 7000 点整轮的物理下限 ≈ 12~13.5 秒,加连接突破不了
→ 3 秒内最多只能采集约 1600~1800 个点

而且多连接还会恶化单次往返耗时(165ms → 750ms,即首批延迟同比变差),并抢占 PLC 的 HMI 连接资源(与现场触摸屏 / WinCC 共享,S7-1200 尤其紧张)。故本客户端就是单连接,不提供并发选项。

那怎么办

  1. ReadStreamAsync:整轮耗时不变,但首批数据 165ms 就能拿到而非等 21 秒,数据持续流入而非每轮一次突发。多数场景这就够了。
  2. 真要秒级刷新:减少每轮点数(关键点高频、其余低频)。3 秒内最多约 1600~1800 个点。

换 PLC 型号(如 S7-1500)请按上表方法重新实测,不要照搬本表数字。


8. 运行机制与可靠性

  • 按需解析:连接成功后不会全量浏览整个 PLC,仅解析实际用到的符号,显著缩短大型程序的连接准备时间。
  • 批量读取:多个点按 PLC 协商的单请求最大点数自动分块(该值由 PLC 下发,客户端不覆盖);并以「一次协议往返」为批取放 I/O 锁(批大小恰好等于该协商值),使写入最多只等一次往返即可插入,无需等整轮读完。
  • 超时控制TimeoutMs 同时作用于 TCP 连接、socket 收发与单次请求等待;连接不可达的地址会按配置超时快速返回,而非等待操作系统默认的数十秒。
  • 自动重连:通信异常 / 读写超时 / PLC 主动终止会话时,Read/Write 会断开连接;下次调用自动重连并重新解析符号。
  • 资源释放:断开/重连及 Dispose 时完整释放底层连接(S7Client/Socket 等,全异步)。
  • 线程安全 / 并发:全部 I/O(连接/读/写/断开)通过同一异步信号量串行化,可安全地从多处并发 await——调用会自动排队,无需调用方手动串行化(避免同一连接的并发请求导致序列号错乱)。
  • 日志排查要点SystemLimits(Info,协商到的每请求点数上限)、handshake ... res=0x?(Debug,握手耗时与返回码,res=0x2 为 TCP 层连接超时)、ReadValues: N tags in M chunks ... ms(Debug,判断 PLC 是否饱和的主要依据)。

9. 技术说明与许可

  • 协议实现基于开源项目 thomas-v2/S7CommPlusDriverLGPL-3.0),并做了以下增强:
    • TLS 层由原生 OpenSSL P/Invoke 迁移为纯托管 BouncyCastle(TLS 1.3 + AES-GCM,支持 RFC 5705 EXPERIMENTAL_OMS 密钥导出)。
    • 全链路真异步(异步接收/发送泵 + 异步信号量串行化,移除后台线程与忙等待),并发调用自动排队。
    • 按需符号解析(移除全量浏览)、自动剥离上位机路径前缀、TimeoutMs 覆盖连接与 socket I/O。
    • 取值按真实类型解码、写值按变量真实结构编码:String/WString 用带 [maxLen][actLen] 头的字节/字数组、日期时间全系(含 DTL 自动预读接口时间戳)均可写;完善资源释放。

许可(双许可 Dual License)

本仓库按组件分别授权,每个源文件以 SPDX-License-Identifier 头声明其许可:

组件 许可 范围
高层封装 MIT S7PlusClient.csS7PlusClientOptions.csS7TagValue.cs
S7CommPlus 协议层 LGPL-3.0-or-later src/EasilyNET.Drivers.S7Plus/S7CommPlus/ 全部文件(派生自 thomas-v2/S7CommPlusDriver,© 2023 Thomas Wiens)
  • 许可全文:根目录 LICENSE(声明 + MIT)、LICENSE-LGPL-3.0.txtLICENSE-GPL-3.0.txt
  • 因协议层为 LGPL-3.0,编译后的整库(EasilyNET.Drivers.S7Plus.dll)须按 LGPL-3.0 条款使用与再分发(接收方有权对 LGPL 部分重新链接/替换)。MIT 仅覆盖上述三个封装文件,不削弱协议层的 LGPL 义务。以 NuGet 包形式动态引用(动态链接)本库时,保留这些声明并向用户提供对应源码即可满足 LGPL。
  • 之所以不用 .NET 内置 SslStream:其至今不支持导出 TLS 密钥材料(RFC 5705),而 S7CommPlus 合法化必须依赖该能力。

About

Development of Communication Driver for Siemens S7-1200/1500 Plcs

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages