23 Commits

Author SHA1 Message Date
a843eb924b Keep v0.3.1 2026-02-12 18:10:35 +11:00
5a210fb88f Experimenting with a claude file and created new logging example 2026-02-12 18:04:02 +11:00
30c93af18b Fix status 2026-01-29 18:45:37 +11:00
d9577be900 Scripts and automatic claude update 2026-01-29 18:42:41 +11:00
f9e72a68fe Examples todo 2025-12-30 20:54:04 +11:00
a8d40ba260 Fix notes on testing 2025-12-29 20:35:47 +11:00
af39db8732 Fix name 2025-12-29 20:31:07 +11:00
26b0196791 Decoding working for MPPT 2025-12-29 20:22:41 +11:00
0863f8572c Finally working decode MPPT 2025-12-29 20:00:05 +11:00
d3b1c632db work on better mac address 2025-12-29 19:26:32 +11:00
03d8da3b7d Cleaning up by using structs and reusing data blocks 2025-12-29 19:12:47 +11:00
6a517246ea Experimental version 2025-12-29 13:38:47 +11:00
4bbab345b0 Working serial on S3 too. Interesting... 2025-12-29 11:40:46 +11:00
1a651b149d Merge branch 'main' of https://gitea.sh3d.com.au/sh3d/VictronBLE 2025-12-29 11:16:43 +11:00
cec45524d3 Add core2 2025-12-29 11:16:41 +11:00
2bd6094955 Cleanup only 2025-12-29 11:09:33 +11:00
9f0f2ce8fd Improved structs 2025-12-28 23:35:27 +11:00
8e5eba47d7 Working C3 build 2025-12-28 23:27:40 +11:00
95d83b492a Playing with debug 2025-12-19 12:46:28 +11:00
139c6f961d Work on decoding using structs 2025-12-18 22:27:15 +11:00
2ccac7b0c8 Experimenting and decoding - seems some structs are wrong, check original code 2025-12-18 21:49:04 +11:00
97a71ce34c TODO and m5stick and debug 2025-12-18 20:43:10 +11:00
e827dea4e5 Ignore builds 2025-12-18 18:26:27 +11:00
16 changed files with 1485 additions and 318 deletions

104
.claude/CLAUDE.md Normal file
View File

@@ -0,0 +1,104 @@
# VictronBLE Project Context
## Project Overview
Arduino/ESP32 library for reading Victron Energy devices via Bluetooth Low Energy (BLE).
## Key Files
- `src/` - Main library source code
- `examples/` - Example sketches
- `experiment/` - Experimental code
- `library.json` / `library.properties` - PlatformIO/Arduino library config
## Build & Test
- This is an Arduino/PlatformIO library
- Test with PlatformIO: `pio run`
## Session Notes
<!-- Add learnings from each session below -->
### Session: 2026-01-29 18:41
**Modified files:**
- TODO
### Session: 2026-02-11 13:51
**Modified files:**
- .claude/CLAUDE.md
- .claude/scripts/update-claude-md.sh
- TODO
- examples/MultiDevice/src/main.cpp
### Session: 2026-02-11 15:57
**Modified files:**
- .claude/CLAUDE.md
- .claude/scripts/update-claude-md.sh
- TODO
- examples/MultiDevice/src/main.cpp
### Session: 2026-02-12 18:02
**Modified files:**
- .claude/CLAUDE.md
- .claude/scripts/update-claude-md.sh
- TODO
- examples/MultiDevice/src/main.cpp
### Session: 2026-02-12 18:02
**Modified files:**
- .claude/CLAUDE.md
- .claude/scripts/update-claude-md.sh
- TODO
- examples/MultiDevice/src/main.cpp
- library.json
### Session: 2026-02-12 18:06
**Commits:**
```
5a210fb Experimenting with a claude file and created new logging example
```
**Modified files:**
- .claude/CLAUDE.md
- TODO
- examples/Logger/platformio.ini
- examples/Logger/src/main.cpp
- examples/MultiDevice/src/main.cpp
- library.json
### Session: 2026-02-12 18:08
**Commits:**
```
5a210fb Experimenting with a claude file and created new logging example
```
**Modified files:**
- .claude/CLAUDE.md
- README.md
- TODO
- VERSIONS
- examples/Logger/platformio.ini
- examples/Logger/src/main.cpp
- examples/MultiDevice/src/main.cpp
- library.json
- library.properties
### Session: 2026-02-12 18:10
**Commits:**
```
5a210fb Experimenting with a claude file and created new logging example
```
**Modified files:**
- .claude/CLAUDE.md
- README.md
- TODO
- VERSIONS
- examples/Logger/platformio.ini
- examples/Logger/src/main.cpp
- examples/MultiDevice/src/main.cpp
- library.json
- library.properties

View File

@@ -0,0 +1,31 @@
#!/bin/bash
# Auto-update CLAUDE.md at end of session
CLAUDE_MD="$(git rev-parse --show-toplevel)/.claude/CLAUDE.md"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M')
# Get recent git activity from this session (last hour)
RECENT_COMMITS=$(git log --oneline --since="1 hour ago" 2>/dev/null | head -5)
MODIFIED_FILES=$(git diff --name-only HEAD~1 2>/dev/null | head -10)
# Append session summary
{
echo ""
echo "### Session: $TIMESTAMP"
if [ -n "$RECENT_COMMITS" ]; then
echo "**Commits:**"
echo "\`\`\`"
echo "$RECENT_COMMITS"
echo "\`\`\`"
fi
if [ -n "$MODIFIED_FILES" ]; then
echo "**Modified files:**"
echo "$MODIFIED_FILES" | sed 's/^/- /'
fi
echo ""
} >> "$CLAUDE_MD"
echo "Updated CLAUDE.md with session summary"

2
.gitignore vendored
View File

@@ -66,3 +66,5 @@ __pycache__/
.Python .Python
venv/ venv/
env/ env/
*.tar.gz

View File

@@ -2,14 +2,17 @@
ESP32 library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements. ESP32 library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements.
**⚠️ INITIAL RELEASE - NOT YET TESTED ON HARDWARE** **⚠️ INITIAL RELEASE - LIMITED TESTING DONE**
This is an initial release (v0.1.1) and has not yet been tested with real Victron devices. Use with caution and please report any issues you encounter. Testing and feedback are greatly appreciated! 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!
--- ---
Why another library? Most of the Victron BLE examples are built into other frameworks (e.g. ESPHome) and I want a library that can be used in all ESP32 systems, including ESPHome or other frameworks. With long term plan to try and move others to this library and improve code with many eyes. Why another library? Most of the Victron BLE examples are built into other frameworks (e.g. ESPHome) and I want a library that can be used in all ESP32 systems, including ESPHome or other frameworks. With long term plan to try and move others to this library and improve code with many eyes.
Currently supportin ESP32 S and C series (tested on older ESP32, and ESP32-S3 and ESP32-C3). Other chipsets can be added with abstraction of Bluetooth code.
## Features ## Features
-**Multiple Device Support**: Monitor multiple Victron devices simultaneously -**Multiple Device Support**: Monitor multiple Victron devices simultaneously

31
TODO Normal file
View File

@@ -0,0 +1,31 @@
# Misc Stuff
* Consider support for upper/lower case MAC address and optionaly ":"
* Scanning - list devices publishing, should be able to get list even without knowing MAC / Encryption key
* Struct vs Manual
* Sh3dNg version and examples uses structs to get data - seems to work
* Example generated uses manually managing a string
* Reconsider what is best and use
# Debugging
Use standard ESP32 debugging stuff. Handler should not be string but more like printf? Consider alternatives
Make sure debugging can be to file or serial etc
# Decrypting
Review 2 methods of decrypting and check it is working correctly
Seems mbedTLS is better choice as auotmatic hardware support even on ESP32 - where as the esp_aes version is not portable.
# Examples
* Multiple threads - scan BLE in the background
* Example scan anything
* With and without Callback
* Platformio.ini files into example data
# Logging and Debugging
* The debugging is very verbose and hard to read - maybe group the messages together for repeats

View File

@@ -1,5 +1,12 @@
# Version History # Version History
## 0.3.1 (2026-02-11)
### Changes
- Added Logger example: change-detection logging for Solar Charger data
- Added message type counters to MultiDevice example
- Tested with MPPT Solar Chargers on ESP32-S3 and ESP32-C3
## 0.1.1 (2025-12-18) ## 0.1.1 (2025-12-18)
Initial release - not yet tested on hardware. Initial release - not yet tested on hardware.

View File

@@ -0,0 +1,143 @@
[env]
lib_extra_dirs = ../..
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
; Serial monitor settings
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
; Build flags
build_flags =
-DCORE_DEBUG_LEVEL=3
; Library dependencies
lib_deps =
; VictronBLE library will be automatically included from parent directory
; Optional: Specify partition scheme if needed
; board_build.partitions = default.csv
[env:esp32-s3]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
build_flags =
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
# -DCORE_DEBUG_LEVEL=3
[env:esp32-s3-debug]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
#monitor_speed = 115200
#monitor_filters = esp32_exception_decoder
upload_protocol = esp-builtin
; Debug configuration for GDB
debug_tool = esp-builtin
debug_init_break = tbreak setup
debug_speed = 5000
debug_load_mode = always
; Build flags for debugging
build_flags =
-DCORE_DEBUG_LEVEL=5 ; Maximum ESP32 debug level
-O0 ; Disable optimization for debugging
-g3 ; Maximum debug information
build_type = debug
[env:esp32-c3]
platform = espressif32
framework = arduino
board = esp32-c3-devkitm-1
board_build.mcu = esp32c3
board_build.f_cpu = 160000000L
board_build.flash_mode = dio
board_build.partitions = default.csv
monitor_speed = 115200
monitor_filters = time, default, esp32_exception_decoder
upload_speed = 921600
# NOTE: Need these two ARDUIO_USB modes to work with serial
build_flags =
-Os
-I src
-D ARDUINO_ESP32C3_DEV
-D CONFIG_IDF_TARGET_ESP32C3
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
lib_deps =
elapsedMillis
[env:esp32-c3-debug]
platform = espressif32
board = esp32-c3-devkitc-02
framework = arduino
monitor_speed = 115200
; Upload configuration
upload_protocol = esp-builtin
; Debug configuration for GDB
debug_tool = esp-builtin
debug_init_break = tbreak setup
debug_speed = 5000
debug_load_mode = always
; Build flags for debugging
build_flags =
-DCORE_DEBUG_LEVEL=5 ; Maximum ESP32 debug level
-O0 ; Disable optimization for debugging
-g3 ; Maximum debug information
build_type = debug
[env:m5stick]
platform = espressif32
board = m5stick-c
framework = arduino
board_build.mcu = esp32
board_build.f_cpu = 240000000L
board_build.partitions = no_ota.csv
#upload_protocol = espota
#upload_port = Button.local
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
#debug_tool = esp-prog ; esp-bridge, esp-prog ; or ftdi, esp-builtin, jlink, etc.
# debug_speed = 5000 ; optional: JTAG speed in kHz
#build_flags =
# -DCORE_DEBUG_LEVEL=5 ; ESP32 debug level
# -O0 ; no optimization
# -g3 ; max debug info
build_flags =
-Os
lib_deps =
M5StickC
elapsedMillis
[env:tough]
board = m5stack-core2
board_build.mcu = esp32
platform = espressif32
framework = arduino
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
debug_tool = esp-bridge ; esp-bridge, esp-prog ; or ftdi, esp-builtin, jlink, etc.
# debug_speed = 5000 ; optional: JTAG speed in kHz
build_flags =
-DCORE_DEBUG_LEVEL=5 ; ESP32 debug level
-O0 ; no optimization
-g3 ; max debug info
-DARDUINO_M5STACK_TOUGH
-DDISPLAY_WIDTH=320
-DDISPLAY_HEIGHT=240
-DHAS_TOUCH=1
-DBUFFER_LINES=10
lib_deps =
M5Unified
elapsedMillis

