Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
This commit is contained in:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1,34 @@
#
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
# template version 3
#
# module setup:
# - ${MODULE_NAME}: module name
ev_setup_cpp_module()
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
# insert your custom targets and additional config variables here
add_subdirectory(test-tool)
target_sources(${MODULE_NAME}
PRIVATE
"can_driver_acdc/CanDevice.cpp"
"can_driver_acdc/UUGreenCanDevice.cpp"
"can_driver_acdc/CanPackets.cpp"
)
target_link_libraries(${MODULE_NAME}
PRIVATE
Pal::Sigslot
everest::log
)
# 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

View File

@@ -0,0 +1,49 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "UUGreenPower_UR1000X0.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 UUGreenPower_UR1000X0::init() {
// open DCDC CAN device
if (!acdc.open_device(config.can_device.c_str())) {
EVLOG_AND_THROW(EVEXCEPTION(Everest::EverestConfigError, "Could not open CAN interface ", config.can_device));
}
// configure module address
auto module_addresses = parse_module_addresses(config.module_addresses);
EVLOG_info << "Amount of modules: " << module_addresses.size();
acdc.set_module_address(module_addresses);
acdc.set_voltage_mode(config.voltage_mode);
acdc.switch_on(false);
invoke_init(*p_main);
acdc.request_module_info();
}
void UUGreenPower_UR1000X0::ready() {
invoke_ready(*p_main);
}
} // namespace module

View File

@@ -0,0 +1,64 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef UUGREEN_POWER_UR1000X0_HPP
#define UUGREEN_POWER_UR1000X0_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/UUGreenCanDevice.hpp"
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
namespace module {
struct Conf {
std::string can_device;
std::string module_addresses;
int voltage_mode;
int max_export_current_A;
int max_export_power_W;
};
class UUGreenPower_UR1000X0 : public Everest::ModuleBase {
public:
UUGreenPower_UR1000X0() = delete;
UUGreenPower_UR1000X0(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
UUGreenCanDevice 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 // UUGREEN_POWER_UR1000X0_HPP

View File

@@ -0,0 +1,120 @@
// 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 "CanDevice.hpp"
#include <everest/logging.hpp>
CanDevice::CanDevice() : exit_rx_thread{false} {
can_fd = 0;
}
CanDevice::~CanDevice() {
close_device();
}
bool CanDevice::open_device(const char* dev) {
if (!dev || std::strlen(dev) >= IFNAMSIZ) {
fprintf(stderr, "Interface name is invalid or too long: %s\n", dev ? dev : "NULL");
return false;
}
if ((can_fd = socket(PF_CAN, SOCK_RAW, CAN_RAW)) < 0) {
perror("Socket");
return false;
}
// retrieve interface index from interface name
struct ifreq ifr;
std::memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, dev, IFNAMSIZ - 1);
if (ioctl(can_fd, SIOCGIFINDEX, &ifr) < 0) {
perror(dev);
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) {
perror("Bind");
close(can_fd);
return false;
}
// spawn read thread
exit_rx_thread = false;
rx_thread_handle = std::thread(&CanDevice::rx_thread, this);
return true;
}
bool CanDevice::close_device() {
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::rx_thread() {
can_frame frame;
while (!exit_rx_thread) {
size_t nbytes = read(can_fd, &frame, sizeof(struct can_frame));
if (nbytes < 0) {
perror("Read");
} 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);
}
}
}
void CanDevice::rx_handler(uint32_t can_id, const std::vector<uint8_t>& payload) {
EVLOG_debug << "CAN frame received";
}
bool CanDevice::_tx(uint32_t can_id, const std::vector<uint8_t>& payload) {
if (can_fd == 0)
return false;
struct can_frame frame;
if (payload.size() > sizeof(frame.data)) {
throw std::runtime_error("Size of can payload data to large (" + std::to_string(payload.size()) + " bytes)");
}
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)) {
throw std::runtime_error(std::string("Failed to send can packet :") + strerror(errno));
}
return true;
}

