#########################################################################################
# Package        HiPi::Interface::BME280
# Description  : Interface to BME280 Temperature, Humidity & Pressure Sensor
# Copyright    : Copyright (c) 2020-2023 Mark Dootson
# License      : This is free software; you can redistribute it and/or modify it under
#                the same terms as the Perl 5 programming language system itself.
#########################################################################################

package HiPi::Interface::BME280;

#########################################################################################

use strict;
use warnings;
use parent qw( HiPi::Interface::Common::Weather );
use HiPi qw( :i2c :rpi :bme280 );
use HiPi::RaspberryPi;
use Carp;
use Try::Tiny;

our $VERSION ='0.89';

__PACKAGE__->create_accessors( qw( backend _id _compensation repeat_oneshot ) );

use constant {
    BMP280 => BM280_TYPE_BMP280,
    BME280 => BM280_TYPE_BME280,
};

sub new {
    my ($class, %userparams) = @_;
    my $pi = HiPi::RaspberryPi->new();
    
    my %params = (
        devicename      => ( $pi->board_type == RPI_BOARD_TYPE_1 ) ? '/dev/i2c-0' : '/dev/i2c-1',
        address         => 0x76,
        device          => undef,
        backend         => 'smbus',
        repeat_oneshot  => 0,
    );
    
    # get user params
    foreach my $key( keys (%userparams) ) {
        $params{$key} = $userparams{$key};
    }
    
    if( $params{busmode} ) {
        $params{backend} = $params{busmode};
    }
    
    unless( defined($params{device}) ) {
        if ( $params{backend} eq 'bcm2835' ) {
            require HiPi::BCM2835::I2C;
            $params{device} = HiPi::BCM2835::I2C->new(
                address    => $params{address},
                peripheral => ( $params{devicename} eq '/dev/i2c-0' ) ? HiPi::BCM2835::I2C::BB_I2C_PERI_0() : HiPi::BCM2835::I2C::BB_I2C_PERI_1(),
            );
        } else {
            require HiPi::Device::I2C;
            $params{device} = HiPi::Device::I2C->new(
                devicename  => $params{devicename},
                address     => $params{address},
                busmode     => $params{backend},
            );
        }
    }
    
    my $self = $class->SUPER::new(%params);
    
    # wait for config to be read
    while ( $self->get_status->{im_update} ) {
        $self->sleep_millisesconds( 10 );
    }
    
    return $self;
}

sub sensor_id {
    my $self= shift;
    # BME280 is 96 == 0x60
    # BMP280 is 88, ( 87 or 86 for samples ) == 0x58 ( 0x57, 0x56 )
    return $self->_id if $self->_id;
    my ( $id ) = $self->device->bus_read( BM280_REG_ID );
    if ( $id == 0x58 || $id == 0x57 || $id == 0x56 ) {
        $id = BMP280;
    } elsif( $id == 0x60 ) {
        $id = BME280
    } else {
        croak sprintf('Unexpected sensor id 0x%X', $id);
    }
    $self->_id( $id );
    return $self->_id;
}

sub get_status {
    my $self = shift;
    my( $statusreg  ) = $self->device->bus_read( BM280_REG_STATUS, 1 );
    my $status = {
        measuring => ( $statusreg & 0b1000 ) >> 3,
        im_update => $statusreg & 0b01,
    };
    return $status;
}

sub reset {
    my $self = shift;
    $self->device->bus_write( BM280_REG_RESET, BM280_VAL_RESET );
    $self->sleep_milliseconds( 3 );
    return;
}

