From 9d5d17fc7cfd5c8f50d36d52c142f555382832aa Mon Sep 17 00:00:00 2001 From: yoyixms <21124824+yoyixms@users.noreply.github.com> Date: Sat, 11 Jul 2026 23:46:37 +0200 Subject: [PATCH] modbus-rtu: accept a configurable set of slave ids modbus_rtu_set_slave_filter() lets one context serve several units on the same bus; _modbus_rtu_check_integrity() consults a 256-bit accept bitmap and broadcast is always accepted --- .gitignore | 1 + docs/index.md | 3 + docs/modbus_rtu_set_slave_filter.md | 61 ++++++ src/modbus-rtu-private.h | 6 + src/modbus-rtu.c | 73 ++++++- src/modbus-rtu.h | 3 + tests/Makefile.am | 6 +- tests/unit-test-rtu-slave-filter.c | 289 ++++++++++++++++++++++++++++ 8 files changed, 437 insertions(+), 5 deletions(-) create mode 100644 docs/modbus_rtu_set_slave_filter.md create mode 100644 tests/unit-test-rtu-slave-filter.c diff --git a/.gitignore b/.gitignore index c562d901c..309a606f8 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-rtu-slave-filter tests/unit-test-server tests/version tests/stamp-h2 diff --git a/docs/index.md b/docs/index.md index 6308d10ef..0d1dd9e2d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -92,6 +92,9 @@ You can tweak the serial mode with the following functions: - [modbus_rtu_get_rts_delay](modbus_rtu_get_rts_delay.md) - [modbus_rtu_set_rts_delay](modbus_rtu_set_rts_delay.md) +To serve several slaves from a single RTU context, use +[modbus_rtu_set_slave_filter](modbus_rtu_set_slave_filter.md). + ### TCP (IPv4) Context The TCP backend implements a Modbus variant used for communications over diff --git a/docs/modbus_rtu_set_slave_filter.md b/docs/modbus_rtu_set_slave_filter.md new file mode 100644 index 000000000..c2d5f66ce --- /dev/null +++ b/docs/modbus_rtu_set_slave_filter.md @@ -0,0 +1,61 @@ +# modbus_rtu_set_slave_filter + +## Name + +modbus_rtu_set_slave_filter - set the unit identifiers accepted in RTU slave mode + +## Synopsis + +```c +int modbus_rtu_set_slave_filter(modbus_t *ctx, const uint8_t *slaves, unsigned int count); +``` + +## Description + +The *modbus_rtu_set_slave_filter()* function shall configure the set of Modbus +unit identifiers accepted by an RTU context in slave mode. By default an RTU +context only handles indications addressed to the single slave set with +[modbus_set_slave](modbus_set_slave.md); a filter lets one context serve several +slaves on the same serial link, each typically backed by its own data mapping. + +*slaves* points to an array of *count* unit identifiers to accept. The broadcast +address is always accepted regardless of the filter. Passing a NULL *slaves* or +a *count* of 0 clears the filter and restores the default single-slave +behaviour. + +When a filter is set, an indication addressed to a unit identifier outside the +set is ignored (*modbus_receive()* returns 0) and the context keeps listening +for the next indication. + +## Return value + +The *modbus_rtu_set_slave_filter()* function shall return 0 if successful. +Otherwise it shall return -1 and set errno to EINVAL if *ctx* is not an RTU +context or if a unit identifier is out of range. + +## Example + +```c +ctx = modbus_new_rtu("/dev/ttyUSB0", 9600, 'N', 8, 1); + +/* Serve unit identifiers 5, 6 and 7 from this context. */ +uint8_t slaves[] = {5, 6, 7}; +modbus_rtu_set_slave_filter(ctx, slaves, 3); + +modbus_connect(ctx); + +for (;;) { + uint8_t req[MODBUS_RTU_MAX_ADU_LENGTH]; + int rc = modbus_receive(ctx, req); + if (rc > 0) { + modbus_reply(ctx, req, rc, mapping_for(req[0])); + } +} +``` + +## See also + +- [modbus_set_slave](modbus_set_slave.md) +- [modbus_new_rtu](modbus_new_rtu.md) +- [modbus_receive](modbus_receive.md) +- [modbus_reply](modbus_reply.md) diff --git a/src/modbus-rtu-private.h b/src/modbus-rtu-private.h index bf6781662..4a3b8a945 100644 --- a/src/modbus-rtu-private.h +++ b/src/modbus-rtu-private.h @@ -82,6 +82,12 @@ typedef struct _modbus_rtu { #endif /* To handle many slaves on the same link */ int confirmation_to_ignore; + /* Multi-slave mode: when slave_filter_active is set, an indication is + * accepted if its unit identifier has its bit set in slave_filter (a + * 256-bit bitmap). Otherwise only ctx->slave (and broadcast) is accepted, + * which preserves the historical single-slave behaviour. */ + int slave_filter_active; + uint8_t slave_filter[32]; } modbus_rtu_t; #endif /* MODBUS_RTU_PRIVATE_H */ diff --git a/src/modbus-rtu.c b/src/modbus-rtu.c index 87e08874e..ad7934b59 100644 --- a/src/modbus-rtu.c +++ b/src/modbus-rtu.c @@ -331,8 +331,12 @@ static int _modbus_rtu_receive(modbus_t *ctx, uint8_t *req) } } else { rc = _modbus_receive_msg(ctx, req, MSG_INDICATION); - if (rc == 0) { - /* The next expected message is a confirmation to ignore */ + if (rc == 0 && !ctx_rtu->slave_filter_active) { + /* An indication for another slave was ignored: on a shared bus the + * addressed slave will answer, so the next message is a + * confirmation to ignore. A multi-slave server (slave filter set) + * is the authoritative responder for its unit ids and keeps + * listening for the next indication instead. */ ctx_rtu->confirmation_to_ignore = TRUE; } } @@ -371,6 +375,20 @@ static int _modbus_rtu_pre_check_confirmation(modbus_t *ctx, } } +/* Returns TRUE if an indication addressed to unit identifier `slave` must be + handled by this context. With a slave filter set (multi-slave mode) the + decision is a bitmap lookup; otherwise only ctx->slave matches. Broadcast is + handled by the caller. */ +static int _modbus_rtu_slave_accepted(modbus_t *ctx, int slave) +{ + modbus_rtu_t *ctx_rtu = ctx->backend_data; + + if (ctx_rtu->slave_filter_active) { + return (ctx_rtu->slave_filter[slave / 8] >> (slave % 8)) & 1; + } + return slave == ctx->slave; +} + /* The check_crc16 function shall return 0 if the message is ignored and the message length if the CRC is valid. Otherwise it shall return -1 and set errno to EMBBADCRC. */ @@ -400,9 +418,9 @@ static int _modbus_rtu_check_integrity(modbus_t *ctx, uint8_t *msg, const int ms } /* Filter on the Modbus unit identifier (slave) in RTU mode */ - if (slave != ctx->slave && slave != MODBUS_BROADCAST_ADDRESS) { + if (slave != MODBUS_BROADCAST_ADDRESS && !_modbus_rtu_slave_accepted(ctx, slave)) { if (ctx->debug) { - printf("Request for slave %d ignored (not %d)\n", slave, ctx->slave); + printf("Request for slave %d ignored\n", slave); } /* Following call to check_confirmation handles this error */ return 0; @@ -1025,6 +1043,48 @@ int modbus_rtu_get_serial_mode(modbus_t *ctx) } } +int modbus_rtu_set_slave_filter(modbus_t *ctx, + const uint8_t *slaves, + unsigned int count) +{ + modbus_rtu_t *ctx_rtu; + int max_slave; + unsigned int i; + + if (ctx == NULL || ctx->backend->backend_type != _MODBUS_BACKEND_TYPE_RTU) { + errno = EINVAL; + return -1; + } + + ctx_rtu = ctx->backend_data; + + /* A NULL list or a count of 0 clears the filter and restores the default + behaviour (only ctx->slave and broadcast are accepted). */ + if (slaves == NULL || count == 0) { + ctx_rtu->slave_filter_active = FALSE; + memset(ctx_rtu->slave_filter, 0, sizeof(ctx_rtu->slave_filter)); + return 0; + } + + /* Validate every id before mutating so a bad entry leaves the filter + unchanged. */ + max_slave = (ctx->quirks & MODBUS_QUIRK_MAX_SLAVE) ? 255 : 247; + for (i = 0; i < count; i++) { + if (slaves[i] > max_slave) { + errno = EINVAL; + return -1; + } + } + + memset(ctx_rtu->slave_filter, 0, sizeof(ctx_rtu->slave_filter)); + for (i = 0; i < count; i++) { + ctx_rtu->slave_filter[slaves[i] / 8] |= (uint8_t) (1 << (slaves[i] % 8)); + } + ctx_rtu->slave_filter_active = TRUE; + + return 0; +} + int modbus_rtu_get_rts(modbus_t *ctx) { if (ctx == NULL) { @@ -1395,5 +1455,10 @@ modbus_new_rtu(const char *device, int baud, char parity, int data_bit, int stop ctx_rtu->confirmation_to_ignore = FALSE; + /* No slave filter by default: only ctx->slave is accepted in slave mode + * (see modbus_rtu_set_slave_filter for multi-slave servers). */ + ctx_rtu->slave_filter_active = FALSE; + memset(ctx_rtu->slave_filter, 0, sizeof(ctx_rtu->slave_filter)); + return ctx; } diff --git a/src/modbus-rtu.h b/src/modbus-rtu.h index 8e89e7304..80e6d53af 100644 --- a/src/modbus-rtu.h +++ b/src/modbus-rtu.h @@ -25,6 +25,9 @@ modbus_new_rtu(const char *device, int baud, char parity, int data_bit, int stop MODBUS_API int modbus_rtu_set_serial_mode(modbus_t *ctx, int mode); MODBUS_API int modbus_rtu_get_serial_mode(modbus_t *ctx); +MODBUS_API int +modbus_rtu_set_slave_filter(modbus_t *ctx, const uint8_t *slaves, unsigned int count); + #define MODBUS_RTU_RTS_NONE 0 #define MODBUS_RTU_RTS_UP 1 #define MODBUS_RTU_RTS_DOWN 2 diff --git a/tests/Makefile.am b/tests/Makefile.am index 45d0f8e72..1315269c8 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-rtu-slave-filter \ 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_rtu_slave_filter_SOURCES = unit-test-rtu-slave-filter.c +unit_test_rtu_slave_filter_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-rtu-slave-filter diff --git a/tests/unit-test-rtu-slave-filter.c b/tests/unit-test-rtu-slave-filter.c new file mode 100644 index 000000000..5a57f241f --- /dev/null +++ b/tests/unit-test-rtu-slave-filter.c @@ -0,0 +1,289 @@ +/* + * Copyright © Stéphane Raimbault + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +/* Regression test for the RTU multi-slave accept filter + * (modbus_rtu_set_slave_filter). The API validation runs everywhere; the + * receive-filtering checks drive modbus_receive() over a socketpair used as a + * fake serial link and therefore run on POSIX only — on Windows the RTU backend + * does its I/O through a serial HANDLE rather than ctx->s. */ + +#include +#include +#include +#include +#include + +#ifndef _WIN32 +#include +#include +#include +#endif + +#include + +/* Standard Modbus RTU CRC-16 (polynomial 0xA001, init 0xFFFF). */ +static uint16_t rtu_crc16(const uint8_t *buf, int len) +{ + uint16_t crc = 0xFFFF; + for (int i = 0; i < len; i++) { + crc ^= (uint16_t) buf[i]; + for (int j = 0; j < 8; j++) { + if (crc & 1) + crc = (crc >> 1) ^ 0xA001; + else + crc >>= 1; + } + } + return crc; +} + +/* Build a "read holding registers" indication addressed to `slave`. + * Returns the frame length. */ +static int build_read_request(uint8_t *frame, int slave) +{ + frame[0] = (uint8_t) slave; + frame[1] = 0x03; /* FC read holding registers */ + frame[2] = 0x00; + frame[3] = 0x00; /* address 0 */ + frame[4] = 0x00; + frame[5] = 0x01; /* quantity 1 */ + uint16_t crc = rtu_crc16(frame, 6); + frame[6] = (uint8_t) (crc & 0xFF); /* RTU sends the CRC low byte first */ + frame[7] = (uint8_t) (crc >> 8); + return 8; +} + +static void test_api_validation(void) +{ + printf("[1] set_slave_filter API validation... "); + fflush(stdout); + + /* NULL context is rejected. */ + errno = 0; + assert(modbus_rtu_set_slave_filter(NULL, NULL, 0) == -1 && errno == EINVAL); + + /* A non-RTU context is rejected. */ + modbus_t *tcp = modbus_new_tcp("127.0.0.1", 1502); + assert(tcp); + uint8_t ids[] = {5}; + errno = 0; + assert(modbus_rtu_set_slave_filter(tcp, ids, 1) == -1 && errno == EINVAL); + modbus_free(tcp); + + modbus_t *ctx = modbus_new_rtu("/dev/null", 9600, 'N', 8, 1); + assert(ctx); + + /* An out-of-range unit id is rejected and leaves the filter unchanged. */ + uint8_t bad[] = {248}; + errno = 0; + assert(modbus_rtu_set_slave_filter(ctx, bad, 1) == -1 && errno == EINVAL); + + /* A valid set is accepted, and a NULL/empty list clears it. */ + uint8_t good[] = {1, 5, 247}; + assert(modbus_rtu_set_slave_filter(ctx, good, 3) == 0); + assert(modbus_rtu_set_slave_filter(ctx, NULL, 0) == 0); + + /* An empty list is also accepted through a non-NULL pointer. */ + assert(modbus_rtu_set_slave_filter(ctx, good, 0) == 0); + + modbus_free(ctx); + printf("PASS\n"); +} + +static void test_max_slave_quirk(void) +{ + printf("[2] set_slave_filter MAX_SLAVE quirk... "); + fflush(stdout); + + modbus_t *ctx = modbus_new_rtu("/dev/null", 9600, 'N', 8, 1); + assert(ctx); + + /* Ids above 247 are reserved by the Modbus specification and rejected. */ + uint8_t reserved[] = {248, 255}; + errno = 0; + assert(modbus_rtu_set_slave_filter(ctx, reserved, 2) == -1 && errno == EINVAL); + + /* MODBUS_QUIRK_MAX_SLAVE raises the accepted range to the full byte. */ + assert(modbus_enable_quirks(ctx, MODBUS_QUIRK_MAX_SLAVE) == 0); + assert(modbus_rtu_set_slave_filter(ctx, reserved, 2) == 0); + + /* Disabling it restores the specification limit. */ + assert(modbus_disable_quirks(ctx, MODBUS_QUIRK_MAX_SLAVE) == 0); + errno = 0; + assert(modbus_rtu_set_slave_filter(ctx, reserved, 2) == -1 && errno == EINVAL); + + /* A rejected list leaves the previously accepted filter in place. */ + uint8_t mixed[] = {5, 248}; + errno = 0; + assert(modbus_rtu_set_slave_filter(ctx, mixed, 2) == -1 && errno == EINVAL); + + modbus_free(ctx); + printf("PASS\n"); +} + +#ifndef _WIN32 +/* Write a request for `slave` to the master end, then receive it on the server + * context. Returns the modbus_receive() result (>0 accepted, 0 ignored). */ +static int receive_for_slave(modbus_t *srv, int fd_master, int slave) +{ + uint8_t frame[8]; + int len = build_read_request(frame, slave); + assert(write(fd_master, frame, len) == len); + + uint8_t req[MODBUS_RTU_MAX_ADU_LENGTH]; + return modbus_receive(srv, req); +} + +static void test_receive_filtering(void) +{ + printf("[3] RTU receive filtering... "); + fflush(stdout); + + int fds[2]; + assert(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0); + + modbus_t *srv = modbus_new_rtu("/dev/null", 9600, 'N', 8, 1); + assert(srv); + modbus_set_socket(srv, fds[1]); + /* A bounded timeout so a wrongly-ignored frame fails instead of hanging. */ + modbus_set_response_timeout(srv, 1, 0); + + /* Multi-slave: accept unit ids 5, 10 and 247 only. The last one exercises + the top of the bitmap (byte 30, bit 7). */ + uint8_t ids[] = {5, 10, 247}; + assert(modbus_rtu_set_slave_filter(srv, ids, 3) == 0); + + /* The filter replaces ctx->slave, it does not extend it: an id set with + modbus_set_slave() but absent from the filter is ignored. */ + modbus_set_slave(srv, 3); + + assert(receive_for_slave(srv, fds[0], 5) > 0); /* accepted */ + assert(receive_for_slave(srv, fds[0], 10) > 0); /* accepted */ + assert(receive_for_slave(srv, fds[0], 247) > 0); /* accepted, high bit */ + assert(receive_for_slave(srv, fds[0], 7) == 0); /* ignored */ + assert(receive_for_slave(srv, fds[0], 3) == 0); /* ctx->slave, ignored */ + + /* Broadcast is always accepted, even with a filter set. */ + assert(receive_for_slave(srv, fds[0], MODBUS_BROADCAST_ADDRESS) > 0); + + /* A filtered server is the authoritative responder for its own ids, so an + ignored indication must not arm the confirmation-to-ignore state: the + very next indication has to be delivered rather than swallowed. */ + assert(receive_for_slave(srv, fds[0], 7) == 0); /* ignored */ + assert(receive_for_slave(srv, fds[0], 5) > 0); /* still delivered */ + + /* Clearing the filter restores single-slave behaviour on ctx->slave. */ + assert(modbus_rtu_set_slave_filter(srv, NULL, 0) == 0); + modbus_set_slave(srv, 17); + assert(receive_for_slave(srv, fds[0], 17) > 0); /* ctx->slave */ + assert(receive_for_slave(srv, fds[0], 5) == 0); /* no longer accepted */ + + modbus_free(srv); + close(fds[0]); + close(fds[1]); + printf("PASS\n"); +} + +/* Read whatever the server put on the link, up to `ms` milliseconds. */ +static int read_response(int fd, uint8_t *buf, int len, int ms) +{ + fd_set rset; + struct timeval tv; + ssize_t n; + + FD_ZERO(&rset); + FD_SET(fd, &rset); + tv.tv_sec = ms / 1000; + tv.tv_usec = (ms % 1000) * 1000; + if (select(fd + 1, &rset, NULL, NULL, &tv) <= 0) + return 0; + + n = read(fd, buf, len); + return n < 0 ? 0 : (int) n; +} + +/* A server answering for several unit ids must reply as the addressed unit + * rather than as ctx->slave, and must leave the link silent for an id it does + * not serve. */ +static void test_reply_addressing(void) +{ + printf("[4] multi-slave reply addressing... "); + fflush(stdout); + + int fds[2]; + assert(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0); + + modbus_t *srv = modbus_new_rtu("/dev/null", 9600, 'N', 8, 1); + assert(srv); + modbus_set_socket(srv, fds[1]); + modbus_set_response_timeout(srv, 1, 0); + + modbus_mapping_t *map = modbus_mapping_new(0, 0, 10, 0); + assert(map); + for (int i = 0; i < 10; i++) + map->tab_registers[i] = (uint16_t) (900 + i); + + uint8_t ids[] = {5, 10}; + assert(modbus_rtu_set_slave_filter(srv, ids, 2) == 0); + /* Set on purpose to an id outside the filter. */ + modbus_set_slave(srv, 3); + + uint8_t req[MODBUS_RTU_MAX_ADU_LENGTH]; + uint8_t rsp[MODBUS_RTU_MAX_ADU_LENGTH]; + uint8_t frame[8]; + int len; + int rc; + int n; + + for (unsigned int i = 0; i < sizeof(ids); i++) { + len = build_read_request(frame, ids[i]); + assert(write(fds[0], frame, len) == len); + + rc = modbus_receive(srv, req); + assert(rc > 0); + assert(modbus_reply(srv, req, rc, map) > 0); + + n = read_response(fds[0], rsp, sizeof(rsp), 1000); + assert(n >= 5); + /* The response carries the addressed unit, not ctx->slave. */ + assert(rsp[0] == ids[i]); + + uint16_t crc = rtu_crc16(rsp, n - 2); + assert(rsp[n - 2] == (uint8_t) (crc & 0xFF)); + assert(rsp[n - 1] == (uint8_t) (crc >> 8)); + } + + /* An id outside the filter is ignored and nothing reaches the link. */ + len = build_read_request(frame, 7); + assert(write(fds[0], frame, len) == len); + assert(modbus_receive(srv, req) == 0); + assert(read_response(fds[0], rsp, sizeof(rsp), 200) == 0); + + modbus_mapping_free(map); + modbus_free(srv); + close(fds[0]); + close(fds[1]); + printf("PASS\n"); +} +#endif + +int main(void) +{ + printf("=== modbus RTU multi-slave filter tests ===\n"); + + test_api_validation(); + test_max_slave_quirk(); +#ifndef _WIN32 + test_receive_filtering(); + test_reply_addressing(); +#else + printf("[3] RTU receive filtering... SKIP (no ctx->s serial link on Windows)\n"); + printf("[4] multi-slave reply addressing... SKIP (no ctx->s serial link on Windows)\n"); +#endif + + printf("All tests passed.\n"); + return EXIT_SUCCESS; +}