Functional Equipment Assembly Integration¶
Integrating a PEM Electrolyzer as a FEA¶
The previous example built a PEA around a pure software function. This one moves to the other end of the spectrum: integrating a smart physical device — a PEM electrolyzer with a Modbus RTU/TCP interface — as a Functional Equipment Assembly (FEA) that is part of a PEA. Figure 1 shows the overall integration architecture: the PEA process is configured from the rMTP, the application logic, and the register mapping; it communicates with the physical electrolyzer over Modbus on one side and exposes the standardized MTP interface to the POL over OPC UA on the other. At the heart of the abstraction are the Data Assemblies: all the application logic that exposes the FEA as MTP state conformant processing unit is expressed on these base interfaces, the details of handling the modbus registered is abstracted away.
flowchart LR
MTP["rMTP <br> (M15.mtp)"]
MTP -.-> PEAcore
Logic["application logic <br> (m15_logic.py)"]
Logic -.-> PEAcore
ModbusCfg["register mapping <br> (m15_modbus.json)"]
ModbusCfg -.-> PEAcore
subgraph PEA["PEA process <br> (m15_pea.py)"]
PEAcore["PEA RunTime"]
DA["Data Assemblies"]
SVC["Services"]
OPCS["OPCUA Server"]
MBC["Modbus Client"]
PEAcore -.-> DA
DA <--> OPCS
DA <--> MBC
SVC <--> DA
end
subgraph FEA["FEA <br> (PEM electrolyzer)"]
Dev["Modbus RTU/TCP"]
PA["Process Automation <br> Physical Process"]
end
subgraph POL["POL"]
Client["OPC UA client"]
REC["Recipe Engine <br> User Interface"]
end
MBC -- "send cmds, read registers" --> Dev
Dev -- "Modbus (TCP, port 5020)" --> MBC
OPCS -- "OPC UA (opc.tcp://localhost:4840)" --> Client
Client -- "send cmds & read values" --> OPCS
The engineering task is to wire together two descriptions into one consistent PEA:
- The rMTP specifies the interface and behavior the POL expects.
- The Modbus register table (device documentation) describes how to read and write the physical electrolyzer.
A device-specific mapping file bridges the two: it binds Modbus registers to MTP Data Assemblies, so the application logic reads and writes MTP Data Assemblies — and the MTPPy2.0 framework handles the Modbus protocol underneath.
The workflow consists of four basic steps:
- Understanding the task
- Input: Control narrative, rMTP, and the Modbus register table.
- Result: A consistent view of the POL-facing contract and the device-facing registers.
- Inspecting the rMTP
- Input: rMTP (and the control narrative).
- Result: Confirmed service, procedure, parameter, and data-assembly names.
- Mapping the device to the MTP model
- Input: Modbus register table, rMTP structure, and the service state model.
- Result: A register→Data Assembly mapping and device behavior registered to the correct state actions.
- Implementing and testing the PEA
- Input: Register mapping, application logic, and the rMTP.
- Result: A running PEA that talks to the (simulated) device and is exposed through its OPC UA endpoint.
This tutorial uses the following tools of the MTPPy2.0 package and the M15
example in src/examples/modbus/:
- The
M15Pea.pyclass assembles the PEA from the rMTP, the Modbus register mappings, and the application logic and starts the I/O loop. - A simple Modbus simulator (
m15_simulator, built onpymodbus) emulates the physical electrolyzer, so the whole flow can be exercised without hardware. mtp_inspectorconfirms the rMTP names before designing and writing the application logic.
Step 1: Understanding the Task¶
The control narrative requires the PEA to
start hydrogen production at a certain pressure level and make the most important process values accessible for recording and monitoring. The detailed Modbus register information is provided by the device documentation, whereas the services, process values, and the HMI exposed to the POL are defined in the rMTP.
As in the introductory example, the narrative is under-specified. The open questions here include:
- Which registers carry the process values the POL should see, and how are they scaled (raw 16-bit vs. engineering units)?
- What engineering units shall be used? SI, imperial, others?
- Which registers are commands (start/stop, valve open/close), and which values mean what?
- What must happen on
holding,stopping, andabortingto leave the electrolyzer in a safe state? - Which operating modes shall be implemented?
Clarifying these against the register table — and writing the answers down as the mapping and the state actions — is the core of this task, exactly as the IREB / Wiegers & MacNair clarification process suggests in the first example.
Step 2: Understanding the Requirements MTP¶
The rMTP resolves the POL-facing details: interface names, data types, service and procedure structure, and which Data Assemblies are exposed to the POL.
For this example the rMTP defines the service produce, the procedure auto,
the procedure parameter p_out_sp, and a set of AnaView based process-value
interfaces. The relevant design view is the HMI of the PEM-electrolyzer
requirements MTP as created in Semodia PEA-Designer, shown in
Figure 2.

