Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .clang-tidy
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ Checks: 'bugprone-*,
readability-static-definition-in-anonymous-namespace,
readability-string-compare,
readability-uniqueptr-delete-release,
readability-use-anyofallof
-modernize-use-trailing-return-type
-bugprone-exception-escape'
readability-use-anyofallof,
-modernize-use-trailing-return-type,
-bugprone-exception-escape,
-clang-diagnostic-switch-default'
WarningsAsErrors: '*,
-modernize-*,
-readability-*
Expand Down
7 changes: 4 additions & 3 deletions .clang-tidy-noerrors
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ Checks: 'bugprone-*,
readability-static-definition-in-anonymous-namespace,
readability-string-compare,
readability-uniqueptr-delete-release,
readability-use-anyofallof
-modernize-use-trailing-return-type
-bugprone-exception-escape'
readability-use-anyofallof,
-modernize-use-trailing-return-type,
-bugprone-exception-escape,
-clang-diagnostic-switch-default'
WarningsAsErrors: ''
HeaderFilterRegex: ''
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ cmake_minimum_required(VERSION 3.22.1 FATAL_ERROR)
# ======================================================================================================================

# project
project(Modbus_TCP_client_shm LANGUAGES CXX VERSION 1.6.3)
project(Modbus_TCP_client_shm LANGUAGES CXX VERSION 1.7.0)

# settings
set(Target "modbus-tcp-client-shm") # Executable name (without file extension!)
Expand Down
50 changes: 38 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,28 @@
Modbus tcp client that stores its data (registers) in shared memory objects.

## Dependencies

