syssi / esphome-jk-bms

ESPHome component to monitor and control a Jikong Battery Management System (JK-BMS) via UART-TTL or BLE
Apache License 2.0
482 stars 161 forks source link

How to implement the BLE communication from scratch using Arduino #562

Closed tssparky closed 3 months ago

tssparky commented 3 months ago

Hello, I am in need of some help, I have been trying ( with my limited coding skills ) to use ble.yaml info to read from a JK_B2A8S20P.....11.XW...11.17...h BMS with ESP32-S3 and for now just print to seral monitor. I'm not used to py so i did this with arduino IDE.

1) I can connect to BMS 2) Send GetInfo Command {0xaa, 0x55, 0x90, 0xeb, 0x97, 0x00, 0xdf, 0x52, 0x88, 0x67, 0x9d, 0x0a, 0x09, 0x6b, 0x9a, 0xf6, 0x70, 0x9a, 0x17, 0xfd} 3) Get response of user data and device info 4) have tried everything i can think of to get other info like cell voltages, pack voltage, soc ect with no luck.... 5) send command - {0xaa, 0x55, 0x90, 0xeb, 0x96, 0x00, 0x79, 0x62, 0x96, 0xed, 0xe3, 0xd0, 0x82, 0xa1, 0x9b, 0x5b, 0x3c, 0x9c, 0x4b, 0x5d} and get a response but i'm not parsing it correctly.. 6) Send Command - {0x4E, 0x57 , 0x00, 0x13 , 0x00, 0x00,0x00, 0x00, 0x06,0x03, 0x00 ,0x00, 0x00, 0x00, 0x00,0x00, 0x68,0x00, 0x00, 0x01, 0x29}; // NO RESPONSE 7) Send Command - {0xDD, 0xA5, 0x05, 0x00, 0xFF, 0xFB, 0x77};// No RESPONSE

This is the code for one of many I tried, anything i do get (with variations of the below code) always seems static.

Any help would be greatly appreciated

#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>

#define SERVICE_UUID        "0000ffe0-0000-1000-8000-00805f9b34fb"
#define CHARACTERISTIC_UUID "0000ffe1-0000-1000-8000-00805f9b34fb"

BLEAddress *pServerAddress;
BLEClient* pClient;
BLERemoteCharacteristic* pRemoteCharacteristic;
bool connected = false;
bool doConnect = false;
bool waitingForResponse = false;
unsigned long responseTimeout = 5000; // 5 seconds timeout
unsigned long requestTime = 0;
enum State {IDLE, SEND_STATUS, WAIT_FOR_STATUS} state = IDLE;

uint8_t requestdeviceinfo[20]={0xaa, 0x55, 0x90, 0xeb, 0x97, 0x00, 0xdf, 0x52, 0x88, 0x67, 0x9d, 0x0a, 0x09, 0x6b, 0x9a, 0xf6, 0x70, 0x9a, 0x17, 0xfd};//works, gets user data
//uint8_t readAllData[21]= {0x4E, 0x57 , 0x00, 0x13 , 0x00, 0x00,0x00, 0x00, 0x06,0x03, 0x00 ,0x00, 0x00, 0x00, 0x00,0x00, 0x68,0x00, 0x00, 0x01, 0x29}; // NO RESPONSE
uint8_t statusCommand[20] = {0xaa, 0x55, 0x90, 0xeb, 0x96, 0x00, 0x79, 0x62, 0x96, 0xed, 0xe3, 0xd0, 0x82, 0xa1, 0x9b, 0x5b, 0x3c, 0x9c, 0x4b, 0x5d};// Get info but not parsed
//uint8_t hwInfoCommand[7] = {0xDD, 0xA5, 0x05, 0x00, 0xFF, 0xFB, 0x77};// No RESPONSE
void sendCommand(uint8_t* command, size_t length) {
    pRemoteCharacteristic->writeValue(command, length);
    Serial.println("Command Sent ###############################");
    waitingForResponse = true;
    requestTime = millis();
}

