Obtener la temperatura y humedad con el sensor DHT11, la Raspberry Pi, C y Java

Escrito por el , actualizado el .
gnu-linux java planeta-codigo programacion
Enlace permanente Comentarios

El kit de iniciación a la electrónica para la Raspberry Pi tiene un sensor para la temperatura y humedad, el modelo DHT11. Obtener la información requiere restricciones de tiempo, este es un caso de uso justificado para usar lenguaje C y JNI para integrarlo con Java. En el ejemplo el código C llama a un método de una clase Java con dos valores enteros o lanza una excepción en caso de que al realizar la lectura haya habido algún error en la transmisión de los bits.

Raspberry Pi

Java

Entre los varios sensores incluídos en el kit de iniciación para la Raspberry Pi está el sensor de temperatura y humedad con el modelo DHT11. El DHT11 es un sensor muy básico pero suficiente y válido si el objetivo es trastear un poco con hardware.

En la especificación del sensor DHT11 está descrita su funcionamiento y forma de comunicación. Utiliza 3 cables, uno para la corriente de 3.3V, otro para tierra y finalmente uno de datos que se conecta a cualquier pin GPIO de la Raspberry Pi. Según la especificación el sensor proporciona sus datos en unos 4 ms cuando se le emite un pulso bajo durante unos pocos microsegundos y a continuación uno alto durante otros pocos microsegundos, momento a partir del cual el sensor emite 40 bits de información empezando por un pulso bajo de inicio y a continuación el propio bit con un pulso alto, según sea la duración del pulso alto el bit se considera un 0 si es menor de unos 27μs y un 1 si dura más de ese tiempo hasta unos 80μs momento en el que se emite el siguiente bit de información. Los primeros 8 bits corresponden a la parte entera de la humedad, los siguientes 8 bits a la parte decimal de la humedad, el tercer grupo de 8 bits a la parte entera de la temperatura y 8 bits más para la parte decimal. El último grupo de 8 bits hasta completar los 40 bits son de checksum para detectar errores en la transmisión. El porcentaje de errores en la transmisión significativo en este sensor y quizá haya que realizar varias lecturas del sensor para obtener una correcta.

Dada las restricciones de tiempo que utiliza el sensor en el ejemplo usaré el lenguaje C para obtener los valores de temperatura y humedad y JNI para acceder a ellos desde Java. Aún usando C se producen errores en la obtención de los valores ya que el método usado por el sensor y transmitir los datos basados en tiempo de microsegundos no es muy fiable. Usando Java la situación sería peor por las restricciones que impone la máquina virtual con sus paradas para la recolección de basura por ejemplo. Así que el ejemplo consistirá en una combinación de C y Java con JNI un poco más avanzado que el Ejemplo de JNI, usar código en C desde Java.

Sensor DHT11 Sensor DHT11

Sensor DHT11

Lo primero que deberemos hacer para acceder a la información del sensor desde Java es crear una clase que contenga un método nativo que realizará la lectura de la información en C. Con la utilidad javah obtendremos el archivo de cabecera que implementará el programa en C. Finalmente, siguiendo la especificación se escribe el código C que realice la lectura que en este caso usará la librería wiringPi para la interacción con los pines GPIO de la Raspberry Pi.

El código en C invocará el método setTemperatureHumidity pasando como parámetros los datos de temperatura y humedad leídos del sensor, el método nativo read es utilizado por el código Java que controla el sensor para realizar la lectura en el código C.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package io.github.picodotdev.blogbitix.javaraspberrypi;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.LocalDateTime;
import java.util.Optional;

public class Dht11 {

