diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/PhyVersoBSP.cpp b/modules/HardwareDrivers/EVSE/PhyVersoBSP/PhyVersoBSP.cpp index 27fc9ed99f..81d40fc12e 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/PhyVersoBSP.cpp +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/PhyVersoBSP.cpp @@ -22,23 +22,76 @@ void PhyVersoBSP::init() { return; } - invoke_init(*p_connector_1); - invoke_init(*p_connector_2); - invoke_init(*p_rcd_1); - invoke_init(*p_rcd_2); - invoke_init(*p_connector_lock_1); - invoke_init(*p_connector_lock_2); + serial.flush_buffers(); serial.signal_config_request.connect([&]() { serial.send_config(); EVLOG_info << "Sent config packet to MCU"; }); -} -void PhyVersoBSP::ready() { + serial.signal_connection_timeout.connect([this]() { + auto err = p_connector_1->error_factory->create_error("evse_board_support/CommunicationFault", "McuToEverest", + "Serial connection to MCU timed out"); + p_connector_1->raise_error(err); + err = p_connector_2->error_factory->create_error("evse_board_support/CommunicationFault", "McuToEverest", + "Serial connection to MCU timed out"); + p_connector_2->raise_error(err); + }); + + serial.signal_error_flags.connect([this](int connector, ErrorFlags error_flags) { + // heartbeat failure from Mcu side (not receiving packets) will be visible in both connector errors + if (error_flags.heartbeat_timeout != last_heartbeat_error) { + if (error_flags.heartbeat_timeout) { + auto err = p_connector_1->error_factory->create_error( + "evse_board_support/CommunicationFault", "EverestToMcu", "MCU did not receive Everest heartbeat"); + p_connector_1->raise_error(err); + err = p_connector_2->error_factory->create_error( + "evse_board_support/CommunicationFault", "EverestToMcu", "MCU did not receive Everest heartbeat"); + p_connector_2->raise_error(err); + } else { + p_connector_1->clear_error("evse_board_support/CommunicationFault", "EverestToMcu"); + p_connector_2->clear_error("evse_board_support/CommunicationFault", "EverestToMcu"); + } + } + last_heartbeat_error = error_flags.heartbeat_timeout; + }); + + serial.signal_keep_alive.connect([this](KeepAlive d) { + mcu_config_done = d.configuration_done; + p_connector_1->clear_error("evse_board_support/CommunicationFault", "McuToEverest"); + p_connector_2->clear_error("evse_board_support/CommunicationFault", "McuToEverest"); + }); + + serial.reset(1); + serial.run(); gpio.run(); + // very sporadically multiple resets needed for MCU to respond -> retrying until we get MCU response in a + // configured, ready and running state + mcu_config_done = false; + uint16_t n_tries = 0; + while (!mcu_config_done) { + serial.keep_alive(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + n_tries++; + if (n_tries > 20) { + EVLOG_info << "Trying reset again"; + serial.flush_buffers(); + serial.reset(1); + n_tries = 0; + } + } + + invoke_init(*p_connector_1); + invoke_init(*p_connector_2); + invoke_init(*p_rcd_1); + invoke_init(*p_rcd_2); + invoke_init(*p_connector_lock_1); + invoke_init(*p_connector_lock_2); +} + +void PhyVersoBSP::ready() { invoke_ready(*p_connector_1); invoke_ready(*p_connector_2); invoke_ready(*p_rcd_1); @@ -58,6 +111,36 @@ void PhyVersoBSP::ready() { // fills evConfig bridge with config values from manifest/everest config void PhyVersoBSP::everest_config_to_verso_config() { + // if a port is configured to be AC and has a socket, a motor lock type specification/usage is mandatory + if ((this->config.conn1_disable_port == false) && (this->config.conn1_dc == false) && + (this->config.conn1_has_socket == true) && (this->config.conn1_motor_lock_type < 1)) { + EVLOG_critical << "Motor lock type for connector 1 has to be specified when using connector 1 as AC charging " + "port with a socket/detachable charging cable!"; + throw std::runtime_error("Motor lock type for connector 1 has to be specified when using connector 1 as AC " + "charging port with a socket/detachable charging cable!"); + } + if ((this->config.conn2_disable_port == false) && (this->config.conn2_dc == false) && + (this->config.conn2_has_socket == true) && (this->config.conn2_motor_lock_type < 1)) { + EVLOG_critical << "Motor lock type for connector 2 has to be specified when using connector 2 as AC charging " + "port with a socket/detachable charging cable!"; + throw std::runtime_error("Motor lock type for connector 2 has to be specified when using connector 2 as AC " + "charging port with a socket/detachable charging cable!"); + } + + if ((this->config.conn1_feedback_pull < 0) || (this->config.conn1_feedback_pull > 2)) { + EVLOG_error << "conn1_feedback_pull out of range! Falling back to default: 2"; + verso_config.conf.conn1_feedback_pull = 2; + } else { + verso_config.conf.conn1_feedback_pull = this->config.conn1_feedback_pull; + } + + if ((this->config.conn2_feedback_pull < 0) || (this->config.conn2_feedback_pull > 2)) { + EVLOG_error << "conn2_feedback_pull out of range! Falling back to default: 2"; + verso_config.conf.conn2_feedback_pull = 2; + } else { + verso_config.conf.conn2_feedback_pull = this->config.conn2_feedback_pull; + } + verso_config.conf.serial_port = this->config.serial_port; verso_config.conf.baud_rate = this->config.baud_rate; verso_config.conf.reset_gpio_bank = this->config.reset_gpio_bank; @@ -72,6 +155,12 @@ void PhyVersoBSP::everest_config_to_verso_config() { verso_config.conf.conn2_gpio_stop_button_bank = this->config.conn2_gpio_stop_button_bank; verso_config.conf.conn2_gpio_stop_button_pin = this->config.conn2_gpio_stop_button_pin; verso_config.conf.conn2_gpio_stop_button_invert = this->config.conn2_gpio_stop_button_invert; + verso_config.conf.conn1_disable_port = this->config.conn1_disable_port; + verso_config.conf.conn2_disable_port = this->config.conn2_disable_port; + verso_config.conf.conn1_feedback_active_low = this->config.conn1_feedback_active_low; + verso_config.conf.conn2_feedback_active_low = this->config.conn2_feedback_active_low; + verso_config.conf.conn1_dc = this->config.conn1_dc; + verso_config.conf.conn2_dc = this->config.conn2_dc; } } // namespace module diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/PhyVersoBSP.hpp b/modules/HardwareDrivers/EVSE/PhyVersoBSP/PhyVersoBSP.hpp index d25244d522..0c20cf3a0a 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/PhyVersoBSP.hpp +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/PhyVersoBSP.hpp @@ -59,6 +59,12 @@ struct Conf { std::string conn2_gpio_stop_button_bank; int conn2_gpio_stop_button_pin; bool conn2_gpio_stop_button_invert; + bool conn1_disable_port; + bool conn2_disable_port; + bool conn1_feedback_active_low; + bool conn2_feedback_active_low; + int conn1_feedback_pull; + int conn2_feedback_pull; }; class PhyVersoBSP : public Everest::ModuleBase { @@ -110,6 +116,8 @@ class PhyVersoBSP : public Everest::ModuleBase { // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 // insert your private definitions here void everest_config_to_verso_config(); + bool last_heartbeat_error; + bool mcu_config_done = false; // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 }; diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/example_split_lock.json b/modules/HardwareDrivers/EVSE/PhyVersoBSP/cli_config_hella_lock.json similarity index 65% rename from modules/HardwareDrivers/EVSE/PhyVersoBSP/example_split_lock.json rename to modules/HardwareDrivers/EVSE/PhyVersoBSP/cli_config_hella_lock.json index b514c94ce2..c1f46bfb72 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/example_split_lock.json +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/cli_config_hella_lock.json @@ -1,5 +1,5 @@ { - "conn1_motor_lock_type": 2, + "conn1_motor_lock_type": 1, "conn2_motor_lock_type": 1, "reset_gpio_bank": 2 } \ No newline at end of file diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/cli_config_potentiometer_lock.json b/modules/HardwareDrivers/EVSE/PhyVersoBSP/cli_config_potentiometer_lock.json new file mode 100644 index 0000000000..6464ee27ea --- /dev/null +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/cli_config_potentiometer_lock.json @@ -0,0 +1,5 @@ +{ + "conn1_motor_lock_type": 2, + "conn2_motor_lock_type": 2, + "reset_gpio_bank": 2 +} \ No newline at end of file diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/connector_1/evse_board_supportImpl.cpp b/modules/HardwareDrivers/EVSE/PhyVersoBSP/connector_1/evse_board_supportImpl.cpp index 0edbd224e3..30b42db78a 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/connector_1/evse_board_supportImpl.cpp +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/connector_1/evse_board_supportImpl.cpp @@ -50,8 +50,8 @@ void evse_board_supportImpl::init() { mod->serial.signal_pp_state.connect([this](int connector, PpState s) { if (connector == 1) { - EVLOG_info << "[1] PpState " << s; if (last_pp_state != s) { + EVLOG_info << "[1] PpState " << s; publish_ac_pp_ampacity(to_pp_ampacity(s)); } last_pp_state = s; @@ -67,6 +67,47 @@ void evse_board_supportImpl::init() { last_stop_button_state = state; } }); + + mod->serial.signal_error_flags.connect([this](int connector, ErrorFlags error_flags) { + if (connector == 1) { + // Contactor feedback divergence + if (error_flags.coil_feedback_diverges != last_error_flags.coil_feedback_diverges) { + if (error_flags.coil_feedback_diverges) { + Everest::error::Error error_object = this->error_factory->create_error( + "evse_board_support/MREC17EVSEContactorFault", "", + "Port 1 contactor feedback diverges from target state", Everest::error::Severity::High); + this->raise_error(error_object); + } else { + this->clear_error("evse_board_support/MREC17EVSEContactorFault"); + } + } + + // Diode fault + if (error_flags.diode_fault != last_error_flags.diode_fault) { + if (error_flags.diode_fault) { + Everest::error::Error error_object = this->error_factory->create_error( + "evse_board_support/DiodeFault", "", "Port 1 diode fault", Everest::error::Severity::High); + this->raise_error(error_object); + } else { + this->clear_error("evse_board_support/DiodeFault"); + } + } + + // PP fault + if (error_flags.pp_signal_fault != last_error_flags.pp_signal_fault) { + if (error_flags.pp_signal_fault) { + Everest::error::Error error_object = + this->error_factory->create_error("evse_board_support/MREC23ProximityFault", "", + "Port 1 PP signal fault", Everest::error::Severity::High); + this->raise_error(error_object); + } else { + this->clear_error("evse_board_support/MREC23ProximityFault"); + } + } + + last_error_flags = error_flags; + } + }); } void evse_board_supportImpl::ready() { @@ -101,14 +142,13 @@ void evse_board_supportImpl::handle_pwm_F() { } void evse_board_supportImpl::handle_allow_power_on(types::evse_board_support::PowerOnOff& value) { + if (mod->config.conn1_disable_port) { + EVLOG_error << "[1] Port disabled; Cannot set power_on!"; + return; + } + if (mod->config.conn1_dc) { mod->serial.set_coil_state_request(1, CoilType_COIL_DC1, value.allow_power_on); - // FIXME: implement in MCU with feedback - if (value.allow_power_on) { - publish_event({types::board_support_common::Event::PowerOn}); - } else { - publish_event({types::board_support_common::Event::PowerOff}); - } } else { mod->serial.set_coil_state_request(1, CoilType_COIL_AC, value.allow_power_on); } diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/connector_1/evse_board_supportImpl.hpp b/modules/HardwareDrivers/EVSE/PhyVersoBSP/connector_1/evse_board_supportImpl.hpp index 0ba87f7aa2..c10ab8df99 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/connector_1/evse_board_supportImpl.hpp +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/connector_1/evse_board_supportImpl.hpp @@ -62,6 +62,7 @@ class evse_board_supportImpl : public evse_board_supportImplBase { CpState last_cp_state; PpState last_pp_state; ///< The last pp state received from the MCU. bool last_stop_button_state; + ErrorFlags last_error_flags; // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 }; diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/connector_2/evse_board_supportImpl.cpp b/modules/HardwareDrivers/EVSE/PhyVersoBSP/connector_2/evse_board_supportImpl.cpp index 742ae9b585..b41b57f074 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/connector_2/evse_board_supportImpl.cpp +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/connector_2/evse_board_supportImpl.cpp @@ -50,8 +50,8 @@ void evse_board_supportImpl::init() { mod->serial.signal_pp_state.connect([this](int connector, PpState s) { if (connector == 2) { - EVLOG_info << "[2] PpState " << s; if (last_pp_state != s) { + EVLOG_info << "[2] PpState " << s; publish_ac_pp_ampacity(to_pp_ampacity(s)); } last_pp_state = s; @@ -67,6 +67,47 @@ void evse_board_supportImpl::init() { last_stop_button_state = state; } }); + + mod->serial.signal_error_flags.connect([this](int connector, ErrorFlags error_flags) { + if (connector == 2) { + // Contactor feedback divergence + if (error_flags.coil_feedback_diverges != last_error_flags.coil_feedback_diverges) { + if (error_flags.coil_feedback_diverges) { + Everest::error::Error error_object = this->error_factory->create_error( + "evse_board_support/MREC17EVSEContactorFault", "", + "Port 2 contactor feedback diverges from target state", Everest::error::Severity::High); + this->raise_error(error_object); + } else { + this->clear_error("evse_board_support/MREC17EVSEContactorFault"); + } + } + + // Diode fault + if (error_flags.diode_fault != last_error_flags.diode_fault) { + if (error_flags.diode_fault) { + Everest::error::Error error_object = this->error_factory->create_error( + "evse_board_support/DiodeFault", "", "Port 2 diode fault", Everest::error::Severity::High); + this->raise_error(error_object); + } else { + this->clear_error("evse_board_support/DiodeFault"); + } + } + + // PP fault + if (error_flags.pp_signal_fault != last_error_flags.pp_signal_fault) { + if (error_flags.pp_signal_fault) { + Everest::error::Error error_object = + this->error_factory->create_error("evse_board_support/MREC23ProximityFault", "", + "Port 2 PP signal fault", Everest::error::Severity::High); + this->raise_error(error_object); + } else { + this->clear_error("evse_board_support/MREC23ProximityFault"); + } + } + + last_error_flags = error_flags; + } + }); } void evse_board_supportImpl::ready() { @@ -101,14 +142,13 @@ void evse_board_supportImpl::handle_pwm_F() { } void evse_board_supportImpl::handle_allow_power_on(types::evse_board_support::PowerOnOff& value) { + if (mod->config.conn2_disable_port) { + EVLOG_error << "[2] Port disabled; Cannot set power_on!"; + return; + } + if (mod->config.conn2_dc) { mod->serial.set_coil_state_request(2, CoilType_COIL_DC1, value.allow_power_on); - // FIXME: implement in MCU with feedback - if (value.allow_power_on) { - publish_event({types::board_support_common::Event::PowerOn}); - } else { - publish_event({types::board_support_common::Event::PowerOff}); - } } else { mod->serial.set_coil_state_request(2, CoilType_COIL_AC, value.allow_power_on); } diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/connector_2/evse_board_supportImpl.hpp b/modules/HardwareDrivers/EVSE/PhyVersoBSP/connector_2/evse_board_supportImpl.hpp index fde416dabe..5d41ce23cb 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/connector_2/evse_board_supportImpl.hpp +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/connector_2/evse_board_supportImpl.hpp @@ -61,6 +61,7 @@ class evse_board_supportImpl : public evse_board_supportImplBase { CpState last_cp_state; PpState last_pp_state; ///< The last pp state received from the MCU. bool last_stop_button_state; + ErrorFlags last_error_flags; // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 }; diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/example_config_pionix.json b/modules/HardwareDrivers/EVSE/PhyVersoBSP/example_config_pionix.json deleted file mode 100644 index de63336b31..0000000000 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/example_config_pionix.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "conn1_motor_lock_type": 2, - "conn2_motor_lock_type": 2 -} \ No newline at end of file diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/example_config_qwello.json b/modules/HardwareDrivers/EVSE/PhyVersoBSP/example_config_qwello.json deleted file mode 100644 index b0a504602f..0000000000 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/example_config_qwello.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "conn1_motor_lock_type": 1, - "conn2_motor_lock_type": 1 -} \ No newline at end of file diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/manifest.yaml b/modules/HardwareDrivers/EVSE/PhyVersoBSP/manifest.yaml index acdcc05386..86583e99ff 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/manifest.yaml +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/manifest.yaml @@ -133,13 +133,25 @@ config: type: integer default: 23 conn1_motor_lock_type: - description: Connector 1 motor lock type; 1 == Hella Style time-based lock, 2 == Valeo potentiometer feedback based + description: > + Connector 1 motor lock type; + -1 == no Lock + 1 == Hella Style time-based lock, + 2 == Valeo potentiometer feedback based; + + If charging port has a socket and is AC charging, it will need a lock specified type: integer - default: 2 + default: -1 conn2_motor_lock_type: - description: Connector 2 motor lock type; 1 == Hella Style time-based lock, 2 == Valeo potentiometer feedback based + description: > + Connector 2 motor lock type; + -1 == no Lock + 1 == Hella Style time-based lock, + 2 == Valeo potentiometer feedback based; + + If charging port has a socket and is AC charging, it will need a lock specified type: integer - default: 2 + default: -1 conn1_gpio_stop_button_enabled: description: Set to true to enable external charging stop button for connector 1 on a GPIO connected to the SOM type: boolean @@ -172,6 +184,31 @@ config: description: Set to true to invert pin logic type: boolean default: false + conn1_disable_port: + description: Set to true if port 1 is neither used for AC nor DC charging (will overwrite conn1_dc parameter) + type: boolean + default: false + conn2_disable_port: + description: Set to true if port 2 is neither used for AC nor DC charging (will overwrite conn2_dc parameter) + type: boolean + default: false + conn1_feedback_active_low: + description: Set to true if relay mirror contact on port 1 feedback is active LOW, false if active HIGH; don't change for AC port config + type: boolean + default: true + conn2_feedback_active_low: + description: Set to true if relay mirror contact on port 2 feedback is active LOW, false if active HIGH; don't change for AC port config + type: boolean + default: true + conn1_feedback_pull: + description: DC port config only - specify which way internal pull resistors will work; 0 -> None, 1 -> PullUp, 2 -> PullDown (default=PD) + type: integer + default: 2 + conn2_feedback_pull: + description: DC port config only - specify which way internal pull resistors will work; 0 -> None, 1 -> PullUp, 2 -> PullDown (default=PD) + type: integer + default: 2 + provides: connector_1: interface: evse_board_support @@ -181,10 +218,10 @@ provides: description: provides the board support interface to low level control the proximity and control pilots, relais and motor lock rcd_1: interface: ac_rcd - description: RCD interface of the onboard RCD + description: RCD interface for an external RDC-MD rcd_2: interface: ac_rcd - description: RCD interface of the onboard RCD + description: RCD interface of the onboard RDC-MD connector_lock_1: interface: connector_lock description: Lock interface @@ -196,4 +233,5 @@ metadata: license: https://opensource.org/licenses/Apache-2.0 authors: - Cornelius Claussen + - Jonas Rockstroh diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_cli/main.cpp b/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_cli/main.cpp index 87ec9299e6..30d1463a8b 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_cli/main.cpp +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_cli/main.cpp @@ -154,6 +154,12 @@ int main(int argc, char* argv[]) { case LockState_UNLOCKED: printf(">> Connector %i: Lock State Unlocked\n", connector); break; + case LockState_LOCKING: + printf(">> Connector %i: Lock State Locking\n", connector); + break; + case LockState_UNLOCKING: + printf(">> Connector %i: Lock State Unlocking\n", connector); + break; } }); @@ -165,6 +171,9 @@ int main(int argc, char* argv[]) { printf("\tventilation_not_available: %d\n", error_flags.ventilation_not_available); printf("\tconnector_lock_failed: %d\n", error_flags.connector_lock_failed); printf("\tcp_signal_fault: %d\n", error_flags.cp_signal_fault); + printf("\theartbeat_timeout: %d\n", error_flags.heartbeat_timeout); + printf("\tcoil_feedback_diverges_ac: %d\n", error_flags.coil_feedback_diverges); + printf("\tpp_signal_fault: %d\n", error_flags.pp_signal_fault); printf("------------\n"); }); @@ -291,6 +300,7 @@ int main(int argc, char* argv[]) { p.reset_rcd(selected_connector, true); break; } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } return 0; diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/evConfig.cpp b/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/evConfig.cpp index 08462ae1b1..aef33a3a70 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/evConfig.cpp +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/evConfig.cpp @@ -20,8 +20,7 @@ bool evConfig::open_file(std::string path) { try { std::ifstream f(path); config_file = json::parse(f); - // check validity first - return check_validity(); + return true; } catch (const std::exception& e) { std::cerr << "error: " << e.what() << std::endl; } catch (...) { @@ -30,19 +29,6 @@ bool evConfig::open_file(std::string path) { return false; } -bool evConfig::check_validity() { - std::vector mandatory_config_keys = {"conn1_motor_lock_type", "conn2_motor_lock_type"}; - - for (const std::string& key : mandatory_config_keys) { - if (!config_file.contains(key)) { - std::cout << fmt::format("Missing '{}' config parameter", key) << std::endl; - return false; - } - } - - return true; -} - // unused for now bool evConfig::read_hw_eeprom(ConfigHardwareRevision& hw_rev) { // TODO: read eeprom on new phyVERSO hw revisions, @@ -51,17 +37,48 @@ bool evConfig::read_hw_eeprom(ConfigHardwareRevision& hw_rev) { return true; } -// todo: needs to refactored a lot void evConfig::fill_config_packet() { config_packet.which_payload = EverestToMcu_config_response_tag; config_packet.connector = 0; read_hw_eeprom(config_packet.payload.config_response.hw_rev); - config_packet.payload.config_response.lock_1.type = static_cast(conf.conn1_motor_lock_type); - config_packet.payload.config_response.has_lock_1 = true; + /* fill port 1 config */ + { + auto& chargeport_config = config_packet.payload.config_response.chargeport_config[0]; + + chargeport_config.has_lock = true; + chargeport_config.lock.type = static_cast(conf.conn1_motor_lock_type); + chargeport_config.feedback_active_low = conf.conn1_feedback_active_low; + chargeport_config.feedback_pull = static_cast(conf.conn1_feedback_pull); + chargeport_config.has_socket = conf.conn1_has_socket; + + if (conf.conn1_disable_port) { + chargeport_config.type = ChargePortType_DISABLED; + } else if (conf.conn1_dc) { + chargeport_config.type = ChargePortType_DC; + } else { + chargeport_config.type = ChargePortType_AC; + } + } + + /* fill port 2 config */ + { + auto& chargeport_config = config_packet.payload.config_response.chargeport_config[1]; + + chargeport_config.has_lock = true; + chargeport_config.lock.type = static_cast(conf.conn2_motor_lock_type); + chargeport_config.feedback_active_low = conf.conn2_feedback_active_low; + chargeport_config.feedback_pull = static_cast(conf.conn2_feedback_pull); + chargeport_config.has_socket = conf.conn2_has_socket; - config_packet.payload.config_response.lock_2.type = static_cast(conf.conn2_motor_lock_type); - config_packet.payload.config_response.has_lock_2 = true; + if (conf.conn2_disable_port) { + chargeport_config.type = ChargePortType_DISABLED; + } else if (conf.conn2_dc) { + chargeport_config.type = ChargePortType_DC; + } else { + chargeport_config.type = ChargePortType_AC; + } + } } EverestToMcu evConfig::get_config_packet() { @@ -69,17 +86,23 @@ EverestToMcu evConfig::get_config_packet() { return config_packet; } +// keep in mind, json config is only used for testing via phyverso_cli void evConfig::json_conf_to_evConfig() { - conf.conn1_motor_lock_type = config_file["conn1_motor_lock_type"]; - conf.conn2_motor_lock_type = config_file["conn2_motor_lock_type"]; - - // set GPIO related settings for evSerial if available - try { - conf.reset_gpio_bank = config_file["reset_gpio_bank"]; - } catch (...) { - } - try { - conf.reset_gpio_pin = config_file["reset_gpio_pin"]; - } catch (...) { - } + // try and get value from json file or keep default values as is + conf.conn1_motor_lock_type = config_file.value("conn1_motor_lock_type", conf.conn1_motor_lock_type); + conf.conn2_motor_lock_type = config_file.value("conn2_motor_lock_type", conf.conn2_motor_lock_type); + conf.reset_gpio_bank = config_file.value("reset_gpio_bank", conf.reset_gpio_bank); + conf.reset_gpio_pin = config_file.value("reset_gpio_pin", conf.reset_gpio_pin); + conf.conn1_disable_port = config_file.value("conn1_disable_port", conf.conn1_disable_port); + conf.conn2_disable_port = config_file.value("conn2_disable_port", conf.conn2_disable_port); + conf.conn1_feedback_active_low = config_file.value("conn1_feedback_active_low", conf.conn1_feedback_active_low); + conf.conn2_feedback_active_low = config_file.value("conn2_feedback_active_low", conf.conn2_feedback_active_low); + conf.conn1_feedback_pull = config_file.value("conn1_feedback_pull", conf.conn1_feedback_pull); + conf.conn2_feedback_pull = config_file.value("conn2_feedback_pull", conf.conn2_feedback_pull); + conf.conn1_dc = config_file.value("conn1_dc", conf.conn1_dc); + conf.conn2_dc = config_file.value("conn2_dc", conf.conn2_dc); + conf.conn1_disable_port = config_file.value("conn1_disable_port", conf.conn1_disable_port); + conf.conn2_disable_port = config_file.value("conn2_disable_port", conf.conn2_disable_port); + conf.conn1_has_socket = config_file.value("conn1_has_socket", conf.conn1_has_socket); + conf.conn2_has_socket = config_file.value("conn2_has_socket", conf.conn2_has_socket); } \ No newline at end of file diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/evConfig.h b/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/evConfig.h index 215d6f4cb3..deec3c3a6f 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/evConfig.h +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/evConfig.h @@ -41,8 +41,8 @@ class evConfig { bool conn2_dc = false; int reset_gpio_bank = 1; int reset_gpio_pin = 23; - int conn1_motor_lock_type = 2; - int conn2_motor_lock_type = 2; + int conn1_motor_lock_type = -1; + int conn2_motor_lock_type = -1; bool conn1_gpio_stop_button_enabled = false; std::string conn1_gpio_stop_button_bank = "gpiochip1"; int conn1_gpio_stop_button_pin = 36; @@ -51,6 +51,12 @@ class evConfig { std::string conn2_gpio_stop_button_bank = "gpiochip1"; int conn2_gpio_stop_button_pin = 37; bool conn2_gpio_stop_button_invert = false; + bool conn1_disable_port = false; + bool conn2_disable_port = false; + bool conn1_feedback_active_low = true; + bool conn2_feedback_active_low = true; + int conn1_feedback_pull = 2; + int conn2_feedback_pull = 2; } conf; evConfig(); diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/evSerial.cpp b/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/evSerial.cpp index 8deb536f75..4f0af44258 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/evSerial.cpp +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/evSerial.cpp @@ -71,6 +71,10 @@ bool evSerial::open_device(const char* device, int _baud) { return set_serial_attributes(); } +void evSerial::flush_buffers() { + tcflush(fd, TCIOFLUSH); +} + bool evSerial::set_serial_attributes() { struct termios tty; if (tcgetattr(fd, &tty) != 0) { @@ -154,6 +158,7 @@ bool evSerial::handle_McuToEverest_packet(uint8_t* buf, int len) { case McuToEverest_keep_alive_tag: signal_keep_alive(msg_in.payload.keep_alive); + last_keep_alive_lo_timestamp = date::utc_clock::now(); break; case McuToEverest_cp_state_tag: @@ -243,6 +248,7 @@ void evSerial::cobs_decode_byte(uint8_t byte) { void evSerial::run() { read_thread_handle = std::thread(&evSerial::read_thread, this); timeout_detection_thread_handle = std::thread(&evSerial::timeout_detection_thread, this); + last_keep_alive_lo_timestamp = date::utc_clock::now(); } void evSerial::timeout_detection_thread() { @@ -374,11 +380,11 @@ bool evSerial::reset(const int reset_pin) { forced_reset = true; if (reset_pin > 0) { - printf("Hard reset\n"); + EVLOG_info << "Hard-resetting PhyVerso"; auto bsl_gpio = BSL_GPIO({.bank = 1, .pin = 12}, // BSL pins are unused here so keep defaults {.bank = static_cast(verso_config.conf.reset_gpio_bank), .pin = static_cast(verso_config.conf.reset_gpio_pin)}); - bsl_gpio.hard_reset(); + bsl_gpio.hard_reset(25); } else { // Try to soft reset phyVERSO controller to be in a known state EverestToMcu msg_out = EverestToMcu_init_default; @@ -387,22 +393,11 @@ bool evSerial::reset(const int reset_pin) { link_write(&msg_out); } - bool success = false; - - // Wait for reset done message from uC - for (int i = 0; i < 20; i++) { - if (reset_done_flag) { - success = true; - break; - } - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - } - - // Reset flag to detect run time spurious resets of uC from now on - reset_done_flag = false; - forced_reset = false; + bool success = true; // send some dummy packets to resync COBS etc. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + cobs_decode_reset(); keep_alive(); keep_alive(); keep_alive(); diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/evSerial.h b/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/evSerial.h index 3192b13664..56f32f4e14 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/evSerial.h +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/evSerial.h @@ -28,6 +28,7 @@ class evSerial { bool is_open() { return fd > 0; }; + void flush_buffers(); void read_thread(); void run(); diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/protobuf/phyverso.options b/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/protobuf/phyverso.options index 886456c89f..8c7479bba6 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/protobuf/phyverso.options +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/protobuf/phyverso.options @@ -1,3 +1,5 @@ KeepAlive.sw_version_string max_length:50 FanState.fan_id int_size:IS_8 -FanState.rpm int_size:IS_16 \ No newline at end of file +FanState.rpm int_size:IS_16 +BootConfigResponse.chargeport_config max_count:2 +BootConfigResponse.chargeport_config fixed_count:true \ No newline at end of file diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/protobuf/phyverso.pb.c b/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/protobuf/phyverso.pb.c index bee5708af6..576fc793e6 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/protobuf/phyverso.pb.c +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/protobuf/phyverso.pb.c @@ -33,6 +33,9 @@ PB_BIND(BootConfigRequest, BootConfigRequest, AUTO) PB_BIND(BootConfigResponse, BootConfigResponse, AUTO) +PB_BIND(ChargePortConfig, ChargePortConfig, AUTO) + + PB_BIND(ConfigMotorLockType, ConfigMotorLockType, AUTO) @@ -47,3 +50,5 @@ PB_BIND(RcdCommand, RcdCommand, AUTO) + + diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/protobuf/phyverso.pb.h b/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/protobuf/phyverso.pb.h index a84ec5f9cb..642bcb149d 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/protobuf/phyverso.pb.h +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/protobuf/phyverso.pb.h @@ -36,7 +36,9 @@ typedef enum _PpState { typedef enum _LockState { LockState_UNDEFINED = 0, LockState_UNLOCKED = 1, - LockState_LOCKED = 2 + LockState_LOCKED = 2, + LockState_LOCKING = 3, + LockState_UNLOCKING = 4 } LockState; typedef enum _CoilType { @@ -48,6 +50,18 @@ typedef enum _CoilType { CoilType_COIL_DC3 = 4 } CoilType; +typedef enum _ChargePortType { + ChargePortType_DISABLED = 0, + ChargePortType_AC = 1, + ChargePortType_DC = 2 +} ChargePortType; + +typedef enum _GpioPull { + GpioPull_NONE = 0, + GpioPull_UP = 1, + GpioPull_DOWN = 2 +} GpioPull; + typedef enum _ConfigHardwareRevision { ConfigHardwareRevision_HW_REV_UNKNOWN = 0, ConfigHardwareRevision_HW_REV_A = 1, @@ -56,8 +70,10 @@ typedef enum _ConfigHardwareRevision { typedef enum _MotorLockType { MotorLockType_MOTOR_LOCK_UNKNOWN = 0, - MotorLockType_MOTOR_LOCK_QWELLO = 1, - MotorLockType_MOTOR_LOCK_DEBUG_VALEO_HVAC = 2 + MotorLockType_MOTOR_LOCK_HELLA = 1, + MotorLockType_MOTOR_LOCK_DEBUG_VALEO_HVAC = 2, + /* add additional locks here */ + MotorLockType_MOTOR_LOCK_NONE = -1 } MotorLockType; /* Struct definitions */ @@ -68,6 +84,9 @@ typedef struct _ErrorFlags { bool ventilation_not_available; bool connector_lock_failed; bool cp_signal_fault; + bool heartbeat_timeout; + bool coil_feedback_diverges; + bool pp_signal_fault; } ErrorFlags; typedef struct _KeepAlive { @@ -75,6 +94,7 @@ typedef struct _KeepAlive { uint32_t hw_type; uint32_t hw_revision; char sw_version_string[51]; + bool configuration_done; } KeepAlive; typedef struct _Telemetry { @@ -117,15 +137,22 @@ typedef struct _McuToEverest { } McuToEverest; typedef struct _ConfigMotorLockType { - MotorLockType type; + MotorLockType type; /* additional lock specific options could be added here later + will still keep this in place even if it only holds the type enum at the moment */ } ConfigMotorLockType; +typedef struct _ChargePortConfig { + ChargePortType type; + bool feedback_active_low; + GpioPull feedback_pull; + bool has_lock; + ConfigMotorLockType lock; + bool has_socket; +} ChargePortConfig; + typedef struct _BootConfigResponse { ConfigHardwareRevision hw_rev; - bool has_lock_1; - ConfigMotorLockType lock_1; - bool has_lock_2; - ConfigMotorLockType lock_2; + ChargePortConfig chargeport_config[2]; } BootConfigResponse; typedef struct _RcdCommand { @@ -169,18 +196,26 @@ extern "C" { #define _PpState_ARRAYSIZE ((PpState)(PpState_STATE_FAULT+1)) #define _LockState_MIN LockState_UNDEFINED -#define _LockState_MAX LockState_LOCKED -#define _LockState_ARRAYSIZE ((LockState)(LockState_LOCKED+1)) +#define _LockState_MAX LockState_UNLOCKING +#define _LockState_ARRAYSIZE ((LockState)(LockState_UNLOCKING+1)) #define _CoilType_MIN CoilType_COIL_UNKNOWN #define _CoilType_MAX CoilType_COIL_DC3 #define _CoilType_ARRAYSIZE ((CoilType)(CoilType_COIL_DC3+1)) +#define _ChargePortType_MIN ChargePortType_DISABLED +#define _ChargePortType_MAX ChargePortType_DC +#define _ChargePortType_ARRAYSIZE ((ChargePortType)(ChargePortType_DC+1)) + +#define _GpioPull_MIN GpioPull_NONE +#define _GpioPull_MAX GpioPull_DOWN +#define _GpioPull_ARRAYSIZE ((GpioPull)(GpioPull_DOWN+1)) + #define _ConfigHardwareRevision_MIN ConfigHardwareRevision_HW_REV_UNKNOWN #define _ConfigHardwareRevision_MAX ConfigHardwareRevision_HW_REV_B #define _ConfigHardwareRevision_ARRAYSIZE ((ConfigHardwareRevision)(ConfigHardwareRevision_HW_REV_B+1)) -#define _MotorLockType_MIN MotorLockType_MOTOR_LOCK_UNKNOWN +#define _MotorLockType_MIN MotorLockType_MOTOR_LOCK_NONE #define _MotorLockType_MAX MotorLockType_MOTOR_LOCK_DEBUG_VALEO_HVAC #define _MotorLockType_ARRAYSIZE ((MotorLockType)(MotorLockType_MOTOR_LOCK_DEBUG_VALEO_HVAC+1)) @@ -199,6 +234,9 @@ extern "C" { #define BootConfigResponse_hw_rev_ENUMTYPE ConfigHardwareRevision +#define ChargePortConfig_type_ENUMTYPE ChargePortType +#define ChargePortConfig_feedback_pull_ENUMTYPE GpioPull + #define ConfigMotorLockType_type_ENUMTYPE MotorLockType @@ -206,24 +244,26 @@ extern "C" { /* Initializer values for message structs */ #define EverestToMcu_init_default {0, {KeepAlive_init_default}, 0} #define McuToEverest_init_default {0, {KeepAlive_init_default}, 0} -#define ErrorFlags_init_default {0, 0, 0, 0, 0, 0} -#define KeepAlive_init_default {0, 0, 0, ""} +#define ErrorFlags_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0} +#define KeepAlive_init_default {0, 0, 0, "", 0} #define Telemetry_init_default {0, 0} #define FanState_init_default {0, 0, 0, 0} #define CoilState_init_default {_CoilType_MIN, 0} #define BootConfigRequest_init_default {0} -#define BootConfigResponse_init_default {_ConfigHardwareRevision_MIN, false, ConfigMotorLockType_init_default, false, ConfigMotorLockType_init_default} +#define BootConfigResponse_init_default {_ConfigHardwareRevision_MIN, {ChargePortConfig_init_default, ChargePortConfig_init_default}} +#define ChargePortConfig_init_default {_ChargePortType_MIN, 0, _GpioPull_MIN, false, ConfigMotorLockType_init_default, 0} #define ConfigMotorLockType_init_default {_MotorLockType_MIN} #define RcdCommand_init_default {0, 0} #define EverestToMcu_init_zero {0, {KeepAlive_init_zero}, 0} #define McuToEverest_init_zero {0, {KeepAlive_init_zero}, 0} -#define ErrorFlags_init_zero {0, 0, 0, 0, 0, 0} -#define KeepAlive_init_zero {0, 0, 0, ""} +#define ErrorFlags_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0} +#define KeepAlive_init_zero {0, 0, 0, "", 0} #define Telemetry_init_zero {0, 0} #define FanState_init_zero {0, 0, 0, 0} #define CoilState_init_zero {_CoilType_MIN, 0} #define BootConfigRequest_init_zero {0} -#define BootConfigResponse_init_zero {_ConfigHardwareRevision_MIN, false, ConfigMotorLockType_init_zero, false, ConfigMotorLockType_init_zero} +#define BootConfigResponse_init_zero {_ConfigHardwareRevision_MIN, {ChargePortConfig_init_zero, ChargePortConfig_init_zero}} +#define ChargePortConfig_init_zero {_ChargePortType_MIN, 0, _GpioPull_MIN, false, ConfigMotorLockType_init_zero, 0} #define ConfigMotorLockType_init_zero {_MotorLockType_MIN} #define RcdCommand_init_zero {0, 0} @@ -234,10 +274,14 @@ extern "C" { #define ErrorFlags_ventilation_not_available_tag 4 #define ErrorFlags_connector_lock_failed_tag 5 #define ErrorFlags_cp_signal_fault_tag 6 +#define ErrorFlags_heartbeat_timeout_tag 7 +#define ErrorFlags_coil_feedback_diverges_tag 8 +#define ErrorFlags_pp_signal_fault_tag 9 #define KeepAlive_time_stamp_tag 1 #define KeepAlive_hw_type_tag 2 #define KeepAlive_hw_revision_tag 3 #define KeepAlive_sw_version_string_tag 6 +#define KeepAlive_configuration_done_tag 7 #define Telemetry_cp_voltage_hi_tag 1 #define Telemetry_cp_voltage_lo_tag 2 #define FanState_fan_id_tag 1 @@ -258,9 +302,13 @@ extern "C" { #define McuToEverest_config_request_tag 11 #define McuToEverest_connector_tag 6 #define ConfigMotorLockType_type_tag 1 +#define ChargePortConfig_type_tag 1 +#define ChargePortConfig_feedback_active_low_tag 2 +#define ChargePortConfig_feedback_pull_tag 3 +#define ChargePortConfig_lock_tag 4 +#define ChargePortConfig_has_socket_tag 5 #define BootConfigResponse_hw_rev_tag 1 -#define BootConfigResponse_lock_1_tag 2 -#define BootConfigResponse_lock_2_tag 3 +#define BootConfigResponse_chargeport_config_tag 6 #define RcdCommand_test_tag 1 #define RcdCommand_reset_tag 2 #define EverestToMcu_keep_alive_tag 1 @@ -321,7 +369,10 @@ X(a, STATIC, SINGULAR, BOOL, rcd_selftest_failed, 2) \ X(a, STATIC, SINGULAR, BOOL, rcd_triggered, 3) \ X(a, STATIC, SINGULAR, BOOL, ventilation_not_available, 4) \ X(a, STATIC, SINGULAR, BOOL, connector_lock_failed, 5) \ -X(a, STATIC, SINGULAR, BOOL, cp_signal_fault, 6) +X(a, STATIC, SINGULAR, BOOL, cp_signal_fault, 6) \ +X(a, STATIC, SINGULAR, BOOL, heartbeat_timeout, 7) \ +X(a, STATIC, SINGULAR, BOOL, coil_feedback_diverges, 8) \ +X(a, STATIC, SINGULAR, BOOL, pp_signal_fault, 9) #define ErrorFlags_CALLBACK NULL #define ErrorFlags_DEFAULT NULL @@ -329,7 +380,8 @@ X(a, STATIC, SINGULAR, BOOL, cp_signal_fault, 6) X(a, STATIC, SINGULAR, UINT32, time_stamp, 1) \ X(a, STATIC, SINGULAR, UINT32, hw_type, 2) \ X(a, STATIC, SINGULAR, UINT32, hw_revision, 3) \ -X(a, STATIC, SINGULAR, STRING, sw_version_string, 6) +X(a, STATIC, SINGULAR, STRING, sw_version_string, 6) \ +X(a, STATIC, SINGULAR, BOOL, configuration_done, 7) #define KeepAlive_CALLBACK NULL #define KeepAlive_DEFAULT NULL @@ -360,15 +412,23 @@ X(a, STATIC, SINGULAR, BOOL, coil_state, 2) #define BootConfigResponse_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UENUM, hw_rev, 1) \ -X(a, STATIC, OPTIONAL, MESSAGE, lock_1, 2) \ -X(a, STATIC, OPTIONAL, MESSAGE, lock_2, 3) +X(a, STATIC, FIXARRAY, MESSAGE, chargeport_config, 6) #define BootConfigResponse_CALLBACK NULL #define BootConfigResponse_DEFAULT NULL -#define BootConfigResponse_lock_1_MSGTYPE ConfigMotorLockType -#define BootConfigResponse_lock_2_MSGTYPE ConfigMotorLockType +#define BootConfigResponse_chargeport_config_MSGTYPE ChargePortConfig + +#define ChargePortConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, type, 1) \ +X(a, STATIC, SINGULAR, BOOL, feedback_active_low, 2) \ +X(a, STATIC, SINGULAR, UENUM, feedback_pull, 3) \ +X(a, STATIC, OPTIONAL, MESSAGE, lock, 4) \ +X(a, STATIC, SINGULAR, BOOL, has_socket, 5) +#define ChargePortConfig_CALLBACK NULL +#define ChargePortConfig_DEFAULT NULL +#define ChargePortConfig_lock_MSGTYPE ConfigMotorLockType #define ConfigMotorLockType_FIELDLIST(X, a) \ -X(a, STATIC, SINGULAR, UENUM, type, 1) +X(a, STATIC, SINGULAR, ENUM, type, 1) #define ConfigMotorLockType_CALLBACK NULL #define ConfigMotorLockType_DEFAULT NULL @@ -387,6 +447,7 @@ extern const pb_msgdesc_t FanState_msg; extern const pb_msgdesc_t CoilState_msg; extern const pb_msgdesc_t BootConfigRequest_msg; extern const pb_msgdesc_t BootConfigResponse_msg; +extern const pb_msgdesc_t ChargePortConfig_msg; extern const pb_msgdesc_t ConfigMotorLockType_msg; extern const pb_msgdesc_t RcdCommand_msg; @@ -400,19 +461,21 @@ extern const pb_msgdesc_t RcdCommand_msg; #define CoilState_fields &CoilState_msg #define BootConfigRequest_fields &BootConfigRequest_msg #define BootConfigResponse_fields &BootConfigResponse_msg +#define ChargePortConfig_fields &ChargePortConfig_msg #define ConfigMotorLockType_fields &ConfigMotorLockType_msg #define RcdCommand_fields &RcdCommand_msg /* Maximum encoded size of messages (where known) */ #define BootConfigRequest_size 0 -#define BootConfigResponse_size 10 +#define BootConfigResponse_size 48 +#define ChargePortConfig_size 21 #define CoilState_size 4 -#define ConfigMotorLockType_size 2 -#define ErrorFlags_size 12 -#define EverestToMcu_size 83 +#define ConfigMotorLockType_size 11 +#define ErrorFlags_size 18 +#define EverestToMcu_size 85 #define FanState_size 15 -#define KeepAlive_size 70 -#define McuToEverest_size 83 +#define KeepAlive_size 72 +#define McuToEverest_size 85 #define PHYVERSO_PB_H_MAX_SIZE EverestToMcu_size #define RcdCommand_size 4 #define Telemetry_size 12 diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/protobuf/phyverso.proto b/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/protobuf/phyverso.proto index 196a04f018..c3e15c3d68 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/protobuf/phyverso.proto +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/phyverso_mcu_comms/protobuf/phyverso.proto @@ -53,6 +53,9 @@ message ErrorFlags { bool ventilation_not_available = 4; bool connector_lock_failed = 5; bool cp_signal_fault = 6; + bool heartbeat_timeout = 7; + bool coil_feedback_diverges = 8; + bool pp_signal_fault = 9; } enum ResetReason { @@ -65,6 +68,7 @@ message KeepAlive { uint32 hw_type = 2; uint32 hw_revision = 3; string sw_version_string = 6; + bool configuration_done = 7; } message Telemetry { @@ -92,6 +96,8 @@ enum LockState { UNDEFINED = 0; UNLOCKED = 1; LOCKED = 2; + LOCKING = 3; + UNLOCKING = 4; } message CoilState { @@ -114,12 +120,33 @@ message BootConfigRequest { message BootConfigResponse { ConfigHardwareRevision hw_rev = 1; - ConfigMotorLockType lock_1 = 2; - ConfigMotorLockType lock_2 = 3; + repeated ChargePortConfig chargeport_config = 6; +} + +message ChargePortConfig { + ChargePortType type = 1; + bool feedback_active_low = 2; + GpioPull feedback_pull = 3; + ConfigMotorLockType lock = 4; + bool has_socket = 5; +} + +enum ChargePortType { + DISABLED = 0; + AC = 1; + DC = 2; +} + +enum GpioPull { + NONE = 0; + UP = 1; + DOWN = 2; } message ConfigMotorLockType { MotorLockType type = 1; + // additional lock specific options could be added here later + // will still keep this in place even if it only holds the type enum at the moment } enum ConfigHardwareRevision { @@ -130,8 +157,10 @@ enum ConfigHardwareRevision { enum MotorLockType { MOTOR_LOCK_UNKNOWN = 0; - MOTOR_LOCK_QWELLO = 1; + MOTOR_LOCK_HELLA = 1; MOTOR_LOCK_DEBUG_VALEO_HVAC = 2; + // add additional locks here + MOTOR_LOCK_NONE = -1; } message RcdCommand { diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/rcd_1/ac_rcdImpl.hpp b/modules/HardwareDrivers/EVSE/PhyVersoBSP/rcd_1/ac_rcdImpl.hpp index 729876866d..c192d6d279 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/rcd_1/ac_rcdImpl.hpp +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/rcd_1/ac_rcdImpl.hpp @@ -49,7 +49,7 @@ class ac_rcdImpl : public ac_rcdImplBase { // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 // insert your private definitions here - ErrorFlags last_error_flags; + ErrorFlags last_error_flags{false}; // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 }; diff --git a/modules/HardwareDrivers/EVSE/PhyVersoBSP/rcd_2/ac_rcdImpl.hpp b/modules/HardwareDrivers/EVSE/PhyVersoBSP/rcd_2/ac_rcdImpl.hpp index e4a0ea442b..8a749f07e8 100644 --- a/modules/HardwareDrivers/EVSE/PhyVersoBSP/rcd_2/ac_rcdImpl.hpp +++ b/modules/HardwareDrivers/EVSE/PhyVersoBSP/rcd_2/ac_rcdImpl.hpp @@ -49,7 +49,7 @@ class ac_rcdImpl : public ac_rcdImplBase { // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 // insert your private definitions here - ErrorFlags last_error_flags; + ErrorFlags last_error_flags{false}; // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 }; diff --git a/modules/HardwareDrivers/PowerSupplies/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/CMakeLists.txt index 1f71208e4e..94b344cb50 100644 --- a/modules/HardwareDrivers/PowerSupplies/CMakeLists.txt +++ b/modules/HardwareDrivers/PowerSupplies/CMakeLists.txt @@ -2,5 +2,6 @@ ev_add_module(DPM1000) ev_add_module(Huawei_R100040Gx) ev_add_module(Huawei_V100R023C10) ev_add_module(UUGreenPower_UR1000X0) +ev_add_module(Winline) ev_add_module(InfyPower) ev_add_module(InfyPower_BEG1K075G) diff --git a/modules/HardwareDrivers/PowerSupplies/Winline/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Winline/CMakeLists.txt new file mode 100644 index 0000000000..0c109b7595 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Winline/CMakeLists.txt @@ -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/CanBus.cpp" + "can_driver_acdc/CanPackets.cpp" + "can_driver_acdc/WinlineCanDevice.cpp" +) + +target_link_libraries(${MODULE_NAME} + PRIVATE + Pal::Sigslot + everest::io +) +# 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 diff --git a/modules/HardwareDrivers/PowerSupplies/Winline/README.md b/modules/HardwareDrivers/PowerSupplies/Winline/README.md new file mode 100644 index 0000000000..d2d6481a37 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Winline/README.md @@ -0,0 +1,83 @@ +# Winline Power Supply Driver + +## 🔧 Next Steps for Customization + +This module is now ready for CAN protocol adaptation. To implement your Winline-specific protocol: + +### 1. Update Protocol Implementation (REQUIRED) +- [ ] **Edit `can_driver_acdc/CanPackets.hpp`**: Replace all WinlineProtocol constants with your actual protocol values +- [ ] **Edit `can_driver_acdc/CanPackets.cpp`**: Update CAN ID encoding functions and packet implementations +- [ ] **Edit `can_driver_acdc/WinlineCanDevice.cpp`**: Update rx_handler logic for your specific protocol +- [ ] **Edit `main/power_supply_DCImpl.cpp`**: Update error mapping function for your error types + +### 2. Update Configuration (REQUIRED) +- [ ] **Edit `manifest.yaml`**: Update description, parameters, and metadata for your hardware +- [ ] **Test configuration**: Verify all parameters work with your Winline hardware + +### 3. Protocol Customization Areas + +**Key files prepared for protocol changes:** + +| File | What to Change | +|------|----------------| +| `CanPackets.hpp` | Protocol constants in `WinlineProtocol` namespace | +| `CanPackets.cpp` | CAN ID encoding/decoding functions | +| `WinlineCanDevice.hpp` | Error enum and telemetry structure | +| `WinlineCanDevice.cpp` | Message handling and protocol logic | + +### 4. Current State + +**✅ Completed:** +- Module renamed from InfyPower to Winline +- All class names updated (WinlineCanDevice, etc.) +- All logging prefixes changed to "Winline:" +- Build system updated (CMakeLists.txt) +- EVerest integration maintained + +**🔧 Ready for protocol customization:** +- CAN packet structures (replace with your protocol) +- Protocol constants (update with your values) +- Error mapping (adapt to your error types) +- Discovery mechanism (adapt to your method) + +### 5. Testing Checklist +- [ ] **Build verification**: Ensure module compiles +- [ ] **Virtual CAN**: Test basic CAN communication with `vcan0` +- [ ] **Protocol validation**: Verify CAN ID encoding/decoding with your protocol +- [ ] **Error handling**: Test all error conditions +- [ ] **Multi-module**: Test with multiple modules if supported +- [ ] **EVerest integration**: Verify power_supply_DC interface compliance + +## Configuration Example + +```yaml +# Current Winline configuration (update for your protocol) +can_device: "can0" +module_addresses: "0,1,2" # Update addressing scheme if needed +group_address: 1 # Update for your discovery mechanism +device_connection_timeout_s: 15 # Update based on your protocol timing +conversion_efficiency_export: 0.95 +controller_address: 240 # Update for your protocol +``` + +## Development Notes + +- **Based on**: InfyPower template - proven, production-ready foundation +- **Architecture**: Single-threaded CAN communication with EVerest integration +- **Thread Safety**: Mutex protection for shared data structures +- **Error Handling**: Comprehensive error mapping to EVerest standard errors +- **Protocol Ready**: Structure prepared for easy protocol adaptation + +## Original Template + +This driver maintains the same proven architecture as the InfyPower driver: +- Robust CAN communication handling +- Comprehensive error management +- Multi-module support with current sharing +- EVerest framework integration +- Thread-safe operation + +**Ready for your Winline protocol implementation!** + +--- +*Generated from InfyPower template - customize for your Winline protocol* \ No newline at end of file diff --git a/modules/HardwareDrivers/PowerSupplies/Winline/Winline.cpp b/modules/HardwareDrivers/PowerSupplies/Winline/Winline.cpp new file mode 100644 index 0000000000..9be4c10341 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Winline/Winline.cpp @@ -0,0 +1,37 @@ +/* + * Licensor: Pionix GmbH, 2024 + * License: BaseCamp - License Version 1.0 + * + * Licensed under the terms and conditions of the BaseCamp License contained in the "LICENSE" file, also available + * under: https://pionix.com/pionix-license-terms + * You may not use this file/code except in compliance with said License. + */ + +#include "Winline.hpp" + +namespace module { + +void Winline::init() { + acdc = std::make_unique(); + acdc->set_can_device(config.can_device); + acdc->set_config_values(config.module_addresses, config.group_address, config.device_connection_timeout_s, + config.controller_address, config.power_state_grace_period_ms, config.altitude_setting_m, + config.input_mode, config.module_current_limit_point); + + // Set altitude on all modules + acdc->set_altitude_all_modules(); + + // Set input mode on all modules + acdc->set_input_mode_all_modules(); + + // Set current limit point on all modules + acdc->set_current_limit_point_all_modules(); + + invoke_init(*p_main); +} + +void Winline::ready() { + invoke_ready(*p_main); +} + +} // namespace module diff --git a/modules/HardwareDrivers/PowerSupplies/Winline/Winline.hpp b/modules/HardwareDrivers/PowerSupplies/Winline/Winline.hpp new file mode 100644 index 0000000000..6678ccfe69 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Winline/Winline.hpp @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef WINLINE_HPP +#define WINLINE_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 2 +// + +#include "ld-ev.hpp" + +// headers for provided interface implementations +#include + +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 +#include "can_driver_acdc/WinlineCanDevice.hpp" +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 + +namespace module { + +struct Conf { + std::string can_device; + std::string module_addresses; + int group_address; + int device_connection_timeout_s; + double conversion_efficiency_export; + int controller_address; + double min_export_voltage_V; + double max_export_voltage_V; + double min_export_current_A; + double max_export_current_A; + int power_state_grace_period_ms; + double current_regulation_tolerance_A; + double peak_current_ripple_A; + int altitude_setting_m; + std::string input_mode; + double module_current_limit_point; +}; + +class Winline : public Everest::ModuleBase { +public: + Winline() = delete; + Winline(const ModuleInfo& info, std::unique_ptr p_main, Conf& config) : + ModuleBase(info), p_main(std::move(p_main)), config(config){}; + + const std::unique_ptr p_main; + const Conf& config; + + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + std::unique_ptr 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 // WINLINE_HPP diff --git a/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/CanBus.cpp b/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/CanBus.cpp new file mode 100644 index 0000000000..00fb3f3aa4 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/CanBus.cpp @@ -0,0 +1,139 @@ +/* + * Licensor: Pionix GmbH, 2024 + * License: BaseCamp - License Version 1.0 + * + * Licensed under the terms and conditions of the BaseCamp License contained in the "LICENSE" file, also available + * under: https://pionix.com/pionix-license-terms + * You may not use this file/code except in compliance with said License. + */ + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include + +#include "CanBus.hpp" +#include + +using namespace std::chrono_literals; + +namespace { +// Timer configuration constants +constexpr auto CAN_RECOVERY_TIMER_INTERVAL = 1000ms; +constexpr auto CAN_POLL_STATUS_TIMER_INTERVAL = 1000ms; +} // namespace + +CanBus::CanBus() : exit_rx_thread{false}, can_bus(nullptr) { +} + +CanBus::~CanBus() { + close_device(); +} + +bool CanBus::open_device(const std::string& dev) { + can_bus = std::make_unique(dev); + can_bus->set_rx_handler([&](auto const& pl, auto&) { + uint32_t can_id = pl.get_can_id(); + 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(); }); + rx_thread_handle = std::thread(&CanBus::rx_thread, this); + return true; +} + +bool CanBus::close_device() { + if (!can_bus) { + return true; // Already closed + } + + EVLOG_info << "Closing CAN device"; + + // Stop the RX thread first + exit_rx_thread = true; + rx_thread_cv.notify_one(); + if (rx_thread_handle.joinable()) { + rx_thread_handle.join(); + } + + // Unregister event handlers (this stops timers and cleans up any pending events) + ev_handler.unregister_event_handler(&recovery_timer); + ev_handler.unregister_event_handler(&poll_status_timer); + ev_handler.unregister_event_handler(can_bus.get()); + + // Close CAN socket + can_bus.reset(); + + // Reset error state + on_error.store(false); + + EVLOG_info << "CAN device closed successfully"; + return true; +} + +void CanBus::rx_thread() { + EVLOG_info << "Starting CAN RX thread" << std::endl; + while (!exit_rx_thread) { + ev_handler.poll(); + } +} + +static std::string bytes_to_hex(const std::vector& bytes) { + std::stringstream ss; + ss << std::hex << std::setfill('0'); + for (size_t i = 0; i < bytes.size(); ++i) { + ss << std::setw(2) << static_cast(bytes[i]); + } + return ss.str(); +} + +bool CanBus::_tx(uint32_t can_id, const std::vector& payload) { + // EVLOG_debug << "CAN frame sent using ID:" << std::hex << can_id << "#" << bytes_to_hex(payload); + + // Validate payload size for CAN protocol compliance + if (payload.size() > 8) { + EVLOG_error << "CAN payload too large (" << payload.size() << " bytes), max 8 bytes allowed"; + return false; + } + + // Winline protocol uses 29-bit extended CAN IDs, so we need to set the extended frame flag + everest::lib::io::can::can_dataset data; + data.set_can_id_with_flags(can_id | CAN_EFF_FLAG); + data.payload = payload; + + if (on_error.load()) { + EVLOG_error << "CAN error detected, not sending frame"; + return false; + } + return can_bus->tx(data); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/CanBus.hpp b/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/CanBus.hpp new file mode 100644 index 0000000000..8334404e58 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/CanBus.hpp @@ -0,0 +1,50 @@ +/* + * Licensor: Pionix GmbH, 2024 + * License: BaseCamp - License Version 1.0 + * + * Licensed under the terms and conditions of the BaseCamp License contained in the "LICENSE" file, also available + * under: https://pionix.com/pionix-license-terms + * You may not use this file/code except in compliance with said License. + */ + +#ifndef CAN_BUS_HPP +#define CAN_BUS_HPP + +#include "CanPackets.hpp" +#include +#include +#include +#include +#include + +#include +#include +#include + +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& payload) = 0; + virtual void poll_status_handler() = 0; + bool _tx(uint32_t can_id, const std::vector& payload); + +private: + std::unique_ptr 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; + std::atomic_bool exit_rx_thread; + std::thread rx_thread_handle; + std::condition_variable rx_thread_cv; + void rx_thread(); +}; + +#endif // CAN_BUS_HPP diff --git a/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/CanPackets.cpp b/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/CanPackets.cpp new file mode 100644 index 0000000000..7cf78984e4 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/CanPackets.cpp @@ -0,0 +1,590 @@ +/* + * Licensor: Pionix GmbH, 2024 + * License: BaseCamp - License Version 1.0 + * + * Licensed under the terms and conditions of the BaseCamp License contained in the "LICENSE" file, also available + * under: https://pionix.com/pionix-license-terms + * You may not use this file/code except in compliance with said License. + */ + +#include "CanPackets.hpp" +#include "Conversions.hpp" +#include + +#include +#include +#include +#include + +namespace can_packet_acdc { + +namespace { +// Winline CAN ID bit positions (29-bit extended CAN ID) +// Bits 31-29: Not used (always 0) +// Bits 28-20: PROTNO (9 bits) = 0x060 +// Bit 19: PTP (1 bit) +// Bits 18-11: DSTADDR (8 bits) +// Bits 10-3: SRCADDR (8 bits) +// Bits 2-0: Group (3 bits) +} + +// Winline CAN ID encoding/decoding functions +uint32_t encode_can_id(uint8_t source_address, uint8_t destination_address, uint8_t group_number, bool point_to_point) { + uint32_t id = 0; + + // Bits 2-0: Group number (3 bits) + id |= (group_number & WinlineProtocol::GROUP_MASK) << WinlineProtocol::GROUP_SHIFT; + + // Bits 10-3: Source Address (8 bits) + id |= (source_address & WinlineProtocol::ADDRESS_MASK) << WinlineProtocol::SRCADDR_SHIFT; + + // Bits 18-11: Destination Address (8 bits) + id |= (destination_address & WinlineProtocol::ADDRESS_MASK) << WinlineProtocol::DSTADDR_SHIFT; + + // Bit 19: Point-to-point flag (1 bit) + id |= (point_to_point ? 1 : 0) << WinlineProtocol::PTP_SHIFT; + + // Bits 28-20: Protocol number (9 bits) - always 0x060 for Winline + id |= (WinlineProtocol::PROTNO & WinlineProtocol::PROTNO_MASK) << WinlineProtocol::PROTNO_SHIFT; + + return id; +} + +uint8_t destination_address_from_can_id(uint32_t id) { + return (id >> WinlineProtocol::DSTADDR_SHIFT) & WinlineProtocol::ADDRESS_MASK; +} + +uint8_t source_address_from_can_id(uint32_t id) { + return (id >> WinlineProtocol::SRCADDR_SHIFT) & WinlineProtocol::ADDRESS_MASK; +} + +uint8_t group_number_from_can_id(uint32_t id) { + return (id >> WinlineProtocol::GROUP_SHIFT) & WinlineProtocol::GROUP_MASK; +} + +bool point_to_point_from_can_id(uint32_t id) { + return ((id >> WinlineProtocol::PTP_SHIFT) & WinlineProtocol::PTP_MASK) != 0; +} + +uint16_t protocol_number_from_can_id(uint32_t id) { + return (id >> WinlineProtocol::PROTNO_SHIFT) & WinlineProtocol::PROTNO_MASK; +} + +// Command building helpers for Winline protocol +uint32_t build_read_command_id(uint8_t source_address, uint8_t destination_address, uint8_t group_number, + bool point_to_point) { + return encode_can_id(source_address, destination_address, group_number, point_to_point); +} + +uint32_t build_set_command_id(uint8_t source_address, uint8_t destination_address, uint8_t group_number, + bool point_to_point) { + return encode_can_id(source_address, destination_address, group_number, point_to_point); +} + +// Command frame builders for Winline register-based communication +std::vector build_read_command(uint16_t register_number) { + std::vector data(8, 0); // Initialize 8-byte payload with zeros + + // Byte 0: Function code for READ operation + data[0] = WinlineProtocol::FUNCTION_READ; + + // Byte 1: Reserved (always 0x00) + data[1] = 0x00; + + // Bytes 2-3: Register number (big-endian) + data[2] = (register_number >> 8) & 0xFF; + data[3] = register_number & 0xFF; + + // Bytes 4-7: Reserved (always 0x00) + data[4] = 0x00; + data[5] = 0x00; + data[6] = 0x00; + data[7] = 0x00; + + return data; +} + +std::vector build_set_command(uint16_t register_number, const std::vector& data_payload) { + std::vector data(8, 0); // Initialize 8-byte payload with zeros + + // Byte 0: Function code for SET operation + data[0] = WinlineProtocol::FUNCTION_SET; + + // Byte 1: Reserved (always 0x00) + data[1] = 0x00; + + // Bytes 2-3: Register number (big-endian) + data[2] = (register_number >> 8) & 0xFF; + data[3] = register_number & 0xFF; + + // Bytes 4-7: Data to set (copy from payload, up to 4 bytes) + size_t copy_size = std::min(data_payload.size(), size_t(4)); + for (size_t i = 0; i < copy_size; ++i) { + data[4 + i] = data_payload[i]; + } + + return data; +} + +std::vector build_set_command_float(uint16_t register_number, float value) { + std::vector float_data; + to_raw(value, float_data); + return build_set_command(register_number, float_data); +} + +std::vector build_set_command_integer(uint16_t register_number, uint32_t value) { + std::vector int_data; + to_raw(value, int_data); + return build_set_command(register_number, int_data); +} + +// packet definitions +PowerModuleStatus::PowerModuleStatus() { +} + +PowerModuleStatus::PowerModuleStatus(const std::vector& raw) { + // Size validation is handled at rx_handler level + + // Winline status parsing based on Chart 2 in protocol document + // The status is a 32-bit integer returned from register 0x0040 + // We expect the response format: [DataType|ErrorCode|Register|StatusData] + // For now, assume the raw data contains the 32-bit status value in bytes 4-7 + + // Check if we have enough data for standardized response format + if (raw.size() >= 8) { + uint8_t data_type = from_raw(raw, 0); + uint8_t error_code = from_raw(raw, 1); + + // Verify this is a valid status response + if (data_type == WinlineProtocol::DATA_TYPE_INTEGER && error_code == WinlineProtocol::ERROR_NORMAL) { + // Extract 32-bit status value from bytes 4-7 + uint32_t status_value = from_raw(raw, 4); + + // Parse Winline status bits according to Chart 2 + module_fault = (status_value & (1U << 0)) != 0; // Bit 0: Module fault (red indicator) + module_protection = (status_value & (1U << 1)) != 0; // Bit 1: Module protection (yellow indicator) + // Bit 2: Reserved + sci_communication_failure = + (status_value & (1U << 3)) != 0; // Bit 3: Module internal SCI communication failure + input_mode_error = (status_value & (1U << 4)) != 0; // Bit 4: Input mode error/wiring error + input_mode_mismatch = + (status_value & (1U << 5)) != 0; // Bit 5: Input mode set by monitor doesn't match actual + // Bit 6: Reserved + dcdc_overvoltage = (status_value & (1U << 7)) != 0; // Bit 7: DCDC overvoltage + pfc_voltage_abnormal = (status_value & (1U << 8)) != 0; // Bit 8: PFC voltage abnormal + ac_overvoltage = (status_value & (1U << 9)) != 0; // Bit 9: AC overvoltage + // Bits 10-13: Reserved + ac_undervoltage = (status_value & (1U << 14)) != 0; // Bit 14: AC undervoltage + // Bit 15: Reserved + can_communication_failure = (status_value & (1U << 16)) != 0; // Bit 16: CAN communication failure + module_current_imbalance = (status_value & (1U << 17)) != 0; // Bit 17: Module current imbalance + // Bits 18-21: Reserved + dcdc_on_off_status = (status_value & (1U << 22)) != 0; // Bit 22: DCDC On/off status (0:On, 1:Off) + module_power_limiting = (status_value & (1U << 23)) != 0; // Bit 23: Module power limiting + temperature_derating = (status_value & (1U << 24)) != 0; // Bit 24: Temperature derating + ac_power_limiting = (status_value & (1U << 25)) != 0; // Bit 25: AC power limiting + // Bit 26: Reserved + fan_fault = (status_value & (1U << 27)) != 0; // Bit 27: Fan fault + dcdc_short_circuit = (status_value & (1U << 28)) != 0; // Bit 28: DCDC short circuit + // Bit 29: Reserved + dcdc_over_temperature = (status_value & (1U << 30)) != 0; // Bit 30: DCDC over temperature + dcdc_output_overvoltage = (status_value & (1U << 31)) != 0; // Bit 31: DCDC output overvoltage + } else { + EVLOG_warning << "Winline: Invalid status response - DataType: 0x" << std::hex + << static_cast(data_type) << ", ErrorCode: 0x" << static_cast(error_code); + } + } else { + EVLOG_warning << "Winline: PowerModuleStatus received insufficient data (size: " << raw.size() << ")"; + } +} + +std::ostream& operator<<(std::ostream& out, const PowerModuleStatus& self) { + out << "PowerModuleStatus: "; + + // Output active status flags with Winline naming + if (self.module_fault) + out << "module_fault "; + if (self.module_protection) + out << "module_protection "; + if (self.sci_communication_failure) + out << "sci_communication_failure "; + if (self.input_mode_error) + out << "input_mode_error "; + if (self.input_mode_mismatch) + out << "input_mode_mismatch "; + if (self.dcdc_overvoltage) + out << "dcdc_overvoltage "; + if (self.pfc_voltage_abnormal) + out << "pfc_voltage_abnormal "; + if (self.ac_overvoltage) + out << "ac_overvoltage "; + if (self.ac_undervoltage) + out << "ac_undervoltage "; + if (self.can_communication_failure) + out << "can_communication_failure "; + if (self.module_current_imbalance) + out << "module_current_imbalance "; + if (self.dcdc_on_off_status) + out << "dcdc_on_off_status "; + if (self.module_power_limiting) + out << "module_power_limiting "; + if (self.temperature_derating) + out << "temperature_derating "; + if (self.ac_power_limiting) + out << "ac_power_limiting "; + if (self.fan_fault) + out << "fan_fault "; + if (self.dcdc_short_circuit) + out << "dcdc_short_circuit "; + if (self.dcdc_over_temperature) + out << "dcdc_over_temperature "; + if (self.dcdc_output_overvoltage) + out << "dcdc_output_overvoltage "; + + return out; +} + +PowerModuleStatus::operator std::vector() const { + // For SET operations, this would create the command payload + // Status is read-only, so this returns empty vector + std::vector data; + return data; +} + +// Winline Register-Based Packet Implementations + +// READ operations +ReadVoltage::ReadVoltage() : voltage(0.0f) { +} + +ReadVoltage::ReadVoltage(const std::vector& raw) { + if (raw.size() >= 8) { + uint8_t data_type = from_raw(raw, 0); + uint8_t error_code = from_raw(raw, 1); + + if (data_type == WinlineProtocol::DATA_TYPE_FLOAT && error_code == WinlineProtocol::ERROR_NORMAL) { + voltage = from_raw(raw, 4); + } else { + EVLOG_warning << "Winline: Invalid voltage response - DataType: 0x" << std::hex + << static_cast(data_type) << ", ErrorCode: 0x" << static_cast(error_code); + } + } +} + +ReadVoltage::operator std::vector() const { + return build_read_command(REGISTER); +} + +ReadCurrent::ReadCurrent() : current(0.0f) { +} + +ReadCurrent::ReadCurrent(const std::vector& raw) { + if (raw.size() >= 8) { + uint8_t data_type = from_raw(raw, 0); + uint8_t error_code = from_raw(raw, 1); + + if (data_type == WinlineProtocol::DATA_TYPE_FLOAT && error_code == WinlineProtocol::ERROR_NORMAL) { + current = from_raw(raw, 4); + } else { + EVLOG_warning << "Winline: Invalid current response - DataType: 0x" << std::hex + << static_cast(data_type) << ", ErrorCode: 0x" << static_cast(error_code); + } + } +} + +ReadCurrent::operator std::vector() const { + return build_read_command(REGISTER); +} + +ReadGroupInfo::ReadGroupInfo() : group_number(0), dip_address(0) { +} + +ReadGroupInfo::ReadGroupInfo(const std::vector& raw) { + if (raw.size() >= 8) { + uint8_t data_type = from_raw(raw, 0); + uint8_t error_code = from_raw(raw, 1); + + if (data_type == WinlineProtocol::DATA_TYPE_INTEGER && error_code == WinlineProtocol::ERROR_NORMAL) { + // Higher 16 bits: group number, Lower 16 bits: DIP address + uint32_t group_info = from_raw(raw, 4); + group_number = (group_info >> 16) & 0xFF; + dip_address = group_info & 0xFF; + } else { + EVLOG_warning << "Winline: Invalid group info response - DataType: 0x" << std::hex + << static_cast(data_type) << ", ErrorCode: 0x" << static_cast(error_code); + } + } +} + +ReadGroupInfo::operator std::vector() const { + return build_read_command(REGISTER); +} + +ReadSerialNumber::ReadSerialNumber() : serial_number("") { +} + +ReadSerialNumber::ReadSerialNumber(uint32_t low_bytes, uint32_t high_bytes) { + // Combine high and low bytes to create serial number string + std::stringstream ss; + ss << "SN_" << std::hex << std::setfill('0') << std::setw(8) << high_bytes << std::setw(8) << low_bytes; + serial_number = ss.str(); +} + +ReadSerialNumber::operator std::vector() const { + // This packet requires reading two registers, so we return empty + // The caller should handle reading both REGISTER_LOW and REGISTER_HIGH + return {}; +} + +ReadRatedOutputPower::ReadRatedOutputPower() : power(0.0f) { +} + +ReadRatedOutputPower::ReadRatedOutputPower(const std::vector& raw) { + if (raw.size() >= 8) { + uint8_t data_type = from_raw(raw, 0); + uint8_t error_code = from_raw(raw, 1); + + if (data_type == WinlineProtocol::DATA_TYPE_FLOAT && error_code == WinlineProtocol::ERROR_NORMAL) { + power = from_raw(raw, 4); + } else { + EVLOG_warning << "Winline: Invalid power response - DataType: 0x" << std::hex << static_cast(data_type) + << ", ErrorCode: 0x" << static_cast(error_code); + } + } +} + +ReadRatedOutputPower::operator std::vector() const { + return build_read_command(REGISTER); +} + +ReadRatedOutputCurrent::ReadRatedOutputCurrent() : current(0.0f) { +} + +ReadRatedOutputCurrent::ReadRatedOutputCurrent(const std::vector& raw) { + if (raw.size() >= 8) { + uint8_t data_type = from_raw(raw, 0); + uint8_t error_code = from_raw(raw, 1); + + if (data_type == WinlineProtocol::DATA_TYPE_FLOAT && error_code == WinlineProtocol::ERROR_NORMAL) { + current = from_raw(raw, 4); + } else { + EVLOG_warning << "Winline: Invalid rated current response - DataType: 0x" << std::hex + << static_cast(data_type) << ", ErrorCode: 0x" << static_cast(error_code); + } + } +} + +ReadRatedOutputCurrent::operator std::vector() const { + return build_read_command(REGISTER); +} + +// Add missing packet constructors with standardized response parsing +ReadCurrentLimitPoint::ReadCurrentLimitPoint() : limit_point(0.0f) { +} + +ReadCurrentLimitPoint::ReadCurrentLimitPoint(const std::vector& raw) { + if (raw.size() >= 8) { + uint8_t data_type = from_raw(raw, 0); + uint8_t error_code = from_raw(raw, 1); + + if (data_type == WinlineProtocol::DATA_TYPE_FLOAT && error_code == WinlineProtocol::ERROR_NORMAL) { + limit_point = from_raw(raw, 4); + } else { + EVLOG_warning << "Winline: Invalid current limit point response - DataType: 0x" << std::hex + << static_cast(data_type) << ", ErrorCode: 0x" << static_cast(error_code); + } + } +} + +ReadCurrentLimitPoint::operator std::vector() const { + return build_read_command(REGISTER); +} + +ReadDCBoardTemperature::ReadDCBoardTemperature() : temperature(0.0f) { +} + +ReadDCBoardTemperature::ReadDCBoardTemperature(const std::vector& raw) { + if (raw.size() >= 8) { + uint8_t data_type = from_raw(raw, 0); + uint8_t error_code = from_raw(raw, 1); + + if (data_type == WinlineProtocol::DATA_TYPE_FLOAT && error_code == WinlineProtocol::ERROR_NORMAL) { + temperature = from_raw(raw, 4); + } else { + EVLOG_warning << "Winline: Invalid DC board temperature response - DataType: 0x" << std::hex + << static_cast(data_type) << ", ErrorCode: 0x" << static_cast(error_code); + } + } +} + +ReadDCBoardTemperature::operator std::vector() const { + return build_read_command(REGISTER); +} + +ReadAmbientTemperature::ReadAmbientTemperature() : temperature(0.0f) { +} + +ReadAmbientTemperature::ReadAmbientTemperature(const std::vector& raw) { + if (raw.size() >= 8) { + uint8_t data_type = from_raw(raw, 0); + uint8_t error_code = from_raw(raw, 1); + + if (data_type == WinlineProtocol::DATA_TYPE_FLOAT && error_code == WinlineProtocol::ERROR_NORMAL) { + temperature = from_raw(raw, 4); + } else { + EVLOG_warning << "Winline: Invalid ambient temperature response - DataType: 0x" << std::hex + << static_cast(data_type) << ", ErrorCode: 0x" << static_cast(error_code); + } + } +} + +ReadAmbientTemperature::operator std::vector() const { + return build_read_command(REGISTER); +} + +ReadDCDCVersion::ReadDCDCVersion() : version(0) { +} + +ReadDCDCVersion::ReadDCDCVersion(const std::vector& raw) { + if (raw.size() >= 8) { + uint8_t data_type = from_raw(raw, 0); + uint8_t error_code = from_raw(raw, 1); + + if (data_type == WinlineProtocol::DATA_TYPE_INTEGER && error_code == WinlineProtocol::ERROR_NORMAL) { + // Version is in lower 16 bits (bytes 6-7) of response + uint32_t version_data = from_raw(raw, 4); + version = static_cast(version_data & 0xFFFF); + } else { + EVLOG_warning << "Winline: Invalid DCDC version response - DataType: 0x" << std::hex + << static_cast(data_type) << ", ErrorCode: 0x" << static_cast(error_code); + } + } +} + +ReadDCDCVersion::operator std::vector() const { + return build_read_command(REGISTER); +} + +ReadPFCVersion::ReadPFCVersion() : version(0) { +} + +ReadPFCVersion::ReadPFCVersion(const std::vector& raw) { + if (raw.size() >= 8) { + uint8_t data_type = from_raw(raw, 0); + uint8_t error_code = from_raw(raw, 1); + + if (data_type == WinlineProtocol::DATA_TYPE_INTEGER && error_code == WinlineProtocol::ERROR_NORMAL) { + // Version is in lower 16 bits (bytes 6-7) of response + uint32_t version_data = from_raw(raw, 4); + version = static_cast(version_data & 0xFFFF); + } else { + EVLOG_warning << "Winline: Invalid PFC version response - DataType: 0x" << std::hex + << static_cast(data_type) << ", ErrorCode: 0x" << static_cast(error_code); + } + } +} + +ReadPFCVersion::operator std::vector() const { + return build_read_command(REGISTER); +} + +// SET operations +SetVoltage::SetVoltage(float v) : voltage(v) { +} + +SetVoltage::operator std::vector() const { + return build_set_command_float(REGISTER, voltage); +} + +SetCurrent::SetCurrent(float c) : current(c) { +} + +SetCurrent::operator std::vector() const { + // Winline requires current to be scaled by 1024 + uint32_t scaled_current = static_cast(current * WinlineProtocol::CURRENT_SCALE_FACTOR); + return build_set_command_integer(REGISTER, scaled_current); +} + +SetCurrentLimitPoint::SetCurrentLimitPoint(float limit_point) : limit_point(limit_point) { +} + +SetCurrentLimitPoint::operator std::vector() const { + // Current limit point is a float percentage (0.0 to 1.0) + return build_set_command_float(REGISTER, limit_point); +} + +SetVoltageUpperLimit::SetVoltageUpperLimit(float voltage_limit) : voltage_limit(voltage_limit) { +} + +SetVoltageUpperLimit::operator std::vector() const { + // Voltage upper limit is a direct float value in volts + return build_set_command_float(REGISTER, voltage_limit); +} + +SetPowerControl::SetPowerControl(bool power_on) : power_on(power_on) { +} + +SetPowerControl::operator std::vector() const { + uint32_t power_value = power_on ? WinlineProtocol::POWER_ON : WinlineProtocol::POWER_OFF; + return build_set_command_integer(REGISTER, power_value); +} + +SetGroupNumber::SetGroupNumber(uint8_t group_num) : group_number(group_num) { +} + +SetGroupNumber::operator std::vector() const { + // Byte 7 lower 6 bits for group number (range 0~60), other bytes are 0 + uint32_t group_value = group_number & 0x3F; // Ensure only lower 6 bits + return build_set_command_integer(REGISTER, group_value); +} + +SetAltitude::SetAltitude(uint32_t alt) : altitude(alt) { +} + +SetAltitude::operator std::vector() const { + // Clamp altitude to valid range + uint32_t clamped_altitude = + std::max(WinlineProtocol::ALTITUDE_MIN, std::min(altitude, WinlineProtocol::ALTITUDE_MAX)); + return build_set_command_integer(REGISTER, clamped_altitude); +} + +SetInputMode::SetInputMode(uint32_t mode) : mode(mode) { +} + +SetInputMode::operator std::vector() const { + return build_set_command_integer(REGISTER, mode); +} + +SetAddressMode::SetAddressMode(uint32_t addr_mode) : mode(addr_mode) { +} + +SetAddressMode::operator std::vector() const { + return build_set_command_integer(REGISTER, mode); +} + +// Error Recovery Operations +SetOvervoltageReset::SetOvervoltageReset(bool enable) : enable(enable) { +} + +SetOvervoltageReset::operator std::vector() const { + uint32_t reset_value = enable ? WinlineProtocol::RESET_ENABLE : WinlineProtocol::RESET_DISABLE; + return build_set_command_integer(REGISTER, reset_value); +} + +SetOvervoltageProtection::SetOvervoltageProtection(bool enable) : enable(enable) { +} + +SetOvervoltageProtection::operator std::vector() const { + uint32_t protection_value = enable ? WinlineProtocol::RESET_DISABLE : WinlineProtocol::RESET_ENABLE; + return build_set_command_integer(REGISTER, protection_value); +} + +SetShortCircuitReset::SetShortCircuitReset(bool enable) : enable(enable) { +} + +SetShortCircuitReset::operator std::vector() const { + uint32_t reset_value = enable ? WinlineProtocol::RESET_ENABLE : WinlineProtocol::RESET_DISABLE; + return build_set_command_integer(REGISTER, reset_value); +} + +} // namespace can_packet_acdc \ No newline at end of file diff --git a/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/CanPackets.hpp b/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/CanPackets.hpp new file mode 100644 index 0000000000..16970e3357 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/CanPackets.hpp @@ -0,0 +1,389 @@ +/* + * Licensor: Pionix GmbH, 2024 + * License: BaseCamp - License Version 1.0 + * + * Licensed under the terms and conditions of the BaseCamp License contained in the "LICENSE" file, also available + * under: https://pionix.com/pionix-license-terms + * You may not use this file/code except in compliance with said License. + */ + +#ifndef CAN_PACKETS_HPP +#define CAN_PACKETS_HPP + +#include +#include +#include +#include + +namespace WinlineProtocol { +// CAN Frame Constants +constexpr uint32_t CAN_EXTENDED_FLAG = 0x80000000U; + +// Winline Protocol Constants +constexpr uint16_t PROTNO = 0x060; // Protocol number (9 bits) - fixed for Winline +constexpr uint8_t FUNCTION_SET = 0x03; // SET operation function code +constexpr uint8_t FUNCTION_READ = 0x10; // READ operation function code + +// Address Constants +constexpr uint8_t MODULE_ADDRESS_MIN = 0x00; // Minimum module address +constexpr uint8_t MODULE_ADDRESS_MAX = 0x3F; // Maximum module address (63 modules) +constexpr uint8_t CONTROLLER_ADDRESS = 0xF0; // Default controller address +constexpr uint8_t BROADCAST_ADDR = 0xFF; // Individual broadcast address +constexpr uint8_t GROUP_BROADCAST_ADDR = 0xFE; // Group broadcast address + +// Group Constants +constexpr uint8_t GROUP_MIN = 0x00; // Minimum group number +constexpr uint8_t GROUP_MAX = 0x07; // Maximum group number (3 bits) + +// Response Error Codes (Winline Protocol Specification) +constexpr uint8_t ERROR_NORMAL = 0xF0; // Normal response - only valid response code +constexpr uint8_t ERROR_FAULT = 0xF2; // Fault response (example from protocol doc) +// Note: Per Winline spec - "F0: Normal, Others: Fault, discard frame" +// Any error code != 0xF0 indicates a fault and frame should be discarded + +// Error Recovery Operations +constexpr uint32_t RESET_ENABLE = 0x00010000; // Enable reset command value +constexpr uint32_t RESET_DISABLE = 0x00000000; // Disable reset command value + +// Response Data Types +constexpr uint8_t DATA_TYPE_FLOAT = 0x41; // Float point data type indicator +constexpr uint8_t DATA_TYPE_INTEGER = 0x42; // Integer data type indicator + +// Bit Masks for CAN ID encoding (Winline format) +constexpr uint32_t PROTNO_MASK = 0x1FF; // 9-bit mask for protocol number +constexpr uint8_t PTP_MASK = 0x01; // 1-bit mask for point-to-point flag +constexpr uint8_t GROUP_MASK = 0x07; // 3-bit mask for group number +constexpr uint8_t ADDRESS_MASK = 0xFF; // 8-bit mask for addresses + +// Bit Positions for CAN ID encoding (Winline format) +constexpr uint8_t SRCADDR_SHIFT = 3; // Bits 10-3: Source address (8 bits) +constexpr uint8_t DSTADDR_SHIFT = 11; // Bits 18-11: Destination address (8 bits) +constexpr uint8_t GROUP_SHIFT = 0; // Bits 2-0: Group number (3 bits) +constexpr uint8_t PTP_SHIFT = 19; // Bit 19: Point-to-point flag +constexpr uint8_t PROTNO_SHIFT = 20; // Bits 28-20: Protocol number + +// Winline Register Definitions +namespace Registers { +// Read-only registers (used with FUNCTION_READ) +constexpr uint16_t VOLTAGE = 0x0001; // Module output voltage (float) +constexpr uint16_t CURRENT = 0x0002; // Module output current (float) +constexpr uint16_t CURRENT_LIMIT_POINT = 0x0003; // Module current limit point (float) +constexpr uint16_t DC_BOARD_TEMPERATURE = 0x0004; // Module DC board temperature (float) +constexpr uint16_t INPUT_VOLTAGE = 0x0005; // Module input voltage (float) +constexpr uint16_t PFC_POSITIVE_VOLTAGE = 0x0008; // PFC positive half bus voltage (float) +constexpr uint16_t PFC_NEGATIVE_VOLTAGE = 0x000A; // PFC negative half bus voltage (float) +constexpr uint16_t AMBIENT_TEMPERATURE = 0x000B; // Panel ambient temperature (float) +constexpr uint16_t AC_PHASE_A_VOLTAGE = 0x000C; // AC phase A voltage (float) +constexpr uint16_t AC_PHASE_B_VOLTAGE = 0x000D; // AC phase B voltage (float) +constexpr uint16_t AC_PHASE_C_VOLTAGE = 0x000E; // AC phase C voltage (float) +constexpr uint16_t PFC_BOARD_TEMPERATURE = 0x0010; // PFC board temperature (float) +constexpr uint16_t RATED_OUTPUT_POWER = 0x0011; // Module rated output power (float) +constexpr uint16_t RATED_OUTPUT_CURRENT = 0x0012; // Module rated output current (float) +constexpr uint16_t STATUS = 0x0040; // Current alarm/status (integer) +constexpr uint16_t GROUP_INFO = 0x0043; // Group number & DIP switch address (integer) +constexpr uint16_t INPUT_POWER = 0x0048; // Input power (integer, unit: 1W) +constexpr uint16_t CURRENT_ALTITUDE = 0x004A; // Current set altitude (integer, unit: m) +constexpr uint16_t INPUT_WORKING_MODE = 0x004B; // Current input working mode (integer) +constexpr uint16_t SERIAL_NUMBER_LOW = 0x0054; // Node serial number low bytes (integer) +constexpr uint16_t SERIAL_NUMBER_HIGH = 0x0055; // Node serial number high bytes (integer) +constexpr uint16_t DCDC_VERSION = 0x0056; // DCDC version (integer) +constexpr uint16_t PFC_VERSION = 0x0057; // PFC version (integer) + +// Read/Write registers (used with FUNCTION_SET) +constexpr uint16_t SET_ALTITUDE = 0x0017; // Set working altitude (integer, 1000-5000m) +constexpr uint16_t SET_OUTPUT_CURRENT = 0x001B; // Set output current (integer, value*1024) +constexpr uint16_t SET_GROUP_NUMBER = 0x001E; // Set group number (integer) +constexpr uint16_t SET_ADDRESS_MODE = 0x001F; // Set address assignment mode (integer) +constexpr uint16_t SET_OUTPUT_VOLTAGE = 0x0021; // Set output voltage (float) +constexpr uint16_t SET_CURRENT_LIMIT_POINT = 0x0022; // Set current limit point (float) +constexpr uint16_t SET_VOLTAGE_UPPER_LIMIT = 0x0023; // Set voltage upper limit (float) +constexpr uint16_t POWER_CONTROL = 0x0030; // Power on/off control (integer) +constexpr uint16_t SET_OVERVOLTAGE_RESET = 0x0031; // Set overvoltage reset (integer) +constexpr uint16_t SET_OVERVOLTAGE_PROTECTION = 0x003E; // Set overvoltage protection permission (integer) +constexpr uint16_t SET_SHORT_CIRCUIT_RESET = 0x0044; // Set short circuit reset (integer) +constexpr uint16_t SET_INPUT_MODE = 0x0046; // Set input mode (integer) +} // namespace Registers + +// Power Control Values (for POWER_CONTROL register) +constexpr uint32_t POWER_ON = 0x00000000; // Power on value +constexpr uint32_t POWER_OFF = 0x00010000; // Power off value + +// Input Mode Values (for SET_INPUT_MODE register) +constexpr uint32_t INPUT_MODE_AC = 0x00000001; // AC input mode (default) +constexpr uint32_t INPUT_MODE_DC = 0x00000002; // DC input mode + +// Address Assignment Mode Values (for SET_ADDRESS_MODE register) +constexpr uint32_t ADDRESS_AUTO = 0x00000000; // Automatically assigned +constexpr uint32_t ADDRESS_DIP = 0x00010000; // Set by DIP switch (default) + +// Current Scaling Factor (for SET_OUTPUT_CURRENT register) +constexpr uint32_t CURRENT_SCALE_FACTOR = 1024; // Current value = actual_current * 1024 + +// Altitude Limits (for SET_ALTITUDE register) +constexpr uint32_t ALTITUDE_MIN = 1000; // Minimum altitude setting (meters) +constexpr uint32_t ALTITUDE_MAX = 5000; // Maximum altitude setting (meters) +constexpr uint32_t ALTITUDE_DEFAULT = 1000; // Default altitude setting (meters) + +// Unit Conversion Constants (legacy, may be useful for some conversions) +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) +} // namespace WinlineProtocol + +namespace can_packet_acdc { + +// Winline CAN ID encoding/decoding functions +uint32_t encode_can_id(uint8_t source_address, uint8_t destination_address, uint8_t group_number, bool point_to_point); + +uint8_t destination_address_from_can_id(uint32_t id); +uint8_t source_address_from_can_id(uint32_t id); +uint8_t group_number_from_can_id(uint32_t id); +bool point_to_point_from_can_id(uint32_t id); +uint16_t protocol_number_from_can_id(uint32_t id); + +// Command building helpers for Winline protocol +uint32_t build_read_command_id(uint8_t source_address, uint8_t destination_address, uint8_t group_number, + bool point_to_point); +uint32_t build_set_command_id(uint8_t source_address, uint8_t destination_address, uint8_t group_number, + bool point_to_point); + +// Command frame builders for Winline register-based communication +std::vector build_read_command(uint16_t register_number); +std::vector build_set_command(uint16_t register_number, const std::vector& data); +std::vector build_set_command_float(uint16_t register_number, float value); +std::vector build_set_command_integer(uint16_t register_number, uint32_t value); + +struct PowerModuleStatus { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::STATUS; + + PowerModuleStatus(); + PowerModuleStatus(const std::vector& raw); + friend std::ostream& operator<<(std::ostream& out, const PowerModuleStatus& self); + operator std::vector() const; + + // Winline status bits (based on Chart 2 in protocol document) + bool module_fault{false}; // Bit 0: Module fault (red indicator) + bool module_protection{false}; // Bit 1: Module protection (yellow indicator) + bool sci_communication_failure{false}; // Bit 3: Module internal SCI communication failure + bool input_mode_error{false}; // Bit 4: Input mode error/wiring error + bool input_mode_mismatch{false}; // Bit 5: Input mode set by monitor doesn't match actual + bool dcdc_overvoltage{false}; // Bit 7: DCDC overvoltage + bool pfc_voltage_abnormal{false}; // Bit 8: PFC voltage abnormal (imbalance/over/under) + bool ac_overvoltage{false}; // Bit 9: AC overvoltage + bool ac_undervoltage{false}; // Bit 14: AC undervoltage + bool can_communication_failure{false}; // Bit 16: CAN communication failure + bool module_current_imbalance{false}; // Bit 17: Module current imbalance + bool dcdc_on_off_status{false}; // Bit 22: DCDC On/off status (0:On, 1:Off) + bool module_power_limiting{false}; // Bit 23: Module power limiting + bool temperature_derating{false}; // Bit 24: Temperature derating + bool ac_power_limiting{false}; // Bit 25: AC power limiting + bool fan_fault{false}; // Bit 27: Fan fault + bool dcdc_short_circuit{false}; // Bit 28: DCDC short circuit + bool dcdc_over_temperature{false}; // Bit 30: DCDC over temperature + bool dcdc_output_overvoltage{false}; // Bit 31: DCDC output overvoltage +}; + +// Winline Register-Based Packet Structures + +// READ operations (Function 0x10 + Register) +struct ReadVoltage { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::VOLTAGE; + + ReadVoltage(); + ReadVoltage(const std::vector& raw); + operator std::vector() const; + float voltage{0.0f}; +}; + +struct ReadCurrent { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::CURRENT; + + ReadCurrent(); + ReadCurrent(const std::vector& raw); + operator std::vector() const; + float current{0.0f}; +}; + +struct ReadCurrentLimitPoint { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::CURRENT_LIMIT_POINT; + + ReadCurrentLimitPoint(); + ReadCurrentLimitPoint(const std::vector& raw); + operator std::vector() const; + float limit_point{0.0f}; +}; + +struct ReadDCBoardTemperature { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::DC_BOARD_TEMPERATURE; + + ReadDCBoardTemperature(); + ReadDCBoardTemperature(const std::vector& raw); + operator std::vector() const; + float temperature{0.0f}; +}; + +struct ReadAmbientTemperature { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::AMBIENT_TEMPERATURE; + + ReadAmbientTemperature(); + ReadAmbientTemperature(const std::vector& raw); + operator std::vector() const; + float temperature{0.0f}; +}; + +struct ReadRatedOutputPower { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::RATED_OUTPUT_POWER; + + ReadRatedOutputPower(); + ReadRatedOutputPower(const std::vector& raw); + operator std::vector() const; + float power{0.0f}; +}; + +struct ReadRatedOutputCurrent { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::RATED_OUTPUT_CURRENT; + + ReadRatedOutputCurrent(); + ReadRatedOutputCurrent(const std::vector& raw); + operator std::vector() const; + float current{0.0f}; +}; + +struct ReadGroupInfo { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::GROUP_INFO; + + ReadGroupInfo(); + ReadGroupInfo(const std::vector& raw); + operator std::vector() const; + uint8_t group_number{0}; + uint8_t dip_address{0}; +}; + +struct ReadSerialNumber { + static constexpr uint16_t REGISTER_LOW = WinlineProtocol::Registers::SERIAL_NUMBER_LOW; + static constexpr uint16_t REGISTER_HIGH = WinlineProtocol::Registers::SERIAL_NUMBER_HIGH; + + ReadSerialNumber(); + ReadSerialNumber(uint32_t low_bytes, uint32_t high_bytes); + operator std::vector() const; + std::string serial_number; +}; + +struct ReadDCDCVersion { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::DCDC_VERSION; + + ReadDCDCVersion(); + ReadDCDCVersion(const std::vector& raw); + operator std::vector() const; + uint16_t version{0}; +}; + +struct ReadPFCVersion { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::PFC_VERSION; + + ReadPFCVersion(); + ReadPFCVersion(const std::vector& raw); + operator std::vector() const; + uint16_t version{0}; +}; + +// SET operations (Function 0x03 + Register) +struct SetVoltage { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::SET_OUTPUT_VOLTAGE; + + SetVoltage(float voltage); + operator std::vector() const; + float voltage{0.0f}; +}; + +struct SetCurrent { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::SET_OUTPUT_CURRENT; + + SetCurrent(float current); + operator std::vector() const; + float current{0.0f}; +}; + +struct SetCurrentLimitPoint { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::SET_CURRENT_LIMIT_POINT; + + SetCurrentLimitPoint(float limit_point); + operator std::vector() const; + float limit_point{1.0f}; // Default to 100% (no limiting) +}; + +struct SetVoltageUpperLimit { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::SET_VOLTAGE_UPPER_LIMIT; + + SetVoltageUpperLimit(float voltage_limit); + operator std::vector() const; + float voltage_limit{0.0f}; +}; + +struct SetPowerControl { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::POWER_CONTROL; + + SetPowerControl(bool power_on); + operator std::vector() const; + bool power_on{false}; +}; + +struct SetGroupNumber { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::SET_GROUP_NUMBER; + + SetGroupNumber(uint8_t group_number); + operator std::vector() const; + uint8_t group_number{0}; +}; + +struct SetAltitude { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::SET_ALTITUDE; + + SetAltitude(uint32_t altitude); + operator std::vector() const; + uint32_t altitude{WinlineProtocol::ALTITUDE_DEFAULT}; +}; + +struct SetInputMode { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::SET_INPUT_MODE; + + SetInputMode(uint32_t mode); + operator std::vector() const; + uint32_t mode{WinlineProtocol::INPUT_MODE_AC}; +}; + +struct SetAddressMode { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::SET_ADDRESS_MODE; + + SetAddressMode(uint32_t mode); + operator std::vector() const; + uint32_t mode{WinlineProtocol::ADDRESS_DIP}; +}; + +// Error Recovery Operations +struct SetOvervoltageReset { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::SET_OVERVOLTAGE_RESET; + + SetOvervoltageReset(bool enable); + operator std::vector() const; + bool enable{false}; +}; + +struct SetOvervoltageProtection { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::SET_OVERVOLTAGE_PROTECTION; + + SetOvervoltageProtection(bool enable); + operator std::vector() const; + bool enable{false}; +}; + +struct SetShortCircuitReset { + static constexpr uint16_t REGISTER = WinlineProtocol::Registers::SET_SHORT_CIRCUIT_RESET; + + SetShortCircuitReset(bool enable); + operator std::vector() const; + bool enable{false}; +}; + +} // namespace can_packet_acdc + +#endif // CAN_PACKETS_HPP \ No newline at end of file diff --git a/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/Conversions.hpp b/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/Conversions.hpp new file mode 100644 index 0000000000..c8c89bb66f --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/Conversions.hpp @@ -0,0 +1,158 @@ +/* + * Licensor: Pionix GmbH, 2024 + * License: BaseCamp - License Version 1.0 + * + * Licensed under the terms and conditions of the BaseCamp License contained in the "LICENSE" file, also available + * under: https://pionix.com/pionix-license-terms + * You may not use this file/code except in compliance with said License. + */ +#ifndef CONVERSIONS_HPP +#define CONVERSIONS_HPP + +#include +#include +#include +#include +#include + +#include + +// Helper template to ensure type safety for conversion operations +template struct is_conversion_safe { + static constexpr bool value = + std::is_trivially_copyable_v && std::is_standard_layout_v && !std::is_pointer_v; +}; + +template +typename std::enable_if_t::value, T> +from_raw(const std::vector& 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 +typename std::enable_if_t::value, T> +from_raw(const std::vector& 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 +typename std::enable_if_t::value, T> +from_raw(const std::vector& 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 +std::enable_if_t::value && is_conversion_safe::value && (sizeof(T) == 4), T> +from_raw(const std::vector& raw, std::size_t idx) { + constexpr std::size_t N = 4; + static_assert(std::is_trivially_copyable::value, "T must be trivially copyable"); + + if (idx + N > raw.size()) { + throw std::out_of_range("from_raw: buffer access out of bounds"); + } + + uint32_t tmp; + std::memcpy(&tmp, raw.data() + idx, sizeof(tmp)); + tmp = be32toh(tmp); + + float f; + std::memcpy(&f, &tmp, sizeof(f)); + return static_cast(f); +} + +template +typename std::enable_if_t::value, T> +from_raw(const std::vector& 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 +typename std::enable_if_t::value> +to_raw(T src, std::vector& dest) { + uint8_t tmp; + memcpy(&tmp, &src, sizeof(T)); + dest.push_back(tmp); +} + +template +typename std::enable_if_t::value> +to_raw(T src, std::vector& 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 +typename std::enable_if_t::value && is_conversion_safe::value && (sizeof(T) == 4)> +to_raw(T src, std::vector& 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 +typename std::enable_if_t::value && is_conversion_safe::value && (sizeof(T) == 4)> +to_raw(T src, std::vector& dest) { + uint32_t tmp = src; + memcpy(&tmp, &src, sizeof(T)); + tmp = htobe32(static_cast(tmp)); + + // Use array for better alignment guarantees + alignas(float) uint8_t ret[sizeof(float)]; + memcpy(ret, &tmp, sizeof(float)); + dest.insert(dest.end(), {ret[0], ret[1], ret[2], ret[3]}); +} + +template +typename std::enable_if_t::value> +to_raw(T src, std::vector& 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 diff --git a/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/WinlineCanDevice.cpp b/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/WinlineCanDevice.cpp new file mode 100644 index 0000000000..c734010416 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/WinlineCanDevice.cpp @@ -0,0 +1,1255 @@ +/* + * Licensor: Pionix GmbH, 2024 + * License: BaseCamp - License Version 1.0 + * + * Licensed under the terms and conditions of the BaseCamp License contained in the "LICENSE" file, also available + * under: https://pionix.com/pionix-license-terms + * You may not use this file/code except in compliance with said License. + */ + +#include "WinlineCanDevice.hpp" +#include "CanPackets.hpp" +#include "Conversions.hpp" +#include + +#include +#include +#include + +static std::vector 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 parse_module_addresses(const std::string& a) { + std::vector 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 << "Winline: Invalid module address '" << ad << "': " << e.what(); + } + } + return addresses; +} + +WinlineCanDevice::WinlineCanDevice() : CanBus() { +} + +WinlineCanDevice::~WinlineCanDevice() { +} + +void WinlineCanDevice::initial_ping() { + if (operating_mode == OperatingMode::GROUP_DISCOVERY) { + // Winline discovery: Query group information from group broadcast address + EVLOG_info << "Winline: Starting group discovery on group " << group_address; + send_read_register(WinlineProtocol::GROUP_BROADCAST_ADDR, WinlineProtocol::Registers::GROUP_INFO, true); + } else { + EVLOG_info << "Winline: Operating in FIXED_ADDRESS mode. No need to ping."; + initialized = true; + switch_on_off(false); + } +} + +void WinlineCanDevice::set_can_device(const std::string& dev) { + can_device = dev; + EVLOG_info << "Winline: Setting config values: CAN device: " << dev; + open_device(can_device.c_str()); +} + +void WinlineCanDevice::set_config_values(const std::string& addrs, int group_addr, int timeout, int controller_address, + int power_state_grace_period_ms, int altitude_setting_m, + const std::string& input_mode, double module_current_limit_point) { + this->device_connection_timeout_s = timeout; + this->group_address = group_addr; + this->controller_address = controller_address; + this->power_state_grace_period_ms = power_state_grace_period_ms; + this->altitude_setting_m = altitude_setting_m; + this->input_mode = input_mode; + this->module_current_limit_point = module_current_limit_point; + + EVLOG_info << "Winline: Operating with controller address: 0x" << std::hex << controller_address; + EVLOG_info << "Winline: Altitude setting: " << altitude_setting_m << "m"; + EVLOG_info << "Winline: Input mode: " << input_mode; + if (!addrs.empty()) { + operating_mode = OperatingMode::FIXED_ADDRESS; + configured_module_addresses = parse_module_addresses(addrs); // Store original configured addresses + active_module_addresses = configured_module_addresses; // Initialize active list with configured addresses + expected_module_count = active_module_addresses.size(); + + // Initialize telemetry entries for configured modules to prevent immediate removal + // This fixes the chicken-and-egg problem where modules are removed before they can respond + auto now = std::chrono::steady_clock::now(); + for (const auto& addr : active_module_addresses) { + auto& telemetry = telemetries[addr]; + telemetry.last_update = now; // Initialize with current time + EVLOG_debug << "Winline: Initialized telemetry for configured module 0x" << std::hex + << static_cast(addr); + } + + EVLOG_info << "Winline: Operating in FIXED_ADDRESS mode with " << expected_module_count + << " addresses: " << addrs; + } else { + operating_mode = OperatingMode::GROUP_DISCOVERY; + EVLOG_info << "Winline: Operating in GROUP_DISCOVERY mode for group address: " << group_address; + } + EVLOG_info << "Winline: module communication timeout: " << device_connection_timeout_s << "s"; +} + +void WinlineCanDevice::rx_handler(uint32_t can_id, const std::vector& payload) { + // if (!(can_id & CAN_EFF_FLAG)) { + // return; + // } + + // Verify this is a Winline protocol message + uint16_t protocol_number = can_packet_acdc::protocol_number_from_can_id(can_id); + if (protocol_number != WinlineProtocol::PROTNO) { + 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 << "Winline: 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); + + // Universal Winline response validation helper + auto validate_response = [&](uint8_t expected_data_type) -> bool { + uint8_t data_type = payload[0]; + uint8_t error_code = payload[1]; + + // Enhanced Winline error code validation + if (error_code != WinlineProtocol::ERROR_NORMAL) { + uint16_t register_number = (static_cast(payload[2]) << 8) | payload[3]; + + // Provide specific error code descriptions based on Winline protocol + std::string error_description; + switch (error_code) { + case WinlineProtocol::ERROR_FAULT: + error_description = "General fault"; + break; + default: + error_description = "Unknown error (frame should be discarded per Winline spec)"; + break; + } + + EVLOG_warning << "Winline: " << error_description << " response from module 0x" << std::hex + << static_cast(source_address) << " for register 0x" << register_number + << " (error_code=0x" << static_cast(error_code) << ")"; + + // Per Winline protocol: "F0: Normal, Others: Fault, discard frame" + return false; + } + + if (data_type != expected_data_type) { + uint16_t register_number = (static_cast(payload[2]) << 8) | payload[3]; + EVLOG_warning << "Winline: Invalid data type from module 0x" << std::hex << static_cast(source_address) + << " for register 0x" << register_number << " (expected=0x" + << static_cast(expected_data_type) << ", received=0x" << static_cast(data_type) + << ")"; + return false; + } + + return true; + }; + + // Basic Winline response parsing - extract common fields + uint8_t data_type = payload[0]; + uint8_t error_code = payload[1]; + uint16_t register_number = (static_cast(payload[2]) << 8) | payload[3]; + + if (error_code != WinlineProtocol::ERROR_NORMAL) { + EVLOG_warning << "Winline: Received error response from module 0x" << std::hex + << static_cast(source_address) << " for register 0x" << register_number << " (error=0x" + << static_cast(error_code) << ")"; + return; + } + + // Comprehensive Winline register response parsing with standardized format validation + switch (register_number) { + // Core telemetry registers + case WinlineProtocol::Registers::VOLTAGE: { + if (data_type == WinlineProtocol::DATA_TYPE_FLOAT) { + can_packet_acdc::ReadVoltage voltage_reading(payload); + auto& telemetry = telemetries[source_address]; + telemetry.voltage = voltage_reading.voltage; + telemetry.last_update = std::chrono::steady_clock::now(); + signalVoltageCurrent(telemetries); + EVLOG_debug << format_module_id(source_address) << ": Voltage = " << voltage_reading.voltage << "V"; + } + } break; + case WinlineProtocol::Registers::CURRENT: { + if (data_type == WinlineProtocol::DATA_TYPE_FLOAT) { + can_packet_acdc::ReadCurrent current_reading(payload); + auto& telemetry = telemetries[source_address]; + telemetry.current = current_reading.current; + telemetry.last_update = std::chrono::steady_clock::now(); + signalVoltageCurrent(telemetries); + EVLOG_debug << format_module_id(source_address) << ": Current = " << current_reading.current << "A"; + } + } break; + + // Module capabilities and ratings + case WinlineProtocol::Registers::RATED_OUTPUT_POWER: { + if (data_type == WinlineProtocol::DATA_TYPE_FLOAT) { + can_packet_acdc::ReadRatedOutputPower power_reading(payload); + auto& telemetry = telemetries[source_address]; + telemetry.dc_rated_output_power = power_reading.power; + telemetry.last_update = std::chrono::steady_clock::now(); + EVLOG_info << format_module_id(source_address) << ": Rated power = " << power_reading.power << "W"; + + // Check if capabilities are now complete and trigger update + check_and_update_capabilities(source_address); + } + } break; + case WinlineProtocol::Registers::RATED_OUTPUT_CURRENT: { + if (data_type == WinlineProtocol::DATA_TYPE_FLOAT) { + can_packet_acdc::ReadRatedOutputCurrent current_reading(payload); + auto& telemetry = telemetries[source_address]; + // + // Strangely the rated output current is not the max output current, it is the basis calculation for the max + // output current The max output current is the rated output current * module_current_limit_point + // + telemetry.dc_max_output_current = current_reading.current * module_current_limit_point; + telemetry.last_update = std::chrono::steady_clock::now(); + EVLOG_info << format_module_id(source_address) << ": Rated current = " << current_reading.current << "A"; + + // Check if capabilities are now complete and trigger update + check_and_update_capabilities(source_address); + } + } break; + + // Status and diagnostic information + case WinlineProtocol::Registers::STATUS: { + if (data_type == WinlineProtocol::DATA_TYPE_INTEGER) { + can_packet_acdc::PowerModuleStatus status(payload); + signalModuleStatus(status); + auto& telemetry = telemetries[source_address]; + check_and_signal_error_status_change(source_address, status, telemetry.status); + + // Enhanced status monitoring - update status and perform analysis + telemetry.status = status; + telemetry.last_update = std::chrono::steady_clock::now(); + + // Perform trend analysis and maintain status history + analyze_status_trends(source_address); + + // Enhanced power control verification + auto& power_tracking = telemetry.power_tracking; + if (power_tracking.power_commands_sent > 0 && !power_tracking.power_state_verified) { + bool verification_result = verify_power_state(source_address, power_tracking.expected_power_state); + if (verification_result) { + EVLOG_debug << "Winline: Power state verification successful for module 0x" << std::hex + << static_cast(source_address); + } + } + + // Update performance metrics for successful status read + telemetry.status_metrics.status_reads_total++; + telemetry.status_metrics.last_status_read = std::chrono::steady_clock::now(); + telemetry.status_metrics.status_read_success_rate = + 100.0f * (telemetry.status_metrics.status_reads_total - telemetry.status_metrics.status_errors_total) / + telemetry.status_metrics.status_reads_total; + + EVLOG_debug << format_module_id(source_address) << ": Status = " << status; + } + } break; + case WinlineProtocol::Registers::GROUP_INFO: { + if (data_type == WinlineProtocol::DATA_TYPE_INTEGER) { + can_packet_acdc::ReadGroupInfo group_info(payload); + auto& telemetry = telemetries[source_address]; + telemetry.group_number = group_info.group_number; + telemetry.dip_address = group_info.dip_address; + telemetry.last_update = std::chrono::steady_clock::now(); + EVLOG_info << format_module_id(source_address) << ": group=" << static_cast(group_info.group_number) + << ", DIP=" << static_cast(group_info.dip_address); + // Enhanced module discovery with group validation + if (operating_mode == OperatingMode::GROUP_DISCOVERY) { + // Validate that the discovered module belongs to our target group + if (group_info.group_number == group_address) { + // Add to configured_module_addresses (persistent discovery list) + if (std::find(configured_module_addresses.begin(), configured_module_addresses.end(), + source_address) == configured_module_addresses.end()) { + configured_module_addresses.push_back(source_address); + EVLOG_info << "Winline: Added discovered module 0x" << std::hex + << static_cast(source_address) << " to configured list (group " + << static_cast(group_info.group_number) << ")"; + } + + // Also add to active_module_addresses for online tracking + if (std::find(active_module_addresses.begin(), active_module_addresses.end(), source_address) == + active_module_addresses.end()) { + active_module_addresses.push_back(source_address); + EVLOG_info << "Winline: Discovered new module at address 0x" << std::hex + << static_cast(source_address) << " in group " + << static_cast(group_info.group_number); + + // Configure newly discovered module + set_altitude_all_modules(); + set_current_limit_point_all_modules(); + } + } else { + EVLOG_debug << "Winline: Ignoring module at address 0x" << std::hex + << static_cast(source_address) << " (belongs to group " + << static_cast(group_info.group_number) << ", expected group " << group_address + << ")"; + } + } + } + } break; + + // Module identification + case WinlineProtocol::Registers::SERIAL_NUMBER_LOW: { + if (data_type == WinlineProtocol::DATA_TYPE_INTEGER) { + uint32_t serial_low = from_raw(payload, 4); + auto& telemetry = telemetries[source_address]; + telemetry.serial_low = serial_low; + telemetry.last_update = std::chrono::steady_clock::now(); + EVLOG_debug << format_module_id(source_address) << ": Serial low = 0x" << std::hex << serial_low; + // Check if we have both parts of serial number + if (telemetry.serial_high != 0) { + can_packet_acdc::ReadSerialNumber serial_number(telemetry.serial_low, telemetry.serial_high); + telemetry.serial_number = serial_number.serial_number; + EVLOG_info << format_module_id(source_address) << ": Complete serial = " << telemetry.serial_number; + } + } + } break; + case WinlineProtocol::Registers::SERIAL_NUMBER_HIGH: { + if (data_type == WinlineProtocol::DATA_TYPE_INTEGER) { + uint32_t serial_high = from_raw(payload, 4); + auto& telemetry = telemetries[source_address]; + telemetry.serial_high = serial_high; + telemetry.last_update = std::chrono::steady_clock::now(); + EVLOG_debug << format_module_id(source_address) << ": Serial high = 0x" << std::hex << serial_high; + // Check if we have both parts of serial number + if (telemetry.serial_low != 0) { + can_packet_acdc::ReadSerialNumber serial_number(telemetry.serial_low, telemetry.serial_high); + telemetry.serial_number = serial_number.serial_number; + EVLOG_info << format_module_id(source_address) << ": Complete serial = " << telemetry.serial_number; + } + } + } break; + + // SET operation responses (confirmation of settings) + case WinlineProtocol::Registers::SET_OUTPUT_VOLTAGE: + case WinlineProtocol::Registers::SET_OUTPUT_CURRENT: + case WinlineProtocol::Registers::SET_ALTITUDE: + case WinlineProtocol::Registers::SET_INPUT_MODE: { + // SET operations return success/failure confirmation + if (error_code == WinlineProtocol::ERROR_NORMAL) { + EVLOG_debug << format_module_id(source_address) << ": SET operation confirmed for register 0x" << std::hex + << register_number; + } else { + EVLOG_warning << format_module_id(source_address) << ": SET operation failed for register 0x" << std::hex + << register_number << " (error=0x" << static_cast(error_code) << ")"; + } + auto& telemetry = telemetries[source_address]; + telemetry.last_update = std::chrono::steady_clock::now(); + } break; + + case WinlineProtocol::Registers::POWER_CONTROL: { + // Enhanced power control response handling + auto& telemetry = telemetries[source_address]; + auto& power_tracking = telemetry.power_tracking; + + if (error_code == WinlineProtocol::ERROR_NORMAL) { + EVLOG_info << format_module_id(source_address) << ": Power control command confirmed - " + << (power_tracking.expected_power_state ? "ON" : "OFF"); + + // Power command was accepted, but we still need to verify actual state change via status + EVLOG_debug << "Winline: Power control response received. Will verify actual state via status register."; + } else { + EVLOG_error << format_module_id(source_address) << ": Power control command FAILED for register 0x" + << std::hex << register_number << " (error=0x" << static_cast(error_code) << ")"; + + // Reset power tracking on command failure + power_tracking.power_state_verified = false; + power_tracking.power_state_mismatches++; + } + + telemetry.last_update = std::chrono::steady_clock::now(); + } break; + + default: { + EVLOG_debug << "Winline: Unhandled register response 0x" << std::hex << register_number << " from module 0x" + << static_cast(source_address) << " (DataType=0x" << static_cast(data_type) + << ", Error=0x" << static_cast(error_code) << ")"; + } + } +} + +size_t WinlineCanDevice::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); + { + 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 + + // In FIXED_ADDRESS mode, ensure all responding modules are in active list + if (operating_mode == OperatingMode::FIXED_ADDRESS) { + bool modules_re_added = false; + for (const auto& [addr, telemetry] : telemetries) { + if (std::find(active_module_addresses.begin(), active_module_addresses.end(), addr) == + active_module_addresses.end()) { + active_module_addresses.push_back(addr); + EVLOG_info << "Winline: Re-added module 0x" << std::hex << static_cast(addr) + << " to active list during cleanup"; + modules_re_added = true; + } + } + + // Signal capabilities update if modules were re-added + if (modules_re_added) { + signalCapabilitiesUpdate(telemetries); + + // Configure reconnected modules + set_altitude_all_modules(); + set_current_limit_point_all_modules(); + } + } + } + } + + return removed_count; +} + +void WinlineCanDevice::poll_status_handler() { + // Remove expired telemetry entries + size_t removed_count = remove_expired_telemetry_entries(); + + if (removed_count > 0) { + EVLOG_info << "Winline: Removed " << removed_count << " expired modules. " + << "Active modules remaining: " << active_module_addresses.size(); + // signal the telemetry updates + signalCapabilitiesUpdate(telemetries); + signalVoltageCurrent(telemetries); + } + + // --- Telemetry Polling --- + // Poll ALL configured modules (not just active ones) to allow offline modules to recover. + // This enables automatic recovery when temporarily offline modules come back online. + + // Unified polling approach for both modes + if (operating_mode == OperatingMode::GROUP_DISCOVERY) { + // First, try to discover new modules + discover_group_modules(); + } + + // Poll ALL configured modules (not just active ones) to allow offline modules to recover. + // This enables automatic recovery when temporarily offline modules come back online. + for (const auto& addr : configured_module_addresses) { + // Send basic telemetry requests to check if module is responding + send_read_register(addr, WinlineProtocol::Registers::VOLTAGE); + send_read_register(addr, WinlineProtocol::Registers::CURRENT); + + // If module responds and is not in active list, re-add it + if (telemetries.find(addr) != telemetries.end() && + std::find(active_module_addresses.begin(), active_module_addresses.end(), addr) == + active_module_addresses.end()) { + active_module_addresses.push_back(addr); + EVLOG_info << "Winline: Module 0x" << std::hex << static_cast(addr) + << " reconnected and added back to active list"; + + // Signal capabilities update when module is re-added + signalCapabilitiesUpdate(telemetries); + + // Configure reconnected module + set_altitude_all_modules(); + set_current_limit_point_all_modules(); + } + } + + // Poll active modules for detailed telemetry + for (const auto& addr : active_module_addresses) { + // Read essential telemetry using Winline registers + send_read_register(addr, WinlineProtocol::Registers::VOLTAGE); // Read voltage + send_read_register(addr, WinlineProtocol::Registers::CURRENT); // Read current + + // Enhanced status monitoring - use comprehensive status check + perform_comprehensive_status_check(addr); + + // Read serial number if we don't have it yet (only poll once to avoid spam) + auto it = telemetries.find(addr); + if (it == telemetries.end() || it->second.serial_number.empty()) { + send_read_register(addr, WinlineProtocol::Registers::RATED_OUTPUT_POWER); // Read capabilities + send_read_register(addr, WinlineProtocol::Registers::RATED_OUTPUT_CURRENT); // Read capabilities + send_read_register(addr, WinlineProtocol::Registers::SERIAL_NUMBER_LOW); // Read serial number low + send_read_register(addr, WinlineProtocol::Registers::SERIAL_NUMBER_HIGH); // Read serial number high + } + + // Log status summary for modules with issues (every 10th poll to avoid spam) + static uint32_t poll_counter = 0; + if (++poll_counter % 10 == 0 && it != telemetries.end()) { + const auto& status = it->second.status; + if (status.module_fault || status.module_protection || status.temperature_derating || + status.module_power_limiting || status.fan_fault) { + std::string summary = get_status_summary(addr); + EVLOG_info << summary; + } + } + } +} + +bool WinlineCanDevice::switch_on_off(bool on) { + EVLOG_info << "Winline: switch_on_off(" << on << ") - active modules: " << active_module_addresses.size(); + + if (active_module_addresses.empty()) { + EVLOG_warning << "Winline: No active modules to send switch_on_off command to."; + return false; + } + + // Use individual module commands (unified approach for both modes) + EVLOG_info << "Winline: Using individual module commands with enhanced power tracking"; + uint32_t power_value = on ? WinlineProtocol::POWER_ON : WinlineProtocol::POWER_OFF; + bool success = true; + + for (const auto& addr : active_module_addresses) { + bool module_success = send_set_register_integer(addr, WinlineProtocol::Registers::POWER_CONTROL, power_value); + if (!module_success) { + EVLOG_warning << "Winline: Failed to send power control to module 0x" << std::hex << static_cast(addr); + success = false; + } else { + // Track power state change for this module + track_power_state_change(addr, on); + } + } + + if (success) { + EVLOG_info + << "Winline: Power commands sent successfully. Power state verification will occur in next status poll."; + } + + return success; +} + +bool WinlineCanDevice::set_voltage_current(float voltage, float current) { + EVLOG_info << "Winline: 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 << "Winline: No active modules to set voltage/current."; + return false; + } + + // Use individual module commands (unified approach for both modes) + EVLOG_info << "Winline: Using individual module commands"; + const float current_per_module = current / static_cast(module_count); + bool success = true; + + for (const auto& addr : active_module_addresses) { + // Set voltage using register 0x0021 (float) + bool voltage_result = send_set_register_float(addr, WinlineProtocol::Registers::SET_OUTPUT_VOLTAGE, voltage); + + // Set current using register 0x001B (integer, scaled by 1024) + uint32_t scaled_current = static_cast(current_per_module * WinlineProtocol::CURRENT_SCALE_FACTOR); + bool current_result = + send_set_register_integer(addr, WinlineProtocol::Registers::SET_OUTPUT_CURRENT, scaled_current); + + success &= (voltage_result && current_result); + } + return success; +} + +// Enhanced Winline group operations + +bool WinlineCanDevice::discover_group_modules() { + EVLOG_info << "Winline: discover_group_modules() - querying group " << group_address; + + // Send group discovery command using register 0x0043 (GROUP_INFO) + bool result = + send_read_register(WinlineProtocol::GROUP_BROADCAST_ADDR, WinlineProtocol::Registers::GROUP_INFO, true); + + if (!result) { + EVLOG_warning << "Winline: Group discovery command failed"; + } else { + EVLOG_info << "Winline: Group discovery command sent successfully (group " << group_address << ")"; + } + + return result; +} + +// Winline error recovery operations +bool WinlineCanDevice::reset_overvoltage_protection(uint8_t module_address) { + EVLOG_info << "Winline: reset_overvoltage_protection(0x" << std::hex << static_cast(module_address) << ")"; + + bool result = send_set_register_integer(module_address, WinlineProtocol::Registers::SET_OVERVOLTAGE_RESET, + WinlineProtocol::RESET_ENABLE); + + if (!result) { + EVLOG_warning << "Winline: Overvoltage reset command failed for module 0x" << std::hex + << static_cast(module_address); + } else { + EVLOG_info << "Winline: Overvoltage reset command sent successfully to module 0x" << std::hex + << static_cast(module_address); + } + + return result; +} + +bool WinlineCanDevice::reset_short_circuit_protection(uint8_t module_address) { + EVLOG_info << "Winline: reset_short_circuit_protection(0x" << std::hex << static_cast(module_address) << ")"; + + bool result = send_set_register_integer(module_address, WinlineProtocol::Registers::SET_SHORT_CIRCUIT_RESET, + WinlineProtocol::RESET_ENABLE); + + if (!result) { + EVLOG_warning << "Winline: Short circuit reset command failed for module 0x" << std::hex + << static_cast(module_address); + } else { + EVLOG_info << "Winline: Short circuit reset command sent successfully to module 0x" << std::hex + << static_cast(module_address); + } + + return result; +} + +bool WinlineCanDevice::set_altitude_all_modules() { + EVLOG_info << "Winline: Setting altitude to " << altitude_setting_m << "m on all modules"; + + // Validate altitude setting + if (altitude_setting_m < WinlineProtocol::ALTITUDE_MIN || altitude_setting_m > WinlineProtocol::ALTITUDE_MAX) { + EVLOG_error << "Winline: Invalid altitude setting " << altitude_setting_m + << "m. Valid range: " << WinlineProtocol::ALTITUDE_MIN << "-" << WinlineProtocol::ALTITUDE_MAX + << "m"; + return false; + } + + bool all_success = true; + + // Send to each configured module individually (unified approach for both modes) + for (const auto& addr : configured_module_addresses) { + EVLOG_info << "Winline: Setting altitude on module 0x" << std::hex << static_cast(addr); + bool result = + send_set_register_integer(addr, WinlineProtocol::Registers::SET_ALTITUDE, altitude_setting_m, false); + if (!result) { + EVLOG_warning << "Winline: Failed to send altitude setting to module 0x" << std::hex + << static_cast(addr); + all_success = false; + } else { + EVLOG_info << "Winline: Altitude setting sent successfully to module 0x" << std::hex + << static_cast(addr); + } + } + + if (all_success) { + EVLOG_info << "Winline: Altitude setting " << altitude_setting_m << "m sent to all modules successfully"; + } else { + EVLOG_warning << "Winline: Some altitude setting commands failed"; + } + + return all_success; +} + +bool WinlineCanDevice::set_current_limit_point_all_modules() { + EVLOG_info << "Winline: Setting current limit point to " << module_current_limit_point << " on all modules"; + + bool all_success = true; + + // Send to each configured module individually (unified approach for both modes) + for (const auto& addr : configured_module_addresses) { + EVLOG_info << "Winline: Setting current limit point on module 0x" << std::hex << static_cast(addr); + bool result = send_set_register_integer(addr, WinlineProtocol::Registers::SET_CURRENT_LIMIT_POINT, + module_current_limit_point, false); + if (!result) { + EVLOG_warning << "Winline: Failed to send current limit point setting to module 0x" << std::hex + << static_cast(addr); + all_success = false; + } else { + EVLOG_info << "Winline: Current limit point setting sent successfully to module 0x" << std::hex + << static_cast(addr); + } + } + + if (all_success) { + EVLOG_info << "Winline: Current limit point setting " << module_current_limit_point + << " sent to all modules successfully"; + } else { + EVLOG_warning << "Winline: Some current limit point setting commands failed"; + } + + return all_success; +} + +bool WinlineCanDevice::set_input_mode_all_modules() { + EVLOG_info << "Winline: Setting input mode to " << input_mode << " on all modules"; + + // Convert input mode string to Winline protocol value + uint32_t input_mode_value; + if (input_mode == "AC") { + input_mode_value = 1; // AC mode + } else if (input_mode == "DC") { + input_mode_value = 2; // DC mode + } else { + EVLOG_error << "Winline: Invalid input mode '" << input_mode << "'. Valid values: AC, DC"; + return false; + } + + bool all_success = true; + + // Send to each configured module individually (unified approach for both modes) + for (const auto& addr : configured_module_addresses) { + EVLOG_info << "Winline: Setting input mode on module 0x" << std::hex << static_cast(addr); + bool result = + send_set_register_integer(addr, WinlineProtocol::Registers::SET_INPUT_MODE, input_mode_value, false); + if (!result) { + EVLOG_warning << "Winline: Failed to send input mode setting to module 0x" << std::hex + << static_cast(addr); + all_success = false; + } else { + EVLOG_info << "Winline: Input mode setting sent successfully to module 0x" << std::hex + << static_cast(addr); + } + } + + if (all_success) { + EVLOG_info << "Winline: Input mode setting " << input_mode << " sent to all modules successfully"; + } else { + EVLOG_warning << "Winline: Some input mode setting commands failed"; + } + + return all_success; +} + +// Enhanced Winline Status Monitoring Capabilities +bool WinlineCanDevice::perform_comprehensive_status_check(uint8_t module_address) { + EVLOG_debug << "Winline: perform_comprehensive_status_check(0x" << std::hex << static_cast(module_address) + << ")"; + + auto it = telemetries.find(module_address); + if (it == telemetries.end()) { + EVLOG_warning << "Winline: Cannot perform status check for unknown module 0x" << std::hex + << static_cast(module_address); + return false; + } + + auto& telemetry = it->second; + telemetry.status_metrics.status_reads_total++; + telemetry.status_metrics.last_status_read = std::chrono::steady_clock::now(); + + // Read comprehensive status information + bool status_result = send_read_register(module_address, WinlineProtocol::Registers::STATUS); + + if (!status_result) { + telemetry.status_metrics.status_errors_total++; + // Update success rate + telemetry.status_metrics.status_read_success_rate = + 100.0f * (telemetry.status_metrics.status_reads_total - telemetry.status_metrics.status_errors_total) / + telemetry.status_metrics.status_reads_total; + + EVLOG_warning << "Winline: Comprehensive status check failed for module 0x" << std::hex + << static_cast(module_address); + return false; + } + + // Update success rate + telemetry.status_metrics.status_read_success_rate = + 100.0f * (telemetry.status_metrics.status_reads_total - telemetry.status_metrics.status_errors_total) / + telemetry.status_metrics.status_reads_total; + + // Log status diagnostics if there are any active status flags + const auto& status = telemetry.status; + if (status.module_fault || status.module_protection || status.dcdc_overvoltage || status.dcdc_short_circuit || + status.dcdc_over_temperature || status.fan_fault) { + log_status_diagnostics(module_address, status); + } + + EVLOG_debug << "Winline: Comprehensive status check completed for module 0x" << std::hex + << static_cast(module_address) + << " (success rate: " << telemetry.status_metrics.status_read_success_rate << "%)"; + + return true; +} + +bool WinlineCanDevice::analyze_status_trends(uint8_t module_address) { + EVLOG_debug << "Winline: analyze_status_trends(0x" << std::hex << static_cast(module_address) << ")"; + + auto it = telemetries.find(module_address); + if (it == telemetries.end()) { + EVLOG_warning << "Winline: Cannot analyze trends for unknown module 0x" << std::hex + << static_cast(module_address); + return false; + } + + auto& telemetry = it->second; + auto& history = telemetry.status_history; + + // Maintain status history (keep last 10 entries) + constexpr size_t MAX_HISTORY_SIZE = 10; + history.recent_status.push_back(telemetry.status); + if (history.recent_status.size() > MAX_HISTORY_SIZE) { + history.recent_status.pop_front(); + } + + // Analyze trends if we have enough history + if (history.recent_status.size() >= 3) { + // Check for persistent faults (fault present in last 3 readings) + bool persistent_fault = true; + for (size_t i = history.recent_status.size() - 3; i < history.recent_status.size(); ++i) { + if (!history.recent_status[i].module_fault && !history.recent_status[i].module_protection) { + persistent_fault = false; + break; + } + } + + if (persistent_fault) { + EVLOG_warning << "Winline: Persistent fault detected in module 0x" << std::hex + << static_cast(module_address) << " (fault present in last 3 status readings)"; + } + + // Check for frequent temperature derating + int temp_derating_count = 0; + for (size_t i = history.recent_status.size() - 5; + i < history.recent_status.size() && i < history.recent_status.size(); ++i) { + if (history.recent_status[i].temperature_derating) { + temp_derating_count++; + } + } + + if (temp_derating_count >= 3) { + EVLOG_warning << "Winline: Frequent temperature derating detected in module 0x" << std::hex + << static_cast(module_address) << " (" << temp_derating_count + << " occurrences in recent readings)"; + } + + // Check for power limiting patterns + int power_limiting_count = 0; + for (size_t i = history.recent_status.size() - 5; + i < history.recent_status.size() && i < history.recent_status.size(); ++i) { + if (history.recent_status[i].module_power_limiting || history.recent_status[i].ac_power_limiting) { + power_limiting_count++; + } + } + + if (power_limiting_count >= 3) { + EVLOG_info << "Winline: Power limiting pattern detected in module 0x" << std::hex + << static_cast(module_address) << " (" << power_limiting_count + << " occurrences in recent readings) - this may indicate thermal or electrical limits"; + } + } + + // Update fault statistics + const auto& current_status = telemetry.status; + bool has_fault = current_status.module_fault || current_status.module_protection || + current_status.dcdc_overvoltage || current_status.dcdc_short_circuit || + current_status.dcdc_over_temperature || current_status.fan_fault; + + if (has_fault) { + history.fault_count++; + history.last_fault_time = std::chrono::steady_clock::now(); + + EVLOG_info << "Winline: Module 0x" << std::hex << static_cast(module_address) + << " fault count: " << history.fault_count; + } + + return true; +} + +void WinlineCanDevice::log_status_diagnostics(uint8_t module_address, + const can_packet_acdc::PowerModuleStatus& status) { + std::stringstream diagnostics; + diagnostics << "Winline: Status diagnostics for module 0x" << std::hex << static_cast(module_address) << ": "; + + // Critical faults + if (status.module_fault) + diagnostics << "[CRITICAL:MODULE_FAULT] "; + if (status.dcdc_overvoltage) + diagnostics << "[CRITICAL:DCDC_OVERVOLTAGE] "; + if (status.dcdc_short_circuit) + diagnostics << "[CRITICAL:SHORT_CIRCUIT] "; + if (status.dcdc_over_temperature) + diagnostics << "[CRITICAL:OVER_TEMPERATURE] "; + if (status.dcdc_output_overvoltage) + diagnostics << "[CRITICAL:OUTPUT_OVERVOLTAGE] "; + + // Warning conditions + if (status.module_protection) + diagnostics << "[WARNING:PROTECTION_ACTIVE] "; + if (status.fan_fault) + diagnostics << "[WARNING:FAN_FAULT] "; + if (status.temperature_derating) + diagnostics << "[WARNING:TEMP_DERATING] "; + if (status.module_power_limiting) + diagnostics << "[WARNING:POWER_LIMITING] "; + if (status.ac_power_limiting) + diagnostics << "[WARNING:AC_LIMITING] "; + + // Communication and input issues + if (status.can_communication_failure) + diagnostics << "[COMM:CAN_FAILURE] "; + if (status.sci_communication_failure) + diagnostics << "[COMM:SCI_FAILURE] "; + if (status.input_mode_error) + diagnostics << "[INPUT:MODE_ERROR] "; + if (status.input_mode_mismatch) + diagnostics << "[INPUT:MODE_MISMATCH] "; + if (status.pfc_voltage_abnormal) + diagnostics << "[INPUT:PFC_ABNORMAL] "; + if (status.ac_overvoltage) + diagnostics << "[INPUT:AC_OVERVOLTAGE] "; + if (status.ac_undervoltage) + diagnostics << "[INPUT:AC_UNDERVOLTAGE] "; + + // Operational status + if (status.dcdc_on_off_status) + diagnostics << "[STATUS:DCDC_OFF] "; + if (status.module_current_imbalance) + diagnostics << "[STATUS:CURRENT_IMBALANCE] "; + + std::string diagnostic_str = diagnostics.str(); + if (diagnostic_str.length() > 80) { // If diagnostics string is long, split it + EVLOG_warning << diagnostic_str; + } else { + EVLOG_info << diagnostic_str; + } +} + +std::string WinlineCanDevice::get_status_summary(uint8_t module_address) const { + auto it = telemetries.find(module_address); + if (it == telemetries.end()) { + return "Module not found"; + } + + const auto& telemetry = it->second; + const auto& status = telemetry.status; + const auto& history = telemetry.status_history; + const auto& metrics = telemetry.status_metrics; + + std::stringstream summary; + summary << "Module[0x" << std::hex << static_cast(module_address) << "] "; + + // Overall health + bool has_critical_fault = status.module_fault || status.dcdc_overvoltage || status.dcdc_short_circuit || + status.dcdc_over_temperature || status.dcdc_output_overvoltage; + + if (has_critical_fault) { + summary << "HEALTH:CRITICAL "; + } else if (status.module_protection || status.fan_fault || status.temperature_derating) { + summary << "HEALTH:WARNING "; + } else { + summary << "HEALTH:NORMAL "; + } + + // Performance metrics + summary << "METRICS:(reads:" << metrics.status_reads_total << ",success:" << std::fixed << std::setprecision(1) + << metrics.status_read_success_rate << "%) "; + + // Fault statistics + summary << "FAULTS:" << history.fault_count << " "; + if (history.recovery_count > 0) { + summary << "RECOVERIES:" << history.recovery_count << " "; + } + + // Enhanced power control statistics + const auto& power_tracking = telemetry.power_tracking; + if (power_tracking.power_commands_sent > 0) { + summary << "POWER_CMDS:" << power_tracking.power_commands_sent << " "; + if (power_tracking.power_state_mismatches > 0) { + summary << "POWER_MISMATCHES:" << power_tracking.power_state_mismatches << " "; + } + if (power_tracking.power_state_verified) { + summary << "POWER_VERIFIED "; + } + } + + // Power status (actual vs expected) + if (status.dcdc_on_off_status) { + summary << "POWER:OFF "; + } else { + summary << "POWER:ON "; + } + + // Show expected vs actual if they differ + if (power_tracking.power_commands_sent > 0 && power_tracking.expected_power_state != (!status.dcdc_on_off_status)) { + summary << "EXPECTED:" << (power_tracking.expected_power_state ? "ON" : "OFF") << " "; + } + + return summary.str(); +} + +bool WinlineCanDevice::verify_power_state(uint8_t module_address, bool expected_on_state) { + auto it = telemetries.find(module_address); + if (it == telemetries.end()) { + EVLOG_warning << "Winline: Cannot verify power state for unknown module 0x" << std::hex + << static_cast(module_address); + return false; + } + + auto& telemetry = it->second; + auto& tracking = telemetry.power_tracking; + const auto& status = telemetry.status; + + // Check if enough time has passed since the last power command + auto now = std::chrono::steady_clock::now(); + auto time_since_command = + std::chrono::duration_cast(now - tracking.last_power_command).count(); + + // Give the module time to process the command (configurable grace period) + if (time_since_command < power_state_grace_period_ms) { + EVLOG_debug << "Winline: Power state verification skipped for module 0x" << std::hex + << static_cast(module_address) << " - Grace period: " << time_since_command << "ms < " + << power_state_grace_period_ms << "ms"; + return true; // Don't fail verification during grace period + } + + // Winline status bit 22: DCDC On/off status (0:On, 1:Off) + bool module_is_on = !status.dcdc_on_off_status; + tracking.actual_power_state = module_is_on; + tracking.last_power_verification = now; + + bool state_matches = (module_is_on == expected_on_state); + tracking.power_state_verified = state_matches; + + if (!state_matches) { + tracking.power_state_mismatches++; + EVLOG_warning << "Winline: Power state mismatch for module 0x" << std::hex << static_cast(module_address) + << " - Expected: " << (expected_on_state ? "ON" : "OFF") + << ", Actual: " << (module_is_on ? "ON" : "OFF") << " (mismatch #" + << tracking.power_state_mismatches << ") - Time since command: " << time_since_command << "ms"; + } else { + EVLOG_debug << "Winline: Power state verified for module 0x" << std::hex << static_cast(module_address) + << " - State: " << (module_is_on ? "ON" : "OFF") << " - Time since command: " << time_since_command + << "ms"; + } + + return state_matches; +} + +bool WinlineCanDevice::handle_power_transition(bool target_state) { + EVLOG_info << "Winline: handle_power_transition(" << (target_state ? "ON" : "OFF") << ")"; + + // Optimize: Use group operations when appropriate + bool result; + EVLOG_info << "Winline: Using individual power transition for " << active_module_addresses.size() << " modules"; + result = switch_on_off(target_state); + + if (result) { + // Track the transition for all active modules + for (const auto& addr : active_module_addresses) { + track_power_state_change(addr, target_state); + } + + EVLOG_info << "Winline: Power transition to " << (target_state ? "ON" : "OFF") << " initiated successfully"; + } else { + EVLOG_error << "Winline: Power transition to " << (target_state ? "ON" : "OFF") << " failed"; + } + + return result; +} + +void WinlineCanDevice::track_power_state_change(uint8_t module_address, bool new_power_state) { + auto it = telemetries.find(module_address); + if (it == telemetries.end()) { + EVLOG_debug << "Winline: Creating telemetry entry for module 0x" << std::hex + << static_cast(module_address); + // Create telemetry entry if it doesn't exist + it = telemetries.emplace(module_address, Telemetry{}).first; + } + + auto& tracking = it->second.power_tracking; + bool state_changed = (tracking.expected_power_state != new_power_state); + + tracking.expected_power_state = new_power_state; + tracking.power_commands_sent++; + tracking.last_power_command = std::chrono::steady_clock::now(); + tracking.power_state_verified = false; // Will be verified on next status read + + if (state_changed) { + EVLOG_info << "Winline: Power state change tracked for module 0x" << std::hex + << static_cast(module_address) << " - New expected state: " << (new_power_state ? "ON" : "OFF") + << " (command #" << tracking.power_commands_sent << ")"; + } +} + +bool WinlineCanDevice::send_command_impl(uint8_t destination_address, uint8_t command_number, + const std::vector& payload, bool group) { + // Note: This old interface is kept for compatibility but should be replaced + // For now, we'll adapt it to work with the new system + EVLOG_warning << "Winline: Using deprecated send_command_impl interface"; + return false; // Disable old interface +} + +// New Winline register-based command sending functions +bool WinlineCanDevice::send_read_register(uint8_t destination_address, uint16_t register_number, bool group) { + uint8_t group_number = group ? group_address : 0; + uint32_t can_id = can_packet_acdc::encode_can_id(controller_address, destination_address, group_number, !group); + can_id |= WinlineProtocol::CAN_EXTENDED_FLAG; // Extended frame format + + std::vector payload = can_packet_acdc::build_read_command(register_number); + auto result = _tx(can_id, payload); + if (!result) { + EVLOG_warning << "Winline: CAN transmission failed for READ register 0x" << std::hex << register_number + << " to address 0x" << static_cast(destination_address); + } + return result; +} + +bool WinlineCanDevice::send_set_register_float(uint8_t destination_address, uint16_t register_number, float value, + bool group) { + uint8_t group_number = group ? group_address : 0; + uint32_t can_id = can_packet_acdc::encode_can_id(controller_address, destination_address, group_number, !group); + can_id |= WinlineProtocol::CAN_EXTENDED_FLAG; // Extended frame format + + std::vector payload = can_packet_acdc::build_set_command_float(register_number, value); + auto result = _tx(can_id, payload); + if (!result) { + EVLOG_warning << "Winline: CAN transmission failed for SET register 0x" << std::hex << register_number + << " (float=" << value << ") to address 0x" << static_cast(destination_address); + } + return result; +} + +bool WinlineCanDevice::send_set_register_integer(uint8_t destination_address, uint16_t register_number, uint32_t value, + bool group) { + uint8_t group_number = group ? group_address : 0; + uint32_t can_id = can_packet_acdc::encode_can_id(controller_address, destination_address, group_number, !group); + can_id |= WinlineProtocol::CAN_EXTENDED_FLAG; // Extended frame format + + std::vector payload = can_packet_acdc::build_set_command_integer(register_number, value); + auto result = _tx(can_id, payload); + if (!result) { + EVLOG_warning << "Winline: CAN transmission failed for SET register 0x" << std::hex << register_number + << " (int=0x" << value << ") to address 0x" << static_cast(destination_address); + } + return result; +} + +void WinlineCanDevice::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); + } + }; + + // Enhanced Winline error handling with automatic recovery + auto check_status_with_recovery = [this, source_address](bool new_val, bool old_val, Error error_type, + std::function recovery_action = nullptr) { + if (new_val != old_val) { + signalError(source_address, error_type, new_val); + + // Attempt automatic recovery for specific error types when they are activated + if (new_val && recovery_action) { + EVLOG_info << "Winline: Attempting automatic recovery for module 0x" << std::hex + << static_cast(source_address); + recovery_action(); + + // Update recovery statistics + auto it = telemetries.find(source_address); + if (it != telemetries.end()) { + it->second.status_history.recovery_count++; + it->second.status_history.last_recovery_time = std::chrono::steady_clock::now(); + EVLOG_info << "Winline: Recovery attempt #" << it->second.status_history.recovery_count + << " for module 0x" << std::hex << static_cast(source_address); + } + } + } + }; + + // Check all error status changes using Winline status bit names with automatic recovery + check_status_change(new_status.module_fault, old_status.module_fault, Error::VendorError); + check_status_change(new_status.dcdc_over_temperature, old_status.dcdc_over_temperature, Error::OverTemperature); + + // Overvoltage with automatic recovery + check_status_with_recovery(new_status.dcdc_output_overvoltage, old_status.dcdc_output_overvoltage, + Error::OverVoltage, + [this, source_address]() { reset_overvoltage_protection(source_address); }); + + check_status_change(new_status.fan_fault, old_status.fan_fault, Error::FanFault); + check_status_change(new_status.can_communication_failure, old_status.can_communication_failure, + Error::CommunicationFault); + check_status_change(new_status.ac_undervoltage, old_status.ac_undervoltage, Error::UnderVoltage); + check_status_change(new_status.ac_overvoltage, old_status.ac_overvoltage, Error::OverVoltage); + check_status_change(new_status.input_mode_error, old_status.input_mode_error, Error::VendorError); + check_status_change(new_status.pfc_voltage_abnormal, old_status.pfc_voltage_abnormal, Error::VendorError); + check_status_change(new_status.module_protection, old_status.module_protection, Error::VendorError); + check_status_change(new_status.module_current_imbalance, old_status.module_current_imbalance, Error::VendorWarning); + + // Short circuit with automatic recovery + check_status_with_recovery(new_status.dcdc_short_circuit, old_status.dcdc_short_circuit, Error::OverCurrent, + [this, source_address]() { reset_short_circuit_protection(source_address); }); + + // Additional status bits that were missing + check_status_change(new_status.sci_communication_failure, old_status.sci_communication_failure, Error::VendorError); + check_status_change(new_status.input_mode_mismatch, old_status.input_mode_mismatch, Error::VendorError); + check_status_change(new_status.dcdc_overvoltage, old_status.dcdc_overvoltage, Error::OverVoltage); + check_status_change(new_status.temperature_derating, old_status.temperature_derating, Error::VendorWarning); + check_status_change(new_status.module_power_limiting, old_status.module_power_limiting, Error::InternalFault); + check_status_change(new_status.ac_power_limiting, old_status.ac_power_limiting, Error::VendorWarning); +} + +void WinlineCanDevice::check_and_update_capabilities(uint8_t source_address) { + auto it = telemetries.find(source_address); + if (it == telemetries.end()) { + return; + } + + auto& telemetry = it->second; + + // Check if we have both power and current data to consider capabilities complete + bool has_power = (telemetry.dc_rated_output_power > 0.0f); + bool has_current = (telemetry.dc_max_output_current > 0.0f); + + // Mark capabilities as valid if we have both power and current data + if (has_power && has_current && !telemetry.valid_caps) { + telemetry.valid_caps = true; + EVLOG_info << format_module_id(source_address) + << ": Capabilities now complete - Power: " << telemetry.dc_rated_output_power + << "W, Current: " << telemetry.dc_max_output_current << "A"; + + // Signal capabilities update when a module's capabilities become complete + signalCapabilitiesUpdate(telemetries); + } +} + +std::string WinlineCanDevice::format_module_id(uint8_t address, const std::string& serial_number) const { + std::stringstream ss; + ss << "Winline[0x" << std::hex << std::uppercase << std::setfill('0') << std::setw(2) << static_cast(address); + if (!serial_number.empty()) { + ss << "/" << serial_number; + } + ss << "]"; + return ss.str(); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/WinlineCanDevice.hpp b/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/WinlineCanDevice.hpp new file mode 100644 index 0000000000..7784d98b68 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Winline/can_driver_acdc/WinlineCanDevice.hpp @@ -0,0 +1,220 @@ +/* + * Licensor: Pionix GmbH, 2024 + * License: BaseCamp - License Version 1.0 + * + * Licensed under the terms and conditions of the BaseCamp License contained in the "LICENSE" file, also available + * under: https://pionix.com/pionix-license-terms + * You may not use this file/code except in compliance with said License. + */ + +#ifndef WINLINE_CAN_DEVICE_HPP +#define WINLINE_CAN_DEVICE_HPP + +#include "CanBus.hpp" +#include +#include // Added for status history +#include +#include +#include +#include +#include + +class WinlineCanDevice : public CanBus { +public: + WinlineCanDevice(); + ~WinlineCanDevice(); + + enum class Error { + OverVoltage, + UnderVoltage, + OverTemperature, + FanFault, + InputPhaseLoss, + CommunicationFault, + InternalFault, + OverCurrent, + InputVoltage, + VendorError, + VendorWarning + }; + + 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, + int power_state_grace_period_ms, int altitude_setting_m, const std::string& input_mode, + double module_current_limit_point); + void initial_ping(); + + // Commands + bool switch_on_off(bool on); + bool set_voltage_current(float voltage, float current); + + // Enhanced Winline group operations + bool discover_group_modules(); + + // Winline error recovery operations + bool reset_overvoltage_protection(uint8_t module_address); + bool reset_short_circuit_protection(uint8_t module_address); + + // Altitude setting operations + bool set_altitude_all_modules(); + + // Current limit point setting operations + bool set_current_limit_point_all_modules(); + + // Input mode setting operations + bool set_input_mode_all_modules(); + + // Winline register-based command functions + bool send_read_register(uint8_t destination_address, uint16_t register_number, bool group = false); + bool send_set_register_float(uint8_t destination_address, uint16_t register_number, float value, + bool group = false); + bool send_set_register_integer(uint8_t destination_address, uint16_t register_number, uint32_t value, + bool group = false); + + // Enhanced Winline status monitoring capabilities + bool perform_comprehensive_status_check(uint8_t module_address); + bool analyze_status_trends(uint8_t module_address); + void log_status_diagnostics(uint8_t module_address, const can_packet_acdc::PowerModuleStatus& status); + std::string get_status_summary(uint8_t module_address) const; + + // Enhanced Winline power control capabilities + bool verify_power_state(uint8_t module_address, bool expected_on_state); + bool handle_power_transition(bool target_state); + void track_power_state_change(uint8_t module_address, bool new_power_state); + + // Template overloads for type-safe command sending (DEPRECATED - use register functions) + template bool send_command(uint8_t destination_address, bool group = false) { + // Use static const vector to avoid repeated allocations + static const std::vector 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 + 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(), + group); + } + + struct Telemetry { + // Core telemetry values + float voltage{0.}; + float current{0.}; + float current_limit_point{0.}; + + // Legacy InfyPower fields (retained for compatibility) + float v_ext{0.}; + float i_avail{0.}; + bool valid_caps{false}; + + // Module capabilities and limits (Winline protocol provides current and power only) + float dc_max_output_current{0.}; + float dc_rated_output_power{0.}; + + // Temperature monitoring (Winline-specific) + float dc_board_temperature{0.}; + float ambient_temperature{0.}; + float pfc_board_temperature{0.}; + + // Status and diagnostic information + can_packet_acdc::PowerModuleStatus status; + + // Module identification (Winline dual-register serial number) + std::string serial_number; // Complete formatted serial number + uint32_t serial_low{0}; // Low bytes from register 0x0054 + uint32_t serial_high{0}; // High bytes from register 0x0055 + + // Version information + uint16_t dcdc_version{0}; + uint16_t pfc_version{0}; + + // Winline-specific settings + uint32_t altitude_setting{1000}; // Working altitude in meters + uint32_t input_mode{1}; // 1=AC, 2=DC + uint8_t group_number{0}; // Module group assignment + uint8_t dip_address{0}; // DIP switch address + + // Enhanced status monitoring + struct StatusHistory { + std::deque recent_status; // Last 10 status readings + uint32_t fault_count{0}; // Total fault occurrences + uint32_t recovery_count{0}; // Successful recovery attempts + std::chrono::time_point last_fault_time; + std::chrono::time_point last_recovery_time; + } status_history; + + struct StatusMetrics { + uint32_t status_reads_total{0}; // Total status reads + uint32_t status_errors_total{0}; // Status read errors + std::chrono::time_point last_status_read; + float status_read_success_rate{100.0f}; // Success rate percentage + } status_metrics; + + // Enhanced power control tracking + struct PowerStateTracking { + bool expected_power_state{false}; // Expected power state (what we commanded) + bool actual_power_state{false}; // Actual power state (from status register) + bool power_state_verified{false}; // Whether power state has been verified + uint32_t power_commands_sent{0}; // Total power commands sent + uint32_t power_state_mismatches{0}; // Power state verification failures + std::chrono::time_point last_power_command; + std::chrono::time_point last_power_verification; + } power_tracking; + + // Timing + std::chrono::time_point last_update; + }; + typedef std::map TelemetryMap; + TelemetryMap telemetries; + + // Data out + sigslot::signal signalVoltageCurrent; + sigslot::signal signalModuleStatus; + sigslot::signal signalError; + sigslot::signal signalCapabilitiesUpdate; + +protected: + virtual void rx_handler(uint32_t can_id, const std::vector& 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}; + int power_state_grace_period_ms{0}; + int altitude_setting_m{0}; + double module_current_limit_point{0.}; + std::string input_mode{"AC"}; + OperatingMode operating_mode{OperatingMode::FIXED_ADDRESS}; + + std::vector active_module_addresses; + std::vector configured_module_addresses; // Store original configured addresses for recovery + 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 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; + + // Helper to check and update capabilities when capability data is received + void check_and_update_capabilities(uint8_t source_address); + + // Private implementation for template methods + bool send_command_impl(uint8_t destination_address, uint8_t command_number, const std::vector& payload, + bool group = false); +}; + +#endif // WINLINE_CAN_DEVICE_HPP \ No newline at end of file diff --git a/modules/HardwareDrivers/PowerSupplies/Winline/main/power_supply_DCImpl.cpp b/modules/HardwareDrivers/PowerSupplies/Winline/main/power_supply_DCImpl.cpp new file mode 100644 index 0000000000..97c324ee13 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Winline/main/power_supply_DCImpl.cpp @@ -0,0 +1,304 @@ +/* + * Licensor: Pionix GmbH, 2024 + * License: BaseCamp - License Version 1.0 + * + * Licensed under the terms and conditions of the BaseCamp License contained in the "LICENSE" file, also available + * under: https://pionix.com/pionix-license-terms + * You may not use this file/code except in compliance with said License. + */ + +#include "power_supply_DCImpl.hpp" +#include +#include + +namespace module { +namespace main { + +void power_supply_DCImpl::init() { + mod->acdc->signalVoltageCurrent.connect([this](WinlineCanDevice::TelemetryMap telemetries) { + float total_current = 0; + float module_voltage = 0; + + for (const auto& telemetry : telemetries) { + total_current += telemetry.second.current; + // Use the proper Winline voltage reading from register 0x0001 + module_voltage = telemetry.second.voltage; // Changed from v_ext to voltage + } + + types::power_supply_DC::VoltageCurrent vc; + vc.current_A = total_current; + vc.voltage_V = module_voltage; + publish_voltage_current(vc); + }); + + mod->acdc->signalModuleStatus.connect([this](can_packet_acdc::PowerModuleStatus status) { + // Publish mode changes + types::power_supply_DC::Mode mode; + + if (status.module_fault) { + mode = types::power_supply_DC::Mode::Fault; + } else if (status.dcdc_on_off_status) { + mode = types::power_supply_DC::Mode::Off; + } else { + mode = types::power_supply_DC::Mode::Export; + } + + if (this->mode.load() != mode || firsttime) { + publish_mode(mode); + firsttime = false; + } + }); + + mod->acdc->signalCapabilitiesUpdate.connect([this](WinlineCanDevice::TelemetryMap telemetries) { + types::power_supply_DC::Capabilities new_caps; + new_caps.bidirectional = false; + new_caps.min_export_current_A = 1; + if (telemetries.size() == 0) { + EVLOG_info << "Winline: No telemetries received, setting default capabilities"; + publish_capabilities(new_caps); + return; + } + // Start with config limits as base + new_caps.min_export_voltage_V = mod->config.min_export_voltage_V; + new_caps.max_export_voltage_V = mod->config.max_export_voltage_V; + new_caps.min_export_current_A = mod->config.min_export_current_A; + new_caps.max_export_current_A = 0.0; // Will be updated with device rated values + new_caps.current_regulation_tolerance_A = mod->config.current_regulation_tolerance_A; + new_caps.peak_current_ripple_A = mod->config.peak_current_ripple_A; + + // Update with device rated values (current and power from protocol) + for (const auto& telemetry : telemetries) { + if (telemetry.second.valid_caps) { + // Replace max current with device rated current (sum of all modules) + new_caps.max_export_current_A += telemetry.second.dc_max_output_current; + // Sum up total power from all modules + new_caps.max_export_power_W += telemetry.second.dc_rated_output_power; + } + } + new_caps.conversion_efficiency_export = mod->config.conversion_efficiency_export; + caps = new_caps; + EVLOG_info << "Winline: Capabilities updated: " << new_caps.max_export_voltage_V << "V / " + << new_caps.min_export_voltage_V << "V, " << new_caps.max_export_current_A << "A, power " + << new_caps.max_export_power_W << "W"; + publish_capabilities(new_caps); + if (last_module_count != telemetries.size() && telemetries.size() > 0) { + double voltage = exportVoltage.load(); + double current = exportCurrentLimit.load(); + types::power_supply_DC::Mode mode = this->mode.load(); + types::power_supply_DC::ChargingPhase phase = this->phase.load(); + + if (telemetries.size() > last_module_count) { + EVLOG_info << "Winline: Hot plug detected - module count increased from " + << static_cast(last_module_count) << " to " << telemetries.size() + << " modules, redistributing " << current << "A from " << static_cast(last_module_count) + << " to " << telemetries.size() << " modules"; + } else if (telemetries.size() < last_module_count) { + EVLOG_info << "Winline: Hot unplug detected - module count decreased from " + << static_cast(last_module_count) << " to " << telemetries.size() + << " modules, redistributing " << current << "A from " << static_cast(last_module_count) + << " to " << telemetries.size() << " modules"; + } + EVLOG_info << "Winline: Restoring last settings: voltage=" << voltage << "V, current=" << current + << "A, mode=" << mode << ", phase=" << phase; + last_module_count = telemetries.size(); + handle_setExportVoltageCurrent(voltage, current); + handle_setMode(mode, phase); + } + last_module_count = telemetries.size(); + }); + + mod->acdc->signalError.connect([this](uint8_t address, WinlineCanDevice::Error error, bool active) { + const std::string error_type = map_winline_error_to_power_supply_dc(error); + const std::string error_message = create_error_message(address, error, active); + const bool is_error_active = error_state_monitor->is_error_active(error_type, ""); + + if (error == WinlineCanDevice::Error::CommunicationFault && active) { + EVLOG_info << "Winline: Communication fault detected - all " << static_cast(last_module_count) + << " modules unresponsive, forcing system OFF for safety"; + this->mode.store(types::power_supply_DC::Mode::Off); + } + + if (active && !is_error_active) { + // New error detected - raise it + EVLOG_error << error_message; + auto severity = (error == WinlineCanDevice::Error::FanFault) ? Everest::error::Severity::Medium + : Everest::error::Severity::High; + raise_error(error_factory->create_error(error_type, "", error_message, severity)); + } else if (!active && is_error_active) { + // Error cleared - clear it + EVLOG_info << error_message; + clear_error(error_type); + } + }); + mod->acdc->initial_ping(); +} + +void power_supply_DCImpl::ready() { +} + +void power_supply_DCImpl::handle_setMode(types::power_supply_DC::Mode& mode, + types::power_supply_DC::ChargingPhase& phase) { + EVLOG_info << "Set mode via CAN: " << mode << " with phase " << phase; + + // Enhanced power control with verification (Task 12) + bool power_result = false; + if (mode == types::power_supply_DC::Mode::Off) { + power_result = mod->acdc->handle_power_transition(false); + } else if (mode == types::power_supply_DC::Mode::Export) { + power_result = mod->acdc->handle_power_transition(true); + } else if (mode == types::power_supply_DC::Mode::Import) { + power_result = mod->acdc->handle_power_transition(true); + } else if (mode == types::power_supply_DC::Mode::Fault) { + power_result = mod->acdc->handle_power_transition(false); + } + + if (power_result) { + EVLOG_info << "Winline: Mode change to " << mode << " initiated successfully"; + this->mode.store(mode); + this->phase.store(phase); + } else { + EVLOG_error << "Winline: Mode change to " << mode << " failed - keeping current mode"; + // Don't update stored mode/phase on failure + } +}; + +void power_supply_DCImpl::handle_setExportVoltageCurrent(double& voltage, double& current) { + EVLOG_info << "Winline: request setting voltage/current: " << voltage << "V / " << current << "A"; + 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; + + // Validate power limits: voltage * current must not exceed max power + double requested_power = voltage * current; + if (requested_power > caps.max_export_power_W) { + EVLOG_warning << "Winline: Requested power " << requested_power << "W exceeds max power " + << caps.max_export_power_W << "W. Reducing current to stay within power limit."; + // Reduce current to stay within power limit + current = caps.max_export_power_W / voltage; + if (current < caps.min_export_current_A) { + EVLOG_error << "Winline: Cannot reduce current below minimum " << caps.min_export_current_A + << "A while staying within power limit. Setting to minimum."; + current = caps.min_export_current_A; + } + } + + EVLOG_info << "Winline: request setting voltage/current: " << voltage << "V / " << current + << "A (power: " << voltage * current << "W)"; + exportVoltage.store(voltage); + exportCurrentLimit.store(current); + + const size_t active_module_count = last_module_count; + if (active_module_count > 0) { + const double current_per_module = exportCurrentLimit.load() / static_cast(active_module_count); + EVLOG_info << "Winline: Updating voltage/current via CAN: " << exportVoltage.load() << "V / " + << exportCurrentLimit.load() << "A total → " << current_per_module + << "A per module - active modules: " << active_module_count; + } else { + EVLOG_info << "Winline: Updating voltage/current via CAN: " << exportVoltage.load() << "V / " + << exportCurrentLimit.load() << "A (but no active modules detected)"; + } + mod->acdc->set_voltage_current(exportVoltage.load(), exportCurrentLimit.load()); +}; + +void power_supply_DCImpl::handle_setImportVoltageCurrent(double& voltage, double& current) { + if (caps.min_import_voltage_V.has_value() && caps.max_import_current_A.has_value()) { + if (voltage > caps.max_import_voltage_V.value()) + voltage = caps.max_import_voltage_V.value(); + else if (voltage < caps.min_import_voltage_V.value()) + voltage = caps.min_import_voltage_V.value(); + + if (current > caps.max_import_current_A.value()) + current = caps.max_import_current_A.value(); + else if (current < caps.min_import_current_A.value()) + current = caps.min_import_current_A.value(); + + minImportVoltage.store(voltage); + importCurrentLimit.store(current); + + EVLOG_info << "Winline: Updating voltage/current via CAN: " << minImportVoltage.load() << "V / " + << importCurrentLimit.load() << "A"; + mod->acdc->set_voltage_current(minImportVoltage.load(), importCurrentLimit.load()); + } +} + +std::string power_supply_DCImpl::map_winline_error_to_power_supply_dc(WinlineCanDevice::Error error) { + switch (error) { + case WinlineCanDevice::Error::OverVoltage: + return "power_supply_DC/OverVoltageDC"; + case WinlineCanDevice::Error::UnderVoltage: + return "power_supply_DC/UnderVoltageDC"; + case WinlineCanDevice::Error::OverTemperature: + return "power_supply_DC/OverTemperature"; + case WinlineCanDevice::Error::OverCurrent: + return "power_supply_DC/OverCurrentDC"; + case WinlineCanDevice::Error::InternalFault: + return "power_supply_DC/HardwareFault"; + case WinlineCanDevice::Error::CommunicationFault: + return "power_supply_DC/CommunicationFault"; + case WinlineCanDevice::Error::InputVoltage: + return "power_supply_DC/UnderVoltageAC"; // Most common case for input voltage issues + case WinlineCanDevice::Error::FanFault: + return "power_supply_DC/VendorWarning"; // Non-critical vendor-specific warning + case WinlineCanDevice::Error::InputPhaseLoss: + return "power_supply_DC/VendorError"; // Critical vendor-specific error + case WinlineCanDevice::Error::VendorError: + return "power_supply_DC/VendorError"; // Critical vendor-specific error + case WinlineCanDevice::Error::VendorWarning: + return "power_supply_DC/VendorWarning"; // Non-critical vendor-specific warning + default: + return "power_supply_DC/VendorError"; // Fallback for unknown errors + } +} + +std::string power_supply_DCImpl::create_error_message(uint8_t module_address, WinlineCanDevice::Error error, + bool active) const { + std::string action = active ? "detected" : "cleared"; + std::string error_name; + + switch (error) { + case WinlineCanDevice::Error::OverVoltage: + error_name = "overvoltage fault"; + break; + case WinlineCanDevice::Error::UnderVoltage: + error_name = "undervoltage fault"; + break; + case WinlineCanDevice::Error::OverTemperature: + error_name = "overtemperature fault"; + break; + case WinlineCanDevice::Error::OverCurrent: + error_name = "overcurrent fault"; + break; + case WinlineCanDevice::Error::InternalFault: + error_name = "internal fault"; + break; + case WinlineCanDevice::Error::CommunicationFault: + error_name = "communication fault"; + break; + case WinlineCanDevice::Error::InputVoltage: + error_name = "input voltage fault"; + break; + case WinlineCanDevice::Error::FanFault: + error_name = "fan fault"; + break; + case WinlineCanDevice::Error::InputPhaseLoss: + error_name = "input phase loss fault"; + break; + default: + error_name = "unknown fault"; + break; + } + + std::stringstream ss; + ss << "Winline[0x" << std::hex << std::uppercase << static_cast(module_address) << "]: " << error_name << " " + << action; + return ss.str(); +} + +} // namespace main +} // namespace module diff --git a/modules/HardwareDrivers/PowerSupplies/Winline/main/power_supply_DCImpl.hpp b/modules/HardwareDrivers/PowerSupplies/Winline/main/power_supply_DCImpl.hpp new file mode 100644 index 0000000000..d41e3e4abe --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Winline/main/power_supply_DCImpl.hpp @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 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 + +#include "../Winline.hpp" + +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 +// insert your custom include headers here +#include +#include +// 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& 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& mod; + const Conf& config; + + virtual void init() override; + virtual void ready() override; + + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 + std::atomic mode{types::power_supply_DC::Mode::Off}; + std::atomic phase{types::power_supply_DC::ChargingPhase::CableCheck}; + std::atomic exportVoltage{0.}; + std::atomic exportCurrentLimit{0.}; + std::atomic minImportVoltage{0.}; + std::atomic importCurrentLimit{0.}; + types::power_supply_DC::Capabilities caps; + + bool firsttime{true}; + uint8_t last_module_count{0}; + + // Error handling helpers + std::string map_winline_error_to_power_supply_dc(WinlineCanDevice::Error error); + std::string create_error_message(uint8_t module_address, WinlineCanDevice::Error error, bool active) const; + // 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 diff --git a/modules/HardwareDrivers/PowerSupplies/Winline/manifest.yaml b/modules/HardwareDrivers/PowerSupplies/Winline/manifest.yaml new file mode 100644 index 0000000000..866a375dad --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Winline/manifest.yaml @@ -0,0 +1,91 @@ +description: Driver for Winline ACDC power supply. Supports multiple Winline modules using CAN protocol either in fixed address mode or group discovery mode. +config: + can_device: + description: CAN interface name + type: string + default: can0 + module_addresses: + description: >- + Module Addresses to use. + If you have multiple PSUs, use individual addresses and list them comma separated, e.g. "1,2". + type: string + default: "" + group_address: + description: >- + Group address. Use this if your PSUs are using automatic allocation of the addresses. + If you have multiple PSUs, set this number in accordance with the group dial + setting of the PSUs you want to use. + Can't use module addresses and group addresses at the same time, if both are set, the module addresses will be used. + type: integer + default: 0 + device_connection_timeout_s: + description: >- + Timeout in seconds to wait for a module to respond before considering it offline. + CRITICAL SAFETY: Must be greater than module internal timeout to prevent overcurrent risk. + Recommended: 15s. + type: integer + default: 15 + conversion_efficiency_export: + description: Conversion efficiency of the export mode. + type: number + default: 0.95 + controller_address: + description: Controller address defaults to 0xF0. + type: integer + default: 240 + min_export_voltage_V: + description: Minimum export voltage limit (V) + type: number + default: 50.0 + max_export_voltage_V: + description: Maximum export voltage limit (V) + type: number + default: 1000.0 + min_export_current_A: + description: Minimum export current limit (A) + type: number + default: 0.0 + max_export_current_A: + description: Maximum export current limit (A) - will be updated by device rated values + type: number + default: 133.3 + power_state_grace_period_ms: + description: Grace period in milliseconds to wait before verifying power state changes + type: integer + default: 2000 + current_regulation_tolerance_A: + description: Current regulation tolerance (A) + type: number + default: 0.5 + peak_current_ripple_A: + description: Peak current ripple (A) + type: number + default: 2 + altitude_setting_m: + description: >- + Working altitude setting in meters. This affects the power derating of the modules + at high altitudes. Valid range: 1000-5000m. Default: 1000m (sea level). + type: integer + default: 1000 + input_mode: + description: >- + Input mode for the power supply modules. AC mode for AC input, DC mode for DC input. + This setting affects the internal configuration of the modules. + type: string + default: "AC" + enum: + - "AC" + - "DC" + module_current_limit_point: + description: Percentage of the rated current that the module will use as the current limit point (0.0-3.3325) + type: number + default: 3.3325 +provides: + main: + description: Main interface + interface: power_supply_DC + config: {} +metadata: + license: https://pionix.com/pionix-license-terms + authors: + - Florin Mihut diff --git a/modules/HardwareDrivers/PowerSupplies/Winline/winlineprotocol.txt b/modules/HardwareDrivers/PowerSupplies/Winline/winlineprotocol.txt new file mode 100644 index 0000000000..ed2e75db83 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Winline/winlineprotocol.txt @@ -0,0 +1,399 @@ +Module Communication Protocol V1.50 +Content +Module Communication Protocol V1.50.................................................................... 1 +1. Overview........................................................................................................... 2 +2. Definition of Frame Format.............................................................................. 2 +2.1 Frame Format.......................................................................................... 2 +2.2 Frame Identifier - 29bits...........................................................................2 +2.3 Data Field................................................................................................ 3 +3. Attached Charts................................................................................................. 5 +3.1 Data Description Chart.............................................................................5 +3.2 Module Alarm/Status Chart......................................................................9 +4. Set Group Number, Address Command...........................................................10 +5. Floating Point Data Description.......................................................................11 +6. Altitude Derating..............................................................................................141. Overview +This protocol applies to Winline Technology N X R & +U X R Series charging module. Baud rate: 125Kbps +2. Definition of Frame Format +2.1 Frame Format +A frame is the basic unit of transmitting information. CAN 2.0B frame format is +shown in the following chart, +Field nameCode +Start of Framesof(1bit) +Arbitration FieldIdentifier (11bit) +SRR +IDE +Identifier (18bit) +RTR +reseal(2 bits) +Control Field +Data Len(4 bits) +Data FieldData(8bytes) +CRC FieldCRC(2bits) +End of Frame(7bits) +2.2 Frame Identifier - 29bits +28 +27 +26 +25 +24 +23 +22 +21 +PROTNO( 9 bits) +20 +19 +PTP +18 +17 +16 +15 +14 +13 +DSTADDR ( 8 bits) +2.2.1 PROTNO +Default: PRONTO =0x060 +2 +12 +1 +10 +9 +8 +7 +6 +5 +SRCADDR( 8 bits) +4 +3 +2 +Group +1 +02.2.3 PTP +PTP=1,point to point communication; +PTP=0,broadcast communication; +2.2.4 DSTADDR +Destination address: +Power supply module address range: 00~63; +Monitor address is fixed to: 0xF0; +Broadcast address: 0xFF; +Group broadcast address: 0xFE +2.2.5 SRCADDR +Source address: +In all communications types, bit3~bit10 are used to indicate source address on the +bus. +Power supply module address range: 00~63; +Monitor address is fixed to: 0xF0 +2.2.6 Group +Group number of the module: 0~7 +2.3 Data Field +2.3.1 Set module parameters +Used to set module voltage, current, current limit point, power on/off. +Transmit frame data field format +Byte0Byte1Byte2~ Byte3Byte4~ Byte7 +Function codeReservedRegister No.Data +0300See Chart 1Data to set(See Chart 1) +Example:Set module voltage +03 +00 +00 21 +0x442F0000 The corresponding floating point format data is: 700.0 +3 +44 2F 00 00Example: Set module current limit point +03 +00 +00 22 +3F 00 00 00 +0x3F000000 corresponding floating point format data is: 0.5(Module current limit +point is set in percentage). +Current limit point corresponding to rate current is 1, other value are calculated +proportionally. +Module current limit point calculating method: Assuming that the required current is +10A and the rated current is 20A, then the current limit point = 10/20 = 0.5 +Example: Set module power on +03 +00 +00 3000 00 00 00 +00 3000 01 00 00 +Example: Set module power off +03 +00 +2.3.2 Read Module Data +Transmit frame data field format +Byte0Byte1Byte2~ Byte3Byte4~ Byte7 +Function codeReservedRegister No.Reserved +1000See Chart 100 00 00 00 +Respond frame data field format +Byte0Byte1Byte2~ Byte3Byte4~ Byte7 +Data type of returned dataError codeRegister No.Reserved +41:Float pointF0:NormalSee Chart 1Returned data +42:IntegerOthers : Fault, discard +frame +Example:Get module voltage +Monitor sending: +410 +0000 0100 00 00 00 +F000 0144 2F 00 00 +Module responding: +41 +0x41: Returned data is in float point format. +0xF0: Response frame is normal. +0x0001: Register number. +0x442F0000 corresponding floating point format data is: 700.0 +Example: Get module status +Monitor sending: +10 +0000 4000 00 00 00 +F000 4000 00 01 00 +Module responding: +42 +0x42: Returned data is in integer format. +0xF0: Response frame is normal. +0x0040: Register number. +0x00000100 See Chart 2 +3. Attached Charts +3.1 Data Description Chart +Chapter 1 +Register +(VALUETY +PE) +Data DescriptionData +(RMP)Format +0x0001Get module voltageFloat Point +0x0002Get module currentFloat Point +0x0003Get module current limit pointFloat Point +0x0004Get module DC board temperatureFloat Point +5 +Format DescriptionGet module input phase voltage(DC +0x0005 +input voltage) +Float Point +Get module PFCO voltage(positive +0x0008 +half bus) +Float Point +Get module PFCO voltage(negative +0x000A +half bus) +Float Point +Get module panel(ambient) +0x000B +temperature +Float Point +0x000CGet module AC phase A voltageFloat Point +0x000DGet module AC phase B voltageFloat Point +0x000EGet module AC phase C voltageFloat Point +0x0010Get module PFC board temperatureFloat Point +0x0011Get module rated output powerFloat Point +0x0012Get module rated output currentFloat Point +Unit :m; range: 1000~5000 No derating when +0x0017 +Set module working altitude +altitude is below 1000m; if altitude is higher +Integer +than 5000m, set 5000.Setting will be saved +after power-off. +0x001B +Set module output current +Set value to output current*1024, for example: +Integer +10240 = 10A*1024 +byte7 lower 6 bits(range 0~60) +0x001E +Set group number +Integer +other bytes byte4~byte6 and byte7 higher 5 bits +are 0s. +0x00000000:Automatically assigned +0x001FSet module address assignation mode0x0021Set module output voltageFloat Point +0x0022Set module current limit pointFloat Point +Integer +6 +0x00010000:set by DIP switch (default)Set module output voltage upper +0x0023 +0x0030 +limit +Power on/off +Set module output overvoltage point +Float Point +do not set unless specially required +0x00010000:Power off ; 0x00000000:Power +Integer +on +0x0031 +Set module overvoltage reset +Integer0x00000000:Disable;0x00010000:Reset +Integer0x00000000:Enable; 0x00010000:Enable +IntegerSee Chart 2 +Set module output overvoltage +0x003E +protection relevance permission +0x0040Get current alarm/status +0x0043Get group number&DIP switch address Integer +Higher16 bits of the returned data(byte4~byte5) +are group number, lower 16 bits of the returned +data(byte6~byte7) are DIP switch address +0x00000000:Disable;0x00010000:Reset +0x0044Set module short circuit resetInteger0x0046Set module input modeInteger0x0048Get input powerIntegerUnit 1W +0x004AGet current set altitudeIntegerUnit: m(efault 1000) +0x00000001:AC mode(defaule);0x00000002: +DC modes; +0x00000001:Single phase AC; +0x00000002:DC; +Get current module input working +0x004B +mode +Integer +0x00000003:Three phase AC; +0x00000005:Partten mismatch (phase +sequence error); +Get node Serial No low bytes(ID +0x0054 +number) +Integer +Get node Serial No high bytes(ID +0x0055 +number) +Integer +Lower 16 bits ofthe returned +0x0056 +Get DCDC version +Integer +data(byte6~byte7), version number refers to the +decimal number of data. +7Lower 16 bits ofthe returned +0x0057 +Get PFC version +Integer +data(byte6~byte7), version number refers to the +decimal number of data. +83.2 Module Alarm/Status Chart +0 invalid 1 valid +Chart 2- Module Alarm/Status +Bit +Description +0Module fault (red indicator steady on) +1Module protection (yellow indicator steady on) +2Reserved +3Module internal SCI communication failure +4Input mode error(or input wiring error) +5Input mode set by monitor does not match the actual working mode +6Reserved +7DCDC overvoltage +8PFC voltage abnormal(imbalance, overvoltage or undervoltage) +9AC overvoltage +10Reserved +11Reserved +12Reserved +13Reserved +14AC undervoltage +15Reserved +16CAN communication failure +17Module current imbalance +18Reserved +19Reserved +20Reserved +21Reserved +22DCDC On/off status 0:On, 1:Off +923Module power limiting +24Temperature derating +25AC power limiting +26Reserved +27Fan fault +28DCDC short circuit +29Reserved +30DCDC over temperature +31DCDC output overvoltage +4. Set Group Number, Address Command +Command to set group number and module address assignation mode is added. After +successful setting, the group number and address assignation mode will be updated +immediately and stored (with power off memory), and the ID in the returned data will +be changed instantly to the latest group number. +Group number setting instructions: +There are two ways to set the group number: DIP switch and communication settings, +respectively, corresponding to the automatic assignation mode and DIP switch setting +address mode. When the module address assignation mode is selected as automatic, +the DIP switch is used to set the group number. Under this circumstance, the +communication group number setting function is invalid, and sending command will +return a set failure response; only when the module address assignation mode is +selected as DIP switch mode, the function of communication group number setting is +effective! +Detailed setting steps are as follows: +Example:Set module group number to 5(0x05) +10Set command(Data field): +03 +00 +00 1E00 00 00 05 +00 1E00 00 00 05 +00 1E00 00 00 05 +Replied command for successful setting: +42 +F0 +Replied command for unsuccessful setting: +F2 +42 +Set Module address assignation mode to DIP switch mode +Set command(Data field): +03 +00 +00 1F00 01 00 00 +00 1F00 01 00 00 +00 1F00 01 00 00 +Replied command for successful setting: +42 +F0 +Replied command for unsuccessful setting: +F2 +42 +5.Floating Point Data Description +Sequence of floating point number transmit: floating point number is stored in the +format of four +bytes, it is transmitted after converted to HEX-ASCII code. When +transmitted, four bytes are send in the order of exponent and sign bit, mantissa higher +bit, mantissa middle bit and mantissa lower bit. Floating point numbers are in IEEE +32-bit standard floating point number format(standard C floating point format) with a +length of 32bits, as shown below, +D31 +D30—D23 +D22—D16D15—D8D7—D0 +Floating point sign Exponent EMantissaMantissaMantissa lower +SHigher bitsmiddle bitsbits +IF the exponent is E and mantissa is M, Then the floating point number is ± +(1+M×2-23) ·2E-127 +Whether a floating point number is positive or negative depends on the sign bit S, +11S=1 means the floating point number is negative; S=0 means the floating point +number is positive. +For example, When 32-bit floating point numbers are 40H, A0H, 00H, +00H,which means +S=0,E= 129, +M=221, +then +the floating point +number +is (1 ++221×2-23 ) ·2129-127 = 5.0. +Given a floating point number 60, corresponding 4 bytes ASCII code is: 42, 70, 00, +00. Transmit order on the bus is 42, 70, 00, 00. +Given a floating point number 1.2, corresponding 4 bytes ASCII code is: 3f, 99, 99, +9a. Transmit order on the bus is 3f, 99, 99, 9a. +6. Altitude Derating +When the module works in the high altitude area, current working altitude can be set +by monitor. The effective setting range is 1000m ~5000m, and the module can +conduct derating operation to different degrees according to the set altitude. +By principle, when the altitude is below 2000 meters, there is no need to set the +altitude value; However, considering that the air duct may not be clear in long-term +operation, when the working altitude of the module is higher than 1000 meters, it is +suggested to set the actual altitude. +14Example: Set module working altitude to 3000m +Set command(Data field): +03 +00 +00 1700 00 0B B8 +00 1700 00 0B B8 +00 1700 00 0B B8 +Replied command for successful setting: +42 +F0 +Replied command for unsuccessful setting: +42 +F2 +15 \ No newline at end of file