Skip to content

Architecture

The MTP is built on two major conceptual pillars

  1. The DataAssembly, the base class for a standardized Data Interfaces between the POL and the PEA.

  2. The Service with Procedures and ParameterElement
    as abstraction and encapsulation of the process & phase oriented automation logic.

flowchart LR

    subgraph Engineering
        SMD["State Machine Design"]
        MTP20["MTP 2.0"]
        USL["User's State Logic"]
    end

    subgraph Runtime
        POL["POL"]
    end

    subgraph mtppy-rt [MTPPy 2.0 RunTime]
        SM@{ shape: subproc, label: "Service"}
        IO@{ shape: win-pane, label: "Data Assembly"}
        OPC@{ shape: subproc, label: "OPC UA Server"}
        SM <== read/write ==> IO
        OPC <== read/write ==> IO
    end

    subgraph mtppy-ld [MTPPy 2.0 Loader]
        PEA
    end

    SM -- calls> --> USL

    PEA -. loads> .- MTP20
    PEA -. loads> .- SMD 
    PEA -. loads> .- USL
    PEA -. creates> .-> IO
    PEA -. creates> .-> SM
    PEA -. configures> .-> OPC

    POL -. loads> .- MTP20
    POL -- send --> OPC
    OPC -- ack --> POL

    USL -. implements > .- SMD
    MTP20 -. implements > .- SMD

Data Assembly

DataAssembly provide the public interfaces to the information space of automation building blocks like drives, valves, pid controller, a.s.o, but also to services and the PEA itself.

Data Item Collection and Data Item

Internally, the DataAssembly is a specialization of a DataItemCollection, who organizes the access to the atomic elements, the DataItems. While the DataItemCollection implements the end user API for reading and writing to elements (e.g. d1.V = d2.VOp + 2), the DataItem handles the notification of subscribers, who want to be informed on changes.

Internal Write Attempts

Whenever some internal program code tries to write on an attribute of a DataAssembly, e.g.d1.V = 10 , internally the setter method of the associated DataItem is called (set_value(self, value) 1⃣. This method organizes the setting of a value in three steps:

1) Value conversion: This step is managed by the class W3cXmlDataTypes, and optimistically tries to convert everything incoming to a value that fits to the type of the DataItem 2⃣.

2) Value Processing: This step ends with assigning a value to the private _value-attribute that actually stores the data. Before, however, processing and perhaps modification is delegated to the int_write_cb 3⃣.

For instance, switching to an internal source in automated mode is achieved by writing d1.SrcIntAut = True. The internal callback checks if this write command is allowed at the current state, modifies related variables like e.g.d1.SrcIntÀct = True and eventually sets SrcIntAut = False as a signal for listeners that the request has been processed. The int_wrt_cb returns an accept flag 4⃣, that indicates the validity of the write attempt. If this flag is true, we finally set the private value store _value 5⃣.

Note

As this process is quite vulnerable to unlimited recursion the complete
value-processing step is guarded by a flag. This flag effectively prevents diving into the abyss of unlimited recursion, even if the functions called attempt to write to the Data Item that called the callback.

3) Notification of external subscribers: Once the previous step is finalized (indicated by a reset of the processing flag), the notification of external subscribers is delegated to a CommObject 6⃣. This object informs the communication layer, currently an OPC-UA-Server, about the change 7⃣, which the POL will see the next time it polls for a change event 8⃣.

The following sequence diagram illustrates the concept.

sequenceDiagram
    participant APP as Application
    participant DI as DataItem<br>d1
    participant CB as InternalCallback<br>int_wrt_cb
    participant CO as CommObject
    participant Node as Node
    participant OPC as OPC UA
    participant POL as POL 

    autonumber
    APP->>DI: d1.set_value(value=10)
    DI->>DI: converse(value)
    alt get_value() != conversed_value
        DI->>+CB: call int_wrt_cb(value)
        CB-->>-DI: True if acceptable change 
        alt true
            DI->>DI: _value = 10
        end
    end
    DI->>CO: _notify_external_subscribers()
    CO->>Node: write_value_callback(_value)
    Node-->>OPC: update
    loop Polling
        POL->>OPC: poll change event
    end

