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
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