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

4
TODO Normal file
View File

@@ -0,0 +1,4 @@
# Misc Stuff
* Consider support for upper/lower case MAC address and optionaly ":"
* Scanning - list devices publishing, should be able to get list even without knowing MAC / Encryption key

View File

@@ -36,3 +36,24 @@ framework = arduino
monitor_speed = 115200 monitor_speed = 115200
build_flags = build_flags =
-DCORE_DEBUG_LEVEL=3 -DCORE_DEBUG_LEVEL=3
[env:m5stick]
platform = espressif32
board = m5stick-c
framework = arduino
board_build.mcu = esp32
board_build.f_cpu = 240000000L
board_build.partitions = no_ota.csv
#upload_protocol = espota
#upload_port = Button.local
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
build_flags =
-Os
lib_deps =
M5StickC
elapsedMillis
TaskScheduler
Button2
ArduinoJson
https://github.com/scottp/PsychicHttp.git

View File

@@ -1,13 +1,13 @@
/** /**
* VictronBLE Example * VictronBLE Example
* *
* This example demonstrates how to use the VictronBLE library to read data * This example demonstrates how to use the VictronBLE library to read data
* from multiple Victron devices simultaneously. * from multiple Victron devices simultaneously.
* *
* Hardware Requirements: * Hardware Requirements:
* - ESP32 board * - ESP32 board
* - Victron devices with BLE (SmartSolar, SmartShunt, etc.) * - Victron devices with BLE (SmartSolar, SmartShunt, etc.)
* *
* Setup: * Setup:
* 1. Get your device encryption keys from the VictronConnect app: * 1. Get your device encryption keys from the VictronConnect app:
* - Open VictronConnect * - Open VictronConnect
@@ -16,7 +16,7 @@
* - Enable "Instant readout via Bluetooth" * - Enable "Instant readout via Bluetooth"
* - Click "Show" next to "Instant readout details" * - Click "Show" next to "Instant readout details"
* - Copy the encryption key (32 hex characters) * - Copy the encryption key (32 hex characters)
* *
* 2. Update the device configurations below with your devices' MAC addresses * 2. Update the device configurations below with your devices' MAC addresses
* and encryption keys * and encryption keys
*/ */
@@ -45,7 +45,7 @@ public:
} }
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago"); Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
} }
void onBatteryMonitorData(const BatteryMonitorData& data) override { void onBatteryMonitorData(const BatteryMonitorData& data) override {
Serial.println("\n=== Battery Monitor: " + data.deviceName + " ==="); Serial.println("\n=== Battery Monitor: " + data.deviceName + " ===");
Serial.println("MAC: " + data.macAddress); Serial.println("MAC: " + data.macAddress);
@@ -54,20 +54,20 @@ public:
Serial.println("Current: " + String(data.current, 2) + " A"); Serial.println("Current: " + String(data.current, 2) + " A");
Serial.println("SOC: " + String(data.soc, 1) + " %"); Serial.println("SOC: " + String(data.soc, 1) + " %");
Serial.println("Consumed: " + String(data.consumedAh, 2) + " Ah"); Serial.println("Consumed: " + String(data.consumedAh, 2) + " Ah");
if (data.remainingMinutes < 65535) { if (data.remainingMinutes < 65535) {
int hours = data.remainingMinutes / 60; int hours = data.remainingMinutes / 60;
int mins = data.remainingMinutes % 60; int mins = data.remainingMinutes % 60;
Serial.println("Time Remaining: " + String(hours) + "h " + String(mins) + "m"); Serial.println("Time Remaining: " + String(hours) + "h " + String(mins) + "m");
} }
if (data.temperature > 0) { if (data.temperature > 0) {
Serial.println("Temperature: " + String(data.temperature, 1) + " °C"); Serial.println("Temperature: " + String(data.temperature, 1) + " °C");
} }
if (data.auxVoltage > 0) { if (data.auxVoltage > 0) {
Serial.println("Aux Voltage: " + String(data.auxVoltage, 2) + " V"); Serial.println("Aux Voltage: " + String(data.auxVoltage, 2) + " V");
} }
// Print alarms // Print alarms
if (data.alarmLowVoltage || data.alarmHighVoltage || data.alarmLowSOC || if (data.alarmLowVoltage || data.alarmHighVoltage || data.alarmLowSOC ||
data.alarmLowTemperature || data.alarmHighTemperature) { data.alarmLowTemperature || data.alarmHighTemperature) {
@@ -79,10 +79,10 @@ public:
if (data.alarmHighTemperature) Serial.print("HIGH-TEMP "); if (data.alarmHighTemperature) Serial.print("HIGH-TEMP ");
Serial.println(); Serial.println();
} }
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago"); Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
} }
void onInverterData(const InverterData& data) override { void onInverterData(const InverterData& data) override {
Serial.println("\n=== Inverter/Charger: " + data.deviceName + " ==="); Serial.println("\n=== Inverter/Charger: " + data.deviceName + " ===");
Serial.println("MAC: " + data.macAddress); Serial.println("MAC: " + data.macAddress);
@@ -91,9 +91,9 @@ public:
Serial.println("Current: " + String(data.batteryCurrent, 2) + " A"); Serial.println("Current: " + String(data.batteryCurrent, 2) + " A");
Serial.println("AC Power: " + String(data.acPower) + " W"); Serial.println("AC Power: " + String(data.acPower) + " W");
Serial.println("State: " + String(data.state)); Serial.println("State: " + String(data.state));
// Print alarms // Print alarms
if (data.alarmLowVoltage || data.alarmHighVoltage || if (data.alarmLowVoltage || data.alarmHighVoltage ||
data.alarmHighTemperature || data.alarmOverload) { data.alarmHighTemperature || data.alarmOverload) {
Serial.print("ALARMS: "); Serial.print("ALARMS: ");
if (data.alarmLowVoltage) Serial.print("LOW-V "); if (data.alarmLowVoltage) Serial.print("LOW-V ");
@@ -102,10 +102,10 @@ public:
if (data.alarmOverload) Serial.print("OVERLOAD "); if (data.alarmOverload) Serial.print("OVERLOAD ");
Serial.println(); Serial.println();
} }
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago"); Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
} }
void onDCDCConverterData(const DCDCConverterData& data) override { void onDCDCConverterData(const DCDCConverterData& data) override {
Serial.println("\n=== DC-DC Converter: " + data.deviceName + " ==="); Serial.println("\n=== DC-DC Converter: " + data.deviceName + " ===");
Serial.println("MAC: " + data.macAddress); Serial.println("MAC: " + data.macAddress);
@@ -119,7 +119,7 @@ public:
} }
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago"); Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
} }
private: private:
String getChargeStateName(SolarChargerState state) { String getChargeStateName(SolarChargerState state) {
switch (state) { switch (state) {
@@ -144,27 +144,35 @@ MyVictronCallback callback;
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
delay(1000); delay(1000);
Serial.println("\n\n================================="); Serial.println("\n\n=================================");
Serial.println("VictronBLE Multi-Device Example"); Serial.println("VictronBLE Multi-Device Example");
Serial.println("=================================\n"); Serial.println("=================================\n");
// Initialize VictronBLE with 5 second scan duration // Initialize VictronBLE with 5 second scan duration
if (!victron.begin(5)) { if (!victron.begin(5)) {
Serial.println("ERROR: Failed to initialize VictronBLE!"); Serial.println("ERROR: Failed to initialize VictronBLE!");
Serial.println(victron.getLastError()); Serial.println(victron.getLastError());
while (1) delay(1000); while (1) delay(1000);
} }
// Enable debug output (optional) // Enable debug output (optional)
victron.setDebug(true); victron.setDebug(true);
// Set callback for data updates // Set callback for data updates
victron.setCallback(&callback); victron.setCallback(&callback);
// Add your devices here // Add your devices here
// Replace with your actual MAC addresses and encryption keys // Replace with your actual MAC addresses and encryption keys
// Temporary - Scott Example
victron.addDevice(
"Rainbow48V", // Device name
"E4:05:42:34:14:F3", // MAC address
"0ec3adf7433dd61793ff2f3b8ad32ed8", // Encryption key (32 hex chars)
DEVICE_TYPE_SOLAR_CHARGER // Device type
);
// Example: Solar Charger #1 // Example: Solar Charger #1
victron.addDevice( victron.addDevice(
"MPPT 100/30", // Device name "MPPT 100/30", // Device name
@@ -172,7 +180,7 @@ void setup() {
"0df4d0395b7d1a876c0c33ecb9e70dcd", // Encryption key (32 hex chars) "0df4d0395b7d1a876c0c33ecb9e70dcd", // Encryption key (32 hex chars)
DEVICE_TYPE_SOLAR_CHARGER // Device type DEVICE_TYPE_SOLAR_CHARGER // Device type
); );
// Example: Solar Charger #2 // Example: Solar Charger #2
victron.addDevice( victron.addDevice(
"MPPT 75/15", "MPPT 75/15",
@@ -180,7 +188,7 @@ void setup() {
"1234567890abcdef1234567890abcdef", "1234567890abcdef1234567890abcdef",
DEVICE_TYPE_SOLAR_CHARGER DEVICE_TYPE_SOLAR_CHARGER
); );
// Example: Battery Monitor (SmartShunt) // Example: Battery Monitor (SmartShunt)
victron.addDevice( victron.addDevice(
"SmartShunt", "SmartShunt",
@@ -188,7 +196,7 @@ void setup() {
"fedcba0987654321fedcba0987654321", "fedcba0987654321fedcba0987654321",
DEVICE_TYPE_BATTERY_MONITOR DEVICE_TYPE_BATTERY_MONITOR
); );
// Example: Inverter/Charger // Example: Inverter/Charger
victron.addDevice( victron.addDevice(
"MultiPlus", "MultiPlus",
@@ -196,7 +204,7 @@ void setup() {
"abcdefabcdefabcdefabcdefabcdefab", "abcdefabcdefabcdefabcdefabcdefab",
DEVICE_TYPE_INVERTER DEVICE_TYPE_INVERTER
); );
Serial.println("Configured " + String(victron.getDeviceCount()) + " devices"); Serial.println("Configured " + String(victron.getDeviceCount()) + " devices");
Serial.println("\nStarting BLE scan...\n"); Serial.println("\nStarting BLE scan...\n");
} }
@@ -204,7 +212,7 @@ void setup() {
void loop() { void loop() {
// Process BLE scanning and data updates // Process BLE scanning and data updates
victron.loop(); victron.loop();
// Optional: You can also manually query device data // Optional: You can also manually query device data
// This is useful if you're not using callbacks // This is useful if you're not using callbacks
/* /*
@@ -212,13 +220,13 @@ void loop() {
if (victron.getSolarChargerData("E7:48:D4:28:B7:9C", solarData)) { if (victron.getSolarChargerData("E7:48:D4:28:B7:9C", solarData)) {
// Do something with solarData // Do something with solarData
} }
BatteryMonitorData batteryData; BatteryMonitorData batteryData;
if (victron.getBatteryMonitorData("11:22:33:44:55:66", batteryData)) { if (victron.getBatteryMonitorData("11:22:33:44:55:66", batteryData)) {
// Do something with batteryData // Do something with batteryData
} }
*/ */
// Add a small delay to avoid overwhelming the serial output // Add a small delay to avoid overwhelming the serial output
delay(100); delay(100);
} }

View File

@@ -1,7 +1,7 @@
/** /**
* VictronBLE - ESP32 library for Victron Energy BLE devices * VictronBLE - ESP32 library for Victron Energy BLE devices
* Implementation file * Implementation file
* *
* Copyright (c) 2025 Scott Penrose * Copyright (c) 2025 Scott Penrose
* License: MIT * License: MIT
*/ */
@@ -9,8 +9,8 @@
#include "VictronBLE.h" #include "VictronBLE.h"
// Constructor // Constructor
VictronBLE::VictronBLE() VictronBLE::VictronBLE()
: pBLEScan(nullptr), callback(nullptr), debugEnabled(false), : pBLEScan(nullptr), callback(nullptr), debugEnabled(false),
scanDuration(5), initialized(false) { scanDuration(5), initialized(false) {
} }
@@ -20,7 +20,7 @@ VictronBLE::~VictronBLE() {
delete pair.second; delete pair.second;
} }
devices.clear(); devices.clear();
if (pBLEScan) { if (pBLEScan) {
pBLEScan->stop(); pBLEScan->stop();
} }
@@ -32,27 +32,27 @@ bool VictronBLE::begin(uint32_t scanDuration) {
debugPrint("VictronBLE already initialized"); debugPrint("VictronBLE already initialized");
return true; return true;
} }
this->scanDuration = scanDuration; this->scanDuration = scanDuration;
debugPrint("Initializing VictronBLE..."); debugPrint("Initializing VictronBLE...");
BLEDevice::init("VictronBLE"); BLEDevice::init("VictronBLE");
pBLEScan = BLEDevice::getScan(); pBLEScan = BLEDevice::getScan();
if (!pBLEScan) { if (!pBLEScan) {
lastError = "Failed to create BLE scanner"; lastError = "Failed to create BLE scanner";
return false; return false;
} }
pBLEScan->setAdvertisedDeviceCallbacks(new VictronBLEAdvertisedDeviceCallbacks(this), true); pBLEScan->setAdvertisedDeviceCallbacks(new VictronBLEAdvertisedDeviceCallbacks(this), true);
pBLEScan->setActiveScan(false); // Passive scan - lower power pBLEScan->setActiveScan(false); // Passive scan - lower power
pBLEScan->setInterval(100); pBLEScan->setInterval(100);
pBLEScan->setWindow(99); pBLEScan->setWindow(99);
initialized = true; initialized = true;
debugPrint("VictronBLE initialized successfully"); debugPrint("VictronBLE initialized successfully");
return true; return true;
} }
@@ -62,42 +62,42 @@ bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
lastError = "MAC address cannot be empty"; lastError = "MAC address cannot be empty";
return false; return false;
} }
if (config.encryptionKey.length() != 32) { if (config.encryptionKey.length() != 32) {
lastError = "Encryption key must be 32 hex characters"; lastError = "Encryption key must be 32 hex characters";
return false; return false;
} }
String normalizedMAC = normalizeMAC(config.macAddress); String normalizedMAC = normalizeMAC(config.macAddress);
// Check if device already exists // Check if device already exists
if (devices.find(normalizedMAC) != devices.end()) { if (devices.find(normalizedMAC) != devices.end()) {
debugPrint("Device " + normalizedMAC + " already exists, updating config"); debugPrint("Device " + normalizedMAC + " already exists, updating config");
delete devices[normalizedMAC]; delete devices[normalizedMAC];
} }
DeviceInfo* info = new DeviceInfo(); DeviceInfo* info = new DeviceInfo();
info->config = config; info->config = config;
info->config.macAddress = normalizedMAC; info->config.macAddress = normalizedMAC;
// Convert encryption key from hex string to bytes // Convert encryption key from hex string to bytes
if (!hexStringToBytes(config.encryptionKey, info->encryptionKeyBytes, 16)) { if (!hexStringToBytes(config.encryptionKey, info->encryptionKeyBytes, 16)) {
lastError = "Invalid encryption key format"; lastError = "Invalid encryption key format";
delete info; delete info;
return false; return false;
} }
// Create appropriate data structure based on device type // Create appropriate data structure based on device type
info->data = createDeviceData(config.expectedType); info->data = createDeviceData(config.expectedType);
if (info->data) { if (info->data) {
info->data->macAddress = normalizedMAC; info->data->macAddress = normalizedMAC;
info->data->deviceName = config.name; info->data->deviceName = config.name;
} }
devices[normalizedMAC] = info; devices[normalizedMAC] = info;
debugPrint("Added device: " + config.name + " (" + normalizedMAC + ")"); debugPrint("Added device: " + config.name + " (" + normalizedMAC + ")");
return true; return true;
} }
@@ -110,7 +110,7 @@ bool VictronBLE::addDevice(String name, String macAddress, String encryptionKey,
// Remove a device // Remove a device
void VictronBLE::removeDevice(String macAddress) { void VictronBLE::removeDevice(String macAddress) {
String normalizedMAC = normalizeMAC(macAddress); String normalizedMAC = normalizeMAC(macAddress);
auto it = devices.find(normalizedMAC); auto it = devices.find(normalizedMAC);
if (it != devices.end()) { if (it != devices.end()) {
delete it->second; delete it->second;
@@ -124,7 +124,7 @@ void VictronBLE::loop() {
if (!initialized) { if (!initialized) {
return; return;
} }
// Start a scan // Start a scan
BLEScanResults scanResults = pBLEScan->start(scanDuration, false); BLEScanResults scanResults = pBLEScan->start(scanDuration, false);
pBLEScan->clearResults(); pBLEScan->clearResults();
@@ -133,6 +133,30 @@ void VictronBLE::loop() {
// BLE callback implementation // BLE callback implementation
void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertisedDevice) { void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertisedDevice) {
if (victronBLE) { 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); victronBLE->processDevice(advertisedDevice);
} }
} }
@@ -141,33 +165,62 @@ void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertise
void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) { void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) {
String mac = macAddressToString(advertisedDevice.getAddress()); String mac = macAddressToString(advertisedDevice.getAddress());
String normalizedMAC = normalizeMAC(mac); String normalizedMAC = normalizeMAC(mac);
// Check if this is one of our configured devices // Check if this is one of our configured devices
auto it = devices.find(normalizedMAC); auto it = devices.find(normalizedMAC);
if (it == devices.end()) { 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 return; // Not a device we're monitoring
} }
DeviceInfo* deviceInfo = it->second; DeviceInfo* deviceInfo = it->second;
// Check if device has manufacturer data // Check if device has manufacturer data
if (!advertisedDevice.haveManufacturerData()) { if (!advertisedDevice.haveManufacturerData()) {
return; return;
} }
std::string mfgData = advertisedDevice.getManufacturerData(); std::string mfgData = advertisedDevice.getManufacturerData();
if (mfgData.length() < 2) { if (mfgData.length() < 2) {
return; return;
} }
// Check if it's Victron (manufacturer ID 0x02E1) // Check if it's Victron (manufacturer ID 0x02E1)
uint16_t mfgId = (uint8_t)mfgData[1] << 8 | (uint8_t)mfgData[0]; uint16_t mfgId = (uint8_t)mfgData[1] << 8 | (uint8_t)mfgData[0];
if (mfgId != VICTRON_MANUFACTURER_ID) { if (mfgId != VICTRON_MANUFACTURER_ID) {
return; return;
} }
debugPrint("Processing data from: " + deviceInfo->config.name); debugPrint("Processing data from: " + deviceInfo->config.name);
// Parse the advertisement // Parse the advertisement
if (parseAdvertisement((const uint8_t*)mfgData.data(), mfgData.length(), normalizedMAC)) { if (parseAdvertisement((const uint8_t*)mfgData.data(), mfgData.length(), normalizedMAC)) {
// Update RSSI // Update RSSI
@@ -185,62 +238,62 @@ bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
if (it == devices.end()) { if (it == devices.end()) {
return false; return false;
} }
DeviceInfo* deviceInfo = it->second; DeviceInfo* deviceInfo = it->second;
if (len < 6) { if (len < 6) {
debugPrint("Manufacturer data too short"); debugPrint("Manufacturer data too short");
return false; return false;
} }
// Structure: [MfgID(2)] [DeviceType(1)] [IV(2)] [EncryptedData(n)] // Structure: [MfgID(2)] [DeviceType(1)] [IV(2)] [EncryptedData(n)]
uint8_t deviceType = manufacturerData[2]; uint8_t deviceType = manufacturerData[2];
// Extract IV (initialization vector) - bytes 3-4, little-endian // Extract IV (initialization vector) - bytes 3-4, little-endian
uint8_t iv[16] = {0}; uint8_t iv[16] = {0};
iv[0] = manufacturerData[3]; iv[0] = manufacturerData[3];
iv[1] = manufacturerData[4]; iv[1] = manufacturerData[4];
// Rest of IV is zero-padded // Rest of IV is zero-padded
// Encrypted data starts at byte 5 // Encrypted data starts at byte 5
const uint8_t* encryptedData = manufacturerData + 5; const uint8_t* encryptedData = manufacturerData + 5;
size_t encryptedLen = len - 5; size_t encryptedLen = len - 5;
if (debugEnabled) { if (debugEnabled) {
debugPrintHex("Encrypted data", encryptedData, encryptedLen); debugPrintHex("Encrypted data", encryptedData, encryptedLen);
debugPrintHex("IV", iv, 16); debugPrintHex("IV", iv, 16);
} }
// Decrypt the data // Decrypt the data
uint8_t decrypted[32]; // Max expected size uint8_t decrypted[32]; // Max expected size
if (!decryptAdvertisement(encryptedData, encryptedLen, if (!decryptAdvertisement(encryptedData, encryptedLen,
deviceInfo->encryptionKeyBytes, iv, decrypted)) { deviceInfo->encryptionKeyBytes, iv, decrypted)) {
lastError = "Decryption failed"; lastError = "Decryption failed";
return false; return false;
} }
if (debugEnabled) { if (debugEnabled) {
debugPrintHex("Decrypted data", decrypted, encryptedLen); debugPrintHex("Decrypted data", decrypted, encryptedLen);
} }
// Parse based on device type // Parse based on device type
bool parseOk = false; bool parseOk = false;
switch (deviceType) { switch (deviceType) {
case DEVICE_TYPE_SOLAR_CHARGER: case DEVICE_TYPE_SOLAR_CHARGER:
if (deviceInfo->data && deviceInfo->data->deviceType == 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); *(SolarChargerData*)deviceInfo->data);
} }
break; break;
case DEVICE_TYPE_BATTERY_MONITOR: case DEVICE_TYPE_BATTERY_MONITOR:
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) { if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) {
parseOk = parseBatteryMonitor(decrypted, encryptedLen, parseOk = parseBatteryMonitor(decrypted, encryptedLen,
*(BatteryMonitorData*)deviceInfo->data); *(BatteryMonitorData*)deviceInfo->data);
} }
break; break;
case DEVICE_TYPE_INVERTER: case DEVICE_TYPE_INVERTER:
case DEVICE_TYPE_INVERTER_RS: case DEVICE_TYPE_INVERTER_RS:
case DEVICE_TYPE_MULTI_RS: case DEVICE_TYPE_MULTI_RS:
@@ -250,22 +303,22 @@ bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
*(InverterData*)deviceInfo->data); *(InverterData*)deviceInfo->data);
} }
break; break;
case DEVICE_TYPE_DCDC_CONVERTER: case DEVICE_TYPE_DCDC_CONVERTER:
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) { if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) {
parseOk = parseDCDCConverter(decrypted, encryptedLen, parseOk = parseDCDCConverter(decrypted, encryptedLen,
*(DCDCConverterData*)deviceInfo->data); *(DCDCConverterData*)deviceInfo->data);
} }
break; break;
default: default:
debugPrint("Unknown device type: 0x" + String(deviceType, HEX)); debugPrint("Unknown device type: 0x" + String(deviceType, HEX));
return false; return false;
} }
if (parseOk && deviceInfo->data) { if (parseOk && deviceInfo->data) {
deviceInfo->data->dataValid = true; deviceInfo->data->dataValid = true;
// Call appropriate callback // Call appropriate callback
if (callback) { if (callback) {
switch (deviceType) { switch (deviceType) {
@@ -287,7 +340,7 @@ bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
} }
} }
} }
return parseOk; return parseOk;
} }
@@ -297,27 +350,27 @@ bool VictronBLE::decryptAdvertisement(const uint8_t* encrypted, size_t encLen,
uint8_t* decrypted) { uint8_t* decrypted) {
mbedtls_aes_context aes; mbedtls_aes_context aes;
mbedtls_aes_init(&aes); mbedtls_aes_init(&aes);
// Set encryption key // Set encryption key
int ret = mbedtls_aes_setkey_enc(&aes, key, 128); int ret = mbedtls_aes_setkey_enc(&aes, key, 128);
if (ret != 0) { if (ret != 0) {
mbedtls_aes_free(&aes); mbedtls_aes_free(&aes);
return false; return false;
} }
// AES-CTR decryption // AES-CTR decryption
size_t nc_off = 0; size_t nc_off = 0;
uint8_t nonce_counter[16]; uint8_t nonce_counter[16];
uint8_t stream_block[16]; uint8_t stream_block[16];
memcpy(nonce_counter, iv, 16); memcpy(nonce_counter, iv, 16);
memset(stream_block, 0, 16); memset(stream_block, 0, 16);
ret = mbedtls_aes_crypt_ctr(&aes, encLen, &nc_off, nonce_counter, ret = mbedtls_aes_crypt_ctr(&aes, encLen, &nc_off, nonce_counter,
stream_block, encrypted, decrypted); stream_block, encrypted, decrypted);
mbedtls_aes_free(&aes); mbedtls_aes_free(&aes);
return (ret == 0); return (ret == 0);
} }
@@ -327,59 +380,59 @@ bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarCharger
debugPrint("Solar charger data too short"); debugPrint("Solar charger data too short");
return false; return false;
} }
// Byte 0: Charge state // Byte 0: Charge state
result.chargeState = (SolarChargerState)data[0]; result.chargeState = (SolarChargerState)data[0];
// Bytes 1-2: Battery voltage (10 mV units) // Bytes 1-2: Battery voltage (10 mV units)
uint16_t vBat = data[1] | (data[2] << 8); uint16_t vBat = data[1] | (data[2] << 8);
result.batteryVoltage = vBat * 0.01f; result.batteryVoltage = vBat * 0.01f;
// Bytes 3-4: Battery current (10 mA units, signed) // Bytes 3-4: Battery current (10 mA units, signed)
int16_t iBat = (int16_t)(data[3] | (data[4] << 8)); int16_t iBat = (int16_t)(data[3] | (data[4] << 8));
result.batteryCurrent = iBat * 0.01f; result.batteryCurrent = iBat * 0.01f;
// Bytes 5-6: Yield today (10 Wh units) // Bytes 5-6: Yield today (10 Wh units)
uint16_t yield = data[5] | (data[6] << 8); uint16_t yield = data[5] | (data[6] << 8);
result.yieldToday = yield * 10; result.yieldToday = yield * 10;
// Bytes 7-8: PV power (1 W units) // Bytes 7-8: PV power (1 W units)
uint16_t pvPower = data[7] | (data[8] << 8); uint16_t pvPower = data[7] | (data[8] << 8);
result.panelPower = pvPower; result.panelPower = pvPower;
// Bytes 9-10: Load current (10 mA units) // Bytes 9-10: Load current (10 mA units)
uint16_t iLoad = data[9] | (data[10] << 8); uint16_t iLoad = data[9] | (data[10] << 8);
if (iLoad != 0xFFFF) { // 0xFFFF means no load output if (iLoad != 0xFFFF) { // 0xFFFF means no load output
result.loadCurrent = iLoad * 0.01f; result.loadCurrent = iLoad * 0.01f;
} }
// Calculate PV voltage from power and current (if current > 0) // Calculate PV voltage from power and current (if current > 0)
if (result.batteryCurrent > 0.1f) { if (result.batteryCurrent > 0.1f) {
result.panelVoltage = result.panelPower / result.batteryCurrent; result.panelVoltage = result.panelPower / result.batteryCurrent;
} }
debugPrint("Solar Charger: " + String(result.batteryVoltage, 2) + "V, " + debugPrint("Solar Charger: " + String(result.batteryVoltage, 2) + "V, " +
String(result.batteryCurrent, 2) + "A, " + String(result.batteryCurrent, 2) + "A, " +
String(result.panelPower) + "W, State: " + String(result.chargeState)); String(result.panelPower) + "W, State: " + String(result.chargeState));
return true; return true;
} }
// Parse Battery Monitor data // Parse Battery Monitor data
bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result) { bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result) {
if (len < 15) { if (len < 15) {
debugPrint("Battery monitor data too short"); debugPrint("Battery monitor data too short");
return false; return false;
} }
// Bytes 0-1: Remaining time (1 minute units) // Bytes 0-1: Remaining time (1 minute units)
uint16_t timeRemaining = data[0] | (data[1] << 8); uint16_t timeRemaining = data[0] | (data[1] << 8);
result.remainingMinutes = timeRemaining; result.remainingMinutes = timeRemaining;
// Bytes 2-3: Battery voltage (10 mV units) // Bytes 2-3: Battery voltage (10 mV units)
uint16_t vBat = data[2] | (data[3] << 8); uint16_t vBat = data[2] | (data[3] << 8);
result.voltage = vBat * 0.01f; result.voltage = vBat * 0.01f;
// Byte 4: Alarms // Byte 4: Alarms
uint8_t alarms = data[4]; uint8_t alarms = data[4];
result.alarmLowVoltage = (alarms & 0x01) != 0; 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.alarmLowSOC = (alarms & 0x04) != 0;
result.alarmLowTemperature = (alarms & 0x10) != 0; result.alarmLowTemperature = (alarms & 0x10) != 0;
result.alarmHighTemperature = (alarms & 0x20) != 0; result.alarmHighTemperature = (alarms & 0x20) != 0;
// Bytes 5-6: Aux voltage/temperature (10 mV or 0.01K units) // Bytes 5-6: Aux voltage/temperature (10 mV or 0.01K units)
uint16_t aux = data[5] | (data[6] << 8); uint16_t aux = data[5] | (data[6] << 8);
if (aux < 3000) { // If < 30V, it's voltage 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.temperature = (aux * 0.01f) - 273.15f;
result.auxVoltage = 0; result.auxVoltage = 0;
} }
// Bytes 7-9: Battery current (22-bit signed, 1 mA units) // Bytes 7-9: Battery current (22-bit signed, 1 mA units)
int32_t current = data[7] | (data[8] << 8) | ((data[9] & 0x3F) << 16); int32_t current = data[7] | (data[8] << 8) | ((data[9] & 0x3F) << 16);
if (current & 0x200000) { // Sign extend if negative if (current & 0x200000) { // Sign extend if negative
current |= 0xFFC00000; current |= 0xFFC00000;
} }
result.current = current * 0.001f; result.current = current * 0.001f;
// Bytes 9-11: Consumed Ah (18-bit signed, 10 mAh units) // 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); int32_t consumedAh = ((data[9] & 0xC0) >> 6) | (data[10] << 2) | ((data[11] & 0xFF) << 10);
if (consumedAh & 0x20000) { // Sign extend if (consumedAh & 0x20000) { // Sign extend
consumedAh |= 0xFFFC0000; consumedAh |= 0xFFFC0000;
} }
result.consumedAh = consumedAh * 0.01f; result.consumedAh = consumedAh * 0.01f;
// Bytes 12-13: SOC (10 = 1.0%) // Bytes 12-13: SOC (10 = 1.0%)
uint16_t soc = data[12] | ((data[13] & 0x03) << 8); uint16_t soc = data[12] | ((data[13] & 0x03) << 8);
result.soc = soc * 0.1f; result.soc = soc * 0.1f;
debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " + debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " +
String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%"); String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%");
return true; return true;
} }
@@ -428,35 +481,35 @@ bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& re
debugPrint("Inverter data too short"); debugPrint("Inverter data too short");
return false; return false;
} }
// Byte 0: Device state // Byte 0: Device state
result.state = data[0]; result.state = data[0];
// Bytes 1-2: Battery voltage (10 mV units) // Bytes 1-2: Battery voltage (10 mV units)
uint16_t vBat = data[1] | (data[2] << 8); uint16_t vBat = data[1] | (data[2] << 8);
result.batteryVoltage = vBat * 0.01f; result.batteryVoltage = vBat * 0.01f;
// Bytes 3-4: Battery current (10 mA units, signed) // Bytes 3-4: Battery current (10 mA units, signed)
int16_t iBat = (int16_t)(data[3] | (data[4] << 8)); int16_t iBat = (int16_t)(data[3] | (data[4] << 8));
result.batteryCurrent = iBat * 0.01f; result.batteryCurrent = iBat * 0.01f;
// Bytes 5-7: AC Power (1 W units, signed 24-bit) // Bytes 5-7: AC Power (1 W units, signed 24-bit)
int32_t acPower = data[5] | (data[6] << 8) | (data[7] << 16); int32_t acPower = data[5] | (data[6] << 8) | (data[7] << 16);
if (acPower & 0x800000) { // Sign extend if (acPower & 0x800000) { // Sign extend
acPower |= 0xFF000000; acPower |= 0xFF000000;
} }
result.acPower = acPower; result.acPower = acPower;
// Byte 8: Alarms // Byte 8: Alarms
uint8_t alarms = data[8]; uint8_t alarms = data[8];
result.alarmLowVoltage = (alarms & 0x01) != 0; result.alarmLowVoltage = (alarms & 0x01) != 0;
result.alarmHighVoltage = (alarms & 0x02) != 0; result.alarmHighVoltage = (alarms & 0x02) != 0;
result.alarmHighTemperature = (alarms & 0x04) != 0; result.alarmHighTemperature = (alarms & 0x04) != 0;
result.alarmOverload = (alarms & 0x08) != 0; result.alarmOverload = (alarms & 0x08) != 0;
debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " + debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " +
String(result.acPower) + "W, State: " + String(result.state)); String(result.acPower) + "W, State: " + String(result.state));
return true; return true;
} }
@@ -466,28 +519,28 @@ bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConvert
debugPrint("DC-DC converter data too short"); debugPrint("DC-DC converter data too short");
return false; return false;
} }
// Byte 0: Charge state // Byte 0: Charge state
result.chargeState = data[0]; result.chargeState = data[0];
// Bytes 1-2: Input voltage (10 mV units) // Bytes 1-2: Input voltage (10 mV units)
uint16_t vIn = data[1] | (data[2] << 8); uint16_t vIn = data[1] | (data[2] << 8);
result.inputVoltage = vIn * 0.01f; result.inputVoltage = vIn * 0.01f;
// Bytes 3-4: Output voltage (10 mV units) // Bytes 3-4: Output voltage (10 mV units)
uint16_t vOut = data[3] | (data[4] << 8); uint16_t vOut = data[3] | (data[4] << 8);
result.outputVoltage = vOut * 0.01f; result.outputVoltage = vOut * 0.01f;
// Bytes 5-6: Output current (10 mA units) // Bytes 5-6: Output current (10 mA units)
uint16_t iOut = data[5] | (data[6] << 8); uint16_t iOut = data[5] | (data[6] << 8);
result.outputCurrent = iOut * 0.01f; result.outputCurrent = iOut * 0.01f;
// Byte 7: Error code // Byte 7: Error code
result.errorCode = data[7]; result.errorCode = data[7];
debugPrint("DC-DC Converter: In=" + String(result.inputVoltage, 2) + "V, Out=" + debugPrint("DC-DC Converter: In=" + String(result.inputVoltage, 2) + "V, Out=" +
String(result.outputVoltage, 2) + "V, " + String(result.outputCurrent, 2) + "A"); String(result.outputVoltage, 2) + "V, " + String(result.outputCurrent, 2) + "A");
return true; return true;
} }
@@ -495,8 +548,8 @@ bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConvert
bool VictronBLE::getSolarChargerData(String macAddress, SolarChargerData& data) { bool VictronBLE::getSolarChargerData(String macAddress, SolarChargerData& data) {
String normalizedMAC = normalizeMAC(macAddress); String normalizedMAC = normalizeMAC(macAddress);
auto it = devices.find(normalizedMAC); 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) { it->second->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
data = *(SolarChargerData*)it->second->data; data = *(SolarChargerData*)it->second->data;
return data.dataValid; return data.dataValid;
@@ -507,7 +560,7 @@ bool VictronBLE::getSolarChargerData(String macAddress, SolarChargerData& data)
bool VictronBLE::getBatteryMonitorData(String macAddress, BatteryMonitorData& data) { bool VictronBLE::getBatteryMonitorData(String macAddress, BatteryMonitorData& data) {
String normalizedMAC = normalizeMAC(macAddress); String normalizedMAC = normalizeMAC(macAddress);
auto it = devices.find(normalizedMAC); 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_BATTERY_MONITOR) { it->second->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) {
data = *(BatteryMonitorData*)it->second->data; data = *(BatteryMonitorData*)it->second->data;
@@ -519,7 +572,7 @@ bool VictronBLE::getBatteryMonitorData(String macAddress, BatteryMonitorData& da
bool VictronBLE::getInverterData(String macAddress, InverterData& data) { bool VictronBLE::getInverterData(String macAddress, InverterData& data) {
String normalizedMAC = normalizeMAC(macAddress); String normalizedMAC = normalizeMAC(macAddress);
auto it = devices.find(normalizedMAC); 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_INVERTER) { it->second->data->deviceType == DEVICE_TYPE_INVERTER) {
data = *(InverterData*)it->second->data; data = *(InverterData*)it->second->data;
@@ -531,7 +584,7 @@ bool VictronBLE::getInverterData(String macAddress, InverterData& data) {
bool VictronBLE::getDCDCConverterData(String macAddress, DCDCConverterData& data) { bool VictronBLE::getDCDCConverterData(String macAddress, DCDCConverterData& data) {
String normalizedMAC = normalizeMAC(macAddress); String normalizedMAC = normalizeMAC(macAddress);
auto it = devices.find(normalizedMAC); 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_DCDC_CONVERTER) { it->second->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) {
data = *(DCDCConverterData*)it->second->data; data = *(DCDCConverterData*)it->second->data;
@@ -543,13 +596,13 @@ bool VictronBLE::getDCDCConverterData(String macAddress, DCDCConverterData& data
// Get devices by type // Get devices by type
std::vector<String> VictronBLE::getDevicesByType(VictronDeviceType type) { std::vector<String> VictronBLE::getDevicesByType(VictronDeviceType type) {
std::vector<String> result; std::vector<String> result;
for (const auto& pair : devices) { for (const auto& pair : devices) {
if (pair.second->data && pair.second->data->deviceType == type) { if (pair.second->data && pair.second->data->deviceType == type) {
result.push_back(pair.first); result.push_back(pair.first);
} }
} }
return result; return result;
} }
@@ -577,7 +630,7 @@ bool VictronBLE::hexStringToBytes(const String& hex, uint8_t* bytes, size_t len)
if (hex.length() != len * 2) { if (hex.length() != len * 2) {
return false; return false;
} }
for (size_t i = 0; i < len; i++) { for (size_t i = 0; i < len; i++) {
String byteStr = hex.substring(i * 2, i * 2 + 2); String byteStr = hex.substring(i * 2, i * 2 + 2);
char* endPtr; char* endPtr;
@@ -586,7 +639,7 @@ bool VictronBLE::hexStringToBytes(const String& hex, uint8_t* bytes, size_t len)
return false; return false;
} }
} }
return true; return true;
} }
@@ -604,7 +657,9 @@ String VictronBLE::macAddressToString(BLEAddress address) {
String VictronBLE::normalizeMAC(String mac) { String VictronBLE::normalizeMAC(String mac) {
String normalized = mac; String normalized = mac;
normalized.toLowerCase(); normalized.toLowerCase();
normalized.replace("-", ":"); // XXX - is this right, was - to : but not consistent location of pairs or not
normalized.replace("-", "");
normalized.replace(":", "");
return normalized; 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) { void VictronBLE::debugPrintHex(const char* label, const uint8_t* data, size_t len) {
if (!debugEnabled) return; if (!debugEnabled) return;
Serial.print("[VictronBLE] "); Serial.print("[VictronBLE] ");
Serial.print(label); Serial.print(label);
Serial.print(": "); Serial.print(": ");