Skip to main content

Assets Metadata and UNS MQTT Topic Structure

Status

[For this library of ADRs, mark the most applicable status at which it was stored in the original project. This can help provide context and validity for folks reviewing this ADR. If it has been deprecated you can add a note on why and date it.]

  • Draft
  • Proposed
  • AcceptedS
  • Deprecated

Context

For the current prototype, we consider two production sites (ex: DK/HI and FR/CH). Each site will have one area and three production lines. Each production line have several assets (edge nodes) that produces simulated tags through Kepware OPC UA Server.

Simulated Lines

We consider the following metadata:

CountrySiteAreaProductionLineAssetNameNodeIDTagNameQueueSizeObservabilityModeSampling Interval Milliseconds
DKHI24ALine01Unit01ns=2;s=Line01.Unit01.AlarmAlarm1none100
DKHI24ALine01Unit01ns=2;s=Line01.Unit01.BatchIDBatchID1none100
DKHI24ALine01Unit01ns=2;s=Line01.Unit01.InfeedInfeed1none100
DKHI24ALine01Unit01ns=2;s=Line01.Unit01.ManualStopManualStop1none100
DKHI24ALine01Unit01ns=2;s=Line01.Unit01.OutfeedOutfeed1none100
DKHI24ALine01Unit01ns=2;s=Line01.Unit01.ProductIDProductID1none100
DKHI24ALine01Unit01ns=2;s=Line01.Unit01.RunningRunning1none100
DKHI24ALine01Unit01ns=2;s=Line01.Unit01.WasteWaste1none100
DKHI24ALine01Unit02ns=2;s=Line01.Unit02.AlarmAlarm1none100
DKHI24ALine01Unit02ns=2;s=Line01.Unit02.InfeedInfeed1none100
DKHI24ALine01Unit02ns=2;s=Line01.Unit02.ManualStopManualStop1none100
DKHI24ALine01Unit02ns=2;s=Line01.Unit02.OutfeedOutfeed1none100
DKHI24ALine01Unit02ns=2;s=Line01.Unit02.RunningRunning1none100
DKHI24ALine01Unit02ns=2;s=Line01.Unit02.WasteWaste1none100
DKHI24ALine01Unit03ns=2;s=Line01.Unit03.AlarmAlarm1none100
DKHI24ALine01Unit03ns=2;s=Line01.Unit03.InfeedInfeed1none100
DKHI24ALine01Unit03ns=2;s=Line01.Unit03.ManualStopManualStop1none100
DKHI24ALine01Unit03ns=2;s=Line01.Unit03.OutfeedOutfeed1none100
DKHI24ALine01Unit03ns=2;s=Line01.Unit03.RunningRunning1none100
DKHI24ALine01Unit03ns=2;s=Line01.Unit03.WasteWaste1none100
DKHI24ALine02Unit01ns=2;s=Line02.Unit01.AlarmAlarm1none100
DKHI24ALine02Unit01ns=2;s=Line02.Unit01.BatchIDBatchID1none100
DKHI24ALine02Unit01ns=2;s=Line02.Unit01.InfeedInfeed1none100
DKHI24ALine02Unit01ns=2;s=Line02.Unit01.ManualStopManualStop1none100
DKHI24ALine02Unit01ns=2;s=Line02.Unit01.OutfeedOutfeed1none100
DKHI24ALine02Unit01ns=2;s=Line02.Unit01.ProductIDProductID1none100
DKHI24ALine02Unit01ns=2;s=Line02.Unit01.RunningRunning1none100
DKHI24ALine02Unit01ns=2;s=Line02.Unit01.WasteWaste1none100
DKHI24ALine02Unit02ns=2;s=Line02.Unit02.AlarmAlarm1none100
DKHI24ALine02Unit02ns=2;s=Line02.Unit02.InfeedInfeed1none100
DKHI24ALine02Unit02ns=2;s=Line02.Unit02.ManualStopManualStop1none100
DKHI24ALine02Unit02ns=2;s=Line02.Unit02.OutfeedOutfeed1none100
DKHI24ALine02Unit02ns=2;s=Line02.Unit02.RunningRunning1none100
DKHI24ALine02Unit02ns=2;s=Line02.Unit02.WasteWaste1none100
DKHI24ALine02Unit03ns=2;s=Line02.Unit03.AlarmAlarm1none100
DKHI24ALine02Unit03ns=2;s=Line02.Unit03.InfeedInfeed1none100
DKHI24ALine02Unit03ns=2;s=Line02.Unit03.ManualStopManualStop1none100
DKHI24ALine02Unit03ns=2;s=Line02.Unit03.OutfeedOutfeed1none100
DKHI24ALine02Unit03ns=2;s=Line02.Unit03.RunningRunning1none100
DKHI24ALine02Unit03ns=2;s=Line02.Unit03.WasteWaste1none100
FRCHNNPILine01Unit01ns=2;s=Line01.Unit01.AlarmAlarm1none100
FRCHNNPILine01Unit01ns=2;s=Line01.Unit01.BatchIDBatchID1none100
FRCHNNPILine01Unit01ns=2;s=Line01.Unit01.InfeedInfeed1none100
FRCHNNPILine01Unit01ns=2;s=Line01.Unit01.ManualStopManualStop1none100
FRCHNNPILine01Unit01ns=2;s=Line01.Unit01.OutfeedOutfeed1none100
FRCHNNPILine01Unit01ns=2;s=Line01.Unit01.ProductIDProductID1none100
FRCHNNPILine01Unit01ns=2;s=Line01.Unit01.RunningRunning1none100
FRCHNNPILine01Unit01ns=2;s=Line01.Unit01.WasteWaste1none100
FRCHNNPILine01Unit02ns=2;s=Line01.Unit02.AlarmAlarm1none100
FRCHNNPILine01Unit02ns=2;s=Line01.Unit02.InfeedInfeed1none100
FRCHNNPILine01Unit02ns=2;s=Line01.Unit02.ManualStopManualStop1none100
FRCHNNPILine01Unit02ns=2;s=Line01.Unit02.OutfeedOutfeed1none100
FRCHNNPILine01Unit02ns=2;s=Line01.Unit02.RunningRunning1none100
FRCHNNPILine01Unit02ns=2;s=Line01.Unit02.WasteWaste1none100
FRCHNNPILine01Unit03ns=2;s=Line01.Unit03.AlarmAlarm1none100
FRCHNNPILine01Unit03ns=2;s=Line01.Unit03.InfeedInfeed1none100
FRCHNNPILine01Unit03ns=2;s=Line01.Unit03.ManualStopManualStop1none100
FRCHNNPILine01Unit03ns=2;s=Line01.Unit03.OutfeedOutfeed1none100
FRCHNNPILine01Unit03ns=2;s=Line01.Unit03.RunningRunning1none100
FRCHNNPILine01Unit03ns=2;s=Line01.Unit03.WasteWaste1none100
FRCHNNPILine02Unit01ns=2;s=Line02.Unit01.AlarmAlarm1none100
FRCHNNPILine02Unit01ns=2;s=Line02.Unit01.BatchIDBatchID1none100
FRCHNNPILine02Unit01ns=2;s=Line02.Unit01.InfeedInfeed1none100
FRCHNNPILine02Unit01ns=2;s=Line02.Unit01.ManualStopManualStop1none100
FRCHNNPILine02Unit01ns=2;s=Line02.Unit01.OutfeedOutfeed1none100
FRCHNNPILine02Unit01ns=2;s=Line02.Unit01.ProductIDProductID1none100
FRCHNNPILine02Unit01ns=2;s=Line02.Unit01.RunningRunning1none100
FRCHNNPILine02Unit01ns=2;s=Line02.Unit01.WasteWaste1none100
FRCHNNPILine02Unit02ns=2;s=Line02.Unit02.AlarmAlarm1none100
FRCHNNPILine02Unit02ns=2;s=Line02.Unit02.InfeedInfeed1none100
FRCHNNPILine02Unit02ns=2;s=Line02.Unit02.ManualStopManualStop1none100
FRCHNNPILine02Unit02ns=2;s=Line02.Unit02.OutfeedOutfeed1none100
FRCHNNPILine02Unit02ns=2;s=Line02.Unit02.RunningRunning1none100
FRCHNNPILine02Unit02ns=2;s=Line02.Unit02.WasteWaste1none100
FRCHNNPILine02Unit03ns=2;s=Line02.Unit03.AlarmAlarm1none100
FRCHNNPILine02Unit03ns=2;s=Line02.Unit03.InfeedInfeed1none100
FRCHNNPILine02Unit03ns=2;s=Line02.Unit03.ManualStopManualStop1none100
FRCHNNPILine02Unit03ns=2;s=Line02.Unit03.OutfeedOutfeed1none100
FRCHNNPILine02Unit03ns=2;s=Line02.Unit03.RunningRunning1none100
FRCHNNPILine02Unit03ns=2;s=Line02.Unit03.WasteWaste1none100

