commit 5620f73fa1641dab67406380ed38de4ceecbe2a9 Author: James Date: Mon Apr 29 09:09:52 2024 +0100 initial version 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 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..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); + } +}