Integrate JBD BMS telemetry into ESP32 status
This commit is contained in:
@@ -0,0 +1,204 @@
|
|||||||
|
#include "bms.h"
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <NimBLEDevice.h>
|
||||||
|
|
||||||
|
static const char* BMS_ADDRESS = "a5:c2:37:2c:05:dc";
|
||||||
|
static const char* BMS_SERVICE_UUID = "ff00";
|
||||||
|
static const char* BMS_NOTIFY_UUID = "ff01";
|
||||||
|
static const char* BMS_WRITE_UUID = "ff02";
|
||||||
|
|
||||||
|
static NimBLEClient* client = nullptr;
|
||||||
|
static NimBLERemoteCharacteristic* notifyChar = nullptr;
|
||||||
|
static NimBLERemoteCharacteristic* writeChar = nullptr;
|
||||||
|
|
||||||
|
static uint8_t responseBuffer[128];
|
||||||
|
static size_t responseLength = 0;
|
||||||
|
static bool responseReady = false;
|
||||||
|
|
||||||
|
static unsigned long lastReadAttempt = 0;
|
||||||
|
static const unsigned long READ_INTERVAL_MS = 5000;
|
||||||
|
|
||||||
|
BmsData bmsData;
|
||||||
|
|
||||||
|
static uint8_t JBD_STATUS_REQUEST[] = {
|
||||||
|
0xDD, 0xA5, 0x03, 0x00, 0xFF, 0xFD, 0x77
|
||||||
|
};
|
||||||
|
|
||||||
|
static uint16_t readU16(const uint8_t* data, int index) {
|
||||||
|
return ((uint16_t)data[index] << 8) | data[index + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
static int16_t readS16(const uint8_t* data, int index) {
|
||||||
|
return (int16_t)readU16(data, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void resetResponseBuffer() {
|
||||||
|
responseLength = 0;
|
||||||
|
responseReady = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleNotify(
|
||||||
|
NimBLERemoteCharacteristic* characteristic,
|
||||||
|
uint8_t* data,
|
||||||
|
size_t length,
|
||||||
|
bool isNotify
|
||||||
|
) {
|
||||||
|
if (responseLength + length > sizeof(responseBuffer)) {
|
||||||
|
resetResponseBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(responseBuffer + responseLength, data, length);
|
||||||
|
responseLength += length;
|
||||||
|
|
||||||
|
if (responseLength > 0 && responseBuffer[responseLength - 1] == 0x77) {
|
||||||
|
responseReady = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool connectBms() {
|
||||||
|
if (client && client->isConnected()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("BMS: Connecting...");
|
||||||
|
|
||||||
|
NimBLEAddress address(std::string(BMS_ADDRESS), BLE_ADDR_PUBLIC);
|
||||||
|
|
||||||
|
client = NimBLEDevice::createClient();
|
||||||
|
|
||||||
|
if (!client->connect(address)) {
|
||||||
|
Serial.println("BMS: Connect failed");
|
||||||
|
bmsData.connected = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
NimBLERemoteService* service = client->getService(BMS_SERVICE_UUID);
|
||||||
|
if (!service) {
|
||||||
|
Serial.println("BMS: Service ff00 not found");
|
||||||
|
client->disconnect();
|
||||||
|
bmsData.connected = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyChar = service->getCharacteristic(BMS_NOTIFY_UUID);
|
||||||
|
writeChar = service->getCharacteristic(BMS_WRITE_UUID);
|
||||||
|
|
||||||
|
if (!notifyChar || !writeChar) {
|
||||||
|
Serial.println("BMS: Characteristics not found");
|
||||||
|
client->disconnect();
|
||||||
|
bmsData.connected = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!notifyChar->subscribe(true, handleNotify)) {
|
||||||
|
Serial.println("BMS: Notify subscribe failed");
|
||||||
|
client->disconnect();
|
||||||
|
bmsData.connected = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("BMS: Connected");
|
||||||
|
bmsData.connected = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool parseBasicInfo() {
|
||||||
|
if (responseLength < 40) {
|
||||||
|
Serial.println("BMS: Response too short");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseBuffer[0] != 0xDD || responseBuffer[1] != 0x03) {
|
||||||
|
Serial.println("BMS: Unexpected response header");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t voltageRaw = readU16(responseBuffer, 4);
|
||||||
|
int16_t currentRaw = readS16(responseBuffer, 6);
|
||||||
|
uint16_t remainingRaw = readU16(responseBuffer, 8);
|
||||||
|
uint16_t capacityRaw = readU16(responseBuffer, 10);
|
||||||
|
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];
|
||||||
|
|
||||||
|
int tempOffset = mosTempAndCellOffset + 3;
|
||||||
|
float tempF = 0;
|
||||||
|
|
||||||
|
if (ntcCount > 0 && responseLength > (size_t)(tempOffset + 1)) {
|
||||||
|
uint16_t tempRaw = readU16(responseBuffer, tempOffset);
|
||||||
|
float tempC = (tempRaw - 2731) / 10.0;
|
||||||
|
tempF = tempC * 9.0 / 5.0 + 32.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bmsData.voltage = voltageRaw / 100.0;
|
||||||
|
bmsData.current = currentRaw / 100.0;
|
||||||
|
bmsData.remainingAh = remainingRaw / 100.0;
|
||||||
|
bmsData.capacityAh = capacityRaw / 100.0;
|
||||||
|
bmsData.cycleCount = cyclesRaw;
|
||||||
|
bmsData.soc = socRaw;
|
||||||
|
bmsData.cellCount = cellCount;
|
||||||
|
bmsData.ntcCount = ntcCount;
|
||||||
|
bmsData.temperatureF = tempF;
|
||||||
|
bmsData.valid = true;
|
||||||
|
|
||||||
|
Serial.print("BMS: SOC ");
|
||||||
|
Serial.print(bmsData.soc);
|
||||||
|
Serial.print("%, V ");
|
||||||
|
Serial.print(bmsData.voltage);
|
||||||
|
Serial.print(", A ");
|
||||||
|
Serial.print(bmsData.current);
|
||||||
|
Serial.print(", Ah ");
|
||||||
|
Serial.println(bmsData.remainingAh);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void initBms() {
|
||||||
|
NimBLEDevice::init("XterraESP32");
|
||||||
|
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
|
||||||
|
resetResponseBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateBms() {
|
||||||
|
if (millis() - lastReadAttempt < READ_INTERVAL_MS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastReadAttempt = millis();
|
||||||
|
|
||||||
|
if (!connectBms()) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long start = millis();
|
||||||
|
while (!responseReady && millis() - start < 2000) {
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!responseReady) {
|
||||||
|
Serial.println("BMS: Read timeout");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseBasicInfo();
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
struct BmsData {
|
||||||
|
bool connected = false;
|
||||||
|
bool valid = false;
|
||||||
|
|
||||||
|
float soc = 0;
|
||||||
|
float voltage = 0;
|
||||||
|
float current = 0;
|
||||||
|
float remainingAh = 0;
|
||||||
|
float capacityAh = 0;
|
||||||
|
float temperatureF = 0;
|
||||||
|
|
||||||
|
int cycleCount = 0;
|
||||||
|
int cellCount = 0;
|
||||||
|
int ntcCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern BmsData bmsData;
|
||||||
|
|
||||||
|
void initBms();
|
||||||
|
void updateBms();
|
||||||
@@ -6,30 +6,59 @@
|
|||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
#include "relays.h"
|
#include "relays.h"
|
||||||
#include "sensors.h"
|
#include "sensors.h"
|
||||||
|
#include "bms/bms.h"
|
||||||
|
|
||||||
WebServer server(80);
|
WebServer server(80);
|
||||||
HardwareSerial DashboardSerial(2);
|
HardwareSerial DashboardSerial(2);
|
||||||
|
|
||||||
String uartLineBuffer;
|
String uartLineBuffer;
|
||||||
|
|
||||||
float batterySOC = 82.0;
|
float calculateRuntimeHours() {
|
||||||
float batteryVoltage = 13.2;
|
if (!bmsData.valid || bmsData.current >= 0) {
|
||||||
float batteryCurrent = -6.4;
|
return 0;
|
||||||
float batteryRemainingAh = 82.0;
|
}
|
||||||
float batteryRuntimeHours = 12.0;
|
|
||||||
float batteryTemp = 76.0;
|
float dischargeAmps = -bmsData.current;
|
||||||
|
if (dischargeAmps <= 0.1) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bmsData.remainingAh / dischargeAmps;
|
||||||
|
}
|
||||||
|
|
||||||
void buildStatusDocument(JsonDocument& doc) {
|
void buildStatusDocument(JsonDocument& doc) {
|
||||||
doc["type"] = MSG_STATUS_RESPONSE;
|
doc["type"] = MSG_STATUS_RESPONSE;
|
||||||
doc["timestamp"] = millis();
|
doc["timestamp"] = millis();
|
||||||
|
|
||||||
JsonObject battery = doc.createNestedObject("battery");
|
JsonObject battery = doc.createNestedObject("battery");
|
||||||
battery["soc"] = batterySOC;
|
|
||||||
battery["voltage"] = batteryVoltage;
|
if (bmsData.valid) {
|
||||||
battery["current"] = batteryCurrent;
|
battery["source"] = "jbd_bms";
|
||||||
battery["remaining_ah"] = batteryRemainingAh;
|
battery["connected"] = bmsData.connected;
|
||||||
battery["runtime_hours"] = batteryRuntimeHours;
|
battery["soc"] = bmsData.soc;
|
||||||
battery["temperature_f"] = batteryTemp;
|
battery["voltage"] = bmsData.voltage;
|
||||||
|
battery["current"] = bmsData.current;
|
||||||
|
battery["remaining_ah"] = bmsData.remainingAh;
|
||||||
|
battery["capacity_ah"] = bmsData.capacityAh;
|
||||||
|
battery["runtime_hours"] = calculateRuntimeHours();
|
||||||
|
battery["temperature_f"] = bmsData.temperatureF;
|
||||||
|
battery["cycle_count"] = bmsData.cycleCount;
|
||||||
|
battery["cell_count"] = bmsData.cellCount;
|
||||||
|
battery["ntc_count"] = bmsData.ntcCount;
|
||||||
|
} else {
|
||||||
|
battery["source"] = "placeholder";
|
||||||
|
battery["connected"] = false;
|
||||||
|
battery["soc"] = 0;
|
||||||
|
battery["voltage"] = 0;
|
||||||
|
battery["current"] = 0;
|
||||||
|
battery["remaining_ah"] = 0;
|
||||||
|
battery["capacity_ah"] = 150;
|
||||||
|
battery["runtime_hours"] = 0;
|
||||||
|
battery["temperature_f"] = 0;
|
||||||
|
battery["cycle_count"] = 0;
|
||||||
|
battery["cell_count"] = 0;
|
||||||
|
battery["ntc_count"] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
JsonObject temps = doc.createNestedObject("temps");
|
JsonObject temps = doc.createNestedObject("temps");
|
||||||
|
|
||||||
@@ -237,6 +266,7 @@ void setup() {
|
|||||||
|
|
||||||
initRelays();
|
initRelays();
|
||||||
initSensors();
|
initSensors();
|
||||||
|
initBms();
|
||||||
|
|
||||||
WiFi.mode(WIFI_AP);
|
WiFi.mode(WIFI_AP);
|
||||||
|
|
||||||
@@ -264,6 +294,7 @@ void setup() {
|
|||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
server.handleClient();
|
server.handleClient();
|
||||||
|
updateBms();
|
||||||
pollDashboardUart();
|
pollDashboardUart();
|
||||||
|
|
||||||
static unsigned long lastSensorUpdate = 0;
|
static unsigned long lastSensorUpdate = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user