Repository navigation
Basic Datatypes
All scalar (non-array) datatypes except COUNTER, TIMER, INCREMENT, and GPIO inherit from a shared Basic template. This gives them a consistent, predictable API regardless of the underlying PLC type.
The built-in types which inherit the Basic Datatype are:
BOOL · SINT · USINT · INT · UINT · DINT · UDINT · LINT · ULINT · REAL · LREAL · SSTRING · STRING
Every basic datatype exposes the following methods via actions.<datatype>.
Reads the current value of a tag. Returns the scalar value directly (not wrapped in a list) when the slice resolves to a single element.
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
— | The tag name to read. |
key |
slice |
slice(0, 1) |
Index range to read. Rarely needed for scalar tags. |
Returns None if the tag does not exist.
actions.bool.get_val("O_BOOL") # → False
actions.uint.get_val("O_UINT") # → 42
actions.string.get_val("O_TEXT") # → "HELLO"Writes a new value to a tag. The value is validated via the datatype's type_validator before being written.
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
— | The tag name to write. |
value |
Any |
— | The new value. Can be a scalar or a single-element list. |
key |
slice |
slice(0, 1) |
Index range to write. Rarely needed for scalar tags. |
actions.bool.set_val("O_BOOL", True)
actions.uint.set_val("O_UINT", 99)
actions.string.set_val("O_TEXT", "DONE")Note: Writing a value via
set_valwill trigger anyon_changelisteners registered for that tag, exactly as if a PLC client had written it.
Registers a listener that fires when the tag is written. This is a thin wrapper around actions.on_change and accepts the same arguments.
# As a decorator
@actions.bool.on_change("O_BOOL")
def handle_bool(attr, key, value) -> None:
print(f"O_BOOL changed to {value[0]}")
# Passing a callback directly
actions.bool.on_change("O_BOOL", handle_bool)See Working With Actions for full documentation on key and defer.
Stores a single boolean value. Accepts True/False or integer 1/0 as input.
("O_Active", actions.type.BOOL(False))
actions.bool.set_val("O_Active", True)
val = actions.bool.get_val("O_Active") # → TrueAll integer types share the same API. Choose the appropriate type based on the signedness and bit-width required by your PLC program.
| Type | Accessor | Signed | Bits |
|---|---|---|---|
SINT |
actions.sint |
✓ | 8 |
USINT |
actions.usint |
✗ | 8 |
INT |
actions.int |
✓ | 16 |
UINT |
actions.uint |
✗ | 16 |
DINT |
actions.dint |
✓ | 32 |
UDINT |
actions.udint |
✗ | 32 |
LINT |
actions.lint |
✓ | 64 |
ULINT |
actions.ulint |
✗ | 64 |
("O_Count", actions.type.UINT(0))
("O_Offset", actions.type.DINT(-500))
actions.uint.set_val("O_Count", 12)
actions.dint.set_val("O_Offset", -100)Some integer accessors expose a MAX constant for use with actions.increment.start:
actions.increment.start(tag_name="O_INCR", start=0, wrap=actions.int.MAX)| Type | Accessor | Precision |
|---|---|---|
REAL |
actions.real |
32-bit IEEE 754 single |
LREAL |
actions.lreal |
64-bit IEEE 754 double |
("O_Temperature", actions.type.REAL(0.0))
("O_Pressure", actions.type.LREAL(0.0))
actions.real.set_val("O_Temperature", 23.5)
val = actions.real.get_val("O_Temperature") # → 23.5| Type | Accessor | Max length | Notes |
|---|---|---|---|
SSTRING |
actions.sstring |
82 chars | Short CIP string; lower overhead |
STRING |
actions.string |
82 chars | Standard CIP string with length prefix |
("I_LotNumber", actions.type.STRING(""))
("O_Status", actions.type.SSTRING("IDLE"))
actions.string.set_val("I_LotNumber", "LOT-2026-001")
val = actions.string.get_val("I_LotNumber") # → "LOT-2026-001"Use
SSTRINGfor short status strings and labels. UseSTRINGwhen interoperating with PLC programs that explicitly expect a CIPSTRINGtype.
If you are creating a custom scalar datatype, inherit from Basic to get get_val, set_val, and on_change for free. Override only what you need:
from ethernetip_emulator.server.datatypes.templates import Basic
from ethernetip_emulator.server.device import actions
@actions.datatype("percent")
class Percent(Basic):
@staticmethod
def type_validator(v):
if not (0.0 <= float(v) <= 100.0):
raise ValueError(f"Percent value must be 0–100, got {v!r}")
return float(v)
def set_clamped(self, name: str, value: float):
"""Write a value, clamping it to the valid 0–100 range."""
self.set_val(name, max(0.0, min(100.0, float(value))))# tag definition
("O_FillLevel", actions.type.PERCENT(0.0))
# usage
actions.percent.set_val("O_FillLevel", 75.0)
actions.percent.set_clamped("O_FillLevel", 150.0) # value not storedThe type_validator is called by set_val before every write, so validation is enforced both at registration time and at runtime.