Skip to main content

MSP_ANALOG (110)

Returns battery voltage, current consumption, and RSSI values.

Request​

Empty payload (0 bytes).

Response Structure​

OffsetTypeFieldDescription
0U8vbat_legacyLegacy voltage (deprecated)
1-2U16mah_drawnConsumed capacity in mAh
3-4U16rssiRSSI value (0-1023)
5-6I16amperageCurrent in 0.01A units
7-8U16vbatBattery voltage in 0.01V units

Total size: 9 bytes

Field Details​

vbat (bytes 7-8)​

Battery voltage with 0.01V resolution.

  • Value 0: No battery detected
  • Range: 0-65535 (0-655.35V theoretical)
  • Typical: 1480 = 14.80V (4S LiPo)

amperage (bytes 5-6)​

Current draw in 0.01A (10mA) steps.

  • Signed: Negative values indicate charging
  • Range: -32768 to 32767 (-327.68A to +327.67A)
  • Typical: 2500 = 25.00A

mah_drawn (bytes 1-2)​

Cumulative capacity consumed since power-on.

  • Unit: milliamp-hours
  • Range: 0-65535 mAh
  • Reset: On power cycle or manual reset

rssi (bytes 3-4)​

Receiver signal strength indicator.

  • Range: 0-1023
  • 0: No signal / not configured
  • 1023: Maximum signal
  • Convert to %: (rssi / 1023) × 100

Rust Implementation​

/// Analog sensor readings
#[derive(Debug, Clone, Copy, Default)]
pub struct MspAnalog {
/// Consumed capacity in mAh
pub mah_drawn: u16,
/// RSSI value (0-1023)
pub rssi: u16,
/// Current in 0.01A units (signed)
pub amperage: i16,
/// Battery voltage in 0.01V units
pub vbat: u16,
}

impl MspAnalog {
pub const COMMAND: u8 = 110;
pub const SIZE: usize = 9;

/// Parse from MSP response payload
pub fn from_payload(data: &[u8]) -> Option<Self> {
if data.len() < Self::SIZE {
return None;
}

Some(MspAnalog {
mah_drawn: u16::from_le_bytes([data[1], data[2]]),
rssi: u16::from_le_bytes([data[3], data[4]]),
amperage: i16::from_le_bytes([data[5], data[6]]),
vbat: u16::from_le_bytes([data[7], data[8]]),
})
}

/// Battery voltage in volts
pub fn voltage(&self) -> f32 {
self.vbat as f32 / 100.0
}

/// Current draw in amps
pub fn current(&self) -> f32 {
self.amperage as f32 / 100.0
}

/// RSSI as percentage (0-100%)
pub fn rssi_percent(&self) -> f32 {
(self.rssi as f32 / 1023.0) * 100.0
}

/// Instantaneous power in watts
pub fn power_watts(&self) -> f32 {
self.voltage() * self.current()
}

/// Check if battery is connected
pub fn battery_connected(&self) -> bool {
self.vbat > 100 // > 1V indicates connected
}

/// Estimate cell count based on voltage
pub fn estimated_cell_count(&self) -> u8 {
let voltage = self.voltage();
match voltage {
v if v < 4.5 => 1,
v if v < 9.0 => 2,
v if v < 13.5 => 3,
v if v < 18.0 => 4,
v if v < 22.5 => 5,
v if v < 27.0 => 6,
_ => ((voltage / 4.2).ceil() as u8).max(1),
}
}

/// Average cell voltage
pub fn cell_voltage(&self) -> f32 {
let cells = self.estimated_cell_count();
if cells > 0 {
self.voltage() / cells as f32
} else {
0.0
}
}

/// Battery state based on cell voltage
pub fn battery_state(&self) -> BatteryState {
let cell_v = self.cell_voltage();
match cell_v {
v if v >= 3.8 => BatteryState::Good,
v if v >= 3.5 => BatteryState::Warning,
v if v >= 3.3 => BatteryState::Critical,
_ => BatteryState::Empty,
}
}
}

/// Battery state indicator
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BatteryState {
Good,
Warning,
Critical,
Empty,
}

impl core::fmt::Display for BatteryState {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
BatteryState::Good => write!(f, "Good"),
BatteryState::Warning => write!(f, "Warning"),
BatteryState::Critical => write!(f, "Critical"),
BatteryState::Empty => write!(f, "Empty"),
}
}
}

Raw Hex Example​

Request frame:

24 4D 3C 00 6E 6E
│ └─ Checksum (0 ^ 110 = 110)
└──── Command (110 = 0x6E)

Response frame:

