Detectando Corriente Alterna - Sensor de Movimiento - Raspberry PI

Última revisión en abril del 2022.
Esta entrada explica cómo detectar la presencia de una persona en nuestro escritorio con un sensor de movimiento conectado a una Raspberry Pi que nos envía una alarma a nuestro móvil. Se utiliza un sensor de movimiento alimentado por corriente alterna porqué queríamos introducir el módulo de detección de AC. Lógicamente, para esta aplicación en concreto se puede utilizar un sensor de movimiento de continua alimentado por la propia Raspberry Pi.

Advertencia - 220V AC

La corriente alterna puede quemarle y puede detener su corazón, así que por favor no intente construir los circuitos descritos aquí a menos que esté absolutamente seguro de que puede hacerlo con seguridad.

Módulo de detección de AC

Este módulo permite conectar tres cables monofásicos de entrada y detectar en ellos la existencia de corriente alterna. Las tres correspondientes salidas, aisladas con optoacopladores de la entradas, se pueden conectar directamente a las GPIO de la Raspberry.

El esquema del circuito

Módulo de aislamiento por optoacoplador de detección de corriente alterna

Después de un análisis somero del módulo cuando detecta AC, vemos que el zener es de 3V, que en la entrada del optoacoplador tenemos 1.1V y que en la salida del módulo, si la alimentamos a 3V3, tenemos un consumo de 2.18 mA. Además, el transistor hace que la detección de alterna sea a nivel bajo en la entrada GPIO de la Raspberry.

Sensor de Movimiento de AC

Utilizamos un detector de movimiento de infrarrojos de los que se utilizan para encender lámparas con corriente alterna por la noche. El detector es de exterior con IP65, un alcance de 12 metros y en el que podemos configurar la luz máxima exterior (3-2000 lumens) a la que actúa como sensor de movimiento y el tiempo que permanece conectado una vez se ha detectado el movimiento (10s-15min). Para el uso que le vamos a dar lo configuramos a 2000 lumens (siempre) y 10 segundos (mínimo).

Dentro del sensor de movimiento, a parte del PIR (Passive Infrared sensor) y el LDR (Light Dependent Resistor) tenemos algún tipo de convertidor de alterna a continua y un relé.

Las conexiones

El sensor de movimiento tiene de entrada la fase (L) y el neutro (N) que lo alimentan y devuelve una fase (L1) que tiene tensión cuando se detecta movimiento. Esta fase (L1) se conecta al módulo de detección de alterna que lo transforma en un nivel bajo (0V sobre 3V3) en la GPIO 6 de la Raspberry Pi.

En la entrada del módulo de relés, también usado en este proyecto, hay más información sobre las conexiones que permiten encender la lámpara.

Cómo lo probamos

Ponemos el sensor de movimiento en nuestro escritorio y hacemos que la Raspberry PI nos envíe una alarma a nuestro móvil y encienda la lámpara cuando se detecta movimiento.

Escaneando las GPIO de Raspberry PI

Para programar las GPIO usamos la librería gpiod. Debajo encontramos el programa principal que básicamente espera a que la GPIO 6, donde está conectado el sensor de movimiento, se ponga a nivel bajo y entonces envía una alarma y enciende la luz conectada a la la GPIO 21. Como cuando la línea del sensor vuelve a nivel alto se generan varios rebotes,  optamos por filtrarlos leyendo los eventos existentes 50 ms después del cambio.


struct gpiod_line *lamp;

// lamp is on when GPIO is low level
void turn_on_lamp()
{  
  gpiod_line_set_value(lamp, 0);
}

// lamp is off when GPIO is high level
void turn_off_lamp()
{
  gpiod_line_set_value(lamp, 1);
}

int main(void)
{
  const char *chipname = "gpiochip0";
  struct gpiod_chip *chip;
  struct gpiod_line *motion_sensor;
  struct timespec ts = {1, 0};
  struct gpiod_line_event event[3];
  int ret;

  // Open GPIO chip
  chip = gpiod_chip_open_by_name(chipname);

  // Open GPIO lines
  lamp = gpiod_chip_get_line(chip, 21);
  motion_sensor = gpiod_chip_get_line(chip, 6);

  // output in state high by default
  gpiod_line_request_output(lamp, "test", GPIOD_LINE_ACTIVE_STATE_HIGH);
  // input with pull-up resistor
  gpiod_line_request_falling_edge_events_flags(motion_sensor, "test",
GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP);

  for (;;)
  {
    for (;;)
    {
      ret = gpiod_line_event_wait(motion_sensor, &ts);
      if (ret == 1)
      {
        break;
      }
    }

    ret = gpiod_line_event_read(motion_sensor, &event[0]);
    if (ret == 0)
    {      
      // AC detected - motion detected
      if (event[0].event_type == GPIOD_LINE_EVENT_FALLING_EDGE)
      {
        turn_on_lamp();

        sendAlarm();

        do
        {          
          usleep(500000); // 500ms
         
          // AC is still detected ?
          ret = gpiod_line_get_value(motion_sensor);          
          if (ret == 1)
          {
            turn_off_lamp();

            // bounces --> multiple events
            usleep(50000); // 50ms
            gpiod_line_event_wait(motion_sensor, &ts);
            ret = gpiod_line_event_read_multiple(motion_sensor, event, 3);
            break;
          }
        } while (1);
      }
    }
  }

  // turn off lamp;
  gpiod_line_set_value(lamp, 0);
  // Release lines and chip
  gpiod_line_release(lamp);
  gpiod_line_release(motion_sensor);
  gpiod_chip_close(chip);
  //
  return 0;
}

