First library, its tested and working
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
CC ?= cc
|
||||
CFLAGS = -std=c11 -Wall -Wextra -Werror -O2
|
||||
.PHONY: test clean
|
||||
test: tests/driver tests/test_bridge tests/bridge_driver
|
||||
./tests/test_bridge
|
||||
python3 tests/test_tcp.py
|
||||
python3 tests/test_tcp.py bridge_driver
|
||||
python3 tests/test_native.py
|
||||
tests/bridge_driver: tests/bridge_driver.c src/atom_bridge.c src/atom_bridge.h src/atom_mb.c src/atom_mb.h src/atom_rtu.c src/atom_rtu.h src/atom_server.c src/atom_server.h
|
||||
$(CC) $(CFLAGS) -Isrc tests/bridge_driver.c src/atom_bridge.c src/atom_mb.c src/atom_rtu.c src/atom_server.c -o $@
|
||||
tests/test_bridge: tests/test_bridge.c src/atom_bridge.c src/atom_bridge.h src/atom_mb.c src/atom_mb.h src/atom_rtu.c src/atom_rtu.h src/atom_server.c src/atom_server.h
|
||||
$(CC) $(CFLAGS) -Isrc tests/test_bridge.c src/atom_bridge.c src/atom_mb.c src/atom_rtu.c src/atom_server.c -o $@
|
||||
tests/driver: tests/driver.c src/atom_mb.c src/atom_mb.h src/atom_rtu.c src/atom_rtu.h src/atom_server.c src/atom_server.h
|
||||
$(CC) $(CFLAGS) -Isrc tests/driver.c src/atom_mb.c src/atom_rtu.c src/atom_server.c -o $@
|
||||
clean:
|
||||
rm -f tests/driver tests/test_bridge tests/bridge_driver
|
||||
@@ -0,0 +1,33 @@
|
||||
#define _DEFAULT_SOURCE
|
||||
#include "../src/atom_bridge.h"
|
||||
#include "CmpStd.h"
|
||||
#include "CmpErrors.h"
|
||||
#include "CmpItf.h"
|
||||
#include "CmpAtomModbusDep.h"
|
||||
|
||||
void CDECL CDECL_EXT atmbcall_cext(atmbcall_cext_struct *p)
|
||||
{
|
||||
p->atMbCall_cext = ab_call(
|
||||
p->command,
|
||||
p->handle,
|
||||
p->ipAddress,
|
||||
p->port,
|
||||
p->unitId,
|
||||
p->functionCode,
|
||||
p->startAddress,
|
||||
p->quantity,
|
||||
p->pData,
|
||||
p->dataCapacity,
|
||||
p->pStatus,
|
||||
p->statusCapacity,
|
||||
p->nowMs,
|
||||
p->connectTimeoutMs,
|
||||
p->responseTimeoutMs,
|
||||
p->retryDelayMs);
|
||||
}
|
||||
|
||||
|
||||
#include "../src/atom_mb.c"
|
||||
#include "../src/atom_rtu.c"
|
||||
#include "../src/atom_server.c"
|
||||
#include "../src/atom_bridge.c"
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* <interfacename>CmpAtomModbus</interfacename>
|
||||
* <description></description>
|
||||
*
|
||||
* <copyright></copyright>
|
||||
*/
|
||||
|
||||
SET_INTERFACE_NAME(`CmpAtomModbus')
|
||||
|
||||
/** EXTERN LIB SECTION BEGIN **/
|
||||
/* Comments are ignored for m4 compiler so restructured text can be used. changecom(`/*', `*/') */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* <description>
|
||||
* Create FUNCTION atMbCall_cext returning UDINT, paste this declaration.
|
||||
* Properties > Build > External implementation must be selected.
|
||||
* Library filename and Project Information title: CmpAtomModbus.
|
||||
* This declaration is the SINGLE external entry point for the C engine.
|
||||
* </description>
|
||||
* <element name="atMbCall_cext" type=OUT></element>
|
||||
* <element name="command" type=IN></element>
|
||||
* <element name="handle" type=IN></element>
|
||||
* <element name="ipAddress" type=IN></element>
|
||||
* <element name="port" type=IN></element>
|
||||
* <element name="unitId" type=IN></element>
|
||||
* <element name="functionCode" type=IN></element>
|
||||
* <element name="startAddress" type=IN></element>
|
||||
* <element name="quantity" type=IN></element>
|
||||
* <element name="pData" type=IN></element>
|
||||
* <element name="dataCapacity" type=IN></element>
|
||||
* <element name="pStatus" type=IN></element>
|
||||
* <element name="statusCapacity" type=IN></element>
|
||||
* <element name="nowMs" type=IN></element>
|
||||
* <element name="connectTimeoutMs" type=IN></element>
|
||||
* <element name="responseTimeoutMs" type=IN></element>
|
||||
* <element name="retryDelayMs" type=IN></element>
|
||||
*/
|
||||
typedef struct tagatmbcall_cext_struct
|
||||
{
|
||||
RTS_IEC_UDINT command;
|
||||
RTS_IEC_UDINT handle;
|
||||
RTS_IEC_DWORD ipAddress;
|
||||
RTS_IEC_UINT port;
|
||||
RTS_IEC_BYTE unitId;
|
||||
RTS_IEC_BYTE functionCode;
|
||||
RTS_IEC_UINT startAddress;
|
||||
RTS_IEC_UINT quantity;
|
||||
RTS_IEC_WORD *pData;
|
||||
RTS_IEC_UDINT dataCapacity;
|
||||
RTS_IEC_UDINT *pStatus;
|
||||
RTS_IEC_UDINT statusCapacity;
|
||||
RTS_IEC_ULINT nowMs;
|
||||
RTS_IEC_UDINT connectTimeoutMs;
|
||||
RTS_IEC_UDINT responseTimeoutMs;
|
||||
RTS_IEC_UDINT retryDelayMs;
|
||||
RTS_IEC_UDINT atMbCall_cext;
|
||||
} atmbcall_cext_struct;
|
||||
|
||||
DEF_API(`void',`CDECL',`atmbcall_cext',`(atmbcall_cext_struct *p)',1,0x709EE65B,0x00020000)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** EXTERN LIB SECTION END **/
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#include "../src/atom_bridge.h"
|
||||
/*****************************************************************************
|
||||
* Copyright: Copyright CODESYS Development GmbH
|
||||
* Program: Extension API for the Linux SL products
|
||||
******************************************************************************/
|
||||
|
||||
#include "CmpStd.h"
|
||||
|
||||
#include DEP_H
|
||||
|
||||
USE_STMT
|
||||
|
||||
DLL_DECL int CDECL ComponentEntry(INIT_STRUCT *pInitStruct)
|
||||
/* Used to exchange function pointers between component manager and components.
|
||||
Called at startup for each component.
|
||||
pInitStruct: IN Pointer to structure with:
|
||||
pfExportFunctions OUT Pointer to function that exports component functions
|
||||
pfImportFunctions OUT Pointer to function that imports functions from other components
|
||||
pfGetVersion OUT Pointer to function to get component version
|
||||
pfRegisterAPI IN Pointer to component manager function to register a API function
|
||||
pfGetAPI IN Pointer to component manager function to get a API function
|
||||
pfCallHook IN Pointer to component manager function to call a hook function
|
||||
Return ERR_OK if library could be initialized, else error code
|
||||
*/
|
||||
{
|
||||
pInitStruct->CmpId = COMPONENT_ID;
|
||||
pInitStruct->pfExportFunctions = ExportFunctions;
|
||||
pInitStruct->pfImportFunctions = ImportFunctions;
|
||||
pInitStruct->pfGetVersion = CmpGetVersion;
|
||||
pInitStruct->pfHookFunction = HookFunction;
|
||||
pInitStruct->pfCreateInstance = NULL;
|
||||
pInitStruct->pfDeleteInstance = NULL;
|
||||
|
||||
s_pfCMRegisterAPI = pInitStruct->pfCMRegisterAPI;
|
||||
s_pfCMRegisterAPI2 = pInitStruct->pfCMRegisterAPI2;
|
||||
s_pfCMGetAPI = pInitStruct->pfCMGetAPI;
|
||||
s_pfCMGetAPI2 = pInitStruct->pfCMGetAPI2;
|
||||
s_pfCMCallHook = pInitStruct->pfCMCallHook;
|
||||
s_pfCMRegisterClass = pInitStruct->pfCMRegisterClass;
|
||||
s_pfCMCreateInstance = pInitStruct->pfCMCreateInstance;
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* <description>Publish exported functions to the component manager, to make them available for other components.
|
||||
* Use generated macro EXPORT_STMT.</description>
|
||||
* <result>ERR_OK or error code in case of error</result>
|
||||
*/
|
||||
static int CDECL ExportFunctions(void)
|
||||
{
|
||||
/* Macro to export functions */
|
||||
EXPORT_STMT;
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* <description>Import function pointers from other components. Use generated macro IMPORT_STMT.</description>
|
||||
* <result>ERR_OK or error code if function name could not be resolved</result>
|
||||
*/
|
||||
static int CDECL ImportFunctions(void)
|
||||
{
|
||||
/* Macro to import functions */
|
||||
IMPORT_STMT;
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* <description>Return the version of our component.</description>
|
||||
* <result>Version as defined in dep file</result>
|
||||
*/
|
||||
static RTS_UI32 CDECL CmpGetVersion(void)
|
||||
{
|
||||
return CMP_VERSION;
|
||||
}
|
||||
|
||||
static RTS_RESULT CDECL HookFunction(RTS_UI32 ulHook, RTS_UINTPTR ulParam1, RTS_UINTPTR ulParam2)
|
||||
{
|
||||
if (ulHook == CH_EXIT) ab_shutdown();
|
||||
|
||||
return ERR_OK;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Generated from supplied ExtensionSDK; native build on ARM64 Pi.
|
||||
SDKDIR ?= ../../ExtensionSDK
|
||||
MAJORVER=0
|
||||
MINORVER=3
|
||||
CMPVERSION=0x00030000
|
||||
# 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
|
||||
@@ -0,0 +1,10 @@
|
||||
#include "CmpStd.h"
|
||||
#include "CmpErrors.h"
|
||||
#include "CmpItf.h"
|
||||
#include "CmpAtomModbusDep.h"
|
||||
|
||||
void CDECL CDECL_EXT atmbcall_cext(atmbcall_cext_struct *p)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* <interfacename>CmpAtomModbus</interfacename>
|
||||
* <description></description>
|
||||
*
|
||||
* <copyright></copyright>
|
||||
*/
|
||||
|
||||
SET_INTERFACE_NAME(`CmpAtomModbus')
|
||||
|
||||
/** EXTERN LIB SECTION BEGIN **/
|
||||
/* Comments are ignored for m4 compiler so restructured text can be used. changecom(`/*', `*/') */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* <description>
|
||||
* Create FUNCTION atMbCall_cext returning UDINT, paste this declaration.
|
||||
* Properties > Build > External implementation must be selected.
|
||||
* Library filename and Project Information title: CmpAtomModbus.
|
||||
* This declaration is the SINGLE external entry point for the C engine.
|
||||
* </description>
|
||||
* <element name="atMbCall_cext" type=OUT></element>
|
||||
* <element name="command" type=IN></element>
|
||||
* <element name="handle" type=IN></element>
|
||||
* <element name="ipAddress" type=IN></element>
|
||||
* <element name="port" type=IN></element>
|
||||
* <element name="unitId" type=IN></element>
|
||||
* <element name="functionCode" type=IN></element>
|
||||
* <element name="startAddress" type=IN></element>
|
||||
* <element name="quantity" type=IN></element>
|
||||
* <element name="pData" type=IN></element>
|
||||
* <element name="dataCapacity" type=IN></element>
|
||||
* <element name="pStatus" type=IN></element>
|
||||
* <element name="statusCapacity" type=IN></element>
|
||||
* <element name="nowMs" type=IN></element>
|
||||
* <element name="connectTimeoutMs" type=IN></element>
|
||||
* <element name="responseTimeoutMs" type=IN></element>
|
||||
* <element name="retryDelayMs" type=IN></element>
|
||||
*/
|
||||
typedef struct tagatmbcall_cext_struct
|
||||
{
|
||||
RTS_IEC_UDINT command;
|
||||
RTS_IEC_UDINT handle;
|
||||
RTS_IEC_DWORD ipAddress;
|
||||
RTS_IEC_UINT port;
|
||||
RTS_IEC_BYTE unitId;
|
||||
RTS_IEC_BYTE functionCode;
|
||||
RTS_IEC_UINT startAddress;
|
||||
RTS_IEC_UINT quantity;
|
||||
RTS_IEC_WORD *pData;
|
||||
RTS_IEC_UDINT dataCapacity;
|
||||
RTS_IEC_UDINT *pStatus;
|
||||
RTS_IEC_UDINT statusCapacity;
|
||||
RTS_IEC_ULINT nowMs;
|
||||
RTS_IEC_UDINT connectTimeoutMs;
|
||||
RTS_IEC_UDINT responseTimeoutMs;
|
||||
RTS_IEC_UDINT retryDelayMs;
|
||||
RTS_IEC_UDINT atMbCall_cext;
|
||||
} atmbcall_cext_struct;
|
||||
|
||||
DEF_API(`void',`CDECL',`atmbcall_cext',`(atmbcall_cext_struct *p)',1,0x709EE65B,0x00020000)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** EXTERN LIB SECTION END **/
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#include "atom_bridge.h"
|
||||
#include "atom_mb.h"
|
||||
#include "atom_rtu.h"
|
||||
#include "atom_server.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
typedef struct { am_client c; ar_transport rtu; int serial; uint32_t generation; int used, retired; } slot;
|
||||
static slot slots[AM_POOL_SIZE];
|
||||
static uint32_t token(unsigned i) { return (slots[i].generation<<8)|(i+1u); }
|
||||
static slot *lookup(uint32_t h) {
|
||||
unsigned n=h&255u;
|
||||
if(!n || n>AM_POOL_SIZE) return NULL;
|
||||
slot *s=&slots[n-1];
|
||||
return s->used && s->generation==(h>>8)?s:NULL;
|
||||
}
|
||||
static void snapshot(const am_client *c,uint32_t h,uint32_t *s) {
|
||||
s[0]=h; s[1]=(uint32_t)c->state; s[2]=(uint32_t)c->busy;
|
||||
s[3]=(uint32_t)c->done; s[4]=(uint32_t)c->error; s[5]=(uint32_t)c->result;
|
||||
s[6]=(uint32_t)c->last_error; s[7]=(uint32_t)c->socket_error;
|
||||
s[8]=c->exception; s[9]=(uint32_t)c->write_uncertain;
|
||||
s[10]=c->connect_attempts; s[11]=c->reconnects; s[12]=c->completed; s[13]=c->failed;
|
||||
s[14]=(uint32_t)c->has_success; s[15]=(uint32_t)c->last_success_ms;
|
||||
s[16]=(uint32_t)(c->last_success_ms>>32); s[17]=c->quantity;
|
||||
s[18]=c->function; s[19]=c->unit; s[20]=(uint32_t)c->enabled;
|
||||
s[21]=c->connect_timeout_ms; s[22]=c->response_timeout_ms; s[23]=c->retry_delay_ms;
|
||||
}
|
||||
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 now,uint32_t ct,uint32_t rt,uint32_t retry) {
|
||||
slot *s; unsigned i; uint32_t result=0;
|
||||
if(!status || sc<AM_STATUS_WORDS) return AB_BAD_CAPACITY;
|
||||
if(cmd>=AS_OPEN && cmd<=AS_PULL)return as_call(cmd,h,ip,port,f,addr,q,data,cap,status,now,retry);
|
||||
/* Caller must provide distinct data/status buffers with declared capacities. */
|
||||
if(cmd==AB_OPEN || cmd==AB_OPEN_RTU) {
|
||||
char text[128];
|
||||
memset(status,0,AM_STATUS_WORDS*sizeof(*status));
|
||||
if(cmd==AB_OPEN && !port) return AM_ARGUMENT;
|
||||
for(i=0;i<AM_POOL_SIZE;i++) if(!slots[i].used && !slots[i].retired) break;
|
||||
if(i==AM_POOL_SIZE) return AB_POOL_FULL;
|
||||
s=&slots[i];
|
||||
snprintf(text,sizeof(text),"%u.%u.%u.%u",(unsigned)(ip>>24),(unsigned)((ip>>16)&255u),
|
||||
(unsigned)((ip>>8)&255u),(unsigned)(ip&255u));
|
||||
s->serial=(cmd==AB_OPEN_RTU);
|
||||
if(s->serial) {
|
||||
if(!data || !q || q>=sizeof(text) || cap<q)return AM_ARGUMENT;
|
||||
for(unsigned j=0;j<q;j++) { if(data[j]==0 || data[j]>127)return AM_ARGUMENT;text[j]=(char)data[j]; }
|
||||
text[q]=0;
|
||||
if(!ar_init(&s->c,&s->rtu,text,ip,unit,f,(uint8_t)addr,ct))return AM_ARGUMENT;
|
||||
} else if(!am_init(&s->c,text,port)) return AM_ARGUMENT;
|
||||
s->generation++; s->used=1; h=token(i);
|
||||
snapshot(&s->c,h,status); return 0;
|
||||
}
|
||||
s=lookup(h);
|
||||
if(!s) { memset(status,0,AM_STATUS_WORDS*sizeof(*status)); return AB_BAD_HANDLE; }
|
||||
switch(cmd) {
|
||||
case AB_CLOSE:
|
||||
if(s->serial)ar_close(&s->c);else am_close(&s->c); s->used=0;
|
||||
/* Never recycle a generation after wrap: stale handles stay invalid. */
|
||||
if(s->generation==0xffffffu) s->retired=1;
|
||||
snapshot(&s->c,0,status); return 0;
|
||||
case AB_ENABLE: am_enable(&s->c,1,now); break;
|
||||
case AB_DISABLE: if(s->serial)ar_close(&s->c);else am_enable(&s->c,0,now); break;
|
||||
case AB_TICK: if(s->serial)ar_tick(&s->c,&s->rtu,now);else am_tick(&s->c,now); break;
|
||||
case AB_RECONNECT: if(s->serial)ar_reconnect(&s->c,&s->rtu,now);else am_reconnect(&s->c,now); break;
|
||||
case AB_CONFIGURE:
|
||||
if(s->c.enabled) result=AM_BUSY;
|
||||
else if(!ct || !rt || !retry) result=AM_ARGUMENT;
|
||||
else { s->c.connect_timeout_ms=ct; s->c.response_timeout_ms=rt; s->c.retry_delay_ms=retry; }
|
||||
break;
|
||||
case AB_SUBMIT:
|
||||
if((f==5 || f==6 || f==15 || f==16) && (!data || cap<q)) result=AB_BAD_CAPACITY;
|
||||
else result=(uint32_t)(s->serial?ar_request(&s->c,&s->rtu,unit,f,addr,q,data,now):am_request(&s->c,unit,f,addr,q,data,now));
|
||||
break;
|
||||
case AB_COPY_READ:
|
||||
if(!s->c.done || s->c.function>4 || s->c.function<1) result=AM_ARGUMENT;
|
||||
else if(!data || cap<s->c.quantity) result=AB_BAD_CAPACITY;
|
||||
else memcpy(data,s->c.data,s->c.quantity*sizeof(*data));
|
||||
break;
|
||||
case AB_ACK: s->c.done=0; s->c.error=0; break;
|
||||
case AB_REQUEST_TIMEOUT:
|
||||
if(s->c.busy) result=AM_BUSY;
|
||||
else if(!rt) result=AM_ARGUMENT;
|
||||
else s->c.response_timeout_ms=rt;
|
||||
break;
|
||||
default: result=AB_BAD_COMMAND; break;
|
||||
}
|
||||
snapshot(&s->c,h,status); return result;
|
||||
}
|
||||
void ab_shutdown(void) {
|
||||
as_shutdown();
|
||||
unsigned i;
|
||||
for(i=0;i<AM_POOL_SIZE;i++) if(slots[i].used) {
|
||||
if(slots[i].serial)ar_close(&slots[i].c);else am_close(&slots[i].c); slots[i].used=0;
|
||||
if(slots[i].generation==0xffffffu) slots[i].retired=1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#ifndef ATOM_BRIDGE_H
|
||||
#define ATOM_BRIDGE_H
|
||||
#include <stdint.h>
|
||||
/* Fixed pool, one IEC task owns ALL calls. Do not share across tasks.
|
||||
* Only synchronous pointer access; generation-checked handles, no heap. */
|
||||
#define AM_STATUS_WORDS 24
|
||||
#define AM_POOL_SIZE 16
|
||||
enum { AB_OPEN=1, AB_CLOSE, AB_ENABLE, AB_DISABLE, AB_TICK, AB_SUBMIT,
|
||||
AB_COPY_READ, AB_RECONNECT, AB_CONFIGURE, AB_ACK, AB_REQUEST_TIMEOUT };
|
||||
enum { AB_OPEN_RTU=20 };
|
||||
enum { AB_BAD_HANDLE=256, AB_POOL_FULL, AB_BAD_CAPACITY, AB_BAD_COMMAND };
|
||||
uint32_t ab_call(uint32_t command,uint32_t handle,uint32_t ip,uint16_t port,
|
||||
uint8_t unit,uint8_t function,uint16_t address,uint16_t quantity,
|
||||
uint16_t *data,uint32_t capacity,uint32_t *status,uint32_t status_capacity,
|
||||
uint64_t now,uint32_t connect_ms,uint32_t response_ms,uint32_t retry_ms);
|
||||
/* Call on component unload; invalidate handles and close all sockets. */
|
||||
void ab_shutdown(void);
|
||||
#endif
|
||||
@@ -0,0 +1,147 @@
|
||||
#include "atom_mb.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
#ifndef MSG_NOSIGNAL
|
||||
#define MSG_NOSIGNAL 0
|
||||
#endif
|
||||
static uint16_t get16(const uint8_t *p) { return (uint16_t)((p[0]<<8)|p[1]); }
|
||||
static void put16(uint8_t *p,uint16_t v) { p[0]=(uint8_t)(v>>8); p[1]=(uint8_t)v; }
|
||||
static int writing(uint8_t f) { return f==5 || f==6 || f==15 || f==16; }
|
||||
static void drop(am_client *c) { if(c->fd>=0) { if(c->serial_port)ioctl(c->fd,TIOCNXCL);close(c->fd); } c->fd=-1; }
|
||||
static void finish(am_client *c,am_error e) {
|
||||
c->busy=0; c->done=e==AM_OK; c->error=e!=AM_OK; c->result=e;
|
||||
if(e==AM_OK) c->completed++; else { c->failed++; c->last_error=e; }
|
||||
}
|
||||
static void fault(am_client *c,am_error e,uint64_t now) {
|
||||
if(c->busy) { c->write_uncertain=writing(c->function) && c->tx_pos>0; finish(c,e); }
|
||||
c->last_error=e; drop(c); c->state=c->enabled?AM_RETRY:AM_DISABLED;
|
||||
c->deadline_ms=now+c->retry_delay_ms;
|
||||
}
|
||||
int am_init(am_client *c,const char *ip,uint16_t port) {
|
||||
struct in_addr a;
|
||||
if(!c || !ip || strlen(ip)>=16 || !port || inet_pton(AF_INET,ip,&a)!=1) return 0;
|
||||
memset(c,0,sizeof(*c)); c->fd=-1; strcpy(c->ip,ip); c->port=port;
|
||||
c->connect_timeout_ms=2000; c->response_timeout_ms=1000; c->retry_delay_ms=1000;
|
||||
return 1;
|
||||
}
|
||||
void am_close(am_client *c) { c->enabled=0; fault(c,AM_CANCELLED,0); }
|
||||
void am_enable(am_client *c,int enabled,uint64_t now) {
|
||||
if(!enabled) { if(c->enabled) am_close(c); return; }
|
||||
if(!c->enabled) { c->enabled=1; c->state=AM_RETRY; c->deadline_ms=now; }
|
||||
}
|
||||
void am_reconnect(am_client *c,uint64_t now) { fault(c,AM_CANCELLED,now); c->deadline_ms=now; }
|
||||
am_error am_request(am_client *c,uint8_t unit,uint8_t f,uint16_t addr,uint16_t q,
|
||||
const uint16_t *v,uint64_t now) {
|
||||
size_t n=12, bytes=0; unsigned i;
|
||||
if(c->busy) return AM_BUSY;
|
||||
if(c->state!=AM_READY) return AM_OFFLINE;
|
||||
if(!q || (uint32_t)addr+q>65536u) return AM_ARGUMENT;
|
||||
switch(f) {
|
||||
case 1: case 2: if(q>2000) return AM_ARGUMENT; break;
|
||||
case 3: case 4: if(q>125) return AM_ARGUMENT; break;
|
||||
case 5: case 6: if(q!=1 || !v || (f==5 && v[0]>1)) return AM_ARGUMENT; break;
|
||||
case 15: if(q>1968 || !v) return AM_ARGUMENT;
|
||||
for(i=0;i<q;i++) if(v[i]>1) return AM_ARGUMENT;
|
||||
bytes=(q+7u)/8u; n=13+bytes; break;
|
||||
case 16: if(q>123 || !v) return AM_ARGUMENT; bytes=2u*q; n=13+bytes; break;
|
||||
default: return AM_ARGUMENT;
|
||||
}
|
||||
memset(c->tx,0,sizeof(c->tx)); c->transaction++;
|
||||
put16(c->tx,c->transaction); put16(c->tx+4,(uint16_t)(n-6));
|
||||
c->tx[6]=unit; c->tx[7]=f; put16(c->tx+8,addr); put16(c->tx+10,q);
|
||||
if(f==5 || f==6) put16(c->tx+10,f==5?(v[0]?0xff00:0):v[0]);
|
||||
if(f==15 || f==16) {
|
||||
c->tx[12]=(uint8_t)bytes;
|
||||
for(i=0;i<q;i++) {
|
||||
if(f==16) put16(c->tx+13+2*i,v[i]);
|
||||
else if(v[i]) c->tx[13+i/8]|=(uint8_t)(1u<<(i%8));
|
||||
}
|
||||
}
|
||||
c->unit=unit; c->function=f; c->address=addr; c->quantity=q;
|
||||
c->tx_len=n; c->tx_pos=0; c->rx_len=0; c->rx_need=7;
|
||||
c->busy=1; c->done=0; c->error=0; c->exception=0; c->write_uncertain=0;
|
||||
c->result=AM_OK; c->state=AM_SENDING; c->deadline_ms=now+c->response_timeout_ms;
|
||||
return AM_OK;
|
||||
}
|
||||
void am_accept_response(am_client *c,uint64_t now) {
|
||||
unsigned i; size_t bytes;
|
||||
if(c->rx[7]==(uint8_t)(c->function|0x80)) {
|
||||
if(c->rx_need!=9 || !c->rx[8]) { fault(c,AM_PROTOCOL,now); return; }
|
||||
c->exception=c->rx[8]; finish(c,AM_EXCEPTION); c->state=AM_READY; return;
|
||||
}
|
||||
if(c->rx[7]!=c->function) { fault(c,AM_PROTOCOL,now); return; }
|
||||
if(writing(c->function)) {
|
||||
if(c->rx_need!=12 || memcmp(c->rx+8,c->tx+8,4)) { fault(c,AM_PROTOCOL,now); return; }
|
||||
} else {
|
||||
bytes=c->function<=2?(c->quantity+7u)/8u:2u*c->quantity;
|
||||
if(c->rx_need!=9+bytes || c->rx[8]!=bytes) { fault(c,AM_PROTOCOL,now); return; }
|
||||
for(i=0;i<c->quantity;i++) c->data[i]=c->function<=2?
|
||||
(uint16_t)((c->rx[9+i/8]>>(i%8))&1):get16(c->rx+9+2*i);
|
||||
}
|
||||
c->last_success_ms=now; c->has_success=1; finish(c,AM_OK); c->state=AM_READY;
|
||||
}
|
||||
void am_tick(am_client *c,uint64_t now) {
|
||||
struct sockaddr_in a; struct pollfd p; int e,r; socklen_t len; ssize_t n;
|
||||
if(!c->enabled) return;
|
||||
if(c->state==AM_RETRY) {
|
||||
if(now<c->deadline_ms) return;
|
||||
c->connect_attempts++; if(c->connect_attempts>1) c->reconnects++;
|
||||
c->socket_error=0; c->fd=socket(AF_INET,SOCK_STREAM,0);
|
||||
if(c->fd<0) { c->socket_error=errno; fault(c,AM_SOCKET,now); return; }
|
||||
r=fcntl(c->fd,F_GETFL,0);
|
||||
if(r<0 || fcntl(c->fd,F_SETFL,r|O_NONBLOCK)<0) { c->socket_error=errno; fault(c,AM_SOCKET,now); return; }
|
||||
memset(&a,0,sizeof(a)); a.sin_family=AF_INET; a.sin_port=htons(c->port);
|
||||
if(inet_pton(AF_INET,c->ip,&a.sin_addr)!=1) { fault(c,AM_ARGUMENT,now); return; }
|
||||
r=connect(c->fd,(struct sockaddr*)&a,sizeof(a));
|
||||
if(r==0) c->state=AM_READY;
|
||||
else if(errno==EINPROGRESS) { c->state=AM_CONNECTING; c->deadline_ms=now+c->connect_timeout_ms; }
|
||||
else { c->socket_error=errno; fault(c,AM_SOCKET,now); }
|
||||
return;
|
||||
}
|
||||
if(c->state==AM_CONNECTING) {
|
||||
if(now>=c->deadline_ms) { fault(c,AM_CONNECT_TIMEOUT,now); return; }
|
||||
p.fd=c->fd; p.events=POLLOUT; p.revents=0; r=poll(&p,1,0);
|
||||
if(r<0) { if(errno!=EINTR) { c->socket_error=errno; fault(c,AM_SOCKET,now); } return; }
|
||||
if(!r) return;
|
||||
len=sizeof(e); e=0;
|
||||
if(getsockopt(c->fd,SOL_SOCKET,SO_ERROR,&e,&len)<0) e=errno;
|
||||
if(e) { c->socket_error=e; fault(c,AM_SOCKET,now); } else c->state=AM_READY;
|
||||
return;
|
||||
}
|
||||
if(c->state==AM_READY) {
|
||||
/* Detect idle peer closure; unsolicited bytes cannot belong to a request. */
|
||||
uint8_t b; n=recv(c->fd,&b,1,MSG_PEEK);
|
||||
if(n==0) fault(c,AM_PEER_CLOSED,now);
|
||||
else if(n>0) fault(c,AM_PROTOCOL,now);
|
||||
else if(errno!=EAGAIN && errno!=EWOULDBLOCK && errno!=EINTR) { c->socket_error=errno; fault(c,AM_SOCKET,now); }
|
||||
return;
|
||||
}
|
||||
if(now>=c->deadline_ms) { fault(c,AM_RESPONSE_TIMEOUT,now); return; }
|
||||
if(c->state==AM_SENDING) {
|
||||
n=send(c->fd,c->tx+c->tx_pos,c->tx_len-c->tx_pos,MSG_NOSIGNAL);
|
||||
if(n>0) { c->tx_pos+=(size_t)n; if(c->tx_pos==c->tx_len) c->state=AM_RECEIVING; }
|
||||
else if(n==0) fault(c,AM_PEER_CLOSED,now);
|
||||
else if(errno!=EAGAIN && errno!=EWOULDBLOCK && errno!=EINTR) { c->socket_error=errno; fault(c,AM_SOCKET,now); }
|
||||
return;
|
||||
}
|
||||
if(c->state!=AM_RECEIVING) return;
|
||||
n=recv(c->fd,c->rx+c->rx_len,c->rx_need-c->rx_len,0);
|
||||
if(n==0) { fault(c,AM_PEER_CLOSED,now); return; }
|
||||
if(n<0) { if(errno!=EAGAIN && errno!=EWOULDBLOCK && errno!=EINTR) { c->socket_error=errno; fault(c,AM_SOCKET,now); } return; }
|
||||
c->rx_len+=(size_t)n;
|
||||
if(c->rx_len!=c->rx_need) return;
|
||||
if(c->rx_need==7) {
|
||||
uint16_t l=get16(c->rx+4);
|
||||
if(get16(c->rx)!=c->transaction || get16(c->rx+2)!=0 || c->rx[6]!=c->unit || l<3 || l>254) {
|
||||
fault(c,AM_PROTOCOL,now); return;
|
||||
}
|
||||
c->rx_need=6u+l; return;
|
||||
}
|
||||
am_accept_response(c,now);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#ifndef ATOM_MB_H
|
||||
#define ATOM_MB_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
/* Single owner/task per context. Init once, close before disposal. No heap allocation.
|
||||
* tick() time must be monotonic milliseconds. POSIX IPv4 transport, numeric IP only. */
|
||||
typedef enum { AM_DISABLED, AM_RETRY, AM_CONNECTING, AM_READY, AM_SENDING, AM_RECEIVING } am_state;
|
||||
typedef enum { AM_OK, AM_ARGUMENT, AM_BUSY, AM_OFFLINE, AM_SOCKET, AM_CONNECT_TIMEOUT,
|
||||
AM_RESPONSE_TIMEOUT, AM_PEER_CLOSED, AM_PROTOCOL, AM_EXCEPTION, AM_CANCELLED } am_error;
|
||||
typedef struct {
|
||||
int fd, enabled, serial_port;
|
||||
am_state state;
|
||||
am_error last_error, result;
|
||||
int socket_error;
|
||||
uint8_t exception, unit, function;
|
||||
uint16_t transaction, address, quantity;
|
||||
uint32_t connect_timeout_ms, response_timeout_ms, retry_delay_ms;
|
||||
uint64_t deadline_ms, last_success_ms;
|
||||
uint32_t connect_attempts, reconnects, completed, failed;
|
||||
int busy, done, error, write_uncertain, has_success;
|
||||
char ip[16]; uint16_t port;
|
||||
uint8_t tx[260], rx[260];
|
||||
size_t tx_len, tx_pos, rx_len, rx_need;
|
||||
/* Reads: coils/discretes unpacked as 0/1; registers as host-order uint16_t.
|
||||
* Result is copied only after the entire response validates. */
|
||||
uint16_t data[2000];
|
||||
} am_client;
|
||||
int am_init(am_client *c, const char *ip, uint16_t port);
|
||||
void am_enable(am_client *c, int enabled, uint64_t now_ms);
|
||||
void am_reconnect(am_client *c, uint64_t now_ms);
|
||||
void am_close(am_client *c);
|
||||
void am_tick(am_client *c, uint64_t now_ms);
|
||||
/* Supported functions: 1,2,3,4,5,6,15,16. Zero-based protocol addresses.
|
||||
* Writes copy input immediately. No automatic replay of a failed request.
|
||||
* Completion flags persist until next accepted request; result is per-request. */
|
||||
am_error am_request(am_client *c, uint8_t unit, uint8_t function,
|
||||
uint16_t address, uint16_t quantity, const uint16_t *values, uint64_t now_ms);
|
||||
/* Shared validated transport response parser. */
|
||||
void am_accept_response(am_client *, uint64_t);
|
||||
#endif
|
||||
@@ -0,0 +1,101 @@
|
||||
#ifndef _DEFAULT_SOURCE
|
||||
#define _DEFAULT_SOURCE
|
||||
#endif
|
||||
#include "atom_rtu.h"
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <string.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/ioctl.h>
|
||||
#ifdef __linux__
|
||||
#include <linux/serial.h>
|
||||
#endif
|
||||
uint16_t ar_crc(const uint8_t *p,size_t n) {
|
||||
uint16_t crc=0xffff;
|
||||
while(n--) { crc^=*p++; for(unsigned i=0;i<8;i++) crc=(uint16_t)((crc>>1)^((crc&1)?0xa001:0)); }
|
||||
return crc;
|
||||
}
|
||||
static speed_t ar_speed(uint32_t baud) {
|
||||
switch(baud) { case 1200:return B1200;case 2400:return B2400;case 4800:return B4800;
|
||||
case 9600:return B9600;case 19200:return B19200;case 38400:return B38400;
|
||||
case 57600:return B57600;case 115200:return B115200;default:return 0; }
|
||||
}
|
||||
int ar_init(am_client *c,ar_transport *r,const char *path,uint32_t baud,uint8_t parity,uint8_t bits,uint8_t stops,uint32_t rs485) {
|
||||
if(!path || !*path || strlen(path)>=sizeof(r->device) || !ar_speed(baud) || parity>2 || bits!=8 || (stops!=1 && stops!=2) || rs485>1) return 0;
|
||||
am_init(c,"127.0.0.1",1);c->serial_port=1;memset(r,0,sizeof(*r));strcpy(r->device,path);
|
||||
r->baud=baud;r->parity=parity;r->bits=bits;r->stops=stops;r->rs485=rs485;
|
||||
r->gap_ms=baud>19200?2:(uint32_t)((3500u*(1u+bits+(parity!=0)+stops)+baud-1u)/baud);
|
||||
return 1;
|
||||
}
|
||||
static void ar_fault(am_client *c,ar_transport *r,am_error error,uint64_t now) {
|
||||
if(c->busy) { c->write_uncertain=(c->function>=5 && r->tx_pos>0);c->busy=0;c->done=0;c->error=1;c->result=error;c->failed++; }
|
||||
c->last_error=error;if(c->fd>=0){ioctl(c->fd,TIOCNXCL);close(c->fd);}c->fd=-1;c->state=c->enabled?AM_RETRY:AM_DISABLED;
|
||||
c->deadline_ms=now+c->retry_delay_ms;r->quiet_until=c->deadline_ms;r->rx_len=0;
|
||||
}
|
||||
am_error ar_request(am_client *c,ar_transport *r,uint8_t unit,uint8_t f,uint16_t addr,uint16_t q,const uint16_t *v,uint64_t now) {
|
||||
/* Broadcast writes have no response; reject explicitly rather than fabricate success. */
|
||||
if(unit<1 || unit>247)return AM_ARGUMENT;
|
||||
am_error error=am_request(c,unit,f,addr,q,v,now);if(error!=AM_OK)return error;
|
||||
r->tx_len=c->tx_len-6;memcpy(r->tx,c->tx+6,r->tx_len);
|
||||
uint16_t crc=ar_crc(r->tx,r->tx_len);r->tx[r->tx_len++]=(uint8_t)crc;r->tx[r->tx_len++]=(uint8_t)(crc>>8);
|
||||
r->tx_pos=0;r->rx_len=0;r->rx_need=0;return AM_OK;
|
||||
}
|
||||
void ar_tick(am_client *c,ar_transport *r,uint64_t now) {
|
||||
struct termios options;ssize_t n;
|
||||
if(!c->enabled)return;
|
||||
if(c->state==AM_RETRY) {
|
||||
if(now<c->deadline_ms)return;
|
||||
c->connect_attempts++;if(c->connect_attempts>1)c->reconnects++;
|
||||
c->socket_error=0;c->fd=open(r->device,O_RDWR|O_NOCTTY|O_NONBLOCK|O_CLOEXEC);
|
||||
if(c->fd<0) { c->socket_error=errno;ar_fault(c,r,AM_SOCKET,now);return; }
|
||||
if(ioctl(c->fd,TIOCEXCL)<0 || tcgetattr(c->fd,&options)<0) goto serial_error;
|
||||
cfmakeraw(&options);options.c_cflag&=~(CSIZE|PARENB|PARODD|CSTOPB|CRTSCTS);
|
||||
options.c_cflag|=CLOCAL|CREAD|(r->bits==7?CS7:CS8);
|
||||
if(r->parity)options.c_cflag|=PARENB|(r->parity==1?PARODD:0);
|
||||
if(r->stops==2)options.c_cflag|=CSTOPB;
|
||||
options.c_cc[VMIN]=0;options.c_cc[VTIME]=0;
|
||||
if(cfsetispeed(&options,ar_speed(r->baud))<0 || cfsetospeed(&options,ar_speed(r->baud))<0 || tcsetattr(c->fd,TCSANOW,&options)<0 || tcflush(c->fd,TCIOFLUSH)<0) goto serial_error;
|
||||
#ifdef __linux__
|
||||
if(r->rs485) { struct serial_rs485 mode;memset(&mode,0,sizeof(mode));mode.flags=SER_RS485_ENABLED|SER_RS485_RTS_ON_SEND;
|
||||
if(ioctl(c->fd,TIOCSRS485,&mode)<0)goto serial_error; }
|
||||
#endif
|
||||
c->state=AM_READY;r->quiet_until=now+r->gap_ms;return;
|
||||
}
|
||||
if(c->state==AM_READY)return;
|
||||
if(now>=c->deadline_ms) { ar_fault(c,r,AM_RESPONSE_TIMEOUT,now);return; }
|
||||
if(c->state==AM_SENDING) {
|
||||
if(now<r->quiet_until)return;
|
||||
if(r->tx_pos==0 && tcflush(c->fd,TCIFLUSH)<0)goto serial_error;
|
||||
n=write(c->fd,r->tx+r->tx_pos,r->tx_len-r->tx_pos);
|
||||
if(n>0) { r->tx_pos+=(size_t)n;c->tx_pos=r->tx_pos;if(r->tx_pos==r->tx_len)c->state=AM_RECEIVING; }
|
||||
else if(n<0 && errno!=EAGAIN && errno!=EWOULDBLOCK && errno!=EINTR)goto serial_error;
|
||||
return;
|
||||
}
|
||||
if(c->state==AM_RECEIVING) {
|
||||
n=read(c->fd,r->rx+r->rx_len,sizeof(r->rx)-r->rx_len);
|
||||
if(n<0) { if(errno!=EAGAIN && errno!=EWOULDBLOCK && errno!=EINTR)goto serial_error;return; }
|
||||
if(n==0)return;
|
||||
r->rx_len+=(size_t)n;r->quiet_until=now+r->gap_ms;
|
||||
if(r->rx_len>=2 && (r->rx[0]!=c->unit || (r->rx[1]!=c->function && r->rx[1]!=(uint8_t)(c->function|0x80)))) { ar_fault(c,r,AM_PROTOCOL,now);return; }
|
||||
if(r->rx_len>=2) {
|
||||
if(r->rx[1]&0x80)r->rx_need=5;
|
||||
else if(c->function>=5)r->rx_need=8;
|
||||
else if(r->rx_len>=3)r->rx_need=5u+r->rx[2];
|
||||
}
|
||||
if(r->rx_need>sizeof(r->rx) || (r->rx_need && r->rx_len>r->rx_need)) { ar_fault(c,r,AM_PROTOCOL,now);return; }
|
||||
if(!r->rx_need || r->rx_len<r->rx_need)return;
|
||||
if(ar_crc(r->rx,r->rx_need)!=0) { ar_fault(c,r,AM_PROTOCOL,now);return; }
|
||||
/* Convert validated RTU ADU to the shared response validator's MBAP representation. */
|
||||
size_t payload=r->rx_need-2;memcpy(c->rx+6,r->rx,payload);c->rx_need=6+payload;
|
||||
am_accept_response(c,now);
|
||||
if(c->state==AM_RETRY)r->quiet_until=c->deadline_ms;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
serial_error:
|
||||
c->socket_error=errno;ar_fault(c,r,AM_SOCKET,now);
|
||||
}
|
||||
|
||||
void ar_close(am_client *c){if(c->fd>=0)ioctl(c->fd,TIOCNXCL);am_close(c);}
|
||||
void ar_reconnect(am_client *c,ar_transport *r,uint64_t now){ar_fault(c,r,AM_CANCELLED,now);c->deadline_ms=now;}
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef ATOM_RTU_H
|
||||
#define ATOM_RTU_H
|
||||
#include "atom_mb.h"
|
||||
typedef struct {
|
||||
char device[128]; uint32_t baud, rs485;
|
||||
uint8_t parity, bits, stops;
|
||||
uint8_t tx[256], rx[256]; size_t tx_len, tx_pos, rx_len, rx_need;
|
||||
uint64_t quiet_until; uint32_t gap_ms;
|
||||
} ar_transport;
|
||||
int ar_init(am_client *, ar_transport *,const char *,uint32_t,uint8_t,uint8_t,uint8_t,uint32_t);
|
||||
am_error ar_request(am_client *,ar_transport *,uint8_t,uint8_t,uint16_t,uint16_t,const uint16_t *,uint64_t);
|
||||
void ar_tick(am_client *,ar_transport *,uint64_t);
|
||||
uint16_t ar_crc(const uint8_t *,size_t);
|
||||
void ar_close(am_client *);
|
||||
void ar_reconnect(am_client *, ar_transport *, uint64_t);
|
||||
#endif
|
||||
@@ -0,0 +1,131 @@
|
||||
#include "atom_server.h"
|
||||
#include "atom_mb.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#define AS_POOL 4
|
||||
#define AS_PEERS 8
|
||||
#define AS_ITEMS 2000
|
||||
#ifndef MSG_NOSIGNAL
|
||||
#define MSG_NOSIGNAL 0
|
||||
#endif
|
||||
typedef struct { int fd;uint8_t rx[260],tx[260];size_t received,needed,sent,length;uint64_t deadline; } as_peer;
|
||||
typedef struct {
|
||||
int used,enabled,listener,running,error,os_error;uint32_t generation,received,replied,exceptions,retry;
|
||||
uint32_t ip;uint16_t port,base[4],count[4],values[4][AS_ITEMS];uint64_t retry_at;
|
||||
as_peer peers[AS_PEERS];
|
||||
} as_server;
|
||||
static as_server as_slots[AS_POOL];
|
||||
static uint16_t as_get16(const uint8_t *p){return (uint16_t)((p[0]<<8)|p[1]);}
|
||||
static void as_put16(uint8_t *p,uint16_t v){p[0]=(uint8_t)(v>>8);p[1]=(uint8_t)v;}
|
||||
static void as_peer_close(as_peer *p){if(p->fd>=0)close(p->fd);p->fd=-1;p->received=0;p->needed=7;p->sent=0;p->length=0;}
|
||||
static void as_stop(as_server *s){if(s->listener>=0)close(s->listener);s->listener=-1;s->running=0;for(unsigned i=0;i<AS_PEERS;i++)as_peer_close(&s->peers[i]);}
|
||||
static void as_snapshot(as_server *s,uint32_t h,uint32_t *o){
|
||||
memset(o,0,24*sizeof(*o));o[0]=h;o[1]=(uint32_t)s->running;o[2]=(uint32_t)s->error;o[3]=s->error?AM_SOCKET:AM_OK;o[4]=(uint32_t)s->os_error;
|
||||
for(unsigned i=0;i<AS_PEERS;i++)if(s->peers[i].fd>=0)o[5]++;
|
||||
o[6]=s->received;o[7]=s->replied;o[8]=s->exceptions;
|
||||
}
|
||||
static int as_nonblock(int fd){int flags=fcntl(fd,F_GETFL,0);return flags>=0 && fcntl(fd,F_SETFL,flags|O_NONBLOCK)==0;}
|
||||
static int as_range(as_server *s,unsigned table,uint16_t addr,uint16_t count){return count && addr>=s->base[table] && (uint32_t)addr+count<=(uint32_t)s->base[table]+s->count[table];}
|
||||
static void as_reply(as_server *s,as_peer *p){
|
||||
uint8_t f=p->rx[7],exception=0;uint16_t address=0,q=0;unsigned table=0,index=0;size_t n=9;
|
||||
if(p->needed<12){exception=(f==1 || f==2 || f==3 || f==4 || f==5 || f==6 || f==15 || f==16)?3:1;goto finish;}
|
||||
address=as_get16(p->rx+8);q=as_get16(p->rx+10);
|
||||
switch(f){
|
||||
case 1:case 2:case 3:case 4:
|
||||
table=f==1?0:f==2?1:f==3?2:3;
|
||||
if(p->needed!=12 || !q || q>(f<=2?2000:125)){exception=3;break;}
|
||||
if(!as_range(s,table,address,q)){exception=2;break;}
|
||||
index=address-s->base[table];p->tx[8]=(uint8_t)(f<=2?(q+7u)/8u:2u*q);n=9+p->tx[8];memset(p->tx+9,0,n-9);
|
||||
for(unsigned i=0;i<q;i++){if(f<=2){if(s->values[table][index+i])p->tx[9+i/8]|=(uint8_t)(1u<<(i%8));}else as_put16(p->tx+9+2*i,s->values[table][index+i]);}
|
||||
break;
|
||||
case 5:case 6:
|
||||
table=f==5?0:2;
|
||||
if(p->needed!=12 || (f==5 && q!=0 && q!=0xff00)){exception=3;break;}
|
||||
if(!as_range(s,table,address,1)){exception=2;break;}
|
||||
s->values[table][address-s->base[table]]=f==5?(q!=0):q;memcpy(p->tx+8,p->rx+8,4);n=12;break;
|
||||
case 15:case 16:
|
||||
table=f==15?0:2;
|
||||
if(!q || q>(f==15?1968:123) || p->needed<13 || p->rx[12]!=(f==15?(q+7u)/8u:2u*q) || p->needed!=(size_t)(13+p->rx[12])){exception=3;break;}
|
||||
if(!as_range(s,table,address,q)){exception=2;break;}
|
||||
index=address-s->base[table];
|
||||
for(unsigned i=0;i<q;i++)s->values[table][index+i]=f==15?(uint16_t)((p->rx[13+i/8]>>(i%8))&1):as_get16(p->rx+13+2*i);
|
||||
memcpy(p->tx+8,p->rx+8,4);n=12;break;
|
||||
default:exception=1;break;
|
||||
}
|
||||
finish:
|
||||
s->received++;memcpy(p->tx,p->rx,4);p->tx[6]=p->rx[6];p->tx[7]=f;
|
||||
if(exception){p->tx[7]=(uint8_t)(f|0x80);p->tx[8]=exception;n=9;s->exceptions++;}
|
||||
as_put16(p->tx+4,(uint16_t)(n-6));p->length=n;p->sent=0;
|
||||
}
|
||||
static void as_tick(as_server *s,uint64_t now){
|
||||
if(!s->enabled)return;
|
||||
if(s->listener<0){
|
||||
if(now<s->retry_at)return;
|
||||
struct sockaddr_in a;int reuse=1;s->listener=socket(AF_INET,SOCK_STREAM,0);
|
||||
if(s->listener<0)goto listener_error;
|
||||
setsockopt(s->listener,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));
|
||||
if(!as_nonblock(s->listener))goto listener_error;
|
||||
memset(&a,0,sizeof(a));a.sin_family=AF_INET;a.sin_port=htons(s->port);a.sin_addr.s_addr=htonl(s->ip);
|
||||
if(bind(s->listener,(struct sockaddr *)&a,sizeof(a))<0 || listen(s->listener,AS_PEERS)<0)goto listener_error;
|
||||
s->running=1;s->error=0;s->os_error=0;
|
||||
}
|
||||
/* One accept and one I/O action per peer per scan; fixed work/storage bounds. */
|
||||
int fd=accept(s->listener,NULL,NULL);
|
||||
if(fd>=0){unsigned i;for(i=0;i<AS_PEERS;i++)if(s->peers[i].fd<0)break;
|
||||
if(i==AS_PEERS || !as_nonblock(fd))close(fd);else{s->peers[i].fd=fd;s->peers[i].deadline=now+60000;}}
|
||||
else if(errno!=EAGAIN && errno!=EWOULDBLOCK && errno!=EINTR)goto listener_error;
|
||||
for(unsigned i=0;i<AS_PEERS;i++){
|
||||
as_peer *p=&s->peers[i];if(p->fd<0)continue;
|
||||
if(now>=p->deadline){as_peer_close(p);continue;}
|
||||
if(p->length){
|
||||
ssize_t n=send(p->fd,p->tx+p->sent,p->length-p->sent,MSG_NOSIGNAL);
|
||||
if(n>0){p->sent+=(size_t)n;if(p->sent==p->length){s->replied++;p->length=0;p->sent=0;p->received=0;p->needed=7;p->deadline=now+60000;}}
|
||||
else if(n==0 || (errno!=EAGAIN && errno!=EWOULDBLOCK && errno!=EINTR))as_peer_close(p);
|
||||
}else{
|
||||
ssize_t n=recv(p->fd,p->rx+p->received,p->needed-p->received,0);
|
||||
if(n>0){if(p->received==0)p->deadline=now+5000;p->received+=(size_t)n;
|
||||
if(p->received==7){uint16_t length=as_get16(p->rx+4);if(as_get16(p->rx+2)!=0 || length<2 || length>254){as_peer_close(p);continue;}p->needed=6u+length;}
|
||||
if(p->received==p->needed){as_reply(s,p);p->deadline=now+5000;}
|
||||
}else if(n==0 || (errno!=EAGAIN && errno!=EWOULDBLOCK && errno!=EINTR))as_peer_close(p);
|
||||
}
|
||||
}
|
||||
return;
|
||||
listener_error:
|
||||
s->os_error=errno;s->error=1;as_stop(s);s->retry_at=now+s->retry;
|
||||
}
|
||||
uint32_t as_call(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port,uint8_t table,uint16_t addr,uint16_t q,uint16_t *data,uint32_t cap,uint32_t *out,uint64_t now,uint32_t retry){
|
||||
as_server *s;unsigned i;uint32_t result=0;
|
||||
if(cmd==AS_OPEN){
|
||||
memset(out,0,24*sizeof(*out));if(!port || !retry)return AM_ARGUMENT;
|
||||
for(i=0;i<AS_POOL;i++)if(!as_slots[i].used && as_slots[i].generation<0xffffffu)break;
|
||||
if(i==AS_POOL)return 257;
|
||||
s=&as_slots[i];uint32_t generation=s->generation+1;memset(s,0,sizeof(*s));s->generation=generation;s->used=1;s->listener=-1;s->ip=ip;s->port=port;s->retry=retry;
|
||||
for(unsigned j=0;j<AS_PEERS;j++){s->peers[j].fd=-1;as_peer_close(&s->peers[j]);}
|
||||
h=(generation<<8)|(i+65u);as_snapshot(s,h,out);return 0;
|
||||
}
|
||||
i=(h&255u);if(i<65 || i>=65+AS_POOL){memset(out,0,24*sizeof(*out));return 256;}i-=64;
|
||||
if( !as_slots[i-1].used || as_slots[i-1].generation!=(h>>8)){memset(out,0,24*sizeof(*out));return 256;}
|
||||
s=&as_slots[i-1];
|
||||
switch(cmd){
|
||||
case AS_CLOSE:as_stop(s);s->used=0;as_snapshot(s,0,out);return 0;
|
||||
case AS_ENABLE:s->enabled=1;break;
|
||||
case AS_DISABLE:s->enabled=0;as_stop(s);break;
|
||||
case AS_TICK:as_tick(s,now);break;
|
||||
case AS_PUSH:case AS_PULL:
|
||||
if(table>3 || !q || q>AS_ITEMS || (uint32_t)addr+q>65536u)result=AM_ARGUMENT;
|
||||
else if(!data || cap<q)result=258;
|
||||
else if(cmd==AS_PUSH){
|
||||
if(table<2){for(unsigned j=0;j<q;j++)if(data[j]>1){result=AM_ARGUMENT;break;}}
|
||||
if(!result){s->base[table]=addr;s->count[table]=q;memcpy(s->values[table],data,q*sizeof(*data));}
|
||||
}else if(s->base[table]!=addr || s->count[table]!=q)result=AM_ARGUMENT;
|
||||
else memcpy(data,s->values[table],q*sizeof(*data));
|
||||
break;
|
||||
default:result=259;break;
|
||||
}
|
||||
as_snapshot(s,h,out);return result;
|
||||
}
|
||||
void as_shutdown(void){for(unsigned i=0;i<AS_POOL;i++)if(as_slots[i].used){as_stop(&as_slots[i]);as_slots[i].used=0;}}
|
||||
@@ -0,0 +1,7 @@
|
||||
#ifndef ATOM_SERVER_H
|
||||
#define ATOM_SERVER_H
|
||||
#include <stdint.h>
|
||||
enum { AS_OPEN=30,AS_CLOSE,AS_ENABLE,AS_DISABLE,AS_TICK,AS_PUSH,AS_PULL };
|
||||
uint32_t as_call(uint32_t,uint32_t,uint32_t,uint16_t,uint8_t,uint16_t,uint16_t,uint16_t *,uint32_t,uint32_t *,uint64_t,uint32_t);
|
||||
void as_shutdown(void);
|
||||
#endif
|
||||
@@ -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