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,16 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "AST_DC650.hpp"
namespace module {
void AST_DC650::init() {
invoke_init(*p_main);
}
void AST_DC650::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 AST_DC650_HPP
#define AST_DC650_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 AST_DC650 : public Everest::ModuleBase {
public:
AST_DC650() = delete;
AST_DC650(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 // AST_DC650_HPP

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
ast_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::framework
everest::crc
)
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,28 @@
// 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() {
return (steady_clock::now() - start) > t;
}
private:
milliseconds t;
time_point<steady_clock> start;
};
#endif

View File

@@ -0,0 +1,375 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "ast_app_layer.hpp"
#include <cstring>
#include <endian.h>
#include <errno.h>
#include <everest/logging.hpp>
#include <fcntl.h>
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <fmt/core.h>
namespace ast_app_layer {
uint32_t timepoint_to_uint32(date::utc_clock::time_point timepoint) {
return static_cast<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 = static_cast<std::uint8_t>((u16 >> 8) & 0x00FF);
std::uint8_t lower = static_cast<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(static_cast<std::uint8_t>(u32 & 0x000000FF));
vec.push_back(static_cast<std::uint8_t>((u32 >> 8) & 0x000000FF));
vec.push_back(static_cast<std::uint8_t>((u32 >> 16) & 0x000000FF));
vec.push_back(static_cast<std::uint8_t>((u32 >> 24) & 0x000000FF));
}
std::vector<std::uint8_t> AstAppLayer::create_command(ast_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((uint8_t)ast_app_layer::CommandStatus::OK);
for (uint16_t i = 0; i < cmd.data.size(); i++) {
command_data.push_back(cmd.data[i]);
}
return command_data;
}
std::vector<std::uint8_t> AstAppLayer::create_simple_command(ast_app_layer::CommandType cmd_type) {
ast_app_layer::Command cmd{};
cmd.type = cmd_type;
cmd.length = 0x0005;
cmd.status = ast_app_layer::CommandStatus::OK;
return create_command(cmd);
}
int8_t AstAppLayer::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 AstAppLayer::create_command_start_transaction(ast_app_layer::UserIdStatus user_id_status,
ast_app_layer::UserIdType user_id_type,
const std::string& user_id_data,
std::int8_t gmt_offset_quarter_hours,
std::vector<std::uint8_t>& command_data) {
ast_app_layer::Command cmd{};
cmd.type = ast_app_layer::CommandType::START_TRANSACTION;
cmd.length = 0x0034;
cmd.status = ast_app_layer::CommandStatus::OK;
insert_u32_as_u8s(cmd.data, (timepoint_to_uint32(date::utc_clock::now())));
cmd.data.push_back(
static_cast<std::uint8_t>(gmt_offset_quarter_hours)); // GMT offset in quarters of an hour, e.g. 0x08 = +2h
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 AstAppLayer::create_command_stop_transaction(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::STOP_TRANSACTION);
}
void AstAppLayer::create_command_get_time(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::TIME);
}
void AstAppLayer::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) {
ast_app_layer::Command cmd{};
cmd.type = ast_app_layer::CommandType::TIME;
cmd.length = 0x000A;
cmd.status = ast_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 AstAppLayer::create_command_get_voltage(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::GET_VOLTAGE_L1);
}
void AstAppLayer::create_command_get_current(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::GET_CURRENT_L1);
}
void AstAppLayer::create_command_get_import_power(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::GET_IMPORT_DEV_POWER);
}
void AstAppLayer::create_command_get_export_power(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::GET_EXPORT_DEV_POWER);
}
void AstAppLayer::create_command_get_total_dev_import_energy(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::GET_TOTAL_IMPORT_DEV_ENERGY);
}
void AstAppLayer::create_command_get_total_dev_export_energy(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::GET_TOTAL_EXPORT_DEV_ENERGY);
}
void AstAppLayer::create_command_get_total_power(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::GET_TOTAL_DEV_POWER);
}
void AstAppLayer::create_command_get_total_start_import_energy(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::GET_TOTAL_START_IMPORT_DEV_ENERGY);
}
void AstAppLayer::create_command_get_total_stop_import_energy(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::GET_TOTAL_STOP_IMPORT_DEV_ENERGY);
}
void AstAppLayer::create_command_get_total_start_export_energy(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::GET_TOTAL_START_EXPORT_DEV_ENERGY);
}
void AstAppLayer::create_command_get_total_stop_export_energy(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::GET_TOTAL_STOP_EXPORT_DEV_ENERGY);
}
void AstAppLayer::create_command_get_total_transaction_duration(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::GET_TRANSACT_TOTAL_DURATION);
}
void AstAppLayer::create_command_get_pubkey_str16(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::GET_PUBKEY_STR16);
}
void AstAppLayer::create_command_get_pubkey_asn1(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::GET_PUBKEY_ASN1);
}
void AstAppLayer::create_command_get_meter_pubkey(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::REQUEST_METER_PUBKEY);
}
void AstAppLayer::create_command_get_ocmf_stats(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::OCMF_STATS);
}
/* OCMF ID: 1..235000
OCMF data from specified transaction will be at minimum import energy of transaction
*/
void AstAppLayer::create_command_get_transaction_ocmf(uint32_t ocmf_id, std::vector<std::uint8_t>& command_data) {
ast_app_layer::Command cmd{};
cmd.type = ast_app_layer::CommandType::GET_OCMF;
cmd.length = 0x0009;
cmd.status = ast_app_layer::CommandStatus::OK;
insert_u32_as_u8s(cmd.data, ocmf_id);
command_data = create_command(cmd);
}
void AstAppLayer::create_command_get_last_transaction_ocmf(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::GET_LAST_OCMF);
}
void AstAppLayer::create_command_get_ocmf_info(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::OCMF_INFO);
}
void AstAppLayer::create_command_get_ocmf_config(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::OCMF_CONFIG);
}
void AstAppLayer::create_command_get_charge_point_id(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::CHARGE_POINT_ID);
}
/* only works in "assembly mode" */
void AstAppLayer::create_command_set_charge_point_id(ast_app_layer::UserIdType id_type, std::string id_data,
std::vector<std::uint8_t>& command_data) {
ast_app_layer::Command cmd{};
cmd.type = ast_app_layer::CommandType::CHARGE_POINT_ID;
cmd.length = 0x0013;
cmd.status = ast_app_layer::CommandStatus::OK;
cmd.data.push_back((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 AstAppLayer::create_command_get_errors(ast_app_layer::ErrorCategory category, ast_app_layer::ErrorSource src,
std::vector<std::uint8_t>& command_data) {
ast_app_layer::Command cmd{};
cmd.type = ast_app_layer::CommandType::GET_ERRORS;
cmd.length = 0x0007;
cmd.status = ast_app_layer::CommandStatus::OK;
cmd.data.push_back((uint8_t)category);
cmd.data.push_back((uint8_t)src);
command_data = create_command(cmd);
}
void AstAppLayer::create_command_get_log_stats(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::GET_LOG_STATS);
}
/* log entry ids: 1..2500 */
void AstAppLayer::create_command_get_log_entry(uint32_t log_entry_id, std::vector<std::uint8_t>& command_data) {
ast_app_layer::Command cmd{};
cmd.type = ast_app_layer::CommandType::GET_LOG_ENTRY;
cmd.length = 0x0009;
cmd.status = ast_app_layer::CommandStatus::OK;
insert_u32_as_u8s(cmd.data, log_entry_id);
command_data = create_command(cmd);
}
void AstAppLayer::create_command_get_last_log_entry(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_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 AstAppLayer::create_command_get_log_entry_reverse(uint32_t log_entry_id, std::vector<std::uint8_t>& command_data) {
ast_app_layer::Command cmd{};
cmd.type = ast_app_layer::CommandType::GET_LOG_ENTRY_REVERSE;
cmd.length = 0x0009;
cmd.status = ast_app_layer::CommandStatus::OK;
insert_u32_as_u8s(cmd.data, log_entry_id);
command_data = create_command(cmd);
}
void AstAppLayer::create_command_get_application_board_mode(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::AB_MODE_SET);
}
void AstAppLayer::create_command_set_application_board_mode(ast_app_layer::ApplicationBoardMode mode,
std::vector<std::uint8_t>& command_data) {
ast_app_layer::Command cmd{};
cmd.type = ast_app_layer::CommandType::AB_MODE_SET;
cmd.length = 0x0006;
cmd.status = ast_app_layer::CommandStatus::OK;
cmd.data.push_back((uint8_t)mode);
command_data = create_command(cmd);
}
// diagnostics
void AstAppLayer::create_command_get_hardware_version(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::AB_HW_VERSION);
}
void AstAppLayer::create_command_get_application_board_server_id(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::AB_SERVER_ID);
}
void AstAppLayer::create_command_get_application_board_serial_number(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::AB_SERIAL_NR);
}
void AstAppLayer::create_command_get_application_board_software_version(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::AB_SW_VERSION);
}
void AstAppLayer::create_command_get_application_board_fw_checksum(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::AB_FW_CHECKSUM);
}
void AstAppLayer::create_command_get_application_board_fw_hash(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::AB_FW_HASH);
}
void AstAppLayer::create_command_get_application_board_status(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::AB_STATUS);
}
void AstAppLayer::create_command_get_metering_board_software_version(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::MB_SW_VERSION);
}
void AstAppLayer::create_command_get_metering_board_fw_checksum(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::MB_FW_CHECKSUM);
}
/* doubles as OCMF "meter model name" */
void AstAppLayer::create_command_get_device_type(std::vector<std::uint8_t>& command_data) {
command_data = create_simple_command(ast_app_layer::CommandType::AB_DEVICE_TYPE);
}
} // namespace ast_app_layer