We expect it to be published into the Enterprise UNS with the following structure:

Enterprise/
└── FR/
└── CH/
└── NNPI
└── Line01
└── Unit01
├── Infeed
├── Outfeed
├── Waste
├── Running
├── ManualStop
├── Alarm
├── BatchID
└── ProductID
└── Unit02
├── Infeed
├── Outfeed
├── Waste
├── Running
├── ManualStop
└── Alarm
└── Unit03
├── Infeed
├── Outfeed
├── Waste
├── Running
├── ManualStop
└── Alarm
└── DK/
└── HI/
└── 24A
└── Line01
└── Unit01
├── Infeed
├── Outfeed
├── Waste
├── Running
├── ManualStop
├── Alarm
├── BatchID
└── ProductID
└── Unit02
├── Infeed
├── Outfeed
├── Waste
├── Running
├── ManualStop
└── Alarm
└── Unit03
├── Infeed
├── Outfeed
├── Waste
├── Running
├── ManualStop
└── Alarm

Decision

Metadata about assets needs to be stored and reused to create a UNS friendly topic structure into Azure IoT MQ.

Decision Drivers

  • We want to store the metadata in a central place, when the assets are created (batch)
  • We want to allow operator to update the metadata from this central place and ensure the changes are reflected in the Local and Enterprise UNS
  • We want to ensure the MQTT topic structure is consistent across the enterprise

