From e5b51cc4e73a973ef9fe63c7a453fdbb4226b474 Mon Sep 17 00:00:00 2001 From: Vyacheslav Verkhovin Date: Fri, 30 Jun 2023 10:11:32 +0700 Subject: [PATCH] Add File Records functions Add both File Records functions (0x14 Read and 0x15 Write) for Client and Server roles. Also, add a little tests and example. Resolves: [#1] and [#24] --- README.md | 4 + examples/linux/client-tcp.c | 25 ++- examples/linux/server-tcp.c | 53 ++++- nanomodbus.c | 400 ++++++++++++++++++++++++++++++++++++ nanomodbus.h | 34 +++ tests/multi_server_rtu.c | 3 +- tests/nanomodbus_tests.c | 146 +++++++++++++ 7 files changed, 659 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e5bba97..bfe4465 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ Its main features are: - 06 (0x06) Write Single Register - 15 (0x0F) Write Multiple Coils - 16 (0x10) Write Multiple registers + - 20 (0x14) Read File Record + - 21 (0x15) Write File Record - Platform-agnostic - Requires only C99 and its standard library - Data transport read/write function are implemented by the user @@ -151,5 +153,7 @@ Please refer to `examples/arduino/README.md` for more info about building and ru - `NMBS_SERVER_WRITE_SINGLE_REGISTER_DISABLED` - `NMBS_SERVER_WRITE_MULTIPLE_COILS_DISABLED` - `NMBS_SERVER_WRITE_MULTIPLE_REGISTERS_DISABLED` + - `NMBS_SERVER_READ_FILE_RECORD_DISABLED` + - `NMBS_SERVER_WRITE_FILE_RECORD_DISABLED` - `NMBS_STRERROR_DISABLED` to disable the code that converts `nmbs_error`s to strings - Debug prints about received and sent messages can be enabled by defining `NMBS_DEBUG` diff --git a/examples/linux/client-tcp.c b/examples/linux/client-tcp.c index 8b3072e..fc85a17 100644 --- a/examples/linux/client-tcp.c +++ b/examples/linux/client-tcp.c @@ -12,7 +12,6 @@ #include "nanomodbus.h" #include "platform.h" - int main(int argc, char* argv[]) { if (argc < 3) { fprintf(stderr, "Usage: client-tcp [address] [port]\n"); @@ -91,6 +90,30 @@ int main(int argc, char* argv[]) { printf("Register at address 27: %d\n", r_regs[1]); } + // Write file + uint16_t file[4] = {0x0000, 0x00AA, 0x5500, 0xFFFF}; + err = nmbs_write_file_record(&nmbs, 1, 0, file, 4); + if (err != NMBS_ERROR_NONE) { + fprintf(stderr, "Error writing file - %s", nmbs_strerror(err)); + if (!nmbs_error_is_exception(err)) + return 1; + } + else { + printf("Write file registers: 0x%04X 0x%04X 0x%04X 0x%04X\n", file[0], file[1], file[2], file[3]); + } + + // Read file + memset(file, 0, sizeof(file)); + err = nmbs_read_file_record(&nmbs, 1, 0, file, 4); + if (err != NMBS_ERROR_NONE) { + fprintf(stderr, "Error writing file - %s", nmbs_strerror(err)); + if (!nmbs_error_is_exception(err)) + return 1; + } + else { + printf("Read file registers: 0x%04X 0x%04X 0x%04X 0x%04X\n", file[0], file[1], file[2], file[3]); + } + // Close the TCP connection disconnect(conn); diff --git a/examples/linux/server-tcp.c b/examples/linux/server-tcp.c index c26f2aa..c42b2b5 100644 --- a/examples/linux/server-tcp.c +++ b/examples/linux/server-tcp.c @@ -22,13 +22,16 @@ // The data model of this sever will support coils addresses 0 to 100 and registers addresses from 0 to 32 #define COILS_ADDR_MAX 100 #define REGS_ADDR_MAX 32 +#define FILE_SIZE_MAX 32 // A single nmbs_bitfield variable can keep 2000 coils nmbs_bitfield server_coils = {0}; uint16_t server_registers[REGS_ADDR_MAX] = {0}; +uint16_t server_file[FILE_SIZE_MAX]; -nmbs_error handle_read_coils(uint16_t address, uint16_t quantity, nmbs_bitfield coils_out, void* arg) { +nmbs_error handle_read_coils(uint16_t address, uint16_t quantity, nmbs_bitfield coils_out, uint8_t unit_id, void* arg) { UNUSED_PARAM(arg); + UNUSED_PARAM(unit_id); if (address + quantity > COILS_ADDR_MAX + 1) return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; @@ -43,8 +46,10 @@ nmbs_error handle_read_coils(uint16_t address, uint16_t quantity, nmbs_bitfield } -nmbs_error handle_write_multiple_coils(uint16_t address, uint16_t quantity, const nmbs_bitfield coils, void* arg) { +nmbs_error handle_write_multiple_coils(uint16_t address, uint16_t quantity, const nmbs_bitfield coils, uint8_t unit_id, + void* arg) { UNUSED_PARAM(arg); + UNUSED_PARAM(unit_id); if (address + quantity > COILS_ADDR_MAX + 1) return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; @@ -58,8 +63,10 @@ nmbs_error handle_write_multiple_coils(uint16_t address, uint16_t quantity, cons } -nmbs_error handler_read_holding_registers(uint16_t address, uint16_t quantity, uint16_t* registers_out, void* arg) { +nmbs_error handler_read_holding_registers(uint16_t address, uint16_t quantity, uint16_t* registers_out, uint8_t unit_id, + void* arg) { UNUSED_PARAM(arg); + UNUSED_PARAM(unit_id); if (address + quantity > REGS_ADDR_MAX + 1) return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; @@ -72,8 +79,10 @@ nmbs_error handler_read_holding_registers(uint16_t address, uint16_t quantity, u } -nmbs_error handle_write_multiple_registers(uint16_t address, uint16_t quantity, const uint16_t* registers, void* arg) { +nmbs_error handle_write_multiple_registers(uint16_t address, uint16_t quantity, const uint16_t* registers, + uint8_t unit_id, void* arg) { UNUSED_PARAM(arg); + UNUSED_PARAM(unit_id); if (address + quantity > REGS_ADDR_MAX + 1) return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; @@ -86,6 +95,40 @@ nmbs_error handle_write_multiple_registers(uint16_t address, uint16_t quantity, } +nmbs_error handle_read_file_record(uint16_t file_number, uint16_t record_number, uint16_t* registers, uint16_t count, + uint8_t unit_id, void* arg) { + UNUSED_PARAM(arg); + UNUSED_PARAM(unit_id); + + if (file_number != 1) + return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; + + if ((record_number + count) > FILE_SIZE_MAX) + return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; + + memcpy(registers, server_file + record_number, count * sizeof(uint16_t)); + + return NMBS_ERROR_NONE; +} + + +nmbs_error handle_write_file_record(uint16_t file_number, uint16_t record_number, const uint16_t* registers, + uint16_t count, uint8_t unit_id, void* arg) { + UNUSED_PARAM(arg); + UNUSED_PARAM(unit_id); + + if (file_number != 1) + return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; + + if ((record_number + count) > FILE_SIZE_MAX) + return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; + + memcpy(server_file + record_number, registers, count * sizeof(uint16_t)); + + return NMBS_ERROR_NONE; +} + + int main(int argc, char* argv[]) { if (argc < 3) { fprintf(stderr, "Usage: server-tcp [address] [port]\n"); @@ -111,6 +154,8 @@ int main(int argc, char* argv[]) { callbacks.write_multiple_coils = handle_write_multiple_coils; callbacks.read_holding_registers = handler_read_holding_registers; callbacks.write_multiple_registers = handle_write_multiple_registers; + callbacks.read_file_record = handle_read_file_record; + callbacks.write_file_record = handle_write_file_record; // Create the modbus server. It's ok to set address_rtu to 0 since we are on TCP nmbs_t nmbs; diff --git a/nanomodbus.c b/nanomodbus.c index b51905b..24e5cfd 100644 --- a/nanomodbus.c +++ b/nanomodbus.c @@ -55,6 +55,11 @@ static void discard_1(nmbs_t* nmbs) { } +static void discard_n(nmbs_t* nmbs, uint16_t n) { + nmbs->msg.buf_idx += n; +} + + static uint16_t get_2(nmbs_t* nmbs) { uint16_t result = ((uint16_t) nmbs->msg.buf[nmbs->msg.buf_idx]) << 8 | (uint16_t) nmbs->msg.buf[nmbs->msg.buf_idx + 1]; @@ -70,6 +75,39 @@ static void put_2(nmbs_t* nmbs, uint16_t data) { } +static uint8_t* get_n(nmbs_t* nmbs, uint16_t n) { + uint8_t* msg_buf_ptr = nmbs->msg.buf + nmbs->msg.buf_idx; + nmbs->msg.buf_idx += n; + return msg_buf_ptr; +} + + +static uint16_t* get_regs(nmbs_t* nmbs, uint16_t n) { + uint16_t* msg_buf_ptr = (uint16_t*) (nmbs->msg.buf + nmbs->msg.buf_idx); + nmbs->msg.buf_idx += n * 2; + while (n--) { + msg_buf_ptr[n] = (msg_buf_ptr[n] << 8) | ((msg_buf_ptr[n] >> 8) & 0xFF); + } + return msg_buf_ptr; +} + + +static void put_regs(nmbs_t* nmbs, const uint16_t* data, uint16_t n) { + uint16_t* msg_buf_ptr = (uint16_t*) (nmbs->msg.buf + nmbs->msg.buf_idx); + nmbs->msg.buf_idx += n * 2; + while (n--) { + msg_buf_ptr[n] = (data[n] << 8) | ((data[n] >> 8) & 0xFF); + } +} + + +static void swap_regs(uint16_t* data, uint16_t n) { + while (n--) { + data[n] = (data[n] << 8) | ((data[n] >> 8) & 0xFF); + } +} + + static void msg_buf_reset(nmbs_t* nmbs) { nmbs->msg.buf_idx = 0; } @@ -593,6 +631,95 @@ nmbs_error recv_write_multiple_registers_res(nmbs_t* nmbs, uint16_t address, uin } +nmbs_error recv_read_file_record_res(nmbs_t* nmbs, uint16_t* registers, uint16_t count) { + nmbs_error err = recv_res_header(nmbs); + if (err != NMBS_ERROR_NONE) + return err; + + err = recv(nmbs, 1); + if (err != NMBS_ERROR_NONE) + return err; + + uint8_t respone_size = get_1(nmbs); + + err = recv(nmbs, respone_size); + if (err != NMBS_ERROR_NONE) + return err; + + uint8_t subreq_data_size = get_1(nmbs) - 1; + uint8_t subreq_reference_type = get_1(nmbs); + uint16_t* subreq_record_data = (uint16_t*) get_n(nmbs, subreq_data_size); + + err = recv_msg_footer(nmbs); + if (err != NMBS_ERROR_NONE) + return err; + + if (registers) { + if (subreq_reference_type != 6) + return NMBS_ERROR_INVALID_RESPONSE; + + if (count != (subreq_data_size / 2)) + return NMBS_ERROR_INVALID_RESPONSE; + + swap_regs(subreq_record_data, subreq_data_size / 2); + memcpy(registers, subreq_record_data, subreq_data_size); + } + + return NMBS_ERROR_NONE; +} + + +nmbs_error recv_write_file_record_res(nmbs_t* nmbs, uint16_t file_number, uint16_t record_number, + const uint16_t* registers, uint16_t count) { + nmbs_error err = recv_res_header(nmbs); + if (err != NMBS_ERROR_NONE) + return err; + + err = recv(nmbs, 1); + if (err != NMBS_ERROR_NONE) + return err; + + uint8_t respone_size = get_1(nmbs); + + err = recv(nmbs, respone_size); + if (err != NMBS_ERROR_NONE) + return err; + + uint8_t subreq_reference_type = get_1(nmbs); + uint16_t subreq_file_number = get_2(nmbs); + uint16_t subreq_record_number = get_2(nmbs); + uint16_t subreq_record_lenght = get_2(nmbs); + NMBS_DEBUG_PRINT("a %d\tr %d\tl %d\t fwrite ", subreq_file_number, subreq_record_number, subreq_record_lenght); + + uint16_t subreq_data_size = subreq_record_lenght * 2; + uint16_t* subreq_record_data = (uint16_t*) get_n(nmbs, subreq_data_size); + + err = recv_msg_footer(nmbs); + if (err != NMBS_ERROR_NONE) + return err; + + if (registers) { + if (subreq_reference_type != 6) + return NMBS_ERROR_INVALID_RESPONSE; + + if (subreq_file_number != file_number) + return NMBS_ERROR_INVALID_RESPONSE; + + if (subreq_record_number != record_number) + return NMBS_ERROR_INVALID_RESPONSE; + + if (subreq_record_lenght != count) + return NMBS_ERROR_INVALID_RESPONSE; + + swap_regs(subreq_record_data, subreq_record_lenght); + if (memcmp(registers, subreq_record_data, subreq_data_size)) + return NMBS_ERROR_INVALID_RESPONSE; + } + + return NMBS_ERROR_NONE; +} + + #ifndef NMBS_SERVER_DISABLED #if !defined(NMBS_SERVER_READ_COILS_DISABLED) || !defined(NMBS_SERVER_READ_DISCRETE_INPUTS_DISABLED) static nmbs_error handle_read_discrete(nmbs_t* nmbs, @@ -1002,6 +1129,206 @@ static nmbs_error handle_write_multiple_registers(nmbs_t* nmbs) { } #endif +#ifndef NMBS_SERVER_READ_FILE_RECORD_DISABLED +static nmbs_error handle_read_file_record(nmbs_t* nmbs) { + nmbs_error err = recv(nmbs, 1); + if (err != NMBS_ERROR_NONE) + return err; + + uint8_t request_size = get_1(nmbs); + + err = recv(nmbs, request_size); + if (err != NMBS_ERROR_NONE) + return err; + + const uint8_t subreq_header_size = 7; + uint8_t subreq_count = request_size / subreq_header_size; + + struct { + uint8_t reference_type; + uint16_t file_number; + uint16_t record_number; + uint16_t record_lenght; + } subreq[subreq_count]; + + uint8_t respone_data_size = 0; + + for (uint8_t i = 0; i < subreq_count; i++) { + subreq[i].reference_type = get_1(nmbs); + subreq[i].file_number = get_2(nmbs); + subreq[i].record_number = get_2(nmbs); + subreq[i].record_lenght = get_2(nmbs); + + respone_data_size += 2 + subreq[i].record_lenght * 2; + } + + discard_n(nmbs, request_size % subreq_header_size); + + err = recv_msg_footer(nmbs); + if (err != NMBS_ERROR_NONE) + return err; + + if (!nmbs->msg.ignored) { + if (request_size % subreq_header_size) + return send_exception_msg(nmbs, NMBS_EXCEPTION_ILLEGAL_DATA_VALUE); + + if (request_size < 0x07 || request_size > 0xF5) + return send_exception_msg(nmbs, NMBS_EXCEPTION_ILLEGAL_DATA_VALUE); + + put_res_header(nmbs, respone_data_size); + put_1(nmbs, respone_data_size); + + for (uint8_t i = 0; i < subreq_count; i++) { + if (subreq[i].reference_type != 0x06) + return send_exception_msg(nmbs, NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS); + + if (subreq[i].file_number == 0x0000) + return send_exception_msg(nmbs, NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS); + + if (subreq[i].record_number > 0x270F) + return send_exception_msg(nmbs, NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS); + + NMBS_DEBUG_PRINT("a %d\tr %d\tl %d\t fread ", subreq[i].file_number, subreq[i].record_number, + subreq[i].record_lenght); + + uint16_t subreq_data_size = subreq[i].record_lenght * 2; + put_1(nmbs, subreq_data_size + 1); + put_1(nmbs, 0x06); // add Reference Type const + uint16_t* subreq_data = (uint16_t*) get_n(nmbs, subreq[i].record_lenght * 2); + + if (nmbs->callbacks.read_file_record) { + err = nmbs->callbacks.read_file_record(subreq[i].file_number, subreq[i].record_number, subreq_data, + subreq[i].record_lenght, nmbs->msg.unit_id, nmbs->platform.arg); + if (err != NMBS_ERROR_NONE) { + if (nmbs_error_is_exception(err)) + return send_exception_msg(nmbs, err); + + return send_exception_msg(nmbs, NMBS_EXCEPTION_SERVER_DEVICE_FAILURE); + } + + swap_regs(subreq_data, subreq[i].record_lenght); + //get_regs(nmbs, subreq[i].record_lenght); + } + else { + return send_exception_msg(nmbs, NMBS_EXCEPTION_ILLEGAL_FUNCTION); + } + } + + if (!nmbs->msg.broadcast) { + err = send_msg(nmbs); + if (err != NMBS_ERROR_NONE) + return err; + } + } + else { + return recv_read_file_record_res(nmbs, NULL, 0); + } + + return NMBS_ERROR_NONE; +} +#endif + +#ifndef NMBS_SERVER_WRITE_FILE_RECORD_DISABLED +static nmbs_error handle_write_file_record(nmbs_t* nmbs) { + nmbs_error err = recv(nmbs, 1); + if (err != NMBS_ERROR_NONE) + return err; + + uint8_t request_size = get_1(nmbs); + + err = recv(nmbs, request_size); + if (err != NMBS_ERROR_NONE) + return err; + + // We can save msg.buf index and use it later for context recovery. + uint16_t msg_buf_idx = nmbs->msg.buf_idx; + discard_n(nmbs, request_size); + + err = recv_msg_footer(nmbs); + if (err != NMBS_ERROR_NONE) + return err; + + if (!nmbs->msg.ignored) { + const uint8_t subreq_header_size = 7; + uint16_t size = request_size; + nmbs->msg.buf_idx = msg_buf_idx; // restore context + + if (request_size < 0x07 || request_size > 0xF5) + return send_exception_msg(nmbs, NMBS_EXCEPTION_ILLEGAL_DATA_VALUE); + + do { + uint8_t subreq_reference_type = get_1(nmbs); + uint16_t subreq_file_number = get_2(nmbs); + uint16_t subreq_record_number = get_2(nmbs); + uint16_t subreq_record_lenght = get_2(nmbs); + discard_n(nmbs, subreq_record_lenght * 2); + + if (subreq_reference_type != 0x06) + return send_exception_msg(nmbs, NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS); + + if (subreq_file_number == 0x0000) + return send_exception_msg(nmbs, NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS); + + if (subreq_record_number > 0x270F) + return send_exception_msg(nmbs, NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS); + + NMBS_DEBUG_PRINT("a %d\tr %d\tl %d\t fwrite ", subreq_file_number, subreq_record_number, + subreq_record_lenght); + size -= (subreq_header_size + subreq_record_lenght * 2); + } while (size >= subreq_header_size); + + if (size) + return send_exception_msg(nmbs, NMBS_EXCEPTION_ILLEGAL_DATA_VALUE); + + // checks completed + + size = request_size; + nmbs->msg.buf_idx = msg_buf_idx; // restore context + + do { + discard_1(nmbs); + uint16_t subreq_file_number = get_2(nmbs); + uint16_t subreq_record_number = get_2(nmbs); + uint16_t subreq_record_lenght = get_2(nmbs); + uint16_t* subreq_data = get_regs(nmbs, subreq_record_lenght); + + if (nmbs->callbacks.write_file_record) { + err = nmbs->callbacks.write_file_record(subreq_file_number, subreq_record_number, subreq_data, + subreq_record_lenght, nmbs->msg.unit_id, nmbs->platform.arg); + if (err != NMBS_ERROR_NONE) { + if (nmbs_error_is_exception(err)) + return send_exception_msg(nmbs, err); + + return send_exception_msg(nmbs, NMBS_EXCEPTION_SERVER_DEVICE_FAILURE); + } + + swap_regs(subreq_data, subreq_record_lenght); // restore swapping + } + else { + return send_exception_msg(nmbs, NMBS_EXCEPTION_ILLEGAL_FUNCTION); + } + + size -= (subreq_header_size + subreq_record_lenght * 2); + } while (size >= subreq_header_size); + + if (!nmbs->msg.broadcast) { + // The normal response to 'Write File' is an echo of the request. + // We can restore buffer index and respone msg. + nmbs->msg.buf_idx = msg_buf_idx; + discard_n(nmbs, request_size); + + err = send_msg(nmbs); + if (err != NMBS_ERROR_NONE) + return err; + } + } + else { + return recv_write_file_record_res(nmbs, 0, 0, NULL, 0); + } + + return NMBS_ERROR_NONE; +} +#endif static nmbs_error handle_req_fc(nmbs_t* nmbs) { NMBS_DEBUG_PRINT("fc %d\t", nmbs->msg.fc); @@ -1056,6 +1383,18 @@ static nmbs_error handle_req_fc(nmbs_t* nmbs) { break; #endif +#ifndef NMBS_SERVER_READ_FILE_RECORD_DISABLED + case 20: + err = handle_read_file_record(nmbs); + break; +#endif + +#ifndef NMBS_SERVER_WRITE_FILE_RECORD_DISABLED + case 21: + err = handle_write_file_record(nmbs); + break; +#endif + default: err = NMBS_EXCEPTION_ILLEGAL_FUNCTION; } @@ -1299,6 +1638,67 @@ nmbs_error nmbs_write_multiple_registers(nmbs_t* nmbs, uint16_t address, uint16_ } +nmbs_error nmbs_read_file_record(nmbs_t* nmbs, uint16_t file_number, uint16_t record_number, uint16_t* registers, + uint16_t count) { + if (file_number == 0x0000) + return NMBS_ERROR_INVALID_ARGUMENT; + + if (record_number > 0x270F) + return NMBS_ERROR_INVALID_ARGUMENT; + + msg_state_req(nmbs, 20); + put_req_header(nmbs, 8); + + put_1(nmbs, 7); // add Byte Count + put_1(nmbs, 6); // add Reference Type const + put_2(nmbs, file_number); + put_2(nmbs, record_number); + put_2(nmbs, count); + NMBS_DEBUG_PRINT("a %d\tr %d\tl %d\t fread ", file_number, record_number, count); + + nmbs_error err = send_msg(nmbs); + if (err != NMBS_ERROR_NONE) + return err; + + if (!nmbs->msg.broadcast) + return recv_read_file_record_res(nmbs, registers, count); + + return NMBS_ERROR_NONE; +} + + +nmbs_error nmbs_write_file_record(nmbs_t* nmbs, uint16_t file_number, uint16_t record_number, const uint16_t* registers, + uint16_t count) { + if (file_number == 0x0000) + return NMBS_ERROR_INVALID_ARGUMENT; + + if (record_number > 0x270F) + return NMBS_ERROR_INVALID_ARGUMENT; + + uint16_t data_size = count * 2; + + msg_state_req(nmbs, 21); + put_req_header(nmbs, 8 + data_size); + + put_1(nmbs, 7 + data_size); // add Byte Count + put_1(nmbs, 6); // add Reference Type const + put_2(nmbs, file_number); + put_2(nmbs, record_number); + put_2(nmbs, count); + put_regs(nmbs, registers, count); + NMBS_DEBUG_PRINT("a %d\tr %d\tl %d\t fwrite ", file_number, record_number, count); + + nmbs_error err = send_msg(nmbs); + if (err != NMBS_ERROR_NONE) + return err; + + if (!nmbs->msg.broadcast) + return recv_write_file_record_res(nmbs, file_number, record_number, registers, count); + + return NMBS_ERROR_NONE; +} + + nmbs_error nmbs_send_raw_pdu(nmbs_t* nmbs, uint8_t fc, const uint8_t* data, uint16_t data_len) { msg_state_req(nmbs, fc); put_msg_header(nmbs, data_len); diff --git a/nanomodbus.h b/nanomodbus.h index 1b6a817..de0de2c 100644 --- a/nanomodbus.h +++ b/nanomodbus.h @@ -183,6 +183,16 @@ typedef struct nmbs_callbacks { nmbs_error (*write_multiple_registers)(uint16_t address, uint16_t quantity, const uint16_t* registers, uint8_t unit_id, void* arg); #endif + +#ifndef NMBS_SERVER_READ_FILE_RECORD_DISABLED + nmbs_error (*read_file_record)(uint16_t file_number, uint16_t record_number, uint16_t* registers, uint16_t count, + uint8_t unit_id, void* arg); +#endif + +#ifndef NMBS_SERVER_WRITE_FILE_RECORD_DISABLED + nmbs_error (*write_file_record)(uint16_t file_number, uint16_t record_number, const uint16_t* registers, + uint16_t count, uint8_t unit_id, void* arg); +#endif #endif char _nonempty; // Struct may become empty, which is undefined behavior @@ -357,6 +367,30 @@ nmbs_error nmbs_write_multiple_coils(nmbs_t* nmbs, uint16_t address, uint16_t qu */ nmbs_error nmbs_write_multiple_registers(nmbs_t* nmbs, uint16_t address, uint16_t quantity, const uint16_t* registers); +/** Send a FC 20 (0x14) Read File Record + * @param nmbs pointer to the nmbs_t instance + * @param file_number file number (1 to 65535) + * @param record_number record number from file (0000 to 9999) + * @param registers array of registers to read + * @param count count of registers + * + * @return NMBS_ERROR_NONE if successful, other errors otherwise. + */ +nmbs_error nmbs_read_file_record(nmbs_t* nmbs, uint16_t file_number, uint16_t record_number, uint16_t* registers, + uint16_t count); + +/** Send a FC 21 (0x15) Write File Record + * @param nmbs pointer to the nmbs_t instance + * @param file_number file number (1 to 65535) + * @param record_number record number from file (0000 to 9999) + * @param registers array of registers to write + * @param count count of registers + * + * @return NMBS_ERROR_NONE if successful, other errors otherwise. + */ +nmbs_error nmbs_write_file_record(nmbs_t* nmbs, uint16_t file_number, uint16_t record_number, const uint16_t* registers, + uint16_t count); + /** Send a raw Modbus PDU. * CRC on RTU will be calculated and sent by this function. * @param nmbs pointer to the nmbs_t instance diff --git a/tests/multi_server_rtu.c b/tests/multi_server_rtu.c index 0687bfd..843f59d 100644 --- a/tests/multi_server_rtu.c +++ b/tests/multi_server_rtu.c @@ -68,8 +68,9 @@ int32_t write_wire(const uint8_t* buf, uint16_t count, int32_t timeout_ms, void* return (int32_t) written; } -nmbs_error read_coils(uint16_t address, uint16_t quantity, nmbs_bitfield coils_out, void* arg) { +nmbs_error read_coils(uint16_t address, uint16_t quantity, nmbs_bitfield coils_out, uint8_t unit_id, void* arg) { UNUSED_PARAM(arg); + UNUSED_PARAM(unit_id); for (int i = 0; i < quantity; i++) nmbs_bitfield_write(coils_out, address + i, 1); diff --git a/tests/nanomodbus_tests.c b/tests/nanomodbus_tests.c index 989d375..0ca351a 100644 --- a/tests/nanomodbus_tests.c +++ b/tests/nanomodbus_tests.c @@ -821,6 +821,148 @@ void test_fc16(nmbs_transport transport) { } +nmbs_error read_file(uint16_t file_number, uint16_t record_number, uint16_t* registers, uint16_t count, uint8_t unit_id, + void* arg) { + UNUSED_PARAM(arg); + UNUSED_PARAM(unit_id); + + if (file_number == 1) + return -1; + + if (file_number == 2) + return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; + + if (file_number == 3) + return NMBS_EXCEPTION_ILLEGAL_DATA_VALUE; + + if (file_number == 4 && record_number == 4 && count == 4) { + registers[0] = 0; + registers[1] = 0xFF; + registers[2] = 0xAA55; + registers[3] = 0xFFFF; + } + + if (file_number == 255 && record_number == 9999 && count == 119) + registers[118] = 42; + + return NMBS_ERROR_NONE; +} + + +void test_fc20(nmbs_transport transport) { + uint16_t registers[128]; + nmbs_callbacks callbacks_empty = {0}; + + start_client_and_server(transport, &callbacks_empty); + + should("return NMBS_EXCEPTION_ILLEGAL_FUNCTION when callback is not registered server-side"); + expect(nmbs_read_file_record(&CLIENT, 1, 0, NULL, 0) == NMBS_EXCEPTION_ILLEGAL_FUNCTION); + + stop_client_and_server(); + + start_client_and_server(transport, &(nmbs_callbacks){.read_file_record = read_file}); + + should("immediately return NMBS_ERROR_INVALID_ARGUMENT when calling with file_number 0"); + expect(nmbs_read_file_record(&CLIENT, 0, 0, registers, 1) == NMBS_ERROR_INVALID_ARGUMENT); + + should("immediately return NMBS_ERROR_INVALID_ARGUMENT when calling with record_number > 9999"); + expect(nmbs_read_file_record(&CLIENT, 1, 10000, registers, 1) == NMBS_ERROR_INVALID_ARGUMENT); + + should("return NMBS_EXCEPTION_SERVER_DEVICE_FAILURE when server handler returns any non-exception error"); + expect(nmbs_read_file_record(&CLIENT, 1, 1, registers, 3) == NMBS_EXCEPTION_SERVER_DEVICE_FAILURE); + + should("return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS if returned by server handler"); + expect(nmbs_read_file_record(&CLIENT, 2, 1, registers, 3) == NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS); + + should("return NMBS_EXCEPTION_ILLEGAL_DATA_VALUE if returned by server handler"); + expect(nmbs_read_file_record(&CLIENT, 3, 1, registers, 3) == NMBS_EXCEPTION_ILLEGAL_DATA_VALUE); + + should("read with no error"); + check(nmbs_read_file_record(&CLIENT, 4, 4, registers, 4)); + expect(registers[0] == 0); + expect(registers[1] == 0xFF); + expect(registers[2] == 0xAA55); + expect(registers[3] == 0xFFFF); + + check(nmbs_read_file_record(&CLIENT, 255, 9999, registers, 119)); + expect(registers[118] == 42); + + stop_client_and_server(); +} + + +nmbs_error write_file(uint16_t file_number, uint16_t record_number, const uint16_t* registers, uint16_t count, + uint8_t unit_id, void* arg) { + UNUSED_PARAM(arg); + UNUSED_PARAM(unit_id); + + if (file_number == 1) + return -1; + + if (file_number == 2) + return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; + + if (file_number == 3) + return NMBS_EXCEPTION_ILLEGAL_DATA_VALUE; + + if (file_number == 4 && record_number == 4 && count == 4) { + expect(registers[0] == 0); + expect(registers[1] == 0xFF); + expect(registers[2] == 0xAA55); + expect(registers[3] == 0xFFFF); + + return NMBS_ERROR_NONE; + } + + if (file_number == 255 && record_number == 9999 && count == 119) + expect(registers[118] == 42); + + return NMBS_ERROR_NONE; +} + + +void test_fc21(nmbs_transport transport) { + uint16_t registers[128]; + nmbs_callbacks callbacks_empty = {0}; + + start_client_and_server(transport, &callbacks_empty); + + should("return NMBS_EXCEPTION_ILLEGAL_FUNCTION when callback is not registered server-side"); + expect(nmbs_write_file_record(&CLIENT, 1, 0, NULL, 0) == NMBS_EXCEPTION_ILLEGAL_FUNCTION); + + stop_client_and_server(); + + start_client_and_server(transport, &(nmbs_callbacks){.write_file_record = write_file}); + + should("immediately return NMBS_ERROR_INVALID_ARGUMENT when calling with file_number 0"); + expect(nmbs_write_file_record(&CLIENT, 0, 0, registers, 1) == NMBS_ERROR_INVALID_ARGUMENT); + + should("immediately return NMBS_ERROR_INVALID_ARGUMENT when calling with record_number > 0x007B"); + expect(nmbs_write_file_record(&CLIENT, 1, 10000, registers, 1) == NMBS_ERROR_INVALID_ARGUMENT); + + should("return NMBS_EXCEPTION_SERVER_DEVICE_FAILURE when server handler returns any non-exception error"); + expect(nmbs_write_file_record(&CLIENT, 1, 1, registers, 1) == NMBS_EXCEPTION_SERVER_DEVICE_FAILURE); + + should("return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS if returned by server handler"); + expect(nmbs_write_file_record(&CLIENT, 2, 2, registers, 2) == NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS); + + should("return NMBS_EXCEPTION_ILLEGAL_DATA_VALUE if returned by server handler"); + expect(nmbs_write_file_record(&CLIENT, 3, 3, registers, 3) == NMBS_EXCEPTION_ILLEGAL_DATA_VALUE); + + should("write with no error"); + registers[0] = 0; + registers[1] = 0xFF; + registers[2] = 0xAA55; + registers[3] = 0xFFFF; + check(nmbs_write_file_record(&CLIENT, 4, 4, registers, 4)); + + registers[118] = 42; + check(nmbs_write_file_record(&CLIENT, 255, 9999, registers, 119)); + + stop_client_and_server(); +} + + nmbs_transport transports[2] = {NMBS_TRANSPORT_RTU, NMBS_TRANSPORT_TCP}; const char* transports_str[2] = {"RTU", "TCP"}; @@ -855,5 +997,9 @@ int main(int argc, char* argv[]) { for_transports(test_fc16, "send and receive FC 16 (0x10) Write Multiple registers"); + for_transports(test_fc20, "send and receive FC 20 (0x14) Read File Record"); + + for_transports(test_fc21, "send and receive FC 21 (0x15) Write File Record"); + return 0; }