Skip to content

Commit cee2ada

Browse files
committed
instrument/battery_monitor: Add initial draft for battery monitor
Attempt to estimate energy usage from the builtin voltage and current reported values. Note that the device must not be charging while in use.
1 parent 7921018 commit cee2ada

File tree

1 file changed

+202
-0
lines changed

1 file changed

+202
-0
lines changed

devlib/instrument/battery_monitor.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# Copyright 2021 ARM Limited
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
from __future__ import division
16+
17+
import logging
18+
import re
19+
import sys
20+
import tempfile
21+
import threading
22+
import time
23+
24+
from devlib.instrument import Instrument, MeasurementsCsv, CONTINUOUS
25+
from devlib.exception import TargetStableError, TargetNotRespondingError, WorkerThreadError
26+
from devlib.utils.csvutil import csvreader, csvwriter
27+
28+
29+
logger = logging.getLogger('BattryMonitor')
30+
31+
32+
BATTERY_STATS_REGEX = re.compile(r'level: (?P<level>\d+).*scale: (?P<scale>\d+).*voltage: (?P<voltage>\d+)', flags=re.DOTALL)
33+
34+
35+
# List of SysFS node for current taken from Ampere Application
36+
# https://forum.xda-developers.com/t/app-4-0-3-ampere-current-meter.3040329/post-59086006
37+
CURRENT_SYSFS_NODES = [
38+
'/sys/class/power_supply/ab8500_fg/current_now',
39+
'/sys/class/power_supply/android-battery/current_now',
40+
'/sys/class/power_supply/battery/batt_attr_text',
41+
'/sys/class/power_supply/battery/batt_chg_current',
42+
'/sys/class/power_supply/battery/batt_current',
43+
'/sys/class/power_supply/battery/batt_current_adc',
44+
'/sys/class/power_supply/battery/batt_current_now',
45+
'/sys/class/power_supply/battery/BatteryAverageCurrent',
46+
'/sys/class/power_supply/battery/charger_current',
47+
'/sys/class/power_supply/battery/current_avg',
48+
'/sys/class/power_supply/battery/current_max',
49+
'/sys/class/power_supply/battery/current_now',
50+
'/sys/class/power_supply/Battery/current_now',
51+
'/sys/class/power_supply/battery/smem_text',
52+
'/sys/class/power_supply/bq27520/current_now',
53+
'/sys/class/power_supply/da9052-bat/current_avg',
54+
'/sys/class/power_supply/ds2784-fuelgauge/current_now',
55+
'/sys/class/power_supply/max17042-0/current_now',
56+
'/sys/class/power_supply/max170xx_battery/current_now',
57+
'/sys/devices/platform/battery/power_supply/battery/BatteryAverageCurrent',
58+
'/sys/devices/platform/cpcap_battery/power_supply/usb/current_now',
59+
'/sys/devices/platform/ds2784-battery/getcurrent',
60+
'/sys/devices/platform/i2c-adapter/i2c-0/0-0036/power_supply/battery/current_now',
61+
'/sys/devices/platform/i2c-adapter/i2c-0/0-0036/power_supply/ds2746-battery/current_now',
62+
'/sys/devices/platform/msm-charger/power_supply/battery_gauge/current_now',
63+
'/sys/devices/platform/mt6320-battery/power_supply/battery/BatteryAverageCurrent',
64+
'/sys/devices/platform/mt6329-battery/FG_Battery_CurrentConsumption',
65+
'/sys/EcControl/BatCurrent',
66+
]
67+
68+
69+
class BatteryMonitorInstrument(Instrument):
70+
71+
name = 'battey_monitor'
72+
mode = CONTINUOUS
73+
74+
def __init__(self, target, period=2, current_scale=1e6,
75+
voltage_scale=1e3, current_node=None,):
76+
77+
if not target.is_rooted:
78+
self.logger.warn('Target is not rooted, current readings are likely to fail.')
79+
super(BatteryMonitorInstrument, self).__init__(target)
80+
81+
self.period = period
82+
self.target = target
83+
84+
self.logger.debug('Discovering available current sysfs node..')
85+
self.current_node, inverse = self._discover_current_node(current_node)
86+
87+
# sensor kind --> unit conversion
88+
self.value_convert = {
89+
'voltage': lambda x: int(x) / voltage_scale,
90+
'current': lambda x: -int(x) if inverse else int(x) / current_scale,
91+
'power': lambda x: -int(x) / (voltage_scale * current_scale),
92+
'percent': lambda x: float(x),
93+
'time': lambda x: x,
94+
}
95+
96+
self.add_channel('battery', 'voltage')
97+
self.add_channel('battery', 'current')
98+
self.add_channel('battery', 'power')
99+
self.add_channel('battery', 'percent')
100+
self.add_channel('timestamp', 'time')
101+
102+
def reset(self, sites=None, kinds=None, channels=None):
103+
super(BatteryMonitorInstrument, self).reset(sites, kinds, channels)
104+
self.raw_data_file = tempfile.mkstemp('.csv')[1]
105+
self.collector = BatteryStatsCollector(self.target,
106+
self.period,
107+
self.current_node,
108+
self.raw_data_file)
109+
110+
def start(self):
111+
if not self.collector:
112+
raise RuntimeError('Must call "reset" before "start"')
113+
self.collector.start()
114+
115+
def stop(self):
116+
self.collector.stop()
117+
118+
def _discover_current_node(self, current_node):
119+
paths = [current_node] if current_node else CURRENT_SYSFS_NODES
120+
for path in paths:
121+
try:
122+
reading = self.target.read_int(path)
123+
except TargetStableError:
124+
continue
125+
if reading:
126+
self.logger.debug('Found current sysfs node at: {}'.format(path))
127+
# Return if the value reported is negative or positive, assuming
128+
# device is currently discharging
129+
return path, reading < 0
130+
131+
raise RuntimeError('Failed to detect valid reading from known current nodes.')
132+
133+
134+
def get_data(self, outfile):
135+
all_channels = self.list_channels()
136+
channels_labels = [c.label for c in all_channels]
137+
active_channels = [c.label for c in self.active_channels]
138+
active_indexes = [channels_labels.index(ac) for ac in active_channels]
139+
140+
with csvreader(self.raw_data_file, skipinitialspace=True) as reader:
141+
with csvwriter(outfile) as writer:
142+
writer.writerow(active_channels)
143+
for row in reader:
144+
output_row = [self.value_convert[all_channels[i].kind](row[i]) for i in active_indexes]
145+
writer.writerow(output_row)
146+
147+
return MeasurementsCsv(outfile, self.active_channels, 1/self.period)
148+
149+
150+
class BatteryStatsCollector(threading.Thread):
151+
152+
def __init__(self, target, period, current_node, raw_data_file):
153+
super(BatteryStatsCollector, self).__init__()
154+
self.target = target
155+
self.period = period
156+
self.current_node = current_node
157+
self.raw_data_file = raw_data_file
158+
self.stop_signal = threading.Event()
159+
self.measurements = []
160+
self.exc = None
161+
162+
def run(self):
163+
logger.debug('Battery stats collection started.')
164+
try:
165+
self.stop_signal.clear()
166+
logger.debug('Using temp file: {}'.format(self.raw_data_file))
167+
wfh = open(self.raw_data_file, 'wb')
168+
try:
169+
while not self.stop_signal.is_set():
170+
self.collect_stats(wfh)
171+
time.sleep(self.period)
172+
finally:
173+
wfh.close()
174+
except (TargetNotRespondingError, TimeoutError):
175+
raise
176+
except Exception as e:
177+
logger.warning('Exception on collector thread: {}({})'.format(e.__class__.__name__, e))
178+
self.exc = WorkerThreadError(self.name, sys.exc_info())
179+
logger.debug('Battery stats collection stopped.')
180+
181+
def collect_stats(self, wfh):
182+
voltage, batt_pct = self._get_battery_stats()
183+
current = self._measure_current()
184+
power = voltage * current
185+
results = ','.join(map(str, [voltage, current, power, batt_pct, time.time()]))
186+
wfh.write('{}\n'.format(results).encode('utf-8'))
187+
188+
def _get_battery_stats(self):
189+
output = self.target.execute('dumpsys battery')
190+
match = BATTERY_STATS_REGEX.search(output)
191+
voltage = int(match.group('voltage'))
192+
batt_pct = (int(match.group('level'))*100)/int(match.group('scale'))
193+
return voltage, batt_pct
194+
195+
def _measure_current(self):
196+
return self.target.read_int(self.current_node)
197+
198+
def stop(self):
199+
self.stop_signal.set()
200+
self.join()
201+
if self.exc:
202+
raise self.exc # pylint: disable=E0702

0 commit comments

Comments
 (0)