Threaded version 0.7

This commit is contained in:
2026-10-06 23:50:07 -06:00
parent 490c9bdc5b
commit c39c226885
15 changed files with 13126 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
Build the updated C component for each target here. See docs/BUILD-AND-USAGE.md. Previous ARM binaries are under backup/artifacts and must not be used with the new IEC export.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
Original uploaded working library/export and ARM binaries, retained for rollback/reference. These are the previous release and do not contain the worker or new statistics. Do not mix them with the updated IEC export.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
+115
View File
@@ -0,0 +1,115 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "atom_bridge.h"
#include "atom_mb.h"
#include <pthread.h>
#include <sched.h>
#include <errno.h>
#include <time.h>
#include <string.h>
/* All engine access is serialized. The worker never holds an IEC pointer.
* Network I/O is nonblocking; no mutex is held across sleep or thread join.
* Serial open/ioctl latency is driver dependent: this is not a hard RT API. */
uint32_t ab_dispatch(uint32_t,uint32_t,uint32_t,uint16_t,uint8_t,uint8_t,uint16_t,uint16_t,
uint16_t *,uint32_t,uint32_t *,uint32_t,uint64_t,uint32_t,uint32_t,uint32_t);
void ab_pump(uint64_t,uint32_t);
void ab_close_all(void);
static pthread_mutex_t aw_lifecycle=PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t aw_engine;
static pthread_once_t aw_once=PTHREAD_ONCE_INIT;
static int aw_init_error;
static void aw_init_mutex(void){
pthread_mutexattr_t a;
int e=pthread_mutexattr_init(&a);
if(e){aw_init_error=e;return;}
/* Priority inheritance limits inversion if the PLC task has RT priority. */
e=pthread_mutexattr_setprotocol(&a,PTHREAD_PRIO_INHERIT);
if(e){pthread_mutexattr_destroy(&a);aw_init_error=e;return;}
aw_init_error=pthread_mutex_init(&aw_engine,&a);
pthread_mutexattr_destroy(&a);
}
static pthread_t aw_thread;
static int aw_running,aw_stop,aw_error,aw_cpu=-1;
static uint32_t aw_interval=1,aw_watchdog=2000;
static uint64_t aw_cycles,aw_heartbeat;
static cpu_set_t aw_original_mask;
static uint64_t aw_now(void){
struct timespec t;clock_gettime(CLOCK_MONOTONIC,&t);
return (uint64_t)t.tv_sec*1000u+(uint64_t)t.tv_nsec/1000000u;
}
static void *aw_main(void *unused){
(void)unused;
for(;;){
pthread_mutex_lock(&aw_engine);
if(aw_stop){pthread_mutex_unlock(&aw_engine);break;}
aw_heartbeat=aw_now();ab_pump(aw_heartbeat,aw_watchdog);aw_cycles++;
uint32_t delay=aw_interval;
pthread_mutex_unlock(&aw_engine);
struct timespec t={delay/1000u,(long)(delay%1000u)*1000000L};
while(nanosleep(&t,&t)<0 && errno==EINTR){}
}
return NULL;
}
static int aw_start(void){
if(aw_running)return 0;
int e=pthread_getaffinity_np(pthread_self(),sizeof(aw_original_mask),&aw_original_mask);
if(e)return e;
pthread_attr_t attr;struct sched_param priority={0};e=pthread_attr_init(&attr);
if(e)return e;
/* Do not inherit a high-priority real-time PLC task's scheduling policy. */
e=pthread_attr_setinheritsched(&attr,PTHREAD_EXPLICIT_SCHED);
if(!e)e=pthread_attr_setschedpolicy(&attr,SCHED_OTHER);
if(!e)e=pthread_attr_setschedparam(&attr,&priority);
aw_stop=0;
if(!e)e=pthread_create(&aw_thread,&attr,aw_main,NULL);
pthread_attr_destroy(&attr);
if(!e){aw_running=1;aw_cpu=-1;}
return e;
}
static void aw_snapshot(uint32_t *o){
memset(o,0,24*sizeof(*o));o[0]=0x00040000u;o[1]=(uint32_t)aw_running;
o[2]=(uint32_t)aw_error;o[3]=(uint32_t)(aw_cpu+1);o[4]=aw_interval;o[5]=aw_watchdog;
o[6]=(uint32_t)aw_cycles;o[7]=(uint32_t)(aw_cycles>>32);
o[8]=(uint32_t)aw_heartbeat;o[9]=(uint32_t)(aw_heartbeat>>32);
}
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 ignored_now,uint32_t ct,uint32_t rt,uint32_t retry){
(void)ignored_now;
if(!status || sc<AM_STATUS_WORDS)return AB_BAD_CAPACITY;
pthread_once(&aw_once,aw_init_mutex);
if(aw_init_error){memset(status,0,24*sizeof(*status));status[2]=status[7]=(uint32_t)aw_init_error;return AB_WORKER_ERROR;}
pthread_mutex_lock(&aw_lifecycle);pthread_mutex_lock(&aw_engine);
uint32_t result=0;
if(cmd==1 || cmd==20 || cmd==30 || cmd==40){
aw_error=aw_start();
if(aw_error){memset(status,0,24*sizeof(*status));status[2]=status[7]=(uint32_t)aw_error;result=AB_WORKER_ERROR;goto done;}
}
if(cmd==40){
/* addr=CPU+1 (0 restores inherited mask), ct=pump interval, rt=scan watchdog. */
if(addr>CPU_SETSIZE || ct<1 || ct>20 || rt<100 || rt>60000){result=AM_ARGUMENT;goto worker_status;}
cpu_set_t mask=aw_original_mask;
if(addr){CPU_ZERO(&mask);CPU_SET(addr-1,&mask);}
aw_error=pthread_setaffinity_np(aw_thread,sizeof(mask),&mask);
if(aw_error){result=AB_WORKER_ERROR;goto worker_status;}
aw_cpu=(int)addr-1;aw_interval=ct;aw_watchdog=rt;
worker_status:
aw_snapshot(status);
}else if(cmd==41){aw_snapshot(status);}
else result=ab_dispatch(cmd,h,ip,port,unit,f,addr,q,data,cap,status,sc,aw_now(),ct,rt,retry);
done:
pthread_mutex_unlock(&aw_engine);pthread_mutex_unlock(&aw_lifecycle);
return result;
}
void ab_shutdown(void){
pthread_once(&aw_once,aw_init_mutex);
if(aw_init_error)return;
pthread_mutex_lock(&aw_lifecycle);pthread_mutex_lock(&aw_engine);
int join=aw_running;aw_stop=1;pthread_mutex_unlock(&aw_engine);
if(join)pthread_join(aw_thread,NULL);
pthread_mutex_lock(&aw_engine);ab_close_all();aw_running=0;aw_cpu=-1;
aw_error=0;aw_interval=1;aw_watchdog=2000;aw_cycles=0;aw_heartbeat=0;
pthread_mutex_unlock(&aw_engine);pthread_mutex_unlock(&aw_lifecycle);
}
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""Real PTY/socket integration tests for the transport RTU and TCP server bridge."""
import ctypes as C,os,pty,select,socket,struct,subprocess,time,tempfile
from pathlib import Path
root=Path(__file__).resolve().parents[1]
with tempfile.TemporaryDirectory() as tmp:
so=Path(tmp)/'test-transport.so'
subprocess.run(['cc','-shared','-fPIC','-std=c11','-Wall','-Wextra','-Werror','-O2','-pthread','-I'+str(root/'src'),*[str(root/'src'/f) for f in ['atom_mb.c','atom_rtu.c','atom_server.c','atom_bridge.c','atom_worker.c']],'-o',str(so)],check=True)
lib=C.CDLL(str(so));fn=lib.ab_call
U=C.c_uint32;W=C.c_uint16;B=C.c_uint8
fn.argtypes=[U,U,U,W,B,B,W,W,C.POINTER(W),U,C.POINTER(U),U,C.c_uint64,U,U,U];fn.restype=U
data=(W*2000)();status=(U*24)()
def now():return int(time.monotonic()*1000)
def call(cmd,h=0,ip=0,port=0,unit=1,f=3,addr=0,q=1,ct=100,rt=100,retry=10,when=None):
return fn(cmd,h,ip,port,unit,f,addr,q,data,2000,status,24,now() if when is None else when,ct,rt,retry)
def crc(frame):
v=0xffff
for byte in frame:
v^=byte
for _ in range(8):v=(v>>1)^(0xa001 if v&1 else 0)
return v
def adu(payload):return payload+struct.pack('<H',crc(payload))
def ready(h):
end=time.monotonic()+1
while time.monotonic()<end:
assert call(5,h)==0
if status[1]==3:return
time.sleep(.001)
raise AssertionError(('not ready',list(status)))
def rtu_scenario(mode,f=3):
master,slave=pty.openpty();path=os.ttyname(slave);os.close(slave);os.set_blocking(master,False)
for i,b in enumerate(path.encode()):data[i]=b
assert call(20,ip=9600,unit=0,f=8,addr=1,q=len(path),ct=0)==0
h=status[0];assert call(9,h)==0;assert call(3,h)==0;ready(h)
for attempt in range(2 if mode in ['crc','timeout','echo'] else 1):
if f==16:data[0]=123;data[1]=456
if f==15:data[0]=1;data[1]=0
assert call(6,h,unit=1,f=f,addr=10,q=2)==0
received=b'';sent=False;fragments=[];deadline=time.monotonic()+1
while time.monotonic()<deadline:
assert call(5,h)==0
if select.select([master],[],[],0)[0]:
try:received+=os.read(master,256)
except OSError:pass
need=(9+received[6]) if len(received)>=7 and f>=15 else 8
if len(received)>=need and not sent:
assert crc(received)==0;sent=True
payload=(bytes([1,f])+received[2:6]) if f>=15 else bytes([1,f,4,0,42,0,43])
if mode=='exception':payload=bytes([1,f|0x80,2])
if mode=='echo' and attempt==0:payload=payload[:3]+bytes([payload[3]^1])+payload[4:]
response=adu(payload)
if mode=='crc' and attempt==0:response=response[:-1]+bytes([response[-1]^1])
if mode!='timeout' or attempt>0:
fragments=[response[:2],response[2:4],response[4:]] if mode=='fragment' else [response]
if fragments:os.write(master,fragments.pop(0))
if status[3] or status[4]:break
time.sleep(.002)
assert sent and (status[3] or status[4]),list(status)
if mode in ['crc','echo'] and attempt==0:assert status[5]==8
elif mode=='timeout' and attempt==0:assert status[5]==6
elif mode=='exception':assert status[5]==9 and status[8]==2 and status[1]==3
else:
assert status[3]==1,list(status)
if f<=4:assert call(7,h)==0 and list(data[:2])==[42,43]
assert call(10,h)==0
if attempt==0 and mode in ['crc','timeout','echo']:ready(h)
assert call(6,h,unit=0,f=3,addr=0,q=1)==1 # Broadcast unsupported, no fake completion.
assert call(2,h)==0;assert call(5,h)==256;os.close(master)
for mode in ['normal','fragment','crc','timeout','exception']:rtu_scenario(mode);print('PASS RTU',mode)
rtu_scenario('normal',16);print('PASS RTU register write')
rtu_scenario('echo',16);print('PASS RTU invalid echo and reopen recovery')
rtu_scenario('normal',15);print('PASS RTU coil write')
# Transport TCP server, four independent tables mapped at nonzero Modbus address.
probe=socket.socket();probe.bind(('127.0.0.1',0));port=probe.getsockname()[1];probe.close()
assert call(30,ip=0x7f000001,port=port)==0;h=status[0]
for table in range(4):
for i in range(16):data[i]=i%2 if table<2 else 100+i
assert call(35,h,f=table,addr=10,q=16)==0
assert call(32,h)==0
deadline=time.monotonic()+1
while time.monotonic()<deadline:
assert call(34,h)==0
if status[1]:break
time.sleep(.002)
assert status[1]==1
peer=socket.create_connection(('127.0.0.1',port));peer.setblocking(False)
tid=0
def exchange(pdu):
global tid
tid+=1;packet=struct.pack('>HHHB',tid,0,len(pdu)+1,1)+pdu
peer.send(packet[:3]);assert call(34,h)==0;peer.send(packet[3:])
reply=b'';end=time.monotonic()+1
while time.monotonic()<end:
assert call(34,h)==0
try:reply+=peer.recv(260)
except BlockingIOError:pass
if len(reply)>=7 and len(reply)==6+struct.unpack('>H',reply[4:6])[0]:return reply[7:]
time.sleep(.001)
raise AssertionError(('server timeout',pdu,reply))
for f in [1,2,3,4]:
rep=exchange(struct.pack('>BHH',f,10,2));assert rep==(bytes([f,1,2]) if f<=2 else bytes([f,4,0,100,0,101]));print('PASS slave read FC',f)
for f in [5,6]:
value=0xff00 if f==5 else 456;pdu=struct.pack('>BHH',f,10,value);assert exchange(pdu)==pdu
assert call(36,h,f=0 if f==5 else 2,addr=10,q=16)==0 and data[0]==(1 if f==5 else 456);print('PASS slave write FC',f)
for f in [15,16]:
payload=b'\x01' if f==15 else struct.pack('>HH',321,654)
pdu=struct.pack('>BHHB',f,10,2,len(payload))+payload
assert exchange(pdu)==struct.pack('>BHH',f,10,2)
assert call(36,h,f=0 if f==15 else 2,addr=10,q=16)==0 and list(data[:2])==([1,0] if f==15 else [321,654]);print('PASS slave write FC',f)
assert exchange(struct.pack('>BHH',3,0,1))==bytes([0x83,2])
assert exchange(struct.pack('>BHH',16,10,0))==bytes([0x90,3])
assert exchange(struct.pack('>BHH',99,10,1))==bytes([0xe3,1])
print('PASS slave exceptions and nonzero table bases')
assert call(31,h)==0;assert call(34,h)==256;peer.close()
# Listener bind error recovers after the conflicting listener disappears.
blocker=socket.socket();blocker.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1);blocker.bind(('127.0.0.1',port));blocker.listen()
assert call(30,ip=0x7f000001,port=port)==0;h=status[0];call(32,h)
end=time.monotonic()+1
while time.monotonic()<end:
call(34,h)
if status[2]:break
time.sleep(.002)
assert status[2]==1
blocker.close()
end=time.monotonic()+1
while time.monotonic()<end:
call(34,h)
if status[1] and not status[2]:break
time.sleep(.002)
assert status[1]==1 and status[2]==0
call(31,h);print('PASS slave listener recovery and stale handles')
lib.ab_shutdown()
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""Exercise production worker with real sockets/PTYS and deliberately sparse PLC calls."""
import ctypes as C
import os,pty,select,socket,struct,subprocess,tempfile,time,threading
from pathlib import Path
root=Path(__file__).resolve().parents[1]
with tempfile.TemporaryDirectory() as td:
so=Path(td)/'worker.so'
subprocess.run(['cc','-shared','-fPIC','-std=c11','-Wall','-Wextra','-Werror','-O2','-pthread',
*[str(root/'src'/f) for f in ['atom_mb.c','atom_rtu.c','atom_server.c','atom_bridge.c','atom_worker.c']],'-o',str(so)],check=True)
lib=C.CDLL(str(so));U=C.c_uint32;W=C.c_uint16;B=C.c_uint8
lib.ab_call.argtypes=[U,U,U,W,B,B,W,W,C.POINTER(W),U,C.POINTER(U),U,C.c_uint64,U,U,U];lib.ab_call.restype=U
def call(cmd,h=0,ip=0,port=0,f=3,addr=0,q=1,values=(),ct=100,rt=100,retry=10,unit=1):
data=(W*2000)(*values);status=(U*24)()
result=lib.ab_call(cmd,h,ip,port,unit,f,addr,q,data,2000,status,24,0,ct,rt,retry)
return result,list(status),list(data)
def until(fn,seconds=1):
end=time.monotonic()+seconds
while time.monotonic()<end:
v=fn()
if v:return v
time.sleep(.002)
raise AssertionError('condition timed out')
def configure(watchdog=2000,cpu=-1):
r,s,_=call(40,addr=cpu+1,ct=1,rt=watchdog);assert r==0,(r,s)
def client(port):
r,s,_=call(1,ip=0x7f000001,port=port);assert not r;h=s[0]
assert not call(9,h,rt=100)[0];assert not call(3,h)[0]
until(lambda:call(5,h)[1][1]==3)
return h
def recv_exact(peer,count):
result=b''
while len(result)<count:
v=peer.recv(count-len(result));assert v;result+=v
return result
def server():
r,s,_=call(30,ip=0x7f000001,port=port);assert not r;h=s[0]
for t in range(4):assert not call(35,h,f=t,q=8,values=[0]*8)[0]
assert not call(32,h)[0]
until(lambda:call(34,h)[1][1])
return h
probe=socket.socket();probe.bind(('127.0.0.1',0));port=probe.getsockname()[1];probe.close()
try:
configure();sh=server();ch=client(port)
assert not call(6,ch,q=2)[0]
# The reply deadline is 100 ms; the PLC deliberately does nothing for 250 ms.
time.sleep(.25)
r,s,d=call(7,ch);assert not r and s[3] and not s[4] and d[:2]==[0,0],s
assert call(6,ch)[0]==2 # Completion cannot be overwritten before acknowledgment.
r,m,_=call(12,ch);assert not r and m[21]<100 and m[22]>=100,m
assert call(10,ch)[0]==0
print('PASS worker completes TCP before timeout with PLC absent for 250 ms; result retained and timed')
# Remote write must survive a stale PLC push before the PLC pulls its result.
peer=socket.create_connection(('127.0.0.1',port));peer.settimeout(1)
pdu=struct.pack('>BHH',6,0,4321);peer.sendall(struct.pack('>HHHB',1,0,len(pdu)+1,1)+pdu)
assert recv_exact(peer,12)[7:]==pdu
assert call(35,sh,f=2,q=8,values=[0]*8)[0]==0
assert call(36,sh,f=2,q=8)[2][0]==4321
# After acknowledgment a deliberate application override is permitted.
call(35,sh,f=2,q=8,values=[42]*8);assert call(36,sh,f=2,q=8)[2][0]==42
assert call(35,sh,f=2,addr=1,q=8,values=[0]*8)[0]==1 # explicit recreate for remap
peer.close();call(2,ch);call(31,sh)
print('PASS autonomous TCP slave and lossless remote-write handoff')
# Real serial PTY, again no PLC ticks after submission.
master,slave=pty.openpty();path=os.ttyname(slave);os.close(slave)
r,s,_=call(20,ip=9600,unit=0,f=8,addr=1,q=len(path),ct=0,values=list(path.encode()));assert not r;h=s[0]
call(9,h,rt=100);call(3,h);until(lambda:call(5,h)[1][1]==3)
call(6,h,unit=1,f=3,q=1)
assert select.select([master],[],[],1)[0];request=os.read(master,256);assert len(request)==8
def crc(p):
c=65535
for b in p:
c^=b
for _ in range(8):c=(c>>1)^(0xa001 if c&1 else 0)
return c
p=bytes([1,3,2,0,77]);os.write(master,p+struct.pack('<H',crc(p)))
time.sleep(.25);r,s,d=call(7,h);assert not r and s[3] and d[0]==77,s
call(2,h);os.close(master)
print('PASS RTU completion while PLC does not scan for 250 ms')
# Watchdog cancels an in-flight write without replaying it on resume.
configure(100)
listener=socket.socket();listener.bind(('127.0.0.1',0));listener.listen();listener.settimeout(1)
h=client(listener.getsockname()[1]);peer,_=listener.accept();peer.settimeout(1)
call(11,h,rt=1000);assert not call(6,h,f=16,q=1,values=[9])[0]
packet=recv_exact(peer,15);assert packet[7]==16
time.sleep(.16)
# command 12 is read-only and does not renew the scan lease.
r,s,_=call(12,h);assert not r and s[1]==0 and s[5]==10 and s[9]==1,s
assert peer.recv(1)==b''
call(5,h);peer2,_=listener.accept();peer2.settimeout(.03)
try:assert not peer2.recv(1), 'write replayed'
except socket.timeout:pass
call(2,h);peer.close();peer2.close();listener.close()
print('PASS scan watchdog closes transport, preserves uncertain write, resumes without replay')
# Server closes its listener when scanning ceases and recovers on refresh.
configure(100);sh=server();time.sleep(.16)
probe=socket.socket();probe.settimeout(.1);assert probe.connect_ex(('127.0.0.1',port))!=0;probe.close()
call(34,sh);until(lambda:call(34,sh)[1][1]);call(31,sh)
print('PASS server watchdog and restart')
# CPU selection, failure reporting, and restoring inherited mask.
configure(cpu=min(os.sched_getaffinity(0)))
r,s,_=call(41);assert not r and s[3]==min(os.sched_getaffinity(0))+1
assert call(40,addr=65535,ct=1,rt=2000)[0]==1
configure()
failures=[]
def reader():
for _ in range(200):
if call(41)[0]:failures.append('snapshot failure')
threads=[threading.Thread(target=reader) for _ in range(4)]
for t in threads:t.start()
for t in threads:t.join()
assert not failures
print('PASS configurable affinity, invalid arguments, concurrent snapshot access')
for _ in range(20):
lib.ab_shutdown();assert call(41)[1][1]==0;configure();assert call(41)[1][1]==1
print('PASS repeated joined shutdown/restart')
finally:lib.ab_shutdown()
+11
View File
@@ -0,0 +1,11 @@
# Export import correction
The Worker object in the first threaded export contained an unnamed List element.
The generator cleared its attributes as well as its children. This revision
restores Name="ChildObjectGuids", preserves an empty child list, and matches the
original export envelope (no XML declaration, CRLF line endings).
All collection elements now retain serialization attributes. XML parsing passes.
CODESYS import has not been verified in an IDE. If import still fails, capture the
exact error text and whether it occurs before or after the object selection dialog.
The C sources and IEC program logic are unchanged by this correction.
+90
View File
@@ -0,0 +1,90 @@
{
"NATIVE_CMD_ACKNOWLEDGE": "CMD_ACKNOWLEDGE",
"NATIVE_CMD_CLOSE": "CMD_CLOSE",
"NATIVE_CMD_CONFIGURE": "CMD_CONFIGURE",
"NATIVE_CMD_COPY_READ": "CMD_COPY_READ",
"NATIVE_CMD_ENABLE": "CMD_ENABLE",
"NATIVE_CMD_OPEN": "CMD_OPEN",
"NATIVE_CMD_REQUEST_TIMEOUT": "CMD_REQUEST_TIMEOUT",
"NATIVE_CMD_SUBMIT": "CMD_SUBMIT",
"NATIVE_CMD_TICK": "CMD_TICK",
"NATIVE_ERROR_CANCELLED": "ERROR_CANCELLED",
"NATIVE_ERROR_INVALID_HANDLE": "ERROR_INVALID_HANDLE",
"NATIVE_STATE_DISABLED": "STATE_DISABLED",
"NATIVE_STATE_READY": "STATE_READY",
"NATIVE_STATE_RECEIVING": "STATE_RECEIVING",
"NATIVE_STATUS_DONE": "STATUS_DONE",
"NATIVE_STATUS_ERROR": "STATUS_ERROR",
"NATIVE_STATUS_EXCEPTION": "STATUS_EXCEPTION",
"NATIVE_STATUS_FUNCTION": "STATUS_FUNCTION",
"NATIVE_STATUS_HANDLE": "STATUS_HANDLE",
"NATIVE_STATUS_LAST_ERROR": "STATUS_LAST_ERROR",
"NATIVE_STATUS_RECONNECTS": "STATUS_RECONNECTS",
"NATIVE_STATUS_RESULT": "STATUS_RESULT",
"NATIVE_STATUS_SOCKET_ERROR": "STATUS_SOCKET_ERROR",
"NATIVE_STATUS_STATE": "STATUS_STATE",
"NATIVE_STATUS_WRITE_UNCERTAIN": "STATUS_WRITE_UNCERTAIN",
"invalidateNativeData": "invalidateData",
"nativeActiveIndex": "engineActiveIndex",
"nativeActiveRequest": "engineActiveRequest",
"nativeArgAddress": "engineArgAddress",
"nativeArgFunction": "engineArgFunction",
"nativeArgQuantity": "engineArgQuantity",
"nativeArgTimeout": "engineArgTimeout",
"nativeArgUnit": "engineArgUnit",
"nativeCall": "callEngine",
"nativeCallResult": "engineCallResult",
"nativeConfigConnect": "engineConfigConnect",
"nativeConfigRetry": "engineConfigRetry",
"nativeConnectTimeoutMilliseconds": "engineConnectTimeoutMilliseconds",
"nativeCopyAllowed": "engineCopyAllowed",
"nativeCycle": "processCycle",
"nativeDataIndex": "engineDataIndex",
"nativeDevice": "engineDevice",
"nativeEnable": "engineEnable",
"nativeEndpointIP": "engineEndpointIP",
"nativeEndpointPort": "engineEndpointPort",
"nativeEnsureContext": "ensureContext",
"nativeFinish": "finishRequest",
"nativeFinishedIndex": "engineFinishedIndex",
"nativeFunction": "engineFunction",
"nativeHandle": "engineHandle",
"nativeHoldUncertainWrites": "engineHoldUncertainWrites",
"nativeIO": "engineIO",
"nativeIP": "engineIP",
"nativeInFlight": "engineInFlight",
"nativeLastSuccessMs": "engineLastSuccessMs",
"nativeMode": "engineMode",
"nativeNowMicroseconds": "engineNowMicroseconds",
"nativeNowMilliseconds": "engineNowMilliseconds",
"nativeOpenCommand": "engineOpenCommand",
"nativePort": "enginePort",
"nativeProcess": "processTransaction",
"nativeReadComplete": "engineReadComplete",
"nativeRelease": "releaseContext",
"nativeReplied": "engineReplied",
"nativeRequestStartMilliseconds": "engineRequestStartMilliseconds",
"nativeResponseSeen": "engineResponseSeen",
"nativeResultReady": "engineResultReady",
"nativeRetryDelayMilliseconds": "engineRetryDelayMilliseconds",
"nativeSelectNextRequest": "selectNextRequest",
"nativeSelected": "engineSelected",
"nativeSent": "engineSent",
"nativeSerialBaud": "engineSerialBaud",
"nativeSerialBits": "engineSerialBits",
"nativeSerialParity": "engineSerialParity",
"nativeSerialRs485": "engineSerialRs485",
"nativeSerialStops": "engineSerialStops",
"nativeStatus": "engineStatus",
"nativeTransactionDataIndex": "engineTransactionDataIndex",
"nativeTransactionQuantity": "engineTransactionQuantity",
"nativeUpdateDiagnostics": "updateDiagnostics",
"nativeUpdateStatistics": "updateStatistics",
"nativeWasConnected": "engineWasConnected",
"nativeWindowCount": "engineWindowCount",
"nativeWindowStartMilliseconds": "engineWindowStartMilliseconds",
"nativeWindowTime": "engineWindowTime",
"nativeWriteHold": "engineWriteHold",
"out_lastNativeError": "out_lastErrorCode",
"out_nativeError": "out_errorCode"
}
+37
View File
@@ -0,0 +1,37 @@
# Worker ABI and lifecycle
The 16-argument atMbCall_cext ABI and its signature are retained. Calls are serialized
by the lifecycle mutex and priority-inheritance engine mutex. The worker accesses
fixed C contexts only. Calls may wait briefly for the engine mutex; no claim of
hard real-time maximum latency is made for OS/serial-driver operations.
New commands:
| Command | Purpose | Parameters / result |
|---|---|---|
| 12 | Client transaction timing | Normal handle; status[21] duration ms, [22] successful-read age ms (saturated UDINT); other client status fields retained |
| 40 | Configure/start global worker | startAddress=CPU+1 (0 inherited mask), connectTimeoutMs=interval 1..20, responseTimeoutMs=watchdog 100..60000 |
| 41 | Worker capabilities/status | No start side effect; available even before worker starts |
Worker status words: [0] capability 0x00040000; [1] running; [2] pthread error;
[3] configured CPU+1, 0 inherited mask; [4] interval ms; [5] watchdog ms;
[6:7] cycle counter low/high; [8:9] monotonic heartbeat low/high. Remaining words zero.
Error 260 reports worker initialization or CPU-affinity failure. Invalid settings
return 1. Invalid settings do not change current affinity/timing.
Client command 5 and server command 34 now refresh the context's scan lease and
snapshot state; they do not perform socket reads/writes. All deadline times are
sourced from CLOCK_MONOTONIC; the ABI nowMs argument is retained but ignored.
Command 6 rejects a new transaction until completion is acknowledged with command
10. Completion data cannot be overwritten by another request before acknowledgment.
Worker shutdown sets a stop flag, releases the mutex, joins the thread, then
closes contexts. Generation-tagged handles prevent access to released slots.
Watchdog expiry closes transport while preserving the slot and terminal result;
refresh resumes it. No worker thread is restarted repeatedly on link failure;
existing TCP/RTU state machines retry within it.
Socket progress is nonblocking. Socket close/configuration calls may also occur
synchronously for explicit close/reconfigure commands. These and serial ioctls are
OS calls, so target execution-time measurement remains necessary. The worker uses
normal SCHED_OTHER, not a high-priority busy loop.
File diff suppressed because one or more lines are too long
+4
View File
@@ -0,0 +1,4 @@
atom
ADAM-CODESYS
9256
639269490056168856