MSP_ANALOG (110)
Returns battery voltage, current consumption, and RSSI values.
Request
Empty payload (0 bytes).
Response Structure
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | U8 | vbat_legacy | Legacy voltage (deprecated) |
| 1-2 | U16 | mah_drawn | Consumed capacity in mAh |
| 3-4 | U16 | rssi | RSSI value (0-1023) |
| 5-6 | I16 | amperage | Current in 0.01A units |
| 7-8 | U16 | vbat | Battery 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
}
}
Related Commands
| Command | Code | Description |
|---|---|---|
| MSP_BATTERY_CONFIG | 32 | Battery parameters (cell voltages) |
| MSP_BATTERY_STATE | 130 | Extended battery info |
| MSP_VOLTAGE_METERS | 128 | All voltage meter readings |
| MSP_CURRENT_METERS | 129 | All current meter readings |
Notes
-
Legacy vbat: The first byte is deprecated and should be ignored. Use bytes 7-8 for voltage.
-
RSSI Source: RSSI can come from:
- Dedicated RSSI pin (ADC)
- Receiver protocol (SBUS, CRSF, etc.)
- Manual channel mapping
-
Current Sensor Calibration: The amperage reading requires proper current sensor calibration for accuracy.
-
Polling Rate: 10-20 Hz is typically sufficient for battery monitoring.