Threaded version 0.7
This commit is contained in:
@@ -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.
|
||||
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.
@@ -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);
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -0,0 +1,4 @@
|
||||
atom
|
||||
ADAM-CODESYS
|
||||
9256
|
||||
639269490056168856
|
||||
Reference in New Issue
Block a user