Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions samples/process-control/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# OPC UA process control

In Azure IoT Operations `aio-opc-ua-commander` lets you send changes to an OPC UA server from the edge or from the cloud. The current preview includes support for writing data points from an asset dataset with simple and complex data-types as well as dumping the address space of an OPC UA server.

The OPC-UA commander:

- Uses the [RPC](https://github.com/Azure/iot-operations-sdks/blob/main/doc/reference/rpc-protocol.md) and MQTT protocol/broker as the underlying messaging plane.
MQTT messages include some system and user properties to define [metadata](https://github.com/Azure/iot-operations-sdks/blob/main/doc/reference/message-metadata.md) values that help with flow control.
- Subscribes to MQTT topic `{AioNamespace}/asset-operations/{AssetId}/{DatasetName}/` for data-set write operations.
- Subscribes to MQTT topic `{AioNamespace}/asset-operations/{AssetId}/{ManagementGroupName}/` for call operations and explicit write.
- Subscribes to MQTT topic `{AioNamespace}/endpoint-operations/{InboundEndpointProfileName}/{ActionName}/` for endpoint operations.
- On MQTT request/response create ad-hoc session based on the device associated with the namespace asset.
- Validates write requests against the generated request schema.
- Validates that the write request only contains data points that exist within the dataset.
- Use write service calls to set all data-points at once.
- Sends response to response topic property defined in MQTT message.

To learn more about how `aio-opc-ua-commander` works, see [How to control OPC UA assets](https://learn.microsoft.com/azure/iot-operations/discover-manage-assets/howto-control-opc-ua).

This sample illustrates some of these capabilities using the OPC PLC simulator boiler.

## Prerequisites

To run the sample application, you need:

- A preview instance of Azure IoT Operations deployed. If you don't already have an instance, see [Create an Azure IoT Operations instance](https://learn.microsoft.com/azure/iot-operations/get-started-end-to-end-sample/quickstart-deploy).
- Access to the internal MQTT broker in the Azure IoT Operations cluster. To configure access the broker, see [Test connectivity to MQTT broker with MQTT clients](https://learn.microsoft.com/azure/iot-operations/manage-mqtt-broker/howto-test-connection).
- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) version 2.67.0 or higher.
- [NET 9 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/9.0)

## Deploy the simulator

The sample application uses the boiler in the OPC PLC simulator.

To deploy the OPC PLC simulator, run the following command:

```bash
kubectl apply -f https://raw.githubusercontent.com/Azure-Samples/explore-iot-operations/main/samples/quickstarts/opc-plc-deployment.yaml
```

> [!CAUTION]
> This configuration uses a self-signed application instance certificate. Don't use this configuration in a production environment. To learn more, see [Configure OPC UA certificates infrastructure for the connector for OPC UA](https://learn.microsoft.com/azure/iot-operations/discover-manage-assets/howto-configure-opc-ua-certificates-infrastructure).


## Configure the device and namespace assets

To add the required device (`opc-ua-commander`), inbound endpoint (`opc-ua-commander-0`), and namespace asset (`boiler`) to your instance, run the following commands:

```bash
wget https://raw.githubusercontent.com/Azure-Samples/explore-iot-operations/main/samples/process-control/boiler-simulation.bicep -O boiler-simulation.bicep

AIO_NAMESPACE_NAME=<YOUR_AIO_NAMESPACE_NAME>
RESOURCE_GROUP=<YOUR_RESOURCE_GROUP_NAME>
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
CUSTOM_LOCATION_NAME=$(az iot ops list -g $RESOURCE_GROUP --query "[0].extendedLocation.name" -o tsv | awk -F'/' '{print $NF}')

az deployment group create --subscription $SUBSCRIPTION_ID --resource-group $RESOURCE_GROUP --template-file boiler-simulation.bicep --parameters customLocationName=$CUSTOM_LOCATION_NAME aioNamespaceName=$AIO_NAMESPACE_NAME
```

You can review these resources in the Operations experience web UI or by running the following commands:

```bash
# View device details
az iot ops ns device query --instance <your instance name> -g <your resource group>
# View namespace asset details
az iot ops ns asset query --instance <your instance name> -g <your resource group>
```

## Configure and run application

Make sure the `process-control-demo/src/write.dataset.client/appsettings.json` file contains the correct values for:

- `HostName` and `Port` in the `ConnectionStrings` setting.
- `Namespace`: typically `azure-iot-operations` if you used the quickstarts deployment.
- `AssetName`: `boiler` if you used the `boiler-simulation.bicep` file to add the device and namespace asset.
- `DatasetName`: `boiler-simple-write` if you used the `boiler-simulation.bicep` file to add the device and namespace asset.

To run the application:

```bash
dotnet run --project process-control-demo/src/write.dataset.client/write.dataset.client.csproj
```

### Usage

For example, to set the `TargetTemperature` and other values on the boiler asset, the sample application publishes the following MQTT message to the topic `azure-iot-operations/asset-operations/<asset name>/<dataset name>`:

```json
{
"BaseTemperature": 42,
"MaintenanceInterval": 360,
"OverheatInterval": 45,
"OverheatedThresholdTemperature": 199,
"TargetTemperature": 176,
"TemperatureChangeSpeed": 6
}
```

The OPC UA commander service in the cluster subscribes to this topic, receives the message, and writes the values to the OPC UA server. The commander service then publishes the result of the write operation to the topic `responseTopic`. If the operation succeeds, the message in the response topic looks like `{}`.
246 changes: 246 additions & 0 deletions samples/process-control/boiler-simulation.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
metadata description = 'This template deploys CRs that map to the boiler in the OPC simulator.'

/*****************************************************************************/
/* Deployment Parameters */
/*****************************************************************************/

param customLocationName string
param aioNamespaceName string

/*****************************************************************************/
/* Existing AIO instance */
/*****************************************************************************/

resource customLocation 'Microsoft.ExtendedLocation/customLocations@2021-08-31-preview' existing = {
name: customLocationName
}

resource namespace 'Microsoft.DeviceRegistry/namespaces@2025-07-01-preview' existing = {
name: aioNamespaceName
}

/*****************************************************************************/
/* Asset */
/*****************************************************************************/

var assetName = 'boiler'
var opcUaEndpointName = 'opc-ua-commander-0'

resource device 'Microsoft.DeviceRegistry/namespaces/devices@2025-07-01-preview' = {
name: 'opc-ua-commander'
parent: namespace
location: resourceGroup().location
extendedLocation: {
type: 'CustomLocation'
name: customLocation.id
}
properties: {
endpoints: {
outbound: {
assigned: {}
}
inbound: {
'${opcUaEndpointName}': {
endpointType: 'Microsoft.OpcUa'
address: 'opc.tcp://opcplc-000000:50000'
authentication: {
method: 'Anonymous'
}
}
}
}
}
}

resource asset 'Microsoft.DeviceRegistry/namespaces/assets@2025-07-01-preview' = {
name: assetName
parent: namespace
location: resourceGroup().location
extendedLocation: {
type: 'CustomLocation'
name: customLocation.id
}
properties: {
displayName: assetName
deviceRef: {
deviceName: device.name
endpointName: opcUaEndpointName
}
description: 'Multi-function boiler simulation.'

enabled: true
attributes: {
manufacturer: 'Contoso'
manufacturerUri: 'http://www.contoso.com/boilers'
model: 'Oven-003'
productCode: '12345C'
hardwareRevision: '2.3'
softwareRevision: '14.1'
serialNumber: '12345'
documentationUri: 'http://docs.contoso.com/boilers/manual'
}

datasets: [
{
name: 'boiler-simple-write'
dataPoints: [
{
name: 'Boiler #2'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=5017'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
{
name: 'AssetId'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6195'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
{
name: 'DeviceHealth'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6198'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
{
name: 'Manufacturer'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6202'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
{
name: 'ManufacturerUri'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6203'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
{
name: 'BaseTemperature'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6210'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
{
name: 'CurrentTemperature'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6211'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
{
name: 'HeaterState'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6212'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
{
name: 'MaintenanceInterval'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6213'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
{
name: 'OverheatInterval'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6350'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
{
name: 'Overheated'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6214'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
{
name: 'OverheatedThresholdTemperature'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6215'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
{
name: 'TargetTemperature'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6217'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
{
name: 'TemperatureChangeSpeed'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6218'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
{
name: 'ProductCode'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6205'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
{
name: 'ProductInstanceUri'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6206'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
{
name: 'RevisionCounter'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6207'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
{
name: 'SerialNumber'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6208'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
{
name: 'SoftwareRevision'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6209'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
]
destinations: [
{
target: 'Mqtt'
configuration: {
topic: 'azure-iot-operations/data/oven-simple-write'
retain: 'Never'
qos: 'Qos1'
}
}
]
}
{
name: 'boiler-complex-write'
dataPoints: [
{
name: 'BoilerStatus'
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=15013'
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
}
]
destinations: [
{
target: 'Mqtt'
configuration: {
topic: 'azure-iot-operations/data/oven-complex-write'
retain: 'Never'
qos: 'Qos1'
}
}
]
}

]

managementGroups: [
{
name: 'boiler-call'
actions: [
{
name: 'Switch'
targetUri: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=5017'
actionType: 'Call'
typeRef: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=5019'
}
]
}
{
name: 'boiler-explicit-write'
actions: [
{
name: 'simple-write'
targetUri: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6217'
actionType: 'Write'
}
]
}
]

defaultDatasetsConfiguration: '{"publishingInterval":1000,"samplingInterval":500,"queueSize":1}'
defaultEventsConfiguration: '{"publishingInterval":1000,"samplingInterval":500,"queueSize":1}'
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"@context": [
"dtmi:dtdl:context;4",
"dtmi:dtdl:extension:mqtt;2",
"dtmi:dtdl:extension:requirement;1"
],
"@id": "dtmi:opcua:write;1",
"@type": [
"Interface",
"Mqtt"
],
"payloadFormat": "custom/0",
"commandTopic": "{ex:namespace}/asset-operations/{ex:asset}/{ex:dataset}",
"schemas": [
],
"contents": [
{
"@type": "Command",
"name": "WriteDataset",
"request": {
"name": "WriteDatasetRequest",
"schema": "bytes"
},
"response": {
"name": "WriteDatasetResponse",
"schema": "bytes"
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/mrpc.client.generated.v3/mrpc.client.generated.v3.csproj" />
<Project Path="src/write.dataset.client/write.dataset.client.csproj" />
</Folder>
</Solution>
Loading