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,183 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <iostream>
#include <vector>
#include "CanBus.hpp"
#include <everest/io/can/can_recv_filter.hpp>
#include <everest/logging.hpp>
using namespace std::chrono_literals;
namespace {
constexpr auto CAN_RECOVERY_TIMER_INTERVAL = 1000ms;
constexpr auto CAN_POLL_STATUS_TIMER_INTERVAL = 1000ms;
// InfyPower protocol: space controller commands 50200 ms apart.
constexpr auto CAN_PACE_TX_INTERVAL = 50ms;
constexpr uint32_t INFY_INNER_FRAME_ID = 0x0757F800;
constexpr uint32_t INFY_INNER_FRAME_MASK = 0x1FFFF800;
std::vector<everest::lib::io::can::can_recv_filter> infy_kernel_recv_filters() {
return {everest::lib::io::can::can_recv_filter::reject_match(INFY_INNER_FRAME_ID, INFY_INNER_FRAME_MASK)};
}
} // namespace
CanBus::CanBus() : rx_thread_online{true}, can_bus(nullptr) {
}
CanBus::~CanBus() {
close_device();
}
bool CanBus::open_device(const std::string& dev) {
can_bus = std::make_unique<can::socket_can>(dev, infy_kernel_recv_filters());
can_bus->set_rx_handler([&](auto const& pl, auto&) {
uint32_t can_id = pl.get_can_id_with_flags();
this->rx_handler(can_id, pl.payload);
});
can_bus->set_error_handler([&](auto err, auto msg) {
if (err != 0) {
EVLOG_error << "CAN error: " << err << " - " << msg << std::endl;
on_error.store(true);
} else {
EVLOG_info << "CAN error cleared: " << msg << std::endl;
on_error.store(false);
}
});
ev_handler.register_event_handler(can_bus.get());
recovery_timer.set_timeout(CAN_RECOVERY_TIMER_INTERVAL);
ev_handler.register_event_handler(&recovery_timer, [&](event::fd_event_handler::event_list const& events) {
if (on_error.load()) {
EVLOG_error << "CAN error detected, attempting recovery";
can_bus->reset();
}
});
poll_status_timer.set_timeout(CAN_POLL_STATUS_TIMER_INTERVAL);
ev_handler.register_event_handler(
&poll_status_timer, [&](event::fd_event_handler::event_list const& events) { poll_status_handler(); });
pace_tx_timer.set_single_shot(true);
pace_tx_timer.disarm();
ev_handler.register_event_handler(&pace_tx_timer,
[&](event::fd_event_handler::event_list const& events) { pace_tx_handler(); });
rx_thread_handle = std::thread(&CanBus::rx_thread, this);
return true;
}
bool CanBus::close_device() {
if (!can_bus) {
return true;
}
EVLOG_info << "Closing CAN device";
rx_thread_online = false;
if (rx_thread_handle.joinable()) {
rx_thread_handle.join();
}
ev_handler.unregister_event_handler(&recovery_timer);
ev_handler.unregister_event_handler(&poll_status_timer);
ev_handler.unregister_event_handler(&pace_tx_timer);
ev_handler.unregister_event_handler(can_bus.get());
clear_paced_tx_queue();
can_bus.reset();
on_error.store(false);
EVLOG_info << "CAN device closed successfully";
return true;
}
void CanBus::rx_thread() {
EVLOG_info << "Starting CAN RX thread" << std::endl;
ev_handler.run(rx_thread_online);
}
void CanBus::enqueue_paced_tx(uint32_t can_id, std::vector<uint8_t> payload) {
m_pace_tx_queue.push_back(paced_tx_frame{can_id, std::move(payload)});
}
void CanBus::clear_paced_tx_queue() {
m_pace_tx_queue.clear();
disarm_pace_tx_timer();
}
void CanBus::start_paced_tx_cycle() {
disarm_pace_tx_timer();
if (m_pace_tx_queue.empty()) {
return;
}
auto frame = std::move(m_pace_tx_queue.front());
m_pace_tx_queue.pop_front();
_tx(frame.can_id, frame.payload);
if (!m_pace_tx_queue.empty()) {
arm_pace_tx_one_shot();
}
}
void CanBus::pace_tx_handler() {
pace_tx_timer.read();
if (m_pace_tx_queue.empty()) {
disarm_pace_tx_timer();
return;
}
auto frame = std::move(m_pace_tx_queue.front());
m_pace_tx_queue.pop_front();
_tx(frame.can_id, frame.payload);
if (!m_pace_tx_queue.empty()) {
arm_pace_tx_one_shot();
} else {
disarm_pace_tx_timer();
}
}
bool CanBus::arm_pace_tx_one_shot() {
return pace_tx_timer.set_timeout(CAN_PACE_TX_INTERVAL);
}
void CanBus::disarm_pace_tx_timer() {
pace_tx_timer.disarm();
}
bool CanBus::_tx(uint32_t can_id, const std::vector<uint8_t>& payload) {
if (payload.size() > 8) {
EVLOG_error << "CAN payload too large (" << payload.size() << " bytes), max 8 bytes allowed";
return false;
}
everest::lib::io::can::can_dataset data;
// Use plain 29-bit id only; EFF must not be OR'd into the arbitration field.
data.set_can_id_with_flags(can_id & CAN_EFF_MASK, true, false, false);
data.payload = payload;
if (on_error.load()) {
EVLOG_error << "CAN error detected, not sending frame";
return false;
}
return can_bus->tx(data);
}

View File

