Cómo controlar tu casa desde una página web con una Raspberry Pi

Última revisión en mayo del 2021.
Notas de cómo controlar dispositivos de nuestra casa desde una página web remota. Se monta un servidor en una Raspberry PI, con soporte para REST y WebSockets, que sirve un página web con un interruptor para un led que tenemos en nuestra casa.

El proyecto

Vamos a usar el led del ejemplo del post la Raspberry Pi 4B como el dispositivo a controlar. Añadiendo un relé este led se podría cambiar por una calefacción, una luz o cualquier cosa enchufable. Al pulsador se le ha dado la funcionalidad de finalizar el programa.

Nuestra casa con un led
El objetivo es encender y apagar este led desde cualquier dispositivo que tenga una conexión a Internet y un navegador con JavaScript, y que todos dispositivos conectados al servidor reciban las actualizaciones del cambio de estado del led.
El esquema

Por una cuestión de mantener el post simple, no se incluye la parte de securización, capa de transporte TLS (OpenSSL library) y password de entrada a la página vía un Social authentication proxy. Tampoco se explica cómo hacer visible el servidor desde fuera de la Intranet.

Tenia claro que el servidor lo quería hacer en C, Raspberry es C con un montón de wrappers en Python, y el cliente tenia que correr en un browser con JavaScript; y usar REST, WebSockets o MQTT.

Prerrequisitos

En el post sobre Raspberry Pi 4B se explica cómo instalar la librería libgpiod y el entorno de desarrollo C/C++, ambos necesarios para este proyecto.

El servidor - Ulfius

Es un caos buscar software en C para Linux, hay millones de variaciones de todo, y la mitad de los proyectos no hay nadie que los mantenga. En algún momento me he plateado recurrir a la simplicidad de JavaScript usando Node / Express, pero entonces se complica el enlace con el hardware.

He empezado por lo tradicional, el popular Nginx con una extensión CGI o fastCGI, pero nadie está manteniendo estas extensiones. Me he mirado libevent que me ha parecido muy interesante, pero he acabado usando la librería Ulfius por su simplicidad.

Ulfius es una librería, bastante amater, que te permite crear un servidor con soporte para REST, WebSockets y datos en JSON.

Cómo instalar la libreria Ulfius

Ulfius forma parte de la distribución oficial de Debian y se puede instalar con apt.

sudo apt install libulfius-dev

La versión que se instala, un poco antigua, es la 2.5.2, que es la que Debian tiene calificada como estable.

Ulfius se apoya en las librerías en C: GNU Libmicrohttpd y Jansson.

REST - Representational State Transfer

He optado por usar REST por su ubiquidad. Creo que no tengo ningún proyecto cliente-servidor en que no haga uso de REST. Además hay servidores que no soportan los WebSockets.

REST son constricciones sobre HTTP que nos marcan cómo hacer las cosas.

La mayor ventaja de REST es que escala muy bien debido a que no mantiene el estado. La desventaja, REST es HTTP, es que el servidor no tiene ninguna manera de enviar información al cliente sin una petición previa; siendo el cliente el que tiene que hacer un polling para obtenerla. El servidor puede convertir este polling en un Long Polling reteniendo la petición a la espera de cambios.

REST se basa en recursos (ROA - Resource-Oriented Architecture) que pueden ser direccionados independientemente. Nuestro proyecto sólo tiene un pequeño recurso llamado led.

Recurso led

Path Verbo Resultado
/led GET Interruptor del led en una página HTML
/led PUT Nuevo estado del led en JSON
Recurso led

El PUT extrae el state de los datos JSON de la entrada y lo convierte en una acción sobre el led.


