Compare commits
14 Commits
8a4e010324
..
v0.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 97e57b0306 | |||
| 05ee88cd31 | |||
| ef50829c81 | |||
| 53fa6fa0e1 | |||
| de6607d870 | |||
| 105d66b07a | |||
| cd3b4626d1 | |||
| 7706f88399 | |||
| e7024d9983 | |||
| 261cc0d1fe | |||
| 39a89c816c | |||
| 4944757903 | |||
| 31765c7ac8 | |||
| 84d153c9a8 |
+151
-1
@@ -1,7 +1,7 @@
|
||||
# VictronBLE Project Context
|
||||
|
||||
## Project Overview
|
||||
Arduino/ESP32 library for reading Victron Energy devices via Bluetooth Low Energy (BLE).
|
||||
Portable Arduino library for reading Victron Energy devices via Bluetooth Low Energy (BLE). Runs on ESP32 (Bluedroid) and nRF52840 (Bluefruit); BLE scanning is the only platform-specific code (src/esp32, src/nrf52), while decoding and AES-128-CTR crypto are common.
|
||||
|
||||
## Key Files
|
||||
- `src/` - Main library source code
|
||||
@@ -198,3 +198,153 @@ a843eb9 Keep v0.3.1
|
||||
- examples/Receiver/src/main.cpp
|
||||
- examples/Repeater/src/main.cpp
|
||||
|
||||
|
||||
### Session: 2026-02-28 14:32
|
||||
**Commits:**
|
||||
```
|
||||
4944757 Fix to be non blocking without tasks
|
||||
31765c7 Update notes
|
||||
84d153c Single callback version - vastly simplified.
|
||||
```
|
||||
**Modified files:**
|
||||
- examples/Logger/src/main.cpp
|
||||
- examples/MultiDevice/src/main.cpp
|
||||
- examples/Repeater/src/main.cpp
|
||||
- library.json
|
||||
- library.properties
|
||||
- src/VictronBLE.cpp
|
||||
- src/VictronBLE.h
|
||||
|
||||
|
||||
### Session: 2026-02-28 14:33
|
||||
**Commits:**
|
||||
```
|
||||
4944757 Fix to be non blocking without tasks
|
||||
31765c7 Update notes
|
||||
84d153c Single callback version - vastly simplified.
|
||||
```
|
||||
**Modified files:**
|
||||
- .claude/CLAUDE.md
|
||||
- VERSIONS
|
||||
- examples/Logger/src/main.cpp
|
||||
- examples/MultiDevice/src/main.cpp
|
||||
- examples/Repeater/src/main.cpp
|
||||
- library.json
|
||||
- library.properties
|
||||
- src/VictronBLE.cpp
|
||||
- src/VictronBLE.h
|
||||
|
||||
|
||||
### Session: 2026-02-28 14:36
|
||||
**Commits:**
|
||||
```
|
||||
4944757 Fix to be non blocking without tasks
|
||||
31765c7 Update notes
|
||||
```
|
||||
**Modified files:**
|
||||
- .claude/CLAUDE.md
|
||||
- VERSIONS
|
||||
- examples/Logger/src/main.cpp
|
||||
- examples/MultiDevice/src/main.cpp
|
||||
- examples/Repeater/src/main.cpp
|
||||
- library.json
|
||||
- library.properties
|
||||
- src/VictronBLE.cpp
|
||||
- src/VictronBLE.h
|
||||
|
||||
|
||||
### Session: 2026-02-28 14:40
|
||||
**Commits:**
|
||||
```
|
||||
39a89c8 Versions v0.4 ready for release
|
||||
4944757 Fix to be non blocking without tasks
|
||||
31765c7 Update notes
|
||||
```
|
||||
**Modified files:**
|
||||
- .claude/CLAUDE.md
|
||||
- README.md
|
||||
- VERSIONS
|
||||
- library.json
|
||||
- library.properties
|
||||
|
||||
|
||||
### Session: 2026-02-28 14:48
|
||||
**Commits:**
|
||||
```
|
||||
261cc0d Improve readme ready for v0.4 release
|
||||
39a89c8 Versions v0.4 ready for release
|
||||
4944757 Fix to be non blocking without tasks
|
||||
31765c7 Update notes
|
||||
```
|
||||
**Modified files:**
|
||||
- .claude/CLAUDE.md
|
||||
- README.md
|
||||
- REVIEW.md
|
||||
|
||||
|
||||
### Session: 2026-06-04 21:23
|
||||
**Commits:**
|
||||
```
|
||||
cd3b462 ignore local
|
||||
7706f88 Bug fixes for reported issues
|
||||
```
|
||||
**Modified files:**
|
||||
- .gitignore
|
||||
- README.md
|
||||
|
||||
|
||||
### Session: 2026-06-04 22:59
|
||||
**Commits:**
|
||||
```
|
||||
105d66b Update info
|
||||
```
|
||||
**Modified files:**
|
||||
- .claude/CLAUDE.md
|
||||
- README.md
|
||||
|
||||
|
||||
### Session: 2026-06-04 23:46
|
||||
**Commits:**
|
||||
```
|
||||
105d66b Update info
|
||||
```
|
||||
**Modified files:**
|
||||
- .claude/CLAUDE.md
|
||||
- README.md
|
||||
- VERSIONS
|
||||
- examples/MultiDevice/platformio.ini
|
||||
- examples/MultiDevice/src/main.cpp
|
||||
- library.json
|
||||
- library.properties
|
||||
- src/VictronBLE.cpp
|
||||
- src/VictronBLE.h
|
||||
- src/crypto/vble_aes.c
|
||||
|
||||
|
||||
### Session: 2026-06-04 23:58
|
||||
**Commits:**
|
||||
```
|
||||
53fa6fa ignore pka file
|
||||
de6607d Branch version ready for testing with nRF52
|
||||
```
|
||||
**Modified files:**
|
||||
- .gitignore
|
||||
- README.md
|
||||
- UPGRADE_V0.4.md
|
||||
- VERSIONS
|
||||
- library.json
|
||||
- library.properties
|
||||
|
||||
|
||||
### Session: 2026-06-05 00:00
|
||||
**Commits:**
|
||||
```
|
||||
53fa6fa ignore pka file
|
||||
de6607d Branch version ready for testing with nRF52
|
||||
```
|
||||
**Modified files:**
|
||||
- .claude/CLAUDE.md
|
||||
- .gitignore
|
||||
- README.md
|
||||
- UPGRADE_V0.4.md
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
PKA_SUMMARY.md
|
||||
|
||||
# PlatformIO
|
||||
.pio
|
||||
.pioenvs
|
||||
@@ -12,6 +14,8 @@ compile_commands.json
|
||||
!.vscode/extensions.json
|
||||
.history/
|
||||
|
||||
bugs
|
||||
|
||||
# Build artifacts
|
||||
*.o
|
||||
*.a
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
# VictronBLE
|
||||
|
||||
ESP32 library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements.
|
||||
A portable Arduino library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements — runs on both **ESP32** and **nRF52840**.
|
||||
|
||||
**⚠️ INITIAL RELEASE - LIMITED TESTING DONE**
|
||||
|
||||
This is an initial release (v0.3.1) and has been tested with MPPT on an ESP32-S3 and ESP32-C3.
|
||||
Use with caution and please report any issues you encounter. Testing and feedback are greatly appreciated!
|
||||
v0.6 adds **multi-platform support** (ESP32 + nRF52840) via a hardware-abstracted BLE backend and dependency-free bundled crypto. (v0.5 brought the decoding accuracy fixes and AC charger support; v0.4 reworked the internals — function-pointer callback API, reduced memory usage, non-blocking scanning.) See [VERSIONS](VERSIONS) for full details. A stable **v1.0** release with a consistent, long-term API is coming soon.
|
||||
|
||||
---
|
||||
|
||||
Why another library? Most of the Victron BLE examples are built into other frameworks (e.g. ESPHome) and I want a library that can be used in all ESP32 systems, including ESPHome or other frameworks. With long term plan to try and move others to this library and improve code with many eyes.
|
||||
Why another library? Most of the Victron BLE examples are built into other frameworks (e.g. ESPHome) or are locked to a single chip. The goal here is one library that works across ESP32 and nRF52 (and is easy to extend to more), usable standalone or inside ESPHome and other frameworks, with a long-term plan to move others onto it and improve the 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.
|
||||
Supports **ESP32** (original, S and C series — tested on older ESP32, ESP32-S3 and ESP32-C3) and **nRF52840** (Adafruit/Seeed Bluefruit core, e.g. Seeed XIAO nRF52840). All decoding and decryption is shared; only a thin BLE scanning backend is platform-specific (`src/esp32/`, `src/nrf52/`), so other chipsets can be added by implementing one more backend.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Multi-Platform**: One API for ESP32 and nRF52840; backend chosen at compile time
|
||||
- ✅ **No External Dependencies**: Bundled AES-128-CTR — no mbedTLS or crypto library needed
|
||||
- ✅ **Multiple Device Support**: Monitor multiple Victron devices simultaneously
|
||||
- ✅ **All Device Types**: Solar chargers, battery monitors, inverters, DC-DC converters
|
||||
- ✅ **Framework Agnostic**: Works with Arduino and ESP-IDF
|
||||
- ✅ **All Device Types**: Solar chargers, battery monitors, inverters, DC-DC converters, AC chargers
|
||||
- ✅ **Framework Friendly**: Works with Arduino (and ESP-IDF on ESP32)
|
||||
- ✅ **Clean API**: Simple, intuitive interface with callback support
|
||||
- ✅ **No Pairing Required**: Reads BLE advertisement data directly
|
||||
- ✅ **Low Power**: Uses passive BLE scanning
|
||||
@@ -30,29 +29,58 @@ Currently supportin ESP32 S and C series (tested on older ESP32, and ESP32-S3 an
|
||||
- **Battery Monitors**: SmartShunt, BMV-712 Smart, BMV-700 series
|
||||
- **Inverters**: MultiPlus, Quattro, Phoenix (with VE.Bus BLE dongle)
|
||||
- **DC-DC Converters**: Orion Smart, Orion XS
|
||||
- **AC Chargers**: Blue Smart IP22 / IP65 / IP67 chargers
|
||||
- **Others**: Smart Battery Protect, Lynx Smart BMS, Smart Lithium batteries
|
||||
|
||||
## Hardware Requirements
|
||||
|
||||
- ESP32, ESP32-S3, or ESP32-C3 board
|
||||
- An ESP32 (original / S / C series) **or** an nRF52840 board (Adafruit/Seeed
|
||||
Bluefruit core — e.g. Seeed XIAO nRF52840)
|
||||
- Victron devices with BLE "Instant Readout" enabled
|
||||
|
||||
The BLE backend is selected automatically at compile time from the board's
|
||||
architecture — no code changes are needed to switch platforms.
|
||||
|
||||
## Installation
|
||||
|
||||
### PlatformIO
|
||||
|
||||
1. Add to `platformio.ini`:
|
||||
1. Add to `platformio.ini` (recommended — installs from the PlatformIO registry):
|
||||
```ini
|
||||
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:
|
||||
```bash
|
||||
cd lib
|
||||
git clone https://gitea.sh3d.com.au/Sh3d/VictronBLE
|
||||
git clone https://gitea.sh3d.com.au/Sh3d/VictronBLE.git
|
||||
```
|
||||
|
||||
#### nRF52840 board note
|
||||
|
||||
The nRF52 backend uses the **Bluefruit** library from the Adafruit/Seeed nRF52
|
||||
core, so pick a board that uses that core. For the Seeed XIAO nRF52840, the
|
||||
board files live in a community platform fork — use the `*_adafruit` variant
|
||||
(the plain `xiaoble` variant uses the mbed core, which has no Bluefruit):
|
||||
```ini
|
||||
[env:xiao_nrf52840]
|
||||
platform = https://github.com/maxgerhardt/platform-nordicnrf52
|
||||
board = xiaoble_adafruit ; XIAO nRF52840 Sense: xiaoblesense_adafruit
|
||||
framework = arduino
|
||||
lib_deps = scottp/victronble
|
||||
```
|
||||
The Adafruit Feather nRF52840 (`board = adafruit_feather_nrf52840`) works out of
|
||||
the box with the stock PlatformIO `nordicnrf52` platform. The `MultiDevice`
|
||||
example's `platformio.ini` includes ready-made ESP32 and nRF52 environments.
|
||||
|
||||
### Arduino IDE
|
||||
|
||||
1. Download or clone this repository
|
||||
@@ -81,38 +109,34 @@ Use the VictronConnect app to get your device's encryption key:
|
||||
|
||||
VictronBLE victron;
|
||||
|
||||
// Callback for data updates
|
||||
class MyCallback : public VictronDeviceCallback {
|
||||
public:
|
||||
void onSolarChargerData(const SolarChargerData& data) override {
|
||||
Serial.printf("Solar: %.2fV, %.2fA, %dW\n",
|
||||
data.batteryVoltage,
|
||||
data.batteryCurrent,
|
||||
data.panelPower);
|
||||
// Callback — receives a VictronDevice*, switch on deviceType
|
||||
void onVictronData(const VictronDevice* dev) {
|
||||
if (dev->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
|
||||
Serial.printf("Solar %s: %.2fV %.2fA %dW\n",
|
||||
dev->name,
|
||||
dev->solar.batteryVoltage,
|
||||
dev->solar.batteryCurrent,
|
||||
(int)dev->solar.panelPower);
|
||||
}
|
||||
};
|
||||
|
||||
MyCallback callback;
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
// Initialize library
|
||||
victron.begin(5); // 5 second scan duration
|
||||
victron.setCallback(&callback);
|
||||
victron.setCallback(onVictronData);
|
||||
|
||||
// Add your device (replace with your MAC and key)
|
||||
victron.addDevice(
|
||||
"My MPPT", // Name
|
||||
"AA:BB:CC:DD:EE:FF", // MAC address
|
||||
"0123456789abcdef0123456789abcdef", // Encryption key
|
||||
DEVICE_TYPE_SOLAR_CHARGER // Device type
|
||||
DEVICE_TYPE_SOLAR_CHARGER // Device type (optional, auto-detected)
|
||||
);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
victron.loop();
|
||||
delay(100);
|
||||
victron.loop(); // Non-blocking, returns immediately
|
||||
}
|
||||
```
|
||||
|
||||
@@ -125,130 +149,113 @@ void loop() {
|
||||
```cpp
|
||||
bool begin(uint32_t scanDuration = 5);
|
||||
```
|
||||
Initialize BLE and start scanning. Returns `true` on success.
|
||||
Initialize BLE scanning. Returns `true` on success.
|
||||
|
||||
**Parameters:**
|
||||
- `scanDuration`: BLE scan duration in seconds (default: 5)
|
||||
- `scanDuration`: BLE scan window in seconds (default: 5)
|
||||
|
||||
#### Device Management
|
||||
|
||||
```cpp
|
||||
bool addDevice(String name, String macAddress, String encryptionKey,
|
||||
VictronDeviceType expectedType = DEVICE_TYPE_UNKNOWN);
|
||||
bool addDevice(const char* name, const char* mac, const char* hexKey,
|
||||
VictronDeviceType type = DEVICE_TYPE_UNKNOWN);
|
||||
```
|
||||
Add a device to monitor.
|
||||
Add a device to monitor (max 8 devices).
|
||||
|
||||
**Parameters:**
|
||||
- `name`: Friendly name for the device
|
||||
- `macAddress`: Device MAC address (format: "AA:BB:CC:DD:EE:FF")
|
||||
- `encryptionKey`: 32-character hex encryption key from VictronConnect
|
||||
- `expectedType`: Device type (optional, for validation)
|
||||
- `mac`: Device MAC address (format: `"AA:BB:CC:DD:EE:FF"` or `"aabbccddeeff"`)
|
||||
- `hexKey`: 32-character hex encryption key from VictronConnect
|
||||
- `type`: Device type (optional, auto-detected from BLE advertisement)
|
||||
|
||||
**Returns:** `true` on success
|
||||
|
||||
```cpp
|
||||
void removeDevice(String macAddress);
|
||||
```
|
||||
Remove a device from monitoring.
|
||||
|
||||
```cpp
|
||||
size_t getDeviceCount();
|
||||
size_t getDeviceCount() const;
|
||||
```
|
||||
Get the number of configured devices.
|
||||
|
||||
#### Data Access
|
||||
#### Callback
|
||||
|
||||
```cpp
|
||||
bool getSolarChargerData(String macAddress, SolarChargerData& data);
|
||||
bool getBatteryMonitorData(String macAddress, BatteryMonitorData& data);
|
||||
bool getInverterData(String macAddress, InverterData& data);
|
||||
bool getDCDCConverterData(String macAddress, DCDCConverterData& data);
|
||||
void setCallback(VictronCallback cb);
|
||||
```
|
||||
Get latest data for a specific device. Returns `true` if data is valid.
|
||||
Set a function pointer callback. Called when new data arrives from a device. The callback receives a `const VictronDevice*` — switch on `deviceType` to access the appropriate data union member.
|
||||
|
||||
```cpp
|
||||
std::vector<String> getDevicesByType(VictronDeviceType type);
|
||||
typedef void (*VictronCallback)(const VictronDevice* device);
|
||||
```
|
||||
Get MAC addresses of all devices of a specific type.
|
||||
|
||||
#### Callbacks
|
||||
#### Configuration
|
||||
|
||||
```cpp
|
||||
void setCallback(VictronDeviceCallback* callback);
|
||||
void setMinInterval(uint32_t ms);
|
||||
```
|
||||
Set callback object to receive data updates automatically.
|
||||
|
||||
#### Utilities
|
||||
Set minimum callback interval per device (default: 1000ms). Callbacks are also suppressed when the device nonce hasn't changed (data unchanged).
|
||||
|
||||
```cpp
|
||||
void setDebug(bool enable);
|
||||
```
|
||||
Enable/disable debug output to Serial.
|
||||
|
||||
```cpp
|
||||
String getLastError();
|
||||
```
|
||||
Get last error message.
|
||||
#### Main Loop
|
||||
|
||||
```cpp
|
||||
void loop();
|
||||
```
|
||||
Process BLE scanning and data updates. Call this in your main loop.
|
||||
Call in your main loop. Non-blocking — returns immediately if a scan is already running. Scan restarts automatically when it completes.
|
||||
|
||||
### Data Structures
|
||||
|
||||
#### SolarChargerData
|
||||
#### VictronDevice (main struct)
|
||||
|
||||
All device types share this struct. Access type-specific data via the union member matching `deviceType`.
|
||||
|
||||
```cpp
|
||||
struct SolarChargerData {
|
||||
String deviceName;
|
||||
String macAddress;
|
||||
struct VictronDevice {
|
||||
char name[32];
|
||||
char mac[13]; // 12 hex chars + null
|
||||
VictronDeviceType deviceType;
|
||||
int8_t rssi; // Signal strength (dBm)
|
||||
uint32_t lastUpdate; // millis() of last update
|
||||
bool dataValid; // Data validity flag
|
||||
bool dataValid;
|
||||
union {
|
||||
VictronSolarData solar;
|
||||
VictronBatteryData battery;
|
||||
VictronInverterData inverter;
|
||||
VictronDCDCData dcdc;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
SolarChargerState chargeState; // Charging state
|
||||
#### VictronSolarData
|
||||
|
||||
```cpp
|
||||
struct VictronSolarData {
|
||||
uint8_t chargeState; // SolarChargerState enum
|
||||
uint8_t errorCode;
|
||||
float batteryVoltage; // V
|
||||
float batteryCurrent; // A
|
||||
float panelVoltage; // V (calculated)
|
||||
float panelPower; // W
|
||||
uint16_t yieldToday; // Wh
|
||||
float loadCurrent; // A (if load output present)
|
||||
};
|
||||
```
|
||||
|
||||
**Charge States:**
|
||||
- `CHARGER_OFF` - Off
|
||||
- `CHARGER_LOW_POWER` - Low power
|
||||
- `CHARGER_FAULT` - Fault
|
||||
- `CHARGER_BULK` - Bulk charging
|
||||
- `CHARGER_ABSORPTION` - Absorption
|
||||
- `CHARGER_FLOAT` - Float
|
||||
- `CHARGER_STORAGE` - Storage mode
|
||||
- `CHARGER_EQUALIZE` - Equalize
|
||||
- `CHARGER_INVERTING` - Inverting (HUB-4)
|
||||
- `CHARGER_POWER_SUPPLY` - Power supply mode
|
||||
- `CHARGER_EXTERNAL_CONTROL` - External control
|
||||
**Charge States** (`chargeState` values):
|
||||
`CHARGER_OFF`, `CHARGER_LOW_POWER`, `CHARGER_FAULT`, `CHARGER_BULK`, `CHARGER_ABSORPTION`, `CHARGER_FLOAT`, `CHARGER_STORAGE`, `CHARGER_EQUALIZE`, `CHARGER_INVERTING`, `CHARGER_POWER_SUPPLY`, `CHARGER_EXTERNAL_CONTROL`
|
||||
|
||||
#### BatteryMonitorData
|
||||
#### VictronBatteryData
|
||||
|
||||
```cpp
|
||||
struct BatteryMonitorData {
|
||||
String deviceName;
|
||||
String macAddress;
|
||||
int8_t rssi;
|
||||
uint32_t lastUpdate;
|
||||
bool dataValid;
|
||||
|
||||
struct VictronBatteryData {
|
||||
float voltage; // V
|
||||
float current; // A (+ charging, - discharging)
|
||||
float temperature; // °C (if configured)
|
||||
float auxVoltage; // V (starter battery/midpoint)
|
||||
uint16_t remainingMinutes; // Time remaining
|
||||
float consumedAh; // Ah consumed
|
||||
float temperature; // C (0 if aux is voltage)
|
||||
float auxVoltage; // V (0 if aux is temperature)
|
||||
uint16_t remainingMinutes;
|
||||
float consumedAh; // Ah
|
||||
float soc; // State of charge %
|
||||
|
||||
// Alarms
|
||||
bool alarmLowVoltage;
|
||||
bool alarmHighVoltage;
|
||||
bool alarmLowSOC;
|
||||
@@ -257,39 +264,25 @@ struct BatteryMonitorData {
|
||||
};
|
||||
```
|
||||
|
||||
#### InverterData
|
||||
#### VictronInverterData
|
||||
|
||||
```cpp
|
||||
struct InverterData {
|
||||
String deviceName;
|
||||
String macAddress;
|
||||
int8_t rssi;
|
||||
uint32_t lastUpdate;
|
||||
bool dataValid;
|
||||
|
||||
struct VictronInverterData {
|
||||
float batteryVoltage; // V
|
||||
float batteryCurrent; // A
|
||||
float acPower; // W (+ inverting, - charging)
|
||||
uint8_t state; // Inverter state
|
||||
|
||||
// Alarms
|
||||
bool alarmHighVoltage;
|
||||
uint8_t state;
|
||||
bool alarmLowVoltage;
|
||||
bool alarmHighVoltage;
|
||||
bool alarmHighTemperature;
|
||||
bool alarmOverload;
|
||||
};
|
||||
```
|
||||
|
||||
#### DCDCConverterData
|
||||
#### VictronDCDCData
|
||||
|
||||
```cpp
|
||||
struct DCDCConverterData {
|
||||
String deviceName;
|
||||
String macAddress;
|
||||
int8_t rssi;
|
||||
uint32_t lastUpdate;
|
||||
bool dataValid;
|
||||
|
||||
struct VictronDCDCData {
|
||||
float inputVoltage; // V
|
||||
float outputVoltage; // V
|
||||
float outputCurrent; // A
|
||||
@@ -298,6 +291,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
|
||||
|
||||
### Multiple Devices
|
||||
@@ -305,80 +315,59 @@ struct DCDCConverterData {
|
||||
```cpp
|
||||
void setup() {
|
||||
victron.begin(5);
|
||||
victron.setCallback(&callback);
|
||||
victron.setCallback(onVictronData);
|
||||
|
||||
// Add multiple devices
|
||||
victron.addDevice("MPPT 1", "AA:BB:CC:DD:EE:01", "key1...", DEVICE_TYPE_SOLAR_CHARGER);
|
||||
victron.addDevice("MPPT 2", "AA:BB:CC:DD:EE:02", "key2...", DEVICE_TYPE_SOLAR_CHARGER);
|
||||
victron.addDevice("SmartShunt", "AA:BB:CC:DD:EE:03", "key3...", DEVICE_TYPE_BATTERY_MONITOR);
|
||||
victron.addDevice("Inverter", "AA:BB:CC:DD:EE:04", "key4...", DEVICE_TYPE_INVERTER);
|
||||
// Add multiple devices (type is auto-detected from BLE advertisements)
|
||||
victron.addDevice("MPPT 1", "AA:BB:CC:DD:EE:01", "key1...");
|
||||
victron.addDevice("MPPT 2", "AA:BB:CC:DD:EE:02", "key2...");
|
||||
victron.addDevice("SmartShunt", "AA:BB:CC:DD:EE:03", "key3...");
|
||||
victron.addDevice("Inverter", "AA:BB:CC:DD:EE:04", "key4...");
|
||||
}
|
||||
```
|
||||
|
||||
### Manual Data Polling
|
||||
### Handling Multiple Device Types
|
||||
|
||||
```cpp
|
||||
void loop() {
|
||||
victron.loop();
|
||||
|
||||
// Query specific device
|
||||
SolarChargerData mpptData;
|
||||
if (victron.getSolarChargerData("AA:BB:CC:DD:EE:FF", mpptData)) {
|
||||
if (mpptData.dataValid) {
|
||||
// Use data
|
||||
float power = mpptData.panelPower;
|
||||
void onVictronData(const VictronDevice* dev) {
|
||||
switch (dev->deviceType) {
|
||||
case DEVICE_TYPE_SOLAR_CHARGER:
|
||||
Serial.printf("%s: %.2fV %dW\n", dev->name,
|
||||
dev->solar.batteryVoltage, (int)dev->solar.panelPower);
|
||||
break;
|
||||
case DEVICE_TYPE_BATTERY_MONITOR:
|
||||
Serial.printf("%s: %.2fV %.1f%%\n", dev->name,
|
||||
dev->battery.voltage, dev->battery.soc);
|
||||
break;
|
||||
case DEVICE_TYPE_INVERTER:
|
||||
Serial.printf("%s: %dW\n", dev->name, (int)dev->inverter.acPower);
|
||||
break;
|
||||
case DEVICE_TYPE_DCDC_CONVERTER:
|
||||
Serial.printf("%s: %.2fV -> %.2fV\n", dev->name,
|
||||
dev->dcdc.inputVoltage, dev->dcdc.outputVoltage);
|
||||
break;
|
||||
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
|
||||
void loop() {
|
||||
victron.loop();
|
||||
void setup() {
|
||||
victron.begin(5);
|
||||
victron.setCallback(onVictronData);
|
||||
victron.setMinInterval(2000); // Callback at most every 2 seconds per device
|
||||
|
||||
// Get all solar chargers
|
||||
std::vector<String> mppts = victron.getDevicesByType(DEVICE_TYPE_SOLAR_CHARGER);
|
||||
|
||||
for (const String& mac : mppts) {
|
||||
SolarChargerData data;
|
||||
if (victron.getSolarChargerData(mac, data)) {
|
||||
Serial.println(data.deviceName + ": " + String(data.panelPower) + "W");
|
||||
}
|
||||
}
|
||||
|
||||
delay(5000);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Callback Interface
|
||||
|
||||
Implement `VictronDeviceCallback` to receive automatic updates:
|
||||
|
||||
```cpp
|
||||
class MyCallback : public VictronDeviceCallback {
|
||||
public:
|
||||
void onSolarChargerData(const SolarChargerData& data) override {
|
||||
// Handle solar charger update
|
||||
}
|
||||
|
||||
void onBatteryMonitorData(const BatteryMonitorData& data) override {
|
||||
// Handle battery monitor update
|
||||
}
|
||||
|
||||
void onInverterData(const InverterData& data) override {
|
||||
// Handle inverter update
|
||||
}
|
||||
|
||||
void onDCDCConverterData(const DCDCConverterData& data) override {
|
||||
// Handle DC-DC converter update
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No Data Received
|
||||
@@ -413,18 +402,55 @@ This library implements the Victron BLE Advertising protocol:
|
||||
|
||||
Based on official [Victron BLE documentation](https://www.victronenergy.com/live/vedirect_protocol:faq).
|
||||
|
||||
## Architecture & Portability
|
||||
|
||||
The library keeps everything platform-independent except the BLE radio:
|
||||
|
||||
```
|
||||
src/
|
||||
├── VictronBLE.{h,cpp} Common API, device management, payload decoding
|
||||
├── crypto/vble_aes.{h,c} Bundled AES-128-CTR (no external dependency)
|
||||
├── esp32/ ESP32 backend — Bluedroid BLEScan
|
||||
└── nrf52/ nRF52 backend — Bluefruit passive scan
|
||||
```
|
||||
|
||||
- **One BLE HAL.** Each backend extracts the manufacturer data, MAC and RSSI
|
||||
from a scan result and calls the shared `onAdvertisement()`. All decryption and
|
||||
decoding is common code. The correct backend is selected automatically at
|
||||
compile time from the board architecture (`ARDUINO_ARCH_ESP32` /
|
||||
`ARDUINO_ARCH_NRF52`) — there is nothing platform-specific in your sketch.
|
||||
- **No external crypto.** AES-128-CTR is bundled (a trimmed, NIST-verified
|
||||
tiny-AES), so the library no longer depends on mbedTLS or any crypto library
|
||||
and builds identically on every target.
|
||||
- **Adding a platform** means implementing one more backend (scan → extract →
|
||||
`onAdvertisement`); the rest is reused unchanged.
|
||||
|
||||
> The data callback runs in the BLE event context (the scan task on ESP32, the
|
||||
> SoftDevice/Bluefruit handler on nRF52). Keep work in the callback light — copy
|
||||
> what you need and process it from `loop()`.
|
||||
|
||||
## Examples
|
||||
|
||||
See the `examples/` directory for:
|
||||
|
||||
- **MultiDevice**: Monitor multiple devices with callbacks
|
||||
- More examples coming soon!
|
||||
- **MultiDevice**: Monitor multiple devices with callbacks. One sketch, multiple
|
||||
PlatformIO environments — builds for ESP32 (`esp32dev`, …) and nRF52840
|
||||
(`xiao_nrf52840`, `adafruit_feather_nrf52840`).
|
||||
- **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
|
||||
|
||||
The primary repository is hosted on [Gitea](https://gitea.sh3d.com.au/Sh3d/VictronBLE),
|
||||
with a mirror on **GitHub at <https://github.com/SH3D/VictronBLE>**. Since the Gitea
|
||||
instance does not currently allow public sign-ups, please raise **issues and pull
|
||||
requests on the GitHub mirror**.
|
||||
|
||||
Contributions welcome! Please:
|
||||
|
||||
1. Fork the repository
|
||||
1. Fork the [GitHub mirror](https://github.com/SH3D/VictronBLE)
|
||||
2. Create a feature branch
|
||||
3. Test thoroughly on real hardware
|
||||
4. Submit a pull request
|
||||
@@ -456,7 +482,10 @@ See [VERSIONS](VERSIONS) file for detailed changelog and release history.
|
||||
|
||||
## Support
|
||||
|
||||
- 📫 Report issues on GitHub
|
||||
- 📫 Report issues on the [GitHub mirror](https://github.com/SH3D/VictronBLE/issues)
|
||||
(the Gitea instance does not currently allow public sign-ups). Bug reports, device
|
||||
decode problems and new device requests are all welcome — debug log output is very
|
||||
helpful.
|
||||
- 📖 Check the examples directory
|
||||
- 🔧 Enable debug mode for diagnostics
|
||||
- 📚 See [Victron documentation](https://www.victronenergy.com/live/)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# VictronBLE Code Review
|
||||
|
||||
## Part 1: Bug Fixes, Efficiency & Simplification
|
||||
## Part 1: Bug Fixes, Efficiency & Simplification ✅ COMPLETE (v0.4.1)
|
||||
|
||||
### Bugs
|
||||
|
||||
@@ -117,10 +117,34 @@ void victron_loop();
|
||||
~4 functions instead of ~15 methods.
|
||||
|
||||
|
||||
All items implemented in v0.4.1. See [VERSIONS](VERSIONS) for full changelog.
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Multi-Platform BLE Support
|
||||
|
||||
### Recommended Test Hardware
|
||||
|
||||
Two cheap BLE development boards for testing the platform abstraction:
|
||||
|
||||
**1. Seeed XIAO nRF52840 (~$10 USD)**
|
||||
- Nordic nRF52840 SoC, Bluetooth 5.0, onboard antenna
|
||||
- Arduino-compatible via Adafruit nRF52 board support package
|
||||
- Ultra-small (21x17.5mm), USB-C, battery charging built in
|
||||
- 1MB flash, 256KB RAM, 2MB QSPI flash
|
||||
- Has mbedtls available via the nRF SDK
|
||||
- https://www.seeedstudio.com/Seeed-XIAO-BLE-nRF52840-p-5201.html
|
||||
|
||||
**2. Raspberry Pi Pico W (~$6 USD)**
|
||||
- RP2040 + Infineon CYW43439 (WiFi + Bluetooth 5.2 with BLE)
|
||||
- Arduino-compatible via arduino-pico core (earlephilhower)
|
||||
- BLE Central role supported (needed for passive scanning)
|
||||
- Very widely available and cheap
|
||||
- Different architecture (ARM Cortex-M0+) from ESP32 (Xtensa/RISC-V), good for testing portability
|
||||
- https://www.raspberrypi.com/products/raspberry-pi-pico/
|
||||
|
||||
Both boards are under $15, Arduino-compatible, and have BLE Central support needed for passive scanning of Victron advertisements. They use different BLE stacks (nRF SoftDevice vs CYW43 BTstack) which will validate the transport abstraction layer.
|
||||
|
||||
### Current BLE Dependencies
|
||||
|
||||
All ESP32-specific BLE code is confined to:
|
||||
|
||||
@@ -1,5 +1,112 @@
|
||||
# Version History
|
||||
|
||||
## 0.6.0 (2026-06-04)
|
||||
|
||||
Multi-platform support. The library now runs on **nRF52840** (Adafruit/Seeed
|
||||
Bluefruit core) in addition to ESP32, sharing all decoding and crypto code.
|
||||
|
||||
### Platform abstraction
|
||||
- The BLE scanning layer is now the only platform-specific code, split into
|
||||
backends under `src/esp32/` (ESP32 Bluedroid `BLEScan`) and `src/nrf52/`
|
||||
(Bluefruit passive scan). Both extract manufacturer data + MAC + RSSI and feed
|
||||
a shared `VictronBLE::onAdvertisement()`. The public API is unchanged.
|
||||
- The correct backend is auto-selected at compile time; no user configuration
|
||||
needed beyond picking the board.
|
||||
|
||||
### Portable crypto (no external dependency)
|
||||
- Replaced the ESP32-only mbedTLS AES with a small bundled AES-128-CTR
|
||||
implementation (`src/crypto/`, trimmed/prefixed tiny-AES, public domain,
|
||||
NIST SP 800-38A verified). Decryption output is byte-identical to the previous
|
||||
mbedTLS path; the library now has no external crypto dependency on any target.
|
||||
|
||||
### New
|
||||
- The `MultiDevice` example now builds for both ESP32 and nRF52840 from a single
|
||||
sketch — its `platformio.ini` adds `xiao_nrf52840` (Seeed XIAO nRF52840, board
|
||||
`xiaoble_adafruit` via the maxgerhardt nRF52 platform fork) and
|
||||
`adafruit_feather_nrf52840` environments alongside the ESP32 ones.
|
||||
- `nrf52` added to the supported architectures / PlatformIO platforms.
|
||||
|
||||
## 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)
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -17,7 +17,6 @@ struct __attribute__((packed)) SolarChargerPacket {
|
||||
uint8_t chargeState;
|
||||
float batteryVoltage; // V
|
||||
float batteryCurrent; // A
|
||||
float panelVoltage; // V
|
||||
float panelPower; // W
|
||||
uint16_t yieldToday; // Wh
|
||||
float loadCurrent; // A
|
||||
@@ -74,7 +73,6 @@ void loop() {
|
||||
pkt.chargeState = (sendCount % 4) + 3; // Cycle through Bulk(3), Absorption(4), Float(5), Storage(6)
|
||||
pkt.batteryVoltage = 51.0f + (sendCount % 20) * 0.15f;
|
||||
pkt.batteryCurrent = 2.0f + (sendCount % 10) * 0.5f;
|
||||
pkt.panelVoltage = 65.0f + (sendCount % 15) * 0.8f;
|
||||
pkt.panelPower = pkt.batteryCurrent * pkt.batteryVoltage;
|
||||
pkt.yieldToday = 100 + sendCount * 10;
|
||||
pkt.loadCurrent = 0;
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
*
|
||||
* Demonstrates change-detection logging for Solar Charger data.
|
||||
* Only logs to serial when a value changes (ignoring RSSI), or once
|
||||
* per minute if nothing has changed. This keeps serial output quiet
|
||||
* and is useful for long-running monitoring / data logging.
|
||||
* per minute if nothing has changed.
|
||||
*
|
||||
* Setup:
|
||||
* 1. Get your device encryption keys from the VictronConnect app
|
||||
@@ -16,13 +15,11 @@
|
||||
|
||||
VictronBLE victron;
|
||||
|
||||
// Tracks last-logged values per device for change detection
|
||||
struct SolarChargerSnapshot {
|
||||
bool valid = false;
|
||||
SolarChargerState chargeState;
|
||||
uint8_t chargeState;
|
||||
float batteryVoltage;
|
||||
float batteryCurrent;
|
||||
float panelVoltage;
|
||||
float panelPower;
|
||||
uint16_t yieldToday;
|
||||
float loadCurrent;
|
||||
@@ -30,26 +27,26 @@ struct SolarChargerSnapshot {
|
||||
uint32_t packetsSinceLastLog = 0;
|
||||
};
|
||||
|
||||
// Store a snapshot per device (index by MAC string)
|
||||
static const int MAX_DEVICES = 4;
|
||||
static String deviceMACs[MAX_DEVICES];
|
||||
static char deviceMACs[MAX_DEVICES][VICTRON_MAC_LEN];
|
||||
static SolarChargerSnapshot snapshots[MAX_DEVICES];
|
||||
static int deviceCount = 0;
|
||||
|
||||
static const unsigned long LOG_INTERVAL_MS = 60000; // 1 minute
|
||||
static const unsigned long LOG_INTERVAL_MS = 60000;
|
||||
|
||||
static int findOrAddDevice(const String& mac) {
|
||||
static int findOrAddDevice(const char* mac) {
|
||||
for (int i = 0; i < deviceCount; i++) {
|
||||
if (deviceMACs[i] == mac) return i;
|
||||
if (strcmp(deviceMACs[i], mac) == 0) return i;
|
||||
}
|
||||
if (deviceCount < MAX_DEVICES) {
|
||||
deviceMACs[deviceCount] = mac;
|
||||
strncpy(deviceMACs[deviceCount], mac, VICTRON_MAC_LEN - 1);
|
||||
deviceMACs[deviceCount][VICTRON_MAC_LEN - 1] = '\0';
|
||||
return deviceCount++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static String chargeStateName(SolarChargerState state) {
|
||||
static const char* chargeStateName(uint8_t state) {
|
||||
switch (state) {
|
||||
case CHARGER_OFF: return "Off";
|
||||
case CHARGER_LOW_POWER: return "Low Power";
|
||||
@@ -66,22 +63,23 @@ static String chargeStateName(SolarChargerState state) {
|
||||
}
|
||||
}
|
||||
|
||||
static void logData(const SolarChargerData& data, const char* reason, uint32_t packets) {
|
||||
Serial.println("[" + data.deviceName + "] " + reason +
|
||||
" pkts:" + String(packets) +
|
||||
" | State:" + chargeStateName(data.chargeState) +
|
||||
" Batt:" + String(data.batteryVoltage, 2) + "V" +
|
||||
" " + String(data.batteryCurrent, 2) + "A" +
|
||||
" PV:" + String(data.panelVoltage, 1) + "V" +
|
||||
" " + String(data.panelPower, 0) + "W" +
|
||||
" Yield:" + String(data.yieldToday) + "Wh" +
|
||||
(data.loadCurrent > 0 ? " Load:" + String(data.loadCurrent, 2) + "A" : ""));
|
||||
static void logData(const VictronDevice* dev, const VictronSolarData& s,
|
||||
const char* reason, uint32_t packets) {
|
||||
Serial.printf("[%s] %s pkts:%lu | State:%s Batt:%.2fV %.2fA PV:%.0fW Yield:%uWh",
|
||||
dev->name, reason, packets,
|
||||
chargeStateName(s.chargeState),
|
||||
s.batteryVoltage, s.batteryCurrent,
|
||||
s.panelPower, s.yieldToday);
|
||||
if (s.loadCurrent > 0)
|
||||
Serial.printf(" Load:%.2fA", s.loadCurrent);
|
||||
Serial.println();
|
||||
}
|
||||
|
||||
class LoggerCallback : public VictronDeviceCallback {
|
||||
public:
|
||||
void onSolarChargerData(const SolarChargerData& data) override {
|
||||
int idx = findOrAddDevice(data.macAddress);
|
||||
void onVictronData(const VictronDevice* dev) {
|
||||
if (dev->deviceType != DEVICE_TYPE_SOLAR_CHARGER) return;
|
||||
const auto& s = dev->solar;
|
||||
|
||||
int idx = findOrAddDevice(dev->mac);
|
||||
if (idx < 0) return;
|
||||
|
||||
SolarChargerSnapshot& prev = snapshots[idx];
|
||||
@@ -89,43 +87,34 @@ public:
|
||||
prev.packetsSinceLastLog++;
|
||||
|
||||
if (!prev.valid) {
|
||||
// First reading - always log
|
||||
logData(data, "INIT", prev.packetsSinceLastLog);
|
||||
logData(dev, s, "INIT", prev.packetsSinceLastLog);
|
||||
} else {
|
||||
// Check for changes (everything except RSSI)
|
||||
bool changed = false;
|
||||
if (prev.chargeState != data.chargeState) changed = true;
|
||||
if (prev.batteryVoltage != data.batteryVoltage) changed = true;
|
||||
if (prev.batteryCurrent != data.batteryCurrent) changed = true;
|
||||
if (prev.panelVoltage != data.panelVoltage) changed = true;
|
||||
if (prev.panelPower != data.panelPower) changed = true;
|
||||
if (prev.yieldToday != data.yieldToday) changed = true;
|
||||
if (prev.loadCurrent != data.loadCurrent) changed = true;
|
||||
bool changed = (prev.chargeState != s.chargeState) ||
|
||||
(prev.batteryVoltage != s.batteryVoltage) ||
|
||||
(prev.batteryCurrent != s.batteryCurrent) ||
|
||||
(prev.panelPower != s.panelPower) ||
|
||||
(prev.yieldToday != s.yieldToday) ||
|
||||
(prev.loadCurrent != s.loadCurrent);
|
||||
|
||||
if (changed) {
|
||||
logData(data, "CHG", prev.packetsSinceLastLog);
|
||||
logData(dev, s, "CHG", prev.packetsSinceLastLog);
|
||||
} else if (now - prev.lastLogTime >= LOG_INTERVAL_MS) {
|
||||
logData(data, "HEARTBEAT", prev.packetsSinceLastLog);
|
||||
logData(dev, s, "HEARTBEAT", prev.packetsSinceLastLog);
|
||||
} else {
|
||||
return; // Nothing to log
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update snapshot
|
||||
prev.packetsSinceLastLog = 0;
|
||||
prev.valid = true;
|
||||
prev.chargeState = data.chargeState;
|
||||
prev.batteryVoltage = data.batteryVoltage;
|
||||
prev.batteryCurrent = data.batteryCurrent;
|
||||
prev.panelVoltage = data.panelVoltage;
|
||||
prev.panelPower = data.panelPower;
|
||||
prev.yieldToday = data.yieldToday;
|
||||
prev.loadCurrent = data.loadCurrent;
|
||||
prev.chargeState = s.chargeState;
|
||||
prev.batteryVoltage = s.batteryVoltage;
|
||||
prev.batteryCurrent = s.batteryCurrent;
|
||||
prev.panelPower = s.panelPower;
|
||||
prev.yieldToday = s.yieldToday;
|
||||
prev.loadCurrent = s.loadCurrent;
|
||||
prev.lastLogTime = now;
|
||||
}
|
||||
};
|
||||
|
||||
LoggerCallback callback;
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
@@ -135,14 +124,12 @@ void setup() {
|
||||
|
||||
if (!victron.begin(5)) {
|
||||
Serial.println("ERROR: Failed to initialize VictronBLE!");
|
||||
Serial.println(victron.getLastError());
|
||||
while (1) delay(1000);
|
||||
}
|
||||
|
||||
victron.setDebug(false);
|
||||
victron.setCallback(&callback);
|
||||
victron.setCallback(onVictronData);
|
||||
|
||||
// Add your devices here
|
||||
victron.addDevice(
|
||||
"Rainbow48V",
|
||||
"E4:05:42:34:14:F3",
|
||||
@@ -157,11 +144,10 @@ void setup() {
|
||||
DEVICE_TYPE_SOLAR_CHARGER
|
||||
);
|
||||
|
||||
Serial.println("Configured " + String(victron.getDeviceCount()) + " devices");
|
||||
Serial.printf("Configured %d devices\n", (int)victron.getDeviceCount());
|
||||
Serial.println("Logging on change, or every 60s heartbeat\n");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
victron.loop();
|
||||
delay(100);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
[env]
|
||||
lib_extra_dirs = ../..
|
||||
|
||||
; --- nRF52840 targets (Bluefruit backend, selected automatically) ---
|
||||
; Seeed XIAO nRF52840. Board files come from a community platform fork; use the
|
||||
; *_adafruit variant (the plain `xiaoble` uses the mbed core, which has no
|
||||
; Bluefruit). For the XIAO nRF52840 Sense use board = xiaoblesense_adafruit.
|
||||
[env:xiao_nrf52840]
|
||||
platform = https://github.com/maxgerhardt/platform-nordicnrf52
|
||||
board = xiaoble_adafruit
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
build_flags = -DCFG_DEBUG=0
|
||||
|
||||
; Adafruit Feather nRF52840 — available in the stock PlatformIO nordicnrf52 platform.
|
||||
[env:adafruit_feather_nrf52840]
|
||||
platform = nordicnrf52
|
||||
board = adafruit_feather_nrf52840
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
build_flags = -DCFG_DEBUG=0
|
||||
|
||||
[env:esp32dev]
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
|
||||
+142
-203
@@ -1,136 +1,30 @@
|
||||
/**
|
||||
* VictronBLE Example
|
||||
* VictronBLE Multi-Device Example
|
||||
*
|
||||
* This example demonstrates how to use the VictronBLE library to read data
|
||||
* from multiple Victron devices simultaneously.
|
||||
* Demonstrates reading data from multiple Victron device types via BLE.
|
||||
*
|
||||
* Hardware Requirements:
|
||||
* - ESP32 board
|
||||
* - Victron devices with BLE (SmartSolar, SmartShunt, etc.)
|
||||
* The same sketch runs on both ESP32 and nRF52840 — the BLE backend is selected
|
||||
* automatically at compile time. Pick the target with the PlatformIO
|
||||
* environment (see platformio.ini): e.g. `esp32dev` or `xiao_nrf52840`.
|
||||
*
|
||||
* Setup:
|
||||
* 1. Get your device encryption keys from the VictronConnect app:
|
||||
* - Open VictronConnect
|
||||
* - Connect to your device
|
||||
* - Go to Settings > Product Info
|
||||
* - Enable "Instant readout via Bluetooth"
|
||||
* - Click "Show" next to "Instant readout details"
|
||||
* - Copy the encryption key (32 hex characters)
|
||||
*
|
||||
* 2. Update the device configurations below with your devices' MAC addresses
|
||||
* and encryption keys
|
||||
* 1. Get your device encryption keys from the VictronConnect app
|
||||
* (Settings > Product Info > Instant readout via Bluetooth > Show)
|
||||
* 2. Update the device configurations below with your MAC and key.
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "VictronBLE.h"
|
||||
|
||||
// Create VictronBLE instance
|
||||
VictronBLE victron;
|
||||
|
||||
// Device callback class - gets called when new data arrives
|
||||
class MyVictronCallback : public VictronDeviceCallback {
|
||||
public:
|
||||
uint32_t solarChargerCount = 0;
|
||||
uint32_t batteryMonitorCount = 0;
|
||||
uint32_t inverterCount = 0;
|
||||
uint32_t dcdcConverterCount = 0;
|
||||
static uint32_t solarChargerCount = 0;
|
||||
static uint32_t batteryMonitorCount = 0;
|
||||
static uint32_t inverterCount = 0;
|
||||
static uint32_t dcdcConverterCount = 0;
|
||||
static uint32_t acChargerCount = 0;
|
||||
|
||||
void onSolarChargerData(const SolarChargerData& data) override {
|
||||
solarChargerCount++;
|
||||
Serial.println("\n=== Solar Charger: " + data.deviceName + " (#" + String(solarChargerCount) + ") ===");
|
||||
Serial.println("MAC: " + data.macAddress);
|
||||
Serial.println("RSSI: " + String(data.rssi) + " dBm");
|
||||
Serial.println("State: " + getChargeStateName(data.chargeState));
|
||||
Serial.println("Battery: " + String(data.batteryVoltage, 2) + " V");
|
||||
Serial.println("Current: " + String(data.batteryCurrent, 2) + " A");
|
||||
Serial.println("Panel Voltage: " + String(data.panelVoltage, 1) + " V");
|
||||
Serial.println("Panel Power: " + String(data.panelPower) + " W");
|
||||
Serial.println("Yield Today: " + String(data.yieldToday) + " Wh");
|
||||
if (data.loadCurrent > 0) {
|
||||
Serial.println("Load Current: " + String(data.loadCurrent, 2) + " A");
|
||||
}
|
||||
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
|
||||
}
|
||||
|
||||
void onBatteryMonitorData(const BatteryMonitorData& data) override {
|
||||
batteryMonitorCount++;
|
||||
Serial.println("\n=== Battery Monitor: " + data.deviceName + " (#" + String(batteryMonitorCount) + ") ===");
|
||||
Serial.println("MAC: " + data.macAddress);
|
||||
Serial.println("RSSI: " + String(data.rssi) + " dBm");
|
||||
Serial.println("Voltage: " + String(data.voltage, 2) + " V");
|
||||
Serial.println("Current: " + String(data.current, 2) + " A");
|
||||
Serial.println("SOC: " + String(data.soc, 1) + " %");
|
||||
Serial.println("Consumed: " + String(data.consumedAh, 2) + " Ah");
|
||||
|
||||
if (data.remainingMinutes < 65535) {
|
||||
int hours = data.remainingMinutes / 60;
|
||||
int mins = data.remainingMinutes % 60;
|
||||
Serial.println("Time Remaining: " + String(hours) + "h " + String(mins) + "m");
|
||||
}
|
||||
|
||||
if (data.temperature > 0) {
|
||||
Serial.println("Temperature: " + String(data.temperature, 1) + " °C");
|
||||
}
|
||||
if (data.auxVoltage > 0) {
|
||||
Serial.println("Aux Voltage: " + String(data.auxVoltage, 2) + " V");
|
||||
}
|
||||
|
||||
// Print alarms
|
||||
if (data.alarmLowVoltage || data.alarmHighVoltage || data.alarmLowSOC ||
|
||||
data.alarmLowTemperature || data.alarmHighTemperature) {
|
||||
Serial.print("ALARMS: ");
|
||||
if (data.alarmLowVoltage) Serial.print("LOW-V ");
|
||||
if (data.alarmHighVoltage) Serial.print("HIGH-V ");
|
||||
if (data.alarmLowSOC) Serial.print("LOW-SOC ");
|
||||
if (data.alarmLowTemperature) Serial.print("LOW-TEMP ");
|
||||
if (data.alarmHighTemperature) Serial.print("HIGH-TEMP ");
|
||||
Serial.println();
|
||||
}
|
||||
|
||||
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
|
||||
}
|
||||
|
||||
void onInverterData(const InverterData& data) override {
|
||||
inverterCount++;
|
||||
Serial.println("\n=== Inverter/Charger: " + data.deviceName + " (#" + String(inverterCount) + ") ===");
|
||||
Serial.println("MAC: " + data.macAddress);
|
||||
Serial.println("RSSI: " + String(data.rssi) + " dBm");
|
||||
Serial.println("Battery: " + String(data.batteryVoltage, 2) + " V");
|
||||
Serial.println("Current: " + String(data.batteryCurrent, 2) + " A");
|
||||
Serial.println("AC Power: " + String(data.acPower) + " W");
|
||||
Serial.println("State: " + String(data.state));
|
||||
|
||||
// Print alarms
|
||||
if (data.alarmLowVoltage || data.alarmHighVoltage ||
|
||||
data.alarmHighTemperature || data.alarmOverload) {
|
||||
Serial.print("ALARMS: ");
|
||||
if (data.alarmLowVoltage) Serial.print("LOW-V ");
|
||||
if (data.alarmHighVoltage) Serial.print("HIGH-V ");
|
||||
if (data.alarmHighTemperature) Serial.print("TEMP ");
|
||||
if (data.alarmOverload) Serial.print("OVERLOAD ");
|
||||
Serial.println();
|
||||
}
|
||||
|
||||
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
|
||||
}
|
||||
|
||||
void onDCDCConverterData(const DCDCConverterData& data) override {
|
||||
dcdcConverterCount++;
|
||||
Serial.println("\n=== DC-DC Converter: " + data.deviceName + " (#" + String(dcdcConverterCount) + ") ===");
|
||||
Serial.println("MAC: " + data.macAddress);
|
||||
Serial.println("RSSI: " + String(data.rssi) + " dBm");
|
||||
Serial.println("Input: " + String(data.inputVoltage, 2) + " V");
|
||||
Serial.println("Output: " + String(data.outputVoltage, 2) + " V");
|
||||
Serial.println("Current: " + String(data.outputCurrent, 2) + " A");
|
||||
Serial.println("State: " + String(data.chargeState));
|
||||
if (data.errorCode != 0) {
|
||||
Serial.println("Error Code: " + String(data.errorCode));
|
||||
}
|
||||
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
|
||||
}
|
||||
|
||||
private:
|
||||
String getChargeStateName(SolarChargerState state) {
|
||||
static const char* chargeStateName(uint8_t state) {
|
||||
switch (state) {
|
||||
case CHARGER_OFF: return "Off";
|
||||
case CHARGER_LOW_POWER: return "Low Power";
|
||||
@@ -145,115 +39,160 @@ private:
|
||||
case CHARGER_EXTERNAL_CONTROL: return "External Control";
|
||||
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;
|
||||
}
|
||||
case DEVICE_TYPE_AC_CHARGER: {
|
||||
const auto& ac = dev->acCharger;
|
||||
acChargerCount++;
|
||||
Serial.printf("\n=== AC Charger: %s (#%lu) ===\n", dev->name, acChargerCount);
|
||||
Serial.printf("MAC: %s\n", dev->mac);
|
||||
Serial.printf("RSSI: %d dBm\n", dev->rssi);
|
||||
Serial.printf("State: %s\n", chargeStateName(ac.chargeState));
|
||||
Serial.printf("Output 1: %.2f V %.2f A\n", ac.voltage1, ac.current1);
|
||||
if (ac.voltage2 > 0) Serial.printf("Output 2: %.2f V %.2f A\n", ac.voltage2, ac.current2);
|
||||
if (ac.voltage3 > 0) Serial.printf("Output 3: %.2f V %.2f A\n", ac.voltage3, ac.current3);
|
||||
if (ac.temperature != 0) Serial.printf("Temperature: %.0f C\n", ac.temperature);
|
||||
if (ac.acCurrent > 0) Serial.printf("AC Current: %.2f A\n", ac.acCurrent);
|
||||
Serial.printf("Last Update: %lus ago\n", (millis() - dev->lastUpdate) / 1000);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
// Wait briefly for USB CDC serial (nRF52/native-USB boards); don't block forever
|
||||
uint32_t start = millis();
|
||||
while (!Serial && (millis() - start) < 5000) delay(10);
|
||||
|
||||
Serial.println("\n\n=================================");
|
||||
Serial.println("VictronBLE Multi-Device Example");
|
||||
Serial.println("=================================\n");
|
||||
|
||||
// Initialize VictronBLE with 5 second scan duration
|
||||
if (!victron.begin(5)) {
|
||||
Serial.println("ERROR: Failed to initialize VictronBLE!");
|
||||
Serial.println(victron.getLastError());
|
||||
while (1) delay(1000);
|
||||
}
|
||||
|
||||
// Enable debug output (optional)
|
||||
victron.setDebug(false);
|
||||
victron.setDebug(true);
|
||||
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
|
||||
// Replace with your own devices (MAC + 32-char hex key from VictronConnect)
|
||||
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(
|
||||
"ScottTrailer", // Device name
|
||||
"e64559783cfb",
|
||||
"3fa658aded4f309b9bc17a2318cb1f56",
|
||||
DEVICE_TYPE_SOLAR_CHARGER // Device type
|
||||
);
|
||||
|
||||
// Example: Solar Charger #1
|
||||
/*
|
||||
victron.addDevice(
|
||||
"MPPT 100/30", // Device name
|
||||
"E7:48:D4:28:B7:9C", // MAC address
|
||||
"0df4d0395b7d1a876c0c33ecb9e70dcd", // Encryption key (32 hex chars)
|
||||
DEVICE_TYPE_SOLAR_CHARGER // Device type
|
||||
);
|
||||
*/
|
||||
|
||||
// Example: Solar Charger #2
|
||||
/*
|
||||
victron.addDevice(
|
||||
"MPPT 75/15",
|
||||
"AA:BB:CC:DD:EE:FF",
|
||||
"1234567890abcdef1234567890abcdef",
|
||||
"Rainbow48V",
|
||||
"E4:05:42:34:14:F3",
|
||||
"0ec3adf7433dd61793ff2f3b8ad32ed8",
|
||||
DEVICE_TYPE_SOLAR_CHARGER
|
||||
);
|
||||
*/
|
||||
|
||||
// Example: Battery Monitor (SmartShunt)
|
||||
/*
|
||||
victron.addDevice(
|
||||
"SmartShunt",
|
||||
"11:22:33:44:55:66",
|
||||
"fedcba0987654321fedcba0987654321",
|
||||
DEVICE_TYPE_BATTERY_MONITOR
|
||||
"ScottTrailer",
|
||||
"e64559783cfb",
|
||||
"3fa658aded4f309b9bc17a2318cb1f56",
|
||||
DEVICE_TYPE_SOLAR_CHARGER
|
||||
);
|
||||
*/
|
||||
|
||||
// Example: Inverter/Charger
|
||||
/*
|
||||
victron.addDevice(
|
||||
"MultiPlus",
|
||||
"99:88:77:66:55:44",
|
||||
"abcdefabcdefabcdefabcdefabcdefab",
|
||||
DEVICE_TYPE_INVERTER
|
||||
);
|
||||
*/
|
||||
|
||||
Serial.println("Configured " + String(victron.getDeviceCount()) + " devices");
|
||||
Serial.printf("Configured %d devices\n", (int)victron.getDeviceCount());
|
||||
Serial.println("\nStarting BLE scan...\n");
|
||||
}
|
||||
|
||||
static uint32_t loopCount = 0;
|
||||
static uint32_t lastReport = 0;
|
||||
|
||||
void loop() {
|
||||
// Process BLE scanning and data updates
|
||||
victron.loop();
|
||||
victron.loop(); // Non-blocking on both backends
|
||||
loopCount++;
|
||||
|
||||
// Optional: You can also manually query device data
|
||||
// This is useful if you're not using callbacks
|
||||
/*
|
||||
SolarChargerData solarData;
|
||||
if (victron.getSolarChargerData("E7:48:D4:28:B7:9C", solarData)) {
|
||||
// Do something with solarData
|
||||
uint32_t now = millis();
|
||||
if (now - lastReport >= 10000) {
|
||||
Serial.printf("Loop iterations in last 10s: %lu\n", loopCount);
|
||||
loopCount = 0;
|
||||
lastReport = now;
|
||||
}
|
||||
|
||||
BatteryMonitorData batteryData;
|
||||
if (victron.getBatteryMonitorData("11:22:33:44:55:66", batteryData)) {
|
||||
// Do something with batteryData
|
||||
}
|
||||
*/
|
||||
|
||||
// Add a small delay to avoid overwhelming the serial output
|
||||
delay(100);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ struct __attribute__((packed)) SolarChargerPacket {
|
||||
uint8_t chargeState;
|
||||
float batteryVoltage; // V
|
||||
float batteryCurrent; // A
|
||||
float panelVoltage; // V
|
||||
float panelPower; // W
|
||||
uint16_t yieldToday; // Wh
|
||||
float loadCurrent; // A
|
||||
@@ -82,13 +81,12 @@ void onDataRecv(const uint8_t* senderMac, const uint8_t* data, int len) {
|
||||
memcpy(name, pkt->deviceName, 16);
|
||||
name[16] = '\0';
|
||||
|
||||
Serial.printf("[RX #%lu] %s | State:%s Batt:%.2fV %.2fA PV:%.1fV %.0fW Yield:%uWh",
|
||||
Serial.printf("[RX #%lu] %s | State:%s Batt:%.2fV %.2fA PV:%.0fW Yield:%uWh",
|
||||
recvCount,
|
||||
name,
|
||||
chargeStateName(pkt->chargeState),
|
||||
pkt->batteryVoltage,
|
||||
pkt->batteryCurrent,
|
||||
pkt->panelVoltage,
|
||||
pkt->panelPower,
|
||||
pkt->yieldToday);
|
||||
|
||||
@@ -202,7 +200,7 @@ void loop() {
|
||||
M5.Lcd.printf("Batt: %.2fA\n", pkt.batteryCurrent);
|
||||
|
||||
// Row 3: PV
|
||||
M5.Lcd.printf("PV: %.1fV %.0fW\n", pkt.panelVoltage, pkt.panelPower);
|
||||
M5.Lcd.printf("PV: %.0fW\n", pkt.panelPower);
|
||||
|
||||
// Row 4: yield + load
|
||||
M5.Lcd.printf("Yield: %uWh", pkt.yieldToday);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* VictronBLE Repeater Example
|
||||
*
|
||||
* Collects Solar Charger data via BLE and transmits the latest
|
||||
* readings over ESPNow broadcast every 30 seconds. Place this ESP32
|
||||
* readings over ESPNow broadcast every 5 seconds. Place this ESP32
|
||||
* near Victron devices and use a separate Receiver ESP32 at a distance.
|
||||
*
|
||||
* ESPNow range is typically much greater than BLE (~200m+ line of sight).
|
||||
@@ -23,7 +23,6 @@ struct __attribute__((packed)) SolarChargerPacket {
|
||||
uint8_t chargeState;
|
||||
float batteryVoltage; // V
|
||||
float batteryCurrent; // A
|
||||
float panelVoltage; // V
|
||||
float panelPower; // W
|
||||
uint16_t yieldToday; // Wh
|
||||
float loadCurrent; // A
|
||||
@@ -31,10 +30,8 @@ struct __attribute__((packed)) SolarChargerPacket {
|
||||
char deviceName[16]; // Null-terminated, truncated
|
||||
};
|
||||
|
||||
// Broadcast address
|
||||
static const uint8_t BROADCAST_ADDR[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
|
||||
static const unsigned long SEND_INTERVAL_MS = 5000; // 30 seconds
|
||||
static const unsigned long SEND_INTERVAL_MS = 5000;
|
||||
|
||||
static uint32_t sendCount = 0;
|
||||
static uint32_t sendFailCount = 0;
|
||||
@@ -49,7 +46,6 @@ static unsigned long lastSendTime = 0;
|
||||
|
||||
VictronBLE victron;
|
||||
|
||||
// Find cached slot by device name, or allocate a new one
|
||||
static int findOrAddCached(const char* name) {
|
||||
for (int i = 0; i < cachedCount; i++) {
|
||||
if (strncmp(cachedPackets[i].deviceName, name, sizeof(cachedPackets[i].deviceName)) == 0)
|
||||
@@ -59,34 +55,28 @@ static int findOrAddCached(const char* name) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
class RepeaterCallback : public VictronDeviceCallback {
|
||||
public:
|
||||
void onSolarChargerData(const SolarChargerData& data) override {
|
||||
void onVictronData(const VictronDevice* dev) {
|
||||
if (dev->deviceType != DEVICE_TYPE_SOLAR_CHARGER) return;
|
||||
blePacketCount++;
|
||||
const auto& s = dev->solar;
|
||||
|
||||
// Build packet
|
||||
SolarChargerPacket pkt;
|
||||
pkt.chargeState = static_cast<uint8_t>(data.chargeState);
|
||||
pkt.batteryVoltage = data.batteryVoltage;
|
||||
pkt.batteryCurrent = data.batteryCurrent;
|
||||
pkt.panelVoltage = data.panelVoltage;
|
||||
pkt.panelPower = data.panelPower;
|
||||
pkt.yieldToday = data.yieldToday;
|
||||
pkt.loadCurrent = data.loadCurrent;
|
||||
pkt.rssi = data.rssi;
|
||||
pkt.chargeState = s.chargeState;
|
||||
pkt.batteryVoltage = s.batteryVoltage;
|
||||
pkt.batteryCurrent = s.batteryCurrent;
|
||||
pkt.panelPower = s.panelPower;
|
||||
pkt.yieldToday = s.yieldToday;
|
||||
pkt.loadCurrent = s.loadCurrent;
|
||||
pkt.rssi = dev->rssi;
|
||||
memset(pkt.deviceName, 0, sizeof(pkt.deviceName));
|
||||
strncpy(pkt.deviceName, data.deviceName.c_str(), sizeof(pkt.deviceName) - 1);
|
||||
strncpy(pkt.deviceName, dev->name, sizeof(pkt.deviceName) - 1);
|
||||
|
||||
// Cache it
|
||||
int idx = findOrAddCached(pkt.deviceName);
|
||||
if (idx >= 0) {
|
||||
cachedPackets[idx] = pkt;
|
||||
cachedValid[idx] = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
RepeaterCallback callback;
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
@@ -98,7 +88,8 @@ void setup() {
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.disconnect();
|
||||
|
||||
Serial.println("MAC: " + WiFi.macAddress());
|
||||
Serial.print("MAC: ");
|
||||
Serial.println(WiFi.macAddress());
|
||||
|
||||
// Init ESPNow
|
||||
if (esp_now_init() != ESP_OK) {
|
||||
@@ -106,10 +97,9 @@ void setup() {
|
||||
while (1) delay(1000);
|
||||
}
|
||||
|
||||
// Add broadcast peer
|
||||
esp_now_peer_info_t peerInfo = {};
|
||||
memcpy(peerInfo.peer_addr, BROADCAST_ADDR, 6);
|
||||
peerInfo.channel = 0; // Use current channel
|
||||
peerInfo.channel = 0;
|
||||
peerInfo.encrypt = false;
|
||||
|
||||
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
|
||||
@@ -122,14 +112,12 @@ void setup() {
|
||||
// Init VictronBLE
|
||||
if (!victron.begin(5)) {
|
||||
Serial.println("ERROR: Failed to initialize VictronBLE!");
|
||||
Serial.println(victron.getLastError());
|
||||
while (1) delay(1000);
|
||||
}
|
||||
|
||||
victron.setDebug(false);
|
||||
victron.setCallback(&callback);
|
||||
victron.setCallback(onVictronData);
|
||||
|
||||
// Add your devices here
|
||||
victron.addDevice(
|
||||
"Rainbow48V",
|
||||
"E4:05:42:34:14:F3",
|
||||
@@ -144,14 +132,13 @@ void setup() {
|
||||
DEVICE_TYPE_SOLAR_CHARGER
|
||||
);
|
||||
|
||||
Serial.println("Configured " + String(victron.getDeviceCount()) + " BLE devices");
|
||||
Serial.println("Packet size: " + String(sizeof(SolarChargerPacket)) + " bytes\n");
|
||||
Serial.printf("Configured %d BLE devices\n", (int)victron.getDeviceCount());
|
||||
Serial.printf("Packet size: %d bytes\n\n", (int)sizeof(SolarChargerPacket));
|
||||
}
|
||||
|
||||
void loop() {
|
||||
victron.loop();
|
||||
victron.loop(); // Blocks for scanDuration seconds
|
||||
|
||||
// Send cached packets every 30 seconds
|
||||
unsigned long now = millis();
|
||||
if (now - lastSendTime >= SEND_INTERVAL_MS) {
|
||||
lastSendTime = now;
|
||||
@@ -167,11 +154,10 @@ void loop() {
|
||||
if (result == ESP_OK) {
|
||||
sendCount++;
|
||||
sent++;
|
||||
Serial.printf("[ESPNow] Sent %s: %.2fV %.1fA PV:%.1fV %.0fW State:%d\n",
|
||||
Serial.printf("[ESPNow] Sent %s: %.2fV %.1fA %.0fW State:%d\n",
|
||||
cachedPackets[i].deviceName,
|
||||
cachedPackets[i].batteryVoltage,
|
||||
cachedPackets[i].batteryCurrent,
|
||||
cachedPackets[i].panelVoltage,
|
||||
cachedPackets[i].panelPower,
|
||||
cachedPackets[i].chargeState);
|
||||
} else {
|
||||
@@ -185,5 +171,4 @@ void loop() {
|
||||
blePacketCount, sendCount, sendFailCount, cachedCount);
|
||||
}
|
||||
|
||||
delay(100);
|
||||
}
|
||||
|
||||
+14
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "victronble",
|
||||
"version": "0.3.1",
|
||||
"description": "ESP32 library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements. Supports SmartSolar MPPT, SmartShunt, BMV, MultiPlus, Orion and other Victron devices.",
|
||||
"keywords": "victron, ble, bluetooth, solar, mppt, battery, smartshunt, smartsolar, bmv, inverter, multiplus, esp32, iot, energy, monitoring",
|
||||
"version": "0.6.0",
|
||||
"description": "Portable Arduino library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements. Runs on ESP32, ESP32-S3, ESP32-C3 and nRF52 (nRF52840, nRF52832). Supports SmartSolar MPPT, SmartShunt, BMV, MultiPlus, Orion, Blue Smart AC chargers and other Victron devices. No external crypto dependency.",
|
||||
"keywords": "victron, ble, bluetooth, solar, mppt, battery, smartshunt, smartsolar, bmv, inverter, multiplus, esp32, esp32-s3, esp32-c3, nrf52, nrf52840, nrf52832, xiao, iot, energy, monitoring",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitea.sh3d.com.au/Sh3d/VictronBLE.git"
|
||||
@@ -18,7 +18,7 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://gitea.sh3d.com.au/Sh3d/VictronBLE",
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"platforms": ["espressif32"],
|
||||
"platforms": ["espressif32", "nordicnrf52"],
|
||||
"headers": ["VictronBLE.h"],
|
||||
"dependencies": [],
|
||||
"examples": [
|
||||
@@ -36,6 +36,16 @@
|
||||
"name": "Repeater",
|
||||
"base": "examples/Repeater",
|
||||
"files": ["src/main.cpp"]
|
||||
},
|
||||
{
|
||||
"name": "Receiver",
|
||||
"base": "examples/Receiver",
|
||||
"files": ["src/main.cpp"]
|
||||
},
|
||||
{
|
||||
"name": "FakeRepeater",
|
||||
"base": "examples/FakeRepeater",
|
||||
"files": ["src/main.cpp"]
|
||||
}
|
||||
],
|
||||
"export": {
|
||||
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
name=VictronBLE
|
||||
version=0.3.1
|
||||
version=0.6.0
|
||||
author=Scott Penrose
|
||||
maintainer=Scott Penrose <scottp@dd.com.au>
|
||||
sentence=ESP32 library for reading Victron Energy device data via BLE for any ESP32
|
||||
paragraph=Read data from Victron SmartSolar, SmartShunt, BMV, inverters and other devices using Bluetooth Low Energy advertisements. Supports multiple devices simultaneously with no pairing required.
|
||||
sentence=Portable library for reading Victron Energy device data via BLE on ESP32/S3/C3 and nRF52 (nRF52840/nRF52832)
|
||||
paragraph=Read data from Victron SmartSolar, SmartShunt, BMV, inverters, Blue Smart AC chargers and other devices using Bluetooth Low Energy advertisements. Runs on ESP32, ESP32-S3, ESP32-C3 and nRF52 (nRF52840, nRF52832 — Bluefruit or Seeed cores) with no external crypto dependency. Supports multiple devices simultaneously with no pairing required.
|
||||
category=Communication
|
||||
url=https://gitea.sh3d.com.au/Sh3d/VictronBLE
|
||||
architectures=esp32
|
||||
architectures=esp32,nrf52
|
||||
depends=
|
||||
includes=VictronBLE.h
|
||||
|
||||
+267
-529
@@ -1,640 +1,378 @@
|
||||
/**
|
||||
* VictronBLE - ESP32 library for Victron Energy BLE devices
|
||||
* Implementation file
|
||||
* VictronBLE - portable library for Victron Energy BLE devices
|
||||
* Common implementation (platform-independent: decoding + AES-128-CTR decrypt).
|
||||
* BLE scanning lives in the per-platform backends under src/esp32 and src/nrf52.
|
||||
*
|
||||
* Copyright (c) 2025 Scott Penrose
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
#include "VictronBLE.h"
|
||||
#include "crypto/vble_aes.h"
|
||||
#include <string.h>
|
||||
|
||||
// Constructor
|
||||
VictronBLE::VictronBLE()
|
||||
: pBLEScan(nullptr), scanCallback(nullptr), callback(nullptr),
|
||||
debugEnabled(false), scanDuration(5), initialized(false) {
|
||||
: deviceCount(0), callback(nullptr), debugEnabled(false),
|
||||
scanDuration(5), minIntervalMs(1000), initialized(false)
|
||||
#if defined(VICTRON_BACKEND_ESP32)
|
||||
, pBLEScan(nullptr), scanCallbackObj(nullptr)
|
||||
#endif
|
||||
{
|
||||
memset(devices, 0, sizeof(devices));
|
||||
}
|
||||
|
||||
// Destructor
|
||||
VictronBLE::~VictronBLE() {
|
||||
for (auto& pair : devices) {
|
||||
delete pair.second;
|
||||
}
|
||||
devices.clear();
|
||||
bool VictronBLE::addDevice(const char* name, const char* mac, const char* hexKey,
|
||||
VictronDeviceType type) {
|
||||
if (deviceCount >= VICTRON_MAX_DEVICES) return false;
|
||||
if (!hexKey || strlen(hexKey) != 32) return false;
|
||||
if (!mac || strlen(mac) == 0) return false;
|
||||
|
||||
if (pBLEScan) {
|
||||
pBLEScan->stop();
|
||||
}
|
||||
char normalizedMAC[VICTRON_MAC_LEN];
|
||||
normalizeMAC(mac, normalizedMAC);
|
||||
|
||||
delete scanCallback;
|
||||
}
|
||||
// Check for duplicate
|
||||
if (findDevice(normalizedMAC)) return false;
|
||||
|
||||
// Initialize BLE
|
||||
bool VictronBLE::begin(uint32_t scanDuration) {
|
||||
if (initialized) {
|
||||
if (debugEnabled) debugPrint("VictronBLE already initialized");
|
||||
return true;
|
||||
}
|
||||
DeviceEntry* entry = &devices[deviceCount];
|
||||
memset(entry, 0, sizeof(DeviceEntry));
|
||||
entry->active = true;
|
||||
|
||||
this->scanDuration = scanDuration;
|
||||
strncpy(entry->device.name, name ? name : "", VICTRON_NAME_LEN - 1);
|
||||
entry->device.name[VICTRON_NAME_LEN - 1] = '\0';
|
||||
memcpy(entry->device.mac, normalizedMAC, VICTRON_MAC_LEN);
|
||||
entry->device.deviceType = type;
|
||||
entry->device.rssi = -100;
|
||||
|
||||
if (debugEnabled) debugPrint("Initializing VictronBLE...");
|
||||
if (!hexToBytes(hexKey, entry->key, 16)) return false;
|
||||
|
||||
BLEDevice::init("VictronBLE");
|
||||
pBLEScan = BLEDevice::getScan();
|
||||
|
||||
if (!pBLEScan) {
|
||||
lastError = "Failed to create BLE scanner";
|
||||
if (debugEnabled) debugPrint(lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
scanCallback = new VictronBLEAdvertisedDeviceCallbacks(this);
|
||||
pBLEScan->setAdvertisedDeviceCallbacks(scanCallback, true);
|
||||
pBLEScan->setActiveScan(false); // Passive scan - lower power
|
||||
pBLEScan->setInterval(100);
|
||||
pBLEScan->setWindow(99);
|
||||
|
||||
initialized = true;
|
||||
if (debugEnabled) debugPrint("VictronBLE initialized successfully");
|
||||
deviceCount++;
|
||||
|
||||
if (debugEnabled) Serial.printf("[VictronBLE] Added: %s (%s)\n", name, normalizedMAC);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add a device to monitor
|
||||
bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
|
||||
if (config.macAddress.length() == 0) {
|
||||
lastError = "MAC address cannot be empty";
|
||||
if (debugEnabled) debugPrint(lastError);
|
||||
return false;
|
||||
}
|
||||
// Platform-independent advertisement handler. Each BLE backend extracts the
|
||||
// manufacturer-data bytes (vendor ID first), MAC string and RSSI from a scan
|
||||
// result and feeds them here.
|
||||
void VictronBLE::onAdvertisement(const uint8_t* mfgData, size_t len,
|
||||
const char* macStr, int8_t rssi) {
|
||||
if (!mfgData || len < 10) return;
|
||||
|
||||
if (config.encryptionKey.length() != 32) {
|
||||
lastError = "Encryption key must be 32 hex characters";
|
||||
if (debugEnabled) debugPrint(lastError);
|
||||
return false;
|
||||
}
|
||||
// Quick vendor ID check before any other work
|
||||
uint16_t vendorID = mfgData[0] | ((uint16_t)mfgData[1] << 8);
|
||||
if (vendorID != VICTRON_MANUFACTURER_ID) return;
|
||||
|
||||
String normalizedMAC = normalizeMAC(config.macAddress);
|
||||
// Copy into the wire-format struct
|
||||
victronManufacturerData mfg;
|
||||
memset(&mfg, 0, sizeof(mfg));
|
||||
size_t copyLen = len > sizeof(mfg) ? sizeof(mfg) : len;
|
||||
memcpy(&mfg, mfgData, copyLen);
|
||||
|
||||
// Check if device already exists
|
||||
if (devices.find(normalizedMAC) != devices.end()) {
|
||||
if (debugEnabled) debugPrint("Device " + normalizedMAC + " already exists, updating config");
|
||||
delete devices[normalizedMAC];
|
||||
}
|
||||
// Normalize MAC and find device
|
||||
char normalizedMAC[VICTRON_MAC_LEN];
|
||||
normalizeMAC(macStr, normalizedMAC);
|
||||
|
||||
DeviceInfo* info = new DeviceInfo();
|
||||
info->config = config;
|
||||
info->config.macAddress = normalizedMAC;
|
||||
|
||||
// Convert encryption key from hex string to bytes
|
||||
if (!hexStringToBytes(config.encryptionKey, info->encryptionKeyBytes, 16)) {
|
||||
lastError = "Invalid encryption key format";
|
||||
if (debugEnabled) debugPrint(lastError);
|
||||
delete info;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create appropriate data structure based on device type
|
||||
info->data = createDeviceData(config.expectedType);
|
||||
if (info->data) {
|
||||
info->data->macAddress = normalizedMAC;
|
||||
info->data->deviceName = config.name;
|
||||
}
|
||||
|
||||
devices[normalizedMAC] = info;
|
||||
|
||||
if (debugEnabled) {
|
||||
debugPrint("Added device: " + config.name + " (MAC: " + normalizedMAC + ")");
|
||||
debugPrint(" Original MAC input: " + config.macAddress);
|
||||
debugPrint(" Stored normalized: " + normalizedMAC);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VictronBLE::addDevice(const String& name, const String& macAddress, const String& encryptionKey,
|
||||
VictronDeviceType expectedType) {
|
||||
VictronDeviceConfig config(name, macAddress, encryptionKey, expectedType);
|
||||
return addDevice(config);
|
||||
}
|
||||
|
||||
// Remove a device
|
||||
void VictronBLE::removeDevice(const String& macAddress) {
|
||||
String normalizedMAC = normalizeMAC(macAddress);
|
||||
|
||||
auto it = devices.find(normalizedMAC);
|
||||
if (it != devices.end()) {
|
||||
delete it->second;
|
||||
devices.erase(it);
|
||||
if (debugEnabled) debugPrint("Removed device: " + normalizedMAC);
|
||||
}
|
||||
}
|
||||
|
||||
// Main loop function
|
||||
void VictronBLE::loop() {
|
||||
if (!initialized) {
|
||||
DeviceEntry* entry = findDevice(normalizedMAC);
|
||||
if (!entry) {
|
||||
if (debugEnabled) Serial.printf("[VictronBLE] Unmonitored Victron: %s\n", normalizedMAC);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start a scan
|
||||
BLEScanResults scanResults = pBLEScan->start(scanDuration, false);
|
||||
pBLEScan->clearResults();
|
||||
}
|
||||
|
||||
// BLE callback implementation
|
||||
void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertisedDevice) {
|
||||
if (victronBLE) {
|
||||
victronBLE->processDevice(advertisedDevice);
|
||||
}
|
||||
}
|
||||
|
||||
// Process advertised device
|
||||
void VictronBLE::processDevice(BLEAdvertisedDevice& advertisedDevice) {
|
||||
// Get MAC address from the advertised device
|
||||
String mac = macAddressToString(advertisedDevice.getAddress());
|
||||
String normalizedMAC = normalizeMAC(mac);
|
||||
|
||||
if (debugEnabled) {
|
||||
debugPrint("Raw MAC: " + mac + " -> Normalized: " + normalizedMAC);
|
||||
}
|
||||
|
||||
// Parse manufacturer data into local struct
|
||||
victronManufacturerData mfgData;
|
||||
memset(&mfgData, 0, sizeof(mfgData));
|
||||
if (advertisedDevice.haveManufacturerData()) {
|
||||
std::string rawMfgData = advertisedDevice.getManufacturerData();
|
||||
if (debugEnabled) debugPrint("Getting manufacturer data: Size=" + String(rawMfgData.length()));
|
||||
rawMfgData.copy(reinterpret_cast<char*>(&mfgData),
|
||||
(rawMfgData.length() > sizeof(mfgData) ? sizeof(mfgData) : rawMfgData.length()));
|
||||
}
|
||||
|
||||
// Debug: Log all discovered BLE devices
|
||||
if (debugEnabled) {
|
||||
String debugMsg = "BLE Device: " + mac;
|
||||
debugMsg += ", RSSI: " + String(advertisedDevice.getRSSI()) + " dBm";
|
||||
if (advertisedDevice.haveName())
|
||||
debugMsg += ", Name: " + String(advertisedDevice.getName().c_str());
|
||||
|
||||
debugMsg += ", Mfg ID: 0x" + String(mfgData.vendorID, HEX);
|
||||
if (mfgData.vendorID == VICTRON_MANUFACTURER_ID) {
|
||||
debugMsg += " (Victron)";
|
||||
}
|
||||
|
||||
debugPrint(debugMsg);
|
||||
}
|
||||
|
||||
// Check if this is one of our configured devices
|
||||
auto it = devices.find(normalizedMAC);
|
||||
if (it == devices.end()) {
|
||||
if (debugEnabled && mfgData.vendorID == VICTRON_MANUFACTURER_ID) {
|
||||
debugPrint("Found unmonitored Victron Device: " + normalizedMAC);
|
||||
}
|
||||
return; // Not a device we're monitoring
|
||||
}
|
||||
|
||||
DeviceInfo* deviceInfo = it->second;
|
||||
|
||||
// Check if it's Victron (manufacturer ID 0x02E1)
|
||||
if (mfgData.vendorID != VICTRON_MANUFACTURER_ID) {
|
||||
if (debugEnabled) debugPrint("Skipping non VICTRON");
|
||||
// Skip if nonce unchanged (data hasn't changed on the device)
|
||||
if (entry->device.dataValid && mfg.nonceDataCounter == entry->lastNonce) {
|
||||
entry->device.rssi = rssi; // still refresh RSSI
|
||||
return;
|
||||
}
|
||||
|
||||
if (debugEnabled) debugPrint("Processing data from: " + deviceInfo->config.name);
|
||||
|
||||
// Parse the advertisement
|
||||
if (parseAdvertisement(deviceInfo, mfgData)) {
|
||||
// Update RSSI
|
||||
if (deviceInfo->data) {
|
||||
deviceInfo->data->rssi = advertisedDevice.getRSSI();
|
||||
deviceInfo->data->lastUpdate = millis();
|
||||
// Skip if 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, mfg.nonceDataCounter);
|
||||
|
||||
if (parseAdvertisement(entry, mfg)) {
|
||||
entry->lastNonce = mfg.nonceDataCounter;
|
||||
entry->device.rssi = rssi;
|
||||
entry->device.lastUpdate = now;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse advertisement data
|
||||
bool VictronBLE::parseAdvertisement(DeviceInfo* deviceInfo, const victronManufacturerData& mfgData) {
|
||||
bool VictronBLE::parseAdvertisement(DeviceEntry* entry, const victronManufacturerData& mfg) {
|
||||
if (debugEnabled) {
|
||||
debugPrint("Vendor ID: 0x" + String(mfgData.vendorID, HEX));
|
||||
debugPrint("Beacon Type: 0x" + String(mfgData.beaconType, HEX));
|
||||
debugPrint("Record Type: 0x" + String(mfgData.victronRecordType, HEX));
|
||||
debugPrint("Nonce: 0x" + String(mfgData.nonceDataCounter, HEX));
|
||||
Serial.printf("[VictronBLE] Beacon:0x%02X Record:0x%02X Nonce:0x%04X\n",
|
||||
mfg.beaconType, mfg.victronRecordType, mfg.nonceDataCounter);
|
||||
}
|
||||
|
||||
// Build IV (initialization vector) from nonce
|
||||
// IV is 16 bytes: nonce (2 bytes little-endian) + zeros (14 bytes)
|
||||
// Quick key check before expensive decryption
|
||||
if (mfg.encryptKeyMatch != entry->key[0]) {
|
||||
if (debugEnabled) Serial.println("[VictronBLE] Key byte mismatch");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build IV from nonce (2 bytes little-endian + 14 zero bytes)
|
||||
uint8_t iv[16] = {0};
|
||||
iv[0] = mfgData.nonceDataCounter & 0xFF; // Low byte
|
||||
iv[1] = (mfgData.nonceDataCounter >> 8) & 0xFF; // High byte
|
||||
// Remaining bytes stay zero
|
||||
iv[0] = mfg.nonceDataCounter & 0xFF;
|
||||
iv[1] = (mfg.nonceDataCounter >> 8) & 0xFF;
|
||||
|
||||
// Decrypt the data
|
||||
const size_t encryptedLen = sizeof(mfgData.victronEncryptedData);
|
||||
uint8_t decrypted[encryptedLen];
|
||||
if (!decryptAdvertisement(mfgData.victronEncryptedData,
|
||||
encryptedLen,
|
||||
deviceInfo->encryptionKeyBytes, iv, decrypted)) {
|
||||
lastError = "Decryption failed";
|
||||
if (debugEnabled) debugPrint(lastError);
|
||||
// Decrypt
|
||||
uint8_t decrypted[VICTRON_ENCRYPTED_LEN];
|
||||
if (!decryptData(mfg.victronEncryptedData, VICTRON_ENCRYPTED_LEN,
|
||||
entry->key, iv, decrypted)) {
|
||||
if (debugEnabled) Serial.println("[VictronBLE] Decryption failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse based on device type
|
||||
bool parseOk = false;
|
||||
|
||||
switch (mfgData.victronRecordType) {
|
||||
// Parse based on record type (auto-detects device type)
|
||||
bool ok = false;
|
||||
switch (mfg.victronRecordType) {
|
||||
case DEVICE_TYPE_SOLAR_CHARGER:
|
||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
|
||||
parseOk = parseSolarCharger(decrypted, encryptedLen,
|
||||
*static_cast<SolarChargerData*>(deviceInfo->data));
|
||||
}
|
||||
entry->device.deviceType = DEVICE_TYPE_SOLAR_CHARGER;
|
||||
ok = parseSolarCharger(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.solar);
|
||||
break;
|
||||
|
||||
case DEVICE_TYPE_BATTERY_MONITOR:
|
||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) {
|
||||
parseOk = parseBatteryMonitor(decrypted, encryptedLen,
|
||||
*static_cast<BatteryMonitorData*>(deviceInfo->data));
|
||||
}
|
||||
entry->device.deviceType = DEVICE_TYPE_BATTERY_MONITOR;
|
||||
ok = parseBatteryMonitor(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.battery);
|
||||
break;
|
||||
|
||||
case DEVICE_TYPE_INVERTER:
|
||||
case DEVICE_TYPE_INVERTER_RS:
|
||||
case DEVICE_TYPE_MULTI_RS:
|
||||
case DEVICE_TYPE_VE_BUS:
|
||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_INVERTER) {
|
||||
parseOk = parseInverter(decrypted, encryptedLen,
|
||||
*static_cast<InverterData*>(deviceInfo->data));
|
||||
}
|
||||
entry->device.deviceType = DEVICE_TYPE_INVERTER;
|
||||
ok = parseInverter(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.inverter);
|
||||
break;
|
||||
|
||||
case DEVICE_TYPE_DCDC_CONVERTER:
|
||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) {
|
||||
parseOk = parseDCDCConverter(decrypted, encryptedLen,
|
||||
*static_cast<DCDCConverterData*>(deviceInfo->data));
|
||||
}
|
||||
entry->device.deviceType = DEVICE_TYPE_DCDC_CONVERTER;
|
||||
ok = parseDCDCConverter(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.dcdc);
|
||||
break;
|
||||
case DEVICE_TYPE_AC_CHARGER:
|
||||
entry->device.deviceType = DEVICE_TYPE_AC_CHARGER;
|
||||
ok = parseACCharger(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.acCharger);
|
||||
break;
|
||||
|
||||
default:
|
||||
if (debugEnabled) debugPrint("Unknown device type: 0x" + String(mfgData.victronRecordType, HEX));
|
||||
if (debugEnabled) Serial.printf("[VictronBLE] Unknown type: 0x%02X\n", mfg.victronRecordType);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parseOk && deviceInfo->data) {
|
||||
deviceInfo->data->dataValid = true;
|
||||
|
||||
// Call appropriate callback
|
||||
if (callback) {
|
||||
switch (mfgData.victronRecordType) {
|
||||
case DEVICE_TYPE_SOLAR_CHARGER:
|
||||
callback->onSolarChargerData(*static_cast<SolarChargerData*>(deviceInfo->data));
|
||||
break;
|
||||
case DEVICE_TYPE_BATTERY_MONITOR:
|
||||
callback->onBatteryMonitorData(*static_cast<BatteryMonitorData*>(deviceInfo->data));
|
||||
break;
|
||||
case DEVICE_TYPE_INVERTER:
|
||||
case DEVICE_TYPE_INVERTER_RS:
|
||||
case DEVICE_TYPE_MULTI_RS:
|
||||
case DEVICE_TYPE_VE_BUS:
|
||||
callback->onInverterData(*static_cast<InverterData*>(deviceInfo->data));
|
||||
break;
|
||||
case DEVICE_TYPE_DCDC_CONVERTER:
|
||||
callback->onDCDCConverterData(*static_cast<DCDCConverterData*>(deviceInfo->data));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
entry->device.dataValid = true;
|
||||
if (callback) callback(&entry->device);
|
||||
}
|
||||
|
||||
return parseOk;
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Decrypt advertisement using AES-128-CTR
|
||||
bool VictronBLE::decryptAdvertisement(const uint8_t* encrypted, size_t encLen,
|
||||
bool VictronBLE::decryptData(const uint8_t* encrypted, size_t len,
|
||||
const uint8_t* key, const uint8_t* iv,
|
||||
uint8_t* decrypted) {
|
||||
mbedtls_aes_context aes;
|
||||
mbedtls_aes_init(&aes);
|
||||
|
||||
// Set encryption key
|
||||
int ret = mbedtls_aes_setkey_enc(&aes, key, 128);
|
||||
if (ret != 0) {
|
||||
mbedtls_aes_free(&aes);
|
||||
return false;
|
||||
}
|
||||
|
||||
// AES-CTR decryption
|
||||
size_t nc_off = 0;
|
||||
uint8_t nonce_counter[16];
|
||||
uint8_t stream_block[16];
|
||||
|
||||
memcpy(nonce_counter, iv, 16);
|
||||
memset(stream_block, 0, 16);
|
||||
|
||||
ret = mbedtls_aes_crypt_ctr(&aes, encLen, &nc_off, nonce_counter,
|
||||
stream_block, encrypted, decrypted);
|
||||
|
||||
mbedtls_aes_free(&aes);
|
||||
|
||||
return (ret == 0);
|
||||
}
|
||||
|
||||
// Parse Solar Charger data
|
||||
bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarChargerData& result) {
|
||||
if (len < sizeof(victronSolarChargerPayload)) {
|
||||
if (debugEnabled) debugPrint("Solar charger data too short: " + String(len) + " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto* payload = reinterpret_cast<const victronSolarChargerPayload*>(data);
|
||||
|
||||
// Parse charge state
|
||||
result.chargeState = static_cast<SolarChargerState>(payload->deviceState);
|
||||
|
||||
// Parse battery voltage (10 mV units -> volts)
|
||||
result.batteryVoltage = payload->batteryVoltage * 0.01f;
|
||||
|
||||
// Parse battery current (10 mA units, signed -> amps)
|
||||
result.batteryCurrent = payload->batteryCurrent * 0.01f;
|
||||
|
||||
// Parse yield today (10 Wh units -> Wh)
|
||||
result.yieldToday = payload->yieldToday * 10;
|
||||
|
||||
// Parse PV power (1 W units)
|
||||
result.panelPower = payload->inputPower;
|
||||
|
||||
// Parse load current (10 mA units -> amps, 0xFFFF = no load)
|
||||
if (payload->loadCurrent != 0xFFFF) {
|
||||
result.loadCurrent = payload->loadCurrent * 0.01f;
|
||||
} else {
|
||||
result.loadCurrent = 0;
|
||||
}
|
||||
|
||||
// Calculate PV voltage from power and current (if current > 0)
|
||||
if (result.batteryCurrent > 0.1f) {
|
||||
result.panelVoltage = result.panelPower / result.batteryCurrent;
|
||||
} else {
|
||||
result.panelVoltage = 0;
|
||||
}
|
||||
|
||||
if (debugEnabled) {
|
||||
debugPrint("Solar Charger: " + String(result.batteryVoltage, 2) + "V, " +
|
||||
String(result.batteryCurrent, 2) + "A, " +
|
||||
String(result.panelPower) + "W, State: " + String(result.chargeState));
|
||||
}
|
||||
|
||||
// AES-128-CTR via the bundled portable implementation (was mbedTLS on ESP32).
|
||||
// CTR is symmetric and operates in place, so copy then XOR the keystream.
|
||||
struct vble_aes_ctx ctx;
|
||||
vble_aes_init_ctx_iv(&ctx, key, iv);
|
||||
memcpy(decrypted, encrypted, len);
|
||||
vble_aes_ctr_xcrypt(&ctx, decrypted, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse Battery Monitor data
|
||||
bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result) {
|
||||
if (len < sizeof(victronBatteryMonitorPayload)) {
|
||||
if (debugEnabled) debugPrint("Battery monitor data too short: " + String(len) + " bytes");
|
||||
return false;
|
||||
bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, VictronSolarData& result) {
|
||||
if (len < sizeof(victronSolarChargerPayload)) return false;
|
||||
const auto* p = reinterpret_cast<const victronSolarChargerPayload*>(data);
|
||||
|
||||
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);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto* payload = reinterpret_cast<const victronBatteryMonitorPayload*>(data);
|
||||
bool VictronBLE::parseACCharger(const uint8_t* data, size_t len, VictronACChargerData& result) {
|
||||
// Payload is bit-packed (10 fields, 104 bits ending in byte 12). Decode LSB-first.
|
||||
if (len < 13) return false;
|
||||
|
||||
// Parse remaining time (1 minute units)
|
||||
result.remainingMinutes = payload->remainingMins;
|
||||
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;
|
||||
};
|
||||
|
||||
// Parse battery voltage (10 mV units -> volts)
|
||||
result.voltage = payload->batteryVoltage * 0.01f;
|
||||
result.chargeState = (uint8_t)readBits(8);
|
||||
result.errorCode = (uint8_t)readBits(8);
|
||||
|
||||
// Parse alarm bits
|
||||
result.alarmLowVoltage = (payload->alarms & 0x01) != 0;
|
||||
result.alarmHighVoltage = (payload->alarms & 0x02) != 0;
|
||||
result.alarmLowSOC = (payload->alarms & 0x04) != 0;
|
||||
result.alarmLowTemperature = (payload->alarms & 0x10) != 0;
|
||||
result.alarmHighTemperature = (payload->alarms & 0x20) != 0;
|
||||
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);
|
||||
|
||||
// Parse aux data: voltage (10 mV units) or temperature (0.01K units)
|
||||
if (payload->auxData < 3000) { // If < 30V, it's voltage
|
||||
result.auxVoltage = payload->auxData * 0.01f;
|
||||
result.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;
|
||||
}
|
||||
|
||||
bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, VictronBatteryData& result) {
|
||||
// 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;
|
||||
|
||||
// TTG (bits 0-15), unsigned minutes
|
||||
result.remainingMinutes = data[0] | ((uint16_t)data[1] << 8);
|
||||
|
||||
// Voltage (bits 16-31), signed, 0.01V units
|
||||
result.voltage = (int16_t)(data[2] | ((uint16_t)data[3] << 8)) * 0.01f;
|
||||
|
||||
// Alarm (bits 32-47), 16-bit bitmask
|
||||
uint16_t alarm = data[4] | ((uint16_t)data[5] << 8);
|
||||
result.alarmLowVoltage = (alarm & 0x0001) != 0;
|
||||
result.alarmHighVoltage = (alarm & 0x0002) != 0;
|
||||
result.alarmLowSOC = (alarm & 0x0004) != 0;
|
||||
result.alarmLowTemperature = (alarm & 0x0010) != 0;
|
||||
result.alarmHighTemperature = (alarm & 0x0020) != 0;
|
||||
|
||||
// Aux value (bits 48-63) interpreted per aux mode (bits 64-65)
|
||||
uint16_t auxRaw = data[6] | ((uint16_t)data[7] << 8);
|
||||
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;
|
||||
} else { // Otherwise temperature in 0.01 Kelvin
|
||||
result.temperature = (payload->auxData * 0.01f) - 273.15f;
|
||||
} else if (auxMode == 2) {
|
||||
result.temperature = auxRaw * 0.01f - 273.15f; // 0.01K -> C
|
||||
result.auxVoltage = 0;
|
||||
} else {
|
||||
result.auxVoltage = 0;
|
||||
result.temperature = 0;
|
||||
}
|
||||
|
||||
// Parse battery current (22-bit signed, 1 mA units)
|
||||
int32_t current = payload->currentLow |
|
||||
(payload->currentMid << 8) |
|
||||
((payload->currentHigh_consumedLow & 0x3F) << 16);
|
||||
// Sign extend from 22 bits to 32 bits
|
||||
if (current & 0x200000) {
|
||||
current |= 0xFFC00000;
|
||||
}
|
||||
result.current = current * 0.001f; // Convert mA to A
|
||||
// Battery current (bits 66-87), 22-bit signed, 0.001A units
|
||||
int32_t current = ((uint32_t)(data[8] >> 2) & 0x3F)
|
||||
| ((uint32_t)data[9] << 6)
|
||||
| ((uint32_t)data[10] << 14);
|
||||
if (current & 0x200000) current |= 0xFFC00000; // Sign extend 22-bit
|
||||
result.current = current * 0.001f;
|
||||
|
||||
// Parse consumed Ah (18-bit signed, 10 mAh units)
|
||||
int32_t consumedAh = ((payload->currentHigh_consumedLow & 0xC0) >> 6) |
|
||||
(payload->consumedMid << 2) |
|
||||
(payload->consumedHigh << 10);
|
||||
// Sign extend from 18 bits to 32 bits
|
||||
if (consumedAh & 0x20000) {
|
||||
consumedAh |= 0xFFFC0000;
|
||||
}
|
||||
result.consumedAh = consumedAh * 0.01f; // Convert 10mAh to Ah
|
||||
// Consumed Ah (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);
|
||||
|
||||
// Parse SOC (10-bit value, 10 = 1.0%)
|
||||
result.soc = (payload->soc & 0x3FF) * 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) {
|
||||
debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " +
|
||||
String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%");
|
||||
Serial.printf("[VictronBLE] Battery: %.2fV %.2fA SOC:%.1f%%\n",
|
||||
result.voltage, result.current, result.soc);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse Inverter data
|
||||
bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& result) {
|
||||
if (len < sizeof(victronInverterPayload)) {
|
||||
if (debugEnabled) debugPrint("Inverter data too short: " + String(len) + " bytes");
|
||||
return false;
|
||||
}
|
||||
bool VictronBLE::parseInverter(const uint8_t* data, size_t len, VictronInverterData& result) {
|
||||
if (len < sizeof(victronInverterPayload)) return false;
|
||||
const auto* p = reinterpret_cast<const victronInverterPayload*>(data);
|
||||
|
||||
const auto* payload = reinterpret_cast<const victronInverterPayload*>(data);
|
||||
result.state = p->deviceState;
|
||||
result.batteryVoltage = p->batteryVoltage * 0.01f;
|
||||
result.batteryCurrent = p->batteryCurrent * 0.01f;
|
||||
|
||||
// Parse device state
|
||||
result.state = payload->deviceState;
|
||||
|
||||
// Parse battery voltage (10 mV units -> volts)
|
||||
result.batteryVoltage = payload->batteryVoltage * 0.01f;
|
||||
|
||||
// Parse battery current (10 mA units, signed -> amps)
|
||||
result.batteryCurrent = payload->batteryCurrent * 0.01f;
|
||||
|
||||
// Parse AC Power (signed 24-bit, 1 W units)
|
||||
int32_t acPower = payload->acPowerLow |
|
||||
(payload->acPowerMid << 8) |
|
||||
(payload->acPowerHigh << 16);
|
||||
// Sign extend from 24 bits to 32 bits
|
||||
if (acPower & 0x800000) {
|
||||
acPower |= 0xFF000000;
|
||||
}
|
||||
// AC Power (signed 24-bit)
|
||||
int32_t acPower = p->acPowerLow | (p->acPowerMid << 8) | (p->acPowerHigh << 16);
|
||||
if (acPower & 0x800000) acPower |= 0xFF000000; // Sign extend
|
||||
result.acPower = acPower;
|
||||
|
||||
// Parse alarm bits
|
||||
result.alarmLowVoltage = (payload->alarms & 0x01) != 0;
|
||||
result.alarmHighVoltage = (payload->alarms & 0x02) != 0;
|
||||
result.alarmHighTemperature = (payload->alarms & 0x04) != 0;
|
||||
result.alarmOverload = (payload->alarms & 0x08) != 0;
|
||||
// Alarm bits
|
||||
result.alarmLowVoltage = (p->alarms & 0x01) != 0;
|
||||
result.alarmHighVoltage = (p->alarms & 0x02) != 0;
|
||||
result.alarmHighTemperature = (p->alarms & 0x04) != 0;
|
||||
result.alarmOverload = (p->alarms & 0x08) != 0;
|
||||
|
||||
if (debugEnabled) {
|
||||
debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " +
|
||||
String(result.acPower) + "W, State: " + String(result.state));
|
||||
Serial.printf("[VictronBLE] Inverter: %.2fV %dW State:%d\n",
|
||||
result.batteryVoltage, (int)result.acPower, result.state);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse DC-DC Converter data
|
||||
bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result) {
|
||||
if (len < sizeof(victronDCDCConverterPayload)) {
|
||||
if (debugEnabled) debugPrint("DC-DC converter data too short: " + String(len) + " bytes");
|
||||
return false;
|
||||
}
|
||||
bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, VictronDCDCData& result) {
|
||||
if (len < sizeof(victronDCDCConverterPayload)) return false;
|
||||
const auto* p = reinterpret_cast<const victronDCDCConverterPayload*>(data);
|
||||
|
||||
const auto* payload = reinterpret_cast<const victronDCDCConverterPayload*>(data);
|
||||
|
||||
// Parse charge state
|
||||
result.chargeState = payload->chargeState;
|
||||
|
||||
// Parse error code
|
||||
result.errorCode = payload->errorCode;
|
||||
|
||||
// Parse input voltage (10 mV units -> volts)
|
||||
result.inputVoltage = payload->inputVoltage * 0.01f;
|
||||
|
||||
// Parse output voltage (10 mV units -> volts)
|
||||
result.outputVoltage = payload->outputVoltage * 0.01f;
|
||||
|
||||
// Parse output current (10 mA units -> amps)
|
||||
result.outputCurrent = payload->outputCurrent * 0.01f;
|
||||
result.chargeState = p->chargeState;
|
||||
result.errorCode = p->errorCode;
|
||||
result.inputVoltage = p->inputVoltage * 0.01f;
|
||||
result.outputVoltage = p->outputVoltage * 0.01f;
|
||||
result.outputCurrent = p->outputCurrent * 0.01f;
|
||||
|
||||
if (debugEnabled) {
|
||||
debugPrint("DC-DC Converter: In=" + String(result.inputVoltage, 2) + "V, Out=" +
|
||||
String(result.outputVoltage, 2) + "V, " + String(result.outputCurrent, 2) + "A");
|
||||
Serial.printf("[VictronBLE] DC-DC: In=%.2fV Out=%.2fV %.2fA\n",
|
||||
result.inputVoltage, result.outputVoltage, result.outputCurrent);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get data methods
|
||||
bool VictronBLE::getSolarChargerData(const String& macAddress, SolarChargerData& data) {
|
||||
String normalizedMAC = normalizeMAC(macAddress);
|
||||
auto it = devices.find(normalizedMAC);
|
||||
|
||||
if (it != devices.end() && it->second->data &&
|
||||
it->second->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
|
||||
data = *static_cast<SolarChargerData*>(it->second->data);
|
||||
return data.dataValid;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool VictronBLE::getBatteryMonitorData(const String& macAddress, BatteryMonitorData& data) {
|
||||
String normalizedMAC = normalizeMAC(macAddress);
|
||||
auto it = devices.find(normalizedMAC);
|
||||
|
||||
if (it != devices.end() && it->second->data &&
|
||||
it->second->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) {
|
||||
data = *static_cast<BatteryMonitorData*>(it->second->data);
|
||||
return data.dataValid;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool VictronBLE::getInverterData(const String& macAddress, InverterData& data) {
|
||||
String normalizedMAC = normalizeMAC(macAddress);
|
||||
auto it = devices.find(normalizedMAC);
|
||||
|
||||
if (it != devices.end() && it->second->data &&
|
||||
it->second->data->deviceType == DEVICE_TYPE_INVERTER) {
|
||||
data = *static_cast<InverterData*>(it->second->data);
|
||||
return data.dataValid;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool VictronBLE::getDCDCConverterData(const String& macAddress, DCDCConverterData& data) {
|
||||
String normalizedMAC = normalizeMAC(macAddress);
|
||||
auto it = devices.find(normalizedMAC);
|
||||
|
||||
if (it != devices.end() && it->second->data &&
|
||||
it->second->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) {
|
||||
data = *static_cast<DCDCConverterData*>(it->second->data);
|
||||
return data.dataValid;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get devices by type
|
||||
std::vector<String> VictronBLE::getDevicesByType(VictronDeviceType type) {
|
||||
std::vector<String> result;
|
||||
|
||||
for (const auto& pair : devices) {
|
||||
if (pair.second->data && pair.second->data->deviceType == type) {
|
||||
result.push_back(pair.first);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Helper: Create device data structure
|
||||
VictronDeviceData* VictronBLE::createDeviceData(VictronDeviceType type) {
|
||||
switch (type) {
|
||||
case DEVICE_TYPE_SOLAR_CHARGER:
|
||||
return new SolarChargerData();
|
||||
case DEVICE_TYPE_BATTERY_MONITOR:
|
||||
return new BatteryMonitorData();
|
||||
case DEVICE_TYPE_INVERTER:
|
||||
case DEVICE_TYPE_INVERTER_RS:
|
||||
case DEVICE_TYPE_MULTI_RS:
|
||||
case DEVICE_TYPE_VE_BUS:
|
||||
return new InverterData();
|
||||
case DEVICE_TYPE_DCDC_CONVERTER:
|
||||
return new DCDCConverterData();
|
||||
default:
|
||||
return new VictronDeviceData();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Convert hex string to bytes
|
||||
bool VictronBLE::hexStringToBytes(const String& hex, uint8_t* bytes, size_t len) {
|
||||
if (hex.length() != len * 2) {
|
||||
return false;
|
||||
}
|
||||
// --- Helpers ---
|
||||
|
||||
bool VictronBLE::hexToBytes(const char* hex, uint8_t* out, size_t len) {
|
||||
if (strlen(hex) != len * 2) return false;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
String byteStr = hex.substring(i * 2, i * 2 + 2);
|
||||
char* endPtr;
|
||||
bytes[i] = strtoul(byteStr.c_str(), &endPtr, 16);
|
||||
if (*endPtr != '\0') {
|
||||
return false;
|
||||
uint8_t hi = hex[i * 2], lo = hex[i * 2 + 1];
|
||||
if (hi >= '0' && hi <= '9') hi -= '0';
|
||||
else if (hi >= 'a' && hi <= 'f') hi = hi - 'a' + 10;
|
||||
else if (hi >= 'A' && hi <= 'F') hi = hi - 'A' + 10;
|
||||
else return false;
|
||||
if (lo >= '0' && lo <= '9') lo -= '0';
|
||||
else if (lo >= 'a' && lo <= 'f') lo = lo - 'a' + 10;
|
||||
else if (lo >= 'A' && lo <= 'F') lo = lo - 'A' + 10;
|
||||
else return false;
|
||||
out[i] = (hi << 4) | lo;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper: MAC address to string
|
||||
String VictronBLE::macAddressToString(BLEAddress address) {
|
||||
return String(address.toString().c_str());
|
||||
void VictronBLE::normalizeMAC(const char* input, char* output) {
|
||||
int j = 0;
|
||||
for (int i = 0; input[i] && j < VICTRON_MAC_LEN - 1; i++) {
|
||||
char c = input[i];
|
||||
if (c == ':' || c == '-') continue;
|
||||
output[j++] = (c >= 'A' && c <= 'F') ? (c + 32) : c;
|
||||
}
|
||||
output[j] = '\0';
|
||||
}
|
||||
|
||||
// Helper: Normalize MAC address format
|
||||
String VictronBLE::normalizeMAC(const String& mac) {
|
||||
String normalized = mac;
|
||||
normalized.toLowerCase();
|
||||
normalized.replace("-", "");
|
||||
normalized.replace(":", "");
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// Debug helper
|
||||
void VictronBLE::debugPrint(const String& message) {
|
||||
if (debugEnabled)
|
||||
Serial.println("[VictronBLE] " + message);
|
||||
VictronBLE::DeviceEntry* VictronBLE::findDevice(const char* normalizedMAC) {
|
||||
for (size_t i = 0; i < deviceCount; i++) {
|
||||
if (devices[i].active && strcmp(devices[i].device.mac, normalizedMAC) == 0) {
|
||||
return &devices[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
+229
-231
@@ -1,5 +1,9 @@
|
||||
/**
|
||||
* VictronBLE - ESP32 library for Victron Energy BLE devices
|
||||
* VictronBLE - portable library for Victron Energy BLE devices
|
||||
*
|
||||
* Runs on ESP32 (Bluedroid) and nRF52840 (Bluefruit); the BLE scanning backend
|
||||
* is the only platform-specific code (see src/esp32 and src/nrf52). Decoding and
|
||||
* AES-128-CTR decryption are common to all targets.
|
||||
*
|
||||
* Based on Victron's official BLE Advertising protocol documentation
|
||||
* Inspired by hoberman's examples and keshavdv's Python library
|
||||
@@ -12,17 +16,30 @@
|
||||
#define VICTRON_BLE_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <BLEDevice.h>
|
||||
#include <BLEAdvertisedDevice.h>
|
||||
#include <BLEScan.h>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include "mbedtls/aes.h"
|
||||
|
||||
// Victron manufacturer ID
|
||||
// --- Platform BLE backend selection ---
|
||||
// The BLE scanning layer is the only platform-specific part of the library.
|
||||
// Decoding and crypto are common to all targets.
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
#include <BLEDevice.h>
|
||||
#include <BLEAdvertisedDevice.h>
|
||||
#include <BLEScan.h>
|
||||
#define VICTRON_BACKEND_ESP32 1
|
||||
#elif defined(ARDUINO_ARCH_NRF52) || defined(NRF52840_XXAA) || defined(NRF52832_XXAA)
|
||||
#include <bluefruit.h>
|
||||
#define VICTRON_BACKEND_NRF52 1
|
||||
#else
|
||||
#error "VictronBLE: unsupported platform (need ESP32 Arduino or Adafruit/Seeed nRF52 core)"
|
||||
#endif
|
||||
|
||||
// --- Constants ---
|
||||
static constexpr uint16_t VICTRON_MANUFACTURER_ID = 0x02E1;
|
||||
static constexpr int VICTRON_MAX_DEVICES = 8;
|
||||
static constexpr int VICTRON_MAC_LEN = 13; // 12 hex chars + null
|
||||
static constexpr int VICTRON_NAME_LEN = 32;
|
||||
static constexpr int VICTRON_ENCRYPTED_LEN = 21;
|
||||
|
||||
// Device type IDs from Victron protocol
|
||||
// --- Device type IDs from Victron protocol ---
|
||||
enum VictronDeviceType {
|
||||
DEVICE_TYPE_UNKNOWN = 0x00,
|
||||
DEVICE_TYPE_SOLAR_CHARGER = 0x01,
|
||||
@@ -31,14 +48,17 @@ enum VictronDeviceType {
|
||||
DEVICE_TYPE_DCDC_CONVERTER = 0x04,
|
||||
DEVICE_TYPE_SMART_LITHIUM = 0x05,
|
||||
DEVICE_TYPE_INVERTER_RS = 0x06,
|
||||
DEVICE_TYPE_SMART_BATTERY_PROTECT = 0x07,
|
||||
DEVICE_TYPE_LYNX_SMART_BMS = 0x08,
|
||||
DEVICE_TYPE_MULTI_RS = 0x09,
|
||||
DEVICE_TYPE_VE_BUS = 0x0A,
|
||||
DEVICE_TYPE_DC_ENERGY_METER = 0x0B
|
||||
DEVICE_TYPE_GX_DEVICE = 0x07,
|
||||
DEVICE_TYPE_AC_CHARGER = 0x08,
|
||||
DEVICE_TYPE_SMART_BATTERY_PROTECT = 0x09,
|
||||
DEVICE_TYPE_LYNX_SMART_BMS = 0x0A,
|
||||
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 {
|
||||
CHARGER_OFF = 0,
|
||||
CHARGER_LOW_POWER = 1,
|
||||
@@ -53,163 +73,238 @@ enum SolarChargerState {
|
||||
CHARGER_EXTERNAL_CONTROL = 252
|
||||
};
|
||||
|
||||
// Binary data structures for decoding BLE advertisements
|
||||
// Must use __attribute__((packed)) to prevent compiler padding
|
||||
// ============================================================
|
||||
// Wire-format packed structures for decoding BLE advertisements
|
||||
// ============================================================
|
||||
|
||||
// Manufacturer data structure (outer envelope)
|
||||
struct victronManufacturerData {
|
||||
uint16_t vendorID; // vendor ID
|
||||
uint8_t beaconType; // Should be 0x10 (Product Advertisement) for the packets we want
|
||||
uint8_t unknownData1[3]; // Unknown data
|
||||
uint8_t victronRecordType; // Should be 0x01 (Solar Charger) for the packets we want
|
||||
uint16_t nonceDataCounter; // Nonce
|
||||
uint8_t encryptKeyMatch; // Should match pre-shared encryption key byte 0
|
||||
uint8_t victronEncryptedData[21]; // (31 bytes max per BLE spec - size of previous elements)
|
||||
uint8_t nullPad; // extra byte because toCharArray() adds a \0 byte.
|
||||
uint16_t vendorID;
|
||||
uint8_t beaconType; // 0x10 = Product Advertisement
|
||||
uint8_t unknownData1[3];
|
||||
uint8_t victronRecordType; // Device type (see VictronDeviceType)
|
||||
uint16_t nonceDataCounter;
|
||||
uint8_t encryptKeyMatch; // Should match encryption key byte 0
|
||||
uint8_t victronEncryptedData[VICTRON_ENCRYPTED_LEN];
|
||||
} __attribute__((packed));
|
||||
// Decrypted payload structures for each device type
|
||||
|
||||
// Solar Charger decrypted payload
|
||||
struct victronSolarChargerPayload {
|
||||
uint8_t deviceState; // Charge state (SolarChargerState enum)
|
||||
uint8_t errorCode; // Error code
|
||||
int16_t batteryVoltage; // Battery voltage in 10mV units
|
||||
int16_t batteryCurrent; // Battery current in 10mA units (signed)
|
||||
uint16_t yieldToday; // Yield today in 10Wh units
|
||||
uint16_t inputPower; // PV power in 1W units
|
||||
uint16_t loadCurrent; // Load current in 10mA units (0xFFFF = no load)
|
||||
uint8_t reserved[2]; // Reserved bytes
|
||||
uint8_t deviceState;
|
||||
uint8_t errorCode;
|
||||
int16_t batteryVoltage; // 0.01V units (signed)
|
||||
int16_t batteryCurrent; // 0.1A units (signed)
|
||||
uint16_t yieldToday; // 0.01kWh (10Wh) units
|
||||
uint16_t inputPower; // 1W units
|
||||
uint16_t loadCurrent; // 9-bit field, 0.1A units (0x1FF = no load)
|
||||
uint8_t reserved[2];
|
||||
} __attribute__((packed));
|
||||
|
||||
// Battery Monitor decrypted payload
|
||||
struct victronBatteryMonitorPayload {
|
||||
uint16_t remainingMins; // Time remaining in minutes
|
||||
uint16_t batteryVoltage; // Battery voltage in 10mV units
|
||||
uint8_t alarms; // Alarm bits
|
||||
uint16_t auxData; // Aux voltage (10mV) or temperature (0.01K)
|
||||
uint8_t currentLow; // Battery current bits 0-7
|
||||
uint8_t currentMid; // Battery current bits 8-15
|
||||
uint8_t currentHigh_consumedLow; // Current bits 16-21 (low 6 bits), consumed bits 0-1 (high 2 bits)
|
||||
uint8_t consumedMid; // Consumed Ah bits 2-9
|
||||
uint8_t consumedHigh; // Consumed Ah bits 10-17
|
||||
uint16_t soc; // State of charge in 0.1% units (10-bit value)
|
||||
uint8_t reserved[2]; // Reserved bytes
|
||||
} __attribute__((packed));
|
||||
// NOTE: The battery monitor payload is bit-packed (16-bit alarm, 2-bit aux mode,
|
||||
// 22-bit current, 20-bit consumed Ah, 10-bit SOC) and does NOT byte-align, so it
|
||||
// is decoded by bit offset directly in parseBatteryMonitor() rather than a struct.
|
||||
|
||||
// Inverter decrypted payload
|
||||
struct victronInverterPayload {
|
||||
uint8_t deviceState; // Device state
|
||||
uint8_t errorCode; // Error code
|
||||
uint16_t batteryVoltage; // Battery voltage in 10mV units
|
||||
int16_t batteryCurrent; // Battery current in 10mA units (signed)
|
||||
uint8_t acPowerLow; // AC Power bits 0-7
|
||||
uint8_t acPowerMid; // AC Power bits 8-15
|
||||
uint8_t acPowerHigh; // AC Power bits 16-23 (signed 24-bit)
|
||||
uint8_t alarms; // Alarm bits
|
||||
uint8_t reserved[4]; // Reserved bytes
|
||||
uint8_t deviceState;
|
||||
uint8_t errorCode;
|
||||
uint16_t batteryVoltage; // 10mV units
|
||||
int16_t batteryCurrent; // 10mA units (signed)
|
||||
uint8_t acPowerLow;
|
||||
uint8_t acPowerMid;
|
||||
uint8_t acPowerHigh; // Signed 24-bit
|
||||
uint8_t alarms;
|
||||
uint8_t reserved[4];
|
||||
} __attribute__((packed));
|
||||
|
||||
// DC-DC Converter decrypted payload
|
||||
struct victronDCDCConverterPayload {
|
||||
uint8_t chargeState; // Charge state
|
||||
uint8_t errorCode; // Error code
|
||||
uint16_t inputVoltage; // Input voltage in 10mV units
|
||||
uint16_t outputVoltage; // Output voltage in 10mV units
|
||||
uint16_t outputCurrent; // Output current in 10mA units
|
||||
uint8_t reserved[6]; // Reserved bytes
|
||||
uint8_t chargeState;
|
||||
uint8_t errorCode;
|
||||
uint16_t inputVoltage; // 10mV units
|
||||
uint16_t outputVoltage; // 10mV units
|
||||
uint16_t outputCurrent; // 10mA units
|
||||
uint8_t reserved[6];
|
||||
} __attribute__((packed));
|
||||
|
||||
// Base structure for all device data
|
||||
struct VictronDeviceData {
|
||||
String deviceName;
|
||||
String macAddress;
|
||||
VictronDeviceType deviceType;
|
||||
int8_t rssi;
|
||||
uint32_t lastUpdate;
|
||||
bool dataValid;
|
||||
// ============================================================
|
||||
// Parsed data structures (flat, no inheritance)
|
||||
// ============================================================
|
||||
|
||||
VictronDeviceData() : deviceType(DEVICE_TYPE_UNKNOWN), rssi(-100),
|
||||
lastUpdate(0), dataValid(false) {}
|
||||
};
|
||||
|
||||
// Solar Charger specific data
|
||||
struct SolarChargerData : public VictronDeviceData {
|
||||
SolarChargerState chargeState;
|
||||
struct VictronSolarData {
|
||||
uint8_t chargeState; // SolarChargerState enum
|
||||
uint8_t errorCode;
|
||||
float batteryVoltage; // V
|
||||
float batteryCurrent; // A
|
||||
float panelVoltage; // V (PV voltage)
|
||||
float panelPower; // W
|
||||
uint16_t yieldToday; // Wh
|
||||
float loadCurrent; // A
|
||||
|
||||
SolarChargerData() : chargeState(CHARGER_OFF), batteryVoltage(0),
|
||||
batteryCurrent(0), panelVoltage(0), panelPower(0),
|
||||
yieldToday(0), loadCurrent(0) {
|
||||
deviceType = DEVICE_TYPE_SOLAR_CHARGER;
|
||||
}
|
||||
};
|
||||
|
||||
// Battery Monitor/SmartShunt specific data
|
||||
struct BatteryMonitorData : public VictronDeviceData {
|
||||
struct 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)
|
||||
};
|
||||
|
||||
struct VictronBatteryData {
|
||||
float voltage; // V
|
||||
float current; // A (positive = charging, negative = discharging)
|
||||
float temperature; // °C
|
||||
float auxVoltage; // V (starter battery or midpoint)
|
||||
uint16_t remainingMinutes; // Minutes
|
||||
float current; // A
|
||||
float temperature; // C (0 if aux is voltage)
|
||||
float auxVoltage; // V (0 if aux is temperature)
|
||||
uint16_t remainingMinutes;
|
||||
float consumedAh; // Ah
|
||||
float soc; // State of Charge %
|
||||
float soc; // %
|
||||
bool alarmLowVoltage;
|
||||
bool alarmHighVoltage;
|
||||
bool alarmLowSOC;
|
||||
bool alarmLowTemperature;
|
||||
bool alarmHighTemperature;
|
||||
|
||||
BatteryMonitorData() : voltage(0), current(0), temperature(0),
|
||||
auxVoltage(0), remainingMinutes(0), consumedAh(0),
|
||||
soc(0), alarmLowVoltage(false), alarmHighVoltage(false),
|
||||
alarmLowSOC(false), alarmLowTemperature(false),
|
||||
alarmHighTemperature(false) {
|
||||
deviceType = DEVICE_TYPE_BATTERY_MONITOR;
|
||||
}
|
||||
};
|
||||
|
||||
// Inverter specific data
|
||||
struct InverterData : public VictronDeviceData {
|
||||
struct VictronInverterData {
|
||||
float batteryVoltage; // V
|
||||
float batteryCurrent; // A
|
||||
float acPower; // W
|
||||
uint8_t state; // Inverter state
|
||||
bool alarmHighVoltage;
|
||||
uint8_t state;
|
||||
bool alarmLowVoltage;
|
||||
bool alarmHighVoltage;
|
||||
bool alarmHighTemperature;
|
||||
bool alarmOverload;
|
||||
|
||||
InverterData() : batteryVoltage(0), batteryCurrent(0), acPower(0),
|
||||
state(0), alarmHighVoltage(false), alarmLowVoltage(false),
|
||||
alarmHighTemperature(false), alarmOverload(false) {
|
||||
deviceType = DEVICE_TYPE_INVERTER;
|
||||
}
|
||||
};
|
||||
|
||||
// DC-DC Converter specific data
|
||||
struct DCDCConverterData : public VictronDeviceData {
|
||||
struct VictronDCDCData {
|
||||
float inputVoltage; // V
|
||||
float outputVoltage; // V
|
||||
float outputCurrent; // A
|
||||
uint8_t chargeState;
|
||||
uint8_t errorCode;
|
||||
|
||||
DCDCConverterData() : inputVoltage(0), outputVoltage(0), outputCurrent(0),
|
||||
chargeState(0), errorCode(0) {
|
||||
deviceType = DEVICE_TYPE_DCDC_CONVERTER;
|
||||
}
|
||||
};
|
||||
|
||||
// Forward declarations
|
||||
class VictronBLE;
|
||||
// ============================================================
|
||||
// Main device struct with tagged union
|
||||
// ============================================================
|
||||
|
||||
struct VictronDevice {
|
||||
char name[VICTRON_NAME_LEN];
|
||||
char mac[VICTRON_MAC_LEN];
|
||||
VictronDeviceType deviceType;
|
||||
int8_t rssi;
|
||||
uint32_t lastUpdate;
|
||||
bool dataValid;
|
||||
union {
|
||||
VictronSolarData solar;
|
||||
VictronBatteryData battery;
|
||||
VictronInverterData inverter;
|
||||
VictronDCDCData dcdc;
|
||||
VictronACChargerData acCharger;
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Callback — simple function pointer
|
||||
// ============================================================
|
||||
|
||||
typedef void (*VictronCallback)(const VictronDevice* device);
|
||||
|
||||
// Forward declaration
|
||||
class VictronBLEAdvertisedDeviceCallbacks;
|
||||
|
||||
// Callback interface for device data updates
|
||||
// ============================================================
|
||||
// Main VictronBLE class
|
||||
// ============================================================
|
||||
|
||||
class VictronBLE {
|
||||
public:
|
||||
VictronBLE();
|
||||
|
||||
bool begin(uint32_t scanDuration = 5);
|
||||
bool addDevice(const char* name, const char* mac, const char* hexKey,
|
||||
VictronDeviceType type = DEVICE_TYPE_UNKNOWN);
|
||||
void setCallback(VictronCallback cb) { callback = cb; }
|
||||
void setDebug(bool enable) { debugEnabled = enable; }
|
||||
void setMinInterval(uint32_t ms) { minIntervalMs = ms; }
|
||||
size_t getDeviceCount() const { return deviceCount; }
|
||||
void loop();
|
||||
|
||||
private:
|
||||
struct DeviceEntry {
|
||||
VictronDevice device;
|
||||
uint8_t key[16];
|
||||
uint16_t lastNonce;
|
||||
bool active;
|
||||
};
|
||||
|
||||
// --- Common state (platform-independent) ---
|
||||
DeviceEntry devices[VICTRON_MAX_DEVICES];
|
||||
size_t deviceCount;
|
||||
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);
|
||||
|
||||
// Common entry point fed by each platform BLE backend with one raw
|
||||
// manufacturer-data record (vendor ID first), the device MAC and RSSI.
|
||||
void onAdvertisement(const uint8_t* mfgData, size_t len,
|
||||
const char* macStr, int8_t rssi);
|
||||
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);
|
||||
|
||||
// --- Platform-specific BLE backend (see src/esp32 and src/nrf52) ---
|
||||
#if defined(VICTRON_BACKEND_ESP32)
|
||||
friend class VictronBLEAdvertisedDeviceCallbacks;
|
||||
BLEScan* pBLEScan;
|
||||
VictronBLEAdvertisedDeviceCallbacks* scanCallbackObj;
|
||||
void processDevice(BLEAdvertisedDevice& dev);
|
||||
#elif defined(VICTRON_BACKEND_NRF52)
|
||||
static VictronBLE* s_instance;
|
||||
static void scanCallback(ble_gap_evt_adv_report_t* report);
|
||||
#endif
|
||||
};
|
||||
|
||||
#if defined(VICTRON_BACKEND_ESP32)
|
||||
// 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;
|
||||
};
|
||||
#endif
|
||||
|
||||
// ============================================================
|
||||
// Commented-out features — kept for reference / future use
|
||||
// ============================================================
|
||||
|
||||
#if 0
|
||||
|
||||
// --- VictronDeviceConfig (use addDevice(name, mac, key, type) directly) ---
|
||||
struct VictronDeviceConfig {
|
||||
String name;
|
||||
String macAddress;
|
||||
String encryptionKey;
|
||||
VictronDeviceType expectedType;
|
||||
VictronDeviceConfig() : expectedType(DEVICE_TYPE_UNKNOWN) {}
|
||||
VictronDeviceConfig(const String& n, const String& mac, const String& key, VictronDeviceType type = DEVICE_TYPE_UNKNOWN)
|
||||
: name(n), macAddress(mac), encryptionKey(key), expectedType(type) {}
|
||||
};
|
||||
|
||||
// --- Virtual callback interface (replaced by function pointer VictronCallback) ---
|
||||
class VictronDeviceCallback {
|
||||
public:
|
||||
virtual ~VictronDeviceCallback() {}
|
||||
@@ -219,114 +314,17 @@ public:
|
||||
virtual void onDCDCConverterData(const DCDCConverterData& data) {}
|
||||
};
|
||||
|
||||
// Device configuration structure
|
||||
struct VictronDeviceConfig {
|
||||
String name;
|
||||
String macAddress;
|
||||
String encryptionKey; // 32 character hex string
|
||||
VictronDeviceType expectedType;
|
||||
// --- Per-type getter methods (use callback instead) ---
|
||||
bool getSolarChargerData(const String& macAddress, SolarChargerData& data);
|
||||
bool getBatteryMonitorData(const String& macAddress, BatteryMonitorData& data);
|
||||
bool getInverterData(const String& macAddress, InverterData& data);
|
||||
bool getDCDCConverterData(const String& macAddress, DCDCConverterData& data);
|
||||
|
||||
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) {}
|
||||
};
|
||||
// --- Other removed methods ---
|
||||
void removeDevice(const String& macAddress);
|
||||
std::vector<String> getDevicesByType(VictronDeviceType type);
|
||||
String getLastError() const;
|
||||
|
||||
// Main VictronBLE class
|
||||
class VictronBLE {
|
||||
public:
|
||||
VictronBLE();
|
||||
~VictronBLE();
|
||||
|
||||
// Initialize BLE and start scanning
|
||||
bool begin(uint32_t scanDuration = 5);
|
||||
|
||||
// Add a device to monitor
|
||||
bool addDevice(const VictronDeviceConfig& config);
|
||||
bool addDevice(const String& name, const String& macAddress, const String& encryptionKey,
|
||||
VictronDeviceType expectedType = DEVICE_TYPE_UNKNOWN);
|
||||
|
||||
// Remove a device
|
||||
void removeDevice(const String& macAddress);
|
||||
|
||||
// Get device count
|
||||
size_t getDeviceCount() const { return devices.size(); }
|
||||
|
||||
// Set callback for data updates
|
||||
void setCallback(VictronDeviceCallback* cb) { callback = cb; }
|
||||
|
||||
// Process scanning (call in loop())
|
||||
void loop();
|
||||
|
||||
// Get latest data for a device
|
||||
bool getSolarChargerData(const String& macAddress, SolarChargerData& data);
|
||||
bool getBatteryMonitorData(const String& macAddress, BatteryMonitorData& data);
|
||||
bool getInverterData(const String& macAddress, InverterData& data);
|
||||
bool getDCDCConverterData(const String& macAddress, DCDCConverterData& data);
|
||||
|
||||
// Get all devices of a specific type
|
||||
std::vector<String> getDevicesByType(VictronDeviceType type);
|
||||
|
||||
// Enable/disable debug output
|
||||
void setDebug(bool enable) { debugEnabled = enable; }
|
||||
|
||||
// Get last error message
|
||||
String getLastError() const { return lastError; }
|
||||
|
||||
private:
|
||||
friend class VictronBLEAdvertisedDeviceCallbacks;
|
||||
|
||||
struct DeviceInfo {
|
||||
VictronDeviceConfig config;
|
||||
VictronDeviceData* data;
|
||||
uint8_t encryptionKeyBytes[16];
|
||||
|
||||
DeviceInfo() : data(nullptr) {
|
||||
memset(encryptionKeyBytes, 0, 16);
|
||||
}
|
||||
~DeviceInfo() {
|
||||
if (data) delete data;
|
||||
}
|
||||
DeviceInfo(const DeviceInfo&) = delete;
|
||||
DeviceInfo& operator=(const DeviceInfo&) = delete;
|
||||
};
|
||||
|
||||
std::map<String, DeviceInfo*> devices;
|
||||
BLEScan* pBLEScan;
|
||||
VictronBLEAdvertisedDeviceCallbacks* scanCallback;
|
||||
VictronDeviceCallback* callback;
|
||||
bool debugEnabled;
|
||||
String lastError;
|
||||
uint32_t scanDuration;
|
||||
bool initialized;
|
||||
|
||||
// Internal methods
|
||||
bool hexStringToBytes(const String& hex, uint8_t* bytes, size_t len);
|
||||
bool decryptAdvertisement(const uint8_t* encrypted, size_t encLen,
|
||||
const uint8_t* key, const uint8_t* iv,
|
||||
uint8_t* decrypted);
|
||||
bool parseAdvertisement(DeviceInfo* deviceInfo, const victronManufacturerData& mfgData);
|
||||
void processDevice(BLEAdvertisedDevice& advertisedDevice);
|
||||
|
||||
VictronDeviceData* createDeviceData(VictronDeviceType type);
|
||||
bool parseSolarCharger(const uint8_t* data, size_t len, SolarChargerData& result);
|
||||
bool parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result);
|
||||
bool parseInverter(const uint8_t* data, size_t len, InverterData& result);
|
||||
bool parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result);
|
||||
|
||||
void debugPrint(const String& message);
|
||||
|
||||
String macAddressToString(BLEAddress address);
|
||||
String normalizeMAC(const String& mac);
|
||||
};
|
||||
|
||||
// BLE scan callback class
|
||||
class VictronBLEAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
|
||||
public:
|
||||
VictronBLEAdvertisedDeviceCallbacks(VictronBLE* parent) : victronBLE(parent) {}
|
||||
void onResult(BLEAdvertisedDevice advertisedDevice) override;
|
||||
|
||||
private:
|
||||
VictronBLE* victronBLE;
|
||||
};
|
||||
#endif // commented-out features
|
||||
|
||||
#endif // VICTRON_BLE_H
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Minimal AES-128 CTR-mode implementation for VictronBLE.
|
||||
*
|
||||
* Trimmed and symbol-prefixed adaptation of kokke/tiny-AES-c (public domain /
|
||||
* Unlicense). Only AES-128 encryption (forward Cipher) and CTR mode are kept,
|
||||
* since CTR uses the forward cipher for both encrypt and decrypt. The S-box and
|
||||
* Rcon tables and the round transforms are unchanged from the upstream, which is
|
||||
* verified against NIST SP 800-38A.
|
||||
*/
|
||||
#include <string.h>
|
||||
#include "vble_aes.h"
|
||||
|
||||
#define Nb 4 // columns in the state
|
||||
#define Nk 4 // 32-bit words in an AES-128 key
|
||||
#define Nr 10 // rounds for AES-128
|
||||
|
||||
typedef uint8_t state_t[4][4];
|
||||
|
||||
static const uint8_t sbox[256] = {
|
||||
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
|
||||
0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
|
||||
0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
|
||||
0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
|
||||
0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
|
||||
0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
|
||||
0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
|
||||
0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
|
||||
0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
|
||||
0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
|
||||
0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
|
||||
0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
|
||||
0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
|
||||
0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
|
||||
0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
|
||||
0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 };
|
||||
|
||||
static const uint8_t Rcon[11] = {
|
||||
0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36 };
|
||||
|
||||
#define getSBoxValue(num) (sbox[(num)])
|
||||
|
||||
static void KeyExpansion(uint8_t* RoundKey, const uint8_t* Key)
|
||||
{
|
||||
unsigned i, j, k;
|
||||
uint8_t tempa[4];
|
||||
|
||||
for (i = 0; i < Nk; ++i) {
|
||||
RoundKey[(i * 4) + 0] = Key[(i * 4) + 0];
|
||||
RoundKey[(i * 4) + 1] = Key[(i * 4) + 1];
|
||||
RoundKey[(i * 4) + 2] = Key[(i * 4) + 2];
|
||||
RoundKey[(i * 4) + 3] = Key[(i * 4) + 3];
|
||||
}
|
||||
|
||||
for (i = Nk; i < Nb * (Nr + 1); ++i) {
|
||||
k = (i - 1) * 4;
|
||||
tempa[0] = RoundKey[k + 0];
|
||||
tempa[1] = RoundKey[k + 1];
|
||||
tempa[2] = RoundKey[k + 2];
|
||||
tempa[3] = RoundKey[k + 3];
|
||||
|
||||
if (i % Nk == 0) {
|
||||
// RotWord
|
||||
const uint8_t u8tmp = tempa[0];
|
||||
tempa[0] = tempa[1];
|
||||
tempa[1] = tempa[2];
|
||||
tempa[2] = tempa[3];
|
||||
tempa[3] = u8tmp;
|
||||
// SubWord
|
||||
tempa[0] = getSBoxValue(tempa[0]);
|
||||
tempa[1] = getSBoxValue(tempa[1]);
|
||||
tempa[2] = getSBoxValue(tempa[2]);
|
||||
tempa[3] = getSBoxValue(tempa[3]);
|
||||
|
||||
tempa[0] = tempa[0] ^ Rcon[i / Nk];
|
||||
}
|
||||
j = i * 4; k = (i - Nk) * 4;
|
||||
RoundKey[j + 0] = RoundKey[k + 0] ^ tempa[0];
|
||||
RoundKey[j + 1] = RoundKey[k + 1] ^ tempa[1];
|
||||
RoundKey[j + 2] = RoundKey[k + 2] ^ tempa[2];
|
||||
RoundKey[j + 3] = RoundKey[k + 3] ^ tempa[3];
|
||||
}
|
||||
}
|
||||
|
||||
static void AddRoundKey(uint8_t round, state_t* state, const uint8_t* RoundKey)
|
||||
{
|
||||
uint8_t i, j;
|
||||
for (i = 0; i < 4; ++i)
|
||||
for (j = 0; j < 4; ++j)
|
||||
(*state)[i][j] ^= RoundKey[(round * Nb * 4) + (i * Nb) + j];
|
||||
}
|
||||
|
||||
static void SubBytes(state_t* state)
|
||||
{
|
||||
uint8_t i, j;
|
||||
for (i = 0; i < 4; ++i)
|
||||
for (j = 0; j < 4; ++j)
|
||||
(*state)[j][i] = getSBoxValue((*state)[j][i]);
|
||||
}
|
||||
|
||||
static void ShiftRows(state_t* state)
|
||||
{
|
||||
uint8_t temp;
|
||||
|
||||
temp = (*state)[0][1];
|
||||
(*state)[0][1] = (*state)[1][1];
|
||||
(*state)[1][1] = (*state)[2][1];
|
||||
(*state)[2][1] = (*state)[3][1];
|
||||
(*state)[3][1] = temp;
|
||||
|
||||
temp = (*state)[0][2];
|
||||
(*state)[0][2] = (*state)[2][2];
|
||||
(*state)[2][2] = temp;
|
||||
temp = (*state)[1][2];
|
||||
(*state)[1][2] = (*state)[3][2];
|
||||
(*state)[3][2] = temp;
|
||||
|
||||
temp = (*state)[0][3];
|
||||
(*state)[0][3] = (*state)[3][3];
|
||||
(*state)[3][3] = (*state)[2][3];
|
||||
(*state)[2][3] = (*state)[1][3];
|
||||
(*state)[1][3] = temp;
|
||||
}
|
||||
|
||||
static uint8_t xtime(uint8_t x)
|
||||
{
|
||||
return ((x << 1) ^ (((x >> 7) & 1) * 0x1b));
|
||||
}
|
||||
|
||||
static void MixColumns(state_t* state)
|
||||
{
|
||||
uint8_t i, Tmp, Tm, t;
|
||||
for (i = 0; i < 4; ++i) {
|
||||
t = (*state)[i][0];
|
||||
Tmp = (*state)[i][0] ^ (*state)[i][1] ^ (*state)[i][2] ^ (*state)[i][3];
|
||||
Tm = (*state)[i][0] ^ (*state)[i][1]; Tm = xtime(Tm); (*state)[i][0] ^= Tm ^ Tmp;
|
||||
Tm = (*state)[i][1] ^ (*state)[i][2]; Tm = xtime(Tm); (*state)[i][1] ^= Tm ^ Tmp;
|
||||
Tm = (*state)[i][2] ^ (*state)[i][3]; Tm = xtime(Tm); (*state)[i][2] ^= Tm ^ Tmp;
|
||||
Tm = (*state)[i][3] ^ t; Tm = xtime(Tm); (*state)[i][3] ^= Tm ^ Tmp;
|
||||
}
|
||||
}
|
||||
|
||||
static void Cipher(state_t* state, const uint8_t* RoundKey)
|
||||
{
|
||||
uint8_t round = 0;
|
||||
AddRoundKey(0, state, RoundKey);
|
||||
for (round = 1; ; ++round) {
|
||||
SubBytes(state);
|
||||
ShiftRows(state);
|
||||
if (round == Nr) break;
|
||||
MixColumns(state);
|
||||
AddRoundKey(round, state, RoundKey);
|
||||
}
|
||||
AddRoundKey(Nr, state, RoundKey);
|
||||
}
|
||||
|
||||
void vble_aes_init_ctx_iv(struct vble_aes_ctx* ctx,
|
||||
const uint8_t* key, const uint8_t* iv)
|
||||
{
|
||||
KeyExpansion(ctx->RoundKey, key);
|
||||
memcpy(ctx->Iv, iv, VBLE_AES_BLOCKLEN);
|
||||
}
|
||||
|
||||
void vble_aes_ctr_xcrypt(struct vble_aes_ctx* ctx, uint8_t* buf, size_t length)
|
||||
{
|
||||
uint8_t buffer[VBLE_AES_BLOCKLEN];
|
||||
size_t i;
|
||||
int bi;
|
||||
for (i = 0, bi = VBLE_AES_BLOCKLEN; i < length; ++i, ++bi) {
|
||||
if (bi == VBLE_AES_BLOCKLEN) { // regenerate keystream block
|
||||
memcpy(buffer, ctx->Iv, VBLE_AES_BLOCKLEN);
|
||||
Cipher((state_t*)buffer, ctx->RoundKey);
|
||||
|
||||
// Increment counter (Iv) from the least-significant byte.
|
||||
for (bi = (VBLE_AES_BLOCKLEN - 1); bi >= 0; --bi) {
|
||||
if (ctx->Iv[bi] == 255) {
|
||||
ctx->Iv[bi] = 0;
|
||||
continue;
|
||||
}
|
||||
ctx->Iv[bi] += 1;
|
||||
break;
|
||||
}
|
||||
bi = 0;
|
||||
}
|
||||
buf[i] = (buf[i] ^ buffer[bi]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Minimal AES-128 CTR-mode implementation for VictronBLE.
|
||||
*
|
||||
* Trimmed (CTR only, AES-128 only) and symbol-prefixed adaptation of
|
||||
* kokke/tiny-AES-c (public domain / Unlicense), verified against the test
|
||||
* vectors in NIST SP 800-38A. Bundled so the library has no external crypto
|
||||
* dependency and builds identically on ESP32, nRF52 and any other target.
|
||||
*
|
||||
* Counter increment matches mbedTLS mbedtls_aes_crypt_ctr (increments the
|
||||
* 128-bit counter from the least-significant byte), so output is byte-identical
|
||||
* to the previous ESP32 mbedTLS-based decryption.
|
||||
*/
|
||||
#ifndef VBLE_AES_H_
|
||||
#define VBLE_AES_H_
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define VBLE_AES_BLOCKLEN 16 // AES block length in bytes (128-bit)
|
||||
#define VBLE_AES_KEYLEN 16 // AES-128 key length in bytes
|
||||
#define VBLE_AES_KEYEXPSIZE 176
|
||||
|
||||
struct vble_aes_ctx {
|
||||
uint8_t RoundKey[VBLE_AES_KEYEXPSIZE];
|
||||
uint8_t Iv[VBLE_AES_BLOCKLEN];
|
||||
};
|
||||
|
||||
// Initialise context with a 16-byte key and 16-byte IV (counter).
|
||||
void vble_aes_init_ctx_iv(struct vble_aes_ctx* ctx,
|
||||
const uint8_t* key, const uint8_t* iv);
|
||||
|
||||
// CTR-mode keystream XOR. Symmetric: same call encrypts and decrypts.
|
||||
// Operates in place on `buf` for `length` bytes (length need not be a
|
||||
// multiple of the block size).
|
||||
void vble_aes_ctr_xcrypt(struct vble_aes_ctx* ctx, uint8_t* buf, size_t length);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // VBLE_AES_H_
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* VictronBLE - ESP32 BLE scanning backend
|
||||
*
|
||||
* Uses the ESP32 Arduino BLE library (Bluedroid). Extracts the manufacturer
|
||||
* data, MAC and RSSI from each passive scan result and hands them to the
|
||||
* platform-independent VictronBLE::onAdvertisement().
|
||||
*
|
||||
* Copyright (c) 2025 Scott Penrose
|
||||
* License: MIT
|
||||
*/
|
||||
#include "../VictronBLE.h"
|
||||
|
||||
#if defined(VICTRON_BACKEND_ESP32)
|
||||
|
||||
#include <string>
|
||||
|
||||
// Scan complete callback — clears the flag so loop() restarts the scan
|
||||
static bool s_scanning = false;
|
||||
static void onScanDone(BLEScanResults results) {
|
||||
s_scanning = false;
|
||||
}
|
||||
|
||||
bool VictronBLE::begin(uint32_t scanDuration) {
|
||||
if (initialized) return true;
|
||||
this->scanDuration = scanDuration;
|
||||
|
||||
BLEDevice::init("VictronBLE");
|
||||
pBLEScan = BLEDevice::getScan();
|
||||
if (!pBLEScan) return false;
|
||||
|
||||
scanCallbackObj = new VictronBLEAdvertisedDeviceCallbacks(this);
|
||||
pBLEScan->setAdvertisedDeviceCallbacks(scanCallbackObj, true);
|
||||
pBLEScan->setActiveScan(false); // passive: Victron beacons are non-connectable
|
||||
pBLEScan->setInterval(100);
|
||||
pBLEScan->setWindow(99);
|
||||
|
||||
initialized = true;
|
||||
if (debugEnabled) Serial.println("[VictronBLE] Initialized (ESP32 backend)");
|
||||
return true;
|
||||
}
|
||||
|
||||
void VictronBLE::loop() {
|
||||
if (!initialized) return;
|
||||
if (!s_scanning) {
|
||||
pBLEScan->clearResults();
|
||||
s_scanning = pBLEScan->start(scanDuration, onScanDone, false);
|
||||
}
|
||||
}
|
||||
|
||||
// BLE scan callback
|
||||
void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertisedDevice) {
|
||||
if (victronBLE) victronBLE->processDevice(advertisedDevice);
|
||||
}
|
||||
|
||||
void VictronBLE::processDevice(BLEAdvertisedDevice& advertisedDevice) {
|
||||
// Debug: print every BLE device seen (before any filtering)
|
||||
if (debugEnabled) {
|
||||
Serial.printf("[VictronBLE] MAC=%-17s RSSI=%-4d Name=%-20s ManData=%s\n",
|
||||
advertisedDevice.getAddress().toString().c_str(),
|
||||
advertisedDevice.getRSSI(),
|
||||
advertisedDevice.haveName() ? advertisedDevice.getName().c_str() : "(none)",
|
||||
advertisedDevice.haveManufacturerData() ? "yes" : "no");
|
||||
}
|
||||
|
||||
if (!advertisedDevice.haveManufacturerData()) return;
|
||||
|
||||
// getManufacturerData() returns std::string on older ESP32 BLE libraries and
|
||||
// an Arduino String on newer ones. Both expose c_str()/length(); building a
|
||||
// std::string from (ptr, len) preserves the binary payload's null bytes.
|
||||
auto mfg = advertisedDevice.getManufacturerData();
|
||||
std::string raw(mfg.c_str(), mfg.length());
|
||||
|
||||
onAdvertisement(reinterpret_cast<const uint8_t*>(raw.data()), raw.length(),
|
||||
advertisedDevice.getAddress().toString().c_str(),
|
||||
advertisedDevice.getRSSI());
|
||||
}
|
||||
|
||||
#endif // VICTRON_BACKEND_ESP32
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* VictronBLE - nRF52 BLE scanning backend (Adafruit/Seeed Bluefruit)
|
||||
*
|
||||
* Uses the Bluefruit nRF52 library bundled with the Adafruit/Seeed nRF52 core.
|
||||
* Performs a continuous passive scan and extracts the manufacturer data, MAC
|
||||
* and RSSI from each advertisement, handing them to the platform-independent
|
||||
* VictronBLE::onAdvertisement().
|
||||
*
|
||||
* Tested target: Seeed XIAO nRF52840.
|
||||
*
|
||||
* Copyright (c) 2025 Scott Penrose
|
||||
* License: MIT
|
||||
*/
|
||||
#include "../VictronBLE.h"
|
||||
|
||||
#if defined(VICTRON_BACKEND_NRF52)
|
||||
|
||||
VictronBLE* VictronBLE::s_instance = nullptr;
|
||||
|
||||
bool VictronBLE::begin(uint32_t scanDuration) {
|
||||
if (initialized) return true;
|
||||
this->scanDuration = scanDuration; // not used for nRF52 (scan is continuous)
|
||||
s_instance = this;
|
||||
|
||||
Bluefruit.begin(0, 1); // 0 peripheral, 1 central (observer)
|
||||
Bluefruit.setName("VictronBLE");
|
||||
|
||||
Bluefruit.Scanner.setRxCallback(VictronBLE::scanCallback);
|
||||
Bluefruit.Scanner.restartOnDisconnect(true);
|
||||
Bluefruit.Scanner.setInterval(160, 80); // 100ms interval / 50ms window (0.625ms units)
|
||||
Bluefruit.Scanner.useActiveScan(false); // passive: Victron beacons are non-connectable
|
||||
Bluefruit.Scanner.start(0); // 0 = scan forever
|
||||
|
||||
initialized = true;
|
||||
if (debugEnabled) Serial.println("[VictronBLE] Initialized (nRF52 Bluefruit backend)");
|
||||
return true;
|
||||
}
|
||||
|
||||
void VictronBLE::loop() {
|
||||
// Scanning is fully event-driven on nRF52 (SoftDevice invokes scanCallback);
|
||||
// nothing to pump here. Kept for API parity with the ESP32 backend.
|
||||
}
|
||||
|
||||
void VictronBLE::scanCallback(ble_gap_evt_adv_report_t* report) {
|
||||
if (s_instance) {
|
||||
// Format MAC (little-endian to big-endian hex)
|
||||
const uint8_t* a = report->peer_addr.addr;
|
||||
char mac[18];
|
||||
snprintf(mac, sizeof(mac), "%02x:%02x:%02x:%02x:%02x:%02x",
|
||||
a[5], a[4], a[3], a[2], a[1], a[0]);
|
||||
|
||||
// Debug: print every BLE device seen (before any filtering)
|
||||
if (s_instance->debugEnabled) {
|
||||
// Manufacturer specific data (AD type 0xFF) — includes the 0x02E1 vendor ID
|
||||
uint8_t mfgBuf[31];
|
||||
uint8_t mfgLen = Bluefruit.Scanner.parseReportByType(
|
||||
report, BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA, mfgBuf, sizeof(mfgBuf));
|
||||
|
||||
// Try to get device name
|
||||
char nameBuf[32] = "(none)";
|
||||
uint8_t nameLen = Bluefruit.Scanner.parseReportByType(
|
||||
report, BLE_GAP_AD_TYPE_COMPLETE_LOCAL_NAME, (uint8_t*)nameBuf, sizeof(nameBuf) - 1);
|
||||
if (nameLen == 0) {
|
||||
nameLen = Bluefruit.Scanner.parseReportByType(
|
||||
report, BLE_GAP_AD_TYPE_SHORT_LOCAL_NAME, (uint8_t*)nameBuf, sizeof(nameBuf) - 1);
|
||||
}
|
||||
if (nameLen > 0) nameBuf[nameLen] = '\0';
|
||||
|
||||
Serial.printf("[VictronBLE] MAC=%-17s RSSI=%-4d Name=%-20s ManData=%s\n",
|
||||
mac, report->rssi, nameBuf, mfgLen >= 2 ? "yes" : "no");
|
||||
}
|
||||
|
||||
// Manufacturer specific data (AD type 0xFF) — includes the 0x02E1 vendor ID
|
||||
uint8_t buf[31];
|
||||
uint8_t len = Bluefruit.Scanner.parseReportByType(
|
||||
report, BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA, buf, sizeof(buf));
|
||||
|
||||
if (len >= 2) {
|
||||
s_instance->onAdvertisement(buf, len, mac, report->rssi);
|
||||
}
|
||||
}
|
||||
|
||||
// Bluefruit pauses scanning while the RX callback runs — must resume.
|
||||
Bluefruit.Scanner.resume();
|
||||
}
|
||||
|
||||
#endif // VICTRON_BACKEND_NRF52
|
||||
Reference in New Issue
Block a user