@@ -0,0 +1,69 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef CAN_BUS_HPP
#define CAN_BUS_HPP
#include <atomic>
#include <condition_variable>
#include <deque>
#include <linux/can.h>
#include <mutex>
#include <thread>
#include <vector>
#include <everest/io/can/socket_can.hpp>
#include <everest/io/event/fd_event_handler.hpp>
#include <everest/io/event/timer_fd.hpp>
using namespace everest::lib::io;
class CanBus {
public:
CanBus();
virtual ~CanBus();
bool open_device(const std::string& dev);
bool close_device();
protected:
virtual void rx_handler(uint32_t can_id, const std::vector<uint8_t>& payload) = 0;
virtual void poll_status_handler() = 0;
bool _tx(uint32_t can_id, const std::vector<uint8_t>& payload);
/**
* @brief Queue a frame for paced transmission on the CAN event thread.
* @details Call \ref start_paced_tx_cycle after enqueueing a poll batch.
*/
void enqueue_paced_tx(uint32_t can_id, std::vector<uint8_t> payload);
/** @brief Discard pending paced frames and disarm the pace timer. */
void clear_paced_tx_queue();
/**
* @brief Send the first queued frame immediately and schedule the rest at the pace interval.
*/
void start_paced_tx_cycle();
private:
struct paced_tx_frame {
uint32_t can_id{0};
std::vector<uint8_t> payload;
};
void pace_tx_handler();
bool arm_pace_tx_one_shot();
void disarm_pace_tx_timer();
std::unique_ptr<can::socket_can> can_bus;
std::atomic_bool on_error{false};
event::fd_event_handler ev_handler;
event::timer_fd recovery_timer;
event::timer_fd poll_status_timer;
event::timer_fd pace_tx_timer;
std::deque<paced_tx_frame> m_pace_tx_queue;
std::atomic_bool rx_thread_online;
std::thread rx_thread_handle;
void rx_thread();
};
#endif // CAN_BUS_HPP

View File

