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:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View 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",
],
)

View 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

View File

@@ -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

View File

@@ -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

View 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;
}

View 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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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, &register_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, &register_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

View File

@@ -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