Seems to be working great

This commit is contained in:
2026-10-07 01:03:18 -06:00
parent c39c226885
commit 739347c4da
26 changed files with 6290 additions and 7007 deletions
+10 -40
View File
@@ -1,43 +1,13 @@
# atMb 0.6 — threaded communication and client statistics # atMb 0.8 — online-change-focused synchronous release
This update moves TCP/RTU transaction progress and TCP server I/O into one shared This release removes the native worker thread and restores TCP, RTU and server
C worker thread. The worker also handles connection recovery between PLC scans. transport progress to the owning PLC call. The C layer retains no IEC pointers
The IEC task still selects/submits the next client request and exchanges slave and uses fixed storage only.
memory. One C worker serves the fixed pools; it is not a separate thread per device.
Start with [upgrade/build instructions](docs/BUILD-AND-USAGE.md), then read the Import `library/atMb-OnlineChange.export` into a fresh CODESYS library project.
[user guide](docs/USER-GUIDE.md) and [API reference](docs/API-REFERENCE.md). The IEC library version is **0.8.0.0** and the C component is **0.5.0.0**.
The previous threaded release remains under `backup/` for rollback/reference.
## Required upgrade `FB_reinit` closes native contexts before IEC instance memory is replaced,
clears request pointers and active-request state, and allows contexts to reopen
Rebuild the C component as **0.4.0.0**, import `library/atMb-Threaded.export` into on subsequent scans.
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.
## Changes
- 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.
## Files
- `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.
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.
+17 -20
View File
@@ -10,40 +10,37 @@ a8de018bfde8d10d3f281bb45de03c4b4f3cd28466eb0fa0981086bc4033a69c ExtensionSDK/i
814d06ca83d775f3bcb93f80859602b6c1debe9511b3d42ef337948bd2e2b5d0 ExtensionSDK/makefile 814d06ca83d775f3bcb93f80859602b6c1debe9511b3d42ef337948bd2e2b5d0 ExtensionSDK/makefile
4fc8013238169e924ee2eb5dd6c2b39c9d274f4c7c2219dc0b9a530687097519 ExtensionSDK/makefile_template 4fc8013238169e924ee2eb5dd6c2b39c9d274f4c7c2219dc0b9a530687097519 ExtensionSDK/makefile_template
aa7ccad11508ccbf4f9b24aa8db8f22392f9d723ae3af5cbfa67e38271cfbac3 ExtensionSDK/src/CmpFrame.c aa7ccad11508ccbf4f9b24aa8db8f22392f9d723ae3af5cbfa67e38271cfbac3 ExtensionSDK/src/CmpFrame.c
3a171751b48b339c731611044bdcb94a409d89568807026f6122ecba06e458fe README.md bf5b5518674a427f0a5c08f4484b07fc5fb92e2581469420f236358b5c2b6a07 README.md
2a58de4b824ac7c41e5a192c05f299a08f8a59c95d7c8046722498a073e2f48e artifacts/README.md 2a58de4b824ac7c41e5a192c05f299a08f8a59c95d7c8046722498a073e2f48e artifacts/README.md
62dd7f0b5d7efdb0e8ca6a13257581c9d2231162e3e6acc311d34f12948acff7 backup/README.md 62dd7f0b5d7efdb0e8ca6a13257581c9d2231162e3e6acc311d34f12948acff7 backup/README.md
07527262a8efcf9add5554cfcaed22cf927af63b985e431ab5341078a6864667 backup/artifacts/arm64/libCmpAtomModbus.so 07527262a8efcf9add5554cfcaed22cf927af63b985e431ab5341078a6864667 backup/artifacts/arm64/libCmpAtomModbus.so
bb39f2b586454b47ccd64f158e0a08f6f5d3947e98e667d9ce240b2ab472f8fa backup/artifacts/armv7hf/libCmpAtomModbus.so bb39f2b586454b47ccd64f158e0a08f6f5d3947e98e667d9ce240b2ab472f8fa backup/artifacts/armv7hf/libCmpAtomModbus.so
fb1dbd96af6bb0a39e760d0e3ef5eab5a3f5c461de66d5d96d3301adce3d821d backup/library/CmpAtomModbus.export fb1dbd96af6bb0a39e760d0e3ef5eab5a3f5c461de66d5d96d3301adce3d821d backup/library/CmpAtomModbus.export
c475ae30bc96965bff86a956d133ebdc7d95f38f6f2326c70ea1ecfbc80d6e4b backup/library/CmpAtomModbus.library c475ae30bc96965bff86a956d133ebdc7d95f38f6f2326c70ea1ecfbc80d6e4b backup/library/CmpAtomModbus.library
ec596402e5fc7a0bdc9fb3994143695a7457bda1077d0b6eee92944ba5798400 component-source/Makefile f50ee94893d4282e6ff125c1830cbea05c0d1f3680bd7ddadd2b4c79aa1edf14 component-source/Makefile
7b36f997e84cfea38246a3d197a72cd76f6c1b94902b934659e9b66a092aadb3 component-source/component/CmpAtomModbus.c d91cffa9751bca79f13ae80552d654519333536c894bb13b41a1182c179c53b7 component-source/component/CmpAtomModbus.c
30a8d7d5dfd372d171d4acdafd0b77cedca684e0b6e661ae1ab67e2627d35708 component-source/component/CmpAtomModbusItf.m4 30a8d7d5dfd372d171d4acdafd0b77cedca684e0b6e661ae1ab67e2627d35708 component-source/component/CmpAtomModbusItf.m4
513586eed183bcc53cdf008c483da03feff6b5cb0f3ed3eff17de1462def12c3 component-source/component/CmpFrame.c 513586eed183bcc53cdf008c483da03feff6b5cb0f3ed3eff17de1462def12c3 component-source/component/CmpFrame.c
cac2eeaad2fba34a0f5dddae57da11205fe98f52d2bc126125e5efe2c085220c component-source/component/makefile d891cd5dbaa6e7d891c42de74368ac482461986472b9d1f92af00d611289b1b7 component-source/component/makefile
bd63f5153b80f5a6ea3000879b83e54dae430f09f1253a2fb3f828c5ccfff2b0 component-source/src/atom_bridge.c ffb81617656709acb3b046a240c03df5ea0d8de9001fa631444b23b714f05036 component-source/src/atom_bridge.c
c0542adac77f60d5f3e7c14e755f07b341ba815b9c0243af4cec09c4844e4ab4 component-source/src/atom_bridge.h 7994895beac2337d1f81216d5971eafa335c950c2050aa6e7cdfc83984955155 component-source/src/atom_bridge.h
115271ae5673cd834fc196e594ff4c1ad0f2a71bb01dc36f713809adeda6efbf component-source/src/atom_mb.c 115271ae5673cd834fc196e594ff4c1ad0f2a71bb01dc36f713809adeda6efbf component-source/src/atom_mb.c
366c879743b51daed51bac909439fc91b908a39ce63ec1d5ef0a8de78cb80bb1 component-source/src/atom_mb.h 366c879743b51daed51bac909439fc91b908a39ce63ec1d5ef0a8de78cb80bb1 component-source/src/atom_mb.h
a2afdfd2f6f607f41464c3483b58d76452164019446986a9430b54cc0b9f6b5d component-source/src/atom_rtu.c a2afdfd2f6f607f41464c3483b58d76452164019446986a9430b54cc0b9f6b5d component-source/src/atom_rtu.c
565a5122de679b2e60990b1252f356e69316b51979485c3cf0101c6e32ac5a4a component-source/src/atom_rtu.h 565a5122de679b2e60990b1252f356e69316b51979485c3cf0101c6e32ac5a4a component-source/src/atom_rtu.h
b7fb2772d7b2967014af4651d7a75570aeda69d7c06c515c3548a8a8d2d671e3 component-source/src/atom_server.c 2555b12b0f1b8dc84847d2a34155f5d496fbb47804a7f27bf094458bed9e3949 component-source/src/atom_server.c
5e9e8e1d92f702af5901751da056e756fe9f988fc618360c42d9039619df183c component-source/src/atom_server.h 51f3b4a5779347e58b9993b3c2d8f180a73e0a70cb2a8b81bf9350aca8b42bad component-source/src/atom_server.h
853f95e1acf7dac8b75a63398ba139ae143744ba42ce211ca339c393637dafb7 component-source/src/atom_worker.c
d770393632f9d3d1acddf229f562148648792398a39d90851aa529d2923f8382 component-source/tests/bridge_driver.c d770393632f9d3d1acddf229f562148648792398a39d90851aa529d2923f8382 component-source/tests/bridge_driver.c
50bb7854c3a966c46a583fba951c1d3a605bf9d1739ac83685322d9a7b740c4d component-source/tests/driver.c 50bb7854c3a966c46a583fba951c1d3a605bf9d1739ac83685322d9a7b740c4d component-source/tests/driver.c
3af1a5f71d69bae2abda3de9dba31740ad254d7157d702847fb921fc2b64df3d component-source/tests/test_bridge.c 3af1a5f71d69bae2abda3de9dba31740ad254d7157d702847fb921fc2b64df3d component-source/tests/test_bridge.c
0cf8f8fefda6376cacf0c56729d24aaa76241e807b0dee0b36001b3ad3cea509 component-source/tests/test_tcp.py 0cf8f8fefda6376cacf0c56729d24aaa76241e807b0dee0b36001b3ad3cea509 component-source/tests/test_tcp.py
c79f2e6d005dac7fb62669c274ff0c82b78e0ac80ef83ee19a596e699fdad7b8 component-source/tests/test_transport.py 575adda751a14ef74dc515190da8f93c8c2b085a7e4388a9b5363cc442e7bdd8 component-source/tests/test_transport.py
76d04a651b2d487cffbd778ea0b0ab8a9efda4b964044615efcacd0233448da4 component-source/tests/test_worker.py e409f8f786c5d4fc6f91230dc2ad6baa1af3388ec74e1a13a6011634f36d430e docs/API-REFERENCE.md
92617cdf8d973b65e662d25ad1986275f770ff01d25823e82b9624b3b9fb7fd5 docs/API-REFERENCE.md 91e5a0a3597d214832ca2acf02a72bfe8678268fc7b88aba1cca4a9336087f56 docs/BUILD-AND-USAGE.md
b014c5caec618cb2863c5a8e3ee98041e0cc5d1da0b5c2933809b471c6170e74 docs/BUILD-AND-USAGE.md dc4b312826bba713d389d86cd35fd01d4ea223c3f084556f9ff2064532a4dcee docs/CLEANUP.md
27c9d27501196d9d4bbc28dd3da9f1ce3b4e16bd4fd0830d7cc1c85c681357b2 docs/CLEANUP.md
997ce9d0fe59de7a214c9692fc9341f85d83ada909bad399532697e2943f88e7 docs/IMPORT-FIX.md
d60f5881289ac66cf5f6e4a90c5c32beaded3426a385825fb3d1750ec758dee1 docs/RENAMED-MEMBERS.json d60f5881289ac66cf5f6e4a90c5c32beaded3426a385825fb3d1750ec758dee1 docs/RENAMED-MEMBERS.json
c44551b2bb75ae7e98fc33a95dad306f419e328ba0571c7277b238705356efe5 docs/USER-GUIDE.md 56fa44b409bb90c3271d162ef36f52644d17f49c2badf32f08c224ff26ced796 docs/USER-GUIDE.md
6bd8db7b034dfd48e52a1af5845c769ca6cbc72236522ddfdd905ae56b5b1f66 docs/VALIDATION.md ba57f003d81114f7b20dedbb0837259cacc8d6e0211308927a477a8609d9f031 docs/VALIDATION.md
fb39a21af08a33d070d186366eb9ccbabdf645c001bd2a91b24e12400775db72 docs/WORKER-ABI.md 7c566ea69d86ff9654f7eb100e3086b9139bb006a8323a5272adf1dd4a6e57d2 docs/WORKER-ABI.md
c5c5363714b6a8ebb3778efb69f4c4213aec7aeecf2b60a88637d1bd4d8ceeb3 library/atMb-Threaded-Review.st b0b0e76734802224de21cb985bf9554edd45fcf5ced4b9b911f9eecc99089002 library/atMb-OnlineChange-Review.st
24efab5b7fb402e84ad2e375c513e2ccf2ca809d9754b67374bd26c787ccd42e library/atMb-Threaded.export fac79d3281a13570e134a599953584a399a884d63fdb2ea32f801548fa3c3c64 library/atMb-OnlineChange.export
Binary file not shown.
Binary file not shown.
Binary file not shown.
+5 -6
View File
@@ -1,16 +1,15 @@
CC ?= cc CC ?= cc
CFLAGS = -std=c11 -Wall -Wextra -Werror -O2 -pthread CFLAGS = -std=c11 -Wall -Wextra -Werror -O2
.PHONY: test clean .PHONY: test clean
test: tests/driver tests/test_bridge tests/bridge_driver test: tests/driver tests/test_bridge tests/bridge_driver
./tests/test_bridge ./tests/test_bridge
python3 tests/test_tcp.py python3 tests/test_tcp.py
python3 tests/test_tcp.py bridge_driver python3 tests/test_tcp.py bridge_driver
python3 tests/test_transport.py 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_mb.c src/atom_mb.h src/atom_rtu.c src/atom_rtu.h src/atom_server.c src/atom_server.h
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_mb.c src/atom_rtu.c src/atom_server.c -o $@
$(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_mb.c src/atom_mb.h src/atom_rtu.c src/atom_rtu.h src/atom_server.c src/atom_server.h
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_mb.c src/atom_rtu.c src/atom_server.c -o $@
$(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 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 $@ $(CC) $(CFLAGS) -Isrc tests/driver.c src/atom_mb.c src/atom_rtu.c src/atom_server.c -o $@
clean: clean:
@@ -1,4 +1,3 @@
#define _GNU_SOURCE
#define _DEFAULT_SOURCE #define _DEFAULT_SOURCE
#include "../src/atom_bridge.h" #include "../src/atom_bridge.h"
#include "CmpStd.h" #include "CmpStd.h"
@@ -32,5 +31,3 @@ void CDECL CDECL_EXT atmbcall_cext(atmbcall_cext_struct *p)
#include "../src/atom_rtu.c" #include "../src/atom_rtu.c"
#include "../src/atom_server.c" #include "../src/atom_server.c"
#include "../src/atom_bridge.c" #include "../src/atom_bridge.c"
#include "../src/atom_worker.c"
+2 -4
View File
@@ -1,8 +1,8 @@
# Generated from supplied ExtensionSDK; build with target Linux toolchain. # Generated from supplied ExtensionSDK; build with target Linux toolchain.
SDKDIR ?= ../../ExtensionSDK SDKDIR ?= ../../ExtensionSDK
MAJORVER=0 MAJORVER=0
MINORVER=7 MINORVER=4
CMPVERSION=0x00040000 CMPVERSION=0x00050000
# Local development ID; check no other component uses this ID. # Local development ID; check no other component uses this ID.
CMPID=0x2001 CMPID=0x2001
DOS2UNIX=dos2unix DOS2UNIX=dos2unix
@@ -10,6 +10,4 @@ M4=m4
CC ?= gcc CC ?= gcc
# The SDK otherwise includes CmpFrame in its component identity wildcard. # The SDK otherwise includes CmpFrame in its component identity wildcard.
override CSRC := CmpAtomModbus.c override CSRC := CmpAtomModbus.c
CFLAGS += -pthread
LDLIBS += -pthread
include $(SDKDIR)/makefile include $(SDKDIR)/makefile
+15 -19
View File
@@ -4,7 +4,7 @@
#include "atom_server.h" #include "atom_server.h"
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
typedef struct { am_client c; ar_transport rtu; int serial; uint32_t generation; int used, retired, suspended; uint64_t heartbeat; } slot; typedef struct { am_client c; ar_transport rtu; int serial; uint32_t generation; int used, retired; } slot;
static slot slots[AM_POOL_SIZE]; static slot slots[AM_POOL_SIZE];
static uint32_t token(unsigned i) { return (slots[i].generation<<8)|(i+1u); } static uint32_t token(unsigned i) { return (slots[i].generation<<8)|(i+1u); }
static slot *lookup(uint32_t h) { static slot *lookup(uint32_t h) {
@@ -47,7 +47,7 @@ uint32_t ab_dispatch(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port,
text[q]=0; text[q]=0;
if(!ar_init(&s->c,&s->rtu,text,ip,unit,f,(uint8_t)addr,ct))return AM_ARGUMENT; 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; } else if(!am_init(&s->c,text,port)) return AM_ARGUMENT;
s->generation++; s->used=1; s->suspended=0; s->heartbeat=now; h=token(i); s->generation++; s->used=1; h=token(i);
snapshot(&s->c,h,status); return 0; snapshot(&s->c,h,status); return 0;
} }
s=lookup(h); s=lookup(h);
@@ -58,12 +58,9 @@ uint32_t ab_dispatch(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port,
/* Never recycle a generation after wrap: stale handles stay invalid. */ /* Never recycle a generation after wrap: stale handles stay invalid. */
if(s->generation==0xffffffu) s->retired=1; if(s->generation==0xffffffu) s->retired=1;
snapshot(&s->c,0,status); return 0; snapshot(&s->c,0,status); return 0;
case AB_ENABLE: s->heartbeat=now; s->suspended=0; am_enable(&s->c,1,now); break; case AB_ENABLE: 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_DISABLE: if(s->serial)ar_close(&s->c);else am_enable(&s->c,0,now); break;
case AB_TICK: case AB_TICK: if(s->serial)ar_tick(&s->c,&s->rtu,now);else am_tick(&s->c,now); break;
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_RECONNECT: if(s->serial)ar_reconnect(&s->c,&s->rtu,now);else am_reconnect(&s->c,now); break;
case AB_CONFIGURE: case AB_CONFIGURE:
if(s->c.enabled) result=AM_BUSY; if(s->c.enabled) result=AM_BUSY;
@@ -106,15 +103,14 @@ void ab_close_all(void) {
} }
} }
/* Called only by the worker while holding the shared engine mutex. */ /* Synchronous ABI entry point. All transport progress occurs on the caller's
void ab_pump(uint64_t now,uint32_t watchdog_ms) { * PLC task; no native worker thread or retained IEC pointer is used. */
for(unsigned i=0;i<AM_POOL_SIZE;i++) if(slots[i].used) { uint32_t ab_call(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port,uint8_t unit,uint8_t f,
slot *s=&slots[i]; uint16_t addr,uint16_t q,uint16_t *data,uint32_t cap,uint32_t *status,uint32_t sc,
if(s->c.enabled && now-s->heartbeat >= watchdog_ms) { uint64_t now,uint32_t ct,uint32_t rt,uint32_t retry) {
if(s->serial)ar_close(&s->c);else am_close(&s->c); return ab_dispatch(cmd,h,ip,port,unit,f,addr,q,data,cap,status,sc,now,ct,rt,retry);
s->suspended=1;
}
if(s->serial)ar_tick(&s->c,&s->rtu,now);else am_tick(&s->c,now);
}
as_pump(now,watchdog_ms);
} }
void ab_shutdown(void) { ab_close_all(); }
/* Called only by the worker while holding the shared engine mutex. */
+2 -2
View File
@@ -1,14 +1,14 @@
#ifndef ATOM_BRIDGE_H #ifndef ATOM_BRIDGE_H
#define ATOM_BRIDGE_H #define ATOM_BRIDGE_H
#include <stdint.h> #include <stdint.h>
/* Fixed pool; serialized calls and worker I/O. IEC instances still belong to one task. /* Fixed pool; transport progress is synchronous in the owning PLC task. IEC instances still belong to one task.
* Only synchronous pointer access; generation-checked handles, no heap. */ * Only synchronous pointer access; generation-checked handles, no heap. */
#define AM_STATUS_WORDS 24 #define AM_STATUS_WORDS 24
#define AM_POOL_SIZE 16 #define AM_POOL_SIZE 16
enum { AB_OPEN=1, AB_CLOSE, AB_ENABLE, AB_DISABLE, AB_TICK, AB_SUBMIT, 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 }; AB_COPY_READ, AB_RECONNECT, AB_CONFIGURE, AB_ACK, AB_REQUEST_TIMEOUT };
enum { AB_OPEN_RTU=20 }; enum { AB_OPEN_RTU=20 };
enum { AB_BAD_HANDLE=256, AB_POOL_FULL, AB_BAD_CAPACITY, AB_BAD_COMMAND, AB_WORKER_ERROR }; enum { AB_BAD_HANDLE=256, AB_POOL_FULL, AB_BAD_CAPACITY, AB_BAD_COMMAND };
uint32_t ab_call(uint32_t command,uint32_t handle,uint32_t ip,uint16_t port, 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, 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, uint16_t *data,uint32_t capacity,uint32_t *status,uint32_t status_capacity,
+5 -9
View File
@@ -114,7 +114,11 @@ uint32_t as_call(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port,uint8_t table
case AS_CLOSE:as_stop(s);s->used=0;as_snapshot(s,0,out);return 0; case AS_CLOSE:as_stop(s);s->used=0;as_snapshot(s,0,out);return 0;
case AS_ENABLE:s->enabled=1;s->suspended=0;s->heartbeat=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_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_TICK:
s->heartbeat=now;
if(s->suspended){s->suspended=0;s->enabled=1;s->retry_at=now;}
as_tick(s,now);
break;
case AS_PUSH:case AS_PULL: case AS_PUSH:case AS_PULL:
if(table>3 || !q || q>AS_ITEMS || (uint32_t)addr+q>65536u)result=AM_ARGUMENT; if(table>3 || !q || q>AS_ITEMS || (uint32_t)addr+q>65536u)result=AM_ARGUMENT;
else if(!data || cap<q)result=258; else if(!data || cap<q)result=258;
@@ -133,11 +137,3 @@ uint32_t as_call(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port,uint8_t table
as_snapshot(s,h,out);return result; 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_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,5 +5,3 @@ 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); 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); void as_shutdown(void);
#endif #endif
void as_pump(uint64_t now,uint32_t watchdog_ms);
-115
View File
@@ -1,115 +0,0 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "atom_bridge.h"
#include "atom_mb.h"
#include <pthread.h>
#include <sched.h>
#include <errno.h>
#include <time.h>
#include <string.h>
/* All engine access is serialized. The worker never holds an IEC pointer.
* Network I/O is nonblocking; no mutex is held across sleep or thread join.
* Serial open/ioctl latency is driver dependent: this is not a hard RT API. */
uint32_t ab_dispatch(uint32_t,uint32_t,uint32_t,uint16_t,uint8_t,uint8_t,uint16_t,uint16_t,
uint16_t *,uint32_t,uint32_t *,uint32_t,uint64_t,uint32_t,uint32_t,uint32_t);
void ab_pump(uint64_t,uint32_t);
void ab_close_all(void);
static pthread_mutex_t aw_lifecycle=PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t aw_engine;
static pthread_once_t aw_once=PTHREAD_ONCE_INIT;
static int aw_init_error;
static void aw_init_mutex(void){
pthread_mutexattr_t a;
int e=pthread_mutexattr_init(&a);
if(e){aw_init_error=e;return;}
/* Priority inheritance limits inversion if the PLC task has RT priority. */
e=pthread_mutexattr_setprotocol(&a,PTHREAD_PRIO_INHERIT);
if(e){pthread_mutexattr_destroy(&a);aw_init_error=e;return;}
aw_init_error=pthread_mutex_init(&aw_engine,&a);
pthread_mutexattr_destroy(&a);
}
static pthread_t aw_thread;
static int aw_running,aw_stop,aw_error,aw_cpu=-1;
static uint32_t aw_interval=1,aw_watchdog=2000;
static uint64_t aw_cycles,aw_heartbeat;
static cpu_set_t aw_original_mask;
static uint64_t aw_now(void){
struct timespec t;clock_gettime(CLOCK_MONOTONIC,&t);
return (uint64_t)t.tv_sec*1000u+(uint64_t)t.tv_nsec/1000000u;
}
static void *aw_main(void *unused){
(void)unused;
for(;;){
pthread_mutex_lock(&aw_engine);
if(aw_stop){pthread_mutex_unlock(&aw_engine);break;}
aw_heartbeat=aw_now();ab_pump(aw_heartbeat,aw_watchdog);aw_cycles++;
uint32_t delay=aw_interval;
pthread_mutex_unlock(&aw_engine);
struct timespec t={delay/1000u,(long)(delay%1000u)*1000000L};
while(nanosleep(&t,&t)<0 && errno==EINTR){}
}
return NULL;
}
static int aw_start(void){
if(aw_running)return 0;
int e=pthread_getaffinity_np(pthread_self(),sizeof(aw_original_mask),&aw_original_mask);
if(e)return e;
pthread_attr_t attr;struct sched_param priority={0};e=pthread_attr_init(&attr);
if(e)return e;
/* Do not inherit a high-priority real-time PLC task's scheduling policy. */
e=pthread_attr_setinheritsched(&attr,PTHREAD_EXPLICIT_SCHED);
if(!e)e=pthread_attr_setschedpolicy(&attr,SCHED_OTHER);
if(!e)e=pthread_attr_setschedparam(&attr,&priority);
aw_stop=0;
if(!e)e=pthread_create(&aw_thread,&attr,aw_main,NULL);
pthread_attr_destroy(&attr);
if(!e){aw_running=1;aw_cpu=-1;}
return e;
}
static void aw_snapshot(uint32_t *o){
memset(o,0,24*sizeof(*o));o[0]=0x00040000u;o[1]=(uint32_t)aw_running;
o[2]=(uint32_t)aw_error;o[3]=(uint32_t)(aw_cpu+1);o[4]=aw_interval;o[5]=aw_watchdog;
o[6]=(uint32_t)aw_cycles;o[7]=(uint32_t)(aw_cycles>>32);
o[8]=(uint32_t)aw_heartbeat;o[9]=(uint32_t)(aw_heartbeat>>32);
}
uint32_t ab_call(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 ignored_now,uint32_t ct,uint32_t rt,uint32_t retry){
(void)ignored_now;
if(!status || sc<AM_STATUS_WORDS)return AB_BAD_CAPACITY;
pthread_once(&aw_once,aw_init_mutex);
if(aw_init_error){memset(status,0,24*sizeof(*status));status[2]=status[7]=(uint32_t)aw_init_error;return AB_WORKER_ERROR;}
pthread_mutex_lock(&aw_lifecycle);pthread_mutex_lock(&aw_engine);
uint32_t result=0;
if(cmd==1 || cmd==20 || cmd==30 || cmd==40){
aw_error=aw_start();
if(aw_error){memset(status,0,24*sizeof(*status));status[2]=status[7]=(uint32_t)aw_error;result=AB_WORKER_ERROR;goto done;}
}
if(cmd==40){
/* addr=CPU+1 (0 restores inherited mask), ct=pump interval, rt=scan watchdog. */
if(addr>CPU_SETSIZE || ct<1 || ct>20 || rt<100 || rt>60000){result=AM_ARGUMENT;goto worker_status;}
cpu_set_t mask=aw_original_mask;
if(addr){CPU_ZERO(&mask);CPU_SET(addr-1,&mask);}
aw_error=pthread_setaffinity_np(aw_thread,sizeof(mask),&mask);
if(aw_error){result=AB_WORKER_ERROR;goto worker_status;}
aw_cpu=(int)addr-1;aw_interval=ct;aw_watchdog=rt;
worker_status:
aw_snapshot(status);
}else if(cmd==41){aw_snapshot(status);}
else result=ab_dispatch(cmd,h,ip,port,unit,f,addr,q,data,cap,status,sc,aw_now(),ct,rt,retry);
done:
pthread_mutex_unlock(&aw_engine);pthread_mutex_unlock(&aw_lifecycle);
return result;
}
void ab_shutdown(void){
pthread_once(&aw_once,aw_init_mutex);
if(aw_init_error)return;
pthread_mutex_lock(&aw_lifecycle);pthread_mutex_lock(&aw_engine);
int join=aw_running;aw_stop=1;pthread_mutex_unlock(&aw_engine);
if(join)pthread_join(aw_thread,NULL);
pthread_mutex_lock(&aw_engine);ab_close_all();aw_running=0;aw_cpu=-1;
aw_error=0;aw_interval=1;aw_watchdog=2000;aw_cycles=0;aw_heartbeat=0;
pthread_mutex_unlock(&aw_engine);pthread_mutex_unlock(&aw_lifecycle);
}
+1 -1
View File
@@ -5,7 +5,7 @@ from pathlib import Path
root=Path(__file__).resolve().parents[1] root=Path(__file__).resolve().parents[1]
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
so=Path(tmp)/'test-transport.so' so=Path(tmp)/'test-transport.so'
subprocess.run(['cc','-shared','-fPIC','-std=c11','-Wall','-Wextra','-Werror','-O2','-pthread','-I'+str(root/'src'),*[str(root/'src'/f) for f in ['atom_mb.c','atom_rtu.c','atom_server.c','atom_bridge.c','atom_worker.c']],'-o',str(so)],check=True) 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 lib=C.CDLL(str(so));fn=lib.ab_call
U=C.c_uint32;W=C.c_uint16;B=C.c_uint8 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 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
-117
View File
@@ -1,117 +0,0 @@
#!/usr/bin/env python3
"""Exercise production worker with real sockets/PTYS and deliberately sparse PLC calls."""
import ctypes as C
import os,pty,select,socket,struct,subprocess,tempfile,time,threading
from pathlib import Path
root=Path(__file__).resolve().parents[1]
with tempfile.TemporaryDirectory() as td:
so=Path(td)/'worker.so'
subprocess.run(['cc','-shared','-fPIC','-std=c11','-Wall','-Wextra','-Werror','-O2','-pthread',
*[str(root/'src'/f) for f in ['atom_mb.c','atom_rtu.c','atom_server.c','atom_bridge.c','atom_worker.c']],'-o',str(so)],check=True)
lib=C.CDLL(str(so));U=C.c_uint32;W=C.c_uint16;B=C.c_uint8
lib.ab_call.argtypes=[U,U,U,W,B,B,W,W,C.POINTER(W),U,C.POINTER(U),U,C.c_uint64,U,U,U];lib.ab_call.restype=U
def call(cmd,h=0,ip=0,port=0,f=3,addr=0,q=1,values=(),ct=100,rt=100,retry=10,unit=1):
data=(W*2000)(*values);status=(U*24)()
result=lib.ab_call(cmd,h,ip,port,unit,f,addr,q,data,2000,status,24,0,ct,rt,retry)
return result,list(status),list(data)
def until(fn,seconds=1):
end=time.monotonic()+seconds
while time.monotonic()<end:
v=fn()
if v:return v
time.sleep(.002)
raise AssertionError('condition timed out')
def configure(watchdog=2000,cpu=-1):
r,s,_=call(40,addr=cpu+1,ct=1,rt=watchdog);assert r==0,(r,s)
def client(port):
r,s,_=call(1,ip=0x7f000001,port=port);assert not r;h=s[0]
assert not call(9,h,rt=100)[0];assert not call(3,h)[0]
until(lambda:call(5,h)[1][1]==3)
return h
def recv_exact(peer,count):
result=b''
while len(result)<count:
v=peer.recv(count-len(result));assert v;result+=v
return result
def server():
r,s,_=call(30,ip=0x7f000001,port=port);assert not r;h=s[0]
for t in range(4):assert not call(35,h,f=t,q=8,values=[0]*8)[0]
assert not call(32,h)[0]
until(lambda:call(34,h)[1][1])
return h
probe=socket.socket();probe.bind(('127.0.0.1',0));port=probe.getsockname()[1];probe.close()
try:
configure();sh=server();ch=client(port)
assert not call(6,ch,q=2)[0]
# The reply deadline is 100 ms; the PLC deliberately does nothing for 250 ms.
time.sleep(.25)
r,s,d=call(7,ch);assert not r and s[3] and not s[4] and d[:2]==[0,0],s
assert call(6,ch)[0]==2 # Completion cannot be overwritten before acknowledgment.
r,m,_=call(12,ch);assert not r and m[21]<100 and m[22]>=100,m
assert call(10,ch)[0]==0
print('PASS worker completes TCP before timeout with PLC absent for 250 ms; result retained and timed')
# Remote write must survive a stale PLC push before the PLC pulls its result.
peer=socket.create_connection(('127.0.0.1',port));peer.settimeout(1)
pdu=struct.pack('>BHH',6,0,4321);peer.sendall(struct.pack('>HHHB',1,0,len(pdu)+1,1)+pdu)
assert recv_exact(peer,12)[7:]==pdu
assert call(35,sh,f=2,q=8,values=[0]*8)[0]==0
assert call(36,sh,f=2,q=8)[2][0]==4321
# After acknowledgment a deliberate application override is permitted.
call(35,sh,f=2,q=8,values=[42]*8);assert call(36,sh,f=2,q=8)[2][0]==42
assert call(35,sh,f=2,addr=1,q=8,values=[0]*8)[0]==1 # explicit recreate for remap
peer.close();call(2,ch);call(31,sh)
print('PASS autonomous TCP slave and lossless remote-write handoff')
# Real serial PTY, again no PLC ticks after submission.
master,slave=pty.openpty();path=os.ttyname(slave);os.close(slave)
r,s,_=call(20,ip=9600,unit=0,f=8,addr=1,q=len(path),ct=0,values=list(path.encode()));assert not r;h=s[0]
call(9,h,rt=100);call(3,h);until(lambda:call(5,h)[1][1]==3)
call(6,h,unit=1,f=3,q=1)
assert select.select([master],[],[],1)[0];request=os.read(master,256);assert len(request)==8
def crc(p):
c=65535
for b in p:
c^=b
for _ in range(8):c=(c>>1)^(0xa001 if c&1 else 0)
return c
p=bytes([1,3,2,0,77]);os.write(master,p+struct.pack('<H',crc(p)))
time.sleep(.25);r,s,d=call(7,h);assert not r and s[3] and d[0]==77,s
call(2,h);os.close(master)
print('PASS RTU completion while PLC does not scan for 250 ms')
# Watchdog cancels an in-flight write without replaying it on resume.
configure(100)
listener=socket.socket();listener.bind(('127.0.0.1',0));listener.listen();listener.settimeout(1)
h=client(listener.getsockname()[1]);peer,_=listener.accept();peer.settimeout(1)
call(11,h,rt=1000);assert not call(6,h,f=16,q=1,values=[9])[0]
packet=recv_exact(peer,15);assert packet[7]==16
time.sleep(.16)
# command 12 is read-only and does not renew the scan lease.
r,s,_=call(12,h);assert not r and s[1]==0 and s[5]==10 and s[9]==1,s
assert peer.recv(1)==b''
call(5,h);peer2,_=listener.accept();peer2.settimeout(.03)
try:assert not peer2.recv(1), 'write replayed'
except socket.timeout:pass
call(2,h);peer.close();peer2.close();listener.close()
print('PASS scan watchdog closes transport, preserves uncertain write, resumes without replay')
# Server closes its listener when scanning ceases and recovers on refresh.
configure(100);sh=server();time.sleep(.16)
probe=socket.socket();probe.settimeout(.1);assert probe.connect_ex(('127.0.0.1',port))!=0;probe.close()
call(34,sh);until(lambda:call(34,sh)[1][1]);call(31,sh)
print('PASS server watchdog and restart')
# CPU selection, failure reporting, and restoring inherited mask.
configure(cpu=min(os.sched_getaffinity(0)))
r,s,_=call(41);assert not r and s[3]==min(os.sched_getaffinity(0))+1
assert call(40,addr=65535,ct=1,rt=2000)[0]==1
configure()
failures=[]
def reader():
for _ in range(200):
if call(41)[0]:failures.append('snapshot failure')
threads=[threading.Thread(target=reader) for _ in range(4)]
for t in threads:t.start()
for t in threads:t.join()
assert not failures
print('PASS configurable affinity, invalid arguments, concurrent snapshot access')
for _ in range(20):
lib.ab_shutdown();assert call(41)[1][1]==0;configure();assert call(41)[1][1]==1
print('PASS repeated joined shutdown/restart')
finally:lib.ab_shutdown()
+13 -42
View File
@@ -1,6 +1,6 @@
# atMb 0.4 API reference # atMb 0.8 API reference
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. Generated from `library/atMb-OnlineChange.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 ## atMbCall_cext
@@ -106,7 +106,7 @@ END_TYPE
## ModbusMaster.ClientBase ## ModbusMaster.ClientBase
Shared abstract client scheduler and registry. Call concrete client every scan; worker handles transaction progress. Exposes aggregated stats and toString. Shared abstract client scheduler and registry. Call concrete client every scan; IEC task handles transaction progress. Exposes aggregated stats and toString.
```iecst ```iecst
{attribute 'monitoring_display' := 'toString'} {attribute 'monitoring_display' := 'toString'}
@@ -564,7 +564,7 @@ VAR_INPUT
sentCnt : UDINT; sentCnt : UDINT;
replyCnt : UDINT; replyCnt : UDINT;
exceptionCnt : UDINT; exceptionCnt : UDINT;
totalValidDuration : ULINT; // Sum of successful worker transaction durations, milliseconds. totalValidDuration : ULINT; // Sum of successful transport transaction durations, milliseconds.
END_VAR END_VAR
VAR VAR
previousOnline : BOOL; previousOnline : BOOL;
@@ -774,7 +774,7 @@ END_VAR
## ClientBase.FB_exit ## ClientBase.FB_exit
Lifecycle resource release. Worker is joined by component exit; context hooks close their owned handle. Use public release for application shutdown. Lifecycle resource release. context hooks close their owned handle. Use public release for application shutdown.
```iecst ```iecst
METHOD FB_exit : BOOL METHOD FB_exit : BOOL
@@ -815,7 +815,7 @@ END_VAR
## ClientBase.processCycle ## ClientBase.processCycle
Refresh scan lease, read completed results, process request completions, acknowledge result and select next transaction. OS I/O progression is performed by the worker. Refresh scan lease, read completed results, process request completions, acknowledge result and select next transaction. Transport I/O progresses during the owning PLC call.
```iecst ```iecst
METHOD processCycle METHOD processCycle
@@ -826,7 +826,7 @@ END_VAR
## ClientBase.ensureContext ## ClientBase.ensureContext
Check 0.4 worker capability, allocate/configure/enable a context. A nonzero handle does not imply connectivity. Old components are rejected with error 259. Allocate/configure/enable a context. A nonzero handle does not imply connectivity. Old components are rejected with error 259.
```iecst ```iecst
METHOD PROTECTED ensureContext : BOOL METHOD PROTECTED ensureContext : BOOL
@@ -1138,7 +1138,7 @@ METHOD PUBLIC invalidateData
## RequestBase.finishRequest ## RequestBase.finishRequest
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. Map a completed result and write uncertainty, collect transport timing/read age, update reply/exception/timeout counters and completion flags. Successful reads become valid; errors retain old array values.
```iecst ```iecst
METHOD finishRequest METHOD finishRequest
@@ -1239,7 +1239,7 @@ PROPERTY reqBusy : BOOL
## RequestStats.reqDone ## RequestStats.reqDone
On successful executing completion, increment saturated success counters, clear errors and execution, update display. Worker supplies duration. On successful executing completion, increment saturated success counters, clear errors and execution, update display. Transport supplies duration.
```iecst ```iecst
PROPERTY reqDone : BOOL PROPERTY reqDone : BOOL
@@ -1307,7 +1307,7 @@ END_VAR
## ModbusSlave.FB_exit ## ModbusSlave.FB_exit
Lifecycle resource release. Worker is joined by component exit; context hooks close their owned handle. Use public release for application shutdown. Lifecycle resource release. context hooks close their owned handle. Use public release for application shutdown.
```iecst ```iecst
METHOD FB_exit : BOOL METHOD FB_exit : BOOL
@@ -1327,7 +1327,7 @@ METHOD FB_reinit : BOOL
## ModbusSlave.serverCall ## ModbusSlave.serverCall
Private synchronous server/worker ABI adapter using server staging/status arrays. Private synchronous server ABI adapter using server staging/status arrays.
```iecst ```iecst
METHOD PRIVATE serverCall : UDINT METHOD PRIVATE serverCall : UDINT
@@ -1339,7 +1339,7 @@ END_VAR
## SlaveTCP.FB_exit ## SlaveTCP.FB_exit
Lifecycle resource release. Worker is joined by component exit; context hooks close their owned handle. Use public release for application shutdown. Lifecycle resource release. context hooks close their owned handle. Use public release for application shutdown.
```iecst ```iecst
METHOD FB_exit : BOOL METHOD FB_exit : BOOL
@@ -1359,7 +1359,7 @@ METHOD FB_reinit : BOOL
## SlaveTCP.serverCall ## SlaveTCP.serverCall
Private synchronous server/worker ABI adapter using server staging/status arrays. Private synchronous server ABI adapter using server staging/status arrays.
```iecst ```iecst
METHOD PRIVATE serverCall : UDINT METHOD PRIVATE serverCall : UDINT
@@ -1539,32 +1539,3 @@ STRUCT
END_STRUCT END_STRUCT
END_TYPE 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
```
+18 -46
View File
@@ -1,61 +1,33 @@
# Build and upgrade to atMb 0.6 # Build and upgrade to atMb 0.8
## Build on ARM64 Linux Build from the repository root and clean between architectures:
From the repository root:
```sh ```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)" clean
make -C component-source/component SDKDIR="$(realpath ExtensionSDK)" all make -C component-source/component SDKDIR="$(realpath ExtensionSDK)" all
mkdir -p artifacts/arm64
cp component-source/component/out/libCmpAtomModbus.so artifacts/arm64/
``` ```
Run these commands on an ARM64 build machine. On an x86 laptop, ordinary gcc For the UR2000 ARMv7 hard-float target:
builds x86 output; use the appropriate cross-compiler instead.
## Cross-compile ARMv7 hard-float on the laptop
```sh ```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)" clean
make -C component-source/component SDKDIR="$(realpath ExtensionSDK)" CC="arm-linux-gnueabihf-gcc -march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=hard" all make -C component-source/component \
mkdir -p artifacts/armv7hf SDKDIR="$(realpath ExtensionSDK)" \
cp component-source/component/out/libCmpAtomModbus.so artifacts/armv7hf/ CC="arm-linux-gnueabihf-gcc -march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=hard" all
arm-linux-gnueabihf-readelf --version-info artifacts/armv7hf/libCmpAtomModbus.so
``` ```
The makefile supplies `-pthread` during compilation/linking. Always clean between Use the matching ARM64 compiler for Raspberry Pi/UC20 and check ELF architecture
architectures. Check `file`, ELF attributes, NEEDED libraries and GLIBC symbol and GLIBC requirements before deployment. The C component is version **0.5.0.0**.
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.
## CODESYS ## CODESYS and online change
1. Keep the prior working `.library` and PLC application for rollback. 1. Create a fresh library project and import `library/atMb-OnlineChange.export`.
2. Create a fresh library project named `CmpAtomModbus.library` and import 2. Verify Project Information version **0.8.0.0**, namespace `atMb`, and the
`library/atMb-Threaded.export`. Importing into an old project does not delete existing Standard/SysTime references.
renamed/removed objects; a fresh project avoids duplicate old methods. 3. Add a C Implemented Library object with the rebuilt **0.5.0.0** component.
3. Preserve Project Information title `CmpAtomModbus`, namespace `atMb`, and 4. Compile the library and update the application reference.
Standard/SysTime references. Verify library version 0.6.0.0 in Project Information. 5. Test online change while idle, during an active read, during disconnect, and
4. Add a C Implemented Library object and attach the **new 0.4.0.0** component for with an active write. Confirm clients reopen and data becomes valid.
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.
The C ABI layout/signature is unchanged (0x709EE65B, API 0.2.0.0). Component version The external ABI signature/API version is unchanged. There is no worker FB,
is 0.4.0.0. Command 41 capability checking prevents this IEC export operating with thread-affinity setting, or native worker thread in this release.
an old component; it does not eliminate target/compiler compatibility checks.
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.
+6 -9
View File
@@ -1,11 +1,8 @@
# Migration housekeeping # Release housekeeping
The prior working library, export and ARM binaries are retained under `backup/`. The active release is `atMb-OnlineChange.export`, library version 0.8.0.0, with
They are not the threaded release. New source is imported from `atMb-Threaded.export` C component version 0.5.0.0. The old threaded export and ARM binaries are kept
and rebuilt in the IDE with a newly built 0.4 C component. Previous IDE caches, under `backup/` for rollback/reference and are not part of the active release.
editor lock/options files and C build outputs were omitted from this package.
Git metadata is retained; no commit or push was made.
`RENAMED-MEMBERS.json` is the exact old-to-new mapping for application changes. No Git commit or push was made. Build outputs and IDE cache files are omitted
The previous transport prefix is removed from active IEC code and comments. from the release archive.
Public request output is now out_errorCode; client output is out_lastErrorCode.
-11
View File
@@ -1,11 +0,0 @@
# Export import correction
The Worker object in the first threaded export contained an unnamed List element.
The generator cleared its attributes as well as its children. This revision
restores Name="ChildObjectGuids", preserves an empty child list, and matches the
original export envelope (no XML declaration, CRLF line endings).
All collection elements now retain serialization attributes. XML parsing passes.
CODESYS import has not been verified in an IDE. If import still fails, capture the
exact error text and whether it occurs before or after the object selection dialog.
The C sources and IEC program logic are unchanged by this correction.
+27 -182
View File
@@ -1,190 +1,35 @@
# atMb 0.4 user guide # atMb 0.8 user guide
## Thread and PLC calls ## Execution model
One C worker progresses all TCP/RTU client transactions and TCP slave connections. TCP, RTU and slave transport progress synchronously when the owning client or
It owns no pointers into IEC memory: request values, replies and slave tables live slave FB is called. Keep the related client, request FBs and data arrays in one
in fixed C storage. The external call copies buffers synchronously and serializes PLC task and call them every scan. This version intentionally has no native
state access using a priority-inheritance engine mutex. No network wait or sleep worker thread, CPU-affinity input, or scan watchdog.
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.
Continue calling each client/request and slave every PLC scan. Client request The C state machines use nonblocking sockets and fixed storage. Serial driver
selection is still performed by the IEC scheduler. A transaction can finish while calls remain target-dependent, and a long PLC task interval can delay transport
the PLC is not scanning, and its result waits for the next scan; the worker does progress; choose request timeouts accordingly.
not independently cycle through the PLC request registry. TCP slaves respond from
the last copied memory snapshot between scans.
## Optional worker configuration and a separate core ## Online change
```iecst `FB_reinit` closes the native context before CODESYS replaces IEC instance memory,
VAR then clears handles, active request pointers and registration arrays. Request FBs
commWorker : atMb.Worker; clear in-flight state. TCP/RTU clients and slave listeners reopen on a later PLC
END_VAR scan. Expect active requests to be cancelled and reads to remain stale until a
validated reply arrives. Writes already transmitted are not replayed.
// Example only: CPU 2 must be online and permitted on this PLC. ## Status and recovery
commWorker(in_cpu := 2, in_interval := T#1MS, in_watchdog := T#2S);
```
Use only one Worker configuration instance for the component. Clients/servers Client statistics aggregate all registered requests. `toString` uses
start the worker automatically with defaults even without this FB. Default CPU -1 `OK:<successful requests>/Er:<failed requests>`. Three consecutive failed request
uses the thread creator's inherited affinity mask; it is not a promise of all CPUs. results are required before a request is exposed as offline; a short TCP-to-RTU
A nonnegative value pins the worker to that Linux logical CPU ID. CPU affinity does converter reconnect therefore does not immediately mark every request offline.
not reserve an exclusive core or move the PLC task: choose a CPU outside the PLC A valid response restores online immediately. Reconnect remains automatic
task's configured affinity where the runtime allows it. Changes affect this worker through the synchronous state machine.
only. Failure is visible in `out_errorCode` and `out_osError`; no silent fallback.
The worker uses normal OS scheduling and explicitly avoids inheriting a PLC task's For TCP-to-RTU converters, set `ClientTCP.in_baudrate` to the converter's
real-time priority. Sleep interval is 1..20 ms, default 1 ms; scheduling jitter can serial baud rate. When no explicit request or client timeout is supplied, the
make actual intervals longer. Very short reply deadlines must account for worker library estimates timeout from the request/response frame size and baud rate,
interval and transaction framing. `out_cycles` helps monitor worker progress. then adds turnaround and device-processing margin. Direct TCP clients should
leave `in_baudrate` at zero and use the normal fixed TCP timeout.
## Scan-loss watchdog
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.
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.
## TCP read
```iecst
VAR
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);
```
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.
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.
## Requests, data and priorities
| 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 |
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.
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.
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 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
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.
`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.
`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.
## Client statistics
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.
| Client member | Aggregation |
|---|---|
| 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 |
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.
`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.
`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 memory exchange
`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.
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.
Online-change pointer ownership and physical hardware certification remain future
work. This release needs a full download and on-target validation.
+23 -33
View File
@@ -1,39 +1,29 @@
# Validation and remaining qualification # Validation and online-change qualification
IEC library version: 0.6.0.0. C component version: 0.4.0.0. Release versions: IEC library **0.8.0.0**, C component **0.5.0.0**. The external
External C ABI signature/API version are unchanged. ABI signature/API version is unchanged.
## Passed on the Linux build host Host validation completed:
- Strict C compilation with warnings treated as errors for the test targets. - Strict synchronous C build and bridge capacity/pool/handle/shutdown tests.
- Bridge capacity, context pool, handle generation and shutdown tests. - Direct and bridge TCP normal, fragmented, timeout, protocol-error, exception,
- Nine direct TCP and nine bridge TCP protocol/error/recovery scenarios. write and reconnect scenarios.
- RTU tests over real pseudo-terminals: fragmented frames, CRC errors, timeouts, - RTU pseudo-terminal frame, CRC, timeout, exception, register/coil write and
exceptions, register/coil writes, invalid write echoes and recovery. recovery scenarios.
- TCP slave FC1/2/3/4/5/6/15/16, address/exception handling and bind recovery. - TCP slave function-code, exception, address and bind-recovery scenarios.
- Worker TCP and RTU responses complete with a 100 ms deadline while PLC calls - XML export parsing, object references, collection metadata and removal of the
are paused for 250 ms; results remain available for the next call. threaded Worker object.
- Worker duration/age reporting and rejection of an unacknowledged result overwrite. - C client `FB_reinit` closes its native context before clearing IEC pointers;
- Slave service between scans and remote writes preserved against stale PLC pushes. slave `FB_reinit` closes its listener before clearing its handle.
- 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 Required on each target before production:
Rebuild the C component for each target with its compatible SDK/toolchain, check - Rebuild for the exact CPU/toolchain and verify ELF/GLIBC compatibility.
its ELF architecture and GLIBC dependencies, import the export in CODESYS, compile, - Compile/import the export in CODESYS and embed the matching C component.
embed/deploy the rebuilt component and perform a full download. No updated compiled - Test online change while idle, during an active read, during disconnect and
.library or ARM binary is supplied. Previous binaries are backups only. during an active write.
- Verify contexts reopen, request pointers are rebuilt, data becomes valid, and
no stale socket/listener remains after repeated online changes.
CODESYS IDE compilation, physical target/serial hardware testing, prolonged load Physical target, prolonged soak, and CODESYS IDE qualification were not available
and disconnect testing, and online-change behavior were not qualified here. in the host environment. Keep the previous threaded release as rollback only.
Online change remains future work. Confirm watchdog and reconnect settings under
actual PLC task jitter and network failures before production use.
+13 -33
View File
@@ -1,37 +1,17 @@
# Worker ABI and lifecycle # Synchronous ABI and online-change lifecycle
The 16-argument atMbCall_cext ABI and its signature are retained. Calls are serialized There is no native worker thread in atMb 0.8. The existing 16-argument
by the lifecycle mutex and priority-inheritance engine mutex. The worker accesses `atMbCall_cext` ABI is retained, and every transport tick runs synchronously in
fixed C contexts only. Calls may wait briefly for the engine mutex; no claim of the PLC task that owns the IEC client or slave instance.
hard real-time maximum latency is made for OS/serial-driver operations.
New commands: The C component uses fixed context pools, generation-checked handles and no
retained IEC pointers. Request values and results are copied through the ABI
buffers during the owning PLC call.
| Command | Purpose | Parameters / result | For online change, `FB_reinit` closes the native context first, clears the client
|---|---|---| handle and all IEC request pointers, and resets active-request state. The next
| 12 | Client transaction timing | Normal handle; status[21] duration ms, [22] successful-read age ms (saturated UDINT); other client status fields retained | PLC scan recreates the context using the current configuration. Slave blocks
| 40 | Configure/start global worker | startAddress=CPU+1 (0 inherited mask), connectTimeoutMs=interval 1..20, responseTimeoutMs=watchdog 100..60000 | close their listener before clearing their native handle.
| 41 | Worker capabilities/status | No start side effect; available even before worker starts |
Worker status words: [0] capability 0x00040000; [1] running; [2] pthread error; The external ABI signature/API version remain unchanged. Component version is
[3] configured CPU+1, 0 inherited mask; [4] interval ms; [5] watchdog ms; 0.5.0.0; IEC library version is 0.8.0.0.
[6:7] cycle counter low/high; [8:9] monotonic heartbeat low/high. Remaining words zero.
Error 260 reports worker initialization or CPU-affinity failure. Invalid settings
return 1. Invalid settings do not change current affinity/timing.
Client command 5 and server command 34 now refresh the context's scan lease and
snapshot state; they do not perform socket reads/writes. All deadline times are
sourced from CLOCK_MONOTONIC; the ABI nowMs argument is retained but ignored.
Command 6 rejects a new transaction until completion is acknowledged with command
10. Completion data cannot be overwritten by another request before acknowledgment.
Worker shutdown sets a stop flag, releases the mutex, joins the thread, then
closes contexts. Generation-tagged handles prevent access to released slots.
Watchdog expiry closes transport while preserving the slot and terminal result;
refresh resumes it. No worker thread is restarted repeatedly on link failure;
existing TCP/RTU state machines retry within it.
Socket progress is nonblocking. Socket close/configuration calls may also occur
synchronously for explicit close/reconfigure commands. These and serial ioctls are
OS calls, so target execution-time measurement remains necessary. The worker uses
normal SCHED_OTHER, not a high-priority busy loop.
File diff suppressed because one or more lines are too long
Binary file not shown.
-4
View File
@@ -1,4 +0,0 @@
atom
ADAM-CODESYS
9256
639269490056168856
File diff suppressed because one or more lines are too long