This commit is contained in:
2025-12-29 11:16:43 +11:00
4 changed files with 250 additions and 136 deletions

View File

@@ -26,16 +26,74 @@ platform = espressif32
board = esp32-s3-devkitc-1 board = esp32-s3-devkitc-1
framework = arduino framework = arduino
monitor_speed = 115200 monitor_speed = 115200
monitor_filters = esp32_exception_decoder
build_flags = 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] [env:esp32-c3]
platform = espressif32 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 board = esp32-c3-devkitc-02
framework = arduino framework = arduino
monitor_speed = 115200 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 = 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] [env:m5stick]
platform = espressif32 platform = espressif32

View File

@@ -206,36 +206,44 @@ void setup() {
*/ */
// Example: Solar Charger #1 // Example: Solar Charger #1
/*
victron.addDevice( victron.addDevice(
"MPPT 100/30", // Device name "MPPT 100/30", // Device name
"E7:48:D4:28:B7:9C", // MAC address "E7:48:D4:28:B7:9C", // MAC address
"0df4d0395b7d1a876c0c33ecb9e70dcd", // Encryption key (32 hex chars) "0df4d0395b7d1a876c0c33ecb9e70dcd", // Encryption key (32 hex chars)
DEVICE_TYPE_SOLAR_CHARGER // Device type DEVICE_TYPE_SOLAR_CHARGER // Device type
); );
*/
// Example: Solar Charger #2 // Example: Solar Charger #2
/*
victron.addDevice( victron.addDevice(
"MPPT 75/15", "MPPT 75/15",
"AA:BB:CC:DD:EE:FF", "AA:BB:CC:DD:EE:FF",
"1234567890abcdef1234567890abcdef", "1234567890abcdef1234567890abcdef",
DEVICE_TYPE_SOLAR_CHARGER DEVICE_TYPE_SOLAR_CHARGER
); );
*/
// Example: Battery Monitor (SmartShunt) // Example: Battery Monitor (SmartShunt)
/*
victron.addDevice( victron.addDevice(
"SmartShunt", "SmartShunt",
"11:22:33:44:55:66", "11:22:33:44:55:66",
"fedcba0987654321fedcba0987654321", "fedcba0987654321fedcba0987654321",
DEVICE_TYPE_BATTERY_MONITOR DEVICE_TYPE_BATTERY_MONITOR
); );
*/
// Example: Inverter/Charger // Example: Inverter/Charger
/*
victron.addDevice( victron.addDevice(
"MultiPlus", "MultiPlus",
"99:88:77:66:55:44", "99:88:77:66:55:44",
"abcdefabcdefabcdefabcdefabcdefab", "abcdefabcdefabcdefabcdefabcdefab",
DEVICE_TYPE_INVERTER DEVICE_TYPE_INVERTER
); );
*/
Serial.println("Configured " + String(victron.getDeviceCount()) + " devices"); Serial.println("Configured " + String(victron.getDeviceCount()) + " devices");
Serial.println("\nStarting BLE scan...\n"); Serial.println("\nStarting BLE scan...\n");

View File

@@ -242,41 +242,43 @@ bool VictronBLE::parseAdvertisement(const uint8_t* manufacturerData, size_t len,
return false; return false;
} }
// XXX Work out second?
DeviceInfo* deviceInfo = it->second; DeviceInfo* deviceInfo = it->second;
if (len < 6) { // Verify minimum size for victronManufacturerData struct
debugPrint("Manufacturer data too short"); if (len < sizeof(victronManufacturerData)) {
debugPrint("Manufacturer data too short: " + String(len) + " bytes");
return false; return false;
} }
// XXX map to struct - NOTE: Check size first (exact? or bigger?) // Cast manufacturer data to struct for easy access
victronManufacturerData * vicData=(victronManufacturerData *)manufacturerData; const victronManufacturerData* vicData = (const victronManufacturerData*)manufacturerData;
debugPrint("VendorID" + String(vicData->vendorID));
debugPrint("Record Type" + String(vicData->victronRecordType));
// Structure: [MfgID(2)] [DeviceType(1)] [IV(2)] [EncryptedData(n)] if (debugEnabled) {
// XXX This is actually 4 - Struct would help - it was 2 debugPrint("Vendor ID: 0x" + String(vicData->vendorID, HEX));
uint8_t deviceType = manufacturerData[4]; 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));
}
// Extract IV (initialization vector) - bytes 3-4, little-endian // Get device type from record type field
// XXX These look wrong 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}; uint8_t iv[16] = {0};
iv[0] = manufacturerData[3]; iv[0] = vicData->nonceDataCounter & 0xFF; // Low byte
iv[1] = manufacturerData[4]; iv[1] = (vicData->nonceDataCounter >> 8) & 0xFF; // High byte
// Rest of IV is zero-padded // Remaining bytes stay zero
// Encrypted data starts at byte 5 // Get pointer to encrypted data
// const uint8_t* encryptedData = manufacturerData + 5;
// size_t encryptedLen = len - 5;
// XXX Experiment
const uint8_t* encryptedData = vicData->victronEncryptedData; const uint8_t* encryptedData = vicData->victronEncryptedData;
size_t encryptedLen = sizeof(vicData->victronEncryptedData); size_t encryptedLen = sizeof(vicData->victronEncryptedData);
if (debugEnabled) { if (debugEnabled) {
debugPrintHex("Encrypted data", encryptedData, encryptedLen);
debugPrintHex("IV", iv, 16); debugPrintHex("IV", iv, 16);
debugPrintHex("Encrypted data", encryptedData, encryptedLen);
} }
// Decrypt the data // Decrypt the data
@@ -391,39 +393,41 @@ bool VictronBLE::decryptAdvertisement(const uint8_t* encrypted, size_t encLen,
// Parse Solar Charger data // Parse Solar Charger data
bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarChargerData& result) { bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarChargerData& result) {
if (len < 12) { if (len < sizeof(victronSolarChargerPayload)) {
debugPrint("Solar charger data too short"); debugPrint("Solar charger data too short: " + String(len) + " bytes");
return false; return false;
} }
// Byte 0: Charge state // Cast decrypted data to struct for easy access
result.chargeState = (SolarChargerState)data[0]; const victronSolarChargerPayload* payload = (const victronSolarChargerPayload*)data;
// Bytes 1-2: Battery voltage (10 mV units) // Parse charge state
uint16_t vBat = data[1] | (data[2] << 8); result.chargeState = (SolarChargerState)payload->deviceState;
result.batteryVoltage = vBat * 0.01f;
// Bytes 3-4: Battery current (10 mA units, signed) // Parse battery voltage (10 mV units -> volts)
int16_t iBat = (int16_t)(data[3] | (data[4] << 8)); result.batteryVoltage = payload->batteryVoltage * 0.01f;
result.batteryCurrent = iBat * 0.01f;
// Bytes 5-6: Yield today (10 Wh units) // Parse battery current (10 mA units, signed -> amps)
uint16_t yield = data[5] | (data[6] << 8); result.batteryCurrent = payload->batteryCurrent * 0.01f;
result.yieldToday = yield * 10;
// Bytes 7-8: PV power (1 W units) // Parse yield today (10 Wh units -> Wh)
uint16_t pvPower = data[7] | (data[8] << 8); result.yieldToday = payload->yieldToday * 10;
result.panelPower = pvPower;
// Bytes 9-10: Load current (10 mA units) // Parse PV power (1 W units)
uint16_t iLoad = data[9] | (data[10] << 8); result.panelPower = payload->inputPower;
if (iLoad != 0xFFFF) { // 0xFFFF means no load output
result.loadCurrent = iLoad * 0.01f; // 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) // Calculate PV voltage from power and current (if current > 0)
if (result.batteryCurrent > 0.1f) { if (result.batteryCurrent > 0.1f) {
result.panelVoltage = result.panelPower / result.batteryCurrent; result.panelVoltage = result.panelPower / result.batteryCurrent;
} else {
result.panelVoltage = 0;
} }
debugPrint("Solar Charger: " + String(result.batteryVoltage, 2) + "V, " + debugPrint("Solar Charger: " + String(result.batteryVoltage, 2) + "V, " +
@@ -435,54 +439,60 @@ bool VictronBLE::parseSolarCharger(const uint8_t* data, size_t len, SolarCharger
// Parse Battery Monitor data // Parse Battery Monitor data
bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result) { bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMonitorData& result) {
if (len < 15) { if (len < sizeof(victronBatteryMonitorPayload)) {
debugPrint("Battery monitor data too short"); debugPrint("Battery monitor data too short: " + String(len) + " bytes");
return false; return false;
} }
// Bytes 0-1: Remaining time (1 minute units) // Cast decrypted data to struct for easy access
uint16_t timeRemaining = data[0] | (data[1] << 8); const victronBatteryMonitorPayload* payload = (const victronBatteryMonitorPayload*)data;
result.remainingMinutes = timeRemaining;
// Bytes 2-3: Battery voltage (10 mV units) // Parse remaining time (1 minute units)
uint16_t vBat = data[2] | (data[3] << 8); result.remainingMinutes = payload->remainingMins;
result.voltage = vBat * 0.01f;
// Byte 4: Alarms // Parse battery voltage (10 mV units -> volts)
uint8_t alarms = data[4]; result.voltage = payload->batteryVoltage * 0.01f;
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;
// Bytes 5-6: Aux voltage/temperature (10 mV or 0.01K units) // Parse alarm bits
uint16_t aux = data[5] | (data[6] << 8); result.alarmLowVoltage = (payload->alarms & 0x01) != 0;
if (aux < 3000) { // If < 30V, it's voltage result.alarmHighVoltage = (payload->alarms & 0x02) != 0;
result.auxVoltage = aux * 0.01f; 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; result.temperature = 0;
} else { // Otherwise temperature in 0.01 Kelvin } 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; result.auxVoltage = 0;
} }
// Bytes 7-9: Battery current (22-bit signed, 1 mA units) // Parse battery current (22-bit signed, 1 mA units)
int32_t current = data[7] | (data[8] << 8) | ((data[9] & 0x3F) << 16); // Bits 0-7: currentLow, Bits 8-15: currentMid, Bits 16-21: low 6 bits of currentHigh_consumedLow
if (current & 0x200000) { // Sign extend if negative 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; 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) // Parse consumed Ah (18-bit signed, 10 mAh units)
int32_t consumedAh = ((data[9] & 0xC0) >> 6) | (data[10] << 2) | ((data[11] & 0xFF) << 10); // Bits 0-1: high 2 bits of currentHigh_consumedLow, Bits 2-9: consumedMid, Bits 10-17: consumedHigh
if (consumedAh & 0x20000) { // Sign extend 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; consumedAh |= 0xFFFC0000;
} }
result.consumedAh = consumedAh * 0.01f; result.consumedAh = consumedAh * 0.01f; // Convert 10mAh to Ah
// Bytes 12-13: SOC (10 = 1.0%) // Parse SOC (10-bit value, 10 = 1.0%)
uint16_t soc = data[12] | ((data[13] & 0x03) << 8); result.soc = (payload->soc & 0x3FF) * 0.1f;
result.soc = soc * 0.1f;
debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " + debugPrint("Battery Monitor: " + String(result.voltage, 2) + "V, " +
String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%"); String(result.current, 2) + "A, SOC: " + String(result.soc, 1) + "%");
@@ -492,35 +502,38 @@ bool VictronBLE::parseBatteryMonitor(const uint8_t* data, size_t len, BatteryMon
// Parse Inverter data // Parse Inverter data
bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& result) { bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& result) {
if (len < 10) { if (len < sizeof(victronInverterPayload)) {
debugPrint("Inverter data too short"); debugPrint("Inverter data too short: " + String(len) + " bytes");
return false; return false;
} }
// Byte 0: Device state // Cast decrypted data to struct for easy access
result.state = data[0]; const victronInverterPayload* payload = (const victronInverterPayload*)data;
// Bytes 1-2: Battery voltage (10 mV units) // Parse device state
uint16_t vBat = data[1] | (data[2] << 8); result.state = payload->deviceState;
result.batteryVoltage = vBat * 0.01f;
// Bytes 3-4: Battery current (10 mA units, signed) // Parse battery voltage (10 mV units -> volts)
int16_t iBat = (int16_t)(data[3] | (data[4] << 8)); result.batteryVoltage = payload->batteryVoltage * 0.01f;
result.batteryCurrent = iBat * 0.01f;
// Bytes 5-7: AC Power (1 W units, signed 24-bit) // Parse battery current (10 mA units, signed -> amps)
int32_t acPower = data[5] | (data[6] << 8) | (data[7] << 16); result.batteryCurrent = payload->batteryCurrent * 0.01f;
if (acPower & 0x800000) { // Sign extend
// 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; acPower |= 0xFF000000;
} }
result.acPower = acPower; result.acPower = acPower;
// Byte 8: Alarms // Parse alarm bits
uint8_t alarms = data[8]; result.alarmLowVoltage = (payload->alarms & 0x01) != 0;
result.alarmLowVoltage = (alarms & 0x01) != 0; result.alarmHighVoltage = (payload->alarms & 0x02) != 0;
result.alarmHighVoltage = (alarms & 0x02) != 0; result.alarmHighTemperature = (payload->alarms & 0x04) != 0;
result.alarmHighTemperature = (alarms & 0x04) != 0; result.alarmOverload = (payload->alarms & 0x08) != 0;
result.alarmOverload = (alarms & 0x08) != 0;
debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " + debugPrint("Inverter: " + String(result.batteryVoltage, 2) + "V, " +
String(result.acPower) + "W, State: " + String(result.state)); String(result.acPower) + "W, State: " + String(result.state));
@@ -530,28 +543,28 @@ bool VictronBLE::parseInverter(const uint8_t* data, size_t len, InverterData& re
// Parse DC-DC Converter data // Parse DC-DC Converter data
bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result) { bool VictronBLE::parseDCDCConverter(const uint8_t* data, size_t len, DCDCConverterData& result) {
if (len < 10) { if (len < sizeof(victronDCDCConverterPayload)) {
debugPrint("DC-DC converter data too short"); debugPrint("DC-DC converter data too short: " + String(len) + " bytes");
return false; return false;
} }
// Byte 0: Charge state // Cast decrypted data to struct for easy access
result.chargeState = data[0]; const victronDCDCConverterPayload* payload = (const victronDCDCConverterPayload*)data;
// Bytes 1-2: Input voltage (10 mV units) // Parse charge state
uint16_t vIn = data[1] | (data[2] << 8); result.chargeState = payload->chargeState;
result.inputVoltage = vIn * 0.01f;
// Bytes 3-4: Output voltage (10 mV units) // Parse error code
uint16_t vOut = data[3] | (data[4] << 8); result.errorCode = payload->errorCode;
result.outputVoltage = vOut * 0.01f;
// Bytes 5-6: Output current (10 mA units) // Parse input voltage (10 mV units -> volts)
uint16_t iOut = data[5] | (data[6] << 8); result.inputVoltage = payload->inputVoltage * 0.01f;
result.outputCurrent = iOut * 0.01f;
// Byte 7: Error code // Parse output voltage (10 mV units -> volts)
result.errorCode = data[7]; 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=" + debugPrint("DC-DC Converter: In=" + String(result.inputVoltage, 2) + "V, Out=" +
String(result.outputVoltage, 2) + "V, " + String(result.outputCurrent, 2) + "A"); String(result.outputVoltage, 2) + "V, " + String(result.outputCurrent, 2) + "A");
@@ -685,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) { void VictronBLE::debugPrintHex(const char* label, const uint8_t* data, size_t len) {
if (!debugEnabled) return; if (!debugEnabled) return;

View File

@@ -53,38 +53,72 @@ enum SolarChargerState {
CHARGER_EXTERNAL_CONTROL = 252 CHARGER_EXTERNAL_CONTROL = 252
}; };
// XXX HARD Core structs // Binary data structures for decoding BLE advertisements
// Used for decoding // Must use __attribute__((packed)) to prevent compiler padding
// But then data is put into specific device structs
// Which means a lot of overlap - reconsider...
// NOTE: c struct vs classes
// Must use the "packed" attribute to make sure the compiler doesn't add any padding to deal with // Manufacturer data structure (outer envelope)
// word alignment.
typedef struct { typedef struct {
uint8_t deviceState; uint16_t vendorID; // Victron vendor ID (0x02E1)
uint8_t errorCode; uint8_t beaconType; // Should be 0x10 (Product Advertisement)
int16_t batteryVoltage; uint8_t modelID; // Model identifier byte
int16_t batteryCurrent; uint8_t readoutType; // Type of data readout
uint16_t todayYield; uint8_t victronRecordType; // Record type (device type)
uint16_t inputPower; uint16_t nonceDataCounter; // Nonce for encryption (IV bytes 0-1)
uint8_t outputCurrentLo; // Low 8 bits of output current (in 0.1 Amp increments) uint8_t encryptKeyMatch; // Should match pre-shared encryption key byte 0
uint8_t outputCurrentHi; // High 1 bit of ourput current (must mask off unused bits) uint8_t victronEncryptedData[21]; // Encrypted payload (max 21 bytes)
uint8_t unused[4];
} __attribute__((packed)) victronPanelData; // XXX Specific type -
typedef struct {
uint16_t vendorID; // vendor ID
uint8_t beaconType; // Should be 0x10 (Product Advertisement) for the packets we want
uint8_t unknownData1[3]; // Unknown data
uint8_t victronRecordType; // Should be 0x01 (Solar Charger) for the packets we want
uint16_t nonceDataCounter; // Nonce
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)) victronManufacturerData;
// XXX End of new bit above // 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 // Base structure for all device data
struct VictronDeviceData { struct VictronDeviceData {