first commit
This commit is contained in:
@@ -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.
@@ -0,0 +1,4 @@
|
||||
atom
|
||||
ADAM-CODESYS
|
||||
5068
|
||||
639263998361642755
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
Save your editable CmpAtomModbus.library and Atom library projects here, including project information and references.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user