void ParseData(uint8_t* receivedBytes, size_t length) {
    Serial.print("Data length: ");
    Serial.println(length);
    Serial.println("Received Data (HEX): ");
    for (int i = 0; i < length; i++) {
        Serial.print(receivedBytes[i], HEX);
        Serial.print(" ");
    }
    Serial.println();

    Serial.println("Received Data (ASCII): ");
    for (int i = 0; i < length; i++) {
        if (receivedBytes[i] >= 32 && receivedBytes[i] <= 126) {
            Serial.print((char)receivedBytes[i]);
        } else {
            Serial.print(".");
        }
    }
    Serial.println();

    // Check if the data starts with the expected header
    if (length >= 4 && receivedBytes[0] == 0xAA && receivedBytes[1] == 0x55 && receivedBytes[2] == 0x90 && receivedBytes[3] == 0xEB) {
        Serial.println("Parsing data...");

        // Example of parsing specific data:
        // Adjust based on the expected structure
        if (length >= 20) {
            Serial.print("Header: ");
            for (int i = 0; i < 4; i++) {
                Serial.print(receivedBytes[i], HEX);
                Serial.print(" ");
            }
            Serial.println();

            // Example of extracting fields starting from byte 4
            uint16_t voltage = (receivedBytes[4] << 8) | receivedBytes[5];
            Serial.print("Voltage (raw): ");
            Serial.println(voltage);

            uint16_t current = (receivedBytes[6] << 8) | receivedBytes[7];
            Serial.print("Current (raw): ");
            Serial.println(current);

            uint16_t capacity = (receivedBytes[8] << 8) | receivedBytes[9];
            Serial.print("Capacity (raw): ");
            Serial.println(capacity);

            uint16_t temperature = (receivedBytes[10] << 8) | receivedBytes[11];
            Serial.print("Temperature (raw): ");
            Serial.println(temperature);

            // Further processing based on actual data format
        } else {
            Serial.println("Data length is less than expected for parsing.");
        }
    } else {
        Serial.println("Data frame does not start correctly or length is insufficient.");
    }
}

void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {

    Serial.print("Notify callback for characteristic ");
    Serial.print(pBLERemoteCharacteristic->getUUID().toString().c_str());
    Serial.print(" of data length ");
    Serial.println(length);
    Serial.print("data: ");
    for (int i = 0; i < length; i++) {
        Serial.print(pData[i], HEX);
        Serial.print(" ");
    }
    Serial.println();

    Serial.print("Received Data (ASCII): ");
    for (int i = 0; i < length; i++) {
        if (pData[i] >= 32 && pData[i] <= 126) {
            Serial.print((char)pData[i]);
        } else {
            Serial.print(".");
        }
    }
    Serial.println();

    waitingForResponse = false;
    ParseData(pData, length);
}

class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
        Serial.print("BLE Advertised Device found: ");
        Serial.println(advertisedDevice.toString().c_str());

        if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(BLEUUID(SERVICE_UUID))) {
            BLEDevice::getScan()->stop();
            pServerAddress = new BLEAddress(advertisedDevice.getAddress());
            doConnect = true;
            BLEDevice::getScan()->clearResults();
        }
    }
};

void setup() {
    Serial.begin(115200);
    Serial.println("Starting BLE Client...");

    BLEDevice::init("");
    pClient = BLEDevice::createClient();
    Serial.println(" - Created client");

    BLEScan* pBLEScan = BLEDevice::getScan();
    pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
    pBLEScan->setInterval(1349);
    pBLEScan->setWindow(449);
    pBLEScan->setActiveScan(true);
    pBLEScan->start(30, false);
}

void loop() {
    if (doConnect) {
        if (connectToBLEServer()) {
            Serial.println("We are now connected to the BLE Server.");
            state = SEND_STATUS;

            delay(1000);
        } else {
            Serial.println("We have failed to connect to the server; there is nothing more we will do.");
        }
        doConnect = false;
    }

    switch (state) {
        case SEND_STATUS:
            sendCommand(requestdeviceinfo, sizeof(requestdeviceinfo));
            state = WAIT_FOR_STATUS;
            break;

        case WAIT_FOR_STATUS:
            if (!waitingForResponse) {
                state = IDLE;
            } else if (millis() - requestTime > responseTimeout) {
                Serial.println("No response for INFO, retrying.");
                waitingForResponse = false;
                state = SEND_STATUS;
            }
            break;

        case IDLE:
        default:
            break;
    }
    delay(1000);
    switch (state) {
        case SEND_STATUS:
            sendCommand(statusCommand, sizeof(statusCommand));
            state = WAIT_FOR_STATUS;
            break;

        case WAIT_FOR_STATUS:
            if (!waitingForResponse) {
                state = IDLE;
            } else if (millis() - requestTime > responseTimeout) {
                Serial.println("No response for status, retrying.");
                waitingForResponse = false;
                state = SEND_STATUS;
            }
            break;

        case IDLE:
        default:
            break;
    }
}

class MyClientCallback : public BLEClientCallbacks {
    void onConnect(BLEClient* pclient) {}

    void onDisconnect(BLEClient* pclient) {
        connected = false;
        Serial.println("Disconnected from server.");
    }
};