External Write Attempts

The second task of the CommObject is to serve as an endpoint to the OPCUAServer and notify the PEA on external write attempts. The CommObject calls the DataItem's _ext_wrt_cb method.

For instance, if the POL want's a service to switch into operations mode, it sends True to the respective variable, e.g. writes True on ns=2;s=rand_num_gen.StateOpOp. The associated ext_wrt_cb of the Service would be set_state_op_op.

The following sequence diagram illustrates the concept.

sequenceDiagram
    participant DI as DataItem<br>d1
    participant CB as ExternalCallback<br>ext_wrt_cb
    participant CO as CommObject
    participant Node as Node
    participant OPC as OPC UA
    participant POL as POL 

    autonumber
    POL->>OPC: set "ns=2#59;s=rand_num_gen.StateOpOp" to True
    OPC->>Node: update node
    Node->>CO: notify subscribers
    CO->>+CB: call ext_wrt_cb
    CB->>DI: check and set values accordingly

Callback Pattern

Note

The CommObject's design with its two-sided callbacks follows the Dependency Inversion Principle (DIP). It is the only element that knows of both worlds and so effectively decouples the PEA from the underlying communication stack:

flowchart LR
    ComObject --> DataItem
    ComObject --> OPC 

Handshake and Echo Suppression

A ReadWriteAccess item is a request the PEA acknowledges, not a raw store. When the PEA processes an accepted request it must accept it (run the logic) and then reset the request to its neutral value so that (a) the POL sees the request clear, and (b) the PEA does not reprocess the same request on its next poll. That reset is the handshake.

The problem: the PEA is subscribed to the very node it just reset. On OPC UA that self-subscription raises a datachange, which would call the item's ext_wrt_cb again -- the PEA reacting to its own write. Left alone this is an infinite echo loop: process -> reset -> (datachange) -> process -> reset -> ...

The handshake is broken with two cooperating parts:

  1. DataItem.write_value(value) -- the PEA-side half. It stores value locally (the reset, usually 0) and pushes it to the communication nodes, but does not invoke ext_wrt_cb. It also tells each node it is the PEA that wrote, via write_value_callback(value, suppress_echo=True).
  2. CommunicationObject(suppress_echo=True) -- the protocol-side half. On OPC UA it suppresses the self-subscription datachange for that node/value so the echo never re-enters ext_wrt_cb. (On polled protocols such as Modbus the flag is accepted but is a no-op, since there is no datachange subscription to suppress.)

This is distinct from the plain _is_notifying guard, which only protects against recursion within a single int_wrt_cb call; suppress_echo closes the cross-cycle loop that would otherwise close between polls.

sequenceDiagram
    participant POL as POL
    participant OPC as OPC UA
    participant CO as CommObject
    participant DI as DataItem
    participant CB as ext_wrt_cb<br>(PEA logic)

    POL->>OPC: write request = 1 (e.g. StateOpOp = True)
    OPC->>CO: datachange
    CO->>CB: ext_wrt_cb(1)
    CB->>DI: run logic (switch mode)
    CB->>DI: write_value(0)  % handshake reset
    DI->>CO: write_value_callback(0, suppress_echo=True)
    Note over CO,OPC: reset pushed to POL,<br/>self datachange suppressed
    OPC--xCO: (echo datachange suppressed)
    Note over POL: sees request = 0 -> done

Data Assembly and Data Object Library Class Hierarchy