View File

@@ -0,0 +1,167 @@
/**
* VictronBLE Logger Example
*
* 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.
*
* Setup:
* 1. Get your device encryption keys from the VictronConnect app
* 2. Update the device configurations below with your MAC and key
*/
#include <Arduino.h>
#include "VictronBLE.h"
VictronBLE victron;
// Tracks last-logged values per device for change detection
struct SolarChargerSnapshot {
bool valid = false;
SolarChargerState chargeState;
float batteryVoltage;
float batteryCurrent;
float panelVoltage;
float panelPower;
uint16_t yieldToday;
float loadCurrent;
unsigned long lastLogTime = 0;
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 SolarChargerSnapshot snapshots[MAX_DEVICES];
static int deviceCount = 0;
static const unsigned long LOG_INTERVAL_MS = 60000; // 1 minute
static int findOrAddDevice(const String& mac) {
for (int i = 0; i < deviceCount; i++) {
if (deviceMACs[i] == mac) return i;
}
if (deviceCount < MAX_DEVICES) {
deviceMACs[deviceCount] = mac;
return deviceCount++;
}
return -1;
}
static String chargeStateName(SolarChargerState state) {
switch (state) {
case CHARGER_OFF: return "Off";
case CHARGER_LOW_POWER: return "Low Power";
case CHARGER_FAULT: return "Fault";
case CHARGER_BULK: return "Bulk";
case CHARGER_ABSORPTION: return "Absorption";
case CHARGER_FLOAT: return "Float";
case CHARGER_STORAGE: return "Storage";
case CHARGER_EQUALIZE: return "Equalize";
case CHARGER_INVERTING: return "Inverting";
case CHARGER_POWER_SUPPLY: return "Power Supply";
case CHARGER_EXTERNAL_CONTROL: return "External Control";
default: return "Unknown";
}
}
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" : ""));
}
class LoggerCallback : public VictronDeviceCallback {
public:
void onSolarChargerData(const SolarChargerData& data) override {
int idx = findOrAddDevice(data.macAddress);
if (idx < 0) return;
SolarChargerSnapshot& prev = snapshots[idx];
unsigned long now = millis();
prev.packetsSinceLastLog++;
if (!prev.valid) {
// First reading - always log
logData(data, "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;
if (changed) {
logData(data, "CHG", prev.packetsSinceLastLog);
} else if (now - prev.lastLogTime >= LOG_INTERVAL_MS) {
logData(data, "HEARTBEAT", prev.packetsSinceLastLog);
} else {
return; // Nothing to log
}
}
// 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.lastLogTime = now;
}
};
LoggerCallback callback;
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n=== VictronBLE Logger Example ===\n");
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);
// Add your devices here
victron.addDevice(
"Rainbow48V",
"E4:05:42:34:14:F3",
"0ec3adf7433dd61793ff2f3b8ad32ed8",
DEVICE_TYPE_SOLAR_CHARGER
);
victron.addDevice(
"ScottTrailer",
"e64559783cfb",
"3fa658aded4f309b9bc17a2318cb1f56",
DEVICE_TYPE_SOLAR_CHARGER
);
Serial.println("Configured " + String(victron.getDeviceCount()) + " devices");
Serial.println("Logging on change, or every 60s heartbeat\n");
}
void loop() {
victron.loop();
delay(100);
}

View File

@@ -26,13 +26,118 @@ platform = espressif32
board = esp32-s3-devkitc-1 board = esp32-s3-devkitc-1
framework = arduino framework = arduino
monitor_speed = 115200 monitor_speed = 115200
monitor_filters = esp32_exception_decoder
build_flags = build_flags =
-DCORE_DEBUG_LEVEL=3 -D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
# -DCORE_DEBUG_LEVEL=3
[env:esp32-s3-debug]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
#monitor_speed = 115200
#monitor_filters = esp32_exception_decoder
upload_protocol = esp-builtin
; Debug configuration for GDB
debug_tool = esp-builtin
debug_init_break = tbreak setup
debug_speed = 5000
debug_load_mode = always
; Build flags for debugging
build_flags =
-DCORE_DEBUG_LEVEL=5 ; Maximum ESP32 debug level
-O0 ; Disable optimization for debugging
-g3 ; Maximum debug information
build_type = debug
[env:esp32-c3] [env:esp32-c3]
platform = espressif32 platform = espressif32
framework = arduino
board = esp32-c3-devkitm-1
board_build.mcu = esp32c3
board_build.f_cpu = 160000000L
board_build.flash_mode = dio
board_build.partitions = default.csv
monitor_speed = 115200
monitor_filters = time, default, esp32_exception_decoder
upload_speed = 921600
# NOTE: Need these two ARDUIO_USB modes to work with serial
build_flags =
-Os
-I src
-D ARDUINO_ESP32C3_DEV
-D CONFIG_IDF_TARGET_ESP32C3
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
lib_deps =
elapsedMillis
[env:esp32-c3-debug]
platform = espressif32
board = esp32-c3-devkitc-02 board = esp32-c3-devkitc-02
framework = arduino framework = arduino
monitor_speed = 115200 monitor_speed = 115200
; Upload configuration
upload_protocol = esp-builtin
; Debug configuration for GDB
debug_tool = esp-builtin
debug_init_break = tbreak setup
debug_speed = 5000
debug_load_mode = always
; Build flags for debugging
build_flags = build_flags =
-DCORE_DEBUG_LEVEL=3 -DCORE_DEBUG_LEVEL=5 ; Maximum ESP32 debug level
-O0 ; Disable optimization for debugging
-g3 ; Maximum debug information
build_type = debug
[env:m5stick]
platform = espressif32
board = m5stick-c
framework = arduino
board_build.mcu = esp32
board_build.f_cpu = 240000000L
board_build.partitions = no_ota.csv
#upload_protocol = espota
#upload_port = Button.local
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
#debug_tool = esp-prog ; esp-bridge, esp-prog ; or ftdi, esp-builtin, jlink, etc.
# debug_speed = 5000 ; optional: JTAG speed in kHz
#build_flags =
# -DCORE_DEBUG_LEVEL=5 ; ESP32 debug level
# -O0 ; no optimization
# -g3 ; max debug info
build_flags =
-Os
lib_deps =
M5StickC
elapsedMillis
[env:tough]
board = m5stack-core2
board_build.mcu = esp32
platform = espressif32
framework = arduino
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
debug_tool = esp-bridge ; esp-bridge, esp-prog ; or ftdi, esp-builtin, jlink, etc.
# debug_speed = 5000 ; optional: JTAG speed in kHz
build_flags =
-DCORE_DEBUG_LEVEL=5 ; ESP32 debug level
-O0 ; no optimization
-g3 ; max debug info
-DARDUINO_M5STACK_TOUGH
-DDISPLAY_WIDTH=320
-DDISPLAY_HEIGHT=240
-DHAS_TOUCH=1
-DBUFFER_LINES=10
lib_deps =
M5Unified
elapsedMillis

View File

