You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
Add new BatteryManager framework
This commit is contained in:
@@ -8,8 +8,10 @@
|
||||
- ActivityNavigator: support pre-instantiated activities to support one activity closing a child activity
|
||||
- Rename PackageManager to AppManager framework
|
||||
- Add new AppearanceManager framework
|
||||
- Add new BatteryManager framework
|
||||
- Add new DeviceInfo framework
|
||||
- Add new DisplayMetrics framework
|
||||
- Add new InputManager framework
|
||||
- Add new VersionInfo framework
|
||||
- Additional board support: Fri3d Camp 2026 (untested on real hardware)
|
||||
- Harmonize frameworks to use same coding patterns
|
||||
|
||||
@@ -49,7 +49,7 @@ battery power:
|
||||
import lvgl as lv
|
||||
import time
|
||||
|
||||
from mpos import battery_voltage, Activity
|
||||
from mpos import BatteryManager, Activity
|
||||
|
||||
class Hello(Activity):
|
||||
|
||||
@@ -70,9 +70,9 @@ class Hello(Activity):
|
||||
|
||||
def update_bat(timer):
|
||||
#global l
|
||||
r = battery_voltage.read_raw_adc()
|
||||
v = battery_voltage.read_battery_voltage()
|
||||
percent = battery_voltage.get_battery_percentage()
|
||||
r = BatteryManager.read_raw_adc()
|
||||
v = BatteryManager.read_battery_voltage()
|
||||
percent = BatteryManager.get_battery_percentage()
|
||||
text = f"{time.localtime()}\n{r}\n{v}V\n{percent}%"
|
||||
#text = f"{time.localtime()}: {r}"
|
||||
print(text)
|
||||
|
||||
@@ -17,6 +17,9 @@ from .time_zone import TimeZone
|
||||
from .device_info import DeviceInfo
|
||||
from .build_info import BuildInfo
|
||||
|
||||
# Battery manager (imported early for UI dependencies)
|
||||
from .battery_manager import BatteryManager
|
||||
|
||||
# Common activities
|
||||
from .app.activities.chooser import ChooserActivity
|
||||
from .app.activities.view import ViewActivity
|
||||
@@ -56,7 +59,6 @@ from . import time
|
||||
from . import sensor_manager
|
||||
from . import camera_manager
|
||||
from . import sdcard
|
||||
from . import battery_voltage
|
||||
from . import audio
|
||||
from . import hardware
|
||||
|
||||
@@ -66,7 +68,7 @@ __all__ = [
|
||||
"Activity",
|
||||
"SharedPreferences",
|
||||
"ConnectivityManager", "DownloadManager", "WifiService", "AudioFlinger", "Intent",
|
||||
"ActivityNavigator", "AppManager", "TaskManager", "CameraManager",
|
||||
"ActivityNavigator", "AppManager", "TaskManager", "CameraManager", "BatteryManager",
|
||||
# Device and build info
|
||||
"DeviceInfo", "BuildInfo",
|
||||
# Common activities
|
||||
@@ -93,7 +95,7 @@ __all__ = [
|
||||
"get_all_widgets_with_text",
|
||||
# Submodules
|
||||
"ui", "config", "net", "content", "time", "sensor_manager",
|
||||
"camera_manager", "sdcard", "battery_voltage", "audio", "hardware", "bootloader",
|
||||
"camera_manager", "sdcard", "audio", "hardware", "bootloader",
|
||||
# Timezone utilities
|
||||
"TimeZone"
|
||||
]
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
"""
|
||||
BatteryManager - Android-inspired battery and power information API.
|
||||
|
||||
Provides direct query access to battery voltage, charge percentage, and raw ADC values.
|
||||
Handles ADC1/ADC2 pin differences on ESP32-S3 with adaptive caching to minimize WiFi interference.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
MIN_VOLTAGE = 3.15
|
||||
MAX_VOLTAGE = 4.15
|
||||
|
||||
# Internal state
|
||||
_adc = None
|
||||
_conversion_func = None
|
||||
_adc_pin = None
|
||||
|
||||
# Cache to reduce WiFi interruptions (ADC2 requires WiFi to be disabled)
|
||||
_cached_raw_adc = None
|
||||
_last_read_time = 0
|
||||
CACHE_DURATION_ADC1_MS = 30000 # 30 seconds (cheaper: no WiFi interference)
|
||||
CACHE_DURATION_ADC2_MS = 600000 # 600 seconds (expensive: requires WiFi disable)
|
||||
|
||||
|
||||
def _is_adc2_pin(pin):
|
||||
"""Check if pin is on ADC2 (ESP32-S3: GPIO11-20)."""
|
||||
return 11 <= pin <= 20
|
||||
|
||||
|
||||
class BatteryManager:
|
||||
"""
|
||||
Android-inspired BatteryManager for querying battery and power information.
|
||||
|
||||
Provides static methods for battery voltage, percentage, and raw ADC readings.
|
||||
Automatically handles ADC1/ADC2 differences and WiFi coordination on ESP32-S3.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def init_adc(pinnr, adc_to_voltage_func):
|
||||
"""
|
||||
Initialize ADC for battery voltage monitoring.
|
||||
|
||||
IMPORTANT for ESP32-S3: ADC2 (GPIO11-20) doesn't work when WiFi is active!
|
||||
Use ADC1 pins (GPIO1-10) for battery monitoring if possible.
|
||||
If using ADC2, WiFi will be temporarily disabled during readings.
|
||||
|
||||
Args:
|
||||
pinnr: GPIO pin number
|
||||
adc_to_voltage_func: Conversion function that takes raw ADC value (0-4095)
|
||||
and returns battery voltage in volts
|
||||
"""
|
||||
global _adc, _conversion_func, _adc_pin
|
||||
|
||||
_conversion_func = adc_to_voltage_func
|
||||
_adc_pin = pinnr
|
||||
|
||||
try:
|
||||
print(f"Initializing ADC pin {pinnr} with conversion function")
|
||||
if _is_adc2_pin(pinnr):
|
||||
print(f" WARNING: GPIO{pinnr} is on ADC2 - WiFi will be disabled during readings")
|
||||
from machine import ADC, Pin
|
||||
_adc = ADC(Pin(pinnr))
|
||||
_adc.atten(ADC.ATTN_11DB) # 0-3.3V range
|
||||
except Exception as e:
|
||||
print(f"Info: this platform has no ADC for measuring battery voltage: {e}")
|
||||
|
||||
initial_adc_value = BatteryManager.read_raw_adc()
|
||||
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)}%")
|
||||
|
||||
@staticmethod
|
||||
def read_raw_adc(force_refresh=False):
|
||||
"""
|
||||
Read raw ADC value (0-4095) with adaptive caching.
|
||||
|
||||
On ESP32-S3 with ADC2, WiFi is temporarily disabled during reading.
|
||||
Raises RuntimeError if WifiService is busy (connecting/scanning) when using ADC2.
|
||||
|
||||
Args:
|
||||
force_refresh: Bypass cache and force fresh reading
|
||||
|
||||
Returns:
|
||||
float: Raw ADC value (0-4095)
|
||||
|
||||
Raises:
|
||||
RuntimeError: If WifiService is busy (only when using ADC2)
|
||||
"""
|
||||
global _cached_raw_adc, _last_read_time
|
||||
|
||||
# Desktop mode - return random value in typical ADC range
|
||||
if not _adc:
|
||||
import random
|
||||
return random.randint(1900, 2600)
|
||||
|
||||
# Check if this is an ADC2 pin (requires WiFi disable)
|
||||
needs_wifi_disable = _adc_pin is not None and _is_adc2_pin(_adc_pin)
|
||||
|
||||
# Use different cache durations based on cost
|
||||
cache_duration = CACHE_DURATION_ADC2_MS if needs_wifi_disable else CACHE_DURATION_ADC1_MS
|
||||
|
||||
# Check cache
|
||||
current_time = time.ticks_ms()
|
||||
if not force_refresh and _cached_raw_adc is not None:
|
||||
age = time.ticks_diff(current_time, _last_read_time)
|
||||
if age < cache_duration:
|
||||
return _cached_raw_adc
|
||||
|
||||
# Import WifiService only if needed
|
||||
WifiService = None
|
||||
if needs_wifi_disable:
|
||||
try:
|
||||
# Needs actual path, not "from mpos" shorthand because it's mocked by test_battery_voltage.py
|
||||
from mpos.net.wifi_service import WifiService
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Temporarily disable WiFi for ADC2 reading
|
||||
was_connected = False
|
||||
if needs_wifi_disable and WifiService:
|
||||
# This will raise RuntimeError if WiFi is already busy
|
||||
was_connected = WifiService.temporarily_disable()
|
||||
time.sleep(0.05) # Brief delay for WiFi to fully disable
|
||||
|
||||
try:
|
||||
# Read ADC (average of 10 samples)
|
||||
total = sum(_adc.read() for _ in range(10))
|
||||
raw_value = total / 10.0
|
||||
|
||||
# Update cache
|
||||
_cached_raw_adc = raw_value
|
||||
_last_read_time = current_time
|
||||
|
||||
return raw_value
|
||||
|
||||
finally:
|
||||
# Re-enable WiFi (only if we disabled it)
|
||||
if needs_wifi_disable and WifiService:
|
||||
WifiService.temporarily_enable(was_connected)
|
||||
|
||||
@staticmethod
|
||||
def read_battery_voltage(force_refresh=False, raw_adc_value=None):
|
||||
"""
|
||||
Read battery voltage in volts.
|
||||
|
||||
Args:
|
||||
force_refresh: Bypass cache and force fresh reading
|
||||
raw_adc_value: Optional pre-computed raw ADC value (for testing)
|
||||
|
||||
Returns:
|
||||
float: Battery voltage in volts (clamped to 0-MAX_VOLTAGE)
|
||||
"""
|
||||
raw = raw_adc_value if raw_adc_value else BatteryManager.read_raw_adc(force_refresh)
|
||||
voltage = _conversion_func(raw) if _conversion_func else 0.0
|
||||
return voltage
|
||||
|
||||
@staticmethod
|
||||
def get_battery_percentage(raw_adc_value=None):
|
||||
"""
|
||||
Get battery charge percentage.
|
||||
|
||||
Args:
|
||||
raw_adc_value: Optional pre-computed raw ADC value (for testing)
|
||||
|
||||
Returns:
|
||||
float: Battery percentage (0-100)
|
||||
"""
|
||||
voltage = BatteryManager.read_battery_voltage(raw_adc_value=raw_adc_value)
|
||||
percentage = (voltage - MIN_VOLTAGE) * 100.0 / (MAX_VOLTAGE - MIN_VOLTAGE)
|
||||
return max(0, min(100.0, percentage)) # limit to 100.0% and make sure it's positive
|
||||
|
||||
@staticmethod
|
||||
def clear_cache():
|
||||
"""Clear the battery voltage cache to force fresh reading on next call."""
|
||||
global _cached_raw_adc, _last_read_time
|
||||
_cached_raw_adc = None
|
||||
_last_read_time = 0
|
||||
@@ -1,157 +0,0 @@
|
||||
import time
|
||||
|
||||
MIN_VOLTAGE = 3.15
|
||||
MAX_VOLTAGE = 4.15
|
||||
|
||||
adc = None
|
||||
conversion_func = None # Conversion function: ADC value -> voltage
|
||||
adc_pin = None
|
||||
|
||||
# Cache to reduce WiFi interruptions (ADC2 requires WiFi to be disabled)
|
||||
_cached_raw_adc = None
|
||||
_last_read_time = 0
|
||||
CACHE_DURATION_ADC1_MS = 30000 # 30 seconds (cheaper: no WiFi interference)
|
||||
CACHE_DURATION_ADC2_MS = 600000 # 600 seconds (expensive: requires WiFi disable)
|
||||
#CACHE_DURATION_ADC2_MS = CACHE_DURATION_ADC1_MS # trigger frequent disconnections for debugging OSUpdate resume
|
||||
# Or at runtime, do:
|
||||
# import mpos.battery_voltage ; mpos.battery_voltage.CACHE_DURATION_ADC2_MS = 30000
|
||||
|
||||
|
||||
def _is_adc2_pin(pin):
|
||||
"""Check if pin is on ADC2 (ESP32-S3: GPIO11-20)."""
|
||||
return 11 <= pin <= 20
|
||||
|
||||
|
||||
def init_adc(pinnr, adc_to_voltage_func):
|
||||
"""
|
||||
Initialize ADC for battery voltage monitoring.
|
||||
|
||||
IMPORTANT for ESP32-S3: ADC2 (GPIO11-20) doesn't work when WiFi is active!
|
||||
Use ADC1 pins (GPIO1-10) for battery monitoring if possible.
|
||||
If using ADC2, WiFi will be temporarily disabled during readings.
|
||||
|
||||
Args:
|
||||
pinnr: GPIO pin number
|
||||
adc_to_voltage_func: Conversion function that takes raw ADC value (0-4095)
|
||||
and returns battery voltage in volts
|
||||
"""
|
||||
global adc, conversion_func, adc_pin
|
||||
|
||||
conversion_func = adc_to_voltage_func
|
||||
adc_pin = pinnr
|
||||
|
||||
try:
|
||||
print(f"Initializing ADC pin {pinnr} with conversion function")
|
||||
if _is_adc2_pin(pinnr):
|
||||
print(f" WARNING: GPIO{pinnr} is on ADC2 - WiFi will be disabled during readings")
|
||||
from machine import ADC, Pin
|
||||
adc = ADC(Pin(pinnr))
|
||||
adc.atten(ADC.ATTN_11DB) # 0-3.3V range
|
||||
except Exception as e:
|
||||
print(f"Info: this platform has no ADC for measuring battery voltage: {e}")
|
||||
|
||||
initial_adc_value = read_raw_adc()
|
||||
print(f"Reading ADC at init to fill cache: {initial_adc_value} => {read_battery_voltage(raw_adc_value=initial_adc_value)}V => {get_battery_percentage(raw_adc_value=initial_adc_value)}%")
|
||||
|
||||
|
||||
def read_raw_adc(force_refresh=False):
|
||||
"""
|
||||
Read raw ADC value (0-4095) with adaptive caching.
|
||||
|
||||
On ESP32-S3 with ADC2, WiFi is temporarily disabled during reading.
|
||||
Raises RuntimeError if WifiService is busy (connecting/scanning) when using ADC2.
|
||||
|
||||
Args:
|
||||
force_refresh: Bypass cache and force fresh reading
|
||||
|
||||
Returns:
|
||||
float: Raw ADC value (0-4095)
|
||||
|
||||
Raises:
|
||||
RuntimeError: If WifiService is busy (only when using ADC2)
|
||||
"""
|
||||
global _cached_raw_adc, _last_read_time
|
||||
|
||||
# Desktop mode - return random value in typical ADC range
|
||||
if not adc:
|
||||
import random
|
||||
return random.randint(1900, 2600)
|
||||
|
||||
# Check if this is an ADC2 pin (requires WiFi disable)
|
||||
needs_wifi_disable = adc_pin is not None and _is_adc2_pin(adc_pin)
|
||||
|
||||
# Use different cache durations based on cost
|
||||
cache_duration = CACHE_DURATION_ADC2_MS if needs_wifi_disable else CACHE_DURATION_ADC1_MS
|
||||
|
||||
# Check cache
|
||||
current_time = time.ticks_ms()
|
||||
if not force_refresh and _cached_raw_adc is not None:
|
||||
age = time.ticks_diff(current_time, _last_read_time)
|
||||
if age < cache_duration:
|
||||
return _cached_raw_adc
|
||||
|
||||
# Import WifiService only if needed
|
||||
WifiService = None
|
||||
if needs_wifi_disable:
|
||||
try:
|
||||
# Needs actual path, not "from mpos" shorthand because it's mocked by test_battery_voltage.py
|
||||
from mpos.net.wifi_service import WifiService
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Temporarily disable WiFi for ADC2 reading
|
||||
was_connected = False
|
||||
if needs_wifi_disable and WifiService:
|
||||
# This will raise RuntimeError if WiFi is already busy
|
||||
was_connected = WifiService.temporarily_disable()
|
||||
time.sleep(0.05) # Brief delay for WiFi to fully disable
|
||||
|
||||
try:
|
||||
# Read ADC (average of 10 samples)
|
||||
total = sum(adc.read() for _ in range(10))
|
||||
raw_value = total / 10.0
|
||||
|
||||
# Update cache
|
||||
_cached_raw_adc = raw_value
|
||||
_last_read_time = current_time
|
||||
|
||||
return raw_value
|
||||
|
||||
finally:
|
||||
# Re-enable WiFi (only if we disabled it)
|
||||
if needs_wifi_disable and WifiService:
|
||||
WifiService.temporarily_enable(was_connected)
|
||||
|
||||
|
||||
def read_battery_voltage(force_refresh=False, raw_adc_value=None):
|
||||
"""
|
||||
Read battery voltage in volts.
|
||||
|
||||
Args:
|
||||
force_refresh: Bypass cache and force fresh reading
|
||||
|
||||
Returns:
|
||||
float: Battery voltage in volts (clamped to 0-MAX_VOLTAGE)
|
||||
"""
|
||||
raw = raw_adc_value if raw_adc_value else read_raw_adc(force_refresh)
|
||||
voltage = conversion_func(raw) if conversion_func else 0.0
|
||||
return voltage
|
||||
|
||||
|
||||
def get_battery_percentage(raw_adc_value=None):
|
||||
"""
|
||||
Get battery charge percentage.
|
||||
|
||||
Returns:
|
||||
float: Battery percentage (0-100)
|
||||
"""
|
||||
voltage = read_battery_voltage(raw_adc_value=raw_adc_value)
|
||||
percentage = (voltage - MIN_VOLTAGE) * 100.0 / (MAX_VOLTAGE - MIN_VOLTAGE)
|
||||
return max(0,min(100.0, percentage)) # limit to 100.0% and make sure it's positive
|
||||
|
||||
|
||||
def clear_cache():
|
||||
"""Clear the battery voltage cache to force fresh reading on next call."""
|
||||
global _cached_raw_adc, _last_read_time
|
||||
_cached_raw_adc = None
|
||||
_last_read_time = 0
|
||||
@@ -264,8 +264,8 @@ InputManager.register_indev(indev)
|
||||
|
||||
# Battery voltage ADC measuring
|
||||
# NOTE: GPIO13 is on ADC2, which requires WiFi to be disabled during reading on ESP32-S3.
|
||||
# battery_voltage.py handles this automatically: disables WiFi, reads ADC, reconnects WiFi.
|
||||
import mpos.battery_voltage
|
||||
# BatteryManager handles this automatically: disables WiFi, reads ADC, reconnects WiFi.
|
||||
from mpos import BatteryManager
|
||||
"""
|
||||
best fit on battery power:
|
||||
2482 is 4.180
|
||||
@@ -289,7 +289,7 @@ def adc_to_voltage(adc_value):
|
||||
"""
|
||||
return (0.001651* adc_value + 0.08709)
|
||||
|
||||
mpos.battery_voltage.init_adc(13, adc_to_voltage)
|
||||
BatteryManager.init_adc(13, adc_to_voltage)
|
||||
|
||||
import mpos.sdcard
|
||||
mpos.sdcard.init(spi_bus, cs_pin=14)
|
||||
|
||||
@@ -185,7 +185,7 @@ indev.enable(True) # NOQA
|
||||
InputManager.register_indev(indev)
|
||||
|
||||
# Battery voltage ADC measuring: sits on PC0 of CH32X035GxUx
|
||||
import mpos.battery_voltage
|
||||
from mpos import BatteryManager
|
||||
def adc_to_voltage(adc_value):
|
||||
"""
|
||||
Convert raw ADC value to battery voltage using calibrated linear function.
|
||||
@@ -193,7 +193,7 @@ def adc_to_voltage(adc_value):
|
||||
This is ~10x more accurate than simple scaling (error ~0.01V vs ~0.1V).
|
||||
"""
|
||||
return (0.001651* adc_value + 0.08709)
|
||||
#mpos.battery_voltage.init_adc(13, adc_to_voltage) # TODO
|
||||
#BatteryManager.init_adc(13, adc_to_voltage) # TODO
|
||||
|
||||
import mpos.sdcard
|
||||
mpos.sdcard.init(spi_bus, cs_pin=14)
|
||||
|
||||
@@ -89,13 +89,13 @@ except Exception as e:
|
||||
|
||||
|
||||
# Simulated battery voltage ADC measuring
|
||||
import mpos.battery_voltage
|
||||
from mpos import BatteryManager
|
||||
|
||||
def adc_to_voltage(adc_value):
|
||||
"""Convert simulated ADC value to voltage."""
|
||||
return adc_value * (3.3 / 4095) * 2
|
||||
|
||||
mpos.battery_voltage.init_adc(999, adc_to_voltage)
|
||||
BatteryManager.init_adc(999, adc_to_voltage)
|
||||
|
||||
# === AUDIO HARDWARE ===
|
||||
from mpos import AudioFlinger
|
||||
|
||||
@@ -82,7 +82,7 @@ lv.init()
|
||||
mpos.ui.main_display.set_rotation(lv.DISPLAY_ROTATION._90) # must be done after initializing display and creating the touch drivers, to ensure proper handling
|
||||
|
||||
# Battery voltage ADC measuring
|
||||
import mpos.battery_voltage
|
||||
from mpos import BatteryManager
|
||||
|
||||
def adc_to_voltage(adc_value):
|
||||
"""
|
||||
@@ -95,7 +95,7 @@ def adc_to_voltage(adc_value):
|
||||
"""
|
||||
return adc_value * 0.00262
|
||||
|
||||
mpos.battery_voltage.init_adc(5, adc_to_voltage)
|
||||
BatteryManager.init_adc(5, adc_to_voltage)
|
||||
|
||||
# On the Waveshare ESP32-S3-Touch-LCD-2, the camera is hard-wired to power on,
|
||||
# so it needs a software power off to prevent it from staying hot all the time and quickly draining the battery.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import lvgl as lv
|
||||
|
||||
import mpos.time
|
||||
import mpos.battery_voltage
|
||||
from ..battery_manager import BatteryManager
|
||||
from .display_metrics import DisplayMetrics
|
||||
from .appearance_manager import AppearanceManager
|
||||
from .util import (get_foreground_app)
|
||||
@@ -138,9 +138,9 @@ def create_notification_bar():
|
||||
|
||||
def update_battery_icon(timer=None):
|
||||
try:
|
||||
percent = mpos.battery_voltage.get_battery_percentage()
|
||||
percent = BatteryManager.get_battery_percentage()
|
||||
except Exception as e:
|
||||
print(f"battery_voltage.get_battery_percentage got exception, not updating battery_icon: {e}")
|
||||
print(f"BatteryManager.get_battery_percentage got exception, not updating battery_icon: {e}")
|
||||
return
|
||||
if percent > 80:
|
||||
battery_icon.set_text(lv.SYMBOL.BATTERY_FULL)
|
||||
|
||||
+106
-88
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Unit tests for mpos.battery_voltage module.
|
||||
Unit tests for mpos.battery_manager.BatteryManager class.
|
||||
|
||||
Tests ADC1/ADC2 detection, caching, WiFi coordination, and voltage calculations.
|
||||
"""
|
||||
@@ -10,7 +10,7 @@ import sys
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, '../internal_filesystem')
|
||||
|
||||
# Mock modules before importing battery_voltage
|
||||
# Mock modules before importing BatteryManager
|
||||
class MockADC:
|
||||
"""Mock ADC for testing."""
|
||||
ATTN_11DB = 3
|
||||
@@ -88,8 +88,8 @@ class MockWifiService:
|
||||
sys.modules['machine'] = MockMachine
|
||||
sys.modules['mpos.net.wifi_service'] = type('module', (), {'WifiService': MockWifiService})()
|
||||
|
||||
# Now import battery_voltage
|
||||
import mpos.battery_voltage as bv
|
||||
# Now import BatteryManager
|
||||
from mpos.battery_manager import BatteryManager
|
||||
|
||||
|
||||
class TestADC2Detection(unittest.TestCase):
|
||||
@@ -97,20 +97,23 @@ class TestADC2Detection(unittest.TestCase):
|
||||
|
||||
def test_adc1_pins_detected(self):
|
||||
"""Test that ADC1 pins (GPIO1-10) are detected correctly."""
|
||||
from mpos.battery_manager import _is_adc2_pin
|
||||
for pin in range(1, 11):
|
||||
self.assertFalse(bv._is_adc2_pin(pin), f"GPIO{pin} should be ADC1")
|
||||
self.assertFalse(_is_adc2_pin(pin), f"GPIO{pin} should be ADC1")
|
||||
|
||||
def test_adc2_pins_detected(self):
|
||||
"""Test that ADC2 pins (GPIO11-20) are detected correctly."""
|
||||
from mpos.battery_manager import _is_adc2_pin
|
||||
for pin in range(11, 21):
|
||||
self.assertTrue(bv._is_adc2_pin(pin), f"GPIO{pin} should be ADC2")
|
||||
self.assertTrue(_is_adc2_pin(pin), f"GPIO{pin} should be ADC2")
|
||||
|
||||
def test_out_of_range_pins(self):
|
||||
"""Test pins outside ADC range."""
|
||||
self.assertFalse(bv._is_adc2_pin(0))
|
||||
self.assertFalse(bv._is_adc2_pin(21))
|
||||
self.assertFalse(bv._is_adc2_pin(30))
|
||||
self.assertFalse(bv._is_adc2_pin(100))
|
||||
from mpos.battery_manager import _is_adc2_pin
|
||||
self.assertFalse(_is_adc2_pin(0))
|
||||
self.assertFalse(_is_adc2_pin(21))
|
||||
self.assertFalse(_is_adc2_pin(30))
|
||||
self.assertFalse(_is_adc2_pin(100))
|
||||
|
||||
|
||||
class TestInitADC(unittest.TestCase):
|
||||
@@ -118,40 +121,44 @@ class TestInitADC(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Reset module state."""
|
||||
bv.adc = None
|
||||
bv.conversion_func = None
|
||||
bv.adc_pin = None
|
||||
import mpos.battery_manager as bm
|
||||
bm._adc = None
|
||||
bm._conversion_func = None
|
||||
bm._adc_pin = None
|
||||
|
||||
def test_init_adc1_pin(self):
|
||||
"""Test initializing with ADC1 pin."""
|
||||
def adc_to_voltage(adc_value):
|
||||
return adc_value * 0.00161
|
||||
|
||||
bv.init_adc(5, adc_to_voltage)
|
||||
BatteryManager.init_adc(5, adc_to_voltage)
|
||||
|
||||
self.assertIsNotNone(bv.adc)
|
||||
self.assertEqual(bv.conversion_func, adc_to_voltage)
|
||||
self.assertEqual(bv.adc_pin, 5)
|
||||
self.assertEqual(bv.adc._atten, MockADC.ATTN_11DB)
|
||||
import mpos.battery_manager as bm
|
||||
self.assertIsNotNone(bm._adc)
|
||||
self.assertEqual(bm._conversion_func, adc_to_voltage)
|
||||
self.assertEqual(bm._adc_pin, 5)
|
||||
self.assertEqual(bm._adc._atten, MockADC.ATTN_11DB)
|
||||
|
||||
def test_init_adc2_pin(self):
|
||||
"""Test initializing with ADC2 pin (should warn but work)."""
|
||||
def adc_to_voltage(adc_value):
|
||||
return adc_value * 0.00197
|
||||
|
||||
bv.init_adc(13, adc_to_voltage)
|
||||
BatteryManager.init_adc(13, adc_to_voltage)
|
||||
|
||||
self.assertIsNotNone(bv.adc)
|
||||
self.assertIsNotNone(bv.conversion_func)
|
||||
self.assertEqual(bv.adc_pin, 13)
|
||||
import mpos.battery_manager as bm
|
||||
self.assertIsNotNone(bm._adc)
|
||||
self.assertIsNotNone(bm._conversion_func)
|
||||
self.assertEqual(bm._adc_pin, 13)
|
||||
|
||||
def test_conversion_func_stored(self):
|
||||
"""Test that conversion function is stored correctly."""
|
||||
def my_conversion(adc_value):
|
||||
return adc_value * 0.12345
|
||||
|
||||
bv.init_adc(5, my_conversion)
|
||||
self.assertEqual(bv.conversion_func, my_conversion)
|
||||
BatteryManager.init_adc(5, my_conversion)
|
||||
import mpos.battery_manager as bm
|
||||
self.assertEqual(bm._conversion_func, my_conversion)
|
||||
|
||||
|
||||
class TestCaching(unittest.TestCase):
|
||||
@@ -159,53 +166,57 @@ class TestCaching(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Reset module state."""
|
||||
bv.clear_cache()
|
||||
BatteryManager.clear_cache()
|
||||
def adc_to_voltage(adc_value):
|
||||
return adc_value * 0.00161
|
||||
bv.init_adc(5, adc_to_voltage) # Use ADC1 to avoid WiFi complexity
|
||||
BatteryManager.init_adc(5, adc_to_voltage) # Use ADC1 to avoid WiFi complexity
|
||||
MockWifiService.reset()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up."""
|
||||
bv.clear_cache()
|
||||
BatteryManager.clear_cache()
|
||||
|
||||
def test_cache_hit_on_first_read(self):
|
||||
"""Test that first read already has a cache (because of read during init) """
|
||||
self.assertIsNotNone(bv._cached_raw_adc)
|
||||
raw = bv.read_raw_adc()
|
||||
self.assertIsNotNone(bv._cached_raw_adc)
|
||||
self.assertEqual(raw, bv._cached_raw_adc)
|
||||
import mpos.battery_manager as bm
|
||||
self.assertIsNotNone(bm._cached_raw_adc)
|
||||
raw = BatteryManager.read_raw_adc()
|
||||
self.assertIsNotNone(bm._cached_raw_adc)
|
||||
self.assertEqual(raw, bm._cached_raw_adc)
|
||||
|
||||
def test_cache_hit_within_duration(self):
|
||||
"""Test that subsequent reads use cache within duration."""
|
||||
raw1 = bv.read_raw_adc()
|
||||
raw1 = BatteryManager.read_raw_adc()
|
||||
|
||||
# Change ADC value but should still get cached value
|
||||
bv.adc.set_read_value(3000)
|
||||
raw2 = bv.read_raw_adc()
|
||||
import mpos.battery_manager as bm
|
||||
bm._adc.set_read_value(3000)
|
||||
raw2 = BatteryManager.read_raw_adc()
|
||||
|
||||
self.assertEqual(raw1, raw2, "Should return cached value")
|
||||
|
||||
def test_force_refresh_bypasses_cache(self):
|
||||
"""Test that force_refresh bypasses cache."""
|
||||
bv.adc.set_read_value(2000)
|
||||
raw1 = bv.read_raw_adc()
|
||||
import mpos.battery_manager as bm
|
||||
bm._adc.set_read_value(2000)
|
||||
raw1 = BatteryManager.read_raw_adc()
|
||||
|
||||
# Change value and force refresh
|
||||
bv.adc.set_read_value(3000)
|
||||
raw2 = bv.read_raw_adc(force_refresh=True)
|
||||
bm._adc.set_read_value(3000)
|
||||
raw2 = BatteryManager.read_raw_adc(force_refresh=True)
|
||||
|
||||
self.assertNotEqual(raw1, raw2, "force_refresh should bypass cache")
|
||||
self.assertEqual(raw2, 3000.0)
|
||||
|
||||
def test_clear_cache_works(self):
|
||||
"""Test that clear_cache() clears the cache."""
|
||||
bv.read_raw_adc()
|
||||
self.assertIsNotNone(bv._cached_raw_adc)
|
||||
BatteryManager.read_raw_adc()
|
||||
import mpos.battery_manager as bm
|
||||
self.assertIsNotNone(bm._cached_raw_adc)
|
||||
|
||||
bv.clear_cache()
|
||||
self.assertIsNone(bv._cached_raw_adc)
|
||||
self.assertEqual(bv._last_read_time, 0)
|
||||
BatteryManager.clear_cache()
|
||||
self.assertIsNone(bm._cached_raw_adc)
|
||||
self.assertEqual(bm._last_read_time, 0)
|
||||
|
||||
|
||||
class TestADC1Reading(unittest.TestCase):
|
||||
@@ -213,23 +224,23 @@ class TestADC1Reading(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Reset module state."""
|
||||
bv.clear_cache()
|
||||
BatteryManager.clear_cache()
|
||||
def adc_to_voltage(adc_value):
|
||||
return adc_value * 0.00161
|
||||
bv.init_adc(5, adc_to_voltage) # GPIO5 is ADC1
|
||||
BatteryManager.init_adc(5, adc_to_voltage) # GPIO5 is ADC1
|
||||
MockWifiService.reset()
|
||||
MockWifiService._connected = True
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up."""
|
||||
bv.clear_cache()
|
||||
BatteryManager.clear_cache()
|
||||
MockWifiService.reset()
|
||||
|
||||
def test_adc1_doesnt_disable_wifi(self):
|
||||
"""Test that ADC1 reading doesn't disable WiFi."""
|
||||
MockWifiService._connected = True
|
||||
|
||||
bv.read_raw_adc(force_refresh=True)
|
||||
BatteryManager.read_raw_adc(force_refresh=True)
|
||||
|
||||
# WiFi should still be connected
|
||||
self.assertTrue(MockWifiService.is_connected())
|
||||
@@ -241,7 +252,7 @@ class TestADC1Reading(unittest.TestCase):
|
||||
|
||||
# Should not raise error
|
||||
try:
|
||||
raw = bv.read_raw_adc(force_refresh=True)
|
||||
raw = BatteryManager.read_raw_adc(force_refresh=True)
|
||||
self.assertIsNotNone(raw)
|
||||
except RuntimeError:
|
||||
self.fail("ADC1 should not raise error when WiFi is busy")
|
||||
@@ -252,22 +263,22 @@ class TestADC2Reading(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Reset module state."""
|
||||
bv.clear_cache()
|
||||
BatteryManager.clear_cache()
|
||||
def adc_to_voltage(adc_value):
|
||||
return adc_value * 0.00197
|
||||
bv.init_adc(13, adc_to_voltage) # GPIO13 is ADC2
|
||||
BatteryManager.init_adc(13, adc_to_voltage) # GPIO13 is ADC2
|
||||
MockWifiService.reset()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up."""
|
||||
bv.clear_cache()
|
||||
BatteryManager.clear_cache()
|
||||
MockWifiService.reset()
|
||||
|
||||
def test_adc2_disables_wifi_when_connected(self):
|
||||
"""Test that ADC2 reading disables WiFi when connected."""
|
||||
MockWifiService._connected = True
|
||||
|
||||
bv.read_raw_adc(force_refresh=True)
|
||||
BatteryManager.read_raw_adc(force_refresh=True)
|
||||
|
||||
# WiFi should be reconnected after reading (if it was connected before)
|
||||
self.assertTrue(MockWifiService.is_connected())
|
||||
@@ -279,7 +290,7 @@ class TestADC2Reading(unittest.TestCase):
|
||||
# wifi_busy should be False before
|
||||
self.assertFalse(MockWifiService.wifi_busy)
|
||||
|
||||
bv.read_raw_adc(force_refresh=True)
|
||||
BatteryManager.read_raw_adc(force_refresh=True)
|
||||
|
||||
# wifi_busy should be False after (cleared in finally)
|
||||
self.assertFalse(MockWifiService.wifi_busy)
|
||||
@@ -289,7 +300,7 @@ class TestADC2Reading(unittest.TestCase):
|
||||
MockWifiService.wifi_busy = True
|
||||
|
||||
with self.assertRaises(RuntimeError) as ctx:
|
||||
bv.read_raw_adc(force_refresh=True)
|
||||
BatteryManager.read_raw_adc(force_refresh=True)
|
||||
|
||||
self.assertIn("WifiService is already busy", str(ctx.exception))
|
||||
|
||||
@@ -297,13 +308,13 @@ class TestADC2Reading(unittest.TestCase):
|
||||
"""Test that ADC2 uses cache even when WiFi is busy."""
|
||||
# First read to populate cache
|
||||
MockWifiService.wifi_busy = False
|
||||
raw1 = bv.read_raw_adc(force_refresh=True)
|
||||
raw1 = BatteryManager.read_raw_adc(force_refresh=True)
|
||||
|
||||
# Now set WiFi busy
|
||||
MockWifiService.wifi_busy = True
|
||||
|
||||
# Should return cached value without error
|
||||
raw2 = bv.read_raw_adc()
|
||||
raw2 = BatteryManager.read_raw_adc()
|
||||
self.assertEqual(raw1, raw2)
|
||||
|
||||
def test_adc2_only_reconnects_if_was_connected(self):
|
||||
@@ -311,7 +322,7 @@ class TestADC2Reading(unittest.TestCase):
|
||||
# WiFi is NOT connected
|
||||
MockWifiService._connected = False
|
||||
|
||||
bv.read_raw_adc(force_refresh=True)
|
||||
BatteryManager.read_raw_adc(force_refresh=True)
|
||||
|
||||
# WiFi should still be disconnected (no unwanted reconnection)
|
||||
self.assertFalse(MockWifiService.is_connected())
|
||||
@@ -322,58 +333,63 @@ class TestVoltageCalculations(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Reset module state."""
|
||||
bv.clear_cache()
|
||||
BatteryManager.clear_cache()
|
||||
def adc_to_voltage(adc_value):
|
||||
return adc_value * 0.00161
|
||||
bv.init_adc(5, adc_to_voltage) # ADC1 pin, scale factor for 2:1 divider
|
||||
bv.adc.set_read_value(2048) # Mid-range
|
||||
BatteryManager.init_adc(5, adc_to_voltage) # ADC1 pin, scale factor for 2:1 divider
|
||||
import mpos.battery_manager as bm
|
||||
bm._adc.set_read_value(2048) # Mid-range
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up."""
|
||||
bv.clear_cache()
|
||||
BatteryManager.clear_cache()
|
||||
|
||||
def test_read_battery_voltage_applies_scale_factor(self):
|
||||
"""Test that voltage is calculated correctly."""
|
||||
bv.adc.set_read_value(2048) # Mid-range
|
||||
bv.clear_cache()
|
||||
import mpos.battery_manager as bm
|
||||
bm._adc.set_read_value(2048) # Mid-range
|
||||
BatteryManager.clear_cache()
|
||||
|
||||
voltage = bv.read_battery_voltage(force_refresh=True)
|
||||
voltage = BatteryManager.read_battery_voltage(force_refresh=True)
|
||||
expected = 2048 * 0.00161
|
||||
self.assertAlmostEqual(voltage, expected, places=4)
|
||||
|
||||
def test_voltage_clamped_to_zero(self):
|
||||
"""Test that negative voltage is clamped to 0."""
|
||||
bv.adc.set_read_value(0)
|
||||
bv.clear_cache()
|
||||
import mpos.battery_manager as bm
|
||||
bm._adc.set_read_value(0)
|
||||
BatteryManager.clear_cache()
|
||||
|
||||
voltage = bv.read_battery_voltage(force_refresh=True)
|
||||
voltage = BatteryManager.read_battery_voltage(force_refresh=True)
|
||||
self.assertGreaterEqual(voltage, 0.0)
|
||||
|
||||
def test_get_battery_percentage_calculation(self):
|
||||
"""Test percentage calculation."""
|
||||
# Set voltage to mid-range between MIN and MAX
|
||||
mid_voltage = (bv.MIN_VOLTAGE + bv.MAX_VOLTAGE) / 2
|
||||
import mpos.battery_manager as bm
|
||||
mid_voltage = (bm.MIN_VOLTAGE + bm.MAX_VOLTAGE) / 2
|
||||
# Inverse of conversion function: if voltage = adc * 0.00161, then adc = voltage / 0.00161
|
||||
raw_adc = mid_voltage / 0.00161
|
||||
bv.adc.set_read_value(int(raw_adc))
|
||||
bv.clear_cache()
|
||||
bm._adc.set_read_value(int(raw_adc))
|
||||
BatteryManager.clear_cache()
|
||||
|
||||
percentage = bv.get_battery_percentage()
|
||||
percentage = BatteryManager.get_battery_percentage()
|
||||
self.assertAlmostEqual(percentage, 50.0, places=0)
|
||||
|
||||
def test_percentage_clamped_to_0_100(self):
|
||||
"""Test that percentage is clamped to 0-100 range."""
|
||||
import mpos.battery_manager as bm
|
||||
# Test minimum
|
||||
bv.adc.set_read_value(0)
|
||||
bv.clear_cache()
|
||||
percentage = bv.get_battery_percentage()
|
||||
bm._adc.set_read_value(0)
|
||||
BatteryManager.clear_cache()
|
||||
percentage = BatteryManager.get_battery_percentage()
|
||||
self.assertGreaterEqual(percentage, 0.0)
|
||||
self.assertLessEqual(percentage, 100.0)
|
||||
|
||||
# Test maximum
|
||||
bv.adc.set_read_value(4095)
|
||||
bv.clear_cache()
|
||||
percentage = bv.get_battery_percentage()
|
||||
bm._adc.set_read_value(4095)
|
||||
BatteryManager.clear_cache()
|
||||
percentage = BatteryManager.get_battery_percentage()
|
||||
self.assertGreaterEqual(percentage, 0.0)
|
||||
self.assertLessEqual(percentage, 100.0)
|
||||
|
||||
@@ -383,21 +399,22 @@ class TestAveragingLogic(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Reset module state."""
|
||||
bv.clear_cache()
|
||||
BatteryManager.clear_cache()
|
||||
def adc_to_voltage(adc_value):
|
||||
return adc_value * 0.00161
|
||||
bv.init_adc(5, adc_to_voltage)
|
||||
BatteryManager.init_adc(5, adc_to_voltage)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up."""
|
||||
bv.clear_cache()
|
||||
BatteryManager.clear_cache()
|
||||
|
||||
def test_adc_read_averages_10_samples(self):
|
||||
"""Test that 10 samples are averaged."""
|
||||
bv.adc.set_read_value(2000)
|
||||
bv.clear_cache()
|
||||
import mpos.battery_manager as bm
|
||||
bm._adc.set_read_value(2000)
|
||||
BatteryManager.clear_cache()
|
||||
|
||||
raw = bv.read_raw_adc(force_refresh=True)
|
||||
raw = BatteryManager.read_raw_adc(force_refresh=True)
|
||||
|
||||
# Should be average of 10 reads
|
||||
self.assertEqual(raw, 2000.0)
|
||||
@@ -408,27 +425,28 @@ class TestDesktopMode(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Disable ADC."""
|
||||
bv.adc = None
|
||||
import mpos.battery_manager as bm
|
||||
bm._adc = None
|
||||
def adc_to_voltage(adc_value):
|
||||
return adc_value * 0.00161
|
||||
bv.conversion_func = adc_to_voltage
|
||||
bm._conversion_func = adc_to_voltage
|
||||
|
||||
def test_read_raw_adc_returns_random_value(self):
|
||||
"""Test that desktop mode returns random ADC value."""
|
||||
raw = bv.read_raw_adc()
|
||||
raw = BatteryManager.read_raw_adc()
|
||||
self.assertIsNotNone(raw)
|
||||
self.assertTrue(raw > 0, f"Expected raw > 0, got {raw}")
|
||||
self.assertTrue(raw < 4096, f"Expected raw < 4096, got {raw}")
|
||||
|
||||
def test_read_battery_voltage_works_without_adc(self):
|
||||
"""Test that voltage reading works in desktop mode."""
|
||||
voltage = bv.read_battery_voltage()
|
||||
voltage = BatteryManager.read_battery_voltage()
|
||||
self.assertIsNotNone(voltage)
|
||||
self.assertTrue(voltage > 0, f"Expected voltage > 0, got {voltage}")
|
||||
|
||||
def test_get_battery_percentage_works_without_adc(self):
|
||||
"""Test that percentage reading works in desktop mode."""
|
||||
percentage = bv.get_battery_percentage()
|
||||
percentage = BatteryManager.get_battery_percentage()
|
||||
self.assertIsNotNone(percentage)
|
||||
self.assertGreaterEqual(percentage, 0)
|
||||
self.assertLessEqual(percentage, 100)
|
||||
|
||||
Reference in New Issue
Block a user