Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson 2024-12-12 16:43:03 -08:00 committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions

View file

@ -0,0 +1,14 @@
# 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.

326
tools/power_monitor/i2c.py Normal file
View file

@ -0,0 +1,326 @@
# 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.
# Code adapted from libmpsse implementation
# May be combined with pyftdi at a later date
# - please do not mix Tintin code in
from pyftdi.pyftdi import ftdi
from array import array as Array
class MPSSE(ftdi.Ftdi):
I2C = 5
CMD_SIZE = 3
# Write bits, not bytes
MPSSE_WRITE_NEG = 0x01
MPSSE_BITMODE = 0x02
MPSSE_READ_NEG = 0x04
MPSSE_DO_WRITE = 0x10
MPSSE_DO_READ = 0x20
MPSSE_OK = 0
MPSSE_FAIL = -1
ACK = 0
NACK = 1
I2C_TRANSFER_SIZE = 64
# Enum low_bits_status
STARTED = 0
STOPPED = 1
# Pins
SK = 1
DO = 2
DI = 4
CS = 8
GPIO0 = 16
GPIO1 = 32
GPIO2 = 64
GPIO3 = 128
DEFAULT_TRIS = (SK | DO | GPIO0 | GPIO1 | GPIO2 | GPIO3)
DEFAULT_PORT = SK
MAX_SETUP_COMMANDS = 10
DISABLE_ADAPTIVE_CLOCK = 0x97
ENABLE_3_PHASE_CLOCK = 0x8C
# 0x0 for MSB, 0x8 for LSB; I2C is MSB
ENDIANESS = 0x0
def __init__(self):
# Init the superconstructor
super(MPSSE, self).__init__()
# Init, will open the mpsse and setup the pins
def Open(self, vid, pid, mode=0, interface=1,
index=0, frequency=1.0E5):
self.usb_read_timeout = 5000
# Ack property
self.rack = 0
# Start/stop status
self.status = self.STOPPED
# Open the mpsse
self.open_mpsse(vendor=vid,
product=pid,
interface=interface,
index=index,
frequency=frequency)
# Finish setup
self._set_mode()
# Start condition
def Start(self):
status = self.MPSSE_OK
# I2C repeated start condition
if self.status == self.STARTED:
status |= self._set_bits_low((self.pidle & ~self.SK))
# Set pins to idle
status |= self._set_bits_low(self.pidle)
# Set start condition
status |= self._set_bits_low(self.pstart)
self.status = self.STARTED
return status
# Stop condition
def Stop(self):
retval = self.MPSSE_OK
retval |= self._set_bits_low((self.pidle & ~self.DO & ~self.SK))
retval |= self._set_bits_low(self.pstop)
if retval == self.MPSSE_OK:
# Pins to idle
retval |= self._set_bits_low(self.pidle)
self.status = self.STOPPED
return retval
# Write in bytes, input MSB
def Write(self, data):
n = 0
# transfer size of I2C is 1
txsize = 1
retval = self.MPSSE_FAIL
size = len(data)
while n < size:
buf = self._build_block_buffer(self.tx, data[n:n+txsize], txsize)
retval = self._ftdi_raw_write(buf)
n += txsize
if retval == self.MPSSE_FAIL:
break
# Read in the ACK bit and store it in self.rack
buf = Array('B')
t, buf = self._ftdi_raw_read(buf, 1)
self.rack = buf[0]
if retval == self.MPSSE_OK and n == size:
retval = self.MPSSE_OK
else:
retval = self.MPSSE_FAIL
return retval
# Read in bytes, output MSB
def Read(self, size):
buf = self._internal_read(size)
return buf
# Ack returned?
def GetAck(self):
return self.rack & 0x01
def SetAck(self, ack):
if ack == self.NACK:
self.tack = 0xFF
else:
self.tack = 0x00
def SendAcks(self):
self.SetAck(self.ACK)
def SendNacks(self):
self.SetAck(self.NACK)
# Close the I2C when done
def Close(self):
self.set_bitmode(0, self.BITMODE_RESET)
self.close()
# Set the low bit pins high/low
def _set_bits_low(self, port):
buf = Array('B')
buf.append(self.SET_BITS_LOW)
buf.append(port)
buf.append(self.tris)
return self._ftdi_raw_write(buf)
# Part of the setup
def _set_mode(self):
retval = self.MPSSE_OK
setup_commands = Array('B')
self.write_data_set_chunksize(65535)
# Set tx and rx
self.tx = self.MPSSE_DO_WRITE | self.ENDIANESS
self.rx = self.MPSSE_DO_READ | self.ENDIANESS
self.txrx = self.MPSSE_DO_WRITE | self.MPSSE_DO_READ | self.ENDIANESS
# Clock, data out, chip select pins are outputs; all others are inputs.
self.tris = self.DEFAULT_TRIS | self.CS
# Clock and chip select pins idle high; all others are low
self.pidle = self.DEFAULT_PORT | self.CS
self.pstart = self.DEFAULT_PORT | self.CS
self.pstop = self.DEFAULT_PORT | self.CS
# During reads and writes the chip select pin is brought low
self.pstart &= ~self.CS
# Send ACKs by default , set tack to 0x00. or 0xFF
self.tack = 0x00
# Ensure adaptive clock is disabled
setup_commands.append(self.DISABLE_ADAPTIVE_CLOCK)
# I2C configurations on pins:
# Send on falling clock edge and read data on falling (or rising) clock edge
self.tx |= self.MPSSE_WRITE_NEG
self.rx &= ~self.MPSSE_READ_NEG
# Both the clock and the data lines are idle high
self.pidle |= self.DO | self.DI
# Start bit == data line goes from high to low while clock line is high
self.pstart &= ~self.DO & ~self.DI
# Stop bit == data line goes from low to high while clock line is high
# - set data line low here, so the transition to the idle state triggers the stop condition.
self.pstop &= ~self.DO & ~self.DI
# Enable three phase clock, data to be available on both rising and falling clock edges
setup_commands.append(self.ENABLE_3_PHASE_CLOCK)
setup_commands_size = len(setup_commands)
# Send any setup commands to the chip
if(retval == self.MPSSE_OK) and (setup_commands_size > 0):
retval = self._ftdi_raw_write(setup_commands)
if retval == self.MPSSE_OK:
# Set the idle pin states
self._set_bits_low(self.pidle)
# All GPIO pins are outputs, set low
self.trish = 0xFF
self.gpioh = 0x00
buf = Array('B')
buf.append(self.SET_BITS_HIGH)
buf.append(self.gpioh)
buf.append(self.trish)
retval = self._ftdi_raw_write(buf)
return retval
# Package to send to chip
def _build_block_buffer(self, cmd, data, size):
buf = Array('B')
k = 0
for j in range(0, size):
# Clock pin set low prior to clocking data
buf.append(self.SET_BITS_LOW)
buf.append(self.pstart & ~self.SK)
if cmd == self.rx:
buf.append(self.tris & ~self.DO)
else:
buf.append(self.tris)
buf.append(cmd)
buf.append(0)
if not (cmd & self.MPSSE_BITMODE):
buf.append(0)
# append data input only if write
if cmd == self.tx or cmd == self.txrx:
buf.append(ord(data[k]))
k += 1
# In I2C mode clock one ACK bit
if cmd == self.rx:
buf.append(self.SET_BITS_LOW)
buf.append(self.pstart & ~self.SK)
buf.append(self.tris)
buf.append(self.tx | self.MPSSE_BITMODE)
buf.append(0)
buf.append(self.tack)
elif cmd == self.tx:
buf.append(self.SET_BITS_LOW)
buf.append(self.pstart & ~self.SK)
buf.append(self.tris & ~self.DO)
buf.append(self.rx | self.MPSSE_BITMODE)
buf.append(0)
buf.append(self.SEND_IMMEDIATE)
return buf
def _internal_read(self, size):
n = 0
buf = Array('B')
while n < size:
rxsize = size - n
rxsize - min(self.I2C_TRANSFER_SIZE, rxsize)
# buf not used by build_block when reading
data = self._build_block_buffer(self.rx, buf, rxsize)
retval = self._ftdi_raw_write(data)
if retval == self.MPSSE_OK:
t, buf = self._ftdi_raw_read(buf, rxsize)
if t == 0:
raise Exception("Corrupt Read")
n += t
else:
break
return buf
# Write data to the FTDI chip
def _ftdi_raw_write(self, buf):
if self.write_data(buf) == len(buf):
return self.MPSSE_OK
else:
return self.MPSSE_FAIL
# Read data from the FTDI chip
def _ftdi_raw_read(self, buf, size):
n = 0
prev = -1
while n < size:
str_buf = self.read_data(size)
for s in str_buf:
buf.append(ord(s))
r = len(buf)
if r < 0:
break
# detect if hanging
elif r == 0:
if prev == r:
return 0, buf
else:
prev = r
n += r
return n, buf

