BatteryState

Battery health and charge state for any battery-powered robot. Reports voltage, current draw, temperature, individual cell voltages, and charge percentage.

When to Use

Use BatteryState when your robot runs on batteries and you need to monitor power levels, trigger low-battery warnings, or initiate safe shutdown. Essential for mobile robots, drones, and any untethered system.

ROS2 Equivalent

sensor_msgs/BatteryState — similar structure (voltage, current, charge, capacity, temperature, cell voltages).

Rust Example

// simplified
use horus::prelude::*;

let battery = BatteryState {
    voltage: 12.6,
    current: -2.5,              // negative = discharging
    charge: f32::NAN,           // NaN if unknown
    capacity: f32::NAN,
    percentage: 85.0,           // 85% charge
    power_supply_status: 2,     // discharging
    temperature: 32.0,          // Celsius
    cell_voltages: [0.0; 16],
    cell_count: 0,
    timestamp_ns: 0,
};

let topic: Topic<BatteryState> = Topic::new("battery.state")?;
topic.send(battery);

Python Example

import horus

battery = horus.BatteryState(
    voltage=12.6,
    current=-2.5,
    percentage=85.0,
    temperature=32.0,
)

Fields

FieldTypeUnitDescription
voltagef32VTotal pack voltage
currentf32ACurrent draw (negative = discharging)
chargef32AhRemaining charge (NaN if unknown)
capacityf32AhFull capacity (NaN if unknown)
percentagef32%State of charge (0–100)
power_supply_statusu80=unknown, 1=charging, 2=discharging, 3=full
temperaturef32°CPack temperature
cell_voltages[f32; 16]VPer-cell voltages (if available)
cell_countu8Number of valid cell readings
timestamp_nsu64nsTimestamp

Common Patterns

Low Battery Warning

// simplified
fn tick(&mut self) {
    // IMPORTANT: always recv() every tick
    if let Some(battery) = self.battery_sub.recv() {
        if battery.percentage < 20.0 {
            hlog!(warn, "Low battery: {:.0}% ({:.1}V)", battery.percentage, battery.voltage);
        }
        if battery.percentage < 5.0 {
            // SAFETY: trigger safe shutdown
            hlog!(error, "Critical battery: {:.0}% — shutting down", battery.percentage);
            self.cmd_pub.send(CmdVel { linear: 0.0, angular: 0.0, timestamp_ns: 0 });
        }
    }
}

Quick Reference

Method / FieldTypeDescription
voltagef32Total pack voltage (V)
currentf32Current draw, negative = discharging (A)
percentagef32State of charge 0--100 (%)
temperaturef32Pack temperature (C)
power_supply_statusu80=unknown, 1=charging, 2=discharging, 3=full
cell_voltages[f32; 16]Per-cell voltages, valid up to cell_count
cell_countu8Number of valid cell readings
chargef32Remaining charge in Ah (NaN if unknown)
capacityf32Full capacity in Ah (NaN if unknown)

Design Decisions

Why NaN for unknown charge/capacity instead of Option or sentinel values? BatteryState is a fixed-size Pod type for zero-copy transport. Option<f32> is not Pod-compatible. IEEE 754 NaN is universally recognized as "not available" and works in both Rust and Python without extra wrapping.

Why negative current for discharging? Sign convention matches physics and electrical engineering standards: positive current flows into the battery (charging), negative flows out (discharging). This lets you compute power with voltage * current directly -- negative power means the battery is supplying energy.

Why 16 cell slots instead of variable-length? Fixed-size arrays enable zero-copy transport. 16 cells cover common battery configurations: 3S/4S/6S LiPo (drones), 4S-14S (ground robots), and most industrial packs. For larger packs, publish multiple BatteryState messages per pack segment.

Why no built-in state-of-health or cycle count? Battery health estimation algorithms vary widely by chemistry (LiPo, LiFePO4, NiMH) and require calibration data. HORUS provides the raw measurements; health estimation belongs in a domain-specific node that publishes DiagnosticStatus.


See Also