/*
 *  JK-BMSToPylontechCAN.cpp
 *
 *  Converts the JK-BMS RS485 data to Pylontech CAN data for inverters
 *  which are not compatible with JK-BMS protocol but with Pylontech protocol, like Deye inverters.
 *  It displays many BMS information and alarms on a locally attached 2004 LCD.
 *  The JK-BMS data are provided as RS232 TTL.
 *
 *  Data from JK-BMS is received by the Hardware USART, and sent by the SoftwareSerialTX library.
 *  Data for serial monitor is sent by Hardware USART (connected to USB).
 *
 *  The software TX and hardware RX lines are connected to the JK-BMS and run with 115200 baud.
 *  CAN is connected to the inverter which must accept Pylontech low voltage protocol, which runs with 500 kBit/s.
 *  This protocol is used e.g by the Pylontech US2000 battery.
 *  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 *  !!! 8 MHz crystal and 500 kBit/s does not work with MCP2515                 !!!
 *  !!! So you must replace the crystal of the module with a 16 (or 20) MHz one !!!
 *  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 *
 *  Internal operation (default every 2 seconds):
 *  1. A request to deliver all informations is sent to the BMS (1.85 ms).
 *  2. Wait and receive the BMS status frame (0.18 to 1 ms + 25.5 ms).
 *  3. The BMS status frame is stored in a buffer and parity and other plausi checks are made.
 *  4. The cell data are converted and enhanced to fill the JKConvertedCellInfoStruct.
 *     Other frame data are mapped to a C structure.
 *     But all words and longs in this structure are filled with big endian and thus cannot be read directly but must be swapped on reading.
 *  5. Other frame data are converted and enhanced to fill the JKComputedDataStruct.
 *  6. The content of the status frame is printed. After reset, all info is printed once, then only dynamic info is printed.
 *  7. The required CAN data is filled in the according PylontechCANFrameInfoStruct.
 *  8. Dynamic data and errors are displayed on the optional 2004 LCD if attached.
 *  9. CAN data is sent..
 *
 *  The LCD has 4 "pages" showing overview data, up to 16 cell voltages, up to 16 cell minimum and maximum statistics, or SOC and current with big numbers.
 *  The pages can be switched by the button at pin 2.
 *
 *  On timeout, the last BMS data is kept.
 *
 *  It uses the following libraries, which are included in this repository:
 *  SoftwareSerialTX for sending Serial to JK-BMS
 *  Modified LiquidCrystal_I2C for I2C connected LCD
 *  LCDBigNumbers for LCD big number generation
 *  EasyButtonAtInt01 for LCD page switching button
 *  SoftI2CMaster for minimal I2C functions
 *  modified mcp_can_dfs.h file from Seed-Studio Seeed_Arduino_CAN
 *
 *  Based on https://github.com/syssi/esphome-jk-bms and https://github.com/maxx-ukoo/jk-bms2pylontech
 *  Tested with SUN-5K-SG03LP1-EU
 *
 *  Available as Wokwi example https://wokwi.com/projects/371657348012321793
 *
 *  Copyright (C) 2023-2024  Armin Joachimsmeyer
 *  Email: [email protected]
 *
 *  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 <http://www.gnu.org/licenses/gpl.html>.
 *
 *
 * # Connection schematic
 * A schottky diode is inserted into the RX line to allow programming the AVR with the JK-BMS still connected.
 *
 * ALTERNATIVE EXTERNAL POWER SUPPLY:
 *                                          78L05    Optional Schottky diode - From Uno/Nano 5 V
 *                                           ___                         to enable powering CAN
 * Optional 6.6 V from Battery #2 >-o-------|___|-------o-|<|-< Uno 5V   module by Nano USB, if
 *                                  |         |         |                battery is not attached
 *                                  |        ---        |
 * . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
 *                                  |                   |
 *  __________ Schottky diode   ____|____           ____|____             _________
 * |        TX|>---|<|-- RX -->|RX Vin   |<- SPI ->|   5V    |           |         |
 * |        RX|<-------- TX --<|4  Uno/  |         | MCP2515 |           |         |
 * |  JK-BMS  |                |   Nano  |         |   CAN   |<-- CAN -->|  DEYE   |
 * |          |<------- GND -->|         |<- GND ->|         |           |         |
 * |__________|                |_________|         |_________|           |_________|
 *
 * # Connection diagram for JK-BMS GPS / UART-TTL socket (4 Pin, JST 1.25mm pitch)
 *  ___ ________ ___
 * |                |
 * | O   O   O   O  |
 * |GND  RX  TX VBAT|
 * |________________|
 *   |   |   |
 *   |   |   --|<|-- RX of Uno / Nano
 *   |   ----------- D4 (or other pin, if specified)
 *   --------------- GND
 */

/*
 * Ideas:
 * Balancing time per day / week / month etc.
 */
#include <Arduino.h>

#define VERSION_EXAMPLE "3.0.0"
// For full revision history see https://github.com/ArminJo/JK-BMSToPylontechCAN?tab=readme-ov-file#revision-history
#define MILLIS_IN_ONE_SECOND 1000L

/*
 * Only for analytics! I.e undefined and not used if NO_ANALYTICS is defined.
 * You can compute the ESR (Equivalent Series Resistor) by looking at a big current jump and then use the voltage jump to compute the ESR.
 * ESR = delta voltage / delta current.
 * The exact values can be extracted more easily by using the Arduino Serial Monitor.
 * Printout of the current jump is forced by connecting pin DISABLE_ESR_IN_GRAPH_OUTPUT_PIN (8) to low.
 */
#if !defined(BATTERY_ESR_MILLIOHM)
#define BATTERY_ESR_MILLIOHM 13   // If defined, the voltage in the analytics graph is corrected to get a smoother voltage curve.
#endif
/*
 * If battery SOC is below this value, the inverter is forced to charge the battery from any available power source regardless of inverter settings.
 */
//#define SOC_THRESHOLD_FOR_FORCE_CHARGE_REQUEST_I        5
#define SOC_THRESHOLD_FOR_FORCE_CHARGE_REQUEST_I        0 // This disables the setting if the force charge request, even if battery SOC is 0.

/*
 * Macros for CAN data modifications
 */