View File

@@ -0,0 +1,30 @@
// 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 <thread>
class CanDevice {
public:
CanDevice();
virtual ~CanDevice();
bool open_device(const char* dev);
bool close_device();
protected:
virtual void rx_handler(uint32_t can_id, const std::vector<uint8_t>& payload);
bool _tx(uint32_t can_id, const std::vector<uint8_t>& payload);
private:
int can_fd;
std::atomic_bool exit_rx_thread;
std::thread rx_thread_handle;
void rx_thread();
};
#endif // CAN_DEVICE_HPP

View File

@@ -0,0 +1,114 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "CanPackets.hpp"
#include "Conversions.hpp"
#include <iostream>
namespace UU {
static std::string to_string(CommandType m) {
switch (m) {
case CommandType::Vout:
return "Vout";
case CommandType::Iout_slow:
return "Iout_slow";
case CommandType::VoutReference:
return "VoutReference";
case CommandType::IoutLimit:
return "IoutLimit";
case CommandType::ShutdownDCDC:
return "ShutdownDCDC";
case CommandType::ReadSN:
return "ReadSN";
case CommandType::ModuleStatus:
return "ModuleStatus";
case CommandType::Vab:
return "Vab";
case CommandType::Vbc:
return "Vbc";
case CommandType::VfanReference:
return "VfanReference";
case CommandType::Tin:
return "Tin";
case CommandType::Iout_fastest:
return "Iout_fastest";
case CommandType::FanSilentLevel:
return "FanSilentLevel";
case CommandType::GroupAddress:
return "GroupAddress";
case CommandType::HiMode_LoMode_Selection:
return "HiMode_LoMode_Selection";
case CommandType::HiMode_LoMode_Status:
return "HiMode_LoMode_Status";
case CommandType::Vout_fast:
return "Vout_fast";
case CommandType::TrueHiLo_Status:
return "TrueHiLo_Status";
case CommandType::CurrentCapability:
return "CurrentCapability";
case CommandType::CurrentAndCapability:
return "CurrentAndCapability";
}
return "Unknown";
}
static std::string to_string(MessageType m) {
switch (m) {
case MessageType::SetData:
return "SetData";
case MessageType::SetDataResponse:
return "SetDataResponse";
case MessageType::ReadData:
return "ReadData";
case MessageType::ReadDataResponse:
return "ReadDataResponse";
case MessageType::ReadSerialNumberResponse:
return "ReadSerialNumberResponse";
case MessageType::AllSetData:
return "AllSetData";
case MessageType::AllSetDataResponse:
return "AllSetDataResponse";
}
return "Unknown";
}
// helper functions
uint32_t encode_can_id(uint8_t module_address) {
uint32_t id = (module_address & 0x7F)
<< 14; // Module address is bits 20:14 (7 bits). Use 0x00 for broadcast to all modules
id |= (1 << 25); // Protocol version 0x01 in bits 28:25 (4 bits)
id |= (1 << 21); // Monitor address is 0x01 in bits 24:21 (4 bits)
// Production date and SerialNumber is not set on our outgoing monitor frames
return id;
}
std::ostream& operator<<(std::ostream& out, const Packet& self) {
out << "UUPacket: grp: " << std::to_string(self.group_address) << " MsgType: " << to_string(self.message_type)
<< " CmdType: " << to_string(self.command_type) << " Data: " << std::to_string(self.data);
return out;
}
Packet::operator std::vector<uint8_t>() {
std::vector<uint8_t> out;
to_raw(static_cast<uint8_t>(((group_address & 0x0F) << 4) |
static_cast<std::underlying_type<MessageType>::type>(message_type) & 0x0F),
out); // 4 bits group address, 4 bits message type
to_raw(static_cast<uint8_t>(static_cast<std::underlying_type<MessageType>::type>(command_type) & 0x7F),
out); // 7 bits of command type
to_raw(static_cast<uint16_t>(0x00), out); // 2 reserved bytes
to_raw(data, out); // 4 bytes of actual data
return out;
}
Packet::Packet(const std::vector<uint8_t> raw) {
uint8_t grp_msg_type = from_raw<uint8_t>(raw, 0);
group_address = grp_msg_type >> 4;
message_type = static_cast<MessageType>(grp_msg_type & 0x0F);
command_type = static_cast<CommandType>(from_raw<uint8_t>(raw, 1));
bytes23 = from_raw<uint16_t>(raw, 2);
data = from_raw<uint32_t>(raw, 4);
}
} // namespace UU