@@ -0,0 +1,220 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "CanPackets.hpp"
#include "Conversions.hpp"
#include <cstring>
#include <everest/logging.hpp>
#include <iomanip>
#include <iostream>
#include <sstream>
namespace can_packet_acdc {
namespace {
// CAN ID bit positions (as per InfyPower V1.13 protocol)
constexpr uint8_t CAN_ID_DESTINATION_ADDRESS_SHIFT = 8; // Bits 15-8
constexpr uint8_t CAN_ID_COMMAND_NUMBER_SHIFT = 16; // Bits 21-16
constexpr uint8_t CAN_ID_DEVICE_NUMBER_SHIFT = 22; // Bits 25-22
constexpr uint8_t CAN_ID_ERROR_CODE_SHIFT = 26; // Bits 28-26
} // namespace
// helper functions
uint32_t encode_can_id(uint8_t source_address, uint8_t destination_address, uint8_t command_number,
uint8_t device_number, uint8_t error_code) {
// Bits 28-26: Error code (0x00 for normal)
// Bits 25-22: Device No (4 bits)
// Bits 21-16: Command No (6 bits)
// Bits 15-8: Destination Address (8 bits)
// Bits 7-0: Source Address (8 bits) - configurable controller address (default 0xF0)
uint32_t id = source_address;
id |= destination_address << CAN_ID_DESTINATION_ADDRESS_SHIFT;
command_number &= InfyProtocol::COMMAND_MASK;
id |= command_number << CAN_ID_COMMAND_NUMBER_SHIFT;
device_number &= InfyProtocol::DEVICE_NUMBER_MASK;
id |= device_number << CAN_ID_DEVICE_NUMBER_SHIFT;
error_code &= InfyProtocol::ERROR_CODE_MASK;
id |= error_code << CAN_ID_ERROR_CODE_SHIFT;
return id;
}
uint8_t destination_address_from_can_id(uint32_t id) {
return ((id & CAN_EFF_MASK) >> CAN_ID_DESTINATION_ADDRESS_SHIFT) & 0xFF;
}
uint8_t source_address_from_can_id(uint32_t id) {
return id & CAN_EFF_MASK & 0xFF;
}
uint8_t command_number_from_can_id(uint32_t id) {
return ((id & CAN_EFF_MASK) >> CAN_ID_COMMAND_NUMBER_SHIFT) & InfyProtocol::COMMAND_MASK;
}
uint8_t error_code_from_can_id(uint32_t id) {
return ((id & CAN_EFF_MASK) >> CAN_ID_ERROR_CODE_SHIFT) & InfyProtocol::ERROR_CODE_MASK;
}
// packet definitions
PowerModuleStatus::PowerModuleStatus() {
}
PowerModuleStatus::PowerModuleStatus(const std::vector<uint8_t>& raw) {
// Size validation is handled at rx_handler level
uint8_t status0 = from_raw<uint8_t>(raw, 7);
uint8_t status1 = from_raw<uint8_t>(raw, 6);
uint8_t status2 = from_raw<uint8_t>(raw, 5);
// PowerModuleStatus bit mapping per InfyPower V1.13 protocol documentation
// status0 (byte 7): Bit0=output_short_current, Bit4=sleeping, Bit5=discharge_abnormal
// status1 (byte 6): Bit0=dc_side_off, Bit1=fault_alarm, Bit2=protection_alarm, Bit3=fan_fault_alarm,
// Bit4=over_temperature_alarm, Bit5=output_over_voltage_alarm, Bit6=walk_in_enable,
// Bit7=communication_interrupt_alarm
// status2 (byte 5): Bit0=power_limit_status, Bit1=id_repeat_alarm, Bit2=load_sharing_alarm,
// Bit3=input_phase_lost_alarm,
// Bit4=input_unbalanced_alarm, Bit5=input_low_voltage_alarm, Bit6=input_over_voltage_protection,
// Bit7=pfc_side_off
output_short_current = status0 & (1 << 0);
sleeping = status0 & (1 << 4);
discharge_abnormal = status0 & (1 << 5);
dc_side_off = status1 & (1 << 0);
fault_alarm = status1 & (1 << 1);
protection_alarm = status1 & (1 << 2);
fan_fault_alarm = status1 & (1 << 3);
over_temperature_alarm = status1 & (1 << 4);
output_over_voltage_alarm = status1 & (1 << 5);
walk_in_enable = status1 & (1 << 6);
communication_interrupt_alarm = status1 & (1 << 7);
power_limit_status = status2 & (1 << 0);
id_repeat_alarm = status2 & (1 << 1);
load_sharing_alarm = status2 & (1 << 2);
input_phase_lost_alarm = status2 & (1 << 3);
input_unbalanced_alarm = status2 & (1 << 4);
input_low_voltage_alarm = status2 & (1 << 5);
input_over_voltage_protection = status2 & (1 << 6);
pfc_side_off = status2 & (1 << 7);
}
std::ostream& operator<<(std::ostream& out, const PowerModuleStatus& self) {
out << "PowerModuleStatus: " << (self.output_short_current ? "output_short_current " : "")
<< (self.sleeping ? "sleeping " : "") << (self.discharge_abnormal ? "discharge_abnormal " : "")
<< (self.dc_side_off ? "dc_side_off " : "") << (self.fault_alarm ? "fault_alarm " : "")
<< (self.protection_alarm ? "protection_alarm " : "") << (self.fan_fault_alarm ? "fan_fault_alarm " : "")
<< (self.over_temperature_alarm ? "over_temperature_alarm " : "")
<< (self.output_over_voltage_alarm ? "output_over_voltage_alarm " : "")
<< (self.walk_in_enable ? "walk_in_enable " : "")
<< (self.communication_interrupt_alarm ? "communication_interrupt_alarm " : "")
<< (self.power_limit_status ? "power_limit_status " : "") << (self.id_repeat_alarm ? "id_repeat_alarm " : "")
<< (self.load_sharing_alarm ? "load_sharing_alarm " : "")
<< (self.input_phase_lost_alarm ? "input_phase_lost_alarm " : "")
<< (self.input_unbalanced_alarm ? "input_unbalanced_alarm " : "")
<< (self.input_low_voltage_alarm ? "input_low_voltage_alarm " : "")
<< (self.input_over_voltage_protection ? "input_over_voltage_protection " : "")
<< (self.pfc_side_off ? "pfc_side_off " : "");
return out;
}
PowerModuleStatus::operator std::vector<uint8_t>() const {
std::vector<uint8_t> data;
return data;
}
// New packet class implementations for V1.13 protocol
ReadModuleCount::ReadModuleCount() : count(0) {
}
ReadModuleCount::ReadModuleCount(const std::vector<uint8_t>& raw) {
if (raw.size() < 3) {
EVLOG_warning << "Received invalid ReadModuleCount packet with size " << raw.size();
return;
}
count = from_raw<uint8_t>(raw, 2);
}
ReadModuleCount::operator std::vector<uint8_t>() const {
std::vector<uint8_t> data(8);
return data;
}
ReadModuleVI::ReadModuleVI(const std::vector<uint8_t>& raw) {
// Size validation is handled at rx_handler level
voltage = from_raw<float>(raw, 0);
current = from_raw<float>(raw, 4);
}
ReadModuleVI::operator std::vector<uint8_t>() const {
return {};
}
ReadModuleCapabilities::ReadModuleCapabilities(const std::vector<uint8_t>& raw) {
// Size validation is handled at rx_handler level
max_voltage = from_raw<uint16_t>(raw, 0) * InfyProtocol::SCALING_FACTOR_1_0;
min_voltage = from_raw<uint16_t>(raw, 2) * InfyProtocol::SCALING_FACTOR_1_0;
max_current = from_raw<uint16_t>(raw, 4) * InfyProtocol::SCALING_FACTOR_0_1;
rated_power = from_raw<uint16_t>(raw, 6) * InfyProtocol::SCALING_FACTOR_10_0;
}
ReadModuleCapabilities::operator std::vector<uint8_t>() const {
return {};
}
ReadModuleVIAfterDiode::ReadModuleVIAfterDiode(const std::vector<uint8_t>& raw) {
// Size validation is handled at rx_handler level
v_ext = from_raw<uint16_t>(raw, 0) * InfyProtocol::SCALING_FACTOR_0_1;
i_avail = from_raw<uint16_t>(raw, 2) * InfyProtocol::SCALING_FACTOR_0_1;
}
ReadModuleVIAfterDiode::operator std::vector<uint8_t>() const {
return {};
}
ReadModuleBarcode::ReadModuleBarcode(const std::vector<uint8_t>& raw) {
// Size validation is handled at rx_handler level
// According to protocol documentation for Command 0x0B:
// Example: Barcode "081807123451V1704" -> Response: 56 13 0C 15 A3 FB 06 A8
// Byte 0: 13th character (ASCII) - 0x56 = 'V'
// Bytes 1-7: Encoded first 12 characters + last 4 characters in complex HEX format
// For now, we'll extract what we can and create a readable serial number
char thirteenth_char = static_cast<char>(from_raw<uint8_t>(raw, 0));
// Extract the remaining bytes as hex values for debugging/identification
std::stringstream ss;
ss << "SN_" << std::hex << std::setfill('0');
for (size_t i = 1; i < 8 && i < raw.size(); ++i) {
ss << std::setw(2) << static_cast<unsigned>(from_raw<uint8_t>(raw, i));
}
ss << "_" << thirteenth_char;
serial_number = ss.str();
}
ReadModuleBarcode::operator std::vector<uint8_t>() const {
return {};
}
SetModuleVI::SetModuleVI(float v, float c) : voltage(v), current(c) {
}
SetModuleVI::operator std::vector<uint8_t>() const {
std::vector<uint8_t> data;
to_raw(static_cast<uint32_t>(voltage * InfyProtocol::VOLTAGE_TO_MV), data); // we need to transform V to mV
to_raw(static_cast<uint32_t>(current * InfyProtocol::CURRENT_TO_MA), data); // we need to transform A to mA
return data;
}
SetModuleOnOff::SetModuleOnOff(bool o) : on(o) {
}
SetModuleOnOff::operator std::vector<uint8_t>() const {
std::vector<uint8_t> data;
to_raw(static_cast<uint8_t>(on ? 0x00 : 0x01), data);
// The rest of the payload is reserved and should be 0
for (int i = 0; i < 7; ++i) {
to_raw(static_cast<uint8_t>(0), data);
}
return data;
}
} // namespace can_packet_acdc

