Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SerialCommHub: support longer transactions and chunking #441

Closed
wants to merge 4 commits into from
Closed
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
22 changes: 12 additions & 10 deletions modules/SerialCommHub/main/serial_communication_hubImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,16 @@ static std::vector<int> vector_to_int(const std::vector<uint16_t>& response) {
// Implementation

void serial_communication_hubImpl::init() {
using namespace std::chrono;
Everest::GpioSettings rxtx_gpio_settings;

rxtx_gpio_settings.chip_name = config.rxtx_gpio_chip;
rxtx_gpio_settings.line_number = config.rxtx_gpio_line;
rxtx_gpio_settings.inverted = config.rxtx_gpio_tx_high;

if (!modbus.open_device(config.serial_port, config.baudrate, config.ignore_echo, rxtx_gpio_settings,
static_cast<tiny_modbus::Parity>(config.parity))) {
static_cast<tiny_modbus::Parity>(config.parity), milliseconds(config.initial_timeout_ms),
milliseconds(config.within_message_timeout_ms))) {
EVLOG_AND_THROW(Everest::EverestConfigError(fmt::format("Cannot open serial port {}.", config.serial_port)));
}
}
Expand All @@ -61,12 +63,12 @@ serial_communication_hubImpl::handle_modbus_read_holding_registers(int& target_d
auto retry_counter = this->num_resends_on_error;
while (retry_counter > 0) {

// EVLOG_info << fmt::format("Try {} Call modbus_client->read_holding_register(id {} addr {} len {})",
// (int)retry_counter, (uint8_t)target_device_id,
// (uint16_t)first_register_address, (uint16_t)num_registers_to_read);
EVLOG_debug << fmt::format("Try {} Call modbus_client->read_holding_register(id {} addr {} len {})",
(int)retry_counter, (uint8_t)target_device_id, (uint16_t)first_register_address,
(uint16_t)num_registers_to_read);

response = modbus.txrx(target_device_id, tiny_modbus::FunctionCode::READ_MULTIPLE_HOLDING_REGISTERS,
first_register_address, num_registers_to_read);
first_register_address, num_registers_to_read, config.max_packet_size);
if (response.size() > 0) {
break;
}
Expand Down Expand Up @@ -97,12 +99,12 @@ serial_communication_hubImpl::handle_modbus_read_input_registers(int& target_dev
uint8_t retry_counter{this->num_resends_on_error};
while (retry_counter-- > 0) {

// EVLOG_info << fmt::format("Try {} Call modbus_client->read_input_register(id {} addr {} len {})",
// (int)retry_counter, (uint8_t)target_device_id,
// (uint16_t)first_register_address, (uint16_t)num_registers_to_read);
EVLOG_debug << fmt::format("Try {} Call modbus_client->read_input_register(id {} addr {} len {})",
(int)retry_counter, (uint8_t)target_device_id, (uint16_t)first_register_address,
(uint16_t)num_registers_to_read);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug needs to be removed before merging

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #535


response = modbus.txrx(target_device_id, tiny_modbus::FunctionCode::READ_INPUT_REGISTERS,
first_register_address, num_registers_to_read);
first_register_address, num_registers_to_read, config.max_packet_size);
if (response.size() > 0) {
break;
}
Expand Down Expand Up @@ -140,7 +142,7 @@ types::serial_comm_hub_requests::StatusCodeEnum serial_communication_hubImpl::ha
(uint16_t)data.size());

response = modbus.txrx(target_device_id, tiny_modbus::FunctionCode::WRITE_MULTIPLE_HOLDING_REGISTERS,
first_register_address, data.size(), true, data);
first_register_address, data.size(), config.max_packet_size, true, data);
if (response.size() > 0) {
break;
}
Expand Down
3 changes: 3 additions & 0 deletions modules/SerialCommHub/main/serial_communication_hubImpl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
std::string rxtx_gpio_chip;
int rxtx_gpio_line;
bool rxtx_gpio_tx_high;
int max_packet_size;

Check notice on line 36 in modules/SerialCommHub/main/serial_communication_hubImpl.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/SerialCommHub/main/serial_communication_hubImpl.hpp#L36

struct member 'Conf::max_packet_size' is never used.
int initial_timeout_ms;

Check notice on line 37 in modules/SerialCommHub/main/serial_communication_hubImpl.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/SerialCommHub/main/serial_communication_hubImpl.hpp#L37

struct member 'Conf::initial_timeout_ms' is never used.
int within_message_timeout_ms;

Check notice on line 38 in modules/SerialCommHub/main/serial_communication_hubImpl.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/SerialCommHub/main/serial_communication_hubImpl.hpp#L38

struct member 'Conf::within_message_timeout_ms' is never used.
};

