Compare commits
11 Commits
v0.1.1
...
1a651b149d
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a651b149d | |||
| cec45524d3 | |||
| 2bd6094955 | |||
| 9f0f2ce8fd | |||
| 8e5eba47d7 | |||
| 95d83b492a | |||
| 139c6f961d | |||
| 2ccac7b0c8 | |||
| 97a71ce34c | |||
| e827dea4e5 | |||
| 364462a4ed |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -66,3 +66,5 @@ __pycache__/
|
|||||||
.Python
|
.Python
|
||||||
venv/
|
venv/
|
||||||
env/
|
env/
|
||||||
|
|
||||||
|
*.tar.gz
|
||||||
|
|||||||
8
TODO
Normal file
8
TODO
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 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
|
||||||
@@ -26,13 +26,125 @@ 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
|
# -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
|
||||||
|
TaskScheduler
|
||||||
|
Button2
|
||||||
|
ArduinoJson
|
||||||
|
https://github.com/scottp/PsychicHttp.git
|
||||||
|
|
||||||
|
|
||||||
|
[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
|
||||||
|
TaskScheduler
|
||||||
|
Button2
|
||||||
|
ArduinoJson
|
||||||
|
https://github.com/scottp/PsychicHttp.git
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
@@ -45,7 +45,7 @@ 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 + " ===");
|
Serial.println("\n=== Battery Monitor: " + data.deviceName + " ===");
|
||||||
Serial.println("MAC: " + data.macAddress);
|
Serial.println("MAC: " + data.macAddress);
|
||||||
@@ -54,20 +54,20 @@ public:
|
|||||||
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,10 +79,10 @@ 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 + " ===");
|
Serial.println("\n=== Inverter/Charger: " + data.deviceName + " ===");
|
||||||
Serial.println("MAC: " + data.macAddress);
|
Serial.println("MAC: " + data.macAddress);
|
||||||
@@ -91,9 +91,9 @@ public:
|
|||||||
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,10 +102,10 @@ 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 + " ===");
|
Serial.println("\n=== DC-DC Converter: " + data.deviceName + " ===");
|
||||||
Serial.println("MAC: " + data.macAddress);
|
Serial.println("MAC: " + data.macAddress);
|
||||||
@@ -119,7 +119,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 +144,107 @@ 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(true);
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
"Rainbow48Vb", // Device name
|
||||||
|
"3ffd00b83ffd00be",
|
||||||
|
"0ec3adf7433dd61793ff2f3b8ad32ed8", // Encryption key (32 hex chars)
|
||||||
|
DEVICE_TYPE_SOLAR_CHARGER // Device type
|
||||||
|
);
|
||||||
|
|
||||||
|
// WHY this one work?
|
||||||
|
victron.addDevice(
|
||||||
|
"Rainbow48Vc", // Device name
|
||||||
|
"3ffd00a83ffd00ae",
|
||||||
|
"0ec3adf7433dd61793ff2f3b8ad32ed8", // Encryption key (32 hex chars)
|
||||||
|
DEVICE_TYPE_SOLAR_CHARGER // Device type
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
[VictronBLE] Encrypted data: A0 01 83 2C 0E CF D6 04 89 72 6E 81 56 E4 2D F1 83
|
||||||
|
[VictronBLE] IV: 02 58 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||||||
|
[VictronBLE] Decrypted data: E1 1C 99 32 D5 7E 81 A3 EB 8C 25 97 3E 0E DD 2D C4
|
||||||
|
[VictronBLE] Unknown device type: 0x10
|
||||||
|
[VictronBLE] BLE Device: 3ffd0148:3ffd014e, RSSI: -27 dBm
|
||||||
|
[VictronBLE] BLE Device: 3ffd0148:3ffd014e, RSSI: -81 dBm, Mfg ID: 0x2e1 (Victron)
|
||||||
|
[VictronBLE] Processing data from: Rainbow48Vc
|
||||||
|
[VictronBLE] Encrypted data: A0 01 83 2C 0E CF D6 04 89 72 6E 81 56 E4 2D F1 83
|
||||||
|
[VictronBLE] IV: 02 58 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||||||
|
[VictronBLE] Decrypted data: E1 1C 99 32 D5 7E 81 A3 EB 8C 25 97 3E 0E DD 2D C4
|
||||||
|
[VictronBLE] Unknown device type: 0x10
|
||||||
|
[VictronBLE] BLE Device: 3ffd0148:3ffd014e, RSSI: -49 dBm, Mfg ID: 0x75
|
||||||
|
[VictronBLE] BLE Device: 3ffd0148:3ffd014e, RSSI: -26 dBm
|
||||||
|
*/
|
||||||
|
|
||||||
// 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 +252,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 +260,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "VictronBLE",
|
"name": "VictronBLE",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"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": {
|
||||||
|
|||||||
@@ -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,27 @@ 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";
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,42 +62,42 @@ bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
|
|||||||
lastError = "MAC address cannot be empty";
|
lastError = "MAC address cannot be empty";
|
||||||
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";
|
||||||
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";
|
||||||
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 + " (" + normalizedMAC + ")");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +110,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 +124,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();
|
||||||
@@ -133,6 +133,30 @@ void VictronBLE::loop() {
|
|||||||
// BLE callback implementation
|
// BLE callback implementation
|
||||||
void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertisedDevice) {
|
void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertisedDevice) {
|
||||||
if (victronBLE) {
|
if (victronBLE) {
|
||||||
|
// Debug: Log all discovered BLE devices
|
||||||
|
if (victronBLE->debugEnabled) {
|
||||||
|
String mac = victronBLE->macAddressToString(advertisedDevice.getAddress());
|
||||||
|
String debugMsg = "BLE Device: " + mac;
|
||||||
|
debugMsg += ", RSSI: " + String(advertisedDevice.getRSSI()) + " dBm";
|
||||||
|
|
||||||
|
if (advertisedDevice.haveName()) {
|
||||||
|
debugMsg += ", Name: " + String(advertisedDevice.getName().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (advertisedDevice.haveManufacturerData()) {
|
||||||
|
std::string mfgData = advertisedDevice.getManufacturerData();
|
||||||
|
if (mfgData.length() >= 2) {
|
||||||
|
uint16_t mfgId = (uint8_t)mfgData[1] << 8 | (uint8_t)mfgData[0];
|
||||||
|
debugMsg += ", Mfg ID: 0x" + String(mfgId, HEX);
|
||||||
|
if (mfgId == VICTRON_MANUFACTURER_ID) {
|
||||||
|
debugMsg += " (Victron)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
victronBLE->debugPrint(debugMsg);
|
||||||
|
}
|
||||||
|
|
||||||
victronBLE->processDevice(advertisedDevice);
|
victronBLE->processDevice(advertisedDevice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,33 +165,64 @@ void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertise
|
|||||||
void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) {
|
void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) {
|
||||||
String mac = macAddressToString(advertisedDevice.getAddress());
|
String mac = macAddressToString(advertisedDevice.getAddress());
|
||||||
String normalizedMAC = normalizeMAC(mac);
|
String normalizedMAC = normalizeMAC(mac);
|
||||||
|
|
||||||
// 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 (advertisedDevice.haveManufacturerData()) {
|
||||||
|
std::string mfgData = advertisedDevice.getManufacturerData();
|
||||||
|
if (mfgData.length() >= 2) {
|
||||||
|
uint16_t mfgId = (uint8_t)mfgData[1] << 8 | (uint8_t)mfgData[0];
|
||||||
|
if (mfgId == 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
|
// Check if device has manufacturer data
|
||||||
if (!advertisedDevice.haveManufacturerData()) {
|
if (!advertisedDevice.haveManufacturerData()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string mfgData = advertisedDevice.getManufacturerData();
|
std::string mfgData = advertisedDevice.getManufacturerData();
|
||||||
if (mfgData.length() < 2) {
|
if (mfgData.length() < 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XXX Use struct like code in Sh3dNg
|
||||||
|
|
||||||
// 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];
|
uint16_t mfgId = (uint8_t)mfgData[1] << 8 | (uint8_t)mfgData[0];
|
||||||
if (mfgId != VICTRON_MANUFACTURER_ID) {
|
if (mfgId != VICTRON_MANUFACTURER_ID) {
|
||||||
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((const uint8_t*)mfgData.data(), mfgData.length(), normalizedMAC)) {
|
||||||
// Update RSSI
|
// Update RSSI
|
||||||
@@ -183,64 +238,79 @@ bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
|
|||||||
const String& macAddress) {
|
const String& macAddress) {
|
||||||
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) {
|
// Verify minimum size for victronManufacturerData struct
|
||||||
debugPrint("Manufacturer data too short");
|
if (len < sizeof(victronManufacturerData)) {
|
||||||
|
debugPrint("Manufacturer data too short: " + String(len) + " bytes");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Structure: [MfgID(2)] [DeviceType(1)] [IV(2)] [EncryptedData(n)]
|
// Cast manufacturer data to struct for easy access
|
||||||
uint8_t deviceType = manufacturerData[2];
|
const victronManufacturerData* vicData = (const victronManufacturerData*)manufacturerData;
|
||||||
|
|
||||||
// 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(vicData->vendorID, HEX));
|
||||||
debugPrintHex("IV", iv, 16);
|
debugPrint("Beacon Type: 0x" + String(vicData->beaconType, HEX));
|
||||||
|
debugPrint("Model ID: 0x" + String(vicData->modelID, HEX));
|
||||||
|
debugPrint("Readout Type: 0x" + String(vicData->readoutType, HEX));
|
||||||
|
debugPrint("Record Type: 0x" + String(vicData->victronRecordType, HEX));
|
||||||
|
debugPrint("Nonce: 0x" + String(vicData->nonceDataCounter, HEX));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get device type from record type field
|
||||||
|
uint8_t deviceType = vicData->victronRecordType;
|
||||||
|
|
||||||
|
// 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] = vicData->nonceDataCounter & 0xFF; // Low byte
|
||||||
|
iv[1] = (vicData->nonceDataCounter >> 8) & 0xFF; // High byte
|
||||||
|
// Remaining bytes stay zero
|
||||||
|
|
||||||
|
// Get pointer to encrypted data
|
||||||
|
const uint8_t* encryptedData = vicData->victronEncryptedData;
|
||||||
|
size_t encryptedLen = sizeof(vicData->victronEncryptedData);
|
||||||
|
|
||||||
|
if (debugEnabled) {
|
||||||
|
debugPrintHex("IV", iv, 16);
|
||||||
|
debugPrintHex("Encrypted data", encryptedData, encryptedLen);
|
||||||
|
}
|
||||||
|
|
||||||
// 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(encryptedData, encryptedLen,
|
||||||
deviceInfo->encryptionKeyBytes, iv, decrypted)) {
|
deviceInfo->encryptionKeyBytes, iv, decrypted)) {
|
||||||
lastError = "Decryption failed";
|
lastError = "Decryption failed";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debugEnabled) {
|
if (debugEnabled) {
|
||||||
debugPrintHex("Decrypted data", decrypted, encryptedLen);
|
debugPrintHex("Decrypted data", decrypted, encryptedLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse based on device type
|
// Parse based on device type
|
||||||
bool parseOk = false;
|
bool parseOk = false;
|
||||||
|
|
||||||
switch (deviceType) {
|
switch (deviceType) {
|
||||||
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, encryptedLen,
|
||||||
*(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, encryptedLen,
|
||||||
*(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:
|
||||||
@@ -250,22 +320,22 @@ bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
|
|||||||
*(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, encryptedLen,
|
||||||
*(DCDCConverterData*)deviceInfo->data);
|
*(DCDCConverterData*)deviceInfo->data);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
debugPrint("Unknown device type: 0x" + String(deviceType, HEX));
|
debugPrint("Unknown device type: 0x" + String(deviceType, 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 (deviceType) {
|
||||||
@@ -287,7 +357,7 @@ bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseOk;
|
return parseOk;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,197 +367,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 +576,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 +588,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 +600,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 +612,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 +624,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 +658,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,7 +667,7 @@ bool VictronBLE::hexStringToBytes(const String& hex, uint8_t* bytes, size_t len)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -604,7 +685,9 @@ String VictronBLE::macAddressToString(BLEAddress address) {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,9 +698,10 @@ void VictronBLE::debugPrint(const String& message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XXX Can't we use debugPrintf instead for hex struct etc?
|
||||||
void VictronBLE::debugPrintHex(const char* label, const uint8_t* data, size_t len) {
|
void VictronBLE::debugPrintHex(const char* label, const uint8_t* data, size_t len) {
|
||||||
if (!debugEnabled) return;
|
if (!debugEnabled) return;
|
||||||
|
|
||||||
Serial.print("[VictronBLE] ");
|
Serial.print("[VictronBLE] ");
|
||||||
Serial.print(label);
|
Serial.print(label);
|
||||||
Serial.print(": ");
|
Serial.print(": ");
|
||||||
|
|||||||
131
src/VictronBLE.h
131
src/VictronBLE.h
@@ -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,73 @@ 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; // Victron vendor ID (0x02E1)
|
||||||
|
uint8_t beaconType; // Should be 0x10 (Product Advertisement)
|
||||||
|
uint8_t modelID; // Model identifier byte
|
||||||
|
uint8_t readoutType; // Type of data readout
|
||||||
|
uint8_t victronRecordType; // Record type (device type)
|
||||||
|
uint16_t nonceDataCounter; // Nonce for encryption (IV bytes 0-1)
|
||||||
|
uint8_t encryptKeyMatch; // Should match pre-shared encryption key byte 0
|
||||||
|
uint8_t victronEncryptedData[21]; // Encrypted payload (max 21 bytes)
|
||||||
|
} __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 +128,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 +142,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 +164,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 +184,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 +199,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 +225,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 +236,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 +287,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 +295,25 @@ private:
|
|||||||
String lastError;
|
String lastError;
|
||||||
uint32_t scanDuration;
|
uint32_t scanDuration;
|
||||||
bool initialized;
|
bool initialized;
|
||||||
|
|
||||||
// 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 uint8_t* manufacturerData, size_t len,
|
||||||
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);
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user