Compare commits
15 Commits
2bd6094955
...
v0.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
| a843eb924b | |||
| 5a210fb88f | |||
| 30c93af18b | |||
| d9577be900 | |||
| f9e72a68fe | |||
| a8d40ba260 | |||
| af39db8732 | |||
| 26b0196791 | |||
| 0863f8572c | |||
| d3b1c632db | |||
| 03d8da3b7d | |||
| 6a517246ea | |||
| 4bbab345b0 | |||
| 1a651b149d | |||
| cec45524d3 |
104
.claude/CLAUDE.md
Normal file
104
.claude/CLAUDE.md
Normal 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
|
||||
|
||||
31
.claude/scripts/update-claude-md.sh
Executable file
31
.claude/scripts/update-claude-md.sh
Executable 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,14 +2,17 @@
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
- ✅ **Multiple Device Support**: Monitor multiple Victron devices simultaneously
|
||||
|
||||
23
TODO
23
TODO
@@ -6,3 +6,26 @@
|
||||
* 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
|
||||
|
||||
7
VERSIONS
7
VERSIONS
@@ -1,5 +1,12 @@
|
||||
# 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)
|
||||
|
||||
Initial release - not yet tested on hardware.
|
||||
|
||||
143
examples/Logger/platformio.ini
Normal file
143
examples/Logger/platformio.ini
Normal 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
|
||||
167
examples/Logger/src/main.cpp
Normal file
167
examples/Logger/src/main.cpp
Normal 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);
|
||||
}
|
||||
@@ -28,6 +28,8 @@ 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]
|
||||
@@ -117,7 +119,25 @@ build_flags =
|
||||
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
|
||||
|
||||
@@ -30,8 +30,14 @@ VictronBLE victron;
|
||||
// Device callback class - gets called when new data arrives
|
||||
class MyVictronCallback : public VictronDeviceCallback {
|
||||
public:
|
||||
uint32_t solarChargerCount = 0;
|
||||
uint32_t batteryMonitorCount = 0;
|
||||
uint32_t inverterCount = 0;
|
||||
uint32_t dcdcConverterCount = 0;
|
||||
|
||||
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("RSSI: " + String(data.rssi) + " dBm");
|
||||
Serial.println("State: " + getChargeStateName(data.chargeState));
|
||||
@@ -47,7 +53,8 @@ public:
|
||||
}
|
||||
|
||||
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("RSSI: " + String(data.rssi) + " dBm");
|
||||
Serial.println("Voltage: " + String(data.voltage, 2) + " V");
|
||||
@@ -84,7 +91,8 @@ public:
|
||||
}
|
||||
|
||||
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("RSSI: " + String(data.rssi) + " dBm");
|
||||
Serial.println("Battery: " + String(data.batteryVoltage, 2) + " V");
|
||||
@@ -107,7 +115,8 @@ public:
|
||||
}
|
||||
|
||||
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("RSSI: " + String(data.rssi) + " dBm");
|
||||
Serial.println("Input: " + String(data.inputVoltage, 2) + " V");
|
||||
@@ -157,7 +166,7 @@ void setup() {
|
||||
}
|
||||
|
||||
// Enable debug output (optional)
|
||||
victron.setDebug(true);
|
||||
victron.setDebug(false);
|
||||
|
||||
// Set callback for data updates
|
||||
victron.setCallback(&callback);
|
||||
@@ -165,6 +174,9 @@ void setup() {
|
||||
// Add your devices here
|
||||
// Replace with your actual MAC addresses and encryption keys
|
||||
|
||||
// CORRECT in Alternative
|
||||
// Rainbow48V at MAC e4:05:42:34:14:f3
|
||||
|
||||
// Temporary - Scott Example
|
||||
victron.addDevice(
|
||||
"Rainbow48V", // Device name
|
||||
@@ -174,37 +186,12 @@ void setup() {
|
||||
);
|
||||
|
||||
victron.addDevice(
|
||||
"Rainbow48Vb", // Device name
|
||||
"3ffd00b83ffd00be",
|
||||
"0ec3adf7433dd61793ff2f3b8ad32ed8", // Encryption key (32 hex chars)
|
||||
"ScottTrailer", // Device name
|
||||
"e64559783cfb",
|
||||
"3fa658aded4f309b9bc17a2318cb1f56",
|
||||
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
|
||||
/*
|
||||
victron.addDevice(
|
||||
|
||||
35
experiment/platformio.ini
Normal file
35
experiment/platformio.ini
Normal 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
397
experiment/src/main.cpp
Normal 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);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "VictronBLE",
|
||||
"version": "0.1.2",
|
||||
"name": "victronble",
|
||||
"version": "0.3.1",
|
||||
"description": "ESP32 library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements. Supports SmartSolar MPPT, SmartShunt, BMV, MultiPlus, Orion and other Victron devices.",
|
||||
"keywords": "victron, ble, bluetooth, solar, mppt, battery, smartshunt, smartsolar, bmv, inverter, multiplus, esp32, iot, energy, monitoring",
|
||||
"repository": {
|
||||
@@ -26,6 +26,11 @@
|
||||
"name": "MultiDevice",
|
||||
"base": "examples/MultiDevice",
|
||||
"files": ["src/main.cpp"]
|
||||
},
|
||||
{
|
||||
"name": "Logger",
|
||||
"base": "examples/Logger",
|
||||
"files": ["src/main.cpp"]
|
||||
}
|
||||
],
|
||||
"export": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name=VictronBLE
|
||||
version=0.1.1
|
||||
version=0.3.1
|
||||
author=Scott Penrose
|
||||
maintainer=Scott Penrose <scottp@dd.com.au>
|
||||
sentence=ESP32 library for reading Victron Energy device data via BLE for any ESP32
|
||||
|
||||
@@ -42,6 +42,7 @@ bool VictronBLE::begin(uint32_t scanDuration) {
|
||||
|
||||
if (!pBLEScan) {
|
||||
lastError = "Failed to create BLE scanner";
|
||||
debugPrint(lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -60,11 +61,13 @@ bool VictronBLE::begin(uint32_t scanDuration) {
|
||||
bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
|
||||
if (config.macAddress.length() == 0) {
|
||||
lastError = "MAC address cannot be empty";
|
||||
debugPrint(lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config.encryptionKey.length() != 32) {
|
||||
lastError = "Encryption key must be 32 hex characters";
|
||||
debugPrint(lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -83,6 +86,7 @@ bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
|
||||
// Convert encryption key from hex string to bytes
|
||||
if (!hexStringToBytes(config.encryptionKey, info->encryptionKeyBytes, 16)) {
|
||||
lastError = "Invalid encryption key format";
|
||||
debugPrint(lastError);
|
||||
delete info;
|
||||
return false;
|
||||
}
|
||||
@@ -96,7 +100,11 @@ bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -133,50 +141,54 @@ void VictronBLE::loop() {
|
||||
// BLE callback implementation
|
||||
void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertisedDevice) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Process advertised device
|
||||
void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) {
|
||||
// Get MAC address from the advertised device
|
||||
String mac = macAddressToString(advertisedDevice.getAddress());
|
||||
String normalizedMAC = normalizeMAC(mac);
|
||||
|
||||
if (debugEnabled) {
|
||||
debugPrint("Raw MAC: " + mac + " -> Normalized: " + normalizedMAC);
|
||||
}
|
||||
|
||||
// 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
|
||||
auto it = devices.find(normalizedMAC);
|
||||
if (it == devices.end()) {
|
||||
|
||||
// XXX Check if the device is a Victron device
|
||||
// This needs lots of improvemet and only do in debug
|
||||
if (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) {
|
||||
if (manufacturerData.vendorID == VICTRON_MANUFACTURER_ID) {
|
||||
debugPrint("Found unmonitored Victron Device: " + normalizeMAC(mac));
|
||||
// DeviceInfo* deviceInfo = new DeviceInfo(mac, advertisedDevice.getName());
|
||||
// devices.insert({normalizedMAC, deviceInfo});
|
||||
@@ -193,38 +205,22 @@ void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) {
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return; // Not a device we're monitoring
|
||||
}
|
||||
|
||||
DeviceInfo* deviceInfo = it->second;
|
||||
|
||||
// Check if device has manufacturer data
|
||||
if (!advertisedDevice.haveManufacturerData()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string mfgData = advertisedDevice.getManufacturerData();
|
||||
if (mfgData.length() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// XXX Use struct like code in Sh3dNg
|
||||
|
||||
// Check if it's Victron (manufacturer ID 0x02E1)
|
||||
uint16_t mfgId = (uint8_t)mfgData[1] << 8 | (uint8_t)mfgData[0];
|
||||
if (mfgId != VICTRON_MANUFACTURER_ID) {
|
||||
if (manufacturerData.vendorID != VICTRON_MANUFACTURER_ID) {
|
||||
debugPrint("Skipping non VICTRON");
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint("Processing data from: " + deviceInfo->config.name);
|
||||
|
||||
// Parse the advertisement
|
||||
if (parseAdvertisement((const uint8_t*)mfgData.data(), mfgData.length(), normalizedMAC)) {
|
||||
if (parseAdvertisement(normalizedMAC)) {
|
||||
// Update RSSI
|
||||
if (deviceInfo->data) {
|
||||
deviceInfo->data->rssi = advertisedDevice.getRSSI();
|
||||
@@ -234,8 +230,8 @@ void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) {
|
||||
}
|
||||
|
||||
// Parse advertisement data
|
||||
bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
|
||||
const String& macAddress) {
|
||||
bool VictronBLE::parseAdvertisement(const String& macAddress) {
|
||||
// XXX We already searched above - try not to again?
|
||||
auto it = devices.find(macAddress);
|
||||
if (it == devices.end()) {
|
||||
debugPrint("parseAdvertisement: Device not found");
|
||||
@@ -244,69 +240,44 @@ bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
|
||||
|
||||
DeviceInfo* deviceInfo = it->second;
|
||||
|
||||
// Verify minimum size for victronManufacturerData struct
|
||||
if (len < sizeof(victronManufacturerData)) {
|
||||
debugPrint("Manufacturer data too short: " + String(len) + " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cast manufacturer data to struct for easy access
|
||||
const victronManufacturerData* vicData = (const victronManufacturerData*)manufacturerData;
|
||||
|
||||
if (debugEnabled) {
|
||||
debugPrint("Vendor ID: 0x" + String(vicData->vendorID, HEX));
|
||||
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));
|
||||
debugPrint("Vendor ID: 0x" + String(manufacturerData.vendorID, HEX));
|
||||
debugPrint("Beacon Type: 0x" + String(manufacturerData.beaconType, HEX));
|
||||
debugPrint("Record Type: 0x" + String(manufacturerData.victronRecordType, HEX));
|
||||
debugPrint("Nonce: 0x" + String(manufacturerData.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
|
||||
iv[0] = manufacturerData.nonceDataCounter & 0xFF; // Low byte
|
||||
iv[1] = (manufacturerData.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
|
||||
uint8_t decrypted[32]; // Max expected size
|
||||
if (!decryptAdvertisement(encryptedData, encryptedLen,
|
||||
if (!decryptAdvertisement(manufacturerData.victronEncryptedData,
|
||||
sizeof(manufacturerData.victronEncryptedData),
|
||||
deviceInfo->encryptionKeyBytes, iv, decrypted)) {
|
||||
lastError = "Decryption failed";
|
||||
debugPrint(lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (debugEnabled) {
|
||||
debugPrintHex("Decrypted data", decrypted, encryptedLen);
|
||||
}
|
||||
|
||||
// Parse based on device type
|
||||
bool parseOk = false;
|
||||
|
||||
switch (deviceType) {
|
||||
switch (manufacturerData.victronRecordType) {
|
||||
case 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);
|
||||
}
|
||||
break;
|
||||
|
||||
case 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);
|
||||
}
|
||||
break;
|
||||
@@ -316,20 +287,20 @@ bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
|
||||
case DEVICE_TYPE_MULTI_RS:
|
||||
case DEVICE_TYPE_VE_BUS:
|
||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_INVERTER) {
|
||||
parseOk = parseInverter(decrypted, encryptedLen,
|
||||
parseOk = parseInverter(decrypted, sizeof(decrypted),
|
||||
*(InverterData*)deviceInfo->data);
|
||||
}
|
||||
break;
|
||||
|
||||
case 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);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
debugPrint("Unknown device type: 0x" + String(deviceType, HEX));
|
||||
debugPrint("Unknown device type: 0x" + String(manufacturerData.victronRecordType, HEX));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -338,7 +309,7 @@ bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
|
||||
|
||||
// Call appropriate callback
|
||||
if (callback) {
|
||||
switch (deviceType) {
|
||||
switch (manufacturerData.victronRecordType) {
|
||||
case DEVICE_TYPE_SOLAR_CHARGER:
|
||||
callback->onSolarChargerData(*(SolarChargerData*)deviceInfo->data);
|
||||
break;
|
||||
@@ -673,12 +644,8 @@ bool VictronBLE::hexStringToBytes(const String& hex, uint8_t* bytes, size_t len)
|
||||
|
||||
// Helper: MAC address to string
|
||||
String VictronBLE::macAddressToString(BLEAddress address) {
|
||||
char macStr[18];
|
||||
snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
|
||||
address.getNative()[0], address.getNative()[1],
|
||||
address.getNative()[2], address.getNative()[3],
|
||||
address.getNative()[4], address.getNative()[5]);
|
||||
return String(macStr);
|
||||
// Use the BLEAddress toString() method which provides consistent formatting
|
||||
return String(address.toString().c_str());
|
||||
}
|
||||
|
||||
// Helper: Normalize MAC address format
|
||||
@@ -693,22 +660,6 @@ String VictronBLE::normalizeMAC(String mac) {
|
||||
|
||||
// Debug helpers
|
||||
void VictronBLE::debugPrint(const String& message) {
|
||||
if (debugEnabled) {
|
||||
if (debugEnabled)
|
||||
Serial.println("[VictronBLE] " + 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) {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -58,16 +58,15 @@ enum SolarChargerState {
|
||||
|
||||
// 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)
|
||||
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]; // Encrypted payload (max 21 bytes)
|
||||
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
|
||||
@@ -296,13 +295,15 @@ private:
|
||||
uint32_t scanDuration;
|
||||
bool initialized;
|
||||
|
||||
// XXX Experiment with actual victron data
|
||||
victronManufacturerData manufacturerData;
|
||||
|
||||
// Internal methods
|
||||
bool hexStringToBytes(const String& hex, uint8_t* bytes, size_t len);
|
||||
bool decryptAdvertisement(const uint8_t* encrypted, size_t encLen,
|
||||
const uint8_t* key, const uint8_t* iv,
|
||||
uint8_t* decrypted);
|
||||
bool parseAdvertisement(const uint8_t* manufacturerData, size_t len,
|
||||
const String& macAddress);
|
||||
bool parseAdvertisement(const String& macAddress);
|
||||
void processDevice(BLEAdvertisedDevice advertisedDevice);
|
||||
|
||||
VictronDeviceData* createDeviceData(VictronDeviceType type);
|
||||
@@ -312,7 +313,6 @@ private:
|
||||
bool parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result);
|
||||
|
||||
void debugPrint(const String& message);
|
||||
void debugPrintHex(const char* label, const uint8_t* data, size_t len);
|
||||
|
||||
String macAddressToString(BLEAddress address);
|
||||
String normalizeMAC(String mac);
|
||||
|
||||
Reference in New Issue
Block a user