@@ -1,13 +1,13 @@
/** /**
* VictronBLE Example * VictronBLE Example
* *
* This example demonstrates how to use the VictronBLE library to read data * This example demonstrates how to use the VictronBLE library to read data
* from multiple Victron devices simultaneously. * from multiple Victron devices simultaneously.
* *
* Hardware Requirements: * Hardware Requirements:
* - ESP32 board * - ESP32 board
* - Victron devices with BLE (SmartSolar, SmartShunt, etc.) * - Victron devices with BLE (SmartSolar, SmartShunt, etc.)
* *
* Setup: * Setup:
* 1. Get your device encryption keys from the VictronConnect app: * 1. Get your device encryption keys from the VictronConnect app:
* - Open VictronConnect * - Open VictronConnect
@@ -16,7 +16,7 @@
* - Enable "Instant readout via Bluetooth" * - Enable "Instant readout via Bluetooth"
* - Click "Show" next to "Instant readout details" * - Click "Show" next to "Instant readout details"
* - Copy the encryption key (32 hex characters) * - Copy the encryption key (32 hex characters)
* *
* 2. Update the device configurations below with your devices' MAC addresses * 2. Update the device configurations below with your devices' MAC addresses
* and encryption keys * and encryption keys
*/ */
@@ -30,8 +30,14 @@ VictronBLE victron;
// Device callback class - gets called when new data arrives // Device callback class - gets called when new data arrives
class MyVictronCallback : public VictronDeviceCallback { class MyVictronCallback : public VictronDeviceCallback {
public: public:
uint32_t solarChargerCount = 0;
uint32_t batteryMonitorCount = 0;
uint32_t inverterCount = 0;
uint32_t dcdcConverterCount = 0;
void onSolarChargerData(const SolarChargerData& data) override { void onSolarChargerData(const SolarChargerData& data) override {
Serial.println("\n=== Solar Charger: " + data.deviceName + " ==="); solarChargerCount++;
Serial.println("\n=== Solar Charger: " + data.deviceName + " (#" + String(solarChargerCount) + ") ===");
Serial.println("MAC: " + data.macAddress); Serial.println("MAC: " + data.macAddress);
Serial.println("RSSI: " + String(data.rssi) + " dBm"); Serial.println("RSSI: " + String(data.rssi) + " dBm");
Serial.println("State: " + getChargeStateName(data.chargeState)); Serial.println("State: " + getChargeStateName(data.chargeState));
@@ -45,29 +51,30 @@ public:
} }
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago"); Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
} }
void onBatteryMonitorData(const BatteryMonitorData& data) override { void onBatteryMonitorData(const BatteryMonitorData& data) override {
Serial.println("\n=== Battery Monitor: " + data.deviceName + " ==="); batteryMonitorCount++;
Serial.println("\n=== Battery Monitor: " + data.deviceName + " (#" + String(batteryMonitorCount) + ") ===");
Serial.println("MAC: " + data.macAddress); Serial.println("MAC: " + data.macAddress);
Serial.println("RSSI: " + String(data.rssi) + " dBm"); Serial.println("RSSI: " + String(data.rssi) + " dBm");
Serial.println("Voltage: " + String(data.voltage, 2) + " V"); Serial.println("Voltage: " + String(data.voltage, 2) + " V");
Serial.println("Current: " + String(data.current, 2) + " A"); Serial.println("Current: " + String(data.current, 2) + " A");
Serial.println("SOC: " + String(data.soc, 1) + " %"); Serial.println("SOC: " + String(data.soc, 1) + " %");
Serial.println("Consumed: " + String(data.consumedAh, 2) + " Ah"); Serial.println("Consumed: " + String(data.consumedAh, 2) + " Ah");
if (data.remainingMinutes < 65535) { if (data.remainingMinutes < 65535) {
int hours = data.remainingMinutes / 60; int hours = data.remainingMinutes / 60;
int mins = data.remainingMinutes % 60; int mins = data.remainingMinutes % 60;
Serial.println("Time Remaining: " + String(hours) + "h " + String(mins) + "m"); Serial.println("Time Remaining: " + String(hours) + "h " + String(mins) + "m");
} }
if (data.temperature > 0) { if (data.temperature > 0) {
Serial.println("Temperature: " + String(data.temperature, 1) + " °C"); Serial.println("Temperature: " + String(data.temperature, 1) + " °C");
} }
if (data.auxVoltage > 0) { if (data.auxVoltage > 0) {
Serial.println("Aux Voltage: " + String(data.auxVoltage, 2) + " V"); Serial.println("Aux Voltage: " + String(data.auxVoltage, 2) + " V");
} }
// Print alarms // Print alarms
if (data.alarmLowVoltage || data.alarmHighVoltage || data.alarmLowSOC || if (data.alarmLowVoltage || data.alarmHighVoltage || data.alarmLowSOC ||
data.alarmLowTemperature || data.alarmHighTemperature) { data.alarmLowTemperature || data.alarmHighTemperature) {
@@ -79,21 +86,22 @@ public:
if (data.alarmHighTemperature) Serial.print("HIGH-TEMP "); if (data.alarmHighTemperature) Serial.print("HIGH-TEMP ");
Serial.println(); Serial.println();
} }
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago"); Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
} }
void onInverterData(const InverterData& data) override { void onInverterData(const InverterData& data) override {
Serial.println("\n=== Inverter/Charger: " + data.deviceName + " ==="); inverterCount++;
Serial.println("\n=== Inverter/Charger: " + data.deviceName + " (#" + String(inverterCount) + ") ===");
Serial.println("MAC: " + data.macAddress); Serial.println("MAC: " + data.macAddress);
Serial.println("RSSI: " + String(data.rssi) + " dBm"); Serial.println("RSSI: " + String(data.rssi) + " dBm");
Serial.println("Battery: " + String(data.batteryVoltage, 2) + " V"); Serial.println("Battery: " + String(data.batteryVoltage, 2) + " V");
Serial.println("Current: " + String(data.batteryCurrent, 2) + " A"); Serial.println("Current: " + String(data.batteryCurrent, 2) + " A");
Serial.println("AC Power: " + String(data.acPower) + " W"); Serial.println("AC Power: " + String(data.acPower) + " W");
Serial.println("State: " + String(data.state)); Serial.println("State: " + String(data.state));
// Print alarms // Print alarms
if (data.alarmLowVoltage || data.alarmHighVoltage || if (data.alarmLowVoltage || data.alarmHighVoltage ||
data.alarmHighTemperature || data.alarmOverload) { data.alarmHighTemperature || data.alarmOverload) {
Serial.print("ALARMS: "); Serial.print("ALARMS: ");
if (data.alarmLowVoltage) Serial.print("LOW-V "); if (data.alarmLowVoltage) Serial.print("LOW-V ");
@@ -102,12 +110,13 @@ public:
if (data.alarmOverload) Serial.print("OVERLOAD "); if (data.alarmOverload) Serial.print("OVERLOAD ");
Serial.println(); Serial.println();
} }
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago"); Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
} }
void onDCDCConverterData(const DCDCConverterData& data) override { void onDCDCConverterData(const DCDCConverterData& data) override {
Serial.println("\n=== DC-DC Converter: " + data.deviceName + " ==="); dcdcConverterCount++;
Serial.println("\n=== DC-DC Converter: " + data.deviceName + " (#" + String(dcdcConverterCount) + ") ===");
Serial.println("MAC: " + data.macAddress); Serial.println("MAC: " + data.macAddress);
Serial.println("RSSI: " + String(data.rssi) + " dBm"); Serial.println("RSSI: " + String(data.rssi) + " dBm");
Serial.println("Input: " + String(data.inputVoltage, 2) + " V"); Serial.println("Input: " + String(data.inputVoltage, 2) + " V");
@@ -119,7 +128,7 @@ public:
} }
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago"); Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
} }
private: private:
String getChargeStateName(SolarChargerState state) { String getChargeStateName(SolarChargerState state) {
switch (state) { switch (state) {
@@ -144,59 +153,85 @@ MyVictronCallback callback;
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
delay(1000); delay(1000);
Serial.println("\n\n================================="); Serial.println("\n\n=================================");
Serial.println("VictronBLE Multi-Device Example"); Serial.println("VictronBLE Multi-Device Example");
Serial.println("=================================\n"); Serial.println("=================================\n");
// Initialize VictronBLE with 5 second scan duration // Initialize VictronBLE with 5 second scan duration
if (!victron.begin(5)) { if (!victron.begin(5)) {
Serial.println("ERROR: Failed to initialize VictronBLE!"); Serial.println("ERROR: Failed to initialize VictronBLE!");
Serial.println(victron.getLastError()); Serial.println(victron.getLastError());
while (1) delay(1000); while (1) delay(1000);
} }
// Enable debug output (optional) // Enable debug output (optional)
victron.setDebug(true); victron.setDebug(false);
// Set callback for data updates // Set callback for data updates
victron.setCallback(&callback); victron.setCallback(&callback);
// Add your devices here // Add your devices here
// Replace with your actual MAC addresses and encryption keys // Replace with your actual MAC addresses and encryption keys
// CORRECT in Alternative
// Rainbow48V at MAC e4:05:42:34:14:f3
// Temporary - Scott Example
victron.addDevice(
"Rainbow48V", // Device name
"E4:05:42:34:14:F3", // MAC address
"0ec3adf7433dd61793ff2f3b8ad32ed8", // Encryption key (32 hex chars)
DEVICE_TYPE_SOLAR_CHARGER // Device type
);
victron.addDevice(
"ScottTrailer", // Device name
"e64559783cfb",
"3fa658aded4f309b9bc17a2318cb1f56",
DEVICE_TYPE_SOLAR_CHARGER // Device type
);
// Example: Solar Charger #1 // Example: Solar Charger #1
/*
victron.addDevice( victron.addDevice(
"MPPT 100/30", // Device name "MPPT 100/30", // Device name
"E7:48:D4:28:B7:9C", // MAC address "E7:48:D4:28:B7:9C", // MAC address
"0df4d0395b7d1a876c0c33ecb9e70dcd", // Encryption key (32 hex chars) "0df4d0395b7d1a876c0c33ecb9e70dcd", // Encryption key (32 hex chars)
DEVICE_TYPE_SOLAR_CHARGER // Device type DEVICE_TYPE_SOLAR_CHARGER // Device type
); );
*/
// Example: Solar Charger #2 // Example: Solar Charger #2
/*
victron.addDevice( victron.addDevice(
"MPPT 75/15", "MPPT 75/15",
"AA:BB:CC:DD:EE:FF", "AA:BB:CC:DD:EE:FF",
"1234567890abcdef1234567890abcdef", "1234567890abcdef1234567890abcdef",
DEVICE_TYPE_SOLAR_CHARGER DEVICE_TYPE_SOLAR_CHARGER
); );
*/
// Example: Battery Monitor (SmartShunt) // Example: Battery Monitor (SmartShunt)
/*
victron.addDevice( victron.addDevice(
"SmartShunt", "SmartShunt",
"11:22:33:44:55:66", "11:22:33:44:55:66",
"fedcba0987654321fedcba0987654321", "fedcba0987654321fedcba0987654321",
DEVICE_TYPE_BATTERY_MONITOR DEVICE_TYPE_BATTERY_MONITOR
); );
*/
// Example: Inverter/Charger // Example: Inverter/Charger
/*
victron.addDevice( victron.addDevice(
"MultiPlus", "MultiPlus",
"99:88:77:66:55:44", "99:88:77:66:55:44",
"abcdefabcdefabcdefabcdefabcdefab", "abcdefabcdefabcdefabcdefabcdefab",
DEVICE_TYPE_INVERTER DEVICE_TYPE_INVERTER
); );
*/
Serial.println("Configured " + String(victron.getDeviceCount()) + " devices"); Serial.println("Configured " + String(victron.getDeviceCount()) + " devices");
Serial.println("\nStarting BLE scan...\n"); Serial.println("\nStarting BLE scan...\n");
} }
@@ -204,7 +239,7 @@ void setup() {
void loop() { void loop() {
// Process BLE scanning and data updates // Process BLE scanning and data updates
victron.loop(); victron.loop();
// Optional: You can also manually query device data // Optional: You can also manually query device data
// This is useful if you're not using callbacks // This is useful if you're not using callbacks
/* /*
@@ -212,13 +247,13 @@ void loop() {
if (victron.getSolarChargerData("E7:48:D4:28:B7:9C", solarData)) { if (victron.getSolarChargerData("E7:48:D4:28:B7:9C", solarData)) {
// Do something with solarData // Do something with solarData
} }
BatteryMonitorData batteryData; BatteryMonitorData batteryData;
if (victron.getBatteryMonitorData("11:22:33:44:55:66", batteryData)) { if (victron.getBatteryMonitorData("11:22:33:44:55:66", batteryData)) {
// Do something with batteryData // Do something with batteryData
} }
*/ */
// Add a small delay to avoid overwhelming the serial output // Add a small delay to avoid overwhelming the serial output
delay(100); delay(100);
} }

35
experiment/platformio.ini Normal file
View File

@@ -0,0 +1,35 @@
[env]
lib_extra_dirs = ..
[env:esp32-s3]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
build_flags =
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
# -DCORE_DEBUG_LEVEL=3
[env:esp32-c3]
platform = espressif32
framework = arduino
board = esp32-c3-devkitm-1
board_build.mcu = esp32c3
board_build.f_cpu = 160000000L
board_build.flash_mode = dio
board_build.partitions = default.csv
monitor_speed = 115200
monitor_filters = time, default, esp32_exception_decoder
upload_speed = 921600
# NOTE: Need these two ARDUIO_USB modes to work with serial
build_flags =
-Os
-I src
-D ARDUINO_ESP32C3_DEV
-D CONFIG_IDF_TARGET_ESP32C3
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
lib_deps =
elapsedMillis

397
experiment/src/main.cpp Normal file
View File

@@ -0,0 +1,397 @@
/*
Scott's original test code - this does work for MPPT chargers - use it as a base
*/
#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEAdvertisedDevice.h>
#include <BLEScan.h>
#include <aes/esp_aes.h> // AES decryption
typedef struct {
char charMacAddr[13]; // 12 character MAC + \0 (initialized as quoted strings below for convenience)
char charKey[33]; // 32 character keys + \0 (initialized as quoted strings below for convenience)
char comment[16]; // 16 character comment (name) for printing during setup()
byte byteMacAddr[6]; // 6 bytes for MAC - initialized by setup() from quoted strings
byte byteKey[16]; // 16 bytes for encryption key - initialized by setup() from quoted strings
char cachedDeviceName[32]; // 31 characters + \0 (filled in as we receive advertisements)
} solarController;
solarController solarControllers[] = {
// { { .charMacAddr = "e64559783cfb" }, { .charKey = "3fa658aded4f309b9bc17a2318cb1f56" }, { .comment = "ScottTrailer" } },
{ { .charMacAddr = "e405423414f3" }, { .charKey = "0ec3adf7433dd61793ff2f3b8ad32ed8" }, { .comment = "Test" } },
};
int knownSolarControllerCount = sizeof(solarControllers) / sizeof(solarControllers[0]);
BLEScan *pBLEScan;
#define AES_KEY_BITS 128
int scanTime = 1; //In seconds
byte hexCharToByte(char hexChar) {
if (hexChar >= '0' && hexChar <='9') { // 0-9
hexChar=hexChar - '0';
}
else if (hexChar >= 'a' && hexChar <= 'f') { // a-f
hexChar=hexChar - 'a' + 10;
}
else if (hexChar >= 'A' && hexChar <= 'F') { // A-F
hexChar=hexChar - 'A' + 10;
}
else {
hexChar=255;
}
return hexChar;
}
// Decryption keys and MAC addresses obtained from the VictronConnect app will be
// a string of hex digits like this:
//
// f4116784732a
// dc73cb155351cf950f9f3a958b5cd96f
//
// Split that up and turn it into an array whose equivalent definition would be like this:
//
// byte key[]={ 0xdc, 0x73, 0xcb, ... 0xd9, 0x6f };
//
void hexCharStrToByteArray(char * hexCharStr, byte * byteArray) {
bool returnVal=false;
int hexCharStrLength=strlen(hexCharStr);
// There are simpler ways of doing this without the fancy nibble-munching,
// but I do it this way so I parse things like colon-separated MAC addresses.
// BUT: be aware that this expects digits in pairs and byte values need to be
// zero-filled. i.e., a MAC address like 8:0:2b:xx:xx:xx won't come out the way
// you want it.
int byteArrayIndex=0;
bool oddByte=true;
byte hiNibble;
for (int i=0; i<hexCharStrLength; i++) {
byte nibble=hexCharToByte(hexCharStr[i]);
if (nibble!=255) {
if (oddByte) {
hiNibble=nibble;
} else {
byteArray[byteArrayIndex]=(hiNibble<<4) | nibble;
byteArrayIndex++;
}
oddByte=!oddByte;
}
}
// do we have a leftover nibble? I guess we'll assume it's a low nibble?
if (! oddByte) {
byteArray[byteArrayIndex]=hiNibble;
}
}
// Victron docs on the manufacturer data in advertisement packets can be found at:
// https://community.victronenergy.com/storage/attachments/48745-extra-manufacturer-data-2022-12-14.pdf
//
// Usage/style note: I use uint16_t in places where I need to force 16-bit unsigned integers
// instead of whatever the compiler/architecture might decide to use. I might not need to do
// the same with byte variables, but I'll do it anyway just to be at least a little consistent.
// Must use the "packed" attribute to make sure the compiler doesn't add any padding to deal with
// word alignment.
typedef struct {
uint8_t deviceState;
uint8_t errorCode;
int16_t batteryVoltage;
int16_t batteryCurrent;
uint16_t todayYield;
uint16_t inputPower;
uint8_t outputCurrentLo; // Low 8 bits of output current (in 0.1 Amp increments)
uint8_t outputCurrentHi; // High 1 bit of ourput current (must mask off unused bits)
uint8_t unused[4];
} __attribute__((packed)) victronPanelData;
typedef struct {
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.
} __attribute__((packed)) victronManufacturerData;
int bestRSSI = -200;
int selectedSolarControllerIndex = -1;
time_t lastLEDBlinkTime=0;
time_t lastTick=0;
int displayRotation=3;
bool packetReceived=false;
char chargeStateNames[][6] = {
" off",
" 1?",
" 2?",
" bulk",
" abs",
"float",
" 6?",
"equal"
};
class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
#define manDataSizeMax 31 // BLE specs say no more than 31 bytes, but see comments below!
// See if we have manufacturer data and then look to see if it's coming from a Victron device.
if (advertisedDevice.haveManufacturerData() == true) {
uint8_t manCharBuf[manDataSizeMax+1];
std::string manData = advertisedDevice.getManufacturerData(); // lib code returns std::string
int manDataSize=manData.length(); // This does not include a null at the end.
Serial.printf("Manufacturer data lengt=%d\n", manData.length());
Serial.printf("Struct Size=%d\n", sizeof(victronManufacturerData));
// Limit size just in case we get a malformed packet.
if (manDataSize > manDataSizeMax) {
Serial.printf(" Note: Truncating malformed %2d byte manufacturer data to max %d byte array size\n",manDataSize,manDataSizeMax);
manDataSize=manDataSizeMax;
}
// Now copy the data from the String to a byte array. Must have the +1 so we
// don't lose the last character to the null terminator.
manData.copy((char *)manCharBuf, manDataSize + 1);
// Now let's use a struct to get to the data more cleanly.
victronManufacturerData * vicData=(victronManufacturerData *)manCharBuf;
// ignore this packet if the Vendor ID isn't Victron.
if (vicData->vendorID!=0x02e1) {
return;
}
// ignore this packet if it isn't type 0x01 (Solar Charger).
if (vicData->victronRecordType != 0x01) {
return;
}
// Get the MAC address of the device we're hearing, and then use that to look up the encryption key
// for the device.
//
// We go through a bit of trouble here to turn the String MAC address that we get from the BLE
// code ("08:00:2b:xx:xx:xx") into a byte array. I'm divided on this... I could have just (and still might!)
// left this as a string and just done a strcmp() match. This would have saved me some coding and execution time
// in exchange for having to format the MAC addresses in my solarControllers list using the embedded colons.
char receivedMacStr[18];
strcpy(receivedMacStr,advertisedDevice.getAddress().toString().c_str());
byte receivedMacByte[6];
hexCharStrToByteArray(receivedMacStr,receivedMacByte);
int solarControllerIndex=-1;
for (int trySolarControllerIndex=0; trySolarControllerIndex<knownSolarControllerCount; trySolarControllerIndex++) {
bool matchedMac=true;
for (int i=0; i<6; i++) {
if (receivedMacByte[i] != solarControllers[trySolarControllerIndex].byteMacAddr[i]) {
matchedMac=false;
break;
}
}
if (matchedMac) {
solarControllerIndex=trySolarControllerIndex;
break;
}
}
// Get the device name (if there's one in this packet).
char deviceName[32]; // 31 characters + \0
strcpy(deviceName,"(unknown device name)");
bool deviceNameFound=false;
if (advertisedDevice.haveName()) {
// This works the same whether getName() returns String or std::string.
strcpy(deviceName,advertisedDevice.getName().c_str());
// This is prone to breaking because it's not very sophisticated. It's meant to
// strip off "SmartSolar" if it's at the beginning of the name, but will do
// ugly things if someone has put it elsewhere like "My SmartSolar Charger".
if (strstr(deviceName,"SmartSolar ") == deviceName) {
strcpy(deviceName,deviceName+11);
}
deviceNameFound=true;
}
// We didn't do this test earlier because we want to print out a name - if we got one.
if (solarControllerIndex == -1) {
Serial.printf("Discarding packet from unconfigured Victron SmartSolar %s at MAC %s\n",deviceName,receivedMacStr);
return;
}
// If we found a device name, cache it for later display.
if (deviceNameFound) {
strcpy(solarControllers[solarControllerIndex].cachedDeviceName,deviceName);
}
// The manufacturer data from Victron contains a byte that's supposed to match the first byte
// of the device's encryption key. If they don't match, when we don't have the right key for
// this device and we just have to throw the data away. ALTERNATELY, we can go ahead and decrypt
// the data - incorrectly - and use the crazy values to indicate that we have a problem.
if (vicData->encryptKeyMatch != solarControllers[solarControllerIndex].byteKey[0]) {
Serial.printf("Encryption key mismatch for %s at MAC %s\n",
solarControllers[solarControllerIndex].cachedDeviceName,receivedMacStr);
return;
}
// Get the signal strength (RSSI) of the beacon.
int RSSI=advertisedDevice.getRSSI();
// If we're showing our data on our integrated graphics hardware,
// then show only the SmartSolar device with the strongest signal.
// I debated on whether to do this with "#if defined..." for conditional compilation
// or I should do this with a boolean "using graphics hardware" variable and a regular
// "if". I decided on #if, but I might change my mind later.
// Get the beacon's RSSI (signal strength). If it's stronger than other beacons we've received,
// then lock on to this SmartSolar and don't display beacons from others anymore.
if (selectedSolarControllerIndex==solarControllerIndex) {
if (RSSI > bestRSSI) {
bestRSSI=RSSI;
}
} else {
if (RSSI > bestRSSI) {
selectedSolarControllerIndex=solarControllerIndex;
Serial.printf("Selected Victon SmartSolar %s at MAC %s as preferred device based on RSSI %d\n",
solarControllers[solarControllerIndex].cachedDeviceName,receivedMacStr,RSSI);
} else {
Serial.printf("Discarding RSSI-based non-selected Victon SmartSolar %s at MAC %s\n",
solarControllers[solarControllerIndex].cachedDeviceName,receivedMacStr);
return;
}
}
// Now that the packet received has met all the criteria for being displayed,
// let's decrypt and decode the manufacturer data.
byte inputData[16];
byte outputData[16]={0};
victronPanelData * victronData = (victronPanelData *) outputData;
// The number of encrypted bytes is given by the number of bytes in the manufacturer
// data as a while minus the number of bytes (10) in the header part of the data.
int encrDataSize=manDataSize-10;
for (int i=0; i<encrDataSize; i++) {
inputData[i]=vicData->victronEncryptedData[i]; // copy for our decrypt below while I figure this out.
}
esp_aes_context ctx;
esp_aes_init(&ctx);
auto status = esp_aes_setkey(&ctx, solarControllers[solarControllerIndex].byteKey, AES_KEY_BITS);
if (status != 0) {
Serial.printf(" Error during esp_aes_setkey operation (%i).\n",status);
esp_aes_free(&ctx);
return;
}
byte data_counter_lsb=(vicData->nonceDataCounter) & 0xff;
byte data_counter_msb=((vicData->nonceDataCounter) >> 8) & 0xff;
u_int8_t nonce_counter[16] = {data_counter_lsb, data_counter_msb, 0};
u_int8_t stream_block[16] = {0};
size_t nonce_offset=0;
status = esp_aes_crypt_ctr(&ctx, encrDataSize, &nonce_offset, nonce_counter, stream_block, inputData, outputData);
if (status != 0) {
Serial.printf("Error during esp_aes_crypt_ctr operation (%i).",status);
esp_aes_free(&ctx);
return;
}
esp_aes_free(&ctx);
byte deviceState=victronData->deviceState; // this is really more like "Charger State"
byte errorCode=victronData->errorCode;
float batteryVoltage=float(victronData->batteryVoltage)*0.01;
float batteryCurrent=float(victronData->batteryCurrent)*0.1;
float todayYield=float(victronData->todayYield)*0.01*1000;
uint16_t inputPower=victronData->inputPower; // this is in watts; no conversion needed
// Getting the output current takes some magic.
int integerOutputCurrent=((victronData->outputCurrentHi & 0x01)<<9) | victronData->outputCurrentLo;
float outputCurrent=float(integerOutputCurrent)*0.1;
// I don't know why, but every so often we'll get half-corrupted data from the Victron.
// Towards the goal of filtering out this noise, I've found that I've rarely (if ever) seen
// corrupted data when the 'unused' bits of the outputCurrent MSB equal 0xfe. We'll use this
// as a litmus test here.
byte unusedBits=victronData->outputCurrentHi & 0xfe;
if (unusedBits != 0xfe) {
return;
}
// The Victron docs say Device State but it's really a Charger State.
char chargeStateName[6];
sprintf(chargeStateName,"%4d?",deviceState);
if (deviceState >=0 && deviceState<=7) {
strcpy(chargeStateName,chargeStateNames[deviceState]);
}
Serial.printf("%-31s Battery: %6.2f Volts %6.2f Amps Solar: %6d Watts Yield: %4.0f Wh Load: %5.1f Amps Charger: %-13s Err: %2d RSSI: %d\n",
solarControllers[solarControllerIndex].cachedDeviceName,
batteryVoltage, batteryCurrent,
inputPower, todayYield,
outputCurrent, chargeStateName, errorCode, RSSI
);
char screenDeviceName[14]; // 13 characters plus /0
strncpy(screenDeviceName,solarControllers[solarControllerIndex].cachedDeviceName,13);
screenDeviceName[13]='\0'; // make sure we have a null byte at the end.
/*
sh3dNbDisplay.line1 = String("Name: ") + String(screenDeviceName);
sh3dNbDisplay.line2 = String("Battery: ") + String(batteryVoltage) + String("V");
sh3dNbDisplay.line3 = String("Charge: ") + String(inputPower) + String("W ") + String(todayYield) + String("Wh");
sh3dNb.setMessage(sh3dNb.iso8601());
sh3dNbDisplay.update();
*/
packetReceived=true;
}
}
};
void setup() {
Serial.begin(115200);
Serial.println("Basic test");
// VICTRON BLUETOOTH
for (int i = 0; i < knownSolarControllerCount; i++) {
hexCharStrToByteArray(solarControllers[i].charMacAddr, solarControllers[i].byteMacAddr);
hexCharStrToByteArray(solarControllers[i].charKey, solarControllers[i].byteKey);
strcpy(solarControllers[i].cachedDeviceName, "(unknown)");
}
for (int i = 0; i < knownSolarControllerCount; i++) {
Serial.printf(" %-16s", solarControllers[i].comment);
Serial.printf(" Mac: ");
for (int j = 0; j < 6; j++) {
Serial.printf(" %2.2x", solarControllers[i].byteMacAddr[j]);
}
Serial.printf(" Key: ");
for (int j = 0; j < 16; j++) {
Serial.printf("%2.2x", solarControllers[i].byteKey[j]);
}
}
BLEDevice::init("");
pBLEScan = BLEDevice::getScan(); //create new scan
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setActiveScan(true); //active scan uses more power, but gets results faster
pBLEScan->setInterval(100);
pBLEScan->setWindow(99); // less or equal setInterval value
}
void loop() {
Serial.println("tick");
BLEScanResults foundDevices = pBLEScan->start(scanTime, false);
pBLEScan->clearResults(); // delete results fromBLEScan buffer to release memory
delay(100);
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "VictronBLE", "name": "victronble",
"version": "0.1.2", "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.", "description": "ESP32 library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements. Supports SmartSolar MPPT, SmartShunt, BMV, MultiPlus, Orion and other Victron devices.",
"keywords": "victron, ble, bluetooth, solar, mppt, battery, smartshunt, smartsolar, bmv, inverter, multiplus, esp32, iot, energy, monitoring", "keywords": "victron, ble, bluetooth, solar, mppt, battery, smartshunt, smartsolar, bmv, inverter, multiplus, esp32, iot, energy, monitoring",
"repository": { "repository": {
@@ -26,6 +26,11 @@
"name": "MultiDevice", "name": "MultiDevice",
"base": "examples/MultiDevice", "base": "examples/MultiDevice",
"files": ["src/main.cpp"] "files": ["src/main.cpp"]
},
{
"name": "Logger",
"base": "examples/Logger",
"files": ["src/main.cpp"]
} }
], ],
"export": { "export": {

View File

@@ -1,5 +1,5 @@
name=VictronBLE name=VictronBLE
version=0.1.1 version=0.3.1
author=Scott Penrose author=Scott Penrose
maintainer=Scott Penrose <scottp@dd.com.au> maintainer=Scott Penrose <scottp@dd.com.au>
sentence=ESP32 library for reading Victron Energy device data via BLE for any ESP32 sentence=ESP32 library for reading Victron Energy device data via BLE for any ESP32

View File

@@ -1,7 +1,7 @@
/** /**
* VictronBLE - ESP32 library for Victron Energy BLE devices * VictronBLE - ESP32 library for Victron Energy BLE devices
* Implementation file * Implementation file
* *
* Copyright (c) 2025 Scott Penrose * Copyright (c) 2025 Scott Penrose
* License: MIT * License: MIT
*/ */
@@ -9,8 +9,8 @@
#include "VictronBLE.h" #include "VictronBLE.h"
// Constructor // Constructor
VictronBLE::VictronBLE() VictronBLE::VictronBLE()
: pBLEScan(nullptr), callback(nullptr), debugEnabled(false), : pBLEScan(nullptr), callback(nullptr), debugEnabled(false),
scanDuration(5), initialized(false) { scanDuration(5), initialized(false) {
} }
@@ -20,7 +20,7 @@ VictronBLE::~VictronBLE() {
delete pair.second; delete pair.second;
} }
devices.clear(); devices.clear();
if (pBLEScan) { if (pBLEScan) {
pBLEScan->stop(); pBLEScan->stop();
} }
@@ -32,27 +32,28 @@ bool VictronBLE::begin(uint32_t scanDuration) {
debugPrint("VictronBLE already initialized"); debugPrint("VictronBLE already initialized");
return true; return true;
} }
this->scanDuration = scanDuration; this->scanDuration = scanDuration;
debugPrint("Initializing VictronBLE..."); debugPrint("Initializing VictronBLE...");
BLEDevice::init("VictronBLE"); BLEDevice::init("VictronBLE");
pBLEScan = BLEDevice::getScan(); pBLEScan = BLEDevice::getScan();
if (!pBLEScan) { if (!pBLEScan) {
lastError = "Failed to create BLE scanner"; lastError = "Failed to create BLE scanner";
debugPrint(lastError);
return false; return false;
} }
pBLEScan->setAdvertisedDeviceCallbacks(new VictronBLEAdvertisedDeviceCallbacks(this), true); pBLEScan->setAdvertisedDeviceCallbacks(new VictronBLEAdvertisedDeviceCallbacks(this), true);
pBLEScan->setActiveScan(false); // Passive scan - lower power pBLEScan->setActiveScan(false); // Passive scan - lower power
pBLEScan->setInterval(100); pBLEScan->setInterval(100);
pBLEScan->setWindow(99); pBLEScan->setWindow(99);
initialized = true; initialized = true;
debugPrint("VictronBLE initialized successfully"); debugPrint("VictronBLE initialized successfully");
return true; return true;
} }
@@ -60,44 +61,51 @@ bool VictronBLE::begin(uint32_t scanDuration) {
bool VictronBLE::addDevice(const VictronDeviceConfig& config) { bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
if (config.macAddress.length() == 0) { if (config.macAddress.length() == 0) {
lastError = "MAC address cannot be empty"; lastError = "MAC address cannot be empty";
debugPrint(lastError);
return false; return false;
} }
if (config.encryptionKey.length() != 32) { if (config.encryptionKey.length() != 32) {
lastError = "Encryption key must be 32 hex characters"; lastError = "Encryption key must be 32 hex characters";
debugPrint(lastError);
return false; return false;
} }
String normalizedMAC = normalizeMAC(config.macAddress); String normalizedMAC = normalizeMAC(config.macAddress);
// Check if device already exists // Check if device already exists
if (devices.find(normalizedMAC) != devices.end()) { if (devices.find(normalizedMAC) != devices.end()) {
debugPrint("Device " + normalizedMAC + " already exists, updating config"); debugPrint("Device " + normalizedMAC + " already exists, updating config");
delete devices[normalizedMAC]; delete devices[normalizedMAC];
} }
DeviceInfo* info = new DeviceInfo(); DeviceInfo* info = new DeviceInfo();
info->config = config; info->config = config;
info->config.macAddress = normalizedMAC; info->config.macAddress = normalizedMAC;
// Convert encryption key from hex string to bytes // Convert encryption key from hex string to bytes
if (!hexStringToBytes(config.encryptionKey, info->encryptionKeyBytes, 16)) { if (!hexStringToBytes(config.encryptionKey, info->encryptionKeyBytes, 16)) {
lastError = "Invalid encryption key format"; lastError = "Invalid encryption key format";
debugPrint(lastError);
delete info; delete info;
return false; return false;
} }
// Create appropriate data structure based on device type // Create appropriate data structure based on device type
info->data = createDeviceData(config.expectedType); info->data = createDeviceData(config.expectedType);
if (info->data) { if (info->data) {
info->data->macAddress = normalizedMAC; info->data->macAddress = normalizedMAC;
info->data->deviceName = config.name; info->data->deviceName = config.name;
} }
devices[normalizedMAC] = info; devices[normalizedMAC] = info;
debugPrint("Added device: " + config.name + " (" + normalizedMAC + ")"); debugPrint("Added device: " + config.name + " (MAC: " + normalizedMAC + ")");
if (debugEnabled) {
debugPrint(" Original MAC input: " + config.macAddress);
debugPrint(" Stored normalized: " + normalizedMAC);
}
return true; return true;
} }
@@ -110,7 +118,7 @@ bool VictronBLE::addDevice(String name, String macAddress, String encryptionKey,
// Remove a device // Remove a device
void VictronBLE::removeDevice(String macAddress) { void VictronBLE::removeDevice(String macAddress) {
String normalizedMAC = normalizeMAC(macAddress); String normalizedMAC = normalizeMAC(macAddress);
auto it = devices.find(normalizedMAC); auto it = devices.find(normalizedMAC);
if (it != devices.end()) { if (it != devices.end()) {
delete it->second; delete it->second;
@@ -124,7 +132,7 @@ void VictronBLE::loop() {
if (!initialized) { if (!initialized) {
return; return;
} }
// Start a scan // Start a scan
BLEScanResults scanResults = pBLEScan->start(scanDuration, false); BLEScanResults scanResults = pBLEScan->start(scanDuration, false);
pBLEScan->clearResults(); pBLEScan->clearResults();
@@ -139,37 +147,80 @@ void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertise
// Process advertised device // Process advertised device
void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) { void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) {
// Get MAC address from the advertised device
String mac = macAddressToString(advertisedDevice.getAddress()); String mac = macAddressToString(advertisedDevice.getAddress());
String normalizedMAC = normalizeMAC(mac); String normalizedMAC = normalizeMAC(mac);
if (debugEnabled) {
debugPrint("Raw MAC: " + mac + " -> Normalized: " + normalizedMAC);
}
// TODO: Consider skipping with no manufacturer data?
memset(&manufacturerData, 0, sizeof(manufacturerData));
if (advertisedDevice.haveManufacturerData()) {
std::string mfgData = advertisedDevice.getManufacturerData();
// XXX Storing it this way is not thread safe - is that issue on this ESP32?
debugPrint("Getting manufacturer data: Size=" + String(mfgData.length()));
mfgData.copy((char*)&manufacturerData, (mfgData.length() > sizeof(manufacturerData) ? sizeof(manufacturerData) : mfgData.length()));
}
// Pointer? XXX
// Debug: Log all discovered BLE devices
if (debugEnabled) {
String debugMsg = "";
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(manufacturerData.vendorID, HEX);
if (manufacturerData.vendorID == VICTRON_MANUFACTURER_ID) {
debugMsg += " (Victron)";
}
debugPrint(debugMsg);
}
// Check if this is one of our configured devices // Check if this is one of our configured devices
auto it = devices.find(normalizedMAC); auto it = devices.find(normalizedMAC);
if (it == devices.end()) { if (it == devices.end()) {
// XXX Check if the device is a Victron device
// This needs lots of improvemet and only do in debug
if (manufacturerData.vendorID == VICTRON_MANUFACTURER_ID) {
debugPrint("Found unmonitored Victron Device: " + normalizeMAC(mac));
// DeviceInfo* deviceInfo = new DeviceInfo(mac, advertisedDevice.getName());
// devices.insert({normalizedMAC, deviceInfo});
// XXX What type of Victron device is it?
// Check if it's a Victron Energy device
/*
if (advertisedDevice.haveServiceData()) {
std::string serviceData = advertisedDevice.getServiceData();
if (serviceData.length() >= 2) {
uint16_t serviceId = (uint8_t)serviceData[1] << 8 | (uint8_t)serviceData[0];
if (serviceId == VICTRON_ENERGY_SERVICE_ID) {
debugPrint("Found Victron Energy Device: " + mac);
}
}
}
*/
}
return; // Not a device we're monitoring return; // Not a device we're monitoring
} }
DeviceInfo* deviceInfo = it->second; DeviceInfo* deviceInfo = it->second;
// Check if device has manufacturer data
if (!advertisedDevice.haveManufacturerData()) {
return;
}
std::string mfgData = advertisedDevice.getManufacturerData();
if (mfgData.length() < 2) {
return;
}
// Check if it's Victron (manufacturer ID 0x02E1) // Check if it's Victron (manufacturer ID 0x02E1)
uint16_t mfgId = (uint8_t)mfgData[1] << 8 | (uint8_t)mfgData[0]; if (manufacturerData.vendorID != VICTRON_MANUFACTURER_ID) {
if (mfgId != VICTRON_MANUFACTURER_ID) { debugPrint("Skipping non VICTRON");
return; return;
} }
debugPrint("Processing data from: " + deviceInfo->config.name); debugPrint("Processing data from: " + deviceInfo->config.name);
// Parse the advertisement // Parse the advertisement
if (parseAdvertisement((const uint8_t*)mfgData.data(), mfgData.length(), normalizedMAC)) { if (parseAdvertisement(normalizedMAC)) {
// Update RSSI // Update RSSI
if (deviceInfo->data) { if (deviceInfo->data) {
deviceInfo->data->rssi = advertisedDevice.getRSSI(); deviceInfo->data->rssi = advertisedDevice.getRSSI();
@@ -179,96 +230,86 @@ void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) {
} }
// Parse advertisement data // Parse advertisement data
bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len, bool VictronBLE::parseAdvertisement(const String& macAddress) {
const String& macAddress) { // XXX We already searched above - try not to again?
auto it = devices.find(macAddress); auto it = devices.find(macAddress);
if (it == devices.end()) { if (it == devices.end()) {
debugPrint("parseAdvertisement: Device not found");
return false; return false;
} }
DeviceInfo* deviceInfo = it->second; DeviceInfo* deviceInfo = it->second;
if (len < 6) {
debugPrint("Manufacturer data too short");
return false;
}
// Structure: [MfgID(2)] [DeviceType(1)] [IV(2)] [EncryptedData(n)]
uint8_t deviceType = manufacturerData[2];
// Extract IV (initialization vector) - bytes 3-4, little-endian
uint8_t iv[16] = {0};
iv[0] = manufacturerData[3];
iv[1] = manufacturerData[4];
// Rest of IV is zero-padded
// Encrypted data starts at byte 5
const uint8_t* encryptedData = manufacturerData + 5;
size_t encryptedLen = len - 5;
if (debugEnabled) { if (debugEnabled) {
debugPrintHex("Encrypted data", encryptedData, encryptedLen); debugPrint("Vendor ID: 0x" + String(manufacturerData.vendorID, HEX));
debugPrintHex("IV", iv, 16); debugPrint("Beacon Type: 0x" + String(manufacturerData.beaconType, HEX));
debugPrint("Record Type: 0x" + String(manufacturerData.victronRecordType, HEX));
debugPrint("Nonce: 0x" + String(manufacturerData.nonceDataCounter, HEX));
} }
// Build IV (initialization vector) from nonce
// IV is 16 bytes: nonce (2 bytes little-endian) + zeros (14 bytes)
uint8_t iv[16] = {0};
iv[0] = manufacturerData.nonceDataCounter & 0xFF; // Low byte
iv[1] = (manufacturerData.nonceDataCounter >> 8) & 0xFF; // High byte
// Remaining bytes stay zero
// Decrypt the data // Decrypt the data
uint8_t decrypted[32]; // Max expected size uint8_t decrypted[32]; // Max expected size
if (!decryptAdvertisement(encryptedData, encryptedLen, if (!decryptAdvertisement(manufacturerData.victronEncryptedData,
sizeof(manufacturerData.victronEncryptedData),
deviceInfo->encryptionKeyBytes, iv, decrypted)) { deviceInfo->encryptionKeyBytes, iv, decrypted)) {
lastError = "Decryption failed"; lastError = "Decryption failed";
debugPrint(lastError);
return false; return false;
} }
if (debugEnabled) {
debugPrintHex("Decrypted data", decrypted, encryptedLen);
}
// Parse based on device type // Parse based on device type
bool parseOk = false; bool parseOk = false;
switch (deviceType) { switch (manufacturerData.victronRecordType) {
case DEVICE_TYPE_SOLAR_CHARGER: case DEVICE_TYPE_SOLAR_CHARGER:
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) { if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
parseOk = parseSolarCharger(decrypted, encryptedLen, parseOk = parseSolarCharger(decrypted, sizeof(decrypted),
*(SolarChargerData*)deviceInfo->data); *(SolarChargerData*)deviceInfo->data);
} }
break; break;
case DEVICE_TYPE_BATTERY_MONITOR: case DEVICE_TYPE_BATTERY_MONITOR:
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) { if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) {
parseOk = parseBatteryMonitor(decrypted, encryptedLen, parseOk = parseBatteryMonitor(decrypted, sizeof(decrypted),
*(BatteryMonitorData*)deviceInfo->data); *(BatteryMonitorData*)deviceInfo->data);
} }
break; break;
case DEVICE_TYPE_INVERTER: case DEVICE_TYPE_INVERTER:
case DEVICE_TYPE_INVERTER_RS: case DEVICE_TYPE_INVERTER_RS:
case DEVICE_TYPE_MULTI_RS: case DEVICE_TYPE_MULTI_RS:
case DEVICE_TYPE_VE_BUS: case DEVICE_TYPE_VE_BUS:
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_INVERTER) { if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_INVERTER) {
parseOk = parseInverter(decrypted, encryptedLen, parseOk = parseInverter(decrypted, sizeof(decrypted),
*(InverterData*)deviceInfo->data); *(InverterData*)deviceInfo->data);
} }
break; break;
case DEVICE_TYPE_DCDC_CONVERTER: case DEVICE_TYPE_DCDC_CONVERTER:
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) { if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) {
parseOk = parseDCDCConverter(decrypted, encryptedLen, parseOk = parseDCDCConverter(decrypted, sizeof(decrypted),
*(DCDCConverterData*)deviceInfo->data); *(DCDCConverterData*)deviceInfo->data);
} }
break; break;
default: default:
debugPrint("Unknown device type: 0x" + String(deviceType, HEX)); debugPrint("Unknown device type: 0x" + String(manufacturerData.victronRecordType, HEX));
return false; return false;
} }
if (parseOk && deviceInfo->data) { if (parseOk && deviceInfo->data) {
deviceInfo->data->dataValid = true; deviceInfo->data->dataValid = true;
// Call appropriate callback // Call appropriate callback
if (callback) { if (callback) {
switch (deviceType) { switch (manufacturerData.victronRecordType) {
case DEVICE_TYPE_SOLAR_CHARGER: case DEVICE_TYPE_SOLAR_CHARGER:
callback->onSolarChargerData(*(SolarChargerData*)deviceInfo->data); callback->onSolarChargerData(*(SolarChargerData*)deviceInfo->data);
break; break;
@@ -287,7 +328,7 @@ bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
} }
} }
} }
return parseOk; return parseOk;
} }
@@ -297,197 +338,208 @@ bool VictronBLE::decryptAdvertisement(const uint8_t* encrypted, size_t encLen,
uint8_t* decrypted) { uint8_t* decrypted) {
mbedtls_aes_context aes; mbedtls_aes_context aes;
mbedtls_aes_init(&aes); mbedtls_aes_init(&aes);
// Set encryption key // Set encryption key
int ret = mbedtls_aes_setkey_enc(&aes, key, 128); int ret = mbedtls_aes_setkey_enc(&aes, key, 128);
if (ret != 0) { if (ret != 0) {
mbedtls_aes_free(&aes); mbedtls_aes_free(&aes);
return false; return false;
} }
// AES-CTR decryption // AES-CTR decryption
size_t nc_off = 0; size_t nc_off = 0;
uint8_t nonce_counter[16]; uint8_t nonce_counter[16];
uint8_t stream_block[16]; uint8_t stream_block[16];
memcpy(nonce_counter, iv, 16); memcpy(nonce_counter, iv, 16);
memset(stream_block, 0, 16); memset(stream_block, 0, 16);
ret = mbedtls_aes_crypt_ctr(&aes, encLen, &nc_off, nonce_counter, ret = mbedtls_aes_crypt_ctr(&aes, encLen, &nc_off, nonce_counter,
stream_block, encrypted, decrypted); stream_block, encrypted, decrypted);
mbedtls_aes_free(&aes); mbedtls_aes_free(&aes);
return (ret == 0); return (ret == 0);
} }
// Parse Solar Charger data // Parse Solar Charger data
bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarChargerData& result) { bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarChargerData& result) {
if (len < 12) { if (len < sizeof(victronSolarChargerPayload)) {
debugPrint("Solar charger data too short"); debugPrint("Solar charger data too short: " + String(len) + " bytes");
return false; return false;
} }
// Byte 0: Charge state // Cast decrypted data to struct for easy access
result.chargeState = (SolarChargerState)data[0]; const victronSolarChargerPayload* payload = (const victronSolarChargerPayload*)data;
// Bytes 1-2: Battery voltage (10 mV units) // Parse charge state
uint16_t vBat = data[1] | (data[2] << 8); result.chargeState = (SolarChargerState)payload->deviceState;
result.batteryVoltage = vBat * 0.01f;
// Parse battery voltage (10 mV units -> volts)
// Bytes 3-4: Battery current (10 mA units, signed) result.batteryVoltage = payload->batteryVoltage * 0.01f;
int16_t iBat = (int16_t)(data[3] | (data[4] << 8));
result.batteryCurrent = iBat * 0.01f; // Parse battery current (10 mA units, signed -> amps)
result.batteryCurrent = payload->batteryCurrent * 0.01f;
// Bytes 5-6: Yield today (10 Wh units)
uint16_t yield = data[5] | (data[6] << 8); // Parse yield today (10 Wh units -> Wh)
result.yieldToday = yield * 10; result.yieldToday = payload->yieldToday * 10;
// Bytes 7-8: PV power (1 W units) // Parse PV power (1 W units)
uint16_t pvPower = data[7] | (data[8] << 8); result.panelPower = payload->inputPower;
result.panelPower = pvPower;
// Parse load current (10 mA units -> amps, 0xFFFF = no load)
// Bytes 9-10: Load current (10 mA units) if (payload->loadCurrent != 0xFFFF) {
uint16_t iLoad = data[9] | (data[10] << 8); result.loadCurrent = payload->loadCurrent * 0.01f;
if (iLoad != 0xFFFF) { // 0xFFFF means no load output } else {
result.loadCurrent = iLoad * 0.01f; result.loadCurrent = 0;
} }
// Calculate PV voltage from power and current (if current > 0) // Calculate PV voltage from power and current (if current > 0)
if (result.batteryCurrent > 0.1f) { if (result.batteryCurrent > 0.1f) {
result.panelVoltage = result.panelPower / result.batteryCurrent; result.panelVoltage = result.panelPower / result.batteryCurrent;
} else {
result.panelVoltage = 0;
} }
debugPrint("Solar Charger: " + String(result.batteryVoltage, 2) + "V, " + debugPrint("Solar Charger: " + String(result.batteryVoltage, 2) + "V, " +
String(result.batteryCurrent, 2) + "A, " + String(result.batteryCurrent, 2) + "A, " +
String(result.panelPower) + "W, State: " + String(result.chargeState)); String(result.panelPower) + "W, State: " + String(result.chargeState));
return true; return true;
} }
// Parse Battery Monitor data // Parse Battery Monitor data
bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result) { bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result) {
if (len < 15) { if (len < sizeof(victronBatteryMonitorPayload)) {
debugPrint("Battery monitor data too short"); debugPrint("Battery monitor data too short: " + String(len) + " bytes");
return false; return false;
} }
// Bytes 0-1: Remaining time (1 minute units) // Cast decrypted data to struct for easy access
uint16_t timeRemaining = data[0] | (data[1] << 8); const victronBatteryMonitorPayload* payload = (const victronBatteryMonitorPayload*)data;
result.remainingMinutes = timeRemaining;
// Parse remaining time (1 minute units)
// Bytes 2-3: Battery voltage (10 mV units) result.remainingMinutes = payload->remainingMins;
uint16_t vBat = data[2] | (data[3] << 8);
result.voltage = vBat * 0.01f; // Parse battery voltage (10 mV units -> volts)
result.voltage = payload->batteryVoltage * 0.01f;
// Byte 4: Alarms
uint8_t alarms = data[4]; // Parse alarm bits
result.alarmLowVoltage = (alarms & 0x01) != 0; result.alarmLowVoltage = (payload->alarms & 0x01) != 0;
result.alarmHighVoltage = (alarms & 0x02) != 0; result.alarmHighVoltage = (payload->alarms & 0x02) != 0;
result.alarmLowSOC = (alarms & 0x04) != 0; result.alarmLowSOC = (payload->alarms & 0x04) != 0;
result.alarmLowTemperature = (alarms & 0x10) != 0; result.alarmLowTemperature = (payload->alarms & 0x10) != 0;
result.alarmHighTemperature = (alarms & 0x20) != 0; result.alarmHighTemperature = (payload->alarms & 0x20) != 0;
// Bytes 5-6: Aux voltage/temperature (10 mV or 0.01K units) // Parse aux data: voltage (10 mV units) or temperature (0.01K units)
uint16_t aux = data[5] | (data[6] << 8); if (payload->auxData < 3000) { // If < 30V, it's voltage
if (aux < 3000) { // If < 30V, it's voltage result.auxVoltage = payload->auxData * 0.01f;
result.auxVoltage = aux * 0.01f;
result.temperature = 0; result.temperature = 0;
} else { // Otherwise temperature in 0.01 Kelvin } else { // Otherwise temperature in 0.01 Kelvin
result.temperature = (aux * 0.01f) - 273.15f; result.temperature = (payload->auxData * 0.01f) - 273.15f;
result.auxVoltage = 0; result.auxVoltage = 0;
} }
// Bytes 7-9: Battery current (22-bit signed, 1 mA units) // Parse battery current (22-bit signed, 1 mA units)
int32_t current = data[7] | (data[8] << 8) | ((data[9] & 0x3F) << 16); // Bits 0-7: currentLow, Bits 8-15: currentMid, Bits 16-21: low 6 bits of currentHigh_consumedLow
if (current & 0x200000) { // Sign extend if negative 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; current |= 0xFFC00000;
} }
result.current = current * 0.001f; result.current = current * 0.001f; // Convert mA to A
// Bytes 9-11: Consumed Ah (18-bit signed, 10 mAh units) // Parse consumed Ah (18-bit signed, 10 mAh units)
int32_t consumedAh = ((data[9] & 0xC0) >> 6) | (data[10] << 2) | ((data[11] & 0xFF) << 10); // Bits 0-1: high 2 bits of currentHigh_consumedLow, Bits 2-9: consumedMid, Bits 10-17: consumedHigh
if (consumedAh & 0x20000) { // Sign extend 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; consumedAh |= 0xFFFC0000;
} }
result.consumedAh = consumedAh * 0.01f; result.consumedAh = consumedAh * 0.01f; // Convert 10mAh to Ah
// Bytes 12-13: SOC (10 = 1.0%) // Parse SOC (10-bit value, 10 = 1.0%)
uint16_t soc = data[12] | ((data[13] & 0x03) << 8); result.soc = (payload->soc & 0x3FF) * 0.1f;
result.soc = soc * 0.1f;
debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " + debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " +
String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%"); String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%");
return true; return true;
} }
// Parse Inverter data // Parse Inverter data
bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& result) { bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& result) {
if (len < 10) { if (len < sizeof(victronInverterPayload)) {
debugPrint("Inverter data too short"); debugPrint("Inverter data too short: " + String(len) + " bytes");
return false; return false;
} }
// Byte 0: Device state // Cast decrypted data to struct for easy access
result.state = data[0]; const victronInverterPayload* payload = (const victronInverterPayload*)data;
// Bytes 1-2: Battery voltage (10 mV units) // Parse device state
uint16_t vBat = data[1] | (data[2] << 8); result.state = payload->deviceState;
result.batteryVoltage = vBat * 0.01f;
// Parse battery voltage (10 mV units -> volts)
// Bytes 3-4: Battery current (10 mA units, signed) result.batteryVoltage = payload->batteryVoltage * 0.01f;
int16_t iBat = (int16_t)(data[3] | (data[4] << 8));
result.batteryCurrent = iBat * 0.01f; // Parse battery current (10 mA units, signed -> amps)
result.batteryCurrent = payload->batteryCurrent * 0.01f;
// Bytes 5-7: AC Power (1 W units, signed 24-bit)
int32_t acPower = data[5] | (data[6] << 8) | (data[7] << 16); // Parse AC Power (signed 24-bit, 1 W units)
if (acPower & 0x800000) { // Sign extend int32_t acPower = payload->acPowerLow |
(payload->acPowerMid << 8) |
(payload->acPowerHigh << 16);
// Sign extend from 24 bits to 32 bits
if (acPower & 0x800000) {
acPower |= 0xFF000000; acPower |= 0xFF000000;
} }
result.acPower = acPower; result.acPower = acPower;
// Byte 8: Alarms // Parse alarm bits
uint8_t alarms = data[8]; result.alarmLowVoltage = (payload->alarms & 0x01) != 0;
result.alarmLowVoltage = (alarms & 0x01) != 0; result.alarmHighVoltage = (payload->alarms & 0x02) != 0;
result.alarmHighVoltage = (alarms & 0x02) != 0; result.alarmHighTemperature = (payload->alarms & 0x04) != 0;
result.alarmHighTemperature = (alarms & 0x04) != 0; result.alarmOverload = (payload->alarms & 0x08) != 0;
result.alarmOverload = (alarms & 0x08) != 0;
debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " + debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " +
String(result.acPower) + "W, State: " + String(result.state)); String(result.acPower) + "W, State: " + String(result.state));
return true; return true;
} }
// Parse DC-DC Converter data // Parse DC-DC Converter data
bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result) { bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result) {
if (len < 10) { if (len < sizeof(victronDCDCConverterPayload)) {
debugPrint("DC-DC converter data too short"); debugPrint("DC-DC converter data too short: " + String(len) + " bytes");
return false; return false;
} }
// Byte 0: Charge state // Cast decrypted data to struct for easy access
result.chargeState = data[0]; const victronDCDCConverterPayload* payload = (const victronDCDCConverterPayload*)data;
// Bytes 1-2: Input voltage (10 mV units) // Parse charge state
uint16_t vIn = data[1] | (data[2] << 8); result.chargeState = payload->chargeState;
result.inputVoltage = vIn * 0.01f;
// Parse error code
// Bytes 3-4: Output voltage (10 mV units) result.errorCode = payload->errorCode;
uint16_t vOut = data[3] | (data[4] << 8);
result.outputVoltage = vOut * 0.01f; // Parse input voltage (10 mV units -> volts)
result.inputVoltage = payload->inputVoltage * 0.01f;
// Bytes 5-6: Output current (10 mA units)
uint16_t iOut = data[5] | (data[6] << 8); // Parse output voltage (10 mV units -> volts)
result.outputCurrent = iOut * 0.01f; result.outputVoltage = payload->outputVoltage * 0.01f;
// Byte 7: Error code // Parse output current (10 mA units -> amps)
result.errorCode = data[7]; result.outputCurrent = payload->outputCurrent * 0.01f;
debugPrint("DC-DC Converter: In=" + String(result.inputVoltage, 2) + "V, Out=" + debugPrint("DC-DC Converter: In=" + String(result.inputVoltage, 2) + "V, Out=" +
String(result.outputVoltage, 2) + "V, " + String(result.outputCurrent, 2) + "A"); String(result.outputVoltage, 2) + "V, " + String(result.outputCurrent, 2) + "A");
return true; return true;
} }
@@ -495,8 +547,8 @@ bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConvert
bool VictronBLE::getSolarChargerData(String macAddress, SolarChargerData& data) { bool VictronBLE::getSolarChargerData(String macAddress, SolarChargerData& data) {
String normalizedMAC = normalizeMAC(macAddress); String normalizedMAC = normalizeMAC(macAddress);
auto it = devices.find(normalizedMAC); auto it = devices.find(normalizedMAC);
if (it != devices.end() && it->second->data && if (it != devices.end() && it->second->data &&
it->second->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) { it->second->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
data = *(SolarChargerData*)it->second->data; data = *(SolarChargerData*)it->second->data;
return data.dataValid; return data.dataValid;
@@ -507,7 +559,7 @@ bool VictronBLE::getSolarChargerData(String macAddress, SolarChargerData& data)
bool VictronBLE::getBatteryMonitorData(String macAddress, BatteryMonitorData& data) { bool VictronBLE::getBatteryMonitorData(String macAddress, BatteryMonitorData& data) {
String normalizedMAC = normalizeMAC(macAddress); String normalizedMAC = normalizeMAC(macAddress);
auto it = devices.find(normalizedMAC); auto it = devices.find(normalizedMAC);
if (it != devices.end() && it->second->data && if (it != devices.end() && it->second->data &&
it->second->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) { it->second->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) {
data = *(BatteryMonitorData*)it->second->data; data = *(BatteryMonitorData*)it->second->data;
@@ -519,7 +571,7 @@ bool VictronBLE::getBatteryMonitorData(String macAddress, BatteryMonitorData& da
bool VictronBLE::getInverterData(String macAddress, InverterData& data) { bool VictronBLE::getInverterData(String macAddress, InverterData& data) {
String normalizedMAC = normalizeMAC(macAddress); String normalizedMAC = normalizeMAC(macAddress);
auto it = devices.find(normalizedMAC); auto it = devices.find(normalizedMAC);
if (it != devices.end() && it->second->data && if (it != devices.end() && it->second->data &&
it->second->data->deviceType == DEVICE_TYPE_INVERTER) { it->second->data->deviceType == DEVICE_TYPE_INVERTER) {
data = *(InverterData*)it->second->data; data = *(InverterData*)it->second->data;
@@ -531,7 +583,7 @@ bool VictronBLE::getInverterData(String macAddress, InverterData& data) {
bool VictronBLE::getDCDCConverterData(String macAddress, DCDCConverterData& data) { bool VictronBLE::getDCDCConverterData(String macAddress, DCDCConverterData& data) {
String normalizedMAC = normalizeMAC(macAddress); String normalizedMAC = normalizeMAC(macAddress);
auto it = devices.find(normalizedMAC); auto it = devices.find(normalizedMAC);
if (it != devices.end() && it->second->data && if (it != devices.end() && it->second->data &&
it->second->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) { it->second->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) {
data = *(DCDCConverterData*)it->second->data; data = *(DCDCConverterData*)it->second->data;
@@ -543,13 +595,13 @@ bool VictronBLE::getDCDCConverterData(String macAddress, DCDCConverterData& data
// Get devices by type // Get devices by type
std::vector<String> VictronBLE::getDevicesByType(VictronDeviceType type) { std::vector<String> VictronBLE::getDevicesByType(VictronDeviceType type) {
std::vector<String> result; std::vector<String> result;
for (const auto& pair : devices) { for (const auto& pair : devices) {
if (pair.second->data && pair.second->data->deviceType == type) { if (pair.second->data && pair.second->data->deviceType == type) {
result.push_back(pair.first); result.push_back(pair.first);
} }
} }
return result; return result;
} }
@@ -577,7 +629,7 @@ bool VictronBLE::hexStringToBytes(const String& hex, uint8_t* bytes, size_t len)
if (hex.length() != len * 2) { if (hex.length() != len * 2) {
return false; return false;
} }
for (size_t i = 0; i < len; i++) { for (size_t i = 0; i < len; i++) {
String byteStr = hex.substring(i * 2, i * 2 + 2); String byteStr = hex.substring(i * 2, i * 2 + 2);
char* endPtr; char* endPtr;
@@ -586,45 +638,28 @@ bool VictronBLE::hexStringToBytes(const String& hex, uint8_t* bytes, size_t len)
return false; return false;
} }
} }
return true; return true;
} }
// Helper: MAC address to string // Helper: MAC address to string
String VictronBLE::macAddressToString(BLEAddress address) { String VictronBLE::macAddressToString(BLEAddress address) {
char macStr[18]; // Use the BLEAddress toString() method which provides consistent formatting
snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x", return String(address.toString().c_str());
address.getNative()[0], address.getNative()[1],
address.getNative()[2], address.getNative()[3],
address.getNative()[4], address.getNative()[5]);
return String(macStr);
} }
// Helper: Normalize MAC address format // Helper: Normalize MAC address format
String VictronBLE::normalizeMAC(String mac) { String VictronBLE::normalizeMAC(String mac) {
String normalized = mac; String normalized = mac;
normalized.toLowerCase(); normalized.toLowerCase();
normalized.replace("-", ":"); // XXX - is this right, was - to : but not consistent location of pairs or not
normalized.replace("-", "");
normalized.replace(":", "");
return normalized; return normalized;
} }
// Debug helpers // Debug helpers
void VictronBLE::debugPrint(const String& message) { void VictronBLE::debugPrint(const String& message) {
if (debugEnabled) { if (debugEnabled)
Serial.println("[VictronBLE] " + message); Serial.println("[VictronBLE] " + message);
}
}
void VictronBLE::debugPrintHex(const char* label, const uint8_t* data, size_t len) {
if (!debugEnabled) return;
Serial.print("[VictronBLE] ");
Serial.print(label);
Serial.print(": ");
for (size_t i = 0; i < len; i++) {
if (data[i] < 0x10) Serial.print("0");
Serial.print(data[i], HEX);
Serial.print(" ");
}
Serial.println();
} }

View File

@@ -1,9 +1,9 @@
/** /**
* VictronBLE - ESP32 library for Victron Energy BLE devices * VictronBLE - ESP32 library for Victron Energy BLE devices
* *
* 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
* *
* Copyright (c) 2025 Scott Penrose * Copyright (c) 2025 Scott Penrose
* License: MIT * License: MIT
*/ */
@@ -53,6 +53,72 @@ enum SolarChargerState {
CHARGER_EXTERNAL_CONTROL = 252 CHARGER_EXTERNAL_CONTROL = 252
}; };
// Binary data structures for decoding BLE advertisements
// Must use __attribute__((packed)) to prevent compiler padding
// Manufacturer data structure (outer envelope)
typedef struct {
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.
} __attribute__((packed)) victronManufacturerData;
// Decrypted payload structures for each device type
// Solar Charger decrypted payload
typedef struct {
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
} __attribute__((packed)) victronSolarChargerPayload;
// Battery Monitor decrypted payload
typedef struct {
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)) victronBatteryMonitorPayload;
// Inverter decrypted payload
typedef struct {
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
} __attribute__((packed)) victronInverterPayload;
// DC-DC Converter decrypted payload
typedef struct {
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
} __attribute__((packed)) victronDCDCConverterPayload;
// Base structure for all device data // Base structure for all device data
struct VictronDeviceData { struct VictronDeviceData {
String deviceName; String deviceName;
@@ -61,8 +127,8 @@ struct VictronDeviceData {
int8_t rssi; int8_t rssi;
uint32_t lastUpdate; uint32_t lastUpdate;
bool dataValid; bool dataValid;
VictronDeviceData() : deviceType(DEVICE_TYPE_UNKNOWN), rssi(-100), VictronDeviceData() : deviceType(DEVICE_TYPE_UNKNOWN), rssi(-100),
lastUpdate(0), dataValid(false) {} lastUpdate(0), dataValid(false) {}
}; };
@@ -75,8 +141,8 @@ struct SolarChargerData : public VictronDeviceData {
float panelPower; // W float panelPower; // W
uint16_t yieldToday; // Wh uint16_t yieldToday; // Wh
float loadCurrent; // A float loadCurrent; // A
SolarChargerData() : chargeState(CHARGER_OFF), batteryVoltage(0), SolarChargerData() : chargeState(CHARGER_OFF), batteryVoltage(0),
batteryCurrent(0), panelVoltage(0), panelPower(0), batteryCurrent(0), panelVoltage(0), panelPower(0),
yieldToday(0), loadCurrent(0) { yieldToday(0), loadCurrent(0) {
deviceType = DEVICE_TYPE_SOLAR_CHARGER; deviceType = DEVICE_TYPE_SOLAR_CHARGER;
@@ -97,8 +163,8 @@ struct BatteryMonitorData : public VictronDeviceData {
bool alarmLowSOC; bool alarmLowSOC;
bool alarmLowTemperature; bool alarmLowTemperature;
bool alarmHighTemperature; bool alarmHighTemperature;
BatteryMonitorData() : voltage(0), current(0), temperature(0), BatteryMonitorData() : voltage(0), current(0), temperature(0),
auxVoltage(0), remainingMinutes(0), consumedAh(0), auxVoltage(0), remainingMinutes(0), consumedAh(0),
soc(0), alarmLowVoltage(false), alarmHighVoltage(false), soc(0), alarmLowVoltage(false), alarmHighVoltage(false),
alarmLowSOC(false), alarmLowTemperature(false), alarmLowSOC(false), alarmLowTemperature(false),
@@ -117,7 +183,7 @@ struct InverterData : public VictronDeviceData {
bool alarmLowVoltage; bool alarmLowVoltage;
bool alarmHighTemperature; bool alarmHighTemperature;
bool alarmOverload; bool alarmOverload;
InverterData() : batteryVoltage(0), batteryCurrent(0), acPower(0), InverterData() : batteryVoltage(0), batteryCurrent(0), acPower(0),
state(0), alarmHighVoltage(false), alarmLowVoltage(false), state(0), alarmHighVoltage(false), alarmLowVoltage(false),
alarmHighTemperature(false), alarmOverload(false) { alarmHighTemperature(false), alarmOverload(false) {
@@ -132,7 +198,7 @@ struct DCDCConverterData : public VictronDeviceData {
float outputCurrent; // A float outputCurrent; // A
uint8_t chargeState; uint8_t chargeState;
uint8_t errorCode; uint8_t errorCode;
DCDCConverterData() : inputVoltage(0), outputVoltage(0), outputCurrent(0), DCDCConverterData() : inputVoltage(0), outputVoltage(0), outputCurrent(0),
chargeState(0), errorCode(0) { chargeState(0), errorCode(0) {
deviceType = DEVICE_TYPE_DCDC_CONVERTER; deviceType = DEVICE_TYPE_DCDC_CONVERTER;
@@ -158,7 +224,7 @@ struct VictronDeviceConfig {
String macAddress; String macAddress;
String encryptionKey; // 32 character hex string String encryptionKey; // 32 character hex string
VictronDeviceType expectedType; VictronDeviceType expectedType;
VictronDeviceConfig() : expectedType(DEVICE_TYPE_UNKNOWN) {} VictronDeviceConfig() : expectedType(DEVICE_TYPE_UNKNOWN) {}
VictronDeviceConfig(String n, String mac, String key, VictronDeviceType type = DEVICE_TYPE_UNKNOWN) VictronDeviceConfig(String n, String mac, String key, VictronDeviceType type = DEVICE_TYPE_UNKNOWN)
: name(n), macAddress(mac), encryptionKey(key), expectedType(type) {} : name(n), macAddress(mac), encryptionKey(key), expectedType(type) {}
@@ -169,50 +235,50 @@ class VictronBLE {
public: public:
VictronBLE(); VictronBLE();
~VictronBLE(); ~VictronBLE();
// Initialize BLE and start scanning // Initialize BLE and start scanning
bool begin(uint32_t scanDuration = 5); bool begin(uint32_t scanDuration = 5);
// Add a device to monitor // Add a device to monitor
bool addDevice(const VictronDeviceConfig& config); bool addDevice(const VictronDeviceConfig& config);
bool addDevice(String name, String macAddress, String encryptionKey, bool addDevice(String name, String macAddress, String encryptionKey,
VictronDeviceType expectedType = DEVICE_TYPE_UNKNOWN); VictronDeviceType expectedType = DEVICE_TYPE_UNKNOWN);
// Remove a device // Remove a device
void removeDevice(String macAddress); void removeDevice(String macAddress);
// Get device count // Get device count
size_t getDeviceCount() const { return devices.size(); } size_t getDeviceCount() const { return devices.size(); }
// Set callback for data updates // Set callback for data updates
void setCallback(VictronDeviceCallback* cb) { callback = cb; } void setCallback(VictronDeviceCallback* cb) { callback = cb; }
// Process scanning (call in loop()) // Process scanning (call in loop())
void loop(); void loop();
// Get latest data for a device // Get latest data for a device
bool getSolarChargerData(String macAddress, SolarChargerData& data); bool getSolarChargerData(String macAddress, SolarChargerData& data);
bool getBatteryMonitorData(String macAddress, BatteryMonitorData& data); bool getBatteryMonitorData(String macAddress, BatteryMonitorData& data);
bool getInverterData(String macAddress, InverterData& data); bool getInverterData(String macAddress, InverterData& data);
bool getDCDCConverterData(String macAddress, DCDCConverterData& data); bool getDCDCConverterData(String macAddress, DCDCConverterData& data);
// Get all devices of a specific type // Get all devices of a specific type
std::vector<String> getDevicesByType(VictronDeviceType type); std::vector<String> getDevicesByType(VictronDeviceType type);
// Enable/disable debug output // Enable/disable debug output
void setDebug(bool enable) { debugEnabled = enable; } void setDebug(bool enable) { debugEnabled = enable; }
// Get last error message // Get last error message
String getLastError() const { return lastError; } String getLastError() const { return lastError; }
private: private:
friend class VictronBLEAdvertisedDeviceCallbacks; friend class VictronBLEAdvertisedDeviceCallbacks;
struct DeviceInfo { struct DeviceInfo {
VictronDeviceConfig config; VictronDeviceConfig config;
VictronDeviceData* data; VictronDeviceData* data;
uint8_t encryptionKeyBytes[16]; uint8_t encryptionKeyBytes[16];
DeviceInfo() : data(nullptr) { DeviceInfo() : data(nullptr) {
memset(encryptionKeyBytes, 0, 16); memset(encryptionKeyBytes, 0, 16);
} }
@@ -220,7 +286,7 @@ private:
if (data) delete data; if (data) delete data;
} }
}; };
std::map<String, DeviceInfo*> devices; std::map<String, DeviceInfo*> devices;
BLEScan* pBLEScan; BLEScan* pBLEScan;
VictronDeviceCallback* callback; VictronDeviceCallback* callback;
@@ -228,25 +294,26 @@ private:
String lastError; String lastError;
uint32_t scanDuration; uint32_t scanDuration;
bool initialized; bool initialized;
// XXX Experiment with actual victron data
victronManufacturerData manufacturerData;
// Internal methods // Internal methods
bool hexStringToBytes(const String& hex, uint8_t* bytes, size_t len); bool hexStringToBytes(const String& hex, uint8_t* bytes, size_t len);
bool decryptAdvertisement(const uint8_t* encrypted, size_t encLen, bool decryptAdvertisement(const uint8_t* encrypted, size_t encLen,
const uint8_t* key, const uint8_t* iv, const uint8_t* key, const uint8_t* iv,
uint8_t* decrypted); uint8_t* decrypted);
bool parseAdvertisement(const uint8_t* manufacturerData, size_t len, bool parseAdvertisement(const String& macAddress);
const String& macAddress);
void processDevice(BLEAdvertisedDevice advertisedDevice); void processDevice(BLEAdvertisedDevice advertisedDevice);
VictronDeviceData* createDeviceData(VictronDeviceType type); VictronDeviceData* createDeviceData(VictronDeviceType type);
bool parseSolarCharger(const uint8_t* data, size_t len, SolarChargerData& result); bool parseSolarCharger(const uint8_t* data, size_t len, SolarChargerData& result);
bool parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result); bool parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result);
bool parseInverter(const uint8_t* data, size_t len, InverterData& result); bool parseInverter(const uint8_t* data, size_t len, InverterData& result);
bool parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result); bool parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result);
void debugPrint(const String& message); void debugPrint(const String& message);
void debugPrintHex(const char* label, const uint8_t* data, size_t len);
String macAddressToString(BLEAddress address); String macAddressToString(BLEAddress address);
String normalizeMAC(String mac); String normalizeMAC(String mac);
}; };
@@ -256,7 +323,7 @@ class VictronBLEAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
public: public:
VictronBLEAdvertisedDeviceCallbacks(VictronBLE* parent) : victronBLE(parent) {} VictronBLEAdvertisedDeviceCallbacks(VictronBLE* parent) : victronBLE(parent) {}
void onResult(BLEAdvertisedDevice advertisedDevice) override; void onResult(BLEAdvertisedDevice advertisedDevice) override;
private: private:
VictronBLE* victronBLE; VictronBLE* victronBLE;
}; };