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/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 8273abd..76470a2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,103 @@ -# 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](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 +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 +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. + +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 9768e4a..1b11c7a 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -12,27 +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" + "Home" => "index.md", + "Manual" => [ + "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" + ] ], warnonly = Documenter.except( - :autodocs_block, + :autodocs_block, # :cross_references, - :docs_block, - :doctest, - :eval_block, - :example_block, - :footnote, - :linkcheck_remotes, - :linkcheck, - :meta_block, - :missing_docs, - :parse_error, + :docs_block, + :doctest, + :eval_block, + :example_block, + :footnote, + :linkcheck_remotes, + :linkcheck, + :meta_block, + #:missing_docs, + :parse_error, :setup_block - ), + ) ) deploydocs(; 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/attributegeneration.md b/docs/src/manual/attributegeneration.md new file mode 100644 index 0000000..b47f328 --- /dev/null +++ b/docs/src/manual/attributegeneration.md @@ -0,0 +1,40 @@ +# 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: + +```@docs +UA_VALUERANK +UA_ACCESSLEVEL +UA_USERACCESSLEVEL +UA_WRITEMASK +UA_USERWRITEMASK +UA_EVENTNOTIFIER +``` + +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 +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..cce5c2e --- /dev/null +++ b/docs/src/manual/client.md @@ -0,0 +1,68 @@ +# Client + +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 +UA_Client_addObjectTypeNode +UA_Client_addViewNode +UA_Client_addReferenceTypeNode +UA_Client_addDataTypeNode +``` + +## Reading from nodes: + +```@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: + +```@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/nodeid.md b/docs/src/manual/nodeid.md new file mode 100644 index 0000000..6970fb6 --- /dev/null +++ b/docs/src/manual/nodeid.md @@ -0,0 +1,22 @@ +# 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/manual/numbertypes.md b/docs/src/manual/numbertypes.md new file mode 100644 index 0000000..584d570 --- /dev/null +++ b/docs/src/manual/numbertypes.md @@ -0,0 +1,14 @@ +# 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/docs/src/manual/server.md b/docs/src/manual/server.md new file mode 100644 index 0000000..f4c0a6e --- /dev/null +++ b/docs/src/manual/server.md @@ -0,0 +1,63 @@ +# Server + +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 +UA_Server_addObjectTypeNode +UA_Server_addViewNode +UA_Server_addReferenceTypeNode +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 +``` diff --git a/docs/src/tutorials/client_first_steps.md b/docs/src/tutorials/client_first_steps.md new file mode 100644 index 0000000..eb204d8 --- /dev/null +++ b/docs/src/tutorials/client_first_steps.md @@ -0,0 +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 +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 new file mode 100644 index 0000000..b422404 --- /dev/null +++ b/docs/src/tutorials/server_first_steps.md @@ -0,0 +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) +``` + +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 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/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/attribute_generation.jl b/src/attribute_generation.jl index 7a6a822..c63a0b3 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,15 +25,11 @@ 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, +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 @@ -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, @@ -137,7 +195,8 @@ 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}, + 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) @@ -146,15 +205,44 @@ 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_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) + a[i] = UA_String_fromChars(value[i]) + end + __set_array_attributes!(attr, a, valuerank) + 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, 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 @@ -166,10 +254,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 @@ -212,7 +297,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) @@ -267,6 +352,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 @@ -279,7 +368,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, @@ -289,25 +378,28 @@ 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{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, 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 @@ -315,7 +407,8 @@ 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 @@ -327,7 +420,8 @@ 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 @@ -337,6 +431,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() @@ -347,7 +451,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) @@ -445,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 af061ba..f245e5b 100644 --- a/src/client.jl +++ b/src/client.jl @@ -41,31 +41,29 @@ 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::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, 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))\".")) 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 #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], @@ -112,88 +110,51 @@ for nodeclass in instances(UA_NodeClass) if funname_sym == :UA_Client_addVariableNode || 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 - function $(funname_sym)(client, - requestedNewNodeId, - parentNodeId, - referenceTypeId, - browseName, - typeDefinition, - attributes, + """ + ``` + $($(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, + 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 + elseif funname_sym != :UA_Client_addMethodNode #can't add method node via client. @eval begin - function $(funname_sym)(client, - requestedNewNodeId, - parentNodeId, - referenceTypeId, - browseName, - attributes, - outNewNodeId) + """ + ``` + $($(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, + 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 @@ -209,18 +170,15 @@ 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]))}) ``` 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, @@ -228,7 +186,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" @@ -255,17 +213,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, @@ -286,12 +240,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 @@ -487,6 +435,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/helper_functions.jl b/src/helper_functions.jl index 4280369..174fa0b 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, type) + wrapped = unsafe_wrap(v)::type + 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 diff --git a/src/highlevel_client.jl b/src/highlevel_client.jl new file mode 100644 index 0000000..601a1c7 --- /dev/null +++ b/src/highlevel_client.jl @@ -0,0 +1,149 @@ +#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 +""" +``` +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 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. +""" +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 + elseif funname_sym != :UA_Client_addMethodNode #can't add method node via client. + @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 +""" +``` +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} + v = UA_Variant_new() + UA_Client_readValueAttribute(client, nodeId, v) + r = __get_juliavalues_from_variant(v, type) + UA_Variant_delete(v) + return r +end + +""" +``` +JUA_Client_writeValueAttribute(server::JUA_Client, nodeId::JUA_NodeId, newvalue)::UA_StatusCode +``` + +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. + +See also [`JUA_Variant`](@ref) + +""" +function JUA_Client_writeValueAttribute(client, nodeId, newvalue) + newvariant = JUA_Variant(newvalue) + return JUA_Client_writeValueAttribute(client, nodeId, newvariant) +end + +function JUA_Client_writeValueAttribute(client, nodeId, newvalue::JUA_Variant) + statuscode = UA_Client_writeValueAttribute(client, nodeId, newvalue) + return statuscode +end diff --git a/src/highlevel_server.jl b/src/highlevel_server.jl new file mode 100644 index 0000000..7df4c27 --- /dev/null +++ b/src/highlevel_server.jl @@ -0,0 +1,184 @@ +#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 + +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) + #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. + +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, + 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), + nodeContext, outNewNodeId, 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), + nodeContext, outNewNodeId) + return $(funname_sym)(server, requestedNewNodeId, + parentNodeId, referenceTypeId, browseName, attributes, + nodeContext, outNewNodeId) + end + end + end + end +end + +#Server read and write functions +""" +``` +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`, then you should +specify this. If the wrong type is specified, the function will throw a TypeError. + +""" +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) + 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`. +`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(server, nodeId, newvalue) + newvariant = JUA_Variant(newvalue) + return JUA_Server_writeValue(server, nodeId, newvariant) +end + +function JUA_Server_writeValue(server, nodeId, newvalue::JUA_Variant) + 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/src/highlevel_types.jl b/src/highlevel_types.jl new file mode 100644 index 0000000..fe3b9ae --- /dev/null +++ b/src/highlevel_types.jl @@ -0,0 +1,821 @@ +#Preliminary definitions +abstract type AbstractOpen62541Wrapper end + +Jpointer(x::AbstractOpen62541Wrapper) = getfield(x, :ptr) +function Base.getproperty(x::AbstractOpen62541Wrapper, f::Symbol) + unsafe_load(getproperty(Jpointer(x), f)) +end + +function Base.unsafe_convert(::Type{Ptr{T}}, obj::AbstractOpen62541Wrapper) where {T} + 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 +""" +``` +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) + UA_String_delete(Jpointer(obj)) +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. + +``` +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} + function JUA_Guid() + obj = new(UA_Guid_new()) + finalizer(release_handle, obj) + return obj + end + function JUA_Guid(guidstring::AbstractString) + obj = new(UA_GUID(guidstring)) + 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) + UA_Guid_delete(Jpointer(obj)) +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::AbstractString) +``` + +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::AbstractString) +``` + +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`. + +``` +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: + +``` +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} + + function JUA_NodeId() + obj = new(UA_NodeId_new()) + 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) + return obj + end +end + +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 +""" +``` +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`. + +``` +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} + + 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) + return obj + end + + function JUA_QualifiedName(ptr::Ptr{UA_QualifiedName}) + obj = new(ptr) + return obj + end +end + +function release_handle(obj::JUA_QualifiedName) + UA_QualifiedName_delete(Jpointer(obj)) +end + +Base.convert(::Type{UA_QualifiedName}, x::JUA_QualifiedName) = unsafe_load(Jpointer(x)) + +#Variant +""" +``` +JUA_Variant +``` + +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 constructor methods are defined: + +``` +JUA_Variant() +``` + +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 `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_Variant(variantptr::Ptr{UA_Variant}) +``` + +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: + +``` +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(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} + 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) + UA_Variant_delete(Jpointer(obj)) +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. +""" +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) + UA_VariableAttributes_delete(Jpointer(obj)) +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. +""" +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) + UA_VariableTypeAttributes_delete(Jpointer(obj)) +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. +""" +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) + UA_ObjectAttributes_delete(Jpointer(obj)) +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. +""" +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) + UA_ObjectTypeAttributes_delete(Jpointer(obj)) +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. +""" +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) + UA_ReferenceTypeAttributes_delete(Jpointer(obj)) +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. +""" +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) + UA_DataTypeAttributes_delete(Jpointer(obj)) +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. +""" +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) + 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 diff --git a/src/open62541.jl b/src/open62541.jl index be9fdfb..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") @@ -20122,7 +20126,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 4de8e3b..d38a169 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`](@ref) on how to define valid attributes. """ function UA_Server_addMethodNode(server, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, attr, method::Function, @@ -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, attr, 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,12 +71,25 @@ 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 @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`](@ref) on how to define valid attributes. + """ function $(funname_sym)(server, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, typeDefinition, attributes, nodeContext, outNewNodeId) @@ -96,23 +97,24 @@ 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 # 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`](@ref) on how to define valid attributes. + """ function $(funname_sym)(server, requestedNewNodeId, parentNodeId, referenceTypeId, browseName, attributes, nodeContext, outNewNodeId) @@ -123,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 @@ -150,16 +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`. + 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" @@ -183,10 +179,10 @@ 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 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)] diff --git a/src/types.jl b/src/types.jl index 783ff1a..1f14737 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 @@ -64,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 @@ -97,15 +98,18 @@ 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)] + 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 # 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"))() @@ -280,10 +284,21 @@ 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)) + +#adapt base complex method for interoperability between ua complex numbers and julia complex numbers +function Base.complex(x::T) where {T <: + Union{UA_ComplexNumberType, UA_DoubleComplexNumberType}} + Complex(x.real, x.imaginary) +end ## UA_BYTESTRING """ @@ -362,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 UA_String). +creates a `UA_NodeId` object by parsing `s`. Example: @@ -391,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() @@ -407,8 +423,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 +450,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 +467,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 +494,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 +511,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 +545,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 +708,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) @@ -743,41 +792,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_copy(value::AbstractArray{T, N}, - type_ptr::Ptr{UA_DataType} = ua_data_type_ptr_default(T)) where {T, N} - var = UA_Variant_new() - var.type = type_ptr - var.storageType = UA_VARIANT_DATA - var.arrayLength = length(value) - 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}, - type_ptr::Ptr{UA_DataType} = ua_data_type_ptr_default(T)) where {T <: Union{ - AbstractFloat, Integer}} - 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 - 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) -end - -function UA_Variant_new_copy(value, type_sym::Symbol) - UA_Variant_new_copy(value, ua_data_type_ptr(Val(type_sym))) -end - function Base.unsafe_wrap(v::UA_Variant) type = juliadatatype(v.type) data = reinterpret(Ptr{type}, v.data) @@ -790,11 +804,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_isEmpty(p::Ptr{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_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 deleted file mode 100644 index 51d570b..0000000 --- a/src/wrappers.jl +++ /dev/null @@ -1,172 +0,0 @@ -#TODO: all of this needs docstrings of course. - -#Preliminary definitions -abstract type AbstractOpen62541Wrapper end - -Jpointer(x::AbstractOpen62541Wrapper) = getfield(x, :ptr) -function Base.getproperty(x::AbstractOpen62541Wrapper, f::Symbol) - unsafe_load(getproperty(Jpointer(x), f)) -end - -function Base.unsafe_convert(::Type{Ptr{T}}, obj::AbstractOpen62541Wrapper) where {T} - Base.unsafe_convert(Ptr{T}, Jpointer(obj)) -end - -## Useful basic types -#String -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 -end - -function release_handle(obj::JUA_String) - UA_String_delete(Jpointer(obj)) -end - -#Guid -mutable struct JUA_Guid <: AbstractOpen62541Wrapper - ptr::Ptr{UA_Guid} - function JUA_Guid() - obj = new(UA_Guid_random()) - finalizer(release_handle, obj) - return obj - end - function JUA_Guid(guidstring::AbstractString) - obj = new(UA_GUID(guidstring)) - finalizer(release_handle, obj) - return obj - end -end - -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 - -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 -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 - -## NodeIds -mutable struct JUA_NodeId <: AbstractOpen62541Wrapper - ptr::Ptr{UA_NodeId} - - function JUA_NodeId() - obj = new(UA_NodeId_new()) - finalizer(release_handle, obj) - return obj - end - function JUA_NodeId(identifier::Union{AbstractString, JUA_String}) - obj = new(UA_NODEID(identifier)) - 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) - return obj - end -end - -function release_handle(obj::JUA_NodeId) - UA_NodeId_delete(Jpointer(obj)) -end - -JUA_NodeId_equal(j1, j2) = UA_NodeId_equal(j1, j2) - -#QualifiedName -mutable struct JUA_QualifiedName <: AbstractOpen62541Wrapper - ptr::Ptr{UA_QualifiedName} - function JUA_QualifiedName(nsIndex::Integer, identifier::AbstractString) - obj = new(UA_QUALIFIEDNAME_ALLOC(nsIndex, identifier)) - finalizer(release_handle, obj) - return obj - end -end - -function release_handle(obj::JUA_QualifiedName) - UA_QualifiedName_delete(Jpointer(obj)) -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_QualifiedName_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 - -# 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 JUA_Client_readValueAttribute(client, nodeId, out = JUA_Variant()) 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/add_change_var_array.jl b/test/add_change_var_array.jl index 55c8e54..cc028e7 100644 --- a/test/add_change_var_array.jl +++ b/test/add_change_var_array.jl @@ -1,33 +1,45 @@ -# 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, ComplexF32/64 + 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 # 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, ComplexF32, ComplexF64] 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) + # Generate random input values and generate nodeid names + input_data = 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 # 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 @@ -36,43 +48,60 @@ 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) - 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) - browsename = UA_QUALIFIEDNAME_ALLOC(1, varnode_ids[type_ind, array_size_ind]) - nodecontext = C_NULL - outnewnodeid = C_NULL + 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 = JUA_QualifiedName(1, varnode_ids[type_ind, array_size_ind]) + nodecontext = JUA_NodeId() + outnewnodeid = JUA_NodeId() retval = UA_Server_addVariableNode(server, varnodeid, parentnodeid, parentreferencenodeid, browsename, typedefinition, attr, nodecontext, outnewnodeid) # 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 + + #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 # 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)) -max_duration = 40.0 # Maximum waiting time for server startup -sleep_time = 2.0 # Sleep time in seconds between each connection trial +client = JUA_Client() +JUA_ClientConfig_setDefault(JUA_ClientConfig(client)) +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 - 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 +116,55 @@ 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 <: Union{AbstractFloat, Complex} + @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(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 - - 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 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]) - @test_throws open62541.AttributeReadWriteError UA_Client_writeValueAttribute( - client, - varnodeid, - UA_Variant_new_copy(new_input)) + 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(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) 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..5b4f155 100644 --- a/test/add_change_var_scalar.jl +++ b/test/add_change_var_scalar.jl @@ -1,69 +1,78 @@ # 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 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, ComplexF32/64 + 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, ComplexF32, ComplexF64] + + # Generate random input values and generate nodeid names + 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 # 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 a scalar to the server 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) - 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]) - nodecontext = C_NULL - outnewnodeid = C_NULL + 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 = JUA_NodeId() + outnewnodeid = JUA_NodeId() retval = UA_Server_addVariableNode(server, varnodeid, parentnodeid, parentreferencenodeid, browsename, typedefinition, attr, nodecontext, outnewnodeid) # 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)) -max_duration = 40.0 # Maximum waiting time for server startup -sleep_time = 2.0 # Sleep time in seconds between each connection trial +client = JUA_Client() +JUA_ClientConfig_setDefault(JUA_ClientConfig(client)) +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 - 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 @@ -77,36 +86,40 @@ 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)) + 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]) - retval = UA_Client_writeValueAttribute(client, - varnodeid, - UA_Variant_new_copy(new_input)) + 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 - - 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 <: Union{AbstractFloat, Complex} + @test all(isapprox.(new_input, output_client_new)) + else + @test all(new_input .== output_client_new) + end 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]) - @test_throws open62541.AttributeReadWriteError UA_Client_writeValueAttribute(client, - varnodeid, - UA_Variant_new_copy(new_input)) + type = types[mod(type_ind, length(types)) + 1] # Select wrong data type + 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) 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/attribute_generation.jl b/test/attribute_generation.jl index 3a94d9c..2e8cb69 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 @@ -10,7 +11,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 +22,19 @@ 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 +56,27 @@ 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) == @@ -50,7 +85,12 @@ 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" @@ -61,30 +101,58 @@ 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, Any) + 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" @@ -92,22 +160,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, Any) + 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/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 diff --git a/test/data_handling.jl b/test/data_handling.jl index c88fdeb..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_copy(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_copy(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 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/highlevel_types.jl b/test/highlevel_types.jl new file mode 100644 index 0000000..70e0305 --- /dev/null +++ b/test/highlevel_types.jl @@ -0,0 +1,88 @@ +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_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 +v = UA_Variant_new() +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") +@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) +j3 = JUA_QualifiedName() +@test j1 isa JUA_QualifiedName +@test j2 isa JUA_QualifiedName +@test j3 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) diff --git a/test/runtests.jl b/test/runtests.jl index 599850a..1ab5786 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 @@ -49,6 +53,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 54f4acc..a8df214 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) @@ -85,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(), @@ -93,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 @@ -151,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) @@ -270,113 +295,73 @@ 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 -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) + 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. - -#Now test with the higher level JUA interface as well -#configure server -server2 = JUA_Server() -retvalj0 = JUA_ServerConfig_setMinimalCustomBuffer(JUA_Server_getConfig(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 = UA_VariableAttributes_generate(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 = C_NULL -outnewnodeid = C_NULL -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 = UA_ObjectTypeAttributes_generate(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, - 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..742ea7b --- /dev/null +++ b/test/server_add_nodes_highlevelinterface.jl @@ -0,0 +1,364 @@ +# 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 + +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 + +#add manufacturer name to device +mnAttr = JUA_VariableAttributes(value = "", + displayname = "ManufacturerName", + description = "Name of the manufacturer") +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 + +## 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 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/server_read.jl b/test/server_read.jl index d4ab4cf..1e1accb 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 @@ -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..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 @@ -54,25 +51,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 diff --git a/test/simple_server_client.jl b/test/simple_server_client.jl index eed560e..1371ab2 100644 --- a/test/simple_server_client.jl +++ b/test/simple_server_client.jl @@ -9,23 +9,23 @@ 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 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 @@ -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])