//#define CAN_DATA_MODIFICATION         // Currently enables the function to reduce max current at high SOC level
//#define USE_CCCV_MODIFY_FUNCTION      // Changes modification to CCCV method my Ngoc: https://github.com/ArminJo/JK-BMSToPylontechCAN/discussions/31
//#define USE_OWN_MODIFY_FUNCTION       // Use (currently empty) function which must be filled in at bottom of Pylontech_CAN.hpp
/*
 * Values for standard CAN data modification
 */
//#define MAX_CURRENT_MODIFICATION_LOWER_SOC_THRESHOLD_PERCENT        80  // Start SOC for linear reducing maximum current. Default 80
//#define MAX_CURRENT_MODIFICATION_MIN_CURRENT_TENTHS_OF_AMPERE       50  // Value of current at 100 % SOC. Units are 100 mA! Default 50
#if !defined(DO_NOT_SHOW_SHORT_CELL_VOLTAGES)
#define SHOW_SHORT_CELL_VOLTAGES // Print 3 digits cell voltage (value - 3.0 V) on Cell Info page. Enables display of up to 20 voltages or additional information.
#endif

#define STANDALONE_TEST       // If activated, fixed BMS data is sent to CAN bus and displayed on LCD.
#if defined(STANDALONE_TEST)
#define ENABLE_MONITORING
#  if defined(USE_NO_LCD)
#undef USE_NO_LCD // LCD is activated for standalone test
#  endif
#endif

/*
 * Options to reduce program size
 */
#if defined(ENABLE_MONITORING)
#  if !defined(MONOTORING_PERIOD_SECONDS)
//#define MONOTORING_PERIOD_FAST    // If active, then print CSV line every 2 seconds, else every minute
#define MONOTORING_PERIOD_SLOW    // If active, then print CSV line every hour, and CSV line every 10 minutes
#  endif
#else
#define DISABLE_MONITORING      // Disables writing cell and current values CSV data to serial output. Saves 534 bytes program space. - currently activated to save program space.
#endif

#if !defined(SERIAL_INFO_PRINT) && !defined(STANDALONE_TEST)
#define NO_SERIAL_INFO_PRINT    // Disables writing some info to serial output. Saves 974 bytes program space. - currently activated to save program space.
#endif

#if !defined(ENABLE_LIFEPO4_PLAUSI_WARNING)
#define SUPPRESS_LIFEPO4_PLAUSI_WARNING     // Disables warning on Serial out about using LiFePO4 beyond 3.0 v to 3.45 V.
#endif
//#define NO_CAPACITY_35F_EXTENSIONS // Disables generating of frame 0x35F for total capacity. This additional frame is no problem for Deye inverters.
//#define NO_CAPACITY_379_EXTENSIONS // Disables generating of frame 0x379 for total capacity. This additional frame is no problem for Deye inverters.
//#define NO_BYD_LIMITS_373_EXTENSIONS // Disables generating of frame 0x373 for cell limits as sent by BYD battery. See https://github.com/dfch/BydCanProtocol/tree/main. This additional frame is no problem for Deye inverters.
//#define NO_CELL_STATISTICS    // Disables generating and display of cell balancing statistics. Saves 16558 bytes program space.
//#define NO_ANALYTICS          // Disables generating, storing and display of SOC graph for Arduino Serial Plotter. Saves 3882 bytes program space.
//#define USE_NO_LCD            // Disables the code for the LCD display. Saves 25% program space on a Nano.

//#define USE_NO_COMMUNICATION_STATUS_LEDS // The code for the BMS and CAN communication status LED is deactivated and the pins are not switched to output

#if !defined(USE_NO_LCD)
#define USE_SERIAL_2004_LCD // Parallel or 1604 LCD not yet supported
#define LCD_MESSAGE_PERSIST_TIME_MILLIS 2000
#endif

#if defined(ARDUINO_AVR_NANO)
/*
 * On a unmodified nano, the fuse settings reserve 2 kB program space, to be backwards compatible.
 * Only 0,5 kB is required for the new bootloader.
 * You can change this by using the Arduino IDE, selecting the Uno as board connect your Nano
 * And then run Burn Bootloader. This will not only leave the 0.5 kB new bootloader
 * but additionally this sets the right fuse settings which reserve only 0.5 kB program space.
 * I regularly do this for all Nano boards I have!
 *
 * With the new fuse settings you can disable the DISABLE_MONITORING macro below,
 * or just compile and upload the source for an Uno board even if you have connected the Nano board.
 */
#define DISABLE_MONITORING              // activating this macro saves 528 bytes program space
#define NO_CELL_STATISTICS          // No cell values, cell minimum, maximum and percentages.
#endif

// sStringBuffer is defined in JK-BMS_LCD.hpp if DISABLE_MONITORING and NO_ANALYTICS are defined
#if !defined(DISABLE_MONITORING)
char sStringBuffer[100];                 // For cvs lines, "Store computed capacity" line and LCD rows
#elif !defined(NO_ANALYTICS)
char sStringBuffer[40];                 // for "Store computed capacity" line, printComputedCapacity() and LCD rows
#endif

/*
 * Pin layout, may be adapted to your requirements
 */
#define BUZZER_PIN                             A2 // To signal errors
#define PAGE_SWITCH_DEBUG_BUTTON_PIN_FOR_INFO   2 // Button at INT0 / D2 for switching LCD pages - definition is not used in program, only for documentation.
// The standard RX of the Arduino is used for the JK_BMS connection.
#define JK_BMS_RX_PIN_FOR_INFO                  0 // We use the Serial RX pin - definition is 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_NO_COMMUNICATION_STATUS_LEDS)
#define TURN_BMS_STATUS_LED_ON                  void()
#define TURN_BMS_STATUS_LED_OFF                 void()
#define TURN_CAN_STATUS_LED_ON                  void()
#define TURN_CAN_STATUS_LED_OFF                 void()
#else
// BMS and CAN communication status LEDs
#  if !defined(BMS_COMMUNICATION_STATUS_LED_PIN)
#define BMS_COMMUNICATION_STATUS_LED_PIN        6
#define CAN_COMMUNICATION_STATUS_LED_PIN        7
#  endif
#define TURN_BMS_STATUS_LED_ON                  digitalWriteFast(BMS_COMMUNICATION_STATUS_LED_PIN, HIGH)
#define TURN_BMS_STATUS_LED_OFF                 digitalWriteFast(BMS_COMMUNICATION_STATUS_LED_PIN, LOW)
#define TURN_CAN_STATUS_LED_ON                  digitalWriteFast(CAN_COMMUNICATION_STATUS_LED_PIN, HIGH)
#define TURN_CAN_STATUS_LED_OFF                 digitalWriteFast(CAN_COMMUNICATION_STATUS_LED_PIN, LOW)
#endif

