Guía para Crear un Sistema de Seguridad con Cámaras IP y una Raspberry Pi: Construye tu Propio Sistema de Vigilancia Domiciliaria
Última revisión en abril del 2022.
Puedes encontrar el código de la Raspberry Pi de esta entrada en el repositorio de GitHub: Consolidando - Raspberry.
El proyecto
El proyecto consta de una Raspberry PI, de un módulo de detección de AC, de un módulo de relés, de una cámara IP, de un sensor de movimiento y un foco. Su conexionado es idéntico al de la entrada Detectando Corriente Alterna - Sensor de Movimiento.
Además, tenemos un servidor Pub/Sub en la Google Cloud Platform y usaremos el Firebase Cloud Messaging. También tenemos una App cliente para recibir notificaciones / alarmas en los móviles.
La cámara IP, la Raspberry PI, el servidor Pub/Sub (en la red local o en la nube) y los móviles (en alguna red de datos) están conectados a través del router. Los dispositivos que están en la red local se les asigna una dirección IP privada fija para facilitar su localización.
Tecnologías Software Utilizadas
- Google Appengine - Java
- Eclipse Jetty
- Eclipse Jersey
- Firebase Cloud Messaging
- Google Sign In
- OpenSSL
- Google Cloud Storage
- Maven
- Vanilla JS
- Android - Java
El hardware
- Raspberry PI 4 Model B
- Cámara IP
- Sensores de Movimiento
- Módulo de Relés con optoacopladores
- Módulo de detección de AC
- Focos
- Un móvil Android
Detección de movimiento
En la entrada Detectando Corriente Alterna - Sensor de Movimiento se explica cómo se usan las GPIO de la Raspberry Pi para para detectar movimiento a través de un sensor de infrarrojos alimentado en alterna.
Tomar una foto con la Cámara IP
En la entrada Cómo hacer una foto con una cámara IP se explica cómo usar el protocolo RTSP para sacar una foto del streaming de una cámara IP.
Enviar una notificación con una imagen a un móvil
Esta parte se basa en la entrada Detectando Corriente Alterna - Sensor de Movimiento donde se explica como enviar notificaciones a los móviles, pero en este caso se incluye una imagen jpeg del movimiento detectado que es un archivo binario que no podemos empotrar en un objeto json. Así optamos por crear un nuevo método notification/image en el recuso alarm en el servidor Pub/Sub. Este método acepta contenido multipart/form-data del cual incluimos debajo el detalle de la implementación en el cliente.
Recurso alarm - método notification/image
La llamada del método notification/image del recurso alarm en la Raspberry, implementado en lenguaje C.
#include "rtsp.h"#include "rest.h"#include "jwt_create.h"
const char *PROJECT_ID = "Project Id";const char *DEVICE_ID = "MyFistDeviceID";const char *RSA_PRIVATE_KEY_NAME = "rsa_private.pem";
#define TMP_FILE "pictureName.jpg"
// Pub/Sub hostconst char *HOST_NAME = "192.168.1.36";const char *HOST_PORT = "8080";const char *HOST_RESOUCE = "resource/alarm/notification/image";
// IP CAM Hostconst char *IP_CAM_RESOURCE = "rtsp://192.168.1.24:554/1/h264major";
const notificationStruct notificationBody = { .title = "IP CAM Alarm", .body = "Motion Detected", .image = TMP_FILE};
int alarmTakePictureAndSendNotification(){ RTSPResource resource; int ret; char *jwt; int socket;
// opens ip cam resource ret = openIpCam(IP_CAM_RESOURCE, &resource); if (ret == 0) ret = openVideoCodec(&resource); // gets an image from the camera if (ret == 0) ret = takeAPicture(&resource, TMP_FILE, 9, 0);
// releases resource closeVideoCodec(&resource); closeIpCam(&resource);
if (ret == 0) { // creates authentification token for the device id jwt = jwtCreate(RSA_PRIVATE_KEY_NAME, PROJECT_ID, DEVICE_ID);
// sends image notification to the subscribed mobiles restPostMultipart(HOST_NAME, HOST_PORT, HOST_RESOUCE, ¬ificationBody, jwt); }
return(ret);}
Detalle de cómo se forma el multipart/form-data con los datos de la notificación.
#include <stdio.h>#include <string.h>#include <stdlib.h>#include <time.h>
#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <netdb.h>#include <unistd.h>#include <errno.h>
#include "rest.h"
#define TIMEOUT 5.0#define USER_AGENT "RPI4B"const char *BOUNDARY = "xxBOUNDARYxx";
int receiveRespose(int server);
int multiPartBody(unsigned char **pBuffer, const notificationStruct *notificationBody){ FILE *file; unsigned long fileLen; const int boundaryLen = strlen(BOUNDARY); unsigned char *buffer;
// Open file file = fopen(notificationBody->image, "rb"); if (!file) { fprintf(stderr, "Unable to open file %s", notificationBody->image); return (0); }
// Get file length fseek(file, 0, SEEK_END); fileLen = ftell(file); fseek(file, 0, SEEK_SET);
// Allocate memory buffer = (char *)malloc(fileLen + 512); if (!buffer) { fprintf(stderr, "Memory error!"); fclose(file); return 0; }
sprintf(buffer, "--%s\r\n", BOUNDARY); sprintf(buffer + strlen(buffer), "Content-Disposition: form-data; name=\"title\"\r\n\r\n"); sprintf(buffer + strlen(buffer), "%s\r\n", notificationBody->title); sprintf(buffer + strlen(buffer), "--%s\r\n", BOUNDARY); sprintf(buffer + strlen(buffer), "Content-Disposition: form-data; name=\"body\"\r\n\r\n"); sprintf(buffer + strlen(buffer), "%s\r\n", notificationBody->body); sprintf(buffer + strlen(buffer), "--%s\r\n", BOUNDARY); sprintf(buffer + strlen(buffer), "Content-Disposition: form-data; name=\"image\"; filename=\"%s\"\r\n", notificationBody->image); sprintf(buffer + strlen(buffer), "Content-Type: image/jpeg\r\n\r\n"); int len = strlen(buffer);
fread(buffer + len, fileLen, sizeof(unsigned char), file); fclose(file); len = len + fileLen; sprintf(buffer + len, "\r\n--%s--\r\n", BOUNDARY); len = len + boundaryLen + 8;
*pBuffer = buffer;
return (len);}
void sendPostMultipart(int socket, const char *hostname, const char *port, const char *path, char *jwt, const notificationStruct *notificationBody){ char header[1024]; unsigned char *buffer = NULL; int len;
len = multiPartBody(&buffer, notificationBody);
sprintf(header, "POST /%s HTTP/1.1\r\n", path); sprintf(header + strlen(header), "Host: %s:%s\r\n", hostname, port); sprintf(header + strlen(header), "Connection: close\r\n"); sprintf(header + strlen(header), "User-Agent: %s\r\n", USER_AGENT); sprintf(header + strlen(header), "Content-Type: multipart/form-data; boundary=%s\r\n", BOUNDARY); sprintf(header + strlen(header), "Authorization: Bearer %s\r\n", jwt); sprintf(header + strlen(header), "Content-Length: %d\r\n", len); sprintf(header + strlen(header), "\r\n");
printf("Sent Headers:\n%s", header);
send(socket, header, strlen(header), 0); send(socket, buffer, len, 0);
free(buffer);}
Servidor Pub/Sub
En la entrada Cómo enviar una alarma a un móvil se explica cómo implementar un servidor Pub / Sub. Sino es que el servidor Pub/Sub se encuentre dentro de nuestra red local, también es importante entender la entrada Cómo autentificar un dispositivo en el servidor.
Recurso alarm - método notification/image
La implementación en Java para el framework Jersey del método método notification/image en el recurso alarm que consume multipart/form-data.
@POST @SecuredDevice @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.TEXT_PLAIN) @Path("/notification/image") public Response sendNotificationImage(@Context SecurityContext securityContext, @FormDataParam("title") String title, @FormDataParam("body") String body, @FormDataParam("image") InputStream stream, @FormDataParam("image") FormDataContentDisposition fileDetail) {
try (InputStream imageStream = stream) { GoogleCloudStorage gcs = GoogleCloudStorage.getInstance(); String mediaLink; mediaLink = gcs.saveImage(fileDetail.getFileName(), imageStream); NotificationDomain nd = new NotificationDomain(); nd.setBody(body); nd.setTitle(title); FirebaseMessage fb = FirebaseMessage.getInstance(); SubscriberDomain sd = subscriberController.get(); String registrationToken = sd.getIdToken(); fb.sendNotification(registrationToken, nd, mediaLink); } catch (IOException ex) { Logger.getLogger(AlarmREST.class.getName()) .log(Level.SEVERE, "Error sending notification image: {0}", ex.getMessage()); return Response.serverError().entity("Error sending notification image").build(); }
return Response.accepted().build(); }
La imagen de la notificación cuando llega al móvil tiene que estar en una dirección URL, así que la guardamos en el Google Cloud Storage.
package com.elmoli.security.controller;
import static com.elmoli.security.Config.BUCKET_NAME;import static com.elmoli.security.Config.PROJECT_ID;import com.google.cloud.storage.Acl;import com.google.cloud.storage.Acl.Role;import com.google.cloud.storage.Acl.User;import com.google.cloud.storage.Blob;import com.google.cloud.storage.BlobId;import com.google.cloud.storage.BlobInfo;import com.google.cloud.storage.Storage;import com.google.cloud.storage.StorageOptions;import java.io.IOException;import java.io.InputStream;import java.util.Arrays;import java.util.List;
public class GoogleCloudStorage{ static final List ACL_PUBLIC = Arrays.asList(Acl.of(User.ofAllUsers(), Role.READER)); static Storage storage = null;
private GoogleCloudStorage() { if (storage == null) { try { storage = StorageOptions.newBuilder().setProjectId(PROJECT_ID).build().getService(); } catch (Exception e) { throw new RuntimeException("Error initializing Google Cloud Storage", e); } } }
public String saveImage(String imageName, InputStream stream) throws IOException { BlobId blobId = BlobId.of(BUCKET_NAME, imageName); BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setAcl(ACL_PUBLIC).build(); Blob blob = storage.createFrom(blobInfo, stream);
// Media Links have a query parameter that changes with the update of // the image that avoids cache problems return (blob.getMediaLink()); }
// Synchronization is not needed because this code relies on the class // loader's internal synchronization public static GoogleCloudStorage getInstance() { return GoogleCloudStorageHolder.INSTANCE; }
// this is only initialized if the getInstance is called. private static class GoogleCloudStorageHolder { private static final GoogleCloudStorage INSTANCE = new GoogleCloudStorage(); }
}
App - notificación móvil
En la entrada Cómo enviar una alarma a un móvil se explica cómo implementar la App para recibir notificaciones en un móvil. La aplicación no es más que el ejemplo Firebase Cloud Messaging Quickstart a la que se ha añadido una autentificación de usuario con Google Identity y una subscription al recurso alarm de nuestro servidor Pub/Sub.
Debajo un ejemplo de notificación/alarma recibido en nuestro móvil que al tocarla nos tendría que redirigir a una página del servidor con un muestreo a base de imágenes de los últimos movimientos detectados, mucho más fáciles de examinar que los videos de un mes que acostubran a guardar las cámaras IP.
Para finalizar
Esta entrada aglutina varias entradas de Consolidando y es la base de un proyecto real. No nos vamos a extender en cómo mejorar el host, la aplicación de la Raspberry o la app del móvil por tratarse sólo de software, sin un interés a nivel de estructurar el proyecto que es lo único que se pretendía.
En el caso de querer implementar el proyecto se aconseja seguir el orden de las entradas.
- Cómo autentificar un dispositivo en el servidor
- Cómo enviar una alarma a un móvil
- Controlando Corriente Alterna - Relé - Raspberry PI
- Detectando Corriente Alterna - Sensor de Movimiento - Raspberry PI
- Cómo hacer una foto con una cámara IP - FFMpeg
El código
Puedes encontrar el código de la Raspberry Pi de esta entrada en el repositorio de GitHub: Consolidando - Raspberry.
Comentarios
Publicar un comentario