diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml new file mode 100644 index 000000000..6a9c312e6 --- /dev/null +++ b/.github/workflows/c-cpp.yml @@ -0,0 +1,23 @@ +name: C/C++ CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: configure + run: ./configure + - name: make + run: make + - name: make check + run: make check + - name: make distcheck + run: make distcheck diff --git a/Software/src/battery/BATTERIES.cpp b/Software/src/battery/BATTERIES.cpp index 210754abe..e17007b45 100644 --- a/Software/src/battery/BATTERIES.cpp +++ b/Software/src/battery/BATTERIES.cpp @@ -104,6 +104,8 @@ const char* name_for_battery_type(BatteryType type) { return PylonBattery::Name; case BatteryType::DalyBms: return DalyBms::Name; + case BatteryType::EmusBms: + return EmusBms::Name; case BatteryType::RjxzsBms: return RjxzsBms::Name; case BatteryType::RangeRoverPhev: @@ -215,6 +217,8 @@ Battery* create_battery(BatteryType type) { return new PylonBattery(); case BatteryType::DalyBms: return new DalyBms(); + case BatteryType::EmusBms: + return new EmusBms(); case BatteryType::RjxzsBms: return new RjxzsBms(); case BatteryType::RangeRoverPhev: diff --git a/Software/src/battery/BATTERIES.h b/Software/src/battery/BATTERIES.h index e6e28c654..7f037baa1 100644 --- a/Software/src/battery/BATTERIES.h +++ b/Software/src/battery/BATTERIES.h @@ -25,6 +25,7 @@ void setup_shunt(); #include "CMP-SMART-CAR-BATTERY.h" #include "DALY-BMS.h" #include "ECMP-BATTERY.h" +#include "EMUS-BMS.h" #include "FORD-MACH-E-BATTERY.h" #include "FOXESS-BATTERY.h" #include "GEELY-GEOMETRY-C-BATTERY.h" diff --git a/Software/src/battery/Battery.h b/Software/src/battery/Battery.h index c83687064..8bacf96c5 100644 --- a/Software/src/battery/Battery.h +++ b/Software/src/battery/Battery.h @@ -53,6 +53,7 @@ enum class BatteryType { CmpSmartCar = 45, MaxusEV80 = 46, ThinkCity = 47, + EmusBms = 48, Highest }; diff --git a/Software/src/battery/EMUS-BMS.cpp b/Software/src/battery/EMUS-BMS.cpp new file mode 100644 index 000000000..69e0ad548 --- /dev/null +++ b/Software/src/battery/EMUS-BMS.cpp @@ -0,0 +1,267 @@ +#include "EMUS-BMS.h" +#include "../battery/BATTERIES.h" +#include "../communication/can/comm_can.h" +#include "../datalayer/datalayer.h" +#include "../devboard/utils/events.h" + +void EmusBms::update_values() { + // SOC from EMUS (0-100). real_soc is kept for diagnostics; reported_soc is what we want the inverter to use. + datalayer_battery->status.real_soc = (SOC * 100); //increase SOC range from 0-100 -> 100.00 + + datalayer_battery->status.soh_pptt = (SOH * 100); //Increase decimals from 100% -> 100.00% + + datalayer_battery->status.voltage_dV = voltage_dV; //value is *10 (3700 = 370.0) + + datalayer_battery->status.current_dA = current_dA; //value is *10 (150 = 15.0) , invert the sign + + datalayer_battery->status.max_charge_power_W = (max_charge_current * (voltage_dV / 10)); + + datalayer_battery->status.max_discharge_power_W = (-max_discharge_current * (voltage_dV / 10)); + + // Option A: Use EMUS estimated remaining charge (Ah) to compute remaining Wh, then derive reported SOC. + // This keeps the inverter-facing (total, remaining, SOC) internally consistent. + const uint32_t total_Wh = datalayer_battery->info.total_capacity_Wh; + const unsigned long now_ms = millis(); + const bool est_charge_fresh = est_charge_valid && ((now_ms - est_charge_last_ms) <= EST_CHARGE_TIMEOUT_MS); + + if (est_charge_fresh && (voltage_dV > 10) && (total_Wh > 0)) { + // remWh = (est_charge_0p1Ah/10) * (voltage_dV/10) = est_charge_0p1Ah * voltage_dV / 100 + uint32_t remWh = (uint32_t)((uint64_t)est_charge_0p1Ah * (uint64_t)voltage_dV / 100ULL); + if (remWh > total_Wh) { + remWh = total_Wh; // Clamp in case EMUS estimate exceeds configured total + } + datalayer_battery->status.remaining_capacity_Wh = remWh; + + uint32_t soc_x100 = (uint32_t)((uint64_t)remWh * 10000ULL / (uint64_t)total_Wh); + if (soc_x100 > 10000) + soc_x100 = 10000; + datalayer_battery->status.reported_soc = soc_x100; + } else { + // Fallback: + // - If total capacity is known, derive remaining from EMUS SOC. + // - If total capacity is unknown, keep inverter SOC aligned with EMUS SOC and report 0Wh remaining. + if (total_Wh > 0) { + datalayer_battery->status.remaining_capacity_Wh = + (uint32_t)((static_cast(datalayer_battery->status.real_soc) / 10000.0) * (double)total_Wh); + } else { + datalayer_battery->status.remaining_capacity_Wh = 0; + } + datalayer_battery->status.reported_soc = datalayer_battery->status.real_soc; + } + + // Update cell count if we've received individual cell data + if (actual_cell_count > 0) { + datalayer_battery->info.number_of_cells = actual_cell_count; + } + + // Use Pylon protocol min/max for alarms (more stable than individual cell data) + // Individual cell voltages from 0x10B5 frames are still available in cell_voltages_mV[] for display + datalayer_battery->status.cell_max_voltage_mV = cellvoltage_max_mV; + datalayer_battery->status.cell_min_voltage_mV = cellvoltage_min_mV; + + // Also populate first two cells for systems that only check those + if (actual_cell_count == 0) { + datalayer_battery->status.cell_voltages_mV[0] = cellvoltage_max_mV; + datalayer_battery->status.cell_voltages_mV[1] = cellvoltage_min_mV; + } + + datalayer_battery->status.temperature_min_dC = celltemperature_min_dC; + + datalayer_battery->status.temperature_max_dC = celltemperature_max_dC; + + datalayer_battery->info.max_design_voltage_dV = charge_cutoff_voltage; + + datalayer_battery->info.min_design_voltage_dV = discharge_cutoff_voltage; +} + +void EmusBms::handle_incoming_can_frame(CAN_frame rx_frame) { + // Handle EMUS extended ID frames first + if (rx_frame.ID == 0x10B50000) { + // EMUS configuration frame containing cell count + // Byte 0-1: Unknown (00 05) + // Byte 2-3: Unknown (00 03) + // Byte 4-5: Unknown (00 01) + // Byte 6-7: Number of cells (00 77 = 0x77 = 119 decimal) + uint8_t cell_count = rx_frame.data.u8[7]; // Just use byte 7 (0x77 = 119) + // Only update if we got a valid non-zero count + if (cell_count > 0 && cell_count <= MAX_CELLS) { + actual_cell_count = cell_count; + datalayer_battery->info.number_of_cells = actual_cell_count; + } + return; + } + + if (rx_frame.ID == EMUS_SOC_PARAMS_ID) { + // EMUS Base+0x05: State of Charge parameters + // Data0-1: current (0.1A, signed) + // Data2-3: estimated remaining charge (0.1Ah) + // Data6: SOC (%) + // Data7: SOH (%) + current_dA = (int16_t)((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]); + est_charge_0p1Ah = (uint16_t)((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]); + est_charge_valid = (est_charge_0p1Ah != 0xFFFF) && (est_charge_0p1Ah > 0); + est_charge_last_ms = millis(); + SOC = rx_frame.data.u8[6]; + SOH = rx_frame.data.u8[7]; + return; + } + + if (rx_frame.ID == EMUS_ENERGY_PARAMS_ID) { + // EMUS Base+0x06: Energy parameters + // Data2-3: estimated energy left (10Wh) + est_energy_10Wh = (uint16_t)((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]); + est_energy_valid = (est_energy_10Wh != 0xFFFF) && (est_energy_10Wh > 0); + return; + } + + switch (rx_frame.ID) { + case 0x7310: + case 0x7311: + ensemble_info_ack = true; + // This message contains software/hardware version info. No interest to us + break; + case 0x7320: + case 0x7321: + ensemble_info_ack = true; + battery_module_quantity = rx_frame.data.u8[0]; + battery_modules_in_series = rx_frame.data.u8[2]; + cell_quantity_in_module = rx_frame.data.u8[3]; + voltage_level = rx_frame.data.u8[4]; + ah_number = rx_frame.data.u8[6]; + break; + case 0x4210: + case 0x4211: + datalayer_battery->status.CAN_battery_still_alive = CAN_STILL_ALIVE; + voltage_dV = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]); + current_dA = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]) - 30000; + SOC = rx_frame.data.u8[6]; + SOH = rx_frame.data.u8[7]; + break; + case 0x4220: + case 0x4221: + charge_cutoff_voltage = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]); + discharge_cutoff_voltage = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]); + max_charge_current = (((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[4]) * 0.1) - 3000; + max_discharge_current = (((rx_frame.data.u8[7] << 8) | rx_frame.data.u8[6]) * 0.1) - 3000; + break; + case 0x4230: + case 0x4231: + cellvoltage_max_mV = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]); + cellvoltage_min_mV = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]); + break; + case 0x4240: + case 0x4241: + celltemperature_max_dC = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]) - 1000; + celltemperature_min_dC = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]) - 1000; + break; + case 0x4250: + case 0x4251: + //Byte0 Basic Status + //Byte1-2 Cycle Period + //Byte3 Error + //Byte4-5 Alarm + //Byte6-7 Protection + break; + case 0x4260: + case 0x4261: + //Byte0-1 Module Max Voltage + //Byte2-3 Module Min Voltage + //Byte4-5 Module Max. Voltage Number + //Byte6-7 Module Min. Voltage Number + break; + case 0x4270: + case 0x4271: + //Byte0-1 Module Max. Temperature + //Byte2-3 Module Min. Temperature + //Byte4-5 Module Max. Temperature Number + //Byte6-7 Module Min. Temperature Number + break; + case 0x4280: + case 0x4281: + charge_forbidden = rx_frame.data.u8[0]; + discharge_forbidden = rx_frame.data.u8[1]; + break; + case 0x4290: + case 0x4291: + break; + default: + // Handle EMUS individual cell voltage messages (0x10B50100-0x10B5011F) + // Each message contains 8 cells (1 byte per cell) + if (rx_frame.ID >= CELL_VOLTAGE_BASE_ID && rx_frame.ID < (CELL_VOLTAGE_BASE_ID + 32)) { + uint8_t group = rx_frame.ID - CELL_VOLTAGE_BASE_ID; + uint8_t cell_start = group * 8; // 8 cells per message + + for (uint8_t i = 0; i < 8; i++) { + uint8_t cell_index = cell_start + i; + // Only process cells up to the actual cell count (if known) + if (cell_index < MAX_CELLS && (actual_cell_count == 0 || cell_index < actual_cell_count)) { + // Cell voltage: 2000mV base + (byte value × 10mV) + // e.g., 0x7B (123) = 2000 + 123 × 10 = 3230mV + uint16_t cell_voltage = 2000 + (rx_frame.data.u8[i] * 10); + // Only update if voltage is in valid LiFePO4 range (2500-4200mV) + if (cell_voltage >= 2500 && cell_voltage <= 4200) { + uint16_t current_voltage = datalayer_battery->status.cell_voltages_mV[cell_index]; + // Reject sudden large changes (>1000mV) as likely data corruption + // Using 1000mV threshold since EMUS updates every 5-6 seconds + if (current_voltage == 0 || abs((int)cell_voltage - (int)current_voltage) <= 1000) { + datalayer_battery->status.cell_voltages_mV[cell_index] = cell_voltage; + } + } + } + } + } + // Handle EMUS individual cell balancing status messages (0x10B50300-0x10B5031F) + // Each message contains 8 cells (1 byte per cell) + else if (rx_frame.ID >= CELL_BALANCING_BASE_ID && rx_frame.ID < (CELL_BALANCING_BASE_ID + 32)) { + uint8_t group = rx_frame.ID - CELL_BALANCING_BASE_ID; + uint8_t cell_start = group * 8; // 8 cells per message + + for (uint8_t i = 0; i < 8; i++) { + uint8_t cell_index = cell_start + i; + if (cell_index < MAX_CELLS && (actual_cell_count == 0 || cell_index < actual_cell_count)) { + // Balancing status: non-zero value = balancing active + datalayer_battery->status.cell_balancing_status[cell_index] = (rx_frame.data.u8[i] > 0); + } + } + } + break; + } +} + +void EmusBms::transmit_can(unsigned long currentMillis) { + // Send 1s CAN Message + if (currentMillis - previousMillis1000 >= INTERVAL_1_S) { + previousMillis1000 = currentMillis; + + transmit_can_frame(&PYLON_3010); // Heartbeat + transmit_can_frame(&PYLON_4200); // Ensemble OR System equipment info, depends on frame0 + transmit_can_frame(&PYLON_8200); // Control device quit sleep status + transmit_can_frame(&PYLON_8210); // Charge command + + if (ensemble_info_ack) { + PYLON_4200.data.u8[0] = 0x00; //Request system equipment info + } + } + + // EMUS BMS auto-broadcasts cell data - no polling needed +} + +void EmusBms::setup(void) { // Performs one time setup at startup + strncpy(datalayer.system.info.battery_protocol, "EMUS BMS (Pylon 250k)", 63); + datalayer.system.info.battery_protocol[63] = '\0'; + datalayer_battery->info.number_of_cells = 2; // Will be updated dynamically based on received data + datalayer_battery->info.max_design_voltage_dV = user_selected_max_pack_voltage_dV; + datalayer_battery->info.min_design_voltage_dV = user_selected_min_pack_voltage_dV; + datalayer_battery->info.max_cell_voltage_mV = user_selected_max_cell_voltage_mV; + datalayer_battery->info.min_cell_voltage_mV = user_selected_min_cell_voltage_mV; + datalayer_battery->info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV; + + // Initialize all cell voltages to a safe mid-range value to prevent false low voltage alarms + for (uint16_t i = 0; i < MAX_CELLS; i++) { + datalayer_battery->status.cell_voltages_mV[i] = 3300; // Safe default value + } + + if (allows_contactor_closing) { + *allows_contactor_closing = true; + } +} diff --git a/Software/src/battery/EMUS-BMS.h b/Software/src/battery/EMUS-BMS.h new file mode 100644 index 000000000..8afc44914 --- /dev/null +++ b/Software/src/battery/EMUS-BMS.h @@ -0,0 +1,122 @@ +#ifndef EMUS_BMS_H +#define EMUS_BMS_H + +#include "../datalayer/datalayer.h" +#include "CanBattery.h" + +class EmusBms : public CanBattery { + public: + // Use this constructor for the second battery. + EmusBms(DATALAYER_BATTERY_TYPE* datalayer_ptr, bool* contactor_closing_allowed_ptr, CAN_Interface targetCan) + : CanBattery(targetCan, CAN_Speed::CAN_SPEED_250KBPS) { + datalayer_battery = datalayer_ptr; + contactor_closing_allowed = contactor_closing_allowed_ptr; + allows_contactor_closing = nullptr; + } + + // Use the default constructor to create the first or single battery. + EmusBms() : CanBattery(CAN_Speed::CAN_SPEED_250KBPS) { + datalayer_battery = &datalayer.battery; + allows_contactor_closing = &datalayer.system.status.battery_allows_contactor_closing; + contactor_closing_allowed = nullptr; + } + + virtual ~EmusBms() = default; + + virtual void setup(void); + virtual void handle_incoming_can_frame(CAN_frame rx_frame); + virtual void update_values(); + virtual void transmit_can(unsigned long currentMillis); + static constexpr const char* Name = "EMUS BMS compatible battery"; + + private: + static const int MAX_CELL_DEVIATION_MV = 150; + static const int MAX_CELLS = 192; // Maximum cells supported + static const uint32_t EMUS_BASE_ID = 0x10B50000; // EMUS extended ID base + static const uint32_t EMUS_SOC_PARAMS_ID = 0x10B50500; // Base + 0x05 (State of Charge parameters) + static const uint32_t EMUS_ENERGY_PARAMS_ID = 0x10B50600; // Base + 0x06 (Energy parameters) + static const uint32_t CELL_VOLTAGE_BASE_ID = 0x10B50100; // Base CAN ID for cell voltages + static const uint32_t CELL_BALANCING_BASE_ID = 0x10B50300; // Base CAN ID for balancing status + + DATALAYER_BATTERY_TYPE* datalayer_battery; + + // If not null, this battery decides when the contactor can be closed and writes the value here. + bool* allows_contactor_closing; + + // If not null, this battery listens to this boolean to determine whether contactor closing is allowed + bool* contactor_closing_allowed; + + unsigned long previousMillis1000 = 0; // will store last time a 1s CAN Message was sent + unsigned long previousMillis5000 = 0; // will store last time a 5s CAN Message was sent + + //Actual content messages + CAN_frame PYLON_3010 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x3010, + .data = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + CAN_frame PYLON_8200 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x8200, + .data = {0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + CAN_frame PYLON_8210 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x8210, + .data = {0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + CAN_frame PYLON_4200 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4200, + .data = {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + // EMUS request for individual cell voltages (group 0 = all cells) + CAN_frame EMUS_CELL_VOLTAGE_REQUEST = {.FD = false, + .ext_ID = true, + .DLC = 0, + .ID = 0x19B50100, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + // EMUS request for individual cell balancing status (group 0 = all cells) + CAN_frame EMUS_CELL_BALANCING_REQUEST = {.FD = false, + .ext_ID = true, + .DLC = 0, + .ID = 0x19B50300, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + + int16_t celltemperature_max_dC; + int16_t celltemperature_min_dC; + int16_t current_dA; + uint16_t voltage_dV = 0; + uint16_t cellvoltage_max_mV = 3300; + uint16_t cellvoltage_min_mV = 3300; + uint16_t charge_cutoff_voltage = 0; + uint16_t discharge_cutoff_voltage = 0; + int16_t max_charge_current = 0; + int16_t max_discharge_current = 0; + uint8_t ensemble_info_ack = 0; + uint8_t battery_module_quantity = 0; + uint8_t battery_modules_in_series = 0; + uint8_t cell_quantity_in_module = 0; + uint8_t voltage_level = 0; + uint8_t ah_number = 0; + uint8_t SOC = 50; + uint8_t SOH = 100; + uint8_t charge_forbidden = 0; + uint8_t discharge_forbidden = 0; + uint8_t actual_cell_count = 0; // Actual number of cells detected + uint8_t stable_data_cycles = 0; // Counter for stable voltage data + uint16_t last_min_voltage = 3300; // Track previous min for stability check + uint16_t last_max_voltage = 3300; // Track previous max for stability check + + // EMUS estimated remaining charge (0.1Ah units) from Base+0x05. + uint16_t est_charge_0p1Ah = 0; + bool est_charge_valid = false; + unsigned long est_charge_last_ms = 0; + static const unsigned long EST_CHARGE_TIMEOUT_MS = 10000; // Treat estimate as stale after 10s + + // EMUS estimated remaining energy (10Wh units) from Base+0x06 (optional diagnostics). + uint16_t est_energy_10Wh = 0; + bool est_energy_valid = false; +}; + +#endif diff --git a/Software/src/inverter/GROWATT-HV-CAN.cpp b/Software/src/inverter/GROWATT-HV-CAN.cpp index 6e33160a9..cad13559a 100644 --- a/Software/src/inverter/GROWATT-HV-CAN.cpp +++ b/Software/src/inverter/GROWATT-HV-CAN.cpp @@ -23,12 +23,45 @@ BMS - Battery Information Collector */ void GrowattHvInverter:: update_values() { //This function maps all the values fetched from battery CAN to the correct CAN messages + // ---- Cell/module topology (Growatt frames 0x3150 and 0x3180) ---- + // Previous revisions hard-coded TOTAL_NUMBER_OF_CELLS=300 and NUMBER_OF_MODULES_IN_SERIES=20. + // EMUS provides the actual series cell count in datalayer.battery.info.number_of_cells (e.g. 119). + // Use that when it is sane. Keep the hard-coded values as a fallback for integrations that do not populate + // number_of_cells (e.g. some Pylon paths set it to a placeholder value). + uint16_t total_cells = TOTAL_NUMBER_OF_CELLS; + if (datalayer.battery.info.number_of_cells >= 10 && datalayer.battery.info.number_of_cells <= 512) { + total_cells = (uint16_t)datalayer.battery.info.number_of_cells; + } + // If we are using a dynamic cell count, a safe default for "modules in series" is 1 unless your integration + // provides a more accurate module topology. + uint16_t modules_in_series = NUMBER_OF_MODULES_IN_SERIES; + if (total_cells != TOTAL_NUMBER_OF_CELLS) { + modules_in_series = 1; + } + if (datalayer.battery.status.voltage_dV > 10) { // Only update value when we have voltage available to avoid div0 - ampere_hours_remaining = - ((datalayer.battery.status.reported_remaining_capacity_Wh / datalayer.battery.status.voltage_dV) * - 100); //(WH[10000] * V+1[3600])*100 = 270 (27.0Ah) - ampere_hours_full = ((datalayer.battery.info.total_capacity_Wh / datalayer.battery.status.voltage_dV) * - 100); //(WH[10000] * V+1[3600])*100 = 270 (27.0Ah) + // 0x3140 expects capacity in 10mAh units. + // capacity_10mAh = Wh * 1000 / dV (because V = dV/10, and 10mAh units = Ah*100) + const uint16_t v_dV = datalayer.battery.status.voltage_dV; + + uint32_t full_10mAh = (uint32_t)((uint64_t)datalayer.battery.info.total_capacity_Wh * 1000ULL / v_dV); + + uint32_t rem_10mAh = 0; + if (datalayer.battery.status.remaining_capacity_Wh > 0) { + rem_10mAh = (uint32_t)((uint64_t)datalayer.battery.status.remaining_capacity_Wh * 1000ULL / v_dV); + } else { + // Fallback: derive remaining capacity from SOC if remaining Wh is not available. + const uint32_t soc_pct = (uint32_t)(datalayer.battery.status.reported_soc / 100); // 0..100 + rem_10mAh = (uint32_t)((uint64_t)full_10mAh * soc_pct / 100ULL); + } + + if (full_10mAh > 0xFFFF) + full_10mAh = 0xFFFF; + if (rem_10mAh > 0xFFFF) + rem_10mAh = 0xFFFF; + + capacity_full_10mAh = (uint16_t)full_10mAh; + capacity_remaining_10mAh = (uint16_t)rem_10mAh; } //Map values to CAN messages @@ -100,11 +133,11 @@ void GrowattHvInverter:: //Battery capacity information //Remaining capacity (10 mAh) [0.0 ~ 500000.0 mAH] - GROWATT_3140.data.u8[0] = ((ampere_hours_remaining * 100) >> 8); - GROWATT_3140.data.u8[1] = ((ampere_hours_remaining * 100) & 0x00FF); + GROWATT_3140.data.u8[0] = (capacity_remaining_10mAh >> 8); + GROWATT_3140.data.u8[1] = (capacity_remaining_10mAh & 0x00FF); //Fully charged capacity (10 mAh) [0.0 ~ 500000.0 mAH] - GROWATT_3140.data.u8[2] = ((ampere_hours_full * 100) >> 8); - GROWATT_3140.data.u8[3] = ((ampere_hours_full * 100) & 0x00FF); + GROWATT_3140.data.u8[2] = (capacity_full_10mAh >> 8); + GROWATT_3140.data.u8[3] = (capacity_full_10mAh & 0x00FF); //Manufacturer code GROWATT_3140.data.u8[4] = MANUFACTURER_ASCII_0; GROWATT_3140.data.u8[5] = MANUFACTURER_ASCII_1; @@ -126,11 +159,11 @@ void GrowattHvInverter:: GROWATT_3150.data.u8[2] = (datalayer.battery.status.temperature_max_dC >> 8); GROWATT_3150.data.u8[3] = (datalayer.battery.status.temperature_max_dC & 0x00FF); //Total number of cells - GROWATT_3150.data.u8[4] = (TOTAL_NUMBER_OF_CELLS >> 8); - GROWATT_3150.data.u8[5] = (TOTAL_NUMBER_OF_CELLS & 0x00FF); + GROWATT_3150.data.u8[4] = (total_cells >> 8); + GROWATT_3150.data.u8[5] = (total_cells & 0x00FF); //Number of modules in series - GROWATT_3150.data.u8[6] = (NUMBER_OF_MODULES_IN_SERIES >> 8); - GROWATT_3150.data.u8[7] = (NUMBER_OF_MODULES_IN_SERIES & 0x00FF); + GROWATT_3150.data.u8[6] = (modules_in_series >> 8); + GROWATT_3150.data.u8[7] = (modules_in_series & 0x00FF); //Battery fault and voltage number information //Fault flag bit @@ -175,8 +208,8 @@ void GrowattHvInverter:: GROWATT_3180.data.u8[2] = (NUMBER_OF_PACKS_IN_PARALLEL >> 8); GROWATT_3180.data.u8[3] = (NUMBER_OF_PACKS_IN_PARALLEL & 0x00FF); //Total number of cells (1-65536) - GROWATT_3180.data.u8[4] = (TOTAL_NUMBER_OF_CELLS >> 8); - GROWATT_3180.data.u8[5] = (TOTAL_NUMBER_OF_CELLS & 0x00FF); + GROWATT_3180.data.u8[4] = (total_cells >> 8); + GROWATT_3180.data.u8[5] = (total_cells & 0x00FF); //Pack number + BIC forward/reverse encoding number // Bits 0-3: Pack number // Bits 4-9: Max. number of BIC in forward BIC encoding in daisy- chain communication diff --git a/Software/src/inverter/GROWATT-HV-CAN.h b/Software/src/inverter/GROWATT-HV-CAN.h index 44b867456..0f896d529 100644 --- a/Software/src/inverter/GROWATT-HV-CAN.h +++ b/Software/src/inverter/GROWATT-HV-CAN.h @@ -130,8 +130,9 @@ class GrowattHvInverter : public CanInverterProtocol { unsigned long previousMillis1s = 0; // will store last time a 1s CAN Message was send unsigned long previousMillisBatchSend = 0; uint32_t unix_time = 0; - uint16_t ampere_hours_remaining = 0; - uint16_t ampere_hours_full = 0; + // 0x3140 expects capacity in 10 mAh units. + uint16_t capacity_remaining_10mAh = 0; + uint16_t capacity_full_10mAh = 0; uint16_t send_times = 0; // Overflows every 18hours. Cumulative number, plus 1 for each transmission uint8_t safety_specification = 0; uint8_t charging_command = 0; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 57512f946..881f3475b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -107,6 +107,7 @@ add_executable(tests ../Software/src/battery/CMP-SMART-CAR-BATTERY.cpp ../Software/src/battery/DALY-BMS.cpp ../Software/src/battery/ECMP-BATTERY.cpp + ../Software/src/battery/EMUS-BMS.cpp ../Software/src/battery/FORD-MACH-E-BATTERY.cpp ../Software/src/battery/FOXESS-BATTERY.cpp ../Software/src/battery/GEELY-GEOMETRY-C-BATTERY.cpp