├── .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 | --------------------------------------------------------------------------------