/** * VictronBLE - portable library for Victron Energy BLE devices * * Runs on ESP32 (Bluedroid) and nRF52840 (Bluefruit); the BLE scanning backend * is the only platform-specific code (see src/esp32 and src/nrf52). Decoding and * AES-128-CTR decryption are common to all targets. * * Based on Victron's official BLE Advertising protocol documentation * Inspired by hoberman's examples and keshavdv's Python library * * Copyright (c) 2025 Scott Penrose * License: MIT */ #ifndef VICTRON_BLE_H #define VICTRON_BLE_H #include // --- Platform BLE backend selection --- // The BLE scanning layer is the only platform-specific part of the library. // Decoding and crypto are common to all targets. #if defined(ARDUINO_ARCH_ESP32) #include #include #include #define VICTRON_BACKEND_ESP32 1 #elif defined(ARDUINO_ARCH_NRF52) || defined(NRF52840_XXAA) || defined(NRF52832_XXAA) #include #define VICTRON_BACKEND_NRF52 1 #else #error "VictronBLE: unsupported platform (need ESP32 Arduino or Adafruit/Seeed nRF52 core)" #endif // --- Constants --- static constexpr uint16_t VICTRON_MANUFACTURER_ID = 0x02E1; static constexpr int VICTRON_MAX_DEVICES = 8; static constexpr int VICTRON_MAC_LEN = 13; // 12 hex chars + null static constexpr int VICTRON_NAME_LEN = 32; static constexpr int VICTRON_ENCRYPTED_LEN = 21; // --- Device type IDs from Victron protocol --- enum VictronDeviceType { DEVICE_TYPE_UNKNOWN = 0x00, DEVICE_TYPE_SOLAR_CHARGER = 0x01, DEVICE_TYPE_BATTERY_MONITOR = 0x02, DEVICE_TYPE_INVERTER = 0x03, DEVICE_TYPE_DCDC_CONVERTER = 0x04, DEVICE_TYPE_SMART_LITHIUM = 0x05, DEVICE_TYPE_INVERTER_RS = 0x06, DEVICE_TYPE_GX_DEVICE = 0x07, DEVICE_TYPE_AC_CHARGER = 0x08, DEVICE_TYPE_SMART_BATTERY_PROTECT = 0x09, DEVICE_TYPE_LYNX_SMART_BMS = 0x0A, DEVICE_TYPE_MULTI_RS = 0x0B, DEVICE_TYPE_VE_BUS = 0x0C, DEVICE_TYPE_DC_ENERGY_METER = 0x0D, DEVICE_TYPE_ORION_XS = 0x0F }; // --- Device state for Solar Charger --- enum SolarChargerState { CHARGER_OFF = 0, CHARGER_LOW_POWER = 1, CHARGER_FAULT = 2, CHARGER_BULK = 3, CHARGER_ABSORPTION = 4, CHARGER_FLOAT = 5, CHARGER_STORAGE = 6, CHARGER_EQUALIZE = 7, CHARGER_INVERTING = 9, CHARGER_POWER_SUPPLY = 11, CHARGER_EXTERNAL_CONTROL = 252 }; // ============================================================ // Wire-format packed structures for decoding BLE advertisements // ============================================================ struct victronManufacturerData { uint16_t vendorID; uint8_t beaconType; // 0x10 = Product Advertisement uint8_t unknownData1[3]; uint8_t victronRecordType; // Device type (see VictronDeviceType) uint16_t nonceDataCounter; uint8_t encryptKeyMatch; // Should match encryption key byte 0 uint8_t victronEncryptedData[VICTRON_ENCRYPTED_LEN]; } __attribute__((packed)); struct victronSolarChargerPayload { uint8_t deviceState; uint8_t errorCode; int16_t batteryVoltage; // 0.01V units (signed) int16_t batteryCurrent; // 0.1A units (signed) uint16_t yieldToday; // 0.01kWh (10Wh) units uint16_t inputPower; // 1W units uint16_t loadCurrent; // 9-bit field, 0.1A units (0x1FF = no load) uint8_t reserved[2]; } __attribute__((packed)); // NOTE: The battery monitor payload is bit-packed (16-bit alarm, 2-bit aux mode, // 22-bit current, 20-bit consumed Ah, 10-bit SOC) and does NOT byte-align, so it // is decoded by bit offset directly in parseBatteryMonitor() rather than a struct. struct victronInverterPayload { uint8_t deviceState; uint8_t errorCode; uint16_t batteryVoltage; // 10mV units int16_t batteryCurrent; // 10mA units (signed) uint8_t acPowerLow; uint8_t acPowerMid; uint8_t acPowerHigh; // Signed 24-bit uint8_t alarms; uint8_t reserved[4]; } __attribute__((packed)); struct victronDCDCConverterPayload { uint8_t chargeState; uint8_t errorCode; uint16_t inputVoltage; // 10mV units uint16_t outputVoltage; // 10mV units uint16_t outputCurrent; // 10mA units uint8_t reserved[6]; } __attribute__((packed)); // ============================================================ // Parsed data structures (flat, no inheritance) // ============================================================ struct VictronSolarData { uint8_t chargeState; // SolarChargerState enum uint8_t errorCode; float batteryVoltage; // V float batteryCurrent; // A float panelPower; // W uint16_t yieldToday; // Wh float loadCurrent; // A }; struct VictronACChargerData { uint8_t chargeState; // SolarChargerState enum (shared charger states) uint8_t errorCode; float voltage1; // V (output 1) float current1; // A (output 1) float voltage2; // V (output 2, 0 if absent) float current2; // A (output 2, 0 if absent) float voltage3; // V (output 3, 0 if absent) float current3; // A (output 3, 0 if absent) float temperature; // C (0 if not available) float acCurrent; // A (0 if not available) }; struct VictronBatteryData { float voltage; // V float current; // A float temperature; // C (0 if aux is voltage) float auxVoltage; // V (0 if aux is temperature) uint16_t remainingMinutes; float consumedAh; // Ah float soc; // % bool alarmLowVoltage; bool alarmHighVoltage; bool alarmLowSOC; bool alarmLowTemperature; bool alarmHighTemperature; }; struct VictronInverterData { float batteryVoltage; // V float batteryCurrent; // A float acPower; // W uint8_t state; bool alarmLowVoltage; bool alarmHighVoltage; bool alarmHighTemperature; bool alarmOverload; }; struct VictronDCDCData { float inputVoltage; // V float outputVoltage; // V float outputCurrent; // A uint8_t chargeState; uint8_t errorCode; }; // ============================================================ // Main device struct with tagged union // ============================================================ struct VictronDevice { char name[VICTRON_NAME_LEN]; char mac[VICTRON_MAC_LEN]; VictronDeviceType deviceType; int8_t rssi; uint32_t lastUpdate; bool dataValid; union { VictronSolarData solar; VictronBatteryData battery; VictronInverterData inverter; VictronDCDCData dcdc; VictronACChargerData acCharger; }; }; // ============================================================ // Callback — simple function pointer // ============================================================ typedef void (*VictronCallback)(const VictronDevice* device); // Forward declaration class VictronBLEAdvertisedDeviceCallbacks; // ============================================================ // Main VictronBLE class // ============================================================ class VictronBLE { public: VictronBLE(); bool begin(uint32_t scanDuration = 5); bool addDevice(const char* name, const char* mac, const char* hexKey, VictronDeviceType type = DEVICE_TYPE_UNKNOWN); void setCallback(VictronCallback cb) { callback = cb; } void setDebug(bool enable) { debugEnabled = enable; } void setMinInterval(uint32_t ms) { minIntervalMs = ms; } size_t getDeviceCount() const { return deviceCount; } void loop(); private: struct DeviceEntry { VictronDevice device; uint8_t key[16]; uint16_t lastNonce; bool active; }; // --- Common state (platform-independent) --- DeviceEntry devices[VICTRON_MAX_DEVICES]; size_t deviceCount; VictronCallback callback; bool debugEnabled; uint32_t scanDuration; uint32_t minIntervalMs; bool initialized; static bool hexToBytes(const char* hex, uint8_t* out, size_t len); static void normalizeMAC(const char* input, char* output); DeviceEntry* findDevice(const char* normalizedMAC); bool decryptData(const uint8_t* encrypted, size_t len, const uint8_t* key, const uint8_t* iv, uint8_t* decrypted); // Common entry point fed by each platform BLE backend with one raw // manufacturer-data record (vendor ID first), the device MAC and RSSI. void onAdvertisement(const uint8_t* mfgData, size_t len, const char* macStr, int8_t rssi); bool parseAdvertisement(DeviceEntry* entry, const victronManufacturerData& mfg); bool parseSolarCharger(const uint8_t* data, size_t len, VictronSolarData& result); bool parseACCharger(const uint8_t* data, size_t len, VictronACChargerData& result); bool parseBatteryMonitor(const uint8_t* data, size_t len, VictronBatteryData& result); bool parseInverter(const uint8_t* data, size_t len, VictronInverterData& result); bool parseDCDCConverter(const uint8_t* data, size_t len, VictronDCDCData& result); // --- Platform-specific BLE backend (see src/esp32 and src/nrf52) --- #if defined(VICTRON_BACKEND_ESP32) friend class VictronBLEAdvertisedDeviceCallbacks; BLEScan* pBLEScan; VictronBLEAdvertisedDeviceCallbacks* scanCallbackObj; void processDevice(BLEAdvertisedDevice& dev); #elif defined(VICTRON_BACKEND_NRF52) static VictronBLE* s_instance; static void scanCallback(ble_gap_evt_adv_report_t* report); #endif }; #if defined(VICTRON_BACKEND_ESP32) // BLE scan callback (required by ESP32 BLE API) class VictronBLEAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks { public: VictronBLEAdvertisedDeviceCallbacks(VictronBLE* parent) : victronBLE(parent) {} void onResult(BLEAdvertisedDevice advertisedDevice) override; private: VictronBLE* victronBLE; }; #endif // ============================================================ // Commented-out features — kept for reference / future use // ============================================================ #if 0 // --- VictronDeviceConfig (use addDevice(name, mac, key, type) directly) --- struct VictronDeviceConfig { String name; String macAddress; String encryptionKey; VictronDeviceType expectedType; VictronDeviceConfig() : expectedType(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) {} }; // --- Virtual callback interface (replaced by function pointer VictronCallback) --- class VictronDeviceCallback { public: virtual ~VictronDeviceCallback() {} virtual void onSolarChargerData(const SolarChargerData& data) {} virtual void onBatteryMonitorData(const BatteryMonitorData& data) {} virtual void onInverterData(const InverterData& data) {} virtual void onDCDCConverterData(const DCDCConverterData& data) {} }; // --- Per-type getter methods (use callback instead) --- 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); // --- Other removed methods --- void removeDevice(const String& macAddress); std::vector getDevicesByType(VictronDeviceType type); String getLastError() const; #endif // commented-out features #endif // VICTRON_BLE_H