int callback_put_led_state (const struct _u_request * request
struct _u_response * response
void * user_data
{
  json_t * json_led = ulfius_get_json_body_request(requestNULL); 
  json_t * json_body = NULL;
  json_int_t state;

  if (json_led != NULL
  {    
    state = json_integer_value(json_object_get(json_led,"state")); 

    // turn on/off the led
    set_led_state(state);
    //
    json_body = json_object();
    json_object_set_new(json_body"state"json_integer(state));
    ulfius_set_json_body_response(response200json_body);
    //
    json_decref(json_led);
    json_decref(json_body);
  }  
  else
  {
    ulfius_set_json_body_response(response400NULL);    
  }

  return U_CALLBACK_CONTINUE;
}
En lugar de que los clientes hagan un polling sobre el servidor, creamos un WebSocket que devuelve los cambios estado del led a los clientes conectados.

WebSockets

Forman parte de la especificación del HTML5. Es un canal full duplex entre el servidor y el cliente, basado en eventos, que permite la interacción a tiempo real entre múltiples usuarios.

WebSocket sólo es una conexión, sin reglas de cómo implementar el contenido.
Path Verbo Resultado
/websocket GET Inicia un WebSocket que envía cambios de estado del led a los clientes en JSON
Recurso websocket

Se muestrea el led cada segundo, en caso de que el estado haya cambiado se envía a todos los clientes. Se ha hecho así para simplificar el código. Realmente el cambio de la GPIO debería generar un evento que acaba informando a los clientes.


void websocket_manager_callback(const struct _u_request * request,
                               struct _websocket_manager * websocket_manager,
                               void * websocket_manager_user_data
{
  int sent_state = -1state;
  
  for (;;) 
  {    
    state = get_led_state();
    if (state != sent_state)
    {
      sent_state = state;
      json_t * message = json_pack("{si}""state"state);
      char * json = json_dumps(messageJSON_COMPACT);
      int ret = ulfius_websocket_send_message(websocket_managerU_WEBSOCKET_OPCODE_TEXTstrlen(json), json);
      o_free(json);
      json_decref(message);
    }

    if (ulfius_websocket_wait_close(websocket_manager1000) != U_WEBSOCKET_STATUS_OPEN)
      break;
  }  
}
En el caso de un sólo led lo correcto habría sido hacerlo todo con WebSockets, sin usar REST.

Datos JSON - JavaScript Object Notation

Para el intercambio de datos usamos el formato estándar para representar objetos de JavaScript en modo texto


{
  "state": 0 o 1,
  "device": "led"
}

En el código sólo tenemos el campo "state" pero si tuviéramos varios dispositivos como mínimo tendríamos que tener un campo "device" para saber a quién queremos cambiar el estado.

Los clientes - La página web

http://IP:8080/led

Nuestra web es muy sencilla sólo tiene un botón que pone "on" o "off" en función del estado del led y al pulsarlo cambia el estado del led.


<html>
<body>
    <script>
    .../...
    </script>
    <button id="button" onclick="changeLedState()">on</button>
</body>
</html>

Para cambiar el estado del led se hace un REST con el verbo PUT y como datos un objeto JSON con el estado 0 o 1 (apagado o encendido).


<script>
   let state = 0
   function setRemoteLedState(state
{                        
     fetch("/led/", {
       method: 'PUT',
       headers:
       {
         'Content-Type': 'application/json'
       },
       body: JSON.stringify({state})
     });
   }

   function setLocaleLedState(newState)
   {
    state = newState;
     document.getElementById("button").innerText  
          = state ? "on" : "off";
   }

   function changeLedState() 
   {                  
      setRemoteLedState(state ? 0 : 1);
   }      
</script>

La inicialización de texto del botón y los posibles cambios de estado los recibimos a través de un WebSocket.


<script>  
let ws = new WebSocket("ws://IP:8080/websocket");

  ws.onmessage = function(e
  {
setLocaleLedState(JSON.parse(e.data).state);
  }

</script>

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.

No se ha examinado el código de la Librería Ulfius, antes de ponerla en un proyecto serio se tendría que mirar si escala correctamente y cómo está montada la concurrencia en los WebSockets.

Si vuestro proyecto sale de la Intranet se tiene que segurizar si no queréis que alguien controle vuestra calefacción o que convierta algún dispositivo en un servidor de spam.

Más información

  • Repositorio Ulfius: Servidor REST - WebSockets - JSON
  • Repositorio libgpiod: Librería C que hace de interfaz con las GPIO.
  • Librería GNU Libmicrohttpd: Concurrent HTTP server.
  • Librería Jansson: JSON.

El código

Puedes encontrar el código de este post en el repositorio de github: Consolidando.

Qué más

Comentarios