sub compensation {
    my $self = shift;
    return $self->_compensation if $self->_compensation;
    
    my $little_endian = 1;
    
    my @comp = ( 0 ) x 18;
    my $is_bme280 = ( $self->sensor_id == BME280 ) ? 1 : 0;
    my @bytes = $self->device->bus_read(BM280_REG_CALIB1, 26 );
    
    my @unsignedvals = ( BM280_COMP_DIG_T1, BM280_COMP_DIG_P1 );
    for my $item ( @unsignedvals ) {
        my $lsb = $item * 2;
        my $msb = $lsb + 1;
        $comp[$item] = HiPi->bytes_to_integer( [ $bytes[$lsb], $bytes[$msb] ], 0, $little_endian);
    }
    
    my @signedvals = ( BM280_COMP_DIG_T2, BM280_COMP_DIG_T3, BM280_COMP_DIG_P2, BM280_COMP_DIG_P3, BM280_COMP_DIG_P4,
                       BM280_COMP_DIG_P5, BM280_COMP_DIG_P6, BM280_COMP_DIG_P7, BM280_COMP_DIG_P8, BM280_COMP_DIG_P9 );
    
    for my $item ( @signedvals ) {
        my $lsb = $item * 2;
        my $msb = $lsb + 1;       
        $comp[$item] = HiPi->bytes_to_integer( [ $bytes[$lsb], $bytes[$msb] ], 1, $little_endian);
    }
    
    if ( $is_bme280 ) {
        # BM280_COMP_DIG_H1 => 12,  unsigned char
        
        $comp[BM280_COMP_DIG_H1] = HiPi->bytes_to_integer( [ $bytes[25] ], 0);
        
        @bytes = $self->device->bus_read(BM280_REG_CALIB2, 7 );
        
        # BM280_COMP_DIG_H2 => 13, signed short
        
        $comp[BM280_COMP_DIG_H2] = HiPi->bytes_to_integer( [ $bytes[0], $bytes[1] ], 1, $little_endian);
        
        # BM280_COMP_DIG_H3 => 14, unsigned char
        $comp[BM280_COMP_DIG_H3] = HiPi->bytes_to_integer( [ $bytes[2] ], 0);
        
        # BM280_COMP_DIG_H4 => 15, 12 bit signed short 
        {
            my $msb = $bytes[3];
            my $lsb = ( $bytes[4] << 4 ) & 0xF0;
            my $valX16 = HiPi->bytes_to_integer( [ $lsb, $msb ], 1, $little_endian);
            $comp[BM280_COMP_DIG_H4] = int($valX16 / 16);
        }
        
        # BM280_COMP_DIG_H5 => 16, 12 bit signed short 
        {
            my $msb = $bytes[5];
            my $lsb = $bytes[4] & 0xF0;
            my $valX16 = HiPi->bytes_to_integer( [ $lsb, $msb ], 1, $little_endian);
            $comp[BM280_COMP_DIG_H5] = int($valX16 / 16);
        }
        
        # BM280_COMP_DIG_H6 => 17, signed char
                
        $comp[BM280_COMP_DIG_H6] = HiPi->bytes_to_integer( [ $bytes[6] ], 1);
    }
    
    $self->_compensation( \@comp );
    return $self->_compensation;
    
    # My test sensor values are
    #Comp T1 = 28432
    #Comp T2 = 26627
    #Comp T3 = 50
    #Comp P1 = 37015
    #Comp P2 = -10620
    #Comp P3 = 3024
    #Comp P4 = 7701
    #Comp P5 = -138
    #Comp P6 = -7
    #Comp P7 = 12300
    #Comp P8 = -12000
    #Comp P9 = 5000
    #Comp H1 = 75
    #Comp H2 = 341
    #Comp H3 = 0
    #Comp H4 = 371
    #Comp H5 = 50
    #Comp H6 = 30
}

sub set_config_preset {
    my( $self, $preset ) = @_;
    
    my $result = 0;
    
    my $presets = {
        
        normal => {
            osrs_h => BM280_OSRS_X2,
            osrs_t => BM280_OSRS_X2,
            osrs_p => BM280_OSRS_X16,
            mode   => BM280_MODE_NORMAL,
            t_sb   => BM280_STANDBY_125,
            filter => BM280_FILTER_OFF,
        },
        
        oneshot => {
            osrs_h => BM280_OSRS_X1,
            osrs_t => BM280_OSRS_X1,
            osrs_p => BM280_OSRS_X1,
            mode   => BM280_MODE_FORCED,
            t_sb   => BM280_STANDBY_125,
            filter => BM280_FILTER_OFF,
        },
        
        filter => {
            osrs_h => BM280_OSRS_X2,
            osrs_t => BM280_OSRS_X2,
            osrs_p => BM280_OSRS_X16,
            mode   => BM280_MODE_NORMAL,
            t_sb   => BM280_STANDBY_125,
            filter => BM280_FILTER_16,
        },
    };
    
    if ( exists($presets->{$preset}) ) {
        $self->set_config( $presets->{$preset} );
    }
    
    return $result;
}