#if !defined(NO_ANALYTICS)
#define DISABLE_ESR_IN_GRAPH_OUTPUT_PIN         8 // If this pin is held low, the ESR value is not used to correct the voltage in the graph output.
#endif

//#define TIMING_TEST
#if defined(TIMING_TEST)
#define TIMING_TEST_PIN                        10
#endif

/*
 * The SPI pins for connection to CAN converter and the I2C / TWI pins for the LCD are determined by hardware.
 * For Uno / Nano:
 *   SPI: MOSI - 11, MISO - 12, SCK - 13. CS cannot be replaced by constant ground.
 *   I2C: SDA - A4, SCL - A5.
 */
#if !defined(SPI_CS_PIN)                          // Allow override by global symbol
#define SPI_CS_PIN                              9 // Pin 9 is the default pin for the Arduino CAN bus shield. Alternately you can use pin 10 on this shield.
//#define SPI_CS_PIN                              10 // Must be specified before #include "MCP2515_TX.hpp"
#define SPI_MOSI_PIN_FOR_INFO                  11 // Definition is not used in program, only for documentation.
#define SPI_MISO_PIN_FOR_INFO                  12 // Definition is not used in program, only for documentation.
#define SPI_SCK_PIN_FOR_INFO                   13 // Definition is not used in program, only for documentation.
#endif

/*
 * Program timing, may be adapted to your requirements
 */
#if defined(STANDALONE_TEST)
#define MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS     1000
#define MILLISECONDS_BETWEEN_CAN_FRAME_SEND             1000
#define SECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS          "1" // Only for display on LCD
#define SECONDS_BETWEEN_CAN_FRAME_SEND                  "1" // Only for display on LCD
#else
#define MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS     2000
#define MILLISECONDS_BETWEEN_CAN_FRAME_SEND             2000
#define SECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS          "2" // Only for display on LCD
#define SECONDS_BETWEEN_CAN_FRAME_SEND                  "2" // Only for display on LCD
#endif

/*
 * Error beep behavior
 * Overvoltage error cannot be suppressed by a macro!
 * If NO NO_BEEP_ON_ERROR, ONE_BEEP_ON_ERROR or MULTIPLE_BEEPS_WITH_TIMEOUT are activated, we beep forever until error vanishes.
 */
//#define NO_BEEP_ON_ERROR              // If activated, Do not beep on error or timeout.
//#define ONE_BEEP_ON_ERROR             // If activated, only beep once if error was detected.
#define SUPPRESS_CONSECUTIVE_SAME_ALARMS // If activated, recurring alarms are suppressed, e.g. undervoltage alarm tends to come and go in a minute period
#define BEEP_TIMEOUT_SECONDS        60L // 1 minute, Maximum is 254 seconds = 4 min 14 s
#if !defined(NO_BEEP_ON_ERROR) && !defined(ONE_BEEP_ON_ERROR)
#define MULTIPLE_BEEPS_WITH_TIMEOUT     // If activated, beep for 1 minute if error was detected. Timeout is disabled if debug is active.
#endif
bool sDoAlarmOrTimeoutBeep = false;     // If true, we do an error beep at the end of the loop
#  if defined(MULTIPLE_BEEPS_WITH_TIMEOUT)  // Beep one minute
uint8_t sBeepTimeoutCounter = 0;
#endif
#if defined(NO_BEEP_ON_ERROR) && defined(ONE_BEEP_ON_ERROR)
#error NO_BEEP_ON_ERROR and ONE_BEEP_ON_ERROR are both defined, which makes no sense!
#endif

/*
 * Page button stuff
 *
 * Button at INT0 / D2 for switching LCD pages
 */
#define USE_BUTTON_0                 // Enable code for 1. button at INT0 / D2
#define BUTTON_DEBOUNCING_MILLIS 100 // With this you can adapt to the characteristic of your button. Default is 50.
#define NO_BUTTON_RELEASE_CALLBACK   // Disables the code for release callback. This saves 2 bytes RAM and 64 bytes program memory.
#include "EasyButtonAtInt01.hpp"

volatile bool sPageButtonJustPressed = false;
void handlePageButtonPress(bool aButtonToggleState);        // The button press callback function sets just a flag.
EasyButton PageSwitchButtonAtPin2(&handlePageButtonPress);  // Button is connected to INT0
#define LONG_PRESS_BUTTON_DURATION_MILLIS   1000
bool sDebugModeActivated = false; // Is activated on long press
void checkButtonPress();

bool readJK_BMSStatusFrame();
void processJK_BMSStatusFrame();
void handleFrameReceiveTimeout();

#if defined(NO_SERIAL_INFO_PRINT)
#define JK_INFO_PRINT(...)      void();
#define JK_INFO_PRINTLN(...)    void();
#else
#define JK_INFO_PRINT(...)      Serial.print(__VA_ARGS__);
#define JK_INFO_PRINTLN(...)    Serial.println(__VA_ARGS__);
#endif

#include "HexDump.hpp"
#include "digitalWriteFast.h"

/*
 * Software serial for JK-BMS stuff
 */
#if !defined(MAXIMUM_NUMBER_OF_CELLS)
#define MAXIMUM_NUMBER_OF_CELLS     24 // Maximum number of cell info which can be converted. Must be before #include "JK-BMS.hpp".
#endif
#include "JK-BMS.hpp"

/*
 * Software serial for JK-BMS request frame sending
 */
#include "SoftwareSerialTX.h"
/*
 * Use a 115200 baud software serial for the short request frame.
 * If available, we also can use a second hardware serial here :-).
 */
SoftwareSerialTX TxToJKBMS(JK_BMS_TX_PIN);
bool sResponseFrameBytesAreExpected = false;   // If true, request was recently sent so now check for serial input of response frame
uint32_t sMillisOfLastRequestedJKDataFrame = -MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS; // Initial value to start first request immediately
/*
 * BMS communication timeout
 */
