Skip to content

Datatypes

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

Datatypes

Every tag in the emulator has an associated datatype that determines how it is stored, transmitted, and validated. Datatypes are registered on the global actions object and are accessible via actions.<datatype> for both tag definition and runtime interaction.

Built-in Datatypes

Scalar Types

Scalar tags hold a single value. Pass the default value directly to the type constructor.

Type Accessor Size Range Example
BOOL actions.bool 1-bit True / False actions.type.BOOL(False)
SINT actions.sint 8-bit signed −128 … 127 actions.type.SINT(0)
USINT actions.usint 8-bit unsigned 0 … 255 actions.type.USINT(0)
INT actions.int 16-bit signed −32,768 … 32,767 actions.type.INT(0)
UINT actions.uint 16-bit unsigned 0 … 65,535 actions.type.UINT(0)
DINT actions.dint 32-bit signed −2³¹ … 2³¹−1 actions.type.DINT(0)
UDINT actions.udint 32-bit unsigned 0 … 2³²−1 actions.type.UDINT(0)
LINT actions.lint 64-bit signed −2⁶³ … 2⁶³−1 actions.type.LINT(0)
ULINT actions.ulint 64-bit unsigned 0 … 2⁶⁴−1 actions.type.ULINT(0)
REAL actions.real 32-bit float IEEE 754 single actions.type.REAL(0.0)
LREAL actions.lreal 64-bit float IEEE 754 double actions.type.LREAL(0.0)
SSTRING actions.sstring Short string ≤ 82 chars actions.type.SSTRING("")
STRING actions.string CIP string ≤ 82 chars actions.type.STRING("HELLO")

Array Types

Array tags hold an ordered list of values of a single type. Pass a list or tuple as the default.

Type Accessor Element type Example
BOOLARRAY actions.boolarray BOOL actions.type.BOOLARRAY([True, False, True, False])
SINTARRAY actions.sintarray SINT actions.type.SINTARRAY([-1, 2, -3, 4])
USINTARRAY actions.usintarray USINT actions.type.USINTARRAY([1, 2, 3, 4])
INTARRAY actions.intarray INT actions.type.INTARRAY([-5, 6, -7, 8])
UINTARRAY actions.uintarray UINT actions.type.UINTARRAY([5, 6, 7, 8])
DINTARRAY actions.dintarray DINT actions.type.DINTARRAY([-9, 10, -11, 12])
UDINTARRAY actions.udintarray UDINT actions.type.UDINTARRAY([9, 10, 11, 12])
LINTARRAY actions.lintarray LINT actions.type.LINTARRAY([-9, 10, -11, 12])
ULINTARRAY actions.ulintarray ULINT actions.type.ULINTARRAY([9, 10, 11, 12])
REALARRAY actions.realarray REAL actions.type.REALARRAY([1.2, 3.4, 5.6, 7.8])
LREALARRAY actions.lrealarray LREAL actions.type.LREALARRAY([1.2, 3.4, 5.6, 7.8])
SSTRINGARRAY actions.sstringarray SSTRING actions.type.SSTRINGARRAY(["A", "B", "C"])

Array elements are individually addressable via key=slice(n, n+1) in on_change handlers. See the Actions guide for details.

Composite Types

Composite types expand into multiple primitive subtags at build time. They are registered with a tag_registry.expander and are accessed using the parent tag's name as a prefix.

COUNTER

A counter with a configurable limit. Expands into subtags for the current count and the limit.

("O_Updates", actions.type.COUNTER(1000))  # counts up to 1000
actions.counter.increment("O_Updates")
actions.counter.is_done("O_Updates")   # → True when count reaches limit

TIMER

A countdown timer. Pass the duration in milliseconds.

("O_Timer", actions.type.TIMER(5000))  # 5-second timer
@actions.timer.is_done("O_Timer")
def handle_timer_done(attr, key, value) -> None:
    actions.bool.set_val("O_Timer.EN", False)

INCREMENT

A background auto-incrementing tag. Does not need to be declared in tag_specs; instead, it is started as a worker from the actions file.

actions.increment.start(tag_name="O_INCR", start=0, wrap=actions.int.MAX)

GPIO

A general-purpose I/O composite type for hardware pin interaction.

Custom Datatypes

When the built-in types are not sufficient, you can define your own composite datatype. A custom datatype consists of four parts:

  1. A class decorated with @actions.datatype that implements the runtime helper methods.
  2. A type_validator static method that validates and normalises the default value at registration time.
  3. An expander function decorated with @tag_registry.expander("TYPENAME") that decomposes the type into primitive subtags.
  4. An on_change_hook method that runs similarly to the on_change action. This method runs automatically from the datatype to allow the datatype itself to be responsible for some automated actions rather than an end user specifing them outside of the class. This is great for Timers that rely on a RES tags.

Registering a Custom Datatype

Use the @actions.datatype decorator on your class. The class name (lowercased) becomes the accessor key on actions:

@actions.datatype
class MFGContainer:
    ...
# Accessible as: actions.mfgcontainer

You can also pass an explicit name:

@actions.datatype("container")
class MFGContainer:
    ...
# Accessible as: actions.container

type_validator

A @staticmethod named type_validator is called at tag registration time to validate the default value passed to actions.type.MFGCONTAINER(...). Raise a TypeError or ValueError for invalid input.

@staticmethod
def type_validator(v: Any) -> Tuple[str, str, str]:
    if (
        not isinstance(v, tuple)
        or len(v) != 3
        or not all(isinstance(x, str) for x in v)
    ):
        raise TypeError(
            f"MFGCONTAINER default must be a (sn, lot, container) tuple, got {v!r}"
        )
    return v

@tag_registry.expander

The expander tells the registry how to decompose a tag of your custom type into primitive subtags. It receives the parent tag name and its default value, and must return a list of 2-tuples in the same format as a @tag_registry.register function.

@staticmethod
@tag_registry.expander("MFGCONTAINER")
def _(name: str, preset: Tuple[str, str, str]):
    sn, lot, container = preset
    return [
        (f"{name}.SN",  actions.type.UINT(int(sn))),
        (f"{name}.SSN", actions.type.STRING(sn)),
        (f"{name}.LN",  actions.type.STRING(lot)),
        (f"{name}.CN",  actions.type.STRING(container)),
        (f"{name}.FLT", actions.type.BOOL(False)),
    ]

A tag registered as ("O_Container", actions.type.MFGCONTAINER(("0", "", ""))) will be transparently expanded into five primitive tags: O_Container.SN, O_Container.SSN, O_Container.LN, O_Container.CN, and O_Container.FLT.

on_set_hook

If your datatype needs to react to writes on any of its subtags, define an on_set_hook method. It is called by AttributeDevice.on_set whenever a tag whose origin type matches your datatype is written.

def on_set_hook(self, tag_name: str, attr: Any, key: slice, value: List[Any]) -> None:
    if tag_name.endswith(".SN"):
        name_prefix = tag_name[: -len(".SN")]
        self._set_ssn(name_prefix, f"{value[0]:04}")

This is the appropriate place for cross-subtag synchronisation logic (e.g. keeping a numeric field and a string representation in sync).

Runtime Helper Methods

The class body is where you define the public API for interacting with your datatype at runtime. These methods are called from action handlers as actions.mfgcontainer.<method>(...).

def set_container(self, name_prefix: str, sn: int, ln: str, cn: str, flt: bool = False):
    self.set_sn(name_prefix, sn)
    self.set_ln(name_prefix, ln)
    self.set_cn(name_prefix, cn)
    self.set_flt(name_prefix, flt)

def get_container(self, name_prefix: str) -> Tuple[int, str, str, str, bool]:
    return (
        self.get_sn(name_prefix),
        self.get_ssn(name_prefix),
        self.get_ln(name_prefix),
        self.get_cn(name_prefix),
        self.get_flt(name_prefix),
    )

def is_empty(self, name_prefix: str) -> bool:
    return (
        self.get_sn(name_prefix) == 0
        and self.get_ln(name_prefix) == ""
        and self.get_cn(name_prefix) == ""
    )

Each method delegates to the primitive datatype helpers on actions, using the dot-separated subtag naming convention:

def get_sn(self, name_prefix: str) -> int:
    return actions.uint.get_val(f"{name_prefix}.SN")

def set_sn(self, name_prefix: str, value: int):
    return actions.uint.set_val(f"{name_prefix}.SN", value)

Used in tag registration:

@tag_registry.register
def _containerTags():
    return [
        ("O_Container", actions.type.MFGCONTAINER(("0", "", ""))),
    ]

Used in action handlers:

actions.mfgcontainer.set_container("O_Container", sn=42, ln="LOT001", cn="BIN-A")
sn, ssn, ln, cn, flt = actions.mfgcontainer.get_container("O_Container")

if actions.mfgcontainer.is_empty("O_Container"):
    print("Container slot is free")

Clone this wiki locally