Skip to content

PRJEDU_rep — two engines, one plant

Every file of this example — the XML, the scripts, the page — is in PRJEDU_rep on GitHub. Run it with run.bat (Windows) or ./run.sh (Linux) from that folder.

Open two command windows in this directory:

run_plant.bat
run_centre.bat

On Linux: ./run_plant.sh and ./run_centre.sh, in two terminals.

Either order. The plant produces the data; the centre mirrors it. Kill either one and start it again — they find each other and the mirror catches up by itself.

Then open http://127.0.0.1:8108/ — the centre's page. It listens to both engines and puts the source next to the mirror: six values each, the level of both on one trend, what only the first sync carried, and a switch for the replication link.

What you are looking at

The plant window:

PLANT:  [source] level 19.1 %, temp 22 C, pump off, starts 0, level sta WARNING

The centre window, at the same second:

CENTRE: [mirror, 5 elements] level 19.1 %, temp 22 C, pump off, starts 0, draining, level sta WARNING

Putting those two lines side by side is the example. Everything below is about why it takes so little configuration to get them.

The page

The centre serves it (CN_WEB on 8108, and CN_LIVE — MQTT inside a WebSocket, /mqtt, on the same port). It also opens a second connection, to the plant's own /mqtt, which the plant mounts on the listener it already has (CN_EXT, 9180) the way CN_PLANT mounts /rep.

The page cannot subscribe to PRC/P01 itself: an MQTT connection publishes the document it belongs to, and both CN_LIVEs belong to SYS/S00. So each node's SYS/S00 copies PRC/P01 into one JSON value, VIEW, twice a second (main.view) — the source on the plant, the mirror on the centre.

Switch the link off and the plant goes on while the mirror keeps the last values it had; the two columns and the two lines part. Switch it on and the bulk sync brings the mirror up to date. Measured, three times on Windows: after a gap of 5.6–16.8 % in the level, the mirror equalled the source again 0.9–1.6 s after the switch (both VIEWs are refreshed every 0.5 s, so that is the resolution).

The switch is LINK on the centre — the only thing the page may write (its own xdb_acl; the document's xdb_acl="0xFFFFFFF1" gives a page that did not log in read and nothing else). Its val_fnc, main.link, writes CN_PLANT's sta: 0x0000 off, 0x0303 on.

The centre has no file for the data

That is the part worth stopping on. In db\ there is one document of process data, db_proc.xml, and it belongs to the plant. The centre's root file declares the same document with no src:

<doc id="PRC/P01" sta="0x8300" desc="HUB - the mirror of pumping station P01" />

Four attributes. No element list, no types, no limits. A HUB document declared this way opens empty and the tree arrives from the field: on connect, replication pulls the whole document once.

Add a point to db_proc.xml and it appears at the centre — you do not go and edit a second file. That is the difference between replicating a document and forwarding messages.

Two numbers decide the direction

Not the connection. The connection only carries.

sta = 0xRC00
        |'- bit 12-13  cluster state
        '-- bit 14-15  role class:  0 plain   1 EDGE (field)   2 HUB (centre)
plant root centre root
SYS/S00 (connections, scripts) 0x0300 plain 0x0300 plain
PRC/P01 (the data) 0x4300 EDGE — the source 0x8300 HUB — the mirror

Swap those two numbers and the data flows the other way. There is no direction= anywhere in this project.

One connection row, written once

<con id="CN_PLANT"> is the same text in db_sys_plant.xml and db_sys_centre.xml. Neither copy says "listen" or "dial", and neither names the node it is sitting on:

<con id="CN_PLANT" xdb_fnc="[REP.cfg,CON.ws,PRO.rep]" pro_nid="PLANT" ... />

REP.cfg runs first and compares pro_nid — the name of the node that listens — with the tid in that node's root file:

  • on the plant (tid="PLANT") they match → SERVER, mounted on the node's existing listener at path /rep
  • on the centre (tid="CENTRE") they differ → CLIENT, dials pro_rep_host:pro_rep_port

With two nodes that is a convenience. With four it is the only way to keep them consistent: one row per link, copied everywhere, and every node works out its own end.

Replication does not open a port

CN_PLANT says con_host="#CN_EXT". The # means hand me the sessions that arrive on that connection for my path. CN_EXT is an ordinary HTTP listener; a real site already has one for its web interface, its REST API and its MCP endpoint. Replication costs it one more path, not one more open port in the firewall.

The alarm is computed once

LEVEL carries four limits and val_fnc="[ATR.rou,ALM.onlvl]". That chain runs at the plant, where the value is born, and what crosses the wire is the finished value and the finished sta — an alarm in AyMON is not a separate object, it is state on the value that raised it, so it travels with the value for free.

Watch level sta go normal → WARNING → ALARM in both windows at once.

And it runs unlicensed, on one machine

Replication is not in the FREE edition. It works here anyway, because the rule is about where the connection goes, not what it costs:

an unlicensed feature is a local feature.

Cluster, replication, RPC and MCP all run between engines on one machine — loopback, and every non-IP transport — so that anybody can learn them, and are refused on a remote address without a licence. What you buy is the second machine.

Four traps

REP.cfg must come first in xdb_fnc It writes con_mod, pro_role, con_host and con_port; CON.ws has to find them already written. Put it after and the connection is never set up.
pro_rep_doc is mandatory on the receiving end An incoming topic names its document, and the name is resolved against this list. No list, nothing applied.
pro_rep_dir="both" on a field-to-centre link It is not the direction of the data (that is <doc sta>). The same row carries the data upward and the mirror's requests downward.
val_typ="0x1308", not 0x0308 The 0x1000 bit is what says "run the val_fnc chain". Without it the chain is declared and never runs: no rounding, no alarm, no error, no log line. It is specific to val_fnc — evt_fnc and sta_fnc run either way (docs/02).

What the log says about itself

PRO.rep prints a line every ten seconds:

PRO.rep: çıkış toplam=247 yayın=96 (+10/10sn) | giriş uygulanan=0 red=0 eksik=0 hata=0
PRO.rep: eleman yarat=0 sil=0 gelen-sil=0 | id'siz=1 | istek=0 xml=0 senkron=2

giriş uygulanan is what was applied from the peer, red what the direction gates refused, eksik elements that were not there yet, id'siz branches that were skipped because nothing under them has an id of its own — an element without an id is not replicated, because the wire addresses elements by id. (The 1 above is the empty <config>.)

A start in either order writes no error lines. If you see this one:

PRO.rep.in: yazılamadı: doc('PRC/P01')/id('LEVEL')/@val

the engine is older than 4.3.4.443. It is a value arriving a few hundred milliseconds before the bulk sync that creates the element; the mirror asks for it, the sync lands, and the data is right either way. Since 4.3.4.443 the source holds deltas back until that subscriber's snapshot is queued, and says so once:

PRO.rep: abone toplu senkronu bekliyor — delta atlandı (anlık görüntü taşıyacak)

The same release stopped the empty mirror from publishing its own start-up state back down to the field, which the field refused with a second error line.

Where to go next

Both engines share this one project directory and are told apart by tid, which is also what %TID% in log_lnk uses to keep their log files separate. To put the centre on another machine, change pro_rep_host (and con_acl) — and get a licence, because that connection is no longer local.

Why this runs without a licence at all is docs/04_Licensing.md. The full field guide — three nodes, automatic MASTER/SPARE election with PRO.clt, TLS on the link, and what pro_rep_atr / pro_rep_id / pro_rep_sta_msk are for — comes with the licensed product.