Compare commits

6 Commits

14 changed files with 998 additions and 1210 deletions

View File

@@ -198,3 +198,86 @@ a843eb9 Keep v0.3.1
- examples/Receiver/src/main.cpp
- examples/Repeater/src/main.cpp
### Session: 2026-02-28 14:32
**Commits:**
```
4944757 Fix to be non blocking without tasks
31765c7 Update notes
84d153c Single callback version - vastly simplified.
```
**Modified files:**
- examples/Logger/src/main.cpp
- examples/MultiDevice/src/main.cpp
- examples/Repeater/src/main.cpp
- library.json
- library.properties
- src/VictronBLE.cpp
- src/VictronBLE.h
### Session: 2026-02-28 14:33
**Commits:**
```
4944757 Fix to be non blocking without tasks
31765c7 Update notes
84d153c Single callback version - vastly simplified.
```
**Modified files:**
- .claude/CLAUDE.md
- VERSIONS
- examples/Logger/src/main.cpp
- examples/MultiDevice/src/main.cpp
- examples/Repeater/src/main.cpp
- library.json
- library.properties
- src/VictronBLE.cpp
- src/VictronBLE.h
### Session: 2026-02-28 14:36
**Commits:**
```
4944757 Fix to be non blocking without tasks
31765c7 Update notes
```
**Modified files:**
- .claude/CLAUDE.md
- VERSIONS
- examples/Logger/src/main.cpp
- examples/MultiDevice/src/main.cpp
- examples/Repeater/src/main.cpp
- library.json
- library.properties
- src/VictronBLE.cpp
- src/VictronBLE.h
### Session: 2026-02-28 14:40
**Commits:**
```
39a89c8 Versions v0.4 ready for release
4944757 Fix to be non blocking without tasks
31765c7 Update notes
```
**Modified files:**
- .claude/CLAUDE.md
- README.md
- VERSIONS
- library.json
- library.properties
### Session: 2026-02-28 14:48
**Commits:**
```
261cc0d Improve readme ready for v0.4 release
39a89c8 Versions v0.4 ready for release
4944757 Fix to be non blocking without tasks
31765c7 Update notes
```
**Modified files:**
- .claude/CLAUDE.md
- README.md
- REVIEW.md

269
README.md
View File

