Compare commits
4
Commits
fe5dcbe715
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc27b9bb27 | ||
|
|
739347c4da | ||
|
|
c39c226885 | ||
|
|
490c9bdc5b |
@@ -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
|
This release removes the native worker thread and restores TCP, RTU and server
|
||||||
slave operation. Public namespace: `atMb`. No ModbusFB dependency is required.
|
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
|
`FB_reinit` closes native contexts before IEC instance memory is replaced,
|
||||||
Repository and reference it from your application. Keep its project title and
|
clears request pointers and active-request state, and allows contexts to reopen
|
||||||
external C interface settings intact. The development target is Raspberry Pi
|
on subsequent scans.
|
||||||
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.
|
|
||||||
|
|||||||
+26
-18
@@ -1,5 +1,5 @@
|
|||||||
d1192772047955638e8bb171080ed9b37e10683de935e4e0503c93b6d26f03fc .gitattributes
|
d1192772047955638e8bb171080ed9b37e10683de935e4e0503c93b6d26f03fc .gitattributes
|
||||||
02f0eff1a80c9a8f2fe037d1b5980eb391b65e4dc07dba6fe5d109b08c180c1a .gitignore
|
675d431f63e2c212360b692b18de50a344aed5664d17b3683f0b687b96479f24 .gitignore
|
||||||
0ac6e4fe82a82d1bd27ff20dec2f18820c0e0a5057cd0cb1a8020f6dc506603f ExtensionSDK/README
|
0ac6e4fe82a82d1bd27ff20dec2f18820c0e0a5057cd0cb1a8020f6dc506603f ExtensionSDK/README
|
||||||
1cc355e875e572de3829d28bff9c100a4eecbfd741d938654be97881023c84d5 ExtensionSDK/include/CmpErrors.h
|
1cc355e875e572de3829d28bff9c100a4eecbfd741d938654be97881023c84d5 ExtensionSDK/include/CmpErrors.h
|
||||||
2010310fbeb8b4734e638610d2f6dc01b600fe42b8b104155806ef0784e95f2d ExtensionSDK/include/CmpItf.h
|
2010310fbeb8b4734e638610d2f6dc01b600fe42b8b104155806ef0784e95f2d ExtensionSDK/include/CmpItf.h
|
||||||
@@ -10,29 +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
|
||||||
11aee0c6ff01eaff1aac5aa46c470ea8367d5eae655068320869a1bcdce1ef53 README.md
|
bf5b5518674a427f0a5c08f4484b07fc5fb92e2581469420f236358b5c2b6a07 README.md
|
||||||
07527262a8efcf9add5554cfcaed22cf927af63b985e431ab5341078a6864667 artifacts/arm64/libCmpAtomModbus.so
|
2a58de4b824ac7c41e5a192c05f299a08f8a59c95d7c8046722498a073e2f48e artifacts/README.md
|
||||||
14db5bb96e65494eed63da0139b37734dfc08e312a3cb0924ad10b7d9476d442 component-source/Makefile
|
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
|
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
|
||||||
0972948a5cc88892421433fb01875f4f51609cda3b0df57ff58f05c61390e1c2 component-source/component/makefile
|
d891cd5dbaa6e7d891c42de74368ac482461986472b9d1f92af00d611289b1b7 component-source/component/makefile
|
||||||
78240595fa115d3706cbc7316bdcd4e0101a3269e5faacce98826bc32143e9d9 component-source/src/atom_bridge.c
|
ffb81617656709acb3b046a240c03df5ea0d8de9001fa631444b23b714f05036 component-source/src/atom_bridge.c
|
||||||
4a3eaca066d0c06ac7fa95d802ff78b270a1ccdc3fc67c6a072b7e09efba08c0 component-source/src/atom_bridge.h
|
7994895beac2337d1f81216d5971eafa335c950c2050aa6e7cdfc83984955155 component-source/src/atom_bridge.h
|
||||||
4f772a0dab94053bfa2f073f6877ec63a8c6b7999d8e3415160705db441f6068 component-source/src/atom_mb.c
|
115271ae5673cd834fc196e594ff4c1ad0f2a71bb01dc36f713809adeda6efbf component-source/src/atom_mb.c
|
||||||
f8140b5076d5cfb0feda5c9b20d0bc494d7b77edf528f10d96ed071fc4dd0461 component-source/src/atom_mb.h
|
366c879743b51daed51bac909439fc91b908a39ce63ec1d5ef0a8de78cb80bb1 component-source/src/atom_mb.h
|
||||||
e5f8e76efdbc2c4875ceee375f1ea3bd93e1cb4e2075373bb8a0eb4b31ea5e4a 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
|
||||||
771f9671ea8f0644596633970dae1d8651275251d6cdb0a44373a00d064ad35c component-source/src/atom_server.c
|
2555b12b0f1b8dc84847d2a34155f5d496fbb47804a7f27bf094458bed9e3949 component-source/src/atom_server.c
|
||||||
51f3b4a5779347e58b9993b3c2d8f180a73e0a70cb2a8b81bf9350aca8b42bad component-source/src/atom_server.h
|
51f3b4a5779347e58b9993b3c2d8f180a73e0a70cb2a8b81bf9350aca8b42bad component-source/src/atom_server.h
|
||||||
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
|
||||||
2a4b582bed0404f865f4fa2c905dd251c3dccfc4961f20467723c7349793a7f2 component-source/tests/test_native.py
|
|
||||||
0cf8f8fefda6376cacf0c56729d24aaa76241e807b0dee0b36001b3ad3cea509 component-source/tests/test_tcp.py
|
0cf8f8fefda6376cacf0c56729d24aaa76241e807b0dee0b36001b3ad3cea509 component-source/tests/test_tcp.py
|
||||||
ca8eca5c351bcc426676213c162606d1be9ef6a07d017dad74aa10c198e4ea91 docs/API-REFERENCE.md
|
575adda751a14ef74dc515190da8f93c8c2b085a7e4388a9b5363cc442e7bdd8 component-source/tests/test_transport.py
|
||||||
43eb385327aa81deb28a55b08068a9b6e565ad290257d2cbd5819d1ab7558ae3 docs/BUILD-AND-USAGE.md
|
e409f8f786c5d4fc6f91230dc2ad6baa1af3388ec74e1a13a6011634f36d430e docs/API-REFERENCE.md
|
||||||
a0931348b32a336798b7d93a5956fd14bd481597626bc1d40b1597ee6ed5e6a3 docs/CLEANUP.md
|
91e5a0a3597d214832ca2acf02a72bfe8678268fc7b88aba1cca4a9336087f56 docs/BUILD-AND-USAGE.md
|
||||||
ef14901695852012e92ae7dde304244e441c17f2bc17abd4fb7e7a2e30f2e9df docs/USER-GUIDE.md
|
dc4b312826bba713d389d86cd35fd01d4ea223c3f084556f9ff2064532a4dcee docs/CLEANUP.md
|
||||||
b35e03e6cd3e23719794802a65663ddc22986731dd52f6713ab71b94d1798f53 docs/VALIDATION.md
|
d60f5881289ac66cf5f6e4a90c5c32beaded3426a385825fb3d1750ec758dee1 docs/RENAMED-MEMBERS.json
|
||||||
be0edea894f02a6f525307da69fd52498d4ff76e4b95728608ff6db8f6877e39 library/CmpAtomModbus.library
|
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
|
||||||
|
|||||||
@@ -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.
|
||||||
Regular → Executable
BIN
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -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.
@@ -5,7 +5,7 @@ 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_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
|
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 $@
|
$(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
|
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
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
#ifndef _GNU_SOURCE
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#endif
|
||||||
|
#include "atmb_boot_link.h"
|
||||||
#include "../src/atom_bridge.h"
|
#include "../src/atom_bridge.h"
|
||||||
/*****************************************************************************
|
/*****************************************************************************
|
||||||
* Copyright: Copyright CODESYS Development GmbH
|
* Copyright: Copyright CODESYS Development GmbH
|
||||||
@@ -38,6 +42,9 @@ DLL_DECL int CDECL ComponentEntry(INIT_STRUCT *pInitStruct)
|
|||||||
s_pfCMCallHook = pInitStruct->pfCMCallHook;
|
s_pfCMCallHook = pInitStruct->pfCMCallHook;
|
||||||
s_pfCMRegisterClass = pInitStruct->pfCMRegisterClass;
|
s_pfCMRegisterClass = pInitStruct->pfCMRegisterClass;
|
||||||
s_pfCMCreateInstance = pInitStruct->pfCMCreateInstance;
|
s_pfCMCreateInstance = pInitStruct->pfCMCreateInstance;
|
||||||
|
|
||||||
|
/* Best effort only: never make component startup depend on this. */
|
||||||
|
atmb_ensure_boot_link();
|
||||||
return ERR_OK;
|
return ERR_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Generated from supplied ExtensionSDK; native build on ARM64 Pi.
|
# Generated from supplied ExtensionSDK; build with target Linux toolchain.
|
||||||
SDKDIR ?= ../../ExtensionSDK
|
SDKDIR ?= ../../ExtensionSDK
|
||||||
MAJORVER=0
|
MAJORVER=0
|
||||||
MINORVER=3
|
MINORVER=5
|
||||||
CMPVERSION=0x00030000
|
CMPVERSION=0x00050001
|
||||||
# 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,4 +10,9 @@ 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
|
||||||
|
# dladdr locates this module; -ldl also supports glibc older than 2.34.
|
||||||
|
LDLIBS += -ldl
|
||||||
include $(SDKDIR)/makefile
|
include $(SDKDIR)/makefile
|
||||||
|
|
||||||
|
# Recompile the startup hook when the link helper changes.
|
||||||
|
$(OUTDIR)/CmpFrame.o: atmb_boot_link.h
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ static void snapshot(const am_client *c,uint32_t h,uint32_t *s) {
|
|||||||
s[18]=c->function; s[19]=c->unit; s[20]=(uint32_t)c->enabled;
|
s[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;
|
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,
|
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) {
|
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;
|
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; }
|
else { s->c.connect_timeout_ms=ct; s->c.response_timeout_ms=rt; s->c.retry_delay_ms=retry; }
|
||||||
break;
|
break;
|
||||||
case AB_SUBMIT:
|
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;
|
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));
|
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;
|
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 if(!data || cap<s->c.quantity) result=AB_BAD_CAPACITY;
|
||||||
else memcpy(data,s->c.data,s->c.quantity*sizeof(*data));
|
else memcpy(data,s->c.data,s->c.quantity*sizeof(*data));
|
||||||
break;
|
break;
|
||||||
|
case 12: break;
|
||||||
case AB_ACK: s->c.done=0; s->c.error=0; break;
|
case AB_ACK: s->c.done=0; s->c.error=0; break;
|
||||||
case AB_REQUEST_TIMEOUT:
|
case AB_REQUEST_TIMEOUT:
|
||||||
if(s->c.busy) result=AM_BUSY;
|
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;
|
break;
|
||||||
default: result=AB_BAD_COMMAND; 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;
|
||||||
}
|
}
|
||||||
void ab_shutdown(void) {
|
return result;
|
||||||
|
}
|
||||||
|
void ab_close_all(void) {
|
||||||
as_shutdown();
|
as_shutdown();
|
||||||
unsigned i;
|
unsigned i;
|
||||||
for(i=0;i<AM_POOL_SIZE;i++) if(slots[i].used) {
|
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;
|
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,7 +1,7 @@
|
|||||||
#ifndef ATOM_BRIDGE_H
|
#ifndef ATOM_BRIDGE_H
|
||||||
#define ATOM_BRIDGE_H
|
#define ATOM_BRIDGE_H
|
||||||
#include <stdint.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. */
|
* 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
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/tcp.h>
|
||||||
#include <sys/ioctl.h>
|
#include <sys/ioctl.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#ifndef MSG_NOSIGNAL
|
#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 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 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 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) {
|
static void finish(am_client *c,am_error e,uint64_t now) {
|
||||||
c->busy=0; c->done=e==AM_OK; c->error=e!=AM_OK; c->result=e;
|
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; }
|
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) {
|
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->last_error=e; drop(c); c->state=c->enabled?AM_RETRY:AM_DISABLED;
|
||||||
c->deadline_ms=now+c->retry_delay_ms;
|
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));
|
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->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->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;
|
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;
|
unsigned i; size_t bytes;
|
||||||
if(c->rx[7]==(uint8_t)(c->function|0x80)) {
|
if(c->rx[7]==(uint8_t)(c->function|0x80)) {
|
||||||
if(c->rx_need!=9 || !c->rx[8]) { fault(c,AM_PROTOCOL,now); return; }
|
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(c->rx[7]!=c->function) { fault(c,AM_PROTOCOL,now); return; }
|
||||||
if(writing(c->function)) {
|
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?
|
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);
|
(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) {
|
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;
|
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(c->state==AM_RETRY) {
|
||||||
if(now<c->deadline_ms) return;
|
if(now<c->deadline_ms) return;
|
||||||
c->connect_attempts++; if(c->connect_attempts>1) c->reconnects++;
|
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; }
|
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);
|
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; }
|
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);
|
memset(&a,0,sizeof(a)); a.sin_family=AF_INET; a.sin_port=htons(c->port);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ typedef struct {
|
|||||||
uint8_t exception, unit, function;
|
uint8_t exception, unit, function;
|
||||||
uint16_t transaction, address, quantity;
|
uint16_t transaction, address, quantity;
|
||||||
uint32_t connect_timeout_ms, response_timeout_ms, retry_delay_ms;
|
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;
|
uint32_t connect_attempts, reconnects, completed, failed;
|
||||||
int busy, done, error, write_uncertain, has_success;
|
int busy, done, error, write_uncertain, has_success;
|
||||||
char ip[16]; uint16_t port;
|
char ip[16]; uint16_t port;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ int ar_init(am_client *c,ar_transport *r,const char *path,uint32_t baud,uint8_t
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
static void ar_fault(am_client *c,ar_transport *r,am_error error,uint64_t now) {
|
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->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;
|
c->deadline_ms=now+c->retry_delay_ms;r->quiet_until=c->deadline_ms;r->rx_len=0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
#endif
|
#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 fd;uint8_t rx[260],tx[260];size_t received,needed,sent,length;uint64_t deadline; } as_peer;
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int used,enabled,listener,running,error,os_error;uint32_t generation,received,replied,exceptions,retry;
|
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;
|
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_peer peers[AS_PEERS];
|
||||||
} as_server;
|
} as_server;
|
||||||
static as_server as_slots[AS_POOL];
|
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;
|
table=f==5?0:2;
|
||||||
if(p->needed!=12 || (f==5 && q!=0 && q!=0xff00)){exception=3;break;}
|
if(p->needed!=12 || (f==5 && q!=0 && q!=0xff00)){exception=3;break;}
|
||||||
if(!as_range(s,table,address,1)){exception=2;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:
|
case 15:case 16:
|
||||||
table=f==15?0:2;
|
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(!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;}
|
if(!as_range(s,table,address,q)){exception=2;break;}
|
||||||
index=address-s->base[table];
|
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;
|
memcpy(p->tx+8,p->rx+8,4);n=12;break;
|
||||||
default:exception=1;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;
|
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;
|
for(i=0;i<AS_POOL;i++)if(!as_slots[i].used && as_slots[i].generation<0xffffffu)break;
|
||||||
if(i==AS_POOL)return 257;
|
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]);}
|
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;
|
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];
|
s=&as_slots[i-1];
|
||||||
switch(cmd){
|
switch(cmd){
|
||||||
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;break;
|
case AS_ENABLE:s->enabled=1;s->suspended=0;s->heartbeat=now;break;
|
||||||
case AS_DISABLE:s->enabled=0;as_stop(s);break;
|
case AS_DISABLE:s->suspended=0;s->enabled=0;as_stop(s);break;
|
||||||
case AS_TICK:as_tick(s,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;
|
||||||
else if(cmd==AS_PUSH){
|
else if(cmd==AS_PUSH){
|
||||||
if(table<2){for(unsigned j=0;j<q;j++)if(data[j]>1){result=AM_ARGUMENT;break;}}
|
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 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;
|
break;
|
||||||
default:result=259;break;
|
default:result=259;break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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
|
import ctypes as C,os,pty,select,socket,struct,subprocess,time,tempfile
|
||||||
from pathlib import Path
|
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-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)
|
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
|
||||||
@@ -70,13 +70,19 @@ with tempfile.TemporaryDirectory() as tmp:
|
|||||||
rtu_scenario('normal',16);print('PASS RTU register write')
|
rtu_scenario('normal',16);print('PASS RTU register write')
|
||||||
rtu_scenario('echo',16);print('PASS RTU invalid echo and reopen recovery')
|
rtu_scenario('echo',16);print('PASS RTU invalid echo and reopen recovery')
|
||||||
rtu_scenario('normal',15);print('PASS RTU coil write')
|
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()
|
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]
|
assert call(30,ip=0x7f000001,port=port)==0;h=status[0]
|
||||||
for table in range(4):
|
for table in range(4):
|
||||||
for i in range(16):data[i]=i%2 if table<2 else 100+i
|
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(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)
|
peer=socket.create_connection(('127.0.0.1',port));peer.setblocking(False)
|
||||||
tid=0
|
tid=0
|
||||||
def exchange(pdu):
|
def exchange(pdu):
|
||||||
@@ -108,7 +114,19 @@ with tempfile.TemporaryDirectory() as tmp:
|
|||||||
assert call(31,h)==0;assert call(34,h)==256;peer.close()
|
assert call(31,h)==0;assert call(34,h)==256;peer.close()
|
||||||
# Listener bind error recovers after the conflicting listener disappears.
|
# 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()
|
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
|
assert call(30,ip=0x7f000001,port=port)==0;h=status[0];call(32,h)
|
||||||
blocker.close();call(34,h,when=now()+100);assert status[1]==1 and status[2]==0
|
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')
|
call(31,h);print('PASS slave listener recovery and stale handles')
|
||||||
lib.ab_shutdown()
|
lib.ab_shutdown()
|
||||||
+475
-524
File diff suppressed because it is too large
Load Diff
+21
-56
@@ -1,68 +1,33 @@
|
|||||||
# Build and deployment
|
# Build and upgrade to atMb 0.8
|
||||||
|
|
||||||
## Build native C
|
Build from the repository root and clean between architectures:
|
||||||
|
|
||||||
From the repository root on the target Linux machine:
|
|
||||||
|
|
||||||
```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
|
||||||
file component-source/component/out/libCmpAtomModbus.so
|
|
||||||
make -C component-source test
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`component-source/component/` is the active SDK build directory. Its
|
For the UR2000 ARMv7 hard-float target:
|
||||||
`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.
|
|
||||||
|
|
||||||
The makefile registers component version 0.3.0.0. The supplied external
|
```sh
|
||||||
`atMbCall_cext` API retains signature 0x709EE65B and API version 0.2.0.0.
|
make -C component-source/component SDKDIR="$(realpath ExtensionSDK)" clean
|
||||||
Do not change the external parameter layout or regenerate it just because the
|
make -C component-source/component \
|
||||||
library title/version was edited. Full SDK/M4 builds must be checked on the target.
|
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.
|
## CODESYS and online change
|
||||||
2. Retain Project Information title `CmpAtomModbus` and namespace `atMb`.
|
|
||||||
3. Retain the Standard and SysTime references required by the IEC code.
|
|
||||||
4. In its C Implemented Library object, update the native binary for the exact
|
|
||||||
device target with the newly built `libCmpAtomModbus.so`. Use the component
|
|
||||||
naming/settings already present in the working project.
|
|
||||||
5. Compile the library, save/install it in Library Repository and update application
|
|
||||||
references. Test with a full download for native component changes.
|
|
||||||
|
|
||||||
The working `.library` is preserved unchanged in this cleanup. Updating a separate
|
1. Create a fresh library project and import `library/atMb-OnlineChange.export`.
|
||||||
`.so` file does not automatically update the binary embedded inside the library.
|
2. Verify Project Information version **0.8.0.0**, namespace `atMb`, and the
|
||||||
The standalone artifact is retained for deployment/build comparison; rebuilding it
|
existing Standard/SysTime references.
|
||||||
does not imply it is identical to the currently embedded implementation.
|
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
|
The external ABI signature/API version is unchanged. There is no worker FB,
|
||||||
|
thread-affinity setting, or native worker thread in this release.
|
||||||
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.
|
|
||||||
|
|||||||
+6
-23
@@ -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
|
No Git commit or push was made. Build outputs and IDE cache files are omitted
|
||||||
from `component-source/component/`.
|
from the release archive.
|
||||||
- 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.
|
|
||||||
|
|||||||
@@ -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
@@ -1,220 +1,35 @@
|
|||||||
# atMb usage and behavior
|
# atMb 0.8 user guide
|
||||||
|
|
||||||
This guide describes the last supplied working IEC export, including `ClientTCP`.
|
## Execution model
|
||||||
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.
|
|
||||||
|
|
||||||
## 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 |
|
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
|
||||||
| TCP master/client | `atMb.ClientTCP` | IPv4 byte array and TCP port |
|
progress; choose request timeouts accordingly.
|
||||||
| 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 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
|
Client statistics aggregate all registered requests. `toString` uses
|
||||||
VAR
|
`OK:<successful requests>/Er:<failed requests>`. Three consecutive failed request
|
||||||
mbClient : atMb.ClientTCP;
|
results are required before a request is exposed as offline; a short TCP-to-RTU
|
||||||
readRegisters : atMb.ReqReadHolding;
|
converter reconnect therefore does not immediately mark every request offline.
|
||||||
registers : ARRAY[0..9] OF UINT;
|
A valid response restores online immediately. Reconnect remains automatic
|
||||||
dataUsable : BOOL;
|
through the synchronous state machine.
|
||||||
END_VAR
|
|
||||||
```
|
|
||||||
|
|
||||||
Cyclic body:
|
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
|
||||||
```iecst
|
library estimates timeout from the request/response frame size and baud rate,
|
||||||
mbClient(
|
then adds turnaround and device-processing margin. Direct TCP clients should
|
||||||
in_ipAddress := atMb.To_IPAddress(192,168,50,10),
|
leave `in_baudrate` at zero and use the normal fixed TCP timeout.
|
||||||
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.
|
|
||||||
|
|||||||
+27
-12
@@ -1,14 +1,29 @@
|
|||||||
# Cleanup validation
|
# Validation and online-change qualification
|
||||||
|
|
||||||
- Working `.library`, standalone ARM64 `.so`, active C/component sources, tests,
|
Release versions: IEC library **0.8.0.0**, C component **0.5.0.0**. The external
|
||||||
SDK and all Git metadata compared byte-for-byte with the uploaded ZIP: unchanged.
|
ABI signature/API version is 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.
|
|
||||||
|
|
||||||
No CODESYS compile or full ARM64 SDK build was run. Physical RTU operation and
|
Host validation completed:
|
||||||
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`.
|
- 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.
|
||||||
|
|||||||
@@ -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.
@@ -0,0 +1,4 @@
|
|||||||
|
atom
|
||||||
|
ADAM-CODESYS
|
||||||
|
4648
|
||||||
|
639270284382256389
|
||||||
File diff suppressed because one or more lines are too long
@@ -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)
|
||||||
Reference in New Issue
Block a user