diff --git a/README.md b/README.md index a3ee7b3..d909671 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,43 @@ -# Atom Modbus Library (`atMb`) +# atMb 0.6 — threaded communication and client statistics -CODESYS library with a native C engine for Modbus TCP client, RTU client and TCP -slave operation. Public namespace: `atMb`. No ModbusFB dependency is required. +This update moves TCP/RTU transaction progress and TCP server I/O into one shared +C worker thread. The worker also handles connection recovery between PLC scans. +The IEC task still selects/submits the next client request and exchanges slave +memory. One C worker serves the fixed pools; it is not a separate thread per device. -## Use the library +Start with [upgrade/build instructions](docs/BUILD-AND-USAGE.md), then read the +[user guide](docs/USER-GUIDE.md) and [API reference](docs/API-REFERENCE.md). -Open `library/CmpAtomModbus.library` in CODESYS, or install it using Library -Repository and reference it from your application. Keep its project title and -external C interface settings intact. The development target is Raspberry Pi -64 SL (ARM64), using CODESYS 3.5 SP22; runtime/target compatibility must be checked -when changing platforms. Read [the user guide](docs/USER-GUIDE.md) for examples -and [the API reference](docs/API-REFERENCE.md) for members and methods. +## Required upgrade -## Repository contents +Rebuild the C component as **0.4.0.0**, import `library/atMb-Threaded.export` into +a fresh CODESYS library project and embed the new `.so` for the device target. +The external function signature is unchanged, but new commands are required. +The new IEC wrapper checks capability version and rejects an older component. +This package contains updated source/export, not an IDE-compiled updated `.library` +or rebuilt ARM binaries. The previous files are retained under `backup/` only. -| Path | Purpose | -|---|---| -| `library/CmpAtomModbus.library` | Working CODESYS library file; edit IEC code here | -| `component-source/src/` | C protocol, client, serial and server implementation | -| `component-source/component/` | Active component frame, ABI interface and makefile | -| `component-source/tests/` | Host protocol and bridge tests | -| `ExtensionSDK/` | Supplied SDK required for component builds | -| `artifacts/arm64/libCmpAtomModbus.so` | Supplied ARM64 native binary | -| `docs/BUILD-AND-USAGE.md` | Native build and deployment instructions | -| `docs/USER-GUIDE.md` | Calling patterns, configuration and diagnostics | -| `docs/API-REFERENCE.md` | Detailed IEC reference based on the last working export | -| `docs/CLEANUP.md` | Removed files, preservation checks and update workflow | -| `docs/VALIDATION.md` | Validation scope | -| `SHA256SUMS.txt` | Current working-file snapshot checksums, excluding Git metadata | +## Changes -## Build on the Pi +- Worker defaults: 1 ms sleep, normal OS scheduling, 2 s per-context scan watchdog. +- Optional `atMb.Worker` FB sets CPU affinity, sleep interval and watchdog. +- Completion and read data stay latched until acknowledged; failed writes are not + replayed by the worker. IEC cyclic retries remain configurable via write holds. +- Server dirty tracking preserves remote writes until the PLC pulls them. +- Public diagnostics now use `out_errorCode` and `out_lastErrorCode`. +- `client.stats` aggregates every registered request's counters and timings. +- Both TCP and RTU clients show `OK:/Er:` like requests. +- Accurate worker transaction durations; timeout counts classify actual timeouts. -Run from the repository root: +## Files -```sh -sudo apt install build-essential m4 dos2unix python3 -make -C component-source/component SDKDIR="$(realpath ExtensionSDK)" clean -make -C component-source/component SDKDIR="$(realpath ExtensionSDK)" all -make -C component-source test -``` +- `component-source/`: protocol implementation, worker, component frame and tests. +- `ExtensionSDK/`: required SDK headers/macros/build support. +- `library/atMb-Threaded.export`: importable IEC source; no old embedded binary. +- `library/atMb-Threaded-Review.st`: readable export source. +- `docs/`: usage, ABI/worker notes, migration, validation and reference. +- `backup/`: original library/export/binaries for rollback, not the new release. -Native output: `component-source/component/out/libCmpAtomModbus.so`. -See the build guide before updating the embedded component. An ARM64 binary -cannot run on ARM32 or x86-64; build for each architecture and verify the SDK/runtime -combination. The supplied binary has not been proven byte-equivalent to this source. - -## Update and review - -Edit the IEC library in CODESYS. After significant IEC changes, export the current -objects and update the API reference against that export. Binary `.library` files -cannot be reviewed as ordinary text diffs. This snapshot intentionally contains no -old migration exports or generators that overwrite current IDE changes. - -This ZIP retains the existing Git history and remote metadata. Cleanup changes -are uncommitted and unstaged. Review locally with `git status` and `git diff`, -then stage/commit when ready. No push is performed by this package. - -Online-change qualification remains future work. Keep all native calls owned by -one IEC task. No new license is assigned to the supplied SDK/source files. +Host tests run with `make -C component-source test`. Physical PLC/RTU testing and +CODESYS IDE compilation remain required. Online-change qualification is future work; +use a full download for this structural update. Git changes are not committed/pushed. diff --git a/SHA256SUMS.txt b/SHA256SUMS.txt index c99521e..8fb4ccc 100644 --- a/SHA256SUMS.txt +++ b/SHA256SUMS.txt @@ -1,5 +1,5 @@ d1192772047955638e8bb171080ed9b37e10683de935e4e0503c93b6d26f03fc .gitattributes -02f0eff1a80c9a8f2fe037d1b5980eb391b65e4dc07dba6fe5d109b08c180c1a .gitignore +675d431f63e2c212360b692b18de50a344aed5664d17b3683f0b687b96479f24 .gitignore 0ac6e4fe82a82d1bd27ff20dec2f18820c0e0a5057cd0cb1a8020f6dc506603f ExtensionSDK/README 1cc355e875e572de3829d28bff9c100a4eecbfd741d938654be97881023c84d5 ExtensionSDK/include/CmpErrors.h 2010310fbeb8b4734e638610d2f6dc01b600fe42b8b104155806ef0784e95f2d ExtensionSDK/include/CmpItf.h @@ -10,29 +10,40 @@ a8de018bfde8d10d3f281bb45de03c4b4f3cd28466eb0fa0981086bc4033a69c ExtensionSDK/i 814d06ca83d775f3bcb93f80859602b6c1debe9511b3d42ef337948bd2e2b5d0 ExtensionSDK/makefile 4fc8013238169e924ee2eb5dd6c2b39c9d274f4c7c2219dc0b9a530687097519 ExtensionSDK/makefile_template aa7ccad11508ccbf4f9b24aa8db8f22392f9d723ae3af5cbfa67e38271cfbac3 ExtensionSDK/src/CmpFrame.c -11aee0c6ff01eaff1aac5aa46c470ea8367d5eae655068320869a1bcdce1ef53 README.md -07527262a8efcf9add5554cfcaed22cf927af63b985e431ab5341078a6864667 artifacts/arm64/libCmpAtomModbus.so -14db5bb96e65494eed63da0139b37734dfc08e312a3cb0924ad10b7d9476d442 component-source/Makefile -d91cffa9751bca79f13ae80552d654519333536c894bb13b41a1182c179c53b7 component-source/component/CmpAtomModbus.c +3a171751b48b339c731611044bdcb94a409d89568807026f6122ecba06e458fe README.md +2a58de4b824ac7c41e5a192c05f299a08f8a59c95d7c8046722498a073e2f48e artifacts/README.md +62dd7f0b5d7efdb0e8ca6a13257581c9d2231162e3e6acc311d34f12948acff7 backup/README.md +07527262a8efcf9add5554cfcaed22cf927af63b985e431ab5341078a6864667 backup/artifacts/arm64/libCmpAtomModbus.so +bb39f2b586454b47ccd64f158e0a08f6f5d3947e98e667d9ce240b2ab472f8fa backup/artifacts/armv7hf/libCmpAtomModbus.so +fb1dbd96af6bb0a39e760d0e3ef5eab5a3f5c461de66d5d96d3301adce3d821d backup/library/CmpAtomModbus.export +c475ae30bc96965bff86a956d133ebdc7d95f38f6f2326c70ea1ecfbc80d6e4b backup/library/CmpAtomModbus.library +ec596402e5fc7a0bdc9fb3994143695a7457bda1077d0b6eee92944ba5798400 component-source/Makefile +7b36f997e84cfea38246a3d197a72cd76f6c1b94902b934659e9b66a092aadb3 component-source/component/CmpAtomModbus.c 30a8d7d5dfd372d171d4acdafd0b77cedca684e0b6e661ae1ab67e2627d35708 component-source/component/CmpAtomModbusItf.m4 513586eed183bcc53cdf008c483da03feff6b5cb0f3ed3eff17de1462def12c3 component-source/component/CmpFrame.c -0972948a5cc88892421433fb01875f4f51609cda3b0df57ff58f05c61390e1c2 component-source/component/makefile -78240595fa115d3706cbc7316bdcd4e0101a3269e5faacce98826bc32143e9d9 component-source/src/atom_bridge.c -4a3eaca066d0c06ac7fa95d802ff78b270a1ccdc3fc67c6a072b7e09efba08c0 component-source/src/atom_bridge.h -4f772a0dab94053bfa2f073f6877ec63a8c6b7999d8e3415160705db441f6068 component-source/src/atom_mb.c -f8140b5076d5cfb0feda5c9b20d0bc494d7b77edf528f10d96ed071fc4dd0461 component-source/src/atom_mb.h -e5f8e76efdbc2c4875ceee375f1ea3bd93e1cb4e2075373bb8a0eb4b31ea5e4a component-source/src/atom_rtu.c +cac2eeaad2fba34a0f5dddae57da11205fe98f52d2bc126125e5efe2c085220c component-source/component/makefile +bd63f5153b80f5a6ea3000879b83e54dae430f09f1253a2fb3f828c5ccfff2b0 component-source/src/atom_bridge.c +c0542adac77f60d5f3e7c14e755f07b341ba815b9c0243af4cec09c4844e4ab4 component-source/src/atom_bridge.h +115271ae5673cd834fc196e594ff4c1ad0f2a71bb01dc36f713809adeda6efbf component-source/src/atom_mb.c +366c879743b51daed51bac909439fc91b908a39ce63ec1d5ef0a8de78cb80bb1 component-source/src/atom_mb.h +a2afdfd2f6f607f41464c3483b58d76452164019446986a9430b54cc0b9f6b5d component-source/src/atom_rtu.c 565a5122de679b2e60990b1252f356e69316b51979485c3cf0101c6e32ac5a4a component-source/src/atom_rtu.h -771f9671ea8f0644596633970dae1d8651275251d6cdb0a44373a00d064ad35c component-source/src/atom_server.c -51f3b4a5779347e58b9993b3c2d8f180a73e0a70cb2a8b81bf9350aca8b42bad component-source/src/atom_server.h +b7fb2772d7b2967014af4651d7a75570aeda69d7c06c515c3548a8a8d2d671e3 component-source/src/atom_server.c +5e9e8e1d92f702af5901751da056e756fe9f988fc618360c42d9039619df183c component-source/src/atom_server.h +853f95e1acf7dac8b75a63398ba139ae143744ba42ce211ca339c393637dafb7 component-source/src/atom_worker.c d770393632f9d3d1acddf229f562148648792398a39d90851aa529d2923f8382 component-source/tests/bridge_driver.c 50bb7854c3a966c46a583fba951c1d3a605bf9d1739ac83685322d9a7b740c4d component-source/tests/driver.c 3af1a5f71d69bae2abda3de9dba31740ad254d7157d702847fb921fc2b64df3d component-source/tests/test_bridge.c -2a4b582bed0404f865f4fa2c905dd251c3dccfc4961f20467723c7349793a7f2 component-source/tests/test_native.py 0cf8f8fefda6376cacf0c56729d24aaa76241e807b0dee0b36001b3ad3cea509 component-source/tests/test_tcp.py -ca8eca5c351bcc426676213c162606d1be9ef6a07d017dad74aa10c198e4ea91 docs/API-REFERENCE.md -43eb385327aa81deb28a55b08068a9b6e565ad290257d2cbd5819d1ab7558ae3 docs/BUILD-AND-USAGE.md -a0931348b32a336798b7d93a5956fd14bd481597626bc1d40b1597ee6ed5e6a3 docs/CLEANUP.md -ef14901695852012e92ae7dde304244e441c17f2bc17abd4fb7e7a2e30f2e9df docs/USER-GUIDE.md -b35e03e6cd3e23719794802a65663ddc22986731dd52f6713ab71b94d1798f53 docs/VALIDATION.md -be0edea894f02a6f525307da69fd52498d4ff76e4b95728608ff6db8f6877e39 library/CmpAtomModbus.library +c79f2e6d005dac7fb62669c274ff0c82b78e0ac80ef83ee19a596e699fdad7b8 component-source/tests/test_transport.py +76d04a651b2d487cffbd778ea0b0ab8a9efda4b964044615efcacd0233448da4 component-source/tests/test_worker.py +92617cdf8d973b65e662d25ad1986275f770ff01d25823e82b9624b3b9fb7fd5 docs/API-REFERENCE.md +b014c5caec618cb2863c5a8e3ee98041e0cc5d1da0b5c2933809b471c6170e74 docs/BUILD-AND-USAGE.md +27c9d27501196d9d4bbc28dd3da9f1ce3b4e16bd4fd0830d7cc1c85c681357b2 docs/CLEANUP.md +997ce9d0fe59de7a214c9692fc9341f85d83ada909bad399532697e2943f88e7 docs/IMPORT-FIX.md +d60f5881289ac66cf5f6e4a90c5c32beaded3426a385825fb3d1750ec758dee1 docs/RENAMED-MEMBERS.json +c44551b2bb75ae7e98fc33a95dad306f419e328ba0571c7277b238705356efe5 docs/USER-GUIDE.md +6bd8db7b034dfd48e52a1af5845c769ca6cbc72236522ddfdd905ae56b5b1f66 docs/VALIDATION.md +fb39a21af08a33d070d186366eb9ccbabdf645c001bd2a91b24e12400775db72 docs/WORKER-ABI.md +c5c5363714b6a8ebb3778efb69f4c4213aec7aeecf2b60a88637d1bd4d8ceeb3 library/atMb-Threaded-Review.st +24efab5b7fb402e84ad2e375c513e2ccf2ca809d9754b67374bd26c787ccd42e library/atMb-Threaded.export diff --git a/artifacts/arm64/libCmpAtomModbus.so b/artifacts/arm64/libCmpAtomModbus.so old mode 100644 new mode 100755 index cba5e2c..aa1be88 Binary files a/artifacts/arm64/libCmpAtomModbus.so and b/artifacts/arm64/libCmpAtomModbus.so differ diff --git a/artifacts/armv7hf/libCmpAtomModbus.so b/artifacts/armv7hf/libCmpAtomModbus.so index 7a1c922..d151d75 100755 Binary files a/artifacts/armv7hf/libCmpAtomModbus.so and b/artifacts/armv7hf/libCmpAtomModbus.so differ diff --git a/component-source/Makefile b/component-source/Makefile index 70d8b2c..799927e 100644 --- a/component-source/Makefile +++ b/component-source/Makefile @@ -1,15 +1,16 @@ CC ?= cc -CFLAGS = -std=c11 -Wall -Wextra -Werror -O2 +CFLAGS = -std=c11 -Wall -Wextra -Werror -O2 -pthread .PHONY: test clean test: tests/driver tests/test_bridge tests/bridge_driver ./tests/test_bridge python3 tests/test_tcp.py python3 tests/test_tcp.py bridge_driver - python3 tests/test_native.py -tests/bridge_driver: tests/bridge_driver.c src/atom_bridge.c src/atom_bridge.h src/atom_mb.c src/atom_mb.h src/atom_rtu.c src/atom_rtu.h src/atom_server.c src/atom_server.h - $(CC) $(CFLAGS) -Isrc tests/bridge_driver.c src/atom_bridge.c src/atom_mb.c src/atom_rtu.c src/atom_server.c -o $@ -tests/test_bridge: tests/test_bridge.c src/atom_bridge.c src/atom_bridge.h src/atom_mb.c src/atom_mb.h src/atom_rtu.c src/atom_rtu.h src/atom_server.c src/atom_server.h - $(CC) $(CFLAGS) -Isrc tests/test_bridge.c src/atom_bridge.c src/atom_mb.c src/atom_rtu.c src/atom_server.c -o $@ + python3 tests/test_transport.py + python3 tests/test_worker.py +tests/bridge_driver: tests/bridge_driver.c src/atom_bridge.c src/atom_bridge.h src/atom_worker.c src/atom_mb.c src/atom_mb.h src/atom_rtu.c src/atom_rtu.h src/atom_server.c src/atom_server.h + $(CC) $(CFLAGS) -Isrc tests/bridge_driver.c src/atom_bridge.c src/atom_worker.c src/atom_mb.c src/atom_rtu.c src/atom_server.c -o $@ +tests/test_bridge: tests/test_bridge.c src/atom_bridge.c src/atom_bridge.h src/atom_worker.c src/atom_mb.c src/atom_mb.h src/atom_rtu.c src/atom_rtu.h src/atom_server.c src/atom_server.h + $(CC) $(CFLAGS) -Isrc tests/test_bridge.c src/atom_bridge.c src/atom_worker.c src/atom_mb.c src/atom_rtu.c src/atom_server.c -o $@ tests/driver: tests/driver.c src/atom_mb.c src/atom_mb.h src/atom_rtu.c src/atom_rtu.h src/atom_server.c src/atom_server.h $(CC) $(CFLAGS) -Isrc tests/driver.c src/atom_mb.c src/atom_rtu.c src/atom_server.c -o $@ clean: diff --git a/component-source/component/CmpAtomModbus.c b/component-source/component/CmpAtomModbus.c index ff44353..07b806f 100644 --- a/component-source/component/CmpAtomModbus.c +++ b/component-source/component/CmpAtomModbus.c @@ -1,3 +1,4 @@ +#define _GNU_SOURCE #define _DEFAULT_SOURCE #include "../src/atom_bridge.h" #include "CmpStd.h" @@ -31,3 +32,5 @@ void CDECL CDECL_EXT atmbcall_cext(atmbcall_cext_struct *p) #include "../src/atom_rtu.c" #include "../src/atom_server.c" #include "../src/atom_bridge.c" + +#include "../src/atom_worker.c" diff --git a/component-source/component/makefile b/component-source/component/makefile index 65a1337..b1d4b7d 100644 --- a/component-source/component/makefile +++ b/component-source/component/makefile @@ -1,8 +1,8 @@ -# Generated from supplied ExtensionSDK; native build on ARM64 Pi. +# Generated from supplied ExtensionSDK; build with target Linux toolchain. SDKDIR ?= ../../ExtensionSDK MAJORVER=0 -MINORVER=3 -CMPVERSION=0x00030000 +MINORVER=7 +CMPVERSION=0x00040000 # Local development ID; check no other component uses this ID. CMPID=0x2001 DOS2UNIX=dos2unix @@ -10,4 +10,6 @@ M4=m4 CC ?= gcc # The SDK otherwise includes CmpFrame in its component identity wildcard. override CSRC := CmpAtomModbus.c +CFLAGS += -pthread +LDLIBS += -pthread include $(SDKDIR)/makefile diff --git a/component-source/src/atom_bridge.c b/component-source/src/atom_bridge.c index 2f64364..7773617 100644 --- a/component-source/src/atom_bridge.c +++ b/component-source/src/atom_bridge.c @@ -4,7 +4,7 @@ #include "atom_server.h" #include #include -typedef struct { am_client c; ar_transport rtu; int serial; uint32_t generation; int used, retired; } slot; +typedef struct { am_client c; ar_transport rtu; int serial; uint32_t generation; int used, retired, suspended; uint64_t heartbeat; } slot; static slot slots[AM_POOL_SIZE]; static uint32_t token(unsigned i) { return (slots[i].generation<<8)|(i+1u); } static slot *lookup(uint32_t h) { @@ -24,7 +24,7 @@ static void snapshot(const am_client *c,uint32_t h,uint32_t *s) { s[18]=c->function; s[19]=c->unit; s[20]=(uint32_t)c->enabled; s[21]=c->connect_timeout_ms; s[22]=c->response_timeout_ms; s[23]=c->retry_delay_ms; } -uint32_t ab_call(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port, +uint32_t ab_dispatch(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port, uint8_t unit,uint8_t f,uint16_t addr,uint16_t q,uint16_t *data,uint32_t cap, uint32_t *status,uint32_t sc,uint64_t now,uint32_t ct,uint32_t rt,uint32_t retry) { slot *s; unsigned i; uint32_t result=0; @@ -47,7 +47,7 @@ uint32_t ab_call(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port, text[q]=0; if(!ar_init(&s->c,&s->rtu,text,ip,unit,f,(uint8_t)addr,ct))return AM_ARGUMENT; } else if(!am_init(&s->c,text,port)) return AM_ARGUMENT; - s->generation++; s->used=1; h=token(i); + s->generation++; s->used=1; s->suspended=0; s->heartbeat=now; h=token(i); snapshot(&s->c,h,status); return 0; } s=lookup(h); @@ -58,9 +58,12 @@ uint32_t ab_call(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port, /* Never recycle a generation after wrap: stale handles stay invalid. */ if(s->generation==0xffffffu) s->retired=1; snapshot(&s->c,0,status); return 0; - case AB_ENABLE: am_enable(&s->c,1,now); break; - case AB_DISABLE: if(s->serial)ar_close(&s->c);else am_enable(&s->c,0,now); break; - case AB_TICK: if(s->serial)ar_tick(&s->c,&s->rtu,now);else am_tick(&s->c,now); break; + case AB_ENABLE: s->heartbeat=now; s->suspended=0; am_enable(&s->c,1,now); break; + case AB_DISABLE: s->suspended=0; if(s->serial)ar_close(&s->c);else am_enable(&s->c,0,now); break; + case AB_TICK: + s->heartbeat=now; + if(s->suspended) { s->suspended=0; am_enable(&s->c,1,now); } + break; case AB_RECONNECT: if(s->serial)ar_reconnect(&s->c,&s->rtu,now);else am_reconnect(&s->c,now); break; case AB_CONFIGURE: if(s->c.enabled) result=AM_BUSY; @@ -68,6 +71,7 @@ uint32_t ab_call(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port, else { s->c.connect_timeout_ms=ct; s->c.response_timeout_ms=rt; s->c.retry_delay_ms=retry; } break; case AB_SUBMIT: + if(s->c.done || s->c.error) { result=AM_BUSY; break; } if((f==5 || f==6 || f==15 || f==16) && (!data || capserial?ar_request(&s->c,&s->rtu,unit,f,addr,q,data,now):am_request(&s->c,unit,f,addr,q,data,now)); break; @@ -76,6 +80,7 @@ uint32_t ab_call(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port, else if(!data || capc.quantity) result=AB_BAD_CAPACITY; else memcpy(data,s->c.data,s->c.quantity*sizeof(*data)); break; + case 12: break; case AB_ACK: s->c.done=0; s->c.error=0; break; case AB_REQUEST_TIMEOUT: if(s->c.busy) result=AM_BUSY; @@ -84,9 +89,15 @@ uint32_t ab_call(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port, break; default: result=AB_BAD_COMMAND; break; } - snapshot(&s->c,h,status); return result; + snapshot(&s->c,h,status); + if(cmd==12){ + status[21]=(uint32_t)(s->c.completed_ms>=s->c.started_ms?s->c.completed_ms-s->c.started_ms:0); + uint64_t age=s->c.has_success?now-s->c.last_success_ms:0; + status[22]=age>UINT32_MAX?UINT32_MAX:(uint32_t)age; + } + return result; } -void ab_shutdown(void) { +void ab_close_all(void) { as_shutdown(); unsigned i; for(i=0;ic.enabled && now-s->heartbeat >= watchdog_ms) { + if(s->serial)ar_close(&s->c);else am_close(&s->c); + s->suspended=1; + } + if(s->serial)ar_tick(&s->c,&s->rtu,now);else am_tick(&s->c,now); + } + as_pump(now,watchdog_ms); +} diff --git a/component-source/src/atom_bridge.h b/component-source/src/atom_bridge.h index 32826cd..7e03ff0 100644 --- a/component-source/src/atom_bridge.h +++ b/component-source/src/atom_bridge.h @@ -1,14 +1,14 @@ #ifndef ATOM_BRIDGE_H #define ATOM_BRIDGE_H #include -/* Fixed pool, one IEC task owns ALL calls. Do not share across tasks. +/* Fixed pool; serialized calls and worker I/O. IEC instances still belong to one task. * Only synchronous pointer access; generation-checked handles, no heap. */ #define AM_STATUS_WORDS 24 #define AM_POOL_SIZE 16 enum { AB_OPEN=1, AB_CLOSE, AB_ENABLE, AB_DISABLE, AB_TICK, AB_SUBMIT, AB_COPY_READ, AB_RECONNECT, AB_CONFIGURE, AB_ACK, AB_REQUEST_TIMEOUT }; enum { AB_OPEN_RTU=20 }; -enum { AB_BAD_HANDLE=256, AB_POOL_FULL, AB_BAD_CAPACITY, AB_BAD_COMMAND }; +enum { AB_BAD_HANDLE=256, AB_POOL_FULL, AB_BAD_CAPACITY, AB_BAD_COMMAND, AB_WORKER_ERROR }; uint32_t ab_call(uint32_t command,uint32_t handle,uint32_t ip,uint16_t port, uint8_t unit,uint8_t function,uint16_t address,uint16_t quantity, uint16_t *data,uint32_t capacity,uint32_t *status,uint32_t status_capacity, diff --git a/component-source/src/atom_mb.c b/component-source/src/atom_mb.c index 453136c..7b174ac 100644 --- a/component-source/src/atom_mb.c +++ b/component-source/src/atom_mb.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #ifndef MSG_NOSIGNAL @@ -14,12 +15,12 @@ static uint16_t get16(const uint8_t *p) { return (uint16_t)((p[0]<<8)|p[1]); } static void put16(uint8_t *p,uint16_t v) { p[0]=(uint8_t)(v>>8); p[1]=(uint8_t)v; } static int writing(uint8_t f) { return f==5 || f==6 || f==15 || f==16; } static void drop(am_client *c) { if(c->fd>=0) { if(c->serial_port)ioctl(c->fd,TIOCNXCL);close(c->fd); } c->fd=-1; } -static void finish(am_client *c,am_error e) { - c->busy=0; c->done=e==AM_OK; c->error=e!=AM_OK; c->result=e; +static void finish(am_client *c,am_error e,uint64_t now) { + c->completed_ms=now; c->busy=0; c->done=e==AM_OK; c->error=e!=AM_OK; c->result=e; if(e==AM_OK) c->completed++; else { c->failed++; c->last_error=e; } } static void fault(am_client *c,am_error e,uint64_t now) { - if(c->busy) { c->write_uncertain=writing(c->function) && c->tx_pos>0; finish(c,e); } + if(c->busy) { c->write_uncertain=writing(c->function) && c->tx_pos>0; finish(c,e,now); } c->last_error=e; drop(c); c->state=c->enabled?AM_RETRY:AM_DISABLED; c->deadline_ms=now+c->retry_delay_ms; } @@ -63,7 +64,7 @@ am_error am_request(am_client *c,uint8_t unit,uint8_t f,uint16_t addr,uint16_t q else if(v[i]) c->tx[13+i/8]|=(uint8_t)(1u<<(i%8)); } } - c->unit=unit; c->function=f; c->address=addr; c->quantity=q; + c->started_ms=now; c->unit=unit; c->function=f; c->address=addr; c->quantity=q; c->tx_len=n; c->tx_pos=0; c->rx_len=0; c->rx_need=7; c->busy=1; c->done=0; c->error=0; c->exception=0; c->write_uncertain=0; c->result=AM_OK; c->state=AM_SENDING; c->deadline_ms=now+c->response_timeout_ms; @@ -73,7 +74,7 @@ void am_accept_response(am_client *c,uint64_t now) { unsigned i; size_t bytes; if(c->rx[7]==(uint8_t)(c->function|0x80)) { if(c->rx_need!=9 || !c->rx[8]) { fault(c,AM_PROTOCOL,now); return; } - c->exception=c->rx[8]; finish(c,AM_EXCEPTION); c->state=AM_READY; return; + c->exception=c->rx[8]; finish(c,AM_EXCEPTION,now); c->state=AM_READY; return; } if(c->rx[7]!=c->function) { fault(c,AM_PROTOCOL,now); return; } if(writing(c->function)) { @@ -84,7 +85,7 @@ void am_accept_response(am_client *c,uint64_t now) { for(i=0;iquantity;i++) c->data[i]=c->function<=2? (uint16_t)((c->rx[9+i/8]>>(i%8))&1):get16(c->rx+9+2*i); } - c->last_success_ms=now; c->has_success=1; finish(c,AM_OK); c->state=AM_READY; + c->last_success_ms=now; c->has_success=1; finish(c,AM_OK,now); c->state=AM_READY; } void am_tick(am_client *c,uint64_t now) { struct sockaddr_in a; struct pollfd p; int e,r; socklen_t len; ssize_t n; @@ -92,8 +93,10 @@ void am_tick(am_client *c,uint64_t now) { if(c->state==AM_RETRY) { if(nowdeadline_ms) return; c->connect_attempts++; if(c->connect_attempts>1) c->reconnects++; - c->socket_error=0; c->fd=socket(AF_INET,SOCK_STREAM,0); + c->socket_error=0; c->fd=socket(AF_INET,SOCK_STREAM|SOCK_CLOEXEC,0); if(c->fd<0) { c->socket_error=errno; fault(c,AM_SOCKET,now); return; } + int one=1; + setsockopt(c->fd,IPPROTO_TCP,TCP_NODELAY,&one,sizeof(one)); r=fcntl(c->fd,F_GETFL,0); if(r<0 || fcntl(c->fd,F_SETFL,r|O_NONBLOCK)<0) { c->socket_error=errno; fault(c,AM_SOCKET,now); return; } memset(&a,0,sizeof(a)); a.sin_family=AF_INET; a.sin_port=htons(c->port); diff --git a/component-source/src/atom_mb.h b/component-source/src/atom_mb.h index dab8cc2..903d53f 100644 --- a/component-source/src/atom_mb.h +++ b/component-source/src/atom_mb.h @@ -15,7 +15,7 @@ typedef struct { uint8_t exception, unit, function; uint16_t transaction, address, quantity; uint32_t connect_timeout_ms, response_timeout_ms, retry_delay_ms; - uint64_t deadline_ms, last_success_ms; + uint64_t deadline_ms, last_success_ms, started_ms, completed_ms; uint32_t connect_attempts, reconnects, completed, failed; int busy, done, error, write_uncertain, has_success; char ip[16]; uint16_t port; diff --git a/component-source/src/atom_rtu.c b/component-source/src/atom_rtu.c index 0cfbfab..94d0e5e 100644 --- a/component-source/src/atom_rtu.c +++ b/component-source/src/atom_rtu.c @@ -29,7 +29,7 @@ int ar_init(am_client *c,ar_transport *r,const char *path,uint32_t baud,uint8_t return 1; } static void ar_fault(am_client *c,ar_transport *r,am_error error,uint64_t now) { - if(c->busy) { c->write_uncertain=(c->function>=5 && r->tx_pos>0);c->busy=0;c->done=0;c->error=1;c->result=error;c->failed++; } + if(c->busy) { c->write_uncertain=(c->function>=5 && r->tx_pos>0);c->completed_ms=now;c->busy=0;c->done=0;c->error=1;c->result=error;c->failed++; } c->last_error=error;if(c->fd>=0){ioctl(c->fd,TIOCNXCL);close(c->fd);}c->fd=-1;c->state=c->enabled?AM_RETRY:AM_DISABLED; c->deadline_ms=now+c->retry_delay_ms;r->quiet_until=c->deadline_ms;r->rx_len=0; } diff --git a/component-source/src/atom_server.c b/component-source/src/atom_server.c index 34a8b71..eb3c380 100644 --- a/component-source/src/atom_server.c +++ b/component-source/src/atom_server.c @@ -14,8 +14,8 @@ #endif typedef struct { int fd;uint8_t rx[260],tx[260];size_t received,needed,sent,length;uint64_t deadline; } as_peer; typedef struct { - int used,enabled,listener,running,error,os_error;uint32_t generation,received,replied,exceptions,retry; - uint32_t ip;uint16_t port,base[4],count[4],values[4][AS_ITEMS];uint64_t retry_at; + int used,enabled,suspended,listener,running,error,os_error;uint32_t generation,received,replied,exceptions,retry; + uint32_t ip;uint16_t port,base[4],count[4],values[4][AS_ITEMS];uint64_t retry_at,heartbeat; uint8_t dirty[4][AS_ITEMS]; as_peer peers[AS_PEERS]; } as_server; static as_server as_slots[AS_POOL]; @@ -46,13 +46,13 @@ static void as_reply(as_server *s,as_peer *p){ table=f==5?0:2; if(p->needed!=12 || (f==5 && q!=0 && q!=0xff00)){exception=3;break;} if(!as_range(s,table,address,1)){exception=2;break;} - s->values[table][address-s->base[table]]=f==5?(q!=0):q;memcpy(p->tx+8,p->rx+8,4);n=12;break; + s->values[table][address-s->base[table]]=f==5?(q!=0):q;s->dirty[table][address-s->base[table]]=1;memcpy(p->tx+8,p->rx+8,4);n=12;break; case 15:case 16: table=f==15?0:2; if(!q || q>(f==15?1968:123) || p->needed<13 || p->rx[12]!=(f==15?(q+7u)/8u:2u*q) || p->needed!=(size_t)(13+p->rx[12])){exception=3;break;} if(!as_range(s,table,address,q)){exception=2;break;} index=address-s->base[table]; - for(unsigned i=0;ivalues[table][index+i]=f==15?(uint16_t)((p->rx[13+i/8]>>(i%8))&1):as_get16(p->rx+13+2*i); + for(unsigned i=0;ivalues[table][index+i]=f==15?(uint16_t)((p->rx[13+i/8]>>(i%8))&1):as_get16(p->rx+13+2*i);s->dirty[table][index+i]=1;} memcpy(p->tx+8,p->rx+8,4);n=12;break; default:exception=1;break; } @@ -103,7 +103,7 @@ uint32_t as_call(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port,uint8_t table memset(out,0,24*sizeof(*out));if(!port || !retry)return AM_ARGUMENT; for(i=0;igeneration+1;memset(s,0,sizeof(*s));s->generation=generation;s->used=1;s->listener=-1;s->ip=ip;s->port=port;s->retry=retry; + s=&as_slots[i];uint32_t generation=s->generation+1;memset(s,0,sizeof(*s));s->generation=generation;s->used=1;s->listener=-1;s->ip=ip;s->port=port;s->retry=retry;s->heartbeat=now; for(unsigned j=0;jpeers[j].fd=-1;as_peer_close(&s->peers[j]);} h=(generation<<8)|(i+65u);as_snapshot(s,h,out);return 0; } @@ -112,20 +112,32 @@ uint32_t as_call(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port,uint8_t table s=&as_slots[i-1]; switch(cmd){ case AS_CLOSE:as_stop(s);s->used=0;as_snapshot(s,0,out);return 0; - case AS_ENABLE:s->enabled=1;break; - case AS_DISABLE:s->enabled=0;as_stop(s);break; - case AS_TICK:as_tick(s,now);break; + case AS_ENABLE:s->enabled=1;s->suspended=0;s->heartbeat=now;break; + case AS_DISABLE:s->suspended=0;s->enabled=0;as_stop(s);break; + case AS_TICK:s->heartbeat=now;if(s->suspended){s->suspended=0;s->enabled=1;s->retry_at=now;}break; case AS_PUSH:case AS_PULL: if(table>3 || !q || q>AS_ITEMS || (uint32_t)addr+q>65536u)result=AM_ARGUMENT; else if(!data || cap1){result=AM_ARGUMENT;break;}} - if(!result){s->base[table]=addr;s->count[table]=q;memcpy(s->values[table],data,q*sizeof(*data));} + if(!result){ + if(s->count[table] && (s->base[table]!=addr || s->count[table]!=q)) { result=AM_ARGUMENT;break; } + s->base[table]=addr;s->count[table]=q; + for(unsigned j=0;jdirty[table][j])s->values[table][j]=data[j]; + } }else if(s->base[table]!=addr || s->count[table]!=q)result=AM_ARGUMENT; - else memcpy(data,s->values[table],q*sizeof(*data)); + else {memcpy(data,s->values[table],q*sizeof(*data));memset(s->dirty[table],0,q);} break; default:result=259;break; } as_snapshot(s,h,out);return result; } void as_shutdown(void){for(unsigned i=0;ienabled && now-s->heartbeat>=watchdog_ms){as_stop(s);s->enabled=0;s->suspended=1;} + as_tick(s,now); + } +} diff --git a/component-source/src/atom_server.h b/component-source/src/atom_server.h index c72d064..a46dc7c 100644 --- a/component-source/src/atom_server.h +++ b/component-source/src/atom_server.h @@ -5,3 +5,5 @@ enum { AS_OPEN=30,AS_CLOSE,AS_ENABLE,AS_DISABLE,AS_TICK,AS_PUSH,AS_PULL }; uint32_t as_call(uint32_t,uint32_t,uint32_t,uint16_t,uint8_t,uint16_t,uint16_t,uint16_t *,uint32_t,uint32_t *,uint64_t,uint32_t); void as_shutdown(void); #endif + +void as_pump(uint64_t now,uint32_t watchdog_ms); diff --git a/component-source/tests/test_native.py b/component-source/tests/test_native.py deleted file mode 100644 index bc388c0..0000000 --- a/component-source/tests/test_native.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python3 -"""Real PTY/socket integration tests for the native RTU and TCP server bridge.""" -import ctypes as C,os,pty,select,socket,struct,subprocess,time,tempfile -from pathlib import Path -root=Path(__file__).resolve().parents[1] -with tempfile.TemporaryDirectory() as tmp: - so=Path(tmp)/'test-native.so' - subprocess.run(['cc','-shared','-fPIC','-std=c11','-Wall','-Wextra','-Werror','-O2','-I'+str(root/'src'),*[str(root/'src'/f) for f in ['atom_mb.c','atom_rtu.c','atom_server.c','atom_bridge.c']],'-o',str(so)],check=True) - lib=C.CDLL(str(so));fn=lib.ab_call - U=C.c_uint32;W=C.c_uint16;B=C.c_uint8 - fn.argtypes=[U,U,U,W,B,B,W,W,C.POINTER(W),U,C.POINTER(U),U,C.c_uint64,U,U,U];fn.restype=U - data=(W*2000)();status=(U*24)() - def now():return int(time.monotonic()*1000) - def call(cmd,h=0,ip=0,port=0,unit=1,f=3,addr=0,q=1,ct=100,rt=100,retry=10,when=None): - return fn(cmd,h,ip,port,unit,f,addr,q,data,2000,status,24,now() if when is None else when,ct,rt,retry) - def crc(frame): - v=0xffff - for byte in frame: - v^=byte - for _ in range(8):v=(v>>1)^(0xa001 if v&1 else 0) - return v - def adu(payload):return payload+struct.pack('=7 and f>=15 else 8 - if len(received)>=need and not sent: - assert crc(received)==0;sent=True - payload=(bytes([1,f])+received[2:6]) if f>=15 else bytes([1,f,4,0,42,0,43]) - if mode=='exception':payload=bytes([1,f|0x80,2]) - if mode=='echo' and attempt==0:payload=payload[:3]+bytes([payload[3]^1])+payload[4:] - response=adu(payload) - if mode=='crc' and attempt==0:response=response[:-1]+bytes([response[-1]^1]) - if mode!='timeout' or attempt>0: - fragments=[response[:2],response[2:4],response[4:]] if mode=='fragment' else [response] - if fragments:os.write(master,fragments.pop(0)) - if status[3] or status[4]:break - time.sleep(.002) - assert sent and (status[3] or status[4]),list(status) - if mode in ['crc','echo'] and attempt==0:assert status[5]==8 - elif mode=='timeout' and attempt==0:assert status[5]==6 - elif mode=='exception':assert status[5]==9 and status[8]==2 and status[1]==3 - else: - assert status[3]==1,list(status) - if f<=4:assert call(7,h)==0 and list(data[:2])==[42,43] - assert call(10,h)==0 - if attempt==0 and mode in ['crc','timeout','echo']:ready(h) - assert call(6,h,unit=0,f=3,addr=0,q=1)==1 # Broadcast unsupported, no fake completion. - assert call(2,h)==0;assert call(5,h)==256;os.close(master) - for mode in ['normal','fragment','crc','timeout','exception']:rtu_scenario(mode);print('PASS RTU',mode) - rtu_scenario('normal',16);print('PASS RTU register write') - rtu_scenario('echo',16);print('PASS RTU invalid echo and reopen recovery') - rtu_scenario('normal',15);print('PASS RTU coil write') - # Native TCP server, four independent tables mapped at nonzero Modbus address. - probe=socket.socket();probe.bind(('127.0.0.1',0));port=probe.getsockname()[1];probe.close() - assert call(30,ip=0x7f000001,port=port)==0;h=status[0] - for table in range(4): - for i in range(16):data[i]=i%2 if table<2 else 100+i - assert call(35,h,f=table,addr=10,q=16)==0 - assert call(32,h)==0;assert call(34,h)==0 and status[1]==1 - peer=socket.create_connection(('127.0.0.1',port));peer.setblocking(False) - tid=0 - def exchange(pdu): - global tid - tid+=1;packet=struct.pack('>HHHB',tid,0,len(pdu)+1,1)+pdu - peer.send(packet[:3]);assert call(34,h)==0;peer.send(packet[3:]) - reply=b'';end=time.monotonic()+1 - while time.monotonic()=7 and len(reply)==6+struct.unpack('>H',reply[4:6])[0]:return reply[7:] - time.sleep(.001) - raise AssertionError(('server timeout',pdu,reply)) - for f in [1,2,3,4]: - rep=exchange(struct.pack('>BHH',f,10,2));assert rep==(bytes([f,1,2]) if f<=2 else bytes([f,4,0,100,0,101]));print('PASS slave read FC',f) - for f in [5,6]: - value=0xff00 if f==5 else 456;pdu=struct.pack('>BHH',f,10,value);assert exchange(pdu)==pdu - assert call(36,h,f=0 if f==5 else 2,addr=10,q=16)==0 and data[0]==(1 if f==5 else 456);print('PASS slave write FC',f) - for f in [15,16]: - payload=b'\x01' if f==15 else struct.pack('>HH',321,654) - pdu=struct.pack('>BHHB',f,10,2,len(payload))+payload - assert exchange(pdu)==struct.pack('>BHH',f,10,2) - assert call(36,h,f=0 if f==15 else 2,addr=10,q=16)==0 and list(data[:2])==([1,0] if f==15 else [321,654]);print('PASS slave write FC',f) - assert exchange(struct.pack('>BHH',3,0,1))==bytes([0x83,2]) - assert exchange(struct.pack('>BHH',16,10,0))==bytes([0x90,3]) - assert exchange(struct.pack('>BHH',99,10,1))==bytes([0xe3,1]) - print('PASS slave exceptions and nonzero table bases') - assert call(31,h)==0;assert call(34,h)==256;peer.close() - # Listener bind error recovers after the conflicting listener disappears. - blocker=socket.socket();blocker.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1);blocker.bind(('127.0.0.1',port));blocker.listen() - assert call(30,ip=0x7f000001,port=port)==0;h=status[0];call(32,h);call(34,h);assert status[2]==1 - blocker.close();call(34,h,when=now()+100);assert status[1]==1 and status[2]==0 - call(31,h);print('PASS slave listener recovery and stale handles') - lib.ab_shutdown() diff --git a/docs/API-REFERENCE.md b/docs/API-REFERENCE.md index 8599947..5a9ba46 100644 --- a/docs/API-REFERENCE.md +++ b/docs/API-REFERENCE.md @@ -1,59 +1,52 @@ -# atMb API reference +# atMb 0.4 API reference -Reference based on the last supplied working `CmpAtomModbus(1).export`, with 95 code objects. The current editable/installable library is `library/CmpAtomModbus.library`. This reference includes internal methods and property accessors. The binary library was preserved unchanged but not decoded to independently confirm its current declarations. Update this document against a fresh export when IEC code changes. +Generated from `library/atMb-Threaded.export`. Declarations below are the current source. Use USER-GUIDE.md for lifecycle, units and examples. Internal fields remain exposed where required by existing IEC inheritance; do not overwrite engine state from application code. ## atMbCall_cext -External C ABI entry point used by the wrappers. Application code should normally call the function blocks. Do not change parameter order/types or external implementation settings. Pointers are accessed synchronously by C; provide distinct valid data/status buffers and truthful capacities. Client status requires 24 UDINT elements. All calls must be owned by one IEC task. +Fixed external C entry point. Preserve parameter order/types and External implementation setting. Use FBs for application logic. ```iecst -(* Create FUNCTION atMbCall_cext returning UDINT, paste this declaration. - Properties > Build > External implementation must be selected. - Library filename and Project Information title: CmpAtomModbus. - This declaration is the SINGLE external entry point for the C engine. *) FUNCTION atMbCall_cext : UDINT VAR_INPUT + command : UDINT; + handle : UDINT; + ipAddress : DWORD; + port : UINT; + unitId : BYTE; + functionCode : BYTE; + startAddress : UINT; + quantity : UINT; + pData : POINTER TO WORD; + dataCapacity : UDINT; + pStatus : POINTER TO UDINT; + statusCapacity : UDINT; + nowMs : ULINT; + connectTimeoutMs : UDINT; + responseTimeoutMs : UDINT; + retryDelayMs : UDINT; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `command` | VAR_INPUT | `UDINT` | Native command selector. Use wrappers in application code. | -| `handle` | VAR_INPUT | `UDINT` | Generation-checked native context handle; 0 for open. | -| `ipAddress` | VAR_INPUT | `DWORD` | Packed IPv4 for TCP commands; command 20 overloads this field with RTU baud rate. | -| `port` | VAR_INPUT | `UINT` | Slave listening port, default 502, must be nonzero. | -| `unitId` | VAR_INPUT | `BYTE` | Transaction unit ID; command 20 overloads it with serial parity. | -| `functionCode` | VAR_INPUT | `BYTE` | Modbus FC; command 20 uses data bits, server push/pull use table ID. | -| `startAddress` | VAR_INPUT | `UINT` | Protocol start; command 20 uses stop bits. | -| `quantity` | VAR_INPUT | `UINT` | Item count; command 20 uses serial path byte count. | -| `pData` | VAR_INPUT | `POINTER TO WORD` | WORD buffer; client reads/writes, serial ASCII path, or server table staging. | -| `dataCapacity` | VAR_INPUT | `UDINT` | Available WORD elements, not bytes. | -| `pStatus` | VAR_INPUT | `POINTER TO UDINT` | UDINT status buffer; requires at least 24 elements. | -| `statusCapacity` | VAR_INPUT | `UDINT` | Available UDINT elements, not bytes. | -| `nowMs` | VAR_INPUT | `ULINT` | Monotonic time in milliseconds; wrappers use SysTimeGetUs / 1000. | -| `connectTimeoutMs` | VAR_INPUT | `UDINT` | TCP connection timeout; command 20 uses kernel RS485 flag. | -| `responseTimeoutMs` | VAR_INPUT | `UDINT` | Reply deadline in milliseconds. | -| `retryDelayMs` | VAR_INPUT | `UDINT` | Native recovery wait in milliseconds. | - ## ModbusMaster.ClientStatus -Aggregate request status used by the RTU monitoring property: noneOk=0, someOk=1, allOk=2. This describes request online flags, not the physical connection. The current toString getter compares onlineCnt with all registered requests and reports allOk for an empty registry. +Retained legacy enum; clients now monitor a STRING through toString. ```iecst {attribute 'qualified_only'} @@ -69,7 +62,7 @@ END_TYPE ## ModbusMaster.Error -IEC error classification. TCP-named socket/timeout members are also used for RTU failures. For the precise native reason inspect out_nativeError/out_lastNativeError; for an OS failure inspect out_socketError. +IEC result classification. TCP socket names also classify RTU OS failures. See out_errorCode for exact C result. ```iecst {attribute 'qualified_only'} @@ -83,37 +76,37 @@ END_TYPE ## ModbusMaster.ExceptionCodes -Modbus exception response values plus local TIMEOUT=-1. A valid Modbus exception proves communication, but the requested operation failed. In current statistics, non-exception errors can also be classified as TIMEOUT; use native diagnostics to distinguish their actual cause. +Standard Modbus exception values and local TIMEOUT=-1. Only true timeout results set timeoutCnt in this version. ```iecst {attribute 'qualified_only'} TYPE ExceptionCodes : ( - // Local / transport-level (non-Modbus) result - TIMEOUT := -1, // Communication timeout / no response + + TIMEOUT := -1, - // Modbus exception codes (returned in exception response PDU) - RESPONSE_SUCCESS := 16#00, // Everything OK (no exception) + + RESPONSE_SUCCESS := 16#00, - ILLEGAL_FUNCTION := 16#01, // Function code not supported by slave - ILLEGAL_DATA_ADDRESS := 16#02, // Data address not valid for slave (out of range / not mapped) - ILLEGAL_DATA_VALUE := 16#03, // Data value not valid (bad length/value/etc.) - SLAVE_DEVICE_FAILURE := 16#04, // Unrecoverable error while performing the requested action + ILLEGAL_FUNCTION := 16#01, + ILLEGAL_DATA_ADDRESS := 16#02, + ILLEGAL_DATA_VALUE := 16#03, + SLAVE_DEVICE_FAILURE := 16#04, - ACKNOWLEDGE := 16#05, // Request accepted; processing started (use to avoid master timeout) - SLAVE_DEVICE_BUSY := 16#06, // Slave busy (e.g. long operation in progress); retry later + ACKNOWLEDGE := 16#05, + SLAVE_DEVICE_BUSY := 16#06, - MEMORY_PARITY_ERROR := 16#08, // Memory parity error (special case; historically for FC 20/21) + MEMORY_PARITY_ERROR := 16#08, - GATEWAY_PATH_UNAVAILABLE := 16#0A, // Gateway problem: path unavailable / misconfigured / overloaded - GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND := 16#0B // Gateway problem: target device did not respond + GATEWAY_PATH_UNAVAILABLE := 16#0A, + GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND := 16#0B ); END_TYPE ``` ## ModbusMaster.ClientBase -Abstract shared client scheduler and native transport owner. Use ClientTCP or ClientRTU. Holds up to 100 registered requests and runs one transaction at a time. Call the concrete client once per scan, from the same task as its requests. +Shared abstract client scheduler and registry. Call concrete client every scan; worker handles transaction progress. Exposes aggregated stats and toString. ```iecst {attribute 'monitoring_display' := 'toString'} @@ -121,22 +114,30 @@ Abstract shared client scheduler and native transport owner. Use ClientTCP or Cl FUNCTION_BLOCK ABSTRACT ClientBase VAR_INPUT {attribute 'symbol' := 'none'} - in_timeout: TIME := T#0MS; // If this is set, this will be used on requests, otherwise it will be calculated from in_dataRate. + + in_timeout: TIME := T#0MS; {attribute 'symbol' := 'none'} - in_resetStats: BOOL; // Set this bool to true to reset statistics. + + in_resetStats: BOOL; END_VAR VAR_OUTPUT {attribute 'symbol' := 'none'} - msgLastSecondCnt: UDINT; //Messages in the last second + + msgLastSecondCnt: UDINT; {attribute 'symbol' := 'none'} - msgLastSecondTime: UDINT; //Total amount of time for all messages sent last second in ms + + msgLastSecondTime: UDINT; {attribute 'symbol' := 'none'} + numMsgSent: UDINT; {attribute 'symbol' := 'none'} + numMsgReply: UDINT; {attribute 'symbol' := 'none'} - allRequestsOnline: BOOL; //All requests are working - reqStats: ARRAY[0..99] OF POINTER TO RequestStats; // For debugging modbus requests + + allRequestsOnline: BOOL; + + reqStats: ARRAY[0..99] OF POINTER TO RequestStats; END_VAR VAR dataRate : DINT; @@ -145,148 +146,139 @@ VAR allRequestsOffline : BOOL; END_VAR VAR -nativeMode : BOOL; -nativeHandle : UDINT; -nativeIP : DWORD; -nativePort : UINT; -nativeNowMicroseconds : SYSTIME; -nativeNowMilliseconds : ULINT; -nativeStatus : ARRAY[0..23] OF UDINT; -nativeActiveIndex : DINT := -1; -nativeSelected : POINTER TO RequestBase; -nativeResultReady : BOOL; -nativeFinishedIndex : DINT := -1; -nativeWindowStartMilliseconds : ULINT; -nativeWasConnected : BOOL; -nativeEnable : BOOL := TRUE; -nativeConnectTimeoutMilliseconds : UDINT := 2000; -nativeRetryDelayMilliseconds : UDINT := 100; -nativeHoldUncertainWrites : BOOL := FALSE; +engineMode : BOOL; +engineHandle : UDINT; +engineIP : DWORD; +enginePort : UINT; +engineNowMicroseconds : SYSTIME; +engineNowMilliseconds : ULINT; +engineStatus : ARRAY[0..23] OF UDINT; +engineActiveIndex : DINT := -1; +engineSelected : POINTER TO RequestBase; +engineResultReady : BOOL; +engineFinishedIndex : DINT := -1; +engineWindowStartMilliseconds : ULINT; +engineWasConnected : BOOL; +engineEnable : BOOL := TRUE; +engineConnectTimeoutMilliseconds : UDINT := 2000; +engineRetryDelayMilliseconds : UDINT := 100; +engineHoldUncertainWrites : BOOL := FALSE; END_VAR VAR_INPUT {attribute 'symbol' := 'none'} -nativeIO : ARRAY[0..1999] OF WORD; +engineIO : ARRAY[0..1999] OF WORD; {attribute 'symbol' := 'none'} -nativeActiveRequest : POINTER TO RequestBase; +engineActiveRequest : POINTER TO RequestBase; {attribute 'symbol' := 'none'} -nativeArgUnit : BYTE; +engineArgUnit : BYTE; {attribute 'symbol' := 'none'} -nativeArgFunction : BYTE; +engineArgFunction : BYTE; {attribute 'symbol' := 'none'} -nativeArgAddress : UINT; +engineArgAddress : UINT; {attribute 'symbol' := 'none'} -nativeArgQuantity : UINT; +engineArgQuantity : UINT; {attribute 'symbol' := 'none'} -nativeArgTimeout : UDINT := 1000; +engineArgTimeout : UDINT := 1000; {attribute 'symbol' := 'none'} -nativeSent : UDINT; +engineSent : UDINT; {attribute 'symbol' := 'none'} -nativeReplied : UDINT; +engineReplied : UDINT; {attribute 'symbol' := 'none'} -nativeWindowCount : UDINT; +engineWindowCount : UDINT; {attribute 'symbol' := 'none'} -nativeWindowTime : UDINT; +engineWindowTime : UDINT; END_VAR VAR_OUTPUT + out_connected : BOOL; + out_connectionState : UDINT; -out_lastNativeError : UDINT; + +out_lastErrorCode : UDINT; + out_socketError : UDINT; + out_reconnectCount : UDINT; + out_registrationFull : BOOL; END_VAR VAR CONSTANT - NATIVE_CMD_OPEN : UDINT := 1; - NATIVE_CMD_CLOSE : UDINT := 2; - NATIVE_CMD_ENABLE : UDINT := 3; - NATIVE_CMD_TICK : UDINT := 5; - NATIVE_CMD_SUBMIT : UDINT := 6; - NATIVE_CMD_COPY_READ : UDINT := 7; - NATIVE_CMD_CONFIGURE : UDINT := 9; - NATIVE_CMD_ACKNOWLEDGE : UDINT := 10; - NATIVE_CMD_REQUEST_TIMEOUT : UDINT := 11; - NATIVE_STATE_DISABLED : UDINT := 0; - NATIVE_STATE_READY : UDINT := 3; - NATIVE_STATE_RECEIVING : UDINT := 5; - NATIVE_ERROR_INVALID_HANDLE : UDINT := 256; - NATIVE_ERROR_CANCELLED : UDINT := 10; - NATIVE_STATUS_HANDLE : UDINT := 0; - NATIVE_STATUS_STATE : UDINT := 1; - NATIVE_STATUS_DONE : UDINT := 3; - NATIVE_STATUS_ERROR : UDINT := 4; - NATIVE_STATUS_RESULT : UDINT := 5; - NATIVE_STATUS_LAST_ERROR : UDINT := 6; - NATIVE_STATUS_SOCKET_ERROR : UDINT := 7; - NATIVE_STATUS_EXCEPTION : UDINT := 8; - NATIVE_STATUS_WRITE_UNCERTAIN : UDINT := 9; - NATIVE_STATUS_RECONNECTS : UDINT := 11; - NATIVE_STATUS_FUNCTION : UDINT := 18; + CMD_OPEN : UDINT := 1; + CMD_CLOSE : UDINT := 2; + CMD_ENABLE : UDINT := 3; + CMD_TICK : UDINT := 5; + CMD_SUBMIT : UDINT := 6; + CMD_COPY_READ : UDINT := 7; + CMD_CONFIGURE : UDINT := 9; + CMD_ACKNOWLEDGE : UDINT := 10; + CMD_REQUEST_TIMEOUT : UDINT := 11; + STATE_DISABLED : UDINT := 0; + STATE_READY : UDINT := 3; + STATE_RECEIVING : UDINT := 5; + ERROR_INVALID_HANDLE : UDINT := 256; + ERROR_CANCELLED : UDINT := 10; + STATUS_HANDLE : UDINT := 0; + STATUS_STATE : UDINT := 1; + STATUS_DONE : UDINT := 3; + STATUS_ERROR : UDINT := 4; + STATUS_RESULT : UDINT := 5; + STATUS_LAST_ERROR : UDINT := 6; + STATUS_SOCKET_ERROR : UDINT := 7; + STATUS_EXCEPTION : UDINT := 8; + STATUS_WRITE_UNCERTAIN : UDINT := 9; + STATUS_RECONNECTS : UDINT := 11; + STATUS_FUNCTION : UDINT := 18; END_VAR VAR - nativeOpenCommand : UDINT := 1; - nativeDevice : STRING(127); - nativeSerialBaud : UDINT; - nativeSerialParity : BYTE; - nativeSerialBits : BYTE; - nativeSerialStops : BYTE; - nativeSerialRs485 : UDINT; + engineOpenCommand : UDINT := 1; + engineDevice : STRING(127); + engineSerialBaud : UDINT; + engineSerialParity : BYTE; + engineSerialBits : BYTE; + engineSerialStops : BYTE; + engineSerialRs485 : UDINT; END_VAR VAR_INPUT + in_enable : BOOL := TRUE; END_VAR + +VAR_OUTPUT + stats : ClientStats; +END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `in_timeout` | VAR_INPUT | `TIME := T#0MS` | Reply timeout override. ClientBase: TIME; RequestBase: SYSTIME numeric milliseconds (not microseconds). Zero selects inherited/calculated timeout. | -| `in_resetStats` | VAR_INPUT | `BOOL` | Reset request consumed and cleared by nativeCycle. Supplying TRUE on every call resets statistics every scan; pulse once. | -| `msgLastSecondCnt` | VAR_OUTPUT | `UDINT` | Submitted request count in the last completed window of at least 1000 ms, not necessarily an exact sliding second. | -| `msgLastSecondTime` | VAR_OUTPUT | `UDINT` | Sum of successful transaction durations in that completed window (ms); excludes failed durations. | -| `numMsgSent` | VAR_OUTPUT | `UDINT` | IEC accepted submission count since resetStats; not a count of physical packets. | -| `numMsgReply` | VAR_OUTPUT | `UDINT` | IEC successful or Modbus-exception reply count since resetStats. | -| `allRequestsOnline` | VAR_OUTPUT | `BOOL` | TRUE when at least one active request exists and onlineCnt equals activeReqCnt. Online can include exception replies. | -| `reqStats` | VAR_OUTPUT | `ARRAY[0..99] OF POINTER TO RequestStats` | Pointers to registered request statistics by registry index, populated on client scans. Null entries are unused; validate before dereference. | -| `nativeIO` | VAR_INPUT | `ARRAY[0..1999] OF WORD` | Internal wrapper state exposed for compatibility; normally leave under library control. | -| `nativeActiveRequest` | VAR_INPUT | `POINTER TO RequestBase` | Internal wrapper state exposed for compatibility; normally leave under library control. | -| `nativeArgUnit` | VAR_INPUT | `BYTE` | Internal wrapper state exposed for compatibility; normally leave under library control. | -| `nativeArgFunction` | VAR_INPUT | `BYTE` | Internal wrapper state exposed for compatibility; normally leave under library control. | -| `nativeArgAddress` | VAR_INPUT | `UINT` | Internal wrapper state exposed for compatibility; normally leave under library control. | -| `nativeArgQuantity` | VAR_INPUT | `UINT` | Internal wrapper state exposed for compatibility; normally leave under library control. | -| `nativeArgTimeout` | VAR_INPUT | `UDINT := 1000` | Internal wrapper state exposed for compatibility; normally leave under library control. | -| `nativeSent` | VAR_INPUT | `UDINT` | Internal wrapper state exposed for compatibility; normally leave under library control. | -| `nativeReplied` | VAR_INPUT | `UDINT` | Internal wrapper state exposed for compatibility; normally leave under library control. | -| `nativeWindowCount` | VAR_INPUT | `UDINT` | Internal wrapper state exposed for compatibility; normally leave under library control. | -| `nativeWindowTime` | VAR_INPUT | `UDINT` | Internal wrapper state exposed for compatibility; normally leave under library control. | -| `out_connected` | VAR_OUTPUT | `BOOL` | Transport is in ready/sending/receiving state. Does not prove a request succeeded or data is fresh. | -| `out_connectionState` | VAR_OUTPUT | `UDINT` | 0 disabled, 1 retry wait, 2 TCP connecting, 3 ready, 4 sending, 5 receiving. | -| `out_lastNativeError` | VAR_OUTPUT | `UDINT` | Native last transport error; may persist after recovery. Check current connection and request diagnostics too. | -| `out_socketError` | VAR_OUTPUT | `UDINT` | Native Linux errno snapshot as UDINT; may be historical. Used for both serial and sockets. | -| `out_reconnectCount` | VAR_OUTPUT | `UDINT` | Native successful reconnections after the first connection. Context recreation resets its native lifetime counters. | -| `out_registrationFull` | VAR_OUTPUT | `BOOL` | Sticky request registry overflow indicator; max 100 requests/client. Client reinitialization clears it. | -| `in_enable` | VAR_INPUT | `BOOL := TRUE` | Enable processing; TRUE by default. FALSE releases transport. Continue scanning to refresh state. | - ## ModbusMaster.ClientRTU -Modbus RTU client using a Linux serial device. Call every scan. Supports 8 data bits, parity 0 none/1 odd/2 even, and 1 or 2 stop bits. Uses unit IDs 1..247; broadcast 0 is unsupported. Serial-setting changes recreate the context. Automatic recovery proceeds while enabled and scanned. +Linux serial/RTU configuration. Changing serial parameters recreates the context. Explicit unit ID 1..247 is required on requests. ```iecst +{attribute 'monitoring_display' := 'toString'} {attribute 'no_assign'} FUNCTION_BLOCK ClientRTU EXTENDS ClientBase VAR_INPUT + in_device : STRING(127) := '/dev/ttyUSB0'; + in_baudRate : UDINT := 9600; - in_parity : BYTE := 0; // 0 none, 1 odd, 2 even + + in_parity : BYTE := 0; + in_dataBits : BYTE := 8; - in_stopBits : BYTE := 1; // 1 or 2 - in_rs485 : BOOL; // Request Linux kernel RS485 direction control. + + in_stopBits : BYTE := 1; + + in_rs485 : BOOL; + in_reconnectDelay : TIME := T#100MS; + in_holdUncertainWrites : BOOL := FALSE; END_VAR VAR @@ -300,243 +292,237 @@ VAR END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `in_device` | VAR_INPUT | `STRING(127) := '/dev/ttyUSB0'` | Linux serial device path, default /dev/ttyUSB0, at most 127 ASCII characters. Runtime account needs access. | -| `in_baudRate` | VAR_INPUT | `UDINT := 9600` | Serial baud rate, default 9600. Supplied C source accepts 1200,2400,4800,9600,19200,38400,57600,115200. | -| `in_parity` | VAR_INPUT | `BYTE := 0` | 0 none, 1 odd, 2 even. Must match the slave. | -| `in_dataBits` | VAR_INPUT | `BYTE := 8` | Only 8 supported by supplied native implementation. | -| `in_stopBits` | VAR_INPUT | `BYTE := 1` | 1 or 2; must match the slave. | -| `in_rs485` | VAR_INPUT | `BOOL` | Request Linux kernel RS485 direction control. Leave FALSE for USB adapters handling direction automatically. Requires driver support. | -| `in_reconnectDelay` | VAR_INPUT | `TIME := T#100MS` | Delay before native recovery attempt, default 100 ms; clamped to at least 1 ms. Effective timing also depends on scan interval and failure detection. | -| `in_holdUncertainWrites` | VAR_INPUT | `BOOL := FALSE` | FALSE allows normal retries after uncertain writes. TRUE holds affected write requests until writeNow(); read requests can continue. | - ## ModbusMaster.ClientTCP -Modbus TCP client with automatic retry after connection or transaction failure. Call every scan. Endpoint or timeout/retry configuration changes close the current context and reopen it on subsequent scans. Requests share this connection and execute sequentially. +TCP connection configuration. Changing endpoint, connect timeout or retry delay recreates its context. Inherits ClientBase statistics and monitoring string. ```iecst +{attribute 'monitoring_display' := 'toString'} {attribute 'no_assign'} FUNCTION_BLOCK ClientTCP EXTENDS ClientBase VAR_INPUT {attribute 'symbol' := 'none'} - in_ipAddress: ARRAY[0..3] OF BYTE; // TCP IP Address + + in_ipAddress: ARRAY[0..3] OF BYTE; {attribute 'symbol' := 'none'} - in_port: UINT := 502; // TCP port + + in_port: UINT := 502; {attribute 'symbol' := 'none'} - in_baudrate: UINT := 0; // Set this if using RS485 to TCP Converter, used to calculate timeout. + + in_baudrate: UINT := 0; END_VAR VAR_INPUT + in_connectTimeout : TIME := T#2S; -in_reconnectDelay : TIME := T#100MS; // Delay after a failed TCP connection/transaction. + +in_reconnectDelay : TIME := T#100MS; + in_holdUncertainWrites : BOOL := FALSE; END_VAR VAR -nativeEndpointIP : DWORD; -nativeEndpointPort : UINT; -nativeConfigConnect : UDINT; -nativeConfigRetry : UDINT; +engineEndpointIP : DWORD; +engineEndpointPort : UINT; +engineConfigConnect : UDINT; +engineConfigRetry : UDINT; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `in_ipAddress` | VAR_INPUT | `ARRAY[0..3] OF BYTE` | ClientTCP: four IPv4 bytes. Slave: packed numeric DWORD, 0 binds all interfaces; e.g. DWORD#16#C0A8321F for 192.168.50.31. | -| `in_port` | VAR_INPUT | `UINT := 502` | Remote TCP port, default 502. | -| `in_baudrate` | VAR_INPUT | `UINT := 0` | TCP gateway serial baud-rate hint for calculated reply timeout; 0 uses internal dataRate 105200. Does not configure a serial port. | -| `in_connectTimeout` | VAR_INPUT | `TIME := T#2S` | TCP connection-attempt deadline, default 2 s; clamped to at least 1 ms. | -| `in_reconnectDelay` | VAR_INPUT | `TIME := T#100MS` | Delay before native recovery attempt, default 100 ms; clamped to at least 1 ms. Effective timing also depends on scan interval and failure detection. | -| `in_holdUncertainWrites` | VAR_INPUT | `BOOL := FALSE` | FALSE allows normal retries after uncertain writes. TRUE holds affected write requests until writeNow(); read requests can continue. | - ## ModbusMaster.ReqReadCoil -Modbus FC 1: read BOOL coils. Call every scan with a client reference and data array. Protocol start address is zero-based; in_dataOffset is the actual array index. Maximum transaction length is 2000. Usable length is clamped to array capacity. Only a successful validated reply updates the destination slice. Old values remain after failures; check out_validData and out_stale. +Read request using the indicated BOOL/UINT array and shared RequestBase inputs. Successful replies update only the selected slice; inspect out_validData/out_stale. ```iecst {attribute 'no_assign'} FUNCTION_BLOCK ReqReadCoil EXTENDS RequestBase VAR_IN_OUT - // ARRAY[] OF BOOL to hold received data + {attribute 'symbol' := 'none'} + data: ARRAY[*] OF BOOL; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `data` | VAR_IN_OUT | `ARRAY[*] OF BOOL` | Open array: BOOL for coils/discretes; UINT for registers. Stable VAR_IN_OUT storage. Writes snapshot values at submission; reads replace only their successful slice. | - ## ModbusMaster.ReqReadDiscrete -Modbus FC 2: read BOOL discrete inputs. Call every scan with a client reference and data array. Protocol start address is zero-based; in_dataOffset is the actual array index. Maximum transaction length is 2000. Usable length is clamped to array capacity. Only a successful validated reply updates the destination slice. Old values remain after failures; check out_validData and out_stale. +Read request using the indicated BOOL/UINT array and shared RequestBase inputs. Successful replies update only the selected slice; inspect out_validData/out_stale. ```iecst {attribute 'no_assign'} FUNCTION_BLOCK ReqReadDiscrete EXTENDS RequestBase VAR_IN_OUT - // ARRAY[] OF BOOL to hold received data + {attribute 'symbol' := 'none'} + data: ARRAY[*] OF BOOL; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `data` | VAR_IN_OUT | `ARRAY[*] OF BOOL` | Open array: BOOL for coils/discretes; UINT for registers. Stable VAR_IN_OUT storage. Writes snapshot values at submission; reads replace only their successful slice. | - ## ModbusMaster.ReqReadHolding -Modbus FC 3: read UINT holding registers. Call every scan with a client reference and data array. Protocol start address is zero-based; in_dataOffset is the actual array index. Maximum transaction length is 125. Usable length is clamped to array capacity. Only a successful validated reply updates the destination slice. Old values remain after failures; check out_validData and out_stale. +Read request using the indicated BOOL/UINT array and shared RequestBase inputs. Successful replies update only the selected slice; inspect out_validData/out_stale. ```iecst {attribute 'no_assign'} FUNCTION_BLOCK ReqReadHolding EXTENDS RequestBase VAR_IN_OUT - // ARRAY[] OF UINT to hold received data + {attribute 'symbol' := 'none'} + data: ARRAY[*] OF UINT; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `data` | VAR_IN_OUT | `ARRAY[*] OF UINT` | Open array: BOOL for coils/discretes; UINT for registers. Stable VAR_IN_OUT storage. Writes snapshot values at submission; reads replace only their successful slice. | - ## ModbusMaster.ReqReadInput -Modbus FC 4: read UINT input registers. Call every scan with a client reference and data array. Protocol start address is zero-based; in_dataOffset is the actual array index. Maximum transaction length is 125. Usable length is clamped to array capacity. Only a successful validated reply updates the destination slice. Old values remain after failures; check out_validData and out_stale. +Read request using the indicated BOOL/UINT array and shared RequestBase inputs. Successful replies update only the selected slice; inspect out_validData/out_stale. ```iecst {attribute 'no_assign'} FUNCTION_BLOCK ReqReadInput EXTENDS RequestBase VAR_IN_OUT - // ARRAY[] OF UINT to hold received data + {attribute 'symbol' := 'none'} + data: ARRAY[*] OF UINT; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `data` | VAR_IN_OUT | `ARRAY[*] OF UINT` | Open array: BOOL for coils/discretes; UINT for registers. Stable VAR_IN_OUT storage. Writes snapshot values at submission; reads replace only their successful slice. | - ## ModbusMaster.RequestBase -Abstract request binding, scheduling and diagnostics. Derived requests register automatically when called with a valid initialized client. Call each instance every scan with stable array storage. Quantity zero disables this request. Actual length is MIN(in_quantity, remaining array capacity). +Abstract registered request. Quantity is clamped to available data capacity; zero disables scheduling. Concrete blocks supply data type and function code. ```iecst {attribute 'no_assign'} FUNCTION_BLOCK ABSTRACT RequestBase VAR_INPUT {attribute 'symbol' := 'none'} - client: REFERENCE TO ClientBase; // Instance of atMBClient + + client: REFERENCE TO ClientBase; {attribute 'symbol' := 'none'} + in_startAddr: UINT; {attribute 'symbol' := 'none'} + in_quantity: UINT; {attribute 'symbol' := 'none'} - in_dataOffset: UINT; // Offset position for data into array. + + in_dataOffset: UINT; {attribute 'symbol' := 'none'} + in_unitId: BYTE := 0; {attribute 'symbol' := 'none'} - in_timeout: SYSTIME := 0; // Reply timeout in ms; zero uses client/default calculation. + + in_timeout: SYSTIME := 0; {attribute 'symbol' := 'none'} - in_minDelay: TIME := T#10MS; // This is the minimum delay between queries, only relevent if high priority. + + in_minDelay: TIME := T#10MS; {attribute 'symbol' := 'none'} - in_prioritySetpoint: INT := 50; // this requests priority compared to other requests. Lower value is higher priority. + + in_prioritySetpoint: INT := 50; {attribute 'symbol' := 'none'} - in_priorityCurrent: INT; //Current priority. This is decrimented and the request is executed when at zero. + + in_priorityCurrent: INT; {attribute 'symbol' := 'none'} - stats: RequestStats; //Request stats + + stats: RequestStats; END_VAR VAR {attribute 'symbol' := 'none'} + dataCapacity: UINT; END_VAR VAR -nativeFunction : BYTE; -nativeTransactionQuantity : UINT; -nativeTransactionDataIndex : UINT; -nativeInFlight : BOOL; -nativeReadComplete : BOOL; -nativeCopyAllowed : BOOL; -nativeWriteHold : BOOL; -nativeRequestStartMilliseconds : ULINT; -nativeDataIndex : DINT; -nativeCallResult : UDINT; +engineFunction : BYTE; +engineTransactionQuantity : UINT; +engineTransactionDataIndex : UINT; +engineInFlight : BOOL; +engineReadComplete : BOOL; +engineCopyAllowed : BOOL; +engineWriteHold : BOOL; +engineRequestStartMilliseconds : ULINT; +engineDataIndex : DINT; +engineCallResult : UDINT; END_VAR VAR_OUTPUT -out_nativeError : UDINT; + +out_errorCode : UDINT; + out_socketError : UDINT; + out_writeUncertain : BOOL; + out_validData : BOOL; + out_dataAgeMs : ULINT; + out_stale : BOOL := TRUE; + out_parameterError : BOOL; END_VAR VAR_INPUT + in_staleAfter : TIME := T#2S; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `client` | VAR_INPUT | `REFERENCE TO ClientBase` | Reference to the owning ClientBase concrete instance. Initialize/call client before first registration; retain a stable instance. | -| `in_startAddr` | VAR_INPUT | `UINT` | Zero-based Modbus protocol address, not a 40001/30001 display reference. Must fit the transaction into address space. | -| `in_quantity` | VAR_INPUT | `UINT` | Requested item count; actual stats.reqLength is MIN(this, available destination/source capacity). Zero makes the request inactive. | -| `in_dataOffset` | VAR_INPUT | `UINT` | Actual first index in the application array. For ARRAY[1..10], use 1 to start at the first element; not a relative offset. | -| `in_unitId` | VAR_INPUT | `BYTE := 0` | Modbus unit/slave ID, default 0. RTU requires explicit 1..247; TCP/gateway values depend on the target. | -| `in_timeout` | VAR_INPUT | `SYSTIME := 0` | Reply timeout override. ClientBase: TIME; RequestBase: SYSTIME numeric milliseconds (not microseconds). Zero selects inherited/calculated timeout. | -| `in_minDelay` | VAR_INPUT | `TIME := T#10MS` | Minimum start-to-start time before scheduler eligibility, default 10 ms. Not a fixed polling period. | -| `in_prioritySetpoint` | VAR_INPUT | `INT := 50` | Priority restored on success, default 50. Lower values win. Error backoff can replace current priority. | -| `in_priorityCurrent` | VAR_INPUT | `INT` | Mutable scheduler priority. Lower values win, equal values use age. WriteNow sets 0. Application need not maintain it each scan. | -| `stats` | VAR_INPUT | `RequestStats` | Per-request RequestStats instance maintained by request processing; inspect rather than overwrite. | -| `out_nativeError` | VAR_OUTPUT | `UDINT` | Result of the last completed/failed request; 0 success. Precise native code, distinct from IEC Error enum. | -| `out_socketError` | VAR_OUTPUT | `UDINT` | Native Linux errno snapshot as UDINT; may be historical. Used for both serial and sockets. | -| `out_writeUncertain` | VAR_OUTPUT | `BOOL` | Write may have reached target without a confirmed response. It is not proof of failure or success. Relevant for write requests. | -| `out_validData` | VAR_OUTPUT | `BOOL` | TRUE after a successful read; subsequent read failures do not necessarily clear it. Client release or invalidateNativeData clears it. | -| `out_dataAgeMs` | VAR_OUTPUT | `ULINT` | Age since last successful read in milliseconds. Updated by normal processing; completion early returns can defer refresh by one scan. | -| `out_stale` | VAR_OUTPUT | `BOOL := TRUE` | TRUE before valid read data or when age exceeds in_staleAfter. Array contents are retained. | -| `out_parameterError` | VAR_OUTPUT | `BOOL` | Array-offset validation for requests; full transaction validation can instead appear in out_nativeError/stats.lastErrorId. Slave: invalid port/interfaceName/table bounds or capacity. | -| `in_staleAfter` | VAR_INPUT | `TIME := T#2S` | Read data age threshold, default 2 s. out_stale becomes TRUE when age is strictly greater; does not stop polling. | - ## ModbusMaster.RequestStats -Request counters, completion properties, online/offline tracking and 100 ms indicator lights. Normally maintained by RequestBase. Online means a valid response was received, including a Modbus exception; it does not guarantee successful data. Several fields are declared VAR_INPUT for compatibility but are maintained internally. +Per-request history, communication flags and 100 ms display indicators. Maintained by request processing; temporary disable retains historical counters. ```iecst {attribute 'monitoring_display' := 'toString'} {attribute 'no_assign'} FUNCTION_BLOCK RequestStats VAR_INPUT - lastErrorId: Error; //Last error id - lastExceptionCode: ExceptionCodes; //Last modbus exception code. These are the standard modbus exceptions. - lastValidDuration: UDINT; // How long last transaction took (ms) - longestValidDuration: UDINT; //Longest valid duration (ms) - interval: UDINT; // Start-to-start interval (ms) - offlineCnt: UINT; //Count of how many times this request has gone offline - doneCnt: UDINT; // Count of successful request - errorCnt: UDINT; // Count of failed requests - doneCntRst: UDINT; // Count of succesful requests, resets on error result - errorCntRst: UDINT; // Count of failed requests, resets on succesful request - sinceLastExec: UDINT; // Time since the last start (ms) + + lastErrorId: Error; + + lastExceptionCode: ExceptionCodes; + + lastValidDuration: UDINT; + + longestValidDuration: UDINT; + + interval: UDINT; + + offlineCnt: UINT; + + doneCnt: UDINT; + + errorCnt: UDINT; + + doneCntRst: UDINT; + + errorCntRst: UDINT; + + sinceLastExec: UDINT; + timeoutCnt: UDINT; - reqLength: UINT; //Number of registers requested in request, used to calc active. + + reqLength: UINT; END_VAR VAR_OUTPUT - executeLight: BOOL; //Execute that stays on momentarily - reqDoneLight: BOOL; //Request done that stays on momentarily - reqErrorLight: BOOL; //Request error that stays on momentarily - reqBusyLight: BOOL; //Request busy that stays on momentarily - online: BOOL; //Online means the requests are being replied to normally or with modbus exceptions. Communications are ok. - offline: BOOL; //Opposite of online, but starts out as false on program start. - active: BOOL; // This is true if its actually being used, and not having a count of 0 on the request. + + executeLight: BOOL; + + reqDoneLight: BOOL; + + reqErrorLight: BOOL; + + reqBusyLight: BOOL; + + online: BOOL; + + offline: BOOL; + + active: BOOL; + executeMbFb: BOOL; END_VAR VAR @@ -548,144 +534,140 @@ VAR busyIndicatorDelay: TOF; {attribute 'symbol' := 'none'} executeIndicatorDelay: TOF; - toString: STRING(20); + toString: STRING(64); {attribute 'symbol' := 'none'} - executeTrig: R_TRIG; //Execute trigger + executeTrig: R_TRIG; {attribute 'symbol' := 'none'} - offlineTrig: R_TRIG; //Offline trigger + offlineTrig: R_TRIG; {attribute 'symbol' := 'none'} - lastRequestStartMicroseconds: SYSTIME; //time of start of last request + lastRequestStartMicroseconds: SYSTIME; {attribute 'symbol' := 'none'} currentTimeMicroseconds: SYSTIME; - _execute: BOOL; //Execute trigger and status. Turn on to trigger request. Will turn off when request is complete - _reqDone: BOOL; //Request done - _reqBusy: BOOL; //Request busy - _reqError: BOOL; //Request error + _execute: BOOL; + _reqDone: BOOL; + _reqBusy: BOOL; + _reqError: BOOL; END_VAR VAR CONSTANT {attribute 'symbol' := 'none'} - INDICATOR_HOLD_TIME: TIME:= T#100MS; //Light on time for debug indicators + INDICATOR_HOLD_TIME: TIME:= T#100MS; END_VAR VAR_INPUT -nativeResponseSeen : BOOL; -nativeLastSuccessMs : ULINT; + +engineResponseSeen : BOOL; + +engineLastSuccessMs : ULINT; +END_VAR + +VAR_INPUT + sentCnt : UDINT; + replyCnt : UDINT; + exceptionCnt : UDINT; + totalValidDuration : ULINT; // Sum of successful worker transaction durations, milliseconds. +END_VAR +VAR + previousOnline : BOOL; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `lastErrorId` | VAR_INPUT | `Error` | Last mapped IEC error; successful completion resets to OK. | -| `lastExceptionCode` | VAR_INPUT | `ExceptionCodes` | Last Modbus exception, success=0; stats may substitute TIMEOUT=-1 for non-exception failures. | -| `lastValidDuration` | VAR_INPUT | `UDINT` | Last successful request duration (ms). | -| `longestValidDuration` | VAR_INPUT | `UDINT` | Maximum successful duration recorded; resetStats does not clear it. | -| `interval` | VAR_INPUT | `UDINT` | Start-to-start execution interval (ms). | -| `offlineCnt` | VAR_INPUT | `UINT` | Count of NOT online rising edges; can include initial not-online state, not only a physical disconnect. | -| `doneCnt` | VAR_INPUT | `UDINT` | Total successful completions; resetStats clears. | -| `errorCnt` | VAR_INPUT | `UDINT` | Total failed completions; resetStats clears. | -| `doneCntRst` | VAR_INPUT | `UDINT` | Consecutive successes; reset on error/offline/resetStats. | -| `errorCntRst` | VAR_INPUT | `UDINT` | Consecutive errors; reset on successful completion/resetStats. | -| `sinceLastExec` | VAR_INPUT | `UDINT` | Age since last execution start (ms), capped around 60000 by the statistics clock logic. | -| `timeoutCnt` | VAR_INPUT | `UDINT` | Statistics non-exception error count; can include other native failures. Not exclusively reply timeouts; resetStats does not clear it. | -| `reqLength` | VAR_INPUT | `UINT` | Effective transaction length; zero means inactive. | -| `executeLight` | VAR_OUTPUT | `BOOL` | Execution indicator with 100 ms off-delay. | -| `reqDoneLight` | VAR_OUTPUT | `BOOL` | Completion indicator with 100 ms off-delay. | -| `reqErrorLight` | VAR_OUTPUT | `BOOL` | Error indicator with 100 ms off-delay. | -| `reqBusyLight` | VAR_OUTPUT | `BOOL` | Busy indicator with 100 ms off-delay. | -| `online` | VAR_OUTPUT | `BOOL` | Communication status; valid normal/exception response sets evidence. Distinct from valid data. | -| `offline` | VAR_OUTPUT | `BOOL` | Offline status; initialized FALSE, explicitly set on client-offline or repeated failures. | -| `active` | VAR_OUTPUT | `BOOL` | TRUE when reqLength is nonzero. | -| `executeMbFb` | VAR_OUTPUT | `BOOL` | Legacy retained output; no assignment in current native implementation. Do not use as a trigger. | -| `nativeResponseSeen` | VAR_INPUT | `BOOL` | Internal communication-response evidence, maintained by nativeFinish and offline handling. | -| `nativeLastSuccessMs` | VAR_INPUT | `ULINT` | Internal successful-read timestamp in monotonic milliseconds. | - ## ModbusMaster.ReqWriteCoil -Modbus FC 15: write BOOL coils. Call every scan with a client reference and data array. Protocol start address is zero-based; in_dataOffset is the actual array index. Maximum transaction length is 1968. Usable length is clamped to array capacity. Values are copied into native storage at submission. This FB uses the multiple-write FC even for one element. Use writeNow() to release a held uncertain write. +Cyclic write request. Values are copied at submission; writeNow changes eligibility/hold state. See uncertain-write policy in USER-GUIDE.md. ```iecst {attribute 'no_assign'} FUNCTION_BLOCK ReqWriteCoil EXTENDS RequestBase VAR_IN_OUT - // ARRAY[] OF BOOL to hold received data + {attribute 'symbol' := 'none'} + data: ARRAY[*] OF BOOL; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `data` | VAR_IN_OUT | `ARRAY[*] OF BOOL` | Open array: BOOL for coils/discretes; UINT for registers. Stable VAR_IN_OUT storage. Writes snapshot values at submission; reads replace only their successful slice. | - ## ModbusMaster.ReqWriteHolding -Modbus FC 16: write UINT holding registers. Call every scan with a client reference and data array. Protocol start address is zero-based; in_dataOffset is the actual array index. Maximum transaction length is 123. Usable length is clamped to array capacity. Values are copied into native storage at submission. This FB uses the multiple-write FC even for one element. Use writeNow() to release a held uncertain write. +Cyclic write request. Values are copied at submission; writeNow changes eligibility/hold state. See uncertain-write policy in USER-GUIDE.md. ```iecst {attribute 'no_assign'} FUNCTION_BLOCK ReqWriteHolding EXTENDS RequestBase VAR_IN_OUT - // ARRAY[] OF UINT to hold received data + {attribute 'symbol' := 'none'} + data: ARRAY[*] OF UINT; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `data` | VAR_IN_OUT | `ARRAY[*] OF UINT` | Open array: BOOL for coils/discretes; UINT for registers. Stable VAR_IN_OUT storage. Writes snapshot values at submission; reads replace only their successful slice. | - ## ModbusMaster.To_IPAddress -Returns the four supplied bytes as ARRAY[0..3] OF BYTE. Example: atMb.To_IPAddress(192,168,50,10). This is the ClientTCP address format; the slave bind address instead uses a packed DWORD. +Return four BYTE IPv4 octets as the ClientTCP address array. ```iecst FUNCTION To_IPAddress : ARRAY[0..3] OF BYTE VAR_INPUT + byte1: BYTE; + byte2: BYTE; + byte3: BYTE; + byte4: BYTE; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `byte1` | VAR_INPUT | `BYTE` | First IPv4 octet. | -| `byte2` | VAR_INPUT | `BYTE` | Second IPv4 octet. | -| `byte3` | VAR_INPUT | `BYTE` | Third IPv4 octet. | -| `byte4` | VAR_INPUT | `BYTE` | Fourth IPv4 octet. | - ## ModbusSlave.ModbusSlave -Native Modbus TCP server. Same implementation and interface as SlaveTCP. Call once per scan with all four memory arrays. Array bounds are protocol addresses; each table is limited to 2000 elements and addresses 0..65535. Pushes application memory, performs bounded network processing, then copies native memory back. This is TCP only, not an RTU slave. +TCP server: four memory arrays, eight peers, up to 2000 entries/table. Array bounds define protocol addresses. Dirty remote writes are protected until PLC pull. Mapping changes require in_reset. No RTU server is provided. ```iecst {attribute 'no_assign'} FUNCTION_BLOCK ModbusSlave VAR_IN_OUT + aDiscreteInputsMemory : ARRAY[*] OF BOOL; + aCoilsMemory : ARRAY[*] OF BOOL; + aInputRegistersMemory : ARRAY[*] OF UINT; + aHoldingRegistersMemory : ARRAY[*] OF UINT; END_VAR VAR_INPUT + in_enable : BOOL := TRUE; + in_reset : BOOL; + in_reconnectDelay : TIME := T#100MS; - interfaceName : WSTRING; // Legacy field; nonempty rejected. Use in_ipAddress. - in_ipAddress : DWORD; // Numeric bind IP, 0 = all interfaces. + + interfaceName : WSTRING; + + in_ipAddress : DWORD; + port : UINT := 502; END_VAR VAR_OUTPUT + out_running : BOOL; + out_error : BOOL; + out_errorId : Error; + out_parameterError : BOOL; + out_socketError : UDINT; + out_connectedClients : UINT; + out_requestsReceived : UDINT; + out_repliesSent : UDINT; + out_exceptionReplies : UDINT; END_VAR VAR @@ -693,6 +675,7 @@ VAR serverStatus : ARRAY[0..23] OF UDINT; serverIO : ARRAY[0..1999] OF WORD; nowUs : SYSTIME; + nowMs : ULINT; lastIP : DWORD; lastPort : UINT; @@ -705,58 +688,55 @@ VAR END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `aDiscreteInputsMemory` | VAR_IN_OUT | `ARRAY[*] OF BOOL` | Read-only from network BOOL input table; FC2 reads. Application updates it. | -| `aCoilsMemory` | VAR_IN_OUT | `ARRAY[*] OF BOOL` | Read/write BOOL coil table. Array bounds are protocol addresses; FC1 reads, FC5/15 write. | -| `aInputRegistersMemory` | VAR_IN_OUT | `ARRAY[*] OF UINT` | Read-only from network UINT input table; FC4 reads. Application updates it. | -| `aHoldingRegistersMemory` | VAR_IN_OUT | `ARRAY[*] OF UINT` | Read/write UINT holding table; FC3 reads, FC6/16 write. | -| `in_enable` | VAR_INPUT | `BOOL := TRUE` | Enable processing; TRUE by default. FALSE releases transport. Continue scanning to refresh state. | -| `in_reset` | VAR_INPUT | `BOOL` | Slave one-shot recreation input. FB clears it internally; supplying TRUE every scan recreates every scan. | -| `in_reconnectDelay` | VAR_INPUT | `TIME := T#100MS` | Delay before native recovery attempt, default 100 ms; clamped to at least 1 ms. Effective timing also depends on scan interval and failure detection. | -| `interfaceName` | VAR_INPUT | `WSTRING` | Legacy slave field. Must be empty; nonempty is rejected. Use packed in_ipAddress instead. | -| `in_ipAddress` | VAR_INPUT | `DWORD` | ClientTCP: four IPv4 bytes. Slave: packed numeric DWORD, 0 binds all interfaces; e.g. DWORD#16#C0A8321F for 192.168.50.31. | -| `port` | VAR_INPUT | `UINT := 502` | Slave listening port, default 502, must be nonzero. | -| `out_running` | VAR_OUTPUT | `BOOL` | Native listener is bound and enabled. Does not mean a peer is connected. | -| `out_error` | VAR_OUTPUT | `BOOL` | Current server error flag when outputs are refreshed; some early returns leave other diagnostics unchanged. | -| `out_errorId` | VAR_OUTPUT | `Error` | IEC server error classification: parameter/internal/native socket class. Consult errno for OS detail. | -| `out_parameterError` | VAR_OUTPUT | `BOOL` | Array-offset validation for requests; full transaction validation can instead appear in out_nativeError/stats.lastErrorId. Slave: invalid port/interfaceName/table bounds or capacity. | -| `out_socketError` | VAR_OUTPUT | `UDINT` | Native Linux errno snapshot as UDINT; may be historical. Used for both serial and sockets. | -| `out_connectedClients` | VAR_OUTPUT | `UINT` | Number of native TCP peers connected (max 8 per server in supplied C source). | -| `out_requestsReceived` | VAR_OUTPUT | `UDINT` | Native request count for current server context. | -| `out_repliesSent` | VAR_OUTPUT | `UDINT` | Native fully transmitted reply count for current context, including exception replies. | -| `out_exceptionReplies` | VAR_OUTPUT | `UDINT` | Number of Modbus exception replies for current server context. | - ## ModbusSlave.SlaveTCP -Native Modbus TCP server with four separate memory tables. Same implementation and interface as ModbusSlave. FC 1/2/3/4 read the tables; FC 5/6/15/16 write coils/holding registers. Array lower bounds define Modbus addresses. Call every scan and retain array storage. This is TCP only. +TCP server: four memory arrays, eight peers, up to 2000 entries/table. Array bounds define protocol addresses. Dirty remote writes are protected until PLC pull. Mapping changes require in_reset. No RTU server is provided. ```iecst {attribute 'no_assign'} FUNCTION_BLOCK SlaveTCP VAR_IN_OUT + aDiscreteInputsMemory : ARRAY[*] OF BOOL; + aCoilsMemory : ARRAY[*] OF BOOL; + aInputRegistersMemory : ARRAY[*] OF UINT; + aHoldingRegistersMemory : ARRAY[*] OF UINT; END_VAR VAR_INPUT + in_enable : BOOL := TRUE; + in_reset : BOOL; + in_reconnectDelay : TIME := T#100MS; - interfaceName : WSTRING; // Legacy field; nonempty rejected. Use in_ipAddress. - in_ipAddress : DWORD; // Numeric bind IP, 0 = all interfaces. + + interfaceName : WSTRING; + + in_ipAddress : DWORD; + port : UINT := 502; END_VAR VAR_OUTPUT + out_running : BOOL; + out_error : BOOL; + out_errorId : Error; + out_parameterError : BOOL; + out_socketError : UDINT; + out_connectedClients : UINT; + out_requestsReceived : UDINT; + out_repliesSent : UDINT; + out_exceptionReplies : UDINT; END_VAR VAR @@ -764,6 +744,7 @@ VAR serverStatus : ARRAY[0..23] OF UDINT; serverIO : ARRAY[0..1999] OF WORD; nowUs : SYSTIME; + nowMs : ULINT; lastIP : DWORD; lastPort : UINT; @@ -776,34 +757,11 @@ VAR END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `aDiscreteInputsMemory` | VAR_IN_OUT | `ARRAY[*] OF BOOL` | Read-only from network BOOL input table; FC2 reads. Application updates it. | -| `aCoilsMemory` | VAR_IN_OUT | `ARRAY[*] OF BOOL` | Read/write BOOL coil table. Array bounds are protocol addresses; FC1 reads, FC5/15 write. | -| `aInputRegistersMemory` | VAR_IN_OUT | `ARRAY[*] OF UINT` | Read-only from network UINT input table; FC4 reads. Application updates it. | -| `aHoldingRegistersMemory` | VAR_IN_OUT | `ARRAY[*] OF UINT` | Read/write UINT holding table; FC3 reads, FC6/16 write. | -| `in_enable` | VAR_INPUT | `BOOL := TRUE` | Enable processing; TRUE by default. FALSE releases transport. Continue scanning to refresh state. | -| `in_reset` | VAR_INPUT | `BOOL` | Slave one-shot recreation input. FB clears it internally; supplying TRUE every scan recreates every scan. | -| `in_reconnectDelay` | VAR_INPUT | `TIME := T#100MS` | Delay before native recovery attempt, default 100 ms; clamped to at least 1 ms. Effective timing also depends on scan interval and failure detection. | -| `interfaceName` | VAR_INPUT | `WSTRING` | Legacy slave field. Must be empty; nonempty is rejected. Use packed in_ipAddress instead. | -| `in_ipAddress` | VAR_INPUT | `DWORD` | ClientTCP: four IPv4 bytes. Slave: packed numeric DWORD, 0 binds all interfaces; e.g. DWORD#16#C0A8321F for 192.168.50.31. | -| `port` | VAR_INPUT | `UINT := 502` | Slave listening port, default 502, must be nonzero. | -| `out_running` | VAR_OUTPUT | `BOOL` | Native listener is bound and enabled. Does not mean a peer is connected. | -| `out_error` | VAR_OUTPUT | `BOOL` | Current server error flag when outputs are refreshed; some early returns leave other diagnostics unchanged. | -| `out_errorId` | VAR_OUTPUT | `Error` | IEC server error classification: parameter/internal/native socket class. Consult errno for OS detail. | -| `out_parameterError` | VAR_OUTPUT | `BOOL` | Array-offset validation for requests; full transaction validation can instead appear in out_nativeError/stats.lastErrorId. Slave: invalid port/interfaceName/table bounds or capacity. | -| `out_socketError` | VAR_OUTPUT | `UDINT` | Native Linux errno snapshot as UDINT; may be historical. Used for both serial and sockets. | -| `out_connectedClients` | VAR_OUTPUT | `UINT` | Number of native TCP peers connected (max 8 per server in supplied C source). | -| `out_requestsReceived` | VAR_OUTPUT | `UDINT` | Native request count for current server context. | -| `out_repliesSent` | VAR_OUTPUT | `UDINT` | Native fully transmitted reply count for current context, including exception replies. | -| `out_exceptionReplies` | VAR_OUTPUT | `UDINT` | Number of Modbus exception replies for current server context. | - ## ClientBase.addRequest -Register a non-null RequestBase pointer once. Duplicate pointers are ignored. Capacity is 100 requests per client; overflow sets out_registrationFull. No removal API is provided. Normally called automatically by the request FB. +Register unique non-null request pointer, max 100. Duplicate registration is ignored. Overflow sets registrationFull. ```iecst -//Adds the specified request to the global list METHOD PUBLIC addRequest VAR_INPUT request: POINTER TO RequestBase; @@ -814,28 +772,21 @@ VAR END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `request` | VAR_INPUT | `POINTER TO RequestBase` | Internal wrapper state exposed for compatibility; normally leave under library control. | - ## ClientBase.FB_exit -CODESYS lifecycle hook releasing owned native resources. Applications should use release/recreateClient rather than call lifecycle hooks manually. Server hooks close the server context; the client hook calls nativeRelease. +Lifecycle resource release. Worker is joined by component exit; context hooks close their owned handle. Use public release for application shutdown. ```iecst METHOD FB_exit : BOOL VAR_INPUT + bInCopyCode : BOOL; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `bInCopyCode` | VAR_INPUT | `BOOL` | Runtime lifecycle copy-code indicator; current implementation does not branch on it. | - ## ClientBase.FB_reinit -CODESYS lifecycle hook clearing native handles and in-flight state. ClientBase also clears the request registry for re-registration; RequestBase clears execution/completion flags; servers clear their handle. These hooks alone do not establish qualified online-change support. +Clear handles/registry or request execution state after instance reinitialization. Does not guarantee online-change correctness. ```iecst METHOD FB_reinit : BOOL @@ -844,13 +795,14 @@ VAR END_VAR ``` -## ClientBase.nativeCall +## ClientBase.callEngine -Internal adapter to atMbCall_cext. Command 20 packs the ASCII serial path into WORD storage and passes serial settings in ABI fields; other commands use normal client arguments and status buffers. Return is the native command result, 0 for success. +Internal synchronous ABI adapter with WORD transfer buffer and 24-word status storage. RTU open repurposes fields to pass serial path/configuration. ```iecst -METHOD nativeCall : UDINT +METHOD callEngine : UDINT VAR_INPUT + command : UDINT; END_VAR @@ -861,49 +813,45 @@ VAR END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `command` | VAR_INPUT | `UDINT` | Native command selector. Use wrappers in application code. | +## ClientBase.processCycle -## ClientBase.nativeCycle - -Internal per-scan engine: acquire context, tick nonblocking I/O, update diagnostics, copy completed read data, process registered requests, acknowledge completion, select the next request, and publish statistics. Invalid native handles are discarded and reopened on a later scan. +Refresh scan lease, read completed results, process request completions, acknowledge result and select next transaction. OS I/O progression is performed by the worker. ```iecst -METHOD nativeCycle +METHOD processCycle VAR callResult : UDINT; END_VAR ``` -## ClientBase.nativeEnsureContext +## ClientBase.ensureContext -Acquire/configure/enable a native context if absent. Returns TRUE when a handle exists or initialization succeeds. A handle does not mean connected; check out_connected. Failure records a native error and closes partially initialized contexts. +Check 0.4 worker capability, allocate/configure/enable a context. A nonzero handle does not imply connectivity. Old components are rejected with error 259. ```iecst -METHOD PROTECTED nativeEnsureContext : BOOL +METHOD PROTECTED ensureContext : BOOL VAR callResult : UDINT; END_VAR ``` -## ClientBase.nativeRelease +## ClientBase.releaseContext -Close the native context, complete any active request as cancelled, clear selection, set registered requests offline and invalidate cached read data. Returns TRUE. It does not set in_enable FALSE; an enabled scanned client can reopen. +Close context, cancel active request, mark requests offline and invalidate cached read data. Returns TRUE. Does not disable in_enable; an enabled client can reopen. ```iecst -METHOD nativeRelease : BOOL +METHOD releaseContext : BOOL VAR requestIndex : DINT; END_VAR ``` -## ClientBase.nativeSelectNextRequest +## ClientBase.selectNextRequest -Select an active, unheld request that meets in_minDelay and was not just completed this scan. Lowest in_priorityCurrent wins; oldest sinceLastExec breaks ties. Positive priorities are decremented when the best selected priority is positive. The selected request is submitted immediately; zero priority is not a prerequisite for execution. +Select lowest current priority among eligible unheld requests; oldest age breaks ties. Minimum start delay applies. No zero-priority prerequisite. ```iecst -METHOD PROTECTED nativeSelectNextRequest +METHOD PROTECTED selectNextRequest VAR requestIndex : DINT; selectedRequestIndex : DINT; @@ -912,20 +860,20 @@ VAR END_VAR ``` -## ClientBase.nativeUpdateDiagnostics +## ClientBase.updateDiagnostics -Publish native connection state, last error, errno and reconnect count. States 3..5 count as connected. A connected-to-disconnected transition marks registered requests offline. +Publish transport state, errno, last result history and reconnect-attempt count. Mark requests offline on connection loss. ```iecst -METHOD PROTECTED nativeUpdateDiagnostics +METHOD PROTECTED updateDiagnostics ``` -## ClientBase.nativeUpdateStatistics +## ClientBase.updateStatistics -Publish IEC send/reply counters, registered statistics pointers and active-request aggregate online flags. Every elapsed window of at least 1000 ms publishes submitted-request count and successful transaction-duration sum, then restarts the window. +Rebuild the client summary from each registered RequestStats. State counts are restricted to active requests; historic counters include inactive ones. Publish the common monitoring string and compatibility counters. ```iecst -METHOD PROTECTED nativeUpdateStatistics +METHOD PROTECTED updateStatistics VAR requestIndex : DINT; END_VAR @@ -933,20 +881,20 @@ END_VAR ## ClientBase.processAllRequests -Internal loop over registered non-null requests, dispatching processRequest. Returns immediately for an empty or invalid registry. Usually invoked by nativeCycle rather than application code. +Dispatch all registered non-null request methods; guarded against empty/invalid count. ```iecst METHOD processAllRequests VAR_INPUT END_VAR VAR - requestIndex: DINT; // Request registry index. + requestIndex: DINT; END_VAR ``` ## ClientBase.recreateClient -Close/release the current context so the next enabled scan creates a fresh one with current settings. Cancels an active transaction and invalidates cached read data. Abstract on ClientBase; implemented by both concrete clients. +Release current context so next enabled scan reopens with current settings. Affects every request using that connection. ```iecst METHOD ABSTRACT recreateClient @@ -956,7 +904,7 @@ END_VAR ## ClientBase.release -Stop this client by setting in_enable FALSE, then release its native context. Returns the release result. To resume, set in_enable TRUE and keep calling the client. A call that supplies in_enable := TRUE every scan will re-enable it. +Disable client input and release its context. Passing in_enable TRUE on a subsequent call resumes it. ```iecst METHOD release : BOOL @@ -964,7 +912,7 @@ METHOD release : BOOL ## ClientBase.resetStats -Reset IEC send/reply/window counters and call each registered stats.resetStats. Does not clear native errno/reconnect counters or all per-request history. No transport recreation. +Clear success/error/send/reply/timeout/exception/offline counters and successful-duration history. Client version delegates to each request and refreshes totals. Connection state and data freshness are preserved. ```iecst METHOD resetStats @@ -975,7 +923,7 @@ END_VAR ## ClientBase.setAllRequestsOffline -Internal helper marking every registered request offline via its statistics FB. Returns TRUE after processing a nonempty valid registry; early return does not explicitly assign the return value. +Force registered request statistics offline. Used when connection disappears or context is released. ```iecst METHOD PROTECTED setAllRequestsOffline : BOOL @@ -988,7 +936,7 @@ END_VAR ## ClientBase.setupClient -Returns whether nativeHandle is nonzero. It does not create a context or guarantee transport connectivity. Normal FB scanning performs initialization. +Return whether a context handle exists. Does not open a context or prove connection success. ```iecst METHOD setupClient : BOOL @@ -998,7 +946,7 @@ END_VAR ## ClientBase.activeIndex -Get the current active request registry index, or -1 when idle. Setting a valid index sets that request stats.execute TRUE; it does not bypass native scheduler selection or directly submit a transaction. +Read active registry index or -1. Setting valid index flags that request execute; it does not bypass scheduler eligibility. ```iecst PROPERTY activeIndex : DINT @@ -1006,7 +954,7 @@ PROPERTY activeIndex : DINT ## ClientBase.activeReqCnt -Private count of registered requests whose stats.active is TRUE. Zero quantity/usable length requests are inactive. +Private count of active registered requests. ```iecst PROPERTY PRIVATE activeReqCnt : UINT @@ -1014,7 +962,7 @@ PROPERTY PRIVATE activeReqCnt : UINT ## ClientBase.allRequestsComplete -Private legacy completion check. TRUE only if none of the registered requests has execute, reqDone, reqBusy or reqError set. Empty registry returns FALSE. This is not a network completion pulse. +Legacy private flag check: FALSE for no requests or any execution/completion/error flag present. ```iecst {attribute 'symbol' := 'none'} @@ -1023,42 +971,41 @@ PROPERTY PRIVATE allRequestsComplete : BOOL ## ClientBase.onlineCnt -Count registered requests with stats.online TRUE. Includes any registered request with that flag; used for aggregate reporting. +Legacy count of registered online flags. Client.stats.onlineCount uses active requests only. ```iecst {attribute 'symbol' := 'none'} -PROPERTY onlineCnt : DINT//Count of requests that are online +PROPERTY onlineCnt : DINT ``` ## ClientBase.requestExecuting -Private property returning activeIndex <> -1. Represents selected/active request state maintained by the scheduler. +Private activeIndex <> -1 test. ```iecst {attribute 'symbol' := 'none'} -PROPERTY PRIVATE requestExecuting : BOOL //There is at least one request executing +PROPERTY PRIVATE requestExecuting : BOOL ``` ## ClientRTU.recreateClient -Close/release the current context so the next enabled scan creates a fresh one with current settings. Cancels an active transaction and invalidates cached read data. Abstract on ClientBase; implemented by both concrete clients. +Release current context so next enabled scan reopens with current settings. Affects every request using that connection. ```iecst METHOD recreateClient ``` -## ClientRTU.toString +## ClientBase.toString -ClientRTU monitoring property returning ClientStatus based on onlineCnt versus requestCnt. Reports allOk for an empty registry. RequestStats also has an internal STRING field of this name; these are different values. +Shared client string property returning stats.toString. Used by both concrete client monitoring displays. ```iecst -{attribute 'symbol' := 'none'} -PROPERTY toString : ClientStatus //Client request status noneOk = no requests working, someOk = some requests are working some are failing, allOk = all requests are working +PROPERTY toString : STRING(64) ``` ## ClientTCP.recreateClient -Close/release the current context so the next enabled scan creates a fresh one with current settings. Cancels an active transaction and invalidates cached read data. Abstract on ClientBase; implemented by both concrete clients. +Release current context so next enabled scan reopens with current settings. Affects every request using that connection. ```iecst METHOD recreateClient @@ -1066,7 +1013,7 @@ METHOD recreateClient ## ReqReadCoil.processRequest -Derived request dispatch: performs ReqReadCoil transaction using shared nativeProcess, validates copy bounds and transfers the configured slice. Called by client scheduler; application calls the request FB body to register/bind its data. +Concrete request adapter performs bounds validation and transfers its typed slice through processTransaction; abstract on RequestBase. Called by scheduler. ```iecst METHOD processRequest @@ -1074,7 +1021,7 @@ METHOD processRequest ## ReqReadCoil.recreateRequest -Clear this request nativeInFlight. If it is the client active request, recreate the whole client connection first. Returns TRUE. May affect other requests sharing the same connection; this is not an independent transport. +Clear request in-flight state; if it owns the active transaction, recreates the client connection. Returns TRUE. ```iecst METHOD recreateRequest : BOOL @@ -1082,7 +1029,7 @@ METHOD recreateRequest : BOOL ## ReqReadDiscrete.processRequest -Derived request dispatch: performs ReqReadDiscrete transaction using shared nativeProcess, validates copy bounds and transfers the configured slice. Called by client scheduler; application calls the request FB body to register/bind its data. +Concrete request adapter performs bounds validation and transfers its typed slice through processTransaction; abstract on RequestBase. Called by scheduler. ```iecst METHOD processRequest @@ -1090,7 +1037,7 @@ METHOD processRequest ## ReqReadDiscrete.recreateRequest -Clear this request nativeInFlight. If it is the client active request, recreate the whole client connection first. Returns TRUE. May affect other requests sharing the same connection; this is not an independent transport. +Clear request in-flight state; if it owns the active transaction, recreates the client connection. Returns TRUE. ```iecst METHOD recreateRequest : BOOL @@ -1098,7 +1045,7 @@ METHOD recreateRequest : BOOL ## ReqReadHolding.processRequest -Derived request dispatch: performs ReqReadHolding transaction using shared nativeProcess, validates copy bounds and transfers the configured slice. Called by client scheduler; application calls the request FB body to register/bind its data. +Concrete request adapter performs bounds validation and transfers its typed slice through processTransaction; abstract on RequestBase. Called by scheduler. ```iecst METHOD processRequest @@ -1106,7 +1053,7 @@ METHOD processRequest ## ReqReadHolding.recreateRequest -Clear this request nativeInFlight. If it is the client active request, recreate the whole client connection first. Returns TRUE. May affect other requests sharing the same connection; this is not an independent transport. +Clear request in-flight state; if it owns the active transaction, recreates the client connection. Returns TRUE. ```iecst METHOD recreateRequest : BOOL @@ -1114,7 +1061,7 @@ METHOD recreateRequest : BOOL ## ReqReadInput.processRequest -Derived request dispatch: performs ReqReadInput transaction using shared nativeProcess, validates copy bounds and transfers the configured slice. Called by client scheduler; application calls the request FB body to register/bind its data. +Concrete request adapter performs bounds validation and transfers its typed slice through processTransaction; abstract on RequestBase. Called by scheduler. ```iecst METHOD processRequest @@ -1122,7 +1069,7 @@ METHOD processRequest ## ReqReadInput.recreateRequest -Clear this request nativeInFlight. If it is the client active request, recreate the whole client connection first. Returns TRUE. May affect other requests sharing the same connection; this is not an independent transport. +Clear request in-flight state; if it owns the active transaction, recreates the client connection. Returns TRUE. ```iecst METHOD recreateRequest : BOOL @@ -1130,81 +1077,73 @@ METHOD recreateRequest : BOOL ## RequestBase.busy -Assign stats.reqBusy from input in. Internal request helper. Although declared BOOL, its implementation does not assign a return value; use the side effect rather than the return. +Set stats.reqBusy and return the assigned flag. ```iecst METHOD busy : BOOL VAR_INPUT + in: BOOL; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `in` | VAR_INPUT | `BOOL` | New state to assign. Normally used internally by completion helpers. | - ## RequestBase.done -Set stats.reqDone. When in is TRUE, restore in_priorityCurrent to in_prioritySetpoint for the next scheduler cycle. Successful completion counters are maintained by the stats property setter. +Set stats.reqDone and restore configured priority on success. ```iecst METHOD done VAR_INPUT - // Done state + + in: BOOL; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `in` | VAR_INPUT | `BOOL` | New state to assign. Normally used internally by completion helpers. | - ## RequestBase.err -Set stats.reqError and, on TRUE, record errorId/exceptionCode. Set next priority to 10 * MIN(consecutive errors,3)^2. Lower numeric priority wins, so this backs off repeated failures relative to lower-priority-number requests. +Set error identity before triggering counter update; apply 10/40/90 consecutive-error priority backoff. ```iecst METHOD err VAR_INPUT - // Error state + + in: BOOL; + errorId: Error; + exceptionCode: ExceptionCodes; END_VAR VAR - priorityMultiplier: INT; //Raise priority when not response + priorityMultiplier: INT; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `in` | VAR_INPUT | `BOOL` | New state to assign. Normally used internally by completion helpers. | -| `errorId` | VAR_INPUT | `Error` | Mapped IEC error supplied to the error helper. | -| `exceptionCode` | VAR_INPUT | `ExceptionCodes` | Modbus exception classification supplied to the error helper. | - ## RequestBase.FB_reinit -CODESYS lifecycle hook clearing native handles and in-flight state. ClientBase also clears the request registry for re-registration; RequestBase clears execution/completion flags; servers clear their handle. These hooks alone do not establish qualified online-change support. +Clear handles/registry or request execution state after instance reinitialization. Does not guarantee online-change correctness. ```iecst METHOD FB_reinit : BOOL ``` -## RequestBase.invalidateNativeData +## RequestBase.invalidateData -Mark cached data invalid and stale. Does not erase the application array. Called during client release; applications should gate use of cached values with validity and age flags. +Invalidate/stale cached read data without clearing its application array. ```iecst -METHOD PUBLIC invalidateNativeData +METHOD PUBLIC invalidateData ``` -## RequestBase.nativeFinish +## RequestBase.finishRequest -Complete a request from native result. Map errors, copy errno/write uncertainty, clear busy, update response/counters and successful-read freshness. Modbus exceptions count as replies and communication-online evidence. If enabled, uncertain writes are held pending writeNow(). +Map a completed result and write uncertainty, collect worker timing/read age, update reply/exception/timeout counters and completion flags. Successful reads become valid; errors retain old array values. ```iecst -METHOD nativeFinish +METHOD finishRequest VAR_INPUT + result : UDINT; END_VAR VAR @@ -1214,28 +1153,21 @@ VAR END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `result` | VAR_INPUT | `UDINT` | Native completion result; 0 success. | +## RequestBase.processTransaction -## RequestBase.nativeProcess - -Shared request processing called by derived processRequest. Completes native results or submits a selected request, sets its timeout and records busy/send counters. Updates data age and stale flags during ordinary processing. Completion branches can return before age refresh, so allow a subsequent scan for freshness diagnostics. +Submit the selected request or consume its buffered result. Record accepted-send counter and refresh data freshness. Normally invoked by the client through concrete processRequest. ```iecst -METHOD nativeProcess +METHOD processTransaction VAR_INPUT + functionCode : BYTE; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `functionCode` | VAR_INPUT | `BYTE` | Modbus FC; command 20 uses data bits, server push/pull use table ID. | - ## RequestBase.processRequest -Abstract internal request dispatch implemented by each concrete read/write block. +Concrete request adapter performs bounds validation and transfers its typed slice through processTransaction; abstract on RequestBase. Called by scheduler. ```iecst METHOD ABSTRACT processRequest @@ -1245,7 +1177,7 @@ END_VAR ## RequestBase.recreateRequest -Clear this request nativeInFlight. If it is the client active request, recreate the whole client connection first. Returns TRUE. May affect other requests sharing the same connection; this is not an independent transport. +Clear request in-flight state; if it owns the active transaction, recreates the client connection. Returns TRUE. ```iecst METHOD ABSTRACT recreateRequest : BOOL @@ -1253,16 +1185,15 @@ METHOD ABSTRACT recreateRequest : BOOL ## RequestBase.replyTimeout -Protected timeout property expressed internally in microseconds. Request in_timeout is milliseconds and takes precedence, then client TIME in_timeout, then a dataRate formula, then 1 second fallback. Submission rounds up to milliseconds and clamps to at least 1 ms. +Internal microsecond timeout calculation. Request milliseconds override client TIME, then baud/data-rate formula. C submission rounds to milliseconds. ```iecst -//Calculates the reply timout setting PROPERTY PROTECTED replyTimeout : UDINT ``` ## RequestStats.resetStats -Reset only doneCnt, doneCntRst, errorCnt and errorCntRst. Retains durations, offlineCnt, timeoutCnt and timestamps. +Clear success/error/send/reply/timeout/exception/offline counters and successful-duration history. Client version delegates to each request and refreshes totals. Connection state and data freshness are preserved. ```iecst METHOD resetStats @@ -1272,7 +1203,7 @@ END_VAR ## RequestStats.setClientOffline -Clear nativeResponseSeen and consecutive-success count, force online FALSE/offline TRUE. Does not clear data arrays. Declared BOOL but does not assign a return value. +Count a real online-to-offline transition, clear response evidence and force offline. Returns TRUE. ```iecst METHOD setClientOffline : BOOL @@ -1282,7 +1213,7 @@ END_VAR ## RequestStats.setToString -Private statistics helper formatting OK:/Er: into STRING(20). Declared BOOL but does not assign a return value. +Update OK:/Er: monitoring text, returning TRUE. ```iecst METHOD PRIVATE setToString : BOOL @@ -1292,7 +1223,7 @@ END_VAR ## RequestStats.execute -Statistics execution property. Setting TRUE starts the execution state only when reqDone and reqError are FALSE; otherwise the setter clears execution. Usually controlled by the scheduler. +Execution state property. Setter refuses execution while completion/error flags remain set. ```iecst PROPERTY execute : BOOL @@ -1300,7 +1231,7 @@ PROPERTY execute : BOOL ## RequestStats.reqBusy -Get/set the underlying request-busy flag. Normally managed by the native wrapper. The scan-held reqBusyLight provides a more visible monitoring indicator. +Underlying request busy flag; normal application code inspects rather than drives it. ```iecst PROPERTY reqBusy : BOOL @@ -1308,7 +1239,7 @@ PROPERTY reqBusy : BOOL ## RequestStats.reqDone -Get/set completion flag. When TRUE during execution, record duration, increment total/consecutive successes, clear consecutive errors and last error, clear execution and update the monitor string. Do not repeatedly drive this manually. +On successful executing completion, increment saturated success counters, clear errors and execution, update display. Worker supplies duration. ```iecst PROPERTY reqDone : BOOL @@ -1316,7 +1247,7 @@ PROPERTY reqDone : BOOL ## RequestStats.reqError -Get/set error flag. When TRUE during execution, increment total/consecutive errors, clear consecutive successes and execution. If lastExceptionCode is success, increment timeoutCnt and set TIMEOUT even for some non-timeout native errors. +On failed executing completion, increment saturated error counters and clear execution. Timeout/exception classification is handled by finishRequest. ```iecst PROPERTY reqError : BOOL @@ -1324,7 +1255,7 @@ PROPERTY reqError : BOOL ## ReqWriteCoil.processRequest -Derived request dispatch: performs ReqWriteCoil transaction using shared nativeProcess, validates copy bounds and transfers the configured slice. Called by client scheduler; application calls the request FB body to register/bind its data. +Concrete request adapter performs bounds validation and transfers its typed slice through processTransaction; abstract on RequestBase. Called by scheduler. ```iecst METHOD processRequest @@ -1332,7 +1263,7 @@ METHOD processRequest ## ReqWriteCoil.recreateRequest -Clear this request nativeInFlight. If it is the client active request, recreate the whole client connection first. Returns TRUE. May affect other requests sharing the same connection; this is not an independent transport. +Clear request in-flight state; if it owns the active transaction, recreates the client connection. Returns TRUE. ```iecst METHOD recreateRequest : BOOL @@ -1340,7 +1271,7 @@ METHOD recreateRequest : BOOL ## ReqWriteCoil.writeNow -Clear uncertain-write hold and out_writeUncertain, then set in_priorityCurrent to 0. Returns TRUE. Requests a prompt opportunity at the next scheduler selection; does not guarantee transmission in this call and does not bypass connection/minimum-delay requirements. +Release uncertain-write hold and set priority zero. Does not send synchronously or make the cyclic write block single-shot. ```iecst METHOD writeNow : BOOL @@ -1350,7 +1281,7 @@ END_VAR ## ReqWriteHolding.processRequest -Derived request dispatch: performs ReqWriteHolding transaction using shared nativeProcess, validates copy bounds and transfers the configured slice. Called by client scheduler; application calls the request FB body to register/bind its data. +Concrete request adapter performs bounds validation and transfers its typed slice through processTransaction; abstract on RequestBase. Called by scheduler. ```iecst METHOD processRequest @@ -1358,7 +1289,7 @@ METHOD processRequest ## ReqWriteHolding.recreateRequest -Clear this request nativeInFlight. If it is the client active request, recreate the whole client connection first. Returns TRUE. May affect other requests sharing the same connection; this is not an independent transport. +Clear request in-flight state; if it owns the active transaction, recreates the client connection. Returns TRUE. ```iecst METHOD recreateRequest : BOOL @@ -1366,7 +1297,7 @@ METHOD recreateRequest : BOOL ## ReqWriteHolding.writeNow -Clear uncertain-write hold and out_writeUncertain, then set in_priorityCurrent to 0. Returns TRUE. Requests a prompt opportunity at the next scheduler selection; does not guarantee transmission in this call and does not bypass connection/minimum-delay requirements. +Release uncertain-write hold and set priority zero. Does not send synchronously or make the cyclic write block single-shot. ```iecst METHOD writeNow : BOOL @@ -1376,22 +1307,19 @@ END_VAR ## ModbusSlave.FB_exit -CODESYS lifecycle hook releasing owned native resources. Applications should use release/recreateClient rather than call lifecycle hooks manually. Server hooks close the server context; the client hook calls nativeRelease. +Lifecycle resource release. Worker is joined by component exit; context hooks close their owned handle. Use public release for application shutdown. ```iecst METHOD FB_exit : BOOL VAR_INPUT + bInCopyCode : BOOL; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `bInCopyCode` | VAR_INPUT | `BOOL` | Runtime lifecycle copy-code indicator; current implementation does not branch on it. | - ## ModbusSlave.FB_reinit -CODESYS lifecycle hook clearing native handles and in-flight state. ClientBase also clears the request registry for re-registration; RequestBase clears execution/completion flags; servers clear their handle. These hooks alone do not establish qualified online-change support. +Clear handles/registry or request execution state after instance reinitialization. Does not guarantee online-change correctness. ```iecst METHOD FB_reinit : BOOL @@ -1399,37 +1327,31 @@ METHOD FB_reinit : BOOL ## ModbusSlave.serverCall -Private adapter for native server commands. Uses separate server status and WORD staging buffers. Returns the command result; 0 means command accepted, not necessarily that the listener has bound. +Private synchronous server/worker ABI adapter using server staging/status arrays. ```iecst METHOD PRIVATE serverCall : UDINT VAR_INPUT + command : UDINT; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `command` | VAR_INPUT | `UDINT` | Native command selector. Use wrappers in application code. | - ## SlaveTCP.FB_exit -CODESYS lifecycle hook releasing owned native resources. Applications should use release/recreateClient rather than call lifecycle hooks manually. Server hooks close the server context; the client hook calls nativeRelease. +Lifecycle resource release. Worker is joined by component exit; context hooks close their owned handle. Use public release for application shutdown. ```iecst METHOD FB_exit : BOOL VAR_INPUT + bInCopyCode : BOOL; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `bInCopyCode` | VAR_INPUT | `BOOL` | Runtime lifecycle copy-code indicator; current implementation does not branch on it. | - ## SlaveTCP.FB_reinit -CODESYS lifecycle hook clearing native handles and in-flight state. ClientBase also clears the request registry for re-registration; RequestBase clears execution/completion flags; servers clear their handle. These hooks alone do not establish qualified online-change support. +Clear handles/registry or request execution state after instance reinitialization. Does not guarantee online-change correctness. ```iecst METHOD FB_reinit : BOOL @@ -1437,32 +1359,29 @@ METHOD FB_reinit : BOOL ## SlaveTCP.serverCall -Private adapter for native server commands. Uses separate server status and WORD staging buffers. Returns the command result; 0 means command accepted, not necessarily that the listener has bound. +Private synchronous server/worker ABI adapter using server staging/status arrays. ```iecst METHOD PRIVATE serverCall : UDINT VAR_INPUT + command : UDINT; END_VAR ``` -| Member | Direction | Declaration | Meaning | -|---|---|---|---| -| `command` | VAR_INPUT | `UDINT` | Native command selector. Use wrappers in application code. | - ## activeIndex.Get -Get accessor for activeIndex; see the owning property reference for side effects. Runtime/property syntax normally invokes this accessor. +Get accessor for activeIndex; see the owning property description for semantics and side effects. ```iecst VAR - requestIndex: DINT; //Loop variable + requestIndex: DINT; END_VAR ``` ## activeIndex.Set -Set accessor for activeIndex; see the owning property reference for side effects. Runtime/property syntax normally invokes this accessor. +Set accessor for activeIndex; see the owning property description for semantics and side effects. ```iecst @@ -1470,18 +1389,18 @@ Set accessor for activeIndex; see the owning property reference for side effects ## activeReqCnt.Get -Get accessor for activeReqCnt; see the owning property reference for side effects. Runtime/property syntax normally invokes this accessor. +Get accessor for activeReqCnt; see the owning property description for semantics and side effects. ```iecst VAR - requestIndex : DINT; //Loop variable + requestIndex : DINT; cnt: UINT; END_VAR ``` ## allRequestsComplete.Get -Get accessor for allRequestsComplete; see the owning property reference for side effects. Runtime/property syntax normally invokes this accessor. +Get accessor for allRequestsComplete; see the owning property description for semantics and side effects. ```iecst VAR @@ -1491,18 +1410,18 @@ END_VAR ## onlineCnt.Get -Get accessor for onlineCnt; see the owning property reference for side effects. Runtime/property syntax normally invokes this accessor. +Get accessor for onlineCnt; see the owning property description for semantics and side effects. ```iecst VAR - requestIndex : DINT; //Loop variable + requestIndex : DINT; cnt: UINT; END_VAR ``` ## requestExecuting.Get -Get accessor for requestExecuting; see the owning property reference for side effects. Runtime/property syntax normally invokes this accessor. +Get accessor for requestExecuting; see the owning property description for semantics and side effects. ```iecst @@ -1510,7 +1429,7 @@ Get accessor for requestExecuting; see the owning property reference for side ef ## toString.Get -Get accessor for toString; see the owning property reference for side effects. Runtime/property syntax normally invokes this accessor. +Get accessor for toString; see the owning property description for semantics and side effects. ```iecst @@ -1518,7 +1437,7 @@ Get accessor for toString; see the owning property reference for side effects. R ## replyTimeout.Get -Get accessor for replyTimeout; see the owning property reference for side effects. Runtime/property syntax normally invokes this accessor. +Get accessor for replyTimeout; see the owning property description for semantics and side effects. ```iecst @@ -1526,7 +1445,7 @@ Get accessor for replyTimeout; see the owning property reference for side effect ## execute.Get -Get accessor for execute; see the owning property reference for side effects. Runtime/property syntax normally invokes this accessor. +Get accessor for execute; see the owning property description for semantics and side effects. ```iecst @@ -1534,7 +1453,7 @@ Get accessor for execute; see the owning property reference for side effects. Ru ## execute.Set -Set accessor for execute; see the owning property reference for side effects. Runtime/property syntax normally invokes this accessor. +Set accessor for execute; see the owning property description for semantics and side effects. ```iecst @@ -1542,7 +1461,7 @@ Set accessor for execute; see the owning property reference for side effects. Ru ## reqBusy.Get -Get accessor for reqBusy; see the owning property reference for side effects. Runtime/property syntax normally invokes this accessor. +Get accessor for reqBusy; see the owning property description for semantics and side effects. ```iecst @@ -1550,7 +1469,7 @@ Get accessor for reqBusy; see the owning property reference for side effects. Ru ## reqBusy.Set -Set accessor for reqBusy; see the owning property reference for side effects. Runtime/property syntax normally invokes this accessor. +Set accessor for reqBusy; see the owning property description for semantics and side effects. ```iecst @@ -1558,7 +1477,7 @@ Set accessor for reqBusy; see the owning property reference for side effects. Ru ## reqDone.Get -Get accessor for reqDone; see the owning property reference for side effects. Runtime/property syntax normally invokes this accessor. +Get accessor for reqDone; see the owning property description for semantics and side effects. ```iecst @@ -1566,7 +1485,7 @@ Get accessor for reqDone; see the owning property reference for side effects. Ru ## reqDone.Set -Set accessor for reqDone; see the owning property reference for side effects. Runtime/property syntax normally invokes this accessor. +Set accessor for reqDone; see the owning property description for semantics and side effects. ```iecst @@ -1574,7 +1493,7 @@ Set accessor for reqDone; see the owning property reference for side effects. Ru ## reqError.Get -Get accessor for reqError; see the owning property reference for side effects. Runtime/property syntax normally invokes this accessor. +Get accessor for reqError; see the owning property description for semantics and side effects. ```iecst @@ -1582,9 +1501,70 @@ Get accessor for reqError; see the owning property reference for side effects. R ## reqError.Set -Set accessor for reqError; see the owning property reference for side effects. Runtime/property syntax normally invokes this accessor. +Set accessor for reqError; see the owning property description for semantics and side effects. ```iecst ``` +## ModbusMaster.ClientStats + +Aggregate data structure: sums all registered request history, counts active communication states and calculates a weighted successful-duration mean. See USER-GUIDE.md for exact semantics. + +```iecst +TYPE ClientStats : +STRUCT + doneCnt : ULINT; // Sum over every registered request, including inactive requests. + errorCnt : ULINT; + sentCnt : ULINT; + replyCnt : ULINT; + exceptionCnt : ULINT; + timeoutCnt : ULINT; + offlineCnt : ULINT; // Sum of request online-to-offline transitions. + requestCount : UINT; + activeCount : UINT; + onlineCount : UINT; + offlineCount : UINT; + unknownCount : UINT; // Active, neither online nor offline yet. + busyCount : UINT; + staleCount : UINT; // Active read requests only. + heldWriteCount : UINT; + totalValidDuration : ULINT; // Milliseconds, all successful requests. + averageValidDuration : ULINT; // Weighted mean = totalValidDuration / doneCnt. + longestValidDuration : UDINT; // Maximum request maximum, milliseconds. + active : BOOL; + online : BOOL; // At least one active request and all active requests online. + offline : BOOL; // At least one active request and all active requests explicitly offline. + toString : STRING(64); +END_STRUCT +END_TYPE +``` + +## ModbusMaster.Worker + +Optional single global worker controller. CPU -1 restores inherited mask; nonnegative selects an OS CPU. Affinity error is explicit. Configure watchdog longer than expected scan gaps. + +```iecst +FUNCTION_BLOCK Worker +VAR_INPUT + in_cpu : DINT := -1; // -1 restores inherited affinity; >=0 selects an OS CPU ID. + in_interval : TIME := T#1MS; // Worker sleep 1..20 ms; not a hard real-time guarantee. + in_watchdog : TIME := T#2S; // 100 ms..60 s; closes contexts whose FB stops scanning. +END_VAR +VAR_OUTPUT + out_running : BOOL; + out_error : BOOL; + out_errorCode : UDINT; + out_osError : UDINT; + out_cpu : DINT := -1; + out_cycles : ULINT; +END_VAR +VAR + status : ARRAY[0..23] OF UDINT; + configured : BOOL; + lastCpu : DINT; + lastInterval : TIME; + lastWatchdog : TIME; + command : UDINT; +END_VAR +``` diff --git a/docs/BUILD-AND-USAGE.md b/docs/BUILD-AND-USAGE.md index 21009e0..1bce6b8 100644 --- a/docs/BUILD-AND-USAGE.md +++ b/docs/BUILD-AND-USAGE.md @@ -1,68 +1,61 @@ -# Build and deployment +# Build and upgrade to atMb 0.6 -## Build native C +## Build on ARM64 Linux -From the repository root on the target Linux machine: +From the repository root: ```sh sudo apt install build-essential m4 dos2unix python3 make -C component-source/component SDKDIR="$(realpath ExtensionSDK)" clean make -C component-source/component SDKDIR="$(realpath ExtensionSDK)" all -file component-source/component/out/libCmpAtomModbus.so -make -C component-source test +mkdir -p artifacts/arm64 +cp component-source/component/out/libCmpAtomModbus.so artifacts/arm64/ ``` -`component-source/component/` is the active SDK build directory. Its -`CmpAtomModbus.c` includes the protocol source files as one translation unit. -The removed `generated/` directory contained original unimplemented stubs; -it was not used by this makefile. Preserve the active `CmpAtomModbusItf.m4` and -`CmpFrame.c`; they define ABI registration and cleanup. +Run these commands on an ARM64 build machine. On an x86 laptop, ordinary gcc +builds x86 output; use the appropriate cross-compiler instead. -The makefile registers component version 0.3.0.0. The supplied external -`atMbCall_cext` API retains signature 0x709EE65B and API version 0.2.0.0. -Do not change the external parameter layout or regenerate it just because the -library title/version was edited. Full SDK/M4 builds must be checked on the target. +## Cross-compile ARMv7 hard-float on the laptop -## Update the CODESYS library +```sh +sudo apt install gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf make m4 dos2unix +make -C component-source/component SDKDIR="$(realpath ExtensionSDK)" clean +make -C component-source/component SDKDIR="$(realpath ExtensionSDK)" CC="arm-linux-gnueabihf-gcc -march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=hard" all +mkdir -p artifacts/armv7hf +cp component-source/component/out/libCmpAtomModbus.so artifacts/armv7hf/ +arm-linux-gnueabihf-readelf --version-info artifacts/armv7hf/libCmpAtomModbus.so +``` -1. Back up `library/CmpAtomModbus.library` and open it in CODESYS. -2. Retain Project Information title `CmpAtomModbus` and namespace `atMb`. -3. Retain the Standard and SysTime references required by the IEC code. -4. In its C Implemented Library object, update the native binary for the exact - device target with the newly built `libCmpAtomModbus.so`. Use the component - naming/settings already present in the working project. -5. Compile the library, save/install it in Library Repository and update application - references. Test with a full download for native component changes. +The makefile supplies `-pthread` during compilation/linking. Always clean between +architectures. Check `file`, ELF attributes, NEEDED libraries and GLIBC symbol +requirements against the target. Adding pthread can change the required glibc +version; the previously checked binaries do not establish compatibility of this build. +The WL2000 runtime was ARMv7/VFPv3-D16 hard-float. The UC20 OS was ARM64/glibc 2.39; +its runtime architecture/component support still needs target verification. -The working `.library` is preserved unchanged in this cleanup. Updating a separate -`.so` file does not automatically update the binary embedded inside the library. -The standalone artifact is retained for deployment/build comparison; rebuilding it -does not imply it is identical to the currently embedded implementation. +## CODESYS -## Runtime deployment +1. Keep the prior working `.library` and PLC application for rollback. +2. Create a fresh library project named `CmpAtomModbus.library` and import + `library/atMb-Threaded.export`. Importing into an old project does not delete + renamed/removed objects; a fresh project avoids duplicate old methods. +3. Preserve Project Information title `CmpAtomModbus`, namespace `atMb`, and + Standard/SysTime references. Verify library version 0.6.0.0 in Project Information. +4. Add a C Implemented Library object and attach the **new 0.4.0.0** component for + each exact target device. Do not attach files from `backup/`. +5. Compile, install the library and update application references. Rename application + diagnostic accesses using `RENAMED-MEMBERS.json`. `ClientTCP`/`ClientRTU` and + request function-block names remain the same. +6. Use a full download and verify the runtime loads component 0.4.0.0. If manually + preloading an old component, remove/replace that preload during maintenance so + it does not conflict with the embedded component. +7. Start on a bench with the default worker settings; verify unplug/replug, device + reboot, disabled client and PLC stop/start behavior before deployment. -Use the established embedded-library deployment or a manual bench installation; -avoid loading two versions of the same component simultaneously. The earlier Pi -setup manually loaded `/usr/lib/libCmpAtomModbus.so` with -`Component.1=CmpAtomModbus` under `[ComponentManager]` in -`/etc/CODESYSControl_User.cfg`. When switching to embedded deployment, remove only -that manual preload entry during maintenance and restart the runtime as required. +The C ABI layout/signature is unchanged (0x709EE65B, API 0.2.0.0). Component version +is 0.4.0.0. Command 41 capability checking prevents this IEC export operating with +an old component; it does not eliminate target/compiler compatibility checks. -For a manual installation, stop the runtime before replacing its component and -restart afterwards. Check `/var/log/codesyscontrol.log` for the loaded component -and version; `/var/opt/codesys/codesyscontrol.log` was stale on the development Pi. -No deployment or runtime restart is performed by this cleanup. - -## Different Linux targets - -Build natively on the desired CPU or use a matching cross-toolchain. ARM64, -ARM32 and x86-64 outputs are distinct. SDK, CODESYS runtime interface and libc -compatibility also matter. A successful host protocol test does not validate native -component loading into every runtime version. RTU support uses Linux termios/ioctl; -verify serial permissions, driver behavior and physical timing on the target. - -## Further documentation - -- [User guide](USER-GUIDE.md): examples, polling, RTU, slave, timeout and recovery. -- [API reference](API-REFERENCE.md): all objects from the last supplied working export. -- [Validation](VALIDATION.md): exact scope of checks performed. +Host validation: `make -C component-source test`. Host frame/function compilation +used the supplied SDK and generated headers; a fresh SDK/M4 build and the IDE +compile were not available in the execution environment. diff --git a/docs/CLEANUP.md b/docs/CLEANUP.md index 9d2a035..363ab9b 100644 --- a/docs/CLEANUP.md +++ b/docs/CLEANUP.md @@ -1,25 +1,11 @@ -# Repository cleanup +# Migration housekeeping -Removed unused files: +The prior working library, export and ARM binaries are retained under `backup/`. +They are not the threaded release. New source is imported from `atMb-Threaded.export` +and rebuilt in the IDE with a newly built 0.4 C component. Previous IDE caches, +editor lock/options files and C build outputs were omitted from this package. +Git metadata is retained; no commit or push was made. -- `component-source/generated/`: original empty C stubs; active makefile builds - from `component-source/component/`. -- Both `CmpAtomModbus_library.precompilecache` files: local CODESYS caches, - regenerated by the IDE; older duplicate was under artifacts. -- `docs/document_export.py`: depended on an export already absent from this ZIP. -- `docs/DOCUMENTATION-VALIDATION.md`: report for an export absent from this ZIP; - current validation is consolidated in `VALIDATION.md`. - -Rewrote README/build instructions to use the actual working `.library`, removing -references to deleted exports, migration scripts and first-time Git initialization. -Retained API/usage details and stated that their baseline is the last supplied export. -Updated ignore rules and file checksums. - -Preserved exact bytes of the `.library`, ARM64 `.so`, active C sources, SDK, tests -and all existing Git metadata. Git changes are not staged, committed or pushed. -Old files remain recoverable from Git history where previously committed. - -To apply to an existing checkout, replacing files alone will not remove obsolete -files. Prefer extracting this full snapshot into a separate directory and reviewing -its existing Git status; alternatively remove the listed obsolete paths explicitly -while applying updated files, keeping your checkout's own `.git` directory. +`RENAMED-MEMBERS.json` is the exact old-to-new mapping for application changes. +The previous transport prefix is removed from active IEC code and comments. +Public request output is now out_errorCode; client output is out_lastErrorCode. diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index c0eb333..8bc6822 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -1,220 +1,190 @@ -# atMb usage and behavior +# atMb 0.4 user guide -This guide describes the last supplied working IEC export, including `ClientTCP`. -The current repository stores the working file at `library/CmpAtomModbus.library`. -Use that file in CODESYS. No IEC or C implementation is changed by repository cleanup. -The binary library format was not decoded to reverify its member declarations; -when changing IEC code, update this guide against a fresh CODESYS export. +## Thread and PLC calls -## Choosing the block +One C worker progresses all TCP/RTU client transactions and TCP slave connections. +It owns no pointers into IEC memory: request values, replies and slave tables live +in fixed C storage. The external call copies buffers synchronously and serializes +state access using a priority-inheritance engine mutex. No network wait or sleep +is performed while that mutex is held. Serial driver calls can still take time; +this is not a hard real-time or lock-free PLC interface. -| Application | Block | Data | -|---|---|---| -| TCP master/client | `atMb.ClientTCP` | IPv4 byte array and TCP port | -| RTU master/client | `atMb.ClientRTU` | Linux serial device and serial settings | -| Read coils, FC1 | `atMb.ReqReadCoil` | BOOL array | -| Read discrete inputs, FC2 | `atMb.ReqReadDiscrete` | BOOL array | -| Read holding registers, FC3 | `atMb.ReqReadHolding` | UINT array | -| Read input registers, FC4 | `atMb.ReqReadInput` | UINT array | -| Write coils, FC15 | `atMb.ReqWriteCoil` | BOOL array | -| Write holding registers, FC16 | `atMb.ReqWriteHolding` | UINT array | -| TCP slave/server | `atMb.SlaveTCP` or `atMb.ModbusSlave` | Four memory arrays | +Continue calling each client/request and slave every PLC scan. Client request +selection is still performed by the IEC scheduler. A transaction can finish while +the PLC is not scanning, and its result waits for the next scan; the worker does +not independently cycle through the PLC request registry. TCP slaves respond from +the last copied memory snapshot between scans. -The two slave blocks have identical code/interfaces. They do not provide RTU slave operation. `ClientBase` and `RequestBase` are abstract and are not instantiated directly. - -## TCP read example - -Declarations: +## Optional worker configuration and a separate core ```iecst VAR - mbClient : atMb.ClientTCP; - readRegisters : atMb.ReqReadHolding; - registers : ARRAY[0..9] OF UINT; - dataUsable : BOOL; + commWorker : atMb.Worker; END_VAR + +// Example only: CPU 2 must be online and permitted on this PLC. +commWorker(in_cpu := 2, in_interval := T#1MS, in_watchdog := T#2S); ``` -Cyclic body: +Use only one Worker configuration instance for the component. Clients/servers +start the worker automatically with defaults even without this FB. Default CPU -1 +uses the thread creator's inherited affinity mask; it is not a promise of all CPUs. +A nonnegative value pins the worker to that Linux logical CPU ID. CPU affinity does +not reserve an exclusive core or move the PLC task: choose a CPU outside the PLC +task's configured affinity where the runtime allows it. Changes affect this worker +only. Failure is visible in `out_errorCode` and `out_osError`; no silent fallback. -```iecst -mbClient( - in_ipAddress := atMb.To_IPAddress(192,168,50,10), - in_port := 502, - in_enable := TRUE, - in_connectTimeout := T#2S, - in_reconnectDelay := T#100MS, - in_timeout := T#500MS); +The worker uses normal OS scheduling and explicitly avoids inheriting a PLC task's +real-time priority. Sleep interval is 1..20 ms, default 1 ms; scheduling jitter can +make actual intervals longer. Very short reply deadlines must account for worker +interval and transaction framing. `out_cycles` helps monitor worker progress. -readRegisters( - client := mbClient, - in_unitId := 1, - in_startAddr := 0, - in_quantity := 10, - in_dataOffset := 0, - in_minDelay := T#100MS, - in_staleAfter := T#2S, - data := registers); +## Scan-loss watchdog -dataUsable := readRegisters.out_validData AND NOT readRegisters.out_stale; -``` +Each client/server refresh call renews its own lease; another client's scans do +not keep a stopped client alive. Default expiry is 2 s, configurable 100 ms..60 s. +If scanning stops, the worker closes that context's connection/listener. In-flight +requests finish as cancelled; potentially transmitted writes retain uncertainty. +Handles/storage remain allocated for recovery. The next client/server refresh +resumes connection attempts using the same context. Explicit release frees it. -Call the client and each request every scan from one IEC task. On the first scan the client initializes its native mode, and the request registers afterwards; polling can begin on a subsequent scan. The request body binds the current array and usable count; the client dispatches the request method. No separate execute trigger is required for cyclic polling. +Choose a watchdog longer than the worst expected PLC scan gap. This bounds how +long a stopped PLC's server can respond with its last memory snapshot; it is not +an immediate CODESYS application-stop hook. Component exit stops and joins the +worker before freeing connections. Repeated thread shutdown/start is tested. -Keep instances and arrays stable. Avoid temporarily allocated request objects or sharing native contexts across tasks. Multiple requests can share a client; they are serialized. The supplied C implementation has 16 client contexts shared between TCP/RTU, 100 registrations per IEC client, and one in-flight transaction per client. - -## Addresses and lengths - -Protocol addresses are zero-based: a manual describing its first holding register as 40001 often maps to `in_startAddr := 0`. Confirm the device's mapping; do not pass 40001 automatically. - -`in_dataOffset` is the actual array index. With `ARRAY[1..10]`, use 1 to access the first element. `in_quantity` is clamped to the remaining array capacity rather than always rejected. Check `stats.reqLength` for the effective count. - -Limits per transaction: FC1/2 2000 bits, FC3/4 125 registers, FC15 1968 coils, FC16 123 registers. A one-element write still uses FC15/16 in the public write blocks. Register values are unsigned 16-bit words; converting floats, signed values or multi-register values and selecting word order are application responsibilities. - -## RTU example - -Use the same requests with this client: +## TCP read ```iecst VAR - serialClient : atMb.ClientRTU; - readRegisters : atMb.ReqReadHolding; - registers : ARRAY[0..9] OF UINT; + mb : atMb.ClientTCP; + readHolding : atMb.ReqReadHolding; + values : ARRAY[0..9] OF UINT; END_VAR + +mb(in_ipAddress := atMb.To_IPAddress(192,168,50,10), in_port := 502, + in_enable := TRUE, in_timeout := T#500MS, in_reconnectDelay := T#100MS); +readHolding(client := mb, in_unitId := 1, in_startAddr := 0, + in_quantity := 10, in_dataOffset := 0, data := values); ``` -```iecst -serialClient( - in_device := '/dev/ttyUSB0', - in_baudRate := 9600, - in_parity := 0, - in_dataBits := 8, - in_stopBits := 1, - in_rs485 := FALSE, - in_enable := TRUE, - in_timeout := T#500MS, - in_reconnectDelay := T#100MS); +Keep one IEC task as owner of the related FB instances and arrays. Call the client +before request registration on startup; polling starts after registration. The C +entry point is serialized but that does not make concurrent IEC FB access safe. +TCP addresses are four BYTEs; slave bind addresses are packed DWORDs. -readRegisters(client := serialClient, in_unitId := 1, - in_startAddr := 0, in_quantity := 10, in_dataOffset := 0, - data := registers); -``` +RTU uses `ClientRTU(in_device := '/dev/ttyUSB0', in_baudRate := 9600, +in_parity := 0, in_dataBits := 8, in_stopBits := 1, in_rs485 := FALSE)` and the same +request FBs. RTU IDs must be 1..247. The runtime account needs serial permissions. +Kernel RS485 direction control requires a supported driver. USB automatic-direction +adapters generally use FALSE. Supported baud rates: 1200,2400,4800,9600,19200,38400, +57600,115200. Physical timing/driver validation remains necessary. -Set RTU unit ID explicitly to 1..247; the inherited request default is 0 and broadcast is unsupported. The Linux runtime account needs serial device permissions. Use a stable device path where available. `in_rs485` requests kernel-managed direction control and requires driver support; USB adapters that control direction automatically generally use FALSE. Serial-port configuration must match the device. +## Requests, data and priorities -The supplied native source supports 1200/2400/4800/9600/19200/38400/57600/115200 baud, 8 bits, parity none/odd/even and one/two stop bits. Timing uses nonblocking I/O and scan-driven millisecond deadlines; physical adapter timing must be verified on the target hardware. +| Block | Function | Array | Maximum quantity | +|---|---|---|---| +| ReqReadCoil | 1 | BOOL | 2000 | +| ReqReadDiscrete | 2 | BOOL | 2000 | +| ReqReadHolding | 3 | UINT | 125 | +| ReqReadInput | 4 | UINT | 125 | +| ReqWriteCoil | 15 | BOOL | 1968 | +| ReqWriteHolding | 16 | UINT | 123 | -## Polling and priority +Protocol addresses are zero-based; `in_dataOffset` is the actual array index, not +a relative offset. Effective length is MIN(quantity, remaining array capacity). +Zero length disables a request and now retains its statistics history. Arrays and +FB instances must stay valid. There are 16 shared TCP/RTU client slots, 100 IEC +requests per client, one in-flight transaction per client and four TCP server slots. -Requests become active when their effective length is nonzero. They register automatically and poll while the client is enabled. `in_quantity := 0` makes a registered request inactive; it does not remove it from the registry. +Lower `in_priorityCurrent` wins; oldest start age breaks ties. A request need not +reach zero to run. Success restores `in_prioritySetpoint`; errors use 10/40/90 +priority backoff. `in_minDelay` is start-to-start eligibility, not a fixed period. -`in_minDelay` controls eligibility by age since the previous start. It is not a guaranteed polling period. Lowest `in_priorityCurrent` wins, and age breaks equal-priority ties. On success, current priority is restored to `in_prioritySetpoint`. On error it becomes `10 * MIN(consecutive errors,3)^2`: 10, 40, then 90. The current scheduler does not require priority to reach zero before submitting. Avoid overwriting current priority every scan unless that is intended. +Successful reads update the destination slice. Errors retain previous data; use +`out_validData AND NOT out_stale`. Data age includes time the completed reply waited +for a PLC scan. Client release invalidates cached data but does not erase arrays. +An exception reply indicates communication, but does not produce valid read data. -## Writes and uncertainty - -Write arrays are copied at submission, so changes made afterwards affect a later transaction. An active write request is cyclic, just like a read; `writeNow()` is a priority/hold-release operation, not a single-shot write API. - -When a connection fails after a write may have been transmitted, `out_writeUncertain` means the target could already have applied it. With `in_holdUncertainWrites := FALSE`, normal scheduling can retry. With TRUE, that write request is held until its `writeNow()` is called. Other eligible requests can continue. - -```iecst -// On an application-controlled event, after preparing the source array: -writeRegisters.writeNow(); -``` - -This clears the hold/uncertain output and sets priority 0. The client must still be enabled/connected, the request active and its minimum-delay condition satisfied. Confirm target behavior before retrying commands whose effects should occur only once. +Writes snapshot their source slice at submission. The worker never automatically +replays a request. The IEC scheduler still polls write FBs cyclically. Set client +`in_holdUncertainWrites := TRUE` to pause an uncertain write after a failure; +`writeNow()` clears the hold and sets priority zero once the application decides +retrying is appropriate. It does not bypass min-delay/connection conditions. ## Timeouts and reconnects -Reply timeout precedence: +Request `in_timeout` is numeric milliseconds (SYSTIME); client `in_timeout` is TIME. +Nonzero request timeout wins, then client timeout, then the existing baud/data-rate +formula. TCP's calculated default is about 38 ms, so an explicit timeout is clearer. +The C worker uses CLOCK_MONOTONIC for deadlines, independent of PLC-provided time. -1. Request `in_timeout`: SYSTIME numeric milliseconds, when nonzero. -2. Client `in_timeout`: TIME, when nonzero. -3. Calculated timeout using client dataRate. -4. A one-second fallback if dataRate is zero. +`in_reconnectDelay` defaults to 100 ms. Recovery proceeds without PLC ticks while +the scan lease is valid. Actual downtime includes failure detection, retry delay, +worker scheduling and remote-device readiness. Idle TCP disconnects are detected; +there is no zero-downtime guarantee. TCP_NODELAY reduces small-message coalescing. -The internal property calculates microseconds; submission rounds up to milliseconds with a minimum of 1 ms. TCP defaults its dataRate hint to 105200; the calculated timeout is approximately 38 ms. RTU uses its baud rate; at 9600 the formula gives approximately 411 ms. Explicit client/request timeouts make behavior easier to control. +`recreateClient()` closes/recreates a context and cancels any active request. +`release()` also sets `in_enable := FALSE`; a subsequent call explicitly passing +TRUE re-enables it. Closing a context clears C lifetime counters. -`in_connectTimeout` is the TCP connection deadline, separate from reply timeout. `in_reconnectDelay` defaults to 100 ms. It is the recovery wait after detecting failure, not a guarantee of recovery within 100 ms: scan time, connection timeout, reply timeout, OS behavior and target readiness also matter. +## Client statistics -Endpoint/configuration changes release and recreate the native context. `recreateClient()` forces this process; it does not permanently disable the client. `release()` also sets `in_enable := FALSE`. To restart, set TRUE and continue scanning. Supplying TRUE as a call parameter every scan will override that disabled input on the next call. +Both clients expose `stats : ClientStats` and a public string property `toString`. +The monitor display and `stats.toString` use `OK:/Er:`. For example, +requests with 10/2 and 7/1 completions/errors show `OK:17/Er:3` on the client. -## Data quality and statistics - -| Diagnostic | Meaning | +| Client member | Aggregation | |---|---| -| Client `out_connected` | Native transport is ready/sending/receiving | -| Request `stats.online` | Valid response evidence, including Modbus exceptions | -| Request `out_validData` | A successful read has populated data since invalidation | -| Request `out_stale` | No valid data, or last successful read is older than threshold | -| Request `out_dataAgeMs` | Age of successful read data | -| Request `out_nativeError` | Last native request result | -| Request `stats.lastErrorId` | Mapped IEC error classification | -| Request `stats.lastExceptionCode` | Modbus exception or local statistics classification | -| `out_socketError` | OS errno snapshot for socket/serial failure | +| doneCnt / errorCnt | Sum of registered request successes/failures | +| sentCnt / replyCnt | Sum of accepted submissions / normal or exception replies | +| exceptionCnt / timeoutCnt | Sum of exception replies / result 5 or 6 timeouts | +| offlineCnt | Sum of request online-to-offline transitions | +| requestCount | Registered requests, including disabled ones | +| activeCount | Nonzero effective-length requests | +| onlineCount / offlineCount / unknownCount | Active request communication state | +| busyCount | Active requests executing | +| staleCount | Active read requests with stale data | +| heldWriteCount | Active write requests paused after uncertain delivery | +| totalValidDuration | Sum of successful transaction duration totals, ms | +| averageValidDuration | Weighted mean: totalValidDuration / doneCnt, integer ms | +| longestValidDuration | Maximum successful duration across requests | +| online / offline | Nonempty active set, all online / all explicitly offline | -These represent different conditions. Exception replies can make communication online while the read fails. Read failures retain old values and may retain `out_validData`; use freshness as well. Releasing the client marks cached data invalid but does not erase arrays. Some completion paths return before updating age, so freshness outputs can refresh on the following scan. +Historical counters include registered inactive requests; state counts include +only active requests. An empty client is neither online nor offline. Aggregate +counters are ULINT. Per-request counters retain their existing types and saturate +rather than wrap; request offline counts saturate at UINT max. Successful duration +accumulation stops when the associated success counter saturates to keep the mean +consistent. There is no misleading aggregate of consecutive-error streaks or +unrelated start intervals. -`reqDone`/`reqError` are internal completion flags; the light outputs use a 100 ms off-delay for monitoring. `timeoutCnt` currently counts non-exception errors that statistics classify as TIMEOUT, not only native reply deadlines. Prefer native result 6 for a precise reply timeout. +`numMsgSent`/`numMsgReply` remain as saturated UDINT mirrors of aggregate totals. +The one-second window fields remain scan-published counts/duration totals. +Request `lastValidDuration` now measures C submit-to-completion, excluding the PLC +pickup delay. `resetStats()` resets totals, duration history, timeout and offline +counts consistently, while retaining connection state and data freshness. Resetting +during a transaction allows its eventual completion to be counted after reset. -Client `resetStats()` resets IEC send/reply/window counters and each request's four success/error counters. Request `stats.resetStats()` resets only `doneCnt`, `doneCntRst`, `errorCnt`, `errorCntRst`. Durations, timestamps, offline count, timeout count and native reconnect/errno history are retained. `in_resetStats` is consumed and cleared by the client; providing TRUE every scan keeps resetting it. +`out_errorCode` is the request result; `out_lastErrorCode` is the client's last +engine error. OS errno is `out_socketError`. Historical last errors may persist +after recovery. Result 260 means worker/affinity setup failed. A non-timeout failure +no longer increments timeoutCnt. See the API reference for all declarations. -## TCP slave example +## TCP slave memory exchange -```iecst -VAR - server : atMb.SlaveTCP; - coils : ARRAY[0..15] OF BOOL; - discreteInputs : ARRAY[0..15] OF BOOL; - holding : ARRAY[0..31] OF UINT; - inputs : ARRAY[0..31] OF UINT; -END_VAR -``` +`SlaveTCP` and `ModbusSlave` provide the same TCP server behavior. Supply four arrays: +`aCoilsMemory`, `aDiscreteInputsMemory`, `aHoldingRegistersMemory`, `aInputRegistersMemory`. +Their lower/upper bounds are Modbus addresses within 0..65535; max 2000 per table. +FC1/2/3/4 read; FC5/6/15/16 write coils/holding registers. Bind IP 0 means all +interfaces; interfaceName must remain empty. Up to eight peers per server. -```iecst -// Publish application inputs before the server call. -inputs[0] := 123; -server(in_enable := TRUE, in_ipAddress := DWORD#0, port := 1502, - aCoilsMemory := coils, - aDiscreteInputsMemory := discreteInputs, - aHoldingRegistersMemory := holding, - aInputRegistersMemory := inputs); -// Consume network-written coils/holding values after this call. -``` +Remote changes are marked dirty in C. An old PLC push cannot overwrite dirty +values before pull acknowledges them. Multiple remote writes before a scan expose +the latest value, not an event history. After pull, a later application push may +intentionally override those values. Other tasks must not race the array exchange. +Table bounds/length changes require `in_reset := TRUE` to recreate the server; +a changed mapping is rejected rather than silently discarding pending writes. -Port 1502 is an example; select the application's port. Keep `interfaceName` empty. `in_ipAddress := 0` binds all interfaces. A specific bind IPv4 is packed into DWORD, e.g. `DWORD#16#C0A8321F` for 192.168.50.31. - -Each table has independent addresses. `ARRAY[100..109] OF UINT` maps addresses 100..109, not 0..9. All tables must use nonnegative bounds within 0..65535 and at most 2000 elements. The current API requires four arrays; a small placeholder can represent an otherwise unused table. - -The wrapper snapshots all four application tables into native memory, processes bounded network work, then copies the tables back. Network writes affect coils and holding registers. Avoid overwriting externally controlled values unconditionally in application code each scan unless that override is intentional. The supplied C engine supports four server contexts and eight peers per server. Server contexts are separate from the 16-client pool. - -`out_running` means the listener is bound; `out_connectedClients` indicates peers. `in_reset` is consumed and cleared; a constant TRUE recreates the server every scan. Listener bind failures retry while enabled. Counters reset with server context recreation. Some early-return paths leave prior outputs unchanged; interpret diagnostic outputs together. - -## Native result codes and client states - -| Result | Meaning | -|---|---| -| 0 | Success | -| 1 | Invalid argument | -| 2 | Native client busy | -| 3 | Client offline/not ready | -| 4 | Socket/serial OS error | -| 5 | TCP connection deadline | -| 6 | Reply deadline | -| 7 | Peer closed | -| 8 | Invalid protocol/frame/reply | -| 9 | Modbus exception response | -| 10 | Cancelled | -| 256 | Invalid/stale native handle | -| 257 | Native pool full | -| 258 | Insufficient/invalid buffer capacity | -| 259 | Unknown command | - -These are distinct from the sparse IEC `Error` enum. IEC mapping combines 4/7 into `TcpSocketError`, 1/258 into `RequestParameterError`; other unmapped results become `InternalError`. TCP names also classify RTU failures. - -States: 0 disabled, 1 retry wait, 2 connecting, 3 ready, 4 sending, 5 receiving. - -## Maintenance and scope - -The cleanup preserves the supplied working library and standalone native binary. Internal native fields are sometimes exposed as VAR_INPUT for compatibility; leave them under library control. Several legacy BOOL methods do not assign a return value; the API reference identifies them so callers rely on their side effects. - -Lifecycle hooks exist, but reliable online-change ownership and pointer relocation remain future work. Documentation adds no behavior changes or online-change guarantee. Use the API reference for every method/property, including internal accessors. +Online-change pointer ownership and physical hardware certification remain future +work. This release needs a full download and on-target validation. diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md index 1c1476d..a8e4037 100644 --- a/docs/VALIDATION.md +++ b/docs/VALIDATION.md @@ -1,14 +1,39 @@ -# Cleanup validation +# Validation and remaining qualification -- Working `.library`, standalone ARM64 `.so`, active C/component sources, tests, - SDK and all Git metadata compared byte-for-byte with the uploaded ZIP: unchanged. -- Removed precompile caches, unused generated stubs and broken generator script. -- README/build instructions no longer refer to absent migration exports/scripts. -- Strict host compilation and bridge tests passed after cleanup. -- Nine direct TCP and nine bridge TCP scenarios passed. -- RTU frame/write/error/recovery tests passed. -- TCP slave FC1/2/3/4/5/6/15/16, exception/address and recovery tests passed. +IEC library version: 0.6.0.0. C component version: 0.4.0.0. +External C ABI signature/API version are unchanged. -No CODESYS compile or full ARM64 SDK build was run. Physical RTU operation and -online-change behavior were not qualified by these checks. API documentation is -based on the last supplied working IEC export, not a decoded binary `.library`. +## Passed on the Linux build host + +- Strict C compilation with warnings treated as errors for the test targets. +- Bridge capacity, context pool, handle generation and shutdown tests. +- Nine direct TCP and nine bridge TCP protocol/error/recovery scenarios. +- RTU tests over real pseudo-terminals: fragmented frames, CRC errors, timeouts, + exceptions, register/coil writes, invalid write echoes and recovery. +- TCP slave FC1/2/3/4/5/6/15/16, address/exception handling and bind recovery. +- Worker TCP and RTU responses complete with a 100 ms deadline while PLC calls + are paused for 250 ms; results remain available for the next call. +- Worker duration/age reporting and rejection of an unacknowledged result overwrite. +- Slave service between scans and remote writes preserved against stale PLC pushes. +- Scan watchdog transport shutdown and resumption, including uncertain sent writes + without automatic worker replay; server watchdog listener restart. +- CPU affinity configuration/restoration, invalid configuration rejection, + concurrent status calls, and 20 worker shutdown/restart cycles. +- Worker tests repeated after priority-inheritance mutex/error-reporting changes. +- Full component host shared-object compile using the uploaded generated SDK headers; + ComponentEntry and atmbcall_cext exported. This is not a fresh M4/ARM SDK build. +- Updated IEC export parsed, object identifiers/references checked, basic structured + text block balance checked, common client toString verified, and old embedded + component removed. These checks do not substitute for the CODESYS compiler. + +## Required before deployment + +Rebuild the C component for each target with its compatible SDK/toolchain, check +its ELF architecture and GLIBC dependencies, import the export in CODESYS, compile, +embed/deploy the rebuilt component and perform a full download. No updated compiled +.library or ARM binary is supplied. Previous binaries are backups only. + +CODESYS IDE compilation, physical target/serial hardware testing, prolonged load +and disconnect testing, and online-change behavior were not qualified here. +Online change remains future work. Confirm watchdog and reconnect settings under +actual PLC task jitter and network failures before production use. diff --git a/library/CmpAtomModbus.library b/library/CmpAtomModbus.library index 369cffe..13ead7a 100644 Binary files a/library/CmpAtomModbus.library and b/library/CmpAtomModbus.library differ