From 085c5994ca7dae0e79b0ec2f8e2316fff241698b Mon Sep 17 00:00:00 2001 From: Thomas Vetter <80452158+thomvet@users.noreply.github.com> Date: Wed, 24 Apr 2024 15:23:01 +0200 Subject: [PATCH 01/26] docstrings for server.jl add node functions (higher level interface missing) --- src/server.jl | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/server.jl b/src/server.jl index 4de8e3b..e882867 100644 --- a/src/server.jl +++ b/src/server.jl @@ -39,7 +39,7 @@ uses the server API to add a method node with the callback `method` to the `serv `UA_MethodCallback_generate` is internally called on the `method` supplied and thus its function signature must match its requirements. -See `UA_MethodAttributes_generate` on how to define valid method attributes. +See `UA_MethodAttributes_generate` on how to define valid attributes. """ function UA_Server_addMethodNode(server, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, attr, method::Function, @@ -63,7 +63,7 @@ function JUA_Server_addNode(server, requestedNewNodeId, method::Function, inputArgumentsSize, inputArguments, outputArgumentsSize, outputArguments) #TODO: consider whether we would like to go even higher level here (automatically generate inputArguments of the correct size etc.) return UA_Server_addMethodNode(server, requestedNewNodeId, parentNodeId, - referenceTypeId, browseName, attr, method, + referenceTypeId, browseName, attributes, method, inputArgumentsSize, inputArguments, outputArgumentsSize, outputArguments, nodeContext, outNewNodeId) end @@ -88,7 +88,19 @@ for nodeclass in instances(UA_NodeClass) funname_sym == :UA_Server_addObjectNode @eval begin # emit specific add node functions - #TODO: add docstring + """ + ``` + $($(funname_sym))(server::Ptr{UA_Server}, requestednewnodeid::Ptr{UA_NodeId}, + parentnodeid::Ptr{UA_NodeId}, referenceTypeId::Ptr{UA_NodeId}, + browseName::Ptr{UA_QualifiedName}, typedefinition::Ptr{UA_NodeId}, + attr::Ptr{$($(attributetype_sym))}, nodeContext::Ptr{UA_NodeId}, + outNewNodeId::Ptr{UA_NodeId})::UA_StatusCode + ``` + + uses the server API to add a $(lowercase(string($nodeclass_sym)[14:end])) node to the `server`. + + See `$($(attributetype_sym))_generate` on how to define valid attributes. + """ function $(funname_sym)(server, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, typeDefinition, attributes, nodeContext, outNewNodeId) @@ -112,7 +124,19 @@ for nodeclass in instances(UA_NodeClass) elseif funname_sym != :UA_Server_addMethodNode @eval begin # emit specific add node functions - #TODO: add docstring + """ + ``` + $($(funname_sym))(server::Ptr{UA_Server}, requestednewnodeid::Ptr{UA_NodeId}, + parentnodeid::Ptr{UA_NodeId}, referenceTypeId::Ptr{UA_NodeId}, + browseName::Ptr{UA_QualifiedName}, typedefinition::Ptr{UA_NodeId}, + attr::Ptr{$($(attributetype_sym))}, nodeContext::Ptr{UA_NodeId}, + outNewNodeId::Ptr{UA_NodeId})::UA_StatusCode + ``` + + uses the server API to add a $(lowercase(string($nodeclass_sym)[14:end])) node to the `server`. + + See `$($(attributetype_sym))_generate` on how to define valid attributes. + """ function $(funname_sym)(server, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, attributes, nodeContext, outNewNodeId) From 79949a0016848b1c0b4033414b6f9b15a85bd112 Mon Sep 17 00:00:00 2001 From: Thomas Vetter <80452158+thomvet@users.noreply.github.com> Date: Wed, 24 Apr 2024 17:02:47 +0200 Subject: [PATCH 02/26] More server and client docstrings + docs --- .gitignore | 1 + docs/make.jl | 7 +- docs/src/manual/attributegeneration.md | 25 +++++++ docs/src/manual/client.md | 21 ++++++ docs/src/manual/server.md | 18 +++++ src/attribute_generation.jl | 96 +++++++++++++++++++++----- src/client.jl | 49 +++++++++---- src/server.jl | 8 +-- 8 files changed, 189 insertions(+), 36 deletions(-) create mode 100644 docs/src/manual/attributegeneration.md create mode 100644 docs/src/manual/client.md create mode 100644 docs/src/manual/server.md diff --git a/.gitignore b/.gitignore index 00147a0..e72a954 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ gen/Manifest.toml gen/Manifest.toml .vscode/settings.json +docs/Manifest.toml diff --git a/docs/make.jl b/docs/make.jl index 9768e4a..dcedd31 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -16,7 +16,12 @@ makedocs(; repolink = "https://github.com/martinkosch/open62541.jl", ), pages = [ - "Home" => "index.md" + "Home" => "index.md", + "Manual" => [ + "manual/attributegeneration.md", + "manual/server.md", + "manual/client.md", + ] ], warnonly = Documenter.except( :autodocs_block, diff --git a/docs/src/manual/attributegeneration.md b/docs/src/manual/attributegeneration.md new file mode 100644 index 0000000..c01cd63 --- /dev/null +++ b/docs/src/manual/attributegeneration.md @@ -0,0 +1,25 @@ +# Attribute generation + +This page lists docstrings of functions used for convenient attribute generation. + +Convenience functions that allow setting values for specific attributes: +```@docs +UA_VALUERANK +UA_ACCESSLEVEL +UA_USERACCESSLEVEL +UA_WRITEMASK +UA_USERWRITEMASK +UA_EVENTNOTIFIER +``` + +Generators for attribute blocks: +```@docs +UA_VariableAttributes_generate +UA_VariableTypeAttributes_generate +UA_ObjectAttributes_generate +UA_ObjectTypeAttributes_generate +UA_MethodAttributes_generate +UA_ViewAttributes_generate +UA_DataTypeAttributes_generate +UA_ReferenceTypeAttributes_generate +``` diff --git a/docs/src/manual/client.md b/docs/src/manual/client.md new file mode 100644 index 0000000..cd322d1 --- /dev/null +++ b/docs/src/manual/client.md @@ -0,0 +1,21 @@ +# Client + +This page lists docstrings relevant to the client API. + +## Adding different types of nodes: +```@docs +UA_Client_addVariableNode +UA_Client_addObjectNode +UA_Client_addVariableTypeNode +UA_Client_addObjectTypeNode +UA_Client_addViewNode +UA_Client_addReferenceTypeNode +UA_Client_addDataTypeNode +UA_Client_addMethodNode +``` + +## Reading from nodes: +Incomplete as of now. + +## Writing to nodes: +Incomplete as of now. diff --git a/docs/src/manual/server.md b/docs/src/manual/server.md new file mode 100644 index 0000000..5e8f508 --- /dev/null +++ b/docs/src/manual/server.md @@ -0,0 +1,18 @@ +# Server + +This page lists docstrings relevant to the server API. + +## Adding different types of nodes: +```@docs +UA_Server_addVariableNode +UA_Server_addObjectNode +UA_Server_addVariableTypeNode +UA_Server_addObjectTypeNode +UA_Server_addViewNode +UA_Server_addReferenceTypeNode +UA_Server_addDataTypeNode +UA_Server_addMethodNode +``` +## Reading from nodes: + +## Writing to nodes: diff --git a/src/attribute_generation.jl b/src/attribute_generation.jl index 7a6a822..d4ab2d7 100644 --- a/src/attribute_generation.jl +++ b/src/attribute_generation.jl @@ -1,6 +1,14 @@ ### attribute generation functions ##generic functions +""" +``` +UA_VALUERANK(N::Integer)::Integer +``` + +returns the valuerank based on the dimensionality of an array `N`. For special +cases see here: [Unified Automation Website](https://documentation.unified-automation.com/uasdknet/2.5.7/html/classUnifiedAutomation_1_1UaBase_1_1ValueRanks.html#details) +""" function UA_VALUERANK(N::Integer) N == 1 && return UA_VALUERANK_ONE_DIMENSION N == 2 && return UA_VALUERANK_TWO_DIMENSIONS @@ -17,16 +25,12 @@ UA_ACCESSLEVEL(; read = false, write = false, historyread = false, calculates a `UInt8` number expressing how the value of a variable can be accessed. Default is to disallow all operations. The meaning of the keywords is explained -here: https://reference.opcfoundation.org/Core/Part3/v105/docs/8.57 +here: [OPC Foundation Website](https://reference.opcfoundation.org/Core/Part3/v105/docs/8.57) """ -function UA_ACCESSLEVEL(; - read::Bool = false, - write::Bool = false, - historyread::Bool = false, - historywrite::Bool = false, - semanticchange::Bool = false, - statuswrite::Bool = false, - timestampwrite::Bool = false) +function UA_ACCESSLEVEL(; read::Bool = false, write::Bool = false, + historyread::Bool = false, historywrite::Bool = false, + semanticchange::Bool = false, statuswrite::Bool = false, + timestampwrite::Bool = false) al = UA_Byte(0) al = read ? al | UA_ACCESSLEVELMASK_READ : al al = write ? al | UA_ACCESSLEVELMASK_WRITE : al @@ -38,6 +42,26 @@ function UA_ACCESSLEVEL(; return UA_Byte(al) end +""" +``` +UA_USERACCESSLEVEL(; read = false, write = false, historyread = false, + historywrite = false, semanticchange = false, statuswrite = false, + timestampwrite = false)::UInt8 +``` + +calculates a `UInt8` number expressing how the value of a variable can be accessed. +Default is to disallow all operations. The meaning of the keywords is explained +here: [OPC Foundation Website](https://reference.opcfoundation.org/Core/Part3/v105/docs/8.57) +""" +function UA_USERACCESSLEVEL(; read::Bool = false, write::Bool = false, + historyread::Bool = false, historywrite::Bool = false, + semanticchange::Bool = false, statuswrite::Bool = false, + timestampwrite::Bool = false) + UA_ACCESSLEVEL(; read = read, write = write, historyread = historyread, + historywrite = historywrite, semanticchange = semanticchange, + statuswrite = statuswrite, timestampwrite = timestampwrite) +end + """ ``` UA_WRITEMASK(; accesslevel = false, arraydimensions = false, @@ -56,14 +80,14 @@ The meaning of the keywords is explained here: https://reference.opcfoundation.o If the specific node type does not support an attribute, the corresponding keyword must be set to false. *This is currently not enforced automatically.* """ -function UA_WRITEMASK(; accesslevel = false, arraydimensions = false, - browsename = false, containsnoloops = false, datatype = false, - description = false, displayname = false, eventnotifier = false, - executable = false, historizing = false, inversename = false, - isabstract = false, minimumsamplinginterval = false, nodeclass = false, - nodeid = false, symmetric = false, useraccesslevel = false, - userexecutable = false, userwritemask = false, valuerank = false, - writemask = false, valueforvariabletype = false) +function UA_WRITEMASK(; accesslevel::Bool = false, arraydimensions::Bool = false, + browsename::Bool = false, containsnoloops::Bool = false, datatype::Bool = false, + description::Bool = false, displayname::Bool = false, eventnotifier::Bool = false, + executable::Bool = false, historizing::Bool = false, inversename::Bool = false, + isabstract::Bool = false, minimumsamplinginterval = false, nodeclass = false, + nodeid::Bool = false, symmetric::Bool = false, useraccesslevel::Bool = false, + userexecutable::Bool = false, userwritemask::Bool = false, valuerank::Bool = false, + writemask::Bool = false, valueforvariabletype::Bool = false) wm = UInt32(0) wm = accesslevel ? wm | UA_WRITEMASK_ACCESSLEVEL : wm wm = arraydimensions ? wm | UA_WRITEMASK_ARRRAYDIMENSIONS : wm #Note: RRR is a typo in open62541, not here. @@ -90,7 +114,41 @@ function UA_WRITEMASK(; accesslevel = false, arraydimensions = false, return wm end -const UA_USERWRITEMASK = UA_WRITEMASK +""" +``` +UA_USERWRITEMASK(; accesslevel = false, arraydimensions = false, + browsename = false, containsnoloops = false, datatype = false, + description = false, displayname = false, eventnotifier = false, + executable = false, historizing = false, inversename = false, + isabstract = false, minimumsamplinginterval = false, nodeclass = false, + nodeid = false, symmetric = false, useraccesslevel = false, + userexecutable = false, userwritemask = false, valuerank = false, + writemask = false, valueforvariabletype = false)::UInt32 +``` + +calculates a `UInt32` number expressing which attributes of a node are writeable. +The meaning of the keywords is explained here: [OPC Foundation Website](https://reference.opcfoundation.org/Core/Part3/v105/docs/8.60) + +If the specific node type does not support an attribute, the corresponding keyword +must be set to false. *This is currently not enforced automatically.* +""" +function UA_USERWRITEMASK(; accesslevel::Bool = false, arraydimensions::Bool = false, + browsename::Bool = false, containsnoloops::Bool = false, datatype::Bool = false, + description::Bool = false, displayname::Bool = false, eventnotifier::Bool = false, + executable::Bool = false, historizing::Bool = false, inversename::Bool = false, + isabstract::Bool = false, minimumsamplinginterval = false, nodeclass = false, + nodeid::Bool = false, symmetric::Bool = false, useraccesslevel::Bool = false, + userexecutable::Bool = false, userwritemask::Bool = false, valuerank::Bool = false, + writemask::Bool = false, valueforvariabletype::Bool = false) + UA_WRITEMASK(; accesslevel = accesslevel, arraydimensions = arraydimensions, + browsename = browsename, containsnoloops = containsnoloops, datatype = datatype, + description = description , displayname = displayname , eventnotifier = eventnotifier, + executable = executable, historizing = historizing, inversename = inversename, + isabstract = isabstract, minimumsamplinginterval = minimumsamplinginterval, nodeclass = nodeclass, + nodeid = nodeid, symmetric = symmetric, useraccesslevel = useraccesslevel, + userexecutable = userexecutable, userwritemask = userwritemask, valuerank = valuerank, + writemask = writemask, valueforvariabletype = valueforvariabletype) +end """ ``` @@ -101,7 +159,7 @@ UA_EVENTNOTIFIER(; subscribetoevent = false, historyread = false, calculates a `UInt8` number expressing whether a node can be used to subscribe to events and/or read/write historic events. -Meaning of keywords is explained here: https://reference.opcfoundation.org/Core/Part3/v105/docs/8.59 +Meaning of keywords is explained here: [OPC Foundation Website](https://reference.opcfoundation.org/Core/Part3/v105/docs/8.59) """ function UA_EVENTNOTIFIER(; subscribetoevent = false, diff --git a/src/client.jl b/src/client.jl index af061ba..05935fa 100644 --- a/src/client.jl +++ b/src/client.jl @@ -113,19 +113,30 @@ for nodeclass in instances(UA_NodeClass) funname_sym == :UA_Client_addObjectNode @eval begin # emit specific add node functions - # original function signatures are the following, note difference in number of arguments. - # UA_Client_addVariableNode (*client, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, typeDefinition, attr, *outNewNodeId) - # UA_Client_addObjectNode (*client, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, typeDefinition, attr, *outNewNodeId) - # UA_Client_addVariableTypeNode (*client, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, attr, *outNewNodeId) - # UA_Client_addReferenceTypeNode(*client, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, attr, *outNewNodeId) - # UA_Client_addObjectTypeNode (*client, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, attr, *outNewNodeId) - # UA_Client_addViewNode (*client, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, attr, *outNewNodeId) - # UA_Client_addReferenceTypeNode(*client, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, attr, *outNewNodeId) - # UA_Client_addDataTypeNode (*client, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, attr, *outNewNodeId) - # UA_Client_addMethodNode (*client, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, attr, *outNewNodeId) - - #TODO: add docstring #TODO: add tests + # Untested: UA_Client_addVariableNode + # Untested: UA_Client_addObjectNode + # Untested: UA_Client_addVariableTypeNode + # Untested: UA_Client_addReferenceTypeNode + # Untested: UA_Client_addObjectTypeNode + # Untested: UA_Client_addReferenceTypeNode + # Untested: UA_Client_addDataTypeNode + # Untested: UA_Client_addMethodNode (does addmethodnode even make sense? There is no possibility to transfer a callback over?!?!) + # --> Tests should go into test/client_add_nodes.jl + """ + ``` + $($(funname_sym))(::Ptr{UA_Client}, requestednewnodeid::Ptr{UA_NodeId}, + parentnodeid::Ptr{UA_NodeId}, referenceTypeId::Ptr{UA_NodeId}, + browseName::Ptr{UA_QualifiedName}, typedefinition::Ptr{UA_NodeId}, + attr::Ptr{$($(attributetype_sym))}, outNewNodeId::Ptr{UA_NodeId})::UA_StatusCode + ``` + + uses the client API to add a $(lowercase(string($nodeclass_sym)[14:end])) + node to the `client`. + + See [`$($(attributetype_sym))_generate`](@ref) on how to define valid + attributes. + """ function $(funname_sym)(client, requestedNewNodeId, parentNodeId, @@ -164,6 +175,20 @@ for nodeclass in instances(UA_NodeClass) end else @eval begin + """ + ``` + $($(funname_sym))(::Ptr{UA_Client}, requestednewnodeid::Ptr{UA_NodeId}, + parentnodeid::Ptr{UA_NodeId}, referenceTypeId::Ptr{UA_NodeId}, + browseName::Ptr{UA_QualifiedName}, attr::Ptr{$($(attributetype_sym))}, + outNewNodeId::Ptr{UA_NodeId})::UA_StatusCode + ``` + + uses the client API to add a $(lowercase(string($nodeclass_sym)[14:end])) + node to the `client`. + + See [`$($(attributetype_sym))_generate`](@ref) on how to define valid + attributes. + """ function $(funname_sym)(client, requestedNewNodeId, parentNodeId, diff --git a/src/server.jl b/src/server.jl index e882867..45b17af 100644 --- a/src/server.jl +++ b/src/server.jl @@ -39,7 +39,7 @@ uses the server API to add a method node with the callback `method` to the `serv `UA_MethodCallback_generate` is internally called on the `method` supplied and thus its function signature must match its requirements. -See `UA_MethodAttributes_generate` on how to define valid attributes. +See [`UA_MethodAttributes_generate`](@ref) on how to define valid attributes. """ function UA_Server_addMethodNode(server, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, attr, method::Function, @@ -99,7 +99,7 @@ for nodeclass in instances(UA_NodeClass) uses the server API to add a $(lowercase(string($nodeclass_sym)[14:end])) node to the `server`. - See `$($(attributetype_sym))_generate` on how to define valid attributes. + See [`$($(attributetype_sym))_generate`](@ref) on how to define valid attributes. """ function $(funname_sym)(server, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, typeDefinition, attributes, @@ -135,7 +135,7 @@ for nodeclass in instances(UA_NodeClass) uses the server API to add a $(lowercase(string($nodeclass_sym)[14:end])) node to the `server`. - See `$($(attributetype_sym))_generate` on how to define valid attributes. + See [`$($(attributetype_sym))_generate`](@ref) on how to define valid attributes. """ function $(funname_sym)(server, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, attributes, nodeContext, @@ -210,7 +210,7 @@ for att in attributes_UA_Server_write $($(fun_name))(server, nodeId, new_val) ``` Uses the Server API to write the value `new_val` to the attribute $($(String(attr_name))) - of the NodeId `nodeId` located on server `server`. + of the NodeId `nodeId` located on the `server`. """ function $(fun_name)(server, nodeId, new_val) data_type_ptr = UA_TYPES_PTRS[$(attr_type_ptr)] From 5316618757092cc391ec8b7c0c380ece19aacfb8 Mon Sep 17 00:00:00 2001 From: Thomas Vetter <80452158+thomvet@users.noreply.github.com> Date: Thu, 25 Apr 2024 00:52:26 +0200 Subject: [PATCH 03/26] distribute docstrings onto different api pages --- docs/src/manual/client.md | 48 ++++++++++++++++++++++++++++++++++-- docs/src/manual/server.md | 39 +++++++++++++++++++++++++++++ test/attribute_generation.jl | 36 ++++++++++++++++++++++++++- 3 files changed, 120 insertions(+), 3 deletions(-) diff --git a/docs/src/manual/client.md b/docs/src/manual/client.md index cd322d1..a7d0013 100644 --- a/docs/src/manual/client.md +++ b/docs/src/manual/client.md @@ -15,7 +15,51 @@ UA_Client_addMethodNode ``` ## Reading from nodes: -Incomplete as of now. +```@docs +UA_Client_readAccessLevelAttribute +UA_Client_readBrowseNameAttribute +UA_Client_readContainsNoLoopsAttribute +UA_Client_readDataTypeAttribute +UA_Client_readDescriptionAttribute +UA_Client_readDisplayNameAttribute +UA_Client_readEventNotifierAttribute +UA_Client_readExecutableAttribute +UA_Client_readHistorizingAttribute +UA_Client_readInverseNameAttribute +UA_Client_readIsAbstractAttribute +UA_Client_readMinimumSamplingIntervalAttribute +UA_Client_readNodeClassAttribute +UA_Client_readNodeIdAttribute +UA_Client_readSymmetricAttribute +UA_Client_readUserAccessLevelAttribute +UA_Client_readUserExecutableAttribute +UA_Client_readUserWriteMaskAttribute +UA_Client_readValueAttribute +UA_Client_readValueRankAttribute +UA_Client_readWriteMaskAttribute +``` ## Writing to nodes: -Incomplete as of now. +```@docs +UA_Client_writeAccessLevelAttribute +UA_Client_writeBrowseNameAttribute +UA_Client_writeContainsNoLoopsAttribute +UA_Client_writeDataTypeAttribute +UA_Client_writeDescriptionAttribute +UA_Client_writeDisplayNameAttribute +UA_Client_writeEventNotifierAttribute +UA_Client_writeExecutableAttribute +UA_Client_writeHistorizingAttribute +UA_Client_writeInverseNameAttribute +UA_Client_writeIsAbstractAttribute +UA_Client_writeMinimumSamplingIntervalAttribute +UA_Client_writeNodeClassAttribute +UA_Client_writeNodeIdAttribute +UA_Client_writeSymmetricAttribute +UA_Client_writeUserAccessLevelAttribute +UA_Client_writeUserExecutableAttribute +UA_Client_writeUserWriteMaskAttribute +UA_Client_writeValueAttribute +UA_Client_writeValueRankAttribute +UA_Client_writeWriteMaskAttribute +``` diff --git a/docs/src/manual/server.md b/docs/src/manual/server.md index 5e8f508..ae05abc 100644 --- a/docs/src/manual/server.md +++ b/docs/src/manual/server.md @@ -14,5 +14,44 @@ UA_Server_addDataTypeNode UA_Server_addMethodNode ``` ## Reading from nodes: +```@docs +UA_Server_readAccessLevel +UA_Server_readArrayDimensions +UA_Server_readBrowseName +UA_Server_readContainsNoLoops +UA_Server_readDataType +UA_Server_readDescription +UA_Server_readDisplayName +UA_Server_readEventNotifier +UA_Server_readExecutable +UA_Server_readHistorizing +UA_Server_readInverseName +UA_Server_readIsAbstract +UA_Server_readMinimumSamplingInterval +UA_Server_readNodeClass +UA_Server_readNodeId +UA_Server_readSymmetric +UA_Server_readValue +UA_Server_readValueRank +UA_Server_readWriteMask +``` ## Writing to nodes: +```@docs +UA_Server_writeAccessLevel +UA_Server_writeArrayDimensions +UA_Server_writeBrowseName +UA_Server_writeDataType +UA_Server_writeDataValue +UA_Server_writeDescription +UA_Server_writeDisplayName +UA_Server_writeEventNotifier +UA_Server_writeExecutable +UA_Server_writeHistorizing +UA_Server_writeInverseName +UA_Server_writeIsAbstract +UA_Server_writeMinimumSamplingInterval +UA_Server_writeValue +UA_Server_writeValueRank +UA_Server_writeWriteMask +``` \ No newline at end of file diff --git a/test/attribute_generation.jl b/test/attribute_generation.jl index 3a94d9c..6032006 100644 --- a/test/attribute_generation.jl +++ b/test/attribute_generation.jl @@ -10,7 +10,7 @@ using Test @test UA_VALUERANK(-2) == -2 #UA_ACCESSLEVEL -@test UA_ACCESSLEVEL() == 0 #default is read = true +@test UA_ACCESSLEVEL() == 0 @test UA_ACCESSLEVEL(write = true) == UA_ACCESSLEVELMASK_WRITE @test UA_ACCESSLEVEL(write = true, read = true) == UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE @@ -21,6 +21,18 @@ using Test UA_ACCESSLEVELMASK_SEMANTICCHANGE | UA_ACCESSLEVELMASK_STATUSWRITE | UA_ACCESSLEVELMASK_TIMESTAMPWRITE +#UA_USERACCESSLEVEL +@test UA_USERACCESSLEVEL() == 0 +@test UA_USERACCESSLEVEL(write = true) == UA_ACCESSLEVELMASK_WRITE +@test UA_USERACCESSLEVEL(write = true, read = true) == + UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE +@test UA_USERACCESSLEVEL(read = true, write = true, historyread = true, historywrite = true, + semanticchange = true, timestampwrite = true, statuswrite = true) == + UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE | + UA_ACCESSLEVELMASK_HISTORYREAD | UA_ACCESSLEVELMASK_HISTORYWRITE | + UA_ACCESSLEVELMASK_SEMANTICCHANGE | UA_ACCESSLEVELMASK_STATUSWRITE | + UA_ACCESSLEVELMASK_TIMESTAMPWRITE + #UA_WRITEMASK @test UA_WRITEMASK() == 0 @test UA_WRITEMASK(; accesslevel = true, arraydimensions = true, @@ -42,6 +54,28 @@ using Test UA_WRITEMASK_USERWRITEMASK | UA_WRITEMASK_VALUERANK | UA_WRITEMASK_WRITEMASK | UA_WRITEMASK_VALUEFORVARIABLETYPE +#UA_WRITEMASK +@test UA_USERWRITEMASK() == 0 +@test UA_USERWRITEMASK(; accesslevel = true, arraydimensions = true, + browsename = true, containsnoloops = true, datatype = true, + description = true, displayname = true, eventnotifier = true, + executable = true, historizing = true, inversename = true, + isabstract = true, minimumsamplinginterval = true, nodeclass = true, + nodeid = true, symmetric = true, useraccesslevel = true, + userexecutable = true, userwritemask = true, valuerank = true, + writemask = true, valueforvariabletype = true) == + UA_WRITEMASK_ACCESSLEVEL | UA_WRITEMASK_ARRRAYDIMENSIONS | + UA_WRITEMASK_BROWSENAME | UA_WRITEMASK_CONTAINSNOLOOPS | UA_WRITEMASK_DATATYPE | + UA_WRITEMASK_DESCRIPTION | UA_WRITEMASK_DISPLAYNAME | + UA_WRITEMASK_EVENTNOTIFIER | UA_WRITEMASK_EXECUTABLE | + UA_WRITEMASK_HISTORIZING | UA_WRITEMASK_INVERSENAME | + UA_WRITEMASK_ISABSTRACT | UA_WRITEMASK_MINIMUMSAMPLINGINTERVAL | + UA_WRITEMASK_NODECLASS | UA_WRITEMASK_NODEID | UA_WRITEMASK_SYMMETRIC | + UA_WRITEMASK_USERACCESSLEVEL | UA_WRITEMASK_USEREXECUTABLE | + UA_WRITEMASK_USERWRITEMASK | UA_WRITEMASK_VALUERANK | UA_WRITEMASK_WRITEMASK | + UA_WRITEMASK_VALUEFORVARIABLETYPE + + #UA_EVENTNOTIFIER @test UA_EVENTNOTIFIER() == 0 @test UA_EVENTNOTIFIER(subscribetoevent = true, historyread = true, historywrite = true) == From f13fcf9fdc51774a2f6cacf577d47e6d076963a2 Mon Sep 17 00:00:00 2001 From: Thomas Vetter <80452158+thomvet@users.noreply.github.com> Date: Thu, 25 Apr 2024 14:19:02 +0200 Subject: [PATCH 04/26] adjust purpose text in server_read.jl --- test/server_read.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/server_read.jl b/test/server_read.jl index d4ab4cf..e07e00f 100644 --- a/test/server_read.jl +++ b/test/server_read.jl @@ -1,9 +1,9 @@ -# Purpose: This testset checks whether the UA_Server_readXXXAttribute(...) functions -#are usable. This is currently only implemented for nodes of "variable" type. For the attributes -#contained in such nodes we check whether the respective read function returns the right type of -#variable. For functions not defined for a variable node, we check that they throw the appropriate exception. - -#TODO: implement other node types, so that we can check the remaining functions. +# Purpose: This testset checks whether the UA_Server_readXXXAttribute(...) +# functions are usable. This is currently only implemented for nodes of +# "variable" and "variabletype" type. For the attributes contained in such nodes +# we check whether the respective read function returns the right type of variable. +# For functions not defined for a variable node, we check that they throw the +# appropriate exception. using open62541 using Test From 055dd517af5654115cc230802405932116f85e22 Mon Sep 17 00:00:00 2001 From: Thomas Vetter <80452158+thomvet@users.noreply.github.com> Date: Fri, 3 May 2024 15:33:27 +0200 Subject: [PATCH 05/26] add readme and further docs --- README.md | 60 ++++++++++++++++++- docs/make.jl | 5 +- docs/src/index.md | 6 +- docs/src/manual/nodeid.md | 21 +++++++ docs/src/tutorials/server_first_steps.md | 23 ++++++++ src/types.jl | 60 +++++++++++++++---- src/wrappers.jl | 75 +++++++++++++++++++++++- 7 files changed, 229 insertions(+), 21 deletions(-) create mode 100644 docs/src/manual/nodeid.md create mode 100644 docs/src/tutorials/server_first_steps.md diff --git a/README.md b/README.md index 8273abd..953d535 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,64 @@ -# open62541 +# open62541.jl [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://martinkosch.github.io/open62541.jl/dev) [![CI](https://github.com/martinkosch/open62541.jl/actions/workflows/CI.yml/badge.svg)](https://github.com/martinkosch/open62541.jl/actions/workflows/CI.yml) [![codecov](https://codecov.io/gh/martinkosch/open62541.jl/graph/badge.svg?token=lJe2xOTO7g)](https://codecov.io/gh/martinkosch/open62541.jl) [![Aqua QA](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl) + +open62541.jl is a [Julia](https://julialang.org) package that interfaces with the [open62541](https://www.open62541.org/) +library written in C ([source](https://github.com/open62541/open62541)). + +As such, it provides functionality following the [OPC Unified Architecture (OPC UA) standard](https://en.wikipedia.org/wiki/OPC_Unified_Architecture) +for data exchange from sensors to cloud applications developed by the [OPC Foundation](). + +In short, it provides the ability to create OPC servers that make data from different +sources available to clients and, naturally, also a client functionality that allows +to read data from OPC UA servers. Features are summarized further on the [open62541 website](https://www.open62541.org/). + +open62541.jl's *ultimate* aim is to provide the full functionality of open62541 to +Julia users through a convenient high level interface without the need to engage +in manual memory management etc. (as required in open62541). + +At its current development stage the high level interface is implemented for a +(commonly used) subset of functionality. An essentially feature-complete lower +level interface that wraps all functionality of open62541 is, however, available. + +## Warning: active development +Note that open62541.jl is still under active development and has not reached a maturity +that would make it safe to use in a production environment. + +The developers aim to observe [semantic versioning](https://semver.org/), but +accidental breakage and evolutions of the API have to be expected. + +Documentation is also a work in progress. + +## Installation +open62541.jl is not yet registered in Julia's General registry, but it will +hopefully be soon (status: May 2024). + +Assuming you have Julia already installed (otherwise: [JuliaLang Website](https://julialang.org/)), +you can install by executing: +```julia + using Pkg + Pkg.add("https://github.com/martinkosch/open62541.jl") +``` + +## Server example +Starting up a server with a default configuration in open62541.jl is very simple. +Just execute the following code in the REPL or as a script: +```julia + server = JUA_Server() + config = JUA_ServerConfig(server) + JUA_ServerConfig_setDefault(config) + JUA_Server_runUntilInterrupt(server) +``` +This will configure a server with the default configuration (address: opc.tcp://localhost:4840/) + and start it. The server can be shut down by pressing CTRL+C multiple times. + +While running the server it can be accessed, for example, either via the Client +API of open62541.jl or it can be browsed and accessed with a graphical client, +such as [UA Expert website](https://www.unified-automation.com/products/development-tools/uaexpert.html). + +## Basic client example + + diff --git a/docs/make.jl b/docs/make.jl index dcedd31..f23dd9a 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -17,7 +17,8 @@ makedocs(; ), pages = [ "Home" => "index.md", - "Manual" => [ + "Manual" => [ + "manual/nodeid.md", "manual/attributegeneration.md", "manual/server.md", "manual/client.md", @@ -34,7 +35,7 @@ makedocs(; :linkcheck_remotes, :linkcheck, :meta_block, - :missing_docs, + #:missing_docs, :parse_error, :setup_block ), diff --git a/docs/src/index.md b/docs/src/index.md index fd07bf0..9f08590 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -2,13 +2,13 @@ CurrentModule = open62541 ``` -# open62541 +# open62541.jl Documentation for [open62541](https://github.com/martinkosch/open62541.jl). ```@index ``` -```@autodocs + diff --git a/docs/src/manual/nodeid.md b/docs/src/manual/nodeid.md new file mode 100644 index 0000000..bc00b19 --- /dev/null +++ b/docs/src/manual/nodeid.md @@ -0,0 +1,21 @@ +# Nodeid + +This page lists docstrings of functions used to create NodeId identifiers. + +## Low level interface + +test +```@docs +UA_NODEID +UA_NODEID_BYTESTRING_ALLOC +UA_NODEID_GUID +UA_NODEID_NUMERIC +UA_NODEID_STRING +UA_NODEID_STRING_ALLOC +``` + +## High level interface + +```@docs +JUA_NodeId +``` diff --git a/docs/src/tutorials/server_first_steps.md b/docs/src/tutorials/server_first_steps.md new file mode 100644 index 0000000..498dae3 --- /dev/null +++ b/docs/src/tutorials/server_first_steps.md @@ -0,0 +1,23 @@ +```@meta +CurrentModule = open62541 +``` + +# First steps with an open62541 server +Starting up a server with a default configuration in open62541.jl is very simple. +Just execute the following code in the REPL or as a script: +```julia + server = JUA_Server() + config = JUA_ServerConfig(server) + JUA_ServerConfig_setDefault(config) + JUA_Server_runUntilInterrupt(server) +``` +This will configure a server with the default configuration (address: opc.tcp://localhost:4840/) + and start it. The server can be shut down by pressing CTRL+C multiple times. + +While running the server it can be accessed, for example, either via the Client +API of open62541.jl or it can be browsed and accessed with a graphical client, +such as [UA Expert website](https://www.unified-automation.com/products/development-tools/uaexpert.html). + +How to add your own variables, objects, methods, and objects is explained in +subsequent tutorials. + diff --git a/src/types.jl b/src/types.jl index 783ff1a..6cbc24b 100644 --- a/src/types.jl +++ b/src/types.jl @@ -24,6 +24,7 @@ end # ## UA_Array # Julia wrapper for C array types +#TODO: move this to wrappers.jl in the long term; think about interface some more. struct UA_Array{T <: Ptr} <: AbstractArray{T, 1} ptr::T length::Int64 @@ -362,7 +363,7 @@ UA_NODEID(s::AbstractString)::Ptr{UA_NodeId} UA_NODEID(s::Ptr{UA_String})::Ptr{UA_NodeId} ``` -creates a `UA_NodeId` object by parsing `s` (which can be a string or UA_String). +creates a `UA_NodeId` object by parsing `s` (which can be a `String` or `Ptr{UA_String}`). Example: @@ -407,8 +408,11 @@ UA_NODEID_STRING_ALLOC(nsIndex::Integer, identifier::AbstractString)::Ptr{UA_Nod UA_NODEID_STRING_ALLOC(nsIndex::Integer, identifier::Ptr{UA_String})::Ptr{UA_NodeId} ``` -creates a `UA_NodeId` object with namespace index `nsIndex` and string identifier `identifier` (which can be a string or UA_String). -Memory is allocated by C and needs to be cleaned up using `UA_NodeId_delete(x::Ptr{UA_NodeId})` after the object is not used anymore. +creates a `UA_NodeId` object with namespace index `nsIndex` and string identifier +`identifier`. + +Memory is allocated by C and needs to be cleaned up using +`UA_NodeId_delete(x::Ptr{UA_NodeId})` after the object is not used anymore. """ function UA_NODEID_STRING_ALLOC(nsIndex::Integer, identifier::Ptr{UA_String}) nodeid = UA_NodeId_new() @@ -431,8 +435,11 @@ UA_NODEID_STRING(nsIndex::Integer, identifier::AbstractString)::Ptr{UA_NodeId} UA_NODEID_STRING(nsIndex::Integer, identifier::Ptr{UA_String})::Ptr{UA_NodeId} ``` -creates a `UA_NodeId` object by with namespace index `nsIndex` and string identifier `identifier` (which can be a string or UA_String). -Memory is allocated by C and needs to be cleaned up using `UA_NodeId_delete(x::Ptr{UA_NodeId})` after the object is not used anymore. +creates a `UA_NodeId` object by with namespace index `nsIndex` and string identifier +`identifier`. + +Memory is allocated by C and needs to be cleaned up using +`UA_NodeId_delete(x::Ptr{UA_NodeId})` after the object is not used anymore. """ function UA_NODEID_STRING(nsIndex::Integer, identifier::Union{AbstractString, Ptr{UA_String}}) @@ -445,8 +452,11 @@ UA_NODEID_BYTESTRING_ALLOC(nsIndex::Integer, identifier::AbstractString)::Ptr{UA UA_NODEID_BYTESTRING_ALLOC(nsIndex::Integer, identifier::Ptr{UA_ByteString})::Ptr{UA_NodeId} ``` -creates a `UA_NodeId` object with namespace index `nsIndex` and bytestring identifier `identifier` (which can be a string or UA_ByteString). -Memory is allocated by C and needs to be cleaned up using `UA_NodeId_delete(x::Ptr{UA_NodeId})` after the object is not used anymore. +creates a `UA_NodeId` object with namespace index `nsIndex` and bytestring +identifier `identifier` (which can be a string or UA_ByteString). + +Memory is allocated by C and needs to be cleaned up using +`UA_NodeId_delete(x::Ptr{UA_NodeId})` after the object is not used anymore. """ function UA_NODEID_BYTESTRING_ALLOC(nsIndex::Integer, identifier::Ptr{UA_String}) nodeid = UA_NodeId_new() @@ -469,8 +479,11 @@ UA_NODEID_BYTESTRING(nsIndex::Integer, identifier::AbstractString)::Ptr{UA_NodeI UA_NODEID_BYTESTRING(nsIndex::Integer, identifier::Ptr{UA_ByteString})::Ptr{UA_NodeId} ``` -creates a `UA_NodeId` object with namespace index `nsIndex` and bytestring identifier `identifier` (which can be a string or UA_ByteString). -Memory is allocated by C and needs to be cleaned up using `UA_NodeId_delete(x::Ptr{UA_NodeId})` after the object is not used anymore. +creates a `UA_NodeId` object with namespace index `nsIndex` and bytestring +identifier `identifier` (which can be a string or UA_ByteString). + +Memory is allocated by C and needs to be cleaned up using +`UA_NodeId_delete(x::Ptr{UA_NodeId})` after the object is not used anymore. """ function UA_NODEID_BYTESTRING(nsIndex::Integer, identifier::Union{AbstractString, Ptr{UA_String}}) @@ -483,9 +496,12 @@ UA_NODEID_GUID(nsIndex::Integer, identifier::AbstractString)::Ptr{UA_NodeId} UA_NODEID_GUID(nsIndex::Integer, identifier::Ptr{UA_Guid})::Ptr{UA_NodeId} ``` -creates a `UA_NodeId` object by with namespace index `nsIndex` and an identifier `identifier` based on a globally unique id (`UA_Guid`) -that can be supplied as a string (which will be parsed) or as a valid `Ptr{UA_Guid}`. -Memory is allocated by C and needs to be cleaned up using `UA_NodeId_delete(x::Ptr{UA_NodeId})` after the object is not used anymore. +creates a `UA_NodeId` object by with namespace index `nsIndex` and an identifier +`identifier` based on a globally unique id (`UA_Guid`) that can be supplied as a +string (which will be parsed) or as a valid `Ptr{UA_Guid}`. + +Memory is allocated by C and needs to be cleaned up using +`UA_NodeId_delete(x::Ptr{UA_NodeId})` after the object is not used anymore. """ function UA_NODEID_GUID(nsIndex, guid::Ptr{UA_Guid}) nodeid = UA_NodeId_new() @@ -514,6 +530,25 @@ function UA_NodeId_equal(n1, n2) end ## ExpandedNodeId +""" +``` +UA_EXPANDEDNODEID(s::AbstractString)::Ptr{UA_ExpandedNodeId} +UA_EXPANDEDNODEID(s::Ptr{UA_String})::Ptr{UA_ExpandedNodeId} +``` + +creates a `UA_ExpandedNodeId` object by parsing `s`. Memory is allocated by C and +needs to be cleaned up using `UA_ExpandedNodeId_delete(x::Ptr{UA_ExpandedNodeId})` +after the object is not used anymore. + +See also: [OPC Foundation Website](https://reference.opcfoundation.org/Core/Part6/v105/docs/5.2.2.10) + +Example: + +``` +UA_EXPANDEDNODEID("svr=1;nsu=http://example.com;i=1234") #generates UA_ExpandedNodeId with numeric identifier +UA_EXPANDEDNODEID("svr=1;nsu=http://example.com;s=test") #generates UA_ExpandedNodeId with string identifier +``` +""" function UA_EXPANDEDNODEID(s::AbstractString) ua_s = UA_STRING(s) nodeid = UA_EXPANDEDNODEID(ua_s) @@ -658,7 +693,6 @@ function UA_ExpandedNodeId_equal(n1, n2) end ## QualifiedName - function UA_QUALIFIEDNAME_ALLOC(nsIndex::Integer, s::AbstractString) ua_s = UA_STRING(s) qn = UA_QUALIFIEDNAME_ALLOC(nsIndex, ua_s) diff --git a/src/wrappers.jl b/src/wrappers.jl index 51d570b..6429b7e 100644 --- a/src/wrappers.jl +++ b/src/wrappers.jl @@ -86,7 +86,71 @@ const JUA_ServerConfig_clean = UA_ServerConfig_clean const JUA_AccessControl_default = UA_AccessControl_default const JUA_AccessControl_defaultWithLoginCallback = UA_AccessControl_defaultWithLoginCallback +function JUA_Server_runUntilInterrupt(server::JUA_Server) + running = Ref(true) + try + Base.exit_on_sigint(false) + UA_Server_run(server, running) + catch err + running[] = false + println("Shutting down server.") + Base.exit_on_sigint(false) + end + return nothing +end + + ## NodeIds +""" +``` +JUA_NodeId +``` + +creates a `JUA_NodeId` object - the equivalent of a `UA_NodeId`, but with memory +managed by Julia rather than C. + +The following methods are defined: +``` +JUA_NodeId() +``` +creates a `JUA_NodeId` with namespaceIndex = 0, numeric identifierType and +identifier = 0 + +``` +JUA_NodeId(s::Union{AbstractString, JUA_String}) +``` +creates a `JUA_NodeId` based on String `s` that is parsed into the relevant +properties. + +``` +JUA_NodeId(nsIndex::Integer, identifier::Integer) +``` +creates a `JUA_NodeId` with namespace index `nsIndex` and numerical identifier +`identifier`. + +``` +JUA_NodeId(nsIndex::Integer, identifier::Union{AbstractString, JUA_String}) +``` +creates a `JUA_NodeId` with namespace index `nsIndex` and string identifier +`identifier`. + +``` +JUA_NodeId(nsIndex::Integer, identifier::JUA_Guid) +``` +creates a `JUA_NodeId` with namespace index `nsIndex` and global unique id identifier +`identifier`. + +Examples: +``` +j = JUA_NodeId() +j = JUA_NodeId("ns=1;i=1234") +j = JUA_NodeId("ns=1;s=example") +j = JUA_NodeId(1, 1234) +j = JUA_NodeId(1, "example") +j = JUA_NodeId(1, JUA_Guid("C496578A-0DFE-4B8F-870A-745238C6AEAE")) +``` + +""" mutable struct JUA_NodeId <: AbstractOpen62541Wrapper ptr::Ptr{UA_NodeId} @@ -95,8 +159,8 @@ mutable struct JUA_NodeId <: AbstractOpen62541Wrapper finalizer(release_handle, obj) return obj end - function JUA_NodeId(identifier::Union{AbstractString, JUA_String}) - obj = new(UA_NODEID(identifier)) + function JUA_NodeId(s::Union{AbstractString, JUA_String}) + obj = new(UA_NODEID(s)) finalizer(release_handle, obj) return obj end @@ -121,6 +185,13 @@ function release_handle(obj::JUA_NodeId) UA_NodeId_delete(Jpointer(obj)) end +""" +``` +JUA_NodeId_equal(j1::JUA_NodeId, n2::JUA_NodeId)::Bool +``` + +returns `true` if `j1` and `j2` are `JUA_NodeId`s with identical content. +""" JUA_NodeId_equal(j1, j2) = UA_NodeId_equal(j1, j2) #QualifiedName From 8490407ecdf9116cbf4a27f1e17b59aea4a49457 Mon Sep 17 00:00:00 2001 From: Thomas Vetter <80452158+thomvet@users.noreply.github.com> Date: Fri, 10 May 2024 01:22:31 +0200 Subject: [PATCH 06/26] more docs; more tests; worked on high level interface. --- Project.toml | 4 +- README.md | 39 +++++++++- docs/make.jl | 6 +- docs/src/tutorials/client_first_steps.md | 34 +++++++++ docs/src/tutorials/server_first_steps.md | 15 ++-- src/attribute_generation.jl | 22 +++--- src/client.jl | 20 +---- src/exceptions.jl | 2 - src/server.jl | 2 +- src/types.jl | 58 +++++++++------ src/wrappers.jl | 93 +++++++++++++++++++----- test/add_change_var_array.jl | 83 ++++++++++++--------- test/add_change_var_scalar.jl | 30 ++++---- test/data_handling.jl | 4 +- test/runtests.jl | 12 +-- test/server_add_nodes.jl | 2 +- test/server_config.jl | 1 - test/simple_server_client.jl | 20 +++-- 18 files changed, 289 insertions(+), 158 deletions(-) create mode 100644 docs/src/tutorials/client_first_steps.md diff --git a/Project.toml b/Project.toml index 37e1d4e..6830afd 100644 --- a/Project.toml +++ b/Project.toml @@ -17,6 +17,7 @@ Dates = "1.6" Distributed = "1.6" OffsetArrays = "1" Pkg = "1.6" +Random = "1.6" SafeTestsets = "0.1.0" Test = "1.6" julia = "1.6" @@ -27,8 +28,9 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Aqua", "Base64", "Distributed", "Pkg", "SafeTestsets", "Test"] +test = ["Aqua", "Base64", "Distributed", "Pkg", "Random", "SafeTestsets", "Test"] diff --git a/README.md b/README.md index 953d535..3c12b3a 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ you can install by executing: Starting up a server with a default configuration in open62541.jl is very simple. Just execute the following code in the REPL or as a script: ```julia + using open62541 server = JUA_Server() config = JUA_ServerConfig(server) JUA_ServerConfig_setDefault(config) @@ -55,10 +56,40 @@ Just execute the following code in the REPL or as a script: This will configure a server with the default configuration (address: opc.tcp://localhost:4840/) and start it. The server can be shut down by pressing CTRL+C multiple times. -While running the server it can be accessed, for example, either via the Client -API of open62541.jl or it can be browsed and accessed with a graphical client, -such as [UA Expert website](https://www.unified-automation.com/products/development-tools/uaexpert.html). +While the server is running, it can be accessed via the Client API of open62541.jl +or it can be browsed and accessed with a graphical client, such as [UA Expert](https://www.unified-automation.com/products/development-tools/uaexpert.html). ## Basic client example +In order to showcase the Client API functionality, we will use the above server +and read some basic information from it, namely the software version number and +the current time. Note that this information should be contained in all OPC UA +servers, so you could also connect to a different server that you know is running. - +```julia + using open62541 + using Printf + + #initiate client, configure it and connect to server + client = JUA_Client() + config = JUA_ClientConfig(client) + JUA_ClientConfig_setDefault(config) + JUA_Client_connect(client, "opc.tcp://localhost:4840") + + #define nodeids that we are interested in + nodeid_currenttime = JUA_NodeId(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME) + nodeid_version = JUA_NodeId(0, UA_NS0ID_SERVER_SERVERSTATUS_BUILDINFO_SOFTWAREVERSION) + + #read data from nodeids + currenttime = JUA_Client_readValueAttribute(client, nodeid_currenttime) #Int64 which represents the number of 100 nanosecond intervals since January 1, 1601 (UTC) + version = JUA_Client_readValueAttribute(client, nodeid_version) #String containing open62541 version number + + #Convert current time into human understandable format + dts = UA_DateTime_toStruct(currenttime) + + #Print results to terminal + Printf.@printf("current date and time (UTC) is: %u-%u-%u %u:%u:%u.%03u\n", dts.day, dts.month, dts.year, dts.hour, dts.min, dts.sec, dts.milliSec) + Printf.@printf("The server is running open62541 version %s.", version) + + #disconnect the client (good housekeeping practice) + JUA_Client_disconnect(client) +``` diff --git a/docs/make.jl b/docs/make.jl index f23dd9a..4f26439 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -22,7 +22,11 @@ makedocs(; "manual/attributegeneration.md", "manual/server.md", "manual/client.md", - ] + ], + "Tutorials" => [ + "tutorials/server_first_steps.md", + "tutorials/client_first_steps.md" + ] ], warnonly = Documenter.except( :autodocs_block, diff --git a/docs/src/tutorials/client_first_steps.md b/docs/src/tutorials/client_first_steps.md new file mode 100644 index 0000000..2567c39 --- /dev/null +++ b/docs/src/tutorials/client_first_steps.md @@ -0,0 +1,34 @@ +# First steps with an open62541 client +In this tutorial we will connect an OPC client to the server started up in [First steps with an open62541 server](@ref) +and read some basic information from it, namely the software version number and +the current time. Note that this information should be contained in all OPC UA +servers, so you could also connect to a different server that you know is running. + +```julia + using open62541 + using Printf + + #initiate client, configure it and connect to server + client = JUA_Client() + config = JUA_ClientConfig(client) + JUA_ClientConfig_setDefault(config) + JUA_Client_connect(client, "opc.tcp://localhost:4840") + + #define nodeids that we are interested in + nodeid_currenttime = JUA_NodeId(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME) + nodeid_version = JUA_NodeId(0, UA_NS0ID_SERVER_SERVERSTATUS_BUILDINFO_SOFTWAREVERSION) + + #read data from nodeids + currenttime = JUA_Client_readValueAttribute(client, nodeid_currenttime) #Int64 which represents the number of 100 nanosecond intervals since January 1, 1601 (UTC) + version = JUA_Client_readValueAttribute(client, nodeid_version) #String containing open62541 version number + + #Convert current time into human understandable format + dts = UA_DateTime_toStruct(currenttime) + + #Print results to terminal + Printf.@printf("current date and time (UTC) is: %u-%u-%u %u:%u:%u.%03u\n", dts.day, dts.month, dts.year, dts.hour, dts.min, dts.sec, dts.milliSec) + Printf.@printf("The server is running open62541 version %s.", version) + + #disconnect the client (good housekeeping practice) + JUA_Client_disconnect(client) +``` diff --git a/docs/src/tutorials/server_first_steps.md b/docs/src/tutorials/server_first_steps.md index 498dae3..8048a7e 100644 --- a/docs/src/tutorials/server_first_steps.md +++ b/docs/src/tutorials/server_first_steps.md @@ -1,11 +1,8 @@ -```@meta -CurrentModule = open62541 -``` - # First steps with an open62541 server Starting up a server with a default configuration in open62541.jl is very simple. Just execute the following code in the REPL or as a script: ```julia + using open62541 server = JUA_Server() config = JUA_ServerConfig(server) JUA_ServerConfig_setDefault(config) @@ -14,10 +11,8 @@ Just execute the following code in the REPL or as a script: This will configure a server with the default configuration (address: opc.tcp://localhost:4840/) and start it. The server can be shut down by pressing CTRL+C multiple times. -While running the server it can be accessed, for example, either via the Client -API of open62541.jl or it can be browsed and accessed with a graphical client, -such as [UA Expert website](https://www.unified-automation.com/products/development-tools/uaexpert.html). - -How to add your own variables, objects, methods, and objects is explained in -subsequent tutorials. +While the server is running, it can be accessed via the Client API of open62541.jl +or it can be browsed and accessed with a graphical client, such as [UA Expert](https://www.unified-automation.com/products/development-tools/uaexpert.html). +Subsequent tutorials will explain how to add your own variables, objects, and +methods to the server. diff --git a/src/attribute_generation.jl b/src/attribute_generation.jl index d4ab2d7..381e1b9 100644 --- a/src/attribute_generation.jl +++ b/src/attribute_generation.jl @@ -195,7 +195,7 @@ function __set_generic_attributes!(attr, end function __set_scalar_attributes!(attr, value::T, - valuerank) where {T <: Union{AbstractFloat, Integer}} + valuerank) where {T <: Union{AbstractFloat, Integer, Ptr{UA_String}}} type_ptr = ua_data_type_ptr_default(T) attr.valueRank = valuerank UA_Variant_setScalarCopy(attr.value, wrap_ref(value), type_ptr) @@ -204,15 +204,22 @@ end function __set_scalar_attributes!(attr, value::AbstractString, valuerank) ua_s = UA_STRING(value) - type_ptr = ua_data_type_ptr_default(UA_String) - attr.valueRank = valuerank - UA_Variant_setScalarCopy(attr.value, ua_s, type_ptr) + __set_scalar_attributes!(attr, ua_s, valuerank) UA_String_delete(ua_s) return nothing end +function __set_array_attributes!(attr, value::AbstractArray{<:AbstractString}, valuerank) + a = similar(value, UA_String) + for i in eachindex(a) + a[i] = UA_String_fromChars(value[i]) + end + __set_array_attributes!(attr, a, valuerank) + return nothing +end + function __set_array_attributes!(attr, value::AbstractArray{T, N}, - valuerank) where {T, N} + valuerank) where {T <: Union{AbstractFloat, Integer, UA_String}, N} type_ptr = ua_data_type_ptr_default(T) attr.valueRank = valuerank #Note: need array dims twice, once to put into the variant, i.e., attr.value @@ -224,10 +231,7 @@ function __set_array_attributes!(attr, value::AbstractArray{T, N}, attr.arrayDimensions = arraydims_attr attr.arrayDimensionsSize = length(arraydims_attr) ua_arr = UA_Array_new(vec(permutedims(value, reverse(1:N))), type_ptr) # Allocate new UA_Array from value with C style indexing - UA_Variant_setArray(attr.value, - ua_arr, - length(value), - type_ptr) + UA_Variant_setArray(attr.value, ua_arr, length(value), type_ptr) attr.value.arrayDimensions = arraydims_variant attr.value.arrayDimensionsSize = length(arraydims_variant) return nothing diff --git a/src/client.jl b/src/client.jl index 05935fa..ac65a4d 100644 --- a/src/client.jl +++ b/src/client.jl @@ -234,12 +234,9 @@ for att in attributes_UA_Client_read ua_attr_name = Symbol("UA_ATTRIBUTEID_", uppercase(att[2])) @eval begin - #TODO: check whether function signature is correct here; decision on whether carrying wrap-ref signature is desirable. """ ``` - $($(fun_name))(client::Union{Ref{UA_Client}, Ptr{UA_Client}, UA_Client}, - nodeId::Union{Ref{UA_NodeId}, Ptr{UA_NodeId}, UA_NodeId}, - out::Ptr{$($(att[3]))} = $($(String(returnobject)))()) + $($(fun_name))(client::Ptr{UA_Client}, nodeId::Ptr{UA_NodeId}, out::Ptr{$($(att[3]))} = $($(String(returnobject)))()) ``` Uses the UA Client API to read the value of attribute $($(String(attr_name))) from the NodeId `nodeId` accessed through the client `client`. @@ -280,17 +277,13 @@ for att in attributes_UA_Client_write @eval begin """ ``` - $($(fun_name))(client::Union{Ref{UA_Client}, Ptr{UA_Client}, UA_Client}, - nodeId::Union{Ref{UA_NodeId}, Ptr{UA_NodeId}, UA_NodeId}, - new_val::Union{Ref{$($(String(attr_type)))},Ptr{$($(String(attr_type)))}, $($(String(attr_type)))}) + $($(fun_name))(client::Ptr{UA_Client}, nodeId::Ptr{UA_NodeId}, new_val::Ptr{$($(String(attr_type)))}) ``` Uses the UA Client API to write the value `new_val` to the attribute $($(String(attr_name))) of the NodeId `nodeId` accessed through the client `client`. """ - function $(fun_name)(client::Ref{UA_Client}, - nodeId::Ref{UA_NodeId}, - new_attr::Ref{$attr_type}) + function $(fun_name)(client, nodeId, new_attr) data_type_ptr = UA_TYPES_PTRS[$(attr_type_ptr)] statuscode = __UA_Client_writeAttribute(client, nodeId, @@ -311,12 +304,6 @@ for att in attributes_UA_Client_write throw(err) end end - #function fallback that wraps any non-ref arguments into refs: - function $(fun_name)(client, nodeId, new_attr) - return ($fun_name)(wrap_ref(client), - wrap_ref(nodeId), - wrap_ref(new_attr)) - end end end @@ -512,6 +499,7 @@ for att in attributes_UA_Client_write_async end end #function fallback that wraps any non-ref arguments into refs: + #TODO: check whether we still need this or whether we have in the meantime managed to organize things well. function $(fun_name)(client, nodeId, out, callback, userdata, reqId) return $(fun_name)(wrap_ref(client), wrap_ref(nodeId), diff --git a/src/exceptions.jl b/src/exceptions.jl index 1a40bbc..7033737 100644 --- a/src/exceptions.jl +++ b/src/exceptions.jl @@ -47,8 +47,6 @@ function Base.showerror(io::IO, e::CallbackGeneratorArgumentError) unclear which one should be used as basis of the callback." else args_in = fieldtypes(first(methods(e.f)).sig) - @show args_in[2:end] - @show e.f, e.argtuple, methods(e.f), args_in ret = Base.return_types(e.f, args_in[2:end])[1] msg = "Callback generator expected a method with f($(join([e.argtuple...], ", ")))::$(string(e.returntype)), but received a method f($(join(args_in[2:end], ", ")))::$(string(ret))." #TODO: does the job, but it's an ugly error message diff --git a/src/server.jl b/src/server.jl index 45b17af..b0ccfe3 100644 --- a/src/server.jl +++ b/src/server.jl @@ -207,7 +207,7 @@ for att in attributes_UA_Server_write @eval begin """ ``` - $($(fun_name))(server, nodeId, new_val) + $($(fun_name))(server::Ptr{UA_Server}, nodeId::Ptr{UA_NodeId}, new_val::Ptr{$($(String(attr_type)))}) ``` Uses the Server API to write the value `new_val` to the attribute $($(String(attr_name))) of the NodeId `nodeId` located on the `server`. diff --git a/src/types.jl b/src/types.jl index 6cbc24b..99b9bbd 100644 --- a/src/types.jl +++ b/src/types.jl @@ -65,7 +65,7 @@ end function UA_Array_new(v::AbstractArray{T}, type_ptr::Ptr{UA_DataType} = ua_data_type_ptr_default(T)) where {T} v_typed = convert(Vector{juliadatatype(type_ptr)}, vec(v)) # Implicit check if T can be converted to type_ptr - arr_ptr = convert(Ptr{T}, UA_Array_new(length(v), type_ptr)) + arr_ptr = convert(Ptr{juliadatatype(type_ptr)}, UA_Array_new(length(v), type_ptr)) GC.@preserve v_typed arr_ptr unsafe_copyto!(arr_ptr, pointer(v_typed), length(v)) return UA_Array(arr_ptr, length(v)) end @@ -98,6 +98,7 @@ for (i, type_name) in enumerate(type_names) ua_data_type_ptr(::$(val_type)) = UA_TYPES_PTRS[$(i - 1)] if !(type_names[$(i)] in types_ambiguous_ignorelist) ua_data_type_ptr_default(::Type{$(julia_type)}) = UA_TYPES_PTRS[$(i - 1)] + ua_data_type_ptr_default(::Type{Ptr{$julia_type}}) = ua_data_type_ptr_default($julia_type) Base.show(io::IO, ::MIME"text/plain", v::$(julia_type)) = print(io, UA_print(v)) end @@ -281,10 +282,16 @@ function UA_STRING(s::AbstractString) return UA_STRING_ALLOC(s) end -#TODO: think whether this can be cleaned up further -Base.unsafe_string(s::UA_String) = unsafe_string(s.data, s.length) -Base.unsafe_string(s::Ref{UA_String}) = unsafe_string(s[]) -Base.unsafe_string(s::Ptr{UA_String}) = unsafe_string(unsafe_load(s)) +function Base.unsafe_string(s::UA_String) + if s.length == 0 #catch NullString + u = "" + else + u = unsafe_string(s.data, s.length) + end + return u +end +Base.unsafe_string(s::Ptr{UA_String}) = Base.unsafe_string(unsafe_load(s)) + ## UA_BYTESTRING """ @@ -777,39 +784,42 @@ unsafe_size(p::Ref{UA_Variant}) = unsafe_size(unsafe_load(p)) Base.length(v::UA_Variant) = Int(v.arrayLength) Base.length(p::Ref{UA_Variant}) = length(unsafe_load(p)) -function UA_Variant_new_copy(value::AbstractArray{T, N}, - type_ptr::Ptr{UA_DataType} = ua_data_type_ptr_default(T)) where {T, N} +function UA_Variant_new(value::AbstractArray{T, N}, + type_ptr::Ptr{UA_DataType} = ua_data_type_ptr_default(T)) where {T <: Union{AbstractFloat, UA_String, Integer}, N} var = UA_Variant_new() var.type = type_ptr var.storageType = UA_VARIANT_DATA var.arrayLength = length(value) + ua_arr = UA_Array_new(vec(permutedims(value, reverse(1:N))), type_ptr) # Allocate new UA_Array from value with C style indexing + UA_Variant_setArray(var, ua_arr, length(value), type_ptr) var.arrayDimensionsSize = length(size(value)) - var.data = UA_Array_new(vec(permutedims(value, reverse(1:N))), type_ptr) var.arrayDimensions = UA_UInt32_Array_new(reverse(size(value))) return var end -function UA_Variant_new_copy(value::Ref{T}, +function UA_Variant_new(value::T, type_ptr::Ptr{UA_DataType} = ua_data_type_ptr_default(T)) where {T <: Union{ - AbstractFloat, Integer}} + AbstractFloat, Integer, Ptr{UA_String}}} var = UA_Variant_new() var.type = type_ptr var.storageType = UA_VARIANT_DATA - var.arrayLength = 0 - var.arrayDimensionsSize = length(size(value)) - UA_Variant_setScalarCopy(var, value, type_ptr) - var.arrayDimensions = C_NULL + UA_Variant_setScalarCopy(var, wrap_ref(value), type_ptr) return var end -function UA_Variant_new_copy(value::T, - type_ptr::Ptr{UA_DataType} = ua_data_type_ptr_default(T)) where {T <: Union{ - AbstractFloat, Integer}} - return UA_Variant_new_copy(Ref(value), type_ptr) +function UA_Variant_new(value::AbstractString) + ua_s = UA_STRING(value) + v = UA_Variant_new(ua_s) + UA_String_delete(ua_s) + return v end -function UA_Variant_new_copy(value, type_sym::Symbol) - UA_Variant_new_copy(value, ua_data_type_ptr(Val(type_sym))) +function UA_Variant_new(value::AbstractArray{<:AbstractString}) + a = similar(value, UA_String) + for i in eachindex(a) + a[i] = UA_String_fromChars(value[i]) + end + return UA_Variant_new(a) end function Base.unsafe_wrap(v::UA_Variant) @@ -824,11 +834,11 @@ function Base.unsafe_wrap(v::UA_Variant) end end -Base.unsafe_wrap(p::Ref{UA_Variant}) = unsafe_wrap(unsafe_load(p)) +Base.unsafe_wrap(p::Ptr{UA_Variant}) = unsafe_wrap(unsafe_load(p)) UA_Variant_isEmpty(v::UA_Variant) = v.type == C_NULL -UA_Variant_isEmpty(p::Ref{UA_Variant}) = UA_Variant_isEmpty(unsafe_load(p)) -UA_Variant_isScalar(v::UA_Variant) = v.arrayLength == 0 && v.data > UA_EMPTY_ARRAY_SENTINEL -UA_Variant_isScalar(p::Ref{UA_Variant}) = UA_Variant_isScalar(unsafe_load(p)) +UA_Variant_isEmpty(p::Ptr{UA_Variant}) = UA_Variant_isEmpty(unsafe_load(p)) +UA_Variant_isScalar(v::UA_Variant) = v.arrayLength == 0 #TODO: this fails when strings are present in the variant, why? && v.data > UA_EMPTY_ARRAY_SENTINEL +UA_Variant_isScalar(p::Ptr{UA_Variant}) = UA_Variant_isScalar(unsafe_load(p)) function UA_Variant_hasScalarType(v::UA_Variant, type::Ref{UA_DataType}) return UA_Variant_isScalar(v) && type == v.type diff --git a/src/wrappers.jl b/src/wrappers.jl index 6429b7e..5109fe2 100644 --- a/src/wrappers.jl +++ b/src/wrappers.jl @@ -69,10 +69,6 @@ mutable struct JUA_ServerConfig <: AbstractOpen62541Wrapper end end -function JUA_Server_getConfig(server::JUA_Server) - return JUA_ServerConfig(server) -end - #aliasing functions that interact with server and serverconfig const JUA_ServerConfig_setMinimalCustomBuffer = UA_ServerConfig_setMinimalCustomBuffer const JUA_ServerConfig_setMinimal = UA_ServerConfig_setMinimal @@ -92,13 +88,40 @@ function JUA_Server_runUntilInterrupt(server::JUA_Server) Base.exit_on_sigint(false) UA_Server_run(server, running) catch err - running[] = false + UA_Server_run_shutdown(server) println("Shutting down server.") - Base.exit_on_sigint(false) + Base.exit_on_sigint(true) end return nothing end +## Client and client config +mutable struct JUA_Client <: AbstractOpen62541Wrapper + ptr::Ptr{UA_Client} + function JUA_Client() + obj = new(UA_Client_new()) + finalizer(release_handle, obj) + return obj + end +end + +function release_handle(obj::JUA_Client) + UA_Client_delete(Jpointer(obj)) +end + +mutable struct JUA_ClientConfig <: AbstractOpen62541Wrapper + ptr::Ptr{UA_ClientConfig} + function JUA_ClientConfig(client::JUA_Client) + #no finalizer, because the config object lives and dies with the client itself + obj = new(UA_Client_getConfig(client)) + return obj + end +end + +const JUA_ClientConfig_setDefault = UA_ClientConfig_setDefault +const JUA_Client_connect = UA_Client_connect +const JUA_Client_disconnect = UA_Client_disconnect + ## NodeIds """ @@ -221,23 +244,53 @@ Base.convert(::Type{UA_QualifiedName}, x::JUA_QualifiedName) = unsafe_load(Jpoin # end # function release_handle(obj::JUA_VariableAttributes) -# UA_QualifiedName_delete(Jpointer(obj)) +# UA_VariableAttributes_delete(Jpointer(obj)) # end #TODO: here we can do automatic unsafe_wrap on the content, so that the user doesn't have to do it. -# mutable struct JUA_Variant{T} <: AbstractOpen62541Wrapper -# ptr::Ptr{UA_Variant} -# v::T +mutable struct JUA_Variant <: AbstractOpen62541Wrapper + ptr::Ptr{UA_Variant} + function JUA_Variant() + obj = new(UA_Variant_new()) + finalizer(release_handle, obj) + return obj + end +end -# function JUA_Variant() -# obj = new(UA_Variant_new()) -# finalizer(release_handle, obj) -# return obj -# end -# end +function release_handle(obj::JUA_Variant) + UA_Variant_delete(Jpointer(obj)) +end -# function release_handle(obj::JUA_Variant) -# UA_Variant_delete(Jpointer(obj)) -# end +function JUA_Client_readValueAttribute(client, nodeId) + #TODO: Is there a way of making this typestable? + #(it's not really known what kind of data is stored inside a nodeid unless + #one checks the datatype beforehand) + v = UA_Variant_new() + UA_Client_readValueAttribute(client, nodeId, v) + r = __get_juliavalues_from_variant(v) + UA_Variant_delete(v) + return r +end -#function JUA_Client_readValueAttribute(client, nodeId, out = JUA_Variant()) +function JUA_Server_readValue(client, nodeId) + #TODO: Is there a way of making this typestable? + #(it's not really known what kind of data is stored inside a nodeid unless + #one checks the datatype beforehand) + v = UA_Variant_new() + UA_Server_readValue(client, nodeId, v) + r = __get_juliavalues_from_variant(v) + UA_Variant_delete(v) + return r +end + +function __get_juliavalues_from_variant(v) + wrapped = unsafe_wrap(v) + if typeof(wrapped) <: Array && eltype(wrapped) == UA_String + r = unsafe_string.(wrapped) + elseif typeof(wrapped) == UA_String + r = unsafe_string(wrapped) + else + r = deepcopy(wrapped) + end + return r +end diff --git a/test/add_change_var_array.jl b/test/add_change_var_array.jl index 55c8e54..d2167bf 100644 --- a/test/add_change_var_array.jl +++ b/test/add_change_var_array.jl @@ -2,32 +2,32 @@ # different types can be created on a server, read, changed and read again (using the server commands and client commands) # We also check that setting a variable node with one type cannot be set to another type (e.g., integer variable node cannot be # set to float64.) + +#TODO: Improve memory management, also test with high level interface. + using Distributed Distributed.addprocs(1) # Add a single worker process to run the server Distributed.@everywhere begin using open62541 using Test + using Random # What types and sizes we are testing for: - types = [Int64, Float64, Bool, Float32, Int32, Int16] + types = [Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float32, Float64, String] array_sizes = (11, (2, 5), (3, 4, 5), (3, 4, 5, 6)) # Generate random default node values and ids - input_data = Tuple(Tuple(rand(type, array_size) for array_size in array_sizes) - for type in types) + input_data = Tuple(Tuple( type != String ? rand(type, array_size) : reshape([randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], array_size...) for array_size in array_sizes) for type in types) varnode_ids = ["$(string(array_size)) $(Symbol(type)) array variable" for type in types, array_size in array_sizes] end # Create nodes with random default values on new server running at a worker process Distributed.@spawnat Distributed.workers()[end] begin - server = UA_Server_new() - retval = UA_ServerConfig_setMinimalCustomBuffer(UA_Server_getConfig(server), - 4842, - C_NULL, - 0, - 0) + server = JUA_Server() + retval = JUA_ServerConfig_setMinimalCustomBuffer(JUA_ServerConfig(server), + 4842, C_NULL, 0, 0) @test retval == UA_STATUSCODE_GOOD # Add variable node containing an array to the server @@ -40,10 +40,10 @@ Distributed.@spawnat Distributed.workers()[end] begin displayname = varnode_ids[type_ind, array_size_ind], description = "this is a $(string(array_size)) $(Symbol(type)) array variable", accesslevel = accesslevel) - varnodeid = UA_NODEID_STRING_ALLOC(1, varnode_ids[type_ind, array_size_ind]) - parentnodeid = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER) - parentreferencenodeid = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES) - typedefinition = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE) + varnodeid = JUA_NodeId(1, varnode_ids[type_ind, array_size_ind]) + parentnodeid = JUA_NodeId(0, UA_NS0ID_OBJECTSFOLDER) + parentreferencenodeid = JUA_NodeId(0, UA_NS0ID_ORGANIZES) + typedefinition = JUA_NodeId(0, UA_NS0ID_BASEDATAVARIABLETYPE) browsename = UA_QUALIFIEDNAME_ALLOC(1, varnode_ids[type_ind, array_size_ind]) nodecontext = C_NULL outnewnodeid = C_NULL @@ -53,8 +53,12 @@ Distributed.@spawnat Distributed.workers()[end] begin # Test whether adding node to the server worked @test retval == UA_STATUSCODE_GOOD # Test whether the correct array is within the server (read from server) - output_server = unsafe_wrap(UA_Server_readValue(server, varnodeid)) - @test all(isapprox.(input, output_server)) + output_server = JUA_Server_readValue(server, varnodeid) + if type <: AbstractFloat + @test all(isapprox.(input, output_server)) + else + @test all(input .== output_server) + end end end @@ -65,14 +69,14 @@ Distributed.@spawnat Distributed.workers()[end] begin end # Specify client and connect to server after server startup -client = UA_Client_new() -UA_ClientConfig_setDefault(UA_Client_getConfig(client)) +client = JUA_Client() +JUA_ClientConfig_setDefault(JUA_ClientConfig(client)) max_duration = 40.0 # Maximum waiting time for server startup sleep_time = 2.0 # Sleep time in seconds between each connection trial let trial trial = 0 while trial < max_duration / sleep_time - retval = UA_Client_connect(client, "opc.tcp://localhost:4842") + retval = JUA_Client_connect(client, "opc.tcp://localhost:4842") if retval == UA_STATUSCODE_GOOD println("Connection established.") break @@ -87,42 +91,49 @@ end for (type_ind, type) in enumerate(types) for (array_size_ind, array_size) in enumerate(array_sizes) input = input_data[type_ind][array_size_ind] - varnodeid = UA_NODEID_STRING_ALLOC(1, varnode_ids[type_ind, array_size_ind]) - output_client = unsafe_wrap(UA_Client_readValueAttribute(client, varnodeid)) - @test all(isapprox.(input, output_client)) + varnodeid = JUA_NodeId(1, varnode_ids[type_ind, array_size_ind]) + output_client = JUA_Client_readValueAttribute(client, varnodeid) + if type <: AbstractFloat + @test all(isapprox.(input, output_client)) + else + @test all(input .== output_client) + end end end # Write new data for (type_ind, type) in enumerate(types) for (array_size_ind, array_size) in enumerate(array_sizes) - new_input = rand(type, array_size) - varnodeid = UA_NODEID_STRING_ALLOC(1, varnode_ids[type_ind, array_size_ind]) - retval = UA_Client_writeValueAttribute(client, - varnodeid, - UA_Variant_new_copy(new_input)) + new_input = type != String ? rand(type, array_size) : reshape([randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], array_size...) + varnodeid = JUA_NodeId(1, varnode_ids[type_ind, array_size_ind]) + new_v = UA_Variant_new(new_input) #TODO: need to come up with a good interface for the write functions as well. + retval = UA_Client_writeValueAttribute(client, varnodeid, new_v) @test retval == UA_STATUSCODE_GOOD - - output_client_new = unsafe_wrap(UA_Client_readValueAttribute(client, varnodeid)) - @test all(isapprox.(new_input, output_client_new)) + output_client_new = JUA_Client_readValueAttribute(client, varnodeid) + if type <: AbstractFloat + @test all(isapprox.(new_input, output_client_new)) + else + @test all(new_input .== output_client_new) + end + UA_Variant_delete(new_v) end end # Test wrong data type write errors for type_ind in eachindex(types) for (array_size_ind, array_size) in enumerate(array_sizes) - new_input = rand(types[mod(type_ind, length(types)) + 1]) # Select wrong data type - varnodeid = UA_NODEID_STRING_ALLOC(1, varnode_ids[type_ind, array_size_ind]) + type = types[mod(type_ind, length(types)) + 1] # Select wrong data type + new_input = type != String ? rand(type, array_size) : reshape([randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], array_size...) + varnodeid = JUA_NodeId(1, varnode_ids[type_ind, array_size_ind]) + new_v = UA_Variant_new(new_input) @test_throws open62541.AttributeReadWriteError UA_Client_writeValueAttribute( - client, - varnodeid, - UA_Variant_new_copy(new_input)) + client, varnodeid, new_v) + UA_Variant_delete(new_v) end end # Disconnect client -UA_Client_disconnect(client) -UA_Client_delete(client) +JUA_Client_disconnect(client) println("Ungracefully kill server process...") Distributed.interrupt(Distributed.workers()[end]) diff --git a/test/add_change_var_scalar.jl b/test/add_change_var_scalar.jl index 906b7f4..4155935 100644 --- a/test/add_change_var_scalar.jl +++ b/test/add_change_var_scalar.jl @@ -1,7 +1,11 @@ # Purpose: This testset checks whether variable nodes containing a scalar of -# different types can be created on a server, read, changed and read again (using the server commands and client commands) -# We also check that setting a variable node with one type cannot be set to another type (e.g., integer variable node cannot be -# set to float64.) +# different types can be created on a server, read, changed and read again +# (using the server commands and client commands APIs) +# We also check that setting a variable node with one type cannot be set to +# another type (e.g., integer variable node cannot be set to float64.) + +#TODO: must test with string as well; improve memory handling and update to high level interface + using Distributed Distributed.addprocs(1) # Add a single worker process to run the server @@ -18,10 +22,7 @@ end Distributed.@spawnat Distributed.workers()[end] begin server = UA_Server_new() retval = UA_ServerConfig_setMinimalCustomBuffer(UA_Server_getConfig(server), - 4842, - C_NULL, - 0, - 0) + 4842, C_NULL, 0, 0) @test retval == UA_STATUSCODE_GOOD # Add variable node containing a scalar to the server @@ -80,28 +81,31 @@ for (type_ind, type) in enumerate(types) varnodeid = UA_NODEID_STRING_ALLOC(1, varnode_ids[type_ind]) output_client = unsafe_wrap(UA_Client_readValueAttribute(client, varnodeid)) @test all(isapprox.(input, output_client)) + UA_NodeId_delete(varnodeid) end # Write new data for (type_ind, type) in enumerate(types) new_input = rand(type) varnodeid = UA_NODEID_STRING_ALLOC(1, varnode_ids[type_ind]) - retval = UA_Client_writeValueAttribute(client, - varnodeid, - UA_Variant_new_copy(new_input)) + new_v = UA_Variant_new(new_input) + retval = UA_Client_writeValueAttribute(client, varnodeid, new_v) @test retval == UA_STATUSCODE_GOOD - output_client_new = unsafe_wrap(UA_Client_readValueAttribute(client, varnodeid)) @test all(isapprox.(new_input, output_client_new)) + UA_Variant_delete(new_v) + UA_NodeId_delete(varnodeid) end # Test wrong data type write errors for type_ind in eachindex(types) new_input = rand(types[mod(type_ind, length(types)) + 1]) # Select wrong data type varnodeid = UA_NODEID_STRING_ALLOC(1, varnode_ids[type_ind]) + new_v = UA_Variant_new(new_input) @test_throws open62541.AttributeReadWriteError UA_Client_writeValueAttribute(client, - varnodeid, - UA_Variant_new_copy(new_input)) + varnodeid, new_v) + UA_Variant_delete(new_v) + UA_NodeId_delete(varnodeid) end # Disconnect client diff --git a/test/data_handling.jl b/test/data_handling.jl index c88fdeb..2d2ec32 100644 --- a/test/data_handling.jl +++ b/test/data_handling.jl @@ -52,7 +52,7 @@ UA_NodeId_delete(id3) # Variants # Set a scalar value -v = UA_Variant_new_copy(Int32(42)) +v = UA_Variant_new(Int32(42)) # Make a copy v2 = UA_Variant_new() @@ -67,7 +67,7 @@ UA_Variant_delete(v2) # Set an array value d = Float64[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] -v3 = UA_Variant_new_copy(d) +v3 = UA_Variant_new(d) Base.promote_rule(::Type{T}, ::Type{UA_Double}) where {T <: AbstractFloat} = Float64 @test all(isapprox.(d, unsafe_wrap(v3))) diff --git a/test/runtests.jl b/test/runtests.jl index 599850a..abd27d9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -49,9 +49,9 @@ end include("server_add_nodes.jl") end -@safetestset "Memory leaks" begin - include("memoryleaks.jl") -end +# @safetestset "Memory leaks" begin +# include("memoryleaks.jl") +# end #Testsets below here use Distributed; normal testsets required # !!! Leakage of variables must be assessed manually. !!! @@ -60,9 +60,9 @@ end include("simple_server_client.jl") end -@testset "Add, read, change scalar variables" begin - include("add_change_var_scalar.jl") -end +# @testset "Add, read, change scalar variables" begin +# include("add_change_var_scalar.jl") +# end @testset "Add, read, change array variables" begin include("add_change_var_array.jl") diff --git a/test/server_add_nodes.jl b/test/server_add_nodes.jl index 54f4acc..92cccfd 100644 --- a/test/server_add_nodes.jl +++ b/test/server_add_nodes.jl @@ -344,7 +344,7 @@ UA_CallMethodResult_delete(answer) #Now test with the higher level JUA interface as well #configure server server2 = JUA_Server() -retvalj0 = JUA_ServerConfig_setMinimalCustomBuffer(JUA_Server_getConfig(server2), +retvalj0 = JUA_ServerConfig_setMinimalCustomBuffer(JUA_ServerConfig(server2), 4842, C_NULL, 0, 0) @test retvalj0 == UA_STATUSCODE_GOOD diff --git a/test/server_config.jl b/test/server_config.jl index efa0b16..3b97fa5 100644 --- a/test/server_config.jl +++ b/test/server_config.jl @@ -18,7 +18,6 @@ UA_Server_delete(server1) # JUA Interface server2 = JUA_Server() config2 = JUA_ServerConfig(server2) -config3 = JUA_Server_getConfig(server2) retval4 = JUA_ServerConfig_setMinimalCustomBuffer(config2, 4842, C_NULL, 0, 0) retval5 = JUA_ServerConfig_setMinimal(config2, 4842, C_NULL) retval6 = JUA_ServerConfig_setDefault(config2) diff --git a/test/simple_server_client.jl b/test/simple_server_client.jl index eed560e..27231df 100644 --- a/test/simple_server_client.jl +++ b/test/simple_server_client.jl @@ -9,17 +9,17 @@ end # Create a new server running at a worker process Distributed.@spawnat Distributed.workers()[end] begin - server = UA_Server_new() - retval1 = UA_ServerConfig_setMinimalCustomBuffer(UA_Server_getConfig(server), + server = JUA_Server() + retval1 = JUA_ServerConfig_setMinimalCustomBuffer(JUA_ServerConfig(server), 4842, C_NULL, 0, 0) @test retval1 == UA_STATUSCODE_GOOD - @test isa(server, Ptr{UA_Server}) - UA_Server_run(server, Ref(true)) + @test isa(server, JUA_Server) + JUA_Server_runUntilInterrupt(server) end # Specify client and connect to server after server startup -client = UA_Client_new() -UA_ClientConfig_setDefault(UA_Client_getConfig(client)) +client = JUA_Client() +JUA_ClientConfig_setDefault(JUA_ClientConfig(client)) max_duration = 30.0 # Maximum waiting time for server startup sleep_time = 2.0 # Sleep time in seconds between each connection trial let trial @@ -37,11 +37,10 @@ let trial end # nodeid containing software version running on server -nodeid = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_BUILDINFO_SOFTWAREVERSION) +nodeid = JUA_NodeId(0, UA_NS0ID_SERVER_SERVERSTATUS_BUILDINFO_SOFTWAREVERSION) # Read nodeid from server -raw_version = UA_Client_readValueAttribute(client, nodeid) -open62541_version_server = unsafe_string(unsafe_wrap(raw_version)) +open62541_version_server = JUA_Client_readValueAttribute(client, nodeid) # Do version numbers agree? vn2string(vn::VersionNumber) = "$(vn.major).$(vn.minor).$(vn.patch)" @@ -59,8 +58,7 @@ open62541_version_julia = vn2string(open62541_version_julia) @test open62541_version_server == open62541_version_julia # Disconnect client -UA_Client_disconnect(client) -UA_Client_delete(client) +JUA_Client_disconnect(client) println("Ungracefully kill server process...") Distributed.interrupt(Distributed.workers()[end]) From ff4ae373fd8bdbd2cc2445456a806862944a18c9 Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Sun, 12 May 2024 21:05:37 +0200 Subject: [PATCH 07/26] test more types in variable nodes (incl. string). --- docs/make.jl | 1 + docs/src/manual/numbertypes.md | 12 ++++++ test/add_change_var_array.jl | 22 +++++----- test/add_change_var_scalar.jl | 79 ++++++++++++++++++++-------------- test/runtests.jl | 6 +-- 5 files changed, 74 insertions(+), 46 deletions(-) create mode 100644 docs/src/manual/numbertypes.md diff --git a/docs/make.jl b/docs/make.jl index 4f26439..b7ab09c 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -18,6 +18,7 @@ makedocs(; pages = [ "Home" => "index.md", "Manual" => [ + "manual/numbertypes.md", "manual/nodeid.md", "manual/attributegeneration.md", "manual/server.md", diff --git a/docs/src/manual/numbertypes.md b/docs/src/manual/numbertypes.md new file mode 100644 index 0000000..426c56e --- /dev/null +++ b/docs/src/manual/numbertypes.md @@ -0,0 +1,12 @@ +# Supported number types + +It is noteworthy that the open62541 library supports the following Julia +number types natively. open62541.jl provides support for the same number types. +Adding other types is possible, but must rely on a custom datatype. See the [open62541 documentation](https://github.com/open62541/open62541/tree/master/examples/custom_datatype). + +**Real:** +- Integers: Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64. +- Float: Float32 and Float64. + +**Complex:** +- Complex{Float32}, Complex{Float64} diff --git a/test/add_change_var_array.jl b/test/add_change_var_array.jl index d2167bf..41addde 100644 --- a/test/add_change_var_array.jl +++ b/test/add_change_var_array.jl @@ -1,7 +1,11 @@ -# Purpose: This testset checks whether variable nodes containing arrays (1, 2, 3, 4 dimensions) of -# different types can be created on a server, read, changed and read again (using the server commands and client commands) -# We also check that setting a variable node with one type cannot be set to another type (e.g., integer variable node cannot be -# set to float64.) +# Purpose: This testset checks whether variable nodes containing an array of +# different types can be created on a server, read, changed and read again +# (using the server and client APIs) +# We also check that setting a variable node with one type cannot be set to +# another type (e.g., integer variable node cannot be set to float64.) + +#Types tested: Bool, Int8/16/32/64, UInt8/16/32/64, Float32/64, String + #TODO: Improve memory management, also test with high level interface. @@ -9,15 +13,13 @@ using Distributed Distributed.addprocs(1) # Add a single worker process to run the server Distributed.@everywhere begin - using open62541 - using Test - using Random + using open62541, Test, Random # What types and sizes we are testing for: types = [Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float32, Float64, String] array_sizes = (11, (2, 5), (3, 4, 5), (3, 4, 5, 6)) - # Generate random default node values and ids + # Generate random input values and generate nodeid names input_data = Tuple(Tuple( type != String ? rand(type, array_size) : reshape([randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], array_size...) for array_size in array_sizes) for type in types) varnode_ids = ["$(string(array_size)) $(Symbol(type)) array variable" for type in types, array_size in array_sizes] @@ -44,7 +46,7 @@ Distributed.@spawnat Distributed.workers()[end] begin parentnodeid = JUA_NodeId(0, UA_NS0ID_OBJECTSFOLDER) parentreferencenodeid = JUA_NodeId(0, UA_NS0ID_ORGANIZES) typedefinition = JUA_NodeId(0, UA_NS0ID_BASEDATAVARIABLETYPE) - browsename = UA_QUALIFIEDNAME_ALLOC(1, varnode_ids[type_ind, array_size_ind]) + browsename = JUA_QualifiedName(1, varnode_ids[type_ind, array_size_ind]) nodecontext = C_NULL outnewnodeid = C_NULL retval = UA_Server_addVariableNode(server, varnodeid, parentnodeid, @@ -65,7 +67,7 @@ Distributed.@spawnat Distributed.workers()[end] begin # Start up the server Distributed.@spawnat Distributed.workers()[end] redirect_stderr() # Turn off all error messages println("Starting up the server...") - UA_Server_run(server, Ref(true)) + JUA_Server_runUntilInterrupt(server) end # Specify client and connect to server after server startup diff --git a/test/add_change_var_scalar.jl b/test/add_change_var_scalar.jl index 4155935..a53f74b 100644 --- a/test/add_change_var_scalar.jl +++ b/test/add_change_var_scalar.jl @@ -1,27 +1,31 @@ # Purpose: This testset checks whether variable nodes containing a scalar of # different types can be created on a server, read, changed and read again -# (using the server commands and client commands APIs) +# (using the server and client APIs) # We also check that setting a variable node with one type cannot be set to # another type (e.g., integer variable node cannot be set to float64.) -#TODO: must test with string as well; improve memory handling and update to high level interface +#Types tested: Bool, Int8/16/32/64, UInt8/16/32/64, Float32/64, String + +#TODO: improve memory handling and update to high level interface using Distributed Distributed.addprocs(1) # Add a single worker process to run the server Distributed.@everywhere begin - using open62541 - using Test + using open62541, Test, Random - types = [Int16, Int32, Int64, Float32, Float64, Bool] - input_data = Tuple(rand(type) for type in types) + # What types we are testing for: + types = [Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float32, Float64, String] + + # Generate random input values and generate nodeid names + input_data = Tuple(type != String ? rand(type) : randstring(Int64(rand(UInt8))) for type in types) varnode_ids = ["$(Symbol(type)) scalar variable" for type in types] end # Create nodes with random default values on new server running at a worker process Distributed.@spawnat Distributed.workers()[end] begin - server = UA_Server_new() - retval = UA_ServerConfig_setMinimalCustomBuffer(UA_Server_getConfig(server), + server = JUA_Server() + retval = JUA_ServerConfig_setMinimalCustomBuffer(JUA_ServerConfig(server), 4842, C_NULL, 0, 0) @test retval == UA_STATUSCODE_GOOD @@ -33,11 +37,11 @@ Distributed.@spawnat Distributed.workers()[end] begin displayname = varnode_ids[type_ind], description = "this is a $(Symbol(type)) scalar variable", accesslevel = accesslevel) - varnodeid = UA_NODEID_STRING_ALLOC(1, varnode_ids[type_ind]) - parentnodeid = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER) - parentreferencenodeid = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES) - typedefinition = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE) - browsename = UA_QUALIFIEDNAME_ALLOC(1, varnode_ids[type_ind]) + varnodeid = JUA_NodeId(1, varnode_ids[type_ind]) + parentnodeid = JUA_NodeId(0, UA_NS0ID_OBJECTSFOLDER) + parentreferencenodeid = JUA_NodeId(0, UA_NS0ID_ORGANIZES) + typedefinition = JUA_NodeId(0, UA_NS0ID_BASEDATAVARIABLETYPE) + browsename = JUA_QualifiedName(1, varnode_ids[type_ind]) nodecontext = C_NULL outnewnodeid = C_NULL retval = UA_Server_addVariableNode(server, varnodeid, parentnodeid, @@ -46,25 +50,29 @@ Distributed.@spawnat Distributed.workers()[end] begin # Test whether adding node to the server worked @test retval == UA_STATUSCODE_GOOD # Test whether the correct array is within the server (read from server) - output_server = unsafe_wrap(UA_Server_readValue(server, varnodeid)) - @test isapprox(input, output_server) + output_server = JUA_Server_readValue(server, varnodeid) + if type <: AbstractFloat + @test all(isapprox.(input, output_server)) + else + @test all(input .== output_server) + end end # Start up the server Distributed.@spawnat Distributed.workers()[end] redirect_stderr() # Turn off all error messages println("Starting up the server...") - UA_Server_run(server, Ref(true)) + JUA_Server_runUntilInterrupt(server) end # Specify client and connect to server after server startup -client = UA_Client_new() -UA_ClientConfig_setDefault(UA_Client_getConfig(client)) +client = JUA_Client() +JUA_ClientConfig_setDefault(JUA_ClientConfig(client)) max_duration = 40.0 # Maximum waiting time for server startup sleep_time = 2.0 # Sleep time in seconds between each connection trial let trial trial = 0 while trial < max_duration / sleep_time - retval = UA_Client_connect(client, "opc.tcp://localhost:4842") + retval = JUA_Client_connect(client, "opc.tcp://localhost:4842") if retval == UA_STATUSCODE_GOOD println("Connection established.") break @@ -78,39 +86,44 @@ end # Read with client from server for (type_ind, type) in enumerate(types) input = input_data[type_ind] - varnodeid = UA_NODEID_STRING_ALLOC(1, varnode_ids[type_ind]) - output_client = unsafe_wrap(UA_Client_readValueAttribute(client, varnodeid)) - @test all(isapprox.(input, output_client)) - UA_NodeId_delete(varnodeid) + varnodeid = JUA_NodeId(1, varnode_ids[type_ind]) + output_client = JUA_Client_readValueAttribute(client, varnodeid) + if type <: AbstractFloat + @test all(isapprox.(input, output_client)) + else + @test all(input .== output_client) + end end # Write new data for (type_ind, type) in enumerate(types) - new_input = rand(type) - varnodeid = UA_NODEID_STRING_ALLOC(1, varnode_ids[type_ind]) + new_input = type != String ? rand(type) : randstring(Int64(rand(UInt8))) + varnodeid = JUA_NodeId(1, varnode_ids[type_ind]) new_v = UA_Variant_new(new_input) retval = UA_Client_writeValueAttribute(client, varnodeid, new_v) @test retval == UA_STATUSCODE_GOOD - output_client_new = unsafe_wrap(UA_Client_readValueAttribute(client, varnodeid)) - @test all(isapprox.(new_input, output_client_new)) + output_client_new = JUA_Client_readValueAttribute(client, varnodeid) + if type <: AbstractFloat + @test all(isapprox.(new_input, output_client_new)) + else + @test all(new_input .== output_client_new) + end UA_Variant_delete(new_v) - UA_NodeId_delete(varnodeid) end # Test wrong data type write errors for type_ind in eachindex(types) - new_input = rand(types[mod(type_ind, length(types)) + 1]) # Select wrong data type - varnodeid = UA_NODEID_STRING_ALLOC(1, varnode_ids[type_ind]) + type = types[mod(type_ind, length(types)) + 1] # Select wrong data type + new_input = type != String ? rand(type) : randstring(Int64(rand(UInt8))) + varnodeid = JUA_NodeId(1, varnode_ids[type_ind]) new_v = UA_Variant_new(new_input) @test_throws open62541.AttributeReadWriteError UA_Client_writeValueAttribute(client, varnodeid, new_v) UA_Variant_delete(new_v) - UA_NodeId_delete(varnodeid) end # Disconnect client -UA_Client_disconnect(client) -UA_Client_delete(client) +JUA_Client_disconnect(client) println("Ungracefully kill server process...") Distributed.interrupt(Distributed.workers()[end]) diff --git a/test/runtests.jl b/test/runtests.jl index abd27d9..f063f72 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -60,9 +60,9 @@ end include("simple_server_client.jl") end -# @testset "Add, read, change scalar variables" begin -# include("add_change_var_scalar.jl") -# end +@testset "Add, read, change scalar variables" begin + include("add_change_var_scalar.jl") +end @testset "Add, read, change array variables" begin include("add_change_var_array.jl") From 07d8ddfd9cf50a51dfe99a1da7b25458258f3192 Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Mon, 13 May 2024 10:50:23 +0200 Subject: [PATCH 08/26] fix array tests for complex numbers and strings --- src/attribute_generation.jl | 27 ++++++++++++++++++++++++--- src/types.jl | 23 +++++++++++++++++++++-- src/wrappers.jl | 28 +++++++++++++++++++++++++--- test/add_change_var_array.jl | 14 ++++++++++---- test/add_change_var_scalar.jl | 12 +++++++----- 5 files changed, 87 insertions(+), 17 deletions(-) diff --git a/src/attribute_generation.jl b/src/attribute_generation.jl index 381e1b9..50e1e16 100644 --- a/src/attribute_generation.jl +++ b/src/attribute_generation.jl @@ -195,7 +195,7 @@ function __set_generic_attributes!(attr, end function __set_scalar_attributes!(attr, value::T, - valuerank) where {T <: Union{AbstractFloat, Integer, Ptr{UA_String}}} + valuerank) where {T <: Union{AbstractFloat, Integer, Ptr{UA_String}, UA_ComplexNumberType, UA_DoubleComplexNumberType}} type_ptr = ua_data_type_ptr_default(T) attr.valueRank = valuerank UA_Variant_setScalarCopy(attr.value, wrap_ref(value), type_ptr) @@ -209,6 +209,13 @@ function __set_scalar_attributes!(attr, value::AbstractString, valuerank) return nothing end +function __set_scalar_attributes!(attr, value::Complex{T}, valuerank) where T <: Union{Float32, Float64} + f = T == Float32 ? UA_ComplexNumberType : UA_DoubleComplexNumberType + ua_c = f(reim(value)...) + __set_scalar_attributes!(attr, ua_c, valuerank) + return nothing +end + function __set_array_attributes!(attr, value::AbstractArray{<:AbstractString}, valuerank) a = similar(value, UA_String) for i in eachindex(a) @@ -218,8 +225,18 @@ function __set_array_attributes!(attr, value::AbstractArray{<:AbstractString}, v return nothing end +function __set_array_attributes!(attr, value::AbstractArray{<:Complex{T}}, valuerank) where T <: Union{Float32, Float64} + f = T == Float32 ? UA_ComplexNumberType : UA_DoubleComplexNumberType + a = similar(value, f) + for i in eachindex(a) + a[i] = f(reim(value[i])...) + end + __set_array_attributes!(attr, a, valuerank) + return nothing +end + function __set_array_attributes!(attr, value::AbstractArray{T, N}, - valuerank) where {T <: Union{AbstractFloat, Integer, UA_String}, N} + valuerank) where {T <: Union{AbstractFloat, Integer, UA_String, UA_ComplexNumberType, UA_DoubleComplexNumberType}, N} type_ptr = ua_data_type_ptr_default(T) attr.valueRank = valuerank #Note: need array dims twice, once to put into the variant, i.e., attr.value @@ -274,7 +291,7 @@ function UA_VariableAttributes_generate(; value::Union{AbstractArray{T}, T}, minimumsamplinginterval::Union{Nothing, Float64} = nothing, historizing::Union{Nothing, Bool} = nothing, valuerank::Union{Nothing, Integer} = nothing) where - {T <: Union{AbstractFloat, Integer, AbstractString}} #TODO: implement array of strings + {T <: Union{Complex, AbstractFloat, Integer, AbstractString}} attr = __generate_variable_attributes(value, displayname, description, localization, writemask, userwritemask, accesslevel, useraccesslevel, minimumsamplinginterval, historizing, valuerank) @@ -329,6 +346,10 @@ function __generic_variable_attributes(displayname, description, localization, end if type <: AbstractString attr.dataType = unsafe_load(UA_TYPES_PTRS[UA_TYPES_STRING].typeId) + elseif type == ComplexF64 + attr.dataType = unsafe_load(UA_TYPES_PTRS[UA_TYPES_DOUBLECOMPLEXNUMBERTYPE].typeId) + elseif type == ComplexF32 + attr.dataType = unsafe_load(UA_TYPES_PTRS[UA_TYPES_COMPLEXNUMBERTYPE].typeId) else attr.dataType = unsafe_load(ua_data_type_ptr_default(type).typeId) end diff --git a/src/types.jl b/src/types.jl index 99b9bbd..4c868ad 100644 --- a/src/types.jl +++ b/src/types.jl @@ -292,6 +292,9 @@ function Base.unsafe_string(s::UA_String) end Base.unsafe_string(s::Ptr{UA_String}) = Base.unsafe_string(unsafe_load(s)) +#adapt base complex method for interoperability between ua complex numbers and julia complex numbers +Base.complex(x::T) where T <: Union{UA_ComplexNumberType, UA_DoubleComplexNumberType} = Complex(x.real, x.imaginary) + ## UA_BYTESTRING """ @@ -785,7 +788,7 @@ Base.length(v::UA_Variant) = Int(v.arrayLength) Base.length(p::Ref{UA_Variant}) = length(unsafe_load(p)) function UA_Variant_new(value::AbstractArray{T, N}, - type_ptr::Ptr{UA_DataType} = ua_data_type_ptr_default(T)) where {T <: Union{AbstractFloat, UA_String, Integer}, N} + type_ptr::Ptr{UA_DataType} = ua_data_type_ptr_default(T)) where {T <: Union{AbstractFloat, UA_String, Integer, UA_ComplexNumberType, UA_DoubleComplexNumberType}, N} var = UA_Variant_new() var.type = type_ptr var.storageType = UA_VARIANT_DATA @@ -799,7 +802,7 @@ end function UA_Variant_new(value::T, type_ptr::Ptr{UA_DataType} = ua_data_type_ptr_default(T)) where {T <: Union{ - AbstractFloat, Integer, Ptr{UA_String}}} + AbstractFloat, Integer, Ptr{UA_String}, UA_ComplexNumberType, UA_DoubleComplexNumberType}} var = UA_Variant_new() var.type = type_ptr var.storageType = UA_VARIANT_DATA @@ -814,6 +817,13 @@ function UA_Variant_new(value::AbstractString) return v end +function UA_Variant_new(value::Complex{T}) where T <: Union{Float32, Float64} + f = T == Float32 ? UA_ComplexNumberType : UA_DoubleComplexNumberType + ua_c = f(reim(value)...) + v = UA_Variant_new(ua_c) + return v +end + function UA_Variant_new(value::AbstractArray{<:AbstractString}) a = similar(value, UA_String) for i in eachindex(a) @@ -822,6 +832,15 @@ function UA_Variant_new(value::AbstractArray{<:AbstractString}) return UA_Variant_new(a) end +function UA_Variant_new(value::AbstractArray{<:Complex{T}}) where T <: Union{Float32, Float64} + f = T == Float32 ? UA_ComplexNumberType : UA_DoubleComplexNumberType + a = similar(value, f) + for i in eachindex(a) + a[i] = f(reim(value[i])...) + end + return UA_Variant_new(a) +end + function Base.unsafe_wrap(v::UA_Variant) type = juliadatatype(v.type) data = reinterpret(Ptr{type}, v.data) diff --git a/src/wrappers.jl b/src/wrappers.jl index 5109fe2..9193af1 100644 --- a/src/wrappers.jl +++ b/src/wrappers.jl @@ -283,14 +283,36 @@ function JUA_Server_readValue(client, nodeId) return r end +function __extract_ExtensionObject(eo::UA_ExtensionObject) + if eo.encoding != UA_EXTENSIONOBJECT_DECODED + error("can't make sense of this extension object yet.") + else + v = eo.content.decoded + type = juliadatatype(v.type) + data = reinterpret(Ptr{type}, v.data) + return GC.@preserve v type data unsafe_load(data) + end +end + function __get_juliavalues_from_variant(v) wrapped = unsafe_wrap(v) - if typeof(wrapped) <: Array && eltype(wrapped) == UA_String - r = unsafe_string.(wrapped) + if typeof(wrapped) == UA_ExtensionObject + wrapped = __extract_ExtensionObject.(wrapped) + elseif typeof(wrapped) <: Array && eltype(wrapped) == UA_ExtensionObject + wrapped = __extract_ExtensionObject.(wrapped) + end + + #now deal with special types + if typeof(wrapped) <: Union{UA_ComplexNumberType, UA_DoubleComplexNumberType} + r = complex(wrapped) + elseif typeof(wrapped) <: Array && eltype(wrapped) <: Union{UA_ComplexNumberType, UA_DoubleComplexNumberType} + r = complex.(wrapped) elseif typeof(wrapped) == UA_String r = unsafe_string(wrapped) + elseif typeof(wrapped) <: Array && eltype(wrapped) == UA_String + r = unsafe_string.(wrapped) else - r = deepcopy(wrapped) + r = deepcopy(wrapped) #TODO: do I need to copy here? test for memory safety! end return r end diff --git a/test/add_change_var_array.jl b/test/add_change_var_array.jl index 41addde..78df4c6 100644 --- a/test/add_change_var_array.jl +++ b/test/add_change_var_array.jl @@ -4,7 +4,7 @@ # We also check that setting a variable node with one type cannot be set to # another type (e.g., integer variable node cannot be set to float64.) -#Types tested: Bool, Int8/16/32/64, UInt8/16/32/64, Float32/64, String +#Types tested: Bool, Int8/16/32/64, UInt8/16/32/64, Float32/64, String, ComplexF32/64 #TODO: Improve memory management, also test with high level interface. @@ -16,7 +16,7 @@ Distributed.@everywhere begin using open62541, Test, Random # What types and sizes we are testing for: - types = [Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float32, Float64, String] + types = [Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float32, Float64, String, ComplexF32, ComplexF64] array_sizes = (11, (2, 5), (3, 4, 5), (3, 4, 5, 6)) # Generate random input values and generate nodeid names @@ -95,7 +95,7 @@ for (type_ind, type) in enumerate(types) input = input_data[type_ind][array_size_ind] varnodeid = JUA_NodeId(1, varnode_ids[type_ind, array_size_ind]) output_client = JUA_Client_readValueAttribute(client, varnodeid) - if type <: AbstractFloat + if type <: Union{AbstractFloat, Complex} @test all(isapprox.(input, output_client)) else @test all(input .== output_client) @@ -124,7 +124,13 @@ end # Test wrong data type write errors for type_ind in eachindex(types) for (array_size_ind, array_size) in enumerate(array_sizes) - type = types[mod(type_ind, length(types)) + 1] # Select wrong data type + if types[type_ind] == ComplexF64 || types[type_ind] == ComplexF32 + type = Float64 + elseif types[type_ind] == String #XXX: This is likely a bug in open62541 (can write complex numbers to string type variable, probably, because extension objects are not recognized properly) + type = Float64 + else + type = types[mod(type_ind, length(types)) + 1] # Select wrong data type + end new_input = type != String ? rand(type, array_size) : reshape([randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], array_size...) varnodeid = JUA_NodeId(1, varnode_ids[type_ind, array_size_ind]) new_v = UA_Variant_new(new_input) diff --git a/test/add_change_var_scalar.jl b/test/add_change_var_scalar.jl index a53f74b..2cd4e0f 100644 --- a/test/add_change_var_scalar.jl +++ b/test/add_change_var_scalar.jl @@ -4,7 +4,7 @@ # We also check that setting a variable node with one type cannot be set to # another type (e.g., integer variable node cannot be set to float64.) -#Types tested: Bool, Int8/16/32/64, UInt8/16/32/64, Float32/64, String +#Types tested: Bool, Int8/16/32/64, UInt8/16/32/64, Float32/64, String, ComplexF32/64 #TODO: improve memory handling and update to high level interface @@ -15,7 +15,7 @@ Distributed.@everywhere begin using open62541, Test, Random # What types we are testing for: - types = [Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float32, Float64, String] + types = [Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float32, Float64, String, ComplexF32, ComplexF64] # Generate random input values and generate nodeid names input_data = Tuple(type != String ? rand(type) : randstring(Int64(rand(UInt8))) for type in types) @@ -84,7 +84,9 @@ let trial end # Read with client from server -for (type_ind, type) in enumerate(types) +#for (type_ind, type) in enumerate(types) +type_ind = 13 +type = types[type_ind] input = input_data[type_ind] varnodeid = JUA_NodeId(1, varnode_ids[type_ind]) output_client = JUA_Client_readValueAttribute(client, varnodeid) @@ -93,7 +95,7 @@ for (type_ind, type) in enumerate(types) else @test all(input .== output_client) end -end +#end # Write new data for (type_ind, type) in enumerate(types) @@ -103,7 +105,7 @@ for (type_ind, type) in enumerate(types) retval = UA_Client_writeValueAttribute(client, varnodeid, new_v) @test retval == UA_STATUSCODE_GOOD output_client_new = JUA_Client_readValueAttribute(client, varnodeid) - if type <: AbstractFloat + if type <: Union{AbstractFloat, Complex} @test all(isapprox.(new_input, output_client_new)) else @test all(new_input .== output_client_new) From 5a7c57804a3ea2b6cf347e2dde5d8bccf7aba111 Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Mon, 13 May 2024 20:52:06 +0200 Subject: [PATCH 09/26] more high level interface --- src/attribute_generation.jl | 44 +++++++--- src/wrappers.jl | 50 +++++++---- test/add_change_var_array.jl | 19 ++--- test/add_change_var_scalar.jl | 24 ++---- test/attribute_generation.jl | 151 ++++++++++++++++++++++++++-------- test/simple_server_client.jl | 2 +- 6 files changed, 200 insertions(+), 90 deletions(-) diff --git a/src/attribute_generation.jl b/src/attribute_generation.jl index 50e1e16..78a0a4a 100644 --- a/src/attribute_generation.jl +++ b/src/attribute_generation.jl @@ -362,7 +362,7 @@ end """ ``` -UA_VariableTypeAttributes_generate(; value::Union{AbstractArray{T}, T}, +UA_VariableTypeAttributes_generate(; value::Union{Nothing, AbstractArray{T}, T} = nothing, displayname::AbstractString, description::AbstractString, localization::AbstractString = "en-US", writemask::Union{Nothing, UInt32} = nothing, @@ -372,25 +372,25 @@ UA_VariableTypeAttributes_generate(; value::Union{AbstractArray{T}, T}, ``` generates a `UA_VariableTypeAttributes` object. Memory for the object is allocated -by C and needs to be cleaned up by calling `UA_VariableAttributes_delete(x)` +by C and needs to be cleaned up by calling `UA_VariableTypeAttributes_delete(x)` after usage. -For keywords given as `nothing`, the respective default value is used, see `UA_VariableAttributes_default[]`. -If nothing is given for keyword `valuerank`, then it is either set to `UA_VALUERANK_SCALAR` -(if `value` is a scalar), or to the dimensionality of the supplied array -(i.e., `N` for an AbstractArray{T,N}). +For keywords given as `nothing`, the respective default value is used, see `UA_VariableTypeAttributes_default[]`. +If a default `value` is specified for the variabletype and nothing is given for +keyword `valuerank`, then it is either set to `UA_VALUERANK_SCALAR` (if `value` +is a scalar), or to the dimensionality of the supplied array (i.e., `N` for an +AbstractArray{T,N}). See also [`UA_WRITEMASK`](@ref), [`UA_USERWRITEMASK`](@ref) for information on how to generate the respective keyword inputs. """ -function UA_VariableTypeAttributes_generate(; value::Union{AbstractArray{T}, T}, +function UA_VariableTypeAttributes_generate(; value::Union{AbstractArray{<:Union{AbstractString, AbstractFloat, Integer, ComplexF32, ComplexF64}}, Union{Nothing, AbstractString, AbstractFloat, Integer, ComplexF32, ComplexF64}} = nothing, displayname::AbstractString, description::AbstractString, localization::AbstractString = "en-US", writemask::Union{Nothing, UInt32} = nothing, userwritemask::Union{Nothing, UInt32} = nothing, valuerank::Union{Nothing, Integer} = nothing, - isabstract::Union{Nothing, Bool} = nothing) where {T <: - Union{AbstractFloat, Integer}} + isabstract::Union{Nothing, Bool} = nothing) attr = __generate_variabletype_attributes(value, displayname, description, localization, writemask, userwritemask, valuerank, isabstract) return attr @@ -398,7 +398,7 @@ end function __generate_variabletype_attributes(value::AbstractArray{T, N}, displayname, description, localization, writemask, userwritemask, valuerank, - isabstract) where {T, N} + isabstract) where {T <: Union{AbstractString, AbstractFloat, Integer, ComplexF32, ComplexF64}, N} if isnothing(valuerank) valuerank = UA_VALUERANK(N) end @@ -410,7 +410,7 @@ end function __generate_variabletype_attributes(value::T, displayname, description, localization, writemask, userwritemask, valuerank, - isabstract) where {T} + isabstract) where {T <: Union{AbstractString, AbstractFloat, Integer, ComplexF32, ComplexF64}} if isnothing(valuerank) valuerank = UA_VALUERANK_SCALAR end @@ -420,6 +420,16 @@ function __generate_variabletype_attributes(value::T, displayname, return attr end +function __generate_variabletype_attributes(value::Nothing, displayname, + description, localization, writemask, userwritemask, valuerank, isabstract) + attr = __generic_variabletype_attributes(displayname, description, localization, + writemask, userwritemask, isabstract, value) + if !isnothing(valuerank) + attr.valueRank = valuerank + end + return attr +end + function __generic_variabletype_attributes(displayname, description, localization, writemask, userwritemask, isabstract, type) attr = UA_VariableTypeAttributes_new() @@ -430,7 +440,17 @@ function __generic_variabletype_attributes(displayname, description, localizatio if !isnothing(isabstract) attr.isAbstract = isabstract end - attr.dataType = unsafe_load(ua_data_type_ptr_default(type).typeId) + if !isnothing(type) + if type <: AbstractString + attr.dataType = unsafe_load(UA_TYPES_PTRS[UA_TYPES_STRING].typeId) + elseif type == ComplexF64 + attr.dataType = unsafe_load(UA_TYPES_PTRS[UA_TYPES_DOUBLECOMPLEXNUMBERTYPE].typeId) + elseif type == ComplexF32 + attr.dataType = unsafe_load(UA_TYPES_PTRS[UA_TYPES_COMPLEXNUMBERTYPE].typeId) + else + attr.dataType = unsafe_load(ua_data_type_ptr_default(type).typeId) + end + end return attr else err = AttributeCopyError(statuscode) diff --git a/src/wrappers.jl b/src/wrappers.jl index 9193af1..ebe7be1 100644 --- a/src/wrappers.jl +++ b/src/wrappers.jl @@ -233,21 +233,34 @@ end Base.convert(::Type{UA_QualifiedName}, x::JUA_QualifiedName) = unsafe_load(Jpointer(x)) -#VariableAttributes - TODO: complete -# mutable struct JUA_VariableAttributes <: AbstractOpen62541Wrapper -# ptr::Ptr{UA_VariableAttributes} -# function JUA_VariableAttributes(kwargs...) -# obj = new(UA_VariableAttributes_generate(kwargs...)) -# finalizer(release_handle, obj) -# return obj -# end -# end - -# function release_handle(obj::JUA_VariableAttributes) -# UA_VariableAttributes_delete(Jpointer(obj)) -# end - -#TODO: here we can do automatic unsafe_wrap on the content, so that the user doesn't have to do it. +#VariableAttributes +mutable struct JUA_VariableAttributes <: AbstractOpen62541Wrapper + ptr::Ptr{UA_VariableAttributes} + function JUA_VariableAttributes(; kwargs...) + obj = new(UA_VariableAttributes_generate(; kwargs...)) + finalizer(release_handle, obj) + return obj + end +end + +function release_handle(obj::JUA_VariableAttributes) + UA_VariableAttributes_delete(Jpointer(obj)) +end + +#VariableTypeAttributes +mutable struct JUA_VariableTypeAttributes <: AbstractOpen62541Wrapper + ptr::Ptr{UA_VariableTypeAttributes} + function JUA_VariableTypeAttributes(; kwargs...) + obj = new(UA_VariableTypeAttributes_generate(; kwargs...)) + finalizer(release_handle, obj) + return obj + end +end + +function release_handle(obj::JUA_VariableTypeAttributes) + UA_VariableTypeAttributes_delete(Jpointer(obj)) +end + mutable struct JUA_Variant <: AbstractOpen62541Wrapper ptr::Ptr{UA_Variant} function JUA_Variant() @@ -272,6 +285,13 @@ function JUA_Client_readValueAttribute(client, nodeId) return r end +function JUA_Client_writeValueAttribute(client, nodeId, newvalue) + newvariant = UA_Variant_new(newvalue) + statuscode = UA_Client_writeValueAttribute(client, nodeId, newvariant) + UA_Variant_delete(newvariant) + return statuscode +end + function JUA_Server_readValue(client, nodeId) #TODO: Is there a way of making this typestable? #(it's not really known what kind of data is stored inside a nodeid unless diff --git a/test/add_change_var_array.jl b/test/add_change_var_array.jl index 78df4c6..1a01096 100644 --- a/test/add_change_var_array.jl +++ b/test/add_change_var_array.jl @@ -6,8 +6,7 @@ #Types tested: Bool, Int8/16/32/64, UInt8/16/32/64, Float32/64, String, ComplexF32/64 - -#TODO: Improve memory management, also test with high level interface. +#TODO: introduce final high level functions using Distributed Distributed.addprocs(1) # Add a single worker process to run the server @@ -38,7 +37,7 @@ Distributed.@spawnat Distributed.workers()[end] begin # Generate a UA_Server with standard config input = input_data[type_ind][array_size_ind] accesslevel = UA_ACCESSLEVEL(read = true, write = true) - attr = UA_VariableAttributes_generate(value = input, + attr = JUA_VariableAttributes(value = input, displayname = varnode_ids[type_ind, array_size_ind], description = "this is a $(string(array_size)) $(Symbol(type)) array variable", accesslevel = accesslevel) @@ -47,8 +46,8 @@ Distributed.@spawnat Distributed.workers()[end] begin parentreferencenodeid = JUA_NodeId(0, UA_NS0ID_ORGANIZES) typedefinition = JUA_NodeId(0, UA_NS0ID_BASEDATAVARIABLETYPE) browsename = JUA_QualifiedName(1, varnode_ids[type_ind, array_size_ind]) - nodecontext = C_NULL - outnewnodeid = C_NULL + nodecontext = JUA_NodeId() + outnewnodeid = JUA_NodeId() retval = UA_Server_addVariableNode(server, varnodeid, parentnodeid, parentreferencenodeid, browsename, typedefinition, attr, nodecontext, outnewnodeid) @@ -108,8 +107,7 @@ for (type_ind, type) in enumerate(types) for (array_size_ind, array_size) in enumerate(array_sizes) new_input = type != String ? rand(type, array_size) : reshape([randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], array_size...) varnodeid = JUA_NodeId(1, varnode_ids[type_ind, array_size_ind]) - new_v = UA_Variant_new(new_input) #TODO: need to come up with a good interface for the write functions as well. - retval = UA_Client_writeValueAttribute(client, varnodeid, new_v) + retval = JUA_Client_writeValueAttribute(client, varnodeid, new_input) @test retval == UA_STATUSCODE_GOOD output_client_new = JUA_Client_readValueAttribute(client, varnodeid) if type <: AbstractFloat @@ -117,7 +115,6 @@ for (type_ind, type) in enumerate(types) else @test all(new_input .== output_client_new) end - UA_Variant_delete(new_v) end end @@ -133,10 +130,8 @@ for type_ind in eachindex(types) end new_input = type != String ? rand(type, array_size) : reshape([randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], array_size...) varnodeid = JUA_NodeId(1, varnode_ids[type_ind, array_size_ind]) - new_v = UA_Variant_new(new_input) - @test_throws open62541.AttributeReadWriteError UA_Client_writeValueAttribute( - client, varnodeid, new_v) - UA_Variant_delete(new_v) + @test_throws open62541.AttributeReadWriteError JUA_Client_writeValueAttribute( + client, varnodeid, new_input) end end diff --git a/test/add_change_var_scalar.jl b/test/add_change_var_scalar.jl index 2cd4e0f..c83be9e 100644 --- a/test/add_change_var_scalar.jl +++ b/test/add_change_var_scalar.jl @@ -6,7 +6,7 @@ #Types tested: Bool, Int8/16/32/64, UInt8/16/32/64, Float32/64, String, ComplexF32/64 -#TODO: improve memory handling and update to high level interface +#TODO: introduce final high level functions using Distributed Distributed.addprocs(1) # Add a single worker process to run the server @@ -33,7 +33,7 @@ Distributed.@spawnat Distributed.workers()[end] begin for (type_ind, type) in enumerate(types) accesslevel = UA_ACCESSLEVEL(read = true, write = true) input = input_data[type_ind] - attr = UA_VariableAttributes_generate(value = input, + attr = JUA_VariableAttributes(value = input, displayname = varnode_ids[type_ind], description = "this is a $(Symbol(type)) scalar variable", accesslevel = accesslevel) @@ -42,8 +42,8 @@ Distributed.@spawnat Distributed.workers()[end] begin parentreferencenodeid = JUA_NodeId(0, UA_NS0ID_ORGANIZES) typedefinition = JUA_NodeId(0, UA_NS0ID_BASEDATAVARIABLETYPE) browsename = JUA_QualifiedName(1, varnode_ids[type_ind]) - nodecontext = C_NULL - outnewnodeid = C_NULL + nodecontext = JUA_NodeId() + outnewnodeid = JUA_Nodeid() retval = UA_Server_addVariableNode(server, varnodeid, parentnodeid, parentreferencenodeid, browsename, typedefinition, attr, nodecontext, outnewnodeid) @@ -84,9 +84,7 @@ let trial end # Read with client from server -#for (type_ind, type) in enumerate(types) -type_ind = 13 -type = types[type_ind] +for (type_ind, type) in enumerate(types) input = input_data[type_ind] varnodeid = JUA_NodeId(1, varnode_ids[type_ind]) output_client = JUA_Client_readValueAttribute(client, varnodeid) @@ -95,14 +93,13 @@ type = types[type_ind] else @test all(input .== output_client) end -#end +end # Write new data for (type_ind, type) in enumerate(types) new_input = type != String ? rand(type) : randstring(Int64(rand(UInt8))) varnodeid = JUA_NodeId(1, varnode_ids[type_ind]) - new_v = UA_Variant_new(new_input) - retval = UA_Client_writeValueAttribute(client, varnodeid, new_v) + retval = JUA_Client_writeValueAttribute(client, varnodeid, new_input) @test retval == UA_STATUSCODE_GOOD output_client_new = JUA_Client_readValueAttribute(client, varnodeid) if type <: Union{AbstractFloat, Complex} @@ -110,7 +107,6 @@ for (type_ind, type) in enumerate(types) else @test all(new_input .== output_client_new) end - UA_Variant_delete(new_v) end # Test wrong data type write errors @@ -118,10 +114,8 @@ for type_ind in eachindex(types) type = types[mod(type_ind, length(types)) + 1] # Select wrong data type new_input = type != String ? rand(type) : randstring(Int64(rand(UInt8))) varnodeid = JUA_NodeId(1, varnode_ids[type_ind]) - new_v = UA_Variant_new(new_input) - @test_throws open62541.AttributeReadWriteError UA_Client_writeValueAttribute(client, - varnodeid, new_v) - UA_Variant_delete(new_v) + @test_throws open62541.AttributeReadWriteError JUA_Client_writeValueAttribute(client, + varnodeid, new_input) end # Disconnect client diff --git a/test/attribute_generation.jl b/test/attribute_generation.jl index 6032006..e5204f3 100644 --- a/test/attribute_generation.jl +++ b/test/attribute_generation.jl @@ -1,6 +1,7 @@ #Purpose: Tests attribute generation functionality and associated functions using open62541 using Test +using Random #UA_VALUERANK @test UA_VALUERANK(1) == UA_VALUERANK_ONE_DIMENSION @@ -84,7 +85,9 @@ using Test #UA_VariableAttributes_generate #define different sized input cases to test both scalar and array codes -inputs = (rand(), rand(2), rand(2, 3), rand(2, 3, 4)) +array_sizes = [1, 2, (2, 3), (2, 3, 4)] +types = [Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float32, Float64, String, ComplexF32, ComplexF64] +inputs = Tuple(Tuple( type != String ? rand(type, array_size) : reshape([randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], array_size...) for array_size in array_sizes) for type in types) valueranks = [-1, 1, 2, 3] displayname = "whatever" description = "this is a whatever variable" @@ -95,30 +98,55 @@ accesslevel = UA_ACCESSLEVEL(read = true, write = true) useraccesslevel = UA_ACCESSLEVEL(read = true, historyread = true) minimumsamplinginterval = rand() historizing = true -for i in 1:length(inputs) - attr = UA_VariableAttributes_generate(value = inputs[i], displayname = displayname, - description = description, localization = localization, - writemask = writemask, userwritemask = userwritemask, - accesslevel = accesslevel, useraccesslevel = useraccesslevel, - minimumsamplinginterval = minimumsamplinginterval, - historizing = historizing) - @test unsafe_string(unsafe_load(attr.displayName.text)) == displayname - @test unsafe_string(unsafe_load(attr.displayName.locale)) == localization - @test unsafe_string(unsafe_load(attr.description.text)) == description - @test unsafe_string(unsafe_load(attr.description.locale)) == localization - @test unsafe_load(attr.writeMask) == writemask - @test unsafe_load(attr.userWriteMask) == userwritemask - @test unsafe_load(attr.accessLevel) == accesslevel - @test unsafe_load(attr.userAccessLevel) == useraccesslevel - @test unsafe_load(attr.minimumSamplingInterval) == minimumsamplinginterval - @test unsafe_load(attr.valueRank) == valueranks[i] - @test unsafe_load(attr.historizing) == historizing - UA_VariableAttributes_delete(attr) +for i in eachindex(array_sizes) + for j in eachindex(types) + if length(inputs[j][i]) == 1 + v = inputs[j][i][1] + else + v = inputs[j][i] + end + attr = UA_VariableAttributes_generate(value = v, displayname = displayname, + description = description, localization = localization, + writemask = writemask, userwritemask = userwritemask, + accesslevel = accesslevel, useraccesslevel = useraccesslevel, + minimumsamplinginterval = minimumsamplinginterval, + historizing = historizing) + @test unsafe_string(unsafe_load(attr.displayName.text)) == displayname + @test unsafe_string(unsafe_load(attr.displayName.locale)) == localization + @test unsafe_string(unsafe_load(attr.description.text)) == description + @test unsafe_string(unsafe_load(attr.description.locale)) == localization + @test unsafe_load(attr.writeMask) == writemask + @test unsafe_load(attr.userWriteMask) == userwritemask + @test unsafe_load(attr.accessLevel) == accesslevel + @test unsafe_load(attr.userAccessLevel) == useraccesslevel + @test unsafe_load(attr.minimumSamplingInterval) == minimumsamplinginterval + @test unsafe_load(attr.valueRank) == valueranks[i] + @test unsafe_load(attr.historizing) == historizing + #TODO: add test that checks dataType being correctly set. + out = open62541.__get_juliavalues_from_variant(attr.value) + if types[j] <: Union{AbstractFloat, Complex} + @test all(out .≈ v) + else + @test all(out == v) + end + UA_VariableAttributes_delete(attr) + + #now a test with the high level interface as well + j = JUA_VariableAttributes(value = v, displayname = displayname, + description = description, localization = localization, + writemask = writemask, userwritemask = userwritemask, + accesslevel = accesslevel, useraccesslevel = useraccesslevel, + minimumsamplinginterval = minimumsamplinginterval, + historizing = historizing) + @test j isa JUA_VariableAttributes + end end #UA_VariableTypeAttributes_generate #define different sized input cases to test both scalar and array codes -inputs = (rand(), rand(2), rand(2, 3), rand(2, 3, 4)) +array_sizes = [1, 2, (2, 3), (2, 3, 4)] +types = [Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float32, Float64, String, ComplexF32, ComplexF64] +inputs = Tuple(Tuple( type != String ? rand(type, array_size) : reshape([randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], array_size...) for array_size in array_sizes) for type in types) valueranks = [-1, 1, 2, 3] displayname = "whatever" description = "this is a whatever variable" @@ -126,22 +154,75 @@ localization = "en-GB" writemask = UA_WRITEMASK(accesslevel = true, valuerank = true, writemask = true) userwritemask = UA_WRITEMASK(accesslevel = true, valuerank = false, writemask = true) isabstract = true -for i in 1:length(inputs) - attr = UA_VariableTypeAttributes_generate(value = inputs[i], displayname = displayname, - description = description, localization = localization, - writemask = writemask, userwritemask = userwritemask, - isabstract = isabstract) - @test unsafe_string(unsafe_load(attr.displayName.text)) == displayname - @test unsafe_string(unsafe_load(attr.displayName.locale)) == localization - @test unsafe_string(unsafe_load(attr.description.text)) == description - @test unsafe_string(unsafe_load(attr.description.locale)) == localization - @test unsafe_load(attr.writeMask) == writemask - @test unsafe_load(attr.userWriteMask) == userwritemask - @test unsafe_load(attr.valueRank) == valueranks[i] - @test unsafe_load(attr.isAbstract) == isabstract - UA_VariableTypeAttributes_delete(attr) +for i in eachindex(array_sizes) + for j in eachindex(types) + if length(inputs[j][i]) == 1 + v = inputs[j][i][1] + else + v = inputs[j][i] + end + attr = UA_VariableTypeAttributes_generate(value = v, displayname = displayname, + description = description, localization = localization, + writemask = writemask, userwritemask = userwritemask, + isabstract = isabstract) + @test unsafe_string(unsafe_load(attr.displayName.text)) == displayname + @test unsafe_string(unsafe_load(attr.displayName.locale)) == localization + @test unsafe_string(unsafe_load(attr.description.text)) == description + @test unsafe_string(unsafe_load(attr.description.locale)) == localization + @test unsafe_load(attr.writeMask) == writemask + @test unsafe_load(attr.userWriteMask) == userwritemask + @test unsafe_load(attr.valueRank) == valueranks[i] + @test unsafe_load(attr.isAbstract) == isabstract + out = open62541.__get_juliavalues_from_variant(attr.value) + if types[j] <: Union{AbstractFloat, Complex} + @test all(out .≈ v) + else + @test all(out == v) + end + UA_VariableTypeAttributes_delete(attr) + + #high level interface + j = JUA_VariableTypeAttributes(value = v, displayname = displayname, + description = description, localization = localization, + writemask = writemask, userwritemask = userwritemask, + isabstract = isabstract) + @test j isa JUA_VariableTypeAttributes + end end +#now test case with no value specified (tests different branches) +def = UA_VariableTypeAttributes_default[] + +## use only mandatory keywords +attr = UA_VariableTypeAttributes_generate(displayname = displayname, + description = description, localization = localization) +@test unsafe_string(unsafe_load(attr.displayName.text)) == displayname +@test unsafe_string(unsafe_load(attr.displayName.locale)) == localization +@test unsafe_string(unsafe_load(attr.description.text)) == description +@test unsafe_string(unsafe_load(attr.description.locale)) == localization +@test unsafe_load(attr.writeMask) == def.writeMask +@test unsafe_load(attr.userWriteMask) == def.userWriteMask +@test unsafe_load(attr.valueRank) == def.valueRank +@test unsafe_load(attr.isAbstract) == def.isAbstract +UA_VariableTypeAttributes_delete(attr) + +## use optional keywords as well +valuerank = 1 +attr = UA_VariableTypeAttributes_generate(displayname = displayname, + description = description, localization = localization, + writemask = writemask, userwritemask = userwritemask, + isabstract = isabstract, valuerank = valuerank) + +@test unsafe_string(unsafe_load(attr.displayName.text)) == displayname +@test unsafe_string(unsafe_load(attr.displayName.locale)) == localization +@test unsafe_string(unsafe_load(attr.description.text)) == description +@test unsafe_string(unsafe_load(attr.description.locale)) == localization +@test unsafe_load(attr.writeMask) == writemask +@test unsafe_load(attr.userWriteMask) == userwritemask +@test unsafe_load(attr.valueRank) == valuerank +@test unsafe_load(attr.isAbstract) == isabstract +UA_VariableTypeAttributes_delete(attr) + #UA_ObjectAttributes_generate displayname = "whatever" description = "this is a whatever variable" diff --git a/test/simple_server_client.jl b/test/simple_server_client.jl index 27231df..1371ab2 100644 --- a/test/simple_server_client.jl +++ b/test/simple_server_client.jl @@ -25,7 +25,7 @@ sleep_time = 2.0 # Sleep time in seconds between each connection trial let trial trial = 0 while trial < max_duration / sleep_time - retval = UA_Client_connect(client, "opc.tcp://localhost:4842") + retval = JUA_Client_connect(client, "opc.tcp://localhost:4842") if retval == UA_STATUSCODE_GOOD println("Connection established.") break From f5fc1b1376eda2372367c633a07b3710ee015524 Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Mon, 13 May 2024 21:27:05 +0200 Subject: [PATCH 10/26] format --- README.md | 118 ++++++++++++----------- docs/make.jl | 42 ++++---- docs/src/manual/attributegeneration.md | 2 + docs/src/manual/client.md | 3 + docs/src/manual/nodeid.md | 1 + docs/src/manual/numbertypes.md | 10 +- docs/src/manual/server.md | 6 +- docs/src/tutorials/client_first_steps.md | 58 +++++------ docs/src/tutorials/server_first_steps.md | 19 ++-- src/attribute_generation.jl | 66 +++++++------ src/server.jl | 2 + src/types.jl | 52 +++++----- src/wrappers.jl | 36 ++++--- test/add_change_var_array.jl | 20 +++- test/add_change_var_scalar.jl | 10 +- test/attribute_generation.jl | 34 ++++--- 16 files changed, 275 insertions(+), 204 deletions(-) diff --git a/README.md b/README.md index 3c12b3a..5c7cbd7 100644 --- a/README.md +++ b/README.md @@ -5,91 +5,99 @@ [![codecov](https://codecov.io/gh/martinkosch/open62541.jl/graph/badge.svg?token=lJe2xOTO7g)](https://codecov.io/gh/martinkosch/open62541.jl) [![Aqua QA](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl) -open62541.jl is a [Julia](https://julialang.org) package that interfaces with the [open62541](https://www.open62541.org/) -library written in C ([source](https://github.com/open62541/open62541)). +open62541.jl is a [Julia](https://julialang.org) package that interfaces with the [open62541](https://www.open62541.org/) +library written in C ([source](https://github.com/open62541/open62541)). -As such, it provides functionality following the [OPC Unified Architecture (OPC UA) standard](https://en.wikipedia.org/wiki/OPC_Unified_Architecture) -for data exchange from sensors to cloud applications developed by the [OPC Foundation](). +As such, it provides functionality following the [OPC Unified Architecture (OPC UA) standard](https://en.wikipedia.org/wiki/OPC_Unified_Architecture) +for data exchange from sensors to cloud applications developed by the [OPC Foundation](). -In short, it provides the ability to create OPC servers that make data from different -sources available to clients and, naturally, also a client functionality that allows +In short, it provides the ability to create OPC servers that make data from different +sources available to clients and, naturally, also a client functionality that allows to read data from OPC UA servers. Features are summarized further on the [open62541 website](https://www.open62541.org/). -open62541.jl's *ultimate* aim is to provide the full functionality of open62541 to -Julia users through a convenient high level interface without the need to engage +open62541.jl's *ultimate* aim is to provide the full functionality of open62541 to +Julia users through a convenient high level interface without the need to engage in manual memory management etc. (as required in open62541). -At its current development stage the high level interface is implemented for a -(commonly used) subset of functionality. An essentially feature-complete lower +At its current development stage the high level interface is implemented for a +(commonly used) subset of functionality. An essentially feature-complete lower level interface that wraps all functionality of open62541 is, however, available. ## Warning: active development -Note that open62541.jl is still under active development and has not reached a maturity -that would make it safe to use in a production environment. -The developers aim to observe [semantic versioning](https://semver.org/), but -accidental breakage and evolutions of the API have to be expected. +Note that open62541.jl is still under active development and has not reached a maturity +that would make it safe to use in a production environment. + +The developers aim to observe [semantic versioning](https://semver.org/), but +accidental breakage and evolutions of the API have to be expected. Documentation is also a work in progress. ## Installation -open62541.jl is not yet registered in Julia's General registry, but it will -hopefully be soon (status: May 2024). -Assuming you have Julia already installed (otherwise: [JuliaLang Website](https://julialang.org/)), +open62541.jl is not yet registered in Julia's General registry, but it will +hopefully be soon (status: May 2024). + +Assuming you have Julia already installed (otherwise: [JuliaLang Website](https://julialang.org/)), you can install by executing: + ```julia - using Pkg - Pkg.add("https://github.com/martinkosch/open62541.jl") +using Pkg +Pkg.add("https://github.com/martinkosch/open62541.jl") ``` ## Server example + Starting up a server with a default configuration in open62541.jl is very simple. Just execute the following code in the REPL or as a script: + ```julia - using open62541 - server = JUA_Server() - config = JUA_ServerConfig(server) - JUA_ServerConfig_setDefault(config) - JUA_Server_runUntilInterrupt(server) +using open62541 +server = JUA_Server() +config = JUA_ServerConfig(server) +JUA_ServerConfig_setDefault(config) +JUA_Server_runUntilInterrupt(server) ``` + This will configure a server with the default configuration (address: opc.tcp://localhost:4840/) - and start it. The server can be shut down by pressing CTRL+C multiple times. +and start it. The server can be shut down by pressing CTRL+C multiple times. -While the server is running, it can be accessed via the Client API of open62541.jl +While the server is running, it can be accessed via the Client API of open62541.jl or it can be browsed and accessed with a graphical client, such as [UA Expert](https://www.unified-automation.com/products/development-tools/uaexpert.html). ## Basic client example -In order to showcase the Client API functionality, we will use the above server -and read some basic information from it, namely the software version number and -the current time. Note that this information should be contained in all OPC UA + +In order to showcase the Client API functionality, we will use the above server +and read some basic information from it, namely the software version number and +the current time. Note that this information should be contained in all OPC UA servers, so you could also connect to a different server that you know is running. ```julia - using open62541 - using Printf - - #initiate client, configure it and connect to server - client = JUA_Client() - config = JUA_ClientConfig(client) - JUA_ClientConfig_setDefault(config) - JUA_Client_connect(client, "opc.tcp://localhost:4840") - - #define nodeids that we are interested in - nodeid_currenttime = JUA_NodeId(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME) - nodeid_version = JUA_NodeId(0, UA_NS0ID_SERVER_SERVERSTATUS_BUILDINFO_SOFTWAREVERSION) - - #read data from nodeids - currenttime = JUA_Client_readValueAttribute(client, nodeid_currenttime) #Int64 which represents the number of 100 nanosecond intervals since January 1, 1601 (UTC) - version = JUA_Client_readValueAttribute(client, nodeid_version) #String containing open62541 version number - - #Convert current time into human understandable format - dts = UA_DateTime_toStruct(currenttime) - - #Print results to terminal - Printf.@printf("current date and time (UTC) is: %u-%u-%u %u:%u:%u.%03u\n", dts.day, dts.month, dts.year, dts.hour, dts.min, dts.sec, dts.milliSec) - Printf.@printf("The server is running open62541 version %s.", version) - - #disconnect the client (good housekeeping practice) - JUA_Client_disconnect(client) +using open62541 +using Printf + +#initiate client, configure it and connect to server +client = JUA_Client() +config = JUA_ClientConfig(client) +JUA_ClientConfig_setDefault(config) +JUA_Client_connect(client, "opc.tcp://localhost:4840") + +#define nodeids that we are interested in +nodeid_currenttime = JUA_NodeId(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME) +nodeid_version = JUA_NodeId(0, UA_NS0ID_SERVER_SERVERSTATUS_BUILDINFO_SOFTWAREVERSION) + +#read data from nodeids +currenttime = JUA_Client_readValueAttribute(client, nodeid_currenttime) #Int64 which represents the number of 100 nanosecond intervals since January 1, 1601 (UTC) +version = JUA_Client_readValueAttribute(client, nodeid_version) #String containing open62541 version number + +#Convert current time into human understandable format +dts = UA_DateTime_toStruct(currenttime) + +#Print results to terminal +Printf.@printf("current date and time (UTC) is: %u-%u-%u %u:%u:%u.%03u\n", + dts.day, dts.month, dts.year, dts.hour, dts.min, dts.sec, dts.milliSec) +Printf.@printf("The server is running open62541 version %s.", version) + +#disconnect the client (good housekeeping practice) +JUA_Client_disconnect(client) ``` diff --git a/docs/make.jl b/docs/make.jl index b7ab09c..1b11c7a 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -12,38 +12,38 @@ makedocs(; prettyurls = get(ENV, "CI", "false") == "true", canonical = "https://martinkosch.github.io/open62541.jl", assets = String[], - size_threshold=8000 * 2^10, - repolink = "https://github.com/martinkosch/open62541.jl", + size_threshold = 8000 * 2^10, + repolink = "https://github.com/martinkosch/open62541.jl" ), pages = [ "Home" => "index.md", "Manual" => [ - "manual/numbertypes.md", - "manual/nodeid.md", - "manual/attributegeneration.md", - "manual/server.md", - "manual/client.md", - ], + "manual/numbertypes.md", + "manual/nodeid.md", + "manual/attributegeneration.md", + "manual/server.md", + "manual/client.md" + ], "Tutorials" => [ - "tutorials/server_first_steps.md", - "tutorials/client_first_steps.md" + "tutorials/server_first_steps.md", + "tutorials/client_first_steps.md" ] ], warnonly = Documenter.except( - :autodocs_block, + :autodocs_block, # :cross_references, - :docs_block, - :doctest, - :eval_block, - :example_block, - :footnote, - :linkcheck_remotes, - :linkcheck, - :meta_block, + :docs_block, + :doctest, + :eval_block, + :example_block, + :footnote, + :linkcheck_remotes, + :linkcheck, + :meta_block, #:missing_docs, - :parse_error, + :parse_error, :setup_block - ), + ) ) deploydocs(; diff --git a/docs/src/manual/attributegeneration.md b/docs/src/manual/attributegeneration.md index c01cd63..b640660 100644 --- a/docs/src/manual/attributegeneration.md +++ b/docs/src/manual/attributegeneration.md @@ -3,6 +3,7 @@ This page lists docstrings of functions used for convenient attribute generation. Convenience functions that allow setting values for specific attributes: + ```@docs UA_VALUERANK UA_ACCESSLEVEL @@ -13,6 +14,7 @@ UA_EVENTNOTIFIER ``` Generators for attribute blocks: + ```@docs UA_VariableAttributes_generate UA_VariableTypeAttributes_generate diff --git a/docs/src/manual/client.md b/docs/src/manual/client.md index a7d0013..a21af9d 100644 --- a/docs/src/manual/client.md +++ b/docs/src/manual/client.md @@ -3,6 +3,7 @@ This page lists docstrings relevant to the client API. ## Adding different types of nodes: + ```@docs UA_Client_addVariableNode UA_Client_addObjectNode @@ -15,6 +16,7 @@ UA_Client_addMethodNode ``` ## Reading from nodes: + ```@docs UA_Client_readAccessLevelAttribute UA_Client_readBrowseNameAttribute @@ -40,6 +42,7 @@ UA_Client_readWriteMaskAttribute ``` ## Writing to nodes: + ```@docs UA_Client_writeAccessLevelAttribute UA_Client_writeBrowseNameAttribute diff --git a/docs/src/manual/nodeid.md b/docs/src/manual/nodeid.md index bc00b19..6970fb6 100644 --- a/docs/src/manual/nodeid.md +++ b/docs/src/manual/nodeid.md @@ -5,6 +5,7 @@ This page lists docstrings of functions used to create NodeId identifiers. ## Low level interface test + ```@docs UA_NODEID UA_NODEID_BYTESTRING_ALLOC diff --git a/docs/src/manual/numbertypes.md b/docs/src/manual/numbertypes.md index 426c56e..584d570 100644 --- a/docs/src/manual/numbertypes.md +++ b/docs/src/manual/numbertypes.md @@ -1,12 +1,14 @@ # Supported number types -It is noteworthy that the open62541 library supports the following Julia +It is noteworthy that the open62541 library supports the following Julia number types natively. open62541.jl provides support for the same number types. Adding other types is possible, but must rely on a custom datatype. See the [open62541 documentation](https://github.com/open62541/open62541/tree/master/examples/custom_datatype). **Real:** -- Integers: Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64. -- Float: Float32 and Float64. + + - Integers: Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64. + - Float: Float32 and Float64. **Complex:** -- Complex{Float32}, Complex{Float64} + + - Complex{Float32}, Complex{Float64} diff --git a/docs/src/manual/server.md b/docs/src/manual/server.md index ae05abc..840efd7 100644 --- a/docs/src/manual/server.md +++ b/docs/src/manual/server.md @@ -3,6 +3,7 @@ This page lists docstrings relevant to the server API. ## Adding different types of nodes: + ```@docs UA_Server_addVariableNode UA_Server_addObjectNode @@ -13,7 +14,9 @@ UA_Server_addReferenceTypeNode UA_Server_addDataTypeNode UA_Server_addMethodNode ``` + ## Reading from nodes: + ```@docs UA_Server_readAccessLevel UA_Server_readArrayDimensions @@ -37,6 +40,7 @@ UA_Server_readWriteMask ``` ## Writing to nodes: + ```@docs UA_Server_writeAccessLevel UA_Server_writeArrayDimensions @@ -54,4 +58,4 @@ UA_Server_writeMinimumSamplingInterval UA_Server_writeValue UA_Server_writeValueRank UA_Server_writeWriteMask -``` \ No newline at end of file +``` diff --git a/docs/src/tutorials/client_first_steps.md b/docs/src/tutorials/client_first_steps.md index 2567c39..eb204d8 100644 --- a/docs/src/tutorials/client_first_steps.md +++ b/docs/src/tutorials/client_first_steps.md @@ -1,34 +1,36 @@ # First steps with an open62541 client + In this tutorial we will connect an OPC client to the server started up in [First steps with an open62541 server](@ref) -and read some basic information from it, namely the software version number and -the current time. Note that this information should be contained in all OPC UA +and read some basic information from it, namely the software version number and +the current time. Note that this information should be contained in all OPC UA servers, so you could also connect to a different server that you know is running. ```julia - using open62541 - using Printf - - #initiate client, configure it and connect to server - client = JUA_Client() - config = JUA_ClientConfig(client) - JUA_ClientConfig_setDefault(config) - JUA_Client_connect(client, "opc.tcp://localhost:4840") - - #define nodeids that we are interested in - nodeid_currenttime = JUA_NodeId(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME) - nodeid_version = JUA_NodeId(0, UA_NS0ID_SERVER_SERVERSTATUS_BUILDINFO_SOFTWAREVERSION) - - #read data from nodeids - currenttime = JUA_Client_readValueAttribute(client, nodeid_currenttime) #Int64 which represents the number of 100 nanosecond intervals since January 1, 1601 (UTC) - version = JUA_Client_readValueAttribute(client, nodeid_version) #String containing open62541 version number - - #Convert current time into human understandable format - dts = UA_DateTime_toStruct(currenttime) - - #Print results to terminal - Printf.@printf("current date and time (UTC) is: %u-%u-%u %u:%u:%u.%03u\n", dts.day, dts.month, dts.year, dts.hour, dts.min, dts.sec, dts.milliSec) - Printf.@printf("The server is running open62541 version %s.", version) - - #disconnect the client (good housekeeping practice) - JUA_Client_disconnect(client) +using open62541 +using Printf + +#initiate client, configure it and connect to server +client = JUA_Client() +config = JUA_ClientConfig(client) +JUA_ClientConfig_setDefault(config) +JUA_Client_connect(client, "opc.tcp://localhost:4840") + +#define nodeids that we are interested in +nodeid_currenttime = JUA_NodeId(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME) +nodeid_version = JUA_NodeId(0, UA_NS0ID_SERVER_SERVERSTATUS_BUILDINFO_SOFTWAREVERSION) + +#read data from nodeids +currenttime = JUA_Client_readValueAttribute(client, nodeid_currenttime) #Int64 which represents the number of 100 nanosecond intervals since January 1, 1601 (UTC) +version = JUA_Client_readValueAttribute(client, nodeid_version) #String containing open62541 version number + +#Convert current time into human understandable format +dts = UA_DateTime_toStruct(currenttime) + +#Print results to terminal +Printf.@printf("current date and time (UTC) is: %u-%u-%u %u:%u:%u.%03u\n", + dts.day, dts.month, dts.year, dts.hour, dts.min, dts.sec, dts.milliSec) +Printf.@printf("The server is running open62541 version %s.", version) + +#disconnect the client (good housekeeping practice) +JUA_Client_disconnect(client) ``` diff --git a/docs/src/tutorials/server_first_steps.md b/docs/src/tutorials/server_first_steps.md index 8048a7e..b422404 100644 --- a/docs/src/tutorials/server_first_steps.md +++ b/docs/src/tutorials/server_first_steps.md @@ -1,18 +1,21 @@ # First steps with an open62541 server + Starting up a server with a default configuration in open62541.jl is very simple. Just execute the following code in the REPL or as a script: + ```julia - using open62541 - server = JUA_Server() - config = JUA_ServerConfig(server) - JUA_ServerConfig_setDefault(config) - JUA_Server_runUntilInterrupt(server) +using open62541 +server = JUA_Server() +config = JUA_ServerConfig(server) +JUA_ServerConfig_setDefault(config) +JUA_Server_runUntilInterrupt(server) ``` + This will configure a server with the default configuration (address: opc.tcp://localhost:4840/) - and start it. The server can be shut down by pressing CTRL+C multiple times. +and start it. The server can be shut down by pressing CTRL+C multiple times. -While the server is running, it can be accessed via the Client API of open62541.jl +While the server is running, it can be accessed via the Client API of open62541.jl or it can be browsed and accessed with a graphical client, such as [UA Expert](https://www.unified-automation.com/products/development-tools/uaexpert.html). -Subsequent tutorials will explain how to add your own variables, objects, and +Subsequent tutorials will explain how to add your own variables, objects, and methods to the server. diff --git a/src/attribute_generation.jl b/src/attribute_generation.jl index 78a0a4a..9489c1c 100644 --- a/src/attribute_generation.jl +++ b/src/attribute_generation.jl @@ -6,7 +6,7 @@ UA_VALUERANK(N::Integer)::Integer ``` -returns the valuerank based on the dimensionality of an array `N`. For special +returns the valuerank based on the dimensionality of an array `N`. For special cases see here: [Unified Automation Website](https://documentation.unified-automation.com/uasdknet/2.5.7/html/classUnifiedAutomation_1_1UaBase_1_1ValueRanks.html#details) """ function UA_VALUERANK(N::Integer) @@ -27,10 +27,10 @@ calculates a `UInt8` number expressing how the value of a variable can be access Default is to disallow all operations. The meaning of the keywords is explained here: [OPC Foundation Website](https://reference.opcfoundation.org/Core/Part3/v105/docs/8.57) """ -function UA_ACCESSLEVEL(; read::Bool = false, write::Bool = false, - historyread::Bool = false, historywrite::Bool = false, - semanticchange::Bool = false, statuswrite::Bool = false, - timestampwrite::Bool = false) +function UA_ACCESSLEVEL(; read::Bool = false, write::Bool = false, + historyread::Bool = false, historywrite::Bool = false, + semanticchange::Bool = false, statuswrite::Bool = false, + timestampwrite::Bool = false) al = UA_Byte(0) al = read ? al | UA_ACCESSLEVELMASK_READ : al al = write ? al | UA_ACCESSLEVELMASK_WRITE : al @@ -53,12 +53,12 @@ calculates a `UInt8` number expressing how the value of a variable can be access Default is to disallow all operations. The meaning of the keywords is explained here: [OPC Foundation Website](https://reference.opcfoundation.org/Core/Part3/v105/docs/8.57) """ -function UA_USERACCESSLEVEL(; read::Bool = false, write::Bool = false, - historyread::Bool = false, historywrite::Bool = false, +function UA_USERACCESSLEVEL(; read::Bool = false, write::Bool = false, + historyread::Bool = false, historywrite::Bool = false, semanticchange::Bool = false, statuswrite::Bool = false, timestampwrite::Bool = false) - UA_ACCESSLEVEL(; read = read, write = write, historyread = historyread, - historywrite = historywrite, semanticchange = semanticchange, + UA_ACCESSLEVEL(; read = read, write = write, historyread = historyread, + historywrite = historywrite, semanticchange = semanticchange, statuswrite = statuswrite, timestampwrite = timestampwrite) end @@ -141,13 +141,13 @@ function UA_USERWRITEMASK(; accesslevel::Bool = false, arraydimensions::Bool = f userexecutable::Bool = false, userwritemask::Bool = false, valuerank::Bool = false, writemask::Bool = false, valueforvariabletype::Bool = false) UA_WRITEMASK(; accesslevel = accesslevel, arraydimensions = arraydimensions, - browsename = browsename, containsnoloops = containsnoloops, datatype = datatype, - description = description , displayname = displayname , eventnotifier = eventnotifier, - executable = executable, historizing = historizing, inversename = inversename, - isabstract = isabstract, minimumsamplinginterval = minimumsamplinginterval, nodeclass = nodeclass, - nodeid = nodeid, symmetric = symmetric, useraccesslevel = useraccesslevel, - userexecutable = userexecutable, userwritemask = userwritemask, valuerank = valuerank, - writemask = writemask, valueforvariabletype = valueforvariabletype) + browsename = browsename, containsnoloops = containsnoloops, datatype = datatype, + description = description, displayname = displayname, eventnotifier = eventnotifier, + executable = executable, historizing = historizing, inversename = inversename, + isabstract = isabstract, minimumsamplinginterval = minimumsamplinginterval, nodeclass = nodeclass, + nodeid = nodeid, symmetric = symmetric, useraccesslevel = useraccesslevel, + userexecutable = userexecutable, userwritemask = userwritemask, valuerank = valuerank, + writemask = writemask, valueforvariabletype = valueforvariabletype) end """ @@ -195,7 +195,8 @@ function __set_generic_attributes!(attr, end function __set_scalar_attributes!(attr, value::T, - valuerank) where {T <: Union{AbstractFloat, Integer, Ptr{UA_String}, UA_ComplexNumberType, UA_DoubleComplexNumberType}} + valuerank) where {T <: Union{AbstractFloat, Integer, Ptr{UA_String}, + UA_ComplexNumberType, UA_DoubleComplexNumberType}} type_ptr = ua_data_type_ptr_default(T) attr.valueRank = valuerank UA_Variant_setScalarCopy(attr.value, wrap_ref(value), type_ptr) @@ -209,7 +210,8 @@ function __set_scalar_attributes!(attr, value::AbstractString, valuerank) return nothing end -function __set_scalar_attributes!(attr, value::Complex{T}, valuerank) where T <: Union{Float32, Float64} +function __set_scalar_attributes!( + attr, value::Complex{T}, valuerank) where {T <: Union{Float32, Float64}} f = T == Float32 ? UA_ComplexNumberType : UA_DoubleComplexNumberType ua_c = f(reim(value)...) __set_scalar_attributes!(attr, ua_c, valuerank) @@ -225,7 +227,8 @@ function __set_array_attributes!(attr, value::AbstractArray{<:AbstractString}, v return nothing end -function __set_array_attributes!(attr, value::AbstractArray{<:Complex{T}}, valuerank) where T <: Union{Float32, Float64} +function __set_array_attributes!(attr, value::AbstractArray{<:Complex{T}}, + valuerank) where {T <: Union{Float32, Float64}} f = T == Float32 ? UA_ComplexNumberType : UA_DoubleComplexNumberType a = similar(value, f) for i in eachindex(a) @@ -236,7 +239,10 @@ function __set_array_attributes!(attr, value::AbstractArray{<:Complex{T}}, value end function __set_array_attributes!(attr, value::AbstractArray{T, N}, - valuerank) where {T <: Union{AbstractFloat, Integer, UA_String, UA_ComplexNumberType, UA_DoubleComplexNumberType}, N} + valuerank) where { + T <: Union{AbstractFloat, Integer, UA_String, + UA_ComplexNumberType, UA_DoubleComplexNumberType}, + N} type_ptr = ua_data_type_ptr_default(T) attr.valueRank = valuerank #Note: need array dims twice, once to put into the variant, i.e., attr.value @@ -376,21 +382,25 @@ by C and needs to be cleaned up by calling `UA_VariableTypeAttributes_delete(x)` after usage. For keywords given as `nothing`, the respective default value is used, see `UA_VariableTypeAttributes_default[]`. -If a default `value` is specified for the variabletype and nothing is given for -keyword `valuerank`, then it is either set to `UA_VALUERANK_SCALAR` (if `value` -is a scalar), or to the dimensionality of the supplied array (i.e., `N` for an +If a default `value` is specified for the variabletype and nothing is given for +keyword `valuerank`, then it is either set to `UA_VALUERANK_SCALAR` (if `value` +is a scalar), or to the dimensionality of the supplied array (i.e., `N` for an AbstractArray{T,N}). See also [`UA_WRITEMASK`](@ref), [`UA_USERWRITEMASK`](@ref) for information on how to generate the respective keyword inputs. """ -function UA_VariableTypeAttributes_generate(; value::Union{AbstractArray{<:Union{AbstractString, AbstractFloat, Integer, ComplexF32, ComplexF64}}, Union{Nothing, AbstractString, AbstractFloat, Integer, ComplexF32, ComplexF64}} = nothing, +function UA_VariableTypeAttributes_generate(; + value::Union{ + AbstractArray{<:Union{ + AbstractString, AbstractFloat, Integer, ComplexF32, ComplexF64}}, + Union{Nothing, AbstractString, AbstractFloat, Integer, ComplexF32, ComplexF64}} = nothing, displayname::AbstractString, description::AbstractString, localization::AbstractString = "en-US", writemask::Union{Nothing, UInt32} = nothing, userwritemask::Union{Nothing, UInt32} = nothing, valuerank::Union{Nothing, Integer} = nothing, - isabstract::Union{Nothing, Bool} = nothing) + isabstract::Union{Nothing, Bool} = nothing) attr = __generate_variabletype_attributes(value, displayname, description, localization, writemask, userwritemask, valuerank, isabstract) return attr @@ -398,7 +408,8 @@ end function __generate_variabletype_attributes(value::AbstractArray{T, N}, displayname, description, localization, writemask, userwritemask, valuerank, - isabstract) where {T <: Union{AbstractString, AbstractFloat, Integer, ComplexF32, ComplexF64}, N} + isabstract) where { + T <: Union{AbstractString, AbstractFloat, Integer, ComplexF32, ComplexF64}, N} if isnothing(valuerank) valuerank = UA_VALUERANK(N) end @@ -410,7 +421,8 @@ end function __generate_variabletype_attributes(value::T, displayname, description, localization, writemask, userwritemask, valuerank, - isabstract) where {T <: Union{AbstractString, AbstractFloat, Integer, ComplexF32, ComplexF64}} + isabstract) where {T <: Union{ + AbstractString, AbstractFloat, Integer, ComplexF32, ComplexF64}} if isnothing(valuerank) valuerank = UA_VALUERANK_SCALAR end diff --git a/src/server.jl b/src/server.jl index b0ccfe3..3fcc78a 100644 --- a/src/server.jl +++ b/src/server.jl @@ -179,6 +179,8 @@ for att in attributes_UA_Server_read Uses the Server API to read the value of the attribute $($(String(attr_name))) from the NodeId `nodeId` located on server `server`. The result is saved into the buffer `out`. + + M """ function $(fun_name)(server, nodeId, out = $(ret_type)()) statuscode = __UA_Server_read(server, nodeId, $(ua_attr_name), out) diff --git a/src/types.jl b/src/types.jl index 4c868ad..4adcbd5 100644 --- a/src/types.jl +++ b/src/types.jl @@ -98,7 +98,9 @@ for (i, type_name) in enumerate(type_names) ua_data_type_ptr(::$(val_type)) = UA_TYPES_PTRS[$(i - 1)] if !(type_names[$(i)] in types_ambiguous_ignorelist) ua_data_type_ptr_default(::Type{$(julia_type)}) = UA_TYPES_PTRS[$(i - 1)] - ua_data_type_ptr_default(::Type{Ptr{$julia_type}}) = ua_data_type_ptr_default($julia_type) + function ua_data_type_ptr_default(::Type{Ptr{$julia_type}}) + ua_data_type_ptr_default($julia_type) + end Base.show(io::IO, ::MIME"text/plain", v::$(julia_type)) = print(io, UA_print(v)) end @@ -282,10 +284,10 @@ function UA_STRING(s::AbstractString) return UA_STRING_ALLOC(s) end -function Base.unsafe_string(s::UA_String) +function Base.unsafe_string(s::UA_String) if s.length == 0 #catch NullString u = "" - else + else u = unsafe_string(s.data, s.length) end return u @@ -293,8 +295,10 @@ end Base.unsafe_string(s::Ptr{UA_String}) = Base.unsafe_string(unsafe_load(s)) #adapt base complex method for interoperability between ua complex numbers and julia complex numbers -Base.complex(x::T) where T <: Union{UA_ComplexNumberType, UA_DoubleComplexNumberType} = Complex(x.real, x.imaginary) - +function Base.complex(x::T) where {T <: + Union{UA_ComplexNumberType, UA_DoubleComplexNumberType}} + Complex(x.real, x.imaginary) +end ## UA_BYTESTRING """ @@ -418,10 +422,10 @@ UA_NODEID_STRING_ALLOC(nsIndex::Integer, identifier::AbstractString)::Ptr{UA_Nod UA_NODEID_STRING_ALLOC(nsIndex::Integer, identifier::Ptr{UA_String})::Ptr{UA_NodeId} ``` -creates a `UA_NodeId` object with namespace index `nsIndex` and string identifier +creates a `UA_NodeId` object with namespace index `nsIndex` and string identifier `identifier`. -Memory is allocated by C and needs to be cleaned up using +Memory is allocated by C and needs to be cleaned up using `UA_NodeId_delete(x::Ptr{UA_NodeId})` after the object is not used anymore. """ function UA_NODEID_STRING_ALLOC(nsIndex::Integer, identifier::Ptr{UA_String}) @@ -445,10 +449,10 @@ UA_NODEID_STRING(nsIndex::Integer, identifier::AbstractString)::Ptr{UA_NodeId} UA_NODEID_STRING(nsIndex::Integer, identifier::Ptr{UA_String})::Ptr{UA_NodeId} ``` -creates a `UA_NodeId` object by with namespace index `nsIndex` and string identifier +creates a `UA_NodeId` object by with namespace index `nsIndex` and string identifier `identifier`. -Memory is allocated by C and needs to be cleaned up using +Memory is allocated by C and needs to be cleaned up using `UA_NodeId_delete(x::Ptr{UA_NodeId})` after the object is not used anymore. """ function UA_NODEID_STRING(nsIndex::Integer, @@ -462,10 +466,10 @@ UA_NODEID_BYTESTRING_ALLOC(nsIndex::Integer, identifier::AbstractString)::Ptr{UA UA_NODEID_BYTESTRING_ALLOC(nsIndex::Integer, identifier::Ptr{UA_ByteString})::Ptr{UA_NodeId} ``` -creates a `UA_NodeId` object with namespace index `nsIndex` and bytestring +creates a `UA_NodeId` object with namespace index `nsIndex` and bytestring identifier `identifier` (which can be a string or UA_ByteString). -Memory is allocated by C and needs to be cleaned up using +Memory is allocated by C and needs to be cleaned up using `UA_NodeId_delete(x::Ptr{UA_NodeId})` after the object is not used anymore. """ function UA_NODEID_BYTESTRING_ALLOC(nsIndex::Integer, identifier::Ptr{UA_String}) @@ -489,10 +493,10 @@ UA_NODEID_BYTESTRING(nsIndex::Integer, identifier::AbstractString)::Ptr{UA_NodeI UA_NODEID_BYTESTRING(nsIndex::Integer, identifier::Ptr{UA_ByteString})::Ptr{UA_NodeId} ``` -creates a `UA_NodeId` object with namespace index `nsIndex` and bytestring +creates a `UA_NodeId` object with namespace index `nsIndex` and bytestring identifier `identifier` (which can be a string or UA_ByteString). -Memory is allocated by C and needs to be cleaned up using +Memory is allocated by C and needs to be cleaned up using `UA_NodeId_delete(x::Ptr{UA_NodeId})` after the object is not used anymore. """ function UA_NODEID_BYTESTRING(nsIndex::Integer, @@ -506,11 +510,11 @@ UA_NODEID_GUID(nsIndex::Integer, identifier::AbstractString)::Ptr{UA_NodeId} UA_NODEID_GUID(nsIndex::Integer, identifier::Ptr{UA_Guid})::Ptr{UA_NodeId} ``` -creates a `UA_NodeId` object by with namespace index `nsIndex` and an identifier -`identifier` based on a globally unique id (`UA_Guid`) that can be supplied as a +creates a `UA_NodeId` object by with namespace index `nsIndex` and an identifier +`identifier` based on a globally unique id (`UA_Guid`) that can be supplied as a string (which will be parsed) or as a valid `Ptr{UA_Guid}`. -Memory is allocated by C and needs to be cleaned up using +Memory is allocated by C and needs to be cleaned up using `UA_NodeId_delete(x::Ptr{UA_NodeId})` after the object is not used anymore. """ function UA_NODEID_GUID(nsIndex, guid::Ptr{UA_Guid}) @@ -546,8 +550,8 @@ UA_EXPANDEDNODEID(s::AbstractString)::Ptr{UA_ExpandedNodeId} UA_EXPANDEDNODEID(s::Ptr{UA_String})::Ptr{UA_ExpandedNodeId} ``` -creates a `UA_ExpandedNodeId` object by parsing `s`. Memory is allocated by C and -needs to be cleaned up using `UA_ExpandedNodeId_delete(x::Ptr{UA_ExpandedNodeId})` +creates a `UA_ExpandedNodeId` object by parsing `s`. Memory is allocated by C and +needs to be cleaned up using `UA_ExpandedNodeId_delete(x::Ptr{UA_ExpandedNodeId})` after the object is not used anymore. See also: [OPC Foundation Website](https://reference.opcfoundation.org/Core/Part6/v105/docs/5.2.2.10) @@ -788,7 +792,10 @@ Base.length(v::UA_Variant) = Int(v.arrayLength) Base.length(p::Ref{UA_Variant}) = length(unsafe_load(p)) function UA_Variant_new(value::AbstractArray{T, N}, - type_ptr::Ptr{UA_DataType} = ua_data_type_ptr_default(T)) where {T <: Union{AbstractFloat, UA_String, Integer, UA_ComplexNumberType, UA_DoubleComplexNumberType}, N} + type_ptr::Ptr{UA_DataType} = ua_data_type_ptr_default(T)) where { + T <: Union{AbstractFloat, UA_String, Integer, + UA_ComplexNumberType, UA_DoubleComplexNumberType}, + N} var = UA_Variant_new() var.type = type_ptr var.storageType = UA_VARIANT_DATA @@ -817,7 +824,7 @@ function UA_Variant_new(value::AbstractString) return v end -function UA_Variant_new(value::Complex{T}) where T <: Union{Float32, Float64} +function UA_Variant_new(value::Complex{T}) where {T <: Union{Float32, Float64}} f = T == Float32 ? UA_ComplexNumberType : UA_DoubleComplexNumberType ua_c = f(reim(value)...) v = UA_Variant_new(ua_c) @@ -828,11 +835,12 @@ function UA_Variant_new(value::AbstractArray{<:AbstractString}) a = similar(value, UA_String) for i in eachindex(a) a[i] = UA_String_fromChars(value[i]) - end + end return UA_Variant_new(a) end -function UA_Variant_new(value::AbstractArray{<:Complex{T}}) where T <: Union{Float32, Float64} +function UA_Variant_new(value::AbstractArray{<:Complex{T}}) where {T <: + Union{Float32, Float64}} f = T == Float32 ? UA_ComplexNumberType : UA_DoubleComplexNumberType a = similar(value, f) for i in eachindex(a) diff --git a/src/wrappers.jl b/src/wrappers.jl index ebe7be1..7fcaf78 100644 --- a/src/wrappers.jl +++ b/src/wrappers.jl @@ -84,12 +84,12 @@ const JUA_AccessControl_defaultWithLoginCallback = UA_AccessControl_defaultWithL function JUA_Server_runUntilInterrupt(server::JUA_Server) running = Ref(true) - try + try Base.exit_on_sigint(false) UA_Server_run(server, running) catch err UA_Server_run_shutdown(server) - println("Shutting down server.") + println("Shutting down server.") Base.exit_on_sigint(true) end return nothing @@ -122,48 +122,54 @@ const JUA_ClientConfig_setDefault = UA_ClientConfig_setDefault const JUA_Client_connect = UA_Client_connect const JUA_Client_disconnect = UA_Client_disconnect - ## NodeIds """ ``` JUA_NodeId ``` -creates a `JUA_NodeId` object - the equivalent of a `UA_NodeId`, but with memory +creates a `JUA_NodeId` object - the equivalent of a `UA_NodeId`, but with memory managed by Julia rather than C. The following methods are defined: + ``` JUA_NodeId() ``` -creates a `JUA_NodeId` with namespaceIndex = 0, numeric identifierType and + +creates a `JUA_NodeId` with namespaceIndex = 0, numeric identifierType and identifier = 0 ``` JUA_NodeId(s::Union{AbstractString, JUA_String}) ``` -creates a `JUA_NodeId` based on String `s` that is parsed into the relevant -properties. + +creates a `JUA_NodeId` based on String `s` that is parsed into the relevant +properties. ``` JUA_NodeId(nsIndex::Integer, identifier::Integer) ``` -creates a `JUA_NodeId` with namespace index `nsIndex` and numerical identifier + +creates a `JUA_NodeId` with namespace index `nsIndex` and numerical identifier `identifier`. ``` JUA_NodeId(nsIndex::Integer, identifier::Union{AbstractString, JUA_String}) ``` -creates a `JUA_NodeId` with namespace index `nsIndex` and string identifier + +creates a `JUA_NodeId` with namespace index `nsIndex` and string identifier `identifier`. ``` JUA_NodeId(nsIndex::Integer, identifier::JUA_Guid) ``` -creates a `JUA_NodeId` with namespace index `nsIndex` and global unique id identifier + +creates a `JUA_NodeId` with namespace index `nsIndex` and global unique id identifier `identifier`. Examples: + ``` j = JUA_NodeId() j = JUA_NodeId("ns=1;i=1234") @@ -172,7 +178,6 @@ j = JUA_NodeId(1, 1234) j = JUA_NodeId(1, "example") j = JUA_NodeId(1, JUA_Guid("C496578A-0DFE-4B8F-870A-745238C6AEAE")) ``` - """ mutable struct JUA_NodeId <: AbstractOpen62541Wrapper ptr::Ptr{UA_NodeId} @@ -274,7 +279,7 @@ function release_handle(obj::JUA_Variant) UA_Variant_delete(Jpointer(obj)) end -function JUA_Client_readValueAttribute(client, nodeId) +function JUA_Client_readValueAttribute(client, nodeId) #TODO: Is there a way of making this typestable? #(it's not really known what kind of data is stored inside a nodeid unless #one checks the datatype beforehand) @@ -292,7 +297,7 @@ function JUA_Client_writeValueAttribute(client, nodeId, newvalue) return statuscode end -function JUA_Server_readValue(client, nodeId) +function JUA_Server_readValue(client, nodeId) #TODO: Is there a way of making this typestable? #(it's not really known what kind of data is stored inside a nodeid unless #one checks the datatype beforehand) @@ -316,7 +321,7 @@ end function __get_juliavalues_from_variant(v) wrapped = unsafe_wrap(v) - if typeof(wrapped) == UA_ExtensionObject + if typeof(wrapped) == UA_ExtensionObject wrapped = __extract_ExtensionObject.(wrapped) elseif typeof(wrapped) <: Array && eltype(wrapped) == UA_ExtensionObject wrapped = __extract_ExtensionObject.(wrapped) @@ -325,7 +330,8 @@ function __get_juliavalues_from_variant(v) #now deal with special types if typeof(wrapped) <: Union{UA_ComplexNumberType, UA_DoubleComplexNumberType} r = complex(wrapped) - elseif typeof(wrapped) <: Array && eltype(wrapped) <: Union{UA_ComplexNumberType, UA_DoubleComplexNumberType} + elseif typeof(wrapped) <: Array && + eltype(wrapped) <: Union{UA_ComplexNumberType, UA_DoubleComplexNumberType} r = complex.(wrapped) elseif typeof(wrapped) == UA_String r = unsafe_string(wrapped) diff --git a/test/add_change_var_array.jl b/test/add_change_var_array.jl index 1a01096..c210337 100644 --- a/test/add_change_var_array.jl +++ b/test/add_change_var_array.jl @@ -15,11 +15,17 @@ Distributed.@everywhere begin using open62541, Test, Random # What types and sizes we are testing for: - types = [Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float32, Float64, String, ComplexF32, ComplexF64] + types = [Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, + UInt64, Float32, Float64, String, ComplexF32, ComplexF64] array_sizes = (11, (2, 5), (3, 4, 5), (3, 4, 5, 6)) # Generate random input values and generate nodeid names - input_data = Tuple(Tuple( type != String ? rand(type, array_size) : reshape([randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], array_size...) for array_size in array_sizes) for type in types) + input_data = Tuple(Tuple(type != String ? rand(type, array_size) : + reshape( + [randstring(Int64(rand(UInt8))) + for i in 1:prod(array_size)], + array_size...) for array_size in array_sizes) + for type in types) varnode_ids = ["$(string(array_size)) $(Symbol(type)) array variable" for type in types, array_size in array_sizes] end @@ -105,7 +111,9 @@ end # Write new data for (type_ind, type) in enumerate(types) for (array_size_ind, array_size) in enumerate(array_sizes) - new_input = type != String ? rand(type, array_size) : reshape([randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], array_size...) + new_input = type != String ? rand(type, array_size) : + reshape( + [randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], array_size...) varnodeid = JUA_NodeId(1, varnode_ids[type_ind, array_size_ind]) retval = JUA_Client_writeValueAttribute(client, varnodeid, new_input) @test retval == UA_STATUSCODE_GOOD @@ -121,14 +129,16 @@ end # Test wrong data type write errors for type_ind in eachindex(types) for (array_size_ind, array_size) in enumerate(array_sizes) - if types[type_ind] == ComplexF64 || types[type_ind] == ComplexF32 + if types[type_ind] == ComplexF64 || types[type_ind] == ComplexF32 type = Float64 elseif types[type_ind] == String #XXX: This is likely a bug in open62541 (can write complex numbers to string type variable, probably, because extension objects are not recognized properly) type = Float64 else type = types[mod(type_ind, length(types)) + 1] # Select wrong data type end - new_input = type != String ? rand(type, array_size) : reshape([randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], array_size...) + new_input = type != String ? rand(type, array_size) : + reshape( + [randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], array_size...) varnodeid = JUA_NodeId(1, varnode_ids[type_ind, array_size_ind]) @test_throws open62541.AttributeReadWriteError JUA_Client_writeValueAttribute( client, varnodeid, new_input) diff --git a/test/add_change_var_scalar.jl b/test/add_change_var_scalar.jl index c83be9e..f6e65ff 100644 --- a/test/add_change_var_scalar.jl +++ b/test/add_change_var_scalar.jl @@ -15,10 +15,12 @@ Distributed.@everywhere begin using open62541, Test, Random # What types we are testing for: - types = [Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float32, Float64, String, ComplexF32, ComplexF64] - + types = [Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, + UInt64, Float32, Float64, String, ComplexF32, ComplexF64] + # Generate random input values and generate nodeid names - input_data = Tuple(type != String ? rand(type) : randstring(Int64(rand(UInt8))) for type in types) + input_data = Tuple(type != String ? rand(type) : randstring(Int64(rand(UInt8))) + for type in types) varnode_ids = ["$(Symbol(type)) scalar variable" for type in types] end @@ -43,7 +45,7 @@ Distributed.@spawnat Distributed.workers()[end] begin typedefinition = JUA_NodeId(0, UA_NS0ID_BASEDATAVARIABLETYPE) browsename = JUA_QualifiedName(1, varnode_ids[type_ind]) nodecontext = JUA_NodeId() - outnewnodeid = JUA_Nodeid() + outnewnodeid = JUA_NodeId() retval = UA_Server_addVariableNode(server, varnodeid, parentnodeid, parentreferencenodeid, browsename, typedefinition, attr, nodecontext, outnewnodeid) diff --git a/test/attribute_generation.jl b/test/attribute_generation.jl index e5204f3..7090869 100644 --- a/test/attribute_generation.jl +++ b/test/attribute_generation.jl @@ -11,7 +11,7 @@ using Random @test UA_VALUERANK(-2) == -2 #UA_ACCESSLEVEL -@test UA_ACCESSLEVEL() == 0 +@test UA_ACCESSLEVEL() == 0 @test UA_ACCESSLEVEL(write = true) == UA_ACCESSLEVELMASK_WRITE @test UA_ACCESSLEVEL(write = true, read = true) == UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE @@ -23,11 +23,12 @@ using Random UA_ACCESSLEVELMASK_TIMESTAMPWRITE #UA_USERACCESSLEVEL -@test UA_USERACCESSLEVEL() == 0 +@test UA_USERACCESSLEVEL() == 0 @test UA_USERACCESSLEVEL(write = true) == UA_ACCESSLEVELMASK_WRITE @test UA_USERACCESSLEVEL(write = true, read = true) == UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE -@test UA_USERACCESSLEVEL(read = true, write = true, historyread = true, historywrite = true, +@test UA_USERACCESSLEVEL( + read = true, write = true, historyread = true, historywrite = true, semanticchange = true, timestampwrite = true, statuswrite = true) == UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE | UA_ACCESSLEVELMASK_HISTORYREAD | UA_ACCESSLEVELMASK_HISTORYWRITE | @@ -76,7 +77,6 @@ using Random UA_WRITEMASK_USERWRITEMASK | UA_WRITEMASK_VALUERANK | UA_WRITEMASK_WRITEMASK | UA_WRITEMASK_VALUEFORVARIABLETYPE - #UA_EVENTNOTIFIER @test UA_EVENTNOTIFIER() == 0 @test UA_EVENTNOTIFIER(subscribetoevent = true, historyread = true, historywrite = true) == @@ -86,8 +86,11 @@ using Random #UA_VariableAttributes_generate #define different sized input cases to test both scalar and array codes array_sizes = [1, 2, (2, 3), (2, 3, 4)] -types = [Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float32, Float64, String, ComplexF32, ComplexF64] -inputs = Tuple(Tuple( type != String ? rand(type, array_size) : reshape([randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], array_size...) for array_size in array_sizes) for type in types) +types = [Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, + UInt64, Float32, Float64, String, ComplexF32, ComplexF64] +inputs = Tuple(Tuple(type != String ? rand(type, array_size) : + reshape([randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], + array_size...) for array_size in array_sizes) for type in types) valueranks = [-1, 1, 2, 3] displayname = "whatever" description = "this is a whatever variable" @@ -145,8 +148,11 @@ end #UA_VariableTypeAttributes_generate #define different sized input cases to test both scalar and array codes array_sizes = [1, 2, (2, 3), (2, 3, 4)] -types = [Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float32, Float64, String, ComplexF32, ComplexF64] -inputs = Tuple(Tuple( type != String ? rand(type, array_size) : reshape([randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], array_size...) for array_size in array_sizes) for type in types) +types = [Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, + UInt64, Float32, Float64, String, ComplexF32, ComplexF64] +inputs = Tuple(Tuple(type != String ? rand(type, array_size) : + reshape([randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], + array_size...) for array_size in array_sizes) for type in types) valueranks = [-1, 1, 2, 3] displayname = "whatever" description = "this is a whatever variable" @@ -180,7 +186,7 @@ for i in eachindex(array_sizes) @test all(out == v) end UA_VariableTypeAttributes_delete(attr) - + #high level interface j = JUA_VariableTypeAttributes(value = v, displayname = displayname, description = description, localization = localization, @@ -195,7 +201,7 @@ def = UA_VariableTypeAttributes_default[] ## use only mandatory keywords attr = UA_VariableTypeAttributes_generate(displayname = displayname, - description = description, localization = localization) + description = description, localization = localization) @test unsafe_string(unsafe_load(attr.displayName.text)) == displayname @test unsafe_string(unsafe_load(attr.displayName.locale)) == localization @test unsafe_string(unsafe_load(attr.description.text)) == description @@ -203,15 +209,15 @@ attr = UA_VariableTypeAttributes_generate(displayname = displayname, @test unsafe_load(attr.writeMask) == def.writeMask @test unsafe_load(attr.userWriteMask) == def.userWriteMask @test unsafe_load(attr.valueRank) == def.valueRank -@test unsafe_load(attr.isAbstract) == def.isAbstract +@test unsafe_load(attr.isAbstract) == def.isAbstract UA_VariableTypeAttributes_delete(attr) ## use optional keywords as well valuerank = 1 attr = UA_VariableTypeAttributes_generate(displayname = displayname, - description = description, localization = localization, - writemask = writemask, userwritemask = userwritemask, - isabstract = isabstract, valuerank = valuerank) + description = description, localization = localization, + writemask = writemask, userwritemask = userwritemask, + isabstract = isabstract, valuerank = valuerank) @test unsafe_string(unsafe_load(attr.displayName.text)) == displayname @test unsafe_string(unsafe_load(attr.displayName.locale)) == localization From 58b3bb76e7e94f48c5985ed290f8b13cecdecc83 Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Mon, 13 May 2024 23:08:42 +0200 Subject: [PATCH 11/26] remove callback tests for mac os x (LLVM limitation) --- test/callbacks.jl | 73 ++++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/test/callbacks.jl b/test/callbacks.jl index 8bb9997..61653eb 100644 --- a/test/callbacks.jl +++ b/test/callbacks.jl @@ -1,44 +1,47 @@ using open62541 using Test -#find all defined callbacks in open62541 (src/callbacks.jl file) -callbacks = filter(x -> occursin(r"Callback.*_generate", string(x)), names(open62541)) +if !Sys.isapple() #closures not supported on Apple M1 processors, LLVM limitation. -#initialized return type list - only callbacks with non-Nothing return types need -#to be added here -callback_names = [ - :UA_MethodCallback_generate, - :UA_NodeTypeLifecycleCallback_constructor_generate, - :UA_DataSourceCallback_read_generate, - :UA_DataSourceCallback_write_generate -] -return_types = ["UA_UInt32(0)", "UA_UInt32(0)", "UA_UInt32(0)", "UA_UInt32(0)"] + #find all defined callbacks in open62541 (src/callbacks.jl file) + callbacks = filter(x -> occursin(r"Callback.*_generate", string(x)), names(open62541)) -#test exception branch of all callbacks -for callback in callbacks - #checks exception branch of the callback - a = () -> 0.0 - @test_throws open62541.CallbackGeneratorArgumentError eval(callback)(a) + #initialized return type list - only callbacks with non-Nothing return types need + #to be added here + callback_names = [ + :UA_MethodCallback_generate, + :UA_NodeTypeLifecycleCallback_constructor_generate, + :UA_DataSourceCallback_read_generate, + :UA_DataSourceCallback_write_generate + ] + return_types = ["UA_UInt32(0)", "UA_UInt32(0)", "UA_UInt32(0)", "UA_UInt32(0)"] - #checks the main branch of the callback - try - eval(callback)(a) - catch e - i = findfirst(x -> x == callback, callback_names) - if !isnothing(i) - rettype = return_types[i] - else - rettype = nothing - end + #test exception branch of all callbacks + for callback in callbacks + #checks exception branch of the callback + a = () -> 0.0 + @test_throws open62541.CallbackGeneratorArgumentError eval(callback)(a) + + #checks the main branch of the callback + try + eval(callback)(a) + catch e + i = findfirst(x -> x == callback, callback_names) + if !isnothing(i) + rettype = return_types[i] + else + rettype = nothing + end - args = e.argtuple - str = "prot(::" * string(args[1]) - for i in 2:length(args) - str = str * ", ::" * string(args[i]) + args = e.argtuple + str = "prot(::" * string(args[1]) + for i in 2:length(args) + str = str * ", ::" * string(args[i]) + end + str = str * ") = $rettype" + eval(Meta.parse(str)) + @test !isa(eval(callback)(prot), Exception) + Base.delete_method(methods(prot)[1]) end - str = str * ") = $rettype" - eval(Meta.parse(str)) - @test !isa(eval(callback)(prot), Exception) - Base.delete_method(methods(prot)[1]) end -end +end \ No newline at end of file From 28f8647ebdfb03aa13c77d94c40a307b2a543b8d Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Mon, 13 May 2024 23:17:40 +0200 Subject: [PATCH 12/26] remove method node test on mac os x --- test/server_add_nodes.jl | 88 ++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/test/server_add_nodes.jl b/test/server_add_nodes.jl index 92cccfd..33f21a9 100644 --- a/test/server_add_nodes.jl +++ b/test/server_add_nodes.jl @@ -294,50 +294,50 @@ function helloWorldMethodCallback(server, sessionId, sessionHandle, methodId, return UA_STATUSCODE_GOOD end -inputArgument = UA_Argument_new() -inputArgument.description = UA_LOCALIZEDTEXT("en-US", "A String") -inputArgument.name = UA_STRING("MyInput"); -inputArgument.dataType = UA_TYPES_PTRS[UA_TYPES_STRING].typeId; -inputArgument.valueRank = UA_VALUERANK_SCALAR -outputArgument = UA_Argument_new() -outputArgument.description = UA_LOCALIZEDTEXT("en-US", "A String"); -outputArgument.name = UA_STRING("MyOutput"); -outputArgument.dataType = UA_TYPES_PTRS[UA_TYPES_STRING].typeId -outputArgument.valueRank = UA_VALUERANK_SCALAR -helloAttr = UA_MethodAttributes_generate(description = "Say Hello World", - displayname = "Hello World", - executable = true, - userexecutable = true) - -methodid = UA_NODEID_NUMERIC(1, 62541) -obj = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER) -retval = UA_Server_addMethodNode(server, methodid, - obj, - UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), - UA_QUALIFIEDNAME(1, "hello world"), - helloAttr, helloWorldMethodCallback, - 1, inputArgument, 1, outputArgument, C_NULL, C_NULL) - -@test retval == UA_STATUSCODE_GOOD - -#UA_Server_run(server, Ref(true)) - checking server in uaexpert shows that the -#hello world method is there and that it produces the correct results when called -inputArguments = UA_Variant_new() -ua_s = UA_STRING("Peter") -UA_Variant_setScalar(inputArguments, ua_s, UA_TYPES_PTRS[UA_TYPES_STRING]) -req = UA_CallMethodRequest_new() -req.objectId = obj -req.methodId = methodid -req.inputArgumentsSize = 1 -req.inputArguments = inputArguments - -answer = UA_CallMethodResult_new() -UA_Server_call(server, req, answer) -@test unsafe_load(answer.statusCode) == UA_STATUSCODE_GOOD -@test unsafe_string(unsafe_wrap(unsafe_load(answer.outputArguments))) == "Hello Peter" - -UA_CallMethodRequest_delete(req) -UA_CallMethodResult_delete(answer) +if !Sys.isapple() + inputArgument = UA_Argument_new() + inputArgument.description = UA_LOCALIZEDTEXT("en-US", "A String") + inputArgument.name = UA_STRING("MyInput"); + inputArgument.dataType = UA_TYPES_PTRS[UA_TYPES_STRING].typeId; + inputArgument.valueRank = UA_VALUERANK_SCALAR + outputArgument = UA_Argument_new() + outputArgument.description = UA_LOCALIZEDTEXT("en-US", "A String"); + outputArgument.name = UA_STRING("MyOutput"); + outputArgument.dataType = UA_TYPES_PTRS[UA_TYPES_STRING].typeId + outputArgument.valueRank = UA_VALUERANK_SCALAR + helloAttr = UA_MethodAttributes_generate(description = "Say Hello World", + displayname = "Hello World", + executable = true, + userexecutable = true) + + methodid = UA_NODEID_NUMERIC(1, 62541) + obj = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER) + retval = UA_Server_addMethodNode(server, methodid, + obj, + UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), + UA_QUALIFIEDNAME(1, "hello world"), + helloAttr, helloWorldMethodCallback, + 1, inputArgument, 1, outputArgument, C_NULL, C_NULL) + + @test retval == UA_STATUSCODE_GOOD + + inputArguments = UA_Variant_new() + ua_s = UA_STRING("Peter") + UA_Variant_setScalar(inputArguments, ua_s, UA_TYPES_PTRS[UA_TYPES_STRING]) + req = UA_CallMethodRequest_new() + req.objectId = obj + req.methodId = methodid + req.inputArgumentsSize = 1 + req.inputArguments = inputArguments + + answer = UA_CallMethodResult_new() + UA_Server_call(server, req, answer) + @test unsafe_load(answer.statusCode) == UA_STATUSCODE_GOOD + @test unsafe_string(unsafe_wrap(unsafe_load(answer.outputArguments))) == "Hello Peter" + + UA_CallMethodRequest_delete(req) + UA_CallMethodResult_delete(answer) +end #TODO: this will need a test to see whether any memory is leaking. From c9d9db65ecbeda28caea69bd819630de4c36cbae Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Mon, 13 May 2024 23:22:10 +0200 Subject: [PATCH 13/26] mac os again --- test/server_add_nodes.jl | 46 ++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/test/server_add_nodes.jl b/test/server_add_nodes.jl index 33f21a9..341e769 100644 --- a/test/server_add_nodes.jl +++ b/test/server_add_nodes.jl @@ -270,31 +270,31 @@ function pumpTypeConstructor(server, sessionId, sessionContext, return UA_STATUSCODE_GOOD end -function addPumpTypeConstructor(server) - c_pumpTypeConstructor = UA_NodeTypeLifecycleCallback_constructor_generate(pumpTypeConstructor) - lifecycle = UA_NodeTypeLifecycle(c_pumpTypeConstructor, C_NULL) - UA_Server_setNodeTypeLifecycle(server, pumpTypeId, lifecycle) -end +if !Sys.isapple() + function addPumpTypeConstructor(server) + c_pumpTypeConstructor = UA_NodeTypeLifecycleCallback_constructor_generate(pumpTypeConstructor) + lifecycle = UA_NodeTypeLifecycle(c_pumpTypeConstructor, C_NULL) + UA_Server_setNodeTypeLifecycle(server, pumpTypeId, lifecycle) + end -addPumpObjectInstance(server, "pump2") #should have status = false (constructor not in place yet) -addPumpObjectInstance(server, "pump3") #should have status = false (constructor not in place yet) -addPumpTypeConstructor(server) -addPumpObjectInstance(server, "pump4") #should have status = true -addPumpObjectInstance(server, "pump5") #should have status = true - -#add method node -#follows this: https://www.open62541.org/doc/1.3/tutorial_server_method.html - -function helloWorldMethodCallback(server, sessionId, sessionHandle, methodId, - methodContext, objectId, objectContext, inputSize, input, outputSize, output) - inputstr = unsafe_string(unsafe_wrap(input)) - tmp = UA_STRING("Hello " * inputstr) - UA_Variant_setScalarCopy(output, tmp, UA_TYPES_PTRS[UA_TYPES_STRING]) - UA_String_delete(tmp) - return UA_STATUSCODE_GOOD -end + addPumpObjectInstance(server, "pump2") #should have status = false (constructor not in place yet) + addPumpObjectInstance(server, "pump3") #should have status = false (constructor not in place yet) + addPumpTypeConstructor(server) + addPumpObjectInstance(server, "pump4") #should have status = true + addPumpObjectInstance(server, "pump5") #should have status = true + + #add method node + #follows this: https://www.open62541.org/doc/1.3/tutorial_server_method.html + + function helloWorldMethodCallback(server, sessionId, sessionHandle, methodId, + methodContext, objectId, objectContext, inputSize, input, outputSize, output) + inputstr = unsafe_string(unsafe_wrap(input)) + tmp = UA_STRING("Hello " * inputstr) + UA_Variant_setScalarCopy(output, tmp, UA_TYPES_PTRS[UA_TYPES_STRING]) + UA_String_delete(tmp) + return UA_STATUSCODE_GOOD + end -if !Sys.isapple() inputArgument = UA_Argument_new() inputArgument.description = UA_LOCALIZEDTEXT("en-US", "A String") inputArgument.name = UA_STRING("MyInput"); From 2caf7fd91eb8c110ad67596f77b99215f34da725 Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Mon, 13 May 2024 23:25:33 +0200 Subject: [PATCH 14/26] shorter test strings --- test/add_change_var_array.jl | 6 +++--- test/add_change_var_scalar.jl | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/add_change_var_array.jl b/test/add_change_var_array.jl index c210337..9e5730c 100644 --- a/test/add_change_var_array.jl +++ b/test/add_change_var_array.jl @@ -22,7 +22,7 @@ Distributed.@everywhere begin # Generate random input values and generate nodeid names input_data = Tuple(Tuple(type != String ? rand(type, array_size) : reshape( - [randstring(Int64(rand(UInt8))) + [randstring(rand(1:10)) for i in 1:prod(array_size)], array_size...) for array_size in array_sizes) for type in types) @@ -113,7 +113,7 @@ for (type_ind, type) in enumerate(types) for (array_size_ind, array_size) in enumerate(array_sizes) new_input = type != String ? rand(type, array_size) : reshape( - [randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], array_size...) + [randstring(rand(1:10)) for i in 1:prod(array_size)], array_size...) varnodeid = JUA_NodeId(1, varnode_ids[type_ind, array_size_ind]) retval = JUA_Client_writeValueAttribute(client, varnodeid, new_input) @test retval == UA_STATUSCODE_GOOD @@ -138,7 +138,7 @@ for type_ind in eachindex(types) end new_input = type != String ? rand(type, array_size) : reshape( - [randstring(Int64(rand(UInt8))) for i in 1:prod(array_size)], array_size...) + [randstring(rand(1:10)) for i in 1:prod(array_size)], array_size...) varnodeid = JUA_NodeId(1, varnode_ids[type_ind, array_size_ind]) @test_throws open62541.AttributeReadWriteError JUA_Client_writeValueAttribute( client, varnodeid, new_input) diff --git a/test/add_change_var_scalar.jl b/test/add_change_var_scalar.jl index f6e65ff..1530360 100644 --- a/test/add_change_var_scalar.jl +++ b/test/add_change_var_scalar.jl @@ -19,7 +19,7 @@ Distributed.@everywhere begin UInt64, Float32, Float64, String, ComplexF32, ComplexF64] # Generate random input values and generate nodeid names - input_data = Tuple(type != String ? rand(type) : randstring(Int64(rand(UInt8))) + input_data = Tuple(type != String ? rand(type) : randstring(rand(1:10)) for type in types) varnode_ids = ["$(Symbol(type)) scalar variable" for type in types] end @@ -99,7 +99,7 @@ end # Write new data for (type_ind, type) in enumerate(types) - new_input = type != String ? rand(type) : randstring(Int64(rand(UInt8))) + new_input = type != String ? rand(type) : randstring(rand(1:10)) varnodeid = JUA_NodeId(1, varnode_ids[type_ind]) retval = JUA_Client_writeValueAttribute(client, varnodeid, new_input) @test retval == UA_STATUSCODE_GOOD @@ -114,7 +114,7 @@ end # Test wrong data type write errors for type_ind in eachindex(types) type = types[mod(type_ind, length(types)) + 1] # Select wrong data type - new_input = type != String ? rand(type) : randstring(Int64(rand(UInt8))) + new_input = type != String ? rand(type) : randstring(rand(1:10)) varnodeid = JUA_NodeId(1, varnode_ids[type_ind]) @test_throws open62541.AttributeReadWriteError JUA_Client_writeValueAttribute(client, varnodeid, new_input) From 9f235c1c48a8f7cca9cbb0f0071e1e6fa31234b4 Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Mon, 13 May 2024 23:30:02 +0200 Subject: [PATCH 15/26] wait longer --- test/add_change_var_array.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/add_change_var_array.jl b/test/add_change_var_array.jl index 9e5730c..d26220e 100644 --- a/test/add_change_var_array.jl +++ b/test/add_change_var_array.jl @@ -78,8 +78,8 @@ end # Specify client and connect to server after server startup client = JUA_Client() JUA_ClientConfig_setDefault(JUA_ClientConfig(client)) -max_duration = 40.0 # Maximum waiting time for server startup -sleep_time = 2.0 # Sleep time in seconds between each connection trial +max_duration = 90.0 # Maximum waiting time for server startup +sleep_time = 3.0 # Sleep time in seconds between each connection trial let trial trial = 0 while trial < max_duration / sleep_time From f167c82246624cbf6650b406eab85d1a506fbded Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Tue, 14 May 2024 11:38:25 +0200 Subject: [PATCH 16/26] restructure files; interface improvements --- src/client.jl | 6 +- src/helper_functions.jl | 35 ++++ src/highlevel_client.jl | 52 ++++++ src/highlevel_server.jl | 145 +++++++++++++++++ src/{wrappers.jl => highlevel_types.jl} | 204 +++++++++--------------- src/open62541.jl | 4 +- src/server.jl | 56 ++----- src/wrappertest.jl | 102 ------------ test/basic_types_and_functions.jl | 8 + test/exceptions.jl | 10 +- test/server_add_nodes.jl | 37 ++++- test/server_read.jl | 16 +- test/server_write.jl | 19 ++- 13 files changed, 396 insertions(+), 298 deletions(-) create mode 100644 src/highlevel_client.jl create mode 100644 src/highlevel_server.jl rename src/{wrappers.jl => highlevel_types.jl} (54%) delete mode 100644 src/wrappertest.jl diff --git a/src/client.jl b/src/client.jl index ac65a4d..783ada6 100644 --- a/src/client.jl +++ b/src/client.jl @@ -236,13 +236,13 @@ for att in attributes_UA_Client_read @eval begin """ ``` - $($(fun_name))(client::Ptr{UA_Client}, nodeId::Ptr{UA_NodeId}, out::Ptr{$($(att[3]))} = $($(String(returnobject)))()) + $($(fun_name))(client::Ptr{UA_Client}, nodeId::Ptr{UA_NodeId}, out::Ptr{$($(att[3]))}) ``` Uses the UA Client API to read the value of attribute $($(String(attr_name))) from the NodeId `nodeId` accessed through the client `client`. """ - function $(fun_name)(client, nodeId, out = $returnobject()) + function $(fun_name)(client, nodeId, out) data_type_ptr = UA_TYPES_PTRS[$(ret_type_ptr)] statuscode = __UA_Client_readAttribute(client, nodeId, @@ -250,7 +250,7 @@ for att in attributes_UA_Client_read out, data_type_ptr) if statuscode == UA_STATUSCODE_GOOD - return out + return statuscode else action = "Reading" side = "Client" diff --git a/src/helper_functions.jl b/src/helper_functions.jl index 4280369..57ae4ad 100644 --- a/src/helper_functions.jl +++ b/src/helper_functions.jl @@ -1,3 +1,38 @@ #function that wraps a non-ref/non-ptr argument into a ref of appropriate type. wrap_ref(x::Union{Ref, Ptr}) = x #no-op fall back wrap_ref(x) = Ref(x) + +function __extract_ExtensionObject(eo::UA_ExtensionObject) + if eo.encoding != UA_EXTENSIONOBJECT_DECODED + error("can't make sense of this extension object yet.") + else + v = eo.content.decoded + type = juliadatatype(v.type) + data = reinterpret(Ptr{type}, v.data) + return GC.@preserve v type data unsafe_load(data) + end +end + +function __get_juliavalues_from_variant(v) + wrapped = unsafe_wrap(v) + if typeof(wrapped) == UA_ExtensionObject + wrapped = __extract_ExtensionObject.(wrapped) + elseif typeof(wrapped) <: Array && eltype(wrapped) == UA_ExtensionObject + wrapped = __extract_ExtensionObject.(wrapped) + end + + #now deal with special types + if typeof(wrapped) <: Union{UA_ComplexNumberType, UA_DoubleComplexNumberType} + r = complex(wrapped) + elseif typeof(wrapped) <: Array && + eltype(wrapped) <: Union{UA_ComplexNumberType, UA_DoubleComplexNumberType} + r = complex.(wrapped) + elseif typeof(wrapped) == UA_String + r = unsafe_string(wrapped) + elseif typeof(wrapped) <: Array && eltype(wrapped) == UA_String + r = unsafe_string.(wrapped) + else + r = deepcopy(wrapped) #TODO: do I need to copy here? test for memory safety! + end + return r +end \ No newline at end of file diff --git a/src/highlevel_client.jl b/src/highlevel_client.jl new file mode 100644 index 0000000..4266385 --- /dev/null +++ b/src/highlevel_client.jl @@ -0,0 +1,52 @@ +#Contains high level interface related to client + +## Client and client config +mutable struct JUA_Client <: AbstractOpen62541Wrapper + ptr::Ptr{UA_Client} + function JUA_Client() + obj = new(UA_Client_new()) + finalizer(release_handle, obj) + return obj + end +end + +function release_handle(obj::JUA_Client) + UA_Client_delete(Jpointer(obj)) +end + +mutable struct JUA_ClientConfig <: AbstractOpen62541Wrapper + ptr::Ptr{UA_ClientConfig} + function JUA_ClientConfig(client::JUA_Client) + #no finalizer, because the config object lives and dies with the client itself + obj = new(UA_Client_getConfig(client)) + return obj + end +end + +const JUA_ClientConfig_setDefault = UA_ClientConfig_setDefault +const JUA_Client_connect = UA_Client_connect +const JUA_Client_disconnect = UA_Client_disconnect + +#Add node functions + + +#Client read and write functions +#TODO: add docstring +function JUA_Client_readValueAttribute(client, nodeId) + #TODO: Is there a way of making this typestable? + #(it's not really known what kind of data is stored inside a nodeid unless + #one checks the datatype beforehand) + v = UA_Variant_new() + UA_Client_readValueAttribute(client, nodeId, v) + r = __get_juliavalues_from_variant(v) + UA_Variant_delete(v) + return r +end + +#TODO: add docstring +function JUA_Client_writeValueAttribute(client, nodeId, newvalue) + newvariant = UA_Variant_new(newvalue) + statuscode = UA_Client_writeValueAttribute(client, nodeId, newvariant) + UA_Variant_delete(newvariant) + return statuscode +end \ No newline at end of file diff --git a/src/highlevel_server.jl b/src/highlevel_server.jl new file mode 100644 index 0000000..751b080 --- /dev/null +++ b/src/highlevel_server.jl @@ -0,0 +1,145 @@ +#Contains high level interface related to server + +#Server and ServerConfig +## Server and server config +mutable struct JUA_Server <: AbstractOpen62541Wrapper + ptr::Ptr{UA_Server} + function JUA_Server() + obj = new(UA_Server_new()) + finalizer(release_handle, obj) + return obj + end +end + +function release_handle(obj::JUA_Server) + UA_Server_delete(Jpointer(obj)) +end + +mutable struct JUA_ServerConfig <: AbstractOpen62541Wrapper + ptr::Ptr{UA_ServerConfig} + function JUA_ServerConfig(server::JUA_Server) + #no finalizer, because the config object lives and dies with the server itself + obj = new(UA_Server_getConfig(server)) + return obj + end +end + +#aliasing functions that interact with server and serverconfig +const JUA_ServerConfig_setMinimalCustomBuffer = UA_ServerConfig_setMinimalCustomBuffer +const JUA_ServerConfig_setMinimal = UA_ServerConfig_setMinimal +const JUA_ServerConfig_setDefault = UA_ServerConfig_setDefault +const JUA_ServerConfig_setBasics = UA_ServerConfig_setBasics +const JUA_ServerConfig_addNetworkLayerTCP = UA_ServerConfig_addNetworkLayerTCP +const JUA_ServerConfig_addSecurityPolicyNone = UA_ServerConfig_addSecurityPolicyNone +const JUA_ServerConfig_addEndpoint = UA_ServerConfig_addEndpoint +const JUA_ServerConfig_addAllEndpoints = UA_ServerConfig_addAllEndpoints +const JUA_ServerConfig_clean = UA_ServerConfig_clean +const JUA_AccessControl_default = UA_AccessControl_default +const JUA_AccessControl_defaultWithLoginCallback = UA_AccessControl_defaultWithLoginCallback + +function JUA_Server_runUntilInterrupt(server::JUA_Server) + running = Ref(true) + try + Base.exit_on_sigint(false) + UA_Server_run(server, running) + catch err + UA_Server_run_shutdown(server) + println("Shutting down server.") + Base.exit_on_sigint(true) + end + return nothing +end + +#Add node functions +""" +``` +JUA_Server_addNode(server::JUA_Server, requestedNewNodeId::JUA_NodeId, + parentNodeId::JUA_NodeId, referenceTypeId::JUA_NodeId, browseName::JUA_QualifiedName, + attributes::Union{JUA_VariableAttributes, JUA_VariableTypeAttributes, JUA_ObjectAttributes}, + outNewNodeId::JUA_NodeId, nodeContext::JUA_NodeId, typeDefinition::JUA_NodeId)::UA_StatusCode +``` + +uses the server API to add a Variable, VariableType, or Object node to the `server`. + +See [`JUA_VariableAttributes`](@ref), [`JUA_VariableTypeAttributes`](@ref), and [`JUA_ObjectAttributes`](@ref) +on how to define valid attributes. + +``` +JUA_Server_addNode(server::JUA_Server, requestedNewNodeId::JUA_NodeId, + parentNodeId, referenceTypeId::JUA_NodeId, browseName::JUA_QualifiedName, + attributes::Union{JUA_ObjectTypeAttributes, JUA_ReferenceTypeAttributes, JUA_DataTypeAttributes, JUA_ViewAttributes}, + outNewNodeId::JUA_NodeId, nodeContext::JUA_NodeId)::UA_StatusCode +``` + +uses the server API to add a ObjectType, ReferenceType, DataType, or View node to the `server`. + +See [`JUA_ObjectTypeAttributes`](@ref), See [`JUA_ReferenceTypeAttributes`](@ref), [`JUA_DataTypeAttributes`](@ref), and [`JUA_ViewAttributes`](@ref) on how to define valid attributes. +""" +function JUA_Server_addNode(server, requestedNewNodeId, + parentNodeId, referenceTypeId, browseName, + attributes::Ptr{UA_MethodAttributes}, outNewNodeId, nodeContext, + method::Function, inputArgumentsSize, inputArguments, outputArgumentsSize, + outputArguments) #TODO: consider whether we would like to go even higher level here (automatically generate inputArguments of the correct size etc.) + return UA_Server_addMethodNode(server, requestedNewNodeId, parentNodeId, + referenceTypeId, browseName, attributes, method, + inputArgumentsSize, inputArguments, outputArgumentsSize, + outputArguments, nodeContext, outNewNodeId) +end + +for nodeclass in instances(UA_NodeClass) + if nodeclass != __UA_NODECLASS_FORCE32BIT && nodeclass != UA_NODECLASS_UNSPECIFIED + nodeclass_sym = Symbol(nodeclass) + funname_sym = Symbol(replace( + "UA_Server_add" * + titlecase(string(nodeclass_sym)[14:end]) * + "Node", + "type" => "Type")) + attributetype_sym_J = Symbol("J"*replace( + "UA_" * + titlecase(string(nodeclass_sym)[14:end]) * + "Attributes", + "type" => "Type")) + if funname_sym == :UA_Server_addVariableNode || + funname_sym == :UA_Server_addVariableTypeNode || + funname_sym == :UA_Server_addObjectNode + @eval begin + #high level function using multiple dispatch + #docstring is with the cumulative docstring for this function above + function JUA_Server_addNode(server, requestedNewNodeId, + parentNodeId, referenceTypeId, browseName, + attributes::$(attributetype_sym_J), + outNewNodeId, nodeContext, typeDefinition) + return $(funname_sym)(server, requestedNewNodeId, + parentNodeId, referenceTypeId, browseName, + typeDefinition, attributes, nodeContext, outNewNodeId) + end + end + elseif funname_sym != :UA_Server_addMethodNode + @eval begin + #higher level function using dispatch + #docstring is with the cumulative docstring for this function above + function JUA_Server_addNode(server, requestedNewNodeId, + parentNodeId, referenceTypeId, browseName, + attributes::$(attributetype_sym_J), + outNewNodeId, nodeContext) + return $(funname_sym)(server, requestedNewNodeId, + parentNodeId, referenceTypeId, browseName, attributes, + nodeContext, outNewNodeId) + end + end + end + end +end + +#Server read and write functions +#TODO: add docstring +function JUA_Server_readValue(client, nodeId) + #TODO: Is there a way of making this typestable? + #(it's not really known what kind of data is stored inside a nodeid unless + #one checks the datatype beforehand) + v = UA_Variant_new() + UA_Server_readValue(client, nodeId, v) + r = __get_juliavalues_from_variant(v) + UA_Variant_delete(v) + return r +end \ No newline at end of file diff --git a/src/wrappers.jl b/src/highlevel_types.jl similarity index 54% rename from src/wrappers.jl rename to src/highlevel_types.jl index 7fcaf78..2674f1e 100644 --- a/src/wrappers.jl +++ b/src/highlevel_types.jl @@ -12,6 +12,8 @@ function Base.unsafe_convert(::Type{Ptr{T}}, obj::AbstractOpen62541Wrapper) wher Base.unsafe_convert(Ptr{T}, Jpointer(obj)) end +Base.show(io::IO, ::MIME"text/plain", v::AbstractOpen62541Wrapper) = print(io, "$(typeof(v)):\n"*UA_print(Jpointer(v))) + ## Useful basic types #String mutable struct JUA_String <: AbstractOpen62541Wrapper @@ -46,82 +48,6 @@ function release_handle(obj::JUA_Guid) UA_Guid_delete(Jpointer(obj)) end -## Server and server config -mutable struct JUA_Server <: AbstractOpen62541Wrapper - ptr::Ptr{UA_Server} - function JUA_Server() - obj = new(UA_Server_new()) - finalizer(release_handle, obj) - return obj - end -end - -function release_handle(obj::JUA_Server) - UA_Server_delete(Jpointer(obj)) -end - -mutable struct JUA_ServerConfig <: AbstractOpen62541Wrapper - ptr::Ptr{UA_ServerConfig} - function JUA_ServerConfig(server::JUA_Server) - #no finalizer, because the config object lives and dies with the server itself - obj = new(UA_Server_getConfig(server)) - return obj - end -end - -#aliasing functions that interact with server and serverconfig -const JUA_ServerConfig_setMinimalCustomBuffer = UA_ServerConfig_setMinimalCustomBuffer -const JUA_ServerConfig_setMinimal = UA_ServerConfig_setMinimal -const JUA_ServerConfig_setDefault = UA_ServerConfig_setDefault -const JUA_ServerConfig_setBasics = UA_ServerConfig_setBasics -const JUA_ServerConfig_addNetworkLayerTCP = UA_ServerConfig_addNetworkLayerTCP -const JUA_ServerConfig_addSecurityPolicyNone = UA_ServerConfig_addSecurityPolicyNone -const JUA_ServerConfig_addEndpoint = UA_ServerConfig_addEndpoint -const JUA_ServerConfig_addAllEndpoints = UA_ServerConfig_addAllEndpoints -const JUA_ServerConfig_clean = UA_ServerConfig_clean -const JUA_AccessControl_default = UA_AccessControl_default -const JUA_AccessControl_defaultWithLoginCallback = UA_AccessControl_defaultWithLoginCallback - -function JUA_Server_runUntilInterrupt(server::JUA_Server) - running = Ref(true) - try - Base.exit_on_sigint(false) - UA_Server_run(server, running) - catch err - UA_Server_run_shutdown(server) - println("Shutting down server.") - Base.exit_on_sigint(true) - end - return nothing -end - -## Client and client config -mutable struct JUA_Client <: AbstractOpen62541Wrapper - ptr::Ptr{UA_Client} - function JUA_Client() - obj = new(UA_Client_new()) - finalizer(release_handle, obj) - return obj - end -end - -function release_handle(obj::JUA_Client) - UA_Client_delete(Jpointer(obj)) -end - -mutable struct JUA_ClientConfig <: AbstractOpen62541Wrapper - ptr::Ptr{UA_ClientConfig} - function JUA_ClientConfig(client::JUA_Client) - #no finalizer, because the config object lives and dies with the client itself - obj = new(UA_Client_getConfig(client)) - return obj - end -end - -const JUA_ClientConfig_setDefault = UA_ClientConfig_setDefault -const JUA_Client_connect = UA_Client_connect -const JUA_Client_disconnect = UA_Client_disconnect - ## NodeIds """ ``` @@ -238,6 +164,25 @@ end Base.convert(::Type{UA_QualifiedName}, x::JUA_QualifiedName) = unsafe_load(Jpointer(x)) +#Variant +mutable struct JUA_Variant <: AbstractOpen62541Wrapper + ptr::Ptr{UA_Variant} + function JUA_Variant() + obj = new(UA_Variant_new()) + finalizer(release_handle, obj) + return obj + end + function JUA_Variant(x) + obj = new(UA_Variant_new(x)) + finalizer(release_handle, obj) + return obj + end +end + +function release_handle(obj::JUA_Variant) + UA_Variant_delete(Jpointer(obj)) +end + #VariableAttributes mutable struct JUA_VariableAttributes <: AbstractOpen62541Wrapper ptr::Ptr{UA_VariableAttributes} @@ -266,79 +211,72 @@ function release_handle(obj::JUA_VariableTypeAttributes) UA_VariableTypeAttributes_delete(Jpointer(obj)) end -mutable struct JUA_Variant <: AbstractOpen62541Wrapper - ptr::Ptr{UA_Variant} - function JUA_Variant() - obj = new(UA_Variant_new()) +#ObjectAttributes +mutable struct JUA_ObjectAttributes <: AbstractOpen62541Wrapper + ptr::Ptr{UA_ObjectAttributes} + function JUA_ObjectAttributes(; kwargs...) + obj = new(UA_ObjectAttributes_generate(; kwargs...)) finalizer(release_handle, obj) return obj end end -function release_handle(obj::JUA_Variant) - UA_Variant_delete(Jpointer(obj)) +function release_handle(obj::JUA_ObjectAttributes) + UA_ObjectAttributes_delete(Jpointer(obj)) end -function JUA_Client_readValueAttribute(client, nodeId) - #TODO: Is there a way of making this typestable? - #(it's not really known what kind of data is stored inside a nodeid unless - #one checks the datatype beforehand) - v = UA_Variant_new() - UA_Client_readValueAttribute(client, nodeId, v) - r = __get_juliavalues_from_variant(v) - UA_Variant_delete(v) - return r +#ObjectTypeAttributes +mutable struct JUA_ObjectTypeAttributes <: AbstractOpen62541Wrapper + ptr::Ptr{UA_ObjectTypeAttributes} + function JUA_ObjectTypeAttributes(; kwargs...) + obj = new(UA_ObjectTypeAttributes_generate(; kwargs...)) + finalizer(release_handle, obj) + return obj + end end -function JUA_Client_writeValueAttribute(client, nodeId, newvalue) - newvariant = UA_Variant_new(newvalue) - statuscode = UA_Client_writeValueAttribute(client, nodeId, newvariant) - UA_Variant_delete(newvariant) - return statuscode +function release_handle(obj::JUA_ObjectTypeAttributes) + UA_ObjectTypeAttributes_delete(Jpointer(obj)) end -function JUA_Server_readValue(client, nodeId) - #TODO: Is there a way of making this typestable? - #(it's not really known what kind of data is stored inside a nodeid unless - #one checks the datatype beforehand) - v = UA_Variant_new() - UA_Server_readValue(client, nodeId, v) - r = __get_juliavalues_from_variant(v) - UA_Variant_delete(v) - return r +#ReferenceTypeAttributes +mutable struct JUA_ReferenceTypeAttributes <: AbstractOpen62541Wrapper + ptr::Ptr{UA_ReferenceTypeAttributes} + function JUA_ReferenceTypeAttributes(; kwargs...) + obj = new(UA_ReferenceTypeAttributes_generate(; kwargs...)) + finalizer(release_handle, obj) + return obj + end end -function __extract_ExtensionObject(eo::UA_ExtensionObject) - if eo.encoding != UA_EXTENSIONOBJECT_DECODED - error("can't make sense of this extension object yet.") - else - v = eo.content.decoded - type = juliadatatype(v.type) - data = reinterpret(Ptr{type}, v.data) - return GC.@preserve v type data unsafe_load(data) - end +function release_handle(obj::JUA_ReferenceTypeAttributes) + UA_ReferenceTypeAttributes_delete(Jpointer(obj)) end -function __get_juliavalues_from_variant(v) - wrapped = unsafe_wrap(v) - if typeof(wrapped) == UA_ExtensionObject - wrapped = __extract_ExtensionObject.(wrapped) - elseif typeof(wrapped) <: Array && eltype(wrapped) == UA_ExtensionObject - wrapped = __extract_ExtensionObject.(wrapped) +#DataTypeAttributes +mutable struct JUA_DataTypeAttributes <: AbstractOpen62541Wrapper + ptr::Ptr{UA_DataTypeAttributes} + function JUA_DataTypeAttributes(; kwargs...) + obj = new(UA_DataTypeAttributes_generate(; kwargs...)) + finalizer(release_handle, obj) + return obj end +end + +function release_handle(obj::JUA_DataTypeAttributes) + UA_DataTypeAttributes_delete(Jpointer(obj)) +end - #now deal with special types - if typeof(wrapped) <: Union{UA_ComplexNumberType, UA_DoubleComplexNumberType} - r = complex(wrapped) - elseif typeof(wrapped) <: Array && - eltype(wrapped) <: Union{UA_ComplexNumberType, UA_DoubleComplexNumberType} - r = complex.(wrapped) - elseif typeof(wrapped) == UA_String - r = unsafe_string(wrapped) - elseif typeof(wrapped) <: Array && eltype(wrapped) == UA_String - r = unsafe_string.(wrapped) - else - r = deepcopy(wrapped) #TODO: do I need to copy here? test for memory safety! +#ViewAttributes +mutable struct JUA_ViewAttributes <: AbstractOpen62541Wrapper + ptr::Ptr{UA_ViewAttributes} + function JUA_ViewAttributes(; kwargs...) + obj = new(UA_ViewAttributes_generate(; kwargs...)) + finalizer(release_handle, obj) + return obj end - return r +end + +function release_handle(obj::JUA_ViewAttributes) + UA_ViewAttributes_delete(Jpointer(obj)) end diff --git a/src/open62541.jl b/src/open62541.jl index be9fdfb..6af4e96 100644 --- a/src/open62541.jl +++ b/src/open62541.jl @@ -20122,7 +20122,9 @@ include("types.jl") include("callbacks.jl") include("server.jl") include("client.jl") -include("wrappers.jl") +include("highlevel_types.jl") +include("highlevel_server.jl") +include("highlevel_client.jl") include("attribute_generation.jl") include("exceptions.jl") include("init.jl") diff --git a/src/server.jl b/src/server.jl index 3fcc78a..0633bfb 100644 --- a/src/server.jl +++ b/src/server.jl @@ -56,18 +56,6 @@ function UA_Server_addMethodNode(server, requestedNewNodeId, parentNodeId, UA_NODEID_NULL, C_NULL, nodeContext, outNewNodeId) end -#TODO: Add docstring -function JUA_Server_addNode(server, requestedNewNodeId, - parentNodeId, referenceTypeId, browseName, - attributes::Ptr{UA_MethodAttributes}, outNewNodeId, nodeContext, - method::Function, inputArgumentsSize, inputArguments, outputArgumentsSize, - outputArguments) #TODO: consider whether we would like to go even higher level here (automatically generate inputArguments of the correct size etc.) - return UA_Server_addMethodNode(server, requestedNewNodeId, parentNodeId, - referenceTypeId, browseName, attributes, method, - inputArgumentsSize, inputArguments, outputArgumentsSize, - outputArguments, nodeContext, outNewNodeId) -end - for nodeclass in instances(UA_NodeClass) if nodeclass != __UA_NODECLASS_FORCE32BIT && nodeclass != UA_NODECLASS_UNSPECIFIED nodeclass_sym = Symbol(nodeclass) @@ -83,6 +71,7 @@ for nodeclass in instances(UA_NodeClass) titlecase(string(nodeclass_sym)[14:end]) * "Attributes", "type" => "Type")) + if funname_sym == :UA_Server_addVariableNode || funname_sym == :UA_Server_addVariableTypeNode || funname_sym == :UA_Server_addObjectNode @@ -108,18 +97,7 @@ for nodeclass in instances(UA_NodeClass) requestedNewNodeId, parentNodeId, referenceTypeId, browseName, typeDefinition, attributes, UA_TYPES_PTRS[$(attributeptr_sym)], nodeContext, outNewNodeId) - end - - #higher level function using dispatch - #TODO: add docstring - function JUA_Server_addNode(server, requestedNewNodeId, - parentNodeId, referenceTypeId, browseName, - attributes::Ptr{$(attributetype_sym)}, - outNewNodeId, nodeContext, typeDefinition) - return $(funname_sym)(server, requestedNewNodeId, - parentNodeId, referenceTypeId, browseName, - typeDefinition, attributes, nodeContext, outNewNodeId) - end + end end elseif funname_sym != :UA_Server_addMethodNode @eval begin @@ -147,17 +125,6 @@ for nodeclass in instances(UA_NodeClass) UA_TYPES_PTRS[$(attributeptr_sym)], nodeContext, outNewNodeId) end - - #higher level function using dispatch - #TODO: add docstring - function JUA_Server_addNode(server, requestedNewNodeId, - parentNodeId, referenceTypeId, browseName, - attributes::Ptr{$(attributetype_sym)}, - outNewNodeId, nodeContext) - return $(funname_sym)(server, requestedNewNodeId, - parentNodeId, referenceTypeId, browseName, attributes, - nodeContext, outNewNodeId) - end end end end @@ -174,18 +141,21 @@ for att in attributes_UA_Server_read @eval begin """ ``` - $($(fun_name))(server, nodeId, out = $($(String(ret_type)))()) + $($(fun_name))(server::Ptr{UA_Server}, nodeId::Ptr{UA_NodeId}, out::Ptr{$($(String(att[3])))}) ``` Uses the Server API to read the value of the attribute $($(String(attr_name))) from the NodeId `nodeId` located on server `server`. The result is saved - into the buffer `out`. - - M + into `out`. + + Note that memory for `out` must be allocated by C before using this function. + This can be accomplished with `out = $($(String(ret_type)))()`. The + resulting object must be cleaned up via `$($(String(att[3])))_delete(out::Ptr{$($(String(att[3])))})` + after its use. """ - function $(fun_name)(server, nodeId, out = $(ret_type)()) + function $(fun_name)(server, nodeId, out) statuscode = __UA_Server_read(server, nodeId, $(ua_attr_name), out) if statuscode == UA_STATUSCODE_GOOD - return out + return statuscode else action = "Reading" side = "Server" @@ -213,6 +183,10 @@ for att in attributes_UA_Server_write ``` Uses the Server API to write the value `new_val` to the attribute $($(String(attr_name))) of the NodeId `nodeId` located on the `server`. + + Note that memory for `new_val` is allocated by C before using this function + and needs to be cleaned up via `$($(String(att[3])))_delete(new_val::Ptr{$($(String(att[3])))})` + after its use. """ function $(fun_name)(server, nodeId, new_val) data_type_ptr = UA_TYPES_PTRS[$(attr_type_ptr)] diff --git a/src/wrappertest.jl b/src/wrappertest.jl deleted file mode 100644 index 556a072..0000000 --- a/src/wrappertest.jl +++ /dev/null @@ -1,102 +0,0 @@ - -using open62541 - -n1 = JUA_NodeId(1, 1234) -n2 = UA_NODEID_NUMERIC(1, 1234) -n3 = JUA_NodeId(1, "my_new_id") -n4 = UA_NODEID_STRING_ALLOC(1, "my_new_id") - -UA_NodeId_equal(n1, n2) -UA_NodeId_equal(n3, n4) - -n3 = 10 #this causes reference to the pointer in n3 to be lost (which is connected to allocated memory). -#since finalizer is defined in the wrapper, the memory gets automatically freed. -GC.gc() #garbage collector frees the memory (normally runs automatically) - -for i in 1:50_000_000 #"forgetting" to free memory creates memory leak - should eat up about 3GB of memory. - n5 = UA_NODEID_STRING_ALLOC(1, "my new id") - n5 = 10 -end - -GC.gc() #memory not recovered by GC. :( - -for i in 1:50_000_000 #no memory leak; usage constant, because the GC is at work. - n6 = open62541.JUA_NodeId(1, "my new id") - n6 = 10 -end - -using Distributed -Distributed.addprocs(1) # Add a single worker process to run the server - -Distributed.@everywhere begin - using open62541 - using Test -end - -# Create nodes with random default values on new server running at a worker process -Distributed.@spawnat Distributed.workers()[end] begin - server = UA_Server_new() - retval = UA_ServerConfig_setMinimalCustomBuffer(UA_Server_getConfig(server), - 4842, - C_NULL, - 0, - 0) - @test retval == UA_STATUSCODE_GOOD - - # Add variable node containing a scalar to the server - input = rand(100, 100) - accesslevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE - attr = UA_generate_variable_attributes(input, - "test", - "test", - accesslevel) - varnodeid = UA_NODEID_STRING_ALLOC(1, "test") - parentnodeid = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER) - parentreferencenodeid = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES) - typedefinition = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE) - browsename = UA_QUALIFIEDNAME_ALLOC(1, "test") - nodecontext = C_NULL - outnewnodeid = C_NULL - retval = UA_Server_addVariableNode(server, varnodeid, parentnodeid, - parentreferencenodeid, - browsename, typedefinition, attr, nodecontext, outnewnodeid) - - # Start up the server - Distributed.@spawnat Distributed.workers()[end] redirect_stderr() # Turn off all error messages - println("Starting up the server...") - UA_Server_run(server, Ref(true)) -end - -# Specify client and connect to server after server startup -client = UA_Client_new() -UA_ClientConfig_setDefault(UA_Client_getConfig(client)) -max_duration = 40.0 # Maximum waiting time for server startup -sleep_time = 2.0 # Sleep time in seconds between each connection trial -let trial - trial = 0 - while trial < max_duration / sleep_time - retval = UA_Client_connect(client, "opc.tcp://localhost:4842") - if retval == UA_STATUSCODE_GOOD - println("Connection established.") - break - end - sleep(sleep_time) - trial = trial + 1 - end - @test trial < max_duration / sleep_time # Check if maximum number of trials has been exceeded -end - -# Read with client from server -for i in 1:3000 - varnodeid = UA_NODEID_STRING_ALLOC(1, "test") - output_client = UA_Client_readValueAttribute(client, varnodeid) - output_client = 10 -end - -# Disconnect client -UA_Client_disconnect(client) -UA_Client_delete(client) - -println("Ungracefully kill server process...") -Distributed.interrupt(Distributed.workers()[end]) -Distributed.rmprocs(Distributed.workers()[end]; waitfor = 0) diff --git a/test/basic_types_and_functions.jl b/test/basic_types_and_functions.jl index 18cf762..e0fa125 100644 --- a/test/basic_types_and_functions.jl +++ b/test/basic_types_and_functions.jl @@ -119,3 +119,11 @@ client = UA_Client_new() context = UA_Client_getContext(client) @test isa(context, Ptr{Ptr{Nothing}}) UA_Client_delete(client) + +#Random wrapper tests +j = JUA_Variant() +@test j isa JUA_Variant +j2 = JUA_QualifiedName(1, "test") +ua_s = UA_STRING("test") +@test unsafe_string(j2.name) == unsafe_string(ua_s) #tests access of properties in high level struct. +UA_String_delete(ua_s) \ No newline at end of file diff --git a/test/exceptions.jl b/test/exceptions.jl index 30fcc14..90d4892 100644 --- a/test/exceptions.jl +++ b/test/exceptions.jl @@ -1,4 +1,4 @@ -#tests custom exceptions +#Tests custom exceptions using open62541 using Test @@ -36,14 +36,16 @@ UA_ClientConfig_setDefault(UA_Client_getConfig(client)) bogusid = UA_NODEID_STRING_ALLOC(1, "bogusid") #AttributeReadWriteError - UA_Server_readX function -@test_throws open62541.AttributeReadWriteError UA_Server_readValue(server, bogusid) +out1 = UA_Variant_new() +@test_throws open62541.AttributeReadWriteError UA_Server_readValue(server, bogusid, out1) #AttributeReadWriteError - UA_Server_writeX function var1 = UA_Variant_new() @test_throws open62541.AttributeReadWriteError UA_Server_writeValue(server, bogusid, var1) #AttributeReadWriteError - UA_Client_readX function -@test_throws open62541.AttributeReadWriteError UA_Client_readValueAttribute(client, bogusid) +out2 = UA_Variant_new() +@test_throws open62541.AttributeReadWriteError UA_Client_readValueAttribute(client, bogusid, out2) #AttributeReadWriteError - UA_Client_readX function var2 = UA_Variant_new() @@ -57,3 +59,5 @@ UA_Client_delete(client) UA_Variant_delete(var1) UA_Variant_delete(var2) UA_NodeId_delete(bogusid) +UA_Variant_delete(out1) +UA_Variant_delete(out2) diff --git a/test/server_add_nodes.jl b/test/server_add_nodes.jl index 341e769..4d97c69 100644 --- a/test/server_add_nodes.jl +++ b/test/server_add_nodes.jl @@ -37,9 +37,20 @@ retval1 = UA_Server_addVariableNode(server, varnodeid, parentnodeid, #test whether adding node to the server worked @test retval1 == UA_STATUSCODE_GOOD # Test whether the correct array is within the server (read from server) -output_server = unsafe_wrap(UA_Server_readValue(server, varnodeid)) +out = UA_Variant_new() +UA_Server_readValue(server, varnodeid, out) +output_server = unsafe_wrap(out) @test all(isapprox(input, output_server)) +#clean up memory for this part of the code +UA_VariableAttributes_delete(attr) +UA_NodeId_delete(varnodeid) +UA_NodeId_delete(parentnodeid) +UA_NodeId_delete(parentreferencenodeid) +UA_NodeId_delete(typedefinition) +UA_QualifiedName_delete(browsename) +UA_Variant_delete(out) + #Variable node: array input = rand(Float64, 2, 3, 4) varnodetext = "array variable" @@ -60,6 +71,20 @@ retval2 = UA_Server_addVariableNode(server, varnodeid, parentnodeid, browsename, typedefinition, attr, nodecontext, outnewnodeid) # Test whether adding node to the server worked @test retval2 == UA_STATUSCODE_GOOD +# Test whether the correct array is within the server (read from server) +out = UA_Variant_new() +UA_Server_readValue(server, varnodeid, out) +output_server = unsafe_wrap(out) +@test all(isapprox(input, output_server)) + +#clean up memory for this part of the code +UA_VariableAttributes_delete(attr) +UA_NodeId_delete(varnodeid) +UA_NodeId_delete(parentnodeid) +UA_NodeId_delete(parentreferencenodeid) +UA_NodeId_delete(typedefinition) +UA_QualifiedName_delete(browsename) +UA_Variant_delete(out) ## VariableTypeNode - array input = zeros(2) @@ -351,7 +376,7 @@ retvalj0 = JUA_ServerConfig_setMinimalCustomBuffer(JUA_ServerConfig(server2), #Variable node: scalar accesslevel = UA_ACCESSLEVEL(read = true, write = true) input = rand(Float64) -attr = UA_VariableAttributes_generate(value = input, +attr = JUA_VariableAttributes(value = input, displayname = "scalar variable", description = "this is a scalar variable", accesslevel = accesslevel) @@ -360,8 +385,8 @@ parentnodeid = JUA_NodeId(0, UA_NS0ID_OBJECTSFOLDER) parentreferencenodeid = JUA_NodeId(0, UA_NS0ID_ORGANIZES) typedefinition = JUA_NodeId(0, UA_NS0ID_BASEDATAVARIABLETYPE) browsename = JUA_QualifiedName(1, "scalar variable") -nodecontext = C_NULL -outnewnodeid = C_NULL +nodecontext = JUA_NodeId() +outnewnodeid = JUA_NodeId() retvalj1 = JUA_Server_addNode(server2, varnodeid, parentnodeid, parentreferencenodeid, browsename, attr, nodecontext, outnewnodeid, typedefinition) @@ -371,12 +396,12 @@ retvalj1 = JUA_Server_addNode(server2, varnodeid, parentnodeid, pumpTypeId = JUA_NodeId(1, 1001) #Define the object type for "Device" deviceTypeId = JUA_NodeId() -attr = UA_ObjectTypeAttributes_generate(displayname = "DeviceType", +attr = JUA_ObjectTypeAttributes(displayname = "DeviceType", description = "Object type for a device") parentnodeid = JUA_NodeId(0, UA_NS0ID_BASEOBJECTTYPE) parentreferencenodeid = JUA_NodeId(0, UA_NS0ID_HASSUBTYPE) browsename = JUA_QualifiedName(1, "DeviceType") retvalj2 = JUA_Server_addNode(server2, JUA_NodeId(), parentnodeid, - parentreferencenodeid, browsename, attr, C_NULL, + parentreferencenodeid, browsename, attr, JUA_NodeId(), outnewnodeid) @test retvalj2 == UA_STATUSCODE_GOOD diff --git a/test/server_read.jl b/test/server_read.jl index e07e00f..1e1accb 100644 --- a/test/server_read.jl +++ b/test/server_read.jl @@ -50,23 +50,31 @@ retval = UA_Server_addVariableTypeNode(server, UA_NodeId_new(), @test retval == UA_STATUSCODE_GOOD #gather the previously defined nodes +#TODO: add more node types nodes = (variablenodeid, variabletypenodeid) for node in nodes - nodeclass = unsafe_load(UA_Server_readNodeClass(server, node)) + out1 = UA_NodeClass_new() + UA_Server_readNodeClass(server, node, out1) + nodeclass = unsafe_load(out1) if nodeclass == UA_NODECLASS_VARIABLE attributeset = UA_VariableAttributes elseif nodeclass == UA_NODECLASS_VARIABLETYPE attributeset = UA_VariableTypeAttributes - end #TODO: add more node types once implemented + end for att in open62541.attributes_UA_Server_read fun_name = Symbol(att[1]) attr_type = Symbol(att[3]) + generator = Symbol(att[3]*"_new") + cleaner = Symbol(att[3]*"_delete") + out2 = eval(generator)() if in(Symbol(lowercasefirst(att[2])), fieldnames(attributeset)) || in(Symbol(lowercasefirst(att[2])), fieldnames(UA_NodeHead)) - @test isa(eval(fun_name)(server, node), Ptr{eval(attr_type)}) + @test isa(eval(fun_name)(server, node, out2), UA_StatusCode) else - @test_throws open62541.AttributeReadWriteError eval(fun_name)(server, node) + @test_throws open62541.AttributeReadWriteError eval(fun_name)(server, node, out2) end + eval(cleaner)(out2) end + UA_NodeClass_delete(out1) end diff --git a/test/server_write.jl b/test/server_write.jl index 3de8496..5b960ab 100644 --- a/test/server_write.jl +++ b/test/server_write.jl @@ -54,25 +54,34 @@ retval = UA_Server_addVariableTypeNode(server, UA_NodeId_new(), #test whether adding node to the server worked @test retval == UA_STATUSCODE_GOOD +#TODO: add more node types nodes = (varnodeid, variabletypenodeid) for node in nodes - nodeclass = unsafe_load(UA_Server_readNodeClass(server, node)) + out1 = UA_NodeClass_new() + UA_Server_readNodeClass(server, node, out1) + nodeclass = unsafe_load(out1) if nodeclass == UA_NODECLASS_VARIABLE attributeset = UA_VariableAttributes elseif nodeclass == UA_NODECLASS_VARIABLETYPE attributeset = UA_VariableTypeAttributes - end #TODO: add more node types once implemented + end for att in open62541.attributes_UA_Server_write fun_write = Symbol(att[1]) fun_read = Symbol(replace(att[1], "write" => "read")) attr_name = Symbol(att[2]) + generator = Symbol(att[3]*"_new") + cleaner = Symbol(att[3]*"_delete") + out2 = eval(generator)() if attr_name != :BrowseName #can't write browsename, see here: https://github.com/open62541/open62541/issues/3545 if in(Symbol(lowercasefirst(att[2])), fieldnames(attributeset)) || in(Symbol(lowercasefirst(att[2])), fieldnames(UA_NodeHead)) - read_value = eval(fun_read)(server, node) #read - statuscode = eval(fun_write)(server, node, read_value) #write read value back... - @test statuscode == UA_STATUSCODE_GOOD + statuscode1 = eval(fun_read)(server, node, out2) #read + statuscode2 = eval(fun_write)(server, node, out2) #write read value back... + @test statuscode1 == UA_STATUSCODE_GOOD + @test statuscode2 == UA_STATUSCODE_GOOD end end + eval(cleaner)(out2) end + UA_NodeClass_delete(out1) end From 01811f6647d20e54970da03d6a3bfb330b58cafd Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Thu, 16 May 2024 13:02:13 +0200 Subject: [PATCH 17/26] Move UA_Variant_new methods into JUA_Variant and adapt tests. --- gen/epilogue.jl | 10 ++- src/helper_functions.jl | 6 +- src/highlevel_client.jl | 25 +++++-- src/highlevel_server.jl | 44 +++++++++++-- src/highlevel_types.jl | 121 ++++++++++++++++++++++++++++++++-- src/open62541.jl | 4 ++ src/types.jl | 62 +---------------- test/add_change_var_scalar.jl | 5 +- test/attribute_generation.jl | 4 +- test/data_handling.jl | 17 +++-- 10 files changed, 209 insertions(+), 89 deletions(-) diff --git a/gen/epilogue.jl b/gen/epilogue.jl index 6598ddc..e427357 100644 --- a/gen/epilogue.jl +++ b/gen/epilogue.jl @@ -2,6 +2,10 @@ const UA_STRING_NULL = UA_String(0, C_NULL) const UA_GUID_NULL = UA_Guid(0, 0, 0, Tuple(zeros(UA_Byte, 8))) const UA_NODEID_NULL = UA_NodeId(Tuple(zeros(UA_Byte, 24))) const UA_EXPANDEDNODEID_NULL = UA_ExpandedNodeId(UA_NODEID_NULL, UA_STRING_NULL, 0) +#Julia number types that are rare built directly into open62541 +#Does NOT include ComplexF32/64 - these have to be treated differently. +const UA_NUMBER_TYPES = Union{Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, + UInt32, UInt64, Float32, Float64} include("generated_defs.jl") include("helper_functions.jl") @@ -9,11 +13,13 @@ include("types.jl") include("callbacks.jl") include("server.jl") include("client.jl") -include("wrappers.jl") +include("highlevel_types.jl") +include("highlevel_server.jl") +include("highlevel_client.jl") include("attribute_generation.jl") include("exceptions.jl") include("init.jl") - + # exports const PREFIXES = ["UA_", "JUA_"] for name in names(@__MODULE__; all = true), prefix in PREFIXES diff --git a/src/helper_functions.jl b/src/helper_functions.jl index 57ae4ad..174fa0b 100644 --- a/src/helper_functions.jl +++ b/src/helper_functions.jl @@ -13,8 +13,8 @@ function __extract_ExtensionObject(eo::UA_ExtensionObject) end end -function __get_juliavalues_from_variant(v) - wrapped = unsafe_wrap(v) +function __get_juliavalues_from_variant(v, type) + wrapped = unsafe_wrap(v)::type if typeof(wrapped) == UA_ExtensionObject wrapped = __extract_ExtensionObject.(wrapped) elseif typeof(wrapped) <: Array && eltype(wrapped) == UA_ExtensionObject @@ -35,4 +35,4 @@ function __get_juliavalues_from_variant(v) r = deepcopy(wrapped) #TODO: do I need to copy here? test for memory safety! end return r -end \ No newline at end of file +end diff --git a/src/highlevel_client.jl b/src/highlevel_client.jl index 4266385..b3dfce8 100644 --- a/src/highlevel_client.jl +++ b/src/highlevel_client.jl @@ -32,21 +32,36 @@ const JUA_Client_disconnect = UA_Client_disconnect #Client read and write functions #TODO: add docstring -function JUA_Client_readValueAttribute(client, nodeId) +function JUA_Client_readValueAttribute(client, nodeId, type::T = Any) where {T} #TODO: Is there a way of making this typestable? #(it's not really known what kind of data is stored inside a nodeid unless #one checks the datatype beforehand) v = UA_Variant_new() UA_Client_readValueAttribute(client, nodeId, v) - r = __get_juliavalues_from_variant(v) + r = __get_juliavalues_from_variant(v, type) UA_Variant_delete(v) return r end -#TODO: add docstring +""" +``` +JUA_Client_writeValueAttribute(server::JUA_Client, nodeId::JUA_NodeId, newvalue)::UA_StatusCode +``` + +uses the client API to write the value `newvalue` to `nodeId` on `server`. +`new_value` must either be a `JUA_Variant` or a Julia value/array compatible with +any of its constructors. + +See also [`JUA_Variant`](@ref) + +""" function JUA_Client_writeValueAttribute(client, nodeId, newvalue) - newvariant = UA_Variant_new(newvalue) + newvariant = JUA_Variant(newvalue) + statuscode = UA_Client_writeValueAttribute(client, nodeId, newvariant) + return statuscode +end + +function JUA_Client_writeValueAttribute(client, nodeId, newvalue::JUA_Variant) statuscode = UA_Client_writeValueAttribute(client, nodeId, newvariant) - UA_Variant_delete(newvariant) return statuscode end \ No newline at end of file diff --git a/src/highlevel_server.jl b/src/highlevel_server.jl index 751b080..77e8a33 100644 --- a/src/highlevel_server.jl +++ b/src/highlevel_server.jl @@ -15,6 +15,8 @@ function release_handle(obj::JUA_Server) UA_Server_delete(Jpointer(obj)) end +Base.show(io::IO, ::MIME"text/plain", v::JUA_Server) = print(io, "$(typeof(v))\n") + mutable struct JUA_ServerConfig <: AbstractOpen62541Wrapper ptr::Ptr{UA_ServerConfig} function JUA_ServerConfig(server::JUA_Server) @@ -132,14 +134,42 @@ for nodeclass in instances(UA_NodeClass) end #Server read and write functions -#TODO: add docstring -function JUA_Server_readValue(client, nodeId) - #TODO: Is there a way of making this typestable? - #(it's not really known what kind of data is stored inside a nodeid unless - #one checks the datatype beforehand) +""" +``` +value = JUA_Server_readValue(server::JUA_Server, nodeId::JUA_NodeId, type = Any) +``` + +uses the server API to read the value of `nodeId` from `server`. Output is +automatically converted to a Julia type (such as Float64, String, Vector{String}, +etc.) if possible. Otherwise, open62541 composite types are returned. + +Note: Since it is unknown what type of value is stored within `nodeId` before reading +it, this function is inherently type unstable. + +Type stability is improved if the optional argument `type` is provided, for example, +if you know that you have stored a Matrix{Float64} in `nodeId`. If the wrong type +is specified, the function will throw a TypeError. + +""" +function JUA_Server_readValue(server, nodeId, type = Any) v = UA_Variant_new() - UA_Server_readValue(client, nodeId, v) - r = __get_juliavalues_from_variant(v) + UA_Server_readValue(server, nodeId, v) + r = __get_juliavalues_from_variant(v, type) UA_Variant_delete(v) return r +end + +""" +``` +JUA_Server_writeValue(server::JUA_Server, nodeId::JUA_NodeId, newvalue)::UA_StatusCode +``` + +uses the server API to write the value `newvalue` to `nodeId` on `server`. + +""" +function JUA_Server_writeValue(client, nodeId, newvalue) + newvariant = UA_Variant_new(newvalue) + statuscode = UA_Server_writeValue(client, nodeId, newvariant) + UA_Variant_delete(newvariant) + return statuscode end \ No newline at end of file diff --git a/src/highlevel_types.jl b/src/highlevel_types.jl index 2674f1e..4d30a21 100644 --- a/src/highlevel_types.jl +++ b/src/highlevel_types.jl @@ -29,6 +29,9 @@ function release_handle(obj::JUA_String) UA_String_delete(Jpointer(obj)) end +ua_data_type_ptr_default(::Type{JUA_String}) = ua_data_type_ptr_default(UA_String) +Base.convert(::Type{UA_String}, x::JUA_String) = unsafe_load(Jpointer(x)) + #Guid mutable struct JUA_Guid <: AbstractOpen62541Wrapper ptr::Ptr{UA_Guid} @@ -67,7 +70,7 @@ creates a `JUA_NodeId` with namespaceIndex = 0, numeric identifierType and identifier = 0 ``` -JUA_NodeId(s::Union{AbstractString, JUA_String}) +JUA_NodeId(s::AbstractString) ``` creates a `JUA_NodeId` based on String `s` that is parsed into the relevant @@ -81,7 +84,7 @@ creates a `JUA_NodeId` with namespace index `nsIndex` and numerical identifier `identifier`. ``` -JUA_NodeId(nsIndex::Integer, identifier::Union{AbstractString, JUA_String}) +JUA_NodeId(nsIndex::Integer, identifier::AbstractString) ``` creates a `JUA_NodeId` with namespace index `nsIndex` and string identifier @@ -165,18 +168,128 @@ end Base.convert(::Type{UA_QualifiedName}, x::JUA_QualifiedName) = unsafe_load(Jpointer(x)) #Variant +""" +``` +JUA_Variant +``` + +creates a `JUA_Variant` object - the equivalent of a `UA_Variant`, but with memory +managed by Julia rather than C (exceptions below). + +The following methods are defined: + +``` +JUA_Variant() +``` + +creates an empty `JUA_Variant`, equivalent to calling `UA_Variant_new()`, but +with memory managed by Julia. + +``` +JUA_Variant(value::Union{T, AbstractArray{T}}) where T <: Union{UA_NUMBER_TYPES, AbstractString, ComplexF32, ComplexF64}) +``` + +creates a `JUA_Variant` containing the based on String `s` that is parsed into the relevant +properties. + +``` +JUA_NodeId(nsIndex::Integer, identifier::Integer) +``` + +creates a `JUA_NodeId` with namespace index `nsIndex` and numerical identifier +`identifier`. + +``` +JUA_NodeId(nsIndex::Integer, identifier::Union{AbstractString, JUA_String}) +``` + +creates a `JUA_NodeId` with namespace index `nsIndex` and string identifier +`identifier`. + +``` +JUA_NodeId(nsIndex::Integer, identifier::JUA_Guid) +``` + +creates a `JUA_NodeId` with namespace index `nsIndex` and global unique id identifier +`identifier`. + +Examples: + +``` +j = JUA_Variant() +j = JUA_Variant("I am a string value") +j = JUA_Variant(["String1", "String2"]) +j = JUA_Variant(rand(Float32, 2, 3, 4)) +j = JUA_Variant(rand(Int32, 2, 2)) +j = JUA_Variant(rand(ComplexF64, 8)) +``` +""" mutable struct JUA_Variant <: AbstractOpen62541Wrapper ptr::Ptr{UA_Variant} + function JUA_Variant() obj = new(UA_Variant_new()) finalizer(release_handle, obj) return obj end - function JUA_Variant(x) - obj = new(UA_Variant_new(x)) + + function JUA_Variant(value::AbstractArray{T, N}, + type_ptr::Ptr{UA_DataType} = ua_data_type_ptr_default(T)) where { + T <: Union{UA_NUMBER_TYPES, UA_String, UA_ComplexNumberType, UA_DoubleComplexNumberType}, N} + var = UA_Variant_new() + var.type = type_ptr + var.storageType = UA_VARIANT_DATA + var.arrayLength = length(value) + ua_arr = UA_Array_new(vec(permutedims(value, reverse(1:N))), type_ptr) # Allocate new UA_Array from value with C style indexing + UA_Variant_setArray(var, ua_arr, length(value), type_ptr) + var.arrayDimensionsSize = length(size(value)) + var.arrayDimensions = UA_UInt32_Array_new(reverse(size(value))) + obj = new(var) finalizer(release_handle, obj) return obj end + + function JUA_Variant(value::T, + type_ptr::Ptr{UA_DataType} = ua_data_type_ptr_default(T)) where {T <: Union{UA_NUMBER_TYPES, Ptr{UA_String}, UA_ComplexNumberType, UA_DoubleComplexNumberType}} + var = UA_Variant_new() + var.type = type_ptr + var.storageType = UA_VARIANT_DATA + UA_Variant_setScalarCopy(var, wrap_ref(value), type_ptr) + obj = new(var) + finalizer(release_handle, obj) + return obj + end + + function JUA_Variant(value::AbstractString) + ua_s = UA_STRING(value) + obj = JUA_Variant(ua_s) + UA_String_delete(ua_s) + return obj + end + + function JUA_Variant(value::Complex{T}) where {T <: Union{Float32, Float64}} + f = T == Float32 ? UA_ComplexNumberType : UA_DoubleComplexNumberType + ua_c = f(reim(value)...) + return JUA_Variant(ua_c) + end + + function JUA_Variant(value::AbstractArray{<:AbstractString}) + a = similar(value, UA_String) + for i in eachindex(a) + a[i] = UA_String_fromChars(value[i]) + end + return JUA_Variant(a) + end + + function JUA_Variant(value::AbstractArray{<:Complex{T}}) where {T <: + Union{Float32, Float64}} + f = T == Float32 ? UA_ComplexNumberType : UA_DoubleComplexNumberType + a = similar(value, f) + for i in eachindex(a) + a[i] = f(reim(value[i])...) + end + return JUA_Variant(a) + end end function release_handle(obj::JUA_Variant) diff --git a/src/open62541.jl b/src/open62541.jl index 6af4e96..64653d9 100644 --- a/src/open62541.jl +++ b/src/open62541.jl @@ -20115,6 +20115,10 @@ const UA_STRING_NULL = UA_String(0, C_NULL) const UA_GUID_NULL = UA_Guid(0, 0, 0, Tuple(zeros(UA_Byte, 8))) const UA_NODEID_NULL = UA_NodeId(Tuple(zeros(UA_Byte, 24))) const UA_EXPANDEDNODEID_NULL = UA_ExpandedNodeId(UA_NODEID_NULL, UA_STRING_NULL, 0) +#Julia number types that are rare built directly into open62541 +#Does NOT include ComplexF32/64 - these have to be treated differently. +const UA_NUMBER_TYPES = Union{Bool, Int8, Int16, Int32, Int64, UInt8, UInt16, + UInt32, UInt64, Float32, Float64} include("generated_defs.jl") include("helper_functions.jl") diff --git a/src/types.jl b/src/types.jl index 4adcbd5..e782003 100644 --- a/src/types.jl +++ b/src/types.jl @@ -107,9 +107,9 @@ for (i, type_name) in enumerate(type_names) # Datatype specific constructors, destructors, initalizers, as well as clear and copy functions """ ``` - $($(type_name))_new"()::Ptr{$($(type_name))} + $($(type_name))_new()::Ptr{$($(type_name))} ``` - creates and initializes a `$($(type_name))` object whose memory is allocated by C. After use, it needs to be + creates and initializes ("zeros") a `$($(type_name))` object whose memory is allocated by C. After use, it needs to be cleaned up with `$($(type_name))_delete(x::Ptr{$($(type_name))})` """ function $(Symbol(type_name, "_new"))() @@ -791,64 +791,6 @@ unsafe_size(p::Ref{UA_Variant}) = unsafe_size(unsafe_load(p)) Base.length(v::UA_Variant) = Int(v.arrayLength) Base.length(p::Ref{UA_Variant}) = length(unsafe_load(p)) -function UA_Variant_new(value::AbstractArray{T, N}, - type_ptr::Ptr{UA_DataType} = ua_data_type_ptr_default(T)) where { - T <: Union{AbstractFloat, UA_String, Integer, - UA_ComplexNumberType, UA_DoubleComplexNumberType}, - N} - var = UA_Variant_new() - var.type = type_ptr - var.storageType = UA_VARIANT_DATA - var.arrayLength = length(value) - ua_arr = UA_Array_new(vec(permutedims(value, reverse(1:N))), type_ptr) # Allocate new UA_Array from value with C style indexing - UA_Variant_setArray(var, ua_arr, length(value), type_ptr) - var.arrayDimensionsSize = length(size(value)) - var.arrayDimensions = UA_UInt32_Array_new(reverse(size(value))) - return var -end - -function UA_Variant_new(value::T, - type_ptr::Ptr{UA_DataType} = ua_data_type_ptr_default(T)) where {T <: Union{ - AbstractFloat, Integer, Ptr{UA_String}, UA_ComplexNumberType, UA_DoubleComplexNumberType}} - var = UA_Variant_new() - var.type = type_ptr - var.storageType = UA_VARIANT_DATA - UA_Variant_setScalarCopy(var, wrap_ref(value), type_ptr) - return var -end - -function UA_Variant_new(value::AbstractString) - ua_s = UA_STRING(value) - v = UA_Variant_new(ua_s) - UA_String_delete(ua_s) - return v -end - -function UA_Variant_new(value::Complex{T}) where {T <: Union{Float32, Float64}} - f = T == Float32 ? UA_ComplexNumberType : UA_DoubleComplexNumberType - ua_c = f(reim(value)...) - v = UA_Variant_new(ua_c) - return v -end - -function UA_Variant_new(value::AbstractArray{<:AbstractString}) - a = similar(value, UA_String) - for i in eachindex(a) - a[i] = UA_String_fromChars(value[i]) - end - return UA_Variant_new(a) -end - -function UA_Variant_new(value::AbstractArray{<:Complex{T}}) where {T <: - Union{Float32, Float64}} - f = T == Float32 ? UA_ComplexNumberType : UA_DoubleComplexNumberType - a = similar(value, f) - for i in eachindex(a) - a[i] = f(reim(value[i])...) - end - return UA_Variant_new(a) -end - function Base.unsafe_wrap(v::UA_Variant) type = juliadatatype(v.type) data = reinterpret(Ptr{type}, v.data) diff --git a/test/add_change_var_scalar.jl b/test/add_change_var_scalar.jl index 1530360..44abdbc 100644 --- a/test/add_change_var_scalar.jl +++ b/test/add_change_var_scalar.jl @@ -69,8 +69,8 @@ end # Specify client and connect to server after server startup client = JUA_Client() JUA_ClientConfig_setDefault(JUA_ClientConfig(client)) -max_duration = 40.0 # Maximum waiting time for server startup -sleep_time = 2.0 # Sleep time in seconds between each connection trial +max_duration = 90.0 # Maximum waiting time for server startup +sleep_time = 3.0 # Sleep time in seconds between each connection trial let trial trial = 0 while trial < max_duration / sleep_time @@ -90,6 +90,7 @@ for (type_ind, type) in enumerate(types) input = input_data[type_ind] varnodeid = JUA_NodeId(1, varnode_ids[type_ind]) output_client = JUA_Client_readValueAttribute(client, varnodeid) + @show output_client, input if type <: AbstractFloat @test all(isapprox.(input, output_client)) else diff --git a/test/attribute_generation.jl b/test/attribute_generation.jl index 7090869..2e8cb69 100644 --- a/test/attribute_generation.jl +++ b/test/attribute_generation.jl @@ -126,7 +126,7 @@ for i in eachindex(array_sizes) @test unsafe_load(attr.valueRank) == valueranks[i] @test unsafe_load(attr.historizing) == historizing #TODO: add test that checks dataType being correctly set. - out = open62541.__get_juliavalues_from_variant(attr.value) + out = open62541.__get_juliavalues_from_variant(attr.value, Any) if types[j] <: Union{AbstractFloat, Complex} @test all(out .≈ v) else @@ -179,7 +179,7 @@ for i in eachindex(array_sizes) @test unsafe_load(attr.userWriteMask) == userwritemask @test unsafe_load(attr.valueRank) == valueranks[i] @test unsafe_load(attr.isAbstract) == isabstract - out = open62541.__get_juliavalues_from_variant(attr.value) + out = open62541.__get_juliavalues_from_variant(attr.value, Any) if types[j] <: Union{AbstractFloat, Complex} @test all(out .≈ v) else diff --git a/test/data_handling.jl b/test/data_handling.jl index 2d2ec32..82c68f8 100644 --- a/test/data_handling.jl +++ b/test/data_handling.jl @@ -52,23 +52,32 @@ UA_NodeId_delete(id3) # Variants # Set a scalar value -v = UA_Variant_new(Int32(42)) +v = UA_Variant_new() +value = Ref(Int32(42)) +type_ptr = UA_TYPES_PTRS[UA_TYPES_INT32] +retcode = UA_Variant_setScalarCopy(v, value, type_ptr) +@test retcode == UA_STATUSCODE_GOOD # Make a copy v2 = UA_Variant_new() UA_Variant_copy(v, v2) -@test unsafe_load(v2.type) == unsafe_load(v.type) @test unsafe_load(v2.storageType) == unsafe_load(v.storageType) @test unsafe_load(v2.arrayLength) == unsafe_load(v.arrayLength) @test open62541.unsafe_size(v2) == open62541.unsafe_size(v) @test open62541.length(v2) == open62541.length(v) @test unsafe_wrap(v2) == unsafe_wrap(v) UA_Variant_delete(v2) +UA_Variant_delete(v) # Set an array value d = Float64[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] -v3 = UA_Variant_new(d) -Base.promote_rule(::Type{T}, ::Type{UA_Double}) where {T <: AbstractFloat} = Float64 +type_ptr = UA_TYPES_PTRS[UA_TYPES_DOUBLE] +v3 = UA_Variant_new() +v3.arrayLength = length(d) +ua_arr = UA_Array_new(vec(permutedims(d, reverse(1:length(size(d))))), type_ptr) # Allocate new UA_Array from value with C style indexing +UA_Variant_setArray(v3, ua_arr, length(d), type_ptr) +v3.arrayDimensionsSize = length(size(d)) +v3.arrayDimensions = UA_UInt32_Array_new(reverse(size(d))) @test all(isapprox.(d, unsafe_wrap(v3))) # Set array dimensions From 5e7070003c2533a04b4fb6b49981990b8dc0ca22 Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Fri, 17 May 2024 00:28:32 +0200 Subject: [PATCH 18/26] started high level add node tests. --- README.md | 2 +- src/highlevel_server.jl | 4 +- src/highlevel_types.jl | 3 - test/add_change_var_scalar.jl | 1 - test/runtests.jl | 4 + test/server_add_nodes.jl | 78 +---- test/server_add_nodes_highlevelinterface.jl | 366 ++++++++++++++++++++ 7 files changed, 392 insertions(+), 66 deletions(-) create mode 100644 test/server_add_nodes_highlevelinterface.jl diff --git a/README.md b/README.md index 5c7cbd7..76470a2 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ open62541.jl is a [Julia](https://julialang.org) package that interfaces with th library written in C ([source](https://github.com/open62541/open62541)). As such, it provides functionality following the [OPC Unified Architecture (OPC UA) standard](https://en.wikipedia.org/wiki/OPC_Unified_Architecture) -for data exchange from sensors to cloud applications developed by the [OPC Foundation](). +for data exchange from sensors to cloud applications developed by the [OPC Foundation](https://opcfoundation.org/). In short, it provides the ability to create OPC servers that make data from different sources available to clients and, naturally, also a client functionality that allows diff --git a/src/highlevel_server.jl b/src/highlevel_server.jl index 77e8a33..d67eeaf 100644 --- a/src/highlevel_server.jl +++ b/src/highlevel_server.jl @@ -110,7 +110,7 @@ for nodeclass in instances(UA_NodeClass) function JUA_Server_addNode(server, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, attributes::$(attributetype_sym_J), - outNewNodeId, nodeContext, typeDefinition) + nodeContext, outNewNodeId, typeDefinition) return $(funname_sym)(server, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, typeDefinition, attributes, nodeContext, outNewNodeId) @@ -123,7 +123,7 @@ for nodeclass in instances(UA_NodeClass) function JUA_Server_addNode(server, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, attributes::$(attributetype_sym_J), - outNewNodeId, nodeContext) + nodeContext, outNewNodeId) return $(funname_sym)(server, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, attributes, nodeContext, outNewNodeId) diff --git a/src/highlevel_types.jl b/src/highlevel_types.jl index 4d30a21..7d4ebbb 100644 --- a/src/highlevel_types.jl +++ b/src/highlevel_types.jl @@ -29,9 +29,6 @@ function release_handle(obj::JUA_String) UA_String_delete(Jpointer(obj)) end -ua_data_type_ptr_default(::Type{JUA_String}) = ua_data_type_ptr_default(UA_String) -Base.convert(::Type{UA_String}, x::JUA_String) = unsafe_load(Jpointer(x)) - #Guid mutable struct JUA_Guid <: AbstractOpen62541Wrapper ptr::Ptr{UA_Guid} diff --git a/test/add_change_var_scalar.jl b/test/add_change_var_scalar.jl index 44abdbc..b0cd15b 100644 --- a/test/add_change_var_scalar.jl +++ b/test/add_change_var_scalar.jl @@ -90,7 +90,6 @@ for (type_ind, type) in enumerate(types) input = input_data[type_ind] varnodeid = JUA_NodeId(1, varnode_ids[type_ind]) output_client = JUA_Client_readValueAttribute(client, varnodeid) - @show output_client, input if type <: AbstractFloat @test all(isapprox.(input, output_client)) else diff --git a/test/runtests.jl b/test/runtests.jl index f063f72..04e4e40 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -49,6 +49,10 @@ end include("server_add_nodes.jl") end +@safetestset "Server Add Nodes Highlevel Interface" begin + include("server_add_nodes_highlevelinterface.jl") +end + # @safetestset "Memory leaks" begin # include("memoryleaks.jl") # end diff --git a/test/server_add_nodes.jl b/test/server_add_nodes.jl index 4d97c69..a8df214 100644 --- a/test/server_add_nodes.jl +++ b/test/server_add_nodes.jl @@ -110,7 +110,7 @@ pointvariableid1 = UA_NodeId_new() accesslevel = UA_ACCESSLEVEL(read = true, write = true) displayname = "a 2D point variable" description = "This is a 2D point variable." -attr = UA_VariableTypeAttributes_generate(value = input, +attr = UA_VariableAttributes_generate(value = input, displayname = displayname, description = description) retval4 = UA_Server_addVariableNode(server, UA_NodeId_new(), @@ -118,7 +118,7 @@ retval4 = UA_Server_addVariableNode(server, UA_NodeId_new(), UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), UA_QUALIFIEDNAME(1, "2DPoint Type"), pointtypeid, attr, C_NULL, pointvariableid1) -# Test whether adding the variable type node to the server worked +# Test whether adding the variable node to the server worked @test retval4 == UA_STATUSCODE_GOOD #now attempt to add a node with the wrong dimensions @@ -176,80 +176,80 @@ retval7 = UA_Server_addObjectNode( server, requestednewnodeid, parentnodeid, referencetypeid, browsename, typedefinition, oAttr, C_NULL, pumpid) -@test retval6 == UA_STATUSCODE_GOOD +@test retval7 == UA_STATUSCODE_GOOD pumpTypeId = UA_NODEID_NUMERIC(1, 1001) #Define the object type for "Device" deviceTypeId = UA_NodeId_new() dtAttr = UA_ObjectTypeAttributes_generate(displayname = "DeviceType", description = "Object type for a device") -retval7 = UA_Server_addObjectTypeNode(server, UA_NodeId_new(), +retval8 = UA_Server_addObjectTypeNode(server, UA_NodeId_new(), UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE), UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE), UA_QUALIFIEDNAME(1, "DeviceType"), dtAttr, C_NULL, deviceTypeId) -@test retval7 == UA_STATUSCODE_GOOD +@test retval8 == UA_STATUSCODE_GOOD #add manufacturer name to device mnAttr = UA_VariableAttributes_generate(value = "", displayname = "ManufacturerName", description = "Name of the manufacturer") manufacturerNameId = UA_NodeId_new() -retval8 = UA_Server_addVariableNode(server, UA_NodeId_new(), deviceTypeId, +retval9 = UA_Server_addVariableNode(server, UA_NodeId_new(), deviceTypeId, UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), UA_QUALIFIEDNAME(1, "ManufacturerName"), UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), mnAttr, C_NULL, manufacturerNameId); -@test retval8 == UA_STATUSCODE_GOOD +@test retval9 == UA_STATUSCODE_GOOD #Make the manufacturer name mandatory -retval9 = UA_Server_addReference(server, manufacturerNameId, +retval10 = UA_Server_addReference(server, manufacturerNameId, UA_NODEID_NUMERIC(0, UA_NS0ID_HASMODELLINGRULE), UA_EXPANDEDNODEID_NUMERIC(0, UA_NS0ID_MODELLINGRULE_MANDATORY), true) -@test retval9 == UA_STATUSCODE_GOOD +@test retval10 == UA_STATUSCODE_GOOD #Add model name modelAttr = UA_VariableAttributes_generate(value = "", displayname = "ModelName", description = "Name of the model") -retval10 = UA_Server_addVariableNode(server, UA_NodeId_new(), deviceTypeId, +retval11 = UA_Server_addVariableNode(server, UA_NodeId_new(), deviceTypeId, UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), UA_QUALIFIEDNAME(1, "ModelName"), UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), modelAttr, C_NULL, C_NULL); -@test retval10 == UA_STATUSCODE_GOOD +@test retval11 == UA_STATUSCODE_GOOD #Define the object type for "Pump" ptAttr = UA_ObjectTypeAttributes_generate(displayname = "PumpType", description = "Object type for a pump") -retval11 = UA_Server_addObjectTypeNode(server, pumpTypeId, +retval12 = UA_Server_addObjectTypeNode(server, pumpTypeId, deviceTypeId, UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE), UA_QUALIFIEDNAME(1, "PumpType"), ptAttr, C_NULL, C_NULL) -@test retval11 == UA_STATUSCODE_GOOD +@test retval12 == UA_STATUSCODE_GOOD statusAttr = UA_VariableAttributes_generate(value = false, displayname = "Status", description = "Status") statusId = UA_NodeId_new() -retval12 = UA_Server_addVariableNode(server, UA_NodeId_new(), pumpTypeId, +retval13 = UA_Server_addVariableNode(server, UA_NodeId_new(), pumpTypeId, UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), UA_QUALIFIEDNAME(1, "Status"), UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), statusAttr, C_NULL, statusId) -@test retval12 == UA_STATUSCODE_GOOD +@test retval13 == UA_STATUSCODE_GOOD #/* Make the status variable mandatory */ -retval13 = UA_Server_addReference(server, statusId, +retval14 = UA_Server_addReference(server, statusId, UA_NODEID_NUMERIC(0, UA_NS0ID_HASMODELLINGRULE), UA_EXPANDEDNODEID_NUMERIC(0, UA_NS0ID_MODELLINGRULE_MANDATORY), true) -@test retval13 == UA_STATUSCODE_GOOD +@test retval14 == UA_STATUSCODE_GOOD rpmAttr = UA_VariableAttributes_generate(displayname = "MotorRPM", description = "Pump speed in rpm", value = 0) -retval14 = UA_Server_addVariableNode(server, UA_NodeId_new(), pumpTypeId, +retval15 = UA_Server_addVariableNode(server, UA_NodeId_new(), pumpTypeId, UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), UA_QUALIFIEDNAME(1, "MotorRPMs"), UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), rpmAttr, C_NULL, C_NULL) -@test retval14 == UA_STATUSCODE_GOOD +@test retval15 == UA_STATUSCODE_GOOD function addPumpObjectInstance(server, name) oAttr = UA_ObjectAttributes_generate(displayname = name, description = name) @@ -365,43 +365,3 @@ if !Sys.isapple() end #TODO: this will need a test to see whether any memory is leaking. - -#Now test with the higher level JUA interface as well -#configure server -server2 = JUA_Server() -retvalj0 = JUA_ServerConfig_setMinimalCustomBuffer(JUA_ServerConfig(server2), - 4842, C_NULL, 0, 0) -@test retvalj0 == UA_STATUSCODE_GOOD - -#Variable node: scalar -accesslevel = UA_ACCESSLEVEL(read = true, write = true) -input = rand(Float64) -attr = JUA_VariableAttributes(value = input, - displayname = "scalar variable", - description = "this is a scalar variable", - accesslevel = accesslevel) -varnodeid = JUA_NodeId(1, "scalar variable") -parentnodeid = JUA_NodeId(0, UA_NS0ID_OBJECTSFOLDER) -parentreferencenodeid = JUA_NodeId(0, UA_NS0ID_ORGANIZES) -typedefinition = JUA_NodeId(0, UA_NS0ID_BASEDATAVARIABLETYPE) -browsename = JUA_QualifiedName(1, "scalar variable") -nodecontext = JUA_NodeId() -outnewnodeid = JUA_NodeId() -retvalj1 = JUA_Server_addNode(server2, varnodeid, parentnodeid, - parentreferencenodeid, browsename, attr, nodecontext, - outnewnodeid, typedefinition) -@test retvalj1 == UA_STATUSCODE_GOOD - -# hit objecttype add node function -pumpTypeId = JUA_NodeId(1, 1001) -#Define the object type for "Device" -deviceTypeId = JUA_NodeId() -attr = JUA_ObjectTypeAttributes(displayname = "DeviceType", - description = "Object type for a device") -parentnodeid = JUA_NodeId(0, UA_NS0ID_BASEOBJECTTYPE) -parentreferencenodeid = JUA_NodeId(0, UA_NS0ID_HASSUBTYPE) -browsename = JUA_QualifiedName(1, "DeviceType") -retvalj2 = JUA_Server_addNode(server2, JUA_NodeId(), parentnodeid, - parentreferencenodeid, browsename, attr, JUA_NodeId(), - outnewnodeid) -@test retvalj2 == UA_STATUSCODE_GOOD diff --git a/test/server_add_nodes_highlevelinterface.jl b/test/server_add_nodes_highlevelinterface.jl new file mode 100644 index 0000000..5f54219 --- /dev/null +++ b/test/server_add_nodes_highlevelinterface.jl @@ -0,0 +1,366 @@ +# Simple checks whether addition of different node types was successful or not +# Closely follows https://www.open62541.org/doc/1.3/tutorial_server_variabletype.html + +#TODO: need to clean up in terms of memory management. + +using open62541 +using Test + +#configure server +server = JUA_Server() +retval0 = JUA_ServerConfig_setMinimalCustomBuffer(JUA_ServerConfig(server), + 4842, C_NULL, 0, 0) +@test retval0 == UA_STATUSCODE_GOOD + +## Variable nodes with scalar and array floats - other number types are tested +## in add_change_var_scalar.jl and add_change_var_array.jl + +#Variable node: scalar +accesslevel = UA_ACCESSLEVEL(read = true, write = true) +input = rand(Float64) +attr = JUA_VariableAttributes(value = input, + displayname = "scalar variable", + description = "this is a scalar variable", + accesslevel = accesslevel) +varnodeid = JUA_NodeId(1, "scalar variable") +parentnodeid = JUA_NodeId(0, UA_NS0ID_OBJECTSFOLDER) +parentreferencenodeid = JUA_NodeId(0, UA_NS0ID_ORGANIZES) +typedefinition = JUA_NodeId(0, UA_NS0ID_BASEDATAVARIABLETYPE) +browsename = JUA_QualifiedName(1, "scalar variable") +nodecontext = JUA_NodeId() +outnewnodeid = JUA_NodeId() + +#UA interface +retval1 = JUA_Server_addNode(server, varnodeid, parentnodeid, + parentreferencenodeid, + browsename, attr, nodecontext, outnewnodeid, typedefinition) +#test whether adding node to the server worked +@test retval1 == UA_STATUSCODE_GOOD +# Test whether the correct array is within the server (read from server) +output_server = JUA_Server_readValue(server, varnodeid, Float64) +@test all(isapprox(input, output_server)) + +#Variable node: array +input = rand(Float64, 2, 3, 4) +varnodetext = "array variable" +accesslevel = UA_ACCESSLEVEL(read = true, write = true) +attr = JUA_VariableAttributes(value = input, + displayname = varnodetext, + description = "this is an array variable", + accesslevel = accesslevel) +varnodeid = JUA_NodeId(1, varnodetext) +parentnodeid = JUA_NodeId(0, UA_NS0ID_OBJECTSFOLDER) +parentreferencenodeid = JUA_NodeId(0, UA_NS0ID_ORGANIZES) +typedefinition = JUA_NodeId(0, UA_NS0ID_BASEDATAVARIABLETYPE) +browsename = JUA_QualifiedName(1, varnodetext) +nodecontext = JUA_NodeId() +outnewnodeid = JUA_NodeId() +retval2 = JUA_Server_addNode(server, varnodeid, parentnodeid, + parentreferencenodeid, browsename, attr, nodecontext, outnewnodeid, + typedefinition) +# Test whether adding node to the server worked +@test retval2 == UA_STATUSCODE_GOOD +# Test whether the correct array is within the server (read from server) +output_server = JUA_Server_readValue(server, varnodeid, Array{Float64, 3}) +@test all(isapprox(input, output_server)) + +## VariableTypeNode - array +input = zeros(2) +accesslevel = UA_ACCESSLEVEL(read = true) +displayname = "2D point type" +description = "This is a 2D point type." +attr = JUA_VariableTypeAttributes(value = input, + displayname = displayname, + description = description) +requestednewnodeid = JUA_NodeId() +parentnodeid = JUA_NodeId(0, UA_NS0ID_BASEDATAVARIABLETYPE) +referencetypeid = JUA_NodeId(0, UA_NS0ID_HASSUBTYPE) +browsename = JUA_QualifiedName(1, "2DPoint Type") +nodecontext = JUA_NodeId() +pointtypeid = JUA_NodeId() +typedefinition = JUA_NodeId() + +retval3 = JUA_Server_addNode(server, requestednewnodeid, parentnodeid, + referencetypeid, browsename, attr, nodecontext, pointtypeid, typedefinition) + +# Test whether adding the variable type node to the server worked +@test retval3 == UA_STATUSCODE_GOOD + +#now add a variable node based on the variabletype node that we just defined. +input = rand(2) +pointvariableid1 = JUA_NodeId() +accesslevel = UA_ACCESSLEVEL(read = true, write = true) +displayname = "a 2D point variable" +description = "This is a 2D point variable." +attr = JUA_VariableAttributes(value = input, + displayname = displayname, + description = description) +varnodeid = JUA_NodeId(1, varnodetext) +parentnodeid = JUA_NodeId(0, UA_NS0ID_OBJECTSFOLDER) +parentreferencenodeid = JUA_NodeId(0, UA_NS0ID_ORGANIZES) +typedefinition = JUA_NodeId(0, UA_NS0ID_BASEDATAVARIABLETYPE) +browsename = JUA_QualifiedName(1, "2DPoint variable") +nodecontext = JUA_NodeId() + +retval4 = JUA_Server_addNode(server, JUA_NodeId(), parentnodeid, + parentreferencenodeid, browsename, attr, nodecontext, pointvariableid1, + pointtypeid) +# Test whether adding the variable type node to the server worked +@test retval4 == UA_STATUSCODE_GOOD + +#now attempt to add a node with the wrong dimensions +input = rand(2, 3) +pointvariableid2 = JUA_NodeId() +accesslevel = UA_ACCESSLEVEL(read = true, write = true) +displayname = "not a 2d point variable" +description = "This should fail" +attr = JUA_VariableAttributes(value = input, + displayname = displayname, + description = description) +parentnodeid = JUA_NodeId(0, UA_NS0ID_BASEDATAVARIABLETYPE) +referencetypeid = JUA_NodeId(0, UA_NS0ID_HASCOMPONENT) +requestednewnodeid = JUA_NodeId() +browsename = JUA_QualifiedName(1, "2DPoint variable - wrong") +retval5 = JUA_Server_addNode(server, requestednewnodeid, parentnodeid, referencetypeid, + browsename, attr, JUA_NodeId(), pointvariableid2, pointtypeid) + +# Test whether adding the variable type node to the server worked +@test retval5 == UA_STATUSCODE_BADTYPEMISMATCH + +#and now we just want to change value rank (which again shouldn't be allowed) +@test_throws open62541.AttributeReadWriteError UA_Server_writeValueRank(server, + pointvariableid1, + UA_VALUERANK_ONE_OR_MORE_DIMENSIONS) + +#variable type node - scalar +input = 42 +scalartypeid = JUA_NodeId() +accesslevel = UA_ACCESSLEVEL(read = true) +displayname = "scalar integer type" +description = "This is a scalar integer type." +attr = JUA_VariableTypeAttributes(value = input, + displayname = displayname, + description = description) +parentnodeid = JUA_NodeId(0, UA_NS0ID_BASEDATAVARIABLETYPE) +referencetypeid = JUA_NodeId(0, UA_NS0ID_HASSUBTYPE) +browsename = JUA_QualifiedName(1, "scalar integer type") +requestednewnodeid = JUA_NodeId() +nodecontext = JUA_NodeId() +typedefinition = JUA_NodeId() +retval6 = JUA_Server_addNode(server, requestednewnodeid, + parentnodeid, referencetypeid, + browsename, attr, nodecontext, scalartypeid, typedefinition) + +# Test whether adding the variable type node to the server worked +@test retval6 == UA_STATUSCODE_GOOD + +#add object node +#follows this tutorial page: https://www.open62541.org/doc/1.3/tutorial_server_object.html +pumpid = JUA_NodeId() +displayname = "Pump (Manual)" +description = "This is a manually added pump." +oAttr = JUA_ObjectAttributes(displayname = displayname, description = description) +requestednewnodeid = JUA_NodeId() +parentnodeid = JUA_NodeId(0, UA_NS0ID_OBJECTSFOLDER) +referencetypeid = JUA_NodeId(0, UA_NS0ID_ORGANIZES) +browsename = JUA_QualifiedName(1, displayname) +typedefinition = JUA_NodeId(0, UA_NS0ID_BASEOBJECTTYPE) +nodecontext = JUA_NodeId() +retval7 = JUA_Server_addNode( + server, requestednewnodeid, parentnodeid, referencetypeid, + browsename, oAttr, nodecontext, pumpid, typedefinition) +@test retval7 == UA_STATUSCODE_GOOD + +#Define the object type for "Device" +pumpTypeId = JUA_NodeId(1, 1001) +deviceTypeId = JUA_NodeId() +dtAttr = JUA_ObjectTypeAttributes(displayname = "DeviceType", + description = "Object type for a device") +browsename = JUA_QualifiedName(1, "DeviceType") +requestednewnodeid = JUA_NodeId() +parentnodeid = JUA_NodeId(0, UA_NS0ID_BASEOBJECTTYPE) +parentreferencenodeid = JUA_NodeId(0, UA_NS0ID_HASSUBTYPE) +nodecontext = JUA_NodeId() +retval8 = JUA_Server_addNode(server, requestednewnodeid, + parentnodeid, parentreferencenodeid, + browsename, dtAttr, + nodecontext, deviceTypeId) +@test retval8 == UA_STATUSCODE_GOOD + +## TODO: Revise rest of the code from here. + +#add manufacturer name to device +mnAttr = UA_VariableAttributes_generate(value = "", + displayname = "ManufacturerName", + description = "Name of the manufacturer") +manufacturerNameId = UA_NodeId_new() +retval8 = UA_Server_addVariableNode(server, UA_NodeId_new(), deviceTypeId, + UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), + UA_QUALIFIEDNAME(1, "ManufacturerName"), + UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), mnAttr, C_NULL, manufacturerNameId); +@test retval8 == UA_STATUSCODE_GOOD + +#Make the manufacturer name mandatory +retval9 = UA_Server_addReference(server, manufacturerNameId, + UA_NODEID_NUMERIC(0, UA_NS0ID_HASMODELLINGRULE), + UA_EXPANDEDNODEID_NUMERIC(0, UA_NS0ID_MODELLINGRULE_MANDATORY), true) +@test retval9 == UA_STATUSCODE_GOOD + +#Add model name +modelAttr = UA_VariableAttributes_generate(value = "", + displayname = "ModelName", + description = "Name of the model") +retval10 = UA_Server_addVariableNode(server, UA_NodeId_new(), deviceTypeId, + UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), + UA_QUALIFIEDNAME(1, "ModelName"), + UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), modelAttr, C_NULL, C_NULL); +@test retval10 == UA_STATUSCODE_GOOD + +#Define the object type for "Pump" +ptAttr = UA_ObjectTypeAttributes_generate(displayname = "PumpType", + description = "Object type for a pump") +retval11 = UA_Server_addObjectTypeNode(server, pumpTypeId, + deviceTypeId, UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE), + UA_QUALIFIEDNAME(1, "PumpType"), ptAttr, + C_NULL, C_NULL) +@test retval11 == UA_STATUSCODE_GOOD + +statusAttr = UA_VariableAttributes_generate(value = false, + displayname = "Status", + description = "Status") +statusId = UA_NodeId_new() +retval12 = UA_Server_addVariableNode(server, UA_NodeId_new(), pumpTypeId, + UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), + UA_QUALIFIEDNAME(1, "Status"), + UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), statusAttr, C_NULL, statusId) +@test retval12 == UA_STATUSCODE_GOOD + +#/* Make the status variable mandatory */ +retval13 = UA_Server_addReference(server, statusId, + UA_NODEID_NUMERIC(0, UA_NS0ID_HASMODELLINGRULE), + UA_EXPANDEDNODEID_NUMERIC(0, UA_NS0ID_MODELLINGRULE_MANDATORY), true) +@test retval13 == UA_STATUSCODE_GOOD + +rpmAttr = UA_VariableAttributes_generate(displayname = "MotorRPM", + description = "Pump speed in rpm", + value = 0) +retval14 = UA_Server_addVariableNode(server, UA_NodeId_new(), pumpTypeId, + UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), + UA_QUALIFIEDNAME(1, "MotorRPMs"), + UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), rpmAttr, C_NULL, C_NULL) +@test retval14 == UA_STATUSCODE_GOOD + +function addPumpObjectInstance(server, name) + oAttr = UA_ObjectAttributes_generate(displayname = name, description = name) + UA_Server_addObjectNode(server, UA_NodeId_new(), + UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER), + UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), + UA_QUALIFIEDNAME(1, name), + pumpTypeId, #/* this refers to the object type + # identifier */ + oAttr, C_NULL, C_NULL) +end + +function pumpTypeConstructor(server, sessionId, sessionContext, + typeId, typeContext, nodeId, nodeContext) + #UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "New pump created"); + + #/* Find the NodeId of the status child variable */ + rpe = UA_RelativePathElement_new() + UA_RelativePathElement_init(rpe) + rpe.referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT) + rpe.isInverse = false + rpe.includeSubtypes = false + rpe.targetName = UA_QUALIFIEDNAME(1, "Status") + + bp = UA_BrowsePath_new() + UA_BrowsePath_init(bp) + bp.startingNode = nodeId + bp.relativePath.elementsSize = 1 + bp.relativePath.elements = rpe + + bpr = UA_Server_translateBrowsePathToNodeIds(server, bp) + if bpr.statusCode != UA_STATUSCODE_GOOD || bpr.targetsSize < 1 + return bpr.statusCode + end + + #Set the status value + status = true + value = UA_Variant_new() + UA_Variant_setScalarCopy(value, Ref(status), UA_TYPES_PTRS[UA_TYPES_BOOLEAN]) + UA_Server_writeValue(server, bpr.targets.targetId.nodeId, value) + + #TODO: clean up to avoid memory leaks + return UA_STATUSCODE_GOOD +end + +if !Sys.isapple() + function addPumpTypeConstructor(server) + c_pumpTypeConstructor = UA_NodeTypeLifecycleCallback_constructor_generate(pumpTypeConstructor) + lifecycle = UA_NodeTypeLifecycle(c_pumpTypeConstructor, C_NULL) + UA_Server_setNodeTypeLifecycle(server, open62541.Jpointer(pumpTypeId), lifecycle) #TODO: probably this needs a high level method as well / + end + + addPumpObjectInstance(server, "pump2") #should have status = false (constructor not in place yet) + addPumpObjectInstance(server, "pump3") #should have status = false (constructor not in place yet) + addPumpTypeConstructor(server) + addPumpObjectInstance(server, "pump4") #should have status = true + addPumpObjectInstance(server, "pump5") #should have status = true + + #add method node + #follows this: https://www.open62541.org/doc/1.3/tutorial_server_method.html + + function helloWorldMethodCallback(server, sessionId, sessionHandle, methodId, + methodContext, objectId, objectContext, inputSize, input, outputSize, output) + inputstr = unsafe_string(unsafe_wrap(input)) + tmp = UA_STRING("Hello " * inputstr) + UA_Variant_setScalarCopy(output, tmp, UA_TYPES_PTRS[UA_TYPES_STRING]) + UA_String_delete(tmp) + return UA_STATUSCODE_GOOD + end + + inputArgument = UA_Argument_new() + inputArgument.description = UA_LOCALIZEDTEXT("en-US", "A String") + inputArgument.name = UA_STRING("MyInput"); + inputArgument.dataType = UA_TYPES_PTRS[UA_TYPES_STRING].typeId; + inputArgument.valueRank = UA_VALUERANK_SCALAR + outputArgument = UA_Argument_new() + outputArgument.description = UA_LOCALIZEDTEXT("en-US", "A String"); + outputArgument.name = UA_STRING("MyOutput"); + outputArgument.dataType = UA_TYPES_PTRS[UA_TYPES_STRING].typeId + outputArgument.valueRank = UA_VALUERANK_SCALAR + helloAttr = UA_MethodAttributes_generate(description = "Say Hello World", + displayname = "Hello World", + executable = true, + userexecutable = true) + + methodid = UA_NODEID_NUMERIC(1, 62541) + obj = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER) + retval = UA_Server_addMethodNode(server, methodid, + obj, + UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), + UA_QUALIFIEDNAME(1, "hello world"), + helloAttr, helloWorldMethodCallback, + 1, inputArgument, 1, outputArgument, C_NULL, C_NULL) + + @test retval == UA_STATUSCODE_GOOD + + inputArguments = UA_Variant_new() + ua_s = UA_STRING("Peter") + UA_Variant_setScalar(inputArguments, ua_s, UA_TYPES_PTRS[UA_TYPES_STRING]) + req = UA_CallMethodRequest_new() + req.objectId = obj + req.methodId = methodid + req.inputArgumentsSize = 1 + req.inputArguments = inputArguments + + answer = UA_CallMethodResult_new() + UA_Server_call(server, req, answer) + @test unsafe_load(answer.statusCode) == UA_STATUSCODE_GOOD + @test unsafe_string(unsafe_wrap(unsafe_load(answer.outputArguments))) == "Hello Peter" + + UA_CallMethodRequest_delete(req) + UA_CallMethodResult_delete(answer) +end + +#TODO: this will need a test to see whether any memory is leaking anywhere From a0aa7bb898783c02c5afd3650073a2313e9f87b4 Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Tue, 21 May 2024 18:10:15 +0200 Subject: [PATCH 19/26] docstring; interface improvements --- docs/src/manual/attributegeneration.md | 17 +- src/attribute_generation.jl | 8 +- src/callbacks.jl | 5 +- src/client.jl | 82 +--- src/highlevel_client.jl | 101 ++++- src/highlevel_server.jl | 16 +- src/highlevel_types.jl | 449 ++++++++++++++++++-- src/server.jl | 4 - src/types.jl | 7 +- test/server_add_nodes_highlevelinterface.jl | 346 ++++++++------- 10 files changed, 737 insertions(+), 298 deletions(-) diff --git a/docs/src/manual/attributegeneration.md b/docs/src/manual/attributegeneration.md index b640660..b47f328 100644 --- a/docs/src/manual/attributegeneration.md +++ b/docs/src/manual/attributegeneration.md @@ -1,6 +1,8 @@ # Attribute generation -This page lists docstrings of functions used for convenient attribute generation. +This page lists docstrings of functions used for the convenient generation of +node attribute structures. Their main use is when adding nodes to a server through +client API (see [`JUA_Client_addNode`](@ref)) or the server API (see [`JUA_Server_addNode`](@ref)). Convenience functions that allow setting values for specific attributes: @@ -13,8 +15,19 @@ UA_USERWRITEMASK UA_EVENTNOTIFIER ``` -Generators for attribute blocks: +High level generators for attribute blocks: +```@docs +JUA_VariableAttributes +JUA_VariableTypeAttributes +JUA_ObjectAttributes +JUA_ObjectTypeAttributes +JUA_MethodAttributes +JUA_ViewAttributes +JUA_DataTypeAttributes +JUA_ReferenceTypeAttributes +``` +Lower level generators for attribute blocks: ```@docs UA_VariableAttributes_generate UA_VariableTypeAttributes_generate diff --git a/src/attribute_generation.jl b/src/attribute_generation.jl index 9489c1c..c63a0b3 100644 --- a/src/attribute_generation.jl +++ b/src/attribute_generation.jl @@ -392,9 +392,8 @@ how to generate the respective keyword inputs. """ function UA_VariableTypeAttributes_generate(; value::Union{ - AbstractArray{<:Union{ - AbstractString, AbstractFloat, Integer, ComplexF32, ComplexF64}}, - Union{Nothing, AbstractString, AbstractFloat, Integer, ComplexF32, ComplexF64}} = nothing, + AbstractArray{<:Union{UA_NUMBER_TYPES, AbstractString, ComplexF32, ComplexF64}}, + Union{Nothing, UA_NUMBER_TYPES, AbstractString, ComplexF32, ComplexF64}} = nothing, displayname::AbstractString, description::AbstractString, localization::AbstractString = "en-US", writemask::Union{Nothing, UInt32} = nothing, @@ -560,7 +559,8 @@ UA_MethodAttributes_generate(; displayname::AbstractString, description::AbstractString, localization::AbstractString = "en-US", writemask::Union{Nothing, UInt32} = nothing, userwritemask::Union{Nothing, UInt32} = nothing, - isabstract::Union{Nothing, Bool} = nothing)::Ptr{UA_MethodAttributes} + executable::Union{Nothing, Bool} = nothing, + userexecutable::Union{Nothing, Bool} = nothing)::Ptr{UA_MethodAttributes} ``` generates a `UA_MethodAttributes` object. Memory for the object is allocated by diff --git a/src/callbacks.jl b/src/callbacks.jl index 5c8398f..48fce8c 100644 --- a/src/callbacks.jl +++ b/src/callbacks.jl @@ -77,7 +77,10 @@ creates a `UA_MethodCallback` that can be attached to a method node using `UA_Server_addMethodNode`. `f` must be a Julia function with the following signature: -```f(server::Ptr{UA_Server}, sessionId::Ptr{UA_NodeId}), sessionContext::Ptr{Cvoid}`, methodId::Ptr{UA_NodeId}, methodContext::Ptr{Cvoid}, objectId::Ptr{UA_NodeId}, objectContext::Ptr{Cvoid}, inputSize::Csize_t, input::Ptr{UA_Variant}, outputSize::Csize_t, output::Ptr{UA_Variant})::UA_StatusCode``` +```f(server::Ptr{UA_Server}, sessionId::Ptr{UA_NodeId}), sessionContext::Ptr{Cvoid}`, + methodId::Ptr{UA_NodeId}, methodContext::Ptr{Cvoid}, objectId::Ptr{UA_NodeId}, + objectContext::Ptr{Cvoid}, inputSize::Csize_t, input::Ptr{UA_Variant}, + outputSize::Csize_t, output::Ptr{UA_Variant})::UA_StatusCode``` """ function UA_MethodCallback_generate(f::Function) argtuple = (Ptr{UA_Server}, Ptr{UA_NodeId}, Ptr{Cvoid}, Ptr{UA_NodeId}, diff --git a/src/client.jl b/src/client.jl index 783ada6..2c15fc1 100644 --- a/src/client.jl +++ b/src/client.jl @@ -112,17 +112,6 @@ for nodeclass in instances(UA_NodeClass) if funname_sym == :UA_Client_addVariableNode || funname_sym == :UA_Client_addObjectNode @eval begin - # emit specific add node functions - #TODO: add tests - # Untested: UA_Client_addVariableNode - # Untested: UA_Client_addObjectNode - # Untested: UA_Client_addVariableTypeNode - # Untested: UA_Client_addReferenceTypeNode - # Untested: UA_Client_addObjectTypeNode - # Untested: UA_Client_addReferenceTypeNode - # Untested: UA_Client_addDataTypeNode - # Untested: UA_Client_addMethodNode (does addmethodnode even make sense? There is no possibility to transfer a callback over?!?!) - # --> Tests should go into test/client_add_nodes.jl """ ``` $($(funname_sym))(::Ptr{UA_Client}, requestednewnodeid::Ptr{UA_NodeId}, @@ -137,40 +126,13 @@ for nodeclass in instances(UA_NodeClass) See [`$($(attributetype_sym))_generate`](@ref) on how to define valid attributes. """ - function $(funname_sym)(client, - requestedNewNodeId, - parentNodeId, - referenceTypeId, - browseName, - typeDefinition, - attributes, + function $(funname_sym)(client, requestedNewNodeId, parentNodeId, + referenceTypeId, browseName, typeDefinition, attributes, outNewNodeId) return __UA_Client_addNode(client, $(nodeclass_sym), - requestedNewNodeId, - parentNodeId, referenceTypeId, browseName, - typeDefinition, attributes, - UA_TYPES_PTRS[$(attributeptr_sym)], - outNewNodeId) - #higher level function using dispatch - #TODO: add docstring - #TODO: add tests - function JUA_Client_addNode(client, - requestedNewNodeId, - parentNodeId, - referenceTypeId, - browseName, - attributes::Ptr{$(attributetype_sym)}, - outNewNodeId, - typeDefinition) - return $(funname_sym)(client, - requestedNewNodeId, - parentNodeId, - referenceTypeId, - browseName, - typeDefinition, - attributes, - outNewNodeId) - end + requestedNewNodeId, parentNodeId, referenceTypeId, + browseName, typeDefinition, attributes, + UA_TYPES_PTRS[$(attributeptr_sym)], outNewNodeId) end end else @@ -189,36 +151,12 @@ for nodeclass in instances(UA_NodeClass) See [`$($(attributetype_sym))_generate`](@ref) on how to define valid attributes. """ - function $(funname_sym)(client, - requestedNewNodeId, - parentNodeId, - referenceTypeId, - browseName, - attributes, - outNewNodeId) + function $(funname_sym)(client, requestedNewNodeId, parentNodeId, + referenceTypeId, browseName, attributes, outNewNodeId) return __UA_Client_addNode(client, $(nodeclass_sym), - requestedNewNodeId, - parentNodeId, referenceTypeId, browseName, - UA_NODEID_NULL, attributes, - UA_TYPES_PTRS[$(attributeptr_sym)], - outNewNodeId) - end - - #higher level function using dispatch - function JUA_Client_addNode(client, - requestedNewNodeId, - parentNodeId, - referenceTypeId, - browseName, - attributes::Ptr{$(attributetype_sym)}, - outNewNodeId) - return $(funname_sym)(client, - requestedNewNodeId, - parentNodeId, - referenceTypeId, - browseName, - attributes, - outNewNodeId) + requestedNewNodeId, parentNodeId, referenceTypeId, + browseName, UA_NODEID_NULL, attributes, + UA_TYPES_PTRS[$(attributeptr_sym)], outNewNodeId) end end end diff --git a/src/highlevel_client.jl b/src/highlevel_client.jl index b3dfce8..35acd2a 100644 --- a/src/highlevel_client.jl +++ b/src/highlevel_client.jl @@ -28,14 +28,99 @@ const JUA_Client_connect = UA_Client_connect const JUA_Client_disconnect = UA_Client_disconnect #Add node functions +JUA_Client_addNode(client, requestedNewNodeId, + parentNodeId, referenceTypeId, browseName, + attributes::$(attributetype_sym_J), + outNewNodeId, typeDefinition) +""" +``` +JUA_Client_addNode(client::JUA_Client, requestedNewNodeId::JUA_NodeId, + parentNodeId::JUA_NodeId, referenceTypeId::JUA_NodeId, browseName::JUA_QualifiedName, + attributes::Union{JUA_VariableAttributes, JUA_VariableTypeAttributes, JUA_ObjectAttributes}, + outNewNodeId::JUA_NodeId, typeDefinition::JUA_NodeId)::UA_StatusCode +``` + +uses the client API to add a Variable, VariableType, or Object node to the server +to which the client is connected to. +See [`JUA_VariableAttributes`](@ref), [`JUA_VariableTypeAttributes`](@ref), and [`JUA_ObjectAttributes`](@ref) +on how to define valid attributes. + +``` +JUA_Client_addNode(client::JUA_Client, requestedNewNodeId::JUA_NodeId, + parentNodeId, referenceTypeId::JUA_NodeId, browseName::JUA_QualifiedName, + attributes::Union{JUA_ObjectTypeAttributes, JUA_ReferenceTypeAttributes, JUA_DataTypeAttributes, JUA_ViewAttributes}, + outNewNodeId::JUA_NodeId)::UA_StatusCode +``` + +uses the client API to add a Variable, VariableType, or Object node to the server +to which the client is connected to. + +See [`JUA_ObjectTypeAttributes`](@ref), See [`JUA_ReferenceTypeAttributes`](@ref), [`JUA_DataTypeAttributes`](@ref), and [`JUA_ViewAttributes`](@ref) on how to define valid attributes. +""" +function JUA_Client_addNode end + +for nodeclass in instances(UA_NodeClass) + if nodeclass != __UA_NODECLASS_FORCE32BIT && nodeclass != UA_NODECLASS_UNSPECIFIED + nodeclass_sym = Symbol(nodeclass) + funname_sym = Symbol(replace( + "UA_Client_add" * + titlecase(string(nodeclass_sym)[14:end]) * + "Node", + "type" => "Type")) + attributeptr_sym = Symbol(uppercase("UA_TYPES_" * string(nodeclass_sym)[14:end] * + "ATTRIBUTES")) + attributetype_sym_J = Symbol("J"*replace( + "UA_" * + titlecase(string(nodeclass_sym)[14:end]) * + "Attributes", + "type" => "Type")) + if funname_sym == :UA_Client_addVariableNode || funname_sym == :UA_Client_addObjectNode + @eval begin + function JUA_Client_addNode(client, requestedNewNodeId, + parentNodeId, referenceTypeId, browseName, + attributes::$(attributetype_sym_J), + outNewNodeId, typeDefinition) + return $(funname_sym)(client, requestedNewNodeId, + parentNodeId, referenceTypeId, browseName, typeDefinition, + attributes, outNewNodeId) + end + end + else + @eval begin + function JUA_Client_addNode(client, requestedNewNodeId, + parentNodeId, referenceTypeId, browseName, + attributes::$(attributetype_sym_J), outNewNodeId) + return $(funname_sym)(client, requestedNewNodeId, + parentNodeId, referenceTypeId, browseName, attributes, + outNewNodeId) + end + end + end + end +end #Client read and write functions -#TODO: add docstring +""" +``` +value = JUA_Client_readValue(server::JUA_Client, nodeId::JUA_NodeId, type = Any) +``` + +uses the client API to read the value of `nodeId` from the server that the `client` +is connected to. + +The output `value` is automatically converted to a Julia type (such as Float64, String, Vector{String}, +etc.) if possible. Otherwise, open62541 composite types are returned. + +Note: Since it is unknown what type of value is stored within `nodeId` before reading +it, this function is inherently type unstable. + +Type stability is improved if the optional argument `type` is provided, for example, +if you know that you have stored a Matrix{Float64} in `nodeId`, then you should +specify this. If the wrong type is specified, the function will throw a TypeError. + +""" function JUA_Client_readValueAttribute(client, nodeId, type::T = Any) where {T} - #TODO: Is there a way of making this typestable? - #(it's not really known what kind of data is stored inside a nodeid unless - #one checks the datatype beforehand) v = UA_Variant_new() UA_Client_readValueAttribute(client, nodeId, v) r = __get_juliavalues_from_variant(v, type) @@ -48,7 +133,9 @@ end JUA_Client_writeValueAttribute(server::JUA_Client, nodeId::JUA_NodeId, newvalue)::UA_StatusCode ``` -uses the client API to write the value `newvalue` to `nodeId` on `server`. +uses the client API to write the value `newvalue` to `nodeId` to the server that +the `client` is connected to. + `new_value` must either be a `JUA_Variant` or a Julia value/array compatible with any of its constructors. @@ -64,4 +151,6 @@ end function JUA_Client_writeValueAttribute(client, nodeId, newvalue::JUA_Variant) statuscode = UA_Client_writeValueAttribute(client, nodeId, newvariant) return statuscode -end \ No newline at end of file +end + + diff --git a/src/highlevel_server.jl b/src/highlevel_server.jl index d67eeaf..575d770 100644 --- a/src/highlevel_server.jl +++ b/src/highlevel_server.jl @@ -76,6 +76,8 @@ JUA_Server_addNode(server::JUA_Server, requestedNewNodeId::JUA_NodeId, uses the server API to add a ObjectType, ReferenceType, DataType, or View node to the `server`. See [`JUA_ObjectTypeAttributes`](@ref), See [`JUA_ReferenceTypeAttributes`](@ref), [`JUA_DataTypeAttributes`](@ref), and [`JUA_ViewAttributes`](@ref) on how to define valid attributes. + +TODO: Need to add docstring for method node addition once I have thought about the interface. """ function JUA_Server_addNode(server, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, @@ -147,11 +149,11 @@ Note: Since it is unknown what type of value is stored within `nodeId` before re it, this function is inherently type unstable. Type stability is improved if the optional argument `type` is provided, for example, -if you know that you have stored a Matrix{Float64} in `nodeId`. If the wrong type -is specified, the function will throw a TypeError. +if you know that you have stored a Matrix{Float64} in `nodeId`, then you should +specify this. If the wrong type is specified, the function will throw a TypeError. """ -function JUA_Server_readValue(server, nodeId, type = Any) +function JUA_Server_readValue(server, nodeId, type::T = Any) where {T} v = UA_Variant_new() UA_Server_readValue(server, nodeId, v) r = __get_juliavalues_from_variant(v, type) @@ -165,11 +167,15 @@ JUA_Server_writeValue(server::JUA_Server, nodeId::JUA_NodeId, newvalue)::UA_Stat ``` uses the server API to write the value `newvalue` to `nodeId` on `server`. +`new_value` must either be a `JUA_Variant` or a Julia value/array compatible with +any of its constructors. + +See also [`JUA_Variant`](@ref) """ -function JUA_Server_writeValue(client, nodeId, newvalue) +function JUA_Server_writeValue(server, nodeId, newvalue) newvariant = UA_Variant_new(newvalue) - statuscode = UA_Server_writeValue(client, nodeId, newvariant) + statuscode = UA_Server_writeValue(server, nodeId, newvariant) UA_Variant_delete(newvariant) return statuscode end \ No newline at end of file diff --git a/src/highlevel_types.jl b/src/highlevel_types.jl index 7d4ebbb..2de3470 100644 --- a/src/highlevel_types.jl +++ b/src/highlevel_types.jl @@ -1,5 +1,3 @@ -#TODO: all of this needs docstrings of course. - #Preliminary definitions abstract type AbstractOpen62541Wrapper end @@ -30,10 +28,34 @@ function release_handle(obj::JUA_String) end #Guid +""" +``` +JUA_Guid +``` + +a mutable struct that defines a globally unique identifier. It is the equivalent +of a `UA_Guid`, but with memory managed by Julia rather than C. + +The following constructor methods are defined: + +``` +JUA_Guid() +``` + +creates an empty `JUA_Guid`, equivalent to calling `UA_Guid_new()`. + +``` +JUA_Guid(guidstring::AbstractString) +``` + +creates a `JUA_Guid` by parsing the string `guidstring`. The string should be +formatted according to the OPC standard defined in Part 6, 5.1.3. + +""" mutable struct JUA_Guid <: AbstractOpen62541Wrapper ptr::Ptr{UA_Guid} function JUA_Guid() - obj = new(UA_Guid_random()) + obj = new(UA_Guid_new()) finalizer(release_handle, obj) return obj end @@ -94,6 +116,17 @@ JUA_NodeId(nsIndex::Integer, identifier::JUA_Guid) creates a `JUA_NodeId` with namespace index `nsIndex` and global unique id identifier `identifier`. +``` +JUA_NodeId(nptr::Ptr{UA_NodeId}) +``` + +creates a `JUA_NodeId` based on the pointer `nptr`. This is a fallback +method that can be used to pass `UA_NodeId`s generated via the low level interface +to the higher level functions. Note that memory management remains on the C side +when using this method, i.e., `nptr` needs to be manually cleaned up with +`UA_NodeId_delete(nptr)` after the object is not needed anymore. It is up +to the user to ensure this. + Examples: ``` @@ -113,21 +146,30 @@ mutable struct JUA_NodeId <: AbstractOpen62541Wrapper finalizer(release_handle, obj) return obj end + + function JUA_NodeId(nptr::Ptr{UA_NodeId}) + obj = new(nptr) + return obj + end + function JUA_NodeId(s::Union{AbstractString, JUA_String}) obj = new(UA_NODEID(s)) finalizer(release_handle, obj) return obj end + function JUA_NodeId(nsIndex::Integer, identifier::Integer) obj = new(UA_NODEID_NUMERIC(nsIndex, identifier)) finalizer(release_handle, obj) return obj end + function JUA_NodeId(nsIndex::Integer, identifier::Union{AbstractString, JUA_String}) obj = new(UA_NODEID_STRING_ALLOC(nsIndex, identifier)) finalizer(release_handle, obj) return obj end + function JUA_NodeId(nsIndex::Integer, identifier::JUA_Guid) obj = new(UA_NODEID_GUID(nsIndex, Jpointer(identifier))) finalizer(release_handle, obj) @@ -149,8 +191,40 @@ returns `true` if `j1` and `j2` are `JUA_NodeId`s with identical content. JUA_NodeId_equal(j1, j2) = UA_NodeId_equal(j1, j2) #QualifiedName +""" +``` +JUA_QualifiedName +``` + +A mutable struct that defines a qualified name comprised of a namespace index +and a text portion (a name). It is the equivalent of a `UA_QualifiedName`, but +with memory managed by Julia rather than C. + +The following constructor methods are defined: + +``` +JUA_QualifiedName() +``` + +creates an empty `JUA_QualifiedName`, equivalent to calling `UA_QualifiedName_new()`. + +``` +JUA_QualifiedName(nsIndex::Integer, identifier::AbstractString) +``` + +creates a `JUA_QualifiedName` with namespace index `nsIndex` and text identifier +`identifier`. + +""" mutable struct JUA_QualifiedName <: AbstractOpen62541Wrapper ptr::Ptr{UA_QualifiedName} + + function JUA_QualifiedName() + obj = new(UA_QualifiedName_new()) + finalizer(release_handle, obj) + return obj + end + function JUA_QualifiedName(nsIndex::Integer, identifier::AbstractString) obj = new(UA_QUALIFIEDNAME_ALLOC(nsIndex, identifier)) finalizer(release_handle, obj) @@ -170,45 +244,37 @@ Base.convert(::Type{UA_QualifiedName}, x::JUA_QualifiedName) = unsafe_load(Jpoin JUA_Variant ``` -creates a `JUA_Variant` object - the equivalent of a `UA_Variant`, but with memory -managed by Julia rather than C (exceptions below). +A mutable struct that defines a `JUA_Variant` object - the equivalent of a +`UA_Variant`, but with memory managed by Julia rather than C (exceptions below). +`JUA_Variant`s can hold any datatype either as a scalar or in array form. -The following methods are defined: +The following constructor methods are defined: ``` JUA_Variant() ``` -creates an empty `JUA_Variant`, equivalent to calling `UA_Variant_new()`, but -with memory managed by Julia. +creates an empty `JUA_Variant`, equivalent to calling `UA_Variant_new()`. ``` JUA_Variant(value::Union{T, AbstractArray{T}}) where T <: Union{UA_NUMBER_TYPES, AbstractString, ComplexF32, ComplexF64}) ``` -creates a `JUA_Variant` containing the based on String `s` that is parsed into the relevant -properties. - -``` -JUA_NodeId(nsIndex::Integer, identifier::Integer) -``` - -creates a `JUA_NodeId` with namespace index `nsIndex` and numerical identifier -`identifier`. - -``` -JUA_NodeId(nsIndex::Integer, identifier::Union{AbstractString, JUA_String}) -``` - -creates a `JUA_NodeId` with namespace index `nsIndex` and string identifier -`identifier`. +creates a `JUA_Variant` containing `value`. All properties of the variant are set +automatically. For example, if `value` is an array, then the arrayDimensions and +arrayDimensionsSize properties are set based on the number of dimensions and +number of elements across each dimension contained in `value`. ``` -JUA_NodeId(nsIndex::Integer, identifier::JUA_Guid) +JUA_Variant(variantptr::Ptr{UA_Variant}) ``` -creates a `JUA_NodeId` with namespace index `nsIndex` and global unique id identifier -`identifier`. +creates a `JUA_Variant` based on the pointer `variantptr`. This is a fallback +method that can be used to pass `UA_Variant`s generated via the low level interface +to the higher level functions. Note that memory management remains on the C side +when using this method, i.e., `variantptr` needs to be manually cleaned up with +`UA_Variant_delete(variantptr)` after the object is not needed anymore. It is up +to the user to ensure this. Examples: @@ -230,6 +296,11 @@ mutable struct JUA_Variant <: AbstractOpen62541Wrapper return obj end + function JUA_Variant(variantptr::Ptr{UA_Variant}) + obj = new(variantptr) + return obj + end + function JUA_Variant(value::AbstractArray{T, N}, type_ptr::Ptr{UA_DataType} = ua_data_type_ptr_default(T)) where { T <: Union{UA_NUMBER_TYPES, UA_String, UA_ComplexNumberType, UA_DoubleComplexNumberType}, N} @@ -294,13 +365,59 @@ function release_handle(obj::JUA_Variant) end #VariableAttributes +""" +``` +JUA_VariableAttributes +``` + +A mutable struct that defines a `JUA_VariableAttributes` object - the equivalent +of a `UA_VariableAttributes`, but with memory managed by Julia rather than C (see +below for exceptions) + +The following constructor methods are defined: + +``` +JUA_VariableAttributes(; kwargs...) +``` + +For valid keyword arguments `kwargs` see [`UA_VariableAttributes_generate`](@ref). + +``` +JUA_VariableAttributes(ptr:Ptr{UA_VariableAttributes}) +``` + +creates a `JUA_VariableAttributes` based on the pointer `ptr`. This is a +fallback method that can be used to pass `UA_VariableAttributes`s generated via +the low level interface to the higher level functions. See also [`UA_VariableAttributes_generate`](@ref). + +Note that memory management remains on the C side when using this method, i.e., +`ptr` needs to be manually cleaned up with `UA_VariableAttributes_delete(ptr)` +after the object is not needed anymore. It is up to the user to ensure this. + +Examples: + +``` +j = JUA_VariableAttributes(value = "Hello", displayname = "String variable", + description = "This is a variable with a string in it.") +ptr = UA_VariableAttributes_generate(value = "Hello", displayname = "String variable", + description = "This is a variable with a string in it.") +j = JUA_VariableAttributes(ptr) +(... do something with j ...) +UA_VariableAttributes_delete(ptr) +``` +""" mutable struct JUA_VariableAttributes <: AbstractOpen62541Wrapper ptr::Ptr{UA_VariableAttributes} + function JUA_VariableAttributes(; kwargs...) obj = new(UA_VariableAttributes_generate(; kwargs...)) finalizer(release_handle, obj) return obj end + + function JUA_VariableAttributes(ptr::Ptr{UA_VariableAttributes}) + return new(ptr) #no finalizer, see docstring + end end function release_handle(obj::JUA_VariableAttributes) @@ -308,13 +425,59 @@ function release_handle(obj::JUA_VariableAttributes) end #VariableTypeAttributes +""" +``` +JUA_VariableTypeAttributes +``` + +A mutable struct that defines a `JUA_VariableTypeAttributes` object - the equivalent +of a `UA_VariableTypeAttributes`, but with memory managed by Julia rather than C (see +below for exceptions) + +The following constructor methods are defined: + +``` +JUA_VariableTypeAttributes(; kwargs...) +``` + +For valid keyword arguments `kwargs` see [`UA_VariableTypeAttributes_generate`](@ref). + +``` +JUA_VariableTypeAttributes(ptr::Ptr{UA_VariableTypeAttributes}) +``` + +creates a `JUA_VariableTypeAttributes` based on the pointer `ptr`. +This is a fallback method that can be used to pass `UA_VariableAttributes`s +generated via the low level interface to the higher level functions. See also [`UA_VariableAttributes_generate`](@ref). + +Note that memory management remains on the C side when using this method, i.e., +`ptr` needs to be manually cleaned up with `UA_VariableTypeAttributes_delete(ptr)` +after the object is not needed anymore. It is up to the user to ensure this. + +Examples: + +``` +j = JUA_VariableTypeAttributes(value = [0.0, 0.0], displayname = "2D point type", + description = "This is a variable type representing a 2D point.") +ptr = UA_VariableTypeAttributes_generate(value = [0.0, 0.0], displayname = "2D point type", + description = "This is a variable type representing a 2D point.") +j = JUA_VariableTypeAttributes(ptr) +(... do something with j ...) +UA_VariableTypeAttributes_delete(ptr) +``` +""" mutable struct JUA_VariableTypeAttributes <: AbstractOpen62541Wrapper ptr::Ptr{UA_VariableTypeAttributes} + function JUA_VariableTypeAttributes(; kwargs...) obj = new(UA_VariableTypeAttributes_generate(; kwargs...)) finalizer(release_handle, obj) return obj end + + function JUA_VariableTypeAttributes(ptr::Ptr{UA_VariableTypeAttributes}) + return new(ptr) #no finalizer, see docstring + end end function release_handle(obj::JUA_VariableTypeAttributes) @@ -322,13 +485,59 @@ function release_handle(obj::JUA_VariableTypeAttributes) end #ObjectAttributes +""" +``` +JUA_ObjectAttributes +``` + +A mutable struct that defines a `JUA_ObjectAttributes` object - the equivalent +of a `UA_ObjectAttributes`, but with memory managed by Julia rather than C (see +below for exceptions) + +The following constructor methods are defined: + +``` +JUA_ObjectAttributes(; kwargs...) +``` + +For valid keyword arguments `kwargs` see [`UA_ObjectAttributes_generate`](@ref). + +``` +JUA_ObjectAttributes(ptr::Ptr{UA_ObjectAttributes}) +``` + +creates a `JUA_ObjectAttributes` based on the pointer `objattrptr`. +This is a fallback method that can be used to pass `UA_ObjectAttributes`s +generated via the low level interface to the higher level functions. See also [`UA_ObjectAttributes_generate`](@ref). + +Note that memory management remains on the C side when using this method, i.e., +`ptr` needs to be manually cleaned up with `UA_ObjectAttributes_delete(ptr)` +after the object is not needed anymore. It is up to the user to ensure this. + +Examples: + +``` +j = JUA_ObjectAttributes(displayname = "Pump 1", + description = "This is the first pump.") +ptr = UA_ObjectAttributes_generate(displayname = "Pump 1", +description = "This is the first pump.") +j = JUA_ObjectAttributes(ptr) +(... do something with j ...) +UA_ObjectAttributes_delete(ptr) +``` +""" mutable struct JUA_ObjectAttributes <: AbstractOpen62541Wrapper ptr::Ptr{UA_ObjectAttributes} + function JUA_ObjectAttributes(; kwargs...) obj = new(UA_ObjectAttributes_generate(; kwargs...)) finalizer(release_handle, obj) return obj end + + function JUA_ObjectAttributes(ptr::Ptr{UA_ObjectAttributes}) + return new(ptr) #no finalizer, see docstring + end end function release_handle(obj::JUA_ObjectAttributes) @@ -336,13 +545,59 @@ function release_handle(obj::JUA_ObjectAttributes) end #ObjectTypeAttributes +""" +``` +JUA_ObjectTypeAttributes +``` + +A mutable struct that defines a `JUA_ObjectTypeAttributes` object - the equivalent +of a `UA_ObjectTypeAttributes`, but with memory managed by Julia rather than C (see +below for exceptions) + +The following constructor methods are defined: + +``` +JUA_ObjectTypeAttributes(; kwargs...) +``` + +For valid keyword arguments `kwargs` see [`UA_ObjectTypeAttributes_generate`](@ref). + +``` +JUA_ObjectTypeAttributes(ptr::Ptr{UA_ObjectTypeAttributes}) +``` + +creates a `JUA_ObjectTypeAttributes` based on the pointer `ptr`. This is a +fallback method that can be used to pass `UA_ObjectTypeAttributes`s generated via +the low level interface to the higher level functions. See also [`UA_ObjectTypeAttributes_generate`](@ref). + +Note that memory management remains on the C side when using this method, i.e., +`ptr` needs to be manually cleaned up with `UA_ObjectTypeAttributes_delete(ptr)` +after the object is not needed anymore. It is up to the user to ensure this. + +Examples: + +``` +j = JUA_ObjectTypeAttributes(displayname = "Pump Type", +description = "This is an object type for a pump.") +varattrptr = UA_ObjectTypeAttributes_generate(displayname = "Pump Type", + description = "This is an object type for a pump.") +j = JUA_ObjectTypeAttributes(ptr) +(... do something with j ...) +UA_ObjectTypeAttributes_delete(ptr) +``` +""" mutable struct JUA_ObjectTypeAttributes <: AbstractOpen62541Wrapper ptr::Ptr{UA_ObjectTypeAttributes} + function JUA_ObjectTypeAttributes(; kwargs...) obj = new(UA_ObjectTypeAttributes_generate(; kwargs...)) finalizer(release_handle, obj) return obj end + + function JUA_ObjectTypeAttributes(ptr::Ptr{UA_ObjectTypeAttributes}) + return new(ptr) #no finalizer, see docstring + end end function release_handle(obj::JUA_ObjectTypeAttributes) @@ -350,13 +605,60 @@ function release_handle(obj::JUA_ObjectTypeAttributes) end #ReferenceTypeAttributes +""" +``` +JUA_ReferenceTypeAttributes +``` + +A mutable struct that defines a `JUA_ReferenceTypeAttributes` object - the equivalent +of a `UA_ReferenceTypeAttributes`, but with memory managed by Julia rather than C (see +below for exceptions) + +The following constructor methods are defined: + +``` +JUA_ReferenceTypeAttributes(; kwargs...) +``` + +For valid keyword arguments `kwargs` see [`UA_ReferenceTypeAttributes_generate`](@ref). + +``` +JUA_ReferenceTypeAttributes(ptr::Ptr{UA_ReferenceTypeAttributes}) +``` + +creates a `JUA_ReferenceTypeAttributes` based on the pointer `ptr`. +This is a fallback method that can be used to pass `UA_ReferenceTypeAttributes`s +generated via the low level interface to the higher level functions. See also [`UA_ReferenceTypeAttributes_generate`](@ref). + +Note that memory management remains on the C side when using this method, i.e., +`ptr` needs to be manually cleaned up with +`UA_ReferenceTypeAttributes_delete(ptr)` after the object is not +needed anymore. It is up to the user to ensure this. + +Examples: + +``` +j = JUA_ReferenceTypeAttributes(displayname = "My reference type", + description = "This is my reference type definining a relationship between nodes.") +varattrptr = UA_ReferenceTypeAttributes_generate(displayname = "My reference type", +description = "This is my reference type definining a relationship between nodes.") +j = JUA_ReferenceTypeAttributes(ptr) +(... do something with j ...) +UA_ReferenceTypeAttributes_delete(ptr) +``` +""" mutable struct JUA_ReferenceTypeAttributes <: AbstractOpen62541Wrapper ptr::Ptr{UA_ReferenceTypeAttributes} + function JUA_ReferenceTypeAttributes(; kwargs...) obj = new(UA_ReferenceTypeAttributes_generate(; kwargs...)) finalizer(release_handle, obj) return obj end + + function JUA_ReferenceTypeAttributes(ptr::Ptr{UA_ReferenceTypeAttributes}) + return new(ptr) #no finalizer, see docstring + end end function release_handle(obj::JUA_ReferenceTypeAttributes) @@ -364,13 +666,60 @@ function release_handle(obj::JUA_ReferenceTypeAttributes) end #DataTypeAttributes +""" +``` +JUA_DataTypeAttributes +``` + +A mutable struct that defines a `JUA_DataTypeAttributes` object - the equivalent +of a `UA_DataTypeAttributes`, but with memory managed by Julia rather than C (see +below for exceptions) + +The following constructor methods are defined: + +``` +JUA_DataTypeAttributes(; kwargs...) +``` + +For valid keyword arguments `kwargs` see [`UA_DataTypeAttributes_generate`](@ref). + +``` +JUA_DataTypeAttributes(ptr::Ptr{UA_DataTypeAttributes}) +``` + +creates a `JUA_DataTypeAttributes` based on the pointer `ptr`. +This is a fallback method that can be used to pass `UA_VariableAttributes`s +generated via the low level interface to the higher level functions. See also [`UA_VariableAttributes_generate`](@ref). + +Note that memory management remains on the C side when using this method, i.e., +`ptr` needs to be manually cleaned up with +`UA_DataTypeAttributes_delete(ptr)` after the object is not +needed anymore. It is up to the user to ensure this. + +Examples: + +``` +j = JUA_DataTypeAttributes(displayname = "my special data type", + description = "This is my special data type with some properties.") +ptr = UA_DataTypeAttributes_generate(displayname = "my special data type", +description = "This is my special data type with some properties.") +j = JUA_DataTypeAttributes(ptr) +(... do something with j ...) +UA_DataTypeAttributes_delete(ptr) +``` +""" mutable struct JUA_DataTypeAttributes <: AbstractOpen62541Wrapper ptr::Ptr{UA_DataTypeAttributes} + function JUA_DataTypeAttributes(; kwargs...) obj = new(UA_DataTypeAttributes_generate(; kwargs...)) finalizer(release_handle, obj) return obj end + + function JUA_DataTypeAttributes(ptr::Ptr{UA_DataTypeAttributes}) + return new(ptr) #no finalizer, see docstring + end end function release_handle(obj::JUA_DataTypeAttributes) @@ -378,13 +727,59 @@ function release_handle(obj::JUA_DataTypeAttributes) end #ViewAttributes +""" +``` +JUA_ViewAttributes +``` + +A mutable struct that defines a `JUA_ViewAttributes` object - the equivalent +of a `UA_ViewAttributes`, but with memory managed by Julia rather than C (see +below for exceptions) + +The following constructor methods are defined: + +``` +JUA_ViewAttributes(; kwargs...) +``` + +For valid keyword arguments `kwargs` see [`UA_ViewAttributes_generate`](@ref). + +``` +JUA_ViewAttributes(ptr::Ptr{UA_ViewAttributes}) +``` + +creates a `JUA_ViewAttributes` based on the pointer `ptr`. +This is a fallback method that can be used to pass `UA_VariableAttributes`s +generated via the low level interface to the higher level functions. See also [`UA_VariableAttributes_generate`](@ref). + +Note that memory management remains on the C side when using this method, i.e., +`ptr` needs to be manually cleaned up with `UA_ViewAttributes_delete(ptr)` after +the object is not needed anymore. It is up to the user to ensure this. + +Examples: + +``` +j = JUA_ViewAttributes(displayname = "View 1", + description = "This is a view showing only certain nodes.") +varattrptr = UA_ViewAttributes_generate(displayname = "View 1", +description = "This is a view showing only certain nodes.") +j = JUA_ViewAttributes(ptr) +(... do something with j ...) +UA_ViewAttributes_delete(ptr) +``` +""" mutable struct JUA_ViewAttributes <: AbstractOpen62541Wrapper ptr::Ptr{UA_ViewAttributes} + function JUA_ViewAttributes(; kwargs...) obj = new(UA_ViewAttributes_generate(; kwargs...)) finalizer(release_handle, obj) return obj end + + function JUA_ViewAttributes(ptr::Ptr{UA_ViewAttributes}) + return new(ptr) #no finalizer, see docstring + end end function release_handle(obj::JUA_ViewAttributes) diff --git a/src/server.jl b/src/server.jl index 0633bfb..d38a169 100644 --- a/src/server.jl +++ b/src/server.jl @@ -183,10 +183,6 @@ for att in attributes_UA_Server_write ``` Uses the Server API to write the value `new_val` to the attribute $($(String(attr_name))) of the NodeId `nodeId` located on the `server`. - - Note that memory for `new_val` is allocated by C before using this function - and needs to be cleaned up via `$($(String(att[3])))_delete(new_val::Ptr{$($(String(att[3])))})` - after its use. """ function $(fun_name)(server, nodeId, new_val) data_type_ptr = UA_TYPES_PTRS[$(attr_type_ptr)] diff --git a/src/types.jl b/src/types.jl index e782003..1f14737 100644 --- a/src/types.jl +++ b/src/types.jl @@ -377,7 +377,7 @@ UA_NODEID(s::AbstractString)::Ptr{UA_NodeId} UA_NODEID(s::Ptr{UA_String})::Ptr{UA_NodeId} ``` -creates a `UA_NodeId` object by parsing `s` (which can be a `String` or `Ptr{UA_String}`). +creates a `UA_NodeId` object by parsing `s`. Example: @@ -406,7 +406,8 @@ UA_NODEID_NUMERIC(nsIndex::Integer, identifier::Integer)::Ptr{UA_NodeId} ``` creates a `UA_NodeId` object with namespace index `nsIndex` and numerical identifier `identifier`. -Memory is allocated by C and needs to be cleaned up using `UA_NodeId_delete(x::Ptr{UA_NodeId})` after the object is not used anymore. +Memory is allocated by C and needs to be cleaned up using `UA_NodeId_delete(x::Ptr{UA_NodeId})` +after the object is not used anymore. """ function UA_NODEID_NUMERIC(nsIndex::Integer, identifier::Integer) nodeid = UA_NodeId_new() @@ -806,7 +807,7 @@ end Base.unsafe_wrap(p::Ptr{UA_Variant}) = unsafe_wrap(unsafe_load(p)) UA_Variant_isEmpty(v::UA_Variant) = v.type == C_NULL UA_Variant_isEmpty(p::Ptr{UA_Variant}) = UA_Variant_isEmpty(unsafe_load(p)) -UA_Variant_isScalar(v::UA_Variant) = v.arrayLength == 0 #TODO: this fails when strings are present in the variant, why? && v.data > UA_EMPTY_ARRAY_SENTINEL +UA_Variant_isScalar(v::UA_Variant) = v.arrayLength == 0 && v.data > UA_EMPTY_ARRAY_SENTINEL UA_Variant_isScalar(p::Ptr{UA_Variant}) = UA_Variant_isScalar(unsafe_load(p)) function UA_Variant_hasScalarType(v::UA_Variant, type::Ref{UA_DataType}) diff --git a/test/server_add_nodes_highlevelinterface.jl b/test/server_add_nodes_highlevelinterface.jl index 5f54219..742ea7b 100644 --- a/test/server_add_nodes_highlevelinterface.jl +++ b/test/server_add_nodes_highlevelinterface.jl @@ -1,8 +1,6 @@ # Simple checks whether addition of different node types was successful or not # Closely follows https://www.open62541.org/doc/1.3/tutorial_server_variabletype.html -#TODO: need to clean up in terms of memory management. - using open62541 using Test @@ -187,180 +185,180 @@ retval8 = JUA_Server_addNode(server, requestednewnodeid, nodecontext, deviceTypeId) @test retval8 == UA_STATUSCODE_GOOD -## TODO: Revise rest of the code from here. - #add manufacturer name to device -mnAttr = UA_VariableAttributes_generate(value = "", +mnAttr = JUA_VariableAttributes(value = "", displayname = "ManufacturerName", description = "Name of the manufacturer") -manufacturerNameId = UA_NodeId_new() -retval8 = UA_Server_addVariableNode(server, UA_NodeId_new(), deviceTypeId, - UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), - UA_QUALIFIEDNAME(1, "ManufacturerName"), - UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), mnAttr, C_NULL, manufacturerNameId); -@test retval8 == UA_STATUSCODE_GOOD - -#Make the manufacturer name mandatory -retval9 = UA_Server_addReference(server, manufacturerNameId, - UA_NODEID_NUMERIC(0, UA_NS0ID_HASMODELLINGRULE), - UA_EXPANDEDNODEID_NUMERIC(0, UA_NS0ID_MODELLINGRULE_MANDATORY), true) +manufacturerNameId = JUA_NodeId() +requestedNewNodeid = JUA_NodeId() +referenceTypeId = JUA_NodeId(0, UA_NS0ID_HASCOMPONENT) +browseName = JUA_QualifiedName(1, "ManufacturerName") +typeDefinition = JUA_NodeId(0, UA_NS0ID_BASEDATAVARIABLETYPE) +nodeContext = JUA_NodeId() +retval9 = JUA_Server_addNode(server, requestedNewNodeid, deviceTypeId, + referenceTypeId, browseName, mnAttr, nodeContext, manufacturerNameId, typeDefinition) @test retval9 == UA_STATUSCODE_GOOD -#Add model name -modelAttr = UA_VariableAttributes_generate(value = "", - displayname = "ModelName", - description = "Name of the model") -retval10 = UA_Server_addVariableNode(server, UA_NodeId_new(), deviceTypeId, - UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), - UA_QUALIFIEDNAME(1, "ModelName"), - UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), modelAttr, C_NULL, C_NULL); -@test retval10 == UA_STATUSCODE_GOOD - -#Define the object type for "Pump" -ptAttr = UA_ObjectTypeAttributes_generate(displayname = "PumpType", - description = "Object type for a pump") -retval11 = UA_Server_addObjectTypeNode(server, pumpTypeId, - deviceTypeId, UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE), - UA_QUALIFIEDNAME(1, "PumpType"), ptAttr, - C_NULL, C_NULL) -@test retval11 == UA_STATUSCODE_GOOD - -statusAttr = UA_VariableAttributes_generate(value = false, - displayname = "Status", - description = "Status") -statusId = UA_NodeId_new() -retval12 = UA_Server_addVariableNode(server, UA_NodeId_new(), pumpTypeId, - UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), - UA_QUALIFIEDNAME(1, "Status"), - UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), statusAttr, C_NULL, statusId) -@test retval12 == UA_STATUSCODE_GOOD - -#/* Make the status variable mandatory */ -retval13 = UA_Server_addReference(server, statusId, - UA_NODEID_NUMERIC(0, UA_NS0ID_HASMODELLINGRULE), - UA_EXPANDEDNODEID_NUMERIC(0, UA_NS0ID_MODELLINGRULE_MANDATORY), true) -@test retval13 == UA_STATUSCODE_GOOD - -rpmAttr = UA_VariableAttributes_generate(displayname = "MotorRPM", - description = "Pump speed in rpm", - value = 0) -retval14 = UA_Server_addVariableNode(server, UA_NodeId_new(), pumpTypeId, - UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), - UA_QUALIFIEDNAME(1, "MotorRPMs"), - UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), rpmAttr, C_NULL, C_NULL) -@test retval14 == UA_STATUSCODE_GOOD - -function addPumpObjectInstance(server, name) - oAttr = UA_ObjectAttributes_generate(displayname = name, description = name) - UA_Server_addObjectNode(server, UA_NodeId_new(), - UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER), - UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), - UA_QUALIFIEDNAME(1, name), - pumpTypeId, #/* this refers to the object type - # identifier */ - oAttr, C_NULL, C_NULL) -end - -function pumpTypeConstructor(server, sessionId, sessionContext, - typeId, typeContext, nodeId, nodeContext) - #UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "New pump created"); - - #/* Find the NodeId of the status child variable */ - rpe = UA_RelativePathElement_new() - UA_RelativePathElement_init(rpe) - rpe.referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT) - rpe.isInverse = false - rpe.includeSubtypes = false - rpe.targetName = UA_QUALIFIEDNAME(1, "Status") - - bp = UA_BrowsePath_new() - UA_BrowsePath_init(bp) - bp.startingNode = nodeId - bp.relativePath.elementsSize = 1 - bp.relativePath.elements = rpe - - bpr = UA_Server_translateBrowsePathToNodeIds(server, bp) - if bpr.statusCode != UA_STATUSCODE_GOOD || bpr.targetsSize < 1 - return bpr.statusCode - end - - #Set the status value - status = true - value = UA_Variant_new() - UA_Variant_setScalarCopy(value, Ref(status), UA_TYPES_PTRS[UA_TYPES_BOOLEAN]) - UA_Server_writeValue(server, bpr.targets.targetId.nodeId, value) - - #TODO: clean up to avoid memory leaks - return UA_STATUSCODE_GOOD -end - -if !Sys.isapple() - function addPumpTypeConstructor(server) - c_pumpTypeConstructor = UA_NodeTypeLifecycleCallback_constructor_generate(pumpTypeConstructor) - lifecycle = UA_NodeTypeLifecycle(c_pumpTypeConstructor, C_NULL) - UA_Server_setNodeTypeLifecycle(server, open62541.Jpointer(pumpTypeId), lifecycle) #TODO: probably this needs a high level method as well / - end - - addPumpObjectInstance(server, "pump2") #should have status = false (constructor not in place yet) - addPumpObjectInstance(server, "pump3") #should have status = false (constructor not in place yet) - addPumpTypeConstructor(server) - addPumpObjectInstance(server, "pump4") #should have status = true - addPumpObjectInstance(server, "pump5") #should have status = true - - #add method node - #follows this: https://www.open62541.org/doc/1.3/tutorial_server_method.html - - function helloWorldMethodCallback(server, sessionId, sessionHandle, methodId, - methodContext, objectId, objectContext, inputSize, input, outputSize, output) - inputstr = unsafe_string(unsafe_wrap(input)) - tmp = UA_STRING("Hello " * inputstr) - UA_Variant_setScalarCopy(output, tmp, UA_TYPES_PTRS[UA_TYPES_STRING]) - UA_String_delete(tmp) - return UA_STATUSCODE_GOOD - end - - inputArgument = UA_Argument_new() - inputArgument.description = UA_LOCALIZEDTEXT("en-US", "A String") - inputArgument.name = UA_STRING("MyInput"); - inputArgument.dataType = UA_TYPES_PTRS[UA_TYPES_STRING].typeId; - inputArgument.valueRank = UA_VALUERANK_SCALAR - outputArgument = UA_Argument_new() - outputArgument.description = UA_LOCALIZEDTEXT("en-US", "A String"); - outputArgument.name = UA_STRING("MyOutput"); - outputArgument.dataType = UA_TYPES_PTRS[UA_TYPES_STRING].typeId - outputArgument.valueRank = UA_VALUERANK_SCALAR - helloAttr = UA_MethodAttributes_generate(description = "Say Hello World", - displayname = "Hello World", - executable = true, - userexecutable = true) - - methodid = UA_NODEID_NUMERIC(1, 62541) - obj = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER) - retval = UA_Server_addMethodNode(server, methodid, - obj, - UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), - UA_QUALIFIEDNAME(1, "hello world"), - helloAttr, helloWorldMethodCallback, - 1, inputArgument, 1, outputArgument, C_NULL, C_NULL) - - @test retval == UA_STATUSCODE_GOOD - - inputArguments = UA_Variant_new() - ua_s = UA_STRING("Peter") - UA_Variant_setScalar(inputArguments, ua_s, UA_TYPES_PTRS[UA_TYPES_STRING]) - req = UA_CallMethodRequest_new() - req.objectId = obj - req.methodId = methodid - req.inputArgumentsSize = 1 - req.inputArguments = inputArguments - - answer = UA_CallMethodResult_new() - UA_Server_call(server, req, answer) - @test unsafe_load(answer.statusCode) == UA_STATUSCODE_GOOD - @test unsafe_string(unsafe_wrap(unsafe_load(answer.outputArguments))) == "Hello Peter" - - UA_CallMethodRequest_delete(req) - UA_CallMethodResult_delete(answer) -end - -#TODO: this will need a test to see whether any memory is leaking anywhere +## TODO: Revise rest of the code from here. +# #Make the manufacturer name mandatory +# retval9 = UA_Server_addReference(server, manufacturerNameId, +# UA_NODEID_NUMERIC(0, UA_NS0ID_HASMODELLINGRULE), +# UA_EXPANDEDNODEID_NUMERIC(0, UA_NS0ID_MODELLINGRULE_MANDATORY), true) +# @test retval9 == UA_STATUSCODE_GOOD + +# #Add model name +# modelAttr = UA_VariableAttributes_generate(value = "", +# displayname = "ModelName", +# description = "Name of the model") +# retval10 = UA_Server_addVariableNode(server, UA_NodeId_new(), deviceTypeId, +# UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), +# UA_QUALIFIEDNAME(1, "ModelName"), +# UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), modelAttr, C_NULL, C_NULL); +# @test retval10 == UA_STATUSCODE_GOOD + +# #Define the object type for "Pump" +# ptAttr = UA_ObjectTypeAttributes_generate(displayname = "PumpType", +# description = "Object type for a pump") +# retval11 = UA_Server_addObjectTypeNode(server, pumpTypeId, +# deviceTypeId, UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE), +# UA_QUALIFIEDNAME(1, "PumpType"), ptAttr, +# C_NULL, C_NULL) +# @test retval11 == UA_STATUSCODE_GOOD + +# statusAttr = UA_VariableAttributes_generate(value = false, +# displayname = "Status", +# description = "Status") +# statusId = UA_NodeId_new() +# retval12 = UA_Server_addVariableNode(server, UA_NodeId_new(), pumpTypeId, +# UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), +# UA_QUALIFIEDNAME(1, "Status"), +# UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), statusAttr, C_NULL, statusId) +# @test retval12 == UA_STATUSCODE_GOOD + +# #/* Make the status variable mandatory */ +# retval13 = UA_Server_addReference(server, statusId, +# UA_NODEID_NUMERIC(0, UA_NS0ID_HASMODELLINGRULE), +# UA_EXPANDEDNODEID_NUMERIC(0, UA_NS0ID_MODELLINGRULE_MANDATORY), true) +# @test retval13 == UA_STATUSCODE_GOOD + +# rpmAttr = UA_VariableAttributes_generate(displayname = "MotorRPM", +# description = "Pump speed in rpm", +# value = 0) +# retval14 = UA_Server_addVariableNode(server, UA_NodeId_new(), pumpTypeId, +# UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), +# UA_QUALIFIEDNAME(1, "MotorRPMs"), +# UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), rpmAttr, C_NULL, C_NULL) +# @test retval14 == UA_STATUSCODE_GOOD + +# function addPumpObjectInstance(server, name) +# oAttr = UA_ObjectAttributes_generate(displayname = name, description = name) +# UA_Server_addObjectNode(server, UA_NodeId_new(), +# UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER), +# UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), +# UA_QUALIFIEDNAME(1, name), +# pumpTypeId, #/* this refers to the object type +# # identifier */ +# oAttr, C_NULL, C_NULL) +# end + +# function pumpTypeConstructor(server, sessionId, sessionContext, +# typeId, typeContext, nodeId, nodeContext) +# #UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "New pump created"); + +# #/* Find the NodeId of the status child variable */ +# rpe = UA_RelativePathElement_new() +# UA_RelativePathElement_init(rpe) +# rpe.referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT) +# rpe.isInverse = false +# rpe.includeSubtypes = false +# rpe.targetName = UA_QUALIFIEDNAME(1, "Status") + +# bp = UA_BrowsePath_new() +# UA_BrowsePath_init(bp) +# bp.startingNode = nodeId +# bp.relativePath.elementsSize = 1 +# bp.relativePath.elements = rpe + +# bpr = UA_Server_translateBrowsePathToNodeIds(server, bp) +# if bpr.statusCode != UA_STATUSCODE_GOOD || bpr.targetsSize < 1 +# return bpr.statusCode +# end + +# #Set the status value +# status = true +# value = UA_Variant_new() +# UA_Variant_setScalarCopy(value, Ref(status), UA_TYPES_PTRS[UA_TYPES_BOOLEAN]) +# UA_Server_writeValue(server, bpr.targets.targetId.nodeId, value) + +# #TODO: clean up to avoid memory leaks +# return UA_STATUSCODE_GOOD +# end + +# if !Sys.isapple() +# function addPumpTypeConstructor(server) +# c_pumpTypeConstructor = UA_NodeTypeLifecycleCallback_constructor_generate(pumpTypeConstructor) +# lifecycle = UA_NodeTypeLifecycle(c_pumpTypeConstructor, C_NULL) +# UA_Server_setNodeTypeLifecycle(server, open62541.Jpointer(pumpTypeId), lifecycle) #TODO: probably this needs a high level method as well / +# end + +# addPumpObjectInstance(server, "pump2") #should have status = false (constructor not in place yet) +# addPumpObjectInstance(server, "pump3") #should have status = false (constructor not in place yet) +# addPumpTypeConstructor(server) +# addPumpObjectInstance(server, "pump4") #should have status = true +# addPumpObjectInstance(server, "pump5") #should have status = true + +# #add method node +# #follows this: https://www.open62541.org/doc/1.3/tutorial_server_method.html + +# function helloWorldMethodCallback(server, sessionId, sessionHandle, methodId, +# methodContext, objectId, objectContext, inputSize, input, outputSize, output) +# inputstr = unsafe_string(unsafe_wrap(input)) +# tmp = UA_STRING("Hello " * inputstr) +# UA_Variant_setScalarCopy(output, tmp, UA_TYPES_PTRS[UA_TYPES_STRING]) +# UA_String_delete(tmp) +# return UA_STATUSCODE_GOOD +# end + +# inputArgument = UA_Argument_new() +# inputArgument.description = UA_LOCALIZEDTEXT("en-US", "A String") +# inputArgument.name = UA_STRING("MyInput"); +# inputArgument.dataType = UA_TYPES_PTRS[UA_TYPES_STRING].typeId; +# inputArgument.valueRank = UA_VALUERANK_SCALAR +# outputArgument = UA_Argument_new() +# outputArgument.description = UA_LOCALIZEDTEXT("en-US", "A String"); +# outputArgument.name = UA_STRING("MyOutput"); +# outputArgument.dataType = UA_TYPES_PTRS[UA_TYPES_STRING].typeId +# outputArgument.valueRank = UA_VALUERANK_SCALAR +# helloAttr = UA_MethodAttributes_generate(description = "Say Hello World", +# displayname = "Hello World", +# executable = true, +# userexecutable = true) + +# methodid = UA_NODEID_NUMERIC(1, 62541) +# obj = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER) +# retval = UA_Server_addMethodNode(server, methodid, +# obj, +# UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), +# UA_QUALIFIEDNAME(1, "hello world"), +# helloAttr, helloWorldMethodCallback, +# 1, inputArgument, 1, outputArgument, C_NULL, C_NULL) + +# @test retval == UA_STATUSCODE_GOOD + +# inputArguments = UA_Variant_new() +# ua_s = UA_STRING("Peter") +# UA_Variant_setScalar(inputArguments, ua_s, UA_TYPES_PTRS[UA_TYPES_STRING]) +# req = UA_CallMethodRequest_new() +# req.objectId = obj +# req.methodId = methodid +# req.inputArgumentsSize = 1 +# req.inputArguments = inputArguments + +# answer = UA_CallMethodResult_new() +# UA_Server_call(server, req, answer) +# @test unsafe_load(answer.statusCode) == UA_STATUSCODE_GOOD +# @test unsafe_string(unsafe_wrap(unsafe_load(answer.outputArguments))) == "Hello Peter" + +# UA_CallMethodRequest_delete(req) +# UA_CallMethodResult_delete(answer) +# end \ No newline at end of file From 8f3ba6d837dea002526a359bb874055f67023fa5 Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Tue, 21 May 2024 21:41:54 +0200 Subject: [PATCH 20/26] fix typo; add methodattributes to high level --- src/highlevel_client.jl | 4 -- src/highlevel_types.jl | 132 +++++++++++++++------------------------- 2 files changed, 48 insertions(+), 88 deletions(-) diff --git a/src/highlevel_client.jl b/src/highlevel_client.jl index 35acd2a..4cbf366 100644 --- a/src/highlevel_client.jl +++ b/src/highlevel_client.jl @@ -28,10 +28,6 @@ const JUA_Client_connect = UA_Client_connect const JUA_Client_disconnect = UA_Client_disconnect #Add node functions -JUA_Client_addNode(client, requestedNewNodeId, - parentNodeId, referenceTypeId, browseName, - attributes::$(attributetype_sym_J), - outNewNodeId, typeDefinition) """ ``` JUA_Client_addNode(client::JUA_Client, requestedNewNodeId::JUA_NodeId, diff --git a/src/highlevel_types.jl b/src/highlevel_types.jl index 2de3470..434719a 100644 --- a/src/highlevel_types.jl +++ b/src/highlevel_types.jl @@ -393,18 +393,6 @@ the low level interface to the higher level functions. See also [`UA_VariableAtt Note that memory management remains on the C side when using this method, i.e., `ptr` needs to be manually cleaned up with `UA_VariableAttributes_delete(ptr)` after the object is not needed anymore. It is up to the user to ensure this. - -Examples: - -``` -j = JUA_VariableAttributes(value = "Hello", displayname = "String variable", - description = "This is a variable with a string in it.") -ptr = UA_VariableAttributes_generate(value = "Hello", displayname = "String variable", - description = "This is a variable with a string in it.") -j = JUA_VariableAttributes(ptr) -(... do something with j ...) -UA_VariableAttributes_delete(ptr) -``` """ mutable struct JUA_VariableAttributes <: AbstractOpen62541Wrapper ptr::Ptr{UA_VariableAttributes} @@ -453,18 +441,6 @@ generated via the low level interface to the higher level functions. See also [` Note that memory management remains on the C side when using this method, i.e., `ptr` needs to be manually cleaned up with `UA_VariableTypeAttributes_delete(ptr)` after the object is not needed anymore. It is up to the user to ensure this. - -Examples: - -``` -j = JUA_VariableTypeAttributes(value = [0.0, 0.0], displayname = "2D point type", - description = "This is a variable type representing a 2D point.") -ptr = UA_VariableTypeAttributes_generate(value = [0.0, 0.0], displayname = "2D point type", - description = "This is a variable type representing a 2D point.") -j = JUA_VariableTypeAttributes(ptr) -(... do something with j ...) -UA_VariableTypeAttributes_delete(ptr) -``` """ mutable struct JUA_VariableTypeAttributes <: AbstractOpen62541Wrapper ptr::Ptr{UA_VariableTypeAttributes} @@ -513,18 +489,6 @@ generated via the low level interface to the higher level functions. See also [` Note that memory management remains on the C side when using this method, i.e., `ptr` needs to be manually cleaned up with `UA_ObjectAttributes_delete(ptr)` after the object is not needed anymore. It is up to the user to ensure this. - -Examples: - -``` -j = JUA_ObjectAttributes(displayname = "Pump 1", - description = "This is the first pump.") -ptr = UA_ObjectAttributes_generate(displayname = "Pump 1", -description = "This is the first pump.") -j = JUA_ObjectAttributes(ptr) -(... do something with j ...) -UA_ObjectAttributes_delete(ptr) -``` """ mutable struct JUA_ObjectAttributes <: AbstractOpen62541Wrapper ptr::Ptr{UA_ObjectAttributes} @@ -573,18 +537,6 @@ the low level interface to the higher level functions. See also [`UA_ObjectTypeA Note that memory management remains on the C side when using this method, i.e., `ptr` needs to be manually cleaned up with `UA_ObjectTypeAttributes_delete(ptr)` after the object is not needed anymore. It is up to the user to ensure this. - -Examples: - -``` -j = JUA_ObjectTypeAttributes(displayname = "Pump Type", -description = "This is an object type for a pump.") -varattrptr = UA_ObjectTypeAttributes_generate(displayname = "Pump Type", - description = "This is an object type for a pump.") -j = JUA_ObjectTypeAttributes(ptr) -(... do something with j ...) -UA_ObjectTypeAttributes_delete(ptr) -``` """ mutable struct JUA_ObjectTypeAttributes <: AbstractOpen62541Wrapper ptr::Ptr{UA_ObjectTypeAttributes} @@ -634,18 +586,6 @@ Note that memory management remains on the C side when using this method, i.e., `ptr` needs to be manually cleaned up with `UA_ReferenceTypeAttributes_delete(ptr)` after the object is not needed anymore. It is up to the user to ensure this. - -Examples: - -``` -j = JUA_ReferenceTypeAttributes(displayname = "My reference type", - description = "This is my reference type definining a relationship between nodes.") -varattrptr = UA_ReferenceTypeAttributes_generate(displayname = "My reference type", -description = "This is my reference type definining a relationship between nodes.") -j = JUA_ReferenceTypeAttributes(ptr) -(... do something with j ...) -UA_ReferenceTypeAttributes_delete(ptr) -``` """ mutable struct JUA_ReferenceTypeAttributes <: AbstractOpen62541Wrapper ptr::Ptr{UA_ReferenceTypeAttributes} @@ -695,18 +635,6 @@ Note that memory management remains on the C side when using this method, i.e., `ptr` needs to be manually cleaned up with `UA_DataTypeAttributes_delete(ptr)` after the object is not needed anymore. It is up to the user to ensure this. - -Examples: - -``` -j = JUA_DataTypeAttributes(displayname = "my special data type", - description = "This is my special data type with some properties.") -ptr = UA_DataTypeAttributes_generate(displayname = "my special data type", -description = "This is my special data type with some properties.") -j = JUA_DataTypeAttributes(ptr) -(... do something with j ...) -UA_DataTypeAttributes_delete(ptr) -``` """ mutable struct JUA_DataTypeAttributes <: AbstractOpen62541Wrapper ptr::Ptr{UA_DataTypeAttributes} @@ -755,18 +683,6 @@ generated via the low level interface to the higher level functions. See also [` Note that memory management remains on the C side when using this method, i.e., `ptr` needs to be manually cleaned up with `UA_ViewAttributes_delete(ptr)` after the object is not needed anymore. It is up to the user to ensure this. - -Examples: - -``` -j = JUA_ViewAttributes(displayname = "View 1", - description = "This is a view showing only certain nodes.") -varattrptr = UA_ViewAttributes_generate(displayname = "View 1", -description = "This is a view showing only certain nodes.") -j = JUA_ViewAttributes(ptr) -(... do something with j ...) -UA_ViewAttributes_delete(ptr) -``` """ mutable struct JUA_ViewAttributes <: AbstractOpen62541Wrapper ptr::Ptr{UA_ViewAttributes} @@ -785,3 +701,51 @@ end function release_handle(obj::JUA_ViewAttributes) UA_ViewAttributes_delete(Jpointer(obj)) end + +#MethodAttributes +""" +``` +JUA_MethodAttributes +``` + +A mutable struct that defines a `JUA_MethodAttributes` object - the equivalent +of a `UA_MethodAttributes`, but with memory managed by Julia rather than C (see +below for exceptions) + +The following constructor methods are defined: + +``` +JUA_MethodAttributes(; kwargs...) +``` + +For valid keyword arguments `kwargs` see [`UA_MethodAttributes_generate`](@ref). + +``` +JUA_MethodAttributes(ptr::Ptr{UA_MethodAttributes}) +``` + +creates a `JUA_MethodAttributes` based on the pointer `ptr`. +This is a fallback method that can be used to pass `UA_MethodAttributes`s +generated via the low level interface to the higher level functions. See also [`UA_MethodAttributes_generate`](@ref). + +Note that memory management remains on the C side when using this method, i.e., +`ptr` needs to be manually cleaned up with `UA_MethodAttributes_delete(ptr)` +after the object is not needed anymore. It is up to the user to ensure this. +""" +mutable struct JUA_MethodAttributes <: AbstractOpen62541Wrapper + ptr::Ptr{UA_MethodAttributes} + + function JUA_MethodAttributes(; kwargs...) + obj = new(UA_MethodAttributes_generate(; kwargs...)) + finalizer(release_handle, obj) + return obj + end + + function JUA_MethodAttributes(ptr::Ptr{UA_MethodAttributes}) + return new(ptr) #no finalizer, see docstring + end +end + +function release_handle(obj::JUA_MethodAttributes) + UA_MethodAttributes_delete(Jpointer(obj)) +end From 2f06e35a0be84a58f68062e7477c2eff86d3c1e8 Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Wed, 22 May 2024 00:39:07 +0200 Subject: [PATCH 21/26] complete high level types for now; need some more tests --- src/client.jl | 6 +-- src/highlevel_client.jl | 6 +-- src/highlevel_types.jl | 70 +++++++++++++++++++++++++++++++ test/basic_types_and_functions.jl | 8 ---- test/highlevel_types.jl | 30 +++++++++++++ test/runtests.jl | 4 ++ 6 files changed, 109 insertions(+), 15 deletions(-) create mode 100644 test/highlevel_types.jl diff --git a/src/client.jl b/src/client.jl index 2c15fc1..d5b3705 100644 --- a/src/client.jl +++ b/src/client.jl @@ -44,7 +44,7 @@ for att in attributes_UA_Client_Service if @isdefined $(req_type) # Skip functions that use undefined types, e.g. deactivated historizing types #TODO: add docstring #TODO: add tests - function $(fun_name)(client::Ref{UA_Client}, request::Ptr{$(req_type)}) + function $(fun_name)(client::Ptr{UA_Client}, request::Ptr{$(req_type)}) response = Ref{$(resp_type)}() statuscode = __UA_Client_Service(client, request, @@ -58,8 +58,6 @@ for att in attributes_UA_Client_Service end end end - #function fallback that wraps any non-ref arguments into refs: - $(fun_name)(client, request) = $(fun_name)(wrap_ref(client), wrap_ref(request)) end end @@ -135,7 +133,7 @@ for nodeclass in instances(UA_NodeClass) UA_TYPES_PTRS[$(attributeptr_sym)], outNewNodeId) end end - else + elseif funname_sym != :UA_Client_addMethodNode #can't add method node via client. @eval begin """ ``` diff --git a/src/highlevel_client.jl b/src/highlevel_client.jl index 4cbf366..d884761 100644 --- a/src/highlevel_client.jl +++ b/src/highlevel_client.jl @@ -49,8 +49,8 @@ JUA_Client_addNode(client::JUA_Client, requestedNewNodeId::JUA_NodeId, outNewNodeId::JUA_NodeId)::UA_StatusCode ``` -uses the client API to add a Variable, VariableType, or Object node to the server -to which the client is connected to. +uses the client API to add a ObjectType, ReferenceType, DataType or View node +to the server to which the client is connected to. See [`JUA_ObjectTypeAttributes`](@ref), See [`JUA_ReferenceTypeAttributes`](@ref), [`JUA_DataTypeAttributes`](@ref), and [`JUA_ViewAttributes`](@ref) on how to define valid attributes. """ @@ -82,7 +82,7 @@ for nodeclass in instances(UA_NodeClass) attributes, outNewNodeId) end end - else + elseif funname_sym != :UA_Client_addMethodNode #can't add method node via client. @eval begin function JUA_Client_addNode(client, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, diff --git a/src/highlevel_types.jl b/src/highlevel_types.jl index 434719a..fe3b9ae 100644 --- a/src/highlevel_types.jl +++ b/src/highlevel_types.jl @@ -14,13 +14,52 @@ Base.show(io::IO, ::MIME"text/plain", v::AbstractOpen62541Wrapper) = print(io, " ## Useful basic types #String +""" +``` +JUA_String +``` + +a mutable struct that defines a string type usable with open62541. It is the equivalent +of a `UA_String`, but with memory managed by Julia rather than C. + +The following constructor methods are defined: + +``` +JUA_String() +``` + +creates an empty `JUA_String`, equivalent to calling `UA_String_new()`. + +``` +JUA_String(s::AbstractString) +``` + +creates a `JUA_String` containing the string `s`. + +``` +JUA_String(ptr::Ptr{UA_String}) +``` + +creates a `JUA_String` based on the pointer `ptr`. This is a fallback +method that can be used to pass `UA_Guid`s generated via the low level interface +to the higher level functions. Note that memory management remains on the C side +when using this method, i.e., `ptr` needs to be manually cleaned up with +`UA_String_delete(ptr)` after the object is not needed anymore. It is up +to the user to ensure this. + +""" mutable struct JUA_String <: AbstractOpen62541Wrapper ptr::Ptr{UA_String} + function JUA_String(s::AbstractString) obj = new(UA_STRING(s)) finalizer(release_handle, obj) return obj end + + function JUA_String(ptr::Ptr{UA_String}) + return new(ptr) + end end function release_handle(obj::JUA_String) @@ -51,6 +90,17 @@ JUA_Guid(guidstring::AbstractString) creates a `JUA_Guid` by parsing the string `guidstring`. The string should be formatted according to the OPC standard defined in Part 6, 5.1.3. +``` +JUA_Guid(ptr::Ptr{UA_Guid}) +``` + +creates a `JUA_Guid` based on the pointer `ptr`. This is a fallback +method that can be used to pass `UA_Guid`s generated via the low level interface +to the higher level functions. Note that memory management remains on the C side +when using this method, i.e., `ptr` needs to be manually cleaned up with +`UA_Guid_delete(ptr)` after the object is not needed anymore. It is up +to the user to ensure this. + """ mutable struct JUA_Guid <: AbstractOpen62541Wrapper ptr::Ptr{UA_Guid} @@ -64,6 +114,10 @@ mutable struct JUA_Guid <: AbstractOpen62541Wrapper finalizer(release_handle, obj) return obj end + + function JUA_Guid(ptr::Ptr{UA_Guid}) + return new(ptr) + end end function release_handle(obj::JUA_Guid) @@ -215,6 +269,17 @@ JUA_QualifiedName(nsIndex::Integer, identifier::AbstractString) creates a `JUA_QualifiedName` with namespace index `nsIndex` and text identifier `identifier`. +``` +JUA_QualifiedName(ptr::Ptr{UA_QualifiedName}) +``` + +creates a `JUA_QualifiedName` based on the pointer `ptr`. This is a fallback +method that can be used to pass `UA_QualifiedName`s generated via the low level +interface to the higher level functions. Note that memory management remains on +the C side when using this method, i.e., `ptr` needs to be manually cleaned up with +`UA_QualifiedName_delete(ptr)` after the object is not needed anymore. It is up +to the user to ensure this. + """ mutable struct JUA_QualifiedName <: AbstractOpen62541Wrapper ptr::Ptr{UA_QualifiedName} @@ -230,6 +295,11 @@ mutable struct JUA_QualifiedName <: AbstractOpen62541Wrapper finalizer(release_handle, obj) return obj end + + function JUA_QualifiedName(ptr::Ptr{UA_QualifiedName}) + obj = new(ptr) + return obj + end end function release_handle(obj::JUA_QualifiedName) diff --git a/test/basic_types_and_functions.jl b/test/basic_types_and_functions.jl index e0fa125..18cf762 100644 --- a/test/basic_types_and_functions.jl +++ b/test/basic_types_and_functions.jl @@ -119,11 +119,3 @@ client = UA_Client_new() context = UA_Client_getContext(client) @test isa(context, Ptr{Ptr{Nothing}}) UA_Client_delete(client) - -#Random wrapper tests -j = JUA_Variant() -@test j isa JUA_Variant -j2 = JUA_QualifiedName(1, "test") -ua_s = UA_STRING("test") -@test unsafe_string(j2.name) == unsafe_string(ua_s) #tests access of properties in high level struct. -UA_String_delete(ua_s) \ No newline at end of file diff --git a/test/highlevel_types.jl b/test/highlevel_types.jl new file mode 100644 index 0000000..6e19f42 --- /dev/null +++ b/test/highlevel_types.jl @@ -0,0 +1,30 @@ +using open62541 +using Test + +#JUA_String +j1 = JUA_String("test") +u1 = UA_STRING("test") +j2 = JUA_String(u1) +@test j1 isa JUA_String +@test j2 isa JUA_String +UA_String_delete(u1) + +#JUA_Variant +j1 = JUA_Variant() +@test j1 isa JUA_Variant +v = UA_Variant_new() +j2 = JUA_Variant(v) +@test j2 isa JUA_Variant +UA_Variant_delete(v) + +#JUA_QualifiedName +j1 = JUA_QualifiedName(1, "test") +ua_s = UA_STRING("test") +@test unsafe_string(j1.name) == unsafe_string(ua_s) #tests access of properties in high level struct. +UA_String_delete(ua_s) +u1 = UA_QUALIFIEDNAME(1, "test") +j2 = JUA_QualifiedName(u1) +@test j1 isa JUA_QualifiedName +@test j2 isa JUA_QualifiedName +UA_QualifiedName_delete(u1) + diff --git a/test/runtests.jl b/test/runtests.jl index 04e4e40..4552a01 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -21,6 +21,10 @@ end include("basic_types_and_functions.jl") end +@safetestset "High level types" begin + include("highlevel_types.jl") +end + @safetestset "Basic data handling" begin include("data_handling.jl") end From d1fa9ce43c6689207e557fb6f5d913edcad880af Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Wed, 22 May 2024 06:45:01 +0200 Subject: [PATCH 22/26] test fallback methods --- test/highlevel_types.jl | 56 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/test/highlevel_types.jl b/test/highlevel_types.jl index 6e19f42..74f28b3 100644 --- a/test/highlevel_types.jl +++ b/test/highlevel_types.jl @@ -9,6 +9,14 @@ j2 = JUA_String(u1) @test j2 isa JUA_String UA_String_delete(u1) +#JUA_Guid +j1 = JUA_Guid() +u1 = UA_Guid_new() +j2 = JUA_Guid(u1) +@test j1 isa JUA_Guid +@test j2 isa JUA_Guid +UA_Guid_delete(u1) + #JUA_Variant j1 = JUA_Variant() @test j1 isa JUA_Variant @@ -17,6 +25,14 @@ j2 = JUA_Variant(v) @test j2 isa JUA_Variant UA_Variant_delete(v) +#JUA_NodeId +j1 = JUA_NodeId() +u1 = UA_NodeId_new() +j2 = JUA_NodeId(u1) +@test j1 isa JUA_NodeId +@test j2 isa JUA_NodeId +UA_NodeId_delete(u1) + #JUA_QualifiedName j1 = JUA_QualifiedName(1, "test") ua_s = UA_STRING("test") @@ -28,3 +44,43 @@ j2 = JUA_QualifiedName(u1) @test j2 isa JUA_QualifiedName UA_QualifiedName_delete(u1) +#Attributes +#just testing the fallback methods here; other methods are tested in more in depth +#testsets +u1 = UA_VariableAttributes_generate(value = "test1", + displayname = "test2", + description = "test3") +u2 = UA_VariableTypeAttributes_generate(value = "test1", + displayname = "test2", + description = "test3") +u3 = UA_ObjectAttributes_generate(displayname = "test2", description = "test3") +u4 = UA_ObjectTypeAttributes_generate(displayname = "test2", description = "test3") +u5 = UA_ReferenceTypeAttributes_generate(displayname = "test2", + description = "test3") +u6 = UA_DataTypeAttributes_generate(displayname = "test2", description = "test3") +u7 = UA_ViewAttributes_generate(displayname = "test2", description = "test3") +u8 = UA_MethodAttributes_generate(displayname = "test2", description = "test3") +j1 = JUA_VariableAttributes(u1) +j2 = JUA_VariableTypeAttributes(u2) +j3 = JUA_ObjectAttributes(u3) +j4 = JUA_ObjectTypeAttributes(u4) +j5 = JUA_ReferenceTypeAttributes(u5) +j6 = JUA_DataTypeAttributes(u6) +j7 = JUA_ViewAttributes(u7) +j8 = JUA_MethodAttributes(u8) +@test j1 isa JUA_VariableAttributes +@test j2 isa JUA_VariableTypeAttributes +@test j3 isa JUA_ObjectAttributes +@test j4 isa JUA_ObjectTypeAttributes +@test j5 isa JUA_ReferenceTypeAttributes +@test j6 isa JUA_DataTypeAttributes +@test j7 isa JUA_ViewAttributes +@test j8 isa JUA_MethodAttributes +UA_VariableAttributes_delete(u1) +UA_VariableTypeAttributes_delete(u2) +UA_ObjectAttributes_delete(u3) +UA_ObjectTypeAttributes_delete(u4) +UA_ReferenceTypeAttributes_delete(u5) +UA_DataTypeAttributes_delete(u6) +UA_ViewAttributes_delete(u7) +UA_MethodAttributes_delete(u8) From 5b20e470ce0562839cf7e60de01d055086b5456a Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Wed, 22 May 2024 07:03:42 +0200 Subject: [PATCH 23/26] align client/server writevalue functions incl. jua_variant case --- docs/src/manual/client.md | 1 + docs/src/manual/server.md | 2 ++ src/highlevel_client.jl | 7 ++----- src/highlevel_server.jl | 13 ++++++++----- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/src/manual/client.md b/docs/src/manual/client.md index a21af9d..d636ecb 100644 --- a/docs/src/manual/client.md +++ b/docs/src/manual/client.md @@ -5,6 +5,7 @@ This page lists docstrings relevant to the client API. ## Adding different types of nodes: ```@docs +JUA_Client_addNode UA_Client_addVariableNode UA_Client_addObjectNode UA_Client_addVariableTypeNode diff --git a/docs/src/manual/server.md b/docs/src/manual/server.md index 840efd7..f4c0a6e 100644 --- a/docs/src/manual/server.md +++ b/docs/src/manual/server.md @@ -4,7 +4,9 @@ This page lists docstrings relevant to the server API. ## Adding different types of nodes: + ```@docs +JUA_Server_addNode UA_Server_addVariableNode UA_Server_addObjectNode UA_Server_addVariableTypeNode diff --git a/src/highlevel_client.jl b/src/highlevel_client.jl index d884761..601a1c7 100644 --- a/src/highlevel_client.jl +++ b/src/highlevel_client.jl @@ -140,13 +140,10 @@ See also [`JUA_Variant`](@ref) """ function JUA_Client_writeValueAttribute(client, nodeId, newvalue) newvariant = JUA_Variant(newvalue) - statuscode = UA_Client_writeValueAttribute(client, nodeId, newvariant) - return statuscode + return JUA_Client_writeValueAttribute(client, nodeId, newvariant) end function JUA_Client_writeValueAttribute(client, nodeId, newvalue::JUA_Variant) - statuscode = UA_Client_writeValueAttribute(client, nodeId, newvariant) + statuscode = UA_Client_writeValueAttribute(client, nodeId, newvalue) return statuscode end - - diff --git a/src/highlevel_server.jl b/src/highlevel_server.jl index 575d770..dce992a 100644 --- a/src/highlevel_server.jl +++ b/src/highlevel_server.jl @@ -173,9 +173,12 @@ any of its constructors. See also [`JUA_Variant`](@ref) """ -function JUA_Server_writeValue(server, nodeId, newvalue) - newvariant = UA_Variant_new(newvalue) - statuscode = UA_Server_writeValue(server, nodeId, newvariant) - UA_Variant_delete(newvariant) +function JUA_Server_writeValueAttribute(server, nodeId, newvalue) + newvariant = JUA_Variant(newvalue) + return JUA_Server_writeValueAttribute(server, nodeId, newvariant) +end + +function JUA_Server_writeValueAttribute(server, nodeId, newvalue::JUA_Variant) + statuscode = UA_Server_writeValueAttribute(server, nodeId, newvalue) return statuscode -end \ No newline at end of file +end From f9c6300831337d0d586fe724df8831acb2534de5 Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Wed, 22 May 2024 07:07:20 +0200 Subject: [PATCH 24/26] remove UA_Client_addMethodNode (unuseable also in open62541) --- docs/src/manual/client.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/src/manual/client.md b/docs/src/manual/client.md index d636ecb..cce5c2e 100644 --- a/docs/src/manual/client.md +++ b/docs/src/manual/client.md @@ -13,7 +13,6 @@ UA_Client_addObjectTypeNode UA_Client_addViewNode UA_Client_addReferenceTypeNode UA_Client_addDataTypeNode -UA_Client_addMethodNode ``` ## Reading from nodes: From 3bc82aefcf2bb43c69cd4fdc5668cf405223574e Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Wed, 22 May 2024 07:53:39 +0200 Subject: [PATCH 25/26] clean up --- src/client.jl | 6 +++--- src/highlevel_server.jl | 8 ++++---- test/add_change_var_array.jl | 26 ++++++++++++++++++++++---- test/add_change_var_scalar.jl | 2 -- test/highlevel_types.jl | 2 ++ test/runtests.jl | 6 +++--- test/server_write.jl | 5 +---- 7 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/client.jl b/src/client.jl index d5b3705..f245e5b 100644 --- a/src/client.jl +++ b/src/client.jl @@ -41,7 +41,7 @@ for att in attributes_UA_Client_Service resp_type_ptr = Symbol("UA_TYPES_", uppercase(String(att[2])), "RESPONSE") @eval begin - if @isdefined $(req_type) # Skip functions that use undefined types, e.g. deactivated historizing types + if @isdefined $(req_type) # Skip functions that use undefined types #TODO: add docstring #TODO: add tests function $(fun_name)(client::Ptr{UA_Client}, request::Ptr{$(req_type)}) @@ -51,7 +51,7 @@ for att in attributes_UA_Client_Service UA_TYPES_PTRS[$(req_type_ptr)], response, UA_TYPES_PTRS[$(resp_type_ptr)]) - if isnothing(statuscode) || statuscode == UA_STATUSCODE_GOOD + if isnothing(statuscode) || statuscode == UA_STATUSCODE_GOOD #TODO: why is isnothing() here? do some open62541 service methods return nothing?? return response[] else throw(ClientServiceRequestError("Service request of type ´$(req_type)´ from UA_Client failed with statuscode \"$(UA_StatusCode_name_print(statuscode))\".")) @@ -63,7 +63,7 @@ end #TODO: add docstring #TODO: add tests -function UA_Client_MonitoredItems_setMonitoringMode(client, request) +function UA_Client_MonitoredItems_setMonitoringMode(client, request) #XXX: this leaks memory? response = UA_SetMonitoringModeResponse_new() __UA_Client_Service(client, request, UA_TYPES_PTRS[UA_TYPES_SETMONITORINGMODEREQUEST], diff --git a/src/highlevel_server.jl b/src/highlevel_server.jl index dce992a..4fc8eee 100644 --- a/src/highlevel_server.jl +++ b/src/highlevel_server.jl @@ -173,12 +173,12 @@ any of its constructors. See also [`JUA_Variant`](@ref) """ -function JUA_Server_writeValueAttribute(server, nodeId, newvalue) +function JUA_Server_writeValue(server, nodeId, newvalue) newvariant = JUA_Variant(newvalue) - return JUA_Server_writeValueAttribute(server, nodeId, newvariant) + return JUA_Server_writeValue(server, nodeId, newvariant) end -function JUA_Server_writeValueAttribute(server, nodeId, newvalue::JUA_Variant) - statuscode = UA_Server_writeValueAttribute(server, nodeId, newvalue) +function JUA_Server_writeValue(server, nodeId, newvalue::JUA_Variant) + statuscode = UA_Server_writeValue(server, nodeId, newvalue) return statuscode end diff --git a/test/add_change_var_array.jl b/test/add_change_var_array.jl index d26220e..4cedb1f 100644 --- a/test/add_change_var_array.jl +++ b/test/add_change_var_array.jl @@ -6,8 +6,6 @@ #Types tested: Bool, Int8/16/32/64, UInt8/16/32/64, Float32/64, String, ComplexF32/64 -#TODO: introduce final high level functions - using Distributed Distributed.addprocs(1) # Add a single worker process to run the server @@ -24,8 +22,15 @@ Distributed.@everywhere begin reshape( [randstring(rand(1:10)) for i in 1:prod(array_size)], - array_size...) for array_size in array_sizes) - for type in types) + array_size...) for array_size in array_sizes) + for type in types) + + # input_data2 = Tuple(Tuple(type != String ? rand(type, array_size) : + # reshape( + # [randstring(rand(1:10)) + # for i in 1:prod(array_size)], + # array_size...) for array_size in array_sizes) + # for type in types) varnode_ids = ["$(string(array_size)) $(Symbol(type)) array variable" for type in types, array_size in array_sizes] end @@ -66,6 +71,19 @@ Distributed.@spawnat Distributed.workers()[end] begin else @test all(input .== output_server) end + + # #do a write-read-write cycle on each node #XXX: This fails, no idea why!? + # input2 = input_data2[type_ind][array_size_ind] + # ret = JUA_Server_writeValue(server, varnodeid, input2) + # @test ret == UA_STATUSCODE_GOOD + # output_server2 = JUA_Server_readValue(server, varnodeid) + # if type <: AbstractFloat + # @test all(isapprox.(input2, output_server2)) + # else + # @test all(input2 .== output_server2) + # end + # ret = JUA_Server_writeValue(server, varnodeid, input) + # @test ret == UA_STATUSCODE_GOOD end end diff --git a/test/add_change_var_scalar.jl b/test/add_change_var_scalar.jl index b0cd15b..5b4f155 100644 --- a/test/add_change_var_scalar.jl +++ b/test/add_change_var_scalar.jl @@ -6,8 +6,6 @@ #Types tested: Bool, Int8/16/32/64, UInt8/16/32/64, Float32/64, String, ComplexF32/64 -#TODO: introduce final high level functions - using Distributed Distributed.addprocs(1) # Add a single worker process to run the server diff --git a/test/highlevel_types.jl b/test/highlevel_types.jl index 74f28b3..70e0305 100644 --- a/test/highlevel_types.jl +++ b/test/highlevel_types.jl @@ -40,8 +40,10 @@ ua_s = UA_STRING("test") UA_String_delete(ua_s) u1 = UA_QUALIFIEDNAME(1, "test") j2 = JUA_QualifiedName(u1) +j3 = JUA_QualifiedName() @test j1 isa JUA_QualifiedName @test j2 isa JUA_QualifiedName +@test j3 isa JUA_QualifiedName UA_QualifiedName_delete(u1) #Attributes diff --git a/test/runtests.jl b/test/runtests.jl index 4552a01..1ab5786 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -57,9 +57,9 @@ end include("server_add_nodes_highlevelinterface.jl") end -# @safetestset "Memory leaks" begin -# include("memoryleaks.jl") -# end +@safetestset "Memory leaks" begin + include("memoryleaks.jl") +end #Testsets below here use Distributed; normal testsets required # !!! Leakage of variables must be assessed manually. !!! diff --git a/test/server_write.jl b/test/server_write.jl index 5b960ab..1e707dc 100644 --- a/test/server_write.jl +++ b/test/server_write.jl @@ -1,12 +1,9 @@ # Purpose: This testset checks whether the UA_Server_writeXXXAttribute(...) functions #are usable. This is currently only implemented for nodes of "variable" type. For the attributes #contained in such nodes we check whether the respective write function is able to write a correct -#variable type to the node (TODO: also check that the correct value is actually readable from the -#node after writing). For functions not defined for a variable node, we check that they throw the +#variable type to the node. For functions not defined for a variable node, we check that they throw the #appropriate exception. -#TODO: implement other node types, so that we can check the remaining functions. - using open62541 using Test using Base.Threads From a9b34b21b58dcb7294376aa66bca56bc7d7e84f5 Mon Sep 17 00:00:00 2001 From: Thomas Vetter Date: Wed, 22 May 2024 08:13:08 +0200 Subject: [PATCH 26/26] bugfix JUA_Server_writeValue --- src/highlevel_server.jl | 2 +- test/add_change_var_array.jl | 36 ++++++++++++++++++------------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/highlevel_server.jl b/src/highlevel_server.jl index 4fc8eee..7df4c27 100644 --- a/src/highlevel_server.jl +++ b/src/highlevel_server.jl @@ -179,6 +179,6 @@ function JUA_Server_writeValue(server, nodeId, newvalue) end function JUA_Server_writeValue(server, nodeId, newvalue::JUA_Variant) - statuscode = UA_Server_writeValue(server, nodeId, newvalue) + statuscode = UA_Server_writeValue(server, nodeId, unsafe_load(Jpointer(newvalue))) #Yes, this is black magic; necessary due to how the function is defined in open62541 return statuscode end diff --git a/test/add_change_var_array.jl b/test/add_change_var_array.jl index 4cedb1f..cc028e7 100644 --- a/test/add_change_var_array.jl +++ b/test/add_change_var_array.jl @@ -25,12 +25,12 @@ Distributed.@everywhere begin array_size...) for array_size in array_sizes) for type in types) - # input_data2 = Tuple(Tuple(type != String ? rand(type, array_size) : - # reshape( - # [randstring(rand(1:10)) - # for i in 1:prod(array_size)], - # array_size...) for array_size in array_sizes) - # for type in types) + input_data2 = Tuple(Tuple(type != String ? rand(type, array_size) : + reshape( + [randstring(rand(1:10)) + for i in 1:prod(array_size)], + array_size...) for array_size in array_sizes) + for type in types) varnode_ids = ["$(string(array_size)) $(Symbol(type)) array variable" for type in types, array_size in array_sizes] end @@ -72,18 +72,18 @@ Distributed.@spawnat Distributed.workers()[end] begin @test all(input .== output_server) end - # #do a write-read-write cycle on each node #XXX: This fails, no idea why!? - # input2 = input_data2[type_ind][array_size_ind] - # ret = JUA_Server_writeValue(server, varnodeid, input2) - # @test ret == UA_STATUSCODE_GOOD - # output_server2 = JUA_Server_readValue(server, varnodeid) - # if type <: AbstractFloat - # @test all(isapprox.(input2, output_server2)) - # else - # @test all(input2 .== output_server2) - # end - # ret = JUA_Server_writeValue(server, varnodeid, input) - # @test ret == UA_STATUSCODE_GOOD + #do a write-read-write cycle on each node + input2 = input_data2[type_ind][array_size_ind] + ret = JUA_Server_writeValue(server, varnodeid, input2) + @test ret == UA_STATUSCODE_GOOD + output_server2 = JUA_Server_readValue(server, varnodeid) + if type <: AbstractFloat + @test all(isapprox.(input2, output_server2)) + else + @test all(input2 .== output_server2) + end + ret = JUA_Server_writeValue(server, varnodeid, input) + @test ret == UA_STATUSCODE_GOOD end end