    static {
        String architecture = System.getProperty("os.arch");
        String library = String.format("/libdht11-%s.so", architecture);
        try (InputStream is = Dht11.class.getResourceAsStream(library)) {
            Path path = File.createTempFile("libdht11", "so").toPath();
            Files.copy(is, path, StandardCopyOption.REPLACE_EXISTING);
            System.load(path.toAbsolutePath().toString());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private int gpio;
    private Optional<Integer> temperature;
    private Optional<Integer> humidity;
    private Optional<LocalDateTime> date;

    public Dht11(int gpio) {
        this.gpio = gpio;
        this.temperature = Optional.empty();
        this.humidity = Optional.empty();
        this.date = Optional.empty();

        init();
    }

    private native void init();
    private native void read(int gpio);

    public void update() {
        try {
            for (int i = 0; i < 5; ++i) {
                try {
                    read(gpio);
                    break;
                } catch (Exception e) {
                    Thread.sleep(3000);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public Optional<Integer> getTemperature() {
        return temperature;
    }

    public Optional<Integer> getHumidity() {
        return humidity;
    }

    public Optional<LocalDateTime> getDate() {
        return date;
    }

    public void setTemperatureHumidity(int temperature, int humidity) {
        this.temperature = Optional.of(temperature);
        this.humidity = Optional.of(humidity);
        this.date = Optional.of(LocalDateTime.now());
    }

    public static void main(String[] args) {
        Dht11 sensor = new Dht11(2);
        sensor.update();
        if (sensor.getTemperature().isPresent() && sensor.getHumidity().isPresent()) {
            System.out.printf("Temperature (C): %f, Humidity: %f%n", sensor.getTemperature().get(), sensor.getHumidity().get());
        } else {
            System.out.println("No temperature/humidity");
        }
    }
}
Dht11.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class io_github_picodotdev_blogbitix_javaraspberrypi_Dht11 */

#ifndef _Included_io_github_picodotdev_blogbitix_javaraspberrypi_Dht11
#define _Included_io_github_picodotdev_blogbitix_javaraspberrypi_Dht11
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     io_github_picodotdev_blogbitix_javaraspberrypi_Dht11
 * Method:    init
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_io_github_picodotdev_blogbitix_javaraspberrypi_Dht11_init
  (JNIEnv *, jobject);

/*
 * Class:     io_github_picodotdev_blogbitix_javaraspberrypi_Dht11
 * Method:    read
 * Signature: (I)V
 */
JNIEXPORT void JNICALL Java_io_github_picodotdev_blogbitix_javaraspberrypi_Dht11_read
  (JNIEnv *, jobject, jint);

#ifdef __cplusplus
}
#endif
#endif
Dht11.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <wiringPi.h>
  
#include "io_github_picodotdev_blogbitix_javaraspberrypi_Dht11.h"

#define MAXTIMINGS 85
int dht11_dat[5] = { 0, 0, 0, 0, 0 };

JNIEXPORT void JNICALL Java_io_github_picodotdev_blogbitix_javaraspberrypi_Dht11_init(JNIEnv *env, jobject obj)
{
    wiringPiSetup();
}

JNIEXPORT void JNICALL Java_io_github_picodotdev_blogbitix_javaraspberrypi_Dht11_read(JNIEnv *env, jobject obj, jint gpio)
{
    uint8_t state	= HIGH;
	uint8_t counter	= 0;
	uint8_t j		= 0, i;

	dht11_dat[0] = dht11_dat[1] = dht11_dat[2] = dht11_dat[3] = dht11_dat[4] = 0;

	pinMode(gpio, OUTPUT);
	digitalWrite(gpio, LOW);
	delay(18);
	digitalWrite(gpio, HIGH);
	delayMicroseconds(40);
	pinMode(gpio, INPUT);

	for (i = 0; i < MAXTIMINGS; i++)
	{
		counter = 0;
		while (digitalRead(gpio) == state)
		{
			counter++;
			delayMicroseconds(1);
			if (counter == 255)
			{
				break;
			}
		}
		state = digitalRead(gpio);

		if (counter == 255)
			break;

		if ((i >= 4) && (i % 2 == 0))
		{
			dht11_dat[j / 8] <<= 1;
			if (counter > 16)
				dht11_dat[j / 8] |= 1;
			j++;
		}
	}

	if ((j >= 40) && (dht11_dat[4] == ((dht11_dat[0] + dht11_dat[1] + dht11_dat[2] + dht11_dat[3]) & 0xFF)))
	{
	    jclass clazz = (*env)->GetObjectClass(env, obj);
	    jmethodID method = (*env)->GetMethodID(env, clazz, "setTemperatureHumidity", "(II)V");
		(*env)->CallVoidMethod(env, obj, method, dht11_dat[2], dht11_dat[0]);
	}
	else {
         jclass clazz = (*env)->FindClass(env, "java/io/IOException");
         (*env)->ThrowNew(env, clazz, "Failed read");
	}

    return;
}
Dht11.c

El código en C del sensor hay que compilarlo en la Raspberry Pi con el compilador gcc obteniendo una librería con código nativo que Java y JNI cargará y enlazará de forma dinámica en el programa Java. Ya que el código C usa la librería wiringPi ha de instalarse previamente junto con el compilador gcc. Obtenida la librería la copiamos mediante FTP o SSH de la Raspberry Pi a nuestro equipo de desarrollo. El código C realiza la lectura usando la librería wiringPi siguiendo la especificación de como se transmiten los datos por el sensor, realizada una lectura correcta usa varias de las funciones de la estructura JNIEnv para obtener la referencia a un método de la clase DHT11 e invocarlo con los valores obtenidos del sensor o lanza una excepción si la lectura ha sido errónea.

1
2
# pacman -S wiringpi gcc

install-packages.sh

El comando para compilar la librería de código nativo a partir del código en C y el archivo de cabecera generado con javah es el siguiente.

1
2
$ gcc -I"/usr/lib/jvm/java-8-openjdk/include" -I"/usr/lib/jvm/java-8-openjdk/include/linux" -shared -fPIC -L/usr/lib -lwiringPi -o libdht11-arm.so Dht11.c

compile.sh

Para facilitar la ejecución la librería la proporcionó ya compilada y ubicada en el directorio src/main/resources de modo que será incluida en el archivo jar generado por Gradle en el ejemplo y que la clase DHT11 extraerá al directorio temporal del sistema y cargará para su uso.

La clase Java del ejemplo que hace uso del sensor realiza una lectura cada 3 segundos e imprime en la terminal y en el display 1602 el último valor obtenido correctamente de la temperatura y humedad.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package io.github.picodotdev.blogbitix.javaraspberrypi;

import com.diozero.I2CLcd;
import com.diozero.api.I2CConstants;

import java.nio.ByteOrder;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class TemperatureHumidity {

    public static void main(String[] args) throws Exception {
        Dht11 sensor = new Dht11(2);
        ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();

        try (I2CLcd lcd = new I2CLcd(I2CConstants.BUS_0, I2CLcd.DEFAULT_DEVICE_ADDRESS, ByteOrder.LITTLE_ENDIAN, 16, 2)) {
            Runnable monitor = new Runnable() {
                @Override
                public void run() {
                    try {
                        sensor.update();

                        System.out.printf("Temperature: %dºC, Humidity: %d%%, Date: %s%n", sensor.getTemperature().get(), sensor.getHumidity().get(), sensor.getDate().get().format(DateTimeFormatter.ISO_DATE_TIME));

                        if (sensor.getTemperature().isPresent() && sensor.getHumidity().isPresent() && sensor.getDate().isPresent()) {
                            lcd.setText(0, String.format("T: %dC, H: %d%% ", sensor.getTemperature().get(), sensor.getHumidity().get()));
                            lcd.setText(1, String.format("%s", sensor.getDate().get().format(DateTimeFormatter.ofPattern("HH:mm:ss"))));
                        } else {
                            lcd.setText(0, String.format("No data"));
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            };

            service.scheduleAtFixedRate(monitor, 1, 5, TimeUnit.SECONDS);
            Thread.sleep(60000);
        } finally {
            service.shutdown();
        }
    }
}
TemperatureHumidity.java

Este es el esquema de conexiones que he utilizado para el ejemplo y una foto del cableado real, he usado del pin GPIO número 2 según la nomenclatura de wiringPi para el cable de datos del sensor DHT11 que se corresponde con pin número 13 según la nomenclatura del header de la Raspberry Pi. Para ver el cableado del display 1602 más detalladamente y la activación del bus de comunicación I2C que necesita consulta el artículo Controlar un display LCD 1602 para mostrar texto con la Raspberry Pi y Java.

Cableado sensor DHT11 y display 1602

Cableado sensor DHT11 y display 1602

Ejecutando el programa del ejemplo y usando el display 1602 (16 columnas y 2 filas) se muestra la temperatura y humedad obtenida del sensor.

Cableado ejemplo y funcionando Cableado ejemplo y funcionando

Cableado ejemplo y funcionando

En el kernel de Linux hay un módulo que proporciona también los valores del sensor, sin embargo, no he conseguido obtener la temperatura y humedad usándolo. Lo he probado con el kernel 4.4 de Arch Linux ARM y en la versión 4.9 veo que hay cambios en este módulo que quizá lo hagan funcionar. Para usar el módulo del kernel hay que añadir un poco de configuración para el inicio de la Raspberry Pi. En los archivos /sys/devices/platform/dht11@0/iio:device0/in_temp_input y /sys/devices/platform/dht11@0/iio:device0/in_temp_input estarán la temperatura y humedad respectivamente.

1
2
dtoverlay=dht11,gpiopin=2

kernel-module-config.txt
1
2
$ cat /sys/bus/iio/devices/iio:device0/in_temp_input
$ cat /sys/devices/platform/dht11@0/iio:device0/in_temp_input
cat.sh

Para ejecutar el ejemplo con ya todo instalado uso uno de los siguientes dos comandos.

1
2
3
4
$ ./gradlew upload
$ ssh -t 192.168.1.101 'cd /home/raspberrypi/scripts/javaraspberrypi && sudo java -classpath "*" io.github.picodotdev.blogbitix.javaraspberrypi.TemperatureHumidity'

$ ./gradlew executeTemperatureHumidity
execute.sh
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew executeTemperatureHumidity


Comparte el artículo: