Repository navigation
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.
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 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 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.
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 1000actions.counter.increment("O_Updates")
actions.counter.is_done("O_Updates") # → True when count reaches limitA 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)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)A general-purpose I/O composite type for hardware pin interaction.
When the built-in types are not sufficient, you can define your own composite datatype. A custom datatype consists of four parts:
-
A class decorated with
@actions.datatypethat implements the runtime helper methods. -
A
type_validatorstatic method that validates and normalises the default value at registration time. -
An expander function decorated with
@tag_registry.expander("TYPENAME")that decomposes the type into primitive subtags. - 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.
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.mfgcontainerYou can also pass an explicit name:
@actions.datatype("container")
class MFGContainer:
...
# Accessible as: actions.containerA @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 vThe 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.
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).
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")