diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index ef0e1bf..577ac94 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -102,3 +102,19 @@ Arduino/ESP32 library for reading Victron Energy devices via Bluetooth Low Energ - library.json - library.properties + +### Session: 2026-02-12 18:23 +**Commits:** +``` +a843eb9 Keep v0.3.1 +5a210fb Experimenting with a claude file and created new logging example +``` +**Modified files:** +- .claude/CLAUDE.md +- README.md +- VERSIONS +- library.json +- library.properties +- src/VictronBLE.cpp +- src/VictronBLE.h + diff --git a/src/VictronBLE.cpp b/src/VictronBLE.cpp index 0975dad..da5011c 100644 --- a/src/VictronBLE.cpp +++ b/src/VictronBLE.cpp @@ -10,8 +10,8 @@ // Constructor VictronBLE::VictronBLE() - : pBLEScan(nullptr), callback(nullptr), debugEnabled(false), - scanDuration(5), initialized(false) { + : pBLEScan(nullptr), scanCallback(nullptr), callback(nullptr), + debugEnabled(false), scanDuration(5), initialized(false) { } // Destructor @@ -24,35 +24,38 @@ VictronBLE::~VictronBLE() { if (pBLEScan) { pBLEScan->stop(); } + + delete scanCallback; } // Initialize BLE bool VictronBLE::begin(uint32_t scanDuration) { if (initialized) { - debugPrint("VictronBLE already initialized"); + if (debugEnabled) debugPrint("VictronBLE already initialized"); return true; } this->scanDuration = scanDuration; - debugPrint("Initializing VictronBLE..."); + if (debugEnabled) debugPrint("Initializing VictronBLE..."); BLEDevice::init("VictronBLE"); pBLEScan = BLEDevice::getScan(); if (!pBLEScan) { lastError = "Failed to create BLE scanner"; - debugPrint(lastError); + if (debugEnabled) debugPrint(lastError); return false; } - pBLEScan->setAdvertisedDeviceCallbacks(new VictronBLEAdvertisedDeviceCallbacks(this), true); + scanCallback = new VictronBLEAdvertisedDeviceCallbacks(this); + pBLEScan->setAdvertisedDeviceCallbacks(scanCallback, true); pBLEScan->setActiveScan(false); // Passive scan - lower power pBLEScan->setInterval(100); pBLEScan->setWindow(99); initialized = true; - debugPrint("VictronBLE initialized successfully"); + if (debugEnabled) debugPrint("VictronBLE initialized successfully"); return true; } @@ -61,13 +64,13 @@ bool VictronBLE::begin(uint32_t scanDuration) { bool VictronBLE::addDevice(const VictronDeviceConfig& config) { if (config.macAddress.length() == 0) { lastError = "MAC address cannot be empty"; - debugPrint(lastError); + if (debugEnabled) debugPrint(lastError); return false; } if (config.encryptionKey.length() != 32) { lastError = "Encryption key must be 32 hex characters"; - debugPrint(lastError); + if (debugEnabled) debugPrint(lastError); return false; } @@ -75,7 +78,7 @@ bool VictronBLE::addDevice(const VictronDeviceConfig& config) { // Check if device already exists if (devices.find(normalizedMAC) != devices.end()) { - debugPrint("Device " + normalizedMAC + " already exists, updating config"); + if (debugEnabled) debugPrint("Device " + normalizedMAC + " already exists, updating config"); delete devices[normalizedMAC]; } @@ -86,7 +89,7 @@ bool VictronBLE::addDevice(const VictronDeviceConfig& config) { // Convert encryption key from hex string to bytes if (!hexStringToBytes(config.encryptionKey, info->encryptionKeyBytes, 16)) { lastError = "Invalid encryption key format"; - debugPrint(lastError); + if (debugEnabled) debugPrint(lastError); delete info; return false; } @@ -100,8 +103,8 @@ bool VictronBLE::addDevice(const VictronDeviceConfig& config) { devices[normalizedMAC] = info; - debugPrint("Added device: " + config.name + " (MAC: " + normalizedMAC + ")"); if (debugEnabled) { + debugPrint("Added device: " + config.name + " (MAC: " + normalizedMAC + ")"); debugPrint(" Original MAC input: " + config.macAddress); debugPrint(" Stored normalized: " + normalizedMAC); } @@ -109,21 +112,21 @@ bool VictronBLE::addDevice(const VictronDeviceConfig& config) { return true; } -bool VictronBLE::addDevice(String name, String macAddress, String encryptionKey, +bool VictronBLE::addDevice(const String& name, const String& macAddress, const String& encryptionKey, VictronDeviceType expectedType) { VictronDeviceConfig config(name, macAddress, encryptionKey, expectedType); return addDevice(config); } // Remove a device -void VictronBLE::removeDevice(String macAddress) { +void VictronBLE::removeDevice(const 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); + if (debugEnabled) debugPrint("Removed device: " + normalizedMAC); } } @@ -146,7 +149,7 @@ void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertise } // Process advertised device -void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) { +void VictronBLE::processDevice(BLEAdvertisedDevice& advertisedDevice) { // Get MAC address from the advertised device String mac = macAddressToString(advertisedDevice.getAddress()); String normalizedMAC = normalizeMAC(mac); @@ -155,28 +158,25 @@ void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) { debugPrint("Raw MAC: " + mac + " -> Normalized: " + normalizedMAC); } - // TODO: Consider skipping with no manufacturer data? - memset(&manufacturerData, 0, sizeof(manufacturerData)); + // Parse manufacturer data into local struct + victronManufacturerData mfgData; + memset(&mfgData, 0, sizeof(mfgData)); if (advertisedDevice.haveManufacturerData()) { - std::string mfgData = advertisedDevice.getManufacturerData(); - // XXX Storing it this way is not thread safe - is that issue on this ESP32? - debugPrint("Getting manufacturer data: Size=" + String(mfgData.length())); - mfgData.copy((char*)&manufacturerData, (mfgData.length() > sizeof(manufacturerData) ? sizeof(manufacturerData) : mfgData.length())); + std::string rawMfgData = advertisedDevice.getManufacturerData(); + if (debugEnabled) debugPrint("Getting manufacturer data: Size=" + String(rawMfgData.length())); + rawMfgData.copy(reinterpret_cast(&mfgData), + (rawMfgData.length() > sizeof(mfgData) ? sizeof(mfgData) : rawMfgData.length())); } - // Pointer? XXX - // Debug: Log all discovered BLE devices if (debugEnabled) { - String debugMsg = ""; - - debugMsg += "BLE Device: " + mac; + String debugMsg = "BLE Device: " + mac; debugMsg += ", RSSI: " + String(advertisedDevice.getRSSI()) + " dBm"; if (advertisedDevice.haveName()) debugMsg += ", Name: " + String(advertisedDevice.getName().c_str()); - debugMsg += ", Mfg ID: 0x" + String(manufacturerData.vendorID, HEX); - if (manufacturerData.vendorID == VICTRON_MANUFACTURER_ID) { + debugMsg += ", Mfg ID: 0x" + String(mfgData.vendorID, HEX); + if (mfgData.vendorID == VICTRON_MANUFACTURER_ID) { debugMsg += " (Victron)"; } @@ -186,25 +186,8 @@ void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) { // 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 (manufacturerData.vendorID == VICTRON_MANUFACTURER_ID) { - debugPrint("Found unmonitored Victron Device: " + normalizeMAC(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); - } - } - } - */ + if (debugEnabled && mfgData.vendorID == VICTRON_MANUFACTURER_ID) { + debugPrint("Found unmonitored Victron Device: " + normalizedMAC); } return; // Not a device we're monitoring } @@ -212,15 +195,15 @@ void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) { DeviceInfo* deviceInfo = it->second; // Check if it's Victron (manufacturer ID 0x02E1) - if (manufacturerData.vendorID != VICTRON_MANUFACTURER_ID) { - debugPrint("Skipping non VICTRON"); + if (mfgData.vendorID != VICTRON_MANUFACTURER_ID) { + if (debugEnabled) debugPrint("Skipping non VICTRON"); return; } - debugPrint("Processing data from: " + deviceInfo->config.name); + if (debugEnabled) debugPrint("Processing data from: " + deviceInfo->config.name); // Parse the advertisement - if (parseAdvertisement(normalizedMAC)) { + if (parseAdvertisement(deviceInfo, mfgData)) { // Update RSSI if (deviceInfo->data) { deviceInfo->data->rssi = advertisedDevice.getRSSI(); @@ -230,55 +213,47 @@ void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) { } // Parse advertisement data -bool VictronBLE::parseAdvertisement(const String& macAddress) { - // XXX We already searched above - try not to again? - auto it = devices.find(macAddress); - if (it == devices.end()) { - debugPrint("parseAdvertisement: Device not found"); - return false; - } - - DeviceInfo* deviceInfo = it->second; - +bool VictronBLE::parseAdvertisement(DeviceInfo* deviceInfo, const victronManufacturerData& mfgData) { if (debugEnabled) { - debugPrint("Vendor ID: 0x" + String(manufacturerData.vendorID, HEX)); - debugPrint("Beacon Type: 0x" + String(manufacturerData.beaconType, HEX)); - debugPrint("Record Type: 0x" + String(manufacturerData.victronRecordType, HEX)); - debugPrint("Nonce: 0x" + String(manufacturerData.nonceDataCounter, HEX)); + debugPrint("Vendor ID: 0x" + String(mfgData.vendorID, HEX)); + debugPrint("Beacon Type: 0x" + String(mfgData.beaconType, HEX)); + debugPrint("Record Type: 0x" + String(mfgData.victronRecordType, HEX)); + debugPrint("Nonce: 0x" + String(mfgData.nonceDataCounter, HEX)); } // Build IV (initialization vector) from nonce // IV is 16 bytes: nonce (2 bytes little-endian) + zeros (14 bytes) uint8_t iv[16] = {0}; - iv[0] = manufacturerData.nonceDataCounter & 0xFF; // Low byte - iv[1] = (manufacturerData.nonceDataCounter >> 8) & 0xFF; // High byte + iv[0] = mfgData.nonceDataCounter & 0xFF; // Low byte + iv[1] = (mfgData.nonceDataCounter >> 8) & 0xFF; // High byte // Remaining bytes stay zero // Decrypt the data - uint8_t decrypted[32]; // Max expected size - if (!decryptAdvertisement(manufacturerData.victronEncryptedData, - sizeof(manufacturerData.victronEncryptedData), + const size_t encryptedLen = sizeof(mfgData.victronEncryptedData); + uint8_t decrypted[encryptedLen]; + if (!decryptAdvertisement(mfgData.victronEncryptedData, + encryptedLen, deviceInfo->encryptionKeyBytes, iv, decrypted)) { lastError = "Decryption failed"; - debugPrint(lastError); + if (debugEnabled) debugPrint(lastError); return false; } // Parse based on device type bool parseOk = false; - switch (manufacturerData.victronRecordType) { + switch (mfgData.victronRecordType) { case DEVICE_TYPE_SOLAR_CHARGER: if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) { - parseOk = parseSolarCharger(decrypted, sizeof(decrypted), - *(SolarChargerData*)deviceInfo->data); + parseOk = parseSolarCharger(decrypted, encryptedLen, + *static_cast(deviceInfo->data)); } break; case DEVICE_TYPE_BATTERY_MONITOR: if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) { - parseOk = parseBatteryMonitor(decrypted, sizeof(decrypted), - *(BatteryMonitorData*)deviceInfo->data); + parseOk = parseBatteryMonitor(decrypted, encryptedLen, + *static_cast(deviceInfo->data)); } break; @@ -287,20 +262,20 @@ bool VictronBLE::parseAdvertisement(const String& macAddress) { case DEVICE_TYPE_MULTI_RS: case DEVICE_TYPE_VE_BUS: if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_INVERTER) { - parseOk = parseInverter(decrypted, sizeof(decrypted), - *(InverterData*)deviceInfo->data); + parseOk = parseInverter(decrypted, encryptedLen, + *static_cast(deviceInfo->data)); } break; case DEVICE_TYPE_DCDC_CONVERTER: if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) { - parseOk = parseDCDCConverter(decrypted, sizeof(decrypted), - *(DCDCConverterData*)deviceInfo->data); + parseOk = parseDCDCConverter(decrypted, encryptedLen, + *static_cast(deviceInfo->data)); } break; default: - debugPrint("Unknown device type: 0x" + String(manufacturerData.victronRecordType, HEX)); + if (debugEnabled) debugPrint("Unknown device type: 0x" + String(mfgData.victronRecordType, HEX)); return false; } @@ -309,21 +284,21 @@ bool VictronBLE::parseAdvertisement(const String& macAddress) { // Call appropriate callback if (callback) { - switch (manufacturerData.victronRecordType) { + switch (mfgData.victronRecordType) { case DEVICE_TYPE_SOLAR_CHARGER: - callback->onSolarChargerData(*(SolarChargerData*)deviceInfo->data); + callback->onSolarChargerData(*static_cast(deviceInfo->data)); break; case DEVICE_TYPE_BATTERY_MONITOR: - callback->onBatteryMonitorData(*(BatteryMonitorData*)deviceInfo->data); + callback->onBatteryMonitorData(*static_cast(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); + callback->onInverterData(*static_cast(deviceInfo->data)); break; case DEVICE_TYPE_DCDC_CONVERTER: - callback->onDCDCConverterData(*(DCDCConverterData*)deviceInfo->data); + callback->onDCDCConverterData(*static_cast(deviceInfo->data)); break; } } @@ -365,15 +340,14 @@ bool VictronBLE::decryptAdvertisement(const uint8_t* encrypted, size_t encLen, // Parse Solar Charger data bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarChargerData& result) { if (len < sizeof(victronSolarChargerPayload)) { - debugPrint("Solar charger data too short: " + String(len) + " bytes"); + if (debugEnabled) debugPrint("Solar charger data too short: " + String(len) + " bytes"); return false; } - // Cast decrypted data to struct for easy access - const victronSolarChargerPayload* payload = (const victronSolarChargerPayload*)data; + const auto* payload = reinterpret_cast(data); // Parse charge state - result.chargeState = (SolarChargerState)payload->deviceState; + result.chargeState = static_cast(payload->deviceState); // Parse battery voltage (10 mV units -> volts) result.batteryVoltage = payload->batteryVoltage * 0.01f; @@ -401,9 +375,11 @@ bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarCharger result.panelVoltage = 0; } - debugPrint("Solar Charger: " + String(result.batteryVoltage, 2) + "V, " + - String(result.batteryCurrent, 2) + "A, " + - String(result.panelPower) + "W, State: " + String(result.chargeState)); + if (debugEnabled) { + debugPrint("Solar Charger: " + String(result.batteryVoltage, 2) + "V, " + + String(result.batteryCurrent, 2) + "A, " + + String(result.panelPower) + "W, State: " + String(result.chargeState)); + } return true; } @@ -411,12 +387,11 @@ bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarCharger // Parse Battery Monitor data bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result) { if (len < sizeof(victronBatteryMonitorPayload)) { - debugPrint("Battery monitor data too short: " + String(len) + " bytes"); + if (debugEnabled) debugPrint("Battery monitor data too short: " + String(len) + " bytes"); return false; } - // Cast decrypted data to struct for easy access - const victronBatteryMonitorPayload* payload = (const victronBatteryMonitorPayload*)data; + const auto* payload = reinterpret_cast(data); // Parse remaining time (1 minute units) result.remainingMinutes = payload->remainingMins; @@ -441,7 +416,6 @@ bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMon } // Parse battery current (22-bit signed, 1 mA units) - // Bits 0-7: currentLow, Bits 8-15: currentMid, Bits 16-21: low 6 bits of currentHigh_consumedLow int32_t current = payload->currentLow | (payload->currentMid << 8) | ((payload->currentHigh_consumedLow & 0x3F) << 16); @@ -452,7 +426,6 @@ bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMon result.current = current * 0.001f; // Convert mA to A // Parse consumed Ah (18-bit signed, 10 mAh units) - // Bits 0-1: high 2 bits of currentHigh_consumedLow, Bits 2-9: consumedMid, Bits 10-17: consumedHigh int32_t consumedAh = ((payload->currentHigh_consumedLow & 0xC0) >> 6) | (payload->consumedMid << 2) | (payload->consumedHigh << 10); @@ -465,8 +438,10 @@ bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMon // Parse SOC (10-bit value, 10 = 1.0%) result.soc = (payload->soc & 0x3FF) * 0.1f; - debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " + - String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%"); + if (debugEnabled) { + debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " + + String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%"); + } return true; } @@ -474,12 +449,11 @@ bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMon // Parse Inverter data bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& result) { if (len < sizeof(victronInverterPayload)) { - debugPrint("Inverter data too short: " + String(len) + " bytes"); + if (debugEnabled) debugPrint("Inverter data too short: " + String(len) + " bytes"); return false; } - // Cast decrypted data to struct for easy access - const victronInverterPayload* payload = (const victronInverterPayload*)data; + const auto* payload = reinterpret_cast(data); // Parse device state result.state = payload->deviceState; @@ -506,8 +480,10 @@ bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& re result.alarmHighTemperature = (payload->alarms & 0x04) != 0; result.alarmOverload = (payload->alarms & 0x08) != 0; - debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " + - String(result.acPower) + "W, State: " + String(result.state)); + if (debugEnabled) { + debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " + + String(result.acPower) + "W, State: " + String(result.state)); + } return true; } @@ -515,12 +491,11 @@ bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& re // Parse DC-DC Converter data bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result) { if (len < sizeof(victronDCDCConverterPayload)) { - debugPrint("DC-DC converter data too short: " + String(len) + " bytes"); + if (debugEnabled) debugPrint("DC-DC converter data too short: " + String(len) + " bytes"); return false; } - // Cast decrypted data to struct for easy access - const victronDCDCConverterPayload* payload = (const victronDCDCConverterPayload*)data; + const auto* payload = reinterpret_cast(data); // Parse charge state result.chargeState = payload->chargeState; @@ -537,56 +512,58 @@ bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConvert // Parse output current (10 mA units -> amps) result.outputCurrent = payload->outputCurrent * 0.01f; - debugPrint("DC-DC Converter: In=" + String(result.inputVoltage, 2) + "V, Out=" + - String(result.outputVoltage, 2) + "V, " + String(result.outputCurrent, 2) + "A"); + if (debugEnabled) { + 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) { +bool VictronBLE::getSolarChargerData(const 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; + data = *static_cast(it->second->data); return data.dataValid; } return false; } -bool VictronBLE::getBatteryMonitorData(String macAddress, BatteryMonitorData& data) { +bool VictronBLE::getBatteryMonitorData(const 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; + data = *static_cast(it->second->data); return data.dataValid; } return false; } -bool VictronBLE::getInverterData(String macAddress, InverterData& data) { +bool VictronBLE::getInverterData(const 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; + data = *static_cast(it->second->data); return data.dataValid; } return false; } -bool VictronBLE::getDCDCConverterData(String macAddress, DCDCConverterData& data) { +bool VictronBLE::getDCDCConverterData(const 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; + data = *static_cast(it->second->data); return data.dataValid; } return false; @@ -644,21 +621,19 @@ bool VictronBLE::hexStringToBytes(const String& hex, uint8_t* bytes, size_t len) // Helper: MAC address to string String VictronBLE::macAddressToString(BLEAddress address) { - // Use the BLEAddress toString() method which provides consistent formatting return String(address.toString().c_str()); } // Helper: Normalize MAC address format -String VictronBLE::normalizeMAC(String mac) { +String VictronBLE::normalizeMAC(const String& mac) { String normalized = mac; normalized.toLowerCase(); - // XXX - is this right, was - to : but not consistent location of pairs or not normalized.replace("-", ""); normalized.replace(":", ""); return normalized; } -// Debug helpers +// Debug helper void VictronBLE::debugPrint(const String& message) { if (debugEnabled) Serial.println("[VictronBLE] " + message); diff --git a/src/VictronBLE.h b/src/VictronBLE.h index 8c5cd51..3817956 100644 --- a/src/VictronBLE.h +++ b/src/VictronBLE.h @@ -20,7 +20,7 @@ #include "mbedtls/aes.h" // Victron manufacturer ID -#define VICTRON_MANUFACTURER_ID 0x02E1 +static constexpr uint16_t VICTRON_MANUFACTURER_ID = 0x02E1; // Device type IDs from Victron protocol enum VictronDeviceType { @@ -57,7 +57,7 @@ enum SolarChargerState { // Must use __attribute__((packed)) to prevent compiler padding // Manufacturer data structure (outer envelope) -typedef struct { +struct victronManufacturerData { uint16_t vendorID; // vendor ID uint8_t beaconType; // Should be 0x10 (Product Advertisement) for the packets we want uint8_t unknownData1[3]; // Unknown data @@ -66,11 +66,11 @@ typedef struct { 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; +} __attribute__((packed)); // Decrypted payload structures for each device type // Solar Charger decrypted payload -typedef struct { +struct victronSolarChargerPayload { uint8_t deviceState; // Charge state (SolarChargerState enum) uint8_t errorCode; // Error code int16_t batteryVoltage; // Battery voltage in 10mV units @@ -79,10 +79,10 @@ typedef struct { uint16_t inputPower; // PV power in 1W units uint16_t loadCurrent; // Load current in 10mA units (0xFFFF = no load) uint8_t reserved[2]; // Reserved bytes -} __attribute__((packed)) victronSolarChargerPayload; +} __attribute__((packed)); // Battery Monitor decrypted payload -typedef struct { +struct victronBatteryMonitorPayload { uint16_t remainingMins; // Time remaining in minutes uint16_t batteryVoltage; // Battery voltage in 10mV units uint8_t alarms; // Alarm bits @@ -94,10 +94,10 @@ typedef struct { uint8_t consumedHigh; // Consumed Ah bits 10-17 uint16_t soc; // State of charge in 0.1% units (10-bit value) uint8_t reserved[2]; // Reserved bytes -} __attribute__((packed)) victronBatteryMonitorPayload; +} __attribute__((packed)); // Inverter decrypted payload -typedef struct { +struct victronInverterPayload { uint8_t deviceState; // Device state uint8_t errorCode; // Error code uint16_t batteryVoltage; // Battery voltage in 10mV units @@ -107,17 +107,17 @@ typedef struct { uint8_t acPowerHigh; // AC Power bits 16-23 (signed 24-bit) uint8_t alarms; // Alarm bits uint8_t reserved[4]; // Reserved bytes -} __attribute__((packed)) victronInverterPayload; +} __attribute__((packed)); // DC-DC Converter decrypted payload -typedef struct { +struct victronDCDCConverterPayload { uint8_t chargeState; // Charge state uint8_t errorCode; // Error code uint16_t inputVoltage; // Input voltage in 10mV units uint16_t outputVoltage; // Output voltage in 10mV units uint16_t outputCurrent; // Output current in 10mA units uint8_t reserved[6]; // Reserved bytes -} __attribute__((packed)) victronDCDCConverterPayload; +} __attribute__((packed)); // Base structure for all device data struct VictronDeviceData { @@ -205,8 +205,9 @@ struct DCDCConverterData : public VictronDeviceData { } }; -// Forward declaration +// Forward declarations class VictronBLE; +class VictronBLEAdvertisedDeviceCallbacks; // Callback interface for device data updates class VictronDeviceCallback { @@ -226,7 +227,7 @@ struct VictronDeviceConfig { VictronDeviceType expectedType; VictronDeviceConfig() : expectedType(DEVICE_TYPE_UNKNOWN) {} - VictronDeviceConfig(String n, String mac, String key, VictronDeviceType type = DEVICE_TYPE_UNKNOWN) + VictronDeviceConfig(const String& n, const String& mac, const String& key, VictronDeviceType type = DEVICE_TYPE_UNKNOWN) : name(n), macAddress(mac), encryptionKey(key), expectedType(type) {} }; @@ -241,11 +242,11 @@ public: // Add a device to monitor bool addDevice(const VictronDeviceConfig& config); - bool addDevice(String name, String macAddress, String encryptionKey, + bool addDevice(const String& name, const String& macAddress, const String& encryptionKey, VictronDeviceType expectedType = DEVICE_TYPE_UNKNOWN); // Remove a device - void removeDevice(String macAddress); + void removeDevice(const String& macAddress); // Get device count size_t getDeviceCount() const { return devices.size(); } @@ -257,10 +258,10 @@ public: 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); + bool getSolarChargerData(const String& macAddress, SolarChargerData& data); + bool getBatteryMonitorData(const String& macAddress, BatteryMonitorData& data); + bool getInverterData(const String& macAddress, InverterData& data); + bool getDCDCConverterData(const String& macAddress, DCDCConverterData& data); // Get all devices of a specific type std::vector getDevicesByType(VictronDeviceType type); @@ -285,26 +286,26 @@ private: ~DeviceInfo() { if (data) delete data; } + DeviceInfo(const DeviceInfo&) = delete; + DeviceInfo& operator=(const DeviceInfo&) = delete; }; std::map devices; BLEScan* pBLEScan; + VictronBLEAdvertisedDeviceCallbacks* scanCallback; VictronDeviceCallback* callback; bool debugEnabled; String lastError; uint32_t scanDuration; bool initialized; - // XXX Experiment with actual victron data - victronManufacturerData manufacturerData; - // 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 String& macAddress); - void processDevice(BLEAdvertisedDevice advertisedDevice); + bool parseAdvertisement(DeviceInfo* deviceInfo, const victronManufacturerData& mfgData); + void processDevice(BLEAdvertisedDevice& advertisedDevice); VictronDeviceData* createDeviceData(VictronDeviceType type); bool parseSolarCharger(const uint8_t* data, size_t len, SolarChargerData& result); @@ -315,7 +316,7 @@ private: void debugPrint(const String& message); String macAddressToString(BLEAddress address); - String normalizeMAC(String mac); + String normalizeMAC(const String& mac); }; // BLE scan callback class