blob: 9d2ce99c4d7d5261ee178dd396b104db31187f2b [file] [log] [blame] [edit]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2021 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Main file for pacman utility"""
from argparse import ArgumentParser
import datetime
import os
import pathlib
from sys import modules
import urllib
import pac_utils
import pacboard
import pacconfig
import pandas
import plotly.express
def main():
"""main function"""
polarity_default = 'bipolar'
argparser = ArgumentParser(description=modules[__name__].__doc__)
argparser.add_argument(
'-s',
'--single',
help=
'Use to take a single voltage, current,'\
' power measurement of all rails and report GPIO status',
action='store_true')
argparser.add_argument('-t',
'--time',
default=9999999,
help='Time to capture in seconds')
argparser.add_argument('--sample_time',
default=1,
type=float,
help='Sample time in seconds')
argparser.add_argument(
'-c',
'--config',
type=pathlib.Path,
help='PAC address and configuration file used by servod')
argparser.add_argument(
'-O',
'--output',
nargs='?',
default=os.path.join(
'./Data',
datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S')),
help='Path for log files')
argparser.add_argument(
'-p',
'--polarity',
default=polarity_default,
help='Measurements can either be unipolar or bipolar')
argparser.add_argument(
'-d',
'--device',
default='',
help='Serial number of provisioned pacdebugger to use'
)
args = argparser.parse_args()
if not args.config:
print('PAC Configuration Required')
return False
# Store everything in output folder.
log_path = args.output
single_log_path = os.path.join(log_path, 'singleLog.csv')
time_log_path = os.path.join(log_path, 'timeLog.csv')
accumulator_log_path = os.path.join(log_path, 'accumulatorData.csv')
report_log_path = os.path.join(log_path, 'report.html')
pathlib.Path(log_path).mkdir(parents=True, exist_ok=True)
# Load board config
print(args.config)
config = pacconfig.PacConfig(args.config)
# Register the custom VID:PID used for provisioned PACDebuggers
pacboard.PacDebugger.configure_custom_devices()
# See if we've been passed a serial number to use
ftdi_url = 'ftdi:///'
if args.device != '':
ftdi_url = pacboard.PacDebugger.url_by_serial(args.device)
# If single, take a single shot of these measurements then quit.
# Print which files are being used for clarity
if args.single:
print(
'Taking a single voltage, current, '\
'power measurement of all rails and reporting GPIO status'
)
log = pac_utils.query_all(config, ftdi_url=ftdi_url,
polarity=args.polarity)
log.to_csv(single_log_path)
return False
# Else we're going to take a time log.
# Print which files are being used for clarity
print('Taking an extended reading')
# Record the sample and log to file.
(log, accumulator_log) = pac_utils.record(config,
ftdi_url=ftdi_url,
record_length=args.time,
polarity=args.polarity,
sample_time=args.sample_time)
log.to_csv(time_log_path)
accumulator_log.to_csv(accumulator_log_path)
# Generate plots using plotly.
time_plot = plotly.express.line(log,
x='relativeTime',
y='power',
color='rail',
labels={
'power': 'Power (w)',
'relativeTime': 'Time (seconds)'
})
time_plot.update_layout(
title='Time Series',
xaxis_title='Time (Seconds)',
yaxis_title='Power (W)',
)
box_plot = plotly.express.box(log, y='power', x='rail')
box_plot.update_layout(title='Instaneous Measurement Statistics',
xaxis_title='Rail',
yaxis_title='Power (W)')
accumulator_log = accumulator_log.sort_values(by='Average Power (w)',
ascending=False)
summary_table = plotly.graph_objects.Figure(data=[
plotly.graph_objects.Table(
header=dict(values=[
'Rail', 'Accumulation Time (s)', 'Sense Resistor (Ohm)',
'Average Power (W)'
],
align='left'),
cells=dict(values=[
accumulator_log.Rail,
accumulator_log.tAccum.round(2), accumulator_log.rSense,
accumulator_log['Average Power (w)'].round(3)
],
align='left'))
])
summary_table.update_layout(title='Accumulator Measurements')
if len(config.rails) > 0:
mapping = []
for rail in config.rails:
mapping.append({'Rail': rail.rail, 'Parent': rail.parent})
skip_sunplot = False
mapping = pandas.DataFrame(mapping)
accumulator_log = pandas.merge(accumulator_log, mapping, on='Rail')
# Bidirectional Values means powers can be negative
avg_power = accumulator_log['Average Power (w)'].abs()
accumulator_log['Average Power (w)'] = avg_power
# Voltage column used for color coding
accumulator_log['voltage (mv)'] = accumulator_log.Rail.apply(
lambda x: x.split('_')[0].strip('PP'))
star_plot = plotly.express.sunburst(accumulator_log,
names='Rail',
parents='Parent',
values='Average Power (w)',
title='Power Sunburst',
color='voltage (mv)')
# Calculate what the sum of the child rails of root is
root = 'PPVAR_SYS'
root_pwr = accumulator_log[accumulator_log.Rail ==
root]['Average Power (w)']
# This should be a single element
root_pwr = root_pwr.iloc[0]
# tier 1 rails, children of root
t1_rails = accumulator_log[accumulator_log['Parent'] == 'PPVAR_SYS']
summary_columns = ['Rail', 'voltage (mv)', 'Average Power (w)']
t1_pwr = t1_rails['Average Power (w)'].sum()
t1_summary = t1_rails[summary_columns]
t1_summary_text = (
f"{'T1 Rail Total:':<20}{t1_pwr:>20.3f}" + '\n' +
f"{'T1 Root %s' % root:<20}{root_pwr:>20.3f}" + '\n' +
f"{'Root - T1 Total:':<20}{(root_pwr - t1_pwr):>20.3f}" + '\n')
print('Tier1 Summary')
print(t1_summary_text)
print(t1_summary)
# HTML summary table
t1_summary_table = plotly.graph_objects.Figure(data=[
plotly.graph_objects.Table(
header=dict(values=summary_columns, align='left'),
cells=dict(values=[
t1_summary.Rail, t1_summary['voltage (mv)'],
t1_summary['Average Power (w)'].round(3)
],
align='left'))
])
t1_summary_table.update_layout(
title=f"{'T1 Rail Total: %.3f Watts' % t1_pwr:6>}")
else:
print('Skipping Sunplot')
skip_sunplot = True
# Generate an HTML Report.
with open(report_log_path, 'w') as f:
f.write(summary_table.to_html(full_html=False, include_plotlyjs='cdn'))
if not skip_sunplot:
f.write(
t1_summary_table.to_html(full_html=False,
include_plotlyjs='cdn',
default_width='100%',
default_height='50%'))
f.write(star_plot.to_html(full_html=False, include_plotlyjs='cdn'))
f.write(box_plot.to_html(full_html=False, include_plotlyjs='cdn'))
f.write(time_plot.to_html(full_html=False, include_plotlyjs='cdn'))
# Use PWD if available to avoid de-referencing symlinks otherwise use
# CWD
full_path = os.path.join(
os.getenv('PWD') or os.getcwd(), report_log_path)
full_path = os.path.normpath(full_path)
print(f'Report: file://{urllib.parse.quote(full_path)}')
return True
if __name__ == '__main__':
main()