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,34 @@
#
# 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_sources(${MODULE_NAME}
PRIVATE
slip_protocol.cpp
serial_device.cpp
app_layer.cpp
diagnostics.cpp
)
target_compile_features(${MODULE_NAME} PUBLIC cxx_std_17)
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
target_sources(${MODULE_NAME}
PRIVATE
"main/powermeterImpl.cpp"
)
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
# insert other things like install cmds etc here
target_link_libraries(${MODULE_NAME} PRIVATE
everest::crc
everest::framework
)
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "DZG_GSH01.hpp"
namespace module {
void DZG_GSH01::init() {
invoke_init(*p_main);
}
void DZG_GSH01::ready() {
invoke_ready(*p_main);
}
} // namespace module

View File

@@ -0,0 +1,60 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef DZG_GSH01_HPP
#define DZG_GSH01_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 2
//
#include "ld-ev.hpp"
// headers for provided interface implementations
#include <generated/interfaces/powermeter/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 DZG_GSH01 : public Everest::ModuleBase {
public:
DZG_GSH01() = delete;
DZG_GSH01(const ModuleInfo& info, Everest::MqttProvider& mqtt_provider, std::unique_ptr<powermeterImplBase> p_main,
Conf& config) :
ModuleBase(info), mqtt(mqtt_provider), p_main(std::move(p_main)), config(config){};
Everest::MqttProvider& mqtt;
const std::unique_ptr<powermeterImplBase> 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 // DZG_GSH01_HPP

View File

@@ -0,0 +1,31 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef TIMEOUT_HPP
#define TIMEOUT_HPP
#include <chrono>
using namespace std::chrono;
/*
Simple helper class for a timeout
*/
class Timeout {
public:
explicit Timeout(milliseconds _t) : t(_t), start(steady_clock::now()) {
}
bool reached() {
if ((steady_clock::now() - start) > t)
return true;
else
return false;
}
private:
milliseconds t;
time_point<steady_clock> start;
};
#endif

View File

@@ -0,0 +1,393 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "app_layer.hpp"
#include <cstring>
#include <endian.h>
#include <everest/logging.hpp>
#include <fcntl.h>
#include <string>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <fmt/core.h>
namespace app_layer {
// Status Word Bit Meaning - initialization in cpp-file necessary (static)
std::map<std::uint64_t, std::string> StatusWord::bit_meaning = {
{0x0000000000000001, "--> Error Real time clock"},
{0x0000000000000002, "--> Error Configuration Memory"},
{0x0000000000000008, "--> Error Signature Module"},
{0x0000000000000020, "--> Error Meter Configuration"},
{0x0000000000000040, "--> Error Meter Communication"},
{0x0000000000000080, "--> Error Meter Fatal"},
{0x0000000000000100, "--> Error External Display Not Available"},
{0x0000000000010000, "--> Status Real time clock not in sync"},
{0x0000000000020000, "--> Status Charging"},
{0x0000000000080000, "--> Status Compensated Mode Impedance"},
{0x0000000000100000, "--> Status external Display used"},
{0x0000000000200000, "--> Status Is Ready for Charging"},
{0x0000000000400000, "--> Status Session Not Completed"},
{0x0000000000800000, "--> Status Eichlog Is Full"},
{0x0000000001000000, "--> Status Charge Process List is full"},
{0x0000000400000000, "--> Status Word Identification Bit Okay"},
{0x0000000800000000, "--> Error DC Line Loss Power Abnormal"},
{0x0000001000000000, "--> Factory Jumper set"}};
std::uint32_t timepoint_to_uint32(date::utc_clock::time_point timepoint) {
return static_cast<std::uint32_t>(
std::chrono::duration_cast<std::chrono::seconds>(timepoint.time_since_epoch()).count());
}
void insert_u16_as_u8s(std::vector<std::uint8_t>& vec, std::uint16_t u16) {
std::uint8_t upper = std::uint8_t((u16 >> 8) & 0x00FF);
std::uint8_t lower = std::uint8_t(u16 & 0x00FF);
vec.push_back(lower);
vec.push_back(upper);
}
void insert_u32_as_u8s(std::vector<std::uint8_t>& vec, std::uint32_t u32) {
vec.push_back(std::uint8_t(u32 & 0x000000FF));
vec.push_back(std::uint8_t((u32 >> 8) & 0x000000FF));
vec.push_back(std::uint8_t((u32 >> 16) & 0x000000FF));
vec.push_back(std::uint8_t((u32 >> 24) & 0x000000FF));
}
std::vector<std::uint8_t> AppLayer::create_command(app_layer::Command cmd) {
std::vector<std::uint8_t> command_data{};
insert_u16_as_u8s(command_data, static_cast<std::uint16_t>(cmd.type));
insert_u16_as_u8s(command_data, static_cast<std::uint16_t>(cmd.length));
command_data.push_back(static_cast<std::uint8_t>(app_layer::CommandStatus::OK));
for (std::uint16_t i = 0; i < cmd.data.size(); i++) {
command_data.push_back(cmd.data[i]);
}
return command_data;
}
std::vector<std::uint8_t> AppLayer::create_simple_command(app_layer::CommandType cmd_type) {
app_layer::Command cmd{};
cmd.type = cmd_type;
cmd.length = 0x0005;
cmd.status = app_layer::CommandStatus::OK;
return create_command(cmd);
}
int8_t AppLayer::get_utc_offset_in_quarter_hours(
const std::chrono::time_point<std::chrono::system_clock>& timepoint_system_clock) {
std::stringstream offset;
std::int8_t offset_quarterhours = 0;
auto tm = std::chrono::system_clock::to_time_t(timepoint_system_clock);
offset << std::put_time(std::localtime(&tm), "%z");
int offset_int = std::stoi(offset.str());
int offset_h = offset_int / 100;
int offset_remaining = offset_int % 100; // in case of timezones that are not full-hour offsets of UTC
if (offset_remaining != 0) {
std::int8_t offset_remaining_extra_hour = offset_remaining / 60;
if (offset_remaining_extra_hour != 0) {
offset_quarterhours += offset_remaining_extra_hour * 4; // can be positive or negative
offset_remaining -= offset_remaining_extra_hour * 4;
}
std::int8_t offset_remaining_quarterhours = offset_remaining / 15;
offset_quarterhours += offset_remaining_quarterhours;
}
offset_quarterhours += offset_h * 4;
return offset_quarterhours;
}
void AppLayer::create_command_start_transaction(app_layer::UserIdStatus user_id_status,
app_layer::UserIdType user_id_type, std::string user_id_data,
std::vector<std::uint8_t>& command_data) {
app_layer::Command cmd{};
cmd.type = app_layer::CommandType::START_TRANSACTION;
cmd.length = 0x0034;
cmd.status = app_layer::CommandStatus::OK;
std::chrono::time_point<std::chrono::system_clock> timepoint = std::chrono::system_clock::now();
std::int8_t gmt_offset_quarters_of_an_hour = get_utc_offset_in_quarter_hours(timepoint);
insert_u32_as_u8s(cmd.data, timepoint_to_uint32(date::utc_clock::from_sys(timepoint)));
cmd.data.push_back(gmt_offset_quarters_of_an_hour);
cmd.data.push_back(static_cast<std::uint8_t>(user_id_status));
cmd.data.push_back(static_cast<std::uint8_t>(user_id_type));
std::uint8_t byte_count = 0;
for (std::uint8_t databyte : user_id_data) { // push up to 40 characters of user id name into command
cmd.data.push_back(databyte);
byte_count++;
if (byte_count >= 40)
break;
}
while (byte_count < 40) { // fill remaining user id name characters with zeros
cmd.data.push_back(0x00);
byte_count++;
}
command_data = create_command(cmd);
}
void AppLayer::create_command_stop_transaction(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::STOP_TRANSACTION);
}
void AppLayer::create_command_get_time(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::TIME);
}
void AppLayer::create_command_set_time(date::utc_clock::time_point timepoint,
std::int8_t gmt_offset_quarters_of_an_hour,
std::vector<std::uint8_t>& command_data) {
app_layer::Command cmd{};
cmd.type = app_layer::CommandType::TIME;
cmd.length = 0x000A;
cmd.status = app_layer::CommandStatus::OK;
insert_u32_as_u8s(cmd.data, timepoint_to_uint32(timepoint));
cmd.data.push_back(gmt_offset_quarters_of_an_hour);
command_data = create_command(cmd);
}
void AppLayer::create_command_get_bus_address(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::METER_BUS_ADDR);
}
void AppLayer::create_command_set_bus_address(std::uint8_t bus_address, std::vector<std::uint8_t>& command_data) {
app_layer::Command cmd{};
cmd.type = app_layer::CommandType::METER_BUS_ADDR;
cmd.length = 0x0006;
cmd.status = app_layer::CommandStatus::OK;
cmd.data.push_back(bus_address);
command_data = create_command(cmd);
}
void AppLayer::create_command_get_voltage(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::GET_VOLTAGE_L1);
}
void AppLayer::create_command_get_current(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::GET_CURRENT_L1);
}
void AppLayer::create_command_get_import_power(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::GET_IMPORT_DEV_POWER);
}
void AppLayer::create_command_get_total_dev_import_energy(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::GET_TOTAL_IMPORT_DEV_ENERGY);
}
void AppLayer::create_command_get_total_power(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::GET_TOTAL_DEV_POWER);
}
void AppLayer::create_command_get_total_start_import_energy(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::GET_TOTAL_START_IMPORT_DEV_ENERGY);
}
void AppLayer::create_command_get_total_stop_import_energy(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::GET_TOTAL_STOP_IMPORT_DEV_ENERGY);
}
void AppLayer::create_command_get_total_transaction_duration(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::GET_TRANSACT_TOTAL_DURATION);
}
void AppLayer::create_command_get_pubkey_str16(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::GET_PUBKEY_STR16);
}
void AppLayer::create_command_get_pubkey_asn1(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::GET_PUBKEY_ASN1);
}
void AppLayer::create_command_get_ocmf_stats(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::OCMF_STATS);
}
/* OCMF ID: 1..235000
OCMF data from specified transaction will be at minimum import energy of transaction
*/
void AppLayer::create_command_get_transaction_ocmf(std::uint32_t ocmf_id, std::vector<std::uint8_t>& command_data) {
app_layer::Command cmd{};
cmd.type = app_layer::CommandType::GET_OCMF;
cmd.length = 0x0009;
cmd.status = app_layer::CommandStatus::OK;
insert_u32_as_u8s(cmd.data, ocmf_id);
command_data = create_command(cmd);
}
void AppLayer::create_command_get_last_transaction_ocmf(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::GET_LAST_OCMF);
}
void AppLayer::create_command_get_charge_point_id(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::CHARGE_POINT_ID);
}
/* only works in "assembly mode" */
void AppLayer::create_command_set_charge_point_id(app_layer::UserIdType id_type, std::string id_data,
std::vector<std::uint8_t>& command_data) {
app_layer::Command cmd{};
cmd.type = app_layer::CommandType::CHARGE_POINT_ID;
cmd.length = 0x0013;
cmd.status = app_layer::CommandStatus::OK;
cmd.data.push_back(static_cast<std::uint8_t>(id_type));
std::uint8_t byte_count = 0;
for (std::uint8_t databyte : id_data) { // push up to 13 characters of id data into command
cmd.data.push_back(databyte);
byte_count++;
if (byte_count >= 13)
break;
}
while (byte_count < 13) { // fill remaining user id name characters with zeros
cmd.data.push_back(0x00);
byte_count++;
}
command_data = create_command(cmd);
}
void AppLayer::create_command_get_log_stats(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::GET_LOG_STATS);
}
/* log entry ids: 1..2500 */
void AppLayer::create_command_get_log_entry(std::uint32_t log_entry_id, std::vector<std::uint8_t>& command_data) {
app_layer::Command cmd{};
cmd.type = app_layer::CommandType::GET_LOG_ENTRY;
cmd.length = 0x0009;
cmd.status = app_layer::CommandStatus::OK;
insert_u32_as_u8s(cmd.data, log_entry_id);
command_data = create_command(cmd);
}
void AppLayer::create_command_get_last_log_entry(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::GET_LAST_LOG_ENTRY);
}
/* log entry ids: 1..2500
thus: if 20 log entries and log_entry_id == 2, then log entry 18 will be returned
*/
void AppLayer::create_command_get_log_entry_reverse(std::uint32_t log_entry_id,
std::vector<std::uint8_t>& command_data) {
app_layer::Command cmd{};
cmd.type = app_layer::CommandType::GET_LOG_ENTRY_REVERSE;
cmd.length = 0x0009;
cmd.status = app_layer::CommandStatus::OK;
insert_u32_as_u8s(cmd.data, log_entry_id);
command_data = create_command(cmd);
}
void AppLayer::create_command_get_application_mode(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::APP_MODE);
}
void AppLayer::create_command_set_application_mode(app_layer::ApplicationBoardMode mode,
std::vector<std::uint8_t>& command_data) {
app_layer::Command cmd{};
cmd.type = app_layer::CommandType::APP_MODE;
cmd.length = 0x0006;
cmd.status = app_layer::CommandStatus::OK;
cmd.data.push_back(static_cast<std::uint8_t>(mode));
command_data = create_command(cmd);
}
void AppLayer::create_command_get_line_loss_impedance(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::LINE_LOSS_IMPEDANCE);
}
void AppLayer::create_command_set_line_loss_impedance(std::uint16_t ll_impedance,
std::vector<std::uint8_t>& command_data) {
app_layer::Command cmd{};
cmd.type = app_layer::CommandType::LINE_LOSS_IMPEDANCE;
cmd.length = 0x0009;
cmd.status = app_layer::CommandStatus::OK;
insert_u32_as_u8s(cmd.data, ll_impedance);
command_data = create_command(cmd);
}
// diagnostics
void AppLayer::create_command_get_hardware_version(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::HW_VERSION);
}
void AppLayer::create_command_get_server_id(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::SERVER_ID);
}
void AppLayer::create_command_get_serial_number(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::SERIAL_NR);
}
void AppLayer::create_command_get_application_fw_version(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::APP_FW_VERSION);
}
void AppLayer::create_command_get_application_fw_checksum(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::APP_FW_CHECKSUM);
}
void AppLayer::create_command_get_application_fw_hash(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::APP_FW_HASH);
}
void AppLayer::create_command_get_status_word(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::STATUS_WORD);
}
void AppLayer::create_command_get_metering_fw_version(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::MT_FW_VERSION);
}
void AppLayer::create_command_get_metering_fw_checksum(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::MT_FW_CHECKSUM);
}
void AppLayer::create_command_get_metering_mode(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::MT_MODE);
}
void AppLayer::create_command_get_bootloader_version(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::BOOTL_VERSION);
}
/* doubles as OCMF "meter model name" */
void AppLayer::create_command_get_device_type(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(app_layer::CommandType::DEVICE_TYPE);
}
} // namespace app_layer

