diff --git a/.gitignore b/.gitignore index 012da87..8939043 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ .idea +cmake-build* +build* /tests/cmake-build* /tests/build* +/examples/cmake-build* +/examples/build* diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f3055e0 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.21) +project(modbusino C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -pedantic") +set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -O0 -g") +set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3") + +include_directories(tests .) + +#add_definitions(-DMBSN_DEBUG) + +add_executable(modbusino_tests modbusino.c tests/modbusino_tests.c) +add_executable(client-tcp modbusino.c examples/client-tcp.c) +add_executable(server-tcp modbusino.c examples/server-tcp.c) + +target_link_libraries(modbusino_tests pthread) diff --git a/examples/client-tcp.c b/examples/client-tcp.c new file mode 100644 index 0000000..c260b1d --- /dev/null +++ b/examples/client-tcp.c @@ -0,0 +1,92 @@ +#include "modbusino.h" +#include "platform.h" +#include + +/* + * This example application connects via TCP to a modbus server at the specified address and port, and sends some + * modbus requests to it. + * + * Since the platform for this example is linux, the platform arg is used to pass (to the linux TCP read/write + * functions) a pointer to the file descriptor of our TCP connection + * + * The platform functions are for Linux systems. + */ + +int main(int argc, char* argv[]) { + if (argc < 3) { + fprintf(stderr, "Usage: client-tcp [address] [port]\n"); + return 1; + } + + // Set up the TCP connection + void* conn = connect_tcp(argv[1], argv[2]); + if (!conn) { + fprintf(stderr, "Error connecting to server\n"); + return 1; + } + + mbsn_platform_conf platform_conf; + platform_conf.transport = MBSN_TRANSPORT_TCP; + platform_conf.read_byte = read_byte_fd_linux; + platform_conf.write_byte = write_byte_fd_linux; + platform_conf.sleep = sleep_linux; + platform_conf.arg = conn; // Passing our TCP connection handle to the read/write functions + + // Create the modbus client + mbsn_t mbsn; + mbsn_error err = mbsn_client_create(&mbsn, &platform_conf); + if (err != MBSN_ERROR_NONE) { + fprintf(stderr, "Error creating modbus client\n"); + return 1; + } + + // Set only the response timeout. Byte timeout will be handled by the TCP connection + mbsn_set_read_timeout(&mbsn, 1000); + + // Write 2 coils from address 64 + mbsn_bitfield coils; + mbsn_bitfield_write(coils, 0, 1); + mbsn_bitfield_write(coils, 1, 1); + err = mbsn_write_multiple_coils(&mbsn, 64, 2, coils); + if (err != MBSN_ERROR_NONE) { + fprintf(stderr, "Error writing coils at address 64 - %s\n", mbsn_strerror(err)); + return 1; + } + + // Read 3 coils from address 64 + mbsn_bitfield_reset(coils); // Reset whole bitfield to zero + err = mbsn_read_coils(&mbsn, 64, 3, coils); + if (err != MBSN_ERROR_NONE) { + fprintf(stderr, "Error reading coils at address 64 - %s\n", mbsn_strerror(err)); + return 1; + } + + printf("Coil at address 64 value: %d\n", mbsn_bitfield_read(coils, 0)); + printf("Coil at address 65 value: %d\n", mbsn_bitfield_read(coils, 1)); + printf("Coil at address 66 value: %d\n", mbsn_bitfield_read(coils, 2)); + + // Write 2 holding registers at address 26 + uint16_t w_regs[2] = {123, 124}; + err = mbsn_write_multiple_registers(&mbsn, 26, 2, w_regs); + if (err != MBSN_ERROR_NONE) { + fprintf(stderr, "Error writing register at address 26 - %s", mbsn_strerror(err)); + return 1; + } + + // Read 2 holding registers from address 26 + uint16_t r_regs[2]; + err = mbsn_read_holding_registers(&mbsn, 26, 2, r_regs); + if (err != MBSN_ERROR_NONE) { + fprintf(stderr, "Error reading 2 holding registers at address 26 - %s\n", mbsn_strerror(err)); + return 1; + } + + printf("Register at address 26: %d\n", r_regs[0]); + printf("Register at address 27: %d\n", r_regs[1]); + + // Close the TCP connection + disconnect(conn); + + // No need to destroy the mbsn instance, bye bye + return 0; +} diff --git a/examples/platform.h b/examples/platform.h new file mode 100644 index 0000000..7d7dede --- /dev/null +++ b/examples/platform.h @@ -0,0 +1,231 @@ +#include "modbusino.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +// Read/write/sleep platform functions + +int read_byte_fd_linux(uint8_t* b, int32_t timeout_ms, void* arg) { + int fd = *(int*) arg; + + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(fd, &rfds); + + struct timeval* tv_p = NULL; + struct timeval tv; + if (timeout_ms >= 0) { + tv_p = &tv; + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + } + + int ret = select(fd + 1, &rfds, NULL, NULL, tv_p); + if (ret == 0) { + return 0; + } + else if (ret == 1) { + ssize_t r = read(fd, b, 1); + if (r != 1) + return -1; + else { + return 1; + } + } + else + return -1; +} + + +int write_byte_fd_linux(uint8_t b, int32_t timeout_ms, void* arg) { + int fd = *(int*) arg; + + fd_set wfds; + FD_ZERO(&wfds); + FD_SET(fd, &wfds); + + struct timeval* tv_p = NULL; + struct timeval tv; + if (timeout_ms >= 0) { + tv_p = &tv; + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + } + + int ret = select(fd + 1, NULL, &wfds, NULL, tv_p); + if (ret == 0) { + return 0; + } + else if (ret == 1) { + ssize_t r = write(fd, &b, 1); + if (r != 1) + return -1; + else { + return 1; + } + } + else + return -1; +} + + +void sleep_linux(uint32_t milliseconds, void* arg) { + usleep(milliseconds * 1000); +} + + +// Connection management + +int client_connection = -1; + +void* connect_tcp(const char* address, const char* port) { + struct addrinfo ainfo = {0}; + struct addrinfo *results, *rp; + int fd; + + ainfo.ai_family = AF_INET; + ainfo.ai_socktype = SOCK_STREAM; + int ret = getaddrinfo(address, port, &ainfo, &results); + if (ret != 0) + return NULL; + + for (rp = results; rp != NULL; rp = rp->ai_next) { + fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (fd == -1) + continue; + + ret = connect(fd, rp->ai_addr, rp->ai_addrlen); + if (ret == 0) + break; + else + close(fd); + } + + freeaddrinfo(results); + + if (rp == NULL) + return NULL; + + client_connection = fd; + return &client_connection; +} + + +int server_fd = -1; +int client_read_fd = -1; +fd_set client_connections; + +void close_server_on_exit(int sig) { + if (server_fd != -1) + close(server_fd); +} + + +int create_tcp_server(const char* address, const char* port) { + struct addrinfo ainfo = {0}; + struct addrinfo *results, *rp; + int fd = -1; + + ainfo.ai_family = AF_INET; + ainfo.ai_socktype = SOCK_STREAM; + int ret = getaddrinfo(address, port, &ainfo, &results); + if (ret != 0) + return -1; + + for (rp = results; rp != NULL; rp = rp->ai_next) { + fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (fd == -1) + continue; + + ret = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)); + if (ret != 0) + return -1; + + ret = bind(fd, rp->ai_addr, rp->ai_addrlen); + if (ret != 0) { + close(fd); + fd = -1; + continue; + } + + ret = listen(fd, 1); + if (ret != 0) { + close(fd); + fd = -1; + continue; + } + + break; + } + + freeaddrinfo(results); + + signal(SIGINT, close_server_on_exit); + signal(SIGTERM, close_server_on_exit); + signal(SIGQUIT, close_server_on_exit); + signal(SIGSTOP, close_server_on_exit); + signal(SIGHUP, close_server_on_exit); + + server_fd = fd; + FD_ZERO(&client_connections); + + return 0; +} + + +void* server_poll() { + fd_set read_fd_set; + FD_ZERO(&read_fd_set); + + while (true) { + read_fd_set = client_connections; + FD_SET(server_fd, &read_fd_set); + + int ret = select(FD_SETSIZE, &read_fd_set, NULL, NULL, NULL); + if (ret < 0) + return NULL; + + for (int i = 0; i < FD_SETSIZE; ++i) { + if (FD_ISSET(i, &read_fd_set)) { + if (i == server_fd) { + struct sockaddr_in client_addr; + socklen_t client_addr_size = sizeof(client_addr); + + int client = accept(server_fd, (struct sockaddr*) &client_addr, &client_addr_size); + if (client < 0) { + fprintf(stderr, "Error accepting client connection from %s - %s\n", + inet_ntoa(client_addr.sin_addr), strerror(errno)); + continue; + } + + FD_SET(client, &client_connections); + printf("Accepted connection from %s\n", inet_ntoa(client_addr.sin_addr)); + } + else { + client_read_fd = i; + return &client_read_fd; + } + } + } + } +} + + +void disconnect(void* conn) { + int fd = *(int*) conn; + close(fd); +} + + +void close_server() { + close(server_fd); +} diff --git a/examples/server-handlers.h b/examples/server-handlers.h new file mode 100644 index 0000000..34fe927 --- /dev/null +++ b/examples/server-handlers.h @@ -0,0 +1,63 @@ +#include "modbusino.h" + +// Server (slave) handler functions + +// This server data model will support coils addresses 0 to 100 and registers addresses from 0 to 26 +#define COILS_ADDR_MAX 100 +#define REGS_ADDR_MAX 32 + +// A single mbsn_bitfield variable can keep 2000 coils +mbsn_bitfield server_coils = {0}; + +uint16_t server_registers[REGS_ADDR_MAX] = {0}; + + +mbsn_error handle_read_coils(uint16_t address, uint16_t quantity, mbsn_bitfield coils_out) { + if (address + quantity > COILS_ADDR_MAX + 1) + return MBSN_EXCEPTION_ILLEGAL_DATA_ADDRESS; + + // Read our coils values into coils_out + for (int i = 0; i < quantity; i++) { + bool value = mbsn_bitfield_read(server_coils, address + i); + mbsn_bitfield_write(coils_out, i, value); + } + + return MBSN_ERROR_NONE; +} + + +mbsn_error handle_write_multiple_coils(uint16_t address, uint16_t quantity, const mbsn_bitfield coils) { + if (address + quantity > COILS_ADDR_MAX + 1) + return MBSN_EXCEPTION_ILLEGAL_DATA_ADDRESS; + + // Write coils values to our server_coils + for (int i = 0; i < quantity; i++) { + mbsn_bitfield_write(server_coils, address + i, mbsn_bitfield_read(coils, i)); + } + + return MBSN_ERROR_NONE; +} + + +mbsn_error handler_read_holding_registers(uint16_t address, uint16_t quantity, uint16_t* registers_out) { + if (address + quantity > REGS_ADDR_MAX + 1) + return MBSN_EXCEPTION_ILLEGAL_DATA_ADDRESS; + + // Read our registers values into registers_out + for (int i = 0; i < quantity; i++) + registers_out[i] = server_registers[address + i]; + + return MBSN_ERROR_NONE; +} + + +mbsn_error handle_write_multiple_registers(uint16_t address, uint16_t quantity, const uint16_t* registers) { + if (address + quantity > REGS_ADDR_MAX + 1) + return MBSN_EXCEPTION_ILLEGAL_DATA_ADDRESS; + + // Write registers values to our server_registers + for (int i = 0; i < quantity; i++) + server_registers[address + i] = registers[i]; + + return MBSN_ERROR_NONE; +} diff --git a/examples/server-tcp.c b/examples/server-tcp.c new file mode 100644 index 0000000..59d7c2f --- /dev/null +++ b/examples/server-tcp.c @@ -0,0 +1,85 @@ +#include "modbusino.h" +#include "platform.h" +#include "server-handlers.h" +#include + +/* + * This example application sets up a TCP server at the specified address and port, and polls from modbus requests + * from more than one modbus client (more specifically from maximum 1024 clients, since it uses select()) + * + * This example server supports the following function codes: + * FC 01 (0x01) Read Coils + * FC 03 (0x03) Read Holding Registers + * FC 15 (0x0F) Write Multiple Coils + * FC 16 (0x10) Write Multiple registers + * +* Since the platform for this example is linux, the platform arg is used to pass (to the linux TCP read/write +* functions) a pointer to the file descriptor of the current read client connection + * + * The platform functions are for Linux systems. + */ + +int main(int argc, char* argv[]) { + if (argc < 3) { + fprintf(stderr, "Usage: server-tcp [address] [port]\n"); + return 1; + } + + // Set up the TCP server + int ret = create_tcp_server(argv[1], argv[2]); + if (ret != 0) { + fprintf(stderr, "Error creating TCP server\n"); + return 1; + } + + mbsn_platform_conf platform_conf = {0}; + platform_conf.transport = MBSN_TRANSPORT_TCP; + platform_conf.read_byte = read_byte_fd_linux; + platform_conf.write_byte = write_byte_fd_linux; + platform_conf.sleep = sleep_linux; + platform_conf.arg = NULL; // We will set the arg (socket fd) later + + // These functions are defined in server.h + mbsn_callbacks callbacks = {0}; + callbacks.read_coils = handle_read_coils; + callbacks.write_multiple_coils = handle_write_multiple_coils; + callbacks.read_holding_registers = handler_read_holding_registers; + callbacks.write_multiple_registers = handle_write_multiple_registers; + + // Create the modbus server. It's ok to set address_rtu to 0 since we are on TCP + mbsn_t mbsn; + mbsn_error err = mbsn_server_create(&mbsn, 0, &platform_conf, &callbacks); + if (err != MBSN_ERROR_NONE) { + fprintf(stderr, "Error creating modbus server\n"); + return 1; + } + + // Set only the polling timeout. Byte timeout will be handled by the TCP connection + mbsn_set_read_timeout(&mbsn, 1000); + + printf("Modbus TCP server started\n"); + + // Our server supports requests from more than one client + while (true) { + // Our server_poll() function will return the next client TCP connection to read from + void* conn = server_poll(); + if (!conn) + break; + + // Set the next connection handler used by the read/write platform functions + mbsn_set_platform_arg(&mbsn, conn); + + err = mbsn_server_poll(&mbsn); + if (err != MBSN_ERROR_NONE) { + fprintf(stderr, "Error polling modbus connection - %s\n", mbsn_strerror(err)); + // In a more complete example, we should handle this error by closing the connection from our side + break; + } + } + + // Close the TCP server + close_server(); + + // No need to destroy the mbsn instance, bye bye + return 0; +} diff --git a/modbusino.c b/modbusino.c index a804053..04d6659 100644 --- a/modbusino.c +++ b/modbusino.c @@ -60,16 +60,17 @@ static void msg_state_reset(mbsn_t* mbsn) { static void msg_state_req(mbsn_t* mbsn, uint8_t fc) { - static uint16_t TID = 1; + if (mbsn->current_tid == UINT16_MAX) + mbsn->current_tid = 1; + else + mbsn->current_tid++; msg_state_reset(mbsn); mbsn->msg.unit_id = mbsn->dest_address_rtu; mbsn->msg.fc = fc; - mbsn->msg.transaction_id = TID; - if (mbsn->msg.unit_id == 0) + mbsn->msg.transaction_id = mbsn->current_tid; + if (mbsn->msg.unit_id == 0 && mbsn->platform.transport == MBSN_TRANSPORT_RTU) mbsn->msg.broadcast = true; - - TID++; } @@ -103,17 +104,17 @@ mbsn_error mbsn_client_create(mbsn_t* mbsn, const mbsn_platform_conf* platform_c } -mbsn_error mbsn_server_create(mbsn_t* mbsn, uint8_t address, const mbsn_platform_conf* transport_conf, - mbsn_callbacks callbacks) { - if (address == 0) +mbsn_error mbsn_server_create(mbsn_t* mbsn, uint8_t address_rtu, const mbsn_platform_conf* platform_conf, + const mbsn_callbacks* callbacks) { + if (platform_conf->transport == MBSN_TRANSPORT_RTU && address_rtu == 0) return MBSN_ERROR_INVALID_ARGUMENT; - mbsn_error ret = mbsn_create(mbsn, transport_conf); + mbsn_error ret = mbsn_create(mbsn, platform_conf); if (ret != MBSN_ERROR_NONE) return ret; - mbsn->address_rtu = address; - mbsn->callbacks = callbacks; + mbsn->address_rtu = address_rtu; + mbsn->callbacks = *callbacks; return MBSN_ERROR_NONE; } @@ -139,9 +140,14 @@ void mbsn_set_destination_rtu_address(mbsn_t* mbsn, uint8_t address) { } -static uint16_t crc_calc(const uint8_t* data, unsigned int length) { +void mbsn_set_platform_arg(mbsn_t* mbsn, void* arg) { + mbsn->platform.arg = arg; +} + + +static uint16_t crc_calc(const uint8_t* data, uint32_t length) { uint16_t crc = 0xFFFF; - for (int i = 0; i < length; i++) { + for (uint32_t i = 0; i < length; i++) { crc ^= (uint16_t) data[i]; for (int j = 8; j != 0; j--) { if ((crc & 0x0001) != 0) { @@ -158,9 +164,10 @@ static uint16_t crc_calc(const uint8_t* data, unsigned int length) { static mbsn_error recv(mbsn_t* mbsn, uint32_t count) { - int r = 0; + uint32_t r = 0; while (r != count) { - int ret = mbsn->platform.read_byte(mbsn->msg.buf + mbsn->msg.buf_idx + r, mbsn->byte_timeout_ms); + int ret = mbsn->platform.read_byte(mbsn->msg.buf + mbsn->msg.buf_idx + r, mbsn->byte_timeout_ms, + mbsn->platform.arg); if (ret == 0) { #ifdef MBSN_DEBUG if (mbsn->address_rtu == 0) @@ -195,9 +202,9 @@ static mbsn_error send(mbsn_t* mbsn) { for (int i = 0; i < mbsn->msg.buf_idx; i++) { if (spacing_ms != 0) - mbsn->platform.sleep(spacing_ms); + mbsn->platform.sleep(spacing_ms, mbsn->platform.arg); - int ret = mbsn->platform.write_byte(mbsn->msg.buf[i], mbsn->read_timeout_ms); + int ret = mbsn->platform.write_byte(mbsn->msg.buf[i], mbsn->read_timeout_ms, mbsn->platform.arg); if (ret == 0) { return MBSN_ERROR_TIMEOUT; } @@ -307,13 +314,15 @@ static mbsn_error recv_req_header(mbsn_t* mbsn, bool* first_byte_received) { if (err != MBSN_ERROR_NONE) return err; - // Check if request is for us - if (mbsn->msg.unit_id == MBSN_BROADCAST_ADDRESS) - mbsn->msg.broadcast = true; - else if (mbsn->msg.unit_id != mbsn->address_rtu) - mbsn->msg.ignored = true; - else - mbsn->msg.ignored = false; + if (mbsn->platform.transport == MBSN_TRANSPORT_RTU) { + // Check if request is for us + if (mbsn->msg.unit_id == MBSN_BROADCAST_ADDRESS) + mbsn->msg.broadcast = true; + else if (mbsn->msg.unit_id != mbsn->address_rtu) + mbsn->msg.ignored = true; + else + mbsn->msg.ignored = false; + } return MBSN_ERROR_NONE; } @@ -374,9 +383,6 @@ static void send_msg_header(mbsn_t* mbsn, uint16_t data_length) { } put_1(mbsn, mbsn->msg.fc); - - if (mbsn->msg.unit_id == MBSN_BROADCAST_ADDRESS) - mbsn->msg.broadcast = true; } @@ -776,7 +782,7 @@ static mbsn_error handle_req_fc(mbsn_t* mbsn) { } -mbsn_error mbsn_server_receive(mbsn_t* mbsn) { +mbsn_error mbsn_server_poll(mbsn_t* mbsn) { msg_state_reset(mbsn); bool first_byte_received = false; @@ -788,75 +794,6 @@ mbsn_error mbsn_server_receive(mbsn_t* mbsn) { return err; } - /* - // We should wait for the read timeout for the first message byte - int32_t old_byte_timeout = mbsn->byte_timeout_ms; - mbsn->byte_timeout_ms = mbsn->read_timeout_ms; - - if (mbsn->transport == MBSN_TRANSPORT_RTU) { - uint8_t id; - mbsn_error err = recv_1(mbsn, &id, &req.crc); - - mbsn->byte_timeout_ms = old_byte_timeout; - - if (err != 0) { - if (err == MBSN_ERROR_TIMEOUT) - return MBSN_ERROR_NONE; - else - return err; - } - - // Check if request is for us - if (id == 0) - req.broadcast = true; - else if (id != mbsn->address_rtu) - req.ignored = true; - else - req.ignored = false; - - err = recv_1(mbsn, &req.fc, &req.crc); - if (err != MBSN_ERROR_NONE) - return err; - } - else if (mbsn->transport == MBSN_TRANSPORT_TCP) { - mbsn_error err = recv_2(mbsn, &req.transaction_id, NULL); - - mbsn->byte_timeout_ms = old_byte_timeout; - - if (err != 0) { - if (err == MBSN_ERROR_TIMEOUT) - return MBSN_ERROR_NONE; - else - return err; - } - - uint16_t protocol_id = 0xFFFF; - err = recv_2(mbsn, &protocol_id, NULL); - if (err != MBSN_ERROR_NONE) - return err; - - uint16_t length = 0xFFFF; - err = recv_2(mbsn, &length, NULL); - if (err != MBSN_ERROR_NONE) - return err; - - err = recv_1(mbsn, &req.unit_id, NULL); - if (err != MBSN_ERROR_NONE) - return err; - - err = recv_1(mbsn, &req.fc, NULL); - if (err != MBSN_ERROR_NONE) - return err; - - if (protocol_id != 0) - return MBSN_ERROR_TRANSPORT; - - // TODO maybe we should actually check the length of the request against this value - if (length == 0xFFFF) - return MBSN_ERROR_TRANSPORT; - } - */ - err = handle_req_fc(mbsn); if (err != MBSN_ERROR_NONE) { if (!mbsn_error_is_exception(err)) @@ -1159,7 +1096,7 @@ mbsn_error mbsn_write_multiple_registers(mbsn_t* mbsn, uint16_t address, uint16_ mbsn_error mbsn_send_raw_pdu(mbsn_t* mbsn, uint8_t fc, const void* data, uint32_t data_len) { msg_state_req(mbsn, fc); send_msg_header(mbsn, data_len); - for (int i = 0; i < data_len; i++) { + for (uint32_t i = 0; i < data_len; i++) { put_1(mbsn, ((uint8_t*) (data))[i]); } @@ -1176,7 +1113,7 @@ mbsn_error mbsn_receive_raw_pdu_response(mbsn_t* mbsn, void* data_out, uint32_t if (err != MBSN_ERROR_NONE) return err; - for (int i = 0; i < data_out_len; i++) { + for (uint32_t i = 0; i < data_out_len; i++) { ((uint8_t*) (data_out))[i] = get_1(mbsn); } @@ -1186,3 +1123,40 @@ mbsn_error mbsn_receive_raw_pdu_response(mbsn_t* mbsn, void* data_out, uint32_t return MBSN_ERROR_NONE; } + + +#ifndef MBSN_STRERROR_DISABLED +const char* mbsn_strerror(mbsn_error error) { + switch (error) { + case MBSN_ERROR_TRANSPORT: + return "transport error"; + + case MBSN_ERROR_TIMEOUT: + return "timeout"; + + case MBSN_ERROR_INVALID_RESPONSE: + return "invalid response received"; + + case MBSN_ERROR_INVALID_ARGUMENT: + return "invalid argument provided"; + + case MBSN_ERROR_NONE: + return "no error"; + + case MBSN_EXCEPTION_ILLEGAL_FUNCTION: + return "modbus exception 1: illegal function"; + + case MBSN_EXCEPTION_ILLEGAL_DATA_ADDRESS: + return "modbus exception 2: illegal data address"; + + case MBSN_EXCEPTION_ILLEGAL_DATA_VALUE: + return "modbus exception 3: data value"; + + case MBSN_EXCEPTION_SERVER_DEVICE_FAILURE: + return "modbus exception 4: server device failure"; + + default: + return "unknown error"; + } +} +#endif diff --git a/modbusino.h b/modbusino.h index 8727766..10e8316 100644 --- a/modbusino.h +++ b/modbusino.h @@ -3,6 +3,7 @@ #include #include +#include typedef enum mbsn_error { @@ -30,6 +31,8 @@ typedef uint8_t mbsn_bitfield[250]; #define mbsn_bitfield_write(bf, b, v) \ (((bf)[(b) / 8]) = ((v) ? (((bf)[(b) / 8]) | (0x1 << ((b) % 8))) : (((bf)[(b) / 8]) & ~(0x1 << ((b) % 8))))) +#define mbsn_bitfield_reset(bf) memset(bf, 0, sizeof(mbsn_bitfield)) + typedef enum mbsn_transport { MBSN_TRANSPORT_RTU = 1, @@ -38,9 +41,10 @@ typedef enum mbsn_transport { typedef struct mbsn_platform_conf { mbsn_transport transport; - int (*read_byte)(uint8_t* b, int32_t); - int (*write_byte)(uint8_t b, int32_t); - void (*sleep)(uint32_t milliseconds); + int (*read_byte)(uint8_t* b, int32_t, void* arg); + int (*write_byte)(uint8_t b, int32_t, void* arg); + void (*sleep)(uint32_t milliseconds, void* arg); + void* arg; } mbsn_platform_conf; @@ -78,6 +82,7 @@ typedef struct mbsn_t { uint8_t address_rtu; uint8_t dest_address_rtu; + uint16_t current_tid; } mbsn_t; @@ -86,8 +91,8 @@ static const uint8_t MBSN_BROADCAST_ADDRESS = 0; mbsn_error mbsn_client_create(mbsn_t* mbsn, const mbsn_platform_conf* platform_conf); -mbsn_error mbsn_server_create(mbsn_t* mbsn, uint8_t address, const mbsn_platform_conf* platform_conf, - mbsn_callbacks callbacks); +mbsn_error mbsn_server_create(mbsn_t* mbsn, uint8_t address_rtu, const mbsn_platform_conf* platform_conf, + const mbsn_callbacks* callbacks); void mbsn_set_read_timeout(mbsn_t* mbsn, int32_t timeout_ms); @@ -95,9 +100,11 @@ void mbsn_set_byte_timeout(mbsn_t* mbsn, int32_t timeout_ms); void mbsn_set_byte_spacing(mbsn_t* mbsn, uint32_t spacing_ms); +void mbsn_set_platform_arg(mbsn_t* mbsn, void* arg); + void mbsn_set_destination_rtu_address(mbsn_t* mbsn, uint8_t address); -mbsn_error mbsn_server_receive(mbsn_t* mbsn); +mbsn_error mbsn_server_poll(mbsn_t* mbsn); mbsn_error mbsn_read_coils(mbsn_t* mbsn, uint16_t address, uint16_t quantity, mbsn_bitfield coils_out); @@ -119,7 +126,9 @@ mbsn_error mbsn_send_raw_pdu(mbsn_t* mbsn, uint8_t fc, const void* data, uint32_ mbsn_error mbsn_receive_raw_pdu_response(mbsn_t* mbsn, void* data_out, uint32_t data_out_len); -const char* mbsn_strerror(int error); +#ifndef MBSN_STRERROR_DISABLED +const char* mbsn_strerror(mbsn_error error); +#endif #endif //MODBUSINO_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt deleted file mode 100644 index 418cca9..0000000 --- a/tests/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -cmake_minimum_required(VERSION 3.21) -project(modbusino_tests C) - -set(CMAKE_C_STANDARD 99) -set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -O0 -g") -set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3") - -include_directories(. ..) - -#add_definitions(-DMBSN_DEBUG) - -add_executable(modbusino_tests ../modbusino.c modbusino_tests.c) - -target_link_libraries(modbusino_tests pthread) diff --git a/tests/modbusino_tests.c b/tests/modbusino_tests.c index 786f71f..549935b 100644 --- a/tests/modbusino_tests.c +++ b/tests/modbusino_tests.c @@ -6,12 +6,18 @@ #include -int read_byte_empty(uint8_t* b, int32_t timeout) { +int read_byte_empty(uint8_t* b, int32_t timeout, void* arg) { + UNUSED_PARAM(b); + UNUSED_PARAM(timeout); + UNUSED_PARAM(arg); return 0; } -int write_byte_empty(uint8_t b, int32_t timeout) { +int write_byte_empty(uint8_t b, int32_t timeout, void* arg) { + UNUSED_PARAM(b); + UNUSED_PARAM(timeout); + UNUSED_PARAM(arg); return 0; } @@ -25,47 +31,56 @@ void test_server_create(mbsn_transport transport) { .write_byte = write_byte_empty, .sleep = platform_sleep}; + mbsn_callbacks callbacks_empty; + should("create a modbus server"); reset(mbsn); - err = mbsn_server_create(&mbsn, TEST_SERVER_ADDR, &platform_conf_empty, (mbsn_callbacks){}); + err = mbsn_server_create(&mbsn, TEST_SERVER_ADDR, &platform_conf_empty, &callbacks_empty); check(err); should("check parameters and fail to create a modbus server"); reset(mbsn); - err = mbsn_server_create(NULL, TEST_SERVER_ADDR, &platform_conf_empty, (mbsn_callbacks){}); + err = mbsn_server_create(NULL, TEST_SERVER_ADDR, &platform_conf_empty, &callbacks_empty); expect(err == MBSN_ERROR_INVALID_ARGUMENT); reset(mbsn); - err = mbsn_server_create(&mbsn, 0, &platform_conf_empty, (mbsn_callbacks){}); - expect(err == MBSN_ERROR_INVALID_ARGUMENT); + err = mbsn_server_create(&mbsn, 0, &platform_conf_empty, &callbacks_empty); + if (transport == MBSN_TRANSPORT_RTU) + expect(err == MBSN_ERROR_INVALID_ARGUMENT); + else + expect(err == MBSN_ERROR_NONE); reset(mbsn); mbsn_platform_conf p = platform_conf_empty; p.transport = 3; - err = mbsn_server_create(&mbsn, 0, &p, (mbsn_callbacks){}); + err = mbsn_server_create(&mbsn, 0, &p, &callbacks_empty); expect(err == MBSN_ERROR_INVALID_ARGUMENT); reset(mbsn); p = platform_conf_empty; p.read_byte = NULL; - err = mbsn_server_create(&mbsn, 0, &p, (mbsn_callbacks){}); + err = mbsn_server_create(&mbsn, 0, &p, &callbacks_empty); expect(err == MBSN_ERROR_INVALID_ARGUMENT); reset(mbsn); p = platform_conf_empty; p.write_byte = NULL; - err = mbsn_server_create(&mbsn, 0, &p, (mbsn_callbacks){}); + err = mbsn_server_create(&mbsn, 0, &p, &callbacks_empty); expect(err == MBSN_ERROR_INVALID_ARGUMENT); } -int read_byte_timeout(uint8_t* b, int32_t timeout) { +int read_byte_timeout(uint8_t* b, int32_t timeout, void* arg) { + UNUSED_PARAM(b); + UNUSED_PARAM(arg); usleep(timeout * 1000); return 0; } -int read_byte_timeout_third(uint8_t* b, int32_t timeout) { +int read_byte_timeout_third(uint8_t* b, int32_t timeout, void* arg) { + UNUSED_PARAM(arg); + static int stage = 0; switch (stage) { case 0: @@ -89,6 +104,7 @@ void test_server_receive_base(mbsn_transport transport) { mbsn_t server, client; mbsn_error err; mbsn_platform_conf platform_conf; + mbsn_callbacks callbacks_empty; should("honor read_timeout and return normally"); @@ -100,7 +116,7 @@ void test_server_receive_base(mbsn_transport transport) { const int32_t read_timeout_ms = 250; - err = mbsn_server_create(&server, TEST_SERVER_ADDR, &platform_conf, (mbsn_callbacks){}); + err = mbsn_server_create(&server, TEST_SERVER_ADDR, &platform_conf, &callbacks_empty); check(err); mbsn_set_read_timeout(&server, read_timeout_ms); @@ -109,12 +125,12 @@ void test_server_receive_base(mbsn_transport transport) { const int polls = 5; for (int i = 0; i < polls; i++) { uint64_t start = now_ms(); - err = mbsn_server_receive(&server); + err = mbsn_server_poll(&server); check(err); uint64_t diff = now_ms() - start; - assert(diff >= read_timeout_ms); + assert(diff >= (uint64_t) read_timeout_ms); } @@ -126,13 +142,13 @@ void test_server_receive_base(mbsn_transport transport) { const int32_t byte_timeout_ms = 250; - err = mbsn_server_create(&server, TEST_SERVER_ADDR, &platform_conf, (mbsn_callbacks){}); + err = mbsn_server_create(&server, TEST_SERVER_ADDR, &platform_conf, &callbacks_empty); check(err); mbsn_set_read_timeout(&server, 1000); mbsn_set_byte_timeout(&server, byte_timeout_ms); - err = mbsn_server_receive(&server); + err = mbsn_server_poll(&server); expect(err == MBSN_ERROR_TIMEOUT); @@ -193,15 +209,16 @@ mbsn_error read_discrete(uint16_t address, uint16_t quantity, mbsn_bitfield coil void test_fc1(mbsn_transport transport) { const uint8_t fc = 1; uint8_t raw_res[260]; + mbsn_callbacks callbacks_empty = {0}; - start_client_and_server(transport, (mbsn_callbacks){}); + start_client_and_server(transport, &callbacks_empty); should("return MBSN_EXCEPTION_ILLEGAL_FUNCTION when callback is not registered server-side"); assert(mbsn_read_coils(&CLIENT, 0, 1, NULL) == MBSN_EXCEPTION_ILLEGAL_FUNCTION); stop_client_and_server(); - start_client_and_server(transport, (mbsn_callbacks){.read_coils = read_discrete}); + start_client_and_server(transport, &(mbsn_callbacks){.read_coils = read_discrete}); should("immediately return MBSN_ERROR_INVALID_ARGUMENT when calling with quantity 0"); assert(mbsn_read_coils(&CLIENT, 1, 0, NULL) == MBSN_ERROR_INVALID_ARGUMENT); @@ -259,15 +276,16 @@ void test_fc1(mbsn_transport transport) { void test_fc2(mbsn_transport transport) { const uint8_t fc = 2; uint8_t raw_res[260]; + mbsn_callbacks callbacks_empty = {0}; - start_client_and_server(transport, (mbsn_callbacks){}); + start_client_and_server(transport, &callbacks_empty); should("return MBSN_EXCEPTION_ILLEGAL_FUNCTION when callback is not registered server-side"); assert(mbsn_read_discrete_inputs(&CLIENT, 0, 1, NULL) == MBSN_EXCEPTION_ILLEGAL_FUNCTION); stop_client_and_server(); - start_client_and_server(transport, (mbsn_callbacks){.read_discrete_inputs = read_discrete}); + start_client_and_server(transport, &(mbsn_callbacks){.read_discrete_inputs = read_discrete}); should("immediately return MBSN_ERROR_INVALID_ARGUMENT when calling with quantity 0"); assert(mbsn_read_discrete_inputs(&CLIENT, 1, 0, NULL) == MBSN_ERROR_INVALID_ARGUMENT); @@ -345,15 +363,16 @@ mbsn_error read_registers(uint16_t address, uint16_t quantity, uint16_t* registe void test_fc3(mbsn_transport transport) { const uint8_t fc = 3; uint8_t raw_res[260]; + mbsn_callbacks callbacks_empty = {0}; - start_client_and_server(transport, (mbsn_callbacks){}); + start_client_and_server(transport, &callbacks_empty); should("return MBSN_EXCEPTION_ILLEGAL_FUNCTION when callback is not registered server-side"); assert(mbsn_read_holding_registers(&CLIENT, 0, 1, NULL) == MBSN_EXCEPTION_ILLEGAL_FUNCTION); stop_client_and_server(); - start_client_and_server(transport, (mbsn_callbacks){.read_holding_registers = read_registers}); + start_client_and_server(transport, &(mbsn_callbacks){.read_holding_registers = read_registers}); should("immediately return MBSN_ERROR_INVALID_ARGUMENT when calling with quantity 0"); assert(mbsn_read_holding_registers(&CLIENT, 1, 0, NULL) == MBSN_ERROR_INVALID_ARGUMENT); @@ -399,15 +418,16 @@ void test_fc3(mbsn_transport transport) { void test_fc4(mbsn_transport transport) { const uint8_t fc = 4; uint8_t raw_res[260]; + mbsn_callbacks callbacks_empty = {0}; - start_client_and_server(transport, (mbsn_callbacks){}); + start_client_and_server(transport, &callbacks_empty); should("return MBSN_EXCEPTION_ILLEGAL_FUNCTION when callback is not registered server-side"); assert(mbsn_read_input_registers(&CLIENT, 0, 1, NULL) == MBSN_EXCEPTION_ILLEGAL_FUNCTION); stop_client_and_server(); - start_client_and_server(transport, (mbsn_callbacks){.read_input_registers = read_registers}); + start_client_and_server(transport, &(mbsn_callbacks){.read_input_registers = read_registers}); should("immediately return MBSN_ERROR_INVALID_ARGUMENT when calling with quantity 0"); assert(mbsn_read_input_registers(&CLIENT, 1, 0, NULL) == MBSN_ERROR_INVALID_ARGUMENT); @@ -473,15 +493,16 @@ mbsn_error write_coil(uint16_t address, bool value) { void test_fc5(mbsn_transport transport) { const uint8_t fc = 5; uint8_t raw_res[260]; + mbsn_callbacks callbacks_empty = {0}; - start_client_and_server(transport, (mbsn_callbacks){}); + start_client_and_server(transport, &callbacks_empty); should("return MBSN_EXCEPTION_ILLEGAL_FUNCTION when callback is not registered server-side"); assert(mbsn_write_single_coil(&CLIENT, 0, true) == MBSN_EXCEPTION_ILLEGAL_FUNCTION); stop_client_and_server(); - start_client_and_server(transport, (mbsn_callbacks){.write_single_coil = write_coil}); + start_client_and_server(transport, &(mbsn_callbacks){.write_single_coil = write_coil}); should("return MBSN_EXCEPTION_ILLEGAL_DATA_VALUE when calling with value !0x0000 or 0xFF000"); check(mbsn_send_raw_pdu(&CLIENT, fc, (uint16_t[]){htons(6), htons(0x0001)}, 4)); @@ -537,15 +558,16 @@ mbsn_error write_register(uint16_t address, uint16_t value) { void test_fc6(mbsn_transport transport) { const uint8_t fc = 6; uint8_t raw_res[260]; + mbsn_callbacks callbacks_empty = {0}; - start_client_and_server(transport, (mbsn_callbacks){}); + start_client_and_server(transport, &callbacks_empty); should("return MBSN_EXCEPTION_ILLEGAL_FUNCTION when callback is not registered server-side"); assert(mbsn_write_single_register(&CLIENT, 0, 123) == MBSN_EXCEPTION_ILLEGAL_FUNCTION); stop_client_and_server(); - start_client_and_server(transport, (mbsn_callbacks){.write_single_register = write_register}); + start_client_and_server(transport, &(mbsn_callbacks){.write_single_register = write_register}); should("return MBSN_EXCEPTION_SERVER_DEVICE_FAILURE when server handler returns any non-exception error"); assert(mbsn_write_single_register(&CLIENT, 1, 123) == MBSN_EXCEPTION_SERVER_DEVICE_FAILURE); @@ -617,15 +639,16 @@ void test_fc15(mbsn_transport transport) { const uint8_t fc = 15; uint8_t raw_res[260]; mbsn_bitfield bf = {0}; + mbsn_callbacks callbacks_empty = {0}; - start_client_and_server(transport, (mbsn_callbacks){}); + start_client_and_server(transport, &callbacks_empty); should("return MBSN_EXCEPTION_ILLEGAL_FUNCTION when callback is not registered server-side"); assert(mbsn_write_multiple_coils(&CLIENT, 0, 1, bf) == MBSN_EXCEPTION_ILLEGAL_FUNCTION); stop_client_and_server(); - start_client_and_server(transport, (mbsn_callbacks){.write_multiple_coils = write_coils}); + start_client_and_server(transport, &(mbsn_callbacks){.write_multiple_coils = write_coils}); should("immediately return MBSN_ERROR_INVALID_ARGUMENT when calling with quantity 0"); assert(mbsn_write_multiple_coils(&CLIENT, 1, 0, bf) == MBSN_ERROR_INVALID_ARGUMENT); @@ -730,15 +753,16 @@ void test_fc16(mbsn_transport transport) { const uint8_t fc = 16; uint8_t raw_res[260]; uint16_t registers[125]; + mbsn_callbacks callbacks_empty = {0}; - start_client_and_server(transport, (mbsn_callbacks){}); + start_client_and_server(transport, &callbacks_empty); should("return MBSN_EXCEPTION_ILLEGAL_FUNCTION when callback is not registered server-side"); assert(mbsn_write_multiple_registers(&CLIENT, 0, 1, registers) == MBSN_EXCEPTION_ILLEGAL_FUNCTION); stop_client_and_server(); - start_client_and_server(transport, (mbsn_callbacks){.write_multiple_registers = write_registers}); + start_client_and_server(transport, &(mbsn_callbacks){.write_multiple_registers = write_registers}); should("immediately return MBSN_ERROR_INVALID_ARGUMENT when calling with quantity 0"); assert(mbsn_write_multiple_registers(&CLIENT, 1, 0, registers) == MBSN_ERROR_INVALID_ARGUMENT); @@ -801,7 +825,7 @@ mbsn_transport transports[2] = {MBSN_TRANSPORT_RTU, MBSN_TRANSPORT_TCP}; const char* transports_str[2] = {"RTU", "TCP"}; void for_transports(void (*test_fn)(mbsn_transport), const char* should_str) { - for (int t = 0; t < sizeof(transports) / sizeof(mbsn_transport); t++) { + for (unsigned long t = 0; t < sizeof(transports) / sizeof(mbsn_transport); t++) { printf("Should %s on %s:\n", should_str, transports_str[t]); test(test_fn(transports[t])); } diff --git a/tests/modbusino_tests.h b/tests/modbusino_tests.h index d6812e1..376bfc8 100644 --- a/tests/modbusino_tests.h +++ b/tests/modbusino_tests.h @@ -16,10 +16,8 @@ #define test(f) (nesting++, (f), nesting--) -/* -#define htons(w) (((((uint16_t) (w) &0xFF)) << 8) | (((uint16_t) (w) &0xFF00) >> 8)) -#define ntohs(w) (((((uint16_t) (w) &0xFF)) << 8) | (((uint16_t) (w) &0xFF00) >> 8)) -*/ +#define UNUSED_PARAM(x) ((x) = (x)) + const uint8_t TEST_SERVER_ADDR = 1; @@ -35,7 +33,7 @@ mbsn_t CLIENT, SERVER; #define should(s) \ - for (int i = 0; i < nesting; i++) { \ + for (unsigned int i = 0; i < nesting; i++) { \ printf("\t"); \ } \ printf("Should %s\n", (s)) @@ -48,7 +46,8 @@ uint64_t now_ms() { } -void platform_sleep(uint32_t milliseconds) { +void platform_sleep(uint32_t milliseconds, void* arg) { + UNUSED_PARAM(arg); usleep(milliseconds * 1000); } @@ -124,22 +123,26 @@ int write_byte_fd(int fd, uint8_t b, int32_t timeout_ms) { } -int read_byte_socket_server(uint8_t* b, int32_t timeout_ms) { +int read_byte_socket_server(uint8_t* b, int32_t timeout_ms, void* arg) { + UNUSED_PARAM(arg); return read_byte_fd(sockets[0], b, timeout_ms); } -int write_byte_socket_server(uint8_t b, int32_t timeout_ms) { +int write_byte_socket_server(uint8_t b, int32_t timeout_ms, void* arg) { + UNUSED_PARAM(arg); return write_byte_fd(sockets[0], b, timeout_ms); } -int read_byte_socket_client(uint8_t* b, int32_t timeout_ms) { +int read_byte_socket_client(uint8_t* b, int32_t timeout_ms, void* arg) { + UNUSED_PARAM(arg); return read_byte_fd(sockets[1], b, timeout_ms); } -int write_byte_socket_client(uint8_t b, int32_t timeout_ms) { +int write_byte_socket_client(uint8_t b, int32_t timeout_ms, void* arg) { + UNUSED_PARAM(arg); return write_byte_fd(sockets[1], b, timeout_ms); } @@ -178,7 +181,7 @@ void* server_listen_thread() { if (is_server_listen_thread_stopped()) break; - check(mbsn_server_receive(&SERVER)); + check(mbsn_server_poll(&SERVER)); } return NULL; @@ -195,7 +198,7 @@ void stop_client_and_server() { } -void start_client_and_server(mbsn_transport transport, mbsn_callbacks server_callbacks) { +void start_client_and_server(mbsn_transport transport, const mbsn_callbacks* server_callbacks) { assert(pthread_mutex_destroy(&server_stopped_m) == 0); assert(pthread_mutex_init(&server_stopped_m, NULL) == 0);