Compare commits

...

5 Commits

Author SHA1 Message Date
Victor Antonovich b07681842f Add pyserial as a dependency for github workflow 2023-09-29 18:16:45 +03:00
Victor Antonovich b464ec9676 Update version to 0.5.2 and year to 2023 2023-09-29 18:09:00 +03:00
Victor Antonovich 6517dea57f Make use of Linux RS-485 support #97 2023-09-29 18:04:09 +03:00
Victor Antonovich 0f7a8c50fb Update .gitignore 2023-09-29 17:20:23 +03:00
Victor Antonovich 18fb6d086e Fix tests for changes in pymodbus API 2023-09-29 17:19:51 +03:00
15 changed files with 108 additions and 49 deletions

View File

@ -32,7 +32,7 @@ jobs:
- name: Install test dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twisted pymodbus
pip install setuptools wheel twisted pyserial pymodbus
sudo apt-get install -y socat
- name: Run tests

26
.gitignore vendored
View File

@ -1,25 +1,11 @@
/autom4te.cache
/config.cache
/config.h
/config.status
/.settings/
/src/.deps/
/src/*.o
/src/mbusd
/doc/mbusd.8
Makefile
/.settings
*.log
*.in~
systemd-units/mbusd@.service
*.conf
*.pyc
CMakeCache.txt
CMakeFiles
CPackConfig.cmake
CPackSourceConfig.cmake
cmake_install.cmake
.idea
*~
/.idea
/.vscode
.project
.cproject
build
.pydevproject
/build
build.sh

View File

@ -1,10 +1,11 @@
cmake_minimum_required(VERSION 3.2)
project(mbusd VERSION 0.5.1)
project(mbusd VERSION 0.5.2)
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/extern_GPL)
include(CheckFunctionExists)
include(CheckLibraryExists)
include(CheckSymbolExists)
include(GNUInstallDirs)
include(FindUnixCommands)
include(FindSystemd)
@ -41,6 +42,10 @@ check_function_exists(cfsetispeed HAVE_CFSETISPEED)
if(HAVE_CFSETSPEED AND HAVE_CFSETISPEED)
add_definitions(-DHAVE_CFSETSPEED)
endif()
check_symbol_exists(TIOCSRS485 sys/ioctl.h HAVE_TIOCRS485)
if(HAVE_TIOCRS485)
add_definitions(-DHAVE_TIOCRS485)
endif()
check_function_exists(time HAVE_TIME)
check_function_exists(localtime HAVE_LOCALTIME)
if(HAVE_TIME AND HAVE_LOCALTIME)

View File

@ -1,4 +1,4 @@
Copyright (c) 2002-2003, 2013-2022 Victor Antonovich (v.antonovich@gmail.com)
Copyright (c) 2002-2003, 2013-2023 Victor Antonovich (v.antonovich@gmail.com)
Copyright (c) 2011 Andrew Denysenko <nitr0@seti.kr.ua>
All rights reserved.

View File

@ -52,8 +52,10 @@ can be altered in many ways, e.g. by using the following tools in the `build` di
Usage:
------
mbusd [-h] [-d] [-L logfile] [-v level] [-c cfgfile] [-p device] [-s speed] [-m mode]
[-t] [-y file] [-Y file] [-A address] [-P port] [-C maxconn] [-N retries]
mbusd [-h] [-d] [-L logfile] [-v level] [-c cfgfile]
[-p device] [-s speed] [-m mode] [-S]
[-t] [-y sysfsfile] [-Y sysfsfile]
[-A address] [-P port] [-C maxconn] [-N retries]
[-R pause] [-W wait] [-T timeout]
-h Usage help.
@ -73,6 +75,7 @@ Usage:
Specifies serial port speed.
-m mode
Specifies serial port mode (like 8N1).
-S Enable RS-485 support for given serial port device (Linux only)
-t Enable RTS RS-485 data direction control (if not disabled while compile).
-y file
Enable RS-485 direction data direction control by writing '1' to file
@ -95,7 +98,7 @@ Usage:
-T timeout
Specifies connection timeout value in seconds (0 disables timeout).
-b
Instructs **mbusd** to reply on a broadcast.
Instructs mbusd to reply on a broadcast.
Please note running **mbusd** on default Modbus TCP port (502) requires root privileges!

View File

@ -15,6 +15,9 @@ speed = 9600
# Serial port mode
mode = 8n1
# Enable RS-485 support for given serial port device (Linux only)
# enable_rs485 = no
# RS-485 data direction control type (addc, rts, sysfs_0, sysfs_1)
trx_control = addc

View File

@ -19,9 +19,9 @@ mbusd \- MODBUS/TCP to MODBUS/RTU gateway.
.IR mode ]
.RB [ -t ]
.RB [ -y
.IR file ]
.IR sysfsfile ]
.RB [ -Y
.IR file ]
.IR sysfsfile ]
.RB [ -A
.IR address ]
.RB [ -P
@ -60,6 +60,8 @@ Specifies serial port device name.
Specifies serial port speed.
.IP "\fB-m \fImode\fR"
Specifies serial port mode (like 8N1).
.IP \fB-S\fR
Enable RS-485 support for given serial port device (Linux only).
.IP "\fB-A \fIaddress\fR"
Specifies TCP address to bind.
.IP "\fB-P \fIport\fR"

View File

@ -64,6 +64,9 @@ cfg_init(void)
strncpy(cfg.ttyport, DEFAULT_PORT, INTBUFSIZE);
cfg.ttyspeed = DEFAULT_SPEED;
strncpy(cfg.ttymode, DEFAULT_MODE, INTBUFSIZE);
#ifdef HAVE_TIOCRS485
cfg.rs485 = FALSE;
#endif
#ifdef TRXCTL
cfg.trxcntl = TRX_ADDC;
*cfg.trxcntl_file = '\0';
@ -183,6 +186,12 @@ cfg_handle_param(char *name, char *value)
cfg.conntimeout = strtoul(value, NULL, 0);
if (cfg.conntimeout > MAX_CONNTIMEOUT)
return 0;
#ifdef HAVE_TIOCRS485
}
else if (CFG_NAME_MATCH("enable_rs485"))
{
cfg.rs485 = CFG_VALUE_BOOL();
#endif
#ifdef TRXCTL
}
else if (CFG_NAME_MATCH("trx_control"))

View File

@ -54,10 +54,16 @@ typedef struct
int ttyspeed;
/* tty mode */
char ttymode[INTBUFSIZE + 1];
#ifdef HAVE_TIOCRS485
/* Linux RS-485 support use flag */
bool rs485;
#endif
#ifdef TRXCTL
/* trx control type (0 - ADDC, 1 - by RTS, 2 - by sysfs GPIO with 1 activating transmit, 3 - by sysfs GPIO with 0 activating transmit) */
int trxcntl;
/* trx control sysfs file */
char trxcntl_file[INTBUFSIZE + 1];
#endif
/* TCP server address */
char serveraddr[INTBUFSIZE + 1];
/* TCP server port number */