uint32_t sMillisOfLastReceivedByte = 0;     // For timeout detection
bool sJKBMSFrameHasTimeout;                 // True, as long as BMS timeout persists.
bool sTimeoutJustdetected = false;          // Is set to true at first detection of timeout and reset by beep timeout

/*
 * CAN stuff
 */
#if !defined(NO_CAPACITY_35F_EXTENSIONS) // SMA Sunny Island inverters
//#define CAPACITY_35F_EXTENSIONS // Add frame 0x35F for total capacity for SMA Sunny Island inverters, which is no problem for Deye inverters.
#endif
#if !defined(NO_CAPACITY_379_EXTENSIONS) // Luxpower SNA inverters
//#define CAPACITY_379_EXTENSIONS // Add frame 0x379 for total capacity for Luxpower SNA inverters, which is no problem for Deye inverters.
#endif
#if !defined(NO_BYD_LIMITS_373_EXTENSIONS) // BYD
//#define BYD_LIMITS_373_EXTENSIONS // Add frame 0x373 for cell limits as sent by BYD battery, which is no problem for Deye inverters.
#endif
#include "Pylontech_CAN.hpp" // Must be before #include "MCP2515_TX.hpp"
#define CAN_BAUDRATE    500000  // 500 kB
#if !defined(MHZ_OF_CRYSTAL_ASSEMBLED_ON_CAN_MODULE)
// Must be specified before #include "MCP2515_TX.hpp"
#define MHZ_OF_CRYSTAL_ASSEMBLED_ON_CAN_MODULE  16 // 16 MHz is default for the Arduino CAN bus shield
//#define MHZ_OF_CRYSTAL_ASSEMBLED_ON_CAN_MODULE   8 // 8 MHz is default for the Chinese breakout board. !!! 8MHz does not work with 500 kB !!!
#endif
#include "MCP2515_TX.hpp"                   // my reduced tx only driver
bool sCANDataIsInitialized = false;         // One time flag, it is never set to false again.
uint32_t sMillisOfLastCANFrameSent = 0;     // For CAN timing

/*
 * Optional analytics stuff
 */
#if defined(NO_ANALYTICS)
#  if defined(BATTERY_ESR_MILLIOHM)
#undef BATTERY_ESR_MILLIOHM // To remove unused code in setup
#  endif
#else
#include "JK-BMS_Analytics.hpp"
#endif

/*
 * Optional LCD stuff
 */
#if !defined(USE_NO_LCD)
#include "JK-BMS_LCD.hpp"
#endif

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.

/*
 * Miscellaneous
 */
#define TIMEOUT_MILLIS_FOR_FRAME_REPLY                  100 // I measured 26 ms between request end and end of received 273 byte result
#if TIMEOUT_MILLIS_FOR_FRAME_REPLY > MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS
#error "TIMEOUT_MILLIS_FOR_FRAME_REPLY must be smaller than MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS to detect timeouts"
#endif
bool sStaticInfoWasSent = false; // Flag to send static info only once after reset.

void processReceivedData();
void printReceivedData();
bool isVCCTooHighSimple();
void handleOvervoltage();

/*
 * Must be after all .hpp includes
 */
//#define LOCAL_DEBUG // This enables debug output only for this file - only for development
#include "LocalDebugLevelStart.h" // no include "LocalDebugLevelEnd.h" required :-)

#if defined(STANDALONE_TEST)
//#define LCD_PAGES_TEST
#  if defined(LCD_PAGES_TEST)
//#define BIG_NUMBER_TEST
#  endif
const uint8_t TestJKReplyStatusFrame[] PROGMEM = { /* Header*/0x4E, 0x57, 0x01, 0x2D, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01,
/*Length of Cell voltages*/
0x79, 0x30,
/*Cell voltages*/
0x01, 0x0C, 0xC6, 0x02, 0x0C, 0xBE, 0x03, 0x0C, 0xC7, 0x04, 0x0C, 0xC7, 0x05, 0x0C, 0xC7, 0x06, 0x0C, 0xC5, 0x07, 0x0C, 0xC6, 0x08,
        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, 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, /*CellOvervoltageProtection*/0x90, 0x0F, 0xD2, 0x91, 0x0F, 0xA0, 0x92, 0x00,
        0x05,
        /*CellOvervoltageProtection*/0x93, 0x0B, 0xEA, 0x94, 0x0C, 0x1C, 0x95, 0x00, 0x05, 0x96, 0x01, 0x2C, 0x97, 0x00, 0x07, 0x98,
        0x00, 0x03, 0x99, 0x00, 0x05, 0x9A, 0x00, 0x05, 0x9B, 0x0C, 0xE4, 0x9C, 0x00, 0x08, 0x9D, 0x01, 0x9E, 0x00, 0x5A, 0x9F,
        0x00, 0x46, 0xA0, 0x00, 0x64, 0xA1, 0x00, 0x64, 0xA2, 0x00, 0x14, /*ChargeOvertemperature*/0xA3, 0x00, 0x46, /*DischargeOvertemperature*/
        0xA4, 0x00, 0x46, /*ChargeUndertemperature*/0xA5, 0xFF, 0xEC, 0xA6, 0xFF, 0xF6, /*DischargeUndertemperature*/0xA7, 0xFF,
        0xEC, 0xA8, 0xFF, 0xF6, 0xA9, 0x0E, /*TotalCapacityAmpereHour*/0xAA, 0x00, 0x00, 0x01, 0x40, 0xAB, 0x01, 0xAC, 0x01, 0xAD,
        0x04, 0x11, 0xAE, 0x01, 0xAF, 0x01, 0xB0, 0x00, 0x0A, 0xB1, 0x14, 0xB2, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x00, 0x00,
        0x00, 0x00, 0xB3, 0x00, 0xB4, 0x49, 0x6E, 0x70, 0x75, 0x74, 0x20, 0x55, 0x73, 0xB5, 0x32, 0x31, 0x30, 0x31, 0xB6, 0x00,
        0x06, 0x07, 0x29, /*Uptime*/0xB7, 0x31, 0x31, 0x2E, 0x58, 0x57, 0x5F, 0x53, 0x31, 0x31, 0x2E, 0x32, 0x36, 0x5F, 0x5F, 0x5F,
        0xB8, 0x00, /*ActualBatteryCapacityAmpereHour*/
        0xB9, 0x00, 0x00, 0x01, 0x30, 0xBA, 0x49, 0x6E, 0x70, 0x75, 0x74, 0x20, 0x55, 0x73, 0x65, 0x72, 0x64, 0x61, 0x4A, 0x4B,
        0x5F, 0x42, 0x32, 0x41, 0x32, 0x30, 0x53, 0x32, 0x30, 0x50, 0xC0, 0x01,
        /*Trailer*/
        0x00, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x51, 0xC2 };

