Mi frambuesa meteorológica

El siguiente código convierte una placa Raspberry PI Pico en un monitor meteorológico experimental con el uso del sensor BME280, que es capaz de medir temperatura ambiente, humedad relativa y presión atmosférica.
La placa ni bien se saca de su bolsita antiestática, si se la conecta por medio el cable USB, no muestra vida alguna. Para que quede lista para funcionar hay que actualizar el firmware, el cual se puede descargar desde aquí.
Para instalarlo, se presiona el botón BOOTSEL y luego se conecta al puerto usb de la computadora. El dispositivo se comportará como una memoria extraíble y aparecerá entre la lista de dispositivos.
La tarea es sencilla, desde la carpeta de Descargas bastará con arrastrar el archivo con extensión .uf2 hacia la unidad extraíble. Una vez completado el procedimiento el dispositivo se reiniciará.
Luego, desde el entorno de desarrollo Thonny Python, vamos al menú superior, en Ejecutar, elegimos Configurar intérprete. En el desplegable seleccionamos Micropython (Raspberry Pi Pico) y luego, debajo, buscamos el puerto USB. (En Windows, te aparecerán como puertos COM)
Si todo va bien, deberíamos ver por consola un mensaje como éste:
MicroPython v1.20.0 on 2023-04-26; Raspberry Pi Pico with RP2040
Type “help()” for more information.

