mirror of
https://github.com/emqx/neuron.git
synced 2026-10-17 04:40:42 +08:00
ft(modbus): add serial port testing
This commit is contained in:
@@ -37,7 +37,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt install cppcheck
|
||||
cppcheck --enable=all --error-exitcode=2 --inline-suppr --suppress=missingInclude --suppress=unusedFunction ./plugins ./src
|
||||
cppcheck --enable=all --error-exitcode=2 --inline-suppr --suppress=missingInclude --suppress=unusedFunction --suppress=variableScope ./simulator
|
||||
cppcheck --enable=all --error-exitcode=2 --inline-suppr --suppress=missingInclude --suppress=variableScope ./simulator
|
||||
|
||||
ut:
|
||||
runs-on: ubuntu-20.04
|
||||
@@ -138,6 +138,7 @@ jobs:
|
||||
sudo apt-get install -y mosquitto
|
||||
fi
|
||||
pip install -U pytest requests prometheus-client gmqtt pynng sniffio
|
||||
sudo apt-get install -y socat
|
||||
|
||||
- name: compile
|
||||
run: |
|
||||
|
||||
@@ -20,3 +20,7 @@ find_package(Threads)
|
||||
add_executable(modbus_simulator modbus_simulator.c modbus_s.c)
|
||||
target_include_directories(modbus_simulator PRIVATE ${CMAKE_SOURCE_DIR}/include/neuron ${CMAKE_SOURCE_DIR})
|
||||
target_link_libraries(modbus_simulator neuron-base ${CMAKE_THREAD_LIBS_INIT} dl)
|
||||
|
||||
add_executable(modbus_tty_simulator modbus_tty_simulator.c modbus_s.c)
|
||||
target_include_directories(modbus_tty_simulator PRIVATE ${CMAKE_SOURCE_DIR}/include/neuron ${CMAKE_SOURCE_DIR})
|
||||
target_link_libraries(modbus_tty_simulator neuron-base ${CMAKE_THREAD_LIBS_INIT} dl)
|
||||
@@ -48,12 +48,12 @@ void modbus_s_init()
|
||||
{
|
||||
for (int i = 0; i < 3; i++) {
|
||||
for (int k = 0; k < 65536; k += 2) {
|
||||
rtu_registers[i].input[k].value = 1;
|
||||
tcp_registers[i].input[k].value = 1;
|
||||
rtu_registers[i].input[k].value = 0;
|
||||
tcp_registers[i].input[k].value = 0;
|
||||
}
|
||||
for (int k = 0; k < 65536; k++) {
|
||||
rtu_registers[i].input_register[k].value = k;
|
||||
tcp_registers[i].input_register[k].value = k;
|
||||
rtu_registers[i].input_register[k].value = 0;
|
||||
tcp_registers[i].input_register[k].value = 0;
|
||||
}
|
||||
|
||||
pthread_mutex_init(&rtu_registers[i].mutex, NULL);
|
||||
@@ -142,14 +142,14 @@ ssize_t modbus_s_rtu_req(uint8_t *req, uint16_t req_len, uint8_t *res,
|
||||
|
||||
if (ntohs(address->start_address) == 8999) {
|
||||
res_code->function += 0x80;
|
||||
len = modbus_read(&tcp_registers[code->slave_id - 1], res_code->function,
|
||||
len = modbus_read(&rtu_registers[code->slave_id - 1], res_code->function,
|
||||
address, res_value);
|
||||
res_data->n_byte = len;
|
||||
crc = (uint16_t *) (res_value + len);
|
||||
break;
|
||||
}
|
||||
|
||||
len = modbus_read(&tcp_registers[code->slave_id - 1], code->function,
|
||||
len = modbus_read(&rtu_registers[code->slave_id - 1], code->function,
|
||||
address, res_value);
|
||||
*res_len += sizeof(struct modbus_data);
|
||||
*res_len += len;
|
||||
@@ -162,7 +162,7 @@ ssize_t modbus_s_rtu_req(uint8_t *req, uint16_t req_len, uint8_t *res,
|
||||
case MODBUS_WRITE_M_COIL:
|
||||
case MODBUS_WRITE_S_HOLD_REG:
|
||||
case MODBUS_WRITE_M_HOLD_REG:
|
||||
modbus_write(&tcp_registers[code->slave_id - 1], code->function,
|
||||
modbus_write(&rtu_registers[code->slave_id - 1], code->function,
|
||||
address, data, value);
|
||||
*res_address = *address;
|
||||
|
||||
@@ -308,6 +308,7 @@ static int modbus_read(struct modbus_register *reg, uint8_t function,
|
||||
len += 1;
|
||||
}
|
||||
|
||||
memset(value, 0, ntohs(address->n_reg) / 8 + 1);
|
||||
for (int i = 0; i < ntohs(address->n_reg); i++) {
|
||||
if (function == MODBUS_READ_COIL) {
|
||||
value[i / 8] |=
|
||||
|
||||
@@ -134,7 +134,7 @@ int main(int argc, char *argv[])
|
||||
|
||||
zlog_init("./config/dev.conf");
|
||||
neuron = zlog_get_category("neuron");
|
||||
|
||||
modbus_s_init();
|
||||
neu_conn_param_t param = {
|
||||
.log = neuron,
|
||||
.type = NEU_CONN_TCP_SERVER,
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* NEURON IIoT System for Industry 4.0
|
||||
* Copyright (C) 2020-2022 EMQ Technologies Co., Ltd All rights reserved.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
**/
|
||||
#include <memory.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <termios.h>
|
||||
|
||||
#include <neuron.h>
|
||||
|
||||
#include "modbus_s.h"
|
||||
|
||||
#define UNUSED(X) ((void) (X))
|
||||
#define BUFFER_SIZE (4096)
|
||||
|
||||
zlog_category_t *neuron = NULL;
|
||||
neu_events_t * events = NULL;
|
||||
neu_event_io_t * event = NULL;
|
||||
neu_conn_t * conn = NULL;
|
||||
|
||||
struct cycle_buf {
|
||||
int len;
|
||||
uint8_t buf[BUFFER_SIZE];
|
||||
};
|
||||
|
||||
static struct cycle_buf recv_buf, res_buf;
|
||||
|
||||
static void connected(void *data, int fd);
|
||||
static void disconnected(void *data, int fd);
|
||||
|
||||
static void sig_handler(int sig)
|
||||
{
|
||||
(void) sig;
|
||||
|
||||
nlog_warn("recv sig: %d\n", sig);
|
||||
neu_conn_destory(conn);
|
||||
neu_event_close(events);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
static int slave_io_cb(enum neu_event_io_type type, int fd, void *usr_data)
|
||||
{
|
||||
UNUSED(usr_data);
|
||||
if (type != NEU_EVENT_IO_READ) {
|
||||
nlog_warn("read close!");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ret = read(fd, recv_buf.buf + recv_buf.len, BUFFER_SIZE - recv_buf.len);
|
||||
|
||||
if (ret <= 0) {
|
||||
nlog_warn("recv failed!");
|
||||
return -1;
|
||||
}
|
||||
|
||||
recv_buf.len += ret;
|
||||
|
||||
ret = modbus_s_rtu_req(recv_buf.buf, recv_buf.len, res_buf.buf, BUFFER_SIZE, &res_buf.len);
|
||||
|
||||
if (ret == -2) {
|
||||
printf("recv modbus adrress for retry_test, continue wait msg\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
neu_msleep(2);
|
||||
|
||||
ret = write(fd, res_buf.buf, res_buf.len);
|
||||
if (ret <= 0) {
|
||||
nlog_warn("send failed!");
|
||||
return -1;
|
||||
}
|
||||
|
||||
recv_buf.len = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
if (argc != 2 && argc != 7) {
|
||||
printf("./modbus_simulator device or device baud data "
|
||||
"parity stop flow\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
char *device = argv[1];
|
||||
|
||||
neu_conn_tty_baud_e baud = NEU_CONN_TTY_BAUD_9600;
|
||||
neu_conn_tty_parity_e parity = NEU_CONN_TTY_PARITY_NONE;
|
||||
neu_conn_tty_stop_e stop = NEU_CONN_TTY_STOP_1;
|
||||
neu_conn_tty_data_e data = NEU_CONN_TTY_DATA_8;
|
||||
neu_conn_tty_flow_e flow = NEU_CONN_TTYP_FLOW_DISABLE;
|
||||
|
||||
char *s_baud = argv[2];
|
||||
char *s_data = argv[3];
|
||||
char *s_parity = argv[4];
|
||||
char *s_stop = argv[5];
|
||||
char *s_flow = argv[6];
|
||||
|
||||
if (argc == 7) {
|
||||
int t_baud = atoi(s_baud);
|
||||
switch (t_baud) {
|
||||
case 2400:
|
||||
baud = NEU_CONN_TTY_BAUD_2400;
|
||||
break;
|
||||
case 4800:
|
||||
baud = NEU_CONN_TTY_BAUD_4800;
|
||||
break;
|
||||
case 9600:
|
||||
baud = NEU_CONN_TTY_BAUD_9600;
|
||||
break;
|
||||
case 19200:
|
||||
baud = NEU_CONN_TTY_BAUD_19200;
|
||||
break;
|
||||
case 38400:
|
||||
baud = NEU_CONN_TTY_BAUD_38400;
|
||||
break;
|
||||
case 57600:
|
||||
baud = NEU_CONN_TTY_BAUD_57600;
|
||||
break;
|
||||
case 115200:
|
||||
baud = NEU_CONN_TTY_BAUD_115200;
|
||||
break;
|
||||
default:
|
||||
printf("baud no match!input:2400 4800 9600 19200 38400 57600 "
|
||||
"115200\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int t_data = atoi(s_data);
|
||||
switch (t_data) {
|
||||
case 5:
|
||||
data = NEU_CONN_TTY_DATA_5;
|
||||
break;
|
||||
case 6:
|
||||
data = NEU_CONN_TTY_DATA_6;
|
||||
break;
|
||||
case 7:
|
||||
data = NEU_CONN_TTY_DATA_7;
|
||||
break;
|
||||
case 8:
|
||||
data = NEU_CONN_TTY_DATA_8;
|
||||
break;
|
||||
|
||||
default:
|
||||
printf("data no match!input:5 6 7 8");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (strcmp(s_parity, "none") == 0) {
|
||||
parity = NEU_CONN_TTY_PARITY_NONE;
|
||||
} else if (strcmp(s_parity, "odd") == 0) {
|
||||
parity = NEU_CONN_TTY_PARITY_ODD;
|
||||
} else if (strcmp(s_parity, "even") == 0) {
|
||||
parity = NEU_CONN_TTY_PARITY_EVEN;
|
||||
} else if (strcmp(s_parity, "mark") == 0) {
|
||||
parity = NEU_CONN_TTY_PARITY_MARK;
|
||||
} else if (strcmp(s_parity, "space") == 0) {
|
||||
parity = NEU_CONN_TTY_PARITY_SPACE;
|
||||
} else {
|
||||
printf("parity no match!input:none odd even mark space\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int t_stop = atoi(s_stop);
|
||||
switch (t_stop) {
|
||||
case 1:
|
||||
stop = NEU_CONN_TTY_STOP_1;
|
||||
break;
|
||||
case 2:
|
||||
stop = NEU_CONN_TTY_STOP_2;
|
||||
break;
|
||||
|
||||
default:
|
||||
printf("stop no match!input:1 2\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int t_flow = atoi(s_flow);
|
||||
switch (t_flow) {
|
||||
case 0:
|
||||
flow = NEU_CONN_TTYP_FLOW_DISABLE;
|
||||
break;
|
||||
case 1:
|
||||
flow = NEU_CONN_TTYP_FLOW_ENABLE;
|
||||
break;
|
||||
|
||||
default:
|
||||
printf("flow no match!input:0 1\n");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
zlog_init("./config/dev.conf");
|
||||
neuron = zlog_get_category("neuron");
|
||||
modbus_s_init();
|
||||
neu_conn_param_t param = {
|
||||
.log = neuron,
|
||||
.type = NEU_CONN_TTY_CLIENT,
|
||||
.params.tty_client.device = device,
|
||||
.params.tty_client.baud = baud,
|
||||
.params.tty_client.data = data,
|
||||
.params.tty_client.parity = parity,
|
||||
.params.tty_client.stop = stop,
|
||||
.params.tty_client.flow = flow,
|
||||
.params.tty_client.timeout = 3000,
|
||||
};
|
||||
|
||||
events = neu_event_new();
|
||||
conn = neu_conn_new(¶m, NULL, connected, disconnected);
|
||||
|
||||
neu_conn_start(conn);
|
||||
|
||||
neu_conn_connect(conn);
|
||||
|
||||
int fd = neu_conn_fd(conn);
|
||||
|
||||
if (fd <= 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
neu_event_io_param_t ioparam = { 0 };
|
||||
ioparam.fd = fd;
|
||||
ioparam.cb = slave_io_cb;
|
||||
ioparam.usr_data = conn;
|
||||
|
||||
event = neu_event_add_io(events, ioparam);
|
||||
|
||||
signal(SIGINT, sig_handler);
|
||||
signal(SIGTERM, sig_handler);
|
||||
signal(SIGABRT, sig_handler);
|
||||
|
||||
while (1) {
|
||||
sleep(10);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void connected(void *data, int fd)
|
||||
{
|
||||
(void) data;
|
||||
nlog_debug("connected %d", fd);
|
||||
return;
|
||||
}
|
||||
|
||||
static void disconnected(void *data, int fd)
|
||||
{
|
||||
(void) data;
|
||||
nlog_debug("disconnected %d", fd);
|
||||
return;
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
import subprocess
|
||||
import select
|
||||
import fcntl
|
||||
import re
|
||||
import os
|
||||
import neuron.api as api
|
||||
import neuron.error as error
|
||||
import neuron.config as config
|
||||
@@ -7,20 +12,53 @@ tcp_port = random_port()
|
||||
rtu_port = random_port()
|
||||
|
||||
|
||||
def start_socat():
|
||||
socat_cmd = ["socat", "-d", "-d", "pty,raw,echo=0", "pty,raw,echo=0"]
|
||||
socat_proc = subprocess.Popen(
|
||||
socat_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
|
||||
fl = fcntl.fcntl(socat_proc.stderr.fileno(), fcntl.F_GETFL)
|
||||
fcntl.fcntl(socat_proc.stderr.fileno(), fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
||||
|
||||
pty_regex = re.compile(r'N PTY is (/dev/pts/\d+)')
|
||||
dev1 = dev2 = None
|
||||
|
||||
while not dev1 or not dev2:
|
||||
ready = select.select([socat_proc.stderr], [], [], 5)
|
||||
if ready[0]:
|
||||
line = socat_proc.stderr.readline()
|
||||
match = pty_regex.search(line.decode('utf-8'))
|
||||
if match:
|
||||
if not dev1:
|
||||
dev1 = match.group(1)
|
||||
else:
|
||||
dev2 = match.group(1)
|
||||
|
||||
return socat_proc, dev1, dev2
|
||||
|
||||
|
||||
@pytest.fixture(params=[('modbus-tcp', config.PLUGIN_MODBUS_TCP),
|
||||
('modbus-rtu', config.PLUGIN_MODBUS_RTU)], scope='class')
|
||||
('modbus-rtu', config.PLUGIN_MODBUS_RTU),
|
||||
('modbus-rtu-tty', config.PLUGIN_MODBUS_RTU)], scope='class')
|
||||
def param(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope='class')
|
||||
def simulator_setup_teardown(param):
|
||||
if param[1] == config.PLUGIN_MODBUS_TCP:
|
||||
if param[0] == 'modbus-tcp':
|
||||
p = process.start_simulator(
|
||||
['./modbus_simulator', 'tcp', f'{tcp_port}', 'ip_v4'])
|
||||
else:
|
||||
elif param[0] == 'modbus-rtu':
|
||||
p = process.start_simulator(
|
||||
['./modbus_simulator', 'rtu', f'{rtu_port}', 'ip_v4'])
|
||||
elif param[0] == 'modbus-rtu-tty':
|
||||
socat_proc, modbus_neuron_dev, modbus_simulator_dev = start_socat()
|
||||
print(modbus_neuron_dev)
|
||||
print(modbus_simulator_dev)
|
||||
p = process.start_simulator(
|
||||
['./modbus_tty_simulator', f'{modbus_simulator_dev}'])
|
||||
|
||||
response = api.add_node(node=param[0], plugin=param[1])
|
||||
assert 200 == response.status_code
|
||||
@@ -28,15 +66,23 @@ def simulator_setup_teardown(param):
|
||||
response = api.add_group(node=param[0], group='group')
|
||||
assert 200 == response.status_code
|
||||
|
||||
if param[1] == config.PLUGIN_MODBUS_TCP:
|
||||
if param[0] == 'modbus-tcp':
|
||||
response = api.modbus_tcp_node_setting(
|
||||
node=param[0], interval=1, port=tcp_port)
|
||||
else:
|
||||
elif param[0] == 'modbus-rtu':
|
||||
response = api.modbus_rtu_node_setting(
|
||||
node=param[0], interval=1, port=rtu_port)
|
||||
elif param[0] == 'modbus-rtu-tty':
|
||||
api.update_group(param[0], group='group', interval=100)
|
||||
response = api.modbus_rtu_node_setting(
|
||||
node=param[0], interval=1, device=modbus_neuron_dev, link=0)
|
||||
|
||||
yield
|
||||
process.stop_simulator(p)
|
||||
if param[0] == 'modbus-rtu-tty':
|
||||
socat_proc.terminate()
|
||||
time.sleep(2)
|
||||
api.node_ctl_check(node=param[0], ctl=1)
|
||||
|
||||
|
||||
hold_bit = [{"name": "hold_bit", "address": "1!400001.15",
|
||||
@@ -926,7 +972,7 @@ class TestModbus:
|
||||
node=param[0], group='group', tag=coil_bit_m10[0]['name'])
|
||||
|
||||
@description(given="created discontinuous modbus node/tag", when="write multiple coil tags in one request", then="read/write success")
|
||||
def test_write_multiple_hold_tags_discontinuous(self, param):
|
||||
def test_write_multiple_coil_tags_discontinuous(self, param):
|
||||
api.write_tags_check(
|
||||
node=param[0], group='group', tag_values=[
|
||||
{"tag": coil_bit_m6[0]['name'], "value": 0},
|
||||
@@ -1075,6 +1121,8 @@ class TestModbus:
|
||||
|
||||
@description(given="created modbus device_error_test node/tag", when="read tag", then="read failed")
|
||||
def test_read_modbus_device_err(self, param):
|
||||
if param[0] == 'modbus-rtu-tty':
|
||||
pytest.skip("modbus rtu tty pass")
|
||||
api.add_tags_check(node=param[0], group='group', tags=hold_int16_device_err)
|
||||
time.sleep(4.0)
|
||||
assert error.NEU_ERR_PLUGIN_READ_FAILURE == api.read_tag_err(
|
||||
@@ -1082,6 +1130,8 @@ class TestModbus:
|
||||
|
||||
@description(given="created modbus node", when="create modbus retry_test tag, write and read tag", then="read/write success after retrying")
|
||||
def test_read_tag_retry(self, param):
|
||||
if param[0] == 'modbus-rtu-tty':
|
||||
pytest.skip("modbus rtu tty pass")
|
||||
api.add_tags_check(node=param[0], group='group', tags=hold_int16_retry_1)
|
||||
api.add_tags_check(node=param[0], group='group', tags=hold_int16_retry_2)
|
||||
|
||||
@@ -1099,12 +1149,15 @@ class TestModbus:
|
||||
def test_write_read_modbus_disconnected(self, param):
|
||||
response = api.add_node(node=param[0]+"_3002", plugin=param[1])
|
||||
assert 200 == response.status_code
|
||||
if param[1] == config.PLUGIN_MODBUS_TCP:
|
||||
if param[0] == 'modbus-tcp':
|
||||
api.modbus_tcp_node_setting(
|
||||
node=param[0]+"_3002", port=29998)
|
||||
else:
|
||||
elif param[0] == 'modbus-rtu':
|
||||
api.modbus_rtu_node_setting(
|
||||
node=param[0]+"_3002", port=29999)
|
||||
elif param[0] == 'modbus-rtu-tty':
|
||||
response = api.modbus_rtu_node_setting(
|
||||
node=param[0]+"_3002", device='tty', link=0)
|
||||
response = api.add_group(node=param[0]+"_3002", group='group')
|
||||
assert 200 == response.status_code
|
||||
|
||||
@@ -1115,12 +1168,14 @@ class TestModbus:
|
||||
|
||||
@description(given="created modbus node", when="set modbus tcp_server mode", then="set success")
|
||||
def test_set_modbus_tcp_server_mode(self, param):
|
||||
if param[1] == config.PLUGIN_MODBUS_TCP:
|
||||
if param[0] == 'modbus-tcp':
|
||||
response = api.modbus_tcp_node_setting(
|
||||
node=param[0]+"_3002", port=29997, connection_mode=1)
|
||||
else:
|
||||
elif param[0] == 'modbus-rtu':
|
||||
response = api.modbus_rtu_node_setting(
|
||||
node=param[0]+"_3002", port=29999, connection_mode=1)
|
||||
else:
|
||||
pytest.skip("modbus rtu tty pass")
|
||||
|
||||
assert 200 == response.status_code
|
||||
assert error.NEU_ERR_SUCCESS == response.json()['error']
|
||||
@@ -311,7 +311,7 @@ def modbus_tcp_node_setting(node, port, connection_mode=0, transport_mode=0, int
|
||||
"host": host, "port": port, "timeout": timeout, "max_retries": max_retries})
|
||||
|
||||
|
||||
def modbus_rtu_node_setting(node, port, connection_mode=0, transport_mode=0, interval=0, host='127.0.0.1', timeout=3000, max_retries=2, link=1, device="", stop=0, parity=0, baud=4, data=3):
|
||||
def modbus_rtu_node_setting(node, port=502, connection_mode=0, transport_mode=0, interval=0, host='127.0.0.1', timeout=3000, max_retries=2, link=1, device="", stop=0, parity=0, baud=4, data=3):
|
||||
return node_setting(node, json={"connection_mode": connection_mode, "transport_mode": transport_mode,
|
||||
"interval": interval, "host": host, "port": port, "timeout": timeout,
|
||||
"max_retries": max_retries, "link": link, "device": device, "stop": stop,
|
||||
|
||||
Reference in New Issue
Block a user