From 9d9326f64bceb2b2ea5f73e203c52f95ca62509a Mon Sep 17 00:00:00 2001 From: Christoph Kliemt Date: Thu, 24 Nov 2022 13:59:44 +0100 Subject: [PATCH] added read input register command Add filtering of echoed messages Make ModubusRTUClient constructor explicit Bump version to 0.2 Signed-off-by: Christoph Kliemt Signed-off-by: Kai-Uwe Hermann --- CMakeLists.txt | 2 +- include/consts.hpp | 4 +- include/modbus/modbus_client.hpp | 234 +++++++++++++++++-------------- include/modbus/utils.hpp | 59 ++++---- src/modbus_client.cpp | 2 +- src/modbus_rtu_client.cpp | 31 +++- src/utils.cpp | 29 ++-- tests/test_rtu.cpp | 9 ++ 8 files changed, 226 insertions(+), 144 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d46ba5d..a63962b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.11) project(modbus - VERSION 0.1 + VERSION 0.2 ) find_package(everest-cmake 0.1 REQUIRED diff --git a/include/consts.hpp b/include/consts.hpp index 6f7b2be..e107051 100644 --- a/include/consts.hpp +++ b/include/consts.hpp @@ -10,8 +10,10 @@ namespace modbus { namespace consts { // General MODBUS constants -constexpr uint16_t READ_HOLDING_REGISTER_MESSAGE_LENGTH = 6; +// TODO: this constant should have the value 5...? +constexpr uint16_t READ_REGISTER_COMMAND_LENGTH = 6; constexpr uint8_t READ_HOLDING_REGISTER_FUNCTION_CODE = 3; +constexpr uint8_t READ_INPUT_REGISTER_FUNCTION_CODE = 4; // MODBUS/RTU specific constants namespace rtu { diff --git a/include/modbus/modbus_client.hpp b/include/modbus/modbus_client.hpp index 2248bed..77c24de 100644 --- a/include/modbus/modbus_client.hpp +++ b/include/modbus/modbus_client.hpp @@ -4,115 +4,139 @@ #ifndef MODBUS_CLIENT_H #define MODBUS_CLIENT_H -#include #include +#include #include #include #include -namespace everest { namespace modbus { - - class ModbusClient { - public: - ModbusClient(connection::Connection& conn_); - virtual ~ModbusClient() = default; - // read_holding_register needs to be virtual, since the rtu format differs from the ip/udp formats. - virtual const std::vector read_holding_register(uint8_t unit_id, uint16_t first_register_address, uint16_t num_registers_to_read, bool return_only_registers_bytes=true) const; - - protected: - - const virtual std::vector full_message_from_body(const std::vector& body, uint16_t message_length, uint8_t unit_id) const = 0; - - virtual uint16_t validate_response(const std::vector& response, const std::vector& request) const = 0; - - // message size including protocol data (addressing, error check, mbap) - virtual std::size_t max_adu_size() const = 0; - // message size without protocol data (addressing, error check, mbap), function code and payload data only - virtual std::size_t max_pdu_size() const = 0; - - ModbusClient(const ModbusClient&) = delete; - ModbusClient& operator=(const ModbusClient&) = delete; - connection::Connection& conn; - }; - - class ModbusIPClient : public ModbusClient { - public: - ModbusIPClient(connection::Connection& conn_); - virtual ~ModbusIPClient() = default; - const std::vector full_message_from_body(const std::vector& body, uint16_t message_length, uint8_t unit_id) const override; - uint16_t validate_response(const std::vector& response, const std::vector& request) const override; - // message size including protocol data (addressing, error check, mbap) - virtual std::size_t max_adu_size() const override { return everest::modbus::consts::tcp::MAX_ADU; } - // message size without protocol data (addressing, error check, mbap), function code and payload data only - virtual std::size_t max_pdu_size() const override { return everest::modbus::consts::tcp::MAX_PDU; } - - }; - - class ModbusTCPClient : public ModbusIPClient { - public: - ModbusTCPClient(connection::TCPConnection& conn_); - ~ModbusTCPClient() override = default; - }; - - class ModbusUDPClient : public ModbusIPClient { - public: - ModbusUDPClient(connection::UDPConnection& conn_); - ~ModbusUDPClient() override = default; - }; - - using DataVectorUint16 = std::vector; - using DataVectorUint8 = std::vector; - - enum struct ByteOrder { BigEndian, LittleEndian }; - - class ModbusDataContainerUint16 { - - public: - - ModbusDataContainerUint16( ByteOrder byte_order, // which byteorder does the parameter payload have - const DataVectorUint16& payload ) : - m_byte_order(byte_order), - m_payload( payload ) - {} - - DataVectorUint8 get_payload_as_bigendian() const; - - std::size_t size() const { return m_payload.size(); } - - protected: - - ByteOrder m_byte_order; - DataVectorUint16 m_payload; - - }; - - class ModbusRTUClient : public ModbusClient { - public: - - ModbusRTUClient(connection::Connection& conn_); - virtual ~ModbusRTUClient() override; - - // throws derived from std::runtime_error, see include/modbus/exceptions.hpp - const DataVectorUint8 read_holding_register(uint8_t unit_id, uint16_t first_register_address, uint16_t num_registers_to_read, bool return_only_registers_bytes = true ) const override; - - // throws derived from std::runtime_error, see include/modbus/exceptions.hpp - DataVectorUint8 write_multiple_registers( uint8_t unit_id, uint16_t first_register_address, uint16_t num_registers_to_write , const ModbusDataContainerUint16& payload, bool return_only_registers_bytes ) const; // errors will be reported by exception std::runtime_error - - // message size including protocol data (addressing, error check, mbap) - virtual std::size_t max_adu_size() const override { return everest::modbus::consts::rtu::MAX_ADU; } - // message size without protocol data (addressing, error check, mbap), function code and payload data only - virtual std::size_t max_pdu_size() const override { return everest::modbus::consts::rtu::MAX_PDU; } - - static DataVectorUint8 response_without_protocol_data( const DataVectorUint8& raw_response, std::size_t payload_length ); - - - protected: - - const DataVectorUint8 full_message_from_body(const DataVectorUint8& body, uint16_t message_length, std::uint8_t unit_id) const override; - uint16_t validate_response(const DataVectorUint8& response, const DataVectorUint8& request) const override; - }; - - } // namespace modbus -}; // namespace everest +namespace everest { +namespace modbus { + +class ModbusClient { +public: + ModbusClient(connection::Connection& conn_); + virtual ~ModbusClient() = default; + // read_holding_register needs to be virtual, since the rtu format differs from the ip/udp formats. + virtual const std::vector read_holding_register(uint8_t unit_id, uint16_t first_register_address, + uint16_t num_registers_to_read, + bool return_only_registers_bytes = true) const; + +protected: + const virtual std::vector full_message_from_body(const std::vector& body, uint16_t message_length, + uint8_t unit_id) const = 0; + + virtual uint16_t validate_response(const std::vector& response, + const std::vector& request) const = 0; + + // message size including protocol data (addressing, error check, mbap) + virtual std::size_t max_adu_size() const = 0; + // message size without protocol data (addressing, error check, mbap), function code and payload data only + virtual std::size_t max_pdu_size() const = 0; + + ModbusClient(const ModbusClient&) = delete; + ModbusClient& operator=(const ModbusClient&) = delete; + connection::Connection& conn; +}; + +class ModbusIPClient : public ModbusClient { +public: + ModbusIPClient(connection::Connection& conn_); + virtual ~ModbusIPClient() = default; + const std::vector full_message_from_body(const std::vector& body, uint16_t message_length, + uint8_t unit_id) const override; + uint16_t validate_response(const std::vector& response, + const std::vector& request) const override; + // message size including protocol data (addressing, error check, mbap) + virtual std::size_t max_adu_size() const override { + return everest::modbus::consts::tcp::MAX_ADU; + } + // message size without protocol data (addressing, error check, mbap), function code and payload data only + virtual std::size_t max_pdu_size() const override { + return everest::modbus::consts::tcp::MAX_PDU; + } +}; + +class ModbusTCPClient : public ModbusIPClient { +public: + ModbusTCPClient(connection::TCPConnection& conn_); + ~ModbusTCPClient() override = default; +}; + +class ModbusUDPClient : public ModbusIPClient { +public: + ModbusUDPClient(connection::UDPConnection& conn_); + ~ModbusUDPClient() override = default; +}; + +using DataVectorUint16 = std::vector; +using DataVectorUint8 = std::vector; + +enum struct ByteOrder { + BigEndian, + LittleEndian +}; + +class ModbusDataContainerUint16 { + +public: + ModbusDataContainerUint16(ByteOrder byte_order, // which byteorder does the parameter payload have + const DataVectorUint16& payload) : + m_byte_order(byte_order), m_payload(payload) { + } + + DataVectorUint8 get_payload_as_bigendian() const; + + std::size_t size() const { + return m_payload.size(); + } + +protected: + ByteOrder m_byte_order; + DataVectorUint16 m_payload; +}; + +class ModbusRTUClient : public ModbusClient { +public: + ModbusRTUClient(connection::Connection& conn_, bool ignore_echo); + explicit ModbusRTUClient(connection::Connection& conn_) : ModbusRTUClient(conn_, false){} + virtual ~ModbusRTUClient() override; + + // throws derived from std::runtime_error, see include/modbus/exceptions.hpp + const DataVectorUint8 read_holding_register(uint8_t unit_id, uint16_t first_register_address, + uint16_t num_registers_to_read, + bool return_only_registers_bytes = true) const override; + + // HACK warning! No virtual method! + const DataVectorUint8 read_input_register(uint8_t unit_id, uint16_t first_register_address, + uint16_t num_registers_to_read, + bool return_only_registers_bytes = true) const; + // throws derived from std::runtime_error, see include/modbus/exceptions.hpp + DataVectorUint8 write_multiple_registers( + uint8_t unit_id, uint16_t first_register_address, uint16_t num_registers_to_write, + const ModbusDataContainerUint16& payload, + bool return_only_registers_bytes) const; // errors will be reported by exception std::runtime_error + + // message size including protocol data (addressing, error check, mbap) + virtual std::size_t max_adu_size() const override { + return everest::modbus::consts::rtu::MAX_ADU; + } + // message size without protocol data (addressing, error check, mbap), function code and payload data only + virtual std::size_t max_pdu_size() const override { + return everest::modbus::consts::rtu::MAX_PDU; + } + + static DataVectorUint8 response_without_protocol_data(const DataVectorUint8& raw_response, + std::size_t payload_length); + +protected: + const DataVectorUint8 full_message_from_body(const DataVectorUint8& body, uint16_t message_length, + std::uint8_t unit_id) const override; + uint16_t validate_response(const DataVectorUint8& response, const DataVectorUint8& request) const override; + bool ignore_echo; +}; + +} // namespace modbus +}; // namespace everest #endif diff --git a/include/modbus/utils.hpp b/include/modbus/utils.hpp index 58964a2..3715e53 100644 --- a/include/modbus/utils.hpp +++ b/include/modbus/utils.hpp @@ -8,30 +8,39 @@ #include "modbus_client.hpp" -namespace everest { namespace modbus { namespace utils { - - // General use utils - std::vector build_read_holding_register_message_body(uint16_t first_register_address, uint16_t num_registers_to_read); - std::vector build_write_multiple_register_body( uint16_t first_register_address, uint16_t num_registers_to_write, const ::everest::modbus::ModbusDataContainerUint16& payload ); - std::vector extract_body_from_response(const std::vector& response, int num_data_bytes); - std::vector extract_registers_bytes_from_response_body(const std::vector& response_body); - std::vector extract_register_bytes_from_response(const std::vector& response, int num_data_bytes); - void print_message_hex(const std::vector& message); - void print_message_first_N_bytes(unsigned char *message, int N); - - using PayloadType = unsigned char; - using CRCResultType = std::uint16_t; - CRCResultType calcCRC_16_ANSI( const PayloadType* payload ,std::size_t payload_length ); - - // MODBUS/IP specific utils - namespace ip { - // Utility funcs - std::vector make_mbap_header(uint16_t message_length, uint8_t unit_id); - uint16_t check_mbap_header(const std::vector& sent_message, const std::vector& received_message); - } - - } // namespace utils - } // namespace modbus -}; // namespace everest +namespace everest { +namespace modbus { +namespace utils { + +// General use utils +std::vector build_read_command_message_body(std::uint8_t function_code, uint16_t first_register_address, + uint16_t num_registers_to_read); +std::vector build_read_holding_register_message_body(uint16_t first_register_address, + uint16_t num_registers_to_read); +std::vector build_read_input_register_message_body(uint16_t first_register_address, + uint16_t num_registers_to_read); +std::vector build_write_multiple_register_body(uint16_t first_register_address, + uint16_t num_registers_to_write, + const ::everest::modbus::ModbusDataContainerUint16& payload); +std::vector extract_body_from_response(const std::vector& response, int num_data_bytes); +std::vector extract_registers_bytes_from_response_body(const std::vector& response_body); +std::vector extract_register_bytes_from_response(const std::vector& response, int num_data_bytes); +void print_message_hex(const std::vector& message); +void print_message_first_N_bytes(unsigned char* message, int N); + +using PayloadType = unsigned char; +using CRCResultType = std::uint16_t; +CRCResultType calcCRC_16_ANSI(const PayloadType* payload, std::size_t payload_length); + +// MODBUS/IP specific utils +namespace ip { +// Utility funcs +std::vector make_mbap_header(uint16_t message_length, uint8_t unit_id); +uint16_t check_mbap_header(const std::vector& sent_message, const std::vector& received_message); +} // namespace ip + +} // namespace utils +} // namespace modbus +}; // namespace everest #endif diff --git a/src/modbus_client.cpp b/src/modbus_client.cpp index 8ea6cf0..ad09b29 100644 --- a/src/modbus_client.cpp +++ b/src/modbus_client.cpp @@ -18,7 +18,7 @@ const std::vector ModbusClient::read_holding_register(uint8_t unit_id, std::vector body = utils::build_read_holding_register_message_body(first_register_address, num_registers_to_read); std::vector full_message = - full_message_from_body(body, consts::READ_HOLDING_REGISTER_MESSAGE_LENGTH, unit_id); + full_message_from_body(body, consts::READ_REGISTER_COMMAND_LENGTH, unit_id); conn.send_bytes(full_message); std::vector response = conn.receive_bytes(max_adu_size()); int num_register_bytes = validate_response(response, full_message); diff --git a/src/modbus_rtu_client.cpp b/src/modbus_rtu_client.cpp index 3fefff4..d683bc2 100644 --- a/src/modbus_rtu_client.cpp +++ b/src/modbus_rtu_client.cpp @@ -19,7 +19,7 @@ using namespace everest::modbus; * This is by design of the older parts of this library. */ -ModbusRTUClient::ModbusRTUClient(connection::Connection& conn_) : ModbusClient(conn_) { +ModbusRTUClient::ModbusRTUClient(connection::Connection& conn_, bool ignore_echo) : ModbusClient(conn_), ignore_echo(ignore_echo) { } ModbusRTUClient::~ModbusRTUClient() { @@ -63,7 +63,7 @@ const DataVectorUint8 ModbusRTUClient::read_holding_register(uint8_t unit_id, ui DataVectorUint8 body = utils::build_read_holding_register_message_body(first_register_address, num_registers_to_read); - DataVectorUint8 full_message = full_message_from_body(body, consts::READ_HOLDING_REGISTER_MESSAGE_LENGTH, unit_id); + DataVectorUint8 full_message = full_message_from_body(body, consts::READ_REGISTER_COMMAND_LENGTH, unit_id); conn.send_bytes(full_message); modbus::DataVectorUint8 response = conn.receive_bytes(max_adu_size()); @@ -77,6 +77,33 @@ const DataVectorUint8 ModbusRTUClient::read_holding_register(uint8_t unit_id, ui #endif } +// HACK: is not a virtual function, not implementing the modbus interface. +const DataVectorUint8 ModbusRTUClient::read_input_register(uint8_t unit_id, uint16_t first_register_address, + uint16_t num_registers_to_read, + bool return_only_registers_bytes) const { + + std::vector body = + utils::build_read_input_register_message_body(first_register_address, num_registers_to_read); + std::vector full_message = full_message_from_body(body, consts::READ_REGISTER_COMMAND_LENGTH, unit_id); + conn.send_bytes(full_message); + std::vector response = conn.receive_bytes(max_adu_size()); + + if (ignore_echo && response.size() > full_message.size()) { + auto request = std::vector(response.begin(), response.begin() + full_message.size()); + if (full_message == request) { + response = std::vector(response.begin() + full_message.size(), response.end()); + } + } + + int num_register_bytes = validate_response(response, full_message); + + if (return_only_registers_bytes) { + return utils::extract_register_bytes_from_response(response, num_register_bytes + 2); + } + + return response; +} + DataVectorUint8 ModbusDataContainerUint16::get_payload_as_bigendian() const { DataVectorUint8 result; diff --git a/src/utils.cpp b/src/utils.cpp index 97ac146..f42b5ba 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -44,13 +44,13 @@ std::vector utils::ip::make_mbap_header(uint16_t message_length, uint8_ return mbap_header; } -std::vector utils::build_read_holding_register_message_body(uint16_t first_register_address, - uint16_t num_registers_to_read) { - // Preparing message body - std::vector message_body(consts::READ_HOLDING_REGISTER_MESSAGE_LENGTH - 1); +std::vector utils::build_read_command_message_body(std::uint8_t function_code, uint16_t first_register_address, + uint16_t num_registers_to_read) { + + std::vector message_body(consts::READ_REGISTER_COMMAND_LENGTH - 1); // Adding read function code - message_body[0] = consts::READ_HOLDING_REGISTER_FUNCTION_CODE; + message_body[0] = function_code; // Adding first register data address message_body[1] = (first_register_address >> 8) & 0xFF; @@ -63,6 +63,18 @@ std::vector utils::build_read_holding_register_message_body(uint16_t fi return message_body; } +std::vector utils::build_read_holding_register_message_body(uint16_t first_register_address, + uint16_t num_registers_to_read) { + return build_read_command_message_body(consts::READ_HOLDING_REGISTER_FUNCTION_CODE, first_register_address, + num_registers_to_read); +} + +std::vector utils::build_read_input_register_message_body(uint16_t first_register_address, + uint16_t num_registers_to_read) { + return build_read_command_message_body(consts::READ_INPUT_REGISTER_FUNCTION_CODE, first_register_address, + num_registers_to_read); +} + std::vector utils::build_write_multiple_register_body(uint16_t first_register_address, uint16_t num_registers_to_write, const ::everest::modbus::ModbusDataContainerUint16& payload) { @@ -130,14 +142,13 @@ std::vector utils::extract_body_from_response(const std::vector utils::extract_register_bytes_from_response(const std::vector& response, int num_data_bytes) { - std::vector body = utils::extract_body_from_response(response, num_data_bytes); - return utils::extract_registers_bytes_from_response_body(body); + return utils::extract_registers_bytes_from_response_body(response); } std::vector utils::extract_registers_bytes_from_response_body(const std::vector& response_body) { - uint8_t num_register_bytes = response_body[2]; + uint8_t num_register_bytes = response_body.at(2); std::vector register_bytes = - std::vector(response_body.end() - num_register_bytes, response_body.end()); + std::vector(response_body.begin() + 3, response_body.begin() + 3 + num_register_bytes); return register_bytes; } diff --git a/tests/test_rtu.cpp b/tests/test_rtu.cpp index ff15d97..6a4442e 100644 --- a/tests/test_rtu.cpp +++ b/tests/test_rtu.cpp @@ -346,6 +346,15 @@ TEST(RTUClientTest, test_rtu_client_read_holding_register) { } } +TEST(RTUClientTest, test_rtu_client_read_input_register) { + + FAIL() << "\n\n(imagine this message displayed in red, blinking...)\n\n *** needs to be implemented, currently we dont have data for this. ***\n\n"; + +} + + + + TEST(RTUClientTest, test_rtu_client_write_multiple_register) { // test the writer_multiple_registers