First library, its tested and working

This commit is contained in:
2026-09-30 22:28:57 -06:00
commit ba3729a4a1
41 changed files with 7801 additions and 0 deletions
+16
View File
@@ -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 **/
+81
View File
@@ -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;
}
+13
View File
@@ -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 **/
+96
View File
@@ -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;
}
}
+18
View File
@@ -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
+147
View File
@@ -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);
}
+40
View File
@@ -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
+101
View File
@@ -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;}
+16
View File
@@ -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
+131
View File
@@ -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;}}
+7
View File
@@ -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
+29
View File
@@ -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;
}
+24
View File
@@ -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;
}
+19
View File
@@ -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;
}
+114
View File
@@ -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()
+59
View File
@@ -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)