mirror of
https://github.com/dalathegreat/Battery-Emulator.git
synced 2025-10-03 17:59:27 +02:00
Merge branch 'main' into feature/advanced-battery-webpage
This commit is contained in:
commit
742f51c658
92 changed files with 5121 additions and 1402 deletions
28
.github/ISSUE_TEMPLATE.md
vendored
Normal file
28
.github/ISSUE_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
### Expected Behavior
|
||||
|
||||
<!-- Write what should happen -->
|
||||
|
||||
### Actual Behavior
|
||||
|
||||
<!-- Write what DID happen. Try to include serial logs, if possible. -->
|
||||
|
||||
### Steps to Reproduce the Problem
|
||||
|
||||
<!-- Write some steps of how to reproduce the problem. -->
|
||||
|
||||
1.
|
||||
1.
|
||||
1.
|
||||
|
||||
### Settings
|
||||
|
||||
<!-- Please fill in the settings used below from USER_SETTINGS.h, as it will help with diagnosis. -->
|
||||
|
||||
- Software version: ``
|
||||
- Battery used: ``
|
||||
- Inverter communication protocol: ``
|
||||
- Hardware used for Battery-Emulator: `HW_LILYGO, HW_STARK, Custom`
|
||||
- CONTACTOR_CONTROL: `yes/no`
|
||||
- DUAL_CAN: `yes/no`
|
||||
- WEBSERVER: `yes/no`
|
||||
- MQTT: `yes/no`
|
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
### What
|
||||
This PR implements ...
|
||||
|
||||
### Why
|
||||
Why does it do it?
|
||||
|
||||
### How
|
||||
How does it do it?
|
4
.github/workflows/compile-all-batteries.yml
vendored
4
.github/workflows/compile-all-batteries.yml
vendored
|
@ -42,10 +42,11 @@ jobs:
|
|||
- KIA_HYUNDAI_HYBRID_BATTERY
|
||||
- NISSAN_LEAF_BATTERY
|
||||
- PYLON_BATTERY
|
||||
- RJXZS_BMS
|
||||
- RENAULT_KANGOO_BATTERY
|
||||
- RENAULT_ZOE_GEN1_BATTERY
|
||||
- RENAULT_ZOE_GEN2_BATTERY
|
||||
- TESLA_MODEL_3_BATTERY
|
||||
- TESLA_MODEL_3Y_BATTERY
|
||||
- VOLVO_SPA_BATTERY
|
||||
- TEST_FAKE_BATTERY
|
||||
- SERIAL_LINK_RECEIVER
|
||||
|
@ -53,7 +54,6 @@ jobs:
|
|||
inverter:
|
||||
- BYD_CAN
|
||||
# - BYD_MODBUS
|
||||
# - LUNA2000_MODBUS
|
||||
# - PYLON_CAN
|
||||
# - SMA_CAN
|
||||
# - SMA_TRIPOWER_CAN
|
||||
|
|
|
@ -44,10 +44,11 @@ jobs:
|
|||
- KIA_HYUNDAI_HYBRID_BATTERY
|
||||
- NISSAN_LEAF_BATTERY
|
||||
- PYLON_BATTERY
|
||||
- RJXZS_BMS
|
||||
- RENAULT_KANGOO_BATTERY
|
||||
- RENAULT_ZOE_GEN1_BATTERY
|
||||
- RENAULT_ZOE_GEN2_BATTERY
|
||||
- TESLA_MODEL_3_BATTERY
|
||||
- TESLA_MODEL_3Y_BATTERY
|
||||
- VOLVO_SPA_BATTERY
|
||||
- TEST_FAKE_BATTERY
|
||||
# These are the emulated inverter communication protocols for which the code will be compiled.
|
||||
|
@ -55,7 +56,7 @@ jobs:
|
|||
- BYD_CAN
|
||||
- BYD_SMA
|
||||
- BYD_MODBUS
|
||||
- LUNA2000_MODBUS
|
||||
- FOXESS_CAN
|
||||
- PYLON_CAN
|
||||
- SMA_CAN
|
||||
- SMA_TRIPOWER_CAN
|
||||
|
|
4
.github/workflows/compile-all-inverters.yml
vendored
4
.github/workflows/compile-all-inverters.yml
vendored
|
@ -39,13 +39,13 @@ jobs:
|
|||
# - KIA_HYUNDAI_64_BATTERY
|
||||
- NISSAN_LEAF_BATTERY
|
||||
# - RENAULT_ZOE_BATTERY
|
||||
# - TESLA_MODEL_3_BATTERY
|
||||
# - TESLA_MODEL_3Y_BATTERY
|
||||
# These are the emulated inverter communication protocols for which the code will be compiled.
|
||||
inverter:
|
||||
- BYD_CAN
|
||||
- BYD_SMA
|
||||
- BYD_MODBUS
|
||||
- LUNA2000_MODBUS
|
||||
- FOXESS_CAN
|
||||
- PYLON_CAN
|
||||
- SMA_CAN
|
||||
- SMA_TRIPOWER_CAN
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -19,3 +19,6 @@ logs
|
|||
# Ignore upload command (removes the need to re-compile after Verify when running Download, in e.g. VS Code)
|
||||
upload.bat
|
||||
compile.bat
|
||||
|
||||
# Ignore binary files
|
||||
*.bin
|
||||
|
|
15
README.md
15
README.md
|
@ -40,19 +40,21 @@ For more examples showing wiring, see each battery types own Wiki page. For inst
|
|||
|
||||
## How to compile the software 💻
|
||||
1. Download the Arduino IDE: https://www.arduino.cc/en/software
|
||||
2. When the Arduino IDE has been started;
|
||||
Click "File" in the upper left corner -> Preferences -> Additional Development >Board Manager URL -> Enter the URL in the input box https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
|
||||
3. Go to "Boards Manager", and install the ESP32 package by Espressif Systems. **NOTE: The version depends on which release of Battery-Emulator you are running!**
|
||||
2. Open the Arduino IDE.
|
||||
3. Click `File` menu -> `Preferences` -> `Additional Development` -> `Additional Board Manager URLs` -> Enter the URL in the input box: `https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json` and click OK.
|
||||
4. Click `Tools` menu -> `Board: "...."` -> `Boards Manager...`, install the `esp32` package by `Espressif Systems` (not `Arduino ESP32 Boards`), then press `Close`.
|
||||
|
||||
**NOTE: The version depends on which release of Battery-Emulator you are running!**
|
||||
|
||||
- ⚠️ Make sure to use a 2.x.x version if you are on a release **older** than 6.0.0 (For instance ESP32 v2.0.11 when using Battery-Emulator v5.4.0)
|
||||
- ⚠️ Make sure to use a 3.x.x version if you are on a release **newer** than 6.0.0 (For instance ESP32 v3.0.0 when using Battery-Emulator v6.0.0)
|
||||
|
||||

|
||||
|
||||
4. The arduino settings should be set to "ESP32 Dev Module" with the following settings;
|
||||
5. The Arduino board should be set to `ESP32 Dev Module` (under `Tools` -> `Board` -> `ESP32 Arduino`) with the following settings:
|
||||

|
||||
5. Select which battery type you will use, along with other optional settings. This is done in the USER_SETTINGS.h file.
|
||||
6. Press Verify and Upload to send the sketch to the board.
|
||||
6. Select which battery type you will use, along with other optional settings. This is done in the `USER_SETTINGS.h` file.
|
||||
7. Press `Verify` and `Upload` to send the sketch to the board.
|
||||
NOTE: In some cases, the LilyGo must be powered through the main power connector instead of USB-C
|
||||
when performing the initial firsmware upload.
|
||||
NOTE: On Mac, the following USB driver may need to be installed: https://github.com/WCHSoftGroup/ch34xser_macos
|
||||
|
@ -84,6 +86,7 @@ It is also based on the information found in the following excellent repositorie
|
|||
- https://github.com/dalathegreat/leaf_can_bus_messages
|
||||
- https://github.com/rand12345/solax_can_bus
|
||||
- https://github.com/Tom-evnut/BMWI3BMS/ SMA-CAN
|
||||
- https://github.com/FozzieUK/FoxESS-Canbus-Protocol FoxESS-CAN
|
||||
- https://github.com/maciek16c/hyundai-santa-fe-phev-battery
|
||||
- https://github.com/ljames28/Renault-Zoe-PH2-ZE50-Canbus-LBC-Information
|
||||
- Renault Zoe CAN Matrix https://docs.google.com/spreadsheets/u/0/d/1Qnk-yzzcPiMArO-QDzO4a8ptAS2Sa4HhVu441zBzlpM/edit?pli=1#gid=0
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "src/charger/CHARGERS.h"
|
||||
#include "src/datalayer/datalayer.h"
|
||||
#include "src/devboard/utils/events.h"
|
||||
#include "src/devboard/utils/led_handler.h"
|
||||
#include "src/devboard/utils/value_mapping.h"
|
||||
|
@ -23,8 +24,8 @@
|
|||
#include "src/lib/miwagner-ESP32-Arduino-CAN/CAN_config.h"
|
||||
#include "src/lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h"
|
||||
|
||||
#include "src/datalayer/datalayer.h"
|
||||
|
||||
#ifdef WIFI
|
||||
#include "src/devboard/wifi/wifi.h"
|
||||
#ifdef WEBSERVER
|
||||
#include "src/devboard/webserver/webserver.h"
|
||||
#ifdef MDNSRESPONDER
|
||||
|
@ -35,10 +36,24 @@
|
|||
#error WEBSERVER needs to be enabled for MDNSRESPONDER!
|
||||
#endif // MDNSRSPONDER
|
||||
#endif // WEBSERVER
|
||||
#ifdef MQTT
|
||||
#include "src/devboard/mqtt/mqtt.h"
|
||||
#endif // MQTT
|
||||
#endif // WIFI
|
||||
|
||||
#ifndef CONTACTOR_CONTROL
|
||||
#ifdef PWM_CONTACTOR_CONTROL
|
||||
#error CONTACTOR_CONTROL needs to be enabled for PWM_CONTACTOR_CONTROL
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef EQUIPMENT_STOP_BUTTON
|
||||
#include "src/devboard/utils/debounce_button.h"
|
||||
#endif
|
||||
|
||||
Preferences settings; // Store user settings
|
||||
// The current software version, shown on webserver
|
||||
const char* version_number = "7.1.dev";
|
||||
const char* version_number = "7.5.dev";
|
||||
|
||||
// Interval settings
|
||||
uint16_t intervalUpdateValues = INTERVAL_5_S; // Interval at which to update inverter values / Modbus registers
|
||||
|
@ -48,10 +63,11 @@ unsigned long previousMillisUpdateVal = 0;
|
|||
// CAN parameters
|
||||
CAN_device_t CAN_cfg; // CAN Config
|
||||
const int rx_queue_size = 10; // Receive Queue size
|
||||
volatile bool send_ok = 0;
|
||||
|
||||
#ifdef DUAL_CAN
|
||||
#include "src/lib/pierremolinaro-acan2515/ACAN2515.h"
|
||||
static const uint32_t QUARTZ_FREQUENCY = 8UL * 1000UL * 1000UL; // 8 MHz
|
||||
static const uint32_t QUARTZ_FREQUENCY = CRYSTAL_FREQUENCY_MHZ * 1000000UL; //MHZ configured in USER_SETTINGS.h
|
||||
ACAN2515 can(MCP2515_CS, SPI, MCP2515_INT);
|
||||
static ACAN2515_Buffer16 gBuffer;
|
||||
#endif
|
||||
|
@ -94,6 +110,8 @@ MyTimer connectivity_task_timer_10s(INTERVAL_10_S);
|
|||
|
||||
MyTimer loop_task_timer_10s(INTERVAL_10_S);
|
||||
|
||||
MyTimer check_pause_2s(INTERVAL_2_S);
|
||||
|
||||
// Contactor parameters
|
||||
#ifdef CONTACTOR_CONTROL
|
||||
enum State { DISCONNECTED, PRECHARGE, NEGATIVE, POSITIVE, PRECHARGE_OFF, COMPLETED, SHUTDOWN_REQUESTED };
|
||||
|
@ -110,6 +128,8 @@ State contactorStatus = DISCONNECTED;
|
|||
#define PWM_Freq 20000 // 20 kHz frequency, beyond audible range
|
||||
#define PWM_Res 10 // 10 Bit resolution 0 to 1023, maps 'nicely' to 0% 100%
|
||||
#define PWM_Hold_Duty 250
|
||||
#define PWM_Off_Duty 0
|
||||
#define PWM_On_Duty 1023
|
||||
#define POSITIVE_PWM_Ch 0
|
||||
#define NEGATIVE_PWM_Ch 1
|
||||
#endif
|
||||
|
@ -118,6 +138,14 @@ unsigned long negativeStartTime = 0;
|
|||
unsigned long timeSpentInFaultedMode = 0;
|
||||
#endif
|
||||
|
||||
#ifdef EQUIPMENT_STOP_BUTTON
|
||||
const unsigned long equipment_button_long_press_duration =
|
||||
15000; // 15 seconds for long press in case of MOMENTARY_SWITCH
|
||||
const unsigned long equipment_button_debounce_duration = 200; // 250ms for debouncing the button
|
||||
unsigned long timeSincePress = 0; // Variable to store the time since the last press
|
||||
DebouncedButton equipment_stop_button; // Debounced button object
|
||||
#endif
|
||||
|
||||
TaskHandle_t main_loop_task;
|
||||
TaskHandle_t connectivity_loop_task;
|
||||
|
||||
|
@ -127,7 +155,7 @@ void setup() {
|
|||
|
||||
init_stored_settings();
|
||||
|
||||
#ifdef WEBSERVER
|
||||
#ifdef WIFI
|
||||
xTaskCreatePinnedToCore((TaskFunction_t)&connectivity_loop, "connectivity_loop", 4096, &connectivity_task_time_us,
|
||||
TASK_CONNECTIVITY_PRIO, &connectivity_loop_task, WIFI_CORE);
|
||||
#endif
|
||||
|
@ -146,6 +174,9 @@ void setup() {
|
|||
|
||||
init_battery();
|
||||
|
||||
#ifdef EQUIPMENT_STOP_BUTTON
|
||||
init_equipment_stop_button();
|
||||
#endif
|
||||
// BOOT button at runtime is used as an input for various things
|
||||
pinMode(0, INPUT_PULLUP);
|
||||
|
||||
|
@ -169,10 +200,16 @@ void loop() {
|
|||
#endif
|
||||
}
|
||||
|
||||
#ifdef WEBSERVER
|
||||
#ifdef WIFI
|
||||
void connectivity_loop(void* task_time_us) {
|
||||
// Init
|
||||
|
||||
// Init wifi
|
||||
init_WiFi();
|
||||
|
||||
#ifdef WEBSERVER
|
||||
// Init webserver
|
||||
init_webserver();
|
||||
#endif
|
||||
#ifdef MDNSRESPONDER
|
||||
init_mDNS();
|
||||
#endif
|
||||
|
@ -183,6 +220,9 @@ void connectivity_loop(void* task_time_us) {
|
|||
while (true) {
|
||||
START_TIME_MEASUREMENT(wifi);
|
||||
wifi_monitor();
|
||||
#ifdef WEBSERVER
|
||||
ota_monitor();
|
||||
#endif
|
||||
END_TIME_MEASUREMENT_MAX(wifi, datalayer.system.status.wifi_task_10s_max_us);
|
||||
#ifdef MQTT
|
||||
START_TIME_MEASUREMENT(mqtt);
|
||||
|
@ -209,6 +249,10 @@ void core_loop(void* task_time_us) {
|
|||
while (true) {
|
||||
START_TIME_MEASUREMENT(all);
|
||||
START_TIME_MEASUREMENT(comm);
|
||||
#ifdef EQUIPMENT_STOP_BUTTON
|
||||
monitor_equipment_stop_button();
|
||||
#endif
|
||||
|
||||
// Input, Runs as fast as possible
|
||||
receive_can_native(); // Receive CAN messages from native CAN port
|
||||
#ifdef CAN_FD
|
||||
|
@ -287,32 +331,16 @@ void core_loop(void* task_time_us) {
|
|||
datalayer.system.status.time_cantx_us = 0;
|
||||
datalayer.system.status.core_task_10s_max_us = 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
if (check_pause_2s.elapsed()) {
|
||||
emulator_pause_state_send_CAN_battery();
|
||||
}
|
||||
|
||||
vTaskDelayUntil(&xLastWakeTime, xFrequency);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef MDNSRESPONDER
|
||||
// Initialise mDNS
|
||||
void init_mDNS() {
|
||||
|
||||
// Calulate the host name using the last two chars from the MAC address so each one is likely unique on a network.
|
||||
// e.g batteryemulator8C.local where the mac address is 08:F9:E0:D1:06:8C
|
||||
String mac = WiFi.macAddress();
|
||||
String mdnsHost = "batteryemulator" + mac.substring(mac.length() - 2);
|
||||
|
||||
// Initialize mDNS .local resolution
|
||||
if (!MDNS.begin(mdnsHost)) {
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Error setting up MDNS responder!");
|
||||
#endif
|
||||
} else {
|
||||
// Advertise via bonjour the service so we can auto discover these battery emulators on the local network.
|
||||
MDNS.addService("battery_emulator", "tcp", 80);
|
||||
}
|
||||
}
|
||||
#endif // MDNSRESPONDER
|
||||
|
||||
// Initialization functions
|
||||
void init_serial() {
|
||||
// Init Serial monitor
|
||||
|
@ -324,12 +352,25 @@ void init_serial() {
|
|||
}
|
||||
|
||||
void init_stored_settings() {
|
||||
static uint32_t temp = 0;
|
||||
settings.begin("batterySettings", false);
|
||||
|
||||
// Always get the equipment stop status
|
||||
datalayer.system.settings.equipment_stop_active = settings.getBool("EQUIPMENT_STOP", false);
|
||||
if (datalayer.system.settings.equipment_stop_active) {
|
||||
set_event(EVENT_EQUIPMENT_STOP, 1);
|
||||
}
|
||||
|
||||
#ifndef LOAD_SAVED_SETTINGS_ON_BOOT
|
||||
settings.clear(); // If this clear function is executed, no settings will be read from storage
|
||||
|
||||
//always save the equipment stop status
|
||||
settings.putBool("EQUIPMENT_STOP", datalayer.system.settings.equipment_stop_active);
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef WIFI
|
||||
|
||||
char tempSSIDstring[63]; // Allocate buffer with sufficient size
|
||||
size_t lengthSSID = settings.getString("SSID", tempSSIDstring, sizeof(tempSSIDstring));
|
||||
if (lengthSSID > 0) { // Successfully read the string from memory. Set it to SSID!
|
||||
|
@ -342,8 +383,8 @@ void init_stored_settings() {
|
|||
password = tempPasswordString;
|
||||
} else { // Reading from settings failed. Do nothing with SSID. Raise event?
|
||||
}
|
||||
#endif
|
||||
|
||||
static uint32_t temp = 0;
|
||||
temp = settings.getUInt("BATTERY_WH_MAX", false);
|
||||
if (temp != 0) {
|
||||
datalayer.battery.info.total_capacity_Wh = temp;
|
||||
|
@ -377,6 +418,9 @@ void init_CAN() {
|
|||
digitalWrite(CAN_SE_PIN, LOW);
|
||||
#endif
|
||||
CAN_cfg.speed = CAN_SPEED_500KBPS;
|
||||
#ifdef NATIVECAN_250KBPS // Some component is requesting lower CAN speed
|
||||
CAN_cfg.speed = CAN_SPEED_250KBPS;
|
||||
#endif
|
||||
CAN_cfg.tx_pin_id = CAN_TX_PIN;
|
||||
CAN_cfg.rx_pin_id = CAN_RX_PIN;
|
||||
CAN_cfg.rx_queue = xQueueCreate(rx_queue_size, sizeof(CAN_frame_t));
|
||||
|
@ -391,7 +435,18 @@ void init_CAN() {
|
|||
SPI.begin(MCP2515_SCK, MCP2515_MISO, MCP2515_MOSI);
|
||||
ACAN2515Settings settings(QUARTZ_FREQUENCY, 500UL * 1000UL); // CAN bit rate 500 kb/s
|
||||
settings.mRequestedMode = ACAN2515Settings::NormalMode;
|
||||
can.begin(settings, [] { can.isr(); });
|
||||
const uint16_t errorCodeMCP = can.begin(settings, [] { can.isr(); });
|
||||
if (errorCodeMCP == 0) {
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Can ok");
|
||||
#endif
|
||||
} else {
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.print("Error Can: 0x");
|
||||
Serial.println(errorCodeMCP, HEX);
|
||||
#endif
|
||||
set_event(EVENT_CANMCP_INIT_FAILURE, (uint8_t)errorCodeMCP);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CAN_FD
|
||||
|
@ -440,23 +495,24 @@ void init_CAN() {
|
|||
void init_contactors() {
|
||||
// Init contactor pins
|
||||
#ifdef CONTACTOR_CONTROL
|
||||
#ifndef PWM_CONTACTOR_CONTROL
|
||||
pinMode(POSITIVE_CONTACTOR_PIN, OUTPUT);
|
||||
digitalWrite(POSITIVE_CONTACTOR_PIN, LOW);
|
||||
pinMode(NEGATIVE_CONTACTOR_PIN, OUTPUT);
|
||||
digitalWrite(NEGATIVE_CONTACTOR_PIN, LOW);
|
||||
#ifdef PWM_CONTACTOR_CONTROL
|
||||
#else
|
||||
ledcAttachChannel(POSITIVE_CONTACTOR_PIN, PWM_Freq, PWM_Res,
|
||||
POSITIVE_PWM_Ch); // Setup PWM Channel Frequency and Resolution
|
||||
ledcAttachChannel(NEGATIVE_CONTACTOR_PIN, PWM_Freq, PWM_Res,
|
||||
NEGATIVE_PWM_Ch); // Setup PWM Channel Frequency and Resolution
|
||||
ledcWrite(POSITIVE_PWM_Ch, 0); // Set Positive PWM to 0%
|
||||
ledcWrite(NEGATIVE_PWM_Ch, 0); // Set Negative PWM to 0%
|
||||
ledcWrite(POSITIVE_CONTACTOR_PIN, PWM_Off_Duty); // Set Positive PWM to 0%
|
||||
ledcWrite(NEGATIVE_CONTACTOR_PIN, PWM_Off_Duty); // Set Negative PWM to 0%
|
||||
#endif
|
||||
pinMode(PRECHARGE_PIN, OUTPUT);
|
||||
digitalWrite(PRECHARGE_PIN, LOW);
|
||||
#endif
|
||||
// Init BMS contactor
|
||||
#ifdef HW_STARK // TODO: Rewrite this so LilyGo can aslo handle this BMS contactor
|
||||
#ifdef HW_STARK // TODO: Rewrite this so LilyGo can also handle this BMS contactor
|
||||
pinMode(BMS_POWER, OUTPUT);
|
||||
digitalWrite(BMS_POWER, HIGH);
|
||||
#endif
|
||||
|
@ -501,6 +557,9 @@ void init_inverter() {
|
|||
datalayer.system.status.inverter_allows_contactor_closing = false; // The inverter needs to allow first
|
||||
intervalUpdateValues = 800; // This protocol also requires the values to be updated faster
|
||||
#endif
|
||||
#ifdef FOXESS_CAN
|
||||
intervalUpdateValues = 950; // This protocol also requires the values to be updated faster
|
||||
#endif
|
||||
#ifdef BYD_SMA
|
||||
datalayer.system.status.inverter_allows_contactor_closing = false; // The inverter needs to allow first
|
||||
pinMode(INVERTER_CONTACTOR_ENABLE_PIN, INPUT);
|
||||
|
@ -516,11 +575,51 @@ void init_battery() {
|
|||
#endif
|
||||
}
|
||||
|
||||
#ifdef EQUIPMENT_STOP_BUTTON
|
||||
|
||||
void monitor_equipment_stop_button() {
|
||||
|
||||
ButtonState changed_state = debounceButton(equipment_stop_button, timeSincePress);
|
||||
|
||||
if (equipment_stop_behavior == LATCHING_SWITCH) {
|
||||
if (changed_state == PRESSED) {
|
||||
// Changed to ON – initiating equipment stop.
|
||||
setBatteryPause(true, true, true);
|
||||
} else if (changed_state == RELEASED) {
|
||||
// Changed to OFF – ending equipment stop.
|
||||
setBatteryPause(false, false, false);
|
||||
}
|
||||
} else if (equipment_stop_behavior == MOMENTARY_SWITCH) {
|
||||
if (changed_state == RELEASED) { // button is released
|
||||
|
||||
if (timeSincePress < equipment_button_long_press_duration) {
|
||||
// Short press detected, trigger equipment stop
|
||||
setBatteryPause(true, true, true);
|
||||
} else {
|
||||
// Long press detected, reset equipment stop state
|
||||
setBatteryPause(false, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void init_equipment_stop_button() {
|
||||
//using external pullup resistors NC
|
||||
pinMode(EQUIPMENT_STOP_PIN, INPUT);
|
||||
// Initialize the debounced button with NC switch type and equipment_button_debounce_duration debounce time
|
||||
initDebouncedButton(equipment_stop_button, EQUIPMENT_STOP_PIN, NC, equipment_button_debounce_duration);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef CAN_FD
|
||||
// Functions
|
||||
#ifdef DEBUG_CANFD_DATA
|
||||
void print_canfd_frame(CANFDMessage rx_frame) {
|
||||
enum frameDirection { MSG_RX, MSG_TX };
|
||||
void print_canfd_frame(CANFDMessage rx_frame, frameDirection msgDir); // Needs to be declared before it is defined
|
||||
void print_canfd_frame(CANFDMessage rx_frame, frameDirection msgDir) {
|
||||
int i = 0;
|
||||
(msgDir == 0) ? Serial.print("RX ") : Serial.print("TX ");
|
||||
Serial.print(rx_frame.id, HEX);
|
||||
Serial.print(" ");
|
||||
for (i = 0; i < rx_frame.len; i++) {
|
||||
|
@ -536,7 +635,7 @@ void receive_canfd() { // This section checks if we have a complete CAN-FD mess
|
|||
if (canfd.available()) {
|
||||
canfd.receive(frame);
|
||||
#ifdef DEBUG_CANFD_DATA
|
||||
print_canfd_frame(frame);
|
||||
print_canfd_frame(frame, frameDirection(MSG_RX));
|
||||
#endif
|
||||
CAN_frame rx_frame;
|
||||
rx_frame.ID = frame.id;
|
||||
|
@ -547,6 +646,7 @@ void receive_canfd() { // This section checks if we have a complete CAN-FD mess
|
|||
}
|
||||
//message incoming, pass it on to the handler
|
||||
receive_can(&rx_frame, CAN_ADDON_FD_MCP2518);
|
||||
receive_can(&rx_frame, CANFD_NATIVE);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@ -571,6 +671,9 @@ void receive_can_native() { // This section checks if we have a complete CAN me
|
|||
}
|
||||
|
||||
void send_can() {
|
||||
if (!allowed_to_send_CAN) {
|
||||
return;
|
||||
}
|
||||
|
||||
send_can_battery();
|
||||
|
||||
|
@ -623,13 +726,11 @@ void check_interconnect_available() {
|
|||
#endif //DOUBLE_BATTERY
|
||||
|
||||
void handle_contactors() {
|
||||
|
||||
#ifdef BYD_SMA
|
||||
datalayer.system.status.inverter_allows_contactor_closing = digitalRead(INVERTER_CONTACTOR_ENABLE_PIN);
|
||||
#endif
|
||||
|
||||
#ifdef CONTACTOR_CONTROL
|
||||
|
||||
// First check if we have any active errors, incase we do, turn off the battery
|
||||
if (datalayer.battery.status.bms_status == FAULT) {
|
||||
timeSpentInFaultedMode++;
|
||||
|
@ -637,25 +738,37 @@ void handle_contactors() {
|
|||
timeSpentInFaultedMode = 0;
|
||||
}
|
||||
|
||||
if (timeSpentInFaultedMode > MAX_ALLOWED_FAULT_TICKS) {
|
||||
//handle contactor control SHUTDOWN_REQUESTED vs DISCONNECTED
|
||||
if (timeSpentInFaultedMode > MAX_ALLOWED_FAULT_TICKS ||
|
||||
(datalayer.system.settings.equipment_stop_active && contactorStatus != SHUTDOWN_REQUESTED)) {
|
||||
contactorStatus = SHUTDOWN_REQUESTED;
|
||||
}
|
||||
if (contactorStatus == SHUTDOWN_REQUESTED && !datalayer.system.settings.equipment_stop_active) {
|
||||
contactorStatus = DISCONNECTED;
|
||||
}
|
||||
if (contactorStatus == SHUTDOWN_REQUESTED) {
|
||||
digitalWrite(PRECHARGE_PIN, LOW);
|
||||
#ifndef PWM_CONTACTOR_CONTROL
|
||||
digitalWrite(NEGATIVE_CONTACTOR_PIN, LOW);
|
||||
digitalWrite(POSITIVE_CONTACTOR_PIN, LOW);
|
||||
#else
|
||||
ledcWrite(NEGATIVE_CONTACTOR_PIN, PWM_Off_Duty);
|
||||
ledcWrite(POSITIVE_CONTACTOR_PIN, PWM_Off_Duty);
|
||||
#endif
|
||||
set_event(EVENT_ERROR_OPEN_CONTACTOR, 0);
|
||||
datalayer.system.status.contactor_control_closed = false;
|
||||
return; // A fault scenario latches the contactor control. It is not possible to recover without a powercycle (and investigation why fault occured)
|
||||
}
|
||||
|
||||
// After that, check if we are OK to start turning on the battery
|
||||
if (contactorStatus == DISCONNECTED) {
|
||||
digitalWrite(PRECHARGE_PIN, LOW);
|
||||
#ifndef PWM_CONTACTOR_CONTROL
|
||||
digitalWrite(NEGATIVE_CONTACTOR_PIN, LOW);
|
||||
digitalWrite(POSITIVE_CONTACTOR_PIN, LOW);
|
||||
#ifdef PWM_CONTACTOR_CONTROL
|
||||
ledcWrite(POSITIVE_PWM_Ch, 0);
|
||||
ledcWrite(NEGATIVE_PWM_Ch, 0);
|
||||
#else
|
||||
ledcWrite(NEGATIVE_CONTACTOR_PIN, PWM_Off_Duty);
|
||||
ledcWrite(POSITIVE_CONTACTOR_PIN, PWM_Off_Duty);
|
||||
#endif
|
||||
|
||||
if (datalayer.system.status.battery_allows_contactor_closing &&
|
||||
|
@ -683,9 +796,10 @@ void handle_contactors() {
|
|||
|
||||
case NEGATIVE:
|
||||
if (currentTime - prechargeStartTime >= PRECHARGE_TIME_MS) {
|
||||
#ifndef PWM_CONTACTOR_CONTROL
|
||||
digitalWrite(NEGATIVE_CONTACTOR_PIN, HIGH);
|
||||
#ifdef PWM_CONTACTOR_CONTROL
|
||||
ledcWrite(NEGATIVE_PWM_Ch, 1023);
|
||||
#else
|
||||
ledcWrite(NEGATIVE_CONTACTOR_PIN, PWM_On_Duty);
|
||||
#endif
|
||||
negativeStartTime = currentTime;
|
||||
contactorStatus = POSITIVE;
|
||||
|
@ -694,9 +808,10 @@ void handle_contactors() {
|
|||
|
||||
case POSITIVE:
|
||||
if (currentTime - negativeStartTime >= NEGATIVE_CONTACTOR_TIME_MS) {
|
||||
#ifndef PWM_CONTACTOR_CONTROL
|
||||
digitalWrite(POSITIVE_CONTACTOR_PIN, HIGH);
|
||||
#ifdef PWM_CONTACTOR_CONTROL
|
||||
ledcWrite(POSITIVE_PWM_Ch, 1023);
|
||||
#else
|
||||
ledcWrite(POSITIVE_CONTACTOR_PIN, PWM_On_Duty);
|
||||
#endif
|
||||
contactorStatus = PRECHARGE_OFF;
|
||||
}
|
||||
|
@ -706,10 +821,11 @@ void handle_contactors() {
|
|||
if (currentTime - negativeStartTime >= POSITIVE_CONTACTOR_TIME_MS) {
|
||||
digitalWrite(PRECHARGE_PIN, LOW);
|
||||
#ifdef PWM_CONTACTOR_CONTROL
|
||||
ledcWrite(NEGATIVE_PWM_Ch, PWM_Hold_Duty);
|
||||
ledcWrite(POSITIVE_PWM_Ch, PWM_Hold_Duty);
|
||||
ledcWrite(NEGATIVE_CONTACTOR_PIN, PWM_Hold_Duty);
|
||||
ledcWrite(POSITIVE_CONTACTOR_PIN, PWM_Hold_Duty);
|
||||
#endif
|
||||
contactorStatus = COMPLETED;
|
||||
datalayer.system.status.contactor_control_closed = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
@ -801,10 +917,18 @@ void init_serialDataLink() {
|
|||
#endif
|
||||
}
|
||||
|
||||
void store_settings_equipment_stop() {
|
||||
settings.begin("batterySettings", false);
|
||||
settings.putBool("EQUIPMENT_STOP", datalayer.system.settings.equipment_stop_active);
|
||||
settings.end();
|
||||
}
|
||||
|
||||
void storeSettings() {
|
||||
settings.begin("batterySettings", false);
|
||||
#ifdef WIFI
|
||||
settings.putString("SSID", String(ssid.c_str()));
|
||||
settings.putString("PASSWORD", String(password.c_str()));
|
||||
#endif
|
||||
settings.putUInt("BATTERY_WH_MAX", datalayer.battery.info.total_capacity_Wh);
|
||||
settings.putUInt("MAXPERCENTAGE",
|
||||
datalayer.battery.settings.max_percentage / 10); // Divide by 10 for backwards compatibility
|
||||
|
@ -892,7 +1016,12 @@ void check_reset_reason() {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void transmit_can(CAN_frame* tx_frame, int interface) {
|
||||
if (!allowed_to_send_CAN) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (interface) {
|
||||
case CAN_NATIVE:
|
||||
CAN_frame_t frame;
|
||||
|
@ -931,7 +1060,14 @@ void transmit_can(CAN_frame* tx_frame, int interface) {
|
|||
for (uint8_t i = 0; i < MCP2518Frame.len; i++) {
|
||||
MCP2518Frame.data[i] = tx_frame->data.u8[i];
|
||||
}
|
||||
canfd.tryToSend(MCP2518Frame);
|
||||
send_ok = canfd.tryToSend(MCP2518Frame);
|
||||
if (!send_ok) {
|
||||
set_event(EVENT_CANFD_BUFFER_FULL, interface);
|
||||
} else {
|
||||
#ifdef DEBUG_CANFD_DATA
|
||||
print_canfd_frame(MCP2518Frame, frameDirection(MSG_TX));
|
||||
#endif
|
||||
}
|
||||
#else // Interface not compiled, and settings try to use it
|
||||
set_event(EVENT_INTERFACE_MISSING, interface);
|
||||
#endif //CAN_FD
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#include "USER_SETTINGS.h"
|
||||
#include <string>
|
||||
#include "src/devboard/hal/hal.h"
|
||||
|
||||
/* This file contains all the battery settings and limits */
|
||||
/* They can be defined here, or later on in the WebUI */
|
||||
/* Most important is to select which CAN interface each component is connected to */
|
||||
|
@ -18,19 +19,41 @@ volatile CAN_Configuration can_config = {
|
|||
.charger = CAN_NATIVE // (OPTIONAL) Which CAN is your charger connected to?
|
||||
};
|
||||
|
||||
#ifdef WEBSERVER
|
||||
#ifdef WIFI
|
||||
|
||||
volatile uint8_t AccessPointEnabled = true; //Set to either true/false to enable direct wifi access point
|
||||
std::string ssid = "REPLACE_WITH_YOUR_SSID"; // Maximum of 63 characters;
|
||||
std::string password = "REPLACE_WITH_YOUR_PASSWORD"; // Minimum of 8 characters;
|
||||
std::string ssid = "REPLACE_WITH_YOUR_SSID"; // Maximum of 63 characters
|
||||
std::string password = "REPLACE_WITH_YOUR_PASSWORD"; // Minimum of 8 characters
|
||||
const char* ssidAP = "Battery Emulator"; // Maximum of 63 characters, also used for device name on web interface
|
||||
const char* passwordAP = "123456789"; // Minimum of 8 characters; set to NULL if you want the access point to be open
|
||||
const uint8_t wifi_channel = 0; // Set to 0 for automatic channel selection
|
||||
|
||||
#ifdef WIFICONFIG
|
||||
// Set your Static IP address
|
||||
IPAddress local_IP(192, 168, 10, 150);
|
||||
// Set your Gateway IP address
|
||||
IPAddress gateway(192, 168, 10, 1);
|
||||
// Set your Subnet IP address
|
||||
IPAddress subnet(255, 255, 255, 0);
|
||||
#endif
|
||||
#ifdef WEBSERVER
|
||||
const char* http_username = "admin"; // username to webserver authentication;
|
||||
const char* http_password = "admin"; // password to webserver authentication;
|
||||
|
||||
#endif // WEBSERVER
|
||||
// MQTT
|
||||
#ifdef MQTT
|
||||
const char* mqtt_user = "REDACTED"; // Set NULL for no username
|
||||
const char* mqtt_password = "REDACTED"; // Set NULL for no password
|
||||
#endif // USE_MQTT
|
||||
#endif // WEBSERVER
|
||||
#endif // WIFI
|
||||
|
||||
#ifdef EQUIPMENT_STOP_BUTTON
|
||||
// Equipment stop button behavior. Use NC button for safety reasons.
|
||||
//LATCHING_SWITCH - Normally closed (NC), latching switch. When pressed it activates e-stop
|
||||
//MOMENTARY_SWITCH - Short press to activate e-stop, long 15s press to deactivate. E-stop is persistent between reboots
|
||||
volatile STOP_BUTTON_BEHAVIOR equipment_stop_behavior = LATCHING_SWITCH;
|
||||
#endif
|
||||
|
||||
/* Charger settings (Optional, when using generator charging) */
|
||||
volatile float CHARGER_SET_HV = 384; // Reasonably appropriate 4.0v per cell charging of a 96s pack
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#ifndef __USER_SETTINGS_H__
|
||||
#define __USER_SETTINGS_H__
|
||||
#include <WiFi.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* This file contains all the battery/inverter protocol settings Battery-Emulator software */
|
||||
|
@ -19,11 +20,13 @@
|
|||
//#define MG_5_BATTERY
|
||||
#define NISSAN_LEAF_BATTERY
|
||||
//#define PYLON_BATTERY
|
||||
//#define RJXZS_BMS
|
||||
//#define RENAULT_KANGOO_BATTERY
|
||||
//#define RENAULT_ZOE_GEN1_BATTERY
|
||||
//#define RENAULT_ZOE_GEN2_BATTERY
|
||||
//#define SANTA_FE_PHEV_BATTERY
|
||||
//#define TESLA_MODEL_3_BATTERY
|
||||
//#define TESLA_MODEL_SX_BATTERY
|
||||
//#define TESLA_MODEL_3Y_BATTERY
|
||||
//#define VOLVO_SPA_BATTERY
|
||||
//#define TEST_FAKE_BATTERY
|
||||
//#define DOUBLE_BATTERY //Enable this line if you use two identical batteries at the same time (requires DUAL_CAN setup)
|
||||
|
@ -32,7 +35,7 @@
|
|||
//#define BYD_CAN //Enable this line to emulate a "BYD Battery-Box Premium HVS" over CAN Bus
|
||||
//#define BYD_SMA //Enable this line to emulate a SMA compatible "BYD Battery-Box HVS 10.2KW battery" over CAN bus
|
||||
//#define BYD_MODBUS //Enable this line to emulate a "BYD 11kWh HVM battery" over Modbus RTU
|
||||
//#define LUNA2000_MODBUS //Enable this line to emulate a "Luna2000 battery" over Modbus RTU
|
||||
//#define FOXESS_CAN //Enable this line to emulate a "HV2600/ECS4100 battery" over CAN bus
|
||||
//#define PYLON_CAN //Enable this line to emulate a "Pylontech battery" over CAN bus
|
||||
//#define SMA_CAN //Enable this line to emulate a "BYD Battery-Box H 8.9kWh, 7 mod" over CAN bus
|
||||
//#define SMA_TRIPOWER_CAN //Enable this line to emulate a "SMA Home Storage battery" over CAN bus
|
||||
|
@ -48,17 +51,23 @@
|
|||
//#define DEBUG_CANFD_DATA //Enable this line to have the USB port output CAN-FD data while program runs (WARNING, raises CPU load, do not use for production)
|
||||
//#define INTERLOCK_REQUIRED //Nissan LEAF specific setting, if enabled requires both high voltage conenctors to be seated before starting
|
||||
//#define CONTACTOR_CONTROL //Enable this line to have pins 25,32,33 handle automatic precharge/contactor+/contactor- closing sequence
|
||||
//#define PWM_CONTACTOR_CONTROL //Enable this line to use PWM logic for contactors, which lower power consumption and heat generation
|
||||
//#define PWM_CONTACTOR_CONTROL //Enable this line to use PWM for CONTACTOR_CONTROL, which lowers power consumption and heat generation. CONTACTOR_CONTROL must be enabled.
|
||||
//#define DUAL_CAN //Enable this line to activate an isolated secondary CAN Bus using add-on MCP2515 chip (Needed for some inverters / double battery)
|
||||
#define CRYSTAL_FREQUENCY_MHZ 8 //DUAL_CAN option, what is your MCP2515 add-on boards crystal frequency?
|
||||
//#define CAN_FD //Enable this line to activate an isolated secondary CAN-FD bus using add-on MCP2518FD chip / Native CANFD on Stark board
|
||||
//#define USE_CANFD_INTERFACE_AS_CLASSIC_CAN // Enable this line if you intend to use the CANFD as normal CAN
|
||||
//#define SERIAL_LINK_RECEIVER //Enable this line to receive battery data over RS485 pins from another Lilygo (This LilyGo interfaces with inverter)
|
||||
//#define SERIAL_LINK_TRANSMITTER //Enable this line to send battery data over RS485 pins to another Lilygo (This LilyGo interfaces with battery)
|
||||
#define WIFI
|
||||
//#define WIFICONFIG //Enable this line to set a static IP address / gateway /subnet mask for the device. see USER_SETTINGS.cpp for the settings
|
||||
#define WEBSERVER //Enable this line to enable WiFi, and to run the webserver. See USER_SETTINGS.cpp for the Wifi settings.
|
||||
#define WEBSERVER_AUTH_REQUIRED \
|
||||
false //Set this line to true to activate webserver authentication (this line must not be commented). Refer to USER_SETTINGS.cpp for setting the credentials.
|
||||
#define WIFIAP //Disable this line to permanently disable WIFI AP mode (make sure to hardcode ssid and password of you home wifi network). When enabled WIFI AP can still be disabled by a setting in the future.
|
||||
#define MDNSRESPONDER //Enable this line to enable MDNS, allows battery monitor te be found by .local address. Requires WEBSERVER to be enabled.
|
||||
#define LOAD_SAVED_SETTINGS_ON_BOOT //Enable this line to read settings stored via the webserver on boot (overrides Wifi/battery settings set below)
|
||||
//#define FUNCTION_TIME_MEASUREMENT // Enable this to record execution times and present them in the web UI (WARNING, raises CPU load, do not use for production)
|
||||
//#define EQUIPMENT_STOP_BUTTON // Enable this to allow an equipment stop button connected to the Battery-Emulator to disengage the battery
|
||||
|
||||
/* MQTT options */
|
||||
// #define MQTT // Enable this line to enable MQTT
|
||||
|
@ -113,4 +122,17 @@ extern volatile float CHARGER_END_A;
|
|||
extern bool charger_HV_enabled;
|
||||
extern bool charger_aux12V_enabled;
|
||||
|
||||
#ifdef EQUIPMENT_STOP_BUTTON
|
||||
typedef enum { LATCHING_SWITCH = 0, MOMENTARY_SWITCH = 1 } STOP_BUTTON_BEHAVIOR;
|
||||
extern volatile STOP_BUTTON_BEHAVIOR equipment_stop_behavior;
|
||||
#endif
|
||||
|
||||
#ifdef WIFICONFIG
|
||||
extern IPAddress local_IP;
|
||||
// Set your Gateway IP address
|
||||
extern IPAddress gateway;
|
||||
// Set your Subnet IP address
|
||||
extern IPAddress subnet;
|
||||
#endif
|
||||
|
||||
#endif // __USER_SETTINGS_H__
|
||||
|
|
|
@ -46,6 +46,10 @@
|
|||
#include "PYLON-BATTERY.h"
|
||||
#endif
|
||||
|
||||
#ifdef RJXZS_BMS
|
||||
#include "RJXZS-BMS.h"
|
||||
#endif
|
||||
|
||||
#ifdef RENAULT_KANGOO_BATTERY
|
||||
#include "RENAULT-KANGOO-BATTERY.h"
|
||||
#endif
|
||||
|
@ -62,8 +66,9 @@
|
|||
#include "SANTA-FE-PHEV-BATTERY.h"
|
||||
#endif
|
||||
|
||||
#ifdef TESLA_MODEL_3_BATTERY
|
||||
#include "TESLA-MODEL-3-BATTERY.h"
|
||||
#if defined(TESLA_MODEL_SX_BATTERY) || defined(TESLA_MODEL_3Y_BATTERY)
|
||||
#define TESLA_BATTERY
|
||||
#include "TESLA-BATTERY.h"
|
||||
#endif
|
||||
|
||||
#ifdef TEST_FAKE_BATTERY
|
||||
|
|
|
@ -395,8 +395,37 @@ void update_values_battery2() { //This function maps all the values fetched via
|
|||
|
||||
datalayer.battery2.status.temperature_max_dC = battery2_temperature_max * 10; // Add a decimal
|
||||
|
||||
datalayer.battery2.status.cell_min_voltage_mV = datalayer.battery2.status.cell_voltages_mV[0];
|
||||
datalayer.battery2.status.cell_max_voltage_mV = datalayer.battery2.status.cell_voltages_mV[1];
|
||||
if (battery2_info_available) {
|
||||
// Start checking safeties. First up, cellvoltages!
|
||||
if (detectedBattery == BATTERY_60AH) {
|
||||
datalayer.battery2.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_60AH;
|
||||
datalayer.battery2.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_60AH;
|
||||
datalayer.battery2.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_60AH;
|
||||
datalayer.battery2.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_60AH;
|
||||
} else if (detectedBattery == BATTERY_94AH) {
|
||||
datalayer.battery2.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_94AH;
|
||||
datalayer.battery2.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_94AH;
|
||||
datalayer.battery2.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_94AH;
|
||||
datalayer.battery2.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_94AH;
|
||||
} else { // BATTERY_120AH
|
||||
datalayer.battery2.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_120AH;
|
||||
datalayer.battery2.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_120AH;
|
||||
datalayer.battery2.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_120AH;
|
||||
datalayer.battery2.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_120AH;
|
||||
}
|
||||
}
|
||||
|
||||
// Perform other safety checks
|
||||
if (battery2_status_error_locking == 2) { // HVIL seated?
|
||||
set_event(EVENT_HVIL_FAILURE, 2);
|
||||
} else {
|
||||
clear_event(EVENT_HVIL_FAILURE);
|
||||
}
|
||||
if (battery2_status_precharge_locked == 2) { // Capacitor seated?
|
||||
set_event(EVENT_PRECHARGE_FAILURE, 2);
|
||||
} else {
|
||||
clear_event(EVENT_PRECHARGE_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
void update_values_battery() { //This function maps all the values fetched via CAN to the battery datalayer
|
||||
|
@ -433,30 +462,18 @@ void update_values_battery() { //This function maps all the values fetched via
|
|||
if (detectedBattery == BATTERY_60AH) {
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_60AH;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_60AH;
|
||||
if (datalayer.battery.status.cell_max_voltage_mV >= MAX_CELL_VOLTAGE_60AH) {
|
||||
set_event(EVENT_CELL_OVER_VOLTAGE, 0);
|
||||
}
|
||||
if (datalayer.battery.status.cell_min_voltage_mV <= MIN_CELL_VOLTAGE_60AH) {
|
||||
set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
|
||||
}
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_60AH;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_60AH;
|
||||
} else if (detectedBattery == BATTERY_94AH) {
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_94AH;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_94AH;
|
||||
if (datalayer.battery.status.cell_max_voltage_mV >= MAX_CELL_VOLTAGE_94AH) {
|
||||
set_event(EVENT_CELL_OVER_VOLTAGE, 0);
|
||||
}
|
||||
if (datalayer.battery.status.cell_min_voltage_mV <= MIN_CELL_VOLTAGE_94AH) {
|
||||
set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
|
||||
}
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_94AH;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_94AH;
|
||||
} else { // BATTERY_120AH
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_120AH;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_120AH;
|
||||
if (datalayer.battery.status.cell_max_voltage_mV >= MAX_CELL_VOLTAGE_120AH) {
|
||||
set_event(EVENT_CELL_OVER_VOLTAGE, 0);
|
||||
}
|
||||
if (datalayer.battery.status.cell_min_voltage_mV <= MIN_CELL_VOLTAGE_120AH) {
|
||||
set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
|
||||
}
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_120AH;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_120AH;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1124,13 +1141,14 @@ void setup_battery(void) { // Performs one time setup at startup
|
|||
//Before we have started up and detected which battery is in use, use 60AH values
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_60AH;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_60AH;
|
||||
|
||||
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV;
|
||||
datalayer.system.status.battery_allows_contactor_closing = true;
|
||||
|
||||
#ifdef DOUBLE_BATTERY
|
||||
Serial.println("Another BMW i3 battery also selected!");
|
||||
datalayer.battery2.info.max_design_voltage_dV = datalayer.battery.info.max_design_voltage_dV;
|
||||
datalayer.battery2.info.min_design_voltage_dV = datalayer.battery.info.min_design_voltage_dV;
|
||||
datalayer.battery2.info.max_cell_voltage_deviation_mV = datalayer.battery.info.max_cell_voltage_deviation_mV;
|
||||
datalayer.battery2.status.voltage_dV =
|
||||
0; //Init voltage to 0 to allow contactor check to operate without fear of default values colliding
|
||||
#endif
|
||||
|
|
|
@ -135,11 +135,11 @@ void update_values_battery() { //This function maps all the values fetched via
|
|||
}
|
||||
|
||||
void receive_can_battery(CAN_frame rx_frame) {
|
||||
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||
switch (rx_frame.ID) { //Log values taken with 422V from battery
|
||||
case 0x244: //00,00,00,04,41,0F,20,8B - Static, values never changes between logs
|
||||
break;
|
||||
case 0x245: //01,00,02,19,3A,25,90,F4 Seems to have a mux in frame0
|
||||
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||
//02,00,90,01,79,79,90,EA // Point of interest, went from 7E,75 to 7B,7C when discharging
|
||||
//03,C6,88,12,FD,48,90,5C
|
||||
//04,00,FF,FF,00,00,90,6D
|
||||
|
@ -371,9 +371,13 @@ void setup_battery(void) { // Performs one time setup at startup
|
|||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("BYD Atto 3 battery selected");
|
||||
#endif
|
||||
|
||||
datalayer.battery.info.max_design_voltage_dV = 4410; // Over this charging is not possible
|
||||
datalayer.battery.info.min_design_voltage_dV = 3800; // Under this discharging is disabled
|
||||
datalayer.battery.info.number_of_cells = 126;
|
||||
datalayer.battery.info.chemistry = battery_chemistry_enum::LFP;
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -4,7 +4,11 @@
|
|||
#include "../include.h"
|
||||
|
||||
#define BATTERY_SELECTED
|
||||
#define MAX_PACK_VOLTAGE_DV 4410 //5000 = 500.0V
|
||||
#define MIN_PACK_VOLTAGE_DV 3800
|
||||
#define MAX_CELL_DEVIATION_MV 150
|
||||
#define MAX_CELL_VOLTAGE_MV 3800 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_MV 2800 //Battery is put into emergency stop if one cell goes below this value
|
||||
|
||||
void setup_battery(void);
|
||||
void transmit_can(CAN_frame* tx_frame, int interface);
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
#include "../include.h"
|
||||
|
||||
#define BATTERY_SELECTED
|
||||
#define MAX_CELL_DEVIATION_MV 9999
|
||||
|
||||
//Contactor control is required for CHADEMO support
|
||||
#define CONTACTOR_CONTROL
|
||||
|
|
|
@ -8,8 +8,6 @@
|
|||
//Figure out if CAN messages need to be sent to keep the system happy?
|
||||
|
||||
/* Do not change code below unless you are sure what you are doing */
|
||||
#define MAX_CELL_VOLTAGE 4150
|
||||
#define MIN_CELL_VOLTAGE 2750
|
||||
static uint8_t errorCode = 0; //stores if we have an error code active from battery control logic
|
||||
static uint8_t BMU_Detected = 0;
|
||||
static uint8_t CMU_Detected = 0;
|
||||
|
@ -89,12 +87,12 @@ void update_values_battery() { //This function maps all the values fetched via
|
|||
for (int i = 0; i < 88; ++i) {
|
||||
datalayer.battery.status.cell_voltages_mV[i] = (uint16_t)(cell_voltages[i] * 1000);
|
||||
}
|
||||
|
||||
if (max_volt_cel > 2200) { // Only update cellvoltage when we have a value
|
||||
datalayer.battery.info.number_of_cells = 88;
|
||||
if (max_volt_cel > 2.2) { // Only update cellvoltage when we have a value
|
||||
datalayer.battery.status.cell_max_voltage_mV = (uint16_t)(max_volt_cel * 1000);
|
||||
}
|
||||
|
||||
if (min_volt_cel > 2200) { // Only update cellvoltage when we have a value
|
||||
if (min_volt_cel > 2.2) { // Only update cellvoltage when we have a value
|
||||
datalayer.battery.status.cell_min_voltage_mV = (uint16_t)(min_volt_cel * 1000);
|
||||
}
|
||||
|
||||
|
@ -106,14 +104,6 @@ void update_values_battery() { //This function maps all the values fetched via
|
|||
datalayer.battery.status.temperature_max_dC = (int16_t)(max_temp_cel * 10);
|
||||
}
|
||||
|
||||
//Check safeties
|
||||
if (datalayer.battery.status.cell_max_voltage_mV >= MAX_CELL_VOLTAGE) {
|
||||
set_event(EVENT_CELL_OVER_VOLTAGE, datalayer.battery.status.cell_max_voltage_mV);
|
||||
}
|
||||
if (datalayer.battery.status.cell_min_voltage_mV <= MIN_CELL_VOLTAGE) {
|
||||
set_event(EVENT_CELL_UNDER_VOLTAGE, datalayer.battery.status.cell_min_voltage_mV);
|
||||
}
|
||||
|
||||
if (!BMU_Detected) {
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("BMU not detected, check wiring!");
|
||||
|
@ -239,9 +229,11 @@ void setup_battery(void) { // Performs one time setup at startup
|
|||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Mitsubishi i-MiEV / Citroen C-Zero / Peugeot Ion battery selected");
|
||||
#endif
|
||||
|
||||
datalayer.battery.info.max_design_voltage_dV = 3696; // 369.6V
|
||||
datalayer.battery.info.min_design_voltage_dV = 3160; // 316.0V
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -4,7 +4,11 @@
|
|||
#include "../include.h"
|
||||
|
||||
#define BATTERY_SELECTED
|
||||
#define MAX_PACK_VOLTAGE_DV 3696 //5000 = 500.0V
|
||||
#define MIN_PACK_VOLTAGE_DV 3160
|
||||
#define MAX_CELL_DEVIATION_MV 500
|
||||
#define MAX_CELL_VOLTAGE_MV 4150 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_MV 2750 //Battery is put into emergency stop if one cell goes below this value
|
||||
|
||||
void setup_battery(void);
|
||||
void transmit_can(CAN_frame* tx_frame, int interface);
|
||||
|
|
|
@ -5,9 +5,7 @@
|
|||
#include "JAGUAR-IPACE-BATTERY.h"
|
||||
|
||||
/* Do not change code below unless you are sure what you are doing */
|
||||
static unsigned long previousMillis10 = 0; // will store last time a 10ms CAN Message was send
|
||||
static unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send
|
||||
static unsigned long previousMillis500 = 0; // will store last time a 500ms CAN Message was send
|
||||
static unsigned long previousMillisKeepAlive = 0;
|
||||
|
||||
static uint8_t HVBattAvgSOC = 0;
|
||||
static uint8_t HVBattFastChgCounter = 0;
|
||||
|
@ -41,17 +39,22 @@ static bool HVBattHVILError = false;
|
|||
static bool HVILBattIsolationError = false;
|
||||
static bool HVIsolationTestStatus = false;
|
||||
|
||||
/* TODO: Actually use a proper keepalive message */
|
||||
/* KeepAlive: PMZ_CAN_GWM_OSEK_NM_Pdu every 200ms.
|
||||
*/
|
||||
CAN_frame ipace_keep_alive = {.FD = false,
|
||||
.ext_ID = false,
|
||||
.DLC = 8,
|
||||
.ID = 0x063,
|
||||
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
|
||||
CAN_frame ipace_7e4 = {.FD = false,
|
||||
.ID = 0x51e,
|
||||
.data = {0x22, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
|
||||
|
||||
/* KeepAlive: PMZ_CAN_NodeGWM_NM every 1s.
|
||||
* TODO: This may be needed for >2021 models.
|
||||
*/
|
||||
/*CAN_frame ipace_keep_alive = {.FD = false,
|
||||
.ext_ID = false,
|
||||
.DLC = 8,
|
||||
.ID = 0x7e4,
|
||||
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
|
||||
.ID = 0x59e,
|
||||
.data = {0x9E, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}};*/
|
||||
|
||||
void print_units(char* header, int value, char* units) {
|
||||
Serial.print(header);
|
||||
|
@ -243,58 +246,14 @@ void receive_can_battery(CAN_frame rx_frame) {
|
|||
Serial.println("");
|
||||
}
|
||||
|
||||
int state = 0;
|
||||
|
||||
void send_can_battery() {
|
||||
unsigned long currentMillis = millis();
|
||||
// Send 100ms CAN Message
|
||||
if (currentMillis - previousMillis100 >= INTERVAL_100_MS) {
|
||||
previousMillis100 = currentMillis;
|
||||
|
||||
//transmit_can(&ipace_keep_alive);
|
||||
}
|
||||
|
||||
// Send 500ms CAN Message
|
||||
if (currentMillis - previousMillis500 >= INTERVAL_500_MS) {
|
||||
previousMillis500 = currentMillis;
|
||||
|
||||
CAN_frame msg;
|
||||
int err;
|
||||
|
||||
switch (state) {
|
||||
case 0:
|
||||
|
||||
// car response: 7ec 07 59 02 8f c0 64 88 28
|
||||
// response: 7EC 07 59 02 8F F0 01 00 28
|
||||
// response: 7EC 03 59 02 8F 00 00 00 00
|
||||
// 7EC 8 3 7F 19 11 0 0 0 0
|
||||
msg = {.FD = false,
|
||||
.ext_ID = false,
|
||||
.DLC = 8,
|
||||
.ID = 0x7e4,
|
||||
.data = {0x03, 0x19, 0x02, 0x8f, 0x00, 0x00, 0x00, 0x00}};
|
||||
transmit_can(&msg, can_config.battery);
|
||||
state++;
|
||||
|
||||
break;
|
||||
case 1:
|
||||
// car response: 7EC 11 fa 59 04 c0 64 88 28
|
||||
// response:
|
||||
msg = {.FD = false,
|
||||
.ext_ID = false,
|
||||
.DLC = 8,
|
||||
.ID = 0x7e4,
|
||||
.data = {0x06, 0x19, 0x04, 0xc0, 0x64, 0x88, 0xff, 0x00}};
|
||||
transmit_can(&msg, can_config.battery);
|
||||
state++;
|
||||
break;
|
||||
case 2:
|
||||
/* reset */
|
||||
state = 0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
/* Send keep-alive every 200ms */
|
||||
if (currentMillis - previousMillisKeepAlive >= INTERVAL_200_MS) {
|
||||
previousMillisKeepAlive = currentMillis;
|
||||
transmit_can(&ipace_keep_alive, can_config.battery);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -304,8 +263,13 @@ void setup_battery(void) { // Performs one time setup at startup
|
|||
#endif
|
||||
|
||||
datalayer.battery.info.number_of_cells = 108;
|
||||
datalayer.battery.info.max_design_voltage_dV = 4546;
|
||||
datalayer.battery.info.min_design_voltage_dV = 3370;
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV;
|
||||
|
||||
datalayer.system.status.battery_allows_contactor_closing = true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -3,7 +3,11 @@
|
|||
#include "../include.h"
|
||||
|
||||
#define BATTERY_SELECTED
|
||||
#define MAX_CELL_DEVIATION_MV 9999 // TODO is this ok ?
|
||||
#define MAX_PACK_VOLTAGE_DV 4546 //5000 = 500.0V
|
||||
#define MIN_PACK_VOLTAGE_DV 3370
|
||||
#define MAX_CELL_DEVIATION_MV 500
|
||||
#define MAX_CELL_VOLTAGE_MV 4250 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value
|
||||
|
||||
void setup_battery(void);
|
||||
void transmit_can(CAN_frame* tx_frame, int interface);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -7,7 +7,11 @@
|
|||
extern ACAN2517FD canfd;
|
||||
|
||||
#define BATTERY_SELECTED
|
||||
#define MAX_PACK_VOLTAGE_DV 8064 //5000 = 500.0V
|
||||
#define MIN_PACK_VOLTAGE_DV 4320
|
||||
#define MAX_CELL_DEVIATION_MV 150
|
||||
#define MAX_CELL_VOLTAGE_MV 4250 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_MV 2950 //Battery is put into emergency stop if one cell goes below this value
|
||||
#define MAXCHARGEPOWERALLOWED 10000
|
||||
#define MAXDISCHARGEPOWERALLOWED 10000
|
||||
|
||||
|
|
|
@ -8,9 +8,6 @@
|
|||
static unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send
|
||||
static unsigned long previousMillis10 = 0; // will store last time a 10s CAN Message was send
|
||||
|
||||
#define MAX_CELL_VOLTAGE 4250 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE 2950 //Battery is put into emergency stop if one cell goes below this value
|
||||
|
||||
static uint16_t soc_calculated = 0;
|
||||
static uint16_t SOC_BMS = 0;
|
||||
static uint16_t SOC_Display = 0;
|
||||
|
@ -147,14 +144,6 @@ void update_values_battery() { //This function maps all the values fetched via
|
|||
set_event(EVENT_12V_LOW, leadAcidBatteryVoltage);
|
||||
}
|
||||
|
||||
// Check if cell voltages are within allowed range
|
||||
if (CellVoltMax_mV >= MAX_CELL_VOLTAGE) {
|
||||
set_event(EVENT_CELL_OVER_VOLTAGE, 0);
|
||||
}
|
||||
if (CellVoltMin_mV <= MIN_CELL_VOLTAGE) {
|
||||
set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
|
||||
}
|
||||
|
||||
/* Safeties verified. Perform USB serial printout if configured to do so */
|
||||
|
||||
#ifdef DEBUG_VIA_USB
|
||||
|
@ -223,12 +212,12 @@ void update_number_of_cells() {
|
|||
// Check if we have 98S or 90S battery
|
||||
if (datalayer.battery.status.cell_voltages_mV[97] > 0) {
|
||||
datalayer.battery.info.number_of_cells = 98;
|
||||
datalayer.battery.info.max_design_voltage_dV = 4040;
|
||||
datalayer.battery.info.min_design_voltage_dV = 3100;
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_98S_DV;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_98S_DV;
|
||||
} else {
|
||||
datalayer.battery.info.number_of_cells = 90;
|
||||
datalayer.battery.info.max_design_voltage_dV = 3870;
|
||||
datalayer.battery.info.min_design_voltage_dV = 2250;
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_90S_DV;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_90S_DV;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -550,9 +539,10 @@ void setup_battery(void) { // Performs one time setup at startup
|
|||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Kia Niro / Hyundai Kona 64kWh battery selected");
|
||||
#endif
|
||||
|
||||
datalayer.battery.info.max_design_voltage_dV = 4040; // 404.0V
|
||||
datalayer.battery.info.min_design_voltage_dV = 3100; // 310.0V
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_98S_DV; //Start with 98S value. Precised later
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_90S_DV; //Start with 90S value. Precised later
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -4,7 +4,13 @@
|
|||
#include "../include.h"
|
||||
|
||||
#define BATTERY_SELECTED
|
||||
#define MAX_PACK_VOLTAGE_98S_DV 4110 //5000 = 500.0V
|
||||
#define MIN_PACK_VOLTAGE_98S_DV 2800
|
||||
#define MAX_PACK_VOLTAGE_90S_DV 3870
|
||||
#define MIN_PACK_VOLTAGE_90S_DV 2250
|
||||
#define MAX_CELL_DEVIATION_MV 150
|
||||
#define MAX_CELL_VOLTAGE_MV 4250 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_MV 2950 //Battery is put into emergency stop if one cell goes below this value
|
||||
|
||||
void setup_battery(void);
|
||||
void update_number_of_cells();
|
||||
|
|
|
@ -265,8 +265,10 @@ void setup_battery(void) { // Performs one time setup at startup
|
|||
Serial.println("Kia/Hyundai Hybrid battery selected");
|
||||
#endif
|
||||
datalayer.battery.info.number_of_cells = 56; // HEV , TODO: Make dynamic according to HEV/PHEV
|
||||
datalayer.battery.info.max_design_voltage_dV = 2550; //TODO: Values OK?
|
||||
datalayer.battery.info.min_design_voltage_dV = 1700;
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -4,7 +4,11 @@
|
|||
#include "../include.h"
|
||||
|
||||
#define BATTERY_SELECTED
|
||||
#define MAX_PACK_VOLTAGE_DV 2550 //5000 = 500.0V
|
||||
#define MIN_PACK_VOLTAGE_DV 1700
|
||||
#define MAX_CELL_DEVIATION_MV 100
|
||||
#define MAX_CELL_VOLTAGE_MV 4250 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value
|
||||
|
||||
void setup_battery(void);
|
||||
void transmit_can(CAN_frame* tx_frame, int interface);
|
||||
|
|
|
@ -141,8 +141,10 @@ void setup_battery(void) { // Performs one time setup at startup
|
|||
Serial.println("MG 5 battery selected");
|
||||
#endif
|
||||
|
||||
datalayer.battery.info.max_design_voltage_dV = 4040; // Over this charging is not possible
|
||||
datalayer.battery.info.min_design_voltage_dV = 3100; // Under this discharging is disabled
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -4,7 +4,11 @@
|
|||
#include "../include.h"
|
||||
|
||||
#define BATTERY_SELECTED
|
||||
#define MAX_PACK_VOLTAGE_DV 4040 //5000 = 500.0V
|
||||
#define MIN_PACK_VOLTAGE_DV 3100
|
||||
#define MAX_CELL_DEVIATION_MV 150
|
||||
#define MAX_CELL_VOLTAGE_MV 4250 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value
|
||||
|
||||
void setup_battery(void);
|
||||
void transmit_can(CAN_frame* tx_frame, int interface);
|
||||
|
|
|
@ -75,8 +75,6 @@ static uint8_t crctable[256] = {
|
|||
#define ZE1_BATTERY 2
|
||||
static uint8_t LEAF_battery_Type = ZE0_BATTERY;
|
||||
static bool battery_can_alive = false;
|
||||
#define MAX_CELL_VOLTAGE 4250 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE 2700 //Battery is put into emergency stop if one cell goes below this value
|
||||
#define WH_PER_GID 77 //One GID is this amount of Watt hours
|
||||
static uint16_t battery_Discharge_Power_Limit = 0; //Limit in kW
|
||||
static uint16_t battery_Charge_Power_Limit = 0; //Limit in kW
|
||||
|
@ -634,12 +632,6 @@ void receive_can_battery2(CAN_frame rx_frame) {
|
|||
datalayer.battery2.status.cell_max_voltage_mV = battery2_min_max_voltage[1];
|
||||
datalayer.battery2.status.cell_min_voltage_mV = battery2_min_max_voltage[0];
|
||||
|
||||
if (battery2_min_max_voltage[1] >= MAX_CELL_VOLTAGE) {
|
||||
set_event(EVENT_CELL_OVER_VOLTAGE, 0);
|
||||
}
|
||||
if (battery2_min_max_voltage[0] <= MIN_CELL_VOLTAGE) {
|
||||
set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -884,12 +876,6 @@ void receive_can_battery(CAN_frame rx_frame) {
|
|||
datalayer.battery.status.cell_max_voltage_mV = battery_min_max_voltage[1];
|
||||
datalayer.battery.status.cell_min_voltage_mV = battery_min_max_voltage[0];
|
||||
|
||||
if (battery_min_max_voltage[1] >= MAX_CELL_VOLTAGE) {
|
||||
set_event(EVENT_CELL_OVER_VOLTAGE, 0);
|
||||
}
|
||||
if (battery_min_max_voltage[0] <= MIN_CELL_VOLTAGE) {
|
||||
set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -1221,13 +1207,19 @@ void setup_battery(void) { // Performs one time setup at startup
|
|||
#endif
|
||||
|
||||
datalayer.battery.info.number_of_cells = 96;
|
||||
datalayer.battery.info.max_design_voltage_dV = 4040; // 404.4V
|
||||
datalayer.battery.info.min_design_voltage_dV = 2600; // 260.0V
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV;
|
||||
|
||||
#ifdef DOUBLE_BATTERY
|
||||
datalayer.battery2.info.number_of_cells = datalayer.battery.info.number_of_cells;
|
||||
datalayer.battery2.info.max_design_voltage_dV = datalayer.battery.info.max_design_voltage_dV;
|
||||
datalayer.battery2.info.min_design_voltage_dV = datalayer.battery.info.min_design_voltage_dV;
|
||||
datalayer.battery2.info.max_cell_voltage_mV = datalayer.battery.info.max_cell_voltage_mV;
|
||||
datalayer.battery2.info.min_cell_voltage_mV = datalayer.battery.info.min_cell_voltage_mV;
|
||||
datalayer.battery2.info.max_cell_voltage_deviation_mV = datalayer.battery.info.max_cell_voltage_deviation_mV;
|
||||
#endif //DOUBLE_BATTERY
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,11 @@
|
|||
#include "../include.h"
|
||||
|
||||
#define BATTERY_SELECTED
|
||||
#define MAX_PACK_VOLTAGE_DV 4040 //5000 = 500.0V
|
||||
#define MIN_PACK_VOLTAGE_DV 2600
|
||||
#define MAX_CELL_DEVIATION_MV 500
|
||||
#define MAX_CELL_VOLTAGE_MV 4250 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value
|
||||
|
||||
uint16_t Temp_fromRAW_to_F(uint16_t temperature);
|
||||
bool is_message_corrupt(CAN_frame rx_frame);
|
||||
|
|
|
@ -179,8 +179,10 @@ void setup_battery(void) { // Performs one time setup at startup
|
|||
Serial.println("Pylon battery selected");
|
||||
#endif
|
||||
|
||||
datalayer.battery.info.max_design_voltage_dV = 4040; // 404.0V, charging over this is not possible
|
||||
datalayer.battery.info.min_design_voltage_dV = 3100; // 310.0V, under this, discharging further is disabled
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -4,7 +4,13 @@
|
|||
#include "../include.h"
|
||||
|
||||
#define BATTERY_SELECTED
|
||||
#define MAX_CELL_DEVIATION_MV 9999
|
||||
|
||||
/* Change the following to suit your battery */
|
||||
#define MAX_PACK_VOLTAGE_DV 5000 //5000 = 500.0V
|
||||
#define MIN_PACK_VOLTAGE_DV 1500
|
||||
#define MAX_CELL_VOLTAGE_MV 4250 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value
|
||||
#define MAX_CELL_DEVIATION_MV 500
|
||||
|
||||
void setup_battery(void);
|
||||
void transmit_can(CAN_frame* tx_frame, int interface);
|
||||
|
|
|
@ -106,13 +106,6 @@ void update_values_battery() { //This function maps all the values fetched via
|
|||
|
||||
datalayer.battery.status.cell_max_voltage_mV = LB_Cell_Max_Voltage;
|
||||
|
||||
if (LB_Cell_Max_Voltage >= ABSOLUTE_CELL_MAX_VOLTAGE) {
|
||||
set_event(EVENT_CELL_OVER_VOLTAGE, (LB_Cell_Max_Voltage / 20));
|
||||
}
|
||||
if (LB_Cell_Min_Voltage <= ABSOLUTE_CELL_MIN_VOLTAGE) {
|
||||
set_event(EVENT_CELL_UNDER_VOLTAGE, (LB_Cell_Min_Voltage / 20));
|
||||
}
|
||||
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Values going to inverter:");
|
||||
Serial.print("SOH%: ");
|
||||
|
@ -248,9 +241,11 @@ void setup_battery(void) { // Performs one time setup at startup
|
|||
Serial.println("Renault Kangoo battery selected");
|
||||
#endif
|
||||
|
||||
datalayer.battery.info.max_design_voltage_dV =
|
||||
4040; // 404.0V, over this, charging is not possible (goes into forced discharge)
|
||||
datalayer.battery.info.min_design_voltage_dV = 3100; // 310.0V under this, discharging further is disabled
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -4,10 +4,11 @@
|
|||
#include "../include.h"
|
||||
|
||||
#define BATTERY_SELECTED
|
||||
|
||||
#define ABSOLUTE_CELL_MAX_VOLTAGE 4150 // If cellvoltage goes over this mV, we go into FAULT mode
|
||||
#define ABSOLUTE_CELL_MIN_VOLTAGE 2500 // If cellvoltage goes under this mV, we go into FAULT mode
|
||||
#define MAX_CELL_DEVIATION_MV 500 // If cell mV delta exceeds this, we go into WARNING mode
|
||||
#define MAX_PACK_VOLTAGE_DV 4150 //5000 = 500.0V
|
||||
#define MIN_PACK_VOLTAGE_DV 2500
|
||||
#define MAX_CELL_DEVIATION_MV 500
|
||||
#define MAX_CELL_VOLTAGE_MV 4250 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value
|
||||
#define MAX_CHARGE_POWER_W 5000 // Battery can be charged with this amount of power
|
||||
|
||||
void setup_battery(void);
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
#include "../include.h"
|
||||
#ifdef RENAULT_ZOE_GEN1_BATTERY
|
||||
#include <algorithm> // For std::min and std::max
|
||||
#include "../datalayer/datalayer.h"
|
||||
#include "../devboard/utils/events.h"
|
||||
#include "RENAULT-ZOE-GEN1-BATTERY.h"
|
||||
|
||||
/* Information in this file is based of the OVMS V3 vehicle_renaultzoe.cpp component
|
||||
https://github.com/openvehicles/Open-Vehicle-Monitoring-System-3/blob/master/vehicle/OVMS.V3/components/vehicle_renaultzoe/src/vehicle_renaultzoe.cpp
|
||||
The Zoe BMS apparently does not send total pack voltage, so we use the polled 96x cellvoltages summed up as total voltage
|
||||
Still TODO:
|
||||
- Find max discharge and max charge values (for now hardcoded to 5kW)
|
||||
- Fix the missing cell96 issue (Only cells 1-95 is shown)
|
||||
- Find current sensor value (OVMS code reads this from inverter, which we dont have)
|
||||
- Figure out why SOH% is not read (low prio)
|
||||
/*
|
||||
|
||||
/* Do not change code below unless you are sure what you are doing */
|
||||
|
@ -15,9 +22,30 @@ static int16_t LB_Average_Temperature = 0;
|
|||
static uint32_t LB_Charge_Power_W = 0;
|
||||
static int32_t LB_Current = 0;
|
||||
static uint16_t LB_kWh_Remaining = 0;
|
||||
static uint16_t LB_Cell_Max_Voltage = 3700;
|
||||
static uint16_t LB_Cell_Min_Voltage = 3700;
|
||||
static uint16_t LB_Battery_Voltage = 3700;
|
||||
static uint8_t frame0 = 0;
|
||||
static uint8_t current_poll = 0;
|
||||
static uint8_t requested_poll = 0;
|
||||
static uint8_t group = 0;
|
||||
static uint16_t cellvoltages[96];
|
||||
static uint32_t calculated_total_pack_voltage_mV = 370000;
|
||||
static uint8_t highbyte_cell_next_frame = 0;
|
||||
static uint16_t SOC_polled = 50;
|
||||
static int16_t cell_1_temperature_polled = 0;
|
||||
static int16_t cell_2_temperature_polled = 0;
|
||||
static int16_t cell_3_temperature_polled = 0;
|
||||
static int16_t cell_4_temperature_polled = 0;
|
||||
static int16_t cell_5_temperature_polled = 0;
|
||||
static int16_t cell_6_temperature_polled = 0;
|
||||
static int16_t cell_7_temperature_polled = 0;
|
||||
static int16_t cell_8_temperature_polled = 0;
|
||||
static int16_t cell_9_temperature_polled = 0;
|
||||
static int16_t cell_10_temperature_polled = 0;
|
||||
static int16_t cell_11_temperature_polled = 0;
|
||||
static int16_t cell_12_temperature_polled = 0;
|
||||
static uint16_t battery_mileage_in_km = 0;
|
||||
static uint16_t kWh_from_beginning_of_battery_life = 0;
|
||||
static bool looping_over_20 = false;
|
||||
|
||||
CAN_frame ZOE_423 = {.FD = false,
|
||||
.ext_ID = false,
|
||||
|
@ -27,19 +55,28 @@ CAN_frame ZOE_423 = {.FD = false,
|
|||
CAN_frame ZOE_POLL_79B = {.FD = false,
|
||||
.ext_ID = false,
|
||||
.DLC = 8,
|
||||
.ID = 0x79B, //0x41 = cell bat module 1-62 , 0x42 = cell bat module 63-96
|
||||
.ID = 0x79B,
|
||||
.data = {0x02, 0x21, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00}};
|
||||
CAN_frame ZOE_ACK_79B = {.FD = false,
|
||||
.ext_ID = false,
|
||||
.DLC = 8,
|
||||
.ID = 0x79B,
|
||||
.data = {0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
|
||||
|
||||
#define GROUP1_CELLVOLTAGES_1_POLL 0x41
|
||||
#define GROUP2_CELLVOLTAGES_2_POLL 0x42
|
||||
#define GROUP3_METRICS 0x61
|
||||
#define GROUP4_SOC 0x03
|
||||
#define GROUP5_TEMPERATURE_POLL 0x04
|
||||
|
||||
static unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was sent
|
||||
static unsigned long previousMillis5000 = 0; // will store last time a 1000ms CAN Message was sent
|
||||
static unsigned long previousMillis250 = 0; // will store last time a 250ms CAN Message was sent
|
||||
static uint8_t counter_423 = 0;
|
||||
|
||||
void update_values_battery() { //This function maps all the values fetched via CAN to the correct parameters used for modbus
|
||||
datalayer.battery.status.soh_pptt = (LB_SOH * 100); // Increase range from 99% -> 99.00%
|
||||
|
||||
datalayer.battery.status.real_soc = (LB_SOC * 10); // Increase LB_SOC range from 0-100.0 -> 100.00
|
||||
|
||||
datalayer.battery.status.voltage_dV = LB_Battery_Voltage;
|
||||
datalayer.battery.status.real_soc = SOC_polled;
|
||||
|
||||
datalayer.battery.status.current_dA = LB_Current; //TODO: Take from CAN
|
||||
|
||||
|
@ -49,24 +86,69 @@ void update_values_battery() { //This function maps all the values fetched via
|
|||
|
||||
datalayer.battery.status.max_discharge_power_W = 5000; //TODO: Take from CAN
|
||||
|
||||
datalayer.battery.status.max_charge_power_W = LB_Charge_Power_W;
|
||||
|
||||
datalayer.battery.status.active_power_W;
|
||||
|
||||
datalayer.battery.status.temperature_min_dC = LB_Average_Temperature;
|
||||
|
||||
datalayer.battery.status.temperature_max_dC = LB_Average_Temperature;
|
||||
|
||||
datalayer.battery.status.cell_min_voltage_mV;
|
||||
|
||||
datalayer.battery.status.cell_max_voltage_mV;
|
||||
|
||||
if (LB_Cell_Max_Voltage >= ABSOLUTE_CELL_MAX_VOLTAGE) {
|
||||
set_event(EVENT_CELL_OVER_VOLTAGE, 0);
|
||||
datalayer.battery.status.max_charge_power_W = 5000; //TODO: Take from CAN
|
||||
// TODO: Remove this hacky wacky scaling down charge power when we find value from CAN
|
||||
if (datalayer.battery.status.real_soc > 9500) {
|
||||
datalayer.battery.status.max_charge_power_W = 3000;
|
||||
}
|
||||
if (LB_Cell_Min_Voltage <= ABSOLUTE_CELL_MIN_VOLTAGE) {
|
||||
set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
|
||||
if (datalayer.battery.status.real_soc > 9600) {
|
||||
datalayer.battery.status.max_charge_power_W = 2000;
|
||||
}
|
||||
if (datalayer.battery.status.real_soc > 9700) {
|
||||
datalayer.battery.status.max_charge_power_W = 1000;
|
||||
}
|
||||
if (datalayer.battery.status.real_soc > 9800) {
|
||||
datalayer.battery.status.max_charge_power_W = 500;
|
||||
}
|
||||
if (datalayer.battery.status.real_soc > 9900) {
|
||||
datalayer.battery.status.max_charge_power_W = 50;
|
||||
}
|
||||
|
||||
//Power in watts, Negative = charging batt
|
||||
datalayer.battery.status.active_power_W =
|
||||
((datalayer.battery.status.voltage_dV * datalayer.battery.status.current_dA) / 100);
|
||||
|
||||
int16_t temperatures[] = {cell_1_temperature_polled, cell_2_temperature_polled, cell_3_temperature_polled,
|
||||
cell_4_temperature_polled, cell_5_temperature_polled, cell_6_temperature_polled,
|
||||
cell_7_temperature_polled, cell_8_temperature_polled, cell_9_temperature_polled,
|
||||
cell_10_temperature_polled, cell_11_temperature_polled, cell_12_temperature_polled};
|
||||
|
||||
// Find the minimum and maximum temperatures
|
||||
int16_t min_temperature = *std::min_element(temperatures, temperatures + 12);
|
||||
int16_t max_temperature = *std::max_element(temperatures, temperatures + 12);
|
||||
|
||||
datalayer.battery.status.temperature_min_dC = min_temperature * 10;
|
||||
|
||||
datalayer.battery.status.temperature_max_dC = max_temperature * 10;
|
||||
|
||||
//Map all cell voltages to the global array
|
||||
memcpy(datalayer.battery.status.cell_voltages_mV, cellvoltages, 96 * sizeof(uint16_t));
|
||||
|
||||
// Initialize min and max, lets find which cells are min and max!
|
||||
uint16_t min_cell_mv_value = std::numeric_limits<uint16_t>::max();
|
||||
uint16_t max_cell_mv_value = 0;
|
||||
calculated_total_pack_voltage_mV =
|
||||
datalayer.battery.status.cell_voltages_mV
|
||||
[0]; // cell96 issue, this value should be initialized to 0, but for now it is initialized to cell0
|
||||
// Loop to find the min and max while ignoring zero values
|
||||
for (uint8_t i = 0; i < datalayer.battery.info.number_of_cells; ++i) {
|
||||
uint16_t voltage_mV = datalayer.battery.status.cell_voltages_mV[i];
|
||||
calculated_total_pack_voltage_mV += voltage_mV;
|
||||
if (voltage_mV != 0) { // Skip unread values (0)
|
||||
min_cell_mv_value = std::min(min_cell_mv_value, voltage_mV);
|
||||
max_cell_mv_value = std::max(max_cell_mv_value, voltage_mV);
|
||||
}
|
||||
}
|
||||
// If all array values are 0, reset min/max to 3700
|
||||
if (min_cell_mv_value == std::numeric_limits<uint16_t>::max()) {
|
||||
min_cell_mv_value = 3700;
|
||||
max_cell_mv_value = 3700;
|
||||
calculated_total_pack_voltage_mV = 370000;
|
||||
}
|
||||
|
||||
datalayer.battery.status.cell_min_voltage_mV = min_cell_mv_value;
|
||||
datalayer.battery.status.cell_max_voltage_mV = max_cell_mv_value;
|
||||
datalayer.battery.status.voltage_dV = static_cast<uint32_t>((calculated_total_pack_voltage_mV / 100)); // mV to dV
|
||||
|
||||
#ifdef DEBUG_VIA_USB
|
||||
|
||||
|
@ -91,8 +173,296 @@ void receive_can_battery(CAN_frame rx_frame) {
|
|||
LB_SOH = (rx_frame.data.u8[4] & 0x7F);
|
||||
break;
|
||||
case 0x7BB: //Reply from active polling
|
||||
// TODO: Handle the cellvoltages
|
||||
frame0 = rx_frame.data.u8[0];
|
||||
|
||||
switch (frame0) {
|
||||
case 0x10: //PID HEADER, datarow 0
|
||||
requested_poll = rx_frame.data.u8[3];
|
||||
transmit_can(&ZOE_ACK_79B, can_config.battery);
|
||||
|
||||
if (requested_poll == GROUP1_CELLVOLTAGES_1_POLL) {
|
||||
cellvoltages[0] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
|
||||
cellvoltages[1] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP2_CELLVOLTAGES_2_POLL) {
|
||||
cellvoltages[62] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
|
||||
cellvoltages[63] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP3_METRICS) {
|
||||
//10,14,61,61,00,0A,8C,00,
|
||||
}
|
||||
if (requested_poll == GROUP4_SOC) {
|
||||
//10,1D,61,03,01,94,1F,85, (1F85 = 8069 real SOC?)
|
||||
}
|
||||
if (requested_poll == GROUP5_TEMPERATURE_POLL) {
|
||||
//10,4D,61,04,09,12,3A,09,
|
||||
cell_1_temperature_polled = (rx_frame.data.u8[6] - 40);
|
||||
}
|
||||
break;
|
||||
case 0x21: //First datarow in PID group
|
||||
if ((requested_poll == GROUP1_CELLVOLTAGES_1_POLL) && (looping_over_20 == false)) {
|
||||
cellvoltages[2] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[3] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[4] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
highbyte_cell_next_frame = rx_frame.data.u8[7];
|
||||
}
|
||||
if ((requested_poll == GROUP1_CELLVOLTAGES_1_POLL) && (looping_over_20 == true)) {
|
||||
cellvoltages[58] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[59] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[60] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
highbyte_cell_next_frame = rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP2_CELLVOLTAGES_2_POLL) {
|
||||
cellvoltages[64] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[65] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[66] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
highbyte_cell_next_frame = rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP3_METRICS) {
|
||||
//21,C8,C8,C8,C0,C0,00,00,
|
||||
}
|
||||
if (requested_poll == GROUP4_SOC) {
|
||||
//21,21,32,00,00,00,00,01, (2132 = 8498 dash SOC?)
|
||||
SOC_polled = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
}
|
||||
if (requested_poll == GROUP5_TEMPERATURE_POLL) {
|
||||
cell_2_temperature_polled = (rx_frame.data.u8[2] - 40);
|
||||
cell_3_temperature_polled = (rx_frame.data.u8[5] - 40);
|
||||
//21,11,3A,09,14,3A,09,0D,
|
||||
}
|
||||
break;
|
||||
case 0x22: //Second datarow in PID group
|
||||
if ((requested_poll == GROUP1_CELLVOLTAGES_1_POLL) && (looping_over_20 == false)) {
|
||||
cellvoltages[5] = (highbyte_cell_next_frame << 8) | rx_frame.data.u8[1];
|
||||
cellvoltages[6] = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3];
|
||||
cellvoltages[7] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
|
||||
cellvoltages[8] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7];
|
||||
}
|
||||
if ((requested_poll == GROUP1_CELLVOLTAGES_1_POLL) && (looping_over_20 == true)) {
|
||||
cellvoltages[61] = (highbyte_cell_next_frame << 8) | rx_frame.data.u8[1];
|
||||
looping_over_20 = false;
|
||||
}
|
||||
if (requested_poll == GROUP2_CELLVOLTAGES_2_POLL) {
|
||||
cellvoltages[67] = (highbyte_cell_next_frame << 8) | rx_frame.data.u8[1];
|
||||
cellvoltages[68] = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3];
|
||||
cellvoltages[69] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
|
||||
cellvoltages[70] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP3_METRICS) {
|
||||
//22,BB,7C,00,00,23,E4,FF, (BB7C = 47996km) (23E4 = 9188kWh)
|
||||
battery_mileage_in_km = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
kWh_from_beginning_of_battery_life = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
}
|
||||
if (requested_poll == GROUP4_SOC) {
|
||||
//22,95,01,93,00,00,00,FF,
|
||||
}
|
||||
if (requested_poll == GROUP5_TEMPERATURE_POLL) {
|
||||
cell_4_temperature_polled = (rx_frame.data.u8[1] - 40);
|
||||
cell_5_temperature_polled = (rx_frame.data.u8[4] - 40);
|
||||
cell_6_temperature_polled = (rx_frame.data.u8[7] - 40);
|
||||
//22,3A,08,F6,3B,08,EE,3B,
|
||||
}
|
||||
break;
|
||||
case 0x23: //Third datarow in PID group
|
||||
if (requested_poll == GROUP1_CELLVOLTAGES_1_POLL) {
|
||||
cellvoltages[9] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[10] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[11] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
highbyte_cell_next_frame = rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP2_CELLVOLTAGES_2_POLL) {
|
||||
cellvoltages[71] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[72] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[73] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
highbyte_cell_next_frame = rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP4_SOC) {
|
||||
//23,FF,FF,FF,01,20,7B,00,
|
||||
}
|
||||
if (requested_poll == GROUP5_TEMPERATURE_POLL) {
|
||||
//23,08,AC,3D,08,C0,3C,09,
|
||||
cell_7_temperature_polled = (rx_frame.data.u8[3] - 40);
|
||||
cell_8_temperature_polled = (rx_frame.data.u8[6] - 40);
|
||||
}
|
||||
break;
|
||||
case 0x24: //Fourth datarow in PID group
|
||||
if (requested_poll == GROUP1_CELLVOLTAGES_1_POLL) {
|
||||
cellvoltages[12] = (highbyte_cell_next_frame << 8) | rx_frame.data.u8[1];
|
||||
cellvoltages[13] = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3];
|
||||
cellvoltages[14] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
|
||||
cellvoltages[15] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP2_CELLVOLTAGES_2_POLL) {
|
||||
cellvoltages[74] = (highbyte_cell_next_frame << 8) | rx_frame.data.u8[1];
|
||||
cellvoltages[75] = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3];
|
||||
cellvoltages[76] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
|
||||
cellvoltages[77] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP4_SOC) {
|
||||
//24,00,00,00,00,00,00,00,
|
||||
}
|
||||
if (requested_poll == GROUP5_TEMPERATURE_POLL) {
|
||||
//24,03,3A,09,11,3A,09,19,
|
||||
cell_9_temperature_polled = (rx_frame.data.u8[2] - 40);
|
||||
cell_10_temperature_polled = (rx_frame.data.u8[5] - 40);
|
||||
}
|
||||
break;
|
||||
case 0x25: //Fifth datarow in PID group
|
||||
if (requested_poll == GROUP1_CELLVOLTAGES_1_POLL) {
|
||||
cellvoltages[16] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[17] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[18] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
highbyte_cell_next_frame = rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP2_CELLVOLTAGES_2_POLL) {
|
||||
cellvoltages[78] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[79] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[80] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
highbyte_cell_next_frame = rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP5_TEMPERATURE_POLL) {
|
||||
//25,3A,09,14,3A,FF,FF,FF,
|
||||
cell_11_temperature_polled = (rx_frame.data.u8[1] - 40);
|
||||
cell_12_temperature_polled = (rx_frame.data.u8[4] - 40);
|
||||
}
|
||||
break;
|
||||
case 0x26:
|
||||
if (requested_poll == GROUP1_CELLVOLTAGES_1_POLL) {
|
||||
cellvoltages[19] = (highbyte_cell_next_frame << 8) | rx_frame.data.u8[1];
|
||||
cellvoltages[20] = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3];
|
||||
cellvoltages[21] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
|
||||
cellvoltages[22] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP2_CELLVOLTAGES_2_POLL) {
|
||||
cellvoltages[81] = (highbyte_cell_next_frame << 8) | rx_frame.data.u8[1];
|
||||
cellvoltages[82] = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3];
|
||||
cellvoltages[83] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
|
||||
cellvoltages[84] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP5_TEMPERATURE_POLL) {
|
||||
//26,FF,FF,FF,FF,FF,FF,FF,G
|
||||
}
|
||||
break;
|
||||
case 0x27:
|
||||
if (requested_poll == GROUP1_CELLVOLTAGES_1_POLL) {
|
||||
cellvoltages[23] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[24] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[25] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
highbyte_cell_next_frame = rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP2_CELLVOLTAGES_2_POLL) {
|
||||
cellvoltages[85] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[86] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[87] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
highbyte_cell_next_frame = rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP5_TEMPERATURE_POLL) {
|
||||
//27,FF,FF,FF,FF,FF,FF,FF,
|
||||
}
|
||||
break;
|
||||
case 0x28:
|
||||
if (requested_poll == GROUP1_CELLVOLTAGES_1_POLL) {
|
||||
cellvoltages[26] = (highbyte_cell_next_frame << 8) | rx_frame.data.u8[1];
|
||||
cellvoltages[27] = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3];
|
||||
cellvoltages[28] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
|
||||
cellvoltages[29] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP2_CELLVOLTAGES_2_POLL) {
|
||||
cellvoltages[88] = (highbyte_cell_next_frame << 8) | rx_frame.data.u8[1];
|
||||
cellvoltages[89] = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3];
|
||||
cellvoltages[90] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
|
||||
cellvoltages[91] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP5_TEMPERATURE_POLL) {
|
||||
//28,FF,FF,FF,FF,FF,FF,FF,
|
||||
}
|
||||
break;
|
||||
case 0x29:
|
||||
if (requested_poll == GROUP1_CELLVOLTAGES_1_POLL) {
|
||||
cellvoltages[30] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[21] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[32] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
highbyte_cell_next_frame = rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP2_CELLVOLTAGES_2_POLL) {
|
||||
cellvoltages[92] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[93] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[94] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
highbyte_cell_next_frame = rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP5_TEMPERATURE_POLL) {
|
||||
//29,FF,FF,FF,FF,FF,FF,FF,
|
||||
}
|
||||
break;
|
||||
case 0x2A:
|
||||
if (requested_poll == GROUP1_CELLVOLTAGES_1_POLL) {
|
||||
cellvoltages[33] = (highbyte_cell_next_frame << 8) | rx_frame.data.u8[1];
|
||||
cellvoltages[34] = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3];
|
||||
cellvoltages[35] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
|
||||
cellvoltages[36] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP2_CELLVOLTAGES_2_POLL) {
|
||||
cellvoltages[95] = (highbyte_cell_next_frame << 8) | rx_frame.data.u8[1];
|
||||
}
|
||||
if (requested_poll == GROUP5_TEMPERATURE_POLL) {
|
||||
//2A,FF,FF,FF,FF,FF,3A,3A,
|
||||
}
|
||||
break;
|
||||
case 0x2B:
|
||||
if (requested_poll == GROUP1_CELLVOLTAGES_1_POLL) {
|
||||
cellvoltages[37] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[38] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[39] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
highbyte_cell_next_frame = rx_frame.data.u8[7];
|
||||
}
|
||||
if (requested_poll == GROUP5_TEMPERATURE_POLL) {
|
||||
//2B,3D,00,00,00,00,00,00,
|
||||
}
|
||||
break;
|
||||
case 0x2C:
|
||||
if (requested_poll == GROUP1_CELLVOLTAGES_1_POLL) {
|
||||
cellvoltages[40] = (highbyte_cell_next_frame << 8) | rx_frame.data.u8[1];
|
||||
cellvoltages[41] = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3];
|
||||
cellvoltages[42] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
|
||||
cellvoltages[43] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7];
|
||||
}
|
||||
break;
|
||||
case 0x2D:
|
||||
if (requested_poll == GROUP1_CELLVOLTAGES_1_POLL) {
|
||||
cellvoltages[44] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[45] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[46] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
highbyte_cell_next_frame = rx_frame.data.u8[7];
|
||||
}
|
||||
break;
|
||||
case 0x2E:
|
||||
if (requested_poll == GROUP1_CELLVOLTAGES_1_POLL) {
|
||||
cellvoltages[47] = (highbyte_cell_next_frame << 8) | rx_frame.data.u8[1];
|
||||
cellvoltages[48] = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3];
|
||||
cellvoltages[49] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
|
||||
cellvoltages[50] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7];
|
||||
}
|
||||
break;
|
||||
case 0x2F:
|
||||
if (requested_poll == GROUP1_CELLVOLTAGES_1_POLL) {
|
||||
cellvoltages[51] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[52] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[53] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
highbyte_cell_next_frame = rx_frame.data.u8[7];
|
||||
}
|
||||
break;
|
||||
case 0x20:
|
||||
if (requested_poll == GROUP1_CELLVOLTAGES_1_POLL) {
|
||||
cellvoltages[54] = (highbyte_cell_next_frame << 8) | rx_frame.data.u8[1];
|
||||
cellvoltages[55] = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3];
|
||||
cellvoltages[56] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
|
||||
cellvoltages[57] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7];
|
||||
looping_over_20 = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -119,9 +489,34 @@ void send_can_battery() {
|
|||
}
|
||||
counter_423 = (counter_423 + 1) % 10;
|
||||
}
|
||||
// 5000ms CAN handling
|
||||
if (currentMillis - previousMillis5000 >= INTERVAL_5_S) {
|
||||
previousMillis5000 = currentMillis;
|
||||
// 250ms CAN handling
|
||||
if (currentMillis - previousMillis250 >= INTERVAL_250_MS) {
|
||||
previousMillis250 = currentMillis;
|
||||
|
||||
switch (group) {
|
||||
case 0:
|
||||
current_poll = GROUP1_CELLVOLTAGES_1_POLL;
|
||||
break;
|
||||
case 1:
|
||||
current_poll = GROUP2_CELLVOLTAGES_2_POLL;
|
||||
break;
|
||||
case 2:
|
||||
current_poll = GROUP3_METRICS;
|
||||
break;
|
||||
case 3:
|
||||
current_poll = GROUP4_SOC;
|
||||
break;
|
||||
case 4:
|
||||
current_poll = GROUP5_TEMPERATURE_POLL;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
group = (group + 1) % 5; // Cycle 0-1-2-3-4-0-1...
|
||||
|
||||
ZOE_POLL_79B.data.u8[2] = current_poll;
|
||||
|
||||
transmit_can(&ZOE_POLL_79B, can_config.battery);
|
||||
}
|
||||
}
|
||||
|
@ -130,9 +525,13 @@ void setup_battery(void) { // Performs one time setup at startup
|
|||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Renault Zoe 22/40kWh battery selected");
|
||||
#endif
|
||||
|
||||
datalayer.battery.info.max_design_voltage_dV = 4040;
|
||||
datalayer.battery.info.min_design_voltage_dV = 2700;
|
||||
datalayer.system.status.battery_allows_contactor_closing = true;
|
||||
datalayer.battery.info.number_of_cells = 96;
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -3,10 +3,12 @@
|
|||
#include "../include.h"
|
||||
|
||||
#define BATTERY_SELECTED
|
||||
|
||||
#define ABSOLUTE_CELL_MAX_VOLTAGE 4100
|
||||
#define ABSOLUTE_CELL_MIN_VOLTAGE 3000
|
||||
#define MAX_CELL_DEVIATION_MV 500
|
||||
#define MAX_PACK_VOLTAGE_DV 4200 //5000 = 500.0V
|
||||
#define MIN_PACK_VOLTAGE_DV 3000
|
||||
#define MAX_CELL_DEVIATION_MV 500
|
||||
#define MAX_CELL_VOLTAGE_MV 4250 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value
|
||||
|
||||
void setup_battery(void);
|
||||
void transmit_can(CAN_frame* tx_frame, int interface);
|
||||
|
|
|
@ -59,13 +59,6 @@ void update_values_battery() { //This function maps all the values fetched via
|
|||
|
||||
datalayer.battery.status.cell_max_voltage_mV;
|
||||
|
||||
if (LB_Cell_Max_Voltage >= ABSOLUTE_CELL_MAX_VOLTAGE) {
|
||||
set_event(EVENT_CELL_OVER_VOLTAGE, 0);
|
||||
}
|
||||
if (LB_Cell_Min_Voltage <= ABSOLUTE_CELL_MIN_VOLTAGE) {
|
||||
set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
|
||||
}
|
||||
|
||||
#ifdef DEBUG_VIA_USB
|
||||
|
||||
#endif
|
||||
|
@ -99,9 +92,13 @@ void setup_battery(void) { // Performs one time setup at startup
|
|||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Renault Zoe 50kWh battery selected");
|
||||
#endif
|
||||
|
||||
datalayer.battery.info.max_design_voltage_dV = 4040;
|
||||
datalayer.battery.info.min_design_voltage_dV = 3100;
|
||||
datalayer.system.status.battery_allows_contactor_closing = true;
|
||||
datalayer.battery.info.number_of_cells = 96;
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
#include "../include.h"
|
||||
|
||||
#define BATTERY_SELECTED
|
||||
|
||||
#define ABSOLUTE_CELL_MAX_VOLTAGE 4100
|
||||
#define ABSOLUTE_CELL_MIN_VOLTAGE 3000
|
||||
#define MAX_PACK_VOLTAGE_DV 4100 //5000 = 500.0V
|
||||
#define MIN_PACK_VOLTAGE_DV 3000
|
||||
#define MAX_CELL_DEVIATION_MV 500
|
||||
#define MAX_CELL_VOLTAGE_MV 4250 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value
|
||||
|
||||
void setup_battery(void);
|
||||
void transmit_can(CAN_frame* tx_frame, int interface);
|
||||
|
|
586
Software/src/battery/RJXZS-BMS.cpp
Normal file
586
Software/src/battery/RJXZS-BMS.cpp
Normal file
|
@ -0,0 +1,586 @@
|
|||
#include "../include.h"
|
||||
#ifdef RJXZS_BMS
|
||||
#include "../datalayer/datalayer.h"
|
||||
#include "../devboard/utils/events.h"
|
||||
#include "RJXZS-BMS.h"
|
||||
|
||||
/* Do not change code below unless you are sure what you are doing */
|
||||
static unsigned long previousMillis10s = 0; // will store last time a 10s CAN Message was sent
|
||||
|
||||
//Actual content messages
|
||||
CAN_frame RJXZS_1C = {.FD = false, .ext_ID = true, .DLC = 3, .ID = 0xF4, .data = {0x1C, 0x00, 0x02}};
|
||||
CAN_frame RJXZS_10 = {.FD = false, .ext_ID = true, .DLC = 3, .ID = 0xF4, .data = {0x10, 0x00, 0x02}};
|
||||
|
||||
#define FIVE_MINUTES 60
|
||||
|
||||
static uint8_t mux = 0;
|
||||
static bool setup_completed = false;
|
||||
static uint16_t total_voltage = 0;
|
||||
static int16_t total_current = 0;
|
||||
static uint16_t total_power = 0;
|
||||
static uint16_t battery_usage_capacity = 0;
|
||||
static uint16_t battery_capacity_percentage = 0;
|
||||
static uint16_t charging_capacity = 0;
|
||||
static uint16_t charging_recovery_voltage = 0;
|
||||
static uint16_t discharging_recovery_voltage = 0;
|
||||
static uint16_t remaining_capacity = 0;
|
||||
static int16_t host_temperature = 0;
|
||||
static uint16_t status_accounting = 0;
|
||||
static uint16_t equalization_starting_voltage = 0;
|
||||
static uint16_t discharge_protection_voltage = 0;
|
||||
static uint16_t protective_current = 0;
|
||||
static uint16_t battery_pack_capacity = 0;
|
||||
static uint16_t number_of_battery_strings = 0;
|
||||
static uint16_t charging_protection_voltage = 0;
|
||||
static int16_t protection_temperature = 0;
|
||||
static bool temperature_below_zero_mod1_4 = false;
|
||||
static bool temperature_below_zero_mod5_8 = false;
|
||||
static bool temperature_below_zero_mod9_12 = false;
|
||||
static bool temperature_below_zero_mod13_16 = false;
|
||||
static uint16_t module_1_temperature = 0;
|
||||
static uint16_t module_2_temperature = 0;
|
||||
static uint16_t module_3_temperature = 0;
|
||||
static uint16_t module_4_temperature = 0;
|
||||
static uint16_t module_5_temperature = 0;
|
||||
static uint16_t module_6_temperature = 0;
|
||||
static uint16_t module_7_temperature = 0;
|
||||
static uint16_t module_8_temperature = 0;
|
||||
static uint16_t module_9_temperature = 0;
|
||||
static uint16_t module_10_temperature = 0;
|
||||
static uint16_t module_11_temperature = 0;
|
||||
static uint16_t module_12_temperature = 0;
|
||||
static uint16_t module_13_temperature = 0;
|
||||
static uint16_t module_14_temperature = 0;
|
||||
static uint16_t module_15_temperature = 0;
|
||||
static uint16_t module_16_temperature = 0;
|
||||
static uint16_t low_voltage_power_outage_protection = 0;
|
||||
static uint16_t low_voltage_power_outage_delayed = 0;
|
||||
static uint16_t num_of_triggering_protection_cells = 0;
|
||||
static uint16_t balanced_reference_voltage = 0;
|
||||
static uint16_t minimum_cell_voltage = 0;
|
||||
static uint16_t maximum_cell_voltage = 0;
|
||||
static uint16_t cellvoltages[MAX_AMOUNT_CELLS];
|
||||
static uint8_t populated_cellvoltages = 0;
|
||||
static uint16_t accumulated_total_capacity_high = 0;
|
||||
static uint16_t accumulated_total_capacity_low = 0;
|
||||
static uint16_t pre_charge_delay_time = 0;
|
||||
static uint16_t LCD_status = 0;
|
||||
static uint16_t differential_pressure_setting_value = 0;
|
||||
static uint16_t use_capacity_to_automatically_reset = 0;
|
||||
static uint16_t low_temperature_protection_setting_value = 0;
|
||||
static uint16_t protecting_historical_logs = 0;
|
||||
static uint16_t hall_sensor_type = 0;
|
||||
static uint16_t fan_start_setting_value = 0;
|
||||
static uint16_t ptc_heating_start_setting_value = 0;
|
||||
static uint16_t default_channel_state = 0;
|
||||
static uint8_t timespent_without_soc = 0;
|
||||
|
||||
void update_values_battery() {
|
||||
|
||||
datalayer.battery.status.real_soc = battery_capacity_percentage * 100;
|
||||
if (battery_capacity_percentage == 0) {
|
||||
//SOC% not available. Raise warning event if we go too long without SOC
|
||||
timespent_without_soc++;
|
||||
if (timespent_without_soc > FIVE_MINUTES) {
|
||||
set_event(EVENT_SOC_UNAVAILABLE, 0);
|
||||
}
|
||||
} else { //SOC is available, stop counting and clear error
|
||||
timespent_without_soc = 0;
|
||||
clear_event(EVENT_SOC_UNAVAILABLE);
|
||||
}
|
||||
|
||||
datalayer.battery.status.remaining_capacity_Wh = static_cast<uint32_t>(
|
||||
(static_cast<double>(datalayer.battery.status.real_soc) / 10000) * datalayer.battery.info.total_capacity_Wh);
|
||||
|
||||
datalayer.battery.status.soh_pptt; // This BMS does not have a SOH% formula
|
||||
|
||||
datalayer.battery.status.voltage_dV = total_voltage;
|
||||
|
||||
datalayer.battery.status.current_dA = total_current;
|
||||
|
||||
datalayer.battery.status.active_power_W = //Power in watts, Negative = charging batt
|
||||
((datalayer.battery.status.voltage_dV * datalayer.battery.status.current_dA) / 100);
|
||||
|
||||
// Charge power is set in .h file
|
||||
if (datalayer.battery.status.real_soc > 9900) {
|
||||
datalayer.battery.status.max_charge_power_W = MAX_CHARGE_POWER_WHEN_TOPBALANCING_W;
|
||||
} else if (datalayer.battery.status.real_soc > RAMPDOWN_SOC) {
|
||||
// When real SOC is between RAMPDOWN_SOC-99%, ramp the value between Max<->0
|
||||
datalayer.battery.status.max_charge_power_W =
|
||||
MAX_CHARGE_POWER_ALLOWED_W *
|
||||
(1 - (datalayer.battery.status.real_soc - RAMPDOWN_SOC) / (10000.0 - RAMPDOWN_SOC));
|
||||
} else { // No limits, max charging power allowed
|
||||
datalayer.battery.status.max_charge_power_W = MAX_CHARGE_POWER_ALLOWED_W;
|
||||
}
|
||||
|
||||
// Discharge power is also set in .h file
|
||||
datalayer.battery.status.max_discharge_power_W = MAX_DISCHARGE_POWER_ALLOWED_W;
|
||||
|
||||
uint16_t temperatures[] = {
|
||||
module_1_temperature, module_2_temperature, module_3_temperature, module_4_temperature,
|
||||
module_5_temperature, module_6_temperature, module_7_temperature, module_8_temperature,
|
||||
module_9_temperature, module_10_temperature, module_11_temperature, module_12_temperature,
|
||||
module_13_temperature, module_14_temperature, module_15_temperature, module_16_temperature};
|
||||
|
||||
uint16_t min_temp = std::numeric_limits<uint16_t>::max();
|
||||
uint16_t max_temp = 0;
|
||||
|
||||
// Loop through the array to find min and max temperatures, ignoring 0 values
|
||||
for (int i = 0; i < 16; i++) {
|
||||
if (temperatures[i] != 0) { // Ignore unavailable temperatures
|
||||
if (temperatures[i] < min_temp) {
|
||||
min_temp = temperatures[i];
|
||||
}
|
||||
if (temperatures[i] > max_temp) {
|
||||
max_temp = temperatures[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
datalayer.battery.status.temperature_min_dC = min_temp;
|
||||
|
||||
datalayer.battery.status.temperature_max_dC = max_temp;
|
||||
|
||||
// The cellvoltages[] array can contain 0s inside it
|
||||
populated_cellvoltages = 0;
|
||||
for (int i = 0; i < MAX_AMOUNT_CELLS; ++i) {
|
||||
if (cellvoltages[i] > 0) { // We have a measurement available
|
||||
datalayer.battery.status.cell_voltages_mV[populated_cellvoltages] = cellvoltages[i];
|
||||
populated_cellvoltages++;
|
||||
}
|
||||
}
|
||||
|
||||
datalayer.battery.info.number_of_cells = populated_cellvoltages; // 1-192S
|
||||
|
||||
datalayer.battery.info.max_design_voltage_dV; // Set according to cells?
|
||||
|
||||
datalayer.battery.info.min_design_voltage_dV; // Set according to cells?
|
||||
|
||||
datalayer.battery.status.cell_max_voltage_mV = maximum_cell_voltage;
|
||||
|
||||
datalayer.battery.status.cell_min_voltage_mV = minimum_cell_voltage;
|
||||
}
|
||||
|
||||
void receive_can_battery(CAN_frame rx_frame) {
|
||||
|
||||
/*
|
||||
// All CAN messages recieved will be logged via serial
|
||||
Serial.print(millis()); // Example printout, time, ID, length, data: 7553 1DB 8 FF C0 B9 EA 0 0 2 5D
|
||||
Serial.print(" ");
|
||||
Serial.print(rx_frame.ID, HEX);
|
||||
Serial.print(" ");
|
||||
Serial.print(rx_frame.DLC);
|
||||
Serial.print(" ");
|
||||
for (int i = 0; i < rx_frame.DLC; ++i) {
|
||||
Serial.print(rx_frame.data.u8[i], HEX);
|
||||
Serial.print(" ");
|
||||
}
|
||||
Serial.println("");
|
||||
*/
|
||||
switch (rx_frame.ID) {
|
||||
case 0xF5: // This is the only message is sent from BMS
|
||||
setup_completed = true; // Let the function know we no longer need to send startup messages
|
||||
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||
mux = rx_frame.data.u8[0];
|
||||
|
||||
if (mux == 0x01) { // discharge protection voltage, protective current, battery pack capacity
|
||||
discharge_protection_voltage = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
protective_current = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
battery_pack_capacity = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x02) { // Number of strings, charge protection voltage, protection temperature
|
||||
number_of_battery_strings = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
charging_protection_voltage = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
protection_temperature = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x03) { // Total voltage/current/power
|
||||
total_voltage = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
total_current = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
total_power = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x04) { // Capacity, percentages
|
||||
battery_usage_capacity = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
battery_capacity_percentage = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
charging_capacity = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x05) { //Recovery / capacity
|
||||
charging_recovery_voltage = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
discharging_recovery_voltage = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
remaining_capacity = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x06) { // temperature, equalization
|
||||
host_temperature = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
status_accounting = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
equalization_starting_voltage = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x07) { // Cellvoltages 1-3
|
||||
cellvoltages[0] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[1] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[2] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x08) { // Cellvoltages 4-6
|
||||
cellvoltages[3] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[4] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[5] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x09) { // Cellvoltages 7-9
|
||||
cellvoltages[6] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[7] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[8] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x0A) { // Cellvoltages 10-12
|
||||
cellvoltages[9] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[10] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[11] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x0B) { // Cellvoltages 13-15
|
||||
cellvoltages[12] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[13] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[14] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x0C) { // Cellvoltages 16-18
|
||||
cellvoltages[15] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[16] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[17] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x0D) { // Cellvoltages 19-21
|
||||
cellvoltages[18] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[19] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[20] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x0E) { // Cellvoltages 22-24
|
||||
cellvoltages[21] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[22] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[23] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x0F) { // Cellvoltages 25-27
|
||||
cellvoltages[24] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[25] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[26] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x10) { // Cellvoltages 28-30
|
||||
cellvoltages[27] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[28] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[29] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x11) { // Cellvoltages 31-33
|
||||
cellvoltages[30] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[31] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[32] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x12) { // Cellvoltages 34-36
|
||||
cellvoltages[33] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[34] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[35] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x13) { // Cellvoltages 37-39
|
||||
cellvoltages[36] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[37] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[38] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x14) { // Cellvoltages 40-42
|
||||
cellvoltages[39] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[40] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[41] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x15) { // Cellvoltages 43-45
|
||||
cellvoltages[42] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[43] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[44] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x16) { // Cellvoltages 46-48
|
||||
cellvoltages[45] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[46] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[47] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x17) { // Cellvoltages 49-51
|
||||
cellvoltages[48] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[49] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[50] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x18) { // Cellvoltages 52-54
|
||||
cellvoltages[51] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[52] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[53] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x19) { // Cellvoltages 55-57
|
||||
cellvoltages[54] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[55] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[56] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x1A) { // Cellvoltages 58-60
|
||||
cellvoltages[57] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[58] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[59] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x1B) { // Cellvoltages 61-63
|
||||
cellvoltages[60] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[61] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[62] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x1C) { // Cellvoltages 64-66
|
||||
cellvoltages[63] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[64] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[65] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x1D) { // Cellvoltages 67-69
|
||||
cellvoltages[66] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[67] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[68] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x1E) { // Cellvoltages 70-72
|
||||
cellvoltages[69] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[70] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[71] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x1F) { // Cellvoltages 73-75
|
||||
cellvoltages[72] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[73] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[74] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x20) { // Cellvoltages 76-78
|
||||
cellvoltages[75] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[76] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[77] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x21) { // Cellvoltages 79-81
|
||||
cellvoltages[78] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[79] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[80] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x22) { // Cellvoltages 82-84
|
||||
cellvoltages[81] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[82] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[83] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x23) { // Cellvoltages 85-87
|
||||
cellvoltages[84] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[85] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[86] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x24) { // Cellvoltages 88-90
|
||||
cellvoltages[87] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[88] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[89] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x25) { // Cellvoltages 91-93
|
||||
cellvoltages[90] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[91] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[92] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x26) { // Cellvoltages 94-96
|
||||
cellvoltages[93] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[94] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[95] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x27) { // Cellvoltages 97-99
|
||||
cellvoltages[96] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[97] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[98] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x28) { // Cellvoltages 100-102
|
||||
cellvoltages[99] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[100] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[101] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x29) { // Cellvoltages 103-105
|
||||
cellvoltages[102] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[103] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[104] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x2A) { // Cellvoltages 106-108
|
||||
cellvoltages[105] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[106] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[107] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x2B) { // Cellvoltages 109-111
|
||||
cellvoltages[108] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[109] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[110] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x2C) { // Cellvoltages 112-114
|
||||
cellvoltages[111] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[112] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[113] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x2D) { // Cellvoltages 115-117
|
||||
cellvoltages[114] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[115] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[116] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x2E) { // Cellvoltages 118-120
|
||||
cellvoltages[117] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[118] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[119] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x2F) { // Cellvoltages 121-123
|
||||
cellvoltages[120] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[121] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[122] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x30) { // Cellvoltages 124-126
|
||||
cellvoltages[123] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[124] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[125] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x31) { // Cellvoltages 127-129
|
||||
cellvoltages[126] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[127] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[128] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x32) { // Cellvoltages 130-132
|
||||
cellvoltages[129] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[130] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[131] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x33) { // Cellvoltages 133-135
|
||||
cellvoltages[132] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[133] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[134] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x34) { // Cellvoltages 136-138
|
||||
cellvoltages[135] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[136] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[137] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x35) { // Cellvoltages 139-141
|
||||
cellvoltages[138] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[139] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[140] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x36) { // Cellvoltages 142-144
|
||||
cellvoltages[141] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[142] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[143] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x37) { // Cellvoltages 145-147
|
||||
cellvoltages[144] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[145] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[146] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x38) { // Cellvoltages 148-150
|
||||
cellvoltages[147] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[148] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[149] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x39) { // Cellvoltages 151-153
|
||||
cellvoltages[150] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[151] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[152] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x3A) { // Cellvoltages 154-156
|
||||
cellvoltages[153] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[154] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[155] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x3B) { // Cellvoltages 157-159
|
||||
cellvoltages[156] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[157] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[158] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x3C) { // Cellvoltages 160-162
|
||||
cellvoltages[159] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[160] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[161] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x3D) { // Cellvoltages 163-165
|
||||
cellvoltages[162] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[163] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[164] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x3E) { // Cellvoltages 166-167
|
||||
cellvoltages[165] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[166] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[167] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x3F) { // Cellvoltages 169-171
|
||||
cellvoltages[168] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[169] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[170] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x40) { // Cellvoltages 172-174
|
||||
cellvoltages[171] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[172] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[173] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x41) { // Cellvoltages 175-177
|
||||
cellvoltages[174] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[175] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[176] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x42) { // Cellvoltages 178-180
|
||||
cellvoltages[177] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[178] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[179] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x43) { // Cellvoltages 181-183
|
||||
cellvoltages[180] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[181] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[182] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x44) { // Cellvoltages 184-186
|
||||
cellvoltages[183] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[184] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[185] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x45) { // Cellvoltages 187-189
|
||||
cellvoltages[186] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[187] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[188] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x46) { // Cellvoltages 190-192
|
||||
cellvoltages[189] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
cellvoltages[190] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
cellvoltages[191] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x47) {
|
||||
temperature_below_zero_mod1_4 = rx_frame.data.u8[2];
|
||||
module_1_temperature = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
} else if (mux == 0x48) {
|
||||
module_2_temperature = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
module_3_temperature = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x49) {
|
||||
module_4_temperature = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
} else if (mux == 0x4A) {
|
||||
temperature_below_zero_mod5_8 = rx_frame.data.u8[2];
|
||||
module_5_temperature = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
} else if (mux == 0x4B) {
|
||||
module_6_temperature = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
module_7_temperature = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x4C) {
|
||||
module_8_temperature = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
} else if (mux == 0x4D) {
|
||||
temperature_below_zero_mod9_12 = rx_frame.data.u8[2];
|
||||
module_9_temperature = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
module_10_temperature = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x4E) {
|
||||
module_11_temperature = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
module_12_temperature = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
module_13_temperature = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x4F) {
|
||||
module_14_temperature = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
module_15_temperature = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
module_16_temperature = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x50) {
|
||||
low_voltage_power_outage_protection = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
low_voltage_power_outage_delayed = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
num_of_triggering_protection_cells = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x51) {
|
||||
balanced_reference_voltage = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
minimum_cell_voltage = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
maximum_cell_voltage = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
} else if (mux == 0x52) {
|
||||
accumulated_total_capacity_high = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
accumulated_total_capacity_low = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
pre_charge_delay_time = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
LCD_status = rx_frame.data.u8[7];
|
||||
} else if (mux == 0x53) {
|
||||
differential_pressure_setting_value = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
use_capacity_to_automatically_reset = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
low_temperature_protection_setting_value = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
protecting_historical_logs = rx_frame.data.u8[7];
|
||||
|
||||
if (protecting_historical_logs == 0x01) {
|
||||
// Overcurrent protection
|
||||
set_event(EVENT_DISCHARGE_LIMIT_EXCEEDED, 0); // could also be EVENT_CHARGE_LIMIT_EXCEEDED
|
||||
} else if (protecting_historical_logs == 0x02) {
|
||||
// over discharge protection
|
||||
set_event(EVENT_BATTERY_UNDERVOLTAGE, 0);
|
||||
} else if (protecting_historical_logs == 0x03) {
|
||||
// overcharge protection
|
||||
set_event(EVENT_BATTERY_OVERVOLTAGE, 0);
|
||||
} else if (protecting_historical_logs == 0x04) {
|
||||
// Over temperature protection
|
||||
set_event(EVENT_BATTERY_OVERHEAT, 0);
|
||||
} else if (protecting_historical_logs == 0x05) {
|
||||
// Battery string error protection
|
||||
set_event(EVENT_BATTERY_CAUTION, 0);
|
||||
} else if (protecting_historical_logs == 0x06) {
|
||||
// Damaged charging relay
|
||||
set_event(EVENT_BATTERY_CHG_STOP_REQ, 0);
|
||||
} else if (protecting_historical_logs == 0x07) {
|
||||
// Damaged discharge relay
|
||||
set_event(EVENT_BATTERY_DISCHG_STOP_REQ, 0);
|
||||
} else if (protecting_historical_logs == 0x08) {
|
||||
// Low voltage power outage protection
|
||||
set_event(EVENT_12V_LOW, 0);
|
||||
} else if (protecting_historical_logs == 0x09) {
|
||||
// Voltage difference protection
|
||||
set_event(EVENT_VOLTAGE_DIFFERENCE, differential_pressure_setting_value);
|
||||
} else if (protecting_historical_logs == 0x0A) {
|
||||
// Low temperature protection
|
||||
set_event(EVENT_BATTERY_FROZEN, low_temperature_protection_setting_value);
|
||||
}
|
||||
} else if (mux == 0x54) {
|
||||
hall_sensor_type = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||||
fan_start_setting_value = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||||
ptc_heating_start_setting_value = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||||
default_channel_state = rx_frame.data.u8[7];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void send_can_battery() {
|
||||
unsigned long currentMillis = millis();
|
||||
// Send 10s CAN Message
|
||||
if (currentMillis - previousMillis10s >= INTERVAL_10_S) {
|
||||
|
||||
previousMillis10s = currentMillis;
|
||||
|
||||
if (datalayer.battery.status.bms_status == FAULT) {
|
||||
// Incase we loose BMS comms, resend CAN start
|
||||
setup_completed = false;
|
||||
}
|
||||
|
||||
if (!setup_completed) {
|
||||
transmit_can(&RJXZS_10, can_config.battery); // Communication connected flag
|
||||
transmit_can(&RJXZS_1C, can_config.battery); // CAN OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setup_battery(void) { // Performs one time setup at startup
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("RJXZS BMS selected");
|
||||
#endif
|
||||
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
|
||||
}
|
||||
|
||||
#endif // RJXZS_BMS
|
24
Software/src/battery/RJXZS-BMS.h
Normal file
24
Software/src/battery/RJXZS-BMS.h
Normal file
|
@ -0,0 +1,24 @@
|
|||
#ifndef RJXZS_BMS_H
|
||||
#define RJXZS_BMS_H
|
||||
#include <Arduino.h>
|
||||
#include "../include.h"
|
||||
|
||||
/* Tweak these according to your battery build */
|
||||
#define MAX_PACK_VOLTAGE_DV 5000 //5000 = 500.0V
|
||||
#define MIN_PACK_VOLTAGE_DV 1500
|
||||
#define MAX_CELL_VOLTAGE_MV 4250 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value
|
||||
#define MAX_CELL_DEVIATION_MV 250
|
||||
#define MAX_DISCHARGE_POWER_ALLOWED_W 5000
|
||||
#define MAX_CHARGE_POWER_ALLOWED_W 5000
|
||||
#define MAX_CHARGE_POWER_WHEN_TOPBALANCING_W 500
|
||||
#define RAMPDOWN_SOC 9000 // (90.00) SOC% to start ramping down from max charge power towards 0 at 100.00%
|
||||
|
||||
/* Do not modify any rows below*/
|
||||
#define BATTERY_SELECTED
|
||||
#define NATIVECAN_250KBPS
|
||||
|
||||
void setup_battery(void);
|
||||
void transmit_can(CAN_frame* tx_frame, int interface);
|
||||
|
||||
#endif
|
|
@ -410,8 +410,10 @@ void setup_battery(void) { // Performs one time setup at startup
|
|||
Serial.println("Hyundai Santa Fe PHEV battery selected");
|
||||
#endif
|
||||
datalayer.battery.info.number_of_cells = 96;
|
||||
datalayer.battery.info.max_design_voltage_dV = 4040;
|
||||
datalayer.battery.info.min_design_voltage_dV = 2880;
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -4,7 +4,11 @@
|
|||
#include "../include.h"
|
||||
|
||||
#define BATTERY_SELECTED
|
||||
#define MAX_PACK_VOLTAGE_DV 4040 //5000 = 500.0V
|
||||
#define MIN_PACK_VOLTAGE_DV 2880
|
||||
#define MAX_CELL_DEVIATION_MV 250
|
||||
#define MAX_CELL_VOLTAGE_MV 4250 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value
|
||||
|
||||
uint8_t CalculateCRC8(CAN_frame rx_frame);
|
||||
void setup_battery(void);
|
||||
|
|
|
@ -4,9 +4,6 @@
|
|||
#define SERIAL_LINK_RECEIVER_FROM_BATTERY_H
|
||||
|
||||
#define BATTERY_SELECTED
|
||||
#ifndef MAX_CELL_DEVIATION_MV
|
||||
#define MAX_CELL_DEVIATION_MV 9999
|
||||
#endif
|
||||
|
||||
#include "../include.h"
|
||||
#include "../lib/mackelec-SerialDataLink/SerialDataLink.h"
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
#include "../include.h"
|
||||
#ifdef TESLA_MODEL_3_BATTERY
|
||||
#ifdef TESLA_BATTERY
|
||||
#include "../datalayer/datalayer.h"
|
||||
#include "../devboard/utils/events.h"
|
||||
#include "TESLA-MODEL-3-BATTERY.h"
|
||||
#include "TESLA-BATTERY.h"
|
||||
|
||||
/* Do not change code below unless you are sure what you are doing */
|
||||
/* Credits: Most of the code comes from Per Carlen's bms_comms_tesla_model3.py (https://gitlab.com/pelle8/batt2gen24/) */
|
||||
|
@ -253,16 +253,6 @@ static const char* hvilStatusState[] = {"NOT OK",
|
|||
"UNKNOWN(14)",
|
||||
"UNKNOWN(15)"};
|
||||
|
||||
#define MAX_CELL_VOLTAGE_NCA_NCM 4250 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_NCA_NCM 2950 //Battery is put into emergency stop if one cell goes below this value
|
||||
#define MAX_CELL_DEVIATION_NCA_NCM 500 //LED turns yellow on the board if mv delta exceeds this value
|
||||
|
||||
#define MAX_CELL_VOLTAGE_LFP 3550 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_LFP 2800 //Battery is put into emergency stop if one cell goes below this value
|
||||
#define MAX_CELL_DEVIATION_LFP 200 //LED turns yellow on the board if mv delta exceeds this value
|
||||
|
||||
#define REASONABLE_ENERGYAMOUNT 20 //When the BMS stops making sense on some values, they are always <20
|
||||
|
||||
void update_values_battery() { //This function maps all the values fetched via CAN to the correct parameters used for modbus
|
||||
//After values are mapped, we perform some safety checks, and do some serial printouts
|
||||
|
||||
|
@ -291,7 +281,7 @@ void update_values_battery() { //This function maps all the values fetched via
|
|||
} else if (battery_soc_vi >
|
||||
RAMPDOWN_SOC) { // When real SOC is between RAMPDOWN_SOC-99%, ramp the value between Max<->0
|
||||
datalayer.battery.status.max_charge_power_W =
|
||||
MAXCHARGEPOWERALLOWED * (1 - (battery_soc_vi - RAMPDOWN_SOC) / (1000.0 - RAMPDOWN_SOC));
|
||||
RAMPDOWNPOWERALLOWED * (1 - (battery_soc_vi - RAMPDOWN_SOC) / (1000.0 - RAMPDOWN_SOC));
|
||||
//If the cellvoltages start to reach overvoltage, only allow a small amount of power in
|
||||
if (datalayer.battery.info.chemistry == battery_chemistry_enum::LFP) {
|
||||
if (battery_cell_max_v > (MAX_CELL_VOLTAGE_LFP - FLOAT_START_MV)) {
|
||||
|
@ -316,6 +306,8 @@ void update_values_battery() { //This function maps all the values fetched via
|
|||
|
||||
datalayer.battery.status.cell_min_voltage_mV = battery_cell_min_v;
|
||||
|
||||
battery_cell_deviation_mV = (battery_cell_max_v - battery_cell_min_v);
|
||||
|
||||
/* Value mapping is completed. Start to check all safeties */
|
||||
|
||||
if (battery_hvil_status ==
|
||||
|
@ -325,8 +317,8 @@ void update_values_battery() { //This function maps all the values fetched via
|
|||
clear_event(EVENT_INTERNAL_OPEN_FAULT);
|
||||
}
|
||||
|
||||
battery_cell_deviation_mV = (battery_cell_max_v - battery_cell_min_v);
|
||||
|
||||
#ifdef TESLA_MODEL_3Y_BATTERY
|
||||
// Autodetect algoritm for chemistry on 3/Y packs.
|
||||
// NCM/A batteries have 96s, LFP has 102-106s
|
||||
// Drawback with this check is that it takes 3-5minutes before all cells have been counted!
|
||||
if (datalayer.battery.info.number_of_cells > 101) {
|
||||
|
@ -335,49 +327,19 @@ void update_values_battery() { //This function maps all the values fetched via
|
|||
|
||||
//Once cell chemistry is determined, set maximum and minimum total pack voltage safety limits
|
||||
if (datalayer.battery.info.chemistry == battery_chemistry_enum::LFP) {
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_LFP;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_LFP;
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_3Y_LFP;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_3Y_LFP;
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_LFP;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_LFP;
|
||||
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_LFP;
|
||||
} else { // NCM/A chemistry
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_NCMA;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_NCMA;
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_3Y_NCMA;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_3Y_NCMA;
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_NCA_NCM;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_NCA_NCM;
|
||||
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_NCA_NCM;
|
||||
}
|
||||
|
||||
//Check if SOC% is plausible
|
||||
if (datalayer.battery.status.voltage_dV >
|
||||
(datalayer.battery.info.max_design_voltage_dV -
|
||||
20)) { // When pack voltage is close to max, and SOC% is still low, raise FAULT
|
||||
if (datalayer.battery.status.real_soc < 5000) { //When SOC is less than 50.00% when approaching max voltage
|
||||
set_event(EVENT_SOC_PLAUSIBILITY_ERROR, datalayer.battery.status.real_soc / 100);
|
||||
}
|
||||
}
|
||||
|
||||
if (datalayer.battery.info.chemistry == battery_chemistry_enum::LFP) { //LFP limits used for voltage safeties
|
||||
if (battery_cell_max_v >= MAX_CELL_VOLTAGE_LFP) {
|
||||
set_event(EVENT_CELL_OVER_VOLTAGE, (battery_cell_max_v - MAX_CELL_VOLTAGE_LFP));
|
||||
}
|
||||
if (battery_cell_min_v <= MIN_CELL_VOLTAGE_LFP) {
|
||||
set_event(EVENT_CELL_UNDER_VOLTAGE, (MIN_CELL_VOLTAGE_LFP - battery_cell_min_v));
|
||||
}
|
||||
if (battery_cell_deviation_mV > MAX_CELL_DEVIATION_LFP) {
|
||||
set_event(EVENT_CELL_DEVIATION_HIGH, battery_cell_deviation_mV);
|
||||
} else {
|
||||
clear_event(EVENT_CELL_DEVIATION_HIGH);
|
||||
}
|
||||
} else { //NCA/NCM limits used
|
||||
if (battery_cell_max_v >= MAX_CELL_VOLTAGE_NCA_NCM) {
|
||||
set_event(EVENT_CELL_OVER_VOLTAGE, (battery_cell_max_v - MAX_CELL_VOLTAGE_NCA_NCM));
|
||||
}
|
||||
if (battery_cell_min_v <= MIN_CELL_VOLTAGE_NCA_NCM) {
|
||||
set_event(EVENT_CELL_UNDER_VOLTAGE, (MIN_CELL_VOLTAGE_NCA_NCM - battery_cell_min_v));
|
||||
}
|
||||
if (battery_cell_deviation_mV > MAX_CELL_DEVIATION_NCA_NCM) {
|
||||
set_event(EVENT_CELL_DEVIATION_HIGH, battery_cell_deviation_mV);
|
||||
} else {
|
||||
clear_event(EVENT_CELL_DEVIATION_HIGH);
|
||||
}
|
||||
}
|
||||
|
||||
/* Safeties verified. Perform USB serial printout if configured to do so */
|
||||
#endif // TESLA_MODEL_3Y_BATTERY
|
||||
|
||||
#ifdef DEBUG_VIA_USB
|
||||
|
||||
|
@ -895,6 +857,8 @@ void update_values_battery2() { //This function maps all the values fetched via
|
|||
|
||||
datalayer.battery2.status.cell_min_voltage_mV = battery2_cell_min_v;
|
||||
|
||||
battery2_cell_deviation_mV = (battery2_cell_max_v - battery2_cell_min_v);
|
||||
|
||||
/* Value mapping is completed. Start to check all safeties */
|
||||
|
||||
if (battery2_hvil_status ==
|
||||
|
@ -904,8 +868,8 @@ void update_values_battery2() { //This function maps all the values fetched via
|
|||
clear_event(EVENT_INTERNAL_OPEN_FAULT);
|
||||
}
|
||||
|
||||
battery2_cell_deviation_mV = (battery2_cell_max_v - battery2_cell_min_v);
|
||||
|
||||
#ifdef TESLA_MODEL_3Y_BATTERY
|
||||
// Autodetect algoritm for chemistry on 3/Y packs.
|
||||
// NCM/A batteries have 96s, LFP has 102-106s
|
||||
// Drawback with this check is that it takes 3-5minutes before all cells have been counted!
|
||||
if (datalayer.battery2.info.number_of_cells > 101) {
|
||||
|
@ -914,56 +878,19 @@ void update_values_battery2() { //This function maps all the values fetched via
|
|||
|
||||
//Once cell chemistry is determined, set maximum and minimum total pack voltage safety limits
|
||||
if (datalayer.battery2.info.chemistry == battery_chemistry_enum::LFP) {
|
||||
datalayer.battery2.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_LFP;
|
||||
datalayer.battery2.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_LFP;
|
||||
datalayer.battery2.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_3Y_LFP;
|
||||
datalayer.battery2.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_3Y_LFP;
|
||||
datalayer.battery2.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_LFP;
|
||||
datalayer.battery2.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_LFP;
|
||||
datalayer.battery2.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_LFP;
|
||||
} else { // NCM/A chemistry
|
||||
datalayer.battery2.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_NCMA;
|
||||
datalayer.battery2.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_NCMA;
|
||||
datalayer.battery2.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_3Y_NCMA;
|
||||
datalayer.battery2.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_3Y_NCMA;
|
||||
datalayer.battery2.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_NCA_NCM;
|
||||
datalayer.battery2.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_NCA_NCM;
|
||||
datalayer.battery2.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_NCA_NCM;
|
||||
}
|
||||
|
||||
//Check if SOC% is plausible
|
||||
if (datalayer.battery2.status.voltage_dV >
|
||||
(datalayer.battery2.info.max_design_voltage_dV -
|
||||
20)) { // When pack voltage is close to max, and SOC% is still low, raise FAULT
|
||||
if (datalayer.battery2.status.real_soc < 5000) { //When SOC is less than 50.00% when approaching max voltage
|
||||
set_event(EVENT_SOC_PLAUSIBILITY_ERROR, datalayer.battery2.status.real_soc / 100);
|
||||
}
|
||||
}
|
||||
|
||||
//Check if BMS is in need of recalibration
|
||||
if (battery2_nominal_full_pack_energy > 1 && battery2_nominal_full_pack_energy < REASONABLE_ENERGYAMOUNT) {
|
||||
set_event(EVENT_KWH_PLAUSIBILITY_ERROR, battery2_nominal_full_pack_energy);
|
||||
} else if (battery2_nominal_full_pack_energy <= 1) {
|
||||
set_event(EVENT_KWH_PLAUSIBILITY_ERROR, battery2_nominal_full_pack_energy);
|
||||
}
|
||||
|
||||
if (datalayer.battery2.info.chemistry == battery_chemistry_enum::LFP) { //LFP limits used for voltage safeties
|
||||
if (battery2_cell_max_v >= MAX_CELL_VOLTAGE_LFP) {
|
||||
set_event(EVENT_CELL_OVER_VOLTAGE, (battery2_cell_max_v - MAX_CELL_VOLTAGE_LFP));
|
||||
}
|
||||
if (battery2_cell_min_v <= MIN_CELL_VOLTAGE_LFP) {
|
||||
set_event(EVENT_CELL_UNDER_VOLTAGE, (MIN_CELL_VOLTAGE_LFP - battery2_cell_min_v));
|
||||
}
|
||||
if (battery2_cell_deviation_mV > MAX_CELL_DEVIATION_LFP) {
|
||||
set_event(EVENT_CELL_DEVIATION_HIGH, battery2_cell_deviation_mV);
|
||||
} else {
|
||||
clear_event(EVENT_CELL_DEVIATION_HIGH);
|
||||
}
|
||||
} else { //NCA/NCM limits used
|
||||
if (battery2_cell_max_v >= MAX_CELL_VOLTAGE_NCA_NCM) {
|
||||
set_event(EVENT_CELL_OVER_VOLTAGE, (battery2_cell_max_v - MAX_CELL_VOLTAGE_NCA_NCM));
|
||||
}
|
||||
if (battery2_cell_min_v <= MIN_CELL_VOLTAGE_NCA_NCM) {
|
||||
set_event(EVENT_CELL_UNDER_VOLTAGE, (MIN_CELL_VOLTAGE_NCA_NCM - battery2_cell_min_v));
|
||||
}
|
||||
if (battery2_cell_deviation_mV > MAX_CELL_DEVIATION_NCA_NCM) {
|
||||
set_event(EVENT_CELL_DEVIATION_HIGH, battery2_cell_deviation_mV);
|
||||
} else {
|
||||
clear_event(EVENT_CELL_DEVIATION_HIGH);
|
||||
}
|
||||
}
|
||||
|
||||
/* Safeties verified. Perform USB serial printout if configured to do so */
|
||||
#endif // TESLA_MODEL_3Y_BATTERY
|
||||
|
||||
#ifdef DEBUG_VIA_USB
|
||||
|
||||
|
@ -1239,28 +1166,57 @@ void printDebugIfActive(uint8_t symbol, const char* message) {
|
|||
|
||||
void setup_battery(void) { // Performs one time setup at startup
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Tesla Model 3 battery selected");
|
||||
Serial.println("Tesla Model S/3/X/Y battery selected");
|
||||
#endif
|
||||
|
||||
datalayer.system.status.battery_allows_contactor_closing = true;
|
||||
|
||||
#ifdef TESLA_MODEL_SX_BATTERY // Always use NCM/A mode on S/X packs
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_SX_NCMA;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_SX_NCMA;
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_NCA_NCM;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_NCA_NCM;
|
||||
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_NCA_NCM;
|
||||
#ifdef DOUBLE_BATTERY
|
||||
datalayer.battery2.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_SX_NCMA;
|
||||
datalayer.battery2.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_SX_NCMA;
|
||||
datalayer.battery2.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_NCA_NCM;
|
||||
datalayer.battery2.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_NCA_NCM;
|
||||
datalayer.battery2.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_NCA_NCM;
|
||||
#endif // DOUBLE_BATTERY
|
||||
#endif // TESLA_MODEL_SX_BATTERY
|
||||
|
||||
#ifdef TESLA_MODEL_3Y_BATTERY // Model 3/Y can be either LFP or NCM/A
|
||||
#ifdef LFP_CHEMISTRY
|
||||
datalayer.battery.info.chemistry = battery_chemistry_enum::LFP;
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_LFP;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_LFP;
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_3Y_LFP;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_3Y_LFP;
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_LFP;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_LFP;
|
||||
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_LFP;
|
||||
#ifdef DOUBLE_BATTERY
|
||||
datalayer.battery2.info.chemistry = battery_chemistry_enum::LFP;
|
||||
datalayer.battery2.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_LFP;
|
||||
datalayer.battery2.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_LFP;
|
||||
#endif
|
||||
datalayer.battery2.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_3Y_LFP;
|
||||
datalayer.battery2.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_3Y_LFP;
|
||||
datalayer.battery2.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_LFP;
|
||||
datalayer.battery2.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_LFP;
|
||||
datalayer.battery2.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_LFP;
|
||||
#endif // DOUBLE_BATTERY
|
||||
#else // Startup in NCM/A mode
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_NCMA;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_NCMA;
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_3Y_NCMA;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_3Y_NCMA;
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_NCA_NCM;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_NCA_NCM;
|
||||
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_NCA_NCM;
|
||||
#ifdef DOUBLE_BATTERY
|
||||
datalayer.battery2.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_NCMA;
|
||||
datalayer.battery2.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_NCMA;
|
||||
#endif
|
||||
#endif
|
||||
datalayer.battery2.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_3Y_NCMA;
|
||||
datalayer.battery2.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_3Y_NCMA;
|
||||
datalayer.battery2.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_NCA_NCM;
|
||||
datalayer.battery2.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_NCA_NCM;
|
||||
datalayer.battery2.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_NCA_NCM;
|
||||
#endif // DOUBLE_BATTERY
|
||||
#endif // !LFP_CHEMISTRY
|
||||
#endif // TESLA_MODEL_3Y_BATTERY
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif // TESLA_BATTERY
|
41
Software/src/battery/TESLA-BATTERY.h
Normal file
41
Software/src/battery/TESLA-BATTERY.h
Normal file
|
@ -0,0 +1,41 @@
|
|||
#ifndef TESLA_BATTERY_H
|
||||
#define TESLA_BATTERY_H
|
||||
#include "../include.h"
|
||||
|
||||
#define BATTERY_SELECTED
|
||||
|
||||
/* Modify these if needed */
|
||||
//#define LFP_CHEMISTRY // Enable this line to startup in LFP mode
|
||||
#define MAXCHARGEPOWERALLOWED 15000 // 15000W we use a define since the value supplied by Tesla is always 0
|
||||
#define MAXDISCHARGEPOWERALLOWED 60000 // 60000W we use a define since the value supplied by Tesla is always 0
|
||||
|
||||
/* Do not change the defines below */
|
||||
#define RAMPDOWN_SOC 900 // 90.0 SOC% to start ramping down from max charge power towards 0 at 100.00%
|
||||
#define RAMPDOWNPOWERALLOWED 15000 // What power we ramp down from towards top balancing
|
||||
#define FLOAT_MAX_POWER_W 200 // W, what power to allow for top balancing battery
|
||||
#define FLOAT_START_MV 20 // mV, how many mV under overvoltage to start float charging
|
||||
|
||||
#define MAX_PACK_VOLTAGE_SX_NCMA 4600 // V+1, if pack voltage goes over this, charge stops
|
||||
#define MIN_PACK_VOLTAGE_SX_NCMA 3100 // V+1, if pack voltage goes over this, charge stops
|
||||
#define MAX_PACK_VOLTAGE_3Y_NCMA 4030 // V+1, if pack voltage goes over this, charge stops
|
||||
#define MIN_PACK_VOLTAGE_3Y_NCMA 3100 // V+1, if pack voltage goes below this, discharge stops
|
||||
#define MAX_PACK_VOLTAGE_3Y_LFP 3880 // V+1, if pack voltage goes over this, charge stops
|
||||
#define MIN_PACK_VOLTAGE_3Y_LFP 2968 // V+1, if pack voltage goes below this, discharge stops
|
||||
#define MAX_CELL_DEVIATION_NCA_NCM 500 //LED turns yellow on the board if mv delta exceeds this value
|
||||
#define MAX_CELL_DEVIATION_LFP 200 //LED turns yellow on the board if mv delta exceeds this value
|
||||
#define MAX_CELL_VOLTAGE_NCA_NCM 4250 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_NCA_NCM 2950 //Battery is put into emergency stop if one cell goes below this value
|
||||
#define MAX_CELL_VOLTAGE_LFP 3550 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_LFP 2800 //Battery is put into emergency stop if one cell goes below this value
|
||||
|
||||
void printFaultCodesIfActive();
|
||||
void printDebugIfActive(uint8_t symbol, const char* message);
|
||||
void print_int_with_units(char* header, int value, char* units);
|
||||
void print_SOC(char* header, int SOC);
|
||||
void setup_battery(void);
|
||||
void transmit_can(CAN_frame* tx_frame, int interface);
|
||||
#ifdef DOUBLE_BATTERY
|
||||
void printFaultCodesIfActive_battery2();
|
||||
#endif //DOUBLE_BATTERY
|
||||
|
||||
#endif
|
|
@ -1,31 +0,0 @@
|
|||
#ifndef TESLA_MODEL_3_BATTERY_H
|
||||
#define TESLA_MODEL_3_BATTERY_H
|
||||
#include "../include.h"
|
||||
|
||||
#define BATTERY_SELECTED
|
||||
|
||||
//#define LFP_CHEMISTRY // Enable this line to startup in LFP mode
|
||||
|
||||
#define RAMPDOWN_SOC 900 // 90.0 SOC% to start ramping down from max charge power towards 0 at 100.00%
|
||||
#define MAX_CELL_DEVIATION_MV 9999 // Handled inside the Tesla.cpp file, just for compilation
|
||||
#define FLOAT_MAX_POWER_W 200 // W, what power to allow for top balancing battery
|
||||
#define FLOAT_START_MV 20 // mV, how many mV under overvoltage to start float charging
|
||||
#define MAXCHARGEPOWERALLOWED 15000 // 15000W we use a define since the value supplied by Tesla is always 0
|
||||
#define MAXDISCHARGEPOWERALLOWED \
|
||||
60000 // 60000W we need to cap this value to max 60kW, most inverters overflow otherwise
|
||||
#define MAX_PACK_VOLTAGE_NCMA 4030 // V+1, if pack voltage goes over this, charge stops
|
||||
#define MIN_PACK_VOLTAGE_NCMA 3100 // V+1, if pack voltage goes below this, discharge stops
|
||||
#define MAX_PACK_VOLTAGE_LFP 3880 // V+1, if pack voltage goes over this, charge stops
|
||||
#define MIN_PACK_VOLTAGE_LFP 2968 // V+1, if pack voltage goes below this, discharge stops
|
||||
|
||||
void printFaultCodesIfActive();
|
||||
void printDebugIfActive(uint8_t symbol, const char* message);
|
||||
void print_int_with_units(char* header, int value, char* units);
|
||||
void print_SOC(char* header, int SOC);
|
||||
void setup_battery(void);
|
||||
void transmit_can(CAN_frame* tx_frame, int interface);
|
||||
#ifdef DOUBLE_BATTERY
|
||||
void printFaultCodesIfActive_battery2();
|
||||
#endif //DOUBLE_BATTERY
|
||||
|
||||
#endif
|
|
@ -92,7 +92,7 @@ void send_can_battery() {
|
|||
if (currentMillis - previousMillis100 >= INTERVAL_100_MS) {
|
||||
previousMillis100 = currentMillis;
|
||||
// Put fake messages here incase you want to test sending CAN
|
||||
transmit_can(&TEST, can_config.battery);
|
||||
//transmit_can(&TEST, can_config.battery);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,6 +104,8 @@ void setup_battery(void) { // Performs one time setup at startup
|
|||
datalayer.battery.info.max_design_voltage_dV =
|
||||
4040; // 404.4V, over this, charging is not possible (goes into forced discharge)
|
||||
datalayer.battery.info.min_design_voltage_dV = 2450; // 245.0V under this, discharging further is disabled
|
||||
|
||||
datalayer.system.status.battery_allows_contactor_closing = true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -8,9 +8,6 @@
|
|||
static unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send
|
||||
static unsigned long previousMillis60s = 0; // will store last time a 60s CAN Message was send
|
||||
|
||||
#define MAX_CELL_VOLTAGE 4210 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE 2700 //Battery is put into emergency stop if one cell goes below this value
|
||||
|
||||
static float BATT_U = 0; //0x3A
|
||||
static float MAX_U = 0; //0x3A
|
||||
static float MIN_U = 0; //0x3A
|
||||
|
@ -22,8 +19,8 @@ static float BATT_T_MIN = 0; //0x413
|
|||
static float BATT_T_AVG = 0; //0x413
|
||||
static uint16_t SOC_BMS = 0; //0X37D
|
||||
static uint16_t SOC_CALC = 0;
|
||||
static uint16_t CELL_U_MAX = 0; //0x37D
|
||||
static uint16_t CELL_U_MIN = 0; //0x37D
|
||||
static uint16_t CELL_U_MAX = 3700; //0x37D
|
||||
static uint16_t CELL_U_MIN = 3700; //0x37D
|
||||
static uint8_t CELL_ID_U_MAX = 0; //0x37D
|
||||
static uint16_t HvBattPwrLimDchaSoft = 0; //0x369
|
||||
static uint8_t batteryModuleNumber = 0x10; // First battery module
|
||||
|
@ -288,12 +285,6 @@ void receive_can_battery(CAN_frame rx_frame) {
|
|||
min_max_voltage[1] = cell_voltages[cellcounter];
|
||||
}
|
||||
|
||||
if (min_max_voltage[1] >= MAX_CELL_VOLTAGE) {
|
||||
set_event(EVENT_CELL_OVER_VOLTAGE, 0);
|
||||
}
|
||||
if (min_max_voltage[0] <= MIN_CELL_VOLTAGE) {
|
||||
set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
|
||||
}
|
||||
transmit_can(&VOLVO_SOH_Req, can_config.battery); //Send SOH read request
|
||||
}
|
||||
rxConsecutiveFrames = 0;
|
||||
|
@ -347,8 +338,10 @@ void setup_battery(void) { // Performs one time setup at startup
|
|||
#endif
|
||||
|
||||
datalayer.battery.info.number_of_cells = 108;
|
||||
datalayer.battery.info.max_design_voltage_dV =
|
||||
4540; // 454.0V, over this, charging is not possible (goes into forced discharge)
|
||||
datalayer.battery.info.min_design_voltage_dV = 2938; // 293.8V under this, discharging further is disabled
|
||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;
|
||||
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
|
||||
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV;
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -4,7 +4,11 @@
|
|||
#include "../include.h"
|
||||
|
||||
#define BATTERY_SELECTED
|
||||
#define MAX_PACK_VOLTAGE_DV 4540 //5000 = 500.0V
|
||||
#define MIN_PACK_VOLTAGE_DV 2938
|
||||
#define MAX_CELL_DEVIATION_MV 250
|
||||
#define MAX_CELL_VOLTAGE_MV 4210 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value
|
||||
|
||||
void setup_battery(void);
|
||||
void transmit_can(CAN_frame* tx_frame, int interface);
|
||||
|
|
|
@ -13,6 +13,12 @@ typedef struct {
|
|||
uint16_t max_design_voltage_dV = 5000;
|
||||
/** The minimum intended packvoltage, in deciVolt. 3300 = 330.0 V */
|
||||
uint16_t min_design_voltage_dV = 2500;
|
||||
/** The maximum cellvoltage before shutting down, in milliVolt. 4300 = 4.250 V */
|
||||
uint16_t max_cell_voltage_mV = 4300;
|
||||
/** The minimum cellvoltage before shutting down, in milliVolt. 2700 = 2.700 V */
|
||||
uint16_t min_cell_voltage_mV = 2700;
|
||||
/** The maxumum allowed deviation between cells, in milliVolt. 500 = 0.500 V */
|
||||
uint16_t max_cell_voltage_deviation_mV = 500;
|
||||
/** BYD CAN specific setting, max charge in deciAmpere. 300 = 30.0 A */
|
||||
uint16_t max_charge_amp_dA = BATTERY_MAX_CHARGE_AMP;
|
||||
/** BYD CAN specific setting, max discharge in deciAmpere. 300 = 30.0 A */
|
||||
|
@ -159,15 +165,26 @@ typedef struct {
|
|||
*/
|
||||
int64_t time_snap_cantx_us = 0;
|
||||
#endif
|
||||
/** uint8_t */
|
||||
/** A counter set each time a new message comes from inverter.
|
||||
* This value then gets decremented each 5 seconds. Incase we reach 0
|
||||
* we report the inverter as missing entirely on the CAN bus.
|
||||
*/
|
||||
uint8_t CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
/** True if the battery allows for the contactors to close */
|
||||
bool battery_allows_contactor_closing = false;
|
||||
/** True if the second battery allows for the contactors to close */
|
||||
bool battery2_allows_contactor_closing = false;
|
||||
/** True if the inverter allows for the contactors to close */
|
||||
bool inverter_allows_contactor_closing = true;
|
||||
#ifdef CONTACTOR_CONTROL
|
||||
/** True if the contactor controlled by battery-emulator is closed */
|
||||
bool contactor_control_closed = false;
|
||||
#endif
|
||||
} DATALAYER_SYSTEM_STATUS_TYPE;
|
||||
|
||||
typedef struct {
|
||||
bool equipment_stop_active = false;
|
||||
} DATALAYER_SYSTEM_SETTINGS_TYPE;
|
||||
|
||||
typedef struct {
|
||||
|
|
|
@ -65,6 +65,9 @@
|
|||
#define LED_PIN 4
|
||||
#define LED_MAX_BRIGHTNESS 40
|
||||
|
||||
// Equipment stop pin
|
||||
#define EQUIPMENT_STOP_PIN 35
|
||||
|
||||
/* ----- Error checks below, don't change (can't be moved to separate file) ----- */
|
||||
#ifndef HW_CONFIGURED
|
||||
#define HW_CONFIGURED
|
||||
|
@ -78,6 +81,18 @@
|
|||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef EQUIPMENT_STOP_BUTTON
|
||||
#ifdef DUAL_CAN
|
||||
#error EQUIPMENT_STOP_BUTTON and DUAL_CAN cannot coexist due to overlapping GPIO pin usage
|
||||
#endif
|
||||
#ifdef CAN_FD
|
||||
#error EQUIPMENT_STOP_BUTTON and CAN_FD cannot coexist due to overlapping GPIO pin usage
|
||||
#endif
|
||||
#ifdef CHADEMO_BATTERY
|
||||
#error EQUIPMENT_STOP_BUTTON and CHADEMO_BATTERY cannot coexist due to overlapping GPIO pin usage
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef BMW_I3_BATTERY
|
||||
#ifdef CONTACTOR_CONTROL
|
||||
#error GPIO PIN 25 cannot be used for both BMWi3 Wakeup and contactor control. Disable CONTACTOR_CONTROL
|
||||
|
|
|
@ -58,6 +58,9 @@ GPIOs on extra header
|
|||
#define LED_PIN 4
|
||||
#define LED_MAX_BRIGHTNESS 40
|
||||
|
||||
// Equipment stop pin
|
||||
#define EQUIPMENT_STOP_PIN 2
|
||||
|
||||
/* ----- Error checks below, don't change (can't be moved to separate file) ----- */
|
||||
#ifndef HW_CONFIGURED
|
||||
#define HW_CONFIGURED
|
||||
|
|
|
@ -7,36 +7,162 @@
|
|||
#include "../../datalayer/datalayer.h"
|
||||
#include "../../lib/bblanchon-ArduinoJson/ArduinoJson.h"
|
||||
#include "../../lib/knolleary-pubsubclient/PubSubClient.h"
|
||||
#include "../utils/events.h"
|
||||
#include "../utils/timer.h"
|
||||
|
||||
WiFiClient espClient;
|
||||
PubSubClient client(espClient);
|
||||
char mqtt_msg[MQTT_MSG_BUFFER_SIZE];
|
||||
static unsigned long previousMillisUpdateVal;
|
||||
MyTimer publish_global_timer(5000);
|
||||
MyTimer publish_global_timer(5000); //publish timer
|
||||
MyTimer check_global_timer(800); // check timmer - low-priority MQTT checks, where responsiveness is not critical.
|
||||
static const char* hostname = WiFi.getHostname();
|
||||
|
||||
// Tracking reconnection attempts and failures
|
||||
static unsigned long lastReconnectAttempt = 0;
|
||||
static uint8_t reconnectAttempts = 0;
|
||||
static const uint8_t maxReconnectAttempts = 5;
|
||||
static bool connected_once = false;
|
||||
|
||||
static void publish_common_info(void);
|
||||
static void publish_cell_voltages(void);
|
||||
static void publish_events(void);
|
||||
|
||||
/** Publish global values and call callbacks for specific modules */
|
||||
static void publish_values(void) {
|
||||
publish_events();
|
||||
publish_common_info();
|
||||
publish_cell_voltages();
|
||||
}
|
||||
|
||||
#ifdef HA_AUTODISCOVERY
|
||||
struct SensorConfig {
|
||||
const char* object_id;
|
||||
const char* name;
|
||||
const char* value_template;
|
||||
const char* unit;
|
||||
const char* device_class;
|
||||
};
|
||||
|
||||
SensorConfig sensorConfigs[] = {
|
||||
{"SOC", "Battery Emulator SOC (scaled)", "{{ value_json.SOC }}", "%", "battery"},
|
||||
{"SOC_real", "Battery Emulator SOC (real)", "{{ value_json.SOC_real }}", "%", "battery"},
|
||||
{"state_of_health", "Battery Emulator State Of Health", "{{ value_json.state_of_health }}", "%", "battery"},
|
||||
{"temperature_min", "Battery Emulator Temperature Min", "{{ value_json.temperature_min }}", "°C", "temperature"},
|
||||
{"temperature_max", "Battery Emulator Temperature Max", "{{ value_json.temperature_max }}", "°C", "temperature"},
|
||||
{"stat_batt_power", "Battery Emulator Stat Batt Power", "{{ value_json.stat_batt_power }}", "W", "power"},
|
||||
{"battery_current", "Battery Emulator Battery Current", "{{ value_json.battery_current }}", "A", "current"},
|
||||
{"cell_max_voltage", "Battery Emulator Cell Max Voltage", "{{ value_json.cell_max_voltage }}", "V", "voltage"},
|
||||
{"cell_min_voltage", "Battery Emulator Cell Min Voltage", "{{ value_json.cell_min_voltage }}", "V", "voltage"},
|
||||
{"battery_voltage", "Battery Emulator Battery Voltage", "{{ value_json.battery_voltage }}", "V", "voltage"},
|
||||
{"total_capacity", "Battery Emulator Battery Total Capacity", "{{ value_json.total_capacity }}", "Wh", "energy"},
|
||||
{"remaining_capacity", "Battery Emulator Battery Remaining Capacity", "{{ value_json.remaining_capacity }}", "Wh",
|
||||
"energy"},
|
||||
{"max_discharge_power", "Battery Emulator Battery Max Discharge Power", "{{ value_json.max_discharge_power }}", "W",
|
||||
"power"},
|
||||
{"max_charge_power", "Battery Emulator Battery Max Charge Power", "{{ value_json.max_charge_power }}", "W",
|
||||
"power"},
|
||||
{"bms_status", "Battery Emulator BMS Status", "{{ value_json.bms_status }}", "", ""},
|
||||
{"pause_status", "Battery Emulator Pause Status", "{{ value_json.pause_status }}", "", ""},
|
||||
|
||||
};
|
||||
|
||||
static String generateCommonInfoAutoConfigTopic(const char* object_id, const char* hostname) {
|
||||
return String("homeassistant/sensor/battery-emulator_") + String(hostname) + "/" + String(object_id) + "/config";
|
||||
}
|
||||
|
||||
static String generateCellVoltageAutoConfigTopic(int cell_number, const char* hostname) {
|
||||
return String("homeassistant/sensor/battery-emulator_") + String(hostname) + "/cell_voltage" + String(cell_number) +
|
||||
"/config";
|
||||
}
|
||||
|
||||
static String generateEventsAutoConfigTopic(const char* object_id, const char* hostname) {
|
||||
return String("homeassistant/sensor/battery-emulator_") + String(hostname) + "/" + String(object_id) + "/config";
|
||||
}
|
||||
|
||||
#endif // HA_AUTODISCOVERY
|
||||
|
||||
static std::vector<EventData> order_events;
|
||||
|
||||
static void publish_common_info(void) {
|
||||
static JsonDocument doc;
|
||||
#ifdef HA_AUTODISCOVERY
|
||||
static bool mqtt_first_transmission = true;
|
||||
#endif // HA_AUTODISCOVERY
|
||||
static String state_topic = String("battery-emulator_") + String(hostname) + "/info";
|
||||
#ifdef HA_AUTODISCOVERY
|
||||
if (mqtt_first_transmission == true) {
|
||||
mqtt_first_transmission = false;
|
||||
for (int i = 0; i < sizeof(sensorConfigs) / sizeof(sensorConfigs[0]); i++) {
|
||||
SensorConfig& config = sensorConfigs[i];
|
||||
doc["name"] = config.name;
|
||||
doc["state_topic"] = state_topic;
|
||||
doc["unique_id"] = "battery-emulator_" + String(hostname) + "_" + String(config.object_id);
|
||||
doc["object_id"] = String(hostname) + "_" + String(config.object_id);
|
||||
doc["value_template"] = config.value_template;
|
||||
if (config.unit != nullptr && strlen(config.unit) > 0)
|
||||
doc["unit_of_measurement"] = config.unit;
|
||||
if (config.device_class != nullptr && strlen(config.device_class) > 0) {
|
||||
doc["device_class"] = config.device_class;
|
||||
doc["state_class"] = "measurement";
|
||||
}
|
||||
doc["enabled_by_default"] = true;
|
||||
doc["expire_after"] = 240;
|
||||
doc["device"]["identifiers"][0] = "battery-emulator";
|
||||
doc["device"]["manufacturer"] = "DalaTech";
|
||||
doc["device"]["model"] = "BatteryEmulator";
|
||||
doc["device"]["name"] = "BatteryEmulator_" + String(hostname);
|
||||
doc["origin"]["name"] = "BatteryEmulator";
|
||||
doc["origin"]["sw"] = String(version_number) + "-mqtt";
|
||||
doc["origin"]["url"] = "https://github.com/dalathegreat/Battery-Emulator";
|
||||
serializeJson(doc, mqtt_msg);
|
||||
mqtt_publish(generateCommonInfoAutoConfigTopic(config.object_id, hostname).c_str(), mqtt_msg, true);
|
||||
doc.clear();
|
||||
}
|
||||
|
||||
} else {
|
||||
#endif // HA_AUTODISCOVERY
|
||||
doc["bms_status"] = getBMSStatus(datalayer.battery.status.bms_status);
|
||||
doc["pause_status"] = get_emulator_pause_status();
|
||||
|
||||
//only publish these values if BMS is active and we are comunication with the battery (can send CAN messages to the battery)
|
||||
if (datalayer.battery.status.bms_status == ACTIVE && allowed_to_send_CAN && millis() > BOOTUP_TIME) {
|
||||
doc["SOC"] = ((float)datalayer.battery.status.reported_soc) / 100.0;
|
||||
doc["SOC_real"] = ((float)datalayer.battery.status.real_soc) / 100.0;
|
||||
doc["state_of_health"] = ((float)datalayer.battery.status.soh_pptt) / 100.0;
|
||||
doc["temperature_min"] = ((float)((int16_t)datalayer.battery.status.temperature_min_dC)) / 10.0;
|
||||
doc["temperature_max"] = ((float)((int16_t)datalayer.battery.status.temperature_max_dC)) / 10.0;
|
||||
doc["stat_batt_power"] = ((float)((int32_t)datalayer.battery.status.active_power_W));
|
||||
doc["battery_current"] = ((float)((int16_t)datalayer.battery.status.current_dA)) / 10.0;
|
||||
doc["battery_voltage"] = ((float)datalayer.battery.status.voltage_dV) / 10.0;
|
||||
// publish only if cell voltages have been populated...
|
||||
if (datalayer.battery.info.number_of_cells != 0u &&
|
||||
datalayer.battery.status.cell_voltages_mV[datalayer.battery.info.number_of_cells - 1] != 0u) {
|
||||
doc["cell_max_voltage"] = ((float)datalayer.battery.status.cell_max_voltage_mV) / 1000.0;
|
||||
doc["cell_min_voltage"] = ((float)datalayer.battery.status.cell_min_voltage_mV) / 1000.0;
|
||||
}
|
||||
doc["total_capacity"] = ((float)datalayer.battery.info.total_capacity_Wh);
|
||||
doc["remaining_capacity"] = ((float)datalayer.battery.status.remaining_capacity_Wh);
|
||||
doc["max_discharge_power"] = ((float)datalayer.battery.status.max_discharge_power_W);
|
||||
doc["max_charge_power"] = ((float)datalayer.battery.status.max_charge_power_W);
|
||||
}
|
||||
|
||||
serializeJson(doc, mqtt_msg);
|
||||
if (!mqtt_publish(state_topic.c_str(), mqtt_msg, false)) {
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Common info MQTT msg could not be sent");
|
||||
#endif // DEBUG_VIA_USB
|
||||
}
|
||||
doc.clear();
|
||||
#ifdef HA_AUTODISCOVERY
|
||||
}
|
||||
#endif // HA_AUTODISCOVERY
|
||||
}
|
||||
|
||||
static void publish_cell_voltages(void) {
|
||||
#ifdef HA_AUTODISCOVERY
|
||||
static bool mqtt_first_transmission = true;
|
||||
#endif // HA_AUTODISCOVERY
|
||||
static JsonDocument doc;
|
||||
static const char* hostname = WiFi.getHostname();
|
||||
static String state_topic = String("battery-emulator_") + String(hostname) + "/spec_data";
|
||||
|
||||
// If the cell voltage number isn't initialized...
|
||||
|
@ -99,55 +225,27 @@ static void publish_cell_voltages(void) {
|
|||
#endif // HA_AUTODISCOVERY
|
||||
}
|
||||
|
||||
#ifdef HA_AUTODISCOVERY
|
||||
struct SensorConfig {
|
||||
const char* object_id;
|
||||
const char* name;
|
||||
const char* value_template;
|
||||
const char* unit;
|
||||
const char* device_class;
|
||||
};
|
||||
void publish_events() {
|
||||
|
||||
SensorConfig sensorConfigs[] = {
|
||||
{"SOC", "Battery Emulator SOC (scaled)", "{{ value_json.SOC }}", "%", "battery"},
|
||||
{"SOC_real", "Battery Emulator SOC (real)", "{{ value_json.SOC_real }}", "%", "battery"},
|
||||
{"state_of_health", "Battery Emulator State Of Health", "{{ value_json.state_of_health }}", "%", "battery"},
|
||||
{"temperature_min", "Battery Emulator Temperature Min", "{{ value_json.temperature_min }}", "°C", "temperature"},
|
||||
{"temperature_max", "Battery Emulator Temperature Max", "{{ value_json.temperature_max }}", "°C", "temperature"},
|
||||
{"stat_batt_power", "Battery Emulator Stat Batt Power", "{{ value_json.stat_batt_power }}", "W", "power"},
|
||||
{"battery_current", "Battery Emulator Battery Current", "{{ value_json.battery_current }}", "A", "current"},
|
||||
{"cell_max_voltage", "Battery Emulator Cell Max Voltage", "{{ value_json.cell_max_voltage }}", "V", "voltage"},
|
||||
{"cell_min_voltage", "Battery Emulator Cell Min Voltage", "{{ value_json.cell_min_voltage }}", "V", "voltage"},
|
||||
{"battery_voltage", "Battery Emulator Battery Voltage", "{{ value_json.battery_voltage }}", "V", "voltage"},
|
||||
};
|
||||
|
||||
static String generateCommonInfoAutoConfigTopic(const char* object_id, const char* hostname) {
|
||||
return String("homeassistant/sensor/battery-emulator_") + String(hostname) + "/" + String(object_id) + "/config";
|
||||
}
|
||||
#endif // HA_AUTODISCOVERY
|
||||
|
||||
static void publish_common_info(void) {
|
||||
static JsonDocument doc;
|
||||
#ifdef HA_AUTODISCOVERY
|
||||
static bool mqtt_first_transmission = true;
|
||||
#endif // HA_AUTODISCOVERY
|
||||
static const char* hostname = WiFi.getHostname();
|
||||
static String state_topic = String("battery-emulator_") + String(hostname) + "/info";
|
||||
static String state_topic = String("battery-emulator_") + String(hostname) + "/events";
|
||||
#ifdef HA_AUTODISCOVERY
|
||||
if (mqtt_first_transmission == true) {
|
||||
mqtt_first_transmission = false;
|
||||
for (int i = 0; i < sizeof(sensorConfigs) / sizeof(sensorConfigs[0]); i++) {
|
||||
SensorConfig& config = sensorConfigs[i];
|
||||
doc["name"] = config.name;
|
||||
|
||||
doc["name"] = "Battery Emulator Event";
|
||||
doc["state_topic"] = state_topic;
|
||||
doc["unique_id"] = "battery-emulator_" + String(hostname) + "_" + String(config.object_id);
|
||||
doc["object_id"] = String(hostname) + "_" + String(config.object_id);
|
||||
doc["value_template"] = config.value_template;
|
||||
doc["unit_of_measurement"] = config.unit;
|
||||
doc["device_class"] = config.device_class;
|
||||
doc["unique_id"] = "battery-emulator_" + String(hostname) + "_event";
|
||||
doc["object_id"] = String(hostname) + "_event";
|
||||
doc["value_template"] =
|
||||
"{{ value_json.event_type ~ ' (c:' ~ value_json.count ~ ',m:' ~ value_json.millis ~ ') ' ~ value_json.message "
|
||||
"}}";
|
||||
doc["json_attributes_topic"] = state_topic;
|
||||
doc["json_attributes_template"] = "{{ value_json | tojson }}";
|
||||
doc["enabled_by_default"] = true;
|
||||
doc["state_class"] = "measurement";
|
||||
doc["expire_after"] = 240;
|
||||
doc["device"]["identifiers"][0] = "battery-emulator";
|
||||
doc["device"]["manufacturer"] = "DalaTech";
|
||||
doc["device"]["model"] = "BatteryEmulator";
|
||||
|
@ -156,52 +254,77 @@ static void publish_common_info(void) {
|
|||
doc["origin"]["sw"] = String(version_number) + "-mqtt";
|
||||
doc["origin"]["url"] = "https://github.com/dalathegreat/Battery-Emulator";
|
||||
serializeJson(doc, mqtt_msg);
|
||||
mqtt_publish(generateCommonInfoAutoConfigTopic(config.object_id, hostname).c_str(), mqtt_msg, true);
|
||||
}
|
||||
mqtt_publish(generateEventsAutoConfigTopic("event", hostname).c_str(), mqtt_msg, true);
|
||||
|
||||
doc.clear();
|
||||
} else {
|
||||
#endif // HA_AUTODISCOVERY
|
||||
doc["SOC"] = ((float)datalayer.battery.status.reported_soc) / 100.0;
|
||||
doc["SOC_real"] = ((float)datalayer.battery.status.real_soc) / 100.0;
|
||||
doc["state_of_health"] = ((float)datalayer.battery.status.soh_pptt) / 100.0;
|
||||
doc["temperature_min"] = ((float)((int16_t)datalayer.battery.status.temperature_min_dC)) / 10.0;
|
||||
doc["temperature_max"] = ((float)((int16_t)datalayer.battery.status.temperature_max_dC)) / 10.0;
|
||||
doc["stat_batt_power"] = ((float)((int32_t)datalayer.battery.status.active_power_W));
|
||||
doc["battery_current"] = ((float)((int16_t)datalayer.battery.status.current_dA)) / 10.0;
|
||||
doc["battery_voltage"] = ((float)datalayer.battery.status.voltage_dV) / 10.0;
|
||||
// publish only if cell voltages have been populated...
|
||||
if (datalayer.battery.info.number_of_cells != 0u &&
|
||||
datalayer.battery.status.cell_voltages_mV[datalayer.battery.info.number_of_cells - 1] != 0u) {
|
||||
doc["cell_max_voltage"] = ((float)datalayer.battery.status.cell_max_voltage_mV) / 1000.0;
|
||||
doc["cell_min_voltage"] = ((float)datalayer.battery.status.cell_min_voltage_mV) / 1000.0;
|
||||
|
||||
const EVENTS_STRUCT_TYPE* event_pointer;
|
||||
|
||||
//clear the vector
|
||||
order_events.clear();
|
||||
// Collect all events
|
||||
for (int i = 0; i < EVENT_NOF_EVENTS; i++) {
|
||||
event_pointer = get_event_pointer((EVENTS_ENUM_TYPE)i);
|
||||
if (event_pointer->occurences > 0 && !event_pointer->MQTTpublished) {
|
||||
order_events.push_back({static_cast<EVENTS_ENUM_TYPE>(i), event_pointer});
|
||||
}
|
||||
}
|
||||
// Sort events by timestamp
|
||||
std::sort(order_events.begin(), order_events.end(), compareEventsByTimestampAsc);
|
||||
|
||||
for (const auto& event : order_events) {
|
||||
|
||||
EVENTS_ENUM_TYPE event_handle = event.event_handle;
|
||||
event_pointer = event.event_pointer;
|
||||
|
||||
doc["event_type"] = String(get_event_enum_string(event_handle));
|
||||
doc["severity"] = String(get_event_level_string(event_handle));
|
||||
doc["count"] = String(event_pointer->occurences);
|
||||
doc["data"] = String(event_pointer->data);
|
||||
doc["message"] = String(get_event_message_string(event_handle));
|
||||
doc["millis"] = String(event_pointer->timestamp);
|
||||
|
||||
serializeJson(doc, mqtt_msg);
|
||||
if (!mqtt_publish(state_topic.c_str(), mqtt_msg, false)) {
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Common info MQTT msg could not be sent");
|
||||
#endif // DEBUG_VIA_USB
|
||||
} else {
|
||||
set_event_MQTTpublished(event_handle);
|
||||
}
|
||||
doc.clear();
|
||||
//clear the vector
|
||||
order_events.clear();
|
||||
}
|
||||
#ifdef HA_AUTODISCOVERY
|
||||
}
|
||||
#endif // HA_AUTODISCOVERY
|
||||
}
|
||||
|
||||
/* If we lose the connection, get it back */
|
||||
static void reconnect() {
|
||||
static bool reconnect() {
|
||||
// attempt one reconnection
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.print("Attempting MQTT connection... ");
|
||||
#endif // DEBUG_VIA_USB
|
||||
const char* hostname = WiFi.getHostname();
|
||||
char clientId[64]; // Adjust the size as needed
|
||||
snprintf(clientId, sizeof(clientId), "LilyGoClient-%s", hostname);
|
||||
// Attempt to connect
|
||||
if (client.connect(clientId, mqtt_user, mqtt_password)) {
|
||||
connected_once = true;
|
||||
clear_event(EVENT_MQTT_DISCONNECT);
|
||||
set_event(EVENT_MQTT_CONNECT, 0);
|
||||
reconnectAttempts = 0; // Reset attempts on successful connection
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("connected");
|
||||
#endif // DEBUG_VIA_USB
|
||||
clear_event(EVENT_MQTT_CONNECT);
|
||||
} else {
|
||||
if (connected_once)
|
||||
set_event(EVENT_MQTT_DISCONNECT, 0);
|
||||
reconnectAttempts++; // Count failed attempts
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.print("failed, rc=");
|
||||
Serial.print(client.state());
|
||||
|
@ -209,6 +332,7 @@ static void reconnect() {
|
|||
#endif // DEBUG_VIA_USB
|
||||
// Wait 5 seconds before retrying
|
||||
}
|
||||
return client.connected();
|
||||
}
|
||||
|
||||
void init_mqtt(void) {
|
||||
|
@ -217,22 +341,38 @@ void init_mqtt(void) {
|
|||
Serial.println("MQTT initialized");
|
||||
#endif // DEBUG_VIA_USB
|
||||
|
||||
previousMillisUpdateVal = millis();
|
||||
client.setKeepAlive(30); // Increase keepalive to manage network latency better. default is 15
|
||||
|
||||
lastReconnectAttempt = millis();
|
||||
reconnect();
|
||||
}
|
||||
|
||||
void mqtt_loop(void) {
|
||||
// Only attempt to publish/reconnect MQTT if Wi-Fi is connectedand checkTimmer is elapsed
|
||||
if (check_global_timer.elapsed() && WiFi.status() == WL_CONNECTED) {
|
||||
if (client.connected()) {
|
||||
client.loop();
|
||||
if (publish_global_timer.elapsed() == true) // Every 5s
|
||||
if (publish_global_timer.elapsed()) // Every 5s
|
||||
{
|
||||
publish_values();
|
||||
}
|
||||
} else {
|
||||
if (millis() - previousMillisUpdateVal >= 5000) // Every 5s
|
||||
if (connected_once)
|
||||
set_event(EVENT_MQTT_DISCONNECT, 0);
|
||||
unsigned long now = millis();
|
||||
if (now - lastReconnectAttempt >= 5000) // Every 5s
|
||||
{
|
||||
previousMillisUpdateVal = millis();
|
||||
reconnect();
|
||||
lastReconnectAttempt = now;
|
||||
if (reconnect()) {
|
||||
lastReconnectAttempt = 0;
|
||||
} else if (reconnectAttempts >= maxReconnectAttempts) {
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Too many failed reconnect attempts, restarting client.");
|
||||
#endif
|
||||
client.disconnect(); // Force close the MQTT client connection
|
||||
reconnectAttempts = 0; // Reset attempts to avoid infinite loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -241,5 +381,8 @@ bool mqtt_publish(const char* topic, const char* mqtt_msg, bool retain) {
|
|||
if (client.connected() == true) {
|
||||
return client.publish(topic, mqtt_msg, retain);
|
||||
}
|
||||
if (connected_once)
|
||||
set_event(EVENT_MQTT_DISCONNECT, 0);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
#define __MQTT_H__
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
#include "../../include.h"
|
||||
|
||||
#define MQTT_MSG_BUFFER_SIZE (1024)
|
||||
|
|
|
@ -9,9 +9,23 @@ static bool battery_empty_event_fired = false;
|
|||
|
||||
#define MAX_SOH_DEVIATION_PPTT 2500
|
||||
|
||||
//battery pause status begin
|
||||
bool emulator_pause_request_ON = false;
|
||||
bool emulator_pause_CAN_send_ON = false;
|
||||
bool allowed_to_send_CAN = true;
|
||||
|
||||
battery_pause_status emulator_pause_status = NORMAL;
|
||||
//battery pause status end
|
||||
|
||||
void update_machineryprotection() {
|
||||
// Start checking that the battery is within reason. Incase we see any funny business, raise an event!
|
||||
|
||||
// Pause function is on
|
||||
if (emulator_pause_request_ON) {
|
||||
datalayer.battery.status.max_discharge_power_W = 0;
|
||||
datalayer.battery.status.max_charge_power_W = 0;
|
||||
}
|
||||
|
||||
// Battery is overheated!
|
||||
if (datalayer.battery.status.temperature_max_dC > 500) {
|
||||
set_event(EVENT_BATTERY_OVERHEAT, datalayer.battery.status.temperature_max_dC);
|
||||
|
@ -40,6 +54,15 @@ void update_machineryprotection() {
|
|||
clear_event(EVENT_BATTERY_UNDERVOLTAGE);
|
||||
}
|
||||
|
||||
// Cell overvoltage, critical latching error without automatic reset. Requires user action.
|
||||
if (datalayer.battery.status.cell_max_voltage_mV >= datalayer.battery.info.max_cell_voltage_mV) {
|
||||
set_event(EVENT_CELL_OVER_VOLTAGE, 0);
|
||||
}
|
||||
// Cell undervoltage, critical latching error without automatic reset. Requires user action.
|
||||
if (datalayer.battery.status.cell_min_voltage_mV <= datalayer.battery.info.min_cell_voltage_mV) {
|
||||
set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
|
||||
}
|
||||
|
||||
// Battery is fully charged. Dont allow any more power into it
|
||||
// Normally the BMS will send 0W allowed, but this acts as an additional layer of safety
|
||||
if (datalayer.battery.status.reported_soc == 10000) //Scaled SOC% value is 100.00%
|
||||
|
@ -74,6 +97,7 @@ void update_machineryprotection() {
|
|||
clear_event(EVENT_SOH_LOW);
|
||||
}
|
||||
|
||||
#ifndef PYLON_BATTERY
|
||||
// Check if SOC% is plausible
|
||||
if (datalayer.battery.status.voltage_dV >
|
||||
(datalayer.battery.info.max_design_voltage_dV -
|
||||
|
@ -84,10 +108,11 @@ void update_machineryprotection() {
|
|||
clear_event(EVENT_SOC_PLAUSIBILITY_ERROR);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Check diff between highest and lowest cell
|
||||
cell_deviation_mV = (datalayer.battery.status.cell_max_voltage_mV - datalayer.battery.status.cell_min_voltage_mV);
|
||||
if (cell_deviation_mV > MAX_CELL_DEVIATION_MV) {
|
||||
if (cell_deviation_mV > datalayer.battery.info.max_cell_voltage_deviation_mV) {
|
||||
set_event(EVENT_CELL_DEVIATION_HIGH, (cell_deviation_mV / 20));
|
||||
} else {
|
||||
clear_event(EVENT_CELL_DEVIATION_HIGH);
|
||||
|
@ -136,8 +161,25 @@ void update_machineryprotection() {
|
|||
clear_event(EVENT_CAN_RX_WARNING);
|
||||
}
|
||||
|
||||
#ifdef CAN_INVERTER_SELECTED
|
||||
// Check if the inverter is still sending CAN messages. If we go 60s without messages we raise an error
|
||||
if (!datalayer.system.status.CAN_inverter_still_alive) {
|
||||
set_event(EVENT_CAN_INVERTER_MISSING, 0);
|
||||
} else {
|
||||
datalayer.system.status.CAN_inverter_still_alive--;
|
||||
clear_event(EVENT_CAN_INVERTER_MISSING);
|
||||
}
|
||||
#endif //CAN_INVERTER_SELECTED
|
||||
|
||||
#ifdef DOUBLE_BATTERY // Additional Double-Battery safeties are checked here
|
||||
// Check if the Battery 2 BMS is still sending CAN messages. If we go 60s without messages we raise an error
|
||||
|
||||
// Pause function is on
|
||||
if (emulator_pause_request_ON) {
|
||||
datalayer.battery2.status.max_discharge_power_W = 0;
|
||||
datalayer.battery2.status.max_charge_power_W = 0;
|
||||
}
|
||||
|
||||
if (!datalayer.battery2.status.CAN_battery_still_alive) {
|
||||
set_event(EVENT_CAN2_RX_FAILURE, 0);
|
||||
} else {
|
||||
|
@ -152,6 +194,23 @@ void update_machineryprotection() {
|
|||
clear_event(EVENT_CAN_RX_WARNING);
|
||||
}
|
||||
|
||||
// Cell overvoltage, critical latching error without automatic reset. Requires user action.
|
||||
if (datalayer.battery2.status.cell_max_voltage_mV >= datalayer.battery2.info.max_cell_voltage_mV) {
|
||||
set_event(EVENT_CELL_OVER_VOLTAGE, 0);
|
||||
}
|
||||
// Cell undervoltage, critical latching error without automatic reset. Requires user action.
|
||||
if (datalayer.battery2.status.cell_min_voltage_mV <= datalayer.battery2.info.min_cell_voltage_mV) {
|
||||
set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
|
||||
}
|
||||
|
||||
// Check diff between highest and lowest cell
|
||||
cell_deviation_mV = (datalayer.battery2.status.cell_max_voltage_mV - datalayer.battery2.status.cell_min_voltage_mV);
|
||||
if (cell_deviation_mV > datalayer.battery2.info.max_cell_voltage_deviation_mV) {
|
||||
set_event(EVENT_CELL_DEVIATION_HIGH, (cell_deviation_mV / 20));
|
||||
} else {
|
||||
clear_event(EVENT_CELL_DEVIATION_HIGH);
|
||||
}
|
||||
|
||||
// Check if SOH% between the packs is too large
|
||||
if ((datalayer.battery.status.soh_pptt != 9900) && (datalayer.battery2.status.soh_pptt != 9900)) {
|
||||
// Both values available, check diff
|
||||
|
@ -171,3 +230,96 @@ void update_machineryprotection() {
|
|||
|
||||
#endif // DOUBLE_BATTERY
|
||||
}
|
||||
|
||||
//battery pause status begin
|
||||
void setBatteryPause(bool pause_battery, bool pause_CAN, bool equipment_stop, bool store_settings) {
|
||||
|
||||
// First handle equipment stop / resume
|
||||
if (equipment_stop && !datalayer.system.settings.equipment_stop_active) {
|
||||
datalayer.system.settings.equipment_stop_active = true;
|
||||
if (store_settings) {
|
||||
store_settings_equipment_stop();
|
||||
}
|
||||
|
||||
set_event(EVENT_EQUIPMENT_STOP, 1);
|
||||
} else if (!equipment_stop && datalayer.system.settings.equipment_stop_active) {
|
||||
datalayer.system.settings.equipment_stop_active = false;
|
||||
if (store_settings) {
|
||||
store_settings_equipment_stop();
|
||||
}
|
||||
clear_event(EVENT_EQUIPMENT_STOP);
|
||||
}
|
||||
|
||||
emulator_pause_CAN_send_ON = pause_CAN;
|
||||
|
||||
if (pause_battery) {
|
||||
|
||||
set_event(EVENT_PAUSE_BEGIN, 1);
|
||||
emulator_pause_request_ON = true;
|
||||
emulator_pause_status = PAUSING;
|
||||
datalayer.battery.status.max_discharge_power_W = 0;
|
||||
datalayer.battery.status.max_charge_power_W = 0;
|
||||
#ifdef DOUBLE_BATTERY
|
||||
datalayer.battery2.status.max_discharge_power_W = 0;
|
||||
datalayer.battery2.status.max_charge_power_W = 0;
|
||||
#endif
|
||||
|
||||
} else {
|
||||
clear_event(EVENT_PAUSE_BEGIN);
|
||||
set_event(EVENT_PAUSE_END, 0);
|
||||
emulator_pause_request_ON = false;
|
||||
emulator_pause_CAN_send_ON = false;
|
||||
emulator_pause_status = RESUMING;
|
||||
clear_event(EVENT_PAUSE_END);
|
||||
}
|
||||
|
||||
//immediate check if we can send CAN messages
|
||||
emulator_pause_state_send_CAN_battery();
|
||||
}
|
||||
|
||||
/// @brief handle emulator pause status
|
||||
/// @return true if CAN messages should be sent to battery, false if not
|
||||
void emulator_pause_state_send_CAN_battery() {
|
||||
bool previous_allowed_to_send_CAN = allowed_to_send_CAN;
|
||||
|
||||
if (emulator_pause_status == NORMAL) {
|
||||
allowed_to_send_CAN = true;
|
||||
}
|
||||
|
||||
// in some inverters this values are not accurate, so we need to check if we are consider 1.8 amps as the limit
|
||||
if (emulator_pause_request_ON && emulator_pause_status == PAUSING && datalayer.battery.status.current_dA < 18 &&
|
||||
datalayer.battery.status.current_dA > -18) {
|
||||
emulator_pause_status = PAUSED;
|
||||
}
|
||||
|
||||
if (!emulator_pause_request_ON && emulator_pause_status == RESUMING) {
|
||||
emulator_pause_status = NORMAL;
|
||||
allowed_to_send_CAN = true;
|
||||
}
|
||||
|
||||
allowed_to_send_CAN = (!emulator_pause_CAN_send_ON || emulator_pause_status == NORMAL);
|
||||
|
||||
if (previous_allowed_to_send_CAN && !allowed_to_send_CAN) {
|
||||
//completely force stop the CAN communication
|
||||
ESP32Can.CANStop();
|
||||
} else if (!previous_allowed_to_send_CAN && allowed_to_send_CAN) {
|
||||
//resume CAN communication
|
||||
ESP32Can.CANInit();
|
||||
}
|
||||
}
|
||||
|
||||
std::string get_emulator_pause_status() {
|
||||
switch (emulator_pause_status) {
|
||||
case NORMAL:
|
||||
return "RUNNING";
|
||||
case PAUSING:
|
||||
return "PAUSING";
|
||||
case PAUSED:
|
||||
return "PAUSED";
|
||||
case RESUMING:
|
||||
return "RESUMING";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
//battery pause status
|
||||
|
|
|
@ -1,11 +1,29 @@
|
|||
#ifndef SAFETY_H
|
||||
#define SAFETY_H
|
||||
#include <Arduino.h>
|
||||
#include <string>
|
||||
#include "../../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h"
|
||||
|
||||
#define MAX_CAN_FAILURES 50
|
||||
|
||||
#define MAX_CHARGE_DISCHARGE_LIMIT_FAILURES 1
|
||||
|
||||
//battery pause status begin
|
||||
enum battery_pause_status { NORMAL = 0, PAUSING = 1, PAUSED = 2, RESUMING = 3 };
|
||||
extern bool emulator_pause_request_ON;
|
||||
extern bool emulator_pause_CAN_send_ON;
|
||||
extern battery_pause_status emulator_pause_status;
|
||||
extern bool allowed_to_send_CAN;
|
||||
//battery pause status end
|
||||
|
||||
extern void store_settings_equipment_stop();
|
||||
|
||||
void update_machineryprotection();
|
||||
|
||||
//battery pause status begin
|
||||
void setBatteryPause(bool pause_battery, bool pause_CAN, bool equipment_stop = false, bool store_settings = true);
|
||||
void emulator_pause_state_send_CAN_battery();
|
||||
std::string get_emulator_pause_status();
|
||||
//battery pause status end
|
||||
|
||||
#endif
|
||||
|
|
56
Software/src/devboard/utils/debounce_button.cpp
Normal file
56
Software/src/devboard/utils/debounce_button.cpp
Normal file
|
@ -0,0 +1,56 @@
|
|||
#include "debounce_button.h"
|
||||
|
||||
// Function to initialize the debounced button with pin, switch type, and debounce delay
|
||||
void initDebouncedButton(DebouncedButton& button, int pin, SwitchType type, unsigned long debounceDelay) {
|
||||
button.pin = pin;
|
||||
button.debounceDelay = debounceDelay;
|
||||
button.lastDebounceTime = 0;
|
||||
button.lastButtonState = (type == NC) ? HIGH : LOW; // NC starts HIGH, NO starts LOW
|
||||
button.buttonState = button.lastButtonState;
|
||||
button.switchType = type;
|
||||
pinMode(pin, INPUT); // Setup pin mode
|
||||
}
|
||||
|
||||
ButtonState debounceButton(DebouncedButton& button, unsigned long& timeSincePress) {
|
||||
int reading = digitalRead(button.pin);
|
||||
|
||||
// If the button state has changed due to noise or a press
|
||||
if (reading != button.lastButtonState) {
|
||||
// Reset debounce timer
|
||||
button.lastDebounceTime = millis();
|
||||
}
|
||||
|
||||
// Check if the state change is stable for the debounce delay
|
||||
if ((millis() - button.lastDebounceTime) > button.debounceDelay) {
|
||||
if (reading != button.buttonState) {
|
||||
button.buttonState = reading;
|
||||
|
||||
// Adjust button logic based on switch type (NC or NO)
|
||||
if (button.switchType == NC) {
|
||||
if (button.buttonState == LOW) {
|
||||
// Button pressed for NC, record the press time
|
||||
button.ulPressTime = millis();
|
||||
return PRESSED;
|
||||
} else {
|
||||
// Button released for NC, calculate the time since last press
|
||||
timeSincePress = millis() - button.ulPressTime;
|
||||
return RELEASED;
|
||||
}
|
||||
} else { // NO type
|
||||
if (button.buttonState == HIGH) {
|
||||
// Button pressed for NO, record the press time
|
||||
button.ulPressTime = millis();
|
||||
return PRESSED;
|
||||
} else {
|
||||
// Button released for NO, calculate the time since last press
|
||||
timeSincePress = millis() - button.ulPressTime;
|
||||
return RELEASED;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remember the last button state
|
||||
button.lastButtonState = reading;
|
||||
return NONE; // No change in button state
|
||||
}
|
36
Software/src/devboard/utils/debounce_button.h
Normal file
36
Software/src/devboard/utils/debounce_button.h
Normal file
|
@ -0,0 +1,36 @@
|
|||
#ifndef DEBOUNCE_H
|
||||
#define DEBOUNCE_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// Enum to define switch type (Normally Closed - NC, Normally Open - NO)
|
||||
enum SwitchType {
|
||||
NC, // Normally Closed
|
||||
NO // Normally Open
|
||||
};
|
||||
|
||||
// Enum to define button state
|
||||
enum ButtonState {
|
||||
NONE, // No change in button state
|
||||
PRESSED, // Button is pressed down
|
||||
RELEASED // Button is released
|
||||
};
|
||||
|
||||
// Struct to hold button state and debounce parameters
|
||||
struct DebouncedButton {
|
||||
int pin; // GPIO pin number
|
||||
unsigned long lastDebounceTime; // Time of last state change
|
||||
unsigned long debounceDelay; // Debounce delay time
|
||||
unsigned long ulPressTime; // Time when the button was last pressed
|
||||
bool lastButtonState; // Previous button state
|
||||
bool buttonState; // Current button state
|
||||
SwitchType switchType; // Switch type (NC or NO)
|
||||
};
|
||||
|
||||
// Function to initialize the debounced button
|
||||
void initDebouncedButton(DebouncedButton& button, int pin, SwitchType type, unsigned long debounceDelay = 50);
|
||||
|
||||
// Function to debounce button and return the button state (PRESSED, RELEASED, or NONE)
|
||||
ButtonState debounceButton(DebouncedButton& button, unsigned long& timeSincePress);
|
||||
|
||||
#endif // DEBOUNCE_H
|
|
@ -44,16 +44,14 @@
|
|||
|
||||
typedef struct {
|
||||
EVENTS_ENUM_TYPE event;
|
||||
uint8_t millisrolloverCount;
|
||||
uint32_t timestamp;
|
||||
uint8_t data;
|
||||
} EVENT_LOG_ENTRY_TYPE;
|
||||
|
||||
typedef struct {
|
||||
EVENTS_STRUCT_TYPE entries[EVENT_NOF_EVENTS];
|
||||
unsigned long time_seconds;
|
||||
MyTimer second_timer;
|
||||
MyTimer ee_timer;
|
||||
MyTimer update_timer;
|
||||
EVENTS_LEVEL_TYPE level;
|
||||
uint16_t event_log_head_index;
|
||||
uint16_t event_log_tail_index;
|
||||
|
@ -66,23 +64,31 @@ static EVENT_TYPE events;
|
|||
static const char* EVENTS_ENUM_TYPE_STRING[] = {EVENTS_ENUM_TYPE(GENERATE_STRING)};
|
||||
static const char* EVENTS_LEVEL_TYPE_STRING[] = {EVENTS_LEVEL_TYPE(GENERATE_STRING)};
|
||||
|
||||
static uint32_t lastMillis = millis();
|
||||
|
||||
/* Local function prototypes */
|
||||
static void update_event_time(void);
|
||||
static void set_event(EVENTS_ENUM_TYPE event, uint8_t data, bool latched);
|
||||
static void update_event_level(void);
|
||||
static void update_bms_status(void);
|
||||
|
||||
static void log_event(EVENTS_ENUM_TYPE event, uint8_t data);
|
||||
static void log_event(EVENTS_ENUM_TYPE event, uint8_t millisrolloverCount, uint32_t timestamp, uint8_t data);
|
||||
static void print_event_log(void);
|
||||
static void check_ee_write(void);
|
||||
|
||||
uint8_t millisrolloverCount = 0;
|
||||
|
||||
/* Exported functions */
|
||||
|
||||
/* Main execution function, should handle various continuous functionality */
|
||||
void run_event_handling(void) {
|
||||
update_event_time();
|
||||
uint32_t currentMillis = millis();
|
||||
if (currentMillis < lastMillis) { // Overflow detected
|
||||
millisrolloverCount++;
|
||||
}
|
||||
lastMillis = currentMillis;
|
||||
|
||||
run_sequence_on_target();
|
||||
check_ee_write();
|
||||
//check_ee_write();
|
||||
update_event_level();
|
||||
}
|
||||
|
||||
|
@ -100,7 +106,7 @@ void init_events(void) {
|
|||
EEPROM.writeUShort(EE_EVENT_LOG_TAIL_INDEX_ADDRESS, 0);
|
||||
|
||||
// Prepare an empty event block to write
|
||||
EVENT_LOG_ENTRY_TYPE entry = {.event = EVENT_NOF_EVENTS, .timestamp = 0, .data = 0};
|
||||
EVENT_LOG_ENTRY_TYPE entry = {.event = EVENT_NOF_EVENTS, .millisrolloverCount = 0, .timestamp = 0, .data = 0};
|
||||
|
||||
// Put the event in (what I guess is) the RAM EEPROM mirror, or write buffer
|
||||
|
||||
|
@ -128,22 +134,28 @@ void init_events(void) {
|
|||
for (uint16_t i = 0; i < EVENT_NOF_EVENTS; i++) {
|
||||
events.entries[i].data = 0;
|
||||
events.entries[i].timestamp = 0;
|
||||
events.entries[i].millisrolloverCount = 0;
|
||||
events.entries[i].occurences = 0;
|
||||
events.entries[i].log = true;
|
||||
events.entries[i].MQTTpublished = false; // Not published by default
|
||||
}
|
||||
|
||||
events.entries[EVENT_CANFD_INIT_FAILURE].level = EVENT_LEVEL_WARNING;
|
||||
events.entries[EVENT_CANMCP_INIT_FAILURE].level = EVENT_LEVEL_WARNING;
|
||||
events.entries[EVENT_CANFD_BUFFER_FULL].level = EVENT_LEVEL_WARNING;
|
||||
events.entries[EVENT_CAN_OVERRUN].level = EVENT_LEVEL_INFO;
|
||||
events.entries[EVENT_CAN_RX_FAILURE].level = EVENT_LEVEL_ERROR;
|
||||
events.entries[EVENT_CAN2_RX_FAILURE].level = EVENT_LEVEL_WARNING;
|
||||
events.entries[EVENT_CANFD_RX_FAILURE].level = EVENT_LEVEL_ERROR;
|
||||
events.entries[EVENT_CAN_RX_WARNING].level = EVENT_LEVEL_WARNING;
|
||||
events.entries[EVENT_CAN_TX_FAILURE].level = EVENT_LEVEL_ERROR;
|
||||
events.entries[EVENT_CAN_INVERTER_MISSING].level = EVENT_LEVEL_WARNING;
|
||||
events.entries[EVENT_WATER_INGRESS].level = EVENT_LEVEL_ERROR;
|
||||
events.entries[EVENT_CHARGE_LIMIT_EXCEEDED].level = EVENT_LEVEL_INFO;
|
||||
events.entries[EVENT_DISCHARGE_LIMIT_EXCEEDED].level = EVENT_LEVEL_INFO;
|
||||
events.entries[EVENT_12V_LOW].level = EVENT_LEVEL_WARNING;
|
||||
events.entries[EVENT_SOC_PLAUSIBILITY_ERROR].level = EVENT_LEVEL_ERROR;
|
||||
events.entries[EVENT_SOC_UNAVAILABLE].level = EVENT_LEVEL_WARNING;
|
||||
events.entries[EVENT_KWH_PLAUSIBILITY_ERROR].level = EVENT_LEVEL_INFO;
|
||||
events.entries[EVENT_BATTERY_EMPTY].level = EVENT_LEVEL_INFO;
|
||||
events.entries[EVENT_BATTERY_FULL].level = EVENT_LEVEL_INFO;
|
||||
|
@ -197,13 +209,18 @@ void init_events(void) {
|
|||
events.entries[EVENT_RESET_EFUSE].level = EVENT_LEVEL_INFO;
|
||||
events.entries[EVENT_RESET_PWR_GLITCH].level = EVENT_LEVEL_INFO;
|
||||
events.entries[EVENT_RESET_CPU_LOCKUP].level = EVENT_LEVEL_WARNING;
|
||||
events.entries[EVENT_PAUSE_BEGIN].level = EVENT_LEVEL_WARNING;
|
||||
events.entries[EVENT_PAUSE_END].level = EVENT_LEVEL_INFO;
|
||||
events.entries[EVENT_WIFI_CONNECT].level = EVENT_LEVEL_INFO;
|
||||
events.entries[EVENT_WIFI_DISCONNECT].level = EVENT_LEVEL_INFO;
|
||||
events.entries[EVENT_MQTT_CONNECT].level = EVENT_LEVEL_INFO;
|
||||
events.entries[EVENT_MQTT_DISCONNECT].level = EVENT_LEVEL_INFO;
|
||||
events.entries[EVENT_EQUIPMENT_STOP].level = EVENT_LEVEL_ERROR;
|
||||
|
||||
events.entries[EVENT_EEPROM_WRITE].log = false; // Don't log the logger...
|
||||
|
||||
events.second_timer.set_interval(600);
|
||||
// Write to EEPROM every X minutes (if an event has been set)
|
||||
events.ee_timer.set_interval(EE_WRITE_PERIOD_MINUTES * 60 * 1000);
|
||||
events.update_timer.set_interval(2000);
|
||||
}
|
||||
|
||||
void set_event(EVENTS_ENUM_TYPE event, uint8_t data) {
|
||||
|
@ -222,10 +239,33 @@ void clear_event(EVENTS_ENUM_TYPE event) {
|
|||
}
|
||||
}
|
||||
|
||||
void reset_all_events() {
|
||||
events.nof_logged_events = 0;
|
||||
for (uint16_t i = 0; i < EVENT_NOF_EVENTS; i++) {
|
||||
events.entries[i].data = 0;
|
||||
events.entries[i].state = EVENT_STATE_INACTIVE;
|
||||
events.entries[i].timestamp = 0;
|
||||
events.entries[i].millisrolloverCount = 0;
|
||||
events.entries[i].occurences = 0;
|
||||
events.entries[i].log = true;
|
||||
events.entries[i].MQTTpublished = false; // Not published by default
|
||||
}
|
||||
events.level = EVENT_LEVEL_INFO;
|
||||
update_bms_status();
|
||||
}
|
||||
|
||||
void set_event_MQTTpublished(EVENTS_ENUM_TYPE event) {
|
||||
events.entries[event].MQTTpublished = true;
|
||||
}
|
||||
|
||||
const char* get_event_message_string(EVENTS_ENUM_TYPE event) {
|
||||
switch (event) {
|
||||
case EVENT_CANFD_INIT_FAILURE:
|
||||
return "CAN-FD initialization failed. Check hardware or bitrate settings";
|
||||
case EVENT_CANMCP_INIT_FAILURE:
|
||||
return "CAN-MCP addon initialization failed. Check hardware";
|
||||
case EVENT_CANFD_BUFFER_FULL:
|
||||
return "CAN-FD buffer overflowed. Some CAN messages were not sent. Contact developers.";
|
||||
case EVENT_CAN_OVERRUN:
|
||||
return "CAN message failed to send within defined time. Contact developers, CPU load might be too high.";
|
||||
case EVENT_CAN_RX_FAILURE:
|
||||
|
@ -238,6 +278,8 @@ const char* get_event_message_string(EVENTS_ENUM_TYPE event) {
|
|||
return "ERROR: High amount of corrupted CAN messages detected. Check CAN wire shielding!";
|
||||
case EVENT_CAN_TX_FAILURE:
|
||||
return "ERROR: CAN messages failed to transmit, or no one on the bus to ACK the message!";
|
||||
case EVENT_CAN_INVERTER_MISSING:
|
||||
return "Warning: Inverter not sending messages on CAN bus. Check wiring!";
|
||||
case EVENT_CHARGE_LIMIT_EXCEEDED:
|
||||
return "Info: Inverter is charging faster than battery is allowing.";
|
||||
case EVENT_DISCHARGE_LIMIT_EXCEEDED:
|
||||
|
@ -247,7 +289,9 @@ const char* get_event_message_string(EVENTS_ENUM_TYPE event) {
|
|||
case EVENT_12V_LOW:
|
||||
return "12V battery source below required voltage to safely close contactors. Inspect the supply/battery!";
|
||||
case EVENT_SOC_PLAUSIBILITY_ERROR:
|
||||
return "ERROR: SOC% reported by battery not plausible. Restart battery!";
|
||||
return "ERROR: SOC reported by battery not plausible. Restart battery!";
|
||||
case EVENT_SOC_UNAVAILABLE:
|
||||
return "Warning: SOC not sent by BMS. Calibrate BMS via app.";
|
||||
case EVENT_KWH_PLAUSIBILITY_ERROR:
|
||||
return "Info: kWh remaining reported by battery not plausible. Battery needs cycling.";
|
||||
case EVENT_BATTERY_EMPTY:
|
||||
|
@ -362,6 +406,20 @@ const char* get_event_message_string(EVENTS_ENUM_TYPE event) {
|
|||
return "Info: The board was reset due to a detected power glitch";
|
||||
case EVENT_RESET_CPU_LOCKUP:
|
||||
return "Warning: The board was reset due to CPU lockup. Inform developers!";
|
||||
case EVENT_PAUSE_BEGIN:
|
||||
return "Warning: The emulator is trying to pause the battery.";
|
||||
case EVENT_PAUSE_END:
|
||||
return "Info: The emulator is attempting to resume battery operation from pause.";
|
||||
case EVENT_WIFI_CONNECT:
|
||||
return "Info: Wifi connected.";
|
||||
case EVENT_WIFI_DISCONNECT:
|
||||
return "Info: Wifi disconnected.";
|
||||
case EVENT_MQTT_CONNECT:
|
||||
return "Info: MQTT connected.";
|
||||
case EVENT_MQTT_DISCONNECT:
|
||||
return "Info: MQTT disconnected.";
|
||||
case EVENT_EQUIPMENT_STOP:
|
||||
return "ERROR: EQUIPMENT STOP ACTIVATED!!!";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
@ -397,13 +455,15 @@ static void set_event(EVENTS_ENUM_TYPE event, uint8_t data, bool latched) {
|
|||
if ((events.entries[event].state != EVENT_STATE_ACTIVE) &&
|
||||
(events.entries[event].state != EVENT_STATE_ACTIVE_LATCHED)) {
|
||||
events.entries[event].occurences++;
|
||||
events.entries[event].MQTTpublished = false;
|
||||
if (events.entries[event].log) {
|
||||
log_event(event, data);
|
||||
log_event(event, events.entries[event].millisrolloverCount, events.entries[event].timestamp, data);
|
||||
}
|
||||
}
|
||||
|
||||
// We should set the event, update event info
|
||||
events.entries[event].timestamp = events.time_seconds;
|
||||
events.entries[event].timestamp = millis();
|
||||
events.entries[event].millisrolloverCount = millisrolloverCount;
|
||||
events.entries[event].data = data;
|
||||
// Check if the event is latching
|
||||
events.entries[event].state = latched ? EVENT_STATE_ACTIVE_LATCHED : EVENT_STATE_ACTIVE;
|
||||
|
@ -436,6 +496,22 @@ static void update_bms_status(void) {
|
|||
}
|
||||
}
|
||||
|
||||
// Function to compare events by timestamp descending
|
||||
bool compareEventsByTimestampDesc(const EventData& a, const EventData& b) {
|
||||
if (a.event_pointer->millisrolloverCount != b.event_pointer->millisrolloverCount) {
|
||||
return a.event_pointer->millisrolloverCount > b.event_pointer->millisrolloverCount;
|
||||
}
|
||||
return a.event_pointer->timestamp > b.event_pointer->timestamp;
|
||||
}
|
||||
|
||||
// Function to compare events by timestamp ascending
|
||||
bool compareEventsByTimestampAsc(const EventData& a, const EventData& b) {
|
||||
if (a.event_pointer->millisrolloverCount != b.event_pointer->millisrolloverCount) {
|
||||
return a.event_pointer->millisrolloverCount < b.event_pointer->millisrolloverCount;
|
||||
}
|
||||
return a.event_pointer->timestamp < b.event_pointer->timestamp;
|
||||
}
|
||||
|
||||
static void update_event_level(void) {
|
||||
EVENTS_LEVEL_TYPE temporary_level = EVENT_LEVEL_INFO;
|
||||
for (uint8_t i = 0u; i < EVENT_NOF_EVENTS; i++) {
|
||||
|
@ -446,22 +522,7 @@ static void update_event_level(void) {
|
|||
events.level = temporary_level;
|
||||
}
|
||||
|
||||
static void update_event_time(void) {
|
||||
// This should run roughly 2 times per second
|
||||
if (events.second_timer.elapsed() == true) {
|
||||
uptime::calculateUptime(); // millis() overflows every 50 days, so update occasionally to adjust
|
||||
events.time_seconds = uptime::getDays() * DAYS_TO_SECS;
|
||||
events.time_seconds += uptime::getHours() * HOURS_TO_SECS;
|
||||
events.time_seconds += uptime::getMinutes() * MINUTES_TO_SECS;
|
||||
events.time_seconds += uptime::getSeconds();
|
||||
}
|
||||
}
|
||||
|
||||
unsigned long get_current_event_time_secs(void) {
|
||||
return events.time_seconds;
|
||||
}
|
||||
|
||||
static void log_event(EVENTS_ENUM_TYPE event, uint8_t data) {
|
||||
static void log_event(EVENTS_ENUM_TYPE event, uint8_t millisrolloverCount, uint32_t timestamp, uint8_t data) {
|
||||
// Update head with wrap to 0
|
||||
if (++events.event_log_head_index == EE_NOF_EVENT_ENTRIES) {
|
||||
events.event_log_head_index = 0;
|
||||
|
@ -479,7 +540,8 @@ static void log_event(EVENTS_ENUM_TYPE event, uint8_t data) {
|
|||
int entry_address = EE_EVENT_ENTRY_START_ADDRESS + EE_EVENT_ENTRY_SIZE * events.event_log_head_index;
|
||||
|
||||
// Prepare an event block to write
|
||||
EVENT_LOG_ENTRY_TYPE entry = {.event = event, .timestamp = events.time_seconds, .data = data};
|
||||
EVENT_LOG_ENTRY_TYPE entry = {
|
||||
.event = event, .millisrolloverCount = millisrolloverCount, .timestamp = timestamp, .data = data};
|
||||
|
||||
// Put the event in (what I guess is) the RAM EEPROM mirror, or write buffer
|
||||
EEPROM.put(entry_address, entry);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
// #define INCLUDE_EVENTS_TEST // Enable to run an event test loop, see events_test_on_target.cpp
|
||||
|
||||
#define EE_MAGIC_HEADER_VALUE 0x0011 // 0x0000 to 0xFFFF
|
||||
#define EE_MAGIC_HEADER_VALUE 0x0015 // 0x0000 to 0xFFFF
|
||||
|
||||
#define GENERATE_ENUM(ENUM) ENUM,
|
||||
#define GENERATE_STRING(STRING) #STRING,
|
||||
|
@ -27,17 +27,21 @@
|
|||
|
||||
#define EVENTS_ENUM_TYPE(XX) \
|
||||
XX(EVENT_CANFD_INIT_FAILURE) \
|
||||
XX(EVENT_CANMCP_INIT_FAILURE) \
|
||||
XX(EVENT_CANFD_BUFFER_FULL) \
|
||||
XX(EVENT_CAN_OVERRUN) \
|
||||
XX(EVENT_CAN_RX_FAILURE) \
|
||||
XX(EVENT_CAN2_RX_FAILURE) \
|
||||
XX(EVENT_CANFD_RX_FAILURE) \
|
||||
XX(EVENT_CAN_RX_WARNING) \
|
||||
XX(EVENT_CAN_TX_FAILURE) \
|
||||
XX(EVENT_CAN_INVERTER_MISSING) \
|
||||
XX(EVENT_CHARGE_LIMIT_EXCEEDED) \
|
||||
XX(EVENT_DISCHARGE_LIMIT_EXCEEDED) \
|
||||
XX(EVENT_WATER_INGRESS) \
|
||||
XX(EVENT_12V_LOW) \
|
||||
XX(EVENT_SOC_PLAUSIBILITY_ERROR) \
|
||||
XX(EVENT_SOC_UNAVAILABLE) \
|
||||
XX(EVENT_KWH_PLAUSIBILITY_ERROR) \
|
||||
XX(EVENT_BATTERY_EMPTY) \
|
||||
XX(EVENT_BATTERY_FULL) \
|
||||
|
@ -93,6 +97,13 @@
|
|||
XX(EVENT_RESET_EFUSE) \
|
||||
XX(EVENT_RESET_PWR_GLITCH) \
|
||||
XX(EVENT_RESET_CPU_LOCKUP) \
|
||||
XX(EVENT_PAUSE_BEGIN) \
|
||||
XX(EVENT_PAUSE_END) \
|
||||
XX(EVENT_WIFI_CONNECT) \
|
||||
XX(EVENT_WIFI_DISCONNECT) \
|
||||
XX(EVENT_MQTT_CONNECT) \
|
||||
XX(EVENT_MQTT_DISCONNECT) \
|
||||
XX(EVENT_EQUIPMENT_STOP) \
|
||||
XX(EVENT_NOF_EVENTS)
|
||||
|
||||
typedef enum { EVENTS_ENUM_TYPE(GENERATE_ENUM) } EVENTS_ENUM_TYPE;
|
||||
|
@ -116,18 +127,27 @@ typedef enum {
|
|||
|
||||
typedef struct {
|
||||
uint32_t timestamp; // Time in seconds since startup when the event occurred
|
||||
uint8_t millisrolloverCount; // number of times millis rollovers before timestamp
|
||||
uint8_t data; // Custom data passed when setting the event, for example cell number for under voltage
|
||||
uint8_t occurences; // Number of occurrences since startup
|
||||
EVENTS_LEVEL_TYPE level; // Event level, i.e. ERROR/WARNING...
|
||||
EVENTS_STATE_TYPE state; // Event state, i.e. ACTIVE/INACTIVE...
|
||||
bool log;
|
||||
bool MQTTpublished;
|
||||
} EVENTS_STRUCT_TYPE;
|
||||
|
||||
// Define a struct to hold event data
|
||||
struct EventData {
|
||||
EVENTS_ENUM_TYPE event_handle;
|
||||
const EVENTS_STRUCT_TYPE* event_pointer;
|
||||
};
|
||||
|
||||
extern uint8_t millisrolloverCount; // number of times millis rollovers
|
||||
|
||||
const char* get_event_enum_string(EVENTS_ENUM_TYPE event);
|
||||
const char* get_event_message_string(EVENTS_ENUM_TYPE event);
|
||||
const char* get_event_level_string(EVENTS_ENUM_TYPE event);
|
||||
const char* get_event_type(EVENTS_ENUM_TYPE event);
|
||||
unsigned long get_current_event_time_secs(void);
|
||||
|
||||
EVENTS_LEVEL_TYPE get_event_level(void);
|
||||
|
||||
|
@ -135,6 +155,8 @@ void init_events(void);
|
|||
void set_event_latched(EVENTS_ENUM_TYPE event, uint8_t data);
|
||||
void set_event(EVENTS_ENUM_TYPE event, uint8_t data);
|
||||
void clear_event(EVENTS_ENUM_TYPE event);
|
||||
void reset_all_events();
|
||||
void set_event_MQTTpublished(EVENTS_ENUM_TYPE event);
|
||||
|
||||
const EVENTS_STRUCT_TYPE* get_event_pointer(EVENTS_ENUM_TYPE event);
|
||||
|
||||
|
@ -142,4 +164,7 @@ void run_event_handling(void);
|
|||
|
||||
void run_sequence_on_target(void);
|
||||
|
||||
bool compareEventsByTimestampAsc(const EventData& a, const EventData& b);
|
||||
bool compareEventsByTimestampDesc(const EventData& a, const EventData& b);
|
||||
|
||||
#endif // __MYTIMER_H__
|
||||
|
|
21
Software/src/devboard/utils/types.cpp
Normal file
21
Software/src/devboard/utils/types.cpp
Normal file
|
@ -0,0 +1,21 @@
|
|||
#include "types.h"
|
||||
|
||||
// Function to get string representation of bms_status_enum
|
||||
std::string getBMSStatus(bms_status_enum status) {
|
||||
switch (status) {
|
||||
case STANDBY:
|
||||
return "STANDBY";
|
||||
case INACTIVE:
|
||||
return "INACTIVE";
|
||||
case DARKSTART:
|
||||
return "DARKSTART";
|
||||
case ACTIVE:
|
||||
return "ACTIVE";
|
||||
case FAULT:
|
||||
return "FAULT";
|
||||
case UPDATING:
|
||||
return "UPDATING";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
#ifndef _TYPES_H_
|
||||
#define _TYPES_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
enum bms_status_enum { STANDBY = 0, INACTIVE = 1, DARKSTART = 2, ACTIVE = 3, FAULT = 4, UPDATING = 5 };
|
||||
enum battery_chemistry_enum { NCA, NMC, LFP };
|
||||
enum led_color { GREEN, YELLOW, RED, BLUE, RGB };
|
||||
|
@ -14,6 +16,7 @@ enum led_color { GREEN, YELLOW, RED, BLUE, RGB };
|
|||
#define INTERVAL_50_MS 50
|
||||
#define INTERVAL_100_MS 100
|
||||
#define INTERVAL_200_MS 200
|
||||
#define INTERVAL_250_MS 250
|
||||
#define INTERVAL_500_MS 500
|
||||
#define INTERVAL_640_MS 640
|
||||
#define INTERVAL_1_S 1000
|
||||
|
@ -46,4 +49,6 @@ typedef struct {
|
|||
} data;
|
||||
} CAN_frame;
|
||||
|
||||
std::string getBMSStatus(bms_status_enum status);
|
||||
|
||||
#endif
|
||||
|
|
|
@ -19,6 +19,10 @@ To update the software over the air:
|
|||
- In your webbrowser, go to the url consisting of the IP address, followed by `/update`, for instance `http://192.168.0.224/update`.
|
||||
- In the webbrowser, follow the steps to select the `.bin` file and to upload the file to the board.
|
||||
|
||||
Security Concerns
|
||||
(https://randomnerdtutorials.com/esp32-esp8266-web-server-http-authentication/)
|
||||
Authentication implemented here is meant to be used in your local network to protect from anyone just typing the ESP IP address and accessing the web server (like unauthorized family member or friend).
|
||||
|
||||
## Future work
|
||||
This section lists a number of features that can be implemented as part of the webserver in the future.
|
||||
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
#include "events_html.h"
|
||||
#include <Arduino.h>
|
||||
#include "../utils/events.h"
|
||||
|
||||
const char EVENTS_HTML_START[] = R"=====(
|
||||
<style>body{background-color:#000;color:#fff}.event-log{display:flex;flex-direction:column}.event{display:flex;flex-wrap:wrap;border:1px solid #fff;padding:10px}.event>div{flex:1;min-width:100px;max-width:90%;word-break:break-word}</style><div style="background-color:#303e47;padding:10px;margin-bottom:10px;border-radius:25px"><div class="event-log"><div class="event" style="background-color:#1e2c33;font-weight:700"><div>Event Type</div><div>Severity</div><div>Last Event</div><div>Count</div><div>Data</div><div>Message</div></div>
|
||||
)=====";
|
||||
const char EVENTS_HTML_END[] = R"=====(
|
||||
</div></div>
|
||||
<button onclick='home()'>Back to main page</button>
|
||||
<style>.event:nth-child(even){background-color:#455a64}.event:nth-child(odd){background-color:#394b52}</style>
|
||||
<script>function showEvent(){document.querySelectorAll(".event").forEach(function(e){var n=e.querySelector(".sec-ago");n&&(n.innerText=new Date(new Date().getTime()-1e3*parseInt(n.innerText,10)).toLocaleString())})}function home(){window.location.href="/"}window.onload=function(){showEvent()}</script>
|
||||
<button onclick="askClear()">Clear all events</button>
|
||||
<button onclick="home()">Back to main page</button>
|
||||
<style>.event:nth-child(even){background-color:#455a64}.event:nth-child(odd){background-color:#394b52}</style><script>function showEvent(){document.querySelectorAll(".event").forEach(function(e){var n=e.querySelector(".sec-ago");n&&(n.innerText=new Date(Date.now()-(4294967296*+n.innerText.split(";")[0]+ +n.innerText.split(";")[1])).toLocaleString())})}function askClear(){window.confirm("Are you sure you want to clear all events?")&&(window.location.href="/clearevents")}function home(){window.location.href="/"}window.onload=function(){showEvent()}</script>
|
||||
)=====";
|
||||
|
||||
static std::vector<EventData> order_events;
|
||||
|
||||
String events_processor(const String& var) {
|
||||
if (var == "X") {
|
||||
String content = "";
|
||||
|
@ -20,65 +20,76 @@ String events_processor(const String& var) {
|
|||
content.concat(FPSTR(EVENTS_HTML_START));
|
||||
const EVENTS_STRUCT_TYPE* event_pointer;
|
||||
|
||||
unsigned long timestamp_now = get_current_event_time_secs();
|
||||
|
||||
//clear the vector
|
||||
order_events.clear();
|
||||
// Collect all events
|
||||
for (int i = 0; i < EVENT_NOF_EVENTS; i++) {
|
||||
event_pointer = get_event_pointer((EVENTS_ENUM_TYPE)i);
|
||||
EVENTS_ENUM_TYPE event_handle = static_cast<EVENTS_ENUM_TYPE>(i);
|
||||
if (event_pointer->occurences > 0) {
|
||||
order_events.push_back({static_cast<EVENTS_ENUM_TYPE>(i), event_pointer});
|
||||
}
|
||||
}
|
||||
// Sort events by timestamp
|
||||
std::sort(order_events.begin(), order_events.end(), compareEventsByTimestampDesc);
|
||||
unsigned long timestamp_now = millis();
|
||||
|
||||
// Generate HTML and debug output
|
||||
for (const auto& event : order_events) {
|
||||
EVENTS_ENUM_TYPE event_handle = event.event_handle;
|
||||
event_pointer = event.event_pointer;
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Event: " + String(get_event_enum_string(event_handle)) +
|
||||
" count: " + String(event_pointer->occurences) + " seconds: " + String(event_pointer->timestamp) +
|
||||
" data: " + String(event_pointer->data) +
|
||||
" level: " + String(get_event_level_string(event_handle)));
|
||||
#endif
|
||||
if (event_pointer->occurences > 0) {
|
||||
content.concat("<div class='event'>");
|
||||
content.concat("<div>" + String(get_event_enum_string(event_handle)) + "</div>");
|
||||
content.concat("<div>" + String(get_event_level_string(event_handle)) + "</div>");
|
||||
content.concat("<div class='sec-ago'>" + String(timestamp_now - event_pointer->timestamp) + "</div>");
|
||||
content.concat("<div class='sec-ago'>" + String(millisrolloverCount) + ";" +
|
||||
String(timestamp_now - event_pointer->timestamp) + "</div>");
|
||||
content.concat("<div>" + String(event_pointer->occurences) + "</div>");
|
||||
content.concat("<div>" + String(event_pointer->data) + "</div>");
|
||||
content.concat("<div>" + String(get_event_message_string(event_handle)) + "</div>");
|
||||
content.concat("</div>"); // End of event row
|
||||
}
|
||||
}
|
||||
|
||||
//clear the vector
|
||||
order_events.clear();
|
||||
content.concat(FPSTR(EVENTS_HTML_END));
|
||||
return content;
|
||||
}
|
||||
return String();
|
||||
}
|
||||
}
|
||||
|
||||
/* Script for displaying event log before it gets minified
|
||||
<button onclick="askClear()">Clear all events</button>
|
||||
<button onclick="home()">Back to main page</button>
|
||||
<style>
|
||||
.event:nth-child(even) {
|
||||
background-color: #455a64;
|
||||
}
|
||||
.event:nth-child(odd) {
|
||||
background-color: #394b52;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function showEvent() {
|
||||
var eventLogElement = document.querySelector('.event-log');
|
||||
// Get the current time on the client side
|
||||
var currentTime = new Date().getTime() / 1000; // Convert milliseconds to seconds
|
||||
// Loop through the events and update the "Last Event" column
|
||||
var events = document.querySelectorAll('.event');
|
||||
events.forEach(function(event) {
|
||||
var secondsAgoElement = event.querySelector('.sec-ago');
|
||||
var timestampElement = event.querySelector('.timestamp');
|
||||
if (secondsAgoElement && timestampElement) {
|
||||
var secondsAgo = parseInt(secondsAgoElement.innerText, 10);
|
||||
var uptimeTimestamp = parseFloat(timestampElement.innerText); // Parse as float to handle seconds with decimal parts
|
||||
// Calculate the actual system time based on the client-side current time
|
||||
var actualTime = new Date((currentTime - uptimeTimestamp + secondsAgo) * 1000);
|
||||
// Format the date and time
|
||||
var formattedTime = actualTime.toLocaleString();
|
||||
// Update the "Last Event" column with the formatted time
|
||||
secondsAgoElement.innerText = formattedTime;
|
||||
}
|
||||
document.querySelectorAll(".event").forEach(function (e) {
|
||||
var n = e.querySelector(".sec-ago");
|
||||
n && (n.innerText = new Date(Date.now() - (+n.innerText.split(";")[0] * 4294967296 + +n.innerText.split(";")[1])).toLocaleString());
|
||||
});
|
||||
}
|
||||
|
||||
// Call the showEvent function when the page is loaded
|
||||
function askClear() {
|
||||
if (window.confirm('Are you sure you want to clear all events?')) {
|
||||
window.location.href = '/clearevents';
|
||||
}
|
||||
}
|
||||
function home() {
|
||||
window.location.href = "/";
|
||||
}
|
||||
window.onload = function () {
|
||||
showEvent();
|
||||
};
|
||||
|
||||
function home() {
|
||||
window.location.href = '/';
|
||||
}
|
||||
</script>
|
||||
*/
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
#define EVENTS_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include "../utils/events.h"
|
||||
|
||||
/**
|
||||
* @brief Replaces placeholder with content section in web page
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#include "webserver.h"
|
||||
#include <Preferences.h>
|
||||
#include "../../datalayer/datalayer.h"
|
||||
#include "../../lib/bblanchon-ArduinoJson/ArduinoJson.h"
|
||||
#include "../utils/events.h"
|
||||
#include "../utils/led_handler.h"
|
||||
#include "../utils/timer.h"
|
||||
|
@ -17,48 +18,37 @@ unsigned long ota_progress_millis = 0;
|
|||
#include "index_html.cpp"
|
||||
#include "settings_html.h"
|
||||
|
||||
enum WifiState {
|
||||
INIT, //before connecting first time
|
||||
RECONNECTING, //we've connected before, but lost connection
|
||||
CONNECTED //we are connected
|
||||
};
|
||||
|
||||
WifiState wifi_state = INIT;
|
||||
|
||||
MyTimer ota_timeout_timer = MyTimer(15000);
|
||||
bool ota_active = false;
|
||||
|
||||
unsigned const long WIFI_MONITOR_INTERVAL_TIME = 15000;
|
||||
unsigned const long INIT_WIFI_CONNECT_TIMEOUT = 8000; // Timeout for initial WiFi connect in milliseconds
|
||||
unsigned const long DEFAULT_WIFI_RECONNECT_INTERVAL = 1000; // Default WiFi reconnect interval in ms
|
||||
unsigned const long MAX_WIFI_RETRY_INTERVAL = 90000; // Maximum wifi retry interval in ms
|
||||
unsigned long last_wifi_monitor_time = millis(); //init millis so wifi monitor doesn't run immediately
|
||||
unsigned long wifi_reconnect_interval = DEFAULT_WIFI_RECONNECT_INTERVAL;
|
||||
unsigned long last_wifi_attempt_time = millis(); //init millis so wifi monitor doesn't run immediately
|
||||
const char get_firmware_info_html[] = R"rawliteral(%X%)rawliteral";
|
||||
|
||||
void init_webserver() {
|
||||
// Configure WiFi
|
||||
#ifdef WIFIAP
|
||||
if (AccessPointEnabled) {
|
||||
WiFi.mode(WIFI_AP_STA); // Simultaneous WiFi AP and Router connection
|
||||
init_WiFi_AP();
|
||||
} else {
|
||||
WiFi.mode(WIFI_STA); // Only Router connection
|
||||
}
|
||||
#else
|
||||
WiFi.mode(WIFI_STA); // Only Router connection
|
||||
#endif // WIFIAP
|
||||
init_WiFi_STA(ssid.c_str(), password.c_str(), wifi_channel);
|
||||
|
||||
String content = index_html;
|
||||
|
||||
server.on("/logout", HTTP_GET, [](AsyncWebServerRequest* request) { request->send(401); });
|
||||
|
||||
// Route for firmware info from ota update page
|
||||
server.on("/GetFirmwareInfo", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
request->send_P(200, "application/json", get_firmware_info_html, get_firmware_info_processor);
|
||||
});
|
||||
|
||||
// Route for root / web page
|
||||
server.on("/", HTTP_GET,
|
||||
[](AsyncWebServerRequest* request) { request->send_P(200, "text/html", index_html, processor); });
|
||||
server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
request->send_P(200, "text/html", index_html, processor);
|
||||
});
|
||||
|
||||
// Route for going to settings web page
|
||||
server.on("/settings", HTTP_GET,
|
||||
[](AsyncWebServerRequest* request) { request->send_P(200, "text/html", index_html, settings_processor); });
|
||||
server.on("/settings", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
request->send_P(200, "text/html", index_html, settings_processor);
|
||||
});
|
||||
|
||||
// Route for going to advanced battery info web page
|
||||
server.on("/advanced", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
|
@ -67,15 +57,35 @@ void init_webserver() {
|
|||
|
||||
// Route for going to cellmonitor web page
|
||||
server.on("/cellmonitor", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
request->send_P(200, "text/html", index_html, cellmonitor_processor);
|
||||
});
|
||||
|
||||
// Route for going to event log web page
|
||||
server.on("/events", HTTP_GET,
|
||||
[](AsyncWebServerRequest* request) { request->send_P(200, "text/html", index_html, events_processor); });
|
||||
server.on("/events", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
request->send_P(200, "text/html", index_html, events_processor);
|
||||
});
|
||||
|
||||
// Route for clearing all events
|
||||
server.on("/clearevents", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
reset_all_events();
|
||||
// Send back a response that includes an instant redirect to /events
|
||||
String response = "<html><body>";
|
||||
response += "<script>window.location.href = '/events';</script>"; // Instant redirect
|
||||
response += "</body></html>";
|
||||
request->send(200, "text/html", response);
|
||||
});
|
||||
|
||||
// Route for editing SSID
|
||||
server.on("/updateSSID", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
|
||||
if (request->hasParam("value")) {
|
||||
String value = request->getParam("value")->value();
|
||||
if (value.length() <= 63) { // Check if SSID is within the allowable length
|
||||
|
@ -91,6 +101,8 @@ void init_webserver() {
|
|||
});
|
||||
// Route for editing Password
|
||||
server.on("/updatePassword", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
if (request->hasParam("value")) {
|
||||
String value = request->getParam("value")->value();
|
||||
if (value.length() > 8) { // Check if password is within the allowable length
|
||||
|
@ -107,6 +119,8 @@ void init_webserver() {
|
|||
|
||||
// Route for editing Wh
|
||||
server.on("/updateBatterySize", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
if (request->hasParam("value")) {
|
||||
String value = request->getParam("value")->value();
|
||||
datalayer.battery.info.total_capacity_Wh = value.toInt();
|
||||
|
@ -119,6 +133,8 @@ void init_webserver() {
|
|||
|
||||
// Route for editing USE_SCALED_SOC
|
||||
server.on("/updateUseScaledSOC", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
if (request->hasParam("value")) {
|
||||
String value = request->getParam("value")->value();
|
||||
datalayer.battery.settings.soc_scaling_active = value.toInt();
|
||||
|
@ -131,6 +147,8 @@ void init_webserver() {
|
|||
|
||||
// Route for editing SOCMax
|
||||
server.on("/updateSocMax", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
if (request->hasParam("value")) {
|
||||
String value = request->getParam("value")->value();
|
||||
datalayer.battery.settings.max_percentage = static_cast<uint16_t>(value.toFloat() * 100);
|
||||
|
@ -141,8 +159,40 @@ void init_webserver() {
|
|||
}
|
||||
});
|
||||
|
||||
// Route for pause/resume Battery emulator
|
||||
server.on("/pause", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
if (request->hasParam("p")) {
|
||||
String valueStr = request->getParam("p")->value();
|
||||
setBatteryPause(valueStr == "true" || valueStr == "1", false);
|
||||
request->send(200, "text/plain", "Updated successfully");
|
||||
} else {
|
||||
request->send(400, "text/plain", "Bad Request");
|
||||
}
|
||||
});
|
||||
|
||||
// Route for equipment stop/resume
|
||||
server.on("/equipmentStop", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
if (request->hasParam("stop")) {
|
||||
String valueStr = request->getParam("stop")->value();
|
||||
if (valueStr == "true" || valueStr == "1") {
|
||||
setBatteryPause(true, true, true);
|
||||
} else {
|
||||
setBatteryPause(false, false, false);
|
||||
}
|
||||
request->send(200, "text/plain", "Updated successfully");
|
||||
} else {
|
||||
request->send(400, "text/plain", "Bad Request");
|
||||
}
|
||||
});
|
||||
|
||||
// Route for editing SOCMin
|
||||
server.on("/updateSocMin", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
if (request->hasParam("value")) {
|
||||
String value = request->getParam("value")->value();
|
||||
datalayer.battery.settings.min_percentage = static_cast<uint16_t>(value.toFloat() * 100);
|
||||
|
@ -155,6 +205,8 @@ void init_webserver() {
|
|||
|
||||
// Route for editing MaxChargeA
|
||||
server.on("/updateMaxChargeA", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
if (request->hasParam("value")) {
|
||||
String value = request->getParam("value")->value();
|
||||
datalayer.battery.info.max_charge_amp_dA = static_cast<uint16_t>(value.toFloat() * 10);
|
||||
|
@ -167,6 +219,8 @@ void init_webserver() {
|
|||
|
||||
// Route for editing MaxDischargeA
|
||||
server.on("/updateMaxDischargeA", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
if (request->hasParam("value")) {
|
||||
String value = request->getParam("value")->value();
|
||||
datalayer.battery.info.max_discharge_amp_dA = static_cast<uint16_t>(value.toFloat() * 10);
|
||||
|
@ -180,6 +234,8 @@ void init_webserver() {
|
|||
#ifdef TEST_FAKE_BATTERY
|
||||
// Route for editing FakeBatteryVoltage
|
||||
server.on("/updateFakeBatteryVoltage", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
if (!request->hasParam("value")) {
|
||||
request->send(400, "text/plain", "Bad Request");
|
||||
}
|
||||
|
@ -196,6 +252,8 @@ void init_webserver() {
|
|||
#if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER
|
||||
// Route for editing ChargerTargetV
|
||||
server.on("/updateChargeSetpointV", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
if (!request->hasParam("value")) {
|
||||
request->send(400, "text/plain", "Bad Request");
|
||||
}
|
||||
|
@ -218,6 +276,8 @@ void init_webserver() {
|
|||
|
||||
// Route for editing ChargerTargetA
|
||||
server.on("/updateChargeSetpointA", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
if (!request->hasParam("value")) {
|
||||
request->send(400, "text/plain", "Bad Request");
|
||||
}
|
||||
|
@ -240,6 +300,8 @@ void init_webserver() {
|
|||
|
||||
// Route for editing ChargerEndA
|
||||
server.on("/updateChargeEndA", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
if (request->hasParam("value")) {
|
||||
String value = request->getParam("value")->value();
|
||||
charger_setpoint_HV_IDC_END = value.toFloat();
|
||||
|
@ -251,6 +313,8 @@ void init_webserver() {
|
|||
|
||||
// Route for enabling/disabling HV charger
|
||||
server.on("/updateChargerHvEnabled", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
if (request->hasParam("value")) {
|
||||
String value = request->getParam("value")->value();
|
||||
charger_HV_enabled = (bool)value.toInt();
|
||||
|
@ -262,6 +326,8 @@ void init_webserver() {
|
|||
|
||||
// Route for enabling/disabling aux12v charger
|
||||
server.on("/updateChargerAux12vEnabled", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
if (request->hasParam("value")) {
|
||||
String value = request->getParam("value")->value();
|
||||
charger_aux12V_enabled = (bool)value.toInt();
|
||||
|
@ -273,13 +339,21 @@ void init_webserver() {
|
|||
#endif // defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER
|
||||
|
||||
// Send a GET request to <ESP_IP>/update
|
||||
server.on("/debug", HTTP_GET,
|
||||
[](AsyncWebServerRequest* request) { request->send(200, "text/plain", "Debug: all OK."); });
|
||||
server.on("/debug", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
request->send(200, "text/plain", "Debug: all OK.");
|
||||
});
|
||||
|
||||
// Route to handle reboot command
|
||||
server.on("/reboot", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
|
||||
return request->requestAuthentication();
|
||||
request->send(200, "text/plain", "Rebooting server...");
|
||||
//TODO: Should we handle contactors gracefully? Ifdef CONTACTOR_CONTROL then what?
|
||||
|
||||
//Equipment STOP without persisting the equipment state before restart
|
||||
// Max Charge/Discharge = 0; CAN = stop; contactors = open
|
||||
setBatteryPause(true, true, true, false);
|
||||
delay(1000);
|
||||
ESP.restart();
|
||||
});
|
||||
|
@ -289,29 +363,8 @@ void init_webserver() {
|
|||
|
||||
// Start server
|
||||
server.begin();
|
||||
|
||||
#ifdef MQTT
|
||||
// Init MQTT
|
||||
init_mqtt();
|
||||
#endif // MQTT
|
||||
}
|
||||
|
||||
#ifdef WIFIAP
|
||||
void init_WiFi_AP() {
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Creating Access Point: " + String(ssidAP));
|
||||
Serial.println("With password: " + String(passwordAP));
|
||||
#endif // DEBUG_VIA_USB
|
||||
WiFi.softAP(ssidAP, passwordAP);
|
||||
IPAddress IP = WiFi.softAPIP();
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Access Point created.");
|
||||
Serial.print("IP address: ");
|
||||
Serial.println(IP);
|
||||
#endif // DEBUG_VIA_USB
|
||||
}
|
||||
#endif // WIFIAP
|
||||
|
||||
String getConnectResultString(wl_status_t status) {
|
||||
switch (status) {
|
||||
case WL_CONNECTED:
|
||||
|
@ -335,62 +388,11 @@ String getConnectResultString(wl_status_t status) {
|
|||
}
|
||||
}
|
||||
|
||||
void wifi_monitor() {
|
||||
unsigned long currentMillis = millis();
|
||||
if (currentMillis - last_wifi_monitor_time > WIFI_MONITOR_INTERVAL_TIME) {
|
||||
last_wifi_monitor_time = currentMillis;
|
||||
wl_status_t status = WiFi.status();
|
||||
if (status != WL_CONNECTED && status != WL_IDLE_STATUS) {
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println(getConnectResultString(status));
|
||||
#endif // DEBUG_VIA_USB
|
||||
if (wifi_state == INIT) { //we haven't been connected yet, try the init logic
|
||||
init_WiFi_STA(ssid.c_str(), password.c_str(), wifi_channel);
|
||||
} else { //we were connected before, try the reconnect logic
|
||||
if (currentMillis - last_wifi_attempt_time > wifi_reconnect_interval) {
|
||||
last_wifi_attempt_time = currentMillis;
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("WiFi not connected, trying to reconnect...");
|
||||
#endif // DEBUG_VIA_USB
|
||||
wifi_state = RECONNECTING;
|
||||
WiFi.reconnect();
|
||||
wifi_reconnect_interval = min(wifi_reconnect_interval * 2, MAX_WIFI_RETRY_INTERVAL);
|
||||
}
|
||||
}
|
||||
} else if (status == WL_CONNECTED && wifi_state != CONNECTED) {
|
||||
wifi_state = CONNECTED;
|
||||
wifi_reconnect_interval = DEFAULT_WIFI_RECONNECT_INTERVAL;
|
||||
// Print local IP address and start web server
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.print("Connected to WiFi network: " + String(ssid.c_str()));
|
||||
Serial.print(" IP address: " + WiFi.localIP().toString());
|
||||
Serial.print(" Signal Strength: " + String(WiFi.RSSI()) + " dBm");
|
||||
Serial.println(" Channel: " + String(WiFi.channel()));
|
||||
Serial.println(" Hostname: " + String(WiFi.getHostname()));
|
||||
#endif // DEBUG_VIA_USB
|
||||
}
|
||||
}
|
||||
|
||||
void ota_monitor() {
|
||||
if (ota_active && ota_timeout_timer.elapsed()) {
|
||||
// OTA timeout, try to restore can and clear the update event
|
||||
ESP32Can.CANInit();
|
||||
clear_event(EVENT_OTA_UPDATE);
|
||||
set_event(EVENT_OTA_UPDATE_TIMEOUT, 0);
|
||||
ota_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
void init_WiFi_STA(const char* ssid, const char* password, const uint8_t wifi_channel) {
|
||||
// Connect to Wi-Fi network with SSID and password
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.print("Connecting to ");
|
||||
Serial.println(ssid);
|
||||
#endif // DEBUG_VIA_USB
|
||||
WiFi.begin(ssid, password, wifi_channel);
|
||||
WiFi.setAutoReconnect(true); // Enable auto reconnect
|
||||
wl_status_t result = static_cast<wl_status_t>(WiFi.waitForConnectResult(INIT_WIFI_CONNECT_TIMEOUT));
|
||||
if (result) {
|
||||
//TODO: Add event or serial print?
|
||||
onOTAEnd(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -403,6 +405,24 @@ void init_ElegantOTA() {
|
|||
ElegantOTA.onEnd(onOTAEnd);
|
||||
}
|
||||
|
||||
String get_firmware_info_processor(const String& var) {
|
||||
if (var == "X") {
|
||||
String content = "";
|
||||
static JsonDocument doc;
|
||||
#ifdef HW_LILYGO
|
||||
doc["hardware"] = "LilyGo T-CAN485";
|
||||
#endif // HW_LILYGO
|
||||
#ifdef HW_STARK
|
||||
doc["hardware"] = "Stark CMR Module";
|
||||
#endif // HW_STARK
|
||||
|
||||
doc["firmware"] = String(version_number);
|
||||
serializeJson(doc, content);
|
||||
return content;
|
||||
}
|
||||
return String();
|
||||
}
|
||||
|
||||
String processor(const String& var) {
|
||||
if (var == "X") {
|
||||
String content = "";
|
||||
|
@ -477,9 +497,9 @@ String processor(const String& var) {
|
|||
#ifdef BYD_MODBUS
|
||||
content += "BYD 11kWh HVM battery over Modbus RTU";
|
||||
#endif // BYD_MODBUS
|
||||
#ifdef LUNA2000_MODBUS
|
||||
content += "Luna2000 battery over Modbus RTU";
|
||||
#endif // LUNA2000_MODBUS
|
||||
#ifdef FOXESS_CAN
|
||||
content += "FoxESS compatible HV2600/ECS4100 battery";
|
||||
#endif // FOXESS_CAN
|
||||
#ifdef PYLON_CAN
|
||||
content += "Pylontech battery over CAN bus";
|
||||
#endif // PYLON_CAN
|
||||
|
@ -528,6 +548,12 @@ String processor(const String& var) {
|
|||
#ifdef NISSAN_LEAF_BATTERY
|
||||
content += "Nissan LEAF";
|
||||
#endif // NISSAN_LEAF_BATTERY
|
||||
#ifdef PYLON_BATTERY
|
||||
content += "Pylon compatible battery";
|
||||
#endif // PYLON_BATTERY
|
||||
#ifdef RJXZS_BMS
|
||||
content += "RJXZS BMS, DIY battery";
|
||||
#endif // RJXZS_BMS
|
||||
#ifdef RENAULT_KANGOO_BATTERY
|
||||
content += "Renault Kangoo";
|
||||
#endif // RENAULT_KANGOO_BATTERY
|
||||
|
@ -543,9 +569,12 @@ String processor(const String& var) {
|
|||
#ifdef SERIAL_LINK_RECEIVER
|
||||
content += "Serial link to another LilyGo board";
|
||||
#endif // SERIAL_LINK_RECEIVER
|
||||
#ifdef TESLA_MODEL_3_BATTERY
|
||||
content += "Tesla Model S/3/X/Y";
|
||||
#endif // TESLA_MODEL_3_BATTERY
|
||||
#ifdef TESLA_MODEL_SX_BATTERY
|
||||
content += "Tesla Model S/X";
|
||||
#endif // TESLA_MODEL_SX_BATTERY
|
||||
#ifdef TESLA_MODEL_3Y_BATTERY
|
||||
content += "Tesla Model 3/Y";
|
||||
#endif // TESLA_MODEL_3Y_BATTERY
|
||||
#ifdef VOLVO_SPA_BATTERY
|
||||
content += "Volvo / Polestar 78kWh battery";
|
||||
#endif // VOLVO_SPA_BATTERY
|
||||
|
@ -554,6 +583,9 @@ String processor(const String& var) {
|
|||
#endif // TEST_FAKE_BATTERY
|
||||
#ifdef DOUBLE_BATTERY
|
||||
content += " (Double battery)";
|
||||
if (datalayer.battery.info.chemistry == battery_chemistry_enum::LFP) {
|
||||
content += " (LFP)";
|
||||
}
|
||||
#endif // DOUBLE_BATTERY
|
||||
content += "</h4>";
|
||||
|
||||
|
@ -616,6 +648,8 @@ String processor(const String& var) {
|
|||
float powerFloat = static_cast<float>(datalayer.battery.status.active_power_W); // Convert to float
|
||||
float tempMaxFloat = static_cast<float>(datalayer.battery.status.temperature_max_dC) / 10.0; // Convert to float
|
||||
float tempMinFloat = static_cast<float>(datalayer.battery.status.temperature_min_dC) / 10.0; // Convert to float
|
||||
uint16_t cell_delta_mv =
|
||||
datalayer.battery.status.cell_max_voltage_mV - datalayer.battery.status.cell_min_voltage_mV;
|
||||
|
||||
content += "<h4 style='color: white;'>Real SOC: " + String(socRealFloat, 2) + "</h4>";
|
||||
content += "<h4 style='color: white;'>Scaled SOC: " + String(socScaledFloat, 2) + "</h4>";
|
||||
|
@ -625,10 +659,22 @@ String processor(const String& var) {
|
|||
content += formatPowerValue("Power", powerFloat, "", 1);
|
||||
content += formatPowerValue("Total capacity", datalayer.battery.info.total_capacity_Wh, "h", 0);
|
||||
content += formatPowerValue("Remaining capacity", datalayer.battery.status.remaining_capacity_Wh, "h", 1);
|
||||
|
||||
if (emulator_pause_status == NORMAL) {
|
||||
content += formatPowerValue("Max discharge power", datalayer.battery.status.max_discharge_power_W, "", 1);
|
||||
content += formatPowerValue("Max charge power", datalayer.battery.status.max_charge_power_W, "", 1);
|
||||
} else {
|
||||
content += formatPowerValue("Max discharge power", datalayer.battery.status.max_discharge_power_W, "", 1, "red");
|
||||
content += formatPowerValue("Max charge power", datalayer.battery.status.max_charge_power_W, "", 1, "red");
|
||||
}
|
||||
|
||||
content += "<h4>Cell max: " + String(datalayer.battery.status.cell_max_voltage_mV) + " mV</h4>";
|
||||
content += "<h4>Cell min: " + String(datalayer.battery.status.cell_min_voltage_mV) + " mV</h4>";
|
||||
if (cell_delta_mv > datalayer.battery.info.max_cell_voltage_deviation_mV) {
|
||||
content += "<h4 style='color: red;'>Cell delta: " + String(cell_delta_mv) + " mV</h4>";
|
||||
} else {
|
||||
content += "<h4>Cell delta: " + String(cell_delta_mv) + " mV</h4>";
|
||||
}
|
||||
content += "<h4>Temperature max: " + String(tempMaxFloat, 1) + " C</h4>";
|
||||
content += "<h4>Temperature min: " + String(tempMinFloat, 1) + " C</h4>";
|
||||
if (datalayer.battery.status.bms_status == ACTIVE) {
|
||||
|
@ -660,6 +706,20 @@ String processor(const String& var) {
|
|||
} else {
|
||||
content += "<span style='color: red;'>✕</span></h4>";
|
||||
}
|
||||
if (emulator_pause_status == NORMAL)
|
||||
content += "<h4>Power status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
|
||||
else
|
||||
content += "<h4 style='color: red;'>Power status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
|
||||
|
||||
#ifdef CONTACTOR_CONTROL
|
||||
content += "<h4>Contactors controlled by Battery-Emulator: ";
|
||||
if (datalayer.system.status.contactor_control_closed) {
|
||||
content += "<span style='color: green;'>ON</span>";
|
||||
} else {
|
||||
content += "<span style='color: red;'>OFF</span>";
|
||||
}
|
||||
content += "</h4>";
|
||||
#endif
|
||||
|
||||
// Close the block
|
||||
content += "</div>";
|
||||
|
@ -692,6 +752,7 @@ String processor(const String& var) {
|
|||
powerFloat = static_cast<float>(datalayer.battery2.status.active_power_W); // Convert to float
|
||||
tempMaxFloat = static_cast<float>(datalayer.battery2.status.temperature_max_dC) / 10.0; // Convert to float
|
||||
tempMinFloat = static_cast<float>(datalayer.battery2.status.temperature_min_dC) / 10.0; // Convert to float
|
||||
cell_delta_mv = datalayer.battery2.status.cell_max_voltage_mV - datalayer.battery2.status.cell_min_voltage_mV;
|
||||
|
||||
content += "<h4 style='color: white;'>Real SOC: " + String(socRealFloat, 2) + "</h4>";
|
||||
content += "<h4 style='color: white;'>Scaled SOC: " + String(socScaledFloat, 2) + "</h4>";
|
||||
|
@ -705,6 +766,11 @@ String processor(const String& var) {
|
|||
content += formatPowerValue("Max charge power", datalayer.battery2.status.max_charge_power_W, "", 1);
|
||||
content += "<h4>Cell max: " + String(datalayer.battery2.status.cell_max_voltage_mV) + " mV</h4>";
|
||||
content += "<h4>Cell min: " + String(datalayer.battery2.status.cell_min_voltage_mV) + " mV</h4>";
|
||||
if (cell_delta_mv > datalayer.battery2.info.max_cell_voltage_deviation_mV) {
|
||||
content += "<h4 style='color: red;'>Cell delta: " + String(cell_delta_mv) + " mV</h4>";
|
||||
} else {
|
||||
content += "<h4>Cell delta: " + String(cell_delta_mv) + " mV</h4>";
|
||||
}
|
||||
content += "<h4>Temperature max: " + String(tempMaxFloat, 1) + " C</h4>";
|
||||
content += "<h4>Temperature min: " + String(tempMinFloat, 1) + " C</h4>";
|
||||
if (datalayer.battery.status.bms_status == ACTIVE) {
|
||||
|
@ -737,6 +803,21 @@ String processor(const String& var) {
|
|||
content += "<span style='color: red;'>✕</span></h4>";
|
||||
}
|
||||
|
||||
#ifdef CONTACTOR_CONTROL
|
||||
content += "<h4>Contactors controlled by Battery-Emulator: ";
|
||||
if (datalayer.system.status.contactor_control_closed) {
|
||||
content += "<span style='color: green;'>ON</span>";
|
||||
} else {
|
||||
content += "<span style='color: red;'>OFF</span>";
|
||||
}
|
||||
content += "</h4>";
|
||||
#endif
|
||||
|
||||
if (emulator_pause_status == NORMAL)
|
||||
content += "<h4>Pause status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
|
||||
else
|
||||
content += "<h4 style='color: red;'>Pause status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
|
||||
|
||||
content += "</div>";
|
||||
content += "</div>";
|
||||
#endif // DOUBLE_BATTERY
|
||||
|
@ -797,6 +878,39 @@ String processor(const String& var) {
|
|||
content += "</div>";
|
||||
#endif // defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER
|
||||
|
||||
if (emulator_pause_request_ON)
|
||||
content += "<button onclick='PauseBattery(false)'>Resume charge/discharge</button>";
|
||||
else
|
||||
content +=
|
||||
"<button onclick=\"if(confirm('Are you sure you want to pause charging and discharging? This will set the "
|
||||
"maximum charge and discharge values to zero, preventing any further power flow.')) { PauseBattery(true); "
|
||||
"}\">Pause charge/discharge</button>";
|
||||
content += " ";
|
||||
content += "<button onclick='OTA()'>Perform OTA update</button>";
|
||||
content += " ";
|
||||
content += "<button onclick='Settings()'>Change Settings</button>";
|
||||
content += " ";
|
||||
content += "<button onclick='Cellmon()'>Cellmonitor</button>";
|
||||
content += " ";
|
||||
content += "<button onclick='Events()'>Events</button>";
|
||||
content += " ";
|
||||
content += "<button onclick='askReboot()'>Reboot Emulator</button>";
|
||||
if (WEBSERVER_AUTH_REQUIRED)
|
||||
content += "<button onclick='logout()'>Logout</button>";
|
||||
if (!datalayer.system.settings.equipment_stop_active)
|
||||
content +=
|
||||
"<br/><br/><button style=\"background:red;color:white;cursor:pointer;\""
|
||||
" onclick=\""
|
||||
"if(confirm('This action will open contactors on the battery and stop all CAN communications. Are you "
|
||||
"sure?')) { estop(true); }\""
|
||||
">Open Contactors</button><br/>";
|
||||
else
|
||||
content +=
|
||||
"<br/><br/><button style=\"background:green;color:white;cursor:pointer;\""
|
||||
"20px;font-size:16px;font-weight:bold;cursor:pointer;border-radius:5px; margin:10px;"
|
||||
" onclick=\""
|
||||
"if(confirm('This action will restore the battery state. Are you sure?')) { estop(false); }\""
|
||||
">Close Contactors</button><br/>";
|
||||
content += "<script>";
|
||||
content += "function OTA() { window.location.href = '/update'; }";
|
||||
content += "function Cellmon() { window.location.href = '/cellmonitor'; }";
|
||||
|
@ -812,6 +926,26 @@ String processor(const String& var) {
|
|||
content += " xhr.open('GET', '/reboot', true);";
|
||||
content += " xhr.send();";
|
||||
content += "}";
|
||||
if (WEBSERVER_AUTH_REQUIRED) {
|
||||
content += "function logout() {";
|
||||
content += " var xhr = new XMLHttpRequest();";
|
||||
content += " xhr.open('GET', '/logout', true);";
|
||||
content += " xhr.send();";
|
||||
content += " setTimeout(function(){ window.open(\"/\",\"_self\"); }, 1000);";
|
||||
content += "}";
|
||||
}
|
||||
content += "function PauseBattery(pause){";
|
||||
content +=
|
||||
"var xhr=new "
|
||||
"XMLHttpRequest();xhr.onload=function() { "
|
||||
"window.location.reload();};xhr.open('GET','/pause?p='+pause,true);xhr.send();";
|
||||
content += "}";
|
||||
content += "function estop(stop){";
|
||||
content +=
|
||||
"var xhr=new "
|
||||
"XMLHttpRequest();xhr.onload=function() { "
|
||||
"window.location.reload();};xhr.open('GET','/equipmentStop?stop='+stop,true);xhr.send();";
|
||||
content += "}";
|
||||
content += "</script>";
|
||||
|
||||
//Script for refreshing page
|
||||
|
@ -825,13 +959,16 @@ String processor(const String& var) {
|
|||
}
|
||||
|
||||
void onOTAStart() {
|
||||
//try to Pause the battery
|
||||
setBatteryPause(true, true);
|
||||
|
||||
// Log when OTA has started
|
||||
ESP32Can.CANStop();
|
||||
set_event(EVENT_OTA_UPDATE, 0);
|
||||
|
||||
// If already set, make a new attempt
|
||||
clear_event(EVENT_OTA_UPDATE_TIMEOUT);
|
||||
ota_active = true;
|
||||
|
||||
ota_timeout_timer.reset();
|
||||
}
|
||||
|
||||
|
@ -848,8 +985,16 @@ void onOTAProgress(size_t current, size_t final) {
|
|||
}
|
||||
|
||||
void onOTAEnd(bool success) {
|
||||
|
||||
ota_active = false;
|
||||
clear_event(EVENT_OTA_UPDATE);
|
||||
|
||||
// Log when OTA has finished
|
||||
if (success) {
|
||||
//Equipment STOP without persisting the equipment state before restart
|
||||
// Max Charge/Discharge = 0; CAN = stop; contactors = open
|
||||
setBatteryPause(true, true, true, false);
|
||||
// a reboot will be done by the OTA library. no need to do anything here
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("OTA update finished successfully!");
|
||||
#endif // DEBUG_VIA_USB
|
||||
|
@ -857,17 +1002,14 @@ void onOTAEnd(bool success) {
|
|||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("There was an error during OTA update!");
|
||||
#endif // DEBUG_VIA_USB
|
||||
|
||||
// If we fail without a timeout, try to restore CAN
|
||||
ESP32Can.CANInit();
|
||||
//try to Resume the battery pause and CAN communication
|
||||
setBatteryPause(false, false);
|
||||
}
|
||||
ota_active = false;
|
||||
clear_event(EVENT_OTA_UPDATE);
|
||||
}
|
||||
|
||||
template <typename T> // This function makes power values appear as W when under 1000, and kW when over
|
||||
String formatPowerValue(String label, T value, String unit, int precision) {
|
||||
String result = "<h4 style='color: white;'>" + label + ": ";
|
||||
String formatPowerValue(String label, T value, String unit, int precision, String color) {
|
||||
String result = "<h4 style='color: " + color + ";'>" + label + ": ";
|
||||
|
||||
if (std::is_same<T, float>::value || std::is_same<T, uint16_t>::value || std::is_same<T, uint32_t>::value) {
|
||||
float convertedValue = static_cast<float>(value);
|
||||
|
|
|
@ -6,24 +6,17 @@
|
|||
#include "../../include.h"
|
||||
#include "../../lib/YiannisBourkelis-Uptime-Library/src/uptime_formatter.h"
|
||||
#include "../../lib/ayushsharma82-ElegantOTA/src/ElegantOTA.h"
|
||||
#ifdef MQTT
|
||||
#include "../../lib/knolleary-pubsubclient/PubSubClient.h"
|
||||
#endif
|
||||
#include "../../lib/me-no-dev-AsyncTCP/src/AsyncTCP.h"
|
||||
#include "../../lib/me-no-dev-ESPAsyncWebServer/src/ESPAsyncWebServer.h"
|
||||
#include "../../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h"
|
||||
#ifdef MQTT
|
||||
#include "../mqtt/mqtt.h"
|
||||
#endif
|
||||
|
||||
extern const char* version_number; // The current software version, shown on webserver
|
||||
|
||||
#include <string>
|
||||
extern std::string ssid;
|
||||
extern std::string password;
|
||||
extern const uint8_t wifi_channel;
|
||||
extern const char* http_username;
|
||||
extern const char* http_password;
|
||||
|
||||
extern const char* ssidAP;
|
||||
extern const char* passwordAP;
|
||||
|
||||
// Common charger parameters
|
||||
extern float charger_stat_HVcur;
|
||||
|
@ -45,36 +38,6 @@ extern uint16_t OBC_Charge_Power;
|
|||
*/
|
||||
void init_webserver();
|
||||
|
||||
/**
|
||||
* @brief Monitoring loop for WiFi. Will attempt to reconnect to access point if the connection goes down.
|
||||
*
|
||||
* @param[in] void
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
void wifi_monitor();
|
||||
|
||||
#ifdef WIFIAP
|
||||
/**
|
||||
* @brief Initialization function that creates a WiFi Access Point.
|
||||
*
|
||||
* @param[in] void
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
void init_WiFi_AP();
|
||||
#endif // WIFIAP
|
||||
|
||||
/**
|
||||
* @brief Initialization function that connects to an existing network.
|
||||
*
|
||||
* @param[in] ssid WiFi network name
|
||||
* @param[in] password WiFi network password
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
void init_WiFi_STA(const char* ssid, const char* password, const uint8_t channel);
|
||||
|
||||
// /**
|
||||
// * @brief Function to handle WiFi reconnection.
|
||||
// *
|
||||
|
@ -101,6 +64,7 @@ void init_ElegantOTA();
|
|||
* @return String
|
||||
*/
|
||||
String processor(const String& var);
|
||||
String get_firmware_info_processor(const String& var);
|
||||
|
||||
/**
|
||||
* @brief Executes on OTA start
|
||||
|
@ -138,8 +102,10 @@ void onOTAEnd(bool success);
|
|||
* @return string: values
|
||||
*/
|
||||
template <typename T>
|
||||
String formatPowerValue(String label, T value, String unit, int precision);
|
||||
String formatPowerValue(String label, T value, String unit, int precision, String color = "white");
|
||||
|
||||
extern void storeSettings();
|
||||
|
||||
void ota_monitor();
|
||||
|
||||
#endif
|
||||
|
|
215
Software/src/devboard/wifi/wifi.cpp
Normal file
215
Software/src/devboard/wifi/wifi.cpp
Normal file
|
@ -0,0 +1,215 @@
|
|||
#include "wifi.h"
|
||||
#include "../../include.h"
|
||||
#include "../utils/events.h"
|
||||
|
||||
// Configuration Parameters
|
||||
static const uint16_t WIFI_CHECK_INTERVAL = 2000; // 1 seconds normal check interval when last connected
|
||||
static const uint16_t STEP_WIFI_CHECK_INTERVAL = 2000; // 3 seconds wait step increase in checks for normal reconnects
|
||||
static const uint16_t MAX_STEP_WIFI_CHECK_INTERVAL =
|
||||
10000; // 15 seconds wait step increase in checks for normal reconnects
|
||||
|
||||
static const uint16_t INIT_WIFI_FULL_RECONNECT_INTERVAL =
|
||||
10000; // 10 seconds starting wait interval for full reconnects and first connection
|
||||
static const uint16_t MAX_WIFI_FULL_RECONNECT_INTERVAL = 60000; // 60 seconds maximum wait interval for full reconnects
|
||||
static const uint16_t STEP_WIFI_FULL_RECONNECT_INTERVAL =
|
||||
5000; // 5 seconds wait step increase in checks for full reconnects
|
||||
static const uint16_t MAX_RECONNECT_ATTEMPTS =
|
||||
3; // Maximum number of reconnect attempts before forcing a full connection
|
||||
|
||||
// State variables
|
||||
static unsigned long lastReconnectAttempt = 0;
|
||||
static unsigned long lastWiFiCheck = 0;
|
||||
static bool hasConnectedBefore = false;
|
||||
|
||||
static uint16_t reconnectAttempts = 0; // Counter for reconnect attempts
|
||||
static uint16_t current_full_reconnect_interval = INIT_WIFI_FULL_RECONNECT_INTERVAL;
|
||||
static uint16_t current_check_interval = WIFI_CHECK_INTERVAL;
|
||||
static bool connected_once = false;
|
||||
|
||||
void init_WiFi() {
|
||||
|
||||
#ifdef WIFIAP
|
||||
if (AccessPointEnabled) {
|
||||
WiFi.mode(WIFI_AP_STA); // Simultaneous WiFi AP and Router connection
|
||||
init_WiFi_AP();
|
||||
} else {
|
||||
WiFi.mode(WIFI_STA); // Only Router connection
|
||||
}
|
||||
#else
|
||||
WiFi.mode(WIFI_STA); // Only Router connection
|
||||
#endif // WIFIAP
|
||||
|
||||
// Set WiFi to auto reconnect
|
||||
WiFi.setAutoReconnect(true);
|
||||
|
||||
#ifdef WIFICONFIG
|
||||
// Set static IP
|
||||
WiFi.config(local_IP, gateway, subnet);
|
||||
#endif
|
||||
|
||||
// Initialize Wi-Fi event handlers
|
||||
WiFi.onEvent(onWifiConnect, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_CONNECTED);
|
||||
WiFi.onEvent(onWifiDisconnect, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
|
||||
WiFi.onEvent(onWifiGotIP, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP);
|
||||
|
||||
// Start Wi-Fi connection
|
||||
connectToWiFi();
|
||||
}
|
||||
|
||||
// Task to monitor Wi-Fi status and handle reconnections
|
||||
void wifi_monitor() {
|
||||
unsigned long currentMillis = millis();
|
||||
|
||||
// Check if it's time to monitor the Wi-Fi status
|
||||
// WIFI_CHECK_INTERVAL for normal checks and INIT_WIFI_FULL_RECONNECT_INTERVAL for first connections or full connect attepts
|
||||
if ((hasConnectedBefore && (currentMillis - lastWiFiCheck > current_check_interval)) ||
|
||||
(!hasConnectedBefore && (currentMillis - lastWiFiCheck > INIT_WIFI_FULL_RECONNECT_INTERVAL))) {
|
||||
|
||||
lastWiFiCheck = currentMillis;
|
||||
|
||||
wl_status_t status = WiFi.status();
|
||||
if (status != WL_CONNECTED) {
|
||||
// Increase the current check interval if it's not at the maximum
|
||||
if (current_check_interval + STEP_WIFI_CHECK_INTERVAL <= MAX_STEP_WIFI_CHECK_INTERVAL)
|
||||
current_check_interval += STEP_WIFI_CHECK_INTERVAL;
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Wi-Fi not connected, attempting to reconnect...");
|
||||
#endif
|
||||
// Try WiFi.reconnect() if it was successfully connected at least once
|
||||
if (hasConnectedBefore) {
|
||||
lastReconnectAttempt = millis(); // Reset reconnection attempt timer
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Wi-Fi reconnect attempt...");
|
||||
#endif
|
||||
if (WiFi.reconnect()) {
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Wi-Fi reconnect attempt sucess...");
|
||||
#endif
|
||||
reconnectAttempts = 0; // Reset the attempt counter on successful reconnect
|
||||
} else {
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Wi-Fi reconnect attempt error...");
|
||||
#endif
|
||||
reconnectAttempts++;
|
||||
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Failed to reconnect multiple times, forcing a full connection attempt...");
|
||||
#endif
|
||||
FullReconnectToWiFi();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no previous connection, force a full connection attempt
|
||||
if (currentMillis - lastReconnectAttempt > current_full_reconnect_interval) {
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("No previous OK connection, force a full connection attempt...");
|
||||
#endif
|
||||
FullReconnectToWiFi();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to force a full reconnect to Wi-Fi
|
||||
static void FullReconnectToWiFi() {
|
||||
|
||||
// Increase the current reconnect interval if it's not at the maximum
|
||||
if (current_full_reconnect_interval + STEP_WIFI_FULL_RECONNECT_INTERVAL <= MAX_WIFI_FULL_RECONNECT_INTERVAL) {
|
||||
current_full_reconnect_interval += STEP_WIFI_FULL_RECONNECT_INTERVAL;
|
||||
}
|
||||
hasConnectedBefore = false; // Reset the flag to force a full reconnect
|
||||
WiFi.disconnect(); //force disconnect from the current network
|
||||
connectToWiFi(); //force a full connection attempt
|
||||
}
|
||||
|
||||
// Function to handle Wi-Fi connection
|
||||
static void connectToWiFi() {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
lastReconnectAttempt = millis(); // Reset the reconnect attempt timer
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Connecting to Wi-Fi...");
|
||||
#endif
|
||||
WiFi.begin(ssid.c_str(), password.c_str(), wifi_channel);
|
||||
} else {
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Wi-Fi already connected.");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// Event handler for successful Wi-Fi connection
|
||||
static void onWifiConnect(WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
clear_event(EVENT_WIFI_DISCONNECT);
|
||||
set_event(EVENT_WIFI_CONNECT, 0);
|
||||
connected_once = true;
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Wi-Fi connected.");
|
||||
Serial.print("IP address: ");
|
||||
Serial.println(WiFi.localIP());
|
||||
#endif
|
||||
hasConnectedBefore = true; // Mark as successfully connected at least once
|
||||
reconnectAttempts = 0; // Reset the attempt counter
|
||||
current_full_reconnect_interval = INIT_WIFI_FULL_RECONNECT_INTERVAL; // Reset the full reconnect interval
|
||||
current_check_interval = WIFI_CHECK_INTERVAL; // Reset the full reconnect interval
|
||||
clear_event(EVENT_WIFI_CONNECT);
|
||||
}
|
||||
|
||||
// Event handler for Wi-Fi Got IP
|
||||
static void onWifiGotIP(WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
//clear disconnects events if we got a IP
|
||||
clear_event(EVENT_WIFI_DISCONNECT);
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Wi-Fi Got IP.");
|
||||
Serial.print("IP address: ");
|
||||
Serial.println(WiFi.localIP());
|
||||
#endif
|
||||
}
|
||||
|
||||
// Event handler for Wi-Fi disconnection
|
||||
static void onWifiDisconnect(WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
if (connected_once)
|
||||
set_event(EVENT_WIFI_DISCONNECT, 0);
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Wi-Fi disconnected.");
|
||||
#endif
|
||||
//we dont do anything here, the reconnect will be handled by the monitor
|
||||
//too many events received when the connection is lost
|
||||
//normal reconnect retry start at first 2 seconds
|
||||
}
|
||||
|
||||
#ifdef MDNSRESPONDER
|
||||
// Initialise mDNS
|
||||
void init_mDNS() {
|
||||
// Calulate the host name using the last two chars from the MAC address so each one is likely unique on a network.
|
||||
// e.g batteryemulator8C.local where the mac address is 08:F9:E0:D1:06:8C
|
||||
String mac = WiFi.macAddress();
|
||||
String mdnsHost = "batteryemulator" + mac.substring(mac.length() - 2);
|
||||
|
||||
// Initialize mDNS .local resolution
|
||||
if (!MDNS.begin(mdnsHost)) {
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Error setting up MDNS responder!");
|
||||
#endif
|
||||
} else {
|
||||
// Advertise via bonjour the service so we can auto discover these battery emulators on the local network.
|
||||
MDNS.addService("battery_emulator", "tcp", 80);
|
||||
}
|
||||
}
|
||||
#endif // MDNSRESPONDER
|
||||
|
||||
#ifdef WIFIAP
|
||||
void init_WiFi_AP() {
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Creating Access Point: " + String(ssidAP));
|
||||
Serial.println("With password: " + String(passwordAP));
|
||||
#endif
|
||||
WiFi.softAP(ssidAP, passwordAP);
|
||||
IPAddress IP = WiFi.softAPIP();
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Access Point created.");
|
||||
Serial.print("IP address: ");
|
||||
Serial.println(IP);
|
||||
#endif
|
||||
}
|
||||
#endif // WIFIAP
|
35
Software/src/devboard/wifi/wifi.h
Normal file
35
Software/src/devboard/wifi/wifi.h
Normal file
|
@ -0,0 +1,35 @@
|
|||
#ifndef WIFI_H
|
||||
#define WIFI_H
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <string>
|
||||
#include "../../include.h"
|
||||
|
||||
#ifdef MDNSRESPONDER
|
||||
#include <ESPmDNS.h>
|
||||
#endif // MDNSRESONDER
|
||||
|
||||
extern std::string ssid;
|
||||
extern std::string password;
|
||||
extern const uint8_t wifi_channel;
|
||||
extern const char* ssidAP;
|
||||
extern const char* passwordAP;
|
||||
|
||||
void init_WiFi();
|
||||
void wifi_monitor();
|
||||
static void connectToWiFi();
|
||||
static void FullReconnectToWiFi();
|
||||
static void onWifiConnect(WiFiEvent_t event, WiFiEventInfo_t info);
|
||||
static void onWifiDisconnect(WiFiEvent_t event, WiFiEventInfo_t info);
|
||||
static void onWifiGotIP(WiFiEvent_t event, WiFiEventInfo_t info);
|
||||
|
||||
#ifdef WIFIAP
|
||||
void init_WiFi_AP();
|
||||
#endif // WIFIAP
|
||||
|
||||
#ifdef MDNSRESPONDER
|
||||
// Initialise mDNS
|
||||
void init_mDNS();
|
||||
#endif // MDNSRESPONDER
|
||||
|
||||
#endif
|
|
@ -180,6 +180,7 @@ void update_values_can_inverter() { //This function maps all the values fetched
|
|||
void receive_can_inverter(CAN_frame rx_frame) {
|
||||
switch (rx_frame.ID) {
|
||||
case 0x151: //Message originating from BYD HVS compatible inverter. Reply with CAN identifier!
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
if (rx_frame.data.u8[0] & 0x01) { //Battery requests identification
|
||||
send_intial_data();
|
||||
} else { // We can identify what inverter type we are connected to
|
||||
|
@ -193,12 +194,15 @@ void receive_can_inverter(CAN_frame rx_frame) {
|
|||
}
|
||||
break;
|
||||
case 0x091:
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
inverter_voltage = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]) * 0.1;
|
||||
break;
|
||||
case 0x0D1:
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
inverter_SOC = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]) * 0.1;
|
||||
break;
|
||||
case 0x111:
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
inverter_timestamp = ((rx_frame.data.u8[3] << 24) | (rx_frame.data.u8[2] << 16) | (rx_frame.data.u8[1] << 8) |
|
||||
rx_frame.data.u8[0]);
|
||||
break;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
/* TODO: Map error bits in 0x158 */
|
||||
|
||||
/* Do not change code below unless you are sure what you are doing */
|
||||
static unsigned long previousMillis60s = 0;
|
||||
static unsigned long previousMillis100ms = 0;
|
||||
|
||||
//Actual content messages
|
||||
CAN_frame SMA_358 = {.FD = false,
|
||||
|
@ -51,7 +51,7 @@ CAN_frame SMA_598 = {.FD = false,
|
|||
.ext_ID = false,
|
||||
.DLC = 8,
|
||||
.ID = 0x598,
|
||||
.data = {0x00, 0xD3, 0x00, 0x01, 0x5C, 0x98, 0xB6, 0xEE}};
|
||||
.data = {0x00, 0x01, 0x0F, 0x2C, 0x5C, 0x98, 0xB6, 0xEE}};
|
||||
CAN_frame SMA_5D8 = {.FD = false,
|
||||
.ext_ID = false,
|
||||
.DLC = 8,
|
||||
|
@ -214,25 +214,33 @@ void update_values_can_inverter() { //This function maps all the values fetched
|
|||
void receive_can_inverter(CAN_frame rx_frame) {
|
||||
switch (rx_frame.ID) {
|
||||
case 0x360: //Message originating from SMA inverter - Voltage and current
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
//Frame0-1 Voltage
|
||||
//Frame2-3 Current
|
||||
break;
|
||||
case 0x3E0: //Message originating from SMA inverter - ?
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
break;
|
||||
case 0x420: //Message originating from SMA inverter - Timestamp
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
//Frame0-3 Timestamp
|
||||
/*
|
||||
transmit_can(&SMA_158, can_config.inverter);
|
||||
transmit_can(&SMA_358, can_config.inverter);
|
||||
transmit_can(&SMA_3D8, can_config.inverter);
|
||||
transmit_can(&SMA_458, can_config.inverter);
|
||||
transmit_can(&SMA_518, can_config.inverter);
|
||||
transmit_can(&SMA_4D8, can_config.inverter);
|
||||
*/
|
||||
break;
|
||||
case 0x5E0: //Message originating from SMA inverter - String
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
break;
|
||||
case 0x560: //Message originating from SMA inverter - Init
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
break;
|
||||
case 0x5E7: //Pairing request
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
transmit_can(&SMA_558, can_config.inverter);
|
||||
transmit_can(&SMA_598, can_config.inverter);
|
||||
transmit_can(&SMA_5D8, can_config.inverter);
|
||||
|
@ -254,9 +262,10 @@ void receive_can_inverter(CAN_frame rx_frame) {
|
|||
void send_can_inverter() {
|
||||
unsigned long currentMillis = millis();
|
||||
|
||||
// Send CAN Message every 60s
|
||||
if (currentMillis - previousMillis60s >= INTERVAL_60_S) {
|
||||
previousMillis60s = currentMillis;
|
||||
// Send CAN Message every 100ms if we're enabled
|
||||
if (datalayer.system.status.inverter_allows_contactor_closing) {
|
||||
if (currentMillis - previousMillis100ms >= 100) {
|
||||
previousMillis100ms = currentMillis;
|
||||
transmit_can(&SMA_158, can_config.inverter);
|
||||
transmit_can(&SMA_358, can_config.inverter);
|
||||
transmit_can(&SMA_3D8, can_config.inverter);
|
||||
|
@ -265,4 +274,5 @@ void send_can_inverter() {
|
|||
transmit_can(&SMA_4D8, can_config.inverter);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
797
Software/src/inverter/FOXESS-CAN.cpp
Normal file
797
Software/src/inverter/FOXESS-CAN.cpp
Normal file
|
@ -0,0 +1,797 @@
|
|||
#include "../include.h"
|
||||
#ifdef FOXESS_CAN
|
||||
#include "../datalayer/datalayer.h"
|
||||
#include "../devboard/utils/events.h"
|
||||
#include "FOXESS-CAN.h"
|
||||
|
||||
/* Based on info from this excellent repo: https://github.com/FozzieUK/FoxESS-Canbus-Protocol */
|
||||
/* The FoxESS protocol emulates the stackable (1-8) 48V towers found in the HV2600 / ECS4100 batteries
|
||||
We emulate a full tower setup by default (8*50V=400V) to be more suitable for an EV pack. There are settings
|
||||
below that you can customize, incase you use a lower voltage battery with this protocol */
|
||||
|
||||
#define STATUS_OPERATIONAL_PACKS \
|
||||
0b11111111 //0x1875 b2 contains status for operational packs (responding) in binary so 01111111 is pack 8 not operational, 11101101 is pack 5 & 2 not operational
|
||||
#define NUMBER_OF_PACKS 8 //1-8
|
||||
#define BATTERY_TYPE_MASTER 0x52 //0x52 is HV2600 V2 BMS master
|
||||
#define BATTERY_TYPE_SLAVE 0x82 //0x82 is HV2600 V1, 0x83 is ECS4100 v1, 0x84 is HV2600 V2
|
||||
#define FIRMWARE_VERSION_MASTER 0xFF
|
||||
#define FIRMWARE_VERSION_SLAVE 0x20
|
||||
//for the PACK_ID (b7 =10,20,30,40,50,60,70,80) then FIRMWARE_VERSION 0x1F = 0001 1111, version is v1.15, and if FIRMWARE_VERSION was 0x20 = 0010 0000 then = v2.0
|
||||
#define MASTER 0
|
||||
#define MAX_AC_VOLTAGE 2567 //256.7VAC max
|
||||
#define TOTAL_LIFETIME_WH_ACCUMULATED 0 //We dont have this value in the emulator
|
||||
|
||||
/* Do not change code below unless you are sure what you are doing */
|
||||
static uint16_t max_charge_rate_amp = 0;
|
||||
static uint16_t max_discharge_rate_amp = 0;
|
||||
static int16_t temperature_average = 0;
|
||||
static uint16_t voltage_per_pack = 0;
|
||||
static int16_t current_per_pack = 0;
|
||||
static uint8_t temperature_max_per_pack = 0;
|
||||
static uint8_t temperature_min_per_pack = 0;
|
||||
static uint8_t current_pack_info = 0;
|
||||
static uint8_t inverterStillAlive = 60; // Inverter can be missing for 1minute on startup
|
||||
|
||||
static bool send_cellvoltages = false;
|
||||
static unsigned long previousMillisCellvoltage = 0; // Store the last time a cellvoltage CAN messages were sent
|
||||
static uint8_t can_message_cellvolt_index = 0;
|
||||
|
||||
//CAN message translations from this amazing repository: https://github.com/rand12345/FOXESS_can_bus
|
||||
|
||||
CAN_frame FOXESS_1872 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x1872,
|
||||
.data = {0x40, 0x12, 0x80, 0x0C, 0xCD, 0x00, 0xF4, 0x01}}; //BMS_Limits
|
||||
CAN_frame FOXESS_1873 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x1873,
|
||||
.data = {0xA3, 0x10, 0x0D, 0x00, 0x5D, 0x00, 0x77, 0x07}}; //BMS_PackData
|
||||
CAN_frame FOXESS_1874 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x1874,
|
||||
.data = {0xA3, 0x10, 0x0D, 0x00, 0x5D, 0x00, 0x77, 0x07}}; //BMS_CellData
|
||||
CAN_frame FOXESS_1875 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x1875,
|
||||
.data = {0xF9, 0x00, 0xFF, 0x08, 0x01, 0x00, 0x8E, 0x00}}; //BMS_Status
|
||||
CAN_frame FOXESS_1876 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x1876,
|
||||
.data = {0x01, 0x00, 0x07, 0x0D, 0x0, 0x0, 0xFE, 0x0C}}; //BMS_PackTemps
|
||||
CAN_frame FOXESS_1877 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x1877,
|
||||
.data = {0x00, 0x00, 0x00, 0x00, 0x82, 0x00, 0x20, 0x50}}; //BMS_Unk1
|
||||
CAN_frame FOXESS_1878 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x1878,
|
||||
.data = {0x07, 0x0A, 0x00, 0x00, 0xD0, 0xFF, 0x4E, 0x00}}; //BMS_PackStats
|
||||
CAN_frame FOXESS_1879 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x1879,
|
||||
.data = {0x00, 0x35, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; //BMS_Unk2
|
||||
CAN_frame FOXESS_1881 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x1881,
|
||||
.data = {0x10, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}};
|
||||
CAN_frame FOXESS_1882 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x1882,
|
||||
.data = {0x10, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}};
|
||||
CAN_frame FOXESS_1883 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x1883,
|
||||
.data = {0x10, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}};
|
||||
|
||||
CAN_frame FOXESS_0C05 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C05,
|
||||
.data = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xD0, 0xD0}};
|
||||
CAN_frame FOXESS_0C06 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C06,
|
||||
.data = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xD0, 0xD0}};
|
||||
CAN_frame FOXESS_0C07 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C07,
|
||||
.data = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xD0, 0xD0}};
|
||||
CAN_frame FOXESS_0C08 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C08,
|
||||
.data = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xD0, 0xD0}};
|
||||
CAN_frame FOXESS_0C09 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C09,
|
||||
.data = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xD0, 0xD0}};
|
||||
CAN_frame FOXESS_0C0A = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C0A,
|
||||
.data = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xD0, 0xD0}};
|
||||
CAN_frame FOXESS_0C0B = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C0B,
|
||||
.data = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xD0, 0xD0}};
|
||||
CAN_frame FOXESS_0C0C = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C0C,
|
||||
.data = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xD0, 0xD0}};
|
||||
// Cellvoltages
|
||||
CAN_frame FOXESS_0C1D = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C1D, //Cell 1-4
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C21 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C21, //Cell 5-8
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C25 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C25, //Cell 9-12
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C29 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C29, //Cell 13-16
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C2D = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C2D, //Cell 17-20
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C31 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C31, //Cell 21-24
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C35 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C35, //Cell 25-28
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C39 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C39, //Cell 29-32
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C3D = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C3D, //Cell 33-36
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C41 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C41, //Cell 37-40
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C45 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C45, //Cell 41-44
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C49 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C49, //Cell 45-48
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C4D = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C4D, //Cell 49-52
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C51 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C51, //Cell 53-56
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C55 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C55, //Cell 57-60
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C59 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C59, //Cell 61-64
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C5D = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C5D, //Cell 65-68
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C61 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C61, //Cell 69-72
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C65 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C65, //Cell 73-76
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C69 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C69, //Cell 77-80
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C6D = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C6D, //Cell 81-84
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C71 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C71, //Cell 85-88
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C75 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C75, //Cell 89-92
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C79 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C79, //Cell 93-96
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C7D = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C7D, //Cell 97-100
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C81 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C81, //Cell 101-104
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C85 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C85, //Cell 105-108
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C89 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C89, //Cell 109-112
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C8D = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C8D, //Cell 113-116
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C91 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C91, //Cell 117-120
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C95 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C95, //Cell 121-124
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C99 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C99, //Cell 125-128
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0C9D = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0C9D, //Cell 129-132
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0CA1 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0CA1, //Cell 133-136
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0CA5 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0CA5, //Cell 137-140
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
CAN_frame FOXESS_0CA9 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0CA9, //Cell 141-144
|
||||
.data = {0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C, 0xE4, 0x0C}}; //All cells init to 3300mV
|
||||
|
||||
// Temperatures
|
||||
CAN_frame FOXESS_0D21 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0D21, //Celltemperatures Pack 1
|
||||
.data = {0x49, 0x48, 0x47, 0x47, 0x48, 0x49, 0x46, 0x47}};
|
||||
CAN_frame FOXESS_0D29 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0D29, //Celltemperatures Pack 2
|
||||
.data = {0x49, 0x48, 0x47, 0x47, 0x48, 0x49, 0x46, 0x47}};
|
||||
CAN_frame FOXESS_0D31 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0D31, //Celltemperatures Pack 3
|
||||
.data = {0x49, 0x48, 0x47, 0x47, 0x48, 0x49, 0x46, 0x47}};
|
||||
CAN_frame FOXESS_0D39 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0D39, //Celltemperatures Pack 4
|
||||
.data = {0x49, 0x48, 0x47, 0x47, 0x48, 0x49, 0x46, 0x47}};
|
||||
CAN_frame FOXESS_0D41 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0D41, //Celltemperatures Pack 5
|
||||
.data = {0x49, 0x48, 0x47, 0x47, 0x48, 0x49, 0x46, 0x47}};
|
||||
CAN_frame FOXESS_0D49 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0D49, //Celltemperatures Pack 6
|
||||
.data = {0x49, 0x48, 0x47, 0x47, 0x48, 0x49, 0x46, 0x47}};
|
||||
CAN_frame FOXESS_0D51 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0D51, //Celltemperatures Pack 7
|
||||
.data = {0x49, 0x48, 0x47, 0x47, 0x48, 0x49, 0x46, 0x47}};
|
||||
CAN_frame FOXESS_0D59 = {.FD = false,
|
||||
.ext_ID = true,
|
||||
.DLC = 8,
|
||||
.ID = 0x0D59, //Celltemperatures Pack 8
|
||||
.data = {0x49, 0x48, 0x47, 0x47, 0x48, 0x49, 0x46, 0x47}};
|
||||
|
||||
void update_values_can_inverter() { //This function maps all the CAN values fetched from battery. It also checks some safeties.
|
||||
|
||||
//Calculate the required values
|
||||
temperature_average =
|
||||
((datalayer.battery.status.temperature_max_dC + datalayer.battery.status.temperature_min_dC) / 2);
|
||||
|
||||
//datalayer.battery.status.max_charge_power_W (30000W max)
|
||||
if (datalayer.battery.status.reported_soc > 9999) { // 99.99%
|
||||
// Additional safety incase SOC% is 100, then do not charge battery further
|
||||
max_charge_rate_amp = 0;
|
||||
} else { // We can pass on the battery charge rate (in W) to the inverter (that takes A)
|
||||
if (datalayer.battery.status.max_charge_power_W >= 30000) {
|
||||
max_charge_rate_amp = 75; // Incase battery can take over 30kW, cap value to 75A
|
||||
} else { // Calculate the W value into A
|
||||
if (datalayer.battery.status.voltage_dV > 10) {
|
||||
max_charge_rate_amp =
|
||||
datalayer.battery.status.max_charge_power_W / (datalayer.battery.status.voltage_dV * 0.1); // P/U=I
|
||||
} else { // We avoid dividing by 0 and crashing the board
|
||||
// If we have no voltage, something has gone wrong, do not allow charging
|
||||
max_charge_rate_amp = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//datalayer.battery.status.max_discharge_power_W (30000W max)
|
||||
if (datalayer.battery.status.reported_soc < 100) { // 1.00%
|
||||
// Additional safety in case SOC% is below 1, then do not discharge battery further
|
||||
max_discharge_rate_amp = 0;
|
||||
} else { // We can pass on the battery discharge rate to the inverter
|
||||
if (datalayer.battery.status.max_discharge_power_W >= 30000) {
|
||||
max_discharge_rate_amp = 75; // Incase battery can be charged with over 30kW, cap value to 75A
|
||||
} else { // Calculate the W value into A
|
||||
if (datalayer.battery.status.voltage_dV > 10) {
|
||||
max_discharge_rate_amp =
|
||||
datalayer.battery.status.max_discharge_power_W / (datalayer.battery.status.voltage_dV * 0.1); // P/U=I
|
||||
} else { // We avoid dividing by 0 and crashing the board
|
||||
// If we have no voltage, something has gone wrong, do not allow discharging
|
||||
max_discharge_rate_amp = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Cap the value according to user settings. Some inverters cannot handle large values.
|
||||
if ((max_charge_rate_amp * 10) > datalayer.battery.info.max_charge_amp_dA) {
|
||||
max_charge_rate_amp = (datalayer.battery.info.max_charge_amp_dA / 10);
|
||||
}
|
||||
if ((max_discharge_rate_amp * 10) > datalayer.battery.info.max_discharge_amp_dA) {
|
||||
max_discharge_rate_amp = (datalayer.battery.info.max_discharge_amp_dA / 10);
|
||||
}
|
||||
|
||||
if (inverterStillAlive > 0) {
|
||||
inverterStillAlive--;
|
||||
}
|
||||
|
||||
if (!inverterStillAlive) {
|
||||
set_event(EVENT_CAN_INVERTER_MISSING, 0);
|
||||
} else {
|
||||
clear_event(EVENT_CAN_INVERTER_MISSING);
|
||||
}
|
||||
|
||||
//Put the values into the CAN messages
|
||||
//BMS_Limits
|
||||
FOXESS_1872.data.u8[0] = (uint8_t)datalayer.battery.info.max_design_voltage_dV;
|
||||
FOXESS_1872.data.u8[1] = (datalayer.battery.info.max_design_voltage_dV >> 8);
|
||||
FOXESS_1872.data.u8[2] = (uint8_t)datalayer.battery.info.min_design_voltage_dV;
|
||||
FOXESS_1872.data.u8[3] = (datalayer.battery.info.min_design_voltage_dV >> 8);
|
||||
FOXESS_1872.data.u8[4] = (uint8_t)(max_charge_rate_amp * 10);
|
||||
FOXESS_1872.data.u8[5] = ((max_charge_rate_amp * 10) >> 8);
|
||||
FOXESS_1872.data.u8[6] = (uint8_t)(max_discharge_rate_amp * 10);
|
||||
FOXESS_1872.data.u8[7] = ((max_discharge_rate_amp * 10) >> 8);
|
||||
|
||||
//BMS_PackData
|
||||
FOXESS_1873.data.u8[0] = (uint8_t)datalayer.battery.status.voltage_dV; // OK
|
||||
FOXESS_1873.data.u8[1] = (datalayer.battery.status.voltage_dV >> 8);
|
||||
FOXESS_1873.data.u8[2] = (int8_t)datalayer.battery.status.current_dA; // OK, Signed (Active current in Amps x 10)
|
||||
FOXESS_1873.data.u8[3] = (datalayer.battery.status.current_dA >> 8);
|
||||
FOXESS_1873.data.u8[4] = (uint8_t)(datalayer.battery.status.reported_soc / 100); //SOC (0-100%)
|
||||
FOXESS_1873.data.u8[5] = 0x00;
|
||||
FOXESS_1873.data.u8[6] = (uint8_t)(datalayer.battery.status.remaining_capacity_Wh / 10);
|
||||
FOXESS_1873.data.u8[7] = ((datalayer.battery.status.remaining_capacity_Wh / 10) >> 8);
|
||||
|
||||
//BMS_CellData
|
||||
FOXESS_1874.data.u8[0] = (int8_t)datalayer.battery.status.temperature_max_dC;
|
||||
FOXESS_1874.data.u8[1] = (datalayer.battery.status.temperature_max_dC >> 8);
|
||||
FOXESS_1874.data.u8[2] = (int8_t)datalayer.battery.status.temperature_min_dC;
|
||||
FOXESS_1874.data.u8[3] = (datalayer.battery.status.temperature_min_dC >> 8);
|
||||
FOXESS_1874.data.u8[4] = (uint8_t)(3300); //cut_mv_max (Should we send a limit, or the actual mV?)
|
||||
FOXESS_1874.data.u8[5] = (3300 >> 8);
|
||||
FOXESS_1874.data.u8[6] = (uint8_t)(3300); //cut_mV_min (Should we send a limit, or the actual mV?)
|
||||
FOXESS_1874.data.u8[7] = (3300 >> 8);
|
||||
|
||||
//BMS_Status
|
||||
FOXESS_1875.data.u8[0] = (uint8_t)temperature_average;
|
||||
FOXESS_1875.data.u8[1] = (temperature_average >> 8);
|
||||
FOXESS_1875.data.u8[2] = (uint8_t)STATUS_OPERATIONAL_PACKS;
|
||||
FOXESS_1875.data.u8[3] = (uint8_t)NUMBER_OF_PACKS;
|
||||
FOXESS_1875.data.u8[4] = (uint8_t)1; // Contactor Status 0=off, 1=on.
|
||||
FOXESS_1875.data.u8[5] = (uint8_t)0; //Unused
|
||||
FOXESS_1875.data.u8[6] = (uint8_t)0x8E; //Cycle count
|
||||
FOXESS_1875.data.u8[7] = (uint8_t)0; //Cycle count
|
||||
|
||||
//BMS_PackTemps
|
||||
// 0x1876 b0 bit 0 appears to be 1 when at maxsoc and BMS says charge is not allowed -
|
||||
// when at 0 indicates charge is possible - additional note there is something more to it than this,
|
||||
// it's not as straight forward - needs more testing to find what sets/unsets bit0 of byte0
|
||||
if ((max_charge_rate_amp == 0) || (datalayer.battery.status.reported_soc == 10000) ||
|
||||
(datalayer.battery.status.bms_status == FAULT)) {
|
||||
FOXESS_1876.data.u8[0] = 0x01;
|
||||
} else { //continue using battery
|
||||
FOXESS_1876.data.u8[0] = 0x00;
|
||||
}
|
||||
|
||||
FOXESS_1876.data.u8[1] = (uint8_t)0; //Unused
|
||||
FOXESS_1876.data.u8[2] = (uint8_t)datalayer.battery.status.cell_max_voltage_mV;
|
||||
FOXESS_1876.data.u8[3] = (datalayer.battery.status.cell_max_voltage_mV >> 8);
|
||||
FOXESS_1876.data.u8[4] = (uint8_t)0; //Unused
|
||||
FOXESS_1876.data.u8[5] = (uint8_t)0; //Unused
|
||||
FOXESS_1876.data.u8[6] = (uint8_t)datalayer.battery.status.cell_min_voltage_mV;
|
||||
FOXESS_1876.data.u8[7] = (datalayer.battery.status.cell_min_voltage_mV >> 8);
|
||||
|
||||
//BMS_ErrorsBrand
|
||||
//0x1877 b0 appears to be an error code, 0x02 when pack is in error.
|
||||
if (datalayer.battery.status.bms_status == FAULT) {
|
||||
FOXESS_1877.data.u8[0] = (uint8_t)0x02;
|
||||
} else {
|
||||
FOXESS_1877.data.u8[0] = (uint8_t)0;
|
||||
}
|
||||
FOXESS_1877.data.u8[1] = (uint8_t)0; //Unused
|
||||
FOXESS_1877.data.u8[2] = (uint8_t)0; //Unused
|
||||
FOXESS_1877.data.u8[3] = (uint8_t)0; //Unused
|
||||
FOXESS_1877.data.u8[5] = (uint8_t)0; //Unused
|
||||
if (current_pack_info == MASTER) {
|
||||
FOXESS_1877.data.u8[4] = (uint8_t)BATTERY_TYPE_MASTER;
|
||||
FOXESS_1877.data.u8[5] = (uint8_t)0x22; //Unused?
|
||||
FOXESS_1877.data.u8[6] = (uint8_t)FIRMWARE_VERSION_MASTER;
|
||||
FOXESS_1877.data.u8[7] = (uint8_t)0x01;
|
||||
} else { // 1-8
|
||||
FOXESS_1877.data.u8[4] = (uint8_t)BATTERY_TYPE_SLAVE;
|
||||
FOXESS_1877.data.u8[6] = (uint8_t)FIRMWARE_VERSION_SLAVE;
|
||||
FOXESS_1877.data.u8[7] = (uint8_t)(current_pack_info << 4);
|
||||
}
|
||||
|
||||
//BMS_PackStats
|
||||
FOXESS_1878.data.u8[0] = (uint8_t)(MAX_AC_VOLTAGE);
|
||||
FOXESS_1878.data.u8[1] = ((MAX_AC_VOLTAGE) >> 8);
|
||||
FOXESS_1878.data.u8[2] = (uint8_t)0; //Unused
|
||||
FOXESS_1878.data.u8[3] = (uint8_t)0; //Unused
|
||||
FOXESS_1878.data.u8[4] = (uint8_t)TOTAL_LIFETIME_WH_ACCUMULATED;
|
||||
FOXESS_1878.data.u8[5] = (TOTAL_LIFETIME_WH_ACCUMULATED >> 8);
|
||||
FOXESS_1878.data.u8[6] = (TOTAL_LIFETIME_WH_ACCUMULATED >> 16);
|
||||
FOXESS_1878.data.u8[7] = (TOTAL_LIFETIME_WH_ACCUMULATED >> 24);
|
||||
|
||||
//Errorcodes and flags
|
||||
FOXESS_1879.data.u8[0] = (uint8_t)0; // Error codes go here, still unsure of bitmasking
|
||||
if (datalayer.battery.status.current_dA > 0) {
|
||||
FOXESS_1879.data.u8[1] = 0x35; //Charging
|
||||
} // Mappings taken from https://github.com/FozzieUK/FoxESS-Canbus-Protocol
|
||||
else {
|
||||
FOXESS_1879.data.u8[1] = 0x2B; //Discharging
|
||||
}
|
||||
|
||||
current_pack_info = (current_pack_info + 1);
|
||||
if (current_pack_info > NUMBER_OF_PACKS) {
|
||||
current_pack_info = 0;
|
||||
}
|
||||
|
||||
if (NUMBER_OF_PACKS > 0) { //div0 safeguard
|
||||
//We calculate how much each emulated pack should show
|
||||
voltage_per_pack = (datalayer.battery.status.voltage_dV / NUMBER_OF_PACKS);
|
||||
current_per_pack = (datalayer.battery.status.current_dA / NUMBER_OF_PACKS);
|
||||
if (datalayer.battery.status.temperature_max_dC >= 0) {
|
||||
temperature_max_per_pack = (uint8_t)((datalayer.battery.status.temperature_max_dC / 10) + 40);
|
||||
} else { // negative values, cap to 0*C for now. Most LFPs are not allowed to go below 0*C.
|
||||
temperature_max_per_pack = 0;
|
||||
} //TODO, make this configurable based on if we detect LFP or not, same as in MODBUS-BYD
|
||||
if (datalayer.battery.status.temperature_min_dC >= 0) {
|
||||
temperature_min_per_pack = (uint8_t)((datalayer.battery.status.temperature_min_dC / 10) + 40);
|
||||
} else { // negative values, cap to 0*C for now. Most LFPs are not allowed to go below 0*C.
|
||||
temperature_min_per_pack = 0;
|
||||
} //TODO, make this configurable based on if we detect LFP or not, same as in MODBUS-BYD
|
||||
}
|
||||
|
||||
// Individual pack data
|
||||
// Pack 1
|
||||
FOXESS_0C05.data.u8[0] = (uint8_t)current_per_pack;
|
||||
FOXESS_0C05.data.u8[1] = (current_per_pack >> 8);
|
||||
FOXESS_0C05.data.u8[2] = (uint8_t)temperature_max_per_pack;
|
||||
FOXESS_0C05.data.u8[3] = (uint8_t)temperature_min_per_pack;
|
||||
FOXESS_0C05.data.u8[4] = (uint8_t)(datalayer.battery.status.reported_soc / 100);
|
||||
FOXESS_0C05.data.u8[5] = 0x0A; //b5-7chg/dis?
|
||||
FOXESS_0C05.data.u8[6] = 0xD0; //pack_1_volts (53.456V) //TODO, does hardcoded value work?
|
||||
FOXESS_0C05.data.u8[7] = 0xD0; //pack_1_volts (53.456V) //Or shall we put in 'voltage_per_pack'
|
||||
|
||||
// Pack 2
|
||||
FOXESS_0C06.data.u8[0] = (uint8_t)current_per_pack;
|
||||
FOXESS_0C06.data.u8[1] = (current_per_pack >> 8);
|
||||
FOXESS_0C06.data.u8[2] = (uint8_t)temperature_max_per_pack;
|
||||
FOXESS_0C06.data.u8[3] = (uint8_t)temperature_min_per_pack;
|
||||
FOXESS_0C06.data.u8[4] = (uint8_t)(datalayer.battery.status.reported_soc / 100);
|
||||
FOXESS_0C06.data.u8[5] = 0x0A; //b5-7chg/dis?
|
||||
FOXESS_0C06.data.u8[6] = 0xD0; //pack_1_volts (53.456V)
|
||||
FOXESS_0C06.data.u8[7] = 0xD0; //pack_1_volts (53.456V)
|
||||
|
||||
// Pack 3
|
||||
FOXESS_0C07.data.u8[0] = (uint8_t)current_per_pack;
|
||||
FOXESS_0C07.data.u8[1] = (current_per_pack >> 8);
|
||||
FOXESS_0C07.data.u8[2] = (uint8_t)temperature_max_per_pack;
|
||||
FOXESS_0C07.data.u8[3] = (uint8_t)temperature_min_per_pack;
|
||||
FOXESS_0C07.data.u8[4] = (uint8_t)(datalayer.battery.status.reported_soc / 100);
|
||||
FOXESS_0C07.data.u8[5] = 0x0A; //b5-7chg/dis?
|
||||
FOXESS_0C07.data.u8[6] = 0xD0; //pack_1_volts (53.456V)
|
||||
FOXESS_0C07.data.u8[7] = 0xD0; //pack_1_volts (53.456V)
|
||||
|
||||
// Pack 4
|
||||
FOXESS_0C08.data.u8[0] = (uint8_t)current_per_pack;
|
||||
FOXESS_0C08.data.u8[1] = (current_per_pack >> 8);
|
||||
FOXESS_0C08.data.u8[2] = (uint8_t)temperature_max_per_pack;
|
||||
FOXESS_0C08.data.u8[3] = (uint8_t)temperature_min_per_pack;
|
||||
FOXESS_0C08.data.u8[4] = (uint8_t)(datalayer.battery.status.reported_soc / 100);
|
||||
FOXESS_0C08.data.u8[5] = 0x0A; //b5-7chg/dis?
|
||||
FOXESS_0C08.data.u8[6] = 0xD0; //pack_1_volts (53.456V)
|
||||
FOXESS_0C08.data.u8[7] = 0xD0; //pack_1_volts (53.456V)
|
||||
|
||||
// Pack 5
|
||||
FOXESS_0C09.data.u8[0] = (uint8_t)current_per_pack;
|
||||
FOXESS_0C09.data.u8[1] = (current_per_pack >> 8);
|
||||
FOXESS_0C09.data.u8[2] = (uint8_t)temperature_max_per_pack;
|
||||
FOXESS_0C09.data.u8[3] = (uint8_t)temperature_min_per_pack;
|
||||
FOXESS_0C09.data.u8[4] = (uint8_t)(datalayer.battery.status.reported_soc / 100);
|
||||
FOXESS_0C09.data.u8[5] = 0x0A; //b5-7chg/dis?
|
||||
FOXESS_0C09.data.u8[6] = 0xD0; //pack_1_volts (53.456V)
|
||||
FOXESS_0C09.data.u8[7] = 0xD0; //pack_1_volts (53.456V)
|
||||
|
||||
// Pack 6
|
||||
FOXESS_0C0A.data.u8[0] = (uint8_t)current_per_pack;
|
||||
FOXESS_0C0A.data.u8[1] = (current_per_pack >> 8);
|
||||
FOXESS_0C0A.data.u8[2] = (uint8_t)temperature_max_per_pack;
|
||||
FOXESS_0C0A.data.u8[3] = (uint8_t)temperature_min_per_pack;
|
||||
FOXESS_0C0A.data.u8[4] = (uint8_t)(datalayer.battery.status.reported_soc / 100);
|
||||
FOXESS_0C0A.data.u8[5] = 0x0A; //b5-7chg/dis?
|
||||
FOXESS_0C0A.data.u8[6] = 0xD0; //pack_1_volts (53.456V)
|
||||
FOXESS_0C0A.data.u8[7] = 0xD0; //pack_1_volts (53.456V)
|
||||
|
||||
// Pack 7
|
||||
FOXESS_0C0B.data.u8[0] = (uint8_t)current_per_pack;
|
||||
FOXESS_0C0B.data.u8[1] = (current_per_pack >> 8);
|
||||
FOXESS_0C0B.data.u8[2] = (uint8_t)temperature_max_per_pack;
|
||||
FOXESS_0C0B.data.u8[3] = (uint8_t)temperature_min_per_pack;
|
||||
FOXESS_0C0B.data.u8[4] = (uint8_t)(datalayer.battery.status.reported_soc / 100);
|
||||
FOXESS_0C0B.data.u8[5] = 0x0A; //b5-7chg/dis?
|
||||
FOXESS_0C0B.data.u8[6] = 0xD0; //pack_1_volts (53.456V)
|
||||
FOXESS_0C0B.data.u8[7] = 0xD0; //pack_1_volts (53.456V)
|
||||
|
||||
// Pack 8
|
||||
FOXESS_0C0C.data.u8[0] = (uint8_t)current_per_pack;
|
||||
FOXESS_0C0C.data.u8[1] = (current_per_pack >> 8);
|
||||
FOXESS_0C0C.data.u8[2] = (uint8_t)temperature_max_per_pack;
|
||||
FOXESS_0C0C.data.u8[3] = (uint8_t)temperature_min_per_pack;
|
||||
FOXESS_0C0C.data.u8[4] = (uint8_t)(datalayer.battery.status.reported_soc / 100);
|
||||
FOXESS_0C0C.data.u8[5] = 0x0A; //b5-7chg/dis?
|
||||
FOXESS_0C0C.data.u8[6] = 0xD0; //pack_1_volts (53.456V)
|
||||
FOXESS_0C0C.data.u8[7] = 0xD0; //pack_1_volts (53.456V)
|
||||
|
||||
//Cellvoltages
|
||||
/*
|
||||
FOXESS_0C1D.data.u8[0] = (uint8_t)datalayer.battery.status.cell_max_voltage_mV;
|
||||
FOXESS_0C1D.data.u8[1] = (datalayer.battery.status.cell_max_voltage_mV >> 8);
|
||||
FOXESS_0C1D.data.u8[2] =
|
||||
FOXESS_0C1D.data.u8[3] =
|
||||
FOXESS_0C1D.data.u8[4] =
|
||||
FOXESS_0C1D.data.u8[5] =
|
||||
FOXESS_0C1D.data.u8[6] =
|
||||
FOXESS_0C1D.data.u8[7] =
|
||||
*/
|
||||
//TODO map cells somehow in a smart way, extend the 96S of a typical EV battery to 144 cells?
|
||||
// Really it does not matter much, since we already send max and min cellvoltage, but we just need to satisfy the inverter
|
||||
|
||||
//Temperatures
|
||||
//TODO, should we even bother mapping all these fake values? We already send min and max temperature,
|
||||
// So do we really need to map the same two values over and over to 32 places?
|
||||
}
|
||||
|
||||
void send_can_inverter() { // This function loops as fast as possible
|
||||
|
||||
if (send_cellvoltages) {
|
||||
unsigned long currentMillis = millis(); // Get the current time
|
||||
|
||||
// Check if enough time has passed since the last batch
|
||||
if (currentMillis - previousMillisCellvoltage >= INTERVAL_10_MS) {
|
||||
previousMillisCellvoltage = currentMillis; // Update the time of the last message batch
|
||||
|
||||
// Send a subset of messages per iteration to avoid overloading the CAN bus / transmit buffer
|
||||
switch (can_message_cellvolt_index) {
|
||||
case 0:
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Sending large batch");
|
||||
#endif
|
||||
transmit_can(&FOXESS_0C1D, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C21, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C29, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C2D, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C31, can_config.inverter);
|
||||
break;
|
||||
case 1:
|
||||
transmit_can(&FOXESS_0C35, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C39, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C3D, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C41, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C45, can_config.inverter);
|
||||
break;
|
||||
case 2:
|
||||
transmit_can(&FOXESS_0C49, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C4D, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C51, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C55, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C59, can_config.inverter);
|
||||
break;
|
||||
case 3:
|
||||
transmit_can(&FOXESS_0C5D, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C61, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C65, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C69, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C6D, can_config.inverter);
|
||||
break;
|
||||
case 4:
|
||||
transmit_can(&FOXESS_0C71, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C75, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C79, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C7D, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C81, can_config.inverter);
|
||||
break;
|
||||
case 5:
|
||||
transmit_can(&FOXESS_0C85, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C89, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C8D, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C91, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C95, can_config.inverter);
|
||||
break;
|
||||
case 6:
|
||||
transmit_can(&FOXESS_0C99, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C9D, can_config.inverter);
|
||||
transmit_can(&FOXESS_0CA1, can_config.inverter);
|
||||
transmit_can(&FOXESS_0CA5, can_config.inverter);
|
||||
transmit_can(&FOXESS_0CA9, can_config.inverter);
|
||||
break;
|
||||
case 7: //Celltemperatures
|
||||
transmit_can(&FOXESS_0D21, can_config.inverter);
|
||||
transmit_can(&FOXESS_0D29, can_config.inverter);
|
||||
transmit_can(&FOXESS_0D31, can_config.inverter);
|
||||
transmit_can(&FOXESS_0D39, can_config.inverter);
|
||||
transmit_can(&FOXESS_0D41, can_config.inverter);
|
||||
transmit_can(&FOXESS_0D49, can_config.inverter);
|
||||
transmit_can(&FOXESS_0D51, can_config.inverter);
|
||||
transmit_can(&FOXESS_0D59, can_config.inverter);
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Sending completed");
|
||||
#endif
|
||||
send_cellvoltages = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Increment message index and wrap around if needed
|
||||
can_message_cellvolt_index++;
|
||||
|
||||
if (send_cellvoltages == false) {
|
||||
can_message_cellvolt_index = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void receive_can_inverter(CAN_frame rx_frame) {
|
||||
|
||||
if (rx_frame.ID == 0x1871) {
|
||||
inverterStillAlive = CAN_STILL_ALIVE;
|
||||
if (rx_frame.data.u8[0] == 0x03) { //0x1871 [0x03, 0x06, 0x17, 0x05, 0x09, 0x09, 0x28, 0x22]
|
||||
//This message is sent by the inverter every '6' seconds (0.5s after the pack serial numbers)
|
||||
//and contains a timestamp in bytes 2-7 i.e. <YY>,<MM>,<DD>,<HH>,<mm>,<ss>
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Inverter sends current time and date");
|
||||
#endif
|
||||
} else if (rx_frame.data.u8[0] == 0x01) {
|
||||
if (rx_frame.data.u8[4] == 0x00) {
|
||||
// Inverter wants to know bms info (every 1s)
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Inverter requests 1s BMS info, we reply");
|
||||
#endif
|
||||
transmit_can(&FOXESS_1872, can_config.inverter);
|
||||
transmit_can(&FOXESS_1873, can_config.inverter);
|
||||
transmit_can(&FOXESS_1874, can_config.inverter);
|
||||
transmit_can(&FOXESS_1875, can_config.inverter);
|
||||
transmit_can(&FOXESS_1876, can_config.inverter);
|
||||
transmit_can(&FOXESS_1877, can_config.inverter);
|
||||
transmit_can(&FOXESS_1878, can_config.inverter);
|
||||
transmit_can(&FOXESS_1879, can_config.inverter);
|
||||
} else if (rx_frame.data.u8[4] == 0x01) { // b4 0x01 , 0x1871 [0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00]
|
||||
//Inverter wants to know all individual cellvoltages (occurs 6 seconds after valid BMS reply)
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Inverter requests individual battery pack status, we reply");
|
||||
#endif
|
||||
transmit_can(&FOXESS_0C05, can_config.inverter); //TODO, should we limit this incase NUMBER_OF_PACKS =! 8?
|
||||
transmit_can(&FOXESS_0C06, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C07, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C08, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C09, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C0A, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C0B, can_config.inverter);
|
||||
transmit_can(&FOXESS_0C0C, can_config.inverter);
|
||||
} else if (rx_frame.data.u8[4] == 0x04) { // b4 0x01 , 0x1871 [0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00]
|
||||
//Inverter wants to know all individual cellvoltages (occurs 6 seconds after valid BMS reply)
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Inverter requests cellvoltages and temps, we reply");
|
||||
#endif
|
||||
send_cellvoltages = true;
|
||||
}
|
||||
} else if (rx_frame.data.u8[0] == 0x02) { //0x1871 [0x02, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00]
|
||||
// Ack message
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Inverter acks, no reply needed");
|
||||
#endif
|
||||
} else if (rx_frame.data.u8[0] == 0x05) { //0x1871 [0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]
|
||||
#ifdef DEBUG_VIA_USB
|
||||
Serial.println("Inverter wants to know serial numbers, we reply");
|
||||
#endif
|
||||
for (uint8_t i = 0; i < (NUMBER_OF_PACKS + 1); i++) {
|
||||
FOXESS_1881.data.u8[0] = (uint8_t)i;
|
||||
FOXESS_1882.data.u8[0] = (uint8_t)i;
|
||||
FOXESS_1883.data.u8[0] = (uint8_t)i;
|
||||
//TODO, should we add something to serial number field?
|
||||
transmit_can(&FOXESS_1881, can_config.inverter);
|
||||
transmit_can(&FOXESS_1882, can_config.inverter);
|
||||
transmit_can(&FOXESS_1883, can_config.inverter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
9
Software/src/inverter/FOXESS-CAN.h
Normal file
9
Software/src/inverter/FOXESS-CAN.h
Normal file
|
@ -0,0 +1,9 @@
|
|||
#ifndef FOXESS_CAN_H
|
||||
#define FOXESS_CAN_H
|
||||
#include "../include.h"
|
||||
|
||||
#define CAN_INVERTER_SELECTED
|
||||
|
||||
void transmit_can(CAN_frame* tx_frame, int interface);
|
||||
|
||||
#endif
|
|
@ -15,8 +15,8 @@
|
|||
#include "BYD-SMA.h"
|
||||
#endif
|
||||
|
||||
#ifdef LUNA2000_MODBUS
|
||||
#include "LUNA2000-MODBUS.h"
|
||||
#ifdef FOXESS_CAN
|
||||
#include "FOXESS-CAN.h"
|
||||
#endif
|
||||
|
||||
#ifdef PYLON_CAN
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
#include "../include.h"
|
||||
#ifdef LUNA2000_MODBUS
|
||||
#include "../datalayer/datalayer.h"
|
||||
#include "LUNA2000-MODBUS.h"
|
||||
|
||||
void update_modbus_registers_inverter() {
|
||||
handle_update_data_modbus32051();
|
||||
handle_update_data_modbus39500();
|
||||
}
|
||||
|
||||
void handle_update_data_modbus32051() {
|
||||
// Store the data into the array
|
||||
static uint16_t system_data[9];
|
||||
system_data[0] = 1;
|
||||
system_data[1] = 534; //Goes between 534-498 depending on SOC
|
||||
system_data[2] = 110; //Goes between 110- -107 [NOTE, SIGNED VALUE]
|
||||
system_data[3] = 0; //Goes between 0 and -1 [NOTE, SIGNED VALUE]
|
||||
system_data[4] = 616; //Goes between 616- -520 [NOTE, SIGNED VALUE]
|
||||
system_data[5] = datalayer.battery.status.temperature_max_dC; //Temperature max?
|
||||
system_data[6] = datalayer.battery.status.temperature_min_dC; //Temperature min?
|
||||
system_data[7] = (datalayer.battery.status.reported_soc / 100); //SOC 0-100%, no decimals
|
||||
system_data[8] = 98; //Always 98 in logs
|
||||
static uint16_t i = 2051;
|
||||
memcpy(&mbPV[i], system_data, sizeof(system_data));
|
||||
}
|
||||
|
||||
void handle_update_data_modbus39500() {
|
||||
// Store the data into the array
|
||||
static uint16_t system_data[26];
|
||||
system_data[0] = 0;
|
||||
system_data[1] = datalayer.battery.info.total_capacity_Wh; //Capacity? 5000 with 5kWh battery
|
||||
system_data[2] = 0;
|
||||
system_data[3] = datalayer.battery.info.total_capacity_Wh; //Capacity? 5000 with 5kWh battery
|
||||
system_data[4] = 0;
|
||||
system_data[5] = 2500; //???
|
||||
system_data[6] = 0;
|
||||
system_data[7] = 2500; //???
|
||||
system_data[8] = (datalayer.battery.status.reported_soc / 100); //SOC 0-100%, no decimals
|
||||
system_data[9] =
|
||||
1; //Running status, equiv to register 37762, 0 = Offline, 1 = Standby,2 = Running, 3 = Fault, 4 = sleep mode
|
||||
system_data[10] = datalayer.battery.status.voltage_dV; //Battery bus voltage (766.5V = 7665)
|
||||
system_data[11] = 9; //TODO: GOES LOWER WITH LOW SOC
|
||||
system_data[12] = 0;
|
||||
system_data[13] = 699; //TODO: GOES LOWER WITH LOW SOC
|
||||
system_data[14] = 1; //Always 1 in logs
|
||||
system_data[15] = 18; //Always 18 in logs
|
||||
system_data[16] = 8066; //TODO: GOES HIGHER WITH LOW SOC (max allowed charge W?)
|
||||
system_data[17] = 17;
|
||||
system_data[18] = 44027; //TODO: GOES LOWER WITH LOW SOC
|
||||
system_data[19] = 0;
|
||||
system_data[20] = 435; //Always 435 in logs
|
||||
system_data[21] = 0;
|
||||
system_data[22] = 0;
|
||||
system_data[23] = 0;
|
||||
system_data[24] = (datalayer.battery.status.reported_soc / 10); //SOC 0-100.0%, 1x decimal
|
||||
system_data[25] = 0;
|
||||
system_data[26] = 1; //Always 1 in logs
|
||||
static uint16_t i = 9500;
|
||||
memcpy(&mbPV[i], system_data, sizeof(system_data));
|
||||
}
|
||||
#endif
|
|
@ -1,13 +0,0 @@
|
|||
#ifndef LUNA2000_MODBUS_H
|
||||
#define LUNA2000_MODBUS_H
|
||||
#include "../include.h"
|
||||
|
||||
#define MODBUS_INVERTER_SELECTED
|
||||
|
||||
#define MB_RTU_NUM_VALUES 30000
|
||||
|
||||
extern uint16_t mbPV[MB_RTU_NUM_VALUES];
|
||||
|
||||
void handle_update_data_modbus32051();
|
||||
void handle_update_data_modbus39500();
|
||||
#endif
|
|
@ -447,6 +447,7 @@ void update_values_can_inverter() { //This function maps all the values fetched
|
|||
void receive_can_inverter(CAN_frame rx_frame) {
|
||||
switch (rx_frame.ID) {
|
||||
case 0x4200: //Message originating from inverter. Depending on which data is required, act accordingly
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
if (rx_frame.data.u8[0] == 0x02) {
|
||||
send_setup_info();
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
/* TODO: Map error bits in 0x158 */
|
||||
|
||||
/* Do not change code below unless you are sure what you are doing */
|
||||
static unsigned long previousMillis60s = 0;
|
||||
static unsigned long previousMillis100ms = 0;
|
||||
|
||||
//Actual content messages
|
||||
CAN_frame SMA_558 = {.FD = false,
|
||||
|
@ -211,25 +211,33 @@ void update_values_can_inverter() { //This function maps all the values fetched
|
|||
void receive_can_inverter(CAN_frame rx_frame) {
|
||||
switch (rx_frame.ID) {
|
||||
case 0x360: //Message originating from SMA inverter - Voltage and current
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
//Frame0-1 Voltage
|
||||
//Frame2-3 Current
|
||||
break;
|
||||
case 0x3E0: //Message originating from SMA inverter - ?
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
break;
|
||||
case 0x420: //Message originating from SMA inverter - Timestamp
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
//Frame0-3 Timestamp
|
||||
/*
|
||||
transmit_can(&SMA_158, can_config.inverter);
|
||||
transmit_can(&SMA_358, can_config.inverter);
|
||||
transmit_can(&SMA_3D8, can_config.inverter);
|
||||
transmit_can(&SMA_458, can_config.inverter);
|
||||
transmit_can(&SMA_518, can_config.inverter);
|
||||
transmit_can(&SMA_4D8, can_config.inverter);
|
||||
*/
|
||||
break;
|
||||
case 0x5E0: //Message originating from SMA inverter - String
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
break;
|
||||
case 0x560: //Message originating from SMA inverter - Init
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
break;
|
||||
case 0x5E7: //Pairing request
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
transmit_can(&SMA_558, can_config.inverter);
|
||||
transmit_can(&SMA_598, can_config.inverter);
|
||||
transmit_can(&SMA_5D8, can_config.inverter);
|
||||
|
@ -251,9 +259,10 @@ void receive_can_inverter(CAN_frame rx_frame) {
|
|||
void send_can_inverter() {
|
||||
unsigned long currentMillis = millis();
|
||||
|
||||
// Send CAN Message every 60s
|
||||
if (currentMillis - previousMillis60s >= INTERVAL_60_S) {
|
||||
previousMillis60s = currentMillis;
|
||||
// Send CAN Message every 100ms if Enable line is HIGH
|
||||
if (datalayer.system.status.inverter_allows_contactor_closing) {
|
||||
if (currentMillis - previousMillis100ms >= 100) {
|
||||
previousMillis100ms = currentMillis;
|
||||
|
||||
transmit_can(&SMA_158, can_config.inverter);
|
||||
transmit_can(&SMA_358, can_config.inverter);
|
||||
|
@ -263,4 +272,5 @@ void send_can_inverter() {
|
|||
transmit_can(&SMA_4D8, can_config.inverter);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -292,15 +292,20 @@ void update_values_can_inverter() { //This function maps all the values fetched
|
|||
void receive_can_inverter(CAN_frame rx_frame) {
|
||||
switch (rx_frame.ID) {
|
||||
case 0x00D: //Inverter Measurements
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
break;
|
||||
case 0x00F: //Inverter Feedback
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
break;
|
||||
case 0x010: //Time from inverter
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
break;
|
||||
case 0x015: //Initialization message from inverter
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
send_tripower_init();
|
||||
break;
|
||||
case 0x017: //Initialization message from inverter 2
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
//send_tripower_init();
|
||||
break;
|
||||
default:
|
||||
|
|
|
@ -233,10 +233,12 @@ void update_values_can_inverter() { //This function maps all the values fetched
|
|||
void receive_can_inverter(CAN_frame rx_frame) {
|
||||
switch (rx_frame.ID) { //In here we need to respond to the inverter. TODO: make logic
|
||||
case 0x605:
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
//frame1_605 = rx_frame.data.u8[1];
|
||||
//frame3_605 = rx_frame.data.u8[3];
|
||||
break;
|
||||
case 0x705:
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
//frame1_705 = rx_frame.data.u8[1];
|
||||
//frame3_705 = rx_frame.data.u8[3];
|
||||
break;
|
||||
|
|
|
@ -129,6 +129,14 @@ void update_values_can_inverter() { //This function maps all the values fetched
|
|||
}
|
||||
}
|
||||
|
||||
//Cap the value according to user settings. Some inverters cannot handle large values.
|
||||
if ((max_charge_rate_amp * 10) > datalayer.battery.info.max_charge_amp_dA) {
|
||||
max_charge_rate_amp = (datalayer.battery.info.max_charge_amp_dA / 10);
|
||||
}
|
||||
if ((max_discharge_rate_amp * 10) > datalayer.battery.info.max_discharge_amp_dA) {
|
||||
max_discharge_rate_amp = (datalayer.battery.info.max_discharge_amp_dA / 10);
|
||||
}
|
||||
|
||||
// Batteries might be larger than uint16_t value can take
|
||||
if (datalayer.battery.info.total_capacity_Wh > 65000) {
|
||||
capped_capacity_Wh = 65000;
|
||||
|
@ -210,6 +218,11 @@ void send_can_inverter() {
|
|||
}
|
||||
|
||||
void receive_can_inverter(CAN_frame rx_frame) {
|
||||
|
||||
if (rx_frame.ID == 0x1871) {
|
||||
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||
}
|
||||
|
||||
if (rx_frame.ID == 0x1871 && rx_frame.data.u8[0] == (0x01) ||
|
||||
rx_frame.ID == 0x1871 && rx_frame.data.u8[0] == (0x02)) {
|
||||
LastFrameTime = millis();
|
||||
|
|
|
@ -37,3 +37,4 @@ node_modules
|
|||
.vscode
|
||||
/build
|
||||
/portal
|
||||
.pio
|
||||
|
|
234
Software/src/lib/ayushsharma82-ElegantOTA/CurrentPlainHTML.txt
Normal file
234
Software/src/lib/ayushsharma82-ElegantOTA/CurrentPlainHTML.txt
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,19 +1,24 @@
|
|||
<p><br/></p>
|
||||
<p align="center"><img src="/docs/feature.png?sanitize=true&raw=true" width="700"></p>
|
||||
<p align="center">
|
||||
<a href="https://elegantota.pro?ref=ghfeature" target="_blank">
|
||||
<img src="https://raw.githubusercontent.com/ayushsharma82/ElegantOTA/master/docs/feature.png" width="1200"></p>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
<p align="center">
|
||||
|
||||
<p>
|
||||
<img src="https://img.shields.io/github/last-commit/ayushsharma82/ElegantOTA.svg?style=for-the-badge" />
|
||||
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/ayushsharma82/ElegantOTA/ci.yml?branch=master&style=for-the-badge" />
|
||||
|
||||
<img src="https://img.shields.io/github/license/ayushsharma82/ElegantOTA.svg?style=for-the-badge" />
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
|
||||
<p>Over-the-Air update library for wireless microcontrollers</p>
|
||||
|
||||
<p align="center">OTA update library for wireless microcontrollers</p>
|
||||
<p align="center">
|
||||
<p>
|
||||
ElegantOTA provides a beautiful user interface to upload over-the-air firmware/filesystem updates to your hardware with precise status and progress. ElegantOTA is designed to make the process of OTA updates slick and simple!
|
||||
</p>
|
||||
|
||||
|
@ -21,6 +26,7 @@ ElegantOTA provides a beautiful user interface to upload over-the-air firmware/f
|
|||
<br/>
|
||||
|
||||
## Features
|
||||
|
||||
- 🔥 Quick & Simple OTA procedure
|
||||
- 🏀 Get useful insight on progress and status of your OTA update
|
||||
- 🎷 No need to learn HTML/CSS/JS
|
||||
|
@ -29,7 +35,9 @@ ElegantOTA provides a beautiful user interface to upload over-the-air firmware/f
|
|||
<br/>
|
||||
|
||||
## Supported MCUs
|
||||
|
||||
ElegantOTA works on the following microcontrollers/boards:
|
||||
|
||||
- ESP8266
|
||||
- ESP32
|
||||
- RP2040 ( Pico W )
|
||||
|
@ -51,12 +59,12 @@ ElegantOTA works on the following microcontrollers/boards:
|
|||
*Preview might appear as blurry due to image optimization.*
|
||||
<br>
|
||||
|
||||
<img src="/docs/demo.gif?raw=true" width="600">
|
||||
<img src="https://raw.githubusercontent.com/ayushsharma82/ElegantOTA/master/docs/demo.gif" width="600">
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## Want More? Upgrade to Pro
|
||||
## Looking for more? Upgrade to Pro.
|
||||
|
||||
ElegantOTA Pro comes with the following extended functionality:
|
||||
- New Drag & Drop Zone
|
||||
|
@ -74,7 +82,7 @@ ElegantOTA Pro comes with the following extended functionality:
|
|||
<br/>
|
||||
|
||||
<a href="https://elegantota.pro" target="_blank">
|
||||
<img src="/docs/pro-preview.jpg" alt="ElegantOTA Pro" width="600">
|
||||
<img src="https://raw.githubusercontent.com/ayushsharma82/ElegantOTA/master/docs/pro-preview.jpg" alt="ElegantOTA Pro" width="600">
|
||||
</a>
|
||||
|
||||
<br>
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
Modifying HTML for the ElegantOTA Library
|
||||
|
||||
This guide provides the necessary steps to update and modify the HTML used in the ElegantOTA library.
|
||||
Follow these steps carefully to ensure that your changes are properly integrated.
|
||||
|
||||
Steps to Modify the HTML:
|
||||
|
||||
1 - Edit the CurrentPlainHTML.txt:
|
||||
|
||||
Locate the file CurrentPlainHTML.txt in your project directory.
|
||||
Modify the HTML content as needed. This file contains the plain HTML code that will be served through the OTA interface.
|
||||
|
||||
Convert the HTML to GZIP and Decimal Format:
|
||||
|
||||
Copy the content of the updated CurrentPlainHTML.txt.
|
||||
Navigate to the CyberChef tool for encoding and compression.
|
||||
Apply the following recipe:
|
||||
Gzip with the "Dynamic Huffman Coding" option enabled.
|
||||
Convert to Decimal with a comma separator.
|
||||
Use this link for the process: https://gchq.github.io/CyberChef/#recipe=Gzip('Dynamic%20Huffman%20Coding','','',false)To_Decimal('Comma',false)
|
||||
|
||||
2 - Update the ELEGANT_HTML Array:
|
||||
|
||||
Copy the resulting decimal output from CyberChef.
|
||||
Replace the existing content of the ELEGANT_HTML array in elop.cpp with the new decimal data from CyberChef.
|
||||
|
||||
3 - Adjust the ELEGANT_HTML Array Size:
|
||||
|
||||
After updating the ELEGANT_HTML array in both elop.h and elop.cpp, update the array size to match the length of the new output from CyberChef.
|
||||
Ensure that the array size reflects the new length of the compressed HTML to avoid errors during compilation.
|
|
@ -15,7 +15,18 @@
|
|||
"maintainer": true
|
||||
}
|
||||
],
|
||||
"version": "3.1.1",
|
||||
"frameworks": "arduino",
|
||||
"platforms": ["espressif8266", "espressif32", "raspberrypi"]
|
||||
"dependencies": [
|
||||
{
|
||||
"owner": "mathieucarbou",
|
||||
"name": "ESPAsyncWebServer",
|
||||
"version": "^3.1.1",
|
||||
"platforms": ["espressif8266", "espressif32"]
|
||||
}
|
||||
],
|
||||
"version": "3.1.5",
|
||||
"frameworks": "arduino",
|
||||
"platforms": ["espressif8266", "espressif32", "raspberrypi"],
|
||||
"build": {
|
||||
"libCompatMode": "strict"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
name=ElegantOTA
|
||||
version=3.1.1
|
||||
version=3.1.5
|
||||
author=Ayush Sharma
|
||||
category=Communication
|
||||
maintainer=Ayush Sharma <asrocks5@gmail.com>
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
#
|
||||
# extra_scripts = platformio_upload.py
|
||||
# upload_protocol = custom
|
||||
# upload_url = <your upload URL>
|
||||
# custom_upload_url = <your upload URL>
|
||||
#
|
||||
# An example of an upload URL:
|
||||
# upload_url = http://192.168.1.123/update
|
||||
# also possible: upload_url = http://domainname/update
|
||||
# custom_upload_url = http://192.168.1.123/update
|
||||
# also possible: custom_upload_url = http://domainname/update
|
||||
|
||||
import sys
|
||||
import requests
|
||||
import hashlib
|
||||
from urllib.parse import urlparse
|
||||
|
@ -54,7 +55,10 @@ def on_upload(source, target, env):
|
|||
'Connection': 'keep-alive'
|
||||
}
|
||||
|
||||
try:
|
||||
checkAuthResponse = requests.get(f"{upload_url_compatibility}/update")
|
||||
except Exception as e:
|
||||
return 'Error checking auth: ' + repr(e)
|
||||
|
||||
if checkAuthResponse.status_code == 401:
|
||||
try:
|
||||
|
@ -66,24 +70,27 @@ def on_upload(source, target, env):
|
|||
print("No authentication values specified.")
|
||||
print('Please, add some Options in your .ini file like: \n\ncustom_username=username\ncustom_password=password\n')
|
||||
if username is None or password is None:
|
||||
print("Authentication required, but no credentials provided.")
|
||||
return
|
||||
return "Authentication required, but no credentials provided."
|
||||
print("Serverconfiguration: authentication needed.")
|
||||
auth = HTTPDigestAuth(username, password)
|
||||
try:
|
||||
doUpdateAuth = requests.get(start_url, headers=start_headers, auth=auth)
|
||||
except Exception as e:
|
||||
return 'Error while authenticating: ' + repr(e)
|
||||
|
||||
if doUpdateAuth.status_code != 200:
|
||||
print("authentication faild " + str(doUpdateAuth.status_code))
|
||||
return
|
||||
print("Authentication successfull")
|
||||
return "Authentication failed " + str(doUpdateAuth.status_code)
|
||||
print("Authentication successful")
|
||||
else:
|
||||
auth = None
|
||||
print("Serverconfiguration: autentication not needed.")
|
||||
print("Serverconfiguration: authentication not needed.")
|
||||
try:
|
||||
doUpdate = requests.get(start_url, headers=start_headers)
|
||||
except Exception as e:
|
||||
return 'Error while starting upload: ' + repr(e)
|
||||
|
||||
if doUpdate.status_code != 200:
|
||||
print("start-request faild " + str(doUpdate.status_code))
|
||||
return
|
||||
return "Start request failed " + str(doUpdate.status_code)
|
||||
|
||||
firmware.seek(0)
|
||||
encoder = MultipartEncoder(fields={
|
||||
|
@ -114,14 +121,16 @@ def on_upload(source, target, env):
|
|||
'Origin': f'{upload_url}'
|
||||
}
|
||||
|
||||
|
||||
try:
|
||||
response = requests.post(f"{upload_url}/ota/upload", data=monitor, headers=post_headers, auth=auth)
|
||||
except Exception as e:
|
||||
return 'Error while uploading: ' + repr(e)
|
||||
|
||||
bar.close()
|
||||
time.sleep(0.1)
|
||||
|
||||
if response.status_code != 200:
|
||||
message = "\nUpload faild.\nServer response: " + response.text
|
||||
message = "\nUpload failed.\nServer response: " + response.text
|
||||
tqdm.write(message)
|
||||
else:
|
||||
message = "\nUpload successful.\nServer response: " + response.text
|
||||
|
|
|
@ -16,16 +16,20 @@ void ElegantOTAClass::begin(ELEGANTOTA_WEBSERVER *server, const char * username,
|
|||
|
||||
#if ELEGANTOTA_USE_ASYNC_WEBSERVER == 1
|
||||
_server->on("/update", HTTP_GET, [&](AsyncWebServerRequest *request){
|
||||
if(_authenticate && !request->authenticate(_username, _password)){
|
||||
if(_authenticate && !request->authenticate(_username.c_str(), _password.c_str())){
|
||||
return request->requestAuthentication();
|
||||
}
|
||||
#if defined(ASYNCWEBSERVER_VERSION) && ASYNCWEBSERVER_VERSION_MAJOR > 2 // This means we are using recommended fork of AsyncWebServer
|
||||
AsyncWebServerResponse *response = request->beginResponse(200, "text/html", ELEGANT_HTML, sizeof(ELEGANT_HTML));
|
||||
#else
|
||||
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", ELEGANT_HTML, sizeof(ELEGANT_HTML));
|
||||
#endif
|
||||
response->addHeader("Content-Encoding", "gzip");
|
||||
request->send(response);
|
||||
});
|
||||
#else
|
||||
_server->on("/update", HTTP_GET, [&](){
|
||||
if (_authenticate && !_server->authenticate(_username, _password)) {
|
||||
if (_authenticate && !_server->authenticate(_username.c_str(), _password.c_str())) {
|
||||
return _server->requestAuthentication();
|
||||
}
|
||||
_server->sendHeader("Content-Encoding", "gzip");
|
||||
|
@ -35,10 +39,13 @@ void ElegantOTAClass::begin(ELEGANTOTA_WEBSERVER *server, const char * username,
|
|||
|
||||
#if ELEGANTOTA_USE_ASYNC_WEBSERVER == 1
|
||||
_server->on("/ota/start", HTTP_GET, [&](AsyncWebServerRequest *request) {
|
||||
if (_authenticate && !request->authenticate(_username, _password)) {
|
||||
if (_authenticate && !request->authenticate(_username.c_str(), _password.c_str())) {
|
||||
return request->requestAuthentication();
|
||||
}
|
||||
|
||||
// Pre-OTA update callback
|
||||
if (preUpdateCallback != NULL) preUpdateCallback();
|
||||
|
||||
// Get header x-ota-mode value, if present
|
||||
OTA_Mode mode = OTA_MODE_FIRMWARE;
|
||||
// Get mode from arg
|
||||
|
@ -69,7 +76,7 @@ void ElegantOTAClass::begin(ELEGANTOTA_WEBSERVER *server, const char * username,
|
|||
#endif
|
||||
|
||||
// Pre-OTA update callback
|
||||
if (preUpdateCallback != NULL) preUpdateCallback();
|
||||
//if (preUpdateCallback != NULL) preUpdateCallback();
|
||||
|
||||
// Start update process
|
||||
#if defined(ESP8266)
|
||||
|
@ -84,7 +91,7 @@ void ElegantOTAClass::begin(ELEGANTOTA_WEBSERVER *server, const char * username,
|
|||
StreamString str;
|
||||
Update.printError(str);
|
||||
_update_error_str = str.c_str();
|
||||
_update_error_str += "\n";
|
||||
_update_error_str.concat("\n");
|
||||
ELEGANTOTA_DEBUG_MSG(_update_error_str.c_str());
|
||||
}
|
||||
#elif defined(ESP32)
|
||||
|
@ -94,7 +101,7 @@ void ElegantOTAClass::begin(ELEGANTOTA_WEBSERVER *server, const char * username,
|
|||
StreamString str;
|
||||
Update.printError(str);
|
||||
_update_error_str = str.c_str();
|
||||
_update_error_str += "\n";
|
||||
_update_error_str.concat("\n");
|
||||
ELEGANTOTA_DEBUG_MSG(_update_error_str.c_str());
|
||||
}
|
||||
#endif
|
||||
|
@ -103,7 +110,7 @@ void ElegantOTAClass::begin(ELEGANTOTA_WEBSERVER *server, const char * username,
|
|||
});
|
||||
#else
|
||||
_server->on("/ota/start", HTTP_GET, [&]() {
|
||||
if (_authenticate && !_server->authenticate(_username, _password)) {
|
||||
if (_authenticate && !_server->authenticate(_username.c_str(), _password.c_str())) {
|
||||
return _server->requestAuthentication();
|
||||
}
|
||||
|
||||
|
@ -152,7 +159,7 @@ void ElegantOTAClass::begin(ELEGANTOTA_WEBSERVER *server, const char * username,
|
|||
StreamString str;
|
||||
Update.printError(str);
|
||||
_update_error_str = str.c_str();
|
||||
_update_error_str += "\n";
|
||||
_update_error_str.concat("\n");
|
||||
ELEGANTOTA_DEBUG_MSG(_update_error_str.c_str());
|
||||
}
|
||||
#elif defined(ESP32)
|
||||
|
@ -162,7 +169,7 @@ void ElegantOTAClass::begin(ELEGANTOTA_WEBSERVER *server, const char * username,
|
|||
StreamString str;
|
||||
Update.printError(str);
|
||||
_update_error_str = str.c_str();
|
||||
_update_error_str += "\n";
|
||||
_update_error_str.concat("\n");
|
||||
ELEGANTOTA_DEBUG_MSG(_update_error_str.c_str());
|
||||
}
|
||||
#elif defined(TARGET_RP2040)
|
||||
|
@ -191,7 +198,7 @@ void ElegantOTAClass::begin(ELEGANTOTA_WEBSERVER *server, const char * username,
|
|||
|
||||
#if ELEGANTOTA_USE_ASYNC_WEBSERVER == 1
|
||||
_server->on("/ota/upload", HTTP_POST, [&](AsyncWebServerRequest *request) {
|
||||
if(_authenticate && !request->authenticate(_username, _password)){
|
||||
if(_authenticate && !request->authenticate(_username.c_str(), _password.c_str())){
|
||||
return request->requestAuthentication();
|
||||
}
|
||||
// Post-OTA update callback
|
||||
|
@ -210,7 +217,7 @@ void ElegantOTAClass::begin(ELEGANTOTA_WEBSERVER *server, const char * username,
|
|||
}, [&](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||
//Upload handler chunks in data
|
||||
if(_authenticate){
|
||||
if(!request->authenticate(_username, _password)){
|
||||
if(!request->authenticate(_username.c_str(), _password.c_str())){
|
||||
return request->requestAuthentication();
|
||||
}
|
||||
}
|
||||
|
@ -236,7 +243,7 @@ void ElegantOTAClass::begin(ELEGANTOTA_WEBSERVER *server, const char * username,
|
|||
StreamString str;
|
||||
Update.printError(str);
|
||||
_update_error_str = str.c_str();
|
||||
_update_error_str += "\n";
|
||||
_update_error_str.concat("\n");
|
||||
ELEGANTOTA_DEBUG_MSG(_update_error_str.c_str());
|
||||
}
|
||||
}else{
|
||||
|
@ -245,7 +252,7 @@ void ElegantOTAClass::begin(ELEGANTOTA_WEBSERVER *server, const char * username,
|
|||
});
|
||||
#else
|
||||
_server->on("/ota/upload", HTTP_POST, [&](){
|
||||
if (_authenticate && !_server->authenticate(_username, _password)) {
|
||||
if (_authenticate && !_server->authenticate(_username.c_str(), _password.c_str())) {
|
||||
return _server->requestAuthentication();
|
||||
}
|
||||
// Post-OTA update callback
|
||||
|
@ -264,7 +271,7 @@ void ElegantOTAClass::begin(ELEGANTOTA_WEBSERVER *server, const char * username,
|
|||
HTTPUpload& upload = _server->upload();
|
||||
if (upload.status == UPLOAD_FILE_START) {
|
||||
// Check authentication
|
||||
if (_authenticate && !_server->authenticate(_username, _password)) {
|
||||
if (_authenticate && !_server->authenticate(_username.c_str(), _password.c_str())) {
|
||||
ELEGANTOTA_DEBUG_MSG("Authentication Failed on UPLOAD_FILE_START\n");
|
||||
return;
|
||||
}
|
||||
|
@ -289,7 +296,7 @@ void ElegantOTAClass::begin(ELEGANTOTA_WEBSERVER *server, const char * username,
|
|||
StreamString str;
|
||||
Update.printError(str);
|
||||
_update_error_str = str.c_str();
|
||||
_update_error_str += "\n";
|
||||
_update_error_str.concat("\n");
|
||||
ELEGANTOTA_DEBUG_MSG(_update_error_str.c_str());
|
||||
}
|
||||
|
||||
|
@ -304,11 +311,9 @@ void ElegantOTAClass::begin(ELEGANTOTA_WEBSERVER *server, const char * username,
|
|||
}
|
||||
|
||||
void ElegantOTAClass::setAuth(const char * username, const char * password){
|
||||
if (strlen(username) > 0 && strlen(password) > 0) {
|
||||
strlcpy(_username, username, sizeof(_username));
|
||||
strlcpy(_password, password, sizeof(_password));
|
||||
_authenticate = true;
|
||||
}
|
||||
_username = username;
|
||||
_password = password;
|
||||
_authenticate = _username.length() && _password.length();
|
||||
}
|
||||
|
||||
void ElegantOTAClass::clearAuth(){
|
||||
|
|
|
@ -119,8 +119,8 @@ class ElegantOTAClass{
|
|||
ELEGANTOTA_WEBSERVER *_server;
|
||||
|
||||
bool _authenticate;
|
||||
char _username[64];
|
||||
char _password[64];
|
||||
String _username;
|
||||
String _password;
|
||||
|
||||
bool _auto_reboot = true;
|
||||
bool _reboot = false;
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -3,6 +3,6 @@
|
|||
|
||||
#include <Arduino.h>
|
||||
|
||||
extern const uint8_t ELEGANT_HTML[9590];
|
||||
extern const uint8_t ELEGANT_HTML[41354];
|
||||
|
||||
#endif
|
||||
|
|
|
@ -160,7 +160,7 @@ class ACAN2517FDSettings {
|
|||
//······················································································································
|
||||
|
||||
//--- Driver transmit buffer size
|
||||
public: uint16_t mDriverTransmitFIFOSize = 16 ; // >= 0
|
||||
public: uint16_t mDriverTransmitFIFOSize = 22 ; // >= 0
|
||||
|
||||
//--- Controller transmit FIFO size
|
||||
public: uint8_t mControllerTransmitFIFOSize = 1 ; // 1 ... 32
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue