1. Introduction

J'ai dans ma chambre un tiroir dans lequel, il y'a beaucoup de données confidentielles que je voudrais protéger. J'ai eu l'idée de faire un système de caméra de surveillance qui prend une vidéo d'une dizaine de secondes qui m'envoie par email, pour que je sois notifié à tout moment quand quelqu'un essaie de voler mes informations. Pour ne pas envoyer une vidéo pour rien quand j'ouvre le tiroir, je pense faire une interface dans le navigateur qui désactivera momentanément pendant un certain temps que j'aurai renseigné.

20220203_211137920_iOS.jpg, fév. 2022
Figure 1 : Tiroir coulissant

Pour pouvoir réaliser ce projet, il faudra que le système puisse détecter l'ouverture du tiroir puis filmer. Le système devra être capable d'envoyer les vidéos par email quand il sera connecté à internet et les envoyer plus tard si la connexion est rompue. Il faudrait pouvoir désactiver momentanément. Et enfin, il devra supprimer automatiquement les fichiers vidéos au bout d'un certain moment pour ne pas saturer la carte SD.

2. Matériel et méthodes

2.1 Matériel

Le matériel dont j'aurai besoin pour mener à bien ce projet sont les suivants:

  • 1x Raspberry Pi 3 model b v1.2
  • 1x Raspberry Pi Camera Module - Pi NoIR
  • 1x 9 Degrees of Freedom Breakout
  • 6x fils (au minimum)
  • 1x Batterie avec la fonction "pass-through"
  • 1x câble d'alimentation USB type-A vers microUSB
  • 1x câble d'alimentation n'importe quoi (max 18w) vers USB type-C
  • 1x Breadboard
  • 1x LED infrarouge
  • 1x résistance 330 Ohm

2.2 Méthode

2.2.1 Quelques notions

Afin de récupérer les données du capteur d'accélération, nous allons avoir besoin d'utiliser le protocole I2C. Développé par Phillips dans les années 1980, ce protocole est maintenant utilisé partout, car il peut être installer logiciellement dans n'importe quel microcontrôleur. La communication se fait toujours entre un maître et un ou tous les esclaves. Il faut lier deux fils entre le maître et l'esclave, le SDA et le SCL. Le SDA ou serial data line fait transiter les bits de données. Le maître envoi un bit de commencement, puis l'adresse de l'esclave avec lequel il veut communiquer, le mode (lecture ou écriture), les bits de données sont alors envoyés avec entre chaque octet le récepteur affirme qu'il a bien reçu la donnée puis cela continue jusqu'à que toutes les données aient transité et enfin fini par un bit de fin.

I2C_EchangeMaitreEsclave.svg, fév. 2022
Figure 2 : Echange maître-esclaves, source : https://fr.wikipedia.org/wiki/I2C

2.2.2 Hardware

Les branchements sont facile à faire. Pour le capteur d'accéléromètre, il suffit simplement de l'alimenter en 3.3V sur le port VDD, relier sa masse à celui du Raspberry Pi et enfin relier ses ports SDA et SCL aux ports homonymes du Raspberry Pi. Il faut installer la LED et la résistance sur la Breadboard et l'alimenter en continue avec le port 3.3V.

20220203_160402990_iOS.jpg, fév. 2022
Figure 3 : Branchement


Pour la caméra, il faut simplement le brancher au port qui se trouve entre la prise jack et le port HDMI (attention au sens ). Vérifier que le "sunny chip" se trouvant sur le module caméra soit bien enfoncé.

20220203_160509310_iOS.jpg, fév. 2022
Figure 4 : Camera PI NOIR

De ce qui est de l'alimentation, il faut simplement alimenter depuis une prise secteur la batterie et depuis la batterie le Raspberry Pi.

2.2.3 Software

Avant toute chose, il faut bien vérifier que la caméra et le protocole I2C soient activés sur le Raspberry Pi. Pour ce faire il faut utiliser la commande suivante dans le terminale.

sudo raspi-config