Currently only selected data assemblies that are necessarily relevant for soft sensors are implemented. This includes IndicatorElements (BinView, AnaView, DIntView, and StringView), ParameterElement (BinServParam, AnaServParam, DIntServParam, StringServParam), InputElements (AnaProcessValueIn, BinProcessValueIn, DIntProcessValueIn, StringProcessValueIn), and ActiveElements (AnaVlv, BinVlv, AnaDrv, BinDrv, PIDCtrl, MonAnaDrv, MonBinDrv, MonAnaVlv, MonBinVlv). Nevertheless, the architecture of the package allows the development of further DataAssembly classes.

classDiagram
    direction LR
    class DataAssembly {
        +str tag_name
        +str tag_description
        +str mtpObjectType
    }

    class DataItem {
        +str tag_name
        +W3cXmlDataTypes w3c_xml_data_type
        +MTPAccess access
        +any init_value
        +str description
        -any _value
        +OPCUACommunicationObject comm_obj
        -callable int_wrt_cb
        -callable ext_wrt_cb
        +get_value()
        +set_value(any)
        +attach_communication_object(OPCUACommunicationObject)
    }

    class OPCUACommunicationObject {
        +opcua_node_obj
        +node_id
        +write_value_callback(value)
    }

    class ParameterElement
    class InputElement
    class IndicatorElement
    class ActiveElement
    class ServiceElement

    class AnaView
    class BinView
    class DIntView
    class StringView
    class AnaServParam
    class BinServParam
    class DIntServParam
    class StringServParam
    class AnaProcessValueIn
    class BinProcessValueIn
    class DIntProcessValueIn
    class StringProcessValueIn
    class AnaDrv
    class BinDrv
    class AnaVlv
    class BinVlv
    class PIDCtrl
    class MonAnaDrv
    class MonBinDrv
    class MonAnaVlv
    class MonBinVlv

    DataItemCollection "1" *-- "0..*" DataItem
    DataAssembly --|> DataItemCollection
    DataItem "1" *-- "1" OPCUACommunicationObject
    DataAssembly <|-- ParameterElement
    DataAssembly <|-- InputElement
    DataAssembly <|-- IndicatorElement
    DataAssembly <|-- ActiveElement
    DataAssembly <|-- ServiceElement
    ServiceElement <|-- ServiceControl
    ServiceElement <|-- ProcedureHealthView

    IndicatorElement <|-- AnaView
    IndicatorElement <|-- BinView
    IndicatorElement <|-- DIntView
    IndicatorElement <|-- StringView

    ParameterElement <|-- AnaServParam
    ParameterElement <|-- BinServParam
    ParameterElement <|-- DIntServParam
    ParameterElement <|-- StringServParam

    InputElement <|-- AnaProcessValueIn
    InputElement <|-- BinProcessValueIn
    InputElement <|-- DIntProcessValueIn
    InputElement <|-- StringProcessValueIn

    ActiveElement <|-- AnaDrv
    ActiveElement <|-- BinDrv
    ActiveElement <|-- AnaVlv
    ActiveElement <|-- BinVlv
    ActiveElement <|-- PIDCtrl
    AnaDrv <|-- MonAnaDrv
    BinDrv <|-- MonBinDrv
    AnaVlv <|-- MonAnaVlv
    BinVlv <|-- MonBinVlv

Due to open-source paradigm, the MTP manifest is extendable, e.g. safety relations or energy consumption aspects that currently not defined in the MTP standard.

Services

MTPPy2.0 provides an MTP 2.0 compatible State Engine that executes the user's application logic. The state engine is controlled by MTP 2.0 commands that are received via OPC UA. A managed process memory hosts MTP 2.0 compatible DataAssembly objects (e.g. AnaView) and links the independent OPC UA thread, the central Startup/Shudown thread and the different State Engine threads that are instantiated for every running service.

Single-Writer Principle

Every service owns one long-lived run task (an asyncio loop). The single-writer principle requires that all service state -- the state machine's act_state, the CommandEn mask, and the ServiceControl data items (StateCur, ProcedureReq/Cur/Op/Ext, a.s.o.) -- is mutated only on that run task. Any caller on another thread (the OPC UA thread, the PEA startup thread, application logic) must funnel its mutation onto the run task.

