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:
@@ -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 50–200 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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user