Compare commits

11 Commits

7 changed files with 581 additions and 260 deletions

2
.gitignore vendored
View File

@@ -66,3 +66,5 @@ __pycache__/
.Python
venv/
env/
*.tar.gz

8
TODO Normal file
View File

@@ -0,0 +1,8 @@
# Misc Stuff
* Consider support for upper/lower case MAC address and optionaly ":"
* Scanning - list devices publishing, should be able to get list even without knowing MAC / Encryption key
* Struct vs Manual
* Sh3dNg version and examples uses structs to get data - seems to work
* Example generated uses manually managing a string
* Reconsider what is best and use

View File

@@ -26,13 +26,125 @@ platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
build_flags =
-DCORE_DEBUG_LEVEL=3
# -DCORE_DEBUG_LEVEL=3
[env:esp32-s3-debug]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
#monitor_speed = 115200
#monitor_filters = esp32_exception_decoder
upload_protocol = esp-builtin
; Debug configuration for GDB
debug_tool = esp-builtin
debug_init_break = tbreak setup
debug_speed = 5000
debug_load_mode = always
; Build flags for debugging
build_flags =
-DCORE_DEBUG_LEVEL=5 ; Maximum ESP32 debug level
-O0 ; Disable optimization for debugging
-g3 ; Maximum debug information
build_type = debug
[env:esp32-c3]
platform = espressif32
framework = arduino
board = esp32-c3-devkitm-1
board_build.mcu = esp32c3
board_build.f_cpu = 160000000L
board_build.flash_mode = dio
board_build.partitions = default.csv
monitor_speed = 115200
monitor_filters = time, default, esp32_exception_decoder
upload_speed = 921600
# NOTE: Need these two ARDUIO_USB modes to work with serial
build_flags =
-Os
-I src
-D ARDUINO_ESP32C3_DEV
-D CONFIG_IDF_TARGET_ESP32C3
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
lib_deps =
elapsedMillis
[env:esp32-c3-debug]
platform = espressif32
board = esp32-c3-devkitc-02
framework = arduino
monitor_speed = 115200
; Upload configuration
upload_protocol = esp-builtin
; Debug configuration for GDB
debug_tool = esp-builtin
debug_init_break = tbreak setup
debug_speed = 5000
debug_load_mode = always
; Build flags for debugging
build_flags =
-DCORE_DEBUG_LEVEL=3
-DCORE_DEBUG_LEVEL=5 ; Maximum ESP32 debug level
-O0 ; Disable optimization for debugging
-g3 ; Maximum debug information
build_type = debug
[env:m5stick]
platform = espressif32
board = m5stick-c
framework = arduino
board_build.mcu = esp32
board_build.f_cpu = 240000000L
board_build.partitions = no_ota.csv
#upload_protocol = espota
#upload_port = Button.local
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
#debug_tool = esp-prog ; esp-bridge, esp-prog ; or ftdi, esp-builtin, jlink, etc.
# debug_speed = 5000 ; optional: JTAG speed in kHz
#build_flags =
# -DCORE_DEBUG_LEVEL=5 ; ESP32 debug level
# -O0 ; no optimization
# -g3 ; max debug info
build_flags =
-Os
lib_deps =
M5StickC
elapsedMillis
TaskScheduler
Button2
ArduinoJson
https://github.com/scottp/PsychicHttp.git
[env:tough]
board = m5stack-core2
board_build.mcu = esp32
platform = espressif32
framework = arduino
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
debug_tool = esp-bridge ; esp-bridge, esp-prog ; or ftdi, esp-builtin, jlink, etc.
# debug_speed = 5000 ; optional: JTAG speed in kHz
build_flags =
-DCORE_DEBUG_LEVEL=5 ; ESP32 debug level
-O0 ; no optimization
-g3 ; max debug info
-DARDUINO_M5STACK_TOUGH
-DDISPLAY_WIDTH=320
-DDISPLAY_HEIGHT=240
-DHAS_TOUCH=1
-DBUFFER_LINES=10
lib_deps =
M5Unified
elapsedMillis
TaskScheduler
Button2
ArduinoJson
https://github.com/scottp/PsychicHttp.git

View File