Para que nuestro script de Micropython funcione correctamente, es necesario cargar la librería que controla el sensor ambiental, bastará con copiar el siguiente código y guardarlo cómo bme280.py en el módulo de la Pico. Archivo>Guardar cómo>Raspberry Pi Pico.
# Authors: Paul Cunnane 2016, Peter Dahlebrg 2016
#
# This module borrows from the Adafruit BME280 Python library. Original
# Copyright notices are reproduced below.
#
# Those libraries were written for the Raspberry Pi. This modification is
# intended for the MicroPython and esp8266 boards.
#
# Copyright (c) 2014 Adafruit Industries
# Author: Tony DiCola
#
# Based on the BMP280 driver with BME280 changes provided by
# David J Taylor, Edinburgh (www.satsignal.eu)
#
# Based on Adafruit_I2C.py created by Kevin Townsend.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import time
from ustruct import unpack, unpack_from
from array import array
# BME280 default address.
BME280_I2CADDR = 0x77
# Operating Modes
BME280_OSAMPLE_1 = 1
BME280_OSAMPLE_2 = 2
BME280_OSAMPLE_4 = 3
BME280_OSAMPLE_8 = 4
BME280_OSAMPLE_16 = 5
BME280_REGISTER_CONTROL_HUM = 0xF2
BME280_REGISTER_CONTROL = 0xF4
class BME280:
def __init__(self,
mode=BME280_OSAMPLE_1,
address=BME280_I2CADDR,
i2c=None,
**kwargs):
# Check that mode is valid.
if mode not in [BME280_OSAMPLE_1, BME280_OSAMPLE_2, BME280_OSAMPLE_4,
BME280_OSAMPLE_8, BME280_OSAMPLE_16]:
raise ValueError(
'Unexpected mode value {0}. Set mode to one of '
'BME280_ULTRALOWPOWER, BME280_STANDARD, BME280_HIGHRES, or '
'BME280_ULTRAHIGHRES'.format(mode))
self._mode = mode
self.address = address
if i2c is None:
raise ValueError('An I2C object is required.')
self.i2c = i2c
# load calibration data
dig_88_a1 = self.i2c.readfrom_mem(self.address, 0x88, 26)
dig_e1_e7 = self.i2c.readfrom_mem(self.address, 0xE1, 7)
self.dig_T1, self.dig_T2, self.dig_T3, self.dig_P1, \
self.dig_P2, self.dig_P3, self.dig_P4, self.dig_P5, \
self.dig_P6, self.dig_P7, self.dig_P8, self.dig_P9, \
_, self.dig_H1 = unpack("<HhhHhhhhhhhhBB", dig_88_a1)
self.dig_H2, self.dig_H3 = unpack("<hB", dig_e1_e7)
e4_sign = unpack_from("<b", dig_e1_e7, 3)[0]
self.dig_H4 = (e4_sign << 4) | (dig_e1_e7[4] & 0xF)
e6_sign = unpack_from("<b", dig_e1_e7, 5)[0]
self.dig_H5 = (e6_sign << 4) | (dig_e1_e7[4] >> 4)
self.dig_H6 = unpack_from("<b", dig_e1_e7, 6)[0]
self.i2c.writeto_mem(self.address, BME280_REGISTER_CONTROL,
bytearray([0x3F]))
self.t_fine = 0
# temporary data holders which stay allocated
self._l1_barray = bytearray(1)
self._l8_barray = bytearray(8)
self._l3_resultarray = array("i", [0, 0, 0])
def read_raw_data(self, result):
""" Reads the raw (uncompensated) data from the sensor.
Args:
result: array of length 3 or alike where the result will be
stored, in temperature, pressure, humidity order
Returns:
None
"""
self._l1_barray[0] = self._mode
self.i2c.writeto_mem(self.address, BME280_REGISTER_CONTROL_HUM,
self._l1_barray)
self._l1_barray[0] = self._mode << 5 | self._mode << 2 | 1
self.i2c.writeto_mem(self.address, BME280_REGISTER_CONTROL,
self._l1_barray)
sleep_time = 1250 + 2300 * (1 << self._mode)
sleep_time = sleep_time + 2300 * (1 << self._mode) + 575
sleep_time = sleep_time + 2300 * (1 << self._mode) + 575
time.sleep_us(sleep_time) # Wait the required time
# burst readout from 0xF7 to 0xFE, recommended by datasheet
self.i2c.readfrom_mem_into(self.address, 0xF7, self._l8_barray)
readout = self._l8_barray
# pressure(0xF7): ((msb << 16) | (lsb << 8) | xlsb) >> 4
raw_press = ((readout[0] << 16) | (readout[1] << 8) | readout[2]) >> 4
# temperature(0xFA): ((msb << 16) | (lsb << 8) | xlsb) >> 4
raw_temp = ((readout[3] << 16) | (readout[4] << 8) | readout[5]) >> 4
# humidity(0xFD): (msb << 8) | lsb
raw_hum = (readout[6] << 8) | readout[7]
result[0] = raw_temp
result[1] = raw_press
result[2] = raw_hum
def read_compensated_data(self, result=None):
""" Reads the data from the sensor and returns the compensated data.
Args:
result: array of length 3 or alike where the result will be
stored, in temperature, pressure, humidity order. You may use
this to read out the sensor without allocating heap memory
Returns:
array with temperature, pressure, humidity. Will be the one from
the result parameter if not None
"""
self.read_raw_data(self._l3_resultarray)
raw_temp, raw_press, raw_hum = self._l3_resultarray
# temperature
var1 = ((raw_temp >> 3) - (self.dig_T1 << 1)) * (self.dig_T2 >> 11)
var2 = (((((raw_temp >> 4) - self.dig_T1) *
((raw_temp >> 4) - self.dig_T1)) >> 12) * self.dig_T3) >> 14
self.t_fine = var1 + var2
temp = (self.t_fine * 5 + 128) >> 8
# pressure
var1 = self.t_fine - 128000
var2 = var1 * var1 * self.dig_P6
var2 = var2 + ((var1 * self.dig_P5) << 17)
var2 = var2 + (self.dig_P4 << 35)
var1 = (((var1 * var1 * self.dig_P3) >> 8) +
((var1 * self.dig_P2) << 12))
var1 = (((1 << 47) + var1) * self.dig_P1) >> 33
if var1 == 0:
pressure = 0
else:
p = 1048576 - raw_press
p = (((p << 31) - var2) * 3125) // var1
var1 = (self.dig_P9 * (p >> 13) * (p >> 13)) >> 25
var2 = (self.dig_P8 * p) >> 19
pressure = ((p + var1 + var2) >> 8) + (self.dig_P7 << 4)
# humidity
h = self.t_fine - 76800
h = (((((raw_hum << 14) - (self.dig_H4 << 20) -
(self.dig_H5 * h)) + 16384)
>> 15) * (((((((h * self.dig_H6) >> 10) *
(((h * self.dig_H3) >> 11) + 32768)) >> 10) +
2097152) * self.dig_H2 + 8192) >> 14))
h = h - (((((h >> 15) * (h >> 15)) >> 7) * self.dig_H1) >> 4)
h = 0 if h < 0 else h
h = 419430400 if h > 419430400 else h
humidity = h >> 12
if result:
result[0] = temp
result[1] = pressure
result[2] = humidity
return result
return array("i", (temp, pressure, humidity))
@property
def values(self):
""" human readable values """
t, p, h = self.read_compensated_data()
p = p // 256
pi = p // 100
pd = p - pi * 100
hi = h // 1024
hd = h * 100 // 1024 - hi * 100
return ("{}C".format(t / 100), "{}.{:02d}hPa".format(pi, pd),
"{}.{:02d}%".format(hi, hd))