View File

@@ -0,0 +1,139 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef CAN_PACKETS_HPP
#define CAN_PACKETS_HPP
#include <linux/can.h>
#include <ostream>
#include <stdint.h>
#include <vector>
namespace InfyProtocol {
// CAN Protocol Constants
constexpr uint8_t DEVICE_SINGLE_MODULE = 0x0A;
constexpr uint8_t DEVICE_GROUP_MODULE = 0x0B;
constexpr uint32_t CAN_EXTENDED_FLAG = 0x80000000U;
// Bit Masks for CAN ID encoding
constexpr uint8_t COMMAND_MASK = 0x3F; // 6-bit mask for command number
constexpr uint8_t DEVICE_NUMBER_MASK = 0x0F; // 4-bit mask for device number
constexpr uint8_t ERROR_CODE_MASK = 0x07; // 3-bit mask for error code
// Unit Conversion Constants
constexpr uint32_t VOLTAGE_TO_MV = 1000U; // Volts to millivolts (V * 1000 = mV)
constexpr uint32_t CURRENT_TO_MA = 1000U; // Amperes to milliamperes (A * 1000 = mA)
// Scaling Factors for Raw Data Conversion
constexpr float SCALING_FACTOR_0_1 = 0.1f; // 0.1 scaling factor for voltage/current raw data
constexpr float SCALING_FACTOR_1_0 = 1.0f; // 1.0 scaling factor for voltage raw data
constexpr float SCALING_FACTOR_10_0 = 10.0f; // 10.0 scaling factor for power raw data
} // namespace InfyProtocol
namespace can_packet_acdc {
uint32_t encode_can_id(uint8_t source_address, uint8_t destination_address, uint8_t command_number,
uint8_t device_number, uint8_t error_code);
uint8_t destination_address_from_can_id(uint32_t id);
uint8_t source_address_from_can_id(uint32_t id);
uint8_t command_number_from_can_id(uint32_t id);
uint8_t error_code_from_can_id(uint32_t id);
struct PowerModuleStatus {
static constexpr uint8_t CMD_ID = 0x04;
PowerModuleStatus();
PowerModuleStatus(const std::vector<uint8_t>& raw);
friend std::ostream& operator<<(std::ostream& out, const PowerModuleStatus& self);
operator std::vector<uint8_t>() const;
bool output_short_current{false}; // Error if all modules have this state
bool sleeping{false}; // Status
bool discharge_abnormal{false}; // Vendor Warning
bool dc_side_off{false}; // Status
bool fault_alarm{false}; // Error if all modules have this state
bool protection_alarm{false}; // Vendor Warning
bool fan_fault_alarm{false}; // Vendor Warning
bool over_temperature_alarm{false}; // Vendor Warning
bool output_over_voltage_alarm{false}; // Vendor Warning
bool walk_in_enable{false}; // Status
bool communication_interrupt_alarm{false};
bool power_limit_status{false}; // Status
bool id_repeat_alarm{false}; // Status
bool load_sharing_alarm{false}; // Vendor Warning
bool input_phase_lost_alarm{false}; // Vendor Warning
bool input_unbalanced_alarm{false}; // Vendor Warning
bool input_low_voltage_alarm{false}; // Vendor Warning
bool input_over_voltage_protection{false}; // Vendor Warning
bool pfc_side_off{false}; // Status
};
// New packet classes for V1.13 protocol
struct ReadModuleCount {
static constexpr uint8_t CMD_ID = 0x02;
ReadModuleCount();
ReadModuleCount(const std::vector<uint8_t>& raw);
operator std::vector<uint8_t>() const;
uint8_t count{0};
};
struct ReadModuleVI {
static constexpr uint8_t CMD_ID = 0x03;
ReadModuleVI(const std::vector<uint8_t>& raw);
operator std::vector<uint8_t>() const;
float voltage{0.0f};
float current{0.0f};
};
struct ReadModuleCapabilities {
static constexpr uint8_t CMD_ID = 0x0A;
ReadModuleCapabilities(const std::vector<uint8_t>& raw);
operator std::vector<uint8_t>() const;
float max_voltage{0.0f};
float min_voltage{0.0f};
float max_current{0.0f};
float rated_power{0.0f};
};
struct ReadModuleVIAfterDiode {
static constexpr uint8_t CMD_ID = 0x0C;
ReadModuleVIAfterDiode(const std::vector<uint8_t>& raw);
operator std::vector<uint8_t>() const;
float v_ext{0.0f};
float i_avail{0.0f};
};
struct ReadModuleBarcode {
static constexpr uint8_t CMD_ID = 0x0B;
ReadModuleBarcode(const std::vector<uint8_t>& raw);
operator std::vector<uint8_t>() const;
std::string serial_number; // Full barcode string (e.g., "081807123451V1704")
};
struct SetModuleVI {
static constexpr uint8_t CMD_ID = 0x1C;
SetModuleVI(float voltage, float current);
operator std::vector<uint8_t>() const;
float voltage{0.0f};
float current{0.0f};
};
struct SetModuleOnOff {
static constexpr uint8_t CMD_ID = 0x1A;
SetModuleOnOff(bool on);
operator std::vector<uint8_t>() const;
bool on{false};
};
} // namespace can_packet_acdc
#endif // CAN_PACKETS_HPP

View File