View File

@ -59,6 +59,7 @@
#include <netdb.h>
#include <fcntl.h>
#include <ctype.h>
#include <stdbool.h>
#ifdef HAVE_LIBUTIL
# include <libutil.h>
#endif

View File

@ -100,14 +100,18 @@ void
usage(char *exename)
{
cfg_init();
printf("%s-%s Copyright (C) 2002-2003, 2011, 2013-2022 Victor Antonovich <v.antonovich@gmail.com>, "
printf("%s-%s Copyright (C) 2002-2003, 2011, 2013-2023 Victor Antonovich <v.antonovich@gmail.com>, "
"Andrew Denysenko <nitr0@seti.kr.ua>\n\n"
"Usage: %s [-h] [-d] "
#ifdef LOG
"[-L logfile] [-v level] "
#endif
"[-c cfgfile] \n"
" [-p device] [-s speed] [-m mode]\n"
" [-p device] [-s speed] [-m mode]"
#ifdef HAVE_TIOCRS485
" [-S]"
#endif
"\n"
#ifdef TRXCTL
" [-t] [-y sysfsfile] [-Y sysfsfile]\n"
#endif
@ -129,6 +133,9 @@ usage(char *exename)
" -p device : set serial port device name (default %s)\n"
" -s speed : set serial port speed (default %d)\n"
" -m mode : set serial port mode (default %s)\n"
#ifdef HAVE_TIOCRS485
" -S : enable Linux RS-485 support for given serial port device\n"
#endif
" -A address : set TCP server address to bind (default %s)\n"
" -P port : set TCP server port number (default %d)\n"
#ifdef TRXCTL
@ -182,6 +189,9 @@ main(int argc, char *argv[])
#ifdef TRXCTL
"ty:Y:"
#endif
#ifdef HAVE_TIOCRS485
"S"
#endif
#ifdef LOG
"v:L:"
#endif
@ -213,11 +223,16 @@ main(int argc, char *argv[])
case 'y':
cfg.trxcntl = TRX_SYSFS_1;
strncpy(cfg.trxcntl_file, optarg, INTBUFSIZE);
break;
break;
case 'Y':
cfg.trxcntl = TRX_SYSFS_0;
strncpy(cfg.trxcntl_file, optarg, INTBUFSIZE);
break;
break;
#endif
#ifdef HAVE_TIOCRS485
case 'S':
cfg.rs485 = TRUE;
break;
#endif
#ifdef LOG
case 'v':

View File

@ -65,6 +65,9 @@ tty_init(ttydata_t *mod)
{
mod->bpc++;
}
#ifdef HAVE_TIOCRS485
mod->rs485 = cfg.rs485;
#endif
#ifdef TRXCTL
mod->trxcntl = cfg.trxcntl;
#endif
@ -187,6 +190,25 @@ tty_set_attr(ttydata_t *mod)
tcflush(mod->fd, TCIOFLUSH);
#ifdef TRXCTL
tty_clr_rts(mod->fd);
#endif
#ifdef HAVE_TIOCRS485
if (mod->rs485)
{
struct serial_rs485 rs485conf;
#ifdef LOG
logw(2, "tty: trying to enable RS-485 support for %s", mod->port);
#endif
if (ioctl(mod->fd, TIOCGRS485, &rs485conf) < 0) {
return RC_ERR;
}
rs485conf.flags |= SER_RS485_ENABLED;
if (ioctl(mod->fd, TIOCSRS485, &rs485conf) < 0) {
return RC_ERR;
}
#ifdef LOG
logw(2, "tty: enabled RS-485 support for %s", mod->port);
#endif
}
#endif
flag = fcntl(mod->fd, F_GETFL, 0);
if (flag < 0)

View File

@ -34,6 +34,10 @@
#ifndef _TTY_H
#define _TTY_H
#ifdef HAVE_TIOCRS485
#include <linux/serial.h>
#endif
#include "globals.h"
#include "cfg.h"
@ -96,6 +100,9 @@ typedef struct
int speed; /* serial port speed */
char *port; /* serial port device name */
int bpc; /* bits per character */
#ifdef HAVE_TIOCRS485
bool rs485; /* use Linux RS-485 support flag */
#endif
#ifdef TRXCTL
int trxcntl; /* trx control type (enum - see values in config.h) */
#endif

View File

@ -14,7 +14,7 @@ import logging
#---------------------------------------------------------------------------#
#from pymodbus.server.async import StartTcpServer
#from pymodbus.server.async import StartUdpServer
from pymodbus.server.asynchronous import StartSerialServer
from pymodbus.server import StartSerialServer
from pymodbus.device import ModbusDeviceIdentification
from pymodbus.datastore import ModbusSequentialDataBlock
@ -92,7 +92,7 @@ class ModbusSerialServer:
hr = ModbusSequentialDataBlock(0, [0]*8), # holding regs
ir = ModbusSequentialDataBlock(0, list(range(8))), # input regs
zero_mode=True) # request(0-7) will map to the address (0-7)
context = ModbusServerContext(slaves=store, single=True)
context = ModbusServerContext(slaves={1: store}, single=False)
#---------------------------------------------------------------------------#
# initialize the server information
@ -112,7 +112,7 @@ class ModbusSerialServer:
#---------------------------------------------------------------------------#
#StartTcpServer(context, identity=identity, address=("localhost", 5020))
#StartUdpServer(context, identity=identity, address=("localhost", 502))
StartSerialServer(context, identity=identity, port=self.serialPort, baudrate=19200, framer=framer, broadcast_enable=True)
StartSerialServer(context=context, identity=identity, port=self.serialPort, baudrate=19200, framer=framer, broadcast_enable=True)
#StartSerialServer(context, identity=identity, port='/dev/pts/3', framer=ModbusAsciiFramer)
p = None

View File

@ -8,7 +8,7 @@ from subprocess import Popen, PIPE, STDOUT
from os.path import isfile
from time import sleep
from pymodbus.client.sync import ModbusTcpClient
from pymodbus.client import ModbusTcpClient
from pymodbus.pdu import ExceptionResponse
from pymodbus.bit_read_message import ReadDiscreteInputsResponse, ReadCoilsResponse
from pymodbus.bit_write_message import WriteMultipleCoilsResponse, WriteSingleCoilResponse
@ -62,35 +62,35 @@ class TestModbusRequests(unittest.TestCase):
bits = [random.randrange(2)>0 for i in range(8)]
# 15 Write Multiple Coils
result = self.client.write_coils(0, bits, unit=1)
result = self.client.write_coils(0, bits, slave=1)
self.assertIsInstance(result, WriteMultipleCoilsResponse, result)
self.assertEqual(result.address, 0, result)
self.assertEqual(result.count, 8, result)
# 01 Read Coils
result = self.client.read_coils(0, 8, unit=1)
result = self.client.read_coils(0, 8, slave=1)
self.assertIsInstance(result, ReadCoilsResponse, result)
self.assertEqual(result.bits, bits, result)
# 05 Write Single Coil
bit1 = not bits[0]
result = self.client.write_coil(0, bit1, unit=1)
result = self.client.write_coil(0, bit1, slave=1)
self.assertIsInstance(result, WriteSingleCoilResponse, result)
self.assertEqual(result.address, 0, result)
self.assertEqual(result.value, bit1, result)
result = self.client.read_coils(0, 1, unit=1)
result = self.client.read_coils(0, 1, slave=1)
self.assertIsInstance(result, ReadCoilsResponse, result)
self.assertEqual(result.bits[0], bit1, result)
def test_discreteInputs(self):
# 02 Read Discrete Inputs
result = self.client.read_discrete_inputs(0, 8, unit=1)
result = self.client.read_discrete_inputs(0, 8, slave=1)
self.assertIsInstance(result, ReadDiscreteInputsResponse, result)
self.assertEqual(result.bits, [True]*8, result)
def test_inputRegisters(self):
# 04 Read Input Registers
result = self.client.read_input_registers(0, 8, unit=1)
result = self.client.read_input_registers(0, 8, slave=1)
self.assertIsInstance(result, ReadInputRegistersResponse, result)
self.assertEqual(result.registers, list(range(8)), result)
@ -98,28 +98,28 @@ class TestModbusRequests(unittest.TestCase):
registers = [random.randrange(8) for i in range(8)]
# 16 Write Multiple Holding Registers
result = self.client.write_registers(0, registers, unit=1)
result = self.client.write_registers(0, registers, slave=1)
self.assertIsInstance(result, WriteMultipleRegistersResponse, result)
self.assertEqual(result.address, 0, result)
self.assertEqual(result.count, 8, result)
# 03 Read Multiple Holding Registers
result = self.client.read_holding_registers(0, 8, unit=1)
result = self.client.read_holding_registers(0, 8, slave=1)
self.assertIsInstance(result, ReadHoldingRegistersResponse, result)
self.assertEqual(result.registers, registers, result)
# 06 Write Single Holding Register
register1 = (registers[0] + 1) % 65535
result = self.client.write_register(0, register1, unit=1)
result = self.client.write_register(0, register1, slave=1)
self.assertIsInstance(result, WriteSingleRegisterResponse, result)
self.assertEqual(result.address, 0, result)
self.assertEqual(result.value, register1, result)
result = self.client.read_holding_registers(0, 1, unit=1)
result = self.client.read_holding_registers(0, 1, slave=1)
self.assertIsInstance(result, ReadHoldingRegistersResponse, result)
self.assertEqual(result.registers[0], register1, result)
def test_exception(self):
result = self.client.write_coil(1000, False, unit=1) # invalid address 1000
result = self.client.write_coil(1000, False, slave=1) # invalid address 1000
self.assertIsInstance(result, ExceptionResponse, result)
self.assertEqual(result.original_code, 5, result) # fc05 Write Single Coil
self.assertEqual(result.exception_code, 2, result) # Illegal Data Address
@ -127,10 +127,10 @@ class TestModbusRequests(unittest.TestCase):
def test_broadcast(self):
registers = [random.randrange(8) for i in range(8)]
# 16 Write Multiple Holding Registers
result = self.client.write_registers(0, registers, unit=0)
result = self.client.write_registers(0, registers, slave=0)
# 03 Read Multiple Holding Registers
result = self.client.read_holding_registers(0, 8, unit=1)
result = self.client.read_holding_registers(0, 8, slave=1)
self.assertIsInstance(result, ReadHoldingRegistersResponse, result)
self.assertEqual(result.registers, registers, result)