From be9871697f7921c25c88eae0835314662154c5f9 Mon Sep 17 00:00:00 2001 From: Marc Schier Date: Mon, 20 Feb 2017 17:36:45 +0100 Subject: [PATCH 1/3] opc-ua module v2 for .net standard loader. --- .gitignore | 239 +------- .../Microsoft.Azure.IoT.Gateway.1.0.0.nupkg | Bin 0 -> 6788 bytes ...soft.Azure.IoT.Gateway.1.0.0.symbols.nupkg | Bin 0 -> 23725 bytes Module.cs | 324 ----------- NuGet.Config | 16 + Opc.Ua.Client.Module.csproj | 112 ---- Opc.Ua.Client.Module.sln | 22 +- Opc.Ua.Client.SampleModule.Config.xml | 249 --------- README.md | 88 ++- binding/dotnet_binding_sample.vcxproj | 175 ------ binding/main.c | 53 -- bld/build.cmd | 282 ++++++++++ bld/build.sh | 220 ++++++++ bld/publish/dummy.cs | 10 + bld/publish/project.json | 26 + gateway_config.json | 35 -- global.json | 5 + packages.config | 8 - samples/gateway_config.json | 61 +++ .../35MSSharedLib1024.snk | Bin 0 -> 160 bytes src/Opc.Ua.Client.Module/Module.cs | 511 ++++++++++++++++++ .../Opc.Ua.Client.Module.xproj | 19 + .../Properties}/AssemblyInfo.cs | 7 + src/Opc.Ua.Client.Module/project.json | 61 +++ 24 files changed, 1313 insertions(+), 1210 deletions(-) create mode 100644 .nuget/Microsoft.Azure.IoT.Gateway.1.0.0.nupkg create mode 100644 .nuget/Microsoft.Azure.IoT.Gateway.1.0.0.symbols.nupkg delete mode 100644 Module.cs create mode 100644 NuGet.Config delete mode 100644 Opc.Ua.Client.Module.csproj delete mode 100644 Opc.Ua.Client.SampleModule.Config.xml delete mode 100644 binding/dotnet_binding_sample.vcxproj delete mode 100644 binding/main.c create mode 100644 bld/build.cmd create mode 100644 bld/build.sh create mode 100644 bld/publish/dummy.cs create mode 100644 bld/publish/project.json delete mode 100644 gateway_config.json create mode 100644 global.json delete mode 100644 packages.config create mode 100644 samples/gateway_config.json create mode 100644 src/Opc.Ua.Client.Module/35MSSharedLib1024.snk create mode 100644 src/Opc.Ua.Client.Module/Module.cs create mode 100644 src/Opc.Ua.Client.Module/Opc.Ua.Client.Module.xproj rename {Properties => src/Opc.Ua.Client.Module/Properties}/AssemblyInfo.cs (83%) create mode 100644 src/Opc.Ua.Client.Module/project.json diff --git a/.gitignore b/.gitignore index c62b954..4abb1c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,223 +1,16 @@ -# Compiled object files -*.o -*.opp - -# Compiled static libraries -*.a - -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.sln.docstates - -# Build results - -build/ -[Dd]ebug/ -[Rr]elease/ -x64/ -[Bb]in/ -[Oo]bj/ - -# Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets -!packages/*/build/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -*_i.c -*_p.c -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.log -*.scc - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -*.ncrunch* -.*crunch*.local.xml - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.Publish.xml - -# NuGet Packages Directory -packages/ - -# Windows Azure Build Output -csx -*.build.csdef - -# Windows Store app package directory -AppPackages/ - -# Others -sql/ -*.Cache -ClientBin/ -[Ss]tyle[Cc]op.* -~$* -*~ -*.dbmdl -*.[Pp]ublish.xml -*.publishsettings - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file to a newer -# Visual Studio version. Backup files are not needed, because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -App_Data/*.mdf -App_Data/*.ldf - - -#LightSwitch generated files -GeneratedArtifacts/ -_Pvt_Extensions/ -ModelManifest.xml - -# ========================= -# Windows detritus -# ========================= - -# Windows image file caches -Thumbs.db -ehthumbs.db - -# Folder config file -Desktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Mac desktop service store files -.DS_Store - -# Visual studio build artifacts -*.tlog -*.lastbuildstate -*.idb -*.exp -*.lib -*.dll - -# Windows CE build artifacts -Build.err -Build.wrn -Buildx86retail.dat -*.dat - -# Tools EXE that doesn't end up in a typical build directory -c/common/tools/macro_utils_h_generator/macro_utils_h_generator.exe - -# hg directories should be ignored -**/hg/ - -# Java -**/java/**/.idea/ -**/java/**/out/ -**/java/**/target/ -**/java/**/*.iml -**/java/**/*dependency-reduced-pom.xml -tools/jenkins-cli.jar -*.class -*.jar -**/target/ -*.iml - -# Node.js -**/.settings/ -**/node_modules/ -**/.idea/ - -# VS Code stuff -typings/** -.vscode/** - -# ignore cmake build folder -.cmake/** -*build/** -build_nodejs/** - -# ignore Atom Editor files -.atom-build.json -.remote-sync.json - -# ignore VS Code C++ files -browse.VC.db - -# api reference docstates -api_reference/ - -/Opc.Ua.Client.Module.VC.VC.opendb -/Opc.Ua.Client.Module.VC.db -/binding/OPC Foundation/CertificateStores -/.vs/config -/OPC Foundation/CertificateStores/MachineDefault +/**/*.user +/**/*.user.json +/**/project.lock.json +/**/obj/** +/**/.vs +/**/bin/** +/**/csx/** +/**/ecf/** +/**/*.log +/**/*.err +/**/*.sdf +/**/*.opendb +/**/*.opensdf +/**/*.vc.db +/.vscode/** +/build/** diff --git a/.nuget/Microsoft.Azure.IoT.Gateway.1.0.0.nupkg b/.nuget/Microsoft.Azure.IoT.Gateway.1.0.0.nupkg new file mode 100644 index 0000000000000000000000000000000000000000..b203fd39c2ecab58ddd3dd09269b816fa256917c GIT binary patch literal 6788 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 literal 0 HcmV?d00001 diff --git a/Module.cs b/Module.cs deleted file mode 100644 index cd24ff6..0000000 --- a/Module.cs +++ /dev/null @@ -1,324 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using System.Runtime.Serialization; -using System.Text; -using Microsoft.Azure.IoT.Gateway; -using Opc.Ua.Configuration; -using Newtonsoft.Json; - -namespace Opc.Ua.Client -{ - [DataContract(Name = "NodeLookup", Namespace = Namespaces.OpcUaXsd)] - public partial class NodeLookup - { - public NodeLookup() - { - } - - [DataMember(Name = "EndpointUrl", IsRequired = true, Order = 0)] - public Uri EndPointURL; - - [DataMember(Name = "NodeId", IsRequired = true, Order = 1)] - public NodeId NodeID; - } - - [CollectionDataContract(Name = "ListOfPublishedNodes", Namespace = Namespaces.OpcUaConfig, ItemName = "NodeLookup")] - public partial class PublishedNodesCollection : List - { - public PublishedNodesCollection() - { - } - - public static PublishedNodesCollection Load(ApplicationConfiguration configuration) - { - return configuration.ParseExtension(); - } - } - - public class SampleModule : IGatewayModule, IGatewayModuleStart - { - private Broker m_broker; - - private ApplicationConfiguration m_configuration = null; - private List m_sessions = new List(); - - private string m_DeviceID = string.Empty; - private string m_SharedAccessKey = string.Empty; - - public async void Create(Broker broker, byte[] configuration) - { - m_broker = broker; - - // TODO: Security: The shared access key should be stored in secure storage, e.g. a TPM - // and the device ID can be used as a lookup - string configurationString = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(configuration)); - m_DeviceID = configurationString.Substring(0, configurationString.IndexOf(';')); - m_SharedAccessKey = configurationString.Substring(configurationString.IndexOf(';') + 1); - - // load the application configuration. - ApplicationInstance application = new ApplicationInstance(); - application.ConfigSectionName = "Opc.Ua.Client.SampleModule"; - m_configuration = await application.LoadApplicationConfiguration(false); - - // check the application certificate. - await application.CheckApplicationInstanceCertificate(false, 0); - - m_configuration.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_CertificateValidation); - - // get a list of persisted endpoint URLs and create a list without duplicates. - List endpointUrls = new List(); - PublishedNodesCollection nodesLookups = PublishedNodesCollection.Load(m_configuration); - foreach (NodeLookup nodeLookup in nodesLookups) - { - if (!endpointUrls.Contains(nodeLookup.EndPointURL)) - { - endpointUrls.Add(nodeLookup.EndPointURL); - } - } - - // now create a session for each unique endpoint - foreach (Uri endpointUrl in endpointUrls) - { - try - { - Console.WriteLine("Opc.Ua.Client.SampleModule: Creating session for endpoint: " + endpointUrl.ToString()); - await EndpointConnect(endpointUrl); - } - catch (Exception ex) - { - string innerException = ex.InnerException != null ? "\r\n. Inner Exception: " + ex.InnerException.ToString() : String.Empty; - Console.WriteLine("Opc.Ua.Client.SampleModule: Could not connect to updated endpoint " + endpointUrl.ToString() + ". Exception: " + ex.ToString() + innerException); - } - } - - Console.WriteLine("Opc.Ua.Client.SampleModule: OPC UA Client Sample Module created."); - } - - private async Task EndpointConnect(Uri endpointUrl) - { - EndpointDescription selectedEndpoint = SelectUaTcpEndpoint(DiscoverEndpoints(m_configuration, endpointUrl, 60)); - ConfiguredEndpoint configuredEndpoint = new ConfiguredEndpoint(selectedEndpoint.Server, EndpointConfiguration.Create(m_configuration)); - configuredEndpoint.Update(selectedEndpoint); - - Session newSession = await Session.Create( - m_configuration, - configuredEndpoint, - true, - false, - m_configuration.ApplicationName, - 60000, - new UserIdentity(new AnonymousIdentityToken()), - null); - - if (newSession != null) - { - Console.WriteLine("Opc.Ua.Client.SampleModule: Created session with updated endpoint " + configuredEndpoint.EndpointUrl + " from server!"); - newSession.KeepAlive += new KeepAliveEventHandler(StandardClient_KeepAlive); - m_sessions.Add(newSession); - } - } - - public void Start() - { - // publish preconfigured nodes - PublishedNodesCollection nodesLookups = PublishedNodesCollection.Load(m_configuration); - foreach (NodeLookup nodeLookup in nodesLookups) - { - CreateMonitoredItem(nodeLookup); - } - - Console.WriteLine("Opc.Ua.Client.SampleModule: OPC UA Client Sample Module started."); - } - - public void Destroy() - { - foreach (Session session in m_sessions) - { - // Disconnect and dispose - session.Dispose(); - } - - m_sessions.Clear(); - - Console.WriteLine("Opc.Ua.Client.SampleModule: OPC UA Client Sample Module destroyed."); - } - - public void Receive(Message received_message) - { - // Nothing to do, we only send! - } - - public void CreateMonitoredItem(NodeLookup nodeLookup) - { - // find the right session using our lookup - Session matchingSession = null; - foreach(Session session in m_sessions) - { - if (session.Endpoint.EndpointUrl.ToLowerInvariant().TrimEnd('/') == Utils.ReplaceLocalhost(nodeLookup.EndPointURL.ToString()).ToLowerInvariant().TrimEnd('/')) - { - matchingSession = session; - break; - } - } - - if (matchingSession != null) - { - Subscription subscription = matchingSession.DefaultSubscription; - if (matchingSession.AddSubscription(subscription)) - { - subscription.Create(); - } - - // add the new monitored item. - MonitoredItem monitoredItem = new MonitoredItem(subscription.DefaultItem); - - monitoredItem.StartNodeId = nodeLookup.NodeID; - monitoredItem.AttributeId = Attributes.Value; - monitoredItem.DisplayName = nodeLookup.NodeID.Identifier.ToString(); - monitoredItem.MonitoringMode = MonitoringMode.Reporting; - monitoredItem.SamplingInterval = 0; - monitoredItem.QueueSize = 0; - monitoredItem.DiscardOldest = true; - - monitoredItem.Notification += new MonitoredItemNotificationEventHandler(MonitoredItem_Notification); - subscription.AddItem(monitoredItem); - subscription.ApplyChanges(); - } - else - { - Console.WriteLine("Opc.Ua.Client.SampleModule: ERROR: Could not find endpoint URL " + nodeLookup.EndPointURL.ToString() + " in active server sessions, NodeID " + nodeLookup.NodeID.Identifier.ToString() + " NOT published!"); - Console.WriteLine("Opc.Ua.Client.SampleModule: To fix this, please update your config.xml with the updated enpoint URL!"); - } - } - - private void MonitoredItem_Notification(MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs e) - { - try - { - if (e.NotificationValue == null || monitoredItem.Subscription.Session == null) - { - return; - } - - JsonEncoder encoder = new JsonEncoder(monitoredItem.Subscription.Session.MessageContext, false); - string hostname = monitoredItem.Subscription.Session.ConfiguredEndpoint.EndpointUrl.DnsSafeHost; - if (hostname == "localhost") - { - hostname = Utils.GetHostName(); - } - encoder.WriteString("HostName", hostname); - encoder.WriteNodeId("MonitoredItem", monitoredItem.ResolvedNodeId); - e.NotificationValue.Encode(encoder); - - string json = encoder.Close(); - - var properties = new Dictionary(); - properties.Add("source", "mapping"); - properties.Add("content-type", "application/opcua+uajson"); - properties.Add("deviceName", m_DeviceID); - properties.Add("deviceKey", m_SharedAccessKey); - - try - { - m_broker.Publish(new Message(json, properties)); - } - catch (Exception ex) - { - Utils.Trace(ex, "Opc.Ua.Client.SampleModule: Failed to publish message, dropping...."); - } - - } - catch (Exception exception) - { - Utils.Trace(exception, "Opc.Ua.Client.SampleModule: Error processing monitored item notification."); - } - } - - private void CertificateValidator_CertificateValidation(CertificateValidator validator, CertificateValidationEventArgs e) - { - if (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted) - { - e.Accept = true; - Console.WriteLine("Opc.Ua.Client.SampleModule: WARNING: Auto-accepting certificate: {0}", e.Certificate.Subject); - } - } - - private void StandardClient_KeepAlive(Session sender, KeepAliveEventArgs e) - { - if (e != null && sender != null) - { - if (!ServiceResult.IsGood(e.Status)) - { - Console.WriteLine(String.Format( - "Opc.Ua.Client.SampleModule: Server {0} Status NOT good: {1} {2}/{3}", - sender.ConfiguredEndpoint.EndpointUrl, - e.Status, - sender.OutstandingRequestCount, - sender.DefunctRequestCount)); - } - } - } - - private EndpointDescriptionCollection DiscoverEndpoints(ApplicationConfiguration config, Uri discoveryUrl, int timeout) - { - EndpointConfiguration configuration = EndpointConfiguration.Create(config); - configuration.OperationTimeout = timeout; - - using (DiscoveryClient client = DiscoveryClient.Create( - discoveryUrl, - EndpointConfiguration.Create(config))) - { - try - { - EndpointDescriptionCollection endpoints = client.GetEndpoints(null); - ReplaceLocalHostWithRemoteHost(endpoints, discoveryUrl); - return endpoints; - } - catch (Exception e) - { - Console.WriteLine("Opc.Ua.Client.SampleModule: Could not fetch endpoints from url: {0}", discoveryUrl); - Console.WriteLine("Opc.Ua.Client.SampleModule: Reason = {0}", e.Message); - throw e; - } - } - } - - private void ReplaceLocalHostWithRemoteHost(EndpointDescriptionCollection endpoints, Uri discoveryUrl) - { - foreach (EndpointDescription endpoint in endpoints) - { - endpoint.EndpointUrl = Utils.ReplaceLocalhost(endpoint.EndpointUrl, discoveryUrl.DnsSafeHost); - StringCollection updatedDiscoveryUrls = new StringCollection(); - - foreach (string url in endpoint.Server.DiscoveryUrls) - { - updatedDiscoveryUrls.Add(Utils.ReplaceLocalhost(url, discoveryUrl.DnsSafeHost)); - } - - endpoint.Server.DiscoveryUrls = updatedDiscoveryUrls; - } - } - - private EndpointDescription SelectUaTcpEndpoint(EndpointDescriptionCollection endpointCollection) - { - EndpointDescription bestEndpoint = null; - foreach (EndpointDescription endpoint in endpointCollection) - { - if (endpoint.TransportProfileUri == Profiles.UaTcpTransport) - { - if ((bestEndpoint == null) || - (endpoint.SecurityLevel > bestEndpoint.SecurityLevel)) - { - bestEndpoint = endpoint; - } - } - } - - return bestEndpoint; - } - } -} diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..261b18e --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Opc.Ua.Client.Module.csproj b/Opc.Ua.Client.Module.csproj deleted file mode 100644 index 96f340f..0000000 --- a/Opc.Ua.Client.Module.csproj +++ /dev/null @@ -1,112 +0,0 @@ - - - - - Debug - AnyCPU - {25D19FFD-D553-4270-8A7A-6F510572BDC0} - Library - Properties - Opc.Ua.Client - Opc.Ua.Client.SampleModule - v4.6 - 512 - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - true - bin\x86\Debug\ - DEBUG;TRACE - full - x86 - prompt - MinimumRecommendedRules.ruleset - - - bin\x86\Release\ - TRACE - true - pdbonly - x86 - prompt - MinimumRecommendedRules.ruleset - - - - packages\Portable.BouncyCastle.1.8.1.2\lib\net4\BouncyCastle.Crypto.dll - True - - - packages\Azure.IoT.Gateway.SDK.Net.2017.1.13.2\lib\net40\Microsoft.Azure.IoT.Gateway.dll - True - - - packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - True - - - False - packages\OPCFoundation.NetStandard.Opc.Ua.SDK.0.1.1\lib\net46\Opc.Ua.Client.dll - - - False - packages\OPCFoundation.NetStandard.Opc.Ua.SDK.0.1.1\lib\net46\Opc.Ua.Configuration.dll - - - False - packages\OPCFoundation.NetStandard.Opc.Ua.Core.0.1.1\lib\net46\Opc.Ua.Core.dll - - - - - - - - - - - - - - - - - - Always - - - Designer - - - - - - Always - - - - - \ No newline at end of file diff --git a/Opc.Ua.Client.Module.sln b/Opc.Ua.Client.Module.sln index 0ad06a0..45ac495 100644 --- a/Opc.Ua.Client.Module.sln +++ b/Opc.Ua.Client.Module.sln @@ -3,27 +3,21 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Opc.Ua.Client.Module", "Opc.Ua.Client.Module.csproj", "{25D19FFD-D553-4270-8A7A-6F510572BDC0}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "dotnet_binding_sample", "binding\dotnet_binding_sample.vcxproj", "{7064A025-384F-3689-89FD-9E480DBFE5C9}" - ProjectSection(ProjectDependencies) = postProject - {25D19FFD-D553-4270-8A7A-6F510572BDC0} = {25D19FFD-D553-4270-8A7A-6F510572BDC0} - EndProjectSection +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Opc.Ua.Client.Module", "src\Opc.Ua.Client.Module\Opc.Ua.Client.Module.xproj", "{43311AFB-D7C4-4E5A-B1DE-855407F90D1B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x86 = Debug|x86 Release|x86 = Release|x86 + Signed|x86 = Signed|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {25D19FFD-D553-4270-8A7A-6F510572BDC0}.Debug|x86.ActiveCfg = Debug|x86 - {25D19FFD-D553-4270-8A7A-6F510572BDC0}.Debug|x86.Build.0 = Debug|x86 - {25D19FFD-D553-4270-8A7A-6F510572BDC0}.Release|x86.ActiveCfg = Release|x86 - {25D19FFD-D553-4270-8A7A-6F510572BDC0}.Release|x86.Build.0 = Release|x86 - {7064A025-384F-3689-89FD-9E480DBFE5C9}.Debug|x86.ActiveCfg = Debug|Win32 - {7064A025-384F-3689-89FD-9E480DBFE5C9}.Debug|x86.Build.0 = Debug|Win32 - {7064A025-384F-3689-89FD-9E480DBFE5C9}.Release|x86.ActiveCfg = Release|Win32 - {7064A025-384F-3689-89FD-9E480DBFE5C9}.Release|x86.Build.0 = Release|Win32 + {43311AFB-D7C4-4E5A-B1DE-855407F90D1B}.Debug|x86.ActiveCfg = Debug|Any CPU + {43311AFB-D7C4-4E5A-B1DE-855407F90D1B}.Debug|x86.Build.0 = Debug|Any CPU + {43311AFB-D7C4-4E5A-B1DE-855407F90D1B}.Release|x86.ActiveCfg = Release|Any CPU + {43311AFB-D7C4-4E5A-B1DE-855407F90D1B}.Release|x86.Build.0 = Release|Any CPU + {43311AFB-D7C4-4E5A-B1DE-855407F90D1B}.Signed|x86.ActiveCfg = Signed|Any CPU + {43311AFB-D7C4-4E5A-B1DE-855407F90D1B}.Signed|x86.Build.0 = Signed|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Opc.Ua.Client.SampleModule.Config.xml b/Opc.Ua.Client.SampleModule.Config.xml deleted file mode 100644 index 2e8f36d..0000000 --- a/Opc.Ua.Client.SampleModule.Config.xml +++ /dev/null @@ -1,249 +0,0 @@ - - - - Opc.Ua.Client.SampleModule - - - urn:localhost:OPCFoundation:SampleModule - - - http://opcfoundation.org/UA/SampleModule/ - - - Client_1 - - - - - - - - - Directory - - - OPC Foundation/CertificateStores/MachineDefault - - - - Opc.Ua.Client.SampleModule - - - - - - - - - Directory - OPC Foundation/CertificateStores/UA Certificate Authorities - - - - - Directory - OPC Foundation/CertificateStores/UA Applications - - - - 32 - - - - Directory - OPC Foundation/CertificateStores/RejectedCertificates - - - - true - - - - - - - - - - 120000 - - - 1048576 - - - 4194304 - - - 65535 - - - 4194304 - - - 65535 - - - 300000 - - - 3600000 - - - - - - - - - - 600000 - - - - opc.tcp://{0}:4840/UADiscovery - http://{0}:52601/UADiscovery - http://{0}/UADiscovery/Default.svc - - - - - - - Opc.Ua.Client.SampleModule.Endpoints.xml - - - 10000 - - - - - - - - opc.tcp://TODO:Add uri here - - - i=2258 - - - - opc.tcp://TODO:Add another uri here - - - i=2258 - - - - - - - - - - Logs/Opc.Ua.Client.SampleModule.log.txt - true - - - - - - 515 - - - - - - - - - - - true - - \ No newline at end of file diff --git a/README.md b/README.md index 0a17181..30e484e 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,84 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments # OPC UA Client Module for the Azure IoT Gateway SDK -This reference implementation demonstrates how the Azure IoT Gateway SDK can be used to connect to existing OPC UA servers and send JSON encoded telemetry data from these servers in OPC UA "Pub/Sub" format (using a JSON payload) to Azure IoT Hub. All transport protocols supported by the Gateway SDK can be used, i.e. HTTPS, AMQP and MQTT. The transport is selected in the transport setting in gateway_config.JSON. +This reference implementation demonstrates how the Azure IoT Gateway SDK can be used to connect to existing OPC UA servers and send JSON encoded telemetry data from these servers in OPC UA "Pub/Sub" format (using a JSON payload) to Azure IoT Hub. All transport protocols supported by the Gateway SDK can be used, i.e. HTTPS, AMQP and MQTT. The transport is selected in the transport setting in gateway_config.json. This module uses the OPC Foundations's OPC UA reference stack and therefore licensing restrictions apply. Visit http://opcfoundation.github.io/UA-.NETStandardLibrary/ for OPC UA documentation and licensing terms. -## Operating System Compatibility -Since this reference implementation is written for .NET, Windows 7, 8, 8.1 & 10 are supported right now. Once the Gateway SDK supports .NET Standard, so will this module and then Linux will also be supported. +# Azure IoT Gateway SDK compatibility +The current version of the Proxy module is targeted at the Azure IoT Gateway SDK at commit 09bbcb7feaf5acc3913abd722b96e993238edd0c. -## Directory Structure +Use the following command line to clone the compatible version Azure IoT Gateway SDK, then follow the build instructions included: -### /binding -This folder contains the native entry point required for the Gateway SDK (main.c). +``` +git clone --recursive https://github.com/Azure/azure-iot-gateway-sdk.git +git checkout 09bbcb7feaf5acc3913abd722b96e993238edd0c +``` +# Directory Structure -### Root Directory -This folder contains the C# OPC UA module source file (Module.cs), the gateway configuration file (gateway_config.json) and the OPC UA gateway module configuration file (Opc.Ua.Client.SampleModule.Config.xml). +## /samples +This folder contains a sample configuration that instructs a vanilla gateway host load the module and IoT Hub proxy module and configures the module to create a +subscription on a standard server which publishes the current server time to Azure IoT Hub. -## Configuring the Module -The OPC UA server endpoints the module should connect to and the list of OPC UA nodes for each endpoint that should be published to Azure IoT Hub can be configured in the **ListOfPublishedNodes** section of the **Opc.Ua.Client.SampleModule.Config.xml** configuration file. The **Current Server Time** node (node ID 2258) is specified as an example. +## /src +This folder contains the C# OPC UA module source file (Module.cs). -Also, in the gateway_config.json, configure the name of IoT Hub you want to send the telemetry to (JSON field "IoTHubName") and the IoT Hub device ID and shared access key to use (JSON field "args"). +## /bld +This folder contains build scripts for Windows and Linux. -## Building and Running the Module -Simply open the solution file in Visual Studio and build it. Make sure you specify the gateway_config.json as a command line parameter, set the debugger type to Mixed and set the working directory to $(OutDir) before debugging. +# Building the Module + +Run ```bld/build``` to build the module both Debug and Release. The published module can be found in ```build/release``` folder. Run the build script with the ```--help``` command line argument to see all build options. + +# Configuring the Module +OPC UA nodes whose values should be published to Azure IoT Hub can be configured in the module JSON configuration. A sample template configuration file can be found in ```samples/gateway_config.json```. The configuration consists of a OPC-UA Application Configuration and Subscriptions section. + +## Application Configuration section +The ```Configuration``` Section must contain at a minimum all items shown in the sample template. The JSON type conforms to the OPC UA reference stack serialization of the ```ApplicationConfiguration``` type. + +E.g. to enable automatic certificate accept (discouraged), set ```"AutoAcceptUntrustedCertificates"``` to true (default is false) inside the ```SecurityConfiguration``` section: + +``` JSON + "args": { + "Configuration": { + "ApplicationName": "Opc.Ua.Client.SampleModule", + "ApplicationType": "Client", + "ApplicationUri": "urn:localhost:OPCFoundation:SampleModule", + "SecurityConfiguration": { + "ApplicationCertificate": {}, + "AutoAcceptUntrustedCertificates": true + }, +``` + +## Subscriptions section +The ```Subscriptions``` Section contains an array of OPC-UA sessions that the module should establish at startup, with each specifying a list of items to monitor. The ```MonitoredItems``` array type in the JSON configuration conforms to the OPC UA reference stack serialization specification of the same type. + +E.g. the sample template shows how to monitor the **Current Server Time** node (node ID 2258): + +``` JSON + }, + "Subscriptions": [ + { + "Id": "", + "SharedAccessKey": "", + "ServerUrl": "opc.tcp://:51210/UA/SampleServer", + "PublishingInterval": 400, + "MonitoredItems": [ + { + "StartNodeId": "i=2258", + "NodeClass": 2, + "DisplayName": "ServerStatusCurrentTime", + "DiscardOldest": false + } + ] + } + ] + }, +``` + +# Running the module + +To run the module and have it publish to IoT Hub, configure the name of your Hub (JSON field ```"IoTHubName"```) and the IoT Hub device ID and shared access key to use (JSON fields ```"Id"``` and ```"SharedAccessKey"```) in your version of ```gateway_config.json```. Note that if you use a "Mapping" module in your configuration, you can omit the ```"SharedAccessKey"``` field. Finally, ensure that the right native module is configured, based on your platform (i.e. iothub.dll for Windows, libiothub.so for Linux, etc.). + +To build a sample gateway to host the OPC-UA module - along with the module itself - clone the gateway SDK repo to your device and run the build script with the ```-i ```. The resulting release folder will contain not just the module and managed dependencies, but also a native IoT Hub proxy module as well as a ```sample_gateway``` executable that you can pass the updated JSON configuration to. -## Updating NuGet Packages -There are manual steps required after updating the OPC UA or Gateway SDK NuGet packages: -1. When updating the OPC UA NuGet packages, you need to re-add the new reference to the updated OPC UA DLLs in the Opc.Ua.Client.Module project as Visual Studio doesn't do that automatically for .Net Standard assemblies in .Net Framework projects. -2. When updating the Gateway SDK NuGet package, you need to update the path in the post-build copy command for the Gateway SDK DLLs in the dotnet_binding_sample project. diff --git a/binding/dotnet_binding_sample.vcxproj b/binding/dotnet_binding_sample.vcxproj deleted file mode 100644 index 40437d0..0000000 --- a/binding/dotnet_binding_sample.vcxproj +++ /dev/null @@ -1,175 +0,0 @@ - - - - - Debug - Win32 - - - Release - Win32 - - - - {7064A025-384F-3689-89FD-9E480DBFE5C9} - 8.1 - Win32Proj - Win32 - dotnet_binding_sample - - - - Application - false - MultiByte - v140 - - - Application - false - MultiByte - v140 - - - - - - - - - - <_ProjectFileVersion>10.0.20506.1 - ..\bin\x86\Debug\ - dotnet_binding_sample.dir\Debug\ - dotnet_binding_sample - .exe - true - true - ..\bin\x86\Release\ - dotnet_binding_sample.dir\Release\ - dotnet_binding_sample - .exe - false - true - - - - - - - - - - - - /guard:cf %(AdditionalOptions) - %(AdditionalIncludeDirectories) - Debug/ - EnableFastChecks - CompileAsC - ProgramDatabase - - - Disabled - Disabled - NotUsing - MultiThreadedDebugDLL - Level3 - WIN32;_WINDOWS;_DEBUG;ARCHITECTURE_x86=1;_CRT_SECURE_NO_WARNINGS;CMAKE_INTDIR="Debug";%(PreprocessorDefinitions) - $(IntDir) - - - WIN32;_WINDOWS;_DEBUG;ARCHITECTURE_x86=1;_CRT_SECURE_NO_WARNINGS;CMAKE_INTDIR=\"Debug\";%(PreprocessorDefinitions) - %(AdditionalIncludeDirectories) - - - %(AdditionalIncludeDirectories) - $(ProjectDir)/$(IntDir) - %(Filename).h - %(Filename).tlb - %(Filename)_i.c - %(Filename)_p.c - - - - - copy $(ProjectDir)\..\packages\Azure.IoT.Gateway.SDK.Net.2017.1.13.2\build\x86\*.dll $(OutDir) - - - /machine:X86 /guard:cf %(AdditionalOptions) - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;crypt32.lib;winhttp.lib;secur32.lib;ws2_32.lib;mswsock.lib;rpcrt4.lib; - %(AdditionalLibraryDirectories) - Debug - %(IgnoreSpecificDefaultLibraries) - Console - - - - - false - - - - - - - - - /guard:cf %(AdditionalOptions) - %(AdditionalIncludeDirectories) - Release/ - CompileAsC - - - AnySuitable - MaxSpeed - NotUsing - MultiThreadedDLL - Level3 - WIN32;_WINDOWS;NDEBUG;ARCHITECTURE_x86=1;_CRT_SECURE_NO_WARNINGS;CMAKE_INTDIR="Release";%(PreprocessorDefinitions) - $(IntDir) - - - - - WIN32;_WINDOWS;NDEBUG;ARCHITECTURE_x86=1;_CRT_SECURE_NO_WARNINGS;CMAKE_INTDIR=\"Release\";%(PreprocessorDefinitions) - %(AdditionalIncludeDirectories) - - - %(AdditionalIncludeDirectories) - $(ProjectDir)/$(IntDir) - %(Filename).h - %(Filename).tlb - %(Filename)_i.c - %(Filename)_p.c - - - - - copy $(ProjectDir)\..\packages\Azure.IoT.Gateway.SDK.Net.2017.1.13.2\build\x86\*.dll $(OutDir) - - - /machine:X86 /guard:cf %(AdditionalOptions) - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;crypt32.lib;winhttp.lib;secur32.lib;ws2_32.lib;mswsock.lib;rpcrt4.lib - %(AdditionalLibraryDirectories) - No - %(IgnoreSpecificDefaultLibraries) - Console - - - - - false - - - - - - - - - - - - - \ No newline at end of file diff --git a/binding/main.c b/binding/main.c deleted file mode 100644 index 8f21a09..0000000 --- a/binding/main.c +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -#include -#include - -typedef void* (__cdecl *P_GATEWAY_CREATE_FROM_JSON)(char*); -typedef void (__cdecl *P_GATEWAY_DESTROY)(void*); - -int main(int argc, char** argv) -{ - if (argc != 2) - { - printf("usage: dotnet_binding_sample configFile\n"); - printf("where configFile is the name of the file that contains the Gateway configuration\n"); - return 1; - } - - HINSTANCE hDLL = LoadLibraryA("gateway.dll"); - if (!hDLL) - { - printf("failed to load gateway.dll, error: %d\n", GetLastError()); - return 1; - } - - P_GATEWAY_CREATE_FROM_JSON Gateway_CreateFromJson = (P_GATEWAY_CREATE_FROM_JSON) GetProcAddress(hDLL, "Gateway_CreateFromJson"); - if (!Gateway_CreateFromJson) - { - printf("failed to load function Gateway_CreateFromJson, error: %d\n", GetLastError()); - return 1; - } - - void* pGateway = Gateway_CreateFromJson(argv[1]); - if (!pGateway) - { - printf("failed to create the gateway from JSON\n"); - return 1; - } - - printf("gateway successfully created from JSON\n"); - printf("gateway will run until ENTER is pressed\n"); - getchar(); - - P_GATEWAY_DESTROY Gateway_Destroy = (P_GATEWAY_DESTROY) GetProcAddress(hDLL, "Gateway_Destroy"); - if (Gateway_Destroy) - { - Gateway_Destroy(pGateway); - } - - FreeLibrary(hDLL); - - return 0; -} diff --git a/bld/build.cmd b/bld/build.cmd new file mode 100644 index 0000000..c6176f3 --- /dev/null +++ b/bld/build.cmd @@ -0,0 +1,282 @@ +@REM Copyright (c) Microsoft. All rights reserved. +@REM Licensed under the MIT license. See LICENSE file in the project root for full license information. + +@setlocal EnableExtensions EnableDelayedExpansion +@echo off + +set current-path=%~dp0 +rem // remove trailing slash +set current-path=%current-path:~0,-1% + +set repo-root=%current-path%\.. +rem // resolve to fully qualified path +for %%i in ("%repo-root%") do set repo-root=%%~fi + +set build-sdk-root=%repo-root%\..\azure-iot-gateway-sdk +for %%i in ("%build-sdk-root%") do set build-sdk-root=%%~fi + +rem ---------------------------------------------------------------------------- +rem -- parse script arguments +rem ---------------------------------------------------------------------------- + +rem // default build options +set build-clean= +set build-configs= +set build-platform=Win32 +if "%PROCESSOR_ARCHITECTURE%" == "AMD64" set build-platform=x64 +set build-root=%repo-root%\build +set build-rel-root=%build-root%\release + +:args-loop +if "%1" equ "" goto :args-done +if "%1" equ "--config" goto :arg-build-config +if "%1" equ "-C" goto :arg-build-config +if "%1" equ "--clean" goto :arg-build-clean +if "%1" equ "-c" goto :arg-build-clean +if "%1" equ "--output" goto :arg-build-rel-root +if "%1" equ "-o" goto :arg-build-rel-root +if "%1" equ "--sdk-root" goto :arg-sdk-root-folder +if "%1" equ "-i" goto :arg-sdk-root-folder +if "%1" equ "--platform" goto :arg-build-platform +if "%1" equ "-p" goto :arg-build-platform +call :usage && exit /b 1 + +:arg-build-clean +set build-clean=1 +goto :args-continue + +:arg-build-config +shift +if "%1" equ "" call :usage && exit /b 1 +set build-configs=%build-configs%%1 +goto :args-continue + +:arg-sdk-root-folder +shift +if "%1" equ "" call :usage && exit /b 1 +set build-sdk-root=%1 +goto :args-continue + +:arg-build-platform +shift +if "%1" equ "" call :usage && exit /b 1 +set build-platform=%1 +goto :args-continue + +:arg-build-rel-root +shift +if "%1" equ "" call :usage && exit /b 1 +set build-rel-root=%1 + +goto :args-continue +:args-continue +shift +goto :args-loop + +:args-done +call dotnet --version +if not !ERRORLEVEL! == 0 echo No dotnet installed, install first... && exit /b 1 +if not exist "%build-sdk-root%\tools\build.cmd" echo no sdk installed at %build-sdk-root%, only building module... && set build-sdk-root= +if "%build-configs%" == "" set build-configs=Release Debug + +rem // Start script +echo Building %build-configs%... +if not "%build-clean%" == "" ( + echo Cleaning previous build output... + call :rmdir-force %build-root% +) +if not exist %build-root% mkdir %build-root% +call :sdk-build +if not !ERRORLEVEL! == 0 echo Failures during sdk build... && exit /b !ERRORLEVEL! +call :module-build +if not !ERRORLEVEL! == 0 echo Failures during dotnet build... && exit /b !ERRORLEVEL! +call :release-all +if not !ERRORLEVEL! == 0 echo Failures building release... && exit /b !ERRORLEVEL! +goto :build-done + +rem ----------------------------------------------------------------------------- +rem -- build the sdk +rem ----------------------------------------------------------------------------- +:sdk-build +if "%build-sdk-root%" == "" goto :eof +rem // Build the sdk for all configurations +for %%c in (%build-configs%) do ( + pushd "%build-sdk-root%\tools + call :sdk-build-and-test %%c + popd + if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! +) +goto :eof +rem // Build the sdk for 1 configuration +:sdk-build-and-test +if /I not "%~1" == "Release" if /I not "%~1" == "Debug" if /I not "%~1" == "MinSizeRel" if /I not "%~1" == "RelWithDebInfo" goto :eof +rem // If incremental, check if we had a successful build before... +if exist %build-root%\sdk\%build-platform%-%~1.done goto :eof +rem // Clean bindings project output +pushd %build-sdk-root%\bindings +for /f %%i in ('dir /b /s project.json') do call :dotnet-project-clean "%%i" +popd +rem // First build the dotnetcore binding for configuration. TODO: Remove once build script is fixed. +call build_dotnet_core.cmd --config %~1 +rem // Force clean cmake output and install-deps to avoid errors. +call :rmdir-force %build-sdk-root%\build +call :rmdir-force %build-sdk-root%\install-deps +rem // Build sdk +echo Building SDK (%~1) ... +call build.cmd --config %~1 --platform %build-platform% --enable-dotnet-core-binding --disable-ble-module +echo Finished building SDK (%~1) +if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! +rem // Copy build output over and mark it as successfully built. +if not exist "%build-root%\sdk\%build-platform%" mkdir "%build-root%\sdk\%build-platform%" +xcopy /e /i /y /q "%build-sdk-root%\build" "%build-root%\sdk\%build-platform%" +if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! +echo %~1 >> %build-root%\sdk\%build-platform%-%~1.done +goto :eof + +rem ----------------------------------------------------------------------------- +rem -- build module +rem ----------------------------------------------------------------------------- +:module-build + +rem // Restore packages +:dotnet-restore +pushd %repo-root% +call dotnet restore +popd +if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! +rem // Build and publish all specified configurations +for %%c in (%build-configs%) do ( + call :dotnet-build-and-publish %%c + if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! +) +goto :eof +rem // Build module and publish for 1 configuration +:dotnet-build-and-publish +if /I not "%~1" == "Release" if /I not "%~1" == "Debug" if /I not "%~1" == "Signed" goto :eof +pushd %repo-root% +call :dotnet-build %~1 +popd +if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! +pushd %repo-root%\bld\publish +call :dotnet-publish %~1 +popd +if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! +goto :eof +rem // Build module +:dotnet-build +for /f %%i in ('dir /b /s project.json') do ( + call :dotnet-project-build %~1 "%%i" + if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! +) +goto :eof +rem // Publish all assemblies using publish dummy exe +:dotnet-publish +if not exist "%build-root%\module\%~1" mkdir "%build-root%\module\%~1" +if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! +echo. +call dotnet publish --no-build -c %~1 -o "%build-root%\module\%~1" +if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! +goto :eof +rem // Build a project +:dotnet-project-build +pushd "%~dp2" +echo. +if not "%build-clean%" == "" call :rmdir-force "bin\%~1" +call dotnet build -c %~1 +popd +if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! +goto :eof +rem // Clean a project +:dotnet-project-clean +pushd "%~dp1" +call :rmdir-force bin +popd +goto :eof + +rem ----------------------------------------------------------------------------- +rem -- Copy everything into a final release folder +rem ----------------------------------------------------------------------------- +:release-all +for %%c in (%build-configs%) do ( + call :release-binaries %%c + if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! +) +goto :eof +:release-binaries +rem // Clean release folder +if not exist "%build-rel-root%\%~1" mkdir "%build-rel-root%\%~1" +del /f /q "%build-rel-root%\%~1\*" + +rem // Flatten runtimes for windows (TODO: Should be done by loader) +xcopy /y /i /q "%build-root%\module\%~1" "%build-rel-root%\%~1" +pushd "%build-root%\module\%~1\runtimes\win" +for /f %%i in ('dir /b /s *.dll') do copy /y "%%i" "%build-rel-root%\%~1" +popd +pushd "%build-root%\module\%~1\runtimes\win7" +for /f %%i in ('dir /b /s *.dll') do copy /y "%%i" "%build-rel-root%\%~1" +popd +rem // Copy configuration json +copy /y "%repo-root%\samples\*.json" "%build-rel-root%\%~1" +if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! +rem // Delete unnecessary publish.dll and .pdb +pushd "%build-rel-root%\%~1" +del /q /f publish.* +popd +if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! + +if "%build-sdk-root%" == "" goto :eof +if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! +rem // Copy a sample gw host.exe, iothub.dll, iothub_client.dll +pushd %build-root%\sdk\%build-platform% +xcopy /e /y /i /q "samples\azure_functions_sample\%~1" "%build-rel-root%\%~1" +popd +if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! +pushd %build-root%\sdk\%build-platform% +xcopy /e /y /i /q "samples\dotnet_core_module_sample\%~1" "%build-rel-root%\%~1" +popd +if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! +pushd %build-root%\sdk\%build-platform% +xcopy /e /y /i /q "modules\iothub\%~1" "%build-rel-root%\%~1" +popd +if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! +rem // rename sample host.exe and delete unneeded files +pushd "%build-rel-root%\%~1" +copy /y dotnet_core_module_sample.* sample_gateway.* +del /q /f dotnet_core_module_sample.* +del /q /f printermodule.* +del /q /f sensormodule.* +popd +if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! +goto :eof + +rem ----------------------------------------------------------------------------- +rem -- subroutines +rem ----------------------------------------------------------------------------- + +:rmdir-force +set _attempt=0 +:try-rmdir +if not exist %1 goto :done-rmdir +set /a _attempt+=1 +if !_attempt! == 30 goto :done-rmdir +echo Removing %~1 (%_attempt%)... +rmdir /s /q %1 +goto :try-rmdir +:done-rmdir +set _attempt= +goto :eof + +:build-done +echo ... Success! +goto :eof + +:usage +echo build.cmd [options] +echo options: +echo -c --clean Build clean (Removes previous build output). +echo -C --config ^ [Debug, Release] build configuration +echo -p --platform ^ [Win32] build platform (e.g. Win32, x64, ...). +echo -i --sdk-root ^ [../azure-iot-gateway-sdk] Gateway SDK repo root. +echo -o --output ^ [/build/release] Root in which to place release. +goto :eof + diff --git a/bld/build.sh b/bld/build.sh new file mode 100644 index 0000000..98e87f1 --- /dev/null +++ b/bld/build.sh @@ -0,0 +1,220 @@ +#!/bin/bash +# Copyright (c) Microsoft. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. + +set -e + +repo_root=$(cd "$(dirname "$0")/.." && pwd) + +skip_unittests=OFF +skip_dotnet=0 +use_zlog=OFF + +build_root="${repo_root}/build" +build_rel_root="${build_root}/release" +build_sdk_root="$(cd "$(dirname "$0")/../.." && pwd)/azure-iot-gateway-sdk" +build_clean=0 +build_pack_only=0 +build_configs=() +build_runtime= + +usage () +{ + echo "build.sh [options]" + echo "options" + echo " -c --clean Build clean (Removes previous build output)." + echo " -C --config ^ [Debug, Release] build configuration" + echo " -r --runtime ^ Runtime to publish for." + echo " -i --sdk-root ^ [../azure-iot-gateway-sdk] Gateway SDK repo root." + echo " -o --output ^ [/build/release] Root in which to place release." + echo " -x --xtrace print a trace of each command" + exit 1 +} + +# ----------------------------------------------------------------------------- +# -- Parse arguments +# ----------------------------------------------------------------------------- +process_args () +{ + save_next_arg=0 + for arg in $*; do + if [ $save_next_arg == 1 ]; then + build_rel_root="$arg" + save_next_arg=0 + elif [ $save_next_arg == 2 ]; then + build_configs+=("$arg") + save_next_arg=0 + elif [ $save_next_arg == 3 ]; then + build_sdk_root="$arg" + save_next_arg=0 + elif [ $save_next_arg == 4 ]; then + build_runtime="$arg" + save_next_arg=0 + else + case "$arg" in + -x | --xtrace) + set -x;; + -o | --build-root) + save_next_arg=1;; + -C | --config) + save_next_arg=2;; + -c | --clean) + build_clean=1;; + -i | --sdk-root) + save_next_arg=3;; + -r | --runtime) + save_next_arg=4;; + *) + usage;; + esac + fi + done + + if [ ! -e "${build_sdk_root}/tools/build.sh" ]; then + echo "No gateway sdk installed at ${build_sdk_root}... " + build_sdk_root= + fi +} + +# ----------------------------------------------------------------------------- +# -- build the sdk +# ----------------------------------------------------------------------------- +sdk_build() +{ + if [ -z "${build_sdk_root}" ]; then + echo "Skipping sdk build..." + else + echo -e "\033[1mBuilding sdk...\033[0m" + for c in ${build_configs[@]}; do + if [ -e "${build_root}/sdk/${c}.done" ]; then + echo "Skipping building sdk ${c}..." + else + echo -e "\033[1m ${c}...\033[0m" + + mkdir -p "${build_root}/sdk/${c}" + + # linux script in tools is totally hosed, build directly + pushd "${build_sdk_root}/bindings/dotnetcore/dotnet-core-binding" > /dev/null + dotnet restore \ + || return $? + dotnet build -c ${c} -r ${build_runtime} \ + ./Microsoft.Azure.IoT.Gateway \ + ./PrinterModule \ + ./SensorModule \ + || return $? + popd > /dev/null + + rm -r -f "${build_sdk_root}/build" || \ + return 1 + rm -r -f "${build_sdk_root}/install-deps" || \ + return 1 + + pushd ${build_sdk_root}/tools > /dev/null + ( ./build.sh --config ${c} --enable-dotnet-core-binding --disable-ble-module ) || \ + return 1 + popd > /dev/null + + cp -r "${build_sdk_root}/build/"* "${build_root}/sdk/${c}" || \ + return 1 + + echo "${c}" >> "${build_root}/sdk/${c}.done" + fi + done + fi + return 0 +} + +# ----------------------------------------------------------------------------- +# -- build module and publish +# ----------------------------------------------------------------------------- +module_build() +{ + pushd "${repo_root}" > /dev/null + echo -e "\033[1mBuilding module...\033[0m" + dotnet restore || exit 1 + for c in ${build_configs[@]}; do + echo -e "\033[1m ${c}...\033[0m" + + mkdir -p "${build_root}/module/${c}" + + dotnet build -c ${c} --framework netstandard1.6 \ + -r ${build_runtime} ./src/Opc.Ua.Client.Module \ + || return $? + + dotnet publish -c ${c} -o "${build_root}/module/${c}" --framework netcoreapp1.1 \ + -r ${build_runtime} ./bld/publish \ + || return $? + + done + popd > /dev/null + return 0 +} + +# ----------------------------------------------------------------------------- +# -- Copy everything into a final release folder +# ----------------------------------------------------------------------------- +release_all() +{ + for c in ${build_configs[@]}; do + rm -rf "${build_rel_root}/${c}" + mkdir -p "${build_rel_root}/${c}" + + pushd "${build_root}/module/${c}" > /dev/null + find . -type f -print0 | xargs -0 -I%%% cp %%% "${build_rel_root}/${c}" || \ + return 1 + popd > /dev/null + + pushd "${build_rel_root}/${c}" > /dev/null + rm -f publish.* + popd > /dev/null + + if [ "${build_sdk_root}" ]; then + pushd "${build_root}/sdk/${c}" > /dev/null + find . -wholename *dotnetcore.so -type f -print0 | xargs -0 \ + -I%%% cp %%% "${build_rel_root}/${c}" || \ + return 1 + find . -wholename *iothub*.so -type f -print0 | xargs -0 \ + -I%%% cp %%% "${build_rel_root}/${c}" || \ + return 1 + find . -wholename *gateway*.so -type f -print0 | xargs -0 \ + -I%%% cp %%% "${build_rel_root}/${c}" || \ + return 1 + cp -r "samples/dotnet_core_module_sample/dotnet_core_module_sample" \ + "${build_rel_root}/${c}/sample_gateway" || \ + return 1 + popd > /dev/null + fi + cp -r "${repo_root}/samples/"*.json "${build_rel_root}/${c}" || \ + return 1 + done + return 0 +} + +pushd "${repo_root}" > /dev/null +process_args $* + +if [ -z "$build_runtime" ]; then + build_runtime=ubuntu.16.10 +fi + +if [ -z "$build_configs" ]; then + build_configs=(Debug Release) +fi + +echo "Building ${build_configs[@]}..." + +if [ $build_clean == 1 ]; then + echo "Cleaning previous build output..." + rm -r -f "${build_root}" +fi + +mkdir -p "${build_root}" + +sdk_build || exit 1 +module_build || exit 1 +release_all || exit 1 + +popd > /dev/null + +[ $? -eq 0 ] || exit $? + diff --git a/bld/publish/dummy.cs b/bld/publish/dummy.cs new file mode 100644 index 0000000..acbf303 --- /dev/null +++ b/bld/publish/dummy.cs @@ -0,0 +1,10 @@ +namespace Opc.Ua.Client +{ + class Program + { + static void Main(string[] args) + { + var module = new SampleModule(); + } + } +} diff --git a/bld/publish/project.json b/bld/publish/project.json new file mode 100644 index 0000000..a30fe44 --- /dev/null +++ b/bld/publish/project.json @@ -0,0 +1,26 @@ +{ + "buildOptions": { + "emitEntryPoint": true + }, + "dependencies": { + "Opc.Ua.Client.Module": "0.1.6" + }, + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.1" + } + } + }, + "netcoreapp1.1": { + "dependencies": { + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.1.0" + } + } + } + } +} diff --git a/gateway_config.json b/gateway_config.json deleted file mode 100644 index 5e77d68..0000000 --- a/gateway_config.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "modules": [ - { - "name": "opc_ua", - "loader": { - "name": "dotnet", - "entrypoint": { - "assembly.name": "Opc.Ua.Client.SampleModule", - "entry.type": "Opc.Ua.Client.SampleModule" - } - }, - "args": ";" - }, - { - "name": "IoTHub", - "loader": { - "name": "native", - "entrypoint": { - "module.path": "iothub.dll" - } - }, - "args": { - "IoTHubName": "", - "IoTHubSuffix": "azure-devices.net", - "Transport": "AMQP" - } - } - ], - "links": [ - { - "source": "opc_ua", - "sink": "IoTHub" - } - ] -} diff --git a/global.json b/global.json new file mode 100644 index 0000000..35eb7c7 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "projects": [ + "src", "bld/publish" + ] +} diff --git a/packages.config b/packages.config deleted file mode 100644 index 16a1fcd..0000000 --- a/packages.config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/samples/gateway_config.json b/samples/gateway_config.json new file mode 100644 index 0000000..706c37d --- /dev/null +++ b/samples/gateway_config.json @@ -0,0 +1,61 @@ +{ + "modules": [ + { + "name": "opc_ua", + "loader": { + "name": "dotnetcore", + "entrypoint": { + "assembly.name": "Opc.Ua.Client.Module", + "entry.type": "Opc.Ua.Client.SampleModule" + } + }, + "args": { + "Configuration": { + "ApplicationName": "Opc.Ua.Client.SampleModule", + "ApplicationType": "Client", + "ApplicationUri": "urn:localhost:OPCFoundation:SampleModule", + "SecurityConfiguration": { + "ApplicationCertificate": {} + } + }, + "Subscriptions": [ + { + "Id": "", + "SharedAccessKey": "", + "ServerUrl": "opc.tcp://:51210/UA/SampleServer", + "PublishingInterval": 400, + "MonitoredItems": [ + { + "StartNodeId": "i=2258", + "NodeClass": 2, + "DisplayName": "ServerStatusCurrentTime", + "DiscardOldest": false + } + ] + } + ] + } + }, + { + "name": "IoTHub", + "loader": { + "name": "native", + "entrypoint": { + "module.path": "" + } + }, + "args": { + "IoTHubName": "", + "IoTHubSuffix": "azure-devices.net", + "Transport": "AMQP" + } + } + ], + "links": [ + { + "source": "opc_ua", + "sink": "IoTHub" + } + ] +} + diff --git a/src/Opc.Ua.Client.Module/35MSSharedLib1024.snk b/src/Opc.Ua.Client.Module/35MSSharedLib1024.snk new file mode 100644 index 0000000000000000000000000000000000000000..695f1b38774e839e5b90059bfb7f32df1dff4223 GIT binary patch literal 160 zcmV;R0AK$ABme*efB*oL000060ssI2Bme+XQ$aBR1ONa50098C{E+7Ye`kjtcRG*W zi8#m|)B?I?xgZ^2Sw5D;l4TxtPwG;3)3^j?qDHjEteSTF{rM+4WI`v zCD?tsZ^;k+S&r1&HRMb=j738S=;J$tCKNrc$@P|lZ + /// Sample Gateway module - acts as Opc.Ua publisher and publishing server + /// + public class SampleModule : IGatewayModule, IGatewayModuleStart + { + /// + /// Create module, throws if configuration is bad + /// + /// + /// + public void Create(Broker broker, byte[] configuration) + { + _broker = broker; + + string configString = Encoding.UTF8.GetString(configuration); + + // Deserialize from configuration string + _configuration = JsonConvert.DeserializeObject(configString); + + foreach (var session in _configuration.Subscriptions) + { + session.Module = this; + } + + Console.WriteLine("Opc.Ua.Client.SampleModule: created."); + } + + /// + /// Disconnect and dispose all sessions + /// + public void Destroy() + { + foreach (var session in _configuration.Subscriptions) + { + // Disconnect and dispose + session.Dispose(); + } + // Then gc. + _configuration.Subscriptions.Clear(); + Console.WriteLine("Opc.Ua.Client.SampleModule: destroyed."); + } + + /// + /// Receive message from broker + /// + /// + public void Receive(Message received_message) + { + // No-op + } + + /// + /// Publish message to bus + /// + /// + public void Publish(Message message) + { + if (_broker != null) + { + _broker.Publish(message); + } + } + + /// + /// Called when gateway starts, establishes the connections to endpoints + /// + public void Start() + { + Console.WriteLine("Opc.Ua.Client.SampleModule: starting..."); + + var connections = new List(); + foreach (var session in _configuration.Subscriptions) + { + connections.Add(session.EndpointConnect()); + } + try + { + Task.WaitAll(connections.ToArray()); + } + catch (AggregateException ae) + { + foreach (var ex in ae.InnerExceptions) + { + Console.WriteLine($"Opc.Ua.Client.SampleModule: Could not connect {ex.ToString()}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Opc.Ua.Client.SampleModule: Could not connect {ex.ToString()}"); + } + + // Wait for all sessions to be connected + Console.WriteLine("Opc.Ua.Client.SampleModule: started."); + } + + + /// + /// Allow access to the opc ua configuration + /// + internal ApplicationConfiguration Configuration + { + get + { + return _configuration.Configuration; + } + } + + private Broker _broker; + private SampleConfiguration _configuration; + } + + /// + /// Module configuration object to deserialize / serialize + /// + [JsonObject(MemberSerialization.OptIn)] + public class SampleConfiguration + { + /// + /// Opc client configuration + /// + [JsonProperty] + public ApplicationConfiguration Configuration { get; set; } + + /// + /// List of sessions to create on startup + /// + [JsonProperty] + public List Subscriptions { get; set; } + + /// + /// Called when the object is deserialized + /// + /// + [OnDeserialized] + internal void OnDeserializedMethod(StreamingContext context) + { + // Validate configuration and set reasonable defaults + + Configuration.ApplicationUri = Configuration.ApplicationUri.Replace("localhost", Utils.GetHostName()); + + if (Configuration.TransportQuotas == null) + Configuration.TransportQuotas = new TransportQuotas { OperationTimeout = 15000 }; + + if (Configuration.ClientConfiguration == null) + Configuration.ClientConfiguration = new ClientConfiguration(); + if (Configuration.ServerConfiguration == null) + Configuration.ServerConfiguration = new ServerConfiguration(); + + if (Configuration.SecurityConfiguration.TrustedPeerCertificates == null) + Configuration.SecurityConfiguration.TrustedPeerCertificates = new CertificateTrustList(); + if (Configuration.SecurityConfiguration.TrustedPeerCertificates.StoreType == null) + Configuration.SecurityConfiguration.TrustedPeerCertificates.StoreType = + "Directory"; + if (Configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath == null) + Configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath = + "OPC Foundation/CertificateStores/UA Applications"; + + if (Configuration.SecurityConfiguration.TrustedIssuerCertificates == null) + Configuration.SecurityConfiguration.TrustedIssuerCertificates = new CertificateTrustList(); + if (Configuration.SecurityConfiguration.TrustedIssuerCertificates.StoreType == null) + Configuration.SecurityConfiguration.TrustedIssuerCertificates.StoreType = + "Directory"; + if (Configuration.SecurityConfiguration.TrustedIssuerCertificates.StorePath == null) + Configuration.SecurityConfiguration.TrustedIssuerCertificates.StorePath = + "OPC Foundation/CertificateStores/UA Certificate Authorities"; + + if (Configuration.SecurityConfiguration.RejectedCertificateStore == null) + Configuration.SecurityConfiguration.RejectedCertificateStore = new CertificateTrustList(); + if (Configuration.SecurityConfiguration.RejectedCertificateStore.StoreType == null) + Configuration.SecurityConfiguration.RejectedCertificateStore.StoreType = + "Directory"; + if (Configuration.SecurityConfiguration.RejectedCertificateStore.StorePath == null) + Configuration.SecurityConfiguration.RejectedCertificateStore.StorePath = + "OPC Foundation/CertificateStores/RejectedCertificates"; + + if (Configuration.SecurityConfiguration.ApplicationCertificate == null) + Configuration.SecurityConfiguration.ApplicationCertificate = new CertificateIdentifier(); + if (Configuration.SecurityConfiguration.ApplicationCertificate.StoreType == null) + Configuration.SecurityConfiguration.ApplicationCertificate.StoreType = + "Directory"; + if (Configuration.SecurityConfiguration.ApplicationCertificate.StorePath == null) + Configuration.SecurityConfiguration.ApplicationCertificate.StorePath = + "OPC Foundation/CertificateStores/MachineDefault"; + if (Configuration.SecurityConfiguration.ApplicationCertificate.SubjectName == null) + Configuration.SecurityConfiguration.ApplicationCertificate.SubjectName = + Configuration.ApplicationName; + if (Configuration.SecurityConfiguration.ApplicationCertificate.Certificate == null) + { + X509Certificate2 certificate = CertificateFactory.CreateCertificate( + Configuration.SecurityConfiguration.ApplicationCertificate.StoreType, + Configuration.SecurityConfiguration.ApplicationCertificate.StorePath, + Configuration.ApplicationUri, + Configuration.ApplicationName, + Configuration.SecurityConfiguration.ApplicationCertificate.SubjectName + ); + Configuration.SecurityConfiguration.ApplicationCertificate.Certificate = certificate; + } + + if (Configuration.SecurityConfiguration.ApplicationCertificate.Certificate != null) + { + Configuration.ApplicationUri = Utils.GetApplicationUriFromCertificate( + Configuration.SecurityConfiguration.ApplicationCertificate.Certificate); + } + else + { + Console.WriteLine("Opc.Ua.Client.SampleModule: WARNING: missing application certificate, using unsecure connection."); + } + Configuration.Validate(Configuration.ApplicationType).Wait(); + + if (Configuration.SecurityConfiguration.AutoAcceptUntrustedCertificates) + Configuration.CertificateValidator.CertificateValidation += + new CertificateValidationEventHandler(CertificateValidator_CertificateValidation); + } + + /// + /// Auto accept certificates + /// + /// + /// + private void CertificateValidator_CertificateValidation(CertificateValidator validator, CertificateValidationEventArgs e) + { + if (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted) + { + e.Accept = true; + Console.WriteLine($"Opc.Ua.Client.SampleModule: WARNING: Auto-accepting certificate: {e.Certificate.Subject}"); + } + } + } + + /// + /// A server session contains a subscription for a list of monitored items + /// + [JsonObject(MemberSerialization.OptIn)] + public class ServerSession : IDisposable + { + /// + /// Url of the Server to connect to + /// + [JsonProperty] + public Uri ServerUrl { get; set; } + + /// + /// SharedAccessKey for device + /// + /// TODO: Security: The shared access key should be stored in secure storage, + /// and the device ID can be used as a lookup + /// + [JsonProperty] + public string SharedAccessKey { get; set; } + + /// + /// Device id or mappable id for mapper + /// + [JsonProperty] + public string Id { get; set; } + + /// + /// Polling interval + /// + [JsonProperty] + public int PublishingInterval { get; set; } + + /// + /// Monitored item configuration + /// + [JsonProperty] + public List MonitoredItems { get; set; } + + /// + /// Default constructor + /// + public ServerSession() + { + MonitoredItems = new List(); + PublishingInterval = 1000; + } + + /// + /// The Module the session is attached to. + /// + internal SampleModule Module { get; set; } + + /// + /// Called when the object is deserialized + /// + /// + [OnDeserialized] + internal void OnDeserializedMethod(StreamingContext context) + { + if (MonitoredItems.Count == 0) + { + throw new Exception("Configuration did not specify monitored items!"); + } + + if (ServerUrl == null) + { + throw new Exception("Configuration did not contain an endpoint Uri"); + } + + if (string.IsNullOrEmpty(Id)) + { + throw new Exception("Configuration did not contain a device name!"); + } + + MonitoredItems.ForEach(i => i.Notification += MonitoredItem_Notification); + } + + public async Task EndpointConnect() + { + EndpointDescription selectedEndpoint = SelectUaTcpEndpoint(DiscoverEndpoints(Module.Configuration, ServerUrl, 60)); + ConfiguredEndpoint configuredEndpoint = new ConfiguredEndpoint(selectedEndpoint.Server, EndpointConfiguration.Create(Module.Configuration)); + configuredEndpoint.Update(selectedEndpoint); + + _session = await Session.Create( + Module.Configuration, + configuredEndpoint, + true, + false, + Module.Configuration.ApplicationName, + 60000, + new UserIdentity(new AnonymousIdentityToken()), + null); + + if (_session != null) + { + var subscription = new Subscription(_session.DefaultSubscription); + subscription.PublishingInterval = PublishingInterval; + // ... + + subscription.AddItems(MonitoredItems); + _session.AddSubscription(subscription); + + subscription.Create(); + + Console.WriteLine($"Opc.Ua.Client.SampleModule: Created session with updated endpoint {configuredEndpoint.EndpointUrl} from server!"); + _session.KeepAlive += new KeepAliveEventHandler(StandardClient_KeepAlive); + } + else + { + throw new Exception("Could not create session"); + } + } + + private void MonitoredItem_Notification(MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs e) + { + try + { + if (e.NotificationValue == null || monitoredItem.Subscription.Session == null) + { + return; + } + + JsonEncoder encoder = new JsonEncoder(monitoredItem.Subscription.Session.MessageContext, false); + string hostname = monitoredItem.Subscription.Session.ConfiguredEndpoint.EndpointUrl.DnsSafeHost; + if (hostname == "localhost") + { + hostname = Utils.GetHostName(); + } + encoder.WriteString("HostName", hostname); + encoder.WriteNodeId("MonitoredItem", monitoredItem.ResolvedNodeId); + e.NotificationValue.Encode(encoder); + + string json = encoder.CloseAndReturnText(); + + var properties = new Dictionary(); + properties.Add("content-type", "application/opcua+uajson"); + properties.Add("deviceName", Id); + + if (SharedAccessKey != null) + { + properties.Add("source", "mapping"); + properties.Add("deviceKey", SharedAccessKey); + } + + try + { + Module.Publish(new Message(json, properties)); + } + catch (Exception ex) + { + Utils.Trace(ex, "Opc.Ua.Client.SampleModule: Failed to publish message, dropping...."); + } + + } + catch (Exception exception) + { + Utils.Trace(exception, "Opc.Ua.Client.SampleModule: Error processing monitored item notification."); + } + } + + private void StandardClient_KeepAlive(Session sender, KeepAliveEventArgs e) + { + if (e != null && sender != null) + { + if (!ServiceResult.IsGood(e.Status)) + { + Console.WriteLine($"Opc.Ua.Client.SampleModule: Server {sender.ConfiguredEndpoint.EndpointUrl} Status NOT good: {e.Status} {sender.OutstandingRequestCount}/{sender.DefunctRequestCount}"); + } + } + } + + private EndpointDescriptionCollection DiscoverEndpoints(ApplicationConfiguration config, Uri discoveryUrl, int timeout) + { + EndpointConfiguration configuration = EndpointConfiguration.Create(config); + configuration.OperationTimeout = timeout; + + using (DiscoveryClient client = DiscoveryClient.Create(discoveryUrl, EndpointConfiguration.Create(config))) + { + try + { + EndpointDescriptionCollection endpoints = client.GetEndpoints(null); + ReplaceLocalHostWithRemoteHost(endpoints, discoveryUrl); + return endpoints; + } + catch (Exception e) + { + Console.WriteLine($"Opc.Ua.Client.SampleModule: Could not fetch endpoints from url: {discoveryUrl}"); + Console.WriteLine($"Opc.Ua.Client.SampleModule: Reason = {e.Message}"); + throw e; + } + } + } + + private void ReplaceLocalHostWithRemoteHost(EndpointDescriptionCollection endpoints, Uri discoveryUrl) + { + foreach (EndpointDescription endpoint in endpoints) + { + endpoint.EndpointUrl = Utils.ReplaceLocalhost(endpoint.EndpointUrl, discoveryUrl.DnsSafeHost); + StringCollection updatedDiscoveryUrls = new StringCollection(); + + foreach (string url in endpoint.Server.DiscoveryUrls) + { + updatedDiscoveryUrls.Add(Utils.ReplaceLocalhost(url, discoveryUrl.DnsSafeHost)); + } + + endpoint.Server.DiscoveryUrls = updatedDiscoveryUrls; + } + } + + private EndpointDescription SelectUaTcpEndpoint(EndpointDescriptionCollection endpointCollection) + { + EndpointDescription bestEndpoint = null; + foreach (EndpointDescription endpoint in endpointCollection) + { + if (endpoint.TransportProfileUri == Profiles.UaTcpTransport) + { + if ((bestEndpoint == null) || + (endpoint.SecurityLevel > bestEndpoint.SecurityLevel)) + { + bestEndpoint = endpoint; + } + } + } + + return bestEndpoint; + } + + /// + /// Dispose + /// + public void Dispose() + { + Dispose(true); + } + + /// + /// Dispose implementation + /// + /// + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + lock (this) + { + if (!_disposedValue) + { + if (disposing) + { + if (_session != null) + { + _session.Dispose(); + _session = null; + } + } + + _disposedValue = true; + } + } + } + } + + private Session _session; + private bool _disposedValue = false; // To detect redundant calls + } +} diff --git a/src/Opc.Ua.Client.Module/Opc.Ua.Client.Module.xproj b/src/Opc.Ua.Client.Module/Opc.Ua.Client.Module.xproj new file mode 100644 index 0000000..4cc457e --- /dev/null +++ b/src/Opc.Ua.Client.Module/Opc.Ua.Client.Module.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + {43311AFB-D7C4-4E5A-B1DE-855407F90D1B} + Opc.Ua.Client.Module + .\obj + .\bin\ + v4.6.1 + + + 2.0 + + + \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/src/Opc.Ua.Client.Module/Properties/AssemblyInfo.cs similarity index 83% rename from Properties/AssemblyInfo.cs rename to src/Opc.Ua.Client.Module/Properties/AssemblyInfo.cs index 8cfd971..a86c411 100644 --- a/Properties/AssemblyInfo.cs +++ b/src/Opc.Ua.Client.Module/Properties/AssemblyInfo.cs @@ -16,3 +16,10 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("6C6DEC77-2861-4150-A307-74F733186A27")] + +[assembly: AssemblyVersion("0.1.15.0")] + +#if RELEASE_DELAY_SIGN +[assembly: AssemblyDelaySign(true)] +[assembly: AssemblyKeyFile("35MSSharedLib1024.snk")] +#endif diff --git a/src/Opc.Ua.Client.Module/project.json b/src/Opc.Ua.Client.Module/project.json new file mode 100644 index 0000000..4d78ea6 --- /dev/null +++ b/src/Opc.Ua.Client.Module/project.json @@ -0,0 +1,61 @@ +{ + "version": "0.1.6", + "name": "Opc.Ua.Client.Module", + "title": "Opc Ua Publisher Module for Azure IoT Field Gateway", + "description": "Managed OPC UA Publisher module for the Azure IoT Field Gateway", + "authors": [ "microsoft" ], + "buildOptions": { + "xmlDoc": false, + "allowUnsafe": true, + "warningsAsErrors": true, + "nowarn": [ "1591", "1734" ], + "define": [ "TRACE" ] + }, + "packOptions": { + "tags": [ "Azure", "IoT", ".NET", "OPC UA", "Gateway" ], + "projectUrl": "https://github.com/Azure/iot-gateway-opc-ua", + "licenseUrl": "https://raw.githubusercontent.com/Azure/iot-gateway-opc-ua/master/license.txt", + "releaseNotes": "https://github.com/Azure/iot-gateway-opc-ua/releases", + "requireLicenseAcceptance": true, + "repository": { + "url": "https://github.com/Azure/iot-gateway-opc-ua" + } + }, + "dependencies": { + "OPCFoundation.NetStandard.Opc.Ua.Core": "0.1.6", + "OPCFoundation.NetStandard.Opc.Ua.SDK": "0.1.6", + "Portable.BouncyCastle": "1.8.1.2", + "NETStandard.Library": "1.6.1", + "Newtonsoft.Json": "9.0.1" + }, + "frameworks": { + "net46": { + "dependencies": { + "Azure.IoT.Gateway.SDK.Net": "2017.1.13.2" + } + }, + "netstandard1.6": { + "dependencies": { + "Microsoft.Azure.IoT.Gateway": "1.0.0-*", + "System.Runtime.Serialization.Json": "4.3.0", + "system.xml.xpath.xmldocument": "4.3.0", + "System.Xml.XmlDocument": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Private.ServiceModel": "4.1.0" + } + } + }, + "configurations": { + "Signed": { + "buildOptions": { + "define": [ "RELEASE_DELAY_SIGN" ], + "optimize": true + } + }, + "Release": { + "buildOptions": { + "optimize": true + } + } + } +} From 2cddb08a38538cec02975d4d942cb81336af4247 Mon Sep 17 00:00:00 2001 From: Marc Schier Date: Tue, 21 Feb 2017 10:18:54 +0100 Subject: [PATCH 2/3] Updates from code review feedback --- README.md | 6 +- bld/build.cmd | 53 ++++----- bld/publish/project.json | 8 -- samples/gateway_config.json | 2 + src/Opc.Ua.Client.Module/Module.cs | 181 +++++++++++++++++------------ 5 files changed, 139 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index 30e484e..6d97de8 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Use the following command line to clone the compatible version Azure IoT Gateway git clone --recursive https://github.com/Azure/azure-iot-gateway-sdk.git git checkout 09bbcb7feaf5acc3913abd722b96e993238edd0c ``` + +The gateway needs to be build with the ```--enable_dotnetcore_binding``` flag to enable it to run this module. # Directory Structure ## /samples @@ -80,5 +82,7 @@ E.g. the sample template shows how to monitor the **Current Server Time** node ( To run the module and have it publish to IoT Hub, configure the name of your Hub (JSON field ```"IoTHubName"```) and the IoT Hub device ID and shared access key to use (JSON fields ```"Id"``` and ```"SharedAccessKey"```) in your version of ```gateway_config.json```. Note that if you use a "Mapping" module in your configuration, you can omit the ```"SharedAccessKey"``` field. Finally, ensure that the right native module is configured, based on your platform (i.e. iothub.dll for Windows, libiothub.so for Linux, etc.). -To build a sample gateway to host the OPC-UA module - along with the module itself - clone the gateway SDK repo to your device and run the build script with the ```-i ```. The resulting release folder will contain not just the module and managed dependencies, but also a native IoT Hub proxy module as well as a ```sample_gateway``` executable that you can pass the updated JSON configuration to. +You can build a sample gateway host as part of the Azure IoT Gateway SDK build system. Ensure that you pass the ```--enable_dotnetcore_binding``` to the build script and use one of the resulting sample gateway hosts. + +To simplify this step, and to build a sample gateway to host the OPC-UA module - along with the module itself - clone the gateway SDK repo to your device and run the build script with the ```-i ```. The resulting release folder will contain not just the module and managed dependencies, but also a native IoT Hub proxy module as well as a ```sample_gateway``` executable that you can pass the updated JSON configuration to. diff --git a/bld/build.cmd b/bld/build.cmd index c6176f3..9cc506f 100644 --- a/bld/build.cmd +++ b/bld/build.cmd @@ -24,6 +24,7 @@ set build-clean= set build-configs= set build-platform=Win32 if "%PROCESSOR_ARCHITECTURE%" == "AMD64" set build-platform=x64 +set build-runtime= set build-root=%repo-root%\build set build-rel-root=%build-root%\release @@ -39,6 +40,8 @@ if "%1" equ "--sdk-root" goto :arg-sdk-root-folder if "%1" equ "-i" goto :arg-sdk-root-folder if "%1" equ "--platform" goto :arg-build-platform if "%1" equ "-p" goto :arg-build-platform +if "%1" equ "--runtime" goto :arg-build-runtime +if "%1" equ "-r" goto :arg-build-runtime call :usage && exit /b 1 :arg-build-clean @@ -63,6 +66,12 @@ if "%1" equ "" call :usage && exit /b 1 set build-platform=%1 goto :args-continue +:arg-build-runtime +shift +if "%1" equ "" call :usage && exit /b 1 +set build-runtime=-r %1 +goto :args-continue + :arg-build-rel-root shift if "%1" equ "" call :usage && exit /b 1 @@ -153,43 +162,30 @@ goto :eof rem // Build module and publish for 1 configuration :dotnet-build-and-publish if /I not "%~1" == "Release" if /I not "%~1" == "Debug" if /I not "%~1" == "Signed" goto :eof -pushd %repo-root% -call :dotnet-build %~1 -popd -if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! -pushd %repo-root%\bld\publish -call :dotnet-publish %~1 -popd -if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! -goto :eof -rem // Build module +rem // Clean +:dotnet-clean +if "%build-clean%" == "" goto :dotnet-build +call :dotnet-project-clean %repo-root%\src\Opc.Ua.Client.Module +call :dotnet-project-clean %repo-root%\bld\publish +rem // Build :dotnet-build -for /f %%i in ('dir /b /s project.json') do ( - call :dotnet-project-build %~1 "%%i" - if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! -) -goto :eof +pushd %repo-root%\src\Opc.Ua.Client.Module +call dotnet build %build-runtime% -c %~1 +popd +if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! rem // Publish all assemblies using publish dummy exe :dotnet-publish -if not exist "%build-root%\module\%~1" mkdir "%build-root%\module\%~1" -if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! -echo. -call dotnet publish --no-build -c %~1 -o "%build-root%\module\%~1" -if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! -goto :eof -rem // Build a project -:dotnet-project-build -pushd "%~dp2" -echo. -if not "%build-clean%" == "" call :rmdir-force "bin\%~1" -call dotnet build -c %~1 +pushd %repo-root%\bld\publish +call dotnet publish %build-runtime% -c %~1 -o "%build-root%\module\%~1" popd if not !ERRORLEVEL! == 0 exit /b !ERRORLEVEL! goto :eof + rem // Clean a project :dotnet-project-clean pushd "%~dp1" call :rmdir-force bin +call :rmdir-force obj popd goto :eof @@ -275,8 +271,9 @@ echo build.cmd [options] echo options: echo -c --clean Build clean (Removes previous build output). echo -C --config ^ [Debug, Release] build configuration -echo -p --platform ^ [Win32] build platform (e.g. Win32, x64, ...). +echo -r --runtime ^ [win] The runtime to build module for. echo -i --sdk-root ^ [../azure-iot-gateway-sdk] Gateway SDK repo root. +echo -p --platform ^ [Win32] build platform (e.g. Win32, x64, ...). echo -o --output ^ [/build/release] Root in which to place release. goto :eof diff --git a/bld/publish/project.json b/bld/publish/project.json index a30fe44..9480361 100644 --- a/bld/publish/project.json +++ b/bld/publish/project.json @@ -6,14 +6,6 @@ "Opc.Ua.Client.Module": "0.1.6" }, "frameworks": { - "netcoreapp1.0": { - "dependencies": { - "Microsoft.NETCore.App": { - "type": "platform", - "version": "1.0.1" - } - } - }, "netcoreapp1.1": { "dependencies": { "Microsoft.NETCore.App": { diff --git a/samples/gateway_config.json b/samples/gateway_config.json index 706c37d..24b2075 100644 --- a/samples/gateway_config.json +++ b/samples/gateway_config.json @@ -23,6 +23,8 @@ "Id": "", "SharedAccessKey": "", "ServerUrl": "opc.tcp://:51210/UA/SampleServer", + "MinimumSecurityLevel": 0, + "MinimumSecurityMode": "SignAndEncrypt", "PublishingInterval": 400, "MonitoredItems": [ { diff --git a/src/Opc.Ua.Client.Module/Module.cs b/src/Opc.Ua.Client.Module/Module.cs index 83b331c..00480c6 100644 --- a/src/Opc.Ua.Client.Module/Module.cs +++ b/src/Opc.Ua.Client.Module/Module.cs @@ -192,10 +192,10 @@ namespace Opc.Ua.Client Configuration.SecurityConfiguration.ApplicationCertificate = new CertificateIdentifier(); if (Configuration.SecurityConfiguration.ApplicationCertificate.StoreType == null) Configuration.SecurityConfiguration.ApplicationCertificate.StoreType = - "Directory"; + "X509Store"; if (Configuration.SecurityConfiguration.ApplicationCertificate.StorePath == null) Configuration.SecurityConfiguration.ApplicationCertificate.StorePath = - "OPC Foundation/CertificateStores/MachineDefault"; + "CurrentUser\\UA_MachineDefault"; if (Configuration.SecurityConfiguration.ApplicationCertificate.SubjectName == null) Configuration.SecurityConfiguration.ApplicationCertificate.SubjectName = Configuration.ApplicationName; @@ -275,6 +275,18 @@ namespace Opc.Ua.Client [JsonProperty] public int PublishingInterval { get; set; } + /// + /// Minimum desired Security Level + /// + [JsonProperty] + public byte MinimumSecurityLevel { get; set; } = 0; + + /// + /// Minimum desired Security mode + /// + [JsonProperty] + public MessageSecurityMode MinimumSecurityMode { get; set; } = MessageSecurityMode.SignAndEncrypt; + /// /// Monitored item configuration /// @@ -322,84 +334,123 @@ namespace Opc.Ua.Client public async Task EndpointConnect() { - EndpointDescription selectedEndpoint = SelectUaTcpEndpoint(DiscoverEndpoints(Module.Configuration, ServerUrl, 60)); - ConfiguredEndpoint configuredEndpoint = new ConfiguredEndpoint(selectedEndpoint.Server, EndpointConfiguration.Create(Module.Configuration)); - configuredEndpoint.Update(selectedEndpoint); + var endpointCollection = DiscoverEndpoints(Module.Configuration, ServerUrl, 60); + var selectedEndpoints = new List(); - _session = await Session.Create( - Module.Configuration, - configuredEndpoint, - true, - false, - Module.Configuration.ApplicationName, - 60000, - new UserIdentity(new AnonymousIdentityToken()), - null); - - if (_session != null) + // Select endpoints + foreach (EndpointDescription endpoint in endpointCollection) { - var subscription = new Subscription(_session.DefaultSubscription); - subscription.PublishingInterval = PublishingInterval; - // ... - - subscription.AddItems(MonitoredItems); - _session.AddSubscription(subscription); - - subscription.Create(); - - Console.WriteLine($"Opc.Ua.Client.SampleModule: Created session with updated endpoint {configuredEndpoint.EndpointUrl} from server!"); - _session.KeepAlive += new KeepAliveEventHandler(StandardClient_KeepAlive); + if (endpoint.TransportProfileUri == Profiles.UaTcpTransport && + endpoint.SecurityLevel >= MinimumSecurityLevel && + endpoint.SecurityMode >= MinimumSecurityMode) + { + selectedEndpoints.Add(endpoint); + } } - else + + // + // Sort, but descending with highest level first i.e. return + // < 0 if x is less than y + // > 0 if x is greater than y + // 0 if x and y are equal + // + selectedEndpoints.Sort((y, x) => x.SecurityLevel - y.SecurityLevel); + + foreach (EndpointDescription endpoint in selectedEndpoints) { - throw new Exception("Could not create session"); + ConfiguredEndpoint configuredEndpoint = new ConfiguredEndpoint( + endpoint.Server, EndpointConfiguration.Create(Module.Configuration)); + configuredEndpoint.Update(endpoint); + + _session = await Session.Create( + Module.Configuration, + configuredEndpoint, + true, + false, + Module.Configuration.ApplicationName, + 60000, + new UserIdentity(new AnonymousIdentityToken()), + null); + + if (_session != null) + { + var subscription = new Subscription(_session.DefaultSubscription); + subscription.PublishingInterval = PublishingInterval; + + // TODO: Make other subscription settings configurable... + + subscription.AddItems(MonitoredItems); + _session.AddSubscription(subscription); + subscription.Create(); + + Console.WriteLine($"Opc.Ua.Client.SampleModule: Created session with updated endpoint {configuredEndpoint.EndpointUrl} from server!"); + _session.KeepAlive += new KeepAliveEventHandler(StandardClient_KeepAlive); + + // Done + return; + } + + Console.WriteLine($"Opc.Ua.Client.SampleModule: WARNING Could not create session to endpoint {endpoint.ToString()}..."); + // ... try another endpoint until we do not have any more... } + + throw new Exception("Failed to find acceptable endpoint to connect to."); } private void MonitoredItem_Notification(MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs e) { try { - if (e.NotificationValue == null || monitoredItem.Subscription.Session == null) + MonitoredItemNotification notification = e.NotificationValue as MonitoredItemNotification; + if (notification == null) { return; } - JsonEncoder encoder = new JsonEncoder(monitoredItem.Subscription.Session.MessageContext, false); - string hostname = monitoredItem.Subscription.Session.ConfiguredEndpoint.EndpointUrl.DnsSafeHost; - if (hostname == "localhost") + DataValue value = notification.Value as DataValue; + if (value == null) { - hostname = Utils.GetHostName(); - } - encoder.WriteString("HostName", hostname); - encoder.WriteNodeId("MonitoredItem", monitoredItem.ResolvedNodeId); - e.NotificationValue.Encode(encoder); - - string json = encoder.CloseAndReturnText(); - - var properties = new Dictionary(); - properties.Add("content-type", "application/opcua+uajson"); - properties.Add("deviceName", Id); - - if (SharedAccessKey != null) - { - properties.Add("source", "mapping"); - properties.Add("deviceKey", SharedAccessKey); + return; } - try + using (var encoder = new JsonEncoder(monitoredItem.Subscription.Session.MessageContext, false)) { - Module.Publish(new Message(json, properties)); - } - catch (Exception ex) - { - Utils.Trace(ex, "Opc.Ua.Client.SampleModule: Failed to publish message, dropping...."); + string applicationURI = monitoredItem.Subscription.Session.Endpoint.Server.ApplicationUri; + encoder.WriteString("ApplicationUri", applicationURI); + encoder.WriteString("DisplayName", monitoredItem.DisplayName); + encoder.WriteNodeId("MonitoredItem", monitoredItem.ResolvedNodeId); + // suppress output of server timestamp in json by setting it to minvalue + value.ServerTimestamp = DateTime.MinValue; + encoder.WriteDataValue("Value", value); + + string json = encoder.CloseAndReturnText(); + + var properties = new Dictionary(); + properties.Add("content-type", "application/opcua+uajson"); + properties.Add("deviceName", Id); + + if (SharedAccessKey != null) + { + properties.Add("source", "mapping"); + properties.Add("deviceKey", SharedAccessKey); + } + + try + { + Module.Publish(new Message(json, properties)); + } + catch (Exception ex) + { + Console.WriteLine("Opc.Ua.Client.SampleModule: Failed to publish message, dropping..."); + Console.WriteLine(ex.ToString()); + } } } catch (Exception exception) { - Utils.Trace(exception, "Opc.Ua.Client.SampleModule: Error processing monitored item notification."); + Console.WriteLine("Opc.Ua.Client.SampleModule: Error processing monitored item notification."); + Console.WriteLine(exception.ToString()); } } @@ -452,24 +503,6 @@ namespace Opc.Ua.Client } } - private EndpointDescription SelectUaTcpEndpoint(EndpointDescriptionCollection endpointCollection) - { - EndpointDescription bestEndpoint = null; - foreach (EndpointDescription endpoint in endpointCollection) - { - if (endpoint.TransportProfileUri == Profiles.UaTcpTransport) - { - if ((bestEndpoint == null) || - (endpoint.SecurityLevel > bestEndpoint.SecurityLevel)) - { - bestEndpoint = endpoint; - } - } - } - - return bestEndpoint; - } - /// /// Dispose /// From 763a6145ee4e52b70c9e75c4003a30e3b9dc5674 Mon Sep 17 00:00:00 2001 From: Marc Schier Date: Tue, 21 Feb 2017 10:39:35 +0100 Subject: [PATCH 3/3] Update readme to reflect security settings --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 6d97de8..3555455 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ E.g. the sample template shows how to monitor the **Current Server Time** node ( "Id": "", "SharedAccessKey": "", "ServerUrl": "opc.tcp://:51210/UA/SampleServer", + "MinimumSecurityLevel": 0, + "MinimumSecurityMode": "SignAndEncrypt", "PublishingInterval": 400, "MonitoredItems": [ { @@ -78,6 +80,8 @@ E.g. the sample template shows how to monitor the **Current Server Time** node ( }, ``` +The JSON snippet above shows the default security settings, which can thus be ommitted. By default the session is created on the endpoint that supports ```SignAndEncrypt``` message mode, regardless of the security level advertised by the server (0). You can adjust these settings to customize the endpoint selection process, e.g. setting the Security mode to ```"Sign"``` will select all ```Sign``` and ```SignAndEncrypt``` endpoints, ```"None"``` will select all endpoints. + # Running the module To run the module and have it publish to IoT Hub, configure the name of your Hub (JSON field ```"IoTHubName"```) and the IoT Hub device ID and shared access key to use (JSON fields ```"Id"``` and ```"SharedAccessKey"```) in your version of ```gateway_config.json```. Note that if you use a "Mapping" module in your configuration, you can omit the ```"SharedAccessKey"``` field. Finally, ensure that the right native module is configured, based on your platform (i.e. iothub.dll for Windows, libiothub.so for Linux, etc.).