void doStandaloneTest();
#endif

/*
 * Helper macro for getting a macro definition as string
 */
#define STR_HELPER(x) #x
#define STR(x) STR_HELPER(x)

const char StringStartingCANFailed[] PROGMEM = "Starting CAN failed!";

void setup() {
// LED_BUILTIN pin is used as SPI Clock !!!
//    pinModeFast(LED_BUILTIN, OUTPUT);
//    digitalWriteFast(LED_BUILTIN, LOW);

#if !defined(USE_NO_COMMUNICATION_STATUS_LEDS)
    pinModeFast(BMS_COMMUNICATION_STATUS_LED_PIN, OUTPUT);
    pinModeFast(CAN_COMMUNICATION_STATUS_LED_PIN, OUTPUT);
#endif
#if defined(TIMING_TEST)
    pinModeFast(TIMING_TEST_PIN, OUTPUT);
#endif
#if !defined(NO_ANALYTICS)
    pinModeFast(DISABLE_ESR_IN_GRAPH_OUTPUT_PIN, INPUT_PULLUP);
#endif

    Serial.begin(115200);
#if defined(__AVR_ATmega32U4__) || defined(SERIAL_PORT_USBVIRTUAL) || defined(SERIAL_USB) /*stm32duino*/|| defined(USBCON) /*STM32_stm32*/|| defined(SERIALUSB_PID) || defined(ARDUINO_attiny3217)
delay(4000); // To be able to connect Serial monitor after reset or power up and before first print out. Do not wait for an attached Serial Monitor!
#endif
    // Just to know which program is running on my Arduino
    Serial.println(F("START " __FILE__ "\r\nVersion " VERSION_EXAMPLE " from " __DATE__));

#if defined(DISABLE_MONITORING)
    JK_INFO_PRINTLN(F("Monitoring disabled"));
#else
    JK_INFO_PRINTLN(F("Monitoring enabled"));
#endif
#if defined(NO_CELL_STATISTICS)
    JK_INFO_PRINTLN(F("Cell statistics deactivated"));
#else
    JK_INFO_PRINTLN(F("Cell statistics activated"));
#endif
#if defined(NO_ANALYTICS)
    JK_INFO_PRINTLN(F("Analytics deactivated"));
#else
    JK_INFO_PRINTLN(F("Analytics activated"));
    readBatteryESRfromEEPROM();
    JK_INFO_PRINT(F("EEPROM BatteryESR="));
    JK_INFO_PRINTLN(sBatteryESRMilliohm);

    findFirstSOCDataPointIndex();
    DEBUG_PRINT(F("EEPROM SOC data start index="));
    DEBUG_PRINT(SOCDataPointsInfo.ArrayStartIndex);
    DEBUG_PRINT(F(" length="));
    DEBUG_PRINT(SOCDataPointsInfo.ArrayLength);
    DEBUG_PRINT(F(", even="));
    DEBUG_PRINTLN(SOCDataPointsInfo.currentlyWritingOnAnEvenPage);

#  if !defined(NO_ANALYTICS)
    JK_INFO_PRINTLN(F("Disable ESR compensation pin=" STR(DISABLE_ESR_IN_GRAPH_OUTPUT_PIN)));
    JK_INFO_PRINT(F("Battery ESR compensation for voltage "));
    if (digitalReadFast(DISABLE_ESR_IN_GRAPH_OUTPUT_PIN) == LOW) {
        JK_INFO_PRINT(F("dis"));
    } else {
        JK_INFO_PRINT(F("en"));
    }
    JK_INFO_PRINT(F("abled. Specified ESR="));
    JK_INFO_PRINT(sBatteryESRMilliohm);
    JK_INFO_PRINTLN(F("mOhm"));

#  endif
#endif

    tone(BUZZER_PIN, 2200, 50);

#if defined(USE_SERIAL_2004_LCD)
    setupLCD();
#else
    JK_INFO_PRINTLN(F("LCD code deactivated"));
#endif

    /*
     * 115200 baud soft serial to JK-BMS. For serial from BMS we use the hardware Serial RX.
     */
    TxToJKBMS.begin(115200);
    Serial.println(F("Serial to JK-BMS started with 115.200 kbit/s!"));
#if defined(USE_SERIAL_2004_LCD)
    if (sSerialLCDAvailable) {
        myLCD.setCursor(0, 2);
        myLCD.print(F("BMS serial started"));
    }
#endif

    /*
     * CAN initialization
     */
    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_SERIAL_2004_LCD)
        if (sSerialLCDAvailable) {
            myLCD.setCursor(0, 3);
            myLCD.print(F("CAN started"));
            delay(2 * LCD_MESSAGE_PERSIST_TIME_MILLIS); // To see the info
        }
#endif
    } else {
        Serial.println(reinterpret_cast<const __FlashStringHelper*>(StringStartingCANFailed));
#if defined(USE_SERIAL_2004_LCD)
        if (sSerialLCDAvailable) {
            myLCD.setCursor(0, 3);
            myLCD.print(reinterpret_cast<const __FlashStringHelper*>(StringStartingCANFailed));
#  if defined(STANDALONE_TEST)
            delay(LCD_MESSAGE_PERSIST_TIME_MILLIS);
#  else
            delay(4 * LCD_MESSAGE_PERSIST_TIME_MILLIS); // To see the info
#  endif
        }
#endif

    }

    /*
     * Print debug pin info
     */
#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(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_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_SERIAL_2004_LCD)
    printDebugInfoOnLCD();
#endif

