Compare commits

..
5 Commits
Author SHA1 Message Date
snkbyte bc27b9bb27 Made online change work on ur20 2026-10-07 21:54:01 -06:00
snkbyte 739347c4da Seems to be working great 2026-10-07 01:03:18 -06:00
snkbyte c39c226885 Threaded version 0.7 2026-10-06 23:50:07 -06:00
snkbyte 490c9bdc5b Threaded version 0.7 2026-10-06 23:50:02 -06:00
snkbyte fe5dcbe715 Added for older ur20 2026-10-01 17:59:20 -06:00
34 changed files with 13442 additions and 935 deletions
+10 -57
View File
@@ -1,60 +1,13 @@
# Atom Modbus Library (`atMb`)
# atMb 0.8 — online-change-focused synchronous release
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 release removes the native worker thread and restores TCP, RTU and server
transport progress to the owning PLC call. The C layer retains no IEC pointers
and uses fixed storage only.
## Use the library
Import `library/atMb-OnlineChange.export` into a fresh CODESYS library project.
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.
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.
## Repository contents
| 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 |
## Build on the Pi
Run 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
make -C component-source test
```
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.
`FB_reinit` closes native contexts before IEC instance memory is replaced,
clears request pointers and active-request state, and allows contexts to reopen
on subsequent scans.
+26 -18
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,37 @@ 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
bf5b5518674a427f0a5c08f4484b07fc5fb92e2581469420f236358b5c2b6a07 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
f50ee94893d4282e6ff125c1830cbea05c0d1f3680bd7ddadd2b4c79aa1edf14 component-source/Makefile
d91cffa9751bca79f13ae80552d654519333536c894bb13b41a1182c179c53b7 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
d891cd5dbaa6e7d891c42de74368ac482461986472b9d1f92af00d611289b1b7 component-source/component/makefile
ffb81617656709acb3b046a240c03df5ea0d8de9001fa631444b23b714f05036 component-source/src/atom_bridge.c
7994895beac2337d1f81216d5971eafa335c950c2050aa6e7cdfc83984955155 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
2555b12b0f1b8dc84847d2a34155f5d496fbb47804a7f27bf094458bed9e3949 component-source/src/atom_server.c
51f3b4a5779347e58b9993b3c2d8f180a73e0a70cb2a8b81bf9350aca8b42bad component-source/src/atom_server.h
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
575adda751a14ef74dc515190da8f93c8c2b085a7e4388a9b5363cc442e7bdd8 component-source/tests/test_transport.py
e409f8f786c5d4fc6f91230dc2ad6baa1af3388ec74e1a13a6011634f36d430e docs/API-REFERENCE.md
91e5a0a3597d214832ca2acf02a72bfe8678268fc7b88aba1cca4a9336087f56 docs/BUILD-AND-USAGE.md
dc4b312826bba713d389d86cd35fd01d4ea223c3f084556f9ff2064532a4dcee docs/CLEANUP.md
d60f5881289ac66cf5f6e4a90c5c32beaded3426a385825fb3d1750ec758dee1 docs/RENAMED-MEMBERS.json
56fa44b409bb90c3271d162ef36f52644d17f49c2badf32f08c224ff26ced796 docs/USER-GUIDE.md
ba57f003d81114f7b20dedbb0837259cacc8d6e0211308927a477a8609d9f031 docs/VALIDATION.md
7c566ea69d86ff9654f7eb100e3086b9139bb006a8323a5272adf1dd4a6e57d2 docs/WORKER-ABI.md
b0b0e76734802224de21cb985bf9554edd45fcf5ced4b9b911f9eecc99089002 library/atMb-OnlineChange-Review.st
fac79d3281a13570e134a599953584a399a884d63fdb2ea32f801548fa3c3c64 library/atMb-OnlineChange.export
+1
View File
@@ -0,0 +1 @@
Build the updated C component for each target here. See docs/BUILD-AND-USAGE.md. Previous ARM binaries are under backup/artifacts and must not be used with the new IEC export.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
Original uploaded working library/export and ARM binaries, retained for rollback/reference. These are the previous release and do not contain the worker or new statistics. Do not mix them with the updated IEC export.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
+1 -1
View File
@@ -5,7 +5,7 @@ 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
python3 tests/test_transport.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
+7
View File
@@ -1,3 +1,7 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "atmb_boot_link.h"
#include "../src/atom_bridge.h"
/*****************************************************************************
* Copyright: Copyright CODESYS Development GmbH
@@ -38,6 +42,9 @@ DLL_DECL int CDECL ComponentEntry(INIT_STRUCT *pInitStruct)
s_pfCMCallHook = pInitStruct->pfCMCallHook;
s_pfCMRegisterClass = pInitStruct->pfCMRegisterClass;
s_pfCMCreateInstance = pInitStruct->pfCMCreateInstance;
/* Best effort only: never make component startup depend on this. */
atmb_ensure_boot_link();
return ERR_OK;
}
+109
View File
@@ -0,0 +1,109 @@
#ifndef ATMB_BOOT_LINK_H
#define ATMB_BOOT_LINK_H
/* Linux-only boot-project pathname workaround. Include in CmpFrame.c only,
* with _GNU_SOURCE defined before all system headers.
* No PLC memory, communication state, worker thread or cyclic work is involved.
*/
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <syslog.h>
#include <unistd.h>
#include <stdint.h>
static void atmb_ensure_boot_link(void)
{
/* ComponentEntry is called at module initialization, not on each scan.
* Guard against repeated entry; failure is deliberately not retried.
*/
static int attempted;
const char *name = "libCmpAtomModbus.so";
const char *problem = NULL;
Dl_info info;
struct stat source, existing;
char *module_path = NULL;
int directory = -1;
int error = 0;
int saved_errno = errno;
if (attempted)
return;
attempted = 1;
/* The component function address identifies this loaded ELF object. Using
* code rather than a local data symbol also works with runtimes that do not
* expose local data addresses through dladdr().
*/
if (!dladdr((void *)(uintptr_t)&atmb_ensure_boot_link, &info) || !info.dli_fname) {
problem = "cannot locate loaded module";
goto finish;
}
module_path = realpath(info.dli_fname, NULL);
if (!module_path) {
error = errno;
problem = "cannot resolve loaded module pathname";
goto finish;
}
if (stat(module_path, &source) != 0) {
error = errno;
problem = "cannot inspect loaded module file";
goto finish;
}
if (!S_ISREG(source.st_mode)) {
problem = "loaded module pathname is not a regular file";
goto finish;
}
/* Boot creation was observed looking up the bare filename relative to
* runtime cwd. Anchor this directory with an FD; never change cwd.
*/
directory = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (directory < 0) {
error = errno;
problem = "cannot open runtime working directory";
goto finish;
}
if (fstatat(directory, name, &existing, AT_SYMLINK_NOFOLLOW) == 0) {
struct stat resolved;
if (fstatat(directory, name, &resolved, 0) == 0 &&
resolved.st_dev == source.st_dev &&
resolved.st_ino == source.st_ino)
goto finish; /* Correct link, hard link, or the source itself. */
problem = "existing file or link left untouched; check boot-project path";
goto finish;
}
if (errno != ENOENT) {
error = errno;
problem = "cannot inspect boot-project pathname";
goto finish;
}
/* symlinkat fails on EEXIST: even a concurrent creator cannot be replaced.
* Keep this link on shutdown; boot-project creation may still need it.
*/
if (symlinkat(module_path, directory, name) != 0) {
error = errno;
problem = "cannot create boot-project link";
goto finish;
}
syslog(LOG_INFO, "CmpAtomModbus: created boot link %s -> %s in runtime cwd",
name, module_path);
finish:
if (problem) {
if (error)
syslog(LOG_WARNING, "CmpAtomModbus: %s: %s (errno=%d); Modbus startup continues",
problem, strerror(error), error);
else
syslog(LOG_WARNING, "CmpAtomModbus: %s; Modbus startup continues", problem);
}
if (directory >= 0)
close(directory);
free(module_path);
errno = saved_errno;
}
#endif
+8 -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=5
CMPVERSION=0x00050001
# Local development ID; check no other component uses this ID.
CMPID=0x2001
DOS2UNIX=dos2unix
@@ -10,4 +10,9 @@ M4=m4
CC ?= gcc
# The SDK otherwise includes CmpFrame in its component identity wildcard.
override CSRC := CmpAtomModbus.c
# dladdr locates this module; -ldl also supports glibc older than 2.34.
LDLIBS += -ldl
include $(SDKDIR)/makefile
# Recompile the startup hook when the link helper changes.
$(OUTDIR)/CmpFrame.o: atmb_boot_link.h
+23 -3
View File
@@ -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;
@@ -68,6 +68,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 +77,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 +86,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 +102,15 @@ void ab_shutdown(void) {
if(slots[i].generation==0xffffffu) slots[i].retired=1;
}
}
/* Synchronous ABI entry point. All transport progress occurs on the caller's
* PLC task; no native worker thread or retained IEC pointer is used. */
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 now,uint32_t ct,uint32_t rt,uint32_t retry) {
return ab_dispatch(cmd,h,ip,port,unit,f,addr,q,data,cap,status,sc,now,ct,rt,retry);
}
void ab_shutdown(void) { ab_close_all(); }
/* Called only by the worker while holding the shared engine mutex. */
+1 -1
View File
@@ -1,7 +1,7 @@
#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; 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. */
#define AM_STATUS_WORDS 24
#define AM_POOL_SIZE 16
+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;
}
+18 -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,17 +112,25 @@ 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;}
as_tick(s,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;
}
@@ -1,10 +1,10 @@
#!/usr/bin/env python3
"""Real PTY/socket integration tests for the native RTU and TCP server bridge."""
"""Real PTY/socket integration tests for the transport 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'
so=Path(tmp)/'test-transport.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
@@ -70,13 +70,19 @@ with tempfile.TemporaryDirectory() as tmp:
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.
# Transport 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
assert call(32,h)==0
deadline=time.monotonic()+1
while time.monotonic()<deadline:
assert call(34,h)==0
if status[1]:break
time.sleep(.002)
assert status[1]==1
peer=socket.create_connection(('127.0.0.1',port));peer.setblocking(False)
tid=0
def exchange(pdu):
@@ -108,7 +114,19 @@ with tempfile.TemporaryDirectory() as tmp:
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
assert call(30,ip=0x7f000001,port=port)==0;h=status[0];call(32,h)
end=time.monotonic()+1
while time.monotonic()<end:
call(34,h)
if status[2]:break
time.sleep(.002)
assert status[2]==1
blocker.close()
end=time.monotonic()+1
while time.monotonic()<end:
call(34,h)
if status[1] and not status[2]:break
time.sleep(.002)
assert status[1]==1 and status[2]==0
call(31,h);print('PASS slave listener recovery and stale handles')
lib.ab_shutdown()
+475 -524
View File
File diff suppressed because it is too large Load Diff
+21 -56
View File
@@ -1,68 +1,33 @@
# Build and deployment
# Build and upgrade to atMb 0.8
## Build native C
From the repository root on the target Linux machine:
Build from the repository root and clean between architectures:
```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
```
`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.
For the UR2000 ARMv7 hard-float target:
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.
```sh
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
```
## Update the CODESYS library
Use the matching ARM64 compiler for Raspberry Pi/UC20 and check ELF architecture
and GLIBC requirements before deployment. The C component is version **0.5.0.0**.
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.
## CODESYS and online change
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.
1. Create a fresh library project and import `library/atMb-OnlineChange.export`.
2. Verify Project Information version **0.8.0.0**, namespace `atMb`, and the
existing Standard/SysTime references.
3. Add a C Implemented Library object with the rebuilt **0.5.0.0** component.
4. Compile the library and update the application reference.
5. Test online change while idle, during an active read, during disconnect, and
with an active write. Confirm clients reopen and data becomes valid.
## Runtime 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.
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.
The external ABI signature/API version is unchanged. There is no worker FB,
thread-affinity setting, or native worker thread in this release.
+6 -23
View File
@@ -1,25 +1,8 @@
# Repository cleanup
# Release housekeeping
Removed unused files:
The active release is `atMb-OnlineChange.export`, library version 0.8.0.0, with
C component version 0.5.0.0. The old threaded export and ARM binaries are kept
under `backup/` for rollback/reference and are not part of the active release.
- `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.
No Git commit or push was made. Build outputs and IDE cache files are omitted
from the release archive.
+90
View File
@@ -0,0 +1,90 @@
{
"NATIVE_CMD_ACKNOWLEDGE": "CMD_ACKNOWLEDGE",
"NATIVE_CMD_CLOSE": "CMD_CLOSE",
"NATIVE_CMD_CONFIGURE": "CMD_CONFIGURE",
"NATIVE_CMD_COPY_READ": "CMD_COPY_READ",
"NATIVE_CMD_ENABLE": "CMD_ENABLE",
"NATIVE_CMD_OPEN": "CMD_OPEN",
"NATIVE_CMD_REQUEST_TIMEOUT": "CMD_REQUEST_TIMEOUT",
"NATIVE_CMD_SUBMIT": "CMD_SUBMIT",
"NATIVE_CMD_TICK": "CMD_TICK",
"NATIVE_ERROR_CANCELLED": "ERROR_CANCELLED",
"NATIVE_ERROR_INVALID_HANDLE": "ERROR_INVALID_HANDLE",
"NATIVE_STATE_DISABLED": "STATE_DISABLED",
"NATIVE_STATE_READY": "STATE_READY",
"NATIVE_STATE_RECEIVING": "STATE_RECEIVING",
"NATIVE_STATUS_DONE": "STATUS_DONE",
"NATIVE_STATUS_ERROR": "STATUS_ERROR",
"NATIVE_STATUS_EXCEPTION": "STATUS_EXCEPTION",
"NATIVE_STATUS_FUNCTION": "STATUS_FUNCTION",
"NATIVE_STATUS_HANDLE": "STATUS_HANDLE",
"NATIVE_STATUS_LAST_ERROR": "STATUS_LAST_ERROR",
"NATIVE_STATUS_RECONNECTS": "STATUS_RECONNECTS",
"NATIVE_STATUS_RESULT": "STATUS_RESULT",
"NATIVE_STATUS_SOCKET_ERROR": "STATUS_SOCKET_ERROR",
"NATIVE_STATUS_STATE": "STATUS_STATE",
"NATIVE_STATUS_WRITE_UNCERTAIN": "STATUS_WRITE_UNCERTAIN",
"invalidateNativeData": "invalidateData",
"nativeActiveIndex": "engineActiveIndex",
"nativeActiveRequest": "engineActiveRequest",
"nativeArgAddress": "engineArgAddress",
"nativeArgFunction": "engineArgFunction",
"nativeArgQuantity": "engineArgQuantity",
"nativeArgTimeout": "engineArgTimeout",
"nativeArgUnit": "engineArgUnit",
"nativeCall": "callEngine",
"nativeCallResult": "engineCallResult",
"nativeConfigConnect": "engineConfigConnect",
"nativeConfigRetry": "engineConfigRetry",
"nativeConnectTimeoutMilliseconds": "engineConnectTimeoutMilliseconds",
"nativeCopyAllowed": "engineCopyAllowed",
"nativeCycle": "processCycle",
"nativeDataIndex": "engineDataIndex",
"nativeDevice": "engineDevice",
"nativeEnable": "engineEnable",
"nativeEndpointIP": "engineEndpointIP",
"nativeEndpointPort": "engineEndpointPort",
"nativeEnsureContext": "ensureContext",
"nativeFinish": "finishRequest",
"nativeFinishedIndex": "engineFinishedIndex",
"nativeFunction": "engineFunction",
"nativeHandle": "engineHandle",
"nativeHoldUncertainWrites": "engineHoldUncertainWrites",
"nativeIO": "engineIO",
"nativeIP": "engineIP",
"nativeInFlight": "engineInFlight",
"nativeLastSuccessMs": "engineLastSuccessMs",
"nativeMode": "engineMode",
"nativeNowMicroseconds": "engineNowMicroseconds",
"nativeNowMilliseconds": "engineNowMilliseconds",
"nativeOpenCommand": "engineOpenCommand",
"nativePort": "enginePort",
"nativeProcess": "processTransaction",
"nativeReadComplete": "engineReadComplete",
"nativeRelease": "releaseContext",
"nativeReplied": "engineReplied",
"nativeRequestStartMilliseconds": "engineRequestStartMilliseconds",
"nativeResponseSeen": "engineResponseSeen",
"nativeResultReady": "engineResultReady",
"nativeRetryDelayMilliseconds": "engineRetryDelayMilliseconds",
"nativeSelectNextRequest": "selectNextRequest",
"nativeSelected": "engineSelected",
"nativeSent": "engineSent",
"nativeSerialBaud": "engineSerialBaud",
"nativeSerialBits": "engineSerialBits",
"nativeSerialParity": "engineSerialParity",
"nativeSerialRs485": "engineSerialRs485",
"nativeSerialStops": "engineSerialStops",
"nativeStatus": "engineStatus",
"nativeTransactionDataIndex": "engineTransactionDataIndex",
"nativeTransactionQuantity": "engineTransactionQuantity",
"nativeUpdateDiagnostics": "updateDiagnostics",
"nativeUpdateStatistics": "updateStatistics",
"nativeWasConnected": "engineWasConnected",
"nativeWindowCount": "engineWindowCount",
"nativeWindowStartMilliseconds": "engineWindowStartMilliseconds",
"nativeWindowTime": "engineWindowTime",
"nativeWriteHold": "engineWriteHold",
"out_lastNativeError": "out_lastErrorCode",
"out_nativeError": "out_errorCode"
}
+27 -212
View File
@@ -1,220 +1,35 @@
# atMb usage and behavior
# atMb 0.8 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.
## Execution model
## Choosing the block
TCP, RTU and slave transport progress synchronously when the owning client or
slave FB is called. Keep the related client, request FBs and data arrays in one
PLC task and call them every scan. This version intentionally has no native
worker thread, CPU-affinity input, or scan watchdog.
| 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 |
The C state machines use nonblocking sockets and fixed storage. Serial driver
calls remain target-dependent, and a long PLC task interval can delay transport
progress; choose request timeouts accordingly.
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.
## Online change
## TCP read example
`FB_reinit` closes the native context before CODESYS replaces IEC instance memory,
then clears handles, active request pointers and registration arrays. Request FBs
clear in-flight state. TCP/RTU clients and slave listeners reopen on a later PLC
scan. Expect active requests to be cancelled and reads to remain stale until a
validated reply arrives. Writes already transmitted are not replayed.
Declarations:
## Status and recovery
```iecst
VAR
mbClient : atMb.ClientTCP;
readRegisters : atMb.ReqReadHolding;
registers : ARRAY[0..9] OF UINT;
dataUsable : BOOL;
END_VAR
```
Client statistics aggregate all registered requests. `toString` uses
`OK:<successful requests>/Er:<failed requests>`. Three consecutive failed request
results are required before a request is exposed as offline; a short TCP-to-RTU
converter reconnect therefore does not immediately mark every request offline.
A valid response restores online immediately. Reconnect remains automatic
through the synchronous state machine.
Cyclic body:
```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);
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);
dataUsable := readRegisters.out_validData AND NOT readRegisters.out_stale;
```
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.
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:
```iecst
VAR
serialClient : atMb.ClientRTU;
readRegisters : atMb.ReqReadHolding;
registers : ARRAY[0..9] OF UINT;
END_VAR
```
```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);
readRegisters(client := serialClient, in_unitId := 1,
in_startAddr := 0, in_quantity := 10, in_dataOffset := 0,
data := registers);
```
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.
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.
## Polling and priority
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.
`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.
## 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.
## Timeouts and reconnects
Reply timeout precedence:
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.
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.
`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.
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.
## Data quality and statistics
| Diagnostic | Meaning |
|---|---|
| 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 |
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.
`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.
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.
## TCP slave example
```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
```
```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.
```
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.
For TCP-to-RTU converters, set `ClientTCP.in_baudrate` to the converter's
serial baud rate. When no explicit request or client timeout is supplied, the
library estimates timeout from the request/response frame size and baud rate,
then adds turnaround and device-processing margin. Direct TCP clients should
leave `in_baudrate` at zero and use the normal fixed TCP timeout.
+27 -12
View File
@@ -1,14 +1,29 @@
# Cleanup validation
# Validation and online-change 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.
Release versions: IEC library **0.8.0.0**, C component **0.5.0.0**. The external
ABI signature/API version is 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`.
Host validation completed:
- Strict synchronous C build and bridge capacity/pool/handle/shutdown tests.
- Direct and bridge TCP normal, fragmented, timeout, protocol-error, exception,
write and reconnect scenarios.
- RTU pseudo-terminal frame, CRC, timeout, exception, register/coil write and
recovery scenarios.
- TCP slave function-code, exception, address and bind-recovery scenarios.
- XML export parsing, object references, collection metadata and removal of the
threaded Worker object.
- C client `FB_reinit` closes its native context before clearing IEC pointers;
slave `FB_reinit` closes its listener before clearing its handle.
Required on each target before production:
- Rebuild for the exact CPU/toolchain and verify ELF/GLIBC compatibility.
- Compile/import the export in CODESYS and embed the matching C component.
- Test online change while idle, during an active read, during disconnect and
during an active write.
- Verify contexts reopen, request pointers are rebuilt, data becomes valid, and
no stale socket/listener remains after repeated online changes.
Physical target, prolonged soak, and CODESYS IDE qualification were not available
in the host environment. Keep the previous threaded release as rollback only.
+17
View File
@@ -0,0 +1,17 @@
# Synchronous ABI and online-change lifecycle
There is no native worker thread in atMb 0.8. The existing 16-argument
`atMbCall_cext` ABI is retained, and every transport tick runs synchronously in
the PLC task that owns the IEC client or slave instance.
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.
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
PLC scan recreates the context using the current configuration. Slave blocks
close their listener before clearing their native handle.
The external ABI signature/API version remain unchanged. Component version is
0.5.0.0; IEC library version is 0.8.0.0.
Binary file not shown.
Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
atom
ADAM-CODESYS
4648
639270284382256389
File diff suppressed because one or more lines are too long
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""Linux host integration tests for the real dladdr/symlinkat helper; needs cc."""
import ctypes
import os
from pathlib import Path
import subprocess
import sys
import tempfile
HEADER = Path(__file__).resolve().parents[1] / 'component-source/component'
CASES = ['missing', 'correct', 'file', 'wrong_link', 'dangling', 'same_directory', 'unwritable', 'missing_source']
def child(case, base):
base = Path(base)
module = base / 'PlcLogic/libCmpAtomModbus.so'
lib = ctypes.CDLL(str(module), use_errno=True)
lib.warning_count.restype = ctypes.c_int
lib.info_count.restype = ctypes.c_int
os.chdir(base)
link = base / 'libCmpAtomModbus.so'
if case == 'correct':
link.symlink_to('PlcLogic/libCmpAtomModbus.so')
elif case == 'file':
link.write_text('do not replace me')
elif case in ('wrong_link', 'dangling'):
if case == 'wrong_link':
(base / 'other').write_text('other file')
link.symlink_to('other')
elif case == 'same_directory':
os.chdir(module.parent)
elif case == 'missing_source':
module.unlink()
elif case == 'unwritable':
# sysfs cannot accept arbitrary regular symlinks, even as root.
os.chdir('/sys')
link = Path('/sys/libCmpAtomModbus.so')
assert not os.path.lexists(link)
ctypes.set_errno(123)
lib.run_check()
assert ctypes.get_errno() == 123, 'caller errno changed'
warnings = lib.warning_count()
infos = lib.info_count()
lib.run_check()
assert (warnings, infos) == (lib.warning_count(), lib.info_count()), 'retried or logged twice'
if case == 'missing':
assert link.is_symlink() and link.resolve() == module
assert (warnings, infos) == (0, 1)
elif case == 'correct':
assert os.readlink(link) == 'PlcLogic/libCmpAtomModbus.so'
assert (warnings, infos) == (0, 0)
elif case == 'same_directory':
assert module.is_file() and not module.is_symlink()
assert (warnings, infos) == (0, 0)
else:
assert (warnings, infos) == (1, 0)
if case == 'file':
assert link.read_text() == 'do not replace me'
elif case in ('wrong_link', 'dangling'):
assert os.readlink(link) == 'other'
else:
assert not os.path.lexists(link)
print('PASS', case)
if len(sys.argv) == 3:
child(sys.argv[1], sys.argv[2])
else:
with tempfile.TemporaryDirectory(prefix='atmb-link-test-') as temp:
root = Path(temp)
root.chmod(0o755)
harness = root / 'harness.c'
harness.write_text('''#define _GNU_SOURCE
#define syslog test_syslog
#include "atmb_boot_link.h"
static int warnings, infos;
void test_syslog(int priority, const char *format, ...) {
(void)format;
if (priority == LOG_WARNING) ++warnings;
if (priority == LOG_INFO) ++infos;
}
void run_check(void) { atmb_ensure_boot_link(); }
int warning_count(void) { return warnings; }
int info_count(void) { return infos; }
''')
output = root / 'test.so'
subprocess.run([os.environ.get('CC', 'cc'), '-std=c99', '-Wall', '-Wextra', '-Werror',
'-fPIC', '-shared', '-I', str(HEADER), str(harness), '-ldl', '-o', str(output)], check=True)
for case in CASES:
base = root / case
(base / 'PlcLogic').mkdir(parents=True)
(base / 'PlcLogic/libCmpAtomModbus.so').write_bytes(output.read_bytes())
subprocess.run([sys.executable, __file__, case, str(base)], check=True)
base.chmod(0o755)