sub set_config {
    my ($self, $config) = @_;
    
    # config members
    #  osrs_h
    #  osrs_t
    #  osrs_p
    #  mode
    #  t_sb
    #  filter
    
    # write BM280_REG_CTRL_MEAS
    
    # write BM280_REG_CONFIG
    
    # set chip into sleep mode
    $self->device->bus_write(BM280_REG_CTRL_MEAS, 0);
    
    # write BM280_REG_CTRL_HUM
    if( $self->sensor_id == BME280 ) {
        my $osrs_h = $config->{osrs_h} || 0;
        $self->device->bus_write(BM280_REG_CTRL_HUM, $osrs_h & 0b111);
    }
    
    # write BM280_REG_CONFIG
    {
        my $t_sb = $config->{t_sb} || 0;
        my $filter = $config->{filter} || 0;
        my $val = (( $t_sb & 0b111 ) << 5) + (( $filter & 0b111 ) << 2);
        $self->device->bus_write(BM280_REG_CONFIG, $val & 0b11111100);
    }
    
    # write BM280_REG_CTRL_MEAS
    {
        my $osrs_t = $config->{osrs_t} || 0;
        my $osrs_p = $config->{osrs_p} || 0;
        my $mode   = $config->{mode}   || 0;
        my $val = (( $osrs_t & 0b111 ) << 5) + (( $osrs_p & 0b111 ) << 2) + ( $mode & 0b11) ;
        $self->device->bus_write(BM280_REG_CTRL_MEAS, $val & 0b11111111);
    }
    
    $self->sleep_milliseconds( 100 );
    
    return 1;
}

sub get_config {
    my $self = shift;
    
    my $config = {};
    
    if( $self->sensor_id == BME280 ) {
        # BM280_REG_CTRL_HUM
        my ( $ctrl_hum ) = $self->device->bus_read(BM280_REG_CTRL_HUM, 1);
        $config->{osrs_h} = $ctrl_hum & 0b111;
    }
    
    
    # BM280_REG_CTRL_MEAS && BM280_REG_CONFIG
    my ( $ctrl_meas, $chip_conf )  = $self->device->bus_read(BM280_REG_CTRL_MEAS, 2);
    $config->{osrs_t} = ( $ctrl_meas >> 5) & 0b111;
    $config->{osrs_p} = ( $ctrl_meas >> 2) & 0b111;
    $config->{mode}   = $ctrl_meas & 0b11;
    $config->{t_sb}   = ( $chip_conf >> 5 ) & 0b111;
    $config->{filter} = ( $chip_conf >> 2 ) & 0b111;
    
    return $config;
}

sub get_values {
    my ( $self ) = @_;
    my $v = $self->get_value_hash();
    return ( $v->{t}, $v->{p}, $v->{h} );
};

