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
};
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();
}
+8
View File
@@ -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;
@@ -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;