Raspberry Pi – Bus I2C y Sensor BME 680

En este caso usaremos el sensor ambiental de Bosch BME680

Para ello usaremos la placa que montamos hace unos días que al final poco a poco será una estación meteorológica con conexión a nuestra domótica vía MQTT

El esquema es el siguiente ( de momento no usaremos la pantalla OLED )

En esta ocasión empezaremos a trabajar con el bus I2C , para ello colocaremos dos resistencias de 33K de pull-up en la líneas de SDA y SCL

Lo primero es activar el bus I2C con raspi-config

learn_raspberry_pi_advancedopt.png

learn_raspberry_pi_i2c.png

learn_raspberry_pi_wouldyoukindly.png

learn_raspberry_pi_i2ckernel.png

Instalaremos i2cdetect que esta en el paquete i2c-tools

apt-get install i2c-tools

Comprobaremos que nos detecta el dispositivo en el bus I2C

sudo i2cdetect -y 1

En este caso ha detectado un dispositivo en la dirección 0x77 , que si miramos aqui el manual veremos que es la dirección por defecto.

By default, the i2c address is 0x77. If you add a jumper from SDO to GND, the address will change to 0x76

Para probar la respuesta podemos usar

i2cget -y 1 0x77 0x00

Nos devolverá el valor de esa posición

En la raiz del proyecto ejecutar

dotnet tool install --global dotnet-ssh

Y para configurar seguir los pasos que nos va preguntando

dotnet-ssh configure MyRaspberryPi --auth-type UserSecrets --auth usuario:contraseña

El contenido del fichero Program.cs seria el siguiente

using System;
using System.Device.Gpio;
using System.Threading;
using Iot.Device.Buzzer;
using System.Device.I2c;
using Iot.Device.Bmxx80;
using Iot.Device.Common;
using UnitsNet;
using Iot.Device.Bmxx80.PowerMode;
using System.Timers;
using System.Threading.Tasks;
using System.IO;
using System.Runtime.InteropServices;

namespace Test_BME680
{

