Skip to content

Basic Datatypes

Stephen Chryn edited this page Aug 3, 2026 · 1 revision

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

Shared API

Every basic datatype exposes the following methods via actions.<datatype>.

get_val(name, key?)

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"

set_val(name, value, key?)

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_val will trigger any on_change listeners registered for that tag, exactly as if a PLC client had written it.

on_change(name, callback?, *, key?, defer?)

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.

Type-Specific Notes

BOOL

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")   # → True

Integer Types

All 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)

Float Types

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

String Types

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 SSTRING for short status strings and labels. Use STRING when interoperating with PLC programs that explicitly expect a CIP STRING type.

Extending the Basic Template

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 stored

The type_validator is called by set_val before every write, so validation is enforced both at registration time and at runtime.

Clone this wiki locally