This is an automated email from the ASF dual-hosted git repository. acassis pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/nuttx-apps.git
commit ceb8c81d81d7fa364b2812ef35a135749d415685 Author: Daniel P. Carvalho <[email protected]> AuthorDate: Wed Oct 21 21:08:54 2026 -0300 industry/nxmodbus: Add FC20 and FC21 file record support. Add the Read File Record (FC20, 0x14) and Write File Record (FC21, 0x15) server functions of the Modbus Application Protocol Specification V1.1b3 (sections 6.14 and 6.15). They move whole files, such as a configuration or a firmware image, to a device that has only a Modbus link. Each function has its own option, NXMODBUS_FUNC_READ_FILE and NXMODBUS_FUNC_WRITE_FILE. Both are off by default so that the existing configurations do not change in size: on an Arm Cortex-M4 the handlers take 612 and 362 bytes of code. The data model stays with the application, as for the other function codes. A new file_cb callback in struct nxmb_callbacks_s gets one group of records (file 1 to 0xffff, record 0 to 9999, nregs 16-bit registers) and reads or writes the buffer in wire order. It returns -ENOENT for a file or record that does not exist, which becomes exception 2. The member sits before priv, so a structure initialized by position would break; nothing in the tree does that. The sub-requests are validated in full before the first callback: the reference type must be 6, the group must stay inside the 10000 records of the file, and the read response must fit in 0xf5 bytes and in the ADU. The response of a read overwrites the request, so its groups are parsed first, up to 35 (the most a request can carry), in an array of 6 bytes each on the stack, 210 bytes in all. A write is not atomic, as the specification does not make it so: a callback failure leaves the earlier groups written. Tested on the host, with the real handlers built against stub headers: the two examples of the specification byte for byte, the limits of the fields, the exceptions, 35 sub-requests in place and the largest group, also under AddressSanitizer and UBSan. Built for an Arm Cortex-M4. Assisted-by: Claude Code:claude-sonnet-5-5 Signed-off-by: Daniel P. Carvalho <[email protected]> --- include/nxmodbus/nxmodbus.h | 13 ++ industry/nxmodbus/CMakeLists.txt | 1 + industry/nxmodbus/Kconfig | 16 ++ industry/nxmodbus/Makefile | 1 + industry/nxmodbus/core/nxmb_func.c | 6 + industry/nxmodbus/core/nxmb_func_file.c | 309 ++++++++++++++++++++++++++++++++ industry/nxmodbus/nxmb_internal.h | 4 + 7 files changed, 350 insertions(+) diff --git a/include/nxmodbus/nxmodbus.h b/include/nxmodbus/nxmodbus.h index f5307989b..2eb1efd70 100644 --- a/include/nxmodbus/nxmodbus.h +++ b/include/nxmodbus/nxmodbus.h @@ -199,6 +199,19 @@ struct nxmb_callbacks_s FAR void *priv); CODE int (*holding_cb)(FAR uint8_t *buf, uint16_t addr, uint16_t nregs, enum nxmb_regmode_e mode, FAR void *priv); + + /* File record access (FC20 Read File Record, FC21 Write File Record). + * The callback gets one group of nregs 16-bit records of a file, starting + * at the given record: file is 1 to 0xffff and record is 0 to 9999, as on + * the wire. buf holds the registers as on the wire (big endian, 2 bytes + * each): the application fills it for NXMB_REG_READ and consumes it for + * NXMB_REG_WRITE. Return -ENOENT for a file or record that does not + * exist (Modbus exception 2); any other error is a device failure. + */ + + CODE int (*file_cb)(FAR uint8_t *buf, uint16_t file, uint16_t record, + uint16_t nregs, enum nxmb_regmode_e mode, + FAR void *priv); FAR void *priv; }; diff --git a/industry/nxmodbus/CMakeLists.txt b/industry/nxmodbus/CMakeLists.txt index 35be513e4..be195774a 100644 --- a/industry/nxmodbus/CMakeLists.txt +++ b/industry/nxmodbus/CMakeLists.txt @@ -33,6 +33,7 @@ if(CONFIG_INDUSTRY_NXMODBUS) list(APPEND CSRCS core/nxmb_func_coils.c core/nxmb_func_discrete.c) list(APPEND CSRCS core/nxmb_func_holding.c core/nxmb_func_input.c) list(APPEND CSRCS core/nxmb_func_diag.c core/nxmb_func_other.c) + list(APPEND CSRCS core/nxmb_func_file.c) endif() if(CONFIG_NXMODBUS_CLIENT) diff --git a/industry/nxmodbus/Kconfig b/industry/nxmodbus/Kconfig index 31f555fe7..c9a16ef5a 100644 --- a/industry/nxmodbus/Kconfig +++ b/industry/nxmodbus/Kconfig @@ -223,6 +223,22 @@ config NXMODBUS_FUNC_REPORT_SERVER_ID ---help--- Enable FC17 Report Server ID function code handler. +config NXMODBUS_FUNC_READ_FILE + bool "FC20 Read File Record" + default n + depends on NXMODBUS_SERVER + ---help--- + Enable FC20 Read File Record function code handler. The data + is provided by the file_cb callback of the application. + +config NXMODBUS_FUNC_WRITE_FILE + bool "FC21 Write File Record" + default n + depends on NXMODBUS_SERVER + ---help--- + Enable FC21 Write File Record function code handler. The data + is consumed by the file_cb callback of the application. + config NXMODBUS_FUNC_READWRITE_HOLDINGS bool "FC23 Read/Write Multiple Holding Registers" default y diff --git a/industry/nxmodbus/Makefile b/industry/nxmodbus/Makefile index 64caacdba..709647f20 100644 --- a/industry/nxmodbus/Makefile +++ b/industry/nxmodbus/Makefile @@ -39,6 +39,7 @@ CSRCS += core/nxmb_func_holding.c CSRCS += core/nxmb_func_input.c CSRCS += core/nxmb_func_diag.c CSRCS += core/nxmb_func_other.c +CSRCS += core/nxmb_func_file.c endif ifeq ($(CONFIG_NXMODBUS_CLIENT),y) diff --git a/industry/nxmodbus/core/nxmb_func.c b/industry/nxmodbus/core/nxmb_func.c index c41f37f03..fda060eac 100644 --- a/industry/nxmodbus/core/nxmb_func.c +++ b/industry/nxmodbus/core/nxmb_func.c @@ -98,6 +98,12 @@ static const struct nxmb_fc_entry_s g_nxmb_fc_table[] = #ifdef CONFIG_NXMODBUS_FUNC_REPORT_SERVER_ID { NXMB_FC_REPORT_SERVER_ID, nxmb_fc17_report_server_id }, #endif +#ifdef CONFIG_NXMODBUS_FUNC_READ_FILE + { NXMB_FC_READ_FILE, nxmb_fc20_read_file }, +#endif +#ifdef CONFIG_NXMODBUS_FUNC_WRITE_FILE + { NXMB_FC_WRITE_FILE, nxmb_fc21_write_file }, +#endif #ifdef CONFIG_NXMODBUS_FUNC_READWRITE_HOLDINGS { NXMB_FC_READWRITE_HOLDINGS, nxmb_fc23_readwrite_holding } #endif diff --git a/industry/nxmodbus/core/nxmb_func_file.c b/industry/nxmodbus/core/nxmb_func_file.c new file mode 100644 index 000000000..2b154fc94 --- /dev/null +++ b/industry/nxmodbus/core/nxmb_func_file.c @@ -0,0 +1,309 @@ +/**************************************************************************** + * apps/industry/nxmodbus/core/nxmb_func_file.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include <nuttx/config.h> + +#include <nuttx/compiler.h> + +#include <errno.h> +#include <stdint.h> + +#include <nxmodbus/nxmodbus.h> + +#include "nxmb_internal.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/* Reference type of every sub-request (Modbus Application Protocol + * Specification V1.1b3, sections 6.14 and 6.15). + */ + +#define NXMB_FILE_REF_TYPE 0x06 + +/* A sub-request has the reference type, the file number, the starting record + * and the record length: 1 + 2 + 2 + 2 bytes. + */ + +#define NXMB_FILE_SUBREQ_LEN 7 +#define NXMB_FILE_REF_OFF 0 +#define NXMB_FILE_FILE_OFF 1 +#define NXMB_FILE_RECORD_OFF 3 +#define NXMB_FILE_LENGTH_OFF 5 + +/* A file has 10000 records (0 to 9999, 0x270f), each one a 16-bit register */ + +#define NXMB_FILE_RECORD_MAX 0x270f +#define NXMB_FILE_RECORDS 10000 + +/* FC20: byte count 0x07 to 0xf5, a group of up to 35 sub-requests */ + +#define NXMB_FC20_BCNT_MIN 0x07 +#define NXMB_FC20_BCNT_MAX 0xf5 +#define NXMB_FC20_SUBREQ_MAX (NXMB_FC20_BCNT_MAX / NXMB_FILE_SUBREQ_LEN) + +/* FC21: request data length 0x09 to 0xfb */ + +#define NXMB_FC21_BCNT_MIN 0x09 +#define NXMB_FC21_BCNT_MAX 0xfb + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +/* One sub-request of a read, parsed before the response overwrites it */ + +struct nxmb_file_group_s +{ + uint16_t file; + uint16_t record; + uint16_t nregs; +}; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_file_check_group + * + * Description: + * Check the file, record and length fields of a sub-request. The + * reference type must be 6, the file number 1 to 0xffff, and the group + * must stay inside the 10000 records of the file. + * + * Input Parameters: + * sub - The sub-request: reference type, file, record and length + * group - Receives the parsed file, record and length on success + * + * Returned Value: + * NXMB_EX_NONE on success; NXMB_EX_ILLEGAL_DATA_ADDRESS otherwise. + * + ****************************************************************************/ + +static enum nxmb_exception_e +nxmb_file_check_group(FAR const uint8_t *sub, + FAR struct nxmb_file_group_s *group) +{ + group->file = nxmb_util_get_u16_be(&sub[NXMB_FILE_FILE_OFF]); + group->record = nxmb_util_get_u16_be(&sub[NXMB_FILE_RECORD_OFF]); + group->nregs = nxmb_util_get_u16_be(&sub[NXMB_FILE_LENGTH_OFF]); + + if (sub[NXMB_FILE_REF_OFF] != NXMB_FILE_REF_TYPE || group->file == 0 || + group->record > NXMB_FILE_RECORD_MAX || group->nregs == 0 || + (uint32_t)group->record + group->nregs > NXMB_FILE_RECORDS) + { + return NXMB_EX_ILLEGAL_DATA_ADDRESS; + } + + return NXMB_EX_NONE; +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_fc20_read_file + * + * Description: + * Handle FC20 (0x14) Read File Record. Every sub-request is a group of + * records of one file; the response carries one sub-response (its length, + * the reference type and the registers) per sub-request. All sub-requests + * are checked and parsed before the first callback, because the response + * overwrites the request in ctx->adu. + * + ****************************************************************************/ + +enum nxmb_exception_e nxmb_fc20_read_file(nxmb_handle_t ctx) +{ + struct nxmb_file_group_s group[NXMB_FC20_SUBREQ_MAX]; + enum nxmb_exception_e ex; + uint16_t data_len = ctx->adu.length - 2; + uint16_t resp_len; + uint16_t pos; + uint8_t bcnt; + uint8_t nsub; + uint8_t i; + int ret; + + if (data_len < 1) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + bcnt = ctx->adu.data[0]; + + if (bcnt < NXMB_FC20_BCNT_MIN || bcnt > NXMB_FC20_BCNT_MAX || + (bcnt % NXMB_FILE_SUBREQ_LEN) != 0 || data_len != 1 + bcnt) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + if (ctx->callbacks == NULL || ctx->callbacks->file_cb == NULL) + { + return NXMB_EX_ILLEGAL_FUNCTION; + } + + nsub = bcnt / NXMB_FILE_SUBREQ_LEN; + resp_len = 0; + + for (i = 0; i < nsub; i++) + { + pos = 1 + i * NXMB_FILE_SUBREQ_LEN; + ex = nxmb_file_check_group(&ctx->adu.data[pos], &group[i]); + if (ex != NXMB_EX_NONE) + { + return ex; + } + + /* Sub-response: its length byte, the reference type, the registers */ + + resp_len += 2 + group[i].nregs * 2; + if (resp_len > NXMB_FC20_BCNT_MAX) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + } + + /* The response data length byte and the sub-responses must fit */ + + if (1 + resp_len > NXMB_ADU_DATA_MAX) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + ctx->adu.fc = NXMB_FC_READ_FILE; + ctx->adu.data[0] = (uint8_t)resp_len; + pos = 1; + + for (i = 0; i < nsub; i++) + { + ctx->adu.data[pos] = (uint8_t)(1 + group[i].nregs * 2); + ctx->adu.data[pos + 1] = NXMB_FILE_REF_TYPE; + + ret = ctx->callbacks->file_cb(&ctx->adu.data[pos + 2], group[i].file, + group[i].record, group[i].nregs, + NXMB_REG_READ, ctx->callbacks->priv); + if (ret != 0) + { + return nxmb_cb_ret_to_exception(ret); + } + + pos += 2 + group[i].nregs * 2; + } + + ctx->adu.length = (uint16_t)(2 + 1 + resp_len); + return NXMB_EX_NONE; +} + +/**************************************************************************** + * Name: nxmb_fc21_write_file + * + * Description: + * Handle FC21 (0x15) Write File Record. The request is checked to the + * last byte before the first callback; the groups are then written in + * order, and the response is an echo of the request. The specification + * does not make the groups atomic: a callback failure leaves the earlier + * groups written. + * + ****************************************************************************/ + +enum nxmb_exception_e nxmb_fc21_write_file(nxmb_handle_t ctx) +{ + struct nxmb_file_group_s group; + enum nxmb_exception_e ex; + uint16_t data_len = ctx->adu.length - 2; + uint16_t pos; + uint8_t bcnt; + int ret; + + if (data_len < 1) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + bcnt = ctx->adu.data[0]; + + if (bcnt < NXMB_FC21_BCNT_MIN || bcnt > NXMB_FC21_BCNT_MAX || + data_len != 1 + bcnt) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + if (ctx->callbacks == NULL || ctx->callbacks->file_cb == NULL) + { + return NXMB_EX_ILLEGAL_FUNCTION; + } + + /* First pass: every sub-request must be complete and valid, and they must + * fill the request data exactly. + */ + + for (pos = 1; pos < data_len; ) + { + if (data_len - pos < NXMB_FILE_SUBREQ_LEN) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + ex = nxmb_file_check_group(&ctx->adu.data[pos], &group); + if (ex != NXMB_EX_NONE) + { + return ex; + } + + pos += NXMB_FILE_SUBREQ_LEN; + + if (data_len - pos < group.nregs * 2) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + pos += group.nregs * 2; + } + + /* Second pass: write the groups, the response echoes the request */ + + for (pos = 1; pos < data_len; ) + { + (void)nxmb_file_check_group(&ctx->adu.data[pos], &group); + pos += NXMB_FILE_SUBREQ_LEN; + + ret = ctx->callbacks->file_cb(&ctx->adu.data[pos], group.file, + group.record, group.nregs, + NXMB_REG_WRITE, ctx->callbacks->priv); + if (ret != 0) + { + return nxmb_cb_ret_to_exception(ret); + } + + pos += group.nregs * 2; + } + + return NXMB_EX_NONE; +} diff --git a/industry/nxmodbus/nxmb_internal.h b/industry/nxmodbus/nxmb_internal.h index 6e781bd04..5ea16d966 100644 --- a/industry/nxmodbus/nxmb_internal.h +++ b/industry/nxmodbus/nxmb_internal.h @@ -58,6 +58,8 @@ #define NXMB_FC_WRITE_COILS 0x0f #define NXMB_FC_WRITE_HOLDINGS 0x10 #define NXMB_FC_REPORT_SERVER_ID 0x11 +#define NXMB_FC_READ_FILE 0x14 +#define NXMB_FC_WRITE_FILE 0x15 #define NXMB_FC_READWRITE_HOLDINGS 0x17 /* Transport operations interface. @@ -375,5 +377,7 @@ enum nxmb_exception_e nxmb_fc16_write_holdings(nxmb_handle_t ctx); enum nxmb_exception_e nxmb_fc23_readwrite_holding(nxmb_handle_t ctx); enum nxmb_exception_e nxmb_fc08_diagnostics(nxmb_handle_t ctx); enum nxmb_exception_e nxmb_fc17_report_server_id(nxmb_handle_t ctx); +enum nxmb_exception_e nxmb_fc20_read_file(nxmb_handle_t ctx); +enum nxmb_exception_e nxmb_fc21_write_file(nxmb_handle_t ctx); #endif /* __APPS_INDUSTRY_NXMODBUS_NXMB_INTERNAL_H */
