Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
S2-group
GitHub Repository: S2-group/android-runner
Path: blob/master/AndroidRunner/Plugins/batterymanager/Batterymanager.py
907 views
1
import csv
2
import numpy as np
3
import os.path as op
4
import os
5
import pandas as pd
6
import time
7
import re
8
9
from AndroidRunner.Plugins.Profiler import Profiler
10
11
12
class Batterymanager(Profiler):
13
14
ANDROID_VERSION_11_API_LEVEL_30 = 30
15
BATTERYMANAGER_DEVICE_OUTPUT_FILE = '/storage/emulated/0/Documents/BatteryManager.csv'
16
AVAILABLE_DATA_POINTS = ['ACTION_CHARGING', 'ACTION_DISCHARGING',
17
'BATTERY_HEALTH_COLD', 'BATTERY_HEALTH_DEAD', 'BATTERY_HEALTH_GOOD',
18
'BATTERY_HEALTH_OVERHEAT',
19
'BATTERY_HEALTH_OVER_VOLTAGE', 'BATTERY_HEALTH_UNKNOWN',
20
'BATTERY_HEALTH_UNSPECIFIED_FAILURE',
21
'BATTERY_PLUGGED_AC', 'BATTERY_PLUGGED_DOCK', 'BATTERY_PLUGGED_USB',
22
'BATTERY_PLUGGED_WIRELESS',
23
'BATTERY_PROPERTY_CAPACITY', 'BATTERY_PROPERTY_CHARGE_COUNTER',
24
'BATTERY_PROPERTY_CURRENT_AVERAGE',
25
'BATTERY_PROPERTY_CURRENT_NOW', 'BATTERY_PROPERTY_ENERGY_COUNTER',
26
'BATTERY_PROPERTY_STATUS',
27
'BATTERY_STATUS_CHARGING', 'BATTERY_STATUS_DISCHARGING', 'BATTERY_STATUS_FULL',
28
'BATTERY_STATUS_NOT_CHARGING', 'BATTERY_STATUS_UNKNOWN',
29
'EXTRA_BATTERY_LOW', 'EXTRA_HEALTH', 'EXTRA_ICON_SMALL', 'EXTRA_LEVEL', 'EXTRA_PLUGGED',
30
'EXTRA_PRESENT',
31
'EXTRA_SCALE', 'EXTRA_STATUS', 'EXTRA_TECHNOLOGY', 'EXTRA_TEMPERATURE', 'EXTRA_VOLTAGE']
32
33
AVAILABLE_PERSISTENCY_STRATEGIES = ['csv', 'adb_log']
34
35
def __init__(self, config, paths):
36
super(Batterymanager, self).__init__(config, paths)
37
self.output_dir = ''
38
self.paths = paths
39
self.profile = False
40
41
self.sampling_rate = config.get('sample_interval', 1000) # default: every second
42
43
self.data_points = self.validate_config('data_points',
44
config['data_points'],
45
Batterymanager.AVAILABLE_DATA_POINTS)
46
47
self.persistency_strategy = self.validate_config('persistency_strategy',
48
config['persistency_strategy'],
49
Batterymanager.AVAILABLE_PERSISTENCY_STRATEGIES)
50
51
def validate_config(self, field, raw_data_points, available_data_points):
52
invalid_data_points = [
53
dp for dp in raw_data_points if dp not in set(available_data_points)]
54
if invalid_data_points:
55
self.logger.warning(
56
'Invalid {} in config: {}'.format(field, invalid_data_points))
57
return [dp for dp in raw_data_points
58
if dp in available_data_points]
59
60
# Check if the selected data points are valid
61
def start_profiling(self, device, **kwargs):
62
device.shell(self.build_intent(True))
63
64
def stop_profiling(self, device, **kwargs):
65
device.shell(self.build_intent(False))
66
67
def build_intent(self, is_start):
68
if is_start:
69
intent_data_fields = ','.join(self.data_points)
70
intent_to_csv = 'true' if 'csv' in self.persistency_strategy else 'false'
71
intent = f'am start-foreground-service -n "com.example.batterymanager_utility/com.example' \
72
f'.batterymanager_utility.DataCollectionService" --ei sampleRate {self.sampling_rate} --es ' \
73
f'"dataFields" "{intent_data_fields}" --ez toCSV {intent_to_csv}'
74
else:
75
intent = f'am stopservice com.example.batterymanager_utility/com.example.batterymanager_utility' \
76
f'.DataCollectionService'
77
78
return intent
79
80
def collect_results(self, device):
81
# sleep for 5 seconds to make sure the service has stopped
82
time.sleep(5)
83
if 'csv' in self.persistency_strategy:
84
batterymanager_csv_file = op.join(self.output_dir,
85
'{}_{}.csv'.format(device.id, time.strftime('%Y.%m.%d_%H%M%S')))
86
device.pull('{}'.format(self.BATTERYMANAGER_DEVICE_OUTPUT_FILE), batterymanager_csv_file)
87
device.shell('rm -f {}'.format(self.BATTERYMANAGER_DEVICE_OUTPUT_FILE))
88
89
if 'adb_log' in self.persistency_strategy:
90
logcat_file = op.join(self.output_dir,
91
'logcat_{}_{}.txt'.format(device.id, time.strftime('%Y.%m.%d_%H%M%S')))
92
self.pull_logcat(device, logcat_file)
93
94
header, rows = self.get_logcat(device)
95
self.write_logcat_csv(device, header, rows)
96
97
@staticmethod
98
def pull_logcat(device, logcat_file):
99
"""
100
From Android 11 (API level 30) the path /mnt/sdcard cannot be accessed via ADB
101
as you don't have permissions to access this path. However, we can access /sdcard.
102
"""
103
device_api_version = int(device.shell("getprop ro.build.version.sdk"))
104
105
if device_api_version >= Batterymanager.ANDROID_VERSION_11_API_LEVEL_30:
106
logcat_output_file_device_dir_path = "/sdcard"
107
else:
108
logcat_output_file_device_dir_path = "/mnt/sdcard"
109
110
device.shell(f"logcat -f {logcat_output_file_device_dir_path}/logcat.txt -d")
111
device.pull(f"{logcat_output_file_device_dir_path}/logcat.txt", logcat_file)
112
device.shell(f"rm -f {logcat_output_file_device_dir_path}/logcat.txt")
113
114
@staticmethod
115
def get_logcat(device):
116
header_pattern = 'BatteryMgr:DataCollectionService: onStartCommand: rawFields => '
117
data_pattern = 'BatteryMgr:DataCollectionService: stats => '
118
119
raw_header = device.logcat_regex(header_pattern)
120
raw_rows = device.logcat_regex(data_pattern)
121
122
return raw_header, raw_rows
123
124
@staticmethod
125
def preprocess_logcat(header, rows):
126
header = header.split('=> ')[1]
127
header = header.split('\n')[0]
128
header = header.split(',')
129
130
# FOR OLDER DEVICES remove all non-letter characters except _
131
header = [re.sub(r'[^a-zA-Z_]', '', h) for h in header]
132
133
rows = rows.split('\n')
134
# FOR OLDER DEVICES remove rows containing "
135
rows = [row for row in rows if '"' not in row]
136
rows = [row.split('=> ')[1].split(',') for row in rows]
137
138
rows.sort(key=lambda x: x[0])
139
return header, rows
140
141
def write_logcat_csv(self, device, header, rows):
142
header, rows = Batterymanager.preprocess_logcat(header, rows)
143
logcat_csv_file = op.join(self.output_dir,
144
'logcat_{}_{}.csv'.format(device.id, time.strftime('%Y.%m.%d_%H%M%S')))
145
146
with open(logcat_csv_file, 'w') as lc_csv_file:
147
csv_writer = csv.writer(lc_csv_file)
148
csv_writer.writerow(header)
149
for row in rows:
150
csv_writer.writerow(row)
151
152
def unload(self, device):
153
return
154
155
def dependencies(self):
156
return ['com.example.batterymanager_utility']
157
158
def load(self, device):
159
return
160
161
def set_output(self, output_dir):
162
self.output_dir = output_dir
163
164
def aggregate_subject(self):
165
return
166
167
@staticmethod
168
def list_subdir(a_dir):
169
"""List immediate subdirectories of a_dir"""
170
# https://stackoverflow.com/a/800201
171
return [name for name in os.listdir(a_dir)
172
if os.path.isdir(os.path.join(a_dir, name))]
173
174
@staticmethod
175
def preprocess_values(df):
176
df['Timestamp'] = df['Timestamp'] - df['Timestamp'][0]
177
# conversion from microseconds to seconds
178
df['Timestamp'] = df['Timestamp'] / 1000
179
return df
180
181
@staticmethod
182
def calculate_power(df):
183
df['power'] = (abs(df['BATTERY_PROPERTY_CURRENT_NOW']) / 1000 / 1000) * (df['EXTRA_VOLTAGE'] / 1000)
184
return df
185
186
@staticmethod
187
def trapezoid_method(df):
188
return np.trapz(df['power'].values, df['Timestamp'].values)
189
190
@staticmethod
191
def aggregate_batterymanager_runs(logs_dir):
192
runs = pd.DataFrame()
193
run_number = 0
194
for run_file in [f for f in os.listdir(logs_dir) if os.path.isfile(os.path.join(logs_dir, f))]:
195
f_name = os.path.join(logs_dir, run_file)
196
if not f_name.endswith(".csv"):
197
continue
198
run_df = pd.read_csv(f_name)
199
200
stats = {}
201
if 'BATTERY_PROPERTY_CURRENT_NOW' in run_df.columns and 'EXTRA_VOLTAGE' in run_df.columns:
202
run_df = Batterymanager.preprocess_values(run_df)
203
run_df = Batterymanager.calculate_power(run_df)
204
avg_power = run_df['power'].mean()
205
stats.update({'Avg power (W)': avg_power})
206
stats.update({'Energy simple (J)': avg_power * run_df['Timestamp'].max()})
207
stats.update({'Energy trapz (J)': Batterymanager.trapezoid_method(run_df)})
208
stats.update(run_df.mean().to_dict())
209
stats.update({'run': run_number})
210
run_number += 1
211
212
runs = pd.concat([runs, pd.DataFrame(stats, index=[0])], ignore_index=True)
213
214
runs = runs.drop(columns=['Timestamp', 'power'], axis=1)
215
return runs
216
217
@staticmethod
218
def aggregate(data_dir):
219
df = pd.DataFrame()
220
for device in Batterymanager.list_subdir(data_dir):
221
device_dir = os.path.join(data_dir, device)
222
for subject in Batterymanager.list_subdir(device_dir):
223
subject_dir = os.path.join(device_dir, subject)
224
if os.path.isdir(os.path.join(subject_dir, 'batterymanager')):
225
runs_df = Batterymanager.aggregate_batterymanager_runs(os.path.join(subject_dir, 'batterymanager'))
226
runs_df['subject'] = subject
227
runs_df['device'] = device
228
df = pd.concat([df, runs_df], ignore_index=True)
229
return df[df.columns[::-1]]
230
231
def aggregate_end(self, data_dir, output_file):
232
print(('Output file: {}'.format(output_file)))
233
rows = self.aggregate(data_dir)
234
rows.to_csv(output_file, index=False)
235
236