diff --git a/examples/MultiDevice/src/main.cpp b/examples/MultiDevice/src/main.cpp index a102683..76b323a 100644 --- a/examples/MultiDevice/src/main.cpp +++ b/examples/MultiDevice/src/main.cpp @@ -165,6 +165,9 @@ void setup() { // Add your devices here // Replace with your actual MAC addresses and encryption keys + // CORRECT in Alternative + // Rainbow48V at MAC e4:05:42:34:14:f3 + // Temporary - Scott Example victron.addDevice( "Rainbow48V", // Device name @@ -173,6 +176,7 @@ void setup() { DEVICE_TYPE_SOLAR_CHARGER // Device type ); + // XXX These numbers make no sesne, one above is correct - wrong MAC? victron.addDevice( "Rainbow48Vb", // Device name "3ffd00b83ffd00be", @@ -188,6 +192,23 @@ void setup() { DEVICE_TYPE_SOLAR_CHARGER // Device type ); + // WHY this one work? + victron.addDevice( + "Rainbow48Vd", // Device name + "3fca91c83fca91ce", + "0ec3adf7433dd61793ff2f3b8ad32ed8", // Encryption key (32 hex chars) + DEVICE_TYPE_SOLAR_CHARGER // Device type + ); + + // WHY this one work? + victron.addDevice( + "Rainbow48Vf", // Device name + "3fcf38283fcf382e", + "0ec3adf7433dd61793ff2f3b8ad32ed8", // Encryption key (32 hex chars) + DEVICE_TYPE_SOLAR_CHARGER // Device type + ); + + /* * [VictronBLE] Encrypted data: A0 01 83 2C 0E CF D6 04 89 72 6E 81 56 E4 2D F1 83 diff --git a/experiment/platformio.ini b/experiment/platformio.ini new file mode 100644 index 0000000..8d30ca8 --- /dev/null +++ b/experiment/platformio.ini @@ -0,0 +1,35 @@ +[env] +lib_extra_dirs = .. + +[env:esp32-s3] +platform = espressif32 +board = esp32-s3-devkitc-1 +framework = arduino +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder +build_flags = + -D ARDUINO_USB_MODE=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 +# -DCORE_DEBUG_LEVEL=3 + +[env:esp32-c3] +platform = espressif32 +framework = arduino +board = esp32-c3-devkitm-1 +board_build.mcu = esp32c3 +board_build.f_cpu = 160000000L +board_build.flash_mode = dio +board_build.partitions = default.csv +monitor_speed = 115200 +monitor_filters = time, default, esp32_exception_decoder +upload_speed = 921600 +# NOTE: Need these two ARDUIO_USB modes to work with serial +build_flags = + -Os + -I src + -D ARDUINO_ESP32C3_DEV + -D CONFIG_IDF_TARGET_ESP32C3 + -D ARDUINO_USB_MODE=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 +lib_deps = + elapsedMillis diff --git a/experiment/src/main.cpp b/experiment/src/main.cpp new file mode 100644 index 0000000..78f13f7 --- /dev/null +++ b/experiment/src/main.cpp @@ -0,0 +1,397 @@ +/* + +TODO + +*/ + +#include +#include +#include +#include +#include // AES decryption + +typedef struct { + char charMacAddr[13]; // 12 character MAC + \0 (initialized as quoted strings below for convenience) + char charKey[33]; // 32 character keys + \0 (initialized as quoted strings below for convenience) + char comment[16]; // 16 character comment (name) for printing during setup() + byte byteMacAddr[6]; // 6 bytes for MAC - initialized by setup() from quoted strings + byte byteKey[16]; // 16 bytes for encryption key - initialized by setup() from quoted strings + char cachedDeviceName[32]; // 31 characters + \0 (filled in as we receive advertisements) +} solarController; + +solarController solarControllers[] = { + // { { .charMacAddr = "e64559783cfb" }, { .charKey = "3fa658aded4f309b9bc17a2318cb1f56" }, { .comment = "ScottTrailer" } }, + { { .charMacAddr = "e405423414f3" }, { .charKey = "0ec3adf7433dd61793ff2f3b8ad32ed8" }, { .comment = "Test" } }, +}; +int knownSolarControllerCount = sizeof(solarControllers) / sizeof(solarControllers[0]); + +BLEScan *pBLEScan; + +#define AES_KEY_BITS 128 +int scanTime = 1; //In seconds + +byte hexCharToByte(char hexChar) { + if (hexChar >= '0' && hexChar <='9') { // 0-9 + hexChar=hexChar - '0'; + } + else if (hexChar >= 'a' && hexChar <= 'f') { // a-f + hexChar=hexChar - 'a' + 10; + } + else if (hexChar >= 'A' && hexChar <= 'F') { // A-F + hexChar=hexChar - 'A' + 10; + } + else { + hexChar=255; + } + + return hexChar; +} + +// Decryption keys and MAC addresses obtained from the VictronConnect app will be +// a string of hex digits like this: +// +// f4116784732a +// dc73cb155351cf950f9f3a958b5cd96f +// +// Split that up and turn it into an array whose equivalent definition would be like this: +// +// byte key[]={ 0xdc, 0x73, 0xcb, ... 0xd9, 0x6f }; +// +void hexCharStrToByteArray(char * hexCharStr, byte * byteArray) { + bool returnVal=false; + + int hexCharStrLength=strlen(hexCharStr); + + // There are simpler ways of doing this without the fancy nibble-munching, + // but I do it this way so I parse things like colon-separated MAC addresses. + // BUT: be aware that this expects digits in pairs and byte values need to be + // zero-filled. i.e., a MAC address like 8:0:2b:xx:xx:xx won't come out the way + // you want it. + int byteArrayIndex=0; + bool oddByte=true; + byte hiNibble; + for (int i=0; i manDataSizeMax) { + Serial.printf(" Note: Truncating malformed %2d byte manufacturer data to max %d byte array size\n",manDataSize,manDataSizeMax); + manDataSize=manDataSizeMax; + } + // Now 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. + manData.copy((char *)manCharBuf, manDataSize + 1); + + // Now let's use 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; + } + + // Get the MAC address of the device we're hearing, and then use that to look up the encryption key + // for the device. + // + // We go through a bit of trouble here to turn the String MAC address that we get from the BLE + // code ("08:00:2b:xx:xx:xx") into a byte array. I'm divided on this... I could have just (and still might!) + // left this as a string and just done a strcmp() match. This would have saved me some coding and execution time + // in exchange for having to format the MAC addresses in my solarControllers list using the embedded colons. + char receivedMacStr[18]; + strcpy(receivedMacStr,advertisedDevice.getAddress().toString().c_str()); + + byte receivedMacByte[6]; + hexCharStrToByteArray(receivedMacStr,receivedMacByte); + + int solarControllerIndex=-1; + for (int trySolarControllerIndex=0; trySolarControllerIndexencryptKeyMatch != solarControllers[solarControllerIndex].byteKey[0]) { + Serial.printf("Encryption key mismatch for %s at MAC %s\n", + solarControllers[solarControllerIndex].cachedDeviceName,receivedMacStr); + return; + } + + // Get the signal strength (RSSI) of the beacon. + int RSSI=advertisedDevice.getRSSI(); + + // If we're showing our data on our integrated graphics hardware, + // then show only the SmartSolar device with the strongest signal. + // I debated on whether to do this with "#if defined..." for conditional compilation + // or I should do this with a boolean "using graphics hardware" variable and a regular + // "if". I decided on #if, but I might change my mind later. + // Get the beacon's RSSI (signal strength). If it's stronger than other beacons we've received, + // then lock on to this SmartSolar and don't display beacons from others anymore. + if (selectedSolarControllerIndex==solarControllerIndex) { + if (RSSI > bestRSSI) { + bestRSSI=RSSI; + } + } else { + if (RSSI > bestRSSI) { + selectedSolarControllerIndex=solarControllerIndex; + Serial.printf("Selected Victon SmartSolar %s at MAC %s as preferred device based on RSSI %d\n", + solarControllers[solarControllerIndex].cachedDeviceName,receivedMacStr,RSSI); + } else { + Serial.printf("Discarding RSSI-based non-selected Victon SmartSolar %s at MAC %s\n", + solarControllers[solarControllerIndex].cachedDeviceName,receivedMacStr); + return; + } + } + + // Now that the packet received has met all the criteria for being displayed, + // let's decrypt and decode the manufacturer data. + + byte inputData[16]; + byte outputData[16]={0}; + victronPanelData * victronData = (victronPanelData *) outputData; + + // The number of encrypted bytes is given by the number of bytes in the manufacturer + // data as a while 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, solarControllers[solarControllerIndex].byteKey, AES_KEY_BITS); + if (status != 0) { + Serial.printf(" Error during esp_aes_setkey operation (%i).\n",status); + esp_aes_free(&ctx); + return; + } + + byte data_counter_lsb=(vicData->nonceDataCounter) & 0xff; + byte 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); + + byte deviceState=victronData->deviceState; // this is really more like "Charger State" + byte 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. + 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. + // Towards the goal of filtering out this noise, I've found that I've rarely (if ever) seen + // corrupted data when the 'unused' bits of the outputCurrent MSB equal 0xfe. We'll use this + // as a litmus test here. + byte unusedBits=victronData->outputCurrentHi & 0xfe; + if (unusedBits != 0xfe) { + return; + } + + // The Victron docs say Device State but it's really a Charger State. + char chargeStateName[6]; + sprintf(chargeStateName,"%4d?",deviceState); + if (deviceState >=0 && deviceState<=7) { + strcpy(chargeStateName,chargeStateNames[deviceState]); + } + + Serial.printf("%-31s Battery: %6.2f Volts %6.2f Amps Solar: %6d Watts Yield: %4.0f Wh Load: %5.1f Amps Charger: %-13s Err: %2d RSSI: %d\n", + solarControllers[solarControllerIndex].cachedDeviceName, + batteryVoltage, batteryCurrent, + inputPower, todayYield, + outputCurrent, chargeStateName, errorCode, RSSI + ); + + char screenDeviceName[14]; // 13 characters plus /0 + strncpy(screenDeviceName,solarControllers[solarControllerIndex].cachedDeviceName,13); + screenDeviceName[13]='\0'; // make sure we have a null byte at the end. + /* + sh3dNbDisplay.line1 = String("Name: ") + String(screenDeviceName); + sh3dNbDisplay.line2 = String("Battery: ") + String(batteryVoltage) + String("V"); + sh3dNbDisplay.line3 = String("Charge: ") + String(inputPower) + String("W ") + String(todayYield) + String("Wh"); + sh3dNb.setMessage(sh3dNb.iso8601()); + sh3dNbDisplay.update(); + */ + + packetReceived=true; + } + } +}; + +void setup() { + Serial.begin(115200); + Serial.println("Basic test"); + + // VICTRON BLUETOOTH + for (int i = 0; i < knownSolarControllerCount; i++) { + hexCharStrToByteArray(solarControllers[i].charMacAddr, solarControllers[i].byteMacAddr); + hexCharStrToByteArray(solarControllers[i].charKey, solarControllers[i].byteKey); + strcpy(solarControllers[i].cachedDeviceName, "(unknown)"); + } + + for (int i = 0; i < knownSolarControllerCount; i++) { + Serial.printf(" %-16s", solarControllers[i].comment); + Serial.printf(" Mac: "); + for (int j = 0; j < 6; j++) { + Serial.printf(" %2.2x", solarControllers[i].byteMacAddr[j]); + } + Serial.printf(" Key: "); + for (int j = 0; j < 16; j++) { + Serial.printf("%2.2x", solarControllers[i].byteKey[j]); + } + } + + BLEDevice::init(""); + pBLEScan = BLEDevice::getScan(); //create new scan + pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); + pBLEScan->setActiveScan(true); //active scan uses more power, but gets results faster + pBLEScan->setInterval(100); + pBLEScan->setWindow(99); // less or equal setInterval value +} + + +void loop() { + Serial.println("tick"); + BLEScanResults foundDevices = pBLEScan->start(scanTime, false); + pBLEScan->clearResults(); // delete results fromBLEScan buffer to release memory + delay(100); +} diff --git a/src/VictronBLE.cpp b/src/VictronBLE.cpp index dcea477..88ca72e 100644 --- a/src/VictronBLE.cpp +++ b/src/VictronBLE.cpp @@ -212,6 +212,7 @@ void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) { if (mfgData.length() < 2) { return; } + debugPrint("Manufacturer Length = " + String(mfgData.length())); // XXX Use struct like code in Sh3dNg @@ -246,7 +247,7 @@ bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len, // Verify minimum size for victronManufacturerData struct if (len < sizeof(victronManufacturerData)) { - debugPrint("Manufacturer data too short: " + String(len) + " bytes"); + debugPrint("Manufacturer data too short: " + String(len) + " bytes, expected: " + String(sizeof(victronManufacturerData))); return false; }