Bug fixes for reported issues

This commit is contained in:
2026-06-04 21:21:18 +10:00
parent e7024d9983
commit 7706f88399
6 changed files with 190 additions and 63 deletions
+36 -4
View File
@@ -29,6 +29,7 @@ Currently supporting ESP32 S and C series (tested on older ESP32, ESP32-S3 and E
- **Battery Monitors**: SmartShunt, BMV-712 Smart, BMV-700 series - **Battery Monitors**: SmartShunt, BMV-712 Smart, BMV-700 series
- **Inverters**: MultiPlus, Quattro, Phoenix (with VE.Bus BLE dongle) - **Inverters**: MultiPlus, Quattro, Phoenix (with VE.Bus BLE dongle)
- **DC-DC Converters**: Orion Smart, Orion XS - **DC-DC Converters**: Orion Smart, Orion XS
- **AC Chargers**: Blue Smart IP22 / IP65 / IP67 chargers
- **Others**: Smart Battery Protect, Lynx Smart BMS, Smart Lithium batteries - **Others**: Smart Battery Protect, Lynx Smart BMS, Smart Lithium batteries
## Hardware Requirements ## Hardware Requirements
@@ -40,16 +41,23 @@ Currently supporting ESP32 S and C series (tested on older ESP32, ESP32-S3 and E
### PlatformIO ### PlatformIO
1. Add to `platformio.ini`: 1. Add to `platformio.ini` (recommended — installs from the PlatformIO registry):
```ini ```ini
lib_deps = lib_deps =
https://gitea.sh3d.com.au/Sh3d/VictronBLE scottp/victronble
```
Or install directly from git. Note the **`.git` suffix is required** — the bare
repository URL is not accepted by PlatformIO:
```ini
lib_deps =
https://gitea.sh3d.com.au/Sh3d/VictronBLE.git
``` ```
2. Or clone into your project's `lib` folder: 2. Or clone into your project's `lib` folder:
```bash ```bash
cd lib cd lib
git clone https://gitea.sh3d.com.au/Sh3d/VictronBLE git clone https://gitea.sh3d.com.au/Sh3d/VictronBLE.git
``` ```
### Arduino IDE ### Arduino IDE
@@ -262,6 +270,23 @@ struct VictronDCDCData {
}; };
``` ```
#### VictronACChargerData
```cpp
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)
};
```
## Advanced Usage ## Advanced Usage
### Multiple Devices ### Multiple Devices
@@ -299,6 +324,11 @@ void onVictronData(const VictronDevice* dev) {
Serial.printf("%s: %.2fV -> %.2fV\n", dev->name, Serial.printf("%s: %.2fV -> %.2fV\n", dev->name,
dev->dcdc.inputVoltage, dev->dcdc.outputVoltage); dev->dcdc.inputVoltage, dev->dcdc.outputVoltage);
break; break;
case DEVICE_TYPE_AC_CHARGER:
Serial.printf("%s: %.2fV %.2fA %.0fC\n", dev->name,
dev->acCharger.voltage1, dev->acCharger.current1,
dev->acCharger.temperature);
break;
default: default:
break; break;
} }
@@ -397,7 +427,9 @@ See [VERSIONS](VERSIONS) file for detailed changelog and release history.
## Support ## Support
- 📫 Report issues on GitHub - 📫 Report issues: the Gitea instance does not currently allow public sign-ups, so
email <scottp@dd.com.au> with bug reports, device decode problems, or new device
requests (debug log output is very helpful).
- 📖 Check the examples directory - 📖 Check the examples directory
- 🔧 Enable debug mode for diagnostics - 🔧 Enable debug mode for diagnostics
- 📚 See [Victron documentation](https://www.victronenergy.com/live/) - 📚 See [Victron documentation](https://www.victronenergy.com/live/)
+28
View File
@@ -1,5 +1,33 @@
# Version History # Version History
## 0.5.0 (2026-06-04)
Decoding accuracy fixes (thanks to community bug reports from Karsten, Cory, Kevin and Dan)
plus new AC Charger support. Field layouts verified against the keshavdv/victron-ble
reference implementation and the Victron "Extra Manufacturer Data" specification.
### Bug fixes
- **Battery monitor decoding rewritten.** The `victronBatteryMonitorPayload` struct had
wrong field widths (8-bit alarm instead of 16, no 2-bit aux-mode field) which cascaded
and misaligned current, consumed Ah and SOC. `parseBatteryMonitor()` now decodes the
bit-packed payload directly by bit offset: signed voltage, 16-bit alarm, aux value +
2-bit aux mode (replacing the unreliable `< 3000` voltage/temperature heuristic),
22-bit signed current, 20-bit consumed Ah (×-0.1 Ah), 10-bit SOC (×0.1 %).
- **Solar charger battery current scale fixed.** Was multiplied by 0.01 (10× too small);
the field is in 0.1 A units. Load current is now read as the correct 9-bit field.
- **Compile error with newer ESP32 BLE library fixed.** `getManufacturerData()` now returns
an Arduino `String` on recent cores; `processDevice()` handles both `String` and
`std::string` while preserving the binary payload's embedded null bytes.
- **Device type IDs corrected.** The enum was off-by-one from 0x07 onward, mislabelling
AC chargers (0x08) as Lynx Smart BMS. Now matches the Victron protocol:
0x07 GX Device, 0x08 AC Charger, 0x09 Smart Battery Protect, 0x0A Lynx Smart BMS,
0x0B Multi RS, 0x0C VE.Bus, 0x0D DC Energy Meter, 0x0F Orion XS.
### New features
- **AC Charger support** (Blue Smart IP22/IP65/IP67, device type 0x08). New
`VictronACChargerData` struct (three output voltage/current banks, temperature, AC
current) and `DEVICE_TYPE_AC_CHARGER` handling.
## 0.4.1 (2026-02-28) ## 0.4.1 (2026-02-28)
Major rework of library internals. Breaking API change — not backwards compatible with 0.3.x. Major rework of library internals. Breaking API change — not backwards compatible with 0.3.x.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "victronble", "name": "victronble",
"version": "0.4.1", "version": "0.5.0",
"description": "ESP32 library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements. Supports SmartSolar MPPT, SmartShunt, BMV, MultiPlus, Orion and other Victron devices.", "description": "ESP32 library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements. Supports SmartSolar MPPT, SmartShunt, BMV, MultiPlus, Orion and other Victron devices.",
"keywords": "victron, ble, bluetooth, solar, mppt, battery, smartshunt, smartsolar, bmv, inverter, multiplus, esp32, iot, energy, monitoring", "keywords": "victron, ble, bluetooth, solar, mppt, battery, smartshunt, smartsolar, bmv, inverter, multiplus, esp32, iot, energy, monitoring",
"repository": { "repository": {
+1 -1
View File
@@ -1,5 +1,5 @@
name=VictronBLE name=VictronBLE
version=0.4.1 version=0.5.0
author=Scott Penrose author=Scott Penrose
maintainer=Scott Penrose <scottp@dd.com.au> maintainer=Scott Penrose <scottp@dd.com.au>
sentence=ESP32 library for reading Victron Energy device data via BLE for any ESP32 sentence=ESP32 library for reading Victron Energy device data via BLE for any ESP32
+93 -34
View File
@@ -87,7 +87,11 @@ void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertise
void VictronBLE::processDevice(BLEAdvertisedDevice& advertisedDevice) { void VictronBLE::processDevice(BLEAdvertisedDevice& advertisedDevice) {
if (!advertisedDevice.haveManufacturerData()) return; if (!advertisedDevice.haveManufacturerData()) return;
std::string raw = advertisedDevice.getManufacturerData(); // getManufacturerData() returns std::string on older ESP32 BLE libraries and
// an Arduino String on newer ones. Both expose c_str()/length(); constructing
// from (ptr, len) preserves the embedded null bytes present in binary data.
auto mfg = advertisedDevice.getManufacturerData();
std::string raw(mfg.c_str(), mfg.length());
if (raw.length() < 10) return; if (raw.length() < 10) return;
// Quick vendor ID check before any other work // Quick vendor ID check before any other work
@@ -180,6 +184,10 @@ bool VictronBLE::parseAdvertisement(DeviceEntry* entry, const victronManufacture
entry->device.deviceType = DEVICE_TYPE_DCDC_CONVERTER; entry->device.deviceType = DEVICE_TYPE_DCDC_CONVERTER;
ok = parseDCDCConverter(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.dcdc); ok = parseDCDCConverter(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.dcdc);
break; break;
case DEVICE_TYPE_AC_CHARGER:
entry->device.deviceType = DEVICE_TYPE_AC_CHARGER;
ok = parseACCharger(decrypted, VICTRON_ENCRYPTED_LEN, entry->device.acCharger);
break;
default: default:
if (debugEnabled) Serial.printf("[VictronBLE] Unknown type: 0x%02X\n", mfg.victronRecordType); if (debugEnabled) Serial.printf("[VictronBLE] Unknown type: 0x%02X\n", mfg.victronRecordType);
return false; return false;
@@ -222,11 +230,13 @@ bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, VictronSolar
result.chargeState = p->deviceState; result.chargeState = p->deviceState;
result.errorCode = p->errorCode; result.errorCode = p->errorCode;
result.batteryVoltage = p->batteryVoltage * 0.01f; result.batteryVoltage = p->batteryVoltage * 0.01f; // 0.01V units
result.batteryCurrent = p->batteryCurrent * 0.01f; result.batteryCurrent = p->batteryCurrent * 0.1f; // 0.1A units
result.yieldToday = p->yieldToday * 10; result.yieldToday = p->yieldToday * 10;
result.panelPower = p->inputPower; result.panelPower = p->inputPower;
result.loadCurrent = (p->loadCurrent != 0xFFFF) ? p->loadCurrent * 0.01f : 0; // Load current is a 9-bit field (0.1A units); 0x1FF = no load output
uint16_t loadRaw = p->loadCurrent & 0x1FF;
result.loadCurrent = (loadRaw != 0x1FF) ? loadRaw * 0.1f : 0;
if (debugEnabled) { if (debugEnabled) {
Serial.printf("[VictronBLE] Solar: %.2fV %.2fA %dW State:%d\n", Serial.printf("[VictronBLE] Solar: %.2fV %.2fA %dW State:%d\n",
@@ -236,47 +246,96 @@ bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, VictronSolar
return true; return true;
} }
bool VictronBLE::parseACCharger(const uint8_t* data, size_t len, VictronACChargerData& result) {
// Payload is bit-packed (10 fields, 104 bits ending in byte 12). Decode LSB-first.
if (len < 13) return false;
size_t bit = 0;
auto readBits = [&](uint8_t width) -> uint32_t {
uint32_t value = 0;
for (uint8_t i = 0; i < width; i++) {
size_t b = bit + i;
value |= (uint32_t)((data[b >> 3] >> (b & 7)) & 0x01) << i;
}
bit += width;
return value;
};
result.chargeState = (uint8_t)readBits(8);
result.errorCode = (uint8_t)readBits(8);
uint32_t v1 = readBits(13), i1 = readBits(11);
uint32_t v2 = readBits(13), i2 = readBits(11);
uint32_t v3 = readBits(13), i3 = readBits(11);
uint32_t temp = readBits(7);
uint32_t acCur = readBits(9);
result.voltage1 = (v1 != 0x1FFF) ? v1 * 0.01f : 0;
result.current1 = (i1 != 0x7FF) ? i1 * 0.1f : 0;
result.voltage2 = (v2 != 0x1FFF) ? v2 * 0.01f : 0;
result.current2 = (i2 != 0x7FF) ? i2 * 0.1f : 0;
result.voltage3 = (v3 != 0x1FFF) ? v3 * 0.01f : 0;
result.current3 = (i3 != 0x7FF) ? i3 * 0.1f : 0;
result.temperature = (temp != 0x7F) ? (float)temp - 40.0f : 0; // C offset by -40
result.acCurrent = (acCur != 0x1FF) ? acCur * 0.1f : 0;
if (debugEnabled) {
Serial.printf("[VictronBLE] AC Charger: %.2fV %.2fA Temp:%.0fC State:%d\n",
result.voltage1, result.current1, result.temperature, result.chargeState);
}
return true;
}
bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, VictronBatteryData& result) { bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, VictronBatteryData& result) {
if (len < sizeof(victronBatteryMonitorPayload)) return false; // The payload is bit-packed and not byte-aligned, so it is decoded by bit
const auto* p = reinterpret_cast<const victronBatteryMonitorPayload*>(data); // offset directly rather than via a struct. SOC ends at bit 117 (byte 14).
if (len < 15) return false;
result.remainingMinutes = p->remainingMins; // TTG (bits 0-15), unsigned minutes
result.voltage = p->batteryVoltage * 0.01f; result.remainingMinutes = data[0] | ((uint16_t)data[1] << 8);
// Alarm bits // Voltage (bits 16-31), signed, 0.01V units
result.alarmLowVoltage = (p->alarms & 0x01) != 0; result.voltage = (int16_t)(data[2] | ((uint16_t)data[3] << 8)) * 0.01f;
result.alarmHighVoltage = (p->alarms & 0x02) != 0;
result.alarmLowSOC = (p->alarms & 0x04) != 0;
result.alarmLowTemperature = (p->alarms & 0x10) != 0;
result.alarmHighTemperature = (p->alarms & 0x20) != 0;
// Aux data: voltage or temperature (heuristic: < 30V = voltage) // Alarm (bits 32-47), 16-bit bitmask
// NOTE: Victron protocol uses a flag bit for this, but it's not exposed uint16_t alarm = data[4] | ((uint16_t)data[5] << 8);
// in the BLE advertisement. This heuristic may misclassify edge cases. result.alarmLowVoltage = (alarm & 0x0001) != 0;
if (p->auxData < 3000) { result.alarmHighVoltage = (alarm & 0x0002) != 0;
result.auxVoltage = p->auxData * 0.01f; result.alarmLowSOC = (alarm & 0x0004) != 0;
result.alarmLowTemperature = (alarm & 0x0010) != 0;
result.alarmHighTemperature = (alarm & 0x0020) != 0;
// Aux value (bits 48-63) interpreted per aux mode (bits 64-65)
uint16_t auxRaw = data[6] | ((uint16_t)data[7] << 8);
uint8_t auxMode = data[8] & 0x03; // 0=aux voltage, 1=midpoint, 2=temperature, 3=none
if (auxMode == 0) {
result.auxVoltage = auxRaw * 0.01f;
result.temperature = 0; result.temperature = 0;
} else { } else if (auxMode == 2) {
result.temperature = (p->auxData * 0.01f) - 273.15f; result.temperature = auxRaw * 0.01f - 273.15f; // 0.01K -> C
result.auxVoltage = 0; result.auxVoltage = 0;
} else {
result.auxVoltage = 0;
result.temperature = 0;
} }
// Battery current (22-bit signed, 1 mA units) // Battery current (bits 66-87), 22-bit signed, 0.001A units
int32_t current = p->currentLow | int32_t current = ((uint32_t)(data[8] >> 2) & 0x3F)
(p->currentMid << 8) | | ((uint32_t)data[9] << 6)
((p->currentHigh_consumedLow & 0x3F) << 16); | ((uint32_t)data[10] << 14);
if (current & 0x200000) current |= 0xFFC00000; // Sign extend if (current & 0x200000) current |= 0xFFC00000; // Sign extend 22-bit
result.current = current * 0.001f; result.current = current * 0.001f;
// Consumed Ah (18-bit signed, 10 mAh units) // Consumed Ah (bits 88-107), 20-bit, stored as a positive count, 0.1Ah units.
int32_t consumedAh = ((p->currentHigh_consumedLow & 0xC0) >> 6) | // Reported as a negative value (amp-hours consumed).
(p->consumedMid << 2) | uint32_t consumed = (uint32_t)data[11]
(p->consumedHigh << 10); | ((uint32_t)data[12] << 8)
if (consumedAh & 0x20000) consumedAh |= 0xFFFC0000; // Sign extend | ((uint32_t)(data[13] & 0x0F) << 16);
result.consumedAh = consumedAh * 0.01f; result.consumedAh = -((float)consumed * 0.1f);
// SOC (10-bit, 0.1% units) // SOC (bits 108-117), 10-bit, 0.1% units
result.soc = (p->soc & 0x3FF) * 0.1f; uint16_t soc = ((uint16_t)(data[13] >> 4) | ((uint16_t)data[14] << 4)) & 0x3FF;
result.soc = soc * 0.1f;
if (debugEnabled) { if (debugEnabled) {
Serial.printf("[VictronBLE] Battery: %.2fV %.2fA SOC:%.1f%%\n", Serial.printf("[VictronBLE] Battery: %.2fV %.2fA SOC:%.1f%%\n",
+30 -22
View File
@@ -33,11 +33,14 @@ enum VictronDeviceType {
DEVICE_TYPE_DCDC_CONVERTER = 0x04, DEVICE_TYPE_DCDC_CONVERTER = 0x04,
DEVICE_TYPE_SMART_LITHIUM = 0x05, DEVICE_TYPE_SMART_LITHIUM = 0x05,
DEVICE_TYPE_INVERTER_RS = 0x06, DEVICE_TYPE_INVERTER_RS = 0x06,
DEVICE_TYPE_SMART_BATTERY_PROTECT = 0x07, DEVICE_TYPE_GX_DEVICE = 0x07,
DEVICE_TYPE_LYNX_SMART_BMS = 0x08, DEVICE_TYPE_AC_CHARGER = 0x08,
DEVICE_TYPE_MULTI_RS = 0x09, DEVICE_TYPE_SMART_BATTERY_PROTECT = 0x09,
DEVICE_TYPE_VE_BUS = 0x0A, DEVICE_TYPE_LYNX_SMART_BMS = 0x0A,
DEVICE_TYPE_DC_ENERGY_METER = 0x0B 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 --- // --- Device state for Solar Charger ---
@@ -72,27 +75,17 @@ struct victronManufacturerData {
struct victronSolarChargerPayload { struct victronSolarChargerPayload {
uint8_t deviceState; uint8_t deviceState;
uint8_t errorCode; uint8_t errorCode;
int16_t batteryVoltage; // 10mV units int16_t batteryVoltage; // 0.01V units (signed)
int16_t batteryCurrent; // 10mA units (signed) int16_t batteryCurrent; // 0.1A units (signed)
uint16_t yieldToday; // 10Wh units uint16_t yieldToday; // 0.01kWh (10Wh) units
uint16_t inputPower; // 1W units uint16_t inputPower; // 1W units
uint16_t loadCurrent; // 10mA units (0xFFFF = no load) uint16_t loadCurrent; // 9-bit field, 0.1A units (0x1FF = no load)
uint8_t reserved[2]; uint8_t reserved[2];
} __attribute__((packed)); } __attribute__((packed));
struct victronBatteryMonitorPayload { // NOTE: The battery monitor payload is bit-packed (16-bit alarm, 2-bit aux mode,
uint16_t remainingMins; // 22-bit current, 20-bit consumed Ah, 10-bit SOC) and does NOT byte-align, so it
uint16_t batteryVoltage; // 10mV units // is decoded by bit offset directly in parseBatteryMonitor() rather than a struct.
uint8_t alarms;
uint16_t auxData; // 10mV (voltage) or 0.01K (temperature)
uint8_t currentLow;
uint8_t currentMid;
uint8_t currentHigh_consumedLow; // Current bits 16-21 (low 6), consumed bits 0-1 (high 2)
uint8_t consumedMid;
uint8_t consumedHigh;
uint16_t soc; // 0.1% units (10-bit)
uint8_t reserved[2];
} __attribute__((packed));
struct victronInverterPayload { struct victronInverterPayload {
uint8_t deviceState; uint8_t deviceState;
@@ -129,6 +122,19 @@ struct VictronSolarData {
float loadCurrent; // A 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 { struct VictronBatteryData {
float voltage; // V float voltage; // V
float current; // A float current; // A
@@ -179,6 +185,7 @@ struct VictronDevice {
VictronBatteryData battery; VictronBatteryData battery;
VictronInverterData inverter; VictronInverterData inverter;
VictronDCDCData dcdc; VictronDCDCData dcdc;
VictronACChargerData acCharger;
}; };
}; };
@@ -236,6 +243,7 @@ private:
void processDevice(BLEAdvertisedDevice& dev); void processDevice(BLEAdvertisedDevice& dev);
bool parseAdvertisement(DeviceEntry* entry, const victronManufacturerData& mfg); bool parseAdvertisement(DeviceEntry* entry, const victronManufacturerData& mfg);
bool parseSolarCharger(const uint8_t* data, size_t len, VictronSolarData& result); 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 parseBatteryMonitor(const uint8_t* data, size_t len, VictronBatteryData& result);
bool parseInverter(const uint8_t* data, size_t len, VictronInverterData& result); bool parseInverter(const uint8_t* data, size_t len, VictronInverterData& result);
bool parseDCDCConverter(const uint8_t* data, size_t len, VictronDCDCData& result); bool parseDCDCConverter(const uint8_t* data, size_t len, VictronDCDCData& result);