bool connectToBLEServer() {
    Serial.print("Forming a connection to ");
    Serial.println(pServerAddress->toString().c_str());

    try {
        pClient->setClientCallbacks(new MyClientCallback());
        pClient->connect(*pServerAddress);
        Serial.println(" - Connected to server");

        pClient->setMTU(517);

        BLERemoteService* pRemoteService = pClient->getService(BLEUUID(SERVICE_UUID));
        if (pRemoteService == nullptr) {
            Serial.print("Failed to find our service UUID: ");
            Serial.println(SERVICE_UUID);
            pClient->disconnect();
            return false;
        }
        Serial.println(" - Found our service");

        pRemoteCharacteristic = pRemoteService->getCharacteristic(BLEUUID(CHARACTERISTIC_UUID));
        if (pRemoteCharacteristic == nullptr) {
            Serial.print("Failed to find our characteristic UUID: ");
            Serial.println(CHARACTERISTIC_UUID);
            pClient->disconnect();
            return false;
        }
        Serial.println(" - Found our characteristic");

        if (pRemoteCharacteristic->canRead()) {
            String value = pRemoteCharacteristic->readValue().c_str();
            Serial.print("The characteristic value was: ");
            Serial.println(value);
        }

        if (pRemoteCharacteristic->canNotify()) {
            pRemoteCharacteristic->registerForNotify(notifyCallback);
            Serial.println("Notify the characteristic");
        }

        connected = true;
        return true;
    } catch (const std::exception& e) {
        Serial.print("Exception: ");
        Serial.println(e.what());
        return false;
    }
}
syssi commented 3 months ago
  1. Send Command - {0x4E, 0x57 , 0x00, 0x13 , 0x00, 0x00,0x00, 0x00, 0x06,0x03, 0x00 ,0x00, 0x00, 0x00, 0x00,0x00, 0x68,0x00, 0x00, 0x01, 0x29}; // NO RESPONSE
  2. Send Command - {0xDD, 0xA5, 0x05, 0x00, 0xFF, 0xFB, 0x77};// No RESPONSE

Your BMS supports a single protocol. Every request starts with 0xAA 0x55. Every response starts with 0x55 0xAA.

Please send {0xaa, 0x55, 0x90, 0xeb, 0x96, 0x00, 0x79, 0x62, 0x96, 0xed, 0xe3, 0xd0, 0x82, 0xa1, 0x9b, 0x5b, 0x3c, 0x9c, 0x4b, 0x5d} just once because it enables a stream of messages (2 cell info frames per second).

syssi commented 3 months ago

Could you provide one of the response frames you aren't able to parse?

syssi commented 3 months ago

Your state machine isn't a good idea because there is always incoming data. No periodic requests required.

tssparky commented 3 months ago

Thanks for reply, in an earlier script i did that but the only info i was getting was U.....JK_B2A8S20P.....11.XW...11.17...........JK_B2A8S20P.....1234............230323..2081215432.0000.Input Userdata..0341...... Over and over, never got any other info.. that's why i tried to send other commands.. So are you saying just send {0xaa, 0x55, 0x90, 0xeb, 0x96, 0x00, 0x79, 0x62, 0x96, 0xed, 0xe3, 0xd0, 0x82, 0xa1, 0x9b, 0x5b, 0x3c, 0x9c, 0x4b, 0x5d} and read ?

Can't parse this... {7C F8 FF F3 F 0 0 0 0 0 0 0 90 F 0 0 0 0 C0 D8 3 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 }

tssparky commented 3 months ago

I get this over and over..... 55 AA EB 90 3 CE 4A 4B 5F 42 32 41 38 53 32 30 50 0 0 0 0 0 31 31 2E 58 57 0 0 0 31 31 2E 31 37 0 0 0 AC E 8E 2 A 0 0 0 4A 4B 5F 42 32 41 38 53 32 30 50 0 0 0 0 0 31 32 33 34 0 0 0 0 0 0 0 0 0 0 0 0 32 33 30 33 32 33 0 0 32 30 38 31 32 31 35 34 33 32 0 30 30 30 30 0 49 6E 70 75 74 20 55 73 65 72 64 61 74 61 0 0 30 34 31 32 0 0 0 0 0 0

tssparky commented 3 months ago

55 AA EB 90 1 CE 58 2 0 0 28 A 0 0 5A A 0 0 10 E 0 0 DE D 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 C4 9 0 0 A0 86 1 0 5 0 0 0 58 2 0 0 E0 22 2 0 5 0 0 0 3C 0 0 0 3C 0 0 0 D0 7 0 0 BC 2 0 0 58 2 0 0 BC 2 0 0 58 2 0 0 38 FF FF FF 9C FF FF FF E8 3 0 0 20 3 0 0 4 0 0 0 1 0 0 0 1 0 0 0 1 0 0 0 C0 45 4 0 DC 5 0 0 7A D 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 B0 71 B 0 0 0 0 0 0 7C F8 FF FF F 0 0 0 0 0 0 0 22

