Follow along with the video below to see how to install our site as a web app on your home screen.
Note: This feature may not be available in some browsers.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <mcp_can.h>
#include <SPI.h>
// Константы для CAN
#define CAN_CS_PIN 8
#define CAN_SPEED CAN_250KBPS
#define CAN_CLOCK MCP_8MHZ
#define INTERRUPT_PIN 2
#define RELAY_PIN 6 // Пин для реле (LOW - включено, HIGH - выключено)
#define BUTTON_PIN 7 // Пин для кнопки (свободный)
// Объявление объекта CAN
MCP_CAN CAN0(CAN_CS_PIN);
// CAN ID для обработки (extended)
#define TARGET_CAN_ID 0x84F83401 // Для напряжения и температуры
#define TARGET_CAN_ID2 0x84F83400 // Для SOC (процентов)
#define SEND_CAN_ID 0x02F83204 // Extended ID
// Пины и настройки LCD (16x2)
LiquidCrystal_I2C lcd(0x27, 16, 2);
// Переменные для CAN
long unsigned int rxId;
unsigned char len = 0;
unsigned char rxBuf[8];
unsigned long lastUpdate = 0;
// Переменные для хранения значений
float lastVoltage = -1.0;
int lastTemperature = -100;
int lastSoc = -1; // Для SOC (процентов)
int RLOFFSoc = 0;
int soc = 0;
// Режимы работы: 0=80% (по умолчанию), 1=100%, 2=60%
int currentMode = 0;
int lastCurrentMode = 0;
const char* modeLabels[3] = {"to 85%", "to 100%", "to 60%"};
const float modeThresholds[3] = {85, 100, 61}; // Пороги отключения реле (соответственно порядку)
// Для debounce кнопки
unsigned long lastButtonPress = 0;
const unsigned long debounceDelay = 200; // 200 мс
// Для анимации бегущих точек (индикатор CAN-пакета)
bool animationActive = false;
unsigned long animationStart = 0;
const unsigned long animationStep = 500; // мс на шаг анимации
// Переменная для отслеживания статуса температуры (для обновления LCD)
int lastTempStatus = -1; // -1: не инициализировано, 0: normal, 1: cold, 2: hot
void setup() {
pinMode(RELAY_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP); // Кнопка с внутренней подтяжкой
Serial.begin(115200);
// Инициализация CAN
if (CAN0.begin(MCP_STDEXT, CAN_SPEED, CAN_CLOCK) == CAN_OK) {
Serial.println("MCP2515 Init Okay!");
} else {
Serial.println("MCP2515 Init Failed!");
while (1); // Остановить программу при ошибке
}
pinMode(INTERRUPT_PIN, INPUT); // Пин для прерывания от CAN
CAN0.setMode(MCP_NORMAL); // Нормальный режим для передачи/приема
digitalWrite(RELAY_PIN, HIGH); // Выключить реле по умолчанию (HIGH - выключено)
// Инициализация LCD
lcd.init();
lcd.backlight();
lcd.clear();
// Начальный экран: пока данных нет, показываем "Waiting..."
lcd.setCursor(0, 0);
lcd.print("Waiting...");
}
void loop() {
// Обработка кнопки для переключения режима (только если данные получены и температура нормальная)
if (digitalRead(BUTTON_PIN) == LOW && millis() - lastButtonPress > debounceDelay && lastTempStatus == 0) {
lastButtonPress = millis();
currentMode = (currentMode + 1) % 3; // Переключить режим: 0(80%)->1(100%)->2(60%)->0(80%)
if (currentMode != lastCurrentMode) {
RLOFFSoc = 0;
}
}
// Анимация бегущих точек в правой части (столбец 15)
if (animationActive) {
unsigned long elapsed = millis() - animationStart;
if (elapsed < animationStep) {
// Шаг 1: Показать точку в строке 1 (нижней)
lcd.setCursor(15, 1);
lcd.print(".");
} else if (elapsed < 2 * animationStep) {
// Шаг 2: Очистить строку 1 и показать в строке 0 (верхней)
lcd.setCursor(15, 1);
lcd.print(" ");
lcd.setCursor(15, 0);
lcd.print(".");
} else {
// Шаг 3: Очистить строку 0 и завершить анимацию
lcd.setCursor(15, 0);
lcd.print(" ");
animationActive = false;
}
}
// Проверяем, есть ли новое CAN-сообщение
if (!digitalRead(INTERRUPT_PIN)) {
CAN0.readMsgBuf(&rxId, &len, rxBuf);
Serial.print("ID: ");
Serial.print(rxId, HEX);
Serial.print(" Data: ");
for (int i = 0; i < len; i++) {
if (rxBuf[i] < 0x10) Serial.print("0");
Serial.print(rxBuf[i], HEX);
Serial.print(" ");
}
Serial.println();
// Обрабатываем сообщение, если ID совпадает и длина достаточна
if (rxId == TARGET_CAN_ID && len >= 5) {
processCANData();
} else if (rxId == TARGET_CAN_ID2 && len >= 5) {
// Парсинг SOC (процентов) из байта 4 (rxBuf[4])
soc = rxBuf[4];
// Запуск анимации бегущих точек при получении подходящего пакета
if (!animationActive) {
animationActive = true;
animationStart = millis();
}
}
}
// Отправляем сообщение каждые 5 секунд
if (millis() - lastUpdate > 5000) {
byte sendStatus = CAN0.sendMsgBuf(SEND_CAN_ID, 1, 0, 0); // 1 для extended ID
lastUpdate = millis();
if (sendStatus == CAN_OK) {
Serial.println("Sent");
} else {
Serial.println("Error Send");
}
}
}
// Функция для обработки данных из CAN-сообщения (напряжение и температура)
void processCANData() {
// Парсинг напряжения (предполагаем little-endian)
int32_t rawVoltage = ((int32_t)rxBuf[3] << 8) | rxBuf[2];
float voltage = rawVoltage / 100.0;
Serial.print(voltage, 2);
Serial.println("V");
// Парсинг температуры
int temperature = rxBuf[4] - 40;
Serial.print(temperature);
Serial.println("°C");
// Определяем статус температуры
int tempStatus = 0; // 0: normal, 1: cold, 2: hot
if (temperature < 10) {
tempStatus = 1; // Cold
} else if (temperature > 38) {
tempStatus = 2; // Hot
}
// Управление реле на основе температуры и напряжения
if (tempStatus == 2 || (tempStatus == 1)) {
digitalWrite(RELAY_PIN, HIGH); // Отключить реле (HIGH - выключено)
} else if (lastSoc >= modeThresholds[currentMode]) {
digitalWrite(RELAY_PIN, HIGH); // Отключить реле (порог достигнут)
RLOFFSoc = 1;
} else if (RLOFFSoc == 0) {
digitalWrite(RELAY_PIN, LOW); // Включить реле (LOW - включено)
}
// Запуск анимации бегущих точек при получении подходящего пакета
if (!animationActive) {
animationActive = true;
animationStart = millis();
}
// Обновляем LCD только если значения или статус изменились
bool updateTopLine = (currentMode != lastCurrentMode || tempStatus != lastTempStatus || soc != lastSoc);
bool updateBottomLine = (abs(voltage - lastVoltage) > 0.01 || temperature != lastTemperature);
if (updateTopLine) {
lastCurrentMode = currentMode;
lastTempStatus = tempStatus;
lastSoc = soc;
lcd.setCursor(0, 0);
lcd.print(" "); // Очистить всю строку 0
lcd.setCursor(0, 0);
if (lastTempStatus == 1) {
lcd.print("Cold Battery");
} else if (lastTempStatus == 2) {
lcd.print("Hot Battery");
} else if (RLOFFSoc == 1) {
lcd.print("Charged to ");
lcd.print(lastSoc);
lcd.print("%");
} else {
// Выводим режим и SOC, если SOC получен
if (lastSoc >= 0) {
lcd.print(lastSoc);
lcd.print("%");
lcd.print(" ");
} else {
lcd.print("##");
lcd.print("%");
lcd.print(" ");
}
lcd.print(modeLabels[currentMode]);
}
}
if (updateBottomLine) {
lastVoltage = voltage;
lastTemperature = temperature;
// Очищаем строки для динамических данных
lcd.setCursor(7, 1);
lcd.print(" "); // Очистка места для напряжения
lcd.setCursor(7, 1);
lcd.print(voltage, 2);
lcd.print(" V");
lcd.setCursor(0, 1);
lcd.print(" "); // Очистка места для температуры
lcd.setCursor(0, 1);
lcd.print(temperature);
lcd.setCursor(3, 1);
lcd.print("C");
}
}
Hi, see attach filesHi, do you have a longer sniffer log? I want to check what data is send by the battery, but unfortunately I don't have the original battery to check.