#!/usr/bin/python3
#
# Module: ina260.py
#
# Description: This module acts as an interface between the INA260 sensor
# and downstream applications that use the data.  Class methods get
# current, voltage, and power data from the INA260 sensor.  It acts as a
# library module that can be imported into and called from other Python
# programs.
#
# Copyright 2022 Jeff Owrey
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see http://www.gnu.org/license.
#
# Revision History
#   * v10 released 01 June 2021 by J L Owrey; first release
#   * v20 released 16 March 2022 by J L Owrey; major revision to add
#     fuctionality allowing i2c serial devices to use i2c serial bus
#     multiplexer.  Also upgraded to python 3. 
#
#2345678901234567890123456789012345678901234567890123456789012345678901234567890

# Import the I2C interface library
import i2cmux

# Define Device Registers
CONFIG_REG = 0x0
ID_REG = 0xFE
CUR_REG = 0x1
VOLT_REG = 0x2
PWR_REG = 0x3

# Define default sm bus address.
DEFAULT_MUX_CHANNEL = 0
DEFAULT_BUS_ADDRESS = 0x40
DEFAULT_BUS_NUMBER = 1

# Define the default sensor configuration.  See the INA260 data sheet
# for meaning of each bit.  The following bytes are written to the
# configuration register
#     byte 1: 11100000
#     byte 2: 00100111
DEFAULT_CONFIG = 0xE027

