Threaded version 0.7

This commit is contained in:
2026-10-06 23:50:02 -06:00
parent fe5dcbe715
commit 490c9bdc5b
21 changed files with 891 additions and 1010 deletions
+34 -51
View File
@@ -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:<successes>/Er:<errors>` 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.
+31 -20
View File
@@ -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
BIN
View File
Binary file not shown.
Binary file not shown.
+7 -6
View File
@@ -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:
@@ -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"
+5 -3
View File
@@ -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
+32 -8
View File
@@ -4,7 +4,7 @@
#include "atom_server.h"
#include <stdio.h>
#include <string.h>
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 || cap<q)) result=AB_BAD_CAPACITY;
else result=(uint32_t)(s->serial?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 || cap<s->c.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;i<AM_POOL_SIZE;i++) if(slots[i].used) {
@@ -94,3 +105,16 @@ void ab_shutdown(void) {
if(slots[i].generation==0xffffffu) slots[i].retired=1;
}
}
/* Called only by the worker while holding the shared engine mutex. */
void ab_pump(uint64_t now,uint32_t watchdog_ms) {
for(unsigned i=0;i<AM_POOL_SIZE;i++) if(slots[i].used) {
slot *s=&slots[i];
if(s->c.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);
}
+2 -2
View File
@@ -1,14 +1,14 @@
#ifndef ATOM_BRIDGE_H
#define ATOM_BRIDGE_H
#include <stdint.h>
/* 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,
+10 -7
View File
@@ -5,6 +5,7 @@
#include <poll.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/tcp.h>
#include <sys/ioctl.h>
#include <unistd.h>
#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;i<c->quantity;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(now<c->deadline_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);
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
}
+22 -10
View File
@@ -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;i<q;i++)s->values[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;i<q;i++){s->values[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;i<AS_POOL;i++)if(!as_slots[i].used && as_slots[i].generation<0xffffffu)break;
if(i==AS_POOL)return 257;
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=&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;j<AS_PEERS;j++){s->peers[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 || cap<q)result=258;
else if(cmd==AS_PUSH){
if(table<2){for(unsigned j=0;j<q;j++)if(data[j]>1){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;j<q;j++)if(!s->dirty[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;i<AS_POOL;i++)if(as_slots[i].used){as_stop(&as_slots[i]);as_slots[i].used=0;}}
void as_pump(uint64_t now,uint32_t watchdog_ms){
for(unsigned i=0;i<AS_POOL;i++)if(as_slots[i].used){
as_server *s=&as_slots[i];
if(s->enabled && now-s->heartbeat>=watchdog_ms){as_stop(s);s->enabled=0;s->suspended=1;}
as_tick(s,now);
}
}
+2
View File
@@ -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);
-114
View File
@@ -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('<H',crc(payload))
def ready(h):
end=time.monotonic()+1
while time.monotonic()<end:
assert call(5,h)==0
if status[1]==3:return
time.sleep(.001)
raise AssertionError(('not ready',list(status)))
def rtu_scenario(mode,f=3):
master,slave=pty.openpty();path=os.ttyname(slave);os.close(slave);os.set_blocking(master,False)
for i,b in enumerate(path.encode()):data[i]=b
assert call(20,ip=9600,unit=0,f=8,addr=1,q=len(path),ct=0)==0
h=status[0];assert call(9,h)==0;assert call(3,h)==0;ready(h)
for attempt in range(2 if mode in ['crc','timeout','echo'] else 1):
if f==16:data[0]=123;data[1]=456
if f==15:data[0]=1;data[1]=0
assert call(6,h,unit=1,f=f,addr=10,q=2)==0
received=b'';sent=False;fragments=[];deadline=time.monotonic()+1
while time.monotonic()<deadline:
assert call(5,h)==0
if select.select([master],[],[],0)[0]:
try:received+=os.read(master,256)
except OSError:pass
need=(9+received[6]) if len(received)>=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()<end:
assert call(34,h)==0
try:reply+=peer.recv(260)
except BlockingIOError:pass
if len(reply)>=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()
+504 -524
View File
File diff suppressed because it is too large Load Diff
+45 -52
View File
@@ -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.
+9 -23
View File
@@ -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.
+146 -176
View File
@@ -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:<doneCnt>/Er:<errorCnt>`. 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.
+37 -12
View File
@@ -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.
Binary file not shown.