Initial readme up
This commit is contained in:
627
src/VictronBLE.cpp
Normal file
627
src/VictronBLE.cpp
Normal file
@@ -0,0 +1,627 @@
|
||||
/**
|
||||
* VictronBLE - ESP32 library for Victron Energy BLE devices
|
||||
* Implementation file
|
||||
*/
|
||||
|
||||
#include "VictronBLE.h"
|
||||
|
||||
// Constructor
|
||||
VictronBLE::VictronBLE()
|
||||
: pBLEScan(nullptr), callback(nullptr), debugEnabled(false),
|
||||
scanDuration(5), initialized(false) {
|
||||
}
|
||||
|
||||
// Destructor
|
||||
VictronBLE::~VictronBLE() {
|
||||
for (auto& pair : devices) {
|
||||
delete pair.second;
|
||||
}
|
||||
devices.clear();
|
||||
|
||||
if (pBLEScan) {
|
||||
pBLEScan->stop();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize BLE
|
||||
bool VictronBLE::begin(uint32_t scanDuration) {
|
||||
if (initialized) {
|
||||
debugPrint("VictronBLE already initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
this->scanDuration = scanDuration;
|
||||
|
||||
debugPrint("Initializing VictronBLE...");
|
||||
|
||||
BLEDevice::init("VictronBLE");
|
||||
pBLEScan = BLEDevice::getScan();
|
||||
|
||||
if (!pBLEScan) {
|
||||
lastError = "Failed to create BLE scanner";
|
||||
return false;
|
||||
}
|
||||
|
||||
pBLEScan->setAdvertisedDeviceCallbacks(new VictronBLEAdvertisedDeviceCallbacks(this), true);
|
||||
pBLEScan->setActiveScan(false); // Passive scan - lower power
|
||||
pBLEScan->setInterval(100);
|
||||
pBLEScan->setWindow(99);
|
||||
|
||||
initialized = true;
|
||||
debugPrint("VictronBLE initialized successfully");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add a device to monitor
|
||||
bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
|
||||
if (config.macAddress.length() == 0) {
|
||||
lastError = "MAC address cannot be empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config.encryptionKey.length() != 32) {
|
||||
lastError = "Encryption key must be 32 hex characters";
|
||||
return false;
|
||||
}
|
||||
|
||||
String normalizedMAC = normalizeMAC(config.macAddress);
|
||||
|
||||
// Check if device already exists
|
||||
if (devices.find(normalizedMAC) != devices.end()) {
|
||||
debugPrint("Device " + normalizedMAC + " already exists, updating config");
|
||||
delete devices[normalizedMAC];
|
||||
}
|
||||
|
||||
DeviceInfo* info = new DeviceInfo();
|
||||
info->config = config;
|
||||
info->config.macAddress = normalizedMAC;
|
||||
|
||||
// Convert encryption key from hex string to bytes
|
||||
if (!hexStringToBytes(config.encryptionKey, info->encryptionKeyBytes, 16)) {
|
||||
lastError = "Invalid encryption key format";
|
||||
delete info;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create appropriate data structure based on device type
|
||||
info->data = createDeviceData(config.expectedType);
|
||||
if (info->data) {
|
||||
info->data->macAddress = normalizedMAC;
|
||||
info->data->deviceName = config.name;
|
||||
}
|
||||
|
||||
devices[normalizedMAC] = info;
|
||||
|
||||
debugPrint("Added device: " + config.name + " (" + normalizedMAC + ")");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VictronBLE::addDevice(String name, String macAddress, String encryptionKey,
|
||||
VictronDeviceType expectedType) {
|
||||
VictronDeviceConfig config(name, macAddress, encryptionKey, expectedType);
|
||||
return addDevice(config);
|
||||
}
|
||||
|
||||
// Remove a device
|
||||
void VictronBLE::removeDevice(String macAddress) {
|
||||
String normalizedMAC = normalizeMAC(macAddress);
|
||||
|
||||
auto it = devices.find(normalizedMAC);
|
||||
if (it != devices.end()) {
|
||||
delete it->second;
|
||||
devices.erase(it);
|
||||
debugPrint("Removed device: " + normalizedMAC);
|
||||
}
|
||||
}
|
||||
|
||||
// Main loop function
|
||||
void VictronBLE::loop() {
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start a scan
|
||||
BLEScanResults scanResults = pBLEScan->start(scanDuration, false);
|
||||
pBLEScan->clearResults();
|
||||
}
|
||||
|
||||
// BLE callback implementation
|
||||
void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertisedDevice) {
|
||||
if (victronBLE) {
|
||||
victronBLE->processDevice(advertisedDevice);
|
||||
}
|
||||
}
|
||||
|
||||
// Process advertised device
|
||||
void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) {
|
||||
String mac = macAddressToString(advertisedDevice.getAddress());
|
||||
String normalizedMAC = normalizeMAC(mac);
|
||||
|
||||
// Check if this is one of our configured devices
|
||||
auto it = devices.find(normalizedMAC);
|
||||
if (it == devices.end()) {
|
||||
return; // Not a device we're monitoring
|
||||
}
|
||||
|
||||
DeviceInfo* deviceInfo = it->second;
|
||||
|
||||
// Check if device has manufacturer data
|
||||
if (!advertisedDevice.haveManufacturerData()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string mfgData = advertisedDevice.getManufacturerData();
|
||||
if (mfgData.length() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's Victron (manufacturer ID 0x02E1)
|
||||
uint16_t mfgId = (uint8_t)mfgData[1] << 8 | (uint8_t)mfgData[0];
|
||||
if (mfgId != VICTRON_MANUFACTURER_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint("Processing data from: " + deviceInfo->config.name);
|
||||
|
||||
// Parse the advertisement
|
||||
if (parseAdvertisement((const uint8_t*)mfgData.data(), mfgData.length(), normalizedMAC)) {
|
||||
// Update RSSI
|
||||
if (deviceInfo->data) {
|
||||
deviceInfo->data->rssi = advertisedDevice.getRSSI();
|
||||
deviceInfo->data->lastUpdate = millis();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse advertisement data
|
||||
bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
|
||||
const String& macAddress) {
|
||||
auto it = devices.find(macAddress);
|
||||
if (it == devices.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DeviceInfo* deviceInfo = it->second;
|
||||
|
||||
if (len < 6) {
|
||||
debugPrint("Manufacturer data too short");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Structure: [MfgID(2)] [DeviceType(1)] [IV(2)] [EncryptedData(n)]
|
||||
uint8_t deviceType = manufacturerData[2];
|
||||
|
||||
// Extract IV (initialization vector) - bytes 3-4, little-endian
|
||||
uint8_t iv[16] = {0};
|
||||
iv[0] = manufacturerData[3];
|
||||
iv[1] = manufacturerData[4];
|
||||
// Rest of IV is zero-padded
|
||||
|
||||
// Encrypted data starts at byte 5
|
||||
const uint8_t* encryptedData = manufacturerData + 5;
|
||||
size_t encryptedLen = len - 5;
|
||||
|
||||
if (debugEnabled) {
|
||||
debugPrintHex("Encrypted data", encryptedData, encryptedLen);
|
||||
debugPrintHex("IV", iv, 16);
|
||||
}
|
||||
|
||||
// Decrypt the data
|
||||
uint8_t decrypted[32]; // Max expected size
|
||||
if (!decryptAdvertisement(encryptedData, encryptedLen,
|
||||
deviceInfo->encryptionKeyBytes, iv, decrypted)) {
|
||||
lastError = "Decryption failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (debugEnabled) {
|
||||
debugPrintHex("Decrypted data", decrypted, encryptedLen);
|
||||
}
|
||||
|
||||
// Parse based on device type
|
||||
bool parseOk = false;
|
||||
|
||||
switch (deviceType) {
|
||||
case DEVICE_TYPE_SOLAR_CHARGER:
|
||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
|
||||
parseOk = parseSolarCharger(decrypted, encryptedLen,
|
||||
*(SolarChargerData*)deviceInfo->data);
|
||||
}
|
||||
break;
|
||||
|
||||
case DEVICE_TYPE_BATTERY_MONITOR:
|
||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) {
|
||||
parseOk = parseBatteryMonitor(decrypted, encryptedLen,
|
||||
*(BatteryMonitorData*)deviceInfo->data);
|
||||
}
|
||||
break;
|
||||
|
||||
case DEVICE_TYPE_INVERTER:
|
||||
case DEVICE_TYPE_INVERTER_RS:
|
||||
case DEVICE_TYPE_MULTI_RS:
|
||||
case DEVICE_TYPE_VE_BUS:
|
||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_INVERTER) {
|
||||
parseOk = parseInverter(decrypted, encryptedLen,
|
||||
*(InverterData*)deviceInfo->data);
|
||||
}
|
||||
break;
|
||||
|
||||
case DEVICE_TYPE_DCDC_CONVERTER:
|
||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) {
|
||||
parseOk = parseDCDCConverter(decrypted, encryptedLen,
|
||||
*(DCDCConverterData*)deviceInfo->data);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
debugPrint("Unknown device type: 0x" + String(deviceType, HEX));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parseOk && deviceInfo->data) {
|
||||
deviceInfo->data->dataValid = true;
|
||||
|
||||
// Call appropriate callback
|
||||
if (callback) {
|
||||
switch (deviceType) {
|
||||
case DEVICE_TYPE_SOLAR_CHARGER:
|
||||
callback->onSolarChargerData(*(SolarChargerData*)deviceInfo->data);
|
||||
break;
|
||||
case DEVICE_TYPE_BATTERY_MONITOR:
|
||||
callback->onBatteryMonitorData(*(BatteryMonitorData*)deviceInfo->data);
|
||||
break;
|
||||
case DEVICE_TYPE_INVERTER:
|
||||
case DEVICE_TYPE_INVERTER_RS:
|
||||
case DEVICE_TYPE_MULTI_RS:
|
||||
case DEVICE_TYPE_VE_BUS:
|
||||
callback->onInverterData(*(InverterData*)deviceInfo->data);
|
||||
break;
|
||||
case DEVICE_TYPE_DCDC_CONVERTER:
|
||||
callback->onDCDCConverterData(*(DCDCConverterData*)deviceInfo->data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parseOk;
|
||||
}
|
||||
|
||||
// Decrypt advertisement using AES-128-CTR
|
||||
bool VictronBLE::decryptAdvertisement(const uint8_t* encrypted, size_t encLen,
|
||||
const uint8_t* key, const uint8_t* iv,
|
||||
uint8_t* decrypted) {
|
||||
mbedtls_aes_context aes;
|
||||
mbedtls_aes_init(&aes);
|
||||
|
||||
// Set encryption key
|
||||
int ret = mbedtls_aes_setkey_enc(&aes, key, 128);
|
||||
if (ret != 0) {
|
||||
mbedtls_aes_free(&aes);
|
||||
return false;
|
||||
}
|
||||
|
||||
// AES-CTR decryption
|
||||
size_t nc_off = 0;
|
||||
uint8_t nonce_counter[16];
|
||||
uint8_t stream_block[16];
|
||||
|
||||
memcpy(nonce_counter, iv, 16);
|
||||
memset(stream_block, 0, 16);
|
||||
|
||||
ret = mbedtls_aes_crypt_ctr(&aes, encLen, &nc_off, nonce_counter,
|
||||
stream_block, encrypted, decrypted);
|
||||
|
||||
mbedtls_aes_free(&aes);
|
||||
|
||||
return (ret == 0);
|
||||
}
|
||||
|
||||
// Parse Solar Charger data
|
||||
bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarChargerData& result) {
|
||||
if (len < 12) {
|
||||
debugPrint("Solar charger data too short");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Byte 0: Charge state
|
||||
result.chargeState = (SolarChargerState)data[0];
|
||||
|
||||
// Bytes 1-2: Battery voltage (10 mV units)
|
||||
uint16_t vBat = data[1] | (data[2] << 8);
|
||||
result.batteryVoltage = vBat * 0.01f;
|
||||
|
||||
// Bytes 3-4: Battery current (10 mA units, signed)
|
||||
int16_t iBat = (int16_t)(data[3] | (data[4] << 8));
|
||||
result.batteryCurrent = iBat * 0.01f;
|
||||
|
||||
// Bytes 5-6: Yield today (10 Wh units)
|
||||
uint16_t yield = data[5] | (data[6] << 8);
|
||||
result.yieldToday = yield * 10;
|
||||
|
||||
// Bytes 7-8: PV power (1 W units)
|
||||
uint16_t pvPower = data[7] | (data[8] << 8);
|
||||
result.panelPower = pvPower;
|
||||
|
||||
// Bytes 9-10: Load current (10 mA units)
|
||||
uint16_t iLoad = data[9] | (data[10] << 8);
|
||||
if (iLoad != 0xFFFF) { // 0xFFFF means no load output
|
||||
result.loadCurrent = iLoad * 0.01f;
|
||||
}
|
||||
|
||||
// Calculate PV voltage from power and current (if current > 0)
|
||||
if (result.batteryCurrent > 0.1f) {
|
||||
result.panelVoltage = result.panelPower / result.batteryCurrent;
|
||||
}
|
||||
|
||||
debugPrint("Solar Charger: " + String(result.batteryVoltage, 2) + "V, " +
|
||||
String(result.batteryCurrent, 2) + "A, " +
|
||||
String(result.panelPower) + "W, State: " + String(result.chargeState));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse Battery Monitor data
|
||||
bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result) {
|
||||
if (len < 15) {
|
||||
debugPrint("Battery monitor data too short");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bytes 0-1: Remaining time (1 minute units)
|
||||
uint16_t timeRemaining = data[0] | (data[1] << 8);
|
||||
result.remainingMinutes = timeRemaining;
|
||||
|
||||
// Bytes 2-3: Battery voltage (10 mV units)
|
||||
uint16_t vBat = data[2] | (data[3] << 8);
|
||||
result.voltage = vBat * 0.01f;
|
||||
|
||||
// Byte 4: Alarms
|
||||
uint8_t alarms = data[4];
|
||||
result.alarmLowVoltage = (alarms & 0x01) != 0;
|
||||
result.alarmHighVoltage = (alarms & 0x02) != 0;
|
||||
result.alarmLowSOC = (alarms & 0x04) != 0;
|
||||
result.alarmLowTemperature = (alarms & 0x10) != 0;
|
||||
result.alarmHighTemperature = (alarms & 0x20) != 0;
|
||||
|
||||
// Bytes 5-6: Aux voltage/temperature (10 mV or 0.01K units)
|
||||
uint16_t aux = data[5] | (data[6] << 8);
|
||||
if (aux < 3000) { // If < 30V, it's voltage
|
||||
result.auxVoltage = aux * 0.01f;
|
||||
result.temperature = 0;
|
||||
} else { // Otherwise temperature in 0.01 Kelvin
|
||||
result.temperature = (aux * 0.01f) - 273.15f;
|
||||
result.auxVoltage = 0;
|
||||
}
|
||||
|
||||
// Bytes 7-9: Battery current (22-bit signed, 1 mA units)
|
||||
int32_t current = data[7] | (data[8] << 8) | ((data[9] & 0x3F) << 16);
|
||||
if (current & 0x200000) { // Sign extend if negative
|
||||
current |= 0xFFC00000;
|
||||
}
|
||||
result.current = current * 0.001f;
|
||||
|
||||
// Bytes 9-11: Consumed Ah (18-bit signed, 10 mAh units)
|
||||
int32_t consumedAh = ((data[9] & 0xC0) >> 6) | (data[10] << 2) | ((data[11] & 0xFF) << 10);
|
||||
if (consumedAh & 0x20000) { // Sign extend
|
||||
consumedAh |= 0xFFFC0000;
|
||||
}
|
||||
result.consumedAh = consumedAh * 0.01f;
|
||||
|
||||
// Bytes 12-13: SOC (10 = 1.0%)
|
||||
uint16_t soc = data[12] | ((data[13] & 0x03) << 8);
|
||||
result.soc = soc * 0.1f;
|
||||
|
||||
debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " +
|
||||
String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse Inverter data
|
||||
bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& result) {
|
||||
if (len < 10) {
|
||||
debugPrint("Inverter data too short");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Byte 0: Device state
|
||||
result.state = data[0];
|
||||
|
||||
// Bytes 1-2: Battery voltage (10 mV units)
|
||||
uint16_t vBat = data[1] | (data[2] << 8);
|
||||
result.batteryVoltage = vBat * 0.01f;
|
||||
|
||||
// Bytes 3-4: Battery current (10 mA units, signed)
|
||||
int16_t iBat = (int16_t)(data[3] | (data[4] << 8));
|
||||
result.batteryCurrent = iBat * 0.01f;
|
||||
|
||||
// Bytes 5-7: AC Power (1 W units, signed 24-bit)
|
||||
int32_t acPower = data[5] | (data[6] << 8) | (data[7] << 16);
|
||||
if (acPower & 0x800000) { // Sign extend
|
||||
acPower |= 0xFF000000;
|
||||
}
|
||||
result.acPower = acPower;
|
||||
|
||||
// Byte 8: Alarms
|
||||
uint8_t alarms = data[8];
|
||||
result.alarmLowVoltage = (alarms & 0x01) != 0;
|
||||
result.alarmHighVoltage = (alarms & 0x02) != 0;
|
||||
result.alarmHighTemperature = (alarms & 0x04) != 0;
|
||||
result.alarmOverload = (alarms & 0x08) != 0;
|
||||
|
||||
debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " +
|
||||
String(result.acPower) + "W, State: " + String(result.state));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse DC-DC Converter data
|
||||
bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result) {
|
||||
if (len < 10) {
|
||||
debugPrint("DC-DC converter data too short");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Byte 0: Charge state
|
||||
result.chargeState = data[0];
|
||||
|
||||
// Bytes 1-2: Input voltage (10 mV units)
|
||||
uint16_t vIn = data[1] | (data[2] << 8);
|
||||
result.inputVoltage = vIn * 0.01f;
|
||||
|
||||
// Bytes 3-4: Output voltage (10 mV units)
|
||||
uint16_t vOut = data[3] | (data[4] << 8);
|
||||
result.outputVoltage = vOut * 0.01f;
|
||||
|
||||
// Bytes 5-6: Output current (10 mA units)
|
||||
uint16_t iOut = data[5] | (data[6] << 8);
|
||||
result.outputCurrent = iOut * 0.01f;
|
||||
|
||||
// Byte 7: Error code
|
||||
result.errorCode = data[7];
|
||||
|
||||
debugPrint("DC-DC Converter: In=" + String(result.inputVoltage, 2) + "V, Out=" +
|
||||
String(result.outputVoltage, 2) + "V, " + String(result.outputCurrent, 2) + "A");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get data methods
|
||||
bool VictronBLE::getSolarChargerData(String macAddress, SolarChargerData& data) {
|
||||
String normalizedMAC = normalizeMAC(macAddress);
|
||||
auto it = devices.find(normalizedMAC);
|
||||
|
||||
if (it != devices.end() && it->second->data &&
|
||||
it->second->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
|
||||
data = *(SolarChargerData*)it->second->data;
|
||||
return data.dataValid;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool VictronBLE::getBatteryMonitorData(String macAddress, BatteryMonitorData& data) {
|
||||
String normalizedMAC = normalizeMAC(macAddress);
|
||||
auto it = devices.find(normalizedMAC);
|
||||
|
||||
if (it != devices.end() && it->second->data &&
|
||||
it->second->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) {
|
||||
data = *(BatteryMonitorData*)it->second->data;
|
||||
return data.dataValid;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool VictronBLE::getInverterData(String macAddress, InverterData& data) {
|
||||
String normalizedMAC = normalizeMAC(macAddress);
|
||||
auto it = devices.find(normalizedMAC);
|
||||
|
||||
if (it != devices.end() && it->second->data &&
|
||||
it->second->data->deviceType == DEVICE_TYPE_INVERTER) {
|
||||
data = *(InverterData*)it->second->data;
|
||||
return data.dataValid;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool VictronBLE::getDCDCConverterData(String macAddress, DCDCConverterData& data) {
|
||||
String normalizedMAC = normalizeMAC(macAddress);
|
||||
auto it = devices.find(normalizedMAC);
|
||||
|
||||
if (it != devices.end() && it->second->data &&
|
||||
it->second->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) {
|
||||
data = *(DCDCConverterData*)it->second->data;
|
||||
return data.dataValid;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get devices by type
|
||||
std::vector<String> VictronBLE::getDevicesByType(VictronDeviceType type) {
|
||||
std::vector<String> result;
|
||||
|
||||
for (const auto& pair : devices) {
|
||||
if (pair.second->data && pair.second->data->deviceType == type) {
|
||||
result.push_back(pair.first);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Helper: Create device data structure
|
||||
VictronDeviceData* VictronBLE::createDeviceData(VictronDeviceType type) {
|
||||
switch (type) {
|
||||
case DEVICE_TYPE_SOLAR_CHARGER:
|
||||
return new SolarChargerData();
|
||||
case DEVICE_TYPE_BATTERY_MONITOR:
|
||||
return new BatteryMonitorData();
|
||||
case DEVICE_TYPE_INVERTER:
|
||||
case DEVICE_TYPE_INVERTER_RS:
|
||||
case DEVICE_TYPE_MULTI_RS:
|
||||
case DEVICE_TYPE_VE_BUS:
|
||||
return new InverterData();
|
||||
case DEVICE_TYPE_DCDC_CONVERTER:
|
||||
return new DCDCConverterData();
|
||||
default:
|
||||
return new VictronDeviceData();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Convert hex string to bytes
|
||||
bool VictronBLE::hexStringToBytes(const String& hex, uint8_t* bytes, size_t len) {
|
||||
if (hex.length() != len * 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
String byteStr = hex.substring(i * 2, i * 2 + 2);
|
||||
char* endPtr;
|
||||
bytes[i] = strtoul(byteStr.c_str(), &endPtr, 16);
|
||||
if (*endPtr != '\0') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper: MAC address to string
|
||||
String VictronBLE::macAddressToString(BLEAddress address) {
|
||||
char macStr[18];
|
||||
snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
|
||||
address.getNative()[0], address.getNative()[1],
|
||||
address.getNative()[2], address.getNative()[3],
|
||||
address.getNative()[4], address.getNative()[5]);
|
||||
return String(macStr);
|
||||
}
|
||||
|
||||
// Helper: Normalize MAC address format
|
||||
String VictronBLE::normalizeMAC(String mac) {
|
||||
String normalized = mac;
|
||||
normalized.toLowerCase();
|
||||
normalized.replace("-", ":");
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// Debug helpers
|
||||
void VictronBLE::debugPrint(const String& message) {
|
||||
if (debugEnabled) {
|
||||
Serial.println("[VictronBLE] " + message);
|
||||
}
|
||||
}
|
||||
|
||||
void VictronBLE::debugPrintHex(const char* label, const uint8_t* data, size_t len) {
|
||||
if (!debugEnabled) return;
|
||||
|
||||
Serial.print("[VictronBLE] ");
|
||||
Serial.print(label);
|
||||
Serial.print(": ");
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (data[i] < 0x10) Serial.print("0");
|
||||
Serial.print(data[i], HEX);
|
||||
Serial.print(" ");
|
||||
}
|
||||
Serial.println();
|
||||
}
|
||||
264
src/VictronBLE.h
Normal file
264
src/VictronBLE.h
Normal file
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* VictronBLE - ESP32 library for Victron Energy BLE devices
|
||||
*
|
||||
* Based on Victron's official BLE Advertising protocol documentation
|
||||
* Inspired by hoberman's examples and keshavdv's Python library
|
||||
*
|
||||
* Copyright (c) 2024
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
#ifndef VICTRON_BLE_H
|
||||
#define VICTRON_BLE_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <BLEDevice.h>
|
||||
#include <BLEAdvertisedDevice.h>
|
||||
#include <BLEScan.h>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include "mbedtls/aes.h"
|
||||
|
||||
// Victron manufacturer ID
|
||||
#define VICTRON_MANUFACTURER_ID 0x02E1
|
||||
|
||||
// Device type IDs from Victron protocol
|
||||
enum VictronDeviceType {
|
||||
DEVICE_TYPE_UNKNOWN = 0x00,
|
||||
DEVICE_TYPE_SOLAR_CHARGER = 0x01,
|
||||
DEVICE_TYPE_BATTERY_MONITOR = 0x02,
|
||||
DEVICE_TYPE_INVERTER = 0x03,
|
||||
DEVICE_TYPE_DCDC_CONVERTER = 0x04,
|
||||
DEVICE_TYPE_SMART_LITHIUM = 0x05,
|
||||
DEVICE_TYPE_INVERTER_RS = 0x06,
|
||||
DEVICE_TYPE_SMART_BATTERY_PROTECT = 0x07,
|
||||
DEVICE_TYPE_LYNX_SMART_BMS = 0x08,
|
||||
DEVICE_TYPE_MULTI_RS = 0x09,
|
||||
DEVICE_TYPE_VE_BUS = 0x0A,
|
||||
DEVICE_TYPE_DC_ENERGY_METER = 0x0B
|
||||
};
|
||||
|
||||
// Device state for Solar Charger
|
||||
enum SolarChargerState {
|
||||
CHARGER_OFF = 0,
|
||||
CHARGER_LOW_POWER = 1,
|
||||
CHARGER_FAULT = 2,
|
||||
CHARGER_BULK = 3,
|
||||
CHARGER_ABSORPTION = 4,
|
||||
CHARGER_FLOAT = 5,
|
||||
CHARGER_STORAGE = 6,
|
||||
CHARGER_EQUALIZE = 7,
|
||||
CHARGER_INVERTING = 9,
|
||||
CHARGER_POWER_SUPPLY = 11,
|
||||
CHARGER_EXTERNAL_CONTROL = 252
|
||||
};
|
||||
|
||||
// Base structure for all device data
|
||||
struct VictronDeviceData {
|
||||
String deviceName;
|
||||
String macAddress;
|
||||
VictronDeviceType deviceType;
|
||||
int8_t rssi;
|
||||
uint32_t lastUpdate;
|
||||
bool dataValid;
|
||||
|
||||
VictronDeviceData() : deviceType(DEVICE_TYPE_UNKNOWN), rssi(-100),
|
||||
lastUpdate(0), dataValid(false) {}
|
||||
};
|
||||
|
||||
// Solar Charger specific data
|
||||
struct SolarChargerData : public VictronDeviceData {
|
||||
SolarChargerState chargeState;
|
||||
float batteryVoltage; // V
|
||||
float batteryCurrent; // A
|
||||
float panelVoltage; // V (PV voltage)
|
||||
float panelPower; // W
|
||||
uint16_t yieldToday; // Wh
|
||||
float loadCurrent; // A
|
||||
|
||||
SolarChargerData() : chargeState(CHARGER_OFF), batteryVoltage(0),
|
||||
batteryCurrent(0), panelVoltage(0), panelPower(0),
|
||||
yieldToday(0), loadCurrent(0) {
|
||||
deviceType = DEVICE_TYPE_SOLAR_CHARGER;
|
||||
}
|
||||
};
|
||||
|
||||
// Battery Monitor/SmartShunt specific data
|
||||
struct BatteryMonitorData : public VictronDeviceData {
|
||||
float voltage; // V
|
||||
float current; // A (positive = charging, negative = discharging)
|
||||
float temperature; // °C
|
||||
float auxVoltage; // V (starter battery or midpoint)
|
||||
uint16_t remainingMinutes; // Minutes
|
||||
float consumedAh; // Ah
|
||||
float soc; // State of Charge %
|
||||
bool alarmLowVoltage;
|
||||
bool alarmHighVoltage;
|
||||
bool alarmLowSOC;
|
||||
bool alarmLowTemperature;
|
||||
bool alarmHighTemperature;
|
||||
|
||||
BatteryMonitorData() : voltage(0), current(0), temperature(0),
|
||||
auxVoltage(0), remainingMinutes(0), consumedAh(0),
|
||||
soc(0), alarmLowVoltage(false), alarmHighVoltage(false),
|
||||
alarmLowSOC(false), alarmLowTemperature(false),
|
||||
alarmHighTemperature(false) {
|
||||
deviceType = DEVICE_TYPE_BATTERY_MONITOR;
|
||||
}
|
||||
};
|
||||
|
||||
// Inverter specific data
|
||||
struct InverterData : public VictronDeviceData {
|
||||
float batteryVoltage; // V
|
||||
float batteryCurrent; // A
|
||||
float acPower; // W
|
||||
uint8_t state; // Inverter state
|
||||
bool alarmHighVoltage;
|
||||
bool alarmLowVoltage;
|
||||
bool alarmHighTemperature;
|
||||
bool alarmOverload;
|
||||
|
||||
InverterData() : batteryVoltage(0), batteryCurrent(0), acPower(0),
|
||||
state(0), alarmHighVoltage(false), alarmLowVoltage(false),
|
||||
alarmHighTemperature(false), alarmOverload(false) {
|
||||
deviceType = DEVICE_TYPE_INVERTER;
|
||||
}
|
||||
};
|
||||
|
||||
// DC-DC Converter specific data
|
||||
struct DCDCConverterData : public VictronDeviceData {
|
||||
float inputVoltage; // V
|
||||
float outputVoltage; // V
|
||||
float outputCurrent; // A
|
||||
uint8_t chargeState;
|
||||
uint8_t errorCode;
|
||||
|
||||
DCDCConverterData() : inputVoltage(0), outputVoltage(0), outputCurrent(0),
|
||||
chargeState(0), errorCode(0) {
|
||||
deviceType = DEVICE_TYPE_DCDC_CONVERTER;
|
||||
}
|
||||
};
|
||||
|
||||
// Forward declaration
|
||||
class VictronBLE;
|
||||
|
||||
// Callback interface for device data updates
|
||||
class VictronDeviceCallback {
|
||||
public:
|
||||
virtual ~VictronDeviceCallback() {}
|
||||
virtual void onSolarChargerData(const SolarChargerData& data) {}
|
||||
virtual void onBatteryMonitorData(const BatteryMonitorData& data) {}
|
||||
virtual void onInverterData(const InverterData& data) {}
|
||||
virtual void onDCDCConverterData(const DCDCConverterData& data) {}
|
||||
};
|
||||
|
||||
// Device configuration structure
|
||||
struct VictronDeviceConfig {
|
||||
String name;
|
||||
String macAddress;
|
||||
String encryptionKey; // 32 character hex string
|
||||
VictronDeviceType expectedType;
|
||||
|
||||
VictronDeviceConfig() : expectedType(DEVICE_TYPE_UNKNOWN) {}
|
||||
VictronDeviceConfig(String n, String mac, String key, VictronDeviceType type = DEVICE_TYPE_UNKNOWN)
|
||||
: name(n), macAddress(mac), encryptionKey(key), expectedType(type) {}
|
||||
};
|
||||
|
||||
// Main VictronBLE class
|
||||
class VictronBLE {
|
||||
public:
|
||||
VictronBLE();
|
||||
~VictronBLE();
|
||||
|
||||
// Initialize BLE and start scanning
|
||||
bool begin(uint32_t scanDuration = 5);
|
||||
|
||||
// Add a device to monitor
|
||||
bool addDevice(const VictronDeviceConfig& config);
|
||||
bool addDevice(String name, String macAddress, String encryptionKey,
|
||||
VictronDeviceType expectedType = DEVICE_TYPE_UNKNOWN);
|
||||
|
||||
// Remove a device
|
||||
void removeDevice(String macAddress);
|
||||
|
||||
// Get device count
|
||||
size_t getDeviceCount() const { return devices.size(); }
|
||||
|
||||
// Set callback for data updates
|
||||
void setCallback(VictronDeviceCallback* cb) { callback = cb; }
|
||||
|
||||
// Process scanning (call in loop())
|
||||
void loop();
|
||||
|
||||
// Get latest data for a device
|
||||
bool getSolarChargerData(String macAddress, SolarChargerData& data);
|
||||
bool getBatteryMonitorData(String macAddress, BatteryMonitorData& data);
|
||||
bool getInverterData(String macAddress, InverterData& data);
|
||||
bool getDCDCConverterData(String macAddress, DCDCConverterData& data);
|
||||
|
||||
// Get all devices of a specific type
|
||||
std::vector<String> getDevicesByType(VictronDeviceType type);
|
||||
|
||||
// Enable/disable debug output
|
||||
void setDebug(bool enable) { debugEnabled = enable; }
|
||||
|
||||
// Get last error message
|
||||
String getLastError() const { return lastError; }
|
||||
|
||||
private:
|
||||
friend class VictronBLEAdvertisedDeviceCallbacks;
|
||||
|
||||
struct DeviceInfo {
|
||||
VictronDeviceConfig config;
|
||||
VictronDeviceData* data;
|
||||
uint8_t encryptionKeyBytes[16];
|
||||
|
||||
DeviceInfo() : data(nullptr) {
|
||||
memset(encryptionKeyBytes, 0, 16);
|
||||
}
|
||||
~DeviceInfo() {
|
||||
if (data) delete data;
|
||||
}
|
||||
};
|
||||
|
||||
std::map<String, DeviceInfo*> devices;
|
||||
BLEScan* pBLEScan;
|
||||
VictronDeviceCallback* callback;
|
||||
bool debugEnabled;
|
||||
String lastError;
|
||||
uint32_t scanDuration;
|
||||
bool initialized;
|
||||
|
||||
// Internal methods
|
||||
bool hexStringToBytes(const String& hex, uint8_t* bytes, size_t len);
|
||||
bool decryptAdvertisement(const uint8_t* encrypted, size_t encLen,
|
||||
const uint8_t* key, const uint8_t* iv,
|
||||
uint8_t* decrypted);
|
||||
bool parseAdvertisement(const uint8_t* manufacturerData, size_t len,
|
||||
const String& macAddress);
|
||||
void processDevice(BLEAdvertisedDevice advertisedDevice);
|
||||
|
||||
VictronDeviceData* createDeviceData(VictronDeviceType type);
|
||||
bool parseSolarCharger(const uint8_t* data, size_t len, SolarChargerData& result);
|
||||
bool parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result);
|
||||
bool parseInverter(const uint8_t* data, size_t len, InverterData& result);
|
||||
bool parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result);
|
||||
|
||||
void debugPrint(const String& message);
|
||||
void debugPrintHex(const char* label, const uint8_t* data, size_t len);
|
||||
|
||||
String macAddressToString(BLEAddress address);
|
||||
String normalizeMAC(String mac);
|
||||
};
|
||||
|
||||
// BLE scan callback class
|
||||
class VictronBLEAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
|
||||
public:
|
||||
VictronBLEAdvertisedDeviceCallbacks(VictronBLE* parent) : victronBLE(parent) {}
|
||||
void onResult(BLEAdvertisedDevice advertisedDevice) override;
|
||||
|
||||
private:
|
||||
VictronBLE* victronBLE;
|
||||
};
|
||||
|
||||
#endif // VICTRON_BLE_H
|
||||
Reference in New Issue
Block a user