Initial readme up
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal 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
277
QUICK_START.md
Normal 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
444
README.md
Normal 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/)
|
||||
38
examples/MultiDevice/platformio.ini
Normal file
38
examples/MultiDevice/platformio.ini
Normal 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
|
||||
224
examples/MultiDevice/src/main.cpp
Normal file
224
examples/MultiDevice/src/main.cpp
Normal 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
67
keywords.txt
Normal 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
29
library.json
Normal 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
11
library.properties
Normal 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
627
src/VictronBLE.cpp
Normal 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
264
src/VictronBLE.h
Normal 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
|
||||
Reference in New Issue
Block a user