Skip to content

Working with Actions

Stevie Chryn edited this page Jul 29, 2026 · 1 revision

Working with Actions

The actions object is the central interface for reading, writing, and reacting to tag state at runtime. It is an instance of AttributeActions and is imported from the device module:

from ethernetip_emulator.server.device import actions

Generic Actions

These low-level methods are available directly on actions and operate on any tag attribute object, regardless of its datatype.

actions._read_attr(attr, key)

Reads a slice of values from a tag attribute.

Parameter Type Default Description
attr tag attribute — The attribute object to read from.
key slice slice(0, 1) The index range to read.

Returns a list of values, or None if the read fails.

attr = actions._lookup("O_Step")
value = actions._read_attr(attr, slice(0, 1))  # [False]

actions._write_attr(attr, key, value)

Writes a list of values to a slice of a tag attribute.

Parameter Type Description
attr tag attribute The attribute object to write to.
key slice The index range to write.
value list The values to write.
attr = actions._lookup("O_Step")
actions._write_attr(attr, slice(0, 1), [True])

actions._lookup(tag_name)

Retrieves a tag attribute object from the device registry by name. Returns None if the tag does not exist or the device is not yet bound.

attr = actions._lookup("I_LotNumber")

Note: In practice you will rarely need _read_attr, _write_attr, or _lookup directly. Datatype-specific helpers (e.g. actions.bool.set_val(...)) wrap these calls for you. The best use-case for these methods are when creating your own custom datatype.

Reacting to Tag Changes: on_change

on_change registers a callback that fires whenever a specific tag is written. It can be used as a decorator or called directly.

Signature

actions.<datatype>.on_change(
    tag_name: str,
    callback=None,   # omit to use as a decorator
    *,
    key: slice = slice(0, 1),
    defer: bool = False,
)

Callback signature

Every on_change callback receives three arguments:

Argument Description
attr The raw tag attribute object.
key The slice that was written.
value A list of the new value(s) at that slice.
@actions.bool.on_change("O_BOOL")
def handle_bool(attr, key, value) -> None:
    if value[0]:
        print(f"O_BOOL was set to True")

key — Subscribing to a specific array index

For array tags, key lets you subscribe to a single element or sub-range rather than the whole array. The callback only fires when the written slice matches the key you specified.

# Fires only when index 0 of the array is written
@actions.usintarray.on_change("O_UsintArray", key=slice(0, 1))
def node_1_changed(attr, key, value):
    print(f"Node 1 changed: {value[0]}")

# Fires only when index 1 is written
@actions.usintarray.on_change("O_UsintArray", key=slice(1, 2))
def node_2_changed(attr, key, value):
    print(f"Node 2 changed: {value[0]}")

# Fires when the whole slice(0, 4) range is written
@actions.usintarray.on_change("O_UsintArray", key=slice(0, 4))
def nodes_changed(attr, key, value):
    print(f"All nodes changed: {value}")

A listener registered with key=slice(0, 4) will not fire when only slice(0, 1) is written, and vice versa. The keys must match exactly.

defer — Running the callback after the tag is set

By default, on_change callbacks run before the new value is formally committed to the tag. Setting defer=True launches the callback in a background thread, which means it runs after the tag write has completed.

Without defer (default)

The callback executes inline, before the tag update is logged:

@actions.bool.on_change("O_BOOL")
def handle_bool(attr, key, value) -> None:
    if value[0]:
        print("On Change Run")

Output:

On Change Run
              O_BOOL[    0-0    ] <= [True]

With defer=True

The callback is dispatched to a daemon thread and runs after the tag write is complete:

@actions.bool.on_change("O_BOOL", defer=True)
def handle_bool(attr, key, value) -> None:
    if value[0]:
        print("On Change Run")

Output:

              O_BOOL[    0-0    ] <= [True]
On Change Run

When to use defer=True

Use defer=True when your callback itself writes back to the same tag or a dependent tag. Because the tag is not yet set when a non-deferred callback runs, writing to it inside a non-deferred handler can cause the write to be overwritten by the original value milliseconds later. A common pattern is toggling a latch:

@actions.bool.on_change("O_BOOL", defer=True)
def handle_bool(attr, key, value) -> None:
    if value[0]:
        # Safe — the original True write has already been committed
        actions.bool.set_val("O_BOOL", False)

Without defer=True, the set_val("O_BOOL", False) call would race with the original write and the tag could end up stuck at True.

Interacting with Datatypes

Both built-in and custom datatypes are accessed through actions.<datatype>. Each datatype exposes its own set of helper methods that internally use _lookup, _read_attr, and _write_attr.

Common method patterns

Most datatypes follow a consistent interface:

# Read the current value
actions.bool.get_val("O_BOOL")                      # → [False]

# Write a new value
actions.bool.set_val("O_BOOL", True)

# Array-specific helpers
actions.boolarray.size("O_BoolArray")               # → 4
actions.boolarray.toggle_bit("O_BoolArray", 0)      # flip index 0
actions.boolarray.append("O_BoolArray", 1)          # add element to end
actions.boolarray.prepend("O_BoolArray", 1)         # add element to front
actions.boolarray.clear("O_BoolArray")              # reset all elements

# Counter helpers
actions.counter.increment("O_Updates")
actions.counter.is_done("O_Updates")                # → bool

# Timer helpers
actions.timer.is_done("O_Timer")                    # usable as a decorator too

# Increment worker (starts a background auto-increment loop)
actions.increment.start(tag_name="O_INCR", start=0, wrap=actions.int.MAX)

Clone this wiki locally