├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml ├── release.yml └── workflows │ ├── BuildTestDeploy.yml │ └── DependabotAutoMerge.yml ├── .gitignore ├── Directory.Build.props ├── LICENSE.txt ├── PLCOnly.slnf ├── PlcInterface.TestPLC.sln ├── PlcInterface.sln ├── README.md ├── global.json ├── src ├── Common │ ├── IObservableExtensions.cs │ ├── IServiceCollectionExtension.cs │ ├── ISymbolInfoExtension.cs │ ├── IndicesHelper.cs │ ├── TaskExtensions.cs │ └── ThrowHelper.cs ├── PlcInterface.Abstraction │ ├── ArrayWrapperExtensions.cs │ ├── Connected.cs │ ├── Connected{T}.cs │ ├── IArrayWrapper.cs │ ├── IConnected.cs │ ├── IMonitor.cs │ ├── IMonitorExtension.cs │ ├── IMonitorResult.cs │ ├── IPlcConnection.cs │ ├── IPlcConnectionExtension.cs │ ├── IReadWrite.cs │ ├── IReadWriteExtension.cs │ ├── ISymbolHandler.cs │ ├── ISymbolInfo.cs │ ├── ITypeActivator.cs │ ├── ITypeConverter.cs │ ├── NonZeroBasedArray.cs │ ├── ObjectActivator.cs │ ├── PlcInterface.Abstraction.csproj │ ├── PropertySetterHelper.cs │ ├── StructActivator.cs │ ├── SymbolException.cs │ └── TypeConverter.cs ├── PlcInterface.Ads │ ├── AdsPlcConnectionOptions.cs │ ├── AdsSymbolHandlerOptions.cs │ ├── AdsTypeConverter.cs │ ├── DefaultAdsPlcConnectionConfigureOptions.cs │ ├── DefaultAdsSymbolHandlerSettingsConfigureOptions.cs │ ├── DisposableMonitorItem.cs │ ├── DynamicObjectExtensions.cs │ ├── IAdsMonitor.cs │ ├── IAdsPlcConnection.cs │ ├── IAdsReadWrite.cs │ ├── IAdsSymbolHandler.cs │ ├── IAdsSymbolInfo.cs │ ├── IAdsTypeConverter.cs │ ├── IServiceCollectionExtension.cs │ ├── ISymbolInfoExtension.cs │ ├── ISymbolLoaderFactory.cs │ ├── IValueSymbolExtensions.cs │ ├── Monitor.Logging.cs │ ├── Monitor.cs │ ├── MonitorResult.cs │ ├── ObjectExtension.cs │ ├── PlcConnection.cs │ ├── PlcInterface.Ads.csproj │ ├── ReadWrite.cs │ ├── SymbolHandler.Logging.cs │ ├── SymbolHandler.cs │ ├── SymbolInfo.cs │ ├── TcAdsClientExtension.cs │ └── TwincatAbstractions │ │ ├── ISumSymbolFactory.cs │ │ ├── ISumSymbolRead.cs │ │ ├── ISumSymbolWrite.cs │ │ ├── SumSymbolFactory.cs │ │ ├── SumSymbolReadAbstraction.cs │ │ ├── SumSymbolWriteAbstraction.cs │ │ └── SymbolLoaderFactoryAbstraction.cs ├── PlcInterface.OpcUa │ ├── DefaultOpcPlcConnectionConfigureOptions.cs │ ├── DefaultOpcSymbolHandlerSettingsConfigureOptions.cs │ ├── ICollectionExtensions.cs │ ├── IOpcMonitor.cs │ ├── IOpcPlcConnection.cs │ ├── IOpcReadWrite.cs │ ├── IOpcSymbolHandler.cs │ ├── IOpcSymbolInfo.cs │ ├── IOpcTypeConverter.cs │ ├── IServiceCollectionExtension.cs │ ├── ISymbolInfoExtension.cs │ ├── Monitor.Logging.cs │ ├── Monitor.cs │ ├── MonitorResult.cs │ ├── NodeInfo.cs │ ├── OpcPlcConnectionOptions.cs │ ├── OpcSymbolHandlerOptions.cs │ ├── OpcTypeConverter.cs │ ├── PlcConnection.Logging.cs │ ├── PlcConnection.cs │ ├── PlcInterface.OpcUa.csproj │ ├── ReadWrite.cs │ ├── SessionExtensions.cs │ ├── SymbolHandler.Logging.cs │ ├── SymbolHandler.cs │ ├── SymbolInfo.cs │ ├── TreeBrowser.Logging.cs │ ├── TreeBrowser.cs │ └── WrappedSession.cs └── PlcInterface.Sandbox │ ├── PLCCommands │ ├── AdsPlcConnectCommand.cs │ ├── AdsPlcDisconnectCommand.cs │ ├── AdsWriteCommand.cs │ ├── OpcWriteCommand.cs │ ├── PlcConnectCommand.cs │ ├── PlcDisconnectCommand.cs │ ├── PlcMonitorCommand.cs │ ├── PlcReadCommand.cs │ ├── PlcStopMonitorCommand.cs │ ├── PlcSymbolAutoCompleteHandler.cs │ ├── PlcSymbolDumpCommand.cs │ ├── PlcToggleCommand.cs │ └── PlcWriteCommand.cs │ ├── PlcInterface.Sandbox.csproj │ ├── Program.cs │ ├── Properties │ └── PublishProfiles │ │ └── FolderProfile.pubxml │ ├── appsettings.json │ └── nlog.config ├── test ├── .editorconfig ├── Directory.Build.props ├── PlcInterface.Abstraction │ ├── ConnectedTests.cs │ ├── IMonitorExtensionTests.cs │ ├── IPlcConnectionExtensionTests.cs │ ├── IReadWriteExtensionTests.cs │ ├── MyTypeBuilder.cs │ ├── ObjectActivatorTests.cs │ ├── PlcInterface.Abstraction.Tests.csproj │ ├── PropertySetterHelperTests.cs │ ├── StructActivatorTests.cs │ ├── TypeConverterMock.cs │ └── TypeConverterTests.cs ├── PlcInterface.Ads.IntegrationTests │ ├── Assembly.cs │ ├── DummyTest.cs │ ├── MonitorTest.cs │ ├── PlcConnectionTest.cs │ ├── PlcInterface.Ads.IntegrationTests.csproj │ ├── ReadValueTest.cs │ ├── Settings.cs │ ├── SymbolHandlerTest.cs │ └── WriteValueTest.cs ├── PlcInterface.Ads.PLC │ ├── PLC_Main │ │ ├── DUTs │ │ │ ├── DUT_TestStruct.TcDUT │ │ │ ├── DUT_TestStruct2.TcDUT │ │ │ ├── MonitorTest.TcDUT │ │ │ ├── MonitorTestData.TcDUT │ │ │ ├── ReadTest.TcDUT │ │ │ ├── ReadTestData.TcDUT │ │ │ ├── SymbolTest.TcDUT │ │ │ ├── TestEnum.TcDUT │ │ │ ├── WriteTest.TcDUT │ │ │ └── WriteTestData.TcDUT │ │ ├── GVLs │ │ │ ├── AdsNet8.TcGVL │ │ │ ├── AdsNet9.TcGVL │ │ │ ├── OpcNet8.TcGVL │ │ │ └── OpcNet9.TcGVL │ │ ├── PLC_Main.noprjfile │ │ ├── PLC_Main.plcproj │ │ ├── POUs │ │ │ ├── MAIN.TcPOU │ │ │ └── ResetWriteData.TcPOU │ │ └── PlcTask.TcTTO │ └── PlcInterface.Ads.PLC.tsproj ├── PlcInterface.Ads.Tests │ ├── AdsTypeConverterTests.cs │ ├── Assembly.cs │ ├── DynamicObjectExtensionsTests.cs │ ├── IServiceCollectionExtensionTests.cs │ ├── ISymbolInfoExtensionTests.cs │ ├── IValueSymbolExtensionsTests.cs │ ├── MonitorTests.cs │ ├── PlcConnectionTests.cs │ ├── PlcInterface.Ads.Tests.csproj │ ├── ReadWriteTests.cs │ ├── SymbolHandlerTests.cs │ └── TcAdsClientExtensionTests.cs ├── PlcInterface.Common.Tests │ ├── IObservableExtensionsTests.cs │ ├── IServiceCollectionExtensionTests.cs │ ├── ISymbolInfoExtensionTests.cs │ ├── IndicesHelperTests.cs │ ├── PlcInterface.Common.Tests.csproj │ └── TaskExtensionsTests.cs ├── PlcInterface.IntegrationTests │ ├── CIConditionAttribute.cs │ ├── DataTypes │ │ ├── DUT_TestClass.cs │ │ ├── DUT_TestClass2.cs │ │ ├── DUT_TestStruct.cs │ │ ├── DUT_TestStruct2.cs │ │ └── TestEnum.cs │ ├── Extension │ │ ├── AssertObjectValue.cs │ │ └── MethodInfoExtensions.cs │ ├── IMonitorTestBase.cs │ ├── IPlcConnectionTestBase.cs │ ├── IReadValueTestBase.cs │ ├── ISymbolHandlerTestBase.cs │ ├── IWriteValueTestBase.cs │ ├── MultiAssert.cs │ └── PlcInterface.IntegrationTests.csproj ├── PlcInterface.Opc.IntegrationTests │ ├── Assembly.cs │ ├── DummyTest.cs │ ├── MonitorTest.cs │ ├── PlcConnectionTest.cs │ ├── PlcInterface.Opc.IntegrationTests.csproj │ ├── ReadValueTest.cs │ ├── Settings.cs │ ├── SymbolHandlerTest.cs │ └── WriteValueTest.cs ├── PlcInterface.OpcUa.OpcServer │ ├── OPCServer.tcopcuasrv │ ├── OPCServer │ │ ├── Alarms and Conditions │ │ │ └── Alarms and Conditions.ac │ │ ├── Data Access │ │ │ └── Data Access.opcuada │ │ ├── Historical Access │ │ │ └── Historical Access.opcuaha │ │ ├── Resources │ │ │ └── English (United States).reslang │ │ └── Security Access │ │ │ └── Security Access.sec │ └── PlcInterface.OpcUa.OpcServer.tcconnproj ├── PlcInterface.OpcUa.Tests │ ├── Assembly.cs │ ├── IServiceCollectionExtensionTest.cs │ ├── ISymbolInfoExtensionTests.cs │ ├── PlcInterface.OpcUa.Tests.csproj │ └── SymbolHandlerTests.cs ├── TestUtilities │ ├── MockDelegates.cs │ ├── MockHelpers..cs │ └── TestUtilities.csproj └── testconfig.json └── version.json /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | open-pull-requests-limit: 5 6 | rebase-strategy: auto 7 | schedule: 8 | interval: weekly 9 | day: friday 10 | time: "23:00" 11 | timezone: Europe/Amsterdam 12 | - package-ecosystem: nuget 13 | directory: / 14 | open-pull-requests-limit: 5 15 | rebase-strategy: auto 16 | schedule: 17 | interval: weekly 18 | day: friday 19 | time: "23:00" 20 | timezone: Europe/Amsterdam 21 | groups: 22 | Beckhoff: 23 | patterns: 24 | - "Beckhoff.TwinCAT.Ads" 25 | - "Beckhoff.TwinCAT.Ads.*" 26 | MSTest: 27 | patterns: 28 | - "MSTest.*" 29 | Coverlet: 30 | patterns: 31 | - "coverlet.*" 32 | IOAbstraction: 33 | patterns: 34 | - "System.IO.Abstractions" 35 | - "System.IO.Abstractions.*" 36 | Analyzers: 37 | patterns: 38 | - "*Analyzer*" 39 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: 🏕 Features 4 | labels: 5 | - '*' 6 | exclude: 7 | labels: 8 | - dependencies 9 | - title: 👒 Dependencies 10 | labels: 11 | - dependencies 12 | -------------------------------------------------------------------------------- /.github/workflows/BuildTestDeploy.yml: -------------------------------------------------------------------------------- 1 | name: BuildTestDeploy 2 | on: 3 | workflow_dispatch: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+" 7 | branches: 8 | - 'main' 9 | paths-ignore: 10 | - '.editorconfig' 11 | - '.gitattributes' 12 | - '.gitignore' 13 | - 'LICENSE.txt' 14 | - 'README.md' 15 | 16 | pull_request: 17 | branches: 18 | - 'main' 19 | paths-ignore: 20 | - '.editorconfig' 21 | - '.gitattributes' 22 | - '.gitignore' 23 | - 'LICENSE.txt' 24 | - 'README.md' 25 | 26 | concurrency: 27 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 28 | cancel-in-progress: true 29 | 30 | jobs: 31 | call-build-test: 32 | strategy: 33 | matrix: 34 | os: [ ubuntu-latest, windows-latest, macos-latest ] 35 | fail-fast: true 36 | uses: Vectron/GithubWorkflows/.github/workflows/BuildAndTest.yml@main 37 | with: 38 | os: ${{ matrix.os }} 39 | dotnet_version: | 40 | 8.0.x 41 | 9.0.x 42 | solution_file: PlcInterface.sln 43 | 44 | call-deploy: 45 | needs: call-build-test 46 | permissions: 47 | packages: write 48 | uses: Vectron/GithubWorkflows/.github/workflows/DeployNugetGithub.yml@main 49 | with: 50 | os: ubuntu-latest 51 | dotnet_version: | 52 | 8.0.x 53 | 9.0.x 54 | solution_file: PlcInterface.sln 55 | secrets: 56 | NUGET_KEY: ${{ secrets.NUGET_KEY }} 57 | 58 | call-release: 59 | needs: call-build-test 60 | permissions: 61 | deployments: write 62 | contents: write 63 | uses: Vectron/GithubWorkflows/.github/workflows/CreateRelease.yml@main 64 | -------------------------------------------------------------------------------- /.github/workflows/DependabotAutoMerge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} 20 | run: gh pr merge --auto --squash "$PR_URL" 21 | env: 22 | PR_URL: ${{github.event.pull_request.html_url}} 23 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 24 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Thijs Bloebaum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PLCOnly.slnf: -------------------------------------------------------------------------------- 1 | { 2 | "solution": { 3 | "path": "PlcInterface.sln", 4 | "projects": [ 5 | "test\\PlcInterface.Ads.PLC\\PlcInterface.Ads.PLC.tsproj", 6 | "test\\PlcInterface.OpcUa.OpcServer\\PlcInterface.OpcUa.OpcServer.tcconnproj" 7 | ] 8 | } 9 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plc interface 2 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/Vectron/PlcInterface/blob/main/LICENSE.txt) 3 | [![Build status](https://github.com/Vectron/PlcInterface/actions/workflows/BuildTestDeploy.yml/badge.svg)](https://github.com/Vectron/PlcInterface/actions) 4 | [![NuGet](https://img.shields.io/nuget/v/PlcInterface.Abstraction.svg)](https://www.nuget.org/packages/PlcInterface.Abstraction) 5 | 6 | An abstraction for communicating with PLC over different protocols. 7 | The abstraction can be used with Microsoft.Extensions.DependencyInjection.Abstractions 8 | 9 | Important interfaces: 10 | IPlcConnection: Open and close connection to the plc. 11 | IReadWrite: For reading and writing variables to the PLC. 12 | IMonitor: For monitoring variables in the PLC, and get a notification when they change. 13 | 14 | 15 | # Plc interface ADS 16 | Implementation for the TwinCAT Ads interface. 17 | [![NuGet](https://img.shields.io/nuget/v/PlcInterface.Ads.svg)](https://www.nuget.org/packages/PlcInterface.Ads) 18 | 19 | # Plc interface OPC 20 | Implementation for the OPC UA interface. 21 | [![NuGet](https://img.shields.io/nuget/v/PlcInterface.OpcUa.svg)](https://www.nuget.org/packages/PlcInterface.OpcUa) 22 | 23 | ## Authors 24 | - [@Vectron](https://www.github.com/Vectron) 25 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "msbuild-sdks": { 3 | "MSTest.Sdk": "3.8.3" 4 | } 5 | } -------------------------------------------------------------------------------- /src/Common/IObservableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Linq; 2 | 3 | namespace PlcInterface; 4 | 5 | /// 6 | /// Extension methods for . 7 | /// 8 | internal static class IObservableExtensions 9 | { 10 | /// 11 | /// Filters the elements of an observable sequence based on if they are . 12 | /// 13 | /// The type of the elements in the produced sequence. 14 | /// An observable sequence whose elements to filter. 15 | /// An observable sequence that contains elements from the input sequence that satisfy the condition. 16 | public static IObservable WhereNotNull(this IObservable source) 17 | => Observable.Create(o => 18 | source.Subscribe( 19 | x => 20 | { 21 | if (x != null) 22 | { 23 | o.OnNext(x); 24 | } 25 | }, 26 | o.OnError, 27 | o.OnCompleted)); 28 | } 29 | -------------------------------------------------------------------------------- /src/Common/IServiceCollectionExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace PlcInterface; 4 | 5 | /// 6 | /// Extension methods for . 7 | /// 8 | internal static class IServiceCollectionExtension 9 | { 10 | /// 11 | /// Add and as a to the . 12 | /// 13 | /// The concrete implementation. 14 | /// The first service. 15 | /// The second service. 16 | /// The to add the service to. 17 | /// A reference to this instance after the operation has completed. 18 | public static IServiceCollection AddSingletonFactory(this IServiceCollection serviceDescriptors) 19 | where TService1 : class 20 | where TService2 : class, TService1 21 | where TImplementation : class, TService1, TService2 22 | => serviceDescriptors 23 | .AddSingleton() 24 | .AddSingleton(x => (TService1)x.GetRequiredService()); 25 | } 26 | -------------------------------------------------------------------------------- /src/Common/ISymbolInfoExtension.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface; 2 | 3 | /// 4 | /// Extension methods for . 5 | /// 6 | internal static class ISymbolInfoExtension 7 | { 8 | /// 9 | /// Flatten the type hierarchy. 10 | /// 11 | /// The to flatten. 12 | /// A implementation. 13 | /// A of all child symbols. 14 | public static IEnumerable Flatten(this ISymbolInfo symbolInfo, ISymbolHandler symbolHandler) 15 | => symbolInfo.ChildSymbols.Count == 0 ? [symbolInfo] : symbolInfo.ChildSymbols.SelectMany(x => symbolHandler.GetSymbolInfo(x).Flatten(symbolHandler)); 16 | 17 | /// 18 | /// Flatten the type hierarchy. 19 | /// 20 | /// The to flatten. 21 | /// A implementation. 22 | /// The to flatten. 23 | /// A of all child symbols and their value. 24 | public static IEnumerable<(ISymbolInfo SymbolInfo, object Value)> FlattenWithValue(this ISymbolInfo symbolInfo, ISymbolHandler symbolHandler, object value) 25 | { 26 | if (symbolInfo.ChildSymbols.Count == 0) 27 | { 28 | return [(symbolInfo, value)]; 29 | } 30 | 31 | return symbolInfo.ChildSymbols 32 | .Select(symbolHandler.GetSymbolInfo) 33 | .SelectMany(x => 34 | { 35 | object? childValue; 36 | if (value is Array array) 37 | { 38 | var indices = IndicesHelper.GetIndices(x.Name); 39 | childValue = array.GetValue(indices); 40 | } 41 | else if (value is IArrayWrapper arrayWrapper) 42 | { 43 | var indices = IndicesHelper.GetIndices(x.Name); 44 | childValue = arrayWrapper.BackingArray.GetValue(indices); 45 | } 46 | else 47 | { 48 | var type = value.GetType(); 49 | var property = type.GetProperty(x.ShortName); 50 | childValue = property?.GetValue(value); 51 | } 52 | 53 | if (childValue == null) 54 | { 55 | return []; 56 | } 57 | 58 | return x.FlattenWithValue(symbolHandler, childValue); 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Common/IndicesHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace PlcInterface; 4 | 5 | /// 6 | /// Extension methods for . 7 | /// 8 | internal static class IndicesHelper 9 | { 10 | /// 11 | /// Iterate over all array indices. 12 | /// 13 | /// only one array will be made and updated every iteration. 14 | /// The to get indices from. 15 | /// A with the array indices. 16 | public static IEnumerable GetIndices(Array array) 17 | { 18 | var indices = new int[array.Rank]; 19 | for (var dimension = 0; dimension < array.Rank; dimension++) 20 | { 21 | indices[dimension] = array.GetLowerBound(dimension); 22 | } 23 | 24 | yield return indices; 25 | for (var i = 0; i < array.Length - 1; i++) 26 | { 27 | indices[^1]++; 28 | for (var dimension = indices.Length - 1; dimension >= 0; dimension--) 29 | { 30 | var length = array.GetLength(dimension); 31 | var lowerBound = array.GetLowerBound(dimension); 32 | if (indices[dimension] == length + lowerBound) 33 | { 34 | indices[dimension - 1]++; 35 | indices[dimension] = lowerBound; 36 | } 37 | } 38 | 39 | yield return indices; 40 | } 41 | } 42 | 43 | /// 44 | /// Gets the array indices from the given . 45 | /// 46 | /// The to filter the indices from. 47 | /// An containing the indices of every array dimension. 48 | public static int[] GetIndices(string value) 49 | => GetIndices(value.AsSpan()); 50 | 51 | /// 52 | /// Gets the array indices from the given . 53 | /// 54 | /// The to filter the indices from. 55 | /// An containing the indices of every array dimension. 56 | public static int[] GetIndices(ReadOnlySpan span) 57 | { 58 | var sliced = span[(span.IndexOf('[') + 1)..]; 59 | var end = sliced.IndexOfAny(']', ','); 60 | var dimensions = new List(); 61 | 62 | while (end != -1) 63 | { 64 | var value = sliced[..end]; 65 | var dimension = int.Parse(value.ToString(), CultureInfo.InvariantCulture); 66 | dimensions.Add(dimension); 67 | if (sliced[end] == ']') 68 | { 69 | break; 70 | } 71 | 72 | sliced = sliced[(end + 1)..]; 73 | end = sliced.IndexOfAny(']', ','); 74 | } 75 | 76 | return [.. dimensions]; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Common/TaskExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace PlcInterface; 4 | 5 | /// 6 | /// Extension methods for . 7 | /// 8 | internal static partial class TaskExtensions 9 | { 10 | /// 11 | /// Log exceptions async. 12 | /// 13 | /// The task to check for exceptions. 14 | /// The to log the error to. 15 | /// The original . 16 | public static Task LogExceptionsAsync(this Task task, ILogger logger) 17 | { 18 | ArgumentNullException.ThrowIfNull(task); 19 | ArgumentNullException.ThrowIfNull(logger); 20 | 21 | return task.ContinueWith( 22 | t => 23 | { 24 | if (t.Exception != null) 25 | { 26 | var aggregateException = t.Exception.Flatten(); 27 | for (var i = aggregateException.InnerExceptions.Count - 1; i >= 0; i--) 28 | { 29 | var exception = aggregateException.InnerExceptions[i]; 30 | logger.LogException(exception); 31 | } 32 | } 33 | }, 34 | TaskContinuationOptions.OnlyOnFaulted); 35 | } 36 | 37 | [LoggerMessage(EventId = 0, Level = LogLevel.Error, Message = "Task Error")] 38 | private static partial void LogException(this ILogger logger, Exception exception); 39 | } 40 | -------------------------------------------------------------------------------- /src/Common/ThrowHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace PlcInterface; 4 | 5 | /// 6 | /// Extension methods for all classes and structs. 7 | /// 8 | internal static class ThrowHelper 9 | { 10 | /// 11 | /// Throw an . 12 | /// 13 | /// The name of the io that was being read. 14 | /// Throws this always. 15 | [DoesNotReturn] 16 | internal static void ThrowInvalidOperationException_FailedToRead(string ioName) 17 | => throw new InvalidOperationException($"Failed to read {ioName}"); 18 | } 19 | -------------------------------------------------------------------------------- /src/PlcInterface.Abstraction/ArrayWrapperExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface; 2 | 3 | /// 4 | /// Extensions for the . 5 | /// 6 | public static class ArrayWrapperExtensions 7 | { 8 | /// 9 | /// Convert the array to a zero based version. 10 | /// 11 | /// The array to turn into a zero based. 12 | /// The zero based array. 13 | public static Array ConvertZeroBased(this IArrayWrapper arrayWrapper) 14 | { 15 | var array = arrayWrapper.BackingArray; 16 | var sizes = Enumerable.Range(0, array.Rank).Select(array.GetLength).ToArray(); 17 | var newArray = Array.CreateInstance(arrayWrapper.ElementType, sizes); 18 | Array.Copy(array, newArray, newArray.Length); 19 | return newArray; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/PlcInterface.Abstraction/Connected.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface; 2 | 3 | /// 4 | /// Helpers for creating . 5 | /// 6 | public static class Connected 7 | { 8 | /// 9 | /// Create a not connected item. 10 | /// 11 | /// The type that is connected. 12 | /// The representing the connection. 13 | public static IConnected No() 14 | => new Connected(); 15 | 16 | /// 17 | /// Create a connected item. 18 | /// 19 | /// The type that is connected. 20 | /// The new that is connected. 21 | /// The representing the connection. 22 | public static IConnected Yes(T value) 23 | => new Connected(value); 24 | } 25 | -------------------------------------------------------------------------------- /src/PlcInterface.Abstraction/Connected{T}.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface; 2 | 3 | /// 4 | /// A implementation. 5 | /// 6 | /// The type that is connected. 7 | public class Connected : IConnected 8 | { 9 | private readonly T? value; 10 | 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// A containing the connection. 15 | internal Connected(T value) 16 | { 17 | this.value = value; 18 | IsConnected = true; 19 | } 20 | 21 | /// 22 | /// Initializes a new instance of the class. 23 | /// 24 | internal Connected() 25 | { 26 | } 27 | 28 | /// 29 | public bool IsConnected 30 | { 31 | get; 32 | } 33 | 34 | /// 35 | public T Value => value ?? throw new InvalidOperationException($"There is no value when {nameof(IsConnected)} returns false"); 36 | } 37 | -------------------------------------------------------------------------------- /src/PlcInterface.Abstraction/IArrayWrapper.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface; 2 | 3 | /// 4 | /// A generic wrapper over a . 5 | /// 6 | public interface IArrayWrapper 7 | { 8 | /// 9 | /// Gets the backing array storage. 10 | /// 11 | public Array BackingArray 12 | { 13 | get; 14 | } 15 | 16 | /// 17 | /// Gets the type of the element stored in the array. 18 | /// 19 | public Type ElementType 20 | { 21 | get; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/PlcInterface.Abstraction/IConnected.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface; 2 | 3 | /// 4 | /// A generic implementation of . 5 | /// 6 | /// The type that is connected. 7 | public interface IConnected : IConnected 8 | { 9 | /// 10 | /// Gets the value containing the lost or opened connection. 11 | /// 12 | /// When returns false. 13 | public T Value 14 | { 15 | get; 16 | } 17 | } 18 | 19 | /// 20 | /// Represents a type containing a opened or closed connection. 21 | /// 22 | public interface IConnected 23 | { 24 | /// 25 | /// Gets a value indicating whether a value indicating of the connection is open. 26 | /// 27 | public bool IsConnected 28 | { 29 | get; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/PlcInterface.Abstraction/IMonitor.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface; 2 | 3 | /// 4 | /// Represents a type used to perform IO monitoring. 5 | /// 6 | public interface IMonitor 7 | { 8 | /// 9 | /// Gets a for getting IO updates. 10 | /// 11 | public IObservable SymbolStream 12 | { 13 | get; 14 | } 15 | 16 | /// 17 | /// Gets a . 18 | /// 19 | public ITypeConverter TypeConverter 20 | { 21 | get; 22 | } 23 | 24 | /// 25 | /// Register IO tags for monitoring. 26 | /// 27 | /// The names of the tags. 28 | /// The interval between IO updates. 29 | public void RegisterIO(IEnumerable ioNames, int updateInterval = 1000); 30 | 31 | /// 32 | /// Register a IO tag for monitoring. 33 | /// 34 | /// The name of the tag. 35 | /// The interval between IO updates. 36 | public void RegisterIO(string ioName, int updateInterval = 1000); 37 | 38 | /// 39 | /// Unregister IO tags for monitoring. 40 | /// 41 | /// The names of the tags. 42 | public void UnregisterIO(IEnumerable ioNames); 43 | 44 | /// 45 | /// Register a IO tag for monitoring. 46 | /// 47 | /// The name of the tag. 48 | public void UnregisterIO(string ioName); 49 | } 50 | -------------------------------------------------------------------------------- /src/PlcInterface.Abstraction/IMonitorResult.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface; 2 | 3 | /// 4 | /// Represents a type containing results from a monitoring event. 5 | /// 6 | public interface IMonitorResult 7 | { 8 | /// 9 | /// Gets the name of the tag. 10 | /// 11 | public string Name 12 | { 13 | get; 14 | } 15 | 16 | /// 17 | /// Gets the new value of the tag. 18 | /// 19 | public object Value 20 | { 21 | get; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/PlcInterface.Abstraction/IPlcConnection.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface; 2 | 3 | /// 4 | /// Represents a generic type used to connect to a PLC. 5 | /// 6 | /// The underlying connection type. 7 | public interface IPlcConnection : IPlcConnection 8 | { 9 | /// 10 | /// Gets the generic session stream. 11 | /// 12 | public new IObservable> SessionStream 13 | { 14 | get; 15 | } 16 | } 17 | 18 | /// 19 | /// Represents a type used to connect to a PLC. 20 | /// 21 | public interface IPlcConnection 22 | { 23 | /// 24 | /// Gets a value indicating whether the connections is connected. 25 | /// 26 | public bool IsConnected 27 | { 28 | get; 29 | } 30 | 31 | /// 32 | /// Gets the session stream. 33 | /// 34 | public IObservable SessionStream 35 | { 36 | get; 37 | } 38 | 39 | /// 40 | /// Gets a settings object for this PLC. 41 | /// 42 | public object Settings 43 | { 44 | get; 45 | } 46 | 47 | /// 48 | /// Connect to the PLC. 49 | /// 50 | /// when connection is opened successful, otherwise . 51 | public bool Connect(); 52 | 53 | /// 54 | /// Asynchronously connect to the PLC. 55 | /// 56 | /// 57 | /// A that handles the connection. when connection is 58 | /// opened successful, otherwise . 59 | /// 60 | public Task ConnectAsync(); 61 | 62 | /// 63 | /// Disconnect from the PLC. 64 | /// 65 | public void Disconnect(); 66 | 67 | /// 68 | /// Asynchronously disconnect from the PLC. 69 | /// 70 | /// A that handles the disconnection. 71 | public Task DisconnectAsync(); 72 | } 73 | -------------------------------------------------------------------------------- /src/PlcInterface.Abstraction/IPlcConnectionExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Linq; 2 | using System.Reactive.Threading.Tasks; 3 | 4 | namespace PlcInterface; 5 | 6 | /// 7 | /// Extension methods for . 8 | /// 9 | public static class IPlcConnectionExtension 10 | { 11 | /// 12 | /// Gets the PLC Connection. 13 | /// 14 | /// The connection type to return. 15 | /// The implementation. 16 | /// The gotten . 17 | /// If no client is returned in 2 seconds. 18 | public static T GetConnectedClient(this IPlcConnection plcConnection) 19 | => plcConnection.GetConnectedClient(TimeSpan.FromSeconds(2)); 20 | 21 | /// 22 | /// Gets the PLC Connection. 23 | /// 24 | /// The connection type to return. 25 | /// The implementation. 26 | /// A indicating how long to wait for getting the connection. 27 | /// The gotten . 28 | /// If no client is returned after . 29 | public static T GetConnectedClient(this IPlcConnection plcConnection, TimeSpan timeout) 30 | => plcConnection 31 | .SessionStream 32 | .FirstAsync(x => x.IsConnected) 33 | .Timeout(timeout) 34 | .ToTask() 35 | .GetAwaiter() 36 | .GetResult() 37 | .Value; 38 | 39 | /// 40 | /// Gets the PLC Connection asynchronous. 41 | /// 42 | /// The connection type to return. 43 | /// The implementation. 44 | /// The gotten . 45 | /// If no client is returned in 2 seconds. 46 | public static Task GetConnectedClientAsync(this IPlcConnection plcConnection) 47 | => plcConnection.GetConnectedClientAsync(TimeSpan.FromSeconds(2)); 48 | 49 | /// 50 | /// Gets the PLC Connection asynchronous. 51 | /// 52 | /// The connection type to return. 53 | /// The implementation. 54 | /// A indicating how long to wait for getting the connection. 55 | /// The gotten . 56 | /// If no client is returned after . 57 | public static async Task GetConnectedClientAsync(this IPlcConnection plcConnection, TimeSpan timeout) 58 | { 59 | var connection = await plcConnection.SessionStream.FirstAsync(x => x.IsConnected).Timeout(timeout); 60 | return connection.Value; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/PlcInterface.Abstraction/IReadWriteExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace PlcInterface; 4 | 5 | /// 6 | /// Extension methods for . 7 | /// 8 | public static class IReadWriteExtension 9 | { 10 | /// 11 | /// Read a tag until we get the expected value, or a timeout happens. 12 | /// 13 | /// The type of the tag. 14 | /// A implementation. 15 | /// Tag name to monitor. 16 | /// The value to wait for. 17 | /// Time before is thrown. 18 | /// If no value is returned after . 19 | public static void WaitForValue(this IReadWrite readWrite, string tag, T filterValue, TimeSpan timeout) 20 | { 21 | using var source = new CancellationTokenSource(timeout); 22 | var token = source.Token; 23 | while (!token.IsCancellationRequested) 24 | { 25 | var value = readWrite.Read(tag); 26 | if (value != null && value.Equals(filterValue)) 27 | { 28 | return; 29 | } 30 | } 31 | 32 | throw new TimeoutException(string.Create(CultureInfo.InvariantCulture, $"Couldn't get a proper response from the PLC in {timeout.TotalSeconds} seconds")); 33 | } 34 | 35 | /// 36 | /// Read a tag until we get the expected value, or a timeout happens. 37 | /// 38 | /// The type of the tag. 39 | /// A implementation. 40 | /// Tag name to monitor. 41 | /// The value to wait for. 42 | /// Time before is thrown. 43 | /// If no value is returned after . 44 | /// A representing the asynchronous operation. 45 | public static async Task WaitForValueAsync(this IReadWrite readWrite, string tag, T filterValue, TimeSpan timeout) 46 | { 47 | using var source = new CancellationTokenSource(timeout); 48 | var token = source.Token; 49 | 50 | while (!token.IsCancellationRequested) 51 | { 52 | var value = await readWrite.ReadAsync(tag).ConfigureAwait(false); 53 | if (value != null && value.Equals(filterValue)) 54 | { 55 | return; 56 | } 57 | } 58 | 59 | throw new TimeoutException(string.Create(CultureInfo.InvariantCulture, $"Couldn't get a proper response from the PLC in {timeout.TotalSeconds} seconds")); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/PlcInterface.Abstraction/ISymbolHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace PlcInterface; 4 | 5 | /// 6 | /// Represents a type used to store PLC symbols. 7 | /// 8 | public interface ISymbolHandler 9 | { 10 | /// 11 | /// Gets a collection of all symbols in the PLC. 12 | /// 13 | public IReadOnlyCollection AllSymbols 14 | { 15 | get; 16 | } 17 | 18 | /// 19 | /// Gets the . 20 | /// 21 | /// The tag name. 22 | /// The found . 23 | public ISymbolInfo GetSymbolInfo(string ioName); 24 | 25 | /// 26 | /// Try to get the . 27 | /// 28 | /// The tag name. 29 | /// The found . 30 | /// when the symbol was found else . 31 | public bool TryGetSymbolInfo(string ioName, [MaybeNullWhen(false)] out ISymbolInfo symbolInfo); 32 | } 33 | -------------------------------------------------------------------------------- /src/PlcInterface.Abstraction/ISymbolInfo.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface; 2 | 3 | /// 4 | /// Represents a type containing information about a PLC symbol. 5 | /// 6 | public interface ISymbolInfo 7 | { 8 | /// 9 | /// Gets a of the child symbols names. 10 | /// 11 | /// A containing all child symbols names. 12 | public IList ChildSymbols 13 | { 14 | get; 15 | } 16 | 17 | /// 18 | /// Gets the comment for this symbol. 19 | /// 20 | /// The comment stored in the plc for this symbol. 21 | public string Comment 22 | { 23 | get; 24 | } 25 | 26 | /// 27 | /// Gets the name of the symbol. 28 | /// 29 | /// 30 | /// The Full name of this symbol (Format: container block + . + symbol name) example: Visualization.L_Display_Door_1_1. 31 | /// 32 | public string Name 33 | { 34 | get; 35 | } 36 | 37 | /// 38 | /// Gets the name of the symbol. in all lowercase. 39 | /// 40 | /// The Full name of this symbol. 41 | public string NameLower 42 | { 43 | get; 44 | } 45 | 46 | /// 47 | /// Gets the short name of the symbol in the PLC. 48 | /// 49 | /// The name of this symbol. 50 | public string ShortName 51 | { 52 | get; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/PlcInterface.Abstraction/ITypeActivator.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace PlcInterface; 4 | 5 | /// 6 | /// Encapsules the logic to create a type by constructor with parameters, or default constructor 7 | /// with property setters. 8 | /// 9 | internal interface ITypeActivator 10 | { 11 | /// 12 | /// Try to create a instance. 13 | /// 14 | /// 15 | /// A for getting the value of the member with the given name. 16 | /// 17 | /// The number of members. 18 | /// The created instance. 19 | /// if creation was successful, otherwise false. 20 | /// is thrown when the data is invalid. 21 | public bool TryCreateInstance(Func memberValueGetter, int memberCount, [MaybeNullWhen(false)] out object instance); 22 | } 23 | -------------------------------------------------------------------------------- /src/PlcInterface.Abstraction/ITypeConverter.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface; 2 | 3 | /// 4 | /// A converter that can be used to convert PLC types to system types. 5 | /// 6 | public interface ITypeConverter 7 | { 8 | /// 9 | /// Converts from object to . 10 | /// 11 | /// The type to convert to. 12 | /// The object to convert. 13 | /// The resulting . 14 | public T Convert(object value); 15 | 16 | /// 17 | /// Converts from object to . 18 | /// 19 | /// The object to convert. 20 | /// The to convert to. 21 | /// The converted object. 22 | public object Convert(object value, Type targetType); 23 | } 24 | -------------------------------------------------------------------------------- /src/PlcInterface.Abstraction/PlcInterface.Abstraction.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | A PLC Abstraction 4 | PlcInterface 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/PlcInterface.Abstraction/PropertySetterHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Reflection; 3 | 4 | namespace PlcInterface; 5 | 6 | /// 7 | /// Helper for setting property values. 8 | /// 9 | internal sealed class PropertySetterHelper 10 | { 11 | private readonly PropertyInfo propertyInfo; 12 | private readonly PropertySetter setter; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The to create a setter binding for. 18 | public PropertySetterHelper(PropertyInfo propertyInfo) 19 | { 20 | this.propertyInfo = propertyInfo; 21 | setter = GetSetter(); 22 | } 23 | 24 | private delegate void PropertySetter(object instance, object value); 25 | 26 | /// 27 | /// Gets the name of the property. 28 | /// 29 | public string Name => propertyInfo.Name; 30 | 31 | /// 32 | /// Gets the of the property. 33 | /// 34 | public Type PropertyType => propertyInfo.PropertyType; 35 | 36 | /// 37 | /// Set the given value to the given instance. 38 | /// 39 | /// The instance containing the property. 40 | /// The value to set. 41 | public void Set(object instance, object value) => setter.Invoke(instance, value); 42 | 43 | private PropertySetter GetSetter() 44 | { 45 | if (propertyInfo.DeclaringType == null) 46 | { 47 | throw new NotSupportedException($"{propertyInfo.Name} has no declaring type"); 48 | } 49 | 50 | var instanceParam = Expression.Parameter(typeof(object)); 51 | var instanceParamCast = Expression.Convert(instanceParam, propertyInfo.DeclaringType); 52 | var propertyParam = Expression.Parameter(typeof(object)); 53 | var propertyParamCast = Expression.Convert(propertyParam, propertyInfo.PropertyType); 54 | var propertyGetterExpression = Expression.Property(instanceParamCast, propertyInfo.Name); 55 | var assignExpression = Expression.Assign(propertyGetterExpression, propertyParamCast); 56 | var lambda = Expression.Lambda(assignExpression, instanceParam, propertyParam); 57 | var compiled = lambda.Compile(); 58 | return compiled; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/PlcInterface.Abstraction/StructActivator.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | 5 | namespace PlcInterface; 6 | 7 | /// 8 | /// Encapsules the logic to create a type by constructor with parameters, or default constructor 9 | /// with property setters. 10 | /// 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// The type of the value type. 15 | internal sealed class StructActivator(Type type) : ITypeActivator 16 | { 17 | private readonly Activator activator = GetActivator(type); 18 | 19 | [SuppressMessage("Style", "IDE0305:Simplify collection initialization", Justification = "Makes it more unreadable")] 20 | private readonly PropertyInfo[] properties = type 21 | .GetProperties() 22 | .Where(x => x.CanWrite) 23 | .ToArray(); 24 | 25 | private delegate object Activator(); 26 | 27 | /// 28 | /// Try to create a instance. 29 | /// 30 | /// 31 | /// A for getting the value of the member with the given name. 32 | /// 33 | /// The number of members. 34 | /// The created instance. 35 | /// if creation was successful, otherwise false. 36 | /// is thrown when the data is invalid. 37 | public bool TryCreateInstance(Func memberValueGetter, int memberCount, [MaybeNullWhen(false)] out object instance) 38 | { 39 | instance = default; 40 | if (properties.Length >= memberCount) 41 | { 42 | instance = activator.Invoke(); 43 | foreach (var property in properties) 44 | { 45 | var memberValue = memberValueGetter.Invoke(property.Name, property.PropertyType) 46 | ?? throw new SymbolException($"Member: {property.Name} was null"); 47 | property.SetValue(instance, memberValue); 48 | } 49 | 50 | return true; 51 | } 52 | 53 | return false; 54 | } 55 | 56 | private static Activator GetActivator(Type type) 57 | { 58 | // make a NewExpression that calls the ctor 59 | var newExp = Expression.New(type); 60 | var cast = Expression.Convert(newExp, typeof(object)); 61 | 62 | // create a lambda with the New Expression as body 63 | var lambda = Expression.Lambda(cast); 64 | 65 | // compile it 66 | var compiled = lambda.Compile(); 67 | return compiled; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/PlcInterface.Abstraction/SymbolException.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface; 2 | 3 | /// 4 | /// Represents error that occur during symbol handling. 5 | /// 6 | /// 7 | /// Initializes a new instance of the class. 8 | /// 9 | /// The message that describes the error. 10 | public sealed class SymbolException(string message) : Exception(message); 11 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/AdsPlcConnectionOptions.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.Ads; 2 | 3 | /// 4 | /// Settings for the . 5 | /// 6 | public class AdsPlcConnectionOptions 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | public AdsPlcConnectionOptions() 12 | { 13 | AmsNetId = string.Empty; 14 | AutoConnect = false; 15 | Port = 0; 16 | } 17 | 18 | /// 19 | /// Gets or sets the address to connect to. 20 | /// 21 | public string AmsNetId 22 | { 23 | get; 24 | set; 25 | } 26 | 27 | /// 28 | /// Gets or sets a value indicating whether the connection should be opened automatically. 29 | /// 30 | public bool AutoConnect 31 | { 32 | get; 33 | set; 34 | } 35 | 36 | /// 37 | /// Gets or sets the port to connect to. 38 | /// 39 | public int Port 40 | { 41 | get; 42 | set; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/AdsSymbolHandlerOptions.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.Ads; 2 | 3 | /// 4 | /// Settings for the . 5 | /// 6 | public class AdsSymbolHandlerOptions 7 | { 8 | /// 9 | /// Gets or sets path where to store the found symbols. 10 | /// 11 | public string OutputPath { get; set; } = string.Empty; 12 | 13 | /// 14 | /// Gets or sets the path to the root node. 15 | /// 16 | /// 17 | /// Sub items are separated by a '.'. 18 | /// 19 | public string RootVariable { get; set; } = string.Empty; 20 | 21 | /// 22 | /// Gets or sets a value indicating whether the symbol list should be written to disk. 23 | /// 24 | public bool StoreSymbolsToDisk 25 | { 26 | get; 27 | set; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/AdsTypeConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Dynamic; 2 | using System.Globalization; 3 | using TwinCAT.TypeSystem; 4 | 5 | namespace PlcInterface.Ads; 6 | 7 | /// 8 | /// A implementation for ADS types. 9 | /// 10 | public sealed class AdsTypeConverter : TypeConverter, IAdsTypeConverter 11 | { 12 | /// 13 | public object Convert(object value, IValueSymbol valueSymbol) 14 | { 15 | if (value is DynamicObject dynamicObject) 16 | { 17 | return dynamicObject.CleanDynamic(); 18 | } 19 | 20 | if (valueSymbol.Category == DataTypeCategory.Enum 21 | && value is short) 22 | { 23 | return System.Convert.ToInt32(value, CultureInfo.InvariantCulture); 24 | } 25 | 26 | if (value is DateTime dateTime) 27 | { 28 | return new DateTimeOffset(dateTime); 29 | } 30 | 31 | return value; 32 | } 33 | 34 | /// 35 | public override object Convert(object value, Type targetType) 36 | { 37 | if (value is TwinCAT.PlcOpen.DateBase dateBase) 38 | { 39 | if (targetType == typeof(DateTimeOffset)) 40 | { 41 | return new DateTimeOffset(dateBase.Value); 42 | } 43 | 44 | return dateBase.Value; 45 | } 46 | 47 | if (value is TwinCAT.PlcOpen.TimeBase timeBase) 48 | { 49 | return timeBase.Time; 50 | } 51 | 52 | if (value is TwinCAT.PlcOpen.LTimeBase lTimeBase) 53 | { 54 | return lTimeBase.Time; 55 | } 56 | 57 | if (value is IDynamicValue valueObject && valueObject.DataType is IArrayType arrayType) 58 | { 59 | return ConvertDynamicValueArray(valueObject, arrayType, targetType); 60 | } 61 | 62 | return base.Convert(value, targetType); 63 | } 64 | 65 | /// 66 | public object ConvertToPLCType(object value) 67 | { 68 | if (value is IArrayWrapper arrayWrapper) 69 | { 70 | return arrayWrapper.BackingArray; 71 | } 72 | 73 | return value; 74 | } 75 | 76 | private Array ConvertDynamicValueArray(IDynamicValue valueObject, IArrayType arrayType, Type targetType) 77 | { 78 | var elementType = targetType.GetElementType() 79 | ?? throw new NotSupportedException($"Unable to retrieve element type"); 80 | var dimensionLengths = arrayType.Dimensions.GetDimensionLengths(); 81 | var destination = Array.CreateInstance(elementType, dimensionLengths); 82 | 83 | foreach (var indices in IndicesHelper.GetIndices(destination)) 84 | { 85 | if (!valueObject.TryGetIndexValue(indices, out var memberValue)) 86 | { 87 | throw new SymbolException($"No value found at index {string.Join(';', indices)}"); 88 | } 89 | 90 | destination.SetValue(Convert(memberValue, elementType), indices); 91 | } 92 | 93 | return destination; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/DefaultAdsPlcConnectionConfigureOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace PlcInterface.Ads; 4 | 5 | /// 6 | /// A for configuring with default values. 7 | /// 8 | public class DefaultAdsPlcConnectionConfigureOptions : IConfigureOptions 9 | { 10 | /// 11 | public void Configure(AdsPlcConnectionOptions options) 12 | { 13 | options.AmsNetId = "local"; 14 | options.Port = 851; 15 | options.AutoConnect = false; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/DefaultAdsSymbolHandlerSettingsConfigureOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace PlcInterface.Ads; 4 | 5 | /// 6 | /// A for configuring with default values. 7 | /// 8 | public class DefaultAdsSymbolHandlerSettingsConfigureOptions : IConfigureOptions 9 | { 10 | /// 11 | public void Configure(AdsSymbolHandlerOptions options) 12 | { 13 | options.OutputPath = string.Empty; 14 | options.StoreSymbolsToDisk = false; 15 | options.RootVariable = string.Empty; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/DisposableMonitorItem.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Disposables; 2 | using System.Reactive.Linq; 3 | using System.Reactive.Subjects; 4 | using TwinCAT.Ads.Reactive; 5 | using TwinCAT.TypeSystem; 6 | 7 | namespace PlcInterface.Ads; 8 | 9 | /// 10 | /// A for counting reference to the item. 11 | /// 12 | internal sealed class DisposableMonitorItem : IDisposable 13 | { 14 | private readonly string name; 15 | private bool disposedValue; 16 | private IDisposable stream; 17 | 18 | private DisposableMonitorItem(string name) 19 | { 20 | stream = Disposable.Empty; 21 | Subscriptions = 1; 22 | this.name = name; 23 | } 24 | 25 | /// 26 | /// Gets or sets the number of references to this item. 27 | /// 28 | public int Subscriptions 29 | { 30 | get; 31 | set; 32 | } 33 | 34 | /// 35 | /// Create a . 36 | /// 37 | /// The name of the symbol. 38 | /// The created . 39 | public static DisposableMonitorItem Create(string name) 40 | => new(name); 41 | 42 | /// 43 | public void Dispose() 44 | => Dispose(disposing: true); 45 | 46 | /// 47 | /// Update the subscriptions. 48 | /// 49 | /// A . 50 | /// The stream to subscribe to. 51 | /// A . 52 | public void Update(IAdsSymbolHandler symbolHandler, ISubject symbolStream, IAdsTypeConverter typeConverter) 53 | { 54 | if (symbolHandler.TryGetSymbolInfo(name, out var symbolInfo) 55 | && symbolInfo.Symbol is IValueSymbol valueSymbol 56 | && valueSymbol.Connection != null 57 | && valueSymbol.Connection.IsConnected) 58 | { 59 | stream.Dispose(); 60 | stream = valueSymbol 61 | .WhenValueChanged() 62 | .Select(x => new MonitorResult(name, typeConverter.Convert(x, valueSymbol))) 63 | .Subscribe(symbolStream); 64 | } 65 | } 66 | 67 | /// 68 | /// Protected implementation of Dispose pattern. 69 | /// 70 | /// Value indicating if we need to cleanup managed resources. 71 | private void Dispose(bool disposing) 72 | { 73 | if (!disposedValue) 74 | { 75 | if (disposing) 76 | { 77 | stream.Dispose(); 78 | } 79 | 80 | disposedValue = true; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/IAdsMonitor.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.Ads; 2 | 3 | /// 4 | /// The Ads implementation of a . 5 | /// 6 | public interface IAdsMonitor : IMonitor 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/IAdsPlcConnection.cs: -------------------------------------------------------------------------------- 1 | using TwinCAT.Ads; 2 | 3 | namespace PlcInterface.Ads; 4 | 5 | /// 6 | /// The Ads implementation of a . 7 | /// 8 | public interface IAdsPlcConnection : IPlcConnection 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/IAdsReadWrite.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.Ads; 2 | 3 | /// 4 | /// The Ads implementation of a . 5 | /// 6 | public interface IAdsReadWrite : IReadWrite 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/IAdsSymbolHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace PlcInterface.Ads; 4 | 5 | /// 6 | /// The Ads implementation of a . 7 | /// 8 | public interface IAdsSymbolHandler : ISymbolHandler 9 | { 10 | /// 11 | /// Gets the . 12 | /// 13 | /// The tag name. 14 | /// The found . 15 | public new IAdsSymbolInfo GetSymbolInfo(string ioName); 16 | 17 | /// 18 | /// Try to get the . 19 | /// 20 | /// The tag name. 21 | /// The found . 22 | /// when the symbol was found else . 23 | public bool TryGetSymbolInfo(string ioName, [MaybeNullWhen(false)] out IAdsSymbolInfo symbolInfo); 24 | } 25 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/IAdsSymbolInfo.cs: -------------------------------------------------------------------------------- 1 | using TwinCAT.TypeSystem; 2 | 3 | namespace PlcInterface.Ads; 4 | 5 | /// 6 | /// The Ads implementation of a . 7 | /// 8 | public interface IAdsSymbolInfo : ISymbolInfo 9 | { 10 | /// 11 | /// Gets a value indicating whether this symbol represents a array. 12 | /// 13 | public bool IsArray 14 | { 15 | get; 16 | } 17 | 18 | /// 19 | /// Gets a value indicating whether this symbol represents a complex type. 20 | /// 21 | public bool IsBigType 22 | { 23 | get; 24 | } 25 | 26 | /// 27 | /// Gets the PLC symbol this encapsules. 28 | /// 29 | public ISymbol Symbol 30 | { 31 | get; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/IAdsTypeConverter.cs: -------------------------------------------------------------------------------- 1 | using TwinCAT.TypeSystem; 2 | 3 | namespace PlcInterface.Ads; 4 | 5 | /// 6 | /// Specialized for Ads Types. 7 | /// 8 | public interface IAdsTypeConverter : ITypeConverter 9 | { 10 | /// 11 | /// Converts Ads types to System types. 12 | /// 13 | /// The value to fix. 14 | /// The symbol containing type information. 15 | /// The converted type. 16 | public object Convert(object value, IValueSymbol valueSymbol); 17 | 18 | /// 19 | /// Conver the given object to a type that can be written to the PLC. 20 | /// 21 | /// The object to convert. 22 | /// The object that can be written to the PLC. 23 | public object ConvertToPLCType(object value); 24 | } 25 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/IServiceCollectionExtension.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Abstractions; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Logging; 4 | using PlcInterface.Ads.TwinCATAbstractions; 5 | 6 | namespace PlcInterface.Ads; 7 | 8 | /// 9 | /// Extension methods for . 10 | /// 11 | public static class IServiceCollectionExtension 12 | { 13 | /// 14 | /// Configure the for this PLC. 15 | /// 16 | /// 17 | /// The to add the services to. 18 | /// 19 | /// A reference to this instance after the operation has completed. 20 | public static IServiceCollection AddAdsPLC(this IServiceCollection serviceDescriptors) 21 | => serviceDescriptors 22 | .AddSingletonFactory() 23 | .AddSingletonFactory() 24 | .AddSingletonFactory() 25 | .AddSingletonFactory, IAdsPlcConnection>() 26 | .AddSingleton(x => x.GetRequiredService()) 27 | .AddTransient() 28 | .AddSingleton(x => new TwinCAT.Ads.AdsClient(x.GetRequiredService>())) 29 | .AddSingleton() 30 | .AddSingleton() 31 | .AddSingleton() 32 | .ConfigureOptions() 33 | .ConfigureOptions(); 34 | } 35 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/ISymbolInfoExtension.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.Ads; 2 | 3 | /// 4 | /// Extension methods for . 5 | /// 6 | internal static class ISymbolInfoExtension 7 | { 8 | /// 9 | /// Convert the to and throw a exception if the conversion fails. 10 | /// 11 | /// The to change. 12 | /// The cast object. 13 | /// If the cast fails. 14 | public static IAdsSymbolInfo CastAndValidate(this ISymbolInfo symbolInfo) 15 | { 16 | if (symbolInfo is not IAdsSymbolInfo symbol) 17 | { 18 | throw new SymbolException($"Symbol is not a {typeof(IAdsSymbolInfo)}"); 19 | } 20 | 21 | return symbol; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/ISymbolLoaderFactory.cs: -------------------------------------------------------------------------------- 1 | using TwinCAT; 2 | using TwinCAT.Ads.TypeSystem; 3 | using TwinCAT.TypeSystem; 4 | 5 | namespace PlcInterface.Ads; 6 | 7 | /// 8 | /// An abstraction layer over the static class . 9 | /// 10 | public interface ISymbolLoaderFactory 11 | { 12 | /// 13 | public ISymbolLoader Create(IConnection connection, ISymbolLoaderSettings settings); 14 | } 15 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/IValueSymbolExtensions.cs: -------------------------------------------------------------------------------- 1 | using TwinCAT.TypeSystem; 2 | 3 | namespace PlcInterface.Ads; 4 | 5 | /// 6 | /// Extension methods for . 7 | /// 8 | internal static class IValueSymbolExtensions 9 | { 10 | /// 11 | /// Convert the to and throw a exception if the conversion fails. 12 | /// 13 | /// The to change. 14 | /// The cast object. 15 | /// If the cast fails. 16 | public static IValueSymbol CastAndValidate(this ISymbol symbolInfo) 17 | { 18 | if (symbolInfo is not IValueSymbol symbol) 19 | { 20 | throw new SymbolException($"Symbol is not a {typeof(IValueSymbol)}"); 21 | } 22 | 23 | return symbol; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/Monitor.Logging.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace PlcInterface.Ads; 4 | 5 | /// 6 | /// Logging source generator methods. 7 | /// 8 | public partial class Monitor 9 | { 10 | [LoggerMessage(EventId = 1, Level = LogLevel.Debug, Message = "Updating subscriptions")] 11 | private partial void LogUpdatingSubscriptions(); 12 | 13 | [LoggerMessage(EventId = 4, Level = LogLevel.Debug, Message = "{VariableName} is not registered")] 14 | private partial void LogVariableNotRegistered(string variableName); 15 | 16 | [LoggerMessage(EventId = 2, Level = LogLevel.Debug, Message = "Registered IO {VariableName} with update interval: {UpdateInterval}")] 17 | private partial void LogVariableRegistered(string variableName, int updateInterval); 18 | 19 | [LoggerMessage(EventId = 5, Level = LogLevel.Debug, Message = "{VariableName} still has {SubscriptionCount} subscriptions left")] 20 | private partial void LogVariableStillHasSubscriptions(string variableName, int subscriptionCount); 21 | 22 | [LoggerMessage(EventId = 3, Level = LogLevel.Debug, Message = "Unregistered IO {VariableName}")] 23 | private partial void LogVariableUnregistered(string variableName); 24 | } 25 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/MonitorResult.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.Ads; 2 | 3 | /// 4 | /// Implementation for . 5 | /// 6 | /// 7 | /// Initializes a new instance of the class. 8 | /// 9 | /// The name of the tag. 10 | /// The value of the tag. 11 | internal sealed class MonitorResult(string name, object value) : IMonitorResult 12 | { 13 | /// 14 | public string Name => name; 15 | 16 | /// 17 | public object Value => value; 18 | } 19 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/ObjectExtension.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.Ads; 2 | 3 | /// 4 | /// Extension methods for any object. 5 | /// 6 | internal static class ObjectExtension 7 | { 8 | /// 9 | /// Do a depth first traversal of a tree. 10 | /// 11 | /// The type of the objects in the tree. 12 | /// The first item to traverse. 13 | /// A for getting the children. 14 | /// An containing all children. 15 | public static IEnumerable DepthFirstTreeTraversal(this T root, Func> children) 16 | { 17 | var stack = new Stack(); 18 | stack.Push(root); 19 | while (stack.Count != 0) 20 | { 21 | var current = stack.Pop(); 22 | foreach (var child in children(current)) 23 | { 24 | stack.Push(child); 25 | } 26 | 27 | yield return current; 28 | } 29 | } 30 | 31 | /// 32 | /// Do a depth first traversal of a tree. 33 | /// 34 | /// The type of the objects in the tree. 35 | /// The root items to traverse. 36 | /// A for getting the children. 37 | /// An containing all children. 38 | public static IEnumerable DepthFirstTreeTraversal(this IEnumerable roots, Func> children) 39 | { 40 | var stack = new Stack(roots); 41 | 42 | while (stack.Count != 0) 43 | { 44 | var current = stack.Pop(); 45 | foreach (var child in children(current)) 46 | { 47 | stack.Push(child); 48 | } 49 | 50 | yield return current; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/PlcInterface.Ads.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | A PLC communication implementation for Beckhoff ADS 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/SymbolHandler.Logging.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace PlcInterface.Ads; 4 | 5 | /// 6 | /// Logging source generator methods. 7 | /// 8 | public partial class SymbolHandler 9 | { 10 | [LoggerMessage(EventId = 5, Level = LogLevel.Error, Message = "Plc not connected")] 11 | private partial void LogPlcNotConnected(); 12 | 13 | [LoggerMessage(EventId = 3, Level = LogLevel.Information, Message = "Symbols updated in {Time} ms, found {Amount} symbols")] 14 | private partial void LogSymbolsUpdated(long time, int amount); 15 | 16 | [LoggerMessage(EventId = 2, Level = LogLevel.Debug, Message = "Updating symbols")] 17 | private partial void LogUpdatingSymbols(); 18 | 19 | [LoggerMessage(EventId = 4, Level = LogLevel.Error, Message = "Updating symbols failed")] 20 | private partial void LogUpdatingSymbolsFailed(Exception exception); 21 | 22 | [LoggerMessage(EventId = 1, Level = LogLevel.Error, Message = "{VariableName} does not exist")] 23 | private partial void LogVariableDoesNotExist(string variableName); 24 | } 25 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/SymbolInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using TwinCAT.TypeSystem; 3 | 4 | namespace PlcInterface.Ads; 5 | 6 | /// 7 | /// Stores data about a PLC symbol. 8 | /// 9 | /// 10 | /// Initializes a new instance of the class. 11 | /// 12 | /// The plc symbol. 13 | /// The root path of the symbol tree. 14 | [DebuggerDisplay("{Name}")] 15 | internal sealed class SymbolInfo(ISymbol symbol, string rootPath) : IAdsSymbolInfo 16 | { 17 | /// 18 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0305:Simplify collection initialization", Justification = "Makes it more unreadable")] 19 | public IList ChildSymbols 20 | => Symbol.SubSymbols.Select(x => CleanInstancePath(x, rootPath)).ToList(); 21 | 22 | /// 23 | public string Comment 24 | => Symbol.Comment; 25 | 26 | /// 27 | public bool IsArray 28 | => Symbol.DataType?.Category == DataTypeCategory.Array; 29 | 30 | /// 31 | public bool IsBigType 32 | => Symbol.DataType?.Category == DataTypeCategory.Struct; 33 | 34 | /// 35 | public string Name { get; } = CleanInstancePath(symbol, rootPath); 36 | 37 | /// 38 | public string NameLower 39 | => Name.ToLower(System.Globalization.CultureInfo.InvariantCulture); 40 | 41 | /// 42 | public string ShortName 43 | => Symbol.InstanceName; 44 | 45 | /// 46 | public ISymbol Symbol => symbol; 47 | 48 | private static string CleanInstancePath(ISymbol symbol, string rootPath) 49 | { 50 | if (string.IsNullOrEmpty(rootPath)) 51 | { 52 | return symbol.InstancePath; 53 | } 54 | 55 | return symbol.InstancePath 56 | .Replace(rootPath, string.Empty, StringComparison.OrdinalIgnoreCase) 57 | .Trim('.'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/TcAdsClientExtension.cs: -------------------------------------------------------------------------------- 1 | using TwinCAT.Ads; 2 | 3 | namespace PlcInterface.Ads; 4 | 5 | /// 6 | /// Extension methods for . 7 | /// 8 | internal static class TcAdsClientExtension 9 | { 10 | /// 11 | /// Validate the PLC connection. 12 | /// 13 | /// The to check. 14 | /// The for chaining. 15 | /// When the plc is not in a valid state. 16 | public static IAdsConnection ValidateConnection(this IAdsConnection client) 17 | { 18 | ArgumentNullException.ThrowIfNull(client); 19 | 20 | if (!client.IsConnected) 21 | { 22 | throw new InvalidOperationException("PLC not connected"); 23 | } 24 | 25 | var errorCode = client.TryReadState(out var lastPLCState); 26 | 27 | if (errorCode != AdsErrorCode.NoError) 28 | { 29 | throw new InvalidOperationException("Unable to read the PLC state"); 30 | } 31 | 32 | if (lastPLCState.AdsState != AdsState.Run) 33 | { 34 | throw new InvalidOperationException("PLC not running"); 35 | } 36 | 37 | return client; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/TwincatAbstractions/ISumSymbolFactory.cs: -------------------------------------------------------------------------------- 1 | using TwinCAT.Ads; 2 | using TwinCAT.TypeSystem; 3 | 4 | namespace PlcInterface.Ads.TwinCATAbstractions; 5 | 6 | /// 7 | /// A factory for creating SumSymbol commands. 8 | /// 9 | public interface ISumSymbolFactory 10 | { 11 | /// 12 | /// Creates a . 13 | /// 14 | /// The ADS Connection. 15 | /// The symbols to read. 16 | /// The constructed instance. 17 | public ISumSymbolRead CreateSumSymbolRead(IAdsConnection connection, IList symbols); 18 | 19 | /// 20 | /// Creates a . 21 | /// 22 | /// The ADS Connection. 23 | /// The symbols to write. 24 | /// The constructed instance. 25 | public ISumSymbolWrite CreateSumSymbolWrite(IAdsConnection connection, IList symbols); 26 | } 27 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/TwincatAbstractions/ISumSymbolRead.cs: -------------------------------------------------------------------------------- 1 | using TwinCAT.Ads.SumCommand; 2 | 3 | namespace PlcInterface.Ads.TwinCATAbstractions; 4 | 5 | /// 6 | /// A Abstraction layer over . 7 | /// 8 | public interface ISumSymbolRead 9 | { 10 | /// 11 | public object[] Read(); 12 | 13 | /// 14 | public Task ReadAsync(CancellationToken cancel); 15 | } 16 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/TwincatAbstractions/ISumSymbolWrite.cs: -------------------------------------------------------------------------------- 1 | using TwinCAT.Ads.SumCommand; 2 | 3 | namespace PlcInterface.Ads.TwinCATAbstractions; 4 | 5 | /// 6 | /// A Abstraction layer over . 7 | /// 8 | public interface ISumSymbolWrite 9 | { 10 | /// 11 | public void Write(object[] values); 12 | 13 | /// 14 | public Task WriteAsync(object[] values, CancellationToken cancel); 15 | } 16 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/TwincatAbstractions/SumSymbolFactory.cs: -------------------------------------------------------------------------------- 1 | using TwinCAT.Ads; 2 | using TwinCAT.TypeSystem; 3 | 4 | namespace PlcInterface.Ads.TwinCATAbstractions; 5 | 6 | /// 7 | /// A abstraction for creating SumSymbol commands. 8 | /// 9 | public class SumSymbolFactory : ISumSymbolFactory 10 | { 11 | /// 12 | public ISumSymbolRead CreateSumSymbolRead(IAdsConnection connection, IList symbols) 13 | => new SumSymbolReadAbstraction(connection, symbols); 14 | 15 | /// 16 | public ISumSymbolWrite CreateSumSymbolWrite(IAdsConnection connection, IList symbols) 17 | => new SumSymbolWriteAbstraction(connection, symbols); 18 | } 19 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/TwincatAbstractions/SumSymbolReadAbstraction.cs: -------------------------------------------------------------------------------- 1 | using TwinCAT.Ads; 2 | using TwinCAT.Ads.SumCommand; 3 | using TwinCAT.TypeSystem; 4 | 5 | namespace PlcInterface.Ads.TwinCATAbstractions; 6 | 7 | /// 8 | /// A implementation of . 9 | /// 10 | /// 11 | internal sealed class SumSymbolReadAbstraction(IAdsConnection connection, IList symbols) : ISumSymbolRead 12 | { 13 | private readonly SumSymbolRead backend = new(connection, symbols); 14 | 15 | /// 16 | public object[] Read() 17 | => backend.Read(); 18 | 19 | /// 20 | public async Task ReadAsync(CancellationToken cancel) 21 | { 22 | var result = await backend.ReadAsync(cancel).ConfigureAwait(false); 23 | return result.Values; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/TwincatAbstractions/SumSymbolWriteAbstraction.cs: -------------------------------------------------------------------------------- 1 | using TwinCAT.Ads; 2 | using TwinCAT.Ads.SumCommand; 3 | using TwinCAT.TypeSystem; 4 | 5 | namespace PlcInterface.Ads.TwinCATAbstractions; 6 | 7 | /// 8 | /// A implementation of . 9 | /// 10 | /// 11 | internal sealed class SumSymbolWriteAbstraction(IAdsConnection connection, IList symbols) : ISumSymbolWrite 12 | { 13 | private readonly SumSymbolWrite backend = new(connection, symbols); 14 | 15 | /// 16 | public void Write(object[] values) 17 | => backend.Write(values); 18 | 19 | /// 20 | public Task WriteAsync(object[] values, CancellationToken cancel) 21 | => backend.WriteAsync(values, cancel); 22 | } 23 | -------------------------------------------------------------------------------- /src/PlcInterface.Ads/TwincatAbstractions/SymbolLoaderFactoryAbstraction.cs: -------------------------------------------------------------------------------- 1 | using TwinCAT; 2 | using TwinCAT.Ads.TypeSystem; 3 | using TwinCAT.TypeSystem; 4 | 5 | namespace PlcInterface.Ads.TwinCATAbstractions; 6 | 7 | /// 8 | /// A implementation of . 9 | /// 10 | public class SymbolLoaderFactoryAbstraction : ISymbolLoaderFactory 11 | { 12 | /// 13 | public ISymbolLoader Create(IConnection connection, ISymbolLoaderSettings settings) 14 | => SymbolLoaderFactory.Create(connection, settings); 15 | } 16 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/DefaultOpcPlcConnectionConfigureOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.Extensions.Options; 3 | using Opc.Ua; 4 | 5 | namespace PlcInterface.OpcUa; 6 | 7 | /// 8 | /// A for configuring with default values. 9 | /// 10 | public class DefaultOpcPlcConnectionConfigureOptions : IConfigureOptions 11 | { 12 | /// 13 | public void Configure(OpcPlcConnectionOptions options) 14 | { 15 | options.Address = "127.0.0.1"; 16 | options.Port = 4840; 17 | options.UriSchema = "opc.tcp"; 18 | options.UserName = string.Empty; 19 | options.Password = string.Empty; 20 | options.AutoConnect = false; 21 | options.UseSecurity = true; 22 | options.AutoGenCertificate = false; 23 | options.ApplicationConfiguration = CreateApplicationConfiguration(); 24 | } 25 | 26 | private static ApplicationConfiguration CreateApplicationConfiguration() 27 | { 28 | var entryAssembly = Assembly.GetEntryAssembly() 29 | ?? Assembly.GetExecutingAssembly(); 30 | 31 | var appName = entryAssembly.GetName().Name; 32 | 33 | return new ApplicationConfiguration() 34 | { 35 | ApplicationName = appName, 36 | ApplicationType = ApplicationType.Client, 37 | ApplicationUri = "urn:" + Utils.GetHostName() + ":" + appName, 38 | SecurityConfiguration = new SecurityConfiguration 39 | { 40 | ApplicationCertificate = new CertificateIdentifier 41 | { 42 | StoreType = "X509Store", 43 | StorePath = "CurrentUser\\My", 44 | SubjectName = appName, 45 | }, 46 | TrustedPeerCertificates = new CertificateTrustList 47 | { 48 | StoreType = "Directory", 49 | StorePath = "OPC Foundation/CertificateStores/UA Applications", 50 | }, 51 | TrustedIssuerCertificates = new CertificateTrustList 52 | { 53 | StoreType = "Directory", 54 | StorePath = "OPC Foundation/CertificateStores/UA Certificate Authorities", 55 | }, 56 | RejectedCertificateStore = new CertificateTrustList 57 | { 58 | StoreType = "Directory", 59 | StorePath = "OPC Foundation/CertificateStores/RejectedCertificates", 60 | }, 61 | NonceLength = 32, 62 | AutoAcceptUntrustedCertificates = true, 63 | }, 64 | TransportConfigurations = [], 65 | TransportQuotas = new TransportQuotas { OperationTimeout = 15000 }, 66 | ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000, }, 67 | }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/DefaultOpcSymbolHandlerSettingsConfigureOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace PlcInterface.OpcUa; 4 | 5 | /// 6 | /// A for configuring with default values. 7 | /// 8 | public class DefaultOpcSymbolHandlerSettingsConfigureOptions : IConfigureOptions 9 | { 10 | /// 11 | public void Configure(OpcSymbolHandlerOptions options) 12 | => options.RootVariable = string.Empty; 13 | } 14 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/ICollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace PlcInterface.OpcUa; 4 | 5 | /// 6 | /// Extension methods for . 7 | /// 8 | internal static class ICollectionExtensions 9 | { 10 | /// 11 | /// Convert a in a . 12 | /// 13 | /// The type of the elements in this collection. 14 | /// The source . 15 | /// A . 16 | public static IReadOnlyCollection AsReadOnly(this ICollection source) 17 | { 18 | ArgumentNullException.ThrowIfNull(source); 19 | return source as IReadOnlyCollection ?? new ReadOnlyCollection(source); 20 | } 21 | 22 | private sealed class ReadOnlyCollection(ICollection source) : IReadOnlyCollection 23 | { 24 | public int Count => source.Count; 25 | 26 | public IEnumerator GetEnumerator() => source.GetEnumerator(); 27 | 28 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/IOpcMonitor.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.OpcUa; 2 | 3 | /// 4 | /// The Opc implementation of a . 5 | /// 6 | public interface IOpcMonitor : IMonitor 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/IOpcPlcConnection.cs: -------------------------------------------------------------------------------- 1 | using Opc.Ua.Client; 2 | 3 | namespace PlcInterface.OpcUa; 4 | 5 | /// 6 | /// The Opc implementation of a . 7 | /// 8 | public interface IOpcPlcConnection : IPlcConnection 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/IOpcReadWrite.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.OpcUa; 2 | 3 | /// 4 | /// The Ads implementation of a . 5 | /// 6 | public interface IOpcReadWrite : IReadWrite 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/IOpcSymbolHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace PlcInterface.OpcUa; 4 | 5 | /// 6 | /// The Opc implementation of a . 7 | /// 8 | public interface IOpcSymbolHandler : ISymbolHandler 9 | { 10 | /// 11 | /// Gets the . 12 | /// 13 | /// The tag name. 14 | /// The found . 15 | public new IOpcSymbolInfo GetSymbolInfo(string ioName); 16 | 17 | /// 18 | /// Try to get the . 19 | /// 20 | /// The tag name. 21 | /// The found . 22 | /// when the symbol was found else . 23 | public bool TryGetSymbolInfo(string ioName, [MaybeNullWhen(false)] out IOpcSymbolInfo symbolInfo); 24 | } 25 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/IOpcSymbolInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection.Metadata; 2 | using Opc.Ua; 3 | 4 | namespace PlcInterface.OpcUa; 5 | 6 | /// 7 | /// The OPC implementation of a . 8 | /// 9 | public interface IOpcSymbolInfo : ISymbolInfo 10 | { 11 | /// 12 | /// Gets get the shape of the array. 13 | /// 14 | public ArrayShape ArrayShape 15 | { 16 | get; 17 | } 18 | 19 | /// 20 | /// Gets the built in type. 21 | /// 22 | public BuiltInType BuiltInType 23 | { 24 | get; 25 | } 26 | 27 | /// 28 | /// Gets the PLC symbol this encapsules. 29 | /// 30 | public NodeId Handle 31 | { 32 | get; 33 | } 34 | 35 | /// 36 | /// Gets the indices of this array item. 37 | /// 38 | public int[] Indices 39 | { 40 | get; 41 | } 42 | 43 | /// 44 | /// Gets a value indicating whether this symbol represents a array. 45 | /// 46 | public bool IsArray 47 | { 48 | get; 49 | } 50 | 51 | /// 52 | /// Gets a value indicating whether this symbol represents a complex type. 53 | /// 54 | public bool IsBigType 55 | { 56 | get; 57 | } 58 | 59 | /// 60 | /// Gets the display name of the type. 61 | /// 62 | public string TypeName 63 | { 64 | get; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/IOpcTypeConverter.cs: -------------------------------------------------------------------------------- 1 | using Opc.Ua; 2 | 3 | namespace PlcInterface.OpcUa; 4 | 5 | /// 6 | /// Specialized for Opc Types. 7 | /// 8 | public interface IOpcTypeConverter : ITypeConverter 9 | { 10 | /// 11 | /// Converts Opc types to System types. 12 | /// 13 | /// The . 14 | /// The value to fix. 15 | /// The converted type. 16 | public object Convert(IOpcSymbolInfo symbolInfo, object value); 17 | 18 | /// 19 | /// Converts Opc types to System types. 20 | /// 21 | /// The name of the symbol. 22 | /// The value to fix. 23 | /// The converted type. 24 | public object Convert(string symbolName, object value); 25 | 26 | /// 27 | /// Create a dynamic type. 28 | /// 29 | /// The . 30 | /// a to enumerate the values. 31 | /// A dynamic object representing the . 32 | public dynamic CreateDynamic(IOpcSymbolInfo symbolInfo, IEnumerator valueEnumerator); 33 | 34 | /// 35 | /// Create a dynamic type. 36 | /// 37 | /// The name of the symbol. 38 | /// a to enumerate the values. 39 | /// A dynamic object representing the . 40 | public dynamic CreateDynamic(string symbolName, IEnumerator valueEnumerator); 41 | 42 | /// 43 | /// Create a for the given symbol with value. 44 | /// 45 | /// The . 46 | /// The value to store. 47 | /// The type containing the value. 48 | public Variant CreateOpcVariant(IOpcSymbolInfo symbolInfo, object value); 49 | 50 | /// 51 | /// Create a for the given symbol with value. 52 | /// 53 | /// The name of the symbol. 54 | /// The value to store. 55 | /// The type containing the value. 56 | public Variant CreateOpcVariant(string symbolName, object value); 57 | } 58 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/IServiceCollectionExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace PlcInterface.OpcUa; 4 | 5 | /// 6 | /// Extension methods for . 7 | /// 8 | public static class IServiceCollectionExtension 9 | { 10 | /// 11 | /// Configure the for this PLC. 12 | /// 13 | /// The to add the services to. 14 | /// A reference to this instance after the operation has completed. 15 | public static IServiceCollection AddOpcPLC(this IServiceCollection serviceDescriptors) 16 | => serviceDescriptors 17 | .AddSingletonFactory() 18 | .AddSingletonFactory() 19 | .AddSingletonFactory() 20 | .AddSingletonFactory, IOpcPlcConnection>() 21 | .AddSingleton(x => x.GetRequiredService()) 22 | .AddTransient() 23 | .ConfigureOptions() 24 | .ConfigureOptions(); 25 | } 26 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/ISymbolInfoExtension.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.OpcUa; 2 | 3 | /// 4 | /// Extension methods for . 5 | /// 6 | internal static class ISymbolInfoExtension 7 | { 8 | /// 9 | /// Convert the to and throw a exception 10 | /// if the conversion fails. 11 | /// 12 | /// The to change. 13 | /// The cast object. 14 | /// If the cast fails. 15 | public static IOpcSymbolInfo ConvertAndValidate(this ISymbolInfo symbolInfo) 16 | { 17 | if (symbolInfo is not IOpcSymbolInfo symbol) 18 | { 19 | throw new SymbolException($"symbol is not a {typeof(IOpcSymbolInfo)}"); 20 | } 21 | 22 | return symbol; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/Monitor.Logging.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace PlcInterface.OpcUa; 4 | 5 | /// 6 | /// Logging source generator methods. 7 | /// 8 | public partial class Monitor 9 | { 10 | [LoggerMessage(EventId = 1, Level = LogLevel.Debug, Message = "Updating subscriptions")] 11 | private partial void LogUpdatingSubscriptions(); 12 | 13 | [LoggerMessage(EventId = 4, Level = LogLevel.Debug, Message = "{VariableName} is not registered")] 14 | private partial void LogVariableNotRegistered(string variableName); 15 | 16 | [LoggerMessage(EventId = 2, Level = LogLevel.Debug, Message = "Registered IO {VariableName}")] 17 | private partial void LogVariableRegistered(string variableName); 18 | 19 | [LoggerMessage(EventId = 5, Level = LogLevel.Debug, Message = "{VariableName} still has {SubscriptionCount} subscriptions left")] 20 | private partial void LogVariableStillHasSubscriptions(string variableName, int subscriptionCount); 21 | 22 | [LoggerMessage(EventId = 3, Level = LogLevel.Debug, Message = "Unregistered IO {VariableName}")] 23 | private partial void LogVariableUnregistered(string variableName); 24 | } 25 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/MonitorResult.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.OpcUa; 2 | 3 | /// 4 | /// Implementation for . 5 | /// 6 | /// 7 | /// Initializes a new instance of the class. 8 | /// 9 | /// The name of the tag. 10 | /// The value of the tag. 11 | internal sealed class MonitorResult(string name, object value) : IMonitorResult 12 | { 13 | /// 14 | public string Name => name; 15 | 16 | /// 17 | public object Value => value; 18 | } 19 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/NodeInfo.cs: -------------------------------------------------------------------------------- 1 | using Opc.Ua; 2 | using Opc.Ua.Client; 3 | 4 | namespace PlcInterface.OpcUa; 5 | 6 | /// 7 | /// Contains extra information about a . 8 | /// 9 | internal sealed class NodeInfo 10 | { 11 | private readonly Lazy builtInType; 12 | private readonly Lazy dataTypeDisplayText; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The session to get the data from. 18 | /// The data type of this node. 19 | /// The description text of this node. 20 | /// The value rank of this node. 21 | public NodeInfo(ISession session, NodeId dataType, string description, int valueRank) 22 | { 23 | Description = description; 24 | ValueRank = valueRank; 25 | DataType = dataType; 26 | 27 | if (NodeId.IsNull(dataType)) 28 | { 29 | builtInType = new Lazy(() => BuiltInType.Null, isThreadSafe: false); 30 | dataTypeDisplayText = new Lazy(() => string.Empty, isThreadSafe: false); 31 | } 32 | else 33 | { 34 | builtInType = new Lazy(() => DataTypes.GetBuiltInType(dataType, session.TypeTree), isThreadSafe: false); 35 | dataTypeDisplayText = new Lazy(() => session.NodeCache.GetDisplayText(dataType), isThreadSafe: false); 36 | } 37 | } 38 | 39 | /// 40 | /// Gets the build in data type of this node. 41 | /// 42 | public BuiltInType BuiltInType 43 | => builtInType.Value; 44 | 45 | /// 46 | /// Gets the for the data type. 47 | /// 48 | public NodeId DataType 49 | { 50 | get; 51 | } 52 | 53 | /// 54 | /// Gets the display text of the . 55 | /// 56 | public string DataTypeDisplayText 57 | => ValueRank >= 0 ? dataTypeDisplayText.Value + "[]" : dataTypeDisplayText.Value; 58 | 59 | /// 60 | /// Gets the description text of the . 61 | /// 62 | public string Description 63 | { 64 | get; 65 | } 66 | 67 | /// 68 | /// Gets the rank if this node represents a array. 69 | /// 70 | public int ValueRank 71 | { 72 | get; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/OpcPlcConnectionOptions.cs: -------------------------------------------------------------------------------- 1 | using Opc.Ua; 2 | 3 | namespace PlcInterface.OpcUa; 4 | 5 | /// 6 | /// Settings for the . 7 | /// 8 | public class OpcPlcConnectionOptions 9 | { 10 | /// 11 | /// Gets or sets the address to connect to. 12 | /// 13 | public string Address { get; set; } = "127.0.0.1"; 14 | 15 | /// 16 | /// Gets or sets the application configuration. 17 | /// 18 | public ApplicationConfiguration? ApplicationConfiguration 19 | { 20 | get; 21 | set; 22 | } 23 | 24 | /// 25 | /// Gets or sets a value indicating whether the connection should be opened automatically. 26 | /// 27 | public bool AutoConnect 28 | { 29 | get; 30 | set; 31 | } 32 | 33 | /// 34 | /// Gets or sets a value indicating whether certificate should be generated automatically. 35 | /// 36 | public bool AutoGenCertificate 37 | { 38 | get; 39 | set; 40 | } 41 | 42 | /// 43 | /// Gets the discovery address. 44 | /// 45 | public Uri DiscoveryAddress => new UriBuilder(FullAddress) { Path = "/discovery", }.Uri; 46 | 47 | /// 48 | /// Gets the address to connect to. 49 | /// 50 | public Uri FullAddress => new UriBuilder(UriSchema, Address, Port, string.Empty).Uri; 51 | 52 | /// 53 | /// Gets or sets the password. 54 | /// 55 | public string? Password 56 | { 57 | get; 58 | set; 59 | } 60 | 61 | /// 62 | /// Gets or sets the port to connect to. 63 | /// 64 | public int Port { get; set; } = 4840; 65 | 66 | /// 67 | /// Gets or sets the connection schema to use. 68 | /// 69 | /// 70 | /// Default value is 'opc.tcp'. 71 | /// 72 | public string UriSchema { get; set; } = "opc.tcp"; 73 | 74 | /// 75 | /// Gets or sets the user name. 76 | /// 77 | public string? UserName 78 | { 79 | get; 80 | set; 81 | } 82 | 83 | /// 84 | /// Gets or sets a value indicating whether use secure connection when available. 85 | /// 86 | public bool UseSecurity 87 | { 88 | get; 89 | set; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/OpcSymbolHandlerOptions.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.OpcUa; 2 | 3 | /// 4 | /// Settings for the . 5 | /// 6 | public class OpcSymbolHandlerOptions 7 | { 8 | /// 9 | /// Gets or sets the path to the root node. 10 | /// 11 | /// 12 | /// Sub items are separated by a '.'. 13 | /// 14 | public string RootVariable { get; set; } = string.Empty; 15 | } 16 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/PlcInterface.OpcUa.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | A PLC communication implementation for OPC 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/SymbolHandler.Logging.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace PlcInterface.OpcUa; 4 | 5 | /// 6 | /// Logging source generator methods. 7 | /// 8 | public partial class SymbolHandler 9 | { 10 | [LoggerMessage(EventId = 5, Level = LogLevel.Error, Message = "Plc not connected")] 11 | private partial void LogPlcNotConnected(); 12 | 13 | [LoggerMessage(EventId = 3, Level = LogLevel.Information, Message = "Symbols updated in {Time} ms, found {Amount} symbols")] 14 | private partial void LogSymbolsUpdated(long time, int amount); 15 | 16 | [LoggerMessage(EventId = 2, Level = LogLevel.Debug, Message = "Updating symbols")] 17 | private partial void LogUpdatingSymbols(); 18 | 19 | [LoggerMessage(EventId = 4, Level = LogLevel.Error, Message = "Updating symbols failed")] 20 | private partial void LogUpdatingSymbolsFailed(Exception exception); 21 | 22 | [LoggerMessage(EventId = 1, Level = LogLevel.Error, Message = "{VariableName} does not exist")] 23 | private partial void LogVariableDoesNotExist(string variableName); 24 | } 25 | -------------------------------------------------------------------------------- /src/PlcInterface.OpcUa/TreeBrowser.Logging.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace PlcInterface.OpcUa; 4 | 5 | /// 6 | /// Logging source generator methods. 7 | /// 8 | internal partial class TreeBrowser 9 | { 10 | [LoggerMessage(EventId = 100, Level = LogLevel.Warning, Message = "Failed to browse symbols (Error: {StatusCode}), changing chunk size {OldSize} -> {NewSize}")] 11 | private partial void LogBrowseFailed(uint statusCode, int oldSize, int newSize); 12 | } 13 | -------------------------------------------------------------------------------- /src/PlcInterface.Sandbox/PLCCommands/AdsPlcConnectCommand.cs: -------------------------------------------------------------------------------- 1 | using PlcInterface.Ads; 2 | using TwinCAT.Ads.TcpRouter; 3 | using Vectron.InteractiveConsole.Commands; 4 | 5 | namespace PlcInterface.Sandbox.PLCCommands; 6 | 7 | /// 8 | /// Base class for a to connect to the PLC through ads. 9 | /// 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// A instance. 14 | /// A for running without TwinCAT installation. 15 | internal sealed class AdsPlcConnectCommand(IAdsPlcConnection plcConnection, IAmsRouter amsRouter) : PlcConnectCommand("ads", plcConnection) 16 | { 17 | private readonly IAmsRouter amsRouter = amsRouter; 18 | 19 | /// 20 | public override void Execute(string[] arguments) 21 | { 22 | if (Environment.GetEnvironmentVariable("TWINCAT3DIR") == null) 23 | { 24 | using var cancellationTokenSource = new CancellationTokenSource(5000); 25 | _ = amsRouter.StartAsync(cancellationTokenSource.Token); 26 | while (!amsRouter.IsRunning) 27 | { 28 | if (cancellationTokenSource.Token.IsCancellationRequested) 29 | { 30 | Console.WriteLine("Failed to start the AMS router."); 31 | return; 32 | } 33 | 34 | Thread.Sleep(100); 35 | } 36 | } 37 | 38 | base.Execute(arguments); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/PlcInterface.Sandbox/PLCCommands/AdsPlcDisconnectCommand.cs: -------------------------------------------------------------------------------- 1 | using PlcInterface.Ads; 2 | using TwinCAT.Ads.TcpRouter; 3 | using Vectron.InteractiveConsole.Commands; 4 | 5 | namespace PlcInterface.Sandbox.PLCCommands; 6 | 7 | /// 8 | /// Base class for a to disconnect from the ads PLC. 9 | /// 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// A instance. 14 | /// A for running without TwinCAT installation. 15 | internal sealed class AdsPlcDisconnectCommand(IAdsPlcConnection plcConnection, IAmsRouter amsRouter) : PlcDisconnectCommand("ads", plcConnection) 16 | { 17 | /// 18 | public override void Execute(string[] arguments) 19 | { 20 | base.Execute(arguments); 21 | if (Environment.GetEnvironmentVariable("TWINCAT3DIR") == null) 22 | { 23 | amsRouter.Stop(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/PlcInterface.Sandbox/PLCCommands/AdsWriteCommand.cs: -------------------------------------------------------------------------------- 1 | using PlcInterface.Ads; 2 | using TwinCAT.TypeSystem; 3 | 4 | namespace PlcInterface.Sandbox.PLCCommands; 5 | 6 | /// 7 | /// Command for writing values with ADS. 8 | /// 9 | /// 10 | /// Initializes a new instance of the class. 11 | /// 12 | /// A instance. 13 | /// A instance. 14 | /// A instance. 15 | internal sealed class AdsWriteCommand(IAdsReadWrite readWrite, IAdsSymbolHandler symbolHandler, IAdsTypeConverter typeConverter) : PlcWriteCommand("ads", readWrite) 16 | { 17 | /// 18 | protected override object ConvertToValidInputValue(string symbolName, string value) 19 | { 20 | if (!symbolHandler.TryGetSymbolInfo(symbolName, out var symbolInfo)) 21 | { 22 | throw new InvalidOperationException("Symbol not found"); 23 | } 24 | 25 | if (symbolInfo.IsArray) 26 | { 27 | if (!value.StartsWith('[') || !value.EndsWith(']')) 28 | { 29 | throw new InvalidOperationException("Arrays must have the following syntax: ['values']"); 30 | } 31 | 32 | if (symbolInfo.Symbol is not IArrayInstance arrayInstance 33 | || arrayInstance.ElementType is not TwinCAT.Ads.TypeSystem.DataType elementDataType 34 | || elementDataType.ManagedType == null) 35 | { 36 | throw new InvalidOperationException("Unable to read data type."); 37 | } 38 | 39 | return value[1..^1] 40 | .Split(',') 41 | .Select(x => typeConverter.Convert(x, elementDataType.ManagedType)) 42 | .ToArray(); 43 | } 44 | 45 | if (symbolInfo.IsBigType) 46 | { 47 | throw new InvalidOperationException("object types are not supported"); 48 | } 49 | 50 | if (symbolInfo.Symbol.DataType == null) 51 | { 52 | throw new InvalidOperationException("Unable to read data type."); 53 | } 54 | 55 | if (!symbolInfo.Symbol.DataType.IsPrimitive()) 56 | { 57 | throw new InvalidOperationException("only primitive types are supported"); 58 | } 59 | 60 | if (symbolInfo.Symbol.DataType is not TwinCAT.Ads.TypeSystem.DataType dataType 61 | || dataType.ManagedType == null) 62 | { 63 | throw new InvalidOperationException("Unable to read data type."); 64 | } 65 | 66 | var boxedValue = typeConverter.Convert(value, dataType.ManagedType) 67 | ?? throw new InvalidOperationException("Unknown data type"); 68 | 69 | return boxedValue; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/PlcInterface.Sandbox/PLCCommands/PlcConnectCommand.cs: -------------------------------------------------------------------------------- 1 | using Vectron.InteractiveConsole.Commands; 2 | 3 | namespace PlcInterface.Sandbox.PLCCommands; 4 | 5 | /// 6 | /// Base class for a to connect to the PLC. 7 | /// 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// The name of the interface. 12 | /// A instance. 13 | internal class PlcConnectCommand(string name, IPlcConnection plcConnection) : IConsoleCommand 14 | { 15 | /// 16 | /// The parameter needed for this command. 17 | /// 18 | public const string Parameter = "connect"; 19 | 20 | /// 21 | public string[]? ArgumentNames => []; 22 | 23 | /// 24 | public string[] CommandParameters { get; } = [name, Parameter]; 25 | 26 | /// 27 | public string HelpText => "Connect to the PLC through " + CommandParameters[0]; 28 | 29 | /// 30 | public int MaxArguments => 0; 31 | 32 | /// 33 | public int MinArguments => 0; 34 | 35 | /// 36 | public virtual void Execute(string[] arguments) 37 | { 38 | _ = plcConnection.ConnectAsync(); 39 | Console.WriteLine("Connecting to the PLC"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/PlcInterface.Sandbox/PLCCommands/PlcDisconnectCommand.cs: -------------------------------------------------------------------------------- 1 | using Vectron.InteractiveConsole.Commands; 2 | 3 | namespace PlcInterface.Sandbox.PLCCommands; 4 | 5 | /// 6 | /// Base class for a to disconnect from the PLC. 7 | /// 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// The name of the interface. 12 | /// A instance. 13 | internal class PlcDisconnectCommand(string name, IPlcConnection plcConnection) : IConsoleCommand 14 | { 15 | /// 16 | /// The parameter needed for this command. 17 | /// 18 | public const string Parameter = "disconnect"; 19 | 20 | /// 21 | public string[]? ArgumentNames => []; 22 | 23 | /// 24 | public string[] CommandParameters { get; } = [name, Parameter]; 25 | 26 | /// 27 | public string HelpText => "Disconnect from the PLC through " + CommandParameters[0]; 28 | 29 | /// 30 | public int MaxArguments => 0; 31 | 32 | /// 33 | public int MinArguments => 0; 34 | 35 | /// 36 | public virtual void Execute(string[] arguments) 37 | { 38 | _ = plcConnection.DisconnectAsync(); 39 | Console.WriteLine("Disconnecting from the PLC"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/PlcInterface.Sandbox/PLCCommands/PlcMonitorCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Linq; 2 | using Vectron.InteractiveConsole.Commands; 3 | 4 | namespace PlcInterface.Sandbox.PLCCommands; 5 | 6 | /// 7 | /// Base class for a to monitor a variable in the PLC. 8 | /// 9 | /// 10 | /// Initializes a new instance of the class. 11 | /// 12 | /// The name of the interface. 13 | /// A instance. 14 | internal sealed class PlcMonitorCommand(string name, IMonitor monitor) : IConsoleCommand, IDisposable 15 | { 16 | /// 17 | /// The parameter needed for this command. 18 | /// 19 | public const string Parameter = "monitor"; 20 | 21 | private readonly IDisposable monitorSubscription = monitor.SymbolStream.Subscribe(x => Console.WriteLine($"{x.Name}: {x.Value}")); 22 | private bool disposed; 23 | 24 | /// 25 | public string[]? ArgumentNames => ["tag"]; 26 | 27 | /// 28 | public string[] CommandParameters { get; } = [name, Parameter]; 29 | 30 | /// 31 | public string HelpText => "Monitor a variable in the PLC with " + CommandParameters[0]; 32 | 33 | /// 34 | public int MaxArguments => 1; 35 | 36 | /// 37 | public int MinArguments => 1; 38 | 39 | /// 40 | public void Dispose() 41 | { 42 | if (disposed) 43 | { 44 | return; 45 | } 46 | 47 | disposed = true; 48 | monitorSubscription?.Dispose(); 49 | } 50 | 51 | /// 52 | public void Execute(string[] arguments) 53 | { 54 | var symbolName = arguments[0]; 55 | monitor.RegisterIO(symbolName, 10); 56 | Console.WriteLine($"Started monitoring {symbolName}"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/PlcInterface.Sandbox/PLCCommands/PlcStopMonitorCommand.cs: -------------------------------------------------------------------------------- 1 | using Vectron.InteractiveConsole.Commands; 2 | 3 | namespace PlcInterface.Sandbox.PLCCommands; 4 | 5 | /// 6 | /// Base class for a to monitor a variable in the PLC. 7 | /// 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// The name of the interface. 12 | /// A instance. 13 | internal sealed class PlcStopMonitorCommand(string name, IMonitor monitor) : IConsoleCommand 14 | { 15 | /// 16 | /// The parameter needed for this command. 17 | /// 18 | public const string Parameter = "unmonitor"; 19 | 20 | /// 21 | public string[]? ArgumentNames => ["tag"]; 22 | 23 | /// 24 | public string[] CommandParameters { get; } = [name, Parameter]; 25 | 26 | /// 27 | public string HelpText => "Stop monitoring a variable in the PLC with " + CommandParameters[0]; 28 | 29 | /// 30 | public int MaxArguments => 1; 31 | 32 | /// 33 | public int MinArguments => 1; 34 | 35 | /// 36 | public void Execute(string[] arguments) 37 | { 38 | var symbolName = arguments[0]; 39 | monitor.UnregisterIO(symbolName); 40 | Console.WriteLine($"Stopped monitoring {symbolName}"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/PlcInterface.Sandbox/PLCCommands/PlcSymbolDumpCommand.cs: -------------------------------------------------------------------------------- 1 | using Vectron.InteractiveConsole.Commands; 2 | 3 | namespace PlcInterface.Sandbox.PLCCommands; 4 | 5 | /// 6 | /// Base class for a dump all symbols from the PLC to the console. 7 | /// 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// The name of the interface. 12 | /// A instance. 13 | internal sealed class PlcSymbolDumpCommand(string name, ISymbolHandler symbolHandler) : IConsoleCommand 14 | { 15 | /// 16 | /// The parameter needed for this command. 17 | /// 18 | public const string Parameter = "dump"; 19 | 20 | /// 21 | public string[]? ArgumentNames => []; 22 | 23 | /// 24 | public string[] CommandParameters { get; } = [name, Parameter]; 25 | 26 | /// 27 | public string HelpText => "Dump all symbols from the PLC with " + CommandParameters[0]; 28 | 29 | /// 30 | public int MaxArguments => 0; 31 | 32 | /// 33 | public int MinArguments => 0; 34 | 35 | /// 36 | public void Execute(string[] arguments) 37 | { 38 | foreach (var symbolName in symbolHandler.AllSymbols.Select(x => x.Name)) 39 | { 40 | Console.WriteLine(symbolName); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/PlcInterface.Sandbox/PLCCommands/PlcToggleCommand.cs: -------------------------------------------------------------------------------- 1 | using Vectron.InteractiveConsole.Commands; 2 | 3 | namespace PlcInterface.Sandbox.PLCCommands; 4 | 5 | /// 6 | /// Base class for a to toggle a boolean variable in the PLC. 7 | /// 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// The name of the interface. 12 | /// A instance. 13 | internal sealed class PlcToggleCommand(string name, IReadWrite readWrite) : IConsoleCommand 14 | { 15 | /// 16 | /// The parameter needed for this command. 17 | /// 18 | public const string Parameter = "toggle"; 19 | 20 | /// 21 | public string[]? ArgumentNames => ["tag"]; 22 | 23 | /// 24 | public string[] CommandParameters { get; } = [name, Parameter]; 25 | 26 | /// 27 | public string HelpText => "Toggle a boolean variable in the PLC with " + CommandParameters[0]; 28 | 29 | /// 30 | public int MaxArguments => 1; 31 | 32 | /// 33 | public int MinArguments => 1; 34 | 35 | /// 36 | public void Execute(string[] arguments) 37 | { 38 | var symbolName = arguments[0]; 39 | readWrite.ToggleBool(symbolName); 40 | Console.WriteLine("Value toggled"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/PlcInterface.Sandbox/PLCCommands/PlcWriteCommand.cs: -------------------------------------------------------------------------------- 1 | using Vectron.InteractiveConsole.Commands; 2 | 3 | namespace PlcInterface.Sandbox.PLCCommands; 4 | 5 | /// 6 | /// Base class for a to read a variable from the PLC. 7 | /// 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// The name of the interface. 12 | /// A instance. 13 | internal abstract class PlcWriteCommand(string name, IReadWrite readWrite) : IConsoleCommand 14 | { 15 | /// 16 | /// The parameter needed for this command. 17 | /// 18 | public const string Parameter = "write"; 19 | 20 | /// 21 | public string[]? ArgumentNames => ["tag", "new value"]; 22 | 23 | /// 24 | public string[] CommandParameters { get; init; } = [name, Parameter]; 25 | 26 | /// 27 | public string HelpText => "Write a variable to the PLC with " + CommandParameters[0]; 28 | 29 | /// 30 | public int MaxArguments => 2; 31 | 32 | /// 33 | public int MinArguments => 2; 34 | 35 | /// 36 | public void Execute(string[] arguments) 37 | { 38 | var symbolName = arguments[0]; 39 | var symbolValue = ConvertToValidInputValue(symbolName, arguments[1]); 40 | readWrite.Write(symbolName, symbolValue); 41 | Console.WriteLine("Value written to PLC"); 42 | } 43 | 44 | /// 45 | /// Convert the input to the valid write value. 46 | /// 47 | /// The name of the symbol to write. 48 | /// The value to convert. 49 | /// An object with the correct type for writing. 50 | protected abstract object ConvertToValidInputValue(string symbolName, string value); 51 | } 52 | -------------------------------------------------------------------------------- /src/PlcInterface.Sandbox/PlcInterface.Sandbox.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | PreserveNewest 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/PlcInterface.Sandbox/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | C:\Users\t.bloebaum\Dropbox\VisualStudio\Projects\PlcInterface\bin\publish\PlcInterface.Sandbox\ 10 | FileSystem 11 | <_TargetId>Folder 12 | false 13 | 14 | -------------------------------------------------------------------------------- /src/PlcInterface.Sandbox/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Trace", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | }, 8 | 9 | "Console": { 10 | "FormatterName": "Themed", 11 | "FormatterOptions": { 12 | "IncludeScopes": true, 13 | "TimestampFormat": "HH:MM:ss", 14 | "UseUtcTimestamp": false, 15 | "ColorWholeLine": false, 16 | "Theme": "MEL-Dark" 17 | }, 18 | "LogLevel": { 19 | "Default": "Information" 20 | } 21 | } 22 | }, 23 | 24 | "Opc": { 25 | "Connection": { 26 | "UriSchema": "opc.tcp", 27 | "Address": "172.30.70.5", 28 | "Port": 4840, 29 | "AutoConnect": false, 30 | "AutoGenCertificate": false, 31 | "UseSecurity": true 32 | }, 33 | 34 | "SymbolHandler": { 35 | "RootVariable": "PLC1" 36 | } 37 | }, 38 | 39 | "Ads": { 40 | "Connection": { 41 | "AmsNetId": "172.99.0.2.1.1", 42 | "AutoConnect": false, 43 | "port": 851 44 | }, 45 | 46 | "SymbolHandler": { 47 | "OutputPath": "", 48 | "StoreSymbolsToDisk": false, 49 | "RootVariable": "" 50 | } 51 | }, 52 | 53 | "AmsRouter": { 54 | "Name": "PlcSandbox", 55 | "NetId": "172.22.50.90.1.1", 56 | "TcpPort": 48898, 57 | "RemoteConnections": [ 58 | { 59 | "Name": "Gantry", 60 | "Address": "172.30.70.5", 61 | "NetId": "172.99.0.2.1.1", 62 | "Type": "TCP_IP" 63 | } 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/PlcInterface.Sandbox/nlog.config: -------------------------------------------------------------------------------- 1 |  2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /test/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $(NoWarn),1573,1591,1712 6 | $(WarningsNotAsErrors);MSTESTOBS 7 | --config-file "$(MSBuildThisFileDirectory)\testconfig.json" --report-trx --coverage --coverage-output-format cobertura 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/PlcInterface.Abstraction/ConnectedTests.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.Abstraction.Tests; 2 | 3 | [TestClass] 4 | public class ConnectedTests 5 | { 6 | [TestMethod] 7 | public void ConnectedImplementsIConnected() 8 | { 9 | // Arrange 10 | var connected = new Connected(); 11 | 12 | // Act 13 | Assert.IsInstanceOfType(connected); 14 | Assert.IsInstanceOfType>(connected); 15 | 16 | // Assert 17 | Assert.IsInstanceOfType(connected); 18 | Assert.IsInstanceOfType>(connected); 19 | } 20 | 21 | [TestMethod] 22 | public void ConnectedNoReturnsAValidIConnected() 23 | { 24 | // Arrange 25 | 26 | // Act 27 | var notConnected = Connected.No(); 28 | 29 | // Assert 30 | Assert.IsInstanceOfType(notConnected); 31 | Assert.IsInstanceOfType>(notConnected); 32 | Assert.IsFalse(notConnected.IsConnected); 33 | _ = Assert.ThrowsExactly(() => 34 | { 35 | var connection = notConnected.Value; 36 | connection.Data = 42; 37 | }); 38 | } 39 | 40 | [TestMethod] 41 | public void ConnectedYesReturnsAValidIConnected() 42 | { 43 | // Arrange 44 | var expectedValue = new GenericParameterHelper(); 45 | 46 | // Act 47 | var connected = Connected.Yes(expectedValue); 48 | 49 | // Assert 50 | Assert.IsInstanceOfType(connected); 51 | Assert.IsInstanceOfType>(connected); 52 | Assert.IsTrue(connected.IsConnected); 53 | Assert.AreSame(expectedValue, connected.Value); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/PlcInterface.Abstraction/IPlcConnectionExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Linq; 2 | using Moq; 3 | 4 | namespace PlcInterface.Abstraction.Tests; 5 | 6 | [TestClass] 7 | public class IPlcConnectionExtensionTests 8 | { 9 | [TestMethod] 10 | public async Task GetConnectedClientAsyncReturnsAValidValueAsync() 11 | { 12 | // Arrange 13 | var plcConnectionMock = new Mock>(); 14 | var iConnectedMock = new Mock>(); 15 | var expected = new GenericParameterHelper(); 16 | _ = iConnectedMock.SetupGet(x => x.IsConnected).Returns(value: true); 17 | _ = iConnectedMock.SetupGet(x => x.Value).Returns(expected); 18 | _ = plcConnectionMock.SetupGet(x => x.SessionStream).Returns(Observable.Repeat(iConnectedMock.Object)); 19 | 20 | // Act 21 | var connection = await plcConnectionMock.Object.GetConnectedClientAsync(); 22 | 23 | // Assert 24 | Assert.AreEqual(expected, connection); 25 | } 26 | 27 | [TestMethod] 28 | public async Task GetConnectedClientAsyncThrowsTimeoutExceptionOnTimeOutAsync() 29 | { 30 | // Arrange 31 | var mock = new Mock>(); 32 | _ = mock.SetupGet(x => x.SessionStream).Returns(Observable.Repeat(Mock.Of>())); 33 | 34 | // Act Assert 35 | _ = await Assert.ThrowsExactlyAsync(() => mock.Object.GetConnectedClientAsync(TimeSpan.FromMilliseconds(10))); 36 | } 37 | 38 | [TestMethod] 39 | public void GetConnectedClientReturnsAValidValue() 40 | { 41 | // Arrange 42 | var plcConnectionMock = new Mock>(); 43 | var iConnectedMock = new Mock>(); 44 | var expected = new GenericParameterHelper(); 45 | _ = iConnectedMock.SetupGet(x => x.IsConnected).Returns(value: true); 46 | _ = iConnectedMock.SetupGet(x => x.Value).Returns(expected); 47 | _ = plcConnectionMock.SetupGet(x => x.SessionStream).Returns(Observable.Repeat(iConnectedMock.Object)); 48 | 49 | // Act 50 | var connection = plcConnectionMock.Object.GetConnectedClient(); 51 | 52 | // Assert 53 | Assert.AreEqual(expected, connection); 54 | } 55 | 56 | [TestMethod] 57 | public void GetConnectedClientThrowsTimeoutExceptionOnTimeOut() 58 | { 59 | // Arrange 60 | var mock = new Mock>(); 61 | _ = mock.SetupGet(x => x.SessionStream).Returns(Observable.Repeat(Mock.Of>())); 62 | 63 | // Act Assert 64 | _ = Assert.ThrowsExactlyAsync(() => mock.Object.GetConnectedClientAsync(TimeSpan.FromMilliseconds(10))); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/PlcInterface.Abstraction/MyTypeBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Reflection.Emit; 3 | 4 | namespace PlcInterface.Abstraction.Tests; 5 | 6 | public static class MyTypeBuilder 7 | { 8 | public static Type CompileResultType() 9 | { 10 | var tb = GetTypeBuilder(); 11 | var constructor = tb.DefineConstructor( 12 | MethodAttributes.Public 13 | | MethodAttributes.SpecialName 14 | | MethodAttributes.RTSpecialName, 15 | CallingConventions.Standard, 16 | [typeof(int)]); 17 | 18 | // generate the code to call the parent's default constructor 19 | var il = constructor.GetILGenerator(); 20 | il.Emit(OpCodes.Ldarg_0); 21 | il.Emit(OpCodes.Ret); 22 | 23 | return tb.CreateType()!; 24 | } 25 | 26 | private static TypeBuilder GetTypeBuilder() 27 | { 28 | var typeSignature = "MyDynamicType"; 29 | var an = new AssemblyName(typeSignature); 30 | var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run); 31 | var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule"); 32 | var tb = moduleBuilder.DefineType( 33 | typeSignature, 34 | TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout, 35 | parent: null); 36 | return tb; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/PlcInterface.Abstraction/ObjectActivatorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Moq; 3 | 4 | namespace PlcInterface.Abstraction.Tests; 5 | 6 | [TestClass] 7 | public class ObjectActivatorTests 8 | { 9 | [TestMethod] 10 | public void DeclaringTypeIsNullThrowsArgumentException() 11 | { 12 | // Arrange 13 | var constructorInfo = Mock.Of(x => x.DeclaringType == null); 14 | 15 | // Act Assert 16 | _ = Assert.ThrowsExactly(() => 17 | { 18 | var objectActivator = new ObjectActivator(constructorInfo); 19 | var type = objectActivator.GetType(); 20 | }); 21 | } 22 | 23 | [TestMethod] 24 | public void FailsCreateInstanceIfParameterNameIsNull() 25 | { 26 | // Arrange 27 | var type = MyTypeBuilder.CompileResultType(); 28 | var constructor = type.GetConstructor([typeof(byte)])!; 29 | var parameterInfo = constructor.GetParameters()[0]; 30 | Assert.IsNull(parameterInfo.Name); 31 | var activator = new ObjectActivator(constructor!); 32 | 33 | // Act 34 | var result = activator.TryCreateInstance((name, type) => null, 1, out var instance); 35 | 36 | // Assert 37 | Assert.IsFalse(result); 38 | Assert.IsNull(instance); 39 | } 40 | 41 | [TestMethod] 42 | public void NullValueThrowsSymbolException() 43 | { 44 | // Arrange 45 | var constructor = typeof(TestClass) 46 | .GetConstructors() 47 | .First(x => x.GetParameters().Length == 1); 48 | var activator = new ObjectActivator(constructor); 49 | void Action() => _ = activator.TryCreateInstance((name, type) => null, 1, out var instance); 50 | 51 | // Act Assert 52 | _ = Assert.ThrowsExactly(Action); 53 | } 54 | 55 | private sealed class TestClass 56 | { 57 | public TestClass() 58 | => Value = 99; 59 | 60 | public TestClass(int value) 61 | => Value = value; 62 | 63 | public int Value 64 | { 65 | get; 66 | set; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/PlcInterface.Abstraction/PlcInterface.Abstraction.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/PlcInterface.Abstraction/PropertySetterHelperTests.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Moq; 3 | 4 | namespace PlcInterface.Abstraction.Tests; 5 | 6 | [TestClass] 7 | public class PropertySetterHelperTests 8 | { 9 | [TestMethod] 10 | public void WhenDeclaringTypeIsNullThrowsNotSupportedException() 11 | { 12 | // Arrange 13 | var constructor = Mock.Of(x => x.DeclaringType == null); 14 | 15 | // Act Assert 16 | _ = Assert.ThrowsExactly(() => 17 | { 18 | var propertySetterHelper = new PropertySetterHelper(constructor); 19 | var type = propertySetterHelper.GetType(); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/PlcInterface.Abstraction/StructActivatorTests.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.Abstraction.Tests; 2 | 3 | [TestClass] 4 | public class StructActivatorTests 5 | { 6 | [TestMethod] 7 | public void FailsToCreateTypeWhenThereAreNotEnoughProperties() 8 | { 9 | // Arrange 10 | var activator = new StructActivator(typeof(TestValueType)); 11 | 12 | // Act 13 | var result = activator.TryCreateInstance((name, type) => null, 10, out var instance); 14 | 15 | // Assert 16 | Assert.IsFalse(result); 17 | Assert.IsNull(instance); 18 | } 19 | 20 | [TestMethod] 21 | public void NullValueThrowsSymbolException() 22 | { 23 | // Arrange 24 | var activator = new StructActivator(typeof(TestValueType)); 25 | void Action() => _ = activator.TryCreateInstance((name, type) => null, 1, out var instance); 26 | 27 | // Act Assert 28 | _ = Assert.ThrowsExactly(Action); 29 | } 30 | 31 | private struct TestValueType 32 | { 33 | public int Value 34 | { 35 | get; 36 | set; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/PlcInterface.Abstraction/TypeConverterMock.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.Abstraction.Tests; 2 | 3 | internal sealed class TypeConverterMock : TypeConverter 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.IntegrationTests/Assembly.cs: -------------------------------------------------------------------------------- 1 | [assembly: TestCategory("Integration")] 2 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.IntegrationTests/DummyTest.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.Ads.IntegrationTests; 2 | 3 | [TestClass] 4 | public class DummyTest 5 | { 6 | [TestMethod] 7 | [Description("This is an always passing test to make sure at least 1 test succeed")] 8 | public void FilterBypassTest() => Thread.Sleep(100); 9 | } 10 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.IntegrationTests/MonitorTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using PlcInterface.IntegrationTests; 6 | 7 | namespace PlcInterface.Ads.IntegrationTests; 8 | 9 | [TestClass] 10 | [CICondition(ConditionMode.Exclude)] 11 | public class MonitorTest : IMonitorTestBase 12 | { 13 | protected override ServiceProvider GetServiceProvider() 14 | { 15 | var services = new ServiceCollection() 16 | .AddAdsPLC() 17 | .Configure(o => 18 | { 19 | o.AmsNetId = Settings.AmsNetId; 20 | o.Port = Settings.Port; 21 | }) 22 | .Configure(o => 23 | { 24 | o.StoreSymbolsToDisk = false; 25 | o.RootVariable = Settings.RootVariable; 26 | }); 27 | 28 | services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(NullLogger<>))); 29 | 30 | return services.BuildServiceProvider(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.IntegrationTests/PlcConnectionTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using PlcInterface.IntegrationTests; 6 | 7 | namespace PlcInterface.Ads.IntegrationTests; 8 | 9 | [TestClass] 10 | [CICondition(ConditionMode.Exclude)] 11 | public sealed class PlcConnectionTest : IPlcConnectionTestBase 12 | { 13 | [TestMethod] 14 | public void GetConnectedClient() 15 | { 16 | // Arrange 17 | using var serviceProvider = GetServiceProvider(); 18 | var connection = serviceProvider.GetRequiredService(); 19 | 20 | // Act 21 | _ = Assert.ThrowsExactly(() => connection.GetConnectedClient()); 22 | } 23 | 24 | [TestMethod] 25 | public void GetConnectedClientReturnsTheActiveConnection() 26 | { 27 | // Arrange 28 | using var serviceProvider = GetServiceProvider(); 29 | var connection = serviceProvider.GetRequiredService(); 30 | 31 | // Act 32 | var connected = connection.Connect(); 33 | Assert.IsTrue(connected, "Plc could not connect"); 34 | var client = connection.GetConnectedClient(TimeSpan.FromSeconds(2)); 35 | 36 | // Assert 37 | Assert.IsNotNull(client); 38 | Assert.IsTrue(client.IsConnected); 39 | } 40 | 41 | [TestMethod] 42 | public async Task GetConnectedClientReturnsTheActiveConnectionAsync() 43 | { 44 | // Arrange 45 | using var serviceProvider = GetServiceProvider(); 46 | var connection = serviceProvider.GetRequiredService(); 47 | 48 | // Act 49 | var connected = await connection.ConnectAsync(); 50 | Assert.IsTrue(connected, "Plc could not connect"); 51 | var client = await connection.GetConnectedClientAsync(TimeSpan.FromSeconds(2)); 52 | 53 | // Assert 54 | Assert.IsNotNull(client); 55 | Assert.IsTrue(client.IsConnected); 56 | } 57 | 58 | protected override ServiceProvider GetServiceProvider() 59 | { 60 | var services = new ServiceCollection() 61 | .AddAdsPLC() 62 | .Configure(o => 63 | { 64 | o.AmsNetId = Settings.AmsNetId; 65 | o.Port = Settings.Port; 66 | }) 67 | .Configure(o => o.StoreSymbolsToDisk = false); 68 | 69 | services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(NullLogger<>))); 70 | 71 | return services.BuildServiceProvider(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.IntegrationTests/PlcInterface.Ads.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.IntegrationTests/ReadValueTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using PlcInterface.IntegrationTests; 6 | 7 | namespace PlcInterface.Ads.IntegrationTests; 8 | 9 | [TestClass] 10 | [CICondition(ConditionMode.Exclude)] 11 | public sealed class ReadValueTest : IReadValueTestBase 12 | { 13 | protected override ServiceProvider GetServiceProvider() 14 | { 15 | var services = new ServiceCollection() 16 | .AddAdsPLC() 17 | .Configure(o => 18 | { 19 | o.AmsNetId = Settings.AmsNetId; 20 | o.Port = Settings.Port; 21 | }) 22 | .Configure(o => 23 | { 24 | o.StoreSymbolsToDisk = false; 25 | o.RootVariable = Settings.RootVariable; 26 | }); 27 | 28 | services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(NullLogger<>))); 29 | 30 | return services.BuildServiceProvider(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.IntegrationTests/Settings.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace PlcInterface.Ads.IntegrationTests; 4 | 5 | internal static class Settings 6 | { 7 | public static string AmsNetId 8 | => "172.99.0.2.1.1"; 9 | 10 | public static int Port 11 | => 851; 12 | 13 | public static string RootVariable 14 | => $"AdsNet{Environment.Version.Major.ToString(CultureInfo.InvariantCulture)}"; 15 | } 16 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.IntegrationTests/SymbolHandlerTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using PlcInterface.IntegrationTests; 6 | 7 | namespace PlcInterface.Ads.IntegrationTests; 8 | 9 | [TestClass] 10 | [CICondition(ConditionMode.Exclude)] 11 | public class SymbolHandlerTest : ISymbolHandlerTestBase 12 | { 13 | protected override ServiceProvider GetServiceProvider() 14 | { 15 | var services = new ServiceCollection() 16 | .AddAdsPLC() 17 | .Configure(o => 18 | { 19 | o.AmsNetId = Settings.AmsNetId; 20 | o.Port = Settings.Port; 21 | }) 22 | .Configure(o => 23 | { 24 | o.StoreSymbolsToDisk = false; 25 | o.RootVariable = Settings.RootVariable; 26 | }); 27 | 28 | services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(NullLogger<>))); 29 | 30 | return services.BuildServiceProvider(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.IntegrationTests/WriteValueTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using PlcInterface.IntegrationTests; 6 | 7 | namespace PlcInterface.Ads.IntegrationTests; 8 | 9 | [TestClass] 10 | [CICondition(ConditionMode.Exclude)] 11 | public class WriteValueTest : IWriteValueTestBase 12 | { 13 | protected override ServiceProvider GetServiceProvider() 14 | { 15 | var services = new ServiceCollection() 16 | .AddAdsPLC() 17 | .Configure(o => 18 | { 19 | o.AmsNetId = Settings.AmsNetId; 20 | o.Port = Settings.Port; 21 | }) 22 | .Configure(o => 23 | { 24 | o.StoreSymbolsToDisk = false; 25 | o.RootVariable = Settings.RootVariable; 26 | }); 27 | 28 | services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(NullLogger<>))); 29 | 30 | return services.BuildServiceProvider(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.PLC/PLC_Main/DUTs/DUT_TestStruct.TcDUT: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 49 | 50 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.PLC/PLC_Main/DUTs/DUT_TestStruct2.TcDUT: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14 | 15 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.PLC/PLC_Main/DUTs/MonitorTest.TcDUT: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.PLC/PLC_Main/DUTs/MonitorTestData.TcDUT: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 39 | 40 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.PLC/PLC_Main/DUTs/ReadTest.TcDUT: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 27 | 28 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.PLC/PLC_Main/DUTs/ReadTestData.TcDUT: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 47 | 48 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.PLC/PLC_Main/DUTs/SymbolTest.TcDUT: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 12 | 13 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.PLC/PLC_Main/DUTs/TestEnum.TcDUT: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14 | 15 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.PLC/PLC_Main/DUTs/WriteTest.TcDUT: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 19 | 20 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.PLC/PLC_Main/GVLs/AdsNet8.TcGVL: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 12 | 13 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.PLC/PLC_Main/GVLs/AdsNet9.TcGVL: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 12 | 13 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.PLC/PLC_Main/GVLs/OpcNet8.TcGVL: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.PLC/PLC_Main/GVLs/OpcNet9.TcGVL: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.PLC/PLC_Main/PLC_Main.noprjfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vectron/PlcInterface/2477d92829e129e11920efbd4db7f99c9ccb5f0a/test/PlcInterface.Ads.PLC/PLC_Main/PLC_Main.noprjfile -------------------------------------------------------------------------------- /test/PlcInterface.Ads.PLC/PLC_Main/POUs/MAIN.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 6 | 7 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.PLC/PLC_Main/PlcTask.TcTTO: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 10000 6 | 20 7 | 8 | MAIN 9 | 10 | {b3ae918d-2258-40af-825c-ee5e450cfd79} 11 | {13957748-6ab2-42a0-a6e8-4ff0fbd6f580} 12 | {edeb04a0-b97d-4a99-a0b3-13dbf2b26abe} 13 | {b05b4670-d768-4280-9842-da090e464cf8} 14 | {b23abefb-f957-4a94-9c47-f7c151069cef} 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.PLC/PlcInterface.Ads.PLC.tsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {8161723B-0C2E-405C-B829-EDC5DAAB5104} 8 | {BDCC0070-42D5-49AE-ABF1-1D4434813D60} 9 | 10 | 11 | 12 | 13 | PlcTask 14 | 15 | 16 | 17 | 18 | 19 | 20 | PLC_Main Instance 21 | {08500001-0000-0000-F000-000000000064} 22 | 23 | 24 | 0 25 | PlcTask 26 | 27 | #x02010030 28 | 29 | 20 30 | 10000000 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.Tests/Assembly.cs: -------------------------------------------------------------------------------- 1 | [assembly: Parallelize(Workers = 0, Scope = ExecutionScope.MethodLevel)] 2 | [assembly: TestCategory("Unit")] 3 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.Tests/ISymbolInfoExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using TwinCAT.TypeSystem; 3 | 4 | namespace PlcInterface.Ads.Tests; 5 | 6 | [TestClass] 7 | public class ISymbolInfoExtensionTests 8 | { 9 | [TestMethod] 10 | public void CastAndValidateReturnsSymbolInfo() 11 | { 12 | // Arrange 13 | ISymbolInfo symbolInfo = new SymbolInfo(Mock.Of(), string.Empty); 14 | 15 | // Act 16 | var actual = symbolInfo.CastAndValidate(); 17 | 18 | // Assert 19 | Assert.IsInstanceOfType(actual); 20 | Assert.IsInstanceOfType(actual); 21 | Assert.AreSame(symbolInfo, actual); 22 | } 23 | 24 | [TestMethod] 25 | public void CastAndValidateThrowsSymbolExceptionWhenNotAdsSymbol() 26 | { 27 | // Arrange 28 | var symbolMock = Mock.Of(); 29 | 30 | // Act 31 | // Assert 32 | _ = Assert.ThrowsExactly(() => symbolMock.CastAndValidate()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.Tests/IValueSymbolExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using TwinCAT.TypeSystem; 3 | 4 | namespace PlcInterface.Ads.Tests; 5 | 6 | [TestClass] 7 | public class IValueSymbolExtensionsTests 8 | { 9 | [TestMethod] 10 | public void CastAndValidateReturnsSymbolInfo() 11 | { 12 | // Arrange 13 | var symbolMock = new Mock(); 14 | _ = symbolMock.As(); 15 | 16 | // Act 17 | var actual = symbolMock.Object.CastAndValidate(); 18 | 19 | // Assert 20 | Assert.IsInstanceOfType(actual); 21 | Assert.AreSame(symbolMock.Object, actual); 22 | } 23 | 24 | [TestMethod] 25 | public void CastAndValidateThrowsSymbolExceptionWhenNotAdsSymbol() 26 | { 27 | // Arrange 28 | var symbolMock = Mock.Of(); 29 | 30 | // Act 31 | // Assert 32 | _ = Assert.ThrowsExactly(() => symbolMock.CastAndValidate()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.Tests/PlcInterface.Ads.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/PlcInterface.Ads.Tests/TcAdsClientExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using TwinCAT.Ads; 3 | 4 | namespace PlcInterface.Ads.Tests; 5 | 6 | [TestClass] 7 | public class TcAdsClientExtensionTests 8 | { 9 | [TestMethod] 10 | public void ValidateConnectionReturnsTheConnectionWhenSuccess() 11 | { 12 | // Arrange 13 | var stateInfo = new StateInfo(AdsState.Run, 0); 14 | var clientMock = new Mock(); 15 | _ = clientMock.SetupGet(x => x.IsConnected).Returns(value: true); 16 | _ = clientMock.Setup(x => x.TryReadState(out stateInfo)).Returns(AdsErrorCode.NoError); 17 | 18 | // Act 19 | var client = clientMock.Object.ValidateConnection(); 20 | 21 | // Assert 22 | Assert.AreEqual(clientMock.Object, client); 23 | } 24 | 25 | [TestMethod] 26 | public void ValidateConnectionThrowsArgumentNullExceptionWhenPassedANull() => 27 | _ = Assert.ThrowsExactly(() => TcAdsClientExtension.ValidateConnection(null!)); 28 | 29 | [TestMethod] 30 | public void ValidateConnectionThrowsInvalidOperationExceptionWhenItCantReadState() 31 | { 32 | // Arrange 33 | var stateInfo = default(StateInfo); 34 | var clientMock = new Mock(); 35 | _ = clientMock.SetupGet(x => x.IsConnected).Returns(value: true); 36 | _ = clientMock.Setup(x => x.TryReadState(out stateInfo)).Returns(AdsErrorCode.InternalError); 37 | 38 | // Act 39 | _ = Assert.ThrowsExactly(() => clientMock.Object.ValidateConnection()); 40 | 41 | // Assert 42 | } 43 | 44 | [TestMethod] 45 | public void ValidateConnectionThrowsInvalidOperationExceptionWhenNotConnected() 46 | { 47 | // Arrange 48 | var stateInfo = default(StateInfo); 49 | var clientMock = new Mock(); 50 | _ = clientMock.SetupGet(x => x.IsConnected).Returns(value: false); 51 | _ = clientMock.Setup(x => x.TryReadState(out stateInfo)).Returns(AdsErrorCode.InternalError); 52 | 53 | // Act 54 | _ = Assert.ThrowsExactly(() => clientMock.Object.ValidateConnection()); 55 | 56 | // Assert 57 | } 58 | 59 | [TestMethod] 60 | public void ValidateConnectionThrowsInvalidOperationExceptionWhenStateIsNotAdsStateRun() 61 | { 62 | // Arrange 63 | var stateInfo = new StateInfo(AdsState.Stop, 0); 64 | var clientMock = new Mock(); 65 | _ = clientMock.SetupGet(x => x.IsConnected).Returns(value: true); 66 | _ = clientMock.Setup(x => x.TryReadState(out stateInfo)).Returns(AdsErrorCode.NoError); 67 | 68 | // Act 69 | _ = Assert.ThrowsExactly(() => clientMock.Object.ValidateConnection()); 70 | 71 | // Assert 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/PlcInterface.Common.Tests/IObservableExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Subjects; 2 | using Moq; 3 | 4 | namespace PlcInterface.Common.Tests; 5 | 6 | [TestClass] 7 | public class IObservableExtensionsTests 8 | { 9 | [TestMethod] 10 | public void WhereNotNullOnlyReturnsItemsThatAreNotNull() 11 | { 12 | // Arrange 13 | using var subject = new Subject(); 14 | var observerMock = new Mock>(); 15 | 16 | // Act 17 | using var subscription = subject.WhereNotNull().Subscribe(observerMock.Object); 18 | subject.OnNext(value: null); 19 | subject.OnNext(value: null); 20 | subject.OnNext(value: true); 21 | subject.OnNext(value: null); 22 | subject.OnNext(value: null); 23 | subject.OnNext(value: null); 24 | subject.OnNext(value: null); 25 | 26 | // Assert 27 | observerMock.Verify(x => x.OnNext(It.IsNotNull()), Times.Once); 28 | } 29 | 30 | [TestMethod] 31 | public void WhereNotNullPassesOnCompletedThrough() 32 | { 33 | // Arrange 34 | using var subject = new Subject(); 35 | var observerMock = new Mock>(); 36 | var expectedException = new InvalidOperationException(); 37 | 38 | // Act 39 | using var subscription = subject.WhereNotNull().Subscribe(observerMock.Object); 40 | subject.OnNext(value: null); 41 | subject.OnNext(value: true); 42 | subject.OnNext(value: null); 43 | subject.OnError(expectedException); 44 | 45 | // Assert 46 | observerMock.Verify(x => x.OnError(It.Is(x => x == expectedException)), Times.Once); 47 | } 48 | 49 | [TestMethod] 50 | public void WhereNotNullPassesOnErrorsThrough() 51 | { 52 | // Arrange 53 | using var subject = new Subject(); 54 | var observerMock = new Mock>(); 55 | 56 | // Act 57 | using var subscription = subject.WhereNotNull().Subscribe(observerMock.Object); 58 | subject.OnNext(value: null); 59 | subject.OnNext(value: true); 60 | subject.OnNext(value: null); 61 | subject.OnCompleted(); 62 | 63 | // Assert 64 | observerMock.Verify(x => x.OnCompleted(), Times.Once); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/PlcInterface.Common.Tests/IServiceCollectionExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Moq; 3 | 4 | namespace PlcInterface.Common.Tests; 5 | 6 | [TestClass] 7 | public class IServiceCollectionExtensionTests 8 | { 9 | private interface IDummyInterface : IDummyInterface2 10 | { 11 | } 12 | 13 | private interface IDummyInterface2 14 | { 15 | } 16 | 17 | [TestMethod] 18 | public void AddSingletonFactoryAddsTheTypesAsSingletons() 19 | { 20 | // Arrange 21 | var provider = new Mock(); 22 | 23 | // Act 24 | _ = provider.Object.AddSingletonFactory(); 25 | 26 | // Assert 27 | provider.Verify( 28 | x => x.Add(It.Is(x => 29 | x.Lifetime == ServiceLifetime.Singleton 30 | && ((x.ImplementationType == typeof(DummyType) && x.ServiceType == typeof(IDummyInterface)) 31 | || (x.ImplementationType == null && x.ServiceType == typeof(IDummyInterface2))))), 32 | Times.Exactly(2)); 33 | } 34 | 35 | private sealed class DummyType : IDummyInterface 36 | { 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/PlcInterface.Common.Tests/IndicesHelperTests.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.Common.Tests; 2 | 3 | [TestClass] 4 | public class IndicesHelperTests 5 | { 6 | private static int[,,] Data 7 | => new int[,,] 8 | { 9 | { 10 | { 00, 01, 02 }, 11 | { 03, 04, 05 }, 12 | { 06, 07, 08 }, 13 | }, 14 | { 15 | { 10, 11, 12 }, 16 | { 13, 14, 15 }, 17 | { 16, 17, 18 }, 18 | }, 19 | { 20 | { 20, 21, 22 }, 21 | { 23, 24, 25 }, 22 | { 26, 27, 28 }, 23 | }, 24 | }; 25 | 26 | [TestMethod] 27 | public void GetIndicesReturnsTheValueFromString() 28 | { 29 | // Arrange 30 | var name = "ArrayObject[5,3]"; 31 | var expected = new[] { 5, 3 }; 32 | 33 | // Act 34 | var indices1 = IndicesHelper.GetIndices(name); 35 | var indices2 = IndicesHelper.GetIndices(name.AsSpan()); 36 | 37 | // Assert 38 | CollectionAssert.AreEqual(expected, indices1); 39 | CollectionAssert.AreEqual(indices1, indices2); 40 | } 41 | 42 | [TestMethod] 43 | public void IndicesOnlyCreatesOneArray() 44 | { 45 | // Arrange 46 | var data = Data; 47 | Array? first = null; 48 | 49 | // Act Assert 50 | foreach (var indices in IndicesHelper.GetIndices(data)) 51 | { 52 | first ??= indices; 53 | Assert.AreSame(first, indices); 54 | } 55 | } 56 | 57 | [TestMethod] 58 | public void IndicesReturnsAllArrayDimensions() 59 | { 60 | // Arrange 61 | var data = Data; 62 | var expected = new int[][] 63 | { 64 | [0, 0, 0], 65 | [0, 0, 1], 66 | [0, 0, 2], 67 | [0, 1, 0], 68 | [0, 1, 1], 69 | [0, 1, 2], 70 | [0, 2, 0], 71 | [0, 2, 1], 72 | [0, 2, 2], 73 | [1, 0, 0], 74 | [1, 0, 1], 75 | [1, 0, 2], 76 | [1, 1, 0], 77 | [1, 1, 1], 78 | [1, 1, 2], 79 | [1, 2, 0], 80 | [1, 2, 1], 81 | [1, 2, 2], 82 | [2, 0, 0], 83 | [2, 0, 1], 84 | [2, 0, 2], 85 | [2, 1, 0], 86 | [2, 1, 1], 87 | [2, 1, 2], 88 | [2, 2, 0], 89 | [2, 2, 1], 90 | [2, 2, 2], 91 | }; 92 | using var expectedEnumerator = expected.AsEnumerable().GetEnumerator(); 93 | 94 | // Act 95 | var actual = IndicesHelper.GetIndices(data); 96 | using var actualEnumerator = actual.GetEnumerator(); 97 | 98 | // Assert 99 | while (actualEnumerator.MoveNext() && expectedEnumerator.MoveNext()) 100 | { 101 | CollectionAssert.AreEqual(expectedEnumerator.Current, actualEnumerator.Current); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /test/PlcInterface.Common.Tests/PlcInterface.Common.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/PlcInterface.Common.Tests/TaskExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Moq; 3 | 4 | namespace PlcInterface.Common.Tests; 5 | 6 | [TestClass] 7 | public class TaskExtensionsTests 8 | { 9 | [TestMethod] 10 | public void LogExceptionsAsyncLogsExceptionsToErrorStream() 11 | { 12 | // Arrange 13 | var loggerMock = new Mock(); 14 | _ = loggerMock 15 | .Setup(x => x.IsEnabled(It.IsAny())) 16 | .Returns(value: true); 17 | 18 | // Act 19 | var task2 = Task.Run(() => throw new NotSupportedException()).LogExceptionsAsync(loggerMock.Object); 20 | task2.Wait(); 21 | 22 | // Assert 23 | loggerMock.Verify( 24 | x => x.Log( 25 | It.Is(x => x == LogLevel.Error), 26 | It.IsAny(), 27 | It.IsAny(), 28 | It.IsAny(), 29 | It.IsAny>()), 30 | Times.Once); 31 | } 32 | 33 | [TestMethod] 34 | public void LogExceptionsAsyncThrowsArgumentNullExceptionWhenArgumentsAreNull() 35 | { 36 | // Arrange 37 | var loggerMock = new Mock(); 38 | var task = new Task(() => { }); 39 | 40 | // Act 41 | // Assert 42 | _ = Assert.ThrowsExactlyAsync(() => TaskExtensions.LogExceptionsAsync(null!, loggerMock.Object)); 43 | _ = Assert.ThrowsExactlyAsync(() => task.LogExceptionsAsync(null!)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/PlcInterface.IntegrationTests/DataTypes/DUT_TestClass2.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace PlcInterface.IntegrationTests.DataTypes; 4 | 5 | [StructLayout(LayoutKind.Sequential, Pack = 0)] 6 | internal sealed class DUT_TestClass2 7 | { 8 | public static DUT_TestClass2 Default => new() 9 | { 10 | ByteValue = byte.MaxValue, 11 | WordValue = ushort.MaxValue, 12 | DWordValue = uint.MaxValue, 13 | LWordValue = ulong.MaxValue, 14 | }; 15 | 16 | public static DUT_TestClass2 Write => new() 17 | { 18 | ByteValue = byte.MinValue, 19 | WordValue = ushort.MinValue, 20 | DWordValue = uint.MinValue, 21 | LWordValue = ulong.MinValue, 22 | }; 23 | 24 | public byte ByteValue 25 | { 26 | get; set; 27 | } 28 | 29 | public ushort WordValue 30 | { 31 | get; set; 32 | } 33 | 34 | public uint DWordValue 35 | { 36 | get; set; 37 | } 38 | 39 | public ulong LWordValue 40 | { 41 | get; set; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/PlcInterface.IntegrationTests/DataTypes/DUT_TestStruct2.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace PlcInterface.IntegrationTests.DataTypes; 4 | 5 | [StructLayout(LayoutKind.Sequential, Pack = 0)] 6 | internal struct DUT_TestStruct2 7 | { 8 | public static DUT_TestStruct2 Default => new() 9 | { 10 | ByteValue = byte.MaxValue, 11 | WordValue = ushort.MaxValue, 12 | DWordValue = uint.MaxValue, 13 | LWordValue = ulong.MaxValue, 14 | }; 15 | 16 | public static DUT_TestStruct2 Write => new() 17 | { 18 | ByteValue = byte.MinValue, 19 | WordValue = ushort.MinValue, 20 | DWordValue = uint.MinValue, 21 | LWordValue = ulong.MinValue, 22 | }; 23 | 24 | public byte ByteValue 25 | { 26 | get; set; 27 | } 28 | 29 | public ushort WordValue 30 | { 31 | get; set; 32 | } 33 | 34 | public uint DWordValue 35 | { 36 | get; set; 37 | } 38 | 39 | public ulong LWordValue 40 | { 41 | get; set; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/PlcInterface.IntegrationTests/DataTypes/TestEnum.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.IntegrationTests.DataTypes; 2 | 3 | internal enum TestEnum 4 | { 5 | First = 0, 6 | Second = 1, 7 | Third = 2, 8 | } 9 | -------------------------------------------------------------------------------- /test/PlcInterface.IntegrationTests/Extension/MethodInfoExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Reflection; 3 | using System.Runtime.ExceptionServices; 4 | 5 | namespace PlcInterface.IntegrationTests.Extension; 6 | 7 | internal static class MethodInfoExtensions 8 | { 9 | [ExcludeFromCodeCoverage] 10 | public static Task InvokeAsyncUnwrappedException(this MethodInfo memberInfo, object obj, object[] parameters) 11 | { 12 | try 13 | { 14 | return memberInfo.Invoke(obj, parameters) is not Task result 15 | ? Task.CompletedTask 16 | : result; 17 | } 18 | catch (TargetInvocationException ex) 19 | { 20 | ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw(); 21 | throw; 22 | } 23 | } 24 | 25 | [ExcludeFromCodeCoverage] 26 | public static object? InvokeUnwrappedException(this MethodInfo memberInfo, object obj, object[] parameters) 27 | { 28 | try 29 | { 30 | return memberInfo.Invoke(obj, parameters); 31 | } 32 | catch (TargetInvocationException ex) 33 | { 34 | ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw(); 35 | throw; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/PlcInterface.IntegrationTests/IPlcConnectionTestBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace PlcInterface.IntegrationTests; 5 | 6 | public abstract class IPlcConnectionTestBase 7 | { 8 | [TestMethod] 9 | public void OpenCloseConnection() 10 | { 11 | // Arrange 12 | var serviceProvider = GetServiceProvider(); 13 | using var disposable = serviceProvider as IDisposable; 14 | var connection = serviceProvider.GetRequiredService(); 15 | 16 | // Act 17 | var connected = connection.Connect(); 18 | 19 | // Assert 20 | Assert.IsTrue(connected, "Plc could not connect"); 21 | Assert.IsTrue(connection.IsConnected); 22 | 23 | // Act 24 | connection.Disconnect(); 25 | 26 | // Assert 27 | Assert.IsFalse(connection.IsConnected); 28 | connection.Disconnect(); 29 | } 30 | 31 | [TestMethod] 32 | public async Task OpenCloseConnectionAsync() 33 | { 34 | // Arrange 35 | var serviceProvider = GetServiceProvider(); 36 | using var disposable = serviceProvider as IDisposable; 37 | var connection = serviceProvider.GetRequiredService(); 38 | 39 | // Act 40 | var connected = await connection.ConnectAsync(); 41 | 42 | // Assert 43 | Assert.IsTrue(connected, "Plc could not connect"); 44 | Assert.IsTrue(connection.IsConnected); 45 | 46 | // Act 47 | await connection.DisconnectAsync(); 48 | 49 | // Assert 50 | Assert.IsFalse(connection.IsConnected); 51 | await connection.DisconnectAsync(); 52 | } 53 | 54 | protected abstract IServiceProvider GetServiceProvider(); 55 | } 56 | -------------------------------------------------------------------------------- /test/PlcInterface.IntegrationTests/MultiAssert.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace PlcInterface.IntegrationTests; 5 | 6 | [ExcludeFromCodeCoverage] 7 | public class MultiAssert 8 | { 9 | private readonly List exceptions = []; 10 | 11 | public static void Aggregate(params Action[] actions) 12 | { 13 | var multiAssert = new MultiAssert(); 14 | 15 | foreach (var action in actions) 16 | { 17 | multiAssert.Check(action); 18 | } 19 | 20 | multiAssert.Assert(); 21 | } 22 | 23 | public void Assert() 24 | { 25 | var assertionTexts = exceptions.Select(assertFailedException => assertFailedException.Message); 26 | if (assertionTexts.Any()) 27 | { 28 | throw new AssertFailedException(assertionTexts.Aggregate((aggregatedMessage, next) => aggregatedMessage + Environment.NewLine + next)); 29 | } 30 | } 31 | 32 | public void Check(Action action) 33 | { 34 | try 35 | { 36 | action(); 37 | } 38 | catch (AssertFailedException ex) 39 | { 40 | exceptions.Add(ex); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/PlcInterface.IntegrationTests/PlcInterface.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/PlcInterface.Opc.IntegrationTests/Assembly.cs: -------------------------------------------------------------------------------- 1 | [assembly: TestCategory("Integration")] 2 | -------------------------------------------------------------------------------- /test/PlcInterface.Opc.IntegrationTests/DummyTest.cs: -------------------------------------------------------------------------------- 1 | namespace PlcInterface.OPC.IntegrationTests; 2 | 3 | [TestClass] 4 | public class DummyTest 5 | { 6 | [TestMethod] 7 | [Description("This is an always passing test to make sure at least 1 test succeed")] 8 | public void FilterBypassTest() => Thread.Sleep(100); 9 | } 10 | -------------------------------------------------------------------------------- /test/PlcInterface.Opc.IntegrationTests/MonitorTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using PlcInterface.IntegrationTests; 6 | using PlcInterface.OpcUa; 7 | 8 | namespace PlcInterface.Opc.IntegrationTests; 9 | 10 | [TestClass] 11 | [CICondition(ConditionMode.Exclude)] 12 | public class MonitorTest : IMonitorTestBase 13 | { 14 | protected override ServiceProvider GetServiceProvider() 15 | { 16 | var services = new ServiceCollection() 17 | .AddOpcPLC() 18 | .Configure(o => 19 | { 20 | o.Address = Settings.OpcIp; 21 | o.Port = Settings.OpcPort; 22 | }) 23 | .Configure(o => o.RootVariable = Settings.RootVariable); 24 | 25 | services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(NullLogger<>))); 26 | 27 | return services.BuildServiceProvider(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/PlcInterface.Opc.IntegrationTests/PlcConnectionTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using PlcInterface.IntegrationTests; 6 | using PlcInterface.OpcUa; 7 | 8 | namespace PlcInterface.Opc.IntegrationTests; 9 | 10 | [TestClass] 11 | [CICondition(ConditionMode.Exclude)] 12 | public sealed class PlcConnectionTest : IPlcConnectionTestBase 13 | { 14 | [TestMethod] 15 | public void GetConnectedClient() 16 | { 17 | // Arrange 18 | using var serviceProvider = GetServiceProvider(); 19 | var connection = serviceProvider.GetRequiredService(); 20 | 21 | // Act Assert 22 | _ = Assert.ThrowsExactly(() => connection.GetConnectedClient()); 23 | } 24 | 25 | [TestMethod] 26 | public void GetConnectedClientReturnsTheActiveConnection() 27 | { 28 | // Arrange 29 | using var serviceProvider = GetServiceProvider(); 30 | var connection = serviceProvider.GetRequiredService(); 31 | 32 | // Act 33 | var connected = connection.Connect(); 34 | Assert.IsTrue(connected, "Plc could not connect"); 35 | var client = connection.GetConnectedClient(TimeSpan.FromSeconds(2)); 36 | 37 | // Assert 38 | Assert.IsNotNull(client); 39 | Assert.IsTrue(client.Connected); 40 | } 41 | 42 | [TestMethod] 43 | public async Task GetConnectedClientReturnsTheActiveConnectionAsync() 44 | { 45 | // Arrange 46 | using var serviceProvider = GetServiceProvider(); 47 | var connection = serviceProvider.GetRequiredService(); 48 | 49 | // Act 50 | var connected = await connection.ConnectAsync(); 51 | Assert.IsTrue(connected, "Plc could not connect"); 52 | var client = await connection.GetConnectedClientAsync(TimeSpan.FromSeconds(2)); 53 | 54 | // Assert 55 | Assert.IsNotNull(client); 56 | Assert.IsTrue(client.Connected); 57 | } 58 | 59 | protected override ServiceProvider GetServiceProvider() 60 | { 61 | var services = new ServiceCollection() 62 | .AddOpcPLC() 63 | .Configure(o => 64 | { 65 | o.Address = Settings.OpcIp; 66 | o.Port = Settings.OpcPort; 67 | }) 68 | .Configure(o => o.RootVariable = Settings.RootVariable); 69 | 70 | services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(NullLogger<>))); 71 | 72 | return services.BuildServiceProvider(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/PlcInterface.Opc.IntegrationTests/PlcInterface.Opc.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/PlcInterface.Opc.IntegrationTests/ReadValueTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using PlcInterface.IntegrationTests; 6 | using PlcInterface.OpcUa; 7 | 8 | namespace PlcInterface.Opc.IntegrationTests; 9 | 10 | [TestClass] 11 | [CICondition(ConditionMode.Exclude)] 12 | public sealed class ReadValueTest : IReadValueTestBase 13 | { 14 | protected override ServiceProvider GetServiceProvider() 15 | { 16 | var services = new ServiceCollection() 17 | .AddOpcPLC() 18 | .Configure(o => 19 | { 20 | o.Address = Settings.OpcIp; 21 | o.Port = Settings.OpcPort; 22 | }) 23 | .Configure(o => o.RootVariable = Settings.RootVariable); 24 | 25 | services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(NullLogger<>))); 26 | 27 | return services.BuildServiceProvider(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/PlcInterface.Opc.IntegrationTests/Settings.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace PlcInterface.Opc.IntegrationTests; 4 | 5 | internal static class Settings 6 | { 7 | public static string OpcIp 8 | => "172.30.70.5"; 9 | 10 | public static int OpcPort 11 | => 4840; 12 | 13 | public static string RootVariable 14 | => $"PLC1.OpcNet{Environment.Version.Major.ToString(CultureInfo.InvariantCulture)}"; 15 | } 16 | -------------------------------------------------------------------------------- /test/PlcInterface.Opc.IntegrationTests/SymbolHandlerTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using PlcInterface.IntegrationTests; 6 | using PlcInterface.OpcUa; 7 | 8 | namespace PlcInterface.Opc.IntegrationTests; 9 | 10 | [TestClass] 11 | [CICondition(ConditionMode.Exclude)] 12 | public class SymbolHandlerTest : ISymbolHandlerTestBase 13 | { 14 | protected override ServiceProvider GetServiceProvider() 15 | { 16 | var services = new ServiceCollection() 17 | .AddOpcPLC() 18 | .Configure(o => 19 | { 20 | o.Address = Settings.OpcIp; 21 | o.Port = Settings.OpcPort; 22 | }) 23 | .Configure(o => o.RootVariable = Settings.RootVariable); 24 | 25 | services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(NullLogger<>))); 26 | 27 | return services.BuildServiceProvider(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/PlcInterface.Opc.IntegrationTests/WriteValueTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using PlcInterface.IntegrationTests; 6 | using PlcInterface.OpcUa; 7 | 8 | namespace PlcInterface.Opc.IntegrationTests; 9 | 10 | [TestClass] 11 | [CICondition(ConditionMode.Exclude)] 12 | public class WriteValueTest : IWriteValueTestBase 13 | { 14 | protected override ServiceProvider GetServiceProvider() 15 | { 16 | var services = new ServiceCollection() 17 | .AddOpcPLC() 18 | .Configure(o => 19 | { 20 | o.Address = Settings.OpcIp; 21 | o.Port = Settings.OpcPort; 22 | }) 23 | .Configure(o => o.RootVariable = Settings.RootVariable); 24 | 25 | services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(NullLogger<>))); 26 | 27 | return services.BuildServiceProvider(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/PlcInterface.OpcUa.OpcServer/OPCServer.tcopcuasrv: -------------------------------------------------------------------------------- 1 | 2 | 3 | opc.tcp://192.168.17.211:4840 4 | opc.tcp://192.168.17.211:4840 [Aes128_Sha256_RsaOaep, SignAndEncrypt] 5 | 6 | 4840 7 | false 8 | true 9 | true 10 | -------------------------------------------------------------------------------- /test/PlcInterface.OpcUa.OpcServer/OPCServer/Alarms and Conditions/Alarms and Conditions.ac: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/PlcInterface.OpcUa.OpcServer/OPCServer/Data Access/Data Access.opcuada: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16384 4 | 5 | 6 | PLC1 7 | 851 8 | 127.0.0.1.1.1 9 | 2000 10 | 20000 11 | 1 12 | 4041 13 | [BootDir]\Plc\Port_851.tmc 14 | 0 15 | 0 16 | 1 17 | 0 18 | 2 19 | false 20 | no_cache 21 | 22 | -------------------------------------------------------------------------------- /test/PlcInterface.OpcUa.OpcServer/OPCServer/Historical Access/Historical Access.opcuaha: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /test/PlcInterface.OpcUa.OpcServer/OPCServer/Resources/English (United States).reslang: -------------------------------------------------------------------------------- 1 | 2 | 3 | defaultMessage in: English (United States) - requested id was not found 4 | -------------------------------------------------------------------------------- /test/PlcInterface.OpcUa.OpcServer/OPCServer/Security Access/Security Access.sec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /test/PlcInterface.OpcUa.OpcServer/PlcInterface.OpcUa.OpcServer.tcconnproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | {c7f34262-5eb4-45ac-a11c-c5339bc82ff9} 5 | PlcInterface.OpcUa.OpcServer 6 | PlcInterface.OpcUa.OpcServer 7 | PlcInterface.OpcUa.OpcServer 8 | 9 | 10 | 11 | Content 12 | 13 | 14 | 15 | 16 | OPCServer.tcopcuasrv 17 | 18 | 19 | 20 | 21 | OPCServer.tcopcuasrv 22 | 23 | 24 | 25 | 26 | OPCServer.tcopcuasrv 27 | 28 | 29 | 30 | 31 | OPCServer.tcopcuasrv 32 | 33 | 34 | 35 | 36 | OPCServer.tcopcuasrv 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /test/PlcInterface.OpcUa.Tests/Assembly.cs: -------------------------------------------------------------------------------- 1 | [assembly: Parallelize(Workers = 0, Scope = ExecutionScope.MethodLevel)] 2 | [assembly: TestCategory("Unit")] 3 | -------------------------------------------------------------------------------- /test/PlcInterface.OpcUa.Tests/ISymbolInfoExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | 3 | namespace PlcInterface.OpcUa.Tests; 4 | 5 | [TestClass] 6 | public class ISymbolInfoExtensionTests 7 | { 8 | [TestMethod] 9 | public void CastAndValidateReturnsSymbolInfo() 10 | { 11 | // Arrange 12 | ISymbolInfo symbolInfo = Mock.Of(); 13 | 14 | // Act 15 | var actual = symbolInfo.ConvertAndValidate(); 16 | 17 | // Assert 18 | Assert.IsInstanceOfType(actual); 19 | Assert.IsInstanceOfType(actual); 20 | Assert.AreSame(symbolInfo, actual); 21 | } 22 | 23 | [TestMethod] 24 | public void CastAndValidateThrowsSymbolExceptionWhenNotAdsSymbol() 25 | { 26 | // Arrange 27 | var symbolMock = Mock.Of(); 28 | 29 | // Act Assert 30 | _ = Assert.ThrowsExactly(() => symbolMock.ConvertAndValidate()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/PlcInterface.OpcUa.Tests/PlcInterface.OpcUa.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/PlcInterface.OpcUa.Tests/SymbolHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using Opc.Ua.Client; 3 | using TestUtilities; 4 | 5 | namespace PlcInterface.OpcUa.Tests; 6 | 7 | [TestClass] 8 | public class SymbolHandlerTests 9 | { 10 | [TestMethod] 11 | public void GetSymbolInfoThrowsSymbolExceptionWhenNoSymbolsAreLoaded() 12 | { 13 | // Arrange 14 | var ioName = "DummyVar1"; 15 | var plcConnection = new Mock(); 16 | _ = plcConnection.SetupGet(x => x.SessionStream).Returns(Mock.Of>>()); 17 | _ = plcConnection.SetupGet(x => x.IsConnected).Returns(value: true); 18 | var symbolHandlerSettings = new OpcSymbolHandlerOptions(); 19 | using var symbolHandler = new SymbolHandler(plcConnection.Object, MockHelpers.GetOptionsMoq(symbolHandlerSettings), MockHelpers.GetLoggerMock()); 20 | 21 | // Act 22 | 23 | // Assert 24 | var exception = Assert.ThrowsExactly(() => symbolHandler.GetSymbolInfo(ioName)); 25 | Assert.AreEqual(exception.Message, $"{ioName} Does not exist in the PLC"); 26 | } 27 | 28 | [TestMethod] 29 | public void SymbolExceptionThrownWhenNoPlcConnected() 30 | { 31 | // Arrange 32 | var ioName = "DummyVar1"; 33 | var plcConnection = new Mock(); 34 | _ = plcConnection.SetupGet(x => x.SessionStream).Returns(Mock.Of>>()); 35 | _ = plcConnection.SetupGet(x => x.IsConnected).Returns(value: false); 36 | var symbolHandlerSettings = new OpcSymbolHandlerOptions(); 37 | using var symbolHandler = new SymbolHandler(plcConnection.Object, MockHelpers.GetOptionsMoq(symbolHandlerSettings), MockHelpers.GetLoggerMock()); 38 | 39 | // Act 40 | 41 | // Assert 42 | var exception = Assert.ThrowsExactly(() => symbolHandler.GetSymbolInfo(ioName)); 43 | Assert.AreEqual("PLC not connected", exception.Message); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/TestUtilities/MockDelegates.cs: -------------------------------------------------------------------------------- 1 | namespace TestUtilities; 2 | 3 | public static class MockDelegates 4 | { 5 | public delegate void OutAction(out TOut outVal); 6 | 7 | public delegate void OutAction(T1 arg1, out TOut outVal); 8 | 9 | public delegate void OutAction(T1 arg1, T2 agr2, out TOut outVal); 10 | 11 | public delegate TReturn OutFunction(out TOut outVal); 12 | 13 | public delegate TReturn OutFunction(T1 arg1, out TOut outVal); 14 | 15 | public delegate TReturn OutFunction(T1 arg1, T2 agr2, out TOut outVal); 16 | } 17 | -------------------------------------------------------------------------------- /test/TestUtilities/MockHelpers..cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.Extensions.Options; 3 | using Moq; 4 | 5 | namespace TestUtilities; 6 | 7 | public static class MockHelpers 8 | { 9 | public static ILogger GetLoggerMock() 10 | { 11 | var mock = new Mock>(); 12 | _ = mock.Setup(x => x.Log( 13 | It.IsAny(), 14 | It.IsAny(), 15 | It.IsAny(), 16 | It.IsAny(), 17 | It.IsAny>())); 18 | return mock.Object; 19 | } 20 | 21 | public static IOptions GetOptionsMoq(T options) 22 | where T : class, new() 23 | { 24 | var connectionSettingsMoq = new Mock>(); 25 | _ = connectionSettingsMoq.Setup(x => x.Value).Returns(options); 26 | return connectionSettingsMoq.Object; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/TestUtilities/TestUtilities.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/testconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "mstest": { 3 | "deployment": { 4 | "deleteDeploymentDirectoryAfterTestRunIsComplete": true, 5 | "deployTestSourceDependencies": true, 6 | "enabled": true 7 | }, 8 | "output": { 9 | "captureTrace": false 10 | }, 11 | "parallelism": { 12 | "enabled": true, 13 | "scope": "method", 14 | "workers": 0 15 | }, 16 | "execution": { 17 | "considerEmptyDataSourceAsInconclusive": true, 18 | "considerFixturesAsSpecialTests": false, 19 | "mapInconclusiveToFailed": true, 20 | "mapNotRunnableToFailed": true, 21 | "treatClassAndAssemblyCleanupWarningsAsErrors": false, 22 | "treatDiscoveryWarningsAsErrors": true 23 | }, 24 | "timeout": { 25 | "assemblyCleanup": 1000, 26 | "assemblyInitialize": 1000, 27 | "classCleanup": 1000, 28 | "classInitialize": 1000, 29 | "test": 600000, 30 | "testCleanup": 1000, 31 | "testInitialize": 1000, 32 | "useCooperativeCancellation": false 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", 3 | "version": "2.7", 4 | "publicReleaseRefSpec": [ 5 | "^refs/tags/v\\d+\\.\\d+" 6 | ], 7 | "release": { 8 | "tagName": "v{version}" 9 | }, 10 | "inherit": false 11 | } 12 | --------------------------------------------------------------------------------