Compare commits
13 Commits
v0.3.1
...
cd3b4626d1
| Author | SHA1 | Date | |
|---|---|---|---|
| cd3b4626d1 | |||
| 7706f88399 | |||
| e7024d9983 | |||
| 261cc0d1fe | |||
| 39a89c816c | |||
| 4944757903 | |||
| 31765c7ac8 | |||
| 84d153c9a8 | |||
| 8a4e010324 | |||
| 977641b093 | |||
| 24712c206a | |||
| 8a2402cb63 | |||
| a64fef899b |
@@ -102,3 +102,182 @@ Arduino/ESP32 library for reading Victron Energy devices via Bluetooth Low Energ
|
|||||||
- library.json
|
- library.json
|
||||||
- library.properties
|
- library.properties
|
||||||
|
|
||||||
|
|
||||||
|
### Session: 2026-02-12 18:23
|
||||||
|
**Commits:**
|
||||||
|
```
|
||||||
|
a843eb9 Keep v0.3.1
|
||||||
|
5a210fb Experimenting with a claude file and created new logging example
|
||||||
|
```
|
||||||
|
**Modified files:**
|
||||||
|
- .claude/CLAUDE.md
|
||||||
|
- README.md
|
||||||
|
- VERSIONS
|
||||||
|
- library.json
|
||||||
|
- library.properties
|
||||||
|
- src/VictronBLE.cpp
|
||||||
|
- src/VictronBLE.h
|
||||||
|
|
||||||
|
|
||||||
|
### Session: 2026-02-12 18:35
|
||||||
|
**Commits:**
|
||||||
|
```
|
||||||
|
a64fef8 New version with smaller memory footprint etc
|
||||||
|
a843eb9 Keep v0.3.1
|
||||||
|
5a210fb Experimenting with a claude file and created new logging example
|
||||||
|
```
|
||||||
|
**Modified files:**
|
||||||
|
- .claude/CLAUDE.md
|
||||||
|
- src/VictronBLE.cpp
|
||||||
|
- src/VictronBLE.h
|
||||||
|
|
||||||
|
|
||||||
|
### Session: 2026-02-13 11:02
|
||||||
|
**Modified files:**
|
||||||
|
- .claude/CLAUDE.md
|
||||||
|
- src/VictronBLE.cpp
|
||||||
|
- src/VictronBLE.h
|
||||||
|
|
||||||
|
|
||||||
|
### Session: 2026-02-15 18:59
|
||||||
|
**Modified files:**
|
||||||
|
- .claude/CLAUDE.md
|
||||||
|
- library.json
|
||||||
|
- src/VictronBLE.cpp
|
||||||
|
- src/VictronBLE.h
|
||||||
|
|
||||||
|
|
||||||
|
### Session: 2026-02-15 19:06
|
||||||
|
**Modified files:**
|
||||||
|
- .claude/CLAUDE.md
|
||||||
|
- library.json
|
||||||
|
- src/VictronBLE.cpp
|
||||||
|
- src/VictronBLE.h
|
||||||
|
|
||||||
|
|
||||||
|
### Session: 2026-02-15 19:10
|
||||||
|
**Modified files:**
|
||||||
|
- .claude/CLAUDE.md
|
||||||
|
- library.json
|
||||||
|
- src/VictronBLE.cpp
|
||||||
|
- src/VictronBLE.h
|
||||||
|
|
||||||
|
|
||||||
|
### Session: 2026-02-15 19:18
|
||||||
|
**Commits:**
|
||||||
|
```
|
||||||
|
8a2402c Repeater and Test code for ESP Now
|
||||||
|
```
|
||||||
|
**Modified files:**
|
||||||
|
- .claude/CLAUDE.md
|
||||||
|
- examples/FakeRepeater/platformio.ini
|
||||||
|
- examples/FakeRepeater/src/main.cpp
|
||||||
|
- examples/Receiver/platformio.ini
|
||||||
|
- examples/Receiver/src/main.cpp
|
||||||
|
- examples/Repeater/platformio.ini
|
||||||
|
- examples/Repeater/src/main.cpp
|
||||||
|
- library.json
|
||||||
|
|
||||||
|
|
||||||
|
### Session: 2026-02-15 19:20
|
||||||
|
**Commits:**
|
||||||
|
```
|
||||||
|
24712c2 Work on receiver and sender
|
||||||
|
8a2402c Repeater and Test code for ESP Now
|
||||||
|
```
|
||||||
|
**Modified files:**
|
||||||
|
- .claude/CLAUDE.md
|
||||||
|
- examples/Receiver/platformio.ini
|
||||||
|
- examples/Receiver/src/main.cpp
|
||||||
|
- examples/Repeater/src/main.cpp
|
||||||
|
|
||||||
|
|
||||||
|
### Session: 2026-02-28 12:26
|
||||||
|
**Modified files:**
|
||||||
|
- .claude/CLAUDE.md
|
||||||
|
- examples/Receiver/src/main.cpp
|
||||||
|
- examples/Repeater/src/main.cpp
|
||||||
|
|
||||||
|
|
||||||
|
### Session: 2026-02-28 14:32
|
||||||
|
**Commits:**
|
||||||
|
```
|
||||||
|
4944757 Fix to be non blocking without tasks
|
||||||
|
31765c7 Update notes
|
||||||
|
84d153c Single callback version - vastly simplified.
|
||||||
|
```
|
||||||
|
**Modified files:**
|
||||||
|
- examples/Logger/src/main.cpp
|
||||||
|
- examples/MultiDevice/src/main.cpp
|
||||||
|
- examples/Repeater/src/main.cpp
|
||||||
|
- library.json
|
||||||
|
- library.properties
|
||||||
|
- src/VictronBLE.cpp
|
||||||
|
- src/VictronBLE.h
|
||||||
|
|
||||||
|
|
||||||
|
### Session: 2026-02-28 14:33
|
||||||
|
**Commits:**
|
||||||
|
```
|
||||||
|
4944757 Fix to be non blocking without tasks
|
||||||
|
31765c7 Update notes
|
||||||
|
84d153c Single callback version - vastly simplified.
|
||||||
|
```
|
||||||
|
**Modified files:**
|
||||||
|
- .claude/CLAUDE.md
|
||||||
|
- VERSIONS
|
||||||
|
- examples/Logger/src/main.cpp
|
||||||
|
- examples/MultiDevice/src/main.cpp
|
||||||
|
- examples/Repeater/src/main.cpp
|
||||||
|
- library.json
|
||||||
|
- library.properties
|
||||||
|
- src/VictronBLE.cpp
|
||||||
|
- src/VictronBLE.h
|
||||||
|
|
||||||
|
|
||||||
|
### Session: 2026-02-28 14:36
|
||||||
|
**Commits:**
|
||||||
|
```
|
||||||
|
4944757 Fix to be non blocking without tasks
|
||||||
|
31765c7 Update notes
|
||||||
|
```
|
||||||
|
**Modified files:**
|
||||||
|
- .claude/CLAUDE.md
|
||||||
|
- VERSIONS
|
||||||
|
- examples/Logger/src/main.cpp
|
||||||
|
- examples/MultiDevice/src/main.cpp
|
||||||
|
- examples/Repeater/src/main.cpp
|
||||||
|
- library.json
|
||||||
|
- library.properties
|
||||||
|
- src/VictronBLE.cpp
|
||||||
|
- src/VictronBLE.h
|
||||||
|
|
||||||
|
|
||||||
|
### Session: 2026-02-28 14:40
|
||||||
|
**Commits:**
|
||||||
|
```
|
||||||
|
39a89c8 Versions v0.4 ready for release
|
||||||
|
4944757 Fix to be non blocking without tasks
|
||||||
|
31765c7 Update notes
|
||||||
|
```
|
||||||
|
**Modified files:**
|
||||||
|
- .claude/CLAUDE.md
|
||||||
|
- README.md
|
||||||
|
- VERSIONS
|
||||||
|
- library.json
|
||||||
|
- library.properties
|
||||||
|
|
||||||
|
|
||||||
|
### Session: 2026-02-28 14:48
|
||||||
|
**Commits:**
|
||||||
|
```
|
||||||
|
261cc0d Improve readme ready for v0.4 release
|
||||||
|
39a89c8 Versions v0.4 ready for release
|
||||||
|
4944757 Fix to be non blocking without tasks
|
||||||
|
31765c7 Update notes
|
||||||
|
```
|
||||||
|
**Modified files:**
|
||||||
|
- .claude/CLAUDE.md
|
||||||
|
- README.md
|
||||||
|
- REVIEW.md
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ compile_commands.json
|
|||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.history/
|
.history/
|
||||||
|
|
||||||
|
bugs
|
||||||
|
|
||||||
# Build artifacts
|
# Build artifacts
|
||||||
*.o
|
*.o
|
||||||
*.a
|
*.a
|
||||||
|
|||||||
@@ -2,16 +2,15 @@
|
|||||||
|
|
||||||
ESP32 library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements.
|
ESP32 library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements.
|
||||||
|
|
||||||
**⚠️ INITIAL RELEASE - LIMITED TESTING DONE**
|
**⚠️ API CHANGE in v0.4 — not backwards compatible with v0.3.x**
|
||||||
|
|
||||||
This is an initial release (v0.3.1) and has been tested with MPPT on an ESP32-S3 and ESP32-C3.
|
v0.4 is a major rework of the library internals: new callback API, reduced memory usage, non-blocking scanning. See [VERSIONS](VERSIONS) for full details. A stable **v1.0** release with a consistent, long-term API is coming soon.
|
||||||
Use with caution and please report any issues you encounter. Testing and feedback are greatly appreciated!
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
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.
|
Why another library? Most of the Victron BLE examples are built into other frameworks (e.g. ESPHome) and I want a library that can be used in all ESP32 systems, including ESPHome or other frameworks. With long term plan to try and move others to this library and improve code with many eyes.
|
||||||
|
|
||||||
Currently supportin ESP32 S and C series (tested on older ESP32, and ESP32-S3 and ESP32-C3). Other chipsets can be added with abstraction of Bluetooth code.
|
Currently supporting ESP32 S and C series (tested on older ESP32, ESP32-S3 and ESP32-C3). Other chipsets can be added with abstraction of Bluetooth code.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -30,6 +29,7 @@ Currently supportin ESP32 S and C series (tested on older ESP32, and ESP32-S3 an
|
|||||||
- **Battery Monitors**: SmartShunt, BMV-712 Smart, BMV-700 series
|
- **Battery Monitors**: SmartShunt, BMV-712 Smart, BMV-700 series
|
||||||
- **Inverters**: MultiPlus, Quattro, Phoenix (with VE.Bus BLE dongle)
|
- **Inverters**: MultiPlus, Quattro, Phoenix (with VE.Bus BLE dongle)
|
||||||
- **DC-DC Converters**: Orion Smart, Orion XS
|
- **DC-DC Converters**: Orion Smart, Orion XS
|
||||||
|
- **AC Chargers**: Blue Smart IP22 / IP65 / IP67 chargers
|
||||||
- **Others**: Smart Battery Protect, Lynx Smart BMS, Smart Lithium batteries
|
- **Others**: Smart Battery Protect, Lynx Smart BMS, Smart Lithium batteries
|
||||||
|
|
||||||
## Hardware Requirements
|
## Hardware Requirements
|
||||||
@@ -41,16 +41,23 @@ Currently supportin ESP32 S and C series (tested on older ESP32, and ESP32-S3 an
|
|||||||
|
|
||||||
### PlatformIO
|
### PlatformIO
|
||||||
|
|
||||||
1. Add to `platformio.ini`:
|
1. Add to `platformio.ini` (recommended — installs from the PlatformIO registry):
|
||||||
```ini
|
```ini
|
||||||
lib_deps =
|
lib_deps =
|
||||||
https://gitea.sh3d.com.au/Sh3d/VictronBLE
|
scottp/victronble
|
||||||
|
```
|
||||||
|
|
||||||
|
Or install directly from git. Note the **`.git` suffix is required** — the bare
|
||||||
|
repository URL is not accepted by PlatformIO:
|
||||||
|
```ini
|
||||||
|
lib_deps =
|
||||||
|
https://gitea.sh3d.com.au/Sh3d/VictronBLE.git
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Or clone into your project's `lib` folder:
|
2. Or clone into your project's `lib` folder:
|
||||||
```bash
|
```bash
|
||||||
cd lib
|
cd lib
|
||||||
git clone https://gitea.sh3d.com.au/Sh3d/VictronBLE
|
git clone https://gitea.sh3d.com.au/Sh3d/VictronBLE.git
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arduino IDE
|
### Arduino IDE
|
||||||
@@ -81,38 +88,34 @@ Use the VictronConnect app to get your device's encryption key:
|
|||||||
|
|
||||||
VictronBLE victron;
|
VictronBLE victron;
|
||||||
|
|
||||||
// Callback for data updates
|
// Callback — receives a VictronDevice*, switch on deviceType
|
||||||
class MyCallback : public VictronDeviceCallback {
|
void onVictronData(const VictronDevice* dev) {
|
||||||
public:
|
if (dev->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
|
||||||
void onSolarChargerData(const SolarChargerData& data) override {
|
Serial.printf("Solar %s: %.2fV %.2fA %dW\n",
|
||||||
Serial.printf("Solar: %.2fV, %.2fA, %dW\n",
|
dev->name,
|
||||||
data.batteryVoltage,
|
dev->solar.batteryVoltage,
|
||||||
data.batteryCurrent,
|
dev->solar.batteryCurrent,
|
||||||
data.panelPower);
|
(int)dev->solar.panelPower);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
MyCallback callback;
|
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
|
|
||||||
// Initialize library
|
|
||||||
victron.begin(5); // 5 second scan duration
|
victron.begin(5); // 5 second scan duration
|
||||||
victron.setCallback(&callback);
|
victron.setCallback(onVictronData);
|
||||||
|
|
||||||
// Add your device (replace with your MAC and key)
|
// Add your device (replace with your MAC and key)
|
||||||
victron.addDevice(
|
victron.addDevice(
|
||||||
"My MPPT", // Name
|
"My MPPT", // Name
|
||||||
"AA:BB:CC:DD:EE:FF", // MAC address
|
"AA:BB:CC:DD:EE:FF", // MAC address
|
||||||
"0123456789abcdef0123456789abcdef", // Encryption key
|
"0123456789abcdef0123456789abcdef", // Encryption key
|
||||||
DEVICE_TYPE_SOLAR_CHARGER // Device type
|
DEVICE_TYPE_SOLAR_CHARGER // Device type (optional, auto-detected)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
victron.loop();
|
victron.loop(); // Non-blocking, returns immediately
|
||||||
delay(100);
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -125,130 +128,113 @@ void loop() {
|
|||||||
```cpp
|
```cpp
|
||||||
bool begin(uint32_t scanDuration = 5);
|
bool begin(uint32_t scanDuration = 5);
|
||||||
```
|
```
|
||||||
Initialize BLE and start scanning. Returns `true` on success.
|
Initialize BLE scanning. Returns `true` on success.
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
- `scanDuration`: BLE scan duration in seconds (default: 5)
|
- `scanDuration`: BLE scan window in seconds (default: 5)
|
||||||
|
|
||||||
#### Device Management
|
#### Device Management
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
bool addDevice(String name, String macAddress, String encryptionKey,
|
bool addDevice(const char* name, const char* mac, const char* hexKey,
|
||||||
VictronDeviceType expectedType = DEVICE_TYPE_UNKNOWN);
|
VictronDeviceType type = DEVICE_TYPE_UNKNOWN);
|
||||||
```
|
```
|
||||||
Add a device to monitor.
|
Add a device to monitor (max 8 devices).
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
- `name`: Friendly name for the device
|
- `name`: Friendly name for the device
|
||||||
- `macAddress`: Device MAC address (format: "AA:BB:CC:DD:EE:FF")
|
- `mac`: Device MAC address (format: `"AA:BB:CC:DD:EE:FF"` or `"aabbccddeeff"`)
|
||||||
- `encryptionKey`: 32-character hex encryption key from VictronConnect
|
- `hexKey`: 32-character hex encryption key from VictronConnect
|
||||||
- `expectedType`: Device type (optional, for validation)
|
- `type`: Device type (optional, auto-detected from BLE advertisement)
|
||||||
|
|
||||||
**Returns:** `true` on success
|
**Returns:** `true` on success
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
void removeDevice(String macAddress);
|
size_t getDeviceCount() const;
|
||||||
```
|
|
||||||
Remove a device from monitoring.
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
size_t getDeviceCount();
|
|
||||||
```
|
```
|
||||||
Get the number of configured devices.
|
Get the number of configured devices.
|
||||||
|
|
||||||
#### Data Access
|
#### Callback
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
bool getSolarChargerData(String macAddress, SolarChargerData& data);
|
void setCallback(VictronCallback cb);
|
||||||
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.
|
Set a function pointer callback. Called when new data arrives from a device. The callback receives a `const VictronDevice*` — switch on `deviceType` to access the appropriate data union member.
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
std::vector<String> getDevicesByType(VictronDeviceType type);
|
typedef void (*VictronCallback)(const VictronDevice* device);
|
||||||
```
|
```
|
||||||
Get MAC addresses of all devices of a specific type.
|
|
||||||
|
|
||||||
#### Callbacks
|
#### Configuration
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
void setCallback(VictronDeviceCallback* callback);
|
void setMinInterval(uint32_t ms);
|
||||||
```
|
```
|
||||||
Set callback object to receive data updates automatically.
|
Set minimum callback interval per device (default: 1000ms). Callbacks are also suppressed when the device nonce hasn't changed (data unchanged).
|
||||||
|
|
||||||
#### Utilities
|
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
void setDebug(bool enable);
|
void setDebug(bool enable);
|
||||||
```
|
```
|
||||||
Enable/disable debug output to Serial.
|
Enable/disable debug output to Serial.
|
||||||
|
|
||||||
```cpp
|
#### Main Loop
|
||||||
String getLastError();
|
|
||||||
```
|
|
||||||
Get last error message.
|
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
void loop();
|
void loop();
|
||||||
```
|
```
|
||||||
Process BLE scanning and data updates. Call this in your main loop.
|
Call in your main loop. Non-blocking — returns immediately if a scan is already running. Scan restarts automatically when it completes.
|
||||||
|
|
||||||
### Data Structures
|
### Data Structures
|
||||||
|
|
||||||
#### SolarChargerData
|
#### VictronDevice (main struct)
|
||||||
|
|
||||||
|
All device types share this struct. Access type-specific data via the union member matching `deviceType`.
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
struct SolarChargerData {
|
struct VictronDevice {
|
||||||
String deviceName;
|
char name[32];
|
||||||
String macAddress;
|
char mac[13]; // 12 hex chars + null
|
||||||
|
VictronDeviceType deviceType;
|
||||||
int8_t rssi; // Signal strength (dBm)
|
int8_t rssi; // Signal strength (dBm)
|
||||||
uint32_t lastUpdate; // millis() of last update
|
uint32_t lastUpdate; // millis() of last update
|
||||||
bool dataValid; // Data validity flag
|
bool dataValid;
|
||||||
|
union {
|
||||||
|
VictronSolarData solar;
|
||||||
|
VictronBatteryData battery;
|
||||||
|
VictronInverterData inverter;
|
||||||
|
VictronDCDCData dcdc;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
SolarChargerState chargeState; // Charging state
|
#### VictronSolarData
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct VictronSolarData {
|
||||||
|
uint8_t chargeState; // SolarChargerState enum
|
||||||
|
uint8_t errorCode;
|
||||||
float batteryVoltage; // V
|
float batteryVoltage; // V
|
||||||
float batteryCurrent; // A
|
float batteryCurrent; // A
|
||||||
float panelVoltage; // V (calculated)
|
|
||||||
float panelPower; // W
|
float panelPower; // W
|
||||||
uint16_t yieldToday; // Wh
|
uint16_t yieldToday; // Wh
|
||||||
float loadCurrent; // A (if load output present)
|
float loadCurrent; // A (if load output present)
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
**Charge States:**
|
**Charge States** (`chargeState` values):
|
||||||
- `CHARGER_OFF` - Off
|
`CHARGER_OFF`, `CHARGER_LOW_POWER`, `CHARGER_FAULT`, `CHARGER_BULK`, `CHARGER_ABSORPTION`, `CHARGER_FLOAT`, `CHARGER_STORAGE`, `CHARGER_EQUALIZE`, `CHARGER_INVERTING`, `CHARGER_POWER_SUPPLY`, `CHARGER_EXTERNAL_CONTROL`
|
||||||
- `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
|
#### VictronBatteryData
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
struct BatteryMonitorData {
|
struct VictronBatteryData {
|
||||||
String deviceName;
|
|
||||||
String macAddress;
|
|
||||||
int8_t rssi;
|
|
||||||
uint32_t lastUpdate;
|
|
||||||
bool dataValid;
|
|
||||||
|
|
||||||
float voltage; // V
|
float voltage; // V
|
||||||
float current; // A (+ charging, - discharging)
|
float current; // A (+ charging, - discharging)
|
||||||
float temperature; // °C (if configured)
|
float temperature; // C (0 if aux is voltage)
|
||||||
float auxVoltage; // V (starter battery/midpoint)
|
float auxVoltage; // V (0 if aux is temperature)
|
||||||
uint16_t remainingMinutes; // Time remaining
|
uint16_t remainingMinutes;
|
||||||
float consumedAh; // Ah consumed
|
float consumedAh; // Ah
|
||||||
float soc; // State of charge %
|
float soc; // State of charge %
|
||||||
|
|
||||||
// Alarms
|
|
||||||
bool alarmLowVoltage;
|
bool alarmLowVoltage;
|
||||||
bool alarmHighVoltage;
|
bool alarmHighVoltage;
|
||||||
bool alarmLowSOC;
|
bool alarmLowSOC;
|
||||||
@@ -257,39 +243,25 @@ struct BatteryMonitorData {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
#### InverterData
|
#### VictronInverterData
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
struct InverterData {
|
struct VictronInverterData {
|
||||||
String deviceName;
|
|
||||||
String macAddress;
|
|
||||||
int8_t rssi;
|
|
||||||
uint32_t lastUpdate;
|
|
||||||
bool dataValid;
|
|
||||||
|
|
||||||
float batteryVoltage; // V
|
float batteryVoltage; // V
|
||||||
float batteryCurrent; // A
|
float batteryCurrent; // A
|
||||||
float acPower; // W (+ inverting, - charging)
|
float acPower; // W (+ inverting, - charging)
|
||||||
uint8_t state; // Inverter state
|
uint8_t state;
|
||||||
|
|
||||||
// Alarms
|
|
||||||
bool alarmHighVoltage;
|
|
||||||
bool alarmLowVoltage;
|
bool alarmLowVoltage;
|
||||||
|
bool alarmHighVoltage;
|
||||||
bool alarmHighTemperature;
|
bool alarmHighTemperature;
|
||||||
bool alarmOverload;
|
bool alarmOverload;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
#### DCDCConverterData
|
#### VictronDCDCData
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
struct DCDCConverterData {
|
struct VictronDCDCData {
|
||||||
String deviceName;
|
|
||||||
String macAddress;
|
|
||||||
int8_t rssi;
|
|
||||||
uint32_t lastUpdate;
|
|
||||||
bool dataValid;
|
|
||||||
|
|
||||||
float inputVoltage; // V
|
float inputVoltage; // V
|
||||||
float outputVoltage; // V
|
float outputVoltage; // V
|
||||||
float outputCurrent; // A
|
float outputCurrent; // A
|
||||||
@@ -298,6 +270,23 @@ struct DCDCConverterData {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### VictronACChargerData
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct VictronACChargerData {
|
||||||
|
uint8_t chargeState; // SolarChargerState enum (shared charger states)
|
||||||
|
uint8_t errorCode;
|
||||||
|
float voltage1; // V (output 1)
|
||||||
|
float current1; // A (output 1)
|
||||||
|
float voltage2; // V (output 2, 0 if absent)
|
||||||
|
float current2; // A (output 2, 0 if absent)
|
||||||
|
float voltage3; // V (output 3, 0 if absent)
|
||||||
|
float current3; // A (output 3, 0 if absent)
|
||||||
|
float temperature; // C (0 if not available)
|
||||||
|
float acCurrent; // A (0 if not available)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
## Advanced Usage
|
## Advanced Usage
|
||||||
|
|
||||||
### Multiple Devices
|
### Multiple Devices
|
||||||
@@ -305,78 +294,57 @@ struct DCDCConverterData {
|
|||||||
```cpp
|
```cpp
|
||||||
void setup() {
|
void setup() {
|
||||||
victron.begin(5);
|
victron.begin(5);
|
||||||
victron.setCallback(&callback);
|
victron.setCallback(onVictronData);
|
||||||
|
|
||||||
// Add multiple devices
|
// Add multiple devices (type is auto-detected from BLE advertisements)
|
||||||
victron.addDevice("MPPT 1", "AA:BB:CC:DD:EE:01", "key1...", DEVICE_TYPE_SOLAR_CHARGER);
|
victron.addDevice("MPPT 1", "AA:BB:CC:DD:EE:01", "key1...");
|
||||||
victron.addDevice("MPPT 2", "AA:BB:CC:DD:EE:02", "key2...", DEVICE_TYPE_SOLAR_CHARGER);
|
victron.addDevice("MPPT 2", "AA:BB:CC:DD:EE:02", "key2...");
|
||||||
victron.addDevice("SmartShunt", "AA:BB:CC:DD:EE:03", "key3...", DEVICE_TYPE_BATTERY_MONITOR);
|
victron.addDevice("SmartShunt", "AA:BB:CC:DD:EE:03", "key3...");
|
||||||
victron.addDevice("Inverter", "AA:BB:CC:DD:EE:04", "key4...", DEVICE_TYPE_INVERTER);
|
victron.addDevice("Inverter", "AA:BB:CC:DD:EE:04", "key4...");
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manual Data Polling
|
### Handling Multiple Device Types
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
void loop() {
|
void onVictronData(const VictronDevice* dev) {
|
||||||
victron.loop();
|
switch (dev->deviceType) {
|
||||||
|
case DEVICE_TYPE_SOLAR_CHARGER:
|
||||||
// Query specific device
|
Serial.printf("%s: %.2fV %dW\n", dev->name,
|
||||||
SolarChargerData mpptData;
|
dev->solar.batteryVoltage, (int)dev->solar.panelPower);
|
||||||
if (victron.getSolarChargerData("AA:BB:CC:DD:EE:FF", mpptData)) {
|
break;
|
||||||
if (mpptData.dataValid) {
|
case DEVICE_TYPE_BATTERY_MONITOR:
|
||||||
// Use data
|
Serial.printf("%s: %.2fV %.1f%%\n", dev->name,
|
||||||
float power = mpptData.panelPower;
|
dev->battery.voltage, dev->battery.soc);
|
||||||
|
break;
|
||||||
|
case DEVICE_TYPE_INVERTER:
|
||||||
|
Serial.printf("%s: %dW\n", dev->name, (int)dev->inverter.acPower);
|
||||||
|
break;
|
||||||
|
case DEVICE_TYPE_DCDC_CONVERTER:
|
||||||
|
Serial.printf("%s: %.2fV -> %.2fV\n", dev->name,
|
||||||
|
dev->dcdc.inputVoltage, dev->dcdc.outputVoltage);
|
||||||
|
break;
|
||||||
|
case DEVICE_TYPE_AC_CHARGER:
|
||||||
|
Serial.printf("%s: %.2fV %.2fA %.0fC\n", dev->name,
|
||||||
|
dev->acCharger.voltage1, dev->acCharger.current1,
|
||||||
|
dev->acCharger.temperature);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delay(1000);
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Find All Devices of a Type
|
### Callback Throttling
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
void loop() {
|
void setup() {
|
||||||
victron.loop();
|
victron.begin(5);
|
||||||
|
victron.setCallback(onVictronData);
|
||||||
|
victron.setMinInterval(2000); // Callback at most every 2 seconds per device
|
||||||
|
|
||||||
// Get all solar chargers
|
// ...
|
||||||
std::vector<String> mppts = victron.getDevicesByType(DEVICE_TYPE_SOLAR_CHARGER);
|
|
||||||
|
|
||||||
for (const String& mac : mppts) {
|
|
||||||
SolarChargerData data;
|
|
||||||
if (victron.getSolarChargerData(mac, data)) {
|
|
||||||
Serial.println(data.deviceName + ": " + String(data.panelPower) + "W");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
delay(5000);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Callback Interface
|
|
||||||
|
|
||||||
Implement `VictronDeviceCallback` to receive automatic updates:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
class MyCallback : public VictronDeviceCallback {
|
|
||||||
public:
|
|
||||||
void onSolarChargerData(const SolarChargerData& data) override {
|
|
||||||
// Handle solar charger update
|
|
||||||
}
|
|
||||||
|
|
||||||
void onBatteryMonitorData(const BatteryMonitorData& data) override {
|
|
||||||
// Handle battery monitor update
|
|
||||||
}
|
|
||||||
|
|
||||||
void onInverterData(const InverterData& data) override {
|
|
||||||
// Handle inverter update
|
|
||||||
}
|
|
||||||
|
|
||||||
void onDCDCConverterData(const DCDCConverterData& data) override {
|
|
||||||
// Handle DC-DC converter update
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
@@ -418,7 +386,10 @@ Based on official [Victron BLE documentation](https://www.victronenergy.com/live
|
|||||||
See the `examples/` directory for:
|
See the `examples/` directory for:
|
||||||
|
|
||||||
- **MultiDevice**: Monitor multiple devices with callbacks
|
- **MultiDevice**: Monitor multiple devices with callbacks
|
||||||
- More examples coming soon!
|
- **Logger**: Change-detection logging for Solar Charger data
|
||||||
|
- **Repeater**: Collect BLE data and re-transmit via ESPNow broadcast
|
||||||
|
- **Receiver**: Receive ESPNow packets from a Repeater and display data
|
||||||
|
- **FakeRepeater**: Generate test ESPNow packets without real Victron hardware
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@@ -456,7 +427,9 @@ See [VERSIONS](VERSIONS) file for detailed changelog and release history.
|
|||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
- 📫 Report issues on GitHub
|
- 📫 Report issues: the Gitea instance does not currently allow public sign-ups, so
|
||||||
|
email <scottp@dd.com.au> with bug reports, device decode problems, or new device
|
||||||
|
requests (debug log output is very helpful).
|
||||||
- 📖 Check the examples directory
|
- 📖 Check the examples directory
|
||||||
- 🔧 Enable debug mode for diagnostics
|
- 🔧 Enable debug mode for diagnostics
|
||||||
- 📚 See [Victron documentation](https://www.victronenergy.com/live/)
|
- 📚 See [Victron documentation](https://www.victronenergy.com/live/)
|
||||||
|
|||||||
@@ -0,0 +1,524 @@
|
|||||||
|
# VictronBLE Code Review
|
||||||
|
|
||||||
|
## Part 1: Bug Fixes, Efficiency & Simplification ✅ COMPLETE (v0.4.1)
|
||||||
|
|
||||||
|
### Bugs
|
||||||
|
|
||||||
|
**1. Missing virtual destructor on `VictronDeviceData` (CRITICAL)**
|
||||||
|
`VictronBLE.h:123` - The base struct has no virtual destructor, but derived objects (`SolarChargerData`, etc.) are deleted through base pointers at `VictronBLE.cpp:287` (`delete data`). This is **undefined behavior** in C++. The derived destructors (which must clean up the `String` members they inherit) may never run.
|
||||||
|
|
||||||
|
Fix: Add `virtual ~VictronDeviceData() {}` — or better, eliminate the inheritance (see simplification below).
|
||||||
|
|
||||||
|
**2. `nullPad` field in `victronManufacturerData` is wrong**
|
||||||
|
`VictronBLE.h:68` - Comment says "extra byte because toCharArray() adds a \0 byte" but the code uses `std::string::copy()` which does NOT null-terminate. This makes the struct 1 byte too large, which is harmless but misleading. If the BLE stack ever returns exactly `sizeof(victronManufacturerData)` bytes, the copy would read past the source buffer.
|
||||||
|
|
||||||
|
Fix: Remove the `nullPad` field.
|
||||||
|
|
||||||
|
**3. `panelVoltage` calculation is unreliable**
|
||||||
|
`VictronBLE.cpp:371-376` - PV voltage is computed as `panelPower / batteryCurrent`. On an MPPT charger, battery current and PV current are different (that's the whole point of MPPT). This gives a meaningless number. The BLE protocol doesn't transmit PV voltage for solar chargers.
|
||||||
|
|
||||||
|
Fix: Remove `panelVoltage` from `SolarChargerData`. It's not in the protocol and the calculation is wrong.
|
||||||
|
|
||||||
|
**4. Aux data voltage/temperature heuristic is fragile**
|
||||||
|
`VictronBLE.cpp:410` - `if (payload->auxData < 3000)` is used to distinguish voltage from temperature. The Victron protocol actually uses a bit flag (bit 15 of the aux field, or the record subtype) to indicate which type of aux input is connected. The magic number 3000 will misclassify edge cases.
|
||||||
|
|
||||||
|
Fix: Use the proper protocol flag if available, or document this as a known limitation.
|
||||||
|
|
||||||
|
### Efficiency Improvements
|
||||||
|
|
||||||
|
**5. `hexStringToBytes` allocates 16 String objects**
|
||||||
|
`VictronBLE.cpp:610-611` - For each byte, `hex.substring()` creates a new heap-allocated `String`. On ESP32, this fragments the heap unnecessarily.
|
||||||
|
|
||||||
|
Fix: Direct char-to-nibble conversion:
|
||||||
|
```cpp
|
||||||
|
bool hexStringToBytes(const char* hex, uint8_t* bytes, size_t len) {
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
uint8_t hi = hex[i*2], lo = hex[i*2+1];
|
||||||
|
hi = (hi >= 'a') ? hi - 'a' + 10 : (hi >= 'A') ? hi - 'A' + 10 : hi - '0';
|
||||||
|
lo = (lo >= 'a') ? lo - 'a' + 10 : (lo >= 'A') ? lo - 'A' + 10 : lo - '0';
|
||||||
|
if (hi > 15 || lo > 15) return false;
|
||||||
|
bytes[i] = (hi << 4) | lo;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**6. MAC normalization on every lookup is wasteful**
|
||||||
|
`normalizeMAC()` is called in `processDevice()` for every BLE advertisement received (could be hundreds per scan), plus in every `getSolarChargerData()` / `getBatteryMonitorData()` call. Each call creates a new String and does 3 replace operations.
|
||||||
|
|
||||||
|
Fix: Normalize once at `addDevice()` time and store as a fixed `char[13]` (12 hex chars + null). Use `memcmp` or `strcmp` for comparison.
|
||||||
|
|
||||||
|
**7. `std::map<String, DeviceInfo*>` is heavy**
|
||||||
|
A typical setup monitors 1-4 devices. `std::map` has significant overhead (red-black tree, heap allocations for nodes). A simple fixed-size array with linear search would be faster and use less memory.
|
||||||
|
|
||||||
|
Fix: Replace with `DeviceInfo devices[MAX_DEVICES]` (where MAX_DEVICES = 8 or similar) and a `uint8_t deviceCount`.
|
||||||
|
|
||||||
|
**8. `loop()` blocks for entire scan duration**
|
||||||
|
`VictronBLE.cpp:140` - `pBLEScan->start(scanDuration, false)` is blocking. With the default 5-second scan duration, `loop()` blocks for 5 seconds every call.
|
||||||
|
|
||||||
|
Fix: Use `pBLEScan->start(0)` for continuous non-blocking scan, or use the async scan API. Data arrives via callbacks anyway.
|
||||||
|
|
||||||
|
### Simplification — Things to Remove
|
||||||
|
|
||||||
|
**9. Remove `VictronDeviceConfig` struct**
|
||||||
|
Only used as a parameter to `addDevice`. The convenience overload `addDevice(name, mac, key, type)` is what all examples use. The config struct adds an unnecessary layer.
|
||||||
|
|
||||||
|
**10. Remove `lastError` / `getLastError()`**
|
||||||
|
Uses heap-allocated String. If `debugEnabled` is true, errors already go to Serial. If debug is off, nobody calls `getLastError()` — none of the examples use it. Remove entirely.
|
||||||
|
|
||||||
|
**11. Remove `getDevicesByType()`**
|
||||||
|
No examples use it. Returns `std::vector<String>` which heap-allocates. Users already know their device MACs since they registered them.
|
||||||
|
|
||||||
|
**12. Remove `removeDevice()`**
|
||||||
|
No examples use it. In a typical embedded deployment, devices are configured once at startup and never removed.
|
||||||
|
|
||||||
|
**13. Remove the per-type getter methods**
|
||||||
|
`getSolarChargerData()`, `getBatteryMonitorData()`, etc. are polling-style accessors. All examples use the callback pattern instead. The getters copy the entire data struct (including Strings) which is expensive. If needed, a single `getData(mac, type)` returning a pointer would suffice.
|
||||||
|
|
||||||
|
**14. Flatten the inheritance hierarchy**
|
||||||
|
`VictronDeviceData` → `SolarChargerData` etc. uses inheritance + dynamic allocation + virtual dispatch (needed once we add virtual destructor). Since each device type is always accessed through its specific type, a tagged union or flat struct per type would be simpler:
|
||||||
|
```cpp
|
||||||
|
struct VictronDevice {
|
||||||
|
char mac[13];
|
||||||
|
char name[32];
|
||||||
|
uint8_t deviceType;
|
||||||
|
int8_t rssi;
|
||||||
|
uint32_t lastUpdate;
|
||||||
|
bool dataValid;
|
||||||
|
union {
|
||||||
|
struct { /* solar fields */ } solar;
|
||||||
|
struct { /* battery fields */ } battery;
|
||||||
|
struct { /* inverter fields */ } inverter;
|
||||||
|
struct { /* dcdc fields */ } dcdc;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
This eliminates heap allocation, virtual dispatch, and the `createDeviceData` factory.
|
||||||
|
|
||||||
|
**15. Replace virtual callback class with function pointer**
|
||||||
|
`VictronDeviceCallback` with 4 virtual methods → a single function pointer:
|
||||||
|
```cpp
|
||||||
|
typedef void (*VictronCallback)(const VictronDevice* device);
|
||||||
|
```
|
||||||
|
The callback receives the device and can switch on `deviceType`. Simpler, no vtable overhead, compatible with C.
|
||||||
|
|
||||||
|
**16. Remove `String` usage throughout**
|
||||||
|
Arduino `String` uses heap allocation and causes fragmentation. MAC addresses are always 12 hex chars. Device names can use fixed `char[]`. This is the single biggest simplification and memory improvement.
|
||||||
|
|
||||||
|
### Summary of Simplified API
|
||||||
|
|
||||||
|
After removing the above, the public API would be approximately:
|
||||||
|
```cpp
|
||||||
|
void victron_init(uint32_t scanDuration);
|
||||||
|
bool victron_add_device(const char* name, const char* mac, const char* hexKey, uint8_t type);
|
||||||
|
void victron_set_callback(VictronCallback cb);
|
||||||
|
void victron_loop();
|
||||||
|
```
|
||||||
|
~4 functions instead of ~15 methods.
|
||||||
|
|
||||||
|
|
||||||
|
All items implemented in v0.4.1. See [VERSIONS](VERSIONS) for full changelog.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 2: Multi-Platform BLE Support
|
||||||
|
|
||||||
|
### Recommended Test Hardware
|
||||||
|
|
||||||
|
Two cheap BLE development boards for testing the platform abstraction:
|
||||||
|
|
||||||
|
**1. Seeed XIAO nRF52840 (~$10 USD)**
|
||||||
|
- Nordic nRF52840 SoC, Bluetooth 5.0, onboard antenna
|
||||||
|
- Arduino-compatible via Adafruit nRF52 board support package
|
||||||
|
- Ultra-small (21x17.5mm), USB-C, battery charging built in
|
||||||
|
- 1MB flash, 256KB RAM, 2MB QSPI flash
|
||||||
|
- Has mbedtls available via the nRF SDK
|
||||||
|
- https://www.seeedstudio.com/Seeed-XIAO-BLE-nRF52840-p-5201.html
|
||||||
|
|
||||||
|
**2. Raspberry Pi Pico W (~$6 USD)**
|
||||||
|
- RP2040 + Infineon CYW43439 (WiFi + Bluetooth 5.2 with BLE)
|
||||||
|
- Arduino-compatible via arduino-pico core (earlephilhower)
|
||||||
|
- BLE Central role supported (needed for passive scanning)
|
||||||
|
- Very widely available and cheap
|
||||||
|
- Different architecture (ARM Cortex-M0+) from ESP32 (Xtensa/RISC-V), good for testing portability
|
||||||
|
- https://www.raspberrypi.com/products/raspberry-pi-pico/
|
||||||
|
|
||||||
|
Both boards are under $15, Arduino-compatible, and have BLE Central support needed for passive scanning of Victron advertisements. They use different BLE stacks (nRF SoftDevice vs CYW43 BTstack) which will validate the transport abstraction layer.
|
||||||
|
|
||||||
|
### Current BLE Dependencies
|
||||||
|
|
||||||
|
All ESP32-specific BLE code is confined to:
|
||||||
|
|
||||||
|
1. **Headers** (`VictronBLE.h`):
|
||||||
|
- `#include <BLEDevice.h>`, `<BLEAdvertisedDevice.h>`, `<BLEScan.h>`
|
||||||
|
- `BLEScan*` member
|
||||||
|
- `VictronBLEAdvertisedDeviceCallbacks` class inheriting `BLEAdvertisedDeviceCallbacks`
|
||||||
|
- `BLEAddress` type in `macAddressToString()`
|
||||||
|
|
||||||
|
2. **Implementation** (`VictronBLE.cpp`):
|
||||||
|
- `BLEDevice::init()` — line 42
|
||||||
|
- `BLEDevice::getScan()` — line 43
|
||||||
|
- `pBLEScan->setAdvertisedDeviceCallbacks()` — line 52
|
||||||
|
- `pBLEScan->setActiveScan/setInterval/setWindow` — lines 53-55
|
||||||
|
- `pBLEScan->start()` / `pBLEScan->clearResults()` — lines 140-141
|
||||||
|
- `BLEAdvertisedDevice` methods in `processDevice()` — lines 152-213
|
||||||
|
|
||||||
|
3. **Non-BLE dependencies**:
|
||||||
|
- `mbedtls/aes.h` — available on ESP32, STM32 (via Mbed), and many others
|
||||||
|
- `Arduino.h` — available on all Arduino-compatible platforms
|
||||||
|
|
||||||
|
### What is NOT platform-specific
|
||||||
|
|
||||||
|
The bulk of the code — packet structures, enums, decryption, payload parsing — is pure data processing with no BLE dependency. This is ~70% of the code.
|
||||||
|
|
||||||
|
### Recommended Approach: BLE Transport Abstraction
|
||||||
|
|
||||||
|
Instead of a full HAL with virtual interfaces (which adds complexity), use a **push-based architecture** where the platform-specific code feeds raw manufacturer data into the parser:
|
||||||
|
|
||||||
|
```
|
||||||
|
Platform BLE Code (user provides) → victron_process_advertisement() → Callback
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 1: Extract parser into standalone module
|
||||||
|
|
||||||
|
Create `victron_parser.h/.c` containing:
|
||||||
|
- All packed structs (manufacturer data, payloads)
|
||||||
|
- All enums (device types, charger states)
|
||||||
|
- `victron_decrypt()` — AES-CTR decryption
|
||||||
|
- `victron_parse_advertisement()` — takes raw manufacturer bytes, returns parsed data
|
||||||
|
- Device registry (add device, lookup by MAC)
|
||||||
|
|
||||||
|
This module has **zero BLE dependency**. It needs only `<stdint.h>`, `<string.h>`, and an AES-CTR implementation.
|
||||||
|
|
||||||
|
#### Step 2: Platform-specific BLE adapter (thin)
|
||||||
|
|
||||||
|
For ESP32 Arduino, provide `VictronBLE_ESP32.h` — a thin wrapper that:
|
||||||
|
- Sets up BLE scanning
|
||||||
|
- In the scan callback, extracts MAC + manufacturer data bytes
|
||||||
|
- Calls `victron_process_advertisement(mac, mfg_data, len, rssi)`
|
||||||
|
|
||||||
|
For STM32 (using STM32 BLE stack, or a BLE module like HM-10):
|
||||||
|
- User writes their own scan callback
|
||||||
|
- Calls the same `victron_process_advertisement()` function
|
||||||
|
|
||||||
|
For NRF52 (using Arduino BLE or nRF SDK):
|
||||||
|
- Same pattern
|
||||||
|
|
||||||
|
#### Step 3: AES portability
|
||||||
|
|
||||||
|
`mbedtls` is widely available but not universal. Allow the AES implementation to be swapped:
|
||||||
|
```c
|
||||||
|
// User can override before including victron_parser.h
|
||||||
|
#ifndef VICTRON_AES_CTR_DECRYPT
|
||||||
|
#define VICTRON_AES_CTR_DECRYPT victron_aes_ctr_mbedtls
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
Or simply provide a function pointer:
|
||||||
|
```c
|
||||||
|
typedef bool (*victron_aes_fn)(const uint8_t* key, const uint8_t* iv,
|
||||||
|
const uint8_t* in, uint8_t* out, size_t len);
|
||||||
|
void victron_set_aes_impl(victron_aes_fn fn);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Result
|
||||||
|
|
||||||
|
- **Parser**: Works on any CPU (ESP32, STM32, NRF52, Linux, etc.)
|
||||||
|
- **BLE adapter**: ~30 lines of platform-specific glue code
|
||||||
|
- **AES**: Pluggable, defaults to mbedtls
|
||||||
|
|
||||||
|
This approach is simpler than a virtual HAL interface and puts the user in control of their BLE stack.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 3: C Core with C++ Wrapper
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
|
||||||
|
The "knowledge" in this library is:
|
||||||
|
1. Victron BLE advertisement packet format (struct layouts)
|
||||||
|
2. Field encoding (scaling factors, bit packing, sign extension)
|
||||||
|
3. AES-CTR decryption with nonce construction
|
||||||
|
4. Device type identification
|
||||||
|
|
||||||
|
All of this is pure data processing — no C++ features needed. Moving it to C enables:
|
||||||
|
- Use in ESP-IDF (C-based) without Arduino
|
||||||
|
- Use on bare-metal STM32, NRF, PIC, etc.
|
||||||
|
- Use from other languages via FFI (Python ctypes, Rust FFI, etc.)
|
||||||
|
- Smaller binary, no RTTI/vtable overhead
|
||||||
|
|
||||||
|
### Proposed File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
victron_ble_parser.h # C header — all public types and functions
|
||||||
|
victron_ble_parser.c # C implementation — parsing, decryption, device registry
|
||||||
|
VictronBLE.h # C++ wrapper (Arduino/ESP32 convenience class)
|
||||||
|
VictronBLE.cpp # C++ wrapper implementation
|
||||||
|
```
|
||||||
|
|
||||||
|
### `victron_ble_parser.h` — C API
|
||||||
|
|
||||||
|
```c
|
||||||
|
#ifndef VICTRON_BLE_PARSER_H
|
||||||
|
#define VICTRON_BLE_PARSER_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/* ---- Constants ---- */
|
||||||
|
#define VICTRON_MANUFACTURER_ID 0x02E1
|
||||||
|
#define VICTRON_MAX_DEVICES 8
|
||||||
|
#define VICTRON_ENCRYPTION_KEY_LEN 16
|
||||||
|
#define VICTRON_MAX_ENCRYPTED_LEN 21
|
||||||
|
#define VICTRON_MAC_STR_LEN 13 /* 12 hex chars + null */
|
||||||
|
#define VICTRON_NAME_MAX_LEN 32
|
||||||
|
|
||||||
|
/* ---- Enums ---- */
|
||||||
|
typedef enum {
|
||||||
|
VICTRON_DEVICE_UNKNOWN = 0x00,
|
||||||
|
VICTRON_DEVICE_SOLAR_CHARGER = 0x01,
|
||||||
|
VICTRON_DEVICE_BATTERY_MONITOR = 0x02,
|
||||||
|
VICTRON_DEVICE_INVERTER = 0x03,
|
||||||
|
VICTRON_DEVICE_DCDC_CONVERTER = 0x04,
|
||||||
|
VICTRON_DEVICE_SMART_LITHIUM = 0x05,
|
||||||
|
VICTRON_DEVICE_INVERTER_RS = 0x06,
|
||||||
|
/* ... etc ... */
|
||||||
|
} victron_device_type_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
VICTRON_CHARGER_OFF = 0,
|
||||||
|
VICTRON_CHARGER_BULK = 3,
|
||||||
|
VICTRON_CHARGER_ABSORPTION = 4,
|
||||||
|
VICTRON_CHARGER_FLOAT = 5,
|
||||||
|
/* ... etc ... */
|
||||||
|
} victron_charger_state_t;
|
||||||
|
|
||||||
|
/* ---- Wire-format structures (packed) ---- */
|
||||||
|
typedef struct {
|
||||||
|
uint16_t vendor_id;
|
||||||
|
uint8_t beacon_type;
|
||||||
|
uint8_t unknown[3];
|
||||||
|
uint8_t record_type;
|
||||||
|
uint16_t nonce;
|
||||||
|
uint8_t key_check;
|
||||||
|
uint8_t encrypted_data[VICTRON_MAX_ENCRYPTED_LEN];
|
||||||
|
} __attribute__((packed)) victron_mfg_data_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t device_state;
|
||||||
|
uint8_t error_code;
|
||||||
|
int16_t battery_voltage_10mv;
|
||||||
|
int16_t battery_current_10ma;
|
||||||
|
uint16_t yield_today_10wh;
|
||||||
|
uint16_t input_power_w;
|
||||||
|
uint16_t load_current_10ma;
|
||||||
|
uint8_t reserved[2];
|
||||||
|
} __attribute__((packed)) victron_solar_raw_t;
|
||||||
|
|
||||||
|
/* ... similar for battery_monitor, inverter, dcdc ... */
|
||||||
|
|
||||||
|
/* ---- Parsed result structures ---- */
|
||||||
|
typedef struct {
|
||||||
|
victron_charger_state_t charge_state;
|
||||||
|
float battery_voltage; /* V */
|
||||||
|
float battery_current; /* A */
|
||||||
|
float panel_power; /* W */
|
||||||
|
uint16_t yield_today_wh;
|
||||||
|
float load_current; /* A */
|
||||||
|
uint8_t error_code;
|
||||||
|
} victron_solar_data_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
float voltage; /* V */
|
||||||
|
float current; /* A */
|
||||||
|
float temperature; /* °C */
|
||||||
|
float aux_voltage; /* V */
|
||||||
|
uint16_t remaining_mins;
|
||||||
|
float consumed_ah;
|
||||||
|
float soc; /* % */
|
||||||
|
uint8_t alarms; /* raw alarm bits */
|
||||||
|
} victron_battery_data_t;
|
||||||
|
|
||||||
|
/* ... similar for inverter, dcdc ... */
|
||||||
|
|
||||||
|
/* Tagged union for any device */
|
||||||
|
typedef struct {
|
||||||
|
char mac[VICTRON_MAC_STR_LEN];
|
||||||
|
char name[VICTRON_NAME_MAX_LEN];
|
||||||
|
victron_device_type_t device_type;
|
||||||
|
int8_t rssi;
|
||||||
|
uint32_t last_update_ms;
|
||||||
|
bool data_valid;
|
||||||
|
union {
|
||||||
|
victron_solar_data_t solar;
|
||||||
|
victron_battery_data_t battery;
|
||||||
|
/* victron_inverter_data_t inverter; */
|
||||||
|
/* victron_dcdc_data_t dcdc; */
|
||||||
|
};
|
||||||
|
} victron_device_t;
|
||||||
|
|
||||||
|
/* ---- AES function signature (user can provide their own) ---- */
|
||||||
|
typedef bool (*victron_aes_ctr_fn)(
|
||||||
|
const uint8_t key[16], const uint8_t iv[16],
|
||||||
|
const uint8_t* input, uint8_t* output, size_t len);
|
||||||
|
|
||||||
|
/* ---- Core API ---- */
|
||||||
|
|
||||||
|
/* Initialize the parser context. Provide AES implementation (NULL = use default mbedtls). */
|
||||||
|
void victron_init(victron_aes_ctr_fn aes_fn);
|
||||||
|
|
||||||
|
/* Register a device to monitor. hex_key is 32-char hex string. Returns device index or -1. */
|
||||||
|
int victron_add_device(const char* name, const char* mac_hex,
|
||||||
|
const char* hex_key, victron_device_type_t type);
|
||||||
|
|
||||||
|
/* Process a raw BLE manufacturer data buffer. Called from your BLE scan callback.
|
||||||
|
Returns pointer to updated device, or NULL if not a monitored device. */
|
||||||
|
const victron_device_t* victron_process(const char* mac_hex, int8_t rssi,
|
||||||
|
const uint8_t* mfg_data, size_t mfg_len,
|
||||||
|
uint32_t timestamp_ms);
|
||||||
|
|
||||||
|
/* Get a device by index */
|
||||||
|
const victron_device_t* victron_get_device(int index);
|
||||||
|
|
||||||
|
/* Get device count */
|
||||||
|
int victron_get_device_count(void);
|
||||||
|
|
||||||
|
/* Optional callback — called when a device is updated */
|
||||||
|
typedef void (*victron_update_callback_t)(const victron_device_t* device);
|
||||||
|
void victron_set_callback(victron_update_callback_t cb);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* VICTRON_BLE_PARSER_H */
|
||||||
|
```
|
||||||
|
|
||||||
|
### `victron_ble_parser.c` — Implementation Sketch
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "victron_ble_parser.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* ---- Internal state ---- */
|
||||||
|
static victron_device_t s_devices[VICTRON_MAX_DEVICES];
|
||||||
|
static uint8_t s_keys[VICTRON_MAX_DEVICES][16];
|
||||||
|
static int s_device_count = 0;
|
||||||
|
static victron_aes_ctr_fn s_aes_fn = NULL;
|
||||||
|
static victron_update_callback_t s_callback = NULL;
|
||||||
|
|
||||||
|
/* ---- Default AES (mbedtls) ---- */
|
||||||
|
#ifdef VICTRON_USE_MBEDTLS /* or auto-detect */
|
||||||
|
#include "mbedtls/aes.h"
|
||||||
|
static bool default_aes_ctr(const uint8_t key[16], const uint8_t iv[16],
|
||||||
|
const uint8_t* in, uint8_t* out, size_t len) {
|
||||||
|
mbedtls_aes_context aes;
|
||||||
|
mbedtls_aes_init(&aes);
|
||||||
|
if (mbedtls_aes_setkey_enc(&aes, key, 128) != 0) {
|
||||||
|
mbedtls_aes_free(&aes);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
size_t nc_off = 0;
|
||||||
|
uint8_t nonce[16], stream[16];
|
||||||
|
memcpy(nonce, iv, 16);
|
||||||
|
memset(stream, 0, 16);
|
||||||
|
int ret = mbedtls_aes_crypt_ctr(&aes, len, &nc_off, nonce, stream, in, out);
|
||||||
|
mbedtls_aes_free(&aes);
|
||||||
|
return ret == 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void victron_init(victron_aes_ctr_fn aes_fn) {
|
||||||
|
s_device_count = 0;
|
||||||
|
memset(s_devices, 0, sizeof(s_devices));
|
||||||
|
s_aes_fn = aes_fn;
|
||||||
|
#ifdef VICTRON_USE_MBEDTLS
|
||||||
|
if (!s_aes_fn) s_aes_fn = default_aes_ctr;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hex_to_bytes, normalize_mac, parse_solar, parse_battery, etc. — all pure C */
|
||||||
|
|
||||||
|
const victron_device_t* victron_process(const char* mac_hex, int8_t rssi,
|
||||||
|
const uint8_t* mfg_data, size_t mfg_len,
|
||||||
|
uint32_t timestamp_ms) {
|
||||||
|
/* 1. Check vendor ID */
|
||||||
|
/* 2. Normalize MAC, find in s_devices[] */
|
||||||
|
/* 3. Build IV from nonce, decrypt */
|
||||||
|
/* 4. Parse based on record_type */
|
||||||
|
/* 5. Update device struct, call callback */
|
||||||
|
/* 6. Return pointer to device */
|
||||||
|
return NULL; /* placeholder */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `VictronBLE.h` — C++ Arduino Wrapper (thin)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#ifndef VICTRON_BLE_H
|
||||||
|
#define VICTRON_BLE_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "victron_ble_parser.h"
|
||||||
|
|
||||||
|
#if defined(ESP32)
|
||||||
|
#include <BLEDevice.h>
|
||||||
|
#include <BLEScan.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class VictronBLE {
|
||||||
|
public:
|
||||||
|
bool begin(uint32_t scanDuration = 5);
|
||||||
|
bool addDevice(const char* name, const char* mac,
|
||||||
|
const char* key, victron_device_type_t type);
|
||||||
|
void setCallback(victron_update_callback_t cb);
|
||||||
|
void loop();
|
||||||
|
private:
|
||||||
|
#if defined(ESP32)
|
||||||
|
BLEScan* scan = nullptr;
|
||||||
|
uint32_t scanDuration = 5;
|
||||||
|
static void onScanResult(BLEAdvertisedDevice dev);
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
### What Goes Where
|
||||||
|
|
||||||
|
| Content | File | Language |
|
||||||
|
|---|---|---|
|
||||||
|
| Packet structs (wire format) | `victron_ble_parser.h` | C |
|
||||||
|
| Device type / state enums | `victron_ble_parser.h` | C |
|
||||||
|
| Parsed data structs | `victron_ble_parser.h` | C |
|
||||||
|
| AES-CTR decryption | `victron_ble_parser.c` | C |
|
||||||
|
| Payload parsing (bit twiddling) | `victron_ble_parser.c` | C |
|
||||||
|
| Device registry | `victron_ble_parser.c` | C |
|
||||||
|
| Hex string conversion | `victron_ble_parser.c` | C |
|
||||||
|
| MAC normalization | `victron_ble_parser.c` | C |
|
||||||
|
| ESP32 BLE scanning | `VictronBLE.cpp` | C++ |
|
||||||
|
| Arduino convenience class | `VictronBLE.h/.cpp` | C++ |
|
||||||
|
|
||||||
|
### Migration Steps
|
||||||
|
|
||||||
|
1. Create `victron_ble_parser.h` with all C types and function declarations
|
||||||
|
2. Create `victron_ble_parser.c` — move parsing functions, convert String→char*, convert class methods→free functions
|
||||||
|
3. Slim down `VictronBLE.h` to just the ESP32 BLE scanning wrapper that calls the C API
|
||||||
|
4. Slim down `VictronBLE.cpp` to just `begin()`, `loop()`, and the scan callback glue
|
||||||
|
5. Update examples (minimal changes — API stays similar)
|
||||||
|
6. Test on ESP32 first, then try compiling the C core on a different target
|
||||||
|
|
||||||
|
### Estimated Code Sizes After Split
|
||||||
|
|
||||||
|
- `victron_ble_parser.h`: ~150 lines (types + API)
|
||||||
|
- `victron_ble_parser.c`: ~300 lines (all the protocol knowledge)
|
||||||
|
- `VictronBLE.h`: ~30 lines (ESP32 wrapper)
|
||||||
|
- `VictronBLE.cpp`: ~50 lines (ESP32 BLE glue)
|
||||||
|
|
||||||
|
vs. current: `VictronBLE.h` ~330 lines + `VictronBLE.cpp` ~640 lines = 970 lines total
|
||||||
|
After: ~530 lines total, with better separation of concerns
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
# Upgrading to VictronBLE v0.4
|
||||||
|
|
||||||
|
v0.4 is a breaking API change that simplifies the library significantly.
|
||||||
|
|
||||||
|
## Summary of Changes
|
||||||
|
|
||||||
|
- **Callback**: Virtual class → function pointer
|
||||||
|
- **Data access**: Inheritance → tagged union (`VictronDevice` with `solar`, `battery`, `inverter`, `dcdc` members)
|
||||||
|
- **Strings**: Arduino `String` → fixed `char[]` arrays
|
||||||
|
- **Memory**: `std::map` + heap allocation → fixed array, zero dynamic allocation
|
||||||
|
- **Removed**: `getLastError()`, `removeDevice()`, `getDevicesByType()`, per-type getter methods, `VictronDeviceConfig` struct, `VictronDeviceCallback` class
|
||||||
|
- **Removed field**: `panelVoltage` (was unreliably derived from `panelPower / batteryCurrent`)
|
||||||
|
|
||||||
|
## Migration Guide
|
||||||
|
|
||||||
|
### 1. Callback: class → function pointer
|
||||||
|
|
||||||
|
**Before (v0.3):**
|
||||||
|
```cpp
|
||||||
|
class MyCallback : public VictronDeviceCallback {
|
||||||
|
void onSolarChargerData(const SolarChargerData& data) override {
|
||||||
|
Serial.println(data.deviceName + ": " + String(data.panelPower) + "W");
|
||||||
|
}
|
||||||
|
void onBatteryMonitorData(const BatteryMonitorData& data) override {
|
||||||
|
Serial.println("SOC: " + String(data.soc) + "%");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
MyCallback callback;
|
||||||
|
victron.setCallback(&callback);
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (v0.4):**
|
||||||
|
```cpp
|
||||||
|
void onVictronData(const VictronDevice* dev) {
|
||||||
|
switch (dev->deviceType) {
|
||||||
|
case DEVICE_TYPE_SOLAR_CHARGER:
|
||||||
|
Serial.printf("%s: %.0fW\n", dev->name, dev->solar.panelPower);
|
||||||
|
break;
|
||||||
|
case DEVICE_TYPE_BATTERY_MONITOR:
|
||||||
|
Serial.printf("SOC: %.1f%%\n", dev->battery.soc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
victron.setCallback(onVictronData);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Data field access
|
||||||
|
|
||||||
|
Fields moved from flat `SolarChargerData` etc. into the `VictronDevice` tagged union:
|
||||||
|
|
||||||
|
| Old (v0.3) | New (v0.4) |
|
||||||
|
|---|---|
|
||||||
|
| `data.deviceName` | `dev->name` (char[32]) |
|
||||||
|
| `data.macAddress` | `dev->mac` (char[13]) |
|
||||||
|
| `data.rssi` | `dev->rssi` |
|
||||||
|
| `data.lastUpdate` | `dev->lastUpdate` |
|
||||||
|
| `data.batteryVoltage` | `dev->solar.batteryVoltage` |
|
||||||
|
| `data.batteryCurrent` | `dev->solar.batteryCurrent` |
|
||||||
|
| `data.panelPower` | `dev->solar.panelPower` |
|
||||||
|
| `data.yieldToday` | `dev->solar.yieldToday` |
|
||||||
|
| `data.loadCurrent` | `dev->solar.loadCurrent` |
|
||||||
|
| `data.chargeState` | `dev->solar.chargeState` (uint8_t, was enum) |
|
||||||
|
| `data.panelVoltage` | **Removed** - see below |
|
||||||
|
|
||||||
|
### 3. panelVoltage removed
|
||||||
|
|
||||||
|
`panelVoltage` was a derived value (`panelPower / batteryCurrent`) that was unreliable (division by zero when no current, inaccurate due to MPPT conversion). It has been removed.
|
||||||
|
|
||||||
|
If you need an estimate:
|
||||||
|
```cpp
|
||||||
|
float panelVoltage = (dev->solar.batteryCurrent > 0.1f)
|
||||||
|
? dev->solar.panelPower / dev->solar.batteryCurrent
|
||||||
|
: 0.0f;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. getLastError() removed
|
||||||
|
|
||||||
|
Debug output now goes directly to Serial when `setDebug(true)` is enabled. Remove any `getLastError()` calls.
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```cpp
|
||||||
|
if (!victron.begin(2)) {
|
||||||
|
Serial.println(victron.getLastError());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```cpp
|
||||||
|
if (!victron.begin(2)) {
|
||||||
|
Serial.println("Failed to initialize VictronBLE!");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. String types
|
||||||
|
|
||||||
|
Device name and MAC are now `char[]` instead of Arduino `String`. Use `Serial.printf()` or `String(dev->name)` if you need a String object.
|
||||||
|
|
||||||
|
### 6. addDevice() parameters
|
||||||
|
|
||||||
|
Parameters changed from `String` to `const char*`. Existing string literals work unchanged. `VictronDeviceConfig` struct is no longer needed.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Both v0.3 and v0.4 - string literals work the same
|
||||||
|
victron.addDevice("MySolar", "f69dfcce55eb",
|
||||||
|
"bf25c098c156afd6a180157b8a3ab1fb", DEVICE_TYPE_SOLAR_CHARGER);
|
||||||
|
```
|
||||||
@@ -1,5 +1,86 @@
|
|||||||
# Version History
|
# Version History
|
||||||
|
|
||||||
|
## 0.5.0 (2026-06-04)
|
||||||
|
|
||||||
|
Decoding accuracy fixes (thanks to community bug reports from Karsten, Cory, Kevin and Dan)
|
||||||
|
plus new AC Charger support. Field layouts verified against the keshavdv/victron-ble
|
||||||
|
reference implementation and the Victron "Extra Manufacturer Data" specification.
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
- **Battery monitor decoding rewritten.** The `victronBatteryMonitorPayload` struct had
|
||||||
|
wrong field widths (8-bit alarm instead of 16, no 2-bit aux-mode field) which cascaded
|
||||||
|
and misaligned current, consumed Ah and SOC. `parseBatteryMonitor()` now decodes the
|
||||||
|
bit-packed payload directly by bit offset: signed voltage, 16-bit alarm, aux value +
|
||||||
|
2-bit aux mode (replacing the unreliable `< 3000` voltage/temperature heuristic),
|
||||||
|
22-bit signed current, 20-bit consumed Ah (×-0.1 Ah), 10-bit SOC (×0.1 %).
|
||||||
|
- **Solar charger battery current scale fixed.** Was multiplied by 0.01 (10× too small);
|
||||||
|
the field is in 0.1 A units. Load current is now read as the correct 9-bit field.
|
||||||
|
- **Compile error with newer ESP32 BLE library fixed.** `getManufacturerData()` now returns
|
||||||
|
an Arduino `String` on recent cores; `processDevice()` handles both `String` and
|
||||||
|
`std::string` while preserving the binary payload's embedded null bytes.
|
||||||
|
- **Device type IDs corrected.** The enum was off-by-one from 0x07 onward, mislabelling
|
||||||
|
AC chargers (0x08) as Lynx Smart BMS. Now matches the Victron protocol:
|
||||||
|
0x07 GX Device, 0x08 AC Charger, 0x09 Smart Battery Protect, 0x0A Lynx Smart BMS,
|
||||||
|
0x0B Multi RS, 0x0C VE.Bus, 0x0D DC Energy Meter, 0x0F Orion XS.
|
||||||
|
|
||||||
|
### New features
|
||||||
|
- **AC Charger support** (Blue Smart IP22/IP65/IP67, device type 0x08). New
|
||||||
|
`VictronACChargerData` struct (three output voltage/current banks, temperature, AC
|
||||||
|
current) and `DEVICE_TYPE_AC_CHARGER` handling.
|
||||||
|
|
||||||
|
## 0.4.1 (2026-02-28)
|
||||||
|
|
||||||
|
Major rework of library internals. Breaking API change — not backwards compatible with 0.3.x.
|
||||||
|
|
||||||
|
### Callback API rewrite
|
||||||
|
- Replaced virtual callback class (`VictronDeviceCallback` with 4 override methods) with a
|
||||||
|
single function pointer (`VictronCallback`). Users now provide a plain function instead of
|
||||||
|
subclassing. The callback receives a `VictronDevice*` and switches on `deviceType` to access
|
||||||
|
the appropriate data via a tagged union.
|
||||||
|
|
||||||
|
### Non-blocking BLE scanning
|
||||||
|
- `loop()` is now non-blocking — returns immediately if a scan is already running.
|
||||||
|
Previously it blocked for the entire scan duration (default 5 seconds).
|
||||||
|
- Scan restarts automatically when it completes.
|
||||||
|
|
||||||
|
### Callback throttling
|
||||||
|
- Nonce-based deduplication: skips decrypt/parse/callback when the device's data hasn't
|
||||||
|
changed (detected via the nonce field in the BLE advertisement header).
|
||||||
|
- Configurable minimum interval (`setMinInterval()`, default 1000ms) limits callback
|
||||||
|
frequency even when data is changing rapidly.
|
||||||
|
- Encryption key byte check before AES decryption for early rejection of mismatched keys.
|
||||||
|
|
||||||
|
### Memory and code reduction
|
||||||
|
- Replaced `std::map<String, DeviceInfo*>` with a fixed array (max 8 devices, linear search).
|
||||||
|
Eliminates heap allocation for device storage.
|
||||||
|
- Replaced Arduino `String` with fixed `char[]` arrays throughout (MAC: 12 chars, name: 32 chars).
|
||||||
|
Eliminates heap fragmentation from dynamic string operations.
|
||||||
|
- Replaced inheritance hierarchy (`VictronDeviceData` base + 4 derived classes) with a flat
|
||||||
|
`VictronDevice` struct using a tagged union. No more `new`/`delete` for device data.
|
||||||
|
- Removed `std::map` and `std::vector` includes entirely.
|
||||||
|
- Source reduced from ~970 lines to ~510 lines (48% reduction).
|
||||||
|
- Flash savings: ~11-14 KB across examples.
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
- Fixed undefined behavior: derived objects were deleted through a base pointer without a
|
||||||
|
virtual destructor. Now uses flat structs, no polymorphic delete.
|
||||||
|
- Removed incorrect `panelVoltage` calculation (was dividing PV power by battery current,
|
||||||
|
which is wrong for MPPT chargers). The BLE protocol does not transmit PV voltage.
|
||||||
|
- Removed spurious `nullPad` byte from manufacturer data struct.
|
||||||
|
- Device type is now auto-detected from the BLE advertisement record type. The type
|
||||||
|
parameter in `addDevice()` is optional.
|
||||||
|
|
||||||
|
### Removed features (commented out in header for reference)
|
||||||
|
- `VictronDeviceConfig` struct — use `addDevice(name, mac, key, type)` directly
|
||||||
|
- Per-type getter methods (`getSolarChargerData()`, etc.) — use callback instead
|
||||||
|
- `removeDevice()`, `getDevicesByType()`, `getLastError()`
|
||||||
|
|
||||||
|
### Examples updated
|
||||||
|
- All examples updated for new callback API
|
||||||
|
- Removed `panelVoltage` from ESPNow packet structs (Repeater, FakeRepeater, Receiver)
|
||||||
|
- Removed unnecessary `delay(100)` from loop functions
|
||||||
|
- Added ESPNow Repeater and Receiver examples
|
||||||
|
|
||||||
## 0.3.1 (2026-02-11)
|
## 0.3.1 (2026-02-11)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
[env:esp32dev]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32dev
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_filters = esp32_exception_decoder
|
||||||
|
|
||||||
|
[env:esp32-s3]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32-s3-devkitc-1
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_filters = esp32_exception_decoder
|
||||||
|
build_flags =
|
||||||
|
-D ARDUINO_USB_MODE=1
|
||||||
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
|
||||||
|
[env:esp32-c3]
|
||||||
|
platform = espressif32
|
||||||
|
framework = arduino
|
||||||
|
board = esp32-c3-devkitm-1
|
||||||
|
board_build.mcu = esp32c3
|
||||||
|
board_build.f_cpu = 160000000L
|
||||||
|
board_build.flash_mode = dio
|
||||||
|
board_build.partitions = default.csv
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_filters = time, default, esp32_exception_decoder
|
||||||
|
upload_speed = 921600
|
||||||
|
build_flags =
|
||||||
|
-Os
|
||||||
|
-D ARDUINO_ESP32C3_DEV
|
||||||
|
-D CONFIG_IDF_TARGET_ESP32C3
|
||||||
|
-D ARDUINO_USB_MODE=1
|
||||||
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
|
||||||
|
[env:m5stick]
|
||||||
|
platform = espressif32
|
||||||
|
board = m5stick-c
|
||||||
|
framework = arduino
|
||||||
|
board_build.mcu = esp32
|
||||||
|
board_build.f_cpu = 240000000L
|
||||||
|
board_build.partitions = no_ota.csv
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_filters = esp32_exception_decoder
|
||||||
|
build_flags =
|
||||||
|
-Os
|
||||||
|
lib_deps =
|
||||||
|
M5StickC
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* VictronBLE FakeRepeater Example
|
||||||
|
*
|
||||||
|
* Sends fake Solar Charger data over ESPNow every 10 seconds.
|
||||||
|
* Use with the Receiver example to test ESPNow without needing
|
||||||
|
* a real Victron device or the VictronBLE library.
|
||||||
|
*
|
||||||
|
* No VictronBLE dependency - just WiFi + ESPNow.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <esp_now.h>
|
||||||
|
|
||||||
|
// ESPNow packet structure - must match Receiver
|
||||||
|
struct __attribute__((packed)) SolarChargerPacket {
|
||||||
|
uint8_t chargeState;
|
||||||
|
float batteryVoltage; // V
|
||||||
|
float batteryCurrent; // A
|
||||||
|
float panelPower; // W
|
||||||
|
uint16_t yieldToday; // Wh
|
||||||
|
float loadCurrent; // A
|
||||||
|
int8_t rssi; // BLE RSSI
|
||||||
|
char deviceName[16]; // Null-terminated, truncated
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t BROADCAST_ADDR[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||||
|
static uint32_t sendCount = 0;
|
||||||
|
static unsigned long lastSendTime = 0;
|
||||||
|
static const unsigned long SEND_INTERVAL_MS = 10000;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
Serial.println("\n=== VictronBLE FakeRepeater ===\n");
|
||||||
|
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.disconnect();
|
||||||
|
|
||||||
|
Serial.println("MAC: " + WiFi.macAddress());
|
||||||
|
|
||||||
|
if (esp_now_init() != ESP_OK) {
|
||||||
|
Serial.println("ERROR: ESPNow init failed!");
|
||||||
|
while (1) delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_now_peer_info_t peerInfo = {};
|
||||||
|
memcpy(peerInfo.peer_addr, BROADCAST_ADDR, 6);
|
||||||
|
peerInfo.channel = 0;
|
||||||
|
peerInfo.encrypt = false;
|
||||||
|
|
||||||
|
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
|
||||||
|
Serial.println("ERROR: Failed to add broadcast peer!");
|
||||||
|
while (1) delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("ESPNow initialized, sending fake data every 10s");
|
||||||
|
Serial.println("Packet size: " + String(sizeof(SolarChargerPacket)) + " bytes\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
unsigned long now = millis();
|
||||||
|
if (now - lastSendTime < SEND_INTERVAL_MS) {
|
||||||
|
delay(100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastSendTime = now;
|
||||||
|
sendCount++;
|
||||||
|
|
||||||
|
// Generate varying fake data
|
||||||
|
SolarChargerPacket pkt;
|
||||||
|
pkt.chargeState = (sendCount % 4) + 3; // Cycle through Bulk(3), Absorption(4), Float(5), Storage(6)
|
||||||
|
pkt.batteryVoltage = 51.0f + (sendCount % 20) * 0.15f;
|
||||||
|
pkt.batteryCurrent = 2.0f + (sendCount % 10) * 0.5f;
|
||||||
|
pkt.panelPower = pkt.batteryCurrent * pkt.batteryVoltage;
|
||||||
|
pkt.yieldToday = 100 + sendCount * 10;
|
||||||
|
pkt.loadCurrent = 0;
|
||||||
|
pkt.rssi = -60 - (sendCount % 30);
|
||||||
|
|
||||||
|
memset(pkt.deviceName, 0, sizeof(pkt.deviceName));
|
||||||
|
strncpy(pkt.deviceName, "FakeMPPT", sizeof(pkt.deviceName) - 1);
|
||||||
|
|
||||||
|
esp_err_t result = esp_now_send(BROADCAST_ADDR,
|
||||||
|
reinterpret_cast<const uint8_t*>(&pkt),
|
||||||
|
sizeof(pkt));
|
||||||
|
|
||||||
|
if (result != ESP_OK) {
|
||||||
|
Serial.println("[TX FAIL] " + String(esp_err_to_name(result)));
|
||||||
|
} else {
|
||||||
|
Serial.printf("[TX #%lu] %s Batt:%.2fV %.2fA PV:%.0fW Yield:%uWh RSSI:%d\n",
|
||||||
|
sendCount,
|
||||||
|
pkt.deviceName,
|
||||||
|
pkt.batteryVoltage,
|
||||||
|
pkt.batteryCurrent,
|
||||||
|
pkt.panelPower,
|
||||||
|
pkt.yieldToday,
|
||||||
|
pkt.rssi);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,7 @@
|
|||||||
*
|
*
|
||||||
* Demonstrates change-detection logging for Solar Charger data.
|
* Demonstrates change-detection logging for Solar Charger data.
|
||||||
* Only logs to serial when a value changes (ignoring RSSI), or once
|
* Only logs to serial when a value changes (ignoring RSSI), or once
|
||||||
* per minute if nothing has changed. This keeps serial output quiet
|
* per minute if nothing has changed.
|
||||||
* and is useful for long-running monitoring / data logging.
|
|
||||||
*
|
*
|
||||||
* Setup:
|
* Setup:
|
||||||
* 1. Get your device encryption keys from the VictronConnect app
|
* 1. Get your device encryption keys from the VictronConnect app
|
||||||
@@ -16,13 +15,11 @@
|
|||||||
|
|
||||||
VictronBLE victron;
|
VictronBLE victron;
|
||||||
|
|
||||||
// Tracks last-logged values per device for change detection
|
|
||||||
struct SolarChargerSnapshot {
|
struct SolarChargerSnapshot {
|
||||||
bool valid = false;
|
bool valid = false;
|
||||||
SolarChargerState chargeState;
|
uint8_t chargeState;
|
||||||
float batteryVoltage;
|
float batteryVoltage;
|
||||||
float batteryCurrent;
|
float batteryCurrent;
|
||||||
float panelVoltage;
|
|
||||||
float panelPower;
|
float panelPower;
|
||||||
uint16_t yieldToday;
|
uint16_t yieldToday;
|
||||||
float loadCurrent;
|
float loadCurrent;
|
||||||
@@ -30,26 +27,26 @@ struct SolarChargerSnapshot {
|
|||||||
uint32_t packetsSinceLastLog = 0;
|
uint32_t packetsSinceLastLog = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store a snapshot per device (index by MAC string)
|
|
||||||
static const int MAX_DEVICES = 4;
|
static const int MAX_DEVICES = 4;
|
||||||
static String deviceMACs[MAX_DEVICES];
|
static char deviceMACs[MAX_DEVICES][VICTRON_MAC_LEN];
|
||||||
static SolarChargerSnapshot snapshots[MAX_DEVICES];
|
static SolarChargerSnapshot snapshots[MAX_DEVICES];
|
||||||
static int deviceCount = 0;
|
static int deviceCount = 0;
|
||||||
|
|
||||||
static const unsigned long LOG_INTERVAL_MS = 60000; // 1 minute
|
static const unsigned long LOG_INTERVAL_MS = 60000;
|
||||||
|
|
||||||
static int findOrAddDevice(const String& mac) {
|
static int findOrAddDevice(const char* mac) {
|
||||||
for (int i = 0; i < deviceCount; i++) {
|
for (int i = 0; i < deviceCount; i++) {
|
||||||
if (deviceMACs[i] == mac) return i;
|
if (strcmp(deviceMACs[i], mac) == 0) return i;
|
||||||
}
|
}
|
||||||
if (deviceCount < MAX_DEVICES) {
|
if (deviceCount < MAX_DEVICES) {
|
||||||
deviceMACs[deviceCount] = mac;
|
strncpy(deviceMACs[deviceCount], mac, VICTRON_MAC_LEN - 1);
|
||||||
|
deviceMACs[deviceCount][VICTRON_MAC_LEN - 1] = '\0';
|
||||||
return deviceCount++;
|
return deviceCount++;
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String chargeStateName(SolarChargerState state) {
|
static const char* chargeStateName(uint8_t state) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case CHARGER_OFF: return "Off";
|
case CHARGER_OFF: return "Off";
|
||||||
case CHARGER_LOW_POWER: return "Low Power";
|
case CHARGER_LOW_POWER: return "Low Power";
|
||||||
@@ -66,22 +63,23 @@ static String chargeStateName(SolarChargerState state) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void logData(const SolarChargerData& data, const char* reason, uint32_t packets) {
|
static void logData(const VictronDevice* dev, const VictronSolarData& s,
|
||||||
Serial.println("[" + data.deviceName + "] " + reason +
|
const char* reason, uint32_t packets) {
|
||||||
" pkts:" + String(packets) +
|
Serial.printf("[%s] %s pkts:%lu | State:%s Batt:%.2fV %.2fA PV:%.0fW Yield:%uWh",
|
||||||
" | State:" + chargeStateName(data.chargeState) +
|
dev->name, reason, packets,
|
||||||
" Batt:" + String(data.batteryVoltage, 2) + "V" +
|
chargeStateName(s.chargeState),
|
||||||
" " + String(data.batteryCurrent, 2) + "A" +
|
s.batteryVoltage, s.batteryCurrent,
|
||||||
" PV:" + String(data.panelVoltage, 1) + "V" +
|
s.panelPower, s.yieldToday);
|
||||||
" " + String(data.panelPower, 0) + "W" +
|
if (s.loadCurrent > 0)
|
||||||
" Yield:" + String(data.yieldToday) + "Wh" +
|
Serial.printf(" Load:%.2fA", s.loadCurrent);
|
||||||
(data.loadCurrent > 0 ? " Load:" + String(data.loadCurrent, 2) + "A" : ""));
|
Serial.println();
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoggerCallback : public VictronDeviceCallback {
|
void onVictronData(const VictronDevice* dev) {
|
||||||
public:
|
if (dev->deviceType != DEVICE_TYPE_SOLAR_CHARGER) return;
|
||||||
void onSolarChargerData(const SolarChargerData& data) override {
|
const auto& s = dev->solar;
|
||||||
int idx = findOrAddDevice(data.macAddress);
|
|
||||||
|
int idx = findOrAddDevice(dev->mac);
|
||||||
if (idx < 0) return;
|
if (idx < 0) return;
|
||||||
|
|
||||||
SolarChargerSnapshot& prev = snapshots[idx];
|
SolarChargerSnapshot& prev = snapshots[idx];
|
||||||
@@ -89,43 +87,34 @@ public:
|
|||||||
prev.packetsSinceLastLog++;
|
prev.packetsSinceLastLog++;
|
||||||
|
|
||||||
if (!prev.valid) {
|
if (!prev.valid) {
|
||||||
// First reading - always log
|
logData(dev, s, "INIT", prev.packetsSinceLastLog);
|
||||||
logData(data, "INIT", prev.packetsSinceLastLog);
|
|
||||||
} else {
|
} else {
|
||||||
// Check for changes (everything except RSSI)
|
bool changed = (prev.chargeState != s.chargeState) ||
|
||||||
bool changed = false;
|
(prev.batteryVoltage != s.batteryVoltage) ||
|
||||||
if (prev.chargeState != data.chargeState) changed = true;
|
(prev.batteryCurrent != s.batteryCurrent) ||
|
||||||
if (prev.batteryVoltage != data.batteryVoltage) changed = true;
|
(prev.panelPower != s.panelPower) ||
|
||||||
if (prev.batteryCurrent != data.batteryCurrent) changed = true;
|
(prev.yieldToday != s.yieldToday) ||
|
||||||
if (prev.panelVoltage != data.panelVoltage) changed = true;
|
(prev.loadCurrent != s.loadCurrent);
|
||||||
if (prev.panelPower != data.panelPower) changed = true;
|
|
||||||
if (prev.yieldToday != data.yieldToday) changed = true;
|
|
||||||
if (prev.loadCurrent != data.loadCurrent) changed = true;
|
|
||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
logData(data, "CHG", prev.packetsSinceLastLog);
|
logData(dev, s, "CHG", prev.packetsSinceLastLog);
|
||||||
} else if (now - prev.lastLogTime >= LOG_INTERVAL_MS) {
|
} else if (now - prev.lastLogTime >= LOG_INTERVAL_MS) {
|
||||||
logData(data, "HEARTBEAT", prev.packetsSinceLastLog);
|
logData(dev, s, "HEARTBEAT", prev.packetsSinceLastLog);
|
||||||
} else {
|
} else {
|
||||||
return; // Nothing to log
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update snapshot
|
|
||||||
prev.packetsSinceLastLog = 0;
|
prev.packetsSinceLastLog = 0;
|
||||||
prev.valid = true;
|
prev.valid = true;
|
||||||
prev.chargeState = data.chargeState;
|
prev.chargeState = s.chargeState;
|
||||||
prev.batteryVoltage = data.batteryVoltage;
|
prev.batteryVoltage = s.batteryVoltage;
|
||||||
prev.batteryCurrent = data.batteryCurrent;
|
prev.batteryCurrent = s.batteryCurrent;
|
||||||
prev.panelVoltage = data.panelVoltage;
|
prev.panelPower = s.panelPower;
|
||||||
prev.panelPower = data.panelPower;
|
prev.yieldToday = s.yieldToday;
|
||||||
prev.yieldToday = data.yieldToday;
|
prev.loadCurrent = s.loadCurrent;
|
||||||
prev.loadCurrent = data.loadCurrent;
|
|
||||||
prev.lastLogTime = now;
|
prev.lastLogTime = now;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
LoggerCallback callback;
|
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
@@ -135,14 +124,12 @@ void setup() {
|
|||||||
|
|
||||||
if (!victron.begin(5)) {
|
if (!victron.begin(5)) {
|
||||||
Serial.println("ERROR: Failed to initialize VictronBLE!");
|
Serial.println("ERROR: Failed to initialize VictronBLE!");
|
||||||
Serial.println(victron.getLastError());
|
|
||||||
while (1) delay(1000);
|
while (1) delay(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
victron.setDebug(false);
|
victron.setDebug(false);
|
||||||
victron.setCallback(&callback);
|
victron.setCallback(onVictronData);
|
||||||
|
|
||||||
// Add your devices here
|
|
||||||
victron.addDevice(
|
victron.addDevice(
|
||||||
"Rainbow48V",
|
"Rainbow48V",
|
||||||
"E4:05:42:34:14:F3",
|
"E4:05:42:34:14:F3",
|
||||||
@@ -157,11 +144,10 @@ void setup() {
|
|||||||
DEVICE_TYPE_SOLAR_CHARGER
|
DEVICE_TYPE_SOLAR_CHARGER
|
||||||
);
|
);
|
||||||
|
|
||||||
Serial.println("Configured " + String(victron.getDeviceCount()) + " devices");
|
Serial.printf("Configured %d devices\n", (int)victron.getDeviceCount());
|
||||||
Serial.println("Logging on change, or every 60s heartbeat\n");
|
Serial.println("Logging on change, or every 60s heartbeat\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
victron.loop();
|
victron.loop();
|
||||||
delay(100);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+117
-201
@@ -1,136 +1,25 @@
|
|||||||
/**
|
/**
|
||||||
* VictronBLE Example
|
* VictronBLE Multi-Device Example
|
||||||
*
|
*
|
||||||
* This example demonstrates how to use the VictronBLE library to read data
|
* Demonstrates reading data from multiple Victron device types via BLE.
|
||||||
* from multiple Victron devices simultaneously.
|
|
||||||
*
|
|
||||||
* Hardware Requirements:
|
|
||||||
* - ESP32 board
|
|
||||||
* - Victron devices with BLE (SmartSolar, SmartShunt, etc.)
|
|
||||||
*
|
*
|
||||||
* Setup:
|
* Setup:
|
||||||
* 1. Get your device encryption keys from the VictronConnect app:
|
* 1. Get your device encryption keys from VictronConnect app
|
||||||
* - Open VictronConnect
|
* (Settings > Product Info > Instant readout via Bluetooth > Show)
|
||||||
* - Connect to your device
|
* 2. Update the device configurations below with your MAC and key
|
||||||
* - Go to Settings > Product Info
|
|
||||||
* - Enable "Instant readout via Bluetooth"
|
|
||||||
* - Click "Show" next to "Instant readout details"
|
|
||||||
* - Copy the encryption key (32 hex characters)
|
|
||||||
*
|
|
||||||
* 2. Update the device configurations below with your devices' MAC addresses
|
|
||||||
* and encryption keys
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "VictronBLE.h"
|
#include "VictronBLE.h"
|
||||||
|
|
||||||
// Create VictronBLE instance
|
|
||||||
VictronBLE victron;
|
VictronBLE victron;
|
||||||
|
|
||||||
// Device callback class - gets called when new data arrives
|
static uint32_t solarChargerCount = 0;
|
||||||
class MyVictronCallback : public VictronDeviceCallback {
|
static uint32_t batteryMonitorCount = 0;
|
||||||
public:
|
static uint32_t inverterCount = 0;
|
||||||
uint32_t solarChargerCount = 0;
|
static uint32_t dcdcConverterCount = 0;
|
||||||
uint32_t batteryMonitorCount = 0;
|
|
||||||
uint32_t inverterCount = 0;
|
|
||||||
uint32_t dcdcConverterCount = 0;
|
|
||||||
|
|
||||||
void onSolarChargerData(const SolarChargerData& data) override {
|
static const char* chargeStateName(uint8_t state) {
|
||||||
solarChargerCount++;
|
|
||||||
Serial.println("\n=== Solar Charger: " + data.deviceName + " (#" + String(solarChargerCount) + ") ===");
|
|
||||||
Serial.println("MAC: " + data.macAddress);
|
|
||||||
Serial.println("RSSI: " + String(data.rssi) + " dBm");
|
|
||||||
Serial.println("State: " + getChargeStateName(data.chargeState));
|
|
||||||
Serial.println("Battery: " + String(data.batteryVoltage, 2) + " V");
|
|
||||||
Serial.println("Current: " + String(data.batteryCurrent, 2) + " A");
|
|
||||||
Serial.println("Panel Voltage: " + String(data.panelVoltage, 1) + " V");
|
|
||||||
Serial.println("Panel Power: " + String(data.panelPower) + " W");
|
|
||||||
Serial.println("Yield Today: " + String(data.yieldToday) + " Wh");
|
|
||||||
if (data.loadCurrent > 0) {
|
|
||||||
Serial.println("Load Current: " + String(data.loadCurrent, 2) + " A");
|
|
||||||
}
|
|
||||||
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
|
|
||||||
}
|
|
||||||
|
|
||||||
void onBatteryMonitorData(const BatteryMonitorData& data) override {
|
|
||||||
batteryMonitorCount++;
|
|
||||||
Serial.println("\n=== Battery Monitor: " + data.deviceName + " (#" + String(batteryMonitorCount) + ") ===");
|
|
||||||
Serial.println("MAC: " + data.macAddress);
|
|
||||||
Serial.println("RSSI: " + String(data.rssi) + " dBm");
|
|
||||||
Serial.println("Voltage: " + String(data.voltage, 2) + " V");
|
|
||||||
Serial.println("Current: " + String(data.current, 2) + " A");
|
|
||||||
Serial.println("SOC: " + String(data.soc, 1) + " %");
|
|
||||||
Serial.println("Consumed: " + String(data.consumedAh, 2) + " Ah");
|
|
||||||
|
|
||||||
if (data.remainingMinutes < 65535) {
|
|
||||||
int hours = data.remainingMinutes / 60;
|
|
||||||
int mins = data.remainingMinutes % 60;
|
|
||||||
Serial.println("Time Remaining: " + String(hours) + "h " + String(mins) + "m");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.temperature > 0) {
|
|
||||||
Serial.println("Temperature: " + String(data.temperature, 1) + " °C");
|
|
||||||
}
|
|
||||||
if (data.auxVoltage > 0) {
|
|
||||||
Serial.println("Aux Voltage: " + String(data.auxVoltage, 2) + " V");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print alarms
|
|
||||||
if (data.alarmLowVoltage || data.alarmHighVoltage || data.alarmLowSOC ||
|
|
||||||
data.alarmLowTemperature || data.alarmHighTemperature) {
|
|
||||||
Serial.print("ALARMS: ");
|
|
||||||
if (data.alarmLowVoltage) Serial.print("LOW-V ");
|
|
||||||
if (data.alarmHighVoltage) Serial.print("HIGH-V ");
|
|
||||||
if (data.alarmLowSOC) Serial.print("LOW-SOC ");
|
|
||||||
if (data.alarmLowTemperature) Serial.print("LOW-TEMP ");
|
|
||||||
if (data.alarmHighTemperature) Serial.print("HIGH-TEMP ");
|
|
||||||
Serial.println();
|
|
||||||
}
|
|
||||||
|
|
||||||
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
|
|
||||||
}
|
|
||||||
|
|
||||||
void onInverterData(const InverterData& data) override {
|
|
||||||
inverterCount++;
|
|
||||||
Serial.println("\n=== Inverter/Charger: " + data.deviceName + " (#" + String(inverterCount) + ") ===");
|
|
||||||
Serial.println("MAC: " + data.macAddress);
|
|
||||||
Serial.println("RSSI: " + String(data.rssi) + " dBm");
|
|
||||||
Serial.println("Battery: " + String(data.batteryVoltage, 2) + " V");
|
|
||||||
Serial.println("Current: " + String(data.batteryCurrent, 2) + " A");
|
|
||||||
Serial.println("AC Power: " + String(data.acPower) + " W");
|
|
||||||
Serial.println("State: " + String(data.state));
|
|
||||||
|
|
||||||
// Print alarms
|
|
||||||
if (data.alarmLowVoltage || data.alarmHighVoltage ||
|
|
||||||
data.alarmHighTemperature || data.alarmOverload) {
|
|
||||||
Serial.print("ALARMS: ");
|
|
||||||
if (data.alarmLowVoltage) Serial.print("LOW-V ");
|
|
||||||
if (data.alarmHighVoltage) Serial.print("HIGH-V ");
|
|
||||||
if (data.alarmHighTemperature) Serial.print("TEMP ");
|
|
||||||
if (data.alarmOverload) Serial.print("OVERLOAD ");
|
|
||||||
Serial.println();
|
|
||||||
}
|
|
||||||
|
|
||||||
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
|
|
||||||
}
|
|
||||||
|
|
||||||
void onDCDCConverterData(const DCDCConverterData& data) override {
|
|
||||||
dcdcConverterCount++;
|
|
||||||
Serial.println("\n=== DC-DC Converter: " + data.deviceName + " (#" + String(dcdcConverterCount) + ") ===");
|
|
||||||
Serial.println("MAC: " + data.macAddress);
|
|
||||||
Serial.println("RSSI: " + String(data.rssi) + " dBm");
|
|
||||||
Serial.println("Input: " + String(data.inputVoltage, 2) + " V");
|
|
||||||
Serial.println("Output: " + String(data.outputVoltage, 2) + " V");
|
|
||||||
Serial.println("Current: " + String(data.outputCurrent, 2) + " A");
|
|
||||||
Serial.println("State: " + String(data.chargeState));
|
|
||||||
if (data.errorCode != 0) {
|
|
||||||
Serial.println("Error Code: " + String(data.errorCode));
|
|
||||||
}
|
|
||||||
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
String getChargeStateName(SolarChargerState state) {
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case CHARGER_OFF: return "Off";
|
case CHARGER_OFF: return "Off";
|
||||||
case CHARGER_LOW_POWER: return "Low Power";
|
case CHARGER_LOW_POWER: return "Low Power";
|
||||||
@@ -146,9 +35,95 @@ private:
|
|||||||
default: return "Unknown";
|
default: return "Unknown";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
MyVictronCallback callback;
|
void onVictronData(const VictronDevice* dev) {
|
||||||
|
switch (dev->deviceType) {
|
||||||
|
case DEVICE_TYPE_SOLAR_CHARGER: {
|
||||||
|
const auto& s = dev->solar;
|
||||||
|
solarChargerCount++;
|
||||||
|
Serial.printf("\n=== Solar Charger: %s (#%lu) ===\n", dev->name, solarChargerCount);
|
||||||
|
Serial.printf("MAC: %s\n", dev->mac);
|
||||||
|
Serial.printf("RSSI: %d dBm\n", dev->rssi);
|
||||||
|
Serial.printf("State: %s\n", chargeStateName(s.chargeState));
|
||||||
|
Serial.printf("Battery: %.2f V\n", s.batteryVoltage);
|
||||||
|
Serial.printf("Current: %.2f A\n", s.batteryCurrent);
|
||||||
|
Serial.printf("Panel Power: %.0f W\n", s.panelPower);
|
||||||
|
Serial.printf("Yield Today: %u Wh\n", s.yieldToday);
|
||||||
|
if (s.loadCurrent > 0)
|
||||||
|
Serial.printf("Load Current: %.2f A\n", s.loadCurrent);
|
||||||
|
Serial.printf("Last Update: %lus ago\n", (millis() - dev->lastUpdate) / 1000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case DEVICE_TYPE_BATTERY_MONITOR: {
|
||||||
|
const auto& b = dev->battery;
|
||||||
|
batteryMonitorCount++;
|
||||||
|
Serial.printf("\n=== Battery Monitor: %s (#%lu) ===\n", dev->name, batteryMonitorCount);
|
||||||
|
Serial.printf("MAC: %s\n", dev->mac);
|
||||||
|
Serial.printf("RSSI: %d dBm\n", dev->rssi);
|
||||||
|
Serial.printf("Voltage: %.2f V\n", b.voltage);
|
||||||
|
Serial.printf("Current: %.2f A\n", b.current);
|
||||||
|
Serial.printf("SOC: %.1f %%\n", b.soc);
|
||||||
|
Serial.printf("Consumed: %.2f Ah\n", b.consumedAh);
|
||||||
|
if (b.remainingMinutes < 65535)
|
||||||
|
Serial.printf("Time Remaining: %dh %dm\n", b.remainingMinutes / 60, b.remainingMinutes % 60);
|
||||||
|
if (b.temperature > 0)
|
||||||
|
Serial.printf("Temperature: %.1f C\n", b.temperature);
|
||||||
|
if (b.auxVoltage > 0)
|
||||||
|
Serial.printf("Aux Voltage: %.2f V\n", b.auxVoltage);
|
||||||
|
if (b.alarmLowVoltage || b.alarmHighVoltage || b.alarmLowSOC ||
|
||||||
|
b.alarmLowTemperature || b.alarmHighTemperature) {
|
||||||
|
Serial.print("ALARMS:");
|
||||||
|
if (b.alarmLowVoltage) Serial.print(" LOW-V");
|
||||||
|
if (b.alarmHighVoltage) Serial.print(" HIGH-V");
|
||||||
|
if (b.alarmLowSOC) Serial.print(" LOW-SOC");
|
||||||
|
if (b.alarmLowTemperature) Serial.print(" LOW-TEMP");
|
||||||
|
if (b.alarmHighTemperature) Serial.print(" HIGH-TEMP");
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
Serial.printf("Last Update: %lus ago\n", (millis() - dev->lastUpdate) / 1000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case DEVICE_TYPE_INVERTER: {
|
||||||
|
const auto& inv = dev->inverter;
|
||||||
|
inverterCount++;
|
||||||
|
Serial.printf("\n=== Inverter/Charger: %s (#%lu) ===\n", dev->name, inverterCount);
|
||||||
|
Serial.printf("MAC: %s\n", dev->mac);
|
||||||
|
Serial.printf("RSSI: %d dBm\n", dev->rssi);
|
||||||
|
Serial.printf("Battery: %.2f V\n", inv.batteryVoltage);
|
||||||
|
Serial.printf("Current: %.2f A\n", inv.batteryCurrent);
|
||||||
|
Serial.printf("AC Power: %.0f W\n", inv.acPower);
|
||||||
|
Serial.printf("State: %d\n", inv.state);
|
||||||
|
if (inv.alarmLowVoltage || inv.alarmHighVoltage ||
|
||||||
|
inv.alarmHighTemperature || inv.alarmOverload) {
|
||||||
|
Serial.print("ALARMS:");
|
||||||
|
if (inv.alarmLowVoltage) Serial.print(" LOW-V");
|
||||||
|
if (inv.alarmHighVoltage) Serial.print(" HIGH-V");
|
||||||
|
if (inv.alarmHighTemperature) Serial.print(" TEMP");
|
||||||
|
if (inv.alarmOverload) Serial.print(" OVERLOAD");
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
Serial.printf("Last Update: %lus ago\n", (millis() - dev->lastUpdate) / 1000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case DEVICE_TYPE_DCDC_CONVERTER: {
|
||||||
|
const auto& dc = dev->dcdc;
|
||||||
|
dcdcConverterCount++;
|
||||||
|
Serial.printf("\n=== DC-DC Converter: %s (#%lu) ===\n", dev->name, dcdcConverterCount);
|
||||||
|
Serial.printf("MAC: %s\n", dev->mac);
|
||||||
|
Serial.printf("RSSI: %d dBm\n", dev->rssi);
|
||||||
|
Serial.printf("Input: %.2f V\n", dc.inputVoltage);
|
||||||
|
Serial.printf("Output: %.2f V\n", dc.outputVoltage);
|
||||||
|
Serial.printf("Current: %.2f A\n", dc.outputCurrent);
|
||||||
|
Serial.printf("State: %d\n", dc.chargeState);
|
||||||
|
if (dc.errorCode != 0)
|
||||||
|
Serial.printf("Error Code: %d\n", dc.errorCode);
|
||||||
|
Serial.printf("Last Update: %lus ago\n", (millis() - dev->lastUpdate) / 1000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
@@ -158,102 +133,43 @@ void setup() {
|
|||||||
Serial.println("VictronBLE Multi-Device Example");
|
Serial.println("VictronBLE Multi-Device Example");
|
||||||
Serial.println("=================================\n");
|
Serial.println("=================================\n");
|
||||||
|
|
||||||
// Initialize VictronBLE with 5 second scan duration
|
|
||||||
if (!victron.begin(5)) {
|
if (!victron.begin(5)) {
|
||||||
Serial.println("ERROR: Failed to initialize VictronBLE!");
|
Serial.println("ERROR: Failed to initialize VictronBLE!");
|
||||||
Serial.println(victron.getLastError());
|
|
||||||
while (1) delay(1000);
|
while (1) delay(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable debug output (optional)
|
|
||||||
victron.setDebug(false);
|
victron.setDebug(false);
|
||||||
|
victron.setCallback(onVictronData);
|
||||||
// Set callback for data updates
|
|
||||||
victron.setCallback(&callback);
|
|
||||||
|
|
||||||
// Add your devices here
|
|
||||||
// Replace with your actual MAC addresses and encryption keys
|
|
||||||
|
|
||||||
// CORRECT in Alternative
|
|
||||||
// Rainbow48V at MAC e4:05:42:34:14:f3
|
|
||||||
|
|
||||||
// Temporary - Scott Example
|
|
||||||
victron.addDevice(
|
|
||||||
"Rainbow48V", // Device name
|
|
||||||
"E4:05:42:34:14:F3", // MAC address
|
|
||||||
"0ec3adf7433dd61793ff2f3b8ad32ed8", // Encryption key (32 hex chars)
|
|
||||||
DEVICE_TYPE_SOLAR_CHARGER // Device type
|
|
||||||
);
|
|
||||||
|
|
||||||
victron.addDevice(
|
victron.addDevice(
|
||||||
"ScottTrailer", // Device name
|
"Rainbow48V",
|
||||||
"e64559783cfb",
|
"E4:05:42:34:14:F3",
|
||||||
"3fa658aded4f309b9bc17a2318cb1f56",
|
"0ec3adf7433dd61793ff2f3b8ad32ed8",
|
||||||
DEVICE_TYPE_SOLAR_CHARGER // Device type
|
|
||||||
);
|
|
||||||
|
|
||||||
// Example: Solar Charger #1
|
|
||||||
/*
|
|
||||||
victron.addDevice(
|
|
||||||
"MPPT 100/30", // Device name
|
|
||||||
"E7:48:D4:28:B7:9C", // MAC address
|
|
||||||
"0df4d0395b7d1a876c0c33ecb9e70dcd", // Encryption key (32 hex chars)
|
|
||||||
DEVICE_TYPE_SOLAR_CHARGER // Device type
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Example: Solar Charger #2
|
|
||||||
/*
|
|
||||||
victron.addDevice(
|
|
||||||
"MPPT 75/15",
|
|
||||||
"AA:BB:CC:DD:EE:FF",
|
|
||||||
"1234567890abcdef1234567890abcdef",
|
|
||||||
DEVICE_TYPE_SOLAR_CHARGER
|
DEVICE_TYPE_SOLAR_CHARGER
|
||||||
);
|
);
|
||||||
*/
|
|
||||||
|
|
||||||
// Example: Battery Monitor (SmartShunt)
|
|
||||||
/*
|
|
||||||
victron.addDevice(
|
victron.addDevice(
|
||||||
"SmartShunt",
|
"ScottTrailer",
|
||||||
"11:22:33:44:55:66",
|
"e64559783cfb",
|
||||||
"fedcba0987654321fedcba0987654321",
|
"3fa658aded4f309b9bc17a2318cb1f56",
|
||||||
DEVICE_TYPE_BATTERY_MONITOR
|
DEVICE_TYPE_SOLAR_CHARGER
|
||||||
);
|
);
|
||||||
*/
|
|
||||||
|
|
||||||
// Example: Inverter/Charger
|
Serial.printf("Configured %d devices\n", (int)victron.getDeviceCount());
|
||||||
/*
|
|
||||||
victron.addDevice(
|
|
||||||
"MultiPlus",
|
|
||||||
"99:88:77:66:55:44",
|
|
||||||
"abcdefabcdefabcdefabcdefabcdefab",
|
|
||||||
DEVICE_TYPE_INVERTER
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
Serial.println("Configured " + String(victron.getDeviceCount()) + " devices");
|
|
||||||
Serial.println("\nStarting BLE scan...\n");
|
Serial.println("\nStarting BLE scan...\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static uint32_t loopCount = 0;
|
||||||
|
static uint32_t lastReport = 0;
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
// Process BLE scanning and data updates
|
victron.loop(); // Non-blocking: returns immediately if scan is running
|
||||||
victron.loop();
|
loopCount++;
|
||||||
|
|
||||||
// Optional: You can also manually query device data
|
uint32_t now = millis();
|
||||||
// This is useful if you're not using callbacks
|
if (now - lastReport >= 10000) {
|
||||||
/*
|
Serial.printf("Loop iterations in last 10s: %lu\n", loopCount);
|
||||||
SolarChargerData solarData;
|
loopCount = 0;
|
||||||
if (victron.getSolarChargerData("E7:48:D4:28:B7:9C", solarData)) {
|
lastReport = now;
|
||||||
// Do something with solarData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BatteryMonitorData batteryData;
|
|
||||||
if (victron.getBatteryMonitorData("11:22:33:44:55:66", batteryData)) {
|
|
||||||
// Do something with batteryData
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Add a small delay to avoid overwhelming the serial output
|
|
||||||
delay(100);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
[env:esp32dev]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32dev
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_filters = esp32_exception_decoder
|
||||||
|
|
||||||
|
[env:esp32-s3]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32-s3-devkitc-1
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_filters = esp32_exception_decoder
|
||||||
|
build_flags =
|
||||||
|
-D ARDUINO_USB_MODE=1
|
||||||
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
|
||||||
|
[env:m5stick]
|
||||||
|
platform = espressif32
|
||||||
|
board = m5stick-c
|
||||||
|
framework = arduino
|
||||||
|
board_build.mcu = esp32
|
||||||
|
board_build.f_cpu = 240000000L
|
||||||
|
board_build.partitions = no_ota.csv
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_filters = esp32_exception_decoder
|
||||||
|
build_flags =
|
||||||
|
-Os
|
||||||
|
-D USE_M5STICK
|
||||||
|
lib_deps =
|
||||||
|
M5StickC
|
||||||
|
|
||||||
|
[env:esp32-c3]
|
||||||
|
platform = espressif32
|
||||||
|
framework = arduino
|
||||||
|
board = esp32-c3-devkitm-1
|
||||||
|
board_build.mcu = esp32c3
|
||||||
|
board_build.f_cpu = 160000000L
|
||||||
|
board_build.flash_mode = dio
|
||||||
|
board_build.partitions = default.csv
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_filters = time, default, esp32_exception_decoder
|
||||||
|
upload_speed = 921600
|
||||||
|
build_flags =
|
||||||
|
-Os
|
||||||
|
-D ARDUINO_ESP32C3_DEV
|
||||||
|
-D CONFIG_IDF_TARGET_ESP32C3
|
||||||
|
-D ARDUINO_USB_MODE=1
|
||||||
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
/**
|
||||||
|
* VictronBLE ESPNow Receiver
|
||||||
|
*
|
||||||
|
* Standalone receiver for data sent by the Repeater example.
|
||||||
|
* Does NOT depend on VictronBLE library - just ESPNow.
|
||||||
|
*
|
||||||
|
* Flash this on a second ESP32 and it will print Solar Charger
|
||||||
|
* data received over ESPNow from the Repeater.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <esp_now.h>
|
||||||
|
|
||||||
|
#ifdef USE_M5STICK
|
||||||
|
#include <M5StickC.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ESPNow packet structure - must match Repeater
|
||||||
|
struct __attribute__((packed)) SolarChargerPacket {
|
||||||
|
uint8_t chargeState;
|
||||||
|
float batteryVoltage; // V
|
||||||
|
float batteryCurrent; // A
|
||||||
|
float panelPower; // W
|
||||||
|
uint16_t yieldToday; // Wh
|
||||||
|
float loadCurrent; // A
|
||||||
|
int8_t rssi; // BLE RSSI
|
||||||
|
char deviceName[16]; // Null-terminated, truncated
|
||||||
|
};
|
||||||
|
|
||||||
|
static uint32_t recvCount = 0;
|
||||||
|
|
||||||
|
#ifdef USE_M5STICK
|
||||||
|
// Display: cache latest packet per device for screen rotation
|
||||||
|
static const int MAX_DISPLAY_DEVICES = 4;
|
||||||
|
static SolarChargerPacket displayPackets[MAX_DISPLAY_DEVICES];
|
||||||
|
static bool displayValid[MAX_DISPLAY_DEVICES] = {};
|
||||||
|
static int displayCount = 0;
|
||||||
|
static int displayPage = 0; // Which device to show
|
||||||
|
static bool displayDirty = true;
|
||||||
|
static unsigned long lastPageSwitch = 0;
|
||||||
|
static const unsigned long PAGE_SWITCH_MS = 5000; // Rotate pages every 5s
|
||||||
|
|
||||||
|
static int findOrAddDisplay(const char* name) {
|
||||||
|
for (int i = 0; i < displayCount; i++) {
|
||||||
|
if (strncmp(displayPackets[i].deviceName, name, 16) == 0) return i;
|
||||||
|
}
|
||||||
|
if (displayCount < MAX_DISPLAY_DEVICES) return displayCount++;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static const char* chargeStateName(uint8_t state) {
|
||||||
|
switch (state) {
|
||||||
|
case 0: return "Off";
|
||||||
|
case 1: return "Low Power";
|
||||||
|
case 2: return "Fault";
|
||||||
|
case 3: return "Bulk";
|
||||||
|
case 4: return "Absorption";
|
||||||
|
case 5: return "Float";
|
||||||
|
case 6: return "Storage";
|
||||||
|
case 7: return "Equalize";
|
||||||
|
case 9: return "Inverting";
|
||||||
|
case 11: return "Power Supply";
|
||||||
|
case 252: return "External Control";
|
||||||
|
default: return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDataRecv(const uint8_t* senderMac, const uint8_t* data, int len) {
|
||||||
|
if (len != sizeof(SolarChargerPacket)) {
|
||||||
|
Serial.println("Unexpected packet size: " + String(len));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* pkt = reinterpret_cast<const SolarChargerPacket*>(data);
|
||||||
|
recvCount++;
|
||||||
|
|
||||||
|
// Ensure device name is null-terminated even if corrupted
|
||||||
|
char name[17];
|
||||||
|
memcpy(name, pkt->deviceName, 16);
|
||||||
|
name[16] = '\0';
|
||||||
|
|
||||||
|
Serial.printf("[RX #%lu] %s | State:%s Batt:%.2fV %.2fA PV:%.0fW Yield:%uWh",
|
||||||
|
recvCount,
|
||||||
|
name,
|
||||||
|
chargeStateName(pkt->chargeState),
|
||||||
|
pkt->batteryVoltage,
|
||||||
|
pkt->batteryCurrent,
|
||||||
|
pkt->panelPower,
|
||||||
|
pkt->yieldToday);
|
||||||
|
|
||||||
|
if (pkt->loadCurrent > 0) {
|
||||||
|
Serial.printf(" Load:%.2fA", pkt->loadCurrent);
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf(" RSSI:%ddBm From:%02X:%02X:%02X:%02X:%02X:%02X\n",
|
||||||
|
pkt->rssi,
|
||||||
|
senderMac[0], senderMac[1], senderMac[2],
|
||||||
|
senderMac[3], senderMac[4], senderMac[5]);
|
||||||
|
|
||||||
|
#ifdef USE_M5STICK
|
||||||
|
int idx = findOrAddDisplay(name);
|
||||||
|
if (idx >= 0) {
|
||||||
|
displayPackets[idx] = *pkt;
|
||||||
|
displayValid[idx] = true;
|
||||||
|
displayDirty = true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
#ifdef USE_M5STICK
|
||||||
|
M5.begin();
|
||||||
|
M5.Lcd.setRotation(3); // Landscape, USB on right
|
||||||
|
M5.Lcd.fillScreen(BLACK);
|
||||||
|
M5.Lcd.setTextColor(WHITE, BLACK);
|
||||||
|
M5.Lcd.setTextSize(1);
|
||||||
|
M5.Lcd.setCursor(0, 0);
|
||||||
|
M5.Lcd.println("ESPNow Receiver");
|
||||||
|
M5.Lcd.println("Waiting...");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
Serial.println("\n=== VictronBLE ESPNow Receiver ===\n");
|
||||||
|
|
||||||
|
// Init WiFi in STA mode (required for ESPNow)
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.disconnect();
|
||||||
|
|
||||||
|
Serial.println("MAC: " + WiFi.macAddress());
|
||||||
|
|
||||||
|
// Init ESPNow
|
||||||
|
if (esp_now_init() != ESP_OK) {
|
||||||
|
Serial.println("ERROR: ESPNow init failed!");
|
||||||
|
while (1) delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_now_register_recv_cb(onDataRecv);
|
||||||
|
|
||||||
|
Serial.println("ESPNow initialized, waiting for packets...");
|
||||||
|
Serial.println("Expecting " + String(sizeof(SolarChargerPacket)) + " byte packets\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
#ifdef USE_M5STICK
|
||||||
|
M5.update();
|
||||||
|
|
||||||
|
// Button A (front): manually cycle to next device
|
||||||
|
if (M5.BtnA.wasPressed()) {
|
||||||
|
if (displayCount > 0) {
|
||||||
|
displayPage = (displayPage + 1) % displayCount;
|
||||||
|
displayDirty = true;
|
||||||
|
lastPageSwitch = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-rotate pages every 5 seconds if multiple devices
|
||||||
|
if (displayCount > 1) {
|
||||||
|
unsigned long now = millis();
|
||||||
|
if (now - lastPageSwitch >= PAGE_SWITCH_MS) {
|
||||||
|
lastPageSwitch = now;
|
||||||
|
displayPage = (displayPage + 1) % displayCount;
|
||||||
|
displayDirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redraw screen when data changes or page switches
|
||||||
|
if (displayDirty && displayCount > 0) {
|
||||||
|
displayDirty = false;
|
||||||
|
|
||||||
|
int p = displayPage % displayCount;
|
||||||
|
if (!displayValid[p]) { delay(100); return; }
|
||||||
|
|
||||||
|
const auto& pkt = displayPackets[p];
|
||||||
|
|
||||||
|
M5.Lcd.fillScreen(BLACK);
|
||||||
|
M5.Lcd.setCursor(0, 0);
|
||||||
|
|
||||||
|
// Row 0: device name + page indicator
|
||||||
|
M5.Lcd.setTextColor(CYAN, BLACK);
|
||||||
|
M5.Lcd.printf("%s", pkt.deviceName);
|
||||||
|
if (displayCount > 1) {
|
||||||
|
M5.Lcd.printf(" [%d/%d]", p + 1, displayCount);
|
||||||
|
}
|
||||||
|
M5.Lcd.println();
|
||||||
|
|
||||||
|
// Row 1: charge state
|
||||||
|
M5.Lcd.setTextColor(YELLOW, BLACK);
|
||||||
|
M5.Lcd.printf("State: %s\n", chargeStateName(pkt.chargeState));
|
||||||
|
|
||||||
|
// Row 2: battery voltage + current (large-ish)
|
||||||
|
M5.Lcd.setTextColor(GREEN, BLACK);
|
||||||
|
M5.Lcd.setTextSize(2);
|
||||||
|
M5.Lcd.printf("%.2fV\n", pkt.batteryVoltage);
|
||||||
|
M5.Lcd.setTextSize(1);
|
||||||
|
M5.Lcd.setTextColor(WHITE, BLACK);
|
||||||
|
M5.Lcd.printf("Batt: %.2fA\n", pkt.batteryCurrent);
|
||||||
|
|
||||||
|
// Row 3: PV
|
||||||
|
M5.Lcd.printf("PV: %.0fW\n", pkt.panelPower);
|
||||||
|
|
||||||
|
// Row 4: yield + load
|
||||||
|
M5.Lcd.printf("Yield: %uWh", pkt.yieldToday);
|
||||||
|
if (pkt.loadCurrent > 0) {
|
||||||
|
M5.Lcd.printf(" Ld:%.1fA", pkt.loadCurrent);
|
||||||
|
}
|
||||||
|
M5.Lcd.println();
|
||||||
|
|
||||||
|
// Row 5: stats
|
||||||
|
M5.Lcd.setTextColor(DARKGREY, BLACK);
|
||||||
|
M5.Lcd.printf("RSSI:%d RX:%lu", pkt.rssi, recvCount);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
delay(100);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
[env]
|
||||||
|
lib_extra_dirs = ../..
|
||||||
|
|
||||||
|
[env:esp32dev]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32dev
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_filters = esp32_exception_decoder
|
||||||
|
build_flags =
|
||||||
|
-DCORE_DEBUG_LEVEL=3
|
||||||
|
|
||||||
|
[env:esp32-s3]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32-s3-devkitc-1
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_filters = esp32_exception_decoder
|
||||||
|
build_flags =
|
||||||
|
-D ARDUINO_USB_MODE=1
|
||||||
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
|
||||||
|
[env:esp32-c3]
|
||||||
|
platform = espressif32
|
||||||
|
framework = arduino
|
||||||
|
board = esp32-c3-devkitm-1
|
||||||
|
board_build.mcu = esp32c3
|
||||||
|
board_build.f_cpu = 160000000L
|
||||||
|
board_build.flash_mode = dio
|
||||||
|
board_build.partitions = huge_app.csv
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_filters = time, default, esp32_exception_decoder
|
||||||
|
upload_speed = 921600
|
||||||
|
build_flags =
|
||||||
|
-Os
|
||||||
|
-I src
|
||||||
|
-D ARDUINO_ESP32C3_DEV
|
||||||
|
-D CONFIG_IDF_TARGET_ESP32C3
|
||||||
|
-D ARDUINO_USB_MODE=1
|
||||||
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
lib_deps =
|
||||||
|
elapsedMillis
|
||||||
|
|
||||||
|
[env:m5stick]
|
||||||
|
platform = espressif32
|
||||||
|
board = m5stick-c
|
||||||
|
framework = arduino
|
||||||
|
board_build.mcu = esp32
|
||||||
|
board_build.f_cpu = 240000000L
|
||||||
|
board_build.partitions = no_ota.csv
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_filters = esp32_exception_decoder
|
||||||
|
build_flags =
|
||||||
|
-Os
|
||||||
|
lib_deps =
|
||||||
|
M5StickC
|
||||||
|
elapsedMillis
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
/**
|
||||||
|
* VictronBLE Repeater Example
|
||||||
|
*
|
||||||
|
* Collects Solar Charger data via BLE and transmits the latest
|
||||||
|
* readings over ESPNow broadcast every 5 seconds. Place this ESP32
|
||||||
|
* near Victron devices and use a separate Receiver ESP32 at a distance.
|
||||||
|
*
|
||||||
|
* ESPNow range is typically much greater than BLE (~200m+ line of sight).
|
||||||
|
*
|
||||||
|
* Setup:
|
||||||
|
* 1. Get your device encryption keys from the VictronConnect app
|
||||||
|
* 2. Update the device configurations below with your MAC and key
|
||||||
|
* 3. Flash the Receiver example on a second ESP32
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <esp_now.h>
|
||||||
|
#include "VictronBLE.h"
|
||||||
|
|
||||||
|
// ESPNow packet structure - must match Receiver
|
||||||
|
struct __attribute__((packed)) SolarChargerPacket {
|
||||||
|
uint8_t chargeState;
|
||||||
|
float batteryVoltage; // V
|
||||||
|
float batteryCurrent; // A
|
||||||
|
float panelPower; // W
|
||||||
|
uint16_t yieldToday; // Wh
|
||||||
|
float loadCurrent; // A
|
||||||
|
int8_t rssi; // BLE RSSI
|
||||||
|
char deviceName[16]; // Null-terminated, truncated
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t BROADCAST_ADDR[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||||
|
static const unsigned long SEND_INTERVAL_MS = 5000;
|
||||||
|
|
||||||
|
static uint32_t sendCount = 0;
|
||||||
|
static uint32_t sendFailCount = 0;
|
||||||
|
static uint32_t blePacketCount = 0;
|
||||||
|
|
||||||
|
// Cache latest packet per device
|
||||||
|
static const int MAX_DEVICES = 4;
|
||||||
|
static SolarChargerPacket cachedPackets[MAX_DEVICES];
|
||||||
|
static bool cachedValid[MAX_DEVICES] = {};
|
||||||
|
static int cachedCount = 0;
|
||||||
|
static unsigned long lastSendTime = 0;
|
||||||
|
|
||||||
|
VictronBLE victron;
|
||||||
|
|
||||||
|
static int findOrAddCached(const char* name) {
|
||||||
|
for (int i = 0; i < cachedCount; i++) {
|
||||||
|
if (strncmp(cachedPackets[i].deviceName, name, sizeof(cachedPackets[i].deviceName)) == 0)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
if (cachedCount < MAX_DEVICES) return cachedCount++;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onVictronData(const VictronDevice* dev) {
|
||||||
|
if (dev->deviceType != DEVICE_TYPE_SOLAR_CHARGER) return;
|
||||||
|
blePacketCount++;
|
||||||
|
const auto& s = dev->solar;
|
||||||
|
|
||||||
|
SolarChargerPacket pkt;
|
||||||
|
pkt.chargeState = s.chargeState;
|
||||||
|
pkt.batteryVoltage = s.batteryVoltage;
|
||||||
|
pkt.batteryCurrent = s.batteryCurrent;
|
||||||
|
pkt.panelPower = s.panelPower;
|
||||||
|
pkt.yieldToday = s.yieldToday;
|
||||||
|
pkt.loadCurrent = s.loadCurrent;
|
||||||
|
pkt.rssi = dev->rssi;
|
||||||
|
memset(pkt.deviceName, 0, sizeof(pkt.deviceName));
|
||||||
|
strncpy(pkt.deviceName, dev->name, sizeof(pkt.deviceName) - 1);
|
||||||
|
|
||||||
|
int idx = findOrAddCached(pkt.deviceName);
|
||||||
|
if (idx >= 0) {
|
||||||
|
cachedPackets[idx] = pkt;
|
||||||
|
cachedValid[idx] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
Serial.println("\n=== VictronBLE ESPNow Repeater ===\n");
|
||||||
|
|
||||||
|
// Init WiFi in STA mode (required for ESPNow)
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.disconnect();
|
||||||
|
|
||||||
|
Serial.print("MAC: ");
|
||||||
|
Serial.println(WiFi.macAddress());
|
||||||
|
|
||||||
|
// Init ESPNow
|
||||||
|
if (esp_now_init() != ESP_OK) {
|
||||||
|
Serial.println("ERROR: ESPNow init failed!");
|
||||||
|
while (1) delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_now_peer_info_t peerInfo = {};
|
||||||
|
memcpy(peerInfo.peer_addr, BROADCAST_ADDR, 6);
|
||||||
|
peerInfo.channel = 0;
|
||||||
|
peerInfo.encrypt = false;
|
||||||
|
|
||||||
|
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
|
||||||
|
Serial.println("ERROR: Failed to add broadcast peer!");
|
||||||
|
while (1) delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("ESPNow initialized, broadcasting on all channels");
|
||||||
|
|
||||||
|
// Init VictronBLE
|
||||||
|
if (!victron.begin(5)) {
|
||||||
|
Serial.println("ERROR: Failed to initialize VictronBLE!");
|
||||||
|
while (1) delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
victron.setDebug(false);
|
||||||
|
victron.setCallback(onVictronData);
|
||||||
|
|
||||||
|
victron.addDevice(
|
||||||
|
"Rainbow48V",
|
||||||
|
"E4:05:42:34:14:F3",
|
||||||
|
"0ec3adf7433dd61793ff2f3b8ad32ed8",
|
||||||
|
DEVICE_TYPE_SOLAR_CHARGER
|
||||||
|
);
|
||||||
|
|
||||||
|
victron.addDevice(
|
||||||
|
"ScottTrailer",
|
||||||
|
"e64559783cfb",
|
||||||
|
"3fa658aded4f309b9bc17a2318cb1f56",
|
||||||
|
DEVICE_TYPE_SOLAR_CHARGER
|
||||||
|
);
|
||||||
|
|
||||||
|
Serial.printf("Configured %d BLE devices\n", (int)victron.getDeviceCount());
|
||||||
|
Serial.printf("Packet size: %d bytes\n\n", (int)sizeof(SolarChargerPacket));
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
victron.loop(); // Blocks for scanDuration seconds
|
||||||
|
|
||||||
|
unsigned long now = millis();
|
||||||
|
if (now - lastSendTime >= SEND_INTERVAL_MS) {
|
||||||
|
lastSendTime = now;
|
||||||
|
|
||||||
|
int sent = 0;
|
||||||
|
for (int i = 0; i < cachedCount; i++) {
|
||||||
|
if (!cachedValid[i]) continue;
|
||||||
|
|
||||||
|
esp_err_t result = esp_now_send(BROADCAST_ADDR,
|
||||||
|
reinterpret_cast<const uint8_t*>(&cachedPackets[i]),
|
||||||
|
sizeof(SolarChargerPacket));
|
||||||
|
|
||||||
|
if (result == ESP_OK) {
|
||||||
|
sendCount++;
|
||||||
|
sent++;
|
||||||
|
Serial.printf("[ESPNow] Sent %s: %.2fV %.1fA %.0fW State:%d\n",
|
||||||
|
cachedPackets[i].deviceName,
|
||||||
|
cachedPackets[i].batteryVoltage,
|
||||||
|
cachedPackets[i].batteryCurrent,
|
||||||
|
cachedPackets[i].panelPower,
|
||||||
|
cachedPackets[i].chargeState);
|
||||||
|
} else {
|
||||||
|
sendFailCount++;
|
||||||
|
Serial.printf("[ESPNow] FAIL sending %s (err=%d)\n",
|
||||||
|
cachedPackets[i].deviceName, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf("[Stats] BLE pkts:%lu ESPNow sent:%lu fail:%lu devices:%d\n",
|
||||||
|
blePacketCount, sendCount, sendFailCount, cachedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+16
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "victronble",
|
"name": "victronble",
|
||||||
"version": "0.3.1",
|
"version": "0.5.0",
|
||||||
"description": "ESP32 library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements. Supports SmartSolar MPPT, SmartShunt, BMV, MultiPlus, Orion and other Victron devices.",
|
"description": "ESP32 library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements. Supports SmartSolar MPPT, SmartShunt, BMV, MultiPlus, Orion and other Victron devices.",
|
||||||
"keywords": "victron, ble, bluetooth, solar, mppt, battery, smartshunt, smartsolar, bmv, inverter, multiplus, esp32, iot, energy, monitoring",
|
"keywords": "victron, ble, bluetooth, solar, mppt, battery, smartshunt, smartsolar, bmv, inverter, multiplus, esp32, iot, energy, monitoring",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -31,6 +31,21 @@
|
|||||||
"name": "Logger",
|
"name": "Logger",
|
||||||
"base": "examples/Logger",
|
"base": "examples/Logger",
|
||||||
"files": ["src/main.cpp"]
|
"files": ["src/main.cpp"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Repeater",
|
||||||
|
"base": "examples/Repeater",
|
||||||
|
"files": ["src/main.cpp"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Receiver",
|
||||||
|
"base": "examples/Receiver",
|
||||||
|
"files": ["src/main.cpp"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FakeRepeater",
|
||||||
|
"base": "examples/FakeRepeater",
|
||||||
|
"files": ["src/main.cpp"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"export": {
|
"export": {
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
name=VictronBLE
|
name=VictronBLE
|
||||||
version=0.3.1
|
version=0.5.0
|
||||||
author=Scott Penrose
|
author=Scott Penrose
|
||||||
maintainer=Scott Penrose <scottp@dd.com.au>
|
maintainer=Scott Penrose <scottp@dd.com.au>
|
||||||
sentence=ESP32 library for reading Victron Energy device data via BLE for any ESP32
|
sentence=ESP32 library for reading Victron Energy device data via BLE for any ESP32
|
||||||
|
|||||||
+282
-521
@@ -7,659 +7,420 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "VictronBLE.h"
|
#include "VictronBLE.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
// Constructor
|
|
||||||
VictronBLE::VictronBLE()
|
VictronBLE::VictronBLE()
|
||||||
: pBLEScan(nullptr), callback(nullptr), debugEnabled(false),
|
: deviceCount(0), pBLEScan(nullptr), scanCallbackObj(nullptr),
|
||||||
scanDuration(5), initialized(false) {
|
callback(nullptr), debugEnabled(false), scanDuration(5),
|
||||||
|
minIntervalMs(1000), initialized(false) {
|
||||||
|
memset(devices, 0, sizeof(devices));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destructor
|
|
||||||
VictronBLE::~VictronBLE() {
|
|
||||||
for (auto& pair : devices) {
|
|
||||||
delete pair.second;
|
|
||||||
}
|
|
||||||
devices.clear();
|
|
||||||
|
|
||||||
if (pBLEScan) {
|
|
||||||
pBLEScan->stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize BLE
|
|
||||||
bool VictronBLE::begin(uint32_t scanDuration) {
|
bool VictronBLE::begin(uint32_t scanDuration) {
|
||||||
if (initialized) {
|
if (initialized) return true;
|
||||||
debugPrint("VictronBLE already initialized");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->scanDuration = scanDuration;
|
this->scanDuration = scanDuration;
|
||||||
|
|
||||||
debugPrint("Initializing VictronBLE...");
|
|
||||||
|
|
||||||
BLEDevice::init("VictronBLE");
|
BLEDevice::init("VictronBLE");
|
||||||
pBLEScan = BLEDevice::getScan();
|
pBLEScan = BLEDevice::getScan();
|
||||||
|
if (!pBLEScan) return false;
|
||||||
|
|
||||||
if (!pBLEScan) {
|
scanCallbackObj = new VictronBLEAdvertisedDeviceCallbacks(this);
|
||||||
lastError = "Failed to create BLE scanner";
|
pBLEScan->setAdvertisedDeviceCallbacks(scanCallbackObj, true);
|
||||||
debugPrint(lastError);
|
pBLEScan->setActiveScan(false);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pBLEScan->setAdvertisedDeviceCallbacks(new VictronBLEAdvertisedDeviceCallbacks(this), true);
|
|
||||||
pBLEScan->setActiveScan(false); // Passive scan - lower power
|
|
||||||
pBLEScan->setInterval(100);
|
pBLEScan->setInterval(100);
|
||||||
pBLEScan->setWindow(99);
|
pBLEScan->setWindow(99);
|
||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
debugPrint("VictronBLE initialized successfully");
|
if (debugEnabled) Serial.println("[VictronBLE] Initialized");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a device to monitor
|
bool VictronBLE::addDevice(const char* name, const char* mac, const char* hexKey,
|
||||||
bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
|
VictronDeviceType type) {
|
||||||
if (config.macAddress.length() == 0) {
|
if (deviceCount >= VICTRON_MAX_DEVICES) return false;
|
||||||
lastError = "MAC address cannot be empty";
|
if (!hexKey || strlen(hexKey) != 32) return false;
|
||||||
debugPrint(lastError);
|
if (!mac || strlen(mac) == 0) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.encryptionKey.length() != 32) {
|
char normalizedMAC[VICTRON_MAC_LEN];
|
||||||
lastError = "Encryption key must be 32 hex characters";
|
normalizeMAC(mac, normalizedMAC);
|
||||||
debugPrint(lastError);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String normalizedMAC = normalizeMAC(config.macAddress);
|
// Check for duplicate
|
||||||
|
if (findDevice(normalizedMAC)) return false;
|
||||||
|
|
||||||
// Check if device already exists
|
DeviceEntry* entry = &devices[deviceCount];
|
||||||
if (devices.find(normalizedMAC) != devices.end()) {
|
memset(entry, 0, sizeof(DeviceEntry));
|
||||||
debugPrint("Device " + normalizedMAC + " already exists, updating config");
|
entry->active = true;
|
||||||
delete devices[normalizedMAC];
|
|
||||||
}
|
|
||||||
|
|
||||||
DeviceInfo* info = new DeviceInfo();
|
strncpy(entry->device.name, name ? name : "", VICTRON_NAME_LEN - 1);
|
||||||
info->config = config;
|
entry->device.name[VICTRON_NAME_LEN - 1] = '\0';
|
||||||
info->config.macAddress = normalizedMAC;
|
memcpy(entry->device.mac, normalizedMAC, VICTRON_MAC_LEN);
|
||||||
|
entry->device.deviceType = type;
|
||||||
|
entry->device.rssi = -100;
|
||||||
|
|
||||||
// Convert encryption key from hex string to bytes
|
if (!hexToBytes(hexKey, entry->key, 16)) return false;
|
||||||
if (!hexStringToBytes(config.encryptionKey, info->encryptionKeyBytes, 16)) {
|
|
||||||
lastError = "Invalid encryption key format";
|
|
||||||
debugPrint(lastError);
|
|
||||||
delete info;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create appropriate data structure based on device type
|
deviceCount++;
|
||||||
info->data = createDeviceData(config.expectedType);
|
|
||||||
if (info->data) {
|
|
||||||
info->data->macAddress = normalizedMAC;
|
|
||||||
info->data->deviceName = config.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
devices[normalizedMAC] = info;
|
|
||||||
|
|
||||||
debugPrint("Added device: " + config.name + " (MAC: " + normalizedMAC + ")");
|
|
||||||
if (debugEnabled) {
|
|
||||||
debugPrint(" Original MAC input: " + config.macAddress);
|
|
||||||
debugPrint(" Stored normalized: " + normalizedMAC);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (debugEnabled) Serial.printf("[VictronBLE] Added: %s (%s)\n", name, normalizedMAC);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VictronBLE::addDevice(String name, String macAddress, String encryptionKey,
|
// Scan complete callback — sets flag so loop() restarts
|
||||||
VictronDeviceType expectedType) {
|
static bool s_scanning = false;
|
||||||
VictronDeviceConfig config(name, macAddress, encryptionKey, expectedType);
|
static void onScanDone(BLEScanResults results) {
|
||||||
return addDevice(config);
|
s_scanning = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove a device
|
|
||||||
void VictronBLE::removeDevice(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() {
|
void VictronBLE::loop() {
|
||||||
if (!initialized) {
|
if (!initialized) return;
|
||||||
return;
|
if (!s_scanning) {
|
||||||
}
|
|
||||||
|
|
||||||
// Start a scan
|
|
||||||
BLEScanResults scanResults = pBLEScan->start(scanDuration, false);
|
|
||||||
pBLEScan->clearResults();
|
pBLEScan->clearResults();
|
||||||
|
s_scanning = pBLEScan->start(scanDuration, onScanDone, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BLE callback implementation
|
// BLE scan callback
|
||||||
void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertisedDevice) {
|
void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertisedDevice) {
|
||||||
if (victronBLE) {
|
if (victronBLE) victronBLE->processDevice(advertisedDevice);
|
||||||
victronBLE->processDevice(advertisedDevice);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process advertised device
|
void VictronBLE::processDevice(BLEAdvertisedDevice& advertisedDevice) {
|
||||||
void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) {
|
if (!advertisedDevice.haveManufacturerData()) return;
|
||||||
// Get MAC address from the advertised device
|
|
||||||
String mac = macAddressToString(advertisedDevice.getAddress());
|
|
||||||
String normalizedMAC = normalizeMAC(mac);
|
|
||||||
|
|
||||||
if (debugEnabled) {
|
// getManufacturerData() returns std::string on older ESP32 BLE libraries and
|
||||||
debugPrint("Raw MAC: " + mac + " -> Normalized: " + normalizedMAC);
|
// an Arduino String on newer ones. Both expose c_str()/length(); constructing
|
||||||
}
|
// from (ptr, len) preserves the embedded null bytes present in binary data.
|
||||||
|
auto mfg = advertisedDevice.getManufacturerData();
|
||||||
|
std::string raw(mfg.c_str(), mfg.length());
|
||||||
|
if (raw.length() < 10) return;
|
||||||
|
|
||||||
// TODO: Consider skipping with no manufacturer data?
|
// Quick vendor ID check before any other work
|
||||||
memset(&manufacturerData, 0, sizeof(manufacturerData));
|
uint16_t vendorID = (uint8_t)raw[0] | ((uint8_t)raw[1] << 8);
|
||||||
if (advertisedDevice.haveManufacturerData()) {
|
if (vendorID != VICTRON_MANUFACTURER_ID) return;
|
||||||
std::string mfgData = advertisedDevice.getManufacturerData();
|
|
||||||
// XXX Storing it this way is not thread safe - is that issue on this ESP32?
|
|
||||||
debugPrint("Getting manufacturer data: Size=" + String(mfgData.length()));
|
|
||||||
mfgData.copy((char*)&manufacturerData, (mfgData.length() > sizeof(manufacturerData) ? sizeof(manufacturerData) : mfgData.length()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pointer? XXX
|
// Parse manufacturer data
|
||||||
|
victronManufacturerData mfgData;
|
||||||
|
memset(&mfgData, 0, sizeof(mfgData));
|
||||||
|
size_t copyLen = raw.length() > sizeof(mfgData) ? sizeof(mfgData) : raw.length();
|
||||||
|
raw.copy(reinterpret_cast<char*>(&mfgData), copyLen);
|
||||||
|
|
||||||
// Debug: Log all discovered BLE devices
|
// Normalize MAC and find device
|
||||||
if (debugEnabled) {
|
char normalizedMAC[VICTRON_MAC_LEN];
|
||||||
String debugMsg = "";
|
normalizeMAC(advertisedDevice.getAddress().toString().c_str(), normalizedMAC);
|
||||||
|
|
||||||
debugMsg += "BLE Device: " + mac;
|
DeviceEntry* entry = findDevice(normalizedMAC);
|
||||||
debugMsg += ", RSSI: " + String(advertisedDevice.getRSSI()) + " dBm";
|
if (!entry) {
|
||||||
if (advertisedDevice.haveName())
|
if (debugEnabled) Serial.printf("[VictronBLE] Unmonitored Victron: %s\n", normalizedMAC);
|
||||||
debugMsg += ", Name: " + String(advertisedDevice.getName().c_str());
|
|
||||||
|
|
||||||
debugMsg += ", Mfg ID: 0x" + String(manufacturerData.vendorID, HEX);
|
|
||||||
if (manufacturerData.vendorID == VICTRON_MANUFACTURER_ID) {
|
|
||||||
debugMsg += " (Victron)";
|
|
||||||
}
|
|
||||||
|
|
||||||
debugPrint(debugMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is one of our configured devices
|
|
||||||
auto it = devices.find(normalizedMAC);
|
|
||||||
if (it == devices.end()) {
|
|
||||||
// XXX Check if the device is a Victron device
|
|
||||||
// This needs lots of improvemet and only do in debug
|
|
||||||
if (manufacturerData.vendorID == VICTRON_MANUFACTURER_ID) {
|
|
||||||
debugPrint("Found unmonitored Victron Device: " + normalizeMAC(mac));
|
|
||||||
// DeviceInfo* deviceInfo = new DeviceInfo(mac, advertisedDevice.getName());
|
|
||||||
// devices.insert({normalizedMAC, deviceInfo});
|
|
||||||
// XXX What type of Victron device is it?
|
|
||||||
// Check if it's a Victron Energy device
|
|
||||||
/*
|
|
||||||
if (advertisedDevice.haveServiceData()) {
|
|
||||||
std::string serviceData = advertisedDevice.getServiceData();
|
|
||||||
if (serviceData.length() >= 2) {
|
|
||||||
uint16_t serviceId = (uint8_t)serviceData[1] << 8 | (uint8_t)serviceData[0];
|
|
||||||
if (serviceId == VICTRON_ENERGY_SERVICE_ID) {
|
|
||||||
debugPrint("Found Victron Energy Device: " + mac);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
return; // Not a device we're monitoring
|
|
||||||
}
|
|
||||||
|
|
||||||
DeviceInfo* deviceInfo = it->second;
|
|
||||||
|
|
||||||
// Check if it's Victron (manufacturer ID 0x02E1)
|
|
||||||
if (manufacturerData.vendorID != VICTRON_MANUFACTURER_ID) {
|
|
||||||
debugPrint("Skipping non VICTRON");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint("Processing data from: " + deviceInfo->config.name);
|
// Skip if nonce unchanged (data hasn't changed on the device)
|
||||||
|
if (entry->device.dataValid && mfgData.nonceDataCounter == entry->lastNonce) {
|
||||||
// Parse the advertisement
|
// Still update RSSI since we got a packet
|
||||||
if (parseAdvertisement(normalizedMAC)) {
|
entry->device.rssi = advertisedDevice.getRSSI();
|
||||||
// Update RSSI
|
return;
|
||||||
if (deviceInfo->data) {
|
|
||||||
deviceInfo->data->rssi = advertisedDevice.getRSSI();
|
|
||||||
deviceInfo->data->lastUpdate = millis();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip if minimum interval hasn't elapsed
|
||||||
|
uint32_t now = millis();
|
||||||
|
if (entry->device.dataValid && (now - entry->device.lastUpdate) < minIntervalMs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debugEnabled) Serial.printf("[VictronBLE] Processing: %s nonce:0x%04X\n",
|
||||||
|
entry->device.name, mfgData.nonceDataCounter);
|
||||||
|
|
||||||
|
if (parseAdvertisement(entry, mfgData)) {
|
||||||
|
entry->lastNonce = mfgData.nonceDataCounter;
|
||||||
|
entry->device.rssi = advertisedDevice.getRSSI();
|
||||||
|
entry->device.lastUpdate = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse advertisement data
|
bool VictronBLE::parseAdvertisement(DeviceEntry* entry, const victronManufacturerData& mfg) {
|
||||||
bool VictronBLE::parseAdvertisement(const String& macAddress) {
|
|
||||||
// XXX We already searched above - try not to again?
|
|
||||||
auto it = devices.find(macAddress);
|
|
||||||
if (it == devices.end()) {
|
|
||||||
debugPrint("parseAdvertisement: Device not found");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
DeviceInfo* deviceInfo = it->second;
|
|
||||||
|
|
||||||
if (debugEnabled) {
|
if (debugEnabled) {
|
||||||
debugPrint("Vendor ID: 0x" + String(manufacturerData.vendorID, HEX));
|
Serial.printf("[VictronBLE] Beacon:0x%02X Record:0x%02X Nonce:0x%04X\n",
|
||||||
debugPrint("Beacon Type: 0x" + String(manufacturerData.beaconType, HEX));
|
mfg.beaconType, mfg.victronRecordType, mfg.nonceDataCounter);
|
||||||
debugPrint("Record Type: 0x" + String(manufacturerData.victronRecordType, HEX));
|
|
||||||
debugPrint("Nonce: 0x" + String(manufacturerData.nonceDataCounter, HEX));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build IV (initialization vector) from nonce
|
// Quick key check before expensive decryption
|
||||||
// IV is 16 bytes: nonce (2 bytes little-endian) + zeros (14 bytes)
|
if (mfg.encryptKeyMatch != entry->key[0]) {
|
||||||
|
if (debugEnabled) Serial.println("[VictronBLE] Key byte mismatch");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build IV from nonce (2 bytes little-endian + 14 zero bytes)
|
||||||
uint8_t iv[16] = {0};
|
uint8_t iv[16] = {0};
|
||||||
iv[0] = manufacturerData.nonceDataCounter & 0xFF; // Low byte
|
iv[0] = mfg.nonceDataCounter & 0xFF;
|
||||||
iv[1] = (manufacturerData.nonceDataCounter >> 8) & 0xFF; // High byte
|
iv[1] = (mfg.nonceDataCounter >> 8) & 0xFF;
|
||||||
// Remaining bytes stay zero
|
|
||||||
|
|
||||||
// Decrypt the data
|
// Decrypt
|
||||||
uint8_t decrypted[32]; // Max expected size
|
uint8_t decrypted[VICTRON_ENCRYPTED_LEN];
|
||||||
if (!decryptAdvertisement(manufacturerData.victronEncryptedData,
|
if (!decryptData(mfg.victronEncryptedData, VICTRON_ENCRYPTED_LEN,
|
||||||
sizeof(manufacturerData.victronEncryptedData),
|
entry->key, iv, decrypted)) {
|
||||||
deviceInfo->encryptionKeyBytes, iv, decrypted)) {
|
if (debugEnabled) Serial.println("[VictronBLE] Decryption failed");
|
||||||
lastError = "Decryption failed";
|
|
||||||
debugPrint(lastError);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse based on device type
|
// Parse based on record type (auto-detects device type)
|
||||||
bool parseOk = false;
|
bool ok = false;
|
||||||
|
switch (mfg.victronRecordType) {
|
||||||
switch (manufacturerData.victronRecordType) {
|
|
||||||
case DEVICE_TYPE_SOLAR_CHARGER:
|
case DEVICE_TYPE_SOLAR_CHARGER:
|
||||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
|
entry->device.deviceType = DEVICE_TYPE_SOLAR_CHARGER;
|
||||||
parseOk = parseSolarCharger(decrypted, sizeof(decrypted),
|
ok = parseSolarCharger(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.solar);
|
||||||
*(SolarChargerData*)deviceInfo->data);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DEVICE_TYPE_BATTERY_MONITOR:
|
case DEVICE_TYPE_BATTERY_MONITOR:
|
||||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) {
|
entry->device.deviceType = DEVICE_TYPE_BATTERY_MONITOR;
|
||||||
parseOk = parseBatteryMonitor(decrypted, sizeof(decrypted),
|
ok = parseBatteryMonitor(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.battery);
|
||||||
*(BatteryMonitorData*)deviceInfo->data);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DEVICE_TYPE_INVERTER:
|
case DEVICE_TYPE_INVERTER:
|
||||||
case DEVICE_TYPE_INVERTER_RS:
|
case DEVICE_TYPE_INVERTER_RS:
|
||||||
case DEVICE_TYPE_MULTI_RS:
|
case DEVICE_TYPE_MULTI_RS:
|
||||||
case DEVICE_TYPE_VE_BUS:
|
case DEVICE_TYPE_VE_BUS:
|
||||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_INVERTER) {
|
entry->device.deviceType = DEVICE_TYPE_INVERTER;
|
||||||
parseOk = parseInverter(decrypted, sizeof(decrypted),
|
ok = parseInverter(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.inverter);
|
||||||
*(InverterData*)deviceInfo->data);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DEVICE_TYPE_DCDC_CONVERTER:
|
case DEVICE_TYPE_DCDC_CONVERTER:
|
||||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) {
|
entry->device.deviceType = DEVICE_TYPE_DCDC_CONVERTER;
|
||||||
parseOk = parseDCDCConverter(decrypted, sizeof(decrypted),
|
ok = parseDCDCConverter(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.dcdc);
|
||||||
*(DCDCConverterData*)deviceInfo->data);
|
break;
|
||||||
}
|
case DEVICE_TYPE_AC_CHARGER:
|
||||||
|
entry->device.deviceType = DEVICE_TYPE_AC_CHARGER;
|
||||||
|
ok = parseACCharger(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.acCharger);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
debugPrint("Unknown device type: 0x" + String(manufacturerData.victronRecordType, HEX));
|
if (debugEnabled) Serial.printf("[VictronBLE] Unknown type: 0x%02X\n", mfg.victronRecordType);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parseOk && deviceInfo->data) {
|
if (ok) {
|
||||||
deviceInfo->data->dataValid = true;
|
entry->device.dataValid = true;
|
||||||
|
if (callback) callback(&entry->device);
|
||||||
// Call appropriate callback
|
|
||||||
if (callback) {
|
|
||||||
switch (manufacturerData.victronRecordType) {
|
|
||||||
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;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt advertisement using AES-128-CTR
|
bool VictronBLE::decryptData(const uint8_t* encrypted, size_t len,
|
||||||
bool VictronBLE::decryptAdvertisement(const uint8_t* encrypted, size_t encLen,
|
|
||||||
const uint8_t* key, const uint8_t* iv,
|
const uint8_t* key, const uint8_t* iv,
|
||||||
uint8_t* decrypted) {
|
uint8_t* decrypted) {
|
||||||
mbedtls_aes_context aes;
|
mbedtls_aes_context aes;
|
||||||
mbedtls_aes_init(&aes);
|
mbedtls_aes_init(&aes);
|
||||||
|
|
||||||
// Set encryption key
|
if (mbedtls_aes_setkey_enc(&aes, key, 128) != 0) {
|
||||||
int ret = mbedtls_aes_setkey_enc(&aes, key, 128);
|
|
||||||
if (ret != 0) {
|
|
||||||
mbedtls_aes_free(&aes);
|
mbedtls_aes_free(&aes);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// AES-CTR decryption
|
|
||||||
size_t nc_off = 0;
|
size_t nc_off = 0;
|
||||||
uint8_t nonce_counter[16];
|
uint8_t nonce_counter[16];
|
||||||
uint8_t stream_block[16];
|
uint8_t stream_block[16];
|
||||||
|
|
||||||
memcpy(nonce_counter, iv, 16);
|
memcpy(nonce_counter, iv, 16);
|
||||||
memset(stream_block, 0, 16);
|
memset(stream_block, 0, 16);
|
||||||
|
|
||||||
ret = mbedtls_aes_crypt_ctr(&aes, encLen, &nc_off, nonce_counter,
|
int ret = mbedtls_aes_crypt_ctr(&aes, len, &nc_off, nonce_counter,
|
||||||
stream_block, encrypted, decrypted);
|
stream_block, encrypted, decrypted);
|
||||||
|
|
||||||
mbedtls_aes_free(&aes);
|
mbedtls_aes_free(&aes);
|
||||||
|
|
||||||
return (ret == 0);
|
return (ret == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse Solar Charger data
|
bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, VictronSolarData& result) {
|
||||||
bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarChargerData& result) {
|
if (len < sizeof(victronSolarChargerPayload)) return false;
|
||||||
if (len < sizeof(victronSolarChargerPayload)) {
|
const auto* p = reinterpret_cast<const victronSolarChargerPayload*>(data);
|
||||||
debugPrint("Solar charger data too short: " + String(len) + " bytes");
|
|
||||||
return false;
|
result.chargeState = p->deviceState;
|
||||||
|
result.errorCode = p->errorCode;
|
||||||
|
result.batteryVoltage = p->batteryVoltage * 0.01f; // 0.01V units
|
||||||
|
result.batteryCurrent = p->batteryCurrent * 0.1f; // 0.1A units
|
||||||
|
result.yieldToday = p->yieldToday * 10;
|
||||||
|
result.panelPower = p->inputPower;
|
||||||
|
// Load current is a 9-bit field (0.1A units); 0x1FF = no load output
|
||||||
|
uint16_t loadRaw = p->loadCurrent & 0x1FF;
|
||||||
|
result.loadCurrent = (loadRaw != 0x1FF) ? loadRaw * 0.1f : 0;
|
||||||
|
|
||||||
|
if (debugEnabled) {
|
||||||
|
Serial.printf("[VictronBLE] Solar: %.2fV %.2fA %dW State:%d\n",
|
||||||
|
result.batteryVoltage, result.batteryCurrent,
|
||||||
|
(int)result.panelPower, result.chargeState);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cast decrypted data to struct for easy access
|
|
||||||
const victronSolarChargerPayload* payload = (const victronSolarChargerPayload*)data;
|
|
||||||
|
|
||||||
// Parse charge state
|
|
||||||
result.chargeState = (SolarChargerState)payload->deviceState;
|
|
||||||
|
|
||||||
// Parse battery voltage (10 mV units -> volts)
|
|
||||||
result.batteryVoltage = payload->batteryVoltage * 0.01f;
|
|
||||||
|
|
||||||
// Parse battery current (10 mA units, signed -> amps)
|
|
||||||
result.batteryCurrent = payload->batteryCurrent * 0.01f;
|
|
||||||
|
|
||||||
// Parse yield today (10 Wh units -> Wh)
|
|
||||||
result.yieldToday = payload->yieldToday * 10;
|
|
||||||
|
|
||||||
// Parse PV power (1 W units)
|
|
||||||
result.panelPower = payload->inputPower;
|
|
||||||
|
|
||||||
// Parse load current (10 mA units -> amps, 0xFFFF = no load)
|
|
||||||
if (payload->loadCurrent != 0xFFFF) {
|
|
||||||
result.loadCurrent = payload->loadCurrent * 0.01f;
|
|
||||||
} else {
|
|
||||||
result.loadCurrent = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate PV voltage from power and current (if current > 0)
|
|
||||||
if (result.batteryCurrent > 0.1f) {
|
|
||||||
result.panelVoltage = result.panelPower / result.batteryCurrent;
|
|
||||||
} else {
|
|
||||||
result.panelVoltage = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugPrint("Solar Charger: " + String(result.batteryVoltage, 2) + "V, " +
|
|
||||||
String(result.batteryCurrent, 2) + "A, " +
|
|
||||||
String(result.panelPower) + "W, State: " + String(result.chargeState));
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse Battery Monitor data
|
bool VictronBLE::parseACCharger(const uint8_t* data, size_t len, VictronACChargerData& result) {
|
||||||
bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result) {
|
// Payload is bit-packed (10 fields, 104 bits ending in byte 12). Decode LSB-first.
|
||||||
if (len < sizeof(victronBatteryMonitorPayload)) {
|
if (len < 13) return false;
|
||||||
debugPrint("Battery monitor data too short: " + String(len) + " bytes");
|
|
||||||
return false;
|
size_t bit = 0;
|
||||||
|
auto readBits = [&](uint8_t width) -> uint32_t {
|
||||||
|
uint32_t value = 0;
|
||||||
|
for (uint8_t i = 0; i < width; i++) {
|
||||||
|
size_t b = bit + i;
|
||||||
|
value |= (uint32_t)((data[b >> 3] >> (b & 7)) & 0x01) << i;
|
||||||
|
}
|
||||||
|
bit += width;
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
result.chargeState = (uint8_t)readBits(8);
|
||||||
|
result.errorCode = (uint8_t)readBits(8);
|
||||||
|
|
||||||
|
uint32_t v1 = readBits(13), i1 = readBits(11);
|
||||||
|
uint32_t v2 = readBits(13), i2 = readBits(11);
|
||||||
|
uint32_t v3 = readBits(13), i3 = readBits(11);
|
||||||
|
uint32_t temp = readBits(7);
|
||||||
|
uint32_t acCur = readBits(9);
|
||||||
|
|
||||||
|
result.voltage1 = (v1 != 0x1FFF) ? v1 * 0.01f : 0;
|
||||||
|
result.current1 = (i1 != 0x7FF) ? i1 * 0.1f : 0;
|
||||||
|
result.voltage2 = (v2 != 0x1FFF) ? v2 * 0.01f : 0;
|
||||||
|
result.current2 = (i2 != 0x7FF) ? i2 * 0.1f : 0;
|
||||||
|
result.voltage3 = (v3 != 0x1FFF) ? v3 * 0.01f : 0;
|
||||||
|
result.current3 = (i3 != 0x7FF) ? i3 * 0.1f : 0;
|
||||||
|
result.temperature = (temp != 0x7F) ? (float)temp - 40.0f : 0; // C offset by -40
|
||||||
|
result.acCurrent = (acCur != 0x1FF) ? acCur * 0.1f : 0;
|
||||||
|
|
||||||
|
if (debugEnabled) {
|
||||||
|
Serial.printf("[VictronBLE] AC Charger: %.2fV %.2fA Temp:%.0fC State:%d\n",
|
||||||
|
result.voltage1, result.current1, result.temperature, result.chargeState);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cast decrypted data to struct for easy access
|
bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, VictronBatteryData& result) {
|
||||||
const victronBatteryMonitorPayload* payload = (const victronBatteryMonitorPayload*)data;
|
// The payload is bit-packed and not byte-aligned, so it is decoded by bit
|
||||||
|
// offset directly rather than via a struct. SOC ends at bit 117 (byte 14).
|
||||||
|
if (len < 15) return false;
|
||||||
|
|
||||||
// Parse remaining time (1 minute units)
|
// TTG (bits 0-15), unsigned minutes
|
||||||
result.remainingMinutes = payload->remainingMins;
|
result.remainingMinutes = data[0] | ((uint16_t)data[1] << 8);
|
||||||
|
|
||||||
// Parse battery voltage (10 mV units -> volts)
|
// Voltage (bits 16-31), signed, 0.01V units
|
||||||
result.voltage = payload->batteryVoltage * 0.01f;
|
result.voltage = (int16_t)(data[2] | ((uint16_t)data[3] << 8)) * 0.01f;
|
||||||
|
|
||||||
// Parse alarm bits
|
// Alarm (bits 32-47), 16-bit bitmask
|
||||||
result.alarmLowVoltage = (payload->alarms & 0x01) != 0;
|
uint16_t alarm = data[4] | ((uint16_t)data[5] << 8);
|
||||||
result.alarmHighVoltage = (payload->alarms & 0x02) != 0;
|
result.alarmLowVoltage = (alarm & 0x0001) != 0;
|
||||||
result.alarmLowSOC = (payload->alarms & 0x04) != 0;
|
result.alarmHighVoltage = (alarm & 0x0002) != 0;
|
||||||
result.alarmLowTemperature = (payload->alarms & 0x10) != 0;
|
result.alarmLowSOC = (alarm & 0x0004) != 0;
|
||||||
result.alarmHighTemperature = (payload->alarms & 0x20) != 0;
|
result.alarmLowTemperature = (alarm & 0x0010) != 0;
|
||||||
|
result.alarmHighTemperature = (alarm & 0x0020) != 0;
|
||||||
|
|
||||||
// Parse aux data: voltage (10 mV units) or temperature (0.01K units)
|
// Aux value (bits 48-63) interpreted per aux mode (bits 64-65)
|
||||||
if (payload->auxData < 3000) { // If < 30V, it's voltage
|
uint16_t auxRaw = data[6] | ((uint16_t)data[7] << 8);
|
||||||
result.auxVoltage = payload->auxData * 0.01f;
|
uint8_t auxMode = data[8] & 0x03; // 0=aux voltage, 1=midpoint, 2=temperature, 3=none
|
||||||
|
if (auxMode == 0) {
|
||||||
|
result.auxVoltage = auxRaw * 0.01f;
|
||||||
result.temperature = 0;
|
result.temperature = 0;
|
||||||
} else { // Otherwise temperature in 0.01 Kelvin
|
} else if (auxMode == 2) {
|
||||||
result.temperature = (payload->auxData * 0.01f) - 273.15f;
|
result.temperature = auxRaw * 0.01f - 273.15f; // 0.01K -> C
|
||||||
result.auxVoltage = 0;
|
result.auxVoltage = 0;
|
||||||
|
} else {
|
||||||
|
result.auxVoltage = 0;
|
||||||
|
result.temperature = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse battery current (22-bit signed, 1 mA units)
|
// Battery current (bits 66-87), 22-bit signed, 0.001A units
|
||||||
// Bits 0-7: currentLow, Bits 8-15: currentMid, Bits 16-21: low 6 bits of currentHigh_consumedLow
|
int32_t current = ((uint32_t)(data[8] >> 2) & 0x3F)
|
||||||
int32_t current = payload->currentLow |
|
| ((uint32_t)data[9] << 6)
|
||||||
(payload->currentMid << 8) |
|
| ((uint32_t)data[10] << 14);
|
||||||
((payload->currentHigh_consumedLow & 0x3F) << 16);
|
if (current & 0x200000) current |= 0xFFC00000; // Sign extend 22-bit
|
||||||
// Sign extend from 22 bits to 32 bits
|
result.current = current * 0.001f;
|
||||||
if (current & 0x200000) {
|
|
||||||
current |= 0xFFC00000;
|
// Consumed Ah (bits 88-107), 20-bit, stored as a positive count, 0.1Ah units.
|
||||||
|
// Reported as a negative value (amp-hours consumed).
|
||||||
|
uint32_t consumed = (uint32_t)data[11]
|
||||||
|
| ((uint32_t)data[12] << 8)
|
||||||
|
| ((uint32_t)(data[13] & 0x0F) << 16);
|
||||||
|
result.consumedAh = -((float)consumed * 0.1f);
|
||||||
|
|
||||||
|
// SOC (bits 108-117), 10-bit, 0.1% units
|
||||||
|
uint16_t soc = ((uint16_t)(data[13] >> 4) | ((uint16_t)data[14] << 4)) & 0x3FF;
|
||||||
|
result.soc = soc * 0.1f;
|
||||||
|
|
||||||
|
if (debugEnabled) {
|
||||||
|
Serial.printf("[VictronBLE] Battery: %.2fV %.2fA SOC:%.1f%%\n",
|
||||||
|
result.voltage, result.current, result.soc);
|
||||||
}
|
}
|
||||||
result.current = current * 0.001f; // Convert mA to A
|
|
||||||
|
|
||||||
// Parse consumed Ah (18-bit signed, 10 mAh units)
|
|
||||||
// Bits 0-1: high 2 bits of currentHigh_consumedLow, Bits 2-9: consumedMid, Bits 10-17: consumedHigh
|
|
||||||
int32_t consumedAh = ((payload->currentHigh_consumedLow & 0xC0) >> 6) |
|
|
||||||
(payload->consumedMid << 2) |
|
|
||||||
(payload->consumedHigh << 10);
|
|
||||||
// Sign extend from 18 bits to 32 bits
|
|
||||||
if (consumedAh & 0x20000) {
|
|
||||||
consumedAh |= 0xFFFC0000;
|
|
||||||
}
|
|
||||||
result.consumedAh = consumedAh * 0.01f; // Convert 10mAh to Ah
|
|
||||||
|
|
||||||
// Parse SOC (10-bit value, 10 = 1.0%)
|
|
||||||
result.soc = (payload->soc & 0x3FF) * 0.1f;
|
|
||||||
|
|
||||||
debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " +
|
|
||||||
String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%");
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse Inverter data
|
bool VictronBLE::parseInverter(const uint8_t* data, size_t len, VictronInverterData& result) {
|
||||||
bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& result) {
|
if (len < sizeof(victronInverterPayload)) return false;
|
||||||
if (len < sizeof(victronInverterPayload)) {
|
const auto* p = reinterpret_cast<const victronInverterPayload*>(data);
|
||||||
debugPrint("Inverter data too short: " + String(len) + " bytes");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cast decrypted data to struct for easy access
|
result.state = p->deviceState;
|
||||||
const victronInverterPayload* payload = (const victronInverterPayload*)data;
|
result.batteryVoltage = p->batteryVoltage * 0.01f;
|
||||||
|
result.batteryCurrent = p->batteryCurrent * 0.01f;
|
||||||
|
|
||||||
// Parse device state
|
// AC Power (signed 24-bit)
|
||||||
result.state = payload->deviceState;
|
int32_t acPower = p->acPowerLow | (p->acPowerMid << 8) | (p->acPowerHigh << 16);
|
||||||
|
if (acPower & 0x800000) acPower |= 0xFF000000; // Sign extend
|
||||||
// Parse battery voltage (10 mV units -> volts)
|
|
||||||
result.batteryVoltage = payload->batteryVoltage * 0.01f;
|
|
||||||
|
|
||||||
// Parse battery current (10 mA units, signed -> amps)
|
|
||||||
result.batteryCurrent = payload->batteryCurrent * 0.01f;
|
|
||||||
|
|
||||||
// Parse AC Power (signed 24-bit, 1 W units)
|
|
||||||
int32_t acPower = payload->acPowerLow |
|
|
||||||
(payload->acPowerMid << 8) |
|
|
||||||
(payload->acPowerHigh << 16);
|
|
||||||
// Sign extend from 24 bits to 32 bits
|
|
||||||
if (acPower & 0x800000) {
|
|
||||||
acPower |= 0xFF000000;
|
|
||||||
}
|
|
||||||
result.acPower = acPower;
|
result.acPower = acPower;
|
||||||
|
|
||||||
// Parse alarm bits
|
// Alarm bits
|
||||||
result.alarmLowVoltage = (payload->alarms & 0x01) != 0;
|
result.alarmLowVoltage = (p->alarms & 0x01) != 0;
|
||||||
result.alarmHighVoltage = (payload->alarms & 0x02) != 0;
|
result.alarmHighVoltage = (p->alarms & 0x02) != 0;
|
||||||
result.alarmHighTemperature = (payload->alarms & 0x04) != 0;
|
result.alarmHighTemperature = (p->alarms & 0x04) != 0;
|
||||||
result.alarmOverload = (payload->alarms & 0x08) != 0;
|
result.alarmOverload = (p->alarms & 0x08) != 0;
|
||||||
|
|
||||||
debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " +
|
|
||||||
String(result.acPower) + "W, State: " + String(result.state));
|
|
||||||
|
|
||||||
|
if (debugEnabled) {
|
||||||
|
Serial.printf("[VictronBLE] Inverter: %.2fV %dW State:%d\n",
|
||||||
|
result.batteryVoltage, (int)result.acPower, result.state);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse DC-DC Converter data
|
bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, VictronDCDCData& result) {
|
||||||
bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result) {
|
if (len < sizeof(victronDCDCConverterPayload)) return false;
|
||||||
if (len < sizeof(victronDCDCConverterPayload)) {
|
const auto* p = reinterpret_cast<const victronDCDCConverterPayload*>(data);
|
||||||
debugPrint("DC-DC converter data too short: " + String(len) + " bytes");
|
|
||||||
return false;
|
result.chargeState = p->chargeState;
|
||||||
|
result.errorCode = p->errorCode;
|
||||||
|
result.inputVoltage = p->inputVoltage * 0.01f;
|
||||||
|
result.outputVoltage = p->outputVoltage * 0.01f;
|
||||||
|
result.outputCurrent = p->outputCurrent * 0.01f;
|
||||||
|
|
||||||
|
if (debugEnabled) {
|
||||||
|
Serial.printf("[VictronBLE] DC-DC: In=%.2fV Out=%.2fV %.2fA\n",
|
||||||
|
result.inputVoltage, result.outputVoltage, result.outputCurrent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cast decrypted data to struct for easy access
|
|
||||||
const victronDCDCConverterPayload* payload = (const victronDCDCConverterPayload*)data;
|
|
||||||
|
|
||||||
// Parse charge state
|
|
||||||
result.chargeState = payload->chargeState;
|
|
||||||
|
|
||||||
// Parse error code
|
|
||||||
result.errorCode = payload->errorCode;
|
|
||||||
|
|
||||||
// Parse input voltage (10 mV units -> volts)
|
|
||||||
result.inputVoltage = payload->inputVoltage * 0.01f;
|
|
||||||
|
|
||||||
// Parse output voltage (10 mV units -> volts)
|
|
||||||
result.outputVoltage = payload->outputVoltage * 0.01f;
|
|
||||||
|
|
||||||
// Parse output current (10 mA units -> amps)
|
|
||||||
result.outputCurrent = payload->outputCurrent * 0.01f;
|
|
||||||
|
|
||||||
debugPrint("DC-DC Converter: In=" + String(result.inputVoltage, 2) + "V, Out=" +
|
|
||||||
String(result.outputVoltage, 2) + "V, " + String(result.outputCurrent, 2) + "A");
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get data methods
|
// --- Helpers ---
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
bool VictronBLE::hexToBytes(const char* hex, uint8_t* out, size_t len) {
|
||||||
|
if (strlen(hex) != len * 2) return false;
|
||||||
for (size_t i = 0; i < len; i++) {
|
for (size_t i = 0; i < len; i++) {
|
||||||
String byteStr = hex.substring(i * 2, i * 2 + 2);
|
uint8_t hi = hex[i * 2], lo = hex[i * 2 + 1];
|
||||||
char* endPtr;
|
if (hi >= '0' && hi <= '9') hi -= '0';
|
||||||
bytes[i] = strtoul(byteStr.c_str(), &endPtr, 16);
|
else if (hi >= 'a' && hi <= 'f') hi = hi - 'a' + 10;
|
||||||
if (*endPtr != '\0') {
|
else if (hi >= 'A' && hi <= 'F') hi = hi - 'A' + 10;
|
||||||
return false;
|
else return false;
|
||||||
|
if (lo >= '0' && lo <= '9') lo -= '0';
|
||||||
|
else if (lo >= 'a' && lo <= 'f') lo = lo - 'a' + 10;
|
||||||
|
else if (lo >= 'A' && lo <= 'F') lo = lo - 'A' + 10;
|
||||||
|
else return false;
|
||||||
|
out[i] = (hi << 4) | lo;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: MAC address to string
|
void VictronBLE::normalizeMAC(const char* input, char* output) {
|
||||||
String VictronBLE::macAddressToString(BLEAddress address) {
|
int j = 0;
|
||||||
// Use the BLEAddress toString() method which provides consistent formatting
|
for (int i = 0; input[i] && j < VICTRON_MAC_LEN - 1; i++) {
|
||||||
return String(address.toString().c_str());
|
char c = input[i];
|
||||||
|
if (c == ':' || c == '-') continue;
|
||||||
|
output[j++] = (c >= 'A' && c <= 'F') ? (c + 32) : c;
|
||||||
|
}
|
||||||
|
output[j] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: Normalize MAC address format
|
VictronBLE::DeviceEntry* VictronBLE::findDevice(const char* normalizedMAC) {
|
||||||
String VictronBLE::normalizeMAC(String mac) {
|
for (size_t i = 0; i < deviceCount; i++) {
|
||||||
String normalized = mac;
|
if (devices[i].active && strcmp(devices[i].device.mac, normalizedMAC) == 0) {
|
||||||
normalized.toLowerCase();
|
return &devices[i];
|
||||||
// XXX - is this right, was - to : but not consistent location of pairs or not
|
|
||||||
normalized.replace("-", "");
|
|
||||||
normalized.replace(":", "");
|
|
||||||
return normalized;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Debug helpers
|
return nullptr;
|
||||||
void VictronBLE::debugPrint(const String& message) {
|
|
||||||
if (debugEnabled)
|
|
||||||
Serial.println("[VictronBLE] " + message);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+204
-234
@@ -15,14 +15,16 @@
|
|||||||
#include <BLEDevice.h>
|
#include <BLEDevice.h>
|
||||||
#include <BLEAdvertisedDevice.h>
|
#include <BLEAdvertisedDevice.h>
|
||||||
#include <BLEScan.h>
|
#include <BLEScan.h>
|
||||||
#include <map>
|
|
||||||
#include <vector>
|
|
||||||
#include "mbedtls/aes.h"
|
#include "mbedtls/aes.h"
|
||||||
|
|
||||||
// Victron manufacturer ID
|
// --- Constants ---
|
||||||
#define VICTRON_MANUFACTURER_ID 0x02E1
|
static constexpr uint16_t VICTRON_MANUFACTURER_ID = 0x02E1;
|
||||||
|
static constexpr int VICTRON_MAX_DEVICES = 8;
|
||||||
|
static constexpr int VICTRON_MAC_LEN = 13; // 12 hex chars + null
|
||||||
|
static constexpr int VICTRON_NAME_LEN = 32;
|
||||||
|
static constexpr int VICTRON_ENCRYPTED_LEN = 21;
|
||||||
|
|
||||||
// Device type IDs from Victron protocol
|
// --- Device type IDs from Victron protocol ---
|
||||||
enum VictronDeviceType {
|
enum VictronDeviceType {
|
||||||
DEVICE_TYPE_UNKNOWN = 0x00,
|
DEVICE_TYPE_UNKNOWN = 0x00,
|
||||||
DEVICE_TYPE_SOLAR_CHARGER = 0x01,
|
DEVICE_TYPE_SOLAR_CHARGER = 0x01,
|
||||||
@@ -31,14 +33,17 @@ enum VictronDeviceType {
|
|||||||
DEVICE_TYPE_DCDC_CONVERTER = 0x04,
|
DEVICE_TYPE_DCDC_CONVERTER = 0x04,
|
||||||
DEVICE_TYPE_SMART_LITHIUM = 0x05,
|
DEVICE_TYPE_SMART_LITHIUM = 0x05,
|
||||||
DEVICE_TYPE_INVERTER_RS = 0x06,
|
DEVICE_TYPE_INVERTER_RS = 0x06,
|
||||||
DEVICE_TYPE_SMART_BATTERY_PROTECT = 0x07,
|
DEVICE_TYPE_GX_DEVICE = 0x07,
|
||||||
DEVICE_TYPE_LYNX_SMART_BMS = 0x08,
|
DEVICE_TYPE_AC_CHARGER = 0x08,
|
||||||
DEVICE_TYPE_MULTI_RS = 0x09,
|
DEVICE_TYPE_SMART_BATTERY_PROTECT = 0x09,
|
||||||
DEVICE_TYPE_VE_BUS = 0x0A,
|
DEVICE_TYPE_LYNX_SMART_BMS = 0x0A,
|
||||||
DEVICE_TYPE_DC_ENERGY_METER = 0x0B
|
DEVICE_TYPE_MULTI_RS = 0x0B,
|
||||||
|
DEVICE_TYPE_VE_BUS = 0x0C,
|
||||||
|
DEVICE_TYPE_DC_ENERGY_METER = 0x0D,
|
||||||
|
DEVICE_TYPE_ORION_XS = 0x0F
|
||||||
};
|
};
|
||||||
|
|
||||||
// Device state for Solar Charger
|
// --- Device state for Solar Charger ---
|
||||||
enum SolarChargerState {
|
enum SolarChargerState {
|
||||||
CHARGER_OFF = 0,
|
CHARGER_OFF = 0,
|
||||||
CHARGER_LOW_POWER = 1,
|
CHARGER_LOW_POWER = 1,
|
||||||
@@ -53,162 +58,224 @@ enum SolarChargerState {
|
|||||||
CHARGER_EXTERNAL_CONTROL = 252
|
CHARGER_EXTERNAL_CONTROL = 252
|
||||||
};
|
};
|
||||||
|
|
||||||
// Binary data structures for decoding BLE advertisements
|
// ============================================================
|
||||||
// Must use __attribute__((packed)) to prevent compiler padding
|
// Wire-format packed structures for decoding BLE advertisements
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
// Manufacturer data structure (outer envelope)
|
struct victronManufacturerData {
|
||||||
typedef struct {
|
uint16_t vendorID;
|
||||||
uint16_t vendorID; // vendor ID
|
uint8_t beaconType; // 0x10 = Product Advertisement
|
||||||
uint8_t beaconType; // Should be 0x10 (Product Advertisement) for the packets we want
|
uint8_t unknownData1[3];
|
||||||
uint8_t unknownData1[3]; // Unknown data
|
uint8_t victronRecordType; // Device type (see VictronDeviceType)
|
||||||
uint8_t victronRecordType; // Should be 0x01 (Solar Charger) for the packets we want
|
uint16_t nonceDataCounter;
|
||||||
uint16_t nonceDataCounter; // Nonce
|
uint8_t encryptKeyMatch; // Should match encryption key byte 0
|
||||||
uint8_t encryptKeyMatch; // Should match pre-shared encryption key byte 0
|
uint8_t victronEncryptedData[VICTRON_ENCRYPTED_LEN];
|
||||||
uint8_t victronEncryptedData[21]; // (31 bytes max per BLE spec - size of previous elements)
|
} __attribute__((packed));
|
||||||
uint8_t nullPad; // extra byte because toCharArray() adds a \0 byte.
|
|
||||||
} __attribute__((packed)) victronManufacturerData;
|
|
||||||
// Decrypted payload structures for each device type
|
|
||||||
|
|
||||||
// Solar Charger decrypted payload
|
struct victronSolarChargerPayload {
|
||||||
typedef struct {
|
uint8_t deviceState;
|
||||||
uint8_t deviceState; // Charge state (SolarChargerState enum)
|
uint8_t errorCode;
|
||||||
uint8_t errorCode; // Error code
|
int16_t batteryVoltage; // 0.01V units (signed)
|
||||||
int16_t batteryVoltage; // Battery voltage in 10mV units
|
int16_t batteryCurrent; // 0.1A units (signed)
|
||||||
int16_t batteryCurrent; // Battery current in 10mA units (signed)
|
uint16_t yieldToday; // 0.01kWh (10Wh) units
|
||||||
uint16_t yieldToday; // Yield today in 10Wh units
|
uint16_t inputPower; // 1W units
|
||||||
uint16_t inputPower; // PV power in 1W units
|
uint16_t loadCurrent; // 9-bit field, 0.1A units (0x1FF = no load)
|
||||||
uint16_t loadCurrent; // Load current in 10mA units (0xFFFF = no load)
|
uint8_t reserved[2];
|
||||||
uint8_t reserved[2]; // Reserved bytes
|
} __attribute__((packed));
|
||||||
} __attribute__((packed)) victronSolarChargerPayload;
|
|
||||||
|
|
||||||
// Battery Monitor decrypted payload
|
// NOTE: The battery monitor payload is bit-packed (16-bit alarm, 2-bit aux mode,
|
||||||
typedef struct {
|
// 22-bit current, 20-bit consumed Ah, 10-bit SOC) and does NOT byte-align, so it
|
||||||
uint16_t remainingMins; // Time remaining in minutes
|
// is decoded by bit offset directly in parseBatteryMonitor() rather than a struct.
|
||||||
uint16_t batteryVoltage; // Battery voltage in 10mV units
|
|
||||||
uint8_t alarms; // Alarm bits
|
|
||||||
uint16_t auxData; // Aux voltage (10mV) or temperature (0.01K)
|
|
||||||
uint8_t currentLow; // Battery current bits 0-7
|
|
||||||
uint8_t currentMid; // Battery current bits 8-15
|
|
||||||
uint8_t currentHigh_consumedLow; // Current bits 16-21 (low 6 bits), consumed bits 0-1 (high 2 bits)
|
|
||||||
uint8_t consumedMid; // Consumed Ah bits 2-9
|
|
||||||
uint8_t consumedHigh; // Consumed Ah bits 10-17
|
|
||||||
uint16_t soc; // State of charge in 0.1% units (10-bit value)
|
|
||||||
uint8_t reserved[2]; // Reserved bytes
|
|
||||||
} __attribute__((packed)) victronBatteryMonitorPayload;
|
|
||||||
|
|
||||||
// Inverter decrypted payload
|
struct victronInverterPayload {
|
||||||
typedef struct {
|
uint8_t deviceState;
|
||||||
uint8_t deviceState; // Device state
|
uint8_t errorCode;
|
||||||
uint8_t errorCode; // Error code
|
uint16_t batteryVoltage; // 10mV units
|
||||||
uint16_t batteryVoltage; // Battery voltage in 10mV units
|
int16_t batteryCurrent; // 10mA units (signed)
|
||||||
int16_t batteryCurrent; // Battery current in 10mA units (signed)
|
uint8_t acPowerLow;
|
||||||
uint8_t acPowerLow; // AC Power bits 0-7
|
uint8_t acPowerMid;
|
||||||
uint8_t acPowerMid; // AC Power bits 8-15
|
uint8_t acPowerHigh; // Signed 24-bit
|
||||||
uint8_t acPowerHigh; // AC Power bits 16-23 (signed 24-bit)
|
uint8_t alarms;
|
||||||
uint8_t alarms; // Alarm bits
|
uint8_t reserved[4];
|
||||||
uint8_t reserved[4]; // Reserved bytes
|
} __attribute__((packed));
|
||||||
} __attribute__((packed)) victronInverterPayload;
|
|
||||||
|
|
||||||
// DC-DC Converter decrypted payload
|
struct victronDCDCConverterPayload {
|
||||||
typedef struct {
|
uint8_t chargeState;
|
||||||
uint8_t chargeState; // Charge state
|
uint8_t errorCode;
|
||||||
uint8_t errorCode; // Error code
|
uint16_t inputVoltage; // 10mV units
|
||||||
uint16_t inputVoltage; // Input voltage in 10mV units
|
uint16_t outputVoltage; // 10mV units
|
||||||
uint16_t outputVoltage; // Output voltage in 10mV units
|
uint16_t outputCurrent; // 10mA units
|
||||||
uint16_t outputCurrent; // Output current in 10mA units
|
uint8_t reserved[6];
|
||||||
uint8_t reserved[6]; // Reserved bytes
|
} __attribute__((packed));
|
||||||
} __attribute__((packed)) victronDCDCConverterPayload;
|
|
||||||
|
|
||||||
// Base structure for all device data
|
// ============================================================
|
||||||
struct VictronDeviceData {
|
// Parsed data structures (flat, no inheritance)
|
||||||
String deviceName;
|
// ============================================================
|
||||||
String macAddress;
|
|
||||||
VictronDeviceType deviceType;
|
|
||||||
int8_t rssi;
|
|
||||||
uint32_t lastUpdate;
|
|
||||||
bool dataValid;
|
|
||||||
|
|
||||||
VictronDeviceData() : deviceType(DEVICE_TYPE_UNKNOWN), rssi(-100),
|
struct VictronSolarData {
|
||||||
lastUpdate(0), dataValid(false) {}
|
uint8_t chargeState; // SolarChargerState enum
|
||||||
};
|
uint8_t errorCode;
|
||||||
|
|
||||||
// Solar Charger specific data
|
|
||||||
struct SolarChargerData : public VictronDeviceData {
|
|
||||||
SolarChargerState chargeState;
|
|
||||||
float batteryVoltage; // V
|
float batteryVoltage; // V
|
||||||
float batteryCurrent; // A
|
float batteryCurrent; // A
|
||||||
float panelVoltage; // V (PV voltage)
|
|
||||||
float panelPower; // W
|
float panelPower; // W
|
||||||
uint16_t yieldToday; // Wh
|
uint16_t yieldToday; // Wh
|
||||||
float loadCurrent; // A
|
float loadCurrent; // A
|
||||||
|
|
||||||
SolarChargerData() : chargeState(CHARGER_OFF), batteryVoltage(0),
|
|
||||||
batteryCurrent(0), panelVoltage(0), panelPower(0),
|
|
||||||
yieldToday(0), loadCurrent(0) {
|
|
||||||
deviceType = DEVICE_TYPE_SOLAR_CHARGER;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Battery Monitor/SmartShunt specific data
|
struct VictronACChargerData {
|
||||||
struct BatteryMonitorData : public VictronDeviceData {
|
uint8_t chargeState; // SolarChargerState enum (shared charger states)
|
||||||
|
uint8_t errorCode;
|
||||||
|
float voltage1; // V (output 1)
|
||||||
|
float current1; // A (output 1)
|
||||||
|
float voltage2; // V (output 2, 0 if absent)
|
||||||
|
float current2; // A (output 2, 0 if absent)
|
||||||
|
float voltage3; // V (output 3, 0 if absent)
|
||||||
|
float current3; // A (output 3, 0 if absent)
|
||||||
|
float temperature; // C (0 if not available)
|
||||||
|
float acCurrent; // A (0 if not available)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VictronBatteryData {
|
||||||
float voltage; // V
|
float voltage; // V
|
||||||
float current; // A (positive = charging, negative = discharging)
|
float current; // A
|
||||||
float temperature; // °C
|
float temperature; // C (0 if aux is voltage)
|
||||||
float auxVoltage; // V (starter battery or midpoint)
|
float auxVoltage; // V (0 if aux is temperature)
|
||||||
uint16_t remainingMinutes; // Minutes
|
uint16_t remainingMinutes;
|
||||||
float consumedAh; // Ah
|
float consumedAh; // Ah
|
||||||
float soc; // State of Charge %
|
float soc; // %
|
||||||
bool alarmLowVoltage;
|
bool alarmLowVoltage;
|
||||||
bool alarmHighVoltage;
|
bool alarmHighVoltage;
|
||||||
bool alarmLowSOC;
|
bool alarmLowSOC;
|
||||||
bool alarmLowTemperature;
|
bool alarmLowTemperature;
|
||||||
bool alarmHighTemperature;
|
bool alarmHighTemperature;
|
||||||
|
|
||||||
BatteryMonitorData() : voltage(0), current(0), temperature(0),
|
|
||||||
auxVoltage(0), remainingMinutes(0), consumedAh(0),
|
|
||||||
soc(0), alarmLowVoltage(false), alarmHighVoltage(false),
|
|
||||||
alarmLowSOC(false), alarmLowTemperature(false),
|
|
||||||
alarmHighTemperature(false) {
|
|
||||||
deviceType = DEVICE_TYPE_BATTERY_MONITOR;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Inverter specific data
|
struct VictronInverterData {
|
||||||
struct InverterData : public VictronDeviceData {
|
|
||||||
float batteryVoltage; // V
|
float batteryVoltage; // V
|
||||||
float batteryCurrent; // A
|
float batteryCurrent; // A
|
||||||
float acPower; // W
|
float acPower; // W
|
||||||
uint8_t state; // Inverter state
|
uint8_t state;
|
||||||
bool alarmHighVoltage;
|
|
||||||
bool alarmLowVoltage;
|
bool alarmLowVoltage;
|
||||||
|
bool alarmHighVoltage;
|
||||||
bool alarmHighTemperature;
|
bool alarmHighTemperature;
|
||||||
bool alarmOverload;
|
bool alarmOverload;
|
||||||
|
|
||||||
InverterData() : batteryVoltage(0), batteryCurrent(0), acPower(0),
|
|
||||||
state(0), alarmHighVoltage(false), alarmLowVoltage(false),
|
|
||||||
alarmHighTemperature(false), alarmOverload(false) {
|
|
||||||
deviceType = DEVICE_TYPE_INVERTER;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// DC-DC Converter specific data
|
struct VictronDCDCData {
|
||||||
struct DCDCConverterData : public VictronDeviceData {
|
|
||||||
float inputVoltage; // V
|
float inputVoltage; // V
|
||||||
float outputVoltage; // V
|
float outputVoltage; // V
|
||||||
float outputCurrent; // A
|
float outputCurrent; // A
|
||||||
uint8_t chargeState;
|
uint8_t chargeState;
|
||||||
uint8_t errorCode;
|
uint8_t errorCode;
|
||||||
|
|
||||||
DCDCConverterData() : inputVoltage(0), outputVoltage(0), outputCurrent(0),
|
|
||||||
chargeState(0), errorCode(0) {
|
|
||||||
deviceType = DEVICE_TYPE_DCDC_CONVERTER;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Forward declaration
|
// ============================================================
|
||||||
class VictronBLE;
|
// Main device struct with tagged union
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
// Callback interface for device data updates
|
struct VictronDevice {
|
||||||
|
char name[VICTRON_NAME_LEN];
|
||||||
|
char mac[VICTRON_MAC_LEN];
|
||||||
|
VictronDeviceType deviceType;
|
||||||
|
int8_t rssi;
|
||||||
|
uint32_t lastUpdate;
|
||||||
|
bool dataValid;
|
||||||
|
union {
|
||||||
|
VictronSolarData solar;
|
||||||
|
VictronBatteryData battery;
|
||||||
|
VictronInverterData inverter;
|
||||||
|
VictronDCDCData dcdc;
|
||||||
|
VictronACChargerData acCharger;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Callback — simple function pointer
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
typedef void (*VictronCallback)(const VictronDevice* device);
|
||||||
|
|
||||||
|
// Forward declaration
|
||||||
|
class VictronBLEAdvertisedDeviceCallbacks;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Main VictronBLE class
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
class VictronBLE {
|
||||||
|
public:
|
||||||
|
VictronBLE();
|
||||||
|
|
||||||
|
bool begin(uint32_t scanDuration = 5);
|
||||||
|
bool addDevice(const char* name, const char* mac, const char* hexKey,
|
||||||
|
VictronDeviceType type = DEVICE_TYPE_UNKNOWN);
|
||||||
|
void setCallback(VictronCallback cb) { callback = cb; }
|
||||||
|
void setDebug(bool enable) { debugEnabled = enable; }
|
||||||
|
void setMinInterval(uint32_t ms) { minIntervalMs = ms; }
|
||||||
|
size_t getDeviceCount() const { return deviceCount; }
|
||||||
|
void loop();
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class VictronBLEAdvertisedDeviceCallbacks;
|
||||||
|
|
||||||
|
struct DeviceEntry {
|
||||||
|
VictronDevice device;
|
||||||
|
uint8_t key[16];
|
||||||
|
uint16_t lastNonce;
|
||||||
|
bool active;
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceEntry devices[VICTRON_MAX_DEVICES];
|
||||||
|
size_t deviceCount;
|
||||||
|
BLEScan* pBLEScan;
|
||||||
|
VictronBLEAdvertisedDeviceCallbacks* scanCallbackObj;
|
||||||
|
VictronCallback callback;
|
||||||
|
bool debugEnabled;
|
||||||
|
uint32_t scanDuration;
|
||||||
|
uint32_t minIntervalMs;
|
||||||
|
bool initialized;
|
||||||
|
|
||||||
|
static bool hexToBytes(const char* hex, uint8_t* out, size_t len);
|
||||||
|
static void normalizeMAC(const char* input, char* output);
|
||||||
|
DeviceEntry* findDevice(const char* normalizedMAC);
|
||||||
|
bool decryptData(const uint8_t* encrypted, size_t len,
|
||||||
|
const uint8_t* key, const uint8_t* iv, uint8_t* decrypted);
|
||||||
|
void processDevice(BLEAdvertisedDevice& dev);
|
||||||
|
bool parseAdvertisement(DeviceEntry* entry, const victronManufacturerData& mfg);
|
||||||
|
bool parseSolarCharger(const uint8_t* data, size_t len, VictronSolarData& result);
|
||||||
|
bool parseACCharger(const uint8_t* data, size_t len, VictronACChargerData& result);
|
||||||
|
bool parseBatteryMonitor(const uint8_t* data, size_t len, VictronBatteryData& result);
|
||||||
|
bool parseInverter(const uint8_t* data, size_t len, VictronInverterData& result);
|
||||||
|
bool parseDCDCConverter(const uint8_t* data, size_t len, VictronDCDCData& result);
|
||||||
|
};
|
||||||
|
|
||||||
|
// BLE scan callback (required by ESP32 BLE API)
|
||||||
|
class VictronBLEAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
|
||||||
|
public:
|
||||||
|
VictronBLEAdvertisedDeviceCallbacks(VictronBLE* parent) : victronBLE(parent) {}
|
||||||
|
void onResult(BLEAdvertisedDevice advertisedDevice) override;
|
||||||
|
private:
|
||||||
|
VictronBLE* victronBLE;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Commented-out features — kept for reference / future use
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
|
||||||
|
// --- VictronDeviceConfig (use addDevice(name, mac, key, type) directly) ---
|
||||||
|
struct VictronDeviceConfig {
|
||||||
|
String name;
|
||||||
|
String macAddress;
|
||||||
|
String encryptionKey;
|
||||||
|
VictronDeviceType expectedType;
|
||||||
|
VictronDeviceConfig() : expectedType(DEVICE_TYPE_UNKNOWN) {}
|
||||||
|
VictronDeviceConfig(const String& n, const String& mac, const String& key, VictronDeviceType type = DEVICE_TYPE_UNKNOWN)
|
||||||
|
: name(n), macAddress(mac), encryptionKey(key), expectedType(type) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Virtual callback interface (replaced by function pointer VictronCallback) ---
|
||||||
class VictronDeviceCallback {
|
class VictronDeviceCallback {
|
||||||
public:
|
public:
|
||||||
virtual ~VictronDeviceCallback() {}
|
virtual ~VictronDeviceCallback() {}
|
||||||
@@ -218,114 +285,17 @@ public:
|
|||||||
virtual void onDCDCConverterData(const DCDCConverterData& data) {}
|
virtual void onDCDCConverterData(const DCDCConverterData& data) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Device configuration structure
|
// --- Per-type getter methods (use callback instead) ---
|
||||||
struct VictronDeviceConfig {
|
bool getSolarChargerData(const String& macAddress, SolarChargerData& data);
|
||||||
String name;
|
bool getBatteryMonitorData(const String& macAddress, BatteryMonitorData& data);
|
||||||
String macAddress;
|
bool getInverterData(const String& macAddress, InverterData& data);
|
||||||
String encryptionKey; // 32 character hex string
|
bool getDCDCConverterData(const String& macAddress, DCDCConverterData& data);
|
||||||
VictronDeviceType expectedType;
|
|
||||||
|
|
||||||
VictronDeviceConfig() : expectedType(DEVICE_TYPE_UNKNOWN) {}
|
// --- Other removed methods ---
|
||||||
VictronDeviceConfig(String n, String mac, String key, VictronDeviceType type = DEVICE_TYPE_UNKNOWN)
|
void removeDevice(const String& macAddress);
|
||||||
: 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);
|
std::vector<String> getDevicesByType(VictronDeviceType type);
|
||||||
|
String getLastError() const;
|
||||||
|
|
||||||
// Enable/disable debug output
|
#endif // commented-out features
|
||||||
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;
|
|
||||||
|
|
||||||
// XXX Experiment with actual victron data
|
|
||||||
victronManufacturerData manufacturerData;
|
|
||||||
|
|
||||||
// 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 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);
|
|
||||||
|
|
||||||
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
|
#endif // VICTRON_BLE_H
|
||||||
|
|||||||
Reference in New Issue
Block a user