Compare commits
3 Commits
8a4e010324
...
4944757903
| Author | SHA1 | Date | |
|---|---|---|---|
| 4944757903 | |||
| 31765c7ac8 | |||
| 84d153c9a8 |
106
UPGRADE_V0.4.md
Normal file
106
UPGRADE_V0.4.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# Upgrading to VictronBLE v0.4
|
||||||
|
|
||||||
|
v0.4 is a breaking API change that simplifies the library significantly.
|
||||||
|
|
||||||
|
## Summary of Changes
|
||||||
|
|
||||||
|
- **Callback**: Virtual class → function pointer
|
||||||
|
- **Data access**: Inheritance → tagged union (`VictronDevice` with `solar`, `battery`, `inverter`, `dcdc` members)
|
||||||
|
- **Strings**: Arduino `String` → fixed `char[]` arrays
|
||||||
|
- **Memory**: `std::map` + heap allocation → fixed array, zero dynamic allocation
|
||||||
|
- **Removed**: `getLastError()`, `removeDevice()`, `getDevicesByType()`, per-type getter methods, `VictronDeviceConfig` struct, `VictronDeviceCallback` class
|
||||||
|
- **Removed field**: `panelVoltage` (was unreliably derived from `panelPower / batteryCurrent`)
|
||||||
|
|
||||||
|
## Migration Guide
|
||||||
|
|
||||||
|
### 1. Callback: class → function pointer
|
||||||
|
|
||||||
|
**Before (v0.3):**
|
||||||
|
```cpp
|
||||||
|
class MyCallback : public VictronDeviceCallback {
|
||||||
|
void onSolarChargerData(const SolarChargerData& data) override {
|
||||||
|
Serial.println(data.deviceName + ": " + String(data.panelPower) + "W");
|
||||||
|
}
|
||||||
|
void onBatteryMonitorData(const BatteryMonitorData& data) override {
|
||||||
|
Serial.println("SOC: " + String(data.soc) + "%");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
MyCallback callback;
|
||||||
|
victron.setCallback(&callback);
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (v0.4):**
|
||||||
|
```cpp
|
||||||
|
void onVictronData(const VictronDevice* dev) {
|
||||||
|
switch (dev->deviceType) {
|
||||||
|
case DEVICE_TYPE_SOLAR_CHARGER:
|
||||||
|
Serial.printf("%s: %.0fW\n", dev->name, dev->solar.panelPower);
|
||||||
|
break;
|
||||||
|
case DEVICE_TYPE_BATTERY_MONITOR:
|
||||||
|
Serial.printf("SOC: %.1f%%\n", dev->battery.soc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
victron.setCallback(onVictronData);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Data field access
|
||||||
|
|
||||||
|
Fields moved from flat `SolarChargerData` etc. into the `VictronDevice` tagged union:
|
||||||
|
|
||||||
|
| Old (v0.3) | New (v0.4) |
|
||||||
|
|---|---|
|
||||||
|
| `data.deviceName` | `dev->name` (char[32]) |
|
||||||
|
| `data.macAddress` | `dev->mac` (char[13]) |
|
||||||
|
| `data.rssi` | `dev->rssi` |
|
||||||
|
| `data.lastUpdate` | `dev->lastUpdate` |
|
||||||
|
| `data.batteryVoltage` | `dev->solar.batteryVoltage` |
|
||||||
|
| `data.batteryCurrent` | `dev->solar.batteryCurrent` |
|
||||||
|
| `data.panelPower` | `dev->solar.panelPower` |
|
||||||
|
| `data.yieldToday` | `dev->solar.yieldToday` |
|
||||||
|
| `data.loadCurrent` | `dev->solar.loadCurrent` |
|
||||||
|
| `data.chargeState` | `dev->solar.chargeState` (uint8_t, was enum) |
|
||||||
|
| `data.panelVoltage` | **Removed** - see below |
|
||||||
|
|
||||||
|
### 3. panelVoltage removed
|
||||||
|
|
||||||
|
`panelVoltage` was a derived value (`panelPower / batteryCurrent`) that was unreliable (division by zero when no current, inaccurate due to MPPT conversion). It has been removed.
|
||||||
|
|
||||||
|
If you need an estimate:
|
||||||
|
```cpp
|
||||||
|
float panelVoltage = (dev->solar.batteryCurrent > 0.1f)
|
||||||
|
? dev->solar.panelPower / dev->solar.batteryCurrent
|
||||||
|
: 0.0f;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. getLastError() removed
|
||||||
|
|
||||||
|
Debug output now goes directly to Serial when `setDebug(true)` is enabled. Remove any `getLastError()` calls.
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```cpp
|
||||||
|
if (!victron.begin(2)) {
|
||||||
|
Serial.println(victron.getLastError());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```cpp
|
||||||
|
if (!victron.begin(2)) {
|
||||||
|
Serial.println("Failed to initialize VictronBLE!");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. String types
|
||||||
|
|
||||||
|
Device name and MAC are now `char[]` instead of Arduino `String`. Use `Serial.printf()` or `String(dev->name)` if you need a String object.
|
||||||
|
|
||||||
|
### 6. addDevice() parameters
|
||||||
|
|
||||||
|
Parameters changed from `String` to `const char*`. Existing string literals work unchanged. `VictronDeviceConfig` struct is no longer needed.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Both v0.3 and v0.4 - string literals work the same
|
||||||
|
victron.addDevice("MySolar", "f69dfcce55eb",
|
||||||
|
"bf25c098c156afd6a180157b8a3ab1fb", DEVICE_TYPE_SOLAR_CHARGER);
|
||||||
|
```
|
||||||
@@ -17,7 +17,6 @@ struct __attribute__((packed)) SolarChargerPacket {
|
|||||||
uint8_t chargeState;
|
uint8_t chargeState;
|
||||||
float batteryVoltage; // V
|
float batteryVoltage; // V
|
||||||
float batteryCurrent; // A
|
float batteryCurrent; // A
|
||||||
float panelVoltage; // V
|
|
||||||
float panelPower; // W
|
float panelPower; // W
|
||||||
uint16_t yieldToday; // Wh
|
uint16_t yieldToday; // Wh
|
||||||
float loadCurrent; // A
|
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.chargeState = (sendCount % 4) + 3; // Cycle through Bulk(3), Absorption(4), Float(5), Storage(6)
|
||||||
pkt.batteryVoltage = 51.0f + (sendCount % 20) * 0.15f;
|
pkt.batteryVoltage = 51.0f + (sendCount % 20) * 0.15f;
|
||||||
pkt.batteryCurrent = 2.0f + (sendCount % 10) * 0.5f;
|
pkt.batteryCurrent = 2.0f + (sendCount % 10) * 0.5f;
|
||||||
pkt.panelVoltage = 65.0f + (sendCount % 15) * 0.8f;
|
|
||||||
pkt.panelPower = pkt.batteryCurrent * pkt.batteryVoltage;
|
pkt.panelPower = pkt.batteryCurrent * pkt.batteryVoltage;
|
||||||
pkt.yieldToday = 100 + sendCount * 10;
|
pkt.yieldToday = 100 + sendCount * 10;
|
||||||
pkt.loadCurrent = 0;
|
pkt.loadCurrent = 0;
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
*
|
*
|
||||||
* Demonstrates change-detection logging for Solar Charger data.
|
* Demonstrates change-detection logging for Solar Charger data.
|
||||||
* Only logs to serial when a value changes (ignoring RSSI), or once
|
* Only logs to serial when a value changes (ignoring RSSI), or once
|
||||||
* per minute if nothing has changed. This keeps serial output quiet
|
* per minute if nothing has changed.
|
||||||
* and is useful for long-running monitoring / data logging.
|
|
||||||
*
|
*
|
||||||
* Setup:
|
* Setup:
|
||||||
* 1. Get your device encryption keys from the VictronConnect app
|
* 1. Get your device encryption keys from the VictronConnect app
|
||||||
@@ -16,13 +15,11 @@
|
|||||||
|
|
||||||
VictronBLE victron;
|
VictronBLE victron;
|
||||||
|
|
||||||
// Tracks last-logged values per device for change detection
|
|
||||||
struct SolarChargerSnapshot {
|
struct SolarChargerSnapshot {
|
||||||
bool valid = false;
|
bool valid = false;
|
||||||
SolarChargerState chargeState;
|
uint8_t chargeState;
|
||||||
float batteryVoltage;
|
float batteryVoltage;
|
||||||
float batteryCurrent;
|
float batteryCurrent;
|
||||||
float panelVoltage;
|
|
||||||
float panelPower;
|
float panelPower;
|
||||||
uint16_t yieldToday;
|
uint16_t yieldToday;
|
||||||
float loadCurrent;
|
float loadCurrent;
|
||||||
@@ -30,26 +27,26 @@ struct SolarChargerSnapshot {
|
|||||||
uint32_t packetsSinceLastLog = 0;
|
uint32_t packetsSinceLastLog = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store a snapshot per device (index by MAC string)
|
|
||||||
static const int MAX_DEVICES = 4;
|
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 SolarChargerSnapshot snapshots[MAX_DEVICES];
|
||||||
static int deviceCount = 0;
|
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++) {
|
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) {
|
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 deviceCount++;
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String chargeStateName(SolarChargerState state) {
|
static const char* chargeStateName(uint8_t state) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case CHARGER_OFF: return "Off";
|
case CHARGER_OFF: return "Off";
|
||||||
case CHARGER_LOW_POWER: return "Low Power";
|
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) {
|
static void logData(const VictronDevice* dev, const VictronSolarData& s,
|
||||||
Serial.println("[" + data.deviceName + "] " + reason +
|
const char* reason, uint32_t packets) {
|
||||||
" pkts:" + String(packets) +
|
Serial.printf("[%s] %s pkts:%lu | State:%s Batt:%.2fV %.2fA PV:%.0fW Yield:%uWh",
|
||||||
" | State:" + chargeStateName(data.chargeState) +
|
dev->name, reason, packets,
|
||||||
" Batt:" + String(data.batteryVoltage, 2) + "V" +
|
chargeStateName(s.chargeState),
|
||||||
" " + String(data.batteryCurrent, 2) + "A" +
|
s.batteryVoltage, s.batteryCurrent,
|
||||||
" PV:" + String(data.panelVoltage, 1) + "V" +
|
s.panelPower, s.yieldToday);
|
||||||
" " + String(data.panelPower, 0) + "W" +
|
if (s.loadCurrent > 0)
|
||||||
" Yield:" + String(data.yieldToday) + "Wh" +
|
Serial.printf(" Load:%.2fA", s.loadCurrent);
|
||||||
(data.loadCurrent > 0 ? " Load:" + String(data.loadCurrent, 2) + "A" : ""));
|
Serial.println();
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoggerCallback : public VictronDeviceCallback {
|
void onVictronData(const VictronDevice* dev) {
|
||||||
public:
|
if (dev->deviceType != DEVICE_TYPE_SOLAR_CHARGER) return;
|
||||||
void onSolarChargerData(const SolarChargerData& data) override {
|
const auto& s = dev->solar;
|
||||||
int idx = findOrAddDevice(data.macAddress);
|
|
||||||
if (idx < 0) return;
|
|
||||||
|
|
||||||
SolarChargerSnapshot& prev = snapshots[idx];
|
int idx = findOrAddDevice(dev->mac);
|
||||||
unsigned long now = millis();
|
if (idx < 0) return;
|
||||||
prev.packetsSinceLastLog++;
|
|
||||||
|
|
||||||
if (!prev.valid) {
|
SolarChargerSnapshot& prev = snapshots[idx];
|
||||||
// First reading - always log
|
unsigned long now = millis();
|
||||||
logData(data, "INIT", prev.packetsSinceLastLog);
|
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 {
|
} else {
|
||||||
// Check for changes (everything except RSSI)
|
return;
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
@@ -135,14 +124,12 @@ void setup() {
|
|||||||
|
|
||||||
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());
|
|
||||||
while (1) delay(1000);
|
while (1) delay(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
victron.setDebug(false);
|
victron.setDebug(false);
|
||||||
victron.setCallback(&callback);
|
victron.setCallback(onVictronData);
|
||||||
|
|
||||||
// Add your devices here
|
|
||||||
victron.addDevice(
|
victron.addDevice(
|
||||||
"Rainbow48V",
|
"Rainbow48V",
|
||||||
"E4:05:42:34:14:F3",
|
"E4:05:42:34:14:F3",
|
||||||
@@ -157,11 +144,10 @@ void setup() {
|
|||||||
DEVICE_TYPE_SOLAR_CHARGER
|
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");
|
Serial.println("Logging on change, or every 60s heartbeat\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
victron.loop();
|
victron.loop();
|
||||||
delay(100);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,154 +1,129 @@
|
|||||||
/**
|
/**
|
||||||
* VictronBLE Example
|
* VictronBLE Multi-Device Example
|
||||||
*
|
*
|
||||||
* This example demonstrates how to use the VictronBLE library to read data
|
* Demonstrates reading data from multiple Victron device types via BLE.
|
||||||
* from multiple Victron devices simultaneously.
|
|
||||||
*
|
|
||||||
* Hardware Requirements:
|
|
||||||
* - ESP32 board
|
|
||||||
* - 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 VictronConnect app
|
||||||
* - Open VictronConnect
|
* (Settings > Product Info > Instant readout via Bluetooth > Show)
|
||||||
* - Connect to your device
|
* 2. Update the device configurations below with your MAC and key
|
||||||
* - 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
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "VictronBLE.h"
|
#include "VictronBLE.h"
|
||||||
|
|
||||||
// Create VictronBLE instance
|
|
||||||
VictronBLE victron;
|
VictronBLE victron;
|
||||||
|
|
||||||
// Device callback class - gets called when new data arrives
|
static uint32_t solarChargerCount = 0;
|
||||||
class MyVictronCallback : public VictronDeviceCallback {
|
static uint32_t batteryMonitorCount = 0;
|
||||||
public:
|
static uint32_t inverterCount = 0;
|
||||||
uint32_t solarChargerCount = 0;
|
static uint32_t dcdcConverterCount = 0;
|
||||||
uint32_t batteryMonitorCount = 0;
|
|
||||||
uint32_t inverterCount = 0;
|
|
||||||
uint32_t dcdcConverterCount = 0;
|
|
||||||
|
|
||||||
void onSolarChargerData(const SolarChargerData& data) override {
|
static const char* chargeStateName(uint8_t state) {
|
||||||
solarChargerCount++;
|
switch (state) {
|
||||||
Serial.println("\n=== Solar Charger: " + data.deviceName + " (#" + String(solarChargerCount) + ") ===");
|
case CHARGER_OFF: return "Off";
|
||||||
Serial.println("MAC: " + data.macAddress);
|
case CHARGER_LOW_POWER: return "Low Power";
|
||||||
Serial.println("RSSI: " + String(data.rssi) + " dBm");
|
case CHARGER_FAULT: return "Fault";
|
||||||
Serial.println("State: " + getChargeStateName(data.chargeState));
|
case CHARGER_BULK: return "Bulk";
|
||||||
Serial.println("Battery: " + String(data.batteryVoltage, 2) + " V");
|
case CHARGER_ABSORPTION: return "Absorption";
|
||||||
Serial.println("Current: " + String(data.batteryCurrent, 2) + " A");
|
case CHARGER_FLOAT: return "Float";
|
||||||
Serial.println("Panel Voltage: " + String(data.panelVoltage, 1) + " V");
|
case CHARGER_STORAGE: return "Storage";
|
||||||
Serial.println("Panel Power: " + String(data.panelPower) + " W");
|
case CHARGER_EQUALIZE: return "Equalize";
|
||||||
Serial.println("Yield Today: " + String(data.yieldToday) + " Wh");
|
case CHARGER_INVERTING: return "Inverting";
|
||||||
if (data.loadCurrent > 0) {
|
case CHARGER_POWER_SUPPLY: return "Power Supply";
|
||||||
Serial.println("Load Current: " + String(data.loadCurrent, 2) + " A");
|
case CHARGER_EXTERNAL_CONTROL: return "External Control";
|
||||||
}
|
default: return "Unknown";
|
||||||
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void onBatteryMonitorData(const BatteryMonitorData& data) override {
|
void onVictronData(const VictronDevice* dev) {
|
||||||
batteryMonitorCount++;
|
switch (dev->deviceType) {
|
||||||
Serial.println("\n=== Battery Monitor: " + data.deviceName + " (#" + String(batteryMonitorCount) + ") ===");
|
case DEVICE_TYPE_SOLAR_CHARGER: {
|
||||||
Serial.println("MAC: " + data.macAddress);
|
const auto& s = dev->solar;
|
||||||
Serial.println("RSSI: " + String(data.rssi) + " dBm");
|
solarChargerCount++;
|
||||||
Serial.println("Voltage: " + String(data.voltage, 2) + " V");
|
Serial.printf("\n=== Solar Charger: %s (#%lu) ===\n", dev->name, solarChargerCount);
|
||||||
Serial.println("Current: " + String(data.current, 2) + " A");
|
Serial.printf("MAC: %s\n", dev->mac);
|
||||||
Serial.println("SOC: " + String(data.soc, 1) + " %");
|
Serial.printf("RSSI: %d dBm\n", dev->rssi);
|
||||||
Serial.println("Consumed: " + String(data.consumedAh, 2) + " Ah");
|
Serial.printf("State: %s\n", chargeStateName(s.chargeState));
|
||||||
|
Serial.printf("Battery: %.2f V\n", s.batteryVoltage);
|
||||||
if (data.remainingMinutes < 65535) {
|
Serial.printf("Current: %.2f A\n", s.batteryCurrent);
|
||||||
int hours = data.remainingMinutes / 60;
|
Serial.printf("Panel Power: %.0f W\n", s.panelPower);
|
||||||
int mins = data.remainingMinutes % 60;
|
Serial.printf("Yield Today: %u Wh\n", s.yieldToday);
|
||||||
Serial.println("Time Remaining: " + String(hours) + "h " + String(mins) + "m");
|
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;
|
||||||
}
|
}
|
||||||
|
case DEVICE_TYPE_BATTERY_MONITOR: {
|
||||||
if (data.temperature > 0) {
|
const auto& b = dev->battery;
|
||||||
Serial.println("Temperature: " + String(data.temperature, 1) + " °C");
|
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) {
|
case DEVICE_TYPE_INVERTER: {
|
||||||
Serial.println("Aux Voltage: " + String(data.auxVoltage, 2) + " V");
|
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;
|
||||||
}
|
}
|
||||||
|
case DEVICE_TYPE_DCDC_CONVERTER: {
|
||||||
// Print alarms
|
const auto& dc = dev->dcdc;
|
||||||
if (data.alarmLowVoltage || data.alarmHighVoltage || data.alarmLowSOC ||
|
dcdcConverterCount++;
|
||||||
data.alarmLowTemperature || data.alarmHighTemperature) {
|
Serial.printf("\n=== DC-DC Converter: %s (#%lu) ===\n", dev->name, dcdcConverterCount);
|
||||||
Serial.print("ALARMS: ");
|
Serial.printf("MAC: %s\n", dev->mac);
|
||||||
if (data.alarmLowVoltage) Serial.print("LOW-V ");
|
Serial.printf("RSSI: %d dBm\n", dev->rssi);
|
||||||
if (data.alarmHighVoltage) Serial.print("HIGH-V ");
|
Serial.printf("Input: %.2f V\n", dc.inputVoltage);
|
||||||
if (data.alarmLowSOC) Serial.print("LOW-SOC ");
|
Serial.printf("Output: %.2f V\n", dc.outputVoltage);
|
||||||
if (data.alarmLowTemperature) Serial.print("LOW-TEMP ");
|
Serial.printf("Current: %.2f A\n", dc.outputCurrent);
|
||||||
if (data.alarmHighTemperature) Serial.print("HIGH-TEMP ");
|
Serial.printf("State: %d\n", dc.chargeState);
|
||||||
Serial.println();
|
if (dc.errorCode != 0)
|
||||||
|
Serial.printf("Error Code: %d\n", dc.errorCode);
|
||||||
|
Serial.printf("Last Update: %lus ago\n", (millis() - dev->lastUpdate) / 1000);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
|
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() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
@@ -158,102 +133,43 @@ void setup() {
|
|||||||
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
|
|
||||||
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());
|
|
||||||
while (1) delay(1000);
|
while (1) delay(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable debug output (optional)
|
|
||||||
victron.setDebug(false);
|
victron.setDebug(false);
|
||||||
|
victron.setCallback(onVictronData);
|
||||||
// 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.addDevice(
|
victron.addDevice(
|
||||||
"ScottTrailer", // Device name
|
"Rainbow48V",
|
||||||
"e64559783cfb",
|
"E4:05:42:34:14:F3",
|
||||||
"3fa658aded4f309b9bc17a2318cb1f56",
|
"0ec3adf7433dd61793ff2f3b8ad32ed8",
|
||||||
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",
|
|
||||||
DEVICE_TYPE_SOLAR_CHARGER
|
DEVICE_TYPE_SOLAR_CHARGER
|
||||||
);
|
);
|
||||||
*/
|
|
||||||
|
|
||||||
// Example: Battery Monitor (SmartShunt)
|
|
||||||
/*
|
|
||||||
victron.addDevice(
|
victron.addDevice(
|
||||||
"SmartShunt",
|
"ScottTrailer",
|
||||||
"11:22:33:44:55:66",
|
"e64559783cfb",
|
||||||
"fedcba0987654321fedcba0987654321",
|
"3fa658aded4f309b9bc17a2318cb1f56",
|
||||||
DEVICE_TYPE_BATTERY_MONITOR
|
DEVICE_TYPE_SOLAR_CHARGER
|
||||||
);
|
);
|
||||||
*/
|
|
||||||
|
|
||||||
// Example: Inverter/Charger
|
Serial.printf("Configured %d devices\n", (int)victron.getDeviceCount());
|
||||||
/*
|
|
||||||
victron.addDevice(
|
|
||||||
"MultiPlus",
|
|
||||||
"99:88:77:66:55:44",
|
|
||||||
"abcdefabcdefabcdefabcdefabcdefab",
|
|
||||||
DEVICE_TYPE_INVERTER
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
Serial.println("Configured " + String(victron.getDeviceCount()) + " devices");
|
|
||||||
Serial.println("\nStarting BLE scan...\n");
|
Serial.println("\nStarting BLE scan...\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static uint32_t loopCount = 0;
|
||||||
|
static uint32_t lastReport = 0;
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
// Process BLE scanning and data updates
|
victron.loop(); // Non-blocking: returns immediately if scan is running
|
||||||
victron.loop();
|
loopCount++;
|
||||||
|
|
||||||
// Optional: You can also manually query device data
|
uint32_t now = millis();
|
||||||
// This is useful if you're not using callbacks
|
if (now - lastReport >= 10000) {
|
||||||
/*
|
Serial.printf("Loop iterations in last 10s: %lu\n", loopCount);
|
||||||
SolarChargerData solarData;
|
loopCount = 0;
|
||||||
if (victron.getSolarChargerData("E7:48:D4:28:B7:9C", solarData)) {
|
lastReport = now;
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ struct __attribute__((packed)) SolarChargerPacket {
|
|||||||
uint8_t chargeState;
|
uint8_t chargeState;
|
||||||
float batteryVoltage; // V
|
float batteryVoltage; // V
|
||||||
float batteryCurrent; // A
|
float batteryCurrent; // A
|
||||||
float panelVoltage; // V
|
|
||||||
float panelPower; // W
|
float panelPower; // W
|
||||||
uint16_t yieldToday; // Wh
|
uint16_t yieldToday; // Wh
|
||||||
float loadCurrent; // A
|
float loadCurrent; // A
|
||||||
@@ -82,13 +81,12 @@ void onDataRecv(const uint8_t* senderMac, const uint8_t* data, int len) {
|
|||||||
memcpy(name, pkt->deviceName, 16);
|
memcpy(name, pkt->deviceName, 16);
|
||||||
name[16] = '\0';
|
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,
|
recvCount,
|
||||||
name,
|
name,
|
||||||
chargeStateName(pkt->chargeState),
|
chargeStateName(pkt->chargeState),
|
||||||
pkt->batteryVoltage,
|
pkt->batteryVoltage,
|
||||||
pkt->batteryCurrent,
|
pkt->batteryCurrent,
|
||||||
pkt->panelVoltage,
|
|
||||||
pkt->panelPower,
|
pkt->panelPower,
|
||||||
pkt->yieldToday);
|
pkt->yieldToday);
|
||||||
|
|
||||||
@@ -202,7 +200,7 @@ void loop() {
|
|||||||
M5.Lcd.printf("Batt: %.2fA\n", pkt.batteryCurrent);
|
M5.Lcd.printf("Batt: %.2fA\n", pkt.batteryCurrent);
|
||||||
|
|
||||||
// Row 3: PV
|
// 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
|
// Row 4: yield + load
|
||||||
M5.Lcd.printf("Yield: %uWh", pkt.yieldToday);
|
M5.Lcd.printf("Yield: %uWh", pkt.yieldToday);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* VictronBLE Repeater Example
|
* VictronBLE Repeater Example
|
||||||
*
|
*
|
||||||
* Collects Solar Charger data via BLE and transmits the latest
|
* 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.
|
* near Victron devices and use a separate Receiver ESP32 at a distance.
|
||||||
*
|
*
|
||||||
* ESPNow range is typically much greater than BLE (~200m+ line of sight).
|
* ESPNow range is typically much greater than BLE (~200m+ line of sight).
|
||||||
@@ -23,7 +23,6 @@ struct __attribute__((packed)) SolarChargerPacket {
|
|||||||
uint8_t chargeState;
|
uint8_t chargeState;
|
||||||
float batteryVoltage; // V
|
float batteryVoltage; // V
|
||||||
float batteryCurrent; // A
|
float batteryCurrent; // A
|
||||||
float panelVoltage; // V
|
|
||||||
float panelPower; // W
|
float panelPower; // W
|
||||||
uint16_t yieldToday; // Wh
|
uint16_t yieldToday; // Wh
|
||||||
float loadCurrent; // A
|
float loadCurrent; // A
|
||||||
@@ -31,10 +30,8 @@ struct __attribute__((packed)) SolarChargerPacket {
|
|||||||
char deviceName[16]; // Null-terminated, truncated
|
char deviceName[16]; // Null-terminated, truncated
|
||||||
};
|
};
|
||||||
|
|
||||||
// Broadcast address
|
|
||||||
static const uint8_t BROADCAST_ADDR[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
static const uint8_t BROADCAST_ADDR[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||||
|
static const unsigned long SEND_INTERVAL_MS = 5000;
|
||||||
static const unsigned long SEND_INTERVAL_MS = 5000; // 30 seconds
|
|
||||||
|
|
||||||
static uint32_t sendCount = 0;
|
static uint32_t sendCount = 0;
|
||||||
static uint32_t sendFailCount = 0;
|
static uint32_t sendFailCount = 0;
|
||||||
@@ -49,7 +46,6 @@ static unsigned long lastSendTime = 0;
|
|||||||
|
|
||||||
VictronBLE victron;
|
VictronBLE victron;
|
||||||
|
|
||||||
// Find cached slot by device name, or allocate a new one
|
|
||||||
static int findOrAddCached(const char* name) {
|
static int findOrAddCached(const char* name) {
|
||||||
for (int i = 0; i < cachedCount; i++) {
|
for (int i = 0; i < cachedCount; i++) {
|
||||||
if (strncmp(cachedPackets[i].deviceName, name, sizeof(cachedPackets[i].deviceName)) == 0)
|
if (strncmp(cachedPackets[i].deviceName, name, sizeof(cachedPackets[i].deviceName)) == 0)
|
||||||
@@ -59,34 +55,28 @@ static int findOrAddCached(const char* name) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
class RepeaterCallback : public VictronDeviceCallback {
|
void onVictronData(const VictronDevice* dev) {
|
||||||
public:
|
if (dev->deviceType != DEVICE_TYPE_SOLAR_CHARGER) return;
|
||||||
void onSolarChargerData(const SolarChargerData& data) override {
|
blePacketCount++;
|
||||||
blePacketCount++;
|
const auto& s = dev->solar;
|
||||||
|
|
||||||
// Build packet
|
SolarChargerPacket pkt;
|
||||||
SolarChargerPacket pkt;
|
pkt.chargeState = s.chargeState;
|
||||||
pkt.chargeState = static_cast<uint8_t>(data.chargeState);
|
pkt.batteryVoltage = s.batteryVoltage;
|
||||||
pkt.batteryVoltage = data.batteryVoltage;
|
pkt.batteryCurrent = s.batteryCurrent;
|
||||||
pkt.batteryCurrent = data.batteryCurrent;
|
pkt.panelPower = s.panelPower;
|
||||||
pkt.panelVoltage = data.panelVoltage;
|
pkt.yieldToday = s.yieldToday;
|
||||||
pkt.panelPower = data.panelPower;
|
pkt.loadCurrent = s.loadCurrent;
|
||||||
pkt.yieldToday = data.yieldToday;
|
pkt.rssi = dev->rssi;
|
||||||
pkt.loadCurrent = data.loadCurrent;
|
memset(pkt.deviceName, 0, sizeof(pkt.deviceName));
|
||||||
pkt.rssi = data.rssi;
|
strncpy(pkt.deviceName, dev->name, sizeof(pkt.deviceName) - 1);
|
||||||
memset(pkt.deviceName, 0, sizeof(pkt.deviceName));
|
|
||||||
strncpy(pkt.deviceName, data.deviceName.c_str(), sizeof(pkt.deviceName) - 1);
|
|
||||||
|
|
||||||
// Cache it
|
int idx = findOrAddCached(pkt.deviceName);
|
||||||
int idx = findOrAddCached(pkt.deviceName);
|
if (idx >= 0) {
|
||||||
if (idx >= 0) {
|
cachedPackets[idx] = pkt;
|
||||||
cachedPackets[idx] = pkt;
|
cachedValid[idx] = true;
|
||||||
cachedValid[idx] = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
RepeaterCallback callback;
|
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
@@ -98,7 +88,8 @@ void setup() {
|
|||||||
WiFi.mode(WIFI_STA);
|
WiFi.mode(WIFI_STA);
|
||||||
WiFi.disconnect();
|
WiFi.disconnect();
|
||||||
|
|
||||||
Serial.println("MAC: " + WiFi.macAddress());
|
Serial.print("MAC: ");
|
||||||
|
Serial.println(WiFi.macAddress());
|
||||||
|
|
||||||
// Init ESPNow
|
// Init ESPNow
|
||||||
if (esp_now_init() != ESP_OK) {
|
if (esp_now_init() != ESP_OK) {
|
||||||
@@ -106,10 +97,9 @@ void setup() {
|
|||||||
while (1) delay(1000);
|
while (1) delay(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add broadcast peer
|
|
||||||
esp_now_peer_info_t peerInfo = {};
|
esp_now_peer_info_t peerInfo = {};
|
||||||
memcpy(peerInfo.peer_addr, BROADCAST_ADDR, 6);
|
memcpy(peerInfo.peer_addr, BROADCAST_ADDR, 6);
|
||||||
peerInfo.channel = 0; // Use current channel
|
peerInfo.channel = 0;
|
||||||
peerInfo.encrypt = false;
|
peerInfo.encrypt = false;
|
||||||
|
|
||||||
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
|
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
|
||||||
@@ -122,14 +112,12 @@ void setup() {
|
|||||||
// Init VictronBLE
|
// Init VictronBLE
|
||||||
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());
|
|
||||||
while (1) delay(1000);
|
while (1) delay(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
victron.setDebug(false);
|
victron.setDebug(false);
|
||||||
victron.setCallback(&callback);
|
victron.setCallback(onVictronData);
|
||||||
|
|
||||||
// Add your devices here
|
|
||||||
victron.addDevice(
|
victron.addDevice(
|
||||||
"Rainbow48V",
|
"Rainbow48V",
|
||||||
"E4:05:42:34:14:F3",
|
"E4:05:42:34:14:F3",
|
||||||
@@ -144,14 +132,13 @@ void setup() {
|
|||||||
DEVICE_TYPE_SOLAR_CHARGER
|
DEVICE_TYPE_SOLAR_CHARGER
|
||||||
);
|
);
|
||||||
|
|
||||||
Serial.println("Configured " + String(victron.getDeviceCount()) + " BLE devices");
|
Serial.printf("Configured %d BLE devices\n", (int)victron.getDeviceCount());
|
||||||
Serial.println("Packet size: " + String(sizeof(SolarChargerPacket)) + " bytes\n");
|
Serial.printf("Packet size: %d bytes\n\n", (int)sizeof(SolarChargerPacket));
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
victron.loop();
|
victron.loop(); // Blocks for scanDuration seconds
|
||||||
|
|
||||||
// Send cached packets every 30 seconds
|
|
||||||
unsigned long now = millis();
|
unsigned long now = millis();
|
||||||
if (now - lastSendTime >= SEND_INTERVAL_MS) {
|
if (now - lastSendTime >= SEND_INTERVAL_MS) {
|
||||||
lastSendTime = now;
|
lastSendTime = now;
|
||||||
@@ -167,11 +154,10 @@ void loop() {
|
|||||||
if (result == ESP_OK) {
|
if (result == ESP_OK) {
|
||||||
sendCount++;
|
sendCount++;
|
||||||
sent++;
|
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].deviceName,
|
||||||
cachedPackets[i].batteryVoltage,
|
cachedPackets[i].batteryVoltage,
|
||||||
cachedPackets[i].batteryCurrent,
|
cachedPackets[i].batteryCurrent,
|
||||||
cachedPackets[i].panelVoltage,
|
|
||||||
cachedPackets[i].panelPower,
|
cachedPackets[i].panelPower,
|
||||||
cachedPackets[i].chargeState);
|
cachedPackets[i].chargeState);
|
||||||
} else {
|
} else {
|
||||||
@@ -185,5 +171,4 @@ void loop() {
|
|||||||
blePacketCount, sendCount, sendFailCount, cachedCount);
|
blePacketCount, sendCount, sendFailCount, cachedCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
delay(100);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,634 +7,361 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "VictronBLE.h"
|
#include "VictronBLE.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
// Constructor
|
|
||||||
VictronBLE::VictronBLE()
|
VictronBLE::VictronBLE()
|
||||||
: pBLEScan(nullptr), scanCallback(nullptr), callback(nullptr),
|
: deviceCount(0), pBLEScan(nullptr), scanCallbackObj(nullptr),
|
||||||
debugEnabled(false), scanDuration(5), initialized(false) {
|
callback(nullptr), debugEnabled(false), scanDuration(5),
|
||||||
|
minIntervalMs(1000), 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) {
|
bool VictronBLE::begin(uint32_t scanDuration) {
|
||||||
if (initialized) {
|
if (initialized) return true;
|
||||||
if (debugEnabled) debugPrint("VictronBLE already initialized");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->scanDuration = scanDuration;
|
this->scanDuration = scanDuration;
|
||||||
|
|
||||||
if (debugEnabled) debugPrint("Initializing VictronBLE...");
|
|
||||||
|
|
||||||
BLEDevice::init("VictronBLE");
|
BLEDevice::init("VictronBLE");
|
||||||
pBLEScan = BLEDevice::getScan();
|
pBLEScan = BLEDevice::getScan();
|
||||||
|
if (!pBLEScan) return false;
|
||||||
|
|
||||||
if (!pBLEScan) {
|
scanCallbackObj = new VictronBLEAdvertisedDeviceCallbacks(this);
|
||||||
lastError = "Failed to create BLE scanner";
|
pBLEScan->setAdvertisedDeviceCallbacks(scanCallbackObj, true);
|
||||||
if (debugEnabled) debugPrint(lastError);
|
pBLEScan->setActiveScan(false);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
scanCallback = new VictronBLEAdvertisedDeviceCallbacks(this);
|
|
||||||
pBLEScan->setAdvertisedDeviceCallbacks(scanCallback, true);
|
|
||||||
pBLEScan->setActiveScan(false); // Passive scan - lower power
|
|
||||||
pBLEScan->setInterval(100);
|
pBLEScan->setInterval(100);
|
||||||
pBLEScan->setWindow(99);
|
pBLEScan->setWindow(99);
|
||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
if (debugEnabled) debugPrint("VictronBLE initialized successfully");
|
if (debugEnabled) Serial.println("[VictronBLE] Initialized");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a device to monitor
|
bool VictronBLE::addDevice(const char* name, const char* mac, const char* hexKey,
|
||||||
bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
|
VictronDeviceType type) {
|
||||||
if (config.macAddress.length() == 0) {
|
if (deviceCount >= VICTRON_MAX_DEVICES) return false;
|
||||||
lastError = "MAC address cannot be empty";
|
if (!hexKey || strlen(hexKey) != 32) return false;
|
||||||
if (debugEnabled) debugPrint(lastError);
|
if (!mac || strlen(mac) == 0) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.encryptionKey.length() != 32) {
|
char normalizedMAC[VICTRON_MAC_LEN];
|
||||||
lastError = "Encryption key must be 32 hex characters";
|
normalizeMAC(mac, normalizedMAC);
|
||||||
if (debugEnabled) debugPrint(lastError);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String normalizedMAC = normalizeMAC(config.macAddress);
|
// Check for duplicate
|
||||||
|
if (findDevice(normalizedMAC)) return false;
|
||||||
|
|
||||||
// Check if device already exists
|
DeviceEntry* entry = &devices[deviceCount];
|
||||||
if (devices.find(normalizedMAC) != devices.end()) {
|
memset(entry, 0, sizeof(DeviceEntry));
|
||||||
if (debugEnabled) debugPrint("Device " + normalizedMAC + " already exists, updating config");
|
entry->active = true;
|
||||||
delete devices[normalizedMAC];
|
|
||||||
}
|
|
||||||
|
|
||||||
DeviceInfo* info = new DeviceInfo();
|
strncpy(entry->device.name, name ? name : "", VICTRON_NAME_LEN - 1);
|
||||||
info->config = config;
|
entry->device.name[VICTRON_NAME_LEN - 1] = '\0';
|
||||||
info->config.macAddress = normalizedMAC;
|
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 (!hexToBytes(hexKey, entry->key, 16)) return false;
|
||||||
if (!hexStringToBytes(config.encryptionKey, info->encryptionKeyBytes, 16)) {
|
|
||||||
lastError = "Invalid encryption key format";
|
|
||||||
if (debugEnabled) debugPrint(lastError);
|
|
||||||
delete info;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create appropriate data structure based on device type
|
deviceCount++;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (debugEnabled) Serial.printf("[VictronBLE] Added: %s (%s)\n", name, normalizedMAC);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VictronBLE::addDevice(const String& name, const String& macAddress, const String& encryptionKey,
|
// Scan complete callback — sets flag so loop() restarts
|
||||||
VictronDeviceType expectedType) {
|
static bool s_scanning = false;
|
||||||
VictronDeviceConfig config(name, macAddress, encryptionKey, expectedType);
|
static void onScanDone(BLEScanResults results) {
|
||||||
return addDevice(config);
|
s_scanning = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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() {
|
void VictronBLE::loop() {
|
||||||
if (!initialized) {
|
if (!initialized) return;
|
||||||
return;
|
if (!s_scanning) {
|
||||||
|
pBLEScan->clearResults();
|
||||||
|
s_scanning = pBLEScan->start(scanDuration, onScanDone, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a scan
|
|
||||||
BLEScanResults scanResults = pBLEScan->start(scanDuration, false);
|
|
||||||
pBLEScan->clearResults();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BLE callback implementation
|
// BLE scan callback
|
||||||
void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertisedDevice) {
|
void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertisedDevice) {
|
||||||
if (victronBLE) {
|
if (victronBLE) victronBLE->processDevice(advertisedDevice);
|
||||||
victronBLE->processDevice(advertisedDevice);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process advertised device
|
|
||||||
void VictronBLE::processDevice(BLEAdvertisedDevice& advertisedDevice) {
|
void VictronBLE::processDevice(BLEAdvertisedDevice& advertisedDevice) {
|
||||||
// Get MAC address from the advertised device
|
if (!advertisedDevice.haveManufacturerData()) return;
|
||||||
String mac = macAddressToString(advertisedDevice.getAddress());
|
|
||||||
String normalizedMAC = normalizeMAC(mac);
|
|
||||||
|
|
||||||
if (debugEnabled) {
|
std::string raw = advertisedDevice.getManufacturerData();
|
||||||
debugPrint("Raw MAC: " + mac + " -> Normalized: " + normalizedMAC);
|
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;
|
victronManufacturerData mfgData;
|
||||||
memset(&mfgData, 0, sizeof(mfgData));
|
memset(&mfgData, 0, sizeof(mfgData));
|
||||||
if (advertisedDevice.haveManufacturerData()) {
|
size_t copyLen = raw.length() > sizeof(mfgData) ? sizeof(mfgData) : raw.length();
|
||||||
std::string rawMfgData = advertisedDevice.getManufacturerData();
|
raw.copy(reinterpret_cast<char*>(&mfgData), copyLen);
|
||||||
if (debugEnabled) debugPrint("Getting manufacturer data: Size=" + String(rawMfgData.length()));
|
|
||||||
rawMfgData.copy(reinterpret_cast<char*>(&mfgData),
|
|
||||||
(rawMfgData.length() > sizeof(mfgData) ? sizeof(mfgData) : rawMfgData.length()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug: Log all discovered BLE devices
|
// Normalize MAC and find device
|
||||||
if (debugEnabled) {
|
char normalizedMAC[VICTRON_MAC_LEN];
|
||||||
String debugMsg = "BLE Device: " + mac;
|
normalizeMAC(advertisedDevice.getAddress().toString().c_str(), normalizedMAC);
|
||||||
debugMsg += ", RSSI: " + String(advertisedDevice.getRSSI()) + " dBm";
|
|
||||||
if (advertisedDevice.haveName())
|
|
||||||
debugMsg += ", Name: " + String(advertisedDevice.getName().c_str());
|
|
||||||
|
|
||||||
debugMsg += ", Mfg ID: 0x" + String(mfgData.vendorID, HEX);
|
DeviceEntry* entry = findDevice(normalizedMAC);
|
||||||
if (mfgData.vendorID == VICTRON_MANUFACTURER_ID) {
|
if (!entry) {
|
||||||
debugMsg += " (Victron)";
|
if (debugEnabled) Serial.printf("[VictronBLE] Unmonitored Victron: %s\n", normalizedMAC);
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debugEnabled) debugPrint("Processing data from: " + deviceInfo->config.name);
|
// Skip if nonce unchanged (data hasn't changed on the device)
|
||||||
|
if (entry->device.dataValid && mfgData.nonceDataCounter == entry->lastNonce) {
|
||||||
|
// Still update RSSI since we got a packet
|
||||||
|
entry->device.rssi = advertisedDevice.getRSSI();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Parse the advertisement
|
// Skip if minimum interval hasn't elapsed
|
||||||
if (parseAdvertisement(deviceInfo, mfgData)) {
|
uint32_t now = millis();
|
||||||
// Update RSSI
|
if (entry->device.dataValid && (now - entry->device.lastUpdate) < minIntervalMs) {
|
||||||
if (deviceInfo->data) {
|
return;
|
||||||
deviceInfo->data->rssi = advertisedDevice.getRSSI();
|
}
|
||||||
deviceInfo->data->lastUpdate = millis();
|
|
||||||
}
|
if (debugEnabled) Serial.printf("[VictronBLE] Processing: %s nonce:0x%04X\n",
|
||||||
|
entry->device.name, mfgData.nonceDataCounter);
|
||||||
|
|
||||||
|
if (parseAdvertisement(entry, mfgData)) {
|
||||||
|
entry->lastNonce = mfgData.nonceDataCounter;
|
||||||
|
entry->device.rssi = advertisedDevice.getRSSI();
|
||||||
|
entry->device.lastUpdate = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse advertisement data
|
bool VictronBLE::parseAdvertisement(DeviceEntry* entry, const victronManufacturerData& mfg) {
|
||||||
bool VictronBLE::parseAdvertisement(DeviceInfo* deviceInfo, const victronManufacturerData& mfgData) {
|
|
||||||
if (debugEnabled) {
|
if (debugEnabled) {
|
||||||
debugPrint("Vendor ID: 0x" + String(mfgData.vendorID, HEX));
|
Serial.printf("[VictronBLE] Beacon:0x%02X Record:0x%02X Nonce:0x%04X\n",
|
||||||
debugPrint("Beacon Type: 0x" + String(mfgData.beaconType, HEX));
|
mfg.beaconType, mfg.victronRecordType, mfg.nonceDataCounter);
|
||||||
debugPrint("Record Type: 0x" + String(mfgData.victronRecordType, HEX));
|
|
||||||
debugPrint("Nonce: 0x" + String(mfgData.nonceDataCounter, HEX));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build IV (initialization vector) from nonce
|
// Quick key check before expensive decryption
|
||||||
// IV is 16 bytes: nonce (2 bytes little-endian) + zeros (14 bytes)
|
if (mfg.encryptKeyMatch != entry->key[0]) {
|
||||||
uint8_t iv[16] = {0};
|
if (debugEnabled) Serial.println("[VictronBLE] Key byte mismatch");
|
||||||
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);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse based on device type
|
// Build IV from nonce (2 bytes little-endian + 14 zero bytes)
|
||||||
bool parseOk = false;
|
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:
|
case DEVICE_TYPE_SOLAR_CHARGER:
|
||||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
|
entry->device.deviceType = DEVICE_TYPE_SOLAR_CHARGER;
|
||||||
parseOk = parseSolarCharger(decrypted, encryptedLen,
|
ok = parseSolarCharger(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.solar);
|
||||||
*static_cast<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) {
|
entry->device.deviceType = DEVICE_TYPE_BATTERY_MONITOR;
|
||||||
parseOk = parseBatteryMonitor(decrypted, encryptedLen,
|
ok = parseBatteryMonitor(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.battery);
|
||||||
*static_cast<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:
|
||||||
case DEVICE_TYPE_VE_BUS:
|
case DEVICE_TYPE_VE_BUS:
|
||||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_INVERTER) {
|
entry->device.deviceType = DEVICE_TYPE_INVERTER;
|
||||||
parseOk = parseInverter(decrypted, encryptedLen,
|
ok = parseInverter(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.inverter);
|
||||||
*static_cast<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) {
|
entry->device.deviceType = DEVICE_TYPE_DCDC_CONVERTER;
|
||||||
parseOk = parseDCDCConverter(decrypted, encryptedLen,
|
ok = parseDCDCConverter(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.dcdc);
|
||||||
*static_cast<DCDCConverterData*>(deviceInfo->data));
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parseOk && deviceInfo->data) {
|
if (ok) {
|
||||||
deviceInfo->data->dataValid = true;
|
entry->device.dataValid = true;
|
||||||
|
if (callback) callback(&entry->device);
|
||||||
// Call appropriate callback
|
|
||||||
if (callback) {
|
|
||||||
switch (mfgData.victronRecordType) {
|
|
||||||
case DEVICE_TYPE_SOLAR_CHARGER:
|
|
||||||
callback->onSolarChargerData(*static_cast<SolarChargerData*>(deviceInfo->data));
|
|
||||||
break;
|
|
||||||
case DEVICE_TYPE_BATTERY_MONITOR:
|
|
||||||
callback->onBatteryMonitorData(*static_cast<BatteryMonitorData*>(deviceInfo->data));
|
|
||||||
break;
|
|
||||||
case DEVICE_TYPE_INVERTER:
|
|
||||||
case DEVICE_TYPE_INVERTER_RS:
|
|
||||||
case DEVICE_TYPE_MULTI_RS:
|
|
||||||
case DEVICE_TYPE_VE_BUS:
|
|
||||||
callback->onInverterData(*static_cast<InverterData*>(deviceInfo->data));
|
|
||||||
break;
|
|
||||||
case DEVICE_TYPE_DCDC_CONVERTER:
|
|
||||||
callback->onDCDCConverterData(*static_cast<DCDCConverterData*>(deviceInfo->data));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseOk;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt advertisement using AES-128-CTR
|
bool VictronBLE::decryptData(const uint8_t* encrypted, size_t len,
|
||||||
bool VictronBLE::decryptAdvertisement(const uint8_t* encrypted, size_t encLen,
|
const uint8_t* key, const uint8_t* iv,
|
||||||
const uint8_t* key, const uint8_t* iv,
|
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
|
if (mbedtls_aes_setkey_enc(&aes, key, 128) != 0) {
|
||||||
int ret = mbedtls_aes_setkey_enc(&aes, key, 128);
|
|
||||||
if (ret != 0) {
|
|
||||||
mbedtls_aes_free(&aes);
|
mbedtls_aes_free(&aes);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,
|
int ret = mbedtls_aes_crypt_ctr(&aes, len, &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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse Solar Charger data
|
bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, VictronSolarData& result) {
|
||||||
bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarChargerData& result) {
|
if (len < sizeof(victronSolarChargerPayload)) return false;
|
||||||
if (len < sizeof(victronSolarChargerPayload)) {
|
const auto* p = reinterpret_cast<const victronSolarChargerPayload*>(data);
|
||||||
if (debugEnabled) debugPrint("Solar charger data too short: " + String(len) + " bytes");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto* payload = reinterpret_cast<const victronSolarChargerPayload*>(data);
|
result.chargeState = p->deviceState;
|
||||||
|
result.errorCode = p->errorCode;
|
||||||
// Parse charge state
|
result.batteryVoltage = p->batteryVoltage * 0.01f;
|
||||||
result.chargeState = static_cast<SolarChargerState>(payload->deviceState);
|
result.batteryCurrent = p->batteryCurrent * 0.01f;
|
||||||
|
result.yieldToday = p->yieldToday * 10;
|
||||||
// Parse battery voltage (10 mV units -> volts)
|
result.panelPower = p->inputPower;
|
||||||
result.batteryVoltage = payload->batteryVoltage * 0.01f;
|
result.loadCurrent = (p->loadCurrent != 0xFFFF) ? p->loadCurrent * 0.01f : 0;
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debugEnabled) {
|
if (debugEnabled) {
|
||||||
debugPrint("Solar Charger: " + String(result.batteryVoltage, 2) + "V, " +
|
Serial.printf("[VictronBLE] Solar: %.2fV %.2fA %dW State:%d\n",
|
||||||
String(result.batteryCurrent, 2) + "A, " +
|
result.batteryVoltage, result.batteryCurrent,
|
||||||
String(result.panelPower) + "W, State: " + String(result.chargeState));
|
(int)result.panelPower, result.chargeState);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse Battery Monitor data
|
bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, VictronBatteryData& result) {
|
||||||
bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result) {
|
if (len < sizeof(victronBatteryMonitorPayload)) return false;
|
||||||
if (len < sizeof(victronBatteryMonitorPayload)) {
|
const auto* p = reinterpret_cast<const victronBatteryMonitorPayload*>(data);
|
||||||
if (debugEnabled) debugPrint("Battery monitor data too short: " + String(len) + " bytes");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto* payload = reinterpret_cast<const victronBatteryMonitorPayload*>(data);
|
result.remainingMinutes = p->remainingMins;
|
||||||
|
result.voltage = p->batteryVoltage * 0.01f;
|
||||||
|
|
||||||
// Parse remaining time (1 minute units)
|
// Alarm bits
|
||||||
result.remainingMinutes = payload->remainingMins;
|
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)
|
// Aux data: voltage or temperature (heuristic: < 30V = voltage)
|
||||||
result.voltage = payload->batteryVoltage * 0.01f;
|
// NOTE: Victron protocol uses a flag bit for this, but it's not exposed
|
||||||
|
// in the BLE advertisement. This heuristic may misclassify edge cases.
|
||||||
// Parse alarm bits
|
if (p->auxData < 3000) {
|
||||||
result.alarmLowVoltage = (payload->alarms & 0x01) != 0;
|
result.auxVoltage = p->auxData * 0.01f;
|
||||||
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;
|
|
||||||
result.temperature = 0;
|
result.temperature = 0;
|
||||||
} else { // Otherwise temperature in 0.01 Kelvin
|
} else {
|
||||||
result.temperature = (payload->auxData * 0.01f) - 273.15f;
|
result.temperature = (p->auxData * 0.01f) - 273.15f;
|
||||||
result.auxVoltage = 0;
|
result.auxVoltage = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse battery current (22-bit signed, 1 mA units)
|
// Battery current (22-bit signed, 1 mA units)
|
||||||
int32_t current = payload->currentLow |
|
int32_t current = p->currentLow |
|
||||||
(payload->currentMid << 8) |
|
(p->currentMid << 8) |
|
||||||
((payload->currentHigh_consumedLow & 0x3F) << 16);
|
((p->currentHigh_consumedLow & 0x3F) << 16);
|
||||||
// Sign extend from 22 bits to 32 bits
|
if (current & 0x200000) current |= 0xFFC00000; // Sign extend
|
||||||
if (current & 0x200000) {
|
result.current = current * 0.001f;
|
||||||
current |= 0xFFC00000;
|
|
||||||
}
|
|
||||||
result.current = current * 0.001f; // Convert mA to A
|
|
||||||
|
|
||||||
// Parse consumed Ah (18-bit signed, 10 mAh units)
|
// Consumed Ah (18-bit signed, 10 mAh units)
|
||||||
int32_t consumedAh = ((payload->currentHigh_consumedLow & 0xC0) >> 6) |
|
int32_t consumedAh = ((p->currentHigh_consumedLow & 0xC0) >> 6) |
|
||||||
(payload->consumedMid << 2) |
|
(p->consumedMid << 2) |
|
||||||
(payload->consumedHigh << 10);
|
(p->consumedHigh << 10);
|
||||||
// Sign extend from 18 bits to 32 bits
|
if (consumedAh & 0x20000) consumedAh |= 0xFFFC0000; // Sign extend
|
||||||
if (consumedAh & 0x20000) {
|
result.consumedAh = consumedAh * 0.01f;
|
||||||
consumedAh |= 0xFFFC0000;
|
|
||||||
}
|
|
||||||
result.consumedAh = consumedAh * 0.01f; // Convert 10mAh to Ah
|
|
||||||
|
|
||||||
// Parse SOC (10-bit value, 10 = 1.0%)
|
// SOC (10-bit, 0.1% units)
|
||||||
result.soc = (payload->soc & 0x3FF) * 0.1f;
|
result.soc = (p->soc & 0x3FF) * 0.1f;
|
||||||
|
|
||||||
if (debugEnabled) {
|
if (debugEnabled) {
|
||||||
debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " +
|
Serial.printf("[VictronBLE] Battery: %.2fV %.2fA SOC:%.1f%%\n",
|
||||||
String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%");
|
result.voltage, result.current, result.soc);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse Inverter data
|
bool VictronBLE::parseInverter(const uint8_t* data, size_t len, VictronInverterData& result) {
|
||||||
bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& result) {
|
if (len < sizeof(victronInverterPayload)) return false;
|
||||||
if (len < sizeof(victronInverterPayload)) {
|
const auto* p = reinterpret_cast<const victronInverterPayload*>(data);
|
||||||
if (debugEnabled) debugPrint("Inverter data too short: " + String(len) + " bytes");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto* payload = reinterpret_cast<const victronInverterPayload*>(data);
|
result.state = p->deviceState;
|
||||||
|
result.batteryVoltage = p->batteryVoltage * 0.01f;
|
||||||
|
result.batteryCurrent = p->batteryCurrent * 0.01f;
|
||||||
|
|
||||||
// Parse device state
|
// AC Power (signed 24-bit)
|
||||||
result.state = payload->deviceState;
|
int32_t acPower = p->acPowerLow | (p->acPowerMid << 8) | (p->acPowerHigh << 16);
|
||||||
|
if (acPower & 0x800000) acPower |= 0xFF000000; // Sign extend
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
result.acPower = acPower;
|
result.acPower = acPower;
|
||||||
|
|
||||||
// Parse alarm bits
|
// Alarm bits
|
||||||
result.alarmLowVoltage = (payload->alarms & 0x01) != 0;
|
result.alarmLowVoltage = (p->alarms & 0x01) != 0;
|
||||||
result.alarmHighVoltage = (payload->alarms & 0x02) != 0;
|
result.alarmHighVoltage = (p->alarms & 0x02) != 0;
|
||||||
result.alarmHighTemperature = (payload->alarms & 0x04) != 0;
|
result.alarmHighTemperature = (p->alarms & 0x04) != 0;
|
||||||
result.alarmOverload = (payload->alarms & 0x08) != 0;
|
result.alarmOverload = (p->alarms & 0x08) != 0;
|
||||||
|
|
||||||
if (debugEnabled) {
|
if (debugEnabled) {
|
||||||
debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " +
|
Serial.printf("[VictronBLE] Inverter: %.2fV %dW State:%d\n",
|
||||||
String(result.acPower) + "W, State: " + String(result.state));
|
result.batteryVoltage, (int)result.acPower, result.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse DC-DC Converter data
|
bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, VictronDCDCData& result) {
|
||||||
bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result) {
|
if (len < sizeof(victronDCDCConverterPayload)) return false;
|
||||||
if (len < sizeof(victronDCDCConverterPayload)) {
|
const auto* p = reinterpret_cast<const victronDCDCConverterPayload*>(data);
|
||||||
if (debugEnabled) debugPrint("DC-DC converter data too short: " + String(len) + " bytes");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto* payload = reinterpret_cast<const victronDCDCConverterPayload*>(data);
|
result.chargeState = p->chargeState;
|
||||||
|
result.errorCode = p->errorCode;
|
||||||
// Parse charge state
|
result.inputVoltage = p->inputVoltage * 0.01f;
|
||||||
result.chargeState = payload->chargeState;
|
result.outputVoltage = p->outputVoltage * 0.01f;
|
||||||
|
result.outputCurrent = p->outputCurrent * 0.01f;
|
||||||
// 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;
|
|
||||||
|
|
||||||
if (debugEnabled) {
|
if (debugEnabled) {
|
||||||
debugPrint("DC-DC Converter: In=" + String(result.inputVoltage, 2) + "V, Out=" +
|
Serial.printf("[VictronBLE] DC-DC: In=%.2fV Out=%.2fV %.2fA\n",
|
||||||
String(result.outputVoltage, 2) + "V, " + String(result.outputCurrent, 2) + "A");
|
result.inputVoltage, result.outputVoltage, result.outputCurrent);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get data methods
|
// --- Helpers ---
|
||||||
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<SolarChargerData*>(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<BatteryMonitorData*>(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<InverterData*>(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<DCDCConverterData*>(it->second->data);
|
|
||||||
return data.dataValid;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get devices by type
|
|
||||||
std::vector<String> VictronBLE::getDevicesByType(VictronDeviceType type) {
|
|
||||||
std::vector<String> result;
|
|
||||||
|
|
||||||
for (const auto& pair : devices) {
|
|
||||||
if (pair.second->data && pair.second->data->deviceType == type) {
|
|
||||||
result.push_back(pair.first);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Create device data structure
|
|
||||||
VictronDeviceData* VictronBLE::createDeviceData(VictronDeviceType type) {
|
|
||||||
switch (type) {
|
|
||||||
case DEVICE_TYPE_SOLAR_CHARGER:
|
|
||||||
return new SolarChargerData();
|
|
||||||
case DEVICE_TYPE_BATTERY_MONITOR:
|
|
||||||
return new BatteryMonitorData();
|
|
||||||
case DEVICE_TYPE_INVERTER:
|
|
||||||
case DEVICE_TYPE_INVERTER_RS:
|
|
||||||
case DEVICE_TYPE_MULTI_RS:
|
|
||||||
case DEVICE_TYPE_VE_BUS:
|
|
||||||
return new InverterData();
|
|
||||||
case DEVICE_TYPE_DCDC_CONVERTER:
|
|
||||||
return new DCDCConverterData();
|
|
||||||
default:
|
|
||||||
return new VictronDeviceData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Convert hex string to bytes
|
|
||||||
bool VictronBLE::hexStringToBytes(const String& hex, uint8_t* bytes, size_t len) {
|
|
||||||
if (hex.length() != len * 2) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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++) {
|
for (size_t i = 0; i < len; i++) {
|
||||||
String byteStr = hex.substring(i * 2, i * 2 + 2);
|
uint8_t hi = hex[i * 2], lo = hex[i * 2 + 1];
|
||||||
char* endPtr;
|
if (hi >= '0' && hi <= '9') hi -= '0';
|
||||||
bytes[i] = strtoul(byteStr.c_str(), &endPtr, 16);
|
else if (hi >= 'a' && hi <= 'f') hi = hi - 'a' + 10;
|
||||||
if (*endPtr != '\0') {
|
else if (hi >= 'A' && hi <= 'F') hi = hi - 'A' + 10;
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: MAC address to string
|
void VictronBLE::normalizeMAC(const char* input, char* output) {
|
||||||
String VictronBLE::macAddressToString(BLEAddress address) {
|
int j = 0;
|
||||||
return String(address.toString().c_str());
|
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
|
VictronBLE::DeviceEntry* VictronBLE::findDevice(const char* normalizedMAC) {
|
||||||
String VictronBLE::normalizeMAC(const String& mac) {
|
for (size_t i = 0; i < deviceCount; i++) {
|
||||||
String normalized = mac;
|
if (devices[i].active && strcmp(devices[i].device.mac, normalizedMAC) == 0) {
|
||||||
normalized.toLowerCase();
|
return &devices[i];
|
||||||
normalized.replace("-", "");
|
}
|
||||||
normalized.replace(":", "");
|
}
|
||||||
return normalized;
|
return nullptr;
|
||||||
}
|
|
||||||
|
|
||||||
// Debug helper
|
|
||||||
void VictronBLE::debugPrint(const String& message) {
|
|
||||||
if (debugEnabled)
|
|
||||||
Serial.println("[VictronBLE] " + message);
|
|
||||||
}
|
}
|
||||||
|
|||||||
399
src/VictronBLE.h
399
src/VictronBLE.h
@@ -15,14 +15,16 @@
|
|||||||
#include <BLEDevice.h>
|
#include <BLEDevice.h>
|
||||||
#include <BLEAdvertisedDevice.h>
|
#include <BLEAdvertisedDevice.h>
|
||||||
#include <BLEScan.h>
|
#include <BLEScan.h>
|
||||||
#include <map>
|
|
||||||
#include <vector>
|
|
||||||
#include "mbedtls/aes.h"
|
#include "mbedtls/aes.h"
|
||||||
|
|
||||||
// Victron manufacturer ID
|
// --- Constants ---
|
||||||
static constexpr uint16_t VICTRON_MANUFACTURER_ID = 0x02E1;
|
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 {
|
enum VictronDeviceType {
|
||||||
DEVICE_TYPE_UNKNOWN = 0x00,
|
DEVICE_TYPE_UNKNOWN = 0x00,
|
||||||
DEVICE_TYPE_SOLAR_CHARGER = 0x01,
|
DEVICE_TYPE_SOLAR_CHARGER = 0x01,
|
||||||
@@ -38,7 +40,7 @@ enum VictronDeviceType {
|
|||||||
DEVICE_TYPE_DC_ENERGY_METER = 0x0B
|
DEVICE_TYPE_DC_ENERGY_METER = 0x0B
|
||||||
};
|
};
|
||||||
|
|
||||||
// Device state for Solar Charger
|
// --- Device state for Solar Charger ---
|
||||||
enum SolarChargerState {
|
enum SolarChargerState {
|
||||||
CHARGER_OFF = 0,
|
CHARGER_OFF = 0,
|
||||||
CHARGER_LOW_POWER = 1,
|
CHARGER_LOW_POWER = 1,
|
||||||
@@ -53,163 +55,219 @@ enum SolarChargerState {
|
|||||||
CHARGER_EXTERNAL_CONTROL = 252
|
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 {
|
struct victronManufacturerData {
|
||||||
uint16_t vendorID; // vendor ID
|
uint16_t vendorID;
|
||||||
uint8_t beaconType; // Should be 0x10 (Product Advertisement) for the packets we want
|
uint8_t beaconType; // 0x10 = Product Advertisement
|
||||||
uint8_t unknownData1[3]; // Unknown data
|
uint8_t unknownData1[3];
|
||||||
uint8_t victronRecordType; // Should be 0x01 (Solar Charger) for the packets we want
|
uint8_t victronRecordType; // Device type (see VictronDeviceType)
|
||||||
uint16_t nonceDataCounter; // Nonce
|
uint16_t nonceDataCounter;
|
||||||
uint8_t encryptKeyMatch; // Should match pre-shared encryption key byte 0
|
uint8_t encryptKeyMatch; // Should match encryption key byte 0
|
||||||
uint8_t victronEncryptedData[21]; // (31 bytes max per BLE spec - size of previous elements)
|
uint8_t victronEncryptedData[VICTRON_ENCRYPTED_LEN];
|
||||||
uint8_t nullPad; // extra byte because toCharArray() adds a \0 byte.
|
|
||||||
} __attribute__((packed));
|
} __attribute__((packed));
|
||||||
// Decrypted payload structures for each device type
|
|
||||||
|
|
||||||
// Solar Charger decrypted payload
|
|
||||||
struct victronSolarChargerPayload {
|
struct victronSolarChargerPayload {
|
||||||
uint8_t deviceState; // Charge state (SolarChargerState enum)
|
uint8_t deviceState;
|
||||||
uint8_t errorCode; // Error code
|
uint8_t errorCode;
|
||||||
int16_t batteryVoltage; // Battery voltage in 10mV units
|
int16_t batteryVoltage; // 10mV units
|
||||||
int16_t batteryCurrent; // Battery current in 10mA units (signed)
|
int16_t batteryCurrent; // 10mA units (signed)
|
||||||
uint16_t yieldToday; // Yield today in 10Wh units
|
uint16_t yieldToday; // 10Wh units
|
||||||
uint16_t inputPower; // PV power in 1W units
|
uint16_t inputPower; // 1W units
|
||||||
uint16_t loadCurrent; // Load current in 10mA units (0xFFFF = no load)
|
uint16_t loadCurrent; // 10mA units (0xFFFF = no load)
|
||||||
uint8_t reserved[2]; // Reserved bytes
|
uint8_t reserved[2];
|
||||||
} __attribute__((packed));
|
} __attribute__((packed));
|
||||||
|
|
||||||
// Battery Monitor decrypted payload
|
|
||||||
struct victronBatteryMonitorPayload {
|
struct victronBatteryMonitorPayload {
|
||||||
uint16_t remainingMins; // Time remaining in minutes
|
uint16_t remainingMins;
|
||||||
uint16_t batteryVoltage; // Battery voltage in 10mV units
|
uint16_t batteryVoltage; // 10mV units
|
||||||
uint8_t alarms; // Alarm bits
|
uint8_t alarms;
|
||||||
uint16_t auxData; // Aux voltage (10mV) or temperature (0.01K)
|
uint16_t auxData; // 10mV (voltage) or 0.01K (temperature)
|
||||||
uint8_t currentLow; // Battery current bits 0-7
|
uint8_t currentLow;
|
||||||
uint8_t currentMid; // Battery current bits 8-15
|
uint8_t currentMid;
|
||||||
uint8_t currentHigh_consumedLow; // Current bits 16-21 (low 6 bits), consumed bits 0-1 (high 2 bits)
|
uint8_t currentHigh_consumedLow; // Current bits 16-21 (low 6), consumed bits 0-1 (high 2)
|
||||||
uint8_t consumedMid; // Consumed Ah bits 2-9
|
uint8_t consumedMid;
|
||||||
uint8_t consumedHigh; // Consumed Ah bits 10-17
|
uint8_t consumedHigh;
|
||||||
uint16_t soc; // State of charge in 0.1% units (10-bit value)
|
uint16_t soc; // 0.1% units (10-bit)
|
||||||
uint8_t reserved[2]; // Reserved bytes
|
uint8_t reserved[2];
|
||||||
} __attribute__((packed));
|
} __attribute__((packed));
|
||||||
|
|
||||||
// Inverter decrypted payload
|
|
||||||
struct victronInverterPayload {
|
struct victronInverterPayload {
|
||||||
uint8_t deviceState; // Device state
|
uint8_t deviceState;
|
||||||
uint8_t errorCode; // Error code
|
uint8_t errorCode;
|
||||||
uint16_t batteryVoltage; // Battery voltage in 10mV units
|
uint16_t batteryVoltage; // 10mV units
|
||||||
int16_t batteryCurrent; // Battery current in 10mA units (signed)
|
int16_t batteryCurrent; // 10mA units (signed)
|
||||||
uint8_t acPowerLow; // AC Power bits 0-7
|
uint8_t acPowerLow;
|
||||||
uint8_t acPowerMid; // AC Power bits 8-15
|
uint8_t acPowerMid;
|
||||||
uint8_t acPowerHigh; // AC Power bits 16-23 (signed 24-bit)
|
uint8_t acPowerHigh; // Signed 24-bit
|
||||||
uint8_t alarms; // Alarm bits
|
uint8_t alarms;
|
||||||
uint8_t reserved[4]; // Reserved bytes
|
uint8_t reserved[4];
|
||||||
} __attribute__((packed));
|
} __attribute__((packed));
|
||||||
|
|
||||||
// DC-DC Converter decrypted payload
|
|
||||||
struct victronDCDCConverterPayload {
|
struct victronDCDCConverterPayload {
|
||||||
uint8_t chargeState; // Charge state
|
uint8_t chargeState;
|
||||||
uint8_t errorCode; // Error code
|
uint8_t errorCode;
|
||||||
uint16_t inputVoltage; // Input voltage in 10mV units
|
uint16_t inputVoltage; // 10mV units
|
||||||
uint16_t outputVoltage; // Output voltage in 10mV units
|
uint16_t outputVoltage; // 10mV units
|
||||||
uint16_t outputCurrent; // Output current in 10mA units
|
uint16_t outputCurrent; // 10mA units
|
||||||
uint8_t reserved[6]; // Reserved bytes
|
uint8_t reserved[6];
|
||||||
} __attribute__((packed));
|
} __attribute__((packed));
|
||||||
|
|
||||||
// Base structure for all device data
|
// ============================================================
|
||||||
struct VictronDeviceData {
|
// Parsed data structures (flat, no inheritance)
|
||||||
String deviceName;
|
// ============================================================
|
||||||
String macAddress;
|
|
||||||
VictronDeviceType deviceType;
|
|
||||||
int8_t rssi;
|
|
||||||
uint32_t lastUpdate;
|
|
||||||
bool dataValid;
|
|
||||||
|
|
||||||
VictronDeviceData() : deviceType(DEVICE_TYPE_UNKNOWN), rssi(-100),
|
struct VictronSolarData {
|
||||||
lastUpdate(0), dataValid(false) {}
|
uint8_t chargeState; // SolarChargerState enum
|
||||||
};
|
uint8_t errorCode;
|
||||||
|
|
||||||
// Solar Charger specific data
|
|
||||||
struct SolarChargerData : public VictronDeviceData {
|
|
||||||
SolarChargerState chargeState;
|
|
||||||
float batteryVoltage; // V
|
float batteryVoltage; // V
|
||||||
float batteryCurrent; // A
|
float batteryCurrent; // A
|
||||||
float panelVoltage; // V (PV voltage)
|
|
||||||
float panelPower; // W
|
float panelPower; // W
|
||||||
uint16_t yieldToday; // Wh
|
uint16_t yieldToday; // Wh
|
||||||
float loadCurrent; // A
|
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 VictronBatteryData {
|
||||||
struct BatteryMonitorData : public VictronDeviceData {
|
|
||||||
float voltage; // V
|
float voltage; // V
|
||||||
float current; // A (positive = charging, negative = discharging)
|
float current; // A
|
||||||
float temperature; // °C
|
float temperature; // C (0 if aux is voltage)
|
||||||
float auxVoltage; // V (starter battery or midpoint)
|
float auxVoltage; // V (0 if aux is temperature)
|
||||||
uint16_t remainingMinutes; // Minutes
|
uint16_t remainingMinutes;
|
||||||
float consumedAh; // Ah
|
float consumedAh; // Ah
|
||||||
float soc; // State of Charge %
|
float soc; // %
|
||||||
bool alarmLowVoltage;
|
bool alarmLowVoltage;
|
||||||
bool alarmHighVoltage;
|
bool alarmHighVoltage;
|
||||||
bool alarmLowSOC;
|
bool alarmLowSOC;
|
||||||
bool alarmLowTemperature;
|
bool alarmLowTemperature;
|
||||||
bool alarmHighTemperature;
|
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 VictronInverterData {
|
||||||
struct InverterData : public VictronDeviceData {
|
|
||||||
float batteryVoltage; // V
|
float batteryVoltage; // V
|
||||||
float batteryCurrent; // A
|
float batteryCurrent; // A
|
||||||
float acPower; // W
|
float acPower; // W
|
||||||
uint8_t state; // Inverter state
|
uint8_t state;
|
||||||
bool alarmHighVoltage;
|
|
||||||
bool alarmLowVoltage;
|
bool alarmLowVoltage;
|
||||||
|
bool alarmHighVoltage;
|
||||||
bool alarmHighTemperature;
|
bool alarmHighTemperature;
|
||||||
bool alarmOverload;
|
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 VictronDCDCData {
|
||||||
struct DCDCConverterData : public VictronDeviceData {
|
|
||||||
float inputVoltage; // V
|
float inputVoltage; // V
|
||||||
float outputVoltage; // V
|
float outputVoltage; // V
|
||||||
float outputCurrent; // A
|
float outputCurrent; // A
|
||||||
uint8_t chargeState;
|
uint8_t chargeState;
|
||||||
uint8_t errorCode;
|
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;
|
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; }
|
||||||
|
void setMinInterval(uint32_t ms) { minIntervalMs = ms; }
|
||||||
|
size_t getDeviceCount() const { return deviceCount; }
|
||||||
|
void loop();
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class VictronBLEAdvertisedDeviceCallbacks;
|
||||||
|
|
||||||
|
struct DeviceEntry {
|
||||||
|
VictronDevice device;
|
||||||
|
uint8_t key[16];
|
||||||
|
uint16_t lastNonce;
|
||||||
|
bool active;
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceEntry devices[VICTRON_MAX_DEVICES];
|
||||||
|
size_t deviceCount;
|
||||||
|
BLEScan* pBLEScan;
|
||||||
|
VictronBLEAdvertisedDeviceCallbacks* scanCallbackObj;
|
||||||
|
VictronCallback callback;
|
||||||
|
bool debugEnabled;
|
||||||
|
uint32_t scanDuration;
|
||||||
|
uint32_t minIntervalMs;
|
||||||
|
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 {
|
class VictronDeviceCallback {
|
||||||
public:
|
public:
|
||||||
virtual ~VictronDeviceCallback() {}
|
virtual ~VictronDeviceCallback() {}
|
||||||
@@ -219,114 +277,17 @@ public:
|
|||||||
virtual void onDCDCConverterData(const DCDCConverterData& data) {}
|
virtual void onDCDCConverterData(const DCDCConverterData& data) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Device configuration structure
|
// --- Per-type getter methods (use callback instead) ---
|
||||||
struct VictronDeviceConfig {
|
bool getSolarChargerData(const String& macAddress, SolarChargerData& data);
|
||||||
String name;
|
bool getBatteryMonitorData(const String& macAddress, BatteryMonitorData& data);
|
||||||
String macAddress;
|
bool getInverterData(const String& macAddress, InverterData& data);
|
||||||
String encryptionKey; // 32 character hex string
|
bool getDCDCConverterData(const String& macAddress, DCDCConverterData& data);
|
||||||
VictronDeviceType expectedType;
|
|
||||||
|
|
||||||
VictronDeviceConfig() : expectedType(DEVICE_TYPE_UNKNOWN) {}
|
// --- Other removed methods ---
|
||||||
VictronDeviceConfig(const String& n, const String& mac, const String& key, VictronDeviceType type = DEVICE_TYPE_UNKNOWN)
|
void removeDevice(const String& macAddress);
|
||||||
: name(n), macAddress(mac), encryptionKey(key), expectedType(type) {}
|
std::vector<String> getDevicesByType(VictronDeviceType type);
|
||||||
};
|
String getLastError() const;
|
||||||
|
|
||||||
// Main VictronBLE class
|
#endif // commented-out features
|
||||||
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<String> getDevicesByType(VictronDeviceType type);
|
|
||||||
|
|
||||||
// Enable/disable debug output
|
|
||||||
void setDebug(bool enable) { debugEnabled = enable; }
|
|
||||||
|
|
||||||
// Get last error message
|
|
||||||
String getLastError() const { return lastError; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
friend class VictronBLEAdvertisedDeviceCallbacks;
|
|
||||||
|
|
||||||
struct DeviceInfo {
|
|
||||||
VictronDeviceConfig config;
|
|
||||||
VictronDeviceData* data;
|
|
||||||
uint8_t encryptionKeyBytes[16];
|
|
||||||
|
|
||||||
DeviceInfo() : data(nullptr) {
|
|
||||||
memset(encryptionKeyBytes, 0, 16);
|
|
||||||
}
|
|
||||||
~DeviceInfo() {
|
|
||||||
if (data) delete data;
|
|
||||||
}
|
|
||||||
DeviceInfo(const DeviceInfo&) = delete;
|
|
||||||
DeviceInfo& operator=(const DeviceInfo&) = delete;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::map<String, DeviceInfo*> 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 // VICTRON_BLE_H
|
#endif // VICTRON_BLE_H
|
||||||
|
|||||||
Reference in New Issue
Block a user