2 — Timers, and what makes things happen¶
Run PRJEDU_101 first. Its <thr> and two <var> timers are the whole of
this guide in practice.
Nothing happens by itself¶
In AyMON there are exactly two ways for something to happen:
- A timer fires. A clock wakes up and writes an attribute.
- Something writes an attribute, and the attribute's
*_fncchain runs.
That is the entire event model. A protocol poll is a timer writing evt; an
alarm is a chain running on a value write; a script is a name in one of those
chains. There is no main loop you can put code in.
The three moving parts¶
<thr id="TH_MAIN" thr_per="100" thr_wrk="1"> the CLOCK and the WORKERS
│
├── timer list one per thread, sorted by what is due next
│
└── work queue one-shot and delayed work: script calls, queued writes
Three things follow from that shape:
- The thread cycle is the floor. A timer fires only when its thread wakes
up;
thr_peris that cycle, in milliseconds. A 50 ms poll on a 100 ms thread runs at 100 ms and no log line tells you. - Timers are not threads. A thousand elements on one
<thr>are a thousand entries in one list, served by one dispatching thread andthr_wrkworkers. Slow work does not block the clock, but it does occupy the workers. - There is one scheduler per thread, so "what runs next" always has a single answer.
The thread element¶
<thr id="TH_MAIN" xdb_fnc="[THR.cre]" sta_fnc="[THR.sta]"
sta="0x0303" sta_typ="0x142"
thr_per="100" thr_wrk="1"/>
| Field | Unit | Default | Meaning |
|---|---|---|---|
thr_per |
ms | 100 | How often the thread wakes to serve its timers (10 … 10000) |
thr_wrk |
count | 0 | Worker threads that run the callbacks |
xdb_fnc |
— | — | Must be [THR.cre], or no thread object is created |
sta_fnc |
— | — | [THR.sta], or writes to sta do nothing |
sta |
— | — | 0x0303 running, 0x0000 off |
thr_wrk="0" is a real mode, not an oversight. With no workers the
dispatching thread runs the callbacks itself: simpler, lower latency, and one
slow callback delays every other timer on that thread. Anything that talks to
a device, a database or runs a script of consequence wants at least one worker.
PRJEDU_101 uses 0; PRJEDU_sqlite uses 1, because it writes to a
database.
Which thread does an element belong to?¶
| You write | The timer lives on |
|---|---|
evt_thr="TH_X" |
thread TH_X |
nothing, but the element is (or is under) <dev pro_con="CN_A"> |
connection CN_A's own thread |
nothing, anywhere else — a <var>, a <list> … |
the document's own thread (engine 4.3.4.495 and later; older engines never ran such a timer, silently) |
evt_thr="/" |
the document's own thread |
evt_thr="TH_TYPO" (no such thread) |
never runs — one ERROR line naming the element and the thread at every start (4.3.4.495+) |
The examples here all name their thread with evt_thr, which works on every
engine; on 4.3.4.495 and later you may leave it out for a document-level
timer. The document's thread is the root's own: its thr_per and thr_wrk
apply.
A connection is a thread — <con> accepts thr_per and thr_wrk just
like <thr>, and poll timers live on it by default. That is why a slow device
on a busy connection is best moved to its own <con>, not just its own
evt_thr: the thing being shared is the conversation, not the clock.
Only
dev,bi,io,var,rec,listanditemelements are scanned for timers. Puttingevt_cfgon anything else is silently dead — no timer, no error.
A timer, in full¶
<var id="T_READ" xdb_fnc="[XDB.evt]" sta="0x0303" sta_typ="0x142" sta_fnc="[EVT.sta]"
evt="" evt_typ="0x1201" evt_thr="TH_MAIN"
evt_cfg="0x0042" evt_per="1" evt_dly="0" evt_syn="0"
evt_fnc="[main.read]"
cnt="0" cnt_typ="0x5104"/>
| Field | Meaning |
|---|---|
xdb_fnc="[XDB.evt]" |
Creates the timer object. Without it the element is inert |
evt / evt_typ |
The attribute the timer writes, and its type |
evt_cfg |
Mode and flags. 0x0042 is the one you want: periodic, queued. Add 0x4000 (0x4042) to fire once at start — see below |
evt_per |
Period, in seconds, decimal allowed. 0.05 is 50 ms, 86400 is a day |
evt_dly |
Phase offset added to each tick |
evt_syn |
0 = free-running. >0 = anchor to a wall-clock boundary of that size |
evt_fnc |
What to run. The same chain syntax as val_fnc |
sta_fnc="[EVT.sta]" |
Lets sta start and stop the timer |
All four time fields are seconds. Queue delays, elsewhere, are milliseconds — the two surfaces differ and always have.
The first firing waits a whole period — unless you ask¶
By default a periodic timer fires for the first time at
start + evt_per + evt_dly: evt_per="10" evt_dly="6" first runs 16
seconds in. That is the right default for polls — a device is not asked
before its connection has had time to come up — but a report or a sampler
usually wants to run at once.
evt_cfg="0x4042" (the 0x4000 bit) moves the first firing to
start + evt_dly, then every evt_per as usual. "Start" is when the
engine starts the timer and when its sta switches it back on; writing
evt_per on a running timer is not a start. Needs engine 4.3.4.441 or later —
an older engine ignores the bit and waits the period.
evt_syn is an anchor, not a second period¶
evt_syn="0" means "start counting when the thread starts, then every
evt_per". That is what you want almost always. evt_syn="86400" with
evt_per="86400" means "midnight, then daily" — use it when the phase
matters, not the interval.
Each firing is a pulse¶
evt goes to 1, which runs evt_fnc once, and returns to 0 silently —
no second event. So evt_per="1" is one firing and one evt = 1 per second.
Between pulses a local reader sees 0. A remote reader (a web front end, a
replicated mirror) is sent the 1 and never the 0, so on the wire treat
evt as "a pulse happened", not as a level.
Two rules that decide what you see under load¶
- Missed ticks are skipped, never replayed. A timer configured faster than its thread can serve simply runs at the thread's rate. There is never a catch-up burst.
- Lateness is free; earliness would double-fire. The engine rounds its sleep up and re-checks. If you measure your period and get it plus a millisecond or two, that is the design.
The chain: what *_fnc actually is¶
val_fnc="[VAL.cnv,ATR.rou,ATR.lmt,ALM.onlvl]"
evt_fnc="[main.read]"
sta_fnc="[EVT.sta,main.on_alarm]"
The names run left to right when that attribute is written. They can be engine
functions (ATR.rou, ALM.onlvl, PRO.pol) or your own script
(main.read) — mixed freely, in any order. An IF.* function that returns
"stop" cuts the rest of the chain.
A chain is declared on the attribute it reacts to. val_fnc runs when
val is written, sta_fnc when sta is written, evt_fnc when the timer
writes evt.
The one bit that decides whether val_fnc runs¶
val_typ="0x1308" not 0x0308
The 0x1000 bit in val_typ is what says "run this value's val_fnc
chain". Leave it out and the chain is declared and never runs: no scaling,
no rounding, no alarm — and no error and no log line either. The value stores
raw and nothing tells you why.
Measured, two elements side by side, same chain [ATR.rou], same
val_rou="0.1", both written with 1.23456:
val_typ=0x0308 -> 1.23456 the chain never ran
val_typ=0x1308 -> 1.2 the chain ran
It makes no difference how the value is written — through an address
(evt.SetValue("id('X')/@val", v)) or through the element object
(elm.val = v); both go through the same gate.
pro_typ works the same way — a pro_fnc="[VAL.put]" under
pro_typ="0x0204" never runs, and the point never reaches val. That is why
every <io> in PRJEDU_modbus carries pro_typ="0x1102" and not 0x0102.
But the rule is not uniform, so do not generalise it.
evt_fncandsta_fncrun whether or not theirevt_typ/sta_typcarries0x1000— measured both ways. That is whyPRJEDU_101can shipsta_typ="0x142"next tosta_fnc="[EVT.sta]"and work perfectly well.The reason is that
evtandstaare given a run mode by the engine when they do not declare one — thestapath supplies it when the event is set up, theevtpath just before a firing is executed. Declare one and yours is what governs the event; declare none and one is supplied anyway.valandproget no such help: there the bit is yours to write, and forgetting it is silent.
If a val_fnc chain is not running, check this before anything else.
Inside a callback, the attribute still holds the OLD value¶
The chain runs before the value is written to the document. So in your
function evt.obj.val is the previous value; the value being written is
evt.valn, and the one before it is evt.valp. That is what lets a function
inspect or veto a change — and it catches everyone once.
Starting and stopping¶
The element's own sta gates it. 0x0303 runs; write 0x0000 and the timer
stops firing but the element stays registered, ready to be switched back on.
Pitfalls¶
evt_peris seconds; queue delays are milliseconds.evt_synis an anchor, not a period.thr_wrk="0"serialises everything on that thread, the clock included.- A missing
0x1000inval_typ/pro_typsilently disables that chain — butevt_fncandsta_fncdo not need it. - Inside a callback the attribute holds the old value — use
evt.valn. evt_perbelow 10 ms costs what it asks for.evt_per="0.0005"will wake the thread every 500 µs and burn a core. Check the decimal point.- Not everything needs a clock. A block is given a timer because its
protocol has to ask — Modbus, SNMP. A subscribed protocol is told: there
is no timer anywhere in
PRJEDU_mqtt's monitoring document, and a<bi>there withevt_fnc="[PRO.pol]"would be refused. See05_Connections_and_Protocols.
Checklist¶
- Every
<thr>hasxdb_fnc="[THR.cre]",sta_fnc="[THR.sta]",sta="0x0303". thr_wrk≥ 1 wherever a callback talks to a device, a database or a script of any size.- Every timer element declares
evt,evt_typ,evt_cfg,sta,sta_fnc="[EVT.sta]"and anevt_fnc. - Each timer runs on the thread you meant: its
evt_thr, or — with none — its connection (a<dev pro_con>element) or the document's own thread (4.3.4.495+; on older engines name the thread). The start log has nonames no <thr> or <con>line. - Every
val/prowith a chain carries0x1000in itsval_typ/pro_typ.