├── .gitignore
├── App.xaml
├── App.xaml.cs
├── Assets
├── LockScreenLogo.scale-200.png
├── SplashScreen.scale-200.png
├── Square150x150Logo.scale-200.png
├── Square44x44Logo.scale-200.png
├── Square44x44Logo.targetsize-24_altform-unplated.png
├── StoreLogo.png
└── Wide310x150Logo.scale-200.png
├── DTDL.cs
├── LICENSE
├── MainPage.xaml
├── MainPage.xaml.cs
├── Models
├── DtdlModel.cs
├── NodeModel.cs
└── NodeReferenceModel.cs
├── OPCUA2DTDL.csproj
├── OPCUA2DTDL.gif
├── OPCUA2DTDL.sln
├── Opc.Ua.SampleClient.Config.xml
├── OpcUaClient.cs
├── Package.appxmanifest
├── Properties
├── AssemblyInfo.cs
└── Default.rd.xml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Aa][Rr][Mm]/
27 | [Aa][Rr][Mm]64/
28 | bld/
29 | [Bb]in/
30 | [Oo]bj/
31 | [Ll]og/
32 | [Ll]ogs/
33 |
34 | # Visual Studio 2015/2017 cache/options directory
35 | .vs/
36 | # Uncomment if you have tasks that create the project's static files in wwwroot
37 | #wwwroot/
38 |
39 | # Visual Studio 2017 auto generated files
40 | Generated\ Files/
41 |
42 | # MSTest test Results
43 | [Tt]est[Rr]esult*/
44 | [Bb]uild[Ll]og.*
45 |
46 | # NUnit
47 | *.VisualState.xml
48 | TestResult.xml
49 | nunit-*.xml
50 |
51 | # Build Results of an ATL Project
52 | [Dd]ebugPS/
53 | [Rr]eleasePS/
54 | dlldata.c
55 |
56 | # Benchmark Results
57 | BenchmarkDotNet.Artifacts/
58 |
59 | # .NET Core
60 | project.lock.json
61 | project.fragment.lock.json
62 | artifacts/
63 |
64 | # StyleCop
65 | StyleCopReport.xml
66 |
67 | # Files built by Visual Studio
68 | *_i.c
69 | *_p.c
70 | *_h.h
71 | *.ilk
72 | *.meta
73 | *.obj
74 | *.iobj
75 | *.pch
76 | *.pdb
77 | *.ipdb
78 | *.pgc
79 | *.pgd
80 | *.rsp
81 | *.sbr
82 | *.tlb
83 | *.tli
84 | *.tlh
85 | *.tmp
86 | *.tmp_proj
87 | *_wpftmp.csproj
88 | *.log
89 | *.vspscc
90 | *.vssscc
91 | .builds
92 | *.pidb
93 | *.svclog
94 | *.scc
95 |
96 | # Chutzpah Test files
97 | _Chutzpah*
98 |
99 | # Visual C++ cache files
100 | ipch/
101 | *.aps
102 | *.ncb
103 | *.opendb
104 | *.opensdf
105 | *.sdf
106 | *.cachefile
107 | *.VC.db
108 | *.VC.VC.opendb
109 |
110 | # Visual Studio profiler
111 | *.psess
112 | *.vsp
113 | *.vspx
114 | *.sap
115 |
116 | # Visual Studio Trace Files
117 | *.e2e
118 |
119 | # TFS 2012 Local Workspace
120 | $tf/
121 |
122 | # Guidance Automation Toolkit
123 | *.gpState
124 |
125 | # ReSharper is a .NET coding add-in
126 | _ReSharper*/
127 | *.[Rr]e[Ss]harper
128 | *.DotSettings.user
129 |
130 | # TeamCity is a build add-in
131 | _TeamCity*
132 |
133 | # DotCover is a Code Coverage Tool
134 | *.dotCover
135 |
136 | # AxoCover is a Code Coverage Tool
137 | .axoCover/*
138 | !.axoCover/settings.json
139 |
140 | # Visual Studio code coverage results
141 | *.coverage
142 | *.coveragexml
143 |
144 | # NCrunch
145 | _NCrunch_*
146 | .*crunch*.local.xml
147 | nCrunchTemp_*
148 |
149 | # MightyMoose
150 | *.mm.*
151 | AutoTest.Net/
152 |
153 | # Web workbench (sass)
154 | .sass-cache/
155 |
156 | # Installshield output folder
157 | [Ee]xpress/
158 |
159 | # DocProject is a documentation generator add-in
160 | DocProject/buildhelp/
161 | DocProject/Help/*.HxT
162 | DocProject/Help/*.HxC
163 | DocProject/Help/*.hhc
164 | DocProject/Help/*.hhk
165 | DocProject/Help/*.hhp
166 | DocProject/Help/Html2
167 | DocProject/Help/html
168 |
169 | # Click-Once directory
170 | publish/
171 |
172 | # Publish Web Output
173 | *.[Pp]ublish.xml
174 | *.azurePubxml
175 | # Note: Comment the next line if you want to checkin your web deploy settings,
176 | # but database connection strings (with potential passwords) will be unencrypted
177 | *.pubxml
178 | *.publishproj
179 |
180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
181 | # checkin your Azure Web App publish settings, but sensitive information contained
182 | # in these scripts will be unencrypted
183 | PublishScripts/
184 |
185 | # NuGet Packages
186 | *.nupkg
187 | # NuGet Symbol Packages
188 | *.snupkg
189 | # The packages folder can be ignored because of Package Restore
190 | **/[Pp]ackages/*
191 | # except build/, which is used as an MSBuild target.
192 | !**/[Pp]ackages/build/
193 | # Uncomment if necessary however generally it will be regenerated when needed
194 | #!**/[Pp]ackages/repositories.config
195 | # NuGet v3's project.json files produces more ignorable files
196 | *.nuget.props
197 | *.nuget.targets
198 |
199 | # Microsoft Azure Build Output
200 | csx/
201 | *.build.csdef
202 |
203 | # Microsoft Azure Emulator
204 | ecf/
205 | rcf/
206 |
207 | # Windows Store app package directories and files
208 | AppPackages/
209 | BundleArtifacts/
210 | Package.StoreAssociation.xml
211 | _pkginfo.txt
212 | *.appx
213 | *.appxbundle
214 | *.appxupload
215 |
216 | # Visual Studio cache files
217 | # files ending in .cache can be ignored
218 | *.[Cc]ache
219 | # but keep track of directories ending in .cache
220 | !?*.[Cc]ache/
221 |
222 | # Others
223 | ClientBin/
224 | ~$*
225 | *~
226 | *.dbmdl
227 | *.dbproj.schemaview
228 | *.jfm
229 | *.pfx
230 | *.publishsettings
231 | orleans.codegen.cs
232 |
233 | # Including strong name files can present a security risk
234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
235 | #*.snk
236 |
237 | # Since there are multiple workflows, uncomment next line to ignore bower_components
238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
239 | #bower_components/
240 |
241 | # RIA/Silverlight projects
242 | Generated_Code/
243 |
244 | # Backup & report files from converting an old project file
245 | # to a newer Visual Studio version. Backup files are not needed,
246 | # because we have git ;-)
247 | _UpgradeReport_Files/
248 | Backup*/
249 | UpgradeLog*.XML
250 | UpgradeLog*.htm
251 | ServiceFabricBackup/
252 | *.rptproj.bak
253 |
254 | # SQL Server files
255 | *.mdf
256 | *.ldf
257 | *.ndf
258 |
259 | # Business Intelligence projects
260 | *.rdl.data
261 | *.bim.layout
262 | *.bim_*.settings
263 | *.rptproj.rsuser
264 | *- [Bb]ackup.rdl
265 | *- [Bb]ackup ([0-9]).rdl
266 | *- [Bb]ackup ([0-9][0-9]).rdl
267 |
268 | # Microsoft Fakes
269 | FakesAssemblies/
270 |
271 | # GhostDoc plugin setting file
272 | *.GhostDoc.xml
273 |
274 | # Node.js Tools for Visual Studio
275 | .ntvs_analysis.dat
276 | node_modules/
277 |
278 | # Visual Studio 6 build log
279 | *.plg
280 |
281 | # Visual Studio 6 workspace options file
282 | *.opt
283 |
284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
285 | *.vbw
286 |
287 | # Visual Studio LightSwitch build output
288 | **/*.HTMLClient/GeneratedArtifacts
289 | **/*.DesktopClient/GeneratedArtifacts
290 | **/*.DesktopClient/ModelManifest.xml
291 | **/*.Server/GeneratedArtifacts
292 | **/*.Server/ModelManifest.xml
293 | _Pvt_Extensions
294 |
295 | # Paket dependency manager
296 | .paket/paket.exe
297 | paket-files/
298 |
299 | # FAKE - F# Make
300 | .fake/
301 |
302 | # CodeRush personal settings
303 | .cr/personal
304 |
305 | # Python Tools for Visual Studio (PTVS)
306 | __pycache__/
307 | *.pyc
308 |
309 | # Cake - Uncomment if you are using it
310 | # tools/**
311 | # !tools/packages.config
312 |
313 | # Tabs Studio
314 | *.tss
315 |
316 | # Telerik's JustMock configuration file
317 | *.jmconfig
318 |
319 | # BizTalk build output
320 | *.btp.cs
321 | *.btm.cs
322 | *.odx.cs
323 | *.xsd.cs
324 |
325 | # OpenCover UI analysis results
326 | OpenCover/
327 |
328 | # Azure Stream Analytics local run output
329 | ASALocalRun/
330 |
331 | # MSBuild Binary and Structured Log
332 | *.binlog
333 |
334 | # NVidia Nsight GPU debugger configuration file
335 | *.nvuser
336 |
337 | # MFractors (Xamarin productivity tool) working folder
338 | .mfractor/
339 |
340 | # Local History for Visual Studio
341 | .localhistory/
342 |
343 | # BeatPulse healthcheck temp database
344 | healthchecksdb
345 |
346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
347 | MigrationBackup/
348 |
349 | # Ionide (cross platform F# VS Code tools) working folder
350 | .ionide/
351 |
--------------------------------------------------------------------------------
/App.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Runtime.InteropServices.WindowsRuntime;
6 | using Windows.ApplicationModel;
7 | using Windows.ApplicationModel.Activation;
8 | using Windows.Foundation;
9 | using Windows.Foundation.Collections;
10 | using Windows.UI.Xaml;
11 | using Windows.UI.Xaml.Controls;
12 | using Windows.UI.Xaml.Controls.Primitives;
13 | using Windows.UI.Xaml.Data;
14 | using Windows.UI.Xaml.Input;
15 | using Windows.UI.Xaml.Media;
16 | using Windows.UI.Xaml.Navigation;
17 |
18 | namespace OPCUA2DTDL
19 | {
20 | ///
21 | /// Provides application-specific behavior to supplement the default Application class.
22 | ///
23 | sealed partial class App : Application
24 | {
25 | ///
26 | /// Initializes the singleton application object. This is the first line of authored code
27 | /// executed, and as such is the logical equivalent of main() or WinMain().
28 | ///
29 | public App()
30 | {
31 | this.InitializeComponent();
32 | this.Suspending += OnSuspending;
33 | }
34 |
35 | ///
36 | /// Invoked when the application is launched normally by the end user. Other entry points
37 | /// will be used such as when the application is launched to open a specific file.
38 | ///
39 | /// Details about the launch request and process.
40 | protected override void OnLaunched(LaunchActivatedEventArgs e)
41 | {
42 | Frame rootFrame = Window.Current.Content as Frame;
43 |
44 | // Do not repeat app initialization when the Window already has content,
45 | // just ensure that the window is active
46 | if (rootFrame == null)
47 | {
48 | // Create a Frame to act as the navigation context and navigate to the first page
49 | rootFrame = new Frame();
50 |
51 | rootFrame.NavigationFailed += OnNavigationFailed;
52 |
53 | if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
54 | {
55 | //TODO: Load state from previously suspended application
56 | }
57 |
58 | // Place the frame in the current Window
59 | Window.Current.Content = rootFrame;
60 | }
61 |
62 | if (e.PrelaunchActivated == false)
63 | {
64 | if (rootFrame.Content == null)
65 | {
66 | // When the navigation stack isn't restored navigate to the first page,
67 | // configuring the new page by passing required information as a navigation
68 | // parameter
69 | rootFrame.Navigate(typeof(MainPage), e.Arguments);
70 | }
71 | // Ensure the current window is active
72 | Window.Current.Activate();
73 | }
74 | }
75 |
76 | ///
77 | /// Invoked when Navigation to a certain page fails
78 | ///
79 | /// The Frame which failed navigation
80 | /// Details about the navigation failure
81 | void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
82 | {
83 | throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
84 | }
85 |
86 | ///
87 | /// Invoked when application execution is being suspended. Application state is saved
88 | /// without knowing whether the application will be terminated or resumed with the contents
89 | /// of memory still intact.
90 | ///
91 | /// The source of the suspend request.
92 | /// Details about the suspend request.
93 | private void OnSuspending(object sender, SuspendingEventArgs e)
94 | {
95 | var deferral = e.SuspendingOperation.GetDeferral();
96 | //TODO: Save application state and stop any background activity
97 | deferral.Complete();
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/Assets/LockScreenLogo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khilscher/OPCUA2DTDL/f0b101319087ca82f851292fabeb537169a0115b/Assets/LockScreenLogo.scale-200.png
--------------------------------------------------------------------------------
/Assets/SplashScreen.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khilscher/OPCUA2DTDL/f0b101319087ca82f851292fabeb537169a0115b/Assets/SplashScreen.scale-200.png
--------------------------------------------------------------------------------
/Assets/Square150x150Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khilscher/OPCUA2DTDL/f0b101319087ca82f851292fabeb537169a0115b/Assets/Square150x150Logo.scale-200.png
--------------------------------------------------------------------------------
/Assets/Square44x44Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khilscher/OPCUA2DTDL/f0b101319087ca82f851292fabeb537169a0115b/Assets/Square44x44Logo.scale-200.png
--------------------------------------------------------------------------------
/Assets/Square44x44Logo.targetsize-24_altform-unplated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khilscher/OPCUA2DTDL/f0b101319087ca82f851292fabeb537169a0115b/Assets/Square44x44Logo.targetsize-24_altform-unplated.png
--------------------------------------------------------------------------------
/Assets/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khilscher/OPCUA2DTDL/f0b101319087ca82f851292fabeb537169a0115b/Assets/StoreLogo.png
--------------------------------------------------------------------------------
/Assets/Wide310x150Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khilscher/OPCUA2DTDL/f0b101319087ca82f851292fabeb537169a0115b/Assets/Wide310x150Logo.scale-200.png
--------------------------------------------------------------------------------
/DTDL.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using OPCUA2DTDL.Models;
3 |
4 | namespace OPCUA2DTDL
5 | {
6 | class DTDL
7 | {
8 |
9 | private static Dictionary _map = new Dictionary(); // Primitive schema map
10 |
11 | ///
12 | /// Generates a DTDL Interface from an OPC UA node
13 | ///
14 | ///
15 | ///
16 | ///
17 | ///
18 | public static DtdlInterface GenerateDTDL(OpcUaNode node, bool expandedDtdlMode, string dtmiPrefix)
19 | {
20 |
21 | // Create schema mapping table
22 | CreateSchemaMap();
23 |
24 | string name;
25 |
26 | // If creating an Interface using an OPC UA Object instead of an ObjectType,
27 | // then lookup the TypeDefinition.
28 | if (node.NodeClass == "Object")
29 | {
30 |
31 | name = OpcUaClient.GetTypeDefinition(node.TypeDefinition);
32 |
33 | }
34 | else
35 | {
36 |
37 | name = node.DisplayName;
38 |
39 | }
40 |
41 | // Create Interface
42 | DtdlInterface dtdlInterface = new DtdlInterface
43 | {
44 | Id = dtmiPrefix + name + ";1",
45 | Type = "Interface",
46 | DisplayName = name,
47 | Contents = new List(),
48 | Comment = $"Derived from {node.NodeId}"
49 | };
50 |
51 | // Generate collapsed mode DTDL
52 | if (expandedDtdlMode == false)
53 | {
54 |
55 | if (node.Children.Count > 0)
56 | {
57 |
58 | foreach (var child in node.Children)
59 | {
60 |
61 | // i=68 (PropertyType) maps to DTDL property
62 | if (child.TypeDefinition == "i=68")
63 | {
64 |
65 | DtdlContents dtdlProperty = new DtdlContents
66 | {
67 | Type = "Property",
68 | Name = child.DisplayName,
69 | Schema = GetDtdlDataType(child.DataType)
70 | };
71 |
72 | dtdlInterface.Contents.Add(dtdlProperty);
73 |
74 | }
75 |
76 | // i=63 (BaseDataVariableType) maps to DTDL telemetry
77 | if (child.TypeDefinition == "i=63")
78 | {
79 |
80 | DtdlContents dtdlTelemetry = new DtdlContents
81 | {
82 | Type = "Telemetry",
83 | Name = child.DisplayName,
84 | Schema = GetDtdlDataType(child.DataType)
85 | };
86 |
87 | dtdlInterface.Contents.Add(dtdlTelemetry);
88 |
89 | }
90 |
91 | // NodeClass == Method maps to DTDL command
92 | if (child.NodeClass == "Method")
93 | {
94 |
95 | DtdlContents dtdlCommand = new DtdlContents
96 | {
97 | Type = "Command",
98 | Name = child.DisplayName
99 | };
100 |
101 | dtdlInterface.Contents.Add(dtdlCommand);
102 |
103 | }
104 |
105 | }
106 |
107 | }
108 |
109 | if (node.Children.Count == 0)
110 | {
111 |
112 | // i=68 (PropertyType) maps to DTDL property
113 | if (node.TypeDefinition == "i=68")
114 | {
115 |
116 | DtdlContents dtdlProperty = new DtdlContents
117 | {
118 | Type = "Property",
119 | Name = node.DisplayName,
120 | Schema = GetDtdlDataType(node.DataType)
121 | };
122 |
123 | dtdlInterface.Contents.Add(dtdlProperty);
124 |
125 | }
126 |
127 | // i=63 (BaseDataVariableType) maps to DTDL telemetry
128 | if (node.TypeDefinition == "i=63")
129 | {
130 | DtdlContents dtdlTelemetry = new DtdlContents
131 | {
132 | Type = "Telemetry",
133 | Name = node.DisplayName,
134 | Schema = GetDtdlDataType(node.DataType)
135 | };
136 |
137 | dtdlInterface.Contents.Add(dtdlTelemetry);
138 | }
139 |
140 | // NodeClass == Method maps to DTDL command
141 | if (node.NodeClass == "Method")
142 | {
143 |
144 | DtdlContents dtdlCommand = new DtdlContents
145 | {
146 | Type = "Command",
147 | Name = node.DisplayName
148 | };
149 |
150 | dtdlInterface.Contents.Add(dtdlCommand);
151 |
152 | }
153 |
154 | }
155 | }
156 |
157 | // Generate expanded mode DTDL
158 | if (expandedDtdlMode == true)
159 | {
160 |
161 | // TODO Remove hard coded ReferenceTypeId's. Support all of them.
162 | // Refactor and combine with the above
163 | if (node.Children.Count > 0)
164 | {
165 |
166 | foreach(var child in node.Children)
167 | {
168 |
169 | if (child.ReferenceTypeId == "i=46")
170 | {
171 |
172 | DtdlContents dtdlProperty = new DtdlContents
173 | {
174 | Type = "Relationship",
175 | Name = "HasProperty",
176 | Target = dtmiPrefix + child.DisplayName + ";1"
177 | };
178 |
179 | dtdlInterface.Contents.Add(dtdlProperty);
180 |
181 | }
182 |
183 | if (child.ReferenceTypeId == "i=47")
184 | {
185 |
186 | DtdlContents dtdlTelemetry = new DtdlContents
187 | {
188 | Type = "Relationship",
189 | Name = "HasComponent",
190 | Target = dtmiPrefix + child.DisplayName + ";1"
191 | };
192 |
193 | dtdlInterface.Contents.Add(dtdlTelemetry);
194 |
195 | }
196 | }
197 | }
198 |
199 | if(node.Children.Count == 0)
200 | {
201 |
202 | // i=68 (PropertyType) maps to DTDL property
203 | if (node.TypeDefinition == "i=68")
204 | {
205 |
206 | DtdlContents dtdlProperty = new DtdlContents
207 | {
208 | Type = "Property",
209 | Name = node.DisplayName,
210 | Schema = GetDtdlDataType(node.DataType)
211 | };
212 |
213 | dtdlInterface.Contents.Add(dtdlProperty);
214 |
215 | }
216 |
217 | // i=63 (BaseDataVariableType) maps to DTDL telemetry
218 | if (node.TypeDefinition == "i=63")
219 | {
220 |
221 | DtdlContents dtdlTelemetry = new DtdlContents
222 | {
223 | Type = "Telemetry",
224 | Name = node.DisplayName,
225 | Schema = GetDtdlDataType(node.DataType)
226 | };
227 |
228 | dtdlInterface.Contents.Add(dtdlTelemetry);
229 |
230 | }
231 |
232 | // NodeClass == Method maps to DTDL command
233 | if (node.NodeClass == "Method")
234 | {
235 |
236 | DtdlContents dtdlCommand = new DtdlContents
237 | {
238 | Type = "Command",
239 | Name = node.DisplayName
240 | };
241 |
242 | dtdlInterface.Contents.Add(dtdlCommand);
243 |
244 | }
245 | }
246 | }
247 |
248 | _map.Clear();
249 |
250 | return dtdlInterface;
251 |
252 | }
253 |
254 | ///
255 | /// Generates a sample DTDL interface
256 | ///
257 | /// DTMI prefix
258 | /// DtdlInterface
259 | public static DtdlInterface GenerateSampleDTDL(string dtmiPrefix)
260 | {
261 |
262 | // Create Interface
263 | DtdlInterface dtdlInterface = new DtdlInterface
264 | {
265 | Id = dtmiPrefix + "sample_interface;1",
266 | Type = "Interface",
267 | DisplayName = "",
268 | Contents = new List(),
269 | Comment = ""
270 | };
271 |
272 | DtdlContents relationship = new DtdlContents
273 | {
274 | Type = "Relationship",
275 | Name = "Controls"
276 | };
277 |
278 | dtdlInterface.Contents.Add(relationship);
279 |
280 | DtdlContents telemetry = new DtdlContents
281 | {
282 | Type = "Telemetry",
283 | Name = "temp",
284 | Schema = "double"
285 | };
286 |
287 | dtdlInterface.Contents.Add(telemetry);
288 |
289 | DtdlContents property = new DtdlContents
290 | {
291 | Type = "Property",
292 | Name = "firmware_version",
293 | Schema = "integer"
294 | };
295 |
296 | dtdlInterface.Contents.Add(property);
297 |
298 | return dtdlInterface;
299 |
300 | }
301 |
302 | ///
303 | /// Create a dictionary to map OPC UA datatypes to DTDL primitives
304 | ///
305 | private static void CreateSchemaMap()
306 | {
307 |
308 | // Customize the schema mappings as needed
309 | // https://reference.opcfoundation.org/v104/Core/docs/Part6/5.1.2/
310 | // -> https://github.com/Azure/opendigitaltwins-dtdl/blob/master/DTDL/v2/dtdlv2.md#primitive-schemas
311 |
312 | _map.Add("Boolean", "boolean");
313 | //_map.Add("", "date");
314 | _map.Add("DateTime", "dateTime");
315 | _map.Add("Double", "double");
316 | //_map.Add("", "duration");
317 | _map.Add("Float", "float");
318 | _map.Add("SByte", "integer");
319 | _map.Add("Byte", "integer");
320 | _map.Add("Integer", "integer");
321 | _map.Add("Int16", "integer");
322 | _map.Add("UInt16", "integer");
323 | _map.Add("Int32", "integer");
324 | _map.Add("UInt32", "integer");
325 | _map.Add("Int64", "long");
326 | _map.Add("UInt64", "long");
327 | _map.Add("String", "string");
328 | //_map.Add("", "time");
329 |
330 | }
331 |
332 | ///
333 | /// Gets a DTDL primitive data type from a OPC UA data type
334 | ///
335 | /// OPC UA data type e.g. Int64
336 | /// DTDL data type
337 | public static string GetDtdlDataType(string id)
338 | {
339 |
340 | try
341 | {
342 |
343 | return _map[id];
344 |
345 | }
346 | catch
347 | {
348 |
349 | return $"No primitive DTDL datatype for {id}.";
350 |
351 | }
352 |
353 | }
354 | }
355 | }
356 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Kevin Hilscher
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MainPage.xaml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
40 |
41 |
42 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
71 |
72 |
73 |
74 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/MainPage.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading;
4 | using Windows.UI.Xaml;
5 | using Windows.UI.Xaml.Controls;
6 | using Windows.UI.Xaml.Media;
7 | using Opc.Ua.Client;
8 | using Windows.UI.Core;
9 | using muxc = Microsoft.UI.Xaml.Controls;
10 | using System.Threading.Tasks;
11 | using OPCUA2DTDL.Models;
12 | using Newtonsoft.Json;
13 | using Microsoft.Azure.DigitalTwins.Parser;
14 | using System.Net.Http;
15 | using System.Net.Http.Headers;
16 | using System.Net;
17 | using System.Collections.Specialized;
18 | using Microsoft.IdentityModel.Clients.ActiveDirectory; // ADAL
19 | using System.Text;
20 | using Newtonsoft.Json.Linq;
21 | using System.Linq;
22 | using Windows.UI.ViewManagement;
23 | using Windows.Foundation;
24 |
25 |
26 | // https://docs.microsoft.com/en-us/windows/uwp/design/layout/
27 |
28 | namespace OPCUA2DTDL
29 | {
30 | ///
31 | /// An empty page that can be used on its own or navigated to within a Frame.
32 | ///
33 | public sealed partial class MainPage : Page
34 | {
35 |
36 | // Do not edit
37 | private static OpcUaClient _opcUaClient;
38 | private OpcUaNodeList _dataSource = new OpcUaNodeList();
39 | private static List _interfaceList = new List();
40 | private static string _opcUaEndpointURL;
41 | private OpcUaNodeList _children = new OpcUaNodeList();
42 | private bool _isExpandedDtdlMode = false;
43 | private bool _autoAccept = true;
44 | private OpcUaNode _selectedNode;
45 | private string _authority = "https://login.microsoftonline.com";
46 | private string _resource = "0b07f429-9f4b-4714-9392-cc5e8e80c8b0"; // ADT resource id. Do not change.
47 | private static HttpClient _httpClient = new HttpClient();
48 |
49 | // Edit these as needed
50 | private const string _appName = "OPCUA2DTDL";
51 | private static string _dtmiPrefix = "dtmi:com:example:";
52 |
53 | // Fill in your ADT instance URL
54 | private string _adtInstanceUrl = "https://.digitaltwins.azure.net";
55 |
56 | // Fill in with your AAD app registration.
57 | // See https://docs.microsoft.com/en-us/azure/digital-twins/how-to-create-app-registration
58 | // Ensure your app has "Azure Digital Twins Data Owner" permissions to your ADT instance
59 | private string _tenantId = "";
60 | private string _clientId = "";
61 | private string _secret = "";
62 |
63 |
64 | public MainPage()
65 | {
66 |
67 | this.InitializeComponent();
68 |
69 | // Set preferred window size
70 | ApplicationView.PreferredLaunchViewSize = new Size(1400, 800);
71 | ApplicationView.PreferredLaunchWindowingMode = ApplicationViewWindowingMode.PreferredLaunchViewSize;
72 |
73 | }
74 |
75 | private async void btnConnect_Click(object sender, RoutedEventArgs e)
76 | {
77 | btnConnect.IsEnabled = false;
78 | ProgressRing.IsActive = true;
79 | this.OpcUaNodeTree.RootNodes.Clear();
80 | _opcUaEndpointURL = txtBoxOpcServer.Text;
81 | _dataSource.Clear();
82 | _children.Clear();
83 |
84 | try
85 | {
86 | await Task.Run(() => ConnectAndBrowse());
87 |
88 | // Hack to get the treeview to refresh.
89 | // ObservableCollection with built-in INotifyCollectionChanged doesn't appear to work.
90 | this.OpcUaNodeTree.ItemsSource = _dataSource;
91 |
92 | NotifyUser("Finished browsing nodes");
93 | }
94 | catch (Exception ex)
95 | {
96 | NotifyUser(ex.Message);
97 | }
98 |
99 | btnConnect.IsEnabled = true;
100 | ProgressRing.IsActive = false;
101 |
102 | }
103 |
104 | private async Task ConnectAndBrowse()
105 | {
106 |
107 | int stopTimeout = Timeout.Infinite;
108 |
109 | _opcUaClient = new OpcUaClient(_opcUaEndpointURL, _autoAccept, stopTimeout, _appName);
110 |
111 | NotifyUser("Connecting...");
112 |
113 | Session opcsession = await _opcUaClient.Connect();
114 |
115 | if (opcsession.Connected)
116 | {
117 |
118 | NotifyUser("Connected");
119 |
120 | NotifyUser("Browsing nodes...");
121 |
122 | // https://docs.microsoft.com/en-us/windows/winui/api/microsoft.ui.xaml.controls.treeview?view=winui-2.5
123 | _dataSource = await _opcUaClient.Browse();
124 |
125 | }
126 |
127 | }
128 |
129 | private void OpcUaNodeTree_ItemInvoked(muxc.TreeView sender, muxc.TreeViewItemInvokedEventArgs args)
130 | {
131 |
132 | _selectedNode = (OpcUaNode)args.InvokedItem;
133 |
134 | // Display the node's properties in the console window when user clicks on the node
135 | NotifyUser("**********************************************************************************************************************");
136 | NotifyUser($"BrowseName: {_selectedNode.BrowseName}");
137 | NotifyUser($"DisplayName: {_selectedNode.DisplayName}");
138 | NotifyUser($"NodeId: {_selectedNode.NodeId}");
139 | NotifyUser($"NodeClass: {_selectedNode.NodeClass}");
140 | NotifyUser($"DataType: {_selectedNode.DataType}");
141 | NotifyUser($"Child Count: {_selectedNode.Children.Count}");
142 | NotifyUser($"ReferenceTypeId: {_selectedNode.ReferenceTypeId} Name: {OpcUaClient.GetTypeDefinition(_selectedNode.ReferenceTypeId)}");
143 | NotifyUser($"TypeDefinitionId: {_selectedNode.TypeDefinition} Name: {OpcUaClient.GetTypeDefinition(_selectedNode.TypeDefinition)}");
144 | NotifyUser("**********************************************************************************************************************");
145 |
146 | }
147 |
148 | private bool IsVariableOrMethod(OpcUaNode node)
149 | {
150 |
151 | if (node.NodeClass == "Variable" || node.NodeClass == "Method")
152 | {
153 |
154 | return true;
155 |
156 | }
157 | else
158 | {
159 |
160 | return false;
161 |
162 | }
163 |
164 | }
165 |
166 | public void NotifyUser(string strMessage)
167 | {
168 | // If called from the UI thread, then update immediately.
169 | // Otherwise, schedule a task on the UI thread to perform the update.
170 | if (Dispatcher.HasThreadAccess)
171 | {
172 |
173 | UpdateStatus(strMessage);
174 |
175 | }
176 | else
177 | {
178 |
179 | var task = Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => UpdateStatus(strMessage));
180 |
181 | }
182 | }
183 |
184 | private void UpdateStatus(string strMessage)
185 | {
186 |
187 | txtBoxConsole.Text += strMessage + "\n";
188 |
189 | }
190 |
191 | // Hack to allow status textbox to autoscroll to bottom
192 | private void txtBoxStatus_TextChanged(object sender, TextChangedEventArgs e)
193 | {
194 |
195 | var grid = (Grid)VisualTreeHelper.GetChild(txtBoxConsole, 0);
196 |
197 | for (var i = 0; i <= VisualTreeHelper.GetChildrenCount(grid) - 1; i++)
198 | {
199 |
200 | object obj = VisualTreeHelper.GetChild(grid, i);
201 |
202 | if (!(obj is ScrollViewer)) continue;
203 |
204 | ((ScrollViewer)obj).ChangeView(0.0f, ((ScrollViewer)obj).ExtentHeight, 1.0f);
205 |
206 | break;
207 |
208 | }
209 |
210 | }
211 |
212 | private void btnValidate_Click(object sender, RoutedEventArgs e)
213 | {
214 |
215 | NotifyUser("Validating DTDL...");
216 |
217 | try
218 | {
219 |
220 | string json = txtBoxDTDL.Text.ToString();
221 |
222 | ModelParser modelParser = new ModelParser();
223 |
224 | List model = new List();
225 |
226 | model.Add(json);
227 |
228 | IReadOnlyDictionary parseTask = modelParser.ParseAsync(model).GetAwaiter().GetResult();
229 |
230 | NotifyUser($"Validation passed");
231 |
232 | }
233 | catch (ParsingException pe)
234 | {
235 |
236 | NotifyUser($"Validation error(s)");
237 |
238 | int errCount = 1;
239 |
240 | foreach (ParsingError err in pe.Errors)
241 | {
242 |
243 | NotifyUser($"Error {errCount}: {err.Message}");
244 | NotifyUser($"Primary ID: {err.PrimaryID}");
245 | NotifyUser($"Secondary ID: {err.SecondaryID}");
246 | NotifyUser($"Property: {err.Property}");
247 | errCount++;
248 |
249 | }
250 |
251 | }
252 | catch (Exception ex)
253 | {
254 |
255 | NotifyUser($"{ex.Message}");
256 |
257 | }
258 |
259 | }
260 |
261 | private void btnClear_Click(object sender, RoutedEventArgs e)
262 | {
263 |
264 | txtBoxDTDL.Text = "";
265 | _interfaceList.Clear();
266 |
267 | }
268 |
269 | private void btnAddSampleInterface_Click(object sender, RoutedEventArgs e)
270 | {
271 |
272 | // If text box contains json, load json from text box into list
273 | if (txtBoxDTDL.Text.Length != 0)
274 | {
275 |
276 | _interfaceList.AddRange(JsonConvert.DeserializeObject>(txtBoxDTDL.Text));
277 |
278 | }
279 |
280 | // Generate DTDL for selected tree node and add to list
281 | var dtdl = DTDL.GenerateSampleDTDL(_dtmiPrefix);
282 | _interfaceList.Add(dtdl);
283 |
284 | // Display list in text box
285 | txtBoxDTDL.Text = JsonConvert.SerializeObject(_interfaceList, Formatting.Indented);
286 |
287 | // Clear list
288 | _interfaceList.Clear();
289 |
290 | }
291 |
292 | private void btnConvertNodeToDtdl_Click(object sender, RoutedEventArgs e)
293 | {
294 |
295 | if (_selectedNode != null)
296 | {
297 |
298 | if (IsVariableOrMethod(_selectedNode) && _isExpandedDtdlMode == false)
299 | {
300 |
301 | NotifyUser("Select this node's parent or enable expanded mode to convert individual Properties, Variables, or Methods to DTDL Interfaces.");
302 |
303 | return;
304 |
305 | }
306 |
307 | // If text box contains json, load json from text box into list
308 | if (txtBoxDTDL.Text.Length != 0)
309 | {
310 |
311 | _interfaceList.AddRange(JsonConvert.DeserializeObject>(txtBoxDTDL.Text));
312 |
313 | }
314 |
315 | // Generate the DTDL for selected node
316 | DtdlInterface dtdl = DTDL.GenerateDTDL(_selectedNode, _isExpandedDtdlMode, _dtmiPrefix);
317 |
318 | // Add DTDL to the list of Interfaces
319 | _interfaceList.Add(dtdl);
320 |
321 | // Display list in text box
322 | txtBoxDTDL.Text = JsonConvert.SerializeObject(_interfaceList, Formatting.Indented);
323 |
324 | // Clear list
325 | _interfaceList.Clear();
326 |
327 | }
328 |
329 | }
330 |
331 | private void btnCollapsedExpandedToggle_Click(object sender, RoutedEventArgs e)
332 | {
333 |
334 | if (btnCollapsedExpandedToggle.IsChecked == true)
335 | {
336 |
337 | btnCollapsedExpandedToggle.Label = "Expanded DTDL";
338 | _isExpandedDtdlMode = true;
339 |
340 | }
341 | else
342 | {
343 |
344 | btnCollapsedExpandedToggle.Label = "Collapsed DTDL";
345 | _isExpandedDtdlMode = false;
346 |
347 | }
348 |
349 | }
350 |
351 | private async void btnUploadToAdt_Click(object sender, RoutedEventArgs e)
352 | {
353 |
354 | try
355 | {
356 |
357 | // ADAL - TODO Replace with MSAL PublicClientApplication
358 | var credentials = new ClientCredential(_clientId, _secret);
359 | var authContext = new AuthenticationContext($"{_authority}/{_tenantId}");
360 | var result = await authContext.AcquireTokenAsync(_resource, credentials);
361 |
362 | _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
363 |
364 | string content = txtBoxDTDL.Text.ToString();
365 | var buffer = Encoding.UTF8.GetBytes(content);
366 | var byteContent = new ByteArrayContent(buffer);
367 | byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
368 |
369 | var response = await _httpClient.PostAsync($"{_adtInstanceUrl}/models?api-version=2020-10-31", byteContent);
370 |
371 | if (response.IsSuccessStatusCode)
372 | {
373 | NotifyUser($"Model upload successful.");
374 | }
375 | else
376 | {
377 | NotifyUser($"Model upload error: {response.StatusCode}");
378 | }
379 | }
380 | catch(Exception ex)
381 | {
382 |
383 | NotifyUser($"{ex.Message}");
384 |
385 | }
386 |
387 | }
388 |
389 | private async void btnDownloadFromAdt_Click(object sender, RoutedEventArgs e)
390 | {
391 |
392 | try
393 | {
394 | // ADAL - TODO Replace with MSAL PublicClientApplication
395 | var credentials = new ClientCredential(_clientId, _secret);
396 | var authContext = new AuthenticationContext($"{_authority}/{_tenantId}");
397 | var result = await authContext.AcquireTokenAsync(_resource, credentials);
398 |
399 | _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
400 |
401 | string json = await _httpClient.GetStringAsync($"{_adtInstanceUrl}/models?includeModelDefinition=true&api-version=2020-10-31");
402 |
403 | if(!String.IsNullOrEmpty(json))
404 | {
405 |
406 | JObject jObject = JObject.Parse(json);
407 |
408 | // Get JSON result objects into a list
409 | IList results = jObject["value"].Children().ToList();
410 |
411 | IList resultsList = new List();
412 |
413 | foreach (JToken r in results)
414 | {
415 | JObject inner = r["model"].Value();
416 | _interfaceList.Add(inner.ToObject());
417 | }
418 |
419 | txtBoxDTDL.Text = JsonConvert.SerializeObject(_interfaceList, Formatting.Indented);
420 |
421 | // Clear list
422 | _interfaceList.Clear();
423 |
424 | NotifyUser($"Model download complete.");
425 |
426 | }
427 |
428 | }
429 | catch (Exception ex)
430 | {
431 |
432 | NotifyUser($"{ex.Message}");
433 |
434 | }
435 |
436 | }
437 |
438 | }
439 |
440 | }
441 |
--------------------------------------------------------------------------------
/Models/DtdlModel.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Newtonsoft.Json;
3 |
4 | namespace OPCUA2DTDL.Models
5 | {
6 |
7 | public class DtdlContents
8 | {
9 | [JsonProperty("@type")]
10 | public string Type { get; set; }
11 |
12 | [JsonProperty("name")]
13 | public string Name { get; set; }
14 |
15 | [JsonProperty("target", NullValueHandling = NullValueHandling.Ignore)]
16 | public string Target { get; set; }
17 |
18 | [JsonProperty("comment", NullValueHandling = NullValueHandling.Ignore)]
19 | public string Comment { get; set; }
20 |
21 | [JsonProperty("displayName", NullValueHandling = NullValueHandling.Ignore)]
22 | public string DisplayName { get; set; }
23 |
24 | [JsonProperty("schema", NullValueHandling = NullValueHandling.Ignore)]
25 | public string Schema { get; set; }
26 | }
27 |
28 | public class DtdlInterface
29 | {
30 | private string _id;
31 |
32 | [JsonProperty("@id")]
33 | public string Id
34 | {
35 | get { return _id; }
36 | set
37 | {
38 | if (value.Length < 128) // DTMI must be < 128 characters
39 | {
40 | _id = value;
41 | }
42 | }
43 | }
44 |
45 | [JsonProperty("@type")]
46 | public string Type { get; set; }
47 |
48 | [JsonProperty("contents", NullValueHandling = NullValueHandling.Ignore)]
49 | public List Contents { get; set; }
50 |
51 | [JsonProperty("@context")]
52 | public string[] Context { get; set; } = { "dtmi:dtdl:context;2" };
53 |
54 | [JsonProperty("extends", NullValueHandling = NullValueHandling.Ignore)]
55 |
56 | public List Extends { get; set; }
57 |
58 | [JsonProperty("displayName", NullValueHandling = NullValueHandling.Ignore)]
59 | public string DisplayName { get; set; }
60 |
61 | [JsonProperty("comment", NullValueHandling = NullValueHandling.Ignore)]
62 | public string Comment { get; set; }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Models/NodeModel.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.ObjectModel;
2 | using System.Collections.Specialized;
3 | using Opc.Ua;
4 |
5 | namespace OPCUA2DTDL.Models
6 | {
7 | public class OpcUaNodeList : ObservableCollection
8 | {
9 | public OpcUaNodeList()
10 | {
11 | // Constructor
12 | }
13 |
14 | }
15 |
16 | public class OpcUaNode
17 | {
18 | public string DisplayName { get; set; }
19 | public string BrowseName { get; set; }
20 | public string NodeClass { get; set; }
21 | public string NodeId { get; set; }
22 | public string DataType { get; set; }
23 | public string ReferenceTypeId { get; set; }
24 | public string TypeDefinition { get; set; }
25 | public ObservableCollection Children { get; set; } = new ObservableCollection();
26 |
27 | public override string ToString()
28 | {
29 | return DisplayName;
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Models/NodeReferenceModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using Opc.Ua;
7 |
8 | namespace OPCUA2DTDL.Models
9 | {
10 | public class NodeReferenceData
11 | {
12 | public ReferenceTypeNode ReferenceType { get; set; }
13 | public bool IsInverse { get; set; }
14 | public Node Target { get; set; }
15 | public Node TypeDefinition { get; set; }
16 |
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/OPCUA2DTDL.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | x86
7 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}
8 | AppContainerExe
9 | Properties
10 | OPCUA2DTDL
11 | OPCUA2DTDL
12 | en-US
13 | UAP
14 | 10.0.19041.0
15 | 10.0.17763.0
16 | 14
17 | 512
18 | {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
19 | true
20 | false
21 |
22 |
23 | true
24 | bin\x86\Debug\
25 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP
26 | ;2008
27 | full
28 | x86
29 | false
30 | prompt
31 | true
32 |
33 |
34 | bin\x86\Release\
35 | TRACE;NETFX_CORE;WINDOWS_UWP
36 | true
37 | ;2008
38 | pdbonly
39 | x86
40 | false
41 | prompt
42 | true
43 | true
44 |
45 |
46 | true
47 | bin\ARM\Debug\
48 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP
49 | ;2008
50 | full
51 | ARM
52 | false
53 | prompt
54 | true
55 |
56 |
57 | bin\ARM\Release\
58 | TRACE;NETFX_CORE;WINDOWS_UWP
59 | true
60 | ;2008
61 | pdbonly
62 | ARM
63 | false
64 | prompt
65 | true
66 | true
67 |
68 |
69 | true
70 | bin\ARM64\Debug\
71 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP
72 | ;2008
73 | full
74 | ARM64
75 | false
76 | prompt
77 | true
78 | true
79 |
80 |
81 | bin\ARM64\Release\
82 | TRACE;NETFX_CORE;WINDOWS_UWP
83 | true
84 | ;2008
85 | pdbonly
86 | ARM64
87 | false
88 | prompt
89 | true
90 | true
91 |
92 |
93 | true
94 | bin\x64\Debug\
95 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP
96 | ;2008
97 | full
98 | x64
99 | false
100 | prompt
101 | true
102 |
103 |
104 | bin\x64\Release\
105 | TRACE;NETFX_CORE;WINDOWS_UWP
106 | true
107 | ;2008
108 | pdbonly
109 | x64
110 | false
111 | prompt
112 | true
113 | true
114 |
115 |
116 | PackageReference
117 |
118 |
119 |
120 | App.xaml
121 |
122 |
123 |
124 | MainPage.xaml
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | Designer
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | MSBuild:Compile
151 | Designer
152 |
153 |
154 | MSBuild:Compile
155 | Designer
156 |
157 |
158 |
159 |
160 | 3.12.5
161 |
162 |
163 | 5.2.8
164 |
165 |
166 | 6.2.12
167 |
168 |
169 | 2.5.0
170 |
171 |
172 | 1.4.364.40
173 |
174 |
175 |
176 | 14.0
177 |
178 |
179 |
186 |
--------------------------------------------------------------------------------
/OPCUA2DTDL.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khilscher/OPCUA2DTDL/f0b101319087ca82f851292fabeb537169a0115b/OPCUA2DTDL.gif
--------------------------------------------------------------------------------
/OPCUA2DTDL.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30517.126
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OPCUA2DTDL", "OPCUA2DTDL.csproj", "{569C9DED-8ECE-4AA9-B51A-F98A311A93E4}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|ARM = Debug|ARM
11 | Debug|ARM64 = Debug|ARM64
12 | Debug|x64 = Debug|x64
13 | Debug|x86 = Debug|x86
14 | Release|ARM = Release|ARM
15 | Release|ARM64 = Release|ARM64
16 | Release|x64 = Release|x64
17 | Release|x86 = Release|x86
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Debug|ARM.ActiveCfg = Debug|ARM
21 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Debug|ARM.Build.0 = Debug|ARM
22 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Debug|ARM.Deploy.0 = Debug|ARM
23 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Debug|ARM64.ActiveCfg = Debug|ARM64
24 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Debug|ARM64.Build.0 = Debug|ARM64
25 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Debug|ARM64.Deploy.0 = Debug|ARM64
26 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Debug|x64.ActiveCfg = Debug|x64
27 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Debug|x64.Build.0 = Debug|x64
28 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Debug|x64.Deploy.0 = Debug|x64
29 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Debug|x86.ActiveCfg = Debug|x86
30 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Debug|x86.Build.0 = Debug|x86
31 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Debug|x86.Deploy.0 = Debug|x86
32 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Release|ARM.ActiveCfg = Release|ARM
33 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Release|ARM.Build.0 = Release|ARM
34 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Release|ARM.Deploy.0 = Release|ARM
35 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Release|ARM64.ActiveCfg = Release|ARM64
36 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Release|ARM64.Build.0 = Release|ARM64
37 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Release|ARM64.Deploy.0 = Release|ARM64
38 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Release|x64.ActiveCfg = Release|x64
39 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Release|x64.Build.0 = Release|x64
40 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Release|x64.Deploy.0 = Release|x64
41 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Release|x86.ActiveCfg = Release|x86
42 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Release|x86.Build.0 = Release|x86
43 | {569C9DED-8ECE-4AA9-B51A-F98A311A93E4}.Release|x86.Deploy.0 = Release|x86
44 | EndGlobalSection
45 | GlobalSection(SolutionProperties) = preSolution
46 | HideSolutionNode = FALSE
47 | EndGlobalSection
48 | GlobalSection(ExtensibilityGlobals) = postSolution
49 | SolutionGuid = {82C2E731-6D49-4C79-9C4B-73B7054C7C34}
50 | EndGlobalSection
51 | EndGlobal
52 |
--------------------------------------------------------------------------------
/Opc.Ua.SampleClient.Config.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 | OPCUA2DTDL
8 | urn:localhost:Microsoft:OPCUA2DTDL
9 | http://someurl/OPCUA2DTDL
10 | Client_1
11 |
12 |
13 |
14 |
15 |
16 | X509Store
17 | CurrentUser\My
18 | CN=OPCUA2DTDL, C=US, S=Washington, O=Microsoft, DC=localhost
19 |
20 |
21 |
22 |
23 | Directory
24 | %LocalApplicationData%/OPC Foundation/pki/issuer
25 |
26 |
27 |
28 |
29 | Directory
30 | %LocalApplicationData%/OPC Foundation/pki/trusted
31 |
32 |
33 |
34 |
35 | Directory
36 | %LocalApplicationData%/OPC Foundation/pki/rejected
37 |
38 |
39 |
41 | false
42 |
43 |
44 |
45 |
46 |
47 |
48 | 600000
49 | 1048576
50 | 4194304
51 | 65535
52 | 4194304
53 | 65535
54 | 300000
55 | 3600000
56 |
57 |
58 |
59 |
60 |
61 |
62 | 600000
63 |
64 |
66 |
67 | opc.tcp://{0}:4840/UADiscovery
68 |
69 |
70 |
71 |
72 |
73 |
76 | 10000
77 |
78 |
79 |
80 | %LocalApplicationData%/Logs/Opc.Ua.CoreSampleClient.log.txt
81 | true
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | true
98 |
99 |
--------------------------------------------------------------------------------
/OpcUaClient.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Opc.Ua;
5 | using Opc.Ua.Client;
6 | using Opc.Ua.Configuration;
7 | using System.Threading;
8 | using System.Diagnostics;
9 | using System.Collections.ObjectModel;
10 | using OPCUA2DTDL.Models;
11 |
12 | namespace OPCUA2DTDL
13 | {
14 |
15 | public enum ExitCode : int
16 | {
17 | Ok = 0,
18 | ErrorCreateApplication = 0x11,
19 | ErrorDiscoverEndpoints = 0x12,
20 | ErrorCreateSession = 0x13,
21 | ErrorBrowseNamespace = 0x14,
22 | ErrorCreateSubscription = 0x15,
23 | ErrorMonitoredItem = 0x16,
24 | ErrorAddSubscription = 0x17,
25 | ErrorRunning = 0x18,
26 | ErrorNoKeepAlive = 0x30,
27 | ErrorInvalidCommandLine = 0x100
28 | };
29 |
30 | class OpcUaClient
31 | {
32 |
33 | private string _appName;
34 | private static Session _session;
35 | private string _endpointURL;
36 | private int _clientRunTime = Timeout.Infinite;
37 | private static bool _autoAccept = false;
38 | private static ExitCode _exitCode;
39 | private OpcUaNodeList _list = new OpcUaNodeList();
40 | private static Dictionary _map = new Dictionary();
41 |
42 | ///
43 | /// Constructor
44 | ///
45 | ///
46 | ///
47 | ///
48 | ///
49 | public OpcUaClient(string endpointURL, bool autoAccept, int stopTimeout, string appName)
50 | {
51 |
52 | _endpointURL = endpointURL;
53 | _autoAccept = autoAccept;
54 | _clientRunTime = stopTimeout <= 0 ? Timeout.Infinite : stopTimeout * 1000;
55 | _appName = appName;
56 |
57 | }
58 |
59 | public static ExitCode ExitCode { get => _exitCode; }
60 |
61 | ///
62 | /// Connects to an OPC UA server endpoint URL
63 | ///
64 | ///
65 | public async Task Connect()
66 | {
67 |
68 | _exitCode = ExitCode.ErrorCreateApplication;
69 |
70 | ApplicationInstance application = new ApplicationInstance
71 | {
72 |
73 | ApplicationName = _appName,
74 | ApplicationType = ApplicationType.Client,
75 | ConfigSectionName = Utils.IsRunningOnMono() ? "Opc.Ua.MonoSampleClient" : "Opc.Ua.SampleClient"
76 |
77 | };
78 |
79 | // Load the application configuration.
80 | ApplicationConfiguration config = await application.LoadApplicationConfiguration(false);
81 |
82 | // Check the application certificate.
83 | bool haveAppCertificate = await application.CheckApplicationInstanceCertificate(false, 0);
84 |
85 | if (!haveAppCertificate)
86 | {
87 |
88 | throw new Exception("Application instance certificate invalid!");
89 |
90 | }
91 |
92 | if (haveAppCertificate)
93 | {
94 |
95 | config.ApplicationUri = Utils.GetApplicationUriFromCertificate(config.SecurityConfiguration.ApplicationCertificate.Certificate);
96 |
97 | if (config.SecurityConfiguration.AutoAcceptUntrustedCertificates)
98 | {
99 | // TODO - Fix this section.
100 | _autoAccept = true;
101 |
102 | }
103 |
104 | config.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_CertificateValidation);
105 |
106 | }
107 | else
108 | {
109 |
110 | Debug.WriteLine("Missing application certificate, using unsecure connection.");
111 |
112 | }
113 |
114 | _exitCode = ExitCode.ErrorDiscoverEndpoints;
115 |
116 | var selectedEndpoint = CoreClientUtils.SelectEndpoint(_endpointURL, haveAppCertificate, 15000);
117 |
118 | _exitCode = ExitCode.ErrorCreateSession;
119 |
120 | var endpointConfiguration = EndpointConfiguration.Create(config);
121 |
122 | var endpoint = new ConfiguredEndpoint(null, selectedEndpoint, endpointConfiguration);
123 |
124 | return _session = await Session.Create(config, endpoint, false, _appName, 60000, new UserIdentity(new AnonymousIdentityToken()), null);
125 |
126 | }
127 |
128 | ///
129 | /// Browse the top level OPC UA structure under \Root
130 | ///
131 | ///
132 | public async Task Browse()
133 | {
134 |
135 | _exitCode = ExitCode.ErrorBrowseNamespace;
136 | ReferenceDescriptionCollection references;
137 | Byte[] continuationPoint;
138 |
139 | //Browse root nodes
140 | _session.Browse(
141 | null,
142 | null,
143 | ObjectIds.RootFolder,
144 | 0u,
145 | BrowseDirection.Forward,
146 | ReferenceTypeIds.HierarchicalReferences,
147 | true,
148 | (uint)NodeClass.Variable | (uint)NodeClass.Object | (uint)NodeClass.Method | (uint)NodeClass.DataType | (uint)NodeClass.ObjectType | (uint)NodeClass.ReferenceType | (uint)NodeClass.VariableType,
149 | out continuationPoint,
150 | out references);
151 |
152 | // Obtain details of each node in the root and add it to the list
153 | foreach (var rd in references)
154 | {
155 |
156 | OpcUaNode item = new OpcUaNode();
157 | item.DisplayName = rd.DisplayName.ToString();
158 | item.BrowseName = rd.BrowseName.ToString();
159 | item.NodeClass = rd.NodeClass.ToString();
160 | item.NodeId = rd.NodeId.ToString();
161 | item.ReferenceTypeId = rd.ReferenceTypeId.ToString();
162 | item.TypeDefinition = rd.TypeDefinition.ToString();
163 | item.DataType = GetDataType(_session, ExpandedNodeId.ToNodeId(rd.NodeId, _session.NamespaceUris));
164 | item.Children = new ObservableCollection();
165 |
166 | // Add top level folders to list
167 | _list.Add(item);
168 |
169 | // For each top level folder, browse the next level down
170 | BrowseNext(ExpandedNodeId.ToNodeId(rd.NodeId, _session.NamespaceUris), item);
171 |
172 | }
173 |
174 | return _list;
175 | }
176 |
177 | ///
178 | /// Browse the next level OPC UA structure
179 | ///
180 | ///
181 | ///
182 | public void BrowseNext(NodeId nodeid, OpcUaNode item)
183 | {
184 |
185 | _exitCode = ExitCode.ErrorBrowseNamespace;
186 | ReferenceDescriptionCollection references;
187 | Byte[] continuationPoint;
188 |
189 | _session.Browse(
190 | null,
191 | null,
192 | nodeid,
193 | 0u,
194 | BrowseDirection.Forward,
195 | ReferenceTypeIds.HierarchicalReferences,
196 | true,
197 | (uint)NodeClass.Variable | (uint)NodeClass.Object | (uint)NodeClass.Method | (uint)NodeClass.DataType | (uint)NodeClass.ObjectType | (uint)NodeClass.ReferenceType | (uint)NodeClass.VariableType,
198 | out continuationPoint,
199 | out references);
200 |
201 | foreach (var rd in references)
202 | {
203 |
204 | try
205 | {
206 |
207 | OpcUaNode childItem = new OpcUaNode();
208 | childItem.DisplayName = rd.DisplayName.ToString();
209 | childItem.BrowseName = rd.BrowseName.ToString();
210 | childItem.NodeClass = rd.NodeClass.ToString();
211 | childItem.NodeId = rd.NodeId.ToString();
212 | childItem.DataType = GetDataType(_session, ExpandedNodeId.ToNodeId(rd.NodeId, _session.NamespaceUris));
213 | childItem.ReferenceTypeId = rd.ReferenceTypeId.ToString();
214 | childItem.TypeDefinition = rd.TypeDefinition.ToString();
215 | childItem.Children = new ObservableCollection();
216 |
217 | // Add it to the parent
218 | item.Children.Add(childItem);
219 |
220 | // Recursion
221 | BrowseNext(ExpandedNodeId.ToNodeId(rd.NodeId, _session.NamespaceUris), childItem);
222 |
223 | }
224 | catch (Exception e)
225 | {
226 | //Debug.WriteLine(e.Message);
227 | }
228 | }
229 | }
230 |
231 | ///
232 | /// Get the OPC UA data type
233 | ///
234 | ///
235 | ///
236 | ///
237 | private string GetDataType(Session session, NodeId nodeId)
238 | {
239 |
240 | // Build list of attributes to read.
241 | ReadValueIdCollection nodesToRead = new ReadValueIdCollection();
242 |
243 | foreach (uint attributeId in new uint[] { Attributes.DataType, Attributes.ValueRank })
244 | {
245 |
246 | ReadValueId nodeToRead = new ReadValueId();
247 | nodeToRead.NodeId = nodeId;
248 | nodeToRead.AttributeId = attributeId;
249 | nodesToRead.Add(nodeToRead);
250 |
251 | }
252 |
253 | // Read the attributes.
254 | DataValueCollection results = null;
255 | DiagnosticInfoCollection diagnosticInfos = null;
256 |
257 | session.Read(
258 | null,
259 | 0,
260 | TimestampsToReturn.Neither,
261 | nodesToRead,
262 | out results,
263 | out diagnosticInfos);
264 |
265 | ClientBase.ValidateResponse(results, nodesToRead);
266 | ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToRead);
267 |
268 | // This call checks for error and checks the data type of the value.
269 | // If an error or mismatch occurs the default value is returned.
270 | NodeId dataTypeId = results[0].GetValue(null);
271 |
272 | // Use the local type cache to look up the base type for the data type.
273 | BuiltInType builtInType = DataTypes.GetBuiltInType(dataTypeId, session.NodeCache.TypeTree);
274 |
275 | // The type info object is used in cast and compare functions.
276 | return builtInType.ToString();
277 |
278 | }
279 |
280 | ///
281 | /// Validate the cert
282 | ///
283 | ///
284 | ///
285 | private static void CertificateValidator_CertificateValidation(CertificateValidator validator, CertificateValidationEventArgs e)
286 | {
287 |
288 | if (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted)
289 | {
290 |
291 | e.Accept = _autoAccept;
292 |
293 | if (_autoAccept)
294 | {
295 |
296 | Debug.WriteLine("Accepted Certificate: {0}", e.Certificate.Subject);
297 |
298 | }
299 | else
300 | {
301 |
302 | Debug.WriteLine("Rejected Certificate: {0}", e.Certificate.Subject);
303 |
304 | }
305 |
306 | }
307 |
308 | }
309 |
310 | ///
311 | /// Get the type definition for a node id
312 | ///
313 | ///
314 | ///
315 | public static string GetTypeDefinition(NodeId id)
316 | {
317 |
318 | string name = "Unknown";
319 |
320 | var typeDefinition = _session.NodeCache.Find(ExpandedNodeId.ToNodeId(id, _session.NamespaceUris)) as Node;
321 |
322 | if (typeDefinition != null)
323 | {
324 |
325 | name = typeDefinition.ToString();
326 |
327 | }
328 |
329 | return name;
330 |
331 | }
332 |
333 | }
334 | }
335 |
--------------------------------------------------------------------------------
/Package.appxmanifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
13 |
14 |
15 |
16 |
17 | OPCUA2DTDL
18 | kehilsch
19 | Assets\StoreLogo.png
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
34 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("OPCUA2DTDL")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("OPCUA2DTDL")]
13 | [assembly: AssemblyCopyright("Copyright © 2020")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Version information for an assembly consists of the following four values:
18 | //
19 | // Major Version
20 | // Minor Version
21 | // Build Number
22 | // Revision
23 | //
24 | // You can specify all the values or you can default the Build and Revision Numbers
25 | // by using the '*' as shown below:
26 | // [assembly: AssemblyVersion("1.0.*")]
27 | [assembly: AssemblyVersion("1.0.0.0")]
28 | [assembly: AssemblyFileVersion("1.0.0.0")]
29 | [assembly: ComVisible(false)]
--------------------------------------------------------------------------------
/Properties/Default.rd.xml:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OPCUA2DTDL
2 | OPC UA to DTDL v2 Conversion Tool built using UWP./NET and the [OPC Foundation OPC UA .NET SDK](http://opcfoundation.github.io/UA-.NETStandard/).
3 |
4 | ## Instructions
5 | Connect and browse an OPC UA server, select nodes and convert them to [DTDL v2](https://github.com/Azure/opendigitaltwins-dtdl/blob/master/DTDL/v2/dtdlv2.md).
6 |
7 | The Root\Types folder contains the OPC UA base information model, custom models (if any) and companion specifications (if any) that have been loaded into that OPC UA server. These can be converted to DTDL v2 constructs using two patterns:
8 |
9 | - **Collapsed pattern** – where OPC UA ObjectTypes, along with related properties, variables and methods, are collapsed down into a single DTDL Interface (Many:1) containing property, telemetry and command types.
10 | - **Expanded pattern** – where every OPC UA DataType, ObjectType, VariableType etc. is mapped to an equivalent DTDL Interface (1:1) and DTDL Relationships are created based on OPC UA References.
11 |
12 |
13 | 
--------------------------------------------------------------------------------