Compare commits
29 Commits
v0.1.1
...
8a4e010324
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a4e010324 | |||
| 977641b093 | |||
| 24712c206a | |||
| 8a2402cb63 | |||
| a64fef899b | |||
| a843eb924b | |||
| 5a210fb88f | |||
| 30c93af18b | |||
| d9577be900 | |||
| f9e72a68fe | |||
| a8d40ba260 | |||
| af39db8732 | |||
| 26b0196791 | |||
| 0863f8572c | |||
| d3b1c632db | |||
| 03d8da3b7d | |||
| 6a517246ea | |||
| 4bbab345b0 | |||
| 1a651b149d | |||
| cec45524d3 | |||
| 2bd6094955 | |||
| 9f0f2ce8fd | |||
| 8e5eba47d7 | |||
| 95d83b492a | |||
| 139c6f961d | |||
| 2ccac7b0c8 | |||
| 97a71ce34c | |||
| e827dea4e5 | |||
| 364462a4ed |
200
.claude/CLAUDE.md
Normal file
200
.claude/CLAUDE.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# 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
|
||||
|
||||
|
||||
### Session: 2026-02-12 18:23
|
||||
**Commits:**
|
||||
```
|
||||
a843eb9 Keep v0.3.1
|
||||
5a210fb Experimenting with a claude file and created new logging example
|
||||
```
|
||||
**Modified files:**
|
||||
- .claude/CLAUDE.md
|
||||
- README.md
|
||||
- VERSIONS
|
||||
- library.json
|
||||
- library.properties
|
||||
- src/VictronBLE.cpp
|
||||
- src/VictronBLE.h
|
||||
|
||||
|
||||
### Session: 2026-02-12 18:35
|
||||
**Commits:**
|
||||
```
|
||||
a64fef8 New version with smaller memory footprint etc
|
||||
a843eb9 Keep v0.3.1
|
||||
5a210fb Experimenting with a claude file and created new logging example
|
||||
```
|
||||
**Modified files:**
|
||||
- .claude/CLAUDE.md
|
||||
- src/VictronBLE.cpp
|
||||
- src/VictronBLE.h
|
||||
|
||||
|
||||
### Session: 2026-02-13 11:02
|
||||
**Modified files:**
|
||||
- .claude/CLAUDE.md
|
||||
- src/VictronBLE.cpp
|
||||
- src/VictronBLE.h
|
||||
|
||||
|
||||
### Session: 2026-02-15 18:59
|
||||
**Modified files:**
|
||||
- .claude/CLAUDE.md
|
||||
- library.json
|
||||
- src/VictronBLE.cpp
|
||||
- src/VictronBLE.h
|
||||
|
||||
|
||||
### Session: 2026-02-15 19:06
|
||||
**Modified files:**
|
||||
- .claude/CLAUDE.md
|
||||
- library.json
|
||||
- src/VictronBLE.cpp
|
||||
- src/VictronBLE.h
|
||||
|
||||
|
||||
### Session: 2026-02-15 19:10
|
||||
**Modified files:**
|
||||
- .claude/CLAUDE.md
|
||||
- library.json
|
||||
- src/VictronBLE.cpp
|
||||
- src/VictronBLE.h
|
||||
|
||||
|
||||
### Session: 2026-02-15 19:18
|
||||
**Commits:**
|
||||
```
|
||||
8a2402c Repeater and Test code for ESP Now
|
||||
```
|
||||
**Modified files:**
|
||||
- .claude/CLAUDE.md
|
||||
- examples/FakeRepeater/platformio.ini
|
||||
- examples/FakeRepeater/src/main.cpp
|
||||
- examples/Receiver/platformio.ini
|
||||
- examples/Receiver/src/main.cpp
|
||||
- examples/Repeater/platformio.ini
|
||||
- examples/Repeater/src/main.cpp
|
||||
- library.json
|
||||
|
||||
|
||||
### Session: 2026-02-15 19:20
|
||||
**Commits:**
|
||||
```
|
||||
24712c2 Work on receiver and sender
|
||||
8a2402c Repeater and Test code for ESP Now
|
||||
```
|
||||
**Modified files:**
|
||||
- .claude/CLAUDE.md
|
||||
- examples/Receiver/platformio.ini
|
||||
- examples/Receiver/src/main.cpp
|
||||
- examples/Repeater/src/main.cpp
|
||||
|
||||
|
||||
### Session: 2026-02-28 12:26
|
||||
**Modified files:**
|
||||
- .claude/CLAUDE.md
|
||||
- examples/Receiver/src/main.cpp
|
||||
- examples/Repeater/src/main.cpp
|
||||
|
||||
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
.gitignore
vendored
2
.gitignore
vendored
@@ -66,3 +66,5 @@ __pycache__/
|
||||
.Python
|
||||
venv/
|
||||
env/
|
||||
|
||||
*.tar.gz
|
||||
|
||||
@@ -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
|
||||
|
||||
500
REVIEW.md
Normal file
500
REVIEW.md
Normal file
@@ -0,0 +1,500 @@
|
||||
# VictronBLE Code Review
|
||||
|
||||
## Part 1: Bug Fixes, Efficiency & Simplification
|
||||
|
||||
### Bugs
|
||||
|
||||
**1. Missing virtual destructor on `VictronDeviceData` (CRITICAL)**
|
||||
`VictronBLE.h:123` - The base struct has no virtual destructor, but derived objects (`SolarChargerData`, etc.) are deleted through base pointers at `VictronBLE.cpp:287` (`delete data`). This is **undefined behavior** in C++. The derived destructors (which must clean up the `String` members they inherit) may never run.
|
||||
|
||||
Fix: Add `virtual ~VictronDeviceData() {}` — or better, eliminate the inheritance (see simplification below).
|
||||
|
||||
**2. `nullPad` field in `victronManufacturerData` is wrong**
|
||||
`VictronBLE.h:68` - Comment says "extra byte because toCharArray() adds a \0 byte" but the code uses `std::string::copy()` which does NOT null-terminate. This makes the struct 1 byte too large, which is harmless but misleading. If the BLE stack ever returns exactly `sizeof(victronManufacturerData)` bytes, the copy would read past the source buffer.
|
||||
|
||||
Fix: Remove the `nullPad` field.
|
||||
|
||||
**3. `panelVoltage` calculation is unreliable**
|
||||
`VictronBLE.cpp:371-376` - PV voltage is computed as `panelPower / batteryCurrent`. On an MPPT charger, battery current and PV current are different (that's the whole point of MPPT). This gives a meaningless number. The BLE protocol doesn't transmit PV voltage for solar chargers.
|
||||
|
||||
Fix: Remove `panelVoltage` from `SolarChargerData`. It's not in the protocol and the calculation is wrong.
|
||||
|
||||
**4. Aux data voltage/temperature heuristic is fragile**
|
||||
`VictronBLE.cpp:410` - `if (payload->auxData < 3000)` is used to distinguish voltage from temperature. The Victron protocol actually uses a bit flag (bit 15 of the aux field, or the record subtype) to indicate which type of aux input is connected. The magic number 3000 will misclassify edge cases.
|
||||
|
||||
Fix: Use the proper protocol flag if available, or document this as a known limitation.
|
||||
|
||||
### Efficiency Improvements
|
||||
|
||||
**5. `hexStringToBytes` allocates 16 String objects**
|
||||
`VictronBLE.cpp:610-611` - For each byte, `hex.substring()` creates a new heap-allocated `String`. On ESP32, this fragments the heap unnecessarily.
|
||||
|
||||
Fix: Direct char-to-nibble conversion:
|
||||
```cpp
|
||||
bool hexStringToBytes(const char* hex, uint8_t* bytes, size_t len) {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
uint8_t hi = hex[i*2], lo = hex[i*2+1];
|
||||
hi = (hi >= 'a') ? hi - 'a' + 10 : (hi >= 'A') ? hi - 'A' + 10 : hi - '0';
|
||||
lo = (lo >= 'a') ? lo - 'a' + 10 : (lo >= 'A') ? lo - 'A' + 10 : lo - '0';
|
||||
if (hi > 15 || lo > 15) return false;
|
||||
bytes[i] = (hi << 4) | lo;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
**6. MAC normalization on every lookup is wasteful**
|
||||
`normalizeMAC()` is called in `processDevice()` for every BLE advertisement received (could be hundreds per scan), plus in every `getSolarChargerData()` / `getBatteryMonitorData()` call. Each call creates a new String and does 3 replace operations.
|
||||
|
||||
Fix: Normalize once at `addDevice()` time and store as a fixed `char[13]` (12 hex chars + null). Use `memcmp` or `strcmp` for comparison.
|
||||
|
||||
**7. `std::map<String, DeviceInfo*>` is heavy**
|
||||
A typical setup monitors 1-4 devices. `std::map` has significant overhead (red-black tree, heap allocations for nodes). A simple fixed-size array with linear search would be faster and use less memory.
|
||||
|
||||
Fix: Replace with `DeviceInfo devices[MAX_DEVICES]` (where MAX_DEVICES = 8 or similar) and a `uint8_t deviceCount`.
|
||||
|
||||
**8. `loop()` blocks for entire scan duration**
|
||||
`VictronBLE.cpp:140` - `pBLEScan->start(scanDuration, false)` is blocking. With the default 5-second scan duration, `loop()` blocks for 5 seconds every call.
|
||||
|
||||
Fix: Use `pBLEScan->start(0)` for continuous non-blocking scan, or use the async scan API. Data arrives via callbacks anyway.
|
||||
|
||||
### Simplification — Things to Remove
|
||||
|
||||
**9. Remove `VictronDeviceConfig` struct**
|
||||
Only used as a parameter to `addDevice`. The convenience overload `addDevice(name, mac, key, type)` is what all examples use. The config struct adds an unnecessary layer.
|
||||
|
||||
**10. Remove `lastError` / `getLastError()`**
|
||||
Uses heap-allocated String. If `debugEnabled` is true, errors already go to Serial. If debug is off, nobody calls `getLastError()` — none of the examples use it. Remove entirely.
|
||||
|
||||
**11. Remove `getDevicesByType()`**
|
||||
No examples use it. Returns `std::vector<String>` which heap-allocates. Users already know their device MACs since they registered them.
|
||||
|
||||
**12. Remove `removeDevice()`**
|
||||
No examples use it. In a typical embedded deployment, devices are configured once at startup and never removed.
|
||||
|
||||
**13. Remove the per-type getter methods**
|
||||
`getSolarChargerData()`, `getBatteryMonitorData()`, etc. are polling-style accessors. All examples use the callback pattern instead. The getters copy the entire data struct (including Strings) which is expensive. If needed, a single `getData(mac, type)` returning a pointer would suffice.
|
||||
|
||||
**14. Flatten the inheritance hierarchy**
|
||||
`VictronDeviceData` → `SolarChargerData` etc. uses inheritance + dynamic allocation + virtual dispatch (needed once we add virtual destructor). Since each device type is always accessed through its specific type, a tagged union or flat struct per type would be simpler:
|
||||
```cpp
|
||||
struct VictronDevice {
|
||||
char mac[13];
|
||||
char name[32];
|
||||
uint8_t deviceType;
|
||||
int8_t rssi;
|
||||
uint32_t lastUpdate;
|
||||
bool dataValid;
|
||||
union {
|
||||
struct { /* solar fields */ } solar;
|
||||
struct { /* battery fields */ } battery;
|
||||
struct { /* inverter fields */ } inverter;
|
||||
struct { /* dcdc fields */ } dcdc;
|
||||
};
|
||||
};
|
||||
```
|
||||
This eliminates heap allocation, virtual dispatch, and the `createDeviceData` factory.
|
||||
|
||||
**15. Replace virtual callback class with function pointer**
|
||||
`VictronDeviceCallback` with 4 virtual methods → a single function pointer:
|
||||
```cpp
|
||||
typedef void (*VictronCallback)(const VictronDevice* device);
|
||||
```
|
||||
The callback receives the device and can switch on `deviceType`. Simpler, no vtable overhead, compatible with C.
|
||||
|
||||
**16. Remove `String` usage throughout**
|
||||
Arduino `String` uses heap allocation and causes fragmentation. MAC addresses are always 12 hex chars. Device names can use fixed `char[]`. This is the single biggest simplification and memory improvement.
|
||||
|
||||
### Summary of Simplified API
|
||||
|
||||
After removing the above, the public API would be approximately:
|
||||
```cpp
|
||||
void victron_init(uint32_t scanDuration);
|
||||
bool victron_add_device(const char* name, const char* mac, const char* hexKey, uint8_t type);
|
||||
void victron_set_callback(VictronCallback cb);
|
||||
void victron_loop();
|
||||
```
|
||||
~4 functions instead of ~15 methods.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Multi-Platform BLE Support
|
||||
|
||||
### Current BLE Dependencies
|
||||
|
||||
All ESP32-specific BLE code is confined to:
|
||||
|
||||
1. **Headers** (`VictronBLE.h`):
|
||||
- `#include <BLEDevice.h>`, `<BLEAdvertisedDevice.h>`, `<BLEScan.h>`
|
||||
- `BLEScan*` member
|
||||
- `VictronBLEAdvertisedDeviceCallbacks` class inheriting `BLEAdvertisedDeviceCallbacks`
|
||||
- `BLEAddress` type in `macAddressToString()`
|
||||
|
||||
2. **Implementation** (`VictronBLE.cpp`):
|
||||
- `BLEDevice::init()` — line 42
|
||||
- `BLEDevice::getScan()` — line 43
|
||||
- `pBLEScan->setAdvertisedDeviceCallbacks()` — line 52
|
||||
- `pBLEScan->setActiveScan/setInterval/setWindow` — lines 53-55
|
||||
- `pBLEScan->start()` / `pBLEScan->clearResults()` — lines 140-141
|
||||
- `BLEAdvertisedDevice` methods in `processDevice()` — lines 152-213
|
||||
|
||||
3. **Non-BLE dependencies**:
|
||||
- `mbedtls/aes.h` — available on ESP32, STM32 (via Mbed), and many others
|
||||
- `Arduino.h` — available on all Arduino-compatible platforms
|
||||
|
||||
### What is NOT platform-specific
|
||||
|
||||
The bulk of the code — packet structures, enums, decryption, payload parsing — is pure data processing with no BLE dependency. This is ~70% of the code.
|
||||
|
||||
### Recommended Approach: BLE Transport Abstraction
|
||||
|
||||
Instead of a full HAL with virtual interfaces (which adds complexity), use a **push-based architecture** where the platform-specific code feeds raw manufacturer data into the parser:
|
||||
|
||||
```
|
||||
Platform BLE Code (user provides) → victron_process_advertisement() → Callback
|
||||
```
|
||||
|
||||
#### Step 1: Extract parser into standalone module
|
||||
|
||||
Create `victron_parser.h/.c` containing:
|
||||
- All packed structs (manufacturer data, payloads)
|
||||
- All enums (device types, charger states)
|
||||
- `victron_decrypt()` — AES-CTR decryption
|
||||
- `victron_parse_advertisement()` — takes raw manufacturer bytes, returns parsed data
|
||||
- Device registry (add device, lookup by MAC)
|
||||
|
||||
This module has **zero BLE dependency**. It needs only `<stdint.h>`, `<string.h>`, and an AES-CTR implementation.
|
||||
|
||||
#### Step 2: Platform-specific BLE adapter (thin)
|
||||
|
||||
For ESP32 Arduino, provide `VictronBLE_ESP32.h` — a thin wrapper that:
|
||||
- Sets up BLE scanning
|
||||
- In the scan callback, extracts MAC + manufacturer data bytes
|
||||
- Calls `victron_process_advertisement(mac, mfg_data, len, rssi)`
|
||||
|
||||
For STM32 (using STM32 BLE stack, or a BLE module like HM-10):
|
||||
- User writes their own scan callback
|
||||
- Calls the same `victron_process_advertisement()` function
|
||||
|
||||
For NRF52 (using Arduino BLE or nRF SDK):
|
||||
- Same pattern
|
||||
|
||||
#### Step 3: AES portability
|
||||
|
||||
`mbedtls` is widely available but not universal. Allow the AES implementation to be swapped:
|
||||
```c
|
||||
// User can override before including victron_parser.h
|
||||
#ifndef VICTRON_AES_CTR_DECRYPT
|
||||
#define VICTRON_AES_CTR_DECRYPT victron_aes_ctr_mbedtls
|
||||
#endif
|
||||
```
|
||||
Or simply provide a function pointer:
|
||||
```c
|
||||
typedef bool (*victron_aes_fn)(const uint8_t* key, const uint8_t* iv,
|
||||
const uint8_t* in, uint8_t* out, size_t len);
|
||||
void victron_set_aes_impl(victron_aes_fn fn);
|
||||
```
|
||||
|
||||
### Result
|
||||
|
||||
- **Parser**: Works on any CPU (ESP32, STM32, NRF52, Linux, etc.)
|
||||
- **BLE adapter**: ~30 lines of platform-specific glue code
|
||||
- **AES**: Pluggable, defaults to mbedtls
|
||||
|
||||
This approach is simpler than a virtual HAL interface and puts the user in control of their BLE stack.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Part 3: C Core with C++ Wrapper
|
||||
|
||||
### Rationale
|
||||
|
||||
The "knowledge" in this library is:
|
||||
1. Victron BLE advertisement packet format (struct layouts)
|
||||
2. Field encoding (scaling factors, bit packing, sign extension)
|
||||
3. AES-CTR decryption with nonce construction
|
||||
4. Device type identification
|
||||
|
||||
All of this is pure data processing — no C++ features needed. Moving it to C enables:
|
||||
- Use in ESP-IDF (C-based) without Arduino
|
||||
- Use on bare-metal STM32, NRF, PIC, etc.
|
||||
- Use from other languages via FFI (Python ctypes, Rust FFI, etc.)
|
||||
- Smaller binary, no RTTI/vtable overhead
|
||||
|
||||
### Proposed File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
victron_ble_parser.h # C header — all public types and functions
|
||||
victron_ble_parser.c # C implementation — parsing, decryption, device registry
|
||||
VictronBLE.h # C++ wrapper (Arduino/ESP32 convenience class)
|
||||
VictronBLE.cpp # C++ wrapper implementation
|
||||
```
|
||||
|
||||
### `victron_ble_parser.h` — C API
|
||||
|
||||
```c
|
||||
#ifndef VICTRON_BLE_PARSER_H
|
||||
#define VICTRON_BLE_PARSER_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/* ---- Constants ---- */
|
||||
#define VICTRON_MANUFACTURER_ID 0x02E1
|
||||
#define VICTRON_MAX_DEVICES 8
|
||||
#define VICTRON_ENCRYPTION_KEY_LEN 16
|
||||
#define VICTRON_MAX_ENCRYPTED_LEN 21
|
||||
#define VICTRON_MAC_STR_LEN 13 /* 12 hex chars + null */
|
||||
#define VICTRON_NAME_MAX_LEN 32
|
||||
|
||||
/* ---- Enums ---- */
|
||||
typedef enum {
|
||||
VICTRON_DEVICE_UNKNOWN = 0x00,
|
||||
VICTRON_DEVICE_SOLAR_CHARGER = 0x01,
|
||||
VICTRON_DEVICE_BATTERY_MONITOR = 0x02,
|
||||
VICTRON_DEVICE_INVERTER = 0x03,
|
||||
VICTRON_DEVICE_DCDC_CONVERTER = 0x04,
|
||||
VICTRON_DEVICE_SMART_LITHIUM = 0x05,
|
||||
VICTRON_DEVICE_INVERTER_RS = 0x06,
|
||||
/* ... etc ... */
|
||||
} victron_device_type_t;
|
||||
|
||||
typedef enum {
|
||||
VICTRON_CHARGER_OFF = 0,
|
||||
VICTRON_CHARGER_BULK = 3,
|
||||
VICTRON_CHARGER_ABSORPTION = 4,
|
||||
VICTRON_CHARGER_FLOAT = 5,
|
||||
/* ... etc ... */
|
||||
} victron_charger_state_t;
|
||||
|
||||
/* ---- Wire-format structures (packed) ---- */
|
||||
typedef struct {
|
||||
uint16_t vendor_id;
|
||||
uint8_t beacon_type;
|
||||
uint8_t unknown[3];
|
||||
uint8_t record_type;
|
||||
uint16_t nonce;
|
||||
uint8_t key_check;
|
||||
uint8_t encrypted_data[VICTRON_MAX_ENCRYPTED_LEN];
|
||||
} __attribute__((packed)) victron_mfg_data_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t device_state;
|
||||
uint8_t error_code;
|
||||
int16_t battery_voltage_10mv;
|
||||
int16_t battery_current_10ma;
|
||||
uint16_t yield_today_10wh;
|
||||
uint16_t input_power_w;
|
||||
uint16_t load_current_10ma;
|
||||
uint8_t reserved[2];
|
||||
} __attribute__((packed)) victron_solar_raw_t;
|
||||
|
||||
/* ... similar for battery_monitor, inverter, dcdc ... */
|
||||
|
||||
/* ---- Parsed result structures ---- */
|
||||
typedef struct {
|
||||
victron_charger_state_t charge_state;
|
||||
float battery_voltage; /* V */
|
||||
float battery_current; /* A */
|
||||
float panel_power; /* W */
|
||||
uint16_t yield_today_wh;
|
||||
float load_current; /* A */
|
||||
uint8_t error_code;
|
||||
} victron_solar_data_t;
|
||||
|
||||
typedef struct {
|
||||
float voltage; /* V */
|
||||
float current; /* A */
|
||||
float temperature; /* °C */
|
||||
float aux_voltage; /* V */
|
||||
uint16_t remaining_mins;
|
||||
float consumed_ah;
|
||||
float soc; /* % */
|
||||
uint8_t alarms; /* raw alarm bits */
|
||||
} victron_battery_data_t;
|
||||
|
||||
/* ... similar for inverter, dcdc ... */
|
||||
|
||||
/* Tagged union for any device */
|
||||
typedef struct {
|
||||
char mac[VICTRON_MAC_STR_LEN];
|
||||
char name[VICTRON_NAME_MAX_LEN];
|
||||
victron_device_type_t device_type;
|
||||
int8_t rssi;
|
||||
uint32_t last_update_ms;
|
||||
bool data_valid;
|
||||
union {
|
||||
victron_solar_data_t solar;
|
||||
victron_battery_data_t battery;
|
||||
/* victron_inverter_data_t inverter; */
|
||||
/* victron_dcdc_data_t dcdc; */
|
||||
};
|
||||
} victron_device_t;
|
||||
|
||||
/* ---- AES function signature (user can provide their own) ---- */
|
||||
typedef bool (*victron_aes_ctr_fn)(
|
||||
const uint8_t key[16], const uint8_t iv[16],
|
||||
const uint8_t* input, uint8_t* output, size_t len);
|
||||
|
||||
/* ---- Core API ---- */
|
||||
|
||||
/* Initialize the parser context. Provide AES implementation (NULL = use default mbedtls). */
|
||||
void victron_init(victron_aes_ctr_fn aes_fn);
|
||||
|
||||
/* Register a device to monitor. hex_key is 32-char hex string. Returns device index or -1. */
|
||||
int victron_add_device(const char* name, const char* mac_hex,
|
||||
const char* hex_key, victron_device_type_t type);
|
||||
|
||||
/* Process a raw BLE manufacturer data buffer. Called from your BLE scan callback.
|
||||
Returns pointer to updated device, or NULL if not a monitored device. */
|
||||
const victron_device_t* victron_process(const char* mac_hex, int8_t rssi,
|
||||
const uint8_t* mfg_data, size_t mfg_len,
|
||||
uint32_t timestamp_ms);
|
||||
|
||||
/* Get a device by index */
|
||||
const victron_device_t* victron_get_device(int index);
|
||||
|
||||
/* Get device count */
|
||||
int victron_get_device_count(void);
|
||||
|
||||
/* Optional callback — called when a device is updated */
|
||||
typedef void (*victron_update_callback_t)(const victron_device_t* device);
|
||||
void victron_set_callback(victron_update_callback_t cb);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* VICTRON_BLE_PARSER_H */
|
||||
```
|
||||
|
||||
### `victron_ble_parser.c` — Implementation Sketch
|
||||
|
||||
```c
|
||||
#include "victron_ble_parser.h"
|
||||
#include <string.h>
|
||||
|
||||
/* ---- Internal state ---- */
|
||||
static victron_device_t s_devices[VICTRON_MAX_DEVICES];
|
||||
static uint8_t s_keys[VICTRON_MAX_DEVICES][16];
|
||||
static int s_device_count = 0;
|
||||
static victron_aes_ctr_fn s_aes_fn = NULL;
|
||||
static victron_update_callback_t s_callback = NULL;
|
||||
|
||||
/* ---- Default AES (mbedtls) ---- */
|
||||
#ifdef VICTRON_USE_MBEDTLS /* or auto-detect */
|
||||
#include "mbedtls/aes.h"
|
||||
static bool default_aes_ctr(const uint8_t key[16], const uint8_t iv[16],
|
||||
const uint8_t* in, uint8_t* out, size_t len) {
|
||||
mbedtls_aes_context aes;
|
||||
mbedtls_aes_init(&aes);
|
||||
if (mbedtls_aes_setkey_enc(&aes, key, 128) != 0) {
|
||||
mbedtls_aes_free(&aes);
|
||||
return false;
|
||||
}
|
||||
size_t nc_off = 0;
|
||||
uint8_t nonce[16], stream[16];
|
||||
memcpy(nonce, iv, 16);
|
||||
memset(stream, 0, 16);
|
||||
int ret = mbedtls_aes_crypt_ctr(&aes, len, &nc_off, nonce, stream, in, out);
|
||||
mbedtls_aes_free(&aes);
|
||||
return ret == 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
void victron_init(victron_aes_ctr_fn aes_fn) {
|
||||
s_device_count = 0;
|
||||
memset(s_devices, 0, sizeof(s_devices));
|
||||
s_aes_fn = aes_fn;
|
||||
#ifdef VICTRON_USE_MBEDTLS
|
||||
if (!s_aes_fn) s_aes_fn = default_aes_ctr;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* hex_to_bytes, normalize_mac, parse_solar, parse_battery, etc. — all pure C */
|
||||
|
||||
const victron_device_t* victron_process(const char* mac_hex, int8_t rssi,
|
||||
const uint8_t* mfg_data, size_t mfg_len,
|
||||
uint32_t timestamp_ms) {
|
||||
/* 1. Check vendor ID */
|
||||
/* 2. Normalize MAC, find in s_devices[] */
|
||||
/* 3. Build IV from nonce, decrypt */
|
||||
/* 4. Parse based on record_type */
|
||||
/* 5. Update device struct, call callback */
|
||||
/* 6. Return pointer to device */
|
||||
return NULL; /* placeholder */
|
||||
}
|
||||
```
|
||||
|
||||
### `VictronBLE.h` — C++ Arduino Wrapper (thin)
|
||||
|
||||
```cpp
|
||||
#ifndef VICTRON_BLE_H
|
||||
#define VICTRON_BLE_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "victron_ble_parser.h"
|
||||
|
||||
#if defined(ESP32)
|
||||
#include <BLEDevice.h>
|
||||
#include <BLEScan.h>
|
||||
#endif
|
||||
|
||||
class VictronBLE {
|
||||
public:
|
||||
bool begin(uint32_t scanDuration = 5);
|
||||
bool addDevice(const char* name, const char* mac,
|
||||
const char* key, victron_device_type_t type);
|
||||
void setCallback(victron_update_callback_t cb);
|
||||
void loop();
|
||||
private:
|
||||
#if defined(ESP32)
|
||||
BLEScan* scan = nullptr;
|
||||
uint32_t scanDuration = 5;
|
||||
static void onScanResult(BLEAdvertisedDevice dev);
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
### What Goes Where
|
||||
|
||||
| Content | File | Language |
|
||||
|---|---|---|
|
||||
| Packet structs (wire format) | `victron_ble_parser.h` | C |
|
||||
| Device type / state enums | `victron_ble_parser.h` | C |
|
||||
| Parsed data structs | `victron_ble_parser.h` | C |
|
||||
| AES-CTR decryption | `victron_ble_parser.c` | C |
|
||||
| Payload parsing (bit twiddling) | `victron_ble_parser.c` | C |
|
||||
| Device registry | `victron_ble_parser.c` | C |
|
||||
| Hex string conversion | `victron_ble_parser.c` | C |
|
||||
| MAC normalization | `victron_ble_parser.c` | C |
|
||||
| ESP32 BLE scanning | `VictronBLE.cpp` | C++ |
|
||||
| Arduino convenience class | `VictronBLE.h/.cpp` | C++ |
|
||||
|
||||
### Migration Steps
|
||||
|
||||
1. Create `victron_ble_parser.h` with all C types and function declarations
|
||||
2. Create `victron_ble_parser.c` — move parsing functions, convert String→char*, convert class methods→free functions
|
||||
3. Slim down `VictronBLE.h` to just the ESP32 BLE scanning wrapper that calls the C API
|
||||
4. Slim down `VictronBLE.cpp` to just `begin()`, `loop()`, and the scan callback glue
|
||||
5. Update examples (minimal changes — API stays similar)
|
||||
6. Test on ESP32 first, then try compiling the C core on a different target
|
||||
|
||||
### Estimated Code Sizes After Split
|
||||
|
||||
- `victron_ble_parser.h`: ~150 lines (types + API)
|
||||
- `victron_ble_parser.c`: ~300 lines (all the protocol knowledge)
|
||||
- `VictronBLE.h`: ~30 lines (ESP32 wrapper)
|
||||
- `VictronBLE.cpp`: ~50 lines (ESP32 BLE glue)
|
||||
|
||||
vs. current: `VictronBLE.h` ~330 lines + `VictronBLE.cpp` ~640 lines = 970 lines total
|
||||
After: ~530 lines total, with better separation of concerns
|
||||
31
TODO
Normal file
31
TODO
Normal file
@@ -0,0 +1,31 @@
|
||||
# Misc Stuff
|
||||
|
||||
* Consider support for upper/lower case MAC address and optionaly ":"
|
||||
* Scanning - list devices publishing, should be able to get list even without knowing MAC / Encryption key
|
||||
* Struct vs Manual
|
||||
* Sh3dNg version and examples uses structs to get data - seems to work
|
||||
* Example generated uses manually managing a string
|
||||
* Reconsider what is best and use
|
||||
|
||||
# Debugging
|
||||
|
||||
Use standard ESP32 debugging stuff. Handler should not be string but more like printf? Consider alternatives
|
||||
|
||||
Make sure debugging can be to file or serial etc
|
||||
|
||||
# Decrypting
|
||||
|
||||
Review 2 methods of decrypting and check it is working correctly
|
||||
|
||||
Seems mbedTLS is better choice as auotmatic hardware support even on ESP32 - where as the esp_aes version is not portable.
|
||||
|
||||
# Examples
|
||||
|
||||
* Multiple threads - scan BLE in the background
|
||||
* Example scan anything
|
||||
* With and without Callback
|
||||
* Platformio.ini files into example data
|
||||
|
||||
# Logging and Debugging
|
||||
|
||||
* The debugging is very verbose and hard to read - maybe group the messages together for repeats
|
||||
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.
|
||||
|
||||
48
examples/FakeRepeater/platformio.ini
Normal file
48
examples/FakeRepeater/platformio.ini
Normal file
@@ -0,0 +1,48 @@
|
||||
[env:esp32dev]
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
build_flags =
|
||||
-Os
|
||||
-D ARDUINO_ESP32C3_DEV
|
||||
-D CONFIG_IDF_TARGET_ESP32C3
|
||||
-D ARDUINO_USB_MODE=1
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
|
||||
[env:m5stick]
|
||||
platform = espressif32
|
||||
board = m5stick-c
|
||||
framework = arduino
|
||||
board_build.mcu = esp32
|
||||
board_build.f_cpu = 240000000L
|
||||
board_build.partitions = no_ota.csv
|
||||
monitor_speed = 115200
|
||||
monitor_filters = esp32_exception_decoder
|
||||
build_flags =
|
||||
-Os
|
||||
lib_deps =
|
||||
M5StickC
|
||||
102
examples/FakeRepeater/src/main.cpp
Normal file
102
examples/FakeRepeater/src/main.cpp
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* VictronBLE FakeRepeater Example
|
||||
*
|
||||
* Sends fake Solar Charger data over ESPNow every 10 seconds.
|
||||
* Use with the Receiver example to test ESPNow without needing
|
||||
* a real Victron device or the VictronBLE library.
|
||||
*
|
||||
* No VictronBLE dependency - just WiFi + ESPNow.
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <esp_now.h>
|
||||
|
||||
// ESPNow packet structure - must match Receiver
|
||||
struct __attribute__((packed)) SolarChargerPacket {
|
||||
uint8_t chargeState;
|
||||
float batteryVoltage; // V
|
||||
float batteryCurrent; // A
|
||||
float panelVoltage; // V
|
||||
float panelPower; // W
|
||||
uint16_t yieldToday; // Wh
|
||||
float loadCurrent; // A
|
||||
int8_t rssi; // BLE RSSI
|
||||
char deviceName[16]; // Null-terminated, truncated
|
||||
};
|
||||
|
||||
static const uint8_t BROADCAST_ADDR[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
static uint32_t sendCount = 0;
|
||||
static unsigned long lastSendTime = 0;
|
||||
static const unsigned long SEND_INTERVAL_MS = 10000;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
|
||||
Serial.println("\n=== VictronBLE FakeRepeater ===\n");
|
||||
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.disconnect();
|
||||
|
||||
Serial.println("MAC: " + WiFi.macAddress());
|
||||
|
||||
if (esp_now_init() != ESP_OK) {
|
||||
Serial.println("ERROR: ESPNow init failed!");
|
||||
while (1) delay(1000);
|
||||
}
|
||||
|
||||
esp_now_peer_info_t peerInfo = {};
|
||||
memcpy(peerInfo.peer_addr, BROADCAST_ADDR, 6);
|
||||
peerInfo.channel = 0;
|
||||
peerInfo.encrypt = false;
|
||||
|
||||
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
|
||||
Serial.println("ERROR: Failed to add broadcast peer!");
|
||||
while (1) delay(1000);
|
||||
}
|
||||
|
||||
Serial.println("ESPNow initialized, sending fake data every 10s");
|
||||
Serial.println("Packet size: " + String(sizeof(SolarChargerPacket)) + " bytes\n");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
unsigned long now = millis();
|
||||
if (now - lastSendTime < SEND_INTERVAL_MS) {
|
||||
delay(100);
|
||||
return;
|
||||
}
|
||||
lastSendTime = now;
|
||||
sendCount++;
|
||||
|
||||
// Generate varying fake data
|
||||
SolarChargerPacket pkt;
|
||||
pkt.chargeState = (sendCount % 4) + 3; // Cycle through Bulk(3), Absorption(4), Float(5), Storage(6)
|
||||
pkt.batteryVoltage = 51.0f + (sendCount % 20) * 0.15f;
|
||||
pkt.batteryCurrent = 2.0f + (sendCount % 10) * 0.5f;
|
||||
pkt.panelVoltage = 65.0f + (sendCount % 15) * 0.8f;
|
||||
pkt.panelPower = pkt.batteryCurrent * pkt.batteryVoltage;
|
||||
pkt.yieldToday = 100 + sendCount * 10;
|
||||
pkt.loadCurrent = 0;
|
||||
pkt.rssi = -60 - (sendCount % 30);
|
||||
|
||||
memset(pkt.deviceName, 0, sizeof(pkt.deviceName));
|
||||
strncpy(pkt.deviceName, "FakeMPPT", sizeof(pkt.deviceName) - 1);
|
||||
|
||||
esp_err_t result = esp_now_send(BROADCAST_ADDR,
|
||||
reinterpret_cast<const uint8_t*>(&pkt),
|
||||
sizeof(pkt));
|
||||
|
||||
if (result != ESP_OK) {
|
||||
Serial.println("[TX FAIL] " + String(esp_err_to_name(result)));
|
||||
} else {
|
||||
Serial.printf("[TX #%lu] %s Batt:%.2fV %.2fA PV:%.0fW Yield:%uWh RSSI:%d\n",
|
||||
sendCount,
|
||||
pkt.deviceName,
|
||||
pkt.batteryVoltage,
|
||||
pkt.batteryCurrent,
|
||||
pkt.panelPower,
|
||||
pkt.yieldToday,
|
||||
pkt.rssi);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -26,13 +26,118 @@ platform = espressif32
|
||||
board = esp32-s3-devkitc-1
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
monitor_filters = esp32_exception_decoder
|
||||
build_flags =
|
||||
-DCORE_DEBUG_LEVEL=3
|
||||
-D ARDUINO_USB_MODE=1
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
# -DCORE_DEBUG_LEVEL=3
|
||||
|
||||
[env:esp32-s3-debug]
|
||||
platform = espressif32
|
||||
board = esp32-s3-devkitc-1
|
||||
framework = arduino
|
||||
#monitor_speed = 115200
|
||||
#monitor_filters = esp32_exception_decoder
|
||||
upload_protocol = esp-builtin
|
||||
|
||||
; Debug configuration for GDB
|
||||
debug_tool = esp-builtin
|
||||
debug_init_break = tbreak setup
|
||||
debug_speed = 5000
|
||||
debug_load_mode = always
|
||||
|
||||
; Build flags for debugging
|
||||
build_flags =
|
||||
-DCORE_DEBUG_LEVEL=5 ; Maximum ESP32 debug level
|
||||
-O0 ; Disable optimization for debugging
|
||||
-g3 ; Maximum debug information
|
||||
build_type = debug
|
||||
|
||||
[env:esp32-c3]
|
||||
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=3
|
||||
-DCORE_DEBUG_LEVEL=5 ; Maximum ESP32 debug level
|
||||
-O0 ; Disable optimization for debugging
|
||||
-g3 ; Maximum debug information
|
||||
build_type = debug
|
||||
|
||||
[env:m5stick]
|
||||
platform = espressif32
|
||||
board = m5stick-c
|
||||
framework = arduino
|
||||
board_build.mcu = esp32
|
||||
board_build.f_cpu = 240000000L
|
||||
board_build.partitions = no_ota.csv
|
||||
#upload_protocol = espota
|
||||
#upload_port = Button.local
|
||||
monitor_speed = 115200
|
||||
monitor_filters = esp32_exception_decoder
|
||||
#debug_tool = esp-prog ; esp-bridge, esp-prog ; or ftdi, esp-builtin, jlink, etc.
|
||||
# debug_speed = 5000 ; optional: JTAG speed in kHz
|
||||
#build_flags =
|
||||
# -DCORE_DEBUG_LEVEL=5 ; ESP32 debug level
|
||||
# -O0 ; no optimization
|
||||
# -g3 ; max debug info
|
||||
build_flags =
|
||||
-Os
|
||||
lib_deps =
|
||||
M5StickC
|
||||
elapsedMillis
|
||||
|
||||
[env:tough]
|
||||
board = m5stack-core2
|
||||
board_build.mcu = esp32
|
||||
platform = espressif32
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
monitor_filters = esp32_exception_decoder
|
||||
debug_tool = esp-bridge ; esp-bridge, esp-prog ; or ftdi, esp-builtin, jlink, etc.
|
||||
# debug_speed = 5000 ; optional: JTAG speed in kHz
|
||||
build_flags =
|
||||
-DCORE_DEBUG_LEVEL=5 ; ESP32 debug level
|
||||
-O0 ; no optimization
|
||||
-g3 ; max debug info
|
||||
-DARDUINO_M5STACK_TOUGH
|
||||
-DDISPLAY_WIDTH=320
|
||||
-DDISPLAY_HEIGHT=240
|
||||
-DHAS_TOUCH=1
|
||||
-DBUFFER_LINES=10
|
||||
lib_deps =
|
||||
M5Unified
|
||||
elapsedMillis
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* VictronBLE Example
|
||||
*
|
||||
*
|
||||
* This example demonstrates how to use the VictronBLE library to read data
|
||||
* from multiple Victron devices simultaneously.
|
||||
*
|
||||
*
|
||||
* Hardware Requirements:
|
||||
* - ESP32 board
|
||||
* - Victron devices with BLE (SmartSolar, SmartShunt, etc.)
|
||||
*
|
||||
*
|
||||
* Setup:
|
||||
* 1. Get your device encryption keys from the VictronConnect app:
|
||||
* - Open VictronConnect
|
||||
@@ -16,7 +16,7 @@
|
||||
* - Enable "Instant readout via Bluetooth"
|
||||
* - Click "Show" next to "Instant readout details"
|
||||
* - Copy the encryption key (32 hex characters)
|
||||
*
|
||||
*
|
||||
* 2. Update the device configurations below with your devices' MAC addresses
|
||||
* and encryption keys
|
||||
*/
|
||||
@@ -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));
|
||||
@@ -45,29 +51,30 @@ public:
|
||||
}
|
||||
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
|
||||
}
|
||||
|
||||
|
||||
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");
|
||||
Serial.println("Current: " + String(data.current, 2) + " A");
|
||||
Serial.println("SOC: " + String(data.soc, 1) + " %");
|
||||
Serial.println("Consumed: " + String(data.consumedAh, 2) + " Ah");
|
||||
|
||||
|
||||
if (data.remainingMinutes < 65535) {
|
||||
int hours = data.remainingMinutes / 60;
|
||||
int mins = data.remainingMinutes % 60;
|
||||
Serial.println("Time Remaining: " + String(hours) + "h " + String(mins) + "m");
|
||||
}
|
||||
|
||||
|
||||
if (data.temperature > 0) {
|
||||
Serial.println("Temperature: " + String(data.temperature, 1) + " °C");
|
||||
}
|
||||
if (data.auxVoltage > 0) {
|
||||
Serial.println("Aux Voltage: " + String(data.auxVoltage, 2) + " V");
|
||||
}
|
||||
|
||||
|
||||
// Print alarms
|
||||
if (data.alarmLowVoltage || data.alarmHighVoltage || data.alarmLowSOC ||
|
||||
data.alarmLowTemperature || data.alarmHighTemperature) {
|
||||
@@ -79,21 +86,22 @@ public:
|
||||
if (data.alarmHighTemperature) Serial.print("HIGH-TEMP ");
|
||||
Serial.println();
|
||||
}
|
||||
|
||||
|
||||
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
|
||||
}
|
||||
|
||||
|
||||
void onInverterData(const InverterData& data) override {
|
||||
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");
|
||||
Serial.println("Current: " + String(data.batteryCurrent, 2) + " A");
|
||||
Serial.println("AC Power: " + String(data.acPower) + " W");
|
||||
Serial.println("State: " + String(data.state));
|
||||
|
||||
|
||||
// Print alarms
|
||||
if (data.alarmLowVoltage || data.alarmHighVoltage ||
|
||||
if (data.alarmLowVoltage || data.alarmHighVoltage ||
|
||||
data.alarmHighTemperature || data.alarmOverload) {
|
||||
Serial.print("ALARMS: ");
|
||||
if (data.alarmLowVoltage) Serial.print("LOW-V ");
|
||||
@@ -102,12 +110,13 @@ public:
|
||||
if (data.alarmOverload) Serial.print("OVERLOAD ");
|
||||
Serial.println();
|
||||
}
|
||||
|
||||
|
||||
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
|
||||
}
|
||||
|
||||
|
||||
void onDCDCConverterData(const DCDCConverterData& data) override {
|
||||
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");
|
||||
@@ -119,7 +128,7 @@ public:
|
||||
}
|
||||
Serial.println("Last Update: " + String((millis() - data.lastUpdate) / 1000) + "s ago");
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
String getChargeStateName(SolarChargerState state) {
|
||||
switch (state) {
|
||||
@@ -144,59 +153,85 @@ MyVictronCallback callback;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
|
||||
|
||||
Serial.println("\n\n=================================");
|
||||
Serial.println("VictronBLE Multi-Device Example");
|
||||
Serial.println("=================================\n");
|
||||
|
||||
|
||||
// Initialize VictronBLE with 5 second scan duration
|
||||
if (!victron.begin(5)) {
|
||||
Serial.println("ERROR: Failed to initialize VictronBLE!");
|
||||
Serial.println(victron.getLastError());
|
||||
while (1) delay(1000);
|
||||
}
|
||||
|
||||
|
||||
// Enable debug output (optional)
|
||||
victron.setDebug(true);
|
||||
|
||||
victron.setDebug(false);
|
||||
|
||||
// Set callback for data updates
|
||||
victron.setCallback(&callback);
|
||||
|
||||
|
||||
// Add your devices here
|
||||
// Replace with your actual MAC addresses and encryption keys
|
||||
|
||||
|
||||
// CORRECT in Alternative
|
||||
// Rainbow48V at MAC e4:05:42:34:14:f3
|
||||
|
||||
// Temporary - Scott Example
|
||||
victron.addDevice(
|
||||
"Rainbow48V", // Device name
|
||||
"E4:05:42:34:14:F3", // MAC address
|
||||
"0ec3adf7433dd61793ff2f3b8ad32ed8", // Encryption key (32 hex chars)
|
||||
DEVICE_TYPE_SOLAR_CHARGER // Device type
|
||||
);
|
||||
|
||||
victron.addDevice(
|
||||
"ScottTrailer", // Device name
|
||||
"e64559783cfb",
|
||||
"3fa658aded4f309b9bc17a2318cb1f56",
|
||||
DEVICE_TYPE_SOLAR_CHARGER // Device type
|
||||
);
|
||||
|
||||
// Example: Solar Charger #1
|
||||
/*
|
||||
victron.addDevice(
|
||||
"MPPT 100/30", // Device name
|
||||
"E7:48:D4:28:B7:9C", // MAC address
|
||||
"0df4d0395b7d1a876c0c33ecb9e70dcd", // Encryption key (32 hex chars)
|
||||
DEVICE_TYPE_SOLAR_CHARGER // Device type
|
||||
);
|
||||
|
||||
*/
|
||||
|
||||
// Example: Solar Charger #2
|
||||
/*
|
||||
victron.addDevice(
|
||||
"MPPT 75/15",
|
||||
"AA:BB:CC:DD:EE:FF",
|
||||
"1234567890abcdef1234567890abcdef",
|
||||
DEVICE_TYPE_SOLAR_CHARGER
|
||||
);
|
||||
|
||||
*/
|
||||
|
||||
// Example: Battery Monitor (SmartShunt)
|
||||
/*
|
||||
victron.addDevice(
|
||||
"SmartShunt",
|
||||
"11:22:33:44:55:66",
|
||||
"fedcba0987654321fedcba0987654321",
|
||||
DEVICE_TYPE_BATTERY_MONITOR
|
||||
);
|
||||
|
||||
*/
|
||||
|
||||
// Example: Inverter/Charger
|
||||
/*
|
||||
victron.addDevice(
|
||||
"MultiPlus",
|
||||
"99:88:77:66:55:44",
|
||||
"abcdefabcdefabcdefabcdefabcdefab",
|
||||
DEVICE_TYPE_INVERTER
|
||||
);
|
||||
|
||||
*/
|
||||
|
||||
Serial.println("Configured " + String(victron.getDeviceCount()) + " devices");
|
||||
Serial.println("\nStarting BLE scan...\n");
|
||||
}
|
||||
@@ -204,7 +239,7 @@ void setup() {
|
||||
void loop() {
|
||||
// Process BLE scanning and data updates
|
||||
victron.loop();
|
||||
|
||||
|
||||
// Optional: You can also manually query device data
|
||||
// This is useful if you're not using callbacks
|
||||
/*
|
||||
@@ -212,13 +247,13 @@ void loop() {
|
||||
if (victron.getSolarChargerData("E7:48:D4:28:B7:9C", solarData)) {
|
||||
// Do something with solarData
|
||||
}
|
||||
|
||||
|
||||
BatteryMonitorData batteryData;
|
||||
if (victron.getBatteryMonitorData("11:22:33:44:55:66", batteryData)) {
|
||||
// Do something with batteryData
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
// Add a small delay to avoid overwhelming the serial output
|
||||
delay(100);
|
||||
}
|
||||
|
||||
49
examples/Receiver/platformio.ini
Normal file
49
examples/Receiver/platformio.ini
Normal file
@@ -0,0 +1,49 @@
|
||||
[env:esp32dev]
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[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
|
||||
|
||||
[env:m5stick]
|
||||
platform = espressif32
|
||||
board = m5stick-c
|
||||
framework = arduino
|
||||
board_build.mcu = esp32
|
||||
board_build.f_cpu = 240000000L
|
||||
board_build.partitions = no_ota.csv
|
||||
monitor_speed = 115200
|
||||
monitor_filters = esp32_exception_decoder
|
||||
build_flags =
|
||||
-Os
|
||||
-D USE_M5STICK
|
||||
lib_deps =
|
||||
M5StickC
|
||||
|
||||
[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
|
||||
build_flags =
|
||||
-Os
|
||||
-D ARDUINO_ESP32C3_DEV
|
||||
-D CONFIG_IDF_TARGET_ESP32C3
|
||||
-D ARDUINO_USB_MODE=1
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
221
examples/Receiver/src/main.cpp
Normal file
221
examples/Receiver/src/main.cpp
Normal file
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* VictronBLE ESPNow Receiver
|
||||
*
|
||||
* Standalone receiver for data sent by the Repeater example.
|
||||
* Does NOT depend on VictronBLE library - just ESPNow.
|
||||
*
|
||||
* Flash this on a second ESP32 and it will print Solar Charger
|
||||
* data received over ESPNow from the Repeater.
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <esp_now.h>
|
||||
|
||||
#ifdef USE_M5STICK
|
||||
#include <M5StickC.h>
|
||||
#endif
|
||||
|
||||
// ESPNow packet structure - must match Repeater
|
||||
struct __attribute__((packed)) SolarChargerPacket {
|
||||
uint8_t chargeState;
|
||||
float batteryVoltage; // V
|
||||
float batteryCurrent; // A
|
||||
float panelVoltage; // V
|
||||
float panelPower; // W
|
||||
uint16_t yieldToday; // Wh
|
||||
float loadCurrent; // A
|
||||
int8_t rssi; // BLE RSSI
|
||||
char deviceName[16]; // Null-terminated, truncated
|
||||
};
|
||||
|
||||
static uint32_t recvCount = 0;
|
||||
|
||||
#ifdef USE_M5STICK
|
||||
// Display: cache latest packet per device for screen rotation
|
||||
static const int MAX_DISPLAY_DEVICES = 4;
|
||||
static SolarChargerPacket displayPackets[MAX_DISPLAY_DEVICES];
|
||||
static bool displayValid[MAX_DISPLAY_DEVICES] = {};
|
||||
static int displayCount = 0;
|
||||
static int displayPage = 0; // Which device to show
|
||||
static bool displayDirty = true;
|
||||
static unsigned long lastPageSwitch = 0;
|
||||
static const unsigned long PAGE_SWITCH_MS = 5000; // Rotate pages every 5s
|
||||
|
||||
static int findOrAddDisplay(const char* name) {
|
||||
for (int i = 0; i < displayCount; i++) {
|
||||
if (strncmp(displayPackets[i].deviceName, name, 16) == 0) return i;
|
||||
}
|
||||
if (displayCount < MAX_DISPLAY_DEVICES) return displayCount++;
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
static const char* chargeStateName(uint8_t state) {
|
||||
switch (state) {
|
||||
case 0: return "Off";
|
||||
case 1: return "Low Power";
|
||||
case 2: return "Fault";
|
||||
case 3: return "Bulk";
|
||||
case 4: return "Absorption";
|
||||
case 5: return "Float";
|
||||
case 6: return "Storage";
|
||||
case 7: return "Equalize";
|
||||
case 9: return "Inverting";
|
||||
case 11: return "Power Supply";
|
||||
case 252: return "External Control";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
void onDataRecv(const uint8_t* senderMac, const uint8_t* data, int len) {
|
||||
if (len != sizeof(SolarChargerPacket)) {
|
||||
Serial.println("Unexpected packet size: " + String(len));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* pkt = reinterpret_cast<const SolarChargerPacket*>(data);
|
||||
recvCount++;
|
||||
|
||||
// Ensure device name is null-terminated even if corrupted
|
||||
char name[17];
|
||||
memcpy(name, pkt->deviceName, 16);
|
||||
name[16] = '\0';
|
||||
|
||||
Serial.printf("[RX #%lu] %s | State:%s Batt:%.2fV %.2fA PV:%.1fV %.0fW Yield:%uWh",
|
||||
recvCount,
|
||||
name,
|
||||
chargeStateName(pkt->chargeState),
|
||||
pkt->batteryVoltage,
|
||||
pkt->batteryCurrent,
|
||||
pkt->panelVoltage,
|
||||
pkt->panelPower,
|
||||
pkt->yieldToday);
|
||||
|
||||
if (pkt->loadCurrent > 0) {
|
||||
Serial.printf(" Load:%.2fA", pkt->loadCurrent);
|
||||
}
|
||||
|
||||
Serial.printf(" RSSI:%ddBm From:%02X:%02X:%02X:%02X:%02X:%02X\n",
|
||||
pkt->rssi,
|
||||
senderMac[0], senderMac[1], senderMac[2],
|
||||
senderMac[3], senderMac[4], senderMac[5]);
|
||||
|
||||
#ifdef USE_M5STICK
|
||||
int idx = findOrAddDisplay(name);
|
||||
if (idx >= 0) {
|
||||
displayPackets[idx] = *pkt;
|
||||
displayValid[idx] = true;
|
||||
displayDirty = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void setup() {
|
||||
#ifdef USE_M5STICK
|
||||
M5.begin();
|
||||
M5.Lcd.setRotation(3); // Landscape, USB on right
|
||||
M5.Lcd.fillScreen(BLACK);
|
||||
M5.Lcd.setTextColor(WHITE, BLACK);
|
||||
M5.Lcd.setTextSize(1);
|
||||
M5.Lcd.setCursor(0, 0);
|
||||
M5.Lcd.println("ESPNow Receiver");
|
||||
M5.Lcd.println("Waiting...");
|
||||
#endif
|
||||
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
|
||||
Serial.println("\n=== VictronBLE ESPNow Receiver ===\n");
|
||||
|
||||
// Init WiFi in STA mode (required for ESPNow)
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.disconnect();
|
||||
|
||||
Serial.println("MAC: " + WiFi.macAddress());
|
||||
|
||||
// Init ESPNow
|
||||
if (esp_now_init() != ESP_OK) {
|
||||
Serial.println("ERROR: ESPNow init failed!");
|
||||
while (1) delay(1000);
|
||||
}
|
||||
|
||||
esp_now_register_recv_cb(onDataRecv);
|
||||
|
||||
Serial.println("ESPNow initialized, waiting for packets...");
|
||||
Serial.println("Expecting " + String(sizeof(SolarChargerPacket)) + " byte packets\n");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
#ifdef USE_M5STICK
|
||||
M5.update();
|
||||
|
||||
// Button A (front): manually cycle to next device
|
||||
if (M5.BtnA.wasPressed()) {
|
||||
if (displayCount > 0) {
|
||||
displayPage = (displayPage + 1) % displayCount;
|
||||
displayDirty = true;
|
||||
lastPageSwitch = millis();
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-rotate pages every 5 seconds if multiple devices
|
||||
if (displayCount > 1) {
|
||||
unsigned long now = millis();
|
||||
if (now - lastPageSwitch >= PAGE_SWITCH_MS) {
|
||||
lastPageSwitch = now;
|
||||
displayPage = (displayPage + 1) % displayCount;
|
||||
displayDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Redraw screen when data changes or page switches
|
||||
if (displayDirty && displayCount > 0) {
|
||||
displayDirty = false;
|
||||
|
||||
int p = displayPage % displayCount;
|
||||
if (!displayValid[p]) { delay(100); return; }
|
||||
|
||||
const auto& pkt = displayPackets[p];
|
||||
|
||||
M5.Lcd.fillScreen(BLACK);
|
||||
M5.Lcd.setCursor(0, 0);
|
||||
|
||||
// Row 0: device name + page indicator
|
||||
M5.Lcd.setTextColor(CYAN, BLACK);
|
||||
M5.Lcd.printf("%s", pkt.deviceName);
|
||||
if (displayCount > 1) {
|
||||
M5.Lcd.printf(" [%d/%d]", p + 1, displayCount);
|
||||
}
|
||||
M5.Lcd.println();
|
||||
|
||||
// Row 1: charge state
|
||||
M5.Lcd.setTextColor(YELLOW, BLACK);
|
||||
M5.Lcd.printf("State: %s\n", chargeStateName(pkt.chargeState));
|
||||
|
||||
// Row 2: battery voltage + current (large-ish)
|
||||
M5.Lcd.setTextColor(GREEN, BLACK);
|
||||
M5.Lcd.setTextSize(2);
|
||||
M5.Lcd.printf("%.2fV\n", pkt.batteryVoltage);
|
||||
M5.Lcd.setTextSize(1);
|
||||
M5.Lcd.setTextColor(WHITE, BLACK);
|
||||
M5.Lcd.printf("Batt: %.2fA\n", pkt.batteryCurrent);
|
||||
|
||||
// Row 3: PV
|
||||
M5.Lcd.printf("PV: %.1fV %.0fW\n", pkt.panelVoltage, pkt.panelPower);
|
||||
|
||||
// Row 4: yield + load
|
||||
M5.Lcd.printf("Yield: %uWh", pkt.yieldToday);
|
||||
if (pkt.loadCurrent > 0) {
|
||||
M5.Lcd.printf(" Ld:%.1fA", pkt.loadCurrent);
|
||||
}
|
||||
M5.Lcd.println();
|
||||
|
||||
// Row 5: stats
|
||||
M5.Lcd.setTextColor(DARKGREY, BLACK);
|
||||
M5.Lcd.printf("RSSI:%d RX:%lu", pkt.rssi, recvCount);
|
||||
}
|
||||
#endif
|
||||
|
||||
delay(100);
|
||||
}
|
||||
57
examples/Repeater/platformio.ini
Normal file
57
examples/Repeater/platformio.ini
Normal file
@@ -0,0 +1,57 @@
|
||||
[env]
|
||||
lib_extra_dirs = ../..
|
||||
|
||||
[env:esp32dev]
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
monitor_filters = esp32_exception_decoder
|
||||
build_flags =
|
||||
-DCORE_DEBUG_LEVEL=3
|
||||
|
||||
[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
|
||||
|
||||
[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 = huge_app.csv
|
||||
monitor_speed = 115200
|
||||
monitor_filters = time, default, esp32_exception_decoder
|
||||
upload_speed = 921600
|
||||
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:m5stick]
|
||||
platform = espressif32
|
||||
board = m5stick-c
|
||||
framework = arduino
|
||||
board_build.mcu = esp32
|
||||
board_build.f_cpu = 240000000L
|
||||
board_build.partitions = no_ota.csv
|
||||
monitor_speed = 115200
|
||||
monitor_filters = esp32_exception_decoder
|
||||
build_flags =
|
||||
-Os
|
||||
lib_deps =
|
||||
M5StickC
|
||||
elapsedMillis
|
||||
189
examples/Repeater/src/main.cpp
Normal file
189
examples/Repeater/src/main.cpp
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* VictronBLE Repeater Example
|
||||
*
|
||||
* Collects Solar Charger data via BLE and transmits the latest
|
||||
* readings over ESPNow broadcast every 30 seconds. Place this ESP32
|
||||
* near Victron devices and use a separate Receiver ESP32 at a distance.
|
||||
*
|
||||
* ESPNow range is typically much greater than BLE (~200m+ line of sight).
|
||||
*
|
||||
* Setup:
|
||||
* 1. Get your device encryption keys from the VictronConnect app
|
||||
* 2. Update the device configurations below with your MAC and key
|
||||
* 3. Flash the Receiver example on a second ESP32
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <esp_now.h>
|
||||
#include "VictronBLE.h"
|
||||
|
||||
// ESPNow packet structure - must match Receiver
|
||||
struct __attribute__((packed)) SolarChargerPacket {
|
||||
uint8_t chargeState;
|
||||
float batteryVoltage; // V
|
||||
float batteryCurrent; // A
|
||||
float panelVoltage; // V
|
||||
float panelPower; // W
|
||||
uint16_t yieldToday; // Wh
|
||||
float loadCurrent; // A
|
||||
int8_t rssi; // BLE RSSI
|
||||
char deviceName[16]; // Null-terminated, truncated
|
||||
};
|
||||
|
||||
// Broadcast address
|
||||
static const uint8_t BROADCAST_ADDR[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
|
||||
static const unsigned long SEND_INTERVAL_MS = 5000; // 30 seconds
|
||||
|
||||
static uint32_t sendCount = 0;
|
||||
static uint32_t sendFailCount = 0;
|
||||
static uint32_t blePacketCount = 0;
|
||||
|
||||
// Cache latest packet per device
|
||||
static const int MAX_DEVICES = 4;
|
||||
static SolarChargerPacket cachedPackets[MAX_DEVICES];
|
||||
static bool cachedValid[MAX_DEVICES] = {};
|
||||
static int cachedCount = 0;
|
||||
static unsigned long lastSendTime = 0;
|
||||
|
||||
VictronBLE victron;
|
||||
|
||||
// Find cached slot by device name, or allocate a new one
|
||||
static int findOrAddCached(const char* name) {
|
||||
for (int i = 0; i < cachedCount; i++) {
|
||||
if (strncmp(cachedPackets[i].deviceName, name, sizeof(cachedPackets[i].deviceName)) == 0)
|
||||
return i;
|
||||
}
|
||||
if (cachedCount < MAX_DEVICES) return cachedCount++;
|
||||
return -1;
|
||||
}
|
||||
|
||||
class RepeaterCallback : public VictronDeviceCallback {
|
||||
public:
|
||||
void onSolarChargerData(const SolarChargerData& data) override {
|
||||
blePacketCount++;
|
||||
|
||||
// Build packet
|
||||
SolarChargerPacket pkt;
|
||||
pkt.chargeState = static_cast<uint8_t>(data.chargeState);
|
||||
pkt.batteryVoltage = data.batteryVoltage;
|
||||
pkt.batteryCurrent = data.batteryCurrent;
|
||||
pkt.panelVoltage = data.panelVoltage;
|
||||
pkt.panelPower = data.panelPower;
|
||||
pkt.yieldToday = data.yieldToday;
|
||||
pkt.loadCurrent = data.loadCurrent;
|
||||
pkt.rssi = data.rssi;
|
||||
memset(pkt.deviceName, 0, sizeof(pkt.deviceName));
|
||||
strncpy(pkt.deviceName, data.deviceName.c_str(), sizeof(pkt.deviceName) - 1);
|
||||
|
||||
// Cache it
|
||||
int idx = findOrAddCached(pkt.deviceName);
|
||||
if (idx >= 0) {
|
||||
cachedPackets[idx] = pkt;
|
||||
cachedValid[idx] = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
RepeaterCallback callback;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
|
||||
Serial.println("\n=== VictronBLE ESPNow Repeater ===\n");
|
||||
|
||||
// Init WiFi in STA mode (required for ESPNow)
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.disconnect();
|
||||
|
||||
Serial.println("MAC: " + WiFi.macAddress());
|
||||
|
||||
// Init ESPNow
|
||||
if (esp_now_init() != ESP_OK) {
|
||||
Serial.println("ERROR: ESPNow init failed!");
|
||||
while (1) delay(1000);
|
||||
}
|
||||
|
||||
// Add broadcast peer
|
||||
esp_now_peer_info_t peerInfo = {};
|
||||
memcpy(peerInfo.peer_addr, BROADCAST_ADDR, 6);
|
||||
peerInfo.channel = 0; // Use current channel
|
||||
peerInfo.encrypt = false;
|
||||
|
||||
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
|
||||
Serial.println("ERROR: Failed to add broadcast peer!");
|
||||
while (1) delay(1000);
|
||||
}
|
||||
|
||||
Serial.println("ESPNow initialized, broadcasting on all channels");
|
||||
|
||||
// Init VictronBLE
|
||||
if (!victron.begin(5)) {
|
||||
Serial.println("ERROR: Failed to initialize VictronBLE!");
|
||||
Serial.println(victron.getLastError());
|
||||
while (1) delay(1000);
|
||||
}
|
||||
|
||||
victron.setDebug(false);
|
||||
victron.setCallback(&callback);
|
||||
|
||||
// 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()) + " BLE devices");
|
||||
Serial.println("Packet size: " + String(sizeof(SolarChargerPacket)) + " bytes\n");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
victron.loop();
|
||||
|
||||
// Send cached packets every 30 seconds
|
||||
unsigned long now = millis();
|
||||
if (now - lastSendTime >= SEND_INTERVAL_MS) {
|
||||
lastSendTime = now;
|
||||
|
||||
int sent = 0;
|
||||
for (int i = 0; i < cachedCount; i++) {
|
||||
if (!cachedValid[i]) continue;
|
||||
|
||||
esp_err_t result = esp_now_send(BROADCAST_ADDR,
|
||||
reinterpret_cast<const uint8_t*>(&cachedPackets[i]),
|
||||
sizeof(SolarChargerPacket));
|
||||
|
||||
if (result == ESP_OK) {
|
||||
sendCount++;
|
||||
sent++;
|
||||
Serial.printf("[ESPNow] Sent %s: %.2fV %.1fA PV:%.1fV %.0fW State:%d\n",
|
||||
cachedPackets[i].deviceName,
|
||||
cachedPackets[i].batteryVoltage,
|
||||
cachedPackets[i].batteryCurrent,
|
||||
cachedPackets[i].panelVoltage,
|
||||
cachedPackets[i].panelPower,
|
||||
cachedPackets[i].chargeState);
|
||||
} else {
|
||||
sendFailCount++;
|
||||
Serial.printf("[ESPNow] FAIL sending %s (err=%d)\n",
|
||||
cachedPackets[i].deviceName, result);
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("[Stats] BLE pkts:%lu ESPNow sent:%lu fail:%lu devices:%d\n",
|
||||
blePacketCount, sendCount, sendFailCount, cachedCount);
|
||||
}
|
||||
|
||||
delay(100);
|
||||
}
|
||||
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);
|
||||
}
|
||||
14
library.json
14
library.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "VictronBLE",
|
||||
"version": "0.1.1",
|
||||
"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,16 @@
|
||||
"name": "MultiDevice",
|
||||
"base": "examples/MultiDevice",
|
||||
"files": ["src/main.cpp"]
|
||||
},
|
||||
{
|
||||
"name": "Logger",
|
||||
"base": "examples/Logger",
|
||||
"files": ["src/main.cpp"]
|
||||
},
|
||||
{
|
||||
"name": "Repeater",
|
||||
"base": "examples/Repeater",
|
||||
"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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* VictronBLE - ESP32 library for Victron Energy BLE devices
|
||||
* Implementation file
|
||||
*
|
||||
*
|
||||
* Copyright (c) 2025 Scott Penrose
|
||||
* License: MIT
|
||||
*/
|
||||
@@ -9,9 +9,9 @@
|
||||
#include "VictronBLE.h"
|
||||
|
||||
// Constructor
|
||||
VictronBLE::VictronBLE()
|
||||
: pBLEScan(nullptr), callback(nullptr), debugEnabled(false),
|
||||
scanDuration(5), initialized(false) {
|
||||
VictronBLE::VictronBLE()
|
||||
: pBLEScan(nullptr), scanCallback(nullptr), callback(nullptr),
|
||||
debugEnabled(false), scanDuration(5), initialized(false) {
|
||||
}
|
||||
|
||||
// Destructor
|
||||
@@ -20,39 +20,43 @@ VictronBLE::~VictronBLE() {
|
||||
delete pair.second;
|
||||
}
|
||||
devices.clear();
|
||||
|
||||
|
||||
if (pBLEScan) {
|
||||
pBLEScan->stop();
|
||||
}
|
||||
|
||||
delete scanCallback;
|
||||
}
|
||||
|
||||
// Initialize BLE
|
||||
bool VictronBLE::begin(uint32_t scanDuration) {
|
||||
if (initialized) {
|
||||
debugPrint("VictronBLE already initialized");
|
||||
if (debugEnabled) debugPrint("VictronBLE already initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
this->scanDuration = scanDuration;
|
||||
|
||||
debugPrint("Initializing VictronBLE...");
|
||||
|
||||
|
||||
if (debugEnabled) debugPrint("Initializing VictronBLE...");
|
||||
|
||||
BLEDevice::init("VictronBLE");
|
||||
pBLEScan = BLEDevice::getScan();
|
||||
|
||||
|
||||
if (!pBLEScan) {
|
||||
lastError = "Failed to create BLE scanner";
|
||||
if (debugEnabled) debugPrint(lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
pBLEScan->setAdvertisedDeviceCallbacks(new VictronBLEAdvertisedDeviceCallbacks(this), true);
|
||||
|
||||
scanCallback = new VictronBLEAdvertisedDeviceCallbacks(this);
|
||||
pBLEScan->setAdvertisedDeviceCallbacks(scanCallback, true);
|
||||
pBLEScan->setActiveScan(false); // Passive scan - lower power
|
||||
pBLEScan->setInterval(100);
|
||||
pBLEScan->setWindow(99);
|
||||
|
||||
|
||||
initialized = true;
|
||||
debugPrint("VictronBLE initialized successfully");
|
||||
|
||||
if (debugEnabled) debugPrint("VictronBLE initialized successfully");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -60,62 +64,69 @@ bool VictronBLE::begin(uint32_t scanDuration) {
|
||||
bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
|
||||
if (config.macAddress.length() == 0) {
|
||||
lastError = "MAC address cannot be empty";
|
||||
if (debugEnabled) debugPrint(lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (config.encryptionKey.length() != 32) {
|
||||
lastError = "Encryption key must be 32 hex characters";
|
||||
if (debugEnabled) debugPrint(lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
String normalizedMAC = normalizeMAC(config.macAddress);
|
||||
|
||||
|
||||
// Check if device already exists
|
||||
if (devices.find(normalizedMAC) != devices.end()) {
|
||||
debugPrint("Device " + normalizedMAC + " already exists, updating config");
|
||||
if (debugEnabled) debugPrint("Device " + normalizedMAC + " already exists, updating config");
|
||||
delete devices[normalizedMAC];
|
||||
}
|
||||
|
||||
|
||||
DeviceInfo* info = new DeviceInfo();
|
||||
info->config = config;
|
||||
info->config.macAddress = normalizedMAC;
|
||||
|
||||
|
||||
// Convert encryption key from hex string to bytes
|
||||
if (!hexStringToBytes(config.encryptionKey, info->encryptionKeyBytes, 16)) {
|
||||
lastError = "Invalid encryption key format";
|
||||
if (debugEnabled) debugPrint(lastError);
|
||||
delete info;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Create appropriate data structure based on device type
|
||||
info->data = createDeviceData(config.expectedType);
|
||||
if (info->data) {
|
||||
info->data->macAddress = normalizedMAC;
|
||||
info->data->deviceName = config.name;
|
||||
}
|
||||
|
||||
|
||||
devices[normalizedMAC] = info;
|
||||
|
||||
debugPrint("Added device: " + config.name + " (" + normalizedMAC + ")");
|
||||
|
||||
|
||||
if (debugEnabled) {
|
||||
debugPrint("Added device: " + config.name + " (MAC: " + normalizedMAC + ")");
|
||||
debugPrint(" Original MAC input: " + config.macAddress);
|
||||
debugPrint(" Stored normalized: " + normalizedMAC);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VictronBLE::addDevice(String name, String macAddress, String encryptionKey,
|
||||
bool VictronBLE::addDevice(const String& name, const String& macAddress, const String& encryptionKey,
|
||||
VictronDeviceType expectedType) {
|
||||
VictronDeviceConfig config(name, macAddress, encryptionKey, expectedType);
|
||||
return addDevice(config);
|
||||
}
|
||||
|
||||
// Remove a device
|
||||
void VictronBLE::removeDevice(String macAddress) {
|
||||
void VictronBLE::removeDevice(const String& macAddress) {
|
||||
String normalizedMAC = normalizeMAC(macAddress);
|
||||
|
||||
|
||||
auto it = devices.find(normalizedMAC);
|
||||
if (it != devices.end()) {
|
||||
delete it->second;
|
||||
devices.erase(it);
|
||||
debugPrint("Removed device: " + normalizedMAC);
|
||||
if (debugEnabled) debugPrint("Removed device: " + normalizedMAC);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +135,7 @@ void VictronBLE::loop() {
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Start a scan
|
||||
BLEScanResults scanResults = pBLEScan->start(scanDuration, false);
|
||||
pBLEScan->clearResults();
|
||||
@@ -138,38 +149,61 @@ void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertise
|
||||
}
|
||||
|
||||
// Process advertised device
|
||||
void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) {
|
||||
void VictronBLE::processDevice(BLEAdvertisedDevice& advertisedDevice) {
|
||||
// Get MAC address from the advertised device
|
||||
String mac = macAddressToString(advertisedDevice.getAddress());
|
||||
String normalizedMAC = normalizeMAC(mac);
|
||||
|
||||
|
||||
if (debugEnabled) {
|
||||
debugPrint("Raw MAC: " + mac + " -> Normalized: " + normalizedMAC);
|
||||
}
|
||||
|
||||
// Parse manufacturer data into local struct
|
||||
victronManufacturerData mfgData;
|
||||
memset(&mfgData, 0, sizeof(mfgData));
|
||||
if (advertisedDevice.haveManufacturerData()) {
|
||||
std::string rawMfgData = advertisedDevice.getManufacturerData();
|
||||
if (debugEnabled) debugPrint("Getting manufacturer data: Size=" + String(rawMfgData.length()));
|
||||
rawMfgData.copy(reinterpret_cast<char*>(&mfgData),
|
||||
(rawMfgData.length() > sizeof(mfgData) ? sizeof(mfgData) : rawMfgData.length()));
|
||||
}
|
||||
|
||||
// Debug: Log all discovered BLE devices
|
||||
if (debugEnabled) {
|
||||
String debugMsg = "BLE Device: " + mac;
|
||||
debugMsg += ", RSSI: " + String(advertisedDevice.getRSSI()) + " dBm";
|
||||
if (advertisedDevice.haveName())
|
||||
debugMsg += ", Name: " + String(advertisedDevice.getName().c_str());
|
||||
|
||||
debugMsg += ", Mfg ID: 0x" + String(mfgData.vendorID, HEX);
|
||||
if (mfgData.vendorID == VICTRON_MANUFACTURER_ID) {
|
||||
debugMsg += " (Victron)";
|
||||
}
|
||||
|
||||
debugPrint(debugMsg);
|
||||
}
|
||||
|
||||
// Check if this is one of our configured devices
|
||||
auto it = devices.find(normalizedMAC);
|
||||
if (it == devices.end()) {
|
||||
if (debugEnabled && mfgData.vendorID == VICTRON_MANUFACTURER_ID) {
|
||||
debugPrint("Found unmonitored Victron Device: " + normalizedMAC);
|
||||
}
|
||||
return; // Not a device we're monitoring
|
||||
}
|
||||
|
||||
|
||||
DeviceInfo* deviceInfo = it->second;
|
||||
|
||||
// Check if device has manufacturer data
|
||||
if (!advertisedDevice.haveManufacturerData()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string mfgData = advertisedDevice.getManufacturerData();
|
||||
if (mfgData.length() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check if it's Victron (manufacturer ID 0x02E1)
|
||||
uint16_t mfgId = (uint8_t)mfgData[1] << 8 | (uint8_t)mfgData[0];
|
||||
if (mfgId != VICTRON_MANUFACTURER_ID) {
|
||||
if (mfgData.vendorID != VICTRON_MANUFACTURER_ID) {
|
||||
if (debugEnabled) debugPrint("Skipping non VICTRON");
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint("Processing data from: " + deviceInfo->config.name);
|
||||
|
||||
|
||||
if (debugEnabled) debugPrint("Processing data from: " + deviceInfo->config.name);
|
||||
|
||||
// Parse the advertisement
|
||||
if (parseAdvertisement((const uint8_t*)mfgData.data(), mfgData.length(), normalizedMAC)) {
|
||||
if (parseAdvertisement(deviceInfo, mfgData)) {
|
||||
// Update RSSI
|
||||
if (deviceInfo->data) {
|
||||
deviceInfo->data->rssi = advertisedDevice.getRSSI();
|
||||
@@ -179,115 +213,97 @@ void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) {
|
||||
}
|
||||
|
||||
// Parse advertisement data
|
||||
bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
|
||||
const String& macAddress) {
|
||||
auto it = devices.find(macAddress);
|
||||
if (it == devices.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DeviceInfo* deviceInfo = it->second;
|
||||
|
||||
if (len < 6) {
|
||||
debugPrint("Manufacturer data too short");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Structure: [MfgID(2)] [DeviceType(1)] [IV(2)] [EncryptedData(n)]
|
||||
uint8_t deviceType = manufacturerData[2];
|
||||
|
||||
// Extract IV (initialization vector) - bytes 3-4, little-endian
|
||||
uint8_t iv[16] = {0};
|
||||
iv[0] = manufacturerData[3];
|
||||
iv[1] = manufacturerData[4];
|
||||
// Rest of IV is zero-padded
|
||||
|
||||
// Encrypted data starts at byte 5
|
||||
const uint8_t* encryptedData = manufacturerData + 5;
|
||||
size_t encryptedLen = len - 5;
|
||||
|
||||
bool VictronBLE::parseAdvertisement(DeviceInfo* deviceInfo, const victronManufacturerData& mfgData) {
|
||||
if (debugEnabled) {
|
||||
debugPrintHex("Encrypted data", encryptedData, encryptedLen);
|
||||
debugPrintHex("IV", iv, 16);
|
||||
debugPrint("Vendor ID: 0x" + String(mfgData.vendorID, HEX));
|
||||
debugPrint("Beacon Type: 0x" + String(mfgData.beaconType, HEX));
|
||||
debugPrint("Record Type: 0x" + String(mfgData.victronRecordType, HEX));
|
||||
debugPrint("Nonce: 0x" + String(mfgData.nonceDataCounter, HEX));
|
||||
}
|
||||
|
||||
|
||||
// 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] = mfgData.nonceDataCounter & 0xFF; // Low byte
|
||||
iv[1] = (mfgData.nonceDataCounter >> 8) & 0xFF; // High byte
|
||||
// Remaining bytes stay zero
|
||||
|
||||
// Decrypt the data
|
||||
uint8_t decrypted[32]; // Max expected size
|
||||
if (!decryptAdvertisement(encryptedData, encryptedLen,
|
||||
const size_t encryptedLen = sizeof(mfgData.victronEncryptedData);
|
||||
uint8_t decrypted[encryptedLen];
|
||||
if (!decryptAdvertisement(mfgData.victronEncryptedData,
|
||||
encryptedLen,
|
||||
deviceInfo->encryptionKeyBytes, iv, decrypted)) {
|
||||
lastError = "Decryption failed";
|
||||
if (debugEnabled) debugPrint(lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (debugEnabled) {
|
||||
debugPrintHex("Decrypted data", decrypted, encryptedLen);
|
||||
}
|
||||
|
||||
|
||||
// Parse based on device type
|
||||
bool parseOk = false;
|
||||
|
||||
switch (deviceType) {
|
||||
|
||||
switch (mfgData.victronRecordType) {
|
||||
case DEVICE_TYPE_SOLAR_CHARGER:
|
||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
|
||||
parseOk = parseSolarCharger(decrypted, encryptedLen,
|
||||
*(SolarChargerData*)deviceInfo->data);
|
||||
parseOk = parseSolarCharger(decrypted, encryptedLen,
|
||||
*static_cast<SolarChargerData*>(deviceInfo->data));
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case DEVICE_TYPE_BATTERY_MONITOR:
|
||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) {
|
||||
parseOk = parseBatteryMonitor(decrypted, encryptedLen,
|
||||
*(BatteryMonitorData*)deviceInfo->data);
|
||||
*static_cast<BatteryMonitorData*>(deviceInfo->data));
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case DEVICE_TYPE_INVERTER:
|
||||
case DEVICE_TYPE_INVERTER_RS:
|
||||
case DEVICE_TYPE_MULTI_RS:
|
||||
case DEVICE_TYPE_VE_BUS:
|
||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_INVERTER) {
|
||||
parseOk = parseInverter(decrypted, encryptedLen,
|
||||
*(InverterData*)deviceInfo->data);
|
||||
*static_cast<InverterData*>(deviceInfo->data));
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case DEVICE_TYPE_DCDC_CONVERTER:
|
||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) {
|
||||
parseOk = parseDCDCConverter(decrypted, encryptedLen,
|
||||
*(DCDCConverterData*)deviceInfo->data);
|
||||
*static_cast<DCDCConverterData*>(deviceInfo->data));
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
debugPrint("Unknown device type: 0x" + String(deviceType, HEX));
|
||||
if (debugEnabled) debugPrint("Unknown device type: 0x" + String(mfgData.victronRecordType, HEX));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (parseOk && deviceInfo->data) {
|
||||
deviceInfo->data->dataValid = true;
|
||||
|
||||
|
||||
// Call appropriate callback
|
||||
if (callback) {
|
||||
switch (deviceType) {
|
||||
switch (mfgData.victronRecordType) {
|
||||
case DEVICE_TYPE_SOLAR_CHARGER:
|
||||
callback->onSolarChargerData(*(SolarChargerData*)deviceInfo->data);
|
||||
callback->onSolarChargerData(*static_cast<SolarChargerData*>(deviceInfo->data));
|
||||
break;
|
||||
case DEVICE_TYPE_BATTERY_MONITOR:
|
||||
callback->onBatteryMonitorData(*(BatteryMonitorData*)deviceInfo->data);
|
||||
callback->onBatteryMonitorData(*static_cast<BatteryMonitorData*>(deviceInfo->data));
|
||||
break;
|
||||
case DEVICE_TYPE_INVERTER:
|
||||
case DEVICE_TYPE_INVERTER_RS:
|
||||
case DEVICE_TYPE_MULTI_RS:
|
||||
case DEVICE_TYPE_VE_BUS:
|
||||
callback->onInverterData(*(InverterData*)deviceInfo->data);
|
||||
callback->onInverterData(*static_cast<InverterData*>(deviceInfo->data));
|
||||
break;
|
||||
case DEVICE_TYPE_DCDC_CONVERTER:
|
||||
callback->onDCDCConverterData(*(DCDCConverterData*)deviceInfo->data);
|
||||
callback->onDCDCConverterData(*static_cast<DCDCConverterData*>(deviceInfo->data));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return parseOk;
|
||||
}
|
||||
|
||||
@@ -297,244 +313,257 @@ bool VictronBLE::decryptAdvertisement(const uint8_t* encrypted, size_t encLen,
|
||||
uint8_t* decrypted) {
|
||||
mbedtls_aes_context aes;
|
||||
mbedtls_aes_init(&aes);
|
||||
|
||||
|
||||
// Set encryption key
|
||||
int ret = mbedtls_aes_setkey_enc(&aes, key, 128);
|
||||
if (ret != 0) {
|
||||
mbedtls_aes_free(&aes);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// AES-CTR decryption
|
||||
size_t nc_off = 0;
|
||||
uint8_t nonce_counter[16];
|
||||
uint8_t stream_block[16];
|
||||
|
||||
|
||||
memcpy(nonce_counter, iv, 16);
|
||||
memset(stream_block, 0, 16);
|
||||
|
||||
|
||||
ret = mbedtls_aes_crypt_ctr(&aes, encLen, &nc_off, nonce_counter,
|
||||
stream_block, encrypted, decrypted);
|
||||
|
||||
|
||||
mbedtls_aes_free(&aes);
|
||||
|
||||
|
||||
return (ret == 0);
|
||||
}
|
||||
|
||||
// Parse Solar Charger data
|
||||
bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarChargerData& result) {
|
||||
if (len < 12) {
|
||||
debugPrint("Solar charger data too short");
|
||||
if (len < sizeof(victronSolarChargerPayload)) {
|
||||
if (debugEnabled) debugPrint("Solar charger data too short: " + String(len) + " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Byte 0: Charge state
|
||||
result.chargeState = (SolarChargerState)data[0];
|
||||
|
||||
// Bytes 1-2: Battery voltage (10 mV units)
|
||||
uint16_t vBat = data[1] | (data[2] << 8);
|
||||
result.batteryVoltage = vBat * 0.01f;
|
||||
|
||||
// Bytes 3-4: Battery current (10 mA units, signed)
|
||||
int16_t iBat = (int16_t)(data[3] | (data[4] << 8));
|
||||
result.batteryCurrent = iBat * 0.01f;
|
||||
|
||||
// Bytes 5-6: Yield today (10 Wh units)
|
||||
uint16_t yield = data[5] | (data[6] << 8);
|
||||
result.yieldToday = yield * 10;
|
||||
|
||||
// Bytes 7-8: PV power (1 W units)
|
||||
uint16_t pvPower = data[7] | (data[8] << 8);
|
||||
result.panelPower = pvPower;
|
||||
|
||||
// Bytes 9-10: Load current (10 mA units)
|
||||
uint16_t iLoad = data[9] | (data[10] << 8);
|
||||
if (iLoad != 0xFFFF) { // 0xFFFF means no load output
|
||||
result.loadCurrent = iLoad * 0.01f;
|
||||
|
||||
const auto* payload = reinterpret_cast<const victronSolarChargerPayload*>(data);
|
||||
|
||||
// Parse charge state
|
||||
result.chargeState = static_cast<SolarChargerState>(payload->deviceState);
|
||||
|
||||
// Parse battery voltage (10 mV units -> volts)
|
||||
result.batteryVoltage = payload->batteryVoltage * 0.01f;
|
||||
|
||||
// Parse battery current (10 mA units, signed -> amps)
|
||||
result.batteryCurrent = payload->batteryCurrent * 0.01f;
|
||||
|
||||
// Parse yield today (10 Wh units -> Wh)
|
||||
result.yieldToday = payload->yieldToday * 10;
|
||||
|
||||
// Parse PV power (1 W units)
|
||||
result.panelPower = payload->inputPower;
|
||||
|
||||
// Parse load current (10 mA units -> amps, 0xFFFF = no load)
|
||||
if (payload->loadCurrent != 0xFFFF) {
|
||||
result.loadCurrent = payload->loadCurrent * 0.01f;
|
||||
} else {
|
||||
result.loadCurrent = 0;
|
||||
}
|
||||
|
||||
|
||||
// Calculate PV voltage from power and current (if current > 0)
|
||||
if (result.batteryCurrent > 0.1f) {
|
||||
result.panelVoltage = result.panelPower / result.batteryCurrent;
|
||||
} else {
|
||||
result.panelVoltage = 0;
|
||||
}
|
||||
|
||||
debugPrint("Solar Charger: " + String(result.batteryVoltage, 2) + "V, " +
|
||||
String(result.batteryCurrent, 2) + "A, " +
|
||||
String(result.panelPower) + "W, State: " + String(result.chargeState));
|
||||
|
||||
|
||||
if (debugEnabled) {
|
||||
debugPrint("Solar Charger: " + String(result.batteryVoltage, 2) + "V, " +
|
||||
String(result.batteryCurrent, 2) + "A, " +
|
||||
String(result.panelPower) + "W, State: " + String(result.chargeState));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse Battery Monitor data
|
||||
// Parse Battery Monitor data
|
||||
bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result) {
|
||||
if (len < 15) {
|
||||
debugPrint("Battery monitor data too short");
|
||||
if (len < sizeof(victronBatteryMonitorPayload)) {
|
||||
if (debugEnabled) debugPrint("Battery monitor data too short: " + String(len) + " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bytes 0-1: Remaining time (1 minute units)
|
||||
uint16_t timeRemaining = data[0] | (data[1] << 8);
|
||||
result.remainingMinutes = timeRemaining;
|
||||
|
||||
// Bytes 2-3: Battery voltage (10 mV units)
|
||||
uint16_t vBat = data[2] | (data[3] << 8);
|
||||
result.voltage = vBat * 0.01f;
|
||||
|
||||
// Byte 4: Alarms
|
||||
uint8_t alarms = data[4];
|
||||
result.alarmLowVoltage = (alarms & 0x01) != 0;
|
||||
result.alarmHighVoltage = (alarms & 0x02) != 0;
|
||||
result.alarmLowSOC = (alarms & 0x04) != 0;
|
||||
result.alarmLowTemperature = (alarms & 0x10) != 0;
|
||||
result.alarmHighTemperature = (alarms & 0x20) != 0;
|
||||
|
||||
// Bytes 5-6: Aux voltage/temperature (10 mV or 0.01K units)
|
||||
uint16_t aux = data[5] | (data[6] << 8);
|
||||
if (aux < 3000) { // If < 30V, it's voltage
|
||||
result.auxVoltage = aux * 0.01f;
|
||||
|
||||
const auto* payload = reinterpret_cast<const victronBatteryMonitorPayload*>(data);
|
||||
|
||||
// Parse remaining time (1 minute units)
|
||||
result.remainingMinutes = payload->remainingMins;
|
||||
|
||||
// Parse battery voltage (10 mV units -> volts)
|
||||
result.voltage = payload->batteryVoltage * 0.01f;
|
||||
|
||||
// Parse alarm bits
|
||||
result.alarmLowVoltage = (payload->alarms & 0x01) != 0;
|
||||
result.alarmHighVoltage = (payload->alarms & 0x02) != 0;
|
||||
result.alarmLowSOC = (payload->alarms & 0x04) != 0;
|
||||
result.alarmLowTemperature = (payload->alarms & 0x10) != 0;
|
||||
result.alarmHighTemperature = (payload->alarms & 0x20) != 0;
|
||||
|
||||
// Parse aux data: voltage (10 mV units) or temperature (0.01K units)
|
||||
if (payload->auxData < 3000) { // If < 30V, it's voltage
|
||||
result.auxVoltage = payload->auxData * 0.01f;
|
||||
result.temperature = 0;
|
||||
} 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;
|
||||
}
|
||||
|
||||
// Bytes 7-9: Battery current (22-bit signed, 1 mA units)
|
||||
int32_t current = data[7] | (data[8] << 8) | ((data[9] & 0x3F) << 16);
|
||||
if (current & 0x200000) { // Sign extend if negative
|
||||
|
||||
// Parse battery current (22-bit signed, 1 mA units)
|
||||
int32_t current = payload->currentLow |
|
||||
(payload->currentMid << 8) |
|
||||
((payload->currentHigh_consumedLow & 0x3F) << 16);
|
||||
// Sign extend from 22 bits to 32 bits
|
||||
if (current & 0x200000) {
|
||||
current |= 0xFFC00000;
|
||||
}
|
||||
result.current = current * 0.001f;
|
||||
|
||||
// Bytes 9-11: Consumed Ah (18-bit signed, 10 mAh units)
|
||||
int32_t consumedAh = ((data[9] & 0xC0) >> 6) | (data[10] << 2) | ((data[11] & 0xFF) << 10);
|
||||
if (consumedAh & 0x20000) { // Sign extend
|
||||
result.current = current * 0.001f; // Convert mA to A
|
||||
|
||||
// Parse consumed Ah (18-bit signed, 10 mAh units)
|
||||
int32_t consumedAh = ((payload->currentHigh_consumedLow & 0xC0) >> 6) |
|
||||
(payload->consumedMid << 2) |
|
||||
(payload->consumedHigh << 10);
|
||||
// Sign extend from 18 bits to 32 bits
|
||||
if (consumedAh & 0x20000) {
|
||||
consumedAh |= 0xFFFC0000;
|
||||
}
|
||||
result.consumedAh = consumedAh * 0.01f;
|
||||
|
||||
// Bytes 12-13: SOC (10 = 1.0%)
|
||||
uint16_t soc = data[12] | ((data[13] & 0x03) << 8);
|
||||
result.soc = soc * 0.1f;
|
||||
|
||||
debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " +
|
||||
String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%");
|
||||
|
||||
result.consumedAh = consumedAh * 0.01f; // Convert 10mAh to Ah
|
||||
|
||||
// Parse SOC (10-bit value, 10 = 1.0%)
|
||||
result.soc = (payload->soc & 0x3FF) * 0.1f;
|
||||
|
||||
if (debugEnabled) {
|
||||
debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " +
|
||||
String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse Inverter data
|
||||
bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& result) {
|
||||
if (len < 10) {
|
||||
debugPrint("Inverter data too short");
|
||||
if (len < sizeof(victronInverterPayload)) {
|
||||
if (debugEnabled) debugPrint("Inverter data too short: " + String(len) + " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Byte 0: Device state
|
||||
result.state = data[0];
|
||||
|
||||
// Bytes 1-2: Battery voltage (10 mV units)
|
||||
uint16_t vBat = data[1] | (data[2] << 8);
|
||||
result.batteryVoltage = vBat * 0.01f;
|
||||
|
||||
// Bytes 3-4: Battery current (10 mA units, signed)
|
||||
int16_t iBat = (int16_t)(data[3] | (data[4] << 8));
|
||||
result.batteryCurrent = iBat * 0.01f;
|
||||
|
||||
// Bytes 5-7: AC Power (1 W units, signed 24-bit)
|
||||
int32_t acPower = data[5] | (data[6] << 8) | (data[7] << 16);
|
||||
if (acPower & 0x800000) { // Sign extend
|
||||
|
||||
const auto* payload = reinterpret_cast<const victronInverterPayload*>(data);
|
||||
|
||||
// Parse device state
|
||||
result.state = payload->deviceState;
|
||||
|
||||
// Parse battery voltage (10 mV units -> volts)
|
||||
result.batteryVoltage = payload->batteryVoltage * 0.01f;
|
||||
|
||||
// Parse battery current (10 mA units, signed -> amps)
|
||||
result.batteryCurrent = payload->batteryCurrent * 0.01f;
|
||||
|
||||
// Parse AC Power (signed 24-bit, 1 W units)
|
||||
int32_t acPower = payload->acPowerLow |
|
||||
(payload->acPowerMid << 8) |
|
||||
(payload->acPowerHigh << 16);
|
||||
// Sign extend from 24 bits to 32 bits
|
||||
if (acPower & 0x800000) {
|
||||
acPower |= 0xFF000000;
|
||||
}
|
||||
result.acPower = acPower;
|
||||
|
||||
// Byte 8: Alarms
|
||||
uint8_t alarms = data[8];
|
||||
result.alarmLowVoltage = (alarms & 0x01) != 0;
|
||||
result.alarmHighVoltage = (alarms & 0x02) != 0;
|
||||
result.alarmHighTemperature = (alarms & 0x04) != 0;
|
||||
result.alarmOverload = (alarms & 0x08) != 0;
|
||||
|
||||
debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " +
|
||||
String(result.acPower) + "W, State: " + String(result.state));
|
||||
|
||||
|
||||
// Parse alarm bits
|
||||
result.alarmLowVoltage = (payload->alarms & 0x01) != 0;
|
||||
result.alarmHighVoltage = (payload->alarms & 0x02) != 0;
|
||||
result.alarmHighTemperature = (payload->alarms & 0x04) != 0;
|
||||
result.alarmOverload = (payload->alarms & 0x08) != 0;
|
||||
|
||||
if (debugEnabled) {
|
||||
debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " +
|
||||
String(result.acPower) + "W, State: " + String(result.state));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse DC-DC Converter data
|
||||
bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result) {
|
||||
if (len < 10) {
|
||||
debugPrint("DC-DC converter data too short");
|
||||
if (len < sizeof(victronDCDCConverterPayload)) {
|
||||
if (debugEnabled) debugPrint("DC-DC converter data too short: " + String(len) + " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Byte 0: Charge state
|
||||
result.chargeState = data[0];
|
||||
|
||||
// Bytes 1-2: Input voltage (10 mV units)
|
||||
uint16_t vIn = data[1] | (data[2] << 8);
|
||||
result.inputVoltage = vIn * 0.01f;
|
||||
|
||||
// Bytes 3-4: Output voltage (10 mV units)
|
||||
uint16_t vOut = data[3] | (data[4] << 8);
|
||||
result.outputVoltage = vOut * 0.01f;
|
||||
|
||||
// Bytes 5-6: Output current (10 mA units)
|
||||
uint16_t iOut = data[5] | (data[6] << 8);
|
||||
result.outputCurrent = iOut * 0.01f;
|
||||
|
||||
// Byte 7: Error code
|
||||
result.errorCode = data[7];
|
||||
|
||||
debugPrint("DC-DC Converter: In=" + String(result.inputVoltage, 2) + "V, Out=" +
|
||||
String(result.outputVoltage, 2) + "V, " + String(result.outputCurrent, 2) + "A");
|
||||
|
||||
|
||||
const auto* payload = reinterpret_cast<const victronDCDCConverterPayload*>(data);
|
||||
|
||||
// Parse charge state
|
||||
result.chargeState = payload->chargeState;
|
||||
|
||||
// Parse error code
|
||||
result.errorCode = payload->errorCode;
|
||||
|
||||
// Parse input voltage (10 mV units -> volts)
|
||||
result.inputVoltage = payload->inputVoltage * 0.01f;
|
||||
|
||||
// Parse output voltage (10 mV units -> volts)
|
||||
result.outputVoltage = payload->outputVoltage * 0.01f;
|
||||
|
||||
// Parse output current (10 mA units -> amps)
|
||||
result.outputCurrent = payload->outputCurrent * 0.01f;
|
||||
|
||||
if (debugEnabled) {
|
||||
debugPrint("DC-DC Converter: In=" + String(result.inputVoltage, 2) + "V, Out=" +
|
||||
String(result.outputVoltage, 2) + "V, " + String(result.outputCurrent, 2) + "A");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get data methods
|
||||
bool VictronBLE::getSolarChargerData(String macAddress, SolarChargerData& data) {
|
||||
bool VictronBLE::getSolarChargerData(const String& macAddress, SolarChargerData& data) {
|
||||
String normalizedMAC = normalizeMAC(macAddress);
|
||||
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) {
|
||||
data = *(SolarChargerData*)it->second->data;
|
||||
data = *static_cast<SolarChargerData*>(it->second->data);
|
||||
return data.dataValid;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool VictronBLE::getBatteryMonitorData(String macAddress, BatteryMonitorData& data) {
|
||||
bool VictronBLE::getBatteryMonitorData(const String& macAddress, BatteryMonitorData& data) {
|
||||
String normalizedMAC = normalizeMAC(macAddress);
|
||||
auto it = devices.find(normalizedMAC);
|
||||
|
||||
|
||||
if (it != devices.end() && it->second->data &&
|
||||
it->second->data->deviceType == DEVICE_TYPE_BATTERY_MONITOR) {
|
||||
data = *(BatteryMonitorData*)it->second->data;
|
||||
data = *static_cast<BatteryMonitorData*>(it->second->data);
|
||||
return data.dataValid;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool VictronBLE::getInverterData(String macAddress, InverterData& data) {
|
||||
bool VictronBLE::getInverterData(const String& macAddress, InverterData& data) {
|
||||
String normalizedMAC = normalizeMAC(macAddress);
|
||||
auto it = devices.find(normalizedMAC);
|
||||
|
||||
|
||||
if (it != devices.end() && it->second->data &&
|
||||
it->second->data->deviceType == DEVICE_TYPE_INVERTER) {
|
||||
data = *(InverterData*)it->second->data;
|
||||
data = *static_cast<InverterData*>(it->second->data);
|
||||
return data.dataValid;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool VictronBLE::getDCDCConverterData(String macAddress, DCDCConverterData& data) {
|
||||
bool VictronBLE::getDCDCConverterData(const String& macAddress, DCDCConverterData& data) {
|
||||
String normalizedMAC = normalizeMAC(macAddress);
|
||||
auto it = devices.find(normalizedMAC);
|
||||
|
||||
|
||||
if (it != devices.end() && it->second->data &&
|
||||
it->second->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) {
|
||||
data = *(DCDCConverterData*)it->second->data;
|
||||
data = *static_cast<DCDCConverterData*>(it->second->data);
|
||||
return data.dataValid;
|
||||
}
|
||||
return false;
|
||||
@@ -543,13 +572,13 @@ bool VictronBLE::getDCDCConverterData(String macAddress, DCDCConverterData& data
|
||||
// Get devices by type
|
||||
std::vector<String> VictronBLE::getDevicesByType(VictronDeviceType type) {
|
||||
std::vector<String> result;
|
||||
|
||||
|
||||
for (const auto& pair : devices) {
|
||||
if (pair.second->data && pair.second->data->deviceType == type) {
|
||||
result.push_back(pair.first);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -577,7 +606,7 @@ bool VictronBLE::hexStringToBytes(const String& hex, uint8_t* bytes, size_t len)
|
||||
if (hex.length() != len * 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
String byteStr = hex.substring(i * 2, i * 2 + 2);
|
||||
char* endPtr;
|
||||
@@ -586,45 +615,26 @@ bool VictronBLE::hexStringToBytes(const String& hex, uint8_t* bytes, size_t len)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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);
|
||||
return String(address.toString().c_str());
|
||||
}
|
||||
|
||||
// Helper: Normalize MAC address format
|
||||
String VictronBLE::normalizeMAC(String mac) {
|
||||
String VictronBLE::normalizeMAC(const String& mac) {
|
||||
String normalized = mac;
|
||||
normalized.toLowerCase();
|
||||
normalized.replace("-", ":");
|
||||
normalized.replace("-", "");
|
||||
normalized.replace(":", "");
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// Debug helpers
|
||||
// Debug helper
|
||||
void VictronBLE::debugPrint(const String& message) {
|
||||
if (debugEnabled) {
|
||||
if (debugEnabled)
|
||||
Serial.println("[VictronBLE] " + message);
|
||||
}
|
||||
}
|
||||
|
||||
void VictronBLE::debugPrintHex(const char* label, const uint8_t* data, size_t len) {
|
||||
if (!debugEnabled) return;
|
||||
|
||||
Serial.print("[VictronBLE] ");
|
||||
Serial.print(label);
|
||||
Serial.print(": ");
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (data[i] < 0x10) Serial.print("0");
|
||||
Serial.print(data[i], HEX);
|
||||
Serial.print(" ");
|
||||
}
|
||||
Serial.println();
|
||||
}
|
||||
|
||||
158
src/VictronBLE.h
158
src/VictronBLE.h
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* VictronBLE - ESP32 library for Victron Energy BLE devices
|
||||
*
|
||||
*
|
||||
* Based on Victron's official BLE Advertising protocol documentation
|
||||
* Inspired by hoberman's examples and keshavdv's Python library
|
||||
*
|
||||
*
|
||||
* Copyright (c) 2025 Scott Penrose
|
||||
* License: MIT
|
||||
*/
|
||||
@@ -20,7 +20,7 @@
|
||||
#include "mbedtls/aes.h"
|
||||
|
||||
// Victron manufacturer ID
|
||||
#define VICTRON_MANUFACTURER_ID 0x02E1
|
||||
static constexpr uint16_t VICTRON_MANUFACTURER_ID = 0x02E1;
|
||||
|
||||
// Device type IDs from Victron protocol
|
||||
enum VictronDeviceType {
|
||||
@@ -53,6 +53,72 @@ enum SolarChargerState {
|
||||
CHARGER_EXTERNAL_CONTROL = 252
|
||||
};
|
||||
|
||||
// Binary data structures for decoding BLE advertisements
|
||||
// Must use __attribute__((packed)) to prevent compiler padding
|
||||
|
||||
// Manufacturer data structure (outer envelope)
|
||||
struct victronManufacturerData {
|
||||
uint16_t vendorID; // vendor ID
|
||||
uint8_t beaconType; // Should be 0x10 (Product Advertisement) for the packets we want
|
||||
uint8_t unknownData1[3]; // Unknown data
|
||||
uint8_t victronRecordType; // Should be 0x01 (Solar Charger) for the packets we want
|
||||
uint16_t nonceDataCounter; // Nonce
|
||||
uint8_t encryptKeyMatch; // Should match pre-shared encryption key byte 0
|
||||
uint8_t victronEncryptedData[21]; // (31 bytes max per BLE spec - size of previous elements)
|
||||
uint8_t nullPad; // extra byte because toCharArray() adds a \0 byte.
|
||||
} __attribute__((packed));
|
||||
// Decrypted payload structures for each device type
|
||||
|
||||
// Solar Charger decrypted payload
|
||||
struct victronSolarChargerPayload {
|
||||
uint8_t deviceState; // Charge state (SolarChargerState enum)
|
||||
uint8_t errorCode; // Error code
|
||||
int16_t batteryVoltage; // Battery voltage in 10mV units
|
||||
int16_t batteryCurrent; // Battery current in 10mA units (signed)
|
||||
uint16_t yieldToday; // Yield today in 10Wh units
|
||||
uint16_t inputPower; // PV power in 1W units
|
||||
uint16_t loadCurrent; // Load current in 10mA units (0xFFFF = no load)
|
||||
uint8_t reserved[2]; // Reserved bytes
|
||||
} __attribute__((packed));
|
||||
|
||||
// Battery Monitor decrypted payload
|
||||
struct victronBatteryMonitorPayload {
|
||||
uint16_t remainingMins; // Time remaining in minutes
|
||||
uint16_t batteryVoltage; // Battery voltage in 10mV units
|
||||
uint8_t alarms; // Alarm bits
|
||||
uint16_t auxData; // Aux voltage (10mV) or temperature (0.01K)
|
||||
uint8_t currentLow; // Battery current bits 0-7
|
||||
uint8_t currentMid; // Battery current bits 8-15
|
||||
uint8_t currentHigh_consumedLow; // Current bits 16-21 (low 6 bits), consumed bits 0-1 (high 2 bits)
|
||||
uint8_t consumedMid; // Consumed Ah bits 2-9
|
||||
uint8_t consumedHigh; // Consumed Ah bits 10-17
|
||||
uint16_t soc; // State of charge in 0.1% units (10-bit value)
|
||||
uint8_t reserved[2]; // Reserved bytes
|
||||
} __attribute__((packed));
|
||||
|
||||
// Inverter decrypted payload
|
||||
struct victronInverterPayload {
|
||||
uint8_t deviceState; // Device state
|
||||
uint8_t errorCode; // Error code
|
||||
uint16_t batteryVoltage; // Battery voltage in 10mV units
|
||||
int16_t batteryCurrent; // Battery current in 10mA units (signed)
|
||||
uint8_t acPowerLow; // AC Power bits 0-7
|
||||
uint8_t acPowerMid; // AC Power bits 8-15
|
||||
uint8_t acPowerHigh; // AC Power bits 16-23 (signed 24-bit)
|
||||
uint8_t alarms; // Alarm bits
|
||||
uint8_t reserved[4]; // Reserved bytes
|
||||
} __attribute__((packed));
|
||||
|
||||
// DC-DC Converter decrypted payload
|
||||
struct victronDCDCConverterPayload {
|
||||
uint8_t chargeState; // Charge state
|
||||
uint8_t errorCode; // Error code
|
||||
uint16_t inputVoltage; // Input voltage in 10mV units
|
||||
uint16_t outputVoltage; // Output voltage in 10mV units
|
||||
uint16_t outputCurrent; // Output current in 10mA units
|
||||
uint8_t reserved[6]; // Reserved bytes
|
||||
} __attribute__((packed));
|
||||
|
||||
// Base structure for all device data
|
||||
struct VictronDeviceData {
|
||||
String deviceName;
|
||||
@@ -61,8 +127,8 @@ struct VictronDeviceData {
|
||||
int8_t rssi;
|
||||
uint32_t lastUpdate;
|
||||
bool dataValid;
|
||||
|
||||
VictronDeviceData() : deviceType(DEVICE_TYPE_UNKNOWN), rssi(-100),
|
||||
|
||||
VictronDeviceData() : deviceType(DEVICE_TYPE_UNKNOWN), rssi(-100),
|
||||
lastUpdate(0), dataValid(false) {}
|
||||
};
|
||||
|
||||
@@ -75,8 +141,8 @@ struct SolarChargerData : public VictronDeviceData {
|
||||
float panelPower; // W
|
||||
uint16_t yieldToday; // Wh
|
||||
float loadCurrent; // A
|
||||
|
||||
SolarChargerData() : chargeState(CHARGER_OFF), batteryVoltage(0),
|
||||
|
||||
SolarChargerData() : chargeState(CHARGER_OFF), batteryVoltage(0),
|
||||
batteryCurrent(0), panelVoltage(0), panelPower(0),
|
||||
yieldToday(0), loadCurrent(0) {
|
||||
deviceType = DEVICE_TYPE_SOLAR_CHARGER;
|
||||
@@ -97,8 +163,8 @@ struct BatteryMonitorData : public VictronDeviceData {
|
||||
bool alarmLowSOC;
|
||||
bool alarmLowTemperature;
|
||||
bool alarmHighTemperature;
|
||||
|
||||
BatteryMonitorData() : voltage(0), current(0), temperature(0),
|
||||
|
||||
BatteryMonitorData() : voltage(0), current(0), temperature(0),
|
||||
auxVoltage(0), remainingMinutes(0), consumedAh(0),
|
||||
soc(0), alarmLowVoltage(false), alarmHighVoltage(false),
|
||||
alarmLowSOC(false), alarmLowTemperature(false),
|
||||
@@ -117,7 +183,7 @@ struct InverterData : public VictronDeviceData {
|
||||
bool alarmLowVoltage;
|
||||
bool alarmHighTemperature;
|
||||
bool alarmOverload;
|
||||
|
||||
|
||||
InverterData() : batteryVoltage(0), batteryCurrent(0), acPower(0),
|
||||
state(0), alarmHighVoltage(false), alarmLowVoltage(false),
|
||||
alarmHighTemperature(false), alarmOverload(false) {
|
||||
@@ -132,15 +198,16 @@ struct DCDCConverterData : public VictronDeviceData {
|
||||
float outputCurrent; // A
|
||||
uint8_t chargeState;
|
||||
uint8_t errorCode;
|
||||
|
||||
|
||||
DCDCConverterData() : inputVoltage(0), outputVoltage(0), outputCurrent(0),
|
||||
chargeState(0), errorCode(0) {
|
||||
deviceType = DEVICE_TYPE_DCDC_CONVERTER;
|
||||
}
|
||||
};
|
||||
|
||||
// Forward declaration
|
||||
// Forward declarations
|
||||
class VictronBLE;
|
||||
class VictronBLEAdvertisedDeviceCallbacks;
|
||||
|
||||
// Callback interface for device data updates
|
||||
class VictronDeviceCallback {
|
||||
@@ -158,9 +225,9 @@ struct VictronDeviceConfig {
|
||||
String macAddress;
|
||||
String encryptionKey; // 32 character hex string
|
||||
VictronDeviceType expectedType;
|
||||
|
||||
|
||||
VictronDeviceConfig() : expectedType(DEVICE_TYPE_UNKNOWN) {}
|
||||
VictronDeviceConfig(String n, String mac, String key, VictronDeviceType type = DEVICE_TYPE_UNKNOWN)
|
||||
VictronDeviceConfig(const String& n, const String& mac, const String& key, VictronDeviceType type = DEVICE_TYPE_UNKNOWN)
|
||||
: name(n), macAddress(mac), encryptionKey(key), expectedType(type) {}
|
||||
};
|
||||
|
||||
@@ -169,86 +236,87 @@ class VictronBLE {
|
||||
public:
|
||||
VictronBLE();
|
||||
~VictronBLE();
|
||||
|
||||
|
||||
// Initialize BLE and start scanning
|
||||
bool begin(uint32_t scanDuration = 5);
|
||||
|
||||
|
||||
// Add a device to monitor
|
||||
bool addDevice(const VictronDeviceConfig& config);
|
||||
bool addDevice(String name, String macAddress, String encryptionKey,
|
||||
bool addDevice(const String& name, const String& macAddress, const String& encryptionKey,
|
||||
VictronDeviceType expectedType = DEVICE_TYPE_UNKNOWN);
|
||||
|
||||
|
||||
// Remove a device
|
||||
void removeDevice(String macAddress);
|
||||
|
||||
void removeDevice(const String& macAddress);
|
||||
|
||||
// Get device count
|
||||
size_t getDeviceCount() const { return devices.size(); }
|
||||
|
||||
|
||||
// Set callback for data updates
|
||||
void setCallback(VictronDeviceCallback* cb) { callback = cb; }
|
||||
|
||||
|
||||
// Process scanning (call in loop())
|
||||
void loop();
|
||||
|
||||
|
||||
// Get latest data for a device
|
||||
bool getSolarChargerData(String macAddress, SolarChargerData& data);
|
||||
bool getBatteryMonitorData(String macAddress, BatteryMonitorData& data);
|
||||
bool getInverterData(String macAddress, InverterData& data);
|
||||
bool getDCDCConverterData(String macAddress, DCDCConverterData& data);
|
||||
|
||||
bool getSolarChargerData(const String& macAddress, SolarChargerData& data);
|
||||
bool getBatteryMonitorData(const String& macAddress, BatteryMonitorData& data);
|
||||
bool getInverterData(const String& macAddress, InverterData& data);
|
||||
bool getDCDCConverterData(const String& macAddress, DCDCConverterData& data);
|
||||
|
||||
// Get all devices of a specific type
|
||||
std::vector<String> getDevicesByType(VictronDeviceType type);
|
||||
|
||||
|
||||
// Enable/disable debug output
|
||||
void setDebug(bool enable) { debugEnabled = enable; }
|
||||
|
||||
|
||||
// Get last error message
|
||||
String getLastError() const { return lastError; }
|
||||
|
||||
|
||||
private:
|
||||
friend class VictronBLEAdvertisedDeviceCallbacks;
|
||||
|
||||
|
||||
struct DeviceInfo {
|
||||
VictronDeviceConfig config;
|
||||
VictronDeviceData* data;
|
||||
uint8_t encryptionKeyBytes[16];
|
||||
|
||||
|
||||
DeviceInfo() : data(nullptr) {
|
||||
memset(encryptionKeyBytes, 0, 16);
|
||||
}
|
||||
~DeviceInfo() {
|
||||
if (data) delete data;
|
||||
}
|
||||
DeviceInfo(const DeviceInfo&) = delete;
|
||||
DeviceInfo& operator=(const DeviceInfo&) = delete;
|
||||
};
|
||||
|
||||
|
||||
std::map<String, DeviceInfo*> devices;
|
||||
BLEScan* pBLEScan;
|
||||
VictronBLEAdvertisedDeviceCallbacks* scanCallback;
|
||||
VictronDeviceCallback* callback;
|
||||
bool debugEnabled;
|
||||
String lastError;
|
||||
uint32_t scanDuration;
|
||||
bool initialized;
|
||||
|
||||
|
||||
// Internal methods
|
||||
bool hexStringToBytes(const String& hex, uint8_t* bytes, size_t len);
|
||||
bool decryptAdvertisement(const uint8_t* encrypted, size_t encLen,
|
||||
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);
|
||||
void processDevice(BLEAdvertisedDevice advertisedDevice);
|
||||
|
||||
bool parseAdvertisement(DeviceInfo* deviceInfo, const victronManufacturerData& mfgData);
|
||||
void processDevice(BLEAdvertisedDevice& advertisedDevice);
|
||||
|
||||
VictronDeviceData* createDeviceData(VictronDeviceType type);
|
||||
bool parseSolarCharger(const uint8_t* data, size_t len, SolarChargerData& result);
|
||||
bool parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result);
|
||||
bool parseInverter(const uint8_t* data, size_t len, InverterData& result);
|
||||
bool parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result);
|
||||
|
||||
|
||||
void debugPrint(const String& message);
|
||||
void debugPrintHex(const char* label, const uint8_t* data, size_t len);
|
||||
|
||||
|
||||
String macAddressToString(BLEAddress address);
|
||||
String normalizeMAC(String mac);
|
||||
String normalizeMAC(const String& mac);
|
||||
};
|
||||
|
||||
// BLE scan callback class
|
||||
@@ -256,7 +324,7 @@ class VictronBLEAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
|
||||
public:
|
||||
VictronBLEAdvertisedDeviceCallbacks(VictronBLE* parent) : victronBLE(parent) {}
|
||||
void onResult(BLEAdvertisedDevice advertisedDevice) override;
|
||||
|
||||
|
||||
private:
|
||||
VictronBLE* victronBLE;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user