-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
battery_monitor.py
223 lines (170 loc) · 6.35 KB
/
battery_monitor.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
"""
Abstraction of various I2C battery "fuel gauges."
"""
import board
import digitalio
import supervisor
from adafruit_lc709203f import LC709203F, LC709203F_CMD_APA
from adafruit_max1704x import MAX17048
from busio import I2C
from util import I2CDeviceAutoSelector
# noinspection PyBroadException
try:
from abc import abstractmethod, ABC
except:
class ABC:
"""
Placeholder for CircuitPython
"""
pass
# noinspection PyUnusedLocal
def abstractmethod(*args, **kwargs):
"""
Placeholder for CircuitPython
:param args: Ignored
:param kwargs: Ignored
"""
pass
class BatteryMonitor(ABC):
"""
Abstraction for a battery monitor, a.k.a. "fuel gauge", to measure the charge level of the attached battery.
Supports both LC709203F and MAX17048 battery monitors, although the latter is more common. A battery size of
2500 mAh is assumed.
The battery monitor hardware is connected by I2C, but that could mean built directly into the board (i.e. Feather)
or connected externally via STEMMA QT/QWIIC/I2C pins.
This is an abstract class. Get instances using get_instance().
"""
def __init__(self, i2c: I2C):
"""
Use get_instance() instead of this constructor to automatically construct a battery monitor of the correct
implementation and to check if one exists on the I2C bus in the first place.
:param i2c: I2C bus that has a battery monitor
"""
self.last_percent = None
self.i2c = i2c
self.device = None
if hasattr(board, "VBUS"):
self.charging_pin = digitalio.DigitalInOut(board.VBUS)
self.charging_pin.direction = digitalio.Direction.INPUT
else:
self.charging_pin = None
def init_device(self) -> None:
"""
Initializes the underlying hardware device used by this instance, if not already initialized. Subclasses must
implement init_raw_device() to define the device.
"""
if self.device is None:
self.device = self.init_raw_device()
@abstractmethod
def init_raw_device(self):
"""
Initializes the underlying hardware device used by this instance. In this abstract class, this method raises a
NotImplementedError.
:return: Underlying hardware device used by this instance
"""
raise NotImplementedError()
def is_charging(self) -> bool:
"""
Checks if the battery is charging. The default implementation assumes the battery is charging if any of the
following conditions are true:
* USB data is connected, like a serial terminal or USB mass storage
* The charging_pin attribute is not None and its value is True
Otherwise returns False because the charging state is either not charging or not known.
Subclasses may override this method based on how their underlying hardware checks if the battery is charging,
but should call this base method first and return True if it does too.
:return: True if the battery is charging, False if not or indeterminate
"""
if supervisor.runtime.usb_connected:
return True
if self.charging_pin is not None:
return self.charging_pin.value
return False
@abstractmethod
def get_current_percent(self) -> float:
"""
Gets the charge percent of the battery (0..100). In this abstract class, raises NotImplementedError() and must
be overridden by subclasses.
:return: Charge percent of the battery
"""
raise NotImplementedError()
def get_percent(self) -> int:
"""
Initializes the battery monitor hardware if necessary, gets the current charge percent, and normalizes the
response to be from 0% to 100%. Returns charge percent or None if it isn't known yet; for example, the hardware
hasn't finished initializing yet.
:return: Battery charge percent (0...100) or None if unknown or the charge is 0% and therefore implausible
"""
self.init_device()
self.last_percent = self.get_current_percent()
if self.last_percent is None:
print("Couldn't get battery percent; it might not be stabilized yet")
else:
self.last_percent = int(round(min(max(self.last_percent, 0), 100)))
if self.last_percent <= 0:
self.last_percent = None
return self.last_percent
@staticmethod
def get_instance(i2c: I2C):
"""
Gets a concrete instance of a battery monitor by scanning the I2C bus for a compatible battery monitor, or None
if one isn't found. Multiple attempts are made with a brief delay between each attempt in case the hardware
isn't immediately available on the I2C bus, but eventually gives up after repeated failures.
:param i2c: I2C bus that contains a battery monitor
:return: Concrete instance of a battery monitor or None if not found
"""
try:
return I2CDeviceAutoSelector(i2c = i2c).get_device(address_map = {
0x0b: lambda _: LC709203FBatteryMonitor(i2c),
0x36: lambda _: MAX17048BatteryMonitor(i2c)
})
except Exception as e:
print(f"Failed to get battery monitor: {e}")
return None
class MAX17048BatteryMonitor(BatteryMonitor):
"""
An implementation of BatteryMonitor based on a MAX17048. Most new Adafruit Feathers seem to use this.
"""
def init_raw_device(self) -> MAX17048:
"""
Creates a MAX17048 instance.
:return: MAX17048 instance
"""
return MAX17048(self.i2c)
def get_current_percent(self) -> float:
"""
Queries the MAX17048 for its cell percentage.
:return: Battery charge percent
"""
return self.device.cell_percent
def is_charging(self) -> bool:
"""
Returns True if the base class does, and if not, returns True if the charge rate exceeds 5%/hr or False if not.
:return: True if the battery is likely charging, False if not or indeterminate
"""
self.init_device()
is_charging = super().is_charging()
if not is_charging:
charge_rate = self.device.charge_rate
is_charging = charge_rate > 0.05
return is_charging
class LC709203FBatteryMonitor(BatteryMonitor):
"""
An implementation of BatteryMonitor based on a LC709203F. Older Adafruit Feathers seem to use this.
"""
# pack size adjustment values: https://www.mouser.com/datasheet/2/308/LC709203F_D-1810548.pdf
BATTERY_LC709203F_AMA = 0x33
def init_raw_device(self) -> LC709203F:
"""
Creates a LC709203F instance and configures it for a 2500 mAh battery.
:return: LC709203F instance
"""
device = LC709203F(self.i2c)
# noinspection PyProtectedMember
device._write_word(LC709203F_CMD_APA, LC709203FBatteryMonitor.BATTERY_LC709203F_AMA)
return device
def get_current_percent(self) -> float:
"""
Queries the LC709203F for its cell percentage.
:return: Battery charge percent
"""
return self.device.cell_percent