Timers & Counters
For an introduction to the DSL vocabulary, see Core Concepts.
Timer and Counter types
Timer and Counter are built-in structured types. Each has a .Done bit (Bool) and an .Acc accumulator (Int for timers, Dint for counters).
from pyrung import Timer, Counter
# Named instances — the 95% case for real programs
OvenTimer = Timer.clone("OvenTimer")
CycleTimer = Timer.clone("CycleTimer")
PartCounter = Counter.clone("PartCounter")
# Anonymous instances — fine for throwaway simulation tests
t = Timer[1]
c = Counter[1]
Use Timer.clone("Name") for production code — OvenTimer_Done in a fault log tells you everything; Timer1_Done tells you nothing.
Use nicknames= when the two field tags must keep an existing naming scheme:
HeatTimer = Timer.clone(
"HeatTimer",
nicknames={"Done": "HeatingComplete", "Acc": "HeatingElapsedMs"},
)
Unspecified fields retain the normal {name}_Done or {name}_Acc name. Nickname overrides are supported on single-instance Timer and Counter clones.
Status bits
Timers and counters also have status bits that reflect instruction state each scan. These are available in the runner API (.when(), .monitor(), .value) but cannot be used in with rung() — CLICK PLCs manage these bits internally.
| Type | Bit | Meaning |
|---|---|---|
| Timer | .EN |
Rung is energizing the timer |
| Timer | .TT |
Timer is actively accumulating (TON: enabled and not done; TOF: disabled and done) |
| Counter | .CU |
Count-up condition is active |
| Counter | .CD |
Count-down condition is active |
# Observation — not ladder logic
runner.when(OvenTimer.TT).snapshot("timing")
runner.monitor(OvenTimer.EN, lambda c, p: print(f"EN: {p} → {c}"))
runner.run_until(OvenTimer.TT == False) # wait for stall or done
Custom types
Timer and counter instructions use a structural contract: any @udt() with a Done: Bool field and an Acc: Int or Acc: Dint field works with on_delay, off_delay, count_up, and count_down.
from pyrung import udt, Bool, Dint
@udt()
class MyCounter:
Done: Bool
Acc: Dint
Faults: Dint # extra fields are fine
Timers
On-delay timer (TON / RTON)
# TON: auto-reset when rung goes False
on_delay(OvenTimer, preset=100)
# RTON: hold accumulator when rung goes False (manual reset required)
on_delay(OvenTimer, preset=100) \
.reset(ResetButton)
TON behavior: - rung True → accumulator counts up; done = True when acc ≥ preset - rung False → immediately resets acc and done
RTON behavior:
- Same as TON while rung is True
- rung False → holds acc and done (does not reset)
- .reset(tag) → resets acc and done regardless of rung state
on_delay(...).reset(...) (RTON) is terminal — no later instruction or branch can follow in the same flow.
Off-delay timer (TOF)
TOF behavior: - rung True → done = True, acc = 0 - rung False → accumulator counts up; done = False when acc ≥ preset
TOF is non-terminal — instructions can follow it in the same rung.
Time units
| Argument | Aliases | Unit |
|---|---|---|
"ms" |
"milliseconds", "msec" |
Milliseconds (default) |
"sec" |
"s", "seconds" |
Seconds |
"min" |
"m", "minutes" |
Minutes |
"hour" |
"h", "hr", "hours" |
Hours |
"day" |
"d", "days" |
Days |
Also accepted: Tms/Ts/Tm/Th/Td — great for tag names (FillTimeTm stays short, and Tm sidesteps the minute-vs-minimum ambiguity of Min).
The accumulator stores integer ticks in the selected unit. The time unit controls how dt is converted to accumulator ticks.
Example: traffic light
GreenTimer = Timer.clone("GreenTimer")
YellowTimer = Timer.clone("YellowTimer")
RedTimer = Timer.clone("RedTimer")
with rung(State == "g"):
on_delay(GreenTimer, preset=3000)
with rung(State == "y"):
on_delay(YellowTimer, preset=1000)
See Structured Tags for the full UDT pattern.
Counters
Counters use a .Done bit (Bool) and a .Acc accumulator (Dint).
Counters count every scan while the condition is True — they are not edge-triggered. Use rise() on the rung condition if you want one increment per leading edge.
Count up (CTU)
- rung True → accumulator increments each scan; done = True when acc ≥ preset
.reset(tag)→ resets acc and done when that tag is True
count_up(...).reset(...) is terminal.
Count down (CTD)
- Accumulator starts at 0 and goes negative each scan
- done = True when acc ≤ −preset
count_down(...).reset(...) is terminal.
Bidirectional counter
Both up and down conditions are evaluated every scan; the net delta is applied once.
Edge-triggered counting
To count edges instead of scans, wrap the condition with rise():
For chained builders (counters, shift registers, drums), complete the full chain (.down(...), .clock(...), .reset(...)) before any later DSL statement.