|
| 1 | +""" |
| 2 | +BatteryManager - Android-inspired battery and power information API. |
| 3 | +
|
| 4 | +Provides direct query access to battery voltage, charge percentage, and raw ADC values. |
| 5 | +Handles ADC1/ADC2 pin differences on ESP32-S3 with adaptive caching to minimize WiFi interference. |
| 6 | +""" |
| 7 | + |
| 8 | +import time |
| 9 | + |
| 10 | +MIN_VOLTAGE = 3.15 |
| 11 | +MAX_VOLTAGE = 4.15 |
| 12 | + |
| 13 | +# Internal state |
| 14 | +_adc = None |
| 15 | +_conversion_func = None |
| 16 | +_adc_pin = None |
| 17 | + |
| 18 | +# Cache to reduce WiFi interruptions (ADC2 requires WiFi to be disabled) |
| 19 | +_cached_raw_adc = None |
| 20 | +_last_read_time = 0 |
| 21 | +CACHE_DURATION_ADC1_MS = 30000 # 30 seconds (cheaper: no WiFi interference) |
| 22 | +CACHE_DURATION_ADC2_MS = 600000 # 600 seconds (expensive: requires WiFi disable) |
| 23 | + |
| 24 | + |
| 25 | +def _is_adc2_pin(pin): |
| 26 | + """Check if pin is on ADC2 (ESP32-S3: GPIO11-20).""" |
| 27 | + return 11 <= pin <= 20 |
| 28 | + |
| 29 | + |
| 30 | +class BatteryManager: |
| 31 | + """ |
| 32 | + Android-inspired BatteryManager for querying battery and power information. |
| 33 | + |
| 34 | + Provides static methods for battery voltage, percentage, and raw ADC readings. |
| 35 | + Automatically handles ADC1/ADC2 differences and WiFi coordination on ESP32-S3. |
| 36 | + """ |
| 37 | + |
| 38 | + @staticmethod |
| 39 | + def init_adc(pinnr, adc_to_voltage_func): |
| 40 | + """ |
| 41 | + Initialize ADC for battery voltage monitoring. |
| 42 | +
|
| 43 | + IMPORTANT for ESP32-S3: ADC2 (GPIO11-20) doesn't work when WiFi is active! |
| 44 | + Use ADC1 pins (GPIO1-10) for battery monitoring if possible. |
| 45 | + If using ADC2, WiFi will be temporarily disabled during readings. |
| 46 | +
|
| 47 | + Args: |
| 48 | + pinnr: GPIO pin number |
| 49 | + adc_to_voltage_func: Conversion function that takes raw ADC value (0-4095) |
| 50 | + and returns battery voltage in volts |
| 51 | + """ |
| 52 | + global _adc, _conversion_func, _adc_pin |
| 53 | + |
| 54 | + _conversion_func = adc_to_voltage_func |
| 55 | + _adc_pin = pinnr |
| 56 | + |
| 57 | + try: |
| 58 | + print(f"Initializing ADC pin {pinnr} with conversion function") |
| 59 | + if _is_adc2_pin(pinnr): |
| 60 | + print(f" WARNING: GPIO{pinnr} is on ADC2 - WiFi will be disabled during readings") |
| 61 | + from machine import ADC, Pin |
| 62 | + _adc = ADC(Pin(pinnr)) |
| 63 | + _adc.atten(ADC.ATTN_11DB) # 0-3.3V range |
| 64 | + except Exception as e: |
| 65 | + print(f"Info: this platform has no ADC for measuring battery voltage: {e}") |
| 66 | + |
| 67 | + initial_adc_value = BatteryManager.read_raw_adc() |
| 68 | + print(f"Reading ADC at init to fill cache: {initial_adc_value} => {BatteryManager.read_battery_voltage(raw_adc_value=initial_adc_value)}V => {BatteryManager.get_battery_percentage(raw_adc_value=initial_adc_value)}%") |
| 69 | + |
| 70 | + @staticmethod |
| 71 | + def read_raw_adc(force_refresh=False): |
| 72 | + """ |
| 73 | + Read raw ADC value (0-4095) with adaptive caching. |
| 74 | +
|
| 75 | + On ESP32-S3 with ADC2, WiFi is temporarily disabled during reading. |
| 76 | + Raises RuntimeError if WifiService is busy (connecting/scanning) when using ADC2. |
| 77 | +
|
| 78 | + Args: |
| 79 | + force_refresh: Bypass cache and force fresh reading |
| 80 | +
|
| 81 | + Returns: |
| 82 | + float: Raw ADC value (0-4095) |
| 83 | +
|
| 84 | + Raises: |
| 85 | + RuntimeError: If WifiService is busy (only when using ADC2) |
| 86 | + """ |
| 87 | + global _cached_raw_adc, _last_read_time |
| 88 | + |
| 89 | + # Desktop mode - return random value in typical ADC range |
| 90 | + if not _adc: |
| 91 | + import random |
| 92 | + return random.randint(1900, 2600) |
| 93 | + |
| 94 | + # Check if this is an ADC2 pin (requires WiFi disable) |
| 95 | + needs_wifi_disable = _adc_pin is not None and _is_adc2_pin(_adc_pin) |
| 96 | + |
| 97 | + # Use different cache durations based on cost |
| 98 | + cache_duration = CACHE_DURATION_ADC2_MS if needs_wifi_disable else CACHE_DURATION_ADC1_MS |
| 99 | + |
| 100 | + # Check cache |
| 101 | + current_time = time.ticks_ms() |
| 102 | + if not force_refresh and _cached_raw_adc is not None: |
| 103 | + age = time.ticks_diff(current_time, _last_read_time) |
| 104 | + if age < cache_duration: |
| 105 | + return _cached_raw_adc |
| 106 | + |
| 107 | + # Import WifiService only if needed |
| 108 | + WifiService = None |
| 109 | + if needs_wifi_disable: |
| 110 | + try: |
| 111 | + # Needs actual path, not "from mpos" shorthand because it's mocked by test_battery_voltage.py |
| 112 | + from mpos.net.wifi_service import WifiService |
| 113 | + except ImportError: |
| 114 | + pass |
| 115 | + |
| 116 | + # Temporarily disable WiFi for ADC2 reading |
| 117 | + was_connected = False |
| 118 | + if needs_wifi_disable and WifiService: |
| 119 | + # This will raise RuntimeError if WiFi is already busy |
| 120 | + was_connected = WifiService.temporarily_disable() |
| 121 | + time.sleep(0.05) # Brief delay for WiFi to fully disable |
| 122 | + |
| 123 | + try: |
| 124 | + # Read ADC (average of 10 samples) |
| 125 | + total = sum(_adc.read() for _ in range(10)) |
| 126 | + raw_value = total / 10.0 |
| 127 | + |
| 128 | + # Update cache |
| 129 | + _cached_raw_adc = raw_value |
| 130 | + _last_read_time = current_time |
| 131 | + |
| 132 | + return raw_value |
| 133 | + |
| 134 | + finally: |
| 135 | + # Re-enable WiFi (only if we disabled it) |
| 136 | + if needs_wifi_disable and WifiService: |
| 137 | + WifiService.temporarily_enable(was_connected) |
| 138 | + |
| 139 | + @staticmethod |
| 140 | + def read_battery_voltage(force_refresh=False, raw_adc_value=None): |
| 141 | + """ |
| 142 | + Read battery voltage in volts. |
| 143 | +
|
| 144 | + Args: |
| 145 | + force_refresh: Bypass cache and force fresh reading |
| 146 | + raw_adc_value: Optional pre-computed raw ADC value (for testing) |
| 147 | +
|
| 148 | + Returns: |
| 149 | + float: Battery voltage in volts (clamped to 0-MAX_VOLTAGE) |
| 150 | + """ |
| 151 | + raw = raw_adc_value if raw_adc_value else BatteryManager.read_raw_adc(force_refresh) |
| 152 | + voltage = _conversion_func(raw) if _conversion_func else 0.0 |
| 153 | + return voltage |
| 154 | + |
| 155 | + @staticmethod |
| 156 | + def get_battery_percentage(raw_adc_value=None): |
| 157 | + """ |
| 158 | + Get battery charge percentage. |
| 159 | +
|
| 160 | + Args: |
| 161 | + raw_adc_value: Optional pre-computed raw ADC value (for testing) |
| 162 | +
|
| 163 | + Returns: |
| 164 | + float: Battery percentage (0-100) |
| 165 | + """ |
| 166 | + voltage = BatteryManager.read_battery_voltage(raw_adc_value=raw_adc_value) |
| 167 | + percentage = (voltage - MIN_VOLTAGE) * 100.0 / (MAX_VOLTAGE - MIN_VOLTAGE) |
| 168 | + return max(0, min(100.0, percentage)) # limit to 100.0% and make sure it's positive |
| 169 | + |
| 170 | + @staticmethod |
| 171 | + def clear_cache(): |
| 172 | + """Clear the battery voltage cache to force fresh reading on next call.""" |
| 173 | + global _cached_raw_adc, _last_read_time |
| 174 | + _cached_raw_adc = None |
| 175 | + _last_read_time = 0 |
0 commit comments