Puis activer s'il le faut la caméra et le protocole I2C. Pour récupérer les données de l'accéléromètre, j'ai utilisé un module déjà fait par une personne. Le code est disponible sur github. Il utilise le module smbus, qui permet de récupérer des données sur des capteurs, en utilisant le protocole I2C. Ce module est de base fait pour le capteur mpu6050 de chez InvenSense, mais fonctionne très bien sur le modèle mpu9150 pour récupérer les données sur l'accélération.

from mpu6050 import 6050

sensor = mpu6050(0x68)

Avec la deuxième ligne, je viens d'initialiser une instance de la classe mpu6050 importée plus haut du module mpu6050. Il faut lui passer en argument le port I2C de l'accéléromètre. Par défaut c'est le 0x68, mais vous pouvez vérifier en faisant la commande suivante dans le terminale.

i2cdetect -y 1
2.2.3.2 La boucle while True dans camsys

La section qui suit est mis dans un while True dans un try pour récupérer à chaque instant l'accélération.
Au début de la boucle, je fais appel à deux fonctions que j'ai définies. Je les explique dans les sections "les fonctions définies dans camsys" et "L'arrêt temporaire".

email_sender_reconnexion()
stop_time()


Pour récupérer les données de l'accéléromètre, il suffit d'utiliser la méthode "get_accel_data()" sur l'instance et attribuer la valeur dans une nouvelle variable.

accel_data = sensor.get_accel_data()

Puisque la valeur attribuée est un dictionnaire, nous pouvons préciser sur quel axe nous voulons récupérer l'accélération. Il suffit maintenant seulement de faire une structure conditionnelle pour dire qu'au-delà d'un seuil définit, le système est en mouvement. Avec le bout de code suivant :

if accel_data['x'] >= TRIGGER:
    ismoving = not ismoving

"ismoving" est de une variable booléenne qui est de base fausse.

note: Il faut changer le signe et/ou l'axe de surveillance selon comment le capteur est disposé dans le tiroir coulissant.

Après il faut lancer la caméra après la détection de mouvement.

import picamera
with picamera.PiCamera() as camera:
    camera.resolution = (1280, 960)
    camera.start_recording('videos/temporary.h264')
    camera.wait_recording(15)
    camera.stop_recording()

Ce bout de code ouvre la caméra et enregistre une vidéo nommée temporary.h264 en 1280x960 de 15 secondes dans un dossier nommé videos. Il faut ensuite convertir le fichier h264 en en mp4 avec comme nom, la date et l'heure actuelle. Pour ce faire nous utilisons une commande bash qui faut installer nommé MP4Box. Il est possible d'exécuter des commandes bash en python grâce au module subprocess

from subprocess import call
import os
command = "MP4Box -add videos/temporary.h264 videos/temporary.mp4"
call([command], shell=True)
commandsupp = "rm -rf videos/temporary.h264"
call([commandsupp], shell=True)
date = time_name()
name = "videos/" +  date  + ".mp4"
os.rename("videos/temporary.mp4", name)

Après avoir renommé et changer le format de la vidéo, il nous reste plus qu'à l'envoyer par email.
note : la fonction time_name() est expliquée plus tard dans le billet.
Il faut d'abord savoir si nous sommes connectés à internet. Pour cela, nous essayons de faire une requête vers un site web dans un bloc try/except.

import json
try:
    request = requests.get(url, timeout=timeout)
    email_sender(name)
    isconnected = True
except (requests.ConnectionError, requests.Timeout) as exception:
    with open(PATH_VIDEOSTOSEND, mode="r") as file:
        data = json.load(file)
        data.append(name)
    with open(PATH_VIDEOSTOSEND, mode='w') as file:
        json.dump(data, file, indent=4)
        isconnected = False

