├── .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 | [](https://github.com/Vectron/PlcInterface/blob/main/LICENSE.txt)
3 | [](https://github.com/Vectron/PlcInterface/actions)
4 | [](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 | [](https://www.nuget.org/packages/PlcInterface.Ads)
18 |
19 | # Plc interface OPC
20 | Implementation for the OPC UA interface.
21 | [](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