diff --git a/firmware/esp32/xterra-controller/bms.cpp b/firmware/esp32/xterra-controller/bms.cpp index 0e8caa8..0b8a391 100644 --- a/firmware/esp32/xterra-controller/bms.cpp +++ b/firmware/esp32/xterra-controller/bms.cpp @@ -25,6 +25,10 @@ static uint8_t JBD_STATUS_REQUEST[] = { 0xDD, 0xA5, 0x03, 0x00, 0xFF, 0xFD, 0x77 }; +static uint8_t JBD_CELL_REQUEST[] = { + 0xDD, 0xA5, 0x04, 0x00, 0xFF, 0xFC, 0x77 +}; + static uint16_t readU16(const uint8_t* data, int index) { return ((uint16_t)data[index] << 8) | data[index + 1]; } @@ -36,6 +40,7 @@ static int16_t readS16(const uint8_t* data, int index) { static void resetResponseBuffer() { responseLength = 0; responseReady = false; + memset(responseBuffer, 0, sizeof(responseBuffer)); } static void handleNotify( @@ -104,14 +109,37 @@ static bool connectBms() { return true; } +static bool requestPacket(uint8_t* command, size_t commandLength) { + resetResponseBuffer(); + + bool ok = writeChar->writeValue(command, commandLength, false); + if (!ok) { + Serial.println("BMS: Write failed"); + bmsData.connected = false; + return false; + } + + unsigned long start = millis(); + while (!responseReady && millis() - start < 2000) { + delay(10); + } + + if (!responseReady) { + Serial.println("BMS: Read timeout"); + return false; + } + + return true; +} + static bool parseBasicInfo() { if (responseLength < 40) { - Serial.println("BMS: Response too short"); + Serial.println("BMS: Basic response too short"); return false; } if (responseBuffer[0] != 0xDD || responseBuffer[1] != 0x03) { - Serial.println("BMS: Unexpected response header"); + Serial.println("BMS: Unexpected basic response header"); return false; } @@ -122,12 +150,12 @@ static bool parseBasicInfo() { uint16_t cyclesRaw = readU16(responseBuffer, 12); uint8_t socRaw = responseBuffer[23]; - uint8_t mosTempAndCellOffset = 24; - uint8_t cellCount = responseBuffer[mosTempAndCellOffset + 1]; - uint8_t ntcCount = responseBuffer[mosTempAndCellOffset + 2]; + uint8_t offset = 24; + uint8_t cellCount = responseBuffer[offset + 1]; + uint8_t ntcCount = responseBuffer[offset + 2]; - int tempOffset = mosTempAndCellOffset + 3; + int tempOffset = offset + 3; float tempF = 0; if (ntcCount > 0 && responseLength > (size_t)(tempOffset + 1)) { @@ -159,6 +187,66 @@ static bool parseBasicInfo() { return true; } +static bool parseCellVoltages() { + if (responseLength < 7) { + Serial.println("BMS: Cell response too short"); + return false; + } + + if (responseBuffer[0] != 0xDD || responseBuffer[1] != 0x04) { + Serial.println("BMS: Unexpected cell response header"); + return false; + } + + uint8_t payloadLength = responseBuffer[3]; + int cellsReported = payloadLength / 2; + + if (cellsReported > BMS_MAX_CELLS) { + cellsReported = BMS_MAX_CELLS; + } + + if (cellsReported <= 0) { + Serial.println("BMS: No cell voltages reported"); + return false; + } + + float minV = 99.0; + float maxV = 0.0; + + for (int i = 0; i < cellsReported; i++) { + uint16_t mv = readU16(responseBuffer, 4 + (i * 2)); + float volts = mv / 1000.0; + + bmsData.cellVoltages[i] = volts; + + if (volts < minV) { + minV = volts; + } + + if (volts > maxV) { + maxV = volts; + } + } + + bmsData.cellCount = cellsReported; + bmsData.cellMinVoltage = minV; + bmsData.cellMaxVoltage = maxV; + bmsData.cellDeltaMv = (int)((maxV - minV) * 1000.0 + 0.5); + bmsData.cellsValid = true; + + Serial.print("BMS: Cells "); + Serial.print(cellsReported); + Serial.print(", min "); + Serial.print(minV, 3); + Serial.print("V, max "); + Serial.print(maxV, 3); + Serial.print("V, delta "); + Serial.print(bmsData.cellDeltaMv); + Serial.println("mV"); + + return true; +} + void initBms() { NimBLEDevice::init("XterraESP32"); NimBLEDevice::setPower(ESP_PWR_LVL_P9); @@ -176,29 +264,13 @@ void updateBms() { return; } - resetResponseBuffer(); - - bool ok = writeChar->writeValue( - JBD_STATUS_REQUEST, - sizeof(JBD_STATUS_REQUEST), - false - ); - - if (!ok) { - Serial.println("BMS: Write failed"); - bmsData.connected = false; - return; + if (requestPacket(JBD_STATUS_REQUEST, sizeof(JBD_STATUS_REQUEST))) { + parseBasicInfo(); } - unsigned long start = millis(); - while (!responseReady && millis() - start < 2000) { - delay(10); - } + delay(100); - if (!responseReady) { - Serial.println("BMS: Read timeout"); - return; + if (requestPacket(JBD_CELL_REQUEST, sizeof(JBD_CELL_REQUEST))) { + parseCellVoltages(); } - - parseBasicInfo(); } diff --git a/firmware/esp32/xterra-controller/bms.h b/firmware/esp32/xterra-controller/bms.h index 96e151e..1136480 100644 --- a/firmware/esp32/xterra-controller/bms.h +++ b/firmware/esp32/xterra-controller/bms.h @@ -1,8 +1,11 @@ #pragma once +#define BMS_MAX_CELLS 8 + struct BmsData { bool connected = false; bool valid = false; + bool cellsValid = false; float soc = 0; float voltage = 0; @@ -14,6 +17,11 @@ struct BmsData { int cycleCount = 0; int cellCount = 0; int ntcCount = 0; + + float cellVoltages[BMS_MAX_CELLS] = {0}; + float cellMinVoltage = 0; + float cellMaxVoltage = 0; + int cellDeltaMv = 0; }; extern BmsData bmsData; diff --git a/firmware/esp32/xterra-controller/xterra-controller.ino b/firmware/esp32/xterra-controller/xterra-controller.ino index dfd9b6f..d96d246 100644 --- a/firmware/esp32/xterra-controller/xterra-controller.ino +++ b/firmware/esp32/xterra-controller/xterra-controller.ino @@ -45,6 +45,17 @@ void buildStatusDocument(JsonDocument& doc) { battery["cycle_count"] = bmsData.cycleCount; battery["cell_count"] = bmsData.cellCount; battery["ntc_count"] = bmsData.ntcCount; + + JsonArray cells = battery.createNestedArray("cell_voltages"); + if (bmsData.cellsValid) { + for (int i = 0; i < bmsData.cellCount; i++) { + cells.add(bmsData.cellVoltages[i]); + } + } + + battery["cell_min_voltage"] = bmsData.cellMinVoltage; + battery["cell_max_voltage"] = bmsData.cellMaxVoltage; + battery["cell_delta_mv"] = bmsData.cellDeltaMv; } else { battery["source"] = "placeholder"; battery["connected"] = false;