View File

@@ -0,0 +1,353 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
/*
This is an implementation for the GSH01 powermeter application layer
*/
#ifndef APP_LAYER
#define APP_LAYER
#include "ld-ev.hpp"
#include <chrono>
#include <cstdint>
#include <everest/crc/crc.hpp>
#include <everest/logging.hpp>
#include <generated/types/powermeter.hpp>
namespace app_layer {
enum class CommandType : std::uint16_t {
///---------------Device Information Registers-------------------
APP_MODE = 0x4102, // get or set the mode of application
TRANSPARENT_MODE = 0x4103, // get the transparent mode meter
SERVER_ID = 0x4110, // get the server ID
SERIAL_NR = 0x4111, // get the serial number
HW_VERSION = 0x4112, // get the hardware version
DEVICE_TYPE = 0x4113, // get the device type
APP_FW_VERSION = 0x4114, // get the software version of application
APP_FW_CHECKSUM = 0x4115, // get the checksum (crc16) of application
MT_FW_VERSION = 0x4116, // get the software version of metering firmware
MT_FW_CHECKSUM = 0x4117, // get the crc16 from software integrity check of metering firmware
BOOTL_VERSION = 0x4118, // get the bootloader version
APP_FW_HASH = 0x4119, // get the application firmware hash(crc16)
MEASUREMENT_MODE = 0x4120, // get the measurement mode
GET_NORMAL_VOLTAGE = 0x4121, // get the normal voltage
GET_NORMAL_CURRENT = 0x4122, // get the normal current
GET_MAX_CURRENT = 0x4123, // get the maximum current
REVERSE_MODE = 0x4124, // get and set(?) the reverse mode
CLEAR_METER_STATUS = 0x4125, // set the clear meter status command
INIT_METER = 0x4126, // set the initialize meter command
LINE_LOSS_IMPEDANCE = 0x4130, // get or set (in assembling mode) the line loss impedance
LINE_LOSS_MEAS_MODE = 0x4131, // get the line loss energy measurement mode
MT_MODE = 0x4133, // get the meter operation mode
TIME = 0x4135, // get or set the device time
STATUS_WORD = 0x4137, // get the statusword
APP_CONFIG_COMPLETE = 0x4211, // set the application configuration complete command
METER_BUS_ADDR = 0x4212, // get or set the meter bus address
AS_CONFIG_COMPLETE = 0x4221, // set the assmbling configuration complete command
///---------------Device Control Registers--------------------------
GET_PUBKEY_STR16 = 0x0211, // get the public key in string format (base16)
GET_PUBKEY_STR32 = 0x0212, // get the public key in string format (base32)
GET_PUBKEY_CSTR16 = 0x0213, // get the public key compressed in string format (base16)
GET_PUBKEY_CSTR32 = 0x0214, // get the public key compressed in string format (base32)
START_TRANSACTION = 0x1501, // start transaction
STOP_TRANSACTION = 0x1502, // stop transaction
OCMF_STATS = 0x1510, // get the statistics of OCMF
GET_OCMF = 0x1520, // get the OCMF
GET_LAST_OCMF = 0x1521, // get last OCMF
GET_OCMF_REVERSE = 0x1522, // get the OCMF reverse
GET_PUBKEY_ASN1 = 0x1530, // get public key in ASN1 format
CHARGE_POINT_ID = 0x1531, // get or set the charge point identification
GET_TRANSACT_IMPORT_LINE_LOSS_ENERGY = 0x1540, // get total transaction import line loss energy
GET_TRANSACT_TOTAL_IMPORT_DEV_ENERGY = 0x1550, // get total transaction import device energy
GET_TRANSACT_TOTAL_IMPORT_MAINS_ENERGY = 0x1560, // get total transaction import mains energy
GET_TRANSACT_TOTAL_DURATION = 0x156A, // get total transaction duration.
GET_TOTAL_START_IMPORT_LINE_LOSS_ENERGY = 0x1570, // get total start import line loss energy
GET_TOTAL_START_IMPORT_DEV_ENERGY = 0x1580, // get total start import device energy
GET_TOTAL_START_IMPORT_MAINS_ENERGY = 0x1590, // get total start import mains energy
GET_TOTAL_STOP_IMPORT_LINE_LOSS_ENERGY = 0x15A0, // get total stop import line loss energy
GET_TOTAL_STOP_IMPORT_DEV_ENERGY = 0x15B0, // get total stop import device energy
GET_TOTAL_STOP_IMPORT_MAINS_ENERGY = 0x15C0, // get total stop import mains energy
GET_LOG_STATS = 0x1710, // get the statistic of log
GET_LOG_ENTRY = 0x1720, // get the log entry
GET_LAST_LOG_ENTRY = 0x1721, // get last log entry
GET_LOG_ENTRY_REVERSE = 0x1722, // get the log entry reverse
///----------------Instantaneous Registers------------------------
GET_TOTAL_IMPORT_MAINS_ENERGY = 0x0110, // get total import mains energy
GET_CURRENT_L1 = 0x0131, // get current (phase L1)
GET_VOLTAGE_L1 = 0x0132, // get voltage (phase L1)
GET_TOTAL_IMPORT_MAINS_POWER = 0x0133, // total import mains power
GET_POS_DEV_VOLTAGE = 0x0135, // get positive device voltage
GET_NEG_DEV_VOLTAGE = 0x0136, // get negative device voltage
GET_TOTAL_VOLTAGE = 0x0137, // get total device voltage
GET_IMPORT_DEV_POWER = 0x0138, // get import device power
GET_IMPORT_LINE_LOSS_POWER = 0x013A, // get import line loss power
GET_TOTAL_DEV_POWER = 0x013C, // get total device power
GET_TOTAL_IMPORT_LINE_LOSS_ENERGY = 0x0160, // get the total import line loss energy
GET_TOTAL_IMPORT_DEV_ENERGY = 0x0170, // get the total import device energy
GET_SECOND_INDEX = 0x0180 // get the second index
};
enum class CommandStatus : std::uint8_t {
OK = 0, // no error
GENERAL_ERROR = 1, // internal process has an error
OUT_OF_RANGE = 2, // data is out of range
SECURITY_ACCES_DENIED = 3, // the current mode of the slave cannot start this command
REJECTED = 4, // error of communication between application and metering board
LOGICAL_ADDRESS_NOT_FOUND = 5, // command not implemented
FORMAL_INVALID = 6, // length in bytes is incorrect; e.g. received more data than expected
NOT_AVAILABLE = 7, // data not available
IS_BUSY = 8, // slave cannot process command; already processing long operation
PUBKEY_MISSING = 9 // public key is not stored in internal memory
};
enum class CommandResult : std::uint8_t {
OK = 0,
GENERAL_ERROR = 1, // read ERROR register (read with GET_ERRORS[0xA004]) for more information
OUT_OF_RANGE = 2, //
SECURITY_ACCESS_DENIED = 3, // application board is not in production mode
REJECTED = 4, // metering board is not responding
FORMAT_INVALID = 6, // payload is not empty
NOT_AVAILABLE = 7, // data could not be read
BUSY = 8, // transaction ongoing, metering board unavailable
PUBLIC_KEY_MISSING = 9, //
PROTOCOL_ERROR = 250, // error on reception at host: protocol error (SLIP protocol)
TIMEOUT = 251, // no response during at least 1100 ms
COMMUNICATION_FAILED = 254, // error on communication between PM and host device
PENDING = 255 // special state for transaction commands
};
inline std::string command_result_to_string(CommandResult res) {
switch (res) {
case CommandResult::OK:
return "OK";
case CommandResult::GENERAL_ERROR:
return "General Error";
case CommandResult::OUT_OF_RANGE:
return "Out of Range";
case CommandResult::SECURITY_ACCESS_DENIED:
return "Security Access Denied";
case CommandResult::REJECTED:
return "Rejected";
case CommandResult::FORMAT_INVALID:
return "Format Invalid";
case CommandResult::NOT_AVAILABLE:
return "Not Available";
case CommandResult::BUSY:
return "Busy";
case CommandResult::PUBLIC_KEY_MISSING:
return "Public Key Missing";
case CommandResult::PROTOCOL_ERROR:
return "Protocol Error";
case CommandResult::TIMEOUT:
return "Timeout";
case CommandResult::COMMUNICATION_FAILED:
return "Communication Failed";
case CommandResult::PENDING:
return "Pending";
}
throw std::out_of_range("No known string conversion for provided enum of type CommandResult");
}
enum class UserIdStatus : std::uint8_t {
USER_NOT_ASSIGNED = 0x00,
USER_ASSIGNED = 0x01
};
enum class UserIdType : std::uint8_t {
NONE = 0, // not available
DENIED = 1, // not retrievable (e.g. two-factor-auth)
UNDEFINED = 2, // type unknown / other
ISO14443 = 10, // UID of RFID card according to ISO14443 (4 or 7 bytes HEX)
ISO15693 = 11, // UID of RFID card according to ISO15693 (8 bytes HEX)
EMAID = 20, // Electro-Mobility-Account-ID according to ISO/IEC15118 (14 or 15 bytes string)
EVCCID = 21, // ID of an EV according to ISO/IEC15118 (max 6 bytes)
EVCOID = 30, // EV-Contract-ID according to DIN91286
ISO7812 = 40, // Identification-Card-Format according to ISO/IEC7812 (credit-/banking-cards, etc.)
CAR_TXN_NR = 50, // Card-Transaction-Number (CardTxNbr) for credit- or banking-cards
CENTRAL = 60, // centrally generated ID (no fixed format, e.g. UUID); OCPP 2.0
CENTRAL_1 = 61, // centrally generated ID (no fixed format, e.g. start-via-SMS); (up to) OCPP 1.6
CENTRAL_2 = 62, // centrally generated ID (no fixed format, e.g. start-by-operator); (up to) OCPP 1.6
LOCAL = 70, // locally generated ID (no fixed format, e.g. UUID); OCPP 2.0
LOCAL_1 = 71, // locally generated ID (no fixed format, e.g. generated by chargepoint); (up to) OCPP 1.6
LOCAL_2 = 72, // locally generated ID (no fixed format, other); (up to) OCPP 1.6
PHONE_NUMBER = 80, // international phone number (leading '+' with country code)
KEY_CODE = 90 // private user key (no fixed format)
};
enum class ErrorCategory : std::uint8_t {
LAST = 0,
LAST_CRITICAL = 1,
FIRST = 2,
FIRST_CRITICAL = 3
};
enum class ErrorSource : std::uint8_t {
SYSTEM = 0,
COMMUNICATION = 1
};
enum class ApplicationBoardMode : std::uint8_t {
APPLICATION = 0,
ASSEMBLY = 1
};
enum class LogType : std::uint8_t {
NONE = 0,
LINE_LOSS_MEASUREMENT_MODE = 1,
IMPEDANCE_CHANGED = 2,
OPERATION_MODE_CHANGED = 3,
ASSEMBLY_CONFIG_CHANGED = 4,
FATAL_ERROR_EVENT = 5,
TIME_DELTA_TOO_BIG_EVENT = 6,
CHARGE_POINT_ID_CHANGED = 7,
EXTERNAL_DISPLAY_PAIRED = 8,
EXTERNAL_DISPLAY_FAILURE = 9,
CHARGE_DATA_OUT_OF_MEMORY = 10,
LOG_DATA_OUT_OF_MEMORY = 11,
FW_VERSION_CHANGED = 12,
PULSE_LED_SOURCE_CHANGED = 13
};
inline std::string log_type_to_string(LogType log) {
switch (log) {
case LogType::NONE:
return "None";
case LogType::LINE_LOSS_MEASUREMENT_MODE:
return "Line loss measurement mode";
case LogType::IMPEDANCE_CHANGED:
return "Impedance changed";
case LogType::OPERATION_MODE_CHANGED:
return "Operation mode changed";
case LogType::ASSEMBLY_CONFIG_CHANGED:
return "Assembly config changed";
case LogType::FATAL_ERROR_EVENT:
return "Fatal error event";
case LogType::TIME_DELTA_TOO_BIG_EVENT:
return "Time delta too big event";
case LogType::CHARGE_POINT_ID_CHANGED:
return "Charge point id changed";
case LogType::EXTERNAL_DISPLAY_PAIRED:
return "External display paired";
case LogType::EXTERNAL_DISPLAY_FAILURE:
return "External display failure";
case LogType::CHARGE_DATA_OUT_OF_MEMORY:
return "Charge data out of memory";
case LogType::LOG_DATA_OUT_OF_MEMORY:
return "Log data out of memory";
case LogType::FW_VERSION_CHANGED:
return "Fw version changed";
case LogType::PULSE_LED_SOURCE_CHANGED:
return "Pulse LED source changed";
}
throw std::out_of_range("No known string conversion for provided enum of type LogType");
}
class LogEntry {
public:
app_layer::LogType type;
std::uint32_t second_index{};
std::uint32_t utc_time{};
std::uint8_t utc_offset{};
std::vector<std::uint8_t> old_value; // max. 10 elements
std::vector<std::uint8_t> new_value; // max. 10 elements
std::vector<std::uint8_t> server_id; // 10 elements
std::vector<std::uint8_t> signature; // 64 elements
};
class StatusWord {
private:
static std::map<std::uint64_t, std::string> bit_meaning;
public:
static void print(std::uint64_t status) {
for (const auto& [key, value] : bit_meaning) {
if (status & key) {
EVLOG_info << value;
}
}
}
};
class Command {
public:
app_layer::CommandType type;
std::uint16_t length;
app_layer::CommandStatus status;
std::vector<std::uint8_t> data;
};
static constexpr std::uint16_t PM_GSH01_MAX_RX_LENGTH = 1500;
static constexpr std::uint16_t PM_GSH01_SERIAL_RX_INITIAL_TIMEOUT_MS = 1100;
static constexpr std::uint16_t PM_GSH01_SERIAL_RX_WITHIN_MESSAGE_TIMEOUT_MS = 100;
class AppLayer {
public:
void create_command_start_transaction(app_layer::UserIdStatus user_id_status, app_layer::UserIdType user_id_type,
std::string user_id_data, std::vector<std::uint8_t>& command_data);
void create_command_stop_transaction(std::vector<std::uint8_t>& command_data);
void create_command_get_time(std::vector<std::uint8_t>& command_data);
void create_command_set_time(date::utc_clock::time_point timepoint, std::int8_t gmt_offset_quarters_of_an_hour,
std::vector<std::uint8_t>& command_data);
void create_command_get_bus_address(std::vector<std::uint8_t>& command_data);
void create_command_set_bus_address(std::uint8_t bus_address, std::vector<std::uint8_t>& command_data);
void create_command_get_voltage(std::vector<std::uint8_t>& command_data);
void create_command_get_current(std::vector<std::uint8_t>& command_data);
void create_command_get_import_power(std::vector<std::uint8_t>& command_data);
void create_command_get_total_power(std::vector<std::uint8_t>& command_data);
void create_command_get_total_start_import_energy(std::vector<std::uint8_t>& command_data);
void create_command_get_total_stop_import_energy(std::vector<std::uint8_t>& command_data);
void create_command_get_total_dev_import_energy(std::vector<std::uint8_t>& command_data);
void create_command_get_total_transaction_duration(std::vector<std::uint8_t>& command_data);
void create_command_get_pubkey_str16(std::vector<std::uint8_t>& command_data);
void create_command_get_pubkey_asn1(std::vector<std::uint8_t>& command_data);
void create_command_get_ocmf_stats(std::vector<std::uint8_t>& command_data);
void create_command_get_transaction_ocmf(std::uint32_t ocmf_id, std::vector<std::uint8_t>& command_data);
void create_command_get_last_transaction_ocmf(std::vector<std::uint8_t>& command_data);
void create_command_get_charge_point_id(std::vector<std::uint8_t>& command_data);
void create_command_set_charge_point_id(app_layer::UserIdType id_type, std::string id_data,
std::vector<std::uint8_t>& command_data);
void create_command_get_log_stats(std::vector<std::uint8_t>& command_data);
void create_command_get_log_entry(std::uint32_t log_entry_id, std::vector<std::uint8_t>& command_data);
void create_command_get_last_log_entry(std::vector<std::uint8_t>& command_data);
void create_command_get_log_entry_reverse(std::uint32_t log_entry_id, std::vector<std::uint8_t>& command_data);
void create_command_get_application_mode(std::vector<std::uint8_t>& command_data);
void create_command_set_application_mode(app_layer::ApplicationBoardMode mode,
std::vector<std::uint8_t>& command_data);
void create_command_get_line_loss_impedance(std::vector<std::uint8_t>& command_data);
void create_command_set_line_loss_impedance(std::uint16_t ll_impedance, std::vector<std::uint8_t>& command_data);
void create_command_get_server_id(std::vector<std::uint8_t>& command_data);
void create_command_get_serial_number(std::vector<std::uint8_t>& command_data);
void create_command_get_hardware_version(std::vector<std::uint8_t>& command_data);
void create_command_get_device_type(std::vector<std::uint8_t>& command_data);
void create_command_get_bootloader_version(std::vector<std::uint8_t>& command_data);
void create_command_get_status_word(std::vector<std::uint8_t>& command_data);
void create_command_get_application_fw_version(std::vector<std::uint8_t>& command_data);
void create_command_get_application_fw_checksum(std::vector<std::uint8_t>& command_data);
void create_command_get_application_fw_hash(std::vector<std::uint8_t>& command_data);
void create_command_get_metering_fw_version(std::vector<std::uint8_t>& command_data);
void create_command_get_metering_fw_checksum(std::vector<std::uint8_t>& command_data);
void create_command_get_metering_mode(std::vector<std::uint8_t>& command_data);
int8_t
get_utc_offset_in_quarter_hours(const std::chrono::time_point<std::chrono::system_clock>& timepoint_system_clock);
private:
std::vector<std::uint8_t> create_command(app_layer::Command cmd);
std::vector<std::uint8_t> create_simple_command(app_layer::CommandType cmd_type);
};
} // namespace app_layer
#endif // APP_LAYER