    class RPi
    {
        private static System.Timers.Timer timer_lecturas;
        static void Main(string[] args)
        {

            //Codigo Raspberry pi 
            //Definicion de I/O
            int Led_Rojo = 26;
            int Led_Verde = 6;
            int Led_Amarillo = 5;

            int pulsador_1 = 17;
            int pulsador_2 = 27;
            int pulsador_3 = 22;

            int pin_buzzer = 23;

            GpioController controller = new GpioController();
            Buzzer buzzer;

            //Timer lecturas cada 30 segundos
            timer_lecturas = new System.Timers.Timer(30000);
            timer_lecturas.Elapsed += evento_timer_lecturas;
            timer_lecturas.AutoReset = true;
            timer_lecturas.Enabled = true;


            //Configuracion
            bool Es_Windows = false;
            string Dir_Logs = "logs";
            string Dir_Config = "config";
            string Path_logs;
            string Path_config;
            string cVersion = "1.0";
            string sTituloVentana = "Test BME680 RPi";

            ////I2C
            const int busId = 1;

            I2cConnectionSettings i2cSettings = new I2cConnectionSettings(busId, 0x77);
            I2cDevice i2cDevice = I2cDevice.Create(i2cSettings);

            Globales.bme680 = new Bme680(i2cDevice, Temperature.FromDegreesCelsius(20.0));
            Globales.bme680.SetPowerMode(Bme680PowerMode.Forced);


            // reset will change settings back to default
            Globales.bme680.Reset();

            //Modo de trabajo I/O
            controller.OpenPin(Led_Verde, PinMode.Output);
            controller.OpenPin(Led_Amarillo, PinMode.Output);
            controller.OpenPin(Led_Rojo, PinMode.Output);

            controller.OpenPin(pulsador_1, PinMode.InputPullUp);
            controller.OpenPin(pulsador_2, PinMode.InputPullUp);
            controller.OpenPin(pulsador_3, PinMode.InputPullUp);

            controller.OpenPin(pin_buzzer, PinMode.Output);
            buzzer = new Buzzer(pin_buzzer);


            controller.Write(Led_Verde, PinValue.Low);
            controller.Write(Led_Amarillo, PinValue.Low);
            controller.Write(Led_Rojo, PinValue.Low);

            Es_Windows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
            Configuracion();

            // Borrado de trazas
            borra_trazas_antiguas();
            Escribe_Traza("Borrando ficheros anteriores a un mes ..... ");
            Escribe_Traza("Aplicacion iniciada  ..... , versión : " + cVersion);

            Console.Title = sTituloVentana;
            if (Es_Windows == true)
            {
                Escribe_Traza("Ejecutandose en entorno Windows");
            }
            else
            {
                Escribe_Traza("Ejecutandose en entorno Linux");
            }

            var localZone = TimeZone.CurrentTimeZone;
            var ahora = DateTime.Now;
            Escribe_Traza("Standard time name: " + localZone.StandardName);
            Escribe_Traza("Daylight saving time name: " + localZone.DaylightName);

            Globales.bme680.Reset();

            lecturaAsync();

            while (true)
            {
                Thread.Sleep(2500);
            }


            static async Task lecturaAsync()
            {

                Console.WriteLine("################################################################");
                Escribe_Consola("Lectura", ConsoleColor.Yellow);

                //Aumentamos la resolución
                Globales.bme680.TemperatureSampling = Sampling.HighResolution;
                Globales.bme680.HumiditySampling = Sampling.HighResolution;
                Globales.bme680.PressureSampling = Sampling.HighResolution;
                Globales.bme680.GasConversionIsEnabled = true;
                Globales.bme680.HeaterIsEnabled = true;
                Globales.bme680.ConfigureHeatingProfile(Bme680HeaterProfile.Profile2, Temperature.FromDegreesCelsius(320), Duration.FromMilliseconds(150), Temperature.FromDegreesCelsius(21));
                Globales.bme680.HeaterProfile = Bme680HeaterProfile.Profile2;

                Thread.Sleep(1000);

                //Leemos los datos
                var readResult = Globales.bme680.Read();

                Globales.resistencia = Convert.ToSingle(readResult.GasResistance?.Ohms);
                Globales.temperatura = Convert.ToSingle(readResult.Temperature?.Value);
                Globales.presion = Convert.ToSingle(readResult.Pressure?.Value) / 100;
                Globales.humedad = Convert.ToSingle(readResult.Humidity?.Value);

                if (readResult.Temperature.HasValue && readResult.Pressure.HasValue)
                {
                    Globales.altitud = Convert.ToSingle(WeatherHelper.CalculateAltitude(readResult.Pressure.Value, Globales.defaultSeaLevelPressure, readResult.Temperature.Value).Meters);
                }

                if (readResult.Temperature.HasValue && readResult.Humidity.HasValue)
                {
                    Globales.sensacion_calor = Convert.ToSingle(WeatherHelper.CalculateHeatIndex(readResult.Temperature.Value, readResult.Humidity.Value).DegreesCelsius);
                    Globales.punto_rocio = Convert.ToSingle(WeatherHelper.CalculateDewPoint(readResult.Temperature.Value, readResult.Humidity.Value).DegreesCelsius);
                }

                Console.WriteLine("################################################################");
                Console.WriteLine($"Resistencia gas: {Globales.resistencia:0.##} Ohmios");
                Console.WriteLine($"Temperatura: {Globales.temperatura:0.#} \u00B0C");
                Console.WriteLine($"Presión: {Globales.presion:0.##} mbs.");
                Console.WriteLine($"Humedad relativa: {Globales.humedad:0.#} %");
                Console.WriteLine($"Altitud: {Globales.altitud:0.##} mts.");
                Console.WriteLine($"Sensación calor: {Globales.sensacion_calor:0.#} \u00B0C");
                Console.WriteLine($"Punto rocio: {Globales.punto_rocio:0.#} \u00B0C");

                Console.WriteLine("################################################################");
                Console.WriteLine("");
            }

            void evento_timer_lecturas(Object source, ElapsedEventArgs e)
            {
                Globales.hora_lectura = e.SignalTime;
                controller.Write(Led_Rojo, PinValue.High);

                lecturaAsync();

                controller.Write(Led_Rojo, PinValue.Low);
            }


            //#################################################################################################
            // Codigo generico
            //#################################################################################################


            void Escribe_ambas_trazas(string cCadena, bool lConsola = true, ConsoleColor Color_Texto = ConsoleColor.White)
            {
                Escribe_Traza(cCadena, lConsola, Color_Texto);
                Escribe_Traza_Errores(cCadena, lConsola, Color_Texto);
            }

            void Configuracion()
            {
                string cSalida = "";

                // Comprobamos que existe el directorio , si no lo creamos
                if (Es_Windows == true)
                {
                    Path_logs = System.AppDomain.CurrentDomain.BaseDirectory + Dir_Logs + @"\";
                    Path_config = System.AppDomain.CurrentDomain.BaseDirectory + Dir_Config + @"\";
                }
                else
                {
                    Path_logs = System.AppDomain.CurrentDomain.BaseDirectory + Dir_Logs + "/";
                    Path_config = System.AppDomain.CurrentDomain.BaseDirectory + Dir_Config + "/";
                }

                Console.WriteLine("Ruta ficheros trazas : " + Path_logs);
                if (System.IO.Directory.Exists(Path_logs) == false)
                {
                    Console.WriteLine("No existe " + Path_logs + ", lo creamos ... ");
                    System.IO.Directory.CreateDirectory(Path_logs);
                }

                Console.WriteLine("Ruta ficheros configuracion : " + Path_config);
                if (System.IO.Directory.Exists(Path_config) == false)
                {
                    Console.WriteLine("No existe " + Path_config + ", lo creamos ... ");
                    System.IO.Directory.CreateDirectory(Path_config);
                }

            }

            static void Escribe_Consola(string cTexto, ConsoleColor Color = ConsoleColor.Green)
            {
                Console.OutputEncoding = System.Text.Encoding.Default;
                Console.ForegroundColor = ConsoleColor.Green;
                Console.Write(DateTime.Now.ToString("HH:mm:ss.fff") + " -> ");
                Console.ForegroundColor = Color;
                Console.WriteLine(cTexto);
                Console.ForegroundColor = ConsoleColor.Gray;
            }

            void Escribe_Traza_Errores(string cLinea, bool lConsola = true, ConsoleColor Color_Texto = ConsoleColor.Red)
            {
                string path = Path_logs + "Errores_" + DateTime.Now.ToString("dd-MM-yy") + ".txt";

                try
                {
                    StreamWriter sw = File.AppendText(path);

                    Escribe_Traza(cLinea, lConsola);

                    // cLinea = Format(Now, "HH:mm:ss.fff") & "-> " & cLinea

                    if (cLinea != null)
                    {
                        sw.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + " -> " + cLinea);
                        sw.Flush();
                        sw.Close();
                    }
                }
                catch
                {
                }

                if (lConsola == true)
                {
                    Console.OutputEncoding = System.Text.Encoding.Default;
                    Console.ForegroundColor = Color_Texto;
                    Console.Write(DateTime.Now.ToString("HH:mm:ss.fff") + " -> ");
                    Console.WriteLine("Error : " + cLinea);
                    Console.ResetColor();
                }

                System.Environment.Exit(0); /* TODO ERROR: Skipped SkippedTokensTrivia */
            }

