diff --git a/common.props b/common.props
index 37422552b2..0421cb61cb 100644
--- a/common.props
+++ b/common.props
@@ -44,8 +44,8 @@
-
-
+
+
diff --git a/docs/opc-publisher/commandline.md b/docs/opc-publisher/commandline.md
index c1b8e8ab2c..59043922e1 100644
--- a/docs/opc-publisher/commandline.md
+++ b/docs/opc-publisher/commandline.md
@@ -70,6 +70,28 @@ General
identity being used
-s, --site, --SiteId=VALUE Sets the site name of the publisher module.
Default: `not set`
+ --pi, --initfile, --InitFilePath[=VALUE]
+ A file from which to read initialization
+ instructions.
+ Use this option to have OPC Publisher run a set
+ of method calls found in this file.
+ The file must be formatted using a subset of the
+ .http/.rest file format without support for
+ indentation, scripting or environment variables.
+ Default: `not set` (disabled). If only a file
+ name is specified, it is loaded from the path
+ specifed using `--pn`. If just the argument is
+ provided without a value the default is `
+ publishednodes.init`.
+ --il, --initlog, --InitLogFile=VALUE
+ A file into which the results of the
+ initialization instructions are written.
+ Only valid if `--pi` option is specified.
+ Default: If a init file is set using `--pi`, it
+ is appended with the `.log` extension. If just a
+ file name is used, the file is created in the
+ same folder as the init file configured using
+ the `--pi` command line option.
--rs, --runtimestatereporting, --RuntimeStateReporting[=VALUE]
Enable that when publisher starts or restarts it
reports its runtime state using a restart
diff --git a/docs/opc-publisher/directmethods.md b/docs/opc-publisher/directmethods.md
index 66f2307414..0f27fa6393 100644
--- a/docs/opc-publisher/directmethods.md
+++ b/docs/opc-publisher/directmethods.md
@@ -12,6 +12,9 @@ az iot hub invoke-module-method --hub-name --device-id _Method Name_: `Shutdown_V2`
+ >
+ > _Request_:
+ >
+ > ```json
+ > true
+ > ```
+ >
+ > _Response_:
+ >
+ > ```json
+ > n/a
+ > ```
+
+## GetServerCertificate_V2
+
+This API can be called to retrieve the HTTP certificate that secures the REST endpoint. A caller should call this API and then validate the server certificate presented against this certificate. The certificate can be cached for the duration of its validity period.
+
+ _Request_: An empty object or `null` can be passed since no argument is required.
+
+ _Response_: when successful Status 200 and the certificate in PEM format.
+
+ _Exceptions_: an exception is thrown when method call returns status other than 200
+
+ _Example_:
+
+ > _Method Name_: `GetServerCertificate_V2`
+ >
+ > _Request_:
+ >
+ > ```json
+ > null
+ > ```
+ >
+ > _Response_:
+ >
+ > ```json
+ > "-----BEGIN CERTIFICATE-----\n ```
+
+The certificate Key can also be read from the IoT Hub device twin as `__certificate__` reported property, together with the ip address, hostname, schema, and port (`__ip__`, `__hostname__`, `__scheme__`, and `__port__`).
+
+## GetApiKey_V2
+
+This API call allows a caller to programmatically obtain the current API key. The API key is passed in the `Authorization` header of the HTTP request sent to the HTTP endpoint. The format of the header value is `ApiKey `, e.g., `ApiKey 85.........F78`.
+
+ _Request_: follows strictly the request [payload schema](./definitions.md#publishednodesentrymodel), the `OpcNodes` attribute being mandatory.
+
+ _Response_: when successful Status 200 and an empty json (`{}`) as payload
+
+ _Exceptions_: an exception is thrown when method call returns status other than 200
+
+ _Example_:
+
+ > _Method Name_: `GetApiKey_V2`
+ >
+ > _Request_:
+ >
+ > ```json
+ > null
+ > ```
+ >
+ > _Response_:
+ >
+ > ```json
+ > "85.........F78"
+ > ```
+
+The API Key can also be read from the IoT Hub device twin as the `__apikey__` reported property.
+
## PublishNodes_V1
PublishNodes enables a client to add a set of nodes to be published. A [`DataSetWriter`](./readme.md#configuration-schema) groups nodes which results in seperate subscriptions being created (grouped further by the Publishing interval, if different ones are configured, but these have no bearing on the `DataSetWriter` identity). A `DataSetWriter`s identity is the combination of `DataSetWriterId`, `DataSetName`, `DataSetKeyFrameCount`, `DataSetClassId`, and connection relevant information such as credentials, security mode, and endpoint Url. To update a `DataSetWriter` this information must match exactly.
@@ -70,7 +153,7 @@ When a `DataSetWriter` already exists, the nodes are incrementally added to the
> }
> ```
-More information can be found in the [API documentation](./api.md#handler-for-publishnodes-direct-method)
+More information can be found in the [API documentation](./api.md#publishnodes)
## AddOrUpdateEndpoints_V1
@@ -122,7 +205,7 @@ This method allows updating multiple endpoints (`DataSetWriter`s) without effect
> }
> ```
-More information can be found in the [API documentation](./api.md#handler-for-addorupdateendpoints-direct-method)
+More information can be found in the [API documentation](./api.md#addorupdateendpoints)
## UnpublishNodes_V1
@@ -170,7 +253,7 @@ _Note_: If all the nodes from a DataSet are to be unpublished, the DataSetWriter
> }
> ```
-More information can be found in the [API documentation](./api.md#handler-for-unpublishallnodes-direct-method)
+More information can be found in the [API documentation](./api.md#unpublishnodes)
## UnpublishAllNodes_V1
@@ -212,7 +295,7 @@ When an empty payload is set or the endpoint in payload is null, the complete co
> }
> ```
-More information can be found in the [API documentation](./api.md#handler-for-unpublishallnodes-direct-method)
+More information can be found in the [API documentation](./api.md#unpublishallnodes)
## GetConfiguredEndpoints_V1
@@ -249,7 +332,7 @@ Returns the configured endpoints (`DataSetWriter`s)
> }
> ```
-More information can be found in the [API documentation](./api.md#handler-for-getconfiguredendpoints-direct-method)
+More information can be found in the [API documentation](./api.md#getconfiguredendpoints)
## SetConfiguredEndpoints_V1
@@ -292,7 +375,7 @@ Sets the configured endpoints (`DataSetWriter`s) and thus allows to update all c
> }
> ```
-More information can be found in the [API documentation](./api.md#handler-for-setconfiguredendpoints-direct-method)
+More information can be found in the [API documentation](./api.md#setconfiguredendpoints)
## GetConfiguredNodesOnEndpoint_V1
@@ -336,7 +419,7 @@ Returns the nodes configured for one Endpoint (`DataSetWriter`s).
> }
> ```
-More information can be found in the [API documentation](./api.md#handler-for-getconfigurednodesonendpoint-direct-method)
+More information can be found in the [API documentation](./api.md#getconfigurednodesonendpoint)
## GetDiagnosticInfo_V1
@@ -397,4 +480,4 @@ Returns a list of actual metrics for all concrete `DataSetWriter`s. This include
> }
> ```
-More information can be found in the [API documentation](./api.md#handler-for-getdiagnosticinfo-direct-method)
+More information can be found in the [API documentation](./api.md#getdiagnosticinfocinfo)
diff --git a/docs/opc-publisher/intfilesamples.md b/docs/opc-publisher/intfilesamples.md
new file mode 100644
index 0000000000..7e186e0b5e
--- /dev/null
+++ b/docs/opc-publisher/intfilesamples.md
@@ -0,0 +1,267 @@
+# Init file samples
+
+[Home](./readme.md#configuration-via-init-file)
+
+You find here examples that leverage the [init file capabilities](./readme.md#configuration-via-init-file) of OPC Publisher (since 2.9.12).
+
+## Table Of Contents
+
+- [Find and create writers for all machine tools in a server](#find-and-create-writers-for-all-machine-tools-in-a-server)
+- [Add all variables in a server to a single data set writer](#add-all-variables-in-a-server-to-a-single-data-set-writer)
+- [Add machine objects as data set writers](#add-machine-objects-as-data-set-writers)
+- [Create a Web of Things Asset and add a data set writer](#create-a-web-of-things-asset-and-add-a-data-set-writer)
+
+## Find and create writers for all machine tools in a server
+
+This init file uses the [ExpandAndCreateOrUpdateDataSetWriterEntries API](./api.md#expandandcreateorupdatedatasetwriterentries) to generate writers for each machine tool found on the server. A machine tool is an object that compiles to the MachineTool ObjectType as defined in the [OPC 40501-1](https://reference.opcfoundation.org/MachineTool/v102/docs/8.1) (machine tool companion specification).
+
+For this reason, this and the following 2 samples use the publicly hosted [Umati](https://umati.org/) reference server giving a good understanding on how to leverage the OPC UA companion specifications.
+
+``` json
+###
+
+// 3 retries in case of failure, with a delay of 5 seconds between
+// @delay 5
+// @retries 3
+
+// Creates writer entries for all objects that implement the
+// machine tool object type or one of its subtypes on the server
+ExpandAndCreateOrUpdateDataSetWriterEntries_V2
+
+{
+ "entry": {
+ "EndpointUrl": "opc.tcp://opcua.umati.app:4840",
+ "UseSecurity": false,
+ "DataSetWriterGroup": "MachineTools",
+ "OpcNodes": [
+ { "Id": "nsu=http://opcfoundation.org/UA/MachineTool/;i=13" }
+ ]
+ }
+}
+
+###
+
+// Shutdown the publisher in case the expansion failed
+// and let docker restart it. The Fail fast argument
+// provided as json payload.
+# @on-error
+Shutdown_V2
+
+true
+
+###
+```
+
+This results in a result (log) file that shows the result of the execution of the individual methods on the publisher API and that looks like this (the response payload is abbreviated and in any case not indented):
+
+``` json
+###
+
+// 3 retries in case of failure, with a delay of 5 seconds between
+// Creates writer entries for all objects that implement the
+// machine tool object type or one of its subtypes on the server
+ExpandAndCreateOrUpdateDataSetWriterEntries_V2
+200
+
+[{"result":{"DataSetWrite ....... tionMode":"Anonymous"}}]
+
+###
+
+// Shutdown the publisher in case the expansion failed
+// and let docker restart it. The Fail fast argument
+// provided as json payload.
+Shutdown_V2
+// @skipped reason = success
+###
+```
+
+## Add all variables in a server to a single data set writer
+
+The following example uses the same API but with the ObjectsFolder (`i=85`) node as root, drilling down 10 levels and capturing all variables into a single writer entry in the published nodes configuration.
+
+``` json
+###
+
+// 3 retries in case of failure, with a delay of 5 seconds between
+// @delay 5
+// @retries 3
+ExpandAndCreateOrUpdateDataSetWriterEntries_V2
+
+{
+ "entry": {
+ "EndpointUrl": "opc.tcp://opcua.umati.app:4840",
+ "UseSecurity": false,
+ "DataSetWriterGroup": "All",
+ "OpcNodes": [
+ { "Id": "i=85" }
+ ]
+ },
+ "request": {
+ "createSingleWriter": true,
+ "maxDepth": 10,
+ "discardErrors": true
+ }
+}
+
+###
+
+// Shutdown the publisher in case the expansion failed
+// and let docker restart it. The Fail fast argument
+// provided as json payload.
+# @on-error
+Shutdown_V2
+
+true
+
+###
+```
+
+## Add machine objects as data set writers
+
+The following example uses again the same API but with the Machines folder (`nsu=http://opcfoundation.org/UA/Machinery/;i=1001`) node as root capturing all variables into several writer entries in the published nodes configuration.
+
+``` json
+###
+// @delay 5
+ExpandAndCreateOrUpdateDataSetWriterEntries
+
+{
+ "entry": {
+ "EndpointUrl": "opc.tcp://opcua.umati.app:4840",
+ "UseSecurity": false,
+ "DataSetWriterGroup": "Machinery Objects",
+ "OpcNodes": [
+ { "Id": "nsu=http://opcfoundation.org/UA/Machinery/;i=1001" }
+ ]
+ }
+}
+
+// @retries 3
+###
+Shutdown
+// @on-error
+###
+```
+
+## Create a Web of Things Asset and add a data set writer
+
+The following shows how to create a Asset in a [WoT connectivity](https://reference.opcfoundation.org/WoT/v100/docs/) compatible server using a WoT Thing instance model using the [Asset](./api.md#createorupdateasset) API. An compatible sample server can be found [here](https://github.com/OPCFoundation/UA-EdgeTranslator).
+
+> Please note that the asset name inside the configuration must match the `DataSetName` property.
+
+``` json
+###
+CreateOrUpdateAsset
+
+{
+ "entry": {
+ "EndpointUrl": "opc.tcp://localhost:4840",
+ "UseSecurity": true,
+ "DataSetWriterGroup": "Assets",
+ "DataSetName": "MyAsset1"
+ },
+ "waitTime": "00:00:01",
+ "configuration": {
+ "@context": [
+ "https://www.w3.org/2022/wot/td/v1.1"
+ ],
+ "id": "urn:Simple PLC",
+ "securityDefinitions": {
+ "nosec_sc": {
+ "scheme": "nosec"
+ }
+ },
+ "security": [
+ "nosec_sc"
+ ],
+ "@type": [
+ "tm:ThingModel"
+ ],
+ "name": "MyAsset1",
+ "base": "ads://127.0.0.1:8534",
+ "title": "Untitled1",
+ "properties": {
+ "Global_Version.stLibVersion_Tc2_Standard": {
+ "type": "number",
+ "opcua:nodeId": null,
+ "opcua:type": null,
+ "opcua:fieldPath": null,
+ "readOnly": true,
+ "observable": true,
+ "forms": [
+ {
+ "href": "Global_Version.stLibVersion_Tc2_Standard?36",
+ "op": [
+ "readproperty",
+ "observeproperty"
+ ],
+ "type": "xsd:float",
+ "pollingTime": 1000
+ }
+ ]
+ },
+ "Global_Version.stLibVersion_Tc2_System": {
+ "type": "number",
+ "opcua:nodeId": null,
+ "opcua:type": null,
+ "opcua:fieldPath": null,
+ "readOnly": true,
+ "observable": true,
+ "forms": [
+ {
+ "href": "Global_Version.stLibVersion_Tc2_System?36",
+ "op": [
+ "readproperty",
+ "observeproperty"
+ ],
+ "type": "xsd:float",
+ "pollingTime": 1000
+ }
+ ]
+ },
+ "Global_Version.stLibVersion_Tc3_Module": {
+ "type": "number",
+ "opcua:nodeId": null,
+ "opcua:type": null,
+ "opcua:fieldPath": null,
+ "readOnly": true,
+ "observable": true,
+ "forms": [
+ {
+ "href": "Global_Version.stLibVersion_Tc3_Module?36",
+ "op": [
+ "readproperty",
+ "observeproperty"
+ ],
+ "type": "xsd:float",
+ "pollingTime": 1000
+ }
+ ]
+ },
+ "GVL_VAR.temp": {
+ "type": "number",
+ "opcua:nodeId": null,
+ "opcua:type": null,
+ "opcua:fieldPath": null,
+ "readOnly": true,
+ "observable": true,
+ "forms": [
+ {
+ "href": "GVL_VAR.temp?4",
+ "op": [
+ "readproperty",
+ "observeproperty"
+ ],
+ "type": "xsd:float",
+ "pollingTime": 1000
+ }
+ ]
+ }
+ }
+}
+
+###
+# @on-error
+Shutdown_V2
+###
+```
diff --git a/docs/opc-publisher/readme.md b/docs/opc-publisher/readme.md
index 69da9ae313..f493a603bb 100644
--- a/docs/opc-publisher/readme.md
+++ b/docs/opc-publisher/readme.md
@@ -22,6 +22,7 @@ Here you find information about
- [Configuration via Configuration File](#configuration-via-configuration-file)
- [Configuration Schema](#configuration-schema)
- [Writer group configuration](#writer-group-configuration)
+ - [Configuration via init file](#configuration-via-init-file)
- [Sampling and Publishing Interval configuration](#sampling-and-publishing-interval-configuration)
- [Key frames, delta frames and extension fields](#key-frames-delta-frames-and-extension-fields)
- [Status codes](#status-codes)
@@ -347,6 +348,7 @@ OPC Publisher has several interfaces that can be used to configure it.
- [Configuration via configuration file](#configuration-via-configuration-file)
- [Command Line options configuration](./commandline.md)
- [Configuration via API](./directmethods.md)
+- [Configuration via init file](#configuration-via-init-file)
- [How to migrate from previous versions of OPC Publisher](./migrationpath.md)
### Configuration via Configuration File
@@ -583,6 +585,64 @@ Due to historic reasons, by default a session is scoped to a writer group. That
OPC Publisher will try to re-use an existing OPC UA subscription or create a new one per `DataSetWriter`.
+### Configuration via init file
+
+OPC Publisher can be configured remotely using its [writer](./api.md#writer) and [node](./api.md#configuration) configuration API. This API can be invoked via HTTP, MQTT (RPC), in many cases through IoT Hub direct methods, but also using OPC Publisher's init file feature.
+
+The init file can be specified using the [command line option](./commandline.md) `--pi, --initfile`. The file can be updated while OPC Publisher is running, in which case the new file content will be executed. The file will not be run if it has not changed. This applies also across restarts. However, this requires that the response file (see the `--il, --initlog` [argument](./commandline.md)) is writeable.
+
+The init file format follows the [.http file format](https://learn.microsoft.com/aspnet/core/test/http-files) with the additional exception that scripting, variables, and `{{ }}` templates are not supported.
+
+While the [method line](https://learn.microsoft.com/aspnet/core/test/http-files#requests) can start with a `HTTPMethod` and end with a `HTTPVersion`, they are effectively discarded at the moment. The `URL` must be a direct method name as specified in the API documentation, e.g. `AddOrUpdateEndpoint_V1`. While headers can be provided, the only relevant one is `Content-Type` which defaults to `application/json`. In addition to the documented format, the init file format supported by OPC Publisher supports the following additional request directives which can be provided after a comment (# or //):
+
+| Directive | Description |
+| --------- | ----------- |
+| **@no-log** | Disable logging for this request after this directive.
This directive must be applied for every request and on the first line so that nothing is emitted to the init log. |
+| **@timeout** | Timeout for the request.
If the request times out it will be an error and all further requests are not sent. |
+| **@retries** | Retry this number of times in case of an error.
An error is any request that returns with status code >= 400. |
+| **@delay** | Delay before executing a request.
If retries are specified the delay applies again before every retry. |
+| **@on-error** | Invoke the request only when the previous request failed.
If the previous request has **@continue-on-error** directive this request will not be executed. If the request succeeds the next request after is run. |
+| **@continue-on-error** | Continue to next request even if the request failed.
The default behavior is to stop execution of requests except for the next request with **@on-error** directive. |
+
+The **@on-error** condition can be used as an error handler e.g. to call the [Shutdown](./directmethods.md#shutdown_v2) method. If the restart is immediate, the init file will be execute again after restart. A delay can throttle these restarts. An example init file is shown here:
+
+``` json
+###
+
+// 3 retries in case of failure, with a delay of 5 seconds between
+// @delay 5
+// @retries 3
+
+// Creates writer entries for all objects that implement the
+// machine tool object type or one of its subtypes on this server
+ExpandAndCreateOrUpdateDataSetWriterEntries_V2
+
+{
+ "entry": {
+ "EndpointUrl": "opc.tcp://opcua.umati.app:4840",
+ "UseSecurity": false,
+ "DataSetWriterGroup": "MachineTools",
+ "OpcNodes": [
+ { "Id": "nsu=http://opcfoundation.org/UA/MachineTool/;i=13" }
+ ]
+ }
+}
+
+###
+
+// Shutdown the publisher in case the expansion failed
+// and let docker restart it. The Fail fast argument
+// provided as json payload.
+# @on-error
+Shutdown_V2
+
+true
+
+###
+```
+
+More init file examples with explanations can be found [here](./intfilesamples.md).
+
### Sampling and Publishing Interval configuration
The OPC UA reference specification provides a detailed overview of the OPC UA [monitored item](https://reference.opcfoundation.org/Core/Part4/v104/docs/5.12) and [subscription](https://reference.opcfoundation.org/Core/Part4/v104/docs/5.13.1) service model.
@@ -676,7 +736,7 @@ The behavior of heartbeat can be fine tuned using the `--hbb, --heartbeatbehavio
"HeartbeatBehavior": "...",
```
-option of the node entry. The behavior can be set to watch dog behavior with Last Known Value (`WatchdogLKV`, which is the default) or Last Known Good (`WatchdogLKG`) semantics. A last known good value has either a status code of `Good` or a valid value (!= Null) and not a bad status code (which covers other Good or Uncertain status codes). Bad values are not causing heartbeat messages in LKG mode.
+option of the node entry. The behavior can be set to watch dog behavior with Last Known Value (`WatchdogLKV`, which is the default) or Last Known Good (`WatchdogLKG`) semantics. A last known good value has either a status code of `Good` or a valid value (!= Null) and not a bad status code (which covers other Good or Uncertain status codes). Bad values are not causing heartbeat messages in LKG mode.
A continuous periodic sending of the last known value (`PeriodicLKV`) or last good value (`PeriodicLKG`) can also be selected. In some cases periodic reporting is all that is needed, and the actual value read that is reported out of period should be dropped. Use the `PeriodicLKVDropValue` or `PeriodicLKGDropValue` behavior to achieve this behavior. The outcome is similar to the [cyclic read](#cyclic-reading-client-side-sampling) mode but using a periodic timer over server side sampled nodes.
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Models/src/Azure.IIoT.OpcUa.Publisher.Models.csproj b/src/Azure.IIoT.OpcUa.Publisher.Models/src/Azure.IIoT.OpcUa.Publisher.Models.csproj
index 84ba60e44e..4a9ebbd880 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Models/src/Azure.IIoT.OpcUa.Publisher.Models.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher.Models/src/Azure.IIoT.OpcUa.Publisher.Models.csproj
@@ -8,12 +8,7 @@
enable
-
+
-
-
-
-
-
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Models/src/PublishedNodeExpansionModel.cs b/src/Azure.IIoT.OpcUa.Publisher.Models/src/PublishedNodeExpansionModel.cs
index f017056ec1..c067a6b9e6 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Models/src/PublishedNodeExpansionModel.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher.Models/src/PublishedNodeExpansionModel.cs
@@ -105,14 +105,19 @@ public sealed record class PublishedNodeExpansionModel
public uint? MaxDepth { get; init; }
///
- /// If the depth is not limited and the node is a
- /// type definition id set this flag to true to find
- /// only the first instance of this type from the
- /// object root.
+ /// If true, treats instance nodes found just like
+ /// objects that need to be expanded. In case of a
+ /// companion spec object type this should be set to
+ /// false, flattening the structure into a single
+ /// writer that represents the object in its entirety.
+ /// However, when using generic interfaces that can
+ /// be implemented across objects in the address
+ /// space and only its variables are important, it
+ /// might be useful to set this to true.
///
- [DataMember(Name = "stopAtFirstFoundInstance", Order = 7,
+ [DataMember(Name = "doNotFlattenTypeInstance", Order = 7,
EmitDefaultValue = false)]
- public bool StopAtFirstFoundInstance { get; init; }
+ public bool DoNotFlattenTypeInstance { get; init; }
///
/// Errors are silently discarded and only
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj
index 108f33c683..5dff748f98 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj
@@ -17,9 +17,9 @@
all
runtime; build; native; contentfiles; analyzers
-
-
-
+
+
+
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Azure.IIoT.OpcUa.Publisher.Module.Cli.csproj b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Azure.IIoT.OpcUa.Publisher.Module.Cli.csproj
index 9ca82b5fea..9141d11d39 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Azure.IIoT.OpcUa.Publisher.Module.Cli.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Azure.IIoT.OpcUa.Publisher.Module.Cli.csproj
@@ -7,8 +7,8 @@
true
-
-
+
+
@@ -22,11 +22,14 @@
Always
-
-
-
-
-
-
+
+ Always
+
+
+
+
+
+ Always
+
\ No newline at end of file
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Initfiles/MachineTools.init b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Initfiles/MachineTools.init
new file mode 100644
index 0000000000..9c564b5bc1
--- /dev/null
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Initfiles/MachineTools.init
@@ -0,0 +1,35 @@
+###
+
+// 3 retries in case of failure, with a delay of 5 seconds between
+// @delay 5
+// @retries 3
+
+// Creates writer entries for all objects that implement the
+// machine tool object type or one of its subtypes on the
+// umati reference server
+ExpandAndCreateOrUpdateDataSetWriterEntries_V2
+
+{
+ "entry": {
+ "EndpointUrl": "opc.tcp://opcua.umati.app:4840",
+ "UseSecurity": false,
+ "DataSetWriterGroup": "MachineTools",
+ "OpcNodes": [
+ { "Id": "nsu=http://opcfoundation.org/UA/MachineTool/;i=13" }
+ ]
+ },
+ "request": {
+ }
+}
+
+###
+
+// Shutdown the publisher in case the expansion failed
+// and let docker restart it. The Fail fast argument
+// provided as json payload.
+# @on-error
+Shutdown_V2
+
+true
+
+###
\ No newline at end of file
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Initfiles/Machinery.init b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Initfiles/Machinery.init
new file mode 100644
index 0000000000..47a6b71564
--- /dev/null
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Initfiles/Machinery.init
@@ -0,0 +1,20 @@
+###
+// @delay 5
+ExpandAndCreateOrUpdateDataSetWriterEntries
+
+{
+ "entry": {
+ "EndpointUrl": "opc.tcp://opcua.umati.app:4840",
+ "UseSecurity": false,
+ "DataSetWriterGroup": "Machinery Objects",
+ "OpcNodes": [
+ { "Id": "nsu=http://opcfoundation.org/UA/Machinery/;i=1001" }
+ ]
+ }
+}
+
+// @retries 3
+###
+Shutdown
+// @on-error
+###
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Initfiles/Objects.init b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Initfiles/Objects.init
new file mode 100644
index 0000000000..466fe6da59
--- /dev/null
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Initfiles/Objects.init
@@ -0,0 +1,21 @@
+###
+// @delay 5
+// @retries 3
+ExpandAndCreateOrUpdateDataSetWriterEntries_V2
+
+{
+ "entry": {
+ "EndpointUrl": "{{EndpointUrl}}",
+ "UseSecurity": false,
+ "DataSetWriterGroup": "Objects",
+ "OpcNodes": [
+ { "Id": "i=85" }
+ ]
+ }
+}
+
+###
+# @on-error
+Shutdown_V2
+true
+###
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Initfiles/Variables.init b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Initfiles/Variables.init
new file mode 100644
index 0000000000..b66e94b162
--- /dev/null
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Initfiles/Variables.init
@@ -0,0 +1,26 @@
+###
+// @delay 5
+// @retries 3
+ExpandAndCreateOrUpdateDataSetWriterEntries_V2
+
+{
+ "entry": {
+ "EndpointUrl": "{{EndpointUrl}}",
+ "UseSecurity": false,
+ "DataSetWriterGroup": "Variables",
+ "OpcNodes": [
+ { "Id": "i=85" }
+ ]
+ },
+ "request": {
+ "createSingleWriter": true,
+ "maxDepth": 10,
+ "discardErrors": true
+ }
+}
+
+###
+# @on-error
+Shutdown_V2
+true
+###
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Profiles/Empty.json b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Profiles/Empty.json
index bb18b89a9d..a081b3ae16 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Profiles/Empty.json
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Profiles/Empty.json
@@ -1,7 +1,2 @@
[
- {
- "EndpointUrl": "{{EndpointUrl}}",
- "EndpointSecurityMode": "Sign",
- "OpcNodes": []
- }
]
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Profiles/NoNodes.json b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Profiles/NoNodes.json
new file mode 100644
index 0000000000..bb18b89a9d
--- /dev/null
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Profiles/NoNodes.json
@@ -0,0 +1,7 @@
+[
+ {
+ "EndpointUrl": "{{EndpointUrl}}",
+ "EndpointSecurityMode": "Sign",
+ "OpcNodes": []
+ }
+]
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Program.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Program.cs
index f26ec8d3d6..a73d956a3f 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Program.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Program.cs
@@ -53,6 +53,8 @@ public static void Main(string[] args)
var instances = 1;
string? connectionString = null;
string? publishProfile = null;
+ string? publishInitProfile = null;
+ string? publishInitFilePath = null;
string? publishedNodesFilePath = null;
var useNullTransport = false;
string? dumpMessages = null;
@@ -155,6 +157,16 @@ public static void Main(string[] args)
break;
}
throw new ArgumentException("Missing argument for --publish-profile");
+ case "-I":
+ case "--init-profile":
+ i++;
+ if (i < args.Length)
+ {
+ publishInitProfile = args[i];
+ withServer = true;
+ break;
+ }
+ throw new ArgumentException("Missing argument for --init-profile");
case "--pnjson":
i++;
if (i < args.Length)
@@ -163,6 +175,14 @@ public static void Main(string[] args)
break;
}
throw new ArgumentException("Missing argument for --pnjson");
+ case "--init":
+ i++;
+ if (i < args.Length)
+ {
+ publishInitFilePath = args[i];
+ break;
+ }
+ throw new ArgumentException("Missing argument for --init");
case "--":
break;
default:
@@ -235,19 +255,36 @@ public static void Main(string[] args)
{
if (dumpMessages != null)
{
- hostingTask = DumpMessagesAsync(dumpMessages, publishProfile, loggerFactory,
- TimeSpan.FromMinutes(2), scaleunits, dumpMessagesOutput, args, cts.Token);
+ hostingTask = DumpMessagesAsync(dumpMessages, publishProfile, publishInitProfile,
+ loggerFactory, TimeSpan.FromMinutes(2), scaleunits, dumpMessagesOutput, args,
+ cts.Token);
}
else if (!withServer)
{
+ if (publishInitFilePath != null && !File.Exists(publishInitFilePath))
+ {
+ publishInitFilePath = $"./Initfiles/{publishInitFilePath}.init";
+ if (File.Exists(publishInitFilePath))
+ {
+ const string copyTo = "profile.init";
+ File.Copy(publishInitFilePath, copyTo, true);
+ File.SetLastWriteTimeUtc(copyTo, DateTime.UtcNow);
+ publishInitFilePath = copyTo;
+ }
+ else
+ {
+ publishInitFilePath = null;
+ }
+ }
hostingTask = HostAsync(connectionString, loggerFactory,
deviceId, moduleId, args, reverseConnectPort, !checkTrust,
- publishedNodesFilePath, cts.Token);
+ publishInitFilePath, publishedNodesFilePath, cts.Token);
}
else
{
- hostingTask = WithServerAsync(connectionString, loggerFactory, deviceId, moduleId, args,
- publishProfile, scaleunits, !checkTrust, reverseConnectPort, cts.Token);
+ hostingTask = WithServerAsync(connectionString, loggerFactory, deviceId,
+ moduleId, args, publishProfile, publishInitProfile, scaleunits,
+ !checkTrust, reverseConnectPort, cts.Token);
}
while (!cts.Token.IsCancellationRequested)
@@ -295,11 +332,13 @@ public static void Main(string[] args)
///
///
///
+ ///
///
///
private static async Task HostAsync(string? connectionString, ILoggerFactory loggerFactory,
string deviceId, string moduleId, string[] args, int? reverseConnectPort,
- bool acceptAll, string? publishedNodesFilePath = null, CancellationToken ct = default)
+ bool acceptAll, string? publishInitFile, string? publishedNodesFilePath = null,
+ CancellationToken ct = default)
{
var logger = loggerFactory.CreateLogger();
logger.LogInformation("Create or retrieve connection string for {DeviceId} {ModuleId}...",
@@ -332,7 +371,7 @@ private static async Task HostAsync(string? connectionString, ILoggerFactory log
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
var running = RunAsync(logger, deviceId, moduleId, args, acceptAll, cs,
- reverseConnectPort, publishedNodesFilePath, cts.Token);
+ reverseConnectPort, publishedNodesFilePath, publishInitFile, cts.Token);
Console.WriteLine("Publisher running (Press P to restart)...");
await _restartPublisher.WaitAsync(ct).ConfigureAwait(false);
@@ -346,12 +385,17 @@ private static async Task HostAsync(string? connectionString, ILoggerFactory log
static async Task RunAsync(ILogger logger, string deviceId, string moduleId, string[] args,
bool acceptAll, ConnectionString? cs, int? reverseConnectPort, string? publishedNodesFilePath,
- CancellationToken ct)
+ string? publishInitFile, CancellationToken ct)
{
logger.LogInformation("Starting publisher module {DeviceId} {ModuleId}...",
deviceId, moduleId);
var arguments = args.ToList();
+ if (publishInitFile != null)
+ {
+ arguments.Add($"--pi={publishInitFile}");
+ }
+
if (publishedNodesFilePath != null)
{
arguments.Add($"--pf={publishedNodesFilePath}");
@@ -394,25 +438,37 @@ static async Task RunAsync(ILogger logger, string deviceId, string moduleId, str
///
///
///
+ ///
///
///
///
///
private static async Task WithServerAsync(string? connectionString, ILoggerFactory loggerFactory,
- string deviceId, string moduleId, string[] args, string? publishProfile, uint scaleunits, bool acceptAll,
- int? reverseConnectPort, CancellationToken ct)
+ string deviceId, string moduleId, string[] args, string? publishProfile, string? publishInitProfile,
+ uint scaleunits, bool acceptAll, int? reverseConnectPort, CancellationToken ct)
{
try
{
// Start test server
using (var server = new ServerWrapper(scaleunits, loggerFactory, reverseConnectPort))
{
+ var endpointUrl = $"opc.tcp://localhost:{server.Port}/UA/SampleServer";
+
+ var publishInitFile = await LoadInitFile(server, publishInitProfile,
+ endpointUrl, ct).ConfigureAwait(false);
+
+ if (publishInitFile != null && publishProfile == null)
+ {
+ publishProfile = "Empty";
+ }
+
var publishedNodesFilePath = await LoadPnJson(server, publishProfile,
- $"opc.tcp://localhost:{server.Port}/UA/SampleServer", ct).ConfigureAwait(false);
+ endpointUrl, ct).ConfigureAwait(false);
// Start publisher module
await HostAsync(connectionString, loggerFactory, deviceId, moduleId,
- args, reverseConnectPort, acceptAll, publishedNodesFilePath, ct).ConfigureAwait(false);
+ args, reverseConnectPort, acceptAll, publishInitFile, publishedNodesFilePath,
+ ct).ConfigureAwait(false);
}
}
catch (OperationCanceledException) { }
@@ -423,6 +479,7 @@ await HostAsync(connectionString, loggerFactory, deviceId, moduleId,
///
///
///
+ ///
///
///
///
@@ -431,8 +488,8 @@ await HostAsync(connectionString, loggerFactory, deviceId, moduleId,
///
///
private static async Task DumpMessagesAsync(string messageMode, string? publishProfile,
- ILoggerFactory loggerFactory, TimeSpan duration, uint scaleunits, string? dumpMessagesOutput,
- string[] args, CancellationToken ct)
+ string? publishInitProfile, ILoggerFactory loggerFactory, TimeSpan duration,
+ uint scaleunits, string? dumpMessagesOutput, string[] args, CancellationToken ct)
{
try
{
@@ -463,20 +520,33 @@ private static async Task DumpMessagesAsync(string messageMode, string? publishP
continue;
}
Directory.CreateDirectory(outputFolder);
- await DumpPublishingProfiles(outputFolder, messageProfile, publishProfile).ConfigureAwait(false);
+ await DumpPublishingProfiles(outputFolder, messageProfile, publishProfile,
+ publishInitProfile).ConfigureAwait(false);
}
}
catch (OperationCanceledException) { }
// Dump message profile for all publishing profiles
- async Task DumpPublishingProfiles(string rootFolder, MessagingProfile messageProfile, string? profile)
+ async Task DumpPublishingProfiles(string rootFolder, MessagingProfile messageProfile,
+ string? profile, string? publishInitProfile)
{
+ if (publishInitProfile != null)
+ {
+ var outputFolder = Path.Combine(rootFolder, publishInitProfile);
+
+ await DumpMessagesForDuration(outputFolder, "Empty", messageProfile,
+ publishInitProfile, args).ConfigureAwait(false);
+
+ return;
+ }
+
foreach (var publishProfile in Directory.EnumerateFiles("./Profiles", "*.json"))
{
var publishProfileName = Path.GetFileNameWithoutExtension(publishProfile);
if (profile == null &&
(publishProfileName.StartsWith("Unified", StringComparison.OrdinalIgnoreCase) ||
- publishProfileName.StartsWith("Empty", StringComparison.OrdinalIgnoreCase)))
+ publishProfileName.StartsWith("Empty", StringComparison.OrdinalIgnoreCase) ||
+ publishProfileName.StartsWith("NoNodes", StringComparison.OrdinalIgnoreCase)))
{
continue;
}
@@ -491,13 +561,13 @@ async Task DumpPublishingProfiles(string rootFolder, MessagingProfile messagePro
continue;
}
Directory.CreateDirectory(outputFolder);
- await DumpMessagesForDuration(outputFolder, publishProfile,
- messageProfile, args).ConfigureAwait(false);
+ await DumpMessagesForDuration(outputFolder, publishProfile, messageProfile,
+ null, args).ConfigureAwait(false);
}
}
async Task DumpMessagesForDuration(string outputFolder, string publishProfile,
- MessagingProfile messageProfile, string[] args)
+ MessagingProfile messageProfile, string? publishInitProfile, string[] args)
{
using var runtime = new CancellationTokenSource(duration);
try
@@ -507,26 +577,31 @@ async Task DumpMessagesForDuration(string outputFolder, string publishProfile,
var name = Path.GetFileNameWithoutExtension(publishProfile);
Console.Title = $"Dumping {messageProfile} for {name}...";
await RunAsync(loggerFactory, publishProfile, messageProfile,
- outputFolder, scaleunits, args, linkedToken.Token).ConfigureAwait(false);
+ outputFolder, scaleunits, args, publishInitProfile, linkedToken.Token).ConfigureAwait(false);
}
catch (OperationCanceledException) when (runtime.IsCancellationRequested) { }
}
static async Task RunAsync(ILoggerFactory loggerFactory, string publishProfile,
MessagingProfile messageProfile, string outputFolder, uint scaleunits, string[] args,
- CancellationToken ct)
+ string? publishInitProfile, CancellationToken ct)
{
// Start test server
using (var server = new ServerWrapper(scaleunits, loggerFactory, null))
{
var name = Path.GetFileNameWithoutExtension(publishProfile);
- var publishedNodesFilePath = await LoadPnJson(server, name,
- $"opc.tcp://localhost:{server.Port}/UA/SampleServer", ct).ConfigureAwait(false);
+ var endpointUrl = $"opc.tcp://localhost:{server.Port}/UA/SampleServer";
+
+ var publishedNodesFilePath = await LoadPnJson(server, name, endpointUrl,
+ ct).ConfigureAwait(false);
if (publishedNodesFilePath == null)
{
return;
}
+ var publishInitFile = await LoadInitFile(server, name, endpointUrl,
+ ct).ConfigureAwait(false);
+
//
// Check whether the profile overrides the messaging mode, then set it to the desired
// one regardless of whether it will work or not
@@ -553,6 +628,10 @@ static async Task RunAsync(ILoggerFactory loggerFactory, string publishProfile,
$"-o={outputFolder}",
"--aa"
};
+ if (publishInitFile != null)
+ {
+ arguments.Add($"--pi={publishInitFile}");
+ }
args.ForEach(a => arguments.Add(a));
await Publisher.Module.Program.RunAsync(arguments.ToArray(), ct).ConfigureAwait(false);
}
@@ -592,6 +671,27 @@ await File.WriteAllTextAsync(publishedNodesFilePath,
return null;
}
+ private static async Task LoadInitFile(ServerWrapper server, string? initProfile,
+ string endpointUrl, CancellationToken ct)
+ {
+ const string initFile = "profile.init";
+ if (!string.IsNullOrEmpty(initProfile))
+ {
+ var publishedNodesFile = $"./Initfiles/{initProfile}.init";
+ if (!File.Exists(publishedNodesFile))
+ {
+ throw new ArgumentException($"Init profile {initProfile} does not exist");
+ }
+ await File.WriteAllTextAsync(initFile,
+ (await File.ReadAllTextAsync(publishedNodesFile, ct).ConfigureAwait(false))
+ .Replace("{{EndpointUrl}}", endpointUrl,
+ StringComparison.Ordinal), ct).ConfigureAwait(false);
+
+ return initFile;
+ }
+ return null;
+ }
+
///
/// Add or get module identity
///
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Azure.IIoT.OpcUa.Publisher.Module.csproj b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Azure.IIoT.OpcUa.Publisher.Module.csproj
index b9693163bf..2bc360bd42 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Azure.IIoT.OpcUa.Publisher.Module.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Azure.IIoT.OpcUa.Publisher.Module.csproj
@@ -33,15 +33,15 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/WriterController.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/WriterController.cs
index d6351560d3..b1a11c0682 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/WriterController.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/WriterController.cs
@@ -22,6 +22,7 @@ namespace Azure.IIoT.OpcUa.Publisher.Module.Controllers
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+ using Furly.Extensions.Serializers;
///
///
@@ -56,12 +57,15 @@ public class WriterController : ControllerBase, IMethodController
///
///
///
+ ///
public WriterController(IPublishedNodesServices publisher,
- IConfigurationServices configuration, IAssetConfiguration assets)
+ IConfigurationServices configuration, IAssetConfiguration assets,
+ IJsonSerializer serializer)
{
_publisher = publisher;
_configuration = configuration;
_assets = assets;
+ _serializer = serializer;
}
///
@@ -493,13 +497,13 @@ public IAsyncEnumerable> ExpandAndCrea
}
///
- /// CreateOrUpdateAsset
+ /// CreateOrUpdateAsset (With binary configuration)
///
///
/// Creates an asset from the entry in the request and the configuration provided
- /// in the Web of Things Asset configuration file. The entry must contain a data
- /// set name which will be used as the asset name. The writer can stay empty. It
- /// will be set to the asset id on successful return. The server must support the
+ /// in the Web of Things Asset configuration byte buffer. The entry must contain a
+ /// data set name which will be used as the asset name. The writer can stay empty.
+ /// It will be set to the asset id on successful return. The server must support the
/// WoT profile per .
/// The asset will be created and the configuration updated to reference it. A
/// wait time can be provided as optional query parameter to wait until the server
@@ -520,7 +524,8 @@ public IAsyncEnumerable> ExpandAndCrea
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status408RequestTimeout)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
[HttpPost("assets/create")]
- public async Task> CreateOrUpdateAssetAsync(
+ [Ignore]
+ public async Task> CreateOrUpdateAsset2Async(
[FromBody][Required] PublishedNodeCreateAssetRequestModel request,
CancellationToken ct = default)
{
@@ -528,6 +533,48 @@ public async Task> CreateOrUpdateAsset
return await _assets.CreateOrUpdateAssetAsync(request, ct).ConfigureAwait(false);
}
+ ///
+ /// CreateOrUpdateAsset
+ ///
+ ///
+ /// Creates an asset from the entry in the request and the configuration provided
+ /// in the Web of Things Asset json configuration property. The entry must contain
+ /// a data set name which will be used as the asset name. The writer can stay empty.
+ /// It will be set to the asset id on successful return. The server must support the
+ /// WoT profile per .
+ /// The asset will be created and the configuration updated to reference it. A
+ /// wait time can be provided as optional query parameter to wait until the server
+ /// has settled after uploading the configuration.
+ ///
+ /// The contains the entry and WoT file to configure the
+ /// server to expose the asset.
+ ///
+ ///
+ /// is null.
+ /// The asset was created
+ /// The passed in information is invalid
+ /// The operation timed out.
+ /// An unexpected error occurred
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status408RequestTimeout)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
+ [HttpPost("assets")]
+ public async Task> CreateOrUpdateAssetAsync(
+ [FromBody][Required] PublishedNodeCreateAssetRequestModel request,
+ CancellationToken ct = default)
+ {
+ ArgumentNullException.ThrowIfNull(request);
+ return await _assets.CreateOrUpdateAssetAsync(new PublishedNodeCreateAssetRequestModel
+ {
+ Entry = request.Entry,
+ WaitTime = request.WaitTime,
+ Header = request.Header,
+ Configuration = _serializer.SerializeObjectToMemory(request.Configuration).ToArray()
+ }, ct).ConfigureAwait(false);
+ }
+
///
/// GetAllAssets
///
@@ -602,5 +649,6 @@ public async Task DeleteAssetAsync(
private readonly IPublishedNodesServices _publisher;
private readonly IConfigurationServices _configuration;
private readonly IAssetConfiguration _assets;
+ private readonly IJsonSerializer _serializer;
}
}
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/CommandLine.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/CommandLine.cs
index 1b4227728b..5646bc3187 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/CommandLine.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/CommandLine.cs
@@ -63,7 +63,7 @@ public CommandLine(string[] args, CommandLineLogger? logger = null)
_ => showHelp = true },
// Publisher configuration options
- { $"f|pf|publishfile=|{PublisherConfig.PublishedNodesFileKey}=",
+ { $"f|pf=|publishfile=|{PublisherConfig.PublishedNodesFileKey}=",
"The name of the file containing the configuration of the nodes to be published as well as the information to connect to the OPC UA server sources.\nThis file is also used to persist changes made through the control plane, e.g., through IoT Hub device method calls.\nWhen no file is specified a default `publishednodes.json` file is created in the working directory.\nDefault: `publishednodes.json`\n",
s => this[PublisherConfig.PublishedNodesFileKey] = s },
{ $"cf|createifnotexist:|{PublisherConfig.CreatePublishFileIfNotExistKey}:",
@@ -81,6 +81,12 @@ public CommandLine(string[] args, CommandLineLogger? logger = null)
{ $"s|site=|{PublisherConfig.SiteIdKey}=",
"Sets the site name of the publisher module.\nDefault: `not set` \n",
s => this[PublisherConfig.SiteIdKey] = s},
+ { $"pi|initfile:|{Configuration.FileSystem.InitFilePathKey}:",
+ "A file from which to read initialization instructions.\nUse this option to have OPC Publisher run a set of method calls found in this file.\nThe file must be formatted using a subset of the .http/.rest file format without support for indentation, scripting or environment variables.\nDefault: `not set` (disabled). If only a file name is specified, it is loaded from the path specifed using `--pn`. If just the argument is provided without a value the default is `publishednodes.init`.\n",
+ pi => this[Configuration.FileSystem.InitFilePathKey] = pi ?? " " },
+ { $"il|initlog=|{Configuration.FileSystem.InitLogFileKey}=",
+ "A file into which the results of the initialization instructions are written.\nOnly valid if `--pi` option is specified.\nDefault: If a init file is set using `--pi`, it is appended with the `.log` extension. If just a file name is used, the file is created in the same folder as the init file configured using the `--pi` command line option.\n",
+ il => this[Configuration.FileSystem.InitLogFileKey] = il },
{ $"rs|runtimestatereporting:|{PublisherConfig.EnableRuntimeStateReportingKey}:",
"Enable that when publisher starts or restarts it reports its runtime state using a restart message.\nDefault: `false` (disabled)\n",
(bool? b) => this[PublisherConfig.EnableRuntimeStateReportingKey] = b?.ToString() ?? "True"},
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Configuration.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Configuration.cs
index cfe3e68df6..42d9993727 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Configuration.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Configuration.cs
@@ -15,6 +15,7 @@ namespace Azure.IIoT.OpcUa.Publisher.Module.Runtime
using Furly.Extensions.Dapr;
using Furly.Extensions.Logging;
using Furly.Extensions.Messaging.Runtime;
+ using Furly.Extensions.Rpc.Runtime;
using Furly.Extensions.Mqtt;
using Furly.Tunnel.Router.Services;
using Microsoft.AspNetCore.Builder;
@@ -36,6 +37,7 @@ namespace Azure.IIoT.OpcUa.Publisher.Module.Runtime
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
+ using System.IO;
///
/// Configuration extensions
@@ -177,7 +179,7 @@ public static void AddEventHubsClient(this ContainerBuilder builder,
public static void AddFileSystemEventClient(this ContainerBuilder builder,
IConfiguration configuration)
{
- var fsOptions = new FileSystemOptions();
+ var fsOptions = new FileSystemEventClientOptions();
new FileSystem(configuration).Configure(fsOptions);
if (fsOptions.OutputFolder != null)
{
@@ -191,6 +193,24 @@ public static void AddFileSystemEventClient(this ContainerBuilder builder,
}
}
+ ///
+ /// Add file system rpc server
+ ///
+ ///
+ ///
+ public static void AddFileSystemRpcServer(this ContainerBuilder builder,
+ IConfiguration configuration)
+ {
+ var fsOptions = new FileSystemRpcServerOptions();
+ new FileSystem(configuration).Configure(fsOptions);
+ if (fsOptions.RequestFilePath != null)
+ {
+ builder.AddFileSystemRpcServer();
+ builder.RegisterType()
+ .AsImplementedInterfaces();
+ }
+ }
+
///
/// Add http event client
///
@@ -199,7 +219,7 @@ public static void AddFileSystemEventClient(this ContainerBuilder builder,
public static void AddHttpEventClient(this ContainerBuilder builder,
IConfiguration configuration)
{
- var httpOptions = new HttpOptions();
+ var httpOptions = new HttpEventClientOptions();
new Http(configuration).Configure(httpOptions);
if (httpOptions.HostName != null)
{
@@ -812,7 +832,7 @@ public Dapr(IConfiguration configuration)
///
/// Configure the http event client
///
- internal sealed class Http : ConfigureOptionBase
+ internal sealed class Http : ConfigureOptionBase
{
public const string HttpConnectionStringKey = "HttpConnectionString";
public const string WebHookHostUrlKey = "WebHookHostUrl";
@@ -822,7 +842,7 @@ internal sealed class Http : ConfigureOptionBase
public const string PutKey = "Put";
///
- public override void Configure(string? name, HttpOptions options)
+ public override void Configure(string? name, HttpEventClientOptions options)
{
var httpConnectionString = GetStringOrDefault(HttpConnectionStringKey);
if (httpConnectionString != null)
@@ -907,12 +927,67 @@ public OpenApi(IConfiguration configuration)
///
/// Configure the file based event client
///
- internal sealed class FileSystem : ConfigureOptionBase
+ internal sealed class FileSystem : ConfigureOptionBase,
+ IConfigureOptions,
+ IConfigureNamedOptions
{
public const string OutputRootKey = "OutputRoot";
+ public const string InitFilePathKey = "InitFilePath";
+ public const string InitLogFileKey = "InitLogFile";
+
+ ///
+ public void Configure(FileSystemRpcServerOptions options)
+ {
+ Configure(null, options);
+ }
+
+ ///
+ public void Configure(string? name, FileSystemRpcServerOptions options)
+ {
+ var publishedNodesFile = GetStringOrDefault(
+ PublisherConfig.PublishedNodesFileKey);
+ var rootFolder = Path.GetDirectoryName(publishedNodesFile)
+ ?? Environment.CurrentDirectory;
+
+ options.RequestFilePath ??= GetStringOrDefault(InitFilePathKey);
+ if (options.RequestFilePath == null)
+ {
+ return;
+ }
+
+ if (options.RequestFilePath.Trim().Length == 0)
+ {
+ // We use pn.json file path and publishednodes.init file name
+ options.RequestFilePath = Path.Combine(rootFolder,
+ "publishednodes.init");
+ }
+
+ // Just file?
+ else if (string.IsNullOrEmpty(
+ Path.GetDirectoryName(options.RequestFilePath)))
+ {
+ options.RequestFilePath = Path.Combine(rootFolder,
+ options.RequestFilePath!);
+ }
+
+ options.ResponseFilePath ??= GetStringOrDefault(InitLogFileKey);
+ if (options.ResponseFilePath == null && options.RequestFilePath != null)
+ {
+ options.ResponseFilePath = options.RequestFilePath + ".log";
+ }
+
+ // Just file?
+ else if (string.IsNullOrEmpty(
+ Path.GetDirectoryName(options.ResponseFilePath)))
+ {
+ options.ResponseFilePath = Path.Combine(Path.GetDirectoryName(
+ options.RequestFilePath) ?? Environment.CurrentDirectory,
+ options.ResponseFilePath!);
+ }
+ }
///
- public override void Configure(string? name, FileSystemOptions options)
+ public override void Configure(string? name, FileSystemEventClientOptions options)
{
options.OutputFolder ??= GetStringOrDefault(OutputRootKey);
}
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Startup.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Startup.cs
index cc8ce718f9..2bdcd3e708 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Startup.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Startup.cs
@@ -166,6 +166,7 @@ public virtual void ConfigureContainer(ContainerBuilder builder)
builder.AddNullEventClient();
builder.AddFileSystemEventClient(Configuration);
+ builder.AddFileSystemRpcServer(Configuration);
builder.AddHttpEventClient(Configuration);
builder.AddDaprPubSubClient(Configuration);
builder.AddEventHubsClient(Configuration);
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Azure.IIoT.OpcUa.Publisher.Module.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Azure.IIoT.OpcUa.Publisher.Module.Tests.csproj
index efab86844c..10dfc657c0 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Azure.IIoT.OpcUa.Publisher.Module.Tests.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Azure.IIoT.OpcUa.Publisher.Module.Tests.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Clients/ConfigurationServicesRestClient.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Clients/ConfigurationServicesRestClient.cs
index 0e79a6d57f..82d3fd11d1 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Clients/ConfigurationServicesRestClient.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Clients/ConfigurationServicesRestClient.cs
@@ -16,12 +16,14 @@ namespace Azure.IIoT.OpcUa.Publisher.Module.Tests.Clients
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+ using static Grpc.Core.Metadata;
///
/// Implementation of file system services over http
///
public sealed class ConfigurationServicesRestClient : IConfigurationServices,
- IAssetConfiguration, IAssetConfiguration
+ IAssetConfiguration, IAssetConfiguration,
+ IAssetConfiguration
{
///
/// Create service client
@@ -96,14 +98,16 @@ public async Task> CreateOrUpdateAsset
requestWithBuffer, _serializer, ct: ct).ConfigureAwait(false);
}
- ///
- public IAsyncEnumerable> GetAllAssetsAsync(
- PublishedNodesEntryModel entry, RequestHeaderModel header, CancellationToken ct)
+ public async Task> CreateOrUpdateAssetAsync(
+ PublishedNodeCreateAssetRequestModel request, CancellationToken ct)
{
- ArgumentNullException.ThrowIfNull(entry);
- var uri = new Uri($"{_serviceUri}/v2/writer/assets/list");
- return _httpClient.PostStreamAsync>(uri,
- RequestBody(entry, header), _serializer, ct: ct);
+ ArgumentNullException.ThrowIfNull(request);
+ ArgumentNullException.ThrowIfNull(request.Entry);
+ ArgumentNullException.ThrowIfNull(request.Entry.DataSetWriterGroup);
+ ArgumentNullException.ThrowIfNull(request.Entry.DataSetName);
+ var uri = new Uri($"{_serviceUri}/v2/writer/assets");
+ return await _httpClient.PostAsync>(uri,
+ request, _serializer, ct: ct).ConfigureAwait(false);
}
///
@@ -120,6 +124,16 @@ public async Task> CreateOrUpdateAsset
request, _serializer, ct: ct).ConfigureAwait(false);
}
+ ///
+ public IAsyncEnumerable> GetAllAssetsAsync(
+ PublishedNodesEntryModel entry, RequestHeaderModel header, CancellationToken ct)
+ {
+ ArgumentNullException.ThrowIfNull(entry);
+ var uri = new Uri($"{_serviceUri}/v2/writer/assets/list");
+ return _httpClient.PostStreamAsync>(uri,
+ RequestBody(entry, header), _serializer, ct: ct);
+ }
+
///
public async Task DeleteAssetAsync(
PublishedNodeDeleteAssetRequestModel request, CancellationToken ct)
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Controller/DmApiPublisherControllerTests.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Controller/DmApiPublisherControllerTests.cs
index c8bee81330..c6e3965abb 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Controller/DmApiPublisherControllerTests.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Controller/DmApiPublisherControllerTests.cs
@@ -56,7 +56,9 @@ public DmApiPublisherControllerTests(ITestOutputHelper output)
// Note that each test is responsible for setting content of _tempFile;
CopyContent("Resources/empty_pn.json", _tempFile);
- _publishedNodesProvider = new PublishedNodesProvider(_options,
+ using var factory = new PhysicalFileProviderFactory(_options,
+ _loggerFactory.CreateLogger());
+ _publishedNodesProvider = new PublishedNodesProvider(factory, _options,
_loggerFactory.CreateLogger());
_triggerMock = new Mock();
var factoryMock = new Mock();
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Controller/TestData/Json/ExpandTests1.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Controller/TestData/Json/ExpandTests1.cs
index ce32e73d8b..93085967e5 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Controller/TestData/Json/ExpandTests1.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Controller/TestData/Json/ExpandTests1.cs
@@ -97,6 +97,12 @@ public Task ExpandBaseObjectTypeTest1Async()
return GetTests().ExpandBaseObjectTypeTest1Async();
}
+ [Fact]
+ public Task ExpandBaseObjectTypeTest2Async()
+ {
+ return GetTests().ExpandBaseObjectTypeTest2Async();
+ }
+
[Fact]
public Task ExpandBaseObjectsAndObjectTypesTestAsync()
{
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Controller/TestData/MsgPack/ExpandTests1.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Controller/TestData/MsgPack/ExpandTests1.cs
index 3238b27607..e3af899017 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Controller/TestData/MsgPack/ExpandTests1.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Controller/TestData/MsgPack/ExpandTests1.cs
@@ -97,6 +97,12 @@ public Task ExpandBaseObjectTypeTest1Async()
return GetTests().ExpandBaseObjectTypeTest1Async();
}
+ [Fact]
+ public Task ExpandBaseObjectTypeTest2Async()
+ {
+ return GetTests().ExpandBaseObjectTypeTest2Async();
+ }
+
[Fact]
public Task ExpandBaseObjectsAndObjectTypesTestAsync()
{
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/Azure.IIoT.OpcUa.Publisher.Sdk.csproj b/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/Azure.IIoT.OpcUa.Publisher.Sdk.csproj
index d54ae84d5e..042317fde2 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/Azure.IIoT.OpcUa.Publisher.Sdk.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/Azure.IIoT.OpcUa.Publisher.Sdk.csproj
@@ -8,20 +8,15 @@
enable
-
-
-
+
+
+
-
+
-
-
-
-
-
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/cli/Azure.IIoT.OpcUa.Publisher.Service.Cli.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/cli/Azure.IIoT.OpcUa.Publisher.Service.Cli.csproj
index f70cf1e976..c859ac047b 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/cli/Azure.IIoT.OpcUa.Publisher.Service.Cli.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/cli/Azure.IIoT.OpcUa.Publisher.Service.Cli.csproj
@@ -12,12 +12,12 @@
-
+
-
-
-
+
+
+
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk.csproj
index dd7bbe555b..03cd73d327 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk.csproj
@@ -8,22 +8,17 @@
enable
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj
index 3cc1f06ca4..ed13add15a 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj
@@ -16,23 +16,23 @@
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Azure.IIoT.OpcUa.Publisher.Service.WebApi.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Azure.IIoT.OpcUa.Publisher.Service.WebApi.Tests.csproj
index 0b4e891962..c081a38050 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Azure.IIoT.OpcUa.Publisher.Service.WebApi.Tests.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Azure.IIoT.OpcUa.Publisher.Service.WebApi.Tests.csproj
@@ -3,10 +3,10 @@
net8.0
-
-
-
-
+
+
+
+
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service/src/Azure.IIoT.OpcUa.Publisher.Service.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service/src/Azure.IIoT.OpcUa.Publisher.Service.csproj
index 1032f3d979..8654f5c0d8 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Service/src/Azure.IIoT.OpcUa.Publisher.Service.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher.Service/src/Azure.IIoT.OpcUa.Publisher.Service.csproj
@@ -6,7 +6,7 @@
enable
-
+
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Testing/src/Azure.IIoT.OpcUa.Publisher.Testing.Servers.csproj b/src/Azure.IIoT.OpcUa.Publisher.Testing/src/Azure.IIoT.OpcUa.Publisher.Testing.Servers.csproj
index ecb4b8b07c..2cc9421ec3 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Testing/src/Azure.IIoT.OpcUa.Publisher.Testing.Servers.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher.Testing/src/Azure.IIoT.OpcUa.Publisher.Testing.Servers.csproj
@@ -58,10 +58,10 @@
-
+
-
+
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj b/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj
index d928520749..9d872cdca0 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj
@@ -5,10 +5,10 @@
enable
-
-
-
-
+
+
+
+
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Tests/TestData/ConfigurationTests1.cs b/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Tests/TestData/ConfigurationTests1.cs
index c906a9d730..62ad7fe430 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Tests/TestData/ConfigurationTests1.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Tests/TestData/ConfigurationTests1.cs
@@ -47,7 +47,6 @@ public async Task ExpandObjectWithBrowsePathTest1Async(CancellationToken ct = de
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = false,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -81,7 +80,6 @@ public async Task ExpandObjectWithBrowsePathTest2Async(CancellationToken ct = de
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = false,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = true
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -113,7 +111,6 @@ public async Task ExpandObjectTest1Async(CancellationToken ct = default)
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = true,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -142,7 +139,6 @@ public async Task ExpandObjectTest2Async(CancellationToken ct = default)
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = true,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = true
@@ -169,7 +165,6 @@ public async Task ExpandServerObjectTest1Async(CancellationToken ct = default)
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = true,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
@@ -199,7 +194,6 @@ public async Task ExpandServerObjectTest2Async(CancellationToken ct = default)
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
@@ -229,7 +223,6 @@ public async Task ExpandServerObjectTest3Async(CancellationToken ct = default)
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = true
@@ -256,7 +249,6 @@ public async Task ExpandServerObjectTest4Async(CancellationToken ct = default)
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
MaxDepth = 1,
NoSubTypesOfTypeNodes = false,
@@ -287,7 +279,6 @@ public async Task ExpandServerObjectTest5Async(CancellationToken ct = default)
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
MaxDepth = 0,
MaxLevelsToExpand = 1,
@@ -316,9 +307,9 @@ public async Task ExpandBaseObjectTypeTest1Async(CancellationToken ct = default)
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
NoSubTypesOfTypeNodes = false,
+ DoNotFlattenTypeInstance = true,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -332,6 +323,36 @@ public async Task ExpandBaseObjectTypeTest1Async(CancellationToken ct = default)
});
}
+ public async Task ExpandBaseObjectTypeTest2Async(CancellationToken ct = default)
+ {
+ var entry = _connection.ToPublishedNodesEntry();
+ entry.OpcNodes = new[]
+ {
+ new OpcNodeModel
+ {
+ Id = Opc.Ua.ObjectTypeIds.BaseObjectType.ToString()
+ }
+ };
+ var results = await _service.ExpandAsync(entry,
+ new PublishedNodeExpansionModel
+ {
+ DiscardErrors = false,
+ ExcludeRootIfInstanceNode = false,
+ NoSubTypesOfTypeNodes = false,
+ DoNotFlattenTypeInstance = false,
+ CreateSingleWriter = false
+ }, ct).ToListAsync(ct).ConfigureAwait(false);
+
+ Assert.Equal(5, results.Count);
+ Assert.All(results, r =>
+ {
+ Assert.Null(r.ErrorInfo);
+ Assert.NotNull(r.Result);
+ Assert.NotNull(r.Result.OpcNodes);
+ Assert.True(r.Result.OpcNodes.Count > 0);
+ });
+ }
+
public async Task ExpandBaseObjectsAndObjectTypesTestAsync(CancellationToken ct = default)
{
var entry = _connection.ToPublishedNodesEntry();
@@ -357,8 +378,8 @@ public async Task ExpandBaseObjectsAndObjectTypesTestAsync(CancellationToken ct
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
+ DoNotFlattenTypeInstance = true,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -387,7 +408,6 @@ public async Task ExpandVariablesTest1Async(CancellationToken ct = default)
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
@@ -416,9 +436,9 @@ public async Task ExpandVariablesAndObjectsTest1Async(CancellationToken ct = def
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
NoSubTypesOfTypeNodes = false,
+ DoNotFlattenTypeInstance = true,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -444,7 +464,6 @@ public async Task ExpandVariableTypesTest1Async(CancellationToken ct = default)
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
@@ -470,7 +489,6 @@ public async Task ExpandVariableTypesTest2Async(CancellationToken ct = default)
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
@@ -497,7 +515,6 @@ public async Task ExpandVariableTypesTest3Async(CancellationToken ct = default)
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
@@ -528,7 +545,6 @@ public async Task ExpandObjectWithNoObjectsTest1Async(CancellationToken ct = def
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = true,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -557,7 +573,6 @@ public async Task ExpandObjectWithNoObjectsTest2Async(CancellationToken ct = def
{
DiscardErrors = true,
ExcludeRootIfInstanceNode = true,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -576,7 +591,6 @@ public async Task ExpandEmptyEntryTest1Async(CancellationToken ct = default)
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = true,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -594,7 +608,6 @@ public async Task ExpandEmptyEntryTest2Async(CancellationToken ct = default)
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = true,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = true
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -615,7 +628,6 @@ public async Task ExpandBadNodeIdTest1Async(CancellationToken ct = default)
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = true,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -642,7 +654,6 @@ public async Task ExpandBadNodeIdTest2Async(CancellationToken ct = default)
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = true,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false)).ConfigureAwait(false);
@@ -663,7 +674,6 @@ public async Task ExpandBadNodeIdTest3Async(CancellationToken ct = default)
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = false,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Tests/TestData/ConfigurationTests2.cs b/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Tests/TestData/ConfigurationTests2.cs
index 2dbbbcf63a..35ec58671e 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Tests/TestData/ConfigurationTests2.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Tests/TestData/ConfigurationTests2.cs
@@ -56,7 +56,6 @@ public async Task ConfigureFromObjectErrorTest1Async(CancellationToken ct = defa
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = true,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -95,7 +94,6 @@ public async Task ConfigureFromObjectErrorTest2Async(CancellationToken ct = defa
{
DiscardErrors = true,
ExcludeRootIfInstanceNode = true,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -126,7 +124,6 @@ public async Task ConfigureFromObjectErrorTest3Async(CancellationToken ct = defa
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = true,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = true
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -163,7 +160,6 @@ public async Task ConfigureFromObjectWithBrowsePathTest1Async(CancellationToken
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = false,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -200,7 +196,6 @@ public async Task ConfigureFromObjectWithBrowsePathTest2Async(CancellationToken
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = false,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = true
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -235,7 +230,6 @@ public async Task ConfigureFromObjectTest1Async(CancellationToken ct = default)
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = true,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -267,7 +261,6 @@ public async Task ConfigureFromObjectTest2Async(CancellationToken ct = default)
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = true,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = true
@@ -297,7 +290,6 @@ public async Task ConfigureFromServerObjectTest1Async(CancellationToken ct = def
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = true,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
@@ -330,7 +322,6 @@ public async Task ConfigureFromServerObjectTest2Async(CancellationToken ct = def
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
@@ -363,7 +354,6 @@ public async Task ConfigureFromServerObjectTest3Async(CancellationToken ct = def
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = true
@@ -393,7 +383,6 @@ public async Task ConfigureFromServerObjectTest4Async(CancellationToken ct = def
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
MaxDepth = 1,
NoSubTypesOfTypeNodes = false,
@@ -427,7 +416,6 @@ public async Task ConfigureFromServerObjectTest5Async(CancellationToken ct = def
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
MaxDepth = 0,
MaxLevelsToExpand = 1,
@@ -459,7 +447,7 @@ public async Task ConfigureFromBaseObjectTypeTest1Async(CancellationToken ct = d
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
+ DoNotFlattenTypeInstance = true,
ExcludeRootIfInstanceNode = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
@@ -477,6 +465,39 @@ public async Task ConfigureFromBaseObjectTypeTest1Async(CancellationToken ct = d
_publishedNodesServices.VerifyNoOtherCalls();
}
+ public async Task ConfigureFromBaseObjectTypeTest2Async(CancellationToken ct = default)
+ {
+ var entry = _connection.ToPublishedNodesEntry();
+ entry.OpcNodes = new[]
+ {
+ new OpcNodeModel
+ {
+ Id = Opc.Ua.ObjectTypeIds.BaseObjectType.ToString()
+ }
+ };
+ _createCall.Verifiable(Times.Exactly(5));
+ var results = await _service(_publishedNodesServices.Object).CreateOrUpdateAsync(entry,
+ new PublishedNodeExpansionModel
+ {
+ DiscardErrors = false,
+ DoNotFlattenTypeInstance = false,
+ ExcludeRootIfInstanceNode = false,
+ NoSubTypesOfTypeNodes = false,
+ CreateSingleWriter = false
+ }, ct).ToListAsync(ct).ConfigureAwait(false);
+
+ Assert.Equal(5, results.Count);
+ Assert.All(results, r =>
+ {
+ Assert.Null(r.ErrorInfo);
+ Assert.NotNull(r.Result);
+ Assert.NotNull(r.Result.OpcNodes);
+ Assert.True(r.Result.OpcNodes.Count > 0);
+ });
+ _publishedNodesServices.Verify();
+ _publishedNodesServices.VerifyNoOtherCalls();
+ }
+
public async Task ConfigureFromBaseObjectsAndObjectTypesTestAsync(CancellationToken ct = default)
{
var entry = _connection.ToPublishedNodesEntry();
@@ -503,7 +524,7 @@ public async Task ConfigureFromBaseObjectsAndObjectTypesTestAsync(CancellationTo
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
+ DoNotFlattenTypeInstance = true,
ExcludeRootIfInstanceNode = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
@@ -536,7 +557,6 @@ public async Task ConfigureFromVariablesTest1Async(CancellationToken ct = defaul
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
@@ -568,7 +588,7 @@ public async Task ConfigureFromVariablesAndObjectsTest1Async(CancellationToken c
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
+ DoNotFlattenTypeInstance = true,
ExcludeRootIfInstanceNode = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
@@ -599,7 +619,6 @@ public async Task ConfigureFromVariableTypesTest1Async(CancellationToken ct = de
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
@@ -628,7 +647,6 @@ public async Task ConfigureFromVariableTypesTest2Async(CancellationToken ct = de
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
@@ -658,7 +676,6 @@ public async Task ConfigureFromVariableTypesTest3Async(CancellationToken ct = de
new PublishedNodeExpansionModel
{
DiscardErrors = false,
- StopAtFirstFoundInstance = false,
ExcludeRootIfInstanceNode = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
@@ -692,7 +709,6 @@ public async Task ConfigureFromObjectWithNoObjectsTest1Async(CancellationToken c
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = true,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -724,7 +740,6 @@ public async Task ConfigureFromObjectWithNoObjectsTest2Async(CancellationToken c
{
DiscardErrors = true,
ExcludeRootIfInstanceNode = true,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -746,7 +761,6 @@ public async Task ConfigureFromEmptyEntryTest1Async(CancellationToken ct = defau
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = true,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -767,7 +781,6 @@ public async Task ConfigureFromEmptyEntryTest2Async(CancellationToken ct = defau
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = true,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = true
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -791,7 +804,6 @@ public async Task ConfigureFromBadNodeIdTest1Async(CancellationToken ct = defaul
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = true,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
@@ -821,7 +833,6 @@ public async Task ConfigureFromBadNodeIdTest2Async(CancellationToken ct = defaul
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = true,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false)).ConfigureAwait(false);
@@ -845,7 +856,6 @@ public async Task ConfigureFromBadNodeIdTest3Async(CancellationToken ct = defaul
{
DiscardErrors = false,
ExcludeRootIfInstanceNode = false,
- StopAtFirstFoundInstance = false,
NoSubTypesOfTypeNodes = false,
CreateSingleWriter = false
}, ct).ToListAsync(ct).ConfigureAwait(false);
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj b/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj
index e09e76cdd9..65261299ac 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj
@@ -6,13 +6,13 @@
-
-
+
+
-
+
-
-
+
+
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Extensions/ContainerBuilderEx.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Extensions/ContainerBuilderEx.cs
index 1b55b43d85..2603f4b854 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Extensions/ContainerBuilderEx.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Extensions/ContainerBuilderEx.cs
@@ -30,6 +30,8 @@ public static void AddPublisherCore(this ContainerBuilder builder)
builder.RegisterType()
.AsImplementedInterfaces().SingleInstance();
+ builder.RegisterType()
+ .AsImplementedInterfaces().SingleInstance();
builder.RegisterType()
.AsImplementedInterfaces().SingleInstance();
builder.RegisterType()
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/AsyncEnumerableBrowser.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/AsyncEnumerableBrowser.cs
index cc5ce570a8..af363e61eb 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/AsyncEnumerableBrowser.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/AsyncEnumerableBrowser.cs
@@ -249,8 +249,9 @@ private IEnumerable MatchReferences(BrowseFrame frame, ServiceCallContext con
.Where(reference => reference.NodeClass == _matchClass
&& (reference.NodeId?.ServerIndex ?? 1u) == 0)
.Where(reference => _typeDefinitionId == null ||
- reference.TypeDefinition == _typeDefinitionId || (_includeTypeDefinitionSubtypes
- && context.Session.TypeTree.IsTypeOf(reference.TypeDefinition, _typeDefinitionId)))
+ reference.TypeDefinition == _typeDefinitionId ||
+ (_includeTypeDefinitionSubtypes && context.Session.TypeTree
+ .IsTypeOf(reference.TypeDefinition, _typeDefinitionId)))
.Select(reference => new BrowseFrame((NodeId)reference.NodeId,
reference.BrowseName, reference.DisplayName?.Text, frame))
.ToList();
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/ConfigurationServices.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/ConfigurationServices.cs
index c198cd5d03..3303998eb4 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/ConfigurationServices.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/ConfigurationServices.cs
@@ -466,7 +466,7 @@ protected override IEnumerable> Handle
{
var node = _currentObject != null ? _currentObject.OriginalNode : CurrentNode;
node.AddErrorInfo(errorInfo);
- _logger.LogDebug("Error expanding node {Node}: {Error}", node, errorInfo);
+ _logger.LogError("Error expanding node {Node}: {Error}", node, errorInfo);
return Enumerable.Empty>();
}
@@ -692,9 +692,8 @@ private bool TryMoveToNextNode()
}
}
var depth = _request.MaxDepth == 0 ? 1 : _request.MaxDepth;
- var refTypeId = _request.StopAtFirstFoundInstance ?
- ReferenceTypeIds.Organizes : ReferenceTypeIds.HierarchicalReferences;
- Restart(CurrentNode.NodeId, maxDepth: depth, referenceTypeId: refTypeId);
+ Restart(CurrentNode.NodeId, maxDepth: depth,
+ referenceTypeId: ReferenceTypeIds.HierarchicalReferences);
return true;
case (uint)Opc.Ua.NodeClass.VariableType:
case (uint)Opc.Ua.NodeClass.ObjectType:
@@ -703,17 +702,13 @@ private bool TryMoveToNextNode()
var instanceClass =
CurrentNode.NodeClass == (uint)Opc.Ua.NodeClass.ObjectType ?
Opc.Ua.NodeClass.Object : Opc.Ua.NodeClass.Variable;
-
- // If stop at first found we only need to use organizes references
- var referenceTypeId =
- _request.StopAtFirstFoundInstance &&
- instanceClass == Opc.Ua.NodeClass.Object ?
- ReferenceTypeIds.Organizes : ReferenceTypeIds.HierarchicalReferences;
-
+ var stopWhenFound = instanceClass == Opc.Ua.NodeClass.Variable ||
+ !_request.DoNotFlattenTypeInstance;
Restart(ObjectIds.ObjectsFolder, maxDepth: _request.MaxDepth,
typeDefinitionId: CurrentNode.NodeId,
- stopWhenFound: _request.StopAtFirstFoundInstance,
- referenceTypeId: referenceTypeId, matchClass: instanceClass);
+ stopWhenFound: stopWhenFound,
+ referenceTypeId: ReferenceTypeIds.HierarchicalReferences,
+ matchClass: instanceClass);
return true;
case (uint)Opc.Ua.NodeClass.Variable:
if (!_request.ExcludeRootIfInstanceNode)
@@ -761,10 +756,18 @@ private bool TryMoveToNextObject()
if (node.TryGetNextObject(out _currentObject))
{
Debug.Assert(_currentObject != null);
- Restart(_currentObject.ObjectFromBrowse.NodeId,
- _request.MaxLevelsToExpand == 0 ? null : _request.MaxLevelsToExpand,
+ var nodeClass = Opc.Ua.NodeClass.Variable;
+ var maxDepth = _request.MaxLevelsToExpand == 0 ? (uint?)null :
+ _request.MaxLevelsToExpand;
+ if (_currentObject.OriginalNode.NodeClass == (uint)Opc.Ua.NodeClass.ObjectType
+ && !_request.DoNotFlattenTypeInstance)
+ {
+ nodeClass |= Opc.Ua.NodeClass.Object;
+ maxDepth = null;
+ }
+ Restart(_currentObject.ObjectFromBrowse.NodeId, maxDepth,
referenceTypeId: ReferenceTypeIds.Aggregates,
- nodeClass: Opc.Ua.NodeClass.Variable);
+ nodeClass: nodeClass, matchClass: Opc.Ua.NodeClass.Variable);
return true;
}
}
@@ -970,7 +973,8 @@ public bool TryGetNextObject(out ObjectToExpand? obj)
///
///
///
- private record class ObjectToExpand(BrowseFrame ObjectFromBrowse, NodeToExpand OriginalNode)
+ private record class ObjectToExpand(BrowseFrame ObjectFromBrowse,
+ NodeToExpand OriginalNode)
{
public bool EntriesAlreadyReturned { get; internal set; }
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/DataSetWriter.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/DataSetWriter.cs
index b699dde642..17c7cd6794 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/DataSetWriter.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/DataSetWriter.cs
@@ -528,7 +528,7 @@ public async ValueTask UpdateAsync(DataSetWriter dataSetWriter, HashSet
// Trigger reevaluation
Subscription.NotifyMonitoredItemsChanged();
- _logger.LogInformation(
+ _logger.LogDebug(
"Updated monitored items for writer {Id} in writer group {WriterGroup}.",
Id, _group.Id);
}
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherModule.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherModule.cs
index b83b095a4d..968d2b90b9 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherModule.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherModule.cs
@@ -8,6 +8,7 @@ namespace Azure.IIoT.OpcUa.Publisher.Services
using Autofac;
using Furly;
using Furly.Azure.IoT.Edge;
+ using Furly.Extensions.Rpc;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
@@ -66,6 +67,13 @@ public async Task StartAsync(CancellationToken cancellationToken)
_logger.LogInformation("Starting OpcPublisher module version {Version}...",
version);
+ // Start rpc servers
+ foreach (var server in _scope.Resolve>())
+ {
+ _logger.LogInformation("... Starting Rpc {Server} server ...", server.Name);
+ server.Start();
+ }
+
// Now report runtime state as restarted. This can crash and we will retry.
await runtimeStateReporter.SendRestartAnnouncementAsync(
cancellationToken).ConfigureAwait(false);
@@ -144,7 +152,7 @@ public bool Shutdown(bool failFast)
_logger.LogInformation("Received request to shutdown publisher process.");
if (failFast)
{
- Environment.FailFast("Shutdown was invoked remotely.");
+ Environment.FailFast("User shutdown of OPC Publisher due to error.");
}
else
{
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClient.Subscription.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClient.Subscription.cs
index 46d987fb3c..7091cbdf06 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClient.Subscription.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClient.Subscription.cs
@@ -131,7 +131,13 @@ internal void TriggerSubscriptionSynchronization(
///
internal void OnSubscriptionCreated(OpcUaSubscription subscription)
{
- _cache.AddOrUpdate(subscription.Template, subscription);
+ lock (_cache)
+ {
+ if (subscription.IsRoot)
+ {
+ _cache.AddOrUpdate(subscription.Template, subscription);
+ }
+ }
}
///
@@ -144,14 +150,23 @@ private bool TryGetSubscription(SubscriptionModel template,
[NotNullWhen(true)] out OpcUaSubscription? subscription)
{
// Fast lookup
- if (_cache.TryGetValue(template, out subscription) &&
- !subscription.IsClosed)
+ lock (_cache)
{
- return true;
+ if (_cache.TryGetValue(template, out subscription) &&
+ !subscription.IsClosed &&
+ subscription.IsRoot)
+ {
+ return true;
+ }
+ subscription = _session?.SubscriptionHandles.Values
+ .FirstOrDefault(s => s.IsRoot && s.Template == template);
+ if (subscription != null)
+ {
+ _cache.AddOrUpdate(template, subscription);
+ return true;
+ }
+ return false;
}
- subscription = _session?.SubscriptionHandles.Values
- .FirstOrDefault(s => s.IsRoot && s.Template == template);
- return subscription != null;
}
///
@@ -250,7 +265,10 @@ await Task.WhenAll(existing.Keys
{
try
{
- _cache.TryRemove(close.Template, out _);
+ lock (_cache)
+ {
+ _cache.Remove(close.Template);
+ }
if (_s2r.TryRemove(close.Template, out var r))
{
Debug.Assert(r.Count == 0,
@@ -560,7 +578,7 @@ private void RemoveNoLockInternal()
#pragma warning restore CA2213 // Disposable fields should be disposed
private readonly Dictionary _registrations = new();
private readonly ConcurrentDictionary> _s2r = new();
- private readonly ConcurrentDictionary _cache = new();
+ private readonly Dictionary _cache = new();
private readonly IOptions _subscriptionOptions;
}
}
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.cs
index 348c0399bc..2acad4ba34 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.cs
@@ -445,7 +445,7 @@ public void LogRevisedSamplingRateAndQueueSize()
}
Debug.Assert(Subscription != null);
if (SamplingInterval != Status.SamplingInterval &&
- QueueSize != Status.QueueSize)
+ QueueSize != Status.QueueSize && Status.QueueSize != 0)
{
_logger.LogInformation("Server revised SamplingInterval from {SamplingInterval} " +
"to {CurrentSamplingInterval} and QueueSize from {QueueSize} " +
@@ -460,7 +460,7 @@ public void LogRevisedSamplingRateAndQueueSize()
SamplingInterval, Status.SamplingInterval,
Subscription.Id, StartNodeId, DisplayName);
}
- else if (QueueSize != Status.QueueSize)
+ else if (QueueSize != Status.QueueSize && Status.QueueSize != 0)
{
_logger.LogInformation("Server revised QueueSize from {QueueSize} " +
"to {CurrentQueueSize} for #{SubscriptionId}|{Item}('{Name}').",
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs
index 4b2538139c..99a3e44e93 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs
@@ -345,12 +345,10 @@ public override int GetHashCode()
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
-
if (!disposing || _disposed)
{
return;
}
- _disposed = true;
try
{
ResetMonitoredItemWatchdogTimer(false);
@@ -392,6 +390,7 @@ protected override void Dispose(bool disposing)
}
finally
{
+ _disposed = true;
_keepAliveWatcher.Dispose();
_monitoredItemWatcher.Dispose();
_timer.Dispose();
@@ -983,7 +982,7 @@ private async ValueTask SynchronizeMonitoredItemsAsync(
var remove = previouslyMonitored.Except(desired).ToHashSet();
var add = desired.Except(previouslyMonitored).ToHashSet();
var same = previouslyMonitored.ToHashSet();
- var errors = 0;
+ var errorsDuringSync = 0;
same.IntersectWith(desired);
//
@@ -1036,7 +1035,7 @@ private async ValueTask SynchronizeMonitoredItemsAsync(
_logger.LogWarning("Failed to resolve browse path for {NodeId} " +
"in {Subscription} due to '{ServiceResult}'",
result.Request!.Value.NodeId, this, result.ErrorInfo);
- errors++;
+ errorsDuringSync++;
}
}
}
@@ -1112,7 +1111,7 @@ private async ValueTask SynchronizeMonitoredItemsAsync(
{
if (!desired.TryGetValue(toUpdate, out var theDesiredUpdate))
{
- errors++;
+ errorsDuringSync++;
continue;
}
desired.Remove(theDesiredUpdate);
@@ -1142,7 +1141,7 @@ private async ValueTask SynchronizeMonitoredItemsAsync(
_logger.LogWarning(ex,
"Failed to update monitored item '{Item}' in {Subscription}...",
toUpdate, this);
- errors++;
+ errorsDuringSync++;
}
finally
{
@@ -1174,7 +1173,7 @@ private async ValueTask SynchronizeMonitoredItemsAsync(
_logger.LogWarning(ex,
"Failed to remove monitored item '{Item}' from {Subscription}...",
toRemove, this);
- errors++;
+ errorsDuringSync++;
}
}
@@ -1208,7 +1207,7 @@ private async ValueTask SynchronizeMonitoredItemsAsync(
_logger.LogWarning(ex,
"Failed to add monitored item '{Item}' to {Subscription}...",
toAdd, this);
- errors++;
+ errorsDuringSync++;
}
}
@@ -1231,7 +1230,7 @@ private async ValueTask SynchronizeMonitoredItemsAsync(
// Perform second pass over all monitored items and complete.
applyChanges = false;
- var invalidItems = 0;
+ var badMonitoredItems = 0;
var desiredMonitoredItems = same;
desiredMonitoredItems.UnionWith(add);
@@ -1276,7 +1275,7 @@ private async ValueTask SynchronizeMonitoredItemsAsync(
this, results.ErrorInfo);
// We will retry later.
- errors++;
+ errorsDuringSync++;
}
else
{
@@ -1311,7 +1310,7 @@ private async ValueTask SynchronizeMonitoredItemsAsync(
if (!monitoredItem.TryCompleteChanges(this, ref applyChanges))
{
// Apply more changes in future passes
- invalidItems++;
+ badMonitoredItems++;
}
}
@@ -1389,7 +1388,7 @@ private async ValueTask SynchronizeMonitoredItemsAsync(
}
}
// Retry later
- errors++;
+ errorsDuringSync++;
}
}
}
@@ -1437,56 +1436,38 @@ private async ValueTask SynchronizeMonitoredItemsAsync(
_logger.LogInformation("ConditionRefresh on subscription " +
"{Subscription} failed with an exception '{Message}'",
this, e.Message);
- errors++;
+ errorsDuringSync++;
}
}
set.ForEach(item => item.LogRevisedSamplingRateAndQueueSize());
- _badMonitoredItems = invalidItems;
- _errorsDuringSync = errors;
- _goodMonitoredItems = Math.Max(set.Count - invalidItems, 0);
-
- _reportingItems = set
+ var goodMonitoredItems =
+ Math.Max(set.Count - badMonitoredItems, 0);
+ var reportingItems = set
.Count(r => r.Status?.MonitoringMode == Opc.Ua.MonitoringMode.Reporting);
- _disabledItems = set
+ var disabledItems = set
.Count(r => r.Status?.MonitoringMode == Opc.Ua.MonitoringMode.Disabled);
- _samplingItems = set
+ var samplingItems = set
.Count(r => r.Status?.MonitoringMode == Opc.Ua.MonitoringMode.Sampling);
- _notAppliedItems = set
+ var notAppliedItems = set
.Count(r => r.Status?.MonitoringMode != r.MonitoringMode);
- _heartbeatItems = set
+ var heartbeatItems = set
.Count(r => r is OpcUaMonitoredItem.Heartbeat);
- _conditionItems = set
+ var conditionItems = set
.Count(r => r is OpcUaMonitoredItem.Condition);
var heartbeatsEnabled = set
.Count(r => r is OpcUaMonitoredItem.Heartbeat h && h.TimerEnabled);
var conditionsEnabled = set
.Count(r => r is OpcUaMonitoredItem.Condition h && h.TimerEnabled);
- _logger.LogInformation(@"{Subscription} - Now monitoring {Count} nodes:
-# Good/Bad: {Good}/{Bad}
-# Errors: {Errors}
-# Reporting: {Reporting}
-# Sampling: {Sampling}
-# Heartbeat/ing: {Heartbeat}/{EnabledHeartbeats}
-# Condition/ing: {Conditions}/{EnabledConditions}
-# Disabled: {Disabled}
-# Not applied: {NotApplied}
-# Removed: {Disposed}",
- this, set.Count,
- _goodMonitoredItems, _badMonitoredItems,
- _errorsDuringSync,
- _reportingItems,
- _samplingItems,
- _heartbeatItems, heartbeatsEnabled,
- _conditionItems, conditionsEnabled,
- _disabledItems,
- _notAppliedItems,
+ ReportMonitoredItemChanges(set.Count, goodMonitoredItems, badMonitoredItems,
+ errorsDuringSync, notAppliedItems, reportingItems, disabledItems, heartbeatItems,
+ heartbeatsEnabled, conditionItems, conditionsEnabled, samplingItems,
dispose.Count);
// Set up subscription management trigger
- if (invalidItems != 0 || errors != 0)
+ if (badMonitoredItems != 0 || errorsDuringSync != 0)
{
// There were items that could not be added to subscription
return Delay(_options.Value.InvalidMonitoredItemRetryDelayDuration,
@@ -1680,6 +1661,97 @@ private void LogRevisedValues(bool created)
CurrentLifetimeCount, LifetimeCount);
}
+ ///
+ /// Report monitored item changes
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private void ReportMonitoredItemChanges(int count,
+ int goodMonitoredItems, int badMonitoredItems,
+ int errorsDuringSync, int notAppliedItems,
+ int reportingItems, int disabledItems,
+ int heartbeatItems, int heartbeatsEnabled,
+ int conditionItems, int conditionsEnabled,
+ int samplingItems, int disposed)
+ {
+ if (_badMonitoredItems != badMonitoredItems ||
+ _errorsDuringSync != errorsDuringSync ||
+ _goodMonitoredItems != goodMonitoredItems ||
+ _reportingItems != reportingItems ||
+ _disabledItems != disabledItems ||
+ _samplingItems != samplingItems ||
+ _notAppliedItems != notAppliedItems ||
+ _heartbeatItems != heartbeatItems ||
+ _conditionItems != conditionItems)
+ {
+ if (samplingItems == 0 && heartbeatItems == 0 && conditionItems == 0 &&
+ notAppliedItems == 0)
+ {
+ if (errorsDuringSync == 0 && disabledItems == 0)
+ {
+ _logger.LogInformation(
+@"{Subscription} - Removed {Removed} - now monitoring {Count} nodes:
+# Good/Bad/Reporting: {Good}/{Bad}/{Reporting}",
+ this, disposed, count,
+ goodMonitoredItems, badMonitoredItems, reportingItems);
+ }
+ else
+ {
+ _logger.LogWarning(
+@"{Subscription} - Removed {Removed} - now monitoring {Count} nodes:
+# Good/Bad/Reporting: {Good}/{Bad}/{Reporting}
+# Disabled/Errors: {Disabled}/{Errors}",
+ this, disposed, count,
+ goodMonitoredItems, badMonitoredItems, reportingItems,
+ disabledItems, errorsDuringSync);
+ }
+ }
+ else
+ {
+ _logger.LogInformation(
+@"{Subscription} - Removed {Removed} - now monitoring {Count} nodes:
+# Good/Bad/Reporting: {Good}/{Bad}/{Reporting}
+# Disabled/Errors: {Disabled}/{Errors} (Not applied: {NotApplied})
+# Sampling: {Sampling}
+# Heartbeat/ing: {Heartbeat}/{EnabledHeartbeats}
+# Condition/ing: {Conditions}/{EnabledConditions}",
+ this, disposed, count,
+ goodMonitoredItems, badMonitoredItems, reportingItems,
+ disabledItems, errorsDuringSync, notAppliedItems,
+ samplingItems,
+ heartbeatItems, heartbeatsEnabled,
+ conditionItems, conditionsEnabled);
+ }
+ }
+ else
+ {
+ _logger.LogDebug(
+ "{ Subscription} Applied changes to monitored items, but nothing changed.",
+ this);
+ }
+
+ _badMonitoredItems = badMonitoredItems;
+ _errorsDuringSync = errorsDuringSync;
+ _goodMonitoredItems = goodMonitoredItems;
+ _reportingItems = reportingItems;
+ _disabledItems = disabledItems;
+ _samplingItems = samplingItems;
+ _notAppliedItems = notAppliedItems;
+ _heartbeatItems = heartbeatItems;
+ _conditionItems = conditionItems;
+ }
+
///
/// Calculate delay
///
@@ -1809,7 +1881,7 @@ private void OnSubscriptionEventNotificationList(Subscription subscription,
EventNotificationList notification, IList? stringTable)
{
Debug.Assert(ReferenceEquals(subscription, this));
- Debug.Assert(!_disposed);
+ ObjectDisposedException.ThrowIf(_disposed, this);
if (notification?.Events == null)
{
@@ -1935,7 +2007,7 @@ private void OnSubscriptionKeepAliveNotification(Subscription subscription,
NotificationData notification)
{
Debug.Assert(ReferenceEquals(subscription, this));
- Debug.Assert(!_disposed);
+ ObjectDisposedException.ThrowIf(_disposed, this);
ResetKeepAliveTimer();
@@ -2011,7 +2083,7 @@ public void OnSubscriptionCylicReadNotification(Subscription subscription,
List values, uint sequenceNumber, DateTime publishTime)
{
Debug.Assert(ReferenceEquals(subscription, this));
- Debug.Assert(!_disposed);
+ ObjectDisposedException.ThrowIf(_disposed, this);
var session = Session;
if (session is not IOpcUaSession sessionContext)
{
@@ -2083,7 +2155,7 @@ private void OnSubscriptionDataChangeNotification(Subscription subscription,
DataChangeNotification notification, IList? stringTable)
{
Debug.Assert(ReferenceEquals(subscription, this));
- Debug.Assert(!_disposed);
+ ObjectDisposedException.ThrowIf(_disposed, this);
var firstDataChangeReceived = _firstDataChangeReceived;
_firstDataChangeReceived = true;
@@ -2460,13 +2532,11 @@ private void OnKeepAliveMissing(object? state)
///
private void OnPublishStatusChange(Subscription subscription, PublishStateChangedEventArgs e)
{
+ ObjectDisposedException.ThrowIf(_disposed, this);
if (_disposed)
{
- // Debug.Fail("Should not be called after dispose");
- // This currently happens because the stack caches the callbacks!
return;
}
-
if (e.Status.HasFlag(PublishStateChangedMask.Stopped) && !_publishingStopped)
{
_logger.LogInformation("Subscription {Subscription} STOPPED!", this);
@@ -2516,13 +2586,11 @@ private void OnPublishStatusChange(Subscription subscription, PublishStateChange
///
private void OnStateChange(Subscription subscription, SubscriptionStateChangedEventArgs e)
{
+ ObjectDisposedException.ThrowIf(_disposed, this);
if (_disposed)
{
- // Debug.Fail("Should not be called after dispose");
- // This currently happens because the stack caches the callbacks!
return;
}
-
if (e.Status.HasFlag(SubscriptionChangeMask.Created))
{
_logger.LogDebug("Subscription {Subscription} created.", this);
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PhysicalFileProviderFactory.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PhysicalFileProviderFactory.cs
new file mode 100644
index 0000000000..d185e82c37
--- /dev/null
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PhysicalFileProviderFactory.cs
@@ -0,0 +1,68 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
+// ------------------------------------------------------------
+
+namespace Azure.IIoT.OpcUa.Publisher.Storage
+{
+ using Azure.IIoT.OpcUa.Publisher;
+ using Furly.Extensions.Storage;
+ using Microsoft.Extensions.FileProviders;
+ using Microsoft.Extensions.Logging;
+ using Microsoft.Extensions.Options;
+ using System;
+ using System.Collections.Concurrent;
+ using System.IO;
+ using System.Linq;
+
+ ///
+ /// Physical file provider factory
+ ///
+ public sealed class PhysicalFileProviderFactory : IFileProviderFactory, IDisposable
+ {
+ ///
+ public PhysicalFileProviderFactory(IOptions options,
+ ILogger logger)
+ {
+ _options = options;
+ _logger = logger;
+ _providers = new ConcurrentDictionary();
+ }
+
+ ///
+ public IFileProvider Create(string root)
+ {
+ root = Path.GetFullPath(string.IsNullOrWhiteSpace(root) ?
+ Environment.CurrentDirectory : root);
+ return _providers.GetOrAdd(root, directory =>
+ {
+ if (!Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ var provider = new PhysicalFileProvider(directory);
+
+ if (_options.Value.UseFileChangePolling == true)
+ {
+ provider.UseActivePolling = true;
+ provider.UsePollingFileWatcher = true;
+ }
+
+ _logger.LogInformation("Mapping directory {Directory} " +
+ "via physical file provider.", directory);
+ return provider;
+ });
+ }
+
+ ///
+ public void Dispose()
+ {
+ _providers.Values.ToList().ForEach(p => p.Dispose());
+ }
+
+ private readonly IOptions _options;
+ private readonly ILogger _logger;
+ private readonly ConcurrentDictionary _providers;
+ }
+}
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesProvider.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesProvider.cs
index ec738f2e7d..3138cf5d12 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesProvider.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesProvider.cs
@@ -6,6 +6,7 @@
namespace Azure.IIoT.OpcUa.Publisher.Storage
{
using Azure.IIoT.OpcUa.Publisher;
+ using Furly.Extensions.Storage;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -16,7 +17,7 @@ namespace Azure.IIoT.OpcUa.Publisher.Storage
using System.Threading;
///
- /// Utilities provider for published nodes file.
+ /// Provider for published nodes file.
///
public sealed class PublishedNodesProvider : IStorageProvider, IDisposable
{
@@ -24,40 +25,34 @@ public sealed class PublishedNodesProvider : IStorageProvider, IDisposable
public event EventHandler? Changed;
///
- /// Get file mode to use
+ /// Provider of storage for published nodes file.
///
- private FileMode FileMode =>
- _options.Value.PublishedNodesFile == null ||
- _options.Value.CreatePublishFileIfNotExist == true ?
- FileMode.OpenOrCreate : FileMode.Open;
-
- ///
- /// Provider of utilities for published nodes file.
- ///
- /// Publisher configuration with location
+ /// File provider factory
+ /// Publisher configuration with location
/// of published nodes file.
- /// Logger
- public PublishedNodesProvider(IOptions options,
+ /// Logger
+ public PublishedNodesProvider(IFileProviderFactory factory,
+ IOptions options,
ILogger logger)
{
- _options = options ?? throw new ArgumentNullException(nameof(options));
+ // TODO: Use IFileProvider and IStorageProvider going forward
+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
- _fileName = _options.Value.PublishedNodesFile ??
- PublisherConfig.PublishedNodesFileDefault;
- var directory = Path.GetDirectoryName(_fileName);
+ _fileMode = options.Value.PublishedNodesFile == null ||
+ options.Value.CreatePublishFileIfNotExist == true ?
+ FileMode.OpenOrCreate : FileMode.Open;
+ _fileName = options.Value.PublishedNodesFile ??
+ PublisherConfig.PublishedNodesFileDefault;
- if (string.IsNullOrWhiteSpace(directory))
+ var root = Path.GetDirectoryName(_fileName);
+ if (string.IsNullOrWhiteSpace(root))
{
- directory = Environment.CurrentDirectory;
+ root = Environment.CurrentDirectory;
}
+ _root = root;
+ _provider = factory.Create(_root);
- _provider = new PhysicalFileProvider(directory);
- if (_options.Value.UseFileChangePolling == true)
- {
- _provider.UseActivePolling = true;
- _provider.UsePollingFileWatcher = true;
- }
_watch = _provider.Watch(Path.GetFileName(_fileName));
_watch.RegisterChangeCallback(ChangeCallback, this);
@@ -91,14 +86,15 @@ public string ReadContent()
}
// Create file only if it is the default file.
using (var fileStream = new FileStream(_fileName,
- FileMode, FileAccess.Read, FileShare.Read))
+ _fileMode, FileAccess.Read, FileShare.Read))
{
return fileStream.ReadAsString(Encoding.UTF8);
}
}
catch (Exception e)
{
- _logger.LogDebug(e, "Failed to read content of published nodes file from \"{Path}\"",
+ _logger.LogDebug(e,
+ "Failed to read content of published nodes file from \"{Path}\"",
_fileName);
throw;
}
@@ -122,7 +118,7 @@ public void WriteContent(string content, bool disableRaisingEvents = false)
try
{
using (var fileStream = new FileStream(_fileName,
- FileMode,
+ _fileMode,
FileAccess.Write,
// We will require that there is no other process using the file.
FileShare.None))
@@ -143,7 +139,7 @@ public void WriteContent(string content, bool disableRaisingEvents = false)
try
{
using (var fileStream = new FileStream(_fileName,
- FileMode,
+ _fileMode,
FileAccess.Write,
// Relaxing requirements.
FileShare.ReadWrite))
@@ -177,7 +173,6 @@ public void WriteContent(string content, bool disableRaisingEvents = false)
///
public void Dispose()
{
- _provider.Dispose();
_lock.Dispose();
}
@@ -199,14 +194,15 @@ private void ChangeCallback(object? obj)
var exists = File.Exists(_fileName);
Changed?.Invoke(this, new FileSystemEventArgs(exists ?
WatcherChangeTypes.Changed : WatcherChangeTypes.Deleted,
- _provider.Root, Path.GetFileName(_fileName)));
+ _root, Path.GetFileName(_fileName)));
}
- private readonly IOptions _options;
+ private readonly string _root;
private readonly ILogger _logger;
+ private readonly FileMode _fileMode;
private readonly string _fileName;
private readonly SemaphoreSlim _lock;
- private readonly PhysicalFileProvider _provider;
+ private readonly IFileProvider _provider;
private bool _disableRaisingEvents;
private IChangeToken _watch;
}
diff --git a/src/Azure.IIoT.OpcUa.Publisher/tests/Azure.IIoT.OpcUa.Publisher.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher/tests/Azure.IIoT.OpcUa.Publisher.Tests.csproj
index d5195c6230..c0e4ac7344 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/tests/Azure.IIoT.OpcUa.Publisher.Tests.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher/tests/Azure.IIoT.OpcUa.Publisher.Tests.csproj
@@ -25,44 +25,8 @@
-
+
Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
+
diff --git a/src/Azure.IIoT.OpcUa.Publisher/tests/Services/PublishedNodesJsonServicesTests.cs b/src/Azure.IIoT.OpcUa.Publisher/tests/Services/PublishedNodesJsonServicesTests.cs
index 8f4acf12b0..c5474770b0 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/tests/Services/PublishedNodesJsonServicesTests.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/tests/Services/PublishedNodesJsonServicesTests.cs
@@ -59,7 +59,9 @@ public PublishedNodesJsonServicesTests(ITestOutputHelper output)
// Note that each test is responsible for setting content of _tempFile;
Utils.CopyContent("Publisher/empty_pn.json", _tempFile);
- _publishedNodesProvider = new PublishedNodesProvider(_options,
+ using var factory = new PhysicalFileProviderFactory(_options,
+ _loggerFactory.CreateLogger());
+ _publishedNodesProvider = new PublishedNodesProvider(factory, _options,
_loggerFactory.CreateLogger());
_triggerMock = new Mock();
var factoryMock = new Mock();
diff --git a/src/Azure.IIoT.OpcUa.Publisher/tests/Services/TestData/ExpandTests1.cs b/src/Azure.IIoT.OpcUa.Publisher/tests/Services/TestData/ExpandTests1.cs
index 8a368c68bc..38648ca405 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/tests/Services/TestData/ExpandTests1.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/tests/Services/TestData/ExpandTests1.cs
@@ -95,6 +95,12 @@ public Task ExpandBaseObjectTypeTest1Async()
return GetTests().ExpandBaseObjectTypeTest1Async();
}
+ [Fact]
+ public Task ExpandBaseObjectTypeTest2Async()
+ {
+ return GetTests().ExpandBaseObjectTypeTest2Async();
+ }
+
[Fact]
public Task ExpandBaseObjectsAndObjectTypesTestAsync()
{
diff --git a/src/Azure.IIoT.OpcUa.Publisher/tests/Services/TestData/ExpandTests2.cs b/src/Azure.IIoT.OpcUa.Publisher/tests/Services/TestData/ExpandTests2.cs
index fc5fd1ed98..bdeffd44ae 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/tests/Services/TestData/ExpandTests2.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/tests/Services/TestData/ExpandTests2.cs
@@ -111,6 +111,12 @@ public Task ConfigureFromBaseObjectTypeTest1Async()
return GetTests().ConfigureFromBaseObjectTypeTest1Async();
}
+ [Fact]
+ public Task ConfigureFromBaseObjectTypeTest2Async()
+ {
+ return GetTests().ConfigureFromBaseObjectTypeTest2Async();
+ }
+
[Fact]
public Task ConfigureFromBaseObjectsAndObjectTypesTestAsync()
{
diff --git a/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj b/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj
index f09eabedcf..45300a856c 100644
--- a/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj
+++ b/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj
@@ -9,8 +9,8 @@
-
-
+
+
diff --git a/src/Azure.IIoT.OpcUa/src/Encoders/JsonEncoderEx.cs b/src/Azure.IIoT.OpcUa/src/Encoders/JsonEncoderEx.cs
index 4139f2d6d4..efa238cf19 100644
--- a/src/Azure.IIoT.OpcUa/src/Encoders/JsonEncoderEx.cs
+++ b/src/Azure.IIoT.OpcUa/src/Encoders/JsonEncoderEx.cs
@@ -1956,18 +1956,21 @@ public void WriteArray(string fieldName, object array, int valueRank,
WriteEnumeratedArray(fieldName, enumArray, enumType);
return;
case BuiltInType.Variant:
- if (array is Variant[] variants)
+ switch (array)
{
- WriteVariantArray(fieldName, variants);
- return;
+ case Variant[] variants:
+ WriteVariantArray(fieldName, variants);
+ return;
+ case object[] objects:
+ WriteObjectArray(fieldName, objects);
+ return;
+ case null:
+ WriteObjectArray(fieldName, null);
+ return;
+ default:
+ throw new EncodingException("Unexpected type encountered " +
+ $"while encoding an array of Variants: {array.GetType()}");
}
- if (array is object[] objects)
- {
- WriteObjectArray(fieldName, objects);
- return;
- }
- throw new EncodingException(
- $"Unexpected type encountered while encoding an array of Variants: {array.GetType()}");
}
}
// write matrix.
diff --git a/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj b/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj
index b15fa5a395..128f2144a0 100644
--- a/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj
+++ b/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj
@@ -14,7 +14,7 @@
all
runtime; build; native; contentfiles; analyzers
-
+