Skip to content

Commit d7e49d0

Browse files
Add new BatteryManager framework
1 parent 6837d56 commit d7e49d0

File tree

11 files changed

+304
-264
lines changed

11 files changed

+304
-264
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
- ActivityNavigator: support pre-instantiated activities to support one activity closing a child activity
99
- Rename PackageManager to AppManager framework
1010
- Add new AppearanceManager framework
11+
- Add new BatteryManager framework
1112
- Add new DeviceInfo framework
1213
- Add new DisplayMetrics framework
14+
- Add new InputManager framework
1315
- Add new VersionInfo framework
1416
- Additional board support: Fri3d Camp 2026 (untested on real hardware)
1517
- Harmonize frameworks to use same coding patterns

internal_filesystem/apps/com.micropythonos.showbattery/assets/hello.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
import lvgl as lv
5050
import time
5151

52-
from mpos import battery_voltage, Activity
52+
from mpos import BatteryManager, Activity
5353

5454
class Hello(Activity):
5555

@@ -70,9 +70,9 @@ def onResume(self, screen):
7070

7171
def update_bat(timer):
7272
#global l
73-
r = battery_voltage.read_raw_adc()
74-
v = battery_voltage.read_battery_voltage()
75-
percent = battery_voltage.get_battery_percentage()
73+
r = BatteryManager.read_raw_adc()
74+
v = BatteryManager.read_battery_voltage()
75+
percent = BatteryManager.get_battery_percentage()
7676
text = f"{time.localtime()}\n{r}\n{v}V\n{percent}%"
7777
#text = f"{time.localtime()}: {r}"
7878
print(text)

internal_filesystem/lib/mpos/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
from .device_info import DeviceInfo
1818
from .build_info import BuildInfo
1919

20+
# Battery manager (imported early for UI dependencies)
21+
from .battery_manager import BatteryManager
22+
2023
# Common activities
2124
from .app.activities.chooser import ChooserActivity
2225
from .app.activities.view import ViewActivity
@@ -56,7 +59,6 @@
5659
from . import sensor_manager
5760
from . import camera_manager
5861
from . import sdcard
59-
from . import battery_voltage
6062
from . import audio
6163
from . import hardware
6264

@@ -66,7 +68,7 @@
6668
"Activity",
6769
"SharedPreferences",
6870
"ConnectivityManager", "DownloadManager", "WifiService", "AudioFlinger", "Intent",
69-
"ActivityNavigator", "AppManager", "TaskManager", "CameraManager",
71+
"ActivityNavigator", "AppManager", "TaskManager", "CameraManager", "BatteryManager",
7072
# Device and build info
7173
"DeviceInfo", "BuildInfo",
7274
# Common activities
@@ -93,7 +95,7 @@
9395
"get_all_widgets_with_text",
9496
# Submodules
9597
"ui", "config", "net", "content", "time", "sensor_manager",
96-
"camera_manager", "sdcard", "battery_voltage", "audio", "hardware", "bootloader",
98+
"camera_manager", "sdcard", "audio", "hardware", "bootloader",
9799
# Timezone utilities
98100
"TimeZone"
99101
]
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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

Comments
 (0)