The funnel is implemented as two layers on DataItemCollection, the base class of every control-plane assembly (ServiceControl, ServiceOperationMode, ServiceSourceMode, ...):

  • Layer 1 -- per-write. Every write through the collection (da[KEY] = v, da.KEY = v, and hence DataItem.set_value) is routed through DataItemCollection.run_on_loop. This makes even a stray, direct scalar write from a foreign thread (an application flipping a setpoint, a mode flag, or PROCEDURE_REQ) single-writer: it is scheduled onto the run task as one callback.
  • Layer 2 -- per-operation. The guarded setters -- set_state_*, set_src_*, set_command_op/ext, set_procedure_op/int/ext -- each post their whole read-decide-write-callback sequence as a single closure onto the run task (an X() wrapper calling _X_impl()). This keeps a transition atomic: the read of the guard flags, the decision, the writes, and the mode-transition callbacks (which mutate act_state / CommandEn) all run together on the run task, so no other operation can interleave between the read and the write.

The two layers compose cleanly because run_on_loop is inline when the caller is already on the bound loop and deferred only from a foreign thread:

flowchart TD
    subgraph Foreign[Foreign thread]
        A["da[KEY] = v  /  set_state_aut_aut(v)"]
    end
    subgraph RunTask[Service run task (asyncio loop)]
        B1["Layer 1: per-write post"]
        B2["Layer 2: whole-op post"]
        C[("state: act_state, CommandEn, Procedure*, StateCur")]
    end
    A -- "call_soon_threadsafe" --> B1
    A -- "call_soon_threadsafe" --> B2
    B1 --> C
    B2 --> C
    C -. "run-task write: inline (no re-post)" .-> C

A write that occurs inside a posted operation (for example the act_state mutation triggered by the offline -> automatic _exit_off cascade) is already on the run task, so it executes inline rather than being re-queued -- the cascade is therefore single-writer for free, and no re-post loop can occur.

The loop is bound once by Service.attach_loop, which attaches it to ThreadControl and to service_control plus its service_op_mode / service_src_mode sub-assemblies. A collection that is never bound (tests, offline use) keeps the legacy synchronous, inline behaviour.

Consequences.

  • Top-level run_on_task wrappers at activation call sites are no longer needed for state held in DataItems -- the assemblies self-funnel.
  • A compound logical update (e.g. setpoint and setpoint-valid together) must be expressed as one operation so it is not torn across two loop callbacks: either a sanctioned setter, or an explicit collection.run_on_loop(lambda: ...). Raw per-field writes from a foreign thread are individually serialized but are not atomic with respect to each other.
  • Mode transitions (PID manual -> automatic, internal -> external setpoint) are expressed through the guarded setters, never by poking the underlying *_ACT flags directly -- the setter is what preserves the guard, the transition, and the callback side-effects. ## PNO MTP 2.0 derived Classes The architecture of MTPPy2.0 is depicted in the following Figure in the form of a simplified class diagram.