View File

@@ -0,0 +1,240 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <diagnostics.hpp>
namespace module {
void to_json(json& j, const DeviceData& k) {
j["UTC"] = module::conversions::u32_epoch_to_rfc3339(k.utc_time_s);
j["GMT_offset_quarterhours"] = k.gmt_offset_quarterhours;
j["total_start_import_energy_Wh"] = k.total_start_import_energy_Wh;
j["total_stop_import_energy_Wh"] = k.total_stop_import_energy_Wh;
j["total_transaction_duration_s"] = k.total_transaction_duration_s;
j["OCMF_stats"] = json();
j["OCMF_stats"]["number_transactions"] = k.ocmf_stats.number_transactions;
j["OCMF_stats"]["timestamp_first_transaction"] = k.ocmf_stats.timestamp_first_transaction;
j["OCMF_stats"]["timestamp_last_transaction"] = k.ocmf_stats.timestamp_last_transaction;
j["OCMF_stats"]["max_number_of_transactions"] = k.ocmf_stats.max_number_of_transactions;
j["last_ocmf_transaction"] = k.last_ocmf_transaction;
j["requested_ocmf"] = k.requested_ocmf;
j["total_dev_import_energy_Wh"] = k.total_dev_import_energy_Wh;
j["status"] = module::conversions::to_bin_string(k.ab_status);
}
std::ostream& operator<<(std::ostream& os, const DeviceData& k) {
os << json(k).dump(4);
return os;
}
void to_json(json& j, const DeviceDiagnostics& k) {
j["charge_point_id"] = k.charge_point_id;
j["charge_point_id_type"] = k.charge_point_id_type;
j["log_stats"] = json();
j["log_stats"]["number_log_entries"] = k.log_stats.number_log_entries;
j["log_stats"]["timestamp_first_log"] = k.log_stats.timestamp_first_log;
j["log_stats"]["timestamp_last_log"] = k.log_stats.timestamp_last_log;
j["log_stats"]["max_number_of_logs"] = k.log_stats.max_number_of_logs;
j["dev_info"] = json();
j["dev_info"]["type"] = k.dev_info.type;
j["dev_info"]["hw_ver"] = k.dev_info.hw_ver;
j["dev_info"]["server_id"] = k.dev_info.server_id;
j["dev_info"]["serial_nr"] = k.dev_info.serial_number;
j["dev_info"]["application"] = json();
j["dev_info"]["application"]["mode"] = k.dev_info.application.mode;
j["dev_info"]["application"]["FW_ver"] = k.dev_info.application.fw_ver;
j["dev_info"]["application"]["FW_CRC"] = module::conversions::hexdump(k.dev_info.application.fw_crc);
j["dev_info"]["application"]["FW_hash"] = module::conversions::hexdump(k.dev_info.application.fw_hash);
j["dev_info"]["metering"] = json();
j["dev_info"]["metering"]["FW_ver"] = k.dev_info.metering.fw_ver;
j["dev_info"]["metering"]["FW_CRC"] = module::conversions::hexdump(k.dev_info.metering.fw_crc);
j["dev_info"]["metering"]["mode"] = k.dev_info.metering.mode;
j["dev_info"]["bus_address"] = module::conversions::hexdump(k.dev_info.bus_address);
j["dev_info"]["bootl_ver"] = k.dev_info.bootl_ver;
j["pubkey"] = json();
j["pubkey"]["asn1"] = json();
j["pubkey"]["str16"] = json();
j["pubkey"]["default"] = json();
j["pubkey"]["asn1"]["key"] = k.pubkey_asn1;
j["pubkey"]["str16"]["key"] = k.pubkey_str16;
j["pubkey"]["str16"]["format"] = k.pubkey_str16_format;
j["pubkey"]["default"]["key"] = k.pubkey;
j["pubkey"]["default"]["format"] = k.pubkey_format;
j["ocmf_config_table"] = json::array();
if (k.ocmf_config_table.size() > 0) {
for (std::uint8_t n = 0; n < k.ocmf_config_table.size(); n++) {
j["ocmf_config_table"][n] =
module::conversions::hexdump(static_cast<std::uint8_t>(k.ocmf_config_table.at(n)));
}
}
}
void from_json(const json& j, DeviceDiagnostics& k) {
EVLOG_error << "[DeviceDiagnostics][from_json()] not implemented";
}
std::ostream& operator<<(std::ostream& os, const DeviceDiagnostics& k) {
os << json(k).dump(4);
return os;
}
void to_json(json& j, const Logging& k) {
j["log"] = json();
j["log"]["last"] = json();
j["log"]["last"]["type"] = "" + std::to_string((int)k.last_log.type) + ": " + log_type_to_string(k.last_log.type);
j["log"]["last"]["second_index"] = k.last_log.second_index;
j["log"]["last"]["utc_time"] = module::conversions::u32_epoch_to_rfc3339(k.last_log.utc_time);
j["log"]["last"]["utc_offset_quarterhours"] = k.last_log.utc_offset;
j["log"]["last"]["old_value"] = module::conversions::hexdump(k.last_log.old_value);
j["log"]["last"]["new_value"] = module::conversions::hexdump(k.last_log.new_value);
j["log"]["last"]["server_id"] = module::conversions::hexdump(k.last_log.server_id);
j["log"]["last"]["signature"] = module::conversions::hexdump(k.last_log.signature);
j["errors"] = json();
j["errors"]["system"] = json();
j["errors"]["system"]["last"] = json::array();
for (std::uint8_t n = 0; n < 5; n++) {
j["errors"]["system"]["last"][n]["id"] =
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::SYSTEM)]
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST)]
.error[n]
.id;
j["errors"]["system"]["last"][n]["priority"] =
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::SYSTEM)]
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST)]
.error[n]
.priority;
j["errors"]["system"]["last"][n]["counter"] =
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::SYSTEM)]
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST)]
.error[n]
.counter;
}
j["errors"]["system"]["last_critical"] = json::array();
for (std::uint8_t n = 0; n < 5; n++) {
j["errors"]["system"]["last_critical"][n]["id"] =
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::SYSTEM)]
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST_CRITICAL)]
.error[n]
.id;
j["errors"]["system"]["last_critical"][n]["priority"] =
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::SYSTEM)]
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST_CRITICAL)]
.error[n]
.priority;
j["errors"]["system"]["last_critical"][n]["counter"] =
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::SYSTEM)]
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST_CRITICAL)]
.error[n]
.counter;
}
j["errors"]["communication"] = json();
j["errors"]["communication"]["last"] = json::array();
for (std::uint8_t n = 0; n < 5; n++) {
j["errors"]["communication"]["last"][n]["id"] =
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::COMMUNICATION)]
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST)]
.error[n]
.id;
j["errors"]["communication"]["last"][n]["priority"] =
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::COMMUNICATION)]
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST)]
.error[n]
.priority;
j["errors"]["communication"]["last"][n]["counter"] =
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::COMMUNICATION)]
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST)]
.error[n]
.counter;
}
j["errors"]["communication"]["last_critical"] = json::array();
for (std::uint8_t n = 0; n < 5; n++) {
j["errors"]["communication"]["last_critical"][n]["id"] =
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::COMMUNICATION)]
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST_CRITICAL)]
.error[n]
.id;
j["errors"]["communication"]["last_critical"][n]["priority"] =
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::COMMUNICATION)]
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST_CRITICAL)]
.error[n]
.priority;
j["errors"]["communication"]["last_critical"][n]["counter"] =
k.source[static_cast<std::uint8_t>(app_layer::ErrorSource::COMMUNICATION)]
.category[static_cast<std::uint8_t>(app_layer::ErrorCategory::LAST_CRITICAL)]
.error[n]
.counter;
}
}
void from_json(const json& j, Logging& k) {
// n/a
EVLOG_error << "[Logging][from_json()] not implemented";
}
std::ostream& operator<<(std::ostream& os, const Logging& k) {
os << json(k).dump(4);
return os;
}
namespace conversions {
std::string hexdump(const std::vector<std::uint8_t>& msg) {
std::stringstream ss;
for (auto index : msg) {
ss << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << static_cast<int>(index) << " ";
}
return ss.str();
}
std::string hexdump(const std::vector<std::uint8_t>& msg, std::uint8_t start, std::uint8_t number_of_chars) {
if ((start + number_of_chars) > msg.size())
return std::string{};
std::stringstream ss;
for (std::uint8_t n = start; n < (start + number_of_chars); n++) {
ss << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << static_cast<int>(msg.at(n));
if (n < (start + number_of_chars - 1))
ss << " ";
}
return ss.str();
}
std::string hexdump(std::uint8_t msg) {
std::stringstream ss;
ss << "0x" << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << static_cast<int>(msg);
return ss.str();
}
std::string hexdump(std::uint16_t msg) {
std::stringstream ss;
ss << "0x" << std::hex << std::uppercase << std::setw(4) << std::setfill('0') << static_cast<std::uint16_t>(msg);
return ss.str();
}
std::string hexdump(std::uint64_t msg) {
std::stringstream ss;
ss << "0x" << std::hex << std::uppercase << std::setw(16) << std::setfill('0') << static_cast<std::uint64_t>(msg);
return ss.str();
}
std::string get_string(const std::vector<std::uint8_t>& vec) {
std::string str{};
for (std::uint16_t n = 0; n < vec.size(); n++) {
if ((vec[n] < ' ') || (vec[n] > '~')) {
str += " ";
} else {
str += vec[n];
}
}
return str;
}
std::string u32_epoch_to_rfc3339(std::uint32_t epoch_time) {
time_t tt = static_cast<time_t>(epoch_time);
std::tm tm = *std::gmtime(&tt);
std::stringstream ss;
ss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S.000Z");
return ss.str();
}
} // namespace conversions
} // namespace module

