Add JBD BMS cell voltage telemetry

modified:   firmware/esp32/xterra-controller/bms.cpp
	modified:   firmware/esp32/xterra-controller/bms.h
	modified:   firmware/esp32/xterra-controller/xterra-controller.ino
This commit is contained in:
2026-06-04 00:50:42 -06:00
parent 5b1b60dc59
commit 81b6e71938
3 changed files with 118 additions and 27 deletions
+99 -27
View File
@@ -25,6 +25,10 @@ static uint8_t JBD_STATUS_REQUEST[] = {
0xDD, 0xA5, 0x03, 0x00, 0xFF, 0xFD, 0x77 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) { static uint16_t readU16(const uint8_t* data, int index) {
return ((uint16_t)data[index] << 8) | data[index + 1]; 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() { static void resetResponseBuffer() {
responseLength = 0; responseLength = 0;
responseReady = false; responseReady = false;
memset(responseBuffer, 0, sizeof(responseBuffer));
} }
static void handleNotify( static void handleNotify(
@@ -104,14 +109,37 @@ static bool connectBms() {
return true; 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() { static bool parseBasicInfo() {
if (responseLength < 40) { if (responseLength < 40) {
Serial.println("BMS: Response too short"); Serial.println("BMS: Basic response too short");
return false; return false;
} }
if (responseBuffer[0] != 0xDD || responseBuffer[1] != 0x03) { if (responseBuffer[0] != 0xDD || responseBuffer[1] != 0x03) {
Serial.println("BMS: Unexpected response header"); Serial.println("BMS: Unexpected basic response header");
return false; return false;
} }
@@ -122,12 +150,12 @@ static bool parseBasicInfo() {
uint16_t cyclesRaw = readU16(responseBuffer, 12); uint16_t cyclesRaw = readU16(responseBuffer, 12);
uint8_t socRaw = responseBuffer[23]; uint8_t socRaw = responseBuffer[23];
uint8_t mosTempAndCellOffset = 24;
uint8_t cellCount = responseBuffer[mosTempAndCellOffset + 1]; uint8_t offset = 24;
uint8_t ntcCount = responseBuffer[mosTempAndCellOffset + 2]; uint8_t cellCount = responseBuffer[offset + 1];
uint8_t ntcCount = responseBuffer[offset + 2];
int tempOffset = mosTempAndCellOffset + 3; int tempOffset = offset + 3;
float tempF = 0; float tempF = 0;
if (ntcCount > 0 && responseLength > (size_t)(tempOffset + 1)) { if (ntcCount > 0 && responseLength > (size_t)(tempOffset + 1)) {
@@ -159,6 +187,66 @@ static bool parseBasicInfo() {
return true; 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() { void initBms() {
NimBLEDevice::init("XterraESP32"); NimBLEDevice::init("XterraESP32");
NimBLEDevice::setPower(ESP_PWR_LVL_P9); NimBLEDevice::setPower(ESP_PWR_LVL_P9);
@@ -176,29 +264,13 @@ void updateBms() {
return; return;
} }
resetResponseBuffer(); if (requestPacket(JBD_STATUS_REQUEST, sizeof(JBD_STATUS_REQUEST))) {
parseBasicInfo();
bool ok = writeChar->writeValue(
JBD_STATUS_REQUEST,
sizeof(JBD_STATUS_REQUEST),
false
);
if (!ok) {
Serial.println("BMS: Write failed");
bmsData.connected = false;
return;
} }
unsigned long start = millis(); delay(100);
while (!responseReady && millis() - start < 2000) {
delay(10);
}
if (!responseReady) { if (requestPacket(JBD_CELL_REQUEST, sizeof(JBD_CELL_REQUEST))) {
Serial.println("BMS: Read timeout"); parseCellVoltages();
return;
} }
parseBasicInfo();
} }
+8
View File
@@ -1,8 +1,11 @@
#pragma once #pragma once
#define BMS_MAX_CELLS 8
struct BmsData { struct BmsData {
bool connected = false; bool connected = false;
bool valid = false; bool valid = false;
bool cellsValid = false;
float soc = 0; float soc = 0;
float voltage = 0; float voltage = 0;
@@ -14,6 +17,11 @@ struct BmsData {
int cycleCount = 0; int cycleCount = 0;
int cellCount = 0; int cellCount = 0;
int ntcCount = 0; int ntcCount = 0;
float cellVoltages[BMS_MAX_CELLS] = {0};
float cellMinVoltage = 0;
float cellMaxVoltage = 0;
int cellDeltaMv = 0;
}; };
extern BmsData bmsData; extern BmsData bmsData;
@@ -45,6 +45,17 @@ void buildStatusDocument(JsonDocument& doc) {
battery["cycle_count"] = bmsData.cycleCount; battery["cycle_count"] = bmsData.cycleCount;
battery["cell_count"] = bmsData.cellCount; battery["cell_count"] = bmsData.cellCount;
battery["ntc_count"] = bmsData.ntcCount; 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 { } else {
battery["source"] = "placeholder"; battery["source"] = "placeholder";
battery["connected"] = false; battery["connected"] = false;