Repository navigation
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 timerEach 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>.ENtoTrue.
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.
actions.timer.reset("O_Timer")reset sets ACC to 0, EN to False, TT to False, and DN to False.
| 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") # → 5000You can change the preset at runtime before or during a timing cycle:
actions.timer.set_preset("O_Timer", 10000) # change to 10 secondsis_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.
if actions.timer.is_done("O_Timer"):
print("Timer has elapsed")
if actions.timer.is_timing("O_Timer"):
print("Timer is currently running")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.
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")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]}")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:
...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.
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. |
- Evaluate the enable gate. If it just transitioned from open to closed, reset
ACC,TT, andDNimmediately. - If the gate is closed, sleep and continue.
- On first open tick, record
t_start = time.monotonic(). - Compute
elapsed_ms = (now - t_start) * 1000. CapACCatPREifPRE > 0. - If
elapsed_ms >= PRE: setTT = False,DN = True. - Otherwise: set
TT = True,DN = False.
# 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 | 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
|