Architecture¶
The MTP is built on two major conceptual pillars
-
The DataAssembly, the base class for a standardized Data Interfaces between the POL and the PEA.
-
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) . 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) 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 .
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 , that indicates the validity of the write attempt. If this
flag is true, we finally set the private value store
_value .
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
. This object informs the communication layer, currently an OPC-UA-Server,
about the change
, which the POL
will see the next time it polls for a change event
.
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:
DataItem.write_value(value)-- the PEA-side half. It storesvaluelocally (the reset, usually0) and pushes it to the communication nodes, but does not invokeext_wrt_cb. It also tells each node it is the PEA that wrote, viawrite_value_callback(value, suppress_echo=True).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-entersext_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 henceDataItem.set_value) is routed throughDataItemCollection.run_on_loop. This makes even a stray, direct scalar write from a foreign thread (an application flipping a setpoint, a mode flag, orPROCEDURE_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 (anX()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 mutateact_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_taskwrappers 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
*_ACTflags 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, aStateMachine, and aThreadControl. - 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, andStringView), 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