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.
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.
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.
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 |
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(request, NULL); 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(response, 200, json_body); // json_decref(json_led); json_decref(json_body); } else { ulfius_set_json_body_response(response, 400, NULL); }
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 |
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 = -1, state; for (;;) { state = get_led_state(); if (state != sent_state) { sent_state = state; json_t * message = json_pack("{si}", "state", state); char * json = json_dumps(message, JSON_COMPACT); int ret = ulfius_websocket_send_message(websocket_manager, U_WEBSOCKET_OPCODE_TEXT, strlen(json), json); o_free(json); json_decref(message); }
if (ulfius_websocket_wait_close(websocket_manager, 1000) != 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
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.
Comentarios
Publicar un comentario