Dengfu E10 New CAN Battery 6 pin + "Smart" charger

ncs

New Member
Joined
Jun 30, 2025
Messages
67
Reaction score
82
Location
Moscow
So I got my new frame E10 with 6 pin battery, put down wiring for it, may be it can be usefull.

IMG_20250703_075832.jpg

IMG_20250703_075815.jpg
IMG_20250703_075850.jpg
IMG_20250703_075913.jpg
IMG_20250703_075927.jpg
E10 CAN.jpg
 
⚡ EMTB Pro Go Pro — exclusive discounts & ad-free Peaty's 25% off & more · Ad-free browsing · Pro badge See the deals →
In winter it is not so many days with good weather and good trail condition, so sometime I need to charge battery to 60% (storage) or 85% (ready to go but don't know will be weather fine) or 100% when I sure to ride tomorrow. Temperature control help start to charge only when battery more than 10 degree and stop charge at 38 degree (for winter it ok, for summer..somewhere it will be ok too).
Solution I found in battery can communication,
With arduino module, can module, lcd, relay, dc-dc module, button, original 6pin connector you can build the same "smart" charger.
IMG_20260107_091834.jpg
IMG_20260107_091854.jpg
IMG_20260107_091819.jpg
IMG_20251130_191806.jpg
IMG_20251130_191756.jpg
IMG_20251130_191748.jpg
IMG_20251130_191743.jpg

I'm only try some things on arduino, so code is not perfect, part of it is GPT gen, but it work for me and I don't need more from it))
Library and code: Arduino code for Nano
C++:
#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");
  }
}
Connection: Can module - standard SPI, LCD - i2c
DC jack from original charger to relay module - after relay - to battery connector
On battery connector short 2 pins to enable battery output when connect - it get 48v output to dc-dc board to 5V to Arduino power
Can packages
0x02F83204 - send to battery like it "keep alive" from motor, without data every 5 sec
from battery
1: Extended ID: 0x04F83401 DLC: 5 Data: 0x00 0x00 0x07 0x13 0x40 it get 0713 --> 48.71v now voltage , 0x40 --> 64 -40 = 24 degree
2: Extended ID: 0x04F83400 DLC: 7 Data: 0xF2 0x44 0x21 0x26 0x37 0x38 0x64 for % --> 0x37 = 55% now % charged
------
but in code
#define TARGET_CAN_ID 0x84F83401
#define TARGET_CAN_ID2 0x84F83400
I don't know why, but when it read like sniffer it is 0 but in this program it read like 8....but it work for me.
 
Last edited:
Hi, 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.
 
Keep reading
    Browse all

    Similar Threads

    Community Stats

    Since 2018
    669K
    Messages
    40,857
    Members
    Join 30,000+ Riders, it's free!
    Back
    Top