View File

@@ -0,0 +1,361 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
/*
This is an implementation for the AST powermeter application layer
*/
#ifndef AST_APP_LAYER
#define AST_APP_LAYER
#include "ld-ev.hpp"
#include <chrono>
#include <cstdint>
#include <everest/logging.hpp>
#include <generated/types/powermeter.hpp>
namespace ast_app_layer {
enum class CommandType : std::uint16_t {
RESET_DC_METER = 0x4101, // reset the DC meter
AB_MODE_SET = 0x4102, // get or set the mode of application board
AB_SERVER_ID = 0x4110, // get or set the server ID of application board
AB_SERIAL_NR = 0x4111, // get or set the serial number
AB_HW_VERSION = 0x4112, // get or set the hardware version
AB_DEVICE_TYPE = 0x4113, // get or set the device type
AB_SW_VERSION = 0x4114, // get the software version of application board
AB_FW_CHECKSUM = 0x4115, // get the checksum (crc16) of application board
MB_SW_VERSION = 0x4116, // get the software version of metering board
MB_FW_CHECKSUM = 0x4117, // get the crc16 from software integrity check of metering board
AB_FW_HASH = 0x4119, // get the firmware hash(crc16) of application board
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
LINE_LOSS_IMPEDANCE = 0x4130, // get or set the line loss impedance
LINE_LOSS_MEAS_MODE = 0x4131, // get the line loss energy measurement mode
TIME = 0x4135, // get or set the device time
TEMPERATURE = 0x4136, // get the temperature
AB_STATUS = 0x4137, // get the statusword of application board
METER_BUS_ADDR = 0x4212, // get or set the meter bus address
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_PUBKEY_BIN = 0x1532, // get public key as binary string
GET_TRANSACT_IMPORT_LINE_LOSS_ENERGY = 0x1540, // get total transaction import line loss energy
GET_TRANSACT_EXPORT_LINE_LOSS_ENERGY = 0x1541, // get total transaction export line loss energy
GET_TRANSACT_TOTAL_IMPORT_DEV_ENERGY = 0x1550, // get total transaction import device energy
GET_TRANSACT_TOTAL_EXPORT_DEV_ENERGY = 0x1551, // get total transaction export device energy
GET_TRANSACT_TOTAL_IMPORT_MAINS_ENERGY = 0x1560, // get total transaction import mains energy
GET_TRANSACT_TOTAL_EXPORT_MAINS_ENERGY = 0x1561, // get total transaction export 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_EXPORT_LINE_LOSS_ENERGY = 0x1571, // get total start export line loss energy
GET_TOTAL_START_IMPORT_DEV_ENERGY = 0x1580, // get total start import device energy
GET_TOTAL_START_EXPORT_DEV_ENERGY = 0x1581, // get total start export device energy
GET_TOTAL_START_IMPORT_MAINS_ENERGY = 0x1590, // get total start import mains energy
GET_TOTAL_START_EXPORT_MAINS_ENERGY = 0x1591, // get total start export mains energy
GET_TOTAL_STOP_IMPORT_LINE_LOSS_ENERGY = 0x15A0, // get total stop import line loss energy
GET_TOTAL_STOP_EXPORT_LINE_LOSS_ENERGY = 0x15A1, // get total stop export line loss energy
GET_TOTAL_STOP_IMPORT_DEV_ENERGY = 0x15B0, // get total stop import device energy
GET_TOTAL_STOP_EXPORT_DEV_ENERGY = 0x15B1, // get total stop export device energy
GET_TOTAL_STOP_IMPORT_MAINS_ENERGY = 0x15C0, // get total stop import mains energy
GET_TOTAL_STOP_EXPORT_MAINS_ENERGY = 0x15C1, // get total stop export 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
REGISTER_DISPLAY_PUBKEY = 0x1801, // register display public key
REQUEST_CHALLENGE = 0x1810, // request challenge
SET_SIGNED_CHALLENGE = 0x1811, // set signed challenge
REQUEST_METER_PUBKEY = 0x1830, // request meter public key
GET_TOTAL_IMPORT_MAINS_ENERGY = 0x0110, // get total import mains energy
GET_TOTAL_EXPORT_MAINS_ENERGY = 0x0120, // get total export 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_TOTAL_EXPORT_MAINS_POWER = 0x0134, // total export mains power
GET_DEV_VOLTAGE_L1 = 0x0137, // get device voltage (phase L1)
GET_IMPORT_DEV_POWER = 0x0138, // get import device power
GET_EXPORT_DEV_POWER = 0x0139, // get export device power
GET_IMPORT_LINE_LOSS_POWER = 0x013A, // get import line loss power
GET_EXPORT_LINE_LOSS_POWER = 0x013B, // get export 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_EXPORT_LINE_LOSS_ENERGY = 0x0163, // get the total export line loss energy
GET_TOTAL_IMPORT_DEV_ENERGY = 0x0170, // get the total import device energy
GET_TOTAL_EXPORT_DEV_ENERGY = 0x0173, // get the total export device energy
GET_SECOND_INDEX = 0x0180, // get the second index
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)
REPEAT_DATA = 0xA000, // repeat data
OCMF_INFO = 0xA001, // get or set OCMF info
OCMF_CONFIG = 0xA002, // get or set the table of OCMF configuration field
GET_ERRORS = 0xA004, // get errors
AB_DMC = 0xA006, // get or set the DMC application board
AB_PROD_DATE = 0xA00B, // get or set the production date of application board
SET_REQUEST_CHALLENGE = 0xA00C // enable/disable the request challenge
};
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,
// PRODUCTION = 2
};
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:
ast_app_layer::LogType type;
std::uint32_t second_index{};
std::uint32_t utc_time{};
std::uint8_t utc_offset{};
std::vector<uint8_t> old_value; // max. 10 elements
std::vector<uint8_t> new_value; // max. 10 elements
std::vector<uint8_t> server_id; // 10 elements
std::vector<uint8_t> signature; // 64 elements
};
class Command {
public:
ast_app_layer::CommandType type;
std::uint16_t length;
ast_app_layer::CommandStatus status;
std::vector<std::uint8_t> data;
};
static constexpr std::uint16_t PM_AST_MAX_RX_LENGTH = 1000;
static constexpr std::uint16_t PM_AST_SERIAL_RX_INITIAL_TIMEOUT_MS = 1100;
static constexpr std::uint16_t PM_AST_SERIAL_RX_WITHIN_MESSAGE_TIMEOUT_MS = 100;
class AstAppLayer {
public:
void create_command_start_transaction(ast_app_layer::UserIdStatus user_id_status,
ast_app_layer::UserIdType user_id_type, const std::string& user_id_data,
std::int8_t gmt_offset_quarter_hours,
std::vector<std::uint8_t>& command_data);
void create_command_stop_transaction(std::vector<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, int8_t gmt_offset_quarters_of_an_hour,
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_export_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_start_export_energy(std::vector<std::uint8_t>& command_data);
void create_command_get_total_stop_export_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_dev_export_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_meter_pubkey(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_ocmf_info(std::vector<std::uint8_t>& command_data);
void create_command_get_ocmf_config(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(ast_app_layer::UserIdType id_type, std::string id_data,
std::vector<std::uint8_t>& command_data);
void create_command_get_errors(ast_app_layer::ErrorCategory category, ast_app_layer::ErrorSource src,
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(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(uint32_t log_entry_id, std::vector<std::uint8_t>& command_data);
void create_command_get_application_board_mode(std::vector<std::uint8_t>& command_data);
void create_command_set_application_board_mode(ast_app_layer::ApplicationBoardMode mode,
std::vector<std::uint8_t>& command_data);
void create_command_get_hardware_version(std::vector<std::uint8_t>& command_data);
void create_command_get_application_board_server_id(std::vector<std::uint8_t>& command_data);
void create_command_get_application_board_serial_number(std::vector<std::uint8_t>& command_data);
void create_command_get_application_board_software_version(std::vector<std::uint8_t>& command_data);
void create_command_get_application_board_fw_checksum(std::vector<std::uint8_t>& command_data);
void create_command_get_application_board_fw_hash(std::vector<std::uint8_t>& command_data);
void create_command_get_application_board_status(std::vector<std::uint8_t>& command_data);
void create_command_get_metering_board_software_version(std::vector<std::uint8_t>& command_data);
void create_command_get_metering_board_fw_checksum(std::vector<std::uint8_t>& command_data);
void create_command_get_device_type(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<uint8_t> create_command(ast_app_layer::Command cmd);
std::vector<uint8_t> create_simple_command(ast_app_layer::CommandType cmd_type);
};
} // namespace ast_app_layer
#endif // AST_APP_LAYER

View File

@@ -0,0 +1,225 @@
// 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_start_export_energy_Wh"] = k.total_start_export_energy_Wh;
j["total_stop_export_energy_Wh"] = k.total_stop_export_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["OCMF_info"] = json();
j["OCMF_info"]["gateway_id"] = k.ocmf_info.gateway_id;
j["OCMF_info"]["manufacturer"] = k.ocmf_info.manufacturer;
j["OCMF_info"]["model"] = k.ocmf_info.model;
j["total_dev_import_energy_Wh"] = k.total_dev_import_energy_Wh;
j["total_dev_export_energy_Wh"] = k.total_dev_export_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["app_board"] = json();
j["app_board"]["type"] = k.app_board.type;
j["app_board"]["HW_ver"] = k.app_board.hw_ver;
j["app_board"]["server_id"] = k.app_board.server_id;
j["app_board"]["mode"] = k.app_board.mode;
j["app_board"]["serial_nr"] = k.app_board.serial_number;
j["app_board"]["SW_ver"] = k.app_board.sw_ver;
j["app_board"]["FW_CRC"] = module::conversions::hexdump(k.app_board.fw_crc);
j["app_board"]["FW_hash"] = module::conversions::hexdump(k.app_board.fw_hash);
j["m_board"] = json();
j["m_board"]["HW_ver"] = k.m_board.hw_ver;
j["m_board"]["SW_ver"] = k.m_board.sw_ver;
j["m_board"]["FW_CRC"] = module::conversions::hexdump(k.m_board.fw_crc);
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 (uint8_t n = 0; n < k.ocmf_config_table.size(); n++) {
j["ocmf_config_table"][n] = module::conversions::hexdump((uint8_t)k.ocmf_config_table.at(n));
}
}
}
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 (uint8_t n = 0; n < 5; n++) {
j["errors"]["system"]["last"][n]["id"] = k.source[(uint8_t)ast_app_layer::ErrorSource::SYSTEM]
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST]
.error[n]
.id;
j["errors"]["system"]["last"][n]["priority"] = k.source[(uint8_t)ast_app_layer::ErrorSource::SYSTEM]
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST]
.error[n]
.priority;
j["errors"]["system"]["last"][n]["counter"] = k.source[(uint8_t)ast_app_layer::ErrorSource::SYSTEM]
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST]
.error[n]
.counter;
}
j["errors"]["system"]["last_critical"] = json::array();
for (uint8_t n = 0; n < 5; n++) {
j["errors"]["system"]["last_critical"][n]["id"] =
k.source[(uint8_t)ast_app_layer::ErrorSource::SYSTEM]
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST_CRITICAL]
.error[n]
.id;
j["errors"]["system"]["last_critical"][n]["priority"] =
k.source[(uint8_t)ast_app_layer::ErrorSource::SYSTEM]
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST_CRITICAL]
.error[n]
.priority;
j["errors"]["system"]["last_critical"][n]["counter"] =
k.source[(uint8_t)ast_app_layer::ErrorSource::SYSTEM]
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST_CRITICAL]
.error[n]
.counter;
}
j["errors"]["communication"] = json();
j["errors"]["communication"]["last"] = json::array();
for (uint8_t n = 0; n < 5; n++) {
j["errors"]["communication"]["last"][n]["id"] = k.source[(uint8_t)ast_app_layer::ErrorSource::COMMUNICATION]
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST]
.error[n]
.id;
j["errors"]["communication"]["last"][n]["priority"] =
k.source[(uint8_t)ast_app_layer::ErrorSource::COMMUNICATION]
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST]
.error[n]
.priority;
j["errors"]["communication"]["last"][n]["counter"] =
k.source[(uint8_t)ast_app_layer::ErrorSource::COMMUNICATION]
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST]
.error[n]
.counter;
}
j["errors"]["communication"]["last_critical"] = json::array();
for (uint8_t n = 0; n < 5; n++) {
j["errors"]["communication"]["last_critical"][n]["id"] =
k.source[(uint8_t)ast_app_layer::ErrorSource::COMMUNICATION]
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST_CRITICAL]
.error[n]
.id;
j["errors"]["communication"]["last_critical"][n]["priority"] =
k.source[(uint8_t)ast_app_layer::ErrorSource::COMMUNICATION]
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST_CRITICAL]
.error[n]
.priority;
j["errors"]["communication"]["last_critical"][n]["counter"] =
k.source[(uint8_t)ast_app_layer::ErrorSource::COMMUNICATION]
.category[(uint8_t)ast_app_layer::ErrorCategory::LAST_CRITICAL]
.error[n]
.counter;
}
}
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') << (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') << (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') << (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') << (uint16_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 std::move(str);
}
std::string u32_epoch_to_rfc3339(std::uint32_t epoch_time) {
auto tt = static_cast<time_t>(epoch_time);
std::tm tm = *std::gmtime(&tt);
std::stringstream ss;
ss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S.000Z");
return std::move(ss.str());
}
} // namespace conversions
} // namespace module

