3 — Scripting¶
Run PRJEDU_101 and PRJEDU_sqlite first. Between them they use most of what is here.
Where script lives¶
Scripts are elements, declared in the document's <config> section:
<config>
<script id="main" xdb_fnc="[SCR.prs]" src="%DB%/db_HIS.js" language="JScript"
sta="0x0303" sta_typ="0x142"></script>
</config>
| Field | Meaning |
|---|---|
id |
The module name. Its functions are reached as main.foo() — not the file name |
xdb_fnc="[SCR.prs]" |
Binds the loader. Without it the script is never loaded and no error is given |
src |
The file. Path macros work: %DB%, %PRJ%, %BIN%, %ROOT%. Leave it out and the element's own text is the source |
language |
JScript is the default. Python runs through the engine's Python module (AyLIB_Python, in bin\win-x64) and needs an installed 64-bit Python 3.11 — see PRJEDU_gauge (numpy and camera frames), and PRJEDU_pycall for calling it (below) |
The file must be UTF-8. A small helper does not need a file at all:
<script id="util" xdb_fnc="[SCR.prs]"><![CDATA[
function kw(v) { return Number.toFixed(v / 1000.0, 2) + " kW"; }
]]></script>
How the XML calls it¶
There is no main(). Script runs in exactly three situations:
1 — A name in a *_fnc chain.
<var id="T_READ" evt_fnc="[main.read]" .../>
<io id="LEVEL" val_fnc="[VAL.cnv,ATR.rou,main.log_it]" .../>
2 — A life-cycle hook on the document root. These are written as an expression — a call, not just a name:
<xdb xdb_init="main.init(evt);" xdb_start="main.start(evt);">
xdb_init runs while the document is being set up (this is where you write
configuration such as a connection string); xdb_start runs once everything
is up.
3 — Inside an address, which is how a web front end reaches your code:
doc('HIS/S00')/api.recent(10)
The
apimodule is a convention with teeth. A call arriving from outside the engine can reach only theapi.scope; other modules are reserved for local calls with an engineer-level role. Put the functions the outside world may call in a module namedapi.
The function you write¶
function read(evt)
{
var n = evt.obj.cnt + 1; // the element this event is on
evt.obj.cnt = n;
var v = 20.0 + 8.0 * Math.sin(0.05 * n);
evt.SetValue("id('ALARM')/@val", v);
return 0;
}
Every function called through a chain receives one argument, evt:
evt.obj |
The element this event is on |
evt.valn / evt.valp |
The value being written / the previous one |
evt.stan / evt.stap |
The same for sta |
evt.atr |
Which attribute is being written |
evt.user_role |
Who caused it (0–7 user, 8 document, 9 system) |
| Method | |
|---|---|
evt.GetValue('adr') |
Read |
evt.SetValue('adr', v) |
Write |
evt.MskValue / XorValue / AddValue |
Masked / bitwise / arithmetic update |
evt.GetElement / AddElement / DelElement |
Element operations |
evt.ForEach(adr, 'handler(evt, idx, prm)', prm) |
Loop over elements |
evt.QueValue(adr, v, pri, delay_ms, syn, thr) |
Do it later, or on another thread |
evt.Log(level, str) |
0 PANIC · 3 ERROR · 4 WARN · 6 INFO · 7 DEBUG |
Adding an element built from text takes three steps, and the first two are
easy to miss: parse it with -1 (the XDB fields on — without it id()
never finds the new element), then take it out of the little document it
was parsed into, because AddElement only takes a node that belongs to no
document (it returns 0 otherwise). PRJPER_adsb does it once per aircraft:
var x = new XML("<ac id=\"AC_4B1805\" sta=\"0x0303\" val=\"0\"/>", -1);
var ac = x['ac'];
x.removeChild(ac);
evt.AddElement("id('SKY')", ac); // 1 = added
evt.DelElement("id('AC_4B1805')"); // and gone again
An element added this way lives in this engine only: replication does not
carry it, so a mirror or a spare never sees it. When the list is part of a
replicated document, let the list make its own elements with ELM.new —
write what the new element should hold into the list's @new, and a script
of yours (new_elm_new_eval) fills it in. PRJEDU_cluster keeps its batch
list that way:
<list id="LOG" new="" new_typ="0x1401" new_fnc="[ELM.new]"
new_elm_new="item" new_elm_new_pos="0" new_elm_new_idatr="id"
new_elm_new_max="B10" new_elm_new_eval="main.fill_item(evt);"/>
new_elm_new_max="B10": ids B01 … B10, ten kept. In fill_item,
evt.obj is the new element and evt.valn what was written; return 1 to
keep it.
Addresses¶
An address names an element and an attribute:
evt.GetValue("id('LEVEL')/@val") // same document
evt.GetValue("doc('PRC/P01')/id('LEVEL')/@val") // another document
evt.GetValue("doc('PRC/P01')/count(data/var)") // a function over it
evt.GetValue("count(id('L_RECENT')/item)") // same document, counted
id('X') is indexed — it does not walk the tree, so it is cheap even in a
document with thousands of elements.
doc()comes first, the function after it.doc('X')/count(data/var)works;count(doc('X')/data/var)silently answers0. The same order applies toxml(...)andjson(...).
Seven places this is not JavaScript¶
The language is close enough to JavaScript to be readable and different enough to bite. These seven are the ones that actually cost people an afternoon.
1 — Arithmetic is typed, and the LEFT operand decides.
c / 12 // integer division if c is an integer
c / 12.0 // what you meant
20 + 0.22 // 20
Math.round(x) / 100.0 // still an integer — Math.round returned one
There is no error and no log line. Write the decimal point.
2 — x == "" does not catch a missing value. A missing attribute, a
missing JSON key, an element that is not there and an SQL NULL all arrive as
undefined. Use !x for "missing or empty", x == null for "missing".
3 — Ordering operators with undefined are not JS. undefined < 1 is
true. Do not rely on them; test for the missing value first.
4 — Hex values stay hex. Arithmetic on a hex-subtype value (such as
evt.stan) produces a hex result, and comparing it with 1 fails:
if (evt.stan == "0x0303") { ... } // correct
5 — switch, classes, async/await, template strings, destructuring,
spread and real regular expressions do not exist. var/let/const,
if/for/while/do, for (x in obj), try/catch/finally, throw,
?:, ??, ?., arrow functions and object/array literals all do. A regex
literal is accepted only for plain text and is turned into a string at compile
time; a metacharacter in one is a compile error with a line and column,
not a silent wrong answer.
6 — String indices are characters, not bytes. String.length counts
characters, String.size counts bytes. The difference only shows up outside
ASCII — which means it shows up in production and not in your test.
7 — An integer attribute can be unsigned. In val_typ the class
0x01.. is UNSIGNED and 0x02.. signed: 0x0104 is an unsigned 32-bit
integer, 0x0204 a signed one, 0x0108 / 0x0208 the 64-bit pair. Read an
unsigned value and subtract, and the result does not go negative, it wraps:
// U32 is val="3" val_typ="0x0104", I32 the same with val_typ="0x0204"
get(evt, "U32") - 5 // 4294967294
get(evt, "I32") - 5 // -2
parseInt("" + get(evt, "U32")) - 5 // -2
Keep time stamps and anything you subtract in a signed type (0x0208 for
milliseconds), or read it through parseInt. Time.GetTimeMs() itself is
signed from engine 4.3.4.466 on; before that it was unsigned and
now - later wrapped the same way — 0.0 + Time.GetTimeMs() is the old
remedy, and it still works.
What is built in¶
| Scope | |
|---|---|
Math |
abs floor ceil round trunc sign min max pow sqrt log exp sin cos tan atan2 random |
String |
search, slice, case, trim, split, replace, pad, fromCharCode |
Array Number Object JSON Buffer |
the usual, with the differences above |
Time |
now GetTimeStr FmtTimeStr GetTimeMs ParseTimeMs AddDurationMs SleepMs |
File |
open close read gets write puts remove rename |
CODEC |
B64_enc/dec Z64_enc/dec HEX_enc/dec JWT_sign/verify/dec |
SQL |
see below |
FILE |
list exists mkdir rmdir move zip — directories and ZIP archives, from the module AyLIB_net; Engineer and above (PRJEDU_net) |
IMG CAM |
image functions on blobs and the camera list, from the module AyLIB_vision (PRJEDU_gauge, PRJEDU_vision) |
FILE, IMG and CAM live in engine modules beside the engine in bin; the
first call attaches the module, no xdb_lib needed. FILE is refused to a
script started from a page that did not log in: it runs with the page's role,
and the engine's own timers run as the engine (PRJEDU_net makes its report
on a timer for that reason).
There is no Date. Time.now() (engine 4.3.4.530 and later) is the instant
as a time value — it prints as 2026-10-01T12:00:00.123+03:00, and
t2 - t1 is milliseconds. The text stamp most examples log is
Time.GetTimeStr(0) → 2026-09-20 22:31:04.
Every scope documents itself, and the key is always lower-case help:
evt.Log(4, Math.help);
evt.Log(4, SQL.help);
evt.Log(4, evt.help);
evt.Log(4, Object.keys(Math));
That is the live list. If anything here disagrees with it, believe the engine.
The engine functions you put in chains¶
These are normally written in XML, not called from script. The names you will meet in the examples:
| Scope | What it does | Seen in |
|---|---|---|
ATR.rou ATR.lmt |
round to val_rou, clamp to val_min/val_max |
101, modbus |
VAL.put VAL.cnv |
pro → val, and scale it |
modbus |
ALM.onlvl |
compare with the four limits, set the alarm bits in sta |
101, modbus, rep |
PRO.pol PRO.sta PRO.inp |
poll, state, distribute a block | modbus |
EVT.sta THR.sta |
let sta start and stop a timer or thread |
all |
XDB.evt THR.cre SCR.prs |
create the timer / thread / script object | all |
SQL.qry |
run the SQL in @qry and turn the rows into elements |
sqlite |
IF.* |
cut the rest of the chain on a condition | — |
REP.cfg |
work out this node's end of a replication link | rep, cluster |
CLT.cfg CLT.cmd |
the same for an election link; a hand-over written to pro_clt_cmd |
cluster |
ELM.new |
make an element from what was written to @new |
cluster |
PRO.out |
send what was written to pro to the device; the value is not stored and the chain ends there — the device's answer comes back into the same pro and runs the functions after PRO.out (4.3.4.559 and later) |
snmp, mqtt, net |
PRO.outc |
the same, but the chain goes on with the value written — for a point nothing ever reads back | — |
Script can also speak a protocol¶
A connection whose protocol is PRO.raw has none of its own: the engine moves
the bytes and pro_onparse hands them to a function of yours, which decides
where one frame ends and may answer on the same socket.
function onparse(exc)
{
var n = exc.buf_recv.len - exc.buf_recv.pos;
var b = exc.buf_recv.copy(exc.buf_recv.pos, n); // bytes; b[0] is a number
exc.buf_recv.pos = exc.buf_recv.len; // consumed
exc.buf_send.remove();
exc.buf_send.append(CODEC.HEX_dec("300b0201 00")); // text -> bytes
exc.send();
return RES.OKY; // frame done
}
CODEC.HEX_dec and HEX_enc are how a script builds and reads binary: an
array of numbers is not a byte source, and a byte in a copied buffer
cannot be written through its index. Returning RES.INF_SUB_CONTINUE after
setting exc.buf_recv.nxt asks for more bytes instead.
PRJEDU_snmp does this for real: its device end is an SNMP v2c agent in
script, because PRO.snmp is a manager and does not answer. Reach for it when
the protocol is not one the engine knows — and expect to keep the parsing
small; a format that turns out to matter belongs in a plug-in later.
Script can also write a web page¶
A file under an HTTP connection's pro_http_root whose name ends in .ayp
is a server page: the engine runs it on every request and answers with
what it writes. It is a script with HTML around it, in the syntax ASP, JSP
and EJS use:
<% var t = main.tank(evt, "T1"); -%>
<p><%= t.name %>: <%= t.level %> %</p> <!-- <%= %> escapes, <%- %> does not -->
<% if (t.alarm > 0) { %><p class="alarm"><%= t.text %></p><% } %>
A page sees evt, req (req.getForm("sp") reads a form field), res
(res.status, res.type, res.header, res.redirect), ses and your
modules by name — main.tank() above is an ordinary function in the
document's script. Everything in this guide holds inside a page: typed
arithmetic, evt.GetValue addresses, return to stop early. Statements need
their ; (there are no automatic semicolons). A page that fails answers
500 and logs why, with the template's line number; its source is never
sent.
PRJEDU_pages is built this way, and PRJEDU_oee adds a printable shift
report (www\report.ayp) to an existing project. A page runs with the
visitor's rights — see 06_Access_and_Safety.md.
Calling Python¶
A Python script (language="Python") runs on the engine's one Python thread.
A function in it marked @xdb_fnc becomes callable from the XML and from
JScript, under the script's id:
@xdb_fnc
def analyse(evt): # one parameter named evt: an EVENT call
blk = json.loads(evt.valn)
xdb.SetValue("id('RMS')/@val", rms)
@xdb_fnc
def forecast(limit, stamp): # anything else: an ARGUMENT call
xdb.SetValue("id('FORECAST')/@val", json.dumps(out))
<var id="WAVE" val_typ="0x1401" val_fnc="[vib.analyse]"/>
var q = vib.forecast(4.5, Time.GetTimeMs()); // q = 1: queued
Both are asynchronous. The call goes into the Python thread's queue and
the caller goes on; JScript gets 1 ("queued"), never the function's result.
The result comes back the way every value does: Python writes it into a point,
and that point runs its own val_fnc — rounding, alarms — like any
measurement. In an event call the engine stores the attribute itself after the
function returns. PRJEDU_pycall measures both calls on every call, and shows
what happens when Python is slower than the data.
Pictures and other large values: blobs¶
A camera frame does not belong in a string. An attribute declared as a blob
(val_typ="0x050E", 0x150E with its val_fnc) holds large binary data by
reference: writing it, reading it, handing it to a val_fnc or to Python
never copies the bytes. MQTT and JSON show only a descriptor, and a saved
XML leaves the attribute empty (a blob is not persistent):
{"blob":1,"len":13890,"fmt":"jpeg","seq":24,"tim":0}
var b = Blob.from(buf, { fmt: "jpeg", seq: n }); // bytes in: one copy, numbered
b.len b.fmt b.seq b.w b.h b.dtype // read-only; no b[i]
var s = IMG.stats(frame, { x: 60, y: 60, w: 120, h: 120 }); // AyLIB_vision, in C
var j = IMG.encode(frame, "jpeg", 80);
From Python a numpy array becomes a blob with one copy, and a blob comes back
as a read-only numpy view with none. A PRO.http connection with
pro_blob="/blob" serves the bytes from memory, once
(GET /blob/DOC/NODE/ID/val) or as an MJPEG stream (?stream=1). The
IMG.* functions come from the engine module AyLIB_vision, named on the
root document (xdb_lib="AyLIB_vision"; up to 4.3.4.562 the plug-in
AyPRO_img). PRJEDU_gauge uses all of it, and keeps the old way
(a base64 PNG in a string on MQTT) one button away, to compare.
SQL from script¶
The shape never changes:
var con = SQL.attach('CN_SQL'); // borrow from the <con>'s pool
var rs = SQL.exec_direct(con, sql); // or prepare / parameter / execute
while (SQL.fetch(rs)) { ... SQL.getdata(rs, 1) ... }
SQL.close(rs); // always
You never disconnect con — it belongs to the connection's pool and goes back
by itself. Closing the result set is what you owe.
- Use
prepare/parameter/executethe moment a value goes into the statement. A bound value is data and never becomes SQL.PRJEDU_sqliteproves it by storing a tag calledx'; DROP TABLE reading; --every tenth reading; the table is still there. SQL.getdata(rs, n)counts from 1, not from 0.- An SQL NULL comes back as
undefined— see difference 2 above. SQL.qryis the other way in: write SQL into a<list>element's@qryand every row becomes an<item>whose attributes are named after the columns. After that the rows are addressable:id('L_RECENT')/item[1]/@val.
Debugging¶
There are no breakpoints and no stepping, on purpose: a script runs on a connection, timer or worker thread which may be holding a session lock, and stopping it would not debug the system, it would freeze it. Instead:
evt.Log(4, ...) |
The first tool. The examples use nothing else |
SCR.trace |
Statement trace |
SCR.prof |
Function timings |
SCR.dump |
Dump a scope |
A syntax error is reported with line, column and the source line. A runtime
error is reported with a call stack — unless it is inside a try/catch,
which swallows it. If you expect an error in the log and there is none, look
for the catch.
Checklist¶
- Does the
<script>element havexdb_fnc="[SCR.prs]"? Without it nothing loads and nothing is logged. - Are the functions the outside world may call in a module called
api? - Are your divisions written with a decimal point where you meant a decimal?
- Are you testing for a missing value with
x == null, notx == ""? - Are hex values compared with a hex string?
- Every
SQL.exec_direct/preparematched by aSQL.close? - Is the
val_typof anything with aval_fnccarrying0x1000? (guide 2) - Is everything you subtract signed - a time stamp in a
0x0208attribute, an unsigned (0x01..) value read throughparseInt? (item 7) - Does a function that runs because a page wrote into ANOTHER document (the
page's MQTT connection belongs to one document, the element to another)
write a local attribute (
val_typ0x4...)? Local attributes refuse writes that come from outside their document - silently. (PRJEDU_robotarm,PSTOP_MS)