Compare commits
7 Commits
e7024d9983
...
nrf52
| Author | SHA1 | Date | |
|---|---|---|---|
| 05ee88cd31 | |||
| ef50829c81 | |||
| 53fa6fa0e1 | |||
| de6607d870 | |||
| 105d66b07a | |||
| cd3b4626d1 | |||
| 7706f88399 |
+68
-1
@@ -1,7 +1,7 @@
|
|||||||
# VictronBLE Project Context
|
# VictronBLE Project Context
|
||||||
|
|
||||||
## Project Overview
|
## 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
|
## Key Files
|
||||||
- `src/` - Main library source code
|
- `src/` - Main library source code
|
||||||
@@ -281,3 +281,70 @@ a843eb9 Keep v0.3.1
|
|||||||
- README.md
|
- README.md
|
||||||
- REVIEW.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
|
# PlatformIO
|
||||||
.pio
|
.pio
|
||||||
.pioenvs
|
.pioenvs
|
||||||
@@ -12,6 +14,8 @@ compile_commands.json
|
|||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.history/
|
.history/
|
||||||
|
|
||||||
|
bugs
|
||||||
|
|
||||||
# Build artifacts
|
# Build artifacts
|
||||||
*.o
|
*.o
|
||||||
*.a
|
*.a
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
# VictronBLE
|
# 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**.
|
||||||
|
|
||||||
**⚠️ API CHANGE in v0.4 — not backwards compatible with v0.3.x**
|
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.
|
||||||
|
|
||||||
v0.4 is a major rework of the library internals: new callback API, reduced memory usage, non-blocking scanning. See [VERSIONS](VERSIONS) for full details. A stable **v1.0** release with a consistent, long-term API is coming soon.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Why another library? Most of the Victron BLE examples are built into other frameworks (e.g. ESPHome) and I want a library that can be used in all ESP32 systems, including ESPHome or other frameworks. With long term plan to try and move others to this library and improve code with many eyes.
|
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 supporting ESP32 S and C series (tested on older ESP32, ESP32-S3 and ESP32-C3). Other chipsets can be added with abstraction of Bluetooth code.
|
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
|
## 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
|
- ✅ **Multiple Device Support**: Monitor multiple Victron devices simultaneously
|
||||||
- ✅ **All Device Types**: Solar chargers, battery monitors, inverters, DC-DC converters
|
- ✅ **All Device Types**: Solar chargers, battery monitors, inverters, DC-DC converters, AC chargers
|
||||||
- ✅ **Framework Agnostic**: Works with Arduino and ESP-IDF
|
- ✅ **Framework Friendly**: Works with Arduino (and ESP-IDF on ESP32)
|
||||||
- ✅ **Clean API**: Simple, intuitive interface with callback support
|
- ✅ **Clean API**: Simple, intuitive interface with callback support
|
||||||
- ✅ **No Pairing Required**: Reads BLE advertisement data directly
|
- ✅ **No Pairing Required**: Reads BLE advertisement data directly
|
||||||
- ✅ **Low Power**: Uses passive BLE scanning
|
- ✅ **Low Power**: Uses passive BLE scanning
|
||||||
@@ -29,29 +29,58 @@ Currently supporting ESP32 S and C series (tested on older ESP32, ESP32-S3 and E
|
|||||||
- **Battery Monitors**: SmartShunt, BMV-712 Smart, BMV-700 series
|
- **Battery Monitors**: SmartShunt, BMV-712 Smart, BMV-700 series
|
||||||
- **Inverters**: MultiPlus, Quattro, Phoenix (with VE.Bus BLE dongle)
|
- **Inverters**: MultiPlus, Quattro, Phoenix (with VE.Bus BLE dongle)
|
||||||
- **DC-DC Converters**: Orion Smart, Orion XS
|
- **DC-DC Converters**: Orion Smart, Orion XS
|
||||||
|
- **AC Chargers**: Blue Smart IP22 / IP65 / IP67 chargers
|
||||||
- **Others**: Smart Battery Protect, Lynx Smart BMS, Smart Lithium batteries
|
- **Others**: Smart Battery Protect, Lynx Smart BMS, Smart Lithium batteries
|
||||||
|
|
||||||
## Hardware Requirements
|
## Hardware Requirements
|
||||||
|
|
||||||
- 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
|
- 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
|
## Installation
|
||||||
|
|
||||||
### PlatformIO
|
### PlatformIO
|
||||||
|
|
||||||
1. Add to `platformio.ini`:
|
1. Add to `platformio.ini` (recommended — installs from the PlatformIO registry):
|
||||||
```ini
|
```ini
|
||||||
lib_deps =
|
lib_deps =
|
||||||
https://gitea.sh3d.com.au/Sh3d/VictronBLE
|
scottp/victronble
|
||||||
|
```
|
||||||
|
|
||||||
|
Or install directly from git. Note the **`.git` suffix is required** — the bare
|
||||||
|
repository URL is not accepted by PlatformIO:
|
||||||
|
```ini
|
||||||
|
lib_deps =
|
||||||
|
https://gitea.sh3d.com.au/Sh3d/VictronBLE.git
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Or clone into your project's `lib` folder:
|
2. Or clone into your project's `lib` folder:
|
||||||
```bash
|
```bash
|
||||||
cd lib
|
cd lib
|
||||||
git clone https://gitea.sh3d.com.au/Sh3d/VictronBLE
|
git clone https://gitea.sh3d.com.au/Sh3d/VictronBLE.git
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 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
|
### Arduino IDE
|
||||||
|
|
||||||
1. Download or clone this repository
|
1. Download or clone this repository
|
||||||
@@ -262,6 +291,23 @@ struct VictronDCDCData {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### VictronACChargerData
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct VictronACChargerData {
|
||||||
|
uint8_t chargeState; // SolarChargerState enum (shared charger states)
|
||||||
|
uint8_t errorCode;
|
||||||
|
float voltage1; // V (output 1)
|
||||||
|
float current1; // A (output 1)
|
||||||
|
float voltage2; // V (output 2, 0 if absent)
|
||||||
|
float current2; // A (output 2, 0 if absent)
|
||||||
|
float voltage3; // V (output 3, 0 if absent)
|
||||||
|
float current3; // A (output 3, 0 if absent)
|
||||||
|
float temperature; // C (0 if not available)
|
||||||
|
float acCurrent; // A (0 if not available)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
## Advanced Usage
|
## Advanced Usage
|
||||||
|
|
||||||
### Multiple Devices
|
### Multiple Devices
|
||||||
@@ -299,6 +345,11 @@ void onVictronData(const VictronDevice* dev) {
|
|||||||
Serial.printf("%s: %.2fV -> %.2fV\n", dev->name,
|
Serial.printf("%s: %.2fV -> %.2fV\n", dev->name,
|
||||||
dev->dcdc.inputVoltage, dev->dcdc.outputVoltage);
|
dev->dcdc.inputVoltage, dev->dcdc.outputVoltage);
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -351,11 +402,40 @@ This library implements the Victron BLE Advertising protocol:
|
|||||||
|
|
||||||
Based on official [Victron BLE documentation](https://www.victronenergy.com/live/vedirect_protocol:faq).
|
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
|
## Examples
|
||||||
|
|
||||||
See the `examples/` directory for:
|
See the `examples/` directory for:
|
||||||
|
|
||||||
- **MultiDevice**: Monitor multiple devices with callbacks
|
- **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
|
- **Logger**: Change-detection logging for Solar Charger data
|
||||||
- **Repeater**: Collect BLE data and re-transmit via ESPNow broadcast
|
- **Repeater**: Collect BLE data and re-transmit via ESPNow broadcast
|
||||||
- **Receiver**: Receive ESPNow packets from a Repeater and display data
|
- **Receiver**: Receive ESPNow packets from a Repeater and display data
|
||||||
@@ -363,9 +443,14 @@ See the `examples/` directory for:
|
|||||||
|
|
||||||
## Contributing
|
## 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:
|
Contributions welcome! Please:
|
||||||
|
|
||||||
1. Fork the repository
|
1. Fork the [GitHub mirror](https://github.com/SH3D/VictronBLE)
|
||||||
2. Create a feature branch
|
2. Create a feature branch
|
||||||
3. Test thoroughly on real hardware
|
3. Test thoroughly on real hardware
|
||||||
4. Submit a pull request
|
4. Submit a pull request
|
||||||
@@ -397,7 +482,10 @@ See [VERSIONS](VERSIONS) file for detailed changelog and release history.
|
|||||||
|
|
||||||
## Support
|
## 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
|
- 📖 Check the examples directory
|
||||||
- 🔧 Enable debug mode for diagnostics
|
- 🔧 Enable debug mode for diagnostics
|
||||||
- 📚 See [Victron documentation](https://www.victronenergy.com/live/)
|
- 📚 See [Victron documentation](https://www.victronenergy.com/live/)
|
||||||
|
|||||||
-106
@@ -1,106 +0,0 @@
|
|||||||
# Upgrading to VictronBLE v0.4
|
|
||||||
|
|
||||||
v0.4 is a breaking API change that simplifies the library significantly.
|
|
||||||
|
|
||||||
## Summary of Changes
|
|
||||||
|
|
||||||
- **Callback**: Virtual class → function pointer
|
|
||||||
- **Data access**: Inheritance → tagged union (`VictronDevice` with `solar`, `battery`, `inverter`, `dcdc` members)
|
|
||||||
- **Strings**: Arduino `String` → fixed `char[]` arrays
|
|
||||||
- **Memory**: `std::map` + heap allocation → fixed array, zero dynamic allocation
|
|
||||||
- **Removed**: `getLastError()`, `removeDevice()`, `getDevicesByType()`, per-type getter methods, `VictronDeviceConfig` struct, `VictronDeviceCallback` class
|
|
||||||
- **Removed field**: `panelVoltage` (was unreliably derived from `panelPower / batteryCurrent`)
|
|
||||||
|
|
||||||
## Migration Guide
|
|
||||||
|
|
||||||
### 1. Callback: class → function pointer
|
|
||||||
|
|
||||||
**Before (v0.3):**
|
|
||||||
```cpp
|
|
||||||
class MyCallback : public VictronDeviceCallback {
|
|
||||||
void onSolarChargerData(const SolarChargerData& data) override {
|
|
||||||
Serial.println(data.deviceName + ": " + String(data.panelPower) + "W");
|
|
||||||
}
|
|
||||||
void onBatteryMonitorData(const BatteryMonitorData& data) override {
|
|
||||||
Serial.println("SOC: " + String(data.soc) + "%");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
MyCallback callback;
|
|
||||||
victron.setCallback(&callback);
|
|
||||||
```
|
|
||||||
|
|
||||||
**After (v0.4):**
|
|
||||||
```cpp
|
|
||||||
void onVictronData(const VictronDevice* dev) {
|
|
||||||
switch (dev->deviceType) {
|
|
||||||
case DEVICE_TYPE_SOLAR_CHARGER:
|
|
||||||
Serial.printf("%s: %.0fW\n", dev->name, dev->solar.panelPower);
|
|
||||||
break;
|
|
||||||
case DEVICE_TYPE_BATTERY_MONITOR:
|
|
||||||
Serial.printf("SOC: %.1f%%\n", dev->battery.soc);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
victron.setCallback(onVictronData);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Data field access
|
|
||||||
|
|
||||||
Fields moved from flat `SolarChargerData` etc. into the `VictronDevice` tagged union:
|
|
||||||
|
|
||||||
| Old (v0.3) | New (v0.4) |
|
|
||||||
|---|---|
|
|
||||||
| `data.deviceName` | `dev->name` (char[32]) |
|
|
||||||
| `data.macAddress` | `dev->mac` (char[13]) |
|
|
||||||
| `data.rssi` | `dev->rssi` |
|
|
||||||
| `data.lastUpdate` | `dev->lastUpdate` |
|
|
||||||
| `data.batteryVoltage` | `dev->solar.batteryVoltage` |
|
|
||||||
| `data.batteryCurrent` | `dev->solar.batteryCurrent` |
|
|
||||||
| `data.panelPower` | `dev->solar.panelPower` |
|
|
||||||
| `data.yieldToday` | `dev->solar.yieldToday` |
|
|
||||||
| `data.loadCurrent` | `dev->solar.loadCurrent` |
|
|
||||||
| `data.chargeState` | `dev->solar.chargeState` (uint8_t, was enum) |
|
|
||||||
| `data.panelVoltage` | **Removed** - see below |
|
|
||||||
|
|
||||||
### 3. panelVoltage removed
|
|
||||||
|
|
||||||
`panelVoltage` was a derived value (`panelPower / batteryCurrent`) that was unreliable (division by zero when no current, inaccurate due to MPPT conversion). It has been removed.
|
|
||||||
|
|
||||||
If you need an estimate:
|
|
||||||
```cpp
|
|
||||||
float panelVoltage = (dev->solar.batteryCurrent > 0.1f)
|
|
||||||
? dev->solar.panelPower / dev->solar.batteryCurrent
|
|
||||||
: 0.0f;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. getLastError() removed
|
|
||||||
|
|
||||||
Debug output now goes directly to Serial when `setDebug(true)` is enabled. Remove any `getLastError()` calls.
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
```cpp
|
|
||||||
if (!victron.begin(2)) {
|
|
||||||
Serial.println(victron.getLastError());
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
```cpp
|
|
||||||
if (!victron.begin(2)) {
|
|
||||||
Serial.println("Failed to initialize VictronBLE!");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. String types
|
|
||||||
|
|
||||||
Device name and MAC are now `char[]` instead of Arduino `String`. Use `Serial.printf()` or `String(dev->name)` if you need a String object.
|
|
||||||
|
|
||||||
### 6. addDevice() parameters
|
|
||||||
|
|
||||||
Parameters changed from `String` to `const char*`. Existing string literals work unchanged. `VictronDeviceConfig` struct is no longer needed.
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// Both v0.3 and v0.4 - string literals work the same
|
|
||||||
victron.addDevice("MySolar", "f69dfcce55eb",
|
|
||||||
"bf25c098c156afd6a180157b8a3ab1fb", DEVICE_TYPE_SOLAR_CHARGER);
|
|
||||||
```
|
|
||||||
@@ -1,5 +1,59 @@
|
|||||||
# Version History
|
# 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)
|
## 0.4.1 (2026-02-28)
|
||||||
|
|
||||||
Major rework of library internals. Breaking API change — not backwards compatible with 0.3.x.
|
Major rework of library internals. Breaking API change — not backwards compatible with 0.3.x.
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
[env]
|
[env]
|
||||||
lib_extra_dirs = ../..
|
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]
|
[env:esp32dev]
|
||||||
platform = espressif32
|
platform = espressif32
|
||||||
board = esp32dev
|
board = esp32dev
|
||||||
|
|||||||
@@ -3,10 +3,14 @@
|
|||||||
*
|
*
|
||||||
* Demonstrates reading data from multiple Victron device types via BLE.
|
* Demonstrates reading data from multiple Victron device types via BLE.
|
||||||
*
|
*
|
||||||
|
* 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:
|
* Setup:
|
||||||
* 1. Get your device encryption keys from VictronConnect app
|
* 1. Get your device encryption keys from the VictronConnect app
|
||||||
* (Settings > Product Info > Instant readout via Bluetooth > Show)
|
* (Settings > Product Info > Instant readout via Bluetooth > Show)
|
||||||
* 2. Update the device configurations below with your MAC and key
|
* 2. Update the device configurations below with your MAC and key.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
@@ -18,6 +22,7 @@ static uint32_t solarChargerCount = 0;
|
|||||||
static uint32_t batteryMonitorCount = 0;
|
static uint32_t batteryMonitorCount = 0;
|
||||||
static uint32_t inverterCount = 0;
|
static uint32_t inverterCount = 0;
|
||||||
static uint32_t dcdcConverterCount = 0;
|
static uint32_t dcdcConverterCount = 0;
|
||||||
|
static uint32_t acChargerCount = 0;
|
||||||
|
|
||||||
static const char* chargeStateName(uint8_t state) {
|
static const char* chargeStateName(uint8_t state) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
@@ -120,6 +125,21 @@ void onVictronData(const VictronDevice* dev) {
|
|||||||
Serial.printf("Last Update: %lus ago\n", (millis() - dev->lastUpdate) / 1000);
|
Serial.printf("Last Update: %lus ago\n", (millis() - dev->lastUpdate) / 1000);
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -127,7 +147,9 @@ void onVictronData(const VictronDevice* dev) {
|
|||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
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("\n\n=================================");
|
||||||
Serial.println("VictronBLE Multi-Device Example");
|
Serial.println("VictronBLE Multi-Device Example");
|
||||||
@@ -138,9 +160,10 @@ void setup() {
|
|||||||
while (1) delay(1000);
|
while (1) delay(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
victron.setDebug(false);
|
victron.setDebug(true);
|
||||||
victron.setCallback(onVictronData);
|
victron.setCallback(onVictronData);
|
||||||
|
|
||||||
|
// Replace with your own devices (MAC + 32-char hex key from VictronConnect)
|
||||||
victron.addDevice(
|
victron.addDevice(
|
||||||
"Rainbow48V",
|
"Rainbow48V",
|
||||||
"E4:05:42:34:14:F3",
|
"E4:05:42:34:14:F3",
|
||||||
@@ -163,7 +186,7 @@ static uint32_t loopCount = 0;
|
|||||||
static uint32_t lastReport = 0;
|
static uint32_t lastReport = 0;
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
victron.loop(); // Non-blocking: returns immediately if scan is running
|
victron.loop(); // Non-blocking on both backends
|
||||||
loopCount++;
|
loopCount++;
|
||||||
|
|
||||||
uint32_t now = millis();
|
uint32_t now = millis();
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "victronble",
|
"name": "victronble",
|
||||||
"version": "0.4.1",
|
"version": "0.6.0",
|
||||||
"description": "ESP32 library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements. Supports SmartSolar MPPT, SmartShunt, BMV, MultiPlus, Orion and other Victron devices.",
|
"description": "Portable Arduino library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements. Runs on ESP32 and nRF52840. 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, iot, energy, monitoring",
|
"keywords": "victron, ble, bluetooth, solar, mppt, battery, smartshunt, smartsolar, bmv, inverter, multiplus, esp32, nrf52, nrf52840, xiao, iot, energy, monitoring",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitea.sh3d.com.au/Sh3d/VictronBLE.git"
|
"url": "https://gitea.sh3d.com.au/Sh3d/VictronBLE.git"
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://gitea.sh3d.com.au/Sh3d/VictronBLE",
|
"homepage": "https://gitea.sh3d.com.au/Sh3d/VictronBLE",
|
||||||
"frameworks": ["arduino", "espidf"],
|
"frameworks": ["arduino", "espidf"],
|
||||||
"platforms": ["espressif32"],
|
"platforms": ["espressif32", "nordicnrf52"],
|
||||||
"headers": ["VictronBLE.h"],
|
"headers": ["VictronBLE.h"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"examples": [
|
"examples": [
|
||||||
|
|||||||
+4
-4
@@ -1,11 +1,11 @@
|
|||||||
name=VictronBLE
|
name=VictronBLE
|
||||||
version=0.4.1
|
version=0.6.0
|
||||||
author=Scott Penrose
|
author=Scott Penrose
|
||||||
maintainer=Scott Penrose <scottp@dd.com.au>
|
maintainer=Scott Penrose <scottp@dd.com.au>
|
||||||
sentence=ESP32 library for reading Victron Energy device data via BLE for any ESP32
|
sentence=Portable library for reading Victron Energy device data via BLE on ESP32 and nRF52840
|
||||||
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.
|
paragraph=Read data from Victron SmartSolar, SmartShunt, BMV, inverters, Blue Smart AC chargers and other devices using Bluetooth Low Energy advertisements. Runs on ESP32 and nRF52840 (Bluefruit) with no external crypto dependency. Supports multiple devices simultaneously with no pairing required.
|
||||||
category=Communication
|
category=Communication
|
||||||
url=https://gitea.sh3d.com.au/Sh3d/VictronBLE
|
url=https://gitea.sh3d.com.au/Sh3d/VictronBLE
|
||||||
architectures=esp32
|
architectures=esp32,nrf52
|
||||||
depends=
|
depends=
|
||||||
includes=VictronBLE.h
|
includes=VictronBLE.h
|
||||||
|
|||||||
+127
-116
@@ -1,40 +1,26 @@
|
|||||||
/**
|
/**
|
||||||
* VictronBLE - ESP32 library for Victron Energy BLE devices
|
* VictronBLE - portable library for Victron Energy BLE devices
|
||||||
* Implementation file
|
* 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
|
* Copyright (c) 2025 Scott Penrose
|
||||||
* License: MIT
|
* License: MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "VictronBLE.h"
|
#include "VictronBLE.h"
|
||||||
|
#include "crypto/vble_aes.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
VictronBLE::VictronBLE()
|
VictronBLE::VictronBLE()
|
||||||
: deviceCount(0), pBLEScan(nullptr), scanCallbackObj(nullptr),
|
: deviceCount(0), callback(nullptr), debugEnabled(false),
|
||||||
callback(nullptr), debugEnabled(false), scanDuration(5),
|
scanDuration(5), minIntervalMs(1000), initialized(false)
|
||||||
minIntervalMs(1000), initialized(false) {
|
#if defined(VICTRON_BACKEND_ESP32)
|
||||||
|
, pBLEScan(nullptr), scanCallbackObj(nullptr)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
memset(devices, 0, sizeof(devices));
|
memset(devices, 0, sizeof(devices));
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
pBLEScan->setInterval(100);
|
|
||||||
pBLEScan->setWindow(99);
|
|
||||||
|
|
||||||
initialized = true;
|
|
||||||
if (debugEnabled) Serial.println("[VictronBLE] Initialized");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VictronBLE::addDevice(const char* name, const char* mac, const char* hexKey,
|
bool VictronBLE::addDevice(const char* name, const char* mac, const char* hexKey,
|
||||||
VictronDeviceType type) {
|
VictronDeviceType type) {
|
||||||
if (deviceCount >= VICTRON_MAX_DEVICES) return false;
|
if (deviceCount >= VICTRON_MAX_DEVICES) return false;
|
||||||
@@ -65,44 +51,26 @@ bool VictronBLE::addDevice(const char* name, const char* mac, const char* hexKey
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan complete callback — sets flag so loop() restarts
|
// Platform-independent advertisement handler. Each BLE backend extracts the
|
||||||
static bool s_scanning = false;
|
// manufacturer-data bytes (vendor ID first), MAC string and RSSI from a scan
|
||||||
static void onScanDone(BLEScanResults results) {
|
// result and feeds them here.
|
||||||
s_scanning = false;
|
void VictronBLE::onAdvertisement(const uint8_t* mfgData, size_t len,
|
||||||
}
|
const char* macStr, int8_t rssi) {
|
||||||
|
if (!mfgData || len < 10) return;
|
||||||
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) {
|
|
||||||
if (!advertisedDevice.haveManufacturerData()) return;
|
|
||||||
|
|
||||||
std::string raw = advertisedDevice.getManufacturerData();
|
|
||||||
if (raw.length() < 10) return;
|
|
||||||
|
|
||||||
// Quick vendor ID check before any other work
|
// Quick vendor ID check before any other work
|
||||||
uint16_t vendorID = (uint8_t)raw[0] | ((uint8_t)raw[1] << 8);
|
uint16_t vendorID = mfgData[0] | ((uint16_t)mfgData[1] << 8);
|
||||||
if (vendorID != VICTRON_MANUFACTURER_ID) return;
|
if (vendorID != VICTRON_MANUFACTURER_ID) return;
|
||||||
|
|
||||||
// Parse manufacturer data
|
// Copy into the wire-format struct
|
||||||
victronManufacturerData mfgData;
|
victronManufacturerData mfg;
|
||||||
memset(&mfgData, 0, sizeof(mfgData));
|
memset(&mfg, 0, sizeof(mfg));
|
||||||
size_t copyLen = raw.length() > sizeof(mfgData) ? sizeof(mfgData) : raw.length();
|
size_t copyLen = len > sizeof(mfg) ? sizeof(mfg) : len;
|
||||||
raw.copy(reinterpret_cast<char*>(&mfgData), copyLen);
|
memcpy(&mfg, mfgData, copyLen);
|
||||||
|
|
||||||
// Normalize MAC and find device
|
// Normalize MAC and find device
|
||||||
char normalizedMAC[VICTRON_MAC_LEN];
|
char normalizedMAC[VICTRON_MAC_LEN];
|
||||||
normalizeMAC(advertisedDevice.getAddress().toString().c_str(), normalizedMAC);
|
normalizeMAC(macStr, normalizedMAC);
|
||||||
|
|
||||||
DeviceEntry* entry = findDevice(normalizedMAC);
|
DeviceEntry* entry = findDevice(normalizedMAC);
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
@@ -111,9 +79,8 @@ void VictronBLE::processDevice(BLEAdvertisedDevice& advertisedDevice) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Skip if nonce unchanged (data hasn't changed on the device)
|
// Skip if nonce unchanged (data hasn't changed on the device)
|
||||||
if (entry->device.dataValid && mfgData.nonceDataCounter == entry->lastNonce) {
|
if (entry->device.dataValid && mfg.nonceDataCounter == entry->lastNonce) {
|
||||||
// Still update RSSI since we got a packet
|
entry->device.rssi = rssi; // still refresh RSSI
|
||||||
entry->device.rssi = advertisedDevice.getRSSI();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,11 +91,11 @@ void VictronBLE::processDevice(BLEAdvertisedDevice& advertisedDevice) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (debugEnabled) Serial.printf("[VictronBLE] Processing: %s nonce:0x%04X\n",
|
if (debugEnabled) Serial.printf("[VictronBLE] Processing: %s nonce:0x%04X\n",
|
||||||
entry->device.name, mfgData.nonceDataCounter);
|
entry->device.name, mfg.nonceDataCounter);
|
||||||
|
|
||||||
if (parseAdvertisement(entry, mfgData)) {
|
if (parseAdvertisement(entry, mfg)) {
|
||||||
entry->lastNonce = mfgData.nonceDataCounter;
|
entry->lastNonce = mfg.nonceDataCounter;
|
||||||
entry->device.rssi = advertisedDevice.getRSSI();
|
entry->device.rssi = rssi;
|
||||||
entry->device.lastUpdate = now;
|
entry->device.lastUpdate = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,6 +147,10 @@ bool VictronBLE::parseAdvertisement(DeviceEntry* entry, const victronManufacture
|
|||||||
entry->device.deviceType = DEVICE_TYPE_DCDC_CONVERTER;
|
entry->device.deviceType = DEVICE_TYPE_DCDC_CONVERTER;
|
||||||
ok = parseDCDCConverter(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.dcdc);
|
ok = parseDCDCConverter(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.dcdc);
|
||||||
break;
|
break;
|
||||||
|
case DEVICE_TYPE_AC_CHARGER:
|
||||||
|
entry->device.deviceType = DEVICE_TYPE_AC_CHARGER;
|
||||||
|
ok = parseACCharger(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.acCharger);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
if (debugEnabled) Serial.printf("[VictronBLE] Unknown type: 0x%02X\n", mfg.victronRecordType);
|
if (debugEnabled) Serial.printf("[VictronBLE] Unknown type: 0x%02X\n", mfg.victronRecordType);
|
||||||
return false;
|
return false;
|
||||||
@@ -196,24 +167,13 @@ bool VictronBLE::parseAdvertisement(DeviceEntry* entry, const victronManufacture
|
|||||||
bool VictronBLE::decryptData(const uint8_t* encrypted, size_t len,
|
bool VictronBLE::decryptData(const uint8_t* encrypted, size_t len,
|
||||||
const uint8_t* key, const uint8_t* iv,
|
const uint8_t* key, const uint8_t* iv,
|
||||||
uint8_t* decrypted) {
|
uint8_t* decrypted) {
|
||||||
mbedtls_aes_context aes;
|
// AES-128-CTR via the bundled portable implementation (was mbedTLS on ESP32).
|
||||||
mbedtls_aes_init(&aes);
|
// CTR is symmetric and operates in place, so copy then XOR the keystream.
|
||||||
|
struct vble_aes_ctx ctx;
|
||||||
if (mbedtls_aes_setkey_enc(&aes, key, 128) != 0) {
|
vble_aes_init_ctx_iv(&ctx, key, iv);
|
||||||
mbedtls_aes_free(&aes);
|
memcpy(decrypted, encrypted, len);
|
||||||
return false;
|
vble_aes_ctr_xcrypt(&ctx, decrypted, len);
|
||||||
}
|
return true;
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
int ret = mbedtls_aes_crypt_ctr(&aes, len, &nc_off, nonce_counter,
|
|
||||||
stream_block, encrypted, decrypted);
|
|
||||||
mbedtls_aes_free(&aes);
|
|
||||||
return (ret == 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, VictronSolarData& result) {
|
bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, VictronSolarData& result) {
|
||||||
@@ -222,11 +182,13 @@ bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, VictronSolar
|
|||||||
|
|
||||||
result.chargeState = p->deviceState;
|
result.chargeState = p->deviceState;
|
||||||
result.errorCode = p->errorCode;
|
result.errorCode = p->errorCode;
|
||||||
result.batteryVoltage = p->batteryVoltage * 0.01f;
|
result.batteryVoltage = p->batteryVoltage * 0.01f; // 0.01V units
|
||||||
result.batteryCurrent = p->batteryCurrent * 0.01f;
|
result.batteryCurrent = p->batteryCurrent * 0.1f; // 0.1A units
|
||||||
result.yieldToday = p->yieldToday * 10;
|
result.yieldToday = p->yieldToday * 10;
|
||||||
result.panelPower = p->inputPower;
|
result.panelPower = p->inputPower;
|
||||||
result.loadCurrent = (p->loadCurrent != 0xFFFF) ? p->loadCurrent * 0.01f : 0;
|
// 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) {
|
if (debugEnabled) {
|
||||||
Serial.printf("[VictronBLE] Solar: %.2fV %.2fA %dW State:%d\n",
|
Serial.printf("[VictronBLE] Solar: %.2fV %.2fA %dW State:%d\n",
|
||||||
@@ -236,47 +198,96 @@ bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, VictronSolar
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, VictronBatteryData& result) {
|
bool VictronBLE::parseACCharger(const uint8_t* data, size_t len, VictronACChargerData& result) {
|
||||||
if (len < sizeof(victronBatteryMonitorPayload)) return false;
|
// Payload is bit-packed (10 fields, 104 bits ending in byte 12). Decode LSB-first.
|
||||||
const auto* p = reinterpret_cast<const victronBatteryMonitorPayload*>(data);
|
if (len < 13) return false;
|
||||||
|
|
||||||
result.remainingMinutes = p->remainingMins;
|
size_t bit = 0;
|
||||||
result.voltage = p->batteryVoltage * 0.01f;
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
// Alarm bits
|
result.chargeState = (uint8_t)readBits(8);
|
||||||
result.alarmLowVoltage = (p->alarms & 0x01) != 0;
|
result.errorCode = (uint8_t)readBits(8);
|
||||||
result.alarmHighVoltage = (p->alarms & 0x02) != 0;
|
|
||||||
result.alarmLowSOC = (p->alarms & 0x04) != 0;
|
|
||||||
result.alarmLowTemperature = (p->alarms & 0x10) != 0;
|
|
||||||
result.alarmHighTemperature = (p->alarms & 0x20) != 0;
|
|
||||||
|
|
||||||
// Aux data: voltage or temperature (heuristic: < 30V = voltage)
|
uint32_t v1 = readBits(13), i1 = readBits(11);
|
||||||
// NOTE: Victron protocol uses a flag bit for this, but it's not exposed
|
uint32_t v2 = readBits(13), i2 = readBits(11);
|
||||||
// in the BLE advertisement. This heuristic may misclassify edge cases.
|
uint32_t v3 = readBits(13), i3 = readBits(11);
|
||||||
if (p->auxData < 3000) {
|
uint32_t temp = readBits(7);
|
||||||
result.auxVoltage = p->auxData * 0.01f;
|
uint32_t acCur = readBits(9);
|
||||||
result.temperature = 0;
|
|
||||||
} else {
|
result.voltage1 = (v1 != 0x1FFF) ? v1 * 0.01f : 0;
|
||||||
result.temperature = (p->auxData * 0.01f) - 273.15f;
|
result.current1 = (i1 != 0x7FF) ? i1 * 0.1f : 0;
|
||||||
result.auxVoltage = 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Battery current (22-bit signed, 1 mA units)
|
bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, VictronBatteryData& result) {
|
||||||
int32_t current = p->currentLow |
|
// The payload is bit-packed and not byte-aligned, so it is decoded by bit
|
||||||
(p->currentMid << 8) |
|
// offset directly rather than via a struct. SOC ends at bit 117 (byte 14).
|
||||||
((p->currentHigh_consumedLow & 0x3F) << 16);
|
if (len < 15) return false;
|
||||||
if (current & 0x200000) current |= 0xFFC00000; // Sign extend
|
|
||||||
|
// 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 if (auxMode == 2) {
|
||||||
|
result.temperature = auxRaw * 0.01f - 273.15f; // 0.01K -> C
|
||||||
|
result.auxVoltage = 0;
|
||||||
|
} else {
|
||||||
|
result.auxVoltage = 0;
|
||||||
|
result.temperature = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
result.current = current * 0.001f;
|
||||||
|
|
||||||
// Consumed Ah (18-bit signed, 10 mAh units)
|
// Consumed Ah (bits 88-107), 20-bit, stored as a positive count, 0.1Ah units.
|
||||||
int32_t consumedAh = ((p->currentHigh_consumedLow & 0xC0) >> 6) |
|
// Reported as a negative value (amp-hours consumed).
|
||||||
(p->consumedMid << 2) |
|
uint32_t consumed = (uint32_t)data[11]
|
||||||
(p->consumedHigh << 10);
|
| ((uint32_t)data[12] << 8)
|
||||||
if (consumedAh & 0x20000) consumedAh |= 0xFFFC0000; // Sign extend
|
| ((uint32_t)(data[13] & 0x0F) << 16);
|
||||||
result.consumedAh = consumedAh * 0.01f;
|
result.consumedAh = -((float)consumed * 0.1f);
|
||||||
|
|
||||||
// SOC (10-bit, 0.1% units)
|
// SOC (bits 108-117), 10-bit, 0.1% units
|
||||||
result.soc = (p->soc & 0x3FF) * 0.1f;
|
uint16_t soc = ((uint16_t)(data[13] >> 4) | ((uint16_t)data[14] << 4)) & 0x3FF;
|
||||||
|
result.soc = soc * 0.1f;
|
||||||
|
|
||||||
if (debugEnabled) {
|
if (debugEnabled) {
|
||||||
Serial.printf("[VictronBLE] Battery: %.2fV %.2fA SOC:%.1f%%\n",
|
Serial.printf("[VictronBLE] Battery: %.2fV %.2fA SOC:%.1f%%\n",
|
||||||
|
|||||||
+66
-29
@@ -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
|
* Based on Victron's official BLE Advertising protocol documentation
|
||||||
* Inspired by hoberman's examples and keshavdv's Python library
|
* Inspired by hoberman's examples and keshavdv's Python library
|
||||||
@@ -12,10 +16,21 @@
|
|||||||
#define VICTRON_BLE_H
|
#define VICTRON_BLE_H
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// --- 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 <BLEDevice.h>
|
||||||
#include <BLEAdvertisedDevice.h>
|
#include <BLEAdvertisedDevice.h>
|
||||||
#include <BLEScan.h>
|
#include <BLEScan.h>
|
||||||
#include "mbedtls/aes.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 ---
|
// --- Constants ---
|
||||||
static constexpr uint16_t VICTRON_MANUFACTURER_ID = 0x02E1;
|
static constexpr uint16_t VICTRON_MANUFACTURER_ID = 0x02E1;
|
||||||
@@ -33,11 +48,14 @@ enum VictronDeviceType {
|
|||||||
DEVICE_TYPE_DCDC_CONVERTER = 0x04,
|
DEVICE_TYPE_DCDC_CONVERTER = 0x04,
|
||||||
DEVICE_TYPE_SMART_LITHIUM = 0x05,
|
DEVICE_TYPE_SMART_LITHIUM = 0x05,
|
||||||
DEVICE_TYPE_INVERTER_RS = 0x06,
|
DEVICE_TYPE_INVERTER_RS = 0x06,
|
||||||
DEVICE_TYPE_SMART_BATTERY_PROTECT = 0x07,
|
DEVICE_TYPE_GX_DEVICE = 0x07,
|
||||||
DEVICE_TYPE_LYNX_SMART_BMS = 0x08,
|
DEVICE_TYPE_AC_CHARGER = 0x08,
|
||||||
DEVICE_TYPE_MULTI_RS = 0x09,
|
DEVICE_TYPE_SMART_BATTERY_PROTECT = 0x09,
|
||||||
DEVICE_TYPE_VE_BUS = 0x0A,
|
DEVICE_TYPE_LYNX_SMART_BMS = 0x0A,
|
||||||
DEVICE_TYPE_DC_ENERGY_METER = 0x0B
|
DEVICE_TYPE_MULTI_RS = 0x0B,
|
||||||
|
DEVICE_TYPE_VE_BUS = 0x0C,
|
||||||
|
DEVICE_TYPE_DC_ENERGY_METER = 0x0D,
|
||||||
|
DEVICE_TYPE_ORION_XS = 0x0F
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Device state for Solar Charger ---
|
// --- Device state for Solar Charger ---
|
||||||
@@ -72,27 +90,17 @@ struct victronManufacturerData {
|
|||||||
struct victronSolarChargerPayload {
|
struct victronSolarChargerPayload {
|
||||||
uint8_t deviceState;
|
uint8_t deviceState;
|
||||||
uint8_t errorCode;
|
uint8_t errorCode;
|
||||||
int16_t batteryVoltage; // 10mV units
|
int16_t batteryVoltage; // 0.01V units (signed)
|
||||||
int16_t batteryCurrent; // 10mA units (signed)
|
int16_t batteryCurrent; // 0.1A units (signed)
|
||||||
uint16_t yieldToday; // 10Wh units
|
uint16_t yieldToday; // 0.01kWh (10Wh) units
|
||||||
uint16_t inputPower; // 1W units
|
uint16_t inputPower; // 1W units
|
||||||
uint16_t loadCurrent; // 10mA units (0xFFFF = no load)
|
uint16_t loadCurrent; // 9-bit field, 0.1A units (0x1FF = no load)
|
||||||
uint8_t reserved[2];
|
uint8_t reserved[2];
|
||||||
} __attribute__((packed));
|
} __attribute__((packed));
|
||||||
|
|
||||||
struct victronBatteryMonitorPayload {
|
// NOTE: The battery monitor payload is bit-packed (16-bit alarm, 2-bit aux mode,
|
||||||
uint16_t remainingMins;
|
// 22-bit current, 20-bit consumed Ah, 10-bit SOC) and does NOT byte-align, so it
|
||||||
uint16_t batteryVoltage; // 10mV units
|
// is decoded by bit offset directly in parseBatteryMonitor() rather than a struct.
|
||||||
uint8_t alarms;
|
|
||||||
uint16_t auxData; // 10mV (voltage) or 0.01K (temperature)
|
|
||||||
uint8_t currentLow;
|
|
||||||
uint8_t currentMid;
|
|
||||||
uint8_t currentHigh_consumedLow; // Current bits 16-21 (low 6), consumed bits 0-1 (high 2)
|
|
||||||
uint8_t consumedMid;
|
|
||||||
uint8_t consumedHigh;
|
|
||||||
uint16_t soc; // 0.1% units (10-bit)
|
|
||||||
uint8_t reserved[2];
|
|
||||||
} __attribute__((packed));
|
|
||||||
|
|
||||||
struct victronInverterPayload {
|
struct victronInverterPayload {
|
||||||
uint8_t deviceState;
|
uint8_t deviceState;
|
||||||
@@ -129,6 +137,19 @@ struct VictronSolarData {
|
|||||||
float loadCurrent; // A
|
float loadCurrent; // A
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 {
|
struct VictronBatteryData {
|
||||||
float voltage; // V
|
float voltage; // V
|
||||||
float current; // A
|
float current; // A
|
||||||
@@ -179,6 +200,7 @@ struct VictronDevice {
|
|||||||
VictronBatteryData battery;
|
VictronBatteryData battery;
|
||||||
VictronInverterData inverter;
|
VictronInverterData inverter;
|
||||||
VictronDCDCData dcdc;
|
VictronDCDCData dcdc;
|
||||||
|
VictronACChargerData acCharger;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -209,8 +231,6 @@ public:
|
|||||||
void loop();
|
void loop();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class VictronBLEAdvertisedDeviceCallbacks;
|
|
||||||
|
|
||||||
struct DeviceEntry {
|
struct DeviceEntry {
|
||||||
VictronDevice device;
|
VictronDevice device;
|
||||||
uint8_t key[16];
|
uint8_t key[16];
|
||||||
@@ -218,10 +238,9 @@ private:
|
|||||||
bool active;
|
bool active;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Common state (platform-independent) ---
|
||||||
DeviceEntry devices[VICTRON_MAX_DEVICES];
|
DeviceEntry devices[VICTRON_MAX_DEVICES];
|
||||||
size_t deviceCount;
|
size_t deviceCount;
|
||||||
BLEScan* pBLEScan;
|
|
||||||
VictronBLEAdvertisedDeviceCallbacks* scanCallbackObj;
|
|
||||||
VictronCallback callback;
|
VictronCallback callback;
|
||||||
bool debugEnabled;
|
bool debugEnabled;
|
||||||
uint32_t scanDuration;
|
uint32_t scanDuration;
|
||||||
@@ -233,14 +252,31 @@ private:
|
|||||||
DeviceEntry* findDevice(const char* normalizedMAC);
|
DeviceEntry* findDevice(const char* normalizedMAC);
|
||||||
bool decryptData(const uint8_t* encrypted, size_t len,
|
bool decryptData(const uint8_t* encrypted, size_t len,
|
||||||
const uint8_t* key, const uint8_t* iv, uint8_t* decrypted);
|
const uint8_t* key, const uint8_t* iv, uint8_t* decrypted);
|
||||||
void processDevice(BLEAdvertisedDevice& dev);
|
|
||||||
|
// 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 parseAdvertisement(DeviceEntry* entry, const victronManufacturerData& mfg);
|
||||||
bool parseSolarCharger(const uint8_t* data, size_t len, VictronSolarData& result);
|
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 parseBatteryMonitor(const uint8_t* data, size_t len, VictronBatteryData& result);
|
||||||
bool parseInverter(const uint8_t* data, size_t len, VictronInverterData& result);
|
bool parseInverter(const uint8_t* data, size_t len, VictronInverterData& result);
|
||||||
bool parseDCDCConverter(const uint8_t* data, size_t len, VictronDCDCData& 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)
|
// BLE scan callback (required by ESP32 BLE API)
|
||||||
class VictronBLEAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
|
class VictronBLEAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
|
||||||
public:
|
public:
|
||||||
@@ -249,6 +285,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
VictronBLE* victronBLE;
|
VictronBLE* victronBLE;
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Commented-out features — kept for reference / future use
|
// Commented-out features — kept for reference / future use
|
||||||
|
|||||||
@@ -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