From 5620f73fa1641dab67406380ed38de4ceecbe2a9 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 29 Apr 2024 09:09:52 +0100 Subject: [PATCH] initial version --- .atom-build.yml | 18 ++ .gitignore | 3 + ESPBMS.ino | 414 +++++++++++++++++++++++++++++++++++++ bin/2graphite.py | 57 +++++ bin/boot_app0.bin | Bin 0 -> 8192 bytes bin/bootloader_qio_80m.bin | Bin 0 -> 18656 bytes bin/build | 17 ++ bin/flash | 17 ++ victron.ino | 196 ++++++++++++++++++ 9 files changed, 722 insertions(+) create mode 100644 .atom-build.yml create mode 100644 .gitignore create mode 100644 ESPBMS.ino create mode 100644 bin/2graphite.py create mode 100644 bin/boot_app0.bin create mode 100644 bin/bootloader_qio_80m.bin create mode 100755 bin/build create mode 100755 bin/flash create mode 100644 victron.ino 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/ESPBMS.ino b/ESPBMS.ino new file mode 100644 index 0000000..2660332 --- /dev/null +++ b/ESPBMS.ino @@ -0,0 +1,414 @@ +#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 + +typedef struct +{ + byte start; + byte type; + byte status; + byte dataLen; +} bmsPacketHeaderStruct; + +void setup() { + Serial.begin(115200); + BLEDevice::init(""); // Initialize BLE device +} + +char currentName[128]; +bool gotBasicInfo; +bool gotCellInfo; + + +void loop() { + Serial.printf("\r\n\r\n===============================\r\n\r\n"); + + Serial.println("Scanning..."); + + 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 + + Serial.println("Devices found: " + String(foundDevices.getCount())); + + 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); + Serial.println("\nFound Device: " + String(advertisedDevice.toString().c_str())); + + std::string targetAddress = "d0:65:de:e5:89:76"; + if (advertisedDevice.getAddress().toString() == targetAddress) { + Serial.println("Victron device found!"); + decodeVictron(advertisedDevice); + break; + } + + if(!advertisedDevice.isAdvertisingService(serviceUUID)) { + Serial.println("Device does not advertise the specified service UUID."); + continue; + } + + BLEClient* pClient = BLEDevice::createClient(); + Serial.println("Connecting to: " + String(advertisedDevice.getName().c_str())); + + int retryCount = 0; + const int maxRetries = 5; // Maximum number of retries + while(retryCount < maxRetries) { + if(pClient->connect(&advertisedDevice)) { + Serial.println("Connected successfully."); + break; // Exit the loop if the connection is successful + } else { + Serial.println("Failed to connect. Retrying..."); + retryCount++; // Increment the retry counter + delay(1000); // Optional: Wait for a second before retrying + } + } + if(retryCount == maxRetries) { + Serial.println("Failed to connect after retries."); + continue; + } + + // Get remote service + Serial.println("Get remote service..."); + BLERemoteService* pRemoteService = pClient->getService(serviceUUID); + if (pRemoteService == nullptr){ + Serial.println(String("BLE: failed to find service UUID")); + Serial.print("Failed to find our service UUID: "); + Serial.println(serviceUUID.toString().c_str()); + pClient->disconnect(); + continue; + } + + // Get BMS receive characteristic + Serial.println("Get BMS receive characteristic..."); + BLERemoteCharacteristic* pRemoteCharacteristic_rx = pRemoteService->getCharacteristic(charUUID_rx); + 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; + } + + // Register callback for remote characteristic (receive channel) + Serial.println("Register callback for remote characteristic..."); + if (pRemoteCharacteristic_rx->canNotify()){ + pRemoteCharacteristic_rx->registerForNotify(MyNotifyCallback); + }else{ + Serial.println(String("BLE: failed to register notification of remote characteristic")); + Serial.println("Failed to register notification of remote characteristic"); + pClient->disconnect(); + continue; + } + + // Get BMS transmit characteristic + Serial.println("Get BMS transmit characteristic..."); + BLERemoteCharacteristic* pRemoteCharacteristic_tx = pRemoteService->getCharacteristic(charUUID_tx); + if (pRemoteCharacteristic_tx == nullptr){ + Serial.println(String("BLE: failed to find TX UUID")); + Serial.print("Failed to find tx UUID: "); + Serial.println(charUUID_tx.toString().c_str()); + pClient->disconnect(); + continue; + } + + // Check whether tx is writeable + Serial.println("Check whether tx is writeable..."); + if (!(pRemoteCharacteristic_tx->canWriteNoResponse())){ + Serial.println(String("BLE: failed TX remote characteristic is not writable")); + Serial.println("Failed TX remote characteristic is not writable"); + pClient->disconnect(); + continue; + } + + Serial.println("Get data..."); + + gotBasicInfo=false; + gotCellInfo=false; + + std::__cxx11::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 + for (char* p = currentName; *p; ++p) { + *p = *p == ' ' ? '.' : *p; + } + + unsigned long start=millis(); + while( millis()-start<10000 && (gotBasicInfo==false || gotCellInfo==false) ){ + // REQUEST BASIC INFO + if(gotBasicInfo==false){ + Serial.println("Request Basic Info"); + delay(500); + // header status command length data checksum footer + // DD A5 03 00 FF FD 77 + uint8_t a_data[7] = {0xdd, 0xa5, 3, 0x0, 0xff, 0xfd, 0x77}; + pRemoteCharacteristic_tx->writeValue(a_data, sizeof(a_data), false); + } + + // REQUEST CELL INFO + if(gotCellInfo==false){ + Serial.println("Request Cell Info"); + delay(500); + // header status command length data checksum footer + // DD A5 03 00 FF FD 77 + uint8_t b_data[7] = {0xdd, 0xa5, 4, 0x0, 0xff, 0xfc, 0x77}; + pRemoteCharacteristic_tx->writeValue(b_data, sizeof(b_data), false); + } + } + pClient->disconnect(); + } + Serial.println("Reboot!"); + delay(100); + ESP.restart(); +} + +static void MyNotifyCallback(BLERemoteCharacteristic *pBLERemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify){ + //hexDump((char*)pData, length); + if(!bleCollectPacket((char *)pData, length)){ + Serial.println("ERROR: packet could not be collected."); + } +} + +void hexDump(const char *data, uint32_t dataSize) +{ + Serial.println("HEX data:"); + + for (int i = 0; i < dataSize; i++) + { + Serial.printf("0x%x, ", data[i]); + } + Serial.println(""); +} + +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."); + + Serial.println( + 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; + + if (data[dataSize - 1] == 0x77) { + //its full packets + packetstate = 2; + } + } + 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 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 3: + { + // Process basic info + result = processBasicInfo(data, dataLen); + break; + } + + case 4: + { + // Process cell info + result = processCellInfo(data, dataLen); + break; + } + + default: + result = false; + Serial.printf("Unsupported packet type detected. Type: %d", pHeader->type); + } + + return result; +} + + +bool processBasicInfo(byte *data, unsigned int dataLen){ + //// NICER!!!: https://github.com/neilsheps/overkill-xiaoxiang-jbd-bms-ble-reader/blob/main/src/main.cpp + + uint16_t Volts = ((uint32_t)two_ints_into16(data[0], data[1])) * 10; // Resolution 10 mV -> convert to milivolts eg 4895 > 48950mV + int32_t Amps = ((int32_t)two_ints_into16(data[2], data[3])) * 10; // Resolution 10 mA -> convert to miliamps + + 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); + + uint32_t CapacityRemainAh = ((uint16_t)two_ints_into16(data[4], data[5])) * 10; + uint8_t CapacityRemainPercent = ((uint8_t)data[19]); + + uint16_t Temp1 = (((uint16_t)two_ints_into16(data[23], data[24])) - 2731); + uint16_t Temp2 = (((uint16_t)two_ints_into16(data[25], data[26])) - 2731); + + uint16_t BalanceCodeLow = (two_ints_into16(data[12], data[13])); + 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); + /* + Serial.printf("%s Balance Code Low: 0x%x\r\n",currentName, BalanceCodeLow); + Serial.printf("%s Balance Code High: 0x%x\r\n",currentName, BalanceCodeHigh); + Serial.printf("%s Mosfet Status: 0x%x\r\n",currentName, MosfetStatus); + */ + gotBasicInfo=true; + + return true; +} + +bool processCellInfo(byte *data, unsigned int dataLen) +{ + uint16_t _cellSum; + uint16_t _cellMin = 5000; + uint16_t _cellMax = 0; + uint16_t _cellAvg; + uint16_t _cellDiff; + + uint8_t NumOfCells = dataLen / 2; // data contains 2 bytes per cell + + //go trough individual cells + for (byte i = 0; i < dataLen / 2; i++){ + int CellVolt = ((uint16_t)two_ints_into16(data[i * 2], data[i * 2 + 1])); // Resolution 1 mV + _cellSum += CellVolt; + if (CellVolt > _cellMax) + { + _cellMax = CellVolt; + } + if (CellVolt < _cellMin) + { + _cellMin = CellVolt; + } + + Serial.printf(">>>RC.%s.Cell.%d.Voltage %f\r\n",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); + + gotCellInfo=true; + + return true; +} + +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; +} + +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/bin/2graphite.py b/bin/2graphite.py new file mode 100644 index 0000000..7561cf7 --- /dev/null +++ b/bin/2graphite.py @@ -0,0 +1,57 @@ +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/boot_app0.bin b/bin/boot_app0.bin new file mode 100644 index 0000000000000000000000000000000000000000..13562cabb9648287fdf70d2a22789fdf1e4156b4 GIT binary patch literal 8192 zcmeI#u?+wq2n0Z!&B7Ip%ZdwNPjZydJlFk*h+E9ra}_6R0t5&UAV7cs0RjXF5FkLH gk-)3}W&dyVhNuJx5FkK+009C72oNAZfWSu}0Te{nn*aa+ literal 0 HcmV?d00001 diff --git a/bin/bootloader_qio_80m.bin b/bin/bootloader_qio_80m.bin new file mode 100644 index 0000000000000000000000000000000000000000..63c05c18a07b944ebf8052a267f25e749e4f1476 GIT binary patch literal 18656 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 literal 0 HcmV?d00001 diff --git a/bin/build b/bin/build new file mode 100755 index 0000000..3c7dfb6 --- /dev/null +++ b/bin/build @@ -0,0 +1,17 @@ +#!/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 + +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}.bin +mv build/esp32.esp32.esp32/*.ino.elf Compiled/${NAME}.elf +mv build/esp32.esp32.esp32/*.ino.partitions.bin Compiled/${NAME}.partitions.bin +bin/flash Compiled/${NAME}.bin 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/victron.ino b/victron.ino new file mode 100644 index 0000000..ab856f5 --- /dev/null +++ b/victron.ino @@ -0,0 +1,196 @@ +// credit to: https://github.com/hoberman/Victron_BLE_Advertising_example + +#include // AES library for decrypting the Victron manufacturer data. + +uint8_t key[16]={0xd7,0x93,0xc8,0xb1,0x79,0x18,0x4c,0x97,0x97,0x6d,0x12,0x27,0xf2,0x23,0x48,0x6b}; + +int keyBits=128; // Number of bits for AES-CTR decrypt. + +// Victron docs on the manufacturer data in advertisement packets can be found at: +// https://community.victronenergy.com/storage/attachments/48745-extra-manufacturer-data-2022-12-14.pdf +// + +// Usage/style note: I use uint16_t in places where I need to force 16-bit unsigned integers +// instead of whatever the compiler/architecture might decide to use. I might not need to do +// the same with byte variables, but I'll do it anyway just to be at least a little consistent. + +// Must use the "packed" attribute to make sure the compiler doesn't add any padding to deal with +// word alignment. +typedef struct { + uint16_t vendorID; // vendor ID + uint8_t beaconType; // Should be 0x10 (Product Advertisement) for the packets we want + uint8_t unknownData1[3]; // Unknown data + uint8_t victronRecordType; // Should be 0x01 (Solar Charger) for the packets we want + uint16_t nonceDataCounter; // Nonce + uint8_t encryptKeyMatch; // Should match pre-shared encryption key byte 0 + uint8_t victronEncryptedData[21]; // (31 bytes max per BLE spec - size of previous elements) + uint8_t nullPad; // extra byte because toCharArray() adds a \0 byte. +} __attribute__((packed)) victronManufacturerData; + + +// Must use the "packed" attribute to make sure the compiler doesn't add any padding to deal with +// word alignment. +typedef struct { + uint8_t deviceState; + uint8_t errorCode; + int16_t batteryVoltage; + int16_t batteryCurrent; + uint16_t todayYield; + uint16_t inputPower; + uint8_t outputCurrentLo; // Low 8 bits of output current (in 0.1 Amp increments) + uint8_t outputCurrentHi; // High 1 bit of output current (must mask off unused bits) + uint8_t unused[4]; // Not currently used by Vistron, but it could be in the future. +} __attribute__((packed)) victronPanelData; + +// FYI, here are Device State values. I haven't seen ones with '???' so I don't know +// if they exist or not or what they might mean: +// 0 = no charge from solar +// 1 = ??? +// 2 = ??? +// 3 = bulk charge +// 4 = absorption charge +// 5 = float +// 6 = ??? +// 7 = equalization +// I've also seen a value '245' for about a second when my solar panel (simulated by a +// benchtop power supply) transitions from off/low voltage to on/higher voltage. There +// be others, but I haven't seen them. + +void decodeVictron(BLEAdvertisedDevice advertisedDevice) { + + #define manDataSizeMax 31 // BLE specs say no more than 31 bytes, but see comments below! + + // See if we have manufacturer data and then look to see if it's coming from a Victron device. + if (advertisedDevice.haveManufacturerData() == true) { + + // Note: This comment (and maybe some code?) needs to be adjusted so it's not so + // specific to String-vs-std:string. I'll leave it as-is for now so you at least + // understand why I have an extra byte added to the manCharBuf array. + // + // Here's the thing: BLE specs say our manufacturer data can be a max of 31 bytes. + // But: The library code puts this data into a String, which we will then copy to + // a character (i.e., byte) buffer using String.toCharArray(). Assuming we have the + // full 31 bytes of manufacturer data allowed by the BLE spec, we'll need to size our + // buffer with an extra byte for a null terminator. Our toCharArray() call will need + // to specify *32* bytes so it will copy 31 bytes of data with a null terminator + // 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 + 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 + + // Now let's setup a pointer to a struct to get to the data more cleanly. + victronManufacturerData * vicData=(victronManufacturerData *)manCharBuf; + + // ignore this packet if the Vendor ID isn't Victron. + if (vicData->vendorID!=0x02e1) { + return; + } + + // ignore this packet if it isn't type 0x01 (Solar Charger). + if (vicData->victronRecordType != 0x01) { + return; + } + + // Not all packets contain a device name, so if we get one we'll save it and use it from now on. + if (advertisedDevice.haveName()) { + // This works the same whether getName() returns String or std::string. + strcpy(currentName,advertisedDevice.getName().c_str()); + } + + if (vicData->encryptKeyMatch != key[0]) { + Serial.printf("Packet encryption key byte 0x%2.2x doesn't match configured key[0] byte 0x%2.2x\n", + vicData->encryptKeyMatch, key[0]); + return; + } + + uint8_t inputData[16]; + uint8_t outputData[16]={0}; // i don't really need to initialize the output. + + // The number of encrypted bytes is given by the number of bytes in the manufacturer + // data as a whole minus the number of bytes (10) in the header part of the data. + int encrDataSize=manDataSize-10; + for (int i=0; ivictronEncryptedData[i]; // copy for our decrypt below while I figure this out. + } + + esp_aes_context ctx; + esp_aes_init(&ctx); + + auto status = esp_aes_setkey(&ctx, key, keyBits); + if (status != 0) { + Serial.printf(" Error during esp_aes_setkey operation (%i).\n",status); + esp_aes_free(&ctx); + return; + } + + // construct the 16-byte nonce counter array by piecing it together byte-by-byte. + uint8_t data_counter_lsb=(vicData->nonceDataCounter) & 0xff; + uint8_t data_counter_msb=((vicData->nonceDataCounter) >> 8) & 0xff; + u_int8_t nonce_counter[16] = {data_counter_lsb, data_counter_msb, 0}; + + u_int8_t stream_block[16] = {0}; + + size_t nonce_offset=0; + status = esp_aes_crypt_ctr(&ctx, encrDataSize, &nonce_offset, nonce_counter, stream_block, inputData, outputData); + if (status != 0) { + Serial.printf("Error during esp_aes_crypt_ctr operation (%i).",status); + esp_aes_free(&ctx); + return; + } + esp_aes_free(&ctx); + + // Now do our same struct magic so we can get to the data more easily. + victronPanelData * victronData = (victronPanelData *) outputData; + + // Getting to these elements is easier using the struct instead of + // hacking around with outputData[x] references. + uint8_t deviceState=victronData->deviceState; + uint8_t errorCode=victronData->errorCode; + float batteryVoltage=float(victronData->batteryVoltage)*0.01; + float batteryCurrent=float(victronData->batteryCurrent)*0.1; + float todayYield=float(victronData->todayYield)*0.01*1000; + uint16_t inputPower=victronData->inputPower; // this is in watts; no conversion needed + + // Getting the output current takes some magic because of the way they have the + // 9-bit value packed into two bytes. The first byte has the low 8 bits of the count + // and the second byte has the upper (most significant) bit of the 9-bit value plus some + // There's some other junk in the remaining 7 bits - i'm not sure if it's useful for + // anything else but we can't use it here! - so we will mask them off. Then combine the + // two bye components to get an integer value in 0.1 Amp increments. + int integerOutputCurrent=((victronData->outputCurrentHi & 0x01)<<9) | victronData->outputCurrentLo; + float outputCurrent=float(integerOutputCurrent)*0.1; + + // I don't know why, but every so often we'll get half-corrupted data from the Victron. As + // far as I can tell it's not a decryption issue because we (usually) get voltage data that + // agrees with non-corrupted records. + // + // Towards the goal of filtering out this noise, I've found that I've rarely (or never) seen + // corrupted data when the 'unused' bits of the outputCurrent MSB equal 0xfe. We'll use this + // as a litmus test here. + uint8_t unusedBits=victronData->outputCurrentHi & 0xfe; + if (unusedBits != 0xfe) { + 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); + } +}