@@ -0,0 +1,120 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef CONVERSIONS_HPP
#define CONVERSIONS_HPP
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <type_traits>
#include <vector>
#include <endian.h>
// Helper template to ensure type safety for conversion operations
template <typename T> struct is_conversion_safe {
static constexpr bool value =
std::is_trivially_copyable_v<T> && std::is_standard_layout_v<T> && !std::is_pointer_v<T>;
};
template <class T>
typename std::enable_if_t<sizeof(T) == sizeof(uint8_t) && is_conversion_safe<T>::value, T>
from_raw(const std::vector<uint8_t>& raw, int idx) {
if (idx + sizeof(T) > raw.size()) {
throw std::out_of_range("from_raw: buffer access out of bounds");
}
T ret;
memcpy(&ret, &raw[idx], 1);
return ret;
}
template <class T>
typename std::enable_if_t<sizeof(T) == sizeof(uint16_t) && is_conversion_safe<T>::value, T>
from_raw(const std::vector<uint8_t>& raw, int idx) {
if (idx + sizeof(T) > raw.size()) {
throw std::out_of_range("from_raw: buffer access out of bounds");
}
uint16_t tmp;
memcpy(&tmp, raw.data() + idx, sizeof(uint16_t)); // Safe copy from buffer
tmp = be16toh(tmp); // Convert endianness
T ret;
memcpy(&ret, &tmp, sizeof(T));
return ret;
}
template <class T>
typename std::enable_if_t<sizeof(T) == sizeof(uint32_t) && is_conversion_safe<T>::value, T>
from_raw(const std::vector<uint8_t>& raw, int idx) {
if (idx + sizeof(T) > raw.size()) {
throw std::out_of_range("from_raw: buffer access out of bounds");
}
uint32_t tmp;
memcpy(&tmp, raw.data() + idx, sizeof(uint32_t)); // Safe copy from buffer
tmp = be32toh(tmp); // Convert endianness
T ret;
memcpy(&ret, &tmp, sizeof(T));
return ret;
}
template <class T>
typename std::enable_if_t<sizeof(T) == sizeof(uint64_t) && is_conversion_safe<T>::value, T>
from_raw(const std::vector<uint8_t>& raw, int idx) {
if (idx + sizeof(T) > raw.size()) {
throw std::out_of_range("from_raw: buffer access out of bounds");
}
uint64_t tmp;
memcpy(&tmp, raw.data() + idx, sizeof(uint64_t)); // Safe copy from buffer
tmp = be64toh(tmp); // Convert endianness
T ret;
memcpy(&ret, &tmp, sizeof(T));
return ret;
}
template <class T>
typename std::enable_if_t<sizeof(T) == sizeof(uint8_t) && is_conversion_safe<T>::value>
to_raw(T src, std::vector<uint8_t>& dest) {
uint8_t tmp;
memcpy(&tmp, &src, sizeof(T));
dest.push_back(tmp);
}
template <class T>
typename std::enable_if_t<sizeof(T) == sizeof(uint16_t) && is_conversion_safe<T>::value>
to_raw(T src, std::vector<uint8_t>& dest) {
uint16_t tmp;
memcpy(&tmp, &src, sizeof(T));
tmp = htobe16(tmp);
// Use array for better alignment guarantees
alignas(uint16_t) uint8_t ret[sizeof(uint16_t)];
memcpy(ret, &tmp, sizeof(uint16_t));
dest.insert(dest.end(), {ret[0], ret[1]});
}
template <class T>
typename std::enable_if_t<sizeof(T) == sizeof(uint32_t) && is_conversion_safe<T>::value>
to_raw(T src, std::vector<uint8_t>& dest) {
uint32_t tmp;
memcpy(&tmp, &src, sizeof(T));
tmp = htobe32(tmp);
// Use array for better alignment guarantees
alignas(uint32_t) uint8_t ret[sizeof(uint32_t)];
memcpy(ret, &tmp, sizeof(uint32_t));
dest.insert(dest.end(), {ret[0], ret[1], ret[2], ret[3]});
}
template <class T>
typename std::enable_if_t<sizeof(T) == sizeof(uint64_t) && is_conversion_safe<T>::value>
to_raw(T src, std::vector<uint8_t>& dest) {
uint64_t tmp;
memcpy(&tmp, &src, sizeof(T));
tmp = htobe64(tmp);
// Use array for better alignment guarantees
alignas(uint64_t) uint8_t ret[sizeof(uint64_t)];
memcpy(ret, &tmp, sizeof(uint64_t));
dest.insert(dest.end(), {ret[0], ret[1], ret[2], ret[3], ret[4], ret[5], ret[6], ret[7]});
}
#endif // CONVERSIONS_HPP

View File