View File

@@ -0,0 +1,143 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef DIAGNOSTICS_HPP
#define DIAGNOSTICS_HPP
#include "ast_app_layer.hpp"
#include <bitset>
#include <cstdint>
#include <date/date.h>
#include <date/tz.h>
#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::uint64_t total_start_export_energy_Wh{}; // meter value needs to be divided by 10 for Wh
std::uint64_t total_stop_export_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;
OcmfInfo ocmf_info;
std::uint64_t total_dev_import_energy_Wh{}; // meter value needs to be divided by 10 for Wh
std::uint64_t total_dev_export_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);
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 ApplicationBoardInfo {
std::string type;
std::string hw_ver;
std::string server_id;
std::uint8_t mode{};
std::uint32_t serial_number{};
std::string sw_ver;
std::uint16_t fw_crc{};
std::uint16_t fw_hash{};
};
struct MeteringBoardInfo {
std::string hw_ver;
std::string sw_ver;
std::uint16_t fw_crc{};
};
struct DeviceDiagnostics {
std::string charge_point_id;
std::uint8_t charge_point_id_type{0};
LogStats log_stats;
ApplicationBoardInfo app_board;
MeteringBoardInfo m_board;
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);
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 {
ast_app_layer::LogEntry last_log;
ErrorSet source[2];
};
void to_json(json& j, const 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 (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 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,123 @@
// 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 "../AST_DC650.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
// insert your custom include headers here
#include "ast_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;
bool ignore_echo;
int gmt_offset_quarter_hours;
bool publish_device_data;
bool publish_device_diagnostics;
};
class powermeterImpl : public powermeterImplBase {
public:
powermeterImpl() = delete;
powermeterImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<AST_DC650>& 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<AST_DC650>& 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
};
MessageStatus start_transaction_msg_status{MessageStatus::NONE};
ast_app_layer::CommandResult start_transact_result{};
MessageStatus stop_transaction_msg_status{MessageStatus::NONE};
ast_app_layer::CommandResult stop_transact_result{};
MessageStatus get_transaction_values_msg_status{MessageStatus::NONE};
serial_device::SerialDevice serial_device{};
slip_protocol::SlipProtocol slip{};
ast_app_layer::AstAppLayer app_layer{};
types::powermeter::Powermeter pm_last_values;
DeviceData device_data_obj{};
DeviceDiagnostics device_diagnostics_obj{};
Logging logging_obj{};
ast_app_layer::ErrorCategory category_requested{};
ast_app_layer::ErrorSource source_requested{};
std::uint8_t error_diagnostics_target{0};
std::string last_ocmf_str;
void init_default_values();
void read_powermeter_values();
void set_device_time();
void set_device_charge_point_id(ast_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();
ast_app_layer::CommandResult process_response(const std::vector<std::uint8_t>& register_message);
void request_device_type();
void request_error_diagnostics(std::uint8_t addr);
void error_diagnostics(std::uint8_t addr);
ast_app_layer::CommandResult receive_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,60 @@
description: Module that collects power and energy measurements from an AST powermeter
provides:
main:
description: Implementation of the driver functionality
interface: powermeter
config:
powermeter_device_id:
description: The powermeter's address on the serial bus
type: integer
minimum: 0
maximum: 255
default: 1
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
rs485_direction_gpio:
description: GPIO to use for direction switching. Set to -1 to disable.
type: integer
default: -1
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
gmt_offset_quarter_hours:
description: GMT offset in quarters of an hour, e.g. 8 = +2h
type: integer
minimum: -47
maximum: 47
default: 0
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

View File

@@ -0,0 +1,190 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "serial_device.hpp"
#include <cstring>
#include <endian.h>
#include <everest/logging.hpp>
#include <fcntl.h>
#include <iostream>
#include <string>
#include <termios.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <fmt/core.h>
namespace {
std::string hexdump(std::uint8_t* 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(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) {
ignore_echo = _ignore_echo;
fd = open(device.c_str(), O_RDWR | O_NOCTTY | O_SYNC);
if (fd < 0) {
EVLOG_error << fmt::format("Serial: error {} opening {}: {}", 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
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: error from 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, static_cast<std::uint8_t*>(&rxbuf[0] + bytes_read_total),
static_cast<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);
}
}
} // namespace serial_device

View File

@@ -0,0 +1,44 @@
// 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 <mutex>
#include <optional>
#include <string>
#include <vector>
namespace serial_device {
constexpr int SERIAL_RX_INITIAL_TIMEOUT_MS = 500;
constexpr int SERIAL_RX_WITHIN_MESSAGE_TIMEOUT_MS = 100;
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);
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);
private:
// Serial interface
int fd{0};
bool ignore_echo{false};
std::mutex serial_mutex;
};
} // 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 DZG_GSH01, 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();
}
const std::uint16_t 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(std::uint8_t address, const std::vector<std::uint8_t>& payload) {
std::vector<std::uint8_t> vec{};
// address
vec.push_back(address);
// payload
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);
}
}
// CRC16
std::uint16_t crc = calculate_xModem_crc16(vec);
vec.push_back(static_cast<std::uint8_t>(crc & 0x00FF)); // LSB CRC16
vec.push_back(static_cast<std::uint8_t>((crc >> 8) & 0x00FF)); // MSB CRC16
// add start frame to front (can be done only after CRC has been calculated, as start frame is not part of CRC)
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);
restore_special_characters(message);
if (is_message_crc_correct(message)) {
// message intact, check for special characters and restore to original contents
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,52 @@
// 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, const 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