View File

@@ -0,0 +1,148 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef DIAGNOSTICS_HPP
#define DIAGNOSTICS_HPP
#include "app_layer.hpp"
#include <bitset>
#include <date/date.h>
#include <date/tz.h>
#include <everest/logging.hpp>
#include <nlohmann/json.hpp>
namespace module {
using json = nlohmann::json;
struct OcmfStats {
std::uint32_t number_transactions{};
std::uint32_t timestamp_first_transaction{};
std::uint32_t timestamp_last_transaction{};
std::uint32_t max_number_of_transactions{};
};
struct OcmfInfo {
std::string gateway_id{};
std::string manufacturer{};
std::string model{};
};
struct DeviceData {
std::uint32_t utc_time_s{};
std::uint8_t gmt_offset_quarterhours{};
std::uint64_t total_start_import_energy_Wh{}; // meter value needs to be divided by 10 for Wh
std::uint64_t total_stop_import_energy_Wh{}; // meter value needs to be divided by 10 for Wh
std::uint32_t total_transaction_duration_s{}; // must be less than 27 days in total
OcmfStats ocmf_stats;
std::string last_ocmf_transaction;
std::string requested_ocmf;
std::uint64_t total_dev_import_energy_Wh{}; // meter value needs to be divided by 10 for Wh
std::uint64_t ab_status{};
};
void to_json(json& j, const DeviceData& k);
void from_json(const json& j, DeviceData& k);
std::ostream& operator<<(std::ostream& os, const DeviceData& k);
struct LogStats {
std::uint32_t number_log_entries{};
std::uint32_t timestamp_first_log{};
std::uint32_t timestamp_last_log{};
std::uint32_t max_number_of_logs{};
};
struct ApplicationInfo {
std::string mode;
std::string fw_ver;
std::uint16_t fw_crc{};
std::uint16_t fw_hash{};
};
struct MeteringInfo {
std::string fw_ver;
std::uint16_t fw_crc{};
std::uint8_t mode{};
};
struct DeviceInfo {
std::string type;
std::string hw_ver;
std::string server_id;
std::uint32_t serial_number{};
std::uint8_t bus_address{};
std::string bootl_ver;
ApplicationInfo application;
MeteringInfo metering;
};
struct DeviceDiagnostics {
std::string charge_point_id;
std::uint8_t charge_point_id_type{0};
DeviceInfo dev_info;
LogStats log_stats;
std::string pubkey_asn1;
std::string pubkey_str16;
std::string pubkey;
std::uint8_t pubkey_str16_format{}; // 0x04 for uncompressed string
std::uint8_t pubkey_format{}; // 0x04 for uncompressed string
std::vector<std::uint8_t> ocmf_config_table;
};
void to_json(json& j, const DeviceDiagnostics& k);
void from_json(const json& j, DeviceDiagnostics& k);
std::ostream& operator<<(std::ostream& os, const DeviceDiagnostics& k);
// TODO(LAD): add error data
struct ErrorData {
std::uint32_t id{0};
std::uint16_t priority{0};
std::uint32_t counter{0};
};
struct FiveErrors {
ErrorData error[5];
};
struct ErrorSet {
FiveErrors category[4];
};
struct Logging {
app_layer::LogEntry last_log;
ErrorSet source[2];
};
void to_json(json& j, const Logging& k);
void from_json(const json& j, Logging& k);
std::ostream& operator<<(std::ostream& os, const Logging& k);
namespace conversions {
template <typename T> static std::string to_bin_string(const T& num) {
std::stringstream ss{};
for (std::uint8_t n = 0; n < sizeof(T); n++) {
ss << std::bitset<8>(num >> ((sizeof(T) - n - 1) * 8));
if (n % 2) {
if (n != sizeof(T) - 1) {
ss << " - ";
}
} else {
ss << " ";
}
}
return ss.str();
}
std::string hexdump(const std::vector<std::uint8_t>& msg);
std::string hexdump(const std::vector<std::uint8_t>& msg, std::uint8_t start, std::uint8_t number_of_chars);
std::string hexdump(std::uint8_t msg);
std::string hexdump(std::uint16_t msg);
std::string hexdump(std::uint64_t msg);
std::string get_string(const std::vector<std::uint8_t>& vec);
std::string u32_epoch_to_rfc3339(std::uint32_t epoch_time);
} // namespace conversions
} // namespace module
#endif // DIAGNOSTICS_HPP

