Skip to content

Timer Datatype

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

Timer Datatype

TIMER is a composite datatype that models a PLC-style on-delay timer. It measures elapsed time against a preset value (in milliseconds) and exposes enable, timing, and done bits. The background worker is started automatically when the tag is registered, no manual start call is required in most cases.

from ethernetip_emulator.server.device import actions

# Tag definition — pass the preset in milliseconds as the default
("O_Timer", actions.type.TIMER(5000))   # 5-second timer

Expanded Subtags

Each TIMER tag expands into five primitive subtags:

Subtag Type Description
<name>.PRE DINT Preset — the timer duration in milliseconds.
<name>.ACC DINT Accumulator — elapsed time in milliseconds. Updated every worker tick.
<name>.EN BOOL Enable bit. The timer runs while this is True and resets when it goes False.
<name>.TT BOOL Timing bit. True while the timer is running and has not yet reached PRE.
<name>.DN BOOL Done bit. True when ACC >= PRE.

Auto-start: The expander calls actions.timer.start(name, enable=f"{name}.EN") automatically at tag registration time, gated on <name>.EN. To run the timer, simply set <name>.EN to True.

Controlling the Timer

The timer is controlled entirely through the EN subtag:

# Start the timer
actions.bool.set_val("O_Timer.EN", True)

# Stop and reset the timer
actions.bool.set_val("O_Timer.EN", False)

When EN goes from True to False, the worker immediately resets ACC, TT, and DN to their initial state.

Manual reset

actions.timer.reset("O_Timer")

reset sets ACC to 0, EN to False, TT to False, and DN to False.

Reading Subtag Values

Method Returns Description
get_preset(name) int Current value of PRE (milliseconds).
set_preset(name, value) — Write a new preset value. Takes effect on the next enable.
get_accumulator(name) int Current value of ACC (elapsed milliseconds).
set_accumulator(name, value) — Directly write ACC (rarely needed).
elapsed = actions.timer.get_accumulator("O_Timer")   # → 2340
preset  = actions.timer.get_preset("O_Timer")        # → 5000

You can change the preset at runtime before or during a timing cycle:

actions.timer.set_preset("O_Timer", 10000)   # change to 10 seconds

Reacting to Timer State

is_enabled, is_timing, and is_done each return a TimerSignal object that can be used as a boolean check or as a decorator for registering listeners.

As a boolean

if actions.timer.is_done("O_Timer"):
    print("Timer has elapsed")

if actions.timer.is_timing("O_Timer"):
    print("Timer is currently running")

As a decorator

By default, the callback fires only on the rising edge (when the bit goes to True). Pass rising_edge=False to fire on every write.

is_done

Fires when DN becomes True (i.e. ACC >= PRE):

@actions.timer.is_done("O_Timer")
def handle_timer_done(attr, key, value) -> None:
    actions.bool.set_val("O_Timer.EN", False)
    actions.string.set_val("O_Status", "Cycle Complete")

is_timing

Fires when TT changes — useful for detecting the start or end of a timing cycle:

@actions.timer.is_timing("O_Timer")
def handle_timing_started(attr, key, value) -> None:
    print("Timer started")

@actions.timer.is_timing("O_Timer", rising_edge=False)
def handle_timing_any(attr, key, value) -> None:
    print(f"TT is now {value[0]}")

is_enabled

Fires when EN changes:

@actions.timer.is_enabled("O_Timer")
def handle_enabled(attr, key, value) -> None:
    print("Timer enabled")

All three signals accept a defer keyword argument, which has the same meaning as in on_change:

@actions.timer.is_done("O_Timer", defer=True)
def handle_done_deferred(attr, key, value) -> None:
    ...

Advanced: Manual start

In most cases the timer worker is started automatically by the expander. If you need a second timer worker with a different configuration — for example, a different poll period or an external enable gate — you can call start manually.

Signature

actions.timer.start(
    tag_prefix,
    *,
    period=0.05,
    preset_ms=None,
    initial_delay=1.0,
    enable=None,
)
Parameter Type Default Description
tag_prefix str — The timer tag name.
period float 0.05 Worker tick interval in seconds. Lower values give finer ACC resolution.
preset_ms int | None None Override PRE when the worker starts.
initial_delay float 1.0 Seconds to wait before the worker begins.
enable str | Callable | None None Gate the timer. Pass a BOOL tag name or a zero-argument callable.

Worker behaviour each tick

  1. Evaluate the enable gate. If it just transitioned from open to closed, reset ACC, TT, and DN immediately.
  2. If the gate is closed, sleep and continue.
  3. On first open tick, record t_start = time.monotonic().
  4. Compute elapsed_ms = (now - t_start) * 1000. Cap ACC at PRE if PRE > 0.
  5. If elapsed_ms >= PRE: set TT = False, DN = True.
  6. Otherwise: set TT = True, DN = False.

Full Example

# tags.py
@tag_registry.register
def _():
    return [
        ("O_CycleTimer", actions.type.TIMER(3000)),   # 3-second timer
        ("O_Running",    actions.type.BOOL(False)),
    ]


# actions.py
@actions.bool.on_change("O_Running")
def handle_running(attr, key, value) -> None:
    if value[0]:
        actions.bool.set_val("O_CycleTimer.EN", True)
    else:
        actions.timer.reset("O_CycleTimer")


@actions.timer.is_done("O_CycleTimer")
def handle_cycle_done(attr, key, value) -> None:
    actions.bool.set_val("O_CycleTimer.EN", False)
    actions.bool.set_val("O_Running", False)
    actions.string.set_val("O_Status", "Cycle Complete")

Method Reference

Method Description
reset(name) Set ACC, EN, TT, DN all to 0/False
start(name, *, period, preset_ms, initial_delay, enable) Manually start a timer worker
is_done(name, *, defer, rising_edge) Boolean check or decorator for DN changes
is_timing(name, *, defer, rising_edge) Boolean check or decorator for TT changes
is_enabled(name, *, defer, rising_edge) Boolean check or decorator for EN changes
get_preset(name) Read PRE
set_preset(name, value) Write PRE
get_accumulator(name) Read ACC
set_accumulator(name, value) Write ACC

Clone this wiki locally