Ok, this one is different

syssi commented 3 months ago

Over and over, never got any other info.. that's why i tried to send other commands.. So are you saying just send {0xaa, 0x55, 0x90, 0xeb, 0x96, 0x00, 0x79, 0x62, 0x96, 0xed, 0xe3, 0xd0, 0x82, 0xa1, 0x9b, 0x5b, 0x3c, 0x9c, 0x4b, 0x5d} and read ?

Please send the 0x97 request once after an established connection. It will retrieve the device info frame. Send 0x96 as second command (once!). The requests the settings frame!

The cell info frame will arrive afterwards periodically.

tssparky commented 3 months ago

ok, i will try that, thank you so much..

syssi commented 3 months ago
55 AA EB 90 3 ...
55 AA EB 90 1 ...
            ^--frame_type
               0x01: Settings frame
               0x02: Cell info frame
               0x03: Device info frame

Please pass the frame / frame types to the appropriate decoder.

https://github.com/syssi/esphome-jk-bms/blob/main/components/jk_bms_ble/jk_bms_ble.cpp#L287-L309

syssi commented 3 months ago

Is this your private project or some task of your university?

tssparky commented 3 months ago

No just a task, I have a panlee LCD screen with esp32 and wanted to make it connect to bms via BLE, im a 54yo electrician having a crack at electronics :)

tssparky commented 3 months ago

Using Chatgpt to help me code :)

syssi commented 3 months ago

I recommend not implement the protocol on your own and use ESPHome. I will provide some support if you like.

This is an example how it could look like:

https://github.com/syssi/esphome-jbd-bms/discussions/19

Not required features can be simplify removed from the configuration YAML. They won't be part of the firmware as soon the sections are removed from the YAML.

tssparky commented 3 months ago

If i can get this info to parse i'll be good... i've gotten everything on lcd working as needed, just need the data from bms, I've been at this foe weeks, i'm very persistent, if i know it's possible, and it's just my lack of coding skills and knowledge, i will never give up until it works :)....

tssparky commented 3 months ago

I recommend not implement the protocol on your own and use ESPHome. I will provide some support if you like.

This is an example how it could look like:

syssi/esphome-jbd-bms#19

Not required features can be simplify removed from the configuration YAML. They won't be part of the firmware as soon the sections are removed from the YAML.

Forgive me if i'm wrong but don't you have to use wifi and a pc to use it? I want to make an independent device if that makes sense...

syssi commented 3 months ago

Forgive me if i'm wrong but don't you have to use wifi and a pc to use it? I want to make an independent device if that makes sense...

ESPHome is a code generator using building blocks. If you don't need WiFi or Over-The-Air-Updates you can simple remove the sections from the recipe / YAML configuration. The smallest recipe would look like this:

esphome:
  name: "device name"

esp32:
  board: wemos_d1_mini32
  framework:
    type: esp-idf

If you want to write some log output to UART0 you have to add the logger component:

logger:

Adding these lines adds this project as external component and sets up a BLE client connection to the JK-BMS:

external_components:
  - source: github://syssi/esphome-jk-bms@main
    refresh: 0s

esp32_ble_tracker:

ble_client:
  - mac_address: C8:47:8C:E1:E2:AA
    id: client0

jk_bms_ble:
  - ble_client_id: client0
    protocol_version: JK02_24S
    throttle: 5s
    id: bms0

This section will expose two measurements as sensor entities:

sensor:
  - platform: jk_bms_ble
    jk_bms_ble_id: bms0
    total_voltage:
      id: total_voltage
      name: "${name} total voltage"
    current:
      id: current
      name: "${name} current"

If you load a display component now. You could access the state of the sensors using id(total_voltage).state.

This will paint the display content periodically:

display:
  - platform: waveshare_epaper
    cs_pin: 5
    dc_pin: 21
    busy_pin: 19
    reset_pin: 22
    model: 1.54in
    rotation: 90°
    update_interval: 3s
    reset_duration: 20ms
    full_update_every: 60   
    id: bms_display
    lambda: |-   
      it.printf(5, 25, id(font_bebas_44), "%0.1f A", id(current).state);
      it.printf(10, 73, id(font_arial_20), "%.1fv", id(total_voltage).state);

Its all standalone. No WiFi or computers required as soon the code is generated, compiled and flashed on your ESP.