├── Doc
├── COTP1.png
├── CPU地址区.png
├── S7.pcapng
├── leaf.png
├── rfc905.pdf
├── 区域地址类型.png
├── COTP-PDU.png
├── S7-Area.png
├── S7-Kind.png
├── newlife.snk
├── COTP-Setup.png
├── S7-Function.png
├── S7-SyntaxID.png
├── 西门子S7协议介绍.docx
├── S7-ReadWrite.png
└── PLC区域类型块.txt
├── XUnitTest
├── BasicTest.cs
├── S7PLCTests.cs
├── XUnitTest.csproj
├── PLCAddressTests.cs
├── COTPTests.cs
└── S7MessageTests.cs
├── NewLife.Siemens
├── Messages
│ ├── S7Functions.cs
│ ├── IDataItems.cs
│ ├── ReadWriteErrorCode.cs
│ ├── SetupMessage.cs
│ ├── ReadRequest.cs
│ ├── S7Parameter.cs
│ ├── DataItem.cs
│ ├── ReadResponse.cs
│ ├── WriteRequest.cs
│ ├── RequestItem.cs
│ └── WriteResponse.cs
├── Protocols
│ ├── PduType.cs
│ ├── COTPParameterKinds.cs
│ ├── COTPParameter.cs
│ ├── S7Kinds.cs
│ ├── DataItemAddress.cs
│ ├── TsapAddress.cs
│ ├── TPKTCodec.cs
│ ├── TPKT.cs
│ ├── S7Server.cs
│ ├── PLCAddress.cs
│ ├── S7Message.cs
│ ├── COTP.cs
│ └── S7Client.cs
├── Models
│ ├── DataType.cs
│ ├── CpuType.cs
│ ├── VarType.cs
│ └── ErrorCode.cs
├── Drivers
│ ├── SiemensNode.cs
│ ├── SiemensParameter.cs
│ └── SiemensS7Driver.cs
├── Common
│ ├── MemoryStreamExtension.cs
│ ├── PlcException.cs
│ ├── StreamExtensions.cs
│ └── PLCExceptions.cs
├── Helper.cs
└── NewLife.Siemens.csproj
├── Test
├── Test.csproj
├── Properties
│ └── PublishProfiles
│ │ └── FolderProfile.pubxml
└── Program.cs
├── .gitignore
├── .github
└── workflows
│ ├── test.yml
│ ├── publish.yml
│ └── publish-beta.yml
├── TestClient
├── Program.cs
├── TestClient.csproj
├── FrmMain.cs
├── FrmMain.resx
└── FrmMain.Designer.cs
├── LICENSE
├── NewLife.Siemens.sln
├── .editorconfig
└── Readme.MD
/Doc/COTP1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NewLifeX/NewLife.Siemens/HEAD/Doc/COTP1.png
--------------------------------------------------------------------------------
/Doc/CPU地址区.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NewLifeX/NewLife.Siemens/HEAD/Doc/CPU地址区.png
--------------------------------------------------------------------------------
/Doc/S7.pcapng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NewLifeX/NewLife.Siemens/HEAD/Doc/S7.pcapng
--------------------------------------------------------------------------------
/Doc/leaf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NewLifeX/NewLife.Siemens/HEAD/Doc/leaf.png
--------------------------------------------------------------------------------
/Doc/rfc905.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NewLifeX/NewLife.Siemens/HEAD/Doc/rfc905.pdf
--------------------------------------------------------------------------------
/Doc/区域地址类型.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NewLifeX/NewLife.Siemens/HEAD/Doc/区域地址类型.png
--------------------------------------------------------------------------------
/Doc/COTP-PDU.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NewLifeX/NewLife.Siemens/HEAD/Doc/COTP-PDU.png
--------------------------------------------------------------------------------
/Doc/S7-Area.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NewLifeX/NewLife.Siemens/HEAD/Doc/S7-Area.png
--------------------------------------------------------------------------------
/Doc/S7-Kind.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NewLifeX/NewLife.Siemens/HEAD/Doc/S7-Kind.png
--------------------------------------------------------------------------------
/Doc/newlife.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NewLifeX/NewLife.Siemens/HEAD/Doc/newlife.snk
--------------------------------------------------------------------------------
/Doc/COTP-Setup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NewLifeX/NewLife.Siemens/HEAD/Doc/COTP-Setup.png
--------------------------------------------------------------------------------
/Doc/S7-Function.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NewLifeX/NewLife.Siemens/HEAD/Doc/S7-Function.png
--------------------------------------------------------------------------------
/Doc/S7-SyntaxID.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NewLifeX/NewLife.Siemens/HEAD/Doc/S7-SyntaxID.png
--------------------------------------------------------------------------------
/Doc/西门子S7协议介绍.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NewLifeX/NewLife.Siemens/HEAD/Doc/西门子S7协议介绍.docx
--------------------------------------------------------------------------------
/Doc/S7-ReadWrite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NewLifeX/NewLife.Siemens/HEAD/Doc/S7-ReadWrite.png
--------------------------------------------------------------------------------
/XUnitTest/BasicTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Threading;
4 | using NewLife.Caching;
5 | using NewLife.Log;
6 | using Xunit;
7 |
8 | namespace XUnitTest;
9 |
10 | public class BasicTest
11 | {
12 | }
--------------------------------------------------------------------------------
/Doc/PLC区域类型块.txt:
--------------------------------------------------------------------------------
1 | Merker: [M]任意标记变量或标志寄存器驻留在这里。
2 |
3 | Data Block: [DB] DB区域是存储设备不同功能所需数据的最常见位置,这些数据块编号为地址的一部分。
4 |
5 | Input: [I]数字和模拟输入模块值,映射到存储器。
6 |
7 | Output: [Q]类似的存储器映射输出。
8 |
9 | Counter: PLC程序使用的不同计数器的[C]值。
10 |
11 | Timer: PLC程序使用的不同定时器的[T]值。
12 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Messages/S7Functions.cs:
--------------------------------------------------------------------------------
1 | namespace NewLife.Siemens.Messages;
2 |
3 | /// S7参数类型
4 | public enum S7Functions : Byte
5 | {
6 | /// 设置通信
7 | Setup = 0xF0,
8 |
9 | /// 读取变量
10 | ReadVar = 0x04,
11 |
12 | /// 写入变量
13 | WriteVar = 0x05,
14 | }
15 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Protocols/PduType.cs:
--------------------------------------------------------------------------------
1 | namespace NewLife.Siemens.Protocols;
2 |
3 | /// PDU类型
4 | public enum PduType : Byte
5 | {
6 | /// 数据帧
7 | Data = 0xf0,
8 |
9 | /// CR连接请求帧
10 | ConnectionRequest = 0xe0,
11 |
12 | /// CC连接确认帧
13 | ConnectionConfirmed = 0xd0
14 | }
15 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Protocols/COTPParameterKinds.cs:
--------------------------------------------------------------------------------
1 | namespace NewLife.Siemens.Protocols;
2 |
3 | /// COTP参数类型
4 | public enum COTPParameterKinds : Byte
5 | {
6 | /// 数据单元大小
7 | TpduSize = 0xC0,
8 |
9 | /// 源设备号/CPU机架号
10 | SrcTsap = 0xC1,
11 |
12 | /// 目的设备号/CPU槽号
13 | DstTsap = 0xC2,
14 | }
--------------------------------------------------------------------------------
/NewLife.Siemens/Messages/IDataItems.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Serialization;
2 |
3 | namespace NewLife.Siemens.Messages;
4 |
5 | /// 支持访问数据项的接口
6 | public interface IDataItems
7 | {
8 | /// 读取数据项
9 | ///
10 | void ReadItems(Binary reader);
11 |
12 | /// 读取数据项
13 | ///
14 | void WriteItems(Binary writer);
15 | }
16 |
--------------------------------------------------------------------------------
/Test/Test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 | ..\Bin\Test
7 | false
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Protocols/COTPParameter.cs:
--------------------------------------------------------------------------------
1 | namespace NewLife.Siemens.Protocols;
2 |
3 | /// 数据项(类型+长度+数值)
4 | public class COTPParameter(COTPParameterKinds kind, Byte length, Object? value)
5 | {
6 | /// 类型
7 | public COTPParameterKinds Kind { get; set; } = kind;
8 |
9 | /// 长度
10 | public Byte Length { get; set; } = length;
11 |
12 | /// 数值
13 | public Object? Value { get; set; } = value;
14 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ################################################################################
2 | # 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。
3 | ################################################################################
4 |
5 | /.vs
6 | [Dd]ebug/
7 | [Dd]ebugPublic/
8 | [Rr]elease/
9 | [Rr]eleases/
10 | x64/
11 | x86/
12 | build/
13 | bld/
14 | [Bb]in/
15 | [Oo]bj/
16 | /packages
17 | *.user
18 | /Data
19 | /Log
20 | *.log
21 | *.htm
22 | *.nuspec
23 | *.nupkg
24 | /BinTest
25 | /BinUnitTest
26 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Protocols/S7Kinds.cs:
--------------------------------------------------------------------------------
1 | namespace NewLife.Siemens.Protocols;
2 |
3 | /// S7报文类型
4 | public enum S7Kinds : Byte
5 | {
6 | /// 作业请求。由主设备发送的请求(读写存储器、块,启动停止设备,设置通信)
7 | Job = 0x01,
8 |
9 | /// 确认响应。没有数据的简单确认
10 | Ack = 0x02,
11 |
12 | /// 确认数据响应。没有可选数据,一般响应Job请求
13 | AckData = 0x03,
14 |
15 | /// 原始协议的扩展。参数字段包含请求响应ID
16 | /// 用于编程/调试,SZL读取,安全功能,时间设置,循环读取
17 | UserData = 0x07,
18 | }
19 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Models/DataType.cs:
--------------------------------------------------------------------------------
1 | namespace NewLife.Siemens.Models;
2 |
3 | /// 存储区数据类型
4 | public enum DataType
5 | {
6 | /// 输入区
7 | Input = 129,
8 |
9 | /// 输出区
10 | Output = 130,
11 |
12 | /// 内存区 (M0, M0.0, ...)
13 | Memory = 131,
14 |
15 | /// 数据块(DB1, DB2, ...) 0x84
16 | DataBlock = 132,
17 |
18 | /// 定时器(T1, T2, ...)
19 | Timer = 29,
20 |
21 | /// 计数器 (C1, C2, ...)
22 | Counter = 28
23 | }
--------------------------------------------------------------------------------
/NewLife.Siemens/Drivers/SiemensNode.cs:
--------------------------------------------------------------------------------
1 | using NewLife.IoT;
2 | using NewLife.IoT.Drivers;
3 |
4 | namespace NewLife.Siemens.Drivers;
5 |
6 | /// 西门子PLC节点
7 | public class SiemensNode : INode
8 | {
9 | /// 主机地址
10 | public String? Address { get; set; }
11 |
12 | /// 通道
13 | public IDriver Driver { get; set; } = null!;
14 |
15 | /// 设备
16 | public IDevice? Device { get; set; }
17 |
18 | /// 参数
19 | public IDriverParameter? Parameter { get; set; }
20 |
21 | }
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches: [ '*' ]
6 | pull_request:
7 | branches: [ '*' ]
8 | workflow_dispatch:
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v4
16 | - name: Setup dotNET
17 | uses: actions/setup-dotnet@v4
18 | with:
19 | dotnet-version: |
20 | 6.x
21 | 7.x
22 | 8.x
23 | 9.x
24 | 10.x
25 | - name: Build
26 | run: dotnet build -c Release
27 |
28 | - name: Test
29 | run: dotnet test -c Release
30 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Common/MemoryStreamExtension.cs:
--------------------------------------------------------------------------------
1 | namespace NewLife.Siemens.Common
2 | {
3 | internal static class MemoryStreamExtension
4 | {
5 | ///
6 | /// Helper function to write to whole content of the given byte array to a memory stream.
7 | ///
8 | /// Writes all bytes in value from 0 to value.Length to the memory stream.
9 | ///
10 | ///
11 | ///
12 | public static void WriteByteArray(this MemoryStream stream, Byte[] value) => stream.Write(value, 0, value.Length);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Helper.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Serialization;
2 |
3 | namespace NewLife.Siemens;
4 |
5 | static class Helper
6 | {
7 | /// 是否已达到末尾
8 | ///
9 | ///
10 | public static Boolean EndOfStream(this Binary binary) => binary.Stream.Position >= binary.Stream.Length;
11 |
12 | /// 检查剩余量是否足够
13 | ///
14 | ///
15 | ///
16 | public static Boolean CheckRemain(this Binary binary, Int32 size) => binary.Stream.Position + size <= binary.Stream.Length;
17 | }
18 |
--------------------------------------------------------------------------------
/TestClient/Program.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Log;
2 |
3 | namespace TestClient
4 | {
5 | internal static class Program
6 | {
7 | ///
8 | /// The main entry point for the application.
9 | ///
10 | [STAThread]
11 | static void Main()
12 | {
13 | XTrace.UseWinForm();
14 |
15 | // To customize application configuration such as set high DPI settings or default font,
16 | // see https://aka.ms/applicationconfiguration.
17 | ApplicationConfiguration.Initialize();
18 | Application.Run(new FrmMain());
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/Test/Properties/PublishProfiles/FolderProfile.pubxml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | Release
8 | Any CPU
9 | ..\Bin\Test\netcoreapp3.1\publish\
10 | FileSystem
11 | netcoreapp3.1
12 | linux-arm
13 | false
14 | False
15 |
16 |
--------------------------------------------------------------------------------
/TestClient/TestClient.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WinExe
5 | net6.0-windows7.0
6 | ..\Bin\TestClient
7 | false
8 | disable
9 | true
10 | enable
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Messages/ReadWriteErrorCode.cs:
--------------------------------------------------------------------------------
1 | namespace NewLife.Siemens.Protocols;
2 |
3 | /// 读写错误码
4 | public enum ReadWriteErrorCode : Byte
5 | {
6 | /// 保留
7 | Reserved = 0x00,
8 |
9 | /// 硬件错误
10 | HardwareFault = 0x01,
11 |
12 | /// 禁止访问对象
13 | AccessingObjectNotAllowed = 0x03,
14 |
15 | /// 地址超出范围
16 | AddressOutOfRange = 0x05,
17 |
18 | /// 数据类型不支持
19 | DataTypeNotSupported = 0x06,
20 |
21 | /// 数据类型不一致
22 | DataTypeInconsistent = 0x07,
23 |
24 | /// 对象不存在
25 | ObjectDoesNotExist = 0x0a,
26 |
27 | /// 成功
28 | Success = 0xff
29 | }
30 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Models/CpuType.cs:
--------------------------------------------------------------------------------
1 | namespace NewLife.Siemens.Models;
2 |
3 | /// 西门子PLC常见种类
4 | public enum CpuType
5 | {
6 | ///
7 | /// S7 200
8 | ///
9 | S7200,
10 |
11 | ///
12 | /// Siemens Logo 0BA8
13 | ///
14 | Logo0BA8,
15 |
16 | ///
17 | /// S7 200 Smart
18 | ///
19 | S7200Smart,
20 |
21 | ///
22 | /// S7 300
23 | ///
24 | S7300,
25 |
26 | ///
27 | /// S7 400
28 | ///
29 | S7400,
30 |
31 | ///
32 | /// S7 1200
33 | ///
34 | S71200,
35 |
36 | ///
37 | /// S7 1500
38 | ///
39 | S71500,
40 | }
41 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: publish
2 |
3 | on:
4 | push:
5 | tags: [ v* ]
6 | workflow_dispatch:
7 |
8 | jobs:
9 | build-publish:
10 |
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v4
15 | - name: Setup dotNET
16 | uses: actions/setup-dotnet@v4
17 | with:
18 | dotnet-version: |
19 | 6.x
20 | 7.x
21 | 8.x
22 | 9.x
23 | 10.x
24 | - name: Restore
25 | run: |
26 | dotnet restore NewLife.Siemens/NewLife.Siemens.csproj
27 | - name: Build
28 | run: |
29 | dotnet pack --no-restore --version-suffix $(date "+%Y.%m%d") -c Release -o out NewLife.Siemens/NewLife.Siemens.csproj
30 | - name: Publish
31 | run: |
32 | dotnet nuget push ./out/*.nupkg --skip-duplicate --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.nugetKey }}
33 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Drivers/SiemensParameter.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 | using NewLife.IoT.Drivers;
3 | using NewLife.Siemens.Models;
4 |
5 | namespace NewLife.Siemens.Drivers;
6 |
7 | /// Omron参数
8 | public class SiemensParameter : IDriverParameter
9 | {
10 | /// 地址。例如 127.0.0.1:9600
11 | [Description("地址。例如 127.0.0.1:9600")]
12 | public String? Address { get; set; }
13 |
14 | /// 西门子PLC种类
15 | [Description("西门子PLC种类")]
16 | public CpuType CpuType { get; set; }
17 |
18 | /// 机架号。通常为0
19 | [Description("机架号。通常为0")]
20 | public Int16 Rack { get; set; }
21 |
22 | ///
23 | /// 插槽,对于S7300-S7400通常为2,对于S7-1200和S7-1500为0。如果使用外部以太网卡,则必须做相应设置
24 | ///
25 | [Description("插槽,对于S7300-S7400通常为2,对于S7-1200和S7-1500为0。如果使用外部以太网卡,则必须做相应设置")]
26 | public Int16 Slot { get; set; }
27 | }
--------------------------------------------------------------------------------
/NewLife.Siemens/Protocols/DataItemAddress.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Siemens.Models;
2 |
3 | namespace NewLife.Siemens.Protocols;
4 |
5 | ///
6 | /// Represents an area of memory in the PLC
7 | ///
8 | internal class DataItemAddress(DataType dataType, Int32 db, Int32 startByteAddress, Int32 byteLength)
9 | {
10 | ///
11 | /// Memory area to read
12 | ///
13 | public DataType DataType { get; } = dataType;
14 |
15 | ///
16 | /// Address of memory area to read (example: for DB1 this value is 1, for T45 this value is 45)
17 | ///
18 | public Int32 DB { get; } = db;
19 |
20 | ///
21 | /// Address of the first byte to read
22 | ///
23 | public Int32 StartByteAddress { get; } = startByteAddress;
24 |
25 | ///
26 | /// Length of data to read
27 | ///
28 | public Int32 ByteLength { get; } = byteLength;
29 | }
30 |
--------------------------------------------------------------------------------
/.github/workflows/publish-beta.yml:
--------------------------------------------------------------------------------
1 | name: publish-beta
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | paths:
7 | - 'NewLife.Siemens/**'
8 | workflow_dispatch:
9 |
10 | jobs:
11 | build-publish:
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v4
17 | - name: Setup dotNET
18 | uses: actions/setup-dotnet@v4
19 | with:
20 | dotnet-version: |
21 | 6.x
22 | 7.x
23 | 8.x
24 | 9.x
25 | 10.x
26 | - name: Restore
27 | run: |
28 | dotnet restore NewLife.Siemens/NewLife.Siemens.csproj
29 | - name: Build
30 | run: |
31 | dotnet pack --no-restore --version-suffix $(date "+%Y.%m%d-beta%H%M") -c Release -o out NewLife.Siemens/NewLife.Siemens.csproj
32 | - name: Publish
33 | run: |
34 | dotnet nuget push ./out/*.nupkg --skip-duplicate --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.nugetKey }}
35 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Models/VarType.cs:
--------------------------------------------------------------------------------
1 | namespace NewLife.Siemens.Models;
2 |
3 | /// 变量类型
4 | public enum VarType : Byte
5 | {
6 | /// 位。布尔型
7 | Bit = 1,
8 |
9 | /// 字节类型
10 | Byte,
11 |
12 | /// 字类型,2字节
13 | Word,
14 |
15 | /// 双字,4字节
16 | DWord,
17 |
18 | /// 整型,2字节
19 | Int,
20 |
21 | /// 长整型,4字节
22 | DInt,
23 |
24 | /// 单精度,4字节
25 | Real,
26 |
27 | /// 双精度,8字节
28 | LReal,
29 |
30 | /// 字符串(C格式)
31 | String,
32 |
33 | /// 字符串(S7格式)
34 | S7String,
35 |
36 | /// 字符串(S7宽)
37 | S7WString,
38 |
39 | /// 定时器
40 | Timer,
41 |
42 | /// 计数器
43 | Counter,
44 |
45 | /// 时间日期
46 | DateTime,
47 |
48 | /// 长时间
49 | DateTimeLong
50 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 新生命开发团队
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/XUnitTest/S7PLCTests.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Log;
2 | using NewLife.Siemens.Models;
3 | using NewLife.Siemens.Protocols;
4 | using NewLife.UnitTest;
5 | using Xunit;
6 |
7 | namespace XUnitTest;
8 |
9 | [TestCaseOrderer("NewLife.UnitTest.PriorityOrderer", "NewLife.UnitTest")]
10 | public class S7PLCTests
11 | {
12 | S7Server _server;
13 |
14 | [TestOrder(1)]
15 | [Fact]
16 | public void StartServer()
17 | {
18 | var server = new S7Server
19 | {
20 | Log = XTrace.Log,
21 | SessionLog = XTrace.Log,
22 | SocketLog = XTrace.Log,
23 |
24 | LogSend = true,
25 | LogReceive = true,
26 | };
27 |
28 | server.Start();
29 |
30 | _server = server;
31 | }
32 |
33 | //[TestOrder(2)]
34 | //[Fact]
35 | //public async void Read()
36 | //{
37 | // var s7 = new S7PLC(CpuType.S7200Smart, "127.0.0.1", 102);
38 |
39 | // await s7.OpenAsync();
40 | //}
41 |
42 | [TestOrder(3)]
43 | [Fact]
44 | public async void S7200SmartTest()
45 | {
46 | var s7 = new S7Client(CpuType.S7200, "127.0.0.1", 102);
47 |
48 | await s7.OpenAsync();
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/XUnitTest/XUnitTest.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | ..\Bin\UnitTest
6 | en
7 | false
8 | false
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | all
19 | runtime; build; native; contentfiles; analyzers; buildtransitive
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Models/ErrorCode.cs:
--------------------------------------------------------------------------------
1 | namespace NewLife.Siemens.Models;
2 |
3 | ///
4 | /// Types of error code that can be set after a function is called
5 | ///
6 | public enum ErrorCode
7 | {
8 | ///
9 | /// The function has been executed correctly
10 | ///
11 | NoError = 0,
12 |
13 | ///
14 | /// Wrong type of CPU error
15 | ///
16 | WrongCPU_Type = 1,
17 |
18 | ///
19 | /// Connection error
20 | ///
21 | ConnectionError = 2,
22 |
23 | ///
24 | /// Ip address not available
25 | ///
26 | IPAddressNotAvailable,
27 |
28 | ///
29 | /// Wrong format of the variable
30 | ///
31 | WrongVarFormat = 10,
32 |
33 | ///
34 | /// Wrong number of received bytes
35 | ///
36 | WrongNumberReceivedBytes = 11,
37 |
38 | ///
39 | /// Error on send data
40 | ///
41 | SendData = 20,
42 |
43 | ///
44 | /// Error on read data
45 | ///
46 | ReadData = 30,
47 |
48 | ///
49 | /// Error on write data
50 | ///
51 | WriteData = 50
52 | }
--------------------------------------------------------------------------------
/NewLife.Siemens/Common/PlcException.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Siemens.Models;
2 |
3 | namespace NewLife.Siemens.Common;
4 |
5 | /// PLC异常
6 | public class PlcException : Exception
7 | {
8 | /// 错误码
9 | public ErrorCode ErrorCode { get; }
10 |
11 | /// 实例化
12 | ///
13 | public PlcException(ErrorCode errorCode) : this(errorCode, $"PLC communication failed with error '{errorCode}'.")
14 | {
15 | }
16 |
17 | /// 实例化
18 | ///
19 | ///
20 | public PlcException(ErrorCode errorCode, Exception innerException) : this(errorCode, innerException.Message,
21 | innerException)
22 | {
23 | }
24 |
25 | /// 实例化
26 | ///
27 | ///
28 | public PlcException(ErrorCode errorCode, String message) : base(message) => ErrorCode = errorCode;
29 |
30 | /// 实例化
31 | ///
32 | ///
33 | ///
34 | public PlcException(ErrorCode errorCode, String message, Exception inner) : base(message, inner) => ErrorCode = errorCode;
35 | }
--------------------------------------------------------------------------------
/NewLife.Siemens/Messages/SetupMessage.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Serialization;
2 |
3 | namespace NewLife.Siemens.Messages;
4 |
5 | /// 设置通信
6 | /// 各个字段都是大端
7 | public class SetupMessage : S7Parameter
8 | {
9 | #region 属性
10 | /// Ack队列的大小(主叫)
11 | public UInt16 MaxAmqCaller { get; set; }
12 |
13 | /// Ack队列的大小(被叫)
14 | public UInt16 MaxAmqCallee { get; set; }
15 |
16 | /// PDU长度
17 | public UInt16 PduLength { get; set; }
18 | #endregion
19 |
20 | #region 构造
21 | /// 实例化
22 | public SetupMessage() => Code = S7Functions.Setup;
23 | #endregion
24 |
25 | #region 方法
26 | /// 读取
27 | ///
28 | protected override void OnRead(Binary reader)
29 | {
30 | // 读取保留字节
31 | _ = reader.ReadByte();
32 |
33 | MaxAmqCaller = reader.ReadUInt16();
34 | MaxAmqCallee = reader.ReadUInt16();
35 | PduLength = reader.ReadUInt16();
36 | }
37 |
38 | /// 写入
39 | ///
40 | protected override void OnWrite(Binary writer)
41 | {
42 | writer.WriteByte(0);
43 |
44 | writer.WriteUInt16(MaxAmqCaller);
45 | writer.WriteUInt16(MaxAmqCallee);
46 | writer.WriteUInt16(PduLength);
47 | }
48 | #endregion
49 | }
50 |
--------------------------------------------------------------------------------
/XUnitTest/PLCAddressTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using NewLife.Siemens.Models;
7 | using NewLife.Siemens.Protocols;
8 | using Xunit;
9 |
10 | namespace XUnitTest;
11 |
12 | public class PLCAddressTests
13 | {
14 | [Fact]
15 | public void Test1()
16 | {
17 | var addr = new PLCAddress("DB1.DBD32");
18 |
19 | Assert.Equal(DataType.DataBlock, addr.DataType);
20 | Assert.Equal(1, addr.DbNumber);
21 | Assert.Equal(32, addr.StartByte);
22 | Assert.Equal(VarType.DWord, addr.VarType);
23 | Assert.Equal(-1, addr.BitNumber);
24 | }
25 |
26 | [Fact]
27 | public void Test2()
28 | {
29 | var addr = new PLCAddress("DB1.DBX5.0");
30 | Assert.Equal(DataType.DataBlock, addr.DataType);
31 | Assert.Equal(1, addr.DbNumber);
32 | Assert.Equal(5, addr.StartByte);
33 | Assert.Equal(VarType.Bit, addr.VarType);
34 | Assert.Equal(0, addr.BitNumber);
35 | }
36 |
37 | [Fact]
38 | public void Test3()
39 | {
40 | var addr = new PLCAddress("DB1.STRING34.20");
41 | Assert.Equal(DataType.DataBlock, addr.DataType);
42 | Assert.Equal(1, addr.DbNumber);
43 | Assert.Equal(34, addr.StartByte);
44 | Assert.Equal(VarType.String, addr.VarType);
45 | Assert.Equal(20, addr.BitNumber);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Protocols/TsapAddress.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Siemens.Models;
2 |
3 | namespace NewLife.Siemens.Protocols;
4 |
5 | /// TSAP地址对
6 | ///
7 | /// 实例化
8 | ///
9 | ///
10 | ///
11 | public class TsapAddress(UInt16 local, UInt16 remote)
12 | {
13 | /// 本地
14 | public UInt16 Local { get; set; } = local;
15 |
16 | /// 远程
17 | public UInt16 Remote { get; set; } = remote;
18 |
19 | /// 获取默认地址对
20 | ///
21 | ///
22 | ///
23 | ///
24 | ///
25 | public static TsapAddress GetDefaultTsapPair(CpuType cpuType, Int32 rack, Int32 slot)
26 | {
27 | if (rack < 0 || rack > 0x0F) throw new ArgumentOutOfRangeException(nameof(rack));
28 | if (slot < 0 || slot > 0x0F) throw new ArgumentOutOfRangeException(nameof(slot));
29 |
30 | return cpuType switch
31 | {
32 | CpuType.S7200 => new TsapAddress(0x1000, 0x1001),
33 | CpuType.Logo0BA8 => new TsapAddress(0x0100, 0x0102),
34 | CpuType.S7200Smart => new TsapAddress(0x1000, (UInt16)(0x03 << 8 | (Byte)((rack << 5) | slot))),
35 | CpuType.S71200 or CpuType.S71500 or CpuType.S7300 or CpuType.S7400 => new TsapAddress(0x0100, (UInt16)(0x03 << 8 | (Byte)((rack << 5) | slot))),
36 | _ => throw new NotSupportedException(),
37 | };
38 | }
39 | }
--------------------------------------------------------------------------------
/NewLife.Siemens/Messages/ReadRequest.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Serialization;
2 |
3 | namespace NewLife.Siemens.Messages;
4 |
5 | /// 读取变量
6 | ///
7 | public class ReadRequest : S7Parameter
8 | {
9 | #region 属性
10 | /// 项个数
11 | public Byte ItemCount { get; set; }
12 |
13 | /// 数据项
14 | public IList Items { get; set; } = [];
15 | #endregion
16 |
17 | #region 构造
18 | /// 实例化
19 | public ReadRequest() => Code = S7Functions.ReadVar;
20 |
21 | /// 已重载。
22 | ///
23 | public override String ToString() => $"[{Code}]{Items.FirstOrDefault()}";
24 | #endregion
25 |
26 | #region 方法
27 | /// 读取
28 | ///
29 | protected override void OnRead(Binary reader)
30 | {
31 | var count = ItemCount = reader.ReadByte();
32 |
33 | var list = new List();
34 | for (var i = 0; i < count; i++)
35 | {
36 | var di = new RequestItem();
37 | di.Read(reader);
38 |
39 | list.Add(di);
40 | }
41 |
42 | Items = list;
43 | }
44 |
45 | /// 写入
46 | ///
47 | protected override void OnWrite(Binary writer)
48 | {
49 | var count = Items.Count;
50 | writer.WriteByte((Byte)count);
51 |
52 | for (var i = 0; i < count; i++)
53 | {
54 | Items[i].Writer(writer);
55 | }
56 | }
57 | #endregion
58 | }
59 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Messages/S7Parameter.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Data;
2 | using NewLife.Serialization;
3 |
4 | namespace NewLife.Siemens.Messages;
5 |
6 | /// 数据项(类型+长度+数值)
7 | public class S7Parameter
8 | {
9 | #region 属性
10 | /// 功能码
11 | public S7Functions Code { get; set; }
12 | #endregion
13 |
14 | #region 构造
15 | /// 已重载。
16 | ///
17 | public override String ToString() => $"[{Code}]";
18 | #endregion
19 |
20 | #region 方法
21 | /// 读取
22 | ///
23 | ///
24 | public Boolean Read(IPacket pk) => Read(new Binary { Stream = pk.GetStream(), IsLittleEndian = false });
25 |
26 | /// 读取
27 | ///
28 | ///
29 | public Boolean Read(Binary reader)
30 | {
31 | Code = (S7Functions)reader.ReadByte();
32 |
33 | OnRead(reader);
34 |
35 | return true;
36 | }
37 |
38 | /// 读取
39 | ///
40 | protected virtual void OnRead(Binary reader) { }
41 |
42 | /// 写入
43 | ///
44 | ///
45 | public Boolean Write(Binary writer)
46 | {
47 | writer.WriteByte((Byte)Code);
48 |
49 | OnWrite(writer);
50 |
51 | return true;
52 | }
53 |
54 | /// 写入
55 | ///
56 | protected virtual void OnWrite(Binary writer) { }
57 | #endregion
58 | }
--------------------------------------------------------------------------------
/NewLife.Siemens/Messages/DataItem.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Serialization;
2 | using NewLife.Siemens.Models;
3 | using NewLife.Siemens.Protocols;
4 |
5 | namespace NewLife.Siemens.Messages;
6 |
7 | /// 数据项
8 | public class DataItem
9 | {
10 | #region 属性
11 | /// 错误码。0xFF表示成功,写入请求时置零
12 | public ReadWriteErrorCode Code { get; set; }
13 |
14 | /// 传输数据类型。按字为04,按位为03
15 | public Byte TransportSize { get; set; }
16 |
17 | /// 数据
18 | public Byte[]? Data { get; set; }
19 | #endregion
20 |
21 | #region 构造函数
22 | /// 已重载。
23 | ///
24 | public override String ToString() => TransportSize > 0 ? $"{TransportSize}({Data.ToHex()})" : $"{Code}";
25 | #endregion
26 |
27 | #region 方法
28 | /// 读取
29 | ///
30 | public void Read(Binary reader)
31 | {
32 | if (reader.EndOfStream()) return;
33 |
34 | Code = (ReadWriteErrorCode)reader.ReadByte();
35 |
36 | // WriteResponse中只有Code
37 | if (reader.EndOfStream()) return;
38 |
39 | var b = reader.ReadByte();
40 | TransportSize = b;
41 |
42 | var len = reader.ReadUInt16();
43 | // BIT=0x03 / Byte/Word/DWord=0x04
44 | if (b == 0x04) len /= 8;
45 |
46 | Data = reader.ReadBytes(len);
47 | }
48 |
49 | /// 写入
50 | ///
51 | public void Writer(Binary writer)
52 | {
53 | writer.WriteByte((Byte)Code);
54 | writer.WriteByte((Byte)TransportSize);
55 |
56 | var len = Data?.Length ?? 0;
57 |
58 | // BIT=0x03 / Byte/Word/DWord=0x04
59 | var b = (Byte)TransportSize;
60 | if (b == 0x04) len *= 8;
61 |
62 | writer.WriteUInt16((UInt16)len);
63 |
64 | if (Data != null) writer.Write(Data, 0, Data.Length);
65 | }
66 | #endregion
67 | }
68 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Messages/ReadResponse.cs:
--------------------------------------------------------------------------------
1 | using System.Drawing;
2 | using NewLife.Serialization;
3 |
4 | namespace NewLife.Siemens.Messages;
5 |
6 | /// 读取变量响应
7 | ///
8 | public class ReadResponse : S7Parameter, IDataItems
9 | {
10 | #region 属性
11 | /// 项个数
12 | public Byte ItemCount { get; set; }
13 |
14 | /// 数据项
15 | public IList Items { get; set; } = [];
16 | #endregion
17 |
18 | #region 构造
19 | /// 实例化
20 | public ReadResponse() => Code = S7Functions.ReadVar;
21 |
22 | /// 已重载。
23 | ///
24 | public override String ToString() => $"[{Code}]{Items.FirstOrDefault()}";
25 | #endregion
26 |
27 | #region 方法
28 | /// 读取
29 | ///
30 | protected override void OnRead(Binary reader)
31 | {
32 | ItemCount = reader.ReadByte();
33 |
34 | // 数据在Data部分
35 | }
36 |
37 | /// 读取数据项
38 | ///
39 | public void ReadItems(Binary reader)
40 | {
41 | var list = new List();
42 | for (var i = 0; i < ItemCount; i++)
43 | {
44 | var di = new DataItem();
45 | di.Read(reader);
46 |
47 | list.Add(di);
48 | }
49 | Items = list.ToArray();
50 | }
51 |
52 | /// 写入
53 | ///
54 | protected override void OnWrite(Binary writer)
55 | {
56 | var count = ItemCount = (Byte)Items.Count;
57 | writer.WriteByte(count);
58 | }
59 |
60 | /// 写入数据项
61 | ///
62 | public void WriteItems(Binary writer)
63 | {
64 | for (var i = 0; i < ItemCount; i++)
65 | {
66 | Items[i].Writer(writer);
67 | }
68 | }
69 | #endregion
70 | }
71 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Protocols/TPKTCodec.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Data;
2 | using NewLife.Messaging;
3 | using NewLife.Model;
4 | using NewLife.Net.Handlers;
5 |
6 | namespace NewLife.Siemens.Protocols;
7 |
8 | /// 编码器
9 | public class TPKTCodec : MessageCodec
10 | {
11 | /// 编码
12 | ///
13 | ///
14 | ///
15 | protected override Object? Encode(IHandlerContext context, TPKT msg)
16 | {
17 | if (msg is TPKT cmd) return cmd.ToPacket();
18 |
19 | return null;
20 | }
21 |
22 | /// 解码
23 | ///
24 | ///
25 | ///
26 | protected override IList? Decode(IHandlerContext context, IPacket pk)
27 | {
28 | if (context.Owner is not IExtend ss) return null;
29 |
30 | if (ss["Codec"] is not PacketCodec pc)
31 | ss["Codec"] = pc = new PacketCodec { GetLength = p => GetLength(p, 3, 1) - 4 };
32 |
33 | var pks = pc.Parse(pk);
34 | var list = pks.Select(e => new TPKT().Read(e)).ToList();
35 |
36 | return list;
37 | }
38 |
39 | /// 连接关闭时,清空粘包编码器
40 | ///
41 | ///
42 | ///
43 | public override Boolean Close(IHandlerContext context, String reason)
44 | {
45 | if (context.Owner is IExtend ss) ss["Codec"] = null;
46 |
47 | return base.Close(context, reason);
48 | }
49 |
50 | /// 是否匹配响应
51 | ///
52 | ///
53 | ///
54 | protected override Boolean IsMatch(Object? request, Object? response)
55 | {
56 | if (request is not TPKT || response is not TPKT) return false;
57 |
58 | // 不支持链路复用,任意响应都是匹配的
59 |
60 | return true;
61 | }
62 | }
--------------------------------------------------------------------------------
/NewLife.Siemens/Common/StreamExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace NewLife.Siemens.Common;
2 |
3 | /// 数据流扩展
4 | public static class StreamExtensions
5 | {
6 | ///
7 | /// Reads bytes from the stream into the buffer until exactly the requested number of bytes (or EOF) have been read
8 | ///
9 | /// the Stream to read from
10 | /// the buffer to read into
11 | /// the offset in the buffer to read into
12 | /// the amount of bytes to read into the buffer
13 | /// returns the amount of read bytes
14 | public static Int32 ReadExact(this Stream stream, Byte[] buffer, Int32 offset, Int32 count)
15 | {
16 | var read = 0;
17 | Int32 received;
18 | do
19 | {
20 | received = stream.Read(buffer, offset + read, count - read);
21 | read += received;
22 | }
23 | while (read < count && received > 0);
24 |
25 | return read;
26 | }
27 |
28 | ///
29 | /// Reads bytes from the stream into the buffer until exactly the requested number of bytes (or EOF) have been read
30 | ///
31 | /// the Stream to read from
32 | /// the buffer to read into
33 | /// the offset in the buffer to read into
34 | /// the amount of bytes to read into the buffer
35 | ///
36 | /// returns the amount of read bytes
37 | public static async Task ReadExactAsync(this Stream stream, Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken)
38 | {
39 | var read = 0;
40 | Int32 received;
41 | do
42 | {
43 | received = await stream.ReadAsync(buffer, offset + read, count - read, cancellationToken).ConfigureAwait(false);
44 | read += received;
45 | }
46 | while (read < count && received > 0);
47 |
48 | return read;
49 | }
50 | }
--------------------------------------------------------------------------------
/NewLife.Siemens/Messages/WriteRequest.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Serialization;
2 |
3 | namespace NewLife.Siemens.Messages;
4 |
5 | /// 写入变量请求
6 | ///
7 | public class WriteRequest : S7Parameter, IDataItems
8 | {
9 | #region 属性
10 | /// 请求项
11 | public IList Items { get; set; } = [];
12 |
13 | /// 数据项
14 | public IList DataItems { get; set; } = [];
15 | #endregion
16 |
17 | #region 构造
18 | /// 实例化
19 | public WriteRequest() => Code = S7Functions.WriteVar;
20 |
21 | /// 已重载。
22 | ///
23 | public override String ToString() => $"[{Code}]{Items.FirstOrDefault()}";
24 | #endregion
25 |
26 | #region 方法
27 | /// 读取
28 | ///
29 | protected override void OnRead(Binary reader)
30 | {
31 | var count = reader.ReadByte();
32 |
33 | var list = new List();
34 | for (var i = 0; i < count; i++)
35 | {
36 | var di = new RequestItem();
37 | di.Read(reader);
38 |
39 | list.Add(di);
40 | }
41 | Items = list.ToArray();
42 | }
43 |
44 | /// 读取数据项
45 | ///
46 | public void ReadItems(Binary reader)
47 | {
48 | var list = new List();
49 | for (var i = 0; i < Items.Count; i++)
50 | {
51 | var di = new DataItem();
52 | di.Read(reader);
53 |
54 | list.Add(di);
55 | }
56 | DataItems = list.ToArray();
57 | }
58 |
59 | /// 写入
60 | ///
61 | protected override void OnWrite(Binary writer)
62 | {
63 | var count = Items.Count;
64 | writer.WriteByte((Byte)count);
65 |
66 | for (var i = 0; i < count; i++)
67 | {
68 | Items[i].Writer(writer);
69 | }
70 | }
71 |
72 | /// 写入数据项
73 | ///
74 | public void WriteItems(Binary writer)
75 | {
76 | for (var i = 0; i < Items.Count; i++)
77 | {
78 | DataItems[i].Writer(writer);
79 | }
80 | }
81 | #endregion
82 | }
83 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Messages/RequestItem.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Serialization;
2 | using NewLife.Siemens.Models;
3 |
4 | namespace NewLife.Siemens.Messages;
5 |
6 | /// 请求数据项
7 | /// 共12字节
8 | public class RequestItem
9 | {
10 | #region 属性
11 | /// 结构类型。总是0x12
12 | public Byte SpecType { get; set; } = 0x12;
13 |
14 | /// 寻址模式。任意类型S7ANY用0x10
15 | public Byte SyntaxId { get; set; }
16 |
17 | /// 传输数据类型。按字为02,按位为01
18 | public Byte TransportSize { get; set; }
19 |
20 | /// 个数
21 | public UInt16 Count { get; set; }
22 |
23 | /// 数据库地址
24 | public UInt16 DbNumber { get; set; }
25 |
26 | /// 存储区域
27 | public DataType Area { get; set; }
28 |
29 | /// 起始地址
30 | public UInt32 Address { get; set; }
31 | #endregion
32 |
33 | #region 构造函数
34 | /// 已重载。
35 | ///
36 | public override String ToString() => $"{(TransportSize > 1 ? "BYTE" : "BIT")}({Area}:{DbNumber}:{Address}, {Count})";
37 | #endregion
38 |
39 | #region 方法
40 | /// 读取
41 | ///
42 | public void Read(Binary reader)
43 | {
44 | SpecType = reader.ReadByte();
45 |
46 | var len = reader.ReadByte();
47 |
48 | SyntaxId = reader.ReadByte();
49 | TransportSize = reader.ReadByte();
50 | Count = reader.ReadUInt16();
51 | DbNumber = reader.ReadUInt16();
52 | Area = (DataType)reader.ReadByte();
53 |
54 | var buf = reader.ReadBytes(3);
55 | var buf2 = new Byte[4];
56 | buf.CopyTo(buf2, 1);
57 | Address = buf2.ToUInt32(0, false);
58 | }
59 |
60 | /// 写入
61 | ///
62 | public void Writer(Binary writer)
63 | {
64 | writer.WriteByte(SpecType);
65 |
66 | var len = 1 + 1 + 2 + 2 + 1 + 3;
67 | writer.WriteByte((Byte)len);
68 | writer.WriteByte(SyntaxId);
69 | writer.WriteByte(TransportSize);
70 | writer.Write(Count);
71 | writer.Write(DbNumber);
72 | writer.WriteByte((Byte)Area);
73 |
74 | var buf = Address.GetBytes(false);
75 | writer.Write(buf, 1, 3);
76 | }
77 | #endregion
78 | }
79 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Messages/WriteResponse.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Serialization;
2 |
3 | namespace NewLife.Siemens.Messages;
4 |
5 | /// 写入变量响应
6 | ///
7 | public class WriteResponse : S7Parameter, IDataItems
8 | {
9 | #region 属性
10 | /// 项个数
11 | public Byte ItemCount { get; set; }
12 |
13 | /// 数据项
14 | public IList Items { get; set; } = [];
15 |
16 | ///// 数据项
17 | //public IList DataItems { get; set; } = [];
18 | #endregion
19 |
20 | #region 构造
21 | /// 实例化
22 | public WriteResponse() => Code = S7Functions.WriteVar;
23 |
24 | /// 已重载。
25 | ///
26 | public override String ToString() => $"[{Code}]{Items.FirstOrDefault()}";
27 | #endregion
28 |
29 | #region 方法
30 | /// 读取
31 | ///
32 | protected override void OnRead(Binary reader)
33 | {
34 | ItemCount = reader.ReadByte();
35 |
36 | //var list = new List();
37 | //for (var i = 0; i < count; i++)
38 | //{
39 | // var di = new DataItem();
40 | // di.Read(reader);
41 |
42 | // list.Add(di);
43 | //}
44 |
45 | //Items = list;
46 | }
47 |
48 | /// 读取数据项
49 | ///
50 | public void ReadItems(Binary reader)
51 | {
52 | var list = new List();
53 | for (var i = 0; i < ItemCount; i++)
54 | {
55 | var di = new DataItem();
56 | di.Read(reader);
57 |
58 | list.Add(di);
59 | }
60 | Items = list.ToArray();
61 | }
62 |
63 | /// 写入
64 | ///
65 | protected override void OnWrite(Binary writer)
66 | {
67 | var count = ItemCount = (Byte)Items.Count;
68 | writer.WriteByte((Byte)count);
69 |
70 | //for (var i = 0; i < count; i++)
71 | //{
72 | // Items[i].Writer(writer);
73 | //}
74 | }
75 |
76 | /// 写入数据项
77 | ///
78 | public void WriteItems(Binary writer)
79 | {
80 | for (var i = 0; i < Items.Count; i++)
81 | {
82 | //Items[i].Writer(writer);
83 |
84 | // 只写Code
85 | writer.WriteByte((Byte)Items[i].Code);
86 | }
87 | }
88 | #endregion
89 | }
90 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Common/PLCExceptions.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.Serialization;
2 |
3 | namespace NewLife.Siemens.Common;
4 |
5 | internal class WrongNumberOfBytesException : Exception
6 | {
7 | public WrongNumberOfBytesException() : base()
8 | {
9 | }
10 |
11 | public WrongNumberOfBytesException(String message) : base(message)
12 | {
13 | }
14 |
15 | public WrongNumberOfBytesException(String message, Exception innerException) : base(message, innerException)
16 | {
17 | }
18 |
19 | protected WrongNumberOfBytesException(SerializationInfo info, StreamingContext context) : base(info, context)
20 | {
21 | }
22 | }
23 |
24 | internal class InvalidAddressException : Exception
25 | {
26 | public InvalidAddressException() : base()
27 | {
28 | }
29 |
30 | public InvalidAddressException(String message) : base(message)
31 | {
32 | }
33 |
34 | public InvalidAddressException(String message, Exception innerException) : base(message, innerException)
35 | {
36 | }
37 |
38 | protected InvalidAddressException(SerializationInfo info, StreamingContext context) : base(info, context)
39 | {
40 | }
41 | }
42 |
43 | internal class InvalidVariableTypeException : Exception
44 | {
45 | public InvalidVariableTypeException() : base()
46 | {
47 | }
48 |
49 | public InvalidVariableTypeException(String message) : base(message)
50 | {
51 | }
52 |
53 | public InvalidVariableTypeException(String message, Exception innerException) : base(message, innerException)
54 | {
55 | }
56 |
57 | protected InvalidVariableTypeException(SerializationInfo info, StreamingContext context) : base(info, context)
58 | {
59 | }
60 | }
61 |
62 | internal class TPKTInvalidException : Exception
63 | {
64 | public TPKTInvalidException() : base()
65 | {
66 | }
67 |
68 | public TPKTInvalidException(String message) : base(message)
69 | {
70 | }
71 |
72 | public TPKTInvalidException(String message, Exception innerException) : base(message, innerException)
73 | {
74 | }
75 |
76 | protected TPKTInvalidException(SerializationInfo info, StreamingContext context) : base(info, context)
77 | {
78 | }
79 | }
80 |
81 | internal class TPDUInvalidException : Exception
82 | {
83 | public TPDUInvalidException() : base()
84 | {
85 | }
86 |
87 | public TPDUInvalidException(String message) : base(message)
88 | {
89 | }
90 |
91 | public TPDUInvalidException(String message, Exception innerException) : base(message, innerException)
92 | {
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/NewLife.Siemens/NewLife.Siemens.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netstandard2.1;netstandard2.0;net461;net45
4 | 西门子PLC协议
5 | 西门子PLC协议
6 | 新生命开发团队
7 | ©2002-2025 新生命开发团队
8 | 1.1
9 | $([System.DateTime]::Now.ToString(`yyyy.MMdd`))
10 | $(VersionPrefix).$(VersionSuffix)
11 | $(Version)
12 | $(VersionPrefix).*
13 | false
14 | ..\Bin
15 | True
16 | enable
17 | enable
18 | latest
19 | True
20 | ..\Doc\newlife.snk
21 |
22 |
23 |
24 | $(AssemblyName)
25 | $(Company)
26 | https://newlifex.com/iot
27 | leaf.png
28 | https://github.com/NewLifeX/NewLife.Siemens
29 | git
30 | 物联网;IoT;边缘计算;Edge;新生命团队;NewLife;PLC;$(AssemblyName)
31 | 重构S7协议架构
32 | MIT
33 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
34 | true
35 | true
36 | true
37 | snupkg
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | all
56 | runtime; build; native; contentfiles; analyzers; buildtransitive
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/Test/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using NewLife;
4 | using NewLife.IoT;
5 | using NewLife.IoT.Drivers;
6 | using NewLife.IoT.ThingModels;
7 | using NewLife.Log;
8 | using NewLife.Siemens.Drivers;
9 | using NewLife.Siemens.Models;
10 | using NewLife.Siemens.Protocols;
11 |
12 | XTrace.UseConsole();
13 |
14 | var server = new S7Server
15 | {
16 | Log = XTrace.Log,
17 | SessionLog = XTrace.Log,
18 | SocketLog = XTrace.Log,
19 | LogSend = true,
20 | LogReceive = true
21 | };
22 |
23 | server.Start();
24 |
25 | Console.WriteLine("服务端地址默认为:127.0.0.1:102,保持默认请回车开始连接,否则请输入服务端地址:");
26 | var address = Console.ReadLine();
27 |
28 | var point = new Point
29 | {
30 | Name = "污泥泵停止时间",
31 | Address = "DB1.DBD60", // "M100",
32 | Type = "long",
33 | Length = 4 //data.Length
34 | };
35 |
36 | var i = point.GetNetType();
37 |
38 |
39 | if (address == null || address == "") address = "127.0.0.1:102";
40 |
41 | var driver = new SiemensS7Driver();
42 | var pm = new SiemensParameter
43 | {
44 | Address = address,
45 | CpuType = CpuType.S7200Smart,
46 | Rack = 0,
47 | Slot = 2,
48 | };
49 | var node = driver.Open(null, pm);
50 |
51 | //// 测试打开两个通道
52 | //node = driver.Open(null, pm);
53 |
54 | Console.WriteLine($"连接成功=>{address}!");
55 |
56 | Console.WriteLine($"读写模式输入1,循环读输入2:");
57 |
58 | var mode = Console.ReadLine();
59 |
60 | var str = "0";
61 |
62 | if (mode == "1")
63 | {
64 | Console.WriteLine("请输入整数值,按q退出:");
65 | str = Console.ReadLine();
66 | }
67 |
68 |
69 |
70 | //var point2 = new Point
71 | //{
72 | // Name = "test2",
73 | // Address = "DB1.DBX1128.0", // "M100",
74 | // Type = "Int32",
75 | // Length = 4 //data.Length
76 | //};
77 |
78 | do
79 | {
80 | if (mode == "1")
81 | {
82 | // 写入
83 | var data = BitConverter.GetBytes(Int32.Parse(str));
84 |
85 | var res = driver.Write(node, point, data);
86 |
87 | // 读取
88 | var dic = driver.Read(node, new[] { point });
89 | var data1 = dic[point.Name] as Byte[];
90 |
91 | Console.WriteLine($"读取结果:{BitConverter.ToInt32(data1)}");
92 | Console.WriteLine($"");
93 | Console.WriteLine("请输入整数值,按q退出:");
94 | }
95 | else
96 | {
97 | // 读取
98 | var dic = driver.Read(node, new[] { point });
99 | var data1 = dic[point.Name] as Byte[];
100 | //var res = BitConverter.ToInt32(data1);
101 | var res = data1.Swap(true, false).ToInt();
102 | Console.WriteLine($"读取结果:{res}");
103 | Console.WriteLine($"");
104 | Thread.Sleep(1000);
105 | }
106 | } while (
107 | (mode == "1" && (str = Console.ReadLine()) != "q")
108 | || mode == "2");
109 |
110 | // 断开连接
111 | //driver.Close(node);
112 | driver.Close(node);
113 |
114 |
115 | public class Point : IPoint
116 | {
117 | public String Name { get; set; }
118 | public String Address { get; set; }
119 | public String Type { get; set; }
120 | public Int32 Length { get; set; }
121 | }
--------------------------------------------------------------------------------
/NewLife.Siemens.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.32112.339
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test", "Test\Test.csproj", "{582E00A2-4FA4-41DC-84AD-EE264A9FF4D8}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnitTest", "XUnitTest\XUnitTest.csproj", "{08A39462-0531-45AB-ACBB-03F62AF4400F}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{719C113A-6804-4150-8473-BF889F06DFD6}"
11 | ProjectSection(SolutionItems) = preProject
12 | .editorconfig = .editorconfig
13 | .github\workflows\publish-beta.yml = .github\workflows\publish-beta.yml
14 | .github\workflows\publish.yml = .github\workflows\publish.yml
15 | Readme.MD = Readme.MD
16 | .github\workflows\test.yml = .github\workflows\test.yml
17 | EndProjectSection
18 | EndProject
19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NewLife.Siemens", "NewLife.Siemens\NewLife.Siemens.csproj", "{2B4793CE-80F1-4D8C-8287-ECE1E031CE41}"
20 | EndProject
21 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestClient", "TestClient\TestClient.csproj", "{12D2CB8E-8641-475B-8EA8-2B57D589BCE9}"
22 | EndProject
23 | Global
24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
25 | Debug|Any CPU = Debug|Any CPU
26 | Release|Any CPU = Release|Any CPU
27 | EndGlobalSection
28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
29 | {582E00A2-4FA4-41DC-84AD-EE264A9FF4D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
30 | {582E00A2-4FA4-41DC-84AD-EE264A9FF4D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
31 | {582E00A2-4FA4-41DC-84AD-EE264A9FF4D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
32 | {582E00A2-4FA4-41DC-84AD-EE264A9FF4D8}.Release|Any CPU.Build.0 = Release|Any CPU
33 | {08A39462-0531-45AB-ACBB-03F62AF4400F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
34 | {08A39462-0531-45AB-ACBB-03F62AF4400F}.Debug|Any CPU.Build.0 = Debug|Any CPU
35 | {08A39462-0531-45AB-ACBB-03F62AF4400F}.Release|Any CPU.ActiveCfg = Release|Any CPU
36 | {08A39462-0531-45AB-ACBB-03F62AF4400F}.Release|Any CPU.Build.0 = Release|Any CPU
37 | {2B4793CE-80F1-4D8C-8287-ECE1E031CE41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38 | {2B4793CE-80F1-4D8C-8287-ECE1E031CE41}.Debug|Any CPU.Build.0 = Debug|Any CPU
39 | {2B4793CE-80F1-4D8C-8287-ECE1E031CE41}.Release|Any CPU.ActiveCfg = Release|Any CPU
40 | {2B4793CE-80F1-4D8C-8287-ECE1E031CE41}.Release|Any CPU.Build.0 = Release|Any CPU
41 | {12D2CB8E-8641-475B-8EA8-2B57D589BCE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
42 | {12D2CB8E-8641-475B-8EA8-2B57D589BCE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
43 | {12D2CB8E-8641-475B-8EA8-2B57D589BCE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
44 | {12D2CB8E-8641-475B-8EA8-2B57D589BCE9}.Release|Any CPU.Build.0 = Release|Any CPU
45 | EndGlobalSection
46 | GlobalSection(SolutionProperties) = preSolution
47 | HideSolutionNode = FALSE
48 | EndGlobalSection
49 | GlobalSection(ExtensibilityGlobals) = postSolution
50 | SolutionGuid = {323831A1-A95B-40AB-B9AD-36A0BC10C2CB}
51 | EndGlobalSection
52 | EndGlobal
53 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Protocols/TPKT.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Data;
2 |
3 | namespace NewLife.Siemens.Protocols;
4 |
5 | /// ISO Transport Service ontop of the TCP
6 | ///
7 | /// 通过TCP的传输服务。介于TCP和COTP之间。属于传输服务类的协议,它为上层的COTP和下层TCP进行了过渡。
8 | /// 功能为在COTP和TCP之间建立桥梁,其内容包含了上层协议数据包的长度。
9 | /// 一般与COTP一起发送,当作Header段。
10 | /// 我们常用的RDP协议(remote desktop protocol,windows的远程桌面协议)也是基于TPKT的,TPKT的默认TCP端口为102(RDP为3389)
11 | ///
12 | public class TPKT
13 | {
14 | #region 属性
15 | /// 版本
16 | public Byte Version { get; set; } = 3;
17 |
18 | /// 保留
19 | public Byte Reserved { get; set; }
20 |
21 | /// 长度。包括当前TPKT头和后续数据
22 | public UInt16 Length { get; set; }
23 |
24 | /// 数据
25 | public IPacket? Data { get; set; }
26 | #endregion
27 |
28 | #region 读写
29 | /// 解析头部数据
30 | ///
31 | public TPKT ReadHeader(Byte[] buf)
32 | {
33 | Version = buf[0];
34 | Reserved = buf[1];
35 | Length = buf.ToUInt16(2, false);
36 |
37 | return this;
38 | }
39 |
40 | /// 序列化头部数据
41 | ///
42 | public void WriteHeader(Byte[] buf)
43 | {
44 | buf[0] = Version;
45 | buf[1] = Reserved;
46 | buf[2] = (Byte)(Length >> 8);
47 | buf[3] = (Byte)(Length & 0xFF);
48 | }
49 |
50 | /// 解析头部以及负载数据
51 | ///
52 | public TPKT Read(IPacket pk)
53 | {
54 | var buf = pk.ReadBytes(0, 4);
55 | ReadHeader(buf);
56 |
57 | if (pk.Total > 4) Data = pk.Slice(4, Length - 4);
58 |
59 | return this;
60 | }
61 |
62 | /// 序列化消息,包括头部和负载数据
63 | ///
64 | public IPacket ToPacket()
65 | {
66 | // 根据数据长度计算总长度
67 | Length = (UInt16)(4 + Data?.Total ?? 0);
68 |
69 | var buf = new Byte[4];
70 | WriteHeader(buf);
71 |
72 | return new ArrayPacket(buf) { Next = Data };
73 | //var pk = new Packet(buf);
74 | //if (Data != null) pk.Append(Data);
75 |
76 | //return pk;
77 | }
78 | #endregion
79 |
80 | /// 读取TPKT数据
81 | ///
82 | ///
83 | ///
84 | public static async Task ReadAsync(Stream stream, CancellationToken cancellationToken)
85 | {
86 | // 读取4字节头部
87 | var buf = new Byte[4];
88 | var len = await stream.ReadAsync(buf, 0, 4, cancellationToken).ConfigureAwait(false);
89 | if (len < 4) throw new InvalidDataException("TPKT is incomplete / invalid");
90 |
91 | var tpkt = new TPKT();
92 | tpkt.ReadHeader(buf);
93 |
94 | // 根据长度读取数据
95 | var data = new OwnerPacket(tpkt.Length - 4);
96 | len = await stream.ReadAsync(data.Buffer, 0, data.Length, cancellationToken).ConfigureAwait(false);
97 | if (len < data.Length)
98 | throw new InvalidDataException("TPKT payload incomplete / invalid");
99 |
100 | return data;
101 | }
102 | }
--------------------------------------------------------------------------------
/TestClient/FrmMain.cs:
--------------------------------------------------------------------------------
1 | using NewLife;
2 | using NewLife.IoT.ThingModels;
3 | using NewLife.Log;
4 | using NewLife.Serialization;
5 | using NewLife.Siemens.Drivers;
6 | using NewLife.Siemens.Models;
7 |
8 | namespace TestClient;
9 |
10 | public partial class FrmMain : Form
11 | {
12 | private SiemensS7Driver _driver;
13 | private SiemensNode _node;
14 |
15 | public FrmMain()
16 | {
17 | InitializeComponent();
18 | }
19 |
20 | private void FrmMain_Load(object sender, EventArgs e)
21 | {
22 | rtb_content.UseWinFormControl();
23 |
24 | XTrace.Log.Level = LogLevel.All;
25 | }
26 |
27 | private void button1_Click(object sender, EventArgs e)
28 | {
29 | try
30 | {
31 | if (btn_conn.Text == "连接")
32 | {
33 | var driver = new SiemensS7Driver
34 | {
35 | Log = XTrace.Log
36 | };
37 |
38 | var pm = new SiemensParameter
39 | {
40 | Address = $"{tb_address.Text}:{tb_port.Text}",
41 | CpuType = CpuType.S7200Smart,
42 | Rack = 0,
43 | Slot = 0,
44 | };
45 |
46 | XTrace.WriteLine("开始连接PLC……");
47 | XTrace.WriteLine(pm.ToJson(true));
48 |
49 | _node = driver.Open(null, pm) as SiemensNode;
50 | if (_node != null)
51 | {
52 | _driver = driver;
53 |
54 | XTrace.WriteLine("连接成功!");
55 |
56 | btn_conn.Text = "断开";
57 | }
58 | else
59 | {
60 | XTrace.WriteLine("连接失败!");
61 |
62 | btn_conn.Text = "连接";
63 | }
64 | }
65 | else
66 | {
67 | _driver.Close(_node);
68 | _driver.Dispose();
69 | _driver = null;
70 |
71 | XTrace.WriteLine("断开链接!");
72 |
73 | btn_conn.Text = "连接";
74 | }
75 | }
76 | catch (Exception ex)
77 | {
78 | XTrace.WriteException(ex);
79 | }
80 | }
81 |
82 | private void btn_write_Click(object sender, EventArgs e)
83 | {
84 | var pointAdd = tb_pointAddress.Text;
85 | var value = tb_value.Text.ToInt();
86 | var length = tb_length.Text.ToInt();
87 | var type = tb_type.Text;
88 |
89 | var point = new PointModel
90 | {
91 | Name = "污泥泵停止时间",
92 | Address = pointAdd, // "M100",
93 | Type = type,
94 | Length = length //data.Length
95 | };
96 |
97 | try
98 | {
99 | XTrace.WriteLine($"写入点位:{pointAdd}, 类型:{type}, 长度:{length},值:{value}");
100 |
101 | var rs = _driver.Write(_node, point, value);
102 |
103 | XTrace.WriteLine(rs.ToJson(true));
104 | }
105 | catch (Exception ex)
106 | {
107 | XTrace.WriteException(ex);
108 | }
109 | }
110 |
111 | private void btn_read_Click(object sender, EventArgs e)
112 | {
113 | var pointAdd = tb_pointAddress.Text;
114 | var length = tb_length.Text.ToInt();
115 | var type = tb_type.Text;
116 |
117 | var point = new PointModel
118 | {
119 | Name = "污泥泵停止时间",
120 | Address = pointAdd, // "M100",
121 | Type = type,
122 | Length = length //data.Length
123 | };
124 |
125 | try
126 | {
127 | XTrace.WriteLine($"读取点位:{pointAdd}, 类型:{type}, 长度:{length}");
128 |
129 | // 读取
130 | var dic = _driver.Read(_node, new[] { point });
131 |
132 | XTrace.WriteLine(dic.ToJson(true));
133 | }
134 | catch (Exception ex)
135 | {
136 | XTrace.WriteException(ex);
137 | }
138 | }
139 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome:http://EditorConfig.org
2 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference
3 |
4 | # top-most EditorConfig file
5 | root = true
6 |
7 | # Don't use tabs for indentation.
8 | [*]
9 | indent_style = space
10 | # (Please don't specify an indent_size here; that has too many unintended consequences.)
11 |
12 | # Code files
13 | [*.{cs,csx,vb,vbx}]
14 | indent_size = 4
15 | insert_final_newline = false
16 | charset = utf-8-bom
17 | end_of_line = crlf
18 |
19 | # Xml project files
20 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
21 | indent_size = 2
22 |
23 | # Xml config files
24 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
25 | indent_size = 2
26 |
27 | # JSON files
28 | [*.json]
29 | indent_size = 2
30 |
31 | # Dotnet code style settings:
32 | [*.{cs,vb}]
33 | # Sort using and Import directives with System.* appearing first
34 | dotnet_sort_system_directives_first = true
35 |
36 | csharp_indent_case_contents = true
37 | csharp_indent_switch_labels = true
38 | csharp_indent_labels = flush_left
39 |
40 | #csharp_space_after_cast = true
41 | #csharp_space_after_keywords_in_control_flow_statements = true
42 | #csharp_space_between_method_declaration_parameter_list_parentheses = true
43 | #csharp_space_between_method_call_parameter_list_parentheses = true
44 | #csharp_space_between_parentheses = control_flow_statements, type_casts
45 |
46 | # 单行放置代码
47 | csharp_preserve_single_line_statements = true
48 | csharp_preserve_single_line_blocks = true
49 |
50 | # Avoid "this." and "Me." if not necessary
51 | dotnet_style_qualification_for_field = false:warning
52 | dotnet_style_qualification_for_property = false:warning
53 | dotnet_style_qualification_for_method = false:warning
54 | dotnet_style_qualification_for_event = false:warning
55 |
56 | # Use language keywords instead of framework type names for type references
57 | dotnet_style_predefined_type_for_locals_parameters_members = false:suggestion
58 | dotnet_style_predefined_type_for_member_access = false:suggestion
59 | #dotnet_style_require_accessibility_modifiers = for_non_interface_members:none/always:suggestion
60 |
61 | # Suggest more modern language features when available
62 | dotnet_style_object_initializer = true:suggestion
63 | dotnet_style_collection_initializer = true:suggestion
64 | dotnet_style_coalesce_expression = true:suggestion
65 | dotnet_style_null_propagation = true:suggestion
66 | dotnet_style_explicit_tuple_names = true:suggestion
67 | dotnet_style_prefer_inferred_tuple_names = true:suggestion
68 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
69 |
70 | # CSharp code style settings:
71 | [*.cs]
72 | # Prefer "var" everywhere
73 | csharp_style_var_for_built_in_types = true:warning
74 | csharp_style_var_when_type_is_apparent = true:warning
75 | csharp_style_var_elsewhere = true:warning
76 |
77 | # Prefer method-like constructs to have a block body
78 | csharp_style_expression_bodied_methods = when_on_single_line:suggestion
79 | csharp_style_expression_bodied_constructors = when_on_single_line:suggestion
80 | csharp_style_expression_bodied_operators = when_on_single_line:suggestion
81 |
82 | # Prefer property-like constructs to have an expression-body
83 | csharp_style_expression_bodied_properties = true:suggestion
84 | csharp_style_expression_bodied_indexers = true:suggestion
85 | #csharp_style_expression_bodied_accessors = true:suggestion
86 |
87 | # Suggest more modern language features when available
88 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
89 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
90 | csharp_style_inlined_variable_declaration = true:suggestion
91 |
92 | csharp_prefer_simple_default_expression = true:suggestion
93 | csharp_style_deconstructed_variable_declaration = true:suggestion
94 | csharp_style_pattern_local_over_anonymous_function = true:suggestion
95 |
96 | csharp_style_throw_expression = true:suggestion
97 | csharp_style_conditional_delegate_call = true:suggestion
98 |
99 | # 单行不需要大括号
100 | csharp_prefer_braces = false:suggestion
101 |
102 | # Newline settings
103 | csharp_new_line_before_open_brace = all
104 | csharp_new_line_before_else = true
105 | csharp_new_line_before_catch = true
106 | csharp_new_line_before_finally = true
107 | csharp_new_line_before_members_in_object_initializers = true
108 | csharp_new_line_before_members_in_anonymous_types = true
109 | csharp_new_line_between_query_expression_clauses = true
110 |
111 | [*.md]
112 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/Readme.MD:
--------------------------------------------------------------------------------
1 | # NewLife.Siemens 西门子PLC
2 |
3 | 
4 | 
5 | 
6 | 
7 | 
8 |
9 | 西门子PLC协议
10 |
11 | 源码: https://github.com/NewLifeX/NewLife.Siemens
12 | Nuget:NewLife.Siemens
13 |
14 |
15 | ## 新生命项目矩阵
16 | 各项目默认支持net9.0/netstandard2.1/netstandard2.0/net4.62/net4.5,旧版(2024.0801)支持net4.0/net2.0
17 |
18 | | 项目 | 年份 | 说明 |
19 | | :--------------------------------------------------------------: | :---: | ------------------------------------------------------------------------------------------- |
20 | | 基础组件 | | 支撑其它中间件以及产品项目 |
21 | | [NewLife.Core](https://github.com/NewLifeX/X) | 2002 | 核心库,日志、配置、缓存、网络、序列化、APM性能追踪 |
22 | | [NewLife.XCode](https://github.com/NewLifeX/NewLife.XCode) | 2005 | 大数据中间件,单表百亿级,MySql/SQLite/SqlServer/Oracle/PostgreSql/达梦,自动分表,读写分离 |
23 | | [NewLife.Net](https://github.com/NewLifeX/NewLife.Net) | 2005 | 网络库,单机千万级吞吐率(2266万tps),单机百万级连接(400万Tcp长连接) |
24 | | [NewLife.Remoting](https://github.com/NewLifeX/NewLife.Remoting) | 2011 | 协议通信库,提供CS应用通信框架,支持Http/RPC通信框架,高吞吐,物联网设备低开销易接入 |
25 | | [NewLife.Cube](https://github.com/NewLifeX/NewLife.Cube) | 2010 | 魔方快速开发平台,集成了用户权限、SSO登录、OAuth服务端等,单表100亿级项目验证 |
26 | | [NewLife.Agent](https://github.com/NewLifeX/NewLife.Agent) | 2008 | 服务管理组件,把应用安装成为操作系统守护进程,Windows服务、Linux的Systemd |
27 | | [NewLife.Zero](https://github.com/NewLifeX/NewLife.Zero) | 2020 | Zero零代脚手架,基于NewLife组件生态的项目模板NewLife.Templates,Web、WebApi、Service |
28 | | 中间件 | | 对接知名中间件平台 |
29 | | [NewLife.Redis](https://github.com/NewLifeX/NewLife.Redis) | 2017 | Redis客户端,微秒级延迟,百万级吞吐,丰富的消息队列,百亿级数据量项目验证 |
30 | | [NewLife.RocketMQ](https://github.com/NewLifeX/NewLife.RocketMQ) | 2018 | RocketMQ纯托管客户端,支持Apache RocketMQ和阿里云消息队列,十亿级项目验 |
31 | | [NewLife.MQTT](https://github.com/NewLifeX/NewLife.MQTT) | 2019 | 物联网消息协议,MqttClient/MqttServer,客户端支持阿里云物联网 |
32 | | [NewLife.IoT](https://github.com/NewLifeX/NewLife.IoT) | 2022 | IoT标准库,定义物联网领域的各种通信协议标准规范 |
33 | | [NewLife.Modbus](https://github.com/NewLifeX/NewLife.Modbus) | 2022 | ModbusTcp/ModbusRTU/ModbusASCII,基于IoT标准库实现,支持ZeroIoT平台和IoTEdge网关 |
34 | | [NewLife.Siemens](https://github.com/NewLifeX/NewLife.Siemens) | 2022 | 西门子PLC协议,基于IoT标准库实现,支持IoT平台和IoTEdge |
35 | | [NewLife.Map](https://github.com/NewLifeX/NewLife.Map) | 2022 | 地图组件库,封装百度地图、高德地图、腾讯地图、天地图 |
36 | | [NewLife.Audio](https://github.com/NewLifeX/NewLife.Audio) | 2023 | 音频编解码库,PCM/ADPCMA/G711A/G722U/WAV/AAC |
37 | | 产品平台 | | 产品平台级,编译部署即用,个性化自定义 |
38 | | [Stardust](https://github.com/NewLifeX/Stardust) | 2018 | 星尘,分布式服务平台,节点管理、APM监控中心、配置中心、注册中心、发布中心 |
39 | | [AntJob](https://github.com/NewLifeX/AntJob) | 2019 | 蚂蚁调度,分布式大数据计算平台(实时/离线),蚂蚁搬家分片思想,万亿级数据量项目验证 |
40 | | [NewLife.ERP](https://github.com/NewLifeX/NewLife.ERP) | 2021 | 企业ERP,产品管理、客户管理、销售管理、供应商管理 |
41 | | [CrazyCoder](https://github.com/NewLifeX/XCoder) | 2006 | 码神工具,众多开发者工具,网络、串口、加解密、正则表达式、Modbus、MQTT |
42 | | [EasyIO](https://github.com/NewLifeX/EasyIO) | 2023 | 简易文件存储,支持分布式系统中文件集中存储。 |
43 | | [XProxy](https://github.com/NewLifeX/XProxy) | 2005 | 产品级反向代理,NAT代理、Http代理 |
44 | | [HttpMeter](https://github.com/NewLifeX/HttpMeter) | 2022 | Http压力测试工具 |
45 | | [GitCandy](https://github.com/NewLifeX/GitCandy) | 2015 | Git源代码管理系统 |
46 | | [SmartOS](https://github.com/NewLifeX/SmartOS) | 2014 | 嵌入式操作系统,完全独立自主,支持ARM Cortex-M芯片架构 |
47 | | [SmartA2](https://github.com/NewLifeX/SmartA2) | 2019 | 嵌入式工业计算机,物联网边缘网关,高性能.NET8主机,应用于工业、农业、交通、医疗 |
48 | | FIoT物联网平台 | 2020 | 物联网整体解决方案,建筑、环保、农业,软硬件及大数据分析一体化,单机十万级点位项目验证 |
49 | | UWB高精度室内定位 | 2020 | 厘米级(10~20cm)高精度室内定位,软硬件一体化,与其它系统联动,大型展厅项目验证 |
50 |
51 |
52 |
53 | ## 新生命开发团队
54 | 
55 |
56 | 新生命团队(NewLife)成立于2002年,是新时代物联网行业解决方案提供者,致力于提供软硬件应用方案咨询、系统架构规划与开发服务。
57 | 团队主导的80多个开源项目已被广泛应用于各行业,Nuget累计下载量高达400余万次。
58 | 团队开发的大数据中间件NewLife.XCode、蚂蚁调度计算平台AntJob、星尘分布式平台Stardust、缓存队列组件NewLife.Redis以及物联网平台FIoT,均成功应用于电力、高校、互联网、电信、交通、物流、工控、医疗、文博等行业,为客户提供了大量先进、可靠、安全、高质量、易扩展的产品和系统集成服务。
59 |
60 | 我们将不断通过服务的持续改进,成为客户长期信赖的合作伙伴,通过不断的创新和发展,成为国内优秀的IoT服务供应商。
61 |
62 | `新生命团队始于2002年,部分开源项目具有20年以上漫长历史,源码库保留有2010年以来所有修改记录`
63 | 网站:https://newlifex.com
64 | 开源:https://github.com/newlifex
65 | QQ群:1600800/1600838
66 | 微信公众号:
67 | 
68 |
--------------------------------------------------------------------------------
/TestClient/FrmMain.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Protocols/S7Server.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using NewLife.Log;
3 | using NewLife.Net;
4 | using NewLife.Security;
5 | using NewLife.Serialization;
6 | using NewLife.Siemens.Messages;
7 | using NewLife.Siemens.Models;
8 |
9 | namespace NewLife.Siemens.Protocols;
10 |
11 | /// S7服务端。用于仿真
12 | public class S7Server : NetServer
13 | {
14 | /// 实例化
15 | public S7Server()
16 | {
17 | Port = 102;
18 | ProtocolType = NetType.Tcp;
19 |
20 | Add(new TPKTCodec());
21 | }
22 | }
23 |
24 | /// S7连接会话
25 | public class S7Session : NetSession
26 | {
27 | private Boolean _logined;
28 |
29 | /// 客户端连接时
30 | protected override void OnConnected()
31 | {
32 | WriteLog("S7连接:{0}", Remote);
33 |
34 | base.OnConnected();
35 | }
36 |
37 | /// 客户端断开连接时
38 | ///
39 | protected override void OnDisconnected(String reason)
40 | {
41 | WriteLog("S7断开:{0} {1}", Remote, reason);
42 |
43 | base.OnDisconnected(reason);
44 | }
45 |
46 | /// 报错时
47 | ///
48 | ///
49 | protected override void OnError(Object? sender, ExceptionEventArgs e)
50 | {
51 | WriteLog("S7错误:{0}", e.Exception.Message);
52 |
53 | base.OnError(sender, e);
54 | }
55 |
56 | /// 收到数据时
57 | ///
58 | protected override void OnReceive(ReceivedEventArgs e)
59 | {
60 | if (e.Message is not TPKT tpkt || tpkt.Data == null) return;
61 |
62 | var cotp = new COTP();
63 | if (cotp.Read(tpkt.Data))
64 | {
65 | WriteLog("<={0}", cotp.ToString());
66 |
67 | switch (cotp.Type)
68 | {
69 | case PduType.Data:
70 | if (!_logined)
71 | OnConnectionRequest(cotp);
72 | else
73 | OnData(cotp);
74 | break;
75 | case PduType.ConnectionRequest:
76 | OnConnectionRequest(cotp);
77 | break;
78 | //case PduType.ConnectionConfirmed:
79 | // break;
80 | default:
81 | break;
82 | }
83 | }
84 |
85 | base.OnReceive(e);
86 | }
87 |
88 | void OnConnectionRequest(COTP cotp)
89 | {
90 | var rs = new COTP
91 | {
92 | Type = PduType.ConnectionConfirmed,
93 | Destination = cotp.Source,
94 | Source = cotp.Destination,
95 | Number = cotp.Number,
96 | };
97 |
98 | Send(rs.ToPacket(true));
99 |
100 | _logined = true;
101 | }
102 |
103 | void OnData(COTP cotp)
104 | {
105 | if (cotp.Data == null) return;
106 |
107 | var msg = new S7Message();
108 | if (!msg.Read(cotp.Data)) return;
109 |
110 | switch (msg.Kind)
111 | {
112 | case S7Kinds.Job:
113 | {
114 | var rs = new S7Message
115 | {
116 | Kind = S7Kinds.AckData,
117 | Sequence = msg.Sequence,
118 | };
119 |
120 | var pm = msg.Parameters.FirstOrDefault();
121 | switch (pm.Code)
122 | {
123 | case S7Functions.ReadVar:
124 | var pm2 = OnRead(pm as ReadRequest);
125 | if (pm2 != null)
126 | rs.Parameters.Add(pm2);
127 | break;
128 | case S7Functions.WriteVar:
129 | var pm3 = OnWrite(pm as WriteRequest);
130 | if (pm3 != null)
131 | rs.Parameters.Add(pm3);
132 | break;
133 | case S7Functions.Setup:
134 | default:
135 | foreach (var item in msg.Parameters)
136 | {
137 | rs.Parameters.Add(item);
138 | }
139 | break;
140 | }
141 |
142 | Send(rs.ToCOTP().ToPacket(true));
143 | }
144 | break;
145 | case S7Kinds.Ack:
146 | break;
147 | case S7Kinds.AckData:
148 | break;
149 | case S7Kinds.UserData:
150 | break;
151 | default:
152 | break;
153 | }
154 | }
155 |
156 | ReadResponse? OnRead(ReadRequest? request)
157 | {
158 | if (request == null) return null;
159 |
160 | WriteLog("读取:{0}", request.ToJson());
161 |
162 | var num = Rand.Next(0, 10000);
163 | WriteLog("数值:{0}", num);
164 |
165 | var di = new DataItem
166 | {
167 | Code = ReadWriteErrorCode.Success,
168 | TransportSize = 0x04,
169 | Data = num.GetBytes(false)
170 | };
171 |
172 | var rs = new ReadResponse();
173 | rs.Items.Add(di);
174 |
175 | return rs;
176 | }
177 |
178 | WriteResponse? OnWrite(WriteRequest? request)
179 | {
180 | if (request == null) return null;
181 |
182 | WriteLog("写入:{0}", request.ToJson());
183 |
184 | var ri = request.DataItems.FirstOrDefault();
185 | if (ri != null && ri.Data != null)
186 | {
187 | Object num = ri.Data.Length switch
188 | {
189 | 1 => ri.Data[0],
190 | 2 => ri.Data.ToUInt16(0, false),
191 | 4 => ri.Data.ToUInt32(0, false),
192 | _ => ri.Data.ToHex(),
193 | };
194 | WriteLog("数值:{0}", num);
195 | }
196 |
197 | var di = new DataItem
198 | {
199 | Code = ReadWriteErrorCode.Success,
200 | };
201 |
202 | var rs = new WriteResponse();
203 | rs.Items.Add(di);
204 |
205 | return rs;
206 | }
207 | }
--------------------------------------------------------------------------------
/XUnitTest/COTPTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using NewLife;
3 | using NewLife.Data;
4 | using NewLife.Siemens.Protocols;
5 | using Xunit;
6 |
7 | namespace XUnitTest;
8 |
9 | public class COTPTests
10 | {
11 | [Fact]
12 | public void DtTest()
13 | {
14 | var cotp = new COTP
15 | {
16 | Type = PduType.Data
17 | };
18 |
19 | var pk = cotp.ToPacket(false);
20 |
21 | var cotp2 = new COTP();
22 | var rs = cotp2.Read(pk);
23 | Assert.True(rs);
24 | Assert.Equal(cotp.Type, cotp2.Type);
25 | }
26 |
27 | [Fact]
28 | public void DecodeCR()
29 | {
30 | var str = "03 00 00 16 11 e0 00 00 00 01 00 c1 02 10 00 c2 02 03 00 c0 01 0a";
31 | var buf = str.ToHex();
32 | var pk = new Packet(buf);
33 |
34 | // 前面有TPKT头
35 | var tpkt = new TPKT();
36 | tpkt.Read(pk);
37 | Assert.Equal(3, tpkt.Version);
38 | Assert.Equal(0, tpkt.Reserved);
39 | Assert.Equal(0x16, tpkt.Length);
40 | Assert.Equal(pk.Slice(4).ToHex(), tpkt.Data.ToHex());
41 |
42 | var cotp = new COTP();
43 | var rs = cotp.Read(tpkt.Data);
44 | Assert.True(rs);
45 | Assert.Equal(PduType.ConnectionRequest, cotp.Type);
46 | Assert.Equal(0x0000, cotp.Destination);
47 | Assert.Equal(0x0001, cotp.Source);
48 | Assert.Equal(0x00, cotp.Option);
49 |
50 | var ps = cotp.Parameters;
51 | Assert.NotEmpty(ps);
52 |
53 | Assert.Equal(COTPParameterKinds.SrcTsap, ps[0].Kind);
54 | Assert.Equal(0x1000, (UInt16)ps[0].Value);
55 |
56 | Assert.Equal(COTPParameterKinds.DstTsap, ps[1].Kind);
57 | Assert.Equal(0x0300, (UInt16)ps[1].Value);
58 |
59 | Assert.Equal(COTPParameterKinds.TpduSize, ps[2].Kind);
60 | Assert.Equal(0x0A, (Byte)ps[2].Value);
61 | }
62 |
63 | Byte[] plcHead1_200smart = [3, 0, 0, 22, 17, 224, 0, 0, 0, 1, 0, 193, 2, 16, 0, 194, 2, 3, 0, 192, 1, 10];
64 | Byte[] plcHead2_200smart = [3, 0, 0, 25, 2, 240, 128, 50, 1, 0, 0, 204, 193, 0, 8, 0, 0, 240, 0, 0, 1, 0, 1, 3, 192];
65 | [Fact]
66 | public void Decode_200smart_CR()
67 | {
68 | var pk = new Packet(plcHead1_200smart);
69 |
70 | // 前面有TPKT头
71 | var tpkt = new TPKT();
72 | tpkt.Read(pk);
73 | Assert.Equal(3, tpkt.Version);
74 | Assert.Equal(0, tpkt.Reserved);
75 | Assert.Equal(0x16, tpkt.Length);
76 | Assert.Equal(pk.Slice(4).ToHex(), tpkt.Data.ToHex());
77 |
78 | var cotp = new COTP();
79 | var rs = cotp.Read(tpkt.Data);
80 | Assert.True(rs);
81 | Assert.Equal(PduType.ConnectionRequest, cotp.Type);
82 | Assert.Equal(0x0000, cotp.Destination);
83 | Assert.Equal(0x0001, cotp.Source);
84 | Assert.Equal(0x00, cotp.Option);
85 |
86 | var ps = cotp.Parameters;
87 | Assert.NotEmpty(ps);
88 |
89 | Assert.Equal(COTPParameterKinds.SrcTsap, ps[0].Kind);
90 | Assert.Equal(0x1000, (UInt16)ps[0].Value);
91 |
92 | Assert.Equal(COTPParameterKinds.DstTsap, ps[1].Kind);
93 | Assert.Equal(0x0300, (UInt16)ps[1].Value);
94 |
95 | Assert.Equal(COTPParameterKinds.TpduSize, ps[2].Kind);
96 | Assert.Equal(0x0A, (Byte)ps[2].Value);
97 | }
98 |
99 | [Fact]
100 | public void Decode_200smart_Data()
101 | {
102 | var pk = new Packet(plcHead2_200smart);
103 |
104 | // 前面有TPKT头
105 | var tpkt = new TPKT();
106 | tpkt.Read(pk);
107 | Assert.Equal(3, tpkt.Version);
108 | Assert.Equal(0, tpkt.Reserved);
109 | Assert.Equal(0x19, tpkt.Length);
110 | Assert.Equal(pk.Slice(4).ToHex(), tpkt.Data.ToHex());
111 |
112 | var cotp = new COTP();
113 | var rs = cotp.Read(tpkt.Data);
114 | Assert.True(rs);
115 | Assert.Equal(PduType.Data, cotp.Type);
116 | Assert.True(cotp.LastDataUnit);
117 |
118 | Assert.NotNull(cotp.Data);
119 | Assert.Equal(18, cotp.Data.Total);
120 |
121 | var msg = new S7Message();
122 | var rs2 = msg.Read(cotp.Data);
123 | Assert.True(rs2);
124 |
125 | Assert.Equal(0x32, msg.ProtocolId);
126 | Assert.Equal(S7Kinds.Job, msg.Kind);
127 | Assert.Equal(0x0000, msg.Reserved);
128 | Assert.Equal(0xCCC1, msg.Sequence);
129 |
130 | Assert.Single(msg.Parameters);
131 | }
132 |
133 | [Fact]
134 | public void DecodeCC()
135 | {
136 | var str = "03 00 00 16 11 d0 00 01 00 07 00 c0 01 0a c1 02 10 00 c2 02 03 00";
137 | var buf = str.ToHex();
138 | var pk = new Packet(buf);
139 |
140 | // 前面有TPKT头
141 | var tpkt = new TPKT();
142 | tpkt.Read(pk);
143 | Assert.Equal(3, tpkt.Version);
144 | Assert.Equal(0, tpkt.Reserved);
145 | Assert.Equal(0x16, tpkt.Length);
146 | Assert.Equal(pk.Slice(4).ToHex(), tpkt.Data.ToHex());
147 |
148 | var cotp = new COTP();
149 | var rs = cotp.Read(tpkt.Data);
150 | Assert.True(rs);
151 | Assert.Equal(PduType.ConnectionConfirmed, cotp.Type);
152 | Assert.Equal(0x0001, cotp.Destination);
153 | Assert.Equal(0x0007, cotp.Source);
154 | Assert.Equal(0x00, cotp.Option);
155 |
156 | var ps = cotp.Parameters;
157 | Assert.NotEmpty(ps);
158 |
159 | Assert.Equal(COTPParameterKinds.TpduSize, ps[0].Kind);
160 | Assert.Equal(0x0A, (Byte)ps[0].Value);
161 |
162 | Assert.Equal(COTPParameterKinds.SrcTsap, ps[1].Kind);
163 | Assert.Equal(0x1000, (UInt16)ps[1].Value);
164 |
165 | Assert.Equal(COTPParameterKinds.DstTsap, ps[2].Kind);
166 | Assert.Equal(0x0300, (UInt16)ps[2].Value);
167 | }
168 |
169 | [Fact]
170 | public void DecodeDT()
171 | {
172 | Byte[] plcHead2_200smart = [3, 0, 0, 25, 2, 240, 128, 50, 1, 0, 0, 204, 193, 0, 8, 0, 0, 240, 0, 0, 1, 0, 1, 3, 192];
173 | var pk = new Packet(plcHead2_200smart);
174 |
175 | // 前面有TPKT头
176 | var tpkt = new TPKT();
177 | tpkt.Read(pk);
178 | Assert.Equal(3, tpkt.Version);
179 | Assert.Equal(0, tpkt.Reserved);
180 | Assert.Equal(25, tpkt.Length);
181 | Assert.Equal(pk.Slice(4).ToHex(), tpkt.Data.ToHex());
182 |
183 | var cotp = new COTP();
184 | var rs = cotp.Read(tpkt.Data);
185 | Assert.True(rs);
186 | Assert.Equal(PduType.Data, cotp.Type);
187 | Assert.True(cotp.LastDataUnit);
188 |
189 | Assert.NotNull(cotp.Data);
190 | Assert.Equal(18, cotp.Data.Total);
191 | }
192 |
193 | [Fact]
194 | public void ConnectTest()
195 | {
196 | var cotp = new COTP
197 | {
198 | Type = PduType.ConnectionRequest
199 | };
200 |
201 | var pk = cotp.ToPacket(false);
202 |
203 | var cotp2 = new COTP();
204 | var rs = cotp2.Read(pk);
205 | Assert.True(rs);
206 | Assert.Equal(cotp.Type, cotp2.Type);
207 | }
208 |
209 | [Fact]
210 | public void ConfirmedTest()
211 | {
212 | var cotp = new COTP
213 | {
214 | Type = PduType.ConnectionConfirmed
215 | };
216 |
217 | var pk = cotp.ToPacket(false);
218 |
219 | var cotp2 = new COTP();
220 | var rs = cotp2.Read(pk);
221 | Assert.True(rs);
222 | Assert.Equal(cotp.Type, cotp2.Type);
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Drivers/SiemensS7Driver.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 | using System.Text;
3 | using NewLife.IoT;
4 | using NewLife.IoT.Drivers;
5 | using NewLife.IoT.ThingModels;
6 | using NewLife.Log;
7 | using NewLife.Siemens.Models;
8 | using NewLife.Siemens.Protocols;
9 |
10 | namespace NewLife.Siemens.Drivers;
11 |
12 | /// 西门子PLC协议封装
13 | [Driver("SiemensPLC")]
14 | [DisplayName("西门子PLC")]
15 | public class SiemensS7Driver : DriverBase
16 | {
17 | #region 属性
18 | private S7Client? _plc;
19 |
20 | ///
21 | /// 打开通道数量
22 | ///
23 | private Int32 _nodes;
24 | #endregion
25 |
26 | #region 构造
27 | ///
28 | /// 销毁时,关闭连接
29 | ///
30 | ///
31 | protected override void Dispose(Boolean disposing)
32 | {
33 | base.Dispose(disposing);
34 |
35 | _plc.TryDispose();
36 | }
37 | #endregion
38 |
39 | #region 方法
40 | ///
41 | /// 创建驱动参数对象,可序列化成Xml/Json作为该协议的参数模板
42 | ///
43 | ///
44 | protected override IDriverParameter OnCreateParameter() => new SiemensParameter
45 | {
46 | Address = "127.0.0.1:102",
47 | CpuType = CpuType.S7200Smart,
48 | Rack = 0,
49 | Slot = 0,
50 | };
51 |
52 | ///
53 | /// 打开通道。一个ModbusTcp设备可能分为多个通道读取,需要共用Tcp连接,以不同节点区分
54 | ///
55 | /// 通道
56 | /// 参数
57 | ///
58 | public override INode Open(IDevice device, IDriverParameter? parameter)
59 | {
60 | if (parameter is not SiemensParameter pm) throw new ArgumentNullException(nameof(parameter));
61 |
62 | var address = pm.Address;
63 | if (address.IsNullOrEmpty()) throw new ArgumentException("参数中未指定地址address");
64 |
65 | var p = address.IndexOf(':');
66 | if (p < 0) throw new ArgumentException($"参数中地址address格式错误:{address}");
67 |
68 | if (!Enum.IsDefined(typeof(CpuType), pm.CpuType))
69 | throw new ArgumentException($"参数中未指定地址CpuType,必须为其中之一:{Enum.GetNames(typeof(CpuType)).Join()}");
70 |
71 | var rack = pm.Rack;
72 | var slot = pm.Slot;
73 |
74 | var node = new SiemensNode
75 | {
76 | Address = address,
77 | Device = device,
78 | Parameter = pm,
79 | };
80 |
81 | if (_plc == null)
82 | {
83 | lock (this)
84 | {
85 | if (_plc == null)
86 | {
87 | var ip = address[..p];
88 | var port = address[(p + 1)..].ToInt();
89 |
90 | _plc = new S7Client(pm.CpuType, ip, port, rack, slot)
91 | {
92 | Timeout = 5000,
93 | };
94 | if (Log != null && Log.Level <= LogLevel.Debug) _plc.Log = Log;
95 |
96 | _plc.OpenAsync().GetAwaiter().GetResult();
97 | }
98 | }
99 | }
100 |
101 | Interlocked.Increment(ref _nodes);
102 |
103 | return node;
104 | }
105 |
106 | ///
107 | /// 关闭设备驱动
108 | ///
109 | ///
110 | public override void Close(INode node)
111 | {
112 | if (Interlocked.Decrement(ref _nodes) <= 0)
113 | {
114 | _plc?.Close();
115 | _plc.TryDispose();
116 | _plc = null;
117 | }
118 | }
119 |
120 | ///
121 | /// 读取数据
122 | ///
123 | /// 节点对象,可存储站号等信息,仅驱动自己识别
124 | /// 点位集合
125 | ///
126 | public override IDictionary Read(INode node, IPoint[] points)
127 | {
128 | var dic = new Dictionary();
129 |
130 | if (points == null || points.Length == 0) return dic;
131 | if (_plc == null) throw new Exception("PLC未打开!");
132 |
133 | var spec = node.Device?.Specification;
134 | foreach (var point in points)
135 | {
136 | var addr = GetAddress(point);
137 | if (addr.IsNullOrWhiteSpace()) continue;
138 |
139 | var name = !point.Name.IsNullOrWhiteSpace() ? point.Name : point.Address;
140 | if (name.IsNullOrEmpty()) continue;
141 |
142 | // 操作字节数组,不用设置bitNumber,但是解析需要带上
143 | if (addr.IndexOf('.') == -1) addr += ".0";
144 |
145 | var plcAddress = new PLCAddress(addr);
146 |
147 | var data = _plc.ReadBytes(plcAddress, point.GetLength());
148 |
149 | // 借助物模型转换数据类型
150 | if (point.GetNetType() != null)
151 | {
152 | //类型string的时候直接返回ToHex
153 | if (point.GetNetType() == typeof(string))
154 | {
155 | //默认去除返回的3C1E开始的通讯分隔符
156 | dic[name] = data.ToStr(Encoding.UTF8, 2);
157 | }
158 | else
159 | {
160 | if (spec != null)
161 | dic[name] = spec.Decode(data, point);
162 | else
163 | dic[name] = point.Convert(data.Swap(true, true));
164 |
165 | }
166 | }
167 | else
168 | dic[name] = data;
169 | }
170 |
171 | return dic;
172 | }
173 |
174 | ///
175 | /// 从点位中解析地址
176 | ///
177 | ///
178 | ///
179 | public virtual String GetAddress(IPoint point)
180 | {
181 | var addr = point.Address;
182 | if (addr.IsNullOrEmpty()) throw new ArgumentException("点位信息不能为空!");
183 |
184 | // 去掉冒号后面的位域
185 | var p = addr.IndexOf(':');
186 | if (p > 0) addr = addr[..p];
187 |
188 | return addr;
189 | }
190 |
191 | ///
192 | /// 写入数据
193 | ///
194 | /// 节点对象,可存储站号等信息,仅驱动自己识别
195 | /// 点位
196 | /// 数值
197 | public override Object? Write(INode node, IPoint point, Object? value)
198 | {
199 | var addr = GetAddress(point);
200 | if (addr.IsNullOrWhiteSpace()) return null;
201 | if (_plc == null) throw new Exception("PLC未打开!");
202 |
203 | // 借助物模型转换数据类型
204 | var spec = node.Device?.Specification;
205 | if (value != null && value is not Byte[])
206 | {
207 | // 普通数值转为字节数组
208 | if (spec != null)
209 | value = spec.Encode(value, point);
210 | else
211 | value = point.GetBytes(value)?.Swap(true, true);
212 | }
213 |
214 | // 操作字节数组,不用设置bitNumber,但是解析需要带上
215 | if (addr.IndexOf('.') == -1) addr += ".0";
216 |
217 | var plcAddress = new PLCAddress(addr);
218 |
219 | Byte[]? bytes = null;
220 | if (value is Byte[] v)
221 | bytes = v;
222 | else
223 | {
224 | if (point.Type.IsNullOrEmpty()) throw new ArgumentNullException(nameof(point.Type));
225 |
226 | bytes = point.Type.ToLower() switch
227 | {
228 | "boolean" or "bool" => BitConverter.GetBytes(value.ToBoolean()),
229 | "short" => BitConverter.GetBytes(Int16.Parse(value + "")),
230 | "int" => BitConverter.GetBytes(value.ToInt()),
231 | "float" => BitConverter.GetBytes(Single.Parse(value + "")),
232 | "byte" => BitConverter.GetBytes(Byte.Parse(value + "")),
233 | "long" => BitConverter.GetBytes(Int64.Parse(value + "")),
234 | "double" => BitConverter.GetBytes(value.ToDouble()),
235 | "time" => BitConverter.GetBytes(value.ToDateTime().Ticks),
236 | "string" or "text" => (value + "").GetBytes(),
237 | _ => throw new ArgumentException($"数据value不是字节数组或有效类型[{point.Type}]!"),
238 | };
239 | }
240 |
241 | _plc.WriteBytes(plcAddress, bytes);
242 |
243 | return "OK";
244 | }
245 | #endregion
246 | }
--------------------------------------------------------------------------------
/NewLife.Siemens/Protocols/PLCAddress.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Siemens.Models;
2 |
3 | namespace NewLife.Siemens.Protocols;
4 |
5 | /// PLC地址
6 | public class PLCAddress
7 | {
8 | /// 数据类型
9 | public DataType DataType { get; set; }
10 |
11 | /// 数据块
12 | public Int32 DbNumber { get; set; }
13 |
14 | /// 开始字节
15 | public Int32 StartByte { get; set; }
16 |
17 | /// 位数字
18 | public Int32 BitNumber { get; set; }
19 |
20 | /// 变量类型
21 | public VarType VarType { get; set; }
22 |
23 | /// 使用字符串实例化PLC地址
24 | ///
25 | public PLCAddress(String address)
26 | {
27 | Parse(address, out var dataType, out var dbNumber, out var varType, out var startByte, out var bitNumber);
28 |
29 | DataType = dataType;
30 | DbNumber = dbNumber;
31 | StartByte = startByte;
32 | BitNumber = bitNumber;
33 | VarType = varType;
34 | }
35 |
36 | /// 分析
37 | ///
38 | ///
39 | ///
40 | ///
41 | ///
42 | ///
43 | public static void Parse(String input, out DataType dataType, out Int32 dbNumber, out VarType varType, out Int32 address, out Int32 bitNumber)
44 | {
45 | bitNumber = -1;
46 | dbNumber = 0;
47 |
48 | switch (input[..2])
49 | {
50 | case "DB":
51 | var strings = input.Split(['.']);
52 | if (strings.Length < 2)
53 | throw new InvalidDataException("To few periods for DB address");
54 | //数据块字节
55 |
56 | dataType = DataType.DataBlock;
57 | dbNumber = Int32.Parse(strings[0][2..]);
58 | if (strings[1].ToLower().Contains("string"))
59 | address = Int32.Parse(strings[1][6..]);
60 | else
61 | address = Int32.Parse(strings[1][3..]);
62 | var dbType = strings[1][..3];
63 | switch (dbType)
64 | {
65 | case "DBB":
66 | varType = VarType.Byte;
67 | return;
68 | case "DBW":
69 | varType = VarType.Word;
70 | return;
71 | case "DBD":
72 | varType = VarType.DWord;
73 | return;
74 | case "DBX":
75 | bitNumber = Int32.Parse(strings[2]);
76 | if (bitNumber > 7)
77 | throw new InvalidDataException("Bit can only be 0-7");
78 | varType = VarType.Bit;
79 | return;
80 | case "STR":
81 | varType = VarType.String;
82 | bitNumber = Int32.Parse(strings[2]);
83 | return;
84 | default:
85 | throw new InvalidDataException();
86 | }
87 | break;
88 | case "IB":
89 | case "EB":
90 | // Input byte
91 | dataType = DataType.Input;
92 | dbNumber = 0;
93 | address = Int32.Parse(input[2..]);
94 | varType = VarType.Byte;
95 | return;
96 | case "IW":
97 | case "EW":
98 | // Input word
99 | dataType = DataType.Input;
100 | dbNumber = 0;
101 | address = Int32.Parse(input[2..]);
102 | varType = VarType.Word;
103 | return;
104 | case "ID":
105 | case "ED":
106 | // Input double-word
107 | dataType = DataType.Input;
108 | dbNumber = 0;
109 | address = Int32.Parse(input[2..]);
110 | varType = VarType.DWord;
111 | return;
112 | case "QB":
113 | case "AB":
114 | case "OB":
115 | // Output byte
116 | dataType = DataType.Output;
117 | dbNumber = 0;
118 | address = Int32.Parse(input[2..]);
119 | varType = VarType.Byte;
120 | return;
121 | case "QW":
122 | case "AW":
123 | case "OW":
124 | // Output word
125 | dataType = DataType.Output;
126 | dbNumber = 0;
127 | address = Int32.Parse(input[2..]);
128 | varType = VarType.Word;
129 | return;
130 | case "QD":
131 | case "AD":
132 | case "OD":
133 | // Output double-word
134 | dataType = DataType.Output;
135 | dbNumber = 0;
136 | address = Int32.Parse(input[2..]);
137 | varType = VarType.DWord;
138 | return;
139 | case "MB":
140 | // Memory byte
141 | dataType = DataType.Memory;
142 | dbNumber = 0;
143 | address = Int32.Parse(input[2..]);
144 | varType = VarType.Byte;
145 | return;
146 | case "MW":
147 | // Memory word
148 | dataType = DataType.Memory;
149 | dbNumber = 0;
150 | address = Int32.Parse(input[2..]);
151 | varType = VarType.Word;
152 | return;
153 | case "MD":
154 | // Memory double-word
155 | dataType = DataType.Memory;
156 | dbNumber = 0;
157 | address = Int32.Parse(input[2..]);
158 | varType = VarType.DWord;
159 | return;
160 | default:
161 | switch (input[..1])
162 | {
163 | case "E":
164 | case "I":
165 | // Input
166 | dataType = DataType.Input;
167 | varType = VarType.Bit;
168 | break;
169 | case "Q":
170 | case "A":
171 | case "O":
172 | // Output
173 | dataType = DataType.Output;
174 | varType = VarType.Bit;
175 | break;
176 | case "M":
177 | // Memory
178 | dataType = DataType.Memory;
179 | varType = VarType.Bit;
180 | break;
181 | case "T":
182 | // Timer
183 | dataType = DataType.Timer;
184 | dbNumber = 0;
185 | address = Int32.Parse(input[1..]);
186 | varType = VarType.Timer;
187 | return;
188 | case "Z":
189 | case "C":
190 | // Counter
191 | dataType = DataType.Counter;
192 | dbNumber = 0;
193 | address = Int32.Parse(input[1..]);
194 | varType = VarType.Counter;
195 | return;
196 | default:
197 | throw new InvalidDataException(String.Format("{0} is not a valid address", input[..1]));
198 | }
199 |
200 | var txt2 = input[1..];
201 | if (txt2.IndexOf(".") == -1)
202 | throw new InvalidDataException("To few periods for DB address");
203 |
204 | address = Int32.Parse(txt2[..txt2.IndexOf(".")]);
205 | bitNumber = Int32.Parse(txt2[(txt2.IndexOf(".") + 1)..]);
206 | if (bitNumber > 7)
207 | throw new InvalidDataException("Bit can only be 0-7");
208 |
209 | return;
210 | }
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Protocols/S7Message.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Data;
2 | using NewLife.Serialization;
3 | using NewLife.Siemens.Messages;
4 |
5 | namespace NewLife.Siemens.Protocols;
6 |
7 | /// S7协议报文
8 | public class S7Message : IAccessor
9 | {
10 | #region 属性
11 | /// 协议常量,始终设置为0x32
12 | public Byte ProtocolId { get; set; } = 0x32;
13 |
14 | /// 消息的一般类型(有时称为ROSCTR类型),消息的其余部分在很大程度上取决于Kind和功能代码。
15 | public S7Kinds Kind { get; set; }
16 |
17 | /// 保留数据
18 | public UInt16 Reserved { get; set; }
19 |
20 | /// 序列号。由主站生成,每次新传输递增,用于链接对其请求的响应。小端字节序
21 | public UInt16 Sequence { get; set; }
22 |
23 | /// 错误类型。仅出现在AckData
24 | public Byte ErrorClass { get; set; }
25 |
26 | /// 错误码。仅出现在AckData
27 | public Byte ErrorCode { get; set; }
28 |
29 | /// 参数集合
30 | public IList Parameters { get; set; } = [];
31 |
32 | ///// 数据
33 | //public Packet Data { get; set; }
34 | #endregion
35 |
36 | #region 构造
37 | /// 友好显示
38 | ///
39 | public override String ToString() => $"[{Kind}]<{Sequence}>[{Parameters.Count}] {Parameters.FirstOrDefault()}";
40 | #endregion
41 |
42 | #region 读写
43 | /// 读取
44 | public Boolean Read(Byte[] data) => Read(new MemoryStream(data), null);
45 |
46 | /// 读取
47 | ///
48 | ///
49 | public Boolean Read(IPacket pk) => Read(pk.GetStream(), pk);
50 |
51 | /// 读取
52 | ///
53 | ///
54 | ///
55 | public Boolean Read(Stream stream, Object? context)
56 | {
57 | //var pk = context as Packet;
58 | //stream ??= pk?.GetStream();
59 | var reader = new Binary { Stream = stream, IsLittleEndian = false };
60 |
61 | ProtocolId = reader.ReadByte();
62 | Kind = (S7Kinds)reader.ReadByte();
63 | Reserved = reader.ReadUInt16();
64 | Sequence = reader.ReadUInt16();
65 |
66 | // 参数长度和数据长度
67 | var plen = reader.ReadUInt16();
68 | var dlen = reader.ReadUInt16();
69 |
70 | // 错误码
71 | if (Kind == S7Kinds.AckData)
72 | {
73 | ErrorClass = reader.ReadByte();
74 | ErrorCode = reader.ReadByte();
75 | }
76 |
77 | // 读取参数
78 | if (plen > 0)
79 | {
80 | var buf = reader.ReadBytes(plen);
81 | ReadParameters(buf);
82 | }
83 |
84 | // 读取数据
85 | if (dlen > 0)
86 | {
87 | var buf = reader.ReadBytes(dlen);
88 | ReadParameterItems(buf);
89 | }
90 |
91 | return true;
92 | }
93 |
94 | void ReadParameters(Byte[] buf)
95 | {
96 | var ms = new MemoryStream(buf);
97 | var reader = new Binary { Stream = ms, IsLittleEndian = false };
98 |
99 | while (ms.Position < ms.Length)
100 | {
101 | var kind = (S7Functions)reader.ReadByte();
102 | ms.Seek(-1, SeekOrigin.Current);
103 | switch (kind)
104 | {
105 | case S7Functions.Setup:
106 | var pm = new SetupMessage();
107 | if (pm.Read(reader))
108 | Parameters.Add(pm);
109 | break;
110 | case S7Functions.ReadVar:
111 | if (Kind == S7Kinds.AckData)
112 | {
113 | var rv = new ReadResponse();
114 | if (rv.Read(reader))
115 | Parameters.Add(rv);
116 | }
117 | else
118 | {
119 | var rv = new ReadRequest();
120 | if (rv.Read(reader))
121 | Parameters.Add(rv);
122 | }
123 | break;
124 | case S7Functions.WriteVar:
125 | if (Kind == S7Kinds.AckData)
126 | {
127 | var rv = new WriteResponse();
128 | if (rv.Read(reader))
129 | Parameters.Add(rv);
130 | }
131 | else
132 | {
133 | var rv = new WriteRequest();
134 | if (rv.Read(reader))
135 | Parameters.Add(rv);
136 | }
137 | break;
138 | default:
139 | throw new NotSupportedException($"不支持的S7参数类型[{kind}]");
140 | }
141 | }
142 | }
143 |
144 | void ReadParameterItems(Byte[] buf)
145 | {
146 | var ms = new MemoryStream(buf);
147 | var reader = new Binary { Stream = ms, IsLittleEndian = false };
148 |
149 | foreach (var pm in Parameters)
150 | {
151 | if (pm is IDataItems rr)
152 | rr.ReadItems(reader);
153 | }
154 | }
155 |
156 | /// 写入
157 | ///
158 | ///
159 | ///
160 | public Boolean Write(Stream stream, Object? context)
161 | {
162 | //var pk = context as Packet;
163 | //stream ??= pk?.GetStream();
164 | var writer = new Binary { Stream = stream, IsLittleEndian = false };
165 |
166 | writer.WriteByte(ProtocolId);
167 | writer.WriteByte((Byte)Kind);
168 | writer.WriteUInt16(Reserved);
169 | writer.WriteUInt16(Sequence);
170 |
171 | var ps = SaveParameters(Parameters);
172 | var dt = SaveParameterItems(Parameters);
173 | var plen = ps?.Length ?? 0;
174 | var dlen = dt?.Length ?? 0;
175 |
176 | writer.WriteUInt16((UInt16)plen);
177 | writer.WriteUInt16((UInt16)dlen);
178 |
179 | if (Kind == S7Kinds.AckData)
180 | {
181 | writer.WriteByte(ErrorClass);
182 | writer.WriteByte(ErrorCode);
183 | }
184 |
185 | if (ps != null && ps.Length > 0) writer.Write(ps, 0, ps.Length);
186 | if (dt != null && dt.Length > 0) writer.Write(dt, 0, dt.Length);
187 |
188 | return true;
189 | }
190 |
191 | Byte[]? SaveParameters(IList ps)
192 | {
193 | if (ps == null || ps.Count == 0) return null;
194 |
195 | var writer = new Binary { IsLittleEndian = false };
196 | foreach (var pm in ps)
197 | {
198 | pm.Write(writer);
199 | }
200 |
201 | return writer.GetBytes();
202 | }
203 |
204 | Byte[]? SaveParameterItems(IList ps)
205 | {
206 | if (ps == null || ps.Count == 0) return null;
207 |
208 | var writer = new Binary { IsLittleEndian = false };
209 | foreach (var pm in ps)
210 | {
211 | if (pm is IDataItems rr)
212 | rr.WriteItems(writer);
213 | }
214 |
215 | return writer.GetBytes();
216 | }
217 |
218 | /// 序列化
219 | ///
220 | public Byte[] GetBytes()
221 | {
222 | var ms = new MemoryStream();
223 | Write(ms, null);
224 |
225 | return ms.ToArray();
226 | }
227 | #endregion
228 |
229 | #region 参数
230 | /// 获取参数
231 | ///
232 | ///
233 | public S7Parameter? GetParameter(S7Functions code) => Parameters?.FirstOrDefault(e => e.Code == code);
234 |
235 | /// 设置参数
236 | ///
237 | public void SetParameter(S7Parameter parameter)
238 | {
239 | var ps = Parameters;
240 | for (var i = 0; i < ps.Count; i++)
241 | {
242 | var pm2 = ps[i];
243 | if (pm2.Code == parameter.Code)
244 | {
245 | ps[i] = parameter;
246 | return;
247 | }
248 | }
249 |
250 | ps.Add(parameter);
251 | }
252 |
253 | /// 设置参数
254 | ///
255 | ///
256 | public void Setup(UInt16 amq, UInt16 pdu)
257 | {
258 | SetParameter(new SetupMessage
259 | {
260 | MaxAmqCaller = amq,
261 | MaxAmqCallee = amq,
262 | PduLength = pdu,
263 | });
264 | }
265 | #endregion
266 |
267 | #region 辅助
268 | /// 序列化为COTP
269 | ///
270 | public COTP ToCOTP()
271 | {
272 | return new COTP
273 | {
274 | Type = PduType.Data,
275 | LastDataUnit = true,
276 | Data = (ArrayPacket)GetBytes()
277 | };
278 | }
279 | #endregion
280 | }
281 |
--------------------------------------------------------------------------------
/TestClient/FrmMain.Designer.cs:
--------------------------------------------------------------------------------
1 | namespace TestClient
2 | {
3 | partial class FrmMain
4 | {
5 | ///
6 | /// Required designer variable.
7 | ///
8 | private System.ComponentModel.IContainer components = null;
9 |
10 | ///
11 | /// Clean up any resources being used.
12 | ///
13 | /// true if managed resources should be disposed; otherwise, false.
14 | protected override void Dispose(bool disposing)
15 | {
16 | if (disposing && (components != null))
17 | {
18 | components.Dispose();
19 | }
20 | base.Dispose(disposing);
21 | }
22 |
23 | #region Windows Form Designer generated code
24 |
25 | ///
26 | /// Required method for Designer support - do not modify
27 | /// the contents of this method with the code editor.
28 | ///
29 | private void InitializeComponent()
30 | {
31 | label1 = new Label();
32 | label2 = new Label();
33 | tb_address = new TextBox();
34 | tb_port = new TextBox();
35 | btn_conn = new Button();
36 | rtb_content = new RichTextBox();
37 | btn_write = new Button();
38 | tb_pointAddress = new TextBox();
39 | tb_value = new TextBox();
40 | label3 = new Label();
41 | label4 = new Label();
42 | btn_read = new Button();
43 | label5 = new Label();
44 | label6 = new Label();
45 | tb_type = new TextBox();
46 | tb_length = new TextBox();
47 | SuspendLayout();
48 | //
49 | // label1
50 | //
51 | label1.AutoSize = true;
52 | label1.Location = new Point(14, 24);
53 | label1.Margin = new Padding(2, 0, 2, 0);
54 | label1.Name = "label1";
55 | label1.Size = new Size(54, 20);
56 | label1.TabIndex = 0;
57 | label1.Text = "地址:";
58 | //
59 | // label2
60 | //
61 | label2.AutoSize = true;
62 | label2.Location = new Point(246, 24);
63 | label2.Margin = new Padding(2, 0, 2, 0);
64 | label2.Name = "label2";
65 | label2.Size = new Size(54, 20);
66 | label2.TabIndex = 1;
67 | label2.Text = "端口:";
68 | //
69 | // tb_address
70 | //
71 | tb_address.Location = new Point(70, 22);
72 | tb_address.Margin = new Padding(2);
73 | tb_address.Name = "tb_address";
74 | tb_address.Size = new Size(172, 27);
75 | tb_address.TabIndex = 2;
76 | tb_address.Text = "127.0.0.1";
77 | //
78 | // tb_port
79 | //
80 | tb_port.Location = new Point(303, 22);
81 | tb_port.Margin = new Padding(2);
82 | tb_port.Name = "tb_port";
83 | tb_port.Size = new Size(151, 27);
84 | tb_port.TabIndex = 3;
85 | tb_port.Text = "102";
86 | //
87 | // btn_conn
88 | //
89 | btn_conn.Location = new Point(468, 20);
90 | btn_conn.Margin = new Padding(2);
91 | btn_conn.Name = "btn_conn";
92 | btn_conn.Size = new Size(92, 28);
93 | btn_conn.TabIndex = 4;
94 | btn_conn.Text = "连接";
95 | btn_conn.UseVisualStyleBackColor = true;
96 | btn_conn.Click += button1_Click;
97 | //
98 | // rtb_content
99 | //
100 | rtb_content.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
101 | rtb_content.Location = new Point(10, 144);
102 | rtb_content.Margin = new Padding(2);
103 | rtb_content.Name = "rtb_content";
104 | rtb_content.Size = new Size(762, 342);
105 | rtb_content.TabIndex = 5;
106 | rtb_content.Text = "";
107 | //
108 | // btn_write
109 | //
110 | btn_write.Location = new Point(468, 61);
111 | btn_write.Margin = new Padding(2);
112 | btn_write.Name = "btn_write";
113 | btn_write.Size = new Size(92, 28);
114 | btn_write.TabIndex = 6;
115 | btn_write.Text = "写入";
116 | btn_write.UseVisualStyleBackColor = true;
117 | btn_write.Click += btn_write_Click;
118 | //
119 | // tb_pointAddress
120 | //
121 | tb_pointAddress.Location = new Point(70, 62);
122 | tb_pointAddress.Margin = new Padding(2);
123 | tb_pointAddress.Name = "tb_pointAddress";
124 | tb_pointAddress.Size = new Size(172, 27);
125 | tb_pointAddress.TabIndex = 7;
126 | tb_pointAddress.Text = "DB1.DBD32";
127 | //
128 | // tb_value
129 | //
130 | tb_value.Location = new Point(303, 62);
131 | tb_value.Margin = new Padding(2);
132 | tb_value.Name = "tb_value";
133 | tb_value.Size = new Size(151, 27);
134 | tb_value.TabIndex = 8;
135 | tb_value.Text = "5";
136 | //
137 | // label3
138 | //
139 | label3.AutoSize = true;
140 | label3.Location = new Point(14, 65);
141 | label3.Margin = new Padding(2, 0, 2, 0);
142 | label3.Name = "label3";
143 | label3.Size = new Size(54, 20);
144 | label3.TabIndex = 9;
145 | label3.Text = "点位:";
146 | //
147 | // label4
148 | //
149 | label4.AutoSize = true;
150 | label4.Location = new Point(261, 65);
151 | label4.Margin = new Padding(2, 0, 2, 0);
152 | label4.Name = "label4";
153 | label4.Size = new Size(39, 20);
154 | label4.TabIndex = 10;
155 | label4.Text = "值:";
156 | //
157 | // btn_read
158 | //
159 | btn_read.Location = new Point(578, 61);
160 | btn_read.Margin = new Padding(2);
161 | btn_read.Name = "btn_read";
162 | btn_read.Size = new Size(92, 28);
163 | btn_read.TabIndex = 11;
164 | btn_read.Text = "读取";
165 | btn_read.UseVisualStyleBackColor = true;
166 | btn_read.Click += btn_read_Click;
167 | //
168 | // label5
169 | //
170 | label5.AutoSize = true;
171 | label5.Location = new Point(14, 106);
172 | label5.Margin = new Padding(2, 0, 2, 0);
173 | label5.Name = "label5";
174 | label5.Size = new Size(54, 20);
175 | label5.TabIndex = 12;
176 | label5.Text = "类型:";
177 | //
178 | // label6
179 | //
180 | label6.AutoSize = true;
181 | label6.Location = new Point(246, 106);
182 | label6.Margin = new Padding(2, 0, 2, 0);
183 | label6.Name = "label6";
184 | label6.Size = new Size(54, 20);
185 | label6.TabIndex = 13;
186 | label6.Text = "长度:";
187 | //
188 | // tb_type
189 | //
190 | tb_type.Location = new Point(70, 103);
191 | tb_type.Margin = new Padding(2);
192 | tb_type.Name = "tb_type";
193 | tb_type.Size = new Size(172, 27);
194 | tb_type.TabIndex = 14;
195 | tb_type.Text = "int";
196 | //
197 | // tb_length
198 | //
199 | tb_length.Location = new Point(303, 103);
200 | tb_length.Margin = new Padding(2);
201 | tb_length.Name = "tb_length";
202 | tb_length.Size = new Size(151, 27);
203 | tb_length.TabIndex = 15;
204 | tb_length.Text = "2";
205 | //
206 | // FrmMain
207 | //
208 | AutoScaleDimensions = new SizeF(9F, 20F);
209 | AutoScaleMode = AutoScaleMode.Font;
210 | ClientSize = new Size(781, 495);
211 | Controls.Add(tb_length);
212 | Controls.Add(tb_type);
213 | Controls.Add(label6);
214 | Controls.Add(label5);
215 | Controls.Add(btn_read);
216 | Controls.Add(label4);
217 | Controls.Add(label3);
218 | Controls.Add(tb_value);
219 | Controls.Add(tb_pointAddress);
220 | Controls.Add(btn_write);
221 | Controls.Add(rtb_content);
222 | Controls.Add(btn_conn);
223 | Controls.Add(tb_port);
224 | Controls.Add(tb_address);
225 | Controls.Add(label2);
226 | Controls.Add(label1);
227 | Margin = new Padding(2);
228 | Name = "FrmMain";
229 | StartPosition = FormStartPosition.CenterScreen;
230 | Text = "S7测试";
231 | Load += FrmMain_Load;
232 | ResumeLayout(false);
233 | PerformLayout();
234 | }
235 |
236 | #endregion
237 |
238 | private Label label1;
239 | private Label label2;
240 | private TextBox tb_address;
241 | private TextBox tb_port;
242 | private Button btn_conn;
243 | private RichTextBox rtb_content;
244 | private Button btn_write;
245 | private TextBox tb_pointAddress;
246 | private TextBox tb_value;
247 | private Label label3;
248 | private Label label4;
249 | private Button btn_read;
250 | private Label label5;
251 | private Label label6;
252 | private TextBox tb_type;
253 | private TextBox tb_length;
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Protocols/COTP.cs:
--------------------------------------------------------------------------------
1 | using NewLife.Buffers;
2 | using NewLife.Data;
3 | using NewLife.Serialization;
4 |
5 | namespace NewLife.Siemens.Protocols;
6 |
7 | /// 面向连接的传输协议(Connection-Oriented Transport Protocol)
8 | ///
9 | /// 文档:https://tools.ietf.org/html/rfc905
10 | ///
11 | public class COTP
12 | {
13 | #region 属性
14 | /// 头部长度。不包含长度字段所在字节
15 | public Byte Length { get; set; }
16 |
17 | /// 包类型。常用CR连接、CC确认、DT数据
18 | public PduType Type { get; set; }
19 | #endregion
20 |
21 | #region CR/CC包
22 | /// 目标的引用,可以认为是用来唯一标识目标
23 | public UInt16 Destination { get; set; }
24 |
25 | /// 源的引用
26 | public UInt16 Source { get; set; }
27 |
28 | /// 选项
29 | public Byte Option { get; set; }
30 |
31 | /// 参数集合。一般CR/CC带有参数,交换Tsap和Tpdu大小
32 | public IList Parameters { get; set; } = [];
33 | #endregion
34 |
35 | #region DT包
36 | /// 编码。仅存在于DT包
37 | public Int32 Number { get; set; }
38 |
39 | /// 是否最后数据单元。仅存在于DT包
40 | public Boolean LastDataUnit { get; set; }
41 |
42 | /// 数据。仅存在于DT包
43 | public IPacket? Data { get; set; }
44 | #endregion
45 |
46 | #region 读写
47 | /// 解析数据
48 | ///
49 | ///
50 | public Boolean Read(IPacket data)
51 | {
52 | var reader = new SpanReader(data) { IsLittleEndian = false };
53 |
54 | // 头部长度。当前字节之后就是头部,然后是数据。CR/CC一般为17字节,DT一般为2字节
55 | Length = reader.ReadByte();
56 | Type = (PduType)reader.ReadByte();
57 |
58 | // 解析不同数据帧
59 | switch (Type)
60 | {
61 | case PduType.Data:
62 | {
63 | var flags = reader.ReadByte();
64 | Number = flags & 0x7F;
65 | LastDataUnit = (flags & 0x80) > 0;
66 |
67 | Data = reader.ReadPacket(-1);
68 | }
69 | break;
70 | case PduType.ConnectionRequest:
71 | case PduType.ConnectionConfirmed:
72 | default:
73 | {
74 | Destination = reader.ReadUInt16();
75 | Source = reader.ReadUInt16();
76 | Option = reader.ReadByte();
77 | Parameters = ReadParameters(ref reader);
78 | }
79 | break;
80 | }
81 |
82 | return true;
83 | }
84 |
85 | IList ReadParameters(ref SpanReader reader)
86 | {
87 | var list = new List();
88 | while (reader.FreeCapacity >= 1 + 1)
89 | {
90 | var tlv = new COTPParameter((COTPParameterKinds)reader.ReadByte(), reader.ReadByte(), null);
91 |
92 | //var buf = reader.ReadBytes(tlv.Length);
93 | tlv.Value = tlv.Length switch
94 | {
95 | 1 => reader.ReadByte(),
96 | 2 => reader.ReadUInt16(),
97 | 4 => reader.ReadUInt32(),
98 | _ => reader.ReadBytes(tlv.Length).ToArray(),
99 | };
100 | list.Add(tlv);
101 | }
102 |
103 | return list;
104 | }
105 |
106 | /// 序列化写入数据
107 | ///
108 | ///
109 | public Boolean Write(Stream stream)
110 | {
111 | var writer = new Binary { Stream = stream, IsLittleEndian = false };
112 |
113 | switch (Type)
114 | {
115 | case PduType.Data:
116 | stream.WriteByte((Byte)(1 + 1));
117 | stream.WriteByte((Byte)Type);
118 |
119 | var flags = (Byte)(Number & 0x7F);
120 | if (LastDataUnit) flags |= 0x80;
121 | stream.WriteByte(flags);
122 |
123 | Data?.CopyTo(stream);
124 | break;
125 | case PduType.ConnectionRequest:
126 | case PduType.ConnectionConfirmed:
127 | default:
128 | // 计算长度。再次之前需要先计算参数长度
129 | var ps = Parameters;
130 | var len = 1 + 2 + 2 + 1;
131 | if (ps != null)
132 | {
133 | FixParameters(ps);
134 | foreach (var item in ps)
135 | {
136 | len += 1 + 1 + item.Length;
137 | }
138 | }
139 | writer.WriteByte((Byte)len);
140 | writer.WriteByte((Byte)Type);
141 |
142 | writer.Write(Destination);
143 | writer.Write(Source);
144 | writer.WriteByte(Option);
145 |
146 | if (ps != null) WriteParameters(writer, ps);
147 | break;
148 | }
149 |
150 | return true;
151 | }
152 |
153 | void FixParameters(IList parameters)
154 | {
155 | foreach (var item in parameters)
156 | {
157 | item.Length = item.Kind switch
158 | {
159 | COTPParameterKinds.TpduSize => 1,
160 | COTPParameterKinds.SrcTsap => 2,
161 | COTPParameterKinds.DstTsap => 2,
162 | _ => item.Value switch
163 | {
164 | Byte => 1,
165 | UInt16 or Int16 => 2,
166 | UInt32 or Int32 => 4,
167 | Byte[] buf => (Byte)buf.Length,
168 | _ => throw new NotSupportedException(),
169 | },
170 | };
171 | }
172 | }
173 |
174 | void WriteParameters(Binary writer, IList parameters)
175 | {
176 | foreach (var item in parameters)
177 | {
178 | writer.WriteByte((Byte)item.Kind);
179 | writer.WriteByte(item.Length);
180 |
181 | if (item.Value is Byte b)
182 | writer.WriteByte(b);
183 | else if (item.Value is UInt16 u16)
184 | writer.Write(u16);
185 | else if (item.Value is UInt32 u32)
186 | writer.Write(u32);
187 | else if (item.Value is Byte[] buf)
188 | writer.Write((ReadOnlySpan)buf);
189 | else
190 | throw new NotSupportedException();
191 | }
192 | }
193 |
194 | /// 序列化消息
195 | /// 是否带TPKT头
196 | ///
197 | public IPacket ToPacket(Boolean withTPKT = true)
198 | {
199 | var ms = new MemoryStream();
200 | Write(ms);
201 |
202 | ms.Position = 0;
203 | var pk = new Packet(ms);
204 | if (withTPKT) return new TPKT { Data = pk }.ToPacket();
205 |
206 | return pk;
207 | }
208 | #endregion
209 |
210 | #region 参数
211 | /// 获取参数
212 | ///
213 | ///
214 | public COTPParameter? GetParameter(COTPParameterKinds kind) => Parameters?.FirstOrDefault(e => e.Kind == kind);
215 |
216 | /// 设置参数
217 | ///
218 | public void SetParameter(COTPParameter parameter)
219 | {
220 | var ps = Parameters;
221 | for (var i = 0; i < ps.Count; i++)
222 | {
223 | var pm2 = ps[i];
224 | if (pm2.Kind == parameter.Kind)
225 | {
226 | ps[i] = parameter;
227 | return;
228 | }
229 | }
230 |
231 | ps.Add(parameter);
232 | }
233 |
234 | /// 设置参数
235 | ///
236 | ///
237 | public void SetParameter(COTPParameterKinds kind, Byte value) => SetParameter(new(kind, 1, value));
238 |
239 | /// 设置参数
240 | ///
241 | ///
242 | public void SetParameter(COTPParameterKinds kind, UInt16 value) => SetParameter(new(kind, 2, value));
243 |
244 | /// 设置参数
245 | ///
246 | ///
247 | public void SetParameter(COTPParameterKinds kind, UInt32 value) => SetParameter(new(kind, 4, value));
248 | #endregion
249 |
250 | #region 方法
251 | /// 从网络流读取一个COTP帧
252 | /// 网络流
253 | ///
254 | ///
255 | public static async Task ReadAsync(Stream stream, CancellationToken cancellationToken)
256 | {
257 | using var data = await TPKT.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
258 | if (data.Length == 0) throw new InvalidDataException("No protocol data received");
259 |
260 | var cotp = new COTP();
261 | if (!cotp.Read(data)) throw new InvalidDataException("Invalid protocol data received");
262 |
263 | return cotp;
264 | }
265 |
266 | /// 从网络流读取多个帧,直到最后一个数据单元
267 | /// 网络流
268 | ///
269 | ///
270 | public static async Task ReadAllAsync(Stream stream, CancellationToken cancellationToken)
271 | {
272 | IPacket? rs = null;
273 | while (true)
274 | {
275 | var cotp = await ReadAsync(stream, cancellationToken).ConfigureAwait(false);
276 | if (rs == null)
277 | rs = cotp.Data;
278 | else if (cotp.Data != null)
279 | rs.Append(cotp.Data);
280 |
281 | if (cotp.LastDataUnit) break;
282 | }
283 |
284 | return rs;
285 | }
286 |
287 | /// 已重载
288 | ///
289 | public override String ToString() => Type == PduType.Data ? $"[{Type}] Data[{Data?.Total}]" : $"[{Type}]";
290 | #endregion
291 | }
--------------------------------------------------------------------------------
/XUnitTest/S7MessageTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using NewLife;
4 | using NewLife.Siemens.Messages;
5 | using NewLife.Siemens.Protocols;
6 | using Xunit;
7 | using NewLife.Siemens.Models;
8 |
9 | namespace XUnitTest;
10 |
11 | public class S7MessageTests
12 | {
13 | [Fact]
14 | public void Test1()
15 | {
16 | var str = "32 01 00 00 ff ff 00 08 00 00 f0 00 00 03 00 03 03 c0";
17 | var hex = str.ToHex();
18 |
19 | var msg = new S7Message();
20 |
21 | var rs = msg.Read(new MemoryStream(hex), null);
22 | Assert.True(rs);
23 |
24 | Assert.Equal(0x32, msg.ProtocolId);
25 | Assert.Equal(S7Kinds.Job, msg.Kind);
26 | Assert.Equal(0x0000, msg.Reserved);
27 | Assert.Equal(0xFFFF, msg.Sequence);
28 |
29 | Assert.Single(msg.Parameters);
30 |
31 | var pm = msg.Parameters[0] as SetupMessage;
32 | Assert.NotNull(pm);
33 | Assert.Equal(S7Functions.Setup, pm.Code);
34 | Assert.Equal(3, pm.MaxAmqCaller);
35 | Assert.Equal(3, pm.MaxAmqCallee);
36 | Assert.Equal(0x03C0, pm.PduLength);
37 | Assert.Equal(960, pm.PduLength);
38 |
39 | //Assert.Null(msg.Data);
40 |
41 | // 序列化
42 | var buf = msg.GetBytes();
43 | Assert.Equal(hex.ToHex(), buf.ToHex());
44 | }
45 |
46 | [Fact]
47 | public void Test2()
48 | {
49 | var hex = new Byte[] { 50, 1, 0, 0, 204, 193, 0, 8, 0, 0, 240, 0, 0, 1, 0, 1, 3, 192 };
50 |
51 | var msg = new S7Message();
52 |
53 | var rs = msg.Read(new MemoryStream(hex), null);
54 | Assert.True(rs);
55 |
56 | Assert.Equal(0x32, msg.ProtocolId);
57 | Assert.Equal(S7Kinds.Job, msg.Kind);
58 | Assert.Equal(0x0000, msg.Reserved);
59 | Assert.Equal(0xCCC1, msg.Sequence);
60 |
61 | Assert.Single(msg.Parameters);
62 |
63 | var pm = msg.Parameters[0] as SetupMessage;
64 | Assert.NotNull(pm);
65 | Assert.Equal(S7Functions.Setup, pm.Code);
66 | Assert.Equal(1, pm.MaxAmqCaller);
67 | Assert.Equal(1, pm.MaxAmqCallee);
68 | Assert.Equal(0x03C0, pm.PduLength);
69 | Assert.Equal(960, pm.PduLength);
70 |
71 | var pm2 = msg.GetParameter(S7Functions.Setup);
72 | Assert.NotNull(pm2);
73 | Assert.Equal(pm, pm2);
74 |
75 | //Assert.Null(msg.Data);
76 |
77 | // 序列化
78 | var buf = msg.GetBytes();
79 | Assert.Equal(hex.ToHex(), buf.ToHex());
80 | }
81 |
82 | [Fact]
83 | public void ReadVar()
84 | {
85 | var str = "32 01 00 00 00 01" +
86 | "00 0e 00 00" +
87 | // 读取请求项
88 | "04 01 12 0a 10 01 00 01 00 01 84 00 00 50";
89 | var hex = str.ToHex();
90 |
91 | var msg = new S7Message();
92 |
93 | var rs = msg.Read(hex);
94 | Assert.True(rs);
95 |
96 | Assert.Equal(0x32, msg.ProtocolId);
97 | Assert.Equal(S7Kinds.Job, msg.Kind);
98 | Assert.Equal(0x0000, msg.Reserved);
99 | Assert.Equal(1, msg.Sequence);
100 |
101 | Assert.Single(msg.Parameters);
102 |
103 | var pm = msg.Parameters[0] as ReadRequest;
104 | Assert.NotNull(pm);
105 | Assert.Equal(S7Functions.ReadVar, pm.Code);
106 | Assert.Single(pm.Items);
107 |
108 | var pm2 = msg.GetParameter(S7Functions.ReadVar);
109 | Assert.NotNull(pm2);
110 | Assert.Equal(pm, pm2);
111 |
112 | var di = pm.Items[0];
113 | Assert.Equal(0x12, di.SpecType);
114 | Assert.Equal(0x10, di.SyntaxId);
115 | Assert.Equal(1, di.TransportSize);
116 | Assert.Equal(1, di.Count);
117 | Assert.Equal(1, di.DbNumber);
118 | Assert.Equal(DataType.DataBlock, di.Area);
119 | Assert.Equal(0x50u, di.Address);
120 |
121 | //Assert.Null(msg.Data);
122 |
123 | // 序列化
124 | var buf = msg.GetBytes();
125 | Assert.Equal(hex.ToHex(), buf.ToHex());
126 | }
127 |
128 | [Fact]
129 | public void ReadVarResponse()
130 | {
131 | var str = "32 03 00 00 00 01" +
132 | // plen + dlen
133 | "00 02 00 05 " +
134 | // error code
135 | "00 00 " +
136 | // 读取请求项
137 | "04 01 " +
138 | // 数据项
139 | "ff 03 00 01 00";
140 | var hex = str.ToHex();
141 |
142 | var msg = new S7Message();
143 |
144 | var rs = msg.Read(hex);
145 | Assert.True(rs);
146 |
147 | Assert.Equal(0x32, msg.ProtocolId);
148 | Assert.Equal(S7Kinds.AckData, msg.Kind);
149 | Assert.Equal(0x0000, msg.Reserved);
150 | Assert.Equal(1, msg.Sequence);
151 |
152 | Assert.Equal(0, msg.ErrorClass);
153 | Assert.Equal(0, msg.ErrorCode);
154 |
155 | Assert.Single(msg.Parameters);
156 |
157 | var pm = msg.Parameters[0] as ReadResponse;
158 | Assert.NotNull(pm);
159 | Assert.Equal(S7Functions.ReadVar, pm.Code);
160 | Assert.Single(pm.Items);
161 |
162 | var pm2 = msg.GetParameter(S7Functions.ReadVar);
163 | Assert.NotNull(pm2);
164 | Assert.Equal(pm, pm2);
165 |
166 | var di = pm.Items[0];
167 | Assert.Equal(ReadWriteErrorCode.Success, di.Code);
168 | Assert.Equal(0x03, di.TransportSize);
169 | Assert.Single(di.Data);
170 | Assert.Equal(0x00, di.Data[0]);
171 |
172 | //Assert.NotNull(msg.Data);
173 |
174 | // 序列化
175 | var buf = msg.GetBytes();
176 | Assert.Equal(hex.ToHex(), buf.ToHex());
177 | }
178 |
179 | [Fact]
180 | public void ReadVarResponse2()
181 | {
182 | var str = "32 03 00 00 00 01" +
183 | // plen + dlen
184 | "00 02 00 06 " +
185 | // error code
186 | "00 00 " +
187 | // 读取请求项
188 | "04 01 " +
189 | // 数据项
190 | "ff 04 00 10 00 00";
191 | var hex = str.ToHex();
192 |
193 | var msg = new S7Message();
194 |
195 | var rs = msg.Read(hex);
196 | Assert.True(rs);
197 |
198 | Assert.Equal(0x32, msg.ProtocolId);
199 | Assert.Equal(S7Kinds.AckData, msg.Kind);
200 | Assert.Equal(0x0000, msg.Reserved);
201 | Assert.Equal(1, msg.Sequence);
202 |
203 | Assert.Equal(0, msg.ErrorClass);
204 | Assert.Equal(0, msg.ErrorCode);
205 |
206 | Assert.Single(msg.Parameters);
207 |
208 | var pm = msg.Parameters[0] as ReadResponse;
209 | Assert.NotNull(pm);
210 | Assert.Equal(S7Functions.ReadVar, pm.Code);
211 | Assert.Single(pm.Items);
212 |
213 | var pm2 = msg.GetParameter(S7Functions.ReadVar);
214 | Assert.NotNull(pm2);
215 | Assert.Equal(pm, pm2);
216 |
217 | var di = pm.Items[0];
218 | Assert.Equal(ReadWriteErrorCode.Success, di.Code);
219 | Assert.Equal(0x04, di.TransportSize);
220 | Assert.Equal(2, di.Data.Length);
221 | Assert.Equal(0x00, di.Data[0]);
222 |
223 | //Assert.NotNull(msg.Data);
224 |
225 | // 序列化
226 | var buf = msg.GetBytes();
227 | Assert.Equal(hex.ToHex(), buf.ToHex());
228 | }
229 |
230 | [Fact]
231 | public void WriteVar()
232 | {
233 | var str = "32 01 00 00 00 01 " +
234 | // plen + dlen
235 | "00 0e 00 05 " +
236 | // 写入请求项
237 | "05 01 12 0a 10 01 00 01 00 01 84 00 00 50 " +
238 | // 数据项
239 | "00 03 00 01 01";
240 | var hex = str.ToHex();
241 |
242 | var msg = new S7Message();
243 |
244 | var rs = msg.Read(hex);
245 | Assert.True(rs);
246 |
247 | Assert.Equal(0x32, msg.ProtocolId);
248 | Assert.Equal(S7Kinds.Job, msg.Kind);
249 | Assert.Equal(0x0000, msg.Reserved);
250 | Assert.Equal(1, msg.Sequence);
251 |
252 | Assert.Single(msg.Parameters);
253 |
254 | var pm = msg.Parameters[0] as WriteRequest;
255 | Assert.NotNull(pm);
256 | Assert.Equal(S7Functions.WriteVar, pm.Code);
257 | Assert.Single(pm.Items);
258 |
259 | var pm2 = msg.GetParameter(S7Functions.WriteVar);
260 | Assert.NotNull(pm2);
261 | Assert.Equal(pm, pm2);
262 |
263 | var ri = pm.Items[0];
264 | Assert.Equal(0x12, ri.SpecType);
265 | Assert.Equal(0x10, ri.SyntaxId);
266 | Assert.Equal(1, ri.TransportSize);
267 | Assert.Equal(1, ri.Count);
268 | Assert.Equal(1, ri.DbNumber);
269 | Assert.Equal(DataType.DataBlock, ri.Area);
270 | Assert.Equal(0x50u, ri.Address);
271 |
272 | var di = pm.DataItems[0];
273 | Assert.Equal(ReadWriteErrorCode.Reserved, di.Code);
274 | Assert.Equal(0x03, di.TransportSize);
275 | Assert.Single(di.Data);
276 | Assert.Equal(1, di.Data[0]);
277 |
278 | //Assert.Null(msg.Data);
279 |
280 | // 序列化
281 | var buf = msg.GetBytes();
282 | Assert.Equal(hex.ToHex(), buf.ToHex());
283 | }
284 |
285 | [Fact]
286 | public void WriteVarResponse()
287 | {
288 | var str = "32 03 00 00 00 01" +
289 | "00 02 00 01 " +
290 | "00 00 " +
291 | "05 01 " +
292 | "ff";
293 | var hex = str.ToHex();
294 |
295 | var msg = new S7Message();
296 |
297 | var rs = msg.Read(hex);
298 | Assert.True(rs);
299 |
300 | Assert.Equal(0x32, msg.ProtocolId);
301 | Assert.Equal(S7Kinds.AckData, msg.Kind);
302 | Assert.Equal(0x0000, msg.Reserved);
303 | Assert.Equal(1, msg.Sequence);
304 | Assert.Equal(0, msg.ErrorClass);
305 | Assert.Equal(0, msg.ErrorCode);
306 |
307 | Assert.Single(msg.Parameters);
308 |
309 | var pm = msg.Parameters[0] as WriteResponse;
310 | Assert.NotNull(pm);
311 | Assert.Equal(S7Functions.WriteVar, pm.Code);
312 | Assert.Single(pm.Items);
313 |
314 | var pm2 = msg.GetParameter(S7Functions.WriteVar);
315 | Assert.NotNull(pm2);
316 | Assert.Equal(pm, pm2);
317 |
318 | var di = pm.Items[0];
319 | Assert.Equal(ReadWriteErrorCode.Success, di.Code);
320 | //Assert.Equal(VarType.Bit, di.Type);
321 | Assert.Null(di.Data);
322 | //Assert.Equal(0x00, di.Data[0]);
323 |
324 | //Assert.NotNull(msg.Data);
325 |
326 | // 序列化
327 | var buf = msg.GetBytes();
328 | Assert.Equal(hex.ToHex(), buf.ToHex());
329 | }
330 | }
331 |
--------------------------------------------------------------------------------
/NewLife.Siemens/Protocols/S7Client.cs:
--------------------------------------------------------------------------------
1 | using System.Net.Sockets;
2 | using NewLife.Data;
3 | using NewLife.Log;
4 | using NewLife.Remoting;
5 | using NewLife.Siemens.Messages;
6 | using NewLife.Siemens.Models;
7 |
8 | namespace NewLife.Siemens.Protocols;
9 |
10 | /// S7驱动
11 | public partial class S7Client : DisposeBase, ILogFeature
12 | {
13 | #region 属性
14 | /// IP地址
15 | public String IP { get; set; }
16 |
17 | /// 端口
18 | public Int32 Port { get; set; } = 102;
19 |
20 | /// 超时时间。默认5000毫秒
21 | public Int32 Timeout { get; set; } = 5_000;
22 |
23 | /// 类型
24 | public CpuType CPU { get; set; }
25 |
26 | /// 机架号。通常为0
27 | public Int16 Rack { get; set; }
28 |
29 | /// 插槽,对于S7300-S7400通常为2,对于S7-1200和S7-1500为0
30 | public Int16 Slot { get; set; }
31 |
32 | /// 最大PDU大小
33 | public Int32 MaxPDUSize { get; private set; } = 1024;
34 |
35 | private TcpClient? _client;
36 | private NetworkStream? _stream;
37 | private Int32 _sequence;
38 | #endregion
39 |
40 | #region 构造
41 | /// 实例化
42 | ///
43 | ///
44 | ///
45 | ///
46 | ///
47 | public S7Client(CpuType cpu, String ip, Int32 port, Int16 rack = 0, Int16 slot = 0)
48 | {
49 | IP = ip;
50 | if (port > 0) Port = port;
51 |
52 | CPU = cpu;
53 | Rack = rack;
54 | Slot = slot;
55 | }
56 |
57 | /// 销毁
58 | ///
59 | protected override void Dispose(Boolean disposing)
60 | {
61 | base.Dispose(disposing);
62 |
63 | Close();
64 | }
65 | #endregion
66 |
67 | #region 连接
68 | /// 打开连接
69 | ///
70 | ///
71 | public async Task OpenAsync(CancellationToken cancellationToken = default)
72 | {
73 | var client = new TcpClient
74 | {
75 | SendTimeout = Timeout,
76 | ReceiveTimeout = Timeout
77 | };
78 |
79 | // 开启KeepAlive,避免长时间空闲时,路由器或防火墙关闭连接
80 | client.Client.SetTcpKeepAlive(true, 15_000, 15_000);
81 |
82 | await client.ConnectAsync(IP, Port).ConfigureAwait(false);
83 |
84 | var stream = client.GetStream();
85 | _stream = stream;
86 | _client = client;
87 |
88 | try
89 | {
90 | cancellationToken.ThrowIfCancellationRequested();
91 | await RequestConnection(stream, cancellationToken).ConfigureAwait(false);
92 | await SetupConnection(stream, cancellationToken).ConfigureAwait(false);
93 | }
94 | catch (Exception)
95 | {
96 | stream.Dispose();
97 | throw;
98 | }
99 | }
100 |
101 | private async Task RequestConnection(Stream stream, CancellationToken cancellationToken)
102 | {
103 | var tsap = TsapAddress.GetDefaultTsapPair(CPU, Rack, Slot);
104 | var request = new COTP
105 | {
106 | Type = PduType.ConnectionRequest,
107 | Destination = 0x00,
108 | Source = 0x01,
109 | Option = 0x00,
110 | };
111 | request.SetParameter(COTPParameterKinds.SrcTsap, tsap.Local);
112 | request.SetParameter(COTPParameterKinds.DstTsap, tsap.Remote);
113 | request.SetParameter(COTPParameterKinds.TpduSize, (Byte)0x0A);
114 |
115 | var response = await RequestAsync(stream, request, cancellationToken).ConfigureAwait(false);
116 |
117 | if (response.Type != PduType.ConnectionConfirmed)
118 | throw new InvalidDataException($"Connection request was denied (PDUType={response.Type})");
119 | }
120 |
121 | private async Task SetupConnection(Stream stream, CancellationToken cancellationToken)
122 | {
123 | var setup = new SetupMessage
124 | {
125 | MaxAmqCaller = 0x0001,
126 | MaxAmqCallee = 0x0001,
127 | PduLength = 960,
128 | };
129 |
130 | var rs = await InvokeAsync(setup, cancellationToken).ConfigureAwait(false);
131 | if (rs == null) return;
132 |
133 | if (rs is SetupMessage pm) MaxPDUSize = pm.PduLength;
134 | }
135 |
136 | /// 获取网络流,检查并具备断线重连能力
137 | ///
138 | ///
139 | private async Task GetStream(CancellationToken cancellationToken)
140 | {
141 | if (_stream != null && _client != null && _client.Connected) return _stream;
142 |
143 | await OpenAsync(cancellationToken).ConfigureAwait(false);
144 |
145 | return _stream!;
146 | }
147 |
148 | /// 关闭连接
149 | public void Close()
150 | {
151 | _client?.Close();
152 | _client = null;
153 | _stream = null;
154 | }
155 | #endregion
156 |
157 | #region 核心方法
158 | /// 发起S7请求
159 | ///
160 | ///
161 | ///
162 | public async Task RequestAsync(S7Message request, CancellationToken cancellationToken = default)
163 | {
164 | var stream = await GetStream(cancellationToken);
165 |
166 | // 设置递增的序列号
167 | if (request.Sequence == 0) request.Sequence = (UInt16)Interlocked.Increment(ref _sequence);
168 |
169 | WriteLog("=> {0}", request);
170 |
171 | var cotp = await RequestAsync(stream, request.ToCOTP(), cancellationToken).ConfigureAwait(false);
172 | if (cotp == null || cotp.Data == null) return null;
173 |
174 | var msg = new S7Message();
175 | if (!msg.Read(cotp.Data)) return null;
176 |
177 | WriteLog("<= {0}", msg);
178 |
179 | return msg;
180 | }
181 |
182 | /// 发起Job请求
183 | ///
184 | ///
185 | ///
186 | public async Task InvokeAsync(S7Parameter request, CancellationToken cancellationToken = default)
187 | {
188 | var msg = new S7Message
189 | {
190 | Kind = S7Kinds.Job,
191 | };
192 |
193 | msg.SetParameter(request);
194 |
195 | var rs = await RequestAsync(msg, cancellationToken).ConfigureAwait(false);
196 | if (rs == null) return null;
197 |
198 | return rs.Parameters?.FirstOrDefault();
199 | }
200 |
201 | /// 异步请求
202 | ///
203 | ///
204 | ///
205 | ///
206 | private async Task RequestAsync(Stream stream, COTP request, CancellationToken cancellationToken = default)
207 | {
208 | cancellationToken.ThrowIfCancellationRequested();
209 | try
210 | {
211 | if (request.Type != PduType.Data) WriteLog("=> {0}", request);
212 |
213 | var pk = request.ToPacket(true);
214 | var buf = pk.ReadBytes();
215 |
216 | using var closeOnCancellation = cancellationToken.Register(Close);
217 | //await pk.CopyToAsync(stream, cancellationToken);
218 | await stream.WriteAsync(buf, 0, buf.Length, cancellationToken);
219 | var rs = await COTP.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
220 |
221 | if (request.Type != PduType.Data) WriteLog("<= {0}", rs);
222 |
223 | return rs;
224 | }
225 | catch (Exception exc)
226 | {
227 | if (exc is InvalidDataException)
228 | {
229 | Close();
230 | }
231 |
232 | throw;
233 | }
234 | }
235 | #endregion
236 |
237 | #region 读取
238 | /// 读取多个字节
239 | ///
240 | ///
241 | ///
242 | public Byte[] ReadBytes(PLCAddress address, Int32 count)
243 | {
244 | var ms = new MemoryStream();
245 | var index = 0;
246 | while (count > 0)
247 | {
248 | // 最大PDU大小
249 | var maxToRead = Math.Min(count, MaxPDUSize - 18);
250 |
251 | var addr = (address.StartByte + index) * 8;
252 | if (address.BitNumber > 0) addr += address.BitNumber;
253 | var request = BuildRead(address.DataType, address.DbNumber, address.VarType, addr, maxToRead);
254 |
255 | // 发起请求
256 | var rs = InvokeAsync(request).ConfigureAwait(false).GetAwaiter().GetResult();
257 | if (rs is not ReadResponse res) break;
258 |
259 | if (res.Items != null)
260 | {
261 | foreach (var item in res.Items)
262 | {
263 | var code = res.Items[0].Code;
264 | if (code != ReadWriteErrorCode.Success) throw new ApiException((Int32)code, code + "");
265 |
266 | if (item.Data != null)
267 | ms.Write(item.Data, 0, item.Data.Length);
268 | }
269 | }
270 |
271 | count -= maxToRead;
272 | index += maxToRead;
273 | }
274 | return ms.ToArray();
275 | }
276 |
277 | private static ReadRequest BuildRead(DataType dataType, Int32 db, VarType varType, Int32 address, Int32 count)
278 | {
279 | var ri = new RequestItem
280 | {
281 | // S7ANY
282 | SyntaxId = 0x10,
283 | TransportSize = (Byte)(varType == VarType.Bit ? 1 : 2),
284 | Count = (UInt16)count,
285 | DbNumber = (UInt16)db,
286 | Area = dataType,
287 |
288 | Address = (UInt32)address,
289 | };
290 |
291 | var request = new ReadRequest();
292 | request.Items.Add(ri);
293 |
294 | return request;
295 | }
296 | #endregion
297 |
298 | #region 写入
299 | /// 从指定DB开始,写入多个字节
300 | ///
301 | ///
302 | public void WriteBytes(PLCAddress address, Byte[] value)
303 | {
304 | var index = 0;
305 | var count = value.Length;
306 | while (count > 0)
307 | {
308 | var pdu = Math.Min(count, MaxPDUSize - 28);
309 |
310 | var addr = (address.StartByte + index) * 8;
311 | if (address.BitNumber > 0) addr += address.BitNumber;
312 | var request = BuildWrite(address.DataType, address.DbNumber, address.VarType, addr, value, index, pdu);
313 |
314 | // 发起请求
315 | var rs = InvokeAsync(request).ConfigureAwait(false).GetAwaiter().GetResult();
316 | if (rs is not WriteResponse res) break;
317 |
318 | if (res.Items != null && res.Items.Count > 0)
319 | {
320 | var code = res.Items[0].Code;
321 | if (code != ReadWriteErrorCode.Success) throw new ApiException((Int32)code, code + "");
322 | }
323 |
324 | count -= pdu;
325 | index += pdu;
326 | }
327 | }
328 |
329 | private WriteRequest BuildWrite(DataType dataType, Int32 db, VarType varType, Int32 address, Byte[] value, Int32 offset, Int32 count)
330 | {
331 | var request = new WriteRequest();
332 | request.Items.Add(new RequestItem
333 | {
334 | SpecType = 0x12,
335 | SyntaxId = 0x10,
336 | TransportSize = (Byte)(varType == VarType.Bit ? 1 : 2),
337 | Count = (UInt16)count,
338 | DbNumber = (UInt16)db,
339 | Area = dataType,
340 | Address = (UInt32)address
341 | });
342 | request.DataItems.Add(new DataItem
343 | {
344 | TransportSize = (Byte)(varType == VarType.Bit ? 0x03 : 0x04),
345 | Data = value.ReadBytes(offset, count),
346 | });
347 |
348 | return request;
349 | }
350 | #endregion
351 |
352 | #region 日志
353 | /// 日志
354 | public ILog Log { get; set; } = Logger.Null;
355 |
356 | /// 写日志
357 | ///
358 | ///
359 | public void WriteLog(String format, params Object[] args) => Log.Info(format, args);
360 | #endregion
361 | }
362 |
--------------------------------------------------------------------------------