From 84d153c9a8df096c414de372eec3629e789f8194 Mon Sep 17 00:00:00 2001 From: Scott Penrose Date: Sat, 28 Feb 2026 13:34:20 +1100 Subject: [PATCH] Single callback version - vastly simplified. --- examples/FakeRepeater/src/main.cpp | 2 - examples/Logger/src/main.cpp | 123 +++--- examples/MultiDevice/src/main.cpp | 326 +++++--------- examples/Receiver/src/main.cpp | 6 +- examples/Repeater/src/main.cpp | 70 ++- src/VictronBLE.cpp | 687 ++++++++--------------------- src/VictronBLE.h | 396 ++++++++--------- 7 files changed, 573 insertions(+), 1037 deletions(-) diff --git a/examples/FakeRepeater/src/main.cpp b/examples/FakeRepeater/src/main.cpp index abbc334..b493c8e 100644 --- a/examples/FakeRepeater/src/main.cpp +++ b/examples/FakeRepeater/src/main.cpp @@ -17,7 +17,6 @@ struct __attribute__((packed)) SolarChargerPacket { uint8_t chargeState; float batteryVoltage; // V float batteryCurrent; // A - float panelVoltage; // V float panelPower; // W uint16_t yieldToday; // Wh float loadCurrent; // A @@ -74,7 +73,6 @@ void loop() { pkt.chargeState = (sendCount % 4) + 3; // Cycle through Bulk(3), Absorption(4), Float(5), Storage(6) pkt.batteryVoltage = 51.0f + (sendCount % 20) * 0.15f; pkt.batteryCurrent = 2.0f + (sendCount % 10) * 0.5f; - pkt.panelVoltage = 65.0f + (sendCount % 15) * 0.8f; pkt.panelPower = pkt.batteryCurrent * pkt.batteryVoltage; pkt.yieldToday = 100 + sendCount * 10; pkt.loadCurrent = 0; diff --git a/examples/Logger/src/main.cpp b/examples/Logger/src/main.cpp index d4a080d..aacb0c4 100644 --- a/examples/Logger/src/main.cpp +++ b/examples/Logger/src/main.cpp @@ -3,8 +3,7 @@ * * Demonstrates change-detection logging for Solar Charger data. * Only logs to serial when a value changes (ignoring RSSI), or once - * per minute if nothing has changed. This keeps serial output quiet - * and is useful for long-running monitoring / data logging. + * per minute if nothing has changed. * * Setup: * 1. Get your device encryption keys from the VictronConnect app @@ -16,13 +15,11 @@ VictronBLE victron; -// Tracks last-logged values per device for change detection struct SolarChargerSnapshot { bool valid = false; - SolarChargerState chargeState; + uint8_t chargeState; float batteryVoltage; float batteryCurrent; - float panelVoltage; float panelPower; uint16_t yieldToday; float loadCurrent; @@ -30,26 +27,26 @@ struct SolarChargerSnapshot { uint32_t packetsSinceLastLog = 0; }; -// Store a snapshot per device (index by MAC string) static const int MAX_DEVICES = 4; -static String deviceMACs[MAX_DEVICES]; +static char deviceMACs[MAX_DEVICES][VICTRON_MAC_LEN]; static SolarChargerSnapshot snapshots[MAX_DEVICES]; static int deviceCount = 0; -static const unsigned long LOG_INTERVAL_MS = 60000; // 1 minute +static const unsigned long LOG_INTERVAL_MS = 60000; -static int findOrAddDevice(const String& mac) { +static int findOrAddDevice(const char* mac) { for (int i = 0; i < deviceCount; i++) { - if (deviceMACs[i] == mac) return i; + if (strcmp(deviceMACs[i], mac) == 0) return i; } if (deviceCount < MAX_DEVICES) { - deviceMACs[deviceCount] = mac; + strncpy(deviceMACs[deviceCount], mac, VICTRON_MAC_LEN - 1); + deviceMACs[deviceCount][VICTRON_MAC_LEN - 1] = '\0'; return deviceCount++; } return -1; } -static String chargeStateName(SolarChargerState state) { +static const char* chargeStateName(uint8_t state) { switch (state) { case CHARGER_OFF: return "Off"; case CHARGER_LOW_POWER: return "Low Power"; @@ -66,66 +63,58 @@ static String chargeStateName(SolarChargerState state) { } } -static void logData(const SolarChargerData& data, const char* reason, uint32_t packets) { - Serial.println("[" + data.deviceName + "] " + reason + - " pkts:" + String(packets) + - " | State:" + chargeStateName(data.chargeState) + - " Batt:" + String(data.batteryVoltage, 2) + "V" + - " " + String(data.batteryCurrent, 2) + "A" + - " PV:" + String(data.panelVoltage, 1) + "V" + - " " + String(data.panelPower, 0) + "W" + - " Yield:" + String(data.yieldToday) + "Wh" + - (data.loadCurrent > 0 ? " Load:" + String(data.loadCurrent, 2) + "A" : "")); +static void logData(const VictronDevice* dev, const VictronSolarData& s, + const char* reason, uint32_t packets) { + Serial.printf("[%s] %s pkts:%lu | State:%s Batt:%.2fV %.2fA PV:%.0fW Yield:%uWh", + dev->name, reason, packets, + chargeStateName(s.chargeState), + s.batteryVoltage, s.batteryCurrent, + s.panelPower, s.yieldToday); + if (s.loadCurrent > 0) + Serial.printf(" Load:%.2fA", s.loadCurrent); + Serial.println(); } -class LoggerCallback : public VictronDeviceCallback { -public: - void onSolarChargerData(const SolarChargerData& data) override { - int idx = findOrAddDevice(data.macAddress); - if (idx < 0) return; +void onVictronData(const VictronDevice* dev) { + if (dev->deviceType != DEVICE_TYPE_SOLAR_CHARGER) return; + const auto& s = dev->solar; - SolarChargerSnapshot& prev = snapshots[idx]; - unsigned long now = millis(); - prev.packetsSinceLastLog++; + int idx = findOrAddDevice(dev->mac); + if (idx < 0) return; - if (!prev.valid) { - // First reading - always log - logData(data, "INIT", prev.packetsSinceLastLog); + SolarChargerSnapshot& prev = snapshots[idx]; + unsigned long now = millis(); + prev.packetsSinceLastLog++; + + if (!prev.valid) { + logData(dev, s, "INIT", prev.packetsSinceLastLog); + } else { + bool changed = (prev.chargeState != s.chargeState) || + (prev.batteryVoltage != s.batteryVoltage) || + (prev.batteryCurrent != s.batteryCurrent) || + (prev.panelPower != s.panelPower) || + (prev.yieldToday != s.yieldToday) || + (prev.loadCurrent != s.loadCurrent); + + if (changed) { + logData(dev, s, "CHG", prev.packetsSinceLastLog); + } else if (now - prev.lastLogTime >= LOG_INTERVAL_MS) { + logData(dev, s, "HEARTBEAT", prev.packetsSinceLastLog); } else { - // Check for changes (everything except RSSI) - bool changed = false; - if (prev.chargeState != data.chargeState) changed = true; - if (prev.batteryVoltage != data.batteryVoltage) changed = true; - if (prev.batteryCurrent != data.batteryCurrent) changed = true; - if (prev.panelVoltage != data.panelVoltage) changed = true; - if (prev.panelPower != data.panelPower) changed = true; - if (prev.yieldToday != data.yieldToday) changed = true; - if (prev.loadCurrent != data.loadCurrent) changed = true; - - if (changed) { - logData(data, "CHG", prev.packetsSinceLastLog); - } else if (now - prev.lastLogTime >= LOG_INTERVAL_MS) { - logData(data, "HEARTBEAT", prev.packetsSinceLastLog); - } else { - return; // Nothing to log - } + return; } - - // Update snapshot - prev.packetsSinceLastLog = 0; - prev.valid = true; - prev.chargeState = data.chargeState; - prev.batteryVoltage = data.batteryVoltage; - prev.batteryCurrent = data.batteryCurrent; - prev.panelVoltage = data.panelVoltage; - prev.panelPower = data.panelPower; - prev.yieldToday = data.yieldToday; - prev.loadCurrent = data.loadCurrent; - prev.lastLogTime = now; } -}; -LoggerCallback callback; + prev.packetsSinceLastLog = 0; + prev.valid = true; + prev.chargeState = s.chargeState; + prev.batteryVoltage = s.batteryVoltage; + prev.batteryCurrent = s.batteryCurrent; + prev.panelPower = s.panelPower; + prev.yieldToday = s.yieldToday; + prev.loadCurrent = s.loadCurrent; + prev.lastLogTime = now; +} void setup() { Serial.begin(115200); @@ -135,14 +124,12 @@ void setup() { if (!victron.begin(5)) { Serial.println("ERROR: Failed to initialize VictronBLE!"); - Serial.println(victron.getLastError()); while (1) delay(1000); } victron.setDebug(false); - victron.setCallback(&callback); + victron.setCallback(onVictronData); - // Add your devices here victron.addDevice( "Rainbow48V", "E4:05:42:34:14:F3", @@ -157,7 +144,7 @@ void setup() { DEVICE_TYPE_SOLAR_CHARGER ); - Serial.println("Configured " + String(victron.getDeviceCount()) + " devices"); + Serial.printf("Configured %d devices\n", (int)victron.getDeviceCount()); Serial.println("Logging on change, or every 60s heartbeat\n"); } diff --git a/examples/MultiDevice/src/main.cpp b/examples/MultiDevice/src/main.cpp index bccd870..75d3655 100644 --- a/examples/MultiDevice/src/main.cpp +++ b/examples/MultiDevice/src/main.cpp @@ -1,154 +1,129 @@ /** - * VictronBLE Example + * VictronBLE Multi-Device Example * - * This example demonstrates how to use the VictronBLE library to read data - * from multiple Victron devices simultaneously. - * - * Hardware Requirements: - * - ESP32 board - * - Victron devices with BLE (SmartSolar, SmartShunt, etc.) + * Demonstrates reading data from multiple Victron device types via BLE. * * Setup: - * 1. Get your device encryption keys from the VictronConnect app: - * - Open VictronConnect - * - Connect to your device - * - Go to Settings > Product Info - * - Enable "Instant readout via Bluetooth" - * - Click "Show" next to "Instant readout details" - * - Copy the encryption key (32 hex characters) - * - * 2. Update the device configurations below with your devices' MAC addresses - * and encryption keys + * 1. Get your device encryption keys from VictronConnect app + * (Settings > Product Info > Instant readout via Bluetooth > Show) + * 2. Update the device configurations below with your MAC and key */ #include #include "VictronBLE.h" -// Create VictronBLE instance VictronBLE victron; -// Device callback class - gets called when new data arrives -class MyVictronCallback : public VictronDeviceCallback { -public: - uint32_t solarChargerCount = 0; - uint32_t batteryMonitorCount = 0; - uint32_t inverterCount = 0; - uint32_t dcdcConverterCount = 0; +static uint32_t solarChargerCount = 0; +static uint32_t batteryMonitorCount = 0; +static uint32_t inverterCount = 0; +static uint32_t dcdcConverterCount = 0; - void onSolarChargerData(const SolarChargerData& data) override { - solarChargerCount++; - Serial.println("\n=== Solar Charger: " + data.deviceName + " (#" + String(solarChargerCount) + ") ==="); - Serial.println("MAC: " + data.macAddress); - Serial.println("RSSI: " + String(data.rssi) + " dBm"); - Serial.println("State: " + getChargeStateName(data.chargeState)); - Serial.println("Battery: " + String(data.batteryVoltage, 2) + " V"); - Serial.println("Current: " + String(data.batteryCurrent, 2) + " A"); - Serial.println("Panel Voltage: " + String(data.panelVoltage, 1) + " V"); - Serial.println("Panel Power: " + String(data.panelPower) + " W"); - Serial.println("Yield Today: " + String(data.yieldToday) + " Wh"); - if (data.loadCurrent > 0) { - Serial.println("Load Current: " + String(data.loadCurrent, 2) + " A"); - } - Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago"); +static const char* chargeStateName(uint8_t state) { + switch (state) { + case CHARGER_OFF: return "Off"; + case CHARGER_LOW_POWER: return "Low Power"; + case CHARGER_FAULT: return "Fault"; + case CHARGER_BULK: return "Bulk"; + case CHARGER_ABSORPTION: return "Absorption"; + case CHARGER_FLOAT: return "Float"; + case CHARGER_STORAGE: return "Storage"; + case CHARGER_EQUALIZE: return "Equalize"; + case CHARGER_INVERTING: return "Inverting"; + case CHARGER_POWER_SUPPLY: return "Power Supply"; + case CHARGER_EXTERNAL_CONTROL: return "External Control"; + default: return "Unknown"; } +} - void onBatteryMonitorData(const BatteryMonitorData& data) override { - batteryMonitorCount++; - Serial.println("\n=== Battery Monitor: " + data.deviceName + " (#" + String(batteryMonitorCount) + ") ==="); - Serial.println("MAC: " + data.macAddress); - Serial.println("RSSI: " + String(data.rssi) + " dBm"); - Serial.println("Voltage: " + String(data.voltage, 2) + " V"); - Serial.println("Current: " + String(data.current, 2) + " A"); - Serial.println("SOC: " + String(data.soc, 1) + " %"); - Serial.println("Consumed: " + String(data.consumedAh, 2) + " Ah"); - - if (data.remainingMinutes < 65535) { - int hours = data.remainingMinutes / 60; - int mins = data.remainingMinutes % 60; - Serial.println("Time Remaining: " + String(hours) + "h " + String(mins) + "m"); +void onVictronData(const VictronDevice* dev) { + switch (dev->deviceType) { + case DEVICE_TYPE_SOLAR_CHARGER: { + const auto& s = dev->solar; + solarChargerCount++; + Serial.printf("\n=== Solar Charger: %s (#%lu) ===\n", dev->name, solarChargerCount); + Serial.printf("MAC: %s\n", dev->mac); + Serial.printf("RSSI: %d dBm\n", dev->rssi); + Serial.printf("State: %s\n", chargeStateName(s.chargeState)); + Serial.printf("Battery: %.2f V\n", s.batteryVoltage); + Serial.printf("Current: %.2f A\n", s.batteryCurrent); + Serial.printf("Panel Power: %.0f W\n", s.panelPower); + Serial.printf("Yield Today: %u Wh\n", s.yieldToday); + if (s.loadCurrent > 0) + Serial.printf("Load Current: %.2f A\n", s.loadCurrent); + Serial.printf("Last Update: %lus ago\n", (millis() - dev->lastUpdate) / 1000); + break; } - - if (data.temperature > 0) { - Serial.println("Temperature: " + String(data.temperature, 1) + " °C"); + case DEVICE_TYPE_BATTERY_MONITOR: { + const auto& b = dev->battery; + batteryMonitorCount++; + Serial.printf("\n=== Battery Monitor: %s (#%lu) ===\n", dev->name, batteryMonitorCount); + Serial.printf("MAC: %s\n", dev->mac); + Serial.printf("RSSI: %d dBm\n", dev->rssi); + Serial.printf("Voltage: %.2f V\n", b.voltage); + Serial.printf("Current: %.2f A\n", b.current); + Serial.printf("SOC: %.1f %%\n", b.soc); + Serial.printf("Consumed: %.2f Ah\n", b.consumedAh); + if (b.remainingMinutes < 65535) + Serial.printf("Time Remaining: %dh %dm\n", b.remainingMinutes / 60, b.remainingMinutes % 60); + if (b.temperature > 0) + Serial.printf("Temperature: %.1f C\n", b.temperature); + if (b.auxVoltage > 0) + Serial.printf("Aux Voltage: %.2f V\n", b.auxVoltage); + if (b.alarmLowVoltage || b.alarmHighVoltage || b.alarmLowSOC || + b.alarmLowTemperature || b.alarmHighTemperature) { + Serial.print("ALARMS:"); + if (b.alarmLowVoltage) Serial.print(" LOW-V"); + if (b.alarmHighVoltage) Serial.print(" HIGH-V"); + if (b.alarmLowSOC) Serial.print(" LOW-SOC"); + if (b.alarmLowTemperature) Serial.print(" LOW-TEMP"); + if (b.alarmHighTemperature) Serial.print(" HIGH-TEMP"); + Serial.println(); + } + Serial.printf("Last Update: %lus ago\n", (millis() - dev->lastUpdate) / 1000); + break; } - if (data.auxVoltage > 0) { - Serial.println("Aux Voltage: " + String(data.auxVoltage, 2) + " V"); + case DEVICE_TYPE_INVERTER: { + const auto& inv = dev->inverter; + inverterCount++; + Serial.printf("\n=== Inverter/Charger: %s (#%lu) ===\n", dev->name, inverterCount); + Serial.printf("MAC: %s\n", dev->mac); + Serial.printf("RSSI: %d dBm\n", dev->rssi); + Serial.printf("Battery: %.2f V\n", inv.batteryVoltage); + Serial.printf("Current: %.2f A\n", inv.batteryCurrent); + Serial.printf("AC Power: %.0f W\n", inv.acPower); + Serial.printf("State: %d\n", inv.state); + if (inv.alarmLowVoltage || inv.alarmHighVoltage || + inv.alarmHighTemperature || inv.alarmOverload) { + Serial.print("ALARMS:"); + if (inv.alarmLowVoltage) Serial.print(" LOW-V"); + if (inv.alarmHighVoltage) Serial.print(" HIGH-V"); + if (inv.alarmHighTemperature) Serial.print(" TEMP"); + if (inv.alarmOverload) Serial.print(" OVERLOAD"); + Serial.println(); + } + Serial.printf("Last Update: %lus ago\n", (millis() - dev->lastUpdate) / 1000); + break; } - - // Print alarms - if (data.alarmLowVoltage || data.alarmHighVoltage || data.alarmLowSOC || - data.alarmLowTemperature || data.alarmHighTemperature) { - Serial.print("ALARMS: "); - if (data.alarmLowVoltage) Serial.print("LOW-V "); - if (data.alarmHighVoltage) Serial.print("HIGH-V "); - if (data.alarmLowSOC) Serial.print("LOW-SOC "); - if (data.alarmLowTemperature) Serial.print("LOW-TEMP "); - if (data.alarmHighTemperature) Serial.print("HIGH-TEMP "); - Serial.println(); + case DEVICE_TYPE_DCDC_CONVERTER: { + const auto& dc = dev->dcdc; + dcdcConverterCount++; + Serial.printf("\n=== DC-DC Converter: %s (#%lu) ===\n", dev->name, dcdcConverterCount); + Serial.printf("MAC: %s\n", dev->mac); + Serial.printf("RSSI: %d dBm\n", dev->rssi); + Serial.printf("Input: %.2f V\n", dc.inputVoltage); + Serial.printf("Output: %.2f V\n", dc.outputVoltage); + Serial.printf("Current: %.2f A\n", dc.outputCurrent); + Serial.printf("State: %d\n", dc.chargeState); + if (dc.errorCode != 0) + Serial.printf("Error Code: %d\n", dc.errorCode); + Serial.printf("Last Update: %lus ago\n", (millis() - dev->lastUpdate) / 1000); + break; } - - Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago"); + default: + break; } - - void onInverterData(const InverterData& data) override { - inverterCount++; - Serial.println("\n=== Inverter/Charger: " + data.deviceName + " (#" + String(inverterCount) + ") ==="); - Serial.println("MAC: " + data.macAddress); - Serial.println("RSSI: " + String(data.rssi) + " dBm"); - Serial.println("Battery: " + String(data.batteryVoltage, 2) + " V"); - Serial.println("Current: " + String(data.batteryCurrent, 2) + " A"); - Serial.println("AC Power: " + String(data.acPower) + " W"); - Serial.println("State: " + String(data.state)); - - // Print alarms - if (data.alarmLowVoltage || data.alarmHighVoltage || - data.alarmHighTemperature || data.alarmOverload) { - Serial.print("ALARMS: "); - if (data.alarmLowVoltage) Serial.print("LOW-V "); - if (data.alarmHighVoltage) Serial.print("HIGH-V "); - if (data.alarmHighTemperature) Serial.print("TEMP "); - if (data.alarmOverload) Serial.print("OVERLOAD "); - Serial.println(); - } - - Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago"); - } - - void onDCDCConverterData(const DCDCConverterData& data) override { - dcdcConverterCount++; - Serial.println("\n=== DC-DC Converter: " + data.deviceName + " (#" + String(dcdcConverterCount) + ") ==="); - Serial.println("MAC: " + data.macAddress); - Serial.println("RSSI: " + String(data.rssi) + " dBm"); - Serial.println("Input: " + String(data.inputVoltage, 2) + " V"); - Serial.println("Output: " + String(data.outputVoltage, 2) + " V"); - Serial.println("Current: " + String(data.outputCurrent, 2) + " A"); - Serial.println("State: " + String(data.chargeState)); - if (data.errorCode != 0) { - Serial.println("Error Code: " + String(data.errorCode)); - } - Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago"); - } - -private: - String getChargeStateName(SolarChargerState state) { - switch (state) { - case CHARGER_OFF: return "Off"; - case CHARGER_LOW_POWER: return "Low Power"; - case CHARGER_FAULT: return "Fault"; - case CHARGER_BULK: return "Bulk"; - case CHARGER_ABSORPTION: return "Absorption"; - case CHARGER_FLOAT: return "Float"; - case CHARGER_STORAGE: return "Storage"; - case CHARGER_EQUALIZE: return "Equalize"; - case CHARGER_INVERTING: return "Inverting"; - case CHARGER_POWER_SUPPLY: return "Power Supply"; - case CHARGER_EXTERNAL_CONTROL: return "External Control"; - default: return "Unknown"; - } - } -}; - -MyVictronCallback callback; +} void setup() { Serial.begin(115200); @@ -158,102 +133,33 @@ void setup() { Serial.println("VictronBLE Multi-Device Example"); Serial.println("=================================\n"); - // Initialize VictronBLE with 5 second scan duration if (!victron.begin(5)) { Serial.println("ERROR: Failed to initialize VictronBLE!"); - Serial.println(victron.getLastError()); while (1) delay(1000); } - // Enable debug output (optional) victron.setDebug(false); - - // Set callback for data updates - victron.setCallback(&callback); - - // Add your devices here - // Replace with your actual MAC addresses and encryption keys - - // CORRECT in Alternative - // Rainbow48V at MAC e4:05:42:34:14:f3 - - // Temporary - Scott Example - victron.addDevice( - "Rainbow48V", // Device name - "E4:05:42:34:14:F3", // MAC address - "0ec3adf7433dd61793ff2f3b8ad32ed8", // Encryption key (32 hex chars) - DEVICE_TYPE_SOLAR_CHARGER // Device type - ); + victron.setCallback(onVictronData); victron.addDevice( - "ScottTrailer", // Device name - "e64559783cfb", - "3fa658aded4f309b9bc17a2318cb1f56", - DEVICE_TYPE_SOLAR_CHARGER // Device type - ); - - // Example: Solar Charger #1 - /* - victron.addDevice( - "MPPT 100/30", // Device name - "E7:48:D4:28:B7:9C", // MAC address - "0df4d0395b7d1a876c0c33ecb9e70dcd", // Encryption key (32 hex chars) - DEVICE_TYPE_SOLAR_CHARGER // Device type - ); - */ - - // Example: Solar Charger #2 - /* - victron.addDevice( - "MPPT 75/15", - "AA:BB:CC:DD:EE:FF", - "1234567890abcdef1234567890abcdef", + "Rainbow48V", + "E4:05:42:34:14:F3", + "0ec3adf7433dd61793ff2f3b8ad32ed8", DEVICE_TYPE_SOLAR_CHARGER ); - */ - // Example: Battery Monitor (SmartShunt) - /* victron.addDevice( - "SmartShunt", - "11:22:33:44:55:66", - "fedcba0987654321fedcba0987654321", - DEVICE_TYPE_BATTERY_MONITOR + "ScottTrailer", + "e64559783cfb", + "3fa658aded4f309b9bc17a2318cb1f56", + DEVICE_TYPE_SOLAR_CHARGER ); - */ - // Example: Inverter/Charger - /* - victron.addDevice( - "MultiPlus", - "99:88:77:66:55:44", - "abcdefabcdefabcdefabcdefabcdefab", - DEVICE_TYPE_INVERTER - ); - */ - - Serial.println("Configured " + String(victron.getDeviceCount()) + " devices"); + Serial.printf("Configured %d devices\n", (int)victron.getDeviceCount()); Serial.println("\nStarting BLE scan...\n"); } void loop() { - // Process BLE scanning and data updates victron.loop(); - - // Optional: You can also manually query device data - // This is useful if you're not using callbacks - /* - SolarChargerData solarData; - if (victron.getSolarChargerData("E7:48:D4:28:B7:9C", solarData)) { - // Do something with solarData - } - - BatteryMonitorData batteryData; - if (victron.getBatteryMonitorData("11:22:33:44:55:66", batteryData)) { - // Do something with batteryData - } - */ - - // Add a small delay to avoid overwhelming the serial output delay(100); } diff --git a/examples/Receiver/src/main.cpp b/examples/Receiver/src/main.cpp index d10ea6b..15df90f 100644 --- a/examples/Receiver/src/main.cpp +++ b/examples/Receiver/src/main.cpp @@ -21,7 +21,6 @@ struct __attribute__((packed)) SolarChargerPacket { uint8_t chargeState; float batteryVoltage; // V float batteryCurrent; // A - float panelVoltage; // V float panelPower; // W uint16_t yieldToday; // Wh float loadCurrent; // A @@ -82,13 +81,12 @@ void onDataRecv(const uint8_t* senderMac, const uint8_t* data, int len) { memcpy(name, pkt->deviceName, 16); name[16] = '\0'; - Serial.printf("[RX #%lu] %s | State:%s Batt:%.2fV %.2fA PV:%.1fV %.0fW Yield:%uWh", + Serial.printf("[RX #%lu] %s | State:%s Batt:%.2fV %.2fA PV:%.0fW Yield:%uWh", recvCount, name, chargeStateName(pkt->chargeState), pkt->batteryVoltage, pkt->batteryCurrent, - pkt->panelVoltage, pkt->panelPower, pkt->yieldToday); @@ -202,7 +200,7 @@ void loop() { M5.Lcd.printf("Batt: %.2fA\n", pkt.batteryCurrent); // Row 3: PV - M5.Lcd.printf("PV: %.1fV %.0fW\n", pkt.panelVoltage, pkt.panelPower); + M5.Lcd.printf("PV: %.0fW\n", pkt.panelPower); // Row 4: yield + load M5.Lcd.printf("Yield: %uWh", pkt.yieldToday); diff --git a/examples/Repeater/src/main.cpp b/examples/Repeater/src/main.cpp index ea9a513..f8a0287 100644 --- a/examples/Repeater/src/main.cpp +++ b/examples/Repeater/src/main.cpp @@ -2,7 +2,7 @@ * VictronBLE Repeater Example * * Collects Solar Charger data via BLE and transmits the latest - * readings over ESPNow broadcast every 30 seconds. Place this ESP32 + * readings over ESPNow broadcast every 5 seconds. Place this ESP32 * near Victron devices and use a separate Receiver ESP32 at a distance. * * ESPNow range is typically much greater than BLE (~200m+ line of sight). @@ -23,7 +23,6 @@ struct __attribute__((packed)) SolarChargerPacket { uint8_t chargeState; float batteryVoltage; // V float batteryCurrent; // A - float panelVoltage; // V float panelPower; // W uint16_t yieldToday; // Wh float loadCurrent; // A @@ -31,10 +30,8 @@ struct __attribute__((packed)) SolarChargerPacket { char deviceName[16]; // Null-terminated, truncated }; -// Broadcast address static const uint8_t BROADCAST_ADDR[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; - -static const unsigned long SEND_INTERVAL_MS = 5000; // 30 seconds +static const unsigned long SEND_INTERVAL_MS = 5000; static uint32_t sendCount = 0; static uint32_t sendFailCount = 0; @@ -49,7 +46,6 @@ static unsigned long lastSendTime = 0; VictronBLE victron; -// Find cached slot by device name, or allocate a new one static int findOrAddCached(const char* name) { for (int i = 0; i < cachedCount; i++) { if (strncmp(cachedPackets[i].deviceName, name, sizeof(cachedPackets[i].deviceName)) == 0) @@ -59,34 +55,28 @@ static int findOrAddCached(const char* name) { return -1; } -class RepeaterCallback : public VictronDeviceCallback { -public: - void onSolarChargerData(const SolarChargerData& data) override { - blePacketCount++; +void onVictronData(const VictronDevice* dev) { + if (dev->deviceType != DEVICE_TYPE_SOLAR_CHARGER) return; + blePacketCount++; + const auto& s = dev->solar; - // Build packet - SolarChargerPacket pkt; - pkt.chargeState = static_cast(data.chargeState); - pkt.batteryVoltage = data.batteryVoltage; - pkt.batteryCurrent = data.batteryCurrent; - pkt.panelVoltage = data.panelVoltage; - pkt.panelPower = data.panelPower; - pkt.yieldToday = data.yieldToday; - pkt.loadCurrent = data.loadCurrent; - pkt.rssi = data.rssi; - memset(pkt.deviceName, 0, sizeof(pkt.deviceName)); - strncpy(pkt.deviceName, data.deviceName.c_str(), sizeof(pkt.deviceName) - 1); + SolarChargerPacket pkt; + pkt.chargeState = s.chargeState; + pkt.batteryVoltage = s.batteryVoltage; + pkt.batteryCurrent = s.batteryCurrent; + pkt.panelPower = s.panelPower; + pkt.yieldToday = s.yieldToday; + pkt.loadCurrent = s.loadCurrent; + pkt.rssi = dev->rssi; + memset(pkt.deviceName, 0, sizeof(pkt.deviceName)); + strncpy(pkt.deviceName, dev->name, sizeof(pkt.deviceName) - 1); - // Cache it - int idx = findOrAddCached(pkt.deviceName); - if (idx >= 0) { - cachedPackets[idx] = pkt; - cachedValid[idx] = true; - } + int idx = findOrAddCached(pkt.deviceName); + if (idx >= 0) { + cachedPackets[idx] = pkt; + cachedValid[idx] = true; } -}; - -RepeaterCallback callback; +} void setup() { Serial.begin(115200); @@ -98,7 +88,8 @@ void setup() { WiFi.mode(WIFI_STA); WiFi.disconnect(); - Serial.println("MAC: " + WiFi.macAddress()); + Serial.print("MAC: "); + Serial.println(WiFi.macAddress()); // Init ESPNow if (esp_now_init() != ESP_OK) { @@ -106,10 +97,9 @@ void setup() { while (1) delay(1000); } - // Add broadcast peer esp_now_peer_info_t peerInfo = {}; memcpy(peerInfo.peer_addr, BROADCAST_ADDR, 6); - peerInfo.channel = 0; // Use current channel + peerInfo.channel = 0; peerInfo.encrypt = false; if (esp_now_add_peer(&peerInfo) != ESP_OK) { @@ -122,14 +112,12 @@ void setup() { // Init VictronBLE if (!victron.begin(5)) { Serial.println("ERROR: Failed to initialize VictronBLE!"); - Serial.println(victron.getLastError()); while (1) delay(1000); } victron.setDebug(false); - victron.setCallback(&callback); + victron.setCallback(onVictronData); - // Add your devices here victron.addDevice( "Rainbow48V", "E4:05:42:34:14:F3", @@ -144,14 +132,13 @@ void setup() { DEVICE_TYPE_SOLAR_CHARGER ); - Serial.println("Configured " + String(victron.getDeviceCount()) + " BLE devices"); - Serial.println("Packet size: " + String(sizeof(SolarChargerPacket)) + " bytes\n"); + Serial.printf("Configured %d BLE devices\n", (int)victron.getDeviceCount()); + Serial.printf("Packet size: %d bytes\n\n", (int)sizeof(SolarChargerPacket)); } void loop() { victron.loop(); - // Send cached packets every 30 seconds unsigned long now = millis(); if (now - lastSendTime >= SEND_INTERVAL_MS) { lastSendTime = now; @@ -167,11 +154,10 @@ void loop() { if (result == ESP_OK) { sendCount++; sent++; - Serial.printf("[ESPNow] Sent %s: %.2fV %.1fA PV:%.1fV %.0fW State:%d\n", + Serial.printf("[ESPNow] Sent %s: %.2fV %.1fA %.0fW State:%d\n", cachedPackets[i].deviceName, cachedPackets[i].batteryVoltage, cachedPackets[i].batteryCurrent, - cachedPackets[i].panelVoltage, cachedPackets[i].panelPower, cachedPackets[i].chargeState); } else { diff --git a/src/VictronBLE.cpp b/src/VictronBLE.cpp index da5011c..8404b7c 100644 --- a/src/VictronBLE.cpp +++ b/src/VictronBLE.cpp @@ -7,634 +7,337 @@ */ #include "VictronBLE.h" +#include -// Constructor VictronBLE::VictronBLE() - : pBLEScan(nullptr), scanCallback(nullptr), callback(nullptr), - debugEnabled(false), scanDuration(5), initialized(false) { + : deviceCount(0), pBLEScan(nullptr), scanCallbackObj(nullptr), + callback(nullptr), debugEnabled(false), scanDuration(5), initialized(false) { + memset(devices, 0, sizeof(devices)); } -// Destructor -VictronBLE::~VictronBLE() { - for (auto& pair : devices) { - delete pair.second; - } - devices.clear(); - - if (pBLEScan) { - pBLEScan->stop(); - } - - delete scanCallback; -} - -// Initialize BLE bool VictronBLE::begin(uint32_t scanDuration) { - if (initialized) { - if (debugEnabled) debugPrint("VictronBLE already initialized"); - return true; - } - + if (initialized) return true; this->scanDuration = scanDuration; - if (debugEnabled) debugPrint("Initializing VictronBLE..."); - BLEDevice::init("VictronBLE"); pBLEScan = BLEDevice::getScan(); + if (!pBLEScan) return false; - if (!pBLEScan) { - lastError = "Failed to create BLE scanner"; - if (debugEnabled) debugPrint(lastError); - return false; - } - - scanCallback = new VictronBLEAdvertisedDeviceCallbacks(this); - pBLEScan->setAdvertisedDeviceCallbacks(scanCallback, true); - pBLEScan->setActiveScan(false); // Passive scan - lower power + scanCallbackObj = new VictronBLEAdvertisedDeviceCallbacks(this); + pBLEScan->setAdvertisedDeviceCallbacks(scanCallbackObj, true); + pBLEScan->setActiveScan(false); pBLEScan->setInterval(100); pBLEScan->setWindow(99); initialized = true; - if (debugEnabled) debugPrint("VictronBLE initialized successfully"); - + if (debugEnabled) Serial.println("[VictronBLE] Initialized"); return true; } -// Add a device to monitor -bool VictronBLE::addDevice(const VictronDeviceConfig& config) { - if (config.macAddress.length() == 0) { - lastError = "MAC address cannot be empty"; - if (debugEnabled) debugPrint(lastError); - return false; - } +bool VictronBLE::addDevice(const char* name, const char* mac, const char* hexKey, + VictronDeviceType type) { + if (deviceCount >= VICTRON_MAX_DEVICES) return false; + if (!hexKey || strlen(hexKey) != 32) return false; + if (!mac || strlen(mac) == 0) return false; - if (config.encryptionKey.length() != 32) { - lastError = "Encryption key must be 32 hex characters"; - if (debugEnabled) debugPrint(lastError); - return false; - } + char normalizedMAC[VICTRON_MAC_LEN]; + normalizeMAC(mac, normalizedMAC); - String normalizedMAC = normalizeMAC(config.macAddress); + // Check for duplicate + if (findDevice(normalizedMAC)) return false; - // Check if device already exists - if (devices.find(normalizedMAC) != devices.end()) { - if (debugEnabled) debugPrint("Device " + normalizedMAC + " already exists, updating config"); - delete devices[normalizedMAC]; - } + DeviceEntry* entry = &devices[deviceCount]; + memset(entry, 0, sizeof(DeviceEntry)); + entry->active = true; - DeviceInfo* info = new DeviceInfo(); - info->config = config; - info->config.macAddress = normalizedMAC; + strncpy(entry->device.name, name ? name : "", VICTRON_NAME_LEN - 1); + entry->device.name[VICTRON_NAME_LEN - 1] = '\0'; + memcpy(entry->device.mac, normalizedMAC, VICTRON_MAC_LEN); + entry->device.deviceType = type; + entry->device.rssi = -100; - // Convert encryption key from hex string to bytes - if (!hexStringToBytes(config.encryptionKey, info->encryptionKeyBytes, 16)) { - lastError = "Invalid encryption key format"; - if (debugEnabled) debugPrint(lastError); - delete info; - return false; - } + if (!hexToBytes(hexKey, entry->key, 16)) 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; - - if (debugEnabled) { - debugPrint("Added device: " + config.name + " (MAC: " + normalizedMAC + ")"); - debugPrint(" Original MAC input: " + config.macAddress); - debugPrint(" Stored normalized: " + normalizedMAC); - } + deviceCount++; + if (debugEnabled) Serial.printf("[VictronBLE] Added: %s (%s)\n", name, normalizedMAC); return true; } -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(const String& macAddress) { - String normalizedMAC = normalizeMAC(macAddress); - - auto it = devices.find(normalizedMAC); - if (it != devices.end()) { - delete it->second; - devices.erase(it); - if (debugEnabled) debugPrint("Removed device: " + normalizedMAC); - } -} - -// Main loop function void VictronBLE::loop() { - if (!initialized) { - return; - } - - // Start a scan - BLEScanResults scanResults = pBLEScan->start(scanDuration, false); + if (!initialized) return; + pBLEScan->start(scanDuration, false); pBLEScan->clearResults(); } -// BLE callback implementation +// BLE scan callback void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertisedDevice) { - if (victronBLE) { - victronBLE->processDevice(advertisedDevice); - } + if (victronBLE) victronBLE->processDevice(advertisedDevice); } -// Process advertised device void VictronBLE::processDevice(BLEAdvertisedDevice& advertisedDevice) { - // Get MAC address from the advertised device - String mac = macAddressToString(advertisedDevice.getAddress()); - String normalizedMAC = normalizeMAC(mac); + if (!advertisedDevice.haveManufacturerData()) return; - if (debugEnabled) { - debugPrint("Raw MAC: " + mac + " -> Normalized: " + normalizedMAC); - } + std::string raw = advertisedDevice.getManufacturerData(); + if (raw.length() < 10) return; - // Parse manufacturer data into local struct + // Quick vendor ID check before any other work + uint16_t vendorID = (uint8_t)raw[0] | ((uint8_t)raw[1] << 8); + if (vendorID != VICTRON_MANUFACTURER_ID) return; + + // Parse manufacturer data victronManufacturerData mfgData; memset(&mfgData, 0, sizeof(mfgData)); - if (advertisedDevice.haveManufacturerData()) { - 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())); - } + size_t copyLen = raw.length() > sizeof(mfgData) ? sizeof(mfgData) : raw.length(); + raw.copy(reinterpret_cast(&mfgData), copyLen); - // Debug: Log all discovered BLE devices - if (debugEnabled) { - String debugMsg = "BLE Device: " + mac; - debugMsg += ", RSSI: " + String(advertisedDevice.getRSSI()) + " dBm"; - if (advertisedDevice.haveName()) - debugMsg += ", Name: " + String(advertisedDevice.getName().c_str()); + // Normalize MAC and find device + char normalizedMAC[VICTRON_MAC_LEN]; + normalizeMAC(advertisedDevice.getAddress().toString().c_str(), normalizedMAC); - debugMsg += ", Mfg ID: 0x" + String(mfgData.vendorID, HEX); - if (mfgData.vendorID == VICTRON_MANUFACTURER_ID) { - debugMsg += " (Victron)"; - } - - debugPrint(debugMsg); - } - - // Check if this is one of our configured devices - auto it = devices.find(normalizedMAC); - if (it == devices.end()) { - if (debugEnabled && mfgData.vendorID == VICTRON_MANUFACTURER_ID) { - debugPrint("Found unmonitored Victron Device: " + normalizedMAC); - } - return; // Not a device we're monitoring - } - - DeviceInfo* deviceInfo = it->second; - - // Check if it's Victron (manufacturer ID 0x02E1) - if (mfgData.vendorID != VICTRON_MANUFACTURER_ID) { - if (debugEnabled) debugPrint("Skipping non VICTRON"); + DeviceEntry* entry = findDevice(normalizedMAC); + if (!entry) { + if (debugEnabled) Serial.printf("[VictronBLE] Unmonitored Victron: %s\n", normalizedMAC); return; } - if (debugEnabled) debugPrint("Processing data from: " + deviceInfo->config.name); + if (debugEnabled) Serial.printf("[VictronBLE] Processing: %s\n", entry->device.name); - // Parse the advertisement - if (parseAdvertisement(deviceInfo, mfgData)) { - // Update RSSI - if (deviceInfo->data) { - deviceInfo->data->rssi = advertisedDevice.getRSSI(); - deviceInfo->data->lastUpdate = millis(); - } + if (parseAdvertisement(entry, mfgData)) { + entry->device.rssi = advertisedDevice.getRSSI(); + entry->device.lastUpdate = millis(); } } -// Parse advertisement data -bool VictronBLE::parseAdvertisement(DeviceInfo* deviceInfo, const victronManufacturerData& mfgData) { +bool VictronBLE::parseAdvertisement(DeviceEntry* entry, const victronManufacturerData& mfg) { if (debugEnabled) { - 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)); + Serial.printf("[VictronBLE] Beacon:0x%02X Record:0x%02X Nonce:0x%04X\n", + mfg.beaconType, mfg.victronRecordType, mfg.nonceDataCounter); } - // 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] = mfgData.nonceDataCounter & 0xFF; // Low byte - iv[1] = (mfgData.nonceDataCounter >> 8) & 0xFF; // High byte - // Remaining bytes stay zero - - // Decrypt the data - const size_t encryptedLen = sizeof(mfgData.victronEncryptedData); - uint8_t decrypted[encryptedLen]; - if (!decryptAdvertisement(mfgData.victronEncryptedData, - encryptedLen, - deviceInfo->encryptionKeyBytes, iv, decrypted)) { - lastError = "Decryption failed"; - if (debugEnabled) debugPrint(lastError); + // Quick key check before expensive decryption + if (mfg.encryptKeyMatch != entry->key[0]) { + if (debugEnabled) Serial.println("[VictronBLE] Key byte mismatch"); return false; } - // Parse based on device type - bool parseOk = false; + // Build IV from nonce (2 bytes little-endian + 14 zero bytes) + uint8_t iv[16] = {0}; + iv[0] = mfg.nonceDataCounter & 0xFF; + iv[1] = (mfg.nonceDataCounter >> 8) & 0xFF; - switch (mfgData.victronRecordType) { + // Decrypt + uint8_t decrypted[VICTRON_ENCRYPTED_LEN]; + if (!decryptData(mfg.victronEncryptedData, VICTRON_ENCRYPTED_LEN, + entry->key, iv, decrypted)) { + if (debugEnabled) Serial.println("[VictronBLE] Decryption failed"); + return false; + } + + // Parse based on record type (auto-detects device type) + bool ok = false; + switch (mfg.victronRecordType) { case DEVICE_TYPE_SOLAR_CHARGER: - if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) { - parseOk = parseSolarCharger(decrypted, encryptedLen, - *static_cast(deviceInfo->data)); - } + entry->device.deviceType = DEVICE_TYPE_SOLAR_CHARGER; + ok = parseSolarCharger(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.solar); break; - case DEVICE_TYPE_BATTERY_MONITOR: - if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) { - parseOk = parseBatteryMonitor(decrypted, encryptedLen, - *static_cast(deviceInfo->data)); - } + entry->device.deviceType = DEVICE_TYPE_BATTERY_MONITOR; + ok = parseBatteryMonitor(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.battery); 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, - *static_cast(deviceInfo->data)); - } + entry->device.deviceType = DEVICE_TYPE_INVERTER; + ok = parseInverter(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.inverter); break; - case DEVICE_TYPE_DCDC_CONVERTER: - if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) { - parseOk = parseDCDCConverter(decrypted, encryptedLen, - *static_cast(deviceInfo->data)); - } + entry->device.deviceType = DEVICE_TYPE_DCDC_CONVERTER; + ok = parseDCDCConverter(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.dcdc); break; - default: - if (debugEnabled) debugPrint("Unknown device type: 0x" + String(mfgData.victronRecordType, HEX)); + if (debugEnabled) Serial.printf("[VictronBLE] Unknown type: 0x%02X\n", mfg.victronRecordType); return false; } - if (parseOk && deviceInfo->data) { - deviceInfo->data->dataValid = true; - - // Call appropriate callback - if (callback) { - switch (mfgData.victronRecordType) { - case DEVICE_TYPE_SOLAR_CHARGER: - callback->onSolarChargerData(*static_cast(deviceInfo->data)); - break; - case DEVICE_TYPE_BATTERY_MONITOR: - 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(*static_cast(deviceInfo->data)); - break; - case DEVICE_TYPE_DCDC_CONVERTER: - callback->onDCDCConverterData(*static_cast(deviceInfo->data)); - break; - } - } + if (ok) { + entry->device.dataValid = true; + if (callback) callback(&entry->device); } - return parseOk; + return ok; } -// 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) { +bool VictronBLE::decryptData(const uint8_t* encrypted, size_t len, + 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) { + if (mbedtls_aes_setkey_enc(&aes, key, 128) != 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); - + int ret = mbedtls_aes_crypt_ctr(&aes, len, &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 < sizeof(victronSolarChargerPayload)) { - if (debugEnabled) debugPrint("Solar charger data too short: " + String(len) + " bytes"); - return false; - } +bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, VictronSolarData& result) { + if (len < sizeof(victronSolarChargerPayload)) return false; + const auto* p = reinterpret_cast(data); - const auto* payload = reinterpret_cast(data); - - // Parse charge state - result.chargeState = static_cast(payload->deviceState); - - // Parse battery voltage (10 mV units -> volts) - result.batteryVoltage = payload->batteryVoltage * 0.01f; - - // Parse battery current (10 mA units, signed -> amps) - result.batteryCurrent = payload->batteryCurrent * 0.01f; - - // Parse yield today (10 Wh units -> Wh) - result.yieldToday = payload->yieldToday * 10; - - // Parse PV power (1 W units) - result.panelPower = payload->inputPower; - - // Parse load current (10 mA units -> amps, 0xFFFF = no load) - if (payload->loadCurrent != 0xFFFF) { - result.loadCurrent = payload->loadCurrent * 0.01f; - } else { - result.loadCurrent = 0; - } - - // Calculate PV voltage from power and current (if current > 0) - if (result.batteryCurrent > 0.1f) { - result.panelVoltage = result.panelPower / result.batteryCurrent; - } else { - result.panelVoltage = 0; - } + result.chargeState = p->deviceState; + result.errorCode = p->errorCode; + result.batteryVoltage = p->batteryVoltage * 0.01f; + result.batteryCurrent = p->batteryCurrent * 0.01f; + result.yieldToday = p->yieldToday * 10; + result.panelPower = p->inputPower; + result.loadCurrent = (p->loadCurrent != 0xFFFF) ? p->loadCurrent * 0.01f : 0; if (debugEnabled) { - debugPrint("Solar Charger: " + String(result.batteryVoltage, 2) + "V, " + - String(result.batteryCurrent, 2) + "A, " + - String(result.panelPower) + "W, State: " + String(result.chargeState)); + Serial.printf("[VictronBLE] Solar: %.2fV %.2fA %dW State:%d\n", + result.batteryVoltage, result.batteryCurrent, + (int)result.panelPower, result.chargeState); } - return true; } -// Parse Battery Monitor data -bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result) { - if (len < sizeof(victronBatteryMonitorPayload)) { - if (debugEnabled) debugPrint("Battery monitor data too short: " + String(len) + " bytes"); - return false; - } +bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, VictronBatteryData& result) { + if (len < sizeof(victronBatteryMonitorPayload)) return false; + const auto* p = reinterpret_cast(data); - const auto* payload = reinterpret_cast(data); + result.remainingMinutes = p->remainingMins; + result.voltage = p->batteryVoltage * 0.01f; - // Parse remaining time (1 minute units) - result.remainingMinutes = payload->remainingMins; + // Alarm bits + result.alarmLowVoltage = (p->alarms & 0x01) != 0; + result.alarmHighVoltage = (p->alarms & 0x02) != 0; + result.alarmLowSOC = (p->alarms & 0x04) != 0; + result.alarmLowTemperature = (p->alarms & 0x10) != 0; + result.alarmHighTemperature = (p->alarms & 0x20) != 0; - // Parse battery voltage (10 mV units -> volts) - result.voltage = payload->batteryVoltage * 0.01f; - - // Parse alarm bits - result.alarmLowVoltage = (payload->alarms & 0x01) != 0; - result.alarmHighVoltage = (payload->alarms & 0x02) != 0; - result.alarmLowSOC = (payload->alarms & 0x04) != 0; - result.alarmLowTemperature = (payload->alarms & 0x10) != 0; - result.alarmHighTemperature = (payload->alarms & 0x20) != 0; - - // Parse aux data: voltage (10 mV units) or temperature (0.01K units) - if (payload->auxData < 3000) { // If < 30V, it's voltage - result.auxVoltage = payload->auxData * 0.01f; + // Aux data: voltage or temperature (heuristic: < 30V = voltage) + // NOTE: Victron protocol uses a flag bit for this, but it's not exposed + // in the BLE advertisement. This heuristic may misclassify edge cases. + if (p->auxData < 3000) { + result.auxVoltage = p->auxData * 0.01f; result.temperature = 0; - } else { // Otherwise temperature in 0.01 Kelvin - result.temperature = (payload->auxData * 0.01f) - 273.15f; + } else { + result.temperature = (p->auxData * 0.01f) - 273.15f; result.auxVoltage = 0; } - // Parse battery current (22-bit signed, 1 mA units) - int32_t current = payload->currentLow | - (payload->currentMid << 8) | - ((payload->currentHigh_consumedLow & 0x3F) << 16); - // Sign extend from 22 bits to 32 bits - if (current & 0x200000) { - current |= 0xFFC00000; - } - result.current = current * 0.001f; // Convert mA to A + // Battery current (22-bit signed, 1 mA units) + int32_t current = p->currentLow | + (p->currentMid << 8) | + ((p->currentHigh_consumedLow & 0x3F) << 16); + if (current & 0x200000) current |= 0xFFC00000; // Sign extend + result.current = current * 0.001f; - // Parse consumed Ah (18-bit signed, 10 mAh units) - int32_t consumedAh = ((payload->currentHigh_consumedLow & 0xC0) >> 6) | - (payload->consumedMid << 2) | - (payload->consumedHigh << 10); - // Sign extend from 18 bits to 32 bits - if (consumedAh & 0x20000) { - consumedAh |= 0xFFFC0000; - } - result.consumedAh = consumedAh * 0.01f; // Convert 10mAh to Ah + // Consumed Ah (18-bit signed, 10 mAh units) + int32_t consumedAh = ((p->currentHigh_consumedLow & 0xC0) >> 6) | + (p->consumedMid << 2) | + (p->consumedHigh << 10); + if (consumedAh & 0x20000) consumedAh |= 0xFFFC0000; // Sign extend + result.consumedAh = consumedAh * 0.01f; - // Parse SOC (10-bit value, 10 = 1.0%) - result.soc = (payload->soc & 0x3FF) * 0.1f; + // SOC (10-bit, 0.1% units) + result.soc = (p->soc & 0x3FF) * 0.1f; if (debugEnabled) { - debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " + - String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%"); + Serial.printf("[VictronBLE] Battery: %.2fV %.2fA SOC:%.1f%%\n", + result.voltage, result.current, result.soc); } - return true; } -// Parse Inverter data -bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& result) { - if (len < sizeof(victronInverterPayload)) { - if (debugEnabled) debugPrint("Inverter data too short: " + String(len) + " bytes"); - return false; - } +bool VictronBLE::parseInverter(const uint8_t* data, size_t len, VictronInverterData& result) { + if (len < sizeof(victronInverterPayload)) return false; + const auto* p = reinterpret_cast(data); - const auto* payload = reinterpret_cast(data); + result.state = p->deviceState; + result.batteryVoltage = p->batteryVoltage * 0.01f; + result.batteryCurrent = p->batteryCurrent * 0.01f; - // Parse device state - result.state = payload->deviceState; - - // Parse battery voltage (10 mV units -> volts) - result.batteryVoltage = payload->batteryVoltage * 0.01f; - - // Parse battery current (10 mA units, signed -> amps) - result.batteryCurrent = payload->batteryCurrent * 0.01f; - - // Parse AC Power (signed 24-bit, 1 W units) - int32_t acPower = payload->acPowerLow | - (payload->acPowerMid << 8) | - (payload->acPowerHigh << 16); - // Sign extend from 24 bits to 32 bits - if (acPower & 0x800000) { - acPower |= 0xFF000000; - } + // AC Power (signed 24-bit) + int32_t acPower = p->acPowerLow | (p->acPowerMid << 8) | (p->acPowerHigh << 16); + if (acPower & 0x800000) acPower |= 0xFF000000; // Sign extend result.acPower = acPower; - // Parse alarm bits - result.alarmLowVoltage = (payload->alarms & 0x01) != 0; - result.alarmHighVoltage = (payload->alarms & 0x02) != 0; - result.alarmHighTemperature = (payload->alarms & 0x04) != 0; - result.alarmOverload = (payload->alarms & 0x08) != 0; + // Alarm bits + result.alarmLowVoltage = (p->alarms & 0x01) != 0; + result.alarmHighVoltage = (p->alarms & 0x02) != 0; + result.alarmHighTemperature = (p->alarms & 0x04) != 0; + result.alarmOverload = (p->alarms & 0x08) != 0; if (debugEnabled) { - debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " + - String(result.acPower) + "W, State: " + String(result.state)); + Serial.printf("[VictronBLE] Inverter: %.2fV %dW State:%d\n", + result.batteryVoltage, (int)result.acPower, result.state); } - return true; } -// Parse DC-DC Converter data -bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result) { - if (len < sizeof(victronDCDCConverterPayload)) { - if (debugEnabled) debugPrint("DC-DC converter data too short: " + String(len) + " bytes"); - return false; - } +bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, VictronDCDCData& result) { + if (len < sizeof(victronDCDCConverterPayload)) return false; + const auto* p = reinterpret_cast(data); - const auto* payload = reinterpret_cast(data); - - // Parse charge state - result.chargeState = payload->chargeState; - - // Parse error code - result.errorCode = payload->errorCode; - - // Parse input voltage (10 mV units -> volts) - result.inputVoltage = payload->inputVoltage * 0.01f; - - // Parse output voltage (10 mV units -> volts) - result.outputVoltage = payload->outputVoltage * 0.01f; - - // Parse output current (10 mA units -> amps) - result.outputCurrent = payload->outputCurrent * 0.01f; + result.chargeState = p->chargeState; + result.errorCode = p->errorCode; + result.inputVoltage = p->inputVoltage * 0.01f; + result.outputVoltage = p->outputVoltage * 0.01f; + result.outputCurrent = p->outputCurrent * 0.01f; if (debugEnabled) { - debugPrint("DC-DC Converter: In=" + String(result.inputVoltage, 2) + "V, Out=" + - String(result.outputVoltage, 2) + "V, " + String(result.outputCurrent, 2) + "A"); + Serial.printf("[VictronBLE] DC-DC: In=%.2fV Out=%.2fV %.2fA\n", + result.inputVoltage, result.outputVoltage, result.outputCurrent); } - return true; } -// Get data methods -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 = *static_cast(it->second->data); - return data.dataValid; - } - return false; -} - -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 = *static_cast(it->second->data); - return data.dataValid; - } - return false; -} - -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 = *static_cast(it->second->data); - return data.dataValid; - } - return false; -} - -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 = *static_cast(it->second->data); - return data.dataValid; - } - return false; -} - -// Get devices by type -std::vector VictronBLE::getDevicesByType(VictronDeviceType type) { - std::vector 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; - } +// --- Helpers --- +bool VictronBLE::hexToBytes(const char* hex, uint8_t* out, size_t len) { + if (strlen(hex) != 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; - } + uint8_t hi = hex[i * 2], lo = hex[i * 2 + 1]; + if (hi >= '0' && hi <= '9') hi -= '0'; + else if (hi >= 'a' && hi <= 'f') hi = hi - 'a' + 10; + else if (hi >= 'A' && hi <= 'F') hi = hi - 'A' + 10; + else return false; + if (lo >= '0' && lo <= '9') lo -= '0'; + else if (lo >= 'a' && lo <= 'f') lo = lo - 'a' + 10; + else if (lo >= 'A' && lo <= 'F') lo = lo - 'A' + 10; + else return false; + out[i] = (hi << 4) | lo; } - return true; } -// Helper: MAC address to string -String VictronBLE::macAddressToString(BLEAddress address) { - return String(address.toString().c_str()); +void VictronBLE::normalizeMAC(const char* input, char* output) { + int j = 0; + for (int i = 0; input[i] && j < VICTRON_MAC_LEN - 1; i++) { + char c = input[i]; + if (c == ':' || c == '-') continue; + output[j++] = (c >= 'A' && c <= 'F') ? (c + 32) : c; + } + output[j] = '\0'; } -// Helper: Normalize MAC address format -String VictronBLE::normalizeMAC(const String& mac) { - String normalized = mac; - normalized.toLowerCase(); - normalized.replace("-", ""); - normalized.replace(":", ""); - return normalized; -} - -// Debug helper -void VictronBLE::debugPrint(const String& message) { - if (debugEnabled) - Serial.println("[VictronBLE] " + message); +VictronBLE::DeviceEntry* VictronBLE::findDevice(const char* normalizedMAC) { + for (size_t i = 0; i < deviceCount; i++) { + if (devices[i].active && strcmp(devices[i].device.mac, normalizedMAC) == 0) { + return &devices[i]; + } + } + return nullptr; } diff --git a/src/VictronBLE.h b/src/VictronBLE.h index 3817956..894a975 100644 --- a/src/VictronBLE.h +++ b/src/VictronBLE.h @@ -15,14 +15,16 @@ #include #include #include -#include -#include #include "mbedtls/aes.h" -// Victron manufacturer ID +// --- Constants --- static constexpr uint16_t VICTRON_MANUFACTURER_ID = 0x02E1; +static constexpr int VICTRON_MAX_DEVICES = 8; +static constexpr int VICTRON_MAC_LEN = 13; // 12 hex chars + null +static constexpr int VICTRON_NAME_LEN = 32; +static constexpr int VICTRON_ENCRYPTED_LEN = 21; -// Device type IDs from Victron protocol +// --- Device type IDs from Victron protocol --- enum VictronDeviceType { DEVICE_TYPE_UNKNOWN = 0x00, DEVICE_TYPE_SOLAR_CHARGER = 0x01, @@ -38,7 +40,7 @@ enum VictronDeviceType { DEVICE_TYPE_DC_ENERGY_METER = 0x0B }; -// Device state for Solar Charger +// --- Device state for Solar Charger --- enum SolarChargerState { CHARGER_OFF = 0, CHARGER_LOW_POWER = 1, @@ -53,163 +55,216 @@ enum SolarChargerState { CHARGER_EXTERNAL_CONTROL = 252 }; -// Binary data structures for decoding BLE advertisements -// Must use __attribute__((packed)) to prevent compiler padding +// ============================================================ +// Wire-format packed structures for decoding BLE advertisements +// ============================================================ -// Manufacturer data structure (outer envelope) 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 - uint8_t victronRecordType; // Should be 0x01 (Solar Charger) for the packets we want - uint16_t nonceDataCounter; // Nonce - 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. + uint16_t vendorID; + uint8_t beaconType; // 0x10 = Product Advertisement + uint8_t unknownData1[3]; + uint8_t victronRecordType; // Device type (see VictronDeviceType) + uint16_t nonceDataCounter; + uint8_t encryptKeyMatch; // Should match encryption key byte 0 + uint8_t victronEncryptedData[VICTRON_ENCRYPTED_LEN]; } __attribute__((packed)); -// Decrypted payload structures for each device type -// Solar Charger decrypted payload struct victronSolarChargerPayload { - uint8_t deviceState; // Charge state (SolarChargerState enum) - uint8_t errorCode; // Error code - int16_t batteryVoltage; // Battery voltage in 10mV units - int16_t batteryCurrent; // Battery current in 10mA units (signed) - uint16_t yieldToday; // Yield today in 10Wh units - 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 + uint8_t deviceState; + uint8_t errorCode; + int16_t batteryVoltage; // 10mV units + int16_t batteryCurrent; // 10mA units (signed) + uint16_t yieldToday; // 10Wh units + uint16_t inputPower; // 1W units + uint16_t loadCurrent; // 10mA units (0xFFFF = no load) + uint8_t reserved[2]; } __attribute__((packed)); -// Battery Monitor decrypted payload struct victronBatteryMonitorPayload { - uint16_t remainingMins; // Time remaining in minutes - uint16_t batteryVoltage; // Battery voltage in 10mV units - uint8_t alarms; // Alarm bits - uint16_t auxData; // Aux voltage (10mV) or temperature (0.01K) - uint8_t currentLow; // Battery current bits 0-7 - uint8_t currentMid; // Battery current bits 8-15 - uint8_t currentHigh_consumedLow; // Current bits 16-21 (low 6 bits), consumed bits 0-1 (high 2 bits) - uint8_t consumedMid; // Consumed Ah bits 2-9 - 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 + uint16_t remainingMins; + uint16_t batteryVoltage; // 10mV units + uint8_t alarms; + uint16_t auxData; // 10mV (voltage) or 0.01K (temperature) + uint8_t currentLow; + uint8_t currentMid; + uint8_t currentHigh_consumedLow; // Current bits 16-21 (low 6), consumed bits 0-1 (high 2) + uint8_t consumedMid; + uint8_t consumedHigh; + uint16_t soc; // 0.1% units (10-bit) + uint8_t reserved[2]; } __attribute__((packed)); -// Inverter decrypted payload struct victronInverterPayload { - uint8_t deviceState; // Device state - uint8_t errorCode; // Error code - uint16_t batteryVoltage; // Battery voltage in 10mV units - int16_t batteryCurrent; // Battery current in 10mA units (signed) - uint8_t acPowerLow; // AC Power bits 0-7 - uint8_t acPowerMid; // AC Power bits 8-15 - uint8_t acPowerHigh; // AC Power bits 16-23 (signed 24-bit) - uint8_t alarms; // Alarm bits - uint8_t reserved[4]; // Reserved bytes + uint8_t deviceState; + uint8_t errorCode; + uint16_t batteryVoltage; // 10mV units + int16_t batteryCurrent; // 10mA units (signed) + uint8_t acPowerLow; + uint8_t acPowerMid; + uint8_t acPowerHigh; // Signed 24-bit + uint8_t alarms; + uint8_t reserved[4]; } __attribute__((packed)); -// DC-DC Converter decrypted payload 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 + uint8_t chargeState; + uint8_t errorCode; + uint16_t inputVoltage; // 10mV units + uint16_t outputVoltage; // 10mV units + uint16_t outputCurrent; // 10mA units + uint8_t reserved[6]; } __attribute__((packed)); -// Base structure for all device data -struct VictronDeviceData { - String deviceName; - String macAddress; - VictronDeviceType deviceType; - int8_t rssi; - uint32_t lastUpdate; - bool dataValid; +// ============================================================ +// Parsed data structures (flat, no inheritance) +// ============================================================ - VictronDeviceData() : deviceType(DEVICE_TYPE_UNKNOWN), rssi(-100), - lastUpdate(0), dataValid(false) {} -}; - -// Solar Charger specific data -struct SolarChargerData : public VictronDeviceData { - SolarChargerState chargeState; +struct VictronSolarData { + uint8_t chargeState; // SolarChargerState enum + uint8_t errorCode; 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 { +struct VictronBatteryData { 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 current; // A + float temperature; // C (0 if aux is voltage) + float auxVoltage; // V (0 if aux is temperature) + uint16_t remainingMinutes; float consumedAh; // Ah - float soc; // State of Charge % + float soc; // % 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 { +struct VictronInverterData { float batteryVoltage; // V float batteryCurrent; // A float acPower; // W - uint8_t state; // Inverter state - bool alarmHighVoltage; + uint8_t state; bool alarmLowVoltage; + bool alarmHighVoltage; 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 { +struct VictronDCDCData { 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 declarations -class VictronBLE; +// ============================================================ +// Main device struct with tagged union +// ============================================================ + +struct VictronDevice { + char name[VICTRON_NAME_LEN]; + char mac[VICTRON_MAC_LEN]; + VictronDeviceType deviceType; + int8_t rssi; + uint32_t lastUpdate; + bool dataValid; + union { + VictronSolarData solar; + VictronBatteryData battery; + VictronInverterData inverter; + VictronDCDCData dcdc; + }; +}; + +// ============================================================ +// Callback — simple function pointer +// ============================================================ + +typedef void (*VictronCallback)(const VictronDevice* device); + +// Forward declaration class VictronBLEAdvertisedDeviceCallbacks; -// Callback interface for device data updates +// ============================================================ +// Main VictronBLE class +// ============================================================ + +class VictronBLE { +public: + VictronBLE(); + + bool begin(uint32_t scanDuration = 5); + bool addDevice(const char* name, const char* mac, const char* hexKey, + VictronDeviceType type = DEVICE_TYPE_UNKNOWN); + void setCallback(VictronCallback cb) { callback = cb; } + void setDebug(bool enable) { debugEnabled = enable; } + size_t getDeviceCount() const { return deviceCount; } + void loop(); + +private: + friend class VictronBLEAdvertisedDeviceCallbacks; + + struct DeviceEntry { + VictronDevice device; + uint8_t key[16]; + bool active; + }; + + DeviceEntry devices[VICTRON_MAX_DEVICES]; + size_t deviceCount; + BLEScan* pBLEScan; + VictronBLEAdvertisedDeviceCallbacks* scanCallbackObj; + VictronCallback callback; + bool debugEnabled; + uint32_t scanDuration; + bool initialized; + + static bool hexToBytes(const char* hex, uint8_t* out, size_t len); + static void normalizeMAC(const char* input, char* output); + DeviceEntry* findDevice(const char* normalizedMAC); + bool decryptData(const uint8_t* encrypted, size_t len, + const uint8_t* key, const uint8_t* iv, uint8_t* decrypted); + void processDevice(BLEAdvertisedDevice& dev); + bool parseAdvertisement(DeviceEntry* entry, const victronManufacturerData& mfg); + bool parseSolarCharger(const uint8_t* data, size_t len, VictronSolarData& result); + bool parseBatteryMonitor(const uint8_t* data, size_t len, VictronBatteryData& result); + bool parseInverter(const uint8_t* data, size_t len, VictronInverterData& result); + bool parseDCDCConverter(const uint8_t* data, size_t len, VictronDCDCData& result); +}; + +// BLE scan callback (required by ESP32 BLE API) +class VictronBLEAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks { +public: + VictronBLEAdvertisedDeviceCallbacks(VictronBLE* parent) : victronBLE(parent) {} + void onResult(BLEAdvertisedDevice advertisedDevice) override; +private: + VictronBLE* victronBLE; +}; + +// ============================================================ +// Commented-out features — kept for reference / future use +// ============================================================ + +#if 0 + +// --- VictronDeviceConfig (use addDevice(name, mac, key, type) directly) --- +struct VictronDeviceConfig { + String name; + String macAddress; + String encryptionKey; + VictronDeviceType expectedType; + VictronDeviceConfig() : expectedType(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) {} +}; + +// --- Virtual callback interface (replaced by function pointer VictronCallback) --- class VictronDeviceCallback { public: virtual ~VictronDeviceCallback() {} @@ -219,114 +274,17 @@ public: virtual void onDCDCConverterData(const DCDCConverterData& data) {} }; -// Device configuration structure -struct VictronDeviceConfig { - String name; - String macAddress; - String encryptionKey; // 32 character hex string - VictronDeviceType expectedType; +// --- Per-type getter methods (use callback instead) --- +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); - VictronDeviceConfig() : expectedType(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) {} -}; +// --- Other removed methods --- +void removeDevice(const String& macAddress); +std::vector getDevicesByType(VictronDeviceType type); +String getLastError() const; -// 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(const String& name, const String& macAddress, const String& encryptionKey, - VictronDeviceType expectedType = DEVICE_TYPE_UNKNOWN); - - // Remove a device - void removeDevice(const 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(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); - - // 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; - } - 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; - - // 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(DeviceInfo* deviceInfo, const victronManufacturerData& mfgData); - 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); - - String macAddressToString(BLEAddress address); - String normalizeMAC(const 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 // commented-out features #endif // VICTRON_BLE_H