Figure 2: HMI of the PEM-electrolyzer requirements MTP in Semodia PEA-Designer.
Inspecting the MTP¶
Before writing or changing logic, inspect the MTP to confirm the service,
procedure, parameter, and data-assembly names, using the same mtp_inspector
utility as in the first example:
The services section lists the service, its procedures, and each element they
use. The relevant part for the electrolyzer is:
{ "produce": { "Name": "produce",
"RefBaseSystemUnitPath": "MTPServiceSUCLib/Service",
"auto": {
"Name": "auto",
"RefBaseSystemUnitPath": "MTPServiceSUCLib/Service/Procedure",
"p_out_sp": { "Name": "p_out_sp",
"RefBaseSystemUnitPath": [
"MTPServiceSUCLib/ServiceParameter/ProcedureParameter",
"MTPServiceSUCLib/RequiredEquipment"]},
"PIC009": { "Name": "PIC009",
"RefBaseSystemUnitPath": "MTPServiceSUCLib/RequiredEquipment"},
"V010": { "Name": "V010",
"RefBaseSystemUnitPath": "MTPServiceSUCLib/RequiredEquipment"},
"XI001": { "Name": "XI001",
"RefBaseSystemUnitPath": "MTPServiceSUCLib/RequiredEquipment"},
"XI003": { "Name": "XI003",
"RefBaseSystemUnitPath": "MTPServiceSUCLib/RequiredEquipment"},
"XI004": { "Name": "XI004",
"RefBaseSystemUnitPath": "MTPServiceSUCLib/RequiredEquipment"},
"TI005": { "Name": "TI005",
"RefBaseSystemUnitPath": "MTPServiceSUCLib/RequiredEquipment"},
"FI002": { "Name": "FI002",
"RefBaseSystemUnitPath": "MTPServiceSUCLib/RequiredEquipment"},
"FI008": { "Name": "FI008",
"RefBaseSystemUnitPath": "MTPServiceSUCLib/RequiredEquipment"},
"P001": { "Name": "P001",
"RefBaseSystemUnitPath": "MTPServiceSUCLib/RequiredEquipment"}
}
}
Matching the narrative against the rMTP gives the POL-facing contract:
Table 1: Mapping from control-narrative statements to rMTP names and MTP element types.
| Control-narrative statement | rMTP name | MTP element (from RefBaseSystemUnitPath) |
|---|---|---|
| "start and supervise hydrogen production" | produce |
Service (MTPServiceSUCLib/Service) |
| (its automatic procedure) | auto |
Procedure (.../Service/Procedure) |
| "outlet pressure setpoint" | p_out_sp |
Procedure parameter (.../ServiceParameter/ProcedureParameter) |
| outlet pressure (process value) | PIC009 |
Required equipment (.../RequiredEquipment) |
| outlet valve | V010 |
Required equipment (.../RequiredEquipment) |
| cell voltage / current, temperature, water quality | XI003, XI004, TI005, XI001 |
Required equipment (.../RequiredEquipment) |
These exact names are what must be used in context.get_base_interface(...) calls and in
the registration names.
Step 3: Mapping the Device to the MTP Model¶
Mapping Modbus registers to Data Assemblies¶
The first concrete step is to bind the Modbus registers to the Data Assemblies
defined (or internal to) the PEA. This is done in the mapping file
src/examples/modbus/m15_modbus.json; each entry names the Data Assembly
class, the register, its access direction, and — where needed — a transform
action.
If not already answered in Step 2, the mapping calls for further clarifications, for instance: Which MTP operating commands shall be mapped? For instance, a valve
can be opened by the POL (valve OpenOp) as well as by the PEA in automatic
mode (valve OpenAut). Shall both ways be considered?
Measurement mappings (process values read from the device):
Table 2: Measurement mappings — Data Assemblies read from the device.
| Class | TagName | Alias | Register | Access | Action / Semantics |
|---|---|---|---|---|---|
AnaView |
XI003 |
V |
1110 | read | scale(0, 65535, 0.0, 100.0) — cell voltage |
AnaView |
XI004 |
V |
1111 | read | scale(0, 65535, 0.0, 100.0) — cell current |
AnaView |
TI005 |
V |
1113 | read | scale(0, 65535, 0.0, 100.0) — processing board temperature |
AnaView |
XI001 |
V |
1116 | read | scale(0, 65535, 0.0, 100.0) — water quality |
PIDCtrl |
PIC009 |
PV |
1118 | read | scale(0, 65535, 0.0, 100.0) — outlet pressure |
Control mappings (device functions written from the PEA):
Table 3: Control mappings — Data Assemblies written to the device.
| Class | TagName | Alias | Register | Access | Action / Semantics |
|---|---|---|---|---|---|
PIDCtrl |
PIC009 |
SP |
400 | write | scale(0, 15954, 0.0, 100.0) — outlet pressure setpoint |
DIntView |
start_stop |
V |
401 | readwrite | 0 = start, 1 = stop |
DIntView |
reset |
V |
403 | write | 0 = reset |
BinVlv |
V010 |
OpenOp |
534 | write | map(1, 1, 'open') — open (operated) |
BinVlv |
V010 |
OpenAut |
534 | write | map(1, 1, 'open') — open (automatic) |
BinVlv |
V010 |
CloseOp |
534 | write | map(1, 0, 'close') — close (operated) |
BinVlv |
V010 |
CloseAut |
534 | write | map(1, 0, 'close') — close (automatic) |
DIntView |
p_eng_units |
V |
1013 | readwrite | 0 = barg, 1 = psig, 2 = kPa |
DIntView |
t_eng_units |
V |
1014 | readwrite | 0 = °C, 1 = °F |
DIntView |
v_eng_units |
V |
1015 | readwrite | 0 = scm, 1 = scf |
Two structural points follow from this:
- Some Data Assemblies are public, others are internal. The ones named in
the rMTP (
PIC009,V010,XI001,XI003,XI004,TI005, …) are exposed to the POL; device-only ones (start_stop,reset,*_eng_units) are not in the rMTP. They need to be created from the mapping file and allocated to the procedure as required equipment, so that they can be retrieved throughcontext.get_base_interface(...)just as the rMTP defined ones. - One register may back several Data Assembly items (a 1:n relation). The
outlet valve
V010writes all four of its aliases to the same register534, so control issued from the POL (OpenOp/CloseOp) and control issued by the automatic procedure (OpenAut/CloseAut) address the same physical valve.
Because the mapping lives in a single configuration file, the application logic
interacts with standardized and device-specific information through the same
abstraction as in the introductory example. Instead of writing to Modbus
register 401 with a Modbus-library-specific command, the logic simply sets
start_stop.V = 0. MTPPy2.0 organizes the communication with the Modbus server
and, for interfaces defined in the rMTP, with updating the POL.
Mapping device behavior to the service state model¶
The application logic then links the relevant states and transitions of the MTP service model to device-specific actions. Figure 3 shows how a single state is specified in the Semodia PEA-Designer

Figure 3: State machine design in the Semodia PEA-Designer.
The simplified sequence of the produce.auto procedure (without stopping and
aborting) is shown in Figure 4.
sequenceDiagram
participant POL as POL <br>(OPC UA)
participant PEA as IO Service
participant SVC as Service <br>(produce.auto)
participant LOG as Application logic
participant DEV as PEM electrolyzer <br>(Modbus)
par
loop every IO cycle (background IO service)
POL->>PEA: subscribe
DEV-->>PEA: read registers → process values (PV, XI, TI, FI, PICV)
POL->>PEA: where there updates?
PEA-->>POL: actual process values
end
and
POL->>SVC : cmd START
rect Lightblue
SVC->>LOG: starting.entry
Note over LOG: reset units <br> set PIC009 to auto-int
LOG->>DEV: write regs 1012/1013/1014 = 0
SVC-->>POL : ack STARTING
SVC->>LOG: starting.do
Note over LOG: PIC009.SPMan = p_out_sp<br>start_stop.V = 0<br>V010.OpenAut = 1
LOG->>DEV: write reg 400 (SP)<br>write reg 401 (start) = 0<br>write reg 534 (V010) = 1 (open)
SVC->>LOG: starting.exit
end
rect Lightcoral
SVC->>LOG: execute.entry
SVC-->>POL : ack EXECUTE
loop every cycle while in execute
SVC->>LOG: execute.do
end
POL->>SVC : cmd COMPLETE
SVC->>LOG: execute.exit
end
rect Lightgreen
SVC->>LOG: completing.entry
SVC->>POL : ack COMPLETING
SVC->>LOG: completing.do
Note over LOG: start_stop.V = 1<br>V010.CloseAut = 1
LOG->>DEV: write reg 401 (start) = 1<br>write reg 534 (V010) = 0 (close)
end
rect Lightgray
SVC->>LOG: completed.entry
SVC-->>POL: ack COMPLETED
loop every cycle while in completed
SVC->>LOG: completed.do: (do nothing)
end
POL->>SVC : cmd RESET
SVC->>LOG: completed.exit
end
end
Figure 4: Simplified sequence of the PEM-Electrolyzer procedure produce.auto.
The guiding principle: put the device in a defined state during initialization (set engineering units, put the pressure controller in automatic mode), issue the commands that begin production on start, and make sure the completing transition returns the device to a defined safe state (production stopped, outlet valve closed).
Attention
Actually, the service's work task execute is empty — it is not even
implemented. There is no execute.do callback registered in
m15_logic.py, so nothing runs cyclically while the procedure is in the
execute state. This is deliberate: once the electrolyzer has been
started, it produces autonomously, and the PEA's role during execute is
purely supervisory — the background IO service continues to read process
values and propagate them to the POL. If additional cyclic work were
required during production (e.g. trend sampling or safety checks), it
would be registered here as produce.auto.execute.do.
Step 4: Implementing and Testing the PEA¶
Starting the electrolyzer¶
The startup sequence (derived from the manual operation handbook) is registered
to produce.auto.starting.do. First, the setpoint of the device's internal
pressure controller PIC009 is set from the procedure parameter p_out_sp
(line 8), then the start command is issued (line 9), and finally the outlet
valve V010 is opened (line 10).
@register("produce.auto.starting.do")
def start_generator(context: Service) -> bool:
start_stop = context.get_base_interface(DA_START_STOP)
valve = context.get_base_interface(DA_VALVE)
p_out_sp = context.get_base_interface("p_out_sp")
pctl: PIDCtrl = context.get_base_interface("PIC009")
start_stop.V = 0
valve.OpenAut = 1
pctl.SPMan = p_out_sp.VOut
return True
Notice what is absent: there is no Modbus call anywhere in the code. The logic writes to Data Assemblies; the device protocol is handled by the framework.
Note
The code above is deliberately simplified for clarity — no abnormal-condition detection, no error handling, no recovery.
Stopping the electrolyzer¶
Stopping is the exact mirror of starting: the start/stop command is set back to
1 (line 6) and the outlet valve V010 is closed (line 7), returning the
electrolyzer to the defined safe state already named in the guiding
principle.
@register(
"produce.auto.completing.do",
"produce.auto.stopping.do",
"produce.auto.aborting.do",
)
def stop_generator(context: Service) -> bool:
start_stop = context.get_base_interface(DA_START_STOP)
valve = context.get_base_interface(DA_VALVE)
start_stop.V = 1
valve.CloseAut = 1
return True
The notable difference is the registration: @register accepts multiple
state-action names, so the single stop_generator callback is bound to three
of them — completing.do, stopping.do, and aborting.do. All three
transitions drive the procedure out of production, and in each case the device
must be left in the same safe state, so they share one handler. This is the
service state model doing its work: the where (which transition triggered it)
is handled by the registration, while the what (put the device into a defined
safe state) is written once.
The full behavior¶
The complete module registers one callback per relevant state action
(src/examples/modbus/m15_logic.py):
Table 4: All registered state-action callbacks in m15_logic.py.
| Registration name | Action |
|---|---|
produce.auto.starting.entry |
Reset engineering units; put PIC009 in automatic mode and scale. |
produce.auto.starting.do |
Set outlet-pressure setpoint, issue start, open outlet valve. |
produce.auto.holding.do |
Close the outlet valve. |
produce.auto.unholding.do |
Open the outlet valve again. |
produce.auto.completing.do / stopping.do / aborting.do |
Stop production and close the outlet valve (safe state). |
The relevant states of produce.auto for this electrolyzer are shown in
Figure 5. The state model is hierarchical: three nested
composites — holdable, stoppable, and abortable — each adds one
command to the states it encloses. A transition drawn out of a composite
means the command is available from any of that composite's inner states,
which is how the model expresses that hold, stop, and abort do not
depend on which state the procedure is currently in.
holdis available in the holdable states —execute, and the pause and unhold transitions around it.stopis available in the stoppable states — everything holdable plusstarting,holding,held, andunholding— to bring the procedure to a controlled stop.abortis available in the abortable states — everything stoppable plusstopping/stopped. In other words, an operation can be aborted even after it has already begun to stop.
The diagram also shows the off/op service-lifecycle entry into idle and the
full pause path (pause → paused → resume), which produce.auto does not
itself register code for but which the service state machine still supports.
stateDiagram-v2
direction LR
[*] --> idle : off/op
idle --> starting : start
resetting --> idle
state "abortable states" as stop_path {
state "stoppable states" as hold_path {
starting --> execute
state "holdable states" as normal {
execute --> pausing : pause
pausing --> paused
paused --> resuming : resume
resuming --> execute
execute --> completing : complete
unholding --> execute
}
normal --> holding : hold
holding --> held
held --> unholding : unhold
}
hold_path --> stopping : stop
stopping --> stopped
}
stop_path --> aborting : abort
aborting --> aborted
completing --> completed
completed --> resetting : reset
stopped --> resetting : reset
aborted --> resetting : reset
classDef transient fill:#fff3cd,stroke:#b7791f,stroke-width:2px
classDef nontransient fill:#e6f4ea,stroke:#2f855a,stroke-width:2px
classDef nontransient_code fill:#7dc98f,stroke:#1e5a38,stroke-width:4px
classDef control fill:#e8eef7,stroke:#405a7d,stroke-width:2px
class starting,holding,unholding,completing,stopping,aborting transient
class pausing,resuming,resetting transient
class idle,held,paused,completed,stopped,aborted nontransient
class execute nontransient_code
class normal,stop_path,hold_path control
note right of stop_path
stop enters stopping,
then ends in stopped
end note
note right of aborting
abort enters aborting,
then ends in aborted
end note
Figure 5: Service state model of produce.auto (relevant states).
Yellow states are transient — the application logic runs once in the order
entry → do → exit, then the state machine advances. Green states are
non-transient — the application logic (if registered) runs cyclically on every
IO cycle while the service remains in that state. The blue composites group the
holdable, stoppable, and abortable states; a reset returns the
terminal states back to idle.
The thick-outlined execute state is the steady-state the procedure returns to
after start, resume, and unhold — the state in which its cyclic application
logic runs. (For produce.auto the registered callbacks are attached to the
transient states starting, holding, unholding, completing, stopping,
and aborting; this figure shows the service state machine, not which states a
particular PEA registers code for.)
Run the PEA against a simulator¶
Start the M15 Modbus simulator that emulates the electrolyzer in one terminal
(it serves the device registers on TCP port 5020):
Then start the PEA in a second terminal. The M15Pea loads
m15_modbus.json, exposes the mapped M15 registers as internal Data
Assemblies, and continuously synchronizes them through the Modbus IOService:
With both running, the PEA is exposed on its OPC UA endpoint
(opc.tcp://localhost:4044) and the mapped device registers can be read and
written through the MTP interface, exactly as in the introductory example.
Summary¶
You followed the four-step rMTP based workflow applied to a real device and should now be able to:
- Read two descriptions — the rMTP (POL contract) and the Modbus register table (device access) — and recognize where they must be reconciled.
- Inspect the rMTP with
mtp_inspectorand confirm the service, procedure, parameter, and data-assembly names before writing logic. - Map the device by binding Modbus registers to Data Assemblies in a configuration file, distinguishing public (rMTP exposed) from internal (device-only) Data Assemblies, and handling the 1:n relation between registers and Data Assembly items.
- Map behavior to the state model by registering device actions to
entry/do/exit, with initialization putting the device in a defined state and every terminating transition returning it to a safe state. - Run and test the PEA with a Modbus simulator, with the framework abstracting all protocol details, so the application logic only touches Data Assemblies.
The core idea: a smart FEA is integrated by mapping its registers to MTP Data Assemblies and registering its behavior to the service state model — the same two levers as a software-only PEA, now with the FEA device protocol hidden behind the abstraction.
Optimizing the Application¶
The solution does not fully use the semantic quality of the MTP approach, in
particular the start_stop base interface is a 1:1 mapping of the Modbus logic
with its obscure number magic. In the code above, start_stop.V = 0 starts the
electrolyzer and start_stop.V = 1 stops it — but why 0 means start and 1
means stop is only documented in the "Action / Semantics" column of the mapping
table. The reader of m15_logic.py has to jump to m15_modbus.json to decode
each number, so the logic reads as register manipulation rather than as intent.
Sketch: a BinView for start/stop¶
The BinView
is a better fit for a two-state command: its value V is a boolean, and it
carries human-readable labels for the two states in the data items VState0 and
VState1. Declaring start_stop as a BinView with
VState0 = "stopped" and VState1 = "running" turns the command into a named
state instead of a code.
Note
Note that V still takes a boolean, not the state label: the framework
coerces every write through the boolean type, so start_stop.V = True or
start_stop.V = False is valid, while start_stop.V = "start" is rejected.
The readable words live in VState0 / VState1, which are looked up from the
boolean — you never assign to V by name.
The mapping file still owns the translation to the register. A BinView
backed by register 401 uses map actions to write the device code that each
state means:
Table 5: Register translation for a BinView-backed start_stop.
| Data Assembly item | When V is |
Register written | Device meaning |
|---|---|---|---|
start_stop.V |
True (VState1 = "running") |
401 = 0 |
start |
start_stop.V |
False (VState0 = "stopped") |
401 = 1 |
stop |
The register-level "number magic" moves out of the logic and into the mapping,
and the state labels are exposed to the POL through OPC UA, so an HMI can show
stopped / running instead of a raw 0 / 1.
The application logic then expresses intent:
@register("produce.auto.starting.do")
def start_generator(context: Service) -> bool:
start_stop = context.get_base_interface(DA_START_STOP)
...
start_stop.V = True # running, instead of "0"
...
Listing 3: Expressing intent with a BinView — start sets V = True.
@register("produce.auto.completing.do")
def stop_generator(context: Service) -> bool:
start_stop = context.get_base_interface(DA_START_STOP)
...
start_stop.V = False # stopped, instead of "1"
...
Listing 4: Expressing intent with a BinView — stop sets V = False.
Reading the two callbacks now tells you the purpose of the action — the electrolyzer is put into the running state on start and back to stopped on completing — without decoding a number.
Sketch: a StringView for the unit selectors¶
The same idea extends, with a different element, to the engineering-unit
selectors. p_eng_units, t_eng_units, and v_eng_units are DIntViews whose
V is a code (0 = barg, 1 = psig, 2 = kPa, …). A
StringView
carries the unit as a string directly — p_eng_units.V = "kPa" — so the
starting.entry reset that currently sets p_eng_units.V = 0 reads as
p_eng_units.V = "kPa": the mapping translates the string to the register code,
and the code disappears from the logic entirely. A StringView is preferable to
a BinView here because the pressure selector has three states (barg / psig /
kPa), and a BinView only models two.
Attention
One caveat: a StringView is a free-form string, not an enum. Its V is
coerced to string only, so it accepts any value — nothing stops a
mistyped p_eng_units.V = "kpa " from being written, and the framework will not
catch it. If the unit selector should be limited to a fixed set of names, that
constraint would have to be added as a true enum element (for example a
StringView subclass that validates V against a class-level allowed set and
raises on anything else), which MTPPy2.0 does not ship today. Until then, the
choice is effectively between a readable but unenforced StringView and a
numeric, range-checkable DIntView.
The takeaway: choose the Data Assembly class that matches the number of meaningful states, and let the mapping file carry the register codes. The logic then speaks in states and units — the semantic quality the MTP approach is meant to provide — instead of in raw numbers.
Troubleshooting¶
The logic never reaches the device¶
Confirm the register mapping (m15_modbus.json) lists the register for the
Data Assembly item you are writing, and that the IOService is running. The
application logic writes Data Assemblies; only the IO service performs Modbus I/O.
A Data Assembly is not found by get_base_interface(...)¶
get_base_interface(...) raises a KeyError when no Data Assembly with the
given tag name is allocated to the current procedure.
Internal Data Assemblies must be allocated to the procedure as required
equipment (via "assign": ["auto"] in the mapping) or be defined in the rMTP.
The name must match the tag exactly, including capitalization.
Class mismatch between mapping and rMTP¶
A Data Assembly that appears in both the mapping and the rMTP must declare the
same class in both. A mismatch is logged and skipped; check the
class value in m15_modbus.json against the rMTP definition.
The simulator or PEA cannot connect¶
Start the simulator first and verify the host/port (Modbus TCP defaults to
localhost:5020). The simulator and the PEA are separate processes; starting
the PEA does not start the device.