View file

@ -0,0 +1,136 @@
#!/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.
import logging
import math
from struct import unpack, pack
from array import array as Array
REG_CONFIG = 0x00
REG_SHUNT_V = 0x01
REG_BUS_V = 0x02
REG_POWER = 0x03
REG_CURRENT = 0x04
REG_CALIBRATION = 0x05
REG_MASK_EN = 0x06
REG_ALERT_LIM = 0x07
REG_DIE_ID = 0xFF
HIGH_BYTES_MULTIPLIER = 256
ACK = 0
class Ina226:
# micro{amps,watts,volts}
UNITS_SCALING = 10**3
# from TI INA226 datasheet
SHUNT_LSB = 0.0025
BUS_LSB = 0.00125
def __init__(self, name, address, max_i, shunt_r):
self.name = name
self.i2cBus = None
# the write address
self.i2cAddress = address & 0xFE
# Convert max current to mA
max_i /= self.UNITS_SCALING
self.calibration_value = 0.00512 / ((max_i / (1 << 15)) * shunt_r)
self.calibration_value = min(self.calibration_value, 0xFFFF)
self.current_lsb = 0.00512 / (shunt_r * self.calibration_value)
self.current_lsb *= self.UNITS_SCALING
self.power_lsb = self.current_lsb * 25
def _i2cWrite16BitReg(self, regAddress, regValue):
self.i2cBus.Start()
# use pack to split uShort sized regValue into 2 char sized
writeString = pack('>BBH', self.i2cAddress, regAddress, regValue)
self.i2cBus.Write(writeString)
if self.i2cBus.GetAck() != ACK:
self.i2cBus.Stop()
logging.warn("No ack received for command string")
self.i2cBus.Stop()
def _i2cSetReadPointer(self, regAddress):
self.i2cBus.Start()
writeString = pack('>BB', self.i2cAddress, regAddress)
self.i2cBus.Write(writeString)
if self.i2cBus.GetAck() != ACK:
self.i2cBus.Stop()
logging.warn("No ack received for command string")
self.i2cBus.Stop()
def _i2cReadCurrent16Bits(self):
self.i2cBus.Start()
writeString = pack('>B', self.i2cAddress + 1)
self.i2cBus.Write(writeString)
if self.i2cBus.GetAck() != ACK:
self.i2cBus.Stop()
logging.warn("No ack received for command string")
data = self.i2cBus.Read(2)
return data
def _i2cRead16BitReg(self, regAddress):
self._i2cSetReadPointer(regAddress)
return self._i2cReadCurrent16Bits()
def setupRail(self, i2cBus):
self.i2cBus = i2cBus
# 1024 Averages, 8.244ms VBUS conversion time, 8.244ms VSH conversion time, Continuous Shunt and Bus Mode
# self._i2cWrite16BitReg(REG_CONFIG, 0x47FF)
self._i2cWrite16BitReg(REG_CONFIG, 0x4127)
# Conversion Ready to Alert pin
# TODO: figure out the mask vs alert register
self._i2cWrite16BitReg(REG_MASK_EN, 0x0400)
self._i2cWrite16BitReg(REG_ALERT_LIM, 0x0400)
self._i2cWrite16BitReg(REG_CALIBRATION, self.calibration_value)
self._i2cWrite16BitReg(REG_ALERT_LIM, 0x1400)
def read_value(self, reg, scale_factor):
value = unpack('!h', self._i2cRead16BitReg(reg))[0]
return value * scale_factor * self.UNITS_SCALING
def readShuntVoltage(self):
""" Returns shunt voltage in uV. """
return self.read_value(REG_SHUNT_V, self.SHUNT_LSB)
def readBusVoltage(self):
""" Returns bus voltage in uV. """
return self.read_value(REG_BUS_V, self.BUS_LSB)
def readPower(self):
""" Returns power in uW. """
return self.read_value(REG_POWER, self.power_lsb)
def readCurrent(self):
""" Returns current in uA. """
return self.read_value(REG_CURRENT, self.current_lsb)

