BME280 Class (Exercise)

BME280.py class template
from machine import I2C, Pin
from struct import unpack
from time import sleep

# Define the following constants to access the registers in the BME280 chip
# Using the const expression saves memoy in the microcontroller
# The meaning of the various registers is explained in the data sheet.

BME_ADR             = const(0x77) # This is the I2C address of the chip

# The following are registers which you might need in the application.
# The first line is an example. Look up the register addresses in the address
# map and complete the lines for all addresses below:
BME_REG_CHIP_ID     = const(0xD0) # This register holds an identification code (0x60)
BME_REG_RESET       = 

# Registers to define details on how the measurements should be performed and
# registers to trigger the actual measurement. 
BME_REG_CTRL_HUM    =             # 5 : oversampling 16
BME_REG_STATUS      =            
BME_REG_CTRL_MEAS   =            
BME_REG_CTRL_CONFIG =            

# Registers which hold the result of a measurement:
BME_REG_PRESS       =             # adr F7 ... F9 are 20 bits pressure (big endian)
                                  # F9 bits 4..7 are the most significant 4 bits)
BME_REG_TEMP        =             # adr FA ... FC contains the 20 bits for temperature
BME_REG_HUM         =             # adr FD and FE contain 16 bits of Humidity


##########################################################################
class BME280 :
##########################################################################

    def __init__( self, i2c ):
        ''' The parameter i2c is an i2c micropython object which is
        created in the main programmed and passed to the constructor
        of this class to access the sensor. Since we do not want to
        hardcode the used pins in this class we let the main program
        create this i2c object'''
        self.i2c = i2c

        self.readCalib()
        self.initSensor()


    # Initialize the sensor for single measurements between sleeps.
    # The maximum amount of oversampling is requested to achieve maximal precision.
    def initSensor( self ):

        # Set the oversampling of the humidity measurement to 16.
        # The data sheet states that this has to be done before writing
        # to the CTRL_MEAS register.
        # Replace the ??? with the correct values.
        # Look at the micropython documentation to understand how to use the writeto_mem function:
        # https://docs.micropython.org/en/latest/library/machine.I2C.html
        # Pay particular attention to the parameter which defines the contents of the register:
        # It is a "byte" value. Hence you have to know how to write a "byte" constant in Python. 

        self.i2c.writeto_mem( ??? )  

        # Set the chip to sleep mode
        # Set the temerature and pressure oversampling to 16
        self.i2c.writeto_mem( ??? )




    # Calibration data needs to be read out of the chip (see below the 
    # function "readCalib"). What follows here is a set of helper functions
    # which read single calibration constants our of the memory of the 
    # sensor chip. The format of the the constants can be different
    # (signed or unsigned short (2 byte values) or signed or unsigned char
    # (one byte values)). There is a dedicated function for all of these four
    # types which converts the read bytes into the appropriate python type.
    #
    # The readfrom_mem function reads a number of bytes via i2c into a
    # python "bytes" object. Essentially this is an array of single bytes.
    # To turn a bytes object into a python number the struct.unpack function 
    # is used. It is documented in the python documentation:
    # https://docs.python.org/3.5/library/struct.html?highlight=unpack#struct.unpack
    # If you do not understand how the unpack works, please ask !!!
    #
    # Fill in the ???

    def _readSignedShort( self, adr ):
        tmp = self.i2c.readfrom_mem( BME_ADR, adr, 2 )
        return unpack('<h', tmp)[0]

    def _readUnsignedShort( self, adr ):
        tmp = self.i2c.readfrom_mem( ??? )
        return unpack(???)[0]

    def _readUnsignedChar( self, adr ):
        tmp = self.i2c.readfrom_mem(???)
        return unpack(???)[0]

    def _readSignedChar( self, adr ):
        tmp = self.i2c.readfrom_mem(???)
        return unpack(???)[0]

    # Read the calibration data which has been programmed into the chip.
    # Due to production tolerances not every sensor gives exactly the same
    # value at a given temperature/pressure/humidity. At the factory every
    # sensor is calibrated and the calibration constants are programmed
    # into the chip (they cannot be changed afterwards). We read out these
    # constants here, since we need them to calculate calibrated 
    # (i.e. 'correct') sensor values.
    #
    def readCalib( self ):
        calib={}
        calib['T1'] = self._readSignedShort( 0x88 )
        calib['T2'] = self._readSignedShort( 0x8A )
        calib['T3'] = self._readSignedShort( 0x8C )
        calib['P1'] = self._readUnsignedShort( 0x8E )
        calib['P2'] = self._readSignedShort( 0x90 )
        calib['P3'] = self._readSignedShort( 0x92 )
        calib['P4'] = self._readSignedShort( 0x94 )
        calib['P5'] = self._readSignedShort( 0x96 )
        calib['P6'] = self._readSignedShort( 0x98 )
        calib['P7'] = self._readSignedShort( 0x9A )
        calib['P8'] = self._readSignedShort( 0x9C )
        calib['P9'] = self._readSignedShort( 0x9E )
        calib['H1'] = self._readUnsignedChar( 0xA1 )
        calib['H2'] = self._readSignedShort( 0xE1 )
        calib['H3'] = self._readUnsignedChar( 0xE3 )

        # The following two constants need extra treatment.
        # For some (not obvious) reason, the chip producer decided 
        # to pack the following 2 values into three bytes of which
        # one of the bytes contains bits belonging to both fo the
        # constants. Hence some fiddling around with the bits is
        # needed in order to extract the two callibration values:

        tmp = self.i2c.readfrom_mem( BME_ADR, 0xE4, 2 )
        calib['H4'] = (int(tmp[0])<<4) + (int(tmp[1])&0xf)
        tmp = self.i2c.readfrom_mem( BME_ADR, 0xE5, 2 )
        calib['H5'] = (int(tmp[0]) & 0xF0 ) >> 4 + (int(tmp[1]) << 4 )

        calib['H6'] = self._readSignedChar( 0xE7 )

        self.calib = calib



    # The formulas of the following calculations come from the data sheet.
    # There is no intelligent work here, just translation from C to python.
    #
    # Calculate the Temperature with help of the calibration data

    def calcTemp( self, adc_T ):
        var1 = ( ( ( (adc_T>>3) - (self.calib['T1']<<1) ) * self.calib['T2'] ) ) >> 11
        var2 = (((((adc_T>>4) - (self.calib['T1'])) * ((adc_T>>4) - (self.calib['T1']))) >> 12) * (self.calib['T3'])) >> 14
        t_fine = var1 + var2
        T = (t_fine * 5 + 128) >> 8
        self.t_fine = t_fine
        return T

    # Calculate the pressure with help of the calibration data
    # This calculation includes a temperature correction.
    def calcPress( self, adc_P ):

        var1 = (self.t_fine >> 1) - 64000;
        var2 = (((var1 >> 2) * (var1 >> 2)) >> 11) * self.calib['P6']
        var2 = var2 + ((var1 * (self.calib['P5'])) << 1 )
        var2 = (var2 >> 2) + ((self.calib['P4']) << 16 )
        var1 = (((self.calib['P3'] * (((var1>>2) * (var1>>2) >> 13 )) >> 3) +
                 (( self.calib['P2']) * var1) >> 1)) >> 18
        var1 = ((((32768+var1)) * (self.calib['P1'])) >> 15) 

        if var1 == 0 :
            return 0

        p = ((((1048576)-adc_P)-(var2>>12)))*3125
        # convert to unsigned int:
        if p < 0:
            p = p + (1<<32)

        if p < 0x80000000:
            p = int((p<<1) / var1)
        else:
            p = ((p / var1) * 2)

        if p < 0:
            p = p + (1<<32)

        var1 = ((self.calib['P9'] * (((p>>3) * (p>>3)) >> 13 ))) >> 12
        var2 = (((p>>2)) * (self.calib['P8'])) >> 13
        p =  (p + ((var1 + var2 + self.calib['P7']) >> 4))
        return p/100.0

    # Calucalate the humidity with help of the calibration data
    # This calculation includes a temperature correction 
    def calcHum(self, adc_H):
        # The formulas below come from the Arduino library for this sensor.
        h = self.t_fine - 76800
        h = (((((adc_H << 14) - (self.calib['H4'] << 20) - (self.calib['H5'] * h)) + 16384) >> 15) *
             (((((((h * self.calib['H6']) >> 10) * (((h * self.calib['H3']) >> 11) + 32768)) >> 10) + 2097152) *
               self.calib['H2'] + 8192) >> 14))
        h = h - (((((h >> 15) * (h >> 15)) >> 7) * self.calib['H1']) >> 4)
        h = 0 if h < 0 else h
        h = 419430400 if h > 419430400 else h
        return h >> 12

    # Do the measurements of Temperature, Pressure and Humidity
    def doMeasure( self ):
        # Leave the oversampling values as defined in the init routine but wake up the chip into "forced mode".
        # This means the chip is exactly performing one measurement and then returns to sleep mode.
        self.i2c.writeto_mem( ??? )

        # Here we wait until the measurement is done.
        # The bit 3 of the register BME_REG_STATIS is '1' when a measurement is ongoing.
        # It goes to '0' once the measurement is completed.
        # Therefore the code below should wait until the bit 3 of the BME_REG_STATUS
        # goes to '0'
        # There are many ways to perform this task. You can try to just fill in the question
        # marks, but if you have another idea to program this you can change the lines below.

        measuring = ???
        while measuring == ???:
            sleep(0.1)
            measuring = ???


        # Now we read out the measurements. The values are raw values which need to
        # be transformed via formulas into Temperature, Pressure and Humidity values.
        # The formulas involve calibration constants. In addition the raw measurement
        # value depend on each other (i.e. the raw values for humidity and pressure
        # are temperature dependent. This dependency is known and worked into to the
        # forumalas for the calculation.

        # Pay attention to the length of the data to be read out (datasheet !!!)

        # read temperature
        temp = self.i2c.readfrom_mem( ??? )
        T = int(temp[0]<<12)+int(temp[1]<<4)+int(temp[2]>>4)
        self.lastT = self.calcTemp( T ) / 100.

        # read pressure
        press = self.i2c.readfrom_mem( ??? )
        P = ???
        self.lastP = ???

        # read humidity
        hum = self.i2c.readfrom_mem( ??? )
        H = ???
        self.lastH = ???

        return( self.lastT, self.lastP, self.lastH )


    def getAltitude( self ):
        # 1013.24 is the reference pressure at sealevel
        # This formula is an approximation, of course. But it is useful
        # to calculate altitude differences (e.g. in a model airplane).
        # You should be able to see altitude differences when holding the
        # Sensor at different heights (1-2 meters of difference should be
        # visible. To make it more obvious sample over multiple measurements
        # and take the mean

        return ( 44330 * ( 1.0 - (self.lastP / 1013.24 )**(1.0 / 5.255)) )


    def dumpLastMeasurement( self ):        
        print( "Temperature : %7.2f C"  % self.lastT )
        print( "Pressure    : %7.2f mb" % self.lastP )
        print( "Humidity    : %7.2f %%" % self.lastH )
        print()
#############################################################################