From 31112b1a2a63017c5c390eb3dda674ba1dd9d8bf Mon Sep 17 00:00:00 2001 From: hxy7yx <1595670487@qq.com> Date: Tue, 20 Feb 2024 17:04:00 +0800 Subject: [PATCH] ft(modbus): add serial port testing --- .github/workflows/check.yml | 3 +- simulator/CMakeLists.txt | 4 + simulator/modbus_s.c | 15 +- simulator/modbus_simulator.c | 2 +- simulator/modbus_tty_simulator.c | 271 +++++++++++++++++++++++++++++++ tests/ft/driver/test_modbus.py | 75 +++++++-- tests/ft/neuron/api.py | 2 +- 7 files changed, 352 insertions(+), 20 deletions(-) create mode 100644 simulator/modbus_tty_simulator.c diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index d26e2889..f72ca4ab 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -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: | diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index 92398dd4..a5ae93b8 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -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) \ No newline at end of file diff --git a/simulator/modbus_s.c b/simulator/modbus_s.c index 69d98f13..5c27191f 100644 --- a/simulator/modbus_s.c +++ b/simulator/modbus_s.c @@ -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] |= diff --git a/simulator/modbus_simulator.c b/simulator/modbus_simulator.c index 7df19c9e..46234ffa 100644 --- a/simulator/modbus_simulator.c +++ b/simulator/modbus_simulator.c @@ -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, diff --git a/simulator/modbus_tty_simulator.c b/simulator/modbus_tty_simulator.c new file mode 100644 index 00000000..2af8ebed --- /dev/null +++ b/simulator/modbus_tty_simulator.c @@ -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 +#include +#include +#include +#include +#include +#include +#include + +#include + +#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; +} \ No newline at end of file diff --git a/tests/ft/driver/test_modbus.py b/tests/ft/driver/test_modbus.py index 644881b6..3d9e0727 100644 --- a/tests/ft/driver/test_modbus.py +++ b/tests/ft/driver/test_modbus.py @@ -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'] \ No newline at end of file diff --git a/tests/ft/neuron/api.py b/tests/ft/neuron/api.py index a12f83ad..2a6df7a0 100644 --- a/tests/ft/neuron/api.py +++ b/tests/ft/neuron/api.py @@ -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,