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,32 @@
|
||||
#
|
||||
# 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
|
||||
"can_driver_acdc/CanDevice.cpp"
|
||||
"can_driver_acdc/HwCanDevice.cpp"
|
||||
"can_driver_acdc/CanPackets.cpp"
|
||||
)
|
||||
|
||||
target_link_libraries(${MODULE_NAME}
|
||||
PRIVATE
|
||||
Pal::Sigslot
|
||||
)
|
||||
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"main/power_supply_DCImpl.cpp"
|
||||
)
|
||||
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
# insert other things like install cmds etc here
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
@@ -0,0 +1,46 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "Huawei_R100040Gx.hpp"
|
||||
#include <regex>
|
||||
|
||||
namespace module {
|
||||
|
||||
static std::vector<std::string> split_by_delimeters(const std::string& s, const std::string& delimeters) {
|
||||
std::regex re("[" + delimeters + "]");
|
||||
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_delimeters(a, ",");
|
||||
for (const auto& ad : adr) {
|
||||
addresses.push_back(std::stoi(ad));
|
||||
}
|
||||
return addresses;
|
||||
}
|
||||
|
||||
void Huawei_R100040Gx::init() {
|
||||
invoke_init(*p_main);
|
||||
|
||||
// open DCDC CAN device
|
||||
acdc.open_device(config.can_device);
|
||||
|
||||
// configure module address
|
||||
if (not config.module_addresses.empty()) {
|
||||
auto module_addresses = parse_module_addresses(config.module_addresses);
|
||||
EVLOG_info << "Amount of modules: " << module_addresses.size();
|
||||
acdc.set_module_address(module_addresses);
|
||||
} else {
|
||||
acdc.set_module_autodetection();
|
||||
}
|
||||
}
|
||||
|
||||
void Huawei_R100040Gx::ready() {
|
||||
invoke_ready(*p_main);
|
||||
acdc.run();
|
||||
acdc.switch_on(false);
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
@@ -0,0 +1,63 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef HUAWEI_R100040GX_HPP
|
||||
#define HUAWEI_R100040GX_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 2
|
||||
//
|
||||
|
||||
#include "ld-ev.hpp"
|
||||
|
||||
// headers for provided interface implementations
|
||||
#include <generated/interfaces/power_supply_DC/Implementation.hpp>
|
||||
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
#include "can_driver_acdc/HwCanDevice.hpp"
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
|
||||
namespace module {
|
||||
|
||||
struct Conf {
|
||||
std::string can_device;
|
||||
std::string module_addresses;
|
||||
int max_export_current_A;
|
||||
int max_export_power_W;
|
||||
};
|
||||
|
||||
class Huawei_R100040Gx : public Everest::ModuleBase {
|
||||
public:
|
||||
Huawei_R100040Gx() = delete;
|
||||
Huawei_R100040Gx(const ModuleInfo& info, std::unique_ptr<power_supply_DCImplBase> p_main, Conf& config) :
|
||||
ModuleBase(info), p_main(std::move(p_main)), config(config){};
|
||||
|
||||
const std::unique_ptr<power_supply_DCImplBase> p_main;
|
||||
const Conf& config;
|
||||
|
||||
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
|
||||
HwCanDevice acdc;
|
||||
// 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 // HUAWEI_R100040GX_HPP
|
||||
@@ -0,0 +1,157 @@
|
||||
// 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 <fcntl.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 "CanDevice.hpp"
|
||||
|
||||
CanDevice::CanDevice() : exit_rx_thread{false} {
|
||||
can_fd = 0;
|
||||
}
|
||||
|
||||
CanDevice::~CanDevice() {
|
||||
close_device();
|
||||
}
|
||||
|
||||
void CanDevice::open_device(const std::string& dev) {
|
||||
can_device = dev;
|
||||
try_open_device_internal();
|
||||
// spawn read thread
|
||||
exit_rx_thread = false;
|
||||
rx_thread_handle = std::thread(&CanDevice::rx_thread, this);
|
||||
}
|
||||
|
||||
bool CanDevice::try_open_device_internal() {
|
||||
|
||||
if (is_open) {
|
||||
return false;
|
||||
}
|
||||
|
||||
is_open = false;
|
||||
if ((can_fd = socket(PF_CAN, SOCK_RAW, CAN_RAW)) < 0) {
|
||||
return false;
|
||||
|
||||
} else {
|
||||
|
||||
// retrieve interface index from interface name
|
||||
struct ifreq ifr;
|
||||
strcpy(ifr.ifr_name, can_device.c_str());
|
||||
if (ioctl(can_fd, SIOCGIFINDEX, &ifr) < 0) {
|
||||
close(can_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
// bind to the interface
|
||||
struct sockaddr_can addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.can_family = AF_CAN;
|
||||
addr.can_ifindex = ifr.ifr_ifindex;
|
||||
|
||||
if (bind(can_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||
close(can_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
// set non blocking
|
||||
int v = fcntl(can_fd, F_GETFD, 0);
|
||||
fcntl(can_fd, F_SETFD, v | O_NONBLOCK);
|
||||
|
||||
is_open = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool CanDevice::close_device() {
|
||||
is_open = false;
|
||||
if (can_fd != 0 && close(can_fd) == 0) {
|
||||
can_fd = 0;
|
||||
exit_rx_thread = true;
|
||||
rx_thread_handle.join();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void CanDevice::close_device_internal() {
|
||||
is_open = false;
|
||||
if (can_fd != 0 && close(can_fd) == 0) {
|
||||
can_fd = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void CanDevice::rx_thread() {
|
||||
can_frame frame;
|
||||
|
||||
while (!exit_rx_thread) {
|
||||
if (is_open) {
|
||||
/* Read non-blocking from CAN Bus.
|
||||
Note that an implementation with select/poll seems to be unreliable on socket CAN.
|
||||
Sometimes select() returns 0 even though there are bytes to read. If that happens, it will never return
|
||||
anything but 0 again. Sometimes it returns 1, but the subsequent read blocks (even in non blocking mode).
|
||||
Maybe this can be fixed by adjusting some buffering in ioctl, but no working solution has been found.
|
||||
*/
|
||||
size_t nbytes = read(can_fd, &frame, sizeof(struct can_frame));
|
||||
|
||||
if (nbytes <= 0) {
|
||||
// If there is nothing to read, give it some time before we try again.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
} else if (nbytes > 0) {
|
||||
// Received a new CAN packet...
|
||||
std::vector<uint8_t> payload;
|
||||
payload.assign(frame.data, frame.data + frame.can_dlc);
|
||||
rx_handler(frame.can_id, payload);
|
||||
}
|
||||
} else {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CanDevice::rx_handler(uint32_t can_id, const std::vector<uint8_t>& payload) {
|
||||
std::cout << "CAN frame received" << std::endl;
|
||||
}
|
||||
|
||||
void CanDevice::connection_established() {
|
||||
std::cout << "Connection established" << std::endl;
|
||||
}
|
||||
|
||||
bool CanDevice::_tx(uint32_t can_id, const std::vector<uint8_t>& payload) {
|
||||
|
||||
try_open_device_internal();
|
||||
|
||||
if (not is_open) {
|
||||
return false;
|
||||
}
|
||||
|
||||
struct can_frame frame;
|
||||
if (payload.size() > sizeof(frame.data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
frame.can_id = can_id;
|
||||
frame.can_dlc = payload.size();
|
||||
memcpy(frame.data, payload.data(), payload.size());
|
||||
|
||||
if (write(can_fd, &frame, sizeof(can_frame)) != sizeof(can_frame)) {
|
||||
// On CAN, frames cannot be partially written. If we cannot write the packet, something is wrong with the bus.
|
||||
// Close device.
|
||||
close_device_internal();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef CAN_DEVICE_HPP
|
||||
#define CAN_DEVICE_HPP
|
||||
|
||||
#include "CanPackets.hpp"
|
||||
#include <atomic>
|
||||
#include <linux/can.h>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
class CanDevice {
|
||||
public:
|
||||
CanDevice();
|
||||
virtual ~CanDevice();
|
||||
void open_device(const std::string& dev);
|
||||
bool close_device();
|
||||
bool is_running() {
|
||||
return is_open;
|
||||
};
|
||||
|
||||
protected:
|
||||
virtual void rx_handler(uint32_t can_id, const std::vector<uint8_t>& payload);
|
||||
virtual void connection_established();
|
||||
bool _tx(uint32_t can_id, const std::vector<uint8_t>& payload);
|
||||
|
||||
private:
|
||||
bool try_open_device_internal();
|
||||
void close_device_internal();
|
||||
bool is_open{false};
|
||||
std::string can_device;
|
||||
int can_fd;
|
||||
std::atomic_bool exit_rx_thread;
|
||||
std::thread rx_thread_handle;
|
||||
void rx_thread();
|
||||
};
|
||||
|
||||
#endif // CAN_DEVICE_HPP
|
||||
@@ -0,0 +1,259 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "CanPackets.hpp"
|
||||
#include "Conversions.hpp"
|
||||
#include <iostream>
|
||||
|
||||
namespace Huawei {
|
||||
|
||||
static std::string to_string(MessageId m) {
|
||||
switch (m) {
|
||||
|
||||
case MessageId::Invalid:
|
||||
return "Invalid";
|
||||
case MessageId::ControlCommand:
|
||||
return "ControlCommand";
|
||||
case MessageId::ConfigurationCommand:
|
||||
return "ConfigurationCommand";
|
||||
case MessageId::QueryCommand:
|
||||
return "QueryCommand";
|
||||
case MessageId::ModuleReport:
|
||||
return "ModuleReport";
|
||||
case MessageId::QueryAllRealtimeData:
|
||||
return "QueryAllRealtimeData";
|
||||
case MessageId::QueryInherentModuleInformation:
|
||||
return "QueryInherentModuleInformation";
|
||||
case MessageId::ReceiveELabelFromMonitoringUnit:
|
||||
return "ReceiveELabelFromMonitoringUnit";
|
||||
case MessageId::ReturnELabelToMonitoringUnit:
|
||||
return "ReturnELabelToMonitoringUnit";
|
||||
case MessageId::OnlineLoadingStartup1:
|
||||
return "OnlineLoadingStartup1";
|
||||
case MessageId::OnlineLoadingStartup2:
|
||||
return "OnlineLoadingStartup2";
|
||||
case MessageId::FrameTransmission1:
|
||||
return "FrameTransmission1";
|
||||
case MessageId::FrameTransmission2:
|
||||
return "FrameTransmission2";
|
||||
case MessageId::FrameConfirmation1:
|
||||
return "FrameConfirmation1";
|
||||
case MessageId::FrameConfirmation2:
|
||||
return "FrameConfirmation2";
|
||||
case MessageId::DownloadEnd1:
|
||||
return "DownloadEnd1";
|
||||
case MessageId::DownloadEnd2:
|
||||
return "DownloadEnd2";
|
||||
case MessageId::LoadingFailure1:
|
||||
return "LoadingFailure1";
|
||||
case MessageId::LoadingFailure2:
|
||||
return "LoadingFailure2";
|
||||
case MessageId::CommonBlackBoxDataQuery:
|
||||
return "CommonBlackBoxDataQuery";
|
||||
}
|
||||
return "InvalidEnum/" + std::to_string(static_cast<uint16_t>(m));
|
||||
}
|
||||
|
||||
static std::string to_string(SignalId id) {
|
||||
switch (id) {
|
||||
case SignalId::NotImplemented:
|
||||
return "NotImplemented";
|
||||
case SignalId::FeatureField:
|
||||
return "FeatureField";
|
||||
case SignalId::SerialNumber:
|
||||
return "SerialNumber";
|
||||
case SignalId::ProductionInformation1:
|
||||
return "ProductionInformation1";
|
||||
case SignalId::ProductionInformation2:
|
||||
return "ProductionInformation2";
|
||||
case SignalId::SwHwVersion:
|
||||
return "SwHwVersion";
|
||||
case SignalId::HardwareAddr:
|
||||
return "HardwareAddr";
|
||||
case SignalId::BatchQuery:
|
||||
return "BatchQuery";
|
||||
case SignalId::VoltageRange:
|
||||
return "VoltageRange";
|
||||
case SignalId::FlowLimitingRange:
|
||||
return "FlowLimitingRange";
|
||||
case SignalId::OutputVoltage:
|
||||
return "OutputVoltage";
|
||||
case SignalId::DefaultVoltage:
|
||||
return "DefaultVoltage";
|
||||
case SignalId::OutputOVPThreshold:
|
||||
return "OutputOVPThreshold";
|
||||
case SignalId::OutputCurrentLimit:
|
||||
return "OutputCurrentLimit";
|
||||
case SignalId::DefaultOutputCurrentLimit:
|
||||
return "DefaultOutputCurrentLimit";
|
||||
case SignalId::OutputPowerLimit:
|
||||
return "OutputPowerLimit";
|
||||
case SignalId::DefaultOutputPowerLimit:
|
||||
return "DefaultOutputPowerLimit";
|
||||
case SignalId::SetOnOffVoltageCurrent:
|
||||
return "SetOnOffVoltageCurrent";
|
||||
case SignalId::RunningTime:
|
||||
return "RunningTime";
|
||||
case SignalId::OutputCurrent:
|
||||
return "OutputCurrent";
|
||||
case SignalId::DefaultOutputCurrent:
|
||||
return "DefaultOutputCurrent";
|
||||
case SignalId::PFCOnOff:
|
||||
return "PFCOnOff";
|
||||
case SignalId::FanDutyCycle:
|
||||
return "FanDutyCycle";
|
||||
case SignalId::RealTimeClock:
|
||||
return "RealTimeClock";
|
||||
case SignalId::CanCommunicationTimeout:
|
||||
return "CanCommunicationTimeout";
|
||||
case SignalId::GroupSettingClearingCmd:
|
||||
return "GroupSettingClearingCmd";
|
||||
case SignalId::TemporaryGroupSettingClearingCmd:
|
||||
return "TemporaryGroupSettingClearingCmd";
|
||||
case SignalId::EmergencyPowerOffControl:
|
||||
return "EmergencyPowerOffControl";
|
||||
case SignalId::PowerOnOff:
|
||||
return "PowerOnOff";
|
||||
case SignalId::ClearOutputOVP:
|
||||
return "ClearOutputOVP";
|
||||
case SignalId::FullFanSpeed:
|
||||
return "FullFanSpeed";
|
||||
case SignalId::ModuleSearch:
|
||||
return "ModuleSearch";
|
||||
case SignalId::DCDCOnOff:
|
||||
return "DCDCOnOff";
|
||||
case SignalId::AllocateSwAddresses:
|
||||
return "AllocateSwAddresses";
|
||||
case SignalId::UnassociatedOutputOVP:
|
||||
return "UnassociatedOutputOVP";
|
||||
case SignalId::ClearShortCircuit:
|
||||
return "ClearShortCircuit";
|
||||
case SignalId::DisableFlowEqualization:
|
||||
return "DisableFlowEqualization";
|
||||
case SignalId::OutputSilentMode:
|
||||
return "OutputSilentMode";
|
||||
case SignalId::ClearRectifierOutput:
|
||||
return "ClearRectifierOutput";
|
||||
case SignalId::AutomaticOutputModeSwitch:
|
||||
return "AutomaticOutputModeSwitch";
|
||||
case SignalId::DefaultOutputMode:
|
||||
return "DefaultOutputMode";
|
||||
case SignalId::CanBaudRate:
|
||||
return "CanBaudRate";
|
||||
case SignalId::ACInputPower:
|
||||
return "ACInputPower";
|
||||
case SignalId::ACInputFrequency:
|
||||
return "ACInputFrequency";
|
||||
case SignalId::ACInputCurrent:
|
||||
return "ACInputCurrent";
|
||||
case SignalId::DCOutputPower:
|
||||
return "DCOutputPower";
|
||||
case SignalId::RealTimeEfficiency:
|
||||
return "RealTimeEfficiency";
|
||||
case SignalId::OutputVoltageQuery:
|
||||
return "OutputVoltageQuery";
|
||||
case SignalId::ACInputVoltageUV:
|
||||
return "ACInputVoltageUV";
|
||||
case SignalId::ACInputVoltageVW:
|
||||
return "ACInputVoltageVW";
|
||||
case SignalId::ACInputVoltageWU:
|
||||
return "ACInputVoltageWU";
|
||||
case SignalId::PFCTemperature:
|
||||
return "PFCTemperature";
|
||||
case SignalId::InternalTemperature:
|
||||
return "InternalTemperature";
|
||||
case SignalId::TemperatureAirIntake:
|
||||
return "TemperatureAirIntake";
|
||||
case SignalId::AlarmStatus:
|
||||
return "AlarmStatus";
|
||||
case SignalId::RatedModuleCurrent:
|
||||
return "RatedModuleCurrent";
|
||||
case SignalId::AlarmInformation:
|
||||
return "AlarmInformation";
|
||||
case SignalId::OutputCurrent1:
|
||||
return "OutputCurrent1";
|
||||
case SignalId::OutputCurrent2:
|
||||
return "OutputCurrent2";
|
||||
case SignalId::OutputVoltageCurrentStatus:
|
||||
return "OutputVoltageCurrentStatus";
|
||||
}
|
||||
return "InvalidEnum/" + std::to_string(static_cast<uint16_t>(id));
|
||||
}
|
||||
|
||||
static std::string to_string(ErrorType e) {
|
||||
switch (e) {
|
||||
|
||||
case ErrorType::Success:
|
||||
return "Success";
|
||||
case ErrorType::InvalidCommand:
|
||||
return "InvalidCommand";
|
||||
case ErrorType::AddressIdentificationInProgress:
|
||||
return "AddressIdentificationInProgress";
|
||||
}
|
||||
return "InvalidEnum/" + std::to_string(static_cast<uint16_t>(e));
|
||||
}
|
||||
|
||||
// encode the 29 bits ID field
|
||||
uint32_t encode_can_id(uint8_t module_address, MessageId message_id) {
|
||||
constexpr uint8_t PROTOCOL_ID = 0x0D;
|
||||
|
||||
uint32_t id = PROTOCOL_ID << 23; // constant protocol id bits 23..28 (6 bits)
|
||||
|
||||
uint32_t m_addr = module_address & 0x7F; // ensure it is only 7 bits long
|
||||
id |= (m_addr << 16); // Module address is bits 16..22 (7 bits long)
|
||||
|
||||
id |= (static_cast<std::underlying_type<MessageId>::type>(message_id)
|
||||
<< 8); // Command/Message id is bits 8..15 (8 bits long)
|
||||
|
||||
id |= (1 << 7); // Source is control unit (bit 7)
|
||||
|
||||
id |= (0x1F << 2); // Group ID: default is all bits 1 (no grouping). Bits 2..6 (5 bits long)
|
||||
|
||||
// Bit 1: Set to 0 to use hardware address
|
||||
// Bit 0: Set to 0 there is no further data frame
|
||||
return id;
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& out, const Packet& self) {
|
||||
out << "Huawei " << (self.packet_source_control_unit ? "H->M" : "M->H")
|
||||
<< " Addr: " << std::to_string(self.module_address) << " MsgId: " << to_string(self.message_id)
|
||||
<< " ErrorType: " << to_string(self.error_type) << " SignalId: " << to_string(self.signal_id)
|
||||
<< " Byte2/3: " << std::to_string(self.byte2) << "/" << std::to_string(self.byte3)
|
||||
<< " Data: " << std::to_string(self.data) << (self.last_packet ? "" : " [+]");
|
||||
return out;
|
||||
}
|
||||
|
||||
// Packs the packet as 8 byte binary
|
||||
Packet::operator std::vector<uint8_t>() {
|
||||
std::vector<uint8_t> out;
|
||||
// 4 bits ErrorType and 12 bits Signal ID
|
||||
to_raw(static_cast<uint8_t>(((static_cast<uint8_t>(error_type) & 0x0F) << 4) |
|
||||
((static_cast<uint16_t>(signal_id) & 0x0FFF) >> 8)),
|
||||
out);
|
||||
to_raw(static_cast<uint8_t>(((static_cast<uint16_t>(signal_id) & 0x00FF))), out);
|
||||
|
||||
to_raw(byte2, out); // 1 byte
|
||||
to_raw(byte3, out); // 1 byte
|
||||
to_raw(data, out); // 4 bytes of actual data
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Constructor to initialize from received raw packet
|
||||
Packet::Packet(const uint32_t can_id, const std::vector<uint8_t>& raw) {
|
||||
module_address = module_address_from_can_id(can_id);
|
||||
message_id = message_id_from_can_id(can_id);
|
||||
group_address = group_address_from_can_id(can_id);
|
||||
packet_source_control_unit = packet_source_from_can_id(can_id);
|
||||
protocol_id = protocol_id_from_can_id(can_id);
|
||||
last_packet = last_packet_from_can_id(can_id);
|
||||
signal_id = static_cast<SignalId>(static_cast<uint16_t>(((from_raw<uint8_t>(raw, 0) & 0x0F) << 8)) |
|
||||
(from_raw<uint8_t>(raw, 1)));
|
||||
error_type = static_cast<ErrorType>(from_raw<uint8_t>(raw, 0) >> 4);
|
||||
|
||||
byte2 = from_raw<uint8_t>(raw, 2);
|
||||
byte3 = from_raw<uint8_t>(raw, 3);
|
||||
data = from_raw<uint32_t>(raw, 4);
|
||||
}
|
||||
|
||||
} // namespace Huawei
|
||||
@@ -0,0 +1,208 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef CAN_PACKETS_HPP
|
||||
#define CAN_PACKETS_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <linux/can.h>
|
||||
#include <ostream>
|
||||
#include <vector>
|
||||
|
||||
namespace Huawei {
|
||||
|
||||
constexpr uint32_t RTQ_RUNNING_TIME{1 << 0};
|
||||
constexpr uint32_t RTQ_AC_INPUT_POWER{1 << 1};
|
||||
constexpr uint32_t RTQ_AC_INPUT_FREQ{1 << 2};
|
||||
constexpr uint32_t RTQ_AC_INPUT_CURRENT{1 << 3};
|
||||
constexpr uint32_t RTQ_DC_OUTPUT_POWER{1 << 4};
|
||||
constexpr uint32_t RTQ_EFFICIENCY{1 << 5};
|
||||
constexpr uint32_t RTQ_OUTPUT_VOLTAGE{1 << 6};
|
||||
constexpr uint32_t RTQ_DC_OUTPUT_CURRENT_LIMIT{1 << 7};
|
||||
// Bit 8 seems to be a bug in the documentation (same as bit 9)
|
||||
constexpr uint32_t RTQ_AC_INPUT_VOLTAGE_UV{1 << 9};
|
||||
constexpr uint32_t RTQ_AC_INPUT_VOLTAGE_VW{1 << 10};
|
||||
constexpr uint32_t RTQ_AC_INPUT_VOLTAGE_WU{1 << 11};
|
||||
constexpr uint32_t RTQ_INTERNAL_TEMPERATURE{1 << 12};
|
||||
constexpr uint32_t RTQ_TEMPERATURE_AIR_INTAKE{1 << 13};
|
||||
constexpr uint32_t RTQ_OUTPUT_CURRENT_1{1 << 14};
|
||||
constexpr uint32_t RTQ_OUTPUT_CURRENT_2{1 << 15};
|
||||
constexpr uint32_t RTQ_ALARM_STATUS{1 << 16};
|
||||
constexpr uint32_t RTQ_DC_OUTPUT_VOLTAGE_SETPOINT{1 << 17};
|
||||
constexpr uint32_t RTQ_DC_OUTPUT_CURRENT_PERCENTAGE{1 << 18};
|
||||
constexpr uint32_t RTQ_DC_OUTPUT_CURRENT_SETPOINT{1 << 19};
|
||||
constexpr uint32_t RTQ_ALARM_INFORMATION{1 << 20};
|
||||
constexpr uint32_t RTQ_SETTING_QUERY{1 << 21};
|
||||
constexpr uint32_t RTQ_OUTPUT_VOLTAGE_CURRENT_STATUS{1 << 22};
|
||||
constexpr uint32_t RTQ_ACTUAL_OUTPUT_POWER_LIMITING{1 << 23};
|
||||
|
||||
enum class MessageId : uint8_t {
|
||||
Invalid = 0x00,
|
||||
ControlCommand = 0x80,
|
||||
ConfigurationCommand = 0x81,
|
||||
QueryCommand = 0x82,
|
||||
ModuleReport = 0x83,
|
||||
QueryAllRealtimeData = 0x40,
|
||||
QueryInherentModuleInformation = 0x50,
|
||||
ReceiveELabelFromMonitoringUnit = 0xD1,
|
||||
ReturnELabelToMonitoringUnit = 0xD2,
|
||||
OnlineLoadingStartup1 = 0xD3,
|
||||
OnlineLoadingStartup2 = 0xE3,
|
||||
FrameTransmission1 = 0xD4,
|
||||
FrameTransmission2 = 0xE4,
|
||||
FrameConfirmation1 = 0xD5,
|
||||
FrameConfirmation2 = 0xE5,
|
||||
DownloadEnd1 = 0xD6,
|
||||
DownloadEnd2 = 0xE6,
|
||||
LoadingFailure1 = 0xD7,
|
||||
LoadingFailure2 = 0xE7,
|
||||
CommonBlackBoxDataQuery = 0xDA,
|
||||
};
|
||||
|
||||
// encode the 29 bits ID field
|
||||
uint32_t encode_can_id(uint8_t module_address, MessageId message_id);
|
||||
|
||||
inline uint8_t module_address_from_can_id(const uint32_t id) {
|
||||
return (id >> 16) & 0x7F;
|
||||
};
|
||||
|
||||
inline uint8_t group_address_from_can_id(const uint32_t id) {
|
||||
return (id >> 2) & 0x1F;
|
||||
};
|
||||
|
||||
inline bool packet_source_from_can_id(const uint32_t id) {
|
||||
return ((id >> 7) & 0x01) == 0x01;
|
||||
};
|
||||
|
||||
inline bool last_packet_from_can_id(const uint32_t id) {
|
||||
return (id & 0x01) == 0x00;
|
||||
};
|
||||
|
||||
inline uint8_t protocol_id_from_can_id(const uint32_t id) {
|
||||
return ((id >> 23) & 0x3F);
|
||||
};
|
||||
|
||||
inline MessageId message_id_from_can_id(const uint32_t id) {
|
||||
const uint8_t m_id = (id >> 8) & 0xFF;
|
||||
return static_cast<MessageId>(m_id);
|
||||
};
|
||||
|
||||
enum class ErrorType : uint8_t {
|
||||
Success = 0x00,
|
||||
InvalidCommand = 0x02,
|
||||
AddressIdentificationInProgress = 0x03,
|
||||
};
|
||||
|
||||
enum class SignalId : uint16_t {
|
||||
NotImplemented = 0xFFFF,
|
||||
BatchQuery = 0x000,
|
||||
FeatureField = 0x001,
|
||||
SerialNumber = 0x002,
|
||||
ProductionInformation1 = 0x003,
|
||||
ProductionInformation2 = 0x004,
|
||||
SwHwVersion = 0x005,
|
||||
HardwareAddr = 0x006,
|
||||
VoltageRange = 0x009,
|
||||
FlowLimitingRange = 0x00A,
|
||||
OutputVoltage = 0x100,
|
||||
DefaultVoltage = 0x101,
|
||||
OutputOVPThreshold = 0x102,
|
||||
OutputCurrentLimit = 0x103,
|
||||
DefaultOutputCurrentLimit = 0x104,
|
||||
OutputPowerLimit = 0x105,
|
||||
DefaultOutputPowerLimit = 0x106,
|
||||
SetOnOffVoltageCurrent = 0x108,
|
||||
RunningTime = 0x10E,
|
||||
OutputCurrent = 0x10F,
|
||||
DefaultOutputCurrent = 0x110,
|
||||
PFCOnOff = 0x111,
|
||||
FanDutyCycle = 0x114,
|
||||
RealTimeClock = 0x117,
|
||||
CanCommunicationTimeout = 0x118,
|
||||
GroupSettingClearingCmd = 0x119,
|
||||
TemporaryGroupSettingClearingCmd = 0x11A,
|
||||
EmergencyPowerOffControl = 0x131,
|
||||
PowerOnOff = 0x132,
|
||||
ClearOutputOVP = 0x133,
|
||||
FullFanSpeed = 0x134,
|
||||
ModuleSearch = 0x135,
|
||||
DCDCOnOff = 0x136,
|
||||
AllocateSwAddresses = 0x13A,
|
||||
UnassociatedOutputOVP = 0x13C,
|
||||
ClearShortCircuit = 0x145,
|
||||
DisableFlowEqualization = 0x146,
|
||||
OutputSilentMode = 0x148,
|
||||
ClearRectifierOutput = 0x149,
|
||||
AutomaticOutputModeSwitch = 0x14A,
|
||||
DefaultOutputMode = 0x14C,
|
||||
CanBaudRate = 0x14D,
|
||||
ACInputPower = 0x170,
|
||||
ACInputFrequency = 0x171,
|
||||
ACInputCurrent = 0x172,
|
||||
DCOutputPower = 0x173,
|
||||
RealTimeEfficiency = 0x174,
|
||||
OutputVoltageQuery = 0x175,
|
||||
ACInputVoltageUV = 0x179,
|
||||
ACInputVoltageVW = 0x17A,
|
||||
ACInputVoltageWU = 0x17B,
|
||||
PFCTemperature = 0x17E,
|
||||
InternalTemperature = 0x17F,
|
||||
TemperatureAirIntake = 0x180,
|
||||
OutputCurrent1 = 0x181,
|
||||
OutputCurrent2 = 0x182,
|
||||
AlarmStatus = 0x183,
|
||||
RatedModuleCurrent = 0x188,
|
||||
AlarmInformation = 0x18F,
|
||||
OutputVoltageCurrentStatus = 0x191,
|
||||
};
|
||||
|
||||
// Addresses
|
||||
const uint8_t ADDR_BROADCAST = 0x00;
|
||||
|
||||
struct Packet {
|
||||
// Constructor for Packets to be sent to modules
|
||||
Packet(MessageId _message_id, SignalId _signal_id, uint8_t _byte2, uint8_t _byte3, uint32_t _data) :
|
||||
message_id(_message_id),
|
||||
signal_id(_signal_id),
|
||||
byte2(_byte2),
|
||||
byte3(_byte3),
|
||||
data(_data),
|
||||
packet_source_control_unit(true){};
|
||||
|
||||
// Constructor for simple requests to module
|
||||
Packet(MessageId _message_id, SignalId _signal_id) :
|
||||
message_id(_message_id), signal_id(_signal_id), packet_source_control_unit(true){};
|
||||
|
||||
// Constructor to initialize from received raw packet
|
||||
Packet(const uint32_t can_id, const std::vector<uint8_t>& raw);
|
||||
friend std::ostream& operator<<(std::ostream& out, const Packet& self);
|
||||
operator std::vector<uint8_t>();
|
||||
|
||||
void set_module_address(uint8_t address) {
|
||||
module_address = address;
|
||||
};
|
||||
|
||||
uint32_t get_can_id() {
|
||||
return encode_can_id(module_address, message_id);
|
||||
};
|
||||
|
||||
// Part of CAN ID field
|
||||
uint8_t protocol_id{0x0D};
|
||||
uint8_t module_address{0};
|
||||
uint8_t group_address{0};
|
||||
MessageId message_id{MessageId::Invalid};
|
||||
|
||||
// In CAN payload bytes
|
||||
ErrorType error_type{ErrorType::Success};
|
||||
uint8_t byte2{0};
|
||||
uint8_t byte3{0};
|
||||
uint32_t data{0};
|
||||
SignalId signal_id{SignalId::NotImplemented};
|
||||
bool packet_source_control_unit{false};
|
||||
|
||||
bool last_packet{true};
|
||||
};
|
||||
|
||||
} // namespace Huawei
|
||||
|
||||
#endif // CAN_PACKETS_HPP
|
||||
@@ -0,0 +1,73 @@
|
||||
// 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 <type_traits>
|
||||
|
||||
#include <endian.h>
|
||||
|
||||
template <class T> typename std::enable_if_t<sizeof(T) == 1, T> from_raw(const std::vector<uint8_t> raw, int idx) {
|
||||
T ret = raw[idx];
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <class T> typename std::enable_if_t<sizeof(T) == 2, T> from_raw(const std::vector<uint8_t> raw, int idx) {
|
||||
uint16_t tmp = be16toh(*((uint16_t*)((uint8_t*)raw.data() + idx)));
|
||||
T ret;
|
||||
memcpy(&ret, &tmp, 2);
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <class T> typename std::enable_if_t<sizeof(T) == 4, T> from_raw(const std::vector<uint8_t> raw, int idx) {
|
||||
uint32_t tmp = be32toh(*((uint32_t*)((uint8_t*)raw.data() + idx)));
|
||||
T ret;
|
||||
memcpy(&ret, &tmp, 4);
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <class T> typename std::enable_if_t<sizeof(T) == 8, T> from_raw(const std::vector<uint8_t> raw, int idx) {
|
||||
uint64_t tmp = be64toh(*((uint64_t*)((uint8_t*)raw.data() + idx)));
|
||||
T ret;
|
||||
memcpy(&ret, &tmp, 8);
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <class T> typename std::enable_if_t<sizeof(T) == 1> to_raw(T src, std::vector<uint8_t>& dest) {
|
||||
uint8_t tmp;
|
||||
memcpy(&tmp, &src, 1);
|
||||
dest.push_back(tmp);
|
||||
}
|
||||
|
||||
// FIXME (aw): these conversions should be optimized!
|
||||
template <class T> typename std::enable_if_t<sizeof(T) == 2> to_raw(T src, std::vector<uint8_t>& dest) {
|
||||
uint16_t tmp;
|
||||
memcpy(&tmp, &src, 2);
|
||||
tmp = htobe16(tmp);
|
||||
uint8_t ret[2];
|
||||
memcpy(ret, &tmp, 2);
|
||||
dest.insert(dest.end(), {ret[0], ret[1]});
|
||||
}
|
||||
|
||||
template <class T> typename std::enable_if_t<sizeof(T) == 4> to_raw(T src, std::vector<uint8_t>& dest) {
|
||||
uint32_t tmp;
|
||||
memcpy(&tmp, &src, 4);
|
||||
tmp = htobe32(tmp);
|
||||
uint8_t ret[4];
|
||||
memcpy(ret, &tmp, 4);
|
||||
dest.insert(dest.end(), {ret[0], ret[1], ret[2], ret[3]});
|
||||
}
|
||||
|
||||
template <class T> typename std::enable_if_t<sizeof(T) == 8> to_raw(T src, std::vector<uint8_t>& dest) {
|
||||
uint64_t tmp;
|
||||
memcpy(&tmp, &src, 8);
|
||||
tmp = htobe64(tmp);
|
||||
uint8_t ret[8];
|
||||
memcpy(ret, &tmp, 8);
|
||||
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,298 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "HwCanDevice.hpp"
|
||||
#include "CanPackets.hpp"
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
HwCanDevice::~HwCanDevice() {
|
||||
exit_tx_thread = true;
|
||||
}
|
||||
|
||||
bool HwCanDevice::switch_on(bool on) {
|
||||
// actual switching on will be handled in tx thread
|
||||
requested_on = on;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HwCanDevice::switch_on_nolock(bool on) {
|
||||
// Power On/Off Packet
|
||||
Huawei::Packet power_on_off(Huawei::MessageId::ControlCommand, Huawei::SignalId::PowerOnOff);
|
||||
power_on_off.byte3 = not on;
|
||||
send_to_all_modules(power_on_off);
|
||||
is_on = on;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HwCanDevice::set_voltage_current(float voltage, float current) {
|
||||
requested_set_point_voltage = voltage;
|
||||
requested_set_point_current = current;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HwCanDevice::set_voltage_current_nolock(float voltage, float current) {
|
||||
Huawei::Packet set_voltage(Huawei::MessageId::ControlCommand, Huawei::SignalId::OutputVoltage);
|
||||
set_voltage.data = voltage * 1024;
|
||||
send_to_all_modules(set_voltage);
|
||||
|
||||
Huawei::Packet set_current(Huawei::MessageId::ControlCommand, Huawei::SignalId::OutputCurrent);
|
||||
set_current.data = current * 1024 / module_addresses.size();
|
||||
set_current.byte3 = module_addresses.size();
|
||||
send_to_all_modules(set_current);
|
||||
|
||||
set_point_voltage = voltage;
|
||||
set_point_current = current;
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::string to_serial_number(uint8_t b2, uint8_t b3, uint8_t data) {
|
||||
return fmt::format("{}:{}:{}", b2, b3, data);
|
||||
}
|
||||
|
||||
static std::string to_version_string(uint8_t b2, uint8_t b3, uint32_t data) {
|
||||
uint16_t hw_version = b2 << 8 | b3;
|
||||
|
||||
int dcdc_versoion_hi = data >> 24;
|
||||
int dcdc_versoion_lo = data >> 16 & 0xFF;
|
||||
|
||||
int pfc_versoion_hi = data >> 8 & 0xFF;
|
||||
int pfc_versoion_lo = data & 0xFF;
|
||||
return fmt::format("HW Version: {} | DCDC: {}.{} | PFC: {}.{}", hw_version, dcdc_versoion_hi, dcdc_versoion_lo,
|
||||
pfc_versoion_hi, pfc_versoion_lo);
|
||||
}
|
||||
|
||||
static bool address_already_in_list(std::vector<uint8_t>& v, uint8_t nv) {
|
||||
auto it = std::find(v.begin(), v.end(), nv);
|
||||
if (it == v.end()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void HwCanDevice::connection_established() {
|
||||
switch_on(false);
|
||||
request_module_info();
|
||||
}
|
||||
|
||||
void HwCanDevice::rx_handler(uint32_t can_id, const std::vector<uint8_t>& payload) {
|
||||
// We only use extended frames here
|
||||
if (!(can_id & CAN_EFF_FLAG)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// parse packet
|
||||
auto packet = Huawei::Packet(can_id, payload);
|
||||
|
||||
if (packet.packet_source_control_unit or packet.protocol_id not_eq 0x0D) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet.error_type not_eq Huawei::ErrorType::Success) {
|
||||
std::cout << "Error in CAN packet: " << packet << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// We received a packet from the PSU, reset timeout timer
|
||||
last_communication_received = std::chrono::steady_clock::now();
|
||||
|
||||
uint8_t source_address = packet.module_address;
|
||||
bool packet_handled = true;
|
||||
|
||||
if (packet.message_id == Huawei::MessageId::QueryInherentModuleInformation) {
|
||||
switch (packet.signal_id) {
|
||||
case Huawei::SignalId::SerialNumber:
|
||||
if (module_auto_detection) {
|
||||
module_serial_numbers[packet.module_address] =
|
||||
"[auto detected] " + to_serial_number(packet.byte2, packet.byte3, packet.data);
|
||||
|
||||
if (not address_already_in_list(module_addresses, source_address)) {
|
||||
// Found a new module? Add it to the list
|
||||
module_addresses.push_back(source_address);
|
||||
// Update capabilities
|
||||
auto caps = get_capabilities();
|
||||
signal_capabilities(caps);
|
||||
}
|
||||
|
||||
} else {
|
||||
module_serial_numbers[packet.module_address] =
|
||||
to_serial_number(packet.byte2, packet.byte3, packet.data);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Huawei::SignalId::SwHwVersion:
|
||||
if (not address_already_in_list(module_addresses_reported, source_address)) {
|
||||
signal_serial_number(packet.module_address,
|
||||
module_serial_numbers[packet.module_address] + " | " +
|
||||
to_version_string(packet.byte2, packet.byte3, packet.data));
|
||||
module_addresses_reported.push_back(source_address);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Ignore all other Inherent Module Information packages
|
||||
packet_handled = true;
|
||||
break;
|
||||
}
|
||||
} else if (packet.message_id == Huawei::MessageId::QueryAllRealtimeData) {
|
||||
switch (packet.signal_id) {
|
||||
case Huawei::SignalId::OutputVoltageCurrentStatus: {
|
||||
const float v = (packet.data & 0x0000FFFF) / 10.;
|
||||
const float c = ((packet.data & 0xFFFF0000) >> 16) / 10.;
|
||||
|
||||
// report sum of all currents
|
||||
telemetries[source_address].current = c;
|
||||
|
||||
// calculate total current
|
||||
total_current = 0.;
|
||||
for (const auto& t : telemetries) {
|
||||
total_current += t.second.current;
|
||||
}
|
||||
|
||||
telemetries[source_address].voltage = v;
|
||||
// report average voltage
|
||||
float voltage = 0.;
|
||||
for (const auto& t : telemetries) {
|
||||
voltage += t.second.voltage;
|
||||
}
|
||||
voltage /= telemetries.size();
|
||||
|
||||
signal_voltage_current(voltage, total_current);
|
||||
} break;
|
||||
default:
|
||||
packet_handled = false;
|
||||
break;
|
||||
}
|
||||
} else if (packet.message_id == Huawei::MessageId::ConfigurationCommand) {
|
||||
// Ignore all config command replies for now
|
||||
packet_handled = true;
|
||||
} else if (packet.message_id == Huawei::MessageId::ControlCommand) {
|
||||
// Ignore all control command replies for now
|
||||
packet_handled = true;
|
||||
} else {
|
||||
packet_handled = false;
|
||||
}
|
||||
|
||||
if (not packet_handled) {
|
||||
std::cout << "UNHANDLED Packet received: " << packet << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
types::power_supply_DC::Capabilities HwCanDevice::get_capabilities() {
|
||||
|
||||
// IMPROVE ME: this could be queried from the power supply instead
|
||||
types::power_supply_DC::Capabilities caps;
|
||||
caps.bidirectional = false;
|
||||
|
||||
caps.current_regulation_tolerance_A = 1;
|
||||
caps.peak_current_ripple_A = 0.5;
|
||||
|
||||
caps.min_export_current_A = 0;
|
||||
caps.max_export_current_A = 133 * module_addresses.size();
|
||||
caps.min_export_voltage_V = 150;
|
||||
caps.max_export_voltage_V = 1000;
|
||||
caps.max_export_power_W = 40000 * module_addresses.size();
|
||||
|
||||
caps.max_import_current_A = 0;
|
||||
caps.min_import_current_A = 0;
|
||||
caps.max_import_power_W = 0;
|
||||
caps.min_import_voltage_V = 0;
|
||||
caps.max_import_voltage_V = 0;
|
||||
|
||||
return caps;
|
||||
}
|
||||
|
||||
void HwCanDevice::request_module_info() {
|
||||
// Request information about modules once
|
||||
send_to_broadcast({Huawei::MessageId::QueryInherentModuleInformation, Huawei::SignalId::BatchQuery});
|
||||
}
|
||||
|
||||
void HwCanDevice::set_mode() {
|
||||
// Set mode to automatic switching
|
||||
Huawei::Packet mode(Huawei::MessageId::ConfigurationCommand, Huawei::SignalId::AutomaticOutputModeSwitch);
|
||||
mode.byte3 = 0x01; // switch according to actual voltage on the terminals
|
||||
send_to_all_modules(mode);
|
||||
}
|
||||
|
||||
void HwCanDevice::tx_thread() {
|
||||
|
||||
Huawei::Packet query_rt_data(Huawei::MessageId::QueryAllRealtimeData, Huawei::SignalId::BatchQuery);
|
||||
query_rt_data.byte3 = 0xAA; // Enable bit configuration of individual messages
|
||||
query_rt_data.data = Huawei::RTQ_OUTPUT_VOLTAGE_CURRENT_STATUS;
|
||||
|
||||
uint8_t telemetry_cnt = 0;
|
||||
|
||||
bool comm_timeout_err = false;
|
||||
bool comm_bus_err = false;
|
||||
|
||||
bool last_err_present = false;
|
||||
|
||||
set_mode();
|
||||
|
||||
while (!exit_tx_thread) {
|
||||
{
|
||||
// Is there a communication problem in the CAN stack?
|
||||
comm_bus_err = not is_running();
|
||||
|
||||
// Did we receive something from the PSU within timeout?
|
||||
// I.e. the CAN stack is working but the hardware does not send anything
|
||||
|
||||
comm_timeout_err = std::chrono::steady_clock::now() - last_communication_received > COMMS_TIMEOUT;
|
||||
|
||||
bool err_present = comm_bus_err or comm_timeout_err;
|
||||
|
||||
if (err_present not_eq last_err_present) {
|
||||
signal_communication_error(err_present,
|
||||
(comm_bus_err ? "CAN Bus error: No such device or wrong settings"
|
||||
: "Power supply does not respond on CAN bus"));
|
||||
}
|
||||
last_err_present = err_present;
|
||||
|
||||
// Request all real time data (telemetry) as configured above
|
||||
if (telemetry_cnt++ % 5 == 0) {
|
||||
send_to_all_modules(query_rt_data);
|
||||
}
|
||||
|
||||
// Configure mode roughly every second
|
||||
if (not is_on and telemetry_cnt++ % 20 == 0) {
|
||||
set_mode();
|
||||
}
|
||||
|
||||
// Read serial numbers every few seconds
|
||||
if (telemetry_cnt++ % 40 == 0) {
|
||||
request_module_info();
|
||||
}
|
||||
|
||||
// Do we need to switch on or off?
|
||||
if (last_requested_on not_eq requested_on) {
|
||||
last_requested_on = requested_on;
|
||||
switch_on_nolock(requested_on);
|
||||
signal_on_off(requested_on);
|
||||
set_voltage_current_nolock(requested_set_point_voltage, requested_set_point_current);
|
||||
}
|
||||
|
||||
// Do we need to set voltage/current limits?
|
||||
if (requested_set_point_voltage not_eq set_point_voltage or
|
||||
requested_set_point_current not_eq set_point_current) {
|
||||
set_voltage_current_nolock(requested_set_point_voltage, requested_set_point_current);
|
||||
}
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
}
|
||||
}
|
||||
|
||||
bool HwCanDevice::tx(Huawei::Packet& packet) {
|
||||
uint32_t can_id = packet.get_can_id();
|
||||
can_id |= 0x80000000U; // Extended frame format
|
||||
return _tx(can_id, packet);
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& out, const HwCanDevice::Telemetry& self) {
|
||||
out << "DC output: " << std::to_string(self.voltage) << "V / " << std::to_string(self.current) << "A" << std::endl;
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// 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 "CanDevice.hpp"
|
||||
#include <atomic>
|
||||
#include <linux/can.h>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <sigslot/signal.hpp>
|
||||
#include <thread>
|
||||
|
||||
#include <generated/interfaces/power_supply_DC/Implementation.hpp>
|
||||
|
||||
class HwCanDevice : public CanDevice {
|
||||
public:
|
||||
HwCanDevice(){
|
||||
|
||||
};
|
||||
|
||||
~HwCanDevice();
|
||||
|
||||
void run() {
|
||||
// spawn thread that requests some data periodically to keep the connection alive
|
||||
last_communication_received = std::chrono::steady_clock::now();
|
||||
exit_tx_thread = false;
|
||||
tx_thread_handle = std::thread(&HwCanDevice::tx_thread, this);
|
||||
};
|
||||
|
||||
void set_module_address(const std::vector<uint8_t>& _module_addresses) {
|
||||
module_addresses = _module_addresses;
|
||||
// Update capabilities
|
||||
auto caps = get_capabilities();
|
||||
signal_capabilities(caps);
|
||||
};
|
||||
|
||||
void set_module_autodetection() {
|
||||
module_auto_detection = true;
|
||||
};
|
||||
|
||||
// Commands
|
||||
bool switch_on(bool on);
|
||||
bool set_voltage_current(float voltage, float current);
|
||||
|
||||
int get_number_of_modules() {
|
||||
return module_addresses.size();
|
||||
}
|
||||
|
||||
void request_module_info();
|
||||
|
||||
// Data out
|
||||
sigslot::signal<float, float> signal_voltage_current;
|
||||
sigslot::signal<bool> signal_on_off; // true: on, false: off
|
||||
sigslot::signal<int, const std::string&> signal_serial_number;
|
||||
sigslot::signal<const types::power_supply_DC::Capabilities&> signal_capabilities;
|
||||
sigslot::signal<bool, const std::string&>
|
||||
signal_communication_error; // true for error, false means error was cleared, error description
|
||||
|
||||
struct Telemetry {
|
||||
float voltage{0.};
|
||||
float current{0.};
|
||||
};
|
||||
|
||||
float total_current{0.};
|
||||
|
||||
std::map<uint8_t, Telemetry> telemetries;
|
||||
|
||||
friend std::ostream& operator<<(std::ostream& out, const Telemetry& self);
|
||||
|
||||
protected:
|
||||
virtual void rx_handler(uint32_t can_id, const std::vector<uint8_t>& payload);
|
||||
virtual void connection_established();
|
||||
|
||||
private:
|
||||
std::atomic_bool exit_tx_thread;
|
||||
std::thread tx_thread_handle;
|
||||
void tx_thread();
|
||||
|
||||
void set_mode();
|
||||
|
||||
bool tx(Huawei::Packet& packet);
|
||||
|
||||
types::power_supply_DC::Capabilities get_capabilities();
|
||||
|
||||
void send_to_all_modules(Huawei::Packet packet) {
|
||||
// Send to each module
|
||||
for (auto module_address : module_addresses) {
|
||||
packet.set_module_address(module_address);
|
||||
tx(packet);
|
||||
}
|
||||
};
|
||||
|
||||
void send_to_broadcast(Huawei::Packet packet) {
|
||||
packet.set_module_address(Huawei::ADDR_BROADCAST);
|
||||
tx(packet);
|
||||
};
|
||||
|
||||
std::vector<uint8_t> module_addresses{};
|
||||
std::vector<uint8_t> module_addresses_reported{};
|
||||
|
||||
bool switch_on_nolock(bool on);
|
||||
bool set_voltage_current_nolock(float voltage, float current);
|
||||
|
||||
std::atomic_bool module_auto_detection{false};
|
||||
|
||||
float set_point_voltage{0.};
|
||||
float set_point_current{0.};
|
||||
|
||||
std::atomic<float> requested_set_point_voltage{0.};
|
||||
std::atomic<float> requested_set_point_current{0.};
|
||||
|
||||
bool is_on{false};
|
||||
std::atomic_bool requested_on{false};
|
||||
bool last_requested_on{false};
|
||||
|
||||
std::chrono::time_point<std::chrono::steady_clock> last_communication_received;
|
||||
static constexpr std::chrono::seconds COMMS_TIMEOUT{2};
|
||||
|
||||
std::unordered_map<uint8_t, std::string> module_serial_numbers;
|
||||
};
|
||||
|
||||
#endif // INFY_CAN_DEVICE_HPP
|
||||
@@ -0,0 +1,119 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "power_supply_DCImpl.hpp"
|
||||
|
||||
namespace module {
|
||||
namespace main {
|
||||
|
||||
void power_supply_DCImpl::init() {
|
||||
|
||||
caps.bidirectional = false;
|
||||
|
||||
caps.current_regulation_tolerance_A = 1;
|
||||
caps.peak_current_ripple_A = 0.5;
|
||||
|
||||
caps.min_export_current_A = 0;
|
||||
caps.max_export_current_A = mod->config.max_export_current_A;
|
||||
caps.min_export_voltage_V = 150;
|
||||
caps.max_export_voltage_V = 1000;
|
||||
caps.max_export_power_W = mod->config.max_export_power_W;
|
||||
|
||||
caps.max_import_current_A = 0;
|
||||
caps.min_import_current_A = 0;
|
||||
caps.max_import_power_W = 0;
|
||||
caps.min_import_voltage_V = 0;
|
||||
caps.max_import_voltage_V = 0;
|
||||
|
||||
mod->acdc.signal_capabilities.connect([this](const types::power_supply_DC::Capabilities& c) {
|
||||
caps = c;
|
||||
// limit by config values
|
||||
if (caps.max_export_current_A > mod->config.max_export_current_A) {
|
||||
caps.max_export_current_A = mod->config.max_export_current_A;
|
||||
}
|
||||
if (caps.max_export_power_W > mod->config.max_export_power_W) {
|
||||
caps.max_export_power_W = mod->config.max_export_power_W;
|
||||
}
|
||||
publish_capabilities(caps);
|
||||
});
|
||||
|
||||
mod->acdc.signal_voltage_current.connect([this](float voltage, float current) {
|
||||
types::power_supply_DC::VoltageCurrent vc;
|
||||
vc.current_A = current;
|
||||
vc.voltage_V = voltage;
|
||||
publish_voltage_current(vc);
|
||||
});
|
||||
|
||||
mod->acdc.signal_serial_number.connect([this](int module_id, const std::string& serial_number) {
|
||||
EVLOG_info << "Module ID " << module_id << ": " << serial_number;
|
||||
});
|
||||
|
||||
mod->acdc.signal_on_off.connect([this](bool on) {
|
||||
if (on) {
|
||||
publish_mode(types::power_supply_DC::Mode::Import);
|
||||
} else {
|
||||
publish_mode(types::power_supply_DC::Mode::Off);
|
||||
}
|
||||
});
|
||||
|
||||
mod->acdc.signal_communication_error.connect([this](bool err, const std::string& desc) {
|
||||
if (err) {
|
||||
Everest::error::Error error_object = error_factory->create_error("power_supply_DC/CommunicationFault", "",
|
||||
desc, Everest::error::Severity::High);
|
||||
raise_error(error_object);
|
||||
comm_fault_error_raised = true;
|
||||
} else {
|
||||
if (comm_fault_error_raised) {
|
||||
clear_error("power_supply_DC/CommunicationFault");
|
||||
comm_fault_error_raised = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::ready() {
|
||||
mod->p_main->publish_capabilities(caps);
|
||||
}
|
||||
|
||||
void power_supply_DCImpl::handle_setMode(types::power_supply_DC::Mode& mode,
|
||||
types::power_supply_DC::ChargingPhase& phase) {
|
||||
std::scoped_lock lock(settings_mutex);
|
||||
|
||||
if (mode == types::power_supply_DC::Mode::Off) {
|
||||
mod->acdc.switch_on(false);
|
||||
} else if (mode == types::power_supply_DC::Mode::Export) {
|
||||
mod->acdc.switch_on(true);
|
||||
} else if (mode == types::power_supply_DC::Mode::Import) {
|
||||
mod->acdc.switch_on(false);
|
||||
} else if (mode == types::power_supply_DC::Mode::Fault) {
|
||||
mod->acdc.switch_on(false);
|
||||
}
|
||||
};
|
||||
|
||||
void power_supply_DCImpl::handle_setExportVoltageCurrent(double& voltage, double& current) {
|
||||
|
||||
if (voltage > caps.max_export_voltage_V)
|
||||
voltage = caps.max_export_voltage_V;
|
||||
else if (voltage < caps.min_export_voltage_V)
|
||||
voltage = caps.min_export_voltage_V;
|
||||
|
||||
if (current > caps.max_export_current_A)
|
||||
current = caps.max_export_current_A;
|
||||
else if (current < caps.min_export_current_A)
|
||||
current = caps.min_export_current_A;
|
||||
|
||||
std::scoped_lock lock(settings_mutex);
|
||||
|
||||
export_voltage = voltage;
|
||||
export_current_limit = current;
|
||||
|
||||
EVLOG_info << "Updating voltage/current via CAN: " << export_voltage << "V / " << export_current_limit << "A";
|
||||
mod->acdc.set_voltage_current(export_voltage, export_current_limit);
|
||||
};
|
||||
|
||||
void power_supply_DCImpl::handle_setImportVoltageCurrent(double& voltage, double& current) {
|
||||
EVLOG_error << "Not implemented";
|
||||
}
|
||||
|
||||
} // namespace main
|
||||
} // namespace module
|
||||
@@ -0,0 +1,72 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef MAIN_POWER_SUPPLY_DC_IMPL_HPP
|
||||
#define MAIN_POWER_SUPPLY_DC_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/power_supply_DC/Implementation.hpp>
|
||||
|
||||
#include "../Huawei_R100040Gx.hpp"
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
// insert your custom include headers here
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
namespace module {
|
||||
namespace main {
|
||||
|
||||
struct Conf {};
|
||||
|
||||
class power_supply_DCImpl : public power_supply_DCImplBase {
|
||||
public:
|
||||
power_supply_DCImpl() = delete;
|
||||
power_supply_DCImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<Huawei_R100040Gx>& mod, Conf& config) :
|
||||
power_supply_DCImplBase(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 void handle_setMode(types::power_supply_DC::Mode& mode,
|
||||
types::power_supply_DC::ChargingPhase& phase) override;
|
||||
virtual void handle_setExportVoltageCurrent(double& voltage, double& current) override;
|
||||
virtual void handle_setImportVoltageCurrent(double& voltage, double& current) override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<Huawei_R100040Gx>& mod;
|
||||
const Conf& config;
|
||||
|
||||
virtual void init() override;
|
||||
virtual void ready() override;
|
||||
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
std::mutex settings_mutex;
|
||||
double export_voltage{0.};
|
||||
double export_current_limit{0.};
|
||||
double minImportVoltage{0.};
|
||||
double importCurrentLimit{0.};
|
||||
types::power_supply_DC::Capabilities caps;
|
||||
|
||||
types::power_supply_DC::Mode last_publish_mode{types::power_supply_DC::Mode::Off};
|
||||
bool comm_fault_error_raised{false};
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
};
|
||||
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
// insert other definitions here
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
|
||||
} // namespace main
|
||||
} // namespace module
|
||||
|
||||
#endif // MAIN_POWER_SUPPLY_DC_IMPL_HPP
|
||||
@@ -0,0 +1,42 @@
|
||||
description: >-
|
||||
Driver for Huawei_R100040Gx ACDC power supply. Supports multiple stacked modules.
|
||||
This module never exits. If communication is lost to the power supply, a CommunicationFault is raised.
|
||||
The error is cleared once communication is back up again.
|
||||
The Linux can device (e.g. can0) should ideally be present and correctly configured when the module is started,
|
||||
but it also works if the device comes up later. Once it was up once, it should not disappear again.
|
||||
The PSU may be appear or disappear on the CAN bus itself any time.
|
||||
config:
|
||||
can_device:
|
||||
description: CAN interface name
|
||||
type: string
|
||||
default: can0
|
||||
module_addresses:
|
||||
description: >-
|
||||
Module Addresses to use. Set to "" for auto detect (Uses all modules that reply to broadcast requests on this CAN bus).
|
||||
If multiple hardware addresses are listed (e.g. "120,121"), only those will be used. Current will be shared between them.
|
||||
type: string
|
||||
default: ""
|
||||
max_export_current_A:
|
||||
description: >-
|
||||
Maximum current that the PSU module can deliver. When using multiple PSUs in parallel, this refers to the total current capability of all modules.
|
||||
Set to 0 to use hardware defaults.
|
||||
type: integer
|
||||
default: 0
|
||||
max_export_power_W:
|
||||
description: >-
|
||||
Maximum power that the PSU module can deliver. When using multiple PSUs in parallel, this refers to the total power capability of all modules.
|
||||
Set to 0 to use hardware defaults.
|
||||
type: integer
|
||||
default: 0
|
||||
provides:
|
||||
main:
|
||||
description: Main interface
|
||||
interface: power_supply_DC
|
||||
config: {}
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Andreas Heinrich
|
||||
- Cornelius Claussen
|
||||
- Florin Mihut
|
||||
- Jan Christoph Habig
|
||||
Reference in New Issue
Block a user