@@ -2,16 +2,15 @@
ESP32 library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements.
**⚠️ INITIAL RELEASE - LIMITED TESTING DONE**
**⚠️ API CHANGE in v0.4 — not backwards compatible with v0.3.x**
This is an initial release (v0.3.1) and has been tested with MPPT on an ESP32-S3 and ESP32-C3.
Use with caution and please report any issues you encounter. Testing and feedback are greatly appreciated!
v0.4 is a major rework of the library internals: new callback API, reduced memory usage, non-blocking scanning. See [VERSIONS](VERSIONS) for full details. A stable **v1.0** release with a consistent, long-term API is coming soon.
---
Why another library? Most of the Victron BLE examples are built into other frameworks (e.g. ESPHome) and I want a library that can be used in all ESP32 systems, including ESPHome or other frameworks. With long term plan to try and move others to this library and improve code with many eyes.
Currently supportin ESP32 S and C series (tested on older ESP32, and ESP32-S3 and ESP32-C3). Other chipsets can be added with abstraction of Bluetooth code.
Currently supporting ESP32 S and C series (tested on older ESP32, ESP32-S3 and ESP32-C3). Other chipsets can be added with abstraction of Bluetooth code.
## Features
@@ -81,38 +80,34 @@ Use the VictronConnect app to get your device's encryption key:
VictronBLE victron;
// Callback for data updates
class MyCallback : public VictronDeviceCallback {
public:
void onSolarChargerData(const SolarChargerData& data) override {
Serial.printf("Solar: %.2fV, %.2fA, %dW\n",
data.batteryVoltage,
data.batteryCurrent,
data.panelPower);
// Callback — receives a VictronDevice*, switch on deviceType
void onVictronData(const VictronDevice* dev) {
if (dev->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
Serial.printf("Solar %s: %.2fV %.2fA %dW\n",
dev->name,
dev->solar.batteryVoltage,
dev->solar.batteryCurrent,
(int)dev->solar.panelPower);
}
}
};
MyCallback callback;
void setup() {
Serial.begin(115200);
// Initialize library
victron.begin(5); // 5 second scan duration
victron.setCallback(&callback);
victron.setCallback(onVictronData);
// Add your device (replace with your MAC and key)
victron.addDevice(
"My MPPT", // Name
"AA:BB:CC:DD:EE:FF", // MAC address
"0123456789abcdef0123456789abcdef", // Encryption key
DEVICE_TYPE_SOLAR_CHARGER // Device type
DEVICE_TYPE_SOLAR_CHARGER // Device type (optional, auto-detected)
);
}
void loop() {
victron.loop();
delay(100);
victron.loop(); // Non-blocking, returns immediately
}
```
@@ -125,130 +120,113 @@ void loop() {
```cpp
bool begin(uint32_t scanDuration = 5);
```
Initialize BLE and start scanning. Returns `true` on success.
Initialize BLE scanning. Returns `true` on success.
**Parameters:**
- `scanDuration`: BLE scan duration in seconds (default: 5)
- `scanDuration`: BLE scan window in seconds (default: 5)
#### Device Management
```cpp
bool addDevice(String name, String macAddress, String encryptionKey,
VictronDeviceType expectedType = DEVICE_TYPE_UNKNOWN);
bool addDevice(const char* name, const char* mac, const char* hexKey,
VictronDeviceType type = DEVICE_TYPE_UNKNOWN);
```
Add a device to monitor.
Add a device to monitor (max 8 devices).
**Parameters:**
- `name`: Friendly name for the device
- `macAddress`: Device MAC address (format: "AA:BB:CC:DD:EE:FF")
- `encryptionKey`: 32-character hex encryption key from VictronConnect
- `expectedType`: Device type (optional, for validation)
- `mac`: Device MAC address (format: `"AA:BB:CC:DD:EE:FF"` or `"aabbccddeeff"`)
- `hexKey`: 32-character hex encryption key from VictronConnect
- `type`: Device type (optional, auto-detected from BLE advertisement)
**Returns:** `true` on success
```cpp
void removeDevice(String macAddress);
```
Remove a device from monitoring.
```cpp
size_t getDeviceCount();
size_t getDeviceCount() const;
```
Get the number of configured devices.
#### Data Access
#### Callback
```cpp
bool getSolarChargerData(String macAddress, SolarChargerData& data);
bool getBatteryMonitorData(String macAddress, BatteryMonitorData& data);
bool getInverterData(String macAddress, InverterData& data);
bool getDCDCConverterData(String macAddress, DCDCConverterData& data);
void setCallback(VictronCallback cb);
```
Get latest data for a specific device. Returns `true` if data is valid.
Set a function pointer callback. Called when new data arrives from a device. The callback receives a `const VictronDevice*` — switch on `deviceType` to access the appropriate data union member.
```cpp
std::vector<String> getDevicesByType(VictronDeviceType type);
typedef void (*VictronCallback)(const VictronDevice* device);
```
Get MAC addresses of all devices of a specific type.
#### Callbacks
#### Configuration
```cpp
void setCallback(VictronDeviceCallback* callback);
void setMinInterval(uint32_t ms);
```
Set callback object to receive data updates automatically.
#### Utilities
Set minimum callback interval per device (default: 1000ms). Callbacks are also suppressed when the device nonce hasn't changed (data unchanged).
```cpp
void setDebug(bool enable);
```
Enable/disable debug output to Serial.
```cpp
String getLastError();
```
Get last error message.
#### Main Loop
```cpp
void loop();
```
Process BLE scanning and data updates. Call this in your main loop.
Call in your main loop. Non-blocking — returns immediately if a scan is already running. Scan restarts automatically when it completes.
### Data Structures
#### SolarChargerData
#### VictronDevice (main struct)
All device types share this struct. Access type-specific data via the union member matching `deviceType`.
```cpp
struct SolarChargerData {
String deviceName;
String macAddress;
struct VictronDevice {
char name[32];
char mac[13]; // 12 hex chars + null
VictronDeviceType deviceType;
int8_t rssi; // Signal strength (dBm)
uint32_t lastUpdate; // millis() of last update
bool dataValid; // Data validity flag
bool dataValid;
union {
VictronSolarData solar;
VictronBatteryData battery;
VictronInverterData inverter;
VictronDCDCData dcdc;
};
};
```
SolarChargerState chargeState; // Charging state
#### VictronSolarData
```cpp
struct VictronSolarData {
uint8_t chargeState; // SolarChargerState enum
uint8_t errorCode;
float batteryVoltage; // V
float batteryCurrent; // A
float panelVoltage; // V (calculated)
float panelPower; // W
uint16_t yieldToday; // Wh
float loadCurrent; // A (if load output present)
};
```
**Charge States:**
- `CHARGER_OFF` - Off
- `CHARGER_LOW_POWER` - Low power
- `CHARGER_FAULT` - Fault
- `CHARGER_BULK` - Bulk charging
- `CHARGER_ABSORPTION` - Absorption
- `CHARGER_FLOAT` - Float
- `CHARGER_STORAGE` - Storage mode
- `CHARGER_EQUALIZE` - Equalize
- `CHARGER_INVERTING` - Inverting (HUB-4)
- `CHARGER_POWER_SUPPLY` - Power supply mode
- `CHARGER_EXTERNAL_CONTROL` - External control
**Charge States** (`chargeState` values):
`CHARGER_OFF`, `CHARGER_LOW_POWER`, `CHARGER_FAULT`, `CHARGER_BULK`, `CHARGER_ABSORPTION`, `CHARGER_FLOAT`, `CHARGER_STORAGE`, `CHARGER_EQUALIZE`, `CHARGER_INVERTING`, `CHARGER_POWER_SUPPLY`, `CHARGER_EXTERNAL_CONTROL`
#### BatteryMonitorData
#### VictronBatteryData
```cpp
struct BatteryMonitorData {
String deviceName;
String macAddress;
int8_t rssi;
uint32_t lastUpdate;
bool dataValid;
struct VictronBatteryData {
float voltage; // V
float current; // A (+ charging, - discharging)
float temperature; // °C (if configured)
float auxVoltage; // V (starter battery/midpoint)
uint16_t remainingMinutes; // Time remaining
float consumedAh; // Ah consumed
float temperature; // C (0 if aux is voltage)
float auxVoltage; // V (0 if aux is temperature)
uint16_t remainingMinutes;
float consumedAh; // Ah
float soc; // State of charge %
// Alarms
bool alarmLowVoltage;
bool alarmHighVoltage;
bool alarmLowSOC;
@@ -257,39 +235,25 @@ struct BatteryMonitorData {
};
```
#### InverterData
#### VictronInverterData
```cpp
struct InverterData {
String deviceName;
String macAddress;
int8_t rssi;
uint32_t lastUpdate;
bool dataValid;
struct VictronInverterData {
float batteryVoltage; // V
float batteryCurrent; // A
float acPower; // W (+ inverting, - charging)
uint8_t state; // Inverter state
// Alarms
bool alarmHighVoltage;
uint8_t state;
bool alarmLowVoltage;
bool alarmHighVoltage;
bool alarmHighTemperature;
bool alarmOverload;
};
```
#### DCDCConverterData
#### VictronDCDCData
```cpp
struct DCDCConverterData {
String deviceName;
String macAddress;
int8_t rssi;
uint32_t lastUpdate;
bool dataValid;
struct VictronDCDCData {
float inputVoltage; // V
float outputVoltage; // V
float outputCurrent; // A
@@ -305,78 +269,52 @@ struct DCDCConverterData {
```cpp
void setup() {
victron.begin(5);
victron.setCallback(&callback);
victron.setCallback(onVictronData);
// Add multiple devices
victron.addDevice("MPPT 1", "AA:BB:CC:DD:EE:01", "key1...", DEVICE_TYPE_SOLAR_CHARGER);
victron.addDevice("MPPT 2", "AA:BB:CC:DD:EE:02", "key2...", DEVICE_TYPE_SOLAR_CHARGER);
victron.addDevice("SmartShunt", "AA:BB:CC:DD:EE:03", "key3...", DEVICE_TYPE_BATTERY_MONITOR);
victron.addDevice("Inverter", "AA:BB:CC:DD:EE:04", "key4...", DEVICE_TYPE_INVERTER);
// Add multiple devices (type is auto-detected from BLE advertisements)
victron.addDevice("MPPT 1", "AA:BB:CC:DD:EE:01", "key1...");
victron.addDevice("MPPT 2", "AA:BB:CC:DD:EE:02", "key2...");
victron.addDevice("SmartShunt", "AA:BB:CC:DD:EE:03", "key3...");
victron.addDevice("Inverter", "AA:BB:CC:DD:EE:04", "key4...");
}
```
### Manual Data Polling
### Handling Multiple Device Types
```cpp
void loop() {
victron.loop();
// Query specific device
SolarChargerData mpptData;
if (victron.getSolarChargerData("AA:BB:CC:DD:EE:FF", mpptData)) {
if (mpptData.dataValid) {
// Use data
float power = mpptData.panelPower;
void onVictronData(const VictronDevice* dev) {
switch (dev->deviceType) {
case DEVICE_TYPE_SOLAR_CHARGER:
Serial.printf("%s: %.2fV %dW\n", dev->name,
dev->solar.batteryVoltage, (int)dev->solar.panelPower);
break;
case DEVICE_TYPE_BATTERY_MONITOR:
Serial.printf("%s: %.2fV %.1f%%\n", dev->name,
dev->battery.voltage, dev->battery.soc);
break;
case DEVICE_TYPE_INVERTER:
Serial.printf("%s: %dW\n", dev->name, (int)dev->inverter.acPower);
break;
case DEVICE_TYPE_DCDC_CONVERTER:
Serial.printf("%s: %.2fV -> %.2fV\n", dev->name,
dev->dcdc.inputVoltage, dev->dcdc.outputVoltage);
break;
default:
break;
}
}
delay(1000);
}
```
### Find All Devices of a Type
### Callback Throttling
```cpp
void loop() {
victron.loop();
void setup() {
victron.begin(5);
victron.setCallback(onVictronData);
victron.setMinInterval(2000); // Callback at most every 2 seconds per device
// Get all solar chargers
std::vector<String> mppts = victron.getDevicesByType(DEVICE_TYPE_SOLAR_CHARGER);
for (const String& mac : mppts) {
SolarChargerData data;
if (victron.getSolarChargerData(mac, data)) {
Serial.println(data.deviceName + ": " + String(data.panelPower) + "W");
// ...
}
}
delay(5000);
}
```
### Callback Interface
Implement `VictronDeviceCallback` to receive automatic updates:
```cpp
class MyCallback : public VictronDeviceCallback {
public:
void onSolarChargerData(const SolarChargerData& data) override {
// Handle solar charger update
}
void onBatteryMonitorData(const BatteryMonitorData& data) override {
// Handle battery monitor update
}
void onInverterData(const InverterData& data) override {
// Handle inverter update
}
void onDCDCConverterData(const DCDCConverterData& data) override {
// Handle DC-DC converter update
}
};
```
## Troubleshooting
@@ -418,7 +356,10 @@ Based on official [Victron BLE documentation](https://www.victronenergy.com/live
See the `examples/` directory for:
- **MultiDevice**: Monitor multiple devices with callbacks
- More examples coming soon!
- **Logger**: Change-detection logging for Solar Charger data
- **Repeater**: Collect BLE data and re-transmit via ESPNow broadcast
- **Receiver**: Receive ESPNow packets from a Repeater and display data
- **FakeRepeater**: Generate test ESPNow packets without real Victron hardware
## Contributing

View File

@@ -1,6 +1,6 @@
# VictronBLE Code Review
## Part 1: Bug Fixes, Efficiency & Simplification
## Part 1: Bug Fixes, Efficiency & Simplification ✅ COMPLETE (v0.4.1)
### Bugs
@@ -117,10 +117,34 @@ void victron_loop();
~4 functions instead of ~15 methods.
All items implemented in v0.4.1. See [VERSIONS](VERSIONS) for full changelog.
---
## Part 2: Multi-Platform BLE Support
### Recommended Test Hardware
Two cheap BLE development boards for testing the platform abstraction:
**1. Seeed XIAO nRF52840 (~$10 USD)**
- Nordic nRF52840 SoC, Bluetooth 5.0, onboard antenna
- Arduino-compatible via Adafruit nRF52 board support package
- Ultra-small (21x17.5mm), USB-C, battery charging built in
- 1MB flash, 256KB RAM, 2MB QSPI flash
- Has mbedtls available via the nRF SDK
- https://www.seeedstudio.com/Seeed-XIAO-BLE-nRF52840-p-5201.html
**2. Raspberry Pi Pico W (~$6 USD)**
- RP2040 + Infineon CYW43439 (WiFi + Bluetooth 5.2 with BLE)
- Arduino-compatible via arduino-pico core (earlephilhower)
- BLE Central role supported (needed for passive scanning)
- Very widely available and cheap
- Different architecture (ARM Cortex-M0+) from ESP32 (Xtensa/RISC-V), good for testing portability
- https://www.raspberrypi.com/products/raspberry-pi-pico/
Both boards are under $15, Arduino-compatible, and have BLE Central support needed for passive scanning of Victron advertisements. They use different BLE stacks (nRF SoftDevice vs CYW43 BTstack) which will validate the transport abstraction layer.
### Current BLE Dependencies
All ESP32-specific BLE code is confined to:

106
UPGRADE_V0.4.md Normal file
View 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);
```

View File

@@ -1,5 +1,58 @@
# Version History
## 0.4.1 (2026-02-28)
Major rework of library internals. Breaking API change — not backwards compatible with 0.3.x.
### Callback API rewrite
- Replaced virtual callback class (`VictronDeviceCallback` with 4 override methods) with a
single function pointer (`VictronCallback`). Users now provide a plain function instead of
subclassing. The callback receives a `VictronDevice*` and switches on `deviceType` to access
the appropriate data via a tagged union.
### Non-blocking BLE scanning
- `loop()` is now non-blocking — returns immediately if a scan is already running.
Previously it blocked for the entire scan duration (default 5 seconds).
- Scan restarts automatically when it completes.
### Callback throttling
- Nonce-based deduplication: skips decrypt/parse/callback when the device's data hasn't
changed (detected via the nonce field in the BLE advertisement header).
- Configurable minimum interval (`setMinInterval()`, default 1000ms) limits callback
frequency even when data is changing rapidly.
- Encryption key byte check before AES decryption for early rejection of mismatched keys.
### Memory and code reduction
- Replaced `std::map<String, DeviceInfo*>` with a fixed array (max 8 devices, linear search).
Eliminates heap allocation for device storage.
- Replaced Arduino `String` with fixed `char[]` arrays throughout (MAC: 12 chars, name: 32 chars).
Eliminates heap fragmentation from dynamic string operations.
- Replaced inheritance hierarchy (`VictronDeviceData` base + 4 derived classes) with a flat
`VictronDevice` struct using a tagged union. No more `new`/`delete` for device data.
- Removed `std::map` and `std::vector` includes entirely.
- Source reduced from ~970 lines to ~510 lines (48% reduction).
- Flash savings: ~11-14 KB across examples.
### Bug fixes
- Fixed undefined behavior: derived objects were deleted through a base pointer without a
virtual destructor. Now uses flat structs, no polymorphic delete.
- Removed incorrect `panelVoltage` calculation (was dividing PV power by battery current,
which is wrong for MPPT chargers). The BLE protocol does not transmit PV voltage.
- Removed spurious `nullPad` byte from manufacturer data struct.
- Device type is now auto-detected from the BLE advertisement record type. The type
parameter in `addDevice()` is optional.
### Removed features (commented out in header for reference)
- `VictronDeviceConfig` struct — use `addDevice(name, mac, key, type)` directly
- Per-type getter methods (`getSolarChargerData()`, etc.) — use callback instead
- `removeDevice()`, `getDevicesByType()`, `getLastError()`
### Examples updated
- All examples updated for new callback API
- Removed `panelVoltage` from ESPNow packet structs (Repeater, FakeRepeater, Receiver)
- Removed unnecessary `delay(100)` from loop functions
- Added ESPNow Repeater and Receiver examples
## 0.3.1 (2026-02-11)
### Changes

View File

@@ -17,7 +17,6 @@ struct __attribute__((packed)) SolarChargerPacket {
uint8_t chargeState;
float batteryVoltage; // V
float batteryCurrent; // A
float panelVoltage; // V
float panelPower; // W
uint16_t yieldToday; // Wh
float loadCurrent; // A
@@ -74,7 +73,6 @@ void loop() {
pkt.chargeState = (sendCount % 4) + 3; // Cycle through Bulk(3), Absorption(4), Float(5), Storage(6)
pkt.batteryVoltage = 51.0f + (sendCount % 20) * 0.15f;
pkt.batteryCurrent = 2.0f + (sendCount % 10) * 0.5f;
pkt.panelVoltage = 65.0f + (sendCount % 15) * 0.8f;
pkt.panelPower = pkt.batteryCurrent * pkt.batteryVoltage;
pkt.yieldToday = 100 + sendCount * 10;
pkt.loadCurrent = 0;

View File

@@ -3,8 +3,7 @@
*
* Demonstrates change-detection logging for Solar Charger data.
* Only logs to serial when a value changes (ignoring RSSI), or once
* per minute if nothing has changed. This keeps serial output quiet
* and is useful for long-running monitoring / data logging.
* per minute if nothing has changed.
*
* Setup:
* 1. Get your device encryption keys from the VictronConnect app
@@ -16,13 +15,11 @@
VictronBLE victron;
// Tracks last-logged values per device for change detection
struct SolarChargerSnapshot {
bool valid = false;
SolarChargerState chargeState;
uint8_t chargeState;
float batteryVoltage;
float batteryCurrent;
float panelVoltage;
float panelPower;
uint16_t yieldToday;
float loadCurrent;
@@ -30,26 +27,26 @@ struct SolarChargerSnapshot {
uint32_t packetsSinceLastLog = 0;
};
// Store a snapshot per device (index by MAC string)
static const int MAX_DEVICES = 4;
static String deviceMACs[MAX_DEVICES];
static char deviceMACs[MAX_DEVICES][VICTRON_MAC_LEN];
static SolarChargerSnapshot snapshots[MAX_DEVICES];
static int deviceCount = 0;
static const unsigned long LOG_INTERVAL_MS = 60000; // 1 minute
static const unsigned long LOG_INTERVAL_MS = 60000;
static int findOrAddDevice(const String& mac) {
static int findOrAddDevice(const char* mac) {
for (int i = 0; i < deviceCount; i++) {
if (deviceMACs[i] == mac) return i;
if (strcmp(deviceMACs[i], mac) == 0) return i;
}
if (deviceCount < MAX_DEVICES) {
deviceMACs[deviceCount] = mac;
strncpy(deviceMACs[deviceCount], mac, VICTRON_MAC_LEN - 1);
deviceMACs[deviceCount][VICTRON_MAC_LEN - 1] = '\0';
return deviceCount++;
}
return -1;
}
static String chargeStateName(SolarChargerState state) {
static const char* chargeStateName(uint8_t state) {
switch (state) {
case CHARGER_OFF: return "Off";
case CHARGER_LOW_POWER: return "Low Power";
@@ -66,22 +63,23 @@ static String chargeStateName(SolarChargerState state) {
}
}
static void logData(const SolarChargerData& data, const char* reason, uint32_t packets) {
Serial.println("[" + data.deviceName + "] " + reason +
" pkts:" + String(packets) +
" | State:" + chargeStateName(data.chargeState) +
" Batt:" + String(data.batteryVoltage, 2) + "V" +
" " + String(data.batteryCurrent, 2) + "A" +
" PV:" + String(data.panelVoltage, 1) + "V" +
" " + String(data.panelPower, 0) + "W" +
" Yield:" + String(data.yieldToday) + "Wh" +
(data.loadCurrent > 0 ? " Load:" + String(data.loadCurrent, 2) + "A" : ""));
static void logData(const VictronDevice* dev, const VictronSolarData& s,
const char* reason, uint32_t packets) {
Serial.printf("[%s] %s pkts:%lu | State:%s Batt:%.2fV %.2fA PV:%.0fW Yield:%uWh",
dev->name, reason, packets,
chargeStateName(s.chargeState),
s.batteryVoltage, s.batteryCurrent,
s.panelPower, s.yieldToday);
if (s.loadCurrent > 0)
Serial.printf(" Load:%.2fA", s.loadCurrent);
Serial.println();
}
class LoggerCallback : public VictronDeviceCallback {
public:
void onSolarChargerData(const SolarChargerData& data) override {
int idx = findOrAddDevice(data.macAddress);
void onVictronData(const VictronDevice* dev) {
if (dev->deviceType != DEVICE_TYPE_SOLAR_CHARGER) return;
const auto& s = dev->solar;
int idx = findOrAddDevice(dev->mac);
if (idx < 0) return;
SolarChargerSnapshot& prev = snapshots[idx];
@@ -89,43 +87,34 @@ public:
prev.packetsSinceLastLog++;
if (!prev.valid) {
// First reading - always log
logData(data, "INIT", prev.packetsSinceLastLog);
logData(dev, s, "INIT", prev.packetsSinceLastLog);
} else {
// Check for changes (everything except RSSI)
bool changed = false;
if (prev.chargeState != data.chargeState) changed = true;
if (prev.batteryVoltage != data.batteryVoltage) changed = true;
if (prev.batteryCurrent != data.batteryCurrent) changed = true;
if (prev.panelVoltage != data.panelVoltage) changed = true;
if (prev.panelPower != data.panelPower) changed = true;
if (prev.yieldToday != data.yieldToday) changed = true;
if (prev.loadCurrent != data.loadCurrent) changed = true;
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(data, "CHG", prev.packetsSinceLastLog);
logData(dev, s, "CHG", prev.packetsSinceLastLog);
} else if (now - prev.lastLogTime >= LOG_INTERVAL_MS) {
logData(data, "HEARTBEAT", prev.packetsSinceLastLog);
logData(dev, s, "HEARTBEAT", prev.packetsSinceLastLog);
} else {
return; // Nothing to log
return;
}
}
// Update snapshot
prev.packetsSinceLastLog = 0;
prev.valid = true;
prev.chargeState = data.chargeState;
prev.batteryVoltage = data.batteryVoltage;
prev.batteryCurrent = data.batteryCurrent;
prev.panelVoltage = data.panelVoltage;
prev.panelPower = data.panelPower;
prev.yieldToday = data.yieldToday;
prev.loadCurrent = data.loadCurrent;
prev.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;
}
};
LoggerCallback callback;
void setup() {
Serial.begin(115200);
@@ -135,14 +124,12 @@ void setup() {
if (!victron.begin(5)) {
Serial.println("ERROR: Failed to initialize VictronBLE!");
Serial.println(victron.getLastError());
while (1) delay(1000);
}
victron.setDebug(false);
victron.setCallback(&callback);
victron.setCallback(onVictronData);
// Add your devices here
victron.addDevice(
"Rainbow48V",
"E4:05:42:34:14:F3",
@@ -157,11 +144,10 @@ void setup() {
DEVICE_TYPE_SOLAR_CHARGER
);
Serial.println("Configured " + String(victron.getDeviceCount()) + " devices");
Serial.printf("Configured %d devices\n", (int)victron.getDeviceCount());
Serial.println("Logging on change, or every 60s heartbeat\n");
}
void loop() {
victron.loop();
delay(100);
}

View File

@@ -1,136 +1,25 @@
/**
* VictronBLE Example
* VictronBLE Multi-Device Example
*
* This example demonstrates how to use the VictronBLE library to read data
* from multiple Victron devices simultaneously.
*
* Hardware Requirements:
* - ESP32 board
* - Victron devices with BLE (SmartSolar, SmartShunt, etc.)
* Demonstrates reading data from multiple Victron device types via BLE.
*
* Setup:
* 1. Get your device encryption keys from the VictronConnect app:
* - Open VictronConnect
* - Connect to your device
* - Go to Settings > Product Info
* - Enable "Instant readout via Bluetooth"
* - Click "Show" next to "Instant readout details"
* - Copy the encryption key (32 hex characters)
*
* 2. Update the device configurations below with your devices' MAC addresses
* and encryption keys
* 1. Get your device encryption keys from VictronConnect app
* (Settings > Product Info > Instant readout via Bluetooth > Show)
* 2. Update the device configurations below with your MAC and key
*/
#include <Arduino.h>
#include "VictronBLE.h"
// Create VictronBLE instance
VictronBLE victron;
// Device callback class - gets called when new data arrives
class MyVictronCallback : public VictronDeviceCallback {
public:
uint32_t solarChargerCount = 0;
uint32_t batteryMonitorCount = 0;
uint32_t inverterCount = 0;
uint32_t dcdcConverterCount = 0;
static uint32_t solarChargerCount = 0;
static uint32_t batteryMonitorCount = 0;
static uint32_t inverterCount = 0;
static uint32_t dcdcConverterCount = 0;
void onSolarChargerData(const SolarChargerData& data) override {
solarChargerCount++;
Serial.println("\n=== Solar Charger: " + data.deviceName + " (#" + String(solarChargerCount) + ") ===");
Serial.println("MAC: " + data.macAddress);
Serial.println("RSSI: " + String(data.rssi) + " dBm");
Serial.println("State: " + getChargeStateName(data.chargeState));
Serial.println("Battery: " + String(data.batteryVoltage, 2) + " V");
Serial.println("Current: " + String(data.batteryCurrent, 2) + " A");
Serial.println("Panel Voltage: " + String(data.panelVoltage, 1) + " V");
Serial.println("Panel Power: " + String(data.panelPower) + " W");
Serial.println("Yield Today: " + String(data.yieldToday) + " Wh");
if (data.loadCurrent > 0) {
Serial.println("Load Current: " + String(data.loadCurrent, 2) + " A");
}
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
}
void onBatteryMonitorData(const BatteryMonitorData& data) override {
batteryMonitorCount++;
Serial.println("\n=== Battery Monitor: " + data.deviceName + " (#" + String(batteryMonitorCount) + ") ===");
Serial.println("MAC: " + data.macAddress);
Serial.println("RSSI: " + String(data.rssi) + " dBm");
Serial.println("Voltage: " + String(data.voltage, 2) + " V");
Serial.println("Current: " + String(data.current, 2) + " A");
Serial.println("SOC: " + String(data.soc, 1) + " %");
Serial.println("Consumed: " + String(data.consumedAh, 2) + " Ah");
if (data.remainingMinutes < 65535) {
int hours = data.remainingMinutes / 60;
int mins = data.remainingMinutes % 60;
Serial.println("Time Remaining: " + String(hours) + "h " + String(mins) + "m");
}
if (data.temperature > 0) {
Serial.println("Temperature: " + String(data.temperature, 1) + " °C");
}
if (data.auxVoltage > 0) {
Serial.println("Aux Voltage: " + String(data.auxVoltage, 2) + " V");
}
// Print alarms
if (data.alarmLowVoltage || data.alarmHighVoltage || data.alarmLowSOC ||
data.alarmLowTemperature || data.alarmHighTemperature) {
Serial.print("ALARMS: ");
if (data.alarmLowVoltage) Serial.print("LOW-V ");
if (data.alarmHighVoltage) Serial.print("HIGH-V ");
if (data.alarmLowSOC) Serial.print("LOW-SOC ");
if (data.alarmLowTemperature) Serial.print("LOW-TEMP ");
if (data.alarmHighTemperature) Serial.print("HIGH-TEMP ");
Serial.println();
}
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
}
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) {
static const char* chargeStateName(uint8_t state) {
switch (state) {
case CHARGER_OFF: return "Off";
case CHARGER_LOW_POWER: return "Low Power";
@@ -146,9 +35,95 @@ private:
default: return "Unknown";
}
}
};
MyVictronCallback callback;
void onVictronData(const VictronDevice* dev) {
switch (dev->deviceType) {
case DEVICE_TYPE_SOLAR_CHARGER: {
const auto& s = dev->solar;
solarChargerCount++;
Serial.printf("\n=== Solar Charger: %s (#%lu) ===\n", dev->name, solarChargerCount);
Serial.printf("MAC: %s\n", dev->mac);
Serial.printf("RSSI: %d dBm\n", dev->rssi);
Serial.printf("State: %s\n", chargeStateName(s.chargeState));
Serial.printf("Battery: %.2f V\n", s.batteryVoltage);
Serial.printf("Current: %.2f A\n", s.batteryCurrent);
Serial.printf("Panel Power: %.0f W\n", s.panelPower);
Serial.printf("Yield Today: %u Wh\n", s.yieldToday);
if (s.loadCurrent > 0)
Serial.printf("Load Current: %.2f A\n", s.loadCurrent);
Serial.printf("Last Update: %lus ago\n", (millis() - dev->lastUpdate) / 1000);
break;
}
case DEVICE_TYPE_BATTERY_MONITOR: {
const auto& b = dev->battery;
batteryMonitorCount++;
Serial.printf("\n=== Battery Monitor: %s (#%lu) ===\n", dev->name, batteryMonitorCount);
Serial.printf("MAC: %s\n", dev->mac);
Serial.printf("RSSI: %d dBm\n", dev->rssi);
Serial.printf("Voltage: %.2f V\n", b.voltage);
Serial.printf("Current: %.2f A\n", b.current);
Serial.printf("SOC: %.1f %%\n", b.soc);
Serial.printf("Consumed: %.2f Ah\n", b.consumedAh);
if (b.remainingMinutes < 65535)
Serial.printf("Time Remaining: %dh %dm\n", b.remainingMinutes / 60, b.remainingMinutes % 60);
if (b.temperature > 0)
Serial.printf("Temperature: %.1f C\n", b.temperature);
if (b.auxVoltage > 0)
Serial.printf("Aux Voltage: %.2f V\n", b.auxVoltage);
if (b.alarmLowVoltage || b.alarmHighVoltage || b.alarmLowSOC ||
b.alarmLowTemperature || b.alarmHighTemperature) {
Serial.print("ALARMS:");
if (b.alarmLowVoltage) Serial.print(" LOW-V");
if (b.alarmHighVoltage) Serial.print(" HIGH-V");
if (b.alarmLowSOC) Serial.print(" LOW-SOC");
if (b.alarmLowTemperature) Serial.print(" LOW-TEMP");
if (b.alarmHighTemperature) Serial.print(" HIGH-TEMP");
Serial.println();
}
Serial.printf("Last Update: %lus ago\n", (millis() - dev->lastUpdate) / 1000);
break;
}
case DEVICE_TYPE_INVERTER: {
const auto& inv = dev->inverter;
inverterCount++;
Serial.printf("\n=== Inverter/Charger: %s (#%lu) ===\n", dev->name, inverterCount);
Serial.printf("MAC: %s\n", dev->mac);
Serial.printf("RSSI: %d dBm\n", dev->rssi);
Serial.printf("Battery: %.2f V\n", inv.batteryVoltage);
Serial.printf("Current: %.2f A\n", inv.batteryCurrent);
Serial.printf("AC Power: %.0f W\n", inv.acPower);
Serial.printf("State: %d\n", inv.state);
if (inv.alarmLowVoltage || inv.alarmHighVoltage ||
inv.alarmHighTemperature || inv.alarmOverload) {
Serial.print("ALARMS:");
if (inv.alarmLowVoltage) Serial.print(" LOW-V");
if (inv.alarmHighVoltage) Serial.print(" HIGH-V");
if (inv.alarmHighTemperature) Serial.print(" TEMP");
if (inv.alarmOverload) Serial.print(" OVERLOAD");
Serial.println();
}
Serial.printf("Last Update: %lus ago\n", (millis() - dev->lastUpdate) / 1000);
break;
}
case DEVICE_TYPE_DCDC_CONVERTER: {
const auto& dc = dev->dcdc;
dcdcConverterCount++;
Serial.printf("\n=== DC-DC Converter: %s (#%lu) ===\n", dev->name, dcdcConverterCount);
Serial.printf("MAC: %s\n", dev->mac);
Serial.printf("RSSI: %d dBm\n", dev->rssi);
Serial.printf("Input: %.2f V\n", dc.inputVoltage);
Serial.printf("Output: %.2f V\n", dc.outputVoltage);
Serial.printf("Current: %.2f A\n", dc.outputCurrent);
Serial.printf("State: %d\n", dc.chargeState);
if (dc.errorCode != 0)
Serial.printf("Error Code: %d\n", dc.errorCode);
Serial.printf("Last Update: %lus ago\n", (millis() - dev->lastUpdate) / 1000);
break;
}
default:
break;
}
}
void setup() {
Serial.begin(115200);
@@ -158,102 +133,43 @@ void setup() {
Serial.println("VictronBLE Multi-Device Example");
Serial.println("=================================\n");
// Initialize VictronBLE with 5 second scan duration
if (!victron.begin(5)) {
Serial.println("ERROR: Failed to initialize VictronBLE!");
Serial.println(victron.getLastError());
while (1) delay(1000);
}
// Enable debug output (optional)
victron.setDebug(false);
// Set callback for data updates
victron.setCallback(&callback);
// Add your devices here
// Replace with your actual MAC addresses and encryption keys
// CORRECT in Alternative
// Rainbow48V at MAC e4:05:42:34:14:f3
// Temporary - Scott Example
victron.addDevice(
"Rainbow48V", // Device name
"E4:05:42:34:14:F3", // MAC address
"0ec3adf7433dd61793ff2f3b8ad32ed8", // Encryption key (32 hex chars)
DEVICE_TYPE_SOLAR_CHARGER // Device type
);
victron.setCallback(onVictronData);
victron.addDevice(
"ScottTrailer", // Device name
"e64559783cfb",
"3fa658aded4f309b9bc17a2318cb1f56",
DEVICE_TYPE_SOLAR_CHARGER // Device type
);
// Example: Solar Charger #1
/*
victron.addDevice(
"MPPT 100/30", // Device name
"E7:48:D4:28:B7:9C", // MAC address
"0df4d0395b7d1a876c0c33ecb9e70dcd", // Encryption key (32 hex chars)
DEVICE_TYPE_SOLAR_CHARGER // Device type
);
*/
// Example: Solar Charger #2
/*
victron.addDevice(
"MPPT 75/15",
"AA:BB:CC:DD:EE:FF",
"1234567890abcdef1234567890abcdef",
"Rainbow48V",
"E4:05:42:34:14:F3",
"0ec3adf7433dd61793ff2f3b8ad32ed8",
DEVICE_TYPE_SOLAR_CHARGER
);
*/
// Example: Battery Monitor (SmartShunt)
/*
victron.addDevice(
"SmartShunt",
"11:22:33:44:55:66",
"fedcba0987654321fedcba0987654321",
DEVICE_TYPE_BATTERY_MONITOR
"ScottTrailer",
"e64559783cfb",
"3fa658aded4f309b9bc17a2318cb1f56",
DEVICE_TYPE_SOLAR_CHARGER
);
*/
// Example: Inverter/Charger
/*
victron.addDevice(
"MultiPlus",
"99:88:77:66:55:44",
"abcdefabcdefabcdefabcdefabcdefab",
DEVICE_TYPE_INVERTER
);
*/
Serial.println("Configured " + String(victron.getDeviceCount()) + " devices");
Serial.printf("Configured %d devices\n", (int)victron.getDeviceCount());
Serial.println("\nStarting BLE scan...\n");
}
static uint32_t loopCount = 0;
static uint32_t lastReport = 0;
void loop() {
// Process BLE scanning and data updates
victron.loop();
victron.loop(); // Non-blocking: returns immediately if scan is running
loopCount++;
// Optional: You can also manually query device data
// This is useful if you're not using callbacks
/*
SolarChargerData solarData;
if (victron.getSolarChargerData("E7:48:D4:28:B7:9C", solarData)) {
// Do something with solarData
uint32_t now = millis();
if (now - lastReport >= 10000) {
Serial.printf("Loop iterations in last 10s: %lu\n", loopCount);
loopCount = 0;
lastReport = now;
}
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);
}

View File

@@ -21,7 +21,6 @@ struct __attribute__((packed)) SolarChargerPacket {
uint8_t chargeState;
float batteryVoltage; // V
float batteryCurrent; // A
float panelVoltage; // V
float panelPower; // W
uint16_t yieldToday; // Wh
float loadCurrent; // A
@@ -82,13 +81,12 @@ void onDataRecv(const uint8_t* senderMac, const uint8_t* data, int len) {
memcpy(name, pkt->deviceName, 16);
name[16] = '\0';
Serial.printf("[RX #%lu] %s | State:%s Batt:%.2fV %.2fA PV:%.1fV %.0fW Yield:%uWh",
Serial.printf("[RX #%lu] %s | State:%s Batt:%.2fV %.2fA PV:%.0fW Yield:%uWh",
recvCount,
name,
chargeStateName(pkt->chargeState),
pkt->batteryVoltage,
pkt->batteryCurrent,
pkt->panelVoltage,
pkt->panelPower,
pkt->yieldToday);
@@ -202,7 +200,7 @@ void loop() {
M5.Lcd.printf("Batt: %.2fA\n", pkt.batteryCurrent);
// Row 3: PV
M5.Lcd.printf("PV: %.1fV %.0fW\n", pkt.panelVoltage, pkt.panelPower);
M5.Lcd.printf("PV: %.0fW\n", pkt.panelPower);
// Row 4: yield + load
M5.Lcd.printf("Yield: %uWh", pkt.yieldToday);

View File

@@ -2,7 +2,7 @@
* VictronBLE Repeater Example
*
* Collects Solar Charger data via BLE and transmits the latest
* readings over ESPNow broadcast every 30 seconds. Place this ESP32
* readings over ESPNow broadcast every 5 seconds. Place this ESP32
* near Victron devices and use a separate Receiver ESP32 at a distance.
*
* ESPNow range is typically much greater than BLE (~200m+ line of sight).
@@ -23,7 +23,6 @@ struct __attribute__((packed)) SolarChargerPacket {
uint8_t chargeState;
float batteryVoltage; // V
float batteryCurrent; // A
float panelVoltage; // V
float panelPower; // W
uint16_t yieldToday; // Wh
float loadCurrent; // A
@@ -31,10 +30,8 @@ struct __attribute__((packed)) SolarChargerPacket {
char deviceName[16]; // Null-terminated, truncated
};
// Broadcast address
static const uint8_t BROADCAST_ADDR[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
static const unsigned long SEND_INTERVAL_MS = 5000; // 30 seconds
static const unsigned long SEND_INTERVAL_MS = 5000;
static uint32_t sendCount = 0;
static uint32_t sendFailCount = 0;
@@ -49,7 +46,6 @@ static unsigned long lastSendTime = 0;
VictronBLE victron;
// Find cached slot by device name, or allocate a new one
static int findOrAddCached(const char* name) {
for (int i = 0; i < cachedCount; i++) {
if (strncmp(cachedPackets[i].deviceName, name, sizeof(cachedPackets[i].deviceName)) == 0)
@@ -59,34 +55,28 @@ static int findOrAddCached(const char* name) {
return -1;
}
class RepeaterCallback : public VictronDeviceCallback {
public:
void onSolarChargerData(const SolarChargerData& data) override {
void onVictronData(const VictronDevice* dev) {
if (dev->deviceType != DEVICE_TYPE_SOLAR_CHARGER) return;
blePacketCount++;
const auto& s = dev->solar;
// Build packet
SolarChargerPacket pkt;
pkt.chargeState = static_cast<uint8_t>(data.chargeState);
pkt.batteryVoltage = data.batteryVoltage;
pkt.batteryCurrent = data.batteryCurrent;
pkt.panelVoltage = data.panelVoltage;
pkt.panelPower = data.panelPower;
pkt.yieldToday = data.yieldToday;
pkt.loadCurrent = data.loadCurrent;
pkt.rssi = data.rssi;
pkt.chargeState = s.chargeState;
pkt.batteryVoltage = s.batteryVoltage;
pkt.batteryCurrent = s.batteryCurrent;
pkt.panelPower = s.panelPower;
pkt.yieldToday = s.yieldToday;
pkt.loadCurrent = s.loadCurrent;
pkt.rssi = dev->rssi;
memset(pkt.deviceName, 0, sizeof(pkt.deviceName));
strncpy(pkt.deviceName, data.deviceName.c_str(), sizeof(pkt.deviceName) - 1);
strncpy(pkt.deviceName, dev->name, sizeof(pkt.deviceName) - 1);
// Cache it
int idx = findOrAddCached(pkt.deviceName);
if (idx >= 0) {
cachedPackets[idx] = pkt;
cachedValid[idx] = true;
}
}
};
RepeaterCallback callback;
void setup() {
Serial.begin(115200);
@@ -98,7 +88,8 @@ void setup() {
WiFi.mode(WIFI_STA);
WiFi.disconnect();
Serial.println("MAC: " + WiFi.macAddress());
Serial.print("MAC: ");
Serial.println(WiFi.macAddress());
// Init ESPNow
if (esp_now_init() != ESP_OK) {
@@ -106,10 +97,9 @@ void setup() {
while (1) delay(1000);
}
// Add broadcast peer
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, BROADCAST_ADDR, 6);
peerInfo.channel = 0; // Use current channel
peerInfo.channel = 0;
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
@@ -122,14 +112,12 @@ void setup() {
// Init VictronBLE
if (!victron.begin(5)) {
Serial.println("ERROR: Failed to initialize VictronBLE!");
Serial.println(victron.getLastError());
while (1) delay(1000);
}
victron.setDebug(false);
victron.setCallback(&callback);
victron.setCallback(onVictronData);
// Add your devices here
victron.addDevice(
"Rainbow48V",
"E4:05:42:34:14:F3",
@@ -144,14 +132,13 @@ void setup() {
DEVICE_TYPE_SOLAR_CHARGER
);
Serial.println("Configured " + String(victron.getDeviceCount()) + " BLE devices");
Serial.println("Packet size: " + String(sizeof(SolarChargerPacket)) + " bytes\n");
Serial.printf("Configured %d BLE devices\n", (int)victron.getDeviceCount());
Serial.printf("Packet size: %d bytes\n\n", (int)sizeof(SolarChargerPacket));
}
void loop() {
victron.loop();
victron.loop(); // Blocks for scanDuration seconds
// Send cached packets every 30 seconds
unsigned long now = millis();
if (now - lastSendTime >= SEND_INTERVAL_MS) {
lastSendTime = now;
@@ -167,11 +154,10 @@ void loop() {
if (result == ESP_OK) {
sendCount++;
sent++;
Serial.printf("[ESPNow] Sent %s: %.2fV %.1fA PV:%.1fV %.0fW State:%d\n",
Serial.printf("[ESPNow] Sent %s: %.2fV %.1fA %.0fW State:%d\n",
cachedPackets[i].deviceName,
cachedPackets[i].batteryVoltage,
cachedPackets[i].batteryCurrent,
cachedPackets[i].panelVoltage,
cachedPackets[i].panelPower,
cachedPackets[i].chargeState);
} else {
@@ -185,5 +171,4 @@ void loop() {
blePacketCount, sendCount, sendFailCount, cachedCount);
}
delay(100);
}

View File

@@ -1,6 +1,6 @@
{
"name": "victronble",
"version": "0.3.1",
"version": "0.4.1",
"description": "ESP32 library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements. Supports SmartSolar MPPT, SmartShunt, BMV, MultiPlus, Orion and other Victron devices.",
"keywords": "victron, ble, bluetooth, solar, mppt, battery, smartshunt, smartsolar, bmv, inverter, multiplus, esp32, iot, energy, monitoring",
"repository": {
@@ -36,6 +36,16 @@
"name": "Repeater",
"base": "examples/Repeater",
"files": ["src/main.cpp"]
},
{
"name": "Receiver",
"base": "examples/Receiver",
"files": ["src/main.cpp"]
},
{
"name": "FakeRepeater",
"base": "examples/FakeRepeater",
"files": ["src/main.cpp"]
}
],
"export": {

View File

@@ -1,5 +1,5 @@
name=VictronBLE
version=0.3.1
version=0.4.1
author=Scott Penrose
maintainer=Scott Penrose <scottp@dd.com.au>
sentence=ESP32 library for reading Victron Energy device data via BLE for any ESP32

View File

@@ -7,634 +7,361 @@
*/
#include "VictronBLE.h"
#include <string.h>
// Constructor
VictronBLE::VictronBLE()
: pBLEScan(nullptr), scanCallback(nullptr), callback(nullptr),
debugEnabled(false), scanDuration(5), initialized(false) {
: deviceCount(0), pBLEScan(nullptr), scanCallbackObj(nullptr),
callback(nullptr), debugEnabled(false), scanDuration(5),
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) {
if (initialized) {
if (debugEnabled) debugPrint("VictronBLE already initialized");
return true;
}
if (initialized) return true;
this->scanDuration = scanDuration;
if (debugEnabled) debugPrint("Initializing VictronBLE...");
BLEDevice::init("VictronBLE");
pBLEScan = BLEDevice::getScan();
if (!pBLEScan) return false;
if (!pBLEScan) {
lastError = "Failed to create BLE scanner";
if (debugEnabled) debugPrint(lastError);
return false;
}
scanCallback = new VictronBLEAdvertisedDeviceCallbacks(this);
pBLEScan->setAdvertisedDeviceCallbacks(scanCallback, true);
pBLEScan->setActiveScan(false); // Passive scan - lower power
scanCallbackObj = new VictronBLEAdvertisedDeviceCallbacks(this);
pBLEScan->setAdvertisedDeviceCallbacks(scanCallbackObj, true);
pBLEScan->setActiveScan(false);
pBLEScan->setInterval(100);
pBLEScan->setWindow(99);
initialized = true;
if (debugEnabled) debugPrint("VictronBLE initialized successfully");
if (debugEnabled) Serial.println("[VictronBLE] Initialized");
return true;
}
// Add a device to monitor
bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
if (config.macAddress.length() == 0) {
lastError = "MAC address cannot be empty";
if (debugEnabled) debugPrint(lastError);
return false;
}
bool VictronBLE::addDevice(const char* name, const char* mac, const char* hexKey,
VictronDeviceType type) {
if (deviceCount >= VICTRON_MAX_DEVICES) return false;
if (!hexKey || strlen(hexKey) != 32) return false;
if (!mac || strlen(mac) == 0) return false;
if (config.encryptionKey.length() != 32) {
lastError = "Encryption key must be 32 hex characters";
if (debugEnabled) debugPrint(lastError);
return false;
}
char normalizedMAC[VICTRON_MAC_LEN];
normalizeMAC(mac, normalizedMAC);
String normalizedMAC = normalizeMAC(config.macAddress);
// Check for duplicate
if (findDevice(normalizedMAC)) return false;
// Check if device already exists
if (devices.find(normalizedMAC) != devices.end()) {
if (debugEnabled) debugPrint("Device " + normalizedMAC + " already exists, updating config");
delete devices[normalizedMAC];
}
DeviceEntry* entry = &devices[deviceCount];
memset(entry, 0, sizeof(DeviceEntry));
entry->active = true;
DeviceInfo* info = new DeviceInfo();
info->config = config;
info->config.macAddress = normalizedMAC;
strncpy(entry->device.name, name ? name : "", VICTRON_NAME_LEN - 1);
entry->device.name[VICTRON_NAME_LEN - 1] = '\0';
memcpy(entry->device.mac, normalizedMAC, VICTRON_MAC_LEN);
entry->device.deviceType = type;
entry->device.rssi = -100;
// Convert encryption key from hex string to bytes
if (!hexStringToBytes(config.encryptionKey, info->encryptionKeyBytes, 16)) {
lastError = "Invalid encryption key format";
if (debugEnabled) debugPrint(lastError);
delete info;
return false;
}
if (!hexToBytes(hexKey, entry->key, 16)) return false;
// Create appropriate data structure based on device type
info->data = createDeviceData(config.expectedType);
if (info->data) {
info->data->macAddress = normalizedMAC;
info->data->deviceName = config.name;
}
devices[normalizedMAC] = info;
if (debugEnabled) {
debugPrint("Added device: " + config.name + " (MAC: " + normalizedMAC + ")");
debugPrint(" Original MAC input: " + config.macAddress);
debugPrint(" Stored normalized: " + normalizedMAC);
}
deviceCount++;
if (debugEnabled) Serial.printf("[VictronBLE] Added: %s (%s)\n", name, normalizedMAC);
return true;
}
bool VictronBLE::addDevice(const String& name, const String& macAddress, const String& encryptionKey,
VictronDeviceType expectedType) {
VictronDeviceConfig config(name, macAddress, encryptionKey, expectedType);
return addDevice(config);
// Scan complete callback — sets flag so loop() restarts
static bool s_scanning = false;
static void onScanDone(BLEScanResults results) {
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() {
if (!initialized) {
return;
}
// Start a scan
BLEScanResults scanResults = pBLEScan->start(scanDuration, false);
if (!initialized) return;
if (!s_scanning) {
pBLEScan->clearResults();
s_scanning = pBLEScan->start(scanDuration, onScanDone, false);
}
}
// BLE callback implementation
// BLE scan callback
void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertisedDevice) {
if (victronBLE) {
victronBLE->processDevice(advertisedDevice);
}
if (victronBLE) victronBLE->processDevice(advertisedDevice);
}
// Process advertised device
void VictronBLE::processDevice(BLEAdvertisedDevice& advertisedDevice) {
// Get MAC address from the advertised device
String mac = macAddressToString(advertisedDevice.getAddress());
String normalizedMAC = normalizeMAC(mac);
if (!advertisedDevice.haveManufacturerData()) return;
if (debugEnabled) {
debugPrint("Raw MAC: " + mac + " -> Normalized: " + normalizedMAC);
}
std::string raw = advertisedDevice.getManufacturerData();
if (raw.length() < 10) return;
// Parse manufacturer data into local struct
// Quick vendor ID check before any other work
uint16_t vendorID = (uint8_t)raw[0] | ((uint8_t)raw[1] << 8);
if (vendorID != VICTRON_MANUFACTURER_ID) return;
// Parse manufacturer data
victronManufacturerData mfgData;
memset(&mfgData, 0, sizeof(mfgData));
if (advertisedDevice.haveManufacturerData()) {
std::string rawMfgData = advertisedDevice.getManufacturerData();
if (debugEnabled) debugPrint("Getting manufacturer data: Size=" + String(rawMfgData.length()));
rawMfgData.copy(reinterpret_cast<char*>(&mfgData),
(rawMfgData.length() > sizeof(mfgData) ? sizeof(mfgData) : rawMfgData.length()));
}
size_t copyLen = raw.length() > sizeof(mfgData) ? sizeof(mfgData) : raw.length();
raw.copy(reinterpret_cast<char*>(&mfgData), copyLen);
// Debug: Log all discovered BLE devices
if (debugEnabled) {
String debugMsg = "BLE Device: " + mac;
debugMsg += ", RSSI: " + String(advertisedDevice.getRSSI()) + " dBm";
if (advertisedDevice.haveName())
debugMsg += ", Name: " + String(advertisedDevice.getName().c_str());
// Normalize MAC and find device
char normalizedMAC[VICTRON_MAC_LEN];
normalizeMAC(advertisedDevice.getAddress().toString().c_str(), normalizedMAC);
debugMsg += ", Mfg ID: 0x" + String(mfgData.vendorID, HEX);
if (mfgData.vendorID == VICTRON_MANUFACTURER_ID) {
debugMsg += " (Victron)";
}
debugPrint(debugMsg);
}
// Check if this is one of our configured devices
auto it = devices.find(normalizedMAC);
if (it == devices.end()) {
if (debugEnabled && mfgData.vendorID == VICTRON_MANUFACTURER_ID) {
debugPrint("Found unmonitored Victron Device: " + normalizedMAC);
}
return; // Not a device we're monitoring
}
DeviceInfo* deviceInfo = it->second;
// Check if it's Victron (manufacturer ID 0x02E1)
if (mfgData.vendorID != VICTRON_MANUFACTURER_ID) {
if (debugEnabled) debugPrint("Skipping non VICTRON");
DeviceEntry* entry = findDevice(normalizedMAC);
if (!entry) {
if (debugEnabled) Serial.printf("[VictronBLE] Unmonitored Victron: %s\n", normalizedMAC);
return;
}
if (debugEnabled) debugPrint("Processing data from: " + deviceInfo->config.name);
// Parse the advertisement
if (parseAdvertisement(deviceInfo, mfgData)) {
// Update RSSI
if (deviceInfo->data) {
deviceInfo->data->rssi = advertisedDevice.getRSSI();
deviceInfo->data->lastUpdate = millis();
// 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;
}
// Skip if minimum interval hasn't elapsed
uint32_t now = millis();
if (entry->device.dataValid && (now - entry->device.lastUpdate) < minIntervalMs) {
return;
}
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(DeviceInfo* deviceInfo, const victronManufacturerData& mfgData) {
bool VictronBLE::parseAdvertisement(DeviceEntry* entry, const victronManufacturerData& mfg) {
if (debugEnabled) {
debugPrint("Vendor ID: 0x" + String(mfgData.vendorID, HEX));
debugPrint("Beacon Type: 0x" + String(mfgData.beaconType, HEX));
debugPrint("Record Type: 0x" + String(mfgData.victronRecordType, HEX));
debugPrint("Nonce: 0x" + String(mfgData.nonceDataCounter, HEX));
Serial.printf("[VictronBLE] Beacon:0x%02X Record:0x%02X Nonce:0x%04X\n",
mfg.beaconType, mfg.victronRecordType, mfg.nonceDataCounter);
}
// Build IV (initialization vector) from nonce
// IV is 16 bytes: nonce (2 bytes little-endian) + zeros (14 bytes)
// Quick key check before expensive decryption
if (mfg.encryptKeyMatch != entry->key[0]) {
if (debugEnabled) Serial.println("[VictronBLE] Key byte mismatch");
return false;
}
// Build IV from nonce (2 bytes little-endian + 14 zero bytes)
uint8_t iv[16] = {0};
iv[0] = mfgData.nonceDataCounter & 0xFF; // Low byte
iv[1] = (mfgData.nonceDataCounter >> 8) & 0xFF; // High byte
// Remaining bytes stay zero
iv[0] = mfg.nonceDataCounter & 0xFF;
iv[1] = (mfg.nonceDataCounter >> 8) & 0xFF;
// 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);
// 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 device type
bool parseOk = false;
switch (mfgData.victronRecordType) {
// Parse based on record type (auto-detects device type)
bool ok = false;
switch (mfg.victronRecordType) {
case DEVICE_TYPE_SOLAR_CHARGER:
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
parseOk = parseSolarCharger(decrypted, encryptedLen,
*static_cast<SolarChargerData*>(deviceInfo->data));
}
entry->device.deviceType = DEVICE_TYPE_SOLAR_CHARGER;
ok = parseSolarCharger(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.solar);
break;
case DEVICE_TYPE_BATTERY_MONITOR:
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) {
parseOk = parseBatteryMonitor(decrypted, encryptedLen,
*static_cast<BatteryMonitorData*>(deviceInfo->data));
}
entry->device.deviceType = DEVICE_TYPE_BATTERY_MONITOR;
ok = parseBatteryMonitor(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.battery);
break;
case DEVICE_TYPE_INVERTER:
case DEVICE_TYPE_INVERTER_RS:
case DEVICE_TYPE_MULTI_RS:
case DEVICE_TYPE_VE_BUS:
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_INVERTER) {
parseOk = parseInverter(decrypted, encryptedLen,
*static_cast<InverterData*>(deviceInfo->data));
}
entry->device.deviceType = DEVICE_TYPE_INVERTER;
ok = parseInverter(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.inverter);
break;
case DEVICE_TYPE_DCDC_CONVERTER:
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) {
parseOk = parseDCDCConverter(decrypted, encryptedLen,
*static_cast<DCDCConverterData*>(deviceInfo->data));
}
entry->device.deviceType = DEVICE_TYPE_DCDC_CONVERTER;
ok = parseDCDCConverter(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.dcdc);
break;
default:
if (debugEnabled) debugPrint("Unknown device type: 0x" + String(mfgData.victronRecordType, HEX));
if (debugEnabled) Serial.printf("[VictronBLE] Unknown type: 0x%02X\n", mfg.victronRecordType);
return false;
}
if (parseOk && deviceInfo->data) {
deviceInfo->data->dataValid = true;
// Call appropriate callback
if (callback) {
switch (mfgData.victronRecordType) {
case DEVICE_TYPE_SOLAR_CHARGER:
callback->onSolarChargerData(*static_cast<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;
}
}
if (ok) {
entry->device.dataValid = true;
if (callback) callback(&entry->device);
}
return parseOk;
return ok;
}
// Decrypt advertisement using AES-128-CTR
bool VictronBLE::decryptAdvertisement(const uint8_t* encrypted, size_t encLen,
bool VictronBLE::decryptData(const uint8_t* encrypted, size_t len,
const uint8_t* key, const uint8_t* iv,
uint8_t* decrypted) {
mbedtls_aes_context aes;
mbedtls_aes_init(&aes);
// Set encryption key
int ret = mbedtls_aes_setkey_enc(&aes, key, 128);
if (ret != 0) {
if (mbedtls_aes_setkey_enc(&aes, key, 128) != 0) {
mbedtls_aes_free(&aes);
return false;
}
// AES-CTR decryption
size_t nc_off = 0;
uint8_t nonce_counter[16];
uint8_t stream_block[16];
memcpy(nonce_counter, iv, 16);
memset(stream_block, 0, 16);
ret = mbedtls_aes_crypt_ctr(&aes, encLen, &nc_off, nonce_counter,
int ret = mbedtls_aes_crypt_ctr(&aes, len, &nc_off, nonce_counter,
stream_block, encrypted, decrypted);
mbedtls_aes_free(&aes);
return (ret == 0);
}
// Parse Solar Charger data
bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarChargerData& result) {
if (len < sizeof(victronSolarChargerPayload)) {
if (debugEnabled) debugPrint("Solar charger data too short: " + String(len) + " bytes");
return false;
}
bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, VictronSolarData& result) {
if (len < sizeof(victronSolarChargerPayload)) return false;
const auto* p = reinterpret_cast<const victronSolarChargerPayload*>(data);
const auto* payload = reinterpret_cast<const victronSolarChargerPayload*>(data);
// Parse charge state
result.chargeState = static_cast<SolarChargerState>(payload->deviceState);
// Parse battery voltage (10 mV units -> volts)
result.batteryVoltage = payload->batteryVoltage * 0.01f;
// Parse battery current (10 mA units, signed -> amps)
result.batteryCurrent = payload->batteryCurrent * 0.01f;
// Parse yield today (10 Wh units -> Wh)
result.yieldToday = payload->yieldToday * 10;
// Parse PV power (1 W units)
result.panelPower = payload->inputPower;
// Parse load current (10 mA units -> amps, 0xFFFF = no load)
if (payload->loadCurrent != 0xFFFF) {
result.loadCurrent = payload->loadCurrent * 0.01f;
} else {
result.loadCurrent = 0;
}
// Calculate PV voltage from power and current (if current > 0)
if (result.batteryCurrent > 0.1f) {
result.panelVoltage = result.panelPower / result.batteryCurrent;
} else {
result.panelVoltage = 0;
}
result.chargeState = p->deviceState;
result.errorCode = p->errorCode;
result.batteryVoltage = p->batteryVoltage * 0.01f;
result.batteryCurrent = p->batteryCurrent * 0.01f;
result.yieldToday = p->yieldToday * 10;
result.panelPower = p->inputPower;
result.loadCurrent = (p->loadCurrent != 0xFFFF) ? p->loadCurrent * 0.01f : 0;
if (debugEnabled) {
debugPrint("Solar Charger: " + String(result.batteryVoltage, 2) + "V, " +
String(result.batteryCurrent, 2) + "A, " +
String(result.panelPower) + "W, State: " + String(result.chargeState));
Serial.printf("[VictronBLE] Solar: %.2fV %.2fA %dW State:%d\n",
result.batteryVoltage, result.batteryCurrent,
(int)result.panelPower, result.chargeState);
}
return true;
}
// Parse Battery Monitor data
bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result) {
if (len < sizeof(victronBatteryMonitorPayload)) {
if (debugEnabled) debugPrint("Battery monitor data too short: " + String(len) + " bytes");
return false;
}
bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, VictronBatteryData& result) {
if (len < sizeof(victronBatteryMonitorPayload)) return false;
const auto* p = reinterpret_cast<const victronBatteryMonitorPayload*>(data);
const auto* payload = reinterpret_cast<const victronBatteryMonitorPayload*>(data);
result.remainingMinutes = p->remainingMins;
result.voltage = p->batteryVoltage * 0.01f;
// Parse remaining time (1 minute units)
result.remainingMinutes = payload->remainingMins;
// Alarm bits
result.alarmLowVoltage = (p->alarms & 0x01) != 0;
result.alarmHighVoltage = (p->alarms & 0x02) != 0;
result.alarmLowSOC = (p->alarms & 0x04) != 0;
result.alarmLowTemperature = (p->alarms & 0x10) != 0;
result.alarmHighTemperature = (p->alarms & 0x20) != 0;
// Parse battery voltage (10 mV units -> volts)
result.voltage = payload->batteryVoltage * 0.01f;
// Parse alarm bits
result.alarmLowVoltage = (payload->alarms & 0x01) != 0;
result.alarmHighVoltage = (payload->alarms & 0x02) != 0;
result.alarmLowSOC = (payload->alarms & 0x04) != 0;
result.alarmLowTemperature = (payload->alarms & 0x10) != 0;
result.alarmHighTemperature = (payload->alarms & 0x20) != 0;
// Parse aux data: voltage (10 mV units) or temperature (0.01K units)
if (payload->auxData < 3000) { // If < 30V, it's voltage
result.auxVoltage = payload->auxData * 0.01f;
// Aux data: voltage or temperature (heuristic: < 30V = voltage)
// NOTE: Victron protocol uses a flag bit for this, but it's not exposed
// in the BLE advertisement. This heuristic may misclassify edge cases.
if (p->auxData < 3000) {
result.auxVoltage = p->auxData * 0.01f;
result.temperature = 0;
} else { // Otherwise temperature in 0.01 Kelvin
result.temperature = (payload->auxData * 0.01f) - 273.15f;
} else {
result.temperature = (p->auxData * 0.01f) - 273.15f;
result.auxVoltage = 0;
}
// Parse battery current (22-bit signed, 1 mA units)
int32_t current = payload->currentLow |
(payload->currentMid << 8) |
((payload->currentHigh_consumedLow & 0x3F) << 16);
// Sign extend from 22 bits to 32 bits
if (current & 0x200000) {
current |= 0xFFC00000;
}
result.current = current * 0.001f; // Convert mA to A
// Battery current (22-bit signed, 1 mA units)
int32_t current = p->currentLow |
(p->currentMid << 8) |
((p->currentHigh_consumedLow & 0x3F) << 16);
if (current & 0x200000) current |= 0xFFC00000; // Sign extend
result.current = current * 0.001f;
// Parse consumed Ah (18-bit signed, 10 mAh units)
int32_t consumedAh = ((payload->currentHigh_consumedLow & 0xC0) >> 6) |
(payload->consumedMid << 2) |
(payload->consumedHigh << 10);
// Sign extend from 18 bits to 32 bits
if (consumedAh & 0x20000) {
consumedAh |= 0xFFFC0000;
}
result.consumedAh = consumedAh * 0.01f; // Convert 10mAh to Ah
// Consumed Ah (18-bit signed, 10 mAh units)
int32_t consumedAh = ((p->currentHigh_consumedLow & 0xC0) >> 6) |
(p->consumedMid << 2) |
(p->consumedHigh << 10);
if (consumedAh & 0x20000) consumedAh |= 0xFFFC0000; // Sign extend
result.consumedAh = consumedAh * 0.01f;
// Parse SOC (10-bit value, 10 = 1.0%)
result.soc = (payload->soc & 0x3FF) * 0.1f;
// SOC (10-bit, 0.1% units)
result.soc = (p->soc & 0x3FF) * 0.1f;
if (debugEnabled) {
debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " +
String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%");
Serial.printf("[VictronBLE] Battery: %.2fV %.2fA SOC:%.1f%%\n",
result.voltage, result.current, result.soc);
}
return true;
}
// Parse Inverter data
bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& result) {
if (len < sizeof(victronInverterPayload)) {
if (debugEnabled) debugPrint("Inverter data too short: " + String(len) + " bytes");
return false;
}
bool VictronBLE::parseInverter(const uint8_t* data, size_t len, VictronInverterData& result) {
if (len < sizeof(victronInverterPayload)) return false;
const auto* p = reinterpret_cast<const victronInverterPayload*>(data);
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
result.state = payload->deviceState;
// Parse battery voltage (10 mV units -> volts)
result.batteryVoltage = payload->batteryVoltage * 0.01f;
// Parse battery current (10 mA units, signed -> amps)
result.batteryCurrent = payload->batteryCurrent * 0.01f;
// Parse AC Power (signed 24-bit, 1 W units)
int32_t acPower = payload->acPowerLow |
(payload->acPowerMid << 8) |
(payload->acPowerHigh << 16);
// Sign extend from 24 bits to 32 bits
if (acPower & 0x800000) {
acPower |= 0xFF000000;
}
// AC Power (signed 24-bit)
int32_t acPower = p->acPowerLow | (p->acPowerMid << 8) | (p->acPowerHigh << 16);
if (acPower & 0x800000) acPower |= 0xFF000000; // Sign extend
result.acPower = acPower;
// Parse alarm bits
result.alarmLowVoltage = (payload->alarms & 0x01) != 0;
result.alarmHighVoltage = (payload->alarms & 0x02) != 0;
result.alarmHighTemperature = (payload->alarms & 0x04) != 0;
result.alarmOverload = (payload->alarms & 0x08) != 0;
// Alarm bits
result.alarmLowVoltage = (p->alarms & 0x01) != 0;
result.alarmHighVoltage = (p->alarms & 0x02) != 0;
result.alarmHighTemperature = (p->alarms & 0x04) != 0;
result.alarmOverload = (p->alarms & 0x08) != 0;
if (debugEnabled) {
debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " +
String(result.acPower) + "W, State: " + String(result.state));
Serial.printf("[VictronBLE] Inverter: %.2fV %dW State:%d\n",
result.batteryVoltage, (int)result.acPower, result.state);
}
return true;
}
// Parse DC-DC Converter data
bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result) {
if (len < sizeof(victronDCDCConverterPayload)) {
if (debugEnabled) debugPrint("DC-DC converter data too short: " + String(len) + " bytes");
return false;
}
bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, VictronDCDCData& result) {
if (len < sizeof(victronDCDCConverterPayload)) return false;
const auto* p = reinterpret_cast<const victronDCDCConverterPayload*>(data);
const auto* payload = reinterpret_cast<const victronDCDCConverterPayload*>(data);
// Parse charge state
result.chargeState = payload->chargeState;
// Parse error code
result.errorCode = payload->errorCode;
// Parse input voltage (10 mV units -> volts)
result.inputVoltage = payload->inputVoltage * 0.01f;
// Parse output voltage (10 mV units -> volts)
result.outputVoltage = payload->outputVoltage * 0.01f;
// Parse output current (10 mA units -> amps)
result.outputCurrent = payload->outputCurrent * 0.01f;
result.chargeState = p->chargeState;
result.errorCode = p->errorCode;
result.inputVoltage = p->inputVoltage * 0.01f;
result.outputVoltage = p->outputVoltage * 0.01f;
result.outputCurrent = p->outputCurrent * 0.01f;
if (debugEnabled) {
debugPrint("DC-DC Converter: In=" + String(result.inputVoltage, 2) + "V, Out=" +
String(result.outputVoltage, 2) + "V, " + String(result.outputCurrent, 2) + "A");
Serial.printf("[VictronBLE] DC-DC: In=%.2fV Out=%.2fV %.2fA\n",
result.inputVoltage, result.outputVoltage, result.outputCurrent);
}
return true;
}
// Get data methods
bool VictronBLE::getSolarChargerData(const String& macAddress, SolarChargerData& data) {
String normalizedMAC = normalizeMAC(macAddress);
auto it = devices.find(normalizedMAC);
if (it != devices.end() && it->second->data &&
it->second->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
data = *static_cast<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;
}
// --- Helpers ---
bool VictronBLE::hexToBytes(const char* hex, uint8_t* out, size_t len) {
if (strlen(hex) != len * 2) return false;
for (size_t i = 0; i < len; i++) {
String byteStr = hex.substring(i * 2, i * 2 + 2);
char* endPtr;
bytes[i] = strtoul(byteStr.c_str(), &endPtr, 16);
if (*endPtr != '\0') {
return false;
uint8_t hi = hex[i * 2], lo = hex[i * 2 + 1];
if (hi >= '0' && hi <= '9') hi -= '0';
else if (hi >= 'a' && hi <= 'f') hi = hi - 'a' + 10;
else if (hi >= 'A' && hi <= 'F') hi = hi - 'A' + 10;
else return false;
if (lo >= '0' && lo <= '9') lo -= '0';
else if (lo >= 'a' && lo <= 'f') lo = lo - 'a' + 10;
else if (lo >= 'A' && lo <= 'F') lo = lo - 'A' + 10;
else return false;
out[i] = (hi << 4) | lo;
}
}
return true;
}
// Helper: MAC address to string
String VictronBLE::macAddressToString(BLEAddress address) {
return String(address.toString().c_str());
void VictronBLE::normalizeMAC(const char* input, char* output) {
int j = 0;
for (int i = 0; input[i] && j < VICTRON_MAC_LEN - 1; i++) {
char c = input[i];
if (c == ':' || c == '-') continue;
output[j++] = (c >= 'A' && c <= 'F') ? (c + 32) : c;
}
output[j] = '\0';
}
// Helper: Normalize MAC address format
String VictronBLE::normalizeMAC(const String& mac) {
String normalized = mac;
normalized.toLowerCase();
normalized.replace("-", "");
normalized.replace(":", "");
return normalized;
VictronBLE::DeviceEntry* VictronBLE::findDevice(const char* normalizedMAC) {
for (size_t i = 0; i < deviceCount; i++) {
if (devices[i].active && strcmp(devices[i].device.mac, normalizedMAC) == 0) {
return &devices[i];
}
// Debug helper
void VictronBLE::debugPrint(const String& message) {
if (debugEnabled)
Serial.println("[VictronBLE] " + message);
}
return nullptr;
}

View File

@@ -15,14 +15,16 @@
#include <BLEDevice.h>
#include <BLEAdvertisedDevice.h>
#include <BLEScan.h>
#include <map>
#include <vector>
#include "mbedtls/aes.h"
// Victron manufacturer ID
// --- Constants ---
static constexpr uint16_t VICTRON_MANUFACTURER_ID = 0x02E1;
static constexpr int VICTRON_MAX_DEVICES = 8;
static constexpr int VICTRON_MAC_LEN = 13; // 12 hex chars + null
static constexpr int VICTRON_NAME_LEN = 32;
static constexpr int VICTRON_ENCRYPTED_LEN = 21;
// Device type IDs from Victron protocol
// --- Device type IDs from Victron protocol ---
enum VictronDeviceType {
DEVICE_TYPE_UNKNOWN = 0x00,
DEVICE_TYPE_SOLAR_CHARGER = 0x01,
@@ -38,7 +40,7 @@ enum VictronDeviceType {
DEVICE_TYPE_DC_ENERGY_METER = 0x0B
};
// Device state for Solar Charger
// --- Device state for Solar Charger ---
enum SolarChargerState {
CHARGER_OFF = 0,
CHARGER_LOW_POWER = 1,
@@ -53,163 +55,219 @@ enum SolarChargerState {
CHARGER_EXTERNAL_CONTROL = 252
};
// Binary data structures for decoding BLE advertisements
// Must use __attribute__((packed)) to prevent compiler padding
// ============================================================
// Wire-format packed structures for decoding BLE advertisements
// ============================================================
// Manufacturer data structure (outer envelope)
struct victronManufacturerData {
uint16_t vendorID; // vendor ID
uint8_t beaconType; // Should be 0x10 (Product Advertisement) for the packets we want
uint8_t unknownData1[3]; // Unknown data
uint8_t victronRecordType; // Should be 0x01 (Solar Charger) for the packets we want
uint16_t nonceDataCounter; // Nonce
uint8_t encryptKeyMatch; // Should match pre-shared encryption key byte 0
uint8_t victronEncryptedData[21]; // (31 bytes max per BLE spec - size of previous elements)
uint8_t nullPad; // extra byte because toCharArray() adds a \0 byte.
uint16_t vendorID;
uint8_t beaconType; // 0x10 = Product Advertisement
uint8_t unknownData1[3];
uint8_t victronRecordType; // Device type (see VictronDeviceType)
uint16_t nonceDataCounter;
uint8_t encryptKeyMatch; // Should match encryption key byte 0
uint8_t victronEncryptedData[VICTRON_ENCRYPTED_LEN];
} __attribute__((packed));
// Decrypted payload structures for each device type
// Solar Charger decrypted payload
struct victronSolarChargerPayload {
uint8_t deviceState; // Charge state (SolarChargerState enum)
uint8_t errorCode; // Error code
int16_t batteryVoltage; // Battery voltage in 10mV units
int16_t batteryCurrent; // Battery current in 10mA units (signed)
uint16_t yieldToday; // Yield today in 10Wh units
uint16_t inputPower; // PV power in 1W units
uint16_t loadCurrent; // Load current in 10mA units (0xFFFF = no load)
uint8_t reserved[2]; // Reserved bytes
uint8_t deviceState;
uint8_t errorCode;
int16_t batteryVoltage; // 10mV units
int16_t batteryCurrent; // 10mA units (signed)
uint16_t yieldToday; // 10Wh units
uint16_t inputPower; // 1W units
uint16_t loadCurrent; // 10mA units (0xFFFF = no load)
uint8_t reserved[2];
} __attribute__((packed));
// Battery Monitor decrypted payload
struct victronBatteryMonitorPayload {
uint16_t remainingMins; // Time remaining in minutes
uint16_t batteryVoltage; // Battery voltage in 10mV units
uint8_t alarms; // Alarm bits
uint16_t auxData; // Aux voltage (10mV) or temperature (0.01K)
uint8_t currentLow; // Battery current bits 0-7
uint8_t currentMid; // Battery current bits 8-15
uint8_t currentHigh_consumedLow; // Current bits 16-21 (low 6 bits), consumed bits 0-1 (high 2 bits)
uint8_t consumedMid; // Consumed Ah bits 2-9
uint8_t consumedHigh; // Consumed Ah bits 10-17
uint16_t soc; // State of charge in 0.1% units (10-bit value)
uint8_t reserved[2]; // Reserved bytes
uint16_t remainingMins;
uint16_t batteryVoltage; // 10mV units
uint8_t alarms;
uint16_t auxData; // 10mV (voltage) or 0.01K (temperature)
uint8_t currentLow;
uint8_t currentMid;
uint8_t currentHigh_consumedLow; // Current bits 16-21 (low 6), consumed bits 0-1 (high 2)
uint8_t consumedMid;
uint8_t consumedHigh;
uint16_t soc; // 0.1% units (10-bit)
uint8_t reserved[2];
} __attribute__((packed));
// Inverter decrypted payload
struct victronInverterPayload {
uint8_t deviceState; // Device state
uint8_t errorCode; // Error code
uint16_t batteryVoltage; // Battery voltage in 10mV units
int16_t batteryCurrent; // Battery current in 10mA units (signed)
uint8_t acPowerLow; // AC Power bits 0-7
uint8_t acPowerMid; // AC Power bits 8-15
uint8_t acPowerHigh; // AC Power bits 16-23 (signed 24-bit)
uint8_t alarms; // Alarm bits
uint8_t reserved[4]; // Reserved bytes
uint8_t deviceState;
uint8_t errorCode;
uint16_t batteryVoltage; // 10mV units
int16_t batteryCurrent; // 10mA units (signed)
uint8_t acPowerLow;
uint8_t acPowerMid;
uint8_t acPowerHigh; // Signed 24-bit
uint8_t alarms;
uint8_t reserved[4];
} __attribute__((packed));
// DC-DC Converter decrypted payload
struct victronDCDCConverterPayload {
uint8_t chargeState; // Charge state
uint8_t errorCode; // Error code
uint16_t inputVoltage; // Input voltage in 10mV units
uint16_t outputVoltage; // Output voltage in 10mV units
uint16_t outputCurrent; // Output current in 10mA units
uint8_t reserved[6]; // Reserved bytes
uint8_t chargeState;
uint8_t errorCode;
uint16_t inputVoltage; // 10mV units
uint16_t outputVoltage; // 10mV units
uint16_t outputCurrent; // 10mA units
uint8_t reserved[6];
} __attribute__((packed));
// Base structure for all device data
struct VictronDeviceData {
String deviceName;
String macAddress;
VictronDeviceType deviceType;
int8_t rssi;
uint32_t lastUpdate;
bool dataValid;
// ============================================================
// Parsed data structures (flat, no inheritance)
// ============================================================
VictronDeviceData() : deviceType(DEVICE_TYPE_UNKNOWN), rssi(-100),
lastUpdate(0), dataValid(false) {}
};
// Solar Charger specific data
struct SolarChargerData : public VictronDeviceData {
SolarChargerState chargeState;
struct VictronSolarData {
uint8_t chargeState; // SolarChargerState enum
uint8_t errorCode;
float batteryVoltage; // V
float batteryCurrent; // A
float panelVoltage; // V (PV voltage)
float panelPower; // W
uint16_t yieldToday; // Wh
float loadCurrent; // A
SolarChargerData() : chargeState(CHARGER_OFF), batteryVoltage(0),
batteryCurrent(0), panelVoltage(0), panelPower(0),
yieldToday(0), loadCurrent(0) {
deviceType = DEVICE_TYPE_SOLAR_CHARGER;
}
};
// Battery Monitor/SmartShunt specific data
struct BatteryMonitorData : public VictronDeviceData {
struct VictronBatteryData {
float voltage; // V
float current; // A (positive = charging, negative = discharging)
float temperature; // °C
float auxVoltage; // V (starter battery or midpoint)
uint16_t remainingMinutes; // Minutes
float current; // A
float temperature; // C (0 if aux is voltage)
float auxVoltage; // V (0 if aux is temperature)
uint16_t remainingMinutes;
float consumedAh; // Ah
float soc; // State of Charge %
float soc; // %
bool alarmLowVoltage;
bool alarmHighVoltage;
bool alarmLowSOC;
bool alarmLowTemperature;
bool alarmHighTemperature;
BatteryMonitorData() : voltage(0), current(0), temperature(0),
auxVoltage(0), remainingMinutes(0), consumedAh(0),
soc(0), alarmLowVoltage(false), alarmHighVoltage(false),
alarmLowSOC(false), alarmLowTemperature(false),
alarmHighTemperature(false) {
deviceType = DEVICE_TYPE_BATTERY_MONITOR;
}
};
// Inverter specific data
struct InverterData : public VictronDeviceData {
struct VictronInverterData {
float batteryVoltage; // V
float batteryCurrent; // A
float acPower; // W
uint8_t state; // Inverter state
bool alarmHighVoltage;
uint8_t state;
bool alarmLowVoltage;
bool alarmHighVoltage;
bool alarmHighTemperature;
bool alarmOverload;
InverterData() : batteryVoltage(0), batteryCurrent(0), acPower(0),
state(0), alarmHighVoltage(false), alarmLowVoltage(false),
alarmHighTemperature(false), alarmOverload(false) {
deviceType = DEVICE_TYPE_INVERTER;
}
};
// DC-DC Converter specific data
struct DCDCConverterData : public VictronDeviceData {
struct VictronDCDCData {
float inputVoltage; // V
float outputVoltage; // V
float outputCurrent; // A
uint8_t chargeState;
uint8_t errorCode;
DCDCConverterData() : inputVoltage(0), outputVoltage(0), outputCurrent(0),
chargeState(0), errorCode(0) {
deviceType = DEVICE_TYPE_DCDC_CONVERTER;
}
};
// Forward declarations
class VictronBLE;
// ============================================================
// Main device struct with tagged union
// ============================================================
struct VictronDevice {
char name[VICTRON_NAME_LEN];
char mac[VICTRON_MAC_LEN];
VictronDeviceType deviceType;
int8_t rssi;
uint32_t lastUpdate;
bool dataValid;
union {
VictronSolarData solar;
VictronBatteryData battery;
VictronInverterData inverter;
VictronDCDCData dcdc;
};
};
// ============================================================
// Callback — simple function pointer
// ============================================================
typedef void (*VictronCallback)(const VictronDevice* device);
// Forward declaration
class VictronBLEAdvertisedDeviceCallbacks;
// Callback interface for device data updates
// ============================================================
// Main VictronBLE class
// ============================================================
class VictronBLE {
public:
VictronBLE();
bool begin(uint32_t scanDuration = 5);
bool addDevice(const char* name, const char* mac, const char* hexKey,
VictronDeviceType type = DEVICE_TYPE_UNKNOWN);
void setCallback(VictronCallback cb) { callback = cb; }
void setDebug(bool enable) { debugEnabled = enable; }
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 {
public:
virtual ~VictronDeviceCallback() {}
@@ -219,114 +277,17 @@ public:
virtual void onDCDCConverterData(const DCDCConverterData& data) {}
};
// Device configuration structure
struct VictronDeviceConfig {
String name;
String macAddress;
String encryptionKey; // 32 character hex string
VictronDeviceType expectedType;
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) {}
};
// Main VictronBLE class
class VictronBLE {
public:
VictronBLE();
~VictronBLE();
// Initialize BLE and start scanning
bool begin(uint32_t scanDuration = 5);
// Add a device to monitor
bool addDevice(const VictronDeviceConfig& config);
bool addDevice(const String& name, const String& macAddress, const String& encryptionKey,
VictronDeviceType expectedType = DEVICE_TYPE_UNKNOWN);
// Remove a device
void removeDevice(const String& macAddress);
// Get device count
size_t getDeviceCount() const { return devices.size(); }
// Set callback for data updates
void setCallback(VictronDeviceCallback* cb) { callback = cb; }
// Process scanning (call in loop())
void loop();
// Get latest data for a device
// --- Per-type getter methods (use callback instead) ---
bool getSolarChargerData(const String& macAddress, SolarChargerData& data);
bool getBatteryMonitorData(const String& macAddress, BatteryMonitorData& data);
bool getInverterData(const String& macAddress, InverterData& data);
bool getDCDCConverterData(const String& macAddress, DCDCConverterData& data);
// Get all devices of a specific type
// --- Other removed methods ---
void removeDevice(const String& macAddress);
std::vector<String> getDevicesByType(VictronDeviceType type);
String getLastError() const;
// 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 // commented-out features
#endif // VICTRON_BLE_H