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
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
target="${1:?Usage: build-linux.sh linux-aarch64|linux-armhf|linux-x86_64}"
case "$target" in linux-aarch64|linux-armhf|linux-x86_64) ;; *) echo "Unsupported target label" >&2; exit 2;; esac
sdk="${SDKDIR:-$root/sdk/ExtensionSDK}"
compiler="${CC:-gcc}"
for tool in "$compiler" make m4 dos2unix python3 file; do command -v "$tool" >/dev/null; done
sdk="$(realpath "$sdk")"
make -C "$root/component" SDKDIR="$sdk" CC="$compiler" clean
make -C "$root/component" SDKDIR="$sdk" CC="$compiler" all
binary="$root/component/out/libCmpAtomModbus.so"
identity="$(file -b "$binary")"
case "$target" in
linux-aarch64) [[ "$identity" == *aarch64* ]] ;;
linux-armhf) [[ "$identity" == *"ELF 32-bit"* && "$identity" == *ARM* ]] ;;
linux-x86_64) [[ "$identity" == *x86-64* ]] ;;
esac || { echo "Binary architecture does not match $target: $identity" >&2; exit 3; }
destination="$root/dist/0.2.0/$target"
mkdir -p "$destination"
cp "$binary" "$destination/"
{
date -u '+Build UTC: %Y-%m-%dT%H:%M:%SZ'
uname -a
"$compiler" --version
file "$binary"
if command -v getconf >/dev/null; then getconf GNU_LIBC_VERSION || true; fi
if command -v git >/dev/null; then git -C "$root" rev-parse HEAD 2>/dev/null || true; fi
printf 'SDK file hashes:\n'
find "$sdk" -type f -exec sha256sum {} +
printf 'Record tested runtime, target OS/libc and cross sysroot here before release.\n'
} > "$destination/build-info.txt"
sha256sum "$destination/libCmpAtomModbus.so" > "$destination/SHA256SUMS.txt"
printf 'Built: %s\n' "$destination/libCmpAtomModbus.so"
+542
View File
@@ -0,0 +1,542 @@
"""Patch original native CODESYS export; keep existing public declarations/IDs."""
from pathlib import Path
import xml.etree.ElementTree as E
import copy, uuid
ROOT=Path(__file__).resolve().parents[1]
r=E.parse(ROOT/'compat/Original_HomeCodesys.export').getroot()
listing=r.find('.//List2[@Name="EntryList"]')
entries=list(listing)
def meta(e,n): return e.find('./Single[@Name="MetaObject"]/Single[@Name="'+n+'"]')
names={meta(e,'Guid').text:meta(e,'Name').text for e in entries}
def key(e):
p=e.find('./Single[@Name="ParentSVNodeGuid"]')
return (names.get(p.text,''),meta(e,'Name').text)
by={key(e):e for e in entries}
def node(parent,name): return by[(parent,name)]
def textnode(e,part): return e.find('./Single[@Name="Object"]/Single[@Name="'+part+'"]//Single[@Name="TextBlobForSerialisation"]')
def get(parent,name,part='Implementation'): return textnode(node(parent,name),part).text or ''
def put(parent,name,body,part='Implementation'): textnode(node(parent,name),part).text=body
def append_vars(parent,name,body,kind='VAR'):
put(parent,name,get(parent,name,'Interface')+'\n'+kind+'\n'+body+'\nEND_VAR\n','Interface')
def method(parent,name,decl,body):
e=copy.deepcopy(node('atMbClient','resetStats'))
old=meta(e,'Guid').text; new=str(uuid.uuid5(uuid.NAMESPACE_URL,'atom-modbus/'+parent+'/'+name))
pg=meta(node('ModbusMaster',parent),'Guid').text
for v in e.iter():
if v.text and old in v.text:v.text=v.text.replace(old,new)
meta(e,'Name').text=name; meta(e,'ParentGuid').text=pg
e.find('./Single[@Name="ParentSVNodeGuid"]').text=pg
path=e.find('./Array[@Name="Path"]'); path[-1].text=parent
textnode(e,'Interface').text=decl; textnode(e,'Implementation').text=body
listing.append(e); by[(parent,name)]=e
append_vars('ModbusMaster','atMbClient','''nativeMode : BOOL;
nativeHandle : UDINT;
nativeIP : DWORD;
nativePort : UINT;
nativeNowUs : SYSTIME;
nativeNowMs : ULINT;
nativeStatus : ARRAY[0..23] OF UDINT;
nativeIO : ARRAY[0..1999] OF WORD;
nativeActiveRequest : POINTER TO atMbReqBase;
nativeActiveIndex : DINT := -1;
nativeSelected : POINTER TO atMbReqBase;
nativeResultReady : BOOL;
nativeFinishedIndex : DINT := -1;
nativeArgUnit : BYTE;
nativeArgFunction : BYTE;
nativeArgAddress : UINT;
nativeArgQuantity : UINT;
nativeArgTimeout : UDINT := 1000;
nativeCallResult : UDINT;
nativeSent : UDINT;
nativeReplied : UDINT;
nativeWindowStart : ULINT;
nativeWindowCount : UDINT;
nativeWindowTime : UDINT;
nativePrevConnected : BOOL;
nativeEnable : BOOL := TRUE;
nativeConnectTimeout : UDINT := 2000;
nativeRetryDelay : UDINT := 1000;
nativeHoldUncertainWrites : BOOL := FALSE;''')
# CODESYS permits another FB to assign inputs, but not local VAR members.
# Match the original stats pattern for the shared scheduler working fields.
e=node('ModbusMaster','atMbClient')
decl=textnode(e,'Interface').text
shared=['nativeActiveRequest','nativeIO','nativeArgUnit','nativeArgFunction',
'nativeArgAddress','nativeArgQuantity','nativeArgTimeout','nativeSent',
'nativeReplied','nativeWindowCount','nativeWindowTime']
lines=decl.splitlines(); moved=[]; kept=[]
for line in lines:
if any(line.strip().startswith(n+' :') for n in shared): moved.append(line)
else: kept.append(line)
textnode(e,'Interface').text='\n'.join(kept)+'\nVAR_INPUT\n'+''.join(
"{attribute 'symbol' := 'none'}\n"+line+'\n' for line in moved)+'END_VAR\n'
put('atMbClient','setAllRequestsOffline',
get('atMbClient','setAllRequestsOffline','Interface').replace('METHOD PRIVATE','METHOD PROTECTED'),
'Interface')
append_vars('ModbusMaster','atMbClient','''out_connected : BOOL;
out_connectionState : UDINT;
out_lastNativeError : UDINT;
out_socketError : UDINT;
out_reconnectCount : UDINT;
out_registrationFull : BOOL;''','VAR_OUTPUT')
method('atMbClient','nativeCall','''METHOD nativeCall : UDINT
VAR_INPUT
command : UDINT;
END_VAR''','''nativeCall := CmpAtomModbus.atMbCall_cext(
command := command, handle := nativeHandle, ipAddress := nativeIP,
port := nativePort, unitId := nativeArgUnit, functionCode := nativeArgFunction,
startAddress := nativeArgAddress, quantity := nativeArgQuantity,
pData := ADR(nativeIO[0]), dataCapacity := 2000,
pStatus := ADR(nativeStatus[0]), statusCapacity := 24, nowMs := nativeNowMs,
connectTimeoutMs := nativeConnectTimeout, responseTimeoutMs := nativeArgTimeout,
retryDelayMs := nativeRetryDelay);''')
method('atMbClient','nativeCycle','''METHOD nativeCycle
VAR
j : DINT;
best : DINT;
priority : INT;
age : UDINT;
rc : UDINT;
END_VAR''','''SysTimeGetUs(nativeNowUs);
nativeNowMs := nativeNowUs / 1000;
IF in_resetStats THEN
resetStats();
in_resetStats := FALSE;
END_IF;
IF nativeHandle = 0 AND nativeEnable THEN
rc := nativeCall(1);
IF rc <> 0 THEN
out_lastNativeError := rc;
RETURN;
END_IF;
nativeHandle := nativeStatus[0];
nativeArgTimeout := 1000;
rc := nativeCall(9);
IF rc <> 0 THEN
out_lastNativeError := rc;
nativeCall(2); nativeHandle := 0;
RETURN;
END_IF;
nativeCall(3);
END_IF;
IF nativeHandle = 0 THEN
out_connected := FALSE;
out_connectionState := 0;
RETURN;
END_IF;
rc := nativeCall(5);
IF rc = 256 THEN
nativeHandle := 0;
IF nativeActiveRequest <> 0 THEN
nativeActiveRequest^.nativeFinish(10);
END_IF;
nativeActiveRequest := 0; nativeActiveIndex := -1;
RETURN;
ELSIF rc <> 0 THEN
out_lastNativeError := rc;
RETURN;
END_IF;
out_connectionState := nativeStatus[1];
out_connected := (nativeStatus[1] >= 3) AND (nativeStatus[1] <= 5);
out_lastNativeError := nativeStatus[6];
out_socketError := nativeStatus[7];
out_reconnectCount := nativeStatus[11];
IF nativePrevConnected AND NOT out_connected THEN
setAllRequestsOffline();
END_IF;
nativePrevConnected := out_connected;
nativeFinishedIndex := -1;
nativeResultReady := (nativeActiveRequest <> 0) AND
((nativeStatus[3] <> 0) OR (nativeStatus[4] <> 0));
IF nativeResultReady AND (nativeStatus[3] <> 0) AND (nativeStatus[18] <= 4) THEN
rc := nativeCall(7);
IF rc <> 0 THEN
nativeStatus[3] := 0;
nativeStatus[4] := 1;
nativeStatus[5] := rc;
END_IF;
END_IF;
processAllRequests();
IF nativeResultReady THEN
nativeFinishedIndex := nativeActiveIndex;
nativeCall(10);
nativeActiveRequest := 0; nativeActiveIndex := -1;
nativeSelected := 0; nativeResultReady := FALSE;
END_IF;
IF nativeEnable AND nativeStatus[1] = 3 AND nativeActiveRequest = 0 THEN
best := -1; priority := 32767; age := 0;
FOR j := 0 TO UINT_TO_DINT(requestCnt)-1 DO
IF requests[j] <> 0 AND_THEN requests[j]^.stats.active
AND NOT requests[j]^.nativeWriteHold AND j <> nativeFinishedIndex THEN
IF requests[j]^.stats.sinceLastExec >= TIME_TO_UDINT(requests[j]^.in_minDelay) THEN
IF (requests[j]^.in_priorityCurrent < priority) OR
((requests[j]^.in_priorityCurrent = priority) AND
(requests[j]^.stats.sinceLastExec > age)) THEN
best := j; priority := requests[j]^.in_priorityCurrent;
age := requests[j]^.stats.sinceLastExec;
END_IF;
END_IF;
END_IF;
END_FOR;
IF best >= 0 THEN
IF priority > 0 THEN
FOR j := 0 TO UINT_TO_DINT(requestCnt)-1 DO
IF requests[j]^.in_priorityCurrent > 0 THEN
requests[j]^.in_priorityCurrent := requests[j]^.in_priorityCurrent - 1;
END_IF;
END_FOR;
END_IF;
nativeSelected := requests[best]; nativeActiveIndex := best;
nativeSelected^.stats.execute := TRUE;
nativeSelected^.processRequest();
IF nativeActiveRequest = 0 THEN
nativeActiveIndex := -1; nativeSelected := 0;
END_IF;
END_IF;
END_IF;
allRequestsOnline := (activeReqCnt > 0) AND (onlineCnt = activeReqCnt);
allRequestsOffline := (activeReqCnt > 0) AND (onlineCnt = 0);
numMsgSent := nativeSent; numMsgReply := nativeReplied;
FOR j := 0 TO UINT_TO_DINT(requestCnt)-1 DO
reqStats[j] := ADR(requests[j]^.stats);
END_FOR;
IF nativeWindowStart = 0 THEN nativeWindowStart := nativeNowMs; END_IF;
IF nativeNowMs - nativeWindowStart >= 1000 THEN
msgLastSecondCnt := nativeWindowCount; msgLastSecondTime := nativeWindowTime;
nativeWindowCount := 0; nativeWindowTime := 0; nativeWindowStart := nativeNowMs;
END_IF;''')
put('ModbusMaster','atMbClient','''IF nativeMode THEN
nativeCycle();
RETURN;
END_IF;
'''+get('ModbusMaster','atMbClient'))
# RTU retains old code and public compatibility methods.
put('atMbClient','resetStats','''IF nativeMode THEN
nativeSent := 0; nativeReplied := 0;
nativeWindowCount := 0; nativeWindowTime := 0;
msgLastSecondCnt := 0; msgLastSecondTime := 0;
numMsgSent := 0; numMsgReply := 0;
FOR i := 0 TO UINT_TO_DINT(requestCnt)-1 DO
requests[i]^.stats.resetStats();
END_FOR;
RETURN;
END_IF;
'''+get('atMbClient','resetStats'))
put('atMbClient','addRequest','''IF request = 0 THEN RETURN; END_IF;
IF requestCnt > 0 THEN
FOR i := 0 TO requestCnt-1 DO
IF request = requests[i] THEN RETURN; END_IF;
END_FOR;
END_IF;
IF requestCnt >= 100 THEN
out_registrationFull := TRUE;
RETURN;
END_IF;
requests[requestCnt] := request;
requestCnt := requestCnt + 1;''')
put('activeIndex','Get','''IF nativeMode THEN
activeIndex := nativeActiveIndex;
RETURN;
END_IF;
'''+get('activeIndex','Get'))
# TCP declarations: existing inputs stay unchanged, extra diagnostics inherited.
append_vars('ModbusMaster','atMbClientTCP','''in_enable : BOOL := TRUE;
in_connectTimeout : TIME := T#2S;
in_reconnectDelay : TIME := T#1S;
in_holdUncertainWrites : BOOL := FALSE;''','VAR_INPUT')
append_vars('ModbusMaster','atMbClientTCP','''nativeEndpointIP : DWORD;
nativeEndpointPort : UINT;
nativeConfigConnect : UDINT;
nativeConfigRetry : UDINT;''')
put('ModbusMaster','atMbClientTCP','''nativeMode := TRUE;
nativeIP := SHL(BYTE_TO_DWORD(in_ipAddress[0]),24) OR
SHL(BYTE_TO_DWORD(in_ipAddress[1]),16) OR
SHL(BYTE_TO_DWORD(in_ipAddress[2]),8) OR BYTE_TO_DWORD(in_ipAddress[3]);
nativePort := in_port;
nativeConnectTimeout := MAX(UDINT#1,TIME_TO_UDINT(in_connectTimeout));
nativeRetryDelay := MAX(UDINT#1,TIME_TO_UDINT(in_reconnectDelay));
nativeHoldUncertainWrites := in_holdUncertainWrites;
IF nativeHandle <> 0 AND ((nativeEndpointIP <> nativeIP) OR
(nativeEndpointPort <> nativePort) OR (nativeConfigConnect <> nativeConnectTimeout) OR
(nativeConfigRetry <> nativeRetryDelay) OR NOT in_enable) THEN
nativeRelease();
END_IF;
nativeEndpointIP := nativeIP; nativeEndpointPort := nativePort;
nativeConfigConnect := nativeConnectTimeout; nativeConfigRetry := nativeRetryDelay;
nativeEnable := in_enable;
IF in_baudrate <> 0 THEN dataRate := in_baudrate; ELSE dataRate := 105200; END_IF;
SUPER^();''')
put('atMbClientTCP','recreateClient','''nativeRelease();
// Next scan creates a fresh native context with current endpoint settings.''')
# Remove the old local pointer used by the recreated built-in TCP client.
put('atMbClientTCP','recreateClient','METHOD recreateClient\nVAR\nEND_VAR','Interface')
method('atMbClientTCP','nativeRelease','''METHOD nativeRelease : BOOL
VAR
j : DINT;
END_VAR''','''IF nativeHandle <> 0 THEN
nativeCall(2);
IF nativeActiveRequest <> 0 THEN
nativeActiveRequest^.nativeFinish(10);
END_IF;
nativeHandle := 0;
END_IF;
nativeActiveRequest := 0; nativeSelected := 0; nativeActiveIndex := -1;
nativeResultReady := FALSE;
out_connected := FALSE; out_connectionState := 0;
setAllRequestsOffline();
FOR j := 0 TO UINT_TO_DINT(requestCnt)-1 DO
requests[j]^.invalidateNativeData();
END_FOR;
nativeRelease := TRUE;''')
method('atMbClientTCP','release','''METHOD release : BOOL''','''in_enable := FALSE;
release := nativeRelease();''')
method('atMbClientTCP','FB_exit','''METHOD FB_exit : BOOL
VAR_INPUT
bInCopyCode : BOOL;
END_VAR''','''FB_exit := nativeRelease();''')
method('atMbClientTCP','FB_reinit','''METHOD FB_reinit : BOOL
VAR
j : DINT;
END_VAR''','''nativeHandle := 0;
nativeActiveRequest := 0; nativeSelected := 0; nativeActiveIndex := -1;
nativeResultReady := FALSE; nativePrevConnected := FALSE;
requestCnt := 0; out_registrationFull := FALSE;
FOR j := 0 TO 99 DO requests[j] := 0; reqStats[j] := 0; END_FOR;
FB_reinit := TRUE;''')
append_vars('ModbusMaster','atMbReqBase','''nativeFunction : BYTE;
nativeCount : UINT;
nativeOffset : UINT;
nativeInFlight : BOOL;
nativeReadComplete : BOOL;
nativeCopyAllowed : BOOL;
nativeWriteHold : BOOL;
nativeStartMs : ULINT;
nativeLoop : DINT;
nativeRC : UDINT;''')
append_vars('ModbusMaster','atMbReqBase','''out_nativeError : UDINT;
out_socketError : UDINT;
out_writeUncertain : BOOL;
out_validData : BOOL;
out_dataAgeMs : ULINT;
out_stale : BOOL := TRUE;
out_parameterError : BOOL;''','VAR_OUTPUT')
append_vars('ModbusMaster','atMbReqBase','''in_staleAfter : TIME := T#2S;''','VAR_INPUT')
method('atMbReqBase','invalidateNativeData','''METHOD PUBLIC invalidateNativeData''','''out_validData := FALSE;
out_stale := TRUE;''')
method('atMbReqBase','nativeFinish','''METHOD nativeFinish
VAR_INPUT
result : UDINT;
END_VAR
VAR
mapped : ModbusFB.Error;
ex : ModbusFB.ExceptionCodes;
duration : UDINT;
END_VAR''','''out_nativeError := result;
out_socketError := client.nativeStatus[7];
out_writeUncertain := nativeInFlight AND (client.nativeStatus[9] <> 0);
IF (result = 10) AND nativeInFlight AND (nativeFunction >= 15) THEN
out_writeUncertain := TRUE;
END_IF;
IF out_writeUncertain AND client.nativeHoldUncertainWrites THEN
nativeWriteHold := TRUE;
END_IF;
stats.reqBusy := FALSE;
IF result = 0 THEN
duration := ULINT_TO_UDINT(client.nativeNowMs - nativeStartMs);
stats.lastValidDuration := duration;
IF duration > stats.longestValidDuration THEN stats.longestValidDuration := duration; END_IF;
client.nativeReplied := client.nativeReplied + 1;
client.nativeWindowTime := client.nativeWindowTime + duration;
stats.nativeResponseSeen := TRUE;
IF nativeFunction <= 4 THEN
nativeReadComplete := TRUE;
stats.nativeLastSuccessMs := client.nativeNowMs;
out_validData := TRUE;
END_IF;
done(TRUE);
ELSE
CASE result OF
1,258: mapped := ModbusFB.Error.RequestParameterError;
4,7: mapped := ModbusFB.Error.TcpSocketError;
5: mapped := ModbusFB.Error.TcpConnectTimeout;
6: mapped := ModbusFB.Error.ReplyTimeout;
8: mapped := ModbusFB.Error.InvalidReply;
9: mapped := ModbusFB.Error.RequestException;
10: mapped := ModbusFB.Error.RequestCancelled;
ELSE mapped := ModbusFB.Error.InternalError;
END_CASE;
ex := ModbusFB.ExceptionCodes.RESPONSE_SUCCESS;
IF result = 9 THEN
CASE client.nativeStatus[8] OF
1: ex := ModbusFB.ExceptionCodes.ILLEGAL_FUNCTION;
2: ex := ModbusFB.ExceptionCodes.ILLEGAL_DATA_ADDRESS;
3: ex := ModbusFB.ExceptionCodes.ILLEGAL_DATA_VALUE;
4: ex := ModbusFB.ExceptionCodes.SLAVE_DEVICE_FAILURE;
5: ex := ModbusFB.ExceptionCodes.ACKNOWLEDGE;
6: ex := ModbusFB.ExceptionCodes.SLAVE_DEVICE_BUSY;
8: ex := ModbusFB.ExceptionCodes.MEMORY_PARITY_ERROR;
10: ex := ModbusFB.ExceptionCodes.GATEWAY_PATH_UNAVAILABLE;
11: ex := ModbusFB.ExceptionCodes.GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND;
END_CASE;
client.nativeReplied := client.nativeReplied + 1;
stats.nativeResponseSeen := TRUE;
ELSE
stats.nativeResponseSeen := FALSE;
END_IF;
err(in := TRUE,errorId := mapped,exceptionCode := ex);
END_IF;
nativeInFlight := FALSE;
stats();''')
method('atMbReqBase','nativeProcess','''METHOD nativeProcess
VAR_INPUT
functionCode : BYTE;
END_VAR''','''nativeReadComplete := FALSE;
stats.reqDone := FALSE; stats.reqError := FALSE;
stats();
IF NOT client.out_connected THEN stats.setClientOffline(); END_IF;
IF client.nativeActiveRequest = THIS AND client.nativeResultReady THEN
IF (client.nativeStatus[3] <> 0) AND (nativeFunction <= 4) AND NOT nativeCopyAllowed THEN
nativeFinish(1);
ELSE
nativeFinish(client.nativeStatus[5]);
END_IF;
RETURN;
END_IF;
IF stats.execute AND NOT nativeInFlight AND client.nativeSelected = THIS THEN
nativeFunction := functionCode;
nativeCount := stats.reqLength; nativeOffset := in_dataOffset;
nativeStartMs := client.nativeNowMs;
IF out_parameterError OR nativeCount = 0 THEN nativeFinish(1); RETURN; END_IF;
client.nativeArgUnit := in_unitId;
client.nativeArgFunction := functionCode;
client.nativeArgAddress := in_startAddr;
client.nativeArgQuantity := nativeCount;
client.nativeArgTimeout := MAX(UDINT#1,(replyTimeout + 999)/1000);
nativeRC := client.nativeCall(11);
IF nativeRC = 0 THEN nativeRC := client.nativeCall(6); END_IF;
IF nativeRC = 0 THEN
client.nativeActiveRequest := THIS;
nativeInFlight := TRUE;
stats.reqBusy := TRUE;
client.nativeSent := client.nativeSent + 1;
client.nativeWindowCount := client.nativeWindowCount + 1;
ELSE nativeFinish(nativeRC);
END_IF;
stats();
END_IF;
IF out_validData THEN
out_dataAgeMs := client.nativeNowMs - stats.nativeLastSuccessMs;
out_stale := out_dataAgeMs > TIME_TO_UDINT(in_staleAfter);
ELSE out_stale := TRUE;
END_IF;''')
# Request body remains registration/clipping; fix remaining array capacity in native mode.
for name,f,bit,write in [('atMbReqReadCoil',1,True,False),('atMbReqReadDiscrete',2,True,False),('atMbReqReadHolding',3,False,False),('atMbReqReadInput',4,False,False),('atMbReqWriteCoil',15,True,True),('atMbReqWriteHolding',16,False,True)]:
original=get('ModbusMaster',name)
put('ModbusMaster',name,'''IF __ISVALIDREF(client) AND_THEN client.nativeMode THEN
out_parameterError := (UINT_TO_DINT(in_dataOffset) < LOWER_BOUND(data,1)) OR
(UINT_TO_DINT(in_dataOffset) > UPPER_BOUND(data,1));
IF out_parameterError THEN dataSize := 0;
ELSE dataSize := DINT_TO_UINT(UPPER_BOUND(data,1) - UINT_TO_DINT(in_dataOffset) + 1);
END_IF;
SUPER^();
RETURN;
END_IF;
'''+original)
native='''IF __ISVALIDREF(client) AND_THEN client.nativeMode THEN
nativeCopyAllowed := (UINT_TO_DINT(nativeOffset) >= LOWER_BOUND(data,1)) AND
(UINT_TO_DINT(nativeOffset) + UINT_TO_DINT(nativeCount) - 1 <= UPPER_BOUND(data,1));
'''
if write:
native+=''' IF stats.execute AND NOT nativeInFlight AND client.nativeSelected = THIS AND NOT out_parameterError THEN
FOR nativeLoop := 0 TO UINT_TO_DINT(stats.reqLength)-1 DO
'''
native+=(' client.nativeIO[nativeLoop] := BOOL_TO_WORD(data[UINT_TO_DINT(in_dataOffset)+nativeLoop]);\n' if bit else ' client.nativeIO[nativeLoop] := UINT_TO_WORD(data[UINT_TO_DINT(in_dataOffset)+nativeLoop]);\n')
native+=' END_FOR;\n END_IF;\n'
native+=f' nativeProcess({f});\n'
if not write:
native+=''' IF nativeReadComplete AND nativeCopyAllowed THEN
FOR nativeLoop := 0 TO UINT_TO_DINT(nativeCount)-1 DO
'''
native+=(' data[UINT_TO_DINT(nativeOffset)+nativeLoop] := client.nativeIO[nativeLoop] <> 0;\n' if bit else ' data[UINT_TO_DINT(nativeOffset)+nativeLoop] := WORD_TO_UINT(client.nativeIO[nativeLoop]);\n')
native+=' END_FOR;\n END_IF;\n'
native+=' RETURN;\nEND_IF;\n'
put(name,'processRequest',native+get(name,'processRequest'))
put(name,'recreateRequest','''IF __ISVALIDREF(client) AND_THEN client.nativeMode THEN
IF client.nativeActiveRequest = THIS THEN client.recreateClient(); END_IF;
nativeInFlight := FALSE;
recreateRequest := 0;
RETURN;
END_IF;
'''+get(name,'recreateRequest'))
if write:put(name,'writeNow','''nativeWriteHold := FALSE;
out_writeUncertain := FALSE;
'''+get(name,'writeNow'))
method('atMbReqBase','FB_reinit','''METHOD FB_reinit : BOOL''','''nativeInFlight := FALSE;
stats.execute := FALSE;
stats.reqBusy := FALSE;
stats.reqDone := FALSE;
stats.reqError := FALSE;
FB_reinit := TRUE;''')
put('atMbClient','setupClient','''IF nativeMode THEN
setupClient := nativeHandle <> 0;
RETURN;
END_IF;
'''+get('atMbClient','setupClient'))
put('atMbReqBase','setupClient','''IF __ISVALIDREF(client) AND_THEN client.nativeMode THEN
setupClient := FALSE; // Native path has no ModbusFB request object.
RETURN;
END_IF;
'''+get('atMbReqBase','setupClient'))
# Stats type names remain unchanged; add native health/freshness fields.
append_vars('ModbusMaster','atMbRequestStats','''nativeResponseSeen : BOOL;
nativeLastSuccessMs : ULINT;''','VAR_INPUT')
s=get('ModbusMaster','atMbRequestStats')
s=s.replace('IF doneCntRst >=3 THEN','IF nativeResponseSeen OR doneCntRst >=3 THEN')
s=s.replace('IF errorCntRst >= 3 THEN','IF NOT nativeResponseSeen AND errorCntRst >= 3 THEN')
a=s.index('IF lastReqStart < TSSus - 60000000 THEN');b=s.index('sinceLastExec :=',a)
s=s[:a]+'''IF TSSus >= lastReqStart THEN
IF TSSus - lastReqStart > 60000000 THEN lastReqStart := TSSus - 60000000; END_IF;
ELSE lastReqStart := TSSus;
END_IF;
'''+s[b:]
put('ModbusMaster','atMbRequestStats',s)
put('atMbRequestStats','setClientOffline','nativeResponseSeen := FALSE;\n'+get('atMbRequestStats','setClientOffline'))
# Dispatch may run before any request FB has registered on the first scan.
for name, call in [('processAllRequests', 'processRequest()'),
('recreateAllRequests', 'recreateRequest()'),
('setAllRequestsOffline', 'stats.setClientOffline()')]:
decl = get('atMbClient', name, 'Interface')
if 'i:' not in decl and 'i :' not in decl:
decl += '\nVAR\n i : DINT;\nEND_VAR\n'
decl = decl.replace('i: UINT;', 'i: DINT;')
put('atMbClient', name, decl, 'Interface')
put('atMbClient', name, f'''IF requestCnt = 0 OR requestCnt > 100 THEN RETURN; END_IF;
FOR i := 0 TO UINT_TO_DINT(requestCnt)-1 DO
IF requests[i] <> 0 THEN
requests[i]^.{call};
END_IF;
END_FOR;''')
# Keep subtraction signed in every other registry consumer too.
import re
for e in listing:
t = textnode(e, 'Implementation')
if t is not None and t.text and 'requestCnt' in t.text:
t.text = re.sub(r'(?<!\()\brequestCnt\s*-\s*1',
'UINT_TO_DINT(requestCnt)-1', t.text)
# Export native CODESYS XML, retaining all original objects and metadata IDs.
E.indent(r,space=' ')
E.ElementTree(r).write(ROOT/'compat/AtomModbus-Compatible.export',encoding='utf-8',xml_declaration=False)
# Readable ST per changed/new object for reviewing or manual import fallback.
changed=[]
for e in listing:
p,n=key(e)
for part in ['Interface','Implementation']:
t=textnode(e,part)
if t is not None:
changed.append(f'\n(* {p}/{n} -- {part} *)\n{t.text or ""}\n')
(ROOT/'compat/All_Source_Review.st').write_text(''.join(changed))
print('Wrote compatible native export:',len(listing),'objects')
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Insert implementation into an official CODESYS-generated external stub.
Fails closed on unexpected syntax; never invents ABI, signature or checksum.
"""
import argparse
from pathlib import Path
import re
def prepare(source, interface, sdk, destination):
if source.stem != 'CmpAtomModbus' or interface.name != 'CmpAtomModbusItf.m4':
raise ValueError('Generate component CmpAtomModbus, with its matching Itf.m4')
c=source.read_text(); m4=interface.read_text()
match=re.search(r'\batmbcall_cext\s*\(\s*(\w+)\s*\*\s*(\w+)\s*\)\s*\{',c,re.I)
if not match: raise ValueError('Cannot find one-struct-pointer atMbCall_cext stub')
typ,param=match.groups()
structs=re.findall(r'typedef\s+struct(?:\s+\w+)?\s*\{([^{}]*)\}\s*(\w+)\s*;',m4,re.S)
fields=next((body for body,name in structs if name==typ),None)
if fields is None: raise ValueError('Cannot find the generated parameter struct in M4')
names=re.findall(r'\b(\w+)\s*(?:\[[^\]]*\])?\s*;',fields)
lookup={n.lower():n for n in names}
args=['command','handle','ipaddress','port','unitid','functioncode','startaddress',
'quantity','pdata','datacapacity','pstatus','statuscapacity','nowms',
'connecttimeoutms','responsetimeoutms','retrydelayms']
for n in args+['atmbcall_cext']:
if n not in lookup: raise ValueError('Missing generated field '+n)
# Match the stub body, preserving all surrounding generated source.
start=match.end()-1; depth=1; end=start+1
# Stub bodies must be empty or comments, not application code.
while end<len(c) and depth:
if c[end]=='{': depth+=1
elif c[end]=='}': depth-=1
end+=1
if depth: raise ValueError('Unbalanced generated stub')
old=c[start+1:end-1]
old=re.sub(r'/\*.*?\*/|//[^\n]*','',old,flags=re.S).strip()
if old: raise ValueError('Stub contains code; inspect manually instead of replacing it')
body='{\n '+param+'->'+lookup['atmbcall_cext']+' = ab_call(\n '
body+=',\n '.join(param+'->'+lookup[n] for n in args)+');\n}'
c=c[:start]+body+c[end:]
# Use one component C translation unit because the supplied SDK derives
# component identity from the top-level C filename list.
c='#include "../src/atom_bridge.h"\n'+c
c+='\n#include "../src/atom_mb.c"\n#include "../src/atom_bridge.c"\n'
frame=(sdk/'src/CmpFrame.c').read_text()
hook=re.search(r'(static\s+RTS_RESULT\s+CDECL\s+HookFunction\([^)]*\)\s*\{)',frame)
if not hook: raise ValueError('Unexpected SDK HookFunction; inspect manually')
frame=frame[:hook.end()]+'\n if (ulHook == CH_EXIT) ab_shutdown();\n'+frame[hook.end():]
frame='#include "../src/atom_bridge.h"\n'+frame
destination.mkdir(parents=True,exist_ok=True)
if any(destination.iterdir()): raise ValueError('Destination must be empty; preserve existing build work')
(destination/'CmpAtomModbus.c').write_text(c)
(destination/interface.name).write_text(m4)
(destination/'CmpFrame.c').write_text(frame)
(destination/'makefile').write_text('''# Generated from supplied ExtensionSDK; native build on ARM64 Pi.
SDKDIR ?= ../../ExtensionSDK
MAJORVER=0
MINORVER=2
CMPVERSION=0x00020000
# Local development ID; check no other component uses this ID.
CMPID=0x2001
DOS2UNIX=dos2unix
M4=m4
CC ?= gcc
# The SDK otherwise includes CmpFrame in its component identity wildcard.
override CSRC := CmpAtomModbus.c
include $(SDKDIR)/makefile
''')
if __name__=='__main__':
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('source',type=Path); p.add_argument('interface',type=Path)
p.add_argument('--sdk',required=True,type=Path)
p.add_argument('--out',type=Path,default=Path('component'))
a=p.parse_args()
prepare(a.source,a.interface,a.sdk,a.out)
print('Prepared component; compile and validate the generated interface on target.')