Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter
- CitrineOS core extracted (CSMS OCPP 2.0.1) - OpenOCPP extracted (firmware OCPP 1.6J/2.0.1) - ShapeShifter library installed (pip install -e) - ShapeShifter specification extracted - EVerest extracted TODO updated with progress
This commit is contained in:
17
tools/EVerest-main/modules/Misc/SerialCommHub/BUILD.bazel
Normal file
17
tools/EVerest-main/modules/Misc/SerialCommHub/BUILD.bazel
Normal file
@@ -0,0 +1,17 @@
|
||||
load("//modules:module.bzl", "cc_everest_module")
|
||||
|
||||
IMPLS = [
|
||||
"main",
|
||||
]
|
||||
|
||||
cc_everest_module(
|
||||
name = "SerialCommHub",
|
||||
srcs = glob([
|
||||
"*.cpp",
|
||||
"*.hpp",
|
||||
]),
|
||||
impls = IMPLS,
|
||||
deps = [
|
||||
"//lib/everest/gpio",
|
||||
],
|
||||
)
|
||||
33
tools/EVerest-main/modules/Misc/SerialCommHub/CMakeLists.txt
Normal file
33
tools/EVerest-main/modules/Misc/SerialCommHub/CMakeLists.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
#
|
||||
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
# template version 3
|
||||
#
|
||||
|
||||
# module setup:
|
||||
# - ${MODULE_NAME}: module name
|
||||
ev_setup_cpp_module()
|
||||
|
||||
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
|
||||
# insert your custom targets and additional config variables here
|
||||
target_link_libraries(${MODULE_NAME}
|
||||
PRIVATE
|
||||
everest::gpio
|
||||
)
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
tiny_modbus_rtu.cpp
|
||||
crc16.cpp
|
||||
)
|
||||
|
||||
target_compile_features(${MODULE_NAME} PUBLIC cxx_std_17)
|
||||
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"main/serial_communication_hubImpl.cpp"
|
||||
)
|
||||
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
# insert other things like install cmds etc here
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest
|
||||
#include "SerialCommHub.hpp"
|
||||
|
||||
namespace module {
|
||||
|
||||
void SerialCommHub::init() {
|
||||
invoke_init(*p_main);
|
||||
}
|
||||
|
||||
void SerialCommHub::ready() {
|
||||
invoke_ready(*p_main);
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
@@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef SERIAL_COMM_HUB_HPP
|
||||
#define SERIAL_COMM_HUB_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 2
|
||||
//
|
||||
|
||||
#include "ld-ev.hpp"
|
||||
|
||||
// headers for provided interface implementations
|
||||
#include <generated/interfaces/serial_communication_hub/Implementation.hpp>
|
||||
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
// insert your custom include headers here
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
|
||||
namespace module {
|
||||
|
||||
struct Conf {};
|
||||
|
||||
class SerialCommHub : public Everest::ModuleBase {
|
||||
public:
|
||||
SerialCommHub() = delete;
|
||||
SerialCommHub(const ModuleInfo& info, std::unique_ptr<serial_communication_hubImplBase> p_main, Conf& config) :
|
||||
ModuleBase(info), p_main(std::move(p_main)), config(config){};
|
||||
|
||||
const std::unique_ptr<serial_communication_hubImplBase> p_main;
|
||||
const Conf& config;
|
||||
|
||||
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
|
||||
// insert your public definitions here
|
||||
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
|
||||
|
||||
protected:
|
||||
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
|
||||
// insert your protected definitions here
|
||||
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
|
||||
|
||||
private:
|
||||
friend class LdEverest;
|
||||
void init();
|
||||
void ready();
|
||||
|
||||
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
|
||||
// insert your private definitions here
|
||||
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
|
||||
};
|
||||
|
||||
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
|
||||
// insert other definitions here
|
||||
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
|
||||
|
||||
} // namespace module
|
||||
|
||||
#endif // SERIAL_COMM_HUB_HPP
|
||||
46
tools/EVerest-main/modules/Misc/SerialCommHub/crc16.cpp
Normal file
46
tools/EVerest-main/modules/Misc/SerialCommHub/crc16.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Tiago Ventura
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "crc16.hpp"
|
||||
|
||||
uint16_t calculate_modbus_crc16(const uint8_t* buf, int len) {
|
||||
uint16_t crc = 0xFFFF;
|
||||
char i;
|
||||
|
||||
while (len--) {
|
||||
crc ^= (*buf++);
|
||||
|
||||
for (i = 0; i < 8; i++) {
|
||||
if (crc & 1) {
|
||||
crc >>= 1;
|
||||
crc ^= 0xA001;
|
||||
} else {
|
||||
crc >>= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
8
tools/EVerest-main/modules/Misc/SerialCommHub/crc16.hpp
Normal file
8
tools/EVerest-main/modules/Misc/SerialCommHub/crc16.hpp
Normal file
@@ -0,0 +1,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef CRC16_HPP
|
||||
#define CRC16_HPP
|
||||
#include <stdint.h>
|
||||
|
||||
uint16_t calculate_modbus_crc16(const uint8_t* buf, int len);
|
||||
#endif
|
||||
@@ -0,0 +1,246 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "serial_communication_hubImpl.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <date/date.h>
|
||||
#include <date/tz.h>
|
||||
#include <fmt/core.h>
|
||||
#include <mutex>
|
||||
#include <typeinfo>
|
||||
|
||||
namespace module {
|
||||
namespace main {
|
||||
|
||||
template <typename T, typename U> static void append_array(std::vector<T>& m, const std::vector<U>& a) {
|
||||
for (auto it = a.begin(); it != a.end(); ++it)
|
||||
m.push_back(*it);
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
static std::vector<int> vector_to_int(const std::vector<uint16_t>& response) {
|
||||
std::vector<int> i;
|
||||
i.reserve(response.size());
|
||||
for (auto r : response) {
|
||||
i.push_back((int)r);
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Converts a Result to a ResultBool by looking at each bit of the uint16_t values and converting them to
|
||||
* bools in the right order. Used for Modbus read coils responses where the result is a bit-packed array of coil states.
|
||||
* @param result The Result to convert
|
||||
* @param number_of_coils The number of coils that were requested to read, used to limit the number of bools in the
|
||||
* output
|
||||
* @return The converted ResultBool
|
||||
*/
|
||||
static types::serial_comm_hub_requests::ResultBool
|
||||
convert_read_coils_result(const types::serial_comm_hub_requests::Result& result, size_t number_of_coils) {
|
||||
constexpr uint8_t BITS_PER_BYTE = 8;
|
||||
constexpr uint16_t BYTE_MASK = 0xFF;
|
||||
|
||||
types::serial_comm_hub_requests::ResultBool out;
|
||||
out.status_code = result.status_code;
|
||||
|
||||
if (result.value.has_value()) {
|
||||
std::vector<bool> result_bool;
|
||||
for (const uint16_t packed_bytes : result.value.value()) {
|
||||
// Modbus read coils response packs bits into raw bytes, the modbus library uses big-endian to build uint16
|
||||
// from those. Here we extract the original MSB and LSB from the BE uint16_t and process them in the correct
|
||||
// order.
|
||||
const auto msb = static_cast<uint8_t>((packed_bytes >> BITS_PER_BYTE) & BYTE_MASK);
|
||||
const auto lsb = static_cast<uint8_t>(packed_bytes & BYTE_MASK);
|
||||
|
||||
for (const uint8_t byte : {msb, lsb}) {
|
||||
for (int bit = 0; bit < BITS_PER_BYTE; bit++) {
|
||||
if (result_bool.size() >= number_of_coils) {
|
||||
break;
|
||||
}
|
||||
result_bool.push_back((byte & (1U << bit)) != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.value = std::move(result_bool);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
system_error_logged = false;
|
||||
|
||||
if (!modbus.open_device(config.serial_port, config.baudrate, config.ignore_echo, rxtx_gpio_settings,
|
||||
static_cast<tiny_modbus::Parity>(config.parity), config.rtscts,
|
||||
milliseconds(config.initial_timeout_ms), milliseconds(config.within_message_timeout_ms))) {
|
||||
EVLOG_error << fmt::format("Cannot open serial port {}, ModBus will not work.", config.serial_port);
|
||||
}
|
||||
}
|
||||
|
||||
void serial_communication_hubImpl::ready() {
|
||||
}
|
||||
|
||||
types::serial_comm_hub_requests::Result
|
||||
serial_communication_hubImpl::perform_modbus_request(uint8_t device_address, tiny_modbus::FunctionCode function,
|
||||
uint16_t first_register_address, uint16_t register_quantity,
|
||||
bool wait_for_reply, std::vector<uint16_t> request) {
|
||||
std::scoped_lock lock(serial_mutex);
|
||||
types::serial_comm_hub_requests::Result result;
|
||||
std::vector<uint16_t> response;
|
||||
auto retry_counter = config.retries + 1;
|
||||
bool last_error_was_timeout = false;
|
||||
|
||||
while (retry_counter > 0) {
|
||||
auto current_trial = config.retries + 1 - retry_counter + 1;
|
||||
|
||||
EVLOG_debug << fmt::format("Trial {}/{}: calling {}(id {} addr {}({:#06x}) len {})", current_trial,
|
||||
config.retries + 1, tiny_modbus::FunctionCode_to_string_with_hex(function),
|
||||
device_address, first_register_address, first_register_address, register_quantity);
|
||||
|
||||
last_error_was_timeout = false;
|
||||
try {
|
||||
response = modbus.txrx(device_address, function, first_register_address, register_quantity,
|
||||
config.max_packet_size, wait_for_reply, request);
|
||||
} catch (const tiny_modbus::TimeoutException& e) {
|
||||
// TimeoutException is a specific type of communication error
|
||||
last_error_was_timeout = true;
|
||||
auto logmsg = fmt::format("Modbus call {} for device id {} addr {}({:#06x}) failed: {}",
|
||||
tiny_modbus::FunctionCode_to_string_with_hex(function), device_address,
|
||||
first_register_address, first_register_address, e.what());
|
||||
|
||||
if (retry_counter != 1) {
|
||||
EVLOG_debug << logmsg;
|
||||
} else {
|
||||
EVLOG_warning << logmsg;
|
||||
}
|
||||
} catch (const tiny_modbus::TinyModbusException& e) {
|
||||
auto logmsg = fmt::format("Modbus call {} for device id {} addr {}({:#06x}) failed: {}",
|
||||
tiny_modbus::FunctionCode_to_string_with_hex(function), device_address,
|
||||
first_register_address, first_register_address, e.what());
|
||||
|
||||
if (retry_counter != 1) {
|
||||
EVLOG_debug << logmsg;
|
||||
} else {
|
||||
EVLOG_warning << logmsg;
|
||||
}
|
||||
} catch (const std::logic_error& e) {
|
||||
EVLOG_warning << "Logic error in Modbus implementation: " << e.what();
|
||||
} catch (const std::system_error& e) {
|
||||
// FIXME: report this to the infrastructure, as soon as an error interface for this is available
|
||||
// Log this only once, as we are convinced this will not go away
|
||||
if (not system_error_logged) {
|
||||
EVLOG_error << "System error in accessing Modbus: [" << e.code() << "] " << e.what();
|
||||
system_error_logged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (response.size() > 0)
|
||||
break;
|
||||
|
||||
retry_counter--;
|
||||
}
|
||||
|
||||
if (response.size() > 0) {
|
||||
EVLOG_debug << fmt::format("Process response (size {})", response.size());
|
||||
result.status_code = types::serial_comm_hub_requests::StatusCodeEnum::Success;
|
||||
result.value = vector_to_int(response);
|
||||
system_error_logged = false; // reset after success
|
||||
} else {
|
||||
// If the last error was a timeout, return Timeout status, otherwise Error
|
||||
if (last_error_was_timeout) {
|
||||
result.status_code = types::serial_comm_hub_requests::StatusCodeEnum::Timeout;
|
||||
} else {
|
||||
result.status_code = types::serial_comm_hub_requests::StatusCodeEnum::Error;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Commands
|
||||
|
||||
types::serial_comm_hub_requests::Result
|
||||
serial_communication_hubImpl::handle_modbus_read_holding_registers(int& target_device_id, int& first_register_address,
|
||||
int& num_registers_to_read) {
|
||||
|
||||
return perform_modbus_request(target_device_id, tiny_modbus::FunctionCode::READ_MULTIPLE_HOLDING_REGISTERS,
|
||||
first_register_address, num_registers_to_read);
|
||||
}
|
||||
|
||||
types::serial_comm_hub_requests::Result
|
||||
serial_communication_hubImpl::handle_modbus_read_input_registers(int& target_device_id, int& first_register_address,
|
||||
int& num_registers_to_read) {
|
||||
|
||||
return perform_modbus_request(target_device_id, tiny_modbus::FunctionCode::READ_INPUT_REGISTERS,
|
||||
first_register_address, num_registers_to_read);
|
||||
}
|
||||
|
||||
types::serial_comm_hub_requests::StatusCodeEnum serial_communication_hubImpl::handle_modbus_write_multiple_registers(
|
||||
int& target_device_id, int& first_register_address, types::serial_comm_hub_requests::VectorUint16& data_raw) {
|
||||
|
||||
types::serial_comm_hub_requests::Result result;
|
||||
std::vector<uint16_t> data;
|
||||
append_array<uint16_t, int>(data, data_raw.data);
|
||||
|
||||
result = perform_modbus_request(target_device_id, tiny_modbus::FunctionCode::WRITE_MULTIPLE_HOLDING_REGISTERS,
|
||||
first_register_address, data.size(), true, data);
|
||||
|
||||
return result.status_code;
|
||||
}
|
||||
|
||||
types::serial_comm_hub_requests::StatusCodeEnum
|
||||
serial_communication_hubImpl::handle_modbus_write_single_register(int& target_device_id, int& register_address,
|
||||
int& data) {
|
||||
types::serial_comm_hub_requests::Result result;
|
||||
|
||||
result = perform_modbus_request(target_device_id, tiny_modbus::FunctionCode::WRITE_SINGLE_HOLDING_REGISTER,
|
||||
register_address, 1, true, {static_cast<uint16_t>(data)});
|
||||
|
||||
return result.status_code;
|
||||
}
|
||||
|
||||
types::serial_comm_hub_requests::StatusCodeEnum
|
||||
serial_communication_hubImpl::handle_modbus_write_single_coil(int& target_device_id, int& coil_address, bool& data) {
|
||||
types::serial_comm_hub_requests::Result result;
|
||||
|
||||
result = perform_modbus_request(target_device_id, tiny_modbus::FunctionCode::WRITE_SINGLE_COIL, coil_address, 1,
|
||||
true, {static_cast<uint16_t>(data ? 0xFF00 : 0x0000)});
|
||||
|
||||
return result.status_code;
|
||||
}
|
||||
|
||||
types::serial_comm_hub_requests::ResultBool
|
||||
serial_communication_hubImpl::handle_modbus_read_coils(int& target_device_id, int& first_coil_address,
|
||||
int& num_coils_to_read) {
|
||||
const auto result = perform_modbus_request(target_device_id, tiny_modbus::FunctionCode::READ_COILS,
|
||||
first_coil_address, num_coils_to_read);
|
||||
|
||||
return convert_read_coils_result(result, num_coils_to_read);
|
||||
}
|
||||
|
||||
void serial_communication_hubImpl::handle_nonstd_write(int& target_device_id, int& first_register_address,
|
||||
int& num_registers_to_read) {
|
||||
}
|
||||
|
||||
types::serial_comm_hub_requests::Result serial_communication_hubImpl::handle_nonstd_read(int& target_device_id,
|
||||
int& first_register_address,
|
||||
int& num_registers_to_read) {
|
||||
types::serial_comm_hub_requests::Result result;
|
||||
result.status_code = types::serial_comm_hub_requests::StatusCodeEnum::Error;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace main
|
||||
} // namespace module
|
||||
@@ -0,0 +1,108 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef MAIN_SERIAL_COMMUNICATION_HUB_IMPL_HPP
|
||||
#define MAIN_SERIAL_COMMUNICATION_HUB_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/serial_communication_hub/Implementation.hpp>
|
||||
|
||||
#include "../SerialCommHub.hpp"
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
// insert your custom include headers here
|
||||
#include "tiny_modbus_rtu.hpp"
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <termios.h>
|
||||
#include <utils/thread.hpp>
|
||||
#include <vector>
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
namespace module {
|
||||
namespace main {
|
||||
|
||||
struct Conf {
|
||||
std::string serial_port;
|
||||
int baudrate;
|
||||
int parity;
|
||||
bool rtscts;
|
||||
bool ignore_echo;
|
||||
std::string rxtx_gpio_chip;
|
||||
int rxtx_gpio_line;
|
||||
bool rxtx_gpio_tx_high;
|
||||
int max_packet_size;
|
||||
int initial_timeout_ms;
|
||||
int within_message_timeout_ms;
|
||||
int retries;
|
||||
};
|
||||
|
||||
class serial_communication_hubImpl : public serial_communication_hubImplBase {
|
||||
public:
|
||||
serial_communication_hubImpl() = delete;
|
||||
serial_communication_hubImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<SerialCommHub>& mod,
|
||||
Conf& config) :
|
||||
serial_communication_hubImplBase(ev, "main"), mod(mod), config(config){};
|
||||
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
// insert your public definitions here
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
|
||||
protected:
|
||||
// command handler functions (virtual)
|
||||
virtual types::serial_comm_hub_requests::Result
|
||||
handle_modbus_read_holding_registers(int& target_device_id, int& first_register_address,
|
||||
int& num_registers_to_read) override;
|
||||
virtual types::serial_comm_hub_requests::Result
|
||||
handle_modbus_read_input_registers(int& target_device_id, int& first_register_address,
|
||||
int& num_registers_to_read) override;
|
||||
virtual types::serial_comm_hub_requests::StatusCodeEnum
|
||||
handle_modbus_write_multiple_registers(int& target_device_id, int& first_register_address,
|
||||
types::serial_comm_hub_requests::VectorUint16& data_raw) override;
|
||||
virtual types::serial_comm_hub_requests::StatusCodeEnum
|
||||
handle_modbus_write_single_register(int& target_device_id, int& register_address, int& data) override;
|
||||
virtual types::serial_comm_hub_requests::ResultBool
|
||||
handle_modbus_read_coils(int& target_device_id, int& first_coil_address, int& num_coils_to_read) override;
|
||||
virtual types::serial_comm_hub_requests::StatusCodeEnum
|
||||
handle_modbus_write_single_coil(int& target_device_id, int& coil_address, bool& data) override;
|
||||
virtual void handle_nonstd_write(int& target_device_id, int& first_register_address,
|
||||
int& num_registers_to_read) override;
|
||||
virtual types::serial_comm_hub_requests::Result
|
||||
handle_nonstd_read(int& target_device_id, int& first_register_address, int& num_registers_to_read) override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<SerialCommHub>& mod;
|
||||
const Conf& config;
|
||||
|
||||
virtual void init() override;
|
||||
virtual void ready() override;
|
||||
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
// insert your private definitions here
|
||||
types::serial_comm_hub_requests::Result
|
||||
perform_modbus_request(uint8_t device_address, tiny_modbus::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>());
|
||||
|
||||
tiny_modbus::TinyModbusRTU modbus;
|
||||
|
||||
std::mutex serial_mutex;
|
||||
bool system_error_logged{false};
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
};
|
||||
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
// insert other definitions here
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
|
||||
} // namespace main
|
||||
} // namespace module
|
||||
|
||||
#endif // MAIN_SERIAL_COMMUNICATION_HUB_IMPL_HPP
|
||||
71
tools/EVerest-main/modules/Misc/SerialCommHub/manifest.yaml
Normal file
71
tools/EVerest-main/modules/Misc/SerialCommHub/manifest.yaml
Normal file
@@ -0,0 +1,71 @@
|
||||
description: Hub to communicate with attached serial devices
|
||||
provides:
|
||||
main:
|
||||
description: Implementation of serial communication hub
|
||||
interface: serial_communication_hub
|
||||
config:
|
||||
serial_port:
|
||||
description: Serial port the hardware is connected to
|
||||
type: string
|
||||
default: /dev/ttyUSB0
|
||||
baudrate:
|
||||
description: Baudrate
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 230400
|
||||
default: 9600
|
||||
parity:
|
||||
description: 'Parity bit: 0: None, 1: Odd, 2: Even'
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 2
|
||||
default: 0
|
||||
rtscts:
|
||||
description: Use RTS/CTS hardware flow control
|
||||
type: boolean
|
||||
default: false
|
||||
ignore_echo:
|
||||
description: On some hardware every message that is sent is read back, this setting filters the sent message in the reply.
|
||||
type: boolean
|
||||
default: false
|
||||
rxtx_gpio_chip:
|
||||
description: GPIO chip to use to switch between RX/TX. An empty string disables GPIO usage.
|
||||
type: string
|
||||
default: ''
|
||||
rxtx_gpio_line:
|
||||
description: GPIO line to use to switch between RX/TX
|
||||
type: integer
|
||||
default: 0
|
||||
rxtx_gpio_tx_high:
|
||||
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
|
||||
# 7 is a minimum packet size to transfer a response
|
||||
minimum: 7
|
||||
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
|
||||
retries:
|
||||
description: Count of retries in case of error in Modbus query.
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 10
|
||||
default: 2
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Lars Dieckmann
|
||||
- Cornelius Claussen
|
||||
@@ -0,0 +1,526 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
// TODOs:
|
||||
// - sometimes we receive 0 bytes from sofar, find out why
|
||||
// - implement echo removal for chargebyte
|
||||
// - implement GPIO to switch rx/tx
|
||||
|
||||
#include "tiny_modbus_rtu.hpp"
|
||||
#include "crc16.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <endian.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <fmt/core.h>
|
||||
#include <iomanip>
|
||||
#include <ios>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <ostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/time.h>
|
||||
#include <system_error>
|
||||
#include <type_traits>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace tiny_modbus {
|
||||
|
||||
std::string FunctionCode_to_string(FunctionCode fc) {
|
||||
switch (fc) {
|
||||
case FunctionCode::READ_COILS:
|
||||
return "READ_COILS";
|
||||
case FunctionCode::READ_DISCRETE_INPUTS:
|
||||
return "READ_DISCRETE_INPUTS";
|
||||
case FunctionCode::READ_MULTIPLE_HOLDING_REGISTERS:
|
||||
return "READ_MULTIPLE_HOLDING_REGISTERS";
|
||||
case FunctionCode::READ_INPUT_REGISTERS:
|
||||
return "READ_INPUT_REGISTERS";
|
||||
case FunctionCode::WRITE_SINGLE_COIL:
|
||||
return "WRITE_SINGLE_COIL";
|
||||
case FunctionCode::WRITE_SINGLE_HOLDING_REGISTER:
|
||||
return "WRITE_SINGLE_HOLDING_REGISTER";
|
||||
case FunctionCode::WRITE_MULTIPLE_COILS:
|
||||
return "WRITE_MULTIPLE_COILS";
|
||||
case FunctionCode::WRITE_MULTIPLE_HOLDING_REGISTERS:
|
||||
return "WRITE_MULTIPLE_HOLDING_REGISTERS";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::string FunctionCode_to_string_with_hex(FunctionCode fc) {
|
||||
return fmt::format("{}({:#04x})", FunctionCode_to_string(fc), (unsigned int)fc);
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const FunctionCode& fc) {
|
||||
os << FunctionCode_to_string_with_hex(fc);
|
||||
return os;
|
||||
}
|
||||
|
||||
// This is a replacement for system library tcdrain().
|
||||
// tcdrain() returns when all bytes are written to the UART, but it actually returns about 10msecs or more after the
|
||||
// last byte has been written. This function tries to return as fast as possible instead.
|
||||
static void fast_tcdrain(int fd) {
|
||||
// in user space, the only way to find out if there are still bits to be shiftet out is to poll line status register
|
||||
// as fast as we can
|
||||
uint32_t lsr;
|
||||
do {
|
||||
ioctl(fd, TIOCSERGETLSR, &lsr);
|
||||
} while (!(lsr & TIOCSER_TEMT));
|
||||
}
|
||||
|
||||
static auto check_for_exception(uint8_t received_function_code) {
|
||||
return received_function_code & (1 << 7);
|
||||
}
|
||||
|
||||
static void clear_exception_bit(uint8_t& received_function_code) {
|
||||
received_function_code &= ~(1 << 7);
|
||||
}
|
||||
|
||||
static std::string hexdump(const uint8_t* msg, int msg_len) {
|
||||
std::stringstream ss;
|
||||
for (int i = 0; i < msg_len; i++) {
|
||||
ss << "<" << std::nouppercase << std::setfill('0') << std::setw(2) << std::hex << (int)msg[i] << ">";
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
static void append_checksum(uint8_t* msg, int msg_len) {
|
||||
if (msg_len < 5)
|
||||
return;
|
||||
uint16_t crc_sum = calculate_modbus_crc16(msg, msg_len - 2);
|
||||
memcpy(msg + msg_len - 2, &crc_sum, 2);
|
||||
}
|
||||
|
||||
static bool validate_checksum(const uint8_t* msg, int msg_len) {
|
||||
if (msg_len < 5)
|
||||
return false;
|
||||
// check crc
|
||||
uint16_t crc_sum = calculate_modbus_crc16(msg, msg_len - 2);
|
||||
uint16_t crc_msg;
|
||||
memcpy(&crc_msg, msg + msg_len - 2, 2);
|
||||
return (crc_msg == crc_sum);
|
||||
}
|
||||
|
||||
static std::vector<uint16_t> decode_reply(const uint8_t* buf, int len, uint8_t expected_device_address,
|
||||
FunctionCode function) {
|
||||
std::vector<uint16_t> result;
|
||||
if (len == 0) {
|
||||
throw TimeoutException("Packet receive timeout");
|
||||
} else if (len < MODBUS_MIN_REPLY_SIZE) {
|
||||
throw ShortPacketException(fmt::format("Packet too small: only {} bytes", len));
|
||||
}
|
||||
if (expected_device_address != buf[DEVICE_ADDRESS_POS]) {
|
||||
throw AddressMismatchException(fmt::format("Device address mismatch: expected: {} received: {}",
|
||||
expected_device_address, buf[DEVICE_ADDRESS_POS]) +
|
||||
": " + hexdump(buf, len));
|
||||
}
|
||||
|
||||
bool exception = false;
|
||||
uint8_t function_code_recvd = buf[FUNCTION_CODE_POS];
|
||||
if (check_for_exception(function_code_recvd)) {
|
||||
// highest bit is set for exception reply
|
||||
exception = true;
|
||||
// clear error bit
|
||||
clear_exception_bit(function_code_recvd);
|
||||
}
|
||||
|
||||
if (function != function_code_recvd) {
|
||||
throw FunctionCodeMismatchException(fmt::format("Function code mismatch: expected: {} received: {}",
|
||||
static_cast<std::underlying_type_t<FunctionCode>>(function),
|
||||
function_code_recvd));
|
||||
}
|
||||
|
||||
if (!validate_checksum(buf, len)) {
|
||||
throw ChecksumErrorException("Retrieved Modbus checksum does not match calculated value.");
|
||||
}
|
||||
|
||||
if (exception) {
|
||||
// handle exception message
|
||||
uint8_t err_code = buf[RES_EXCEPTION_CODE];
|
||||
switch (err_code) {
|
||||
case 0x01:
|
||||
throw ModbusException("Modbus exception: Illegal function");
|
||||
break;
|
||||
case 0x02:
|
||||
throw ModbusException("Modbus exception: Illegal data address");
|
||||
break;
|
||||
case 0x03:
|
||||
throw ModbusException("Modbus exception: Illegal data value");
|
||||
break;
|
||||
case 0x04:
|
||||
throw ModbusException("Modbus exception: Client device failure");
|
||||
break;
|
||||
case 0x05:
|
||||
throw ModbusException("Modbus ACK");
|
||||
break;
|
||||
case 0x06:
|
||||
throw ModbusException("Modbus exception: Client device busy");
|
||||
break;
|
||||
case 0x07:
|
||||
throw ModbusException("Modbus exception: NACK");
|
||||
break;
|
||||
case 0x08:
|
||||
throw ModbusException("Modbus exception: Memory parity error");
|
||||
break;
|
||||
case 0x09:
|
||||
throw ModbusException("Modbus exception: Out of resources");
|
||||
break;
|
||||
case 0x0A:
|
||||
throw ModbusException("Modbus exception: Gateway path unavailable");
|
||||
break;
|
||||
case 0x0B:
|
||||
throw ModbusException("Modbus exception: Gateway target device failed to respond");
|
||||
break;
|
||||
default:
|
||||
throw ModbusException("Modbus exception: Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
// For a write reply we always get 4 bytes
|
||||
uint8_t byte_cnt = 4;
|
||||
int start_of_result = RES_TX_START_OF_PAYLOAD;
|
||||
bool even_byte_cnt_expected = false;
|
||||
|
||||
// Was it a read reply?
|
||||
switch (function) {
|
||||
case FunctionCode::WRITE_SINGLE_COIL:
|
||||
case FunctionCode::WRITE_SINGLE_HOLDING_REGISTER:
|
||||
case FunctionCode::WRITE_MULTIPLE_COILS:
|
||||
case FunctionCode::WRITE_MULTIPLE_HOLDING_REGISTERS:
|
||||
// no - nothing to do
|
||||
break;
|
||||
case FunctionCode::READ_MULTIPLE_HOLDING_REGISTERS:
|
||||
case FunctionCode::READ_INPUT_REGISTERS:
|
||||
// yes - for 16-bit wide registers thus we can assume an even byte count
|
||||
even_byte_cnt_expected = true;
|
||||
[[fallthrough]];
|
||||
case FunctionCode::READ_COILS:
|
||||
case FunctionCode::READ_DISCRETE_INPUTS:
|
||||
// yes
|
||||
// adapt byte count and starting pos
|
||||
byte_cnt = buf[RES_RX_LEN_POS];
|
||||
start_of_result = RES_RX_START_OF_PAYLOAD;
|
||||
break;
|
||||
default:
|
||||
throw std::logic_error("Missing implementation for function code " + FunctionCode_to_string_with_hex(function));
|
||||
}
|
||||
|
||||
// check if result is completely in received data
|
||||
if (start_of_result + byte_cnt > len) {
|
||||
throw IncompletePacketException("Result data not completely in received message.");
|
||||
}
|
||||
|
||||
// check even number of bytes
|
||||
if (even_byte_cnt_expected && byte_cnt % 2 == 1) {
|
||||
throw OddByteCountException("For " + FunctionCode_to_string_with_hex(function) +
|
||||
" an even byte count is expected in the response.");
|
||||
}
|
||||
|
||||
// ready to copy actual result data to output, so pre-allocate enough memory for the output
|
||||
result.reserve((byte_cnt + 1) / 2);
|
||||
|
||||
for (int i = start_of_result; i < start_of_result + byte_cnt; i += 2) {
|
||||
uint16_t t = 0;
|
||||
const size_t num_bytes_to_copy = (i < len - 1) ? 2 : 1;
|
||||
memcpy(&t, buf + i, num_bytes_to_copy);
|
||||
t = be16toh(t);
|
||||
result.push_back(t);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
TinyModbusRTU::~TinyModbusRTU() {
|
||||
if (fd != -1)
|
||||
close(fd);
|
||||
}
|
||||
|
||||
bool TinyModbusRTU::open_device(const std::string& device, int _baud, bool _ignore_echo,
|
||||
const Everest::GpioSettings& rxtx_gpio_settings, const Parity parity, bool rtscts,
|
||||
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);
|
||||
rxtx_gpio.set_output(true);
|
||||
|
||||
fd = open(device.c_str(), O_RDWR | O_NOCTTY | O_SYNC);
|
||||
if (fd < 0) {
|
||||
EVLOG_error << fmt::format("Serial: error {} opening {}: {}\n", errno, device, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
int baud;
|
||||
switch (_baud) {
|
||||
case 9600:
|
||||
baud = B9600;
|
||||
break;
|
||||
case 19200:
|
||||
baud = B19200;
|
||||
break;
|
||||
case 38400:
|
||||
baud = B38400;
|
||||
break;
|
||||
case 57600:
|
||||
baud = B57600;
|
||||
break;
|
||||
case 115200:
|
||||
baud = B115200;
|
||||
break;
|
||||
case 230400:
|
||||
baud = B230400;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
struct termios tty;
|
||||
if (tcgetattr(fd, &tty) != 0) {
|
||||
printf("Serial: error %d from tcgetattr\n", errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
cfsetospeed(&tty, baud);
|
||||
cfsetispeed(&tty, baud);
|
||||
|
||||
tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8; // 8-bit chars
|
||||
// disable IGNBRK for mismatched speed tests; otherwise receive break
|
||||
// as \000 chars
|
||||
tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON | IXOFF | IXANY);
|
||||
tty.c_lflag = 0; // no signaling chars, no echo,
|
||||
// no canonical processing
|
||||
tty.c_oflag = 0; // no remapping, no delays
|
||||
tty.c_cc[VMIN] = 1; // read blocks
|
||||
tty.c_cc[VTIME] = 1; // 0.1 seconds inter character read timeout after first byte was received
|
||||
|
||||
tty.c_cflag |= (CLOCAL | CREAD); // ignore modem controls,
|
||||
// enable reading
|
||||
if (parity == Parity::ODD) {
|
||||
tty.c_cflag |= (PARENB | PARODD); // odd parity
|
||||
} else if (parity == Parity::EVEN) { // even parity
|
||||
tty.c_cflag &= ~PARODD;
|
||||
tty.c_cflag |= PARENB;
|
||||
} else {
|
||||
tty.c_cflag &= ~(PARENB | PARODD); // shut off parity
|
||||
}
|
||||
tty.c_cflag &= ~CSTOPB; // 1 Stop bit
|
||||
|
||||
if (rtscts) {
|
||||
tty.c_cflag |= CRTSCTS;
|
||||
} else {
|
||||
tty.c_cflag &= ~CRTSCTS;
|
||||
}
|
||||
|
||||
if (tcsetattr(fd, TCSANOW, &tty) != 0) {
|
||||
printf("Serial: error %d from tcsetattr\n", errno);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int TinyModbusRTU::read_reply(uint8_t* rxbuf, int rxbuf_len) {
|
||||
if (fd == -1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Lambda to convert std::chrono to timeval.
|
||||
auto to_timeval = [](const auto& time) {
|
||||
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 = 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
|
||||
break;
|
||||
} else { // received more bytes, add them to buffer
|
||||
// do we have space in the rx buffer left?
|
||||
if (bytes_read_total >= rxbuf_len) {
|
||||
// no buffer space left, but more to read.
|
||||
break;
|
||||
}
|
||||
|
||||
int bytes_read = read(fd, rxbuf + bytes_read_total, rxbuf_len - bytes_read_total);
|
||||
if (bytes_read > 0) {
|
||||
bytes_read_total += bytes_read;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
if (max_packet_size < MODBUS_MIN_REPLY_SIZE + 2) {
|
||||
EVLOG_error << fmt::format("Max packet size too small: {}", max_packet_size);
|
||||
return {};
|
||||
}
|
||||
|
||||
const uint16_t register_chunk = (max_packet_size - MODBUS_MIN_REPLY_SIZE) / 2;
|
||||
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;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> _make_single_write_request(uint8_t device_address, FunctionCode function,
|
||||
uint16_t register_address, bool wait_for_reply, uint16_t data) {
|
||||
const int req_len = 8;
|
||||
std::vector<uint8_t> req(req_len);
|
||||
|
||||
req[DEVICE_ADDRESS_POS] = device_address;
|
||||
req[FUNCTION_CODE_POS] = static_cast<uint8_t>(function);
|
||||
|
||||
register_address = htobe16(register_address);
|
||||
data = htobe16(data);
|
||||
memcpy(req.data() + REQ_TX_FIRST_REGISTER_ADDR_POS, ®ister_address, 2);
|
||||
memcpy(req.data() + REQ_TX_SINGLE_REG_PAYLOAD_POS, &data, 2);
|
||||
append_checksum(req.data(), req_len);
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> _make_generic_request(uint8_t device_address, FunctionCode function,
|
||||
uint16_t first_register_address, uint16_t register_quantity,
|
||||
std::vector<uint16_t> request) {
|
||||
// size of request
|
||||
int req_len = (request.size() == 0 ? 0 : 2 * request.size() + 1) + MODBUS_BASE_PAYLOAD_SIZE;
|
||||
std::vector<uint8_t> req(req_len);
|
||||
|
||||
// add header
|
||||
req[DEVICE_ADDRESS_POS] = device_address;
|
||||
req[FUNCTION_CODE_POS] = function;
|
||||
|
||||
first_register_address = htobe16(first_register_address);
|
||||
register_quantity = htobe16(register_quantity);
|
||||
memcpy(req.data() + REQ_TX_FIRST_REGISTER_ADDR_POS, &first_register_address, 2);
|
||||
memcpy(req.data() + REQ_TX_QUANTITY_POS, ®ister_quantity, 2);
|
||||
|
||||
if (function == FunctionCode::WRITE_MULTIPLE_HOLDING_REGISTERS) {
|
||||
// write byte count
|
||||
req[REQ_TX_MULTIPLE_REG_BYTE_COUNT_POS] = request.size() * 2;
|
||||
// add request data
|
||||
int i = REQ_TX_MULTIPLE_REG_BYTE_COUNT_POS + 1;
|
||||
for (auto r : request) {
|
||||
r = htobe16(r);
|
||||
memcpy(req.data() + i, &r, 2);
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
|
||||
// set checksum in the last 2 bytes
|
||||
append_checksum(req.data(), req_len);
|
||||
|
||||
return req;
|
||||
}
|
||||
/*
|
||||
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_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) {
|
||||
{
|
||||
if (fd == -1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto req =
|
||||
function == FunctionCode::WRITE_SINGLE_HOLDING_REGISTER or function == FunctionCode::WRITE_SINGLE_COIL
|
||||
? _make_single_write_request(device_address, function, first_register_address, wait_for_reply,
|
||||
request.at(0))
|
||||
: _make_generic_request(device_address, function, first_register_address, register_quantity, request);
|
||||
// clear input and output buffer
|
||||
tcflush(fd, TCIOFLUSH);
|
||||
|
||||
// write to serial port
|
||||
rxtx_gpio.set(false);
|
||||
|
||||
uint8_t* buffer = req.data();
|
||||
ssize_t written = 0;
|
||||
|
||||
while (written < req.size()) {
|
||||
ssize_t c = write(fd, &buffer[written], req.size() - written);
|
||||
if (c == -1)
|
||||
throw std::system_error(errno, std::generic_category(), "Could not send Modbus request");
|
||||
written += c;
|
||||
}
|
||||
|
||||
if (rxtx_gpio.is_ready()) {
|
||||
// if we are using GPIO to switch between RX/TX, use the fast version of tcdrain with exact timing
|
||||
fast_tcdrain(fd);
|
||||
} else {
|
||||
// without GPIO switching, use regular tcdrain as not all UART drivers implement the ioctl
|
||||
tcdrain(fd);
|
||||
}
|
||||
rxtx_gpio.set(true);
|
||||
|
||||
if (ignore_echo) {
|
||||
// read back echo of what we sent and ignore it
|
||||
read_reply(req.data(), req.size());
|
||||
}
|
||||
}
|
||||
|
||||
if (wait_for_reply) {
|
||||
// wait for reply
|
||||
uint8_t rxbuf[MODBUS_MAX_REPLY_SIZE];
|
||||
int bytes_read_total = read_reply(rxbuf, sizeof(rxbuf));
|
||||
return decode_reply(rxbuf, bytes_read_total, device_address, function);
|
||||
}
|
||||
return std::vector<uint16_t>();
|
||||
}
|
||||
|
||||
} // namespace tiny_modbus
|
||||
@@ -0,0 +1,118 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/*
|
||||
This is a tiny and fast modbus RTU implementation
|
||||
*/
|
||||
#ifndef TINY_MODBUS_RTU
|
||||
#define TINY_MODBUS_RTU
|
||||
|
||||
#include <chrono>
|
||||
#include <ostream>
|
||||
#include <stdexcept>
|
||||
#include <stdint.h>
|
||||
#include <termios.h>
|
||||
|
||||
#include <everest/gpio/gpio.hpp>
|
||||
#include <everest/logging.hpp>
|
||||
|
||||
namespace tiny_modbus {
|
||||
|
||||
constexpr int DEVICE_ADDRESS_POS = 0x00;
|
||||
constexpr int FUNCTION_CODE_POS = 0x01;
|
||||
|
||||
constexpr int REQ_TX_FIRST_REGISTER_ADDR_POS = 0x02;
|
||||
constexpr int REQ_TX_QUANTITY_POS = 0x04;
|
||||
constexpr int REQ_TX_SINGLE_REG_PAYLOAD_POS = 0x04;
|
||||
|
||||
constexpr int REQ_TX_MULTIPLE_REG_BYTE_COUNT_POS = 0x06;
|
||||
|
||||
constexpr int RES_RX_LEN_POS = 0x02;
|
||||
constexpr int RES_RX_START_OF_PAYLOAD = 0x03;
|
||||
constexpr int RES_TX_START_OF_PAYLOAD = 0x02;
|
||||
constexpr int RES_EXCEPTION_CODE = 0x02;
|
||||
|
||||
constexpr int MODBUS_MAX_REPLY_SIZE = 255 + 6;
|
||||
constexpr int MODBUS_MIN_REPLY_SIZE = 5;
|
||||
constexpr int MODBUS_BASE_PAYLOAD_SIZE = 8;
|
||||
|
||||
enum class Parity : uint8_t {
|
||||
NONE = 0,
|
||||
ODD = 1,
|
||||
EVEN = 2
|
||||
};
|
||||
|
||||
enum FunctionCode : uint8_t {
|
||||
READ_COILS = 0x01,
|
||||
READ_DISCRETE_INPUTS = 0x02,
|
||||
READ_MULTIPLE_HOLDING_REGISTERS = 0x03,
|
||||
READ_INPUT_REGISTERS = 0x04,
|
||||
WRITE_SINGLE_COIL = 0x05,
|
||||
WRITE_SINGLE_HOLDING_REGISTER = 0x06,
|
||||
WRITE_MULTIPLE_COILS = 0x0F,
|
||||
WRITE_MULTIPLE_HOLDING_REGISTERS = 0x10,
|
||||
};
|
||||
|
||||
std::string FunctionCode_to_string(FunctionCode fc);
|
||||
std::string FunctionCode_to_string_with_hex(FunctionCode fc);
|
||||
std::ostream& operator<<(std::ostream& os, const FunctionCode& fc);
|
||||
|
||||
class TinyModbusException : public std::runtime_error {
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
class TimeoutException : public TinyModbusException {
|
||||
using TinyModbusException::TinyModbusException;
|
||||
};
|
||||
class ShortPacketException : public TinyModbusException {
|
||||
using TinyModbusException::TinyModbusException;
|
||||
};
|
||||
class AddressMismatchException : public TinyModbusException {
|
||||
using TinyModbusException::TinyModbusException;
|
||||
};
|
||||
class FunctionCodeMismatchException : public TinyModbusException {
|
||||
using TinyModbusException::TinyModbusException;
|
||||
};
|
||||
class ChecksumErrorException : public TinyModbusException {
|
||||
using TinyModbusException::TinyModbusException;
|
||||
};
|
||||
class IncompletePacketException : public TinyModbusException {
|
||||
using TinyModbusException::TinyModbusException;
|
||||
};
|
||||
class OddByteCountException : public TinyModbusException {
|
||||
using TinyModbusException::TinyModbusException;
|
||||
};
|
||||
class ModbusException : public TinyModbusException {
|
||||
using TinyModbusException::TinyModbusException;
|
||||
};
|
||||
|
||||
class TinyModbusRTU {
|
||||
|
||||
public:
|
||||
~TinyModbusRTU();
|
||||
|
||||
bool open_device(const std::string& device, int baud, bool ignore_echo,
|
||||
const Everest::GpioSettings& rxtx_gpio_settings, const Parity parity, bool rtscts,
|
||||
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, uint16_t chunk_size, bool wait_for_reply = true,
|
||||
std::vector<uint16_t> request = std::vector<uint16_t>());
|
||||
|
||||
private:
|
||||
// Serial interface
|
||||
int fd{-1};
|
||||
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;
|
||||
};
|
||||
|
||||
} // namespace tiny_modbus
|
||||
#endif
|
||||
Reference in New Issue
Block a user