diff --git a/JK-BMSToPylontechCAN/JK-BMS.h b/JK-BMSToPylontechCAN/JK-BMS.h index 5c8d356..aa219f4 100644 --- a/JK-BMSToPylontechCAN/JK-BMS.h +++ b/JK-BMSToPylontechCAN/JK-BMS.h @@ -6,9 +6,9 @@ * Copyright (C) 2023 Armin Joachimsmeyer * Email: armin.joachimsmeyer@gmail.com * - * This file is part of ArduinoUtils https://github.com/ArminJo/PVUtils. + * This file is part of ArduinoUtils https://github.com/ArminJo/JK-BMSToPylontechCAN. * - * Arduino-Utils is free software: you can redistribute it and/or modify + * JK-BMSToPylontechCAN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. @@ -51,7 +51,6 @@ void printJKReplyFrameBuffer(); #define JK_BMS_RECEIVE_FINISHED 1 #define JK_BMS_RECEIVE_ERROR 2 uint8_t readJK_BMSStatusFrameByte(); -void fillJKConvertedCellInfo(); void fillJKComputedData(); extern const uint8_t sSOCThresholdForForceCharge; @@ -60,14 +59,17 @@ extern uint16_t sReplyFrameBufferIndex; // Index of next byte to writ extern uint8_t JKReplyFrameBuffer[350]; // The raw big endian data as received from JK BMS extern struct JKReplyStruct *sJKFAllReplyPointer; extern bool sJKBMSFrameHasTimeout; // For sending CAN data -extern struct JKConvertedCellInfoStruct JKConvertedCellInfo; // The converted little endian cell voltage data extern struct JKComputedDataStruct JKComputedData; // All derived converted and computed data useful for display -extern const char *sErrorStringForLCD; extern bool sErrorStatusJustChanged; extern bool sErrorStatusIsError; + extern char sUpTimeString[12]; // "1000D23H12M" is 11 bytes long extern bool sUpTimeStringMinuteHasChanged; +#if !defined(USE_NO_LCD) +extern const char *sErrorStringForLCD; +#endif + int16_t getJKTemperature(uint16_t aJKRAWTemperature); int16_t getCurrent(uint16_t aJKRAWCurrent); @@ -90,6 +92,60 @@ void computeUpTimeString(); void printJKStaticInfo(); void printJKDynamicInfo(); void handleAndPrintAlarmInfo(); +#if defined(ENABLE_MONITORING) +void printMonitoringInfo(); +#endif + +struct JKCellInfoStruct { + uint16_t CellMillivolt; +#if !defined(USE_NO_LCD) + uint8_t VoltageIsMinMaxOrBetween; // One of VOLTAGE_IS_MINIMUM, VOLTAGE_IS_MAXIMUM or VOLTAGE_IS_BETWEEN_MINIMUM_AND_MAXIMUM +#endif +}; + +struct JKConvertedCellInfoStruct { + uint8_t ActualNumberOfCellInfoEntries; + JKCellInfoStruct CellInfoStructArray[MAXIMUM_NUMBER_OF_CELLS]; + uint16_t MinimumCellMillivolt; + uint16_t MaximumCellMillivolt; + uint16_t DeltaCellMillivolt; // Difference between MinimumVoltagCell and MaximumVoltagCell + uint16_t AverageCellMillivolt; +}; +extern struct JKConvertedCellInfoStruct JKConvertedCellInfo; // The converted little endian cell voltage data +void fillJKConvertedCellInfo(); + +#if !defined(NO_INTERNAL_STATISTICS) +#define VOLTAGE_IS_BETWEEN_MINIMUM_AND_MAXIMUM 0 +#define VOLTAGE_IS_MINIMUM 1 +#define VOLTAGE_IS_MAXIMUM 2 +#if !defined(USE_NO_LCD) +#define SIZE_OF_COMPUTED_CAPACITY_ARRAY 4 // LCD_ROWS +#else +#define SIZE_OF_COMPUTED_CAPACITY_ARRAY 16 +#endif + +/* + * Arrays of counters, which count the times, a cell has minimal or maximal voltage + * To identify runaway cells + */ +extern uint16_t CellMinimumArray[MAXIMUM_NUMBER_OF_CELLS]; +extern uint16_t CellMaximumArray[MAXIMUM_NUMBER_OF_CELLS]; +extern uint8_t CellMinimumPercentageArray[MAXIMUM_NUMBER_OF_CELLS]; +extern uint8_t CellMaximumPercentageArray[MAXIMUM_NUMBER_OF_CELLS]; +#define MINIMUM_BALANCING_COUNT_FOR_DISPLAY 60 // 120 seconds / 2 minutes of balancing +extern uint32_t sBalancingCount; // Count of active balancing in SECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS (2 seconds) units + +struct JKComputedCapacityStruct { + uint8_t StartSOC; + uint8_t EndSOC; + uint16_t Capacity; + uint16_t TotalCapacity; +}; + +extern struct JKComputedCapacityStruct JKComputedCapacity[SIZE_OF_COMPUTED_CAPACITY_ARRAY]; // The last 4 values +void checkAndStoreCapacityComputationValues(); +void printComputedCapacity(uint8_t aCapacityArrayIndex); +#endif // NO_INTERNAL_STATISTICS #define JK_BMS_FRAME_HEADER_LENGTH 11 #define JK_BMS_FRAME_TRAILER_LENGTH 9 @@ -116,35 +172,6 @@ struct JKFrameTailStruct { uint16_t Checksum; // Including StartFrameToken }; -#define VOLTAGE_IS_BETWEEN_MINIMUM_AND_MAXIMUM 0 -#define VOLTAGE_IS_MINIMUM 1 -#define VOLTAGE_IS_MAXIMUM 2 - -struct JKCellInfoStruct { - uint16_t CellMillivolt; - uint8_t VoltageIsMinMaxOrBetween; // One of VOLTAGE_IS_MINIMUM, VOLTAGE_IS_MAXIMUM or VOLTAGE_IS_BETWEEN_MINIMUM_AND_MAXIMUM -}; - -struct JKConvertedCellInfoStruct { - uint8_t ActualNumberOfCellInfoEntries; - JKCellInfoStruct CellInfoStructArray[MAXIMUM_NUMBER_OF_CELLS]; - uint16_t MinimumCellMillivolt; - uint16_t MaximumCellMillivolt; - uint16_t DeltaCellMillivolt; // Difference between MinimumVoltagCell and MaximumVoltagCell - uint16_t AverageCellMillivolt; -}; - -/* - * Arrays of counters, which count the times, a cell has minimal or maximal voltage - * To identify runaway cells - */ -uint16_t CellMinimumArray[MAXIMUM_NUMBER_OF_CELLS]; -uint16_t CellMaximumArray[MAXIMUM_NUMBER_OF_CELLS]; -uint8_t CellMinimumPercentageArray[MAXIMUM_NUMBER_OF_CELLS]; -uint8_t CellMaximumPercentageArray[MAXIMUM_NUMBER_OF_CELLS]; -#define MINIMUM_BALANCING_COUNT_FOR_DISPLAY 60 // 120 seconds / 2 minutes of balancing -uint32_t sBalancingCount; // Count of active balancing in SECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS (2 seconds) units - /* * This structure contains all converted and computed data useful for display */ @@ -193,7 +220,18 @@ union BMSStatusUnion { * Sensor2 temperature sensor is originally named Battery * Battery values are often originally named Total * + * List of values seen in Bluetooth, but not in reply: + * ChargeOvercurrentRecoverySeconds (Charge OCPR Time(S)) + * DischargeOvercurrentRecoverySeconds (Discharge OCPR Time(S)) + * ShortCircuitProtectionDelay (SCP Delay(us)) + * ShortCircuitProtectionRecoverySeconds (SCPR Time(S)) + * CellPowerOffVoltage (Power Off Vol.(V)) + * + * List of values in reply, but not in Bluetooth: + * CellOvervoltageDelaySeconds (0x92) + * CellUndervoltageDelaySeconds (0x95) */ + #define NUMBER_OF_DEFINED_ALARM_BITS 14 struct JKReplyStruct { uint8_t TokenTemperaturePowerMosFet; // 0x80 @@ -227,7 +265,7 @@ struct JKReplyStruct { bool Sensor2OvertemperatureAlarm :1; // 0x0100 bool Sensor1Or2UndertemperatureAlarm :1; // 0x0200 Disables charging, but Has no effect on discharging bool CellOvervoltageAlarm :1; // 0x0400 - bool CellUndervoltageAlarm :1; + bool CellUndervoltageAlarm :1; // 0x0800 Discharging undervoltage forces SOC to 0 and a few seconds later switches off discharge mosfet bool _309_A_ProtectionAlarm :1; // 0x1000 bool _309_B_ProtectionAlarm :1; bool Reserved1Alarm :1; // Two highest bits are reserved @@ -243,7 +281,7 @@ struct JKReplyStruct { * Set with delay of (Dis)ChargeOvercurrentDelaySeconds / "OCP Delay(S)" seconds initially or on retry. * Retry is done after "OCPR Time(S)" */ - bool ChargeOvercurrentAlarm :1; // 0x0020 - Set with delay of ChargeOvercurrentDelaySeconds seconds initially or on retry + bool ChargeOvercurrentAlarm :1; // 0x0020 - Set with delay of ChargeOvercurrentDelaySeconds seconds initially or on retry bool DischargeOvercurrentAlarm :1; // 0x0040 - Set with delay of DischargeOvercurrentDelaySeconds seconds initially or on retry bool CellVoltageDifferenceAlarm :1; // 0x0080 } AlarmBits; @@ -271,13 +309,13 @@ struct JKReplyStruct { uint8_t TokenCellOvervoltageRecoveryMillivolt; // 0x91 uint16_t CellOvervoltageRecoveryMillivolt; // 1000 to 4500 uint8_t TokenCellOvervoltageDelaySeconds; // 0x92 - uint16_t CellOvervoltageDelaySeconds; // 1 to 60 + uint16_t CellOvervoltageDelaySeconds; // Cannot be set by App! I received 5 uint8_t TokenCellUndervoltageProtectionMillivolt; // 0x93 uint16_t CellUndervoltageProtectionMillivolt; uint8_t TokenCellUndervoltageRecoveryMillivolt; // 0x94 uint16_t CellUndervoltageRecoveryMillivolt; uint8_t TokenCellUndervoltageDelaySeconds; // 0x95 - uint16_t CellUndervoltageDelaySeconds; + uint16_t CellUndervoltageDelaySeconds; // Cannot be set by App! I received 5 but the BMS took 100 seconds to signal an undervoltage, see CellUndervoltage.log uint8_t TokenVoltageDifferenceProtectionMillivolt; // 0x96 uint16_t VoltageDifferenceProtectionMillivolt; // 0 to 100 @@ -285,11 +323,11 @@ struct JKReplyStruct { uint8_t TokenDischargeOvercurrentProtectionAmpere; // 0x97 uint16_t DischargeOvercurrentProtectionAmpere; // 1 to 1000 uint8_t TokenDischargeOvercurrentDelaySeconds; // 0x98 - uint16_t DischargeOvercurrentDelaySeconds; // 1 to 60 + uint16_t DischargeOvercurrentDelaySeconds; // DischargeOvercurrentRecoverySeconds can be set by App, but is not contained in reply uint8_t TokenChargeOvercurrentProtectionAmpere; // 0x99 uint16_t ChargeOvercurrentProtectionAmpere; // 1 to 1000 uint8_t TokenChargeOvercurrentDelaySeconds; // 0x9A - uint16_t ChargeOvercurrentDelaySeconds; // 1 to 60 + uint16_t ChargeOvercurrentDelaySeconds; // ChargeOvercurrentRecoverySeconds can be set by App, but is not contained in reply uint8_t TokenBalancingStartVoltage; // 0x9B uint16_t BalancingStartMillivolt; // 2000 to 4500 @@ -395,4 +433,53 @@ struct JKReplyStruct { // with the highest bit being 0 for discharge and 1 for charge }; +/* + * Contains only values which are compared witch current ones to detect changes + */ +struct JKLastReplyStruct { + uint8_t SOCPercent; // 0-100% 0x85 + union { // 0x8B + uint16_t AlarmsAsWord; + struct { + // High byte of alarms + bool Sensor2OvertemperatureAlarm :1; // 0x0100 + bool Sensor1Or2UndertemperatureAlarm :1; // 0x0200 Disables charging, but Has no effect on discharging + bool CellOvervoltageAlarm :1; // 0x0400 + bool CellUndervoltageAlarm :1; + bool _309_A_ProtectionAlarm :1; // 0x1000 + bool _309_B_ProtectionAlarm :1; + bool Reserved1Alarm :1; // Two highest bits are reserved + bool Reserved2Alarm :1; + + // Low byte of alarms + bool LowCapacityAlarm :1; // 0x0001 + bool PowerMosFetOvertemperatureAlarm :1; + bool ChargeOvervoltageAlarm :1; // 0x0004 This happens quite often, if battery charging is approaching 100 % + bool DischargeUndervoltageAlarm :1; + bool Sensor1Or2OvertemperatureAlarm :1; // 0x0010 - Affects the charging/discharging MosFet state, not the enable flags + /* + * Set with delay of (Dis)ChargeOvercurrentDelaySeconds / "OCP Delay(S)" seconds initially or on retry. + * Retry is done after "OCPR Time(S)" + */ + bool ChargeOvercurrentAlarm :1; // 0x0020 - Set with delay of ChargeOvercurrentDelaySeconds seconds initially or on retry + bool DischargeOvercurrentAlarm :1; // 0x0040 - Set with delay of DischargeOvercurrentDelaySeconds seconds initially or on retry + bool CellVoltageDifferenceAlarm :1; // 0x0080 + } AlarmBits; + } AlarmUnion; + + union { // 0x8C + uint16_t StatusAsWord; + struct { + uint8_t ReservedStatusHighByte; // This is the low byte of StatusAsWord, but it was sent as high byte of status + bool ChargeMosFetActive :1; // 0x01 // Is disabled e.g. on over current or temperature + bool DischargeMosFetActive :1; // 0x02 // Is disabled e.g. on over current or temperature + bool BalancerActive :1; // 0x04 + bool BatteryDown :1; // 0x08 + uint8_t ReservedStatus :4; + } StatusBits; + } BMSStatus; + + uint32_t SystemWorkingMinutes; // Minutes 0xB6 +}; + #endif // _JK_BMS_H diff --git a/JK-BMSToPylontechCAN/JK-BMS.hpp b/JK-BMSToPylontechCAN/JK-BMS.hpp index 603e4ba..2da3189 100644 --- a/JK-BMSToPylontechCAN/JK-BMS.hpp +++ b/JK-BMSToPylontechCAN/JK-BMS.hpp @@ -7,9 +7,9 @@ * Copyright (C) 2023 Armin Joachimsmeyer * Email: armin.joachimsmeyer@gmail.com * - * This file is part of ArduinoUtils https://github.com/ArminJo/PVUtils. + * This file is part of ArduinoUtils https://github.com/ArminJo/JK-BMSToPylontechCAN. * - * Arduino-Utils is free software: you can redistribute it and/or modify + * JK-BMSToPylontechCAN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. @@ -31,7 +31,7 @@ #include "JK-BMS.h" -JKReplyStruct lastJKReply; +JKLastReplyStruct lastJKReply; #if defined(DEBUG) #define LOCAL_DEBUG @@ -46,14 +46,13 @@ uint8_t JKRequestStatusFrame[21] = { 0x4E, 0x57 /*4E 57 = StartOfFrame*/, 0x00, 0x00/*0=ReadAllData or commandToken*/, 0x00, 0x00, 0x00, 0x00/*RecordNumber High byte is random code, low 3 bytes is record number*/, JK_FRAME_END_BYTE/*0x68 = EndIdentifier*/, 0x00, 0x00, 0x01, 0x29 /*Checksum, high 2 bytes for checksum not yet enabled -> 0, low 2 Byte for checksum*/}; -uint8_t JKrequestStatusFrameOld[] = { 0xDD, 0xA5, 0x03, 0x00, 0xFF, 0xFD, 0x77 }; +//uint8_t JKrequestStatusFrameOld[] = { 0xDD, 0xA5, 0x03, 0x00, 0xFF, 0xFD, 0x77 }; uint16_t sReplyFrameBufferIndex = 0; // Index of next byte to write to array, except for last byte received. Starting with 0. uint16_t sReplyFrameLength; // Received length of frame uint8_t JKReplyFrameBuffer[350]; // The raw big endian data as received from JK BMS bool sJKBMSFrameHasTimeout; // If true, timeout message or CAN Info page is displayed. -JKConvertedCellInfoStruct JKConvertedCellInfo; // The converted little endian cell voltage data JKComputedDataStruct JKComputedData; // All derived converted and computed data useful for display JKComputedDataStruct lastJKComputedData; // For detecting changes char sUpTimeString[12]; // "9999D23H12M" is 11 bytes long @@ -62,6 +61,38 @@ bool sUpTimeStringMinuteHasChanged; bool sUpTimeStringTenthOfMinuteHasChanged; char sLastUpTimeTenthOfMinuteCharacter; // For detecting changes in string and setting sUpTimeStringTenthOfMinuteHasChanged +JKConvertedCellInfoStruct JKConvertedCellInfo; // The converted little endian cell voltage data +#if !defined(NO_INTERNAL_STATISTICS) +/* + * Arrays of counters, which count the times, a cell has minimal or maximal voltage + * To identify runaway cells + */ +uint16_t CellMinimumArray[MAXIMUM_NUMBER_OF_CELLS]; +uint16_t CellMaximumArray[MAXIMUM_NUMBER_OF_CELLS]; +uint8_t CellMinimumPercentageArray[MAXIMUM_NUMBER_OF_CELLS]; +uint8_t CellMaximumPercentageArray[MAXIMUM_NUMBER_OF_CELLS]; +uint32_t sBalancingCount; // Count of active balancing in SECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS (2 seconds) units + +/* + * The firt entry [0] holds the current computed value if more than 1 Ah are accumulated + */ +struct JKComputedCapacityStruct JKComputedCapacity[SIZE_OF_COMPUTED_CAPACITY_ARRAY]; + +#define CAPACITY_COMPUTATION_MODE_IDLE 0 +#define CAPACITY_COMPUTATION_MODE_CHARGE 1 +#define CAPACITY_COMPUTATION_MODE_DISCHARGE 2 +#define CAPACITY_COMPUTATION_MAX_WRONG_CHARGE_DIRECTION 4 // If we have 5 wrong directions, we end computation +/* + * 100 is factorfor 10 mA to 1 A + * 60 * 60 * 1000L / MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS is number of samples in 1 hour + */ +#define CAPACITY_ACCUMULATOR_1_AMPERE_HOUR (100L * 60L * 60L * 1000L / MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS) // 180000 +uint8_t sCapacityComputationWrongDirectionCount = 0; +uint8_t sCapacityComputationMode = CAPACITY_COMPUTATION_MODE_IDLE; +uint32_t sCapacityComputationAccumulator10MilliAmpere = 0; +uint8_t sLastCapacityComputationDeltaSOC = 0; +#endif //NO_INTERNAL_STATISTICS + /* * The JKFrameAllDataStruct starts behind the header + cell data header 0x79 + CellInfoSize + the variable length cell data (CellInfoSize is contained in JKReplyFrameBuffer[12]) */ @@ -228,12 +259,13 @@ uint8_t readJK_BMSStatusFrameByte() { } /* - * Charge is positive, discharge is negative + * Highest bit is set means charge + * @return Charge is positive, discharge is negative */ int16_t getCurrent(uint16_t aJKRAWCurrent) { uint16_t tCurrent = swap(aJKRAWCurrent); if (tCurrent == 0 || (tCurrent & 0x8000) == 0x8000) { - // Charge + // Charge - NO two's complement! return (tCurrent & 0x7FFF); } // discharge @@ -357,6 +389,7 @@ void fillJKConvertedCellInfo() { JKConvertedCellInfo.DeltaCellMillivolt = tMaximumMillivolt - tMinimumMillivolt; JKConvertedCellInfo.AverageCellMillivolt = tMillivoltSum / tNumberOfNonNullCellInfo; +#if !defined(USE_NO_LCD) /* * Mark and count minimum and maximum cell voltages */ @@ -375,7 +408,9 @@ void fillJKConvertedCellInfo() { JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween = VOLTAGE_IS_BETWEEN_MINIMUM_AND_MAXIMUM; } } +#endif +#if !defined(NO_INTERNAL_STATISTICS) /* * Process minimum statistics */ @@ -446,6 +481,7 @@ void fillJKConvertedCellInfo() { CellMaximumArray[i] = CellMaximumArray[i] / 2; } } +#endif // NO_INTERNAL_STATISTICS #if defined(LOCAL_DEBUG) Serial.print(tNumberOfCellInfo); @@ -461,6 +497,43 @@ void fillJKConvertedCellInfo() { } } +#if !defined(NO_INTERNAL_STATISTICS) +/* + * Print formatted cell info on Serial + */ +void printJKCellStatisticsInfo() { + uint8_t tNumberOfCellInfo = JKConvertedCellInfo.ActualNumberOfCellInfoEntries; + + /* + * Cell statistics + */ + char tStringBuffer[18]; // "12=12 % | 4042, " + + Serial.println(F("Cell Minimum percentages")); + for (uint8_t i = 0; i < tNumberOfCellInfo; ++i) { + if (i != 0 && (i % 8) == 0) { + Serial.println(); + } + sprintf_P(tStringBuffer, PSTR("%2u=%2u %% |%5u, "), i + 1, CellMinimumPercentageArray[i], CellMinimumArray[i]); + Serial.print(tStringBuffer); + } + Serial.println(); + + Serial.println(F("Cell Maximum percentages")); + for (uint8_t i = 0; i < tNumberOfCellInfo; ++i) { + if (i != 0 && (i % 8) == 0) { + Serial.println(); + } + sprintf_P(tStringBuffer, PSTR("%2u=%2u %% |%5u, "), i + 1, CellMaximumPercentageArray[i], CellMaximumArray[i]); + Serial.print(tStringBuffer); + } + Serial.println(); + + Serial.println(); +} + +#endif // NO_INTERNAL_STATISTICS + void fillJKComputedData() { JKComputedData.TemperaturePowerMosFet = getJKTemperature(sJKFAllReplyPointer->TemperaturePowerMosFet); int16_t tMaxTemperature = JKComputedData.TemperaturePowerMosFet; @@ -504,46 +577,152 @@ void fillJKComputedData() { JKComputedData.BatteryLoadPower = JKComputedData.BatteryVoltageFloat * JKComputedData.BatteryLoadCurrentFloat; +#if !defined(NO_INTERNAL_STATISTICS) + /* + * Increment sBalancingCount and fill sBalancingTimeString + */ if (sJKFAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { sBalancingCount++; sprintf_P(sBalancingTimeString, PSTR("%3uD%02uH%02uM"), (uint16_t) (sBalancingCount / (60 * 24 * 30UL)), (uint16_t) ((sBalancingCount / (60 * 30)) % 24), (uint16_t) (sBalancingCount / 30) % 60); } -} - -/* - * Print formatted cell info on Serial - */ -void printJKCellStatisticsInfo() { - uint8_t tNumberOfCellInfo = JKConvertedCellInfo.ActualNumberOfCellInfoEntries; /* - * Cell statistics + * Compute total capacity based on current and SOC + * + * If state is idle and current > 1 A: If current direction is from battery, start discharge computation else start charge computation. + * If current direction is 5 times wrong, stop computation. + * If SOC delta between start and end > 40% store value to array. */ - char tStringBuffer[18]; // "12=12 % | 4042, " + auto tCurrentSOCPercent = sJKFAllReplyPointer->SOCPercent; + auto tBattery10MilliAmpere = JKComputedData.Battery10MilliAmpere; - Serial.println(F("Cell Minimum percentages")); - for (uint8_t i = 0; i < tNumberOfCellInfo; ++i) { - if (i != 0 && (i % 8) == 0) { - Serial.println(); + if (sCapacityComputationMode == CAPACITY_COMPUTATION_MODE_IDLE) { + /* + * Check for start condition + */ + if (tBattery10MilliAmpere < -100) { + sCapacityComputationMode = CAPACITY_COMPUTATION_MODE_DISCHARGE; + JKComputedCapacity[0].StartSOC = tCurrentSOCPercent; + } else if (tBattery10MilliAmpere > 100) { + sCapacityComputationMode = CAPACITY_COMPUTATION_MODE_CHARGE; + JKComputedCapacity[0].StartSOC = tCurrentSOCPercent; + } +# if defined(LOCAL_DEBUG) + Serial.print(F("Start capacity computation at SOC=")); + Serial.println(tCurrentSOCPercent); +# endif + } else { + /* + * Mode CAPACITY_COMPUTATION_MODE_CHARGE and CAPACITY_COMPUTATION_MODE_DISCHARGE here + */ + if (sCapacityComputationMode == CAPACITY_COMPUTATION_MODE_DISCHARGE) { + // Convert discharge current + tBattery10MilliAmpere = -tBattery10MilliAmpere; } - sprintf_P(tStringBuffer, PSTR("%2u=%2u %% |%5u, "), i + 1, CellMinimumPercentageArray[i], CellMinimumArray[i]); - Serial.print(tStringBuffer); - } - Serial.println(); - Serial.println(F("Cell Maximum percentages")); - for (uint8_t i = 0; i < tNumberOfCellInfo; ++i) { - if (i != 0 && (i % 8) == 0) { - Serial.println(); + // Add capacity, even the first value with wrong direction + sCapacityComputationAccumulator10MilliAmpere += tBattery10MilliAmpere; + /* + * Write current data to array. If Capacity < 1, then values are not displayed + */ + uint8_t tDeltaSOC = abs(JKComputedCapacity[0].StartSOC - tCurrentSOCPercent); + if (tDeltaSOC > 1) { + JKComputedCapacity[0].EndSOC = tCurrentSOCPercent; + uint16_t tCapacityComputationAccumulator10MilliAmpereHour = sCapacityComputationAccumulator10MilliAmpere + / (CAPACITY_ACCUMULATOR_1_AMPERE_HOUR / 100); + JKComputedCapacity[0].Capacity = tCapacityComputationAccumulator10MilliAmpereHour / 100; + JKComputedCapacity[0].TotalCapacity = tCapacityComputationAccumulator10MilliAmpereHour / tDeltaSOC; +// Direct 32 bit computation. It is 12 bytes longer +// JKComputedCapacity[0].Capacity = sCapacityComputationAccumulator10MilliAmpere / CAPACITY_ACCUMULATOR_1_AMPERE_HOUR; +// JKComputedCapacity[0].TotalCapacity = sCapacityComputationAccumulator10MilliAmpere +// / ((CAPACITY_ACCUMULATOR_1_AMPERE_HOUR / 100) * tDeltaSOC); + + if (sLastCapacityComputationDeltaSOC != tDeltaSOC) { + // Delta SOC changed by 1 -> print values + sLastCapacityComputationDeltaSOC = tDeltaSOC; + printComputedCapacity(0); + } + } + +# if defined(LOCAL_DEBUG) + Serial.print(F("Mode=")); + Serial.print(sCapacityComputationMode); + Serial.print(F(" CapAcc=")); + Serial.println(sCapacityComputationAccumulator10MilliAmpere); +# endif + /* + * Check for wrong current direction. 0 mA is no direction :-) + */ + if (tBattery10MilliAmpere > 0) { + sCapacityComputationWrongDirectionCount = 0; + } else if (tBattery10MilliAmpere < 0) { + sCapacityComputationWrongDirectionCount++; + if (sCapacityComputationWrongDirectionCount > CAPACITY_COMPUTATION_MAX_WRONG_CHARGE_DIRECTION) { + + if (JKComputedCapacity[0].Capacity != 0) { + // process only if we have valid data + Serial.println( + F( + "More than " STR(CAPACITY_COMPUTATION_MAX_WRONG_CHARGE_DIRECTION) " wrong current directions -> end capacity computation")); + printComputedCapacity(0); + checkAndStoreCapacityComputationValues(); + } + + /* + * Reset capacity computation mode + */ + memset(&JKComputedCapacity, 0, + (sizeof(JKComputedCapacity[0].StartSOC) + sizeof(JKComputedCapacity[0].EndSOC) + + sizeof(JKComputedCapacity[0].Capacity) + sizeof(JKComputedCapacity[0].TotalCapacity))); +// JKComputedCapacity[0].StartSOC = 0; +// JKComputedCapacity[0].EndSOC = 0; +// JKComputedCapacity[0].Capacity = 0; +// JKComputedCapacity[0].TotalCapacity = 0; + sLastCapacityComputationDeltaSOC = 0; + sCapacityComputationMode = CAPACITY_COMPUTATION_MODE_IDLE; + sCapacityComputationAccumulator10MilliAmpere = 0; + sCapacityComputationWrongDirectionCount = 0; + } } - sprintf_P(tStringBuffer, PSTR("%2u=%2u %% |%5u, "), i + 1, CellMaximumPercentageArray[i], CellMaximumArray[i]); - Serial.print(tStringBuffer); } - Serial.println(); +#endif // NO_INTERNAL_STATISTICS +} - Serial.println(); +#if !defined(NO_INTERNAL_STATISTICS) +/* + * Print only valid data, i.e. Capacity != 0 + */ +void printComputedCapacity(uint8_t aCapacityArrayIndex) { + if (JKComputedCapacity[aCapacityArrayIndex].Capacity != 0) { + snprintf_P(sStringBuffer, sizeof(sStringBuffer), PSTR("%u%% -> %u%% = %uAh => 100%% = %uAh"), + JKComputedCapacity[aCapacityArrayIndex].StartSOC, JKComputedCapacity[aCapacityArrayIndex].EndSOC, + JKComputedCapacity[aCapacityArrayIndex].Capacity, JKComputedCapacity[aCapacityArrayIndex].TotalCapacity); + Serial.println(sStringBuffer); + } } + +void checkAndStoreCapacityComputationValues() { + int8_t tDeltaSOC = JKComputedCapacity[0].StartSOC - JKComputedCapacity[0].EndSOC; + if (tDeltaSOC <= -40 || 40 <= tDeltaSOC) { + Serial.flush(); // TEST!!!!! +// JKComputedCapacity[3] = JKComputedCapacity[2]; +// JKComputedCapacity[2] = JKComputedCapacity[1]; +// JKComputedCapacity[1] = JKComputedCapacity[0]; +// substituted by memmove, requires 14 bytes more, but is more flexible + memmove(&JKComputedCapacity[1], &JKComputedCapacity[0], + ((sizeof(JKComputedCapacity[0].StartSOC) + sizeof(JKComputedCapacity[0].EndSOC) + + sizeof(JKComputedCapacity[0].Capacity) + sizeof(JKComputedCapacity[0].TotalCapacity))) + * (SIZE_OF_COMPUTED_CAPACITY_ARRAY - 1)); + + Serial.println(F("Store computed capacity")); + for (uint8_t i = 2; i < SIZE_OF_COMPUTED_CAPACITY_ARRAY; ++i) { + printComputedCapacity(i); + } + } +} +#endif // NO_INTERNAL_STATISTICS + /* * Print formatted cell info on Serial */ @@ -589,17 +768,22 @@ void printVoltageProtectionInfo() { /* * Voltage protection */ - myPrint(F("Battery Overvoltage Protection[mV]="), JKComputedData.BatteryFullVoltage10Millivolt * 10); - myPrintln(F(", Undervoltage="), swap(tJKFAllReply->BatteryUndervoltageProtection10Millivolt) * 10); + myPrint(F("Battery Overvoltage Protection[mV]="), (uint16_t) (JKComputedData.BatteryFullVoltage10Millivolt * 10)); + myPrintln(F(", Undervoltage="), (uint16_t) (swap(tJKFAllReply->BatteryUndervoltageProtection10Millivolt) * 10)); + myPrintSwap(F("Cell Overvoltage Protection[mV]="), tJKFAllReply->CellOvervoltageProtectionMillivolt); myPrintSwap(F(", Recovery="), tJKFAllReply->CellOvervoltageRecoveryMillivolt); myPrintlnSwap(F(", Delay[s]="), tJKFAllReply->CellOvervoltageDelaySeconds); + myPrintSwap(F("Cell Undervoltage Protection[mV]="), tJKFAllReply->CellUndervoltageProtectionMillivolt); myPrintSwap(F(", Recovery="), tJKFAllReply->CellUndervoltageRecoveryMillivolt); myPrintlnSwap(F(", Delay[s]="), tJKFAllReply->CellUndervoltageDelaySeconds); + myPrintlnSwap(F("Cell Voltage Difference Protection[mV]="), tJKFAllReply->VoltageDifferenceProtectionMillivolt); + myPrintSwap(F("Discharging Overcurrent Protection[A]="), tJKFAllReply->DischargeOvercurrentProtectionAmpere); myPrintlnSwap(F(", Delay[s]="), tJKFAllReply->DischargeOvercurrentDelaySeconds); + myPrintSwap(F("Charging Overcurrent Protection[A]="), tJKFAllReply->ChargeOvercurrentProtectionAmpere); myPrintlnSwap(F(", Delay[s]="), tJKFAllReply->ChargeOvercurrentDelaySeconds); Serial.println(); @@ -612,13 +796,18 @@ void printTemperatureProtectionInfo() { */ myPrintSwap(F("Power MosFet Temperature Protection="), tJKFAllReply->PowerMosFetTemperatureProtection); myPrintlnSwap(F(", Recovery="), tJKFAllReply->PowerMosFetRecoveryTemperature); + myPrintSwap(F("Sensor1 Temperature Protection="), tJKFAllReply->Sensor1TemperatureProtection); myPrintlnSwap(F(", Recovery="), tJKFAllReply->Sensor1RecoveryTemperature); + myPrintlnSwap(F("Sensor1 to Sensor2 Temperature Difference Protection="), tJKFAllReply->BatteryDifferenceTemperatureProtection); + myPrintSwap(F("Charge Overtemperature Protection="), tJKFAllReply->ChargeOvertemperatureProtection); myPrintlnSwap(F(", Discharge="), tJKFAllReply->DischargeOvertemperatureProtection); + myPrintSwap(F("Charge Undertemperature Protection="), tJKFAllReply->ChargeUndertemperatureProtection); myPrintlnSwap(F(", Recovery="), tJKFAllReply->ChargeRecoveryUndertemperature); + myPrintSwap(F("Discharge Undertemperature Protection="), tJKFAllReply->DischargeUndertemperatureProtection); myPrintlnSwap(F(", Recovery="), tJKFAllReply->DischargeRecoveryUndertemperature); Serial.println(); @@ -634,13 +823,16 @@ void printBatteryInfo() { Serial.print(F("Manufacturer Id=")); // First 8 characters of the manufacturer id entered in the app field "User Private Data" tJKFAllReply->TokenProtocolVersionNumber = '\0'; // Set end of string token Serial.println(tJKFAllReply->ManufacturerId); + Serial.print(F("Device ID String=")); // First 8 characters of ManufacturerId tJKFAllReply->TokenManufacturerDate = '\0'; // Set end of string token Serial.println(tJKFAllReply->DeviceIdString); myPrintln(F("Device Address="), tJKFAllReply->BoardAddress); + myPrint(F("Total Battery Capacity[Ah]="), JKComputedData.TotalCapacityAmpereHour); // 0xAA myPrintln(F(", Low Capacity Alarm Percent="), tJKFAllReply->LowCapacityAlarmPercent); // 0xB1 + myPrintlnSwap(F("Charging Cycles="), tJKFAllReply->Cycles); myPrintlnSwap(F("Total Charging Cycle Capacity="), tJKFAllReply->TotalBatteryCycleCapacity); myPrintSwap(F("# Battery Cells="), tJKFAllReply->NumberOfBatteryCells); // 0x8A Total number of battery strings @@ -652,9 +844,11 @@ void printBMSInfo() { JKReplyStruct *tJKFAllReply = sJKFAllReplyPointer; myPrintln(F("Protocol Version Number="), tJKFAllReply->ProtocolVersionNumber); + Serial.print(F("Software Version Number=")); tJKFAllReply->TokenStartCurrentCalibration = '\0'; // set end of string token Serial.println(tJKFAllReply->SoftwareVersionNumber); + Serial.print(F("Modify Parameter Password=")); tJKFAllReply->TokenDedicatedChargerSwitchState = '\0'; // set end of string token Serial.println(tJKFAllReply->ModifyParameterPassword); @@ -699,14 +893,13 @@ void handleAndPrintAlarmInfo() { if (tJKFAllReply->AlarmUnion.AlarmsAsWord == 0) { sErrorStringForLCD = NULL; // reset error string sErrorStatusIsError = false; - Serial.println(F("All alarms are cleared")); + Serial.println(F("All alarms are cleared now")); } else { uint16_t tAlarms = swap(tJKFAllReply->AlarmUnion.AlarmsAsWord); Serial.println(F("*** ALARM FLAGS ***")); - Serial.print(F("Alarm bits=0x")); - Serial.print(tAlarms, HEX); - Serial.print(F(" | 0b")); - Serial.println(tAlarms, BIN); + Serial.print(sUpTimeString); // print uptime to have a timestamp for the alarm + Serial.print(F(": Alarm bits=0x")); + Serial.println(tAlarms, HEX); uint16_t tAlarmMask = 1; for (uint_fast8_t i = 0; i < NUMBER_OF_DEFINED_ALARM_BITS; ++i) { @@ -765,7 +958,7 @@ void computeUpTimeString() { uint32_t tSystemWorkingMinutes = swap(sJKFAllReplyPointer->SystemWorkingMinutes); // 1 kByte for sprintf creates string "1234D23H12M" sprintf_P(sUpTimeString, PSTR("%4uD%02uH%02uM"), (uint16_t) (tSystemWorkingMinutes / (60 * 24)), - (uint16_t) ((tSystemWorkingMinutes / 60) % 24), (uint16_t) tSystemWorkingMinutes % 60); + (uint16_t) ((tSystemWorkingMinutes / 60) % 24), (uint16_t) (tSystemWorkingMinutes % 60)); if (sLastUpTimeTenthOfMinuteCharacter != sUpTimeString[8]) { sLastUpTimeTenthOfMinuteCharacter = sUpTimeString[8]; sUpTimeStringTenthOfMinuteHasChanged = true; @@ -781,9 +974,14 @@ void computeUpTimeString() { void printJKDynamicInfo() { JKReplyStruct *tJKFAllReply = sJKFAllReplyPointer; +#if defined(ENABLE_MONITORING) + printMonitoringInfo(); +#endif + /* * Print it every ten minutes */ +// // Print it every minute // if (sUpTimeStringMinuteHasChanged) { // sUpTimeStringMinuteHasChanged = false; if (sUpTimeStringTenthOfMinuteHasChanged) { @@ -796,7 +994,7 @@ void printJKDynamicInfo() { Serial.println(F("*** CELL INFO ***")); printJKCellInfo(); - +#if !defined(NO_INTERNAL_STATISTICS) if (sBalancingCount > MINIMUM_BALANCING_COUNT_FOR_DISPLAY) { Serial.println(F("*** CELL STATISTICS ***")); Serial.print(F("Total balancing time=")); @@ -810,6 +1008,7 @@ void printJKDynamicInfo() { Serial.println(tString); printJKCellStatisticsInfo(); } +#endif #if !defined(SUPPRESS_LIFEPO4_PLAUSI_WARNING) if (swap(tJKFAllReply->CellOvervoltageProtectionMillivolt) > 3450) { @@ -873,8 +1072,10 @@ void printJKDynamicInfo() { * Charge, Discharge and Balancer flags */ if (tJKFAllReply->BMSStatus.StatusBits.ChargeMosFetActive != lastJKReply.BMSStatus.StatusBits.ChargeMosFetActive - || tJKFAllReply->BMSStatus.StatusBits.DischargeMosFetActive != lastJKReply.BMSStatus.StatusBits.DischargeMosFetActive - || tJKFAllReply->BMSStatus.StatusBits.BalancerActive != lastJKReply.BMSStatus.StatusBits.BalancerActive) { + || tJKFAllReply->BMSStatus.StatusBits.DischargeMosFetActive != lastJKReply.BMSStatus.StatusBits.DischargeMosFetActive) { + /* + * This happens quite seldom! + */ Serial.print(F("Charging MosFet")); printEnabledState(tJKFAllReply->ChargeIsEnabled); Serial.print(','); @@ -883,13 +1084,66 @@ void printJKDynamicInfo() { printEnabledState(tJKFAllReply->DischargeIsEnabled); Serial.print(','); printActiveState(tJKFAllReply->BMSStatus.StatusBits.DischargeMosFetActive); - Serial.print(F(" | Balancing")); + Serial.print(F(" | Balancing")); // including balancer state to be complete :-) printEnabledState(tJKFAllReply->BalancingIsEnabled); Serial.print(','); printActiveState(tJKFAllReply->BMSStatus.StatusBits.BalancerActive); Serial.println(); // printActiveState does no println() + } else if (tJKFAllReply->BMSStatus.StatusBits.BalancerActive != lastJKReply.BMSStatus.StatusBits.BalancerActive) { + /* + * Only Balancer, since it happens very often + */ + Serial.print(F("Balancing")); + printActiveState(tJKFAllReply->BMSStatus.StatusBits.BalancerActive); + Serial.println(); // printActiveState does no println() + } +} + +#if defined(ENABLE_MONITORING) +/* + * Prints all (cell voltages - 3000 mV), voltage, current, SOC in CSV format + * E.g. 185;185;186;185;185;206;185;214;183;185;201;186;186;186;185;186;5096;-565;54;1 + * + */ +void setCSVString() { + JKReplyStruct *tJKFAllReply = sJKFAllReplyPointer; + /* + * Individual cell voltages + */ + uint_fast8_t tBufferIndex = 0; + +// for (uint8_t i = 0; i < JKConvertedCellInfo.ActualNumberOfCellInfoEntries; ++i) { + if (sizeof(sStringBuffer) > (5 * 16)) { + for (uint8_t i = 0; i < 16; ++i) { // only 16 fits into sStringBuffer[90] + // check for valid data, otherwise we will get a string buffer overflow + if (JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt > 2500) { + tBufferIndex += sprintf_P(&sStringBuffer[tBufferIndex], PSTR("%d;"), + (int16_t) (JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt - 3000)); // difference may become negative + } + } + } + + if ((uint_fast8_t) (tBufferIndex + 16) >= sizeof(sStringBuffer)) { + Serial.print(F("String buffer overflow, tBufferIndex=")); + Serial.print(tBufferIndex); + Serial.print(F(" sizeof(sStringBuffer)=")); + Serial.println(sizeof(sStringBuffer)); + sStringBuffer[0] = '\0'; + } else { + // maximal string 5096;-2000;100;1 -> 16 characters + sprintf_P(&sStringBuffer[tBufferIndex], PSTR("%u;%d;%d;%d"), JKComputedData.BatteryVoltage10Millivolt, + JKComputedData.Battery10MilliAmpere, tJKFAllReply->SOCPercent, tJKFAllReply->BMSStatus.StatusBits.BalancerActive); } } + +void printMonitoringInfo() { + setCSVString(); + + Serial.print(F("CSV: ")); + Serial.println(sStringBuffer); +} +#endif + #if defined(LOCAL_DEBUG) #undef LOCAL_DEBUG #endif diff --git a/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino b/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino index 7f08555..40c5071 100644 --- a/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino +++ b/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino @@ -49,9 +49,9 @@ * Copyright (C) 2023 Armin Joachimsmeyer * Email: armin.joachimsmeyer@gmail.com * - * This file is part of ArduinoUtils https://github.com/ArminJo/PVUtils. + * This file is part of ArduinoUtils https://github.com/ArminJo/JK-BMSToPylontechCAN. * - * Arduino-Utils is free software: you can redistribute it and/or modify + * JK-BMSToPylontechCAN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. @@ -93,7 +93,6 @@ /* * Ideas: * Balancing time per day / week / month etc. - * Maximum, minimum cell while balancing */ #include @@ -106,17 +105,34 @@ const uint8_t sSOCThresholdForForceCharge = SOC_THRESHOLD_FOR_FORCE_CHARGE_REQUE #define VERSION_EXAMPLE "2.3.0" +//#define USE_NO_LCD // The code for the LCD display is deactivated +#if !defined(USE_NO_LCD) +//#define NO_INTERNAL_STATISTICS // No cell values, cell minimum, maximum and percentages. No capacity. +#endif + +#define ENABLE_MONITORING // Write cell and current values CSV data to serial output +#if defined(ENABLE_MONITORING) +char sStringBuffer[90]; // for cvs lines, "Store computed capacity" line and LCD rows +#elif !defined(NO_INTERNAL_STATISTICS) +char sStringBuffer[40]; // for "Store computed capacity" line and LCD rows +#endif +//#define USE_SD_CARD_FOR_MONITORING // Write cell and current values CSV data to SD card into JK-BMS.CSV + /* * Pin layout, may be adapted to your requirements */ #define BUZZER_PIN A2 // To signal errors -#define LCD_PAGE_BUTTON_PIN 2 // Just for documentation +#define PAGE_BUTTON_PIN 2 // Just for documentation #define DEBUG_PIN 3 // Just for documentation. If pressed, print additional info and switch to LCD CAN page. // The standard RX of the Arduino is used for the JK_BMS connection. #define JK_BMS_RX_PIN 0 // We use the Serial RX pin. Not used in program, only for documentation #if !defined(JK_BMS_TX_PIN) // Allow override by global symbol #define JK_BMS_TX_PIN 4 #endif +#if defined(USE_SD_CARD_FOR_MONITORING) +#define SD_CS_PIN 8 +#endif + /* * The SPI pins for connection to CAN converter and the I2C / TWI pins for the LCD are determined by hardware. * For Uno / Nano: @@ -164,38 +180,6 @@ uint16_t sTimeoutFrameCounter = 0; // Counts BMS frame timeouts, (every 2 s //#define SUPPRESS_LIFEPO4_PLAUSI_WARNING // Disables warning on Serial out about using LiFePO4 beyond 3.0 v to 3.45 V. -//#define USE_NO_LCD -#if !defined(USE_NO_LCD) -#define USE_SERIAL_LCD -#endif - -#if defined(USE_SERIAL_LCD) || defined(USE_PARALLEL_LCD) -#define USE_LCD -#endif - -#if defined(USE_LCD) -/* - * Display timeouts, may be adapted to your requirements - */ -# if defined(STANDALONE_TEST) -#define DISPLAY_ON_TIME_STRING "30 s" -#define DISPLAY_ON_TIME_SECONDS 30L // L to avoid overflow at macro processing -//#define NO_MULTIPLE_BEEPS_ON_TIMEOUT // Activate it if you do not want multiple beeps -#define BEEP_ON_TIME_SECONDS_IF_TIMEOUT 10L // 10 s -# else -#define DISPLAY_ON_TIME_STRING "5 min" // Only for display on LCD -#define DISPLAY_ON_TIME_SECONDS 300L // 5 minutes. L to avoid overflow at macro processing -# endif // DEBUG - -//#define DISPLAY_ALWAYS_ON // Activate this, if you want the display to be always on. -# if !defined(DISPLAY_ALWAYS_ON) -void doLCDBacklightTimeoutHandling(); -bool checkAndTurnLCDOn(); -bool sSerialLCDIsSwitchedOff = false; -uint16_t sFrameCounterForLCDTAutoOff = 0; -# endif -#endif // defined(USE_LCD) - /* * Page button stuff * @@ -258,6 +242,34 @@ uint32_t sMillisOfLastReceivedByte = 0; // For timeout bool sCANDataIsInitialized = false; // One time flag, it is never set to false again. uint32_t sMillisOfLastCANFrameSent = 0; // For CAN timing +/* + * Optional LCD stuff + */ +#if !defined(USE_NO_LCD) +#include "JK-BMS_LCD.hpp" +#endif + +/* + * Optional SD card stuff + */ +#if defined(ENABLE_MONITORING) +const char sCaption[] PROGMEM + = "Cell_1;Cell_2;Cell_3;Cell_4;Cell_5;Cell_6;Cell_7;Cell_8;Cell_9;Cell_10;Cell_11;Cell_12;Cell_13;Cell_14;Cell_15;Cell_16;Voltage,Current;SOC;Balancing;"; + +# if defined(USE_SD_CARD_FOR_MONITORING) +#define CSV_DATA_8_3_FILENAME "JK-BMS.CSV" // is anyway converted to uppercase +//#include "SdFat.h" +//#include "sdios.h" +//SdFat32 SD; +//File32 LogFile; +#include +File LogFile; +#define DATASETS_BEFORE_SD_FLUSH 60 // flush every 60 datasets / every hour -> write directory and file length to SD +uint16_t sDatasetNumber = 1; +bool initSDCardAndOpenFile(); +# endif +#endif + /* * Optional sleep stuff */ @@ -270,53 +282,6 @@ void LoopDelayWithSleep(); bool sBMSFrameProcessingComplete = false; // True if one status frame was received and processed or timeout happened. Then we can do a sleep at the end of the loop. -/* - * Optional LCD hardware stuff - */ -#if defined(USE_SERIAL_LCD) -#define LCD_COLUMNS 20 -#define LCD_ROWS 4 -#define LCD_I2C_ADDRESS 0x27 // Default LCD address is 0x27 for a 20 chars and 4 line / 2004 display -bool sSerialLCDAvailable; - -#include "LiquidCrystal_I2C.hpp" // This defines USE_SOFT_I2C_MASTER, if SoftI2CMasterConfig.h is available. Use only the modified version delivered with this program! -LiquidCrystal_I2C myLCD(LCD_I2C_ADDRESS, LCD_COLUMNS, LCD_ROWS); - -/* - * Big numbers for LCD JK_BMS_PAGE_BIG_INFO page - */ -#define USE_SERIAL_2004_LCD // required by LCDBigNumbers.hpp -#include "LCDBigNumbers.hpp" // Include sources for LCD big number generation -//LCDBigNumbers bigNumberLCD(&myLCD, BIG_NUMBERS_FONT_2_COLUMN_3_ROWS_VARIANT_1); // Use 2x3 numbers, 1. variant -//#define UNITS_ROW_FOR_BIG_INFO 2 -LCDBigNumbers bigNumberLCD(&myLCD, BIG_NUMBERS_FONT_2_COLUMN_3_ROWS_VARIANT_2); // Use 2x3 numbers, 2. variant -#define UNITS_ROW_FOR_BIG_INFO 1 -const uint8_t bigNumbersTopBlock[8] PROGMEM = { 0x0F, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // char 1: top block for maximum cell voltage marker -const uint8_t bigNumbersBottomBlock[8] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x0F }; // char 2: bottom block for minimum cell voltage marker - -/* - * LCD display pages - */ -#define JK_BMS_PAGE_OVERVIEW 0 // is displayed in case of BMS error message -#define JK_BMS_PAGE_CELL_INFO 1 -#define JK_BMS_PAGE_CELL_STATISTICS 2 -#define CELL_STATISTICS_COUNTER_MASK 0x04 // must be a multiple of 2 and determines how often one page (min or max) is displayed. -#define JK_BMS_PAGE_BIG_INFO 3 -#define JK_BMS_PAGE_CAN_INFO 4 // If debug was pressed -#define JK_BMS_PAGE_MAX JK_BMS_PAGE_BIG_INFO -#define JK_BMS_START_PAGE JK_BMS_PAGE_BIG_INFO -//uint8_t sLCDDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; // Start with Overview page -uint8_t sLCDDisplayPageNumber = JK_BMS_START_PAGE; // Start with Big Info page -uint8_t sCellStatisticsDisplayCounter; // counter for CELL_STATISTICS_COUNTER_MASK, to determine max or min page -char sStringBuffer[7]; // For rendering numbers with sprintf_P() -void setDisplayPage(uint8_t aDisplayPageNumber); - -void printBMSDataOnLCD(); -void printCANInfoOnLCD(); -void LCDPrintSpaces(uint8_t aNumberOfSpacesToPrint); -void LCDClearLine(uint8_t aLineNumber); -#endif // defined(USE_SERIAL_LCD) - /* * Miscellaneous */ @@ -348,7 +313,8 @@ const uint8_t TestJKReplyStatusFrame[] PROGMEM = { /* Header*/0x4E, 0x57, 0x01, 0x0C, 0xC7, 0x09, 0x0C, 0xC2, 0x0A, 0x0C, 0xC2, 0x0B, 0x0C, 0xC2, 0x0C, 0x0C, 0xC2, 0x0D, 0x0C, 0xC1, 0x0E, 0x0C, 0xBE, 0x0F, 0x0C, 0xC1, 0x10, 0x0C, 0xC1, /*JKFrameAllDataStruct*/ - 0x80, 0x00, 0x16, 0x81, 0x00, 0x15, 0x82, 0x00, 0x15, /*Voltage*/0x83, 0x14, 0x6C, /*Current*/0x84, 0x80, 0xD0, /*SOC*/0x85, + 0x80, 0x00, 0x16, 0x81, 0x00, 0x15, 0x82, 0x00, 0x15, /*Voltage*/0x83, 0x14, 0x6C, /*Current*/0x84, 0x08, 0xD0, /*SOC*/0x85, +// 0x80, 0x00, 0x16, 0x81, 0x00, 0x15, 0x82, 0x00, 0x15, /*Voltage*/0x83, 0x14, 0x6C, /*Current*/0x84, 0x80, 0xD0, /*SOC*/0x85, 0x47, 0x86, 0x02, 0x87, 0x00, 0x04, 0x89, 0x00, 0x00, 0x01, 0xE0, 0x8A, 0x00, 0x0E, /*Warnings*/0x8B, 0x00, 0x00, 0x8C, 0x00, 0x07, 0x8E, 0x16, 0x26, 0x8F, 0x10, 0xAE, 0x90, 0x0F, 0xD2, 0x91, 0x0F, 0xA0, 0x92, 0x00, 0x05, 0x93, 0x0B, 0xEA, 0x94, 0x0C, 0x1C, 0x95, 0x00, 0x05, 0x96, 0x01, 0x2C, 0x97, 0x00, 0x07, 0x98, 0x00, 0x03, 0x99, 0x00, 0x05, 0x9A, 0x00, @@ -391,38 +357,19 @@ delay(4000); // To be able to connect Serial monitor after reset or power up and // Just to know which program is running on my Arduino Serial.println(F("START " __FILE__ "\r\nVersion " VERSION_EXAMPLE " from " __DATE__)); - tone(BUZZER_PIN, 2200, 50); - -#if defined(USE_SERIAL_LCD) - /* - * Initialize I2C and check for bus lockup - */ - if (!i2c_init()) { - Serial.println(F("I2C init failed")); - } - - /* - * Check for LCD connected - */ - sSerialLCDAvailable = i2c_start(LCD_I2C_ADDRESS << 1); - i2c_stop(); +#if defined(ENABLE_MONITORING) + Serial.println(F("Monitoring enabled")); +#endif +#if defined(NO_INTERNAL_STATISTICS) + Serial.println(F("Statistics deactivated")); +#endif - if (sSerialLCDAvailable) { - /* - * Print program, version and date on the upper two LCD lines - */ - myLCD.init(); - myLCD.clear(); - myLCD.backlight(); // Switch backlight LED on - myLCD.setCursor(0, 0); - myLCD.print(F("JK-BMS to CAN conv.")); - myLCD.setCursor(0, 1); - myLCD.print(F(VERSION_EXAMPLE " " __DATE__)); - bigNumberLCD.begin(); // This creates the custom character used for printing big numbers + tone(BUZZER_PIN, 2200, 50); - } else { - Serial.println(F("No I2C LCD connected at address " STR(LCD_I2C_ADDRESS))); - } +#if defined(USE_SERIAL_2004_LCD) + setupLCD(); +#else + Serial.println(F("LCD code deactivated")); #endif /* @@ -430,19 +377,19 @@ delay(4000); // To be able to connect Serial monitor after reset or power up and */ TxToJKBMS.begin(115200); Serial.println(F("Serial to JK-BMS started with 115200 bit/s!")); -#if defined(USE_LCD) +#if defined(USE_SERIAL_2004_LCD) if (sSerialLCDAvailable) { myLCD.setCursor(0, 2); myLCD.print(F("BMS serial started")); } #endif + /* * CAN initialization */ - // if (CAN.begin(500000)) { // Resets the device and start the CAN bus at 500 kbps if (initializeCAN(CAN_BAUDRATE, MHZ_OF_CRYSTAL_ASSEMBLED_ON_CAN_MODULE, &Serial) == MCP2515_RETURN_OK) { // Resets the device and start the CAN bus at 500 kbps Serial.println(F("CAN started with 500 kbit/s!")); -#if defined(USE_LCD) +#if defined(USE_SERIAL_2004_LCD) if (sSerialLCDAvailable) { myLCD.setCursor(0, 3); myLCD.print(F("CAN started")); @@ -451,7 +398,7 @@ delay(4000); // To be able to connect Serial monitor after reset or power up and #endif } else { Serial.println(F("Starting CAN failed!")); -#if defined(USE_LCD) +#if defined(USE_SERIAL_2004_LCD) if (sSerialLCDAvailable) { myLCD.setCursor(0, 3); myLCD.print(F("Starting CAN failed!")); @@ -468,39 +415,28 @@ delay(4000); // To be able to connect Serial monitor after reset or power up and /* * Print debug pin info */ -#if defined(USE_LCD) - Serial.println(F("Page switching button is at pin " STR(LCD_PAGE_BUTTON_PIN))); +#if defined(USE_SERIAL_2004_LCD) + Serial.println(F("Page switching button is at pin " STR(PAGE_BUTTON_PIN))); Serial.println(F("At long press, CAN Info page is entered and additional debug data is printed as long as button is pressed")); #else - Serial.println(F("Debug button is at pin " STR(LCD_PAGE_BUTTON_PIN))); + Serial.println(F("Debug button is at pin " STR(PAGE_BUTTON_PIN))); Serial.println(F("Additional debug data is printed as long as button is pressed")); #endif Serial.println(F(STR(MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS) " ms between 2 BMS requests")); Serial.println(F(STR(MILLISECONDS_BETWEEN_CAN_FRAME_SEND) " ms between 2 CAN transmissions")); -#if defined(USE_LCD) && !defined(DISPLAY_ALWAYS_ON) +#if defined(USE_SERIAL_2004_LCD) && !defined(DISPLAY_ALWAYS_ON) Serial.println(F("LCD Backlight timeout is " DISPLAY_ON_TIME_STRING)); +#else + Serial.println(F("No LCD Backlight timeout")); #endif Serial.println(); -#if defined(USE_LCD) - if (sSerialLCDAvailable) { -# if !defined(DISPLAY_ALWAYS_ON) - myLCD.setCursor(0, 0); - myLCD.print(F("Screen timeout " DISPLAY_ON_TIME_STRING)); -# endif - myLCD.setCursor(0, 1); - myLCD.print(F("Long press = debug")); - myLCD.setCursor(0, 2); -#if defined(STANDALONE_TEST) - myLCD.print(F("Test -fixed BMS data")); -#else - myLCD.print(F("Get BMS every " SECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS " s ")); +#if defined(USE_SERIAL_2004_LCD) + printDebugInfoOnLCD(); #endif - myLCD.setCursor(0, 3); - myLCD.print(F("Send CAN every " SECONDS_BETWEEN_CAN_FRAME_SEND " s ")); - delay(2000); // To see the messages - myLCD.clear(); - } + +#if defined(USE_SD_CARD_FOR_MONITORING) + initSDCardAndOpenFile(); #endif #if defined(USE_SLEEP) @@ -523,7 +459,10 @@ delay(4000); // To be able to connect Serial monitor after reset or power up and * Copy complete reply and computed values for change determination */ lastJKComputedData = JKComputedData; - lastJKReply = *sJKFAllReplyPointer; // 221 bytes + lastJKReply.SOCPercent = sJKFAllReplyPointer->SOCPercent; + lastJKReply.AlarmUnion.AlarmsAsWord = sJKFAllReplyPointer->AlarmUnion.AlarmsAsWord; + lastJKReply.BMSStatus.StatusAsWord = sJKFAllReplyPointer->BMSStatus.StatusAsWord; + lastJKReply.SystemWorkingMinutes = sJKFAllReplyPointer->SystemWorkingMinutes; doStandaloneTest(); #endif } @@ -570,6 +509,9 @@ void loop() { digitalWriteFast(TIMING_TEST_PIN, HIGH); # endif if (readJK_BMSStatusFrame()) { + /* + * Frame completely received, now process it + */ processJK_BMSStatusFrame(); // Process the complete receiving of the status frame and set the appropriate flags } # if defined(TIMING_TEST) @@ -626,7 +568,7 @@ void loop() { * Not required for non errors */ sErrorStatusJustChanged = false; -#if defined(USE_LCD) +#if defined(USE_SERIAL_2004_LCD) setDisplayPage(JK_BMS_PAGE_OVERVIEW); # if !defined(DISPLAY_ALWAYS_ON) if (checkAndTurnLCDOn()) { @@ -637,7 +579,7 @@ void loop() { } } -#if defined(USE_LCD) && !defined(DISPLAY_ALWAYS_ON) +#if defined(USE_SERIAL_2004_LCD) && !defined(DISPLAY_ALWAYS_ON) if (sSerialLCDAvailable) { doLCDBacklightTimeoutHandling(); } @@ -675,8 +617,7 @@ void loop() { tone(BUZZER_PIN, 2200, 50); delay(200); tone(BUZZER_PIN, 2200, 50); - delay(200); - noTone(BUZZER_PIN); // to avoid tone interrupts waking us up from sleep + delay(200); // to avoid tone interrupts waking us up from sleep } } #endif // NO_BEEP_ON_ERROR @@ -716,7 +657,7 @@ void processJK_BMSStatusFrame() { if (sTimeoutFrameCounter > 0) { // First frame after timeout sTimeoutFrameCounter = 0; -#if defined(USE_LCD) && !defined(DISPLAY_ALWAYS_ON) +#if defined(USE_SERIAL_2004_LCD) && !defined(DISPLAY_ALWAYS_ON) if (checkAndTurnLCDOn()) { Serial.println(F("successfully receiving first BMS status frame after BMS communication timeout")); // Switch on LCD display, triggered by successfully receiving first BMS status frame } @@ -728,7 +669,10 @@ void processJK_BMSStatusFrame() { * Copy complete reply and computed values for change determination */ lastJKComputedData = JKComputedData; - lastJKReply = *sJKFAllReplyPointer; // 221 bytes + lastJKReply.SOCPercent = sJKFAllReplyPointer->SOCPercent; + lastJKReply.AlarmUnion.AlarmsAsWord = sJKFAllReplyPointer->AlarmUnion.AlarmsAsWord; + lastJKReply.BMSStatus.StatusAsWord = sJKFAllReplyPointer->BMSStatus.StatusAsWord; + lastJKReply.SystemWorkingMinutes = sJKFAllReplyPointer->SystemWorkingMinutes; } /* @@ -780,7 +724,7 @@ void handleFrameReceiveTimeout() { printJKReplyFrameBuffer(); } modifyAllCanDataToInactive(); -#if defined(USE_LCD) +#if defined(USE_SERIAL_2004_LCD) if (sSerialLCDAvailable && sLCDDisplayPageNumber == JK_BMS_PAGE_CAN_INFO) { // Update the changed values on LCD myLCD.clear(); @@ -799,22 +743,8 @@ void handleFrameReceiveTimeout() { sTimeoutFrameCounter--; // To avoid overflow, we have an unsigned integer here } -#if defined(USE_LCD) - /* - * Global timeout message - */ - if (sSerialLCDAvailable -# if !defined(DISPLAY_ALWAYS_ON) - && !sSerialLCDIsSwitchedOff -# endif - && sLCDDisplayPageNumber != JK_BMS_PAGE_CAN_INFO) { - myLCD.clear(); - myLCD.setCursor(0, 0); - myLCD.print(F("Receive timeout ")); - myLCD.print(sTimeoutFrameCounter); - myLCD.setCursor(0, 1); - myLCD.print(F("Is BMS switched off?")); - } +#if defined(USE_SERIAL_2004_LCD) + printTimeoutMessageOnLCD(); #endif } @@ -842,563 +772,11 @@ void printReceivedData() { printJKStaticInfo(); } printJKDynamicInfo(); -#if defined(USE_LCD) - if (sSerialLCDAvailable -# if !defined(DISPLAY_ALWAYS_ON) - && !sSerialLCDIsSwitchedOff -# endif - ) { - printBMSDataOnLCD(); - } +#if defined(USE_SERIAL_2004_LCD) + printBMSDataOnLCD(); #endif } -#if defined(USE_LCD) -# if !defined(DISPLAY_ALWAYS_ON) - -/* - * Called on button press, BMS communication timeout and new error - * Always reset timeout counter! - * @return true if LCD was switched off before - */ -bool checkAndTurnLCDOn() { - sFrameCounterForLCDTAutoOff = 0; // Always start again to enable backlight switch off after 5 minutes - - if (sSerialLCDIsSwitchedOff) { - /* - * If backlight LED off, switch it on, but do not select next page, except if debug button was pressed. - */ - myLCD.backlight(); - sSerialLCDIsSwitchedOff = false; - Serial.print(F("Switch on LCD display, triggered by ")); // to be continued by caller - return true; - } - return false; -} -/* - * Display backlight handling - */ -void doLCDBacklightTimeoutHandling() { - sFrameCounterForLCDTAutoOff++; - if (sFrameCounterForLCDTAutoOff == 0) { - sFrameCounterForLCDTAutoOff--; // To avoid overflow, we have an unsigned integer here - } - - if (sFrameCounterForLCDTAutoOff == (DISPLAY_ON_TIME_SECONDS * 1000U) / MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS) { - myLCD.noBacklight(); // switch off backlight after 5 minutes - sSerialLCDIsSwitchedOff = true; - Serial.println(F("Switch off LCD display, triggered by LCD \"ON\" timeout reached.")); - } -} -# endif - -void LCDPrintSpaces(uint8_t aNumberOfSpacesToPrint) { - for (uint_fast8_t i = 0; i < aNumberOfSpacesToPrint; ++i) { - myLCD.print(' '); - } -} - -void LCDClearLine(uint8_t aLineNumber) { - myLCD.setCursor(0, aLineNumber); - LCDPrintSpaces(20); - myLCD.setCursor(0, aLineNumber); -} - -void printShortEnableFlagsOnLCD() { - if (sJKFAllReplyPointer->ChargeIsEnabled) { - myLCD.print('C'); - } else { - myLCD.print(' '); - } - if (sJKFAllReplyPointer->ChargeIsEnabled) { - myLCD.print('D'); - } else { - myLCD.print(' '); - } - if (sJKFAllReplyPointer->BalancingIsEnabled) { - myLCD.print('B'); - } -} - -/* - * Prints state of Charge Overvoltage warning, and Charge, Discharge and Balancing flag - */ -void printShortStateOnLCD() { - if (sJKFAllReplyPointer->AlarmUnion.AlarmBits.ChargeOvervoltageAlarm) { - myLCD.print('F'); - } else { - myLCD.print(' '); - } - if (sJKFAllReplyPointer->BMSStatus.StatusBits.ChargeMosFetActive) { - myLCD.print('C'); - } else { - myLCD.print(' '); - } - if (sJKFAllReplyPointer->BMSStatus.StatusBits.DischargeMosFetActive) { - myLCD.print('D'); - } else { - myLCD.print(' '); - } - if (sJKFAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { - myLCD.print('B'); - } -} - -void printLongStateOnLCD() { - if (sJKFAllReplyPointer->BMSStatus.StatusBits.ChargeMosFetActive) { - myLCD.print(F("CH ")); - } else { - myLCD.print(F(" ")); - } - - if (sJKFAllReplyPointer->BMSStatus.StatusBits.DischargeMosFetActive) { - myLCD.print(F("DC ")); - } else { - myLCD.print(F(" ")); - } - - if (sJKFAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { - myLCD.print(F("BAL")); - } -} - -/* - * Print current as 5 character including sign - */ -void printCurrentOnLCD() { - int16_t tBattery10MilliAmpere = JKComputedData.Battery10MilliAmpere; - if (tBattery10MilliAmpere >= 0) { - myLCD.print(' '); // handle not printed + sign - } - tBattery10MilliAmpere = abs(tBattery10MilliAmpere); // remove sign for length computation - uint8_t tNumberOfDecimalPlaces; - if (tBattery10MilliAmpere < 1000) { - // less than 10 A (1000 * 10mA) - tNumberOfDecimalPlaces = 2; // -9.99 - } else if (tBattery10MilliAmpere < 10000) { - tNumberOfDecimalPlaces = 1; // -99.9 - } else { - tNumberOfDecimalPlaces = 0; // -9999 - } - myLCD.print(JKComputedData.BatteryLoadCurrentFloat, tNumberOfDecimalPlaces); - myLCD.print(F("A ")); -} - -/* - * We can display only up to 16 cell values on the LCD :-( - */ -void printCellInfoOnLCD() { - uint_fast8_t tRowNumber; - auto tNumberOfCellInfoEntries = JKConvertedCellInfo.ActualNumberOfCellInfoEntries; - if (tNumberOfCellInfoEntries > 12) { - tRowNumber = 0; - if (tNumberOfCellInfoEntries > 16) { - tNumberOfCellInfoEntries = 16; - } - } else { - myLCD.print(F(" -CELL INFO-")); - tRowNumber = 1; - } - - for (uint8_t i = 0; i < tNumberOfCellInfoEntries; ++i) { - if (i % 4 == 0) { - myLCD.setCursor(0, tRowNumber); - tRowNumber++; - } - - // print maximum or minimum indicator - if (JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween == VOLTAGE_IS_MAXIMUM) { - myLCD.print((char) (0x1)); - } else if (JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween == VOLTAGE_IS_MINIMUM) { - myLCD.print((char) (0x2)); - } else { - myLCD.print(' '); - } - - myLCD.print(JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt); - } -} - -/* - * Switch between display of minimum and maximum at each 4. call - * The sum of percentages may not give 100% because of rounding errors - */ -void printCellStatisticsOnLCD() { - // check if minimum or maximum is to be displayed - bool tDisplayCellMinimumStatistics = sCellStatisticsDisplayCounter & CELL_STATISTICS_COUNTER_MASK; // 0x04 - sCellStatisticsDisplayCounter++; - - auto tNumberOfCellInfoEntries = JKConvertedCellInfo.ActualNumberOfCellInfoEntries; - uint_fast8_t tRowNumber; - if (tNumberOfCellInfoEntries > 12) { - tRowNumber = 0; - if (tNumberOfCellInfoEntries > 16) { - tNumberOfCellInfoEntries = 16; - } - } else { - if (tDisplayCellMinimumStatistics) { - myLCD.print(F("CELL Minimum Percent")); - } else { - myLCD.print(F("CELL Maximum Percent")); - } - tRowNumber = 1; - } - - char *tBalancingTimeStringPtr = &sBalancingTimeString[0]; - for (uint8_t i = 0; i < tNumberOfCellInfoEntries; ++i) { - uint8_t tPercent; - if (tDisplayCellMinimumStatistics) { - tPercent = CellMinimumPercentageArray[i]; - } else { - tPercent = CellMaximumPercentageArray[i]; - } - if (tPercent < 10) { - myLCD.print(' '); - } - myLCD.print(tPercent); - myLCD.print(F("% ")); - if (i % 4 == 3) { - /* - * Use the last 4 characters of a LCD row for information - * about min or max and the total balancing time - */ - if (i == 3) { - // first line - if (tDisplayCellMinimumStatistics) { - myLCD.print(F("MIN")); - } else { - myLCD.print(F("MAX")); - } - } else { - if (i == 7) { - // Second line print 4 characters days - myLCD.print(*tBalancingTimeStringPtr++); - } else { - myLCD.print(' '); - } - // print 3 character for hours or minutes - myLCD.print(*tBalancingTimeStringPtr++); - myLCD.print(*tBalancingTimeStringPtr++); - myLCD.print(*tBalancingTimeStringPtr++); - } - tRowNumber++; - myLCD.setCursor(0, tRowNumber); - } - } -} - -void printBigInfoOnLCD() { - bigNumberLCD.setBigNumberCursor(0, 0); - bigNumberLCD.print(sJKFAllReplyPointer->SOCPercent); - uint8_t tColumn; - if (sJKFAllReplyPointer->SOCPercent < 10) { - tColumn = 3; - } else if (sJKFAllReplyPointer->SOCPercent < 100) { - tColumn = 6; - } else { - tColumn = 8; // 100% - } - - myLCD.setCursor(tColumn, UNITS_ROW_FOR_BIG_INFO); // 3, 6 or 8 - myLCD.print('%'); - /* - * Here we can start the power string at column 4, 7 or 9 - */ - uint8_t tAvailableColumns = (LCD_COLUMNS - 2) - tColumn; - char tKiloWattChar = ' '; - int16_t tBatteryLoadPower = JKComputedData.BatteryLoadPower; - /* - * First print string to buffer - */ - if (tBatteryLoadPower >= 1000 || tBatteryLoadPower <= -1000) { - tKiloWattChar = 'k'; - float tBatteryLoadPowerFloat = tBatteryLoadPower * 0.001; // convert to kW - dtostrf(tBatteryLoadPowerFloat, 5, 2, sStringBuffer); - } else { - sprintf_P(sStringBuffer, PSTR("%d"), JKComputedData.BatteryLoadPower); - } - /* - * Then compute maximum possible string length - */ - uint8_t tColumnsRequiredForString = 0; - uint8_t i = 0; - while (true) { - char tChar = sStringBuffer[i]; - uint8_t tCharacterWidth; - if (tChar == '\0') { - break; // end of string - } else if (tChar == '.') { - tCharacterWidth = 1; // decimal point - } else if (tChar == '-') { - tCharacterWidth = 2; // minus sign - } else { - tCharacterWidth = 3; // plain number - } - - /* - * Check if next character can be rendered - */ - if (tAvailableColumns >= tCharacterWidth) { - tColumnsRequiredForString += tCharacterWidth; - tAvailableColumns -= tCharacterWidth; - } else { - /* - * Next character cannot be rendered here - */ - if (sStringBuffer[i - 1] == '.') { - // do not render trailing decimal point - tColumnsRequiredForString--; - tAvailableColumns++; - i--; - } - // Terminate string and exit - sStringBuffer[i] = '\0'; - break; - } - i++; - } - bigNumberLCD.setBigNumberCursor((LCD_COLUMNS - 1) - tColumnsRequiredForString, 0); - bigNumberLCD.print(sStringBuffer); -// Print units - myLCD.setCursor(19, UNITS_ROW_FOR_BIG_INFO - 1); - myLCD.print(tKiloWattChar); - myLCD.setCursor(19, UNITS_ROW_FOR_BIG_INFO); - myLCD.print('W'); - -// Bottom row: Max temperature, current and the actual states - myLCD.setCursor(0, 3); - myLCD.print(JKComputedData.TemperatureMaximum); - myLCD.print(F("\xDF ")); - - myLCD.setCursor(4, 3); - printCurrentOnLCD(); - - myLCD.setCursor(11, 3); - uint16_t tBatteryToFullDifference10Millivolt = JKComputedData.BatteryFullVoltage10Millivolt - - JKComputedData.BatteryVoltage10Millivolt; - if (tBatteryToFullDifference10Millivolt < 100) { - // Print small values as ".43" instead of "0.4" - sprintf_P(sStringBuffer, PSTR(".%02d"), tBatteryToFullDifference10Millivolt); - myLCD.print(sStringBuffer); - } else { - myLCD.print(((float) tBatteryToFullDifference10Millivolt) / 100.0, 1); - } - myLCD.print('V'); - - myLCD.setCursor(16, 3); - printShortStateOnLCD(); -} - -void printCANInfoOnLCD() { - /* - * sLCDDisplayPageNumber == JK_BMS_PAGE_CAN_INFO - */ - if (!sCANDataIsInitialized || JKComputedData.BMSIsStarting) { - myLCD.print(F("No CAN data are sent")); - myLCD.setCursor(0, 1); - if (JKComputedData.BMSIsStarting) { - myLCD.print(F("BMS is starting")); - } else { - myLCD.print(F("No BMS data received")); - } - } else { - PylontechCANFrameStruct *tCANFrameDataPointer = - reinterpret_cast(&PylontechCANErrorsWarningsFrame); - if (tCANFrameDataPointer->FrameData.ULong.LowLong == 0) { - /* - * Caption in row 1 and "No errors / warnings" in row 2 - */ - myLCD.print(F(" -CAN INFO- ")); - if (PylontechCANSohSocFrame.FrameData.SOCPercent < 100) { - myLCD.print(' '); - } - myLCD.print(F("SOC=")); - myLCD.print(PylontechCANSohSocFrame.FrameData.SOCPercent); - myLCD.print('%'); - myLCD.setCursor(0, 1); - myLCD.print(F("No errors / warnings")); - } else { - /* - * Errors in row 1 and warnings in row 2 - */ - myLCD.print(F("Errors: 0x")); - myLCD.print(tCANFrameDataPointer->FrameData.UBytes[0], HEX); - myLCD.print(F(" 0x")); - myLCD.print(tCANFrameDataPointer->FrameData.UBytes[1], HEX); - myLCD.setCursor(0, 1); - myLCD.print(F("Warnings: 0x")); - myLCD.print(tCANFrameDataPointer->FrameData.UBytes[2], HEX); - myLCD.print(F(" 0x")); - myLCD.print(tCANFrameDataPointer->FrameData.UBytes[3], HEX); - } - /* - * Voltage, current and maximum temperature in row 3 - */ - myLCD.setCursor(0, 2); - // Voltage - myLCD.print(PylontechCANCurrentValuesFrame.FrameData.Voltage10Millivolt / 100); - myLCD.print('.'); - myLCD.print(PylontechCANCurrentValuesFrame.FrameData.Voltage10Millivolt % 100); - myLCD.print(F("V ")); - // Current - int16_t tCurrent100Milliampere = PylontechCANCurrentValuesFrame.FrameData.Current100Milliampere; - myLCD.print(tCurrent100Milliampere / 10); - if (tCurrent100Milliampere < 0) { - // avoid negative numbers after decimal point - tCurrent100Milliampere = -tCurrent100Milliampere; - } - if (tCurrent100Milliampere < 100) { - // Print fraction if value < 100 - myLCD.print('.'); - myLCD.print(tCurrent100Milliampere % 10); - } - myLCD.print(F("A ")); - // Temperature - myLCD.setCursor(14, 2); - myLCD.print(PylontechCANCurrentValuesFrame.FrameData.Temperature100Millicelsius / 10); - if (PylontechCANCurrentValuesFrame.FrameData.Temperature100Millicelsius < 100) { - // Print fraction if value < 100 - myLCD.print('.'); - myLCD.print(PylontechCANCurrentValuesFrame.FrameData.Temperature100Millicelsius % 10); - } - myLCD.print(F("\xDF" "C ")); - - /* - * Request flags in row 4 - */ - myLCD.setCursor(0, 3); - // Charge enable - if (PylontechCANBatteryRequestFrame.FrameData.ChargeEnable) { - myLCD.print(F("CH ")); - } else { - myLCD.print(F(" ")); - } - if (PylontechCANBatteryRequestFrame.FrameData.DischargeEnable) { - myLCD.print(F("DC")); - } - myLCD.setCursor(6, 3); - if (PylontechCANBatteryRequestFrame.FrameData.ForceChargeRequestI) { - myLCD.print(F("FORCEI")); - } - myLCD.setCursor(13, 3); - if (PylontechCANBatteryRequestFrame.FrameData.ForceChargeRequestII) { - myLCD.print(F("FORCEII")); - } - - // Currently constant 0 -// myLCD.setCursor(10, 3); -// if (PylontechCANBatteryRequestFrame.FrameData.FullChargeRequest) { -// myLCD.print(F("FULL")); -// } - } -} - -void printOverwiewInfoOnLCD() { - /* - * Top row 1 - Error message or up time - */ - if (sErrorStringForLCD != NULL) { - // print not more than 20 characters - char t20CharacterString[LCD_COLUMNS + 1]; - memcpy_P(t20CharacterString, sErrorStringForLCD, LCD_COLUMNS); - t20CharacterString[LCD_COLUMNS] = '\0'; - myLCD.print(t20CharacterString); - } else { - myLCD.print(F("Uptime: ")); - myLCD.print(sUpTimeString); - } - /* - * Row 2 - SOC and remaining capacity and state of MosFets or Error - */ - myLCD.setCursor(0, 1); -// Percent of charge - myLCD.print(sJKFAllReplyPointer->SOCPercent); - myLCD.print(F("% ")); -// Remaining capacity - myLCD.print(JKComputedData.RemainingCapacityAmpereHour); - myLCD.print(F("Ah ")); - if (JKComputedData.RemainingCapacityAmpereHour < 100) { - myLCD.print(' '); - } - // Last 3 characters are the enable states - myLCD.setCursor(14, 1); - myLCD.print(F("En:")); - printShortEnableFlagsOnLCD(); - - /* - * Row 3 - Voltage, Current and Power - */ - myLCD.setCursor(0, 2); -// Voltage - myLCD.print(JKComputedData.BatteryVoltageFloat, 2); - myLCD.print(F("V ")); -// Current - printCurrentOnLCD(); -// Power - if (JKComputedData.BatteryLoadPower < -10000) { - // over 10 kW - myLCD.setCursor(13, 2); - myLCD.print(JKComputedData.BatteryLoadPower); // requires 6 columns - } else { - myLCD.setCursor(14, 2); - sprintf_P(sStringBuffer, PSTR("%5d"), JKComputedData.BatteryLoadPower); // force use of 5 columns - myLCD.print(sStringBuffer); - } - myLCD.print('W'); - /* - * Row 4 - 3 Temperatures and 3 enable states - */ - myLCD.setCursor(0, 3); -// 3 temperatures - myLCD.print(JKComputedData.TemperaturePowerMosFet); - myLCD.print(F("\xDF" "C ")); - myLCD.print(JKComputedData.TemperatureSensor1); - myLCD.print(F("\xDF" "C ")); - myLCD.print(JKComputedData.TemperatureSensor2); - myLCD.print(F("\xDF" "C ")); -// Last 4 characters are the actual states - myLCD.setCursor(16, 3); - printShortStateOnLCD(); - -} - -void printBMSDataOnLCD() { - myLCD.clear(); - myLCD.setCursor(0, 0); - - if (sLCDDisplayPageNumber == JK_BMS_PAGE_OVERVIEW) { - printOverwiewInfoOnLCD(); - - } else if (sLCDDisplayPageNumber == JK_BMS_PAGE_CELL_INFO) { - printCellInfoOnLCD(); - - } else if (sLCDDisplayPageNumber == JK_BMS_PAGE_CELL_STATISTICS) { - printCellStatisticsOnLCD(); - - } else if (sLCDDisplayPageNumber == JK_BMS_PAGE_BIG_INFO) { - printBigInfoOnLCD(); - - } else { //sLCDDisplayPageNumber == JK_BMS_PAGE_CAN_INFO - printCANInfoOnLCD(); - } -} - -void setDisplayPage(uint8_t aDisplayPageNumber) { - sLCDDisplayPageNumber = aDisplayPageNumber; - tone(BUZZER_PIN, 2200, 30); - Serial.print(F("Set LCD display page to: ")); - Serial.println(aDisplayPageNumber); - - // If BMS communication timeout, only timeout message or CAN Info page can be displayed. - if (!sJKBMSFrameHasTimeout || aDisplayPageNumber == JK_BMS_PAGE_CAN_INFO) { - /* - * Show new page - */ - printBMSDataOnLCD(); - } -} -#endif // #if defined(USE_LCD) - /* * Callback handlers for button press * Just set flags for evaluation in checkButtonPress(), otherwise readButtonState() may again be false when checkButtonPress() is called @@ -1411,64 +789,8 @@ void handlePageButtonPress(bool aButtonToggleState __attribute__((unused))) { * Manually handle button press */ void checkButtonPress() { -#if defined(USE_LCD) - if (sSerialLCDAvailable) { - /* - * Handle Page button - */ - uint8_t tDisplayPageNumber = sLCDDisplayPageNumber; - if (sPageButtonJustPressed) { - sPageButtonJustPressed = false; -# if !defined(DISPLAY_ALWAYS_ON) - /* - * If backlight LED off, switch it on, but do not select next page - */ - if (checkAndTurnLCDOn()) { - Serial.println(F("button press")); // Switch on LCD display, triggered by button press - sPageButtonJustPressed = false; // avoid switching pages if page button was pressed. - } else -# endif - { - /* - * Switch display pages to next page - */ - tDisplayPageNumber++; - - if (sJKBMSFrameHasTimeout || tDisplayPageNumber > JK_BMS_PAGE_MAX) { - // Receive timeout or wrap around here - tDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; - - } else if (tDisplayPageNumber == JK_BMS_PAGE_CELL_INFO) { - // Create symbols character for maximum and minimum - bigNumberLCD._createChar(1, bigNumbersTopBlock); - bigNumberLCD._createChar(2, bigNumbersBottomBlock); - - // Prepare for statistics page here display max first but for half the regular time - sCellStatisticsDisplayCounter = (CELL_STATISTICS_COUNTER_MASK >> 1) - 1; - - } else if (tDisplayPageNumber == JK_BMS_PAGE_BIG_INFO) { - bigNumberLCD.begin(); // Creates custom character used for generating big numbers - } - setDisplayPage(tDisplayPageNumber); - } - } else if (PageSwitchButtonAtPin2.readDebouncedButtonState()) { - if (tDisplayPageNumber == JK_BMS_PAGE_CAN_INFO) { - // Button is still pressed - sDebugModeActivated = true; // Is set to false in loop - } - if (PageSwitchButtonAtPin2.checkForLongPress(LONG_PRESS_BUTTON_DURATION_MILLIS) == EASY_BUTTON_LONG_PRESS_DETECTED) { - /* - * Long press detected -> switch to CAN Info page - */ - sDebugModeActivated = true; // Is set to false in loop - if (sSerialLCDAvailable && tDisplayPageNumber != JK_BMS_PAGE_CAN_INFO) { - Serial.println(); - Serial.println(F("Long press detected -> switch to CAN page and activate one time debug print")); - setDisplayPage(JK_BMS_PAGE_CAN_INFO); - } - } - } // PageSwitchButtonAtPin2.ButtonStateHasJustChanged - } +#if defined(USE_SERIAL_2004_LCD) + checkButtonPressForLCD(); #else /* * Treat Page button as Debug button @@ -1481,7 +803,7 @@ void checkButtonPress() { // Button is still pressed sDebugModeActivated = true; // Is set to false in loop } -#endif // defined(USE_LCD) +#endif // defined(USE_SERIAL_2004_LCD) } @@ -1501,110 +823,10 @@ void doStandaloneTest() { # endif } -void testLCDPages() { - sLCDDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; - printBMSDataOnLCD(); - - sLCDDisplayPageNumber = JK_BMS_PAGE_CELL_INFO; -// Create symbols character for maximum and minimum - bigNumberLCD._createChar(1, bigNumbersTopBlock); - bigNumberLCD._createChar(2, bigNumbersBottomBlock); - delay(2000); - printBMSDataOnLCD(); - - sLCDDisplayPageNumber = JK_BMS_PAGE_CAN_INFO; - delay(2000); - printBMSDataOnLCD(); - - /* - * Check display of maximum values - */ - sJKFAllReplyPointer->SOCPercent = 100; - JKComputedData.BatteryLoadPower = -11000; - JKComputedData.BatteryLoadCurrentFloat = JKComputedData.BatteryLoadPower / JKComputedData.BatteryVoltageFloat; - JKComputedData.TemperaturePowerMosFet = 111; - JKComputedData.TemperatureSensor1 = 100; - - sLCDDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; - delay(2000); - printBMSDataOnLCD(); - - sLCDDisplayPageNumber = JK_BMS_PAGE_BIG_INFO; - bigNumberLCD.begin(); - delay(2000); - printBMSDataOnLCD(); - - /* - * Test other values - */ - sJKFAllReplyPointer->AlarmUnion.AlarmBits.PowerMosFetOvertemperatureAlarm = true; - JKComputedData.TemperaturePowerMosFet = 90; - JKComputedData.TemperatureSensor1 = 25; - handleAndPrintAlarmInfo(); // this sets the LCD alarm string - - sJKFAllReplyPointer->SOCPercent = 1; - JKComputedData.BatteryLoadPower = 12345; - JKComputedData.BatteryLoadCurrentFloat = JKComputedData.BatteryLoadPower / JKComputedData.BatteryVoltageFloat; - - sLCDDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; - delay(2000); - printBMSDataOnLCD(); - - sLCDDisplayPageNumber = JK_BMS_PAGE_BIG_INFO; - delay(2000); - printBMSDataOnLCD(); - - lastJKReply.AlarmUnion.AlarmBits.PowerMosFetOvertemperatureAlarm = true; // to enable reset of LCD alarm string - sJKFAllReplyPointer->AlarmUnion.AlarmBits.PowerMosFetOvertemperatureAlarm = false; - JKComputedData.TemperaturePowerMosFet = 33; - handleAndPrintAlarmInfo(); // this resets the LCD alarm string - - sJKFAllReplyPointer->SOCPercent = 100; - JKComputedData.BatteryLoadCurrentFloat = -100; - - sLCDDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; - delay(2000); - printBMSDataOnLCD(); -} - -void testBigNumbers() { - sLCDDisplayPageNumber = JK_BMS_PAGE_BIG_INFO; - - for (int j = 0; j < 3; ++j) { - // Test with 100 % and 42 % - - /* - * test with positive numbers - */ - JKComputedData.BatteryLoadPower = 12345; - JKComputedData.BatteryLoadCurrentFloat = JKComputedData.BatteryLoadPower / JKComputedData.BatteryVoltageFloat; - - for (int i = 0; i < 5; ++i) { - delay(4000); - printBMSDataOnLCD(); - JKComputedData.BatteryLoadPower /= 10; // 1234 -> 12 - JKComputedData.BatteryLoadCurrentFloat = JKComputedData.BatteryLoadPower / JKComputedData.BatteryVoltageFloat; - } - /* - * test with negative numbers - */ - JKComputedData.BatteryLoadPower = -12345; - JKComputedData.BatteryLoadCurrentFloat = JKComputedData.BatteryLoadPower / JKComputedData.BatteryVoltageFloat; - - for (int i = 0; i < 5; ++i) { - delay(4000); - printBMSDataOnLCD(); - JKComputedData.BatteryLoadPower /= 10; // 1234 -> 12 - JKComputedData.BatteryLoadCurrentFloat = JKComputedData.BatteryLoadPower / JKComputedData.BatteryVoltageFloat; - } - - sJKFAllReplyPointer->SOCPercent /= 10; - } -} #endif void handleOvervoltage() { -#if defined(USE_LCD) +#if defined(USE_SERIAL_2004_LCD) if (sSerialLCDAvailable) { # if !defined(DISPLAY_ALWAYS_ON) if (sSerialLCDIsSwitchedOff) { @@ -1626,7 +848,7 @@ void handleOvervoltage() { delay(1000); } -#ifndef _ADC_UTILS_HPP +#if !defined(_ADC_UTILS_HPP) /* * Recommended VCC is 1.8 V to 5.5 V, absolute maximum VCC is 6.0 V. * Check for 5.25 V, because such overvoltage is quite unlikely to happen during regular operation. @@ -1649,7 +871,28 @@ bool isVCCTooHighSimple() { return tRawValue < 214; } -#endif +#endif // _ADC_UTILS_HPP + +#if defined(USE_SD_CARD_FOR_MONITORING) +/* + * @return true, if begin and open was successful + */ +bool initSDCardAndOpenFile() { +// if (SD.begin(SD_CS_PIN)) { + if (SD.begin(SD_CS_PIN)) { + bool tFileAlreadyExists = SD.exists(CSV_DATA_8_3_FILENAME); + LogFile = SD.open(CSV_DATA_8_3_FILENAME, FILE_WRITE); // FILE_WRITE -> append if existent + if (!tFileAlreadyExists) { + // Write CSV caption. This is not stored to file until next flush! + LogFile.println((__FlashStringHelper*) sCaption); + sDatasetNumber = 1; + Serial.println(F("Writing caption to SD card buffer.")); + } + return true; + } + return false; +} +#endif // USE_SD_CARD_FOR_MONITORING #if defined(USE_SLEEP) void LoopDelayWithSleep() { diff --git a/JK-BMSToPylontechCAN/JK-BMS_LCD.hpp b/JK-BMSToPylontechCAN/JK-BMS_LCD.hpp new file mode 100644 index 0000000..bbcfb92 --- /dev/null +++ b/JK-BMSToPylontechCAN/JK-BMS_LCD.hpp @@ -0,0 +1,918 @@ +/* + * JK-BMS_LCD.hpp + * + * Contains LCD related variables and functions + * + * Copyright (C) 2023 Armin Joachimsmeyer + * Email: armin.joachimsmeyer@gmail.com + * + * This file is part of ArduinoUtils https://github.com/ArminJo/JK-BMSToPylontechCAN. + * + * JK-BMSToPylontechCAN is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef _JK_BMS_LCD_HPP +#define _JK_BMS_LCD_HPP + +#define USE_SERIAL_2004_LCD // Parallel or 1604 LCD not yet supported + +/* + * LCD hardware stuff + */ +#define LCD_COLUMNS 20 +#define LCD_ROWS 4 +#define LCD_I2C_ADDRESS 0x27 // Default LCD address is 0x27 for a 20 chars and 4 line / 2004 display +bool sSerialLCDAvailable; + +/* + * Display timeouts, may be adapted to your requirements + */ +# if defined(STANDALONE_TEST) +#define DISPLAY_ON_TIME_STRING "30 s" +#define DISPLAY_ON_TIME_SECONDS 30L // L to avoid overflow at macro processing +//#define NO_MULTIPLE_BEEPS_ON_TIMEOUT // Activate it if you do not want multiple beeps +#define BEEP_ON_TIME_SECONDS_IF_TIMEOUT 10L // 10 s +# else +#define DISPLAY_ON_TIME_STRING "5 min" // Only for display on LCD +#define DISPLAY_ON_TIME_SECONDS 300L // 5 minutes. L to avoid overflow at macro processing +# endif // DEBUG + +//#define DISPLAY_ALWAYS_ON // Activate this, if you want the display to be always on. +# if !defined(DISPLAY_ALWAYS_ON) +void doLCDBacklightTimeoutHandling(); +bool checkAndTurnLCDOn(); +bool sSerialLCDIsSwitchedOff = false; +uint16_t sFrameCounterForLCDTAutoOff = 0; +# endif + +#include "LiquidCrystal_I2C.hpp" // This defines USE_SOFT_I2C_MASTER, if SoftI2CMasterConfig.h is available. Use only the modified version delivered with this program! +LiquidCrystal_I2C myLCD(LCD_I2C_ADDRESS, LCD_COLUMNS, LCD_ROWS); + +/* + * Big numbers for LCD JK_BMS_PAGE_BIG_INFO page + */ +#define USE_SERIAL_2004_LCD // required by LCDBigNumbers.hpp +#include "LCDBigNumbers.hpp" // Include sources for LCD big number generation +//LCDBigNumbers bigNumberLCD(&myLCD, BIG_NUMBERS_FONT_2_COLUMN_3_ROWS_VARIANT_1); // Use 2x3 numbers, 1. variant +//#define UNITS_ROW_FOR_BIG_INFO 2 +LCDBigNumbers bigNumberLCD(&myLCD, BIG_NUMBERS_FONT_2_COLUMN_3_ROWS_VARIANT_2); // Use 2x3 numbers, 2. variant +#define UNITS_ROW_FOR_BIG_INFO 1 +const uint8_t bigNumbersTopBlock[8] PROGMEM = { 0x0F, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // char 1: top block for maximum cell voltage marker +const uint8_t bigNumbersBottomBlock[8] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x0F }; // char 2: bottom block for minimum cell voltage marker + +/* + * LCD display pages + */ +#define JK_BMS_PAGE_OVERVIEW 0 // is displayed in case of BMS error message +#define JK_BMS_PAGE_CELL_INFO 1 +#define JK_BMS_PAGE_CELL_STATISTICS 2 +#define CELL_STATISTICS_COUNTER_MASK 0x04 // must be a multiple of 2 and determines how often one page (min or max) is displayed. +#define JK_BMS_PAGE_BIG_INFO 3 +#define JK_BMS_PAGE_CAN_INFO 4 // If debug was pressed +#define JK_BMS_PAGE_CAPACITY_INFO 5 // If debug was pressed +#define JK_BMS_PAGE_MAX JK_BMS_PAGE_BIG_INFO +#define JK_BMS_DEBUG_PAGE_MAX JK_BMS_PAGE_CAPACITY_INFO +#define JK_BMS_START_PAGE JK_BMS_PAGE_BIG_INFO +//uint8_t sLCDDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; // Start with Overview page +uint8_t sLCDDisplayPageNumber = JK_BMS_START_PAGE; // Start with Big Info page +uint8_t sCellStatisticsDisplayCounter; // counter for CELL_STATISTICS_COUNTER_MASK, to determine max or min page +#if !defined(ENABLE_MONITORING) && defined(NO_INTERNAL_STATISTICS) +char sStringBuffer[LCD_COLUMNS + 1]; // For rendering a LCD row with sprintf_P() +#endif +void setDisplayPage(uint8_t aDisplayPageNumber); + +void printBMSDataOnLCD(); +void printCANInfoOnLCD(); +void LCDPrintSpaces(uint8_t aNumberOfSpacesToPrint); +void LCDClearLine(uint8_t aLineNumber); + +void setupLCD() { + + /* + * Initialize I2C and check for bus lockup + */ + if (!i2c_init()) { + Serial.println(F("I2C init failed")); + } + + /* + * Check for LCD connected + */ + sSerialLCDAvailable = i2c_start(LCD_I2C_ADDRESS << 1); + i2c_stop(); + + if (sSerialLCDAvailable) { + /* + * Print program, version and date on the upper two LCD lines + */ + myLCD.init(); + myLCD.clear(); + myLCD.backlight(); // Switch backlight LED on + myLCD.setCursor(0, 0); + myLCD.print(F("JK-BMS to CAN conv.")); + myLCD.setCursor(0, 1); + myLCD.print(F(VERSION_EXAMPLE " " __DATE__)); + bigNumberLCD.begin(); // This creates the custom character used for printing big numbers + + } else { + Serial.println(F("No I2C LCD connected at address " STR(LCD_I2C_ADDRESS))); + } +} + +void printDebugInfoOnLCD() { + /* + * Print debug pin info + */ + + if (sSerialLCDAvailable) { +# if !defined(DISPLAY_ALWAYS_ON) + myLCD.setCursor(0, 0); + myLCD.print(F("Screen timeout " DISPLAY_ON_TIME_STRING)); +# endif + myLCD.setCursor(0, 1); + myLCD.print(F("Long press = debug")); + myLCD.setCursor(0, 2); +# if defined(STANDALONE_TEST) + myLCD.print(F("Test -fixed BMS data")); +# else + myLCD.print(F("Get BMS every " SECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS " s ")); +# endif + myLCD.setCursor(0, 3); + myLCD.print(F("Send CAN every " SECONDS_BETWEEN_CAN_FRAME_SEND " s ")); + delay(2000); // To see the messages + myLCD.clear(); + } +} + +# if !defined(DISPLAY_ALWAYS_ON) + +/* + * Called on button press, BMS communication timeout and new error + * Always reset timeout counter! + * @return true if LCD was switched off before + */ +bool checkAndTurnLCDOn() { + sFrameCounterForLCDTAutoOff = 0; // Always start again to enable backlight switch off after 5 minutes + + if (sSerialLCDIsSwitchedOff) { + /* + * If backlight LED off, switch it on, but do not select next page, except if debug button was pressed. + */ + myLCD.backlight(); + sSerialLCDIsSwitchedOff = false; + Serial.print(F("Switch on LCD display, triggered by ")); // to be continued by caller + return true; + } + return false; +} +/* + * Display backlight handling + */ +void doLCDBacklightTimeoutHandling() { + sFrameCounterForLCDTAutoOff++; + if (sFrameCounterForLCDTAutoOff == 0) { + sFrameCounterForLCDTAutoOff--; // To avoid overflow, we have an unsigned integer here + } + + if (sFrameCounterForLCDTAutoOff == (DISPLAY_ON_TIME_SECONDS * 1000U) / MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS) { + myLCD.noBacklight(); // switch off backlight after 5 minutes + sSerialLCDIsSwitchedOff = true; + Serial.println(F("Switch off LCD display, triggered by LCD \"ON\" timeout reached.")); + } +} +# endif + +void LCDPrintSpaces(uint8_t aNumberOfSpacesToPrint) { + for (uint_fast8_t i = 0; i < aNumberOfSpacesToPrint; ++i) { + myLCD.print(' '); + } +} + +void LCDClearLine(uint8_t aLineNumber) { + myLCD.setCursor(0, aLineNumber); + LCDPrintSpaces(20); + myLCD.setCursor(0, aLineNumber); +} + +/* + * Global timeout message + */ +void printTimeoutMessageOnLCD() { + if (sSerialLCDAvailable +# if !defined(DISPLAY_ALWAYS_ON) + && !sSerialLCDIsSwitchedOff +# endif + && sLCDDisplayPageNumber != JK_BMS_PAGE_CAN_INFO) { + myLCD.clear(); + myLCD.setCursor(0, 0); + myLCD.print(F("Receive timeout ")); + myLCD.print(sTimeoutFrameCounter); + myLCD.setCursor(0, 1); + myLCD.print(F("Is BMS switched off?")); + } +} + +void printShortEnableFlagsOnLCD() { + if (sJKFAllReplyPointer->ChargeIsEnabled) { + myLCD.print('C'); + } else { + myLCD.print(' '); + } + if (sJKFAllReplyPointer->ChargeIsEnabled) { + myLCD.print('D'); + } else { + myLCD.print(' '); + } + if (sJKFAllReplyPointer->BalancingIsEnabled) { + myLCD.print('B'); + } +} + +/* + * Prints state of Charge Overvoltage warning, and Charge, Discharge and Balancing flag + */ +void printShortStateOnLCD() { + if (sJKFAllReplyPointer->AlarmUnion.AlarmBits.ChargeOvervoltageAlarm) { + myLCD.print('F'); + } else { + myLCD.print(' '); + } + if (sJKFAllReplyPointer->BMSStatus.StatusBits.ChargeMosFetActive) { + myLCD.print('C'); + } else { + myLCD.print(' '); + } + if (sJKFAllReplyPointer->BMSStatus.StatusBits.DischargeMosFetActive) { + myLCD.print('D'); + } else { + myLCD.print(' '); + } + if (sJKFAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { + myLCD.print('B'); + } +} + +void printLongStateOnLCD() { + if (sJKFAllReplyPointer->BMSStatus.StatusBits.ChargeMosFetActive) { + myLCD.print(F("CH ")); + } else { + myLCD.print(F(" ")); + } + + if (sJKFAllReplyPointer->BMSStatus.StatusBits.DischargeMosFetActive) { + myLCD.print(F("DC ")); + } else { + myLCD.print(F(" ")); + } + + if (sJKFAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { + myLCD.print(F("BAL")); + } +} + +/* + * Print current as 5 character including sign + */ +void printCurrentOnLCD() { + int16_t tBattery10MilliAmpere = JKComputedData.Battery10MilliAmpere; + if (tBattery10MilliAmpere >= 0) { + myLCD.print(' '); // handle not printed + sign + } + tBattery10MilliAmpere = abs(tBattery10MilliAmpere); // remove sign for length computation + uint8_t tNumberOfDecimalPlaces; + if (tBattery10MilliAmpere < 1000) { + // less than 10 A (1000 * 10mA) + tNumberOfDecimalPlaces = 2; // -9.99 + } else if (tBattery10MilliAmpere < 10000) { + tNumberOfDecimalPlaces = 1; // -99.9 + } else { + tNumberOfDecimalPlaces = 0; // -9999 + } + myLCD.print(JKComputedData.BatteryLoadCurrentFloat, tNumberOfDecimalPlaces); + myLCD.print(F("A ")); +} + +/* + * We can display only up to 16 cell values on the LCD :-( + */ +void printCellInfoOnLCD() { + uint_fast8_t tRowNumber; + auto tNumberOfCellInfoEntries = JKConvertedCellInfo.ActualNumberOfCellInfoEntries; + if (tNumberOfCellInfoEntries > 12) { + tRowNumber = 0; + if (tNumberOfCellInfoEntries > 16) { + tNumberOfCellInfoEntries = 16; + } + } else { + myLCD.print(F(" -CELL INFO-")); + tRowNumber = 1; + } + + for (uint8_t i = 0; i < tNumberOfCellInfoEntries; ++i) { + if (i % 4 == 0) { + myLCD.setCursor(0, tRowNumber); + tRowNumber++; + } + + // print maximum or minimum indicator + if (JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween == VOLTAGE_IS_MAXIMUM) { + myLCD.print((char) (0x1)); + } else if (JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween == VOLTAGE_IS_MINIMUM) { + myLCD.print((char) (0x2)); + } else { + myLCD.print(' '); + } + + myLCD.print(JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt); + } +} + +/* + * Switch between display of minimum and maximum at each 4. call + * The sum of percentages may not give 100% because of rounding errors + */ +void printCellStatisticsOnLCD() { +// check if minimum or maximum is to be displayed + bool tDisplayCellMinimumStatistics = sCellStatisticsDisplayCounter & CELL_STATISTICS_COUNTER_MASK; // 0x04 + sCellStatisticsDisplayCounter++; + + auto tNumberOfCellInfoEntries = JKConvertedCellInfo.ActualNumberOfCellInfoEntries; + uint_fast8_t tRowNumber; + if (tNumberOfCellInfoEntries > 12) { + tRowNumber = 0; + if (tNumberOfCellInfoEntries > 16) { + tNumberOfCellInfoEntries = 16; + } + } else { + if (tDisplayCellMinimumStatistics) { + myLCD.print(F("CELL Minimum Percent")); + } else { + myLCD.print(F("CELL Maximum Percent")); + } + tRowNumber = 1; + } + + char *tBalancingTimeStringPtr = &sBalancingTimeString[0]; + for (uint8_t i = 0; i < tNumberOfCellInfoEntries; ++i) { + uint8_t tPercent; + if (tDisplayCellMinimumStatistics) { + tPercent = CellMinimumPercentageArray[i]; + } else { + tPercent = CellMaximumPercentageArray[i]; + } + if (tPercent < 10) { + myLCD.print(' '); + } + myLCD.print(tPercent); + myLCD.print(F("% ")); + if (i % 4 == 3) { + /* + * Use the last 4 characters of a LCD row for information + * about min or max and the total balancing time + */ + if (i == 3) { + // first line + if (tDisplayCellMinimumStatistics) { + myLCD.print(F("MIN")); + } else { + myLCD.print(F("MAX")); + } + } else { + if (i == 7) { + // Second line print 4 characters days + myLCD.print(*tBalancingTimeStringPtr++); + } else { + myLCD.print(' '); + } + // print 3 character for hours or minutes + myLCD.print(*tBalancingTimeStringPtr++); + myLCD.print(*tBalancingTimeStringPtr++); + myLCD.print(*tBalancingTimeStringPtr++); + } + tRowNumber++; + myLCD.setCursor(0, tRowNumber); + } + } +} + +/* + * Display the last measured capacities + * Long: "100%->30%=111 130Ah" + * Short: "99 30 111 " + */ +void printCapacityInfoOnLCD() { + if (JKComputedCapacity[0].Capacity == 0 && JKComputedCapacity[1].Capacity == 0) { + myLCD.print(F("No capacity computed")); + } else { + for (uint8_t i = 0; i < LCD_ROWS; ++i) { + if (JKComputedCapacity[i].Capacity != 0) { + myLCD.setCursor(0, i); + snprintf_P(sStringBuffer, LCD_COLUMNS + 1, PSTR("%2d%%->%2d%%=%3u %3uAh"), JKComputedCapacity[i].StartSOC, + JKComputedCapacity[i].EndSOC, JKComputedCapacity[i].Capacity, JKComputedCapacity[i].TotalCapacity); + myLCD.print(sStringBuffer); + } + } + } +} + +void printBigInfoOnLCD() { + bigNumberLCD.setBigNumberCursor(0, 0); + bigNumberLCD.print(sJKFAllReplyPointer->SOCPercent); + uint8_t tColumn; + if (sJKFAllReplyPointer->SOCPercent < 10) { + tColumn = 3; + } else if (sJKFAllReplyPointer->SOCPercent < 100) { + tColumn = 6; + } else { + tColumn = 8; // 100% + } + + myLCD.setCursor(tColumn, UNITS_ROW_FOR_BIG_INFO); // 3, 6 or 8 + myLCD.print('%'); + /* + * Here we can start the power string at column 4, 7 or 9 + */ + uint8_t tAvailableColumns = (LCD_COLUMNS - 2) - tColumn; + char tKiloWattChar = ' '; + int16_t tBatteryLoadPower = JKComputedData.BatteryLoadPower; + /* + * First print string to buffer + */ + if (tBatteryLoadPower >= 1000 || tBatteryLoadPower <= -1000) { + tKiloWattChar = 'k'; + float tBatteryLoadPowerFloat = tBatteryLoadPower * 0.001; // convert to kW + dtostrf(tBatteryLoadPowerFloat, 5, 2, sStringBuffer); + } else { + sprintf_P(sStringBuffer, PSTR("%d"), JKComputedData.BatteryLoadPower); + } + /* + * Then compute maximum possible string length + */ + uint8_t tColumnsRequiredForString = 0; + uint8_t i = 0; + while (true) { + char tChar = sStringBuffer[i]; + uint8_t tCharacterWidth; + if (tChar == '\0') { + break; // end of string + } else if (tChar == '.') { + tCharacterWidth = 1; // decimal point + } else if (tChar == '-') { + tCharacterWidth = 2; // minus sign + } else { + tCharacterWidth = 3; // plain number + } + + /* + * Check if next character can be rendered + */ + if (tAvailableColumns >= tCharacterWidth) { + tColumnsRequiredForString += tCharacterWidth; + tAvailableColumns -= tCharacterWidth; + } else { + /* + * Next character cannot be rendered here + */ + if (sStringBuffer[i - 1] == '.') { + // do not render trailing decimal point + tColumnsRequiredForString--; + tAvailableColumns++; + i--; + } + // Terminate string and exit + sStringBuffer[i] = '\0'; + break; + } + i++; + } + bigNumberLCD.setBigNumberCursor((LCD_COLUMNS - 1) - tColumnsRequiredForString, 0); + bigNumberLCD.print(sStringBuffer); +// Print units + myLCD.setCursor(19, UNITS_ROW_FOR_BIG_INFO - 1); + myLCD.print(tKiloWattChar); + myLCD.setCursor(19, UNITS_ROW_FOR_BIG_INFO); + myLCD.print('W'); + +// Bottom row: Max temperature, current and the actual states + myLCD.setCursor(0, 3); + myLCD.print(JKComputedData.TemperatureMaximum); + myLCD.print(F("\xDF ")); + + myLCD.setCursor(4, 3); + printCurrentOnLCD(); + + myLCD.setCursor(11, 3); + uint16_t tBatteryToFullDifference10Millivolt = JKComputedData.BatteryFullVoltage10Millivolt + - JKComputedData.BatteryVoltage10Millivolt; + if (tBatteryToFullDifference10Millivolt < 100) { + // Print small values as ".43" instead of "0.4" + sprintf_P(sStringBuffer, PSTR(".%02d"), tBatteryToFullDifference10Millivolt); + myLCD.print(sStringBuffer); + } else { + myLCD.print(((float) tBatteryToFullDifference10Millivolt) / 100.0, 1); + } + myLCD.print('V'); + + myLCD.setCursor(16, 3); + printShortStateOnLCD(); +} + +void printCANInfoOnLCD() { + /* + * sLCDDisplayPageNumber == JK_BMS_PAGE_CAN_INFO + */ + if (!sCANDataIsInitialized || JKComputedData.BMSIsStarting) { + myLCD.print(F("No CAN data are sent")); + myLCD.setCursor(0, 1); + if (JKComputedData.BMSIsStarting) { + myLCD.print(F("BMS is starting")); + } else { + myLCD.print(F("No BMS data received")); + } + } else { + PylontechCANFrameStruct *tCANFrameDataPointer = + reinterpret_cast(&PylontechCANErrorsWarningsFrame); + if (tCANFrameDataPointer->FrameData.ULong.LowLong == 0) { + /* + * Caption in row 1 and "No errors / warnings" in row 2 + */ + myLCD.print(F(" -CAN INFO- ")); + if (PylontechCANSohSocFrame.FrameData.SOCPercent < 100) { + myLCD.print(' '); + } + myLCD.print(F("SOC=")); + myLCD.print(PylontechCANSohSocFrame.FrameData.SOCPercent); + myLCD.print('%'); + myLCD.setCursor(0, 1); + myLCD.print(F("No errors / warnings")); + } else { + /* + * Errors in row 1 and warnings in row 2 + */ + myLCD.print(F("Errors: 0x")); + myLCD.print(tCANFrameDataPointer->FrameData.UBytes[0], HEX); + myLCD.print(F(" 0x")); + myLCD.print(tCANFrameDataPointer->FrameData.UBytes[1], HEX); + myLCD.setCursor(0, 1); + myLCD.print(F("Warnings: 0x")); + myLCD.print(tCANFrameDataPointer->FrameData.UBytes[2], HEX); + myLCD.print(F(" 0x")); + myLCD.print(tCANFrameDataPointer->FrameData.UBytes[3], HEX); + } + /* + * Voltage, current and maximum temperature in row 3 + */ + myLCD.setCursor(0, 2); + // Voltage + myLCD.print(PylontechCANCurrentValuesFrame.FrameData.Voltage10Millivolt / 100); + myLCD.print('.'); + myLCD.print(PylontechCANCurrentValuesFrame.FrameData.Voltage10Millivolt % 100); + myLCD.print(F("V ")); + // Current + int16_t tCurrent100Milliampere = PylontechCANCurrentValuesFrame.FrameData.Current100Milliampere; + myLCD.print(tCurrent100Milliampere / 10); + if (tCurrent100Milliampere < 0) { + // avoid negative numbers after decimal point + tCurrent100Milliampere = -tCurrent100Milliampere; + } + if (tCurrent100Milliampere < 100) { + // Print fraction if value < 100 + myLCD.print('.'); + myLCD.print(tCurrent100Milliampere % 10); + } + myLCD.print(F("A ")); + // Temperature + myLCD.setCursor(14, 2); + myLCD.print(PylontechCANCurrentValuesFrame.FrameData.Temperature100Millicelsius / 10); + if (PylontechCANCurrentValuesFrame.FrameData.Temperature100Millicelsius < 100) { + // Print fraction if value < 100 + myLCD.print('.'); + myLCD.print(PylontechCANCurrentValuesFrame.FrameData.Temperature100Millicelsius % 10); + } + myLCD.print(F("\xDF" "C ")); + + /* + * Request flags in row 4 + */ + myLCD.setCursor(0, 3); + // Charge enable + if (PylontechCANBatteryRequestFrame.FrameData.ChargeEnable) { + myLCD.print(F("CH ")); + } else { + myLCD.print(F(" ")); + } + if (PylontechCANBatteryRequestFrame.FrameData.DischargeEnable) { + myLCD.print(F("DC")); + } + myLCD.setCursor(6, 3); + if (PylontechCANBatteryRequestFrame.FrameData.ForceChargeRequestI) { + myLCD.print(F("FORCEI")); + } + myLCD.setCursor(13, 3); + if (PylontechCANBatteryRequestFrame.FrameData.ForceChargeRequestII) { + myLCD.print(F("FORCEII")); + } + + // Currently constant 0 +// myLCD.setCursor(10, 3); +// if (PylontechCANBatteryRequestFrame.FrameData.FullChargeRequest) { +// myLCD.print(F("FULL")); +// } + } +} + +void printOverwiewInfoOnLCD() { + /* + * Top row 1 - Error message or up time + */ + if (sErrorStringForLCD != NULL) { + // Copy error message from flash, but not more than 20 characters + memcpy_P(sStringBuffer, sErrorStringForLCD, LCD_COLUMNS); + sStringBuffer[LCD_COLUMNS] = '\0'; + myLCD.print(sStringBuffer); + } else { + myLCD.print(F("Uptime: ")); + myLCD.print(sUpTimeString); + } + /* + * Row 2 - SOC and remaining capacity and state of MosFets or Error + */ + myLCD.setCursor(0, 1); +// Percent of charge + myLCD.print(sJKFAllReplyPointer->SOCPercent); + myLCD.print(F("% ")); +// Remaining capacity + myLCD.print(JKComputedData.RemainingCapacityAmpereHour); + myLCD.print(F("Ah ")); + if (JKComputedData.RemainingCapacityAmpereHour < 100) { + myLCD.print(' '); + } +// Last 3 characters are the enable states + myLCD.setCursor(14, 1); + myLCD.print(F("En:")); + printShortEnableFlagsOnLCD(); + + /* + * Row 3 - Voltage, Current and Power + */ + myLCD.setCursor(0, 2); +// Voltage + myLCD.print(JKComputedData.BatteryVoltageFloat, 2); + myLCD.print(F("V ")); +// Current + printCurrentOnLCD(); +// Power + if (JKComputedData.BatteryLoadPower < -10000) { + // over 10 kW + myLCD.setCursor(13, 2); + myLCD.print(JKComputedData.BatteryLoadPower); // requires 6 columns + } else { + myLCD.setCursor(14, 2); + sprintf_P(sStringBuffer, PSTR("%5d"), JKComputedData.BatteryLoadPower); // force use of 5 columns + myLCD.print(sStringBuffer); + } + myLCD.print('W'); + /* + * Row 4 - 3 Temperatures and 3 enable states + */ + myLCD.setCursor(0, 3); +// 3 temperatures + myLCD.print(JKComputedData.TemperaturePowerMosFet); + myLCD.print(F("\xDF" "C ")); + myLCD.print(JKComputedData.TemperatureSensor1); + myLCD.print(F("\xDF" "C ")); + myLCD.print(JKComputedData.TemperatureSensor2); + myLCD.print(F("\xDF" "C ")); +// Last 4 characters are the actual states + myLCD.setCursor(16, 3); + printShortStateOnLCD(); + +} + +void printBMSDataOnLCD() { + if (sSerialLCDAvailable +# if !defined(DISPLAY_ALWAYS_ON) + && !sSerialLCDIsSwitchedOff +# endif + ) { + myLCD.clear(); + myLCD.setCursor(0, 0); + + if (sLCDDisplayPageNumber == JK_BMS_PAGE_OVERVIEW) { + printOverwiewInfoOnLCD(); + + } else if (sLCDDisplayPageNumber == JK_BMS_PAGE_CELL_INFO) { + printCellInfoOnLCD(); + + } else if (sLCDDisplayPageNumber == JK_BMS_PAGE_CELL_STATISTICS) { + printCellStatisticsOnLCD(); + + } else if (sLCDDisplayPageNumber == JK_BMS_PAGE_BIG_INFO) { + printBigInfoOnLCD(); + + } else if (sLCDDisplayPageNumber == JK_BMS_PAGE_CAN_INFO) { + printCANInfoOnLCD(); + + } else { //sLCDDisplayPageNumber == JK_BMS_PAGE_CAPACITY_INFO + printCapacityInfoOnLCD(); + } + } +} + +void checkButtonPressForLCD() { + if (sSerialLCDAvailable) { + /* + * Handle Page button + */ + uint8_t tDisplayPageNumber = sLCDDisplayPageNumber; + if (sPageButtonJustPressed) { + sPageButtonJustPressed = false; +# if !defined(DISPLAY_ALWAYS_ON) + /* + * If backlight LED off, switch it on, but do not select next page + */ + if (checkAndTurnLCDOn()) { + Serial.println(F("button press")); // Switch on LCD display, triggered by button press + sPageButtonJustPressed = false; // avoid switching pages if page button was pressed. + } else +# endif + { + + if (sJKBMSFrameHasTimeout || tDisplayPageNumber == JK_BMS_PAGE_MAX || tDisplayPageNumber == JK_BMS_DEBUG_PAGE_MAX) { + // Receive timeout or wrap around here + tDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; + + } else { + /* + * Switch display pages to next page + */ + tDisplayPageNumber++; + if (tDisplayPageNumber == JK_BMS_PAGE_CELL_INFO) { + // Create symbols character for maximum and minimum + bigNumberLCD._createChar(1, bigNumbersTopBlock); + bigNumberLCD._createChar(2, bigNumbersBottomBlock); + + // Prepare for statistics page here display max first but for half the regular time + sCellStatisticsDisplayCounter = (CELL_STATISTICS_COUNTER_MASK >> 1) - 1; + + } else if (tDisplayPageNumber == JK_BMS_PAGE_BIG_INFO) { + bigNumberLCD.begin(); // Creates custom character used for generating big numbers + } + } + setDisplayPage(tDisplayPageNumber); + } + + } else if (PageSwitchButtonAtPin2.readDebouncedButtonState()) { + /* + * Here button was not pressed in the loop before. Check if button is still active. + */ + if (tDisplayPageNumber == JK_BMS_PAGE_CAN_INFO) { + // Button is still pressed on CAN Info page -> enable serial debug output as long as button is pressed + sDebugModeActivated = true; // Is set to false in loop + } else if (PageSwitchButtonAtPin2.checkForLongPress(LONG_PRESS_BUTTON_DURATION_MILLIS) + == EASY_BUTTON_LONG_PRESS_DETECTED) { + /* + * Long press detected -> switch to CAN Info page + */ + sDebugModeActivated = true; // Is set to false in loop + if (sSerialLCDAvailable) { + Serial.println(); + Serial.println(F("Long press detected -> switch to CAN page and activate one time debug print")); + setDisplayPage(JK_BMS_PAGE_CAN_INFO); + } + } + } // PageSwitchButtonAtPin2.ButtonStateHasJustChanged + } +} + +void setDisplayPage(uint8_t aDisplayPageNumber) { + sLCDDisplayPageNumber = aDisplayPageNumber; + tone(BUZZER_PIN, 2200, 30); + Serial.print(F("Set LCD display page to: ")); + Serial.println(aDisplayPageNumber); + +// If BMS communication timeout, only timeout message or CAN Info page can be displayed. + if (!sJKBMSFrameHasTimeout || aDisplayPageNumber == JK_BMS_PAGE_CAN_INFO) { + /* + * Show new page + */ + printBMSDataOnLCD(); + } +} + +#if defined(STANDALONE_TEST) +void testLCDPages() { + sLCDDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; + printBMSDataOnLCD(); + + sLCDDisplayPageNumber = JK_BMS_PAGE_CELL_INFO; +// Create symbols character for maximum and minimum + bigNumberLCD._createChar(1, bigNumbersTopBlock); + bigNumberLCD._createChar(2, bigNumbersBottomBlock); + delay(2000); + printBMSDataOnLCD(); + + sLCDDisplayPageNumber = JK_BMS_PAGE_CAN_INFO; + delay(2000); + printBMSDataOnLCD(); + + /* + * Check display of maximum values + */ + sJKFAllReplyPointer->SOCPercent = 100; + JKComputedData.BatteryLoadPower = -11000; + JKComputedData.BatteryLoadCurrentFloat = JKComputedData.BatteryLoadPower / JKComputedData.BatteryVoltageFloat; + JKComputedData.TemperaturePowerMosFet = 111; + JKComputedData.TemperatureSensor1 = 100; + + sLCDDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; + delay(2000); + printBMSDataOnLCD(); + + sLCDDisplayPageNumber = JK_BMS_PAGE_BIG_INFO; + bigNumberLCD.begin(); + delay(2000); + printBMSDataOnLCD(); + + /* + * Test other values + */ + sJKFAllReplyPointer->AlarmUnion.AlarmBits.PowerMosFetOvertemperatureAlarm = true; + JKComputedData.TemperaturePowerMosFet = 90; + JKComputedData.TemperatureSensor1 = 25; + handleAndPrintAlarmInfo(); // this sets the LCD alarm string + + sJKFAllReplyPointer->SOCPercent = 1; + JKComputedData.BatteryLoadPower = 12345; + JKComputedData.BatteryLoadCurrentFloat = JKComputedData.BatteryLoadPower / JKComputedData.BatteryVoltageFloat; + + sLCDDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; + delay(2000); + printBMSDataOnLCD(); + + sLCDDisplayPageNumber = JK_BMS_PAGE_BIG_INFO; + delay(2000); + printBMSDataOnLCD(); + + lastJKReply.AlarmUnion.AlarmBits.PowerMosFetOvertemperatureAlarm = true; // to enable reset of LCD alarm string + sJKFAllReplyPointer->AlarmUnion.AlarmBits.PowerMosFetOvertemperatureAlarm = false; + JKComputedData.TemperaturePowerMosFet = 33; + handleAndPrintAlarmInfo(); // this resets the LCD alarm string + + sJKFAllReplyPointer->SOCPercent = 100; + JKComputedData.BatteryLoadCurrentFloat = -100; + + sLCDDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; + delay(2000); + printBMSDataOnLCD(); +} + +void testBigNumbers() { + sLCDDisplayPageNumber = JK_BMS_PAGE_BIG_INFO; + + for (int j = 0; j < 3; ++j) { + // Test with 100 % and 42 % + + /* + * test with positive numbers + */ + JKComputedData.BatteryLoadPower = 12345; + JKComputedData.BatteryLoadCurrentFloat = JKComputedData.BatteryLoadPower / JKComputedData.BatteryVoltageFloat; + + for (int i = 0; i < 5; ++i) { + delay(4000); + printBMSDataOnLCD(); + JKComputedData.BatteryLoadPower /= 10; // 1234 -> 12 + JKComputedData.BatteryLoadCurrentFloat = JKComputedData.BatteryLoadPower / JKComputedData.BatteryVoltageFloat; + } + /* + * test with negative numbers + */ + JKComputedData.BatteryLoadPower = -12345; + JKComputedData.BatteryLoadCurrentFloat = JKComputedData.BatteryLoadPower / JKComputedData.BatteryVoltageFloat; + + for (int i = 0; i < 5; ++i) { + delay(4000); + printBMSDataOnLCD(); + JKComputedData.BatteryLoadPower /= 10; // 1234 -> 12 + JKComputedData.BatteryLoadCurrentFloat = JKComputedData.BatteryLoadPower / JKComputedData.BatteryVoltageFloat; + } + + sJKFAllReplyPointer->SOCPercent /= 10; + } +} +#endif // STANDALONE_TEST + +#endif // _JK_BMS_LCD_HPP diff --git a/JK-BMSToPylontechCAN/MCP2515_TX.h b/JK-BMSToPylontechCAN/MCP2515_TX.h index e9c25bd..6457520 100644 --- a/JK-BMSToPylontechCAN/MCP2515_TX.h +++ b/JK-BMSToPylontechCAN/MCP2515_TX.h @@ -5,9 +5,9 @@ * Copyright (C) 2023 Armin Joachimsmeyer * Email: armin.joachimsmeyer@gmail.com * - * This file is part of ArduinoUtils https://github.com/ArminJo/PVUtils. + * This file is part of ArduinoUtils https://github.com/ArminJo/JK-BMSToPylontechCAN. * - * Arduino-Utils is free software: you can redistribute it and/or modify + * JK-BMSToPylontechCAN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. diff --git a/JK-BMSToPylontechCAN/MCP2515_TX.hpp b/JK-BMSToPylontechCAN/MCP2515_TX.hpp index 9049a60..30aae13 100644 --- a/JK-BMSToPylontechCAN/MCP2515_TX.hpp +++ b/JK-BMSToPylontechCAN/MCP2515_TX.hpp @@ -7,9 +7,9 @@ * Copyright (C) 2023 Armin Joachimsmeyer * Email: armin.joachimsmeyer@gmail.com * - * This file is part of ArduinoUtils https://github.com/ArminJo/PVUtils. + * This file is part of ArduinoUtils https://github.com/ArminJo/JK-BMSToPylontechCAN. * - * Arduino-Utils is free software: you can redistribute it and/or modify + * JK-BMSToPylontechCAN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. diff --git a/JK-BMSToPylontechCAN/Pylontech_CAN.h b/JK-BMSToPylontechCAN/Pylontech_CAN.h index ae5863e..a0fd6db 100644 --- a/JK-BMSToPylontechCAN/Pylontech_CAN.h +++ b/JK-BMSToPylontechCAN/Pylontech_CAN.h @@ -8,9 +8,9 @@ * Copyright (C) 2023 Armin Joachimsmeyer * Email: armin.joachimsmeyer@gmail.com * - * This file is part of ArduinoUtils https://github.com/ArminJo/PVUtils. + * This file is part of ArduinoUtils https://github.com/ArminJo/JK-BMSToPylontechCAN. * - * Arduino-Utils is free software: you can redistribute it and/or modify + * JK-BMSToPylontechCAN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. @@ -95,6 +95,10 @@ struct PylontechCANAliveFrameStruct { } FrameData; }; +/* + * -Static data- + * Limits + */ struct PylontechCANBatteryLimitsFrameStruct { struct PylontechCANFrameInfoStruct PylontechCANFrameInfo = { PYLON_CAN_BATTERY_LIMITS_FRAME_ID, 8 }; // 0x351 struct { @@ -111,6 +115,10 @@ struct PylontechCANBatteryLimitsFrameStruct { } }; +/* + * -Dynamic data- + * SOC value, SOH is fixed to 100% + */ struct PylontechCANSohSocFrameStruct { struct PylontechCANFrameInfoStruct PylontechCANFrameInfo = { PYLON_CAN_BATTERY_SOC_SOH_FRAME_ID, 4 }; // 0x355 struct { @@ -125,6 +133,9 @@ struct PylontechCANSohSocFrameStruct { } }; +/* + * -Dynamic data- + */ struct PylontechCANCurrentValuesFrameStruct { struct PylontechCANFrameInfoStruct PylontechCANFrameInfo = { PYLON_CAN_BATTERY_CURRENT_VALUES_U_I_T_FRAME_ID, 6 }; // 0x356 struct { @@ -140,6 +151,10 @@ struct PylontechCANCurrentValuesFrameStruct { } }; +/* + * -Dynamic data- + * Errors and warnings + */ struct PylontechCANErrorsWarningsFrameStruct { struct PylontechCANFrameInfoStruct PylontechCANFrameInfo = { PYLON_CAN_BATTERY_ERROR_WARNINGS_FRAME_ID, 7 }; // 0x359 struct FrameDataStruct { @@ -230,6 +245,7 @@ struct PylontechCANErrorsWarningsFrameStruct { }; /* + * -Dynamic data- * ForceChargeRequestI / bit 5 is designed for inverter allows battery to shut down, and able to wake battery up to charge it. * ForceChargeRequestII / bit 4 is designed for inverter doesn`t want battery to shut down, able to charge battery before shut down to avoid low energy. * 2 bytes @@ -275,6 +291,7 @@ struct PylontechCANBatteryRequesFrameStruct { }; /* + * -Static data, no fill- * Character array DIYPYLON is not recognized by Deye, array PYLONDIY is recognized as PYLON */ struct PylontechCANManufacturerFrameStruct { @@ -284,7 +301,9 @@ struct PylontechCANManufacturerFrameStruct { } FrameData; }; +/**************** Extensions to the standard Pylontech protocol *************/ /* + * -Static data- * Frame for total capacity for SMA - Sunny Island inverters * Description was found in: https://github.com/Uksa007/esphome-jk-bms-can/blob/main/docs/SMA%20CAN%20Protocol%20Mapping.pdf * and in UserManual9R_SMA.pdf of www.rec-bms.com @@ -308,6 +327,7 @@ struct PylontechCANSMACapacityFrameStruct { }; /* + * -Static data- * Frame for total capacity for Luxpower - SNA inverters * Description was found in: https://github.com/dfch/BydCanProtocol/tree/main */ diff --git a/JK-BMSToPylontechCAN/Pylontech_CAN.hpp b/JK-BMSToPylontechCAN/Pylontech_CAN.hpp index 99c9475..bfd570a 100644 --- a/JK-BMSToPylontechCAN/Pylontech_CAN.hpp +++ b/JK-BMSToPylontechCAN/Pylontech_CAN.hpp @@ -10,9 +10,9 @@ * Copyright (C) 2023 Armin Joachimsmeyer * Email: armin.joachimsmeyer@gmail.com * - * This file is part of ArduinoUtils https://github.com/ArminJo/PVUtils. + * This file is part of ArduinoUtils https://github.com/ArminJo/JK-BMSToPylontechCAN. * - * Arduino-Utils is free software: you can redistribute it and/or modify + * JK-BMSToPylontechCAN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. @@ -46,6 +46,7 @@ struct PylontechCANSohSocFrameStruct PylontechCANSohSocFrame; struct PylontechCANCurrentValuesFrameStruct PylontechCANCurrentValuesFrame; struct PylontechCANBatteryRequesFrameStruct PylontechCANBatteryRequestFrame; struct PylontechCANErrorsWarningsFrameStruct PylontechCANErrorsWarningsFrame; +// Extensions to the standard Pylontech protocol struct PylontechCANSMACapacityFrameStruct PylontechCANSMACapacityFrame; struct PylontechCANLuxpowerCapacityFrameStruct PylontechCANLuxpowerCapacityFrame; diff --git a/README.md b/README.md index 17ba511..ec89e1d 100644 --- a/README.md +++ b/README.md @@ -187,11 +187,11 @@ Modify them by enabling / disabling them, or change the values if applicable. | `MULTIPLE_BEEPS_WITH_TIMEOUT` | enabled | If error was detected, beep for 60 s. | | `SUPPRESS_LIFEPO4_PLAUSI_WARNING` | disabled | Disables warning on Serial out about using LiFePO4 beyond 3.0 v to 3.45 V. | | `MAXIMUM_NUMBER_OF_CELLS` | 24 | Maximum number of cell info which can be converted. Saves RAM. | -| `USE_NO_LCD` | disabled | If activated, the code for the LCD display and page button is deactivated. Saves 25% program space on a Nano. | +| `USE_NO_LCD` | disabled | If activated, the code for the LCD display is deactivated. Saves 25% program space on a Nano. | | `DISPLAY_ALWAYS_ON` | disabled | If activated, the display backlight is always on. This disables the value of `DISPLAY_ON_TIME_SECONDS`. | | `DISPLAY_ON_TIME_SECONDS` | 300 | 300 s / 5 min after the last button press, the backlight of the LCD display is switched off. | | `DISPLAY_ON_TIME_SECONDS_IF_TIMEOUT` | 180 | 180 s / 3 min after the first timeout / BMS shutdown, the backlight of the LCD display is switched off. | -| `STANDALONE_TEST` | disabled | If activated, fixed BMS data is sent to CAN bus. | +| `STANDALONE_TEST` | disabled | If activated, fixed BMS data is sent to CAN bus and displayed on LCD. | | `NO_SMA_EXTENSIONS` | disabled | If activated, supress sending of SMA extension frame over CAN. | | `NO_LUXPOWER_EXTENSIONS` | disabled | If activated, supress sending of Luxpower extension frame over CAN. | diff --git a/extras/CellUndervoltage.log b/extras/CellUndervoltage.log new file mode 100644 index 0000000..b01a553 --- /dev/null +++ b/extras/CellUndervoltage.log @@ -0,0 +1,244 @@ +CSV: 1;0;11;18;18;18;18;13;15;18;18;18;20;20;15;18;4824;0;20;0 +CSV: 1;0;10;18;18;18;18;13;15;18;18;20;18;18;13;18;4823;0;20;0 +CSV: 2;0;10;18;18;18;18;13;13;18;18;18;20;20;15;18;4823;0;20;0 +CSV: 0;0;10;18;18;18;18;15;15;18;18;20;18;18;13;18;4823;0;20;0 +CSV: 1;0;10;18;18;18;18;13;15;18;18;18;20;18;13;18;4823;0;20;0 +CSV: 1;0;10;18;18;18;17;14;13;18;18;18;18;18;13;18;4823;-20;20;0 +CSV: 1;0;11;18;17;18;18;14;15;18;18;18;20;18;15;18;4824;-20;20;0 +CSV: 1;0;10;17;17;18;18;14;13;18;18;18;18;20;13;18;4823;0;20;0 +CSV: 1;0;10;17;18;18;18;13;15;18;18;18;18;18;15;18;4823;0;20;0 +CSV: 1;0;10;17;18;18;17;14;13;18;18;20;20;20;15;18;4823;-20;20;0 +CSV: 1;0;10;17;18;18;18;14;13;20;18;18;20;18;13;18;4823;-20;20;0 +CSV: 1;0;10;17;18;18;18;14;15;18;20;20;18;18;13;18;4823;-20;20;0 +CSV: 1;0;10;17;17;18;17;12;13;20;18;20;20;18;13;18;4823;0;20;0 +CSV: 1;0;10;18;18;18;17;13;15;20;18;18;18;18;13;18;4823;0;20;0 +CSV: 0;0;10;17;18;18;18;14;15;18;18;18;20;20;13;18;4823;-20;20;0 +CSV: 1;0;10;17;17;18;17;13;15;18;18;20;20;18;13;18;4823;0;20;0 +CSV: 1;0;10;17;17;18;17;13;13;18;18;18;18;18;13;17;4823;0;20;0 +CSV: 1;-1;11;18;17;18;17;14;13;18;18;18;18;18;13;18;4823;-20;20;0 +CSV: 1;0;10;18;18;18;18;13;15;18;18;18;18;20;13;18;4824;0;20;0 +CSV: 0;0;10;18;18;18;18;13;13;18;18;18;18;18;13;18;4823;0;20;0 +CSV: 1;0;10;18;17;17;18;14;13;18;18;18;18;18;13;18;4823;-20;20;0 +CSV: 1;0;10;18;18;18;18;13;13;18;18;18;18;18;13;18;4823;0;20;0 +CSV: 1;0;10;17;17;18;17;13;13;18;18;18;18;18;12;18;4823;0;20;0 +CSV: 0;-1;10;18;18;18;17;13;15;18;17;18;18;18;13;18;4823;0;20;0 +CSV: 1;0;10;18;17;18;18;14;13;18;18;18;18;18;13;18;4823;-20;20;0 +CSV: 0;0;10;18;18;18;18;13;13;18;18;20;18;18;13;18;4823;0;20;0 +CSV: 1;-1;10;17;18;18;17;13;15;18;18;20;18;18;13;18;4823;0;20;0 +CSV: 1;0;10;17;17;18;18;14;13;18;18;18;18;18;13;18;4823;-20;20;0 +CSV: 0;0;10;18;18;18;18;13;13;18;18;18;18;18;13;18;4823;-20;20;0 +CSV: 0;-1;10;17;17;18;17;13;15;18;18;18;18;18;13;18;4823;0;20;0 +CSV: 1;-1;10;18;18;18;17;13;15;18;18;18;18;20;13;18;4823;0;20;0 +CSV: 1;-1;10;17;17;18;17;13;15;18;18;18;18;18;13;18;4823;0;20;0 +CSV: 0;0;10;17;17;18;18;13;15;18;18;20;20;18;13;18;4823;0;20;0 +CSV: 0;-1;10;17;18;18;17;13;15;18;18;18;18;18;13;18;4823;0;20;0 +CSV: 1;0;10;17;17;18;18;13;15;18;18;20;18;18;13;18;4823;0;20;0 +CSV: 1;-1;10;17;18;18;17;13;13;18;18;18;20;18;13;17;4823;0;20;0 +CSV: 1;0;10;17;17;18;17;13;13;18;18;18;18;18;13;18;4822;0;20;0 +CSV: 1;0;10;17;17;18;17;13;15;18;18;18;18;18;12;18;4823;0;20;0 +CSV: 0;-1;10;17;17;17;18;14;15;18;18;18;18;18;13;17;4822;-20;20;0 +CSV: 0;0;10;18;18;18;18;14;13;18;18;18;18;18;13;18;4823;-20;20;0 +CSV: 0;0;10;17;18;18;17;12;15;18;18;18;18;18;13;18;4823;20;20;0 +CSV: 0;-1;10;17;17;18;17;13;13;18;18;18;18;18;13;18;4822;0;20;0 +CSV: 1;0;10;18;17;18;17;13;13;18;18;18;18;18;13;18;4823;0;20;0 +CSV: 0;-1;10;18;18;18;17;13;13;18;18;18;18;18;13;18;4823;0;20;0 +CSV: 1;-1;10;17;17;18;17;14;15;18;18;18;18;18;13;17;4822;-20;20;0 +CSV: 0;0;10;17;17;17;18;13;13;18;18;18;18;18;13;18;4823;0;20;0 +CSV: 1;-1;10;18;17;18;17;13;13;18;17;18;18;18;13;17;4822;0;20;0 +CSV: 0;0;10;17;18;18;17;13;13;18;18;18;18;18;13;18;4823;0;20;0 +CSV: 0;-1;10;17;17;18;18;13;13;18;17;18;18;18;13;18;4823;0;20;0 +CSV: 0;0;10;18;18;18;17;13;15;20;18;18;18;18;13;18;4823;0;20;0 +CSV: 1;-1;10;17;17;17;17;13;15;18;18;18;18;18;13;18;4823;0;20;0 +CSV: 0;0;8;17;17;18;17;13;15;18;18;18;18;18;13;18;4822;0;20;0 +CSV: 1;-1;10;17;17;18;18;13;13;18;17;18;18;18;13;18;4822;0;20;0 +CSV: 0;-1;10;18;18;18;18;13;15;18;20;18;18;18;13;18;4823;0;20;0 +CSV: 0;-1;10;17;17;18;17;13;15;18;18;20;18;18;13;18;4823;0;20;0 +CSV: 1;0;10;17;17;18;18;13;15;18;18;18;18;18;13;17;4823;0;20;0 +CSV: 0;-1;8;17;17;18;17;13;15;18;18;18;18;18;13;18;4822;0;20;0 +CSV: 1;0;10;17;18;18;17;13;13;18;18;18;18;18;13;18;4823;0;20;0 +CSV: 0;0;10;17;17;18;17;12;13;18;18;18;18;18;13;18;4822;0;20;0 +CSV: 0;-1;10;17;17;18;17;13;15;18;18;18;18;18;13;17;4822;-20;20;0 +CSV: 0;-1;10;17;17;18;17;13;13;18;18;18;18;18;13;18;4822;0;20;0 +CSV: 0;-1;10;17;17;18;18;12;13;18;18;18;18;18;13;18;4823;0;20;0 +CSV: 0;0;10;17;17;18;17;13;15;18;18;18;18;18;13;17;4822;0;20;0 +CSV: 0;0;10;18;17;18;17;12;13;17;18;18;18;18;13;18;4822;20;20;0 +CSV: 1;-1;8;17;17;18;17;14;15;18;17;18;18;18;13;18;4822;-20;20;0 +CSV: 0;-1;10;17;18;18;17;13;13;18;18;18;18;18;13;18;4822;0;20;0 +*** ALARM FLAGS *** +Alarm bits=0x8 | 0b1000 +Alarm bit=0b1000 -> Discharging undervoltage + +CSV: 0;-1;10;17;18;18;17;13;13;18;18;18;18;18;13;18;4822;0;0;0 +SOC[%]=0 -> Remaining Capacity[Ah]=0 +Set LCD display page to: 0 +CSV: 1;0;11;17;18;18;18;13;15;18;18;18;18;18;13;17;4823;0;0;0 +Charging MosFet enabled, active | Discharging MosFet enabled, not active | Balancing enabled, not active +CSV: 1;-1;10;18;17;18;18;13;15;18;18;18;20;21;15;18;4823;0;0;0 +CSV: 1;0;11;17;17;20;20;15;15;18;20;18;20;18;13;18;4823;0;0;0 +CSV: 1;0;10;17;18;18;18;13;15;18;18;20;20;18;13;17;4823;0;0;0 +CSV: 4;7;17;25;24;22;22;15;28;23;24;26;26;27;24;29;4830;125;0;0 +Battery Voltage[V]=48.30, Current[A]=1.25, Power[W]=60, Difference to full[V]=6.9 +Charging MosFet enabled, active | Discharging MosFet enabled, active | Balancing enabled, not active +CSV: 23;21;34;31;21;-5;-5;-37;49;-5;3;16;16;31;38;45;4862;1236;0;0 +Battery Voltage[V]=48.62, Current[A]=12.36, Power[W]=600, Difference to full[V]=6.6 +CSV: 28;26;37;35;25;-1;-1;-32;55;-57;-34;-8;-8;23;43;47;4870;2766;0;1 +Battery Voltage[V]=48.70, Current[A]=27.66, Power[W]=1347, Difference to full[V]=6.5 +Charging MosFet enabled, active | Discharging MosFet enabled, active | Balancing enabled, active +CSV: 34;33;44;24;65532;65475;65475;65414;50;65477;65503;-5;-5;24;44;48;4872;2787;0;1 +Battery Voltage[V]=48.72, Current[A]=27.87, Power[W]=1357, Difference to full[V]=6.5 +CSV: 38;37;50;28;0;-57;65478;65420;61;65483;65507;65532;65532;26;48;53;4881;2787;0;1 +Battery Voltage[V]=48.81, Current[A]=27.87, Power[W]=1360, Difference to full[V]=6.4 +CSV: 44;40;51;31;0;65481;65481;65424;55;65483;65509;1;1;31;51;55;4884;2766;0;1 +Battery Voltage[V]=48.84, Current[A]=27.66, Power[W]=1350, Difference to full[V]=6.4 +CSV: 44;40;51;31;0;65481;65481;65426;58;65485;65512;3;5;34;54;58;4887;2787;0;1 +Battery Voltage[V]=48.87, Current[A]=27.87, Power[W]=1362, Difference to full[V]=6.3 +CSV: 47;44;54;33;4;65483;65483;65428;61;65487;65513;4;7;37;56;60;4892;2745;0;1 +Battery Voltage[V]=48.92, Current[A]=27.45, Power[W]=1342, Difference to full[V]=6.3 +CSV: 49;47;56;35;6;65487;65487;65433;64;65492;65518;8;12;39;59;65;4896;2787;0;1 +Battery Voltage[V]=48.96, Current[A]=27.87, Power[W]=1364, Difference to full[V]=6.2 +CSV: 51;49;58;38;7;65489;65487;65433;66;65492;65519;9;12;42;62;67;4899;2766;0;1 +Battery Voltage[V]=48.99, Current[A]=27.66, Power[W]=1355, Difference to full[V]=6.2 +CSV: 53;50;59;39;10;65491;65491;65436;69;65493;65519;9;13;43;65;69;4902;2808;0;1 +Battery Voltage[V]=49.02, Current[A]=28.08, Power[W]=1376, Difference to full[V]=6.2 +CSV: 55;53;61;39;10;65491;65491;65439;73;65497;65522;10;14;42;65;69;4902;2766;0;1 +Battery Voltage[V]=49.02, Current[A]=27.66, Power[W]=1355, Difference to full[V]=6.2 +CSV: 58;54;62;42;14;65496;65495;65436;71;65496;65522;11;16;45;66;71;4905;2766;0;1 +Battery Voltage[V]=49.05, Current[A]=27.66, Power[W]=1356, Difference to full[V]=6.2 +CSV: 59;55;65;43;15;65496;65497;65437;72;65498;65523;12;16;46;69;72;4908;2766;0;1 +Battery Voltage[V]=49.08, Current[A]=27.66, Power[W]=1357, Difference to full[V]=6.1 +CSV: 58;55;67;48;21;-34;-37;65442;75;-37;65524;15;18;49;70;75;4911;2787;0;1 +Battery Voltage[V]=49.11, Current[A]=27.87, Power[W]=1368, Difference to full[V]=6.1 +CSV: 62;59;70;50;21;65500;65498;65442;82;65503;65526;15;17;48;70;76;4914;2787;0;1 +Battery Voltage[V]=49.14, Current[A]=27.87, Power[W]=1369, Difference to full[V]=6.1 +CSV: 62;59;70;50;21;65500;65498;65445;76;65503;65530;20;23;51;73;75;4917;2787;0;1 +Battery Voltage[V]=49.17, Current[A]=27.87, Power[W]=1370, Difference to full[V]=6.0 +CSV: 65;61;71;50;20;65501;65500;65445;81;65505;65529;17;23;53;75;78;4919;2787;0;1 +Battery Voltage[V]=49.19, Current[A]=27.87, Power[W]=1370, Difference to full[V]=6.0 +CSV: 67;64;73;54;23;65503;-34;65444;88;65511;-1;22;25;51;75;78;4919;2787;0;1 +CSV: 67;64;71;51;23;65505;65506;65449;81;65511;0;23;26;54;73;77;4923;2787;0;1 +Battery Voltage[V]=49.23, Current[A]=27.87, Power[W]=1372, Difference to full[V]=6.0 +CSV: 67;66;76;58;30;65510;65506;65446;89;65508;65534;22;28;58;78;83;4924;2766;1;1 +SOC[%]=1 -> Remaining Capacity[Ah]=1 +CSV: 71;67;77;59;30;65511;65508;65448;91;65511;-1;23;30;59;80;85;4927;2766;1;1 +Battery Voltage[V]=49.27, Current[A]=27.66, Power[W]=1362, Difference to full[V]=5.9 +CSV: 72;69;78;59;30;65508;65508;65449;91;65512;2;23;30;59;82;86;4930;2787;1;1 +Battery Voltage[V]=49.30, Current[A]=27.87, Power[W]=1373, Difference to full[V]=5.9 +CSV: 72;69;78;59;30;65511;65510;65452;89;65514;3;25;31;59;82;87;4932;2787;1;1 +Battery Voltage[V]=49.32, Current[A]=27.87, Power[W]=1374, Difference to full[V]=5.9 +CSV: 75;71;78;58;29;65510;65509;65451;86;65516;3;25;30;59;82;87;4934;2808;1;1 +Battery Voltage[V]=49.34, Current[A]=28.08, Power[W]=1385, Difference to full[V]=5.9 +Timeout reached, suppress consecutive error beeps +CSV: 75;71;78;60;31;65512;65512;65455;88;65518;8;31;33;61;83;86;4936;2787;1;1 +Battery Voltage[V]=49.36, Current[A]=27.87, Power[W]=1375, Difference to full[V]=5.8 +CSV: 75;71;78;61;33;65516;65516;65457;94;65521;10;33;34;62;85;87;4939;2787;1;1 +Battery Voltage[V]=49.39, Current[A]=27.87, Power[W]=1376, Difference to full[V]=5.8 +CSV: 75;72;81;62;36;65517;65517;65458;99;65521;10;34;38;65;86;88;4941;2787;1;1 +Battery Voltage[V]=49.41, Current[A]=27.87, Power[W]=1377, Difference to full[V]=5.8 +CSV: 76;72;85;64;37;65518;65517;65459;97;65521;9;35;37;66;89;91;4942;2808;1;1 +CSV: 77;75;87;67;41;65520;65519;65459;97;65518;10;35;37;66;89;91;4942;2766;1;1 +Battery Voltage[V]=49.42, Current[A]=27.66, Power[W]=1366, Difference to full[V]=5.8 +CSV: 80;77;88;69;41;65521;65518;65458;100;65519;11;36;41;70;91;93;4945;2787;1;1 +Battery Voltage[V]=49.45, Current[A]=27.87, Power[W]=1378, Difference to full[V]=5.8 +CSV: 82;80;91;70;42;65521;65518;65460;102;65521;11;34;41;70;92;94;4946;2787;1;1 +CSV: 86;81;89;69;39;65521;65519;65462;99;-8;14;39;41;69;91;92;4949;2787;1;1 +Battery Voltage[V]=49.49, Current[A]=27.87, Power[W]=1379, Difference to full[V]=5.7 +CSV: 86;81;89;69;39;65521;65524;65466;103;65524;15;39;43;72;93;94;4950;2787;1;1 +CSV: 83;80;91;72;45;65525;65523;65463;104;65526;16;41;44;75;96;97;4952;2766;1;1 +Battery Voltage[V]=49.52, Current[A]=27.66, Power[W]=1369, Difference to full[V]=5.7 +CSV: 85;81;93;73;45;-8;65525;65466;107;65526;16;41;47;76;96;99;4955;2766;1;1 +Battery Voltage[V]=49.55, Current[A]=27.66, Power[W]=1370, Difference to full[V]=5.7 +CSV: 86;85;94;76;48;-8;65523;65465;107;65527;17;42;48;77;98;100;4956;2787;1;1 +CSV: 87;86;96;76;47;65527;65524;65465;107;-8;18;41;48;77;98;103;4958;2808;1;1 +Battery Voltage[V]=49.58, Current[A]=28.08, Power[W]=1392, Difference to full[V]=5.6 +CSV: 88;87;97;77;49;65530;65527;65466;108;65530;20;41;47;80;100;104;4960;2787;1;1 +CSV: 89;88;97;77;49;-8;65526;65466;110;65529;20;42;50;80;100;104;4960;2808;2;1 +SOC[%]=2 -> Remaining Capacity[Ah]=2 +CSV: 92;89;98;79;49;65529;65526;65467;110;65529;20;42;49;79;102;105;4961;2808;2;1 +CSV: 94;91;100;81;52;-5;65529;65470;112;65533;21;43;50;81;103;107;4963;2766;2;1 +Battery Voltage[V]=49.63, Current[A]=27.66, Power[W]=1372, Difference to full[V]=5.6 +CSV: 94;92;102;81;52;65532;65529;65472;113;65533;22;44;52;81;104;108;4965;2787;2;1 +Battery Voltage[V]=49.65, Current[A]=27.87, Power[W]=1383, Difference to full[V]=5.6 +CSV: 96;93;103;82;52;65534;-5;65473;114;-1;22;45;52;81;105;108;4966;2766;2;1 +CSV: 96;93;103;82;52;65534;-5;65473;108;65534;23;46;52;81;105;109;4968;2808;2;1 +Battery Voltage[V]=49.68, Current[A]=28.08, Power[W]=1395, Difference to full[V]=5.5 +CSV: 97;94;104;82;52;65532;-5;65472;108;-1;25;48;54;83;107;110;4969;2787;2;1 +CSV: 99;94;105;85;54;65534;65532;65474;114;3;29;52;56;82;105;107;4972;2808;2;1 +Battery Voltage[V]=49.72, Current[A]=28.08, Power[W]=1396, Difference to full[V]=5.5 +CSV: 100;96;103;83;53;65534;65534;65477;118;2;30;54;58;87;109;109;4972;2787;2;1 +CSV: 97;94;105;86;59;4;2;-57;119;6;30;52;55;83;107;110;4975;2766;2;1 +Battery Voltage[V]=49.75, Current[A]=27.66, Power[W]=1376, Difference to full[V]=5.5 +CSV: 102;98;108;87;58;3;0;65482;112;5;31;57;60;88;110;112;4977;2766;2;1 +Battery Voltage[V]=49.77, Current[A]=27.66, Power[W]=1376, Difference to full[V]=5.4 +CSV: 103;99;109;87;57;1;65534;65481;121;6;30;52;57;85;109;112;4977;2808;2;1 +Battery Voltage[V]=49.77, Current[A]=28.08, Power[W]=1397, Difference to full[V]=5.4 +CSV: 104;100;109;88;58;2;1;65482;114;3;30;56;59;88;112;113;4978;2787;2;1 +CSV: 105;102;109;87;57;2;1;65481;114;4;31;57;61;91;113;114;4979;2808;2;1 +CSV: 105;103;110;88;58;4;2;65483;115;5;31;56;60;90;114;116;4980;2787;2;1 +CSV: 108;104;109;89;61;6;6;65486;118;9;33;58;64;93;114;118;4981;2745;2;1 +Battery Voltage[V]=49.81, Current[A]=27.45, Power[W]=1367, Difference to full[V]=5.4 +CSV: 108;104;109;89;60;6;6;65484;119;8;33;57;63;93;115;118;4983;2766;2;1 +Battery Voltage[V]=49.83, Current[A]=27.66, Power[W]=1378, Difference to full[V]=5.4 +CSV: 108;104;109;89;60;6;6;65487;121;9;33;58;63;93;115;119;4984;2787;2;1 +CSV: 108;104;110;91;61;7;8;65486;125;9;34;57;62;92;116;120;4985;2808;3;1 +SOC[%]=3 -> Remaining Capacity[Ah]=3 +CSV: 109;104;112;90;62;8;9;65488;127;13;36;58;64;93;116;121;4987;2766;3;1 +Battery Voltage[V]=49.87, Current[A]=27.66, Power[W]=1379, Difference to full[V]=5.3 +CSV: 109;104;112;93;65;9;11;65487;130;13;36;59;64;93;118;123;4990;2808;3;1 +Battery Voltage[V]=49.90, Current[A]=28.08, Power[W]=1401, Difference to full[V]=5.3 +CSV: 110;108;116;97;68;13;9;65489;124;16;42;63;68;96;116;119;4992;2766;3;1 +Battery Voltage[V]=49.92, Current[A]=27.66, Power[W]=1380, Difference to full[V]=5.3 +CSV: 113;109;116;96;65;13;10;65490;129;17;42;63;68;96;116;119;4992;2766;3;1 +CSV: 110;107;115;97;69;14;14;65493;124;15;41;63;70;99;121;124;4992;2787;3;1 +CSV: 112;108;115;97;70;15;15;65494;125;18;44;68;70;97;118;120;4994;2787;3;1 +Battery Voltage[V]=49.94, Current[A]=27.87, Power[W]=1391, Difference to full[V]=5.3 +CSV: 113;108;115;98;70;15;15;65495;125;19;44;68;70;98;119;123;4996;2787;3;1 +Battery Voltage[V]=49.96, Current[A]=27.87, Power[W]=1392, Difference to full[V]=5.2 +CSV: 114;110;118;98;70;16;16;65495;126;20;46;68;71;98;120;124;4997;2787;3;1 +CSV: 115;110;118;99;71;17;15;65494;127;22;46;69;71;99;121;124;4999;2766;3;1 +Battery Voltage[V]=49.99, Current[A]=27.66, Power[W]=1382, Difference to full[V]=5.2 +CSV: 115;110;118;99;71;19;19;65497;130;22;47;69;71;100;123;124;5000;2766;3;1 +CSV: 116;113;119;100;71;17;19;65497;127;23;47;69;72;100;124;126;5001;2787;3;1 +CSV: 118;114;120;100;72;18;18;65498;134;20;46;71;76;105;127;129;5002;2787;3;1 +CSV: 115;113;121;102;75;20;20;-37;131;20;46;69;75;105;129;130;5003;2787;3;1 +CSV: 120;116;126;105;76;19;16;65495;134;21;47;69;75;105;129;130;5003;2787;3;1 +CSV: 120;118;127;107;79;21;18;65496;137;21;47;70;76;107;129;131;5005;2787;3;1 +Battery Voltage[V]=50.05, Current[A]=27.87, Power[W]=1394, Difference to full[V]=5.2 +CSV: 120;118;129;107;79;24;20;-37;139;21;47;71;77;107;130;132;5006;2766;3;1 +CSV: 121;118;129;108;80;24;20;-37;140;24;49;74;79;108;131;132;5007;2787;3;1 +CSV: 120;119;129;109;81;25;21;-37;140;23;49;74;80;109;131;134;5009;2787;4;1 +SOC[%]=4 -> Remaining Capacity[Ah]=4 +Battery Voltage[V]=50.09, Current[A]=27.87, Power[W]=1396, Difference to full[V]=5.1 +CSV: 123;120;130;107;79;27;25;65505;142;24;51;75;80;110;131;134;5010;2766;4;1 +CSV: 123;120;130;109;82;26;22;65501;141;24;52;76;81;110;134;134;5011;2787;4;1 +CSV: 124;120;131;110;82;26;23;65501;142;25;52;76;82;110;134;136;5013;2787;4;1 +Battery Voltage[V]=50.13, Current[A]=27.87, Power[W]=1397, Difference to full[V]=5.1 +CSV: 125;123;132;110;82;26;24;65501;141;27;52;76;82;112;135;136;5014;2766;4;1 +CSV: 125;124;132;112;84;29;25;-32;141;29;53;76;84;113;135;137;5015;2766;4;1 +CSV: 126;125;134;113;84;27;25;-32;139;29;53;77;84;113;136;139;5016;2766;4;1 +CSV: 129;126;135;113;84;29;25;-32;146;29;53;77;84;113;136;139;5016;2787;4;1 +CSV: 130;127;135;113;82;28;25;-32;146;29;53;77;82;113;136;139;5017;2787;4;1 +CSV: 130;126;132;112;82;28;28;65508;140;35;58;82;85;114;135;136;5019;2787;4;1 +Battery Voltage[V]=50.19, Current[A]=27.87, Power[W]=1398, Difference to full[V]=5.0 +CSV: 129;127;136;116;87;31;26;65505;147;31;57;81;87;118;137;140;5019;2787;4;1 +CSV: 130;126;134;113;85;34;32;65510;148;31;58;81;87;118;139;142;5021;2787;4;1 +Battery Voltage[V]=50.21, Current[A]=27.87, Power[W]=1399, Difference to full[V]=5.0 +CSV: 130;129;137;118;88;32;28;65506;147;32;57;81;88;118;139;145;5023;2787;4;1 +Battery Voltage[V]=50.23, Current[A]=27.87, Power[W]=1399, Difference to full[V]=5.0 +CSV: 132;130;137;119;90;32;28;65508;145;35;58;81;88;119;140;145;5024;2766;4;1 +CSV: 134;131;139;119;90;33;30;65509;146;34;59;81;90;120;142;146;5026;2787;4;1 +CSV: 134;131;139;119;91;34;30;65508;146;34;60;82;90;120;143;146;5026;2787;4;1 +CSV: 135;132;140;120;91;35;31;65510;147;36;62;82;91;121;143;146;5028;2766;4;1 +Battery Voltage[V]=50.28, Current[A]=27.66, Power[W]=1390, Difference to full[V]=4.9 +CSV: 134;132;142;121;92;37;33;65511;153;36;62;82;91;121;143;146;5028;2787;4;1 +CSV: 135;132;143;123;93;37;34;65512;152;36;62;85;92;121;145;147;5030;2787;4;1 +Battery Voltage[V]=50.30, Current[A]=27.87, Power[W]=1401, Difference to full[V]=4.9 +CSV: 137;135;145;123;93;36;34;65516;152;40;64;85;90;119;143;147;5031;2787;5;1 +SOC[%]=5 -> Remaining Capacity[Ah]=5 +CSV: 139;135;143;121;92;36;35;65515;153;41;65;88;92;120;142;145;5033;2787;5;1 +Battery Voltage[V]=50.33, Current[A]=27.87, Power[W]=1402, Difference to full[V]=4.9 +CSV: 131;127;132;112;80;25;23;65516;148;37;65;88;95;124;147;148;5033;2787;5;1 +CSV: 131;127;123;121;109;91;87;69;125;21;49;71;76;104;129;130;5010;1006;5;1 +Battery Voltage[V]=50.10, Current[A]=10.06, Power[W]=504, Difference to full[V]=5.1 +All alarms are cleared +CSV: 118;115;124;120;109;91;87;69;125;90;100;104;110;120;126;126;5002;83;5;1 +Battery Voltage[V]=50.02, Current[A]=0.83, Power[W]=41, Difference to full[V]=5.2 diff --git a/extras/JK-BMS.log b/extras/JK-BMS.log index 8e2f1dd..df5f9ea 100644 --- a/extras/JK-BMS.log +++ b/extras/JK-BMS.log @@ -170,3 +170,20 @@ Charging MosFet enabled, active | Discharging MosFet enabled, active | Balancing Charging MosFet enabled, active | Discharging MosFet enabled, active | Balancing enabled, not active Charging MosFet enabled, active | Discharging MosFet enabled, active | Balancing enabled, active Charging MosFet enabled, active | Discharging MosFet enabled, active | Balancing enabled, not active + +... + +Total Runtime Minutes=124506 -> 86D11H50M +*** CELL INFO *** +16 Cells, Minimum=3069 mV, Maximum=3077mV, Delta=8 mV, Average=3074 mV + 1=3069 mV, 2=3069 mV, 3=3070 mV, 4=3076 mV, 5=3076 mV, 6=3076 mV, 7=3076 mV, 8=3072 mV, + 9=3072 mV, 10=3076 mV, 11=3076 mV, 12=3077 mV, 13=3077 mV, 14=3076 mV, 15=3071 mV, 16=3076 mV, + +*** CELL STATISTICS *** +Total balancing time=2008 s -> 0D00H33M28S +Cell Minimum percentages + 1=37 % | 768, 2=37 % | 758, 3=16 % | 328, 4= 0 % | 0, 5= 0 % | 0, 6= 0 % | 0, 7= 0 % | 0, 8= 3 % | 68, + 9= 0 % | 0, 10= 0 % | 0, 11= 0 % | 0, 12= 0 % | 0, 13= 0 % | 0, 14= 0 % | 0, 15= 5 % | 107, 16= 0 % | 0, +Cell Maximum percentages + 1= 0 % | 0, 2= 0 % | 0, 3= 0 % | 0, 4= 5 % | 226, 5= 6 % | 263, 6= 7 % | 311, 7= 7 % | 329, 8= 0 % | 0, + 9= 0 % | 0, 10= 9 % | 400, 11=12 % | 531, 12=13 % | 572, 13=14 % | 615, 14=10 % | 441, 15= 0 % | 0, 16=14 % | 624,