https://www.kite.com/python/answers/how-to-check-internet-connection-in-python
S'il est connecté, il va recevoir une réponse du site et va envoyé un email grâce à la fonction email_sender() ( je vais montrer plus tard dans le billet), s'il n'est pas connecté à internet, il va enregistrer le nom de la vidéo qu'il doit envoyer par email dans un fichier Json. Pour ce faire, il faut d'abord ouvrir le fichier Json, récupérer ce qu'il contient et puisqu'on le stocke sous forme d'une liste nous ajoutons seulement le nom de la vidéo filmée à l'instant et enfin on écrase le contenu du Json avec la nouvelle liste. A la fin de ce bloc conditionnel, il faut changer la valeur de ismoving en False.

2.2.3.3 Les fonctions définies dans camsys.py

La première fonction sert à mettre en pause pendant un certain temps donné par l'utilisateur via une interface web. J'expliquerai plus tard comment cela fonctionne, dans la section "L'arrêt temporaire".
La deuxième est la fonction qui sert à donner la date et l'heure comme nom pour les fichiers vidéos.

from datetime import datetime
def time_name():
     create_time = datetime.now()
     format_time_str = create_time.strftime("%d-%m-%Y_%H.%M.%S")
     new_file_name = format_time_str
     return new_file_name

Ce code récupère le temps actuel et le met sous un format Année-Mois-Jour_heure.minutes.secondes et le retourne la date et l'heure.
Il faut ensuite envoyer un email

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
mail_content = '''Une ouverture du tiroir a ete detecte'''
SENDER_ADDRESS = 'mailsender@mail.com'
SENDER_PASS = 'password'
receiver_address = 'receiver@mail.com'
def email_sender(name):
    global SENDER_ADDRESS, receiver_address, mail_content, SENDER_PASS
    message = MIMEMultipart()
    message['From'] = SENDER_ADDRESS
    message['To'] = receiver_address
    message['Subject'] = 'Intrusion detectee.'
    #The subject line
    #The body and the attachments for the mail
    message.attach(MIMEText(mail_content, 'plain'))
    attach_file_name = name
    with open(attach_file_name, 'rb') as attach_file :# Open the file as binary mode     
        payload = MIMEBase('application', 'octet-stream') 
        payload.set_payload((attach_file).read())
    encoders.encode_base64(payload) #encode the attachment
    #add payload header with filename
    payload.add_header('Content-Disposition', 'attachment; filename= %s .mp4' % (date) )
    message.attach(payload)
    text = message.as_string()
    #Create SMTP session for sending the mail  
    session = smtplib.SMTP('smtp.gmail.com', 587,)#use gmail with port
    session.set_debuglevel(1)
    session.ehlo()
    session.starttls()
    session.ehlo()
    session.login(SENDER_ADDRESS, SENDER_PASS) #login with mail_id and password
    session.sendmail(SENDER_ADDRESS, receiver_address, text)
    session.quit()

https://www.tutorialspoint.com/send-mail-with-attachment-from-your-gmail-account-using-python

Ce bout de code nous permet d'envoyer un email avec un fichier vidéo joint. Tout d'abord on crée une nouvelle instance de la classe MIMEMultipart(). On lui attribue des valeurs telles que l'objet de l'email. On attache à cette instance le texte et la vidéo. La vidéo est d'abord ouverte puis encodée. Elle est ensuite attaché en pièce jointe. Enfin, nous ouvrons une session gmail avec l'addresse email et le mot de passe du bot et nous envoyons l'email que nous venons de paramètrer au receveur.

note: Il faut désactiver la sécurité de connexion pour que le code puisse se connecter au compte email bot.

Il faut ensuite envoyer le vidéos qui n'ont pas pu être envoyées.

import json
def email_sender_reconnexion():
    global isconnected
    with open(PATH_VIDEOSTOSEND, mode='r') as file:
        videostosend = json.load(file)
    for name in videostosend:
        
        try:
            email_sender(name)
            isconnected = True

            
        except:
            isconnected = False
    if isconnected == True:
        with open(PATH_VIDEOSTOSEND, mode='w') as file:
            videostosend = []
            json.dump(videostosend, file)

