first commit

This commit is contained in:
2026-09-30 21:17:41 -06:00
commit 07244fc761
57 changed files with 30094 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
# Ready-to-build component, generated interface validated
The supplied CmpAtomModbus.c and CmpAtomModbusItf.m4 were inspected and the C
stub implemented. The M4 file is unchanged. External registration metadata:
signature 0x709EE65B, version 0x00020000 (0.2.0.0).
This source bundle is revision 0.3; the IEC library/component version remains
0.2.0.0 to match your generated interface. Do not change it independently.
## Build
Copy/extract this source folder and your ExtensionSDK onto the Pi:
```text
~/modbus-build/atom-modbus/
~/modbus-build/ExtensionSDK/
```
Then:
```sh
sudo apt-get install build-essential m4 sed dos2unix make
cd ~/modbus-build/atom-modbus
make -C component SDKDIR="$(realpath sdk/ExtensionSDK)" all
file component/out/libCmpAtomModbus.so
nm -D component/out/libCmpAtomModbus.so | sed -n '/ComponentEntry/p; /atmbcall_cext/p'
```
Expected: AArch64 shared object; ComponentEntry and atmbcall_cext exported.
You do NOT need to run the preparation script: component/ is already prepared.
The original generated files are retained in generated/ for reference.
## Install during a bench-test maintenance window
Stop your PLC application safely first. Keep a backup of the configuration.
```sh
sudo cp /etc/CODESYSControl_User.cfg /etc/CODESYSControl_User.cfg.before-atom-modbus
sudo /etc/init.d/codesyscontrol stop
sudo cp component/out/libCmpAtomModbus.so /usr/lib/libCmpAtomModbus.so
sudo nano /etc/CODESYSControl_User.cfg
```
Under the existing [ComponentManager] section add:
```ini
Component.N=CmpAtomModbus
```
Replace N with an unused numeric component index. Do not add a duplicate section
or overwrite another component. Development component ID is 0xffff2001; ensure
it does not collide with another custom component.
```sh
sudo /etc/init.d/codesyscontrol start
sudo tail -n 80 /var/log/codesyscontrol.log
```
Verify the component loads with no interface/signature/version errors. Save and
install your CmpAtomModbus IEC library in CODESYS' Library Repository, then add
it to a separate Raspberry Pi test project's Library Manager.
## Read-only PLC test
Create an ST PROGRAM PLC_PRG. Add the SysTime library. Paste
PLC_PRG_declaration.st in its declaration pane and
PLC_PRG_implementation.st in its implementation pane. Ensure PLC_PRG is called
by one cyclic task, e.g. 10 ms. Namespace-qualify atMbCall_cext if required.
Set the IP, unit ID and zero-based start address for a device with at least ten
readable holding registers. Download, start and set run=TRUE in the watch window.
Inspect status[1] (state), callResult, registers, readDone, successfulReadCount,
dataStale and requestErrorCount. Unplug and restore Ethernet or restart the
device to exercise recovery. status[11] counts additional connection attempts.
Set run=FALSE and let the program scan until handle=0 before stopping,
downloading or making online changes. Automatic application lifecycle cleanup
and the original atMb scheduler migration are still pending; this is a bench
program, not yet a production replacement. RTU is not implemented.
## Verification scope
Native engine/bridge tests passed locally. The real generated parameter struct
and function implementation were compiled against the supplied SDK headers
on the x86-64 development host. Full SDK/M4 registration build and execution
on the ARM64 Pi remain to be verified. No precompiled Pi binary is included.
The ST example has not been compiled in the CODESYS IDE here.
Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
atom
ADAM-CODESYS
5068
639263998361642755
+88
View File
@@ -0,0 +1,88 @@
# Integration into atomLibrary
This package contains a working C TCP core and native C bridge, not a compiled
CODESYS library. Target supplied: Raspberry Pi ARM64, runtime 4.16.0.0,
development environment SP22. ExtensionSDK inspected. See PI_SETUP.md for
the original generated-interface step. Real generated C/M4 files are now
supplied and the stub is implemented; use BUILD_ON_PI.md for the prepared build.
Do not map `am_client` directly to an IEC STRUCT: C int, size_t, alignment and
pointer sizes vary by target. Use SDK-generated external function signatures.
## Implemented bridge API
The separate operations below are implemented as commands through ONE external
function, `atMbCall_cext`. See STATUS.md for command IDs and the status array.
The table describes operations rather than separate exported function names.
The bridge uses a fixed pool of contexts and returns generation-checked UDINT handles.
Zero is invalid. Use functions named with `_cext` for the Control SL Extension SDK:
| Function | Purpose |
|---|---|
| atMbOpen_cext | Allocate a fixed context slot; configure numeric IPv4 and port |
| atMbConfigure_cext | Set connect, response and reconnect times in milliseconds |
| atMbEnable_cext | Enable/disable recovery |
| atMbTick_cext | Advance one nonblocking step using monotonic time |
| atMbSubmit_cext | Validate and copy request/write data into the context |
| atMbGetStatus_cext | Copy explicitly sized status fields to IEC outputs |
| atMbCopyRead_cext | Copy validated read result with destination capacity check |
| atMbReconnect_cext | Cancel active transaction and create a fresh connection |
| atMbClose_cext | Close socket and release pool slot |
Only pass pointers during a synchronous call. Never store pointers into IEC
application memory in C across scans or online changes. All operations for one
handle must run from one task. Add target-specific lifecycle handling for stop,
reset, download and online change; close or invalidate old handles before reuse.
## Map existing function blocks
Keep the existing names and data interfaces, with these internal changes:
- `atMbClientTCP`: own the C handle and call Tick once per scan. Keep
`in_ipAddress`, `in_port`, request registration and priority scheduling.
Changing the endpoint cancels the transaction and opens the new endpoint.
- `atMbClient`: schedule only while Ready and no request is pending. Complete
the prior request and copy its read data BEFORE submitting the next request.
Preserve priority ageing/minimum delay, adding explicit enabled eligibility
and starvation checks. Enforce the existing 100-request registration limit.
- `atMbReqBase`: snapshot parameters when submitted; validate offset plus
quantity against destination capacity. Reject overflow rather than silently
truncate. Quantity zero means disabled at the wrapper level.
- `atMbReadCoil` / `atMbReadDiscrete`: FC01 / FC02; convert unpacked C uint16_t
values to the actual IEC BOOL array type when copying.
- `atMbReadHolding` / `atMbReadInput`: FC03 / FC04, WORD values.
- `atMbWriteCoil` / `atMbWriteHolding`: FC15 / FC16; preserve `writeNow` semantics.
Snapshot write values on submit. A failed write is not automatically replayed;
the application must explicitly decide whether to reissue it.
- `atMbRequestStats`: replace `ModbusFB.Error` with your own transport-error enum;
keep Modbus exception separately. Keep counts, intervals and HMI indicators.
- `atMbClientRTU`: retain as a separate future transport; this C core does not
implement serial RTU framing, CRC, timing or serial-port control.
## Status additions
Client: state, connection attempt count, reconnect count, last transport error,
OS socket error, active request index and last successful response time.
Request: Busy/Done/Error, result, exception, consecutive successes/failures,
last successful read time, hasValidData, dataAgeMs, stale and writeUncertain.
Use connection state for transport readiness. Use per-request successful read
timestamps for freshness; another successful request must not refresh stale
data. A valid exception proves the peer responded but does not validate data.
Keep startup/never-read distinct from stale. The C core exposes a CLIENT-wide
last_success timestamp; per-request timestamps belong in the scheduler wrapper.
## Deployment gate
1. Confirm PLC model, CODESYS runtime version, operating system and architecture.
2. Confirm vendor support for custom C components; Control SL SDK support must
not be assumed on a vendor-supplied PLC runtime.
3. Generate the external declarations/stubs in the matching SDK, implement the
bridge, compile for the actual target and package its component.
4. Compile the IEC wrapper in CODESYS and exercise stop/start and online change.
5. Bench-test unplug/replug, device restart, long outage, endpoint change, full
request list, priority fairness and uncertain writes before site deployment.
References:
- https://content.helpme-codesys.com/en/CODESYS%20Control/_rtsl_extension_extension_sdk.html
- https://www.modbus.org/modbus-specifications
+72
View File
@@ -0,0 +1,72 @@
# Raspberry Pi integration — known target
UPDATE: Real generated C/M4 files have been supplied and component/ is prepared.
Start with BUILD_ON_PI.md; the interface-generation steps below are historical
reference and do not need to be repeated.
User-reported: aarch64, 64-bit userland, codesyscontrol 4.16.0.0;
development environment CODESYS 3.5 SP22. Supplied ExtensionSDK inspected.
The runtime package reports `all`, so verify the actual component/runtime
binary architecture on the Pi after building. No ARM binary is included here.
## Generate the official CODESYS interface
1. Create an **Empty library** named `CmpAtomModbus.library`. In Project
Information set Title = `CmpAtomModbus`, Version = `0.2.0.0`.
2. Add one Function named `atMbCall_cext`, return type UDINT. Paste the matching
declaration from atMbCall_cext.st (the FUNCTION line may already exist in
the declaration editor; do not duplicate it).
3. Right-click the function > Properties > Build > **External implementation**.
4. Build > **Generate Runtime System Files**. Select **M4 interface file** and
**C stub file**. Component name must be `CmpAtomModbus`.
5. Send the generated `CmpAtomModbus.c` and `CmpAtomModbusItf.m4` back for actual
interface validation. The generated signature/checksum must be preserved;
a plain `.so` exported symbol alone is insufficient for the IEC call.
## Prepare and build on Pi after generating those files
Keep your ExtensionSDK directory outside this source folder. Install host build
tools on the Pi if missing:
```sh
sudo apt-get install build-essential m4 sed dos2unix make
```
From the atom-modbus source folder, with the generated files in `generated/`:
```sh
python3 tools/prepare_component.py generated/CmpAtomModbus.c generated/CmpAtomModbusItf.m4 --sdk ../ExtensionSDK
make -C component SDKDIR="$(realpath sdk/ExtensionSDK)" all
file component/out/libCmpAtomModbus.so
```
The last output must identify an AArch64 ELF shared object. Native compilation
on the Pi avoids guessing a cross-toolchain or glibc version. The preparation
script validates field names from the real generated M4 struct and preserves
the generated interface. Its parser has only been exercised with a synthetic
fixture until the real generated files are supplied.
The generated component adds cleanup at `CH_EXIT`, after runtime tasks have
stopped. This does not yet handle application-only lifecycle changes.
## Install only after final review/bench test
The SDK's documented method is to place the component in `/usr/lib`, stop the
runtime and add `Component.N=CmpAtomModbus` under `[ComponentManager]` in
`/etc/CODESYSControl_User.cfg`, choosing an UNUSED component index. Restart the
runtime and inspect `/var/log/codesyscontrol.log`. Use a maintenance
window: a runtime restart stops the PLC application.
Alternatively, package it using the C-Implemented Library object for the exact
Raspberry Pi device target. That packaging still requires CODESYS IDE work.
## First bench exercise
Use a separate test application/task. Open -> configure -> enable -> tick every
scan until Ready. Submit an FC03 read. Tick until Done/Error. On Done, CopyRead,
then Acknowledge. On Error, record diagnostics, Acknowledge and allow recovery.
Do not submit writes during initial connection testing. Use a monotonic
millisecond clock (e.g. SysTimeGetUs value divided by 1000).
Preserve your existing production atomLibrary until the IEC wrapper and
stop/start/download/online-change handling have passed target tests.
+30
View File
@@ -0,0 +1,30 @@
(* Bench test: create PROGRAM PLC_PRG, ST language. Add the SysTime library.
Add CmpAtomModbus as a library; qualify atMbCall_cext with its namespace if
the Library Manager requires it. Paste this in the DECLARATION pane. *)
PROGRAM PLC_PRG
VAR
run : BOOL := FALSE; (* Set TRUE in watch window to start. FALSE closes. *)
ipAddress : DWORD := 16#C0A80132; (* EDIT: 192.168.1.50 *)
tcpPort : UINT := 502;
unitId : BYTE := 1;
startAddress : UINT := 0; (* Zero-based wire address, not 40001. *)
registers : ARRAY[0..9] OF WORD;
status : ARRAY[0..23] OF UDINT;
handle : UDINT := 0; (* Do NOT retain/persist this. *)
command : UDINT := 0;
callResult : UDINT;
timeResult : UDINT;
nowUs : SYSTIME;
nowMs : ULINT;
lastReadMs : ULINT;
readDone : BOOL;
requestError : BOOL;
hasValidData : BOOL;
dataStale : BOOL := TRUE;
lastRequestResult : UDINT;
lastException : UDINT;
lastSocketError : UDINT;
requestErrorCount : UDINT;
successfulReadCount : UDINT;
pollDelay : TON;
END_VAR
+93
View File
@@ -0,0 +1,93 @@
(* Paste in IMPLEMENTATION pane. This only issues FC03 reads.
Leave run FALSE until the component is loaded and target is configured.
Set run FALSE and allow scans to execute before stopping/downloading.
This bench program does not implement automatic disposal on online change. *)
readDone := FALSE;
requestError := FALSE;
timeResult := SysTimeGetUs(nowUs);
IF timeResult <> 0 THEN
RETURN;
END_IF;
nowMs := nowUs / 1000;
dataStale := NOT hasValidData;
IF hasValidData THEN
dataStale := (nowMs - lastReadMs) > 2000;
END_IF;
pollDelay(IN := (command = 5) AND (status[1] = 3) AND (status[2] = 0),
PT := T#200MS);
IF NOT run THEN
IF handle <> 0 THEN
command := 2; (* Close *)
ELSE
command := 0;
END_IF;
ELSIF (handle = 0) AND (command = 0) THEN
command := 1; (* Open *)
END_IF;
IF command = 0 THEN
RETURN;
END_IF;
callResult := atMbCall_cext(
command := command,
handle := handle,
ipAddress := ipAddress,
port := tcpPort,
unitId := unitId,
functionCode := 3,
startAddress := startAddress,
quantity := 10,
pData := ADR(registers[0]),
dataCapacity := 10,
pStatus := ADR(status[0]),
statusCapacity := 24,
nowMs := nowMs,
connectTimeoutMs := 2000,
responseTimeoutMs := 1000,
retryDelayMs := 1000
);
IF callResult <> 0 THEN
IF callResult = 256 THEN (* Stale handle: obtain a fresh context. *)
handle := 0;
command := 0;
ELSIF (command = 6) AND ((callResult = 2) OR (callResult = 3)) THEN
command := 5; (* Busy/offline: return to tick. *)
ELSE
(* Preserve callResult for diagnostics. Turning run off closes context. *)
run := FALSE;
END_IF;
RETURN;
END_IF;
CASE command OF
1: handle := status[0]; command := 9; (* Configure *)
9: command := 3; (* Enable *)
3: command := 5; (* Tick *)
5:
IF status[3] <> 0 THEN
command := 7; (* Copy validated read result before acknowledge. *)
ELSIF status[4] <> 0 THEN
lastRequestResult := status[5];
lastException := status[8];
lastSocketError := status[7];
requestErrorCount := requestErrorCount + 1;
requestError := TRUE;
command := 10;
ELSIF (status[1] = 3) AND pollDelay.Q THEN
command := 6; (* Submit *)
END_IF;
6: command := 5;
7:
lastReadMs := nowMs;
hasValidData := TRUE;
dataStale := FALSE;
readDone := TRUE;
successfulReadCount := successfulReadCount + 1;
command := 10; (* Acknowledge *)
10: command := 5;
2: handle := 0; command := 0; hasValidData := FALSE; dataStale := TRUE;
END_CASE;
+1
View File
@@ -0,0 +1 @@
Save your editable CmpAtomModbus.library and Atom library projects here, including project information and references.
+54
View File
@@ -0,0 +1,54 @@
# Bridge contract
Return: 0 = accepted, 1..10 = am_error values in atom_mb.h.
256 = invalid/stale handle, 257 = pool full, 258 = insufficient/null buffer,
259 = unknown command. Always inspect return codes. A rejected call is not a
completed Modbus request.
Commands: 1 Open, 2 Close, 3 Enable, 4 Disable, 5 Tick, 6 Submit,
7 CopyRead, 8 Reconnect, 9 Configure, 10 Acknowledge,
11 Set request timeout (responseTimeoutMs only; permitted while not busy).
Configure only while disabled; all three timeouts must be nonzero.
Open receives an IPv4 DWORD in human octet order: 16#C0A80132 = 192.168.1.50.
Open returns its handle in status[0], NOT in the function return value.
Close returns status[0]=0; old handles fail even when their slot is reused.
Capacity values count WORD/UDINT elements, not bytes. Pass separate buffers.
CopyRead copies `quantity` WORDs; coils/discretes are unpacked WORD 0/1.
| Status index | Meaning |
|---|---|
| 0 | Handle |
| 1 | State: 0 Disabled, 1 Retry, 2 Connecting, 3 Ready, 4 Sending, 5 Receiving |
| 2 | Busy |
| 3 | Done |
| 4 | Error |
| 5 | Latest transaction result |
| 6 | Last failure, retained after success |
| 7 | OS socket error |
| 8 | Modbus exception |
| 9 | Write outcome uncertain |
| 10 | Connection attempts |
| 11 | Attempts after first attempt |
| 12 | Completed successful requests |
| 13 | Failed requests |
| 14 | Has successful transaction |
| 15 | Last success timestamp, low 32 bits |
| 16 | Last success timestamp, high 32 bits |
| 17 | Quantity of latest request |
| 18 | Function code |
| 19 | Unit ID |
| 20 | Enabled |
| 21 | Connect timeout milliseconds |
| 22 | Whole-request timeout milliseconds |
| 23 | Retry delay milliseconds |
The PLC wrapper consumes Done/Error then Acknowledge before submitting a new
request. For Done reads, call CopyRead BEFORE Acknowledge. Completed read data
is retained internally but CopyRead requires Done. For writes, do not replay
an uncertain result without an application decision.
All calls to the entire bridge pool must originate from ONE IEC task. Contexts
and the pool are not thread-safe. Handles must not be RETAIN/PERSISTENT. Closing
on program disposal and invalidating on application reset/download remain PLC
wrapper responsibilities. Component unload cleanup alone does not cover an IEC
application stop or download.
+23
View File
@@ -0,0 +1,23 @@
(* Create FUNCTION atMbCall_cext returning UDINT, paste this declaration.
Properties > Build > External implementation must be selected.
Library filename and Project Information title: CmpAtomModbus.
This declaration is the SINGLE external entry point for the C engine. *)
FUNCTION atMbCall_cext : UDINT
VAR_INPUT
command : UDINT;
handle : UDINT;
ipAddress : DWORD;
port : UINT;
unitId : BYTE;
functionCode : BYTE;
startAddress : UINT;
quantity : UINT;
pData : POINTER TO WORD;
dataCapacity : UDINT;
pStatus : POINTER TO UDINT;
statusCapacity : UDINT;
nowMs : ULINT;
connectTimeoutMs : UDINT;
responseTimeoutMs : UDINT;
retryDelayMs : UDINT;
END_VAR