diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 07282db..0a00f8f 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -2,7 +2,6 @@ name: CMake on: push: - branches: [ "main", "development" ] pull_request: branches: [ "main" ] diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3213389..60b03fb 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,7 +13,7 @@ name: "CodeQL" on: push: - branches: [ "main", "development" ] + branches: [ "main", "release" ] pull_request: # The branches below must be a subset of the branches above branches: [ "main" ] diff --git a/.github/workflows/flatpak_test.yml b/.github/workflows/flatpak_test.yml index 92917dc..baf2af9 100644 --- a/.github/workflows/flatpak_test.yml +++ b/.github/workflows/flatpak_test.yml @@ -1,7 +1,5 @@ on: - push: - branches: [ "main", "development" ] - pull_request: + [ push, pull_request ] name: Flatpak_test jobs: flatpak: @@ -27,4 +25,4 @@ jobs: with: bundle: test_modbus-tcp-client-shm.flatpak manifest-path: network.koesling.test-modbus-tcp-client-shm.yml - cache-key: flatpak-builder-${{ github.sha }} \ No newline at end of file + cache-key: flatpak-builder-${{ github.sha }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b4a73e..e8ba176 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ cmake_minimum_required(VERSION 3.13.4 FATAL_ERROR) # ====================================================================================================================== # project -project(Modbus_TCP_client_shm LANGUAGES CXX VERSION 1.1.1) +project(Modbus_TCP_client_shm LANGUAGES CXX VERSION 1.2.0) # settings set(Target "modbus-tcp-client-shm") # Executable name (without file extension!) diff --git a/docs/index.md b/docs/index.md index 5a7c822..5fe324b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -118,6 +118,30 @@ cmake --build build The binary is located in the build directory. +## Common Problems and Fixes + +### Failed to create Shared Memory +It can happen that the client reports the following error on startup: +``` +Failed to create shared memory ...: File exists +``` +This can be caused by: + - Another modbus client is running that uses the shared memory with the given name. + If you want to run multiple instances simultaneously use the option ```--name-prefix``` to change the name of the shared memory. + - Any other application uses a shared memory with the given name (unlikely but possible) + - A previous instance of a modbus client crashed or was forcefully terminated and was not able to unlink the shared memory. + In this case, the option ```--force``` can be used to force the use of shared memory. + In the other cases this option should not be used. + +### Connection frequently times out + +If the connection frequently times out, it may be reasonable to increase the tcp timeout with the option ```--tcp-timeout```. +It is per default set to 5 seconds. + +The two options ```--byte-timeout``` and ```--response-timeout``` change the timeout behavior of the modbus connection. +These should only be changed by experienced users. +See the [libmodbus documentation](https://libmodbus.org/docs/v3.1.7/) ([byte timeout](https://libmodbus.org/docs/v3.1.7/modbus_set_byte_timeout.html) and [response timeout](https://libmodbus.org/docs/v3.1.7/modbus_set_response_timeout.html)) for more details. + ## Links to related projects ### General Shared Memory Tools diff --git a/src/Modbus_TCP_Slave.cpp b/src/Modbus_TCP_Slave.cpp index 1591ed4..51ac6f8 100644 --- a/src/Modbus_TCP_Slave.cpp +++ b/src/Modbus_TCP_Slave.cpp @@ -16,6 +16,8 @@ #include #include +#include + namespace Modbus { namespace TCP { @@ -29,21 +31,74 @@ Slave::Slave(const std::string &ip, unsigned short port, modbus_mapping_t *mappi throw std::runtime_error("failed to create modbus instance: " + error_msg); } + modbus_mapping_t *mb_mapping; + if (mapping == nullptr) { // create new mapping with the maximum number of registers - this->mapping = modbus_mapping_new(MAX_REGS, MAX_REGS, MAX_REGS, MAX_REGS); - if (this->mapping == nullptr) { + mb_mapping = modbus_mapping_new(MAX_REGS, MAX_REGS, MAX_REGS, MAX_REGS); + if (mb_mapping == nullptr) { const std::string error_msg = modbus_strerror(errno); modbus_free(modbus); throw std::runtime_error("failed to allocate memory: " + error_msg); } - delete_mapping = true; + delete_mapping = mapping; } else { // use the provided mapping object - this->mapping = mapping; - delete_mapping = false; + mb_mapping = mapping; + delete_mapping = nullptr; + } + + // use mapping for all client ids + for (std::size_t i = 0; i < MAX_CLIENT_IDS; ++i) { + this->mappings[i] = mapping; + } + + listen(); + +#ifdef OS_LINUX + if (tcp_timeout) set_tcp_timeout(tcp_timeout); +#else + static_cast(tcp_timeout); +#endif +} + +Slave::Slave(const std::string &ip, unsigned short port, modbus_mapping_t **mappings, std::size_t tcp_timeout) { + // create modbus object + modbus = modbus_new_tcp(ip.c_str(), static_cast(port)); + if (modbus == nullptr) { + const std::string error_msg = modbus_strerror(errno); + throw std::runtime_error("failed to create modbus instance: " + error_msg); } + delete_mapping = nullptr; + + for (std::size_t i = 0; i < MAX_CLIENT_IDS; ++i) { + if (mappings[i] == nullptr) { + if (delete_mapping == nullptr) { + delete_mapping = modbus_mapping_new(MAX_REGS, MAX_REGS, MAX_REGS, MAX_REGS); + + if (delete_mapping == nullptr) { + const std::string error_msg = modbus_strerror(errno); + modbus_free(modbus); + throw std::runtime_error("failed to allocate memory: " + error_msg); + } + } + this->mappings[i] = delete_mapping; + } else { + this->mappings[i] = mappings[i]; + } + } + + listen(); + +#ifdef OS_LINUX + if (tcp_timeout) set_tcp_timeout(tcp_timeout); +#else + static_cast(tcp_timeout); +#endif +} + +void Slave::listen() { // create tcp socket socket = modbus_tcp_listen(modbus, 1); if (socket == -1) { @@ -58,48 +113,47 @@ Slave::Slave(const std::string &ip, unsigned short port, modbus_mapping_t *mappi if (tmp != 0) { throw std::system_error(errno, std::generic_category(), "Failed to set socket option SO_KEEPALIVE"); } +} #ifdef OS_LINUX - if (tcp_timeout) { - // set user timeout (~= timeout for tcp connection) - unsigned user_timeout = static_cast(tcp_timeout) * 1000; - tmp = setsockopt(socket, IPPROTO_TCP, TCP_USER_TIMEOUT, &user_timeout, sizeof(keepalive)); - if (tmp != 0) { - throw std::system_error(errno, std::generic_category(), "Failed to set socket option TCP_USER_TIMEOUT"); - } +void Slave::set_tcp_timeout(std::size_t tcp_timeout) { + // set user timeout (~= timeout for tcp connection) + unsigned user_timeout = static_cast(tcp_timeout) * 1000; + int tmp = setsockopt(socket, IPPROTO_TCP, TCP_USER_TIMEOUT, &user_timeout, sizeof(tcp_timeout)); + if (tmp != 0) { + throw std::system_error(errno, std::generic_category(), "Failed to set socket option TCP_USER_TIMEOUT"); + } - // start sending keepalive request after one second without request - unsigned keepidle = 1; - tmp = setsockopt(socket, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle)); - if (tmp != 0) { - throw std::system_error(errno, std::generic_category(), "Failed to set socket option TCP_KEEPIDLE"); - } + // start sending keepalive request after one second without request + unsigned keepidle = 1; + tmp = setsockopt(socket, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle)); + if (tmp != 0) { + throw std::system_error(errno, std::generic_category(), "Failed to set socket option TCP_KEEPIDLE"); + } - // send up to 5 keepalive requests during the timeout time, but not more than one per second - unsigned keepintvl = std::max(static_cast(tcp_timeout / 5), 1u); - tmp = setsockopt(socket, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl)); - if (tmp != 0) { - throw std::system_error(errno, std::generic_category(), "Failed to set socket option TCP_KEEPINTVL"); - } + // send up to 5 keepalive requests during the timeout time, but not more than one per second + unsigned keepintvl = std::max(static_cast(tcp_timeout / 5), 1u); + tmp = setsockopt(socket, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl)); + if (tmp != 0) { + throw std::system_error(errno, std::generic_category(), "Failed to set socket option TCP_KEEPINTVL"); + } - // 5 keepalive requests if the timeout time is >= 5s; else send one request each second - unsigned keepcnt = std::min(static_cast(tcp_timeout), 5u); - tmp = setsockopt(socket, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt)); - if (tmp != 0) { - throw std::system_error(errno, std::generic_category(), "Failed to set socket option TCP_KEEPCNT"); - } + // 5 keepalive requests if the timeout time is >= 5s; else send one request each second + unsigned keepcnt = std::min(static_cast(tcp_timeout), 5u); + tmp = setsockopt(socket, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt)); + if (tmp != 0) { + throw std::system_error(errno, std::generic_category(), "Failed to set socket option TCP_KEEPCNT"); } -#else - static_cast(tcp_timeout); -#endif } +#endif + Slave::~Slave() { if (modbus != nullptr) { modbus_close(modbus); modbus_free(modbus); } - if (mapping != nullptr && delete_mapping) modbus_mapping_free(mapping); + if (delete_mapping) modbus_mapping_free(delete_mapping); if (socket != -1) { close(socket); } } @@ -141,6 +195,11 @@ bool Slave::handle_request() { int rc = modbus_receive(modbus, query); if (rc > 0) { + const auto CLIENT_ID = query[6]; + + // get mapping + auto mapping = mappings[CLIENT_ID]; + // handle request int ret = modbus_reply(modbus, query, rc, mapping); if (ret == -1) { diff --git a/src/Modbus_TCP_Slave.hpp b/src/Modbus_TCP_Slave.hpp index 2b88b04..0d489c4 100644 --- a/src/Modbus_TCP_Slave.hpp +++ b/src/Modbus_TCP_Slave.hpp @@ -7,30 +7,49 @@ #include #include +#include namespace Modbus { namespace TCP { +constexpr std::size_t MAX_CLIENT_IDS = 256; + //! Modbus TCP slave class Slave { private: - modbus_t *modbus; //!< modbus object (see libmodbus library) - modbus_mapping_t *mapping; //!< modbus data object (see libmodbus library) - bool delete_mapping; //!< indicates whether the mapping object was created by this instance - int socket = -1; //!< socket of the modbus connection + modbus_t *modbus; //!< modbus object (see libmodbus library) + modbus_mapping_t + *mappings[MAX_CLIENT_IDS]; //!< modbus data objects (one per possible client id) (see libmodbus library) + modbus_mapping_t *delete_mapping; //!< contains a pointer to a mapping that is to be deleted + int socket = -1; //!< socket of the modbus connection public: /*! \brief create modbus slave (TCP server) * * @param ip ip to listen for incoming connections (default 0.0.0.0 (any)) * @param port port to listen for incoming connections (default 502) - * @param mapping modbus mapping object (nullptr: an mapping object with maximum size is generated) + * @param mapping modbus mapping object for all client ids + * nullptr: an mapping object with maximum size is generated + * @param tcp_timeout tcp timeout (currently only available on linux systems) */ explicit Slave(const std::string &ip = "0.0.0.0", short unsigned int port = 502, modbus_mapping_t *mapping = nullptr, std::size_t tcp_timeout = 5); + /** + * @brief create modbus slave (TCP server) with dedicated mappings per client id + * + * @param ip ip to listen for incoming connections + * @param port port to listen for incoming connections + * @param mappings modbus mappings (one for each possible id) + * @param tcp_timeout tcp timeout (currently only available on linux systems) + */ + Slave(const std::string &ip, + short unsigned int port, + modbus_mapping_t *mappings[MAX_CLIENT_IDS], + std::size_t tcp_timeout = 5); + /*! \brief destroy the modbus slave * */ @@ -89,6 +108,13 @@ class Slave { * @return socket of the modbus connection */ [[nodiscard]] int get_socket() const noexcept { return socket; } + +private: +#ifdef OS_LINUX + void set_tcp_timeout(std::size_t tcp_timeout); +#endif + + void listen(); }; } // namespace TCP diff --git a/src/main.cpp b/src/main.cpp index 331aa32..d471067 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,6 +9,7 @@ #include #include #include +#include // cxxopts, but all warnings disabled #ifdef COMPILER_CLANG @@ -48,6 +49,17 @@ static void sig_term_handler(int) { terminate = true; } +constexpr std::array TERM_SIGNALS = {SIGINT, + SIGTERM, + SIGHUP, + SIGIO, // should not happen + SIGPIPE, + SIGPOLL, // should not happen + SIGPROF, // should not happen + SIGUSR1, + SIGUSR2, + SIGVTALRM}; + /*! \brief main function * * @param argc number of arguments @@ -66,16 +78,24 @@ int main(int argc, char **argv) { auto euid = geteuid(); if (!euid) std::cerr << "!!!! WARNING: You should not execute this program with root privileges !!!!" << std::endl; +#ifdef COMPILER_CLANG +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wdisabled-macro-expansion" +#endif // establish signal handler - if (signal(SIGINT, sig_term_handler) || signal(SIGTERM, sig_term_handler)) { - perror("Failed to establish signal handler"); - return EX_OSERR; - } - - if (signal(SIGALRM, [](int) { exit(EX_OK); })) { - perror("Failed to establish signal handler"); - return EX_OSERR; + struct sigaction term_sa; + term_sa.sa_handler = sig_term_handler; + term_sa.sa_flags = SA_RESTART; + sigemptyset(&term_sa.sa_mask); + for (const auto SIGNO : TERM_SIGNALS) { + if (sigaction(SIGNO, &term_sa, nullptr)) { + perror("Failed to establish signal handler"); + return EX_OSERR; + } } +#ifdef COMPILER_CLANG +# pragma clang diagnostic pop +#endif // all command line arguments // clang-format off @@ -127,6 +147,15 @@ int main(int argc, char **argv) { "Do not use this option per default! " "It should only be used if the shared memory of an improperly terminated instance continues " "to exist as an orphan and is no longer used.") + ("s,separate", + "Use a separate shared memory for requests with the specified client id. " + "The the client id (as hex value) is appended to the shared memory prefix (e.g. modbus_fc_DO)" + ". You can specify multiple client ids by separating them with ','. " + "Use --separate-all to generate separate shared memories for all possible client ids.", + cxxopts::value>()) + ("separate-all", + "like --separate, but for all client ids (creates 1028 shared memory files! " + "check/set 'ulimit -n' before using this option.)") ("h,help", "print usage") ("version", @@ -201,26 +230,89 @@ int main(int argc, char **argv) { return exit_usage(); } + const auto SEPARATE = args.count("separate"); + const auto SEPARATE_ALL = args.count("separate-all"); + if (SEPARATE && SEPARATE_ALL) { + std::cerr << "The options --separate and --separate-all cannot be used together." << std::endl; + return EX_USAGE; + } + + const auto FORCE_SHM = args.count("force") > 0; + // create shared memory object for modbus registers - std::unique_ptr mapping; - try { - mapping = std::make_unique(args["do-registers"].as(), - args["di-registers"].as(), - args["ao-registers"].as(), - args["ai-registers"].as(), - args["name-prefix"].as(), - args.count("force") > 0); - } catch (const std::system_error &e) { - std::cerr << e.what() << std::endl; - return EX_OSERR; + std::unique_ptr fallback_mapping; + if (args.count("separate-all") == 0) { + try { + fallback_mapping = std::make_unique(args["do-registers"].as(), + args["di-registers"].as(), + args["ao-registers"].as(), + args["ai-registers"].as(), + args["name-prefix"].as(), + FORCE_SHM); + } catch (const std::system_error &e) { + std::cerr << e.what() << std::endl; + return EX_OSERR; + } } + std::array mb_mappings; + std::vector> separate_mappings; + + if (SEPARATE_ALL) { + for (std::size_t i = 0; i < Modbus::TCP::MAX_CLIENT_IDS; ++i) { + std::ostringstream sstr; + sstr << args["name-prefix"].as() << std::setfill('0') << std::hex << std::setw(2) << i << '_'; + + try { + separate_mappings.emplace_back( + std::make_unique(args["do-registers"].as(), + args["di-registers"].as(), + args["ao-registers"].as(), + args["ai-registers"].as(), + sstr.str(), + FORCE_SHM)); + mb_mappings[i] = separate_mappings.back()->get_mapping(); + } catch (const std::system_error &e) { + std::cerr << e.what() << std::endl; + return EX_OSERR; + } + } + } else { + mb_mappings.fill(fallback_mapping->get_mapping()); + } + + if (SEPARATE) { + auto id_list = args["separate"].as>(); + std::unordered_set id_set(id_list.begin(), id_list.end()); + + for (auto a : id_set) { + std::ostringstream sstr; + sstr << args["name-prefix"].as() << std::setfill('0') << std::hex << std::setw(2) + << static_cast(a) << '_'; + + try { + separate_mappings.emplace_back( + std::make_unique(args["do-registers"].as(), + args["di-registers"].as(), + args["ao-registers"].as(), + args["ai-registers"].as(), + sstr.str(), + FORCE_SHM)); + mb_mappings[a] = separate_mappings.back()->get_mapping(); + } catch (const std::system_error &e) { + std::cerr << e.what() << std::endl; + return EX_OSERR; + } + } + } + + // create slave std::unique_ptr slave; try { slave = std::make_unique(args["ip"].as(), args["port"].as(), - mapping->get_mapping(), + mb_mappings.data(), #ifdef OS_LINUX args["tcp-timeout"].as()); #else