Pinout


A continuación, el código que permite visualizar por la consola de Thonny los parámetros de temperatura, humedad relativa y presión atmosférica. Lo hace cada 5 segundos, en cada iteración se encenderá led del built-in de la placa.
El bucle while se desarrolla con un try que, ante una eventualidad, como una mala conexión del sensor, captura el error y lo muestra por consola, sin interrumpir el flujo del programa.
Así de manera, experimental , la salida de la consola debería mostrar algo así:
%Run -c $EDITOR_CONTENT
Temperatura: 32.0 °C
Presión: 996.94 hPa
Humedad: 46 %
# Importamos las librerías necesarias
import machine # Librería principal para interactuar con los pines de la Raspberry Pi Pico
from machine import I2C, Pin # Importamos I2C para comunicación y Pin para configurar los pines
import time # Para pausar el programa
# Importamos la clase BME280 desde el archivo bme280_float.py
from bme280_float import BME280
# ************************************************
# CONFIGURACIÓN INICIAL
# Dirección I2C del sensor BME280 (verificar que puede ser 0x76 o 0x77)
DIRECCION_BME280 = 0x77
# Configuración de los pines I2C
# SDA en GPIO18 y SCL en GPIO19
i2c = I2C(1, sda=Pin(18), scl=Pin(19))
# Creamos una instancia del sensor BME280
bme = BME280(i2c, addr=DIRECCION_BME280)
# Configuración del LED integrado (GPIO25)
led = Pin(25, Pin.OUT) # El LED integrado está conectado al pin GPIO25
# Función para hacer parpadear el LED
def parpadear_led():
led.on() # Encendemos el LED
time.sleep(0.5) # Esperamos 0.5 segundos
led.off() # Apagamos el LED
# Bucle principal
while True:
try:
# Obtenemos los valores del sensor: temperatura (°C), presión (hPa) y humedad relativa (%)
temp, pres, hum = bme.values()
# Mostramos los datos por consola
print(f"Temperatura: {temp:.1f} °C") # Temperatura con un decimal
print(f"Presión: {pres:.2f} hPa") # Presión con dos decimales
print(f"Humedad: {int(hum)} %") # Humedad relativa como valor entero
parpadear_led()
# Esperamos 5 segundos antes de leer nuevamente
time.sleep(5)
except Exception as e:
# Si hay algún error, lo mostramos en la consola
print(f"Error: {str(e)[:30]}")
El próximo paso es utilizar la hermana mayor de ésta placa, se trata de la Raspberry Pi Pico W, que ofrece conexión inalámbrica WIFI.
La gran ventaja es que podremos “sacar a tomar aire” la Pico y mostrar valores ambientales más reales.
Otra novedad interesante es que no veremos los datos por consola, vamos a hacer algo más divertido, el script será autónomo, no requerirá de una pc. Podremos alimentar nuestra placa con pilas. La idea es visualizar los datos a través de una página web, muy sencilla, que se alojará en la memoria de la misma placa.
¡Agradecimiento a Luisito, LW6DIO, que trabajó en para que el formato del código de la presente publicación quede prolijito… como jardín de jubilado ;)!.