TODO and m5stick and debug

This commit is contained in:
2025-12-18 20:43:10 +11:00
parent e827dea4e5
commit 97a71ce34c
4 changed files with 214 additions and 126 deletions

View File

@@ -1,7 +1,7 @@
/**
* VictronBLE - ESP32 library for Victron Energy BLE devices
* Implementation file
*
*
* Copyright (c) 2025 Scott Penrose
* License: MIT
*/
@@ -9,8 +9,8 @@
#include "VictronBLE.h"
// Constructor
VictronBLE::VictronBLE()
: pBLEScan(nullptr), callback(nullptr), debugEnabled(false),
VictronBLE::VictronBLE()
: pBLEScan(nullptr), callback(nullptr), debugEnabled(false),
scanDuration(5), initialized(false) {
}
@@ -20,7 +20,7 @@ VictronBLE::~VictronBLE() {
delete pair.second;
}
devices.clear();
if (pBLEScan) {
pBLEScan->stop();
}
@@ -32,27 +32,27 @@ bool VictronBLE::begin(uint32_t scanDuration) {
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;
}
@@ -62,42 +62,42 @@ bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
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;
}
@@ -110,7 +110,7 @@ bool VictronBLE::addDevice(String name, String macAddress, String encryptionKey,
// Remove a device
void VictronBLE::removeDevice(String macAddress) {
String normalizedMAC = normalizeMAC(macAddress);
auto it = devices.find(normalizedMAC);
if (it != devices.end()) {
delete it->second;
@@ -124,7 +124,7 @@ void VictronBLE::loop() {
if (!initialized) {
return;
}
// Start a scan
BLEScanResults scanResults = pBLEScan->start(scanDuration, false);
pBLEScan->clearResults();
@@ -133,6 +133,30 @@ void VictronBLE::loop() {
// BLE callback implementation
void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertisedDevice) {
if (victronBLE) {
// Debug: Log all discovered BLE devices
if (victronBLE->debugEnabled) {
String mac = victronBLE->macAddressToString(advertisedDevice.getAddress());
String debugMsg = "BLE Device: " + mac;
debugMsg += ", RSSI: " + String(advertisedDevice.getRSSI()) + " dBm";
if (advertisedDevice.haveName()) {
debugMsg += ", Name: " + String(advertisedDevice.getName().c_str());
}
if (advertisedDevice.haveManufacturerData()) {
std::string mfgData = advertisedDevice.getManufacturerData();
if (mfgData.length() >= 2) {
uint16_t mfgId = (uint8_t)mfgData[1] << 8 | (uint8_t)mfgData[0];
debugMsg += ", Mfg ID: 0x" + String(mfgId, HEX);
if (mfgId == VICTRON_MANUFACTURER_ID) {
debugMsg += " (Victron)";
}
}
}
victronBLE->debugPrint(debugMsg);
}
victronBLE->processDevice(advertisedDevice);
}
}
@@ -141,33 +165,62 @@ void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertise
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()) {
// XXX Check if the device is a Victron device
// This needs lots of improvemet and only do in debug
if (advertisedDevice.haveManufacturerData()) {
std::string mfgData = advertisedDevice.getManufacturerData();
if (mfgData.length() >= 2) {
uint16_t mfgId = (uint8_t)mfgData[1] << 8 | (uint8_t)mfgData[0];
if (mfgId == VICTRON_MANUFACTURER_ID) {
debugPrint("Found unmonitored Victron Device: " + mac);
// DeviceInfo* deviceInfo = new DeviceInfo(mac, advertisedDevice.getName());
// devices.insert({normalizedMAC, deviceInfo});
// XXX What type of Victron device is it?
// Check if it's a Victron Energy device
/*
if (advertisedDevice.haveServiceData()) {
std::string serviceData = advertisedDevice.getServiceData();
if (serviceData.length() >= 2) {
uint16_t serviceId = (uint8_t)serviceData[1] << 8 | (uint8_t)serviceData[0];
if (serviceId == VICTRON_ENERGY_SERVICE_ID) {
debugPrint("Found Victron Energy Device: " + mac);
}
}
}
*/
}
}
}
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
@@ -185,62 +238,62 @@ bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
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,
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,
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:
@@ -250,22 +303,22 @@ bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
*(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) {
@@ -287,7 +340,7 @@ bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
}
}
}
return parseOk;
}
@@ -297,27 +350,27 @@ bool VictronBLE::decryptAdvertisement(const uint8_t* encrypted, size_t encLen,
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);
}
@@ -327,59 +380,59 @@ bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarCharger
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
// 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;
@@ -387,7 +440,7 @@ bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMon
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
@@ -397,28 +450,28 @@ bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMon
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;
}
@@ -428,35 +481,35 @@ bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& re
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;
}
@@ -466,28 +519,28 @@ bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConvert
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;
}
@@ -495,8 +548,8 @@ bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConvert
bool VictronBLE::getSolarChargerData(String macAddress, SolarChargerData& data) {
String normalizedMAC = normalizeMAC(macAddress);
auto it = devices.find(normalizedMAC);
if (it != devices.end() && it->second->data &&
if (it != devices.end() && it->second->data &&
it->second->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
data = *(SolarChargerData*)it->second->data;
return data.dataValid;
@@ -507,7 +560,7 @@ bool VictronBLE::getSolarChargerData(String macAddress, SolarChargerData& data)
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;
@@ -519,7 +572,7 @@ bool VictronBLE::getBatteryMonitorData(String macAddress, BatteryMonitorData& da
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;
@@ -531,7 +584,7 @@ bool VictronBLE::getInverterData(String macAddress, InverterData& data) {
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;
@@ -543,13 +596,13 @@ bool VictronBLE::getDCDCConverterData(String macAddress, DCDCConverterData& data
// 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;
}
@@ -577,7 +630,7 @@ 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;
@@ -586,7 +639,7 @@ bool VictronBLE::hexStringToBytes(const String& hex, uint8_t* bytes, size_t len)
return false;
}
}
return true;
}
@@ -604,7 +657,9 @@ String VictronBLE::macAddressToString(BLEAddress address) {
String VictronBLE::normalizeMAC(String mac) {
String normalized = mac;
normalized.toLowerCase();
normalized.replace("-", ":");
// XXX - is this right, was - to : but not consistent location of pairs or not
normalized.replace("-", "");
normalized.replace(":", "");
return normalized;
}
@@ -617,7 +672,7 @@ void VictronBLE::debugPrint(const String& 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(": ");