├── .github └── workflows │ └── dotnet.yml ├── .gitignore ├── AdsClient.sln ├── LICENSE ├── README.md ├── icon.png ├── mdsnippets.json ├── samples └── Samples │ ├── Program.cs │ ├── Samples.cs │ └── Samples.csproj ├── src ├── AdsClient │ ├── AdsClient.cs │ ├── Ams.cs │ ├── AmsSocket.cs │ ├── AmsSocketConnection.cs │ ├── CommandResponse │ │ ├── AdsAddDeviceNotificationCommandResponse.cs │ │ ├── AdsCommandResponse.cs │ │ ├── AdsDeleteDeviceNotificationCommandResponse.cs │ │ ├── AdsReadCommandResponse.cs │ │ ├── AdsReadDeviceInfoCommandResponse.cs │ │ ├── AdsReadStateCommandResponse.cs │ │ ├── AdsWriteCommandResponse.cs │ │ └── AdsWriteReadCommandResponse.cs │ ├── Commands │ │ ├── AdsAddDeviceNotificationCommand.cs │ │ ├── AdsCommand.cs │ │ ├── AdsDeleteDeviceNotificationCommand.cs │ │ ├── AdsReadCommand.cs │ │ ├── AdsReadDeviceInfoCommand.cs │ │ ├── AdsReadStateCommand.cs │ │ ├── AdsWriteCommand.cs │ │ └── AdsWriteReadCommand.cs │ ├── Common │ │ ├── AdsArrayRange.cs │ │ ├── AdsCommandEnum.cs │ │ ├── AdsCommandId.cs │ │ ├── AdsConnectionSettings.cs │ │ ├── AdsDataType.cs │ │ ├── AdsDataTypeDto.cs │ │ ├── AdsDataTypeFlags.cs │ │ ├── AdsDeviceInfo.cs │ │ ├── AdsError.cs │ │ ├── AdsException.cs │ │ ├── AdsNotification.cs │ │ ├── AdsNotificationArgs.cs │ │ ├── AdsReservedIndexGroup.cs │ │ ├── AdsReservedPort.cs │ │ ├── AdsState.cs │ │ ├── AdsSymbol.cs │ │ ├── AdsTransmissionMode.cs │ │ ├── AmsNetId.cs │ │ ├── AmsSocketResponseArgs.cs │ │ ├── Date.cs │ │ ├── IAdsConnectionSettings.cs │ │ └── Time.cs │ ├── Conversation │ │ ├── AdsReadRequest.cs │ │ ├── CreateVariableHandles │ │ │ ├── AdsCreateVariableHandlesConversation.cs │ │ │ └── AdsCreateVariableHandlesRequest.cs │ │ ├── IAdsConversation.cs │ │ ├── IAdsRequest.cs │ │ ├── ReadDataTypes │ │ │ ├── AdsDataTypeParser.cs │ │ │ └── AdsReadDataTypesConversation.cs │ │ ├── ReadMultiple │ │ │ ├── AdsReadMultipleConversation.cs │ │ │ ├── AdsReadMultipleRequest.cs │ │ │ ├── AdsReadVariableException.cs │ │ │ ├── ArrayMultiReadResult.cs │ │ │ ├── ArrayMultiReadResultFactory.cs │ │ │ ├── IMultiReadResult.cs │ │ │ ├── IReadResultFactory.cs │ │ │ ├── PooledArrayMultiReadResult.cs │ │ │ ├── PooledArrayMultiReadResultFactory.cs │ │ │ └── ReadMultipleException.cs │ │ ├── ReadSymbols │ │ │ ├── AdsReadSymbolsConversation.cs │ │ │ └── AdsSymbolParser.cs │ │ ├── ReadUploadInfo │ │ │ ├── AdsReadUploadInfoConversation.cs │ │ │ └── AdsUploadInfoDto.cs │ │ └── WriteMultiple │ │ │ ├── AdsWriteMultipleConversation.cs │ │ │ ├── AdsWriteMultipleRequest.cs │ │ │ ├── AdsWriteVariableException.cs │ │ │ └── WriteMultipleException.cs │ ├── Framework │ │ ├── EncodingExtensions.cs │ │ └── IsExternalInit.cs │ ├── Helpers │ │ ├── AdsAttribute.cs │ │ ├── AdsSerializableAttribute.cs │ │ ├── AmsHeaderHelper.cs │ │ ├── AmsMessageBuilder.cs │ │ ├── ByteArrayHelper.cs │ │ ├── Extensions.cs │ │ ├── GenericHelper.cs │ │ ├── IdGenerator.cs │ │ ├── MemberInfoHelper.cs │ │ ├── Signal.cs │ │ └── StringHelper.cs │ ├── IAmsSocket.cs │ ├── IIncomingMessageHandler.cs │ ├── Internal │ │ ├── Assertions.cs │ │ ├── EnumExtensions.cs │ │ ├── LittleEndianDeserializer.cs │ │ ├── LittleEndianSerializer.cs │ │ ├── SocketAwaitable.cs │ │ ├── SocketExtensions.cs │ │ ├── TypeExtensions.cs │ │ └── WireFormatting.cs │ ├── Special │ │ └── AdsSpecial.cs │ ├── Variables │ │ ├── ArraySegmentVariableData.cs │ │ ├── ArrayVariableData.cs │ │ ├── IVariableAddress.cs │ │ ├── IVariableAddressAndSize.cs │ │ ├── IVariableData.cs │ │ ├── MemoryVariableData.cs │ │ ├── VariableAddress.cs │ │ └── VariableAddressAndSize.cs │ └── Viscon.Communication.Ads.csproj └── AdsDumper │ ├── AdsDumper.csproj │ └── Program.cs └── test └── AdsClient ├── AdsCommandsAsyncTest.cs └── Viscon.Communication.Ads.Test.csproj /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | 3 | name: .NET 4 | on: 5 | workflow_dispatch: # Allow running the workflow manually from the GitHub UI 6 | push: 7 | branches: 8 | - 'main' # Run the workflow when pushing to the main branch 9 | pull_request: 10 | branches: 11 | - '*' # Run the workflow for all pull requests 12 | release: 13 | types: 14 | - published # Run the workflow when a new GitHub release is published 15 | 16 | env: 17 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 18 | DOTNET_NOLOGO: true 19 | NuGetDirectory: ${{ github.workspace}}/nuget 20 | 21 | defaults: 22 | run: 23 | shell: pwsh 24 | 25 | jobs: 26 | create_nuget: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v3 30 | with: 31 | fetch-depth: 0 # Get all history to allow automatic versioning 32 | 33 | - name: Install GitVersion 34 | uses: gittools/actions/gitversion/setup@v0 35 | with: 36 | versionSpec: '6.x' 37 | includePrerelease: true 38 | preferLatestVersion: true 39 | 40 | - name: Determine Version 41 | id: gitversion 42 | uses: gittools/actions/gitversion/execute@v0 43 | 44 | - name: Setup .NET 45 | uses: actions/setup-dotnet@v3 46 | 47 | - run: > 48 | dotnet pack 49 | --configuration Release 50 | /p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} 51 | /p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} 52 | /p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }} 53 | /p:PackageVersion=${{ steps.gitversion.outputs.semVer }} 54 | --output ${{ env.NuGetDirectory }} 55 | 56 | - uses: actions/upload-artifact@v3 57 | with: 58 | name: nuget 59 | if-no-files-found: error 60 | retention-days: 7 61 | path: | 62 | ${{ env.NuGetDirectory }}/*.nupkg 63 | ${{ env.NuGetDirectory }}/*.snupkg 64 | 65 | run_test: 66 | runs-on: ubuntu-latest 67 | steps: 68 | - uses: actions/checkout@v3 69 | - name: Setup .NET 70 | uses: actions/setup-dotnet@v3 71 | - name: Run tests 72 | run: dotnet test --configuration Release 73 | 74 | deploy: 75 | # Publish only when creating a GitHub Release 76 | # https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository 77 | # You can update this logic if you want to manage releases differently 78 | if: github.event_name == 'release' 79 | runs-on: ubuntu-latest 80 | needs: [ create_nuget, run_test ] 81 | steps: 82 | - uses: actions/download-artifact@v3 83 | with: 84 | name: nuget 85 | path: ${{ env.NuGetDirectory }} 86 | 87 | - name: Setup .NET Core 88 | uses: actions/setup-dotnet@v3 89 | 90 | # Publish all NuGet packages to NuGet.org 91 | # Use --skip-duplicate to prevent errors if a package with the same version already exists. 92 | # If you retry a failed workflow, already published packages will be skipped without error. 93 | - name: Publish NuGet package 94 | run: | 95 | foreach($file in (Get-ChildItem "${{ env.NuGetDirectory }}" -Recurse -Include *.nupkg)) { 96 | dotnet nuget push $file --api-key "${{ secrets.NUGET_APIKEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate 97 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | obj 2 | bin 3 | TestResults 4 | *.suo 5 | *.pdb 6 | *.csproj.user 7 | *.userprefs 8 | temp 9 | TestResult.xml 10 | testrunner 11 | 12 | .vs 13 | *.DotSettings.user 14 | -------------------------------------------------------------------------------- /AdsClient.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FAE628B5-BAFA-4F21-8174-7E47DCC63E29}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{42A63E66-DB8D-48F6-AA8A-5943080A0202}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdsDumper", "src\AdsDumper\AdsDumper.csproj", "{DA8FC5FD-E7B9-49F4-9B5D-2C6E9EE5DA7F}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Viscon.Communication.Ads", "src\AdsClient\Viscon.Communication.Ads.csproj", "{EC644FCA-386D-431B-AD82-922081D0DA45}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Viscon.Communication.Ads.Test", "test\AdsClient\Viscon.Communication.Ads.Test.csproj", "{C32CE653-CDB2-4354-9174-4EDBD82E769A}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{E40E9B0E-4921-4B81-B38A-E17D374E9986}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples", "samples\Samples\Samples.csproj", "{4F4C2736-8474-4738-9228-9FB30D11107E}" 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0568D3C3-8A19-4342-B046-FB961D6DCA57}" 21 | ProjectSection(SolutionItems) = preProject 22 | README.md = README.md 23 | EndProjectSection 24 | EndProject 25 | Global 26 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 27 | Debug|Any CPU = Debug|Any CPU 28 | Release|Any CPU = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {DA8FC5FD-E7B9-49F4-9B5D-2C6E9EE5DA7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {DA8FC5FD-E7B9-49F4-9B5D-2C6E9EE5DA7F}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {DA8FC5FD-E7B9-49F4-9B5D-2C6E9EE5DA7F}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {DA8FC5FD-E7B9-49F4-9B5D-2C6E9EE5DA7F}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {EC644FCA-386D-431B-AD82-922081D0DA45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {EC644FCA-386D-431B-AD82-922081D0DA45}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {EC644FCA-386D-431B-AD82-922081D0DA45}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {EC644FCA-386D-431B-AD82-922081D0DA45}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {C32CE653-CDB2-4354-9174-4EDBD82E769A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {C32CE653-CDB2-4354-9174-4EDBD82E769A}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {C32CE653-CDB2-4354-9174-4EDBD82E769A}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {C32CE653-CDB2-4354-9174-4EDBD82E769A}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {4F4C2736-8474-4738-9228-9FB30D11107E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {4F4C2736-8474-4738-9228-9FB30D11107E}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {4F4C2736-8474-4738-9228-9FB30D11107E}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {4F4C2736-8474-4738-9228-9FB30D11107E}.Release|Any CPU.Build.0 = Release|Any CPU 47 | EndGlobalSection 48 | GlobalSection(SolutionProperties) = preSolution 49 | HideSolutionNode = FALSE 50 | EndGlobalSection 51 | GlobalSection(NestedProjects) = preSolution 52 | {DA8FC5FD-E7B9-49F4-9B5D-2C6E9EE5DA7F} = {FAE628B5-BAFA-4F21-8174-7E47DCC63E29} 53 | {EC644FCA-386D-431B-AD82-922081D0DA45} = {FAE628B5-BAFA-4F21-8174-7E47DCC63E29} 54 | {C32CE653-CDB2-4354-9174-4EDBD82E769A} = {42A63E66-DB8D-48F6-AA8A-5943080A0202} 55 | {4F4C2736-8474-4738-9228-9FB30D11107E} = {E40E9B0E-4921-4B81-B38A-E17D374E9986} 56 | EndGlobalSection 57 | GlobalSection(ExtensibilityGlobals) = postSolution 58 | SolutionGuid = {0528BDEE-A5EE-4879-B57E-35AC1826F92B} 59 | EndGlobalSection 60 | EndGlobal 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Inando 2 | Copyright (c) 2023 Viscon Factory Intelligence B.V. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a 5 | copy of this software and associated documentation files (the "Software"), 6 | to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 20 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![license](https://img.shields.io/github/license/VisconFactoryIntelligence/AdsClient.svg)](https://github.com/VisconFactoryIntelligence/AdsClient/blob/main/LICENSE) 2 | [![.NET](https://github.com/VisconFactoryIntelligence/AdsClient/actions/workflows/dotnet.yml/badge.svg)](https://github.com/VisconFactoryIntelligence/AdsClient/actions/workflows/dotnet.yml) 3 | [![NuGet](https://img.shields.io/nuget/v/Viscon.Communication.Ads.svg)](https://www.nuget.org/packages/Viscon.Communication.Ads) 4 | 5 | This is the client implementation of the [Twincat](http://www.beckhoff.com/english.asp?twincat/default.htm) Ads protocol from [Beckhoff](http://http://www.beckhoff.com/). 6 | 7 | The implementation is in C# and targets .NET Framework 4.6.2, .NET Standard 2.0 and .NET Standard 2.1. 8 | 9 | All communication methods are async. 10 | 11 | Contributors 12 | ============ 13 | - [Inando](https://github.com/inando) 14 | - [MrCircuit](https://github.com/MrCircuit) 15 | - [mycroes](https://github.com/mycroes) 16 | - [Viscon Factory Intelligence](https://github.com/VisconFactoryIntelligence) 17 | 18 | Getting started 19 | =============== 20 | 21 | Ads Route 22 | --------- 23 | 24 | First you have to give your device/machine the permission to communicate with the Twincat Ads server by adding a route. 25 | 26 | There are different ways of doing this depending on the device. 27 | You can use the Twincat Remote Manager for example. 28 | On a CX9001 device you can connect with cerhost.exe and add a route with 29 | \Hard Disk\System\TcAmsRemoteMgr.exe 30 | (You may not to reboot after this!) 31 | 32 | *If the library is not working, an incorrect/missing route may be the problem!.* 33 | 34 | Installation 35 | ------------ 36 | You only need this library. 37 | Twincat is _not_ needed. 38 | It will not work if you have programs like system manager or PLC control running. 39 | 40 | The package is available from [NuGet](https://www.nuget.org/packages/Viscon.Communication.Ads). 41 | 42 | Examples 43 | ======== 44 | 45 | ## Connect to the PLC 46 | 47 | 48 | 49 | ```cs 50 | using var client = new AdsClient(amsNetIdSource: "10.0.0.120.1.1", ipTarget: "10.0.0.2", 51 | amsNetIdTarget: "10.0.0.2.1.1"); 52 | 53 | await client.Ams.ConnectAsync(); 54 | ``` 55 | snippet source | anchor 56 | 57 | 58 | ## Read device info 59 | 60 | 61 | 62 | ```cs 63 | AdsDeviceInfo deviceInfo = await client.ReadDeviceInfoAsync(); 64 | Console.WriteLine(deviceInfo.ToString()); 65 | ``` 66 | snippet source | anchor 67 | 68 | 69 | ## Read/Write a variable by name 70 | 71 | 72 | 73 | ```cs 74 | var varHandle = await client.GetSymhandleByNameAsync(".TestVar"); 75 | await client.WriteAsync(varHandle, 0); 76 | var value = await client.ReadAsync(varHandle); 77 | await client.ReleaseSymhandleAsync(varHandle); 78 | ``` 79 | snippet source | anchor 80 | 81 | 82 | You can also use the AdsCommands directly if you need to write directly with IndexGroup/IndexOffset 83 | 84 | ## Working with notifications 85 | 86 | 87 | 88 | ```cs 89 | client.OnNotification += (sender, e) => { Console.WriteLine(e.Notification.ToString()); }; 90 | var varHandle1 = await client.GetSymhandleByNameAsync(".VarTest1"); 91 | var varHandle2 = await client.GetSymhandleByNameAsync(".VarTest2"); 92 | var notificationHandle1 = await client.AddNotificationAsync(varHandle1, AdsTransmissionMode.Cyclic, 2000, null); 93 | var notificationHandle2 = await client.AddNotificationAsync(varHandle2, AdsTransmissionMode.OnChange, 10, null); 94 | ``` 95 | snippet source | anchor 96 | 97 | 98 | ## Simple example with most basic functions 99 | 100 | Here is a sample which shows usage of most basic functions. 101 | 102 | 103 | 104 | ```cs 105 | using Viscon.Communication.Ads; 106 | using Viscon.Communication.Ads.Common; 107 | 108 | namespace Samples; 109 | 110 | public static class Program 111 | { 112 | static async Task Main() 113 | { 114 | var timeout = Task.Delay(10000); 115 | var task = await Task.WhenAny(RunTestAsync(), timeout); 116 | if (task == timeout) 117 | { 118 | Console.Error.WriteLine("Operation timed out!"); 119 | } 120 | else 121 | { 122 | Console.WriteLine("Done!"); 123 | } 124 | } 125 | 126 | private static async Task RunTestAsync() 127 | { 128 | using var client = new AdsClient( 129 | amsNetIdSource:"192.168.5.6.1.1", 130 | ipTarget:"192.168.3.4", 131 | amsNetIdTarget:"192.168.3.4.1.1"); 132 | 133 | await client.Ams.ConnectAsync(); 134 | 135 | var deviceInfo = await client.ReadDeviceInfoAsync(); 136 | Console.WriteLine($"Device info: {deviceInfo}"); 137 | 138 | var state = await client.ReadStateAsync(); 139 | Console.WriteLine($"State: {state}"); 140 | 141 | client.OnNotification += (sender,e) => { 142 | Console.WriteLine(e.Notification.ToString()); 143 | }; 144 | 145 | var varHandle1 = await client.GetSymhandleByNameAsync(".VariableName1"); 146 | Console.WriteLine($"Variable1 handle: {varHandle1}"); 147 | 148 | var varHandle2 = await client.GetSymhandleByNameAsync(".VariableName2"); 149 | Console.WriteLine($"Variable2 handle: {varHandle2}"); 150 | 151 | var notification1Handle = await client.AddNotificationAsync( 152 | varHandle1, AdsTransmissionMode.Cyclic, 5000, null); 153 | var notification2Handle = await client.AddNotificationAsync( 154 | varHandle2, AdsTransmissionMode.OnChange, 10, null); 155 | 156 | var value = await client.ReadAsync(varHandle1); 157 | Console.WriteLine($"Value before write: {value}"); 158 | 159 | await client.WriteAsync(varHandle1, 1); 160 | Console.WriteLine("I turned something on"); 161 | 162 | value = await client.ReadAsync(varHandle1); 163 | Console.WriteLine($"Value after write: {value}"); 164 | 165 | await Task.Delay(5000); 166 | 167 | await client.WriteAsync(varHandle1, 0); 168 | Console.WriteLine("I turned something off"); 169 | 170 | Console.WriteLine("Deleting active notifications..."); 171 | await client.DeleteActiveNotificationsAsync(); 172 | } 173 | } 174 | ``` 175 | snippet source | anchor 176 | 177 | 178 | ## Using commands directly 179 | 180 | 181 | 182 | ```cs 183 | var stateCmd = new AdsReadStateCommand(); 184 | var state = (await stateCmd.RunAsync(client.Ams, CancellationToken.None)).AdsState.ToString(); 185 | Console.WriteLine($"State: {state}"); 186 | ``` 187 | snippet source | anchor 188 | 189 | 190 | ## Serialize to class 191 | 192 | It's possible to read directly to a class or write from a class. 193 | You need to set the AdsSerializable attribute on the class and the Ads attribute on the fields/properties you need. 194 | The fields without the Ads attribute are ignored. 195 | 196 | 197 | 198 | ```cs 199 | [AdsSerializable] 200 | public class TestClass 201 | { 202 | [Ads] 203 | public ushort Var1 { get; set; } 204 | 205 | [Ads] 206 | public byte Var2 { get; set; } 207 | } 208 | ``` 209 | snippet source | anchor 210 | 211 | 212 | 213 | 214 | ```cs 215 | var handle = await client.GetSymhandleByNameAsync(".Test"); 216 | var testInstance = await client.ReadAsync(handle); 217 | await client.WriteAsync(handle, testInstance); 218 | ``` 219 | snippet source | anchor 220 | 221 | 222 | This is an example struct in Twincat: 223 | ``` 224 | TYPE TestStruct : 225 | STRUCT 226 | Var1 : INT; 227 | Var2 : BYTE; 228 | END_STRUCT 229 | END_TYPE 230 | ``` 231 | 232 | ## Special functions 233 | 234 | These functions aren't documented by Beckhoff: 235 | 236 | ### Get target description 237 | 238 | 239 | 240 | ```cs 241 | var xml = await client.Special.GetTargetDescAsync(); 242 | xml = XDocument.Parse(xml).ToString(); 243 | ``` 244 | snippet source | anchor 245 | 246 | 247 | Credits, sources and inspiration 248 | ================================ 249 | * [ads-client NodeJS library by Jussi Isotalo](https://github.com/jisotalo/ads-client) 250 | * [RabbitMQ .NET Client](https://github.com/rabbitmq/rabbitmq-dotnet-client) 251 | * [Sally7](https://github.com/mycroes/Sally7) 252 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VisconFactoryIntelligence/AdsClient/16abe22dc9c2067dbd9bf94de469c874c141fcfc/icon.png -------------------------------------------------------------------------------- /mdsnippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/schema.json", 3 | "Convention": "InPlaceOverwrite" 4 | } -------------------------------------------------------------------------------- /samples/Samples/Program.cs: -------------------------------------------------------------------------------- 1 | // begin-snippet: Program 2 | using Viscon.Communication.Ads; 3 | using Viscon.Communication.Ads.Common; 4 | 5 | namespace Samples; 6 | 7 | public static class Program 8 | { 9 | static async Task Main() 10 | { 11 | var timeout = Task.Delay(10000); 12 | var task = await Task.WhenAny(RunTestAsync(), timeout); 13 | if (task == timeout) 14 | { 15 | Console.Error.WriteLine("Operation timed out!"); 16 | } 17 | else 18 | { 19 | Console.WriteLine("Done!"); 20 | } 21 | } 22 | 23 | private static async Task RunTestAsync() 24 | { 25 | using var client = new AdsClient( 26 | amsNetIdSource:"192.168.5.6.1.1", 27 | ipTarget:"192.168.3.4", 28 | amsNetIdTarget:"192.168.3.4.1.1"); 29 | 30 | await client.Ams.ConnectAsync(); 31 | 32 | var deviceInfo = await client.ReadDeviceInfoAsync(); 33 | Console.WriteLine($"Device info: {deviceInfo}"); 34 | 35 | var state = await client.ReadStateAsync(); 36 | Console.WriteLine($"State: {state}"); 37 | 38 | client.OnNotification += (sender,e) => { 39 | Console.WriteLine(e.Notification.ToString()); 40 | }; 41 | 42 | var varHandle1 = await client.GetSymhandleByNameAsync(".VariableName1"); 43 | Console.WriteLine($"Variable1 handle: {varHandle1}"); 44 | 45 | var varHandle2 = await client.GetSymhandleByNameAsync(".VariableName2"); 46 | Console.WriteLine($"Variable2 handle: {varHandle2}"); 47 | 48 | var notification1Handle = await client.AddNotificationAsync( 49 | varHandle1, AdsTransmissionMode.Cyclic, 5000, null); 50 | var notification2Handle = await client.AddNotificationAsync( 51 | varHandle2, AdsTransmissionMode.OnChange, 10, null); 52 | 53 | var value = await client.ReadAsync(varHandle1); 54 | Console.WriteLine($"Value before write: {value}"); 55 | 56 | await client.WriteAsync(varHandle1, 1); 57 | Console.WriteLine("I turned something on"); 58 | 59 | value = await client.ReadAsync(varHandle1); 60 | Console.WriteLine($"Value after write: {value}"); 61 | 62 | await Task.Delay(5000); 63 | 64 | await client.WriteAsync(varHandle1, 0); 65 | Console.WriteLine("I turned something off"); 66 | 67 | Console.WriteLine("Deleting active notifications..."); 68 | await client.DeleteActiveNotificationsAsync(); 69 | } 70 | } 71 | // end-snippet -------------------------------------------------------------------------------- /samples/Samples/Samples.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Linq; 2 | using Viscon.Communication.Ads; 3 | using Viscon.Communication.Ads.Commands; 4 | using Viscon.Communication.Ads.Common; 5 | using Viscon.Communication.Ads.Helpers; 6 | 7 | namespace Samples; 8 | 9 | public class Samples 10 | { 11 | public async Task Sample() 12 | { 13 | // begin-snippet: Connect 14 | using var client = new AdsClient(amsNetIdSource: "10.0.0.120.1.1", ipTarget: "10.0.0.2", 15 | amsNetIdTarget: "10.0.0.2.1.1"); 16 | 17 | await client.Ams.ConnectAsync(); 18 | // end-snippet 19 | 20 | // begin-snippet: ReadDeviceInfoAsync 21 | AdsDeviceInfo deviceInfo = await client.ReadDeviceInfoAsync(); 22 | Console.WriteLine(deviceInfo.ToString()); 23 | // end-snippet 24 | 25 | // begin-snippet: ReadWriteVariableByName 26 | var varHandle = await client.GetSymhandleByNameAsync(".TestVar"); 27 | await client.WriteAsync(varHandle, 0); 28 | var value = await client.ReadAsync(varHandle); 29 | await client.ReleaseSymhandleAsync(varHandle); 30 | // end-snippet 31 | 32 | // begin-snippet: WorkingWithNotifications 33 | client.OnNotification += (sender, e) => { Console.WriteLine(e.Notification.ToString()); }; 34 | var varHandle1 = await client.GetSymhandleByNameAsync(".VarTest1"); 35 | var varHandle2 = await client.GetSymhandleByNameAsync(".VarTest2"); 36 | var notificationHandle1 = await client.AddNotificationAsync(varHandle1, AdsTransmissionMode.Cyclic, 2000, null); 37 | var notificationHandle2 = await client.AddNotificationAsync(varHandle2, AdsTransmissionMode.OnChange, 10, null); 38 | // end-snippet 39 | 40 | // begin-snippet: UsingCommands 41 | var stateCmd = new AdsReadStateCommand(); 42 | var state = (await stateCmd.RunAsync(client.Ams, CancellationToken.None)).AdsState.ToString(); 43 | Console.WriteLine($"State: {state}"); 44 | // end-snippet 45 | 46 | // begin-snippet: ReadTestClass 47 | var handle = await client.GetSymhandleByNameAsync(".Test"); 48 | var testInstance = await client.ReadAsync(handle); 49 | await client.WriteAsync(handle, testInstance); 50 | // end-snippet 51 | 52 | // begin-snippet: GetTargetDesc 53 | var xml = await client.Special.GetTargetDescAsync(); 54 | xml = XDocument.Parse(xml).ToString(); 55 | // end-snippet 56 | } 57 | 58 | // begin-snippet: TestClass 59 | [AdsSerializable] 60 | public class TestClass 61 | { 62 | [Ads] 63 | public ushort Var1 { get; set; } 64 | 65 | [Ads] 66 | public byte Var2 { get; set; } 67 | } 68 | // end-snippet 69 | } -------------------------------------------------------------------------------- /samples/Samples/Samples.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | exe 6 | enable 7 | enable 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/AdsClient/Ams.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Viscon.Communication.Ads.Common; 10 | using Viscon.Communication.Ads.Conversation; 11 | using Viscon.Communication.Ads.Helpers; 12 | using Viscon.Communication.Ads.Internal; 13 | 14 | namespace Viscon.Communication.Ads 15 | { 16 | 17 | public class Ams : IDisposable 18 | { 19 | private readonly IdGenerator invokeIdGenerator = new(); 20 | private readonly Signal sendSignal = new(); 21 | 22 | private readonly ConcurrentDictionary> pendingInvocations = new(); 23 | 24 | public Ams(IAmsSocket amsSocket) 25 | { 26 | AmsSocket = amsSocket; 27 | NotificationRequests = new List(); 28 | } 29 | 30 | public IAmsSocket AmsSocket { get; } 31 | 32 | internal ushort AmsPortTarget { get; set; } 33 | internal AmsNetId AmsNetIdTarget { get; set; } 34 | internal AmsNetId AmsNetIdSource { get; set; } 35 | internal List NotificationRequests; 36 | 37 | 38 | /// 39 | /// Ams source port. Default of 32905 40 | /// 41 | public ushort AmsPortSource { get; set; } = 32905; 42 | 43 | public Task ConnectAsync(CancellationToken cancellationToken = default) 44 | { 45 | return AmsSocket.ConnectAsync(new MessageHandler(this), cancellationToken); 46 | } 47 | 48 | public void Dispose() 49 | { 50 | CloseConnection(); 51 | (AmsSocket as IDisposable)?.Dispose(); 52 | sendSignal.Dispose(); 53 | } 54 | 55 | private void CloseConnection() 56 | { 57 | AmsSocket.Close(); 58 | } 59 | 60 | internal event AdsNotificationDelegate OnNotification; 61 | 62 | internal Task PerformRequestAsync(IAdsConversation conversation, 63 | CancellationToken cancellationToken) where TRequest : struct, IAdsRequest => 64 | PerformRequestAsync(conversation, AmsPortTarget, cancellationToken); 65 | 66 | internal async Task PerformRequestAsync( 67 | IAdsConversation conversation, ushort amsPortTarget, CancellationToken cancellationToken) where TRequest : struct, IAdsRequest 68 | { 69 | static void WriteRequest(Span span, ref TRequest request, int requestedLength, Ams ams, ushort amsPortTarget, uint invokeId) 70 | { 71 | var len = request.BuildRequest(span.Slice(AmsHeaderHelper.AmsTcpHeaderSize + AmsHeaderHelper.AmsHeaderSize)); 72 | Debug.Assert(len == requestedLength, $"Serialized to wrong size, expect {requestedLength}, actual {len}"); 73 | 74 | AmsMessageBuilder.WriteHeader(span, ams, request.Command, amsPortTarget, invokeId, len); 75 | } 76 | 77 | static TResult HandleResponse(IAdsConversation conversation, ReadOnlySpan span) 78 | { 79 | var offset = WireFormatting.ReadUInt32(span.Slice(AmsHeaderHelper.AmsHeaderSize), out var errorCode); 80 | 81 | // Needs some work on returning the buffer in case of exception. 82 | 83 | if (errorCode != 0) throw new AdsException(errorCode); 84 | 85 | WireFormatting.ReadInt32(span.Slice(AmsHeaderHelper.AmsDataLengthOffset), out var dataLength); 86 | 87 | // Error is already processed, so omit it from returned data. 88 | return conversation.ParseResponse(span.Slice(AmsHeaderHelper.AmsHeaderSize + offset, 89 | dataLength - offset)); 90 | } 91 | 92 | if (!AmsSocket.Connected) throw new InvalidOperationException("Not connected to PLC."); 93 | 94 | var tcs = new TaskCompletionSource(); 95 | using var cancelTcs = cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)); 96 | 97 | uint invokeId; 98 | do 99 | { 100 | invokeId = invokeIdGenerator.Next(); 101 | } while (!pendingInvocations.TryAdd(invokeId, tcs)); 102 | 103 | using var cancelPendingInvocation = 104 | cancellationToken.Register(() => pendingInvocations.TryRemove(invokeId, out _)); 105 | 106 | try 107 | { 108 | // Avoid message building if already cancelled. 109 | cancellationToken.ThrowIfCancellationRequested(); 110 | 111 | var request = conversation.BuildRequest(); 112 | var requestedLength = request.GetRequestLength(); 113 | var buffer = ArrayPool.Shared.Rent(AmsHeaderHelper.AmsTcpHeaderSize + 114 | AmsHeaderHelper.AmsHeaderSize + requestedLength); 115 | try 116 | { 117 | WriteRequest(buffer, ref request, requestedLength, this, amsPortTarget, invokeId); 118 | 119 | _ = await sendSignal.WaitAsync(cancellationToken).ConfigureAwait(false); 120 | try 121 | { 122 | // Avoid request sending if already cancelled. Some time might have elapsed waiting for the signal. 123 | cancellationToken.ThrowIfCancellationRequested(); 124 | await AmsSocket.SendAsync(new ArraySegment(buffer, 0, 125 | AmsHeaderHelper.AmsTcpHeaderSize + AmsHeaderHelper.AmsHeaderSize + requestedLength)) 126 | .ConfigureAwait(false); 127 | } 128 | finally 129 | { 130 | if (!sendSignal.TryRelease()) 131 | { 132 | throw new Exception("Failed to release the send signal."); 133 | } 134 | } 135 | } 136 | finally 137 | { 138 | ArrayPool.Shared.Return(buffer); 139 | } 140 | } 141 | catch 142 | { 143 | pendingInvocations.TryRemove(invokeId, out _); 144 | throw; 145 | } 146 | 147 | var responseBytes = await tcs.Task.ConfigureAwait(false); 148 | cancellationToken.ThrowIfCancellationRequested(); 149 | 150 | return HandleResponse(conversation, responseBytes); 151 | } 152 | 153 | private void HandleException(Exception exception) 154 | { 155 | foreach (var id in pendingInvocations.Keys) 156 | { 157 | if (pendingInvocations.TryRemove(id, out var tcs)) 158 | { 159 | tcs.TrySetException(exception); 160 | } 161 | } 162 | 163 | CloseConnection(); 164 | } 165 | 166 | private void HandleMessage(byte[] message) 167 | { 168 | uint amsErrorCode = AmsHeaderHelper.GetErrorCode(message); 169 | uint invokeId = AmsHeaderHelper.GetInvokeId(message); 170 | bool isNotification = (AmsHeaderHelper.GetCommandId(message) == AdsCommandId.DeviceNotification); 171 | 172 | if (AmsPortTarget != AmsHeaderHelper.GetAmsPortSource(message)) return; 173 | if (!AmsNetIdTarget.Bytes.SequenceEqual(AmsHeaderHelper.GetAmsNetIdSource(message))) return; 174 | 175 | //If notification then just start the callback 176 | if (isNotification && (OnNotification != null)) 177 | { 178 | var notifications = AdsNotification.GetNotifications(message); 179 | foreach (var notification in notifications) 180 | { 181 | var notificationRequest = NotificationRequests.FirstOrDefault(n => n.NotificationHandle == notification.NotificationHandle); 182 | if (notificationRequest != null) 183 | { 184 | notificationRequest.ByteValue = notification.ByteValue; 185 | 186 | OnNotification(null, new AdsNotificationArgs(notificationRequest)); 187 | } 188 | } 189 | } 190 | 191 | //If not a notification then find the original command and call async callback 192 | if (!isNotification) 193 | { 194 | if (!pendingInvocations.TryRemove(invokeId, out var adsCommandResult)) 195 | { 196 | // Unknown or timed-out request 197 | return; 198 | } 199 | 200 | if (amsErrorCode > 0) 201 | { 202 | adsCommandResult.TrySetException(new AdsException(amsErrorCode)); 203 | } 204 | else 205 | { 206 | adsCommandResult.TrySetResult(message); 207 | } 208 | } 209 | } 210 | 211 | private class MessageHandler : IIncomingMessageHandler 212 | { 213 | private readonly Ams ams; 214 | 215 | public MessageHandler(Ams ams) => this.ams = ams; 216 | 217 | public void HandleException(Exception exception) 218 | { 219 | ams.HandleException(exception); 220 | } 221 | 222 | public void HandleMessage(byte[] message) 223 | { 224 | ams.HandleMessage(message); 225 | } 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/AdsClient/AmsSocket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Sockets; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Viscon.Communication.Ads.Internal; 6 | 7 | namespace Viscon.Communication.Ads 8 | { 9 | internal class AmsSocket : IAmsSocket, IDisposable 10 | { 11 | public AmsSocket(string host, int port = 48898) 12 | { 13 | Host = host; 14 | Port = port; 15 | 16 | TcpClient = new TcpClient { NoDelay = true }; 17 | } 18 | 19 | public TcpClient TcpClient { get; } 20 | public Socket Socket => TcpClient.Client; 21 | 22 | public string Host { get; } 23 | public int Port { get; } 24 | 25 | /// 26 | public bool Connected => TcpClient.Connected; 27 | 28 | private AmsSocketConnection connection; 29 | private SocketAwaitable socketAwaitable; 30 | 31 | public void Close() 32 | { 33 | connection?.Close(); 34 | } 35 | 36 | public async Task ConnectAsync(IIncomingMessageHandler messageHandler, CancellationToken cancellationToken = default) 37 | { 38 | if (connection is not null) throw new InvalidOperationException("Connection was already established."); 39 | 40 | using (cancellationToken.Register(state => ((Socket)state).Close(), Socket)) 41 | { 42 | try 43 | { 44 | await TcpClient.ConnectAsync(Host, Port).ConfigureAwait(false); 45 | } 46 | catch (Exception) when (cancellationToken.IsCancellationRequested) 47 | { 48 | // The exception handling is quite generic, but exceptions thrown differ across target frameworks. 49 | // (See https://stackoverflow.com/a/66656805/1085457) 50 | // This is probably not something to worry about, since apparently cancellation was requested anyway. 51 | cancellationToken.ThrowIfCancellationRequested(); 52 | } 53 | } 54 | 55 | connection = new AmsSocketConnection(TcpClient.Client, messageHandler); 56 | } 57 | 58 | public async Task SendAsync(byte[] message) 59 | { 60 | var sa = Interlocked.Exchange(ref socketAwaitable, null) ?? new SocketAwaitable(); 61 | sa.SetBuffer(message, 0, message.Length); 62 | 63 | await Socket.SendAwaitable(sa); 64 | 65 | if (Interlocked.CompareExchange(ref socketAwaitable, sa, null) != null) 66 | { 67 | sa.Dispose(); 68 | } 69 | } 70 | 71 | public async Task SendAsync(ArraySegment buffer) 72 | { 73 | var sa = Interlocked.Exchange(ref socketAwaitable, null) ?? new SocketAwaitable(); 74 | sa.SetBuffer(buffer.Array, buffer.Offset, buffer.Count); 75 | 76 | await Socket.SendAwaitable(sa); 77 | 78 | if (Interlocked.CompareExchange(ref socketAwaitable, sa, null) != null) 79 | { 80 | sa.Dispose(); 81 | } 82 | } 83 | 84 | void IDisposable.Dispose() 85 | { 86 | Close(); 87 | TcpClient?.Dispose(); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/AdsClient/AmsSocketConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Sockets; 3 | using System.Threading.Tasks; 4 | using Viscon.Communication.Ads.Helpers; 5 | using Viscon.Communication.Ads.Internal; 6 | 7 | namespace Viscon.Communication.Ads; 8 | 9 | internal sealed class AmsSocketConnection 10 | { 11 | private const int ReceiveTaskTimeout = 3000; 12 | 13 | private readonly Socket socket; 14 | private readonly IIncomingMessageHandler messageHandler; 15 | private readonly Task receiveTask; 16 | private readonly SocketAwaitable socketAwaitable = new SocketAwaitable(); 17 | private readonly byte[] headerBuffer = new byte[AmsHeaderHelper.AmsTcpHeaderSize]; 18 | 19 | public AmsSocketConnection(Socket socket, IIncomingMessageHandler messageHandler) 20 | { 21 | this.socket = socket; 22 | this.messageHandler = messageHandler; 23 | receiveTask = ReceiveLoop(); 24 | } 25 | 26 | private volatile bool closed; 27 | 28 | public void Close() 29 | { 30 | closed = true; 31 | socket.Shutdown(SocketShutdown.Both); 32 | socket.Close(); 33 | 34 | receiveTask.Wait(ReceiveTaskTimeout); 35 | socketAwaitable.Dispose(); 36 | } 37 | 38 | private async Task GetAmsMessage(byte[] tcpHeader) 39 | { 40 | uint responseLength = AmsHeaderHelper.GetResponseLength(tcpHeader); 41 | byte[] response = new byte[responseLength]; 42 | 43 | await GetMessage(response); 44 | 45 | return response; 46 | } 47 | 48 | private Task GetMessage(byte[] response) 49 | { 50 | return ReceiveAsync(response); 51 | } 52 | 53 | private async Task Listen() 54 | { 55 | try 56 | { 57 | var buffer = await ListenForHeader(); 58 | // If a ams header is received, then read the rest 59 | try 60 | { 61 | byte[] response = await GetAmsMessage(buffer); 62 | 63 | #if DEBUG_AMS 64 | Debug.WriteLine("Received bytes: " + ByteArrayHelper.ByteArrayToTestString(buffer) + ',' + 65 | ByteArrayHelper.ByteArrayToTestString(response)); 66 | #endif 67 | 68 | messageHandler.HandleMessage(response); 69 | } 70 | catch (Exception ex) 71 | { 72 | messageHandler.HandleException(ex); 73 | } 74 | } 75 | catch (Exception ex) 76 | { 77 | if (!ReferenceEquals(ex.GetType(), typeof(ObjectDisposedException))) throw; 78 | } 79 | } 80 | 81 | private async Task ListenForHeader() 82 | { 83 | await ReceiveAsync(headerBuffer); 84 | return headerBuffer; 85 | } 86 | 87 | private async Task ReceiveAsync(byte[] buffer) 88 | { 89 | var sa = socketAwaitable; 90 | sa.SetBuffer(buffer, 0, buffer.Length); 91 | 92 | do 93 | { 94 | sa.SetBuffer(sa.Offset + sa.BytesTransferred, sa.Count - sa.BytesTransferred); 95 | await socket.ReceiveAwaitable(sa); 96 | 97 | if (sa.BytesTransferred == 0) 98 | { 99 | messageHandler.HandleException(new Exception("Remote host closed the connection.")); 100 | Close(); 101 | } 102 | } while (socketAwaitable.BytesTransferred != buffer.Length); 103 | } 104 | 105 | private async Task ReceiveLoop() 106 | { 107 | await Task.Yield(); 108 | 109 | while (!closed) 110 | { 111 | await Listen(); 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /src/AdsClient/CommandResponse/AdsAddDeviceNotificationCommandResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.CommandResponse 4 | { 5 | public class AdsAddDeviceNotificationCommandResponse : AdsCommandResponse 6 | { 7 | private uint notificationHandle; 8 | public uint NotificationHandle 9 | { 10 | get { return notificationHandle; } 11 | } 12 | 13 | 14 | protected override void AdsResponseIsChanged() 15 | { 16 | notificationHandle = BitConverter.ToUInt32(this.AdsResponse, 4); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/AdsClient/CommandResponse/AdsCommandResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.CommandResponse 4 | { 5 | public class AdsCommandResponse 6 | { 7 | public AdsCommandResponse() 8 | { 9 | } 10 | 11 | internal byte[] AdsResponse { get; set; } 12 | 13 | internal uint CommandInvokeId { get; set; } 14 | 15 | internal void SetResponse(byte[] adsresponseInclAmsHeader) 16 | { 17 | //32 amsheader + data 18 | int datalength = BitConverter.ToInt32(adsresponseInclAmsHeader, 20); 19 | 20 | this.AdsResponse = new byte[datalength]; 21 | Array.Copy(adsresponseInclAmsHeader, 32, this.AdsResponse, 0, datalength); 22 | 23 | errorCode = GetErrorCode(); 24 | 25 | AdsResponseIsChanged(); 26 | } 27 | 28 | protected virtual void AdsResponseIsChanged() 29 | { 30 | } 31 | 32 | public void ProcessResponse() => AdsResponseIsChanged(); 33 | 34 | private uint errorCode; 35 | public uint AdsErrorCode 36 | { 37 | get { return errorCode; } 38 | } 39 | 40 | internal uint AmsErrorCode 41 | { 42 | get { return errorCode; } 43 | set { errorCode = value; } 44 | } 45 | 46 | protected virtual uint GetErrorCode() 47 | { 48 | return BitConverter.ToUInt32(AdsResponse, 0); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/AdsClient/CommandResponse/AdsDeleteDeviceNotificationCommandResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Viscon.Communication.Ads.CommandResponse 2 | { 3 | public class AdsDeleteDeviceNotificationCommandResponse : AdsCommandResponse 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/AdsClient/CommandResponse/AdsReadCommandResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.CommandResponse 4 | { 5 | public class AdsReadCommandResponse : AdsCommandResponse 6 | { 7 | private byte[] data; 8 | public byte[] Data 9 | { 10 | get { return data; } 11 | } 12 | 13 | 14 | protected override void AdsResponseIsChanged() 15 | { 16 | uint dataLength = BitConverter.ToUInt32(this.AdsResponse, 4); 17 | data = new byte[dataLength]; 18 | Array.Copy(AdsResponse, 8, data, 0, (int)dataLength); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/AdsClient/CommandResponse/AdsReadDeviceInfoCommandResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Viscon.Communication.Ads.Common; 3 | using Viscon.Communication.Ads.Helpers; 4 | 5 | namespace Viscon.Communication.Ads.CommandResponse 6 | { 7 | public class AdsReadDeviceInfoCommandResponse : AdsCommandResponse 8 | { 9 | private AdsDeviceInfo adsDeviceInfo; 10 | public AdsDeviceInfo AdsDeviceInfo 11 | { 12 | get { return adsDeviceInfo; } 13 | } 14 | 15 | protected override void AdsResponseIsChanged() 16 | { 17 | adsDeviceInfo = new AdsDeviceInfo(); 18 | adsDeviceInfo.MajorVersion = this.AdsResponse[4]; 19 | adsDeviceInfo.MinorVersion = this.AdsResponse[5]; 20 | adsDeviceInfo.VersionBuild = BitConverter.ToUInt16(this.AdsResponse, 6); 21 | var deviceNameArray = new byte[16]; 22 | Array.Copy(this.AdsResponse, 8, deviceNameArray, 0, 16); 23 | adsDeviceInfo.DeviceName = ByteArrayHelper.ByteArrayToString(deviceNameArray); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/AdsClient/CommandResponse/AdsReadStateCommandResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Viscon.Communication.Ads.Common; 3 | 4 | namespace Viscon.Communication.Ads.CommandResponse 5 | { 6 | public class AdsReadStateCommandResponse : AdsCommandResponse 7 | { 8 | private AdsState adsState; 9 | public AdsState AdsState 10 | { 11 | get { return adsState; } 12 | } 13 | 14 | protected override void AdsResponseIsChanged() 15 | { 16 | adsState = new AdsState(); 17 | adsState.State = BitConverter.ToUInt16(this.AdsResponse, 4); 18 | adsState.DeviceState = BitConverter.ToUInt16(this.AdsResponse, 6); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/AdsClient/CommandResponse/AdsWriteCommandResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Viscon.Communication.Ads.CommandResponse 2 | { 3 | public class AdsWriteCommandResponse : AdsCommandResponse 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/AdsClient/CommandResponse/AdsWriteReadCommandResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.CommandResponse 4 | { 5 | public class AdsWriteReadCommandResponse : AdsCommandResponse 6 | { 7 | private byte[] data; 8 | public byte[] Data 9 | { 10 | get { return data; } 11 | } 12 | 13 | 14 | protected override void AdsResponseIsChanged() 15 | { 16 | uint dataLength = BitConverter.ToUInt16(this.AdsResponse, 4); 17 | data = new byte[dataLength]; 18 | Array.Copy(AdsResponse, 8, data, 0, (int)dataLength); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/AdsClient/Commands/AdsAddDeviceNotificationCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Viscon.Communication.Ads.CommandResponse; 5 | using Viscon.Communication.Ads.Common; 6 | 7 | namespace Viscon.Communication.Ads.Commands 8 | { 9 | public class AdsAddDeviceNotificationCommand : AdsCommand 10 | { 11 | public AdsAddDeviceNotificationCommand(uint indexGroup, uint indexOffset, uint readLength, AdsTransmissionMode transmissionMode) 12 | : base(AdsCommandId.AddDeviceNotification) 13 | { 14 | this.readLength = readLength; 15 | this.indexGroup = indexGroup; 16 | this.indexOffset = indexOffset; 17 | this.transmissionMode = transmissionMode; 18 | this.notification = new AdsNotification(); 19 | 20 | } 21 | 22 | private AdsNotification notification; 23 | public AdsNotification Notification 24 | { 25 | get { return notification; } 26 | } 27 | 28 | public object UserData 29 | { 30 | get { return notification.UserData; } 31 | set { notification.UserData = value; } 32 | } 33 | 34 | public Type TypeOfValue 35 | { 36 | get { return notification.TypeOfValue; } 37 | set { notification.TypeOfValue = value; } 38 | } 39 | 40 | private AdsTransmissionMode transmissionMode; 41 | private uint readLength; 42 | private uint indexOffset; 43 | private uint indexGroup; 44 | 45 | public uint MaxDelay { get; set; } 46 | 47 | private uint cycleTime; 48 | public uint CycleTime 49 | { 50 | get { return cycleTime/10000; } 51 | set { cycleTime = value*10000; } 52 | } 53 | 54 | internal override IEnumerable GetBytes() 55 | { 56 | IEnumerable data = BitConverter.GetBytes(indexGroup); 57 | data = data.Concat(BitConverter.GetBytes(indexOffset)); 58 | data = data.Concat(BitConverter.GetBytes(readLength)); 59 | data = data.Concat(BitConverter.GetBytes((uint)transmissionMode)); 60 | data = data.Concat(BitConverter.GetBytes(MaxDelay)); 61 | data = data.Concat(BitConverter.GetBytes(cycleTime)); 62 | data = data.Concat(BitConverter.GetBytes((UInt64)0)); 63 | data = data.Concat(BitConverter.GetBytes((UInt64)0)); 64 | return data; 65 | } 66 | 67 | protected override void RunBefore(Ams ams) 68 | { 69 | ams.NotificationRequests.Add(Notification); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/AdsClient/Commands/AdsCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Viscon.Communication.Ads.CommandResponse; 7 | using Viscon.Communication.Ads.Common; 8 | using Viscon.Communication.Ads.Conversation; 9 | 10 | namespace Viscon.Communication.Ads.Commands 11 | { 12 | public abstract class AdsCommand where TResponse : AdsCommandResponse, new() 13 | { 14 | public AdsCommand(ushort commandId) 15 | { 16 | this.commandId = commandId; 17 | } 18 | 19 | 20 | private ushort commandId; 21 | public ushort CommandId 22 | { 23 | get { return commandId; } 24 | } 25 | 26 | public ushort? AmsPortTargetOverride { get; set; } 27 | 28 | protected virtual void RunBefore(Ams ams) 29 | { 30 | } 31 | 32 | protected virtual void RunAfter(Ams ams) 33 | { 34 | } 35 | 36 | public async Task RunAsync(Ams ams, CancellationToken cancellationToken) 37 | { 38 | RunBefore(ams); 39 | var result = await ams.PerformRequestAsync(new AdsCommandConversation(this), 40 | AmsPortTargetOverride ?? ams.AmsPortTarget, cancellationToken).ConfigureAwait(false); 41 | RunAfter(ams); 42 | 43 | return result; 44 | } 45 | 46 | internal abstract IEnumerable GetBytes(); 47 | 48 | private readonly struct AdsCommandRequest : IAdsRequest 49 | { 50 | private readonly AdsCommand command; 51 | private readonly Lazy> data; 52 | 53 | public AdsCommandRequest(AdsCommand command) 54 | { 55 | this.command = command; 56 | data = new Lazy>(command.GetBytes); 57 | } 58 | 59 | 60 | public AdsCommandEnum Command => (AdsCommandEnum)command.CommandId; 61 | 62 | public int GetRequestLength() 63 | { 64 | return data.Value.Count(); 65 | } 66 | 67 | public int BuildRequest(Span span) 68 | { 69 | var i = 0; 70 | foreach (var b in data.Value) 71 | { 72 | span[i] = b; 73 | i++; 74 | } 75 | 76 | return i; 77 | } 78 | } 79 | 80 | private class AdsCommandConversation : IAdsConversation 81 | { 82 | public AdsCommand Command { get; } 83 | 84 | public AdsCommandConversation(AdsCommand command) 85 | { 86 | Command = command; 87 | } 88 | 89 | public AdsCommandRequest BuildRequest() 90 | { 91 | return new AdsCommandRequest(Command); 92 | } 93 | 94 | public TResponse ParseResponse(ReadOnlySpan buffer) 95 | { 96 | var response = new TResponse(); 97 | 98 | // AdsCommandResponse expects [uint error, ...data] 99 | // Error is already checked by PerformRequestAsync, leaving 0 here. 100 | var target = new byte[4 + buffer.Length]; 101 | var targetSpan = target.AsSpan(); 102 | buffer.CopyTo(targetSpan.Slice(4)); 103 | 104 | response.AdsResponse = target; 105 | response.ProcessResponse(); 106 | 107 | return response; 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/AdsClient/Commands/AdsDeleteDeviceNotificationCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Viscon.Communication.Ads.CommandResponse; 5 | using Viscon.Communication.Ads.Common; 6 | 7 | namespace Viscon.Communication.Ads.Commands 8 | { 9 | public class AdsDeleteDeviceNotificationCommand : AdsCommand 10 | { 11 | public AdsDeleteDeviceNotificationCommand(uint notificationHandle) 12 | : base(AdsCommandId.DeleteDeviceNotification) 13 | { 14 | 15 | this.notificationHandle = notificationHandle; 16 | } 17 | 18 | private uint notificationHandle; 19 | 20 | internal override IEnumerable GetBytes() 21 | { 22 | IEnumerable data = BitConverter.GetBytes(notificationHandle); 23 | return data; 24 | } 25 | 26 | protected override void RunAfter(Ams ams) 27 | { 28 | var notification = ams.NotificationRequests.FirstOrDefault(n => n.NotificationHandle == notificationHandle); 29 | if (notification != null) 30 | ams.NotificationRequests.Remove(notification); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/AdsClient/Commands/AdsReadCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Viscon.Communication.Ads.CommandResponse; 5 | using Viscon.Communication.Ads.Common; 6 | 7 | namespace Viscon.Communication.Ads.Commands 8 | { 9 | internal class AdsReadCommand : AdsCommand 10 | { 11 | public AdsReadCommand(uint indexGroup, uint indexOffset, uint readLength) 12 | : base(AdsCommandId.Read) 13 | { 14 | this.readLength = readLength; 15 | this.indexGroup = indexGroup; 16 | this.indexOffset = indexOffset; 17 | } 18 | 19 | private uint readLength; 20 | private uint indexOffset; 21 | private uint indexGroup; 22 | 23 | internal override IEnumerable GetBytes() 24 | { 25 | IEnumerable data = BitConverter.GetBytes(indexGroup); 26 | data = data.Concat(BitConverter.GetBytes(indexOffset)); 27 | data = data.Concat(BitConverter.GetBytes(readLength)); 28 | 29 | return data; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/AdsClient/Commands/AdsReadDeviceInfoCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Viscon.Communication.Ads.CommandResponse; 3 | using Viscon.Communication.Ads.Common; 4 | 5 | namespace Viscon.Communication.Ads.Commands 6 | { 7 | public class AdsReadDeviceInfoCommand : AdsCommand 8 | { 9 | 10 | public AdsReadDeviceInfoCommand() 11 | : base(AdsCommandId.ReadDeviceInfo) 12 | { 13 | 14 | } 15 | 16 | internal override IEnumerable GetBytes() 17 | { 18 | return new List(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/AdsClient/Commands/AdsReadStateCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Viscon.Communication.Ads.CommandResponse; 3 | using Viscon.Communication.Ads.Common; 4 | 5 | namespace Viscon.Communication.Ads.Commands 6 | { 7 | public class AdsReadStateCommand : AdsCommand 8 | { 9 | public AdsReadStateCommand() 10 | : base(AdsCommandId.ReadState) 11 | { 12 | 13 | } 14 | 15 | internal override IEnumerable GetBytes() 16 | { 17 | return new List(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/AdsClient/Commands/AdsWriteCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Viscon.Communication.Ads.CommandResponse; 5 | using Viscon.Communication.Ads.Common; 6 | 7 | namespace Viscon.Communication.Ads.Commands 8 | { 9 | public class AdsWriteCommand : AdsCommand 10 | { 11 | private IEnumerable varValue; 12 | 13 | public AdsWriteCommand(uint indexGroup, uint indexOffset, IEnumerable varValue) 14 | : base(AdsCommandId.Write) 15 | { 16 | this.varValue = varValue; 17 | this.indexGroup = indexGroup; 18 | this.indexOffset = indexOffset; 19 | } 20 | 21 | private uint indexOffset; 22 | private uint indexGroup; 23 | 24 | internal override IEnumerable GetBytes() 25 | { 26 | IEnumerable data = BitConverter.GetBytes(indexGroup); 27 | data = data.Concat(BitConverter.GetBytes(indexOffset)); 28 | data = data.Concat(BitConverter.GetBytes((uint)varValue.Count())); 29 | data = data.Concat(varValue); 30 | 31 | return data; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/AdsClient/Commands/AdsWriteReadCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Viscon.Communication.Ads.CommandResponse; 5 | using Viscon.Communication.Ads.Common; 6 | 7 | namespace Viscon.Communication.Ads.Commands 8 | { 9 | public class AdsWriteReadCommand : AdsCommand 10 | { 11 | private uint readLength; 12 | private byte[] value; 13 | 14 | public AdsWriteReadCommand(uint indexGroup, uint indexOffset, byte[] value, uint readLength) 15 | : base(AdsCommandId.ReadWrite) 16 | { 17 | this.readLength = readLength; 18 | this.value = value; 19 | this.indexGroup = indexGroup; 20 | this.indexOffset = indexOffset; 21 | } 22 | 23 | private uint indexOffset; 24 | private uint indexGroup; 25 | 26 | internal override IEnumerable GetBytes() 27 | { 28 | IEnumerable data = BitConverter.GetBytes(indexGroup); 29 | data = data.Concat(BitConverter.GetBytes(indexOffset)); 30 | data = data.Concat(BitConverter.GetBytes(readLength)); 31 | data = data.Concat(BitConverter.GetBytes((uint)value.Length)); 32 | data = data.Concat(value); 33 | return data; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/AdsClient/Common/AdsArrayRange.cs: -------------------------------------------------------------------------------- 1 | namespace Viscon.Communication.Ads.Common; 2 | 3 | public record AdsArrayRange(uint Start, uint Length); -------------------------------------------------------------------------------- /src/AdsClient/Common/AdsCommandEnum.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable InconsistentNaming 2 | // ReSharper disable IdentifierTypo 3 | // ReSharper disable UnusedMember.Global 4 | // ReSharper disable CommentTypo 5 | namespace Viscon.Communication.Ads.Common; 6 | 7 | /// 8 | /// Specifies the ADS commands. 9 | /// 10 | /// 11 | /// This list is imported from ads-client (https://github.com/jisotalo/ads-client), which sourced 12 | /// most values from the TwinCAT.Ads.dll by Beckhoff. 13 | /// 14 | // Temporary name, until AdsCommand from Commands namespace is gone. 15 | public enum AdsCommandEnum : ushort 16 | { 17 | None = 0, 18 | ReadDeviceInfo = 1, 19 | Read = 2, 20 | Write = 3, 21 | ReadState = 4, 22 | WriteControl = 5, 23 | AddNotification = 6, 24 | DeleteNotification = 7, 25 | Notification = 8, 26 | ReadWrite = 9, 27 | } -------------------------------------------------------------------------------- /src/AdsClient/Common/AdsCommandId.cs: -------------------------------------------------------------------------------- 1 | namespace Viscon.Communication.Ads.Common 2 | { 3 | public static class AdsCommandId 4 | { 5 | public const ushort ReadDeviceInfo = 1; 6 | public const ushort Read = 2; 7 | public const ushort Write = 3; 8 | public const ushort ReadState = 4; 9 | public const ushort WriteControl = 5; 10 | public const ushort AddDeviceNotification = 6; 11 | public const ushort DeleteDeviceNotification = 7; 12 | public const ushort DeviceNotification = 8; 13 | public const ushort ReadWrite = 9; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/AdsClient/Common/AdsConnectionSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Viscon.Communication.Ads.Common 2 | { 3 | public class AdsConnectionSettings : IAdsConnectionSettings 4 | { 5 | public AdsConnectionSettings () 6 | { 7 | AmsPortTarget = 801; 8 | } 9 | 10 | public string Name { get; set; } 11 | public string AmsNetIdSource { get; set; } 12 | //public string IpTarget { get; set; } 13 | public IAmsSocket AmsSocket {get;set;} 14 | public string AmsNetIdTarget { get; set; } 15 | public ushort AmsPortTarget { get; set; } 16 | 17 | } 18 | } -------------------------------------------------------------------------------- /src/AdsClient/Common/AdsDataType.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable InconsistentNaming 2 | // ReSharper disable IdentifierTypo 3 | // ReSharper disable UnusedMember.Global 4 | // ReSharper disable CommentTypo 5 | namespace Viscon.Communication.Ads.Common; 6 | 7 | /// 8 | /// Specifies the ADS data types. 9 | /// 10 | /// 11 | /// This list is imported from ads-client (https://github.com/jisotalo/ads-client), which sourced 12 | /// most values from the TwinCAT.Ads.dll by Beckhoff. 13 | /// 14 | public enum AdsDataType : uint 15 | { 16 | /// Empty Type 17 | ADST_VOID = 0, 18 | /// Integer 16 Bit 19 | ADST_INT16 = 2, 20 | /// Integer 32 Bit 21 | ADST_INT32 = 3, 22 | /// Real (32 Bit) 23 | ADST_REAL32 = 4, 24 | /// Real 64 Bit 25 | ADST_REAL64 = 5, 26 | /// Integer 8 Bit 27 | ADST_INT8 = 16, 28 | /// Unsigned integer 8 Bit 29 | ADST_UINT8 = 17, 30 | /// Unsigned integer 16 Bit 31 | ADST_UINT16 = 18, 32 | /// Unsigned Integer 32 Bit 33 | ADST_UINT32 = 19, 34 | /// LONG Integer 64 Bit 35 | ADST_INT64 = 20, 36 | /// Unsigned Long integer 64 Bit 37 | ADST_UINT64 = 21, 38 | /// STRING 39 | ADST_STRING = 30, 40 | /// WSTRING 41 | ADST_WSTRING = 31, 42 | /// ADS REAL80 43 | ADST_REAL80 = 32, 44 | /// ADS BIT 45 | ADST_BIT = 33, 46 | /// Internal Only 47 | ADST_MAXTYPES = 34, 48 | /// Blob 49 | ADST_BIGTYPE = 65, 50 | } 51 | -------------------------------------------------------------------------------- /src/AdsClient/Common/AdsDataTypeDto.cs: -------------------------------------------------------------------------------- 1 | namespace Viscon.Communication.Ads.Common; 2 | 3 | public record AdsDataTypeDto(uint Version, uint HashValue, uint BaseTypeHashValue, uint Size, uint Offset, 4 | AdsDataType DataType, AdsDataTypeFlags Flags, string Name, string Type, string Comment, AdsArrayRange[] ArrayRanges, 5 | AdsDataTypeDto[] SubItems); -------------------------------------------------------------------------------- /src/AdsClient/Common/AdsDataTypeFlags.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable InconsistentNaming 2 | // ReSharper disable IdentifierTypo 3 | // ReSharper disable UnusedMember.Global 4 | // ReSharper disable CommentTypo 5 | namespace Viscon.Communication.Ads.Common; 6 | 7 | /// 8 | /// Specifies the ADS data type flags. 9 | /// 10 | /// 11 | /// This list is imported from ads-client (https=//github.com/jisotalo/ads-client), which sourced 12 | /// most values from the TwinCAT.Ads.dll by Beckhoff. 13 | /// 14 | public enum AdsDataTypeFlags : uint 15 | { 16 | None = 0, 17 | DataType = 1 << 0, 18 | DataItem = 1 << 1, 19 | ReferenceTo = 1 << 2, 20 | MethodDeref = 1 << 3, 21 | Oversample = 1 << 4, 22 | BitValues = 1 << 5, 23 | PropItem = 1 << 6, 24 | TypeGuid = 1 << 7, 25 | Persistent = 1 << 8, 26 | CopyMask = 1 << 9, 27 | TComInterfacePtr = 1 << 10, 28 | MethodInfos = 1 << 11, 29 | Attributes = 1 << 12, 30 | EnumInfos = 1 << 13, 31 | 32 | /// 33 | /// Sets whether the datatype is aligned. 34 | /// 35 | Aligned = 1 << 16, 36 | 37 | /// 38 | /// Sets whether the data item is static - do not use offs. 39 | /// 40 | Static = 1 << 17, 41 | 42 | // means "ContainSpLevelss" for DATATYPES and "HasSpLevels" for DATAITEMS 43 | SpLevels = 1 << 18, 44 | 45 | /// 46 | /// Sets whether persistent data is not restored. 47 | /// 48 | IgnorePersist = 1 << 19, 49 | 50 | //Any size array (ADSDATATYPEFLAG_ANYSIZEARRAY) 51 | // 52 | // If the index is exeeded, a value access to this array will return 53 | // 54 | AnySizeArray = 1 << 20, 55 | 56 | /// 57 | /// Sets whether the data type is used for persistent variables. 58 | /// 59 | PersistantDatatype = 1 << 21, 60 | 61 | /// 62 | /// Sets whether persistent data will not be restored after reset (cold). 63 | /// 64 | InitOnResult = 1 << 22, 65 | } 66 | -------------------------------------------------------------------------------- /src/AdsClient/Common/AdsDeviceInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.Common 4 | { 5 | public class AdsDeviceInfo 6 | { 7 | public byte MajorVersion { get; set; } 8 | public byte MinorVersion { get; set; } 9 | public ushort VersionBuild { get; set; } 10 | public string DeviceName { get; set; } 11 | 12 | public override string ToString() 13 | { 14 | return String.Format("Version: {0}.{1}.{2} Devicename: {3}", MajorVersion, MinorVersion, VersionBuild, DeviceName); 15 | } 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/AdsClient/Common/AdsError.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable IdentifierTypo 2 | // ReSharper disable InconsistentNaming 3 | // ReSharper disable UnusedMember.Global 4 | 5 | namespace Viscon.Communication.Ads.Common 6 | { 7 | /// 8 | /// Specifies known ADS errors. 9 | /// 10 | public enum AdsError : uint 11 | { 12 | /// 13 | /// No error 14 | /// 15 | None = 0, 16 | 17 | /// 18 | /// Internal error 19 | /// 20 | InternalError = 1, 21 | 22 | /// 23 | /// No Rtime 24 | /// 25 | NoRTime = 2, 26 | 27 | /// 28 | /// Allocation locked memory error 29 | /// 30 | AllocationLockedMemory = 3, 31 | 32 | /// 33 | /// Insert mailbox error 34 | /// 35 | InsertMailBoxError = 4, 36 | 37 | /// 38 | /// Wrong receive HMSG 39 | /// 40 | WrongReceiveHMSG = 5, 41 | 42 | /// 43 | /// target port not found 44 | /// 45 | TargetPortNotFound = 6, 46 | 47 | /// 48 | /// target machine not found 49 | /// 50 | TargetMachineNotFound = 7, 51 | 52 | /// 53 | /// "Unknown command ID 54 | /// 55 | UnknownCommandId = 8, 56 | 57 | /// 58 | /// Bad task ID 59 | /// 60 | BadTaskId = 9, 61 | 62 | /// 63 | /// No IO 64 | /// 65 | NoIO = 10, 66 | 67 | /// 68 | /// Unknown AMS command 69 | /// 70 | UnknownAmsCommand = 11, 71 | 72 | /// 73 | /// Win 32 error 74 | /// 75 | Win32Error = 12, 76 | 77 | /// 78 | /// Port not connected 79 | /// 80 | PortNotConnected = 13, 81 | 82 | /// 83 | /// Invalid AMS length 84 | /// 85 | InvalidAmsLength = 14, 86 | 87 | /// 88 | /// Invalid AMS Net ID 89 | /// 90 | InvalidAmsNetId = 15, 91 | 92 | /// 93 | /// Low Installation level 94 | /// 95 | LowInstallationLevel = 16, 96 | 97 | /// 98 | /// No debug available 99 | /// 100 | NoDebugAvailable = 17, 101 | 102 | /// 103 | /// Port disabled 104 | /// 105 | PortDisabled = 18, 106 | 107 | /// 108 | /// Port already connected 109 | /// 110 | PortAlreadyConnected = 19, 111 | 112 | /// 113 | /// AMS Sync Win32 error 114 | /// 115 | AmsSyncWin32Error = 20, 116 | 117 | /// 118 | /// AMS Sync Timeout 119 | /// 120 | AmsSyncTimeout = 21, 121 | 122 | /// 123 | /// AMS Sync AMS error 124 | /// 125 | AmsSyncAmsError = 22, 126 | 127 | /// 128 | /// AMS Sync no index map 129 | /// 130 | AmsSyncNoIndexMap = 23, 131 | 132 | /// 133 | /// Invalid AMS port 134 | /// 135 | InvalidAmsPort = 24, 136 | 137 | /// 138 | /// No memory 139 | /// 140 | NoMemory = 25, 141 | 142 | /// 143 | /// TCP send error 144 | /// 145 | TcpSendError = 26, 146 | 147 | /// 148 | /// Host unreachable 149 | /// 150 | HostUnreachable = 27, 151 | 152 | /// 153 | /// error class <device error> 154 | /// 155 | DeviceError = 1792, 156 | 157 | /// 158 | /// Service is not supported by server 159 | /// 160 | ServiceNotSupported = 1793, 161 | 162 | /// 163 | /// invalid index group 164 | /// 165 | InvalidIndexGroup = 1794, 166 | 167 | /// 168 | /// invalid index offset 169 | /// 170 | InvalidIndexOffset = 1795, 171 | 172 | /// 173 | /// reading/writing not permitted 174 | /// 175 | ReadWriteNotPermitted = 1796, 176 | 177 | /// 178 | /// parameter size not correct 179 | /// 180 | ParameterSizeIncorrect = 1797, 181 | 182 | /// 183 | /// invalid parameter value(s) 184 | /// 185 | InvalidParameterValue = 1798, 186 | 187 | /// 188 | /// device is not in a ready state 189 | /// 190 | DeviceNotReady = 1799, 191 | 192 | /// 193 | /// device is busy 194 | /// 195 | DeviceBusy = 1800, 196 | 197 | /// 198 | /// invalid context (must be in Windows) 199 | /// 200 | InvalidContext = 1801, 201 | 202 | /// 203 | /// out of memory 204 | /// 205 | OutOfMemory = 1802, 206 | 207 | /// 208 | /// invalid parameter value(s) 209 | /// 210 | InvalidParameterValue2 = 1803, 211 | 212 | /// 213 | /// not found (files, ...) 214 | /// 215 | NotFound = 1804, 216 | 217 | /// 218 | /// syntax error in command or file 219 | /// 220 | SyntaxError = 1805, 221 | 222 | /// 223 | /// objects do not match 224 | /// 225 | ObjectsDoNotMatch = 1806, 226 | 227 | /// 228 | /// object already exists 229 | /// 230 | ObjectAlreadyExists = 1807, 231 | 232 | /// 233 | /// symbol not found 234 | /// 235 | SymbolNotFound = 1808, 236 | 237 | /// 238 | /// symbol version invalid 239 | /// 240 | SymbolVersionInvalid = 1809, 241 | 242 | /// 243 | /// server is in invalid state 244 | /// 245 | ServerStateInvalid = 1810, 246 | 247 | /// 248 | /// AdsTransMode not supported 249 | /// 250 | AdsTransModeNotSupported = 1811, 251 | 252 | /// 253 | /// Notification handle is invalid 254 | /// 255 | InvalidNotificationHandle = 1812, 256 | 257 | /// 258 | /// Notification client not registered 259 | /// 260 | NotificationClientNotRegistered = 1813, 261 | 262 | /// 263 | /// no more notification handles 264 | /// 265 | NoMoreNotificationHandles = 1814, 266 | 267 | /// 268 | /// size for watch too big 269 | /// 270 | SizeForWatchTooBig = 1815, 271 | 272 | /// 273 | /// device not initialized 274 | /// 275 | DeviceNotInitialized = 1816, 276 | 277 | /// 278 | /// device has a timeout 279 | /// 280 | DeviceTimeout = 1817, 281 | 282 | /// 283 | /// query interface failed 284 | /// 285 | QueryInterfaceFailed = 1818, 286 | 287 | /// 288 | /// wrong interface required 289 | /// 290 | WrongInterfaceRequired = 1819, 291 | 292 | /// 293 | /// class ID is invalid 294 | /// 295 | InvalidClassId = 1820, 296 | 297 | /// 298 | /// object ID is invalid 299 | /// 300 | InvalidObjectId = 1821, 301 | 302 | /// 303 | /// request is pending 304 | /// 305 | RequestIsPending = 1822, 306 | 307 | /// 308 | /// request is aborted 309 | /// 310 | RequestIsAborted = 1823, 311 | 312 | /// 313 | /// signal warning 314 | /// 315 | SignalWarning = 1824, 316 | 317 | /// 318 | /// invalid array index 319 | /// 320 | InvalidArrayIndex = 1825, 321 | 322 | /// 323 | /// symbol not active -> release handle and try again 324 | /// 325 | SymbolNotActive = 1826, 326 | 327 | /// 328 | /// access denied 329 | /// 330 | AccessDenied = 1827, 331 | 332 | /// 333 | /// Error class <client error> 334 | /// 335 | ClientError = 1856, 336 | 337 | /// 338 | /// invalid parameter at service 339 | /// 340 | InvalidServiceParameter = 1857, 341 | 342 | /// 343 | /// polling list is empty 344 | /// 345 | PollingListEmpty = 1858, 346 | 347 | /// 348 | /// var connection already in use 349 | /// 350 | VarConnectionInUse = 1859, 351 | 352 | /// 353 | /// invoke ID in use 354 | /// 355 | InvokeIdInUse = 1860, 356 | 357 | /// 358 | /// timeout elapsed 359 | /// 360 | TimeoutElapsed = 1861, 361 | 362 | /// 363 | /// error in win32 subsystem 364 | /// 365 | Win32SubsystemError = 1862, 366 | 367 | /// 368 | /// Invalid client timeout value 369 | /// 370 | InvalidClientTimeout = 1863, 371 | 372 | /// 373 | /// ads-port not opened 374 | /// 375 | AdsPortNotOpened = 1864, 376 | 377 | /// 378 | /// internal error in ads sync 379 | /// 380 | InternalErrorInAdsSync = 1872, 381 | 382 | /// 383 | /// hash table overflow 384 | /// 385 | HashTableOverflow = 1873, 386 | 387 | /// 388 | /// key not found in hash 389 | /// 390 | KeyNotFoundInHash = 1874, 391 | 392 | /// 393 | /// no more symbols in cache 394 | /// 395 | NoMoreSymbolsInCache = 1875, 396 | 397 | /// 398 | /// invalid response received 399 | /// 400 | InvalidResponseReceived = 1876, 401 | 402 | /// 403 | /// sync port is locked 404 | /// 405 | SyncPortIsLocked = 1877, 406 | } 407 | } -------------------------------------------------------------------------------- /src/AdsClient/Common/AdsException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.Common 4 | { 5 | public class TestException : Exception 6 | { 7 | public TestException(string message) 8 | : base(message) 9 | { 10 | System.Diagnostics.Debug.WriteLine("Cause: " + message); 11 | } 12 | } 13 | 14 | public class AdsException : TestException 15 | { 16 | public AdsException(uint errorCode) : base(GetErrorMessage(errorCode)) 17 | { 18 | this.errorCode = errorCode; 19 | } 20 | 21 | public AdsException(string message) : base(message) 22 | { 23 | this.errorCode = 0; 24 | } 25 | 26 | private uint errorCode; 27 | public AdsError ErrorCode => (AdsError)errorCode; 28 | 29 | private static string GetErrorMessage(uint errorCode) 30 | { 31 | string msg = ""; 32 | switch (errorCode) 33 | { 34 | case 0: msg = "no error"; break; 35 | case 1 : msg = "Internal error"; break; 36 | case 2 : msg = "No Rtime"; break; 37 | case 3 : msg = "Allocation locked memory error"; break; 38 | case 4 : msg = "Insert mailbox error"; break; 39 | case 5 : msg = "Wrong receive HMSG"; break; 40 | case 6 : msg = "target port not found"; break; 41 | case 7 : msg = "target machine not found"; break; 42 | case 8 : msg = "Unknown command ID"; break; 43 | case 9 : msg = "Bad task ID"; break; 44 | case 10: msg = "No IO"; break; 45 | case 11: msg = "Unknown AMS command"; break; 46 | case 12: msg = "Win 32 error"; break; 47 | case 13: msg = "Port not connected"; break; 48 | case 14: msg = "Invalid AMS length"; break; 49 | case 15: msg = "Invalid AMS Net ID"; break; 50 | case 16: msg = "Low Installation level"; break; 51 | case 17: msg = "No debug available"; break; 52 | case 18: msg = "Port disabled"; break; 53 | case 19: msg = "Port already connected"; break; 54 | case 20: msg = "AMS Sync Win32 error"; break; 55 | case 21: msg = "AMS Sync Timeout"; break; 56 | case 22: msg = "AMS Sync AMS error"; break; 57 | case 23: msg = "AMS Sync no index map"; break; 58 | case 24: msg = "Invalid AMS port"; break; 59 | case 25: msg = "No memory"; break; 60 | case 26: msg = "TCP send error"; break; 61 | case 27: msg = "Host unreachable"; break; 62 | 63 | case 1792: msg="error class "; break; 64 | case 1793: msg="Service is not supported by server"; break; 65 | case 1794: msg="invalid index group"; break; 66 | case 1795: msg="invalid index offset"; break; 67 | case 1796: msg="reading/writing not permitted"; break; 68 | case 1797: msg="parameter size not correct"; break; 69 | case 1798: msg="invalid parameter value(s)"; break; 70 | case 1799: msg="device is not in a ready state"; break; 71 | case 1800: msg="device is busy"; break; 72 | case 1801: msg="invalid context (must be in Windows)"; break; 73 | case 1802: msg="out of memory"; break; 74 | case 1803: msg="invalid parameter value(s)"; break; 75 | case 1804: msg="not found (files, ...)"; break; 76 | case 1805: msg="syntax error in command or file"; break; 77 | case 1806: msg="objects do not match"; break; 78 | case 1807: msg="object already exists"; break; 79 | case 1808: msg="symbol not found"; break; 80 | case 1809: msg="symbol version invalid"; break; 81 | case 1810: msg="server is in invalid state"; break; 82 | case 1811: msg="AdsTransMode not supported"; break; 83 | case 1812: msg="Notification handle is invalid"; break; 84 | case 1813: msg="Notification client not registered"; break; 85 | case 1814: msg="no more notification handles"; break; 86 | case 1815: msg="size for watch too big"; break; 87 | case 1816: msg="device not initialized"; break; 88 | case 1817: msg="device has a timeout"; break; 89 | case 1818: msg="query interface failed"; break; 90 | case 1819: msg="wrong interface required"; break; 91 | case 1820: msg="class ID is invalid"; break; 92 | case 1821: msg="object ID is invalid"; break; 93 | case 1822: msg="request is pending"; break; 94 | case 1823: msg="request is aborted"; break; 95 | case 1824: msg="signal warning"; break; 96 | case 1825: msg="invalid array index"; break; 97 | case 1826: msg="symbol not active -> release handle and try again"; break; 98 | case 1827: msg="access denied"; break; 99 | case 1856: msg="Error class "; break; 100 | case 1857: msg="invalid parameter at service"; break; 101 | case 1858: msg="polling list is empty"; break; 102 | case 1859: msg="var connection already in use"; break; 103 | case 1860: msg="invoke ID in use"; break; 104 | case 1861: msg="timeout elapsed"; break; 105 | case 1862: msg="error in win32 subsystem"; break; 106 | case 1863: msg="Invalid client timeout value"; break; 107 | case 1864: msg="ads-port not opened"; break; 108 | case 1872: msg="internal error in ads sync"; break; 109 | case 1873: msg="hash table overflow"; break; 110 | case 1874: msg="key not found in hash"; break; 111 | case 1875: msg="no more symbols in cache"; break; 112 | case 1876: msg="invalid response received"; break; 113 | case 1877: msg = "sync port is locked"; break; 114 | 115 | } 116 | 117 | return msg; 118 | } 119 | 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/AdsClient/Common/AdsNotification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Viscon.Communication.Ads.Helpers; 4 | 5 | namespace Viscon.Communication.Ads.Common 6 | { 7 | public delegate void AdsNotificationDelegate(object sender, AdsNotificationArgs e); 8 | 9 | public class AdsNotification 10 | { 11 | public AdsNotification() 12 | { 13 | } 14 | 15 | public uint NotificationHandle { get; set; } 16 | public byte[] ByteValue { get; set; } 17 | public Type TypeOfValue { get; set; } 18 | public object UserData { get; set; } 19 | 20 | public object Value { get { return ByteArrayHelper.ByteArrayToTypeValue(ByteValue, TypeOfValue); } } 21 | 22 | 23 | public override string ToString() 24 | { 25 | return String.Format("NotificationHandle: {0} Value: {1}", NotificationHandle, BitConverter.ToString(ByteValue)); 26 | } 27 | 28 | //Move this to a helper class? 29 | internal static List GetNotifications(byte[] adsresponseInclAmsHeader) 30 | { 31 | var notifications = new List(); 32 | 33 | int pos = 32; 34 | uint stamps = BitConverter.ToUInt32(adsresponseInclAmsHeader, pos + 4); 35 | pos += 8; 36 | 37 | for (int i = 0; i < stamps; i++) 38 | { 39 | uint samples = BitConverter.ToUInt32(adsresponseInclAmsHeader, pos + 8); 40 | pos += 12; 41 | 42 | for (int j = 0; j < samples; j++) 43 | { 44 | var notification = new AdsNotification(); 45 | notification.NotificationHandle = BitConverter.ToUInt32(adsresponseInclAmsHeader, pos); 46 | uint length = BitConverter.ToUInt32(adsresponseInclAmsHeader, pos + 4); 47 | pos += 8; 48 | notification.ByteValue = new byte[length]; 49 | Array.Copy(adsresponseInclAmsHeader, pos, notification.ByteValue, 0, (int)length); 50 | notifications.Add(notification); 51 | pos += (int)length; 52 | } 53 | } 54 | 55 | return notifications; 56 | } 57 | 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/AdsClient/Common/AdsNotificationArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.Common 4 | { 5 | public class AdsNotificationArgs : EventArgs 6 | { 7 | public AdsNotificationArgs(AdsNotification notification) 8 | { 9 | this.Notification = notification; 10 | } 11 | 12 | public AdsNotification Notification { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/AdsClient/Common/AdsReservedIndexGroup.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable InconsistentNaming 2 | // ReSharper disable IdentifierTypo 3 | // ReSharper disable UnusedMember.Global 4 | // ReSharper disable CommentTypo 5 | namespace Viscon.Communication.Ads.Common; 6 | 7 | /// 8 | /// Specifies the reserved index groups. 9 | /// 10 | /// 11 | /// This list is imported from ads-client (https://github.com/jisotalo/ads-client), which sourced 12 | /// most values from the TwinCAT.Ads.dll by Beckhoff. 13 | /// 14 | public enum AdsReservedIndexGroup : uint 15 | { 16 | PlcRWIB = 0x4000, 17 | PlcRWOB = 0x4010, 18 | PlcRWMB = 0x4020, 19 | PlcRWRB = 0x4030, 20 | PlcRWDB = 0x4040, 21 | SymbolTable = 0xf000, 22 | SymbolName = 0xf001, 23 | SymbolValue = 0xf002, 24 | SymbolHandleByName = 0xf003, 25 | SymbolValueByName = 0xf004, 26 | SymbolValueByHandle = 0xf005, 27 | SymbolReleaseHandle = 0xf006, 28 | SymbolInfoByName = 0xf007, 29 | SymbolVersion = 0xf008, 30 | SymbolInfoByNameEx = 0xf009, 31 | SymbolDownload = 0xf00a, 32 | SymbolUpload = 0xf00b, 33 | SymbolUploadInfo = 0xf00c, 34 | SymbolDownload2 = 0xf00d, 35 | SymbolDataTypeUpload = 0xf00e, 36 | SymbolUploadInfo2 = 0xf00f, // 24 bytes of info, uploadinfo3 would contain 64 bytes 37 | SymbolNote = 0xf010, 38 | DataDataTypeInfoByNameEx = 0xf011, 39 | IOImageRWIB = 0xf020, 40 | IOImageRWIX = 0xf021, 41 | IOImageRWOB = 0xf030, 42 | IOImageRWOX = 0xf031, 43 | IOImageClearI = 0xf040, 44 | IOImageClearO = 0xf050, 45 | // 46 | //ADS Sum Read Command (ADSIGRP_SUMUP_READ) 47 | // 48 | SumCommandRead = 0xf080, 49 | // 50 | //ADS Sum Write Command (ADSIGRP_SUMUP_WRITE) 51 | // 52 | SumCommandWrite = 0xf081, 53 | // 54 | //ADS sum Read/Write command (ADSIGRP_SUMUP_READWRITE) 55 | // 56 | SumCommandReadWrite = 0xf082, 57 | // 58 | //ADS sum ReadEx command (ADSIGRP_SUMUP_READEX) 59 | //AdsRW IOffs list size 60 | //W= {list of IGrp, IOffs, Length} 61 | //R= {list of results, Length} followed by {list of data (expepted lengths)} 62 | // 63 | SumCommandReadEx = 0xf083, 64 | // 65 | //ADS sum ReadEx2 command (ADSIGRP_SUMUP_READEX2) 66 | //AdsRW IOffs list size 67 | //W= {list of IGrp, IOffs, Length} 68 | //R= {list of results, Length} followed by {list of data (returned lengths)} 69 | // 70 | SumCommandReadEx2 = 0xf084, 71 | // 72 | //ADS sum AddDevNote command (ADSIGRP_SUMUP_ADDDEVNOTE) 73 | //AdsRW IOffs list size 74 | //W= {list of IGrp, IOffs, Attrib} 75 | //R= {list of results, handles} 76 | // 77 | SumCommandAddDevNote = 0xf085, 78 | // 79 | //ADS sum DelDevNot command (ADSIGRP_SUMUP_DELDEVNOTE) 80 | //AdsRW IOffs list size 81 | //W= {list of handles} 82 | //R= {list of results} 83 | // 84 | SumCommandDelDevNote = 0xf086, 85 | DeviceData = 0xf100, 86 | } -------------------------------------------------------------------------------- /src/AdsClient/Common/AdsReservedPort.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable InconsistentNaming 2 | // ReSharper disable IdentifierTypo 3 | // ReSharper disable UnusedMember.Global 4 | // ReSharper disable CommentTypo 5 | namespace Viscon.Communication.Ads.Common; 6 | 7 | /// 8 | /// Specifies the reserved ports for ADS routing. 9 | /// 10 | /// 11 | /// This list is imported from ads-client (https://github.com/jisotalo/ads-client), which sourced 12 | /// most values from the TwinCAT.Ads.dll by Beckhoff. 13 | /// 14 | public enum AdsReservedPort : ushort 15 | { 16 | None = 0, 17 | /// 18 | /// AMS Router 19 | /// 20 | Router = 1, 21 | /// 22 | /// AMS Debugger 23 | /// 24 | Debugger = 2, 25 | /// 26 | /// The TCom Server. Dpc or passive level. 27 | /// 28 | R0_TComServer = 10, 29 | /// 30 | /// TCom Server Task. RT context. 31 | /// 32 | R0_TComServerTask = 11, 33 | /// 34 | /// TCom Serve Task. Passive level. 35 | /// 36 | R0_TComServer_PL = 12, 37 | /// 38 | /// TwinCAT Debugger 39 | /// 40 | R0_TcDebugger = 20, 41 | /// 42 | /// TwinCAT Debugger Task 43 | /// 44 | R0_TcDebuggerTask = 21, 45 | /// 46 | /// The License Server 47 | /// 48 | R0_LicenseServer = 30, 49 | /// 50 | /// Logger 51 | /// 52 | Logger = 100, 53 | /// 54 | /// Event Logger 55 | /// 56 | EventLog = 110, 57 | /// 58 | /// application for coupler (EK), gateway (EL), etc. 59 | /// 60 | DeviceApplication = 120, 61 | /// 62 | /// Event Logger UM 63 | /// 64 | EventLog_UM = 130, 65 | /// 66 | /// Event Logger RT 67 | /// 68 | EventLog_RT = 131, 69 | /// 70 | /// Event Logger Publisher 71 | /// 72 | EventLogPublisher = 132, 73 | /// 74 | /// R0 Realtime 75 | /// 76 | R0_Realtime = 200, 77 | /// 78 | /// R0 Trace 79 | /// 80 | R0_Trace = 290, 81 | /// 82 | /// R0 IO 83 | /// 84 | R0_IO = 300, 85 | /// 86 | /// NC (R0) 87 | /// 88 | R0_NC = 500, 89 | /// 90 | /// R0 Satzausführung 91 | /// 92 | R0_NCSAF = 501, 93 | /// 94 | /// R0 Satzvorbereitung 95 | /// 96 | R0_NCSVB = 511, 97 | /// 98 | /// Preconfigured Nc2-Nc3-Instance 99 | /// 100 | R0_NCINSTANCE = 520, 101 | /// 102 | /// R0 ISG 103 | /// 104 | R0_ISG = 550, 105 | /// 106 | /// R0 CNC 107 | /// 108 | R0_CNC = 600, 109 | /// 110 | /// R0 Line 111 | /// 112 | R0_LINE = 700, 113 | /// 114 | /// R0 PLC 115 | /// 116 | R0_PLC = 800, 117 | /// 118 | /// Tc2 PLC RuntimeSystem 1 119 | /// 120 | Tc2_Plc1 = 801, 121 | /// 122 | /// Tc2 PLC RuntimeSystem 2 123 | /// 124 | Tc2_Plc2 = 811, 125 | /// 126 | /// Tc2 PLC RuntimeSystem 3 127 | /// 128 | Tc2_Plc3 = 821, 129 | /// 130 | /// Tc2 PLC RuntimeSystem 4 131 | /// 132 | Tc2_Plc4 = 831, 133 | /// 134 | /// R0 RTS 135 | /// 136 | R0_RTS = 850, 137 | /// 138 | /// Tc3 PLC RuntimeSystem 1 139 | /// 140 | Tc3_Plc1 = 851, 141 | /// 142 | /// Tc3 PLC RuntimeSystem 2 143 | /// 144 | Tc3_Plc2 = 852, 145 | /// 146 | /// Tc3 PLC RuntimeSystem 3 147 | /// 148 | Tc3_Plc3 = 853, 149 | /// 150 | /// Tc3 PLC RuntimeSystem 4 151 | /// 152 | Tc3_Plc4 = 854, 153 | /// 154 | /// Tc3 PLC RuntimeSystem 5 155 | /// 156 | Tc3_Plc5 = 855, 157 | /// 158 | /// Camshaft Controller (R0) 159 | /// 160 | CamshaftController = 900, 161 | /// 162 | /// R0 CAM Tool 163 | /// 164 | R0_CAMTOOL = 950, 165 | /// 166 | /// R0 User 167 | /// 168 | R0_USER = 2000, 169 | R3_CTRLPROG = 10000, 170 | /// 171 | /// System Service (AMSPORT_R3_SYSSERV) 172 | /// 173 | SystemService = 10000, 174 | R3_SYSCTRL = 10001, 175 | R3_SYSSAMPLER = 10100, 176 | R3_TCPRAWCONN = 10200, 177 | R3_TCPIPSERVER = 10201, 178 | R3_SYSMANAGER = 10300, 179 | R3_SMSSERVER = 10400, 180 | R3_MODBUSSERVER = 10500, 181 | R3_AMSLOGGER = 10502, 182 | R3_XMLDATASERVER = 10600, 183 | R3_AUTOCONFIG = 10700, 184 | R3_PLCCONTROL = 10800, 185 | R3_FTPCLIENT = 10900, 186 | R3_NCCTRL = 11000, 187 | R3_NCINTERPRETER = 11500, 188 | R3_GSTINTERPRETER = 11600, 189 | R3_STRECKECTRL = 12000, 190 | R3_CAMCTRL = 13000, 191 | R3_SCOPE = 14000, 192 | R3_CONDITIONMON = 14100, 193 | R3_SINECH1 = 15000, 194 | R3_CONTROLNET = 16000, 195 | R3_OPCSERVER = 17000, 196 | R3_OPCCLIENT = 17500, 197 | R3_MAILSERVER = 18000, 198 | R3_EL60XX = 19000, 199 | R3_MANAGEMENT = 19100, 200 | R3_MIELEHOME = 19200, 201 | R3_CPLINK3 = 19300, 202 | R3_VNSERVICE = 19500, 203 | /// 204 | /// Multiuser 205 | /// 206 | R3_MULTIUSER = 19600, 207 | /// 208 | /// Default (AMS router assigns) 209 | /// 210 | USEDEFAULT = 65535, 211 | } -------------------------------------------------------------------------------- /src/AdsClient/Common/AdsState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.Common 4 | { 5 | public class AdsState 6 | { 7 | const byte ADSSTATE_INVALID = 0; //ADS Status: invalid 8 | const byte ADSSTATE_IDLE = 1; //ADS Status: idle 9 | const byte ADSSTATE_RESET = 2; //ADS Status: reset. 10 | const byte ADSSTATE_INIT = 3; //ADS Status: init 11 | const byte ADSSTATE_START = 4; //ADS Status: start 12 | const byte ADSSTATE_RUN = 5; //ADS Status: run 13 | const byte ADSSTATE_STOP = 6; //ADS Status: stop 14 | const byte ADSSTATE_SAVECFG = 7; //ADS Status: save configuration 15 | const byte ADSSTATE_LOADCFG = 8; //ADS Status: load configuration 16 | const byte ADSSTATE_POWERFAILURE = 9; //ADS Status: Power failure 17 | const byte ADSSTATE_POWERGOOD = 10; //ADS Status: Power good 18 | const byte ADSSTATE_ERROR = 11; //ADS Status: Error 19 | const byte ADSSTATE_SHUTDOWN = 12; //ADS Status: Shutdown 20 | const byte ADSSTATE_SUSPEND = 13; //ADS Status: Suspend 21 | const byte ADSSTATE_RESUME = 14; //ADS Status: Resume 22 | const byte ADSSTATE_CONFIG = 15; //ADS Status: Configuration 23 | const byte ADSSTATE_RECONFIG = 16; //ADS Status: Reconfiguration 24 | 25 | public ushort State { get; set; } 26 | public ushort DeviceState { get; set; } 27 | 28 | public override string ToString() 29 | { 30 | return String.Format("Ads state: {0} ({1}) Device state: {2}", State, AdsStateDescripton, DeviceState); 31 | } 32 | 33 | public string AdsStateDescripton 34 | { 35 | get 36 | { 37 | string desc = ""; 38 | switch (State) 39 | { 40 | case ADSSTATE_INVALID: desc = "Invalid"; break; 41 | case ADSSTATE_IDLE: desc = "Idle"; break; 42 | case ADSSTATE_RESET: desc = "Reset"; break; 43 | case ADSSTATE_INIT: desc = "Init"; break; 44 | case ADSSTATE_START: desc = "Start"; break; 45 | case ADSSTATE_RUN: desc = "Run"; break; 46 | case ADSSTATE_STOP: desc = "Stop"; break; 47 | case ADSSTATE_SAVECFG: desc = "Save configuration"; break; 48 | case ADSSTATE_LOADCFG: desc = "Load configuration"; break; 49 | case ADSSTATE_POWERFAILURE: desc = "Power failure"; break; 50 | case ADSSTATE_POWERGOOD: desc = "Power good"; break; 51 | case ADSSTATE_ERROR: desc = "Error"; break; 52 | case ADSSTATE_SHUTDOWN: desc = "Shutdown"; break; 53 | case ADSSTATE_SUSPEND: desc = "Suspend"; break; 54 | case ADSSTATE_RESUME: desc = "Resume"; break; 55 | case ADSSTATE_CONFIG: desc = "Configuration"; break; 56 | case ADSSTATE_RECONFIG: desc = "Reconfiguration"; break; 57 | } 58 | return desc; 59 | } 60 | } 61 | 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/AdsClient/Common/AdsSymbol.cs: -------------------------------------------------------------------------------- 1 | using Viscon.Communication.Ads.Variables; 2 | 3 | namespace Viscon.Communication.Ads.Common; 4 | 5 | public record AdsSymbol(uint IndexGroup, uint IndexOffset, uint Size, string Name, string TypeName, string Comment) : IVariableAddressAndSize; -------------------------------------------------------------------------------- /src/AdsClient/Common/AdsTransmissionMode.cs: -------------------------------------------------------------------------------- 1 | namespace Viscon.Communication.Ads.Common 2 | { 3 | public enum AdsTransmissionMode : uint 4 | { 5 | None = 0, 6 | ClientCycle = 1, 7 | ClientOnChange = 2, 8 | 9 | /// 10 | /// Cyclic transmission. 11 | /// 12 | Cyclic = 3, 13 | 14 | /// 15 | /// Transmission on value change. 16 | /// 17 | OnChange = 4, 18 | CyclicInContext = 5, 19 | OnChangeInContext = 6, 20 | } 21 | } -------------------------------------------------------------------------------- /src/AdsClient/Common/AmsNetId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Viscon.Communication.Ads.Common 5 | { 6 | public class AmsNetId 7 | { 8 | public IList Bytes { get; set; } 9 | 10 | internal AmsNetId(string amsNetId) 11 | { 12 | ParseString(amsNetId); 13 | } 14 | 15 | private void ParseString(string amsNetId) 16 | { 17 | Bytes = new List(); 18 | 19 | string[] byteStrings = amsNetId.Split('.'); 20 | foreach (string byteString in byteStrings) 21 | { 22 | byte b = Convert.ToByte(byteString); 23 | Bytes.Add(b); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/AdsClient/Common/AmsSocketResponseArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.Common 4 | { 5 | public class AmsSocketResponseArgs : EventArgs 6 | { 7 | public Exception Error { get; set; } 8 | public byte[] Response { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/AdsClient/Common/Date.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.Common 4 | { 5 | public class Date 6 | { 7 | private uint intValue; 8 | private DateTime dateValue; 9 | 10 | public Date(uint value) 11 | { 12 | this.intValue = value; 13 | this.dateValue = new DateTime(1970, 1, 1).AddSeconds(intValue); 14 | } 15 | 16 | public Date(DateTime value) 17 | { 18 | this.dateValue = value; 19 | this.intValue = (uint)((value - new DateTime(1970, 1, 1)).TotalSeconds); 20 | } 21 | 22 | public bool ToBoolean(IFormatProvider provider) 23 | { 24 | return Convert.ToBoolean(intValue); 25 | } 26 | 27 | public byte ToByte(IFormatProvider provider) 28 | { 29 | return Convert.ToByte(intValue); 30 | } 31 | 32 | public char ToChar(IFormatProvider provider) 33 | { 34 | return Convert.ToChar(intValue); 35 | } 36 | 37 | public DateTime ToDateTime(IFormatProvider provider) 38 | { 39 | return dateValue; 40 | } 41 | 42 | public decimal ToDecimal(IFormatProvider provider) 43 | { 44 | return Convert.ToDecimal(intValue); ; 45 | } 46 | 47 | public double ToDouble(IFormatProvider provider) 48 | { 49 | return Convert.ToDouble(intValue); 50 | } 51 | 52 | public short ToInt16(IFormatProvider provider) 53 | { 54 | return Convert.ToInt16(intValue); 55 | } 56 | 57 | public int ToInt32(IFormatProvider provider) 58 | { 59 | return Convert.ToInt32(intValue); 60 | } 61 | 62 | public long ToInt64(IFormatProvider provider) 63 | { 64 | return Convert.ToInt64(intValue); 65 | } 66 | 67 | public sbyte ToSByte(IFormatProvider provider) 68 | { 69 | return Convert.ToSByte(intValue); 70 | } 71 | 72 | public float ToSingle(IFormatProvider provider) 73 | { 74 | return Convert.ToSingle(intValue); 75 | } 76 | 77 | public string ToString(IFormatProvider provider) 78 | { 79 | return Convert.ToString(dateValue, provider); 80 | } 81 | 82 | public override string ToString() 83 | { 84 | return Convert.ToString(dateValue); 85 | } 86 | 87 | public object ToType(Type conversionType, IFormatProvider provider) 88 | { 89 | return Convert.ChangeType(intValue, conversionType, provider); 90 | } 91 | 92 | public ushort ToUInt16(IFormatProvider provider) 93 | { 94 | return Convert.ToUInt16(intValue); 95 | } 96 | 97 | public uint ToUInt32(IFormatProvider provider) 98 | { 99 | return Convert.ToUInt32(intValue); 100 | } 101 | 102 | public ulong ToUInt64(IFormatProvider provider) 103 | { 104 | return Convert.ToUInt64(intValue); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/AdsClient/Common/IAdsConnectionSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Viscon.Communication.Ads.Common 2 | { 3 | public interface IAdsConnectionSettings 4 | { 5 | string Name { get; set; } 6 | string AmsNetIdSource { get; set; } 7 | //string IpTarget { get; set; } 8 | IAmsSocket AmsSocket { get; set; } 9 | string AmsNetIdTarget { get; set; } 10 | ushort AmsPortTarget { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/AdsClient/Common/Time.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.Common 4 | { 5 | public class Time 6 | { 7 | private uint intValue; 8 | private TimeSpan timeSpanValue; 9 | 10 | public Time(uint value) 11 | { 12 | this.intValue = value; 13 | this.timeSpanValue = TimeSpan.FromMilliseconds(intValue); 14 | } 15 | 16 | public Time(TimeSpan value) 17 | { 18 | this.timeSpanValue = value; 19 | this.intValue = (uint)value.TotalMilliseconds; 20 | } 21 | 22 | public TimeSpan ToTimeSpan() 23 | { 24 | return this.timeSpanValue; 25 | } 26 | 27 | public bool ToBoolean(IFormatProvider provider) 28 | { 29 | return Convert.ToBoolean(intValue); 30 | } 31 | 32 | public byte ToByte(IFormatProvider provider) 33 | { 34 | return Convert.ToByte(intValue); 35 | } 36 | 37 | public char ToChar(IFormatProvider provider) 38 | { 39 | return Convert.ToChar(intValue); 40 | } 41 | 42 | public DateTime ToDateTime(IFormatProvider provider) 43 | { 44 | return new DateTime(1900, 1, 1).AddMilliseconds(intValue); 45 | } 46 | 47 | public decimal ToDecimal(IFormatProvider provider) 48 | { 49 | return Convert.ToDecimal(intValue); ; 50 | } 51 | 52 | public double ToDouble(IFormatProvider provider) 53 | { 54 | return Convert.ToDouble(intValue); 55 | } 56 | 57 | public short ToInt16(IFormatProvider provider) 58 | { 59 | return Convert.ToInt16(intValue); 60 | } 61 | 62 | public int ToInt32(IFormatProvider provider) 63 | { 64 | return Convert.ToInt32(intValue); 65 | } 66 | 67 | public long ToInt64(IFormatProvider provider) 68 | { 69 | return Convert.ToInt64(intValue); 70 | } 71 | 72 | public sbyte ToSByte(IFormatProvider provider) 73 | { 74 | return Convert.ToSByte(intValue); 75 | } 76 | 77 | public float ToSingle(IFormatProvider provider) 78 | { 79 | return Convert.ToSingle(intValue); 80 | } 81 | 82 | public string ToString(IFormatProvider provider) 83 | { 84 | return Convert.ToString(timeSpanValue, provider); 85 | } 86 | 87 | public override string ToString() 88 | { 89 | return Convert.ToString(timeSpanValue); 90 | } 91 | 92 | public object ToType(Type conversionType, IFormatProvider provider) 93 | { 94 | return Convert.ChangeType(intValue, conversionType, provider); 95 | } 96 | 97 | public ushort ToUInt16(IFormatProvider provider) 98 | { 99 | return Convert.ToUInt16(intValue); 100 | } 101 | 102 | public uint ToUInt32(IFormatProvider provider) 103 | { 104 | return Convert.ToUInt32(intValue); 105 | } 106 | 107 | public ulong ToUInt64(IFormatProvider provider) 108 | { 109 | return Convert.ToUInt64(intValue); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/AdsClient/Conversation/AdsReadRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Viscon.Communication.Ads.Common; 3 | using Viscon.Communication.Ads.Internal; 4 | 5 | namespace Viscon.Communication.Ads.Conversation; 6 | 7 | public readonly struct AdsReadRequest : IAdsRequest 8 | { 9 | public AdsReadRequest(uint indexGroup, uint indexOffset, uint size) 10 | { 11 | IndexGroup = indexGroup; 12 | IndexOffset = indexOffset; 13 | Size = size; 14 | } 15 | 16 | public readonly uint IndexGroup; 17 | public readonly uint IndexOffset; 18 | public readonly uint Size; 19 | 20 | public AdsCommandEnum Command => AdsCommandEnum.Read; 21 | 22 | public int GetRequestLength() 23 | { 24 | return 12; 25 | } 26 | 27 | public int BuildRequest(Span span) 28 | { 29 | var offset = WireFormatting.WriteUInt32(ref span.GetStart(), IndexGroup); 30 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), IndexOffset); 31 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), Size); 32 | 33 | return offset; 34 | } 35 | } -------------------------------------------------------------------------------- /src/AdsClient/Conversation/CreateVariableHandles/AdsCreateVariableHandlesConversation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Viscon.Communication.Ads.Common; 3 | using Viscon.Communication.Ads.Internal; 4 | 5 | namespace Viscon.Communication.Ads.Conversation.CreateVariableHandles 6 | { 7 | internal class AdsCreateVariableHandlesConversation : IAdsConversation 8 | { 9 | public string[] VariableNames { get; } 10 | 11 | public AdsCreateVariableHandlesConversation(string[] variableNames) 12 | { 13 | VariableNames = variableNames; 14 | } 15 | 16 | public AdsCreateVariableHandlesRequest BuildRequest() 17 | { 18 | return new AdsCreateVariableHandlesRequest(VariableNames); 19 | } 20 | 21 | public uint[] ParseResponse(ReadOnlySpan buffer) 22 | { 23 | var offset = WireFormatting.ReadInt32(buffer, out var dataLength); 24 | if (dataLength != buffer.Length - 4) 25 | { 26 | throw new Exception( 27 | $"Received {buffer.Length} bytes of data while length indicates length should be {dataLength} bytes."); 28 | } 29 | 30 | foreach (var _ in VariableNames) 31 | { 32 | offset += WireFormatting.ReadUInt32(buffer.Slice(offset), out var errorCode); 33 | if (errorCode != 0) throw new AdsException(errorCode); 34 | 35 | offset += WireFormatting.ReadUInt32(buffer.Slice(offset), out var length); 36 | if (length != sizeof(uint)) 37 | throw new AdsException( 38 | $"Received handle response of length {length}, expected length {sizeof(uint)}."); 39 | } 40 | 41 | var handles = new uint[VariableNames.Length]; 42 | for (var i = 0; i < VariableNames.Length; i++) 43 | { 44 | offset += WireFormatting.ReadUInt32(buffer.Slice(offset), out handles[i]); 45 | } 46 | 47 | return handles; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/AdsClient/Conversation/CreateVariableHandles/AdsCreateVariableHandlesRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Viscon.Communication.Ads.Common; 4 | using Viscon.Communication.Ads.Internal; 5 | 6 | namespace Viscon.Communication.Ads.Conversation.CreateVariableHandles; 7 | 8 | internal readonly struct AdsCreateVariableHandlesRequest : IAdsRequest 9 | { 10 | public readonly string[] VariableNames; 11 | 12 | public AdsCreateVariableHandlesRequest(string[] variableNames) 13 | { 14 | VariableNames = variableNames; 15 | } 16 | 17 | public AdsCommandEnum Command => AdsCommandEnum.ReadWrite; 18 | 19 | private int GetRequestDataLength() 20 | { 21 | // 16 bytes per sub-request + encoded name lengths 22 | return VariableNames.Length * 16 + VariableNames.Sum(x => x.Length + 1); 23 | } 24 | 25 | public int GetRequestLength() 26 | { 27 | // 16 bytes command + request data length 28 | return 16 + GetRequestDataLength(); 29 | } 30 | 31 | public int BuildRequest(Span span) 32 | { 33 | var offset = WireFormatting.WriteUInt32(ref span.GetStart(), AdsReservedIndexGroup.SumCommandReadWrite.ToUInt32()); 34 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), (uint) VariableNames.Length); 35 | // Response data length 36 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), (uint)(12 * VariableNames.Length)); 37 | // Request data length 38 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), GetRequestDataLength()); 39 | 40 | foreach (var variableName in VariableNames) 41 | { 42 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), 43 | AdsReservedIndexGroup.SymbolHandleByName.ToUInt32()); 44 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), (uint) 0); 45 | // Size of the requested data, which is sizeof(Symbol.IndexOffset) 46 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), (uint) sizeof(uint)); 47 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), (uint) variableName.Length + 1); 48 | } 49 | 50 | foreach (var variableName in VariableNames) 51 | { 52 | offset += WireFormatting.WriteString(span.Slice(offset), variableName); 53 | } 54 | 55 | return offset; 56 | } 57 | } -------------------------------------------------------------------------------- /src/AdsClient/Conversation/IAdsConversation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.Conversation; 4 | 5 | public interface IAdsConversation where TRequest : struct, IAdsRequest 6 | { 7 | TRequest BuildRequest(); 8 | TResponse ParseResponse(ReadOnlySpan buffer); 9 | } -------------------------------------------------------------------------------- /src/AdsClient/Conversation/IAdsRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Viscon.Communication.Ads.Common; 3 | 4 | namespace Viscon.Communication.Ads.Conversation; 5 | 6 | public interface IAdsRequest 7 | { 8 | AdsCommandEnum Command { get; } 9 | int GetRequestLength(); 10 | int BuildRequest(Span span); 11 | } -------------------------------------------------------------------------------- /src/AdsClient/Conversation/ReadDataTypes/AdsDataTypeParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Viscon.Communication.Ads.Common; 3 | using Viscon.Communication.Ads.Internal; 4 | 5 | namespace Viscon.Communication.Ads.Conversation.ReadDataTypes; 6 | 7 | internal static class AdsDataTypeParser 8 | { 9 | public static AdsDataTypeDto ParseDataType(ReadOnlySpan buffer) 10 | { 11 | var offset = WireFormatting.ReadUInt32(buffer, out var version); 12 | offset += WireFormatting.ReadUInt32(buffer.Slice(offset), out var hashValue); 13 | offset += WireFormatting.ReadUInt32(buffer.Slice(offset), out var baseTypeHashValue); 14 | offset += WireFormatting.ReadUInt32(buffer.Slice(offset), out var size); 15 | offset += WireFormatting.ReadUInt32(buffer.Slice(offset), out var typeOffset); 16 | offset += WireFormatting.ReadUInt32(buffer.Slice(offset), out var adsDataType); 17 | offset += WireFormatting.ReadUInt32(buffer.Slice(offset), out var flags); 18 | offset += WireFormatting.ReadUInt16(buffer.Slice(offset), out var nameLength); 19 | offset += WireFormatting.ReadUInt16(buffer.Slice(offset), out var typeLength); 20 | offset += WireFormatting.ReadUInt16(buffer.Slice(offset), out var commentLength); 21 | offset += WireFormatting.ReadUInt16(buffer.Slice(offset), out var arrayDimensions); 22 | offset += WireFormatting.ReadUInt16(buffer.Slice(offset), out var subItemCount); 23 | offset += WireFormatting.ReadString(buffer.Slice(offset), nameLength, out var name); 24 | offset += WireFormatting.ReadString(buffer.Slice(offset), typeLength, out var type); 25 | offset += WireFormatting.ReadString(buffer.Slice(offset), commentLength, out var comment); 26 | offset += ReadArrayRanges(buffer.Slice(offset), arrayDimensions, out var arrayRanges); 27 | offset += ReadSubItems(buffer.Slice(offset), subItemCount, out var subItems); 28 | 29 | // End here for now, there's more data available. 30 | 31 | return new AdsDataTypeDto(version, hashValue, baseTypeHashValue, size, typeOffset, adsDataType.ToDataType(), 32 | flags.ToDataTypeFlags(), name, type, comment, arrayRanges, subItems); 33 | } 34 | 35 | private static int ReadArrayRanges(ReadOnlySpan buffer, ushort dimensions, out AdsArrayRange[] ranges) 36 | { 37 | ranges = new AdsArrayRange[dimensions]; 38 | var offset = 0; 39 | for (var i = 0; i < dimensions; i++) 40 | { 41 | offset += WireFormatting.ReadUInt32(buffer.Slice(offset), out var start); 42 | offset += WireFormatting.ReadUInt32(buffer.Slice(offset), out var length); 43 | 44 | ranges[i] = new AdsArrayRange(start, length); 45 | } 46 | 47 | return offset; 48 | } 49 | 50 | private static int ReadSubItems(ReadOnlySpan buffer, ushort count, out AdsDataTypeDto[] subItems) 51 | { 52 | subItems = new AdsDataTypeDto[count]; 53 | var offset = 0; 54 | for (var i = 0; i < count; i++) 55 | { 56 | WireFormatting.ReadInt32(buffer, out var len); 57 | subItems[i] = ParseDataType(buffer.Slice(4, len - 4)); 58 | 59 | offset += len; 60 | buffer = buffer.Slice(len); 61 | } 62 | 63 | return offset; 64 | } 65 | } 66 | 67 | /* 68 | 69 | //Subitems data 70 | dataType.subItems = [] 71 | for (let i = 0; i < dataType.subItemCount; i++) { 72 | //Get subitem length 73 | let len = data.readUInt32LE(pos) 74 | pos += 4 75 | 76 | //Parse the subitem recursively 77 | dataType.subItems.push(await _parseDataType.call(this, data.slice(pos, pos + len))) 78 | 79 | pos += (len - 4) 80 | } 81 | 82 | //If flags contain TypeGuid 83 | if (dataType.flagsStr.includes('TypeGuid')) { 84 | dataType.typeGuid = data.slice(pos, pos + 16).toString('hex') 85 | pos += 16 86 | } 87 | 88 | //If flags contain CopyMask 89 | if (dataType.flagsStr.includes('CopyMask')) { 90 | //Let's skip this for now 91 | pos += dataType.size 92 | } 93 | 94 | dataType.rpcMethods = [] 95 | 96 | //If flags contain MethodInfos (TwinCAT.Ads.dll: AdsMethodEntry) 97 | if (dataType.flagsStr.includes('MethodInfos')) { 98 | dataType.methodCount = data.readUInt16LE(pos) 99 | pos += 2 100 | 101 | //RPC methods 102 | for (let i = 0; i < dataType.methodCount; i++) { 103 | const method = {} 104 | 105 | //Get method length 106 | let len = data.readUInt32LE(pos) 107 | pos += 4 108 | 109 | //4..7 Version 110 | method.version = data.readUInt32LE(pos) 111 | pos += 4 112 | 113 | //8..11 Virtual table index 114 | method.vTableIndex = data.readUInt32LE(pos) 115 | pos += 4 116 | 117 | //12..15 Return size 118 | method.returnSize = data.readUInt32LE(pos) 119 | pos += 4 120 | 121 | //16..19 Return align size 122 | method.returnAlignSize = data.readUInt32LE(pos) 123 | pos += 4 124 | 125 | //20..23 Reserved 126 | method.reserved = data.readUInt32LE(pos) 127 | pos += 4 128 | 129 | //24..27 Return type GUID 130 | method.returnTypeGuid = data.slice(pos, pos + 16).toString('hex') 131 | pos += 16 132 | 133 | //28..31 Return data type 134 | method.retunAdsDataType = data.readUInt32LE(pos) 135 | method.retunAdsDataTypeStr = ADS.ADS_DATA_TYPES.toString(method.retunAdsDataType) 136 | pos += 4 137 | 138 | //27..30 Flags (AdsDataTypeFlags) 139 | method.flags = data.readUInt16LE(pos) 140 | method.flagsStr = ADS.ADS_DATA_TYPE_FLAGS.toStringArray(method.flags) 141 | pos += 4 142 | 143 | //31..32 Name length 144 | method.nameLength = data.readUInt16LE(pos) 145 | pos += 2 146 | 147 | //33..34 Return type length 148 | method.returnTypeLength = data.readUInt16LE(pos) 149 | pos += 2 150 | 151 | //35..36 Comment length 152 | method.commentLength = data.readUInt16LE(pos) 153 | pos += 2 154 | 155 | //37..38 Parameter count 156 | method.parameterCount = data.readUInt16LE(pos) 157 | pos += 2 158 | 159 | //39.... Name 160 | method.name = _trimPlcString(iconv.decode(data.slice(pos, pos + method.nameLength + 1), 'cp1252')) 161 | pos += method.nameLength + 1 162 | 163 | //...... Return type 164 | method.returnType = _trimPlcString(iconv.decode(data.slice(pos, pos + method.returnTypeLength + 1), 'cp1252')) 165 | pos += method.returnTypeLength + 1 166 | 167 | //...... Comment 168 | method.comment = _trimPlcString(iconv.decode(data.slice(pos, pos + method.commentLength + 1), 'cp1252')) 169 | pos += method.commentLength + 1 170 | 171 | method.parameters = [] 172 | 173 | for (let p = 0; p < method.parameterCount; p++) { 174 | const param = {} 175 | 176 | let paramStartPos = pos 177 | 178 | //Get parameter length 179 | let paramLen = data.readUInt32LE(pos) 180 | pos += 4 181 | 182 | //4..7 Size 183 | param.size = data.readUInt32LE(pos) 184 | pos += 4 185 | 186 | //8..11 Align size 187 | param.alignSize = data.readUInt32LE(pos) 188 | pos += 4 189 | 190 | //12..15 Data type 191 | param.adsDataType = data.readUInt32LE(pos) 192 | param.adsDataTypeStr = ADS.ADS_DATA_TYPES.toString(param.adsDataType) 193 | pos += 4 194 | 195 | //16..19 Flags (RCP_METHOD_PARAM_FLAGS) 196 | param.flags = data.readUInt16LE(pos) 197 | param.flagsStr = ADS.RCP_METHOD_PARAM_FLAGS.toStringArray(param.flags) 198 | pos += 4 199 | 200 | //20..23 Reserved 201 | param.reserved = data.readUInt32LE(pos) 202 | pos += 4 203 | 204 | //24..27 Type GUID 205 | param.typeGuid = data.slice(pos, pos + 16).toString('hex') 206 | pos += 16 207 | 208 | //28..31 LengthIsPara 209 | param.lengthIsPara = data.readUInt16LE(pos) 210 | pos += 2 211 | 212 | //32..33 Name length 213 | param.nameLength = data.readUInt16LE(pos) 214 | pos += 2 215 | 216 | //34..35 Type length 217 | param.typeLength = data.readUInt16LE(pos) 218 | pos += 2 219 | 220 | //36..37 Comment length 221 | param.commentLength = data.readUInt16LE(pos) 222 | pos += 2 223 | 224 | //38.... Name 225 | param.name = _trimPlcString(iconv.decode(data.slice(pos, pos + param.nameLength + 1), 'cp1252')) 226 | pos += param.nameLength + 1 227 | 228 | //...... Type 229 | param.type = _trimPlcString(iconv.decode(data.slice(pos, pos + param.typeLength + 1), 'cp1252')) 230 | pos += param.typeLength + 1 231 | 232 | //...... Comment 233 | param.comment = _trimPlcString(iconv.decode(data.slice(pos, pos + param.commentLength + 1), 'cp1252')) 234 | pos += param.commentLength + 1 235 | 236 | if (pos - paramStartPos > paramLen) { 237 | //There is some additional data 238 | param.reserved2 = data.slice(pos) 239 | } 240 | method.parameters.push(param) 241 | } 242 | 243 | dataType.rpcMethods.push(method) 244 | } 245 | } 246 | 247 | //If flags contain Attributes (TwinCAT.Ads.dll: AdsAttributeEntry) 248 | //Attribute is for example, a pack-mode attribute above struct 249 | dataType.attributes = [] 250 | if (dataType.flagsStr.includes('Attributes')) { 251 | dataType.attributeCount = data.readUInt16LE(pos) 252 | pos += 2 253 | 254 | //Attributes 255 | for (let i = 0; i < dataType.attributeCount; i++) { 256 | let attr = {} 257 | 258 | //Name length 259 | let nameLen = data.readUInt8(pos) 260 | pos += 1 261 | 262 | //Value length 263 | let valueLen = data.readUInt8(pos) 264 | pos += 1 265 | 266 | //Name 267 | attr.name = _trimPlcString(iconv.decode(data.slice(pos, pos + nameLen + 1), 'cp1252')) 268 | pos += (nameLen + 1) 269 | 270 | //Value 271 | attr.value = _trimPlcString(iconv.decode(data.slice(pos, pos + valueLen + 1), 'cp1252')) 272 | pos += (valueLen + 1) 273 | 274 | dataType.attributes.push(attr) 275 | } 276 | } 277 | 278 | 279 | //If flags contain EnumInfos (TwinCAT.Ads.dll: AdsEnumInfoEntry) 280 | //EnumInfo contains the enumeration values as string 281 | if (dataType.flagsStr.includes('EnumInfos')) { 282 | dataType.enumInfoCount = data.readUInt16LE(pos) 283 | pos += 2 284 | //EnumInfos 285 | dataType.enumInfo = [] 286 | for (let i = 0; i < dataType.enumInfoCount; i++) { 287 | let enumInfo = {} 288 | 289 | //Name length 290 | let nameLen = data.readUInt8(pos) 291 | pos += 1 292 | 293 | //Name 294 | enumInfo.name = _trimPlcString(iconv.decode(data.slice(pos, pos + nameLen + 1), 'cp1252')) 295 | pos += (nameLen + 1) 296 | 297 | //Value 298 | enumInfo.value = data.slice(pos, pos + dataType.size) 299 | pos += dataType.size 300 | 301 | dataType.enumInfo.push(enumInfo) 302 | } 303 | } 304 | 305 | //Reserved, if any 306 | dataType.reserved = data.slice(pos) 307 | 308 | return dataType 309 | } 310 | */ -------------------------------------------------------------------------------- /src/AdsClient/Conversation/ReadDataTypes/AdsReadDataTypesConversation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Viscon.Communication.Ads.Common; 4 | using Viscon.Communication.Ads.Conversation.ReadUploadInfo; 5 | using Viscon.Communication.Ads.Internal; 6 | 7 | namespace Viscon.Communication.Ads.Conversation.ReadDataTypes; 8 | 9 | internal class AdsReadDataTypesConversation : IAdsConversation> 10 | { 11 | public uint DataTypeLength { get; } 12 | 13 | public AdsReadDataTypesConversation(uint dataTypeLength) => DataTypeLength = dataTypeLength; 14 | 15 | public AdsReadDataTypesConversation(AdsUploadInfoDto uploadInfo) : this(uploadInfo.DataTypeLength) { } 16 | 17 | public AdsReadRequest BuildRequest() => new(AdsReservedIndexGroup.SymbolDataTypeUpload.ToUInt32(), 0, DataTypeLength); 18 | 19 | public List ParseResponse(ReadOnlySpan buffer) 20 | { 21 | var length = LittleEndianDeserializer.ReadInt32(buffer); 22 | if (length != buffer.Length - 4) 23 | { 24 | throw new Exception( 25 | $"Received {buffer.Length} bytes of data while length indicates length should be {length} bytes."); 26 | } 27 | 28 | buffer = buffer.Slice(sizeof(uint)); 29 | 30 | var results = new List(); 31 | while (buffer.Length > 0) 32 | { 33 | var len = LittleEndianDeserializer.ReadInt32(buffer); 34 | results.Add(AdsDataTypeParser.ParseDataType(buffer.Slice(4, len - 4))); 35 | 36 | buffer = buffer.Slice(len); 37 | } 38 | 39 | return results; 40 | } 41 | } -------------------------------------------------------------------------------- /src/AdsClient/Conversation/ReadMultiple/AdsReadMultipleConversation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.InteropServices; 4 | using Viscon.Communication.Ads.Internal; 5 | using Viscon.Communication.Ads.Variables; 6 | 7 | namespace Viscon.Communication.Ads.Conversation.ReadMultiple 8 | { 9 | internal class AdsReadMultipleConversation : IAdsConversation 10 | { 11 | private readonly IVariableAddressAndSize[] variables; 12 | private readonly IReadResultFactory resultFactory; 13 | 14 | public AdsReadMultipleConversation(IVariableAddressAndSize[] variables, IReadResultFactory resultFactory) 15 | { 16 | this.variables = variables; 17 | this.resultFactory = resultFactory; 18 | } 19 | 20 | public AdsReadMultipleRequest BuildRequest() 21 | { 22 | return new AdsReadMultipleRequest(variables); 23 | } 24 | 25 | public TResult ParseResponse(ReadOnlySpan buffer) 26 | { 27 | var offset = WireFormatting.ReadInt32(buffer, out var length); 28 | Assertions.AssertDataLength(buffer, length, offset); 29 | 30 | var resultsLength = variables.Length * sizeof(uint); 31 | var results = MemoryMarshal.Cast(buffer.Slice(offset, resultsLength)); 32 | List exceptions = null; 33 | for (var i = 0; i < variables.Length; i++) 34 | { 35 | if (results[i] == 0) continue; 36 | 37 | exceptions ??= new List(); 38 | exceptions.Add(new AdsReadVariableException(variables[i], results[i])); 39 | } 40 | 41 | if (exceptions is not null) ReadMultipleException.Throw(exceptions); 42 | 43 | offset += resultsLength; 44 | 45 | return resultFactory.CreateResult(variables, buffer.Slice(offset)); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/AdsClient/Conversation/ReadMultiple/AdsReadMultipleRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Viscon.Communication.Ads.Common; 4 | using Viscon.Communication.Ads.Internal; 5 | using Viscon.Communication.Ads.Variables; 6 | 7 | namespace Viscon.Communication.Ads.Conversation.ReadMultiple; 8 | 9 | public readonly struct AdsReadMultipleRequest : IAdsRequest 10 | { 11 | private readonly IVariableAddressAndSize[] variables; 12 | 13 | public AdsReadMultipleRequest(IVariableAddressAndSize[] variables) 14 | { 15 | this.variables = variables; 16 | } 17 | 18 | public AdsCommandEnum Command => AdsCommandEnum.ReadWrite; 19 | 20 | public int GetRequestLength() 21 | { 22 | return sizeof(uint) * 4 + variables.Length * sizeof(uint) * 3; 23 | } 24 | 25 | public int BuildRequest(Span span) 26 | { 27 | var offset = 28 | WireFormatting.WriteUInt32(ref span.GetStart(), AdsReservedIndexGroup.SumCommandRead.ToUInt32()); 29 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), variables.Length); 30 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), (uint) variables.Sum(v => v.Size + sizeof(uint))); 31 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), (uint) GetDataLength()); 32 | 33 | foreach (var variable in variables) 34 | { 35 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), variable.IndexGroup); 36 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), variable.IndexOffset); 37 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), variable.Size); 38 | } 39 | 40 | return offset; 41 | } 42 | 43 | private int GetDataLength() 44 | { 45 | return variables.Length * sizeof(uint) * 3; 46 | } 47 | } -------------------------------------------------------------------------------- /src/AdsClient/Conversation/ReadMultiple/AdsReadVariableException.cs: -------------------------------------------------------------------------------- 1 | using Viscon.Communication.Ads.Common; 2 | using Viscon.Communication.Ads.Variables; 3 | 4 | namespace Viscon.Communication.Ads.Conversation.ReadMultiple; 5 | 6 | public class AdsReadVariableException : AdsException 7 | { 8 | public IVariableAddressAndSize Variable { get; } 9 | 10 | public AdsReadVariableException(IVariableAddressAndSize variable, uint errorCode) : base(errorCode) 11 | { 12 | Variable = variable; 13 | } 14 | } -------------------------------------------------------------------------------- /src/AdsClient/Conversation/ReadMultiple/ArrayMultiReadResult.cs: -------------------------------------------------------------------------------- 1 | using Viscon.Communication.Ads.Variables; 2 | 3 | namespace Viscon.Communication.Ads.Conversation.ReadMultiple; 4 | 5 | public record ArrayMultiReadResult(IVariableData[] Results) : IMultiReadResult; -------------------------------------------------------------------------------- /src/AdsClient/Conversation/ReadMultiple/ArrayMultiReadResultFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Viscon.Communication.Ads.Variables; 3 | 4 | namespace Viscon.Communication.Ads.Conversation.ReadMultiple; 5 | 6 | public class ArrayMultiReadResultFactory : IReadResultFactory 7 | { 8 | public ArrayMultiReadResult CreateResult(IVariableAddressAndSize[] variables, ReadOnlySpan span) 9 | { 10 | var results = new IVariableData[variables.Length]; 11 | 12 | var offset = 0; 13 | for (var i = 0; i < variables.Length; i++) 14 | { 15 | var variable = variables[i]; 16 | 17 | results[i] = new ArrayVariableData(variable, span.Slice(offset, (int)variable.Size).ToArray()); 18 | 19 | offset += (int)variable.Size; 20 | } 21 | 22 | return new ArrayMultiReadResult(results); 23 | } 24 | } -------------------------------------------------------------------------------- /src/AdsClient/Conversation/ReadMultiple/IMultiReadResult.cs: -------------------------------------------------------------------------------- 1 | using Viscon.Communication.Ads.Variables; 2 | 3 | namespace Viscon.Communication.Ads.Conversation.ReadMultiple; 4 | 5 | public interface IMultiReadResult 6 | { 7 | IVariableData[] Results { get; } 8 | } -------------------------------------------------------------------------------- /src/AdsClient/Conversation/ReadMultiple/IReadResultFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Viscon.Communication.Ads.Variables; 3 | 4 | namespace Viscon.Communication.Ads.Conversation.ReadMultiple; 5 | 6 | public interface IReadResultFactory 7 | { 8 | TResult CreateResult(IVariableAddressAndSize[] variables, ReadOnlySpan span); 9 | } -------------------------------------------------------------------------------- /src/AdsClient/Conversation/ReadMultiple/PooledArrayMultiReadResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using Viscon.Communication.Ads.Variables; 4 | 5 | namespace Viscon.Communication.Ads.Conversation.ReadMultiple; 6 | 7 | public class PooledArrayMultiReadResult : IMultiReadResult, IDisposable 8 | { 9 | private readonly byte[] data; 10 | 11 | public PooledArrayMultiReadResult(IVariableData[] results, byte[] data) 12 | { 13 | Results = results; 14 | 15 | this.data = data; 16 | } 17 | 18 | public IVariableData[] Results { get; } 19 | 20 | public void Dispose() 21 | { 22 | ArrayPool.Shared.Return(data); 23 | } 24 | } -------------------------------------------------------------------------------- /src/AdsClient/Conversation/ReadMultiple/PooledArrayMultiReadResultFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Linq; 4 | using Viscon.Communication.Ads.Variables; 5 | 6 | namespace Viscon.Communication.Ads.Conversation.ReadMultiple; 7 | 8 | public class PooledArrayMultiReadResultFactory : IReadResultFactory 9 | { 10 | public PooledArrayMultiReadResult CreateResult(IVariableAddressAndSize[] variables, ReadOnlySpan span) 11 | { 12 | var data = ArrayPool.Shared.Rent((int)variables.Sum(v => v.Size)); 13 | try 14 | { 15 | var results = new IVariableData[variables.Length]; 16 | span.CopyTo(data); 17 | 18 | var offset = 0; 19 | for (var i = 0; i < variables.Length;i++) 20 | { 21 | var variable = variables[i]; 22 | 23 | results[i] = new ArraySegmentVariableData(variable, 24 | new ArraySegment(data, offset, (int)variable.Size)); 25 | 26 | offset += (int) variable.Size; 27 | } 28 | 29 | return new PooledArrayMultiReadResult(results, data); 30 | } 31 | catch 32 | { 33 | ArrayPool.Shared.Return(data); 34 | throw; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/AdsClient/Conversation/ReadMultiple/ReadMultipleException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Viscon.Communication.Ads.Conversation.ReadMultiple; 5 | 6 | internal class ReadMultipleException 7 | { 8 | public static void Throw(IEnumerable innerExceptions) 9 | { 10 | throw new AggregateException( 11 | "One or multiple variables could not be read, see InnerExceptions for details.", innerExceptions); 12 | } 13 | } -------------------------------------------------------------------------------- /src/AdsClient/Conversation/ReadSymbols/AdsReadSymbolsConversation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Viscon.Communication.Ads.Common; 4 | using Viscon.Communication.Ads.Conversation.ReadUploadInfo; 5 | using Viscon.Communication.Ads.Internal; 6 | 7 | namespace Viscon.Communication.Ads.Conversation.ReadSymbols 8 | { 9 | internal class AdsReadSymbolsConversation : IAdsConversation> 10 | { 11 | public uint SymbolLength { get; } 12 | 13 | public AdsReadSymbolsConversation(uint symbolLength) 14 | { 15 | SymbolLength = symbolLength; 16 | } 17 | 18 | public AdsReadSymbolsConversation(AdsUploadInfoDto uploadInfo) : this(uploadInfo.SymbolLength) 19 | { 20 | } 21 | 22 | public AdsReadRequest BuildRequest() 23 | { 24 | return new AdsReadRequest(AdsReservedIndexGroup.SymbolUpload.ToUInt32(), 0, SymbolLength); 25 | } 26 | 27 | public List ParseResponse(ReadOnlySpan buffer) 28 | { 29 | var length = LittleEndianDeserializer.ReadInt32(buffer); 30 | if (length != buffer.Length - 4) 31 | { 32 | throw new Exception( 33 | $"Received {buffer.Length} bytes of data while length indicates length should be {length} bytes."); 34 | } 35 | 36 | buffer = buffer.Slice(sizeof(uint)); 37 | 38 | var results = new List(); 39 | while (buffer.Length > 0) 40 | { 41 | var offset = WireFormatting.ReadInt32(buffer, out var len); 42 | results.Add(AdsSymbolParser.ParseSymbol(buffer.Slice(offset, len - offset))); 43 | 44 | buffer = buffer.Slice(len); 45 | } 46 | 47 | return results; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/AdsClient/Conversation/ReadSymbols/AdsSymbolParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Viscon.Communication.Ads.Common; 3 | using Viscon.Communication.Ads.Internal; 4 | 5 | namespace Viscon.Communication.Ads.Conversation.ReadSymbols; 6 | 7 | internal class AdsSymbolParser 8 | { 9 | public static AdsSymbol ParseSymbol(ReadOnlySpan buffer) 10 | { 11 | var offset = WireFormatting.ReadUInt32(buffer, out var indexGroup); 12 | offset += WireFormatting.ReadUInt32(buffer.Slice(offset), out var indexOffset); 13 | offset += WireFormatting.ReadUInt32(buffer.Slice(offset), out var size); 14 | offset += WireFormatting.ReadUInt32(buffer.Slice(offset), out var adsDataType); 15 | offset += WireFormatting.ReadUInt16(buffer.Slice(offset), out var adsSymbolFlags); 16 | offset += WireFormatting.ReadUInt16(buffer.Slice(offset), out var arrayDimension); 17 | offset += WireFormatting.ReadUInt16(buffer.Slice(offset), out var nameLength); 18 | offset += WireFormatting.ReadUInt16(buffer.Slice(offset), out var typeLength); 19 | offset += WireFormatting.ReadUInt16(buffer.Slice(offset), out var commentLength); 20 | offset += WireFormatting.ReadString(buffer.Slice(offset), nameLength, out var name); 21 | offset += WireFormatting.ReadString(buffer.Slice(offset), typeLength, out var type); 22 | offset += WireFormatting.ReadString(buffer.Slice(offset), commentLength, out var comment); 23 | 24 | return new AdsSymbol(indexGroup, indexOffset, size, name, type, comment); 25 | } 26 | } -------------------------------------------------------------------------------- /src/AdsClient/Conversation/ReadUploadInfo/AdsReadUploadInfoConversation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Viscon.Communication.Ads.Common; 3 | using Viscon.Communication.Ads.Internal; 4 | 5 | namespace Viscon.Communication.Ads.Conversation.ReadUploadInfo; 6 | 7 | internal class AdsReadUploadInfoConversation : IAdsConversation 8 | { 9 | public AdsReadRequest BuildRequest() => new(AdsReservedIndexGroup.SymbolUploadInfo2.ToUInt32(), 0, 24); 10 | 11 | public AdsUploadInfoDto ParseResponse(ReadOnlySpan buffer) 12 | { 13 | var length = LittleEndianDeserializer.ReadUInt32(buffer); 14 | if (length != 24) 15 | { 16 | throw new Exception( 17 | $"Received {buffer.Length} bytes of data while length indicates length should be {length} bytes."); 18 | } 19 | 20 | return new AdsUploadInfoDto( 21 | LittleEndianDeserializer.ReadUInt32(buffer.Slice(4)), 22 | LittleEndianDeserializer.ReadUInt32(buffer.Slice(8)), 23 | LittleEndianDeserializer.ReadUInt32(buffer.Slice(12)), 24 | LittleEndianDeserializer.ReadUInt32(buffer.Slice(16)), 25 | LittleEndianDeserializer.ReadUInt32(buffer.Slice(20)), 26 | LittleEndianDeserializer.ReadUInt32(buffer.Slice(24))); 27 | } 28 | } -------------------------------------------------------------------------------- /src/AdsClient/Conversation/ReadUploadInfo/AdsUploadInfoDto.cs: -------------------------------------------------------------------------------- 1 | namespace Viscon.Communication.Ads.Conversation.ReadUploadInfo; 2 | 3 | public record AdsUploadInfoDto(uint SymbolCount, uint SymbolLength, uint DataTypeCount, uint DataTypeLength, 4 | uint ExtraCount, uint ExtraLength); -------------------------------------------------------------------------------- /src/AdsClient/Conversation/WriteMultiple/AdsWriteMultipleConversation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.InteropServices; 4 | using Viscon.Communication.Ads.Internal; 5 | using Viscon.Communication.Ads.Variables; 6 | 7 | namespace Viscon.Communication.Ads.Conversation.WriteMultiple 8 | { 9 | internal class AdsWriteMultipleConversation : IAdsConversation 10 | { 11 | public IVariableData[] Variables { get; } 12 | 13 | public AdsWriteMultipleConversation(IVariableData[] variables) 14 | { 15 | Variables = variables; 16 | } 17 | 18 | public AdsWriteMultipleRequest BuildRequest() 19 | { 20 | return new AdsWriteMultipleRequest(Variables); 21 | } 22 | 23 | public object ParseResponse(ReadOnlySpan buffer) 24 | { 25 | var offset = WireFormatting.ReadInt32(buffer, out var dataLength); 26 | Assertions.AssertDataLength(buffer, dataLength, offset); 27 | 28 | var results = MemoryMarshal.Cast(buffer.Slice(offset)); 29 | List exceptions = null; 30 | for (var i = 0; i < Variables.Length; i++) 31 | { 32 | if (results[i] <= 0) continue; 33 | 34 | exceptions ??= new List(); 35 | exceptions.Add(new AdsWriteVariableException(Variables[i], results[i])); 36 | } 37 | 38 | if (exceptions is not null) WriteMultipleException.Throw(exceptions); 39 | 40 | return null; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/AdsClient/Conversation/WriteMultiple/AdsWriteMultipleRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Viscon.Communication.Ads.Common; 4 | using Viscon.Communication.Ads.Internal; 5 | using Viscon.Communication.Ads.Variables; 6 | 7 | namespace Viscon.Communication.Ads.Conversation.WriteMultiple; 8 | 9 | public readonly struct AdsWriteMultipleRequest : IAdsRequest 10 | { 11 | private readonly IVariableData[] variables; 12 | 13 | public AdsWriteMultipleRequest(IVariableData[] variables) 14 | { 15 | this.variables = variables; 16 | } 17 | 18 | public AdsCommandEnum Command => AdsCommandEnum.ReadWrite; 19 | 20 | public int GetRequestLength() 21 | { 22 | return sizeof(uint) * 4 + GetDataLength(); 23 | } 24 | 25 | public int BuildRequest(Span span) 26 | { 27 | var offset = WireFormatting.WriteUInt32(ref span.GetStart(), AdsReservedIndexGroup.SumCommandWrite); 28 | // Number of variables 29 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), (uint) variables.Length); 30 | // Expected response size (error code per item) 31 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), (uint) (sizeof(uint) * variables.Length)); 32 | // Data length (address + length of value + value per item) 33 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), (uint) GetDataLength()); 34 | 35 | foreach (var variable in variables) 36 | { 37 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), variable.Address.IndexGroup); 38 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), variable.Address.IndexOffset); 39 | offset += WireFormatting.WriteUInt32(ref span.GetOffset(offset), variable.Data.Length); 40 | } 41 | 42 | foreach (var variable in variables) 43 | { 44 | var variableData = variable.Data; 45 | variableData.CopyTo(span.Slice(offset)); 46 | offset += variableData.Length; 47 | } 48 | 49 | return offset; 50 | } 51 | 52 | private int GetDataLength() => variables.Sum(v => v.Data.Length + sizeof(uint) * 3); 53 | } -------------------------------------------------------------------------------- /src/AdsClient/Conversation/WriteMultiple/AdsWriteVariableException.cs: -------------------------------------------------------------------------------- 1 | using Viscon.Communication.Ads.Common; 2 | using Viscon.Communication.Ads.Variables; 3 | 4 | namespace Viscon.Communication.Ads.Conversation.WriteMultiple 5 | { 6 | public class AdsWriteVariableException : AdsException 7 | { 8 | public IVariableData Variable { get; } 9 | 10 | public AdsWriteVariableException(IVariableData variable, uint errorCode) : base(errorCode) 11 | { 12 | Variable = variable; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/AdsClient/Conversation/WriteMultiple/WriteMultipleException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Viscon.Communication.Ads.Conversation.WriteMultiple; 5 | 6 | internal static class WriteMultipleException 7 | { 8 | public static void Throw(IEnumerable itemExceptions) 9 | { 10 | throw new AggregateException("One or multiple items could not be written, see InnerExceptions for details.", 11 | itemExceptions); 12 | } 13 | } -------------------------------------------------------------------------------- /src/AdsClient/Framework/EncodingExtensions.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable once CheckNamespace 2 | namespace System.Text; 3 | 4 | #if !NETSTANDARD2_1_OR_GREATER 5 | 6 | internal static class EncodingExtensions 7 | { 8 | public static int GetBytes(this Encoding encoding, ReadOnlySpan chars, Span buffer) 9 | { 10 | var bufferArr = new byte[buffer.Length]; 11 | 12 | var len = encoding.GetBytes(chars.ToArray(), 0, chars.Length, bufferArr, 0); 13 | 14 | bufferArr.AsSpan().CopyTo(buffer); 15 | 16 | return len; 17 | } 18 | 19 | public static string GetString(this Encoding encoding, ReadOnlySpan buffer) 20 | { 21 | return encoding.GetString(buffer.ToArray()); 22 | } 23 | } 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /src/AdsClient/Framework/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | // ReSharper disable once CheckNamespace 4 | namespace System.Runtime.CompilerServices; 5 | 6 | [EditorBrowsable(EditorBrowsableState.Never)] 7 | internal record IsExternalInit; -------------------------------------------------------------------------------- /src/AdsClient/Helpers/AdsAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace Viscon.Communication.Ads.Helpers 5 | { 6 | /// 7 | /// An attribute class for members of serializable classes. 8 | /// 9 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] 10 | public class AdsAttribute : Attribute 11 | { 12 | /// 13 | /// The byte size. 14 | /// 15 | public uint ByteSize; 16 | 17 | /// 18 | /// The order. 19 | /// 20 | public uint Order; 21 | 22 | /// 23 | /// The array size. 24 | /// 25 | public uint ArraySize; 26 | 27 | /// 28 | /// Creates a new instance. 29 | /// 30 | public AdsAttribute() 31 | { 32 | ByteSize = 0; 33 | Order = 99; 34 | ArraySize = 1; 35 | } 36 | 37 | private MemberInfo member; 38 | /// 39 | /// Gets or sets the member. 40 | /// 41 | internal MemberInfo Member 42 | { 43 | get { return member; } 44 | set { member = value; } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/AdsClient/Helpers/AdsSerializableAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.Helpers 4 | { 5 | [System.AttributeUsage(System.AttributeTargets.Class)] 6 | public class AdsSerializableAttribute : Attribute 7 | { 8 | public AdsSerializableAttribute() 9 | { 10 | 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/AdsClient/Helpers/AmsHeaderHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.Helpers 4 | { 5 | internal class AmsHeaderHelper 6 | { 7 | public const int AmsTcpHeaderSize = 6; 8 | public const int AmsHeaderSize = 32; 9 | public const int AmsDataLengthOffset = 20; 10 | 11 | public static uint GetResponseLength(byte[] tcpHeader) 12 | { 13 | return BitConverter.ToUInt32(tcpHeader, 2); 14 | } 15 | 16 | public static byte[] GetAmsNetIdTarget(byte[] amsHeader) 17 | { 18 | byte[] id = new byte[6]; 19 | Array.Copy(amsHeader, 0, id, 0, 6); 20 | return id; 21 | } 22 | 23 | public static ushort GetAmsPortTarget(byte[] amsHeader) 24 | { 25 | return BitConverter.ToUInt16(amsHeader, 14); 26 | } 27 | 28 | public static byte[] GetAmsNetIdSource(byte[] amsHeader) 29 | { 30 | byte[] id = new byte[6]; 31 | Array.Copy(amsHeader, 8, id, 0, 6); 32 | return id; 33 | } 34 | 35 | public static ushort GetAmsPortSource(byte[] amsHeader) 36 | { 37 | return BitConverter.ToUInt16(amsHeader, 14); 38 | } 39 | 40 | public static ushort GetCommandId(byte[] amsHeader) 41 | { 42 | return BitConverter.ToUInt16(amsHeader, 16); 43 | } 44 | 45 | public static uint GetErrorCode(byte[] amsHeader) 46 | { 47 | return BitConverter.ToUInt32(amsHeader, 24); 48 | } 49 | 50 | public static uint GetInvokeId(byte[] amsHeader) 51 | { 52 | return BitConverter.ToUInt32(amsHeader, 28); 53 | } 54 | 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/AdsClient/Helpers/AmsMessageBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Viscon.Communication.Ads.CommandResponse; 5 | using Viscon.Communication.Ads.Commands; 6 | using Viscon.Communication.Ads.Common; 7 | using Viscon.Communication.Ads.Internal; 8 | 9 | namespace Viscon.Communication.Ads.Helpers 10 | { 11 | internal static class AmsMessageBuilder 12 | { 13 | public static byte[] BuildAmsMessage(Ams ams, AdsCommand adsCommand, uint invokeId) where TResponse : AdsCommandResponse, new() 14 | { 15 | 16 | IEnumerable data = adsCommand.GetBytes(); 17 | IEnumerable message = ams.AmsNetIdTarget.Bytes; //AmsNetId Target 18 | message = message.Concat( 19 | BitConverter.GetBytes(adsCommand.AmsPortTargetOverride ?? ams.AmsPortTarget)); //AMSPort Target 20 | message = message.Concat(ams.AmsNetIdSource.Bytes); //AmsNetId Source 21 | message = message.Concat(BitConverter.GetBytes(ams.AmsPortSource)); //AMSPort Source 22 | message = message.Concat(BitConverter.GetBytes(adsCommand.CommandId)); //Command Id 23 | message = message.Concat(BitConverter.GetBytes((ushort)0x0004)); //State Flags 24 | message = message.Concat(BitConverter.GetBytes((uint)data.Count())); //Length 25 | message = message.Concat(BitConverter.GetBytes((uint)0)); //Error Code 26 | message = message.Concat(BitConverter.GetBytes(invokeId)); //Invoke Id 27 | message = message.Concat(data); //Data 28 | 29 | //2 bytes reserved 0 + 4 bytes for length + the rest 30 | message = BitConverter.GetBytes((ushort)0).Concat(BitConverter.GetBytes((uint)message.Count())) 31 | .Concat(message); 32 | 33 | return message.ToArray(); 34 | } 35 | 36 | public static void WriteHeader(Span buffer, Ams ams, AdsCommandEnum command, ushort amsPortTarget, uint invokeId, int messageLength) 37 | { 38 | var offset = WireFormatting.WriteUInt16(ref buffer.GetStart(), default); 39 | offset += WireFormatting.WriteUInt32(ref buffer.GetOffset(offset), 40 | AmsHeaderHelper.AmsHeaderSize + messageLength); 41 | 42 | ams.AmsNetIdTarget.Bytes.ToArray().CopyTo(buffer.Slice(offset)); 43 | offset += 6; 44 | 45 | offset += WireFormatting.WriteUInt16(ref buffer.GetOffset(offset), amsPortTarget); 46 | 47 | ams.AmsNetIdSource.Bytes.ToArray().CopyTo(buffer.Slice(offset)); 48 | offset += 6; 49 | 50 | offset += WireFormatting.WriteUInt16(ref buffer.GetOffset(offset), ams.AmsPortSource); 51 | offset += WireFormatting.WriteUInt16(ref buffer.GetOffset(offset), command); 52 | offset += WireFormatting.WriteUInt16(ref buffer.GetOffset(offset), (ushort) 0x0004); 53 | offset += WireFormatting.WriteUInt32(ref buffer.GetOffset(offset), (uint) messageLength); 54 | offset += WireFormatting.WriteUInt32(ref buffer.GetOffset(offset), (uint) 0); 55 | offset += WireFormatting.WriteUInt32(ref buffer.GetOffset(offset), invokeId); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/AdsClient/Helpers/ByteArrayHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | using Viscon.Communication.Ads.Common; 5 | 6 | namespace Viscon.Communication.Ads.Helpers 7 | { 8 | public class ByteArrayHelper 9 | { 10 | /// 11 | /// Convert byte array to value defined by TypeOfValue 12 | /// 13 | /// The byte array that needs conversion 14 | /// The type of the result 15 | /// 16 | public static object ByteArrayToTypeValue(byte[] value, Type typeOfValue) 17 | { 18 | //if (Type.Equals(TypeOfValue, null)) return null; 19 | //if (Type.Equals(TypeOfValue, typeof(byte[]))) return value; 20 | //var method = typeof(GenericHelper).GetMethod("GetResultFromBytes"); 21 | //var generic = method.MakeGenericMethod(TypeOfValue); 22 | //return generic.Invoke(null, new object[] { value }); 23 | return GenericHelper.GetResultFromBytes(typeOfValue, value, 0); 24 | } 25 | 26 | public static string ByteArrayToString(byte[] value) 27 | { 28 | if (value == null) 29 | return ""; 30 | else 31 | return string.Concat(value.Select(b => b <= 0x7f ? (char)b : '?').TakeWhile(b => b > 0)) ?? ""; 32 | } 33 | 34 | public static string ByteArrayToTestString(byte[] value) 35 | { 36 | StringBuilder sb = new StringBuilder(); 37 | foreach (byte val in value) 38 | { 39 | if (sb.Length > 0) sb.Append(','); 40 | sb.Append(val.ToString()); 41 | } 42 | return sb.ToString(); 43 | } 44 | 45 | public static DateTime ByteArrayToDateTime(byte[] value) 46 | { 47 | var seconds = BitConverter.ToUInt32(value, 0); 48 | var val = new Date(seconds); 49 | return val.ToDateTime(null); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/AdsClient/Helpers/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Viscon.Communication.Ads.Common; 3 | 4 | namespace Viscon.Communication.Ads.Helpers 5 | { 6 | public static class Extensions 7 | { 8 | public static byte[] ToAdsBytes(this String value) 9 | { 10 | byte[] result = new byte[value.Length + 1]; 11 | 12 | for (int index = 0; index < value.Length; ++index) 13 | { 14 | char ch = value[index]; 15 | if (ch <= 0x7f) 16 | result[index] = (byte)ch; 17 | else 18 | result[index] = (byte)'?'; 19 | } 20 | result[result.Length-1] = 0; 21 | 22 | return result; 23 | } 24 | 25 | public static Time ToTime(this TimeSpan value) 26 | { 27 | return new Time(value); 28 | } 29 | 30 | public static Date ToDate(this DateTime value) 31 | { 32 | return new Date(value); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/AdsClient/Helpers/GenericHelper.cs: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2011 Inando 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included 11 | in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 18 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 19 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | using System; 23 | using System.Collections.Generic; 24 | using System.Linq; 25 | using Viscon.Communication.Ads.Common; 26 | 27 | namespace Viscon.Communication.Ads.Helpers 28 | { 29 | /// 30 | /// An enumeration of types known to ADS. 31 | /// 32 | internal enum AdsTypeEnum { Unknown, Bool, Byte, Char, Int16, Int32, Int64, UInt16, UInt32, UInt64, Single, Double, String, DateTime, Date, Time }; 33 | 34 | /// 35 | /// A helper class for various conversions. 36 | /// 37 | internal static class GenericHelper 38 | { 39 | const string TypeNotImplementedError = "Type {0} must be implemented or has the AdsSerializable attribute!"; 40 | 41 | #region Type byte length 42 | /// 43 | /// Get length in bytes from a valuetype or AdsSerializable 44 | /// 45 | /// The default string length. 46 | /// The array length. 47 | /// The length. 48 | public static uint GetByteLengthFromType(uint defaultStringLength, uint arrayLength = 1) 49 | { 50 | return GetByteLengthFromType(typeof(T), defaultStringLength, arrayLength); 51 | } 52 | 53 | /// 54 | /// Get length in bytes from a valuetype or AdsSerializable 55 | /// 56 | /// The type. 57 | /// The default string length. 58 | /// The array length. 59 | /// The length 60 | public static uint GetByteLengthFromType(Type type, uint defaultStringLength, uint arrayLength = 1) 61 | { 62 | uint factor = 1; 63 | 64 | if (type.IsArray) 65 | { 66 | factor = arrayLength; 67 | type = type.GetElementType(); 68 | } 69 | 70 | var adsType = GetEnumFromType(type); 71 | 72 | if (adsType != AdsTypeEnum.Unknown) 73 | { 74 | var length = GetByteLengthFromConvertible(adsType, defaultStringLength); 75 | if (length == 0) 76 | throw new AdsException(String.Format("Function GetByteLengthFromType doesn't support this type ({0}) yet!", type.FullName)); 77 | return length * factor; 78 | } 79 | else 80 | { 81 | if (type.IsAdsSerializable()) 82 | { 83 | uint length = 0; 84 | var attributes = GetAdsAttributes(type, defaultStringLength); 85 | foreach (var a in attributes) 86 | { 87 | length += a.ByteSize; 88 | } 89 | return length * factor; 90 | } 91 | else 92 | throw new AdsException(String.Format(TypeNotImplementedError, type.FullName)); 93 | } 94 | } 95 | #endregion 96 | 97 | #region Byte array -> Object 98 | /// 99 | /// Convert byte array to valuetype or AdsSerializable 100 | /// 101 | /// 102 | /// 103 | /// 104 | /// 105 | /// 106 | /// The resulting object. 107 | public static object GetResultFromBytes(Type type, byte[] value, uint defaultStringLength, uint arrayLength = 1, object adsObj = null) 108 | { 109 | if (value == null) 110 | throw new ArgumentNullException("value", "GetResultFromBytes"); 111 | 112 | if (type.IsArray) 113 | { 114 | var elementType = type.GetElementType(); 115 | var elementSize = (int)(value.Length / arrayLength); 116 | var array = (adsObj as Array) ?? Array.CreateInstance(elementType, new int[] { (int)arrayLength }); 117 | 118 | for (var i = 0; i < arrayLength; i++) 119 | { 120 | // Prepare the new value array. 121 | var valarray = new byte[elementSize]; 122 | Array.Copy(value, i * elementSize, valarray, 0, elementSize); 123 | 124 | // Get the previous value (if available) and store the results. 125 | var val = array.GetValue(i); 126 | val = GetResultFromBytes(elementType, valarray, defaultStringLength, 1, val); 127 | array.SetValue(val, i); 128 | } 129 | 130 | return array; 131 | } 132 | else 133 | return GetResultFromBytesInternal(type, value, defaultStringLength, adsObj); 134 | } 135 | 136 | private static object GetResultFromBytesInternal(Type type, byte[] value, uint defaultStringLength, object adsObj) 137 | { 138 | var adsType = GetEnumFromType(type); 139 | if (adsType != AdsTypeEnum.Unknown) 140 | { 141 | return ConvertBytesToType(adsType, value); 142 | } 143 | else 144 | { 145 | if (type.IsAdsSerializable()) 146 | { 147 | if (adsObj == null) 148 | adsObj = Activator.CreateInstance(type); 149 | 150 | var attributes = GetAdsAttributes(type, defaultStringLength); 151 | 152 | uint pos = 0; 153 | 154 | foreach (var attr in attributes) 155 | { 156 | try 157 | { 158 | // Prepare the new value array. 159 | var valarray = new byte[attr.ByteSize]; 160 | Array.Copy(value, (int)pos, valarray, 0, (int)attr.ByteSize); 161 | 162 | // Get the previous data (if available) and store the results. 163 | var proptype = attr.Member.GetMemberType(); 164 | var val = attr.Member.GetValue(adsObj); 165 | val = GetResultFromBytes(proptype, valarray, defaultStringLength, attr.ArraySize, val); 166 | attr.Member.SetValue(adsObj, val); 167 | 168 | // Increment byte pointer. 169 | pos += attr.ByteSize; 170 | } 171 | catch 172 | { break; } 173 | } 174 | 175 | return adsObj; 176 | } 177 | else 178 | throw new AdsException(String.Format(TypeNotImplementedError, type.FullName)); 179 | } 180 | } 181 | 182 | 183 | /// 184 | /// Convert byte array to generic valuetype or AdsSerializable 185 | /// 186 | /// ValueType or AdsSerializable 187 | /// 188 | /// 189 | public static T GetResultFromBytes(byte[] value, uint defaultStringLength, uint arrayLength = 1, object adsObj = null) 190 | { 191 | Type type = typeof(T); 192 | var o = GetResultFromBytes(type, value, defaultStringLength, arrayLength, adsObj); 193 | if (o == null) 194 | return default(T); 195 | else 196 | return (T)Convert.ChangeType(o, type); 197 | } 198 | #endregion 199 | 200 | #region Object -> Byte array 201 | /// 202 | /// Convert ValueType or AdsSerializable to byte array 203 | /// 204 | /// ValueType or AdsSerializable 205 | /// The value that needs conversion 206 | /// 207 | public static IEnumerable GetBytesFromType(T varValue, uint defaultStringLength) 208 | { 209 | List varValueBytes = null; 210 | var type = typeof(T); 211 | 212 | var adsType = GetEnumFromType(type); 213 | if (adsType != AdsTypeEnum.Unknown) 214 | { 215 | varValueBytes = GetBytesFromConvertible(adsType, varValue, defaultStringLength).ToList(); 216 | } 217 | else 218 | { 219 | if (type.IsAdsSerializable()) 220 | { 221 | var totallength = GetByteLengthFromType(type, defaultStringLength); 222 | varValueBytes = new List((int)totallength); 223 | var attributes = GetAdsAttributes(type, defaultStringLength); 224 | 225 | foreach (var attr in attributes) 226 | { 227 | var memberType = attr.Member.GetMemberType(); 228 | adsType = GetEnumFromType(memberType); 229 | var val = attr.Member.GetValue(varValue); 230 | var bytes = GetBytesFromConvertible(adsType, val, defaultStringLength); 231 | if (bytes.Count() != attr.ByteSize) 232 | { 233 | if (bytes.Count() > attr.ByteSize) 234 | bytes = bytes.Take((int)attr.ByteSize).ToList(); 235 | } 236 | varValueBytes.AddRange(bytes); 237 | } 238 | } 239 | } 240 | 241 | if (varValueBytes == null) 242 | throw new AdsException("Function GetBytesFromType doesn't support this type yet!"); 243 | 244 | return varValueBytes; 245 | } 246 | #endregion 247 | 248 | #region AdsAttribute enumeration 249 | /// 250 | /// Gets all members marked with an . 251 | /// 252 | /// The type. 253 | /// The default string length. 254 | /// An enumeration of attributes. 255 | public static IEnumerable GetAdsAttributes(Type type, uint defaultStringLength) 256 | { 257 | var attributes = new List(); 258 | var members = type.GetPublicFieldsAndProperties(); 259 | 260 | foreach (var member in members) 261 | { 262 | var attr = member.GetAdsAttribute(); 263 | 264 | if (attr != null) 265 | { 266 | attr.Member = member; 267 | if (attr.ByteSize == 0) 268 | { 269 | var memberType = member.GetMemberType(); 270 | attr.ByteSize = GetByteLengthFromType(memberType, defaultStringLength, attr.ArraySize); 271 | } 272 | attributes.Add(attr); 273 | } 274 | } 275 | 276 | return attributes.OrderBy(a => a.Order); 277 | } 278 | #endregion 279 | 280 | #region Known type conversions 281 | private static IEnumerable GetBytesFromConvertible(AdsTypeEnum adsType, object value, uint defaultStringLength) 282 | { 283 | IEnumerable varValueBytes = null; 284 | 285 | if (value == null) return null; 286 | 287 | switch (adsType) 288 | { 289 | case AdsTypeEnum.Bool: varValueBytes = BitConverter.GetBytes((bool)value); break; 290 | case AdsTypeEnum.Byte: varValueBytes = new byte[] { (byte)value }; break; 291 | case AdsTypeEnum.Char: varValueBytes = BitConverter.GetBytes((char)value); break; 292 | case AdsTypeEnum.Int16: varValueBytes = BitConverter.GetBytes((Int16)value); break; 293 | case AdsTypeEnum.Int32: varValueBytes = BitConverter.GetBytes((Int32)value); break; 294 | case AdsTypeEnum.Int64: varValueBytes = BitConverter.GetBytes((Int64)value); break; 295 | case AdsTypeEnum.UInt16: varValueBytes = BitConverter.GetBytes((UInt16)value); break; 296 | case AdsTypeEnum.UInt32: varValueBytes = BitConverter.GetBytes((UInt32)value); break; 297 | case AdsTypeEnum.UInt64: varValueBytes = BitConverter.GetBytes((UInt64)value); break; 298 | case AdsTypeEnum.Single: varValueBytes = BitConverter.GetBytes((Single)value); break; 299 | case AdsTypeEnum.Double: varValueBytes = BitConverter.GetBytes((Double)value); break; 300 | case AdsTypeEnum.DateTime: varValueBytes = BitConverter.GetBytes((Int32)value); break; 301 | case AdsTypeEnum.String: varValueBytes = value.ToString().ToAdsBytes(); break; 302 | case AdsTypeEnum.Date: varValueBytes = BitConverter.GetBytes((Int32)value); break; 303 | case AdsTypeEnum.Time: varValueBytes = BitConverter.GetBytes((Int32)value); break; 304 | } 305 | 306 | return varValueBytes; 307 | } 308 | 309 | private static uint GetByteLengthFromConvertible(AdsTypeEnum adsType, uint defaultStringLength) 310 | { 311 | uint length = 0; 312 | 313 | switch (adsType) 314 | { 315 | case AdsTypeEnum.Bool: length = 1; break; 316 | case AdsTypeEnum.Byte: length = 1; break; 317 | case AdsTypeEnum.Int16: length = 2; break; 318 | case AdsTypeEnum.Int32: length = 4; break; 319 | case AdsTypeEnum.Int64: length = 8; break; 320 | case AdsTypeEnum.UInt16: length = 2; break; 321 | case AdsTypeEnum.UInt32: length = 4; break; 322 | case AdsTypeEnum.UInt64: length = 8; break; 323 | case AdsTypeEnum.Single: length = 4; break; 324 | case AdsTypeEnum.Double: length = 8; break; 325 | case AdsTypeEnum.String: length = defaultStringLength; break; 326 | case AdsTypeEnum.DateTime: length = 4; break; 327 | case AdsTypeEnum.Date: length = 4; break; 328 | case AdsTypeEnum.Time: length = 4; break; 329 | } 330 | 331 | return length; 332 | } 333 | 334 | private static object ConvertBytesToType(AdsTypeEnum adsType, byte[] value) 335 | { 336 | try 337 | { 338 | switch (adsType) 339 | { 340 | case AdsTypeEnum.Bool: return BitConverter.ToBoolean(value, 0); 341 | case AdsTypeEnum.Byte: return value[0]; 342 | case AdsTypeEnum.Int16: return BitConverter.ToInt16(value, 0); 343 | case AdsTypeEnum.Int32: return BitConverter.ToInt32(value, 0); 344 | case AdsTypeEnum.Int64: return BitConverter.ToInt64(value, 0); 345 | case AdsTypeEnum.UInt16: return BitConverter.ToUInt16(value, 0); 346 | case AdsTypeEnum.UInt32: return BitConverter.ToUInt32(value, 0); 347 | case AdsTypeEnum.UInt64: return BitConverter.ToUInt64(value, 0); 348 | case AdsTypeEnum.Single: return BitConverter.ToSingle(value, 0); 349 | case AdsTypeEnum.Double: return BitConverter.ToDouble(value, 0); 350 | case AdsTypeEnum.String: return ByteArrayHelper.ByteArrayToString(value); 351 | case AdsTypeEnum.DateTime: return ByteArrayHelper.ByteArrayToDateTime(value); 352 | case AdsTypeEnum.Date: return new Date(BitConverter.ToUInt32(value, 0)); 353 | case AdsTypeEnum.Time: return new Time(BitConverter.ToUInt32(value, 0)); 354 | default: 355 | return null; 356 | } 357 | } 358 | catch 359 | { 360 | return null; 361 | } 362 | } 363 | 364 | public static AdsTypeEnum GetEnumFromType(Type type) 365 | { 366 | if (Type.Equals(type, typeof(bool))) return AdsTypeEnum.Bool; 367 | if (Type.Equals(type, typeof(byte))) return AdsTypeEnum.Byte; 368 | if (Type.Equals(type, typeof(Int16))) return AdsTypeEnum.Int16; 369 | if (Type.Equals(type, typeof(Int32))) return AdsTypeEnum.Int32; 370 | if (Type.Equals(type, typeof(Int64))) return AdsTypeEnum.Int64; 371 | if (Type.Equals(type, typeof(UInt16))) return AdsTypeEnum.UInt16; 372 | if (Type.Equals(type, typeof(UInt32))) return AdsTypeEnum.UInt32; 373 | if (Type.Equals(type, typeof(UInt64))) return AdsTypeEnum.UInt64; 374 | if (Type.Equals(type, typeof(Single))) return AdsTypeEnum.Single; 375 | if (Type.Equals(type, typeof(Double))) return AdsTypeEnum.Double; 376 | if (Type.Equals(type, typeof(String))) return AdsTypeEnum.String; 377 | if (Type.Equals(type, typeof(DateTime))) return AdsTypeEnum.DateTime; 378 | if (Type.Equals(type, typeof(Date))) return AdsTypeEnum.Date; 379 | if (Type.Equals(type, typeof(Time))) return AdsTypeEnum.Time; 380 | 381 | return AdsTypeEnum.Unknown; 382 | } 383 | #endregion 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /src/AdsClient/Helpers/IdGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | 3 | namespace Viscon.Communication.Ads.Helpers 4 | { 5 | internal class IdGenerator 6 | { 7 | private int id; 8 | 9 | public uint Next() 10 | { 11 | return (uint)Interlocked.Increment(ref id); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/AdsClient/Helpers/MemberInfoHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace Viscon.Communication.Ads.Helpers 7 | { 8 | /// 9 | /// Extension classes for MemberInfo 10 | /// 11 | public static class MemberInfoHelper 12 | { 13 | private static object memberDictLock = new object(); 14 | private static Dictionary> memberDict = new Dictionary>(); 15 | 16 | private static object adsAttributeDictLock = new object(); 17 | private static Dictionary adsAttributeDict = new Dictionary(); 18 | 19 | private static object adsSerializableDictLock = new object(); 20 | private static Dictionary adsSerializableDict = new Dictionary(); 21 | 22 | /// 23 | /// Gets a flag, whether the type is ADS serializable. 24 | /// 25 | /// The type. 26 | /// True, if the type is serializable. 27 | public static bool IsAdsSerializable(this Type type) 28 | { 29 | lock (adsSerializableDictLock) 30 | { 31 | if (!adsSerializableDict.ContainsKey(type)) 32 | adsSerializableDict.Add(type, type.GetTypeInfo().GetCustomAttributes().OfType().Count() > 0); 33 | return adsSerializableDict[type]; 34 | } 35 | } 36 | 37 | /// 38 | /// Gets the that is associated to this member. 39 | /// 40 | /// The or . 41 | /// The attached , if available. Otherwise, null. 42 | public static AdsAttribute GetAdsAttribute(this MemberInfo info) 43 | { 44 | lock (adsAttributeDictLock) 45 | { 46 | if (!adsAttributeDict.ContainsKey(info)) 47 | adsAttributeDict.Add(info, info.GetAdsAttributeInternal()); 48 | return adsAttributeDict[info]; 49 | } 50 | } 51 | 52 | /// 53 | /// Gets the that is associated to this member. 54 | /// 55 | /// The or . 56 | /// The attached , if available. Otherwise, null. 57 | private static AdsAttribute GetAdsAttributeInternal(this MemberInfo info) 58 | { 59 | var attributes = info.GetCustomAttributes(typeof(AdsAttribute), false); 60 | 61 | if ((attributes != null) && (attributes.Count() > 0)) 62 | return (AdsAttribute)attributes.FirstOrDefault(); 63 | else 64 | return null; 65 | } 66 | 67 | /// 68 | /// Get all public fields and properties. 69 | /// 70 | /// The type. 71 | /// All public fields and properties. 72 | public static IEnumerable GetPublicFieldsAndProperties(this Type type) 73 | { 74 | lock (memberDictLock) 75 | { 76 | if (!memberDict.ContainsKey(type)) 77 | memberDict.Add(type, type.GetPublicFieldsAndPropertiesInternal().ToList()); 78 | return memberDict[type]; 79 | } 80 | } 81 | 82 | /// 83 | /// Get all public fields and properties. 84 | /// 85 | /// The type. 86 | /// All public fields and properties. 87 | private static IEnumerable GetPublicFieldsAndPropertiesInternal(this Type type) 88 | { 89 | // First, return all fields. 90 | foreach (var f in type.GetRuntimeFields()) 91 | if (f.IsPublic) 92 | yield return f; 93 | 94 | // Then, return all properties. 95 | foreach (var p in type.GetRuntimeProperties()) 96 | yield return p; 97 | } 98 | 99 | /// 100 | /// Gets the member type. 101 | /// 102 | /// The or . 103 | /// The underlying member type. 104 | public static Type GetMemberType(this MemberInfo info) 105 | { 106 | if (info is FieldInfo) 107 | return ((FieldInfo)info).FieldType; 108 | else 109 | return ((PropertyInfo)info).PropertyType; 110 | } 111 | 112 | /// 113 | /// Sets the value of a member. 114 | /// 115 | /// The or . 116 | /// The object. 117 | /// The value. 118 | public static void SetValue(this MemberInfo info, object obj, object value) 119 | { 120 | if (info is FieldInfo) 121 | ((FieldInfo)info).SetValue(obj, value); 122 | else 123 | { 124 | var pi = (PropertyInfo)info; 125 | if (pi.CanWrite) 126 | pi.SetValue(obj, value); 127 | } 128 | } 129 | 130 | /// 131 | /// Gets the value of a member. 132 | /// 133 | /// The or . 134 | /// The object. 135 | /// The value. 136 | public static object GetValue(this MemberInfo info, object obj) 137 | { 138 | if (info is FieldInfo) 139 | return ((FieldInfo)info).GetValue(obj); 140 | else 141 | return ((PropertyInfo)info).GetValue(obj); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/AdsClient/Helpers/Signal.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using System.Threading.Channels; 5 | using System.Threading.Tasks; 6 | 7 | namespace Viscon.Communication.Ads.Helpers 8 | { 9 | [DebuggerNonUserCode] 10 | [DebuggerDisplay(nameof(NeedToWait) + ": {" + nameof(NeedToWait) + ",nq}")] 11 | internal class Signal : IDisposable 12 | { 13 | public Signal() 14 | { 15 | if (!channel.Writer.TryWrite(0)) 16 | { 17 | throw new Exception("Failed to initialize the send signal."); 18 | } 19 | } 20 | 21 | private readonly Channel channel = Channel.CreateBounded(1); 22 | 23 | public void Dispose() => channel.Writer.Complete(); 24 | 25 | public ValueTask WaitAsync(CancellationToken cancellationToken) => 26 | channel.Reader.ReadAsync(cancellationToken); 27 | 28 | public bool TryRelease() => channel.Writer.TryWrite(0); 29 | 30 | private bool NeedToWait => channel.Reader.Count == 0; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/AdsClient/Helpers/StringHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Text; 4 | 5 | namespace Viscon.Communication.Ads.Helpers 6 | { 7 | public static class StringHelper 8 | { 9 | public static Encoding CP1252 = CodePagesEncodingProvider.Instance.GetEncoding(1252); 10 | 11 | public static int Encode(ReadOnlySpan chars, Span buffer) 12 | { 13 | var len = CP1252.GetBytes(chars, buffer); 14 | buffer[len] = 0; 15 | 16 | return len + 1; 17 | } 18 | 19 | public static string Decode(ReadOnlySpan buffer) 20 | { 21 | var end = buffer.IndexOf((byte) 0); 22 | if (end > -1) buffer = buffer.Slice(0, end); 23 | 24 | return CP1252.GetString(buffer); 25 | } 26 | 27 | public static string Decode(ReadOnlySequence buffer) 28 | { 29 | return Decode(buffer.First.Span.Length < buffer.Length ? buffer.ToArray() : buffer.First.Span); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/AdsClient/IAmsSocket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Viscon.Communication.Ads 6 | { 7 | public interface IAmsSocket 8 | { 9 | bool Connected { get;} 10 | 11 | void Close(); 12 | 13 | Task ConnectAsync(IIncomingMessageHandler messageHandler, CancellationToken cancellationToken = default); 14 | 15 | Task SendAsync(byte[] message); 16 | 17 | Task SendAsync(ArraySegment buffer); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/AdsClient/IIncomingMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads; 4 | 5 | public interface IIncomingMessageHandler 6 | { 7 | void HandleException(Exception exception); 8 | void HandleMessage(byte[] message); 9 | } -------------------------------------------------------------------------------- /src/AdsClient/Internal/Assertions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.Internal; 4 | 5 | internal class Assertions 6 | { 7 | public static void AssertDataLength(ReadOnlySpan buffer, int length, int offset) 8 | { 9 | if (length != buffer.Length - offset) 10 | { 11 | throw new Exception( 12 | $"Received {buffer.Length} bytes of data, but length indicates {length} bytes remaining at offset {offset}, resulting in a expected total of {length + offset} bytes."); 13 | } 14 | } 15 | 16 | public static void AssertTimeoutIsValid(TimeSpan value) 17 | { 18 | var totalMilliseconds = (long)value.TotalMilliseconds; 19 | if (totalMilliseconds is < -1 or > int.MaxValue) 20 | { 21 | ThrowTimeoutIsInvalid(value); 22 | } 23 | } 24 | 25 | private static void ThrowTimeoutIsInvalid(TimeSpan value) => 26 | throw new ArgumentOutOfRangeException(nameof(value), 27 | $"The timeout {value.TotalMilliseconds}ms is less than -1 or greater than Int32.MaxValue."); 28 | } -------------------------------------------------------------------------------- /src/AdsClient/Internal/EnumExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using Viscon.Communication.Ads.Common; 3 | 4 | namespace Viscon.Communication.Ads.Internal; 5 | 6 | internal static class EnumExtensions 7 | { 8 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 9 | public static uint ToUInt32(this AdsReservedIndexGroup indexGroup) => 10 | Unsafe.As(ref indexGroup); 11 | 12 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 13 | public static AdsDataType ToDataType(this uint value) => Unsafe.As(ref value); 14 | 15 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 16 | public static AdsDataTypeFlags ToDataTypeFlags(this uint value) => Unsafe.As(ref value); 17 | } -------------------------------------------------------------------------------- /src/AdsClient/Internal/LittleEndianDeserializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Buffers.Binary; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace Viscon.Communication.Ads.Internal; 7 | 8 | /// 9 | /// Serializer to read little endian values from the input stream. 10 | /// 11 | /// 12 | /// Heavily inspired by the NetworkOrderDeserializer from RabbitMQ.Client 13 | /// (https://github.com/rabbitmq/rabbitmq-dotnet-client). 14 | /// 15 | internal static class LittleEndianDeserializer 16 | { 17 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 18 | public static ushort ReadUInt16(ReadOnlySpan span) 19 | { 20 | return BinaryPrimitives.ReadUInt16LittleEndian(span); 21 | } 22 | 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | public static ushort ReadUInt16(ReadOnlySequence buffer) 25 | { 26 | if (!BinaryPrimitives.TryReadUInt16LittleEndian(buffer.First.Span, out var value)) 27 | { 28 | Span bytes = stackalloc byte[4]; 29 | buffer.Slice(0, 4).CopyTo(bytes); 30 | value = BinaryPrimitives.ReadUInt16LittleEndian(bytes); 31 | } 32 | 33 | return value; 34 | } 35 | 36 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 37 | public static int ReadInt32(ReadOnlySpan span) 38 | { 39 | return BinaryPrimitives.ReadInt32LittleEndian(span); 40 | } 41 | 42 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 43 | public static uint ReadUInt32(ReadOnlySpan span) 44 | { 45 | return BinaryPrimitives.ReadUInt32LittleEndian(span); 46 | } 47 | 48 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 49 | public static uint ReadUInt32(ReadOnlySequence buffer) 50 | { 51 | if (!BinaryPrimitives.TryReadUInt32LittleEndian(buffer.First.Span, out var value)) 52 | { 53 | Span bytes = stackalloc byte[4]; 54 | buffer.Slice(0, 4).CopyTo(bytes); 55 | value = BinaryPrimitives.ReadUInt32LittleEndian(bytes); 56 | } 57 | 58 | return value; 59 | } 60 | } -------------------------------------------------------------------------------- /src/AdsClient/Internal/LittleEndianSerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace Viscon.Communication.Ads.Internal; 6 | 7 | /// 8 | /// Serializer to write little endian values to the output stream. 9 | /// 10 | /// 11 | /// Heavily inspired by the NetworkOrderSerializer from RabbitMQ.Client 12 | /// (https://github.com/rabbitmq/rabbitmq-dotnet-client). 13 | /// 14 | internal static class LittleEndianSerializer 15 | { 16 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 17 | public static void WriteUInt16(ref byte destination, ushort val) 18 | { 19 | Unsafe.WriteUnaligned(ref destination, 20 | BitConverter.IsLittleEndian ? val : BinaryPrimitives.ReverseEndianness(val)); 21 | } 22 | 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | public static void WriteUInt32(ref byte destination, uint val) 25 | { 26 | Unsafe.WriteUnaligned(ref destination, 27 | BitConverter.IsLittleEndian ? val : BinaryPrimitives.ReverseEndianness(val)); 28 | } 29 | } -------------------------------------------------------------------------------- /src/AdsClient/Internal/SocketAwaitable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Sockets; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading; 5 | 6 | namespace Viscon.Communication.Ads.Internal; 7 | 8 | /// 9 | /// Reusable awaitable for socket operations. 10 | /// 11 | /// 12 | /// Based on https://devblogs.microsoft.com/pfxteam/awaiting-socket-operations/. 13 | /// 14 | internal sealed class SocketAwaitable : SocketAsyncEventArgs, INotifyCompletion 15 | { 16 | private static readonly Action Sentinel = () => { }; 17 | 18 | public volatile bool WasCompleted; 19 | private Action? continuation; 20 | 21 | protected override void OnCompleted(SocketAsyncEventArgs _) 22 | { 23 | var prev = continuation ?? Interlocked.CompareExchange(ref continuation, Sentinel, null); 24 | prev?.Invoke(); 25 | } 26 | 27 | internal void Reset() 28 | { 29 | WasCompleted = false; 30 | continuation = null; 31 | } 32 | 33 | public SocketAwaitable GetAwaiter() 34 | { 35 | return this; 36 | } 37 | 38 | public bool IsCompleted => WasCompleted; 39 | 40 | public void OnCompleted(Action continuation) 41 | { 42 | if (this.continuation == Sentinel || 43 | Interlocked.CompareExchange(ref this.continuation, continuation, null) == Sentinel) 44 | { 45 | continuation.Invoke(); 46 | } 47 | } 48 | 49 | public void GetResult() 50 | { 51 | if (SocketError != SocketError.Success) 52 | { 53 | throw new SocketException((int)SocketError); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/AdsClient/Internal/SocketExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Sockets; 2 | 3 | namespace Viscon.Communication.Ads.Internal; 4 | 5 | /// 6 | /// Socket extensions to send and receive using . 7 | /// 8 | /// 9 | /// Based on https://devblogs.microsoft.com/pfxteam/awaiting-socket-operations/. 10 | /// 11 | internal static class SocketExtensions 12 | { 13 | public static SocketAwaitable ReceiveAwaitable(this Socket socket, SocketAwaitable awaitable) 14 | { 15 | awaitable.Reset(); 16 | if (!socket.ReceiveAsync(awaitable)) 17 | awaitable.WasCompleted = true; 18 | return awaitable; 19 | } 20 | 21 | public static SocketAwaitable SendAwaitable(this Socket socket, SocketAwaitable awaitable) 22 | { 23 | awaitable.Reset(); 24 | if (!socket.SendAsync(awaitable)) 25 | awaitable.WasCompleted = true; 26 | return awaitable; 27 | } 28 | } -------------------------------------------------------------------------------- /src/AdsClient/Internal/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Viscon.Communication.Ads.Internal; 6 | 7 | /// 8 | /// Serializer to write little endian values to the output stream. 9 | /// 10 | /// 11 | /// Heavily inspired by the TypeExtensions from RabbitMQ.Client 12 | /// (https://github.com/rabbitmq/rabbitmq-dotnet-client). 13 | /// 14 | internal static class TypeExtensions 15 | { 16 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 17 | public static ref byte GetStart(this Span span) 18 | { 19 | return ref MemoryMarshal.GetReference(span); 20 | } 21 | 22 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 23 | public static ref byte GetOffset(this Span span, int offset) 24 | { 25 | return ref span.GetStart().GetOffset(offset); 26 | } 27 | 28 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 29 | public static ref byte GetOffset(this ref byte source, int offset) 30 | { 31 | // The cast to uint is in order to avoid a sign-extending move 32 | // in the machine code. 33 | return ref Unsafe.Add(ref source, (uint)offset); 34 | } 35 | } -------------------------------------------------------------------------------- /src/AdsClient/Internal/WireFormatting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.CompilerServices; 4 | using Viscon.Communication.Ads.Helpers; 5 | 6 | namespace Viscon.Communication.Ads.Internal; 7 | 8 | /// 9 | /// Helpers for reading and writing data in the expect wire format for the ADS protocol. 10 | /// 11 | public static class WireFormatting 12 | { 13 | public static int ReadInt32(ReadOnlySpan span, out int value) 14 | { 15 | value = LittleEndianDeserializer.ReadInt32(span); 16 | return sizeof(int); 17 | } 18 | 19 | public static int ReadString(ReadOnlySpan span, ushort length, out string value) 20 | { 21 | value = StringHelper.Decode(span.Slice(0, length)); 22 | return length + 1; 23 | } 24 | 25 | public static int ReadUInt16(ReadOnlySpan span, out ushort value) 26 | { 27 | value = LittleEndianDeserializer.ReadUInt16(span); 28 | return sizeof(ushort); 29 | } 30 | 31 | public static int ReadUInt32(ReadOnlySpan span, out uint value) 32 | { 33 | value = LittleEndianDeserializer.ReadUInt32(span); 34 | return sizeof(uint); 35 | } 36 | 37 | public static int WriteString(Span destination, string value) 38 | { 39 | return StringHelper.Encode(value.AsSpan(), destination); 40 | } 41 | 42 | public static int WriteUInt16(ref byte destination, ushort value) 43 | { 44 | LittleEndianSerializer.WriteUInt16(ref destination, value); 45 | return sizeof(ushort); 46 | } 47 | 48 | public static int WriteUInt16(ref byte destination, T value) 49 | { 50 | Debug.Assert(Unsafe.SizeOf() == sizeof(ushort), 51 | $"Passed type {typeof(T)} of size {Unsafe.SizeOf()} to method {nameof(WriteUInt32)}, expected size of {sizeof(ushort)}"); 52 | 53 | return WriteUInt16(ref destination, Unsafe.As(ref value)); 54 | } 55 | 56 | public static int WriteUInt32(ref byte destination, uint value) 57 | { 58 | LittleEndianSerializer.WriteUInt32(ref destination, value); 59 | return sizeof(uint); 60 | } 61 | 62 | public static int WriteUInt32(ref byte destination, T value) where T : struct 63 | { 64 | Debug.Assert(Unsafe.SizeOf() == sizeof(uint), $"Passed type {typeof(T)} of size {Unsafe.SizeOf()} to method {nameof(WriteUInt32)}, expected size of {sizeof(uint)}"); 65 | 66 | return WriteUInt32(ref destination, Unsafe.As(ref value)); 67 | } 68 | } -------------------------------------------------------------------------------- /src/AdsClient/Special/AdsSpecial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Viscon.Communication.Ads.Commands; 6 | using Viscon.Communication.Ads.Common; 7 | using Viscon.Communication.Ads.Helpers; 8 | 9 | namespace Viscon.Communication.Ads.Special 10 | { 11 | /// 12 | /// Special functions. (functionality not documented by Beckhoff) 13 | /// 14 | public class AdsSpecial 15 | { 16 | private Ams ams; 17 | 18 | internal AdsSpecial(Ams ams) 19 | { 20 | this.ams = ams; 21 | } 22 | 23 | /// 24 | /// Get an xml description of the plc 25 | /// You can use XDocument.Parse(xml).ToString() to make the xml more readable 26 | /// 27 | /// The token to monitor for cancellation requests. 28 | /// 29 | public async Task GetTargetDescAsync(CancellationToken cancellationToken = default) 30 | { 31 | AdsReadCommand adsCommand = new AdsReadCommand(0x000002bc, 0x00000001, 4) { AmsPortTargetOverride = 10000 }; 32 | var result = await adsCommand.RunAsync(ams, cancellationToken).ConfigureAwait(false); 33 | uint length = BitConverter.ToUInt32(result.Data, 0); 34 | adsCommand = new AdsReadCommand(0x000002bc, 0x00000001, length) { AmsPortTargetOverride = 10000 }; 35 | result = await adsCommand.RunAsync(ams, cancellationToken).ConfigureAwait(false); 36 | string xml = ByteArrayHelper.ByteArrayToString(result.Data); 37 | return xml; 38 | } 39 | 40 | /// 41 | /// Get the current routes 42 | /// 43 | /// The token to monitor for cancellation requests. 44 | /// 45 | public async Task> GetCurrentRoutesAsync(CancellationToken cancellationToken = default) 46 | { 47 | bool ok = true; 48 | uint index = 0; 49 | var routes = new List(); 50 | 51 | while (ok) 52 | { 53 | try 54 | { 55 | AdsReadCommand adsCommand = 56 | new AdsReadCommand(0x00000323, index++, 0x0800) { AmsPortTargetOverride = 10000 }; 57 | var result = await adsCommand.RunAsync(ams, cancellationToken).ConfigureAwait(false); 58 | int length = result.Data.Length - 44; 59 | byte[] routeBytes = new byte[length]; 60 | Array.Copy(result.Data, 44, routeBytes, 0, length); 61 | string routeString = ByteArrayHelper.ByteArrayToString(routeBytes); 62 | int stringLlength = routeString.Length + 1; 63 | Array.Copy(routeBytes, stringLlength, routeBytes, 0, length - stringLlength); 64 | routeString += " " + ByteArrayHelper.ByteArrayToString(routeBytes); 65 | routes.Add(routeString); 66 | } 67 | catch (AdsException ex) 68 | { 69 | if ((uint) ex.ErrorCode == 1814) ok = false; 70 | else throw; 71 | } 72 | } 73 | return routes; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/AdsClient/Variables/ArraySegmentVariableData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.Variables; 4 | 5 | public record ArraySegmentVariableData(IVariableAddress Address, ArraySegment Data) : IVariableData 6 | { 7 | ReadOnlySpan IVariableData.Data => Data; 8 | } -------------------------------------------------------------------------------- /src/AdsClient/Variables/ArrayVariableData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.Variables; 4 | 5 | public record ArrayVariableData(IVariableAddress Address, byte[] Data) : IVariableData 6 | { 7 | ReadOnlySpan IVariableData.Data => Data; 8 | } -------------------------------------------------------------------------------- /src/AdsClient/Variables/IVariableAddress.cs: -------------------------------------------------------------------------------- 1 | namespace Viscon.Communication.Ads.Variables; 2 | 3 | public interface IVariableAddress 4 | { 5 | uint IndexGroup { get; } 6 | uint IndexOffset { get; } 7 | } -------------------------------------------------------------------------------- /src/AdsClient/Variables/IVariableAddressAndSize.cs: -------------------------------------------------------------------------------- 1 | namespace Viscon.Communication.Ads.Variables; 2 | 3 | public interface IVariableAddressAndSize : IVariableAddress 4 | { 5 | uint Size { get; } 6 | } -------------------------------------------------------------------------------- /src/AdsClient/Variables/IVariableData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.Variables; 4 | 5 | public interface IVariableData 6 | { 7 | IVariableAddress Address { get; } 8 | ReadOnlySpan Data { get; } 9 | } -------------------------------------------------------------------------------- /src/AdsClient/Variables/MemoryVariableData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Viscon.Communication.Ads.Variables; 4 | 5 | public record MemoryVariableData(IVariableAddress Address, ReadOnlyMemory Data) : IVariableData 6 | { 7 | ReadOnlySpan IVariableData.Data => Data.Span; 8 | } -------------------------------------------------------------------------------- /src/AdsClient/Variables/VariableAddress.cs: -------------------------------------------------------------------------------- 1 | namespace Viscon.Communication.Ads.Variables; 2 | 3 | public record VariableAddress(uint IndexGroup, uint IndexOffset) : IVariableAddress; -------------------------------------------------------------------------------- /src/AdsClient/Variables/VariableAddressAndSize.cs: -------------------------------------------------------------------------------- 1 | namespace Viscon.Communication.Ads.Variables; 2 | 3 | public record VariableAddressAndSize(uint IndexGroup, uint IndexOffset, uint Size) : VariableAddress(IndexGroup, IndexOffset), IVariableAddressAndSize; -------------------------------------------------------------------------------- /src/AdsClient/Viscon.Communication.Ads.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.1;netstandard2.0;net462 4 | latest 5 | portable 6 | 7 | 8 | Copyright (c) 2011 Inando 9 | Copyright (c) 2023 Viscon Factory Intelligence B.V. 10 | 11 | AdsClient PCL library 12 | 13 | A client implementation of the Beckhoff Twincat ADS protocol, targeting 14 | NetStandard2.0 compatible platforms. 15 | 16 | inando;mycroes 17 | README.md 18 | icon.png 19 | MIT 20 | Ads;AdsClient;Twincat;Beckhoff 21 | https://github.com/VisconFactoryIntelligence/AdsClient 22 | https://github.com/VisconFactoryIntelligence/AdsClient.git 23 | 24 | true 25 | true 26 | snupkg 27 | true 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | runtime; build; native; contentfiles; analyzers; buildtransitive 37 | all 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/AdsDumper/AdsDumper.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/AdsDumper/Program.cs: -------------------------------------------------------------------------------- 1 | using Viscon.Communication.Ads; 2 | 3 | using var client = new AdsClient("172.31.20.201.1.1", "172.31.20.20", "172.31.20.20.1.1"); 4 | try 5 | { 6 | await client.Ams.ConnectAsync(); 7 | Console.WriteLine("Connected"); 8 | 9 | Console.WriteLine(); 10 | Console.WriteLine("Reading device info..."); 11 | var deviceInfo = await client.ReadDeviceInfoAsync(); 12 | Console.WriteLine(deviceInfo); 13 | 14 | Console.WriteLine(); 15 | Console.WriteLine("Reading device state..."); 16 | var deviceState = await client.ReadStateAsync(); 17 | Console.WriteLine(deviceState); 18 | 19 | Console.WriteLine(); 20 | Console.WriteLine("Reading symbols..."); 21 | var symbols = await client.GetSymbolsAsync(); 22 | 23 | Console.WriteLine($"Name\tTypeName (size)\tComment\tIndexGroup\tIndexOffset"); 24 | foreach (var symbol in symbols) 25 | { 26 | Console.WriteLine( 27 | $"{symbol.Name}\t{symbol.TypeName} ({symbol.Size})\t{symbol.Comment}\t{symbol.IndexGroup}\t{symbol.IndexOffset}"); 28 | } 29 | 30 | Console.WriteLine(); 31 | Console.WriteLine("Reading types..."); 32 | var types = await client.GetDataTypesAsync(); 33 | 34 | Console.WriteLine("Name\tType (size)\tComment\tDataType\tVersion"); 35 | foreach (var type in types) 36 | { 37 | Console.WriteLine($"{type.Name}\t{type.Type} ({type.Size})\t{type.Comment}\t{type.DataType}\t{type.Version}"); 38 | foreach (var subItem in type.SubItems) 39 | { 40 | Console.WriteLine( 41 | $"- {subItem.Name}\t{subItem.Type} ({subItem.Size})\t{subItem.Comment}\t{subItem.DataType}\t{subItem.Version}"); 42 | } 43 | } 44 | } 45 | catch (Exception e) 46 | { 47 | Console.WriteLine($"Unhandled exception occurred: {e}"); 48 | Environment.Exit(1); 49 | } 50 | -------------------------------------------------------------------------------- /test/AdsClient/AdsCommandsAsyncTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using FakeItEasy; 6 | using Shouldly; 7 | using Viscon.Communication.Ads.Common; 8 | using Viscon.Communication.Ads.Helpers; 9 | using Xunit; 10 | 11 | namespace Viscon.Communication.Ads.Test 12 | { 13 | public class AdsCommandsAsyncTest : IDisposable 14 | { 15 | private readonly IAmsSocket amsSocket = A.Fake(); 16 | private readonly AdsClient client; 17 | private IIncomingMessageHandler messageHandler; 18 | private bool connected; 19 | 20 | public AdsCommandsAsyncTest() 21 | { 22 | A.CallTo(() => amsSocket.ConnectAsync(A.Ignored, A.Ignored)) 23 | .ReturnsLazily(call => 24 | { 25 | messageHandler = call.GetArgument(0); 26 | connected = true; 27 | return Task.CompletedTask; 28 | }); 29 | A.CallTo(() => amsSocket.Connected).ReturnsLazily(() => connected); 30 | 31 | client = new AdsClient(amsNetIdSource: "10.0.0.120.1.1", amsSocket: amsSocket, 32 | amsNetIdTarget: "5.1.204.123.1.1"); 33 | 34 | if (!client.Ams.ConnectAsync().IsCompletedSuccessfully) 35 | { 36 | throw new Exception("Connect call should have completed synchronously."); 37 | } 38 | } 39 | 40 | public void Dispose() 41 | { 42 | client.Dispose(); 43 | } 44 | 45 | [Fact] 46 | public void ReadDeviceInfoAsync() 47 | { 48 | // Arrange 49 | var msgSend = new byte[] 50 | { 51 | 0, 0, 32, 0, 0, 0, 5, 1, 204, 123, 1, 1, 33, 3, 10, 0, 0, 120, 1, 1, 137, 128, 1, 0, 4, 0, 0, 0, 0, 52 | 0, 0, 0, 0, 0, 1, 0, 0, 0 53 | }; 54 | 55 | var msgReceive = new byte[] 56 | { 57 | 0, 0, 56, 0, 0, 0, 10, 0, 0, 120, 1, 1, 137, 128, 5, 1, 204, 123, 1, 1, 33, 3, 1, 0, 5, 0, 24, 0, 0, 58 | 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 11, 234, 5, 84, 67, 97, 116, 80, 108, 99, 67, 116, 114, 59 | 108, 0, 0, 0, 0, 0 60 | }; 61 | 62 | SetupRequestResponse(msgSend, msgReceive); 63 | 64 | // Act 65 | var task = client.ReadDeviceInfoAsync(); 66 | 67 | // Assert 68 | task.IsCompleted.ShouldBeTrue("Call should have completed synchronously."); 69 | AdsDeviceInfo deviceInfo = task.Result; 70 | deviceInfo.ToString().ShouldBe("Version: 2.11.1514 Devicename: TCatPlcCtrl"); 71 | } 72 | 73 | [Fact] 74 | public void ReadStateAsync() 75 | { 76 | // Arrange 77 | var msgSend = new byte[] 78 | { 79 | 0, 0, 32, 0, 0, 0, 5, 1, 204, 123, 1, 1, 33, 3, 10, 0, 0, 120, 1, 1, 137, 128, 4, 0, 4, 0, 0, 0, 0, 80 | 0, 0, 0, 0, 0, 1, 0, 0, 0 81 | }; 82 | 83 | var msgReceive = new byte[] 84 | { 85 | 0, 0, 40, 0, 0, 0, 10, 0, 0, 120, 1, 1, 137, 128, 5, 1, 204, 123, 1, 1, 33, 3, 4, 0, 5, 0, 8, 0, 0, 86 | 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0 87 | }; 88 | 89 | SetupRequestResponse(msgSend, msgReceive); 90 | 91 | // Act 92 | var task = client.ReadStateAsync(); 93 | 94 | // Assert 95 | task.IsCompleted.ShouldBeTrue("Call should have completed synchronously."); 96 | var state = task.Result; 97 | state.ToString().ShouldBe("Ads state: 5 (Run) Device state: 0"); 98 | } 99 | 100 | [Fact] 101 | public void GetSymhandleByNameAsync() 102 | { 103 | // Arrange 104 | var msgSend = new byte[] 105 | { 106 | 0, 0, 58, 0, 0, 0, 5, 1, 204, 123, 1, 1, 33, 3, 10, 0, 0, 120, 1, 1, 137, 128, 9, 0, 4, 0, 26, 0, 0, 107 | 0, 0, 0, 0, 0, 1, 0, 0, 0, 3, 240, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 10, 0, 0, 0, 46, 84, 69, 83, 84, 108 | 84, 73, 77, 69, 0 109 | }; 110 | 111 | var msgReceive = new byte[] 112 | { 113 | 0, 0, 44, 0, 0, 0, 10, 0, 0, 120, 1, 1, 137, 128, 5, 1, 204, 123, 1, 1, 33, 3, 9, 0, 5, 0, 12, 0, 0, 114 | 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 141, 2, 0, 164 115 | }; 116 | 117 | SetupRequestResponse(msgSend, msgReceive); 118 | 119 | // Act 120 | var task = client.GetSymhandleByNameAsync(".TESTTIME"); 121 | 122 | // Assert 123 | task.IsCompleted.ShouldBeTrue("Call should have completed synchronously."); 124 | var handle = task.Result; 125 | handle.ShouldBe(2751464077); 126 | } 127 | 128 | private void SetupRequestResponse(byte[] tx, byte[] rx) 129 | { 130 | const int invocationIdOffset = 34; 131 | 132 | var isMatch = (byte[] sendData) => 133 | { 134 | var beforeInvocationId = new Range(Index.Start, invocationIdOffset); 135 | var afterInvocationId = new Range(invocationIdOffset + sizeof(uint), Index.End); 136 | 137 | return tx[beforeInvocationId].SequenceEqual(sendData[beforeInvocationId]) && 138 | tx[afterInvocationId].SequenceEqual(sendData[afterInvocationId]); 139 | }; 140 | 141 | A.CallTo(() => amsSocket.SendAsync(A.That.Matches(buffer => isMatch(buffer)))) 142 | .ReturnsLazily(call => 143 | { 144 | // The receive header is consumed by the AmsSocket, so strip it from the data passed to Ams 145 | var res = rx[AmsHeaderHelper.AmsTcpHeaderSize..]; 146 | 147 | // Copy the invocation ID from request to response 148 | var buffer = call.GetArgument(0); 149 | buffer.AsSpan().Slice(invocationIdOffset, sizeof(uint)) 150 | .CopyTo(res.AsSpan()[(invocationIdOffset - AmsHeaderHelper.AmsTcpHeaderSize)..]); 151 | 152 | messageHandler.HandleMessage(res); 153 | 154 | return Task.CompletedTask; 155 | }); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /test/AdsClient/Viscon.Communication.Ads.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6 4 | webding 5 | Inando 6 | latest 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | --------------------------------------------------------------------------------