├── .editorconfig
├── .github
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yml
└── workflows
│ └── codeql-analysis.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Directory.Build.targets
├── LICENSE.md
├── NOTICE.txt
├── README.md
├── azure-pipelines.yml
├── common.props
├── deterministic-alarms.md
├── docs
└── media
│ └── icon.png
├── opcplc.sln
├── samples
├── OpcPlcBase.cs
├── OpcUaUnitTests.cs
├── OpcUaUnitTests.prj
└── nuget.config
├── scripts
└── run
├── snap
├── hooks
│ ├── configure
│ └── default-configure
└── snapcraft.yaml
├── src
├── AlarmCondition
│ ├── AlarmConditionNodeManager.cs
│ ├── Model
│ │ ├── AreaConfiguration.cs
│ │ ├── AreaConfigurationCollection.cs
│ │ ├── AreaState.cs
│ │ ├── ModelUtils.cs
│ │ ├── Namespaces.cs
│ │ └── SourceState.cs
│ └── UnderlyingSystem
│ │ ├── UnderlyingSystem.cs
│ │ ├── UnderlyingSystemAlarm.cs
│ │ ├── UnderlyingSystemAlarmStates.cs
│ │ └── UnderlyingSystemSource.cs
├── BaseDataVariableStateExtended.cs
├── Boilers
│ ├── Boiler1
│ │ ├── BoilerModel1.Classes.cs
│ │ ├── BoilerModel1.Constants.cs
│ │ ├── BoilerModel1.DataTypes.cs
│ │ ├── BoilerModel1.NodeIds.csv
│ │ ├── BoilerModel1.NodeSet.xml
│ │ ├── BoilerModel1.NodeSet2.xml
│ │ ├── BoilerModel1.PredefinedNodes.uanodes
│ │ ├── BoilerModel1.PredefinedNodes.xml
│ │ ├── BoilerModel1.Types.bsd
│ │ ├── BoilerModel1.Types.xsd
│ │ ├── Build_ModelDesign.bat
│ │ ├── ModelDesign.csv
│ │ └── ModelDesign.xml
│ ├── Boiler2
│ │ ├── BoilerModel2.Classes.cs
│ │ ├── BoilerModel2.Constants.cs
│ │ ├── BoilerModel2.DataTypes.cs
│ │ ├── BoilerModel2.NodeIds.csv
│ │ ├── BoilerModel2.NodeSet.xml
│ │ ├── BoilerModel2.NodeSet2.xml
│ │ ├── BoilerModel2.PredefinedNodes.uanodes
│ │ ├── BoilerModel2.PredefinedNodes.xml
│ │ ├── BoilerModel2.Types.bsd
│ │ ├── BoilerModel2.Types.xsd
│ │ └── Build_NodeSet2.bat
│ └── BoilerModel2.NodeSet2.xml
├── CompanionSpecs
│ ├── DI
│ │ ├── Build_NodeSet2.bat
│ │ ├── DiNodeManager.cs
│ │ ├── Opc.Ua.DI.Classes.cs
│ │ ├── Opc.Ua.DI.Constants.cs
│ │ ├── Opc.Ua.DI.DataTypes.cs
│ │ ├── Opc.Ua.DI.NodeIds.csv
│ │ ├── Opc.Ua.DI.NodeSet.xml
│ │ ├── Opc.Ua.DI.NodeSet2.xml
│ │ ├── Opc.Ua.DI.PredefinedNodes.uanodes
│ │ ├── Opc.Ua.DI.PredefinedNodes.xml
│ │ ├── Opc.Ua.DI.Types.bsd
│ │ └── Opc.Ua.DI.Types.xsd
│ └── Opc.Ua.Di.NodeSet2.xml
├── Configuration
│ ├── CliOptions.cs
│ ├── Configuration.cs
│ ├── OpcApplicationConfiguration.cs
│ ├── OpcApplicationConfigurationSecurity.cs
│ └── OpcUaAppConfigFactory.cs
├── DeterministicAlarms
│ ├── Configuration
│ │ ├── Alarm.cs
│ │ ├── AlarmObjectStates.cs
│ │ ├── ConditionStates.cs
│ │ ├── Configuration.cs
│ │ ├── Event.cs
│ │ ├── Folder.cs
│ │ ├── Script.cs
│ │ ├── ScriptException.cs
│ │ ├── Source.cs
│ │ ├── SourceObjectState.cs
│ │ ├── StateChange.cs
│ │ └── Step.cs
│ ├── DeterministicAlarmsNodeManager.cs
│ ├── Model
│ │ ├── SimConditionStatesEnum.cs
│ │ ├── SimFolderState.cs
│ │ └── SimSourceNodeState.cs
│ ├── ScriptEngine.cs
│ └── SimBackend
│ │ ├── SimAlarmStateBackend.cs
│ │ ├── SimBackendService.cs
│ │ └── SimSourceNodeBackend.cs
├── Directory.Build.props
├── Extensions
│ └── CancellationTokenExtensions.cs
├── FlatDirectoryCertificateStore.cs
├── FlatDirectoryCertificateStoreType.cs
├── Helpers
│ ├── CliHelper.cs
│ ├── DeterministicGuid.cs
│ ├── MetricsHelper.cs
│ ├── OtelHelper.cs
│ ├── PluginNodesHelper.cs
│ └── PnJsonHelper.cs
├── Logging
│ ├── LoggingProvider.cs
│ ├── SyslogFormatter.cs
│ └── SyslogFormatterOptions.cs
├── Microsoft.IoTEdge.OpcPlc.targets
├── ModelCompiler.cmd
├── ModelCompilerNodeSet2.cmd
├── NamespaceType.cs
├── Namespaces.cs
├── NodeType.cs
├── OpcPlcServer.cs
├── PlcNodeManager.cs
├── PlcServer.cs
├── PlcSimulation.cs
├── PluginNodes
│ ├── Boiler2PluginNodes.cs
│ ├── ComplexTypeBoilerPluginNode.cs
│ ├── ConfigNode.cs
│ ├── DataPluginNodes.cs
│ ├── DeterministicGuidPluginNodes.cs
│ ├── DipPluginNode.cs
│ ├── FastPluginNodes.cs
│ ├── LongIdPluginNode.cs
│ ├── LongStringPluginNodes.cs
│ ├── Models
│ │ ├── IPluginNodes.cs
│ │ └── NodeWithIntervals.cs
│ ├── NegTrendPluginNode.cs
│ ├── NodeSet2PluginNodes.cs
│ ├── OpaqueAndNodeIdPluginNode.cs
│ ├── PluginNodeBase.cs
│ ├── PosTrendPluginNode.cs
│ ├── SlowFastCommon.cs
│ ├── SlowPluginNodes.cs
│ ├── SpecialCharNamePluginNode.cs
│ ├── SpikePluginNode.cs
│ ├── UaNodesPluginNodes.cs
│ ├── UserDefinedPluginNodes.cs
│ ├── VeryFastByteStringPluginNodes.cs
│ └── WorkingSetPluginNode.cs
├── Program.cs
├── Reference
│ └── ReferenceNodeManager.cs
├── SimpleEvent
│ ├── Build_ModelDesign.bat
│ ├── ModelDesign.csv
│ ├── ModelDesign.xml
│ ├── SimpleEvents.Classes.cs
│ ├── SimpleEvents.Constants.cs
│ ├── SimpleEvents.DataTypes.cs
│ ├── SimpleEvents.NodeIds.csv
│ ├── SimpleEvents.NodeSet.xml
│ ├── SimpleEvents.NodeSet2.xml
│ ├── SimpleEvents.PredefinedNodes.uanodes
│ ├── SimpleEvents.PredefinedNodes.xml
│ ├── SimpleEvents.Types.bsd
│ ├── SimpleEvents.Types.xsd
│ └── SimpleEventsNodeManager.cs
├── SimulatedVariableNode.cs
├── Startup.cs
├── TimeService.cs
├── UserAuthentication.cs
├── appsettings.json
├── container.json
├── nodesfile.json
├── opc-plc.csproj
└── project.props
├── tests
├── AlarmTests.cs
├── Boiler2DeviceHealthEventsTests.cs
├── Boiler2Tests.cs
├── BoilerTests.cs
├── DataMonitoringTests.cs
├── DataRandomizationTests.cs
├── DeterministicAlarmsTests.cs
├── DeterministicAlarmsTests
│ ├── dalm001.json
│ └── dalm002.json
├── DeterministicAlarmsTests2.cs
├── EventInstancesTests.cs
├── EventMonitoringTests.cs
├── FastTimerTests.cs
├── GuidNodesTests.cs
├── MetricsTests.cs
├── MonitoringTestsBase.cs
├── OpaqueAndNodeIdTests.cs
├── PlcSimulatorFixture.Config.xml
├── PlcSimulatorFixture.cs
├── README.md
├── SimulatorNodesTests.cs
├── SimulatorTestsBase.cs
├── TestLogger.cs
├── UserDefinedNodesTests.cs
├── VariableTests.cs
└── opc-plc-tests.csproj
├── tools
├── scripts
│ ├── acr-build.ps1
│ ├── acr-matrix.ps1
│ ├── build.ps1
│ ├── docker-build.ps1
│ ├── docker-source.ps1
│ ├── get-matrix.ps1
│ ├── get-root.ps1
│ ├── get-version.ps1
│ └── set-version.ps1
└── templates
│ ├── acrbuild.yml
│ ├── azuredeploy.opcplc.aci.json
│ └── ci.yml
└── version.json
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
4 | > Please provide us with the following information:
5 | > ---------------------------------------------------------------
6 |
7 | ### This issue is for a: (mark with an `x`)
8 | ```
9 | - [ ] bug report -> please search issues before submitting
10 | - [ ] feature request
11 | - [ ] documentation issue or request
12 | - [ ] regression (a behavior that used to work and stopped in a new release)
13 | ```
14 |
15 | ### Minimal steps to reproduce
16 | >
17 |
18 | ### Any log messages given by the failure
19 | >
20 |
21 | ### Expected/desired behavior
22 | >
23 |
24 | ### OS and Version?
25 | > Windows 10/11?. Linux (which distribution?).
26 |
27 | ### Versions
28 | >
29 |
30 | ### Mention any other details that might be useful
31 |
32 | > ---------------------------------------------------------------
33 | > Thanks! We'll be in touch soon.
34 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Purpose
2 |
3 | * ...
4 |
5 | ## Does this introduce a breaking change?
6 |
7 | ```
8 | [ ] Yes
9 | [ ] No
10 | ```
11 |
12 | ## Pull Request Type
13 | What kind of change does this Pull Request introduce?
14 |
15 |
16 | ```
17 | [ ] Bugfix
18 | [ ] Feature
19 | [ ] Code style update (formatting, local variables)
20 | [ ] Refactoring (no functional changes, no api changes)
21 | [ ] Documentation content changes
22 | [ ] Other... Please describe:
23 | ```
24 |
25 | ## How to Test
26 | * Get the code
27 |
28 | ```
29 | git clone [repo-address]
30 | cd [repo-name]
31 | git checkout [branch-name]
32 | npm install
33 | ```
34 |
35 | * Test the code
36 |
37 | ```
38 | ```
39 |
40 | ## What to Check
41 | Verify that the following are valid
42 | * ...
43 |
44 | ## Other Information
45 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "nuget"
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "daily"
12 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | # The branches below must be a subset of the branches above
8 | branches: [ main ]
9 | schedule:
10 | - cron: '35 11 * * 6'
11 |
12 | jobs:
13 | analyze:
14 | name: Analyze
15 | runs-on: ubuntu-latest
16 | permissions:
17 | actions: read
18 | contents: read
19 | security-events: write
20 |
21 | strategy:
22 | fail-fast: false
23 | matrix:
24 | language: [ 'csharp' ]
25 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
26 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
27 |
28 | steps:
29 | - name: Checkout repository
30 | uses: actions/checkout@v2
31 | with:
32 | fetch-depth: 0
33 |
34 | # Initializes the CodeQL tools for scanning.
35 | - name: Initialize CodeQL
36 | uses: github/codeql-action/init@v3
37 | with:
38 | languages: ${{ matrix.language }}
39 | # If you wish to specify custom queries, you can do so here or in a config file.
40 | # By default, queries listed here will override any specified in a config file.
41 | # Prefix the list here with "+" to use these queries and those in the config file.
42 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
43 |
44 | - name: Set up .NET
45 | uses: actions/setup-dotnet@v4
46 | with:
47 | dotnet-version: '9.0.x'
48 |
49 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
50 | # If this step fails, then you should remove it and run the build manually (see below)
51 | - name: Autobuild
52 | uses: github/codeql-action/autobuild@v3
53 |
54 | # ℹ️ Command-line programs to run using the OS shell.
55 | # 📚 https://git.io/JvXDl
56 |
57 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
58 | # and modify them (or add more) to build your code if your project
59 | # uses a compiled language
60 |
61 | #- run: |
62 | # make bootstrap
63 | # make release
64 |
65 | - name: Perform CodeQL Analysis
66 | uses: github/codeql-action/analyze@v3
67 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /**/*.user
2 | /**/*.cache
3 | /**/*.user.json
4 | /**/*.nupkg
5 | /**/obj/**
6 | /**/.vs
7 | /**/bin/**
8 | /**/pki/**
9 | /**/csx/**
10 | /**/ecf/**
11 | /**/*.log
12 | /**/*.err
13 | /**/*.sdf
14 | /**/*.rsa
15 | /**/*.opendb
16 | /**/*.opensdf
17 | /**/*.vc.db
18 | /**/launchsettings.json
19 | /.vscode/**
20 | /build/**
21 | *.dll
22 | *.so
23 | *.pdb
24 | *.der
25 | *.pfx
26 | *.exe
27 | /src/out
28 | /src/Logs
29 | /_tmpout/**
30 | /TestResults/**
31 | /**/TestResults/**
32 | /**/packages/**
33 | /**/appsettings.*.json
34 | .env
35 | *.crt
36 | *.key
37 | *.user
38 | *.cache
39 | CodeCoverage
40 | coverage.cobertura.xml
41 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [project-title] Changelog
2 |
3 |
4 | # x.y.z (yyyy-mm-dd)
5 |
6 | *Features*
7 | * ...
8 |
9 | *Bug Fixes*
10 | * ...
11 |
12 | *Breaking Changes*
13 | * ...
14 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | We'll be glad to accept patches and contributions to the project. There are just few guidelines we ask to follow.
2 |
3 | Contribution License Agreement
4 | ==============================
5 |
6 | If you want/plan to contribute, we ask you to sign a [CLA](https://cla.microsoft.com/) (Contribution License Agreement). A friendly bot will remind you about it when you submit a pull-request.
7 |
8 | Submitting a contribution
9 | =========================
10 |
11 | It's generally best to start by [opening a new issue](https://help.github.com/articles/creating-an-issue) describing the work you intend to submit. Even for minor tasks, it's helpful to know what contributors are working on. Please mention in the initial issue that you are planning to work on it, so that it can be assigned to you.
12 |
13 | Follow the usual GitHub flow process of [forking the project](https://help.github.com/articles/fork-a-repo), and setup a new branch to work in. Each group of changes should be done in separate branches, in order to ensure that a pull request only includes the changes related to one issue.
14 |
15 | Any significant change should almost always be accompanied by tests. Look at the existing tests to see the testing approach and style used.
16 |
17 | Follow the project coding style, to ensure consistency and quick code reviews. We heavily rely on dependency injection using *Autofac* and thus ask to follow the same paradigm when adding new code.
18 |
19 | Do your best to have clear commit messages for each change, in order to keep consistency throughout the project. Reference the issue number (#num). A good commit message serves at least these purposes:
20 | * Speed up the pull request review process
21 | * Help future developers to understand the purpose of your code
22 | * Help the maintainer write release notes
23 |
24 | One-line messages are fine for small changes, but bigger changes should look like this:
25 | ```
26 | $ git commit -m "A brief summary of the commit
27 | >
28 | > A paragraph describing what changed and its impact."
29 | ```
30 |
31 | Finally, push the commits to your fork, submit a pull request, wait for all gates to pass and fix any issues found as part of the gate process. The team might ask for some [changes](https://help.github.com/articles/committing-changes-to-a-pull-request-branch-created-from-a-fork) before merging the pull request.
--------------------------------------------------------------------------------
/Directory.Build.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation. All rights reserved.
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
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | stages:
2 | - stage: build
3 | displayName: 'Build and Test Code'
4 | jobs:
5 | - template: tools/templates/ci.yml
6 | - stage: images
7 | displayName: 'Build Images'
8 | dependsOn:
9 | - build
10 | jobs:
11 | - template: tools/templates/acrbuild.yml
12 |
--------------------------------------------------------------------------------
/common.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | Azure Industrial OPC UA PLC
4 | https://github.com/Azure-Samples/iot-edge-opc-plc
5 | MIT
6 | NU5125
7 | Microsoft
8 | Microsoft
9 | © Microsoft Corporation. All rights reserved.
10 | icon.png
11 | $(RepositoryUrl)/README.md
12 | $(RepositoryUrl)
13 | true
14 | Industrial;Industrial IoT;Manufacturing;Azure;IoT;.NET
15 | true
16 | true
17 | en-US
18 | false
19 | true
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | true
28 | snupkg
29 | true
30 | true
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | true
44 |
45 |
46 |
47 | false
48 |
49 |
50 |
51 | $(PrereleaseVersionNoLeadingHyphen)-$(GitCommitIdShort)
52 | [$(NuGetPackageVersion)]
53 | $(VersionPrefix)-$(PrereleaseVersion)*
54 | $(VersionPrefix)-*
55 | $(VersionPrefix)
56 | $(NuGetPackageVersion)
57 |
58 |
59 |
--------------------------------------------------------------------------------
/docs/media/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/iot-edge-opc-plc/d899d80ce719aa47c8b3e1e1dd1a890bd8787a21/docs/media/icon.png
--------------------------------------------------------------------------------
/opcplc.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.4.33205.214
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{99F0F8B6-79F2-45FB-A5B7-5A3CD731A43D}"
7 | ProjectSection(SolutionItems) = preProject
8 | .editorconfig = .editorconfig
9 | azure-pipelines.yml = azure-pipelines.yml
10 | common.props = common.props
11 | deterministic-alarms.md = deterministic-alarms.md
12 | README.md = README.md
13 | version.json = version.json
14 | EndProjectSection
15 | EndProject
16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "opc-plc", "src\opc-plc.csproj", "{7CAC7271-E0D2-4BB5-A9FB-D6EE4EDFE19D}"
17 | EndProject
18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{5DD19E36-0601-491E-932A-E6AB807AD85C}"
19 | EndProject
20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{5C55721F-4E2A-4DEF-864F-17D68BEBC3DB}"
21 | ProjectSection(SolutionItems) = preProject
22 | tools\scripts\acr-build.ps1 = tools\scripts\acr-build.ps1
23 | tools\scripts\acr-matrix.ps1 = tools\scripts\acr-matrix.ps1
24 | tools\scripts\build.ps1 = tools\scripts\build.ps1
25 | tools\scripts\docker-build.ps1 = tools\scripts\docker-build.ps1
26 | tools\scripts\docker-source.ps1 = tools\scripts\docker-source.ps1
27 | tools\scripts\get-matrix.ps1 = tools\scripts\get-matrix.ps1
28 | tools\scripts\get-root.ps1 = tools\scripts\get-root.ps1
29 | tools\scripts\get-version.ps1 = tools\scripts\get-version.ps1
30 | tools\scripts\set-version.ps1 = tools\scripts\set-version.ps1
31 | EndProjectSection
32 | EndProject
33 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "templates", "templates", "{E32C2238-5380-471E-B15E-4B11A8C27528}"
34 | ProjectSection(SolutionItems) = preProject
35 | tools\templates\acrbuild.yml = tools\templates\acrbuild.yml
36 | tools\templates\azuredeploy.opcplc.aci.json = tools\templates\azuredeploy.opcplc.aci.json
37 | tools\templates\ci.yml = tools\templates\ci.yml
38 | .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml
39 | EndProjectSection
40 | EndProject
41 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "opc-plc-tests", "tests\opc-plc-tests.csproj", "{978DA9FC-4211-46CE-8548-D16E3DC3550F}"
42 | EndProject
43 | # Uncomment the following to include local OPC UA SDK files, e.g. for debugging.
44 | # You will also need to define the constant UseLocalOpcUaSdk in the file Directory.Build.targets.
45 | +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Opc.Ua.Configuration", "..\UA-.NETStandard\Libraries\Opc.Ua.Configuration\Opc.Ua.Configuration.csproj", "{19796333-27CA-461A-A3B3-96F7DB378F23}"
46 | +EndProject
47 | +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Opc.Ua.Server", "..\UA-.NETStandard\Libraries\Opc.Ua.Server\Opc.Ua.Server.csproj", "{6D71B6B9-2A80-44A7-A5BE-E1012C2419E0}"
48 | +EndProject
49 | +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Opc.Ua.Core", "..\UA-.NETStandard\Stack\Opc.Ua.Core\Opc.Ua.Core.csproj", "{322731A7-E3EA-40FA-889B-81243B67DFC7}"
50 | +EndProject
51 | Global
52 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
53 | Debug|Any CPU = Debug|Any CPU
54 | Release|Any CPU = Release|Any CPU
55 | EndGlobalSection
56 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
57 | {7CAC7271-E0D2-4BB5-A9FB-D6EE4EDFE19D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
58 | {7CAC7271-E0D2-4BB5-A9FB-D6EE4EDFE19D}.Debug|Any CPU.Build.0 = Debug|Any CPU
59 | {7CAC7271-E0D2-4BB5-A9FB-D6EE4EDFE19D}.Release|Any CPU.ActiveCfg = Release|Any CPU
60 | {7CAC7271-E0D2-4BB5-A9FB-D6EE4EDFE19D}.Release|Any CPU.Build.0 = Release|Any CPU
61 | {978DA9FC-4211-46CE-8548-D16E3DC3550F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
62 | {978DA9FC-4211-46CE-8548-D16E3DC3550F}.Debug|Any CPU.Build.0 = Debug|Any CPU
63 | {978DA9FC-4211-46CE-8548-D16E3DC3550F}.Release|Any CPU.ActiveCfg = Release|Any CPU
64 | {978DA9FC-4211-46CE-8548-D16E3DC3550F}.Release|Any CPU.Build.0 = Release|Any CPU
65 | EndGlobalSection
66 | GlobalSection(SolutionProperties) = preSolution
67 | HideSolutionNode = FALSE
68 | EndGlobalSection
69 | GlobalSection(NestedProjects) = preSolution
70 | {5DD19E36-0601-491E-932A-E6AB807AD85C} = {99F0F8B6-79F2-45FB-A5B7-5A3CD731A43D}
71 | {5C55721F-4E2A-4DEF-864F-17D68BEBC3DB} = {5DD19E36-0601-491E-932A-E6AB807AD85C}
72 | {E32C2238-5380-471E-B15E-4B11A8C27528} = {5DD19E36-0601-491E-932A-E6AB807AD85C}
73 | EndGlobalSection
74 | GlobalSection(ExtensibilityGlobals) = postSolution
75 | SolutionGuid = {90B4B490-DDDE-4BB2-A111-E5DAE5000248}
76 | EndGlobalSection
77 | EndGlobal
78 |
--------------------------------------------------------------------------------
/samples/OpcPlcBase.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests;
2 |
3 | using OpcPlc;
4 | using System;
5 | using System.Collections.Concurrent;
6 | using System.Linq;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 |
10 | ///
11 | /// Base class for tests that use the OPC PLC server NuGet.
12 | ///
13 | public class OpcPlcBase
14 | {
15 | private const int MaxPortTries = 10;
16 |
17 | private static readonly ConcurrentDictionary _opcPlcs = new();
18 |
19 | private readonly OpcPlcServer _opcPlcServer;
20 | private readonly bool _endpointUrlOverridden;
21 | private readonly Random _rnd = new(1234); // Seeded (deterministic) random number generator.
22 |
23 | ///
24 | /// Initializes a new instance of the class.
25 | /// Set the to override spawning a server and use an existing one instead.
26 | ///
27 | public OpcPlcBase(string[] args, int port = 51234, string? endpointUriOverride = null)
28 | {
29 | if (_opcPlcs.TryGetValue(port, out var existingOpcPlc))
30 | {
31 | // Already running at the same port.
32 | _opcPlcServer = existingOpcPlc;
33 | OpcPlcEndpointUrl = existingOpcPlc.PlcServer.GetEndpoints()[0].EndpointUrl;
34 | return;
35 | }
36 |
37 | _opcPlcServer = new OpcPlcServer();
38 |
39 | if (!string.IsNullOrEmpty(endpointUriOverride))
40 | {
41 | OpcPlcEndpointUrl = endpointUriOverride;
42 | _endpointUrlOverridden = true;
43 | return;
44 | }
45 |
46 | // Try to use the specified port, otherwise find a deterministic random port.
47 | int usedPortRetries = MaxPortTries;
48 | while (OpcPlcEndpointUrl == string.Empty)
49 | {
50 | try
51 | {
52 | OpcPlcEndpointUrl = StartOpcPlcServerAsync(args, port, CancellationToken.None).GetAwaiter().GetResult();
53 | _ = _opcPlcs.TryAdd(port, _opcPlcServer);
54 | break;
55 | }
56 | catch (Exception)
57 | {
58 | #pragma warning disable S2589 // Boolean expressions should not be gratuitous
59 | if (--usedPortRetries == 0)
60 | {
61 | throw;
62 | }
63 | #pragma warning restore S2589 // Boolean expressions should not be gratuitous
64 |
65 | port = GetRandomEphemeralPort();
66 | }
67 | }
68 | }
69 |
70 | ///
71 | /// Gets the OPC PLC server endpoint URL.
72 | ///
73 | public string OpcPlcEndpointUrl { get; private set; } = string.Empty;
74 |
75 | ///
76 | /// Restarts the OPC PLC server with the same configuration.
77 | ///
78 | public Task RestartOpcPlcServerAsync()
79 | {
80 | if (_endpointUrlOverridden)
81 | {
82 | throw new InvalidOperationException("Cannot restart OPC PLC server when the endpoint URL is overridden.");
83 | }
84 |
85 | return _opcPlcServer.RestartAsync();
86 | }
87 |
88 | private async Task WaitForServerUpAsync(Task serverTask, CancellationToken cancellationToken)
89 | {
90 | while (true)
91 | {
92 | cancellationToken.ThrowIfCancellationRequested();
93 |
94 | if (serverTask.IsFaulted)
95 | {
96 | throw serverTask.Exception!;
97 | }
98 |
99 | if (serverTask.IsCompleted)
100 | {
101 | throw new Exception("The OPC PLC server failed to start.");
102 | }
103 |
104 | if (!_opcPlcServer.Ready)
105 | {
106 | await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
107 | continue;
108 | }
109 |
110 | return _opcPlcServer.PlcServer.GetEndpoints()[0].EndpointUrl;
111 | }
112 | }
113 |
114 | private Task StartOpcPlcServerAsync(string[] args, int port, CancellationToken cancellationToken)
115 | {
116 | // Passed args override the following defaults.
117 | var serverTask = Task.Run(
118 | async () => await _opcPlcServer.StartAsync(
119 | args?.Concat(
120 | new[]
121 | {
122 | "--autoaccept",
123 | $"--portnum={port}",
124 | }).ToArray(),
125 | cancellationToken)
126 | .ConfigureAwait(false),
127 | cancellationToken);
128 |
129 | return WaitForServerUpAsync(serverTask, cancellationToken);
130 | }
131 |
132 | private int GetRandomEphemeralPort()
133 | {
134 | // Port range 49152–65535 (https://en.wikipedia.org/wiki/Ephemeral_port).
135 | return _rnd.Next(49152, 65535);
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/samples/OpcUaUnitTests.cs:
--------------------------------------------------------------------------------
1 | namespace UnitTests;
2 |
3 | using System.Threading.Tasks;
4 |
5 | public class OpcUaUnitTests : OpcPlcBase
6 | {
7 | public OpcUaUnitTests()
8 | : base(
9 | new[] // Additional arguments.
10 | {
11 | "--gn=2",
12 | })
13 | {
14 | }
15 |
16 | [Test]
17 | public async Task TestConnectedClientSession()
18 | {
19 | using var opcUaClient = await OpcUaClientFactory
20 | .GetConnectedClient(OpcPlcEndpointUrl)
21 | .ConfigureAwait(false);
22 |
23 | opcUaClient.Session.Connected.Should().BeTrue();
24 | opcUaClient.Session.Close();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/samples/OpcUaUnitTests.prj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/samples/nuget.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/scripts/run:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | trustedcertbase64="$(snapctl get trustedcertbase64)"
4 |
5 | cmd="\"$SNAP\"/opcplc --pn=50000 --sn=10 --sr=10 --st=uint --fn=10 --fr=1 --ft=uint --gn=10 \
6 | --appcertstorepath=\"$SNAP_USER_DATA/pki/own\" \
7 | --trustedcertstorepath=\"$SNAP_USER_DATA/pki/trusted\" \
8 | --rejectedcertstorepath=\"$SNAP_USER_DATA/pki/rejected\" \
9 | --issuercertstorepath=\"$SNAP_USER_DATA/pki/issuer\" \
10 | --logfile=\"$SNAP_USER_DATA/hostname-port-plc.log\""
11 |
12 | if [ -n "$trustedcertbase64" ]; then
13 | cmd="$cmd --addtrustedcertfile=\"$SNAP_DATA/config/pki/trusted/certs/cert_1.crt\""
14 | fi
15 |
16 | eval "$cmd"
17 |
--------------------------------------------------------------------------------
/snap/hooks/configure:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | # Supported keys:
4 | # - trustedcertbase64 (string)
5 | # Certificate in base64 string format to be trusted by the server.
6 |
7 | handle_trustedcertbase64()
8 | {
9 | trustedcertbase64="$(snapctl get trustedcertbase64)"
10 | if [ -n "$trustedcertbase64" ]; then
11 | echo "$trustedcertbase64" > "$SNAP_DATA/config/pki/trusted/certs/cert_1.crt"
12 | fi
13 | }
14 |
15 | handle_trustedcertbase64
16 |
--------------------------------------------------------------------------------
/snap/hooks/default-configure:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | mkdir -p "$SNAP_DATA/config/pki/trusted/certs"
4 |
--------------------------------------------------------------------------------
/snap/snapcraft.yaml:
--------------------------------------------------------------------------------
1 | name: iot-edge-opc-plc
2 | base: core22
3 | version: '0.1'
4 | summary: Sample OPC UA server
5 | description: |
6 | Sample OPC UA server with nodes that generate random
7 | and increasing data, anomalies and much more.
8 |
9 | grade: stable
10 | confinement: strict
11 |
12 | architectures:
13 | - build-on: amd64
14 | - build-on: arm64
15 |
16 | parts:
17 | opc-plc:
18 | plugin: dotnet
19 | dotnet-build-configuration: Release
20 | dotnet-self-contained-runtime-identifier: linux-x64
21 | source: .
22 | build-packages:
23 | - dotnet-sdk-9.0
24 | scripts:
25 | plugin: dump
26 | source: scripts/
27 | organize:
28 | '*' : scripts/
29 | appsettings:
30 | plugin: dump
31 | source: src/
32 | prime:
33 | - appsettings.json
34 |
35 | apps:
36 | opc-plc:
37 | command: scripts/run
38 | plugs:
39 | - network
40 | - network-bind
41 |
--------------------------------------------------------------------------------
/src/AlarmCondition/Model/AreaConfiguration.cs:
--------------------------------------------------------------------------------
1 | namespace AlarmCondition;
2 |
3 | using Opc.Ua;
4 |
5 | public class AreaConfiguration
6 | {
7 | public string Name { get; set; }
8 | public AreaConfigurationCollection SubAreas { get; set; }
9 | public StringCollection SourcePaths { get; set; }
10 | }
11 |
--------------------------------------------------------------------------------
/src/AlarmCondition/Model/AreaConfigurationCollection.cs:
--------------------------------------------------------------------------------
1 | namespace AlarmCondition;
2 |
3 | using System.Collections.Generic;
4 |
5 | public class AreaConfigurationCollection : List
6 | {
7 | }
8 |
--------------------------------------------------------------------------------
/src/AlarmCondition/Model/AreaState.cs:
--------------------------------------------------------------------------------
1 | /* ========================================================================
2 | * Copyright (c) 2005-2019 The OPC Foundation, Inc. All rights reserved.
3 | *
4 | * OPC Foundation MIT License 1.00
5 | *
6 | * Permission is hereby granted, free of charge, to any person
7 | * obtaining a copy of this software and associated documentation
8 | * files (the "Software"), to deal in the Software without
9 | * restriction, including without limitation the rights to use,
10 | * copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the
12 | * Software is furnished to do so, subject to the following
13 | * conditions:
14 | *
15 | * The above copyright notice and this permission notice shall be
16 | * included in all copies or substantial portions of the Software.
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24 | * OTHER DEALINGS IN THE SOFTWARE.
25 | *
26 | * The complete license agreement can be found here:
27 | * http://opcfoundation.org/License/MIT/1.00/
28 | * ======================================================================*/
29 |
30 | using Opc.Ua;
31 |
32 | namespace AlarmCondition
33 | {
34 | ///
35 | /// Maps an alarm area to a UA object node.
36 | ///
37 | public partial class AreaState : FolderState
38 | {
39 | #region Constructors
40 | ///
41 | /// Initializes the area.
42 | ///
43 | public AreaState(
44 | ISystemContext context,
45 | AreaState parent,
46 | NodeId nodeId,
47 | AreaConfiguration configuration)
48 | :
49 | base(parent)
50 | {
51 | Initialize(context);
52 |
53 | // initialize the area with the fixed metadata.
54 | this.SymbolicName = configuration.Name;
55 | this.NodeId = nodeId;
56 | this.BrowseName = new QualifiedName(Utils.Format("{0}", configuration.Name), nodeId.NamespaceIndex);
57 | this.DisplayName = BrowseName.Name;
58 | this.Description = null;
59 | this.ReferenceTypeId = ReferenceTypeIds.HasNotifier;
60 | this.TypeDefinitionId = ObjectTypeIds.FolderType;
61 | this.EventNotifier = EventNotifiers.SubscribeToEvents;
62 | }
63 | #endregion
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/AlarmCondition/Model/Namespaces.cs:
--------------------------------------------------------------------------------
1 | /* ========================================================================
2 | * Copyright (c) 2005-2019 The OPC Foundation, Inc. All rights reserved.
3 | *
4 | * OPC Foundation MIT License 1.00
5 | *
6 | * Permission is hereby granted, free of charge, to any person
7 | * obtaining a copy of this software and associated documentation
8 | * files (the "Software"), to deal in the Software without
9 | * restriction, including without limitation the rights to use,
10 | * copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the
12 | * Software is furnished to do so, subject to the following
13 | * conditions:
14 | *
15 | * The above copyright notice and this permission notice shall be
16 | * included in all copies or substantial portions of the Software.
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24 | * OTHER DEALINGS IN THE SOFTWARE.
25 | *
26 | * The complete license agreement can be found here:
27 | * http://opcfoundation.org/License/MIT/1.00/
28 | * ======================================================================*/
29 |
30 | namespace AlarmCondition
31 | {
32 | ///
33 | /// Defines constants for namespaces used by the application.
34 | ///
35 | public static partial class Namespaces
36 | {
37 | ///
38 | /// The namespace for the nodes provided by the server.
39 | ///
40 | public const string AlarmCondition = "http://opcfoundation.org/AlarmCondition";
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/AlarmCondition/UnderlyingSystem/UnderlyingSystemAlarmStates.cs:
--------------------------------------------------------------------------------
1 | /* ========================================================================
2 | * Copyright (c) 2005-2019 The OPC Foundation, Inc. All rights reserved.
3 | *
4 | * OPC Foundation MIT License 1.00
5 | *
6 | * Permission is hereby granted, free of charge, to any person
7 | * obtaining a copy of this software and associated documentation
8 | * files (the "Software"), to deal in the Software without
9 | * restriction, including without limitation the rights to use,
10 | * copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the
12 | * Software is furnished to do so, subject to the following
13 | * conditions:
14 | *
15 | * The above copyright notice and this permission notice shall be
16 | * included in all copies or substantial portions of the Software.
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24 | * OTHER DEALINGS IN THE SOFTWARE.
25 | *
26 | * The complete license agreement can be found here:
27 | * http://opcfoundation.org/License/MIT/1.00/
28 | * ======================================================================*/
29 |
30 | using System;
31 |
32 | namespace AlarmCondition
33 | {
34 | ///
35 | /// Defines the possible states for the condition.
36 | ///
37 | [Flags]
38 | public enum UnderlyingSystemAlarmStates
39 | {
40 | ///
41 | /// The condition state is unknown.
42 | ///
43 | Undefined = 0x0,
44 |
45 | ///
46 | /// The condition is enabled and will produce events.
47 | ///
48 | Enabled = 0x1,
49 |
50 | ///
51 | /// The condition requires acknowledgement by the user.
52 | ///
53 | Acknowledged = 0x2,
54 |
55 | ///
56 | /// The condition requires that the used confirm that action was taken.
57 | ///
58 | Confirmed = 0x4,
59 |
60 | ///
61 | /// The condition is active.
62 | ///
63 | Active = 0x8,
64 |
65 | ///
66 | /// The condition has been suppressed by the system.
67 | ///
68 | Suppressed = 0x10,
69 |
70 | ///
71 | /// The condition has been shelved by the user.
72 | ///
73 | Shelved = 0x20,
74 |
75 | ///
76 | /// The condition has exceeded the high-high limit.
77 | ///
78 | HighHigh = 0x40,
79 |
80 | ///
81 | /// The condition has exceeded the high limit.
82 | ///
83 | High = 0x80,
84 |
85 | ///
86 | /// The condition has exceeded the low limit.
87 | ///
88 | Low = 0x100,
89 |
90 | ///
91 | /// The condition has exceeded the low-low limit.
92 | ///
93 | LowLow = 0x200,
94 |
95 | ///
96 | /// A mask used to clear all limit bits.
97 | ///
98 | Limits = 0x3C0,
99 |
100 | ///
101 | /// The condition has deleted.
102 | ///
103 | Deleted = 0x400
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/BaseDataVariableStateExtended.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc;
2 |
3 | using Opc.Ua;
4 | using System;
5 |
6 | ///
7 | /// Extended BaseDataVariableState class to hold additional parameters for simulation.
8 | ///
9 | public class BaseDataVariableStateExtended : BaseDataVariableState
10 | {
11 | public bool Randomize { get; }
12 | public object StepSize { get; }
13 | public object MinValue { get; }
14 | public object MaxValue { get; }
15 |
16 | public BaseDataVariableStateExtended(NodeState nodeState, bool randomize, object stepSize, object minValue, object maxValue) : base(nodeState)
17 | {
18 | ArgumentNullException.ThrowIfNull(nodeState);
19 |
20 | Randomize = randomize;
21 | StepSize = stepSize;
22 | MinValue = minValue;
23 | MaxValue = maxValue;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Boilers/Boiler1/BoilerModel1.NodeIds.csv:
--------------------------------------------------------------------------------
1 | Boiler_BinarySchema,15074,Variable
2 | Boiler_XmlSchema,15086,Variable
3 | Boiler1,15070,Object
4 | Boiler1Type,3,ObjectType
5 | BoilerDataType,15032,DataType
6 | BoilerDataType_Encoding_DefaultBinary,15072,Object
7 | BoilerDataType_Encoding_DefaultJson,15096,Object
8 | BoilerDataType_Encoding_DefaultXml,15084,Object
9 | BoilerHeaterStateType,15014,DataType
10 | Boilers,5,Object
11 | BoilerTemperatureType,15001,DataType
12 | BoilerTemperatureType_Encoding_DefaultBinary,15004,Object
13 | BoilerTemperatureType_Encoding_DefaultJson,15012,Object
14 | BoilerTemperatureType_Encoding_DefaultXml,15008,Object
15 |
--------------------------------------------------------------------------------
/src/Boilers/Boiler1/BoilerModel1.PredefinedNodes.uanodes:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/iot-edge-opc-plc/d899d80ce719aa47c8b3e1e1dd1a890bd8787a21/src/Boilers/Boiler1/BoilerModel1.PredefinedNodes.uanodes
--------------------------------------------------------------------------------
/src/Boilers/Boiler1/BoilerModel1.Types.bsd:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 | Temperature in °C, pressure in Pa and heater state.
13 |
14 |
15 |
16 |
17 |
18 |
19 | Temperature in °C next to the heater at the bottom, and away from the heater at the top.
20 |
21 |
22 |
23 |
24 |
25 | Heater working state.
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/Boilers/Boiler1/BoilerModel1.Types.xsd:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Temperature in °C, pressure in Pa and heater state.
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | Temperature in °C next to the heater at the bottom, and away from the heater at the top.
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | Heater working state.
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/Boilers/Boiler1/Build_ModelDesign.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | REM Pass the model file name without .xml extension
4 | ..\..\ModelCompiler.cmd ModelDesign
5 |
--------------------------------------------------------------------------------
/src/Boilers/Boiler1/ModelDesign.csv:
--------------------------------------------------------------------------------
1 | BoilerModel1Type,1,Unspecified
2 | BoilerModel1Type_BoilerStatus,2,Unspecified
3 | Boiler1Type,3,ObjectType
4 | Boiler1Type_BoilerStatus,4,Variable
5 | Boilers,5,Object
6 | BoilerTemperatureType,15001,DataType
7 | BoilerType_BoilerStatus,15003,Unspecified
8 | BoilerTemperatureType_Encoding_DefaultBinary,15004,Object
9 | Boiler_BinarySchema_BoilerTemperatureType,15005,Variable
10 | Boiler_BinarySchema_BoilerTemperatureType_DataTypeVersion,15006,Variable
11 | Boiler_BinarySchema_BoilerTemperatureType_DictionaryFragment,15007,Variable
12 | BoilerTemperatureType_Encoding_DefaultXml,15008,Object
13 | Boiler_XmlSchema_BoilerTemperatureType,15009,Variable
14 | Boiler_XmlSchema_BoilerTemperatureType_DataTypeVersion,15010,Variable
15 | Boiler_XmlSchema_BoilerTemperatureType_DictionaryFragment,15011,Variable
16 | BoilerTemperatureType_Encoding_DefaultJson,15012,Object
17 | Boiler1_BoilerStatus,15013,Variable
18 | BoilerHeaterStateType,15014,DataType
19 | BoilerHeaterStateType_EnumStrings,15015,Variable
20 | BoilerDataType,15032,DataType
21 | BoilerType,15068,Unspecified
22 | Boiler1,15070,Object
23 | BoilerDataType_Encoding_DefaultBinary,15072,Object
24 | Boiler_BinarySchema,15074,Variable
25 | Boiler_BinarySchema_DataTypeVersion,15075,Variable
26 | Boiler_BinarySchema_NamespaceUri,15076,Variable
27 | Boiler_BinarySchema_Deprecated,15077,Variable
28 | Boiler_BinarySchema_BoilerDataType,15078,Variable
29 | Boiler_BinarySchema_BoilerDataType_DataTypeVersion,15079,Variable
30 | Boiler_BinarySchema_BoilerDataType_DictionaryFragment,15080,Variable
31 | BoilerDataType_Encoding_DefaultXml,15084,Object
32 | Boiler_XmlSchema,15086,Variable
33 | Boiler_XmlSchema_DataTypeVersion,15087,Variable
34 | Boiler_XmlSchema_NamespaceUri,15088,Variable
35 | Boiler_XmlSchema_Deprecated,15089,Variable
36 | Boiler_XmlSchema_BoilerDataType,15090,Variable
37 | Boiler_XmlSchema_BoilerDataType_DataTypeVersion,15091,Variable
38 | Boiler_XmlSchema_BoilerDataType_DictionaryFragment,15092,Variable
39 | BoilerDataType_Encoding_DefaultJson,15096,Object
40 |
--------------------------------------------------------------------------------
/src/Boilers/Boiler1/ModelDesign.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
15 |
16 | http://opcfoundation.org/UA/
17 | http://microsoft.com/Opc/OpcPlc/Boiler
18 |
19 |
20 |
21 |
22 | Temperature in °C, pressure in Pa and heater state.
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Temperature in °C next to the heater at the bottom, and away from the heater at the top.
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | Heater working state.
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | 20
59 | 20
60 |
61 |
62 | 100020
63 |
64 | On
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | Boilers
77 |
78 | Sample boilers.
79 |
80 |
81 |
82 |
83 | ua:Organizes
84 | ua:ObjectsFolder
85 |
86 |
87 |
88 |
89 |
90 |
95 |
96 | Boiler #1
97 |
98 | A simple boiler.
99 |
100 |
101 |
102 |
103 | ua:Organizes
104 | Boilers
105 |
106 |
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/src/Boilers/Boiler2/BoilerModel2.DataTypes.cs:
--------------------------------------------------------------------------------
1 | /* ========================================================================
2 | * Copyright (c) 2005-2021 The OPC Foundation, Inc. All rights reserved.
3 | *
4 | * OPC Foundation MIT License 1.00
5 | *
6 | * Permission is hereby granted, free of charge, to any person
7 | * obtaining a copy of this software and associated documentation
8 | * files (the "Software"), to deal in the Software without
9 | * restriction, including without limitation the rights to use,
10 | * copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the
12 | * Software is furnished to do so, subject to the following
13 | * conditions:
14 | *
15 | * The above copyright notice and this permission notice shall be
16 | * included in all copies or substantial portions of the Software.
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24 | * OTHER DEALINGS IN THE SOFTWARE.
25 | *
26 | * The complete license agreement can be found here:
27 | * http://opcfoundation.org/License/MIT/1.00/
28 | * ======================================================================*/
29 |
30 | using System;
31 | using System.Collections.Generic;
32 | using System.Text;
33 | using System.Xml;
34 | using System.Runtime.Serialization;
35 | using Opc.Ua;
36 | using Opc.Ua.DI;
37 |
38 | namespace BoilerModel2
39 | {
40 | }
--------------------------------------------------------------------------------
/src/Boilers/Boiler2/BoilerModel2.PredefinedNodes.uanodes:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/iot-edge-opc-plc/d899d80ce719aa47c8b3e1e1dd1a890bd8787a21/src/Boilers/Boiler2/BoilerModel2.PredefinedNodes.uanodes
--------------------------------------------------------------------------------
/src/Boilers/Boiler2/BoilerModel2.Types.bsd:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/Boilers/Boiler2/BoilerModel2.Types.xsd:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Boilers/Boiler2/Build_NodeSet2.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | REM Pass the model file name without .xml extension
4 | ..\..\ModelCompilerNodeSet2.cmd ..\BoilerModel2.NodeSet2 BoilerModel2 Boiler2 ..\..\CompanionSpecs\Opc.Ua.Di.NodeSet2 Opc.Ua.DI OpcUaDI
5 |
--------------------------------------------------------------------------------
/src/CompanionSpecs/DI/Build_NodeSet2.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | REM Pass the model file name without .xml extension
4 | ..\..\ModelCompilerNodeSet2.cmd ..\Opc.Ua.Di.NodeSet2 Opc.Ua.DI OpcUaDI
5 |
--------------------------------------------------------------------------------
/src/CompanionSpecs/DI/DiNodeManager.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.CompanionSpecs.DI;
2 |
3 | using Opc.Ua;
4 | using Opc.Ua.Server;
5 | using System;
6 | using System.IO;
7 | using System.Reflection;
8 |
9 | ///
10 | /// Node manager for a server that exposes the Device Information (DI) companion spec.
11 | /// https://opcfoundation.org/developer-tools/documents/view/197
12 | ///
13 | public sealed class DiNodeManager : CustomNodeManager2
14 | {
15 | ///
16 | /// Initializes a new instance of the class.
17 | ///
18 | public DiNodeManager(IServerInternal server, ApplicationConfiguration _)
19 | :
20 | base(server, _)
21 | {
22 | SystemContext.NodeIdFactory = this;
23 |
24 | // Set one namespace for the type model and one namespace for dynamically created nodes.
25 | string[] namespaceUrls = [OpcPlc.Namespaces.DI];
26 | SetNamespaces(namespaceUrls);
27 | }
28 |
29 | ///
30 | /// Loads a node set from a file or resource and adds them to the set of predefined nodes.
31 | ///
32 | protected override NodeStateCollection LoadPredefinedNodes(ISystemContext context)
33 | {
34 | var uanodesPath = "CompanionSpecs/DI/Opc.Ua.DI.PredefinedNodes.uanodes";
35 | var snapLocation = Environment.GetEnvironmentVariable("SNAP");
36 | if (!string.IsNullOrWhiteSpace(snapLocation))
37 | {
38 | // Application running as a snap
39 | uanodesPath = Path.Join(snapLocation, uanodesPath);
40 | }
41 |
42 | var predefinedNodes = new NodeStateCollection();
43 | predefinedNodes.LoadFromBinaryResource(context,
44 | uanodesPath, // CopyToOutputDirectory -> PreserveNewest.
45 | typeof(DiNodeManager).GetTypeInfo().Assembly,
46 | updateTables: true);
47 |
48 | return predefinedNodes;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/CompanionSpecs/DI/Opc.Ua.DI.PredefinedNodes.uanodes:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/iot-edge-opc-plc/d899d80ce719aa47c8b3e1e1dd1a890bd8787a21/src/CompanionSpecs/DI/Opc.Ua.DI.PredefinedNodes.uanodes
--------------------------------------------------------------------------------
/src/CompanionSpecs/DI/Opc.Ua.DI.Types.bsd:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/Configuration/Configuration.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Configuration;
2 |
3 | using OpenTelemetry.Exporter;
4 | using System;
5 |
6 | public class OpcPlcConfiguration
7 | {
8 | ///
9 | /// Name of the application.
10 | ///
11 | public readonly string ProgramName = "OpcPlc";
12 |
13 | public bool DisableAnonymousAuth { get; set; }
14 |
15 | public bool DisableUsernamePasswordAuth { get; set; }
16 |
17 | public bool DisableCertAuth { get; set; }
18 |
19 | ///
20 | /// Admin user.
21 | ///
22 | public string AdminUser { get; set; } = "sysadmin";
23 |
24 | ///
25 | /// Admin user password.
26 | ///
27 | public string AdminPassword { get; set; } = "demo";
28 |
29 | ///
30 | /// Default user.
31 | ///
32 | public string DefaultUser { get; set; } = "user1";
33 |
34 | ///
35 | /// Default user password.
36 | ///
37 | public string DefaultPassword { get; set; } = "password";
38 |
39 | ///
40 | /// Gets or sets OTLP reporting endpoint URI.
41 | ///
42 | public string OtlpEndpointUri { get; set; } // e.g. "http://localhost:4317"
43 |
44 | ///
45 | /// Gets or sets the OTLP export interval in seconds.
46 | ///
47 | public TimeSpan OtlpExportInterval { get; set; } = TimeSpan.FromSeconds(60);
48 |
49 | //
50 | /// Gets or sets the OTLP export protocol: grpc, protobuf.
51 | ///
52 | public string OtlpExportProtocol { get; set; } = "grpc";
53 |
54 | ///
55 | /// Gets or sets how to handle metrics for publish requests.
56 | /// Allowed values:
57 | /// disable=Always disabled,
58 | /// enable=Always enabled,
59 | /// auto=Auto-disable when sessions > 40 or monitored items > 500.
60 | ///
61 | public string OtlpPublishMetrics { get; set; } = "auto";
62 |
63 | ///
64 | /// Show OPC Publisher configuration file using IP address as EndpointUrl.
65 | ///
66 | public bool ShowPublisherConfigJsonIp { get; set; }
67 |
68 | ///
69 | /// Show OPC Publisher configuration file using plchostname as EndpointUrl.
70 | ///
71 | public bool ShowPublisherConfigJsonPh { get; set; }
72 |
73 | ///
74 | /// Web server port for hosting OPC Publisher file.
75 | ///
76 | public uint WebServerPort { get; set; } = 8080;
77 |
78 | ///
79 | /// Show usage help.
80 | ///
81 | public bool ShowHelp { get; set; }
82 |
83 | public string PnJson { get; set; } = "pn.json";
84 |
85 | ///
86 | /// Logging configuration.
87 | ///
88 | public string LogFileName { get; set; } = $"hostname-port-plc.log"; // Set in InitLogging().
89 |
90 | public string LogLevelCli { get; set; } = "info";
91 |
92 | public TimeSpan LogFileFlushTimeSpanSec { get; set; } = TimeSpan.FromSeconds(30);
93 |
94 | public OpcApplicationConfiguration OpcUa { get; set; } = new OpcApplicationConfiguration();
95 |
96 | ///
97 | /// Configure chaos mode
98 | ///
99 | public bool RunInChaosMode { get; set; }
100 | }
101 |
--------------------------------------------------------------------------------
/src/Configuration/OpcApplicationConfiguration.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Configuration;
2 |
3 | using Opc.Ua;
4 |
5 | ///
6 | /// Class for OPC Application configuration.
7 | ///
8 | public partial class OpcApplicationConfiguration
9 | {
10 | ///
11 | /// Configuration info for the OPC application.
12 | ///
13 | public ApplicationConfiguration ApplicationConfiguration { get; set; }
14 |
15 | public string Hostname
16 | {
17 | get => _hostname;
18 | set => _hostname = value.ToLowerInvariant();
19 | }
20 |
21 | public string HostnameLabel => _hostname.Contains('.')
22 | ? _hostname[.._hostname.IndexOf('.')]
23 | : _hostname;
24 |
25 | public string ProductUri => "https://github.com/azure-samples/iot-edge-opc-plc";
26 |
27 | public ushort ServerPort { get; set; } = 50000;
28 |
29 | public string ServerPath { get; set; } = string.Empty;
30 |
31 | public int MaxSessionCount { get; set; } = 100;
32 |
33 | public int MaxSessionTimeout { get; set; } = 3_600_000; // 1 h.
34 |
35 | public int MaxSubscriptionCount { get; set; } = 100;
36 |
37 | public int MaxQueuedRequestCount { get; set; } = 2_000;
38 |
39 | ///
40 | /// Default endpoint security of the application.
41 | ///
42 | public string ServerSecurityPolicy { get; set; } = SecurityPolicies.Basic128Rsa15;
43 |
44 | ///
45 | /// Enables unsecure endpoint access to the application.
46 | ///
47 | public bool EnableUnsecureTransport { get; set; }
48 |
49 | ///
50 | /// Sets the LDS registration interval in milliseconds.
51 | ///
52 | public int LdsRegistrationInterval { get; set; }
53 |
54 | ///
55 | /// Set the max string length the OPC stack supports.
56 | ///
57 | public int OpcMaxStringLength { get; set; } = 4 * 1024 * 1024;
58 |
59 | // Number of publish responses that can be cached per subscription for republish.
60 | // If this value is too high and if the publish responses are not acknowledged,
61 | // the server may run out of memory for large number of subscriptions.
62 | public int MaxMessageQueueSize { get; } = 20;
63 |
64 | // Max. queue size for monitored items.
65 | public int MaxNotificationQueueSize { get; } = 1_000;
66 |
67 | // Max. number of notifications per publish response. Limit on server side.
68 | public int MaxNotificationsPerPublish { get; } = 2_000;
69 |
70 | // Max. number of publish requests per session that can be queued for processing.
71 | public int MaxPublishRequestPerSession { get; } = 20;
72 |
73 | // Max. number of threads that can be used for processing service requests.
74 | // The value should be higher than MaxPublishRequestPerSession to avoid a deadlock.
75 | public int MaxRequestThreadCount { get; } = 200;
76 |
77 | private string _hostname = Utils.GetHostName().ToLowerInvariant();
78 | }
79 |
--------------------------------------------------------------------------------
/src/Configuration/OpcApplicationConfigurationSecurity.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Configuration;
2 |
3 | using Opc.Ua;
4 | using System.Collections.Generic;
5 | using System.IO;
6 |
7 | ///
8 | /// Class for OPC Application configuration. Here the security relevant configuration.
9 | ///
10 | public partial class OpcApplicationConfiguration
11 | {
12 | public OpcApplicationConfiguration()
13 | {
14 | OpcOwnCertStorePath = OpcOwnCertDirectoryStorePathDefault;
15 | OpcTrustedCertStorePath = OpcTrustedCertDirectoryStorePathDefault;
16 | OpcRejectedCertStorePath = OpcRejectedCertDirectoryStorePathDefault;
17 | OpcIssuerCertStorePath = OpcIssuerCertDirectoryStorePathDefault;
18 | }
19 |
20 | ///
21 | /// Add own certificate to trusted peer store.
22 | ///
23 | public bool TrustMyself { get; set; }
24 |
25 | ///
26 | /// Certificate store configuration for own, trusted peer, issuer and rejected stores.
27 | ///
28 | public string OpcOwnPKIRootDefault { get; } = "pki";
29 | public string OpcOwnCertStoreType { get; set; } = CertificateStoreType.Directory;
30 | public string OpcOwnCertDirectoryStorePathDefault => Path.Combine(OpcOwnPKIRootDefault, "own");
31 | public string OpcOwnCertX509StorePathDefault => "CurrentUser\\UA_MachineDefault";
32 | public string OpcOwnCertStorePath { get; set; }
33 | public string OpcTrustedCertDirectoryStorePathDefault => Path.Combine(OpcOwnPKIRootDefault, "trusted");
34 | public string OpcTrustedCertStorePath { get; set; }
35 |
36 | public string OpcRejectedCertDirectoryStorePathDefault => Path.Combine(OpcOwnPKIRootDefault, "rejected");
37 | public string OpcRejectedCertStorePath { get; set; }
38 |
39 | public string OpcIssuerCertDirectoryStorePathDefault => Path.Combine(OpcOwnPKIRootDefault, "issuer");
40 | public string OpcIssuerCertStorePath { get; set; }
41 |
42 | ///
43 | /// Accept certs of the clients automatically.
44 | ///
45 | public bool AutoAcceptCerts { get; set; }
46 |
47 | ///
48 | /// Don't reject chain validation with CA certs with unknown revocation status,
49 | /// e.g. when the CRL is not available or the OCSP provider is offline.
50 | /// The default value is , so rejection is enabled.
51 | ///
52 | public bool DontRejectUnknownRevocationStatus { get; set; }
53 |
54 | ///
55 | /// Show CSR information during startup.
56 | ///
57 | public bool ShowCreateSigningRequestInfo { get; set; }
58 |
59 | ///
60 | /// Update application certificate.
61 | ///
62 | public string NewCertificateBase64String { get; set; }
63 | public string NewCertificateFileName { get; set; }
64 | public string CertificatePassword { get; set; } = string.Empty;
65 |
66 | ///
67 | /// If there is no application cert installed we need to install the private key as well.
68 | ///
69 | public string PrivateKeyBase64String { get; set; }
70 | public string PrivateKeyFileName { get; set; }
71 |
72 | ///
73 | /// Issuer certificates to add.
74 | ///
75 | public List IssuerCertificateBase64Strings { get; set; }
76 | public List IssuerCertificateFileNames { get; set; }
77 |
78 | ///
79 | /// Trusted certificates to add.
80 | ///
81 | public List TrustedCertificateBase64Strings { get; set; }
82 | public List TrustedCertificateFileNames { get; set; }
83 |
84 | ///
85 | /// CRL to update/install.
86 | ///
87 | public string CrlFileName { get; set; }
88 | public string CrlBase64String { get; set; }
89 |
90 | ///
91 | /// Thumbprint of certificates to delete.
92 | ///
93 | public List ThumbprintsToRemove { get; set; }
94 |
95 | ///
96 | /// Additional certificate DNS names.
97 | ///
98 | public List DnsNames { get; set; } = new();
99 | }
100 |
--------------------------------------------------------------------------------
/src/DeterministicAlarms/Configuration/Alarm.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.DeterministicAlarms.Configuration;
2 |
3 | public class Alarm
4 | {
5 | public AlarmObjectStates ObjectType { get; set; }
6 |
7 | public string Name { get; set; }
8 |
9 | public string Id { get; set; }
10 | }
11 |
--------------------------------------------------------------------------------
/src/DeterministicAlarms/Configuration/AlarmObjectStates.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.DeterministicAlarms.Configuration;
2 |
3 | public enum AlarmObjectStates
4 | {
5 | TripAlarmType,
6 | OffNormalAlarmType,
7 | AlarmConditionType,
8 | LimitAlarmType,
9 | ConditionType
10 | }
11 |
--------------------------------------------------------------------------------
/src/DeterministicAlarms/Configuration/ConditionStates.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.DeterministicAlarms.Configuration;
2 |
3 | public enum ConditionStates
4 | {
5 | Enabled,
6 | Activated
7 | }
8 |
--------------------------------------------------------------------------------
/src/DeterministicAlarms/Configuration/Configuration.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.DeterministicAlarms.Configuration;
2 |
3 | using System.Collections.Generic;
4 | using System.Text.Json;
5 | using System.Text.Json.Serialization;
6 |
7 | public class Configuration
8 | {
9 | static readonly JsonSerializerOptions _fromJsonOptions = new JsonSerializerOptions {
10 | ReadCommentHandling = JsonCommentHandling.Skip,
11 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
12 | Converters =
13 | {
14 | new JsonStringEnumConverter(),
15 | },
16 | };
17 |
18 | static readonly JsonSerializerOptions _toJsonOptions = new JsonSerializerOptions {
19 | WriteIndented = true,
20 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
21 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
22 | Converters =
23 | {
24 | new JsonStringEnumConverter()
25 | }
26 | };
27 |
28 | public List Folders { get; set; }
29 |
30 | public Script Script { get; set; }
31 |
32 | public string ToJson()
33 | {
34 | return JsonSerializer.Serialize(this, _toJsonOptions);
35 | }
36 |
37 | public static Configuration FromJson(string json)
38 | {
39 | return JsonSerializer.Deserialize(json, _fromJsonOptions);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/DeterministicAlarms/Configuration/Event.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.DeterministicAlarms.Configuration;
2 |
3 | using Opc.Ua;
4 | using System.Collections.Generic;
5 |
6 | public class Event
7 | {
8 | public string AlarmId { get; set; }
9 |
10 | public string Reason { get; set; }
11 |
12 | public EventSeverity Severity { get; set; }
13 |
14 | public string EventId { get; set; }
15 |
16 | public List StateChanges { get; set; }
17 | }
18 |
--------------------------------------------------------------------------------
/src/DeterministicAlarms/Configuration/Folder.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.DeterministicAlarms.Configuration;
2 |
3 | using System.Collections.Generic;
4 |
5 | public class Folder
6 | {
7 | public string Name { get; set; }
8 |
9 | public List Sources { get; set; }
10 | }
11 |
--------------------------------------------------------------------------------
/src/DeterministicAlarms/Configuration/Script.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.DeterministicAlarms.Configuration;
2 |
3 | using System.Collections.Generic;
4 |
5 | public class Script
6 | {
7 | public int WaitUntilStartInSeconds { get; set; }
8 |
9 | public bool IsScriptInRepeatingLoop { get; set; }
10 |
11 | public int RunningForSeconds { get; set; }
12 |
13 | public List Steps { get; set; }
14 | }
15 |
--------------------------------------------------------------------------------
/src/DeterministicAlarms/Configuration/ScriptException.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.DeterministicAlarms.Configuration;
2 |
3 | using System;
4 |
5 | #nullable enable
6 | public class ScriptException(string? message) : Exception(message)
7 | {
8 | }
9 |
--------------------------------------------------------------------------------
/src/DeterministicAlarms/Configuration/Source.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.DeterministicAlarms.Configuration;
2 |
3 | using System.Collections.Generic;
4 |
5 | public class Source
6 | {
7 | public SourceObjectState ObjectType { get; set; }
8 |
9 | public string Name { get; set; }
10 |
11 | public List Alarms { get; set; }
12 | }
13 |
--------------------------------------------------------------------------------
/src/DeterministicAlarms/Configuration/SourceObjectState.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.DeterministicAlarms.Configuration;
2 |
3 | public enum SourceObjectState
4 | {
5 | BaseObjectState
6 | }
7 |
--------------------------------------------------------------------------------
/src/DeterministicAlarms/Configuration/StateChange.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.DeterministicAlarms.Configuration;
2 |
3 | public class StateChange
4 | {
5 | public ConditionStates StateType { get; set; }
6 |
7 | public bool State { get; set; }
8 | }
9 |
--------------------------------------------------------------------------------
/src/DeterministicAlarms/Configuration/Step.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.DeterministicAlarms.Configuration;
2 |
3 | public class Step
4 | {
5 | public @Event Event { get; set; }
6 |
7 | public int SleepInSeconds { get; set; }
8 | }
9 |
--------------------------------------------------------------------------------
/src/DeterministicAlarms/Model/SimConditionStatesEnum.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.DeterministicAlarms.Model;
2 |
3 | using System;
4 |
5 | [Flags]
6 | public enum SimConditionStatesEnum
7 | {
8 | ///
9 | /// The condition state is unknown.
10 | ///
11 | Undefined = 0x0,
12 |
13 | ///
14 | /// The condition is enabled and will produce events.
15 | ///
16 | Enabled = 0x1,
17 |
18 | ///
19 | /// The condition requires acknowledgement by the user.
20 | ///
21 | Acknowledged = 0x2,
22 |
23 | ///
24 | /// The condition requires that the used confirm that action was taken.
25 | ///
26 | Confirmed = 0x4,
27 |
28 | ///
29 | /// The condition is active.
30 | ///
31 | Active = 0x8,
32 |
33 | ///
34 | /// The condition has been suppressed by the system.
35 | ///
36 | Suppressed = 0x10,
37 |
38 | ///
39 | /// The condition has been shelved by the user.
40 | ///
41 | Shelved = 0x20,
42 |
43 | /////
44 | ///// The condition has exceeded the high-high limit.
45 | /////
46 | //HighHigh = 0x40,
47 |
48 | /////
49 | ///// The condition has exceeded the high limit.
50 | /////
51 | //High = 0x80,
52 |
53 | /////
54 | ///// The condition has exceeded the low limit.
55 | /////
56 | //Low = 0x100,
57 |
58 | /////
59 | ///// The condition has exceeded the low-low limit.
60 | /////
61 | //LowLow = 0x200,
62 |
63 | /////
64 | ///// A mask used to clear all limit bits.
65 | /////
66 | //Limits = 0x3C0,
67 |
68 | ///
69 | /// The condition has deleted.
70 | ///
71 | Deleted = 0x400
72 | }
73 |
--------------------------------------------------------------------------------
/src/DeterministicAlarms/Model/SimFolderState.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.DeterministicAlarms.Model;
2 |
3 | using Opc.Ua;
4 |
5 | public class SimFolderState : FolderState
6 | {
7 | public SimFolderState(ISystemContext context, NodeState parent, NodeId nodeId, string name) : base(parent)
8 | {
9 | Initialize(context);
10 |
11 | // initialize the area with the fixed metadata.
12 | SymbolicName = name;
13 | NodeId = nodeId;
14 | BrowseName = new QualifiedName(name, nodeId.NamespaceIndex);
15 | DisplayName = BrowseName.Name;
16 | Description = null;
17 | ReferenceTypeId = ReferenceTypeIds.HasNotifier;
18 | TypeDefinitionId = ObjectTypeIds.FolderType;
19 | EventNotifier = EventNotifiers.SubscribeToEvents;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/DeterministicAlarms/ScriptEngine.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.DeterministicAlarms;
2 |
3 | using OpcPlc.DeterministicAlarms.Configuration;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Timers;
7 |
8 | public class ScriptEngine
9 | {
10 | public delegate void NextScriptStepAvailable(Step step, long numberOfLoops);
11 |
12 | public NextScriptStepAvailable OnNextScriptStepAvailable;
13 |
14 | private LinkedList _steps;
15 | private LinkedListNode _currentStep;
16 | private ITimer _stepsTimer;
17 | private readonly Script _script;
18 | private long _numberOfLoops = 1;
19 | private DateTime _scriptStopTime;
20 | private readonly TimeService _timeService;
21 |
22 | ///
23 | /// Initialize ScriptEngine
24 | ///
25 | ///
26 | ///
27 | ///
28 | public ScriptEngine(Script script, NextScriptStepAvailable scriptCallback, TimeService timeService)
29 | {
30 | OnNextScriptStepAvailable += scriptCallback ?? throw new ScriptException("Script Callback is not defined");
31 |
32 | _script = script;
33 | _timeService = timeService;
34 |
35 | CreateLinkedList(script.Steps);
36 |
37 | StartScript();
38 | }
39 |
40 | private void StartScript()
41 | {
42 | _stepsTimer = _timeService.NewTimer(OnStepTimedEvent, Convert.ToUInt32(_script.WaitUntilStartInSeconds * 1000));
43 | _scriptStopTime = _timeService.Now().AddSeconds(_script.RunningForSeconds + _script.WaitUntilStartInSeconds);
44 | }
45 |
46 | private void StopScript()
47 | {
48 | _stepsTimer.Close();
49 | _stepsTimer = null;
50 | }
51 |
52 | ///
53 | /// Create the Linked List that will be used internally to go through the steps
54 | ///
55 | ///
56 | private void CreateLinkedList(List steps)
57 | {
58 | _steps = new LinkedList();
59 | foreach (var step in steps)
60 | {
61 | _steps.AddLast(step);
62 | }
63 | }
64 |
65 | ///
66 | /// Active a new step
67 | ///
68 | ///
69 | private void ActivateCurrentStep(LinkedListNode step)
70 | {
71 | _currentStep = step;
72 | OnNextScriptStepAvailable?.Invoke(step?.Value, _numberOfLoops);
73 | if (_stepsTimer != null)
74 | {
75 | _stepsTimer.Interval = Math.Max(1, step.Value.SleepInSeconds * 1000);
76 | }
77 | }
78 |
79 | ///
80 | /// Get the next step
81 | ///
82 | ///
83 | ///
84 | private LinkedListNode GetNextValue(LinkedListNode step)
85 | {
86 | // Script should end because it has been executed as long as expected in the parameter
87 | // RunningForSeconds
88 | if (_scriptStopTime < _timeService.Now())
89 | {
90 | StopScript();
91 | return null;
92 | }
93 |
94 | // Is it the first step?
95 | if (step == null)
96 | {
97 | return _steps.First;
98 | }
99 |
100 | // Do we have a next step?
101 | if (step.Next != null)
102 | {
103 | return step.Next;
104 | }
105 |
106 | // We don't have a next step, now we should see if we should repeat
107 | // and start on first step again or terminate.
108 | if (_script.IsScriptInRepeatingLoop)
109 | {
110 | _numberOfLoops++;
111 | return _steps.First;
112 | }
113 |
114 | StopScript();
115 | return null;
116 | }
117 |
118 | ///
119 | /// Trigger when next step should be executed
120 | ///
121 | ///
122 | ///
123 | private void OnStepTimedEvent(Object source, ElapsedEventArgs e)
124 | {
125 | ActivateCurrentStep(GetNextValue(_currentStep));
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/DeterministicAlarms/SimBackend/SimAlarmStateBackend.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.DeterministicAlarms.SimBackend;
2 |
3 | using Opc.Ua;
4 | using OpcPlc.DeterministicAlarms.Configuration;
5 | using OpcPlc.DeterministicAlarms.Model;
6 | using System;
7 |
8 | public class SimAlarmStateBackend
9 | {
10 | public string Name { get; internal set; }
11 |
12 | public AlarmObjectStates AlarmType { get; set; }
13 |
14 | public DateTime Time { get; internal set; }
15 |
16 | public string Reason { get; internal set; }
17 |
18 | public SimConditionStatesEnum State { get; internal set; }
19 |
20 | public LocalizedText Comment { get; internal set; }
21 |
22 | public string UserName { get; internal set; }
23 |
24 | public EventSeverity Severity { get; internal set; }
25 |
26 | public DateTime EnableTime { get; internal set; }
27 |
28 | public DateTime ActiveTime { get; internal set; }
29 |
30 | public SimSourceNodeBackend Source { get; internal set; }
31 |
32 | public string Id { get; internal set; }
33 |
34 | internal SimAlarmStateBackend CreateSnapshot()
35 | {
36 | return (SimAlarmStateBackend)MemberwiseClone();
37 | }
38 |
39 | internal bool SetStateBits(SimConditionStatesEnum bits, bool isSet)
40 | {
41 | if (isSet)
42 | {
43 | bool currentlySet = ((State & bits) == bits);
44 | State |= bits;
45 | return !currentlySet;
46 | }
47 |
48 | bool currentlyCleared = ((State & ~bits) == State);
49 | State &= ~bits;
50 | return !currentlyCleared;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/DeterministicAlarms/SimBackend/SimBackendService.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.DeterministicAlarms.SimBackend;
2 |
3 | using System.Collections.Generic;
4 | using OpcPlc.DeterministicAlarms.Configuration;
5 | using static OpcPlc.DeterministicAlarms.SimBackend.SimSourceNodeBackend;
6 |
7 | public class SimBackendService
8 | {
9 | private readonly object _lock = new object();
10 | public Dictionary SourceNodes = new Dictionary();
11 |
12 | public SimSourceNodeBackend CreateSourceNodeBackend(string name, List alarms, AlarmChangedEventHandler alarmChangeCallback)
13 | {
14 | SimSourceNodeBackend simSourceNodeBackend;
15 |
16 | lock (_lock)
17 | {
18 | simSourceNodeBackend = new SimSourceNodeBackend
19 | {
20 | Name = name,
21 | OnAlarmChanged = alarmChangeCallback
22 | };
23 |
24 | simSourceNodeBackend.CreateAlarms(alarms);
25 |
26 | SourceNodes[name] = simSourceNodeBackend;
27 | }
28 |
29 | return simSourceNodeBackend;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/DeterministicAlarms/SimBackend/SimSourceNodeBackend.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.DeterministicAlarms.SimBackend;
2 |
3 | using OpcPlc.DeterministicAlarms.Configuration;
4 | using System;
5 | using System.Collections.Generic;
6 |
7 | public class SimSourceNodeBackend
8 | {
9 | public Dictionary Alarms { get; set; } = new Dictionary();
10 |
11 | public string Name { get; internal set; }
12 |
13 | public AlarmChangedEventHandler OnAlarmChanged;
14 |
15 | public delegate void AlarmChangedEventHandler(SimAlarmStateBackend alarm);
16 |
17 | public void CreateAlarms(List alarmsFromConfiguration)
18 | {
19 | foreach (var alarmConfiguration in alarmsFromConfiguration)
20 | {
21 | var alarmStateBackend = new SimAlarmStateBackend
22 | {
23 | Source = this,
24 | Id = alarmConfiguration.Id,
25 | Name = alarmConfiguration.Name,
26 | Time = DateTime.UtcNow,
27 | AlarmType = alarmConfiguration.ObjectType
28 | };
29 |
30 | lock (Alarms)
31 | {
32 | Alarms.Add(alarmConfiguration.Id, alarmStateBackend);
33 | }
34 | }
35 | }
36 |
37 | internal void Refresh()
38 | {
39 | var snapshots = new List();
40 |
41 | lock (Alarms)
42 | {
43 | foreach (var alarm in Alarms.Values)
44 | {
45 | snapshots.Add(alarm.CreateSnapshot());
46 | }
47 | }
48 |
49 | foreach (var snapshotAlarm in snapshots)
50 | {
51 | OnAlarmChanged!.Invoke(snapshotAlarm);
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/Extensions/CancellationTokenExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Extensions;
2 |
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | public static class CancellationTokenExtensions
7 | {
8 | ///
9 | /// Extension method to await a cancellation token.
10 | ///
11 | ///
12 | ///
13 | public static Task WhenCanceled(this CancellationToken cancellationToken)
14 | {
15 | var tcs = new TaskCompletionSource();
16 | cancellationToken.Register(s => ((TaskCompletionSource)s).SetResult(true), tcs);
17 | return tcs.Task;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/FlatDirectoryCertificateStoreType.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Certs;
2 |
3 | using Opc.Ua;
4 |
5 | ///
6 | /// Defines type for .
7 | ///
8 | public sealed class FlatDirectoryCertificateStoreType : ICertificateStoreType
9 | {
10 | ///
11 | public ICertificateStore CreateStore()
12 | {
13 | return new FlatDirectoryCertificateStore();
14 | }
15 |
16 | ///
17 | public bool SupportsStorePath(string storePath)
18 | {
19 | return !string.IsNullOrEmpty(storePath) && storePath.StartsWith(FlatDirectoryCertificateStore.StoreTypePrefix);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Helpers/DeterministicGuid.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Helpers;
2 |
3 | using System;
4 |
5 | public class DeterministicGuid
6 | {
7 | private readonly Random _rnd = new Random(1234); // Seeded (deterministic) random number generator.
8 |
9 | public Guid NewGuid()
10 | {
11 | // https://en.wikipedia.org/wiki/Universally_unique_identifier#Format
12 | // xxxxxxxx-xxxx-4xxx-[8|9|a|b]xxx-xxxxxxxxxxxx
13 | return new Guid($"{GetRandHexExp(0, 16, 8)}-{GetRandHexExp(0, 16, 4)}-{GetRandHex(16_384, 20_479, 4)}-{GetRandHex(32_768, 49_151, 4)}-{GetRandHexExp(0, 16, 12)}");
14 | }
15 |
16 | private string GetRandHexExp(long minIncl, int maxExclBase, int maxExclExponent)
17 | {
18 | return GetRandHex(minIncl, (long)Math.Pow(maxExclBase, maxExclExponent), maxExclExponent);
19 | }
20 |
21 | private string GetRandHex(long minIncl, long maxExcl, int digits)
22 | {
23 | return string.Format($"{{0:x{digits}}}", _rnd.NextInt64(minIncl, maxExcl));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Helpers/OtelHelper.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Helpers;
2 |
3 | using Opc.Ua;
4 | using OpenTelemetry;
5 | using OpenTelemetry.Exporter;
6 | using OpenTelemetry.Metrics;
7 | using OpenTelemetry.Resources;
8 | using OpenTelemetry.Trace;
9 |
10 | using System;
11 |
12 | public static class OtelHelper
13 | {
14 | public static void ConfigureOpenTelemetry(string serviceName, string exportEndpointUri, string exportProtocol, TimeSpan exportInterval)
15 | {
16 | _ = Sdk.CreateTracerProviderBuilder()
17 | .AddAspNetCoreInstrumentation()
18 | .AddSource(EndpointBase.ActivitySourceName)
19 | .SetResourceBuilder(ResourceBuilder.CreateDefault()
20 | .AddService(serviceName))
21 | .AddOtlpExporter(exporterOptions => {
22 | exporterOptions.Endpoint = new Uri(exportEndpointUri);
23 | exporterOptions.Protocol = exportProtocol == "protobuf"
24 | ? OtlpExportProtocol.HttpProtobuf
25 | : OtlpExportProtocol.Grpc;
26 | exporterOptions.BatchExportProcessorOptions.ExporterTimeoutMilliseconds = (int)exportInterval.TotalMilliseconds;
27 | })
28 | .Build();
29 |
30 | _ = Sdk.CreateMeterProviderBuilder()
31 | .SetResourceBuilder(ResourceBuilder.CreateDefault()
32 | .AddService(MetricsHelper.ServiceName).AddTelemetrySdk())
33 | .AddMeter(MetricsHelper.Meter.Name)
34 | .AddRuntimeInstrumentation()
35 | .AddOtlpExporter((exporterOptions, metricsReaderOptions) => {
36 | exporterOptions.Endpoint = new Uri(exportEndpointUri);
37 | exporterOptions.Protocol = exportProtocol == "protobuf"
38 | ? OtlpExportProtocol.HttpProtobuf
39 | : OtlpExportProtocol.Grpc;
40 |
41 | metricsReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Cumulative;
42 | metricsReaderOptions.PeriodicExportingMetricReaderOptions = new PeriodicExportingMetricReaderOptions {
43 | ExportIntervalMilliseconds = (int?)exportInterval.TotalMilliseconds
44 | };
45 | })
46 | .Build();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Helpers/PluginNodesHelper.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace OpcPlc.Helpers;
3 |
4 | using Opc.Ua;
5 | using OpcPlc.PluginNodes.Models;
6 | using System;
7 |
8 | ///
9 | /// Helper class for plugin nodes.
10 | ///
11 | public static class PluginNodesHelper
12 | {
13 | ///
14 | /// Get node with correct type prefix, id, namespace, and intervals.
15 | ///
16 | public static NodeWithIntervals GetNodeWithIntervals(NodeId nodeId, PlcNodeManager plcNodeManager)
17 | {
18 | ExpandedNodeId expandedNodeId = NodeId.ToExpandedNodeId(nodeId, plcNodeManager.Server.NamespaceUris);
19 |
20 | return new NodeWithIntervals
21 | {
22 | NodeId = expandedNodeId.IdType == IdType.Opaque
23 | ? Convert.ToBase64String((byte[])expandedNodeId.Identifier)
24 | : expandedNodeId.Identifier.ToString(),
25 | NodeIdTypePrefix = GetTypePrefix(expandedNodeId.IdType),
26 | Namespace = expandedNodeId.NamespaceUri,
27 | };
28 | }
29 |
30 | ///
31 | /// Get a single lowercase character that represents the type of the node.
32 | ///
33 | ///
34 | private static string GetTypePrefix(IdType idType)
35 | {
36 | return idType switch
37 | {
38 | IdType.Numeric => "i",
39 | IdType.String => "s",
40 | IdType.Guid => "g",
41 | IdType.Opaque => "b",
42 | _ => throw new ArgumentOutOfRangeException(nameof(idType), idType.ToString(), message: null),
43 | };
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Helpers/PnJsonHelper.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Helpers;
2 |
3 | using Microsoft.Extensions.Logging;
4 | using OpcPlc.PluginNodes.Models;
5 | using System;
6 | using System.Collections.Immutable;
7 | using System.IO;
8 | using System.Text;
9 | using System.Text.Encodings.Web;
10 | using System.Text.Json;
11 | using System.Threading.Tasks;
12 |
13 | public static class PnJsonHelper
14 | {
15 | ///
16 | /// Show and save pn.json
17 | ///
18 | public static async Task PrintPublisherConfigJsonAsync(string pnJsonFileName, string serverPath, bool useSecurity, ImmutableList pluginNodes, ILogger logger)
19 | {
20 | var sb = new StringBuilder();
21 |
22 | sb.AppendLine(Environment.NewLine + "[");
23 | sb.AppendLine(" {");
24 | sb.AppendLine($" \"EndpointUrl\": \"opc.tcp://{serverPath}\",");
25 | sb.AppendLine($" \"UseSecurity\": {(useSecurity).ToString().ToLowerInvariant()},");
26 | sb.AppendLine(" \"OpcNodes\": [");
27 |
28 | // Print config from plugin nodes list.
29 | foreach (var plugin in pluginNodes)
30 | {
31 | foreach (var node in plugin.Nodes)
32 | {
33 | // Show only if > 0 and != 1000 ms.
34 | string publishingInterval = node.PublishingInterval is > 0 and not 1000
35 | ? $", \"OpcPublishingInterval\": {node.PublishingInterval}"
36 | : string.Empty;
37 | // Show only if > 0 ms.
38 | string samplingInterval = node.SamplingInterval > 0
39 | ? $", \"OpcSamplingInterval\": {node.SamplingInterval}"
40 | : string.Empty;
41 |
42 | string nodeId = JsonEncodedText.Encode(node.NodeId, JavaScriptEncoder.Default).ToString();
43 | sb.AppendLine($" {{ \"Id\": \"nsu={node.Namespace};{node.NodeIdTypePrefix}={nodeId}\"{publishingInterval}{samplingInterval} }},");
44 | }
45 | }
46 |
47 | int trimLen = Environment.NewLine.Length + 1;
48 | sb.Remove(sb.Length - trimLen, trimLen); // Trim trailing ,\n.
49 |
50 | sb.AppendLine(Environment.NewLine + " ]");
51 | sb.AppendLine(" }");
52 | sb.AppendLine("]");
53 |
54 | string pnJson = sb.ToString();
55 | logger.LogInformation("OPC Publisher configuration file: {PnJsonFile}", $"{pnJsonFileName}{pnJson}");
56 |
57 | await File.WriteAllTextAsync(pnJsonFileName, pnJson.Trim()).ConfigureAwait(false);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Logging/LoggingProvider.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------
2 | // Copyright (c) Microsoft Corporation. All rights reserved.
3 | // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
4 | // ------------------------------------------------------------
5 |
6 | namespace OpcPlc.Logging;
7 |
8 | using Microsoft.Extensions.Logging;
9 |
10 | ///
11 | /// Provides utility for creating logger factory.
12 | ///
13 | public static class LoggingProvider
14 | {
15 | ///
16 | /// Create ILoggerFactory object with default configuration.
17 | ///
18 | public static ILoggerFactory CreateDefaultLoggerFactory(LogLevel level)
19 | {
20 | var loggerFactory = LoggerFactory.Create(builder =>
21 | {
22 | builder.AddConsole(options => options.FormatterName = nameof(SyslogFormatter))
23 | .SetMinimumLevel(level)
24 | .AddConsoleFormatter<
25 | SyslogFormatter,
26 | SyslogFormatterOptions>();
27 | });
28 |
29 | return loggerFactory;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Logging/SyslogFormatter.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------
2 | // Copyright (c) Microsoft Corporation. All rights reserved.
3 | // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
4 | // ------------------------------------------------------------
5 |
6 | namespace OpcPlc.Logging;
7 |
8 | using Microsoft.Extensions.Logging;
9 | using Microsoft.Extensions.Logging.Abstractions;
10 | using Microsoft.Extensions.Logging.Console;
11 | using Microsoft.Extensions.Options;
12 | using System;
13 | using System.Globalization;
14 | using System.IO;
15 | using System.Text;
16 |
17 | ///
18 | /// Logging formatter compatible with syslogs format.
19 | ///
20 | public sealed class SyslogFormatter : ConsoleFormatter, IDisposable
21 | {
22 | private const int _initialLength = 256;
23 |
24 | ///
25 | /// Map of to syslog severity.
26 | ///
27 | private static readonly string[] _syslogMap = [
28 | /* Trace */ "<7>",
29 | /* Debug */ "<7>",
30 | /* Info */ "<6>",
31 | /* Warn */ "<4>",
32 | /* Error */ "<3>",
33 | /* Crit */ "<3>",
34 | ];
35 |
36 | private readonly IDisposable _optionsReloadToken;
37 |
38 | private string _timestampFormat;
39 | private string _serviceId;
40 | private bool _includeScopes;
41 |
42 | private SyslogFormatterOptions _options;
43 |
44 | ///
45 | /// Initializes a new instance of the class.
46 | ///
47 | public SyslogFormatter(SyslogFormatterOptions options)
48 | : base(nameof(SyslogFormatter))
49 | {
50 | _optionsReloadToken = null;
51 | _serviceId = options.ServiceId;
52 | _timestampFormat = options.TimestampFormat ?? SyslogFormatterOptions.DefaultTimestampFormat;
53 | _includeScopes = options.IncludeScopes;
54 | }
55 |
56 | ///
57 | /// Initializes a new instance of the class.
58 | ///
59 | public SyslogFormatter(IOptionsMonitor options)
60 | : base(nameof(SyslogFormatter))
61 | {
62 | _optionsReloadToken = options.OnChange(opt =>
63 | {
64 | _options = opt;
65 | _serviceId = opt.ServiceId;
66 | _timestampFormat = opt.TimestampFormat ?? SyslogFormatterOptions.DefaultTimestampFormat;
67 | _includeScopes = opt.IncludeScopes;
68 | });
69 | _options = options.CurrentValue;
70 | _serviceId = _options.ServiceId;
71 | _timestampFormat = _options.TimestampFormat ?? SyslogFormatterOptions.DefaultTimestampFormat;
72 | _includeScopes = _options.IncludeScopes;
73 | }
74 |
75 | ///
76 | public override void Write(
77 | in LogEntry logEntry,
78 | IExternalScopeProvider scopeProvider,
79 | TextWriter textWriter)
80 | {
81 | string message =
82 | logEntry.Formatter?.Invoke(
83 | logEntry.State, logEntry.Exception);
84 |
85 | if (message is null)
86 | {
87 | return;
88 | }
89 |
90 | var messageBuilder = new StringBuilder(_initialLength);
91 | messageBuilder.Append(_syslogMap[(int)logEntry.LogLevel]);
92 | messageBuilder.Append(DateTime.UtcNow.ToString(_timestampFormat, CultureInfo.InvariantCulture));
93 |
94 | if (_includeScopes && scopeProvider != null && !string.IsNullOrEmpty(_serviceId))
95 | {
96 | bool structuredData = false;
97 | scopeProvider.ForEachScope(
98 | (scope, state) =>
99 | {
100 | StringBuilder builder = state;
101 | if (!structuredData)
102 | {
103 | messageBuilder.Append('[');
104 | messageBuilder.Append(_serviceId);
105 | structuredData = true;
106 | }
107 |
108 | builder.Append(' ').Append(scope);
109 | },
110 | messageBuilder);
111 | if (structuredData)
112 | {
113 | messageBuilder.Append("] ");
114 | }
115 | }
116 |
117 | messageBuilder.Append("- ");
118 | messageBuilder.AppendLine(message);
119 |
120 | if (logEntry.Exception != null)
121 | {
122 | // TODO: syslog format does not support stack traces
123 | messageBuilder.AppendLine(logEntry.Exception.ToString());
124 | }
125 |
126 | textWriter.Write(messageBuilder.ToString());
127 | }
128 |
129 | ///
130 | public void Dispose()
131 | {
132 | _optionsReloadToken?.Dispose();
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/Logging/SyslogFormatterOptions.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------
2 | // Copyright (c) Microsoft Corporation. All rights reserved.
3 | // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
4 | // ------------------------------------------------------------
5 |
6 | namespace OpcPlc.Logging;
7 |
8 | using Microsoft.Extensions.Logging.Console;
9 |
10 | ///
11 | /// Options for log formatter.
12 | ///
13 | public sealed class SyslogFormatterOptions : ConsoleFormatterOptions
14 | {
15 | ///
16 | /// The default timestamp format for all IoT compatible logging events..
17 | ///
18 | public static readonly string DefaultTimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ ";
19 |
20 | ///
21 | /// Initializes a new instance of the class.
22 | ///
23 | public SyslogFormatterOptions()
24 | {
25 | ServiceId = "opcua@311";
26 | UseUtcTimestamp = true;
27 | IncludeScopes = true;
28 | TimestampFormat = DefaultTimestampFormat;
29 | }
30 |
31 | ///
32 | /// Gets the service id which is added to the syslog output, e.g. 'service@311'.
33 | /// see https://www.iana.org/assignments/enterprise-numbers/?q=microsoft for enterprise ids.
34 | ///
35 | public string ServiceId { get; }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Microsoft.IoTEdge.OpcPlc.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/ModelCompiler.cmd:
--------------------------------------------------------------------------------
1 | @echo off
2 | setlocal
3 | set modelName=%1
4 |
5 | REM If docker is not available, ensure that Opc.Ua.ModelCompiler.exe is in the PATH environment variable
6 | set MODELCOMPILER=Opc.Ua.ModelCompiler.exe
7 | set MODELCOMPILERIMAGE=ghcr.io/opcfoundation/ua-modelcompiler:latest
8 | set MODELROOT=.
9 |
10 | echo Pulling latest ModelCompiler from the GitHub container registry ...
11 | docker pull %MODELCOMPILERIMAGE%
12 | IF ERRORLEVEL 1 (
13 | :nodocker
14 | echo The docker command to download ModelCompiler failed, using local PATH instead
15 | ) ELSE (
16 | echo Successfully pulled the latest docker container for ModelCompiler
17 | set MODELROOT=/model
18 | set MODELCOMPILER=docker run -v "%CD%:/model" -it --rm --name ua-modelcompiler %MODELCOMPILERIMAGE%
19 | )
20 |
21 | echo:
22 | echo Building %modelName%.xml ...
23 | %MODELCOMPILER% compile -version v104 -d2 "%MODELROOT%/%modelName%.xml" -cg "%MODELROOT%/%modelName%.csv" -o2 "%MODELROOT%/"
24 |
25 | echo:
26 | IF ERRORLEVEL 1 (
27 | echo ModelCompiler failed!
28 | ) ELSE (
29 | echo ModelCompiler succeeded!
30 | )
--------------------------------------------------------------------------------
/src/ModelCompilerNodeSet2.cmd:
--------------------------------------------------------------------------------
1 | @echo off
2 | setlocal
3 | set modelName1=%1
4 | set namespace1=%2
5 | set prefix1=%3
6 | set modelName2=%4
7 | set namespace2=%5
8 | set prefix2=%6
9 |
10 | REM If docker is not available, ensure that Opc.Ua.ModelCompiler.exe is in the PATH environment variable
11 | set MODELCOMPILER=Opc.Ua.ModelCompiler.exe
12 | set MODELCOMPILERIMAGE=ghcr.io/opcfoundation/ua-modelcompiler:latest
13 | REM private build until the official image is updated
14 | set MODELCOMPILERIMAGE=ghcr.io/mregen/ua-modelcompiler:latest-docker-nodeset2
15 | set MODELROOT=.
16 |
17 | echo Pulling latest ModelCompiler from the GitHub container registry ...
18 | docker pull %MODELCOMPILERIMAGE%
19 | IF ERRORLEVEL 1 (
20 | :nodocker
21 | echo The docker command to download ModelCompiler failed, using local PATH instead
22 | ) ELSE (
23 | echo Successfully pulled the latest docker container for ModelCompiler
24 | set MODELROOT=/model
25 | set MODELCOMPILER=docker run -v "%CD%:/model" -it --rm --name ua-modelcompiler %MODELCOMPILERIMAGE%
26 | )
27 |
28 | rem filename1
29 | for %%A in ("%modelName1%") do (
30 | set "filename1=%%~nxA"
31 | )
32 | rem filename2
33 | for %%A in ("%modelName2%") do (
34 | set "filename2=%%~nxA"
35 | )
36 |
37 | rem Display the results
38 | echo Model1 Filename : %filename1%
39 | echo Model2 Filename : %filename2%
40 | mkdir temp
41 |
42 | IF "%modelName2%" == "" (
43 | echo Building Nodeset2 %modelName1%.xml,%namespace1%,%prefix1% ...
44 | COPY %modelName1%.xml temp
45 | %MODELCOMPILER% compile -version v104 -id 1000 -d2 "%MODELROOT%/temp/%filename1%.xml,%namespace1%,%prefix1%" -o2 "%MODELROOT%/"
46 | ) ELSE (
47 | COPY %modelName1%.xml temp
48 | COPY %modelName2%.xml temp
49 | echo Building Nodeset2 %modelName1%.xml,%namespace1%,%prefix1% %modelName2%.xml,%namespace2%,%prefix2%...
50 | %MODELCOMPILER% compile -version v104 -d2 "%MODELROOT%/temp/%filename1%.xml,%namespace1%,%prefix1%" -d2 "%MODELROOT%/temp/%filename2%.xml,%namespace2%,%prefix2%" -o2 "%MODELROOT%/"
51 | )
52 |
53 | rmdir /s/q temp
54 |
55 | echo:
56 | IF ERRORLEVEL 1 (
57 | echo ModelCompiler failed!
58 | ) ELSE (
59 | echo ModelCompiler succeeded!
60 | )
--------------------------------------------------------------------------------
/src/NamespaceType.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc;
2 |
3 | public enum NamespaceType
4 | {
5 | OpcPlcApplications,
6 | Boiler,
7 | BoilerInstance,
8 | }
9 |
--------------------------------------------------------------------------------
/src/Namespaces.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc;
2 |
3 | ///
4 | /// Defines constants for namespaces used by the application.
5 | ///
6 | public static partial class Namespaces
7 | {
8 | ///
9 | /// The namespace for the nodes provided by for boiler type.
10 | ///
11 | public const string OpcPlcBoiler = "http://microsoft.com/Opc/OpcPlc/Boiler";
12 |
13 | ///
14 | /// The namespace for the nodes provided by the for the boiler instance.
15 | ///
16 | public const string OpcPlcBoilerInstance = "http://microsoft.com/Opc/OpcPlc/BoilerInstance";
17 |
18 | ///
19 | /// The namespace for the nodes provided by the plc server.
20 | ///
21 | public const string OpcPlcApplications = "http://microsoft.com/Opc/OpcPlc/";
22 |
23 | ///
24 | /// The namespace for the nodes provided by the plc server for simple events
25 | ///
26 | public const string OpcPlcSimpleEvents = "http://microsoft.com/Opc/OpcPlc/SimpleEvents";
27 |
28 | ///
29 | /// The namespace for the nodes provided by the plc server for simple events instance
30 | ///
31 | public const string OpcPlcSimpleEventsInstance = "http://microsoft.com/Opc/OpcPlc/SimpleEventsInstance";
32 |
33 | ///
34 | /// The namespace for the nodes provided by the plc server for alarm.
35 | ///
36 | public const string OpcPlcAlarms = "http://microsoft.com/Opc/OpcPlc/Alarms";
37 |
38 | ///
39 | /// The namespace for the nodes provided by the plc server for alarm instance.
40 | ///
41 | public const string OpcPlcAlarmsInstance = "http://microsoft.com/Opc/OpcPlc/AlarmsInstance";
42 |
43 | ///
44 | /// The namespace for the nodes provided by the plc server for simulation and test purposes.
45 | ///
46 | public const string OpcPlcReferenceTest = "http://microsoft.com/Opc/OpcPlc/ReferenceTest";
47 |
48 | ///
49 | /// The namespace for the nodes provided by the plc server for alarm instance.
50 | ///
51 | public const string OpcPlcDeterministicAlarmsInstance = "http://microsoft.com/Opc/OpcPlc/DetermAlarmsInstance";
52 |
53 | ///
54 | /// The namespace for DI nodes.
55 | ///
56 | public const string DI = "http://opcfoundation.org/UA/DI/";
57 | }
58 |
--------------------------------------------------------------------------------
/src/NodeType.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc;
2 |
3 | public enum NodeType
4 | {
5 | UInt,
6 | Double,
7 | Bool,
8 | UIntArray,
9 | }
10 |
--------------------------------------------------------------------------------
/src/PlcSimulation.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc;
2 |
3 | using Opc.Ua;
4 | using OpcPlc.PluginNodes.Models;
5 | using System.Collections.Immutable;
6 | using System.Diagnostics;
7 | using System.Timers;
8 |
9 | public class PlcSimulation
10 | {
11 | private readonly ImmutableList _pluginNodes;
12 |
13 | public PlcSimulation(ImmutableList pluginNodes)
14 | {
15 | _pluginNodes = pluginNodes;
16 | }
17 |
18 | ///
19 | /// Flags for node generation.
20 | ///
21 |
22 | // alm|alarms
23 | // add alarm simulation to address space.
24 | public bool AddAlarmSimulation { get; set; }
25 |
26 | // ses|simpleevents
27 | // Add simple events simulation to address space.
28 | public bool AddSimpleEventsSimulation { get; set; }
29 |
30 | // ref|referencetest
31 | // Add reference test simulation node manager to address space.
32 | public bool AddReferenceTestSimulation { get; set; } = true;
33 | public string DeterministicAlarmSimulationFile { get; set; }
34 |
35 | public uint EventInstanceCount { get; set; }
36 | public uint EventInstanceRate { get; set; } = 1000; // ms.
37 |
38 | ///
39 | /// Simulation data.
40 | ///
41 | public int SimulationCycleCount { get; set; } = SIMULATION_CYCLECOUNT_DEFAULT;
42 | public int SimulationCycleLength { get; set; } = SIMULATION_CYCLELENGTH_DEFAULT;
43 |
44 | ///
45 | /// Start the simulation.
46 | ///
47 | public void Start(PlcServer plcServer)
48 | {
49 | _plcServer = plcServer;
50 |
51 | if (EventInstanceCount > 0)
52 | {
53 | _eventInstanceGenerator = EventInstanceRate >= 50 || !Stopwatch.IsHighResolution
54 | ? _plcServer.TimeService.NewTimer(UpdateEventInstances, intervalInMilliseconds: EventInstanceRate)
55 | : _plcServer.TimeService.NewFastTimer(UpdateVeryFastEventInstances, intervalInMilliseconds: EventInstanceRate);
56 | }
57 |
58 | // Start simulation of nodes from plugin nodes list.
59 | foreach (var plugin in _pluginNodes)
60 | {
61 | plugin.StartSimulation();
62 | }
63 | }
64 |
65 | ///
66 | /// Stop the simulation.
67 | ///
68 | public void Stop()
69 | {
70 | Disable(_eventInstanceGenerator);
71 |
72 | // Stop simulation of nodes from plugin nodes list.
73 | foreach (var plugin in _pluginNodes)
74 | {
75 | plugin.StopSimulation();
76 | }
77 | }
78 |
79 | private void UpdateEventInstances(object state, ElapsedEventArgs elapsedEventArgs)
80 | {
81 | UpdateEventInstances();
82 | }
83 |
84 | private void UpdateEventInstances()
85 | {
86 | uint eventInstanceCycle = _eventInstanceCycle++;
87 |
88 | for (uint i = 0; i < EventInstanceCount; i++)
89 | {
90 | var e = new BaseEventState(null);
91 | var info = new TranslationInfo(
92 | "EventInstanceCycleEventKey",
93 | locale: string.Empty, // Invariant.
94 | "Event with index '{0}' and event cycle '{1}'",
95 | i, eventInstanceCycle);
96 |
97 | e.Initialize(
98 | _plcServer.PlcNodeManager.SystemContext,
99 | source: null,
100 | EventSeverity.Medium,
101 | new LocalizedText(info));
102 |
103 | e.SetChildValue(_plcServer.PlcNodeManager.SystemContext, BrowseNames.SourceName, "System", false);
104 | e.SetChildValue(_plcServer.PlcNodeManager.SystemContext, BrowseNames.SourceNode, ObjectIds.Server, false);
105 |
106 | _plcServer.PlcNodeManager.Server.ReportEvent(e);
107 | }
108 | }
109 |
110 | private void UpdateVeryFastEventInstances(object state, FastTimerElapsedEventArgs elapsedEventArgs)
111 | {
112 | UpdateEventInstances();
113 | }
114 |
115 | private void Disable(ITimer timer)
116 | {
117 | if (timer == null)
118 | {
119 | return;
120 | }
121 |
122 | timer.Enabled = false;
123 | }
124 |
125 | private const int SIMULATION_CYCLECOUNT_DEFAULT = 50; // in cycles
126 | private const int SIMULATION_CYCLELENGTH_DEFAULT = 100; // in msec
127 |
128 | private PlcServer _plcServer;
129 |
130 | private ITimer _eventInstanceGenerator;
131 | private uint _eventInstanceCycle;
132 | }
133 |
--------------------------------------------------------------------------------
/src/PluginNodes/ConfigNode.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.PluginNodes;
2 |
3 | using Newtonsoft.Json;
4 | using System.Collections.Generic;
5 | using System.ComponentModel;
6 |
7 | ///
8 | /// Defines the configuration folder, which holds the list of nodes.
9 | ///
10 | public class ConfigFolder
11 | {
12 | public string Folder { get; set; }
13 |
14 | public List FolderList { get; set; }
15 |
16 | public List NodeList { get; set; }
17 | }
18 |
19 | ///
20 | /// Used to define the node, which will be published by the server.
21 | ///
22 | public class ConfigNode
23 | {
24 | [JsonProperty(Required = Required.Always)]
25 | public dynamic NodeId { get; set; }
26 |
27 | public string Name { get; set; }
28 |
29 | [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
30 | [DefaultValue("Int32")]
31 | public string DataType { get; set; }
32 |
33 | [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
34 | [DefaultValue(-1)]
35 | public int ValueRank { get; set; }
36 |
37 | [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
38 | [DefaultValue("CurrentReadOrWrite")]
39 | public string AccessLevel { get; set; }
40 |
41 | public string Description { get; set; }
42 |
43 | public object Value { get; set; }
44 | }
45 |
--------------------------------------------------------------------------------
/src/PluginNodes/DeterministicGuidPluginNodes.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.PluginNodes;
2 |
3 | using Microsoft.Extensions.Logging;
4 | using Opc.Ua;
5 | using OpcPlc.Helpers;
6 | using OpcPlc.PluginNodes.Models;
7 | using System;
8 | using System.Collections.Generic;
9 |
10 | ///
11 | /// Nodes with deterministic GUIDs as ID.
12 | ///
13 | public class DeterministicGuidPluginNodes(TimeService timeService, ILogger logger) : PluginNodeBase(timeService, logger), IPluginNodes
14 | {
15 | private readonly DeterministicGuid _deterministicGuid = new ();
16 | private PlcNodeManager _plcNodeManager;
17 | private SimulatedVariableNode[] _nodes;
18 |
19 | private static uint NodeCount { get; set; } = 1;
20 | private uint NodeRate { get; set; } = 1000; // ms.
21 | private NodeType NodeType { get; set; } = NodeType.UInt;
22 |
23 | public void AddOptions(Mono.Options.OptionSet optionSet)
24 | {
25 | optionSet.Add(
26 | "gn|guidnodes=",
27 | $"number of nodes with deterministic GUID IDs.\nDefault: {NodeCount}",
28 | (uint i) => NodeCount = i);
29 | }
30 |
31 | public void AddToAddressSpace(FolderState telemetryFolder, FolderState methodsFolder, PlcNodeManager plcNodeManager)
32 | {
33 | _plcNodeManager = plcNodeManager;
34 |
35 | FolderState folder = _plcNodeManager.CreateFolder(
36 | telemetryFolder,
37 | path: "Deterministic GUID",
38 | name: "Deterministic GUID",
39 | NamespaceType.OpcPlcApplications);
40 |
41 | AddNodes(folder);
42 | }
43 |
44 | public void StartSimulation()
45 | {
46 | foreach (var node in _nodes)
47 | {
48 | node.Start(value => value + 1, periodMs: 1000);
49 | }
50 | }
51 |
52 | public void StopSimulation()
53 | {
54 | foreach (var node in _nodes)
55 | {
56 | node.Stop();
57 | }
58 | }
59 |
60 | private void AddNodes(FolderState folder)
61 | {
62 | _nodes = new SimulatedVariableNode[NodeCount];
63 | var nodes = new List((int)NodeCount);
64 |
65 | if (NodeCount > 0)
66 | {
67 | _logger.LogInformation($"Creating {NodeCount} GUID node(s) of type: {NodeType}");
68 | _logger.LogInformation($"Node values will change every {NodeRate} ms");
69 | }
70 |
71 | for (int i = 0; i < NodeCount; i++)
72 | {
73 | Guid id = _deterministicGuid.NewGuid();
74 |
75 | BaseDataVariableState variable = _plcNodeManager.CreateBaseVariable(
76 | folder,
77 | path: id,
78 | name: id.ToString(),
79 | new NodeId((uint)BuiltInType.UInt32),
80 | ValueRanks.Scalar,
81 | AccessLevels.CurrentReadOrWrite,
82 | "Constantly increasing value",
83 | NamespaceType.OpcPlcApplications,
84 | defaultValue: (uint)0);
85 |
86 | _nodes[i] = _plcNodeManager.CreateVariableNode(variable);
87 |
88 | // Add to node list for creation of pn.json.
89 | nodes.Add(PluginNodesHelper.GetNodeWithIntervals(variable.NodeId, _plcNodeManager));
90 | }
91 |
92 | Nodes = nodes;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/PluginNodes/DipPluginNode.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.PluginNodes;
2 |
3 | using Microsoft.Extensions.Logging;
4 | using Opc.Ua;
5 | using OpcPlc.Helpers;
6 | using OpcPlc.PluginNodes.Models;
7 | using System;
8 | using System.Collections.Generic;
9 |
10 | ///
11 | /// Node with a sine wave value with a dip anomaly.
12 | ///
13 | public class DipPluginNode(TimeService timeService, ILogger logger) : PluginNodeBase(timeService, logger), IPluginNodes
14 | {
15 | private bool _isEnabled = true;
16 | private PlcNodeManager _plcNodeManager;
17 | private SimulatedVariableNode _node;
18 | private readonly Random _random = new Random();
19 | private int _dipCycleInPhase;
20 | private int _dipAnomalyCycle;
21 | private const double SimulationMaxAmplitude = 100.0;
22 |
23 | public void AddOptions(Mono.Options.OptionSet optionSet)
24 | {
25 | optionSet.Add(
26 | "nd|nodips",
27 | $"do not generate dip data.\nDefault: {!_isEnabled}",
28 | (string s) => _isEnabled = s == null);
29 | }
30 |
31 | public void AddToAddressSpace(FolderState telemetryFolder, FolderState methodsFolder, PlcNodeManager plcNodeManager)
32 | {
33 | _plcNodeManager = plcNodeManager;
34 |
35 | if (_isEnabled)
36 | {
37 | FolderState folder = _plcNodeManager.CreateFolder(
38 | telemetryFolder,
39 | path: "Anomaly",
40 | name: "Anomaly",
41 | NamespaceType.OpcPlcApplications);
42 |
43 | AddNodes(folder);
44 | }
45 | }
46 |
47 | public void StartSimulation()
48 | {
49 | if (_isEnabled)
50 | {
51 | _dipCycleInPhase = _plcNodeManager.PlcSimulationInstance.SimulationCycleCount;
52 | _dipAnomalyCycle = _random.Next(_plcNodeManager.PlcSimulationInstance.SimulationCycleCount);
53 | _logger.LogTrace($"First dip anomaly cycle: {_dipAnomalyCycle}");
54 |
55 | _node.Start(DipGenerator, _plcNodeManager.PlcSimulationInstance.SimulationCycleLength);
56 | }
57 | }
58 |
59 | public void StopSimulation()
60 | {
61 | if (_isEnabled)
62 | {
63 | _node.Stop();
64 | }
65 | }
66 |
67 | private void AddNodes(FolderState folder)
68 | {
69 | BaseDataVariableState variable = _plcNodeManager.CreateBaseVariable(
70 | folder,
71 | path: "DipData",
72 | name: "DipData",
73 | new NodeId((uint)BuiltInType.Double),
74 | ValueRanks.Scalar,
75 | AccessLevels.CurrentRead,
76 | "Value with random dips",
77 | NamespaceType.OpcPlcApplications);
78 |
79 | _node = _plcNodeManager.CreateVariableNode(variable);
80 |
81 | // Add to node list for creation of pn.json.
82 | Nodes = new List
83 | {
84 | PluginNodesHelper.GetNodeWithIntervals(variable.NodeId, _plcNodeManager),
85 | };
86 | }
87 |
88 | ///
89 | /// Generates a sine wave with dips at a random cycle in the phase.
90 | /// Called each SimulationCycleLength msec.
91 | ///
92 | private double DipGenerator(double value)
93 | {
94 | // calculate next value
95 | double nextValue;
96 | if (_isEnabled && _dipCycleInPhase == _dipAnomalyCycle)
97 | {
98 | nextValue = SimulationMaxAmplitude * -10;
99 | _logger.LogTrace("Generate dip anomaly");
100 | }
101 | else
102 | {
103 | nextValue = SimulationMaxAmplitude * Math.Sin(((2 * Math.PI) / _plcNodeManager.PlcSimulationInstance.SimulationCycleCount) * _dipCycleInPhase);
104 | }
105 | _logger.LogTrace($"Spike cycle: {_dipCycleInPhase} data: {nextValue}");
106 |
107 | // end of cycle: reset cycle count and calc next anomaly cycle
108 | if (--_dipCycleInPhase == 0)
109 | {
110 | _dipCycleInPhase = _plcNodeManager.PlcSimulationInstance.SimulationCycleCount;
111 | _dipAnomalyCycle = _random.Next(_plcNodeManager.PlcSimulationInstance.SimulationCycleCount);
112 | _logger.LogTrace($"Next dip anomaly cycle: {_dipAnomalyCycle}");
113 | }
114 |
115 | return nextValue;
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/PluginNodes/LongIdPluginNode.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.PluginNodes;
2 |
3 | using Microsoft.Extensions.Logging;
4 | using Opc.Ua;
5 | using OpcPlc.Helpers;
6 | using OpcPlc.PluginNodes.Models;
7 | using System.Collections.Generic;
8 | using System.Text;
9 |
10 | ///
11 | /// Node with ID of 3950 chars.
12 | ///
13 | public class LongIdPluginNode(TimeService timeService, ILogger logger) : PluginNodeBase(timeService, logger), IPluginNodes
14 | {
15 | private PlcNodeManager _plcNodeManager;
16 | private SimulatedVariableNode _node;
17 |
18 | public void AddOptions(Mono.Options.OptionSet optionSet)
19 | {
20 | // lid|longid
21 | // Add node with ID of 3950 chars.
22 | // Enabled by default.
23 | }
24 |
25 | public void AddToAddressSpace(FolderState telemetryFolder, FolderState methodsFolder, PlcNodeManager plcNodeManager)
26 | {
27 | _plcNodeManager = plcNodeManager;
28 |
29 | FolderState folder = _plcNodeManager.CreateFolder(
30 | telemetryFolder,
31 | path: "Special",
32 | name: "Special",
33 | NamespaceType.OpcPlcApplications);
34 |
35 | AddNodes(folder);
36 | }
37 |
38 | public void StartSimulation()
39 | {
40 | _node.Start(value => value + 1, periodMs: 1000);
41 | }
42 |
43 | public void StopSimulation()
44 | {
45 | _node.Stop();
46 | }
47 |
48 | private void AddNodes(FolderState folder)
49 | {
50 | // Repeat A-Z until 3950 chars are collected.
51 | var id = new StringBuilder(4000);
52 | for (int i = 0; i < 3950; i++)
53 | {
54 | id.Append((char)(65 + (i % 26)));
55 | }
56 |
57 | BaseDataVariableState variable = _plcNodeManager.CreateBaseVariable(
58 | folder,
59 | path: id.ToString(),
60 | name: "LongId3950",
61 | new NodeId((uint)BuiltInType.UInt32),
62 | ValueRanks.Scalar,
63 | AccessLevels.CurrentReadOrWrite,
64 | "Constantly increasing value",
65 | NamespaceType.OpcPlcApplications,
66 | defaultValue: (uint)0);
67 |
68 | _node = _plcNodeManager.CreateVariableNode(variable);
69 |
70 | // Add to node list for creation of pn.json.
71 | Nodes = new List
72 | {
73 | PluginNodesHelper.GetNodeWithIntervals(variable.NodeId, _plcNodeManager),
74 | };
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/PluginNodes/Models/IPluginNodes.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.PluginNodes.Models;
2 |
3 | using Opc.Ua;
4 | using System.Collections.Generic;
5 |
6 | public interface IPluginNodes
7 | {
8 | IReadOnlyCollection Nodes { get; }
9 |
10 | void AddOptions(Mono.Options.OptionSet optionSet);
11 |
12 | void AddToAddressSpace(FolderState telemetryFolder, FolderState methodsFolder, PlcNodeManager plcNodeManager);
13 |
14 | void StartSimulation();
15 |
16 | void StopSimulation();
17 | }
18 |
--------------------------------------------------------------------------------
/src/PluginNodes/Models/NodeWithIntervals.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.PluginNodes.Models;
2 |
3 | public class NodeWithIntervals
4 | {
5 | public string NodeId { get; set; }
6 | public string NodeIdTypePrefix { get; set; } = "s"; // Default type is string.
7 | public string Namespace { get; set; }
8 | public uint PublishingInterval { get; set; }
9 | public uint SamplingInterval { get; set; }
10 | }
11 |
--------------------------------------------------------------------------------
/src/PluginNodes/NodeSet2PluginNodes.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.PluginNodes;
2 |
3 | using Microsoft.Extensions.Logging;
4 | using Opc.Ua;
5 | using OpcPlc.Helpers;
6 | using OpcPlc.PluginNodes.Models;
7 | using System;
8 | using System.Collections.Generic;
9 | using System.IO;
10 | using System.Linq;
11 |
12 | ///
13 | /// Nodes that are configured via *.NodeSet2.xml file(s).
14 | ///
15 | public class NodeSet2PluginNodes(TimeService timeService, ILogger logger) : PluginNodeBase(timeService, logger), IPluginNodes
16 | {
17 | private List _nodesFileNames;
18 | private PlcNodeManager _plcNodeManager;
19 | private Stream _nodes2File;
20 |
21 | public void AddOptions(Mono.Options.OptionSet optionSet)
22 | {
23 | optionSet.Add(
24 | "ns2|nodeset2file=",
25 | "the *.NodeSet2.xml file that contains the nodes to be created in the OPC UA address space (multiple comma separated filenames supported).",
26 | (string s) => _nodesFileNames = CliHelper.ParseListOfFileNames(s, "ns2"));
27 | }
28 |
29 | public void AddToAddressSpace(FolderState telemetryFolder, FolderState methodsFolder, PlcNodeManager plcNodeManager)
30 | {
31 | _plcNodeManager = plcNodeManager;
32 |
33 | if (_nodesFileNames?.Any() ?? false)
34 | {
35 | AddNodes();
36 | }
37 | }
38 |
39 | public void StartSimulation()
40 | {
41 | }
42 |
43 | public void StopSimulation()
44 | {
45 | }
46 |
47 | private void AddNodes()
48 | {
49 | foreach (var file in _nodesFileNames)
50 | {
51 | try
52 | {
53 | _nodes2File = File.OpenRead(file);
54 |
55 | // Load complex types from NodeSet2 file.
56 | _plcNodeManager.LoadPredefinedNodes(LoadPredefinedNodes);
57 | }
58 | catch (Exception e)
59 | {
60 | _logger.LogError(e, "Error loading NodeSet2 file {file}: {error}", file, e.Message);
61 | }
62 | }
63 |
64 | _logger.LogInformation("Completed processing NodeSet2 file(s)");
65 | }
66 |
67 | ///
68 | /// Loads a node set from a file and adds them to the set of predefined nodes.
69 | ///
70 | private NodeStateCollection LoadPredefinedNodes(ISystemContext context)
71 | {
72 | var predefinedNodes = new NodeStateCollection();
73 | var namespaces = new Dictionary();
74 |
75 | using (_nodes2File)
76 | {
77 | var importedNodeSet = Opc.Ua.Export.UANodeSet.Read(_nodes2File);
78 |
79 | if (importedNodeSet.NamespaceUris != null)
80 | {
81 | foreach (var namespaceUri in importedNodeSet.NamespaceUris)
82 | {
83 | namespaces[namespaceUri] = namespaceUri;
84 | }
85 | }
86 | importedNodeSet.Import(_plcNodeManager.SystemContext, predefinedNodes);
87 | }
88 |
89 | // Add to node list for creation of pn.json.
90 | Nodes ??= new List();
91 | Nodes = Nodes.Append(PluginNodesHelper.GetNodeWithIntervals(predefinedNodes[0].NodeId, _plcNodeManager)).ToList();
92 |
93 | return predefinedNodes;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/PluginNodes/PluginNodeBase.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.PluginNodes;
2 |
3 | using Microsoft.Extensions.Logging;
4 | using OpcPlc.PluginNodes.Models;
5 | using System.Collections.Generic;
6 |
7 | public class PluginNodeBase(TimeService timeService, ILogger logger)
8 | {
9 | protected readonly TimeService _timeService = timeService;
10 | protected readonly ILogger _logger = logger;
11 |
12 | public IReadOnlyCollection Nodes { get; protected set; } = new List();
13 | }
14 |
--------------------------------------------------------------------------------
/src/PluginNodes/SpecialCharNamePluginNode.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.PluginNodes;
2 |
3 | using Microsoft.Extensions.Logging;
4 | using Opc.Ua;
5 | using OpcPlc.Helpers;
6 | using OpcPlc.PluginNodes.Models;
7 | using System.Collections.Generic;
8 | using System.Web;
9 |
10 | ///
11 | /// Node with special chars in name and ID.
12 | ///
13 | public class SpecialCharNamePluginNode(TimeService timeService, ILogger logger) : PluginNodeBase(timeService, logger), IPluginNodes
14 | {
15 | private PlcNodeManager _plcNodeManager;
16 | private SimulatedVariableNode _node;
17 |
18 | public void AddOptions(Mono.Options.OptionSet optionSet)
19 | {
20 | // scn|specialcharname
21 | // Add node with special characters in name.
22 | // Enabled by default.
23 | }
24 |
25 | public void AddToAddressSpace(FolderState telemetryFolder, FolderState methodsFolder, PlcNodeManager plcNodeManager)
26 | {
27 | _plcNodeManager = plcNodeManager;
28 |
29 | FolderState folder = _plcNodeManager.CreateFolder(
30 | telemetryFolder,
31 | path: "Special",
32 | name: "Special",
33 | NamespaceType.OpcPlcApplications);
34 |
35 | AddNodes(folder);
36 | }
37 |
38 | public void StartSimulation()
39 | {
40 | _node.Start(value => value + 1, periodMs: 1000);
41 | }
42 |
43 | public void StopSimulation()
44 | {
45 | _node.Stop();
46 | }
47 |
48 | private void AddNodes(FolderState folder)
49 | {
50 | string specialChars = HttpUtility.HtmlDecode(@""!§$%&/()=?`´\+~*'#_-:.;,<>|@^°€µ{[]}");
51 |
52 | BaseDataVariableState variable = _plcNodeManager.CreateBaseVariable(
53 | folder,
54 | path: "Special_" + specialChars,
55 | name: specialChars,
56 | new NodeId((uint)BuiltInType.UInt32),
57 | ValueRanks.Scalar,
58 | AccessLevels.CurrentReadOrWrite,
59 | "Constantly increasing value",
60 | NamespaceType.OpcPlcApplications,
61 | defaultValue: (uint)0);
62 |
63 | _node = _plcNodeManager.CreateVariableNode(variable);
64 |
65 | // Add to node list for creation of pn.json.
66 | Nodes = new List
67 | {
68 | PluginNodesHelper.GetNodeWithIntervals(variable.NodeId, _plcNodeManager),
69 | };
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/PluginNodes/SpikePluginNode.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.PluginNodes;
2 |
3 | using Microsoft.Extensions.Logging;
4 | using Opc.Ua;
5 | using OpcPlc.Helpers;
6 | using OpcPlc.PluginNodes.Models;
7 | using System;
8 | using System.Collections.Generic;
9 |
10 | ///
11 | /// Node with a sine wave value with a spike anomaly.
12 | ///
13 | public class SpikePluginNode(TimeService timeService, ILogger logger) : PluginNodeBase(timeService, logger), IPluginNodes
14 | {
15 | private bool _isEnabled = true;
16 | private PlcNodeManager _plcNodeManager;
17 | private SimulatedVariableNode _node;
18 | private readonly Random _random = new();
19 | private int _spikeCycleInPhase;
20 | private int _spikeAnomalyCycle;
21 | private const double SimulationMaxAmplitude = 100.0;
22 |
23 | public void AddOptions(Mono.Options.OptionSet optionSet)
24 | {
25 | optionSet.Add(
26 | "ns|nospikes",
27 | $"do not generate spike data.\nDefault: {!_isEnabled}",
28 | (string s) => _isEnabled = s == null);
29 | }
30 |
31 | public void AddToAddressSpace(FolderState telemetryFolder, FolderState methodsFolder, PlcNodeManager plcNodeManager)
32 | {
33 | _plcNodeManager = plcNodeManager;
34 |
35 | if (_isEnabled)
36 | {
37 | FolderState folder = _plcNodeManager.CreateFolder(
38 | telemetryFolder,
39 | path: "Anomaly",
40 | name: "Anomaly",
41 | NamespaceType.OpcPlcApplications);
42 |
43 | AddNodes(folder);
44 | }
45 | }
46 |
47 | public void StartSimulation()
48 | {
49 | if (_isEnabled)
50 | {
51 | _spikeCycleInPhase = _plcNodeManager.PlcSimulationInstance.SimulationCycleCount;
52 | _spikeAnomalyCycle = _random.Next(_plcNodeManager.PlcSimulationInstance.SimulationCycleCount);
53 | _logger.LogTrace($"First spike anomaly cycle: {_spikeAnomalyCycle}");
54 |
55 | _node.Start(SpikeGenerator, _plcNodeManager.PlcSimulationInstance.SimulationCycleLength);
56 | }
57 | }
58 |
59 | public void StopSimulation()
60 | {
61 | if (_isEnabled)
62 | {
63 | _node.Stop();
64 | }
65 | }
66 |
67 | private void AddNodes(FolderState folder)
68 | {
69 | BaseDataVariableState variable = _plcNodeManager.CreateBaseVariable(
70 | folder,
71 | path: "SpikeData",
72 | name: "SpikeData",
73 | new NodeId((uint)BuiltInType.Double),
74 | ValueRanks.Scalar,
75 | AccessLevels.CurrentRead,
76 | "Value with random spikes",
77 | NamespaceType.OpcPlcApplications);
78 |
79 | _node = _plcNodeManager.CreateVariableNode(variable);
80 |
81 | // Add to node list for creation of pn.json.
82 | Nodes = new List
83 | {
84 | PluginNodesHelper.GetNodeWithIntervals(variable.NodeId, _plcNodeManager),
85 | };
86 | }
87 |
88 | ///
89 | /// Generates a sine wave with spikes at a random cycle in the phase.
90 | /// Called each SimulationCycleLength msec.
91 | ///
92 | private double SpikeGenerator(double value)
93 | {
94 | // calculate next value
95 | double nextValue;
96 | if (_isEnabled && _spikeCycleInPhase == _spikeAnomalyCycle)
97 | {
98 | // TODO: calculate
99 | nextValue = SimulationMaxAmplitude * 10;
100 | _logger.LogTrace("Generate spike anomaly");
101 | }
102 | else
103 | {
104 | nextValue = SimulationMaxAmplitude * Math.Sin(((2 * Math.PI) / _plcNodeManager.PlcSimulationInstance.SimulationCycleCount) * _spikeCycleInPhase);
105 | }
106 | _logger.LogTrace($"Spike cycle: {_spikeCycleInPhase} data: {nextValue}");
107 |
108 | // end of cycle: reset cycle count and calc next anomaly cycle
109 | if (--_spikeCycleInPhase == 0)
110 | {
111 | _spikeCycleInPhase = _plcNodeManager.PlcSimulationInstance.SimulationCycleCount;
112 | _spikeAnomalyCycle = _random.Next(_plcNodeManager.PlcSimulationInstance.SimulationCycleCount);
113 | _logger.LogTrace($"Next spike anomaly cycle: {_spikeAnomalyCycle}");
114 | }
115 |
116 | return nextValue;
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/PluginNodes/UaNodesPluginNodes.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.PluginNodes;
2 |
3 | using Microsoft.Extensions.Logging;
4 | using Opc.Ua;
5 | using OpcPlc.Helpers;
6 | using OpcPlc.PluginNodes.Models;
7 | using System;
8 | using System.Collections.Generic;
9 | using System.IO;
10 | using System.Linq;
11 |
12 | ///
13 | /// Nodes that are configured via binary *.PredefinedNodes.uanodes file(s).
14 | /// To produce a binary *.PredefinedNodes.uanodes file from an XML NodeSet file, run the following command:
15 | /// ModelCompiler.cmd
16 | ///
17 | public class UaNodesPluginNodes(TimeService timeService, ILogger logger) : PluginNodeBase(timeService, logger), IPluginNodes
18 | {
19 |
20 | private List _nodesFileNames;
21 | private PlcNodeManager _plcNodeManager;
22 | private Stream _uanodesFile;
23 |
24 | public void AddOptions(Mono.Options.OptionSet optionSet)
25 | {
26 | optionSet.Add(
27 | "unf|uanodesfile=",
28 | "the binary *.PredefinedNodes.uanodes file that contains the nodes to be created in the OPC UA address space (multiple comma separated filenames supported), use ModelCompiler.cmd to compile.",
29 | (string s) => _nodesFileNames = CliHelper.ParseListOfFileNames(s, "unf"));
30 | }
31 |
32 | public void AddToAddressSpace(FolderState telemetryFolder, FolderState methodsFolder, PlcNodeManager plcNodeManager)
33 | {
34 | _plcNodeManager = plcNodeManager;
35 |
36 | if (_nodesFileNames?.Any() ?? false)
37 | {
38 | AddNodes();
39 | }
40 | }
41 |
42 | public void StartSimulation()
43 | {
44 | // No simulation.
45 | }
46 |
47 | public void StopSimulation()
48 | {
49 | // No simulation.
50 | }
51 |
52 | private void AddNodes()
53 | {
54 | foreach (var file in _nodesFileNames)
55 | {
56 | try
57 | {
58 | _uanodesFile = File.OpenRead(file);
59 |
60 | // Load complex types from binary uanodes file.
61 | _plcNodeManager.LoadPredefinedNodes(LoadPredefinedNodes);
62 | }
63 | catch (Exception e)
64 | {
65 | _logger.LogError(e, "Error loading binary uanodes file {file}: {error}", file, e.Message);
66 | }
67 | }
68 |
69 | _logger.LogInformation("Completed processing binary uanodes file(s)");
70 | }
71 |
72 | ///
73 | /// Loads a node set from a file and adds them to the set of predefined nodes.
74 | ///
75 | private NodeStateCollection LoadPredefinedNodes(ISystemContext context)
76 | {
77 | var predefinedNodes = new NodeStateCollection();
78 |
79 | using (_uanodesFile)
80 | {
81 | predefinedNodes.LoadFromBinary(context,
82 | _uanodesFile,
83 | updateTables: true);
84 | }
85 |
86 | // Add to node list for creation of pn.json.
87 | Nodes ??= new List();
88 | Nodes = Nodes.Append(PluginNodesHelper.GetNodeWithIntervals(predefinedNodes[0].NodeId, _plcNodeManager)).ToList();
89 |
90 | return predefinedNodes;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/PluginNodes/WorkingSetPluginNode.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.PluginNodes;
2 |
3 | using Microsoft.Extensions.Logging;
4 | using Opc.Ua;
5 | using OpcPlc.Helpers;
6 | using OpcPlc.PluginNodes.Models;
7 | using System.Diagnostics;
8 |
9 | ///
10 | /// Node that shows current working set memory consumption in MB.
11 | ///
12 | public class WorkingSetPluginNode(TimeService timeService, ILogger logger) : PluginNodeBase(timeService, logger), IPluginNodes
13 | {
14 | private PlcNodeManager _plcNodeManager;
15 | private SimulatedVariableNode _node;
16 |
17 | public void AddOptions(Mono.Options.OptionSet optionSet)
18 | {
19 | // Enabled by default.
20 | }
21 |
22 | public void AddToAddressSpace(FolderState telemetryFolder, FolderState methodsFolder, PlcNodeManager plcNodeManager)
23 | {
24 | _plcNodeManager = plcNodeManager;
25 |
26 | FolderState folder = _plcNodeManager.CreateFolder(
27 | telemetryFolder,
28 | path: "Special",
29 | name: "Special",
30 | NamespaceType.OpcPlcApplications);
31 |
32 | AddNodes(folder);
33 | }
34 |
35 | public void StartSimulation()
36 | {
37 | _node.Start(value => GetWorkingSetMB(), periodMs: 1000);
38 | }
39 |
40 | public void StopSimulation()
41 | {
42 | _node.Stop();
43 | }
44 |
45 | private void AddNodes(FolderState folder)
46 | {
47 | BaseDataVariableState variable = _plcNodeManager.CreateBaseVariable(
48 | folder,
49 | path: "WorkingSetMB",
50 | name: "WorkingSetMB",
51 | new NodeId((uint)BuiltInType.UInt32),
52 | ValueRanks.Scalar,
53 | AccessLevels.CurrentReadOrWrite,
54 | "Working set memory consumption in MB",
55 | NamespaceType.OpcPlcApplications,
56 | defaultValue: GetWorkingSetMB());
57 |
58 | _node = _plcNodeManager.CreateVariableNode(variable);
59 |
60 | // Add to node list for creation of pn.json.
61 | Nodes =
62 | [
63 | PluginNodesHelper.GetNodeWithIntervals(variable.NodeId, _plcNodeManager),
64 | ];
65 | }
66 |
67 | private uint GetWorkingSetMB() => (uint)(Process.GetCurrentProcess().WorkingSet64 / 1024 / 1024);
68 | }
69 |
--------------------------------------------------------------------------------
/src/Program.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc;
2 |
3 | public static class Program
4 | {
5 | public static OpcPlcServer OpcPlcServer { get; private set; }
6 |
7 | ///
8 | /// Synchronous main method of the app.
9 | ///
10 | public static void Main(string[] args)
11 | {
12 | OpcPlcServer = new OpcPlcServer();
13 |
14 | // Start OPC UA server.
15 | OpcPlcServer.StartAsync(args).Wait();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/SimpleEvent/Build_ModelDesign.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | REM Pass the model file name without .xml extension
4 | ..\ModelCompiler.cmd ModelDesign
5 |
--------------------------------------------------------------------------------
/src/SimpleEvent/ModelDesign.csv:
--------------------------------------------------------------------------------
1 | CycleStepDataType,1,DataType
2 | SystemCycleStatusEventType,2,ObjectType
3 | SystemCycleStatusEventType_EventId,3,Variable
4 | SystemCycleStatusEventType_EventType,4,Variable
5 | SystemCycleStatusEventType_SourceNode,5,Variable
6 | SystemCycleStatusEventType_SourceName,6,Variable
7 | SystemCycleStatusEventType_Time,7,Variable
8 | SystemCycleStatusEventType_ReceiveTime,8,Variable
9 | SystemCycleStatusEventType_LocalTime,9,Variable
10 | SystemCycleStatusEventType_Message,10,Variable
11 | SystemCycleStatusEventType_Severity,11,Variable
12 | SystemCycleStatusEventType_CycleId,12,Variable
13 | SystemCycleStatusEventType_CurrentStep,13,Variable
14 | SystemCycleStartedEventType,14,ObjectType
15 | SystemCycleStartedEventType_EventId,15,Variable
16 | SystemCycleStartedEventType_EventType,16,Variable
17 | SystemCycleStartedEventType_SourceNode,17,Variable
18 | SystemCycleStartedEventType_SourceName,18,Variable
19 | SystemCycleStartedEventType_Time,19,Variable
20 | SystemCycleStartedEventType_ReceiveTime,20,Variable
21 | SystemCycleStartedEventType_LocalTime,21,Variable
22 | SystemCycleStartedEventType_Message,22,Variable
23 | SystemCycleStartedEventType_Severity,23,Variable
24 | SystemCycleStartedEventType_CycleId,24,Variable
25 | SystemCycleStartedEventType_CurrentStep,25,Variable
26 | SystemCycleStartedEventType_Steps,26,Variable
27 | SystemCycleAbortedEventType,27,ObjectType
28 | SystemCycleAbortedEventType_EventId,28,Variable
29 | SystemCycleAbortedEventType_EventType,29,Variable
30 | SystemCycleAbortedEventType_SourceNode,30,Variable
31 | SystemCycleAbortedEventType_SourceName,31,Variable
32 | SystemCycleAbortedEventType_Time,32,Variable
33 | SystemCycleAbortedEventType_ReceiveTime,33,Variable
34 | SystemCycleAbortedEventType_LocalTime,34,Variable
35 | SystemCycleAbortedEventType_Message,35,Variable
36 | SystemCycleAbortedEventType_Severity,36,Variable
37 | SystemCycleAbortedEventType_CycleId,37,Variable
38 | SystemCycleAbortedEventType_CurrentStep,38,Variable
39 | SystemCycleAbortedEventType_Error,39,Variable
40 | SystemCycleFinishedEventType,40,ObjectType
41 | SystemCycleFinishedEventType_EventId,41,Variable
42 | SystemCycleFinishedEventType_EventType,42,Variable
43 | SystemCycleFinishedEventType_SourceNode,43,Variable
44 | SystemCycleFinishedEventType_SourceName,44,Variable
45 | SystemCycleFinishedEventType_Time,45,Variable
46 | SystemCycleFinishedEventType_ReceiveTime,46,Variable
47 | SystemCycleFinishedEventType_LocalTime,47,Variable
48 | SystemCycleFinishedEventType_Message,48,Variable
49 | SystemCycleFinishedEventType_Severity,49,Variable
50 | SystemCycleFinishedEventType_CycleId,50,Variable
51 | SystemCycleFinishedEventType_CurrentStep,51,Variable
52 | CycleStepDataType_Encoding_DefaultBinary,52,Object
53 | SimpleEvents_BinarySchema,53,Variable
54 | SimpleEvents_BinarySchema_DataTypeVersion,54,Variable
55 | SimpleEvents_BinarySchema_NamespaceUri,55,Variable
56 | SimpleEvents_BinarySchema_Deprecated,56,Variable
57 | SimpleEvents_BinarySchema_CycleStepDataType,57,Variable
58 | SimpleEvents_BinarySchema_CycleStepDataType_DataTypeVersion,58,Variable
59 | SimpleEvents_BinarySchema_CycleStepDataType_DictionaryFragment,59,Variable
60 | CycleStepDataType_Encoding_DefaultXml,60,Object
61 | SimpleEvents_XmlSchema,61,Variable
62 | SimpleEvents_XmlSchema_DataTypeVersion,62,Variable
63 | SimpleEvents_XmlSchema_NamespaceUri,63,Variable
64 | SimpleEvents_XmlSchema_Deprecated,64,Variable
65 | SimpleEvents_XmlSchema_CycleStepDataType,65,Variable
66 | SimpleEvents_XmlSchema_CycleStepDataType_DataTypeVersion,66,Variable
67 | SimpleEvents_XmlSchema_CycleStepDataType_DictionaryFragment,67,Variable
68 | CycleStepDataType_Encoding_DefaultJson,68,Object
69 |
--------------------------------------------------------------------------------
/src/SimpleEvent/ModelDesign.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 | http://opcfoundation.org/UA/
13 | http://microsoft.com/Opc/OpcPlc/SimpleEvents
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | An event raised when a system cycle starts.
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | An event raised when a system cycle starts.
33 |
34 |
35 |
36 |
37 |
38 |
39 | An event raised when a system cycle is aborted.
40 |
41 |
42 |
43 |
44 |
45 |
46 | An event raised when a system cycle completes.
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/SimpleEvent/SimpleEvents.NodeIds.csv:
--------------------------------------------------------------------------------
1 | CycleStepDataType,1,DataType
2 | CycleStepDataType_Encoding_DefaultBinary,52,Object
3 | CycleStepDataType_Encoding_DefaultJson,68,Object
4 | CycleStepDataType_Encoding_DefaultXml,60,Object
5 | SimpleEvents_BinarySchema,53,Variable
6 | SimpleEvents_XmlSchema,61,Variable
7 | SystemCycleAbortedEventType,27,ObjectType
8 | SystemCycleFinishedEventType,40,ObjectType
9 | SystemCycleStartedEventType,14,ObjectType
10 | SystemCycleStatusEventType,2,ObjectType
11 |
--------------------------------------------------------------------------------
/src/SimpleEvent/SimpleEvents.PredefinedNodes.uanodes:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/iot-edge-opc-plc/d899d80ce719aa47c8b3e1e1dd1a890bd8787a21/src/SimpleEvent/SimpleEvents.PredefinedNodes.uanodes
--------------------------------------------------------------------------------
/src/SimpleEvent/SimpleEvents.Types.bsd:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/SimpleEvent/SimpleEvents.Types.xsd:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/SimulatedVariableNode.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc;
2 |
3 | using Opc.Ua;
4 | using System;
5 |
6 | public class SimulatedVariableNode : IDisposable
7 | {
8 | private readonly ISystemContext _context;
9 | private readonly BaseDataVariableState _variable;
10 | private ITimer _timer;
11 | private readonly TimeService _timeService;
12 |
13 | public T Value
14 | {
15 | get => (T)_variable.Value;
16 | set => SetValue(_variable, value);
17 | }
18 |
19 | public SimulatedVariableNode(ISystemContext context, BaseDataVariableState variable, TimeService timeService)
20 | {
21 | _context = context;
22 | _variable = variable;
23 | _timeService = timeService;
24 | }
25 |
26 | public void Dispose()
27 | {
28 | Stop();
29 | }
30 |
31 | ///
32 | /// Start periodic update.
33 | /// The update Func gets the current value as input and should return the updated value.
34 | ///
35 | public void Start(Func update, int periodMs)
36 | {
37 | _timer = _timeService.NewTimer((s, o) =>
38 | {
39 | Value = update(Value);
40 | },
41 | (uint)periodMs);
42 | }
43 |
44 | public void Stop()
45 | {
46 | if (_timer == null)
47 | {
48 | return;
49 | }
50 |
51 | _timer.Enabled = false;
52 | }
53 |
54 | private void SetValue(BaseDataVariableState variable, T value)
55 | {
56 | variable.Value = value;
57 | variable.Timestamp = _timeService.Now();
58 | variable.ClearChangeMasks(_context, false);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Startup.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc;
2 |
3 | using Microsoft.AspNetCore.Builder;
4 | using Microsoft.AspNetCore.Hosting;
5 | using Microsoft.AspNetCore.Http;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using Microsoft.Extensions.Hosting;
9 | using Opc.Ua;
10 | using OpenTelemetry;
11 | using OpenTelemetry.Exporter;
12 | using OpenTelemetry.Resources;
13 | using OpenTelemetry.Trace;
14 | using System;
15 | using System.IO;
16 |
17 | public class Startup
18 | {
19 | public Startup(IConfiguration configuration)
20 | {
21 | Configuration = configuration;
22 | }
23 |
24 | public IConfiguration Configuration { get; }
25 |
26 | // This method gets called by the runtime. Use this method to add services to the container.
27 | #pragma warning disable IDE0060 // Remove unused parameter
28 | public void ConfigureServices(IServiceCollection services)
29 | #pragma warning restore IDE0060 // Remove unused parameter
30 | {
31 | }
32 |
33 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
34 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
35 | {
36 | if (env.IsDevelopment())
37 | {
38 | app.UseDeveloperExceptionPage();
39 | }
40 |
41 | // Serve pn.json
42 | app.Run(async context => {
43 | if (context.Request.Method == "GET" &&
44 | context.Request.Path == (Program.OpcPlcServer.Config.PnJson[0] != '/' ? "/" : string.Empty) + Program.OpcPlcServer.Config.PnJson &&
45 | File.Exists(Program.OpcPlcServer.Config.PnJson))
46 | {
47 | context.Response.ContentType = "application/json";
48 | await context.Response.WriteAsync(await File.ReadAllTextAsync(Program.OpcPlcServer.Config.PnJson).ConfigureAwait(false)).ConfigureAwait(false);
49 | }
50 | else
51 | {
52 | context.Response.StatusCode = 404;
53 | }
54 | });
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*"
10 | }
11 |
--------------------------------------------------------------------------------
/src/container.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "iotedge/opc-plc",
3 | "buildAlways": true,
4 | "exposes": [
5 | "8080",
6 | "50000"
7 | ]
8 | }
--------------------------------------------------------------------------------
/src/nodesfile.json:
--------------------------------------------------------------------------------
1 | {
2 | "Folder": "MyTelemetry",
3 | "FolderList": [
4 | {
5 | "Folder": "Child",
6 | "NodeList": [
7 | {
8 | "NodeId": 9999,
9 | "Name": "Child Node",
10 | "Description": "Child Node for testing"
11 | },
12 | {
13 | "NodeId": "c6522393-5ca1-4547-9551-29d110ca4a3f",
14 | "Name": "Guid",
15 | "Description": "Guid test"
16 | }
17 | ]
18 | }
19 | ],
20 | "NodeList": [
21 | {
22 | "NodeId": 1023,
23 | "Name": "ActualSpeed",
24 | "Description": "Rotational speed"
25 | },
26 | {
27 | "NodeId": "aRMS"
28 | },
29 | {
30 | "NodeId": "1025",
31 | "Name": "DKW",
32 | "DataType": "Float",
33 | "ValueRank": -1,
34 | "AccessLevel": "CurrentReadOrWrite",
35 | "Description": "Diagnostic characteristic value"
36 | },
37 | {
38 | "NodeId": "1026",
39 | "Name": "Peak value",
40 | "DataType": "Float",
41 | "ValueRank": -1,
42 | "AccessLevel": "CurrentReadOrWrite",
43 | "Description": "Peak value of vibration acceleration"
44 | },
45 | {
46 | "NodeId": "1027",
47 | "Name": "Vibration Velocity",
48 | "DataType": "Float",
49 | "ValueRank": -1,
50 | "AccessLevel": "CurrentReadOrWrite",
51 | "Description": "Mean of the vibration velocity"
52 | },
53 | {
54 | "NodeId": "1029",
55 | "Name": "Active power",
56 | "DataType": "Float",
57 | "ValueRank": -1,
58 | "AccessLevel": "CurrentReadOrWrite",
59 | "Description": "Active power"
60 | },
61 | {
62 | "NodeId": "1030",
63 | "Name": "SampleStringNode",
64 | "DataType": "String",
65 | "Value": "Some value"
66 | },
67 | {
68 | "NodeId": "1031",
69 | "Name": "SampleUint32Node",
70 | "DataType": "UInt32",
71 | "Value": 1234
72 | },
73 | {
74 | "NodeId": "1032",
75 | "Name": "SampleInt32Node",
76 | "DataType": "Int32",
77 | "Value": -4321
78 | },
79 | {
80 | "NodeId": "1033",
81 | "Name": "SampleBooleanNode",
82 | "DataType": "Boolean",
83 | "Value": true
84 | },
85 | {
86 | "NodeId": "1048",
87 | "Name": "Int32 array",
88 | "DataType": "Int32",
89 | "ValueRank": 1,
90 | "ArrayDimensions": [ 5 ],
91 | "AccessLevel": "CurrentReadOrWrite",
92 | "Description": "Test array of 5 Int32 elements",
93 | "Value": [ 1, 2, 3, 4, 5 ]
94 | }
95 | ]
96 | }
97 |
--------------------------------------------------------------------------------
/src/project.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | $(PackageTags);OPC;OPCUA;IoT Edge;Edge
4 |
5 |
6 |
--------------------------------------------------------------------------------
/tests/AlarmTests.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Tests;
2 |
3 | using FluentAssertions;
4 | using NUnit.Framework;
5 | using Opc.Ua;
6 | using System.Collections.Generic;
7 |
8 | [TestFixture]
9 | public class AlarmTests : SubscriptionTestsBase
10 | {
11 | private NodeId _eventType;
12 |
13 | public AlarmTests() : base(["--alm"])
14 | {
15 | }
16 |
17 | [SetUp]
18 | public void CreateMonitoredItem()
19 | {
20 | _eventType = ToNodeId(ObjectTypes.TripAlarmType);
21 |
22 | var areaNode = FindNode(Server, OpcPlc.Namespaces.OpcPlcAlarmsInstance, "Green", "East", "Blue");
23 | var southMotor = FindNode(areaNode, OpcPlc.Namespaces.OpcPlcAlarmsInstance, "SouthMotor");
24 |
25 | SetUpMonitoredItem(areaNode, NodeClass.Object, Attributes.EventNotifier);
26 |
27 | // add condition fields to retrieve selected event.
28 | var filter = (EventFilter)MonitoredItem.Filter;
29 | var whereClause = filter.WhereClause;
30 | var element1 = whereClause.Push(FilterOperator.OfType, _eventType);
31 | var element2 = whereClause.Push(FilterOperator.InList,
32 | new SimpleAttributeOperand
33 | {
34 | AttributeId = Attributes.Value,
35 | TypeDefinitionId = ObjectTypeIds.BaseEventType,
36 | BrowsePath = new QualifiedName[] { BrowseNames.SourceNode },
37 | },
38 | new LiteralOperand
39 | {
40 | Value = new Variant(southMotor)
41 | });
42 |
43 | whereClause.Push(FilterOperator.And, element1, element2);
44 |
45 | AddMonitoredItem();
46 | }
47 |
48 | [Test]
49 | public void AlarmEventSubscribed_FiresNotification()
50 | {
51 | // Assert
52 | var events = ReceiveEventsAsDictionary(1);
53 | foreach (var value in events)
54 | {
55 | value.Should().Contain(new Dictionary
56 | {
57 | ["/EventType"] = _eventType,
58 | ["/SourceName"] = "SouthMotor",
59 | });
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/tests/DataMonitoringTests.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Tests;
2 |
3 | using System;
4 | using System.Linq;
5 | using FluentAssertions;
6 | using NUnit.Framework;
7 | using Opc.Ua;
8 | using static System.TimeSpan;
9 |
10 | ///
11 | /// Tests for OPC-UA Monitoring for Data changes.
12 | ///
13 | [TestFixture]
14 | public class DataMonitoringTests : SubscriptionTestsBase
15 | {
16 | // Set any cmd params needed for the plc server explicitly.
17 | public DataMonitoringTests() : base(Array.Empty())
18 | {
19 | }
20 |
21 | [SetUp]
22 | public void CreateMonitoredItem()
23 | {
24 | var nodeId = GetOpcPlcNodeId("FastUInt1");
25 | nodeId.Should().NotBeNull();
26 |
27 | SetUpMonitoredItem(nodeId, NodeClass.Variable, Attributes.Value);
28 |
29 | AddMonitoredItem();
30 | }
31 |
32 | [Test]
33 | public void Monitoring_NotifiesValueUpdates()
34 | {
35 | // Arrange
36 | ClearEvents();
37 |
38 | // Act: collect events during 5 seconds
39 | // Value is updated every second
40 | FireTimersWithPeriod(FromSeconds(1), numberOfTimes: 5);
41 |
42 | // Assert
43 | var events = ReceiveEvents(6);
44 | var values = events.Select(a => (uint)((MonitoredItemNotification)a.NotificationValue).Value.Value).ToList();
45 | var differences = values.Zip(values.Skip(1), (x, y) => y - x);
46 | differences.Should().AllBeEquivalentTo(1, $"elements of sequence {string.Join(",", values)} should be increasing by interval 1");
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/DataRandomizationTests.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Tests;
2 |
3 | using System.Linq;
4 | using FluentAssertions;
5 | using NUnit.Framework;
6 | using Opc.Ua;
7 | using static System.TimeSpan;
8 |
9 | ///
10 | /// Tests for OPC-UA Monitoring for Data changes.
11 | ///
12 | [TestFixture]
13 | public class DataRandomizationTests : SubscriptionTestsBase
14 | {
15 | // Set any cmd params needed for the plc server explicitly
16 | public DataRandomizationTests() : base(["--str=true"])
17 | {
18 | }
19 |
20 | [SetUp]
21 | public void CreateMonitoredItem()
22 | {
23 | var slowNodeId = GetOpcPlcNodeId("SlowUInt1");
24 | slowNodeId.Should().NotBeNull();
25 |
26 | SetUpMonitoredItem(slowNodeId, NodeClass.Variable, Attributes.Value);
27 | AddMonitoredItem();
28 | }
29 |
30 | [Test]
31 | public void Node_GeneratesRandomValues()
32 | {
33 | // Arrange
34 | ClearEvents();
35 |
36 | // Act: collect events during 50 seconds
37 | // Value is updated every 10 seconds
38 | FireTimersWithPeriod(FromSeconds(10), numberOfTimes: 5);
39 |
40 | // Assert
41 | var events = ReceiveEvents(6);
42 | var values = events.Select(a => (uint)((MonitoredItemNotification)a.NotificationValue).Value.Value).ToList();
43 | var differences = values.Zip(values.Skip(1), (x, y) => y - x);
44 | var differencesOfDifferences = differences.Zip(differences.Skip(1), (x, y) => y - x);
45 |
46 | var uniqueCount = differencesOfDifferences.Distinct().Count();
47 |
48 | // We are expecting random numbers to be unique mostly, not always so the differences between numbers should also be unique mostly.
49 | uniqueCount.Should().BeInRange(3, 4);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tests/DeterministicAlarmsTests/dalm001.json:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "name": "VendingMachines",
5 | "sources": [
6 | {
7 | "objectType": "BaseObjectState",
8 | "name": "VendingMachine1",
9 | "alarms": [
10 | {
11 | "objectType": "TripAlarmType",
12 | "name": "VendingMachine1_DoorOpen",
13 | "id": "V1_DoorOpen"
14 | },
15 | {
16 | "objectType": "LimitAlarmType",
17 | "name": "VendingMachine1_TemperatureHigh",
18 | "id": "V1_TemperatureHigh"
19 | }
20 | ]
21 | },
22 | {
23 | "objectType": "BaseObjectState",
24 | "name": "VendingMachine2",
25 | "alarms": [
26 | {
27 | "objectType": "TripAlarmType",
28 | "name": "VendingMachine2_DoorOpen",
29 | "id": "V2_DoorOpen"
30 | },
31 | {
32 | "objectType": "OffNormalAlarmType",
33 | "name": "VendingMachine2_LightOff",
34 | "id": "V2_LightOff"
35 | }
36 | ]
37 | }
38 | ]
39 | }
40 | ],
41 | "script": {
42 | "waitUntilStartInSeconds": 9,
43 | "isScriptInRepeatingLoop": true,
44 | "runningForSeconds": 22,
45 | "steps": [
46 | {
47 | "event": {
48 | "alarmId": "V1_DoorOpen",
49 | "reason": "Door Open",
50 | "severity": "High",
51 | "eventId": "V1_DoorOpen-1",
52 | "stateChanges": [
53 | {
54 | "stateType": "Enabled",
55 | "state": true
56 | },
57 | {
58 | "stateType": "Activated",
59 | "state": true
60 | }
61 | ]
62 | }
63 | },
64 | {
65 | "sleepInSeconds": 5
66 | },
67 | {
68 | "event": {
69 | "alarmId": "V2_LightOff",
70 | "reason": "Light Off in machine",
71 | "severity": "Medium",
72 | "eventId": "V2_LightOff-1",
73 | "stateChanges": [
74 | {
75 | "stateType": "Enabled",
76 | "state": true
77 | },
78 | {
79 | "stateType": "Activated",
80 | "state": true
81 | }
82 | ]
83 | }
84 | },
85 | {
86 | "sleepInSeconds": 7
87 | },
88 | {
89 | "event": {
90 | "alarmId": "V1_DoorOpen",
91 | "reason": "Door Closed",
92 | "severity": "Medium",
93 | "eventId": "V1_DoorOpen-2",
94 | "stateChanges": [
95 | {
96 | "stateType": "Activated",
97 | "state": false
98 | }
99 | ]
100 | }
101 | },
102 | {
103 | "sleepInSeconds": 4
104 | },
105 | {
106 | "event": {
107 | "alarmId": "V1_TemperatureHigh",
108 | "reason": "Temperature is HIGH",
109 | "severity": "High",
110 | "eventId": "V1_TemperatureHigh-1",
111 | "stateChanges": [
112 | {
113 | "stateType": "Activated",
114 | "state": true
115 | },
116 | {
117 | "stateType": "Enabled",
118 | "state": true
119 | }
120 | ]
121 | }
122 | }
123 | ]
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/tests/DeterministicAlarmsTests/dalm002.json:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "name": "VendingMachines",
5 | "sources": [
6 | {
7 | "objectType": "BaseObjectState",
8 | "name": "VendingMachine1",
9 | "alarms": [
10 | {
11 | "objectType": "TripAlarmType",
12 | "name": "VendingMachine1_DoorOpen",
13 | "id": "V1_DoorOpen"
14 | }
15 | ]
16 | }
17 | ]
18 | }
19 | ],
20 | "script": {
21 | "waitUntilStartInSeconds": 5,
22 | "isScriptInRepeatingLoop": false,
23 | "runningForSeconds": 36000,
24 | "steps": [
25 | {
26 | "event": {
27 | "alarmId": "V1_DoorOpen",
28 | "reason": "Door Open",
29 | "severity": "High",
30 | "eventId": "V1_DoorOpen-1",
31 | "stateChanges": [
32 | {
33 | "stateType": "Enabled",
34 | "state": true
35 | },
36 | {
37 | "stateType": "Activated",
38 | "state": true
39 | }
40 | ]
41 | }
42 | },
43 | {
44 | "sleepInSeconds": 5
45 | },
46 | {
47 | "event": {
48 | "alarmId": "V1_DoorOpen",
49 | "reason": "Door Closed",
50 | "severity": "Medium",
51 | "eventId": "V1_DoorOpen-2",
52 | "stateChanges": [
53 | {
54 | "stateType": "Enabled",
55 | "state": false
56 | },
57 | {
58 | "stateType": "Activated",
59 | "state": false
60 | }
61 | ]
62 | }
63 | }
64 | ]
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/tests/DeterministicAlarmsTests2.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Tests;
2 |
3 | using FluentAssertions;
4 | using NUnit.Framework;
5 | using Opc.Ua;
6 | using System;
7 | using System.Linq;
8 | using static System.TimeSpan;
9 |
10 | [TestFixture]
11 | public class DeterministicAlarmsTests2 : SubscriptionTestsBase
12 | {
13 | private const string Alarms = OpcPlc.Namespaces.OpcPlcDeterministicAlarmsInstance;
14 | private static readonly LocalizedText Active = English("Active");
15 | private static readonly LocalizedText Inactive = English("Inactive");
16 | private static readonly LocalizedText Disabled = English("Disabled");
17 | private static readonly LocalizedText Enabled = English("Enabled");
18 |
19 | public DeterministicAlarmsTests2() : base(
20 | [
21 | "--dalm=DeterministicAlarmsTests/dalm002.json",
22 | ])
23 | {
24 | }
25 |
26 | [SetUp]
27 | public void CreateMonitoredItem()
28 | {
29 | SetUpMonitoredItem(AlarmNodeId("VendingMachines"), NodeClass.Object, Attributes.EventNotifier);
30 |
31 | AddMonitoredItem();
32 | }
33 |
34 | [Test]
35 | public void VerifyThatTimeForEventsChanges()
36 | {
37 | var machine1 = AlarmNodeId("VendingMachine1");
38 |
39 | var doorOpen1 = FindNode(machine1, Alarms, "VendingMachine1_DoorOpen");
40 |
41 | NodeShouldHaveStates(doorOpen1, Inactive, Disabled);
42 |
43 | var waitUntilStartInSeconds = FromSeconds(5); // value in dalm002.json file
44 | var opcEvent1 = FireTimersWithPeriodAndReceiveEvents(waitUntilStartInSeconds, expectedCount: 1).First();
45 | var timeForFirstEvent = DateTime.Parse(opcEvent1["/Time"].ToString());
46 |
47 | NodeShouldHaveStates(doorOpen1, Active, Enabled);
48 |
49 | AdvanceToNextStep();
50 |
51 | var opcEvent2 = FireTimersWithPeriodAndReceiveEvents(waitUntilStartInSeconds, expectedCount: 1).First();
52 | var timeForNextEvent = DateTime.Parse(opcEvent2["/Time"].ToString());
53 |
54 | NodeShouldHaveStates(doorOpen1, Inactive, Disabled);
55 |
56 | timeForFirstEvent.Should().NotBe(timeForNextEvent);
57 | }
58 |
59 | private void AdvanceToNextStep()
60 | {
61 | FireTimersWithPeriodAndReceiveEvents(FromMilliseconds(1), 0);
62 | }
63 |
64 | private void NodeShouldHaveStates(NodeId node, LocalizedText activeState, LocalizedText enabledState)
65 | {
66 | NodeShouldHaveState(node, "ActiveState", activeState);
67 | NodeShouldHaveState(node, "EnabledState", enabledState);
68 | }
69 |
70 | private void NodeShouldHaveState(NodeId node, string state, LocalizedText expectedValue)
71 | {
72 | var nodeId = FindNode(node, Namespaces.OpcUa, state);
73 | var value = ReadValue(nodeId);
74 | value.Should().Be(expectedValue, "{0} should be {1}", state, expectedValue);
75 | }
76 |
77 | private NodeId AlarmNodeId(string identifier)
78 | {
79 | return NodeId.Create(identifier, Alarms, Session.NamespaceUris);
80 | }
81 |
82 | private static LocalizedText English(string text)
83 | {
84 | return new LocalizedText("en-US", text);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/tests/EventInstancesTests.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Tests;
2 |
3 | using FluentAssertions;
4 | using NUnit.Framework;
5 | using Opc.Ua;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using static System.TimeSpan;
9 |
10 | ///
11 | /// Tests for OPC-UA Monitoring for Events.
12 | ///
13 | [TestFixture]
14 | public class EventInstancesTests : SubscriptionTestsBase
15 | {
16 | private NodeId _eventType;
17 |
18 | // Set any cmd params needed for the plc server explicitly.
19 | public EventInstancesTests() : base(["--ei=1", "--er=1000"])
20 | {
21 | }
22 |
23 | [SetUp]
24 | public void CreateMonitoredItem()
25 | {
26 | _eventType = ToNodeId(ObjectTypeIds.BaseEventType);
27 |
28 | SetUpMonitoredItem(Server, NodeClass.Object, Attributes.EventNotifier);
29 |
30 | // add condition fields to retrieve selected event.
31 | var filter = (EventFilter)MonitoredItem.Filter;
32 | var whereClause = filter.WhereClause;
33 | whereClause.Push(FilterOperator.OfType, _eventType);
34 |
35 | AddMonitoredItem();
36 | }
37 |
38 | [Test]
39 | public void EventSubscribed_FiresNotification()
40 | {
41 | // Arrange
42 | ClearEvents();
43 |
44 | // Act
45 | // Event is fired every second
46 | FireTimersWithPeriod(FromSeconds(1), numberOfTimes: 5);
47 |
48 | // Assert
49 | var events = ReceiveAtMostEvents(5);
50 | var values = events
51 | .Select(a => (EventFieldList)a.NotificationValue)
52 | .Select(EventFieldListToDictionary);
53 | foreach (var value in values)
54 | {
55 | value.Should().Contain(new Dictionary
56 | {
57 | ["/EventType"] = _eventType,
58 | ["/SourceNode"] = Server,
59 | ["/SourceName"] = "System",
60 | });
61 | value.Should().ContainKey("/Message")
62 | .WhoseValue.Should().BeOfType()
63 | .Which.Text.Should().MatchRegex("^Event with index '0' and event cycle '\\d+'$");
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/tests/EventMonitoringTests.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Tests;
2 |
3 | using System.Collections.Generic;
4 | using FluentAssertions;
5 | using NUnit.Framework;
6 | using Opc.Ua;
7 |
8 | ///
9 | /// Tests for OPC-UA Monitoring for Events.
10 | ///
11 | [TestFixture]
12 | public class EventMonitoringTests : SubscriptionTestsBase
13 | {
14 | private NodeId _eventType;
15 |
16 | public EventMonitoringTests() : base(["--simpleevents"])
17 | {
18 | }
19 |
20 | [SetUp]
21 | public void CreateMonitoredItem()
22 | {
23 | _eventType = ToNodeId(SimpleEvents.ObjectTypeIds.SystemCycleStartedEventType);
24 |
25 | SetUpMonitoredItem(Server, NodeClass.Object, Attributes.EventNotifier);
26 |
27 | // add condition fields to retrieve selected event.
28 | var filter = (EventFilter)MonitoredItem.Filter;
29 | var whereClause = filter.WhereClause;
30 | whereClause.Push(FilterOperator.OfType, _eventType);
31 |
32 | AddMonitoredItem();
33 | }
34 |
35 | [Test]
36 | public void EventSubscribed_FiresNotification()
37 | {
38 | // Arrange
39 | ClearEvents();
40 |
41 | // Assert
42 | var values = ReceiveEventsAsDictionary(6);
43 | foreach (var value in values)
44 | {
45 | value.Should().Contain(new Dictionary
46 | {
47 | ["/EventType"] = _eventType,
48 | ["/SourceNode"] = Server,
49 | ["/SourceName"] = "System",
50 | });
51 | value.Should().ContainKey("/Message")
52 | .WhoseValue.Should().BeOfType()
53 | .Which.Text.Should().MatchRegex("^The system cycle '\\d+' has started\\.$");
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/tests/FastTimerTests.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Tests;
2 |
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Threading;
6 | using FluentAssertions;
7 | using NUnit.Framework;
8 |
9 | [TestFixture]
10 | public class FastTimerTests
11 | {
12 |
13 | [Test]
14 | public void FastTimer_ShouldFire_20TimesPerSecond()
15 | {
16 | // Arrange
17 | var fastTimer = new FastTimer(50)
18 | {
19 | Enabled = false
20 | };
21 | fastTimer.Elapsed += Callback;
22 | _callbacks.Clear();
23 |
24 | // Act
25 | fastTimer.Enabled = true;
26 | Thread.Sleep(2000);
27 | fastTimer.Enabled = false;
28 |
29 | // Assert (let's have some wiggle room here for timing issues)
30 | _callbacks.Count.Should().BeInRange(35, 45);
31 | }
32 |
33 | [Test]
34 | public void FastTimer_ShouldFire_100TimesPerSecond()
35 | {
36 | // Arrange
37 | var fastTimer = new FastTimer(10)
38 | {
39 | Enabled = false
40 | };
41 | fastTimer.Elapsed += Callback;
42 | _callbacks.Clear();
43 |
44 | // Act
45 | fastTimer.Enabled = true;
46 | Thread.Sleep(2000);
47 | fastTimer.Enabled = false;
48 |
49 | // Assert (let's have some wiggle room here for timing issues)
50 | _callbacks.Count.Should().BeInRange(185, 215);
51 | }
52 |
53 | [Test]
54 | public void FastTimerNotEnabled_ShouldFire_0TimesPerSecond()
55 | {
56 | // Arrange
57 | var fastTimer = new FastTimer(30)
58 | {
59 | Enabled = false
60 | };
61 | fastTimer.Elapsed += Callback;
62 | _callbacks.Clear();
63 |
64 | // Act
65 | Thread.Sleep(2000);
66 |
67 | // Assert
68 | _callbacks.Count.Should().Be(0);
69 | }
70 |
71 | [Test]
72 | public void FastTimerWithNoCallback_ShouldFire_0TimesPerSecond()
73 | {
74 | // Arrange
75 | var fastTimer = new FastTimer(30)
76 | {
77 | Enabled = false
78 | };
79 | _callbacks.Clear();
80 |
81 | // Act
82 | fastTimer.Enabled = true;
83 | Thread.Sleep(2000);
84 | fastTimer.Enabled = false;
85 |
86 | // Assert
87 | _callbacks.Count.Should().Be(0);
88 | }
89 |
90 | [Test]
91 | public void FastTimerWithNoAutoReset_ShouldFire_Once()
92 | {
93 | // Arrange
94 | var fastTimer = new FastTimer(30)
95 | {
96 | Enabled = false,
97 | AutoReset = false
98 | };
99 | fastTimer.Elapsed += Callback;
100 | _callbacks.Clear();
101 |
102 | // Act
103 | fastTimer.Enabled = true;
104 | Thread.Sleep(2000);
105 | fastTimer.Enabled = false;
106 |
107 | // Assert
108 | _callbacks.Count.Should().Be(1);
109 | }
110 |
111 |
112 | private readonly List _callbacks = new();
113 |
114 | private void Callback(object state, FastTimerElapsedEventArgs elapsedEventArgs)
115 | {
116 | _callbacks.Add(DateTime.UtcNow);
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/tests/GuidNodesTests.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Tests;
2 |
3 | using FluentAssertions;
4 | using NUnit.Framework;
5 |
6 | ///
7 | /// Tests deterministic GUID nodes.
8 | ///
9 | [TestFixture]
10 | public class GuidNodesTests : SubscriptionTestsBase
11 | {
12 | // Set any cmd params needed for the plc server explicitly
13 | public GuidNodesTests() : base(["--gn=2"])
14 | {
15 | }
16 |
17 | [Test]
18 | public void TestDeterministicGuidNodes()
19 | {
20 | var deterministicGuidNode = FindNode(ObjectsFolder, Namespaces.OpcPlcApplications, "OpcPlc", "Telemetry", "Deterministic GUID");
21 | deterministicGuidNode.Should().NotBeNull();
22 |
23 | var guidNode1 = FindNode(deterministicGuidNode, Namespaces.OpcPlcApplications, "51b74e55-f2e3-4a4d-b79c-bf57c76ea67c");
24 | guidNode1.Should().NotBeNull();
25 |
26 | var guidNode2 = FindNode(deterministicGuidNode, Namespaces.OpcPlcApplications, "1313895e-c776-4201-b893-e514864c6692");
27 | guidNode2.Should().NotBeNull();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/MetricsTests.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Tests;
2 |
3 | using FluentAssertions;
4 | using NUnit.Framework;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Diagnostics.Metrics;
8 | #nullable enable
9 |
10 | ///
11 | /// Tests for Metrics.
12 | ///
13 | [NonParallelizable]
14 | internal class MetricsTests : SimulatorTestsBase
15 | {
16 | private readonly MeterListener _meterListener;
17 | private readonly Dictionary _metrics;
18 |
19 | public MetricsTests()
20 | {
21 | _metrics = new Dictionary();
22 | _meterListener = new MeterListener();
23 |
24 | _meterListener.InstrumentPublished = (instrument, listener) => {
25 | if (instrument.Meter.Name == MetricsHelper.Meter.Name)
26 | {
27 | listener.EnableMeasurementEvents(instrument);
28 | }
29 | };
30 |
31 | _meterListener.SetMeasurementEventCallback(
32 | (Instrument instrument, long measurement, ReadOnlySpan> tags, object? state) => _metrics.Add(instrument.Name, measurement));
33 |
34 | _meterListener.SetMeasurementEventCallback(
35 | (Instrument instrument, double measurement, ReadOnlySpan> tags, object? state) => _metrics.Add(instrument.Name, measurement));
36 |
37 | _meterListener.SetMeasurementEventCallback(
38 | (Instrument instrument, int measurement, ReadOnlySpan> tags, object? state) => _metrics.Add(instrument.Name, measurement));
39 |
40 | _meterListener.Start();
41 | }
42 |
43 | [SetUp]
44 | public void SetUp()
45 | {
46 | _metrics.Clear();
47 |
48 | MetricsHelper.IsEnabled = true;
49 | }
50 |
51 | [Test]
52 | public void TestAddSessionCount()
53 | {
54 | var sessionId = Guid.NewGuid().ToString();
55 | MetricsHelper.AddSessionCount(sessionId.ToString());
56 | _metrics.TryGetValue("opc_plc_session_count", out var counter).Should().BeTrue();
57 | counter.Should().Be(1);
58 | }
59 |
60 | [Test]
61 | public void TestAddSubscriptionCount()
62 | {
63 | var sessionId = Guid.NewGuid().ToString();
64 | var subscriptionId = Guid.NewGuid().ToString();
65 | MetricsHelper.AddSubscriptionCount(sessionId, subscriptionId);
66 | _metrics.TryGetValue("opc_plc_subscription_count", out var counter).Should().BeTrue();
67 | counter.Should().Be(1);
68 | }
69 |
70 | [Test]
71 | public void TestAddMonitoredItemCount()
72 | {
73 | MetricsHelper.AddMonitoredItemCount(1);
74 | _metrics.TryGetValue("opc_plc_monitored_item_count", out var counter).Should().BeTrue();
75 | counter.Should().Be(1);
76 | }
77 |
78 | [Test]
79 | public void TestAddPublishedCount()
80 | {
81 | MetricsHelper.AddPublishedCount(1, 0);
82 | _metrics.TryGetValue("opc_plc_published_count_with_type", out var counter).Should().BeTrue();
83 | counter.Should().Be(1);
84 | }
85 |
86 | [Test]
87 | public void TestRecordTotalErrors()
88 | {
89 | MetricsHelper.RecordTotalErrors("operation");
90 | _metrics.TryGetValue("opc_plc_total_errors", out var counter).Should().BeTrue();
91 | counter.Should().Be(1);
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/tests/OpaqueAndNodeIdTests.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Tests;
2 |
3 | using FluentAssertions;
4 | using NUnit.Framework;
5 |
6 | ///
7 | /// Tests NodeIds of various IdTypes and ExpandedNodeIds.
8 | ///
9 | [TestFixture]
10 | public class OpaqueAndNodeIdTests : SubscriptionTestsBase
11 | {
12 | [Test]
13 | public void TestNodeIdNodes()
14 | {
15 | var specialFolder = FindNode(ObjectsFolder, Namespaces.OpcPlcApplications, "OpcPlc", "Telemetry", "Special");
16 | specialFolder.Should().NotBeNull();
17 |
18 | var stringNodeId = FindNode(specialFolder, Namespaces.OpcPlcApplications, "ScalarStaticNodeIdString");
19 | stringNodeId.Should().NotBeNull();
20 | var stringNodeIdValue = ReadValue(stringNodeId);
21 | stringNodeIdValue.Should().NotBeNull();
22 | stringNodeIdValue.IdType.Should().Be(Opc.Ua.IdType.String);
23 |
24 | var numericNodeId = FindNode(specialFolder, Namespaces.OpcPlcApplications, "ScalarStaticNodeIdNumeric");
25 | numericNodeId.Should().NotBeNull();
26 | var numericNodeIdValue = ReadValue(numericNodeId);
27 | numericNodeIdValue.Should().NotBeNull();
28 | numericNodeIdValue.IdType.Should().Be(Opc.Ua.IdType.Numeric);
29 |
30 | var guidNodeId = FindNode(specialFolder, Namespaces.OpcPlcApplications, "ScalarStaticNodeIdGuid");
31 | guidNodeId.Should().NotBeNull();
32 | var guidNodeIdValue = ReadValue(guidNodeId);
33 | guidNodeIdValue.Should().NotBeNull();
34 | guidNodeIdValue.IdType.Should().Be(Opc.Ua.IdType.Guid);
35 |
36 | var opaqueNodeId = FindNode(specialFolder, Namespaces.OpcPlcApplications, "ScalarStaticNodeIdOpaque");
37 | opaqueNodeId.Should().NotBeNull();
38 | var opaqueNodeIdValue = ReadValue(opaqueNodeId);
39 | opaqueNodeIdValue.Should().NotBeNull();
40 | opaqueNodeIdValue.IdType.Should().Be(Opc.Ua.IdType.Opaque);
41 |
42 | var stringExpandedNodeId = FindNode(specialFolder, Namespaces.OpcPlcApplications, "ScalarStaticExpandedNodeIdString");
43 | stringExpandedNodeId.Should().NotBeNull();
44 | var stringExpandedNodeIdValue = ReadValue(stringExpandedNodeId);
45 | stringExpandedNodeIdValue.Should().NotBeNull();
46 | stringExpandedNodeIdValue.IdType.Should().Be(Opc.Ua.IdType.String);
47 |
48 | var numericExpandedNodeId = FindNode(specialFolder, Namespaces.OpcPlcApplications, "ScalarStaticExpandedNodeIdNumeric");
49 | numericExpandedNodeId.Should().NotBeNull();
50 | var numericExpandedNodeIdValue = ReadValue(numericExpandedNodeId);
51 | numericExpandedNodeIdValue.Should().NotBeNull();
52 | numericExpandedNodeIdValue.IdType.Should().Be(Opc.Ua.IdType.Numeric);
53 |
54 | var guidExpandedNodeId = FindNode(specialFolder, Namespaces.OpcPlcApplications, "ScalarStaticExpandedNodeIdGuid");
55 | guidExpandedNodeId.Should().NotBeNull();
56 | var guidExpandedNodeIdValue = ReadValue(guidExpandedNodeId);
57 | guidExpandedNodeIdValue.Should().NotBeNull();
58 | guidExpandedNodeIdValue.IdType.Should().Be(Opc.Ua.IdType.Guid);
59 |
60 | var opaqueExpandedNodeId = FindNode(specialFolder, Namespaces.OpcPlcApplications, "ScalarStaticExpandedNodeIdOpaque");
61 | opaqueExpandedNodeId.Should().NotBeNull();
62 | var opaqueExpandedNodeIdValue = ReadValue(opaqueExpandedNodeId);
63 | opaqueExpandedNodeIdValue.Should().NotBeNull();
64 | opaqueExpandedNodeIdValue.IdType.Should().Be(Opc.Ua.IdType.Opaque);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/tests/README.md:
--------------------------------------------------------------------------------
1 | # OPC PLC server tests
2 | This project contains integration tests.
3 |
4 | The test fixture runs an instance of the OPC PLC Server per test class, as a background thread, and performs test from the client side.
5 |
6 | The Server is instrumented with mocks for time-related objects and methods (DateTime.Now, Timers) so
7 | that time can be controlled programmatically.
8 |
9 | Tests can be run directly with no configuration required.
10 |
11 |
--------------------------------------------------------------------------------
/tests/TestLogger.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------
2 | // Copyright (c) Microsoft Corporation. All rights reserved.
3 | // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
4 | // ------------------------------------------------------------
5 |
6 | namespace OpcPlc.Tests;
7 |
8 | using Microsoft.Extensions.Logging;
9 | using Microsoft.Extensions.Logging.Abstractions;
10 | using Microsoft.Extensions.Logging.Console;
11 | using System;
12 | using System.IO;
13 |
14 | public class TestLogger : ILogger
15 | {
16 | private readonly TextWriter _outputWriter;
17 | private readonly ConsoleFormatter _formatter;
18 | private readonly string _category = typeof(T).FullName;
19 |
20 | public TestLogger(TextWriter outputWriter, ConsoleFormatter formatter)
21 | {
22 | _outputWriter = outputWriter;
23 | _formatter = formatter;
24 | }
25 |
26 | public LogLevel MinimumLogLevel { get; set; } = LogLevel.Debug;
27 |
28 | public IDisposable BeginScope(TState state)
29 | where TState : notnull
30 | {
31 | return null;
32 | }
33 |
34 | public bool IsEnabled(LogLevel logLevel)
35 | {
36 | return logLevel >= MinimumLogLevel;
37 | }
38 |
39 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
40 | {
41 | if (logLevel < MinimumLogLevel)
42 | {
43 | return;
44 | }
45 |
46 | _formatter.Write(new LogEntry(logLevel, _category, eventId, state, exception, formatter), null, _outputWriter);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/UserDefinedNodesTests.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Tests;
2 |
3 | using FluentAssertions;
4 | using NUnit.Framework;
5 |
6 | ///
7 | /// Tests the nodes configured via nodesfile.json.
8 | ///
9 | [TestFixture]
10 | public class UserDefinedNodesTests : SubscriptionTestsBase
11 | {
12 | // Set any cmd params needed for the plc server explicitly
13 | public UserDefinedNodesTests() : base(["--nodesfile=nodesfile.json"])
14 | {
15 | }
16 |
17 | [Test]
18 | public void TestUserDefinedNodes()
19 | {
20 | var myTelemetryNode = FindNode(ObjectsFolder, Namespaces.OpcPlcApplications, "OpcPlc", "MyTelemetry");
21 | myTelemetryNode.Should().NotBeNull();
22 |
23 | var childNode = FindNode(myTelemetryNode, Namespaces.OpcPlcApplications, "Child");
24 | childNode.Should().NotBeNull();
25 |
26 | FindNode(childNode, Namespaces.OpcPlcApplications, "9999")
27 | .Should().NotBeNull();
28 |
29 | FindNode(childNode, Namespaces.OpcPlcApplications, "Guid")
30 | .Should().NotBeNull();
31 |
32 | FindNode(myTelemetryNode, Namespaces.OpcPlcApplications, "1023")
33 | .Should().NotBeNull();
34 |
35 | FindNode(myTelemetryNode, Namespaces.OpcPlcApplications, "aRMS")
36 | .Should().NotBeNull();
37 |
38 | FindNode(myTelemetryNode, Namespaces.OpcPlcApplications, "1025")
39 | .Should().NotBeNull();
40 |
41 | FindNode(myTelemetryNode, Namespaces.OpcPlcApplications, "1026")
42 | .Should().NotBeNull();
43 |
44 | FindNode(myTelemetryNode, Namespaces.OpcPlcApplications, "1027")
45 | .Should().NotBeNull();
46 |
47 | FindNode(myTelemetryNode, Namespaces.OpcPlcApplications, "1029")
48 | .Should().NotBeNull();
49 |
50 | FindNode(myTelemetryNode, Namespaces.OpcPlcApplications, "1030")
51 | .Should().NotBeNull();
52 |
53 | FindNode(myTelemetryNode, Namespaces.OpcPlcApplications, "1031")
54 | .Should().NotBeNull();
55 |
56 | FindNode(myTelemetryNode, Namespaces.OpcPlcApplications, "1032")
57 | .Should().NotBeNull();
58 |
59 | FindNode(myTelemetryNode, Namespaces.OpcPlcApplications, "1033")
60 | .Should().NotBeNull();
61 |
62 | var arrayNodeId = FindNode(myTelemetryNode, Namespaces.OpcPlcApplications, "1048");
63 | arrayNodeId.Should().NotBeNull();
64 | Session.ReadValue(arrayNodeId).Value.Should().BeEquivalentTo(new int[] { 1, 2, 3, 4, 5 });
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/tests/VariableTests.cs:
--------------------------------------------------------------------------------
1 | namespace OpcPlc.Tests;
2 |
3 | using FluentAssertions;
4 | using NUnit.Framework;
5 | using Opc.Ua;
6 | using System.Collections;
7 |
8 | ///
9 | /// Tests for interacting with OPC-UA Variable nodes.
10 | ///
11 | [TestFixture]
12 | public class VariableTests : SimulatorTestsBase
13 | {
14 | private NodeId _scalarStaticNode;
15 |
16 | // Set any cmd params needed for the plc server explicitly.
17 | public VariableTests() : base(["--ref"])
18 | {
19 | }
20 |
21 | [SetUp]
22 | public void SetUp()
23 | {
24 | _scalarStaticNode = FindNode(ObjectsFolder, OpcPlc.Namespaces.OpcPlcReferenceTest, "ReferenceTest", "Scalar", "Scalar_Static");
25 | }
26 |
27 | public static IEnumerable NodeWriteTestCases
28 | {
29 | get
30 | {
31 | yield return new TestCaseData(new[] { "Scalar_Static_Double" }, Fake.Random.Double());
32 | yield return new TestCaseData(new[] { "Scalar_Static_Arrays", "Scalar_Static_Arrays_String" }, Fake.Lorem.Words());
33 | }
34 | }
35 |
36 | [Test]
37 | [TestCaseSource(nameof(NodeWriteTestCases))]
38 | public void WriteValue_UpdatesValue(string[] pathParts, object newValue)
39 | {
40 | var nodeId = FindNode(_scalarStaticNode, OpcPlc.Namespaces.OpcPlcReferenceTest, pathParts);
41 |
42 | var results = WriteValue(nodeId, newValue);
43 |
44 | results.Should().Be(StatusCodes.Good);
45 |
46 | Session.ReadValue(nodeId)
47 | .Value
48 | .Should().BeEquivalentTo(newValue);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tests/opc-plc-tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | OpcPlc.Tests
6 | false
7 | Preview
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 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | PreserveNewest
56 |
57 |
58 | PreserveNewest
59 |
60 |
61 | PreserveNewest
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/tools/scripts/acr-matrix.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | Creates the container build matrix from the container.json files in the tree.
4 |
5 | .DESCRIPTION
6 | The script traverses the build root to find all folders with an container.json
7 | file and populates the matrix to create the individual build jobs.
8 |
9 | .PARAMETER BuildRoot
10 | The root folder to start traversing the repository from.
11 |
12 | .PARAMETER Registry
13 | The name of the registry
14 |
15 | .PARAMETER Subscription
16 | The subscription to use - otherwise uses default
17 |
18 | .PARAMETER Build
19 | If not set the task generates jobs in azure pipeline.
20 |
21 | .PARAMETER Debug
22 | Build debug and include debugger into images (where applicable)
23 |
24 | .PARAMETER Fast
25 | Only build images that are absolutely needed (tagged with buildAlways:true)
26 | #>
27 |
28 | Param(
29 | [string] $BuildRoot,
30 | [string] $Registry,
31 | [string] $Subscription,
32 | [switch] $Build,
33 | [switch] $Fast,
34 | [switch] $Debug
35 | )
36 |
37 | if ([string]::IsNullOrEmpty($BuildRoot)) {
38 | $BuildRoot = & (Join-Path $PSScriptRoot "get-root.ps1") -fileName "*.sln"
39 | }
40 |
41 | $acrMatrix = @{}
42 |
43 | # Traverse from build root and find all container.json metadata files to acr matrix
44 | Get-ChildItem $BuildRoot -Recurse -Include "container.json" `
45 | | ForEach-Object {
46 |
47 | # Get root
48 | $dockerFolder = $_.DirectoryName.Replace($BuildRoot, "").Substring(1)
49 | $metadata = Get-Content -Raw -Path $_.FullName | ConvertFrom-Json
50 |
51 | if ($Fast.IsPresent) {
52 | if (!$metadata.buildAlways) {
53 | return
54 | }
55 | }
56 |
57 | try {
58 | $jobName = "$($metadata.name)"
59 | if (![string]::IsNullOrEmpty($metadata.tag)) {
60 | $jobName = "$($jobName)/$($metadata.tag)"
61 | }
62 | if (![string]::IsNullOrEmpty($jobName)) {
63 | $acrMatrix.Add($jobName, @{ "dockerFolder" = $dockerFolder })
64 | }
65 | }
66 | catch {
67 | # continue to next
68 | }
69 | }
70 |
71 | if ($Build.IsPresent) {
72 | $acrMatrix.Values | ForEach-Object {
73 | & (Join-Path $PSScriptRoot "acr-build.ps1") -Path $_.dockerFolder `
74 | -Debug:$Debug -Registry $Registry -Subscription $Subscription
75 | }
76 | }
77 | else {
78 | # Set pipeline variable
79 | Write-Host ("##vso[task.setVariable variable=acrMatrix;isOutput=true] {0}" `
80 | -f ($acrMatrix | ConvertTo-Json -Compress))
81 | }
82 |
--------------------------------------------------------------------------------
/tools/scripts/build.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | Builds docker images from definition files in folder or the entire tree
4 |
5 | .DESCRIPTION
6 | The script traverses the build root to find all folders with an container.json
7 | file builds each one
8 |
9 | .PARAMETER Path
10 | The root folder to start traversing the repository from (Optional).
11 |
12 | .PARAMETER Debug
13 | Whether to build debug images.
14 | #>
15 |
16 | Param(
17 | [string] $Path = $null,
18 | [switch] $Debug
19 | )
20 |
21 | $BuildRoot = & (Join-Path $PSScriptRoot "get-root.ps1") -fileName "*.sln"
22 |
23 | if ([string]::IsNullOrEmpty($Path)) {
24 | $Path = $BuildRoot
25 | }
26 |
27 | # Traverse from build root and find all container.json metadata files and build
28 | Get-ChildItem $Path -Recurse -Include "container.json" `
29 | | ForEach-Object {
30 |
31 | # Get root
32 | $dockerFolder = $_.DirectoryName.Replace($BuildRoot, "")
33 | if ([string]::IsNullOrEmpty($dockerFolder)) {
34 | $dockerFolder = "."
35 | }
36 | else {
37 | $dockerFolder = $dockerFolder.Substring(1)
38 | }
39 |
40 | $metadata = Get-Content -Raw -Path (join-path $_.DirectoryName "container.json") `
41 | | ConvertFrom-Json
42 | & (Join-Path $PSScriptRoot "docker-build.ps1") `
43 | -ImageName $metadata.name -Path $dockerFolder -Debug:$Debug
44 | }
45 |
--------------------------------------------------------------------------------
/tools/scripts/docker-build.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | Build docker container for a container.json definition in the tree.
4 |
5 | .PARAMETER Path
6 | The folder to build the docker files from which also contains
7 | the container.json file.
8 |
9 | .PARAMETER ImageName
10 | The name of the image.
11 |
12 | .PARAMETER Debug
13 | Build debug and include debugger into images (where applicable)
14 | #>
15 |
16 | Param(
17 | [string] $Path = ".",
18 | [string] $ImageName,
19 | [switch] $Debug
20 | )
21 |
22 | if ([string]::IsNullOrEmpty($ImageName)) {
23 | throw "Must specify an image name"
24 | }
25 |
26 | Write-Host "Building $($ImageName)"
27 |
28 | # Source definitions
29 | $definitions = & (Join-Path $PSScriptRoot "docker-source.ps1") `
30 | -Path $Path -Debug:$Debug
31 | if ($definitions.Count -eq 0) {
32 | Write-Host "Nothing to build."
33 | return
34 | }
35 |
36 | # Get currently active platform
37 | $dockerversion = &docker @("version") 2>&1 | %{ "$_" } `
38 | | ConvertFrom-Csv -Delimiter ':' -Header @("Key", "Value") `
39 | | Where-Object { $_.Key -eq "OS/Arch" } `
40 | | ForEach-Object { $platform = $_.Value }
41 | if ($LastExitCode -ne 0) {
42 | throw "docker version failed with $($LastExitCode)."
43 | }
44 |
45 | if ([string]::IsNullOrEmpty($platform)) {
46 | $platform = "linux/amd64"
47 | }
48 | if ($platform -eq "windows/amd64") {
49 | $osVerToPlatform = @{
50 | "10.0.17134" = "windows/amd64:10.0.17134.885"
51 | "10.0.17763" = "windows/amd64:10.0.17763.615"
52 | "10.0.18362" = "windows/amd64:10.0.17134.885"
53 | }
54 | $osver = (Get-WmiObject Win32_OperatingSystem).Version
55 | $platform = $osVerToPlatform.Item($osver)
56 | }
57 | Write-Host "Current docker OS/Arch: $platform"
58 |
59 | # Select build definition
60 | $def = $definitions `
61 | | Where-Object { $_.platform -eq $platform } `
62 | | Select-Object
63 |
64 | # Build docker image from definition
65 | $dockerfile = $def.dockerfile
66 | $buildContext = $def.buildContext
67 |
68 | # Create docker build command line
69 | $argumentList = @("build",
70 | "-f", $dockerfile,
71 | "-t", "$($ImageName):latest"
72 | )
73 | $argumentList += $buildContext
74 | & docker $argumentList 2>&1 | %{ "$_" }
75 | if ($LastExitCode -ne 0) {
76 | throw "Error building $($ImageName): 'docker $($args)' failed with $($LastExitCode)."
77 | }
78 |
--------------------------------------------------------------------------------
/tools/scripts/get-matrix.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | Creates buildjob matrix based on the specified file names in the tree.
4 |
5 | .DESCRIPTION
6 | The script traverses the build root to find all folders with a matching
7 | file and populates the matrix. The matrix is used to spawn jobs that
8 | run on multiple different environments for each file. E.g. build all
9 | solution files on all platforms, run tests per particular folder of
10 | the tree, etc.
11 |
12 | .PARAMETER BuildRoot
13 | The root folder to start traversing the repository from.
14 |
15 | .PARAMETER FileName
16 | File patterns to match defining the folders and files in the matrix
17 |
18 | .PARAMETER JobPrefix
19 | Optional name prefix for each job
20 | #>
21 |
22 | Param(
23 | [string] $BuildRoot = $null,
24 | [string] $FileName = $null,
25 | [string] $JobPrefix = ""
26 | )
27 |
28 | if ([string]::IsNullOrEmpty($BuildRoot)) {
29 | $BuildRoot = & (Join-Path $PSScriptRoot "get-root.ps1") -fileName "*.sln"
30 | }
31 |
32 | if ([string]::IsNullOrEmpty($FileName)) {
33 | $FileName = "Directory.Build.props"
34 | }
35 | if (![string]::IsNullOrEmpty($JobPrefix)) {
36 | $JobPrefix = "$($JobPrefix)-"
37 | }
38 |
39 | # https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/hosted?view=azure-devops&tabs=yaml#software
40 | $agents = @{
41 | windows = "windows-2022"
42 | linux = "ubuntu-22.04"
43 | }
44 |
45 | $jobMatrix = @{}
46 |
47 | # Traverse from build root and find all files to create job matrix
48 | Get-ChildItem $BuildRoot -Recurse `
49 | | Where-Object Name -like $FileName `
50 | | ForEach-Object {
51 |
52 | $fullFolder = $_.DirectoryName.Replace("\", "/")
53 | $folder = $_.DirectoryName.Replace($BuildRoot, "").Replace("\", "/").TrimStart("/")
54 | $file = $_.FullName.Replace($BuildRoot, "").Replace("\", "/").TrimStart("/")
55 | if ([string]::IsNullOrEmpty($folder)) {
56 | $postFix = ""
57 | }
58 | else {
59 | $postFix = $folder.Replace("/", "-")
60 | $postFix = "$($postFix)-"
61 | }
62 | $agents.keys | ForEach-Object {
63 | $jobName = "$($JobPrefix)$($postFix)$($_)"
64 | $jobMatrix.Add($jobName, @{
65 | "poolImage" = $agents.Item($_)
66 | "folder" = $folder
67 | "fullFolder" = $fullFolder
68 | "file" = $file
69 | "agent" = $($_)
70 | })
71 | }
72 | }
73 |
74 | # Set pipeline variable
75 | Write-Host ("##vso[task.setVariable variable=jobMatrix;isOutput=true] {0}" `
76 | -f ($jobMatrix | ConvertTo-Json -Compress))
77 |
--------------------------------------------------------------------------------
/tools/scripts/get-root.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | find the top most folder with file in it and return the path
4 |
5 | .DESCRIPTION
6 | Generic functionality needed to find a root.
7 | #>
8 | param(
9 | [string] $startDir,
10 | [string] $fileName
11 | )
12 |
13 | if ([string]::IsNullOrEmpty($startDir)) {
14 | $startDir = $PSScriptRoot
15 | }
16 |
17 | $cur = $startDir
18 | while (![string]::IsNullOrEmpty($cur)) {
19 | $test = Join-Path $cur $fileName
20 | if (Test-Path -Path $test -PathType Any) {
21 | return $cur
22 | }
23 | $cur = Split-Path $cur
24 | }
25 | return $startDir
26 |
--------------------------------------------------------------------------------
/tools/scripts/get-version.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | Sets CI version build variables and/or returns version information.
4 |
5 | .DESCRIPTION
6 | The script is a wrapper around any versioning tool we use and abstracts it from
7 | the rest of the build system.
8 | #>
9 |
10 | try {
11 | # Try install tool
12 | & dotnet @("tool", "install", "-g", "nbgv") 2>&1 | Out-Null
13 |
14 | $props = (& nbgv @("get-version", "-f", "json")) | ConvertFrom-Json
15 | if ($LastExitCode -ne 0) {
16 | throw "Error: 'nbgv get-version -f json' failed with $($LastExitCode)."
17 | }
18 |
19 | return [pscustomobject] @{
20 | Full = $props.CloudBuildAllVars.NBGV_NuGetPackageVersion
21 | Prefix = $props.CloudBuildAllVars.NBGV_SimpleVersion
22 | }
23 | }
24 | catch {
25 | Write-Warning $_.Exception
26 | return $null
27 | }
28 |
--------------------------------------------------------------------------------
/tools/scripts/set-version.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | Sets CI version build variables and/or returns version information.
4 |
5 | .DESCRIPTION
6 | The script is a wrapper around any versioning tool we use and abstracts it from
7 | the rest of the build system.
8 | #>
9 |
10 | $version = & (Join-Path $PSScriptRoot "get-version.ps1")
11 |
12 | # Call versioning for build
13 | & nbgv @("cloud", "-c", "-a")
14 | if ($LastExitCode -ne 0) {
15 | Write-Warning "Error: 'nbgv cloud -c -a' failed with $($LastExitCode)."
16 | }
17 |
18 | # Set build environment version numbers in pipeline context
19 | Write-Host "Setting version build variables:"
20 |
21 | Write-Host "##vso[task.setvariable variable=Version_Full;isOutput=true]$($version.Full)"
22 | Write-Host "##vso[task.setvariable variable=Version_Prefix;isOutput=true]$($version.Prefix)"
23 |
--------------------------------------------------------------------------------
/tools/templates/acrbuild.yml:
--------------------------------------------------------------------------------
1 | #
2 | # Build images in acr
3 | #
4 | jobs:
5 | - job: imagesprep
6 | displayName: Prepare Image Jobs
7 | pool:
8 | name: Hosted Windows 2019 with VS2019
9 | variables:
10 | skipComponentGovernanceDetection: true
11 | steps:
12 | - task: PowerShell@2
13 | name: acrmatrix
14 | displayName: Prepare Builds
15 | inputs:
16 | targetType: filePath
17 | filePath: ./tools/scripts/acr-matrix.ps1
18 | - job: imagesall
19 | displayName: Build Images for
20 | pool:
21 | name: Hosted Windows 2019 with VS2019
22 | dependsOn: imagesprep
23 | strategy:
24 | matrix: $[ dependencies.imagesprep.outputs['acrmatrix.acrMatrix'] ]
25 | variables:
26 | skipComponentGovernanceDetection: true
27 | steps:
28 | - task: PowerShell@2
29 | displayName: Versioning
30 | inputs:
31 | targetType: filePath
32 | filePath: ./tools/scripts/set-version.ps1
33 | - task: UseDotNet@2
34 | displayName: 'Install .NET Core SDK'
35 | inputs:
36 | packageType: sdk
37 | version: 9.0.x
38 | includePreviewVersions: false
39 | installationPath: $(Agent.ToolsDirectory)/dotnet
40 | - task: AzureCLI@1
41 | name: acrbuildr
42 | displayName: Build Release Images
43 | inputs:
44 | azureSubscription: azureiiot
45 | scriptLocation: inlineScript
46 | inlineScript: powershell ./tools/scripts/acr-build.ps1 $(dockerFolder)
47 | - task: AzureCLI@1
48 | name: acrbuildd
49 | displayName: Build Debug Images
50 | inputs:
51 | azureSubscription: azureiiot
52 | scriptLocation: inlineScript
53 | inlineScript: powershell ./tools/scripts/acr-build.ps1 $(dockerFolder) -debug
54 |
--------------------------------------------------------------------------------
/tools/templates/ci.yml:
--------------------------------------------------------------------------------
1 | #
2 | # Build and test all on all platforms
3 | #
4 | jobs:
5 | - job: buildprep
6 | displayName: Prepare Build Jobs
7 | pool:
8 | vmImage: 'windows-2022'
9 | variables:
10 | skipComponentGovernanceDetection: true
11 | DOTNET_CLI_TELEMETRY_OPTOUT: true
12 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
13 | steps:
14 | - task: PowerShell@2
15 | name: buildmatrix
16 | displayName: Prepare Solutions
17 | inputs:
18 | targetType: filePath
19 | filePath: ./tools/scripts/get-matrix.ps1
20 | arguments: -FileName *.sln
21 | - job: buildall
22 | displayName: Building
23 | dependsOn: buildprep
24 | strategy:
25 | matrix: $[dependencies.buildprep.outputs['buildmatrix.jobMatrix'] ]
26 | pool:
27 | vmImage: $(poolImage)
28 | steps:
29 | - task: UseDotNet@2
30 | displayName: 'Install .NET Core SDK'
31 | inputs:
32 | packageType: sdk
33 | version: 9.0.x
34 | includePreviewVersions: false
35 | installationPath: $(Agent.ToolsDirectory)/dotnet
36 | - task: PowerShell@2
37 | displayName: Versioning
38 | inputs:
39 | targetType: filePath
40 | filePath: ./tools/scripts/set-version.ps1
41 | - task: DotNetCoreCLI@2
42 | displayName: Release Build
43 | inputs:
44 | command: build
45 | projects: '$(file)'
46 | arguments: '--configuration Release'
47 | - task: DotNetCoreCLI@2
48 | displayName: Debug Build
49 | inputs:
50 | command: build
51 | projects: '$(file)'
52 | arguments: '--configuration Debug'
53 | - job: testprep
54 | displayName: Prepare Test Jobs
55 | pool:
56 | vmImage: 'windows-2022'
57 | variables:
58 | skipComponentGovernanceDetection: true
59 | DOTNET_CLI_TELEMETRY_OPTOUT: true
60 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
61 | steps:
62 | - task: PowerShell@2
63 | name: testmatrix
64 | displayName: Prepare Tests
65 | inputs:
66 | targetType: filePath
67 | filePath: ./tools/scripts/get-matrix.ps1
68 | # arguments: -FileName Directory.Build.props
69 | arguments: -FileName azure-pipelines.yml
70 | - job: testall
71 | displayName: Run Tests for
72 | dependsOn: testprep
73 | strategy:
74 | matrix: $[dependencies.testprep.outputs['testmatrix.jobMatrix'] ]
75 | variables:
76 | skipComponentGovernanceDetection: true
77 | DOTNET_CLI_TELEMETRY_OPTOUT: true
78 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
79 | runCodesignValidationInjection: false
80 | pool:
81 | vmImage: $(poolImage)
82 | steps:
83 | - task: UseDotNet@2
84 | displayName: 'Install .NET Core SDK'
85 | inputs:
86 | packageType: sdk
87 | version: 9.0.x
88 | includePreviewVersions: false
89 | installationPath: $(Agent.ToolsDirectory)/dotnet
90 | - task: PowerShell@2
91 | displayName: Versioning
92 | inputs:
93 | targetType: filePath
94 | filePath: ./tools/scripts/set-version.ps1
95 | - task: DotNetCoreCLI@2
96 | displayName: Test
97 | timeoutInMinutes: 30
98 | inputs:
99 | command: test
100 | # projects: '$(folder)/**/tests/*.csproj'
101 | projects: '**/tests/*.csproj'
102 | arguments: '--configuration Release'
103 |
--------------------------------------------------------------------------------
/version.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
3 | "version": "2.12.35",
4 | "versionHeightOffset": -1,
5 | "publicReleaseRefSpec": [
6 | "^refs/heads/main$",
7 | "^refs/heads/release/\\d+\\.\\d+"
8 | ],
9 | "cloudBuild": {
10 | "setVersionVariables": true,
11 | "buildNumber": {
12 | "enabled": true,
13 | "includeCommitId": {
14 | "when": "nonPublicReleaseOnly",
15 | "where": "buildMetadata"
16 | }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------