            void borra_trazas_antiguas()
            {

                // Trazas
                System.IO.DirectoryInfo Folder;
                DateTime fecha_borrado = DateTime.Now.AddMonths(-1);

                Folder = new System.IO.DirectoryInfo(Path_logs);

                // WINDOWS 
                // If Folder.FullName.StartsWith("C:\") = True Then
                if (Es_Windows == true)
                {
                    foreach (System.IO.FileInfo fichero in Folder.GetFiles("traza_*.txt", System.IO.SearchOption.AllDirectories))
                    {
                        if (fichero.LastWriteTime <= fecha_borrado)
                        {
                            try
                            {
                                File.Delete(fichero.FullName);
                                Console.WriteLine("Borrado del fichero : " + fichero.Name);
                            }
                            catch (Exception ex)
                            {
                                Console.WriteLine("ERROR al borrar el fichero : " + fichero.Name + " ," + ex.Message);
                            }
                        }
                    }
                    foreach (System.IO.FileInfo fichero in Folder.GetFiles("Errores_*.txt", System.IO.SearchOption.AllDirectories))
                    {
                        if (fichero.LastWriteTime <= fecha_borrado)
                        {
                            try
                            {
                                File.Delete(fichero.FullName);
                                Console.WriteLine("Borrado del fichero : " + fichero.Name);
                            }
                            catch (Exception ex)
                            {
                                Console.WriteLine("ERROR al borrar el fichero : " + fichero.Name + " , " + ex.Message);
                            }
                        }
                    }
                }
                else
                {
                    // LINUX y/o DOCKER
                    foreach (System.IO.FileInfo fichero in Folder.GetFiles("traza_*.txt", System.IO.SearchOption.AllDirectories))
                    {
                        if (fichero.LastWriteTime <= fecha_borrado)
                        {
                            try
                            {
                                File.Delete(fichero.FullName);
                                Console.WriteLine("Borrado del fichero : " + fichero.Name);
                            }
                            catch (Exception ex)
                            {
                                Console.WriteLine("ERROR al borrar el fichero : " + fichero.Name + " , " + ex.Message);
                            }
                        }
                    }
                    foreach (System.IO.FileInfo fichero in Folder.GetFiles("Errores_*.txt", System.IO.SearchOption.AllDirectories))
                    {
                        if (fichero.LastWriteTime <= fecha_borrado)
                        {
                            try
                            {
                                File.Delete(fichero.FullName);
                                Console.WriteLine("Borrado del fichero : " + fichero.Name);
                            }
                            catch (Exception ex)
                            {
                                Console.WriteLine("ERROR al borrar el fichero : " + fichero.Name + " , " + ex.Message);
                            }
                        }
                    }
                }
            }

