pebble/tools/power_monitor/power_monitor.py
2025-01-27 11:38:16 -08:00

320 lines
13 KiB
Python
Executable file

#!/usr/bin/env python
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# To use this script:
# - You need to install libmpsse
# + svn checkout http://libmpsse.googlecode.com/svn/trunk/ libmpsse-read-only
# + You will have to build the module following the instructions in docs/INSTALL
# + Note: For osx, the makefile calls 'install -D', which is an invalid argument so
# you need to replace it doing the following from the src/ directory:
# - sed -i '.backup' 's/install -D/install/' Makefile
# - For OSX, you need to unload the FTDI driver when you are using the power stats tool:
# sudo kextunload -b com.FTDI.driver.FTDIUSBSerialDriver
# (use kextload to reload once you are done to get console to work again)
import argparse
import atexit
import csv
import signal
import sys
import time
from ina226 import Ina226
from mcp23009 import Mcp23009
PowerRailMonitors = {
"snowy": {
# TODO: [PBL-36477] These resolutions ought to be calibrated.
'VBAT': Ina226('VBAT', 0x80, 200.0, 0.47),
'1V8': Ina226('1V8', 0x82, 100.0, 0.47),
'1V2_MCU': Ina226('1V2_MCU', 0x84, 100.0, 0.47),
'1V2_FPGA': Ina226('1V2_FPGA', 0x86, 100.0, 0.47),
'1V8_BT': Ina226('1V8_BT', 0x88, 100.0, 0.47),
'SPARE1': Ina226('SPARE1', 0x8A, 100.0, 0.47),
'SPARE2': Ina226('SPARE2', 0x8C, 100.0, 0.47),
'VUSB': Ina226('VUSB', 0x8E, 200.0, 0.47)
},
"silk": {
# TODO: [PBL-36477] These resolutions ought to be calibrated.
'VBAT': Ina226('VBAT', 0x80, 200.0, 0.47),
'1V8': Ina226('1V8', 0x82, 100.0, 0.47),
'1V8_MCU': Ina226('1V8_MCU', 0x84, 100.0, 0.47),
'3V0_LCD': Ina226('3V0_LCD', 0x86, 100.0, 0.47),
'1V8_BT': Ina226('1V8_BT', 0x88, 100.0, 0.47),
'SPARE1': Ina226('SPARE1', 0x8A, 100.0, 0.47),
'SPARE2': Ina226('SPARE2', 0x8C, 100.0, 0.47),
'VUSB': Ina226('VUSB', 0x8E, 100.0, 0.47),
'HRM': Ina226('HRM', 0x90, 200.0, 0.47),
},
"robert": {
# TODO: [PBL-36477] These resolutions ought to be calibrated.
'VBAT': Ina226('VBAT', 0x80, 200.0, 0.47),
'1V8': Ina226('1V8', 0x82, 100.0, 0.47),
'1V2_MCU': Ina226('1V2_MCU', 0x84, 100.0, 0.47),
'1V2_FPGA': Ina226('1V2_FPGA', 0x86, 100.0, 0.47),
'1V8_BT': Ina226('1V8_BT', 0x88, 100.0, 0.47),
'SPARE1': Ina226('SPARE1', 0x8A, 100.0, 0.47),
'SPARE2': Ina226('SPARE2', 0x8C, 100.0, 0.47),
'VUSB': Ina226('VUSB', 0x8E, 100.0, 0.47),
}
}
MCP = Mcp23009(0x4E)
class MatplotlibCurrentGraph:
"""Plots current readings using the Matplotlib library
"""
def __init__(self):
import matplotlib.pyplot as plt
import numpy as np
def exit_gracefully(signal, frame):
plt.close("all")
sys.exit(0)
signal.signal(signal.SIGINT, exit_gracefully)
f = plt.figure()
f.show()
self.plt = plt
self.np = np
self.graph_data = np.array([])
self.roll_at = 60000
self.samples_to_batch = 100
self.ax = f.add_subplot(111)
self.start_time = time.time()
def graph_function(self, read_input, count):
if len(self.graph_data) < self.roll_at:
self.graph_data = self.np.append(self.graph_data, read_input[1])
else:
self.graph_data = self.np.roll(self.graph_data, -1)
self.graph_data[self.roll_at - 1] = read_input[1]
if count % self.samples_to_batch == 0:
print "%.3f secs to collect last %d samples" % \
(time.time() - self.start_time, self.samples_to_batch)
self.plt.clf()
self.plt.plot(list(self.graph_data), 'b')
avg_all = "Avg: %.3f mA" % (self.np.average(self.graph_data) / 1000)
avg_last_set = "Last Collection Avg: %.3f uA" % (self.np.average(self.graph_data[-100:]))
self.plt.text(0.9, 0.9, avg_all,
horizontalalignment='center',
verticalalignment='center',
transform=self.ax.transAxes)
self.plt.text(0.3, 0.95, avg_last_set,
horizontalalignment='center',
verticalalignment='center',
transform=self.ax.transAxes)
self.plt.draw()
self.plt.pause(.01) # a brief stall of the UI thread lets you zoom in, etc
self.start_time = time.time()
def auto_int(x):
return int(x, 0)
class BokehCurrentGraph:
"""Plots current readings using the Bokeh library
"""
def __init__(self):
from bokeh.client import push_session
from bokeh.plotting import figure, curdoc
from bokeh.driving import cosine
import numpy as np
p = figure()
self.avg_text = p.text(1, 1, ["Computing average"])
self.avg_ds = self.avg_text.data_source
x = np.linspace(0, 4*3.14, 80)
y = np.sin(x)
r2 = p.line(x, y, color="navy", line_width=4)
self.ds = r2.data_source
self.np = np
self.graph_data = np.array([])
self.roll_at = 10000
self.samples_to_batch = 20
self.start_time = time.time()
self.session = push_session(curdoc())
self.session.show(p)
def graph_function(self, read_input, count):
if len(self.graph_data) < self.roll_at:
self.graph_data = self.np.append(self.graph_data, read_input[1])
else:
self.graph_data = self.np.roll(self.graph_data, -1)
self.graph_data[self.roll_at - 1] = read_input[1]
if count % self.samples_to_batch == 0:
print "%.3f secs to collect last %d samples" % \
(time.time() - self.start_time, self.samples_to_batch)
avg_all = "Avg: %.3f ma" % (self.np.average(self.graph_data) / 1000)
avg_last_set = "Last Collection Avg: %.3f mA %s" % \
(self.np.average(self.graph_data[-100:])/1000, avg_all)
self.ds.data["y"] = list(self.graph_data)
self.ds.data["x"] = self.np.linspace(1, len(self.graph_data), len(self.graph_data))
self.ds._dirty = True
self.avg_text.glyph.y = self.np.max(self.graph_data) * 0.99
self.avg_ds.data["text"] = [avg_last_set]
self.avg_ds._dirty = True
self.start_time = time.time()
def enabled_bool(str):
if str == 'enable':
return True
if str == 'disable':
return False
raise ValueError('Invalid state %s' % str)
if __name__ == "__main__":
PlatformNames = sorted(PowerRailMonitors.keys())
BackButtonNames = ['back', 'b']
UpButtonNames = ['up', 'u']
SelectButtonNames = ['select', 's']
DownButtonNames = ['down', 'd']
NoneButtonNames = ['none', 'n']
ButtonNames = BackButtonNames + UpButtonNames + SelectButtonNames + DownButtonNames + NoneButtonNames
parser = argparse.ArgumentParser(description='INA226 Power Monitor')
parser.add_argument('--vid', help='FTDI USB Vendor ID', default=0x0403, type=auto_int)
parser.add_argument('--pid', help='FTDI USB Product ID', default=0x6011, type=auto_int)
parser.add_argument('--index', help='FTDI Device Index', default=0)
# Snowy 4232 chip uses interface=2
parser.add_argument('--interface', choices=[1,2,3,4], default=2,
help='FT4232 Interface (default to interface B = 1)')
parser.add_argument('-o', '--outfile', help='Output CSV file for power data')
parser.add_argument('-r', '--rails', nargs='+', help='The Power rails to measure')
parser.add_argument('-c', '--continuous', help='Continuously monitor the current', action='store_true')
parser.add_argument('--avg', action='store_true', help='calculate the average of the rails')
parser.add_argument('-g', '--graph', help='locally graph power data', action='store_true')
parser.add_argument('-b', '--buttons', nargs='+', choices=ButtonNames, help=
'Push these buttons, release the rest. If this argument is omitted, the buttons are left as is')
# LEDs are currently disabled because I am dumb and made a mistake in the schematic
# parser.add_argument('--leds', nargs='+', choices=LedNames, help='Turn on these LEDs,
# turn off the rest. If this argument is omitted, the LEDs are left as is')
parser.add_argument('--usb_pwr', choices=['enable', 'disable'], help='Turn on or off USB power')
parser.add_argument('--acc_pu', choices=['enable', 'disable'], help='enable/disable the accessory pull-up')
parser.add_argument('-f', '--fast', help='Use libmpsse instead of pyftdi which collects samples ~6x faster at '\
'the cost of long term stability', action='store_true')
parser.add_argument('--platform', required=True, choices=PlatformNames, help='Specify the platform being measured on.')
parser.add_argument('--bokeh', action='store_true',
help='Live plot with bokeh instead of matplotlib (it\'s is faster!)')
args = parser.parse_args()
PlatformRails = PowerRailMonitors[args.platform]
RailNames = sorted(PlatformRails.keys())
if args.rails:
for r in args.rails:
if r not in RailNames:
print 'Rail "{}" not valid for platform "{}"'.format(r, args.platform)
print 'Valid rails are: {}'.format(RailNames)
sys.exit(1)
# local graphing setup
graph = None
if args.graph:
if args.bokeh:
graph = BokehCurrentGraph()
else:
graph = MatplotlibCurrentGraph()
# Open the MPSSE connection and setup I2C
if args.fast:
from mpsse import *
mode = I2C
frequency = ONE_HUNDRED_KHZ
else:
from i2c import *
mode = 0
frequency = 100000
I2CBus = MPSSE()
I2CBus.Open(vid=args.vid,
pid=args.pid,
mode=mode,
frequency=frequency,
interface=args.interface,
index=args.index)
atexit.register(I2CBus.Close)
MCP.setup(I2CBus)
# Toggling USB power
if args.usb_pwr:
MCP.setUsbChargeEn(enabled_bool(args.usb_pwr))
# Toggle accessory pull-up
if args.acc_pu:
MCP.setAccessoryPullup(enabled_bool(args.acc_pu))
# Change button states
if args.buttons:
up = down = back = select = False
for button in args.buttons:
if button in UpButtonNames:
up = True
elif button in DownButtonNames:
down = True
elif button in BackButtonNames:
back = True
elif button in SelectButtonNames:
select = True
MCP.setButtons(up=up, down=down, select=select, back=back)
# Power Monitoring
if args.rails:
for rail in args.rails:
sensor = PlatformRails[rail]
sensor.setupRail(I2CBus)
totals = {}
averages = {}
for rail in args.rails:
totals[rail] = 0
averages[rail] = 0
count = 0
def read_currents():
millis = int(round(time.time() * 1000))
read_tuple = [millis]+[PlatformRails[rail].readCurrent() for rail in args.rails]
return read_tuple
if args.outfile:
with open(args.outfile, 'wb') as csvfile:
powercsv = csv.writer(csvfile)
powercsv.writerow(args.rails)
powercsv.writerow(read_currents())
while args.rails and args.continuous:
powercsv.writerow(read_currents())
else:
print args.rails
while args.continuous:
readings = read_currents()
count += 1
if graph is not None:
graph.graph_function(readings, count-1)
elif args.avg:
for rail in args.rails:
totals[rail] += readings[args.rails.index(rail)+1]
averages[rail] = totals[rail]/count
print averages
else:
print readings