diff --git a/examples/nxmbserver/CMakeLists.txt b/examples/nxmbserver/CMakeLists.txt new file mode 100644 index 00000000000..d6880be3837 --- /dev/null +++ b/examples/nxmbserver/CMakeLists.txt @@ -0,0 +1,35 @@ +# ############################################################################## +# apps/examples/nxmbserver/CMakeLists.txt +# +# 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. +# +# ############################################################################## + +if(CONFIG_EXAMPLES_NXMBSERVER) + nuttx_add_application( + NAME + ${CONFIG_EXAMPLES_NXMBSERVER_PROGNAME} + PRIORITY + ${CONFIG_EXAMPLES_NXMBSERVER_PRIORITY} + STACKSIZE + ${CONFIG_EXAMPLES_NXMBSERVER_STACKSIZE} + MODULE + ${CONFIG_EXAMPLES_NXMBSERVER} + SRCS + nxmbserver_main.c) +endif() diff --git a/examples/nxmbserver/Kconfig b/examples/nxmbserver/Kconfig new file mode 100644 index 00000000000..428f49c5e54 --- /dev/null +++ b/examples/nxmbserver/Kconfig @@ -0,0 +1,33 @@ +# +# For a description of the syntax of this configuration file, +# see the file kconfig-language.txt in the NuttX tools repository. +# + +config EXAMPLES_NXMBSERVER + tristate "NxModbus Server Example" + default n + depends on NXMODBUS_SERVER + ---help--- + Enable the NxModbus server example. This example demonstrates + how to create a Modbus server (slave) that responds to client + requests. Supports RTU, ASCII, and TCP transports with simulated + register data. + +if EXAMPLES_NXMBSERVER + +config EXAMPLES_NXMBSERVER_PROGNAME + string "Program name" + default "nxmbserver" + ---help--- + This is the name of the program that will be used when the NSH ELF + program is installed. + +config EXAMPLES_NXMBSERVER_PRIORITY + int "NxModbus server task priority" + default 100 + +config EXAMPLES_NXMBSERVER_STACKSIZE + int "NxModbus server stack size" + default DEFAULT_TASK_STACKSIZE + +endif # EXAMPLES_NXMBSERVER diff --git a/examples/nxmbserver/Make.defs b/examples/nxmbserver/Make.defs new file mode 100644 index 00000000000..57fd777e7ed --- /dev/null +++ b/examples/nxmbserver/Make.defs @@ -0,0 +1,25 @@ +############################################################################ +# apps/examples/nxmbserver/Make.defs +# +# 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. +# +############################################################################ + +ifneq ($(CONFIG_EXAMPLES_NXMBSERVER),) +CONFIGURED_APPS += $(APPDIR)/examples/nxmbserver +endif diff --git a/examples/nxmbserver/Makefile b/examples/nxmbserver/Makefile new file mode 100644 index 00000000000..d3699d90bcb --- /dev/null +++ b/examples/nxmbserver/Makefile @@ -0,0 +1,32 @@ +############################################################################ +# apps/examples/nxmbserver/Makefile +# +# 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. +# +############################################################################ + +include $(APPDIR)/Make.defs + +MAINSRC = nxmbserver_main.c + +PROGNAME = $(CONFIG_EXAMPLES_NXMBSERVER_PROGNAME) +PRIORITY = $(CONFIG_EXAMPLES_NXMBSERVER_PRIORITY) +STACKSIZE = $(CONFIG_EXAMPLES_NXMBSERVER_STACKSIZE) +MODULE = $(CONFIG_EXAMPLES_NXMBSERVER) + +include $(APPDIR)/Application.mk diff --git a/examples/nxmbserver/nxmbserver_main.c b/examples/nxmbserver/nxmbserver_main.c new file mode 100644 index 00000000000..8e8572dda5d --- /dev/null +++ b/examples/nxmbserver/nxmbserver_main.c @@ -0,0 +1,543 @@ +/**************************************************************************** + * apps/examples/nxmbserver/nxmbserver_main.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 + +#include +#include +#include +#include +#include +#include + +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define DEFAULT_UNIT_ID 1 +#define DEFAULT_TCP_PORT 502 +#define DEFAULT_BAUDRATE 19200 + +#define NUM_COILS 100 +#define NUM_DISCRETE 100 +#define NUM_INPUT_REGS 100 +#define NUM_HOLDING_REGS 100 + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +/* Modbus type supported by example */ + +enum transport_type_e +{ + TRANSPORT_RTU = 0, + TRANSPORT_ASCII, + TRANSPORT_TCP +}; + +/* Modbus registers */ + +struct server_data_s +{ + uint8_t coils[NUM_COILS / 8 + 1]; + uint8_t discrete[NUM_DISCRETE / 8 + 1]; + uint16_t input_regs[NUM_INPUT_REGS]; + uint16_t holding_regs[NUM_HOLDING_REGS]; +}; + +/* Modbus server configuration */ + +struct server_config_s +{ + enum transport_type_e transport; + enum nxmb_parity_e parity; + FAR const char *device; + FAR const char *bindaddr; + uint32_t baudrate; + uint16_t port; + uint8_t unit_id; +}; + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static struct server_data_s g_data; +static volatile bool g_running = true; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: signal_handler + ****************************************************************************/ + +static void signal_handler(int signo) +{ + g_running = false; +} + +/**************************************************************************** + * Name: show_usage + ****************************************************************************/ + +static void show_usage(FAR const char *progname) +{ + printf("Usage: %s [OPTIONS]\n\n", progname); + printf("Transport Options:\n"); + printf(" -t TYPE Transport type: rtu, ascii, tcp (required)\n"); + printf(" -d DEVICE Serial device path (for RTU/ASCII)\n"); + printf(" -b BAUD Baud rate (default: %u)\n", DEFAULT_BAUDRATE); + printf(" -p PARITY Parity: none, even, odd (default: even)\n"); + printf(" -a ADDR Bind address (for TCP, default: 0.0.0.0)\n"); + printf(" -P PORT TCP port (default: %u)\n", DEFAULT_TCP_PORT); + printf("\nModbus Options:\n"); + printf(" -u UNIT Unit ID (default: %u)\n", DEFAULT_UNIT_ID); + printf("\nExample:\n"); + printf(" %s -t rtu -d /dev/ttyS1 -b 19200\n", progname); + printf(" %s -t tcp -P 502\n", progname); +} + +/**************************************************************************** + * Name: parse_parity + ****************************************************************************/ + +static int parse_parity(FAR const char *str, FAR enum nxmb_parity_e *parity) +{ + if (strcmp(str, "none") == 0) + { + *parity = NXMB_PAR_NONE; + } + else if (strcmp(str, "even") == 0) + { + *parity = NXMB_PAR_EVEN; + } + else if (strcmp(str, "odd") == 0) + { + *parity = NXMB_PAR_ODD; + } + else + { + return -EINVAL; + } + + return OK; +} + +/**************************************************************************** + * Name: parse_transport + ****************************************************************************/ + +static int parse_transport(FAR const char *str, + FAR enum transport_type_e *transport) +{ + if (strcmp(str, "rtu") == 0) + { + *transport = TRANSPORT_RTU; + } + else if (strcmp(str, "ascii") == 0) + { + *transport = TRANSPORT_ASCII; + } + else if (strcmp(str, "tcp") == 0) + { + *transport = TRANSPORT_TCP; + } + else + { + return -EINVAL; + } + + return OK; +} + +/**************************************************************************** + * Name: coils_callback + ****************************************************************************/ + +static int coils_callback(FAR uint8_t *buf, uint16_t addr, uint16_t count, + enum nxmb_regmode_e mode, FAR void *priv) +{ + uint16_t byte_idx; + uint16_t bit_idx; + uint16_t i; + + if (addr + count > NUM_COILS) + { + return -ENOENT; + } + + if (mode == NXMB_REG_READ) + { + for (i = 0; i < count; i++) + { + byte_idx = (addr + i) / 8; + bit_idx = (addr + i) % 8; + + if (g_data.coils[byte_idx] & (1 << bit_idx)) + { + buf[i / 8] |= (1 << (i % 8)); + } + else + { + buf[i / 8] &= ~(1 << (i % 8)); + } + } + } + else + { + for (i = 0; i < count; i++) + { + byte_idx = (addr + i) / 8; + bit_idx = (addr + i) % 8; + + if (buf[i / 8] & (1 << (i % 8))) + { + g_data.coils[byte_idx] |= (1 << bit_idx); + } + else + { + g_data.coils[byte_idx] &= ~(1 << bit_idx); + } + } + } + + return OK; +} + +/**************************************************************************** + * Name: discrete_callback + ****************************************************************************/ + +static int discrete_callback(FAR uint8_t *buf, uint16_t addr, + uint16_t count, FAR void *priv) +{ + uint16_t byte_idx; + uint16_t bit_idx; + uint16_t i; + + if (addr + count > NUM_DISCRETE) + { + return -ENOENT; + } + + for (i = 0; i < count; i++) + { + byte_idx = (addr + i) / 8; + bit_idx = (addr + i) % 8; + + if (g_data.discrete[byte_idx] & (1 << bit_idx)) + { + buf[i / 8] |= (1 << (i % 8)); + } + else + { + buf[i / 8] &= ~(1 << (i % 8)); + } + } + + return OK; +} + +/**************************************************************************** + * Name: input_callback + ****************************************************************************/ + +static int input_callback(FAR uint8_t *buf, uint16_t addr, uint16_t count, + FAR void *priv) +{ + uint16_t i; + + if (addr + count > NUM_INPUT_REGS) + { + return -ENOENT; + } + + for (i = 0; i < count; i++) + { + buf[i * 2] = (uint8_t)(g_data.input_regs[addr + i] >> 8); + buf[i * 2 + 1] = (uint8_t)(g_data.input_regs[addr + i] & 0xff); + } + + return OK; +} + +/**************************************************************************** + * Name: holding_callback + ****************************************************************************/ + +static int holding_callback(FAR uint8_t *buf, uint16_t addr, uint16_t count, + enum nxmb_regmode_e mode, FAR void *priv) +{ + uint16_t i; + + if (addr + count > NUM_HOLDING_REGS) + { + return -ENOENT; + } + + if (mode == NXMB_REG_READ) + { + for (i = 0; i < count; i++) + { + buf[i * 2] = (uint8_t)(g_data.holding_regs[addr + i] >> 8); + buf[i * 2 + 1] = (uint8_t)(g_data.holding_regs[addr + i] & 0xff); + } + } + else + { + for (i = 0; i < count; i++) + { + g_data.holding_regs[addr + i] = + (uint16_t)(buf[i * 2] << 8) | (uint16_t)buf[i * 2 + 1]; + } + } + + return OK; +} + +/**************************************************************************** + * Name: init_data + ****************************************************************************/ + +static void init_data(void) +{ + uint16_t i; + + memset(&g_data, 0, sizeof(g_data)); + + for (i = 0; i < NUM_INPUT_REGS; i++) + { + g_data.input_regs[i] = i * 10; + } + + for (i = 0; i < NUM_HOLDING_REGS; i++) + { + g_data.holding_regs[i] = i * 100; + } +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmbserver_main + ****************************************************************************/ + +int main(int argc, FAR char *argv[]) +{ + struct server_config_s config; + struct nxmb_config_s mb_config; + struct nxmb_callbacks_s callbacks; + nxmb_handle_t handle; + bool transport_set = false; + int option; + int ret; + + /* Default config */ + + memset(&config, 0, sizeof(config)); + config.baudrate = DEFAULT_BAUDRATE; + config.parity = NXMB_PAR_EVEN; + config.port = DEFAULT_TCP_PORT; + config.unit_id = DEFAULT_UNIT_ID; + + /* Handle CLI */ + + while ((option = getopt(argc, argv, "t:d:b:p:a:P:u:h")) != -1) + { + switch (option) + { + case 't': + if (parse_transport(optarg, &config.transport) < 0) + { + fprintf(stderr, "Error: invalid transport '%s'\n", optarg); + return EXIT_FAILURE; + } + + transport_set = true; + break; + + case 'd': + config.device = optarg; + break; + + case 'b': + config.baudrate = (uint32_t)strtoul(optarg, NULL, 0); + break; + + case 'p': + if (parse_parity(optarg, &config.parity) < 0) + { + fprintf(stderr, "Error: invalid parity '%s'\n", optarg); + return EXIT_FAILURE; + } + + break; + + case 'a': + config.bindaddr = optarg; + break; + + case 'P': + config.port = (uint16_t)strtoul(optarg, NULL, 0); + break; + + case 'u': + config.unit_id = (uint8_t)strtoul(optarg, NULL, 0); + break; + + case 'h': + default: + show_usage(argv[0]); + return (option == 'h') ? EXIT_SUCCESS : EXIT_FAILURE; + } + } + + /* Validate input */ + + if (!transport_set) + { + fprintf(stderr, "Error: transport type (-t) is required\n\n"); + show_usage(argv[0]); + return EXIT_FAILURE; + } + + if ((config.transport == TRANSPORT_RTU || + config.transport == TRANSPORT_ASCII) && + config.device == NULL) + { + fprintf(stderr, "Error: device path (-d) required for RTU/ASCII\n"); + return EXIT_FAILURE; + } + + /* Connect signals */ + + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + + /* Init modbus data */ + + init_data(); + + /* Init modbus stack */ + + memset(&callbacks, 0, sizeof(callbacks)); + callbacks.coil_cb = coils_callback; + callbacks.discrete_cb = discrete_callback; + callbacks.input_cb = input_callback; + callbacks.holding_cb = holding_callback; + + memset(&mb_config, 0, sizeof(mb_config)); + mb_config.unit_id = config.unit_id; + mb_config.is_client = false; + + switch (config.transport) + { + case TRANSPORT_RTU: + mb_config.mode = NXMB_MODE_RTU; + mb_config.transport.serial.devpath = config.device; + mb_config.transport.serial.baudrate = config.baudrate; + mb_config.transport.serial.parity = config.parity; + printf("Starting Modbus RTU server on %s (baud=%" PRId32 + ", unit=%u)\n", config.device, config.baudrate, + config.unit_id); + break; + + case TRANSPORT_ASCII: + mb_config.mode = NXMB_MODE_ASCII; + mb_config.transport.serial.devpath = config.device; + mb_config.transport.serial.baudrate = config.baudrate; + mb_config.transport.serial.parity = config.parity; + printf("Starting Modbus ASCII server on %s (baud=%" PRId32 + ", unit=%u)\n", config.device, config.baudrate, + config.unit_id); + break; + + case TRANSPORT_TCP: + mb_config.mode = NXMB_MODE_TCP; + mb_config.transport.tcp.port = config.port; + mb_config.transport.tcp.bindaddr = config.bindaddr; + printf("Starting Modbus TCP server on port %u (unit=%u)\n", + config.port, config.unit_id); + break; + } + + ret = nxmb_create(&handle, &mb_config); + if (ret < 0) + { + fprintf(stderr, "Error: failed to create Modbus context: %d\n", ret); + return EXIT_FAILURE; + } + + ret = nxmb_set_callbacks(handle, &callbacks); + if (ret < 0) + { + fprintf(stderr, "Error: failed to set callbacks: %d\n", ret); + nxmb_destroy(handle); + return EXIT_FAILURE; + } + + ret = nxmb_enable(handle); + if (ret < 0) + { + fprintf(stderr, "Error: failed to enable context: %d\n", ret); + nxmb_destroy(handle); + return EXIT_FAILURE; + } + + printf("Server running. Press Ctrl+C to stop.\n"); + printf("Register map:\n"); + printf(" Coils: 1-%d (read/write)\n", NUM_COILS); + printf(" Discrete: 1-%d (read-only)\n", NUM_DISCRETE); + printf(" Input regs: 1-%d (read-only, value=addr*10)\n", + NUM_INPUT_REGS); + printf(" Holding regs: 1-%d (read/write, initial=addr*100)\n", + NUM_HOLDING_REGS); + + /* Poll loop */ + + while (g_running) + { + ret = nxmb_poll(handle); + if (ret < 0 && ret != -EAGAIN) + { + fprintf(stderr, "Error: poll failed: %d\n", ret); + break; + } + } + + printf("\nShutting down...\n"); + nxmb_disable(handle); + nxmb_destroy(handle); + + return EXIT_SUCCESS; +} diff --git a/include/nxmodbus/nxmb_client.h b/include/nxmodbus/nxmb_client.h new file mode 100644 index 00000000000..49c10e5424c --- /dev/null +++ b/include/nxmodbus/nxmb_client.h @@ -0,0 +1,261 @@ +/**************************************************************************** + * apps/include/nxmodbus/nxmb_client.h + * + * 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. + * + ****************************************************************************/ + +#ifndef __APPS_INCLUDE_NXMODBUS_NXMB_CLIENT_H +#define __APPS_INCLUDE_NXMODBUS_NXMB_CLIENT_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include +#include + +#include +#include + +#include + +/**************************************************************************** + * Public Function Prototypes + ****************************************************************************/ + +#ifdef __cplusplus +extern "C" +{ +#endif + +/**************************************************************************** + * Name: nxmb_read_coils + * + * Description: + * Read coil values from a remote unit. + * + * Input Parameters: + * h - The NxModbus client instance. + * uid - The remote unit identifier. + * addr - The first coil address to read. + * count - The number of coils to read. + * buf - The destination buffer for the returned coil values. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_read_coils(nxmb_handle_t h, uint8_t uid, uint16_t addr, + uint16_t count, FAR uint8_t *buf); + +/**************************************************************************** + * Name: nxmb_read_discrete + * + * Description: + * Read discrete input values from a remote unit. + * + * Input Parameters: + * h - The NxModbus client instance. + * uid - The remote unit identifier. + * addr - The first discrete input address to read. + * count - The number of discrete inputs to read. + * buf - The destination buffer for the returned values. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_read_discrete(nxmb_handle_t h, uint8_t uid, uint16_t addr, + uint16_t count, FAR uint8_t *buf); + +/**************************************************************************** + * Name: nxmb_read_input + * + * Description: + * Read input registers from a remote unit. + * + * Input Parameters: + * h - The NxModbus client instance. + * uid - The remote unit identifier. + * addr - The first input register address to read. + * count - The number of registers to read. + * buf - The destination buffer for the returned register values. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_read_input(nxmb_handle_t h, uint8_t uid, uint16_t addr, + uint16_t count, FAR uint16_t *buf); + +/**************************************************************************** + * Name: nxmb_read_holding + * + * Description: + * Read holding registers from a remote unit. + * + * Input Parameters: + * h - The NxModbus client instance. + * uid - The remote unit identifier. + * addr - The first holding register address to read. + * count - The number of registers to read. + * buf - The destination buffer for the returned register values. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_read_holding(nxmb_handle_t h, uint8_t uid, uint16_t addr, + uint16_t count, FAR uint16_t *buf); + +/**************************************************************************** + * Name: nxmb_write_coil + * + * Description: + * Write a single coil on a remote unit. + * + * Input Parameters: + * h - The NxModbus client instance. + * uid - The remote unit identifier. + * addr - The coil address to update. + * value - The coil state to write. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_write_coil(nxmb_handle_t h, uint8_t uid, uint16_t addr, bool value); + +/**************************************************************************** + * Name: nxmb_write_holding + * + * Description: + * Write a single holding register on a remote unit. + * + * Input Parameters: + * h - The NxModbus client instance. + * uid - The remote unit identifier. + * addr - The holding register address to update. + * value - The register value to write. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_write_holding(nxmb_handle_t h, uint8_t uid, uint16_t addr, + uint16_t value); + +/**************************************************************************** + * Name: nxmb_write_coils + * + * Description: + * Write multiple coils on a remote unit. + * + * Input Parameters: + * h - The NxModbus client instance. + * uid - The remote unit identifier. + * addr - The first coil address to update. + * count - The number of coils to write. + * buf - The source buffer containing coil values. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_write_coils(nxmb_handle_t h, uint8_t uid, uint16_t addr, + uint16_t count, FAR const uint8_t *buf); + +/**************************************************************************** + * Name: nxmb_write_holdings + * + * Description: + * Write multiple holding registers on a remote unit. + * + * Input Parameters: + * h - The NxModbus client instance. + * uid - The remote unit identifier. + * addr - The first holding register address to update. + * count - The number of registers to write. + * buf - The source buffer containing register values. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_write_holdings(nxmb_handle_t h, uint8_t uid, uint16_t addr, + uint16_t count, FAR const uint16_t *buf); + +/**************************************************************************** + * Name: nxmb_readwrite_holdings + * + * Description: + * Perform a combined read/write of holding registers on a remote unit + * using FC23 (0x17). The write is performed before the read on the + * server side. + * + * Input Parameters: + * h - The NxModbus client instance. + * uid - The remote unit identifier. + * rd_addr - The first holding register address to read. + * rd_count - The number of registers to read (1-125). + * rd_buf - The destination buffer for the returned register values. + * wr_addr - The first holding register address to write. + * wr_count - The number of registers to write (1-121). + * wr_buf - The source buffer containing register values to write. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_readwrite_holdings(nxmb_handle_t h, uint8_t uid, uint16_t rd_addr, + uint16_t rd_count, FAR uint16_t *rd_buf, + uint16_t wr_addr, uint16_t wr_count, + FAR const uint16_t *wr_buf); + +/**************************************************************************** + * Name: nxmb_set_timeout + * + * Description: + * Set the client-side response timeout. + * + * Input Parameters: + * h - The NxModbus client instance. + * timeout_ms - The timeout in milliseconds. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_set_timeout(nxmb_handle_t h, uint32_t timeout_ms); + +#ifdef __cplusplus +} +#endif + +#endif /* __APPS_INCLUDE_NXMODBUS_NXMB_CLIENT_H */ diff --git a/include/nxmodbus/nxmb_raw.h b/include/nxmodbus/nxmb_raw.h new file mode 100644 index 00000000000..e01f42598d5 --- /dev/null +++ b/include/nxmodbus/nxmb_raw.h @@ -0,0 +1,108 @@ +/**************************************************************************** + * apps/include/nxmodbus/nxmb_raw.h + * + * 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. + * + ****************************************************************************/ + +#ifndef __APPS_INCLUDE_NXMODBUS_NXMB_RAW_H +#define __APPS_INCLUDE_NXMODBUS_NXMB_RAW_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include +#include + +#include +#include + +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define NXMB_RAW_HDR_SIZE 8 + +/**************************************************************************** + * Public Function Prototypes + ****************************************************************************/ + +#ifdef __cplusplus +extern "C" +{ +#endif + +/**************************************************************************** + * Name: nxmb_raw_submit_rx + * + * Description: + * Submit a received raw ADU into the NxModbus protocol core. + * + * Input Parameters: + * h - The NxModbus instance receiving the frame. + * adu - The received raw ADU container. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_raw_submit_rx(nxmb_handle_t h, FAR const struct nxmb_adu_s *adu); + +/**************************************************************************** + * Name: nxmb_raw_put_header + * + * Description: + * Serialize the 8-byte MBAP+FC header for an ADU. + * + * Input Parameters: + * adu - The ADU providing the header fields. + * hdr - The destination buffer for the serialized header. + * + * Returned Value: + * None. + * + ****************************************************************************/ + +void nxmb_raw_put_header(FAR const struct nxmb_adu_s *adu, FAR uint8_t *hdr); + +/**************************************************************************** + * Name: nxmb_raw_get_header + * + * Description: + * Parse the 8-byte MBAP+FC header into an ADU structure. + * + * Input Parameters: + * adu - The destination ADU structure. + * hdr - The source buffer containing the serialized header. + * + * Returned Value: + * None. + * + ****************************************************************************/ + +void nxmb_raw_get_header(FAR struct nxmb_adu_s *adu, FAR const uint8_t *hdr); + +#ifdef __cplusplus +} +#endif + +#endif /* __APPS_INCLUDE_NXMODBUS_NXMB_RAW_H */ diff --git a/include/nxmodbus/nxmodbus.h b/include/nxmodbus/nxmodbus.h new file mode 100644 index 00000000000..f5307989baf --- /dev/null +++ b/include/nxmodbus/nxmodbus.h @@ -0,0 +1,378 @@ +/**************************************************************************** + * apps/include/nxmodbus/nxmodbus.h + * + * 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. + * + ****************************************************************************/ + +#ifndef __APPS_INCLUDE_NXMODBUS_NXMODBUS_H +#define __APPS_INCLUDE_NXMODBUS_NXMODBUS_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include +#include + +#include +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define NXMB_TCP_PORT_DEFAULT 502 + +/* Maximum number of PDU data bytes (everything after the FC byte). + * Configured via Kconfig (CONFIG_NXMODBUS_ADU_DATA_MAX). + */ + +#define NXMB_ADU_DATA_MAX CONFIG_NXMODBUS_ADU_DATA_MAX + +/**************************************************************************** + * Public Types + ****************************************************************************/ + +/* Opaque handle to an NxModbus instance. */ + +typedef FAR struct nxmb_context_s *nxmb_handle_t; + +/* Custom frame handler */ + +typedef CODE int (*nxmb_custom_fc_handler_t)(nxmb_handle_t ctx); + +/* Modbus Application Data Unit container. + * + * Single canonical ADU representation shared by the protocol core and all + * transports. Transports parse incoming wire frames into this struct and + * serialize this struct back out on transmit. + * + * trans_id, proto_id, length - MBAP header fields (TCP-only). + * unit_id - Modbus address byte. + * fc - Modbus function code byte. + * data[] - PDU payload after the function code. + * crc - RTU/ASCII checksum (serial-only). + * + * The MBAP `length` field counts the bytes following it: unit_id + fc + + * data_count. Transports that don't carry MBAP still populate this field + * with the same value so that consumers can derive data_count uniformly: + * data_count = adu.length - 2 + */ + +struct nxmb_adu_s +{ + uint16_t trans_id; + uint16_t proto_id; + uint16_t length; + uint8_t unit_id; + uint8_t fc; + uint8_t data[NXMB_ADU_DATA_MAX]; + uint16_t crc; +}; + +/* Callback used by raw ADU mode to emit a fully-formed frame. */ + +typedef int (*nxmb_raw_tx_cb_t)(FAR const struct nxmb_adu_s *adu, + FAR void *user_data); + +/* Supported transport modes. */ + +enum nxmb_mode_e +{ + NXMB_MODE_INVAL = 0, + NXMB_MODE_RTU, /* Modbus RTU */ + NXMB_MODE_ASCII, /* Modbus ASCII */ + NXMB_MODE_TCP, /* Modbus TCP */ + NXMB_MODE_RAW /* Custom ADU transport */ +}; + +/* Serial parity configuration. */ + +enum nxmb_parity_e +{ + NXMB_PAR_NONE = 0, + NXMB_PAR_EVEN, + NXMB_PAR_ODD +}; + +/* Register access direction passed to callbacks. */ + +enum nxmb_regmode_e +{ + NXMB_REG_READ = 0, + NXMB_REG_WRITE +}; + +/* Standard Modbus exception codes. */ + +enum nxmb_exception_e +{ + NXMB_EX_NONE = 0x00, + NXMB_EX_ILLEGAL_FUNCTION = 0x01, + NXMB_EX_ILLEGAL_DATA_ADDRESS = 0x02, + NXMB_EX_ILLEGAL_DATA_VALUE = 0x03, + NXMB_EX_DEVICE_FAILURE = 0x04, + NXMB_EX_ACKNOWLEDGE = 0x05, + NXMB_EX_DEVICE_BUSY = 0x06, + NXMB_EX_MEMORY_PARITY_ERROR = 0x08, + NXMB_EX_GATEWAY_PATH_FAILED = 0x0a, + NXMB_EX_GATEWAY_TGT_FAILED = 0x0b +}; + +/* Serial transport configuration. */ + +struct nxmb_serial_config_s +{ + FAR const char *devpath; + enum nxmb_parity_e parity; + uint32_t baudrate; +}; + +/* TCP transport configuration. + * + * For slave mode, bindaddr selects the local address and port selects the + * listening port. For client mode, host selects the remote endpoint and + * port selects the remote port. + */ + +struct nxmb_tcp_config_s +{ + FAR const char *host; + FAR const char *bindaddr; + uint16_t port; +}; + +/* Raw ADU transport configuration. */ + +struct nxmb_raw_config_s +{ + FAR void *user_data; + nxmb_raw_tx_cb_t tx_cb; +}; + +union nxmb_transport_config_u +{ + struct nxmb_serial_config_s serial; + struct nxmb_tcp_config_s tcp; + struct nxmb_raw_config_s raw; +}; + +/* Generic NxModbus instance configuration. */ + +struct nxmb_config_s +{ + union nxmb_transport_config_u transport; + enum nxmb_mode_e mode; + uint8_t unit_id; + bool is_client; +}; + +/* Application callback table for Modbus data model access. + * + * The protocol stack passes buffers covering a contiguous address range and + * the application fills or consumes them depending on the register mode. + */ + +struct nxmb_callbacks_s +{ + CODE int (*coil_cb)(FAR uint8_t *buf, uint16_t addr, uint16_t ncoils, + enum nxmb_regmode_e mode, FAR void *priv); + CODE int (*discrete_cb)(FAR uint8_t *buf, uint16_t addr, + uint16_t ndiscrete, FAR void *priv); + CODE int (*input_cb)(FAR uint8_t *buf, uint16_t addr, uint16_t nregs, + 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); + FAR void *priv; +}; + +/**************************************************************************** + * Public Function Prototypes + ****************************************************************************/ + +#ifdef __cplusplus +extern "C" +{ +#endif + +/**************************************************************************** + * Name: nxmb_create + * + * Description: + * Create and initialize an NxModbus instance. This function allocates one + * entry from the static NxModbus instance pool and initializes it for the + * selected transport mode. + * + * Input Parameters: + * handle - A location to receive the opaque NxModbus handle. + * config - The transport and role configuration for the new instance. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_create(FAR nxmb_handle_t *handle, + FAR const struct nxmb_config_s *config); + +/**************************************************************************** + * Name: nxmb_destroy + * + * Description: + * Destroy a previously created NxModbus instance. This function releases + * a handle previously obtained from nxmb_create(). + * + * Input Parameters: + * handle - The NxModbus instance to destroy. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_destroy(nxmb_handle_t handle); + +/**************************************************************************** + * Name: nxmb_set_callbacks + * + * Description: + * Register the application callback table for an instance. The registered + * callback table defines how the application exposes coils, discrete + * inputs, input registers, and holding registers to the protocol stack. + * + * Input Parameters: + * handle - The NxModbus instance to update. + * cbs - The callback table to associate with the instance. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_set_callbacks(nxmb_handle_t handle, + FAR const struct nxmb_callbacks_s *cbs); + +/**************************************************************************** + * Name: nxmb_enable + * + * Description: + * Enable the configured transport and protocol handling. This function + * prepares the configured transport backend so the instance can start + * sending or receiving Modbus ADUs. + * + * Input Parameters: + * handle - The NxModbus instance to enable. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_enable(nxmb_handle_t handle); + +/**************************************************************************** + * Name: nxmb_disable + * + * Description: + * Disable an active NxModbus instance. This function stops frame + * processing and releases any transport state held by an enabled instance. + * + * Input Parameters: + * handle - The NxModbus instance to disable. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_disable(nxmb_handle_t handle); + +/**************************************************************************** + * Name: nxmb_poll + * + * Description: + * Execute one server-side polling iteration. This function performs a + * single poll of the transport backend and processes one server-side + * Modbus transaction when data is available. + * + * Input Parameters: + * handle - The NxModbus instance to poll. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_poll(nxmb_handle_t handle); + +/**************************************************************************** + * Name: nxmb_set_server_id + * + * Description: + * Configure the data returned by FC17 (Report Server ID). The response + * contains the server ID byte, a run indicator, and optional additional + * data supplied by the application. + * + * Input Parameters: + * handle - The NxModbus instance to update. + * id - The server ID byte. + * is_running - True if the device is in a running state. + * additional - Optional additional data bytes (may be NULL). + * addlen - Length of additional data. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_set_server_id(nxmb_handle_t handle, uint8_t id, bool is_running, + FAR const uint8_t *additional, uint16_t addlen); + +#ifdef CONFIG_NXMODBUS_CUSTOM_FC +/**************************************************************************** + * Name: nxmb_register_custom_fc + * + * Description: + * Register a custom Modbus function code handler for an instance. + * Custom handlers are checked after standard handlers, so standard + * function codes cannot be overridden. + * + * Input Parameters: + * h - The NxModbus instance handle. + * fc - The custom function code to register. + * handler - The handler function. Receives the instance handle and + * operates on ctx->adu in place: the request fields + * (adu.fc, adu.data[], adu.length) are overwritten with the + * response. Returns zero on success or a negated errno value + * on failure. + * + * Returned Value: + * Zero on success; a negated errno value on failure. + * + ****************************************************************************/ + +int nxmb_register_custom_fc(nxmb_handle_t h, uint8_t fc, + nxmb_custom_fc_handler_t handler); +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* __APPS_INCLUDE_NXMODBUS_NXMODBUS_H */ diff --git a/industry/nxmodbus/CMakeLists.txt b/industry/nxmodbus/CMakeLists.txt new file mode 100644 index 00000000000..35be513e471 --- /dev/null +++ b/industry/nxmodbus/CMakeLists.txt @@ -0,0 +1,68 @@ +# ############################################################################## +# apps/industry/nxmodbus/CMakeLists.txt +# +# 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. +# +# ############################################################################## + +if(CONFIG_INDUSTRY_NXMODBUS) + + set(CSRCS) + + # Core sources + list(APPEND CSRCS core/nxmb_context.c core/nxmb_utils.c) + + if(CONFIG_NXMODBUS_SERVER) + list(APPEND CSRCS core/nxmb_server.c) + list(APPEND CSRCS core/nxmb_func.c) + 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) + endif() + + if(CONFIG_NXMODBUS_CLIENT) + list(APPEND CSRCS core/nxmb_client.c) + endif() + + # Transport layer sources + if(CONFIG_NXMODBUS_RTU OR CONFIG_NXMODBUS_ASCII) + list(APPEND CSRCS transport/nxmb_serial_common.c) + endif() + + if(CONFIG_NXMODBUS_RTU) + list(APPEND CSRCS transport/nxmb_crc.c transport/nxmb_serial.c) + endif() + + if(CONFIG_NXMODBUS_ASCII) + list(APPEND CSRCS transport/nxmb_lrc.c transport/nxmb_ascii.c) + endif() + + if(CONFIG_NXMODBUS_RAW_ADU) + list(APPEND CSRCS transport/nxmb_raw.c) + endif() + + if(CONFIG_NXMODBUS_TCP) + list(APPEND CSRCS transport/nxmb_tcp.c) + endif() + + if(CSRCS) + target_include_directories(apps PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_sources(apps PRIVATE ${CSRCS}) + endif() + +endif() diff --git a/industry/nxmodbus/Kconfig b/industry/nxmodbus/Kconfig new file mode 100644 index 00000000000..31f555fe777 --- /dev/null +++ b/industry/nxmodbus/Kconfig @@ -0,0 +1,245 @@ +# +# For a description of the syntax of this configuration file, +# see the file kconfig-language.txt in the NuttX tools repository. +# + +menuconfig INDUSTRY_NXMODBUS + bool "NxModbus - New Modbus Stack" + default n + ---help--- + NxModbus is a modern Modbus stack for NuttX with multi-instance + support, per-instance register callbacks, and pluggable transport + backends (RTU, ASCII, TCP, Raw ADU). + + This implementation follows NuttX coding standards and is + designed for upstream contribution to nuttx-apps. + +if INDUSTRY_NXMODBUS + +config NXMODBUS_SERVER + bool "Server (slave) support" + default y + ---help--- + Enable Modbus server (slave) support. The server processes + incoming Modbus requests and calls registered register + callbacks. + +config NXMODBUS_CLIENT + bool "Client (master) support" + default n + ---help--- + Enable Modbus client (master) support. The client sends + Modbus requests to a server and receives responses. + +config NXMODBUS_RTU + bool "Modbus RTU mode" + default y + ---help--- + Enable Modbus RTU (Remote Terminal Unit) serial transmission + mode. Uses binary encoding with CRC16 error checking. + +config NXMODBUS_ASCII + bool "Modbus ASCII mode" + default n + ---help--- + Enable Modbus ASCII serial transmission mode. Uses + hex-encoded text with LRC error checking. Slower but + more human-readable than RTU. + +config NXMODBUS_TCP + bool "Modbus TCP mode" + default n + depends on NET_TCP + ---help--- + Enable Modbus TCP support. Modbus frames are encapsulated + in TCP/IP using the MBAP header format. Requires network + stack with TCP support. + +config NXMODBUS_RAW_ADU + bool "Raw ADU transport support" + default n + ---help--- + Enable raw ADU (Application Data Unit) transport mode. + The application provides callbacks for transmitting and + receiving raw Modbus frames. This enables custom transports + such as TLS, CAN, BLE, or MQTT. + +config NXMODBUS_MAX_INSTANCES + int "Maximum number of NxModbus instances" + default 1 + range 1 16 + ---help--- + Maximum number of simultaneous NxModbus instances. Each + instance can use a different transport (RTU, TCP, etc.) + and configuration. Instances are allocated from a static + pool at compile time. + +config NXMODBUS_ADU_DATA_MAX + int "ADU data buffer size in bytes" + default 252 + range 4 252 + ---help--- + Size of the data[] payload in struct nxmb_adu_s, in bytes. + This is the PDU body after the function code. The Modbus + specification allows up to 252 data bytes (1 fc + 252 data = + 253-byte PDU max). Reduce this value to save RAM on + resource-constrained systems. + +config NXMODBUS_REP_SERVER_ID_BUF + int "Report Server ID buffer size" + default 32 + range 4 253 + ---help--- + Size of the buffer used by FC17 (Report Server ID). + This buffer holds the server ID byte, run indicator, and + optional additional data configured via nxmb_set_server_id(). + +config NXMODBUS_ASCII_TIMEOUT_SEC + int "ASCII character timeout (seconds)" + default 1 + range 1 60 + depends on NXMODBUS_ASCII + ---help--- + Character timeout for Modbus ASCII mode in seconds. If no + character is received within this period, the current frame + is discarded. Default is 1 second per the Modbus specification. + +config NXMODBUS_CLIENT_TIMEOUT_MS + int "Client default response timeout (ms)" + default 1000 + range 100 60000 + depends on NXMODBUS_CLIENT + ---help--- + Default response timeout for client (master) mode in + milliseconds. If no response is received from the server + within this period, the request fails with ETIMEDOUT. + Can be overridden at runtime via nxmb_set_timeout(). + +config NXMODBUS_TCP_MAX_CLIENTS + int "Maximum simultaneous TCP client connections" + default 1 + range 1 8 + depends on NXMODBUS_TCP + ---help--- + Maximum number of simultaneous TCP client connections + accepted by a Modbus TCP server instance. Each connection + uses one file descriptor. Set to 1 for minimal resource + usage. + +config NXMODBUS_TCP_TIMEOUT_SEC + int "TCP idle connection timeout (seconds)" + default 60 + range 1 3600 + depends on NXMODBUS_TCP + ---help--- + Idle timeout for TCP connections in seconds. If no data is + received on a client connection within this period, the + connection is closed. Set to a higher value for slow networks. + +config NXMODBUS_RTU_IDLE_TIMEOUT_MS + int "RTU inter-frame idle timeout (ms)" + default 50 + range 1 1000 + depends on NXMODBUS_RTU + ---help--- + Fallback idle timeout in milliseconds used by the RTU + transport when waiting for a new frame via select(). This + is the maximum time to block waiting for data when no + frame is in progress. The T3.5 character timing is still + used for frame delimiting within an active reception. + +menu "Function Code Selection" + +config NXMODBUS_FUNC_READ_COILS + bool "FC01 Read Coils" + default y + depends on NXMODBUS_SERVER + ---help--- + Enable FC01 Read Coils function code handler. + +config NXMODBUS_FUNC_READ_DISCRETE + bool "FC02 Read Discrete Inputs" + default y + depends on NXMODBUS_SERVER + ---help--- + Enable FC02 Read Discrete Inputs function code handler. + +config NXMODBUS_FUNC_READ_HOLDING + bool "FC03 Read Holding Registers" + default y + depends on NXMODBUS_SERVER + ---help--- + Enable FC03 Read Holding Registers function code handler. + +config NXMODBUS_FUNC_READ_INPUT + bool "FC04 Read Input Registers" + default y + depends on NXMODBUS_SERVER + ---help--- + Enable FC04 Read Input Registers function code handler. + +config NXMODBUS_FUNC_WRITE_COIL + bool "FC05 Write Single Coil" + default y + depends on NXMODBUS_SERVER + ---help--- + Enable FC05 Write Single Coil function code handler. + +config NXMODBUS_FUNC_WRITE_HOLDING + bool "FC06 Write Single Holding Register" + default y + depends on NXMODBUS_SERVER + ---help--- + Enable FC06 Write Single Holding Register function + code handler. + +config NXMODBUS_FUNC_DIAGNOSTICS + bool "FC08 Diagnostics" + default y + depends on NXMODBUS_SERVER + ---help--- + Enable FC08 Diagnostics function code handler. + Only sub-function 0x0000 (Return Query Data) is supported. + +config NXMODBUS_FUNC_WRITE_COILS + bool "FC15 Write Multiple Coils" + default y + depends on NXMODBUS_SERVER + ---help--- + Enable FC15 Write Multiple Coils function code handler. + +config NXMODBUS_FUNC_WRITE_HOLDINGS + bool "FC16 Write Multiple Holding Registers" + default y + depends on NXMODBUS_SERVER + ---help--- + Enable FC16 Write Multiple Holding Registers function + code handler. + +config NXMODBUS_FUNC_REPORT_SERVER_ID + bool "FC17 Report Server ID" + default y + depends on NXMODBUS_SERVER + ---help--- + Enable FC17 Report Server ID function code handler. + +config NXMODBUS_FUNC_READWRITE_HOLDINGS + bool "FC23 Read/Write Multiple Holding Registers" + default y + depends on NXMODBUS_SERVER + ---help--- + Enable FC23 Read/Write Multiple Holding Registers + function code handler. + +endmenu # Function Code Selection + +config NXMODBUS_CUSTOM_FC + bool "Custom function code extension support" + default y + depends on NXMODBUS + ---help--- + Enable support for registering custom Modbus function + code handlers. Applications can extend the Modbus + function code set via nxmb_register_custom_fc(). + +endif # INDUSTRY_NXMODBUS diff --git a/industry/nxmodbus/Make.defs b/industry/nxmodbus/Make.defs new file mode 100644 index 00000000000..539b48d64e9 --- /dev/null +++ b/industry/nxmodbus/Make.defs @@ -0,0 +1,25 @@ +############################################################################ +# apps/modbus/nxmodbus/Make.defs +# +# 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. +# +############################################################################ + +ifneq ($(CONFIG_INDUSTRY_NXMODBUS),) +CONFIGURED_APPS += $(APPDIR)/industry/nxmodbus +endif diff --git a/industry/nxmodbus/Makefile b/industry/nxmodbus/Makefile new file mode 100644 index 00000000000..64caacdbae1 --- /dev/null +++ b/industry/nxmodbus/Makefile @@ -0,0 +1,76 @@ +############################################################################ +# apps/industry/nxmodbus/Makefile +# +# 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. +# +############################################################################ + +include $(APPDIR)/Make.defs + +# NxModbus Library + +ifeq ($(CONFIG_INDUSTRY_NXMODBUS),y) + +# Core sources +CSRCS := core/nxmb_context.c +CSRCS += core/nxmb_utils.c + +ifeq ($(CONFIG_NXMODBUS_SERVER),y) +CSRCS += core/nxmb_server.c +CSRCS += core/nxmb_func.c +CSRCS += core/nxmb_func_coils.c +CSRCS += core/nxmb_func_discrete.c +CSRCS += core/nxmb_func_holding.c +CSRCS += core/nxmb_func_input.c +CSRCS += core/nxmb_func_diag.c +CSRCS += core/nxmb_func_other.c +endif + +ifeq ($(CONFIG_NXMODBUS_CLIENT),y) +CSRCS += core/nxmb_client.c +endif + +# Transport layer sources +ifneq (,$(filter y,$(CONFIG_NXMODBUS_RTU) $(CONFIG_NXMODBUS_ASCII))) +CSRCS += transport/nxmb_serial_common.c +endif + +ifeq ($(CONFIG_NXMODBUS_RTU),y) +CSRCS += transport/nxmb_crc.c +CSRCS += transport/nxmb_serial.c +endif + +ifeq ($(CONFIG_NXMODBUS_ASCII),y) +CSRCS += transport/nxmb_lrc.c +CSRCS += transport/nxmb_ascii.c +endif + +ifeq ($(CONFIG_NXMODBUS_RAW_ADU),y) +CSRCS += transport/nxmb_raw.c +endif + +ifeq ($(CONFIG_NXMODBUS_TCP),y) +CSRCS += transport/nxmb_tcp.c +endif + +# Include private headers +CFLAGS += ${INCDIR_PREFIX}$(CURDIR) + +endif + +include $(APPDIR)/Application.mk diff --git a/industry/nxmodbus/core/nxmb_client.c b/industry/nxmodbus/core/nxmb_client.c new file mode 100644 index 00000000000..24b264f9206 --- /dev/null +++ b/industry/nxmodbus/core/nxmb_client.c @@ -0,0 +1,739 @@ +/**************************************************************************** + * apps/industry/nxmodbus/core/nxmb_client.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 + +#include +#include +#include +#include +#include + +#include +#include + +#include "nxmb_internal.h" + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +struct nxmb_client_state_s +{ + uint32_t timeout_ms; +}; + +/**************************************************************************** + * Private Function Prototypes + ****************************************************************************/ + +static int nxmb_client_tx_wait_rx(nxmb_handle_t ctx, + uint8_t expected_uid, uint8_t expected_fc); +static int nxmb_client_validate_response(nxmb_handle_t ctx, + uint8_t expected_uid, + uint8_t expected_fc); + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_exception_to_errno + ****************************************************************************/ + +static int nxmb_exception_to_errno(uint8_t exception) +{ + switch (exception) + { + case NXMB_EX_ILLEGAL_FUNCTION: + return -ENXIO; + + case NXMB_EX_ILLEGAL_DATA_ADDRESS: + return -EFAULT; + + case NXMB_EX_ILLEGAL_DATA_VALUE: + return -EINVAL; + + case NXMB_EX_DEVICE_FAILURE: + return -EIO; + + case NXMB_EX_ACKNOWLEDGE: + return -EBUSY; + + case NXMB_EX_DEVICE_BUSY: + return -EBUSY; + + default: + return -EPROTO; + } +} + +/**************************************************************************** + * Name: nxmb_client_validate_response + ****************************************************************************/ + +static int nxmb_client_validate_response(nxmb_handle_t ctx, + uint8_t expected_uid, + uint8_t expected_fc) +{ + uint8_t rx_uid; + uint8_t rx_fc; + + if (ctx->adu.length < 2) + { + return -EPROTO; + } + + rx_uid = ctx->adu.unit_id; + rx_fc = ctx->adu.fc; + + if (rx_uid != expected_uid) + { + return -EPROTO; + } + + if (rx_fc == (expected_fc | 0x80)) + { + if (ctx->adu.length < 3) + { + return -EPROTO; + } + + return nxmb_exception_to_errno(ctx->adu.data[0]); + } + + if (rx_fc != expected_fc) + { + return -EPROTO; + } + + return OK; +} + +/**************************************************************************** + * Name: nxmb_client_tx_wait_rx + ****************************************************************************/ + +static int nxmb_client_tx_wait_rx(nxmb_handle_t ctx, uint8_t expected_uid, + uint8_t expected_fc) +{ + FAR struct nxmb_client_state_s *state; + uint64_t deadline; + int ret; + + if (ctx == NULL || ctx->client_state == NULL) + { + return -EINVAL; + } + + state = (FAR struct nxmb_client_state_s *)ctx->client_state; + + ret = ctx->transport_ops->send(ctx); + if (ret < 0) + { + return ret; + } + + /* Broadcast frames do not receive a response */ + + if (expected_uid == NXMB_ADDRESS_BROADCAST) + { + return OK; + } + + deadline = nxmb_util_clock_ms() + state->timeout_ms; + + for (; ; ) + { + if (nxmb_util_clock_ms() >= deadline) + { + return -ETIMEDOUT; + } + + ret = ctx->transport_ops->receive(ctx); + if (ret > 0) + { + return nxmb_client_validate_response(ctx, expected_uid, + expected_fc); + } + else if (ret < 0 && ret != -EAGAIN) + { + return ret; + } + } +} + +/**************************************************************************** + * Name: nxmb_client_read_bits + * + * Description: + * Common helper for FC01 (Read Coils) and FC02 (Read Discrete Inputs). + * + ****************************************************************************/ + +static int nxmb_client_read_bits(nxmb_handle_t ctx, uint8_t uid, uint8_t fc, + uint16_t addr, uint16_t count, + FAR uint8_t *buf) +{ + uint16_t nbytes; + int ret; + + nbytes = (count + 7) / 8; + + pthread_mutex_lock(&ctx->lock); + + ctx->adu.unit_id = uid; + ctx->adu.fc = fc; + nxmb_util_put_u16_be(&ctx->adu.data[0], addr); + nxmb_util_put_u16_be(&ctx->adu.data[2], count); + ctx->adu.length = 6; + + ret = nxmb_client_tx_wait_rx(ctx, uid, fc); + if (ret < 0) + { + pthread_mutex_unlock(&ctx->lock); + return ret; + } + + if (ctx->adu.length < (3 + nbytes) || ctx->adu.data[0] != nbytes) + { + pthread_mutex_unlock(&ctx->lock); + return -EPROTO; + } + + memcpy(buf, &ctx->adu.data[1], nbytes); + + pthread_mutex_unlock(&ctx->lock); + + return OK; +} + +/**************************************************************************** + * Name: nxmb_client_read_regs + * + * Description: + * Common helper for FC03 (Read Holding) and FC04 (Read Input) registers. + * + ****************************************************************************/ + +static int nxmb_client_read_regs(nxmb_handle_t ctx, uint8_t uid, uint8_t fc, + uint16_t addr, uint16_t count, + FAR uint16_t *buf) +{ + uint16_t nbytes; + int ret; + int i; + + nbytes = count * 2; + + pthread_mutex_lock(&ctx->lock); + + ctx->adu.unit_id = uid; + ctx->adu.fc = fc; + nxmb_util_put_u16_be(&ctx->adu.data[0], addr); + nxmb_util_put_u16_be(&ctx->adu.data[2], count); + ctx->adu.length = 6; + + ret = nxmb_client_tx_wait_rx(ctx, uid, fc); + if (ret < 0) + { + pthread_mutex_unlock(&ctx->lock); + return ret; + } + + if (ctx->adu.length < (3 + nbytes) || ctx->adu.data[0] != nbytes) + { + pthread_mutex_unlock(&ctx->lock); + return -EPROTO; + } + + for (i = 0; i < count; i++) + { + buf[i] = nxmb_util_get_u16_be(&ctx->adu.data[1 + i * 2]); + } + + pthread_mutex_unlock(&ctx->lock); + + return OK; +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_read_coils + ****************************************************************************/ + +int nxmb_read_coils(nxmb_handle_t ctx, uint8_t uid, uint16_t addr, + uint16_t count, FAR uint8_t *buf) +{ + DEBUGASSERT(ctx && buf); + + if (uid == NXMB_ADDRESS_BROADCAST || count == 0 || count > 2000) + { + return -EINVAL; + } + + if (!ctx->is_client) + { + return -ENOTSUP; + } + + return nxmb_client_read_bits(ctx, uid, NXMB_FC_READ_COILS, addr, + count, buf); +} + +/**************************************************************************** + * Name: nxmb_read_discrete + ****************************************************************************/ + +int nxmb_read_discrete(nxmb_handle_t ctx, uint8_t uid, uint16_t addr, + uint16_t count, FAR uint8_t *buf) +{ + DEBUGASSERT(ctx && buf); + + if (uid == NXMB_ADDRESS_BROADCAST || count == 0 || count > 2000) + { + return -EINVAL; + } + + if (!ctx->is_client) + { + return -ENOTSUP; + } + + return nxmb_client_read_bits(ctx, uid, NXMB_FC_READ_DISCRETE, addr, + count, buf); +} + +/**************************************************************************** + * Name: nxmb_read_input + ****************************************************************************/ + +int nxmb_read_input(nxmb_handle_t ctx, uint8_t uid, uint16_t addr, + uint16_t count, FAR uint16_t *buf) +{ + DEBUGASSERT(ctx && buf); + + if (uid == NXMB_ADDRESS_BROADCAST || count == 0 || count > 125) + { + return -EINVAL; + } + + if (!ctx->is_client) + { + return -ENOTSUP; + } + + return nxmb_client_read_regs(ctx, uid, NXMB_FC_READ_INPUT, addr, + count, buf); +} + +/**************************************************************************** + * Name: nxmb_read_holding + ****************************************************************************/ + +int nxmb_read_holding(nxmb_handle_t ctx, uint8_t uid, uint16_t addr, + uint16_t count, FAR uint16_t *buf) +{ + DEBUGASSERT(ctx && buf); + + if (uid == NXMB_ADDRESS_BROADCAST || count == 0 || count > 125) + { + return -EINVAL; + } + + if (!ctx->is_client) + { + return -ENOTSUP; + } + + return nxmb_client_read_regs(ctx, uid, NXMB_FC_READ_HOLDING, addr, + count, buf); +} + +/**************************************************************************** + * Name: nxmb_write_coil + ****************************************************************************/ + +int nxmb_write_coil(nxmb_handle_t ctx, uint8_t uid, uint16_t addr, + bool value) +{ + uint16_t coil_value; + int ret; + + DEBUGASSERT(ctx); + + if (!ctx->is_client) + { + return -ENOTSUP; + } + + coil_value = value ? 0xff00 : 0x0000; + + pthread_mutex_lock(&ctx->lock); + + ctx->adu.unit_id = uid; + ctx->adu.fc = NXMB_FC_WRITE_COIL; + nxmb_util_put_u16_be(&ctx->adu.data[0], addr); + nxmb_util_put_u16_be(&ctx->adu.data[2], coil_value); + ctx->adu.length = 6; + + ret = nxmb_client_tx_wait_rx(ctx, uid, NXMB_FC_WRITE_COIL); + if (ret < 0) + { + pthread_mutex_unlock(&ctx->lock); + return ret; + } + + if (uid != NXMB_ADDRESS_BROADCAST && ctx->adu.length < 6) + { + pthread_mutex_unlock(&ctx->lock); + return -EPROTO; + } + + pthread_mutex_unlock(&ctx->lock); + + return OK; +} + +/**************************************************************************** + * Name: nxmb_write_holding + ****************************************************************************/ + +int nxmb_write_holding(nxmb_handle_t ctx, uint8_t uid, uint16_t addr, + uint16_t value) +{ + int ret; + + DEBUGASSERT(ctx); + + if (!ctx->is_client) + { + return -ENOTSUP; + } + + pthread_mutex_lock(&ctx->lock); + + ctx->adu.unit_id = uid; + ctx->adu.fc = NXMB_FC_WRITE_HOLDING; + nxmb_util_put_u16_be(&ctx->adu.data[0], addr); + nxmb_util_put_u16_be(&ctx->adu.data[2], value); + ctx->adu.length = 6; + + ret = nxmb_client_tx_wait_rx(ctx, uid, NXMB_FC_WRITE_HOLDING); + if (ret < 0) + { + pthread_mutex_unlock(&ctx->lock); + return ret; + } + + if (uid != NXMB_ADDRESS_BROADCAST && ctx->adu.length < 6) + { + pthread_mutex_unlock(&ctx->lock); + return -EPROTO; + } + + pthread_mutex_unlock(&ctx->lock); + + return OK; +} + +/**************************************************************************** + * Name: nxmb_write_coils + ****************************************************************************/ + +int nxmb_write_coils(nxmb_handle_t ctx, uint8_t uid, uint16_t addr, + uint16_t count, FAR const uint8_t *buf) +{ + uint16_t nbytes; + int ret; + + DEBUGASSERT(ctx && buf); + + if (count == 0 || count > 1968) + { + return -EINVAL; + } + + if (!ctx->is_client) + { + return -ENOTSUP; + } + + nbytes = (count + 7) / 8; + + /* Request layout in adu.data[]: addr(2) + count(2) + bcnt(1) + nbytes */ + + if (5 + nbytes > NXMB_ADU_DATA_MAX) + { + return -EMSGSIZE; + } + + pthread_mutex_lock(&ctx->lock); + + ctx->adu.unit_id = uid; + ctx->adu.fc = NXMB_FC_WRITE_COILS; + nxmb_util_put_u16_be(&ctx->adu.data[0], addr); + nxmb_util_put_u16_be(&ctx->adu.data[2], count); + ctx->adu.data[4] = nbytes; + memcpy(&ctx->adu.data[5], buf, nbytes); + ctx->adu.length = 7 + nbytes; + + ret = nxmb_client_tx_wait_rx(ctx, uid, NXMB_FC_WRITE_COILS); + if (ret < 0) + { + pthread_mutex_unlock(&ctx->lock); + return ret; + } + + if (uid != NXMB_ADDRESS_BROADCAST && ctx->adu.length < 6) + { + pthread_mutex_unlock(&ctx->lock); + return -EPROTO; + } + + pthread_mutex_unlock(&ctx->lock); + + return OK; +} + +/**************************************************************************** + * Name: nxmb_write_holdings + ****************************************************************************/ + +int nxmb_write_holdings(nxmb_handle_t ctx, uint8_t uid, uint16_t addr, + uint16_t count, FAR const uint16_t *buf) +{ + uint16_t nbytes; + int ret; + int i; + + DEBUGASSERT(ctx && buf); + + if (count == 0 || count > 123) + { + return -EINVAL; + } + + if (!ctx->is_client) + { + return -ENOTSUP; + } + + nbytes = count * 2; + + /* Request layout in adu.data[]: addr(2) + count(2) + bcnt(1) + nbytes */ + + if (5 + nbytes > NXMB_ADU_DATA_MAX) + { + return -EMSGSIZE; + } + + pthread_mutex_lock(&ctx->lock); + + ctx->adu.unit_id = uid; + ctx->adu.fc = NXMB_FC_WRITE_HOLDINGS; + nxmb_util_put_u16_be(&ctx->adu.data[0], addr); + nxmb_util_put_u16_be(&ctx->adu.data[2], count); + ctx->adu.data[4] = nbytes; + + for (i = 0; i < count; i++) + { + nxmb_util_put_u16_be(&ctx->adu.data[5 + i * 2], buf[i]); + } + + ctx->adu.length = 7 + nbytes; + + ret = nxmb_client_tx_wait_rx(ctx, uid, NXMB_FC_WRITE_HOLDINGS); + if (ret < 0) + { + pthread_mutex_unlock(&ctx->lock); + return ret; + } + + if (uid != NXMB_ADDRESS_BROADCAST && ctx->adu.length < 6) + { + pthread_mutex_unlock(&ctx->lock); + return -EPROTO; + } + + pthread_mutex_unlock(&ctx->lock); + + return OK; +} + +/**************************************************************************** + * Name: nxmb_readwrite_holdings + ****************************************************************************/ + +int nxmb_readwrite_holdings(nxmb_handle_t ctx, uint8_t uid, uint16_t rd_addr, + uint16_t rd_count, FAR uint16_t *rd_buf, + uint16_t wr_addr, uint16_t wr_count, + FAR const uint16_t *wr_buf) +{ + uint16_t rd_nbytes; + uint16_t wr_nbytes; + int ret; + int i; + + DEBUGASSERT(ctx && rd_buf && wr_buf); + + if (uid == NXMB_ADDRESS_BROADCAST || rd_count == 0 || rd_count > 125 || + wr_count == 0 || wr_count > 121) + { + return -EINVAL; + } + + if (!ctx->is_client) + { + return -ENOTSUP; + } + + rd_nbytes = rd_count * 2; + wr_nbytes = wr_count * 2; + + /* Request layout: rd_addr(2) + rd_qty(2) + wr_addr(2) + wr_qty(2) + + * wr_bcnt(1) + wr_nbytes + */ + + if (9 + wr_nbytes > NXMB_ADU_DATA_MAX) + { + return -EMSGSIZE; + } + + pthread_mutex_lock(&ctx->lock); + + ctx->adu.unit_id = uid; + ctx->adu.fc = NXMB_FC_READWRITE_HOLDINGS; + nxmb_util_put_u16_be(&ctx->adu.data[0], rd_addr); + nxmb_util_put_u16_be(&ctx->adu.data[2], rd_count); + nxmb_util_put_u16_be(&ctx->adu.data[4], wr_addr); + nxmb_util_put_u16_be(&ctx->adu.data[6], wr_count); + ctx->adu.data[8] = (uint8_t)wr_nbytes; + + for (i = 0; i < wr_count; i++) + { + nxmb_util_put_u16_be(&ctx->adu.data[9 + i * 2], wr_buf[i]); + } + + ctx->adu.length = 11 + wr_nbytes; + + ret = nxmb_client_tx_wait_rx(ctx, uid, NXMB_FC_READWRITE_HOLDINGS); + if (ret < 0) + { + pthread_mutex_unlock(&ctx->lock); + return ret; + } + + if (ctx->adu.length < (3 + rd_nbytes)) + { + pthread_mutex_unlock(&ctx->lock); + return -EPROTO; + } + + if (ctx->adu.data[0] != rd_nbytes) + { + pthread_mutex_unlock(&ctx->lock); + return -EPROTO; + } + + for (i = 0; i < rd_count; i++) + { + rd_buf[i] = nxmb_util_get_u16_be(&ctx->adu.data[1 + i * 2]); + } + + pthread_mutex_unlock(&ctx->lock); + + return OK; +} + +/**************************************************************************** + * Name: nxmb_set_timeout + ****************************************************************************/ + +int nxmb_set_timeout(nxmb_handle_t ctx, uint32_t timeout_ms) +{ + FAR struct nxmb_client_state_s *state; + + DEBUGASSERT(ctx && ctx->client_state); + + if (!ctx->is_client) + { + return -ENOTSUP; + } + + state = (FAR struct nxmb_client_state_s *)ctx->client_state; + + pthread_mutex_lock(&ctx->lock); + state->timeout_ms = timeout_ms; + pthread_mutex_unlock(&ctx->lock); + + return OK; +} + +/**************************************************************************** + * Name: nxmb_client_init + ****************************************************************************/ + +int nxmb_client_init(nxmb_handle_t ctx) +{ + FAR struct nxmb_client_state_s *state; + + DEBUGASSERT(ctx && ctx->is_client); + + state = calloc(1, sizeof(struct nxmb_client_state_s)); + if (state == NULL) + { + return -ENOMEM; + } + + state->timeout_ms = CONFIG_NXMODBUS_CLIENT_TIMEOUT_MS; + + ctx->client_state = state; + + return OK; +} + +/**************************************************************************** + * Name: nxmb_client_deinit + ****************************************************************************/ + +int nxmb_client_deinit(nxmb_handle_t ctx) +{ + FAR struct nxmb_client_state_s *state; + + DEBUGASSERT(ctx && ctx->client_state); + + state = (FAR struct nxmb_client_state_s *)ctx->client_state; + + free(state); + + ctx->client_state = NULL; + + return OK; +} diff --git a/industry/nxmodbus/core/nxmb_context.c b/industry/nxmodbus/core/nxmb_context.c new file mode 100644 index 00000000000..e6a76c5813b --- /dev/null +++ b/industry/nxmodbus/core/nxmb_context.c @@ -0,0 +1,392 @@ +/**************************************************************************** + * apps/industry/nxmodbus/core/nxmb_context.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 + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include "nxmb_internal.h" + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static struct nxmb_context_s g_nxmb_pool[CONFIG_NXMODBUS_MAX_INSTANCES]; +static pthread_mutex_t g_pool_lock = PTHREAD_MUTEX_INITIALIZER; + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_create + * + * Description: + * Create a new NxModbus instance. + * + * Input Parameters: + * handle - Pointer to receive the instance handle + * config - Instance configuration + * + * Returned Value: + * 0 on success, negative errno on failure + * + ****************************************************************************/ + +int nxmb_create(FAR nxmb_handle_t *handle, + FAR const struct nxmb_config_s *config) +{ + nxmb_handle_t ctx = NULL; + int ret = -ENOMEM; + int i; + + DEBUGASSERT(handle && config); + +#ifndef CONFIG_NXMODBUS_CLIENT + if (config->is_client) + { + /* Master not supported */ + + return -EINVAL; + } +#endif + + pthread_mutex_lock(&g_pool_lock); + + for (i = 0; i < CONFIG_NXMODBUS_MAX_INSTANCES; i++) + { + if (g_nxmb_pool[i].mode == NXMB_MODE_INVAL) + { + ctx = &g_nxmb_pool[i]; + break; + } + } + + if (ctx != NULL) + { + memset(ctx, 0, sizeof(struct nxmb_context_s)); + + ret = pthread_mutex_init(&ctx->lock, NULL); + if (ret == 0) + { + ctx->mode = config->mode; + ctx->unit_id = config->unit_id; + ctx->is_client = config->is_client; + + switch (config->mode) + { +#ifdef CONFIG_NXMODBUS_RTU + case NXMB_MODE_RTU: + ctx->transport_cfg.serial = config->transport.serial; + ctx->transport_ops = &g_nxmb_serial_ops; + break; +#endif + +#ifdef CONFIG_NXMODBUS_ASCII + case NXMB_MODE_ASCII: + ctx->transport_cfg.serial = config->transport.serial; + ctx->transport_ops = &g_nxmb_ascii_ops; + break; +#endif + +#ifdef CONFIG_NXMODBUS_TCP + case NXMB_MODE_TCP: + ctx->transport_cfg.tcp = config->transport.tcp; + ctx->transport_ops = &g_nxmb_tcp_ops; + break; +#endif + +#ifdef CONFIG_NXMODBUS_RAW_ADU + case NXMB_MODE_RAW: + ctx->transport_cfg.raw = config->transport.raw; + ctx->transport_ops = &g_nxmb_raw_ops; + break; +#endif + + default: + pthread_mutex_destroy(&ctx->lock); + memset(ctx, 0, sizeof(struct nxmb_context_s)); + ret = -EINVAL; + goto out; + } + + *handle = ctx; + ret = 0; + } + else + { + memset(ctx, 0, sizeof(struct nxmb_context_s)); + ret = -ret; + } + } + +out: + pthread_mutex_unlock(&g_pool_lock); + return ret; +} + +/**************************************************************************** + * Name: nxmb_destroy + * + * Description: + * Destroy an NxModbus instance. + * + * Input Parameters: + * handle - Instance handle + * + * Returned Value: + * 0 on success, negative errno on failure + * + ****************************************************************************/ + +int nxmb_destroy(nxmb_handle_t ctx) +{ +#ifdef CONFIG_NXMODBUS_CUSTOM_FC + FAR struct nxmb_custom_fc_s *entry; + FAR struct nxmb_custom_fc_s *next; +#endif + + DEBUGASSERT(ctx); + + pthread_mutex_lock(&g_pool_lock); + pthread_mutex_lock(&ctx->lock); + + if (ctx->enabled) + { + pthread_mutex_unlock(&ctx->lock); + pthread_mutex_unlock(&g_pool_lock); + return -EBUSY; + } + + pthread_mutex_unlock(&ctx->lock); + pthread_mutex_destroy(&ctx->lock); + +#ifdef CONFIG_NXMODBUS_CUSTOM_FC + entry = ctx->custom_fc_list; + while (entry != NULL) + { + next = entry->next; + free(entry); + entry = next; + } +#endif + + memset(ctx, 0, sizeof(struct nxmb_context_s)); + pthread_mutex_unlock(&g_pool_lock); + + return 0; +} + +/**************************************************************************** + * Name: nxmb_set_callbacks + * + * Description: + * Set application callbacks for register access. + * + * Input Parameters: + * handle - Instance handle + * callbacks - Pointer to callback structure + * + * Returned Value: + * 0 on success, negative errno on failure + * + ****************************************************************************/ + +int nxmb_set_callbacks(nxmb_handle_t ctx, + FAR const struct nxmb_callbacks_s *callbacks) +{ + DEBUGASSERT(ctx); + + pthread_mutex_lock(&ctx->lock); + ctx->callbacks = callbacks; + pthread_mutex_unlock(&ctx->lock); + + return 0; +} + +/**************************************************************************** + * Name: nxmb_enable + * + * Description: + * Enable an NxModbus instance and initialize transport. + * + * Input Parameters: + * handle - Instance handle + * + * Returned Value: + * 0 on success, negative errno on failure + * + ****************************************************************************/ + +int nxmb_enable(nxmb_handle_t ctx) +{ + int ret; + + DEBUGASSERT(ctx && ctx->transport_ops && ctx->transport_ops->init); + + pthread_mutex_lock(&ctx->lock); + + if (ctx->enabled) + { + pthread_mutex_unlock(&ctx->lock); + return -EBUSY; + } + +#ifdef CONFIG_NXMODBUS_CLIENT + if (ctx->is_client) + { + ret = nxmb_client_init(ctx); + if (ret < 0) + { + pthread_mutex_unlock(&ctx->lock); + return ret; + } + } +#endif + + /* Initialize transport */ + + ret = ctx->transport_ops->init(ctx); + if (ret < 0) + { +#ifdef CONFIG_NXMODBUS_CLIENT + if (ctx->is_client) + { + nxmb_client_deinit(ctx); + } +#endif + + pthread_mutex_unlock(&ctx->lock); + return ret; + } + + ctx->enabled = true; + pthread_mutex_unlock(&ctx->lock); + + return 0; +} + +/**************************************************************************** + * Name: nxmb_set_server_id + * + * Description: + * Configure the data returned by FC17 (Report Server ID). + * + * Input Parameters: + * handle - Instance handle + * id - Server ID byte + * is_running - True if device is running + * additional - Optional additional data (may be NULL) + * addlen - Length of additional data + * + * Returned Value: + * 0 on success, negative errno on failure + * + ****************************************************************************/ + +int nxmb_set_server_id(nxmb_handle_t ctx, uint8_t id, bool is_running, + FAR const uint8_t *additional, uint16_t addlen) +{ + DEBUGASSERT(ctx); + + /* The first two bytes are server ID and run indicator. + * The rest is optional additional data. + */ + + if (addlen + 2 > CONFIG_NXMODBUS_REP_SERVER_ID_BUF) + { + return -ENOMEM; + } + + pthread_mutex_lock(&ctx->lock); + + ctx->server_id_len = 0; + ctx->server_id_buf[ctx->server_id_len++] = id; + ctx->server_id_buf[ctx->server_id_len++] = + (uint8_t)(is_running ? 0xff : 0x00); + + if (addlen > 0 && additional != NULL) + { + memcpy(&ctx->server_id_buf[ctx->server_id_len], additional, + (size_t)addlen); + ctx->server_id_len += addlen; + } + + pthread_mutex_unlock(&ctx->lock); + + return 0; +} + +/**************************************************************************** + * Name: nxmb_disable + * + * Description: + * Disable an NxModbus instance and deinitialize transport. + * + * Input Parameters: + * handle - Instance handle + * + * Returned Value: + * 0 on success, negative errno on failure + * + ****************************************************************************/ + +int nxmb_disable(nxmb_handle_t ctx) +{ + DEBUGASSERT(ctx && ctx->transport_ops && ctx->transport_ops->deinit); + + pthread_mutex_lock(&ctx->lock); + + if (!ctx->enabled) + { + pthread_mutex_unlock(&ctx->lock); + return 0; + } + + ctx->transport_ops->deinit(ctx); + +#ifdef CONFIG_NXMODBUS_CLIENT + if (ctx->is_client) + { + nxmb_client_deinit(ctx); + } +#endif + + ctx->enabled = false; + pthread_mutex_unlock(&ctx->lock); + + return 0; +} diff --git a/industry/nxmodbus/core/nxmb_func.c b/industry/nxmodbus/core/nxmb_func.c new file mode 100644 index 00000000000..13b595b4f92 --- /dev/null +++ b/industry/nxmodbus/core/nxmb_func.c @@ -0,0 +1,354 @@ +/**************************************************************************** + * apps/industry/nxmodbus/core/nxmb_func.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 + +#include + +#include +#include +#include +#include + +#include + +#include "nxmb_internal.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define NXMB_EXCEPTION_FLAG 0x80 + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +/* Function code handler signature */ + +typedef enum nxmb_exception_e (*nxmb_fc_handler_t)(nxmb_handle_t ctx); + +/* Dispatch table entry */ + +struct nxmb_fc_entry_s +{ + uint8_t fc; + nxmb_fc_handler_t handler; +}; + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +/* Standard function code dispatch table */ + +static const struct nxmb_fc_entry_s g_nxmb_fc_table[] = +{ +#ifdef CONFIG_NXMODBUS_FUNC_READ_COILS + { NXMB_FC_READ_COILS, nxmb_fc01_read_coils }, +#endif +#ifdef CONFIG_NXMODBUS_FUNC_READ_DISCRETE + { NXMB_FC_READ_DISCRETE, nxmb_fc02_read_discrete }, +#endif +#ifdef CONFIG_NXMODBUS_FUNC_READ_HOLDING + { NXMB_FC_READ_HOLDING, nxmb_fc03_read_holding }, +#endif +#ifdef CONFIG_NXMODBUS_FUNC_READ_INPUT + { NXMB_FC_READ_INPUT, nxmb_fc04_read_input }, +#endif +#ifdef CONFIG_NXMODBUS_FUNC_WRITE_COIL + { NXMB_FC_WRITE_COIL, nxmb_fc05_write_coil }, +#endif +#ifdef CONFIG_NXMODBUS_FUNC_WRITE_HOLDING + { NXMB_FC_WRITE_HOLDING, nxmb_fc06_write_holding }, +#endif +#ifdef CONFIG_NXMODBUS_FUNC_DIAGNOSTICS + { NXMB_FC_DIAGNOSTICS, nxmb_fc08_diagnostics }, +#endif +#ifdef CONFIG_NXMODBUS_FUNC_WRITE_COILS + { NXMB_FC_WRITE_COILS, nxmb_fc15_write_coils }, +#endif +#ifdef CONFIG_NXMODBUS_FUNC_WRITE_HOLDINGS + { NXMB_FC_WRITE_HOLDINGS, nxmb_fc16_write_holdings }, +#endif +#ifdef CONFIG_NXMODBUS_FUNC_REPORT_SERVER_ID + { NXMB_FC_REPORT_SERVER_ID, nxmb_fc17_report_server_id }, +#endif +#ifdef CONFIG_NXMODBUS_FUNC_READWRITE_HOLDINGS + { NXMB_FC_READWRITE_HOLDINGS, nxmb_fc23_readwrite_holding } +#endif +}; + +#define NXMB_FC_TABLE_SIZE \ + (sizeof(g_nxmb_fc_table) / sizeof(g_nxmb_fc_table[0])) + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_build_exception + * + * Description: + * Build a Modbus exception response in ctx->adu in place. + * + * Input Parameters: + * ctx - Instance context + * fc - Function code + * exception - NxModbus exception code + * + ****************************************************************************/ + +static void nxmb_build_exception(nxmb_handle_t ctx, uint8_t fc, + enum nxmb_exception_e exception) +{ + ctx->adu.fc = fc | NXMB_EXCEPTION_FLAG; + ctx->adu.data[0] = (uint8_t)exception; + + /* unit_id + fc + 1 exception byte */ + + ctx->adu.length = 3; +} + +/**************************************************************************** + * Name: nxmb_lookup_standard_fc + * + * Description: + * Look up a standard function code handler in the dispatch table. + * + * Input Parameters: + * fc - Function code to look up + * + * Returned Value: + * Handler function pointer, or NULL if not found + * + ****************************************************************************/ + +static nxmb_fc_handler_t nxmb_lookup_standard_fc(uint8_t fc) +{ + int i; + + for (i = 0; i < NXMB_FC_TABLE_SIZE; i++) + { + if (g_nxmb_fc_table[i].fc == fc) + { + return g_nxmb_fc_table[i].handler; + } + } + + return NULL; +} + +#ifdef CONFIG_NXMODBUS_CUSTOM_FC + +/**************************************************************************** + * Name: nxmb_lookup_custom_fc + * + * Description: + * Look up a custom function code handler in the instance list. + * + * Input Parameters: + * ctx - Instance context + * fc - Function code to look up + * + * Returned Value: + * Handler function pointer, or NULL if not found + * + ****************************************************************************/ + +static nxmb_custom_fc_handler_t nxmb_lookup_custom_fc( + nxmb_handle_t ctx, uint8_t fc) +{ + FAR struct nxmb_custom_fc_s *entry; + + for (entry = ctx->custom_fc_list; entry != NULL; entry = entry->next) + { + if (entry->fc == fc) + { + return entry->handler; + } + } + + return NULL; +} + +#endif /* CONFIG_NXMODBUS_CUSTOM_FC */ + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_dispatch_function + * + * Description: + * Dispatch a Modbus function code to the appropriate handler. Operates in + * place on ctx->adu. Checks standard handlers first, then custom handlers + * if enabled. Builds an exception response if no handler is found or the + * handler fails. + * + * Input Parameters: + * ctx - Instance context + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +int nxmb_dispatch_function(nxmb_handle_t ctx) +{ + nxmb_fc_handler_t handler; + enum nxmb_exception_e exception; + uint8_t fc; +#ifdef CONFIG_NXMODBUS_CUSTOM_FC + nxmb_custom_fc_handler_t custom_handler; + int ret; +#endif + + DEBUGASSERT(ctx); + + if (ctx->adu.length < 2) + { + return -EINVAL; + } + + fc = ctx->adu.fc; + + /* Look up standard handler first */ + + handler = nxmb_lookup_standard_fc(fc); + +#ifdef CONFIG_NXMODBUS_CUSTOM_FC + + /* If not found, check custom handlers */ + + if (handler == NULL) + { + custom_handler = nxmb_lookup_custom_fc(ctx, fc); + if (custom_handler != NULL) + { + ret = custom_handler(ctx); + if (ret < 0) + { + nxmb_build_exception(ctx, fc, NXMB_EX_DEVICE_FAILURE); + } + + return 0; + } + } +#endif + + /* If no handler found, return illegal function exception */ + + if (handler == NULL) + { + nxmb_build_exception(ctx, fc, NXMB_EX_ILLEGAL_FUNCTION); + return 0; + } + + /* Call the handler */ + + exception = handler(ctx); + + /* If handler returned exception, build exception response */ + + if (exception != NXMB_EX_NONE) + { + nxmb_build_exception(ctx, fc, exception); + } + + return 0; +} + +#ifdef CONFIG_NXMODBUS_CUSTOM_FC +/**************************************************************************** + * Name: nxmb_register_custom_fc + * + * Description: + * Register a custom Modbus function code handler for an instance. + * + * Custom handlers are checked after standard handlers, so standard + * function codes (FC01-FC16) cannot be overridden. + * + * Input Parameters: + * h - The NxModbus instance handle + * fc - The custom function code to register + * handler - The handler function + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +int nxmb_register_custom_fc(nxmb_handle_t ctx, uint8_t fc, + nxmb_custom_fc_handler_t handler) +{ + FAR struct nxmb_custom_fc_s *entry; + FAR struct nxmb_custom_fc_s *new_entry; + + DEBUGASSERT(ctx && handler); + + /* Check if FC is already a standard handler */ + + if (nxmb_lookup_standard_fc(fc) != NULL) + { + return -EEXIST; + } + + pthread_mutex_lock(&ctx->lock); + + /* Check if FC is already registered as custom */ + + for (entry = ctx->custom_fc_list; entry != NULL; entry = entry->next) + { + if (entry->fc == fc) + { + pthread_mutex_unlock(&ctx->lock); + return -EEXIST; + } + } + + /* Allocate new entry */ + + new_entry = malloc(sizeof(struct nxmb_custom_fc_s)); + if (new_entry == NULL) + { + pthread_mutex_unlock(&ctx->lock); + return -ENOMEM; + } + + /* Initialize and prepend to list */ + + new_entry->fc = fc; + new_entry->handler = handler; + new_entry->next = ctx->custom_fc_list; + ctx->custom_fc_list = new_entry; + + pthread_mutex_unlock(&ctx->lock); + return 0; +} + +#endif /* CONFIG_NXMODBUS_CUSTOM_FC */ diff --git a/industry/nxmodbus/core/nxmb_func_coils.c b/industry/nxmodbus/core/nxmb_func_coils.c new file mode 100644 index 00000000000..3235fcc9ea6 --- /dev/null +++ b/industry/nxmodbus/core/nxmb_func_coils.c @@ -0,0 +1,259 @@ +/**************************************************************************** + * apps/industry/nxmodbus/core/nxmb_func_coils.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 + +#include + +#include +#include + +#include + +#include "nxmb_internal.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/* Offsets are in adu.data[]; lengths refer to data only. */ + +/* FC01 Read Coils */ + +#define NXMB_FC01_ADDR_OFF 0 +#define NXMB_FC01_QTY_OFF 2 +#define NXMB_FC01_REQ_DATA_LEN 4 + +/* FC05 Write Single Coil */ + +#define NXMB_FC05_ADDR_OFF 0 +#define NXMB_FC05_VALUE_OFF 2 +#define NXMB_FC05_REQ_DATA_LEN 4 + +/* FC15 Write Multiple Coils */ + +#define NXMB_FC15_ADDR_OFF 0 +#define NXMB_FC15_QTY_OFF 2 +#define NXMB_FC15_BCNT_OFF 4 +#define NXMB_FC15_DATA_OFF 5 +#define NXMB_FC15_RESP_DATA_LEN 4 + +/* Maximum coil quantity per Modbus specification. + * FC01/FC02 read max is 2000 (0x07D0). + * FC15 write max is 1968 (0x07B0). + */ + +#define NXMB_COIL_READ_QTY_MAX 2000 +#define NXMB_COIL_WRITE_QTY_MAX 1968 + +/* Coil ON/OFF wire values for FC05 */ + +#define NXMB_COIL_ON 0xff00 +#define NXMB_COIL_OFF 0x0000 + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_fc01_read_coils + ****************************************************************************/ + +enum nxmb_exception_e nxmb_fc01_read_coils(nxmb_handle_t ctx) +{ + uint16_t data_len = ctx->adu.length - 2; + uint16_t addr; + uint16_t qty; + uint8_t nbytes; + int ret; + + if (data_len != NXMB_FC01_REQ_DATA_LEN) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + if (ctx->callbacks == NULL || ctx->callbacks->coil_cb == NULL) + { + return NXMB_EX_ILLEGAL_FUNCTION; + } + + addr = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC01_ADDR_OFF]); + qty = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC01_QTY_OFF]); + + if (qty < 1 || qty > NXMB_COIL_READ_QTY_MAX) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + if ((uint32_t)addr + qty > 0xffffu) + { + return NXMB_EX_ILLEGAL_DATA_ADDRESS; + } + + nbytes = (uint8_t)((qty + 7) / 8); + + /* Response is byte_count + coil data */ + + if (1 + nbytes > NXMB_ADU_DATA_MAX) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + /* Build response: byte_count + coil data */ + + ctx->adu.fc = NXMB_FC_READ_COILS; + ctx->adu.data[0] = nbytes; + + /* Zero data area so unused trailing bits are clear */ + + memset(&ctx->adu.data[1], 0, nbytes); + + ret = ctx->callbacks->coil_cb(&ctx->adu.data[1], addr, qty, NXMB_REG_READ, + ctx->callbacks->priv); + if (ret != 0) + { + return nxmb_cb_ret_to_exception(ret); + } + + ctx->adu.length = (uint16_t)(2 + 1 + nbytes); + return NXMB_EX_NONE; +} + +/**************************************************************************** + * Name: nxmb_fc05_write_coil + ****************************************************************************/ + +enum nxmb_exception_e nxmb_fc05_write_coil(nxmb_handle_t ctx) +{ + uint16_t data_len = ctx->adu.length - 2; + uint16_t addr; + uint16_t value; + uint8_t coil_buf[2]; + int ret; + + if (data_len != NXMB_FC05_REQ_DATA_LEN) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + if (ctx->callbacks == NULL || ctx->callbacks->coil_cb == NULL) + { + return NXMB_EX_ILLEGAL_FUNCTION; + } + + addr = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC05_ADDR_OFF]); + value = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC05_VALUE_OFF]); + + if (addr == 0xffffu) + { + return NXMB_EX_ILLEGAL_DATA_ADDRESS; + } + + if (value != NXMB_COIL_ON && value != NXMB_COIL_OFF) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + /* Convert wire value to single-bit buffer for callback */ + + coil_buf[0] = (value == NXMB_COIL_ON) ? 1 : 0; + coil_buf[1] = 0; + + ret = ctx->callbacks->coil_cb(coil_buf, addr, 1, NXMB_REG_WRITE, + ctx->callbacks->priv); + if (ret != 0) + { + return nxmb_cb_ret_to_exception(ret); + } + + /* Response echoes the request — adu.length is already correct */ + + return NXMB_EX_NONE; +} + +/**************************************************************************** + * Name: nxmb_fc15_write_coils + ****************************************************************************/ + +enum nxmb_exception_e nxmb_fc15_write_coils(nxmb_handle_t ctx) +{ + uint16_t data_len = ctx->adu.length - 2; + uint16_t addr; + uint16_t qty; + uint8_t byte_count; + uint8_t byte_count_exp; + int ret; + + if (data_len < NXMB_FC15_DATA_OFF) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + if (ctx->callbacks == NULL || ctx->callbacks->coil_cb == NULL) + { + return NXMB_EX_ILLEGAL_FUNCTION; + } + + addr = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC15_ADDR_OFF]); + qty = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC15_QTY_OFF]); + byte_count = ctx->adu.data[NXMB_FC15_BCNT_OFF]; + + if (qty < 1 || qty > NXMB_COIL_WRITE_QTY_MAX) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + if ((uint32_t)addr + qty > 0xffffu) + { + return NXMB_EX_ILLEGAL_DATA_ADDRESS; + } + + byte_count_exp = (uint8_t)((qty + 7) / 8); + + if (byte_count != byte_count_exp) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + /* Verify total data length matches header + payload */ + + if (data_len != (uint16_t)(NXMB_FC15_DATA_OFF + byte_count)) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + ret = ctx->callbacks->coil_cb(&ctx->adu.data[NXMB_FC15_DATA_OFF], addr, + qty, NXMB_REG_WRITE, ctx->callbacks->priv); + if (ret != 0) + { + return nxmb_cb_ret_to_exception(ret); + } + + /* Response: addr(2) + qty(2) — already in data[0..3] from request */ + + ctx->adu.length = (uint16_t)(2 + NXMB_FC15_RESP_DATA_LEN); + return NXMB_EX_NONE; +} diff --git a/industry/nxmodbus/core/nxmb_func_diag.c b/industry/nxmodbus/core/nxmb_func_diag.c new file mode 100644 index 00000000000..8575c610d18 --- /dev/null +++ b/industry/nxmodbus/core/nxmb_func_diag.c @@ -0,0 +1,83 @@ +/**************************************************************************** + * apps/industry/nxmodbus/core/nxmb_func_diag.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 + +#include + +#include + +#include + +#include "nxmb_internal.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define NXMB_DIAG_SUBFUNC_OFF 0 +#define NXMB_DIAG_DATA_OFF 2 +#define NXMB_DIAG_REQ_MIN_DATA_LEN 4 + +/* Sub-function codes */ + +#define NXMB_DIAG_RETURN_QUERY 0x0000 + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_fc08_diagnostics + ****************************************************************************/ + +enum nxmb_exception_e nxmb_fc08_diagnostics(nxmb_handle_t ctx) +{ + uint16_t data_len = ctx->adu.length - 2; + uint16_t subfunc; + + if (data_len < NXMB_DIAG_REQ_MIN_DATA_LEN) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + subfunc = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_DIAG_SUBFUNC_OFF]); + + switch (subfunc) + { + case NXMB_DIAG_RETURN_QUERY: + { + /* Response echoes the request — adu is already correct */ + + return NXMB_EX_NONE; + } + + default: + { + return NXMB_EX_ILLEGAL_FUNCTION; + } + } +} diff --git a/industry/nxmodbus/core/nxmb_func_discrete.c b/industry/nxmodbus/core/nxmb_func_discrete.c new file mode 100644 index 00000000000..b30a0a7205b --- /dev/null +++ b/industry/nxmodbus/core/nxmb_func_discrete.c @@ -0,0 +1,110 @@ +/**************************************************************************** + * apps/industry/nxmodbus/core/nxmb_func_discrete.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 + +#include + +#include +#include + +#include + +#include "nxmb_internal.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define NXMB_FC02_ADDR_OFF 0 +#define NXMB_FC02_QTY_OFF 2 +#define NXMB_FC02_REQ_DATA_LEN 4 + +#define NXMB_DISCRETE_QTY_MAX 2000 + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_fc02_read_discrete + ****************************************************************************/ + +enum nxmb_exception_e nxmb_fc02_read_discrete(nxmb_handle_t ctx) +{ + uint16_t data_len = ctx->adu.length - 2; + uint16_t addr; + uint16_t qty; + uint8_t nbytes; + int ret; + + if (data_len != NXMB_FC02_REQ_DATA_LEN) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + if (ctx->callbacks == NULL || ctx->callbacks->discrete_cb == NULL) + { + return NXMB_EX_ILLEGAL_FUNCTION; + } + + addr = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC02_ADDR_OFF]); + qty = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC02_QTY_OFF]); + + if (qty < 1 || qty > NXMB_DISCRETE_QTY_MAX) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + if ((uint32_t)addr + qty > 0xffffu) + { + return NXMB_EX_ILLEGAL_DATA_ADDRESS; + } + + nbytes = (uint8_t)((qty + 7) / 8); + + /* Response is byte_count + discrete data */ + + if (1 + nbytes > NXMB_ADU_DATA_MAX) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + ctx->adu.fc = NXMB_FC_READ_DISCRETE; + ctx->adu.data[0] = nbytes; + + memset(&ctx->adu.data[1], 0, nbytes); + + ret = ctx->callbacks->discrete_cb(&ctx->adu.data[1], addr, qty, + ctx->callbacks->priv); + if (ret != 0) + { + return nxmb_cb_ret_to_exception(ret); + } + + ctx->adu.length = (uint16_t)(2 + 1 + nbytes); + return NXMB_EX_NONE; +} diff --git a/industry/nxmodbus/core/nxmb_func_holding.c b/industry/nxmodbus/core/nxmb_func_holding.c new file mode 100644 index 00000000000..a4fe74cc587 --- /dev/null +++ b/industry/nxmodbus/core/nxmb_func_holding.c @@ -0,0 +1,331 @@ +/**************************************************************************** + * apps/industry/nxmodbus/core/nxmb_func_holding.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 + +#include + +#include + +#include + +#include "nxmb_internal.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/* PDU data offsets are relative to ctx->adu.data[] (after unit_id + fc). + * Lengths refer to the data payload only — adu.length = 2 + data_len. + */ + +/* FC03 Read Holding Registers */ + +#define NXMB_FC03_ADDR_OFF 0 +#define NXMB_FC03_QTY_OFF 2 +#define NXMB_FC03_REQ_DATA_LEN 4 + +/* FC06 Write Single Holding Register */ + +#define NXMB_FC06_ADDR_OFF 0 +#define NXMB_FC06_VALUE_OFF 2 +#define NXMB_FC06_REQ_DATA_LEN 4 + +/* FC16 Write Multiple Holding Registers */ + +#define NXMB_FC16_ADDR_OFF 0 +#define NXMB_FC16_QTY_OFF 2 +#define NXMB_FC16_BCNT_OFF 4 +#define NXMB_FC16_DATA_OFF 5 +#define NXMB_FC16_RESP_DATA_LEN 4 + +/* FC23 Read/Write Multiple Holding Registers */ + +#define NXMB_FC23_RD_ADDR_OFF 0 +#define NXMB_FC23_RD_QTY_OFF 2 +#define NXMB_FC23_WR_ADDR_OFF 4 +#define NXMB_FC23_WR_QTY_OFF 6 +#define NXMB_FC23_WR_BCNT_OFF 8 +#define NXMB_FC23_WR_DATA_OFF 9 +#define NXMB_FC23_REQ_MIN_DATA_LEN 9 + +/* Quantity limits per Modbus Application Protocol Specification */ + +#define NXMB_REG_READ_QTY_MAX 125 /* 0x007D */ +#define NXMB_REG_WRITE_MUL_QTY_MAX 123 /* 0x007B */ +#define NXMB_REG_READWRITE_RD_QTY_MAX 125 /* 0x007D */ +#define NXMB_REG_READWRITE_WR_QTY_MAX 121 /* 0x0079 */ + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_fc03_read_holding + ****************************************************************************/ + +enum nxmb_exception_e nxmb_fc03_read_holding(nxmb_handle_t ctx) +{ + uint16_t data_len = ctx->adu.length - 2; + uint16_t addr; + uint16_t qty; + uint8_t nbytes; + int ret; + + if (data_len != NXMB_FC03_REQ_DATA_LEN) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + if (ctx->callbacks == NULL || ctx->callbacks->holding_cb == NULL) + { + return NXMB_EX_ILLEGAL_FUNCTION; + } + + addr = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC03_ADDR_OFF]); + qty = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC03_QTY_OFF]); + + if (qty < 1 || qty > NXMB_REG_READ_QTY_MAX) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + if ((uint32_t)addr + qty > 0xffffu) + { + return NXMB_EX_ILLEGAL_DATA_ADDRESS; + } + + nbytes = (uint8_t)(qty * 2); + + /* Response is byte_count + register data */ + + if (1 + nbytes > NXMB_ADU_DATA_MAX) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + /* Build response: byte_count + register data (big-endian) */ + + ctx->adu.fc = NXMB_FC_READ_HOLDING; + ctx->adu.data[0] = nbytes; + + ret = ctx->callbacks->holding_cb(&ctx->adu.data[1], addr, qty, + NXMB_REG_READ, ctx->callbacks->priv); + if (ret != 0) + { + return nxmb_cb_ret_to_exception(ret); + } + + ctx->adu.length = (uint16_t)(2 + 1 + nbytes); + return NXMB_EX_NONE; +} + +/**************************************************************************** + * Name: nxmb_fc06_write_holding + ****************************************************************************/ + +enum nxmb_exception_e nxmb_fc06_write_holding(nxmb_handle_t ctx) +{ + uint16_t data_len = ctx->adu.length - 2; + uint16_t addr; + int ret; + + if (data_len != NXMB_FC06_REQ_DATA_LEN) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + if (ctx->callbacks == NULL || ctx->callbacks->holding_cb == NULL) + { + return NXMB_EX_ILLEGAL_FUNCTION; + } + + addr = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC06_ADDR_OFF]); + + if (addr == 0xffffu) + { + return NXMB_EX_ILLEGAL_DATA_ADDRESS; + } + + ret = ctx->callbacks->holding_cb(&ctx->adu.data[NXMB_FC06_VALUE_OFF], addr, + 1, NXMB_REG_WRITE, ctx->callbacks->priv); + if (ret != 0) + { + return nxmb_cb_ret_to_exception(ret); + } + + /* Response echoes the request — adu.length is already correct */ + + return NXMB_EX_NONE; +} + +/**************************************************************************** + * Name: nxmb_fc16_write_holdings + ****************************************************************************/ + +enum nxmb_exception_e nxmb_fc16_write_holdings(nxmb_handle_t ctx) +{ + uint16_t data_len = ctx->adu.length - 2; + uint16_t addr; + uint16_t qty; + uint8_t byte_count; + int ret; + + if (data_len < NXMB_FC16_DATA_OFF) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + if (ctx->callbacks == NULL || ctx->callbacks->holding_cb == NULL) + { + return NXMB_EX_ILLEGAL_FUNCTION; + } + + addr = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC16_ADDR_OFF]); + qty = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC16_QTY_OFF]); + byte_count = ctx->adu.data[NXMB_FC16_BCNT_OFF]; + + if (qty < 1 || qty > NXMB_REG_WRITE_MUL_QTY_MAX) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + if ((uint32_t)addr + qty > 0xffffu) + { + return NXMB_EX_ILLEGAL_DATA_ADDRESS; + } + + if (byte_count != (uint8_t)(qty * 2)) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + /* Verify total data length matches header + payload */ + + if (data_len != (uint16_t)(NXMB_FC16_DATA_OFF + byte_count)) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + ret = ctx->callbacks->holding_cb(&ctx->adu.data[NXMB_FC16_DATA_OFF], addr, + qty, NXMB_REG_WRITE, + ctx->callbacks->priv); + if (ret != 0) + { + return nxmb_cb_ret_to_exception(ret); + } + + /* Response: addr(2) + qty(2) — already in data[0..3] from request */ + + ctx->adu.length = (uint16_t)(2 + NXMB_FC16_RESP_DATA_LEN); + return NXMB_EX_NONE; +} + +/**************************************************************************** + * Name: nxmb_fc23_readwrite_holding + ****************************************************************************/ + +enum nxmb_exception_e nxmb_fc23_readwrite_holding(nxmb_handle_t ctx) +{ + uint16_t data_len = ctx->adu.length - 2; + uint16_t rd_addr; + uint16_t rd_qty; + uint16_t wr_addr; + uint16_t wr_qty; + uint8_t wr_bcnt; + uint16_t rd_bcnt; + int ret; + + if (data_len < NXMB_FC23_REQ_MIN_DATA_LEN) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + if (ctx->callbacks == NULL || ctx->callbacks->holding_cb == NULL) + { + return NXMB_EX_ILLEGAL_FUNCTION; + } + + rd_addr = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC23_RD_ADDR_OFF]); + rd_qty = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC23_RD_QTY_OFF]); + wr_addr = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC23_WR_ADDR_OFF]); + wr_qty = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC23_WR_QTY_OFF]); + wr_bcnt = ctx->adu.data[NXMB_FC23_WR_BCNT_OFF]; + + if (rd_qty < 1 || rd_qty > NXMB_REG_READWRITE_RD_QTY_MAX || wr_qty < 1 || + wr_qty > NXMB_REG_READWRITE_WR_QTY_MAX || wr_bcnt != (wr_qty * 2)) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + if ((uint32_t)rd_addr + rd_qty > 0xffffu) + { + return NXMB_EX_ILLEGAL_DATA_ADDRESS; + } + + if ((uint32_t)wr_addr + wr_qty > 0xffffu) + { + return NXMB_EX_ILLEGAL_DATA_ADDRESS; + } + + /* Verify total request data length matches header + write payload */ + + if (data_len != (uint16_t)(NXMB_FC23_WR_DATA_OFF + wr_bcnt)) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + /* Response is byte_count + read register data — must fit in adu.data[] */ + + if (1 + rd_qty * 2 > NXMB_ADU_DATA_MAX) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + ret = ctx->callbacks->holding_cb(&ctx->adu.data[NXMB_FC23_WR_DATA_OFF], + wr_addr, wr_qty, NXMB_REG_WRITE, + ctx->callbacks->priv); + if (ret != 0) + { + return nxmb_cb_ret_to_exception(ret); + } + + /* Build response: byte_count + read register data, overwriting request */ + + ctx->adu.fc = NXMB_FC_READWRITE_HOLDINGS; + rd_bcnt = rd_qty * 2; + ctx->adu.data[0] = (uint8_t)rd_bcnt; + + ret = ctx->callbacks->holding_cb(&ctx->adu.data[1], rd_addr, rd_qty, + NXMB_REG_READ, ctx->callbacks->priv); + if (ret != 0) + { + return nxmb_cb_ret_to_exception(ret); + } + + ctx->adu.length = (uint16_t)(2 + 1 + rd_bcnt); + return NXMB_EX_NONE; +} diff --git a/industry/nxmodbus/core/nxmb_func_input.c b/industry/nxmodbus/core/nxmb_func_input.c new file mode 100644 index 00000000000..151d2510b47 --- /dev/null +++ b/industry/nxmodbus/core/nxmb_func_input.c @@ -0,0 +1,115 @@ +/**************************************************************************** + * apps/industry/nxmodbus/core/nxmb_func_input.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 + +#include + +#include + +#include + +#include "nxmb_internal.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/* FC04 Read Input Registers — offsets in adu.data[], lengths are data + * only + */ + +#define NXMB_FC04_ADDR_OFF 0 +#define NXMB_FC04_QTY_OFF 2 +#define NXMB_FC04_REQ_DATA_LEN 4 + +/* Maximum register quantity per Modbus specification */ + +#define NXMB_REG_READ_QTY_MAX 125 /* 0x007D */ + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_fc04_read_input + ****************************************************************************/ + +enum nxmb_exception_e nxmb_fc04_read_input(nxmb_handle_t ctx) +{ + uint16_t data_len = ctx->adu.length - 2; + uint16_t addr; + uint16_t qty; + uint8_t nbytes; + int ret; + + if (data_len != NXMB_FC04_REQ_DATA_LEN) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + if (ctx->callbacks == NULL || ctx->callbacks->input_cb == NULL) + { + return NXMB_EX_ILLEGAL_FUNCTION; + } + + addr = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC04_ADDR_OFF]); + qty = nxmb_util_get_u16_be(&ctx->adu.data[NXMB_FC04_QTY_OFF]); + + if (qty < 1 || qty > NXMB_REG_READ_QTY_MAX) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + if ((uint32_t)addr + qty > 0xffffu) + { + return NXMB_EX_ILLEGAL_DATA_ADDRESS; + } + + nbytes = (uint8_t)(qty * 2); + + /* Response is byte_count + register data */ + + if (1 + nbytes > NXMB_ADU_DATA_MAX) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + /* Build response: byte_count + register data (big-endian) */ + + ctx->adu.fc = NXMB_FC_READ_INPUT; + ctx->adu.data[0] = nbytes; + + ret = ctx->callbacks->input_cb(&ctx->adu.data[1], addr, qty, + ctx->callbacks->priv); + if (ret != 0) + { + return nxmb_cb_ret_to_exception(ret); + } + + ctx->adu.length = (uint16_t)(2 + 1 + nbytes); + return NXMB_EX_NONE; +} diff --git a/industry/nxmodbus/core/nxmb_func_other.c b/industry/nxmodbus/core/nxmb_func_other.c new file mode 100644 index 00000000000..5827026dfb8 --- /dev/null +++ b/industry/nxmodbus/core/nxmb_func_other.c @@ -0,0 +1,99 @@ +/**************************************************************************** + * apps/industry/nxmodbus/core/nxmb_func_other.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 + +#include + +#include +#include + +#include + +#include "nxmb_internal.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define NXMB_REPORT_ID_REQ_DATA_LEN 0 + +/* Run indicator status */ + +#define NXMB_RUN_STATUS_ON 0xff +#define NXMB_RUN_STATUS_OFF 0x00 + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_fc17_report_server_id + * + * Description: + * Handle FC17 (0x11) Report Server ID request. Returns the configured + * unit ID and a run indicator byte (0xFF = ON). + * + ****************************************************************************/ + +enum nxmb_exception_e nxmb_fc17_report_server_id(nxmb_handle_t ctx) +{ + uint16_t data_len = ctx->adu.length - 2; + + if (data_len != NXMB_REPORT_ID_REQ_DATA_LEN) + { + return NXMB_EX_ILLEGAL_DATA_VALUE; + } + + /* If the application configured server ID data via nxmb_set_server_id(), + * use that; otherwise fall back to unit_id + run status ON. + */ + + ctx->adu.fc = NXMB_FC_REPORT_SERVER_ID; + + if (ctx->server_id_len > 0) + { + /* Response is byte_count + server_id_buf */ + + if (1 + ctx->server_id_len > NXMB_ADU_DATA_MAX) + { + return NXMB_EX_DEVICE_FAILURE; + } + + ctx->adu.data[0] = (uint8_t)ctx->server_id_len; + memcpy(&ctx->adu.data[1], ctx->server_id_buf, ctx->server_id_len); + ctx->adu.length = (uint16_t)(2 + 1 + ctx->server_id_len); + } + else + { + ctx->adu.data[0] = 2; /* byte count */ + ctx->adu.data[1] = ctx->unit_id; /* server ID */ + ctx->adu.data[2] = NXMB_RUN_STATUS_ON; /* run indicator */ + ctx->adu.length = (uint16_t)(2 + 3); + } + + return NXMB_EX_NONE; +} diff --git a/industry/nxmodbus/core/nxmb_server.c b/industry/nxmodbus/core/nxmb_server.c new file mode 100644 index 00000000000..4168b53f24e --- /dev/null +++ b/industry/nxmodbus/core/nxmb_server.c @@ -0,0 +1,121 @@ +/**************************************************************************** + * apps/industry/nxmodbus/core/nxmb_server.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 + +#include + +#include +#include +#include +#include + +#include + +#include "nxmb_internal.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define NXMB_FUNC_ERROR 0x80 + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_poll + * + * Description: + * Poll for Modbus events and process received frames. + * + * Input Parameters: + * handle - Instance handle + * + * Returned Value: + * 0 on success, negative errno on failure + * + ****************************************************************************/ + +int nxmb_poll(nxmb_handle_t ctx) +{ + uint8_t rcv_address; + int ret; + + DEBUGASSERT(ctx && ctx->transport_ops && ctx->transport_ops->receive); + + pthread_mutex_lock(&ctx->lock); + + if (!ctx->enabled) + { + pthread_mutex_unlock(&ctx->lock); + return -EAGAIN; + } + + ret = ctx->transport_ops->receive(ctx); + if (ret < 0) + { + pthread_mutex_unlock(&ctx->lock); + return ret; + } + + if (ret == 0 || ctx->adu.length == 0) + { + pthread_mutex_unlock(&ctx->lock); + return 0; + } + + rcv_address = ctx->adu.unit_id; + + if (rcv_address != NXMB_ADDRESS_BROADCAST && rcv_address != ctx->unit_id) + { + pthread_mutex_unlock(&ctx->lock); + return 0; + } + + /* Dispatch to function code handler. Handlers mutate ctx->adu in place, + * turning the request into the response. + */ + + ret = nxmb_dispatch_function(ctx); + + if (rcv_address != NXMB_ADDRESS_BROADCAST) + { + if (ctx->transport_ops->send != NULL) + { + ret = ctx->transport_ops->send(ctx); + if (ret < 0) + { + pthread_mutex_unlock(&ctx->lock); + return ret; + } + } + } + + pthread_mutex_unlock(&ctx->lock); + return 0; +} diff --git a/industry/nxmodbus/core/nxmb_utils.c b/industry/nxmodbus/core/nxmb_utils.c new file mode 100644 index 00000000000..016d370c56d --- /dev/null +++ b/industry/nxmodbus/core/nxmb_utils.c @@ -0,0 +1,127 @@ +/**************************************************************************** + * apps/industry/nxmodbus/core/nxmb_utils.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 + +#include + +#include + +#include "nxmb_internal.h" + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_util_set_bits + * + * Description: + * Set bits in a byte buffer. + * + * Input Parameters: + * buf - Byte buffer + * bit_offset - Bit offset from start of buffer + * nbits - Number of bits to set (max 8) + * value - Value to set + * + * Returned Value: + * None + * + ****************************************************************************/ + +void nxmb_util_set_bits(FAR uint8_t *buf, uint16_t bit_offset, uint8_t nbits, + uint8_t value) +{ + uint16_t word_buf; + uint16_t mask; + uint16_t byte_offset; + uint16_t pre_bits; + uint16_t val = value; + + byte_offset = bit_offset / 8; + pre_bits = bit_offset - (byte_offset * 8); + + val <<= pre_bits; + mask = (uint16_t)((1 << nbits) - 1); + mask <<= pre_bits; + + word_buf = buf[byte_offset]; + if (pre_bits + nbits > 8) + { + word_buf |= buf[byte_offset + 1] << 8; + } + + word_buf = (word_buf & (~mask)) | val; + + buf[byte_offset] = (uint8_t)(word_buf & 0xff); + if (pre_bits + nbits > 8) + { + buf[byte_offset + 1] = (uint8_t)(word_buf >> 8); + } +} + +/**************************************************************************** + * Name: nxmb_util_get_bits + * + * Description: + * Get bits from a byte buffer. + * + * Input Parameters: + * buf - Byte buffer + * bit_offset - Bit offset from start of buffer + * nbits - Number of bits to get (max 8) + * + * Returned Value: + * Bit value + * + ****************************************************************************/ + +uint8_t nxmb_util_get_bits(FAR const uint8_t *buf, uint16_t bit_offset, + uint8_t nbits) +{ + uint16_t word_buf; + uint16_t mask; + uint16_t byte_offset; + uint16_t pre_bits; + + byte_offset = bit_offset / 8; + pre_bits = bit_offset - (byte_offset * 8); + + mask = (uint16_t)((1 << nbits) - 1); + + word_buf = buf[byte_offset]; + if (pre_bits + nbits > 8) + { + word_buf |= buf[byte_offset + 1] << 8; + } + + word_buf >>= pre_bits; + word_buf &= mask; + + return (uint8_t)word_buf; +} + diff --git a/industry/nxmodbus/nxmb_internal.h b/industry/nxmodbus/nxmb_internal.h new file mode 100644 index 00000000000..0ec3db5c994 --- /dev/null +++ b/industry/nxmodbus/nxmb_internal.h @@ -0,0 +1,379 @@ +/**************************************************************************** + * apps/industry/nxmodbus/nxmb_internal.h + * + * 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. + * + ****************************************************************************/ + +#ifndef __APPS_INDUSTRY_NXMODBUS_NXMB_INTERNAL_H +#define __APPS_INDUSTRY_NXMODBUS_NXMB_INTERNAL_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include +#include + +#include +#include +#include +#include +#include + +#include + +/**************************************************************************** + * Public Types + ****************************************************************************/ + +/* Pre-processor Definitions */ + +#define NXMB_ADDRESS_BROADCAST 0 + +/* Modbus function codes */ + +#define NXMB_FC_READ_COILS 0x01 +#define NXMB_FC_READ_DISCRETE 0x02 +#define NXMB_FC_READ_HOLDING 0x03 +#define NXMB_FC_READ_INPUT 0x04 +#define NXMB_FC_WRITE_COIL 0x05 +#define NXMB_FC_WRITE_HOLDING 0x06 +#define NXMB_FC_DIAGNOSTICS 0x08 +#define NXMB_FC_WRITE_COILS 0x0f +#define NXMB_FC_WRITE_HOLDINGS 0x10 +#define NXMB_FC_REPORT_SERVER_ID 0x11 +#define NXMB_FC_READWRITE_HOLDINGS 0x17 + +/* Transport operations interface. + * + * receive() must not block indefinitely. Implementations should use an + * internal timeout (e.g. select()) and return 0 or -EAGAIN when no + * complete frame is available yet. The client polling loop relies on + * this to enforce its own response timeout. + */ + +struct nxmb_transport_ops_s +{ + CODE int (*init)(nxmb_handle_t ctx); + CODE int (*deinit)(nxmb_handle_t ctx); + CODE int (*send)(nxmb_handle_t ctx); + CODE int (*receive)(nxmb_handle_t ctx); +}; + +/* Custom function code handler */ + +struct nxmb_custom_fc_s +{ + FAR struct nxmb_custom_fc_s *next; + nxmb_custom_fc_handler_t handler; + uint8_t fc; +}; + +/* NxModbus instance context */ + +struct nxmb_context_s +{ + FAR const struct nxmb_callbacks_s *callbacks; + FAR const struct nxmb_transport_ops_s *transport_ops; + enum nxmb_mode_e mode; + pthread_mutex_t lock; + uint8_t unit_id; + bool is_client; + bool enabled; + + /* Transport configuration */ + + union nxmb_transport_config_u transport_cfg; + + /* Single ADU container reused for both RX and TX. Modbus is half-duplex, + * so request and response never coexist: handlers mutate the request + * in place to form the response. + */ + + struct nxmb_adu_s adu; + + /* Report Server ID (FC17) data */ + + uint8_t server_id_buf[CONFIG_NXMODBUS_REP_SERVER_ID_BUF]; + uint16_t server_id_len; + + /* Custom handlers */ + + FAR struct nxmb_custom_fc_s *custom_fc_list; + FAR void *transport_state; + FAR void *client_state; +}; + +/**************************************************************************** + * Public Data + ****************************************************************************/ + +#ifdef CONFIG_NXMODBUS_RTU +extern const struct nxmb_transport_ops_s g_nxmb_serial_ops; +#endif + +#ifdef CONFIG_NXMODBUS_ASCII +extern const struct nxmb_transport_ops_s g_nxmb_ascii_ops; +#endif + +#ifdef CONFIG_NXMODBUS_RAW_ADU +extern const struct nxmb_transport_ops_s g_nxmb_raw_ops; +#endif + +#ifdef CONFIG_NXMODBUS_TCP +extern const struct nxmb_transport_ops_s g_nxmb_tcp_ops; +#endif + +/**************************************************************************** + * Inline Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_cb_ret_to_exception + * + * Description: + * Map a callback return value to a Modbus exception code. + * + * -ENOENT -> address not supported by the application + * anything else -> generic device failure + * + ****************************************************************************/ + +static inline enum nxmb_exception_e nxmb_cb_ret_to_exception(int ret) +{ + if (ret == -ENOENT) + { + return NXMB_EX_ILLEGAL_DATA_ADDRESS; + } + + return NXMB_EX_DEVICE_FAILURE; +} + +/**************************************************************************** + * Name: nxmb_util_get_u16_be + * + * Description: + * Extract a big-endian 16-bit value from a buffer. + * + ****************************************************************************/ + +static inline uint16_t nxmb_util_get_u16_be(FAR const uint8_t *buf) +{ + return (uint16_t)((buf[0] << 8) | buf[1]); +} + +/**************************************************************************** + * Name: nxmb_util_put_u16_be + * + * Description: + * Store a 16-bit value as big-endian in a buffer. + * + ****************************************************************************/ + +static inline void nxmb_util_put_u16_be(FAR uint8_t *buf, uint16_t value) +{ + buf[0] = (uint8_t)(value >> 8); + buf[1] = (uint8_t)(value & 0xff); +} + +/**************************************************************************** + * Name: nxmb_util_clock_ms + * + * Description: + * Return the current monotonic clock value in milliseconds. + * + ****************************************************************************/ + +static inline uint64_t nxmb_util_clock_ms(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +/**************************************************************************** + * Public Function Prototypes + ****************************************************************************/ + +#ifdef CONFIG_SERIAL_TERMIOS +/**************************************************************************** + * Name: nxmb_serial_configure + * + * Description: + * Configure serial port with termios settings for Modbus communication. + * Shared by RTU and ASCII transports. + * + * Input Parameters: + * fd - File descriptor + * baudrate - Baud rate + * parity - Parity setting + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +int nxmb_serial_configure(int fd, uint32_t baudrate, + enum nxmb_parity_e parity); +#endif + +/**************************************************************************** + * Name: nxmb_util_set_bits + * + * Description: + * Set up to 8 bits in a byte buffer at the given bit offset. + * + * Input Parameters: + * buf - Byte buffer + * bit_offset - Bit offset from start of buffer + * nbits - Number of bits to set (max 8) + * value - Bit values to write + * + ****************************************************************************/ + +void nxmb_util_set_bits(FAR uint8_t *buf, uint16_t bit_offset, uint8_t nbits, + uint8_t value); + +/**************************************************************************** + * Name: nxmb_util_get_bits + * + * Description: + * Extract up to 8 bits from a byte buffer at the given bit offset. + * + * Input Parameters: + * buf - Byte buffer + * bit_offset - Bit offset from start of buffer + * nbits - Number of bits to extract (max 8) + * + * Returned Value: + * Extracted bit values + * + ****************************************************************************/ + +uint8_t nxmb_util_get_bits(FAR const uint8_t *buf, uint16_t bit_offset, + uint8_t nbits); + +#ifdef CONFIG_NXMODBUS_RTU +/**************************************************************************** + * Name: nxmb_crc16 + * + * Description: + * Calculate Modbus RTU CRC16 checksum. + * + * Input Parameters: + * buf - Pointer to data buffer + * len - Length of data in bytes + * + * Returned Value: + * CRC16 value + * + ****************************************************************************/ + +uint16_t nxmb_crc16(FAR const uint8_t *buf, uint16_t len); +#endif + +#ifdef CONFIG_NXMODBUS_ASCII +/**************************************************************************** + * Name: nxmb_lrc + * + * Description: + * Calculate Modbus ASCII LRC checksum. + * + * Input Parameters: + * buf - Pointer to data buffer + * len - Length of data in bytes + * + * Returned Value: + * LRC value (8-bit) + * + ****************************************************************************/ + +uint8_t nxmb_lrc(FAR const uint8_t *buf, uint16_t len); +#endif + +#ifdef CONFIG_NXMODBUS_CLIENT +/**************************************************************************** + * Name: nxmb_client_init + * + * Description: + * Initialize client state for master mode. + * + * Input Parameters: + * ctx - Instance context + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +int nxmb_client_init(nxmb_handle_t ctx); + +/**************************************************************************** + * Name: nxmb_client_deinit + * + * Description: + * Deinitialize client state. + * + * Input Parameters: + * ctx - Instance context + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +int nxmb_client_deinit(nxmb_handle_t ctx); +#endif + +/**************************************************************************** + * Name: nxmb_dispatch_function + * + * Description: + * Dispatch a Modbus function code to the appropriate handler. Operates + * in place on ctx->adu. + * + * Input Parameters: + * ctx - Instance context + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +int nxmb_dispatch_function(nxmb_handle_t ctx); + +/* Function-code handlers operate on ctx->adu in place: they read the + * request from adu.fc/adu.data[]/adu.length and overwrite those fields + * with the response. + */ + +enum nxmb_exception_e nxmb_fc01_read_coils(nxmb_handle_t ctx); +enum nxmb_exception_e nxmb_fc02_read_discrete(nxmb_handle_t ctx); +enum nxmb_exception_e nxmb_fc05_write_coil(nxmb_handle_t ctx); +enum nxmb_exception_e nxmb_fc15_write_coils(nxmb_handle_t ctx); +enum nxmb_exception_e nxmb_fc03_read_holding(nxmb_handle_t ctx); +enum nxmb_exception_e nxmb_fc04_read_input(nxmb_handle_t ctx); +enum nxmb_exception_e nxmb_fc06_write_holding(nxmb_handle_t ctx); +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); + +#endif /* __APPS_INDUSTRY_NXMODBUS_NXMB_INTERNAL_H */ diff --git a/industry/nxmodbus/transport/nxmb_ascii.c b/industry/nxmodbus/transport/nxmb_ascii.c new file mode 100644 index 00000000000..668a8982158 --- /dev/null +++ b/industry/nxmodbus/transport/nxmb_ascii.c @@ -0,0 +1,550 @@ +/**************************************************************************** + * apps/industry/nxmodbus/transport/nxmb_ascii.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 + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef CONFIG_SERIAL_TERMIOS +# include +#endif + +#include + +#include "nxmb_internal.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define NXMB_ASCII_START_CHAR ':' +#define NXMB_ASCII_CR '\r' +#define NXMB_ASCII_LF '\n' + +/* Serialized binary frame: unit_id(1) + fc(1) + data + LRC(1) */ + +#define NXMB_ASCII_BIN_MAX (NXMB_ADU_DATA_MAX + 3) + +/* Hex-encoded ASCII frame: ':' + 2*bin + CR + LF */ + +#define NXMB_ASCII_BUFLEN (NXMB_ASCII_BIN_MAX * 2 + 4) + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +struct nxmb_ascii_state_s +{ + int fd; + uint8_t rx_buf[NXMB_ASCII_BUFLEN]; + uint16_t rx_pos; + bool in_frame; +#ifdef CONFIG_SERIAL_TERMIOS + struct termios old_tio; +#endif +}; + +/**************************************************************************** + * Private Function Prototypes + ****************************************************************************/ + +static int nxmb_ascii_char_to_hex(char c); +static char nxmb_ascii_hex_to_char(uint8_t nibble); +static uint16_t nxmb_ascii_encode(FAR const uint8_t *bin, uint16_t bin_len, + FAR char *ascii); +static int nxmb_ascii_decode(FAR const char *ascii, uint16_t ascii_len, + FAR uint8_t *bin); +static int nxmb_ascii_init(nxmb_handle_t ctx); +static int nxmb_ascii_deinit(nxmb_handle_t ctx); +static int nxmb_ascii_send(nxmb_handle_t ctx); +static int nxmb_ascii_receive(nxmb_handle_t ctx); + +/**************************************************************************** + * Public Data + ****************************************************************************/ + +const struct nxmb_transport_ops_s g_nxmb_ascii_ops = +{ + .init = nxmb_ascii_init, + .deinit = nxmb_ascii_deinit, + .send = nxmb_ascii_send, + .receive = nxmb_ascii_receive +}; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_ascii_char_to_hex + * + * Description: + * Convert ASCII hex character to binary nibble. + * + * Input Parameters: + * c - ASCII hex character + * + * Returned Value: + * Binary value 0-15, or -1 on error + * + ****************************************************************************/ + +static int nxmb_ascii_char_to_hex(char c) +{ + if (c >= '0' && c <= '9') + { + return c - '0'; + } + else if (c >= 'A' && c <= 'F') + { + return c - 'A' + 10; + } + else if (c >= 'a' && c <= 'f') + { + return c - 'a' + 10; + } + + return -1; +} + +/**************************************************************************** + * Name: nxmb_ascii_hex_to_char + * + * Description: + * Convert binary nibble to ASCII hex character. + * + * Input Parameters: + * nibble - Binary value 0-15 + * + * Returned Value: + * ASCII hex character + * + ****************************************************************************/ + +static char nxmb_ascii_hex_to_char(uint8_t nibble) +{ + if (nibble < 10) + { + return '0' + nibble; + } + else + { + return 'A' + (nibble - 10); + } +} + +/**************************************************************************** + * Name: nxmb_ascii_encode + * + * Description: + * Encode binary data to ASCII hex string. + * + * Input Parameters: + * bin - Binary data + * bin_len - Binary data length + * ascii - Output ASCII buffer + * + * Returned Value: + * ASCII string length + * + ****************************************************************************/ + +static uint16_t nxmb_ascii_encode(FAR const uint8_t *bin, uint16_t bin_len, + FAR char *ascii) +{ + uint16_t pos = 0; + uint16_t i; + + for (i = 0; i < bin_len; i++) + { + ascii[pos++] = nxmb_ascii_hex_to_char(bin[i] >> 4); + ascii[pos++] = nxmb_ascii_hex_to_char(bin[i] & 0x0f); + } + + return pos; +} + +/**************************************************************************** + * Name: nxmb_ascii_decode + * + * Description: + * Decode ASCII hex string to binary data. + * + * Input Parameters: + * ascii - ASCII hex string + * ascii_len - ASCII string length (must be even) + * bin - Output binary buffer + * + * Returned Value: + * Binary data length, or negative errno on error + * + ****************************************************************************/ + +static int nxmb_ascii_decode(FAR const char *ascii, uint16_t ascii_len, + FAR uint8_t *bin) +{ + uint16_t i; + int high; + int low; + + if (ascii_len % 2 != 0) + { + return -EINVAL; + } + + for (i = 0; i < ascii_len; i += 2) + { + high = nxmb_ascii_char_to_hex(ascii[i]); + low = nxmb_ascii_char_to_hex(ascii[i + 1]); + + if (high < 0 || low < 0) + { + return -EINVAL; + } + + bin[i / 2] = (high << 4) | low; + } + + return ascii_len / 2; +} + +/**************************************************************************** + * Name: nxmb_ascii_init + * + * Description: + * Initialize serial ASCII transport. + * + * Input Parameters: + * ctx - Instance context + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +static int nxmb_ascii_init(nxmb_handle_t ctx) +{ + FAR struct nxmb_ascii_state_s *state; + FAR const struct nxmb_serial_config_s *cfg; + int ret; + + DEBUGASSERT(ctx); + + cfg = &ctx->transport_cfg.serial; + + if (cfg->devpath == NULL) + { + return -EINVAL; + } + + state = calloc(1, sizeof(struct nxmb_ascii_state_s)); + if (state == NULL) + { + return -ENOMEM; + } + + state->fd = open(cfg->devpath, O_RDWR | O_NOCTTY); + if (state->fd < 0) + { + ret = -errno; + free(state); + return ret; + } + +#ifdef CONFIG_SERIAL_TERMIOS + if (tcgetattr(state->fd, &state->old_tio) < 0) + { + ret = -errno; + close(state->fd); + free(state); + return ret; + } + + ret = nxmb_serial_configure(state->fd, cfg->baudrate, cfg->parity); + if (ret < 0) + { + close(state->fd); + free(state); + return ret; + } +#endif + + state->rx_pos = 0; + state->in_frame = false; + + ctx->transport_state = state; + + return 0; +} + +/**************************************************************************** + * Name: nxmb_ascii_deinit + * + * Description: + * Deinitialize serial ASCII transport. + * + * Input Parameters: + * ctx - Instance context + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +static int nxmb_ascii_deinit(nxmb_handle_t ctx) +{ + FAR struct nxmb_ascii_state_s *state; + + DEBUGASSERT(ctx && ctx->transport_state); + + state = ctx->transport_state; + +#ifdef CONFIG_SERIAL_TERMIOS + tcsetattr(state->fd, TCSANOW, &state->old_tio); +#endif + close(state->fd); + free(state); + + ctx->transport_state = NULL; + + return 0; +} + +/**************************************************************************** + * Name: nxmb_ascii_send + * + * Description: + * Send ASCII frame over serial port. + * + * Input Parameters: + * ctx - Instance context + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +static int nxmb_ascii_send(nxmb_handle_t ctx) +{ + FAR struct nxmb_ascii_state_s *state; + uint8_t bin[NXMB_ASCII_BIN_MAX]; + char tx_buf[NXMB_ASCII_BUFLEN]; + uint16_t data_len; + uint16_t pos = 0; + uint8_t lrc; + ssize_t written; + + DEBUGASSERT(ctx && ctx->transport_state); + + state = ctx->transport_state; + + if (ctx->adu.length < 2) + { + return -EINVAL; + } + + data_len = ctx->adu.length - 2; + + /* Linearize the ADU into the wire byte order: unit_id, fc, data... */ + + bin[0] = ctx->adu.unit_id; + bin[1] = ctx->adu.fc; + memcpy(&bin[2], ctx->adu.data, data_len); + + lrc = nxmb_lrc(bin, ctx->adu.length); + ctx->adu.crc = lrc; + + tx_buf[pos++] = NXMB_ASCII_START_CHAR; + pos += nxmb_ascii_encode(bin, ctx->adu.length, &tx_buf[pos]); + tx_buf[pos++] = nxmb_ascii_hex_to_char(lrc >> 4); + tx_buf[pos++] = nxmb_ascii_hex_to_char(lrc & 0x0f); + tx_buf[pos++] = NXMB_ASCII_CR; + tx_buf[pos++] = NXMB_ASCII_LF; + + written = write(state->fd, tx_buf, pos); + + if (written < 0) + { + return -errno; + } + + if (written != pos) + { + return -EIO; + } + + return 0; +} + +/**************************************************************************** + * Name: nxmb_ascii_receive + * + * Description: + * Receive ASCII frame from serial port. + * + * Input Parameters: + * ctx - Instance context + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +static int nxmb_ascii_receive(nxmb_handle_t ctx) +{ + FAR struct nxmb_ascii_state_s *state; + uint8_t bin[NXMB_ASCII_BIN_MAX]; + struct timeval timeout; + fd_set readfds; + ssize_t nread; + uint8_t c; + uint8_t lrc_calc; + uint8_t lrc_recv; + int ret; + int bin_len; + + DEBUGASSERT(ctx && ctx->transport_state); + + state = ctx->transport_state; + + FD_ZERO(&readfds); + FD_SET(state->fd, &readfds); + + timeout.tv_sec = CONFIG_NXMODBUS_ASCII_TIMEOUT_SEC; + timeout.tv_usec = 0; + + ret = select(state->fd + 1, &readfds, NULL, NULL, &timeout); + + if (ret < 0) + { + return -errno; + } + + if (ret == 0) + { + return 0; + } + + if (!FD_ISSET(state->fd, &readfds)) + { + return 0; + } + + nread = read(state->fd, &c, 1); + if (nread < 0) + { + return -errno; + } + + if (nread == 0) + { + return 0; + } + + if (c == NXMB_ASCII_START_CHAR) + { + state->rx_pos = 0; + state->in_frame = true; + return 0; + } + + if (!state->in_frame) + { + return 0; + } + + if (c == NXMB_ASCII_LF) + { + state->in_frame = false; + + /* Minimum valid frame: addr(2) + func(2) + lrc(2) + CR = 7 chars. + * Maximum valid frame: NXMB_ASCII_BIN_MAX hex pairs + CR. Reject + * anything larger so the decoded length fits bin[]. + */ + + if (state->rx_pos < 7 || + state->rx_pos > NXMB_ASCII_BIN_MAX * 2 + 1 || + state->rx_buf[state->rx_pos - 1] != NXMB_ASCII_CR) + { + state->rx_pos = 0; + return 0; + } + + /* Decode all hex chars (exclude CR) into a temp binary buffer. + * Layout: unit_id + fc + data... + LRC. + */ + + bin_len = nxmb_ascii_decode((FAR const char *)state->rx_buf, + state->rx_pos - 1, bin); + + if (bin_len < 3) + { + state->rx_pos = 0; + return 0; + } + + lrc_calc = nxmb_lrc(bin, bin_len - 1); + lrc_recv = bin[bin_len - 1]; + + if (lrc_calc != lrc_recv) + { + state->rx_pos = 0; + return 0; + } + + /* Unpack into ctx->adu. Length excludes LRC. */ + + ctx->adu.unit_id = bin[0]; + ctx->adu.fc = bin[1]; + memcpy(ctx->adu.data, &bin[2], (bin_len - 3)); + ctx->adu.crc = lrc_recv; + ctx->adu.length = (bin_len - 1); + + state->rx_pos = 0; + + return 1; + } + + if (state->rx_pos < sizeof(state->rx_buf)) + { + state->rx_buf[state->rx_pos++] = c; + } + else + { + state->in_frame = false; + state->rx_pos = 0; + } + + return 0; +} diff --git a/industry/nxmodbus/transport/nxmb_crc.c b/industry/nxmodbus/transport/nxmb_crc.c new file mode 100644 index 00000000000..abfffffb4b1 --- /dev/null +++ b/industry/nxmodbus/transport/nxmb_crc.c @@ -0,0 +1,108 @@ +/**************************************************************************** + * apps/industry/nxmodbus/transport/nxmb_crc.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 + +#include + +#include + +#include "nxmb_internal.h" + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static const uint16_t g_crc16_table[256] = +{ + 0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, + 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01, 0x0cc0, + 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81, + 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0, 0x1980, 0xd941, + 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, + 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, + 0x1680, 0xd641, 0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, + 0x1040, 0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240, + 0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441, 0x3c00, + 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41, 0xfa01, 0x3ac0, + 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840, 0x2800, 0xe8c1, 0xe981, + 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41, 0xee01, 0x2ec0, 0x2f80, 0xef41, + 0x2d00, 0xedc1, 0xec81, 0x2c40, 0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, + 0xe7c1, 0xe681, 0x2640, 0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, + 0x2080, 0xe041, 0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, + 0x6240, 0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441, + 0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41, 0xaa01, + 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840, 0x7800, 0xb8c1, + 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41, 0xbe01, 0x7ec0, 0x7f80, + 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40, 0xb401, 0x74c0, 0x7580, 0xb541, + 0x7700, 0xb7c1, 0xb681, 0x7640, 0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, + 0x71c0, 0x7080, 0xb041, 0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, + 0x5280, 0x9241, 0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, + 0x5440, 0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40, + 0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841, 0x8801, + 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1, + 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581, + 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341, + 0x4100, 0x81c1, 0x8081, 0x4040 +}; + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_crc16 + * + * Description: + * Calculate Modbus RTU CRC16 checksum. + * + * Input Parameters: + * buf - Pointer to data buffer + * len - Length of data in bytes + * + * Returned Value: + * CRC16 value + * + ****************************************************************************/ + +uint16_t nxmb_crc16(FAR const uint8_t *buf, uint16_t len) +{ + uint16_t crc = 0xffff; + uint8_t index; + + if (buf == NULL || len == 0) + { + return crc; + } + + while (len--) + { + index = (crc ^ *buf++); + crc = (crc >> 8) ^ g_crc16_table[index]; + } + + return crc; +} diff --git a/industry/nxmodbus/transport/nxmb_lrc.c b/industry/nxmodbus/transport/nxmb_lrc.c new file mode 100644 index 00000000000..7768c9cb536 --- /dev/null +++ b/industry/nxmodbus/transport/nxmb_lrc.c @@ -0,0 +1,70 @@ +/**************************************************************************** + * apps/industry/nxmodbus/transport/nxmb_lrc.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 + +#include + +#include + +#include "nxmb_internal.h" + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_lrc + * + * Description: + * Calculate Modbus ASCII LRC checksum. + * + * Input Parameters: + * buf - Pointer to data buffer + * len - Length of data in bytes + * + * Returned Value: + * LRC value (8-bit) + * + ****************************************************************************/ + +uint8_t nxmb_lrc(FAR const uint8_t *buf, uint16_t len) +{ + uint8_t lrc = 0; + uint16_t i; + + if (buf == NULL || len == 0) + { + return lrc; + } + + for (i = 0; i < len; i++) + { + lrc += buf[i]; + } + + return (uint8_t)(-((int8_t)lrc)); +} diff --git a/industry/nxmodbus/transport/nxmb_raw.c b/industry/nxmodbus/transport/nxmb_raw.c new file mode 100644 index 00000000000..8352581fef7 --- /dev/null +++ b/industry/nxmodbus/transport/nxmb_raw.c @@ -0,0 +1,303 @@ +/**************************************************************************** + * apps/industry/nxmodbus/transport/nxmb_raw.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 + +#include +#include +#include +#include +#include + +#include +#include + +#include "nxmb_internal.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define MBAP_PROTO_ID 0x0000 + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +/* Raw ADU transport state */ + +struct nxmb_raw_state_s +{ + FAR void *user_data; + nxmb_raw_tx_cb_t tx_callback; +}; + +/**************************************************************************** + * Private Function Prototypes + ****************************************************************************/ + +static int nxmb_raw_init(nxmb_handle_t ctx); +static int nxmb_raw_deinit(nxmb_handle_t ctx); +static int nxmb_raw_send(nxmb_handle_t ctx); +static int nxmb_raw_receive(nxmb_handle_t ctx); + +/**************************************************************************** + * Public Data + ****************************************************************************/ + +const struct nxmb_transport_ops_s g_nxmb_raw_ops = +{ + .init = nxmb_raw_init, + .deinit = nxmb_raw_deinit, + .send = nxmb_raw_send, + .receive = nxmb_raw_receive, +}; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_raw_init + * + * Description: + * Initialize raw ADU transport. + * + * Input Parameters: + * ctx - Instance context + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +static int nxmb_raw_init(nxmb_handle_t ctx) +{ + FAR const struct nxmb_raw_config_s *cfg; + FAR struct nxmb_raw_state_s *state; + + DEBUGASSERT(ctx); + + cfg = &ctx->transport_cfg.raw; + + if (cfg->tx_cb == NULL) + { + return -EINVAL; + } + + state = malloc(sizeof(struct nxmb_raw_state_s)); + if (state == NULL) + { + return -ENOMEM; + } + + state->tx_callback = cfg->tx_cb; + state->user_data = cfg->user_data; + + ctx->transport_state = state; + + return OK; +} + +/**************************************************************************** + * Name: nxmb_raw_deinit + * + * Description: + * Deinitialize raw ADU transport. + * + * Input Parameters: + * ctx - Instance context + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +static int nxmb_raw_deinit(nxmb_handle_t ctx) +{ + DEBUGASSERT(ctx && ctx->transport_state); + + free(ctx->transport_state); + ctx->transport_state = NULL; + + return OK; +} + +/**************************************************************************** + * Name: nxmb_raw_send + * + * Description: + * Send raw ADU via user callback. + * + * Input Parameters: + * ctx - Instance context + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +static int nxmb_raw_send(nxmb_handle_t ctx) +{ + FAR struct nxmb_raw_state_s *state; + int ret; + + DEBUGASSERT(ctx && ctx->transport_state); + + state = ctx->transport_state; + + if (state->tx_callback == NULL) + { + return -EINVAL; + } + + ctx->adu.proto_id = MBAP_PROTO_ID; + + ret = state->tx_callback(&ctx->adu, state->user_data); + if (ret < 0) + { + return ret; + } + + return OK; +} + +/**************************************************************************** + * Name: nxmb_raw_receive + * + * Description: + * Receive raw ADU (not used in raw mode, frames are submitted via + * nxmb_raw_submit_rx). + * + * Input Parameters: + * ctx - Instance context + * + * Returned Value: + * -ENOTSUP (raw mode uses submit pattern, not polling) + * + ****************************************************************************/ + +static int nxmb_raw_receive(nxmb_handle_t ctx) +{ + return -ENOTSUP; +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_raw_submit_rx + * + * Description: + * Submit a received raw ADU into the NxModbus protocol core. + * + * Input Parameters: + * h - The NxModbus instance receiving the frame + * adu - The received raw ADU container + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +int nxmb_raw_submit_rx(nxmb_handle_t ctx, FAR const struct nxmb_adu_s *adu) +{ + DEBUGASSERT(ctx && adu); + + if (adu->proto_id != MBAP_PROTO_ID) + { + return -EPROTO; + } + + if (adu->length < 2) + { + return -EINVAL; + } + + if ((adu->length - 2) > NXMB_ADU_DATA_MAX) + { + return -EMSGSIZE; + } + + pthread_mutex_lock(&ctx->lock); + ctx->adu = *adu; + pthread_mutex_unlock(&ctx->lock); + + return OK; +} + +/**************************************************************************** + * Name: nxmb_raw_put_header + * + * Description: + * Serialize the 8-byte MBAP+FC header for an ADU. + * + * Input Parameters: + * adu - The ADU providing the header fields + * hdr - The destination buffer for the serialized header + * + * Returned Value: + * None + * + ****************************************************************************/ + +void nxmb_raw_put_header(FAR const struct nxmb_adu_s *adu, FAR uint8_t *hdr) +{ + DEBUGASSERT(adu && hdr); + + nxmb_util_put_u16_be(&hdr[0], adu->trans_id); + nxmb_util_put_u16_be(&hdr[2], adu->proto_id); + nxmb_util_put_u16_be(&hdr[4], adu->length); + hdr[6] = adu->unit_id; + hdr[7] = adu->fc; +} + +/**************************************************************************** + * Name: nxmb_raw_get_header + * + * Description: + * Parse the 8-byte MBAP+FC header into an ADU structure. + * + * Input Parameters: + * adu - The destination ADU structure + * hdr - The source buffer containing the serialized header + * + * Returned Value: + * None + * + ****************************************************************************/ + +void nxmb_raw_get_header(FAR struct nxmb_adu_s *adu, FAR const uint8_t *hdr) +{ + DEBUGASSERT(adu && hdr); + + adu->trans_id = nxmb_util_get_u16_be(&hdr[0]); + adu->proto_id = nxmb_util_get_u16_be(&hdr[2]); + adu->length = nxmb_util_get_u16_be(&hdr[4]); + adu->unit_id = hdr[6]; + adu->fc = hdr[7]; +} diff --git a/industry/nxmodbus/transport/nxmb_serial.c b/industry/nxmodbus/transport/nxmb_serial.c new file mode 100644 index 00000000000..679e0d59752 --- /dev/null +++ b/industry/nxmodbus/transport/nxmb_serial.c @@ -0,0 +1,423 @@ +/**************************************************************************** + * apps/industry/nxmodbus/transport/nxmb_serial.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 + +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef CONFIG_SERIAL_TERMIOS +# include +#endif + +#include + +#include "nxmb_internal.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define NXMB_RTU_MIN_FRAME_SIZE 5 + +/* Wire frame: unit_id + fc + data + 2 CRC bytes */ + +#define NXMB_RTU_FRAME_MAX (NXMB_ADU_DATA_MAX + 4) + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +struct nxmb_serial_state_s +{ + int fd; + uint32_t t35_usec; + bool rx_frame_now; + uint16_t rx_pos; + uint8_t rx_buf[NXMB_RTU_FRAME_MAX]; +#ifdef CONFIG_SERIAL_TERMIOS + struct termios old_tio; +#endif +}; + +/**************************************************************************** + * Private Function Prototypes + ****************************************************************************/ + +static uint32_t nxmb_serial_calculate_t35(uint32_t baudrate); +static int nxmb_serial_init(nxmb_handle_t ctx); +static int nxmb_serial_deinit(nxmb_handle_t ctx); +static int nxmb_serial_send(nxmb_handle_t ctx); +static int nxmb_serial_receive(nxmb_handle_t ctx); + +/**************************************************************************** + * Public Data + ****************************************************************************/ + +const struct nxmb_transport_ops_s g_nxmb_serial_ops = +{ + .init = nxmb_serial_init, + .deinit = nxmb_serial_deinit, + .send = nxmb_serial_send, + .receive = nxmb_serial_receive +}; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_serial_calculate_t35 + * + * Description: + * Calculate T3.5 character timeout in microseconds based on baud rate. + * + * Input Parameters: + * baudrate - Serial baud rate + * + * Returned Value: + * T3.5 timeout in microseconds + * + ****************************************************************************/ + +static uint32_t nxmb_serial_calculate_t35(uint32_t baudrate) +{ + if (baudrate <= 19200) + { + /* T3.5 = 3.5 * 11 bits/char * 1e6 us/s / baudrate + * = 38500000 / baudrate (in microseconds). + * Use integer arithmetic to avoid floating-point, which may + * require FPU context save/restore on embedded targets. + */ + + return 38500000u / baudrate; + } + else + { + /* Per Modbus spec, fixed 1750 us for baud rates above 19200 */ + + return 1750; + } +} + +/**************************************************************************** + * Name: nxmb_serial_init + * + * Description: + * Initialize serial RTU transport. + * + * Input Parameters: + * ctx - Instance context + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +static int nxmb_serial_init(nxmb_handle_t ctx) +{ + FAR struct nxmb_serial_state_s *state; + FAR const struct nxmb_serial_config_s *cfg; + int ret; + + DEBUGASSERT(ctx); + + cfg = &ctx->transport_cfg.serial; + + if (cfg->devpath == NULL) + { + return -EINVAL; + } + + state = calloc(1, sizeof(struct nxmb_serial_state_s)); + if (state == NULL) + { + return -ENOMEM; + } + + state->fd = open(cfg->devpath, O_RDWR | O_NOCTTY); + if (state->fd < 0) + { + ret = -errno; + free(state); + return ret; + } + +#ifdef CONFIG_SERIAL_TERMIOS + if (tcgetattr(state->fd, &state->old_tio) < 0) + { + ret = -errno; + close(state->fd); + free(state); + return ret; + } + + ret = nxmb_serial_configure(state->fd, cfg->baudrate, cfg->parity); + + if (ret < 0) + { + close(state->fd); + free(state); + return ret; + } + + tcflush(state->fd, TCIOFLUSH); +#endif + + state->t35_usec = nxmb_serial_calculate_t35(cfg->baudrate); + state->rx_frame_now = false; + + ctx->transport_state = state; + + return 0; +} + +/**************************************************************************** + * Name: nxmb_serial_deinit + * + * Description: + * Deinitialize serial RTU transport. + * + * Input Parameters: + * ctx - Instance context + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +static int nxmb_serial_deinit(nxmb_handle_t ctx) +{ + FAR struct nxmb_serial_state_s *state; + + DEBUGASSERT(ctx && ctx->transport_state); + + state = ctx->transport_state; + +#ifdef CONFIG_SERIAL_TERMIOS + tcsetattr(state->fd, TCSANOW, &state->old_tio); +#endif + close(state->fd); + free(state); + + ctx->transport_state = NULL; + + return 0; +} + +/**************************************************************************** + * Name: nxmb_serial_send + * + * Description: + * Send RTU frame over serial port. + * + * Input Parameters: + * ctx - Instance context + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +static int nxmb_serial_send(nxmb_handle_t ctx) +{ + FAR struct nxmb_serial_state_s *state; + uint8_t frame[NXMB_RTU_FRAME_MAX]; + uint16_t data_len; + uint16_t total; + uint16_t crc; + ssize_t written; + + DEBUGASSERT(ctx && ctx->transport_state); + + state = ctx->transport_state; + + if (ctx->adu.length < 2) + { + return -EINVAL; + } + + data_len = ctx->adu.length - 2; + + /* Linearize the ADU and append CRC: unit_id, fc, data..., crc_lo, crc_hi */ + + frame[0] = ctx->adu.unit_id; + frame[1] = ctx->adu.fc; + memcpy(&frame[2], ctx->adu.data, data_len); + + crc = nxmb_crc16(frame, ctx->adu.length); + + frame[ctx->adu.length] = (crc & 0xff); + frame[ctx->adu.length + 1] = (crc >> 8); + + ctx->adu.crc = crc; + + total = ctx->adu.length + 2; + written = write(state->fd, frame, total); + + if (written < 0) + { + return -errno; + } + + if (written != total) + { + return -EIO; + } + + return 0; +} + +/**************************************************************************** + * Name: nxmb_serial_receive + * + * Description: + * Receive RTU frame from serial port with dynamic timeout. + * + * Input Parameters: + * ctx - Instance context + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +static int nxmb_serial_receive(nxmb_handle_t ctx) +{ + FAR struct nxmb_serial_state_s *state; + struct timeval timeout; + fd_set readfds; + ssize_t nread; + uint16_t crc_calc; + uint16_t crc_recv; + uint16_t data_len; + int ret; + + DEBUGASSERT(ctx && ctx->transport_state); + + state = ctx->transport_state; + + FD_ZERO(&readfds); + FD_SET(state->fd, &readfds); + + if (state->rx_frame_now) + { + timeout.tv_sec = 0; + timeout.tv_usec = state->t35_usec; + } + else + { + timeout.tv_sec = 0; + timeout.tv_usec = CONFIG_NXMODBUS_RTU_IDLE_TIMEOUT_MS * 1000; + } + + ret = select(state->fd + 1, &readfds, NULL, NULL, &timeout); + + if (ret < 0) + { + return -errno; + } + + if (ret == 0) + { + if (state->rx_frame_now) + { + state->rx_frame_now = false; + + if (state->rx_pos < NXMB_RTU_MIN_FRAME_SIZE) + { + state->rx_pos = 0; + return 0; + } + + crc_calc = nxmb_crc16(state->rx_buf, state->rx_pos - 2); + crc_recv = state->rx_buf[state->rx_pos - 2] | + (state->rx_buf[state->rx_pos - 1] << 8); + + if (crc_calc != crc_recv) + { + state->rx_pos = 0; + return 0; + } + + /* Unpack frame into ctx->adu (excluding the trailing CRC). */ + + data_len = (state->rx_pos - 4); + ctx->adu.unit_id = state->rx_buf[0]; + ctx->adu.fc = state->rx_buf[1]; + memcpy(ctx->adu.data, &state->rx_buf[2], data_len); + ctx->adu.crc = crc_recv; + ctx->adu.length = (state->rx_pos - 2); + + state->rx_pos = 0; + return 1; + } + + return 0; + } + + if (!FD_ISSET(state->fd, &readfds)) + { + return 0; + } + + if (!state->rx_frame_now) + { + state->rx_pos = 0; + state->rx_frame_now = true; + } + + nread = read(state->fd, + &state->rx_buf[state->rx_pos], + sizeof(state->rx_buf) - state->rx_pos); + if (nread < 0) + { + return -errno; + } + + if (nread == 0) + { + return 0; + } + + state->rx_pos += nread; + + if (state->rx_pos >= sizeof(state->rx_buf)) + { + state->rx_pos = 0; + state->rx_frame_now = false; + return 0; + } + + return 0; +} diff --git a/industry/nxmodbus/transport/nxmb_serial_common.c b/industry/nxmodbus/transport/nxmb_serial_common.c new file mode 100644 index 00000000000..72b2d1a87b3 --- /dev/null +++ b/industry/nxmodbus/transport/nxmb_serial_common.c @@ -0,0 +1,138 @@ +/**************************************************************************** + * apps/industry/nxmodbus/transport/nxmb_serial_common.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 + +#include + +#include +#include + +#ifdef CONFIG_SERIAL_TERMIOS +# include +#endif + +#include + +#include "nxmb_internal.h" + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +#ifdef CONFIG_SERIAL_TERMIOS + +/**************************************************************************** + * Name: nxmb_serial_configure + * + * Description: + * Configure serial port with termios settings for Modbus communication. + * + * Input Parameters: + * fd - File descriptor + * baudrate - Baud rate + * parity - Parity setting + * + * Returned Value: + * Zero on success; a negated errno value on failure + * + ****************************************************************************/ + +int nxmb_serial_configure(int fd, uint32_t baudrate, + enum nxmb_parity_e parity) +{ + struct termios tio; + speed_t speed; + + if (tcgetattr(fd, &tio) < 0) + { + return -errno; + } + + switch (baudrate) + { + case 9600: + speed = B9600; + break; + case 19200: + speed = B19200; + break; + case 38400: + speed = B38400; + break; + case 57600: + speed = B57600; + break; + case 115200: + speed = B115200; + break; + default: + return -EINVAL; + } + + cfsetispeed(&tio, speed); + cfsetospeed(&tio, speed); + + tio.c_cflag &= ~(CSIZE | PARENB | PARODD | CSTOPB); + tio.c_cflag |= CS8 | CLOCAL | CREAD; + + switch (parity) + { + case NXMB_PAR_NONE: + + /* Modbus spec: no parity requires 2 stop bits so the + * character frame remains 11 bits. + */ + + tio.c_cflag |= CSTOPB; + break; + case NXMB_PAR_EVEN: + tio.c_cflag |= PARENB; + break; + case NXMB_PAR_ODD: + tio.c_cflag |= PARENB | PARODD; + break; + default: + return -EINVAL; + } + + tio.c_iflag &= + ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); + tio.c_oflag &= ~OPOST; + tio.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); + + tio.c_cc[VMIN] = 0; + tio.c_cc[VTIME] = 0; + + if (tcsetattr(fd, TCSANOW, &tio) < 0) + { + return -errno; + } + + return 0; +} + +#endif /* CONFIG_SERIAL_TERMIOS */ diff --git a/industry/nxmodbus/transport/nxmb_tcp.c b/industry/nxmodbus/transport/nxmb_tcp.c new file mode 100644 index 00000000000..f4b2dc94021 --- /dev/null +++ b/industry/nxmodbus/transport/nxmb_tcp.c @@ -0,0 +1,684 @@ +/**************************************************************************** + * apps/industry/nxmodbus/transport/nxmb_tcp.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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "nxmb_internal.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define MBAP_HEADER_SIZE 7 +#define MBAP_FC_SIZE 8 +#define NXMB_TCP_RECV_MS 1000 + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +struct nxmb_tcp_client_s +{ + int fd; + uint16_t trans_id; + time_t last_activity; +}; + +struct nxmb_tcp_state_s +{ + struct nxmb_tcp_client_s clients[CONFIG_NXMODBUS_TCP_MAX_CLIENTS]; + int listen_fd; + int active_idx; +}; + +/**************************************************************************** + * Private Function Prototypes + ****************************************************************************/ + +static int nxmb_tcp_init(nxmb_handle_t ctx); +static int nxmb_tcp_deinit(nxmb_handle_t ctx); +static int nxmb_tcp_send(nxmb_handle_t ctx); +static int nxmb_tcp_receive(nxmb_handle_t ctx); + +/**************************************************************************** + * Public Data + ****************************************************************************/ + +const struct nxmb_transport_ops_s g_nxmb_tcp_ops = +{ + .init = nxmb_tcp_init, + .deinit = nxmb_tcp_deinit, + .send = nxmb_tcp_send, + .receive = nxmb_tcp_receive, +}; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmb_tcp_put_header + ****************************************************************************/ + +static void nxmb_tcp_put_header(FAR const struct nxmb_adu_s *adu, + FAR uint8_t *hdr) +{ + nxmb_util_put_u16_be(&hdr[0], adu->trans_id); + nxmb_util_put_u16_be(&hdr[2], adu->proto_id); + nxmb_util_put_u16_be(&hdr[4], adu->length); + hdr[6] = adu->unit_id; + hdr[7] = adu->fc; +} + +/**************************************************************************** + * Name: nxmb_tcp_get_header + ****************************************************************************/ + +static void nxmb_tcp_get_header(FAR struct nxmb_adu_s *adu, + FAR const uint8_t *hdr) +{ + adu->trans_id = nxmb_util_get_u16_be(&hdr[0]); + adu->proto_id = nxmb_util_get_u16_be(&hdr[2]); + adu->length = nxmb_util_get_u16_be(&hdr[4]); + adu->unit_id = hdr[6]; + adu->fc = hdr[7]; +} + +/**************************************************************************** + * Name: nxmb_tcp_create_server + ****************************************************************************/ + +static int nxmb_tcp_create_server(FAR const struct nxmb_tcp_config_s *cfg) +{ + struct sockaddr_in addr; + int listen_fd; + int ret; +#ifdef CONFIG_NET_SOCKOPTS + int opt = 1; +#endif + + listen_fd = socket(AF_INET, SOCK_STREAM, 0); + if (listen_fd < 0) + { + return -errno; + } + +#ifdef CONFIG_NET_SOCKOPTS + setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); +#endif + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(cfg->port); + + if (cfg->bindaddr != NULL) + { + ret = inet_pton(AF_INET, cfg->bindaddr, &addr.sin_addr); + if (ret != 1) + { + close(listen_fd); + return -EINVAL; + } + } + else + { + addr.sin_addr.s_addr = htonl(INADDR_ANY); + } + + ret = bind(listen_fd, (FAR struct sockaddr *)&addr, sizeof(addr)); + if (ret < 0) + { + close(listen_fd); + return -errno; + } + + ret = listen(listen_fd, CONFIG_NXMODBUS_TCP_MAX_CLIENTS); + if (ret < 0) + { + close(listen_fd); + return -errno; + } + + return listen_fd; +} + +/**************************************************************************** + * Name: nxmb_tcp_connect_client + ****************************************************************************/ + +static int nxmb_tcp_connect_client(FAR const struct nxmb_tcp_config_s *cfg) +{ + struct sockaddr_in addr; + int client_fd; + int ret; + + if (cfg->host == NULL) + { + return -EINVAL; + } + + client_fd = socket(AF_INET, SOCK_STREAM, 0); + if (client_fd < 0) + { + return -errno; + } + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(cfg->port); + + ret = inet_pton(AF_INET, cfg->host, &addr.sin_addr); + if (ret != 1) + { + close(client_fd); + return -EINVAL; + } + + ret = connect(client_fd, (FAR struct sockaddr *)&addr, sizeof(addr)); + if (ret < 0) + { + close(client_fd); + return -errno; + } + + return client_fd; +} + +/**************************************************************************** + * Name: nxmb_tcp_recv_full + ****************************************************************************/ + +static int nxmb_tcp_recv_full(int fd, FAR uint8_t *buf, size_t len, + int timeout_ms) +{ + struct timeval tv; + fd_set readfds; + size_t received; + int ret; + + received = 0; + + while (received < len) + { + FD_ZERO(&readfds); + FD_SET(fd, &readfds); + + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + + ret = select(fd + 1, &readfds, NULL, NULL, &tv); + if (ret < 0) + { + return -errno; + } + else if (ret == 0) + { + return -ETIMEDOUT; + } + + ret = recv(fd, &buf[received], len - received, 0); + if (ret < 0) + { + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) + { + continue; + } + + return -errno; + } + else if (ret == 0) + { + return -ECONNRESET; + } + + received += ret; + } + + return received; +} + +/**************************************************************************** + * Name: nxmb_tcp_init + ****************************************************************************/ + +static int nxmb_tcp_init(nxmb_handle_t ctx) +{ + FAR const struct nxmb_tcp_config_s *cfg; + FAR struct nxmb_tcp_state_s *state; + int fd; + int i; + + DEBUGASSERT(ctx); + + cfg = &ctx->transport_cfg.tcp; + + state = calloc(1, sizeof(struct nxmb_tcp_state_s)); + if (state == NULL) + { + return -ENOMEM; + } + + state->listen_fd = -1; + state->active_idx = -1; + + for (i = 0; i < CONFIG_NXMODBUS_TCP_MAX_CLIENTS; i++) + { + state->clients[i].fd = -1; + } + + if (ctx->is_client) + { + fd = nxmb_tcp_connect_client(cfg); + if (fd < 0) + { + free(state); + return fd; + } + + state->clients[0].fd = fd; + state->clients[0].last_activity = time(NULL); + } + else + { + fd = nxmb_tcp_create_server(cfg); + if (fd < 0) + { + free(state); + return fd; + } + + state->listen_fd = fd; + } + + ctx->transport_state = state; + + return OK; +} + +/**************************************************************************** + * Name: nxmb_tcp_deinit + ****************************************************************************/ + +static int nxmb_tcp_deinit(nxmb_handle_t ctx) +{ + FAR struct nxmb_tcp_state_s *state; + int i; + + DEBUGASSERT(ctx && ctx->transport_state); + + state = ctx->transport_state; + + for (i = 0; i < CONFIG_NXMODBUS_TCP_MAX_CLIENTS; i++) + { + if (state->clients[i].fd >= 0) + { + close(state->clients[i].fd); + } + } + + if (state->listen_fd >= 0) + { + close(state->listen_fd); + } + + free(state); + ctx->transport_state = NULL; + + return OK; +} + +/**************************************************************************** + * Name: nxmb_tcp_send + ****************************************************************************/ + +static int nxmb_tcp_send(nxmb_handle_t ctx) +{ + FAR struct nxmb_tcp_state_s *state; + FAR struct nxmb_tcp_client_s *client; + uint8_t header[MBAP_FC_SIZE]; + struct iovec iov[2]; + uint16_t data_len; + uint16_t total; + ssize_t sent; + int fd; + + DEBUGASSERT(ctx && ctx->transport_state); + + state = ctx->transport_state; + + /* For server mode, respond to the client that sent the request. + * For client mode, use the single connection (index 0). + */ + + if (ctx->is_client) + { + client = &state->clients[0]; + } + else + { + if (state->active_idx < 0) + { + return -ENOTCONN; + } + + client = &state->clients[state->active_idx]; + } + + fd = client->fd; + if (fd < 0) + { + return -ENOTCONN; + } + + if (ctx->adu.length < 2) + { + return -EINVAL; + } + + /* Populate the MBAP fields before serializing the header */ + + ctx->adu.trans_id = client->trans_id; + ctx->adu.proto_id = 0x0000; + + data_len = ctx->adu.length - 2; + + /* Emit MBAP header + PDU data via writev() to avoid a stack-side + * copy. The kernel gathers both iovecs into a single TCP segment. + */ + + nxmb_tcp_put_header(&ctx->adu, header); + + iov[0].iov_base = header; + iov[0].iov_len = MBAP_FC_SIZE; + iov[1].iov_base = ctx->adu.data; + iov[1].iov_len = data_len; + + total = MBAP_FC_SIZE + data_len; + sent = writev(fd, iov, (data_len > 0) ? 2 : 1); + if (sent != total) + { + return (sent < 0) ? -errno : -EIO; + } + + client->last_activity = time(NULL); + + return OK; +} + +/**************************************************************************** + * Name: nxmb_tcp_evict_idle + * + * Description: + * Close idle client connections that exceed the configured timeout. + * + ****************************************************************************/ + +static void nxmb_tcp_evict_idle(FAR struct nxmb_tcp_state_s *state, + time_t now) +{ + int i; + + for (i = 0; i < CONFIG_NXMODBUS_TCP_MAX_CLIENTS; i++) + { + if (state->clients[i].fd >= 0 && + (now - state->clients[i].last_activity) > + CONFIG_NXMODBUS_TCP_TIMEOUT_SEC) + { + close(state->clients[i].fd); + state->clients[i].fd = -1; + } + } +} + +/**************************************************************************** + * Name: nxmb_tcp_accept_new + * + * Description: + * Accept a pending connection into the first free client slot. + * + ****************************************************************************/ + +static void nxmb_tcp_accept_new(FAR struct nxmb_tcp_state_s *state) +{ + int fd; + int i; + + fd = accept(state->listen_fd, NULL, NULL); + if (fd < 0) + { + return; + } + + for (i = 0; i < CONFIG_NXMODBUS_TCP_MAX_CLIENTS; i++) + { + if (state->clients[i].fd < 0) + { + state->clients[i].fd = fd; + state->clients[i].last_activity = time(NULL); + return; + } + } + + /* No free slot -- reject the connection */ + + close(fd); +} + +/**************************************************************************** + * Name: nxmb_tcp_read_frame + * + * Description: + * Read a complete Modbus TCP frame from a client connection. + * On error (except timeout), the connection is closed. + * + ****************************************************************************/ + +static int nxmb_tcp_read_frame(nxmb_handle_t ctx, + FAR struct nxmb_tcp_client_s *client) +{ + uint8_t header[MBAP_FC_SIZE]; + uint16_t data_len; + int ret; + + ret = nxmb_tcp_recv_full(client->fd, header, MBAP_FC_SIZE, + NXMB_TCP_RECV_MS); + if (ret < 0) + { + if (ret == -ETIMEDOUT || ret == -EAGAIN) + { + return -EAGAIN; + } + + close(client->fd); + client->fd = -1; + return ret; + } + + nxmb_tcp_get_header(&ctx->adu, header); + + if (ctx->adu.proto_id != 0x0000) + { + close(client->fd); + client->fd = -1; + return -EPROTO; + } + + if (ctx->adu.length < 2) + { + close(client->fd); + client->fd = -1; + return -EINVAL; + } + + data_len = ctx->adu.length - 2; + + if (data_len > NXMB_ADU_DATA_MAX) + { + close(client->fd); + client->fd = -1; + return -EMSGSIZE; + } + + if (data_len > 0) + { + ret = nxmb_tcp_recv_full(client->fd, ctx->adu.data, data_len, + NXMB_TCP_RECV_MS); + if (ret < 0) + { + close(client->fd); + client->fd = -1; + return ret; + } + } + + client->trans_id = ctx->adu.trans_id; + client->last_activity = time(NULL); + + return ctx->adu.length; +} + +static int nxmb_tcp_receive(nxmb_handle_t ctx) +{ + FAR struct nxmb_tcp_state_s *state; + struct timeval tv; + fd_set readfds; + time_t now; + int maxfd; + int ret; + int i; + + DEBUGASSERT(ctx && ctx->transport_state); + + state = ctx->transport_state; + state->active_idx = -1; + + /* Client mode: single connection, read directly */ + + if (ctx->is_client) + { + if (state->clients[0].fd < 0) + { + return -ENOTCONN; + } + + state->active_idx = 0; + return nxmb_tcp_read_frame(ctx, &state->clients[0]); + } + + /* Server mode: multiplex listen socket + all client connections */ + + now = time(NULL); + nxmb_tcp_evict_idle(state, now); + + FD_ZERO(&readfds); + maxfd = -1; + + if (state->listen_fd >= 0) + { + FD_SET(state->listen_fd, &readfds); + maxfd = state->listen_fd; + } + + for (i = 0; i < CONFIG_NXMODBUS_TCP_MAX_CLIENTS; i++) + { + if (state->clients[i].fd >= 0) + { + FD_SET(state->clients[i].fd, &readfds); + if (state->clients[i].fd > maxfd) + { + maxfd = state->clients[i].fd; + } + } + } + + if (maxfd < 0) + { + return -EAGAIN; + } + + tv.tv_sec = 0; + tv.tv_usec = 50000; + + ret = select(maxfd + 1, &readfds, NULL, NULL, &tv); + if (ret < 0) + { + return -errno; + } + + if (ret == 0) + { + return -EAGAIN; + } + + /* Accept new connections if listen socket is ready */ + + if (state->listen_fd >= 0 && FD_ISSET(state->listen_fd, &readfds)) + { + nxmb_tcp_accept_new(state); + } + + /* Check each client for incoming data (first ready wins). + * In server mode, a per-client read failure (disconnect, protocol + * error, etc.) must not be fatal - nxmb_tcp_read_frame has already + * closed the offending fd, and the listen socket remains open for new + * connections. Translate any such error to -EAGAIN so the caller + * keeps polling. + */ + + for (i = 0; i < CONFIG_NXMODBUS_TCP_MAX_CLIENTS; i++) + { + if (state->clients[i].fd >= 0 && + FD_ISSET(state->clients[i].fd, &readfds)) + { + state->active_idx = i; + ret = nxmb_tcp_read_frame(ctx, &state->clients[i]); + if (ret < 0 && ret != -EAGAIN) + { + state->active_idx = -1; + return -EAGAIN; + } + + return ret; + } + } + + return -EAGAIN; +} diff --git a/system/nxmbclient/CMakeLists.txt b/system/nxmbclient/CMakeLists.txt new file mode 100644 index 00000000000..fa463ae85ef --- /dev/null +++ b/system/nxmbclient/CMakeLists.txt @@ -0,0 +1,35 @@ +# ############################################################################## +# apps/system/nxmbclient/CMakeLists.txt +# +# 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. +# +# ############################################################################## + +if(CONFIG_SYSTEM_NXMBCLIENT) + nuttx_add_application( + NAME + ${CONFIG_NXMBCLIENT_PROGNAME} + PRIORITY + ${CONFIG_NXMBCLIENT_PRIORITY} + STACKSIZE + ${CONFIG_NXMBCLIENT_STACKSIZE} + MODULE + ${CONFIG_SYSTEM_NXMBCLIENT} + SRCS + nxmbclient_main.c) +endif() diff --git a/system/nxmbclient/Kconfig b/system/nxmbclient/Kconfig new file mode 100644 index 00000000000..8768d601ca3 --- /dev/null +++ b/system/nxmbclient/Kconfig @@ -0,0 +1,32 @@ +# +# For a description of the syntax of this configuration file, +# see the file kconfig-language.txt in the NuttX tools repository. +# + +config SYSTEM_NXMBCLIENT + tristate "NxModbus Client Tool" + default n + depends on NXMODBUS_CLIENT + ---help--- + Enable the NxModbus client command-line tool. This utility allows + you to perform Modbus client operations from the command line, + supporting RTU, ASCII, and TCP transports. + +if SYSTEM_NXMBCLIENT + +config NXMBCLIENT_PROGNAME + string "Program name" + default "nxmbclient" + ---help--- + This is the name of the program that will be used when the NSH ELF + program is installed. + +config NXMBCLIENT_PRIORITY + int "NxModbus client task priority" + default 100 + +config NXMBCLIENT_STACKSIZE + int "NxModbus client stack size" + default DEFAULT_TASK_STACKSIZE + +endif # SYSTEM_NXMBCLIENT diff --git a/system/nxmbclient/Make.defs b/system/nxmbclient/Make.defs new file mode 100644 index 00000000000..e171ba4d4db --- /dev/null +++ b/system/nxmbclient/Make.defs @@ -0,0 +1,25 @@ +############################################################################ +# apps/system/nxmbclient/Make.defs +# +# 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. +# +############################################################################ + +ifneq ($(CONFIG_SYSTEM_NXMBCLIENT),) +CONFIGURED_APPS += $(APPDIR)/system/nxmbclient +endif diff --git a/system/nxmbclient/Makefile b/system/nxmbclient/Makefile new file mode 100644 index 00000000000..f4e1a7cc0c0 --- /dev/null +++ b/system/nxmbclient/Makefile @@ -0,0 +1,32 @@ +############################################################################ +# apps/system/nxmbclient/Makefile +# +# 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. +# +############################################################################ + +include $(APPDIR)/Make.defs + +MAINSRC = nxmbclient_main.c + +PROGNAME = $(CONFIG_NXMBCLIENT_PROGNAME) +PRIORITY = $(CONFIG_NXMBCLIENT_PRIORITY) +STACKSIZE = $(CONFIG_NXMBCLIENT_STACKSIZE) +MODULE = $(CONFIG_SYSTEM_NXMBCLIENT) + +include $(APPDIR)/Application.mk diff --git a/system/nxmbclient/nxmbclient_main.c b/system/nxmbclient/nxmbclient_main.c new file mode 100644 index 00000000000..eb550d07cc4 --- /dev/null +++ b/system/nxmbclient/nxmbclient_main.c @@ -0,0 +1,799 @@ +/**************************************************************************** + * apps/system/nxmbclient/nxmbclient_main.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 + +#include +#include +#include +#include +#include +#include + +#include +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define DEFAULT_TIMEOUT_MS 1000 +#define DEFAULT_UNIT_ID 1 +#define DEFAULT_TCP_PORT 502 +#define DEFAULT_BAUDRATE 115200 + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +/* Modbus client type */ + +enum transport_type_e +{ + TRANSPORT_RTU = 0, + TRANSPORT_ASCII, + TRANSPORT_TCP +}; + +/* Modbus client configuration */ + +struct nxmbclient_config_s +{ + enum transport_type_e transport; + enum nxmb_parity_e parity; + FAR const char *device; + FAR const char *host; + uint32_t baudrate; + uint16_t port; + uint8_t unit_id; + uint32_t timeout_ms; + uint32_t poll_ms; +}; + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static volatile bool g_running = true; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: signal_handler + ****************************************************************************/ + +static void signal_handler(int signo) +{ + g_running = false; +} + +/**************************************************************************** + * Name: show_usage + ****************************************************************************/ + +static void show_usage(FAR const char *progname) +{ + printf("Usage: %s [OPTIONS] COMMAND [ARGS...]\n\n", progname); + printf("Transport Options:\n"); + printf(" -t TYPE Transport type: rtu, ascii, tcp (required)\n"); + printf(" -d DEVICE Serial device path (for RTU/ASCII)\n"); + printf(" -b BAUD Baud rate (default: %u)\n", DEFAULT_BAUDRATE); + printf(" -p PARITY Parity: none, even, odd (default: none)\n"); + printf(" -h HOST TCP host address (for TCP)\n"); + printf(" -P PORT TCP port (default: %u)\n", DEFAULT_TCP_PORT); + printf("\nModbus Options:\n"); + printf(" -u UNIT Unit ID (default: %u)\n", DEFAULT_UNIT_ID); + printf(" -T TIMEOUT Timeout in ms (default: %u)\n", DEFAULT_TIMEOUT_MS); + printf(" --poll MS Polling interval in ms (0 = one-shot)\n"); + printf("\nCommands:\n"); + printf(" read-coils ADDR COUNT\n"); + printf(" read-discrete ADDR COUNT\n"); + printf(" read-input ADDR COUNT\n"); + printf(" read-holding ADDR COUNT\n"); + printf(" write-coil ADDR VALUE\n"); + printf(" write-holding ADDR VALUE\n"); + printf(" write-coils ADDR VALUE...\n"); + printf(" write-holdings ADDR VALUE...\n"); +} + +/**************************************************************************** + * Name: parse_parity + ****************************************************************************/ + +static int parse_parity(FAR const char *str, FAR enum nxmb_parity_e *parity) +{ + if (strcmp(str, "none") == 0) + { + *parity = NXMB_PAR_NONE; + } + else if (strcmp(str, "even") == 0) + { + *parity = NXMB_PAR_EVEN; + } + else if (strcmp(str, "odd") == 0) + { + *parity = NXMB_PAR_ODD; + } + else + { + return -EINVAL; + } + + return OK; +} + +/**************************************************************************** + * Name: parse_transport + ****************************************************************************/ + +static int parse_transport(FAR const char *str, + FAR enum transport_type_e *transport) +{ + if (strcmp(str, "rtu") == 0) + { + *transport = TRANSPORT_RTU; + } + else if (strcmp(str, "ascii") == 0) + { + *transport = TRANSPORT_ASCII; + } + else if (strcmp(str, "tcp") == 0) + { + *transport = TRANSPORT_TCP; + } + else + { + return -EINVAL; + } + + return OK; +} + +/**************************************************************************** + * Name: cmd_read_coils + ****************************************************************************/ + +static int cmd_read_coils(nxmb_handle_t handle, uint8_t unit_id, + int argc, FAR char **argv) +{ + FAR uint8_t *values; + uint16_t addr; + uint16_t count; + uint16_t nbytes; + int ret; + int i; + + if (argc < 2) + { + fprintf(stderr, "Error: read-coils requires ADDR COUNT\n"); + return -EINVAL; + } + + addr = (uint16_t)strtoul(argv[0], NULL, 0); + count = (uint16_t)strtoul(argv[1], NULL, 0); + nbytes = (count + 7) / 8; + + values = malloc(nbytes); + if (values == NULL) + { + return -ENOMEM; + } + + memset(values, 0, nbytes); + + ret = nxmb_read_coils(handle, unit_id, addr, count, values); + if (ret < 0) + { + fprintf(stderr, "Error: read-coils failed: %d\n", ret); + free(values); + return ret; + } + + for (i = 0; i < count; i++) + { + printf("%u\t%u\n", addr + i, + (values[i / 8] >> (i % 8)) & 1); + } + + free(values); + return OK; +} + +/**************************************************************************** + * Name: cmd_read_discrete + ****************************************************************************/ + +static int cmd_read_discrete(nxmb_handle_t handle, uint8_t unit_id, + int argc, FAR char **argv) +{ + FAR uint8_t *values; + uint16_t addr; + uint16_t count; + uint16_t nbytes; + int ret; + int i; + + if (argc < 2) + { + fprintf(stderr, "Error: read-discrete requires ADDR COUNT\n"); + return -EINVAL; + } + + addr = (uint16_t)strtoul(argv[0], NULL, 0); + count = (uint16_t)strtoul(argv[1], NULL, 0); + nbytes = (count + 7) / 8; + + values = malloc(nbytes); + if (values == NULL) + { + return -ENOMEM; + } + + memset(values, 0, nbytes); + + ret = nxmb_read_discrete(handle, unit_id, addr, count, values); + if (ret < 0) + { + fprintf(stderr, "Error: read-discrete failed: %d\n", ret); + free(values); + return ret; + } + + for (i = 0; i < count; i++) + { + printf("%u\t%u\n", addr + i, + (values[i / 8] >> (i % 8)) & 1); + } + + free(values); + return OK; +} + +/**************************************************************************** + * Name: cmd_read_input + ****************************************************************************/ + +static int cmd_read_input(nxmb_handle_t handle, uint8_t unit_id, + int argc, FAR char **argv) +{ + FAR uint16_t *values; + uint16_t addr; + uint16_t count; + int ret; + int i; + + if (argc < 2) + { + fprintf(stderr, "Error: read-input requires ADDR COUNT\n"); + return -EINVAL; + } + + addr = (uint16_t)strtoul(argv[0], NULL, 0); + count = (uint16_t)strtoul(argv[1], NULL, 0); + + values = malloc(count * sizeof(uint16_t)); + if (values == NULL) + { + return -ENOMEM; + } + + ret = nxmb_read_input(handle, unit_id, addr, count, values); + if (ret < 0) + { + fprintf(stderr, "Error: read-input failed: %d\n", ret); + free(values); + return ret; + } + + for (i = 0; i < count; i++) + { + printf("%u\t%u\n", addr + i, values[i]); + } + + free(values); + return OK; +} + +/**************************************************************************** + * Name: cmd_read_holding + ****************************************************************************/ + +static int cmd_read_holding(nxmb_handle_t handle, uint8_t unit_id, + int argc, FAR char **argv) +{ + FAR uint16_t *values; + uint16_t addr; + uint16_t count; + int ret; + int i; + + if (argc < 2) + { + fprintf(stderr, "Error: read-holding requires ADDR COUNT\n"); + return -EINVAL; + } + + addr = (uint16_t)strtoul(argv[0], NULL, 0); + count = (uint16_t)strtoul(argv[1], NULL, 0); + + values = malloc(count * sizeof(uint16_t)); + if (values == NULL) + { + return -ENOMEM; + } + + ret = nxmb_read_holding(handle, unit_id, addr, count, values); + if (ret < 0) + { + fprintf(stderr, "Error: read-holding failed: %d\n", ret); + free(values); + return ret; + } + + for (i = 0; i < count; i++) + { + printf("%u\t%u\n", addr + i, values[i]); + } + + free(values); + return OK; +} + +/**************************************************************************** + * Name: cmd_write_coil + ****************************************************************************/ + +static int cmd_write_coil(nxmb_handle_t handle, uint8_t unit_id, + int argc, FAR char **argv) +{ + uint16_t addr; + uint8_t value; + int ret; + + if (argc < 2) + { + fprintf(stderr, "Error: write-coil requires ADDR VALUE\n"); + return -EINVAL; + } + + addr = (uint16_t)strtoul(argv[0], NULL, 0); + value = (uint8_t)strtoul(argv[1], NULL, 0); + + ret = nxmb_write_coil(handle, unit_id, addr, value); + if (ret < 0) + { + fprintf(stderr, "Error: write-coil failed: %d\n", ret); + return ret; + } + + printf("OK\n"); + return OK; +} + +/**************************************************************************** + * Name: cmd_write_holding + ****************************************************************************/ + +static int cmd_write_holding(nxmb_handle_t handle, uint8_t unit_id, + int argc, FAR char **argv) +{ + uint16_t addr; + uint16_t value; + int ret; + + if (argc < 2) + { + fprintf(stderr, "Error: write-holding requires ADDR VALUE\n"); + return -EINVAL; + } + + addr = (uint16_t)strtoul(argv[0], NULL, 0); + value = (uint16_t)strtoul(argv[1], NULL, 0); + + ret = nxmb_write_holding(handle, unit_id, addr, value); + if (ret < 0) + { + fprintf(stderr, "Error: write-holding failed: %d\n", ret); + return ret; + } + + printf("OK\n"); + return OK; +} + +/**************************************************************************** + * Name: cmd_write_coils + ****************************************************************************/ + +static int cmd_write_coils(nxmb_handle_t handle, uint8_t unit_id, + int argc, FAR char **argv) +{ + FAR uint8_t *values; + uint16_t addr; + uint16_t count; + int ret; + int i; + + if (argc < 2) + { + fprintf(stderr, "Error: write-coils requires ADDR VALUE...\n"); + return -EINVAL; + } + + addr = (uint16_t)strtoul(argv[0], NULL, 0); + count = argc - 1; + + values = malloc(count); + if (values == NULL) + { + return -ENOMEM; + } + + for (i = 0; i < count; i++) + { + values[i] = (uint8_t)strtoul(argv[i + 1], NULL, 0); + } + + ret = nxmb_write_coils(handle, unit_id, addr, count, values); + if (ret < 0) + { + fprintf(stderr, "Error: write-coils failed: %d\n", ret); + free(values); + return ret; + } + + free(values); + printf("OK\n"); + return OK; +} + +/**************************************************************************** + * Name: cmd_write_holdings + ****************************************************************************/ + +static int cmd_write_holdings(nxmb_handle_t handle, uint8_t unit_id, + int argc, FAR char **argv) +{ + FAR uint16_t *values; + uint16_t addr; + uint16_t count; + int ret; + int i; + + if (argc < 2) + { + fprintf(stderr, "Error: write-holdings requires ADDR VALUE...\n"); + return -EINVAL; + } + + addr = (uint16_t)strtoul(argv[0], NULL, 0); + count = argc - 1; + + values = malloc(count * sizeof(uint16_t)); + if (values == NULL) + { + return -ENOMEM; + } + + for (i = 0; i < count; i++) + { + values[i] = (uint16_t)strtoul(argv[i + 1], NULL, 0); + } + + ret = nxmb_write_holdings(handle, unit_id, addr, count, values); + if (ret < 0) + { + fprintf(stderr, "Error: write-holdings failed: %d\n", ret); + free(values); + return ret; + } + + free(values); + printf("OK\n"); + return OK; +} + +/**************************************************************************** + * Name: execute_command + ****************************************************************************/ + +static int execute_command(nxmb_handle_t handle, + uint8_t unit_id, + FAR const char *cmd, + int argc, + FAR char **argv) +{ + if (strcmp(cmd, "read-coils") == 0) + { + return cmd_read_coils(handle, unit_id, argc, argv); + } + else if (strcmp(cmd, "read-discrete") == 0) + { + return cmd_read_discrete(handle, unit_id, argc, argv); + } + else if (strcmp(cmd, "read-input") == 0) + { + return cmd_read_input(handle, unit_id, argc, argv); + } + else if (strcmp(cmd, "read-holding") == 0) + { + return cmd_read_holding(handle, unit_id, argc, argv); + } + else if (strcmp(cmd, "write-coil") == 0) + { + return cmd_write_coil(handle, unit_id, argc, argv); + } + else if (strcmp(cmd, "write-holding") == 0) + { + return cmd_write_holding(handle, unit_id, argc, argv); + } + else if (strcmp(cmd, "write-coils") == 0) + { + return cmd_write_coils(handle, unit_id, argc, argv); + } + else if (strcmp(cmd, "write-holdings") == 0) + { + return cmd_write_holdings(handle, unit_id, argc, argv); + } + else + { + fprintf(stderr, "Error: unknown command '%s'\n", cmd); + return -EINVAL; + } +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: nxmbclient_main + ****************************************************************************/ + +int main(int argc, FAR char *argv[]) +{ + struct nxmbclient_config_s config; + struct nxmb_config_s mb_config; + nxmb_handle_t handle = NULL; + struct sigaction sa; + FAR const char *cmd; + int option; + int ret; + int cmd_argc; + FAR char **cmd_argv; + + /* Initialize config with defaults */ + + memset(&config, 0, sizeof(config)); + config.baudrate = DEFAULT_BAUDRATE; + config.parity = NXMB_PAR_NONE; + config.port = DEFAULT_TCP_PORT; + config.unit_id = DEFAULT_UNIT_ID; + config.timeout_ms = DEFAULT_TIMEOUT_MS; + config.poll_ms = 0; + + /* Parse options */ + + while ((option = getopt(argc, argv, "t:d:b:p:h:P:u:T:-:")) != -1) + { + switch (option) + { + case 't': + if (parse_transport(optarg, &config.transport) < 0) + { + fprintf(stderr, "Error: invalid transport '%s'\n", optarg); + return EXIT_FAILURE; + } + break; + + case 'd': + config.device = optarg; + break; + + case 'b': + config.baudrate = strtoul(optarg, NULL, 0); + break; + + case 'p': + if (parse_parity(optarg, &config.parity) < 0) + { + fprintf(stderr, "Error: invalid parity '%s'\n", optarg); + return EXIT_FAILURE; + } + break; + + case 'h': + config.host = optarg; + break; + + case 'P': + config.port = (uint16_t)strtoul(optarg, NULL, 0); + break; + + case 'u': + config.unit_id = (uint8_t)strtoul(optarg, NULL, 0); + break; + + case 'T': + config.timeout_ms = strtoul(optarg, NULL, 0); + break; + + case '-': + if (strcmp(optarg, "poll") == 0) + { + if (optind < argc) + { + config.poll_ms = strtoul(argv[optind++], NULL, 0); + } + else + { + fprintf(stderr, "Error: --poll requires argument\n"); + return EXIT_FAILURE; + } + } + else + { + fprintf(stderr, "Error: unknown option --%s\n", optarg); + return EXIT_FAILURE; + } + break; + + default: + show_usage(argv[0]); + return EXIT_FAILURE; + } + } + + /* Check for command */ + + if (optind >= argc) + { + fprintf(stderr, "Error: no command specified\n"); + show_usage(argv[0]); + return EXIT_FAILURE; + } + + cmd = argv[optind]; + cmd_argc = argc - optind - 1; + cmd_argv = &argv[optind + 1]; + + /* Validate transport-specific options */ + + if (config.transport == TRANSPORT_RTU || + config.transport == TRANSPORT_ASCII) + { + if (config.device == NULL) + { + fprintf(stderr, "Error: -d DEVICE required for RTU/ASCII\n"); + return EXIT_FAILURE; + } + } + else if (config.transport == TRANSPORT_TCP) + { + if (config.host == NULL) + { + fprintf(stderr, "Error: -h HOST required for TCP\n"); + return EXIT_FAILURE; + } + } + + /* Setup signal handler for clean shutdown */ + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = signal_handler; + sigaction(SIGINT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); + + /* Create Modbus context */ + + memset(&mb_config, 0, sizeof(mb_config)); + mb_config.unit_id = config.unit_id; + mb_config.is_client = true; + + if (config.transport == TRANSPORT_RTU) + { + mb_config.mode = NXMB_MODE_RTU; + mb_config.transport.serial.devpath = config.device; + mb_config.transport.serial.baudrate = config.baudrate; + mb_config.transport.serial.parity = config.parity; + } + else if (config.transport == TRANSPORT_ASCII) + { + mb_config.mode = NXMB_MODE_ASCII; + mb_config.transport.serial.devpath = config.device; + mb_config.transport.serial.baudrate = config.baudrate; + mb_config.transport.serial.parity = config.parity; + } + else if (config.transport == TRANSPORT_TCP) + { + mb_config.mode = NXMB_MODE_TCP; + mb_config.transport.tcp.host = config.host; + mb_config.transport.tcp.port = config.port; + } + + ret = nxmb_create(&handle, &mb_config); + if (ret < 0) + { + fprintf(stderr, "Error: failed to create Modbus context: %d\n", ret); + return EXIT_FAILURE; + } + + /* Enable context */ + + ret = nxmb_enable(handle); + if (ret < 0) + { + fprintf(stderr, "Error: failed to enable context: %d\n", ret); + nxmb_destroy(handle); + return EXIT_FAILURE; + } + + /* Set timeout */ + + ret = nxmb_set_timeout(handle, config.timeout_ms); + if (ret < 0) + { + fprintf(stderr, "Error: failed to set timeout: %d\n", ret); + nxmb_disable(handle); + nxmb_destroy(handle); + return EXIT_FAILURE; + } + + /* Execute command (with optional polling) */ + + if (config.poll_ms > 0) + { + /* Polling mode */ + + while (g_running) + { + ret = execute_command(handle, config.unit_id, cmd, cmd_argc, + cmd_argv); + if (ret < 0) + { + break; + } + + usleep(config.poll_ms * 1000); + } + } + else + { + /* One-shot mode */ + + ret = execute_command(handle, config.unit_id, cmd, cmd_argc, + cmd_argv); + } + + /* Cleanup */ + + nxmb_disable(handle); + nxmb_destroy(handle); + + return (ret < 0) ? EXIT_FAILURE : EXIT_SUCCESS; +}