Enviando la alarma al móvil

Esta parte se basa en la entrada Cómo enviar una alarma a un móvil que explica cómo implementar las diferentes partes del proyecto. Aquí sólo explicamos cómo implementar la parte del dispositivo cliente en C, ya que en la entrada referenciada está todo implementado en Java.


// it sends an alarm to a server which forwards it to the mobile subscriber
void sendAlarm()
{
  char *jwt;
 
  // creates authentification token
  jwt = jwtCreate("rsa_private.pem", "Project Id", "MyFistDeviceID");

  // send an alarm
  restPostJSON("192.168.1.36",
           "8080",
           "resource/alarm/notification",
           notificationJSONCreate(),
           jwt);
}

Para enviar la alarma se crea un token JWT que permite la autentificación y autorización del dispositivo cliente en el servidor, se empaquetan los datos de la notificación que queremos enviar al móvil en un objeto JSON y se envía todo al método notification del recurso alarm de un servidor que tenemos en Google Clould Platform.

No entramos en cómo se crea el socket, ni en el envío de la solicitud en HTTP, ni en la recepción de la respuesta en HTTP que lo podemos encontrar en el repositorio, sólo comentamos las partes que nos parecen interesantes.

REST - HTTP POST

Detalle de la implementación en C de la solicitud HTTP POST con datos en JSON y un Bearer Authentication.


void sendPost(int socket, char *hostname, char *port, char *path,char *jwt, char *json)
{
    char buffer[2048];

    sprintf(buffer, "POST /%s HTTP/1.1\r\n", path);
    sprintf(buffer + strlen(buffer), "Host: %s:%s\r\n", hostname, port);
    sprintf(buffer + strlen(buffer), "Connection: close\r\n");
    sprintf(buffer + strlen(buffer), "User-Agent: RPI4B\r\n");
    sprintf(buffer + strlen(buffer), "Content-Type: application/json\r\n");
    sprintf(buffer + strlen(buffer), "Authorization: Bearer %s\r\n", jwt);
    sprintf(buffer + strlen(buffer), "Content-Length: %d\r\n", strlen(json));
    sprintf(buffer + strlen(buffer), "\r\n");
    sprintf(buffer + strlen(buffer), "%s\r\n", json);

    send(socket, buffer, strlen(buffer), 0);    
}

Datos de la notificación en JSON

Para empaquetar los datos de la notificación en JSON utilizamos la librería Jansson.


char* notificationJSONCreate()
{
   // current time
  time_t current_time = time(NULL);
  struct tm *local_time = localtime(&current_time);
  char aux[100];
  char *dump;

  // json notification  
  json_t *root = json_object();

  json_object_set_new(root, "title", json_string("Desk Alarm"));
  sprintf(aux, "Motion detected at %s", asctime(local_time));
  json_object_set_new(root, "body", json_string(aux));

  dump = json_dumps(root, 0);

  json_decref(root);

  return(dump);

}

JWT autentificación y autorización - Bearer Authentication

Implementación de la generación del token JWT en C utilizando la JWT C library.

El parámetro opt_key_name de la función jwtCreate és la clave rsa privada, en formato PEM (Privacy Enhanced Mail), del dispositivo con la identificación en el servidor deviceId. En la entrada Cómo autentificar un dispositivo en el servidor se explican los detalles.

Ponemos un exp claim (Expiration Time) de 1 día para el token.


#include <stdio.h>
#include <stdlib.h>
#include <jwt.h>
#include <getopt.h>
#include <string.h>
#include <time.h>
#include <string.h>
#include <libgen.h>

// gets iat and exp claims
// expiration time: 24 h
static void getIatExp(char *iat, char *exp, int time_size)
{
    time_t now_seconds = time(NULL);
    snprintf(iat, time_size, "%lu", now_seconds);
    snprintf(exp, time_size, "%lu", now_seconds + 24 * 60 * 60);
}

// creates an authentication token - jwt
char* jwtCreate(char *opt_key_name, char* aud, char* deviceId)
{
    size_t key_len = 0;
    FILE *fp_priv_key;
    unsigned char key[10240];    
    jwt_t *jwt = NULL;    
    char iat_time[sizeof(time_t) * 3 + 2];
    char exp_time[sizeof(time_t) * 3 + 2];
    char* out = NULL;

    fp_priv_key = fopen(opt_key_name, "r");
    key_len = fread(key, 1, sizeof(key), fp_priv_key);
    fclose(fp_priv_key);
    key[key_len] = '\0';    

    jwt_new(&jwt);
    getIatExp(iat_time, exp_time, sizeof(iat_time));
    jwt_add_grant(jwt, "iat", iat_time);
    jwt_add_grant(jwt, "exp", exp_time);
    jwt_add_grant(jwt, "aud", aud);
    jwt_add_grant(jwt, "deviceId", deviceId);
    jwt_set_alg(jwt, JWT_ALG_RS256, key, key_len);
    out = jwt_encode_str(jwt);

    jwt_free(jwt);
    return out;
}

Para finalizar

El código se ha hecho específicamente para este post, sólo se ha buscado que fuera funcional y breve, se ha depurado superficialmente y puede contener errores.

El código de la Raspberry PI

Puedes encontrar el código de la Raspberry PI de esta entrada en el repositorio de github: Consolidando.

Más información

.

Comentarios