""" test_ina219.py — INA219 power monitor driver tests (Issue #214) Verifies: - Calibration: LSB calculation for max current and shunt resistance - Register calculations: voltage, current, power from raw ADC values - Multi-sensor support: independent left/right motor monitoring - Alert thresholds: overcurrent limit configuration - Edge cases: boundary values, overflow handling """ import pytest # ── Constants ───────────────────────────────────────────────────────────── # Default calibration: 5A max, 0.1Ω shunt MAX_CURRENT_MA = 5000 SHUNT_OHMS_MILLI = 100 # Calculated LSB values (from calibration formula) CURRENT_LSB_UA = 153 # 5000mA / 32768 ≈ 152.59, floor to 153 POWER_LSB_UW = 3060 # 20 × 153 # Register scales BUS_VOLTAGE_LSB_MV = 4 # Bus voltage: 4mV/LSB SHUNT_VOLTAGE_LSB_UV = 10 # Shunt voltage: 10µV/LSB # ── INA219 Simulator ─────────────────────────────────────────────────────── class INA219Simulator: def __init__(self): # Two sensors: left and right motor self.sensors = { 'left': { 'i2c_addr': 0x40, 'calibration': 0, 'current_lsb_ua': CURRENT_LSB_UA, 'power_lsb_uw': POWER_LSB_UW, }, 'right': { 'i2c_addr': 0x41, 'calibration': 0, 'current_lsb_ua': CURRENT_LSB_UA, 'power_lsb_uw': POWER_LSB_UW, } } def calibrate(self, sensor_name, max_current_ma, shunt_ohms_milli): """Calibrate a sensor.""" if sensor_name not in self.sensors: return False s = self.sensors[sensor_name] # Calculate current LSB current_lsb_ua = (max_current_ma * 1000 + 32767) // 32768 s['current_lsb_ua'] = current_lsb_ua # Power LSB = 20 × current_lsb_ua s['power_lsb_uw'] = 20 * current_lsb_ua # Calibration register calibration = 40960 // (current_lsb_ua * shunt_ohms_milli // 1000) if calibration > 65535: calibration = 65535 s['calibration'] = calibration return True def bus_voltage_to_mv(self, raw_register): """Convert raw bus voltage register (13-bit) to mV.""" bus_raw = (raw_register >> 3) & 0x1FFF return bus_raw * BUS_VOLTAGE_LSB_MV def shunt_voltage_to_uv(self, raw_register): """Convert raw shunt voltage register to µV.""" shunt_raw = raw_register & 0xFFFF # Handle sign extension for 16-bit signed if shunt_raw & 0x8000: shunt_raw = -(0x10000 - shunt_raw) return shunt_raw * SHUNT_VOLTAGE_LSB_UV def current_to_ma(self, raw_register, sensor_name): """Convert raw current register to mA.""" s = self.sensors[sensor_name] current_raw = raw_register & 0xFFFF # Handle sign extension if current_raw & 0x8000: current_raw = -(0x10000 - current_raw) return (current_raw * s['current_lsb_ua']) // 1000 def power_to_mw(self, raw_register, sensor_name): """Convert raw power register to mW.""" s = self.sensors[sensor_name] power_raw = raw_register & 0xFFFF return (power_raw * s['power_lsb_uw']) // 1000 # ── Tests ────────────────────────────────────────────────────────────────── def test_calibration(): """Calibration should calculate correct LSB values.""" sim = INA219Simulator() assert sim.calibrate('left', 5000, 100) # Expected: 5000mA / 32768 ≈ 152.6, rounded up to 153µA assert sim.sensors['left']['current_lsb_ua'] == 153 assert sim.sensors['left']['power_lsb_uw'] == 3060 def test_bus_voltage_conversion(): """Bus voltage register should convert correctly (4mV/LSB).""" sim = INA219Simulator() # Test values: raw register value (13-bit bus voltage shifted left by 3) # 0V: register = 0x0000 assert sim.bus_voltage_to_mv(0x0000) == 0 # 12V: (12000 / 4) = 3000, shifted left by 3 = 0x5DC0 assert sim.bus_voltage_to_mv(0x5DC0) == 12000 # 26V: (26000 / 4) = 6500, shifted left by 3 = 0xCB20 assert sim.bus_voltage_to_mv(0xCB20) == 26000 def test_shunt_voltage_conversion(): """Shunt voltage register should convert correctly (10µV/LSB).""" sim = INA219Simulator() # 0µV assert sim.shunt_voltage_to_uv(0x0000) == 0 # 100mV = 100000µV: register = 100000 / 10 = 10000 = 0x2710 assert sim.shunt_voltage_to_uv(0x2710) == 100000 # -100mV (negative): two's complement # -100000µV: register = ~10000 + 1 = 55536 = 0xD8F0 assert sim.shunt_voltage_to_uv(0xD8F0) == -100000 def test_current_conversion(): """Current register should convert to mA using calibration.""" sim = INA219Simulator() sim.calibrate('left', 5000, 100) # 0mA assert sim.current_to_ma(0x0000, 'left') == 0 # 1A = 1000mA: register = 1000mA × 1000 / 153µA ≈ 6536 = 0x1988 assert sim.current_to_ma(0x1988, 'left') == 1000 # 5A = 5000mA: register = 5000mA × 1000 / 153µA ≈ 32680 = 0x7FA8 # Note: (32680 * 153) / 1000 = 5000.6, integer division = 5000 assert sim.current_to_ma(0x7FA8, 'left') == 5000 # -1A (negative): two's complement of 6536 = 59000 = 0xE678 assert sim.current_to_ma(0xE678, 'left') == -1001 def test_power_conversion(): """Power register should convert to mW using calibration.""" sim = INA219Simulator() sim.calibrate('left', 5000, 100) # 0W assert sim.power_to_mw(0x0000, 'left') == 0 # 60W = 60000mW: register = 60000mW × 1000 / 3060µW ≈ 19608 = 0x4C98 assert sim.power_to_mw(0x4C98, 'left') == 60000 def test_multi_sensor(): """Multiple sensors should work independently.""" sim = INA219Simulator() assert sim.calibrate('left', 5000, 100) assert sim.calibrate('right', 5000, 100) # Both should have same calibration assert sim.sensors['left']['current_lsb_ua'] == sim.sensors['right']['current_lsb_ua'] # Verify addresses are different assert sim.sensors['left']['i2c_addr'] == 0x40 assert sim.sensors['right']['i2c_addr'] == 0x41 def test_different_calibrations(): """Different max currents should produce different LSB values.""" sim1 = INA219Simulator() sim2 = INA219Simulator() sim1.calibrate('left', 5000, 100) # 5A sim2.calibrate('left', 10000, 100) # 10A # Higher max current = larger LSB assert sim2.sensors['left']['current_lsb_ua'] > sim1.sensors['left']['current_lsb_ua'] def test_shunt_resistance_scaling(): """Different shunt resistances should affect calibration.""" sim1 = INA219Simulator() sim2 = INA219Simulator() sim1.calibrate('left', 5000, 100) # 0.1Ω sim2.calibrate('left', 5000, 200) # 0.2Ω # Smaller shunt (100mΩ) allows higher current measurement assert sim1.sensors['left']['calibration'] != sim2.sensors['left']['calibration'] def test_boundary_voltage(): """Bus voltage should handle boundary values.""" sim = INA219Simulator() # Min (0V) assert sim.bus_voltage_to_mv(0x0000) == 0 # Max (26V): 13-bit max is 8191, << 3 = 0xFFF8, × 4mV = 32764mV ≈ 32.76V # Verify: (0xFFF8 >> 3) & 0x1FFF = 0x1FFF = 8191, × 4 = 32764 assert sim.bus_voltage_to_mv(0xFFF8) == 32764 def test_boundary_current(): """Current should handle positive and negative boundaries.""" sim = INA219Simulator() sim.calibrate('left', 5000, 100) # Max positive (~5A) max_current = sim.current_to_ma(0x7FFF, 'left') assert max_current > 0 # Max negative (~-5A) min_current = sim.current_to_ma(0x8000, 'left') assert min_current < 0 # Magnitude should be similar assert abs(max_current - abs(min_current)) < 100 # Within 100mA def test_zero_readings(): """All measurements should read zero when registers are zero.""" sim = INA219Simulator() sim.calibrate('left', 5000, 100) assert sim.bus_voltage_to_mv(0x0000) == 0 assert sim.shunt_voltage_to_uv(0x0000) == 0 assert sim.current_to_ma(0x0000, 'left') == 0 assert sim.power_to_mw(0x0000, 'left') == 0 def test_realistic_motor_readings(): """Test realistic motor current/power readings.""" sim = INA219Simulator() sim.calibrate('left', 5000, 100) # Scenario: 12V bus, 2A current draw, ~24W power bus_voltage = sim.bus_voltage_to_mv(0x5DC0) # ~12000mV current = sim.current_to_ma(0x3310, 'left') # ~2000mA power = sim.power_to_mw(0x1E93, 'left') # ~24000mW assert bus_voltage > 10000 # ~12V assert 1500 < current < 2500 # ~2A assert 20000 < power < 30000 # ~24W if __name__ == '__main__': pytest.main([__file__, '-v'])