Considered Options

We considered the following options to store the assets metadata:

  • Azure tags: as every asset is an Azure resource, we can benefit from the usual Azure tags to store the metadata.
  • Asset custom properties: Azure IoT Operations assets can have a key/value property bags with any custom properties. It is stored directly at the resource level in Azure, and it can be set using the Azure CLI, ARM Templates (batch operation) or through the Azure IoT Operations portal (manual).
  • IoT MQ State Store: The MQ state store is a distributed storage system within the Azure IoT Operations cluster. It allows to store key-value data that could be used for such kind of metadata.

Decision Conclusion

Azure IoT Operations asset custom properties seem to be the best candidate to store this metadata information and let operators update it in the future, BUT no automation is currently supported (Azure CLI / Bicep) to ingest these properties at scale.

For now, we will store it temporary in Azure resources tags, while starting a discussion with product group to understand when we will be able to use custom properties.

Another script will be responsible to extract metadata from the Azure resources tags and publish it into the Enterprise UNS. Once in the Enterprise UNS, it can be replicated into different production sites Local UNS (through MQTT bridge) and used inside data processor to build UNS topic path when data is ingested from Kepware simulated PLC, through the OPC UA broker.

What we imagine to have in the Enterprise UNS:

Meta/
└── DK/
└── HI/
└── 24A
└── Line01
└── Unit01
└── azure-iot-operations
└── data
└── kepware-opc-ua-connector
└── line01-unit01: { "UnsTopicName": "DK/HI/24A/Line01/Unit01" }
└── Outfeed: { "SpecificTagMetadata": "SpecificTagMetadataValue" }
└── line01-unit02: { "UnsTopicName": "DK/HI/24A/Line01/Unit02" }
└── Outfeed: { "SpecificTagMetadata": "SpecificTagMetadataValue" }
└── line01-unit03: { "UnsTopicName": "DK/HI/24A/Line01/Unit02" }
└── Outfeed: { "SpecificTagMetadata": "SpecificTagMetadataValue" }
└── FR/
└── CH/
└── NNPI
└── Line01
└── Unit01
└── azure-iot-operations
└── data
└── kepware-opc-ua-connector
└── line01-unit01: { "UnsTopicName": "FR/CH/NNPI/Line01/Unit01" }
└── Outfeed: { "SpecificTagMetadata": "SpecificTagMetadataValue" }
└── line01-unit02: { "UnsTopicName": "FR/CH/NNPI/Line01/Unit02" }
└── Outfeed: { "SpecificTagMetadata": "SpecificTagMetadataValue" }
└── line01-unit03: { "UnsTopicName": "FR/CH/NNPI/Line01/Unit02" }
└── Outfeed: { "SpecificTagMetadata": "SpecificTagMetadataValue" }

