commit 904eda08b84baabda30def0b1a77c4aac8162826 Author: James Date: Tue Apr 23 19:45:13 2024 +0100 initial diff --git a/.atom-build.yml b/.atom-build.yml new file mode 100644 index 0000000..b65527c --- /dev/null +++ b/.atom-build.yml @@ -0,0 +1,18 @@ +cmd: bin/build +name: "All" +errorMatch: + - (?[\/0-9a-zA-Z\._]+):(?\d+):(?\d+):\s+(?.+) +targets: + Build: + cmd: bin/build + name: "Build" + errorMatch: + - (?[\/0-9a-zA-Z\._]+):(?\d+):(?\d+):\s+(?.+) + Erase: + cmd: esptool.py erase_flash + name: "Erase" + errorMatch: + - (?[\/0-9a-zA-Z\._]+):(?\d+):(?\d+):\s+(?.+) + Upload: + cmd: bin/flash Compiled/latest + name: "Upload" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ddbfbf0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/Compiled/* +/build/ +/compile_flags.h diff --git a/BLE.ino b/BLE.ino new file mode 100644 index 0000000..f21d1b7 --- /dev/null +++ b/BLE.ino @@ -0,0 +1,358 @@ +BLEScan* pBLEScan = nullptr; +BLEClient* pClient = nullptr; +BLEAdvertisedDevice* pRemoteDevice = nullptr; +BLERemoteService* pRemoteService = nullptr; +BLERemoteCharacteristic* pRemoteCharacteristic_rx = nullptr; +BLERemoteCharacteristic* pRemoteCharacteristic_tx = nullptr; + +// 0000ff01-0000-1000-8000-00805f9b34fb +// Notifications from this characteristic is received data from BMS +// NOTIFY, READ + +// 0000ff02-0000-1000-8000-00805f9b34fb +// Write this characteristic to send data to BMS +// READ, WRITE, WRITE NO RESPONSE + +boolean doScan = false; // becomes true when BLE is initialized and scanning is allowed +boolean doConnect = false; // becomes true when correct ID is found during scanning + +boolean ble_client_connected = false; // true when fully connected + +unsigned int ble_packets_requested = 0b00; // keeps track of requested packets +unsigned int ble_packets_received = 0b00; // keeps track of received packets + + +// ======= CALLBACKS ========= + +void MyEndOfScanCallback(BLEScanResults pBLEScanResult){ + bms_status=false; // BMS not found + + if(BLE_CALLBACK_DEBUG){ + debug("BLE: scan finished"); + Serial.println("Scan finished."); + } +} + +class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks{ + // called for each advertising BLE server + + void onResult(BLEAdvertisedDevice advertisedDevice){ + // found a device + + + if(BLE_CALLBACK_DEBUG){ + debug( + String("BLE: found ") + + String(advertisedDevice.getName().c_str()) + + String(" with address ") + + String(advertisedDevice.getAddress().toString().c_str()) + + String(" and RSSI ") + + String(advertisedDevice.getRSSI()) + ); + + Serial.print("BLE: found "); + Serial.println(advertisedDevice.toString().c_str()); + } + + + + // Check if found device is the one we are looking for + if( + strcmp(advertisedDevice.getName().c_str(), BLE_NAME)==0 && + strcmp(advertisedDevice.getAddress().toString().c_str(), BLE_ADDRESS)==0 && + advertisedDevice.haveServiceUUID() && + advertisedDevice.isAdvertisingService(serviceUUID) + ){ + + if(BLE_CALLBACK_DEBUG){ + debug("BLE: target device found"); + } + + pBLEScan->stop(); + + if(advertisedDevice.getRSSI() >= BLE_MIN_RSSI){ + + // delete old remote device, create new one + if(pRemoteDevice != nullptr){ + delete pRemoteDevice; + } + pRemoteDevice = new BLEAdvertisedDevice(advertisedDevice); + + doConnect = true; + }else{ + if(BLE_CALLBACK_DEBUG){ + debug("BLE: RSSI of target device below minimum"); + } + } + } + } +}; + +class MyClientCallback : public BLEClientCallbacks{ + // called on connect/disconnect + void onConnect(BLEClient* pclient){ + + if(BLE_CALLBACK_DEBUG){ + debug(String("BLE: connecting to ") + String(pclient->getPeerAddress().toString().c_str())); + } + } + + void onDisconnect(BLEClient* pclient){ + ble_client_connected = false; + doConnect = false; + + if(BLE_CALLBACK_DEBUG){ + debug(String("BLE: disconnected from ") + String(pclient->getPeerAddress().toString().c_str())); + } + + } +}; + +static void MyNotifyCallback(BLERemoteCharacteristic *pBLERemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify){ + //this is called when BLE server sents data via notification + //hexDump((char*)pData, length); + if(!bleCollectPacket((char *)pData, length)){ + debug("ERROR: packet could not be collected."); + } +} + + + + + +// ======= OTHERS ========= + +void handleBLE(){ + static unsigned long prev_millis_standby = 0; + + prev_millis_standby = millis(); + + while(true){ // loop until we hit a timeout or gathered all packets + + if((ble_packets_received == BLE_PACKETSRECEIVED_BEFORE_STANDBY) || (millis()>prev_millis_standby+BLE_TIMEOUT)){ + if(ble_packets_received == BLE_PACKETSRECEIVED_BEFORE_STANDBY){ + debug("BLE: all packets received"); + bms_status=true; // BMS was connected, data up-to-date + + }else{ + debug("BLE: connection timeout"); + bms_status=false; // BMS not (fully) connected + } + + break; // we're done with BLE, exit while loop + } + else if (doConnect){ + + // found the desired BLE server, now connect to it + if (connectToServer()){ + ble_client_connected = true; + ble_packets_received=0; + ble_packets_requested=0; + + }else{ + ble_client_connected = false; + debug("BLE: failed to connect"); + } + + doConnect = false; + } + + if (ble_client_connected){ + // if connected to BLE server, request all data + if((ble_packets_requested & 0b01)!=0b01){ + // request packet 0b01 + debug("BLE: requesting packet 0b01"); + delay(50); + if(bmsRequestBasicInfo()){ + ble_packets_requested |= 0b01; + } + + }else if(((ble_packets_received & 0b01)==0b01) && ((ble_packets_requested & 0b10)!=0b10)){ + // request packet 0b10 after 0b01 has been received + debug("BLE: requesting packet 0b10"); + delay(50); + if(bmsRequestCellInfo()){ + ble_packets_requested |= 0b10; + } + } + + }else if ((!doConnect)&&(doScan)){ + // we are not connected, so we can scan for devices + debug("BLE: not connected, starting scan"); + Serial.print("BLE is not connected, starting scan"); + + // Disconnect client + if((pClient != nullptr)&&(pClient->isConnected())){ + pClient->disconnect(); + } + + // stop scan (if running) and start a new one + pBLEScan->setActiveScan(true); + pBLEScan->setInterval(1 << 8); // 160 ms + pBLEScan->setWindow(1 << 7); // 80 ms + pBLEScan->start(BLE_SCAN_DURATION, MyEndOfScanCallback, false); // non-blocking, use a callback + + doScan=false; + + debug("BLE: scan started"); + } + } +} + +void bleGatherPackets(){ + bleStart(); + handleBLE(); + blePause(); + BLEDevice::deinit(false); +} + +void bleStart(){ + Serial.print("Starting BLE... "); + + BLEDevice::init(""); + //esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); // release some unused memory + + // Retrieve a BLE client + pClient = BLEDevice::createClient(); + pClient->setClientCallbacks(new MyClientCallback()); + + // Retrieve a BLE scanner + pBLEScan = BLEDevice::getScan(); + pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); + + bleContinue(); + Serial.println("done"); +} + +void blePause(){ + // stop scanning and disconnect from all devices + doScan=false; + + // Disconnect client + if((pClient != nullptr)&&(pClient->isConnected())){ + pClient->disconnect(); + } + + delay(50); + + pBLEScan->stop(); + + ble_client_connected=false; + doConnect=false; + ble_packets_received=0; + ble_packets_requested=0; + +} + + +void bleContinue(){ + // Prepare for scanning + ble_client_connected=false; + doConnect=false; + ble_packets_received=0; + + doScan=true; // start scanning for new devices +} + +bool connectToServer(){ + if(pRemoteDevice==nullptr){ + Serial.println("Invalid remote device, can't connect"); + return false; + } + + // Disconnect client + if((pClient != nullptr)&&(pClient->isConnected())){ + pClient->disconnect(); + } + + Serial.print("Forming a connection to "); + Serial.println(pRemoteDevice->getAddress().toString().c_str()); + + delay(100); + + // Connect to the remote BLE Server. + pClient->connect(pRemoteDevice); + if(!(pClient->isConnected())){ + debug(String("BLE: failed to connect")); + Serial.println("Failed to connect to server"); + pClient->disconnect(); + return false; + } + + Serial.println(" - Connected to server"); + + + // Get remote service + pRemoteService = pClient->getService(serviceUUID); + if (pRemoteService == nullptr){ + debug(String("BLE: failed to find service UUID")); + Serial.print("Failed to find our service UUID: "); + Serial.println(serviceUUID.toString().c_str()); + pClient->disconnect(); + return false; + } + Serial.println(" - Found our service"); + + + // Get BMS receive characteristic + pRemoteCharacteristic_rx = pRemoteService->getCharacteristic(charUUID_rx); + if (pRemoteCharacteristic_rx == nullptr){ + debug(String("BLE: failed to find RX UUID")); + Serial.print("Failed to find rx UUID: "); + Serial.println(charUUID_rx.toString().c_str()); + pClient->disconnect(); + return false; + } + Serial.println(" - Found RX characteristic"); + + + // Register callback for remote characteristic (receive channel) + if (pRemoteCharacteristic_rx->canNotify()){ + pRemoteCharacteristic_rx->registerForNotify(MyNotifyCallback); + }else{ + debug(String("BLE: failed to register notification of remote characteristic")); + Serial.println("Failed to register notification of remote characteristic"); + pClient->disconnect(); + return false; + } + Serial.println(" - Registered remote characteristic for notification"); + + + // Get BMS transmit characteristic + pRemoteCharacteristic_tx = pRemoteService->getCharacteristic(charUUID_tx); + if (pRemoteCharacteristic_tx == nullptr){ + debug(String("BLE: failed to find TX UUID")); + Serial.print("Failed to find tx UUID: "); + Serial.println(charUUID_tx.toString().c_str()); + pClient->disconnect(); + return false; + } + Serial.println(" - Found TX characteristic"); + + + // Check whether tx is writeable + if (!(pRemoteCharacteristic_tx->canWriteNoResponse())){ + debug(String("BLE: failed TX remote characteristic is not writable")); + Serial.println("Failed TX remote characteristic is not writable"); + pClient->disconnect(); + return false; + } + Serial.println(" - TX is writable"); + + + delay(BLE_REQUEST_DELAY); // wait, otherwise writeValue doesn't work for some reason + // to do: fix this ugly hack + + debug(String("BLE: connected")); + + return true; +} + +bool sendCommand(uint8_t *data, uint32_t dataLen){ + if((pClient!=nullptr)&&(pClient->isConnected())){ + pRemoteCharacteristic_tx->writeValue(data, dataLen, false); + return true; + }else{ + return false; + } +} diff --git a/BMS_process_data.ino b/BMS_process_data.ino new file mode 100644 index 0000000..70b8463 --- /dev/null +++ b/BMS_process_data.ino @@ -0,0 +1,293 @@ +bool isPacketValid(byte *packet) //check if packet is valid +{ + if (packet == nullptr){ + return false; + } + + bmsPacketHeaderStruct *pHeader = (bmsPacketHeaderStruct *)packet; + int checksumPos = pHeader->dataLen + 2; // status + data len + data + + int offset = 2; // header 0xDD and command type are not in data length + + if (packet[0] != 0xDD){ + // start bit missing + return false; + } + + if (packet[offset + checksumPos + 2] != 0x77){ + // stop bit missing + return false; + } + + byte checksum = 0; + for (int i = 0; i < checksumPos; i++){ + checksum += packet[offset + i]; + } + checksum = ((checksum ^ 0xFF) + 1) & 0xFF; + + if (checksum != packet[offset + checksumPos + 1]){ + return false; + } + + return true; +} + +bool processBasicInfo(packBasicInfoStruct *output, byte *data, unsigned int dataLen) +{ + // Expected data len + if (dataLen != 0x1B) + { + return false; + } + + output->Volts = ((uint32_t)two_ints_into16(data[0], data[1])) * 10; // Resolution 10 mV -> convert to milivolts eg 4895 > 48950mV + output->Amps = ((int32_t)two_ints_into16(data[2], data[3])) * 10; // Resolution 10 mA -> convert to miliamps + + output->Watts = output->Volts * output->Amps / 1000000; // W + + output->CapacityRemainAh = ((uint16_t)two_ints_into16(data[4], data[5])) * 10; + output->CapacityRemainPercent = ((uint8_t)data[19]); + + output->Temp1 = (((uint16_t)two_ints_into16(data[23], data[24])) - 2731); + output->Temp2 = (((uint16_t)two_ints_into16(data[25], data[26])) - 2731); + output->BalanceCodeLow = (two_ints_into16(data[12], data[13])); + output->BalanceCodeHigh = (two_ints_into16(data[14], data[15])); + output->MosfetStatus = ((byte)data[20]); + + return true; +} + +bool processCellInfo(packCellInfoStruct *output, byte *data, unsigned int dataLen) +{ + uint16_t _cellSum; + uint16_t _cellMin = 5000; + uint16_t _cellMax = 0; + uint16_t _cellAvg; + uint16_t _cellDiff; + + output->NumOfCells = dataLen / 2; // data contains 2 bytes per cell + + //go trough individual cells + for (byte i = 0; i < dataLen / 2; i++) + { + output->CellVolt[i] = ((uint16_t)two_ints_into16(data[i * 2], data[i * 2 + 1])); // Resolution 1 mV + _cellSum += output->CellVolt[i]; + if (output->CellVolt[i] > _cellMax) + { + _cellMax = output->CellVolt[i]; + } + if (output->CellVolt[i] < _cellMin) + { + _cellMin = output->CellVolt[i]; + } + } + + output->CellMin = _cellMin; + output->CellMax = _cellMax; + output->CellDiff = _cellMax - _cellMin; + output->CellAvg = _cellSum / output->NumOfCells; + + return true; +} + +bool bmsProcessPacket(byte *packet) +{ + bool isValid = isPacketValid(packet); + + if (isValid != true) + { + Serial.println("Invalid packer received"); + return false; + } + + bmsPacketHeaderStruct *pHeader = (bmsPacketHeaderStruct *)packet; + byte *data = packet + sizeof(bmsPacketHeaderStruct); // TODO Fix this ugly hack + unsigned int dataLen = pHeader->dataLen; + + bool result = false; + + // find packet type (basic info or cell info) + switch (pHeader->type) + { + case cBasicInfo: + { + // Process basic info + result = processBasicInfo(&packBasicInfo, data, dataLen); + if(result==true){ + ble_packets_received |= 0b01; + bms_last_update_time=millis(); + } + + break; + } + + case cCellInfo: + { + // Process cell info + result = processCellInfo(&packCellInfo, data, dataLen); + if(result==true){ + ble_packets_received |= 0b10; + bms_last_update_time=millis(); + } + break; + } + + default: + result = false; + Serial.printf("Unsupported packet type detected. Type: %d", pHeader->type); + } + + return result; +} + +bool bleCollectPacket(char *data, uint32_t dataSize) // reconstruct packet, called by notifyCallback function +{ + static uint8_t packetstate = 0; //0 - empty, 1 - first half of packet received, 2- second half of packet received + + // packet sizes: + // (packet ID 03) = 4 (header) + 23 + 2*N_NTCs + 2 (checksum) + 1 (stop) + // (packet ID 04) = 4 (header) + 2*NUM_CELLS + 2 (checksum) + 1 (stop) + static uint8_t packetbuff[4 + 2*25 + 2 + 1] = {0x0}; // buffer size suitable for up to 25 cells + + static uint32_t totalDataSize = 0; + bool retVal = false; + //hexDump(data,dataSize); + + if(totalDataSize + dataSize > sizeof(packetbuff)){ + Serial.printf("ERROR: datasize is overlength."); + + debug( + String("ERROR: datasize is overlength. ") + + String("allocated=") + + String(sizeof(packetbuff)) + + String(", size=") + + String(totalDataSize + dataSize) + ); + + totalDataSize = 0; + packetstate = 0; + + retVal = false; + } + else if (data[0] == 0xdd && packetstate == 0) // probably got 1st half of packet + { + packetstate = 1; + for (uint8_t i = 0; i < dataSize; i++) + { + packetbuff[i] = data[i]; + } + totalDataSize = dataSize; + retVal = true; + } + else if (data[dataSize - 1] == 0x77 && packetstate == 1) //probably got 2nd half of the packet + { + packetstate = 2; + for (uint8_t i = 0; i < dataSize; i++) + { + packetbuff[i + totalDataSize] = data[i]; + } + totalDataSize += dataSize; + retVal = true; + } + + if (packetstate == 2) //got full packet + { + uint8_t packet[totalDataSize]; + memcpy(packet, packetbuff, totalDataSize); + + bmsProcessPacket(packet); //pass pointer to retrieved packet to processing function + packetstate = 0; + totalDataSize = 0; + retVal = true; + } + return retVal; +} + +bool bmsRequestBasicInfo(){ + // header status command length data checksum footer + // DD A5 03 00 FF FD 77 + uint8_t data[7] = {0xdd, 0xa5, cBasicInfo, 0x0, 0xff, 0xfd, 0x77}; + return sendCommand(data, sizeof(data)); +} + +bool bmsRequestCellInfo(){ + // header status command length data checksum footer + // DD A5 04 00 FF FC 77 + uint8_t data[7] = {0xdd, 0xa5, cCellInfo, 0x0, 0xff, 0xfc, 0x77}; + return sendCommand(data, sizeof(data)); +} + +/* +void printBasicInfo() //debug all data to uart +{ + Serial.printf("Total voltage: %f\n", (float)packBasicInfo.Volts / 1000); + Serial.printf("Amps: %f\n", (float)packBasicInfo.Amps / 1000); + Serial.printf("CapacityRemainAh: %f\n", (float)packBasicInfo.CapacityRemainAh / 1000); + Serial.printf("CapacityRemainPercent: %d\n", packBasicInfo.CapacityRemainPercent); + Serial.printf("Temp1: %f\n", (float)packBasicInfo.Temp1 / 10); + Serial.printf("Temp2: %f\n", (float)packBasicInfo.Temp2 / 10); + Serial.printf("Balance Code Low: 0x%x\n", packBasicInfo.BalanceCodeLow); + Serial.printf("Balance Code High: 0x%x\n", packBasicInfo.BalanceCodeHigh); + Serial.printf("Mosfet Status: 0x%x\n", packBasicInfo.MosfetStatus); +} + +void printCellInfo() //debug all data to uart +{ + Serial.printf("Number of cells: %u\n", packCellInfo.NumOfCells); + for (byte i = 1; i <= packCellInfo.NumOfCells; i++) + { + Serial.printf("Cell no. %u", i); + Serial.printf(" %f\n", (float)packCellInfo.CellVolt[i - 1] / 1000); + } + Serial.printf("Max cell volt: %f\n", (float)packCellInfo.CellMax / 1000); + Serial.printf("Min cell volt: %f\n", (float)packCellInfo.CellMin / 1000); + Serial.printf("Difference cell volt: %f\n", (float)packCellInfo.CellDiff / 1000); + Serial.printf("Average cell volt: %f\n", (float)packCellInfo.CellAvg / 1000); + Serial.println(); +} + +void constructBigString() //debug all data to uart +{ + stringBuffer[0] = '\0'; //clear old data + snprintf(stringBuffer, STRINGBUFFERSIZE, "Total voltage: %f\n", (float)packBasicInfo.Volts / 1000); + snprintf(stringBuffer, STRINGBUFFERSIZE, "Amps: %f\n", (float)packBasicInfo.Amps / 1000); + snprintf(stringBuffer, STRINGBUFFERSIZE, "CapacityRemainAh: %f\n", (float)packBasicInfo.CapacityRemainAh / 1000); + snprintf(stringBuffer, STRINGBUFFERSIZE, "CapacityRemainPercent: %d\n", packBasicInfo.CapacityRemainPercent); + snprintf(stringBuffer, STRINGBUFFERSIZE, "Temp1: %f\n", (float)packBasicInfo.Temp1 / 10); + snprintf(stringBuffer, STRINGBUFFERSIZE, "Temp2: %f\n", (float)packBasicInfo.Temp2 / 10); + snprintf(stringBuffer, STRINGBUFFERSIZE, "Balance Code Low: 0x%x\n", packBasicInfo.BalanceCodeLow); + snprintf(stringBuffer, STRINGBUFFERSIZE, "Balance Code High: 0x%x\n", packBasicInfo.BalanceCodeHigh); + snprintf(stringBuffer, STRINGBUFFERSIZE, "Mosfet Status: 0x%x\n", packBasicInfo.MosfetStatus); + + snprintf(stringBuffer, STRINGBUFFERSIZE, "Number of cells: %u\n", packCellInfo.NumOfCells); + for (byte i = 1; i <= packCellInfo.NumOfCells; i++) + { + snprintf(stringBuffer, STRINGBUFFERSIZE, "Cell no. %u", i); + snprintf(stringBuffer, STRINGBUFFERSIZE, " %f\n", (float)packCellInfo.CellVolt[i - 1] / 1000); + } + snprintf(stringBuffer, STRINGBUFFERSIZE, "Max cell volt: %f\n", (float)packCellInfo.CellMax / 1000); + snprintf(stringBuffer, STRINGBUFFERSIZE, "Min cell volt: %f\n", (float)packCellInfo.CellMin / 1000); + snprintf(stringBuffer, STRINGBUFFERSIZE, "Difference cell volt: %f\n", (float)packCellInfo.CellDiff / 1000); + snprintf(stringBuffer, STRINGBUFFERSIZE, "Average cell volt: %f\n", (float)packCellInfo.CellAvg / 1000); + snprintf(stringBuffer, STRINGBUFFERSIZE, "\n"); +} + +void hexDump(const char *data, uint32_t dataSize) //debug function +{ + Serial.println("HEX data:"); + + for (int i = 0; i < dataSize; i++) + { + Serial.printf("0x%x, ", data[i]); + } + Serial.println(""); +} +*/ + +int16_t two_ints_into16(int highbyte, int lowbyte) // turns two bytes into a single long integer +{ + int16_t result = (highbyte); + result <<= 8; //Left shift 8 bits, + result = (result | lowbyte); //OR operation, merge the two + return result; +} diff --git a/Compiled/main.elf.7z b/Compiled/main.elf.7z new file mode 100644 index 0000000..af4a39f Binary files /dev/null and b/Compiled/main.elf.7z differ diff --git a/Compiled/main_HW0.bin b/Compiled/main_HW0.bin new file mode 100644 index 0000000..9d4224b Binary files /dev/null and b/Compiled/main_HW0.bin differ diff --git a/Compiled/main_HW0.partitions.bin b/Compiled/main_HW0.partitions.bin new file mode 100644 index 0000000..2108af9 Binary files /dev/null and b/Compiled/main_HW0.partitions.bin differ diff --git a/ESPBMS.ino b/ESPBMS.ino new file mode 100644 index 0000000..d464072 --- /dev/null +++ b/ESPBMS.ino @@ -0,0 +1,133 @@ +/* + * Xiaoxiang BMS to MQTT via WiFi + * by Bas Vermulst - https://github.com/BeaverUI/ESP32-BluetoothBMS2MQTT + * + * Based on original work from https://github.com/kolins-cz/Smart-BMS-Bluetooth-ESP32/blob/master/README.md + * + + === configuring === + Using the #define parameters in the CONFIGURATION section, do the following: + 1) configure WiFi via WIFI_SSID and WIFI_PASSWORD + 2) configure MQTT broker via MQTTSERVER + 3) set unique node name via NODE_NAME + 4) ensure the BMS settings are OK. You can verify the name and address using the "BLE Scanner" app on an Android phone. + + + === compiling === + 1) Add ESP-WROVER to the board manager: + - File --> Preferences, add to board manager URLs: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json + - Then, Tools --> Board --> Board manager... --> install ESP32 package v2.0.3 or later. + + + 2) Install required libraries: + - MQTT (by Joel Gaehwiler) + Install via "Manage Libraries..." under Tools. + + 3) Configure board: + Select Tools --> Board --> "ESP32 Arduino" --> "ESP WRover module" + + 4) Connect board via USB. + + 5) Configure programming options: + Select appropriate programming port under Tools --> Port + Select correct partitioning: 1.9MB app (with OTA) or 2MB app with 2 MB SPIFFS - otherwise it won't fit + + 6) Program and go! + + */ + +// ==== CONFIGURATION ==== +// BMS +#define BMS_MAX_CELLS 15 // defines size of data types +#define BMS_POLLING_INTERVAL 10*60*1000 // data output interval (shorter = connect more often = more battery consumption from BMS) in ms + +// BLE +#define BLE_MIN_RSSI -75 // minimum signal strength before connection is attempted +#define BLE_NAME "BMS 1.1" // name of BMS +#define BLE_ADDRESS "70:e3:97:d2:b0:bb" // address of BMS + +#define BLE_SCAN_DURATION 1 // duration of scan in seconds +#define BLE_REQUEST_DELAY 500 // package request delay after connecting - make this large enough to have the connection established in ms +#define BLE_TIMEOUT 10*1000 // timeout of scan + gathering packets (too short will fail collecting all packets) in ms + +#define BLE_CALLBACK_DEBUG true // send debug messages via MQTT & serial in callbacks (handy for finding your BMS address, name, RSSI, etc) + +// watchdog timeout +#define WATCHDOG_TIMEOUT (BLE_TIMEOUT+10*1000) // go to sleep after x seconds + + +// ==== MAIN CODE ==== +#include "datatypes.h" // for brevity the BMS stuff is in this file +#include // for BLE + +#include // to read ESP battery voltage +#include // to get reset reason + + +// Init BMS +static BLEUUID serviceUUID("0000ff00-0000-1000-8000-00805f9b34fb"); //xiaoxiang bms service +static BLEUUID charUUID_rx("0000ff01-0000-1000-8000-00805f9b34fb"); //xiaoxiang bms rx id +static BLEUUID charUUID_tx("0000ff02-0000-1000-8000-00805f9b34fb"); //xiaoxiang bms tx id + +const byte cBasicInfo = 3; //datablock 3=basic info +const byte cCellInfo = 4; //datablock 4=individual cell info +packBasicInfoStruct packBasicInfo; +packCellInfoStruct packCellInfo; +unsigned long bms_last_update_time=0; +bool bms_status; +#define BLE_PACKETSRECEIVED_BEFORE_STANDBY 0b11 // packets to gather before disconnecting + +// Other stuff +float battery_voltage=0; // internal battery voltage +String debug_log_string=""; +hw_timer_t * wd_timer = NULL; + +void debug(String s){ + Serial.println(s); +} + +void setup(){ + Serial.begin(115200); +} + + +// === Main stuff ==== +void loop(){ + bleGatherPackets(); +} + +/* + mqttclient.publish(GetTopic("ip"), IPAddressString(WiFi.localIP())); + mqttclient.publish(GetTopic("free-heap"), String(ESP.getFreeHeap())); + mqttclient.publish(GetTopic("maxalloc-heap"), String(xPortGetMinimumEverFreeHeapSize())); + mqttclient.publish(GetTopic("ssid"), WiFi.SSID()); + mqttclient.publish(GetTopic("rssi"), String(WiFi.RSSI())); + + mqttclient.publish(GetTopic("reset-reason"), String(GetResetReason(0)) + String(" | ") + String(GetResetReason(1))); + + mqttclient.publish(GetTopic("runtime"), String(millis()/1000)); + mqttclient.publish(GetTopic("battery-voltage"), String(battery_voltage,2)); + + mqttclient.publish(GetTopic("bms-status"), String(bms_status)); + mqttclient.publish(GetTopic("bms-status-age"), String( (millis()-bms_last_update_time)/1000 )); + + if(bms_status){ + mqttclient.publish(GetTopic("number-of-cells"), String(packCellInfo.NumOfCells)); + mqttclient.publish(GetTopic("current"), String((float)packBasicInfo.Amps / 1000,2)); + mqttclient.publish(GetTopic("voltage"), String((float)packBasicInfo.Volts / 1000,2)); + if(packCellInfo.NumOfCells != 0){ + mqttclient.publish(GetTopic("cell-voltage"), String((float)packBasicInfo.Volts /(1000*packCellInfo.NumOfCells), 2)); + } + mqttclient.publish(GetTopic("cell-diff"), String((float)packCellInfo.CellDiff, 0)); + mqttclient.publish(GetTopic("soc"), String((float)packBasicInfo.CapacityRemainPercent,1)); + + mqttclient.publish(GetTopic("temperature-1"), String((float)packBasicInfo.Temp1 / 10,1)); + mqttclient.publish(GetTopic("temperature-2"), String((float)packBasicInfo.Temp2 / 10,1)); + } + + MqttPublishDebug(); + + mqttclient.loop(); +} + +*/ diff --git a/EasyFunctions.ino b/EasyFunctions.ino new file mode 100644 index 0000000..e6d4960 --- /dev/null +++ b/EasyFunctions.ino @@ -0,0 +1,48 @@ +// EasyFunctions - some handy functions that I regularly use +// by Bas Vermulst + +bool InterruptPending(unsigned long *prev_millis, unsigned int period, int mode){ + // mode = 0: approximate mode without catch-up + // mode = 1: exact mode without catch-up + // mode = 2: exact mode with catch-up + // note: overflow is handled correctly and exactly (tested) + + if( (millis()-(*prev_millis) > period) || (millis()-(*prev_millis) < 0)){ + // trigger detected + switch(mode){ + default: + case 0: + // approximate mode without catch-up + *prev_millis=millis(); + break; + + case 1: + // exact mode without catch-up + while(millis()-(*prev_millis) > period){ // unwind + *prev_millis=*prev_millis+period; + } + break; + + case 2: + // exact mode with catch-up + *prev_millis=*prev_millis+period; + break; + } + + return true; + }else{ + return false; + } +} + + +String IPAddressString(IPAddress address){ + return String(address[0]) + "." + String(address[1]) + "." + String(address[2]) + "." + String(address[3]); +} + + +String Float2SciStr(float number, int digits){ + char char_buffer[40]; + sprintf(char_buffer,"%.*E", digits, number); + return String(char_buffer); +} diff --git a/bin/boot_app0.bin b/bin/boot_app0.bin new file mode 100644 index 0000000..13562ca Binary files /dev/null and b/bin/boot_app0.bin differ diff --git a/bin/bootloader_qio_80m.bin b/bin/bootloader_qio_80m.bin new file mode 100644 index 0000000..63c05c1 Binary files /dev/null and b/bin/bootloader_qio_80m.bin differ diff --git a/bin/build b/bin/build new file mode 100755 index 0000000..d5a9e10 --- /dev/null +++ b/bin/build @@ -0,0 +1,29 @@ +#!/bin/bash +if [ ! -d "Compiled" ] ; then mkdir Compiled ; fi + +if [ "${DRONE_TAG}" ] ; then + NAME=${DRONE_TAG} +elif [ "${DRONE_BRANCH}" ] ; then + NAME=${DRONE_BRANCH}-${DRONE_BUILD_NUMBER} +else + NAME=`git symbolic-ref --short HEAD` +fi + +LIBS="--library `pwd`/libs/ESPAsyncWebServer/src --library `pwd`/libs/ArduinoRS485/src" +LIBS="" + +if [ ! "$1" ] || [ "$1" == "0" ] ; then + echo "#define VERSION \"${NAME}\"" >> compile_flags.h + arduino-cli compile $LIBS -e -b esp32:esp32:esp32 || exit 1 + mv build/esp32.esp32.esp32/*.ino.bin Compiled/${NAME}_HW0.bin + mv build/esp32.esp32.esp32/*.ino.elf Compiled/${NAME}_HW0.elf + mv build/esp32.esp32.esp32/*.ino.partitions.bin Compiled/${NAME}_HW0.partitions.bin + #if [ "$1" ] ; then + bin/flash Compiled/${NAME}_HW0.bin + #fi +fi + +if [ ! "$1" ] ; then + 7z a -mx=9 Compiled/${NAME}.elf.7z Compiled/*.elf + rm Compiled/*.elf +fi diff --git a/bin/flash b/bin/flash new file mode 100755 index 0000000..e72e8b1 --- /dev/null +++ b/bin/flash @@ -0,0 +1,17 @@ +#!/bin/sh +BASENAME=${1%.bin}; + +esptool.py \ +-b 921600 \ +--chip esp32 \ +--before default_reset \ +--after hard_reset \ +write_flash \ +-z \ +--flash_mode dio \ +--flash_freq 80m \ +--flash_size detect \ +0x10000 $BASENAME.bin \ +0x8000 $BASENAME.partitions.bin \ +0xe000 bin/boot_app0.bin \ +0x1000 bin/bootloader_qio_80m.bin diff --git a/datatypes.h b/datatypes.h new file mode 100644 index 0000000..b337546 --- /dev/null +++ b/datatypes.h @@ -0,0 +1,56 @@ +#ifndef mydatatypes_H_ +#define mydatatypes_H_ + +typedef struct +{ + byte start; + byte type; + byte status; + byte dataLen; +} bmsPacketHeaderStruct; + +typedef struct +{ + uint16_t Volts; // unit 1mV + int32_t Amps; // unit 1mA + int32_t Watts; // unit 1W + uint16_t CapacityRemainAh; + uint8_t CapacityRemainPercent; //unit 1% + uint16_t Temp1; //unit 0.1C + uint16_t Temp2; //unit 0.1C + uint16_t BalanceCodeLow; + uint16_t BalanceCodeHigh; + uint8_t MosfetStatus; + +} packBasicInfoStruct; + +typedef struct +{ + uint8_t NumOfCells; + uint16_t CellVolt[BMS_MAX_CELLS]; //cell 1 has index 0 :-/ + uint16_t CellMax; + uint16_t CellMin; + uint16_t CellDiff; // difference between highest and lowest + uint16_t CellAvg; +} packCellInfoStruct; + +/* +struct packEepromStruct +{ + uint16_t POVP; + uint16_t PUVP; + uint16_t COVP; + uint16_t CUVP; + uint16_t POVPRelease; + uint16_t PUVPRelease; + uint16_t COVPRelease; + uint16_t CUVPRelease; + uint16_t CHGOC; + uint16_t DSGOC; +}; + +#define STRINGBUFFERSIZE 300 +char stringBuffer[STRINGBUFFERSIZE]; +*/ + +#endif /* mydatatypes_H_ */