classDiagram
    class PEA {
        +PEA(endpoint)
        +add_data_assembly(DataAssembly)
        +add_alarm(Alarm)
        +add_process_value_in(InputElement)
        +add_service(Service)
        +start()
    }

    class PeaOPCUAServer {
        +init_opcua_server()
        +run_opcua_server(data_assemblies, service_set)
    }

    class Alarm {
        +str tag_name
        +DataAssembly data_assembly
        +DataItem trigger
        +object trigger_value
        +str message
        +bool ack_required
        +int severity
    }

    class MyService {
    }

    class Service {
        <<abstract>>
        +command_execution(int)
        +state_change()
        +add_configuration_parameter(ParameterElement)
        +add_procedure(Procedure)
        +add_ressource(DataAssembly)
        +apply_procedure_parameters()
        +idle()* bool
        +starting()* bool
        +execute()* bool
        +...()* bool
    }

    class Procedure {
        +Procedure(TBD)
        +add_procedure_parameter(ParameterElement)
        +add_process_value_in(InputElement)
        +add_process_value_out(IndicatorElement)
        +add_report_value(IndicatorElement)
        +apply_procedure_parameters()
    }

    class ServiceControl {
        +ServiceControl(TBD)
        +set_command_op(int)
        +set_procedure_req(int)
    }

    class StateMachine {
        +StateMachine(TBD)
        +command_execution(int)
        +state_change()
    }

    class CommandEnControl {
        +set_default()
        +disable_all()
        +is_enabled(str)
        +get_command_en()
        +set_command_en(str, bool)
        +execute(str)
    }

    PEA "1" *-- "0..*" Service
    PEA "1" *-- "0..*" Alarm
    PEA "1" *-- "1" PeaOPCUAServer
    Alarm "0..*" --> "1" DataAssembly : references
    Alarm "0..*" --> "1" DataItem : trigger
    Service "1" *-- "1" StateMachine
    Service "1" *-- "0..*" ParameterElement
    Service "1" *--  "0..*" DataAssembly
    StateMachine "1" *-- "1" ServiceControl
    StateMachine "1" *-- "1" CommandEnControl
    Service "1" *-- "1..*" Procedure
    MyService --|> Service
  • Service: represents single services and provides methods to add configuration parameters, procedures, and resources. Single states of the state machine, e.g. idle, starting, execute, completing, etc., are defined as abstract methods that must be further defined by the user for each concrete service. The class Service contains a ServiceControl, a StateMachine, and a ThreadControl.
  • Procedures: instances of this class can be added to a service using the corresponding method in the class Service. The procedure itself is defined by means of related procedure parameters, process value in, process value out, report values, and a procedure health view.
  • ServiceControl: represents an object to control state changes that can be executed by the state machine. It manages the available procedures, the service operation/source modes, operator interaction, and OS level. Each time any command is received from the OPC UA server, a check is made whether the incoming command can be executed from the current state.
  • DataAssembly: currently only selected data assemblies that are necessarily relevant for soft sensors are implemented. This includes IndicatorElements (BinView, AnaView, DIntView, and StringView), ParameterElements (BinServParam, AnaServParam, DIntServParam, StringServParam), InputElements (AnaProcessValueIn, BinProcessValueIn, DIntProcessValueIn, StringProcessValueIn), and ActiveElements (AnaVlv, BinVlv, AnaDrv, BinDrv, PIDCtrl, MonAnaDrv, MonBinDrv, MonAnaVlv, MonBinVlv). Nevertheless, the architecture of the package allows the development of further DataAssembly classes.
  • Alarm: is the only public Python model of the initial Part 6 implementation. PEA.add_alarm() stores its static AlarmSet.Base metadata, which references a direct DataItem of an existing DataAssembly. AlarmGroup, AlarmMessage, AlarmText, and their ID links are generated CAEX objects rather than Python runtime models. The POL evaluates the equality condition; the PEA installs no callbacks and maintains no alarm state.

Due to open-source paradigm, the MTP manifest is extendable, e.g. safety relations or energy consumption aspects that currently not defined in the MTP standard.

Detailed Class Diagram