@@ -0,0 +1,368 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "InfyCanDevice.hpp"
#include "CanPackets.hpp"
#include <everest/logging.hpp>
#include <algorithm>
#include <iomanip>
#include <regex>
static std::vector<std::string> split_by_delimiters(const std::string& s, const std::string& delimiters) {
std::regex re("[" + delimiters + "]");
std::sregex_token_iterator first{s.begin(), s.end(), re, -1}, last;
return {first, last};
}
static std::vector<uint8_t> parse_module_addresses(const std::string& a) {
std::vector<uint8_t> addresses;
auto adr = split_by_delimiters(a, ",");
addresses.reserve(adr.size()); // Pre-allocate memory for efficiency
for (const auto& ad : adr) {
try {
addresses.push_back(std::stoi(ad));
} catch (const std::exception& e) {
EVLOG_error << "Infy: Invalid module address '" << ad << "': " << e.what();
}
}
return addresses;
}
InfyCanDevice::InfyCanDevice() : CanBus() {
}
InfyCanDevice::~InfyCanDevice() {
}
void InfyCanDevice::initial_ping() {
if (operating_mode == OperatingMode::GROUP_DISCOVERY) {
send_command<can_packet_acdc::ReadModuleCount>(group_address, true);
} else {
EVLOG_info << "Infy: Operating in FIXED_ADDRESS mode. No need to ping.";
initialized = true;
switch_on_off(false);
}
}
void InfyCanDevice::set_can_device(const std::string& dev) {
can_device = dev;
EVLOG_info << "Infy: Setting config values: CAN device: " << dev;
open_device(can_device.c_str());
}
void InfyCanDevice::set_config_values(const std::string& addrs, int group_addr, int timeout, int controller_address) {
this->device_connection_timeout_s = timeout;
this->group_address = group_addr;
this->controller_address = controller_address;
EVLOG_info << "Infy: Operating with controller address: 0x" << std::hex << controller_address;
if (!addrs.empty()) {
operating_mode = OperatingMode::FIXED_ADDRESS;
active_module_addresses = parse_module_addresses(addrs);
expected_module_count = active_module_addresses.size();
EVLOG_info << "Infy: Operating in FIXED_ADDRESS mode with " << expected_module_count << " addresses: " << addrs;
} else {
operating_mode = OperatingMode::GROUP_DISCOVERY;
EVLOG_info << "Infy: Operating in GROUP_DISCOVERY mode for group address: " << group_address;
}
EVLOG_info << "Infy: module communication timeout: " << device_connection_timeout_s << "s";
}
void InfyCanDevice::rx_handler(uint32_t can_id, const std::vector<uint8_t>& payload) {
if (!(can_id & CAN_EFF_FLAG)) {
return;
}
// Ignore messages not addressed to us (the controller)
if (can_packet_acdc::destination_address_from_can_id(can_id) != controller_address) {
return;
}
// Discard malformed CAN frames with insufficient data
if (payload.size() < 8) {
EVLOG_error << "Infy: Received malformed CAN frame with size " << payload.size()
<< " (expected 8 bytes). Discarding frame.";
return;
}
const uint8_t source_address = can_packet_acdc::source_address_from_can_id(can_id);
const uint8_t command_number = can_packet_acdc::command_number_from_can_id(can_id);
switch (command_number) {
case can_packet_acdc::ReadModuleCount::CMD_ID: {
handle_module_count_packet(payload);
} break;
case can_packet_acdc::ReadModuleVI::CMD_ID: {
handle_simple_telemetry_update(source_address, payload, command_number);
} break;
case can_packet_acdc::PowerModuleStatus::CMD_ID: {
can_packet_acdc::PowerModuleStatus status(payload);
signalModuleStatus(status);
// Signal error status changes (excluding fields marked as "Status")
auto& telemetry = telemetries[source_address];
check_and_signal_error_status_change(source_address, status, telemetry.status);
telemetry.status = status;
// using status message to set the last_update time
telemetry.last_update = std::chrono::steady_clock::now();
} break;
case can_packet_acdc::ReadModuleVIAfterDiode::CMD_ID: {
handle_simple_telemetry_update(source_address, payload, command_number);
} break;
case can_packet_acdc::ReadModuleCapabilities::CMD_ID: {
handle_simple_telemetry_update(source_address, payload, command_number);
} break;
case can_packet_acdc::ReadModuleBarcode::CMD_ID: {
can_packet_acdc::ReadModuleBarcode barcode(payload);
auto& telemetry = telemetries[source_address];
telemetry.serial_number = barcode.serial_number;
EVLOG_info << format_module_id(source_address) << ": serial number: " << barcode.serial_number;
} break;
default: {
// Not implemented yet
}
}
}
size_t InfyCanDevice::remove_expired_telemetry_entries() {
auto now = std::chrono::steady_clock::now();
auto timeout_duration = std::chrono::seconds(device_connection_timeout_s);
size_t removed_count = 0;
// Remove expired telemetry entries
for (auto it = telemetries.begin(); it != telemetries.end();) {
const auto& [address, telemetry] = *it;
if (now - telemetry.last_update > timeout_duration) {
EVLOG_warning << format_module_id(address, telemetry.serial_number)
<< ": module communication expired (timeout: " << device_connection_timeout_s
<< "s). Removing from active modules.";
it = telemetries.erase(it);
{
std::lock_guard<std::mutex> lock(active_modules_mutex);
active_module_addresses.erase(
std::remove(active_module_addresses.begin(), active_module_addresses.end(), address),
active_module_addresses.end());
}
++removed_count;
} else {
++it;
}
}
// Update active_module_addresses to match current telemetries keys
{
// Check CommunicationFault state: trigger if no active modules but we expect some, clear otherwise
if (removed_count != 0 && telemetries.empty()) {
// No modules responding - trigger CommunicationFault
signalError(0xFF, Error::CommunicationFault, true); // Use address 0xFF for system-wide fault
} else if (!telemetries.empty()) {
// At least one module responding - clear CommunicationFault
signalError(0xFF, Error::CommunicationFault, false); // Use address 0xFF for system-wide fault
}
}
return removed_count;
}
void InfyCanDevice::poll_status_handler() {
// Remove expired telemetry entries
size_t removed_count = remove_expired_telemetry_entries();
if (removed_count > 0) {
EVLOG_info << "Infy: Removed " << removed_count << " expired modules. "
<< "Active modules remaining: " << active_module_addresses.size();
// signal the telemetry updates
signalCapabilitiesUpdate(telemetries);
signalVoltageCurrent(telemetries);
}
// --- Telemetry Polling (paced on CAN event thread) ---
static const std::vector<uint8_t> empty_read_payload(8, 0);
clear_paced_tx_queue();
if (operating_mode == OperatingMode::GROUP_DISCOVERY) {
enqueue_poll_command(group_address, can_packet_acdc::ReadModuleCount::CMD_ID, empty_read_payload, true);
}
std::vector<uint8_t> poll_addresses;
{
std::lock_guard<std::mutex> lock(active_modules_mutex);
poll_addresses = active_module_addresses;
}
for (const auto& addr : poll_addresses) {
enqueue_poll_command(addr, can_packet_acdc::ReadModuleVI::CMD_ID, empty_read_payload);
enqueue_poll_command(addr, can_packet_acdc::PowerModuleStatus::CMD_ID, empty_read_payload);
enqueue_poll_command(addr, can_packet_acdc::ReadModuleVIAfterDiode::CMD_ID, empty_read_payload);
auto it = telemetries.find(addr);
if (it == telemetries.end() || it->second.serial_number.empty()) {
enqueue_poll_command(addr, can_packet_acdc::ReadModuleCapabilities::CMD_ID, empty_read_payload);
enqueue_poll_command(addr, can_packet_acdc::ReadModuleBarcode::CMD_ID, empty_read_payload);
}
}
start_paced_tx_cycle();
}
bool InfyCanDevice::switch_on_off(bool on) {
std::lock_guard<std::mutex> lock(active_modules_mutex);
EVLOG_info << "Infy: switch_on_off(" << on << ") - active modules: " << active_module_addresses.size();
bool success = true;
if (active_module_addresses.empty()) {
EVLOG_warning << "Infy: No active modules to send switch_on_off command to.";
return false;
}
for (const auto& addr : active_module_addresses) {
bool tx_result = send_command(addr, can_packet_acdc::SetModuleOnOff(on));
success &= tx_result;
}
return success;
}
bool InfyCanDevice::set_voltage_current(float voltage, float current) {
std::lock_guard<std::mutex> lock(active_modules_mutex);
EVLOG_info << "Infy: set_voltage_current(" << voltage << "V, " << current
<< "A) - active modules: " << active_module_addresses.size();
// Validate that we have active modules before attempting to divide current
const size_t module_count = active_module_addresses.size();
if (module_count == 0) {
EVLOG_warning << "Infy: No active modules to set voltage/current.";
return false;
}
// Current is shared between all modules - safe division guaranteed by check above
const float current_per_module = current / static_cast<float>(module_count);
bool success = true;
for (const auto& addr : active_module_addresses) {
bool tx_result = send_command(addr, can_packet_acdc::SetModuleVI(voltage, current_per_module));
success &= tx_result;
}
return success;
}
bool InfyCanDevice::send_command_impl(uint8_t destination_address, uint8_t command_number,
const std::vector<uint8_t>& payload, bool group) {
uint32_t can_id = can_packet_acdc::encode_can_id(
controller_address, destination_address, command_number,
group ? InfyProtocol::DEVICE_GROUP_MODULE : InfyProtocol::DEVICE_SINGLE_MODULE, 0x00);
auto result = _tx(can_id, payload);
if (not result) {
EVLOG_warning << "Infy: CAN transmission failed for can_id: 0x" << std::hex << std::uppercase << can_id;
}
return result;
}
void InfyCanDevice::enqueue_poll_command(uint8_t destination_address, uint8_t command_number,
const std::vector<uint8_t>& payload, bool group) {
uint32_t can_id = can_packet_acdc::encode_can_id(
controller_address, destination_address, command_number,
group ? InfyProtocol::DEVICE_GROUP_MODULE : InfyProtocol::DEVICE_SINGLE_MODULE, 0x00);
enqueue_paced_tx(can_id, payload);
}
void InfyCanDevice::handle_module_count_packet(const std::vector<uint8_t>& payload) {
can_packet_acdc::ReadModuleCount n(payload);
if (operating_mode != OperatingMode::GROUP_DISCOVERY) {
return;
}
// n count must be at least 1, it is the module that it is answering (the group master)
expected_module_count = n.count;
if (expected_module_count != telemetries.size()) {
EVLOG_info << "Infy: System reports " << expected_module_count
<< " total modules in group, we might have lost some modules, waiting for timeout or recovery";
}
// Initially assume all configured modules are active (will be updated based on responses)
{
std::lock_guard<std::mutex> lock(active_modules_mutex);
active_module_addresses.clear();
active_module_addresses.reserve(n.count); // Pre-allocate before assignment
for (uint8_t i = 0; i < n.count; ++i) {
active_module_addresses.push_back(i);
}
}
if (!initialized) {
initialized = true;
EVLOG_info << "Infy: Received first module count packet. Make sure that the modules are off";
switch_on_off(false);
}
}
void InfyCanDevice::handle_simple_telemetry_update(uint8_t source_address, const std::vector<uint8_t>& payload,
uint8_t command_number) {
auto& telemetry = telemetries[source_address];
switch (command_number) {
case can_packet_acdc::ReadModuleVI::CMD_ID: {
can_packet_acdc::ReadModuleVI vi(payload);
telemetry.voltage = vi.voltage;
telemetry.current = vi.current;
signalVoltageCurrent(telemetries);
} break;
case can_packet_acdc::ReadModuleVIAfterDiode::CMD_ID: {
can_packet_acdc::ReadModuleVIAfterDiode v_after_diode(payload);
telemetry.v_ext = v_after_diode.v_ext;
telemetry.i_avail = v_after_diode.i_avail;
signalVoltageCurrent(telemetries);
} break;
case can_packet_acdc::ReadModuleCapabilities::CMD_ID: {
can_packet_acdc::ReadModuleCapabilities caps(payload);
telemetry.valid_caps = true;
telemetry.dc_max_output_voltage = caps.max_voltage;
telemetry.dc_min_output_voltage = caps.min_voltage;
telemetry.dc_max_output_current = caps.max_current;
telemetry.dc_rated_output_power = caps.rated_power;
EVLOG_info << format_module_id(source_address) << ": capabilities: " << caps.max_voltage << "V / "
<< caps.min_voltage << "V, " << caps.max_current << "A, power " << caps.rated_power << "W";
signalCapabilitiesUpdate(telemetries);
} break;
}
}
void InfyCanDevice::check_and_signal_error_status_change(uint8_t source_address,
const can_packet_acdc::PowerModuleStatus& new_status,
const can_packet_acdc::PowerModuleStatus& old_status) {
// Helper lambda to reduce repetition in error status checking
auto check_status_change = [this, source_address](bool new_val, bool old_val, Error error_type) {
if (new_val != old_val) {
signalError(source_address, error_type, new_val);
}
};
// Check all error status changes (excluding fields marked as "Status")
check_status_change(new_status.fault_alarm, old_status.fault_alarm, Error::InternalFault);
check_status_change(new_status.over_temperature_alarm, old_status.over_temperature_alarm, Error::OverTemperature);
check_status_change(new_status.output_over_voltage_alarm, old_status.output_over_voltage_alarm, Error::OverVoltage);
check_status_change(new_status.fan_fault_alarm, old_status.fan_fault_alarm, Error::FanFault);
check_status_change(new_status.input_phase_lost_alarm, old_status.input_phase_lost_alarm, Error::InputPhaseLoss);
check_status_change(new_status.output_short_current, old_status.output_short_current, Error::OverCurrent);
check_status_change(new_status.communication_interrupt_alarm, old_status.communication_interrupt_alarm,
Error::CommunicationFault);
check_status_change(new_status.input_low_voltage_alarm, old_status.input_low_voltage_alarm, Error::UnderVoltage);
check_status_change(new_status.input_unbalanced_alarm, old_status.input_unbalanced_alarm, Error::InputVoltage);
check_status_change(new_status.input_over_voltage_protection, old_status.input_over_voltage_protection,
Error::InputVoltage);
check_status_change(new_status.protection_alarm, old_status.protection_alarm, Error::InternalFault);
check_status_change(new_status.load_sharing_alarm, old_status.load_sharing_alarm, Error::InternalFault);
check_status_change(new_status.discharge_abnormal, old_status.discharge_abnormal, Error::InternalFault);
}
std::string InfyCanDevice::format_module_id(uint8_t address, const std::string& serial_number) const {
std::stringstream ss;
ss << "Infy[0x" << std::hex << std::uppercase << std::setfill('0') << std::setw(2) << static_cast<int>(address);
if (!serial_number.empty()) {
ss << "/" << serial_number;
}
ss << "]";
return ss.str();
}

