Initial readme up

This commit is contained in:
2025-12-18 17:39:13 +11:00
commit d5f3d3ecbe
10 changed files with 2002 additions and 0 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 VictronBLE Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

277
QUICK_START.md Normal file
View File

@@ -0,0 +1,277 @@
# VictronBLE Quick Start Guide
## Getting Started in 5 Minutes
### Step 1: Get Your Device Encryption Keys
1. Install **VictronConnect** app on your phone/computer
2. Connect to your Victron device via Bluetooth
3. Navigate to: **Settings****Product Info**
4. Enable **"Instant readout via Bluetooth"** (if not already enabled)
5. Click **"Show"** next to **"Instant readout details"**
6. Copy the **Encryption Key** (32 hex characters like: `0df4d0395b7d1a876c0c33ecb9e70dcd`)
7. Note your device's **MAC address** (format: `AA:BB:CC:DD:EE:FF`)
**Important:** Do this for EACH device you want to monitor.
### Step 2: Install the Library
#### Option A: PlatformIO (Recommended)
1. Copy the `VictronBLE` folder to your project's `lib/` directory
2. Your project structure should look like:
```
your-project/
├── lib/
│ └── VictronBLE/
│ ├── src/
│ ├── examples/
│ ├── library.json
│ └── README.md
├── src/
│ └── main.cpp
└── platformio.ini
```
#### Option B: Arduino IDE
1. Copy the `VictronBLE` folder to your Arduino `libraries` directory:
- Windows: `Documents\Arduino\libraries\`
- Mac: `~/Documents/Arduino/libraries/`
- Linux: `~/Arduino/libraries/`
2. Restart Arduino IDE
### Step 3: Update the Example Code
Open `examples/MultiDevice/main.cpp` and update these lines with YOUR device information:
```cpp
// Replace these with YOUR actual device details:
victron.addDevice(
"MPPT 100/30", // Give it a friendly name
"E7:48:D4:28:B7:9C", // YOUR device MAC address
"0df4d0395b7d1a876c0c33ecb9e70dcd", // YOUR encryption key
DEVICE_TYPE_SOLAR_CHARGER // Device type
);
```
#### For Multiple Devices:
```cpp
// Solar Charger #1
victron.addDevice("MPPT 1", "AA:BB:CC:DD:EE:01", "key1here", DEVICE_TYPE_SOLAR_CHARGER);
// Solar Charger #2
victron.addDevice("MPPT 2", "AA:BB:CC:DD:EE:02", "key2here", DEVICE_TYPE_SOLAR_CHARGER);
// Battery Monitor
victron.addDevice("SmartShunt", "AA:BB:CC:DD:EE:03", "key3here", DEVICE_TYPE_BATTERY_MONITOR);
// Inverter/Charger
victron.addDevice("MultiPlus", "AA:BB:CC:DD:EE:04", "key4here", DEVICE_TYPE_INVERTER);
```
### Step 4: Build and Upload
#### PlatformIO:
```bash
cd examples/MultiDevice
pio run -t upload && pio device monitor
```
#### Arduino IDE:
1. Open `examples/MultiDevice/main.cpp` as an .ino file
2. Select your ESP32 board from Tools → Board
3. Select your COM port from Tools → Port
4. Click Upload
5. Open Serial Monitor at 115200 baud
### Step 5: Watch the Data!
You should see output like:
```
=== Solar Charger: MPPT 100/30 ===
MAC: e7:48:d4:28:b7:9c
RSSI: -65 dBm
State: Bulk
Battery: 13.45 V
Current: 12.50 A
Panel Voltage: 18.2 V
Panel Power: 168 W
Yield Today: 1250 Wh
=== Battery Monitor: SmartShunt ===
MAC: 11:22:33:44:55:66
RSSI: -58 dBm
Voltage: 13.45 V
Current: 12.50 A
SOC: 87.5 %
Consumed: 2.34 Ah
Time Remaining: 6h 45m
```
## Troubleshooting
### "No data received"
1. **Make sure VictronConnect app is CLOSED/DISCONNECTED**
- The app prevents BLE advertisements when connected
- Close the app completely on all devices
2. **Verify encryption key is correct**
- Must be exactly 32 hex characters
- Copy/paste from VictronConnect to avoid typos
3. **Check MAC address format**
- Must use colons: `AA:BB:CC:DD:EE:FF`
- Case doesn't matter
4. **Enable debug mode to see what's happening:**
```cpp
victron.setDebug(true);
```
5. **Check distance**
- BLE range is typically 10-30 meters
- Move ESP32 closer to Victron device
### "Decryption failed"
- Encryption key must match EXACTLY
- Get the key again from VictronConnect
- Make sure you're using the CURRENT key (not an old one)
### "Device not found"
- Verify MAC address is correct
- Check that "Instant Readout" is enabled in VictronConnect
- Make sure device has Bluetooth (older models may need BLE dongle)
## Next Steps
### Customize the Output
Edit the callback functions in your code to change what data is displayed:
```cpp
class MyCallback : public VictronDeviceCallback {
public:
void onSolarChargerData(const SolarChargerData& data) override {
// Display only what you want:
Serial.printf("%s: %dW @ %.1fV\n",
data.deviceName.c_str(),
data.panelPower,
data.batteryVoltage);
}
};
```
### Add Your Own Logic
```cpp
void onSolarChargerData(const SolarChargerData& data) override {
// Turn on relay if producing power
if (data.panelPower > 100) {
digitalWrite(RELAY_PIN, HIGH);
}
// Log to SD card
logToSDCard(data);
// Send to MQTT
publishToMQTT(data);
// Update display
updateLCD(data);
}
```
### Integration Ideas
- **Web Dashboard**: Serve data over WiFi
- **MQTT**: Publish to Home Assistant, Node-RED
- **Display**: Show on OLED/TFT screen
- **Data Logging**: Log to SD card or cloud
- **Automation**: Control loads based on solar power
- **Alarms**: Send notifications on low battery
## Device Type Reference
| Device Type | Constant | Examples |
|------------|----------|----------|
| Solar Charger | `DEVICE_TYPE_SOLAR_CHARGER` | SmartSolar MPPT, BlueSolar MPPT |
| Battery Monitor | `DEVICE_TYPE_BATTERY_MONITOR` | SmartShunt, BMV-712, BMV-700 |
| Inverter | `DEVICE_TYPE_INVERTER` | MultiPlus, Quattro, Phoenix |
| DC-DC Converter | `DEVICE_TYPE_DCDC_CONVERTER` | Orion Smart, Orion XS |
## Common Use Cases
### Home Solar Monitoring
```cpp
// Monitor solar production and battery
victron.addDevice("Solar", "MAC1", "KEY1", DEVICE_TYPE_SOLAR_CHARGER);
victron.addDevice("Battery", "MAC2", "KEY2", DEVICE_TYPE_BATTERY_MONITOR);
```
### RV/Van Setup
```cpp
// Monitor multiple solar panels and battery bank
victron.addDevice("Roof MPPT", "MAC1", "KEY1", DEVICE_TYPE_SOLAR_CHARGER);
victron.addDevice("Portable MPPT", "MAC2", "KEY2", DEVICE_TYPE_SOLAR_CHARGER);
victron.addDevice("House Battery", "MAC3", "KEY3", DEVICE_TYPE_BATTERY_MONITOR);
victron.addDevice("Starter Battery", "MAC4", "KEY4", DEVICE_TYPE_DCDC_CONVERTER);
```
### Boat Power System
```cpp
// Complete boat power monitoring
victron.addDevice("Solar", "MAC1", "KEY1", DEVICE_TYPE_SOLAR_CHARGER);
victron.addDevice("Battery Bank", "MAC2", "KEY2", DEVICE_TYPE_BATTERY_MONITOR);
victron.addDevice("Inverter", "MAC3", "KEY3", DEVICE_TYPE_INVERTER);
```
## Performance Tips
### Scan Duration
```cpp
victron.begin(5); // 5 seconds - balanced (default)
victron.begin(1); // 1 second - faster updates, may miss devices
victron.begin(10); // 10 seconds - slower but very reliable
```
### Power Consumption
- Passive BLE scanning uses minimal power (~10-20mA)
- No WiFi needed (unless you add it yourself)
- Perfect for battery-powered projects
### Update Rate
- Victron devices broadcast every ~1 second
- Your ESP32 will receive updates based on scan duration
- Data is cached between scans
## Support
- 📖 Read the full README.md
- 🐛 Report issues on GitHub
- 💬 Check existing GitHub issues
- 🔧 Enable debug mode: `victron.setDebug(true)`
## Example Project Layout
```
my-victron-monitor/
├── lib/
│ └── VictronBLE/ # Copy the whole VictronBLE folder here
├── src/
│ └── main.cpp # Your code (based on example)
└── platformio.ini # PlatformIO config
# Or in Arduino:
Arduino/libraries/VictronBLE/ # Copy VictronBLE folder here
my-victron-monitor/
└── my-victron-monitor.ino # Your code
```
Happy monitoring! 🔋⚡

444
README.md Normal file
View File

@@ -0,0 +1,444 @@
# VictronBLE
ESP32 library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements.
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.
## Features
-**Multiple Device Support**: Monitor multiple Victron devices simultaneously
-**All Device Types**: Solar chargers, battery monitors, inverters, DC-DC converters
-**Framework Agnostic**: Works with Arduino and ESP-IDF
-**Clean API**: Simple, intuitive interface with callback support
-**No Pairing Required**: Reads BLE advertisement data directly
-**Low Power**: Uses passive BLE scanning
-**Full Data Access**: Battery voltage, current, SOC, power, alarms, and more
-**Production Ready**: Error handling, data validation, debug logging
## Supported Devices
- **Solar Chargers**: SmartSolar MPPT, BlueSolar MPPT (with BLE dongle)
- **Battery Monitors**: SmartShunt, BMV-712 Smart, BMV-700 series
- **Inverters**: MultiPlus, Quattro, Phoenix (with VE.Bus BLE dongle)
- **DC-DC Converters**: Orion Smart, Orion XS
- **Others**: Smart Battery Protect, Lynx Smart BMS, Smart Lithium batteries
## Hardware Requirements
- ESP32, ESP32-S3, or ESP32-C3 board
- Victron devices with BLE "Instant Readout" enabled
## Installation
### PlatformIO
1. Add to `platformio.ini`:
```ini
lib_deps =
https://github.com/yourusername/VictronBLE.git
```
2. Or clone into your project's `lib` folder:
```bash
cd lib
git clone https://github.com/yourusername/VictronBLE.git
```
### Arduino IDE
1. Download or clone this repository
2. Move the `VictronBLE` folder to your Arduino libraries directory
3. Restart Arduino IDE
## Quick Start
### 1. Get Your Encryption Keys
Use the VictronConnect app to get your device's encryption key:
1. Open VictronConnect
2. Connect to your device
3. Go to **Settings****Product Info**
4. Enable **"Instant readout via Bluetooth"**
5. Click **"Show"** next to **"Instant readout details"**
6. Copy the **encryption key** (32 hexadecimal characters)
7. Note your device's **MAC address**
### 2. Basic Example
```cpp
#include <Arduino.h>
#include "VictronBLE.h"
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);
}
};
MyCallback callback;
void setup() {
Serial.begin(115200);
// Initialize library
victron.begin(5); // 5 second scan duration
victron.setCallback(&callback);
// 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
);
}
void loop() {
victron.loop();
delay(100);
}
```
## API Reference
### VictronBLE Class
#### Initialization
```cpp
bool begin(uint32_t scanDuration = 5);
```
Initialize BLE and start scanning. Returns `true` on success.
**Parameters:**
- `scanDuration`: BLE scan duration in seconds (default: 5)
#### Device Management
```cpp
bool addDevice(String name, String macAddress, String encryptionKey,
VictronDeviceType expectedType = DEVICE_TYPE_UNKNOWN);
```
Add a device to monitor.
**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)
**Returns:** `true` on success
```cpp
void removeDevice(String macAddress);
```
Remove a device from monitoring.
```cpp
size_t getDeviceCount();
```
Get the number of configured devices.
#### Data Access
```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);
```
Get latest data for a specific device. Returns `true` if data is valid.
```cpp
std::vector<String> getDevicesByType(VictronDeviceType type);
```
Get MAC addresses of all devices of a specific type.
#### Callbacks
```cpp
void setCallback(VictronDeviceCallback* callback);
```
Set callback object to receive data updates automatically.
#### Utilities
```cpp
void setDebug(bool enable);
```
Enable/disable debug output to Serial.
```cpp
String getLastError();
```
Get last error message.
```cpp
void loop();
```
Process BLE scanning and data updates. Call this in your main loop.
### Data Structures
#### SolarChargerData
```cpp
struct SolarChargerData {
String deviceName;
String macAddress;
int8_t rssi; // Signal strength (dBm)
uint32_t lastUpdate; // millis() of last update
bool dataValid; // Data validity flag
SolarChargerState chargeState; // Charging state
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
#### BatteryMonitorData
```cpp
struct BatteryMonitorData {
String deviceName;
String macAddress;
int8_t rssi;
uint32_t lastUpdate;
bool dataValid;
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 soc; // State of charge %
// Alarms
bool alarmLowVoltage;
bool alarmHighVoltage;
bool alarmLowSOC;
bool alarmLowTemperature;
bool alarmHighTemperature;
};
```
#### InverterData
```cpp
struct InverterData {
String deviceName;
String macAddress;
int8_t rssi;
uint32_t lastUpdate;
bool dataValid;
float batteryVoltage; // V
float batteryCurrent; // A
float acPower; // W (+ inverting, - charging)
uint8_t state; // Inverter state
// Alarms
bool alarmHighVoltage;
bool alarmLowVoltage;
bool alarmHighTemperature;
bool alarmOverload;
};
```
#### DCDCConverterData
```cpp
struct DCDCConverterData {
String deviceName;
String macAddress;
int8_t rssi;
uint32_t lastUpdate;
bool dataValid;
float inputVoltage; // V
float outputVoltage; // V
float outputCurrent; // A
uint8_t chargeState;
uint8_t errorCode;
};
```
## Advanced Usage
### Multiple Devices
```cpp
void setup() {
victron.begin(5);
victron.setCallback(&callback);
// 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);
}
```
### Manual Data Polling
```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;
}
}
delay(1000);
}
```
### Find All Devices of a Type
```cpp
void loop() {
victron.loop();
// 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
### No Data Received
1. **Check encryption key**: Must be exactly 32 hex characters from VictronConnect
2. **Verify MAC address**: Use correct format (AA:BB:CC:DD:EE:FF)
3. **Enable Instant Readout**: Must be enabled in VictronConnect settings
4. **Check range**: BLE range is typically 10-30 meters
5. **Disconnect VictronConnect**: App must be disconnected from device
6. **Enable debug**: `victron.setDebug(true);` to see detailed logs
### Decryption Failures
- Encryption key must match exactly
- Victron may have multiple keys per device; use the current one
- Keys are case-insensitive hex
### Poor Performance
- Reduce `scanDuration` for faster updates (minimum 1 second)
- Increase `scanDuration` for better reliability (5-10 seconds recommended)
- Ensure good signal strength (RSSI > -80 dBm)
## Protocol Details
This library implements the Victron BLE Advertising protocol:
- **Encryption**: AES-128-CTR
- **Update Rate**: ~1 Hz from Victron devices
- **No Pairing**: Reads broadcast advertisements
- **No Connection**: Extremely low power consumption
Based on official [Victron BLE documentation](https://www.victronenergy.com/live/vedirect_protocol:faq).
## Examples
See the `examples/` directory for:
- **MultiDevice**: Monitor multiple devices with callbacks
- More examples coming soon!
## Contributing
Contributions welcome! Please:
1. Fork the repository
2. Create a feature branch
3. Test thoroughly on real hardware
4. Submit a pull request
## Credits
This library was inspired by and builds upon excellent prior work:
- **[hoberman's Victron BLE Advertising examples](https://github.com/hoberman/Victron_BLE_Advertising_example)**: ESP32 examples demonstrating Victron BLE protocol implementation
- **[keshavdv's victron-ble Python library](https://github.com/keshavdv/victron-ble)**: Comprehensive Python implementation of Victron BLE protocol
- Protocol documentation and specifications by Victron Energy
## License
MIT License - see LICENSE file for details
## Disclaimer
This library is not officially supported by Victron Energy. Use at your own risk.
## Support
- 📫 Report issues on GitHub
- 📖 Check the examples directory
- 🔧 Enable debug mode for diagnostics
- 📚 See [Victron documentation](https://www.victronenergy.com/live/)

View File

@@ -0,0 +1,38 @@
[env]
lib_extra_dirs = ../..
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
; Serial monitor settings
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
; Build flags
build_flags =
-DCORE_DEBUG_LEVEL=3
; Library dependencies
lib_deps =
; VictronBLE library will be automatically included from parent directory
; Optional: Specify partition scheme if needed
; board_build.partitions = default.csv
[env:esp32-s3]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200
build_flags =
-DCORE_DEBUG_LEVEL=3
[env:esp32-c3]
platform = espressif32
board = esp32-c3-devkitc-02
framework = arduino
monitor_speed = 115200
build_flags =
-DCORE_DEBUG_LEVEL=3

View File

@@ -0,0 +1,224 @@
/**
* VictronBLE 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.)
*
* 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
*/
#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:
void onSolarChargerData(const SolarChargerData& data) override {
Serial.println("\n=== Solar Charger: " + data.deviceName + " ===");
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 {
Serial.println("\n=== Battery Monitor: " + data.deviceName + " ===");
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 {
Serial.println("\n=== Inverter/Charger: " + data.deviceName + " ===");
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 {
Serial.println("\n=== DC-DC Converter: " + data.deviceName + " ===");
Serial.println("MAC: " + data.macAddress);
Serial.println("RSSI: " + String(data.rssi) + " dBm");
Serial.println("Input: " + String(data.inputVoltage, 2) + " V");
Serial.println("Output: " + String(data.outputVoltage, 2) + " V");
Serial.println("Current: " + String(data.outputCurrent, 2) + " A");
Serial.println("State: " + String(data.chargeState));
if (data.errorCode != 0) {
Serial.println("Error Code: " + String(data.errorCode));
}
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
}
private:
String getChargeStateName(SolarChargerState state) {
switch (state) {
case CHARGER_OFF: return "Off";
case CHARGER_LOW_POWER: return "Low Power";
case CHARGER_FAULT: return "Fault";
case CHARGER_BULK: return "Bulk";
case CHARGER_ABSORPTION: return "Absorption";
case CHARGER_FLOAT: return "Float";
case CHARGER_STORAGE: return "Storage";
case CHARGER_EQUALIZE: return "Equalize";
case CHARGER_INVERTING: return "Inverting";
case CHARGER_POWER_SUPPLY: return "Power Supply";
case CHARGER_EXTERNAL_CONTROL: return "External Control";
default: return "Unknown";
}
}
};
MyVictronCallback callback;
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n\n=================================");
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(true);
// Set callback for data updates
victron.setCallback(&callback);
// Add your devices here
// Replace with your actual MAC addresses and encryption keys
// 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
);
// Example: Battery Monitor (SmartShunt)
victron.addDevice(
"SmartShunt",
"11:22:33:44:55:66",
"fedcba0987654321fedcba0987654321",
DEVICE_TYPE_BATTERY_MONITOR
);
// Example: Inverter/Charger
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");
}
void loop() {
// Process BLE scanning and data updates
victron.loop();
// Optional: You can also manually query device data
// This is useful if you're not using callbacks
/*
SolarChargerData solarData;
if (victron.getSolarChargerData("E7:48:D4:28:B7:9C", solarData)) {
// Do something with solarData
}
BatteryMonitorData batteryData;
if (victron.getBatteryMonitorData("11:22:33:44:55:66", batteryData)) {
// Do something with batteryData
}
*/
// Add a small delay to avoid overwhelming the serial output
delay(100);
}

67
keywords.txt Normal file
View File

@@ -0,0 +1,67 @@
#######################################
# Syntax Coloring Map For VictronBLE
#######################################
#######################################
# Datatypes (KEYWORD1)
#######################################
VictronBLE KEYWORD1
VictronDeviceCallback KEYWORD1
VictronDeviceConfig KEYWORD1
VictronDeviceData KEYWORD1
SolarChargerData KEYWORD1
BatteryMonitorData KEYWORD1
InverterData KEYWORD1
DCDCConverterData KEYWORD1
#######################################
# Methods and Functions (KEYWORD2)
#######################################
begin KEYWORD2
addDevice KEYWORD2
removeDevice KEYWORD2
getDeviceCount KEYWORD2
setCallback KEYWORD2
loop KEYWORD2
getSolarChargerData KEYWORD2
getBatteryMonitorData KEYWORD2
getInverterData KEYWORD2
getDCDCConverterData KEYWORD2
getDevicesByType KEYWORD2
setDebug KEYWORD2
getLastError KEYWORD2
onSolarChargerData KEYWORD2
onBatteryMonitorData KEYWORD2
onInverterData KEYWORD2
onDCDCConverterData KEYWORD2
#######################################
# Constants (LITERAL1)
#######################################
DEVICE_TYPE_UNKNOWN LITERAL1
DEVICE_TYPE_SOLAR_CHARGER LITERAL1
DEVICE_TYPE_BATTERY_MONITOR LITERAL1
DEVICE_TYPE_INVERTER LITERAL1
DEVICE_TYPE_DCDC_CONVERTER LITERAL1
DEVICE_TYPE_SMART_LITHIUM LITERAL1
DEVICE_TYPE_INVERTER_RS LITERAL1
DEVICE_TYPE_SMART_BATTERY_PROTECT LITERAL1
DEVICE_TYPE_LYNX_SMART_BMS LITERAL1
DEVICE_TYPE_MULTI_RS LITERAL1
DEVICE_TYPE_VE_BUS LITERAL1
DEVICE_TYPE_DC_ENERGY_METER LITERAL1
CHARGER_OFF LITERAL1
CHARGER_LOW_POWER LITERAL1
CHARGER_FAULT LITERAL1
CHARGER_BULK LITERAL1
CHARGER_ABSORPTION LITERAL1
CHARGER_FLOAT LITERAL1
CHARGER_STORAGE LITERAL1
CHARGER_EQUALIZE LITERAL1
CHARGER_INVERTING LITERAL1
CHARGER_POWER_SUPPLY LITERAL1
CHARGER_EXTERNAL_CONTROL LITERAL1

29
library.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "VictronBLE",
"version": "1.0.0",
"description": "ESP32 library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements",
"keywords": "victron, ble, bluetooth, solar, mppt, battery, smartshunt, esp32",
"repository": {
"type": "git",
"url": "https://github.com/yourusername/VictronBLE.git"
},
"authors": [
{
"name": "Your Name",
"email": "your.email@example.com"
}
],
"license": "MIT",
"homepage": "https://github.com/yourusername/VictronBLE",
"frameworks": ["arduino", "espidf"],
"platforms": ["espressif32"],
"dependencies": [],
"export": {
"exclude": [
"examples",
"test",
"tests",
".git"
]
}
}

11
library.properties Normal file
View File

@@ -0,0 +1,11 @@
name=VictronBLE
version=1.0.0
author=VictronBLE Contributors
maintainer=Your Name <your.email@example.com>
sentence=ESP32 library for reading Victron Energy device data via BLE
paragraph=Read data from Victron SmartSolar, SmartShunt, BMV, inverters and other devices using Bluetooth Low Energy advertisements. Supports multiple devices simultaneously with no pairing required.
category=Communication
url=https://github.com/yourusername/VictronBLE
architectures=esp32
depends=
includes=VictronBLE.h

627
src/VictronBLE.cpp Normal file
View File

@@ -0,0 +1,627 @@
/**
* VictronBLE - ESP32 library for Victron Energy BLE devices
* Implementation file
*/
#include "VictronBLE.h"
// Constructor
VictronBLE::VictronBLE()
: pBLEScan(nullptr), callback(nullptr), debugEnabled(false),
scanDuration(5), initialized(false) {
}
// Destructor
VictronBLE::~VictronBLE() {
for (auto& pair : devices) {
delete pair.second;
}
devices.clear();
if (pBLEScan) {
pBLEScan->stop();
}
}
// Initialize BLE
bool VictronBLE::begin(uint32_t scanDuration) {
if (initialized) {
debugPrint("VictronBLE already initialized");
return true;
}
this->scanDuration = scanDuration;
debugPrint("Initializing VictronBLE...");
BLEDevice::init("VictronBLE");
pBLEScan = BLEDevice::getScan();
if (!pBLEScan) {
lastError = "Failed to create BLE scanner";
return false;
}
pBLEScan->setAdvertisedDeviceCallbacks(new VictronBLEAdvertisedDeviceCallbacks(this), true);
pBLEScan->setActiveScan(false); // Passive scan - lower power
pBLEScan->setInterval(100);
pBLEScan->setWindow(99);
initialized = true;
debugPrint("VictronBLE initialized successfully");
return true;
}
// Add a device to monitor
bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
if (config.macAddress.length() == 0) {
lastError = "MAC address cannot be empty";
return false;
}
if (config.encryptionKey.length() != 32) {
lastError = "Encryption key must be 32 hex characters";
return false;
}
String normalizedMAC = normalizeMAC(config.macAddress);
// Check if device already exists
if (devices.find(normalizedMAC) != devices.end()) {
debugPrint("Device " + normalizedMAC + " already exists, updating config");
delete devices[normalizedMAC];
}
DeviceInfo* info = new DeviceInfo();
info->config = config;
info->config.macAddress = normalizedMAC;
// Convert encryption key from hex string to bytes
if (!hexStringToBytes(config.encryptionKey, info->encryptionKeyBytes, 16)) {
lastError = "Invalid encryption key format";
delete info;
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;
debugPrint("Added device: " + config.name + " (" + normalizedMAC + ")");
return true;
}
bool VictronBLE::addDevice(String name, String macAddress, String encryptionKey,
VictronDeviceType expectedType) {
VictronDeviceConfig config(name, macAddress, encryptionKey, expectedType);
return addDevice(config);
}
// Remove a device
void VictronBLE::removeDevice(String macAddress) {
String normalizedMAC = normalizeMAC(macAddress);
auto it = devices.find(normalizedMAC);
if (it != devices.end()) {
delete it->second;
devices.erase(it);
debugPrint("Removed device: " + normalizedMAC);
}
}
// Main loop function
void VictronBLE::loop() {
if (!initialized) {
return;
}
// Start a scan
BLEScanResults scanResults = pBLEScan->start(scanDuration, false);
pBLEScan->clearResults();
}
// BLE callback implementation
void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertisedDevice) {
if (victronBLE) {
victronBLE->processDevice(advertisedDevice);
}
}
// Process advertised device
void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) {
String mac = macAddressToString(advertisedDevice.getAddress());
String normalizedMAC = normalizeMAC(mac);
// Check if this is one of our configured devices
auto it = devices.find(normalizedMAC);
if (it == devices.end()) {
return; // Not a device we're monitoring
}
DeviceInfo* deviceInfo = it->second;
// Check if device has manufacturer data
if (!advertisedDevice.haveManufacturerData()) {
return;
}
std::string mfgData = advertisedDevice.getManufacturerData();
if (mfgData.length() < 2) {
return;
}
// Check if it's Victron (manufacturer ID 0x02E1)
uint16_t mfgId = (uint8_t)mfgData[1] << 8 | (uint8_t)mfgData[0];
if (mfgId != VICTRON_MANUFACTURER_ID) {
return;
}
debugPrint("Processing data from: " + deviceInfo->config.name);
// Parse the advertisement
if (parseAdvertisement((const uint8_t*)mfgData.data(), mfgData.length(), normalizedMAC)) {
// Update RSSI
if (deviceInfo->data) {
deviceInfo->data->rssi = advertisedDevice.getRSSI();
deviceInfo->data->lastUpdate = millis();
}
}
}
// Parse advertisement data
bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
const String& macAddress) {
auto it = devices.find(macAddress);
if (it == devices.end()) {
return false;
}
DeviceInfo* deviceInfo = it->second;
if (len < 6) {
debugPrint("Manufacturer data too short");
return false;
}
// Structure: [MfgID(2)] [DeviceType(1)] [IV(2)] [EncryptedData(n)]
uint8_t deviceType = manufacturerData[2];
// Extract IV (initialization vector) - bytes 3-4, little-endian
uint8_t iv[16] = {0};
iv[0] = manufacturerData[3];
iv[1] = manufacturerData[4];
// Rest of IV is zero-padded
// Encrypted data starts at byte 5
const uint8_t* encryptedData = manufacturerData + 5;
size_t encryptedLen = len - 5;
if (debugEnabled) {
debugPrintHex("Encrypted data", encryptedData, encryptedLen);
debugPrintHex("IV", iv, 16);
}
// Decrypt the data
uint8_t decrypted[32]; // Max expected size
if (!decryptAdvertisement(encryptedData, encryptedLen,
deviceInfo->encryptionKeyBytes, iv, decrypted)) {
lastError = "Decryption failed";
return false;
}
if (debugEnabled) {
debugPrintHex("Decrypted data", decrypted, encryptedLen);
}
// Parse based on device type
bool parseOk = false;
switch (deviceType) {
case DEVICE_TYPE_SOLAR_CHARGER:
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
parseOk = parseSolarCharger(decrypted, encryptedLen,
*(SolarChargerData*)deviceInfo->data);
}
break;
case DEVICE_TYPE_BATTERY_MONITOR:
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) {
parseOk = parseBatteryMonitor(decrypted, encryptedLen,
*(BatteryMonitorData*)deviceInfo->data);
}
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,
*(InverterData*)deviceInfo->data);
}
break;
case DEVICE_TYPE_DCDC_CONVERTER:
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) {
parseOk = parseDCDCConverter(decrypted, encryptedLen,
*(DCDCConverterData*)deviceInfo->data);
}
break;
default:
debugPrint("Unknown device type: 0x" + String(deviceType, HEX));
return false;
}
if (parseOk && deviceInfo->data) {
deviceInfo->data->dataValid = true;
// Call appropriate callback
if (callback) {
switch (deviceType) {
case DEVICE_TYPE_SOLAR_CHARGER:
callback->onSolarChargerData(*(SolarChargerData*)deviceInfo->data);
break;
case DEVICE_TYPE_BATTERY_MONITOR:
callback->onBatteryMonitorData(*(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(*(InverterData*)deviceInfo->data);
break;
case DEVICE_TYPE_DCDC_CONVERTER:
callback->onDCDCConverterData(*(DCDCConverterData*)deviceInfo->data);
break;
}
}
}
return parseOk;
}
// Decrypt advertisement using AES-128-CTR
bool VictronBLE::decryptAdvertisement(const uint8_t* encrypted, size_t encLen,
const uint8_t* key, const uint8_t* iv,
uint8_t* decrypted) {
mbedtls_aes_context aes;
mbedtls_aes_init(&aes);
// Set encryption key
int ret = mbedtls_aes_setkey_enc(&aes, key, 128);
if (ret != 0) {
mbedtls_aes_free(&aes);
return false;
}
// AES-CTR decryption
size_t nc_off = 0;
uint8_t nonce_counter[16];
uint8_t stream_block[16];
memcpy(nonce_counter, iv, 16);
memset(stream_block, 0, 16);
ret = mbedtls_aes_crypt_ctr(&aes, encLen, &nc_off, nonce_counter,
stream_block, encrypted, decrypted);
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 < 12) {
debugPrint("Solar charger data too short");
return false;
}
// Byte 0: Charge state
result.chargeState = (SolarChargerState)data[0];
// Bytes 1-2: Battery voltage (10 mV units)
uint16_t vBat = data[1] | (data[2] << 8);
result.batteryVoltage = vBat * 0.01f;
// Bytes 3-4: Battery current (10 mA units, signed)
int16_t iBat = (int16_t)(data[3] | (data[4] << 8));
result.batteryCurrent = iBat * 0.01f;
// Bytes 5-6: Yield today (10 Wh units)
uint16_t yield = data[5] | (data[6] << 8);
result.yieldToday = yield * 10;
// Bytes 7-8: PV power (1 W units)
uint16_t pvPower = data[7] | (data[8] << 8);
result.panelPower = pvPower;
// Bytes 9-10: Load current (10 mA units)
uint16_t iLoad = data[9] | (data[10] << 8);
if (iLoad != 0xFFFF) { // 0xFFFF means no load output
result.loadCurrent = iLoad * 0.01f;
}
// Calculate PV voltage from power and current (if current > 0)
if (result.batteryCurrent > 0.1f) {
result.panelVoltage = result.panelPower / result.batteryCurrent;
}
debugPrint("Solar Charger: " + String(result.batteryVoltage, 2) + "V, " +
String(result.batteryCurrent, 2) + "A, " +
String(result.panelPower) + "W, State: " + String(result.chargeState));
return true;
}
// Parse Battery Monitor data
bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result) {
if (len < 15) {
debugPrint("Battery monitor data too short");
return false;
}
// Bytes 0-1: Remaining time (1 minute units)
uint16_t timeRemaining = data[0] | (data[1] << 8);
result.remainingMinutes = timeRemaining;
// Bytes 2-3: Battery voltage (10 mV units)
uint16_t vBat = data[2] | (data[3] << 8);
result.voltage = vBat * 0.01f;
// Byte 4: Alarms
uint8_t alarms = data[4];
result.alarmLowVoltage = (alarms & 0x01) != 0;
result.alarmHighVoltage = (alarms & 0x02) != 0;
result.alarmLowSOC = (alarms & 0x04) != 0;
result.alarmLowTemperature = (alarms & 0x10) != 0;
result.alarmHighTemperature = (alarms & 0x20) != 0;
// Bytes 5-6: Aux voltage/temperature (10 mV or 0.01K units)
uint16_t aux = data[5] | (data[6] << 8);
if (aux < 3000) { // If < 30V, it's voltage
result.auxVoltage = aux * 0.01f;
result.temperature = 0;
} else { // Otherwise temperature in 0.01 Kelvin
result.temperature = (aux * 0.01f) - 273.15f;
result.auxVoltage = 0;
}
// Bytes 7-9: Battery current (22-bit signed, 1 mA units)
int32_t current = data[7] | (data[8] << 8) | ((data[9] & 0x3F) << 16);
if (current & 0x200000) { // Sign extend if negative
current |= 0xFFC00000;
}
result.current = current * 0.001f;
// Bytes 9-11: Consumed Ah (18-bit signed, 10 mAh units)
int32_t consumedAh = ((data[9] & 0xC0) >> 6) | (data[10] << 2) | ((data[11] & 0xFF) << 10);
if (consumedAh & 0x20000) { // Sign extend
consumedAh |= 0xFFFC0000;
}
result.consumedAh = consumedAh * 0.01f;
// Bytes 12-13: SOC (10 = 1.0%)
uint16_t soc = data[12] | ((data[13] & 0x03) << 8);
result.soc = soc * 0.1f;
debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " +
String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%");
return true;
}
// Parse Inverter data
bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& result) {
if (len < 10) {
debugPrint("Inverter data too short");
return false;
}
// Byte 0: Device state
result.state = data[0];
// Bytes 1-2: Battery voltage (10 mV units)
uint16_t vBat = data[1] | (data[2] << 8);
result.batteryVoltage = vBat * 0.01f;
// Bytes 3-4: Battery current (10 mA units, signed)
int16_t iBat = (int16_t)(data[3] | (data[4] << 8));
result.batteryCurrent = iBat * 0.01f;
// Bytes 5-7: AC Power (1 W units, signed 24-bit)
int32_t acPower = data[5] | (data[6] << 8) | (data[7] << 16);
if (acPower & 0x800000) { // Sign extend
acPower |= 0xFF000000;
}
result.acPower = acPower;
// Byte 8: Alarms
uint8_t alarms = data[8];
result.alarmLowVoltage = (alarms & 0x01) != 0;
result.alarmHighVoltage = (alarms & 0x02) != 0;
result.alarmHighTemperature = (alarms & 0x04) != 0;
result.alarmOverload = (alarms & 0x08) != 0;
debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " +
String(result.acPower) + "W, State: " + String(result.state));
return true;
}
// Parse DC-DC Converter data
bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result) {
if (len < 10) {
debugPrint("DC-DC converter data too short");
return false;
}
// Byte 0: Charge state
result.chargeState = data[0];
// Bytes 1-2: Input voltage (10 mV units)
uint16_t vIn = data[1] | (data[2] << 8);
result.inputVoltage = vIn * 0.01f;
// Bytes 3-4: Output voltage (10 mV units)
uint16_t vOut = data[3] | (data[4] << 8);
result.outputVoltage = vOut * 0.01f;
// Bytes 5-6: Output current (10 mA units)
uint16_t iOut = data[5] | (data[6] << 8);
result.outputCurrent = iOut * 0.01f;
// Byte 7: Error code
result.errorCode = data[7];
debugPrint("DC-DC Converter: In=" + String(result.inputVoltage, 2) + "V, Out=" +
String(result.outputVoltage, 2) + "V, " + String(result.outputCurrent, 2) + "A");
return true;
}
// Get data methods
bool VictronBLE::getSolarChargerData(String macAddress, SolarChargerData& data) {
String normalizedMAC = normalizeMAC(macAddress);
auto it = devices.find(normalizedMAC);
if (it != devices.end() && it->second->data &&
it->second->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
data = *(SolarChargerData*)it->second->data;
return data.dataValid;
}
return false;
}
bool VictronBLE::getBatteryMonitorData(String macAddress, BatteryMonitorData& data) {
String normalizedMAC = normalizeMAC(macAddress);
auto it = devices.find(normalizedMAC);
if (it != devices.end() && it->second->data &&
it->second->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) {
data = *(BatteryMonitorData*)it->second->data;
return data.dataValid;
}
return false;
}
bool VictronBLE::getInverterData(String macAddress, InverterData& data) {
String normalizedMAC = normalizeMAC(macAddress);
auto it = devices.find(normalizedMAC);
if (it != devices.end() && it->second->data &&
it->second->data->deviceType == DEVICE_TYPE_INVERTER) {
data = *(InverterData*)it->second->data;
return data.dataValid;
}
return false;
}
bool VictronBLE::getDCDCConverterData(String macAddress, DCDCConverterData& data) {
String normalizedMAC = normalizeMAC(macAddress);
auto it = devices.find(normalizedMAC);
if (it != devices.end() && it->second->data &&
it->second->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) {
data = *(DCDCConverterData*)it->second->data;
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;
}
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;
}
}
return true;
}
// Helper: MAC address to string
String VictronBLE::macAddressToString(BLEAddress address) {
char macStr[18];
snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
address.getNative()[0], address.getNative()[1],
address.getNative()[2], address.getNative()[3],
address.getNative()[4], address.getNative()[5]);
return String(macStr);
}
// Helper: Normalize MAC address format
String VictronBLE::normalizeMAC(String mac) {
String normalized = mac;
normalized.toLowerCase();
normalized.replace("-", ":");
return normalized;
}
// Debug helpers
void VictronBLE::debugPrint(const String& message) {
if (debugEnabled) {
Serial.println("[VictronBLE] " + message);
}
}
void VictronBLE::debugPrintHex(const char* label, const uint8_t* data, size_t len) {
if (!debugEnabled) return;
Serial.print("[VictronBLE] ");
Serial.print(label);
Serial.print(": ");
for (size_t i = 0; i < len; i++) {
if (data[i] < 0x10) Serial.print("0");
Serial.print(data[i], HEX);
Serial.print(" ");
}
Serial.println();
}

264
src/VictronBLE.h Normal file
View File

@@ -0,0 +1,264 @@
/**
* VictronBLE - ESP32 library for Victron Energy BLE devices
*
* Based on Victron's official BLE Advertising protocol documentation
* Inspired by hoberman's examples and keshavdv's Python library
*
* Copyright (c) 2024
* License: MIT
*/
#ifndef VICTRON_BLE_H
#define VICTRON_BLE_H
#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEAdvertisedDevice.h>
#include <BLEScan.h>
#include <map>
#include <vector>
#include "mbedtls/aes.h"
// Victron manufacturer ID
#define VICTRON_MANUFACTURER_ID 0x02E1
// Device type IDs from Victron protocol
enum VictronDeviceType {
DEVICE_TYPE_UNKNOWN = 0x00,
DEVICE_TYPE_SOLAR_CHARGER = 0x01,
DEVICE_TYPE_BATTERY_MONITOR = 0x02,
DEVICE_TYPE_INVERTER = 0x03,
DEVICE_TYPE_DCDC_CONVERTER = 0x04,
DEVICE_TYPE_SMART_LITHIUM = 0x05,
DEVICE_TYPE_INVERTER_RS = 0x06,
DEVICE_TYPE_SMART_BATTERY_PROTECT = 0x07,
DEVICE_TYPE_LYNX_SMART_BMS = 0x08,
DEVICE_TYPE_MULTI_RS = 0x09,
DEVICE_TYPE_VE_BUS = 0x0A,
DEVICE_TYPE_DC_ENERGY_METER = 0x0B
};
// Device state for Solar Charger
enum SolarChargerState {
CHARGER_OFF = 0,
CHARGER_LOW_POWER = 1,
CHARGER_FAULT = 2,
CHARGER_BULK = 3,
CHARGER_ABSORPTION = 4,
CHARGER_FLOAT = 5,
CHARGER_STORAGE = 6,
CHARGER_EQUALIZE = 7,
CHARGER_INVERTING = 9,
CHARGER_POWER_SUPPLY = 11,
CHARGER_EXTERNAL_CONTROL = 252
};
// Base structure for all device data
struct VictronDeviceData {
String deviceName;
String macAddress;
VictronDeviceType deviceType;
int8_t rssi;
uint32_t lastUpdate;
bool dataValid;
VictronDeviceData() : deviceType(DEVICE_TYPE_UNKNOWN), rssi(-100),
lastUpdate(0), dataValid(false) {}
};
// Solar Charger specific data
struct SolarChargerData : public VictronDeviceData {
SolarChargerState chargeState;
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 {
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 consumedAh; // Ah
float soc; // State of Charge %
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 {
float batteryVoltage; // V
float batteryCurrent; // A
float acPower; // W
uint8_t state; // Inverter state
bool alarmHighVoltage;
bool alarmLowVoltage;
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 {
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 declaration
class VictronBLE;
// Callback interface for device data updates
class VictronDeviceCallback {
public:
virtual ~VictronDeviceCallback() {}
virtual void onSolarChargerData(const SolarChargerData& data) {}
virtual void onBatteryMonitorData(const BatteryMonitorData& data) {}
virtual void onInverterData(const InverterData& data) {}
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(String n, String mac, 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(String name, String macAddress, String encryptionKey,
VictronDeviceType expectedType = DEVICE_TYPE_UNKNOWN);
// Remove a device
void removeDevice(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(String macAddress, SolarChargerData& data);
bool getBatteryMonitorData(String macAddress, BatteryMonitorData& data);
bool getInverterData(String macAddress, InverterData& data);
bool getDCDCConverterData(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;
}
};
std::map<String, DeviceInfo*> devices;
BLEScan* pBLEScan;
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(const uint8_t* manufacturerData, size_t len,
const String& macAddress);
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);
void debugPrintHex(const char* label, const uint8_t* data, size_t len);
String macAddressToString(BLEAddress address);
String normalizeMAC(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