Compare commits
5 Commits
v0.3.1
...
8a4e010324
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a4e010324 | |||
| 977641b093 | |||
| 24712c206a | |||
| 8a2402cb63 | |||
| a64fef899b |
@@ -102,3 +102,99 @@ Arduino/ESP32 library for reading Victron Energy devices via Bluetooth Low Energ
|
||||
- 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
|
||||
|
||||
|
||||
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
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -31,6 +31,11 @@
|
||||
"name": "Logger",
|
||||
"base": "examples/Logger",
|
||||
"files": ["src/main.cpp"]
|
||||
},
|
||||
{
|
||||
"name": "Repeater",
|
||||
"base": "examples/Repeater",
|
||||
"files": ["src/main.cpp"]
|
||||
}
|
||||
],
|
||||
"export": {
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
// Constructor
|
||||
VictronBLE::VictronBLE()
|
||||
: pBLEScan(nullptr), callback(nullptr), debugEnabled(false),
|
||||
scanDuration(5), initialized(false) {
|
||||
: pBLEScan(nullptr), scanCallback(nullptr), callback(nullptr),
|
||||
debugEnabled(false), scanDuration(5), initialized(false) {
|
||||
}
|
||||
|
||||
// Destructor
|
||||
@@ -24,35 +24,38 @@ VictronBLE::~VictronBLE() {
|
||||
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";
|
||||
debugPrint(lastError);
|
||||
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;
|
||||
}
|
||||
@@ -61,13 +64,13 @@ bool VictronBLE::begin(uint32_t scanDuration) {
|
||||
bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
|
||||
if (config.macAddress.length() == 0) {
|
||||
lastError = "MAC address cannot be empty";
|
||||
debugPrint(lastError);
|
||||
if (debugEnabled) debugPrint(lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config.encryptionKey.length() != 32) {
|
||||
lastError = "Encryption key must be 32 hex characters";
|
||||
debugPrint(lastError);
|
||||
if (debugEnabled) debugPrint(lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -75,7 +78,7 @@ bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
|
||||
|
||||
// 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];
|
||||
}
|
||||
|
||||
@@ -86,7 +89,7 @@ bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
|
||||
// Convert encryption key from hex string to bytes
|
||||
if (!hexStringToBytes(config.encryptionKey, info->encryptionKeyBytes, 16)) {
|
||||
lastError = "Invalid encryption key format";
|
||||
debugPrint(lastError);
|
||||
if (debugEnabled) debugPrint(lastError);
|
||||
delete info;
|
||||
return false;
|
||||
}
|
||||
@@ -100,8 +103,8 @@ bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
|
||||
|
||||
devices[normalizedMAC] = info;
|
||||
|
||||
debugPrint("Added device: " + config.name + " (MAC: " + normalizedMAC + ")");
|
||||
if (debugEnabled) {
|
||||
debugPrint("Added device: " + config.name + " (MAC: " + normalizedMAC + ")");
|
||||
debugPrint(" Original MAC input: " + config.macAddress);
|
||||
debugPrint(" Stored normalized: " + normalizedMAC);
|
||||
}
|
||||
@@ -109,21 +112,21 @@ bool VictronBLE::addDevice(const VictronDeviceConfig& config) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +149,7 @@ 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);
|
||||
@@ -155,28 +158,25 @@ void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) {
|
||||
debugPrint("Raw MAC: " + mac + " -> Normalized: " + normalizedMAC);
|
||||
}
|
||||
|
||||
// TODO: Consider skipping with no manufacturer data?
|
||||
memset(&manufacturerData, 0, sizeof(manufacturerData));
|
||||
// Parse manufacturer data into local struct
|
||||
victronManufacturerData mfgData;
|
||||
memset(&mfgData, 0, sizeof(mfgData));
|
||||
if (advertisedDevice.haveManufacturerData()) {
|
||||
std::string mfgData = advertisedDevice.getManufacturerData();
|
||||
// XXX Storing it this way is not thread safe - is that issue on this ESP32?
|
||||
debugPrint("Getting manufacturer data: Size=" + String(mfgData.length()));
|
||||
mfgData.copy((char*)&manufacturerData, (mfgData.length() > sizeof(manufacturerData) ? sizeof(manufacturerData) : mfgData.length()));
|
||||
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()));
|
||||
}
|
||||
|
||||
// Pointer? XXX
|
||||
|
||||
// Debug: Log all discovered BLE devices
|
||||
if (debugEnabled) {
|
||||
String debugMsg = "";
|
||||
|
||||
debugMsg += "BLE Device: " + mac;
|
||||
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(manufacturerData.vendorID, HEX);
|
||||
if (manufacturerData.vendorID == VICTRON_MANUFACTURER_ID) {
|
||||
debugMsg += ", Mfg ID: 0x" + String(mfgData.vendorID, HEX);
|
||||
if (mfgData.vendorID == VICTRON_MANUFACTURER_ID) {
|
||||
debugMsg += " (Victron)";
|
||||
}
|
||||
|
||||
@@ -186,25 +186,8 @@ void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) {
|
||||
// Check if this is one of our configured devices
|
||||
auto it = devices.find(normalizedMAC);
|
||||
if (it == devices.end()) {
|
||||
// XXX Check if the device is a Victron device
|
||||
// This needs lots of improvemet and only do in debug
|
||||
if (manufacturerData.vendorID == VICTRON_MANUFACTURER_ID) {
|
||||
debugPrint("Found unmonitored Victron Device: " + normalizeMAC(mac));
|
||||
// DeviceInfo* deviceInfo = new DeviceInfo(mac, advertisedDevice.getName());
|
||||
// devices.insert({normalizedMAC, deviceInfo});
|
||||
// XXX What type of Victron device is it?
|
||||
// Check if it's a Victron Energy device
|
||||
/*
|
||||
if (advertisedDevice.haveServiceData()) {
|
||||
std::string serviceData = advertisedDevice.getServiceData();
|
||||
if (serviceData.length() >= 2) {
|
||||
uint16_t serviceId = (uint8_t)serviceData[1] << 8 | (uint8_t)serviceData[0];
|
||||
if (serviceId == VICTRON_ENERGY_SERVICE_ID) {
|
||||
debugPrint("Found Victron Energy Device: " + mac);
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
if (debugEnabled && mfgData.vendorID == VICTRON_MANUFACTURER_ID) {
|
||||
debugPrint("Found unmonitored Victron Device: " + normalizedMAC);
|
||||
}
|
||||
return; // Not a device we're monitoring
|
||||
}
|
||||
@@ -212,15 +195,15 @@ void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) {
|
||||
DeviceInfo* deviceInfo = it->second;
|
||||
|
||||
// Check if it's Victron (manufacturer ID 0x02E1)
|
||||
if (manufacturerData.vendorID != VICTRON_MANUFACTURER_ID) {
|
||||
debugPrint("Skipping non VICTRON");
|
||||
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(normalizedMAC)) {
|
||||
if (parseAdvertisement(deviceInfo, mfgData)) {
|
||||
// Update RSSI
|
||||
if (deviceInfo->data) {
|
||||
deviceInfo->data->rssi = advertisedDevice.getRSSI();
|
||||
@@ -230,55 +213,47 @@ void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) {
|
||||
}
|
||||
|
||||
// Parse advertisement data
|
||||
bool VictronBLE::parseAdvertisement(const String& macAddress) {
|
||||
// XXX We already searched above - try not to again?
|
||||
auto it = devices.find(macAddress);
|
||||
if (it == devices.end()) {
|
||||
debugPrint("parseAdvertisement: Device not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
DeviceInfo* deviceInfo = it->second;
|
||||
|
||||
bool VictronBLE::parseAdvertisement(DeviceInfo* deviceInfo, const victronManufacturerData& mfgData) {
|
||||
if (debugEnabled) {
|
||||
debugPrint("Vendor ID: 0x" + String(manufacturerData.vendorID, HEX));
|
||||
debugPrint("Beacon Type: 0x" + String(manufacturerData.beaconType, HEX));
|
||||
debugPrint("Record Type: 0x" + String(manufacturerData.victronRecordType, HEX));
|
||||
debugPrint("Nonce: 0x" + String(manufacturerData.nonceDataCounter, HEX));
|
||||
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] = manufacturerData.nonceDataCounter & 0xFF; // Low byte
|
||||
iv[1] = (manufacturerData.nonceDataCounter >> 8) & 0xFF; // High byte
|
||||
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(manufacturerData.victronEncryptedData,
|
||||
sizeof(manufacturerData.victronEncryptedData),
|
||||
const size_t encryptedLen = sizeof(mfgData.victronEncryptedData);
|
||||
uint8_t decrypted[encryptedLen];
|
||||
if (!decryptAdvertisement(mfgData.victronEncryptedData,
|
||||
encryptedLen,
|
||||
deviceInfo->encryptionKeyBytes, iv, decrypted)) {
|
||||
lastError = "Decryption failed";
|
||||
debugPrint(lastError);
|
||||
if (debugEnabled) debugPrint(lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse based on device type
|
||||
bool parseOk = false;
|
||||
|
||||
switch (manufacturerData.victronRecordType) {
|
||||
switch (mfgData.victronRecordType) {
|
||||
case DEVICE_TYPE_SOLAR_CHARGER:
|
||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_SOLAR_CHARGER) {
|
||||
parseOk = parseSolarCharger(decrypted, sizeof(decrypted),
|
||||
*(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, sizeof(decrypted),
|
||||
*(BatteryMonitorData*)deviceInfo->data);
|
||||
parseOk = parseBatteryMonitor(decrypted, encryptedLen,
|
||||
*static_cast<BatteryMonitorData*>(deviceInfo->data));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -287,20 +262,20 @@ bool VictronBLE::parseAdvertisement(const String& macAddress) {
|
||||
case DEVICE_TYPE_MULTI_RS:
|
||||
case DEVICE_TYPE_VE_BUS:
|
||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_INVERTER) {
|
||||
parseOk = parseInverter(decrypted, sizeof(decrypted),
|
||||
*(InverterData*)deviceInfo->data);
|
||||
parseOk = parseInverter(decrypted, encryptedLen,
|
||||
*static_cast<InverterData*>(deviceInfo->data));
|
||||
}
|
||||
break;
|
||||
|
||||
case DEVICE_TYPE_DCDC_CONVERTER:
|
||||
if (deviceInfo->data && deviceInfo->data->deviceType == DEVICE_TYPE_DCDC_CONVERTER) {
|
||||
parseOk = parseDCDCConverter(decrypted, sizeof(decrypted),
|
||||
*(DCDCConverterData*)deviceInfo->data);
|
||||
parseOk = parseDCDCConverter(decrypted, encryptedLen,
|
||||
*static_cast<DCDCConverterData*>(deviceInfo->data));
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
debugPrint("Unknown device type: 0x" + String(manufacturerData.victronRecordType, HEX));
|
||||
if (debugEnabled) debugPrint("Unknown device type: 0x" + String(mfgData.victronRecordType, HEX));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -309,21 +284,21 @@ bool VictronBLE::parseAdvertisement(const String& macAddress) {
|
||||
|
||||
// Call appropriate callback
|
||||
if (callback) {
|
||||
switch (manufacturerData.victronRecordType) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -365,15 +340,14 @@ bool VictronBLE::decryptAdvertisement(const uint8_t* encrypted, size_t encLen,
|
||||
// Parse Solar Charger data
|
||||
bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarChargerData& result) {
|
||||
if (len < sizeof(victronSolarChargerPayload)) {
|
||||
debugPrint("Solar charger data too short: " + String(len) + " bytes");
|
||||
if (debugEnabled) debugPrint("Solar charger data too short: " + String(len) + " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cast decrypted data to struct for easy access
|
||||
const victronSolarChargerPayload* payload = (const victronSolarChargerPayload*)data;
|
||||
const auto* payload = reinterpret_cast<const victronSolarChargerPayload*>(data);
|
||||
|
||||
// Parse charge state
|
||||
result.chargeState = (SolarChargerState)payload->deviceState;
|
||||
result.chargeState = static_cast<SolarChargerState>(payload->deviceState);
|
||||
|
||||
// Parse battery voltage (10 mV units -> volts)
|
||||
result.batteryVoltage = payload->batteryVoltage * 0.01f;
|
||||
@@ -401,9 +375,11 @@ bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarCharger
|
||||
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;
|
||||
}
|
||||
@@ -411,12 +387,11 @@ bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarCharger
|
||||
// Parse Battery Monitor data
|
||||
bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result) {
|
||||
if (len < sizeof(victronBatteryMonitorPayload)) {
|
||||
debugPrint("Battery monitor data too short: " + String(len) + " bytes");
|
||||
if (debugEnabled) debugPrint("Battery monitor data too short: " + String(len) + " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cast decrypted data to struct for easy access
|
||||
const victronBatteryMonitorPayload* payload = (const victronBatteryMonitorPayload*)data;
|
||||
const auto* payload = reinterpret_cast<const victronBatteryMonitorPayload*>(data);
|
||||
|
||||
// Parse remaining time (1 minute units)
|
||||
result.remainingMinutes = payload->remainingMins;
|
||||
@@ -441,7 +416,6 @@ bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMon
|
||||
}
|
||||
|
||||
// Parse battery current (22-bit signed, 1 mA units)
|
||||
// Bits 0-7: currentLow, Bits 8-15: currentMid, Bits 16-21: low 6 bits of currentHigh_consumedLow
|
||||
int32_t current = payload->currentLow |
|
||||
(payload->currentMid << 8) |
|
||||
((payload->currentHigh_consumedLow & 0x3F) << 16);
|
||||
@@ -452,7 +426,6 @@ bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMon
|
||||
result.current = current * 0.001f; // Convert mA to A
|
||||
|
||||
// Parse consumed Ah (18-bit signed, 10 mAh units)
|
||||
// Bits 0-1: high 2 bits of currentHigh_consumedLow, Bits 2-9: consumedMid, Bits 10-17: consumedHigh
|
||||
int32_t consumedAh = ((payload->currentHigh_consumedLow & 0xC0) >> 6) |
|
||||
(payload->consumedMid << 2) |
|
||||
(payload->consumedHigh << 10);
|
||||
@@ -465,8 +438,10 @@ bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMon
|
||||
// Parse SOC (10-bit value, 10 = 1.0%)
|
||||
result.soc = (payload->soc & 0x3FF) * 0.1f;
|
||||
|
||||
debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " +
|
||||
String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%");
|
||||
if (debugEnabled) {
|
||||
debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " +
|
||||
String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -474,12 +449,11 @@ bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMon
|
||||
// Parse Inverter data
|
||||
bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& result) {
|
||||
if (len < sizeof(victronInverterPayload)) {
|
||||
debugPrint("Inverter data too short: " + String(len) + " bytes");
|
||||
if (debugEnabled) debugPrint("Inverter data too short: " + String(len) + " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cast decrypted data to struct for easy access
|
||||
const victronInverterPayload* payload = (const victronInverterPayload*)data;
|
||||
const auto* payload = reinterpret_cast<const victronInverterPayload*>(data);
|
||||
|
||||
// Parse device state
|
||||
result.state = payload->deviceState;
|
||||
@@ -506,8 +480,10 @@ bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& re
|
||||
result.alarmHighTemperature = (payload->alarms & 0x04) != 0;
|
||||
result.alarmOverload = (payload->alarms & 0x08) != 0;
|
||||
|
||||
debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " +
|
||||
String(result.acPower) + "W, State: " + String(result.state));
|
||||
if (debugEnabled) {
|
||||
debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " +
|
||||
String(result.acPower) + "W, State: " + String(result.state));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -515,12 +491,11 @@ bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& re
|
||||
// Parse DC-DC Converter data
|
||||
bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result) {
|
||||
if (len < sizeof(victronDCDCConverterPayload)) {
|
||||
debugPrint("DC-DC converter data too short: " + String(len) + " bytes");
|
||||
if (debugEnabled) debugPrint("DC-DC converter data too short: " + String(len) + " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cast decrypted data to struct for easy access
|
||||
const victronDCDCConverterPayload* payload = (const victronDCDCConverterPayload*)data;
|
||||
const auto* payload = reinterpret_cast<const victronDCDCConverterPayload*>(data);
|
||||
|
||||
// Parse charge state
|
||||
result.chargeState = payload->chargeState;
|
||||
@@ -537,56 +512,58 @@ bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConvert
|
||||
// Parse output current (10 mA units -> amps)
|
||||
result.outputCurrent = payload->outputCurrent * 0.01f;
|
||||
|
||||
debugPrint("DC-DC Converter: In=" + String(result.inputVoltage, 2) + "V, Out=" +
|
||||
String(result.outputVoltage, 2) + "V, " + String(result.outputCurrent, 2) + "A");
|
||||
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 &&
|
||||
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;
|
||||
@@ -644,21 +621,19 @@ bool VictronBLE::hexStringToBytes(const String& hex, uint8_t* bytes, size_t len)
|
||||
|
||||
// Helper: MAC address to string
|
||||
String VictronBLE::macAddressToString(BLEAddress address) {
|
||||
// Use the BLEAddress toString() method which provides consistent formatting
|
||||
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();
|
||||
// XXX - is this right, was - to : but not consistent location of pairs or not
|
||||
normalized.replace("-", "");
|
||||
normalized.replace(":", "");
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// Debug helpers
|
||||
// Debug helper
|
||||
void VictronBLE::debugPrint(const String& message) {
|
||||
if (debugEnabled)
|
||||
Serial.println("[VictronBLE] " + message);
|
||||
|
||||
@@ -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 {
|
||||
@@ -57,7 +57,7 @@ enum SolarChargerState {
|
||||
// Must use __attribute__((packed)) to prevent compiler padding
|
||||
|
||||
// Manufacturer data structure (outer envelope)
|
||||
typedef struct {
|
||||
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
|
||||
@@ -66,11 +66,11 @@ typedef struct {
|
||||
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;
|
||||
} __attribute__((packed));
|
||||
// Decrypted payload structures for each device type
|
||||
|
||||
// Solar Charger decrypted payload
|
||||
typedef struct {
|
||||
struct victronSolarChargerPayload {
|
||||
uint8_t deviceState; // Charge state (SolarChargerState enum)
|
||||
uint8_t errorCode; // Error code
|
||||
int16_t batteryVoltage; // Battery voltage in 10mV units
|
||||
@@ -79,10 +79,10 @@ typedef struct {
|
||||
uint16_t inputPower; // PV power in 1W units
|
||||
uint16_t loadCurrent; // Load current in 10mA units (0xFFFF = no load)
|
||||
uint8_t reserved[2]; // Reserved bytes
|
||||
} __attribute__((packed)) victronSolarChargerPayload;
|
||||
} __attribute__((packed));
|
||||
|
||||
// Battery Monitor decrypted payload
|
||||
typedef struct {
|
||||
struct victronBatteryMonitorPayload {
|
||||
uint16_t remainingMins; // Time remaining in minutes
|
||||
uint16_t batteryVoltage; // Battery voltage in 10mV units
|
||||
uint8_t alarms; // Alarm bits
|
||||
@@ -94,10 +94,10 @@ typedef struct {
|
||||
uint8_t consumedHigh; // Consumed Ah bits 10-17
|
||||
uint16_t soc; // State of charge in 0.1% units (10-bit value)
|
||||
uint8_t reserved[2]; // Reserved bytes
|
||||
} __attribute__((packed)) victronBatteryMonitorPayload;
|
||||
} __attribute__((packed));
|
||||
|
||||
// Inverter decrypted payload
|
||||
typedef struct {
|
||||
struct victronInverterPayload {
|
||||
uint8_t deviceState; // Device state
|
||||
uint8_t errorCode; // Error code
|
||||
uint16_t batteryVoltage; // Battery voltage in 10mV units
|
||||
@@ -107,17 +107,17 @@ typedef struct {
|
||||
uint8_t acPowerHigh; // AC Power bits 16-23 (signed 24-bit)
|
||||
uint8_t alarms; // Alarm bits
|
||||
uint8_t reserved[4]; // Reserved bytes
|
||||
} __attribute__((packed)) victronInverterPayload;
|
||||
} __attribute__((packed));
|
||||
|
||||
// DC-DC Converter decrypted payload
|
||||
typedef struct {
|
||||
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)) victronDCDCConverterPayload;
|
||||
} __attribute__((packed));
|
||||
|
||||
// Base structure for all device data
|
||||
struct VictronDeviceData {
|
||||
@@ -205,8 +205,9 @@ struct DCDCConverterData : public VictronDeviceData {
|
||||
}
|
||||
};
|
||||
|
||||
// Forward declaration
|
||||
// Forward declarations
|
||||
class VictronBLE;
|
||||
class VictronBLEAdvertisedDeviceCallbacks;
|
||||
|
||||
// Callback interface for device data updates
|
||||
class VictronDeviceCallback {
|
||||
@@ -226,7 +227,7 @@ struct VictronDeviceConfig {
|
||||
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) {}
|
||||
};
|
||||
|
||||
@@ -241,11 +242,11 @@ public:
|
||||
|
||||
// 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(); }
|
||||
@@ -257,10 +258,10 @@ public:
|
||||
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);
|
||||
@@ -285,26 +286,26 @@ private:
|
||||
~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;
|
||||
|
||||
// XXX Experiment with actual victron data
|
||||
victronManufacturerData manufacturerData;
|
||||
|
||||
// Internal methods
|
||||
bool hexStringToBytes(const String& hex, uint8_t* bytes, size_t len);
|
||||
bool decryptAdvertisement(const uint8_t* encrypted, size_t encLen,
|
||||
const uint8_t* key, const uint8_t* iv,
|
||||
uint8_t* decrypted);
|
||||
bool parseAdvertisement(const 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);
|
||||
@@ -315,7 +316,7 @@ private:
|
||||
void debugPrint(const String& message);
|
||||
|
||||
String macAddressToString(BLEAddress address);
|
||||
String normalizeMAC(String mac);
|
||||
String normalizeMAC(const String& mac);
|
||||
};
|
||||
|
||||
// BLE scan callback class
|
||||
|
||||
Reference in New Issue
Block a user