├── 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 | ![GitHub top language](https://img.shields.io/github/languages/top/newlifex/NewLife.Siemens?logo=github) 4 | ![GitHub License](https://img.shields.io/github/license/newlifex/NewLife.Siemens?logo=github) 5 | ![Nuget Downloads](https://img.shields.io/nuget/dt/NewLife.Siemens?logo=nuget) 6 | ![Nuget](https://img.shields.io/nuget/v/NewLife.Siemens?logo=nuget) 7 | ![Nuget (with prereleases)](https://img.shields.io/nuget/vpre/NewLife.Siemens?label=dev%20nuget&logo=nuget) 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 | ![XCode](https://newlifex.com/logo.png) 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 | ![智能大石头](https://newlifex.com/stone.jpg) 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 | --------------------------------------------------------------------------------