From 5e5a5fc1c8096e69ec83895fbd419ab62cdd0043 Mon Sep 17 00:00:00 2001 From: Ricardo Olsen Date: Wed, 2 Aug 2023 12:40:33 -0300 Subject: [PATCH] Version 0.5. --- OPC2PowerBI.sln | 16 +- OPC2PowerBI/App.config | 6 - OPC2PowerBI/OPC2PowerBI.csproj | 91 +--- OPC2PowerBI/Program.cs | 113 +++-- OPC2PowerBI/Properties/AssemblyInfo.cs | 36 -- OPC2PowerBI/opc2powerbi.conf | 26 +- README.md | 4 +- h-opc/Common/ClientExtensions.cs | 32 ++ h-opc/Common/IClient.cs | 99 ++++ h-opc/Common/Node.cs | 46 ++ h-opc/Common/OpcException.cs | 69 +++ h-opc/Common/OpcStatus.cs | 18 + h-opc/Common/Quality.cs | 28 ++ h-opc/Common/ReadEvent.cs | 32 ++ h-opc/Da/DaClient.cs | 298 +++++++++++ h-opc/Da/DaClient_async.cs | 42 ++ h-opc/Da/DaNode.cs | 22 + h-opc/Ua/ClientUtils.cs | 98 ++++ h-opc/Ua/NodeExtensions.cs | 24 + h-opc/Ua/UaClient.cs | 659 +++++++++++++++++++++++++ h-opc/Ua/UaClientOptions.cs | 120 +++++ h-opc/Ua/UaNode.cs | 29 ++ h-opc/h-opc.csproj | 15 + 23 files changed, 1722 insertions(+), 201 deletions(-) delete mode 100644 OPC2PowerBI/App.config delete mode 100644 OPC2PowerBI/Properties/AssemblyInfo.cs create mode 100644 h-opc/Common/ClientExtensions.cs create mode 100644 h-opc/Common/IClient.cs create mode 100644 h-opc/Common/Node.cs create mode 100644 h-opc/Common/OpcException.cs create mode 100644 h-opc/Common/OpcStatus.cs create mode 100644 h-opc/Common/Quality.cs create mode 100644 h-opc/Common/ReadEvent.cs create mode 100644 h-opc/Da/DaClient.cs create mode 100644 h-opc/Da/DaClient_async.cs create mode 100644 h-opc/Da/DaNode.cs create mode 100644 h-opc/Ua/ClientUtils.cs create mode 100644 h-opc/Ua/NodeExtensions.cs create mode 100644 h-opc/Ua/UaClient.cs create mode 100644 h-opc/Ua/UaClientOptions.cs create mode 100644 h-opc/Ua/UaNode.cs create mode 100644 h-opc/h-opc.csproj diff --git a/OPC2PowerBI.sln b/OPC2PowerBI.sln index 36fe786..d2b5461 100644 --- a/OPC2PowerBI.sln +++ b/OPC2PowerBI.sln @@ -1,16 +1,16 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28922.388 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33829.357 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OPC2PowerBI", "OPC2PowerBI\OPC2PowerBI.csproj", "{8B1E587C-CF3F-406F-9CAB-6B256D55EB49}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OPC2PowerBI", "OPC2PowerBI\OPC2PowerBI.csproj", "{8B1E587C-CF3F-406F-9CAB-6B256D55EB49}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{68E834A2-D28D-4051-9864-22F84C8738C8}" ProjectSection(SolutionItems) = preProject README.md = README.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "h-opc", "..\h-opc-master\h-opc\h-opc.csproj", "{4F43B6F0-0C32-4C34-978E-9B8B5B0B6E80}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "h-opc", "h-opc\h-opc.csproj", "{8DD14193-B42D-46DF-B10B-0A4184EFB7CD}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -22,10 +22,10 @@ Global {8B1E587C-CF3F-406F-9CAB-6B256D55EB49}.Debug|Any CPU.Build.0 = Debug|Any CPU {8B1E587C-CF3F-406F-9CAB-6B256D55EB49}.Release|Any CPU.ActiveCfg = Release|Any CPU {8B1E587C-CF3F-406F-9CAB-6B256D55EB49}.Release|Any CPU.Build.0 = Release|Any CPU - {4F43B6F0-0C32-4C34-978E-9B8B5B0B6E80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4F43B6F0-0C32-4C34-978E-9B8B5B0B6E80}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4F43B6F0-0C32-4C34-978E-9B8B5B0B6E80}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4F43B6F0-0C32-4C34-978E-9B8B5B0B6E80}.Release|Any CPU.Build.0 = Release|Any CPU + {8DD14193-B42D-46DF-B10B-0A4184EFB7CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8DD14193-B42D-46DF-B10B-0A4184EFB7CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8DD14193-B42D-46DF-B10B-0A4184EFB7CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8DD14193-B42D-46DF-B10B-0A4184EFB7CD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/OPC2PowerBI/App.config b/OPC2PowerBI/App.config deleted file mode 100644 index 56efbc7..0000000 --- a/OPC2PowerBI/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/OPC2PowerBI/OPC2PowerBI.csproj b/OPC2PowerBI/OPC2PowerBI.csproj index b8a0b08..ab22a48 100644 --- a/OPC2PowerBI/OPC2PowerBI.csproj +++ b/OPC2PowerBI/OPC2PowerBI.csproj @@ -1,85 +1,20 @@ - - - + + - Debug - AnyCPU - {8B1E587C-CF3F-406F-9CAB-6B256D55EB49} + net6.0 + h_opc + enable + disable Exe - OPC2PowerBI - OPC2PowerBI - v4.7.2 - 512 - true - true - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - False - ..\..\h-opc-master\ext_packages\Opc.Ua.Client.dll - - - False - ..\..\h-opc-master\ext_packages\Opc.Ua.Configuration.dll - - - False - ..\..\h-opc-master\ext_packages\Opc.Ua.Core.dll - - - False - ..\..\h-opc-master\ext_packages\OpcComRcw.dll - - - False - ..\..\h-opc-master\ext_packages\OpcNetApi.dll - - - False - ..\..\h-opc-master\ext_packages\OpcNetApi.Com.dll - - - - - - - - - - - - - - - - + - + + + - - {4f43b6f0-0c32-4c34-978e-9b8b5b0b6e80} - h-opc - + - - \ No newline at end of file + + diff --git a/OPC2PowerBI/Program.cs b/OPC2PowerBI/Program.cs index 36bd103..eca98ce 100644 --- a/OPC2PowerBI/Program.cs +++ b/OPC2PowerBI/Program.cs @@ -2,7 +2,7 @@ OPC2PowerBI OPC UA / DA --> Power BI (OData v4 JSON) - Copyright 2019 - Ricardo L. Olsen + Copyright 2019-2023 - Ricardo L. Olsen This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or @@ -15,15 +15,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -using System; -using System.Threading; using Hylasoft.Opc.Ua; -using System.Collections.Generic; using Hylasoft.Opc.Da; using System.Collections.Concurrent; using System.Globalization; -using System.IO; -using System.Linq; using System.Net; using System.Text; using System.Web; @@ -32,7 +27,7 @@ namespace OPC2PowerBI { class Program { - static public string Version = "OPC2PowerBI Version 0.4 - Copyright 2019-2020 - Ricardo L. Olsen"; + static public string Version = "OPC2PowerBI Version 0.5 - Copyright 2019-2023 - Ricardo L. Olsen"; static public string ConfigFile = "opc2powerbi.conf"; static public bool logevent = true; static public bool logread = true; @@ -112,8 +107,10 @@ static void ProcessUa(String URI, List entries, int readperiod, strin using (var client = new UaClient(new Uri(URI), options)) { + Console.WriteLine("Connecting UA " + URI); client.Connect(); - Console.WriteLine("Connect UA " + URI); + + Console.WriteLine("UA " + URI + " " + client.Status) ; foreach (OPC_entry entry in entries) { @@ -147,7 +144,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + sval); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + sval); }); } break; @@ -168,7 +165,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value.ToString("G", CultureInfo.CreateSpecificCulture("en-US"))); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value.ToString("G", CultureInfo.CreateSpecificCulture("en-US"))); }); } break; @@ -188,7 +185,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value.ToString("G", CultureInfo.CreateSpecificCulture("en-US"))); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value.ToString("G", CultureInfo.CreateSpecificCulture("en-US"))); }); } break; @@ -209,7 +206,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + sval); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + sval); }); } break; @@ -229,7 +226,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); }); } break; @@ -249,7 +246,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); }); } break; @@ -269,7 +266,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); }); } break; @@ -289,7 +286,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); }); } break; @@ -310,7 +307,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); }); } break; @@ -331,7 +328,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); }); } break; @@ -351,7 +348,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); }); } break; @@ -371,7 +368,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); }); } break; @@ -406,7 +403,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); }); } break; @@ -428,7 +425,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value.ToString()); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value.ToString()); }); } break; @@ -463,7 +460,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "float": @@ -481,7 +478,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "double": @@ -498,7 +495,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "system.decimal": @@ -514,7 +511,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "byte": @@ -531,7 +528,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "sbyte": @@ -548,7 +545,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "int16": @@ -565,7 +562,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "uint16": @@ -582,7 +579,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "integer": @@ -600,7 +597,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "statuscode": @@ -618,7 +615,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "int64": @@ -635,7 +632,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "uint64": @@ -652,7 +649,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "expandednodeid": @@ -684,7 +681,7 @@ static void ProcessUa(String URI, List entries, int readperiod, strin ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "time": @@ -704,11 +701,11 @@ static void ProcessUa(String URI, List entries, int readperiod, strin ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; default: - if (logread) Console.WriteLine("READ UNSUPPORTED TYPE: " + URI + " " + entry.opc_path + " " + stype); + if (logread) Console.WriteLine("READ UNSUPPORTED TYPE: " + appname + " " + entry.opc_path + " " + stype); break; } } @@ -721,14 +718,14 @@ static void ProcessUa(String URI, List entries, int readperiod, strin catch (Exception e) { // EXCEPTION HANDLER - Console.WriteLine("Exception UA " + URI); + Console.WriteLine("Exception UA " + appname); Console.WriteLine(e); System.Threading.Thread.Sleep(15000); } } while (true); } - static void ProcessDa(String URI, List entries, int readperiod) + static void ProcessDa(String URI, List entries, int readperiod, string appname) { CultureInfo ci = new CultureInfo("en-US"); Thread.CurrentThread.CurrentCulture = ci; @@ -775,7 +772,7 @@ static void ProcessDa(String URI, List entries, int readperiod) quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + sval); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + sval); }); } break; @@ -796,7 +793,7 @@ static void ProcessDa(String URI, List entries, int readperiod) quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value.ToString("G", CultureInfo.CreateSpecificCulture("en-US"))); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value.ToString("G", CultureInfo.CreateSpecificCulture("en-US"))); }); } break; @@ -818,7 +815,7 @@ static void ProcessDa(String URI, List entries, int readperiod) quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value.ToString("G", CultureInfo.CreateSpecificCulture("en-US"))); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value.ToString("G", CultureInfo.CreateSpecificCulture("en-US"))); }); } break; @@ -839,7 +836,7 @@ static void ProcessDa(String URI, List entries, int readperiod) quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); }); } break; @@ -860,7 +857,7 @@ static void ProcessDa(String URI, List entries, int readperiod) quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); }); } break; @@ -883,7 +880,7 @@ static void ProcessDa(String URI, List entries, int readperiod) quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); }); } break; @@ -913,7 +910,7 @@ static void ProcessDa(String URI, List entries, int readperiod) quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value); }); } break; @@ -935,7 +932,7 @@ static void ProcessDa(String URI, List entries, int readperiod) quality = readEvent.Quality }; MapValues[entry.tag] = ov; - if (logevent) Console.WriteLine("EVENT " + URI + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value.ToString()); + if (logevent) Console.WriteLine("EVENT " + appname + " " + entry.opc_path + " " + entry.tag + " " + readEvent.Value.ToString()); }); } break; @@ -974,7 +971,7 @@ static void ProcessDa(String URI, List entries, int readperiod) ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "vt_r4": @@ -991,7 +988,7 @@ static void ProcessDa(String URI, List entries, int readperiod) ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "vt_r8": @@ -1009,7 +1006,7 @@ static void ProcessDa(String URI, List entries, int readperiod) ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "vt_i1": @@ -1027,7 +1024,7 @@ static void ProcessDa(String URI, List entries, int readperiod) ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "vt_i2": @@ -1045,7 +1042,7 @@ static void ProcessDa(String URI, List entries, int readperiod) ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "state": @@ -1065,7 +1062,7 @@ static void ProcessDa(String URI, List entries, int readperiod) ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "string": @@ -1090,7 +1087,7 @@ static void ProcessDa(String URI, List entries, int readperiod) ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; case "vt_date": @@ -1110,11 +1107,11 @@ static void ProcessDa(String URI, List entries, int readperiod) ov.serverTimestamp = task.Result.ServerTimestamp; ov.quality = task.Result.Quality; MapValues[entry.tag] = ov; - if (logread) Console.WriteLine("READ " + URI + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); + if (logread) Console.WriteLine("READ " + appname + " " + entry.opc_path + " " + entry.tag + " " + ov.string_value); } break; default: - if (logread) Console.WriteLine("READ UNSUPPORTED TYPE: " + URI + " " + entry.opc_path + " " + stype); + if (logread) Console.WriteLine("READ UNSUPPORTED TYPE: " + appname + " " + entry.opc_path + " " + stype); break; } @@ -1128,7 +1125,7 @@ static void ProcessDa(String URI, List entries, int readperiod) catch (Exception e) { // EXCEPTION HANDLER - Console.WriteLine("Exception DA " + URI); + Console.WriteLine("Exception DA " + appname); Console.WriteLine(e.ToString().Substring(0, e.ToString().IndexOf(Environment.NewLine))); System.Threading.Thread.Sleep(3000); } @@ -1434,7 +1431,7 @@ static void Main(string[] args) } else { - Thread t = new Thread(() => ProcessDa(srv.opc_url, srv.entries, srv.read_period)); + Thread t = new Thread(() => ProcessDa(srv.opc_url, srv.entries, srv.read_period, srv.opc_server_name)); t.Start(); } } diff --git a/OPC2PowerBI/Properties/AssemblyInfo.cs b/OPC2PowerBI/Properties/AssemblyInfo.cs deleted file mode 100644 index 14eb61f..0000000 --- a/OPC2PowerBI/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// As informações gerais sobre um assembly são controladas por -// conjunto de atributos. Altere estes valores de atributo para modificar as informações -// associadas a um assembly. -[assembly: AssemblyTitle("OPC2PowerBI")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("OPC2PowerBI")] -[assembly: AssemblyCopyright("Copyright © 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Definir ComVisible como false torna os tipos neste assembly invisíveis -// para componentes COM. Caso precise acessar um tipo neste assembly de -// COM, defina o atributo ComVisible como true nesse tipo. -[assembly: ComVisible(false)] - -// O GUID a seguir será destinado à ID de typelib se este projeto for exposto para COM -[assembly: Guid("8b1e587c-cf3f-406f-9cab-6b256d55eb49")] - -// As informações da versão de um assembly consistem nos quatro valores a seguir: -// -// Versão Principal -// Versão Secundária -// Número da Versão -// Revisão -// -// É possível especificar todos os valores ou usar como padrão os Números de Build e da Revisão -// usando o "*" como mostrado abaixo: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/OPC2PowerBI/opc2powerbi.conf b/OPC2PowerBI/opc2powerbi.conf index cc5c67d..72dc1d6 100644 --- a/OPC2PowerBI/opc2powerbi.conf +++ b/OPC2PowerBI/opc2powerbi.conf @@ -24,19 +24,19 @@ #ns=1;s=Countries.US.Queens.Timezone ,String ,N ,US.Queens.Timezone #ns=1;s=Countries.US.Queens.Icon ,String ,N ,US.Queens.Icon -#opc.tcp://opcuaserver.com:48010, 30, Server2 -#ns=2;s=Demo.Dynamic.Scalar.Double ,Double ,N ,Demo.Dynamic.Scalar.Double -#ns=2;s=Demo.Dynamic.Scalar.Int16 ,Int16 ,N ,Demo.Dynamic.Scalar.Int16 -#ns=2;s=Demo.Dynamic.Scalar.Boolean ,Boolean ,N ,Demo.Dynamic.Scalar.Boolean -#ns=2;s=Demo.Dynamic.Scalar.Byte ,Byte ,N ,Demo.Dynamic.Scalar.Byte -#ns=2;s=Demo.Dynamic.Scalar.SByte ,SByte ,N ,Demo.Dynamic.Scalar.SByte -#ns=3;s=AirConditioner_1.Temperature ,Double ,Y ,AirConditioner_1.Temperature +opc.tcp://opcuaserver.com:48010, 30, Server2 +ns=2;s=Demo.Dynamic.Scalar.Double ,Double ,N ,Demo.Dynamic.Scalar.Double +ns=2;s=Demo.Dynamic.Scalar.Int16 ,Int16 ,N ,Demo.Dynamic.Scalar.Int16 +ns=2;s=Demo.Dynamic.Scalar.Boolean ,Boolean ,N ,Demo.Dynamic.Scalar.Boolean +ns=2;s=Demo.Dynamic.Scalar.Byte ,Byte ,N ,Demo.Dynamic.Scalar.Byte +ns=2;s=Demo.Dynamic.Scalar.SByte ,SByte ,N ,Demo.Dynamic.Scalar.SByte +ns=3;s=AirConditioner_1.Temperature ,Double ,Y ,AirConditioner_1.Temperature -opc.tcp://opcua.demo-this.com:51210/UA/SampleServer, 15, Server3 -ns=2;i=10851 ,Int64 ,N ,Data.Dynamic.Scalar.Int64Value -ns=2;i=10856 ,DateTime ,N ,Data.Dynamic.Scalar.DateTimeValue -ns=2;i=10844 ,Boolean ,Y ,Data.Dynamic.Scalar.BooleanValue -ns=2;i=10855 ,String ,N ,Data.Dynamic.Scalar.StringValue +#opc.tcp://opcua.demo-this.com:51210/UA/SampleServer, 15, Server3 +#ns=2;i=10851 ,Int64 ,N ,Data.Dynamic.Scalar.Int64Value +#ns=2;i=10856 ,DateTime ,N ,Data.Dynamic.Scalar.DateTimeValue +#ns=2;i=10844 ,Boolean ,Y ,Data.Dynamic.Scalar.BooleanValue +#ns=2;i=10855 ,String ,N ,Data.Dynamic.Scalar.StringValue ##################################################################################################################### @@ -67,3 +67,5 @@ ns=2;i=10855 ,String ,N ,Dat #Random.PsState1 ,state ,N , #Random.PsDateTime1 ,DateTime ,N , +opc.tcp://150.230.171.172:4840/, 15, JsonScada +ns=2;i=3287 ,Double ,Y ,KAW2AL-21MAPH--B \ No newline at end of file diff --git a/README.md b/README.md index 806cfc0..4d45308 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,7 @@ The code is written in C# and it uses the h-OPC library for C#. _Warning: The h-opc library is unmantained legacy and is known to have problems to enable OPC connections using certificates_. -Requires the .NET fremework 4.6 or later. - -Requires also the forked h-opc library https://github.com/riclolsen/h-opc. +Requires the .NET 6.0 runtime or later. Executable binaries are available for download in the Releases section. diff --git a/h-opc/Common/ClientExtensions.cs b/h-opc/Common/ClientExtensions.cs new file mode 100644 index 0000000..3116830 --- /dev/null +++ b/h-opc/Common/ClientExtensions.cs @@ -0,0 +1,32 @@ +namespace Hylasoft.Opc.Common +{ + /// + /// Useful extension methods for OPC Clients + /// + public static class ClientExtensions + { + /// + /// Reads a tag from the OPC. If for whatever reason the read fails (Tag doesn't exist, server not available) returns a default value + /// + /// the opc client to use for the read + /// The fully qualified identifier of the tag + /// the default value to read if the read fails + /// + public static ReadEvent ReadOrdefault(this IClient client, string tag, T defaultValue = default(T)) + { + try + { + return client.Read(tag); + } + catch (OpcException) + { + var readEvent = new ReadEvent(); + readEvent.Quality = Quality.Good; + readEvent.Value = defaultValue; + readEvent.SourceTimestamp = DateTime.Now; + readEvent.ServerTimestamp = DateTime.Now; + return readEvent; + } + } + } +} \ No newline at end of file diff --git a/h-opc/Common/IClient.cs b/h-opc/Common/IClient.cs new file mode 100644 index 0000000..dbfa34d --- /dev/null +++ b/h-opc/Common/IClient.cs @@ -0,0 +1,99 @@ +namespace Hylasoft.Opc.Common +{ + /// + /// Client interface to perform basic Opc tasks, like discovery, monitoring, reading/writing tags, + /// + public interface IClient : IDisposable + where TNode : Node + { + /// + /// Connect the client to the OPC Server + /// + void Connect(); + + /// + /// Gets the current status of the OPC Client + /// + OpcStatus Status { get; } + + /// + /// Gets the datatype of an OPC tag + /// + /// Tag to get datatype of + /// System Type + System.Type GetDataType(string tag); + + /// + /// Read a tag + /// + /// The type of tag to read + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` reads the tag `bar` on the folder `foo` + /// The value retrieved from the OPC + ReadEvent Read(string tag); + + /// + /// Write a value on the specified opc tag + /// + /// The type of tag to write on + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` writes on the tag `bar` on the folder `foo` + /// + void Write(string tag, T item); + + /// + /// Monitor the specified tag for changes + /// + /// the type of tag to monitor + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` monitors the tag `bar` on the folder `foo` + /// the callback to execute when the value is changed. + /// The first parameter is the new value of the node, the second is an `unsubscribe` function to unsubscribe the callback + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an async method.")] + void Monitor(string tag, Action, Action> callback); + + /// + /// Finds a node on the Opc Server + /// + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` finds the tag `bar` on the folder `foo` + /// If there is a tag, it returns it, otherwise it throws an + TNode FindNode(string tag); + + /// + /// Gets the root node of the server + /// + TNode RootNode { get; } + + /// + /// Explore a folder on the Opc Server + /// + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` finds the sub nodes of `bar` on the folder `foo` + /// The list of sub-nodes + IEnumerable ExploreFolder(string tag); + + /// + /// Read a tag asynchronusly + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an async method.")] + Task> ReadAsync(string tag); + + /// + /// Write a value on the specified opc tag asynchronously + /// + Task WriteAsync(string tag, T item); + + /// + /// Finds a node on the Opc Server asynchronously + /// + Task FindNodeAsync(string tag); + + /// + /// Explore a folder on the Opc Server asynchronously + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", + Justification = "Task")] + Task> ExploreFolderAsync(string tag); + } +} \ No newline at end of file diff --git a/h-opc/Common/Node.cs b/h-opc/Common/Node.cs new file mode 100644 index 0000000..ae6750f --- /dev/null +++ b/h-opc/Common/Node.cs @@ -0,0 +1,46 @@ +namespace Hylasoft.Opc.Common +{ + /// + /// Base class representing a node on the OPC server + /// + public abstract class Node + { + /// + /// Gets the displayed name of the node + /// + public string Name { get; protected set; } + + /// + /// Gets the dot-separated fully qualified tag of the node + /// + public string Tag { get; protected set; } + + /// + /// Gets the parent node. If the node is root, returns null + /// + public Node Parent { get; private set; } + + /// + /// Creates a new node + /// + /// the name of the node + /// The parent node + protected Node(string name, Node parent = null) + { + Name = name; + Parent = parent; + if (parent != null && !string.IsNullOrEmpty(parent.Tag)) + Tag = parent.Tag + '.' + name; + else + Tag = name; + } + + /// + /// Overrides ToString() + /// + public override string ToString() + { + return Tag; + } + } +} diff --git a/h-opc/Common/OpcException.cs b/h-opc/Common/OpcException.cs new file mode 100644 index 0000000..adfead0 --- /dev/null +++ b/h-opc/Common/OpcException.cs @@ -0,0 +1,69 @@ +using Opc.Ua; +using System.Runtime.Serialization; + +namespace Hylasoft.Opc.Common +{ + /// + /// Identifies an exception occurred during OPC Communication + /// + [Serializable] + public class OpcException : Exception + { + /// + /// Initialize a new instance of the OpcException class + /// + public OpcException() + { + } + + /// + /// Initialize a new instance of the OpcException class + /// + public OpcException(string message) + : base(message) + { + } + + /// + /// Returns an (optional) associated OPC UA StatusCode for the exception. + /// + public StatusCode? Status { get; private set; } + + /// + /// Initialize a new instance of the OpcException class + /// + public OpcException(string message, StatusCode status) + : base(message) + { + Status = status; + } + + /// + /// Initialize a new instance of the OpcException class + /// + public OpcException(string message, Exception inner) + : base(message, inner) + { + } + + /// + /// Initialize a new instance of the OpcException class + /// + protected OpcException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + /// + /// Sets the System.Runtime.Serialization.SerializationInfo with information about the exception. + /// + /// The System.Runtime.Serialization.SerializationInfo that holds the serialized object data about the exception being thrown. + /// The System.Runtime.Serialization.StreamingContext that contains contextual information about the source or destination. + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + } + + } + +} \ No newline at end of file diff --git a/h-opc/Common/OpcStatus.cs b/h-opc/Common/OpcStatus.cs new file mode 100644 index 0000000..3534d4a --- /dev/null +++ b/h-opc/Common/OpcStatus.cs @@ -0,0 +1,18 @@ +namespace Hylasoft.Opc.Common +{ + /// + /// Identifies the status of an OPC connector + /// + public enum OpcStatus + { + /// + /// The client is not connected + /// + NotConnected, + + /// + /// The client is connected + /// + Connected + } +} \ No newline at end of file diff --git a/h-opc/Common/Quality.cs b/h-opc/Common/Quality.cs new file mode 100644 index 0000000..b9bfcc9 --- /dev/null +++ b/h-opc/Common/Quality.cs @@ -0,0 +1,28 @@ +using System.ComponentModel; + +namespace Hylasoft.Opc.Common +{ + /// + /// Represents the quality of the value captured + /// + public enum Quality + { + /// + /// Quality: Unknown, the value of the quality could not be inferred by the library + /// + [Description("Unknown")] + Unknown, + + /// + /// Quality: Good + /// + [Description("Good")] + Good, + + /// + /// Quality: Bad + /// + [Description("Bad")] + Bad + } +} \ No newline at end of file diff --git a/h-opc/Common/ReadEvent.cs b/h-opc/Common/ReadEvent.cs new file mode 100644 index 0000000..6bc48d2 --- /dev/null +++ b/h-opc/Common/ReadEvent.cs @@ -0,0 +1,32 @@ +using System.ComponentModel; + +namespace Hylasoft.Opc.Common +{ + /// + /// Base class representing a monitor event on the OPC server + /// + /// + public class ReadEvent + { + /// + /// Gets the value that was read from the server + /// + public T Value { get; set; } + + /// + /// Gets the quality of the signal from the server + /// + [DefaultValue(Common.Quality.Unknown)] + public Quality Quality { get; set; } + + /// + /// Gets the source timestamp on when the event ocurred + /// + public DateTime SourceTimestamp { get; set; } + + /// + /// Gets the server timestamp on when the event ocurred + /// + public DateTime ServerTimestamp { get; set; } + } +} diff --git a/h-opc/Da/DaClient.cs b/h-opc/Da/DaClient.cs new file mode 100644 index 0000000..456fe48 --- /dev/null +++ b/h-opc/Da/DaClient.cs @@ -0,0 +1,298 @@ +using Hylasoft.Opc.Common; +using System.Globalization; +using Technosoftware.DaAeHdaClient; +using Factory = Technosoftware.DaAeHdaClient.Com.Factory; +using OpcDa = Technosoftware.DaAeHdaClient.Da; + +namespace Hylasoft.Opc.Da +{ + /// + /// Client Implementation for DA + /// + public partial class DaClient : IClient + { + private readonly OpcUrl _url; + private OpcDa.TsCDaServer _server; + private long _sub; + private readonly IDictionary _nodesCache = new Dictionary(); + + // default monitor interval in Milliseconds + private const int DefaultMonitorInterval = 100; + + /// + /// Initialize a new Data Access Client + /// + /// The url of the server to connect to. WARNING: If server URL includes + /// spaces (ex. "RSLinx OPC Server") then pass the server URL in to the constructor as an Opc.URL object + /// directly instead. + public DaClient(Uri serverUrl) + { + _url = new OpcUrl(serverUrl.OriginalString) + { + Scheme = serverUrl.Scheme, + HostName = serverUrl.Host + }; + } + + /// + /// Initialize a new Data Access Client + /// + /// The url of the server to connect to + public DaClient(OpcUrl serverUrl) + { + _url = serverUrl; + } + + /// + /// Gets the datatype of an OPC tag + /// + /// Tag to get datatype of + /// System Type + public System.Type GetDataType(string tag) + { + var item = new OpcDa.TsCDaItem { ItemName = tag }; + OpcDa.TsCDaItemProperty result; + try + { + var propertyCollection = _server.GetProperties(new[] { item }, new[] { new OpcDa.TsDaPropertyID(1) }, false)[0]; + result = propertyCollection[0]; + } + catch (NullReferenceException) + { + throw new OpcException("Could not find node because server not connected."); + } + return result.DataType; + } + + /// + /// OpcDa underlying server object. + /// + protected OpcDa.TsCDaServer Server + { + get + { + return _server; + } + } + + #region interface methods + + /// + /// Connect the client to the OPC Server + /// + public void Connect() + { + if (Status == OpcStatus.Connected) + return; + _server = new OpcDa.TsCDaServer(new Factory(), _url); + _server.Connect(); + var root = new DaNode(string.Empty, string.Empty); + RootNode = root; + AddNodeToCache(root); + } + + /// + /// Gets the current status of the OPC Client + /// + public OpcStatus Status + { + get + { + if (_server == null || _server.GetServerStatus().ServerState != OpcServerState.Operational) + return OpcStatus.NotConnected; + return OpcStatus.Connected; + } + } + + /// + /// Read a tag + /// + /// The type of tag to read + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` reads the tag `bar` on the folder `foo` + /// The value retrieved from the OPC + public ReadEvent Read(string tag) + { + var item = new OpcDa.TsCDaItem { ItemName = tag }; + if (Status == OpcStatus.NotConnected) + { + throw new OpcException("Server not connected. Cannot read tag."); + } + var result = _server.Read(new[] { item })[0]; + T casted; + TryCastResult(result.Value, out casted); + + var readEvent = new ReadEvent(); + readEvent.Value = casted; + readEvent.SourceTimestamp = result.Timestamp; + readEvent.ServerTimestamp = result.Timestamp; + if (result.Quality == OpcDa.TsCDaQuality.Good) readEvent.Quality = Quality.Good; + if (result.Quality == OpcDa.TsCDaQuality.Bad) readEvent.Quality = Quality.Bad; + + return readEvent; + } + + /// + /// Write a value on the specified opc tag + /// + /// The type of tag to write on + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` writes on the tag `bar` on the folder `foo` + /// + public void Write(string tag, T item) + { + var itmVal = new OpcDa.TsCDaItemValue + { + ItemName = tag, + Value = item + }; + var result = _server.Write(new[] { itmVal })[0]; + CheckResult(result, tag); + } + + /// + /// Casts result of monitoring and reading values + /// + /// Value to convert + /// The casted result + /// Type of object to try to cast + public void TryCastResult(object value, out T casted) + { + try + { + casted = (T)value; + } + catch (InvalidCastException) + { + throw new InvalidCastException( + string.Format( + "Could not monitor tag. Cast failed for type \"{0}\" on the new value \"{1}\" with type \"{2}\". Make sure tag data type matches.", + typeof(T), value, value.GetType())); + } + } + + /// + /// Monitor the specified tag for changes + /// + /// the type of tag to monitor + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` monitors the tag `bar` on the folder `foo` + /// the callback to execute when the value is changed. + /// The first parameter is a MonitorEvent object which represents the data point, the second is an `unsubscribe` function to unsubscribe the callback + public void Monitor(string tag, Action, Action> callback) + { + var subItem = new OpcDa.TsCDaSubscriptionState + { + Name = (++_sub).ToString(CultureInfo.InvariantCulture), + Active = true, + UpdateRate = DefaultMonitorInterval + }; + var sub = _server.CreateSubscription(subItem); + + // I have to start a new thread here because unsubscribing + // the subscription during a datachanged event causes a deadlock + Action unsubscribe = () => new Thread(o => + _server.CancelSubscription(sub)).Start(); + + sub.DataChangedEvent += (handle, requestHandle, values) => + { + T casted; + TryCastResult(values[0].Value, out casted); + var monitorEvent = new ReadEvent(); + monitorEvent.Value = casted; + monitorEvent.SourceTimestamp = values[0].Timestamp; + monitorEvent.ServerTimestamp = values[0].Timestamp; + if (values[0].Quality == OpcDa.TsCDaQuality.Good) monitorEvent.Quality = Quality.Good; + if (values[0].Quality == OpcDa.TsCDaQuality.Bad) monitorEvent.Quality = Quality.Bad; + callback(monitorEvent, unsubscribe); + }; + sub.AddItems(new[] { new OpcDa.TsCDaItem { ItemName = tag } }); + sub.SetEnabled(true); + } + + /// + /// Finds a node on the Opc Server + /// + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` finds the tag `bar` on the folder `foo` + /// If there is a tag, it returns it, otherwise it throws an + public DaNode FindNode(string tag) + { + // if the tag already exists in cache, return it + if (_nodesCache.ContainsKey(tag)) + return _nodesCache[tag]; + + // try to find the tag otherwise + var item = new OpcDa.TsCDaItem { ItemName = tag }; + OpcDa.TsCDaItemValueResult result; + try + { + result = _server.Read(new[] { item })[0]; + } + catch (NullReferenceException) + { + throw new OpcException("Could not find node because server not connected."); + } + CheckResult(result, tag); + var node = new DaNode(item.ItemName, item.ItemName, RootNode); + AddNodeToCache(node); + return node; + } + + /// + /// Gets the root node of the server + /// + public DaNode RootNode { get; private set; } + + /// + /// Explore a folder on the Opc Server + /// + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` finds the sub nodes of `bar` on the folder `foo` + /// The list of sub-nodes + public IEnumerable ExploreFolder(string tag) + { + var parent = FindNode(tag); + OpcDa.TsCDaBrowsePosition p; + var nodes = _server.Browse(new OpcItem(parent.Tag), new OpcDa.TsCDaBrowseFilters(), out p) + .Select(t => new DaNode(t.Name, t.ItemName, parent)) + .ToList(); + //add nodes to cache + foreach (var node in nodes) + AddNodeToCache(node); + + return nodes; + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + if (_server != null) + _server.Dispose(); + GC.SuppressFinalize(this); + } + + #endregion + + /// + /// Adds a node to the cache using the tag as its key + /// + /// the node to add + private void AddNodeToCache(DaNode node) + { + if (!_nodesCache.ContainsKey(node.Tag)) + _nodesCache.Add(node.Tag, node); + } + + private static void CheckResult(IOpcResult result, string tag) + { + if (result == null) + throw new OpcException("The server replied with an empty response"); + if (result.Result.ToString() != "S_OK" && result.Result.ToString() != "E_READONLY") + throw new OpcException(string.Format("Invalid response from the server. (Response Status: {0}, Opc Tag: {1})", result.Result, tag)); + } + } +} + diff --git a/h-opc/Da/DaClient_async.cs b/h-opc/Da/DaClient_async.cs new file mode 100644 index 0000000..d974466 --- /dev/null +++ b/h-opc/Da/DaClient_async.cs @@ -0,0 +1,42 @@ +using Hylasoft.Opc.Common; + +namespace Hylasoft.Opc.Da +{ + /// + /// Client Implementation for DA + /// + public partial class DaClient + { + /// + /// Read a tag asynchronusly + /// + public async Task> ReadAsync(string tag) + { + return await Task.Run(() => Read(tag)); + } + + /// + /// Write a value on the specified opc tag asynchronously + /// + public async Task WriteAsync(string tag, T item) + { + await Task.Run(() => Write(tag, item)); + } + + /// + /// Finds a node on the Opc Server asynchronously + /// + public async Task FindNodeAsync(string tag) + { + return await Task.Run(() => FindNode(tag)); + } + + /// + /// Explore a folder on the Opc Server asynchronously + /// + public async Task> ExploreFolderAsync(string tag) + { + return await Task.Run(() => ExploreFolder(tag)); + } + } +} diff --git a/h-opc/Da/DaNode.cs b/h-opc/Da/DaNode.cs new file mode 100644 index 0000000..9cc1edd --- /dev/null +++ b/h-opc/Da/DaNode.cs @@ -0,0 +1,22 @@ +using Hylasoft.Opc.Common; + +namespace Hylasoft.Opc.Da +{ + /// + /// Represents a node to be used specifically for OPC DA + /// + public class DaNode : Node + { + /// + /// Instantiates a DaNode class + /// + /// the name of the node + /// + /// The parent node + public DaNode(string name, string tag, Node parent = null) + : base(name, parent) + { + Tag = tag; + } + } +} diff --git a/h-opc/Ua/ClientUtils.cs b/h-opc/Ua/ClientUtils.cs new file mode 100644 index 0000000..5e43f78 --- /dev/null +++ b/h-opc/Ua/ClientUtils.cs @@ -0,0 +1,98 @@ +using Opc.Ua; +using Opc.Ua.Client; + +namespace Hylasoft.Opc.Ua +{ + /// + /// List of static utility methods + /// + internal static class ClientUtils + { + // TODO I didn't write these methods. I should rewrite it once I understand whtat it does, beacuse it looks crazy + + public static EndpointDescription SelectEndpoint(Uri discoveryUrl, bool useSecurity) + { + var configuration = EndpointConfiguration.Create(); + configuration.OperationTimeout = 5000; + EndpointDescription endpointDescription1 = null; + using (var discoveryClient = DiscoveryClient.Create(discoveryUrl, configuration)) + { + var endpoints = discoveryClient.GetEndpoints(null); + foreach (var endpointDescription2 in endpoints.Where(endpointDescription2 => endpointDescription2.EndpointUrl.StartsWith(discoveryUrl.Scheme))) + { + if (useSecurity) + { + if (endpointDescription2.SecurityMode == MessageSecurityMode.None) + continue; + } + else if (endpointDescription2.SecurityMode != MessageSecurityMode.None) + continue; + if (endpointDescription1 == null) + endpointDescription1 = endpointDescription2; + if (endpointDescription2.SecurityLevel > endpointDescription1.SecurityLevel) + endpointDescription1 = endpointDescription2; + } + if (endpointDescription1 == null) + { + if (endpoints.Count > 0) + endpointDescription1 = endpoints[0]; + } + } + var uri = Utils.ParseUri(endpointDescription1.EndpointUrl); + if (uri != null && uri.Scheme == discoveryUrl.Scheme) + endpointDescription1.EndpointUrl = new UriBuilder(uri) + { + Host = discoveryUrl.DnsSafeHost, + Port = discoveryUrl.Port + }.ToString(); + return endpointDescription1; + } + + public static ReferenceDescriptionCollection Browse(Session session, NodeId nodeId) + { + var desc = new BrowseDescription + { + NodeId = nodeId, + BrowseDirection = BrowseDirection.Forward, + IncludeSubtypes = true, + NodeClassMask = 0U, + ResultMask = 63U, + }; + return Browse(session, desc, true); + } + + public static ReferenceDescriptionCollection Browse(Session session, BrowseDescription nodeToBrowse, bool throwOnError) + { + try + { + var descriptionCollection = new ReferenceDescriptionCollection(); + var nodesToBrowse = new BrowseDescriptionCollection { nodeToBrowse }; + BrowseResultCollection results; + DiagnosticInfoCollection diagnosticInfos; + session.Browse(null, null, 0U, nodesToBrowse, out results, out diagnosticInfos); + ClientBase.ValidateResponse(results, nodesToBrowse); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToBrowse); + while (!StatusCode.IsBad(results[0].StatusCode)) + { + for (var index = 0; index < results[0].References.Count; ++index) + descriptionCollection.Add(results[0].References[index]); + if (results[0].References.Count == 0 || results[0].ContinuationPoint == null) + return descriptionCollection; + var continuationPoints = new ByteStringCollection(); + continuationPoints.Add(results[0].ContinuationPoint); + session.BrowseNext(null, false, continuationPoints, out results, out diagnosticInfos); + ClientBase.ValidateResponse(results, continuationPoints); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, continuationPoints); + } + throw new ServiceResultException(results[0].StatusCode); + } + catch (Exception ex) + { + if (throwOnError) + throw new ServiceResultException(ex, 2147549184U); + return null; + } + } + } +} + diff --git a/h-opc/Ua/NodeExtensions.cs b/h-opc/Ua/NodeExtensions.cs new file mode 100644 index 0000000..8913f4b --- /dev/null +++ b/h-opc/Ua/NodeExtensions.cs @@ -0,0 +1,24 @@ +using Hylasoft.Opc.Common; +using OpcF = Opc.Ua; + +namespace Hylasoft.Opc.Ua +{ + /// + /// Class with extension methods for OPC UA + /// + public static class NodeExtensions + { + /// + /// Converts an OPC Foundation node to an Hylasoft OPC UA Node + /// + /// The node to convert + /// the parent node (optional) + /// + internal static UaNode ToHylaNode(this OpcF.ReferenceDescription node, Node parent = null) + { + var name = node.DisplayName.ToString(); + var nodeId = node.NodeId.ToString(); + return new UaNode(name, nodeId, parent); + } + } +} \ No newline at end of file diff --git a/h-opc/Ua/UaClient.cs b/h-opc/Ua/UaClient.cs new file mode 100644 index 0000000..aa400d6 --- /dev/null +++ b/h-opc/Ua/UaClient.cs @@ -0,0 +1,659 @@ +using Hylasoft.Opc.Common; +using Opc.Ua; +using Opc.Ua.Client; +using Opc.Ua.Configuration; + +namespace Hylasoft.Opc.Ua +{ + /// + /// Client Implementation for UA + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", + Justification = "Doesn't make sense to split this class")] + public class UaClient : IClient + { + private readonly UaClientOptions _options = new UaClientOptions(); + private readonly Uri _serverUrl; + private Session _session; + + private readonly IDictionary _nodesCache = new Dictionary(); + private readonly IDictionary> _folderCache = new Dictionary>(); + + /// + /// Creates a server object + /// + /// the url of the server to connect to + public UaClient(Uri serverUrl) + { + _serverUrl = serverUrl; + Status = OpcStatus.NotConnected; + } + + /// + /// Creates a server object + /// + /// the url of the server to connect to + /// custom options to use with ua client + public UaClient(Uri serverUrl, UaClientOptions options) + { + _serverUrl = serverUrl; + _options = options; + Status = OpcStatus.NotConnected; + } + + /// + /// Options to configure the UA client session + /// + public UaClientOptions Options + { + get { return _options; } + } + + /// + /// OPC Foundation underlying session object + /// + protected Session Session + { + get + { + return _session; + } + } + + private void PostInitializeSession() + { + var node = _session.NodeCache.Find(ObjectIds.ObjectsFolder); + RootNode = new UaNode(string.Empty, node.NodeId.ToString()); + AddNodeToCache(RootNode); + Status = OpcStatus.Connected; + } + + /// + /// Connect the client to the OPC Server + /// + public void Connect() + { + if (Status == OpcStatus.Connected) + return; + _session = InitializeSession(_serverUrl).Result; + _session.KeepAlive += SessionKeepAlive; + _session.SessionClosing += SessionClosing; + PostInitializeSession(); + } + + /// + /// Gets the datatype of an OPC tag + /// + /// Tag to get datatype of + /// System Type + public System.Type GetDataType(string tag) + { + var nodesToRead = BuildReadValueIdCollection(tag, Attributes.Value); + DataValueCollection results; + DiagnosticInfoCollection diag; + _session.Read( + requestHeader: null, + maxAge: 0, + timestampsToReturn: TimestampsToReturn.Neither, + nodesToRead: nodesToRead, + results: out results, + diagnosticInfos: out diag); + var type = results[0].WrappedValue.TypeInfo.BuiltInType; + return System.Type.GetType("System." + type.ToString()); + } + + private void SessionKeepAlive(ISession session, KeepAliveEventArgs e) + { + if (e.CurrentState != ServerState.Running) + { + if (Status == OpcStatus.Connected) + { + Status = OpcStatus.NotConnected; + NotifyServerConnectionLost(); + } + } + else if (e.CurrentState == ServerState.Running) + { + if (Status == OpcStatus.NotConnected) + { + Status = OpcStatus.Connected; + NotifyServerConnectionRestored(); + } + } + } + + private void SessionClosing(object sender, EventArgs e) + { + Status = OpcStatus.NotConnected; + NotifyServerConnectionLost(); + } + + + /// + /// Reconnect the OPC session + /// + public void ReConnect() + { + Status = OpcStatus.NotConnected; + _session.Reconnect(); + Status = OpcStatus.Connected; + } + + /// + /// Create a new OPC session, based on the current session parameters. + /// + public void RecreateSession() + { + Status = OpcStatus.NotConnected; + _session = Session.Recreate(_session); + PostInitializeSession(); + } + + + /// + /// Gets the current status of the OPC Client + /// + public OpcStatus Status { get; private set; } + + + private ReadValueIdCollection BuildReadValueIdCollection(string tag, uint attributeId) + { + var n = FindNode(tag, RootNode); + var readValue = new ReadValueId + { + NodeId = n.NodeId, + AttributeId = attributeId + }; + return new ReadValueIdCollection { readValue }; + } + + /// + /// Read a tag + /// + /// The type of tag to read + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` reads the tag `bar` on the folder `foo` + /// The value retrieved from the OPC + public ReadEvent Read(string tag) + { + var nodesToRead = BuildReadValueIdCollection(tag, Attributes.Value); + DataValueCollection results; + DiagnosticInfoCollection diag; + _session.Read( + requestHeader: null, + maxAge: 0, + timestampsToReturn: TimestampsToReturn.Neither, + nodesToRead: nodesToRead, + results: out results, + diagnosticInfos: out diag); + var val = results[0]; + + var readEvent = new ReadEvent(); + readEvent.Value = (T)val.Value; + readEvent.SourceTimestamp = val.SourceTimestamp; + readEvent.ServerTimestamp = val.ServerTimestamp; + if (StatusCode.IsGood(val.StatusCode)) readEvent.Quality = Quality.Good; + if (StatusCode.IsBad(val.StatusCode)) readEvent.Quality = Quality.Bad; + return readEvent; + } + + + /// + /// Read a tag asynchronously + /// + /// The type of tag to read + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` reads the tag `bar` on the folder `foo` + /// The value retrieved from the OPC + public Task> ReadAsync(string tag) + { + var nodesToRead = BuildReadValueIdCollection(tag, Attributes.Value); + + // Wrap the ReadAsync logic in a TaskCompletionSource, so we can use C# async/await syntax to call it: + var taskCompletionSource = new TaskCompletionSource>(); + _session.BeginRead( + requestHeader: null, + maxAge: 0, + timestampsToReturn: TimestampsToReturn.Neither, + nodesToRead: nodesToRead, + callback: ar => + { + DataValueCollection results; + DiagnosticInfoCollection diag; + var response = _session.EndRead( + result: ar, + results: out results, + diagnosticInfos: out diag); + + try + { + CheckReturnValue(response.ServiceResult); + var val = results[0]; + var readEvent = new ReadEvent(); + readEvent.Value = (T)val.Value; + readEvent.SourceTimestamp = val.SourceTimestamp; + readEvent.ServerTimestamp = val.ServerTimestamp; + if (StatusCode.IsGood(val.StatusCode)) readEvent.Quality = Quality.Good; + if (StatusCode.IsBad(val.StatusCode)) readEvent.Quality = Quality.Bad; + taskCompletionSource.TrySetResult(readEvent); + } + catch (Exception ex) + { + taskCompletionSource.TrySetException(ex); + } + }, + asyncState: null); + + return taskCompletionSource.Task; + } + + + private WriteValueCollection BuildWriteValueCollection(string tag, uint attributeId, object dataValue) + { + var n = FindNode(tag, RootNode); + var writeValue = new WriteValue + { + NodeId = n.NodeId, + AttributeId = attributeId, + Value = { Value = dataValue } + }; + return new WriteValueCollection { writeValue }; + } + + /// + /// Write a value on the specified opc tag + /// + /// The type of tag to write on + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` writes on the tag `bar` on the folder `foo` + /// The value for the item to write + public void Write(string tag, T item) + { + var nodesToWrite = BuildWriteValueCollection(tag, Attributes.Value, item); + + StatusCodeCollection results; + DiagnosticInfoCollection diag; + _session.Write( + requestHeader: null, + nodesToWrite: nodesToWrite, + results: out results, + diagnosticInfos: out diag); + + CheckReturnValue(results[0]); + } + + /// + /// Write a value on the specified opc tag asynchronously + /// + /// The type of tag to write on + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` writes on the tag `bar` on the folder `foo` + /// The value for the item to write + public Task WriteAsync(string tag, T item) + { + var nodesToWrite = BuildWriteValueCollection(tag, Attributes.Value, item); + + // Wrap the WriteAsync logic in a TaskCompletionSource, so we can use C# async/await syntax to call it: + var taskCompletionSource = new TaskCompletionSource(); + _session.BeginWrite( + requestHeader: null, + nodesToWrite: nodesToWrite, + callback: ar => + { + StatusCodeCollection results; + DiagnosticInfoCollection diag; + var response = _session.EndWrite( + result: ar, + results: out results, + diagnosticInfos: out diag); + try + { + CheckReturnValue(response.ServiceResult); + CheckReturnValue(results[0]); + taskCompletionSource.SetResult(response.ServiceResult); + } + catch (Exception ex) + { + taskCompletionSource.TrySetException(ex); + } + }, + asyncState: null); + return taskCompletionSource.Task; + } + + + /// + /// Monitor the specified tag for changes + /// + /// the type of tag to monitor + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` monitors the tag `bar` on the folder `foo` + /// the callback to execute when the value is changed. + /// The first parameter is a MonitorEvent object which represents the data point, the second is an `unsubscribe` function to unsubscribe the callback + public void Monitor(string tag, Action, Action> callback) + { + var node = FindNode(tag); + + var sub = new Subscription + { + PublishingInterval = _options.DefaultMonitorInterval, + PublishingEnabled = true, + LifetimeCount = _options.SubscriptionLifetimeCount, + KeepAliveCount = _options.SubscriptionKeepAliveCount, + DisplayName = tag, + Priority = byte.MaxValue + }; + + var item = new MonitoredItem + { + StartNodeId = node.NodeId, + AttributeId = Attributes.Value, + DisplayName = tag, + SamplingInterval = _options.DefaultMonitorInterval + }; + sub.AddItem(item); + _session.AddSubscription(sub); + sub.Create(); + sub.ApplyChanges(); + + item.Notification += (monitoredItem, args) => + { + var p = (MonitoredItemNotification)args.NotificationValue; + var t = p.Value.WrappedValue.Value; + Action unsubscribe = () => + { + sub.RemoveItems(sub.MonitoredItems); + sub.Delete(true); + _session.RemoveSubscription(sub); + sub.Dispose(); + }; + + var monitorEvent = new ReadEvent(); + monitorEvent.Value = (T)t; + monitorEvent.SourceTimestamp = p.Value.SourceTimestamp; + monitorEvent.ServerTimestamp = p.Value.ServerTimestamp; + if (StatusCode.IsGood(p.Value.StatusCode)) monitorEvent.Quality = Quality.Good; + if (StatusCode.IsBad(p.Value.StatusCode)) monitorEvent.Quality = Quality.Bad; + callback(monitorEvent, unsubscribe); + }; + } + + /// + /// Explore a folder on the Opc Server + /// + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` finds the sub nodes of `bar` on the folder `foo` + /// The list of sub-nodes + public IEnumerable ExploreFolder(string tag) + { + IList nodes; + _folderCache.TryGetValue(tag, out nodes); + if (nodes != null) + return nodes; + + var folder = FindNode(tag); + nodes = ClientUtils.Browse(_session, folder.NodeId) + .GroupBy(n => n.NodeId) //this is to select distinct + .Select(n => n.First()) + .Where(n => n.NodeClass == NodeClass.Variable || n.NodeClass == NodeClass.Object) + .Select(n => n.ToHylaNode(folder)) + .ToList(); + + //add nodes to cache + if (!_folderCache.ContainsKey(tag)) + _folderCache.Add(tag, nodes); + foreach (var node in nodes) + AddNodeToCache(node); + + return nodes; + } + + /// + /// Explores a folder asynchronously + /// + public async Task> ExploreFolderAsync(string tag) + { + return await Task.Run(() => ExploreFolder(tag)); + } + + /// + /// Finds a node on the Opc Server + /// + /// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. + /// E.g: the tag `foo.bar` finds the tag `bar` on the folder `foo` + /// If there is a tag, it returns it, otherwise it throws an + public UaNode FindNode(string tag) + { + // if the tag already exists in cache, return it + if (_nodesCache.ContainsKey(tag)) + return _nodesCache[tag]; + + UaNode found; + if (tag.Contains("ns=")) + found = new UaNode(tag, tag); + else + // try to find the tag otherwise + found = FindNode(tag, RootNode); + + if (found != null) + { + AddNodeToCache(found); + return found; + } + + // throws an exception if not found + throw new OpcException(string.Format("The tag \"{0}\" doesn't exist on the Server", tag)); + } + + /// + /// Find node asynchronously + /// + public async Task FindNodeAsync(string tag) + { + return await Task.Run(() => FindNode(tag)); + } + + /// + /// Gets the root node of the server + /// + public UaNode RootNode { get; private set; } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + if (_session != null) + { + _session.RemoveSubscriptions(_session.Subscriptions.ToList()); + _session.Close(); + _session.Dispose(); + } + GC.SuppressFinalize(this); + } + + private void CheckReturnValue(StatusCode status) + { + if (!StatusCode.IsGood(status)) + throw new OpcException(string.Format("Invalid response from the server. (Response Status: {0})", status), status); + } + + /// + /// Adds a node to the cache using the tag as its key + /// + /// the node to add + private void AddNodeToCache(UaNode node) + { + if (!_nodesCache.ContainsKey(node.Tag)) + _nodesCache.Add(node.Tag, node); + } + + /// + /// Return identity login object for a given URI. + /// + /// Login URI + /// AnonUser or User with name and password + private UserIdentity GetIdentity(Uri url) + { + if (_options.UserIdentity != null) + { + return _options.UserIdentity; + } + var uriLogin = new UserIdentity(); + if (!string.IsNullOrEmpty(url.UserInfo)) + { + var uis = url.UserInfo.Split(':'); + uriLogin = new UserIdentity(uis[0], uis[1]); + } + return uriLogin; + } + + /// + /// Crappy method to initialize the session. I don't know what many of these things do, sincerely. + /// + private async Task InitializeSession(Uri url) + { + var certificateValidator = new CertificateValidator(); + certificateValidator.CertificateValidation += (sender, eventArgs) => + { + if (ServiceResult.IsGood(eventArgs.Error)) + eventArgs.Accept = true; + else if ((eventArgs.Error.StatusCode.Code == StatusCodes.BadCertificateUntrusted) && _options.AutoAcceptUntrustedCertificates) + eventArgs.Accept = true; + else + throw new OpcException(string.Format("Failed to validate certificate with error code {0}: {1}", eventArgs.Error.Code, eventArgs.Error.AdditionalInfo), eventArgs.Error.StatusCode); + }; + // Build the application configuration + var appInstance = new ApplicationInstance + { + ApplicationType = ApplicationType.Client, + ConfigSectionName = _options.ConfigSectionName, + ApplicationConfiguration = new ApplicationConfiguration + { + ApplicationUri = url.ToString(), + ApplicationName = _options.ApplicationName, + ApplicationType = ApplicationType.Client, + CertificateValidator = certificateValidator, + ServerConfiguration = new ServerConfiguration + { + MaxSubscriptionCount = _options.MaxSubscriptionCount, + MaxMessageQueueSize = _options.MaxMessageQueueSize, + MaxNotificationQueueSize = _options.MaxNotificationQueueSize, + MaxPublishRequestCount = _options.MaxPublishRequestCount + }, + SecurityConfiguration = new SecurityConfiguration + { + AutoAcceptUntrustedCertificates = _options.AutoAcceptUntrustedCertificates + }, + TransportQuotas = new TransportQuotas + { + OperationTimeout = 600000, + MaxStringLength = 1048576, + MaxByteStringLength = 1048576, + MaxArrayLength = 65535, + MaxMessageSize = 4194304, + MaxBufferSize = 65535, + ChannelLifetime = 600000, + SecurityTokenLifetime = 3600000 + }, + ClientConfiguration = new ClientConfiguration + { + DefaultSessionTimeout = 60000, + MinSubscriptionLifetime = 10000 + }, + DisableHiResClock = true + } + }; + + // Assign a application certificate (when specified) + if (_options.ApplicationCertificate != null) + appInstance.ApplicationConfiguration.SecurityConfiguration.ApplicationCertificate = new CertificateIdentifier(_options.ApplicationCertificate); + + // Find the endpoint to be used + var endpoints = ClientUtils.SelectEndpoint(url, _options.UseMessageSecurity); + + // Create the OPC session: + var session = await Session.Create( + configuration: appInstance.ApplicationConfiguration, + endpoint: new ConfiguredEndpoint( + collection: null, + description: endpoints, + configuration: EndpointConfiguration.Create(applicationConfiguration: appInstance.ApplicationConfiguration)), + updateBeforeConnect: false, + checkDomain: false, + sessionName: _options.SessionName, + sessionTimeout: _options.SessionTimeout, + identity: GetIdentity(url), + preferredLocales: new string[] { }); + + return session; + } + + /// + /// Finds a node starting from the specified node as the root folder + /// + /// the tag to find + /// the root node + /// + private UaNode FindNode(string tag, UaNode node) + { + // if the tag already exists in cache, return it + if (_nodesCache.ContainsKey(tag)) + return _nodesCache[tag]; + + var folders = tag.Split('.'); + var head = folders.FirstOrDefault(); + + UaNode found; + if (tag.Contains("ns=")) + { + found = new UaNode(tag, tag); + } + else + { + try + { + var subNodes = ExploreFolder(node.Tag); + found = subNodes.Single(n => n.Name.Equals(tag) || n.Name == head); + } + catch (Exception ex) + { + throw new OpcException(string.Format("The tag \"{0}\" doesn't exist on folder \"{1}\"", head, node.Tag), ex); + } + } + + // remove an array element by converting it to a list + var folderList = folders.ToList(); + folderList.RemoveAt(0); // remove the first node + folders = folderList.ToArray(); + return found.Name.Equals(tag) + ? found // last node, return it + : FindNode(string.Join(".", folders), found); // find sub nodes + } + + + private void NotifyServerConnectionLost() + { + if (ServerConnectionLost != null) + ServerConnectionLost(this, EventArgs.Empty); + } + + private void NotifyServerConnectionRestored() + { + if (ServerConnectionRestored != null) + ServerConnectionRestored(this, EventArgs.Empty); + } + + /// + /// This event is raised when the connection to the OPC server is lost. + /// + public event EventHandler ServerConnectionLost; + + /// + /// This event is raised when the connection to the OPC server is restored. + /// + public event EventHandler ServerConnectionRestored; + + } + +} diff --git a/h-opc/Ua/UaClientOptions.cs b/h-opc/Ua/UaClientOptions.cs new file mode 100644 index 0000000..5bb82a4 --- /dev/null +++ b/h-opc/Ua/UaClientOptions.cs @@ -0,0 +1,120 @@ +using System.Security.Cryptography.X509Certificates; +using OpcUa = Opc.Ua; + +namespace Hylasoft.Opc.Ua +{ + /// + /// This class defines the configuration options for the setup of the UA client session + /// + public class UaClientOptions + { + /// + /// Specifies the (optional) certificate for the application to connect to the server + /// + public X509Certificate2 ApplicationCertificate { get; set; } + + /// + /// Specifies the ApplicationName for the client application. + /// + public string ApplicationName { get; set; } + + /// + /// Should untrusted certificates be silently accepted by the client? + /// + public bool AutoAcceptUntrustedCertificates { get; set; } + + /// + /// Specifies the ConfigSectionName for the client configuration. + /// + public string ConfigSectionName { get; set; } + + /// + /// default monitor interval in Milliseconds. + /// + public int DefaultMonitorInterval { get; set; } + + /// + /// Specifies a name to be associated with the created sessions. + /// + public string SessionName { get; set; } + + /// + /// Specifies the timeout for the sessions. + /// + public uint SessionTimeout { get; set; } + + /// + /// Specify whether message exchange should be secured. + /// + public bool UseMessageSecurity { get; set; } + + /// + /// The maximum number of notifications per publish request. + /// The client’s responsibility is to send PublishRequests to the server, + /// in order to enable the server to send PublishResponses back. + /// The PublishResponses are used to deliver the notifications: but if there + /// are no PublishRequests, the server cannot send a notification to the client. + /// The server will also verify that the client is alive by checking that + /// new PublishRequests are received – LifeTimeCount defines the number of + /// PublishingIntervals to wait for a new PublishRequest, before realizing + /// that the client is no longer active.The Subscription is then removed from + /// the server. + /// + public uint SubscriptionLifetimeCount { get; set; } + + /// + /// If there is no data to send after the next PublishingInterval, + /// the server will skip it. But KeepAlive defines how many intervals may be skipped, + /// before an empty notification is sent anyway: to give the client a hint that + /// the subscription is still alive in the server and that there just has not been + /// any data arriving to the client. + /// + public uint SubscriptionKeepAliveCount { get; set; } + + /// + /// Gets or sets the max subscription count. + /// + public int MaxSubscriptionCount { get; set; } + + /// + /// The maximum number of messages saved in the queue for each subscription. + /// + public int MaxMessageQueueSize { get; set; } + + /// + /// The maximum number of notificates saved in the queue for each monitored item. + /// + public int MaxNotificationQueueSize { get; set; } + + /// + /// Gets or sets the max publish request count. + /// + public int MaxPublishRequestCount { get; set; } + + /// + /// The identity to connect to the OPC server as + /// + public OpcUa.UserIdentity UserIdentity { get; set; } + + /// + /// Creates a client options object + /// + public UaClientOptions() + { + // Initialize default values: + ApplicationName = "h-opc-client"; + AutoAcceptUntrustedCertificates = true; + ConfigSectionName = "h-opc-client"; + DefaultMonitorInterval = 100; + SessionName = "h-opc-client"; + SessionTimeout = 60000U; + UseMessageSecurity = false; + SubscriptionLifetimeCount = 0; + SubscriptionKeepAliveCount = 0; + MaxSubscriptionCount = 100; + MaxMessageQueueSize = 10; + MaxNotificationQueueSize = 100; + MaxPublishRequestCount = 20; + } + } +} diff --git a/h-opc/Ua/UaNode.cs b/h-opc/Ua/UaNode.cs new file mode 100644 index 0000000..edaa021 --- /dev/null +++ b/h-opc/Ua/UaNode.cs @@ -0,0 +1,29 @@ +using Hylasoft.Opc.Common; + +namespace Hylasoft.Opc.Ua +{ + /// + /// Represents a node to be used specifically for OPC UA + /// + public class UaNode : Node + { + /// + /// The UA Id of the node + /// + public string NodeId { get; private set; } + + /// + /// Instantiates a UaNode class + /// + /// the name of the node + /// The UA Id of the node + /// The parent node + internal UaNode(string name, string nodeId, Node parent = null) + : base(name, parent) + { + NodeId = nodeId; + } + + } + +} diff --git a/h-opc/h-opc.csproj b/h-opc/h-opc.csproj new file mode 100644 index 0000000..15f0fda --- /dev/null +++ b/h-opc/h-opc.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + h_opc + enable + disable + + + + + + + +