@@ -165,37 +165,85 @@ void setup() {
// Add your devices here
// Replace with your actual MAC addresses and encryption keys
// Temporary - Scott Example
victron.addDevice(
"Rainbow48V", // Device name
"E4:05:42:34:14:F3", // MAC address
"0ec3adf7433dd61793ff2f3b8ad32ed8", // Encryption key (32 hex chars)
DEVICE_TYPE_SOLAR_CHARGER // Device type
);
victron.addDevice(
"Rainbow48Vb", // Device name
"3ffd00b83ffd00be",
"0ec3adf7433dd61793ff2f3b8ad32ed8", // Encryption key (32 hex chars)
DEVICE_TYPE_SOLAR_CHARGER // Device type
);
// WHY this one work?
victron.addDevice(
"Rainbow48Vc", // Device name
"3ffd00a83ffd00ae",
"0ec3adf7433dd61793ff2f3b8ad32ed8", // Encryption key (32 hex chars)
DEVICE_TYPE_SOLAR_CHARGER // Device type
);
/*
*
[VictronBLE] Encrypted data: A0 01 83 2C 0E CF D6 04 89 72 6E 81 56 E4 2D F1 83
[VictronBLE] IV: 02 58 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[VictronBLE] Decrypted data: E1 1C 99 32 D5 7E 81 A3 EB 8C 25 97 3E 0E DD 2D C4
[VictronBLE] Unknown device type: 0x10
[VictronBLE] BLE Device: 3ffd0148:3ffd014e, RSSI: -27 dBm
[VictronBLE] BLE Device: 3ffd0148:3ffd014e, RSSI: -81 dBm, Mfg ID: 0x2e1 (Victron)
[VictronBLE] Processing data from: Rainbow48Vc
[VictronBLE] Encrypted data: A0 01 83 2C 0E CF D6 04 89 72 6E 81 56 E4 2D F1 83
[VictronBLE] IV: 02 58 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[VictronBLE] Decrypted data: E1 1C 99 32 D5 7E 81 A3 EB 8C 25 97 3E 0E DD 2D C4
[VictronBLE] Unknown device type: 0x10
[VictronBLE] BLE Device: 3ffd0148:3ffd014e, RSSI: -49 dBm, Mfg ID: 0x75
[VictronBLE] BLE Device: 3ffd0148:3ffd014e, RSSI: -26 dBm
*/
// Example: Solar Charger #1
/*
victron.addDevice(
"MPPT 100/30", // Device name
"E7:48:D4:28:B7:9C", // MAC address
"0df4d0395b7d1a876c0c33ecb9e70dcd", // Encryption key (32 hex chars)
DEVICE_TYPE_SOLAR_CHARGER // Device type
);
*/
// Example: Solar Charger #2
/*
victron.addDevice(
"MPPT 75/15",
"AA:BB:CC:DD:EE:FF",
"1234567890abcdef1234567890abcdef",
DEVICE_TYPE_SOLAR_CHARGER
);
*/
// Example: Battery Monitor (SmartShunt)
/*
victron.addDevice(
"SmartShunt",
"11:22:33:44:55:66",
"fedcba0987654321fedcba0987654321",
DEVICE_TYPE_BATTERY_MONITOR
);
*/
// Example: Inverter/Charger
/*
victron.addDevice(
"MultiPlus",
"99:88:77:66:55:44",
"abcdefabcdefabcdefabcdefabcdefab",
DEVICE_TYPE_INVERTER
);
*/
Serial.println("Configured " + String(victron.getDeviceCount()) + " devices");
Serial.println("\nStarting BLE scan...\n");

View File

@@ -1,6 +1,6 @@
{
"name": "VictronBLE",
"version": "0.1.1",
"version": "0.1.2",
"description": "ESP32 library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements. Supports SmartSolar MPPT, SmartShunt, BMV, MultiPlus, Orion and other Victron devices.",
"keywords": "victron, ble, bluetooth, solar, mppt, battery, smartshunt, smartsolar, bmv, inverter, multiplus, esp32, iot, energy, monitoring",
"repository": {

View File

@@ -133,6 +133,30 @@ void VictronBLE::loop() {
// BLE callback implementation
void VictronBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertisedDevice) {
if (victronBLE) {
// Debug: Log all discovered BLE devices
if (victronBLE->debugEnabled) {
String mac = victronBLE->macAddressToString(advertisedDevice.getAddress());
String debugMsg = "BLE Device: " + mac;
debugMsg += ", RSSI: " + String(advertisedDevice.getRSSI()) + " dBm";
if (advertisedDevice.haveName()) {
debugMsg += ", Name: " + String(advertisedDevice.getName().c_str());
}
if (advertisedDevice.haveManufacturerData()) {
std::string mfgData = advertisedDevice.getManufacturerData();
if (mfgData.length() >= 2) {
uint16_t mfgId = (uint8_t)mfgData[1] << 8 | (uint8_t)mfgData[0];
debugMsg += ", Mfg ID: 0x" + String(mfgId, HEX);
if (mfgId == VICTRON_MANUFACTURER_ID) {
debugMsg += " (Victron)";
}
}
}
victronBLE->debugPrint(debugMsg);
}
victronBLE->processDevice(advertisedDevice);
}
}
@@ -145,6 +169,35 @@ 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 (advertisedDevice.haveManufacturerData()) {
std::string mfgData = advertisedDevice.getManufacturerData();
if (mfgData.length() >= 2) {
uint16_t mfgId = (uint8_t)mfgData[1] << 8 | (uint8_t)mfgData[0];
if (mfgId == 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);
}
}
}
*/
}
}
}
return; // Not a device we're monitoring
}
@@ -160,6 +213,8 @@ void VictronBLE::processDevice(BLEAdvertisedDevice advertisedDevice) {
return;
}
// XXX Use struct like code in Sh3dNg
// Check if it's Victron (manufacturer ID 0x02E1)
uint16_t mfgId = (uint8_t)mfgData[1] << 8 | (uint8_t)mfgData[0];
if (mfgId != VICTRON_MANUFACTURER_ID) {
@@ -183,32 +238,47 @@ bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
const String& macAddress) {
auto it = devices.find(macAddress);
if (it == devices.end()) {
debugPrint("parseAdvertisement: Device not found");
return false;
}
DeviceInfo* deviceInfo = it->second;
if (len < 6) {
debugPrint("Manufacturer data too short");
// Verify minimum size for victronManufacturerData struct
if (len < sizeof(victronManufacturerData)) {
debugPrint("Manufacturer data too short: " + String(len) + " bytes");
return false;
}
// Structure: [MfgID(2)] [DeviceType(1)] [IV(2)] [EncryptedData(n)]
uint8_t deviceType = manufacturerData[2];
// Extract IV (initialization vector) - bytes 3-4, little-endian
uint8_t iv[16] = {0};
iv[0] = manufacturerData[3];
iv[1] = manufacturerData[4];
// Rest of IV is zero-padded
// Encrypted data starts at byte 5
const uint8_t* encryptedData = manufacturerData + 5;
size_t encryptedLen = len - 5;
// Cast manufacturer data to struct for easy access
const victronManufacturerData* vicData = (const victronManufacturerData*)manufacturerData;
if (debugEnabled) {
debugPrint("Vendor ID: 0x" + String(vicData->vendorID, HEX));
debugPrint("Beacon Type: 0x" + String(vicData->beaconType, HEX));
debugPrint("Model ID: 0x" + String(vicData->modelID, HEX));
debugPrint("Readout Type: 0x" + String(vicData->readoutType, HEX));
debugPrint("Record Type: 0x" + String(vicData->victronRecordType, HEX));
debugPrint("Nonce: 0x" + String(vicData->nonceDataCounter, HEX));
}
// Get device type from record type field
uint8_t deviceType = vicData->victronRecordType;
// 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] = vicData->nonceDataCounter & 0xFF; // Low byte
iv[1] = (vicData->nonceDataCounter >> 8) & 0xFF; // High byte
// Remaining bytes stay zero
// Get pointer to encrypted data
const uint8_t* encryptedData = vicData->victronEncryptedData;
size_t encryptedLen = sizeof(vicData->victronEncryptedData);
if (debugEnabled) {
debugPrintHex("Encrypted data", encryptedData, encryptedLen);
debugPrintHex("IV", iv, 16);
debugPrintHex("Encrypted data", encryptedData, encryptedLen);
}
// Decrypt the data
@@ -323,39 +393,41 @@ 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 < 12) {
debugPrint("Solar charger data too short");
if (len < sizeof(victronSolarChargerPayload)) {
debugPrint("Solar charger data too short: " + String(len) + " bytes");
return false;
}
// Byte 0: Charge state
result.chargeState = (SolarChargerState)data[0];
// Cast decrypted data to struct for easy access
const victronSolarChargerPayload* payload = (const victronSolarChargerPayload*)data;
// Bytes 1-2: Battery voltage (10 mV units)
uint16_t vBat = data[1] | (data[2] << 8);
result.batteryVoltage = vBat * 0.01f;
// Parse charge state
result.chargeState = (SolarChargerState)payload->deviceState;
// Bytes 3-4: Battery current (10 mA units, signed)
int16_t iBat = (int16_t)(data[3] | (data[4] << 8));
result.batteryCurrent = iBat * 0.01f;
// Parse battery voltage (10 mV units -> volts)
result.batteryVoltage = payload->batteryVoltage * 0.01f;
// Bytes 5-6: Yield today (10 Wh units)
uint16_t yield = data[5] | (data[6] << 8);
result.yieldToday = yield * 10;
// Parse battery current (10 mA units, signed -> amps)
result.batteryCurrent = payload->batteryCurrent * 0.01f;
// Bytes 7-8: PV power (1 W units)
uint16_t pvPower = data[7] | (data[8] << 8);
result.panelPower = pvPower;
// Parse yield today (10 Wh units -> Wh)
result.yieldToday = payload->yieldToday * 10;
// Bytes 9-10: Load current (10 mA units)
uint16_t iLoad = data[9] | (data[10] << 8);
if (iLoad != 0xFFFF) { // 0xFFFF means no load output
result.loadCurrent = iLoad * 0.01f;
// Parse PV power (1 W units)
result.panelPower = payload->inputPower;
// Parse load current (10 mA units -> amps, 0xFFFF = no load)
if (payload->loadCurrent != 0xFFFF) {
result.loadCurrent = payload->loadCurrent * 0.01f;
} else {
result.loadCurrent = 0;
}
// Calculate PV voltage from power and current (if current > 0)
if (result.batteryCurrent > 0.1f) {
result.panelVoltage = result.panelPower / result.batteryCurrent;
} else {
result.panelVoltage = 0;
}
debugPrint("Solar Charger: " + String(result.batteryVoltage, 2) + "V, " +
@@ -367,54 +439,60 @@ 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 < 15) {
debugPrint("Battery monitor data too short");
if (len < sizeof(victronBatteryMonitorPayload)) {
debugPrint("Battery monitor data too short: " + String(len) + " bytes");
return false;
}
// Bytes 0-1: Remaining time (1 minute units)
uint16_t timeRemaining = data[0] | (data[1] << 8);
result.remainingMinutes = timeRemaining;
// Cast decrypted data to struct for easy access
const victronBatteryMonitorPayload* payload = (const victronBatteryMonitorPayload*)data;
// Bytes 2-3: Battery voltage (10 mV units)
uint16_t vBat = data[2] | (data[3] << 8);
result.voltage = vBat * 0.01f;
// Parse remaining time (1 minute units)
result.remainingMinutes = payload->remainingMins;
// Byte 4: Alarms
uint8_t alarms = data[4];
result.alarmLowVoltage = (alarms & 0x01) != 0;
result.alarmHighVoltage = (alarms & 0x02) != 0;
result.alarmLowSOC = (alarms & 0x04) != 0;
result.alarmLowTemperature = (alarms & 0x10) != 0;
result.alarmHighTemperature = (alarms & 0x20) != 0;
// Parse battery voltage (10 mV units -> volts)
result.voltage = payload->batteryVoltage * 0.01f;
// Bytes 5-6: Aux voltage/temperature (10 mV or 0.01K units)
uint16_t aux = data[5] | (data[6] << 8);
if (aux < 3000) { // If < 30V, it's voltage
result.auxVoltage = aux * 0.01f;
// Parse alarm bits
result.alarmLowVoltage = (payload->alarms & 0x01) != 0;
result.alarmHighVoltage = (payload->alarms & 0x02) != 0;
result.alarmLowSOC = (payload->alarms & 0x04) != 0;
result.alarmLowTemperature = (payload->alarms & 0x10) != 0;
result.alarmHighTemperature = (payload->alarms & 0x20) != 0;
// Parse aux data: voltage (10 mV units) or temperature (0.01K units)
if (payload->auxData < 3000) { // If < 30V, it's voltage
result.auxVoltage = payload->auxData * 0.01f;
result.temperature = 0;
} else { // Otherwise temperature in 0.01 Kelvin
result.temperature = (aux * 0.01f) - 273.15f;
result.temperature = (payload->auxData * 0.01f) - 273.15f;
result.auxVoltage = 0;
}
// Bytes 7-9: Battery current (22-bit signed, 1 mA units)
int32_t current = data[7] | (data[8] << 8) | ((data[9] & 0x3F) << 16);
if (current & 0x200000) { // Sign extend if negative
// Parse battery current (22-bit signed, 1 mA units)
// 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);
// Sign extend from 22 bits to 32 bits
if (current & 0x200000) {
current |= 0xFFC00000;
}
result.current = current * 0.001f;
result.current = current * 0.001f; // Convert mA to A
// Bytes 9-11: Consumed Ah (18-bit signed, 10 mAh units)
int32_t consumedAh = ((data[9] & 0xC0) >> 6) | (data[10] << 2) | ((data[11] & 0xFF) << 10);
if (consumedAh & 0x20000) { // Sign extend
// 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);
// Sign extend from 18 bits to 32 bits
if (consumedAh & 0x20000) {
consumedAh |= 0xFFFC0000;
}
result.consumedAh = consumedAh * 0.01f;
result.consumedAh = consumedAh * 0.01f; // Convert 10mAh to Ah
// Bytes 12-13: SOC (10 = 1.0%)
uint16_t soc = data[12] | ((data[13] & 0x03) << 8);
result.soc = soc * 0.1f;
// 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) + "%");
@@ -424,35 +502,38 @@ 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 < 10) {
debugPrint("Inverter data too short");
if (len < sizeof(victronInverterPayload)) {
debugPrint("Inverter data too short: " + String(len) + " bytes");
return false;
}
// Byte 0: Device state
result.state = data[0];
// Cast decrypted data to struct for easy access
const victronInverterPayload* payload = (const victronInverterPayload*)data;
// Bytes 1-2: Battery voltage (10 mV units)
uint16_t vBat = data[1] | (data[2] << 8);
result.batteryVoltage = vBat * 0.01f;
// Parse device state
result.state = payload->deviceState;
// Bytes 3-4: Battery current (10 mA units, signed)
int16_t iBat = (int16_t)(data[3] | (data[4] << 8));
result.batteryCurrent = iBat * 0.01f;
// Parse battery voltage (10 mV units -> volts)
result.batteryVoltage = payload->batteryVoltage * 0.01f;
// Bytes 5-7: AC Power (1 W units, signed 24-bit)
int32_t acPower = data[5] | (data[6] << 8) | (data[7] << 16);
if (acPower & 0x800000) { // Sign extend
// Parse battery current (10 mA units, signed -> amps)
result.batteryCurrent = payload->batteryCurrent * 0.01f;
// Parse AC Power (signed 24-bit, 1 W units)
int32_t acPower = payload->acPowerLow |
(payload->acPowerMid << 8) |
(payload->acPowerHigh << 16);
// Sign extend from 24 bits to 32 bits
if (acPower & 0x800000) {
acPower |= 0xFF000000;
}
result.acPower = acPower;
// Byte 8: Alarms
uint8_t alarms = data[8];
result.alarmLowVoltage = (alarms & 0x01) != 0;
result.alarmHighVoltage = (alarms & 0x02) != 0;
result.alarmHighTemperature = (alarms & 0x04) != 0;
result.alarmOverload = (alarms & 0x08) != 0;
// Parse alarm bits
result.alarmLowVoltage = (payload->alarms & 0x01) != 0;
result.alarmHighVoltage = (payload->alarms & 0x02) != 0;
result.alarmHighTemperature = (payload->alarms & 0x04) != 0;
result.alarmOverload = (payload->alarms & 0x08) != 0;
debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " +
String(result.acPower) + "W, State: " + String(result.state));
@@ -462,28 +543,28 @@ 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 < 10) {
debugPrint("DC-DC converter data too short");
if (len < sizeof(victronDCDCConverterPayload)) {
debugPrint("DC-DC converter data too short: " + String(len) + " bytes");
return false;
}
// Byte 0: Charge state
result.chargeState = data[0];
// Cast decrypted data to struct for easy access
const victronDCDCConverterPayload* payload = (const victronDCDCConverterPayload*)data;
// Bytes 1-2: Input voltage (10 mV units)
uint16_t vIn = data[1] | (data[2] << 8);
result.inputVoltage = vIn * 0.01f;
// Parse charge state
result.chargeState = payload->chargeState;
// Bytes 3-4: Output voltage (10 mV units)
uint16_t vOut = data[3] | (data[4] << 8);
result.outputVoltage = vOut * 0.01f;
// Parse error code
result.errorCode = payload->errorCode;
// Bytes 5-6: Output current (10 mA units)
uint16_t iOut = data[5] | (data[6] << 8);
result.outputCurrent = iOut * 0.01f;
// Parse input voltage (10 mV units -> volts)
result.inputVoltage = payload->inputVoltage * 0.01f;
// Byte 7: Error code
result.errorCode = data[7];
// Parse output voltage (10 mV units -> volts)
result.outputVoltage = payload->outputVoltage * 0.01f;
// Parse output current (10 mA units -> amps)
result.outputCurrent = payload->outputCurrent * 0.01f;
debugPrint("DC-DC Converter: In=" + String(result.inputVoltage, 2) + "V, Out=" +
String(result.outputVoltage, 2) + "V, " + String(result.outputCurrent, 2) + "A");
@@ -604,7 +685,9 @@ String VictronBLE::macAddressToString(BLEAddress address) {
String VictronBLE::normalizeMAC(String mac) {
String normalized = mac;
normalized.toLowerCase();
normalized.replace("-", ":");
// XXX - is this right, was - to : but not consistent location of pairs or not
normalized.replace("-", "");
normalized.replace(":", "");
return normalized;
}
@@ -615,6 +698,7 @@ void VictronBLE::debugPrint(const String& message) {
}
}
// XXX Can't we use debugPrintf instead for hex struct etc?
void VictronBLE::debugPrintHex(const char* label, const uint8_t* data, size_t len) {
if (!debugEnabled) return;

View File

@@ -53,6 +53,73 @@ enum SolarChargerState {
CHARGER_EXTERNAL_CONTROL = 252
};
// Binary data structures for decoding BLE advertisements
// Must use __attribute__((packed)) to prevent compiler padding
// Manufacturer data structure (outer envelope)
typedef struct {
uint16_t vendorID; // Victron vendor ID (0x02E1)
uint8_t beaconType; // Should be 0x10 (Product Advertisement)
uint8_t modelID; // Model identifier byte
uint8_t readoutType; // Type of data readout
uint8_t victronRecordType; // Record type (device type)
uint16_t nonceDataCounter; // Nonce for encryption (IV bytes 0-1)
uint8_t encryptKeyMatch; // Should match pre-shared encryption key byte 0
uint8_t victronEncryptedData[21]; // Encrypted payload (max 21 bytes)
} __attribute__((packed)) victronManufacturerData;
// Decrypted payload structures for each device type
// Solar Charger decrypted payload
typedef struct {
uint8_t deviceState; // Charge state (SolarChargerState enum)
uint8_t errorCode; // Error code
int16_t batteryVoltage; // Battery voltage in 10mV units
int16_t batteryCurrent; // Battery current in 10mA units (signed)
uint16_t yieldToday; // Yield today in 10Wh units
uint16_t inputPower; // PV power in 1W units
uint16_t loadCurrent; // Load current in 10mA units (0xFFFF = no load)
uint8_t reserved[2]; // Reserved bytes
} __attribute__((packed)) victronSolarChargerPayload;
// Battery Monitor decrypted payload
typedef struct {
uint16_t remainingMins; // Time remaining in minutes
uint16_t batteryVoltage; // Battery voltage in 10mV units
uint8_t alarms; // Alarm bits
uint16_t auxData; // Aux voltage (10mV) or temperature (0.01K)
uint8_t currentLow; // Battery current bits 0-7
uint8_t currentMid; // Battery current bits 8-15
uint8_t currentHigh_consumedLow; // Current bits 16-21 (low 6 bits), consumed bits 0-1 (high 2 bits)
uint8_t consumedMid; // Consumed Ah bits 2-9
uint8_t consumedHigh; // Consumed Ah bits 10-17
uint16_t soc; // State of charge in 0.1% units (10-bit value)
uint8_t reserved[2]; // Reserved bytes
} __attribute__((packed)) victronBatteryMonitorPayload;
// Inverter decrypted payload
typedef struct {
uint8_t deviceState; // Device state
uint8_t errorCode; // Error code
uint16_t batteryVoltage; // Battery voltage in 10mV units
int16_t batteryCurrent; // Battery current in 10mA units (signed)
uint8_t acPowerLow; // AC Power bits 0-7
uint8_t acPowerMid; // AC Power bits 8-15
uint8_t acPowerHigh; // AC Power bits 16-23 (signed 24-bit)
uint8_t alarms; // Alarm bits
uint8_t reserved[4]; // Reserved bytes
} __attribute__((packed)) victronInverterPayload;
// DC-DC Converter decrypted payload
typedef struct {
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;
// Base structure for all device data
struct VictronDeviceData {
String deviceName;