class ina260:
    # Initialize the INA260 sensor at the supplied address (default
    # address is 0x40), and supplied bus (default is 1).  Creates
    # a new SMBus object for each instance of this class.  Writes
    # configuration data (two bytes) to the INA260 configuration
    # register.
    def __init__(self, objMux, sAddr=DEFAULT_BUS_ADDRESS,
                       chan=DEFAULT_MUX_CHANNEL,
                       config=DEFAULT_CONFIG,
                       debug=False):
        # Instantiate a smbus object.
        self.mux = objMux
        self.sensorAddr = sAddr
        self.channel = chan
        self.debugMode = debug

        # Initialize INA260 sensor.  
        initData = [(config >> 8), (config & 0x00FF)]
        self.mux.write_i2c_block_data(self.channel, self.sensorAddr, \
                CONFIG_REG, initData)

        if self.debugMode:
            data = self.getInfo()
            print(self)
            print("manufacturer ID: %s %s\n"\
                  "INA260 configuration register: %s %s\n" % data)
    ## end def

    def getInfo(self):
        # Read manufacture identification data.
        mfcid = self.mux.read_i2c_block_data(self.channel, \
                self.sensorAddr, ID_REG, 2)
        mfcidB1 = format(mfcid[0], "08b")
        mfcidB2 = format(mfcid[1], "08b")
        # Read configuration data.
        config = self.mux.read_i2c_block_data(self.channel, \
                 self.sensorAddr, CONFIG_REG, 2)
        configB1 = format(config[0], "08b")
        configB2 = format(config[1], "08b")
        return (mfcidB1, mfcidB2, configB1, configB2)
    ## end def

    def getCurrentReg(self):
        # Read current register and return raw binary data for test and
        # debug.
        data = self.mux.read_i2c_block_data(self.channel, \
               self.sensorAddr, CUR_REG, 2)
        dataB1 = format(data[0], "08b")
        dataB2 = format(data[1], "08b")
        return (dataB1, dataB2)
    ## end def

    def getCurrent(self):
        # Get the current data from the sensor.
        # INA260 returns the data in two bytes formatted as follows
        #        -------------------------------------------------
        #    bit |  7  |  6  |  5  |  4  |  3  |  2  |  1  |  0  |
        #        -------------------------------------------------
        # byte 1 | d15 | d14 | d13 | d12 | d11 | d10 | d9  | d8  |
        #        -------------------------------------------------
        # byte 2 | d7  | d6  | d5  | d4  | d3  |  d2 | d1  | d0  |
        #        -------------------------------------------------
        # The current is returned in d15-d0, a two's complement,
        # 16 bit number.  This means that d15 is the sign bit.        
        data=self.mux.read_i2c_block_data(self.channel, \
             self.sensorAddr, CUR_REG, 2)

        if self.debugMode:
            dataB1 = format(data[0], "08b")
            dataB2 = format(data[1], "08b")
            print("current register: %s %s" % (dataB1, dataB2))

        # Format into a 16 bit word.
        bdata = data[0] << 8 | data[1]
        # Convert from two's complement to integer.
        # If d15 is 1, the the number is a negative two's complement
        # number.  The absolute value is 2^16 - 1 minus the value
        # of d15-d0 taken as a positive number.
        if bdata > 0x7FFF:
            bdata = -(0xFFFF - bdata) # 0xFFFF equals 2^16 - 1
        # Convert integer data to mAmps.
        mAmps = bdata * 1.25  # LSB is 1.25 mA
        return mAmps
    ## end def

    def getVoltageReg(self):
        # Read voltage register and return raw binary data for test
        # and debug.
        data = self.mux.read_i2c_block_data(self.sensorAddr, VOLT_REG, 2)
        dataB1 = format(data[0], "08b")
        dataB2 = format(data[1], "08b")
        return (dataB1, dataB2)
    ## end def

    def getVoltage(self):
        # Get the voltage data from the sensor.
        # INA260 returns the data in two bytes formatted as follows
        #        -------------------------------------------------
        #    bit |  7  |  6  |  5  |  4  |  3  |  2  |  1  |  0  |
        #        -------------------------------------------------
        # byte 1 | d15 | d14 | d13 | d12 | d11 | d10 | d9  | d8  |
        #        -------------------------------------------------
        # byte 2 | d7  | d6  | d5  | d4  | d3  |  d2 | d1  | d0  |
        #        -------------------------------------------------
        # The voltage is returned in d15-d0 as an unsigned integer.
        data=self.mux.read_i2c_block_data(self.channel, \
             self.sensorAddr, VOLT_REG, 2)

        if self.debugMode:
            dataB1 = format(data[0], "08b")
            dataB2 = format(data[1], "08b")
            print("voltage register: %s %s" % (dataB1, dataB2))

        # Convert data to volts.
        volts = (data[0] << 8 | data[1]) * 0.00125 # LSB is 1.25 mV
        return volts
    ## end def

    def getPower(self):
        # Get the wattage data from the sensor.
        # INA260 returns the data in two bytes formatted as follows
        #        -------------------------------------------------
        #    bit | 7   |  6  |  5  |  4  |  3  |  2  |  1  |  0  |
        #        -------------------------------------------------
        # byte 1 | d15 | d14 | d13 | d12 | d11 | d10 | d9  | d8  |
        #        -------------------------------------------------
        # byte 2 | d7  | d6  | d5  | d4  | d3  |  d2 | d1  | d0  |
        #        -------------------------------------------------
        # The wattage is returned in d15-d0 as an unsigned integer.
        data=self.mux.read_i2c_block_data(self.channel, \
                self.sensorAddr, PWR_REG, 2)

        if self.debugMode:
            dataB1 = format(data[0], "08b")
            dataB2 = format(data[1], "08b")
            print("power register: %s %s" % (dataB1, dataB2))

        # Convert data to milliWatts. 
        mW = (data[0] << 8 | data[1]) * 10.0  # LSB is 10.0 mW
        return mW
   ## end def
## end class

def test():
    import time

    # Initialize the smbus and INA260 sensor.
    mux = i2cmux.i2cmux()
    pwr1 = ina260(mux, sAddr=0x40, chan=0, debug=True)
    # Print out sensor values.
    while True:
        print("%6.2f mA" % pwr1.getCurrent())
        print("%6.2f V" % pwr1.getVoltage())
        print("%6.2f mW\n" % pwr1.getPower())
        time.sleep(2)
## end def

if __name__ == '__main__':
    test()