View File

@@ -0,0 +1,93 @@
// 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 UU {
uint32_t encode_can_id(uint8_t destination_address);
inline uint8_t module_address_from_can_id(uint32_t id) {
return (id >> 14) & 0x7F;
};
inline uint8_t monitor_adress_from_can_id(uint32_t id) {
return (id >> 21) & 0x0F;
};
inline uint8_t production_date_from_can_id(uint32_t id) {
return (id >> 9) & 0x1F;
};
inline uint8_t serial_num_low_from_can_id(uint32_t id) {
return id & 0x1FF;
};
// Commands
const uint8_t CMD_READ = 0x23;
const uint8_t CMD_WRITE = 0x24;
// Addresses
const uint8_t ADDR_BROADCAST = 0x00;
// RX and TX packet definitions
enum class MessageType : uint8_t {
SetData = 0,
SetDataResponse = 1,
ReadData = 2,
ReadDataResponse = 3,
ReadSerialNumberResponse = 4,
AllSetData = 11,
AllSetDataResponse = 12
};
// Note: naming in this enum is questionable, but follows the Can protocol documentation from UUGreenPower
enum class CommandType : uint8_t {
Vout = 0,
Iout_slow = 1,
VoutReference = 2,
IoutLimit = 3,
ShutdownDCDC = 4,
ReadSN = 5,
ModuleStatus = 8,
Vab = 20,
Vbc = 21,
Vca = 22,
VfanReference = 26,
Tin = 30,
Iout_fastest = 47,
FanSilentLevel = 62,
GroupAddress = 89,
HiMode_LoMode_Selection = 95,
HiMode_LoMode_Status = 96,
Vout_fast = 98,
TrueHiLo_Status = 101,
CurrentCapability = 104,
CurrentAndCapability = 114
};
struct Packet {
Packet(MessageType _message_type, CommandType _command_type, uint32_t _data = 0x00, uint8_t _group_address = 0x00) :
message_type(_message_type), command_type(_command_type), data(_data), group_address(_group_address){};
Packet(const std::vector<uint8_t> raw);
friend std::ostream& operator<<(std::ostream& out, const Packet& self);
operator std::vector<uint8_t>();
uint8_t group_address{0};
MessageType message_type{MessageType::ReadData};
CommandType command_type{CommandType::ReadSN};
uint16_t bytes23{0};
uint32_t data{0};
};
} // namespace UU
#endif // CAN_PACKETS_HPP

View File

@@ -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

View File