            void Escribe_Traza(string cLinea, bool lConsola = true, ConsoleColor Color_Texto = ConsoleColor.White)
            {
                string path = Path_logs + "traza_" + DateTime.Now.ToString("dd-MM-yy") + ".txt";

                try
                {
                    StreamWriter sw = File.AppendText(path);

                    // cLinea = Format(Now, "HH:mm:ss.fff") & "-> " & cLinea

                    if (cLinea != null)
                    {
                        sw.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + " -> " + cLinea);
                        sw.Flush();
                        sw.Close();
                    }
                }
                catch
                {
                }

                if (lConsola == true)
                {
                    Console.OutputEncoding = System.Text.Encoding.Default;
                    Console.ForegroundColor = ConsoleColor.Green;
                    Console.Write(DateTime.Now.ToString("HH:mm:ss.fff") + " -> ");
                    Console.ForegroundColor = Color_Texto;
                    Console.WriteLine(cLinea);
                    Console.ForegroundColor = ConsoleColor.Gray;
                }
            }
        }

        static class Globales
        {
            public static Bme680 bme680;
            public static float temperatura = 0;
            public static float resistencia = 0;
            public static float presion = 0;
            public static float humedad = 0;
            public static float altitud = 0;
            public static float sensacion_calor = 0;
            public static float punto_rocio = 0;
            public static Pressure defaultSeaLevelPressure = WeatherHelper.MeanSeaLevel;
            public static DateTime hora_lectura = DateTime.Now;
        }


    }

}

Lo compilaremos y subiremos a nuestra Raspberry Pi

Y lo ejecutaremos con

dotnet /home/antonio/net/test_bme680/test_bme680.dll

Veremos como empieza a mostrarnos las lecturas cada 30 segundos.

Problemas surgidos durante la programación del dispositivo , perdí un par de horas bien buenas debido a que al ejecutar el código daba un error genérico 121 , al final después de buscar y probar era una supina tonteria , por lo visto los señores de Microsoft y a los señores de Bosch no tienen en mismo concepto de default address ,

Al final la solución esta en forzar la dirección del dispositivo

 I2cConnectionSettings i2cSettings = new I2cConnectionSettings(busId, 0x77);

ya que la asignada por defecto en Microsoft apunta entre Cuenca y Badajoz , aunque en el código de GitHub es correcto , en las librerías no lo es.

Con esto y un bizcocho ………….

BONUS :

Con este comando veremos la configuración actual del GPIO

