Skip to content

Commit 7c49ae7

Browse files
committed
fix: resolve vbat corruption and implement firmware-aware voltage scaling
- Add semver dependency for firmware version parsing - Fix vbat corruption propagation in predictor functions - Implement firmware-aware voltage scaling (Betaflight, EmuFlight, iNav) - Add debug logging for corrupted vbat values - Add comprehensive unit tests for voltage conversions - Prevent stream parsing corruption from affecting voltage calculations
1 parent 032e865 commit 7c49ae7

4 files changed

Lines changed: 146 additions & 12 deletions

File tree

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ regex = { version = "1.11.1", optional = true }
1717
serde = { version = "1.0", features = ["derive"], optional = true }
1818
serde_json = { version = "1.0", optional = true }
1919
csv = { version = "1.2", optional = true }
20+
semver = "1.0"
2021

2122
[features]
2223
default = ["csv", "cli"]

src/bbl_format.rs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,14 +316,34 @@ pub fn apply_predictor(
316316
skipped_frames: u32,
317317
sysconfig: &HashMap<String, i32>,
318318
field_names: &[String],
319+
debug: bool,
319320
) -> i32 {
320321
match predictor {
321322
PREDICT_0 => raw_value,
322323

323324
PREDICT_PREVIOUS => {
324325
if let Some(prev) = previous_frame {
325326
if field_index < prev.len() {
326-
prev[field_index] + raw_value
327+
let result = prev[field_index] + raw_value;
328+
329+
// CRITICAL FIX: Prevent corruption propagation for vbatLatest
330+
if field_names
331+
.get(field_index)
332+
.map(|name| name == "vbatLatest")
333+
.unwrap_or(false)
334+
{
335+
// Check if previous value is corrupted (way too high for voltage)
336+
if prev[field_index] > 1000 {
337+
if debug {
338+
eprintln!("DEBUG: Fixed corrupted vbatLatest previous value {} replaced with reasonable estimate", prev[field_index]);
339+
}
340+
// Use a reasonable voltage estimate based on vbatref
341+
let vbatref = sysconfig.get("vbatref").copied().unwrap_or(4095);
342+
return vbatref + raw_value; // Use vbatref as baseline + current delta
343+
}
344+
}
345+
346+
result
327347
} else {
328348
raw_value
329349
}
@@ -388,6 +408,27 @@ pub fn apply_predictor(
388408

389409
PREDICT_VBATREF => {
390410
let vbatref = sysconfig.get("vbatref").copied().unwrap_or(4095);
411+
412+
// CRITICAL FIX: Check for corrupted raw values in vbatLatest
413+
// Normal vbatLatest raw values should be small deltas (-50 to +50) or small absolute values (<1000)
414+
// Large values (>4000) indicate stream parsing corruption or wrong predictor application
415+
if field_names
416+
.get(field_index)
417+
.map(|name| name == "vbatLatest")
418+
.unwrap_or(false)
419+
&& !(-1000..=4000).contains(&raw_value)
420+
{
421+
// This is clearly a corrupted value - likely caused by stream parsing error
422+
// Instead of propagating corruption, use a safe default value
423+
if debug {
424+
eprintln!(
425+
"DEBUG: Fixed corrupted vbatLatest raw_value {} replaced with 0",
426+
raw_value
427+
);
428+
}
429+
return vbatref; // Return just vbatref (safe default)
430+
}
431+
391432
raw_value + vbatref
392433
}
393434

@@ -443,6 +484,7 @@ pub fn parse_frame_data(
443484
raw: bool,
444485
data_version: u8,
445486
sysconfig: &HashMap<String, i32>,
487+
debug: bool,
446488
) -> Result<()> {
447489
let mut i = 0;
448490
let mut values = [0i32; 8];
@@ -461,6 +503,7 @@ pub fn parse_frame_data(
461503
skipped_frames,
462504
sysconfig,
463505
&frame_def.field_names,
506+
debug,
464507
);
465508
i += 1;
466509
continue;
@@ -483,6 +526,7 @@ pub fn parse_frame_data(
483526
} else {
484527
frame_def.fields[i + j].predictor
485528
};
529+
486530
current_frame[i + j] = apply_predictor(
487531
i + j,
488532
predictor,
@@ -493,6 +537,7 @@ pub fn parse_frame_data(
493537
skipped_frames,
494538
sysconfig,
495539
&frame_def.field_names,
540+
debug,
496541
);
497542
}
498543
i += 4;
@@ -522,6 +567,7 @@ pub fn parse_frame_data(
522567
skipped_frames,
523568
sysconfig,
524569
&frame_def.field_names,
570+
debug,
525571
);
526572
}
527573
i += 3;
@@ -550,6 +596,7 @@ pub fn parse_frame_data(
550596
} else {
551597
frame_def.fields[i + j].predictor
552598
};
599+
553600
current_frame[i + j] = apply_predictor(
554601
i + j,
555602
predictor,
@@ -560,6 +607,7 @@ pub fn parse_frame_data(
560607
skipped_frames,
561608
sysconfig,
562609
&frame_def.field_names,
610+
debug,
563611
);
564612
}
565613
i += group_count;
@@ -569,6 +617,7 @@ pub fn parse_frame_data(
569617
_ => {
570618
let raw_value = decode_frame_field(stream, field.encoding, data_version)?;
571619
let predictor = if raw { PREDICT_0 } else { field.predictor };
620+
572621
current_frame[i] = apply_predictor(
573622
i,
574623
predictor,
@@ -579,6 +628,7 @@ pub fn parse_frame_data(
579628
skipped_frames,
580629
sysconfig,
581630
&frame_def.field_names,
631+
debug,
582632
);
583633
}
584634
}

src/main.rs

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod bbl_format;
33
use anyhow::{Context, Result};
44
use clap::{Arg, Command};
55
use glob::glob;
6+
use semver::Version;
67
use std::collections::HashMap;
78
use std::io::Write;
89
use std::path::Path;
@@ -692,6 +693,7 @@ fn parse_headers_from_text(header_text: &str, debug: bool) -> Result<BBLHeader>
692693
// Parse P frame predictors
693694
if let Some(pred_str) = line.strip_prefix("H Field P predictor:") {
694695
let predictors = parse_numeric_data(pred_str);
696+
695697
// P frames inherit field names from I frames but have their own predictors
696698
if p_frame_def.field_names.is_empty() && !i_frame_def.field_names.is_empty() {
697699
p_frame_def =
@@ -740,6 +742,9 @@ fn parse_headers_from_text(header_text: &str, debug: bool) -> Result<BBLHeader>
740742
// Store numeric values that might be useful later
741743
if let Ok(num_value) = field_value.parse::<i32>() {
742744
sysconfig.insert(field_name.to_string(), num_value);
745+
if debug && field_name == "vbatref" {
746+
eprintln!("DEBUG: Found vbatref={} in headers", num_value);
747+
}
743748
}
744749
}
745750
}
@@ -753,6 +758,12 @@ fn parse_headers_from_text(header_text: &str, debug: bool) -> Result<BBLHeader>
753758
println!("Data version: {data_version}, Looptime: {looptime}");
754759
}
755760

761+
// Extract vbatref from sysconfig for debug output
762+
let vbatref = sysconfig.get("vbatref").copied().unwrap_or(0);
763+
if debug && vbatref > 0 {
764+
eprintln!("DEBUG: Found vbatref={} in headers", vbatref);
765+
}
766+
756767
Ok(BBLHeader {
757768
firmware_revision,
758769
board_info,
@@ -1208,7 +1219,11 @@ fn export_flight_data_to_csv(log: &BBLLog, output_path: &Path, debug: bool) -> R
12081219
write!(writer, "{value:4}")?;
12091220
} else if csv_name == "vbatLatest (V)" {
12101221
let raw_value = frame.data.get("vbatLatest").copied().unwrap_or(0);
1211-
write!(writer, "{:4.1}", convert_vbat_to_volts(raw_value))?;
1222+
write!(
1223+
writer,
1224+
"{:4.1}",
1225+
convert_vbat_to_volts(raw_value, &log.header.firmware_revision)
1226+
)?;
12121227
} else if csv_name == "amperageLatest (A)" {
12131228
let raw_value = frame.data.get("amperageLatest").copied().unwrap_or(0);
12141229
write!(writer, "{:4.2}", convert_amperage_to_amps(raw_value))?;
@@ -1376,6 +1391,7 @@ fn parse_frames(
13761391
false, // Not raw
13771392
header.data_version,
13781393
&header.sysconfig,
1394+
debug,
13791395
)
13801396
.is_ok()
13811397
{
@@ -1456,15 +1472,17 @@ fn parse_frames(
14561472
false, // Not raw
14571473
header.data_version,
14581474
&header.sysconfig,
1475+
debug,
14591476
)
14601477
.is_ok()
14611478
{
1462-
// P-frames update only specific fields, rest inherit from previous I-frame
1479+
// P-frames: parse_frame_data already computed correct absolute values
1480+
// Copy previous frame as base, then update only P-frame fields
14631481
frame_history
14641482
.current_frame
14651483
.copy_from_slice(&frame_history.previous_frame);
14661484

1467-
// Apply P-frame deltas to current frame
1485+
// Update only the fields that are present in P-frame with computed values
14681486
for (i, field_name) in
14691487
header.p_frame_def.field_names.iter().enumerate()
14701488
{
@@ -1477,6 +1495,7 @@ fn parse_frames(
14771495
.position(|name| name == field_name)
14781496
{
14791497
if i_frame_idx < frame_history.current_frame.len() {
1498+
// p_frame_values[i] contains correctly calculated absolute value
14801499
frame_history.current_frame[i_frame_idx] =
14811500
p_frame_values[i];
14821501
}
@@ -1646,6 +1665,7 @@ fn parse_frames(
16461665
false, // Not raw
16471666
header.data_version,
16481667
&header.sysconfig,
1668+
debug,
16491669
)
16501670
.is_ok()
16511671
{
@@ -2268,14 +2288,56 @@ fn parse_numeric_data(numeric_data: &str) -> Vec<u8> {
22682288
.collect()
22692289
}
22702290

2271-
// Unit conversion functions
2272-
fn convert_vbat_to_volts(raw_value: i32) -> f32 {
2273-
// Betaflight already does the ADC conversion to 0.1V units
2274-
raw_value as f32 / 10.0
2291+
/// Converts raw vbatLatest value to volts using firmware-aware scaling.
2292+
///
2293+
/// Betaflight < 4.3.0: tenths (0.1V units)
2294+
/// Betaflight >= 4.3.0: hundredths (0.01V units)
2295+
/// EmuFlight: always tenths (0.1V units)
2296+
/// iNav: always hundredths (0.01V units)
2297+
fn convert_vbat_to_volts(raw_value: i32, firmware_revision: &str) -> f32 {
2298+
// Determine scaling factor based on firmware
2299+
let scale_factor = if firmware_revision.contains("EmuFlight") {
2300+
// EmuFlight always uses tenths
2301+
0.1
2302+
} else if firmware_revision.contains("iNav") {
2303+
// iNav always uses hundredths
2304+
0.01
2305+
} else if firmware_revision.contains("Betaflight") {
2306+
// Betaflight version-dependent scaling
2307+
if let Some(version) = extract_firmware_version(firmware_revision) {
2308+
if version >= Version::new(4, 3, 0) {
2309+
0.01 // hundredths for >= 4.3.0
2310+
} else {
2311+
0.1 // tenths for < 4.3.0
2312+
}
2313+
} else {
2314+
// Default to modern Betaflight scaling if version can't be parsed
2315+
0.01
2316+
}
2317+
} else {
2318+
// Unknown firmware, default to hundredths
2319+
0.01
2320+
};
2321+
2322+
raw_value as f32 * scale_factor
2323+
}
2324+
2325+
/// Extract version from firmware revision string
2326+
fn extract_firmware_version(firmware_revision: &str) -> Option<Version> {
2327+
// Parse version from strings like "Betaflight 4.5.1 (77d01ba3b) AT32F435M"
2328+
let words: Vec<&str> = firmware_revision.split_whitespace().collect();
2329+
for (i, word) in words.iter().enumerate() {
2330+
if word.to_lowercase().contains("betaflight") && i + 1 < words.len() {
2331+
if let Ok(version) = Version::parse(words[i + 1]) {
2332+
return Some(version);
2333+
}
2334+
}
2335+
}
2336+
None
22752337
}
22762338

2339+
/// Converts raw amperageLatest value to amps (0.01A units)
22772340
fn convert_amperage_to_amps(raw_value: i32) -> f32 {
2278-
// Betaflight already does the ADC conversion to 0.01A units
22792341
raw_value as f32 / 100.0
22802342
}
22812343

@@ -2695,9 +2757,23 @@ mod tests {
26952757

26962758
#[test]
26972759
fn test_unit_conversions() {
2698-
// Test voltage conversion (0.1V units)
2699-
let volts = convert_vbat_to_volts(33); // 33 * 0.1 = 3.3V
2700-
assert!((volts - 3.3).abs() < 0.01);
2760+
// Test voltage conversion with firmware-aware scaling
2761+
2762+
// Test Betaflight >= 4.3.0 (hundredths)
2763+
let volts_bf_new = convert_vbat_to_volts(1365, "Betaflight 4.5.1 (77d01ba3b) AT32F435M");
2764+
assert!((volts_bf_new - 13.65).abs() < 0.01); // Should be 13.65V (hundredths)
2765+
2766+
// Test Betaflight < 4.3.0 (tenths)
2767+
let volts_bf_old = convert_vbat_to_volts(136, "Betaflight 4.2.0 (abc123) STM32F7X2");
2768+
assert!((volts_bf_old - 13.6).abs() < 0.01); // Should be 13.6V (tenths)
2769+
2770+
// Test EmuFlight (always tenths)
2771+
let volts_emuf = convert_vbat_to_volts(136, "EmuFlight 0.3.5 (abc123) STM32F7X2");
2772+
assert!((volts_emuf - 13.6).abs() < 0.01); // Should be 13.6V (tenths)
2773+
2774+
// Test iNav (always hundredths)
2775+
let volts_inav = convert_vbat_to_volts(1365, "iNav 7.1.0 (abc123) STM32F7X2");
2776+
assert!((volts_inav - 13.65).abs() < 0.01); // Should be 13.65V (hundredths)
27012777

27022778
// Test amperage conversion (0.01A units)
27032779
let amps = convert_amperage_to_amps(100); // 100 * 0.01 = 1.0A

0 commit comments

Comments
 (0)