#if defined(STANDALONE_TEST)
    /*
     * Copy test data to receive buffer
     */
    Serial.println(F("Standalone test. Use fixed demo data"));
    Serial.println();
    memcpy_P(JKReplyFrameBuffer, TestJKReplyStatusFrame, sizeof(TestJKReplyStatusFrame));
    sReplyFrameBufferIndex = sizeof(TestJKReplyStatusFrame) - 1;
    printJKReplyFrameBuffer();
    Serial.println();
    processReceivedData();
    printReceivedData();
    /*
     * Copy complete reply and computed values for change determination
     */
    lastJKReply.SOCPercent = sJKFAllReplyPointer->SOCPercent;
    lastJKReply.AlarmUnion.AlarmsAsWord = sJKFAllReplyPointer->AlarmUnion.AlarmsAsWord;
    lastJKReply.BMSStatus.StatusAsWord = sJKFAllReplyPointer->BMSStatus.StatusAsWord;
    lastJKReply.SystemWorkingMinutes = sJKFAllReplyPointer->SystemWorkingMinutes;

//    if (true) {
    // first value is still written by processReceivedData
    if (SOCDataPointsInfo.ArrayLength > 250) {
        Serial.println(F("EEPROM for standalone test is already initialized"));
    } else {
        // If EEPROM empty, fill in some values
        int8_t tIncrement = -1;
        Serial.println();
        Serial.println();
        Serial.println(F("Now write initial data to EEPROM"));
        Serial.print(F("SOC="));
        Serial.println(sJKFAllReplyPointer->SOCPercent);
        Serial.print(F("JKComputedData.Battery10MilliAmpere="));
        Serial.println(JKComputedData.Battery10MilliAmpere);
        Serial.print(F("JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt="));
        Serial.println(JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt);

        for (int i = 0; i < 260; ++i) { // force buffer wrap around
            sJKFAllReplyPointer->SOCPercent += tIncrement;
            writeSOCData(); // write too EEPROM here.

            // change accumulated value
            JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt += tIncrement * 4;

            Serial.print(sJKFAllReplyPointer->SOCPercent);
            Serial.print(F(", "));

            // Test negative values
            if (sJKFAllReplyPointer->SOCPercent == 2 && tIncrement < 0) {
                JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt = -10; // -100 mV
                Serial.print(F(" -100 mV "));
            }

            /*
             * Generate EEPROM data by calling writeSOCData(); multiple times for each SOC value
             */
            for (int j = 0; j < (i + 4) * 2; ++j) { // 320 corresponds to 2 Ah
                // accumulate values
                writeSOCData();
                lastJKReply.SOCPercent = sJKFAllReplyPointer->SOCPercent; // for transition detection from 0 to 1
            }

            // Manage 0% and 100 %
            if (sJKFAllReplyPointer->SOCPercent == 0 || sJKFAllReplyPointer->SOCPercent == 100) {
                if (sJKFAllReplyPointer->SOCPercent == 100) {
                    // Add additional capacity after 100%
                    for (int j = 0; j < 8000; ++j) {
                        writeSOCData();
                    }
                } else {
                    // set difference voltage to 0
                    JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt = 0;
                }
                // reverse values at 0 and 100
                JKComputedData.Battery10MilliAmpere = -JKComputedData.Battery10MilliAmpere;
                tIncrement = -tIncrement;
            }
        }
        Serial.println();
        findFirstSOCDataPointIndex();
        Serial.print(F("EEPROM SOC data start index="));
        Serial.print(SOCDataPointsInfo.ArrayStartIndex);
        Serial.print(F(" length="));
        Serial.print(SOCDataPointsInfo.ArrayLength);
        Serial.print(F(", even="));
        Serial.println(SOCDataPointsInfo.currentlyWritingOnAnEvenPage);
    }

#if !defined(DISABLE_MONITORING)
    printCSVLine('t');
#  endif

    doStandaloneTest();
#endif
}