raspi-gpio get
BANK0 (GPIO 0 to 27):
GPIO 0: level=1 fsel=0 func=INPUT pull=UP
GPIO 1: level=1 fsel=0 func=INPUT pull=UP
GPIO 2: level=1 fsel=4 alt=0 func=SDA1 pull=UP
GPIO 3: level=1 fsel=4 alt=0 func=SCL1 pull=UP
GPIO 4: level=1 fsel=0 func=INPUT pull=UP
GPIO 5: level=1 fsel=0 func=INPUT pull=UP
GPIO 6: level=1 fsel=0 func=INPUT pull=UP
GPIO 7: level=1 fsel=0 func=INPUT pull=UP
GPIO 8: level=1 fsel=0 func=INPUT pull=UP
GPIO 9: level=0 fsel=0 func=INPUT pull=DOWN
GPIO 10: level=0 fsel=0 func=INPUT pull=DOWN
GPIO 11: level=0 fsel=0 func=INPUT pull=DOWN
GPIO 12: level=0 fsel=0 func=INPUT pull=DOWN
GPIO 13: level=0 fsel=0 func=INPUT pull=DOWN
GPIO 14: level=1 fsel=0 func=INPUT pull=NONE
GPIO 15: level=1 fsel=0 func=INPUT pull=UP
GPIO 16: level=0 fsel=0 func=INPUT pull=DOWN
GPIO 17: level=1 fsel=0 func=INPUT pull=DOWN
GPIO 18: level=0 fsel=0 func=INPUT pull=DOWN
GPIO 19: level=0 fsel=0 func=INPUT pull=DOWN
GPIO 20: level=1 fsel=0 func=INPUT pull=UP
GPIO 21: level=0 fsel=0 func=INPUT pull=DOWN
GPIO 22: level=1 fsel=0 func=INPUT pull=DOWN
GPIO 23: level=0 fsel=0 func=INPUT pull=DOWN
GPIO 24: level=0 fsel=0 func=INPUT pull=DOWN
GPIO 25: level=0 fsel=0 func=INPUT pull=DOWN
GPIO 26: level=0 fsel=0 func=INPUT pull=DOWN
GPIO 27: level=1 fsel=0 func=INPUT pull=DOWN
BANK1 (GPIO 28 to 45):
GPIO 28: level=1 fsel=2 alt=5 func=RGMII_MDIO pull=UP
GPIO 29: level=0 fsel=2 alt=5 func=RGMII_MDC pull=DOWN
GPIO 30: level=0 fsel=7 alt=3 func=CTS0 pull=UP
GPIO 31: level=0 fsel=7 alt=3 func=RTS0 pull=NONE
GPIO 32: level=1 fsel=7 alt=3 func=TXD0 pull=NONE
GPIO 33: level=1 fsel=7 alt=3 func=RXD0 pull=UP
GPIO 34: level=1 fsel=7 alt=3 func=SD1_CLK pull=NONE
GPIO 35: level=1 fsel=7 alt=3 func=SD1_CMD pull=UP
GPIO 36: level=1 fsel=7 alt=3 func=SD1_DAT0 pull=UP
GPIO 37: level=1 fsel=7 alt=3 func=SD1_DAT1 pull=UP
GPIO 38: level=1 fsel=7 alt=3 func=SD1_DAT2 pull=UP
GPIO 39: level=1 fsel=7 alt=3 func=SD1_DAT3 pull=UP
GPIO 40: level=0 fsel=4 alt=0 func=PWM1_0 pull=NONE
GPIO 41: level=0 fsel=4 alt=0 func=PWM1_1 pull=NONE
GPIO 42: level=0 fsel=1 func=OUTPUT pull=UP
GPIO 43: level=1 fsel=0 func=INPUT pull=UP
GPIO 44: level=1 fsel=0 func=INPUT pull=UP
GPIO 45: level=1 fsel=0 func=INPUT pull=UP
BANK2 (GPIO 46 to 53):
GPIO 46: level=0 fsel=0 func=INPUT pull=UP
GPIO 47: level=0 fsel=0 func=INPUT pull=UP
GPIO 48: level=0 fsel=0 func=INPUT pull=DOWN
GPIO 49: level=0 fsel=0 func=INPUT pull=DOWN
GPIO 50: level=0 fsel=0 func=INPUT pull=DOWN
GPIO 51: level=0 fsel=0 func=INPUT pull=DOWN
GPIO 52: level=0 fsel=0 func=INPUT pull=DOWN
GPIO 53: level=0 fsel=0 func=INPUT pull=DOWN

Si queremos cambiar la configuración del bus I2C lo haremos en el fichero config.txt

 nano  /boot/config.txt

Habilitaremos estas estas líneas y añadiremos las que falten

# Uncomment some or all of these to enable the optional hardware interfaces
#dtparam=i2c_arm=on
dtparam=i2s=on
dtparam=spi=on

# *** I2C ***
# *** Changingspeed***
# dtparam=i2c_arm=on,i2c_arm_baudrate=50000
# dtparam=i2c_arm=on,i2c_arm_baudrate=100000
# dtparam=i2c_arm=on,i2c_arm_baudrate=400000
# dtparam=i2c_arm=on,i2c_arm_baudrate=1000000


# *** Configuring  I2 buses ***
dtoverlay=i2c-gpio,i2c_gpio_sda=2,i2c_gpio_scl=3,i2c_gpio_delay_us=2,bus=1