From 0c6e61aff4bc903471ac1e3392e8802b9420a9e8 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 29 Apr 2024 09:17:04 +0100 Subject: [PATCH] POST to HQ using ESPv3 --- Buffer.h | 52 +++++++++++++++++++++ ESPBMS.ino | 118 ++++++++++++++++++++++++++++++++++++----------- HQ.h | 98 +++++++++++++++++++++++++++++++++++++++ bin/2graphite.py | 57 ----------------------- bin/build | 2 +- ca_cert.h | 23 +++++++++ victron.ino | 26 ++++------- 7 files changed, 273 insertions(+), 103 deletions(-) create mode 100644 Buffer.h create mode 100644 HQ.h delete mode 100644 bin/2graphite.py create mode 100644 ca_cert.h diff --git a/Buffer.h b/Buffer.h new file mode 100644 index 0000000..99099db --- /dev/null +++ b/Buffer.h @@ -0,0 +1,52 @@ +#ifndef BUFFER_H +#define BUFFER_H + +#include + +class Buffer { +private: + char* data_; + size_t size_; + size_t currentLength_; + const char separator_; + boolean overflow=false; + +public: + Buffer(size_t size, const char separator = '\0') + : size_(size), currentLength_(0), separator_(separator) { + data_ = new char[size_]; + data_[0] = '\0'; // Ensure the buffer is null-terminated + } + + ~Buffer() { + delete[] data_; + } + + bool add(const char* str) { + size_t strLength = strlen(str); + if (currentLength_ + strLength + 1 + (currentLength_ > 0 && separator_ != '\0') < size_) { + if (currentLength_ > 0 && separator_ != '\0') { + strcat(data_, &separator_); + ++currentLength_; + } + strcat(data_, str); + currentLength_ += strLength; + return true; + } else { + overflow=true; + return false; + } + } + + void clear() { + data_[0] = '\0'; + currentLength_ = 0; + overflow=false; + } + + const char* get() const { + return data_; + } +}; + +#endif diff --git a/ESPBMS.ino b/ESPBMS.ino index 2660332..b5921e3 100644 --- a/ESPBMS.ino +++ b/ESPBMS.ino @@ -1,9 +1,21 @@ -#include "BLEDevice.h" +#include "HQ.h" +#include "BLEDevice.h" 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 +#ifndef ETH_PHY_TYPE +#define ETH_PHY_TYPE ETH_PHY_LAN8720 +#define ETH_PHY_ADDR 1 +#define ETH_PHY_MDC 23 +#define ETH_PHY_MDIO 18 +#define ETH_PHY_POWER 16 +#define ETH_CLK_MODE ETH_CLOCK_GPIO0_IN +#endif +#include + + typedef struct { byte start; @@ -12,15 +24,56 @@ typedef struct byte dataLen; } bmsPacketHeaderStruct; -void setup() { - Serial.begin(115200); - BLEDevice::init(""); // Initialize BLE device -} - char currentName[128]; bool gotBasicInfo; bool gotCellInfo; +static bool eth_ready = false; +void onEvent(arduino_event_id_t event){ + Serial.print("E:"); + Serial.println(event); + switch (event) { + case ARDUINO_EVENT_ETH_START: + Serial.println("[ETH] Started"); + //set eth hostname here + ETH.setHostname("esp32-ethernet"); + break; + case ARDUINO_EVENT_ETH_CONNECTED: + Serial.println("[ETH] Connected"); + break; + case ARDUINO_EVENT_ETH_GOT_IP: + Serial.print("[ETH] MAC: "); + Serial.print(ETH.macAddress()); + Serial.print(", IPv4: "); + Serial.print(ETH.localIP()); + if (ETH.fullDuplex()) { + Serial.print(", FULL_DUPLEX"); + } + Serial.print(", "); + Serial.print(ETH.linkSpeed()); + Serial.println("Mbps"); + eth_ready = true; + break; + case ARDUINO_EVENT_ETH_DISCONNECTED: + Serial.println("[ETH] Disconnected"); + eth_ready = false; + break; + case ARDUINO_EVENT_ETH_STOP: + Serial.println("[ETH] Stopped"); + eth_ready = false; + break; + default: + break; + } +} + +void setup() { + esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); + Serial.begin(115200); + Network.onEvent(onEvent); + ETH.begin(); + BLEDevice::init(""); +} void loop() { Serial.printf("\r\n\r\n===============================\r\n\r\n"); @@ -29,19 +82,24 @@ void loop() { BLEScan* pBLEScan = BLEDevice::getScan(); // Create new scan pBLEScan->setActiveScan(true); // Active scan uses more power, but get results faster - BLEScanResults foundDevices = pBLEScan->start(5); //seconds + BLEScanResults* foundDevices = pBLEScan->start(5); //seconds - Serial.println("Devices found: " + String(foundDevices.getCount())); + Serial.println("Devices found: " + String(foundDevices->getCount())); - for (int i = 0; i < foundDevices.getCount(); i++) { + while(!eth_ready){ + Serial.println("Wait for eth..."); + delay(250); + } + + for (int i = 0; i < foundDevices->getCount(); i++) { delay(1000); Serial.printf("\r\n\r\n===============================\r\n\r\n"); - BLEAdvertisedDevice advertisedDevice = foundDevices.getDevice(i); + BLEAdvertisedDevice advertisedDevice = foundDevices->getDevice(i); Serial.println("\nFound Device: " + String(advertisedDevice.toString().c_str())); - std::string targetAddress = "d0:65:de:e5:89:76"; - if (advertisedDevice.getAddress().toString() == targetAddress) { + String targetAddress = "d0:65:de:e5:89:76"; + if (advertisedDevice.getAddress().toString().c_str() == targetAddress) { Serial.println("Victron device found!"); decodeVictron(advertisedDevice); break; @@ -86,12 +144,15 @@ void loop() { // Get BMS receive characteristic Serial.println("Get BMS receive characteristic..."); BLERemoteCharacteristic* pRemoteCharacteristic_rx = pRemoteService->getCharacteristic(charUUID_rx); + Serial.println("Got BMS receive characteristic..."); if (pRemoteCharacteristic_rx == nullptr){ Serial.println(String("BLE: failed to find RX UUID")); Serial.print("Failed to find rx UUID: "); Serial.println(charUUID_rx.toString().c_str()); pClient->disconnect(); continue; + }else { + Serial.println("RX characteristic found successfully."); } // Register callback for remote characteristic (receive channel) @@ -130,7 +191,7 @@ void loop() { gotBasicInfo=false; gotCellInfo=false; - std::__cxx11::string deviceName = advertisedDevice.getName(); + String deviceName = advertisedDevice.getName(); strncpy(currentName, deviceName.c_str(), sizeof(currentName) - 1); currentName[sizeof(currentName) - 1] = '\0'; // Ensure null-termination // Convert to lowercase and replace spaces with dots @@ -161,6 +222,7 @@ void loop() { } } pClient->disconnect(); + hq.poll(); } Serial.println("Reboot!"); delay(100); @@ -304,8 +366,8 @@ bool processBasicInfo(byte *data, unsigned int dataLen){ int32_t Watts = Volts * Amps / 1000000; // W - //Serial.printf("Remaining Capacity: %4.2fAhr\n", ((float)(data[4] * 256 + data[5]))/100); - //Serial.printf("Nominal Capacity: %4.2fAhr\n", ((float)(data[6] * 256 + data[7]))/100); + //Serial.printf("Remaining Capacity: %4.2fAh\n", ((float)(data[4] * 256 + data[5]))/100); + //Serial.printf("Nominal Capacity: %4.2fAh\n", ((float)(data[6] * 256 + data[7]))/100); uint32_t CapacityRemainAh = ((uint16_t)two_ints_into16(data[4], data[5])) * 10; uint8_t CapacityRemainPercent = ((uint8_t)data[19]); @@ -317,14 +379,14 @@ bool processBasicInfo(byte *data, unsigned int dataLen){ uint16_t BalanceCodeHigh = (two_ints_into16(data[14], data[15])); uint8_t MosfetStatus = ((byte)data[20]); - Serial.printf(">>>RC.%s.Voltage %f\r\n",currentName, (float)Volts / 1000); - Serial.printf(">>>RC.%s.Amps %f\r\n",currentName, (float)Amps / 1000); - Serial.printf(">>>RC.%s.Watts %f\r\n",currentName, (float)Watts); - Serial.printf(">>>RC.%s.Capacity_Remain_Ah %f\r\n",currentName, (float)CapacityRemainAh / 1000); - Serial.printf(">>>RC.%s.Capacity_Remain_Wh %f\r\n",currentName, ((float)(CapacityRemainAh) / 1000) * ((float)(Volts) / 1000)); - Serial.printf(">>>RC.%s.Capacity_Remain_Percent %d\r\n",currentName, CapacityRemainPercent); - Serial.printf(">>>RC.%s.Temp1 %f\r\n",currentName, (float)Temp1 / 10); - Serial.printf(">>>RC.%s.Temp2 %f\r\n",currentName, (float)Temp2 / 10); + hq.send("test.RC.%s.Voltage %f",currentName, (float)Volts / 1000); + hq.send("test.RC.%s.Amps %f",currentName, (float)Amps / 1000); + hq.send("test.RC.%s.Watts %f",currentName, (float)Watts); + hq.send("test.RC.%s.Capacity_Remain_Ah %f",currentName, (float)CapacityRemainAh / 1000); + hq.send("test.RC.%s.Capacity_Remain_Wh %f",currentName, ((float)(CapacityRemainAh) / 1000) * ((float)(Volts) / 1000)); + hq.send("test.RC.%s.Capacity_Remain_Percent %d",currentName, CapacityRemainPercent); + hq.send("test.RC.%s.Temp1 %f",currentName, (float)Temp1 / 10); + hq.send("test.RC.%s.Temp2 %f",currentName, (float)Temp2 / 10); /* Serial.printf("%s Balance Code Low: 0x%x\r\n",currentName, BalanceCodeLow); Serial.printf("%s Balance Code High: 0x%x\r\n",currentName, BalanceCodeHigh); @@ -358,13 +420,13 @@ bool processCellInfo(byte *data, unsigned int dataLen) _cellMin = CellVolt; } - Serial.printf(">>>RC.%s.Cell.%d.Voltage %f\r\n",currentName, i+1,(float)CellVolt/1000); + hq.send("test.RC.%s.Cell.%d.Voltage %f",currentName, i+1,(float)CellVolt/1000); } - Serial.printf(">>>RC.%s.Max_Cell_Voltage %f\r\n",currentName, (float)_cellMax / 1000); - Serial.printf(">>>RC.%s.Min_Cell_Voltage %f\r\n",currentName, (float)_cellMin / 1000); - Serial.printf(">>>RC.%s.Difference_Cell_Voltage %f\r\n",currentName, (float)(_cellMax - _cellMin) / 1000); - Serial.printf(">>>RC.%s.Average_Cell_Voltage %f\r\n",currentName, (float)(_cellSum / NumOfCells) / 1000); + hq.send("test.RC.%s.Max_Cell_Voltage %f",currentName, (float)_cellMax / 1000); + hq.send("test.RC.%s.Min_Cell_Voltage %f",currentName, (float)_cellMin / 1000); + hq.send("test.RC.%s.Difference_Cell_Voltage %f",currentName, (float)(_cellMax - _cellMin) / 1000); + hq.send("test.RC.%s.Average_Cell_Voltage %f",currentName, (float)(_cellSum / NumOfCells) / 1000); gotCellInfo=true; diff --git a/HQ.h b/HQ.h new file mode 100644 index 0000000..2ccfd09 --- /dev/null +++ b/HQ.h @@ -0,0 +1,98 @@ +#ifndef HQ_H +#define HQ_H + +#include +#include +#include "Buffer.h" +#include "ca_cert.h" + +class HQ { +public: + // Public static function to get the instance of the singleton class + static HQ& getInstance() { + static HQ instance; // Create a single instance of the class on first call + return instance; // Return the single instance on each call + } + +private: + // Private constructor and copy constructor to prevent outside instantiation + HQ(){ + client = new NetworkClientSecure; + if(!client) { + Serial.printf("Unable to create HTTP client\r\n"); + } + client->setCACert(ca_cert); + client->setHandshakeTimeout(5); + } + HQ(const HQ&) = delete; + Buffer txBuffer=Buffer(4096,'&'); + Buffer rxBuffer=Buffer(1024,'\n'); + NetworkClientSecure *client; + +public: + unsigned long httpErrors=0; + boolean send(const char *s){ + Serial.printf("HQ_SEND:%s\r\n",s); + return txBuffer.add(s); + } + template + boolean send(const char* format, T arg1, Args... args) + { + static char msg[1024]; + snprintf(msg, 1024, format, arg1, args...); + + return send(msg); + } + + boolean poll(){ + String payload; + if(strlen(txBuffer.get())){ + payload+=txBuffer.get(); + } + if(payload.isEmpty()){ + return true; + } + HTTPClient https; + https.setTimeout(1000); + https.setConnectTimeout(1000); + https.addHeader("Content-Type","application/x-www-form-urlencoded"); + String url="https://api.ecomotus.co.uk/api/v1/graphite"; + Serial.printf("Connecting to %s\r\n",url.c_str()); + if(!https.begin(*client, url)) { // HTTPS + Serial.printf("HTTP Connection Failed\r\n"); + httpErrors++; + return false; + } + int httpCode=https.POST(payload); + if(httpCode==200){ + txBuffer.clear(); + // Read all the lines of the reply from server and print them to Serial + boolean command=false; + char buff[128]; + int totalSize=0; + NetworkClient * stream = https.getStreamPtr(); + while(stream->available() && totalSize<1000) { + size_t size=stream->readBytesUntil('\n',buff,120); + totalSize+=size; + buff[size]=0; + Serial.printf("Response: %s\r\n",buff); + if(command==true){ + rxBuffer.add(buff); + command=false; + } + if(0==strcmp(buff,"COMMAND")){ + command=true; + } + } + Serial.printf("Got %d bytes\r\n",totalSize); + }else{ + Serial.printf("HTTP Error: %d\r\n",httpCode); + httpErrors++; + return false; + } + return true; + } +}; + +HQ& hq = HQ::getInstance(); +#endif diff --git a/bin/2graphite.py b/bin/2graphite.py deleted file mode 100644 index 7561cf7..0000000 --- a/bin/2graphite.py +++ /dev/null @@ -1,57 +0,0 @@ -import asyncio -import aiohttp -import sys -import serial_asyncio # Ensure this package is installed - -def parse_to_graphite(data): - results = [] - - lines = data.strip().split('\n') - for line in lines: - if line.startswith('>>>'): - metric_path = line[3:].strip() # Remove the ">>>" prefix and any leading/trailing whitespace - results.append(metric_path) - return results - -async def send_to_graphite(data, session, api_url): - for message in data: - #print(f"Sending data to API: {message}") # Debug message for sending data - try: - # Sending raw metric path directly as plain text - headers = {'Content-Type': 'text/plain'} - async with session.post(api_url, data=message, headers=headers) as response: - if response.status != 200: - print(f"Failed to send data: {response.status}", await response.text()) - except Exception as e: - print(f"Error sending data: {e}") - -async def handle_serial(reader, api_url): - session = aiohttp.ClientSession() - try: - while True: - line = await reader.readline() - if not line: - break - line = line.decode('utf-8') - print(line.strip()) # echo output for received line - graphite_data = parse_to_graphite(line) - if graphite_data: - await send_to_graphite(graphite_data, session, api_url) - finally: - await session.close() - -async def main(): - if len(sys.argv) < 3: - print("Usage: python script.py ") - sys.exit(1) - - serial_device = sys.argv[1] - api_url = sys.argv[2] - baud_rate = 115200 # You can modify this as needed - - # Creating the connection to the serial port - reader, _ = await serial_asyncio.open_serial_connection(url=serial_device, baudrate=baud_rate) - await handle_serial(reader, api_url) - -if __name__ == '__main__': - asyncio.run(main()) diff --git a/bin/build b/bin/build index 3c7dfb6..93d5a6f 100755 --- a/bin/build +++ b/bin/build @@ -10,7 +10,7 @@ else fi echo "#define VERSION \"${NAME}\"" >> compile_flags.h -arduino-cli compile $LIBS -e -b esp32:esp32:esp32 || exit 1 +arduino-cli compile --build-property build.partitions=huge_app --build-property upload.maximum_size=3145728 -e -b esp32:esp32:esp32 || exit 1 mv build/esp32.esp32.esp32/*.ino.bin Compiled/${NAME}.bin mv build/esp32.esp32.esp32/*.ino.elf Compiled/${NAME}.elf mv build/esp32.esp32.esp32/*.ino.partitions.bin Compiled/${NAME}.partitions.bin diff --git a/ca_cert.h b/ca_cert.h new file mode 100644 index 0000000..c77eccc --- /dev/null +++ b/ca_cert.h @@ -0,0 +1,23 @@ +#ifndef CA_CERT_H +#define CA_CERT_H +const char* ca_cert = \ + "-----BEGIN CERTIFICATE-----\n" \ + "MIIDHTCCAgWgAwIBAgIUWLAb5lCXs4G6QxCaV78EtsRQgkEwDQYJKoZIhvcNAQEL\n" \ + "BQAwHTEbMBkGA1UEAwwSYXBpLmVjb21vdHVzLmNvLnVrMCAXDTIyMTAxOTE1Mjgx\n" \ + "N1oYDzIxMjIwOTI1MTUyODE3WjAdMRswGQYDVQQDDBJhcGkuZWNvbW90dXMuY28u\n" \ + "dWswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwXfttIwX1y1tSeaiZ\n" \ + "0LtnQ3q9xosglFsXyoLcctJmOf+zgdHbNNxMH8CRbm4Z3ZQ4ghBoL/1RHaERl5aA\n" \ + "U7oVxr4MPHn6fWsYrLlaXLIcmL6ZS91woTKLejhf6D991sH2Jt0xVDhqerimnF4p\n" \ + "Ut1U6rY6Lw7aUAUUldChhzRUAkAcMHApWwzxElAM+KFFleLq63AESkT21xYOO+WG\n" \ + "2hLTQB+hDcBvN9IQ4Ud1V7AQ/MDKzVvJsn/z+KnslbH246l1w6haJk230UbPizau\n" \ + "fAWl63O2/xIxPJzWBXJeUuvi+lTqCf+ZVPBU6chpyL4xX+I7vCTBoKmpaI+qAF9F\n" \ + "2C4HAgMBAAGjUzBRMB0GA1UdDgQWBBTtxYrL2Cg9PVp4wx6MllB/cKXXYjAfBgNV\n" \ + "HSMEGDAWgBTtxYrL2Cg9PVp4wx6MllB/cKXXYjAPBgNVHRMBAf8EBTADAQH/MA0G\n" \ + "CSqGSIb3DQEBCwUAA4IBAQBAai8ewCT3Q2CgBMxvDLKQx7YRBNlv1gbUtYq88rvK\n" \ + "iz6yzGmbPP1Ax5LCv0oRtRdnrz0h2F80tBibS2mJ2tqsLd3277yMN81mHB0qVIrR\n" \ + "tq9aTzjGHUXgXmcezEgkTLTfISebvCB8jdR7cjvFUaTUKH3MLR3jNAAqU6WLVY6Q\n" \ + "wCYLKRhTU+aYkDeObOu2fsoph8FwR9gB9D4K0/W78UTiOQxLFJmCqubooNtGLrph\n" \ + "dz1hmIkYSKH3pdhE3kZwNilYVjyfxq3UFkh2/2J0Fz7vB7eaJE6PptcPJ2KgxTMO\n" \ + "i7QEQ+jNru8B20F4DrbvEa0IY5wv9mywugBsXg5rcfjs\n" \ + "-----END CERTIFICATE-----\n"; +#endif diff --git a/victron.ino b/victron.ino index ab856f5..d20d001 100644 --- a/victron.ino +++ b/victron.ino @@ -76,20 +76,12 @@ void decodeVictron(BLEAdvertisedDevice advertisedDevice) { // at the end. uint8_t manCharBuf[manDataSizeMax+1]; - #ifdef USE_String - String manData = advertisedDevice.getManufacturerData(); // lib code returns String. - #else - std::string manData = advertisedDevice.getManufacturerData(); // lib code returns std::string - #endif + String manData = advertisedDevice.getManufacturerData(); // lib code returns String. int manDataSize=manData.length(); // This does not count the null at the end. // Copy the data from the String to a byte array. Must have the +1 so we // don't lose the last character to the null terminator. - #ifdef USE_String - manData.toCharArray((char *)manCharBuf,manDataSize+1); - #else - manData.copy((char *)manCharBuf, manDataSize+1); - #endif + manData.toCharArray((char *)manCharBuf,manDataSize+1); // Now let's setup a pointer to a struct to get to the data more cleanly. victronManufacturerData * vicData=(victronManufacturerData *)manCharBuf; @@ -185,12 +177,12 @@ void decodeVictron(BLEAdvertisedDevice advertisedDevice) { return; } - Serial.printf(">>>RC.MPPT.1.Battery_Volts %f\r\n",batteryVoltage); - Serial.printf(">>>RC.MPPT.1.Battery_Amps %f\r\n",batteryCurrent); - Serial.printf(">>>RC.MPPT.1.Battery_Watts %f\r\n",batteryVoltage*batteryCurrent); - Serial.printf(">>>RC.MPPT.1.Solar_Watts %f\r\n",inputPower); - Serial.printf(">>>RC.MPPT.1.Output_Current %f\r\n",outputCurrent); - Serial.printf(">>>RC.MPPT.1.Yield %f\r\n",todayYield); - Serial.printf(">>>RC.MPPT.1.State %d\r\n",deviceState); + hq.send("test.RC.MPPT.1.Battery_Volts %f",batteryVoltage); + hq.send("test.RC.MPPT.1.Battery_Amps %f",batteryCurrent); + hq.send("test.RC.MPPT.1.Battery_Watts %f",batteryVoltage*batteryCurrent); + hq.send("test.RC.MPPT.1.Solar_Watts %f",inputPower); + hq.send("test.RC.MPPT.1.Output_Current %f",outputCurrent); + hq.send("test.RC.MPPT.1.Yield %f",todayYield); + hq.send("test.RC.MPPT.1.State %d",deviceState); } }