void loop() {

    checkButtonPress();

    /*
     * Request status frame every 2 seconds
     */
    if (millis() - sMillisOfLastRequestedJKDataFrame >= MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS) {
        sMillisOfLastRequestedJKDataFrame = millis(); // set for next check
        /*
         * Flush input buffer and send request to JK-BMS
         */
        while (Serial.available()) {
            Serial.read();
        }
#if defined(TIMING_TEST)
        digitalWriteFast(TIMING_TEST_PIN, HIGH);
#endif
        requestJK_BMSStatusFrame(&TxToJKBMS, sDebugModeActivated); // 1.85 ms
#if defined(TIMING_TEST)
        digitalWriteFast(TIMING_TEST_PIN, LOW);
#endif
        sResponseFrameBytesAreExpected = true; // Enable check for serial input of response
        initJKReplyFrameBuffer();
        sMillisOfLastReceivedByte = millis(); // initialize reply timeout
    }

#if defined(STANDALONE_TEST)
    sBMSFrameProcessingComplete = true; // for LCD timeout etc.
    processReceivedData(); // for statistics
    printBMSDataOnLCD(); // for switching between MAX and MIN display
    delay(MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS); // do it simple :-)

#if !defined(USE_NO_COMMUNICATION_STATUS_LEDS)
    digitalWriteFast(BMS_COMMUNICATION_STATUS_LED_PIN, HIGH); // Turn on status LED. LED is turned off at end of loop.
    delay(20); // do it simple :-)
    digitalWriteFast(BMS_COMMUNICATION_STATUS_LED_PIN, LOW); // Turn on status LED. LED is turned off at end of loop.
    delay(20); // do it simple :-)
    digitalWriteFast(CAN_COMMUNICATION_STATUS_LED_PIN, HIGH); // Turn on status LED. LED is turned off at end of loop.
    delay(20); // do it simple :-)
    digitalWriteFast(CAN_COMMUNICATION_STATUS_LED_PIN, LOW); // Turn on status LED. LED is turned off at end of loop.
#endif

#else
    /*
     * Get reply from BMS and check timeout
     */
    if (sResponseFrameBytesAreExpected) {
        if (Serial.available()) {
#  if defined(TIMING_TEST)
            digitalWriteFast(TIMING_TEST_PIN, HIGH);
#  endif
            if (readJK_BMSStatusFrame()) {
                /*
                 * Frame completely received, now process it
                 */
                sBMSFrameProcessingComplete = true;
                TURN_BMS_STATUS_LED_ON;
                processJK_BMSStatusFrame(); // Process the complete receiving of the status frame and set the appropriate flags
                TURN_BMS_STATUS_LED_OFF;
            }
#  if defined(TIMING_TEST)
            digitalWriteFast(TIMING_TEST_PIN, LOW);
#  endif

        } else if (millis() - sMillisOfLastReceivedByte >= TIMEOUT_MILLIS_FOR_FRAME_REPLY) {
            /*
             * Here we have requested response frame, but serial was not available fore a longer time => timeout at receiving
             */
            sBMSFrameProcessingComplete = true; // Do frame complete handling like beep anyway
            sResponseFrameBytesAreExpected = false; // Do not try to receive more response bytes

            if (!sJKBMSFrameHasTimeout) {
                /*
                 * Do this only once per timeout
                 */
                sJKBMSFrameHasTimeout = true;
                sTimeoutJustdetected = true; // This forces the beep
                handleFrameReceiveTimeout();
            }
        }
    }
#endif // !defined(STANDALONE_TEST)

    /*
     * Send CAN frame independently of the period of JK-BMS data requests,
     * but do not send, if frame is currently requested and not completely received and processed.
     * 0.5 MB/s
     * Inverter reply every second: 0x305: 00-00-00-00-00-00-00-00
     * Do not send, if BMS is starting up, the 0% SOC during this time will trigger a deye error beep.
     */
    if (sCANDataIsInitialized && !JKComputedData.BMSIsStarting && !sResponseFrameBytesAreExpected
            && millis() - sMillisOfLastCANFrameSent >= MILLISECONDS_BETWEEN_CAN_FRAME_SEND) {
        sMillisOfLastCANFrameSent = millis();
        TURN_CAN_STATUS_LED_ON;
        if (sDebugModeActivated) {
            Serial.println(F("Send CAN"));
        }
        sendAllCANFrames(sDebugModeActivated);
        TURN_CAN_STATUS_LED_OFF;
    }

    /**********************************************************
     * Do this once after each complete status frame or timeout
     **********************************************************/
    if (sBMSFrameProcessingComplete) {
        sDebugModeActivated = false; // reset flag here. It may be set again at start of next loop.

        /*
         * Check for overvoltage
         */
        while (isVCCTooHighSimple()) {
            handleOvervoltage();
        }

#if defined(USE_SERIAL_2004_LCD) && !defined(DISPLAY_ALWAYS_ON)
        if (sSerialLCDAvailable) {
            doLCDBacklightTimeoutHandling();
        }
#endif

        /*
         * Check for BMS alarm flags
         */
        if (sAlarmJustGetsActive || sTimeoutJustdetected) {
            sDoAlarmOrTimeoutBeep = true;
#if defined(MULTIPLE_BEEPS_WITH_TIMEOUT)
        } else {
            sBeepTimeoutCounter = 0;
#endif
        }

        /*
         * Alarm / beep handling
         */
#if !defined(NO_BEEP_ON_ERROR)      // Beep enabled

#  if defined(ONE_BEEP_ON_ERROR)    // Beep once
        if (sDoAlarmOrTimeoutBeep) {
            sAlarmJustGetsActive = false; // disable further alarm beeps
            sTimeoutJustdetected = false; // disable further timeout beeps
        }
#  endif

        if (sDoAlarmOrTimeoutBeep) {
            sDoAlarmOrTimeoutBeep = false;
#  if defined(MULTIPLE_BEEPS_WITH_TIMEOUT) // Beep one minute
            sBeepTimeoutCounter++; // incremented at each frame request
            if (sBeepTimeoutCounter == (BEEP_TIMEOUT_SECONDS * 1000U) / MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS) {
                JK_INFO_PRINTLN(F("Timeout reached, suppress consecutive error beeps"));
                sAlarmJustGetsActive = false; // disable further alarm beeps
                sTimeoutJustdetected = false; // disable further timeout beeps
            } else
#  endif
            {
                tone(BUZZER_PIN, 2200);
                tone(BUZZER_PIN, 2200, 50);
                delay(200);
                tone(BUZZER_PIN, 2200, 50);
                delay(200); // to avoid tone interrupts waking us up from sleep
            }
        }
#endif // NO_BEEP_ON_ERROR

        sBMSFrameProcessingComplete = false; // prepare for next loop

    } // if (sBMSFrameProcessingComplete)
}

/*
 * Process the complete receiving of the status frame and set the appropriate flags
 */
void processJK_BMSStatusFrame() {
    if (sDebugModeActivated) {
        /*
         * Do it once at every debug start
         */
        if (sReplyFrameBufferIndex == 0) {
            Serial.println(F("sReplyFrameBufferIndex is 0"));
        } else {
            Serial.print(sReplyFrameBufferIndex + 1);
            Serial.println(F(" bytes received"));
            printJKReplyFrameBuffer();
        }
        Serial.println();
    }

    if (sJKBMSFrameHasTimeout) {
        // First response frame after timeout :-)
        sJKBMSFrameHasTimeout = false;
        sTimeoutJustdetected = false;
#if defined(USE_SERIAL_2004_LCD) && !defined(DISPLAY_ALWAYS_ON)
        checkAndTurnLCDOn(); // Reason is printed anyway :-)
#endif
        JK_INFO_PRINTLN(F("Successfully receiving first BMS status frame after BMS communication timeout"));
    }
    processReceivedData();
    printReceivedData();
#if !defined(NO_ANALYTICS)
    writeSOCData();
#endif
    /*
     * Copy complete reply and computed values for change determination
     */
    lastJKReply.SOCPercent = sJKFAllReplyPointer->SOCPercent;
    lastJKReply.AlarmUnion.AlarmsAsWord = sJKFAllReplyPointer->AlarmUnion.AlarmsAsWord;
    lastJKReply.BMSStatus.StatusAsWord = sJKFAllReplyPointer->BMSStatus.StatusAsWord;
    lastJKReply.SystemWorkingMinutes = sJKFAllReplyPointer->SystemWorkingMinutes;
}

/*
 * Reads all bytes of the requested frame into buffer and prints errors
 * Sets sResponseFrameBytesAreExpected to false, if frame was complete and manages other flags too
 * @return true if frame was completely received
 */
bool readJK_BMSStatusFrame() {
    sMillisOfLastReceivedByte = millis();
    uint8_t tReceiveResultCode = readJK_BMSStatusFrameByte();
    if (tReceiveResultCode == JK_BMS_RECEIVE_FINISHED) {
        /*
         * All JK-BMS status frame data received
         */
        sResponseFrameBytesAreExpected = false; // Do not try to receive more response bytes
        return true;

    } else if (tReceiveResultCode != JK_BMS_RECEIVE_OK) {
        /*
         * Error here
         */
        Serial.print(F("Receive error="));
        Serial.print(tReceiveResultCode);
        Serial.print(F(" at index"));
        Serial.println(sReplyFrameBufferIndex);
        sResponseFrameBytesAreExpected = false; // Do not try to receive more response bytes
        sBMSFrameProcessingComplete = true; // In case of error, we do not call processJK_BMSStatusFrame(), so set flag here.
        printJKReplyFrameBuffer();
    }
    return false;
}