24 4D 3E 09 6E
00 ← vbat_legacy (deprecated)
20 03 ← mah_drawn = 800 mAh
C8 02 ← rssi = 712 (~70%)
C4 09 ← amperage = 2500 (25.00A)
C8 05 ← vbat = 1480 (14.80V)
XX ← Checksum

Payload breakdown:

Offset 0:   00        → Legacy vbat (ignored)
Offset 1-2: 20 03 → mah_drawn = 0x0320 = 800 mAh
Offset 3-4: C8 02 → rssi = 0x02C8 = 712
Offset 5-6: C4 09 → amperage = 0x09C4 = 2500 (25.00A)
Offset 7-8: C8 05 → vbat = 0x05C8 = 1480 (14.80V)

Usage Example​

fn main() {
// Example: 4S battery at 14.8V, drawing 25A, 800mAh used
let payload = [
0x00, // legacy vbat
0x20, 0x03, // mah_drawn = 800
0xC8, 0x02, // rssi = 712
0xC4, 0x09, // amperage = 2500 (25.00A)
0xC8, 0x05, // vbat = 1480 (14.80V)
];

if let Some(analog) = MspAnalog::from_payload(&payload) {
println!("Battery Voltage: {:.2}V", analog.voltage());
println!("Current Draw: {:.2}A", analog.current());
println!("Power: {:.1}W", analog.power_watts());
println!("Capacity Used: {} mAh", analog.mah_drawn);
println!("RSSI: {:.0}%", analog.rssi_percent());
println!("Estimated Cells: {}S", analog.estimated_cell_count());
println!("Cell Voltage: {:.2}V", analog.cell_voltage());
println!("Battery State: {}", analog.battery_state());
}
}

Output:

Battery Voltage: 14.80V
Current Draw: 25.00A
Power: 370.0W
Capacity Used: 800 mAh
RSSI: 69.6%
Estimated Cells: 4S
Cell Voltage: 3.70V
Battery State: Warning

Battery Monitoring Utilities​

/// Battery monitoring with history
pub struct BatteryMonitor {
samples: Vec<MspAnalog>,
max_samples: usize,
capacity_mah: u16,
}

impl BatteryMonitor {
pub fn new(capacity_mah: u16, max_samples: usize) -> Self {
BatteryMonitor {
samples: Vec::with_capacity(max_samples),
max_samples,
capacity_mah,
}
}

pub fn update(&mut self, analog: MspAnalog) {
if self.samples.len() >= self.max_samples {
self.samples.remove(0);
}
self.samples.push(analog);
}

/// Remaining capacity percentage
pub fn remaining_percent(&self) -> f32 {
if let Some(latest) = self.samples.last() {
let remaining = self.capacity_mah.saturating_sub(latest.mah_drawn);
(remaining as f32 / self.capacity_mah as f32) * 100.0
} else {
100.0
}
}

/// Average current over sample window
pub fn average_current(&self) -> f32 {
if self.samples.is_empty() {
return 0.0;
}
let sum: f32 = self.samples.iter().map(|s| s.current()).sum();
sum / self.samples.len() as f32
}

/// Estimated flight time remaining (minutes)
pub fn estimated_flight_time(&self) -> f32 {
let avg_current = self.average_current();
if avg_current <= 0.0 {
return f32::INFINITY;
}

if let Some(latest) = self.samples.last() {
let remaining_mah = self.capacity_mah.saturating_sub(latest.mah_drawn);
(remaining_mah as f32 / avg_current) / 60.0 // Convert to minutes
} else {
f32::INFINITY
}
}

/// Voltage trend (positive = charging/recovering, negative = discharging)
pub fn voltage_trend(&self) -> f32 {
if self.samples.len() < 2 {
return 0.0;
}
let first = self.samples.first().unwrap().voltage();
let last = self.samples.last().unwrap().voltage();
last - first
}
}
CommandCodeDescription
MSP_BATTERY_CONFIG32Battery parameters (cell voltages)
MSP_BATTERY_STATE130Extended battery info
MSP_VOLTAGE_METERS128All voltage meter readings
MSP_CURRENT_METERS129All current meter readings

Notes​

  1. Legacy vbat: The first byte is deprecated and should be ignored. Use bytes 7-8 for voltage.

  2. RSSI Source: RSSI can come from:

    • Dedicated RSSI pin (ADC)
    • Receiver protocol (SBUS, CRSF, etc.)
    • Manual channel mapping
  3. Current Sensor Calibration: The amperage reading requires proper current sensor calibration for accuracy.

  4. Polling Rate: 10-20 Hz is typically sufficient for battery monitoring.