Skip to content

Commit ea10e01

Browse files
authored
Add process control sample (#148)
## Purpose Add process control sample ## Does this introduce a breaking change? <!-- Mark one with an "x". --> ``` [ ] Yes [x] No ``` ## Pull Request Type What kind of change does this Pull Request introduce? <!-- Please check the one that applies to this PR using "x". --> ``` [ ] Bugfix [ ] Feature [ ] Code style update (formatting, local variables) [ ] Refactoring (no functional changes, no api changes) [x] Documentation content changes [ ] Other... Please describe: ```
1 parent e0a0929 commit ea10e01

File tree

14 files changed

+993
-0
lines changed

14 files changed

+993
-0
lines changed

samples/process-control/README.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# OPC UA process control
2+
3+
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.
4+
5+
The OPC-UA commander:
6+
7+
- 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.
8+
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.
9+
- Subscribes to MQTT topic `{AioNamespace}/asset-operations/{AssetId}/{DatasetName}/` for data-set write operations.
10+
- Subscribes to MQTT topic `{AioNamespace}/asset-operations/{AssetId}/{ManagementGroupName}/` for call operations and explicit write.
11+
- Subscribes to MQTT topic `{AioNamespace}/endpoint-operations/{InboundEndpointProfileName}/{ActionName}/` for endpoint operations.
12+
- On MQTT request/response create ad-hoc session based on the device associated with the namespace asset.
13+
- Validates write requests against the generated request schema.
14+
- Validates that the write request only contains data points that exist within the dataset.
15+
- Use write service calls to set all data-points at once.
16+
- Sends response to response topic property defined in MQTT message.
17+
18+
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).
19+
20+
This sample illustrates some of these capabilities using the OPC PLC simulator boiler.
21+
22+
## Prerequisites
23+
24+
To run the sample application, you need:
25+
26+
- 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).
27+
- 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).
28+
- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) version 2.67.0 or higher.
29+
- [NET 9 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/9.0)
30+
31+
## Deploy the simulator
32+
33+
The sample application uses the boiler in the OPC PLC simulator.
34+
35+
To deploy the OPC PLC simulator, run the following command:
36+
37+
```bash
38+
kubectl apply -f https://raw.githubusercontent.com/Azure-Samples/explore-iot-operations/main/samples/quickstarts/opc-plc-deployment.yaml
39+
```
40+
41+
> [!CAUTION]
42+
> 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).
43+
44+
45+
## Configure the device and namespace assets
46+
47+
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:
48+
49+
```bash
50+
wget https://raw.githubusercontent.com/Azure-Samples/explore-iot-operations/main/samples/process-control/boiler-simulation.bicep -O boiler-simulation.bicep
51+
52+
AIO_NAMESPACE_NAME=<YOUR_AIO_NAMESPACE_NAME>
53+
RESOURCE_GROUP=<YOUR_RESOURCE_GROUP_NAME>
54+
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
55+
CUSTOM_LOCATION_NAME=$(az iot ops list -g $RESOURCE_GROUP --query "[0].extendedLocation.name" -o tsv | awk -F'/' '{print $NF}')
56+
57+
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
58+
```
59+
60+
You can review these resources in the Operations experience web UI or by running the following commands:
61+
62+
```bash
63+
# View device details
64+
az iot ops ns device query --instance <your instance name> -g <your resource group>
65+
# View namespace asset details
66+
az iot ops ns asset query --instance <your instance name> -g <your resource group>
67+
```
68+
69+
## Configure and run application
70+
71+
Make sure the `process-control-demo/src/write.dataset.client/appsettings.json` file contains the correct values for:
72+
73+
- `HostName` and `Port` in the `ConnectionStrings` setting.
74+
- `Namespace`: typically `azure-iot-operations` if you used the quickstarts deployment.
75+
- `AssetName`: `boiler` if you used the `boiler-simulation.bicep` file to add the device and namespace asset.
76+
- `DatasetName`: `boiler-simple-write` if you used the `boiler-simulation.bicep` file to add the device and namespace asset.
77+
78+
To run the application:
79+
80+
```bash
81+
dotnet run --project process-control-demo/src/write.dataset.client/write.dataset.client.csproj
82+
```
83+
84+
### Usage
85+
86+
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>`:
87+
88+
```json
89+
{
90+
"BaseTemperature": 42,
91+
"MaintenanceInterval": 360,
92+
"OverheatInterval": 45,
93+
"OverheatedThresholdTemperature": 199,
94+
"TargetTemperature": 176,
95+
"TemperatureChangeSpeed": 6
96+
}
97+
```
98+
99+
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 `{}`.
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
metadata description = 'This template deploys CRs that map to the boiler in the OPC simulator.'
2+
3+
/*****************************************************************************/
4+
/* Deployment Parameters */
5+
/*****************************************************************************/
6+
7+
param customLocationName string
8+
param aioNamespaceName string
9+
10+
/*****************************************************************************/
11+
/* Existing AIO instance */
12+
/*****************************************************************************/
13+
14+
resource customLocation 'Microsoft.ExtendedLocation/customLocations@2021-08-31-preview' existing = {
15+
name: customLocationName
16+
}
17+
18+
resource namespace 'Microsoft.DeviceRegistry/namespaces@2025-07-01-preview' existing = {
19+
name: aioNamespaceName
20+
}
21+
22+
/*****************************************************************************/
23+
/* Asset */
24+
/*****************************************************************************/
25+
26+
var assetName = 'boiler'
27+
var opcUaEndpointName = 'opc-ua-commander-0'
28+
29+
resource device 'Microsoft.DeviceRegistry/namespaces/devices@2025-07-01-preview' = {
30+
name: 'opc-ua-commander'
31+
parent: namespace
32+
location: resourceGroup().location
33+
extendedLocation: {
34+
type: 'CustomLocation'
35+
name: customLocation.id
36+
}
37+
properties: {
38+
endpoints: {
39+
outbound: {
40+
assigned: {}
41+
}
42+
inbound: {
43+
'${opcUaEndpointName}': {
44+
endpointType: 'Microsoft.OpcUa'
45+
address: 'opc.tcp://opcplc-000000:50000'
46+
authentication: {
47+
method: 'Anonymous'
48+
}
49+
}
50+
}
51+
}
52+
}
53+
}
54+
55+
resource asset 'Microsoft.DeviceRegistry/namespaces/assets@2025-07-01-preview' = {
56+
name: assetName
57+
parent: namespace
58+
location: resourceGroup().location
59+
extendedLocation: {
60+
type: 'CustomLocation'
61+
name: customLocation.id
62+
}
63+
properties: {
64+
displayName: assetName
65+
deviceRef: {
66+
deviceName: device.name
67+
endpointName: opcUaEndpointName
68+
}
69+
description: 'Multi-function boiler simulation.'
70+
71+
enabled: true
72+
attributes: {
73+
manufacturer: 'Contoso'
74+
manufacturerUri: 'http://www.contoso.com/boilers'
75+
model: 'Oven-003'
76+
productCode: '12345C'
77+
hardwareRevision: '2.3'
78+
softwareRevision: '14.1'
79+
serialNumber: '12345'
80+
documentationUri: 'http://docs.contoso.com/boilers/manual'
81+
}
82+
83+
datasets: [
84+
{
85+
name: 'boiler-simple-write'
86+
dataPoints: [
87+
{
88+
name: 'Boiler #2'
89+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=5017'
90+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
91+
}
92+
{
93+
name: 'AssetId'
94+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6195'
95+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
96+
}
97+
{
98+
name: 'DeviceHealth'
99+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6198'
100+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
101+
}
102+
{
103+
name: 'Manufacturer'
104+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6202'
105+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
106+
}
107+
{
108+
name: 'ManufacturerUri'
109+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6203'
110+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
111+
}
112+
{
113+
name: 'BaseTemperature'
114+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6210'
115+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
116+
}
117+
{
118+
name: 'CurrentTemperature'
119+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6211'
120+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
121+
}
122+
{
123+
name: 'HeaterState'
124+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6212'
125+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
126+
}
127+
{
128+
name: 'MaintenanceInterval'
129+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6213'
130+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
131+
}
132+
{
133+
name: 'OverheatInterval'
134+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6350'
135+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
136+
}
137+
{
138+
name: 'Overheated'
139+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6214'
140+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
141+
}
142+
{
143+
name: 'OverheatedThresholdTemperature'
144+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6215'
145+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
146+
}
147+
{
148+
name: 'TargetTemperature'
149+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6217'
150+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
151+
}
152+
{
153+
name: 'TemperatureChangeSpeed'
154+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6218'
155+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
156+
}
157+
{
158+
name: 'ProductCode'
159+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6205'
160+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
161+
}
162+
{
163+
name: 'ProductInstanceUri'
164+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6206'
165+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
166+
}
167+
{
168+
name: 'RevisionCounter'
169+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6207'
170+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
171+
}
172+
{
173+
name: 'SerialNumber'
174+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6208'
175+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
176+
}
177+
{
178+
name: 'SoftwareRevision'
179+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6209'
180+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
181+
}
182+
]
183+
destinations: [
184+
{
185+
target: 'Mqtt'
186+
configuration: {
187+
topic: 'azure-iot-operations/data/oven-simple-write'
188+
retain: 'Never'
189+
qos: 'Qos1'
190+
}
191+
}
192+
]
193+
}
194+
{
195+
name: 'boiler-complex-write'
196+
dataPoints: [
197+
{
198+
name: 'BoilerStatus'
199+
dataSource: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=15013'
200+
dataPointConfiguration: '{"samplingInterval":500,"queueSize":1}'
201+
}
202+
]
203+
destinations: [
204+
{
205+
target: 'Mqtt'
206+
configuration: {
207+
topic: 'azure-iot-operations/data/oven-complex-write'
208+
retain: 'Never'
209+
qos: 'Qos1'
210+
}
211+
}
212+
]
213+
}
214+
215+
]
216+
217+
managementGroups: [
218+
{
219+
name: 'boiler-call'
220+
actions: [
221+
{
222+
name: 'Switch'
223+
targetUri: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=5017'
224+
actionType: 'Call'
225+
typeRef: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=5019'
226+
}
227+
]
228+
}
229+
{
230+
name: 'boiler-explicit-write'
231+
actions: [
232+
{
233+
name: 'simple-write'
234+
targetUri: 'nsu=http://microsoft.com/Opc/OpcPlc/Boiler;i=6217'
235+
actionType: 'Write'
236+
}
237+
]
238+
}
239+
]
240+
241+
defaultDatasetsConfiguration: '{"publishingInterval":1000,"samplingInterval":500,"queueSize":1}'
242+
defaultEventsConfiguration: '{"publishingInterval":1000,"samplingInterval":500,"queueSize":1}'
243+
}
244+
}
245+
246+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"@context": [
3+
"dtmi:dtdl:context;4",
4+
"dtmi:dtdl:extension:mqtt;2",
5+
"dtmi:dtdl:extension:requirement;1"
6+
],
7+
"@id": "dtmi:opcua:write;1",
8+
"@type": [
9+
"Interface",
10+
"Mqtt"
11+
],
12+
"payloadFormat": "custom/0",
13+
"commandTopic": "{ex:namespace}/asset-operations/{ex:asset}/{ex:dataset}",
14+
"schemas": [
15+
],
16+
"contents": [
17+
{
18+
"@type": "Command",
19+
"name": "WriteDataset",
20+
"request": {
21+
"name": "WriteDatasetRequest",
22+
"schema": "bytes"
23+
},
24+
"response": {
25+
"name": "WriteDatasetResponse",
26+
"schema": "bytes"
27+
}
28+
}
29+
]
30+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<Solution>
2+
<Folder Name="/src/">
3+
<Project Path="src/mrpc.client.generated.v3/mrpc.client.generated.v3.csproj" />
4+
<Project Path="src/write.dataset.client/write.dataset.client.csproj" />
5+
</Folder>
6+
</Solution>

0 commit comments

Comments
 (0)