View File

@@ -0,0 +1,10 @@
.. _everest_modules_handwritten_DZG_GSH01:
.. *********
.. DZG_GSH01
.. *********
This is a prototype driver for the DZG GSH01 powermeter. It has been tested and confirmed to work with EVerest.
It does not fully implement all functionality of the operation manual.
Use with caution and report any issues or limitations encountered.

View File

@@ -0,0 +1,142 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef MAIN_POWERMETER_IMPL_HPP
#define MAIN_POWERMETER_IMPL_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 3
//
#include <generated/interfaces/powermeter/Implementation.hpp>
#include "../DZG_GSH01.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
// insert your custom include headers here
#include "app_layer.hpp"
#include "diagnostics.hpp"
#include "serial_device.hpp"
#include "slip_protocol.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
namespace module {
namespace main {
struct Conf {
int powermeter_device_id;
std::string serial_port;
int baudrate;
int parity;
int rs485_direction_gpio;
int num_of_retries;
bool ignore_echo;
int max_clock_diff_s;
bool publish_device_data;
bool publish_device_diagnostics;
};
class powermeterImpl : public powermeterImplBase {
public:
powermeterImpl() = delete;
powermeterImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<DZG_GSH01>& mod, Conf& config) :
powermeterImplBase(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::powermeter::TransactionStartResponse
handle_start_transaction(types::powermeter::TransactionReq& value) override;
virtual types::powermeter::TransactionStopResponse handle_stop_transaction(std::string& transaction_id) override;
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
// insert your protected definitions here
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
private:
const Everest::PtrContainer<DZG_GSH01>& mod;
const Conf& config;
virtual void init() override;
virtual void ready() override;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
// insert your private definitions here
enum class MessageStatus : std::uint8_t {
NONE = 0,
SENT = 1,
RECEIVED = 2
};
std::atomic_bool communication_timeout{false};
void cleanup_dangling_transaction(void);
types::powermeter::TransactionStopResponse do_stop_transaction(const std::string& transaction_id);
MessageStatus start_transaction_msg_status{MessageStatus::NONE};
app_layer::CommandResult start_transact_result{};
MessageStatus stop_transaction_msg_status{MessageStatus::NONE};
app_layer::CommandResult stop_transact_result{};
MessageStatus get_transaction_values_msg_status{MessageStatus::NONE};
bool charging_in_progress{false};
bool no_charging_done{true};
bool need_to_stop_transaction{false};
serial_device::SerialDevice serial_device{};
slip_protocol::SlipProtocol slip{};
app_layer::AppLayer app_layer{};
types::powermeter::Powermeter pm_last_values;
DeviceData device_data_obj{};
DeviceDiagnostics device_diagnostics_obj{};
Logging logging_obj{};
app_layer::ErrorCategory category_requested{};
app_layer::ErrorSource source_requested{};
uint8_t error_diagnostics_target{0};
std::string last_ocmf_str{};
void init_default_values();
void read_powermeter_values();
void time_sync();
void get_device_time();
void set_device_time();
void get_meter_bus_address();
void set_meter_bus_address(uint8_t old_bus_address, uint8_t new_bus_address);
void get_status_word();
// void set_device_charge_point_id(app_layer::UserIdType id_type, std::string charge_point_id);
void read_device_data();
void read_diagnostics_data();
void publish_device_data_topic();
void publish_device_diagnostics_topic();
void publish_logging_topic();
void get_device_public_key();
void readRegisters();
app_layer::CommandResult process_response(const std::vector<uint8_t>& register_message);
void request_device_type();
void get_app_fw_version();
void get_application_operation_mode();
void set_application_operation_mode(app_layer::ApplicationBoardMode mode);
void get_line_loss_impedance();
void set_line_loss_impedance(uint16_t ll_impedance);
void request_error_diagnostics(uint8_t addr);
void error_diagnostics(uint8_t addr);
void send_receive(std::vector<uint8_t>& request);
app_layer::CommandResult handle_response(std::vector<uint8_t>& response);
std::string get_meter_ocmf();
static constexpr auto TIMEOUT_2s{std::chrono::seconds(2)};
// 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_POWERMETER_IMPL_HPP

View File

@@ -0,0 +1,67 @@
description: Module that collects power and energy measurements from an GSH01 powermeter
provides:
main:
description: Implementation of the driver functionality
interface: powermeter
config:
powermeter_device_id:
description: The powermeter's address on the serial bus, 255 is broadcast
type: integer
minimum: 0
maximum: 255
default: 255
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: 115200
parity:
description: 'Parity bit: 0: None, 1: Odd, 2: Even'
type: integer
minimum: 0
maximum: 2
default: 0
rs485_direction_gpio:
description: GPIO to use for direction switching. Set to -1 to disable.
type: integer
default: -1
num_of_retries:
description: Number of retries sent via serial interface. Set to 0 to disable retry.
type: integer
minimum: 0
maximum: 3
default: 0
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
max_clock_diff_s:
description: Maximal time difference (in seconds) between meter clock and system clock, until new set_time command is sent
type: integer
minimum: 30
maximum: 300
default: 60
publish_device_data:
description: Regularly publish device data not covered by the "powermeter" interface (for debugging purposes).
type: boolean
default: false
publish_device_diagnostics:
description: Regularly publish device diagnostics (e.g. log messages, errors, SW/HW revisions; for debugging purposes).
type: boolean
default: false
enable_external_mqtt: true
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Lars Dieckmann
- Andreas Heinrich
- Cornelius Claussen
- Florin Mihut
- Jan Christoph Habig
- Kai-Uwe Hermann
- Miriam Thome

View File

@@ -0,0 +1,208 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "serial_device.hpp"
#include "everest/logging.hpp"
#include <cstring>
#include <endian.h>
#include <errno.h>
#include <fcntl.h>
#include <string>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <fmt/core.h>
namespace {
std::string hexdump(const std::uint8_t* const msg, int msg_len) {
std::stringstream ss;
for (int i = 0; i < msg_len; i++) {
ss << std::hex << static_cast<int>(msg[i]) << " ";
}
return ss.str();
}
std::string hexdump(const std::vector<std::uint8_t>& msg) {
std::stringstream ss;
for (auto index : msg) {
ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(index) << " ";
}
return ss.str();
}
} // namespace
namespace serial_device {
SerialDevice::~SerialDevice() {
if (fd != 0) {
close(fd);
}
}
bool SerialDevice::open_device(const std::string& device, int _baud, bool _ignore_echo, std::uint8_t _num_of_retries) {
ignore_echo = _ignore_echo;
retry_struct.num_of_retries = _num_of_retries;
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) {
EVLOG_error << "Serial: errorfrom tcgetattr: " << 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
tty.c_cflag &= ~(PARENB | PARODD); // shut off parity
tty.c_cflag &= ~CSTOPB; // 1 Stop bit
tty.c_cflag &= ~CRTSCTS;
if (tcsetattr(fd, TCSANOW, &tty) != 0) {
EVLOG_error << "Serial: errorfrom tcsetattr: " << errno;
return false;
}
return true;
}
/*
This function receives a byte array.
*/
int SerialDevice::rx(std::vector<std::uint8_t>& rxbuf, std::optional<int> initial_timeout_ms,
std::optional<int> in_msg_timeout_ms) {
std::scoped_lock lock(serial_mutex);
int _initial_timeout = SERIAL_RX_INITIAL_TIMEOUT_MS;
if (initial_timeout_ms.has_value()) {
_initial_timeout = initial_timeout_ms.value();
}
int _in_msg_timeout = SERIAL_RX_WITHIN_MESSAGE_TIMEOUT_MS;
if (in_msg_timeout_ms.has_value()) {
_in_msg_timeout = in_msg_timeout_ms.value();
}
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = _initial_timeout * 1000; // intial timeout until device responds
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 = _in_msg_timeout * 1000; // reduce timeout after first chunk,
// no uneccesary waiting at the end of the message
if (rv == -1) { // error in select function call
perror("rx: select:");
break;
} else if (rv == 0) { // no more bytes to read within timeout, so transfer is complete
EVLOG_debug << "No more bytes to read within timeout. (rv == 0)";
break;
} else { // received more bytes, add them to buffer
// do we have space in the rx buffer left?
if (bytes_read_total >= rxbuf.capacity()) {
// no buffer space left, but more to read.
EVLOG_info
<< R"(No buffer space left, but more to read. (Did you mean to set "ignore_echo" to "false"?))";
break;
}
rxbuf.resize(rxbuf.capacity());
int bytes_read =
read(fd, (std::uint8_t*)(&rxbuf[0] + bytes_read_total), (size_t)(rxbuf.capacity() - bytes_read_total));
if (bytes_read > 0) {
bytes_read_total += bytes_read;
rxbuf.resize(bytes_read_total);
} else if (bytes_read < 0) {
EVLOG_error << "Error reading from device: " << strerror(errno);
}
}
}
return bytes_read_total;
}
/*
This function transmits a byte vector.
*/
void SerialDevice::tx(const std::vector<std::uint8_t>& request) {
{
std::scoped_lock lock(serial_mutex);
// clear input and output buffer
tcflush(fd, TCIOFLUSH);
// write to serial port
write(fd, request.data(), request.size());
tcdrain(fd);
}
if (ignore_echo) {
// read back echo of what we sent and ignore it
std::vector<std::uint8_t> req_buf{};
req_buf.reserve(request.size() + 1);
rx(req_buf, std::nullopt, std::nullopt);
}
}
int SerialDevice::tx_rx_blocking(const std::vector<std::uint8_t>& request, std::vector<std::uint8_t>& rxbuf,
std::optional<int> initial_timeout_ms, std::optional<int> in_msg_timeout_ms) {
std::scoped_lock lock(txrx_mutex);
int bytes_rx = 0;
tx(request);
bytes_rx = rx(rxbuf, initial_timeout_ms, in_msg_timeout_ms);
while (bytes_rx == 0 && retry_struct.num_of_retries_done < retry_struct.num_of_retries) {
tx(request);
bytes_rx = rx(rxbuf, initial_timeout_ms, in_msg_timeout_ms);
retry_struct.num_of_retries_done++;
}
retry_struct.num_of_retries_done = 0;
return bytes_rx;
}
} // namespace serial_device

View File

@@ -0,0 +1,55 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
/*
This is an implementation for custom serial communications
*/
#ifndef SERIAL_DEVICE
#define SERIAL_DEVICE
#include <everest/logging.hpp>
#include <mutex>
#include <optional>
#include <stdint.h>
#include <termios.h>
namespace serial_device {
constexpr int SERIAL_RX_INITIAL_TIMEOUT_MS = 500;
constexpr int SERIAL_RX_WITHIN_MESSAGE_TIMEOUT_MS = 100;
struct Retry {
std::uint8_t num_of_retries{0};
std::uint8_t num_of_retries_done{0};
};
class SerialDevice {
public:
SerialDevice() = default;
SerialDevice(const SerialDevice&) = delete;
SerialDevice(SerialDevice&&) = delete;
SerialDevice& operator=(const SerialDevice&) = delete;
SerialDevice& operator=(SerialDevice&&) = delete;
~SerialDevice();
bool open_device(const std::string& device, int baud, bool ignore_echo, std::uint8_t _num_of_retries);
int tx_rx_blocking(const std::vector<std::uint8_t>& request, std::vector<std::uint8_t>& rxbuf,
std::optional<int> initial_timeout_ms, std::optional<int> in_msg_timeout_ms);
private:
// Serial interface
int fd{0};
bool ignore_echo{false};
void tx(const std::vector<std::uint8_t>& request);
int rx(std::vector<std::uint8_t>& rxbuf, std::optional<int> initial_timeout_ms,
std::optional<int> in_msg_timeout_ms);
std::mutex serial_mutex;
std::mutex txrx_mutex;
Retry retry_struct;
};
} // namespace serial_device
#endif // SERIAL_DEVICE

View File

@@ -0,0 +1,218 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "slip_protocol.hpp"
#include <cstring>
#include <endian.h>
#include <everest/crc/crc.hpp>
#include <everest/logging.hpp>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <fmt/core.h>
namespace {
inline void remove_start_and_stop_frame(std::vector<std::uint8_t>& vec) {
vec.erase(vec.begin());
vec.pop_back();
}
// TODO: this is a duplicate of the implementation in AST_DC650, refactor into common library
inline bool is_message_crc_correct(std::vector<std::uint8_t>& vec) {
if (vec.size() < 2) {
return false;
}
const auto crc_check = static_cast<std::uint16_t>((vec.at(vec.size() - 1) << 8) | vec.at(vec.size() - 2));
// remove CRC tail from vector
for (std::uint8_t i = 0; i < 2; i++) {
vec.pop_back();
}
auto crc_calc = calculate_xModem_crc16(vec);
return crc_check == crc_calc;
}
inline void restore_special_characters(std::vector<std::uint8_t>& vec) {
for (std::uint16_t j = 0; j < (vec.size() - 1);
j++) { // can only go to vec.size() - 1 because two bytes will be checked
if ((vec.at(j) == 0xDB) && (vec.at(j + 1) == 0xDC)) {
vec.at(j) = 0xC0;
vec.erase(vec.begin() + j + 1);
} else if ((vec.at(j) == 0xDB) && (vec.at(j + 1) == 0xDD)) {
vec.at(j) = 0xDB;
vec.erase(vec.begin() + j + 1);
}
}
}
} // namespace
namespace slip_protocol {
std::vector<std::uint8_t> SlipProtocol::package_single(const std::uint8_t address, std::vector<std::uint8_t>& payload) {
std::vector<std::uint8_t> vec{};
// address to the front
payload.insert(payload.begin(), address);
// CRC16 to the end
std::uint16_t crc = calculate_xModem_crc16(payload);
payload.push_back(static_cast<std::uint8_t>(crc & 0x00FF)); // LSB CRC16
payload.push_back(static_cast<std::uint8_t>((crc >> 8) & 0x00FF)); // MSB CRC16
// replacement of special characters (including address and CRC)
for (auto payload_byte : payload) {
if (payload_byte == 0xC0) { // check for and replace special char 0xC0
vec.push_back(0xDB);
vec.push_back(0xDC);
} else if (payload_byte == 0xDB) { // check for and replace special char 0xDB
vec.push_back(0xDB);
vec.push_back(0xDD);
} else { // otherwise just use normal input
vec.push_back(payload_byte);
}
}
// add start frame to front (can be done only after special characters have been replaced)
vec.insert(vec.begin(), SLIP_START_END_FRAME);
// end frame
vec.push_back(SLIP_START_END_FRAME);
return vec;
}
std::vector<std::uint8_t> SlipProtocol::package_multi(const std::uint8_t address,
const std::vector<std::vector<std::uint8_t>>& multi_payload) {
std::vector<std::uint8_t> payload{};
// concatenate requests
for (auto multi_payload_part : multi_payload) {
payload.insert(payload.end(), multi_payload_part.begin(), multi_payload_part.end());
}
// ...and return as one long request
return package_single(address, payload);
}
SlipReturnStatus SlipProtocol::unpack(std::vector<std::uint8_t>& message, std::uint8_t listen_to_address) {
SlipReturnStatus retval = SlipReturnStatus::SLIP_ERROR_UNINITIALIZED;
std::uint8_t message_start_end_frame_counter = 0;
if (message.size() < 1) {
return SlipReturnStatus::SLIP_ERROR_SIZE_ERROR;
}
// check if first element is SLIP_START_END_FRAME and if not, drop first item(s) until start frame is first
std::uint8_t i = 0;
while (i < message.size()) {
if (message.at(i) == SLIP_START_END_FRAME)
break;
i++;
}
if (i > 0) {
std::uint8_t j = 0;
while (message.size() > 0) {
if (j == i)
break;
message.erase(message.begin());
j++;
}
}
// count number of SLIP_START_END_FRAMEs to check for multiple (or broken) messages
for (std::uint16_t n = 0; n < message.size(); n++) {
if (message.at(n) == SLIP_START_END_FRAME) {
message_start_end_frame_counter++;
}
}
if (message_start_end_frame_counter != 2) { // unexpected/broken message -OR- multiple messages received
// split message vector into sub-vectors by delimiter (SLIP_START_END_FRAME)
std::vector<std::vector<std::uint8_t>> sub_messages{};
std::vector<std::uint8_t> current_sub_message{};
for (std::uint16_t n = 0; n < message.size(); n++) {
if (message.at(n) == SLIP_START_END_FRAME) {
if (current_sub_message.size() > 0) {
if (listen_to_address == 0xFF) {
// listen_to_address is broadcast address: listen to all messages
sub_messages.push_back(current_sub_message);
} else {
// dedicated client address selected, only listen to this address
if (current_sub_message.at(0) == listen_to_address) {
sub_messages.push_back(current_sub_message);
}
}
current_sub_message.clear();
} else {
// intentionally do nothing on empty sub_message
}
} else {
current_sub_message.push_back(message.at(n));
}
}
// from here on, we have all message parts as elements in sub_messages
for (auto sub_message : sub_messages) {
restore_special_characters(sub_message);
// check all sub-messages' CRC and only process on match
if (is_message_crc_correct(sub_message)) {
// on correct CRC
message_queue.push_back(sub_message);
message_counter++;
if (retval == SlipReturnStatus::SLIP_ERROR_UNINITIALIZED) { // only set SLIP_OK if no other error
retval = SlipReturnStatus::SLIP_OK;
}
} else {
retval = SlipReturnStatus::SLIP_ERROR_MALFORMED;
EVLOG_error << "Malformed message received!";
}
}
} else { // single message received
if (message.size() > 3) {
// only process messages for correct client or if listen_to_address is broadcast address
if ((message.at(1) == listen_to_address) || (0xFF == listen_to_address)) {
// check if last element is SLIP_START_END_FRAME and if not, reduce message size
while (message.at(message.size() - 1) != SLIP_START_END_FRAME) {
if (message.size() <= 3)
break;
message.pop_back();
}
remove_start_and_stop_frame(message);
// check for special characters and restore to original contents
restore_special_characters(message);
if (is_message_crc_correct(message)) {
message_queue.push_back(message);
message_counter++;
retval = SlipReturnStatus::SLIP_OK;
} else {
retval = SlipReturnStatus::SLIP_ERROR_CRC_MISMATCH;
EVLOG_error << "CRC mismatch!";
}
} else {
retval = SlipReturnStatus::SLIP_OK;
}
} else {
retval = SlipReturnStatus::SLIP_ERROR_SIZE_ERROR;
EVLOG_error << "Message broken: too short!";
}
}
return retval;
}
std::vector<std::uint8_t> SlipProtocol::retrieve_single_message() {
if (message_queue.size() > 0) {
message_counter--;
auto ret_vec = message_queue.back();
message_queue.pop_back();
return ret_vec;
}
return {};
}
} // namespace slip_protocol

View File

@@ -0,0 +1,51 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
/*
This is an implementation for the SLIP serial protocol
*/
#ifndef SLIP_PROTOCOL
#define SLIP_PROTOCOL
#include <cstdint>
#include <everest/crc/crc.hpp>
#include <everest/logging.hpp>
namespace slip_protocol {
constexpr int SLIP_START_END_FRAME = 0xC0;
constexpr int SLIP_BROADCAST_ADDR = 0xFF;
constexpr std::uint16_t SLIP_SIZE_ON_ERROR = 1;
constexpr std::int8_t SLIP_ERROR_SIZE_ERROR = -1;
constexpr std::int8_t SLIP_ERROR_MALFORMED = -2;
constexpr std::int8_t SLIP_ERROR_CRC_MISMATCH = -3;
enum class SlipReturnStatus : std::int8_t {
SLIP_ERROR_CRC_MISMATCH = -3,
SLIP_ERROR_MALFORMED = -2,
SLIP_ERROR_SIZE_ERROR = -1,
SLIP_OK = 0,
SLIP_ERROR_UNINITIALIZED = 1
};
class SlipProtocol {
public:
std::vector<std::uint8_t> package_single(std::uint8_t address, std::vector<std::uint8_t>& payload);
std::vector<std::uint8_t> package_multi(std::uint8_t address,
const std::vector<std::vector<std::uint8_t>>& multi_payload);
SlipReturnStatus unpack(std::vector<std::uint8_t>& message, std::uint8_t listen_to_address);
auto get_message_counter() const {
return message_counter;
}
std::vector<std::uint8_t> retrieve_single_message();
private:
std::vector<std::vector<std::uint8_t>> message_queue;
std::uint8_t message_counter{0};
};
} // namespace slip_protocol
#endif // SLIP_PROTOCOL