View File

@@ -0,0 +1,120 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef INFY_CAN_DEVICE_HPP
#define INFY_CAN_DEVICE_HPP
#include "CanBus.hpp"
#include "CanPackets.hpp"
#include <chrono>
#include <linux/can.h>
#include <map>
#include <mutex>
#include <sigslot/signal.hpp>
#include <vector>
class InfyCanDevice : public CanBus {
public:
InfyCanDevice();
~InfyCanDevice();
enum class Error {
OverVoltage,
UnderVoltage,
OverTemperature,
FanFault,
InputPhaseLoss,
CommunicationFault,
InternalFault,
OverCurrent,
InputVoltage
};
enum class OperatingMode {
FIXED_ADDRESS,
GROUP_DISCOVERY
};
void set_can_device(const std::string& dev);
void set_config_values(const std::string& addrs, int group_address, int timeout, int controller_address);
void initial_ping();
// Commands
bool switch_on_off(bool on);
bool set_voltage_current(float voltage, float current);
// Template overloads for type-safe command sending
template <typename PacketType> bool send_command(uint8_t destination_address, bool group = false) {
// Use static const vector to avoid repeated allocations
static const std::vector<uint8_t> empty_payload(
8, 0); // 8 zero bytes for read commands, otherwise the device returns an error
return send_command_impl(destination_address, PacketType::CMD_ID, empty_payload, group);
}
template <typename PacketType>
bool send_command(uint8_t destination_address, const PacketType& packet, bool group = false) {
return send_command_impl(destination_address, PacketType::CMD_ID, packet.operator std::vector<uint8_t>(),
group);
}
struct Telemetry {
float voltage{0.};
float current{0.};
float v_ext{0.};
float i_avail{0.};
bool valid_caps{false};
float dc_max_output_voltage{0.};
float dc_min_output_voltage{0.};
float dc_max_output_current{0.};
float dc_rated_output_power{0.};
can_packet_acdc::PowerModuleStatus status;
std::string serial_number; // Module barcode/serial number for identification
std::chrono::time_point<std::chrono::steady_clock> last_update;
};
typedef std::map<uint8_t, Telemetry> TelemetryMap;
TelemetryMap telemetries;
// Data out
sigslot::signal<TelemetryMap> signalVoltageCurrent;
sigslot::signal<can_packet_acdc::PowerModuleStatus> signalModuleStatus;
sigslot::signal<uint8_t, Error, bool> signalError;
sigslot::signal<TelemetryMap> signalCapabilitiesUpdate;
protected:
virtual void rx_handler(uint32_t can_id, const std::vector<uint8_t>& payload);
private:
bool initialized{false}; // Set to true when we have received the very first module count packet
uint8_t controller_address{0};
std::string can_device{""};
int group_address{0};
size_t expected_module_count{0};
int device_connection_timeout_s{0};
OperatingMode operating_mode{OperatingMode::FIXED_ADDRESS};
std::vector<uint8_t> active_module_addresses;
std::mutex active_modules_mutex;
void poll_status_handler() override;
size_t remove_expired_telemetry_entries();
// Helper methods to reduce code duplication in packet handling
void handle_module_count_packet(const std::vector<uint8_t>& payload);
void handle_simple_telemetry_update(uint8_t source_address, const std::vector<uint8_t>& payload,
uint8_t command_number);
void check_and_signal_error_status_change(uint8_t source_address,
const can_packet_acdc::PowerModuleStatus& new_status,
const can_packet_acdc::PowerModuleStatus& old_status);
// Helper for standardized module identification in logging
std::string format_module_id(uint8_t address, const std::string& serial_number = "") const;
// Private implementation for template methods
bool send_command_impl(uint8_t destination_address, uint8_t command_number, const std::vector<uint8_t>& payload,
bool group = false);
void enqueue_poll_command(uint8_t destination_address, uint8_t command_number, const std::vector<uint8_t>& payload,
bool group = false);
};
#endif // INFY_CAN_DEVICE_HPP