classDiagram
    class PEA {
        +str endpoint
        +dict service_set
        +dict data_assemblies
        +dict process_value_ins
        +dict process_value_outs
        +dict alarms
        +PeaOPCUAServer pea_opcua_server
        +SubscriptionList subscription_list
        +add_data_assembly(DataAssembly)
        +add_alarm(Alarm)
        +add_process_value_in(InputElement)
        +add_service(Service)
        +start()
    }

    class PeaOPCUAServer {
        +str endpoint
        +Server opcua_server
        +int opcua_ns
        +int opcua_ns_index
        +OPCUAOptionsProvider opcua_options_provider
        +SubscriptionList subscription_list
        +init_opcua_server()
        +run_opcua_server(data_assemblies, service_set)
    }

    class Alarm {
        +str tag_name
        +DataAssembly data_assembly
        +DataItem trigger
        +object trigger_value
        +str message
        +bool ack_required
        +int severity
    }

    class SubscriptionList {
        +dict sub_list
        +append(node_id, cb_value_change)
        +get_nodeid_list()
        +get_callback(node_id)
    }

    class Marshalling {
        +SubscriptionList subscription_list
        +import_subscription_list(SubscriptionList)
        +datachange_notification(node, val, data)
    }

    class Service {
        <<abstract>>
        +str name
        +ThreadControl thread_ctrl
        +dict configuration_parameters
        +dict ressources
        +ServiceControl service_control
        +StateMachine state_machine
        +command_execution(int)
        +state_change()
        +add_configuration_parameter(ParameterElement)
        +add_procedure(Procedure)
        +add_ressource(DataAssembly)
        +idle()* bool
        +starting()* bool
        +execute()* bool
    }

    class ThreadControl {
        +request_state(str, callable)
        +reallocate_running_thread()
    }

    class StateMachine {
        +ServiceControl service_control
        +ServiceOperationMode service_op_mode
        +ServiceSourceMode service_src_mode
        +Callable execution_routine
        +CommandEnControl command_en_ctrl
        +CommandCodes command_codes
        +StateCodes state_codes
        +int act_state
        +int prev_state
        +set_command_op(int)
        +set_command_int(int)
        +set_command_ext(int)
        +command_execution(int)
        +start()
        +restart()
        +complete()
        +pause()
        +resume()
        +reset()
        +hold()
        +unhold()
        +stop()
        +abort()
        +state_change()
    }

    class CommandEnControl {
        +dict command_en
        +bool hold_enabled
        +bool pause_enabled
        +bool restart_enabled
        +set_default()
        +disable_all()
        +is_enabled(str)
        +get_command_en()
        +set_command_en(str, bool)
        +enable_pause_loop(bool)
        +enable_restart(bool)
        +execute(str)
    }

    class ServiceControl {
        +dict procedures
        +Callable execution_procedure
        +OSLevelBaseFunction os_level
        +ServiceOperationMode service_op_mode
        +ServiceSourceMode service_src_mode
        +ServiceOperatorInteraction service_operator_interaction
        +set_command_op(int)
        +set_command_ext(int)
        +set_command_int(int)
        +set_procedure_op(int)
        +set_procedure_int(int)
        +set_procedure_ext(int)
        +apply_proc_param()
        +apply_config_param()
    }

    class ServiceOperationMode {
        +bool switch_to_offline_mode_allowed
        +list enter_offline_callbacks
        +list exit_offline_callbacks
        +allow_switch_to_offline_mode(bool)
        +add_enter_offline_callback(callable)
        +add_exit_offline_callback(callable)
        +_opmode_to_off()
        +_opmode_to_aut()
        +_opmode_to_op()
    }

    class ServiceSourceMode {
        +set_src_channel(bool)
        +set_src_int_aut(bool)
        +set_src_int_op(bool)
        +_src_to_off()
        +_src_to_int()
        +_src_to_ext()
    }

    class ServiceOperatorInteraction {
        +set_interact_answer_id(value)
    }

    class OSLevelBaseFunction {
        +set_os_level(value)
    }

    class Procedure {
        +str name
        +int procedure_id
        +bool is_self_completing
        +ProcedureHealthView procedure_health_view
        +dict procedure_parameters
        +dict process_value_ins
        +dict process_value_outs
        +dict report_values
        +add_procedure_parameter(ParameterElement)
        +add_process_value_in(InputElement)
        +add_process_value_out(IndicatorElement)
        +add_report_value(IndicatorElement)
        +apply_procedure_parameters()
    }

    class ProcedureHealthView {
    }

    class DataAssembly {
        +str tag_name
        +str tag_description
        +str mtpObjectType
        +dict data_items
    }

    class DataItem {
        +str name
        +W3cXmlDataTypes w3c_xml_data_type
        +MTPAccess access
        +any init_value
        +any value
        +OPCUACommunicationObject comm_obj
        +callable sub_cb
        +get_value()
        +set_value(any)
        +attach_communication_object(OPCUACommunicationObject)
    }

    class OPCUACommunicationObject {
        +opcua_node_obj
        +node_id
        +write_value_callback(value)
    }

    class ParameterElement
    class InputElement
    class IndicatorElement
    class ActiveElement
    class ServiceElement

    class AnaView
    class BinView
    class DIntView
    class StringView
    class AnaServParam
    class BinServParam
    class DIntServParam
    class StringServParam
    class AnaProcessValueIn
    class BinProcessValueIn
    class DIntProcessValueIn
    class StringProcessValueIn
    class AnaDrv
    class BinDrv
    class AnaVlv
    class BinVlv
    class PIDCtrl
    class MonAnaDrv
    class MonBinDrv
    class MonAnaVlv
    class MonBinVlv

    PEA "1" *-- "1" PeaOPCUAServer
    PEA "1" *-- "0..*" Service
    PEA "1" *-- "0..*" Alarm
    Alarm "0..*" --> "1" DataAssembly : references
    Alarm "0..*" --> "1" DataItem : trigger
    PeaOPCUAServer "1" *-- "1" SubscriptionList
    PeaOPCUAServer --> Marshalling : uses
    Marshalling "1" *-- "1" SubscriptionList
    Service "1" *-- "1" ThreadControl
    Service "1" *-- "1" StateMachine
    Service "1" *-- "1" ServiceControl
    Service "1" *-- "0..*" ParameterElement
    Service "1" *-- "0..*" DataAssembly
    StateMachine "1" *-- "1" CommandEnControl
    ServiceControl "1" *-- "1..*" Procedure
    ServiceControl "1" *-- "1" ServiceOperationMode
    ServiceControl "1" *-- "1" ServiceSourceMode
    ServiceControl "1" *-- "1" ServiceOperatorInteraction
    ServiceControl "1" *-- "1" OSLevelBaseFunction
    Procedure "1" *-- "1" ProcedureHealthView
    Procedure "1" *-- "0..*" ParameterElement
    Procedure "1" *-- "0..*" InputElement
    Procedure "1" *-- "0..*" IndicatorElement

    DataAssembly "1" *-- "0..*" DataItem
    DataItem "1" *-- "1" OPCUACommunicationObject
    DataAssembly <|-- ParameterElement
    DataAssembly <|-- InputElement
    DataAssembly <|-- IndicatorElement
    DataAssembly <|-- ActiveElement
    DataAssembly <|-- ServiceElement
    ServiceElement <|-- ServiceControl
    ServiceElement <|-- ProcedureHealthView

    IndicatorElement <|-- AnaView
    IndicatorElement <|-- BinView
    IndicatorElement <|-- DIntView
    IndicatorElement <|-- StringView

    ParameterElement <|-- AnaServParam
    ParameterElement <|-- BinServParam
    ParameterElement <|-- DIntServParam
    ParameterElement <|-- StringServParam

    InputElement <|-- AnaProcessValueIn
    InputElement <|-- BinProcessValueIn
    InputElement <|-- DIntProcessValueIn
    InputElement <|-- StringProcessValueIn

    ActiveElement <|-- AnaDrv
    ActiveElement <|-- BinDrv
    ActiveElement <|-- AnaVlv
    ActiveElement <|-- BinVlv
    ActiveElement <|-- PIDCtrl
    AnaDrv <|-- MonAnaDrv
    BinDrv <|-- MonBinDrv
    AnaVlv <|-- MonAnaVlv
    BinVlv <|-- MonBinVlv