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]
This commit is contained in:
Vyacheslav Verkhovin 2023-06-30 10:11:32 +07:00
parent cbd4fa69fb
commit e5b51cc4e7
7 changed files with 659 additions and 6 deletions

View File

@ -24,6 +24,8 @@ Its main features are:
- 06 (0x06) Write Single Register - 06 (0x06) Write Single Register
- 15 (0x0F) Write Multiple Coils - 15 (0x0F) Write Multiple Coils
- 16 (0x10) Write Multiple registers - 16 (0x10) Write Multiple registers
- 20 (0x14) Read File Record
- 21 (0x15) Write File Record
- Platform-agnostic - Platform-agnostic
- Requires only C99 and its standard library - Requires only C99 and its standard library
- Data transport read/write function are implemented by the user - 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_SINGLE_REGISTER_DISABLED`
- `NMBS_SERVER_WRITE_MULTIPLE_COILS_DISABLED` - `NMBS_SERVER_WRITE_MULTIPLE_COILS_DISABLED`
- `NMBS_SERVER_WRITE_MULTIPLE_REGISTERS_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 - `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` - Debug prints about received and sent messages can be enabled by defining `NMBS_DEBUG`

View File

@ -12,7 +12,6 @@
#include "nanomodbus.h" #include "nanomodbus.h"
#include "platform.h" #include "platform.h"
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
if (argc < 3) { if (argc < 3) {
fprintf(stderr, "Usage: client-tcp [address] [port]\n"); 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]); 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 // Close the TCP connection
disconnect(conn); disconnect(conn);

View File

@ -22,13 +22,16 @@
// The data model of this sever will support coils addresses 0 to 100 and registers addresses from 0 to 32 // 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 COILS_ADDR_MAX 100
#define REGS_ADDR_MAX 32 #define REGS_ADDR_MAX 32
#define FILE_SIZE_MAX 32
// A single nmbs_bitfield variable can keep 2000 coils // A single nmbs_bitfield variable can keep 2000 coils
nmbs_bitfield server_coils = {0}; nmbs_bitfield server_coils = {0};
uint16_t server_registers[REGS_ADDR_MAX] = {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(arg);
UNUSED_PARAM(unit_id);
if (address + quantity > COILS_ADDR_MAX + 1) if (address + quantity > COILS_ADDR_MAX + 1)
return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; 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(arg);
UNUSED_PARAM(unit_id);
if (address + quantity > COILS_ADDR_MAX + 1) if (address + quantity > COILS_ADDR_MAX + 1)
return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; 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(arg);
UNUSED_PARAM(unit_id);
if (address + quantity > REGS_ADDR_MAX + 1) if (address + quantity > REGS_ADDR_MAX + 1)
return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; 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(arg);
UNUSED_PARAM(unit_id);
if (address + quantity > REGS_ADDR_MAX + 1) if (address + quantity > REGS_ADDR_MAX + 1)
return NMBS_EXCEPTION_ILLEGAL_DATA_ADDRESS; 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[]) { int main(int argc, char* argv[]) {
if (argc < 3) { if (argc < 3) {
fprintf(stderr, "Usage: server-tcp [address] [port]\n"); 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.write_multiple_coils = handle_write_multiple_coils;
callbacks.read_holding_registers = handler_read_holding_registers; callbacks.read_holding_registers = handler_read_holding_registers;
callbacks.write_multiple_registers = handle_write_multiple_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 // Create the modbus server. It's ok to set address_rtu to 0 since we are on TCP
nmbs_t nmbs; nmbs_t nmbs;

View File

@ -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) { static uint16_t get_2(nmbs_t* nmbs) {
uint16_t result = uint16_t result =
((uint16_t) nmbs->msg.buf[nmbs->msg.buf_idx]) << 8 | (uint16_t) nmbs->msg.buf[nmbs->msg.buf_idx + 1]; ((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) { static void msg_buf_reset(nmbs_t* nmbs) {
nmbs->msg.buf_idx = 0; 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 #ifndef NMBS_SERVER_DISABLED
#if !defined(NMBS_SERVER_READ_COILS_DISABLED) || !defined(NMBS_SERVER_READ_DISCRETE_INPUTS_DISABLED) #if !defined(NMBS_SERVER_READ_COILS_DISABLED) || !defined(NMBS_SERVER_READ_DISCRETE_INPUTS_DISABLED)
static nmbs_error handle_read_discrete(nmbs_t* nmbs, static nmbs_error handle_read_discrete(nmbs_t* nmbs,
@ -1002,6 +1129,206 @@ static nmbs_error handle_write_multiple_registers(nmbs_t* nmbs) {
} }
#endif #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) { static nmbs_error handle_req_fc(nmbs_t* nmbs) {
NMBS_DEBUG_PRINT("fc %d\t", nmbs->msg.fc); NMBS_DEBUG_PRINT("fc %d\t", nmbs->msg.fc);
@ -1056,6 +1383,18 @@ static nmbs_error handle_req_fc(nmbs_t* nmbs) {
break; break;
#endif #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: default:
err = NMBS_EXCEPTION_ILLEGAL_FUNCTION; 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) { 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); msg_state_req(nmbs, fc);
put_msg_header(nmbs, data_len); put_msg_header(nmbs, data_len);

View File

@ -183,6 +183,16 @@ typedef struct nmbs_callbacks {
nmbs_error (*write_multiple_registers)(uint16_t address, uint16_t quantity, const uint16_t* registers, nmbs_error (*write_multiple_registers)(uint16_t address, uint16_t quantity, const uint16_t* registers,
uint8_t unit_id, void* arg); uint8_t unit_id, void* arg);
#endif #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 #endif
char _nonempty; // Struct may become empty, which is undefined behavior 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); 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. /** Send a raw Modbus PDU.
* CRC on RTU will be calculated and sent by this function. * CRC on RTU will be calculated and sent by this function.
* @param nmbs pointer to the nmbs_t instance * @param nmbs pointer to the nmbs_t instance

View File

@ -68,8 +68,9 @@ int32_t write_wire(const uint8_t* buf, uint16_t count, int32_t timeout_ms, void*
return (int32_t) written; 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(arg);
UNUSED_PARAM(unit_id);
for (int i = 0; i < quantity; i++) for (int i = 0; i < quantity; i++)
nmbs_bitfield_write(coils_out, address + i, 1); nmbs_bitfield_write(coils_out, address + i, 1);

View File

@ -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}; nmbs_transport transports[2] = {NMBS_TRANSPORT_RTU, NMBS_TRANSPORT_TCP};
const char* transports_str[2] = {"RTU", "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_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; return 0;
} }