add victron decoder
This commit is contained in:
parent
62a13860da
commit
85434230d0
@ -40,6 +40,13 @@ void loop() {
|
||||
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;
|
||||
|
195
victron.ino
Normal file
195
victron.ino
Normal file
@ -0,0 +1,195 @@
|
||||
// credit to: https://github.com/hoberman/Victron_BLE_Advertising_example
|
||||
|
||||
#include <aes/esp_aes.h> // 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; i<encrDataSize; i++) {
|
||||
inputData[i]=vicData->victronEncryptedData[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("%-31s Battery: %6.2f Volts %6.2f Amps Solar: %6d Watts Yield: %6.0f Wh Load: %6.1f Amps State: %3d\n",
|
||||
currentName,
|
||||
batteryVoltage, batteryCurrent,
|
||||
inputPower, todayYield,
|
||||
outputCurrent, deviceState
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user