First library, its tested and working
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "atom_bridge.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
static uint64_t now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t); return (uint64_t)t.tv_sec*1000+(uint64_t)t.tv_nsec/1000000; }
|
||||
int main(int argc,char **argv) {
|
||||
uint32_t s[24],h=0; uint16_t data[2]={42,43}; unsigned ok=0,err=0;
|
||||
int uncertain=0,last=0; uint64_t start=now(); struct timespec pause={0,1000000};
|
||||
if(argc!=4) return 2;
|
||||
uint16_t port=(uint16_t)atoi(argv[1]); uint8_t f=(uint8_t)atoi(argv[2]);
|
||||
#define CALL(cmd) ab_call(cmd,h,0x7f000001u,port,1,f,0,2,data,2,s,24,now(),100,80,20)
|
||||
if(CALL(AB_OPEN)) return 2;
|
||||
h=s[0]; if(CALL(AB_CONFIGURE) || CALL(AB_ENABLE)) return 2;
|
||||
while(now()-start<1500 && ok<(unsigned)atoi(argv[3])) {
|
||||
if(CALL(AB_TICK)) return 2;
|
||||
if(s[3]) {
|
||||
if(f<=4 && CALL(AB_COPY_READ)) return 2;
|
||||
ok++; if(CALL(AB_ACK)) return 2;
|
||||
}
|
||||
if(s[4]) { err++; last=(int)s[5]; uncertain|=(int)s[9]; if(CALL(AB_ACK)) return 2; }
|
||||
if(s[1]==3 && ok<(unsigned)atoi(argv[3])) {
|
||||
if(CALL(AB_REQUEST_TIMEOUT) || CALL(AB_SUBMIT)) return 2;
|
||||
}
|
||||
nanosleep(&pause,0);
|
||||
}
|
||||
printf("%u %u %u %d %d %u %u\n",ok,err,s[11],uncertain,last,data[0],data[1]);
|
||||
CALL(AB_CLOSE); return ok==(unsigned)atoi(argv[3])?0:1;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "atom_mb.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
static uint64_t now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t); return (uint64_t)t.tv_sec*1000+(uint64_t)t.tv_nsec/1000000; }
|
||||
int main(int argc,char **argv) {
|
||||
am_client c; uint64_t start=now(); unsigned successes=0,errors=0; int uncertain=0,last=0;
|
||||
struct timespec pause={0,1000000}; uint16_t values[2]={42,43};
|
||||
if(argc!=4 || !am_init(&c,"127.0.0.1",(uint16_t)atoi(argv[1]))) return 2;
|
||||
c.retry_delay_ms=20; c.response_timeout_ms=80; c.connect_timeout_ms=100;
|
||||
am_enable(&c,1,start);
|
||||
while(now()-start<1500 && successes<(unsigned)atoi(argv[3])) {
|
||||
uint64_t t=now(); am_tick(&c,t);
|
||||
if(c.done) { successes++; c.done=0; }
|
||||
if(c.error) { errors++; last=c.result; uncertain|=c.write_uncertain; c.error=0; }
|
||||
if(c.state==AM_READY && successes<(unsigned)atoi(argv[3])) {
|
||||
int f=atoi(argv[2]); am_request(&c,1,(uint8_t)f,0,2,values,t);
|
||||
}
|
||||
nanosleep(&pause,0);
|
||||
}
|
||||
printf("%u %u %u %d %d %u %u\n",successes,errors,c.reconnects,uncertain,last,c.data[0],c.data[1]);
|
||||
am_close(&c); return successes==(unsigned)atoi(argv[3])?0:1;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#include "atom_bridge.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#define CALL(cmd,h) ab_call(cmd,h,0x7f000001u,502,1,3,0,2,NULL,0,status,24,100,2000,1000,1000)
|
||||
int main(void) {
|
||||
uint32_t status[24],handles[16],old,newh; unsigned i;
|
||||
assert(ab_call(AB_OPEN,0,0,502,0,0,0,0,NULL,0,status,23,0,0,0,0)==AB_BAD_CAPACITY);
|
||||
for(i=0;i<16;i++) { assert(CALL(AB_OPEN,0)==0); handles[i]=status[0]; assert(handles[i]); }
|
||||
assert(CALL(AB_OPEN,0)==AB_POOL_FULL);
|
||||
old=handles[0]; assert(CALL(AB_CLOSE,old)==0); assert(CALL(AB_TICK,old)==AB_BAD_HANDLE);
|
||||
assert(CALL(AB_OPEN,0)==0); newh=status[0]; assert(newh!=old);
|
||||
assert(CALL(AB_CONFIGURE,newh)==0); assert(CALL(AB_ENABLE,newh)==0);
|
||||
assert(CALL(AB_REQUEST_TIMEOUT,newh)==0); assert(status[22]==1000);
|
||||
assert(CALL(AB_CONFIGURE,newh)==2); assert(CALL(AB_SUBMIT,newh)==3);
|
||||
assert(CALL(AB_DISABLE,newh)==0); assert(CALL(AB_COPY_READ,newh)==1);
|
||||
ab_shutdown(); assert(CALL(AB_TICK,newh)==AB_BAD_HANDLE);
|
||||
puts("PASS bridge: capacity, pool limit, stale handles, configure, offline submission, shutdown");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Real PTY/socket integration tests for the native 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-native.so'
|
||||
subprocess.run(['cc','-shared','-fPIC','-std=c11','-Wall','-Wextra','-Werror','-O2','-I'+str(root/'src'),*[str(root/'src'/f) for f in ['atom_mb.c','atom_rtu.c','atom_server.c','atom_bridge.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')
|
||||
# Native 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;assert call(34,h)==0 and 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);call(34,h);assert status[2]==1
|
||||
blocker.close();call(34,h,when=now()+100);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,59 @@
|
||||
import socket, struct, subprocess, threading, time, sys
|
||||
from pathlib import Path
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
DRIVER=ROOT/'tests'/(sys.argv[1] if len(sys.argv)>1 else 'driver')
|
||||
def exact(s,n):
|
||||
b=b''
|
||||
while len(b)<n:
|
||||
part=s.recv(n-len(b))
|
||||
if not part: raise EOFError
|
||||
b+=part
|
||||
return b
|
||||
def run(mode, function=3):
|
||||
listener=socket.socket(); listener.bind(('127.0.0.1',0)); listener.listen(); listener.settimeout(2)
|
||||
issues=[]
|
||||
def server():
|
||||
first=True
|
||||
try:
|
||||
while True:
|
||||
try: s,_=listener.accept()
|
||||
except (socket.timeout,OSError): break
|
||||
with s:
|
||||
s.settimeout(2)
|
||||
while True:
|
||||
try:
|
||||
h=exact(s,7); p=exact(s,struct.unpack('>H',h[4:6])[0]-1)
|
||||
except (EOFError,ConnectionError,socket.timeout): break
|
||||
if first and mode in ('close','timeout','bad_tid','bad_length','write_drop'):
|
||||
first=False
|
||||
if mode=='timeout': time.sleep(.12)
|
||||
elif mode=='bad_tid': s.sendall(b'\xff\xff'+h[2:4]+b'\x00\x07'+h[6:7]+b'\x03\x04\x00\x2a\x00\x2b')
|
||||
elif mode=='bad_length': s.sendall(h[:4]+b'\xff\xff'+h[6:7])
|
||||
break
|
||||
if first and mode=='exception': reply=bytes([p[0]|128,2]); first=False
|
||||
elif function==16: reply=p[:5]
|
||||
elif function==1: reply=b'\x01\x01\x03'
|
||||
else: reply=b'\x03\x04\x00\x2a\x00\x2b'
|
||||
packet=h[:4]+struct.pack('>H',len(reply)+1)+h[6:7]+reply
|
||||
if mode=='fragment':
|
||||
for b in packet: s.sendall(bytes([b])); time.sleep(.001)
|
||||
else: s.sendall(packet)
|
||||
except Exception as e: issues.append(e)
|
||||
worker=threading.Thread(target=server); worker.start()
|
||||
try:
|
||||
p=subprocess.run([str(DRIVER),str(listener.getsockname()[1]),str(function),'2'],capture_output=True,text=True,timeout=4)
|
||||
assert p.returncode==0,(mode,p.stdout,p.stderr)
|
||||
ok,err,reconnect,uncertain,last,a,b=map(int,p.stdout.split())
|
||||
assert ok==2
|
||||
if function==3: assert (a,b)==(42,43)
|
||||
if function==1: assert (a,b)==(1,1)
|
||||
if mode in ('close','timeout','bad_tid','bad_length','write_drop'): assert err>=1 and reconnect>=1
|
||||
if mode=='exception': assert err==1 and reconnect==0 and last==9
|
||||
if mode=='write_drop': assert uncertain==1
|
||||
print('PASS',mode,p.stdout.strip())
|
||||
finally:
|
||||
listener.close(); worker.join(3)
|
||||
assert not issues,issues
|
||||
for mode in ('normal','fragment','close','timeout','bad_tid','bad_length','exception'): run(mode)
|
||||
run('write_drop',16)
|
||||
run('coils',1)
|
||||
Reference in New Issue
Block a user