View file

@ -0,0 +1,164 @@
#!/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.
from struct import *
from array import array as Array
import time
REG_IODIR = 0x00
REG_IPOL = 0x01
REG_GPINTEN = 0x02
REG_DEFVAL = 0x03
REG_INTCON = 0x04
REG_IOCON = 0x05
REG_GPPU = 0x06
REG_INTF = 0x07
REG_INTCAP = 0x08
REG_GPIO = 0x09
REG_OLAT = 0x0a
ACK = 0
class Mcp23009:
def __init__(self, address):
self.i2cBus = None
# the write address
self.i2cAddress = address & 0xFE
def setup(self, i2cBus):
self.i2cBus = i2cBus
# I was dumb and put the LEDs to GND even though the IO expander is OD
# outputs :(. They will never work. Ignore them for now.
# make buttons, USB power outputs (OD), leave accessory PU and LEDS as is.
curIODIR = unpack('>B', self._i2cRead8BitReg(REG_IODIR))[0] & 0xEF
self._i2cWrite8BitReg(REG_IODIR, curIODIR)
self._i2cWrite8BitReg(REG_GPPU, 0x10)
def _i2cWrite8BitReg(self, regAddress, regValue):
self.i2cBus.Start()
writeString = pack('>BBB', self.i2cAddress, regAddress, regValue)
self.i2cBus.Write(writeString)
if self.i2cBus.GetAck() != ACK:
print "NO ACK RECEIVED w0"
#self.i2cBus.Stop()
#raise Exception("No ack received for command string %s" % writeString)
self.i2cBus.Stop()
def _i2cRead8BitReg(self, regAddress):
self.i2cBus.Start()
writeString = pack('>BB', self.i2cAddress, regAddress)
self.i2cBus.Write(writeString)
if self.i2cBus.GetAck() != ACK:
print "NO ACK RECEIVED r1"
self.i2cBus.Start()
writeString = pack('B', (self.i2cAddress + 0x01))
self.i2cBus.Write(writeString)
if self.i2cBus.GetAck() != ACK:
print "NO ACK RECEIVED r3"
self.i2cBus.SendNacks()
data = self.i2cBus.Read(1)
self.i2cBus.SendAcks()
self.i2cBus.Stop()
return data
def setButtons(self, back=False, up=False, down=False, select=False):
# read the current GPIO register and mask out the buttons
curGPIO = unpack('>B',self._i2cRead8BitReg(REG_GPIO))[0] | 0x0f
print "Before - setButton: 0x%x" % curGPIO
if back:
curGPIO &= 0xf7
if up:
curGPIO &= 0xfb
if select:
curGPIO &= 0xfd
if down:
curGPIO &= 0xfe
print "After - setButton: 0x%x" % curGPIO
self._i2cWrite8BitReg(REG_GPIO, curGPIO)
def configureGPIODirection(self, gpio_mask, as_output=True):
# The mask tells us what IOs we would like to be an output or input
gpiodir = unpack('>B', self._i2cRead8BitReg(REG_IODIR))[0]
if as_output:
new_gpiodir = gpiodir & ~gpio_mask # 1 == input, 0 == output
else:
new_gpiodir = gpiodir | gpio_mask
if gpiodir != new_gpiodir:
self._i2cWrite8BitReg(REG_IODIR, new_gpiodir)
gpiodir = unpack('>B', self._i2cRead8BitReg(REG_IODIR))[0]
print "REG_IODIR = 0x%x" % gpiodir
def setUsbChargeEn(self, chargeEnable=False):
usb_en_mask = 0x10
self.configureGPIODirection(usb_en_mask)
# read the current GPIO register and mask out the USB V+ En
curGPIO = unpack('>B', self._i2cRead8BitReg(REG_GPIO))[0] & 0xEF
print "Before - setUsbChargeEn: 0x%x" % curGPIO
if chargeEnable:
curGPIO |= usb_en_mask
print "After - setUsbChargeEn 0x%x" % curGPIO
self._i2cWrite8BitReg(REG_GPIO, curGPIO)
def setAccessoryPullup(self, pullupEnable=False):
acc_en_mask = 0x20
self.configureGPIODirection(acc_en_mask)
# read the current GPIO register
curGPIO = unpack('>B', self._i2cRead8BitReg(REG_GPIO))[0]
is_enabled = (curGPIO & acc_en_mask) == 0
# Are we requesting a change in state?
if (is_enabled != pullupEnable):
curGPIO &= ~acc_en_mask # Clear the current setting
if not pullupEnable: # The ACC_PU_EN is active low
curGPIO |= acc_en_mask
self._i2cWrite8BitReg(REG_GPIO, curGPIO)
curGPIO = unpack('>B', self._i2cRead8BitReg(REG_GPIO))[0]
print "REG_GPIO = 0x%x" % curGPIO
def reset(self):
# this is the reset sequence
self._i2cWrite8BitReg(REG_IODIR, 0xFF)
self._i2cWrite8BitReg(REG_IPOL, 0x00)
self._i2cWrite8BitReg(REG_GPINTEN, 0x00)
self._i2cWrite8BitReg(REG_DEFVAL, 0x00)
self._i2cWrite8BitReg(REG_INTCON, 0x00)
self._i2cWrite8BitReg(REG_IOCON, 0x00)
self._i2cWrite8BitReg(REG_GPPU, 0x00)
self._i2cWrite8BitReg(REG_INTF, 0x00)
self._i2cWrite8BitReg(REG_INTCAP, 0x00)
self._i2cWrite8BitReg(REG_GPIO, 0x00)
self._i2cWrite8BitReg(REG_OLAT, 0x00)
def readRegs(self):
for reg in range(0,11):
print "%x: %x" %(reg, unpack('>B',self._i2cRead8BitReg(reg))[0])

View file

@ -0,0 +1,320 @@
#!/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