From 2703c0dc491dfb9d994e7262b09ff173ad064bd5 Mon Sep 17 00:00:00 2001 From: yoyixms <21124824+yoyixms@users.noreply.github.com> Date: Sat, 11 Jul 2026 23:53:42 +0200 Subject: [PATCH] modbus: add modbus_reply_router for per-slave dispatch answers an indication with the mapping returned by a resolver callback for the addressed unit id; modbus_get_request_slave() extracts it, and an unserved unit gets a gateway path exception --- .gitignore | 1 + docs/index.md | 2 + docs/modbus_get_request_slave.md | 28 +++++ docs/modbus_reply_router.md | 57 +++++++++ src/modbus.c | 46 +++++++ src/modbus.h | 9 ++ tests/Makefile.am | 6 +- tests/unit-test-reply-router.c | 210 +++++++++++++++++++++++++++++++ 8 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 docs/modbus_get_request_slave.md create mode 100644 docs/modbus_reply_router.md create mode 100644 tests/unit-test-reply-router.c diff --git a/.gitignore b/.gitignore index c562d901c..44b7091cf 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ tests/random-test-server tests/proxy-test-client tests/proxy-test-server tests/unit-test-client +tests/unit-test-reply-router tests/unit-test-server tests/version tests/stamp-h2 diff --git a/docs/index.md b/docs/index.md index 6308d10ef..b0d07b6f2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -178,11 +178,13 @@ Data mapping: Receive: - [modbus_receive](modbus_receive.md) +- [modbus_get_request_slave](modbus_get_request_slave.md) Reply: - [modbus_reply](modbus_reply.md) - [modbus_reply_exception](modbus_reply_exception.md) +- [modbus_reply_router](modbus_reply_router.md) Proxy: diff --git a/docs/modbus_get_request_slave.md b/docs/modbus_get_request_slave.md new file mode 100644 index 000000000..13042c5a2 --- /dev/null +++ b/docs/modbus_get_request_slave.md @@ -0,0 +1,28 @@ +# modbus_get_request_slave + +## Name + +modbus_get_request_slave - get the unit identifier addressed by a request + +## Synopsis + +```c +int modbus_get_request_slave(modbus_t *ctx, const uint8_t *req); +``` + +## Description + +The *modbus_get_request_slave()* function shall return the Modbus unit identifier +(slave) addressed by the request or indication *req*. The identifier is located +just before the function code, at the end of the backend header (offset 0 in RTU, +6 in TCP). This is useful to dispatch an indication to a per-slave data mapping. + +## Return value + +The *modbus_get_request_slave()* function shall return the unit identifier. +Otherwise it shall return -1 and set errno to EINVAL if *ctx* or *req* is NULL. + +## See also + +- [modbus_reply_router](modbus_reply_router.md) +- [modbus_receive](modbus_receive.md) diff --git a/docs/modbus_reply_router.md b/docs/modbus_reply_router.md new file mode 100644 index 000000000..56b08dd07 --- /dev/null +++ b/docs/modbus_reply_router.md @@ -0,0 +1,57 @@ +# modbus_reply_router + +## Name + +modbus_reply_router - reply to an indication using a per-slave data mapping + +## Synopsis + +```c +typedef modbus_mapping_t *(*modbus_mapping_resolver_t)(int slave, void *user); + +int modbus_reply_router(modbus_t *ctx, const uint8_t *req, int req_length, modbus_mapping_resolver_t resolve, void *user); +``` + +## Description + +The *modbus_reply_router()* function shall answer the indication *req* of length +*req_length* using the data mapping returned by the *resolve* callback for the +addressed unit identifier. It is a convenience over +[modbus_reply](modbus_reply.md) for a server that handles several slaves on one +context, each backed by its own mapping. + +The *resolve* callback receives the unit identifier and the opaque *user* pointer +passed to *modbus_reply_router()*, and shall return the *modbus_mapping_t* serving +that slave, or NULL if the slave is not served. When it returns NULL, a gateway +path exception (MODBUS_EXCEPTION_GATEWAY_PATH) is sent so the client learns the +unit is unavailable. + +## Return value + +The *modbus_reply_router()* function shall return the length of the response sent +if successful. Otherwise it shall return -1 and set errno. + +## Example + +```c +static modbus_mapping_t *resolve(int slave, void *user) +{ + modbus_mapping_t **maps = user; /* indexed by unit id */ + return maps[slave]; +} + +for (;;) { + uint8_t req[MODBUS_TCP_MAX_ADU_LENGTH]; + int rc = modbus_receive(ctx, req); + if (rc > 0) { + modbus_reply_router(ctx, req, rc, resolve, maps); + } +} +``` + +## See also + +- [modbus_reply](modbus_reply.md) +- [modbus_get_request_slave](modbus_get_request_slave.md) +- [modbus_mapping_new](modbus_mapping_new.md) +- [modbus_receive](modbus_receive.md) diff --git a/src/modbus.c b/src/modbus.c index abdd718c4..9c770fd86 100644 --- a/src/modbus.c +++ b/src/modbus.c @@ -1297,6 +1297,52 @@ int modbus_reply_exception(modbus_t *ctx, const uint8_t *req, unsigned int excep } } +/* Return the unit identifier (slave) addressed by a request or indication. + The identifier sits just before the function code, at the end of the backend + header (offset 0 in RTU, 6 in TCP). */ +int modbus_get_request_slave(modbus_t *ctx, const uint8_t *req) +{ + if (ctx == NULL || req == NULL) { + errno = EINVAL; + return -1; + } + + return req[ctx->backend->header_length - 1]; +} + +/* Reply to an indication using the mapping returned by `resolve` for the + addressed unit identifier. This is a convenience over modbus_reply() for a + server that handles several slaves, each with its own mapping. When `resolve` + returns NULL, a gateway path exception is sent so the client learns the unit + is unavailable. */ +int modbus_reply_router(modbus_t *ctx, + const uint8_t *req, + int req_length, + modbus_mapping_resolver_t resolve, + void *user) +{ + int slave; + modbus_mapping_t *mb_mapping; + + if (ctx == NULL || req == NULL || resolve == NULL) { + errno = EINVAL; + return -1; + } + + if (req_length < (int) (ctx->backend->header_length + 1)) { + errno = EMBBADDATA; + return -1; + } + + slave = req[ctx->backend->header_length - 1]; + mb_mapping = resolve(slave, user); + if (mb_mapping == NULL) { + return modbus_reply_exception(ctx, req, MODBUS_EXCEPTION_GATEWAY_PATH); + } + + return modbus_reply(ctx, req, req_length, mb_mapping); +} + /* Forward a request received on one context to another and relay the response back. This function is useful to implement a Modbus gateway/proxy that bridges two different backends (eg. TCP to RTU). */ diff --git a/src/modbus.h b/src/modbus.h index 237d3e11f..65ce6fe3b 100644 --- a/src/modbus.h +++ b/src/modbus.h @@ -276,6 +276,15 @@ MODBUS_API int modbus_reply(modbus_t *ctx, modbus_mapping_t *mb_mapping); MODBUS_API int modbus_reply_exception(modbus_t *ctx, const uint8_t *req, unsigned int exception_code); + +/* Resolve the data mapping serving a unit identifier, or NULL if none. */ +typedef modbus_mapping_t *(*modbus_mapping_resolver_t)(int slave, void *user); +MODBUS_API int modbus_reply_router(modbus_t *ctx, + const uint8_t *req, + int req_length, + modbus_mapping_resolver_t resolve, + void *user); +MODBUS_API int modbus_get_request_slave(modbus_t *ctx, const uint8_t *req); MODBUS_API int modbus_proxy(modbus_t *frontend_ctx, modbus_t *backend_ctx, const uint8_t *req, diff --git a/tests/Makefile.am b/tests/Makefile.am index 45d0f8e72..b804aa126 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -8,6 +8,7 @@ noinst_PROGRAMS = \ random-test-client \ unit-test-server \ unit-test-client \ + unit-test-reply-router \ proxy-test-server \ proxy-test-client \ version @@ -36,6 +37,9 @@ unit_test_server_LDADD = $(common_ldflags) unit_test_client_SOURCES = unit-test-client.c unit-test.h unit_test_client_LDADD = $(common_ldflags) +unit_test_reply_router_SOURCES = unit-test-reply-router.c +unit_test_reply_router_LDADD = $(common_ldflags) + proxy_test_server_SOURCES = proxy-test-server.c proxy_test_server_LDADD = $(common_ldflags) @@ -57,4 +61,4 @@ AM_CFLAGS = $(LIBMODBUSCFLAGS) $(WARNING_CFLAGS) CLEANFILES = *~ *.log noinst_SCRIPTS=unit-tests.sh -TESTS=./unit-tests.sh +TESTS=./unit-tests.sh unit-test-reply-router diff --git a/tests/unit-test-reply-router.c b/tests/unit-test-reply-router.c new file mode 100644 index 000000000..67b585ea0 --- /dev/null +++ b/tests/unit-test-reply-router.c @@ -0,0 +1,210 @@ +/* + * Copyright © Stéphane Raimbault + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +/* Regression test for the per-slave reply router (modbus_reply_router) and the + * modbus_get_request_slave() helper. The helper and argument checks run + * everywhere; the + * router dispatch drives a TCP client and server over a socketpair with the + * server in a child process, so it runs on POSIX only. */ + +#include +#include +#include +#include +#include + +#ifndef _WIN32 +#include +#include +#include +#endif + +#include + +typedef struct { + modbus_mapping_t *map1; + modbus_mapping_t *map2; +} maps_t; + +/* Serve unit 1 from map1 and unit 2 from map2; anything else is unavailable. */ +static modbus_mapping_t *resolve(int slave, void *user) +{ + maps_t *maps = user; + if (slave == 1) + return maps->map1; + if (slave == 2) + return maps->map2; + return NULL; +} + +static void test_get_request_slave(void) +{ + printf("[1] get_request_slave... "); + fflush(stdout); + + /* TCP header is 7 bytes, so the unit id is at offset 6. */ + modbus_t *tcp = modbus_new_tcp("127.0.0.1", 1502); + assert(tcp); + uint8_t tcp_req[] = {0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 42, 0x03}; + assert(modbus_get_request_slave(tcp, tcp_req) == 42); + + errno = 0; + assert(modbus_get_request_slave(tcp, NULL) == -1 && errno == EINVAL); + modbus_free(tcp); + + /* RTU header is 1 byte, so the unit id is at offset 0. */ + modbus_t *rtu = modbus_new_rtu("/dev/null", 9600, 'N', 8, 1); + assert(rtu); + uint8_t rtu_req[] = {17, 0x03, 0x00, 0x00}; + assert(modbus_get_request_slave(rtu, rtu_req) == 17); + modbus_free(rtu); + + errno = 0; + assert(modbus_get_request_slave(NULL, tcp_req) == -1 && errno == EINVAL); + + printf("PASS\n"); +} + +/* Resolver for the argument checks; the router must fail before calling it. */ +static modbus_mapping_t *resolve_none(int slave, void *user) +{ + (void) slave; + (void) user; + assert(0 && "resolver called on an invalid request"); + return NULL; +} + +static void test_reply_router_validation(void) +{ + printf("[2] reply_router argument validation... "); + fflush(stdout); + + modbus_t *ctx = modbus_new_tcp("127.0.0.1", 1502); + assert(ctx); + + /* A TCP indication: 7 header bytes then the function code. */ + uint8_t req[] = {0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 1, 0x03}; + + errno = 0; + assert(modbus_reply_router(NULL, req, (int) sizeof(req), resolve_none, NULL) == -1 && + errno == EINVAL); + errno = 0; + assert(modbus_reply_router(ctx, NULL, (int) sizeof(req), resolve_none, NULL) == -1 && + errno == EINVAL); + errno = 0; + assert(modbus_reply_router(ctx, req, (int) sizeof(req), NULL, NULL) == -1 && + errno == EINVAL); + + /* A request truncated before the function code is rejected without asking + the resolver. */ + errno = 0; + assert(modbus_reply_router(ctx, req, 7, resolve_none, NULL) == -1 && + errno == EMBBADDATA); + + modbus_free(ctx); + printf("PASS\n"); +} + +#ifndef _WIN32 +static void serve_requests(int fd, int n) +{ + maps_t maps; + maps.map1 = modbus_mapping_new(0, 0, 10, 0); + maps.map2 = modbus_mapping_new(0, 0, 10, 0); + assert(maps.map1 && maps.map2); + for (int i = 0; i < 10; i++) { + maps.map1->tab_registers[i] = (uint16_t) (100 + i); + maps.map2->tab_registers[i] = (uint16_t) (200 + i); + } + + modbus_t *srv = modbus_new_tcp("127.0.0.1", 1502); + assert(srv); + modbus_set_socket(srv, fd); + modbus_set_debug(srv, FALSE); + + /* Every reply is sent, including the gateway path exception raised for the + unserved unit, so the router returns the response length each time. + Failures abort the child and the parent catches them in its exit status. */ + for (int i = 0; i < n; i++) { + uint8_t req[MODBUS_TCP_MAX_ADU_LENGTH]; + int rc = modbus_receive(srv, req); + assert(rc > 0); + assert(modbus_reply_router(srv, req, rc, resolve, &maps) > 0); + } + + modbus_free(srv); + modbus_mapping_free(maps.map1); + modbus_mapping_free(maps.map2); +} + +static void test_reply_router(void) +{ + printf("[3] reply_router dispatch... "); + fflush(stdout); + + int fds[2]; + assert(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0); + + pid_t pid = fork(); + if (pid == 0) { + close(fds[0]); + serve_requests(fds[1], 3); + _exit(0); + } + close(fds[1]); + + modbus_t *cli = modbus_new_tcp("127.0.0.1", 1502); + assert(cli); + modbus_set_socket(cli, fds[0]); + modbus_set_response_timeout(cli, 2, 0); + modbus_set_debug(cli, FALSE); + + uint16_t out[10]; + + /* Unit 1 is served from map1. */ + modbus_set_slave(cli, 1); + assert(modbus_read_registers(cli, 0, 10, out) == 10); + for (int i = 0; i < 10; i++) + assert(out[i] == (uint16_t) (100 + i)); + + /* Unit 2 is served from map2. */ + modbus_set_slave(cli, 2); + assert(modbus_read_registers(cli, 0, 10, out) == 10); + for (int i = 0; i < 10; i++) + assert(out[i] == (uint16_t) (200 + i)); + + /* An unserved unit gets a gateway path exception. */ + modbus_set_slave(cli, 9); + errno = 0; + assert(modbus_read_registers(cli, 0, 10, out) == -1 && errno == EMBXGPATH); + + modbus_free(cli); + close(fds[0]); + + /* The server checks the router return codes itself, so its exit status + carries those results back here. */ + int status; + assert(waitpid(pid, &status, 0) == pid); + assert(WIFEXITED(status) && WEXITSTATUS(status) == 0); + printf("PASS\n"); +} +#endif + +int main(void) +{ + printf("=== modbus reply router tests ===\n"); + + test_get_request_slave(); + test_reply_router_validation(); +#ifndef _WIN32 + test_reply_router(); +#else + printf("[3] reply_router dispatch... SKIP (no fork on Windows)\n"); +#endif + + printf("All tests passed.\n"); + return EXIT_SUCCESS; +}