diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 577ac94..8069ca0 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -118,3 +118,47 @@ a843eb9 Keep v0.3.1 - 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 + diff --git a/examples/FakeRepeater/platformio.ini b/examples/FakeRepeater/platformio.ini new file mode 100644 index 0000000..26410af --- /dev/null +++ b/examples/FakeRepeater/platformio.ini @@ -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 diff --git a/examples/FakeRepeater/src/main.cpp b/examples/FakeRepeater/src/main.cpp new file mode 100644 index 0000000..abbc334 --- /dev/null +++ b/examples/FakeRepeater/src/main.cpp @@ -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 +#include +#include + +// 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(&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); + } +} diff --git a/examples/Receiver/platformio.ini b/examples/Receiver/platformio.ini new file mode 100644 index 0000000..ea25906 --- /dev/null +++ b/examples/Receiver/platformio.ini @@ -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: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 + +[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 diff --git a/examples/Receiver/src/main.cpp b/examples/Receiver/src/main.cpp new file mode 100644 index 0000000..d4fda77 --- /dev/null +++ b/examples/Receiver/src/main.cpp @@ -0,0 +1,107 @@ +/** + * 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 +#include +#include + +// 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; + +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(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]); +} + +void setup() { + 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() { + delay(100); +} diff --git a/examples/Repeater/platformio.ini b/examples/Repeater/platformio.ini new file mode 100644 index 0000000..2acf9b3 --- /dev/null +++ b/examples/Repeater/platformio.ini @@ -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 diff --git a/examples/Repeater/src/main.cpp b/examples/Repeater/src/main.cpp new file mode 100644 index 0000000..bebb34e --- /dev/null +++ b/examples/Repeater/src/main.cpp @@ -0,0 +1,142 @@ +/** + * VictronBLE Repeater Example + * + * Collects Solar Charger data via BLE and forwards every packet + * over ESPNow broadcast. 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 +#include +#include +#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 uint32_t sendCount = 0; +static uint32_t sendFailCount = 0; + +VictronBLE victron; + +class RepeaterCallback : public VictronDeviceCallback { +public: + void onSolarChargerData(const SolarChargerData& data) override { + SolarChargerPacket pkt; + pkt.chargeState = static_cast(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; + + // Copy device name, truncate to fit + memset(pkt.deviceName, 0, sizeof(pkt.deviceName)); + strncpy(pkt.deviceName, data.deviceName.c_str(), sizeof(pkt.deviceName) - 1); + + esp_err_t result = esp_now_send(BROADCAST_ADDR, + reinterpret_cast(&pkt), + sizeof(pkt)); + + sendCount++; + if (result != ESP_OK) { + sendFailCount++; + Serial.println("ESPNow send failed: " + String(esp_err_to_name(result))); + } else { + Serial.println("[TX] " + String(pkt.deviceName) + + " Batt:" + String(pkt.batteryVoltage, 2) + "V" + + " PV:" + String(pkt.panelPower, 0) + "W" + + " (sent:" + String(sendCount) + + " fail:" + String(sendFailCount) + ")"); + } + } +}; + +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(); + delay(100); +} diff --git a/library.json b/library.json index f936322..646951f 100644 --- a/library.json +++ b/library.json @@ -31,6 +31,11 @@ "name": "Logger", "base": "examples/Logger", "files": ["src/main.cpp"] + }, + { + "name": "Repeater", + "base": "examples/Repeater", + "files": ["src/main.cpp"] } ], "export": {