sub get_value_hash {
    my ( $self ) = @_;
    
    my $is_bme280 = ( $self->sensor_id == BME280 ) ? 1 : 0;
    
    my $result = $self->get_raw_value_hash();
    my $cmp = $self->compensation;
    my $t_fine;
        
    # TEMPERATURE
    try {
        my $var1 = ( $result->{raw_t} / 16384.0  - $cmp->[BM280_COMP_DIG_T1] / 1024.0 ) * $cmp->[BM280_COMP_DIG_T2]; 
        my $var2 = ( ($result->{raw_t} / 131072.0 - $cmp->[BM280_COMP_DIG_T1] / 8192.0) *
                     ($result->{raw_t} / 131072.0 - $cmp->[BM280_COMP_DIG_T1] / 8192.0) ) * $cmp->[BM280_COMP_DIG_T3];
        $t_fine = ( $var1 + $var2 ) * 1.0;
        $result->{t} = sprintf('%.2f', ( $var1 + $var2 ) / 5120.0 );
    } catch {
        carp 'error calculating compensated temperature : ' . $_;   
    };
    
    # PRESSURE    
    try {
        my $var1 = $t_fine / 2.0 - 64000.0;
        my $var2 = $var1 * $var1 * $cmp->[BM280_COMP_DIG_P6] / 32768.0;
        $var2 = $var2 + $var1 * $cmp->[BM280_COMP_DIG_P5] * 2.0;
        $var2 = $var2 / 4.0 + $cmp->[BM280_COMP_DIG_P4] * 65536.0;
        $var1 = (($cmp->[BM280_COMP_DIG_P3] * $var1 * $var1 / 524288.0 + $cmp->[BM280_COMP_DIG_P2] * $var1)) / 524288.0;
        $var1 = (1.0 + $var1 / 32768.0) * $cmp->[BM280_COMP_DIG_P1];

        # avoid div by zero
        return if( $var1 == 0 );
        
        my $pressure = 1048576.0 - $result->{raw_p};
        $pressure = (($pressure - $var2 / 4096.0) * 6250.0) / $var1;
        $var1 = $cmp->[BM280_COMP_DIG_P9] * $pressure * $pressure / 2147483648.0;
        $var2 = $pressure * $cmp->[BM280_COMP_DIG_P8] / 32768.0;
        $pressure = $pressure + ($var1 + $var2 + $cmp->[BM280_COMP_DIG_P7]) / 16.0;
        $result->{p} = sprintf('%.2f', $pressure );
    } catch {
        carp 'error calculating compensated pressure : ' . $_;   
    };
    
    # HUMIDITY
    
    try {
        return unless $is_bme280;
        my $varH = $t_fine - 76800.0;
        $varH =  (( $result->{raw_h} * 1.0 ) - ($cmp->[BM280_COMP_DIG_H4] * 64.0 + $cmp->[BM280_COMP_DIG_H5] / 16384.0 *
                  $varH)) * ( $cmp->[BM280_COMP_DIG_H2] / 65536.0 * ( 1.0 + $cmp->[BM280_COMP_DIG_H6] /
                  67108864.0 * $varH *
                  ( 1.0 + $cmp->[BM280_COMP_DIG_H3] / 67108864.0 * $varH )));
        
        my $humidity = $varH * (1.0 - $cmp->[BM280_COMP_DIG_H1] * $varH / 524288.0);
        
        if ( $humidity > 100 ) {
            $humidity = 100;
        } elsif( $humidity < 0 ) {
            $humidity = 0;
        }
        
        $result->{h} = sprintf('%.2f', $humidity );
        
    } catch {
        carp 'error calculating compensated humidity : ' . $_;   
    };
    
    return $result;
}

sub get_raw_value_hash {
    my ( $self  ) = @_;
    
    my $is_bme280 = ( $self->sensor_id == BME280 ) ? 1 : 0;
    
    if ( $self->repeat_oneshot ) {
        my $chipconfig = $self->get_config();
        $chipconfig->{mode} = BM280_MODE_FORCED;
        $self->set_config( $chipconfig );
    }
    
    my $counter = 5000;
    
    # wait till chip ready
    my $currentstatus = $self->get_status;
    while ( $counter > 0 && ( $currentstatus->{measuring} || $currentstatus->{im_update} ) ) {
        $self->sleep_milliseconds(1);
        $counter --;
        $currentstatus = $self->get_status;
    }
    
    my $rval = {};
    
    my $numbytes = $is_bme280 ? 8 : 6;
    my @values = $self->device->bus_read(BM280_REG_PRESS_MSB, $numbytes );
    
    $rval->{raw_p} = (( $values[0] & 0xFF ) << 12 ) + (( $values[1] & 0xFF ) << 4 ) + ( ( $values[2] & 0xF0 ) >> 4 );
    $rval->{raw_t} = (( $values[3] & 0xFF ) << 12 ) + (( $values[4] & 0xFF ) << 4 ) + ( ( $values[5] & 0xF0 ) >> 4 );
    
    if ( $is_bme280 ) {
        $rval->{raw_h} = (( $values[6] & 0xFF ) << 8 ) + ( $values[7] & 0xFF );
    }
    
    return $rval;
}

1;

__END__