Ce morceau de code ouvre le json avec une liste de noms de vidéos à envoyer. Je crée une boucle for pour chaque vidéo n'ayant pu être envoyé, j'essaie de les envoyer avec la fonction email_sender. Si cela échoue, cela veut dire que le raspberry pi n'est toujours pas connecté. Une fois qu'il aura tout envoyé, il ne faut pas oublier de supprimer le contenu de la liste dans le fichier json, pour ne pas réenvoyer.

2.2.3.4 Gestion des vidéos

Pour éviter que les vidéos saturent le stockage interne du rasperberry pi, il suffit de faire un cronjob qui toutes les vidéos âgées de x temps. Pour ce faire, il faut d'abord faire un script shell.

find videos/ -type f -name '*.mp4' -mtime +7 -exec rm {} \;

Ce morceau de code supprime toutes les vidéos se trouvant dans le dossier videos qui datent de plus de 7 jours. Il ne faut pas oublier de changer le mode du fichier pour qu'il puisse être exécuté. Il faut maintenant mettre en place le cronjob. Pour ajouter un cronjob, il faut simplement entrer crontab -e dans le terminal.

0 1 * * * chemin/absolu/autodeleter.sh

Ce cronjob va s'exécuter tous les matins à 1h et va exécuté le autodeleter.sh

2.2.3.5 L'arrêt temporaire

Il faut d'abord récupérer la durée d'arrêt. Pour ce faire, j'utilise une interface web avec la librairie Flask. Cette librairie est plus simple que Django, mais permet moins de choses. Pour accéder au site, il faut entrer l'addresse IP du raspberry pi et un / en étant connecté sur le même réseau local. Une fois qu'on est dessus, la page nous affiche une boîte de dialogue dans lequel on renseigne la durée d'arrêt en seconde. Dès que nous appuyons sur submit, nous faisons l'action POST.

Screenshot from 2022-02-03 22-52-48.png, fév. 2022
Figure 5 : Page web
from flask import Flask, render_template, url_for, request, redirect, session
import requests
import json
from markupsafe import escape
import time
from datetime import datetime

app = Flask(__name__)

PATH_HISTORY = 'stop_history.json'


@app.route('/', methods=['POST', 'GET'])
def index():
    if request.method == "POST":
        duration = request.form.get('seconds')
        global duration_int
        try :
            duration_int = int(duration)
            print("test int duration")
            try:
                with open(PATH_HISTORY, mode='r') as file:
                    data = json.load(file)
                data.append(duration_int)
                with open(PATH_HISTORY, mode='w') as file:
                    file.seek(0)
                    json.dump(data, file, indent=4)

                return redirect("/good/" + str(duration_int)) 
            except:
                return render_template("error_save.html")
            
        except:
            return redirect("/error")
    return render_template("index.html")
    
@app.route('/good/<duration>', methods=['POST', 'GET']) 
def good(duration):
    templateData = {'time': duration}
    return render_template("good.html", **templateData)


@app.route("/error")
def error():
    return render_template("error.html")


if __name__ == "__main__":
    app.run(host='0.0.0.0', port=80, debug=True)

Voici le script permettant de faire fonctionner la page. Nous définissons une page en indiquant quelle chemin absolu, on veut définir. Ensuite nous définissons une fonction dans lequel nous codons quel fichier html à afficher et récupérer les données entrées par l'utilisateur. Ainsi, dans def index(), dès que l'utlisateur valide sa durée, nous la récupérons et essayons de la transformer en variable numérique et nous la stockons dans un fichier json. Si cela échoue, nous redirigons l'utilsateur vers une erreur, soit parce que l'utilisateur n'a pas entré que des chiffres soit le fichier json est inutilisable. Enfin si cela s'est correctement passé, l'utilisateur est redirigé vers une page de confirmation.

note: Le script se trouve dans un fichier différent de camsys

Pour arrêter la capture de vidéo temporairement, j'utilise la fonction time.sleep() dans le fichier camsys. Elle a l'avantage d'être une fonction bloquante, c'est-à-dire qu'aucune autre ligne de code est executé. Il faut seulement rentrer le temps que nous voulons arrêter dans la fonction.