Then, it's possible to replicate this structure into the Local UNS of each production site, for example in DK/HI:

Meta/
└── DK/
└── HI/
└── 24A
└── Line01
└── Unit01
└── azure-iot-operations
└── data
└── kepware-opc-ua-connector
└── line01-unit01: { "UnsTopicName": "DK/HI/24A/Line01/Unit01" }
└── Outfeed: { "SpecificTagMetadata": "SpecificTagMetadataValue" }
└── line01-unit02: { "UnsTopicName": "DK/HI/24A/Line01/Unit02" }
└── Outfeed: { "SpecificTagMetadata": "SpecificTagMetadataValue" }
└── line01-unit03: { "UnsTopicName": "DK/HI/24A/Line01/Unit02" }
└── Outfeed: { "SpecificTagMetadata": "SpecificTagMetadataValue" }

This will be used to create a reference data at the local UNS structure when we have a mapping between OPC-UA broker generated topic name and UNS desired destination. Something like:

{
"UnsTopicName": "DK/HI/24A/Line01/Unit01",
"OpcUaTopicName": "azure-iot-operations/data/kepware-opc-ua-connector/line01-unit01"
},
{
"UnsTopicName": "DK/HI/24A/Line01/Unit02",
"OpcUaTopicName": "azure-iot-operations/data/kepware-opc-ua-connector/line01-unit02"
},
{
"UnsTopicName": "DK/HI/24A/Line01/Unit03",
"OpcUaTopicName": "azure-iot-operations/data/kepware-opc-ua-connector/line01-unit03"
}

The we can use the data set to enrich the incoming data with the UNS topic path and route it to the right destination using dynamic output path in the data processor.

The final expected result for data in Local UNS will be the following:

DK/
└── HI/
└── 24A
└── Line01
└── Unit01
├── Infeed
├── Outfeed
├── Waste
├── Running
├── ManualStop
├── Alarm
├── BatchID
└── ProductID
└── Unit02
├── Infeed
├── Outfeed
├── Waste
├── Running
├── ManualStop
└── Alarm
└── Unit03
├── Infeed
├── Outfeed
├── Waste
├── Running
├── ManualStop
└── Alarm
└── Line02
└── Unit01
├── Infeed
├── Outfeed
├── Waste
├── Running
├── ManualStop
├── Alarm
├── BatchID
└── ProductID
└── Unit02
├── Infeed
├── Outfeed
├── Waste
├── Running
├── ManualStop
└── Alarm
└── Unit03
├── Infeed
├── Outfeed
├── Waste
├── Running
├── ManualStop
└── Alarm
└── LineFilling
└── RRU
├── Infeed
├── Outfeed
├── Waste
├── Running
├── ManualStop
├── Alarm
├── BatchID
└── ProductID
└── HQL
├── Infeed
├── Outfeed
├── Waste
├── Running
├── ManualStop
└── Alarm
└── MLD
├── Infeed
├── Outfeed
├── Waste
├── Running
├── ManualStop
└── Alarm

AI and automation capabilities described in this scenario should be implemented following responsible AI principles, including fairness, reliability, safety, privacy, inclusiveness, transparency, and accountability. Organizations should ensure appropriate governance, monitoring, and human oversight are in place for all AI-powered solutions.