- cxxopts by jarro2783 (https://github.com/jarro2783/cxxopts) (only required for building the application)
- libmodbus by Stéphane Raimbault (https://github.com/stephane/libmodbus)
- cxxshm (https://github.com/NikolasK-source/cxxshm)
- cxxsemaphore (https://github.com/NikolasK-source/cxxsemaphore)

On Arch linux they are available via the official repositories and the AUR:

- https://archlinux.org/packages/extra/any/cxxopts/
- https://aur.archlinux.org/packages/libmodbus
- https://aur.archlinux.org/packages/cxxshm
- https://aur.archlinux.org/packages/cxxsemaphore

## Build

```
cmake -B build -DCMAKE_CXX_COMPILER=$(which clang++) -DCMAKE_BUILD_TYPE=Release -DCLANG_FORMAT=OFF -DCLANG_TIDY=OFF -DCOMPILER_WARNINGS=OFF -DBUILD_DOC=OFF
cmake --build .
```

## Use

```
modbus-tcp-client-shm [OPTION...]

Expand All @@ -33,14 +37,21 @@ modbus-tcp-client-shm [OPTION...]

shared memory options:
-n, --name-prefix arg shared memory name prefix (default: modbus_)
--force Force the use of the shared memory even if it already exists. 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 arg Use a separate shared memory for requests with the specified client id. 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.
--separate-all like --separate, but for all client ids (creates 1028 shared memory files! check/set 'ulimit -n' before using this option.)
--force Force the use of the shared memory even if it already exists.
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 arg Use a separate shared memory for requests with the specified client id.
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.
--separate-all like --separate, but for all client ids (creates 1028 shared memory files!
check/set 'ulimit -n' before using this option.)
--semaphore arg protect the shared memory with a named semaphore against simultaneous access
--semaphore-force Force the use of the semaphore even if it already exists. Do not use this option per default! It should only be used if the semaphore of an improperly terminated instance continues to exist as an orphan and is
no longer used.
--semaphore-force Force the use of the semaphore even if it already exists.
Do not use this option per default!
It should only be used if the semaphore of an improperly terminated instance continues
to exist as an orphan and is no longer used.
-b, --permissions arg permission bits that are applied when creating a shared memory. (default: 0640)

modbus options:
Expand All @@ -49,9 +60,15 @@ modbus-tcp-client-shm [OPTION...]
--ao-registers arg number of analog output registers (default: 65536)
--ai-registers arg number of analog input registers (default: 65536)
-m, --monitor output all incoming and outgoing packets to stdout
--byte-timeout arg timeout interval in seconds between two consecutive bytes of the same message. In most cases it is sufficient to set the response timeout. Fractional values are possible.
--response-timeout arg set the timeout interval in seconds used to wait for a response. When a byte timeout is set, if the elapsed time for the first byte of response is longer than the given timeout, a timeout is detected. When
byte timeout is disabled, the full confirmation response must be received before expiration of the response timeout. Fractional values are possible.
--byte-timeout arg timeout interval in seconds between two consecutive bytes of the same message.
In most cases it is sufficient to set the response timeout.
Fractional values are possible.
--response-timeout arg set the timeout interval in seconds used to wait for a response.
When a byte timeout is set, if the elapsed time for the first byte of response is
longer than the given timeout, a timeout is detected.
When byte timeout is disabled, the full confirmation response must be received
before expiration of the response timeout.
Fractional values are possible.

other options:
-h, --help print usage
Expand All @@ -63,6 +80,11 @@ modbus-tcp-client-shm [OPTION...]
--longversion print version (including compiler and system info) and exit
--shortversion print version (only version string) and exit
--git-hash print git hash

signal options:
-k, --signal arg send SIGUSR1 to process on writing modbus commands
--signal-register allow processes to register themselves for receiving SIGUSR1 on writing modbus commands
by sending SIGUSR1.


The modbus registers are mapped to shared memory objects:
Expand All @@ -75,10 +97,14 @@ The modbus registers are mapped to shared memory objects:
```

### Use privileged ports
The standard modbus port (502) can be used only by the root user under linux by default.
To circumvent this, you can create an entry in the iptables that redirects packets on the standard modbus port to a higher port.

The standard modbus port (502) can be used only by the root user under linux by default.
To circumvent this, you can create an entry in the iptables that redirects packets on the standard modbus port to a
higher port.
The following example redirects packets from port 502 (standard modbus port) to port 5020

```
iptables -A PREROUTING -t nat -p tcp --dport 502 -j REDIRECT --to-port 5020
```

The modbus client must be called with the option ```-p 5020```
2 changes: 2 additions & 0 deletions cmake_files/warnings.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ function(clangwarn target)
target_compile_options(${target} PUBLIC -Wno-nested-anon-types)
target_compile_options(${target} PUBLIC -Wno-gnu-anonymous-struct)
target_compile_options(${target} PUBLIC -Wno-source-uses-openmp)
target_compile_options(${target} PUBLIC -Wno-switch-default)
target_compile_options(${target} PUBLIC -Wno-disabled-macro-expansion)

endfunction()

Expand Down
11 changes: 1 addition & 10 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,7 @@ target_sources(${Target} PRIVATE Modbus_TCP_Client_poll.cpp)
target_sources(${Target} PRIVATE license.cpp)
target_sources(${Target} PRIVATE sa_to_str.cpp)
target_sources(${Target} PRIVATE Print_Time.cpp)


# ---------------------------------------- header files (*.jpp, *.h, ...) ----------------------------------------------
# ======================================================================================================================
target_sources(${Target} PRIVATE modbus_shm.hpp)
target_sources(${Target} PRIVATE Modbus_TCP_Client_poll.hpp)
target_sources(${Target} PRIVATE license.hpp)
target_sources(${Target} PRIVATE sa_to_str.hpp)
target_sources(${Target} PRIVATE Print_Time.hpp)

target_sources(${Target} PRIVATE Mb_Proc_Signal.cpp)

# ---------------------------------------- subdirectories --------------------------------------------------------------
# ======================================================================================================================
Expand Down
66 changes: 66 additions & 0 deletions src/Mb_Proc_Signal.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright (C) 2024 Nikolas Koesling <nikolas@koesling.info>.
* This program is free software. You can redistribute it and/or modify it under the terms of the GPLv3 License.
*/

#include "Mb_Proc_Signal.hpp"
#include "Print_Time.hpp"

#include <cerrno>
#include <format>
#include <iostream>
#include <modbus/modbus.h>
#include <system_error>
#include <vector>

Mb_Proc_Signal Mb_Proc_Signal::instance; // NOLINT

Mb_Proc_Signal &Mb_Proc_Signal::get_instance() {
return instance;
}

void Mb_Proc_Signal::add_process(pid_t process) {
auto ret = kill(process, 0);
if (ret == -1) {
if (errno == ESRCH) { throw std::runtime_error(std::format("no such process: {}", process)); }
throw std::system_error(
errno, std::generic_category(), std::format("Failed to send signal to process {}", process));
}
processes.insert(process);
}

void Mb_Proc_Signal::send_signal(const union sigval &value) {
std::vector<pid_t> erased;
for (auto proc : processes) {
auto ret = sigqueue(proc, SIGUSR1, value);
if (ret == -1) {
if (errno == ESRCH) {
erased.emplace_back(proc);
} else {
throw std::system_error(
errno, std::generic_category(), std::format("Failed to send signal to process {}", proc));
}
}
}

for (auto proc : erased) {
std::cerr << Print_Time::iso << " WARNING: process " << proc
<< " does no longer exist. Removing from SIGUSR1 receivers.\n";
processes.erase(proc);
}
}

void mb_callback(uint8_t mb_funtion_code) {
switch (mb_funtion_code) {
case MODBUS_FC_WRITE_SINGLE_COIL:
case MODBUS_FC_WRITE_SINGLE_REGISTER:
case MODBUS_FC_WRITE_MULTIPLE_COILS:
case MODBUS_FC_WRITE_MULTIPLE_REGISTERS:
case MODBUS_FC_WRITE_AND_READ_REGISTERS:
Mb_Proc_Signal::get_instance().send_signal({.sival_int = mb_funtion_code});
break;
default:
// do nothing
break;
}
}
32 changes: 32 additions & 0 deletions src/Mb_Proc_Signal.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2024 Nikolas Koesling <nikolas@koesling.info>.
* This program is free software. You can redistribute it and/or modify it under the terms of the GPLv3 License.
*/

#include <cstdint>
#include <unistd.h>
#include <unordered_set>

class Mb_Proc_Signal final {
private:
std::unordered_set<pid_t> processes;

Mb_Proc_Signal() = default;

static Mb_Proc_Signal instance;

public:
Mb_Proc_Signal(const Mb_Proc_Signal &) = delete;
Mb_Proc_Signal(Mb_Proc_Signal &&) = delete;
Mb_Proc_Signal &operator=(const Mb_Proc_Signal &) = delete;
Mb_Proc_Signal &operator=(Mb_Proc_Signal &&) = delete;
~Mb_Proc_Signal() = default;

static Mb_Proc_Signal &get_instance();

void add_process(pid_t process);

void send_signal(const union sigval &value);
};

void mb_callback(uint8_t mb_funtion_code);
53 changes: 45 additions & 8 deletions src/Modbus_TCP_Client_poll.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include "Modbus_TCP_Client_poll.hpp"

#include "Mb_Proc_Signal.hpp"
#include "Print_Time.hpp"
#include "sa_to_str.hpp"

Expand All @@ -13,6 +14,7 @@
#include <netinet/tcp.h>
#include <sstream>
#include <stdexcept>
#include <sys/signalfd.h>
#include <sys/socket.h>
#include <system_error>

Expand All @@ -31,14 +33,15 @@ static constexpr long SEMAPHORE_ERROR_DEC = 1;
static constexpr long SEMAPHORE_ERROR_MAX = 1000;

//* maximum time to wait for semaphore (100ms)
static constexpr struct timespec SEMAPHORE_MAX_TIME = {0, 100'000};
static constexpr struct timespec SEMAPHORE_MAX_TIME = {.tv_sec = 0, .tv_nsec = 100'000};

Client_Poll::Client_Poll(const std::string &host,
const std::string &service,
bool allow_sigusr1,
modbus_mapping_t *mapping,
std::size_t tcp_timeout, // NOLINT
std::size_t max_clients) // NOLINT
: max_clients(max_clients), poll_fds(max_clients + 2, {0, 0, 0}) {
: max_clients(max_clients), poll_fds(max_clients + 2, {0, 0, 0}), allow_sigusr1(allow_sigusr1) {
const char *host_str = "::";
if (!(host.empty() || host == "any")) host_str = host.c_str();

Expand Down Expand Up @@ -82,10 +85,11 @@ Client_Poll::Client_Poll(const std::string &host,

Client_Poll::Client_Poll(const std::string &host,
const std::string &service,
bool allow_sigusr1,
std::array<modbus_mapping_t *, MAX_CLIENT_IDS> &mappings,
std::size_t tcp_timeout, // NOLINT
std::size_t max_clients) // NOLINT
: max_clients(max_clients), poll_fds(max_clients + 2, {0, 0, 0}) {
: max_clients(max_clients), poll_fds(max_clients + 2, {0, 0, 0}), allow_sigusr1(allow_sigusr1) {
const char *host_str = "::";
if (!(host.empty() || host == "any")) host_str = host.c_str();

Expand Down Expand Up @@ -263,7 +267,10 @@ void Client_Poll::set_response_timeout(double timeout) {
return static_cast<double>(timeout.sec) + (static_cast<double>(timeout.usec) / (1000.0 * 1000.0)); // NOLINT
}

Client_Poll::run_t Client_Poll::run(int signal_fd, bool reconnect, int timeout) {
Client_Poll::run_t Client_Poll::run(int signal_fd,
bool reconnect,
int timeout,
void (*mb_function_callback)(uint8_t mb_function_code)) {
std::size_t i = 0;

// poll signal fd
Expand Down Expand Up @@ -308,10 +315,33 @@ Client_Poll::run_t Client_Poll::run(int signal_fd, bool reconnect, int timeout)
if (fd.revents & POLLNVAL) throw std::logic_error("poll (server socket) returned POLLNVAL");
if (fd.revents & POLLERR) throw std::logic_error("poll (signal fd) returned POLLERR");
if (fd.revents & POLLHUP) throw std::logic_error("poll (signal fd) returned POLLHUP");
if (fd.revents & POLLIN) return run_t::term_signal;
std::ostringstream sstr;
sstr << "poll (signal fd) returned unknown revent: " << fd.revents;
throw std::logic_error(sstr.str());
if (fd.revents & POLLIN) {
signalfd_siginfo siginfo {};
const auto read_size = read(signal_fd, &siginfo, sizeof(siginfo));
if (read_size == -1) {
throw std::system_error(errno, std::generic_category(), "Failed to read signalfd");
}

if (siginfo.ssi_signo == SIGUSR1 && allow_sigusr1) {
const auto pid = siginfo.ssi_pid;
try {
Mb_Proc_Signal::get_instance().add_process(static_cast<pid_t>(pid));
std::cerr << Print_Time::iso << " INFO: process " << pid
<< " registered for SIGUSR1 on writing modbus commands\n";
} catch (const std::runtime_error &err) {
std::cerr << Print_Time::iso << " WARNING: process " << pid
<< " registered for SIGUSR1: " << err.what() << "\n";
}
return run_t::ok;

} else {
return run_t::term_signal;
}
} else {
std::ostringstream sstr;
sstr << "poll (signal fd) returned unknown revent: " << fd.revents;
throw std::logic_error(sstr.str());
}
}
}

Expand Down Expand Up @@ -418,6 +448,13 @@ Client_Poll::run_t Client_Poll::run(int signal_fd, bool reconnect, int timeout)
<< std::endl; // NOLINT
close_con(client_addrs);
}

// function code callback
if (mb_function_callback) {
const auto FUNCTION_CODE = query[7];
mb_function_callback(FUNCTION_CODE);
}

} else if (rc == -1) {
if (errno != ECONNRESET) {
std::cerr << Print_Time::iso << " ERROR: modbus_receive failed: " << modbus_strerror(errno)
Expand Down
Loading