with open(PATH_LEN, mode='r') as file:
    data_len = json.load(file)
def stop_time():
    global data_len
    with open(PATH_HISTORY, mode='r') as file:
        data = json.load(file)  
    if data_len < len(data):
        stop_time = data[-1]
        time.sleep(stop_time)
        data_len = len(data)
finally:
    with open(PATH_LEN, mode='w') as file:
            json.dump(data_len, file)

On récupère tout d'abord la longueur de la liste d'arrêt qu'on a stocké dans un fichier json. Ensuite dans stop_time(), nous comparons le nombre d'itération d'arrêt stocké et la longueur actuelle du fichier json d'historique d'arrêt. Si sa longueur actuelle est plus grande, cela veut dire qu'une nouvelle demande d'arrêt à été rentrée par l'utilisateur et donc exécute l'arrêt temporaire. Le finally est là au cas ou le script bogue, il stocke le nombre de fois qu'il a fait l'arrêt.

3. Résultats

Le système fonctionne très bien. L'accéléromètre détecte correctement et la caméra filme correctement de jour. Cependant, il y'a un problème quand cela se passe de nuit, les LEDs infrarouges n'émettant pas assez de lumière, la caméra infrarouge ne peut filmer qu'à environ 10 cm. Le système envoie correctement les vidéos par email, mais j'ai remarqué que si nous essayons de télécharger la vidéo depuis l'application mail sur IPhone, le nom et l'extension est transformé en MiMe. Cela est embêtant pour connaître l'heure à laquelle l'ouverture s'est faite. Le système d'envoi des emails lorsque le raspberry pi n'était pas connecté fonctionne très bien. Le seul problème avec est si la caméra est déconnectée d'internet depuis plus de 7 jours et que le cronjob supprime la vidéo en question. Le système de désactivation fonctionne très bien.

4. Discussion

Le projet n'est pas tout à fait aboutit, il y'a plusieurs point à améliorer pour que le système soit véritablement utilisable. La première chose à améliorer serait l'interface web. Certes celle-ci est utilisable, mais la page n'est pas belle. On pourrait rajouter des images, de la couleur, etc. La deuxième est la pièce jointe qui n'affiche pas le nom et supprime l'extension. Il faudrait donner la date et l'heure de l'intrusion autrement, par exemple directement dans le contenu texte de l'email avec la méthode .format(). La troisième est qu'il faudrait aussi mettre une sécurité pour pouvoir mettre en stop, une demande de mot de passe par exemple.
Un gros point noir de ce système est son inefficacité quand il fait noir. Les LEDs ne produisent pas assez de lumière et cela empêche une utilisation nocturne, cela veut donc dire que la moitié du temps, le système est inutilisable. On doit aussi bien enfoncé le sunny chip et même en ayant bien enfoncé, j'ai eu un problème où les vidéos étaient noires, mais le problème s'est résolu tout seul.
Mais le plus gros point noir de ce projet est l'imposante taille que fait ce système. Le système n'est pas du tout discret et prend beaucoup de place inutilement. Il faudrait utilisé une mini breadboard, visser la caméra à quelque chose au minimum.
Je pense réellement que le système pourrait être utilisable si ces problèmes sont résolus, car il y a quand même des points positifs, comme la gestion des vidéos quand le système n'est plus connecté ou bien la batterie de secours en cas de coupure de courant.

5. Conclusion

Le système de caméra de surveillance fonctionne. C'est le plus important. Il reste des pistes d'amélioration, mais le système peut être utilisé pour surveiller des choses pas trop important.

Avec ce projet, j'ai fait face à plusieurs problèmes, face auxquelles je m'y étais préparé et d'autres non. J'ai appris beaucoup de subtilité de Python avec ce projet.

Références

https://fr.wikipedia.org/wiki/I2C

https://github.com/m-rtijn/mpu6050

https://www.kite.com/python/answers/how-to-check-internet-connection-in-python

https://www.tutorialspoint.com/send-mail-with-attachment-from-your-gmail-account-using-python