class serial_communication_hubImpl : public serial_communication_hubImplBase {
Expand Down
17 changes: 17 additions & 0 deletions modules/SerialCommHub/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,23 @@ provides:
description: GPIO direction, false means low for TX, true means high for TX
type: boolean
default: false
max_packet_size:
description: >-
Maximum size of a packet to read/write in bytes. Payload exceeding the size will be chunked.
The APU size according to [wikipedia](https://en.wikipedia.org/wiki/Modbus) is 256 bytes,
which is used as default here.
type: integer
minimum: 0
maximum: 65536
default: 256
initial_timeout_ms:
description: Timeout in ms for the first packet.
type: integer
default: 500
within_message_timeout_ms:
description: Timeout in ms for subsequent packets.
type: integer
default: 100
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
Expand Down
79 changes: 67 additions & 12 deletions modules/SerialCommHub/tiny_modbus_rtu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
#include <sys/select.h>
#include <sys/time.h>

#include <algorithm>
#include <fmt/core.h>
#include <iterator>
#include <ostream>

#include "crc16.hpp"

Expand Down Expand Up @@ -174,6 +177,8 @@ static std::vector<uint16_t> decode_reply(const uint8_t* buf, int len, uint8_t e
case 0x0B:
EVLOG_error << "Modbus exception: Gateway target device failed to respond";
break;
default:
EVLOG_error << "Modbus exception: Unknown";
}
return result;
}
Expand All @@ -185,8 +190,12 @@ TinyModbusRTU::~TinyModbusRTU() {
}

bool TinyModbusRTU::open_device(const std::string& device, int _baud, bool _ignore_echo,
const Everest::GpioSettings& rxtx_gpio_settings, const Parity parity) {
const Everest::GpioSettings& rxtx_gpio_settings, const Parity parity,
std::chrono::milliseconds _initial_timeout,
std::chrono::milliseconds _within_message_timeout) {

initial_timeout = _initial_timeout;
within_message_timeout = _within_message_timeout;
ignore_echo = _ignore_echo;

rxtx_gpio.open(rxtx_gpio_settings);
Expand Down Expand Up @@ -262,19 +271,28 @@ bool TinyModbusRTU::open_device(const std::string& device, int _baud, bool _igno
}

int TinyModbusRTU::read_reply(uint8_t* rxbuf, int rxbuf_len) {
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = MODBUS_RX_INITIAL_TIMEOUT_MS * 1000; // 500msec intial timeout until device responds
// Lambda to convert std::chrono to timeval.
auto to_timeval = [](const auto& time) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a stand alone function in the anonymous namespace in this file. It does not capture anything from its environment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree - We can only use auto as a function argument in c++20. The the extracted function therefore would either need to pin down the exact chrono type (milliseconds) making the code slightly less generic or be a template function making the code less readable

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dorezyuk do you mean, that function

template<T>
timeval to_timeval(const T& time) {
    // existing code without change
}

would make it less readable?

using namespace std::chrono;
struct timeval timeout;
auto sec = duration_cast<seconds>(time);
timeout.tv_sec = sec.count();
timeout.tv_usec = duration_cast<microseconds>(time - sec).count();
return timeout;
};

auto timeout = to_timeval(initial_timeout);
const auto within_message_timeval = to_timeval(within_message_timeout);

fd_set set;
FD_ZERO(&set);
FD_SET(fd, &set);

int bytes_read_total = 0;
while (true) {
int rv = select(fd + 1, &set, NULL, NULL, &timeout);
timeout.tv_usec = MODBUS_RX_WITHIN_MESSAGE_TIMEOUT_MS *
1000; // reduce timeout after first chunk, no uneccesary waiting at the end of the message
if (rv == -1) { // error in select function call
timeout = within_message_timeval;
if (rv == -1) { // error in select function call
perror("txrx: select:");
break;
} else if (rv == 0) { // no more bytes to read within timeout, so transfer is complete
Expand All @@ -289,20 +307,57 @@ int TinyModbusRTU::read_reply(uint8_t* rxbuf, int rxbuf_len) {
int bytes_read = read(fd, rxbuf + bytes_read_total, rxbuf_len - bytes_read_total);
if (bytes_read > 0) {
bytes_read_total += bytes_read;
// EVLOG_info << "RECVD: " << hexdump(rxbuf, bytes_read_total);
EVLOG_debug << "RECVD: " << hexdump(rxbuf, bytes_read_total);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove debug

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #535

}
}
}
return bytes_read_total;
}

std::vector<uint16_t> TinyModbusRTU::txrx(uint8_t device_address, FunctionCode function,
uint16_t first_register_address, uint16_t register_quantity,
uint16_t max_packet_size, bool wait_for_reply,
std::vector<uint16_t> request) {
// This only supports chunking of the read-requests.
std::vector<uint16_t> out;

const uint16_t register_chunk = (max_packet_size - MODBUS_MIN_REPLY_SIZE) / 2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If register_chunk is zero because max_packet_size is < MODBUS_MIN_REPLY_SIZE this loop never ends. Add input validation here and increase the min setting for max_packet_size in manifest to > MIN_REPLY_SIZE

size_t written_elements = 0;
while (register_quantity) {
const auto current_register_quantity = std::min(register_quantity, register_chunk);
std::vector<uint16_t> current_request;
if (request.size() > written_elements + current_register_quantity) {
current_request = std::vector<uint16_t>(request.begin() + written_elements,
request.begin() + written_elements + current_register_quantity);
written_elements += current_register_quantity;
} else {
current_request = std::vector<uint16_t>(request.begin() + written_elements, request.end());
written_elements = request.size();
}

const auto res = txrx_impl(device_address, function, first_register_address, current_register_quantity,
wait_for_reply, current_request);

// We failed to read/write.
if (res.empty()) {
return res;
}

out.insert(out.end(), res.begin(), res.end());
first_register_address += current_register_quantity;
register_quantity -= current_register_quantity;
}

return out;
}

/*
This function transmits a modbus request and waits for the reply.
Parameter request is optional and is only used for writing multiple registers.
*/
std::vector<uint16_t> TinyModbusRTU::txrx(uint8_t device_address, FunctionCode function,
uint16_t first_register_address, uint16_t register_quantity,
bool wait_for_reply, std::vector<uint16_t> request) {
std::vector<uint16_t> TinyModbusRTU::txrx_impl(uint8_t device_address, FunctionCode function,
uint16_t first_register_address, uint16_t register_quantity,
bool wait_for_reply, std::vector<uint16_t> request) {
{
// size of request
int req_len = (request.size() == 0 ? 0 : 2 * request.size() + 1) + MODBUS_BASE_PAYLOAD_SIZE;
Expand Down Expand Up @@ -331,7 +386,7 @@ std::vector<uint16_t> TinyModbusRTU::txrx(uint8_t device_address, FunctionCode f
// set checksum in the last 2 bytes
append_checksum(req.get(), req_len);

// EVLOG_info << "SEND: " << hexdump(req.get(), req_len);
EVLOG_debug << "SEND: " << hexdump(req.get(), req_len);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove debug

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #535


// clear input and output buffer
tcflush(fd, TCIOFLUSH);
Expand Down
17 changes: 12 additions & 5 deletions modules/SerialCommHub/tiny_modbus_rtu.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#ifndef TINY_MODBUS_RTU
#define TINY_MODBUS_RTU

#include <chrono>
#include <stdint.h>
#include <termios.h>

Expand All @@ -31,9 +32,6 @@ constexpr int MODBUS_MAX_REPLY_SIZE = 255 + 6;
constexpr int MODBUS_MIN_REPLY_SIZE = 5;
constexpr int MODBUS_BASE_PAYLOAD_SIZE = 8;

constexpr int MODBUS_RX_INITIAL_TIMEOUT_MS = 500;
constexpr int MODBUS_RX_WITHIN_MESSAGE_TIMEOUT_MS = 100;

enum class Parity : uint8_t {
NONE = 0,
ODD = 1,
Expand All @@ -57,18 +55,27 @@ class TinyModbusRTU {
~TinyModbusRTU();

bool open_device(const std::string& device, int baud, bool ignore_echo,
const Everest::GpioSettings& rxtx_gpio_settings, const Parity parity);
const Everest::GpioSettings& rxtx_gpio_settings, const Parity parity,
std::chrono::milliseconds initial_timeout, std::chrono::milliseconds within_message_timeout);

std::vector<uint16_t> txrx(uint8_t device_address, FunctionCode function, uint16_t first_register_address,
uint16_t register_quantity, bool wait_for_reply = true,
uint16_t register_quantity, uint16_t chunk_size, bool wait_for_reply = true,
std::vector<uint16_t> request = std::vector<uint16_t>());

private:
// Serial interface
int fd{0};
bool ignore_echo{false};

std::vector<uint16_t> txrx_impl(uint8_t device_address, FunctionCode function, uint16_t first_register_address,
uint16_t register_quantity, bool wait_for_reply = true,
std::vector<uint16_t> request = std::vector<uint16_t>());

int read_reply(uint8_t* rxbuf, int rxbuf_len);

Everest::Gpio rxtx_gpio;
std::chrono::milliseconds initial_timeout;
std::chrono::milliseconds within_message_timeout;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both could be const and initialized in the initializer list

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be a better design. But in the way the tiny_modbus implemented and used it would require quite a refactoring:

  • there is no constructor defined for TinyModbusRTU
  • the object modbus is default-constructed in SerialComHub, and the device is opened later on.
    So we will need to figure out when and with which parameters to call the TinyModbusRTU constructor

Do you think it makes sense to add this refactoring into this MR?

};

} // namespace tiny_modbus
Expand Down