/*
 * Here we have requested frame, but serial was not available fore a longer time => timeout at receiving
 * We are called only once per timeout
 */
void handleFrameReceiveTimeout() {
    Serial.print(F("Receive timeout"));

    if (sReplyFrameBufferIndex != 0) {
        // Print bytes received so far
        Serial.print(F(" at ReplyFrameBufferIndex="));
        Serial.print(sReplyFrameBufferIndex);
        if (sReplyFrameBufferIndex != 0) {
            printJKReplyFrameBuffer();
        }
    }
    Serial.println();

    modifyAllCanDataToInactive();

#if defined(USE_SERIAL_2004_LCD)
    /*
     * Switch on LCD and print timeout message once in case of timeout detection
     */
    if (sSerialLCDAvailable) {
#  if !defined(DISPLAY_ALWAYS_ON)
        if (checkAndTurnLCDOn()) {
            Serial.println(F("BMS communication timeout")); // Reason for switching on LCD display
        }
#  endif
        printTimeoutMessageOnLCD();
    }
#endif
}

void processReceivedData() {
    /*
     * Set the static pointer to the start of the reply data which depends on the number of cell voltage entries
     * The JKFrameAllDataStruct starts behind the header + cell data header 0x79 + CellInfoSize + the variable length cell data 3 bytes per cell, (CellInfoSize is contained in JKReplyFrameBuffer[12])
     */
    sJKFAllReplyPointer = reinterpret_cast<JKReplyStruct*>(&JKReplyFrameBuffer[JK_BMS_FRAME_HEADER_LENGTH + 2
            + JKReplyFrameBuffer[JK_BMS_FRAME_INDEX_OF_CELL_INFO_LENGTH]]);

    fillJKConvertedCellInfo();
    fillJKComputedData();

    computeUpTimeString();
    detectAndPrintAlarmInfo(); // UpTimeString is used here

    fillAllCANData(sJKFAllReplyPointer);
    sCANDataIsInitialized = true; // One time flag
}

void printReceivedData() {
    if (!sStaticInfoWasSent) {
        // Send static info only once after reset
        sStaticInfoWasSent = true;
        printJKStaticInfo();
    }
#if defined(USE_SERIAL_2004_LCD)
    if (sLCDDisplayPageNumber != JK_BMS_PAGE_CAPACITY_INFO) {
        printJKDynamicInfo(); // do not interfere with plotter output
    }
    printBMSDataOnLCD();
#else
    printJKDynamicInfo();
#endif
}

/*
 * Callback handlers for button press
 * Just set flags for evaluation in checkButtonPress(), otherwise readButtonState() may again be false when checkButtonPress() is called
 */
void handlePageButtonPress(bool aButtonToggleState __attribute__((unused))) {
    sPageButtonJustPressed = true;
}

/*
 * Synchronously handle button press (called from loop)
 */
void checkButtonPress() {
#if defined(USE_SERIAL_2004_LCD)
    checkButtonPressForLCD();
#else
    /*
     * Treat Page button as Debug button
     */
    if (sPageButtonJustPressed) {
        sPageButtonJustPressed = false;
        sDebugModeActivated = true; // Is set to false in loop
        Serial.println(F("One time debug print just activated"));
    } else if (PageSwitchButtonAtPin2.readDebouncedButtonState()) {
        // Button is still pressed
        sDebugModeActivated = true; // Is set to false in loop
    }
#endif // defined(USE_SERIAL_2004_LCD)
}

#if defined(STANDALONE_TEST)
void doStandaloneTest() {

#  if defined(LCD_PAGES_TEST)
    if (sSerialLCDAvailable) {
        testLCDPages();
        delay(LCD_MESSAGE_PERSIST_TIME_MILLIS);
#    if defined(BIG_NUMBER_TEST)
        testBigNumbers();
#    endif
        memcpy_P(JKReplyFrameBuffer, TestJKReplyStatusFrame, sizeof(TestJKReplyStatusFrame));
        processReceivedData(); // to clear every changes
    }
#  endif
}

#endif

void handleOvervoltage() {
#if defined(USE_SERIAL_2004_LCD)
    if (sSerialLCDAvailable) {
#  if !defined(DISPLAY_ALWAYS_ON)
        if (sSerialLCDIsSwitchedOff) {
            myLCD.backlight();
            sSerialLCDIsSwitchedOff = false;
        }
#  endif
        myLCD.clear();
        myLCD.setCursor(0, 0);
        myLCD.print(F("VCC overvoltage"));
        myLCD.setCursor(0, 1);
        myLCD.print(F("VCC > 5.25 V"));
    }
#endif
// Do it as long as overvoltage happens
    tone(BUZZER_PIN, 1100, 300);
    delay(300);
    tone(BUZZER_PIN, 2200, 1000);
    delay(1000);
}

#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.
 * Raw reading of 1.1 V is 225 at 5 V.
 * Raw reading of 1.1 V is 221 at 5.1 V.
 * Raw reading of 1.1 V is 214 at 5.25 V (+5 %).
 * Raw reading of 1.1 V is 204 at 5.5 V (+10 %).
 * @return true if overvoltage reached
 */
bool isVCCTooHighSimple() {
    ADMUX = 14 | (DEFAULT << 6);
// ADCSRB = 0; // Only active if ADATE is set to 1.
// ADSC-StartConversion ADIF-Reset Interrupt Flag - NOT free running mode
    ADCSRA = (_BV(ADEN) | _BV(ADSC) | _BV(ADIF) | 7); //  7 -> 104 microseconds per ADC conversion at 16 MHz --- Arduino default
// wait for single conversion to finish
    loop_until_bit_is_clear(ADCSRA, ADSC);

// Get value
    uint16_t tRawValue = ADCL | (ADCH << 8);

    return tRawValue < 214;
}
#endif // _ADC_UTILS_HPP
Page button
Optional serial 2004 LCD
BMS communication
CAN communication
ESR Voltage compensation
off
on