@@ -0,0 +1,252 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "UUGreenCanDevice.hpp"
#include "CanPackets.hpp"
#include <iostream>
#include <unistd.h>
#include <everest/logging.hpp>
UUGreenCanDevice::~UUGreenCanDevice() {
exit_tx_thread = true;
exit_cmd_thread = true;
}
bool UUGreenCanDevice::switch_on(bool on) {
// actual switching on will be handled in tx thread
requested_on = on;
return true;
}
bool UUGreenCanDevice::switch_on_nolock(bool on) {
uint32_t data{0x00};
if (not on) {
data = 0x01;
}
bool success = true;
for (auto module_address : module_addresses) {
if (not tx(module_address, UU::Packet(UU::MessageType::SetData, UU::CommandType::ShutdownDCDC, data))) {
success = false;
} else {
is_on = on;
}
}
if (on) {
// After switching on, update the voltage/current settings
set_voltage_current_nolock(set_point_voltage, set_point_current);
}
return success;
}
bool UUGreenCanDevice::set_voltage_current(float voltage, float current) {
requested_set_point_voltage = voltage;
requested_set_point_current = current;
return true;
}
bool UUGreenCanDevice::set_voltage_current_nolock(float voltage, float current) {
bool success = true;
set_point_voltage = voltage;
set_point_current = current;
// check if we need to switch between high and lo voltage mode
if (hi_mode_config_setting == VoltageMode::Automatic) {
if (voltage > LO_MODE_MAX_VOLTAGE) {
hi_mode_commanded = VoltageMode::High;
} else {
hi_mode_commanded = VoltageMode::Low;
}
}
internal_update_voltage_mode();
for (auto module_address : module_addresses) {
if (not tx(module_address, UU::Packet(UU::MessageType::SetData, UU::CommandType::VoutReference,
static_cast<uint32_t>(voltage * 1000)))) {
success = false;
}
// Split current equally between modules
if (not tx(module_address, UU::Packet(UU::MessageType::SetData, UU::CommandType::IoutLimit,
static_cast<uint32_t>(current * 1000 / module_addresses.size())))) {
success = false;
}
}
return success;
}
bool UUGreenCanDevice::internal_update_voltage_mode() {
bool success = true;
if (hi_mode_last_commanded not_eq hi_mode_commanded) {
hi_mode_last_commanded = hi_mode_commanded;
bool was_on = is_on;
if (was_on) {
// Switch it off if it was on before
switch_on_nolock(false);
}
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
for (auto module_address : module_addresses) {
// We need to change hi/lo voltage mode, so power it off, change mode, and power it on again if it was
// powered on before
if (not tx(module_address,
UU::Packet(UU::MessageType::SetData, UU::CommandType::HiMode_LoMode_Selection,
static_cast<std::underlying_type<VoltageMode>::type>(hi_mode_commanded)))) {
success = false;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
if (was_on) {
// Switch it on if it was on before
switch_on_nolock(true);
}
}
return success;
}
static std::string to_serial_number(uint16_t a, uint32_t b) {
std::string ser = "Module type: " + std::to_string((int)(a & 0x01FF));
ser += " | OV Type code: " + std::to_string((int)(a >> 10));
// Note: the S/N reported here does not match the sticker on the case
ser += " | S/N: " + std::to_string(b);
return ser;
}
void UUGreenCanDevice::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;
}
EVLOG_debug << "UUGreen: CAN frame received. ID: 0x" << std::hex << can_id;
// is it for us?
auto monitor_address = UU::monitor_adress_from_can_id(can_id);
if (not(monitor_address == UU::ADDR_BROADCAST or monitor_address == 0x01)) {
EVLOG_debug << "UU: Not for us: ";
return;
}
// parse packet
auto packet = UU::Packet(payload);
uint8_t source_address = UU::module_address_from_can_id(can_id);
EVLOG_debug << "RX packet: " << packet;
if (packet.message_type == UU::MessageType::ReadDataResponse) {
switch (packet.command_type) {
case UU::CommandType::Vout: {
telemetries[source_address].voltage = packet.data / 1000.;
// 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;
case UU::CommandType::Iout_slow: {
// report sum of all currents
telemetries[source_address].current = packet.data / 1000.;
// calculate total current
total_current = 0.;
for (const auto& t : telemetries) {
total_current += t.second.current;
}
} break;
case UU::CommandType::ReadSN: {
// print serial number information
signal_serial_number(source_address, to_serial_number(packet.bytes23, packet.data));
} break;
}
}
if (packet.message_type == UU::MessageType::ReadSerialNumberResponse) {
// print serial number information
signal_serial_number(source_address, to_serial_number(packet.bytes23, packet.data));
}
}
void UUGreenCanDevice::request_module_info() {
// Request information about modules once
for (auto module_address : module_addresses) {
// request serial number
tx(module_address, UU::Packet(UU::MessageType::ReadData, UU::CommandType::ReadSN));
}
}
void UUGreenCanDevice::tx_thread() {
while (!exit_tx_thread) {
{
for (auto module_address : module_addresses) {
// request current system DC voltage. Answer will be processed by RX thread.
tx(module_address, UU::Packet(UU::MessageType::ReadData, UU::CommandType::Vout));
// request current system DC current. Answer will be processed by RX thread.
tx(module_address, UU::Packet(UU::MessageType::ReadData, UU::CommandType::Iout_slow));
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void UUGreenCanDevice::cmd_thread() {
while (!exit_cmd_thread) {
{
// 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);
}
// 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 UUGreenCanDevice::tx(const uint8_t module_address, const std::vector<uint8_t>& payload) {
uint32_t can_id = UU::encode_can_id(module_address);
can_id |= 0x80000000U; // Extended frame format
return _tx(can_id, payload);
}
std::ostream& operator<<(std::ostream& out, const UUGreenCanDevice::Telemetry& self) {
out << "\n------------------------------------------------\nTelemetry\n---------\n";
out << "DC output: " << std::to_string(self.voltage) << "V / " << std::to_string(self.current) << "A" << std::endl;
out << "------------------------------------------------\n";
return out;
}

View File

@@ -0,0 +1,120 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef INFY_CAN_DEVICE_HPP
#define INFY_CAN_DEVICE_HPP
#include "CanDevice.hpp"
#include <atomic>
#include <linux/can.h>
#include <map>
#include <mutex>
#include <sigslot/signal.hpp>
#include <thread>
class UUGreenCanDevice : public CanDevice {
public:
UUGreenCanDevice() {
// spawn thread that requests some data periodically to keep the connection alive
exit_tx_thread = false;
tx_thread_handle = std::thread(&UUGreenCanDevice::tx_thread, this);
exit_cmd_thread = false;
cmd_thread_handle = std::thread(&UUGreenCanDevice::cmd_thread, this);
};
~UUGreenCanDevice();
void set_module_address(const std::vector<uint8_t>& _module_addresses) {
module_addresses = _module_addresses;
};
// Commands
bool switch_on(bool on);
bool set_voltage_current(float voltage, float current);
enum class VoltageMode : uint32_t {
Automatic = 0,
High = 1,
Low = 2,
};
bool set_voltage_mode(VoltageMode high_mode) {
hi_mode_config_setting = high_mode;
return internal_update_voltage_mode();
}
bool set_voltage_mode(int high_mode) {
return set_voltage_mode(from_int(high_mode));
}
int get_number_of_modules() {
return module_addresses.size();
}
void request_module_info();
// Data out
sigslot::signal<float, float> signal_voltage_current;
sigslot::signal<int, const std::string&> signal_serial_number;
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);
private:
VoltageMode from_int(int mode) {
VoltageMode m{VoltageMode::Automatic};
if (mode == 1) {
m = VoltageMode::High;
} else if (mode == 2) {
m = VoltageMode::Low;
}
return m;
};
std::atomic_bool exit_tx_thread;
std::thread tx_thread_handle;
void tx_thread();
std::atomic_bool exit_cmd_thread;
std::thread cmd_thread_handle;
void cmd_thread();
bool tx(const uint8_t module_address, const std::vector<uint8_t>& payload);
std::vector<uint8_t> module_addresses{UU::ADDR_BROADCAST};
bool switch_on_nolock(bool on);
bool internal_update_voltage_mode();
bool set_voltage_current_nolock(float voltage, float current);
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.};
VoltageMode hi_mode_config_setting{VoltageMode::Automatic};
VoltageMode hi_mode_commanded{VoltageMode::Low};
VoltageMode hi_mode_last_commanded{VoltageMode::High};
bool is_on{false};
std::atomic_bool requested_on{false};
bool last_requested_on{false};
static constexpr float LO_MODE_MAX_VOLTAGE{500};
};
#endif // INFY_CAN_DEVICE_HPP

View File

@@ -0,0 +1,90 @@
// 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() {
int number_of_modules = mod->acdc.get_number_of_modules();
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 = number_of_modules * mod->config.max_export_current_A;
caps.min_export_voltage_V = 150;
caps.max_export_voltage_V = 1000;
caps.max_export_power_W = number_of_modules * mod->config.max_export_power_W;
caps.max_import_current_A = 25;
caps.min_import_current_A = 0;
caps.max_import_power_W = 10000;
caps.min_import_voltage_V = 200;
caps.max_import_voltage_V = 1000;
mod->acdc.signal_voltage_current.connect([this](float voltage, float current) {
static uint8_t throttle_cnt = 0;
if (throttle_cnt++ % 10 == 0) {
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;
});
}
void power_supply_DCImpl::ready() {
publish_capabilities(caps);
mod->acdc.switch_on(false);
}
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

View File

@@ -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 "../UUGreenPower_UR1000X0.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<UUGreenPower_UR1000X0>& 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<UUGreenPower_UR1000X0>& 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};
// 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

View File

@@ -0,0 +1,38 @@
description: Driver for UUGreenPower_UR1000X0 ACDC power supply. Currently supports only one module.
config:
can_device:
description: CAN interface name
type: string
default: can0
module_addresses:
description: >-
Module Addresses to use. Set to "0" for broadcast. Make sure to connect only one PSU on the CAN bus in broadcast mode, otherwise
it will charge at a too high current (as every module will produce the full current the EV requests!)
If you have multiple PSUs, use individual addresses and list them comma separated, e.g. "1,2". Then it will share the current between
these PSUs.
type: string
default: "0"
voltage_mode:
description: Set to 0 for automatic switching, 1 for high voltage mode, 2 for low voltage mode
type: integer
default: 0
max_export_current_A:
description: Maximum current that the PSU module can deliver. When using multiple PSUs in parallel, this refers to each individual PSU current capability.
type: integer
default: 100
max_export_power_W:
description: Maximum power that the PSU module can deliver. When using multiple PSUs in parallel, this refers to each individual PSU power capability.
type: integer
default: 40000
provides:
main:
description: Main interface
interface: power_supply_DC
config: {}
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Cornelius Claussen
- Jan Christoph Habig
- Andreas Heinrich
- Florin Mihut

View File

@@ -0,0 +1,13 @@
cmake_minimum_required(VERSION 3.10)
# set the project name
project(uugreen-acdc-test-tool VERSION 0.1)
# specify the C++ standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
# add the executable
add_executable(uugreen-acdc-test-tool main.cpp ../can_driver_acdc/CanDevice.cpp ../can_driver_acdc/UUGreenCanDevice.cpp ../can_driver_acdc/CanPackets.cpp)
target_include_directories(uugreen-acdc-test-tool PRIVATE ".." "../can_driver_acdc" ${everest-framework_SOURCE_DIR})
target_link_libraries(uugreen-acdc-test-tool PRIVATE Pal::Sigslot date::date date::date-tz everest::log)

View File

@@ -0,0 +1,28 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "UUGreenCanDevice.hpp"
#include <iostream>
#include <unistd.h>
#include <everest/logging.hpp>
int main(int argc, char** argv) {
UUGreenCanDevice can;
if (!can.open_device("can0")) {
return 1;
}
EVLOG_debug << "CAN device started.";
can.switch_on(true);
can.set_voltage_current(200, 10);
while (true) {
usleep(50000);
}
can.close_device();
return 0;
}