Blog
Raspberry Pi + Arduino : Système de Surveillance IoT Complet en Tunisie
Raspberry Pi + Arduino : Système de Surveillance IoT Complet en Tunisie
Architecture hybride Pi + Arduino pour la surveillance IoT en Tunisie : OpenCV, PIR, Telegram, Home Assistant, ML face recognition. Code Python + Arduino complet.
Sfax, mars 2026. Hamza, étudiant en 5ème année à l’INSAT, défend son PFE devant le jury. Son projet : un système de surveillance domotique complet pour la villa familiale, capable de détecter une intrusion, identifier les membres de la famille par reconnaissance faciale, déclencher une sirène, prévenir les voisins via Telegram, et tout journaliser dans un Home Assistant accessible depuis n’importe où dans le monde. Coût total des composants : moins de 350 dinars. Coût de l’équivalent commercial (Ring Pro + abonnement + capteurs Aqara + alarme Verisure) : plus de 4000 dinars. La différence ? Hamza a marié un Raspberry Pi 4 et un Arduino UNO dans une architecture hybride bien pensée. Voyons comment reproduire ce système chez vous, étape par étape.
📋 Table des matières
- Pourquoi pas un seul microcontrôleur ?
- Architecture hybride Pi + Arduino
- Composants et budget complet
- Communication Pi ↔ Arduino : USB Serial ou I2C
- Code Arduino : capteurs et actionneurs temps réel
- Code Python Pi : OpenCV, Telegram, Flask
- Enregistrement vidéo segmenté
- Accès distant : SSH, ngrok, IPv6
- Intégration Home Assistant
- ML face recognition pour la famille
- Sécurité réseau et hardening
- FAQ Pi + Arduino surveillance
Pourquoi pas un seul microcontrôleur ?
La question est légitime. Pourquoi compliquer avec 2 cartes au lieu d’utiliser un ESP32 unique, ou un Raspberry Pi seul ? Réponse : séparation des tâches selon les forces de chaque plateforme.
Forces du Raspberry Pi
- Linux complet → OpenCV, Python, Docker, Home Assistant tournent nativement
- Wi-Fi 5 + Bluetooth + Ethernet Gigabit
- 4-8 Go de RAM pour traiter de la vidéo HD
- Stockage SD/USB illimité pour archiver des vidéos
- SSH, web server, accès distant — tout l’arsenal cloud-native
Faiblesses du Raspberry Pi
- GPIO 3.3V uniquement (le 5V grille le SoC)
- Pas de timer hardware déterministe (Linux n’est pas temps réel)
- Boot 30-60 secondes (catastrophique pour une alarme qui doit démarrer en 100 ms)
- Sensible aux coupures de courant (corruption SD)
- Une seule interruption GPIO bloque potentiellement le système
Forces de l’Arduino UNO
- Microcontrôleur ATmega328P pur — pas d’OS, démarrage instantané
- Pins 5V tolérants → directement compatibles avec PIR, capteurs Hall, relais 5V
- Interruptions hardware déterministes
- Pas de risque de corruption à l’arrêt brutal
- Consommation ultra-faible (~20 mA)
Faiblesses de l’Arduino
- 2 Ko de SRAM — pas question d’OpenCV ou de TensorFlow
- Pas de Wi-Fi natif
- Pas de système de fichiers
La conclusion s’impose : Arduino fait le travail temps réel (lire PIR, déclencher sirène, gérer relais), Pi fait le travail intelligent (analyser la vidéo, notifier, archiver, exposer une API).
Architecture hybride : la division des tâches
Voici le schéma fonctionnel du système :
┌───────────────────────────────────────┐
│ RASPBERRY PI 4 (Linux) │
│ │
│ Pi Camera v3 ──► OpenCV ──► Motion │
│ │ Detect │
│ ▼ │
│ Telegram Notify │
│ │ │
│ Home Assistant │
│ │ │
│ Flask Dashboard │
└───────────────┬───────────────────────┘
│ USB Serial 115200
│ JSON messages
▼
┌───────────────────────────────────────┐
│ ARDUINO UNO (temps réel) │
│ │
│ PIR HC-SR501 ──► Détection IR │
│ Reed switch ──► Porte ouverte ? │
│ Buzzer ◄── Alerte sonore │
│ Relais 5V ◄── Sirène 12V │
│ LED RGB ◄── État système │
└───────────────────────────────────────┘
Le Pi est le « cerveau » et exécute la logique métier (algorithmes, notifications, web). L’Arduino est le « bras armé » qui interface avec le monde physique en temps réel.
Composants et budget complet
| Composant | Référence | Prix DT (Didactico) |
| Raspberry Pi 4 4GB | RPi 4B 4GB | 180 DT |
| Alim Pi USB-C 5V/3A | Officielle | 25 DT |
| microSD 64 Go classe A1 | SanDisk Ultra | 30 DT |
| Pi Camera v3 | Module v3 Wide | 90 DT |
| Arduino UNO R3 | Officiel ou clone | 35-65 DT |
| Câble USB-A vers USB-B | Standard | 5 DT |
| Capteur PIR HC-SR501 | 5V, portée 7m | 8 DT |
| Capteur magnétique porte | Reed switch | 6 DT |
| Buzzer 5V passif | Piézo | 3 DT |
| Module relais 5V 1 canal | SRD-05VDC | 8 DT |
| Sirène 12V 110 dB | Alarme classique | 25 DT |
| Boîtier impression 3D | PLA Bambu Lab A1 | ~5 DT filament |
| TOTAL | ~420 DT |
Communication Pi ↔ Arduino
Deux protocoles dominants :
Option A : USB Serial 115200 baud (recommandé)
- Simple : un câble USB-A vers USB-B, et c’est fini
- Alimente l’Arduino (5V) en même temps
- Le Pi voit l’Arduino comme
/dev/ttyACM0ou/dev/ttyUSB0 - Pas de problème de niveaux logiques (l’USB s’occupe de tout)
- Protocole : JSON ligne par ligne
Option B : I2C avec Pi en master
- Plus rapide pour transactions courtes (1 ms vs 8 ms USB Serial)
- Permet plusieurs Arduino sur le même bus
- MAIS nécessite un convertisseur de niveau 3.3V ↔ 5V obligatoire
- Complexité supplémentaire — réservez à l’option B aux cas spécifiques
Pour ce projet, on choisit l’option A USB Serial. Plus simple, plus robuste, suffisamment rapide.
Protocole JSON ligne par ligne
# Messages Arduino → Pi (1 ligne JSON par événement) :
{"type":"pir","value":1,"ts":12345}
{"type":"door","value":"open","ts":12350}
{"type":"heartbeat","uptime":3600}
# Messages Pi → Arduino :
{"cmd":"siren","state":"on","duration":30}
{"cmd":"led","color":"red"}
{"cmd":"buzzer","freq":440,"ms":200}
Code Arduino : capteurs et actionneurs temps réel
// surveillance-arduino.ino
#include <ArduinoJson.h>
const int PIR_PIN = 2; // Interruption hardware
const int DOOR_PIN = 3; // Reed switch (pull-up interne)
const int BUZZER_PIN = 8;
const int RELAY_PIN = 9;
const int LED_R = 5;
const int LED_G = 6;
const int LED_B = 7;
volatile bool pirTriggered = false;
bool sirenActive = false;
unsigned long sirenUntil = 0;
unsigned long lastHeartbeat = 0;
void pirISR() { pirTriggered = true; }
void setup() {
Serial.begin(115200);
pinMode(PIR_PIN, INPUT);
pinMode(DOOR_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(RELAY_PIN, OUTPUT);
pinMode(LED_R, OUTPUT);
pinMode(LED_G, OUTPUT);
pinMode(LED_B, OUTPUT);
attachInterrupt(digitalPinToInterrupt(PIR_PIN), pirISR, RISING);
// LED verte = système OK
digitalWrite(LED_G, HIGH);
}
void sendEvent(const char* type, const char* value) {
StaticJsonDocument<128> doc;
doc["type"] = type;
doc["value"] = value;
doc["ts"] = millis();
serializeJson(doc, Serial);
Serial.println();
}
void checkCommands() {
if (Serial.available()) {
StaticJsonDocument<256> doc;
DeserializationError err = deserializeJson(doc, Serial);
if (err) return;
const char* cmd = doc["cmd"];
if (strcmp(cmd, "siren") == 0) {
const char* state = doc["state"];
if (strcmp(state, "on") == 0) {
int duration = doc["duration"] | 30;
digitalWrite(RELAY_PIN, HIGH);
sirenActive = true;
sirenUntil = millis() + duration * 1000UL;
digitalWrite(LED_R, HIGH);
digitalWrite(LED_G, LOW);
} else {
digitalWrite(RELAY_PIN, LOW);
sirenActive = false;
digitalWrite(LED_R, LOW);
digitalWrite(LED_G, HIGH);
}
} else if (strcmp(cmd, "buzzer") == 0) {
int freq = doc["freq"] | 440;
int ms = doc["ms"] | 200;
tone(BUZZER_PIN, freq, ms);
}
}
}
void loop() {
// 1. Capteur PIR (interruption ISR)
if (pirTriggered) {
pirTriggered = false;
sendEvent("pir", "1");
}
// 2. Capteur porte (lecture polling - changement d'état)
static int lastDoor = HIGH;
int curDoor = digitalRead(DOOR_PIN);
if (curDoor != lastDoor) {
sendEvent("door", curDoor == LOW ? "open" : "closed");
lastDoor = curDoor;
}
// 3. Timeout sirène automatique (sécurité)
if (sirenActive && millis() > sirenUntil) {
digitalWrite(RELAY_PIN, LOW);
sirenActive = false;
digitalWrite(LED_R, LOW);
digitalWrite(LED_G, HIGH);
}
// 4. Heartbeat toutes les 30 s
if (millis() - lastHeartbeat > 30000) {
StaticJsonDocument<128> doc;
doc["type"] = "heartbeat";
doc["uptime"] = millis() / 1000;
serializeJson(doc, Serial);
Serial.println();
lastHeartbeat = millis();
}
// 5. Lire commandes du Pi
checkCommands();
}
Code Python Pi : OpenCV, Telegram, Flask
Côté Pi, on a 3 tâches en parallèle (lancées via threads ou processus) :
- Lecture série Arduino et réaction aux événements
- Analyse vidéo OpenCV en continu (motion detection)
- Dashboard Flask exposant le live stream et l’historique
Lecture série + Telegram
# surveillance_pi.py
import serial
import json
import requests
import threading
import time
from datetime import datetime
TELEGRAM_TOKEN = "votre_bot_token"
TELEGRAM_CHAT = "votre_chat_id"
SERIAL_PORT = "/dev/ttyACM0"
ser = serial.Serial(SERIAL_PORT, 115200, timeout=1)
time.sleep(2) # Reset Arduino
def notify_telegram(text, photo_path=None):
"""Envoie un message ou une photo via Telegram bot."""
base = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}"
if photo_path:
with open(photo_path, "rb") as f:
requests.post(
f"{base}/sendPhoto",
data={"chat_id": TELEGRAM_CHAT, "caption": text},
files={"photo": f},
timeout=10
)
else:
requests.post(
f"{base}/sendMessage",
data={"chat_id": TELEGRAM_CHAT, "text": text},
timeout=10
)
def send_arduino(cmd):
"""Envoie une commande JSON à l'Arduino."""
ser.write((json.dumps(cmd) + "n").encode())
def listen_arduino():
"""Thread d'écoute série permanent."""
while True:
line = ser.readline().decode(errors="ignore").strip()
if not line:
continue
try:
event = json.loads(line)
except json.JSONDecodeError:
continue
ts = datetime.now().strftime("%H:%M:%S")
print(f"[{ts}] {event}")
if event["type"] == "pir":
# Mouvement détecté → capture photo et notify
from camera import capture_snapshot
snap = capture_snapshot()
notify_telegram(f"🚨 Mouvement à {ts}", snap)
# Déclenche sirène 30 s
send_arduino({"cmd": "siren", "state": "on", "duration": 30})
elif event["type"] == "door" and event["value"] == "open":
notify_telegram(f"🚪 Porte ouverte à {ts}")
if __name__ == "__main__":
threading.Thread(target=listen_arduino, daemon=True).start()
while True:
time.sleep(60)
Détection de mouvement OpenCV (camera.py)
# camera.py
import cv2
import numpy as np
from picamera2 import Picamera2
from datetime import datetime
picam = Picamera2()
config = picam.create_video_configuration(main={"size": (1280, 720)})
picam.configure(config)
picam.start()
prev_frame = None
def capture_snapshot():
"""Capture une photo et retourne le chemin."""
path = f"/tmp/snap_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jpg"
frame = picam.capture_array()
cv2.imwrite(path, frame)
return path
def detect_motion():
"""Boucle OpenCV de détection de mouvement (différence frames)."""
global prev_frame
while True:
frame = picam.capture_array()
gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
gray = cv2.GaussianBlur(gray, (21, 21), 0)
if prev_frame is None:
prev_frame = gray
continue
delta = cv2.absdiff(prev_frame, gray)
thresh = cv2.threshold(delta, 25, 255, cv2.THRESH_BINARY)[1]
thresh = cv2.dilate(thresh, None, iterations=2)
contours, _ = cv2.findContours(
thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for c in contours:
if cv2.contourArea(c) < 5000: # Ignore petits mouvements
continue
# Mouvement significatif détecté
snap = capture_snapshot()
from surveillance_pi import notify_telegram
notify_telegram("📹 Mouvement caméra", snap)
break
prev_frame = gray
Dashboard Flask
# dashboard.py
from flask import Flask, Response, render_template_string
import cv2
app = Flask(__name__)
HTML = """
<h1>🏠 Surveillance Didactico</h1>
<img src="/stream" width="800">
<p>Statut Arduino : {{ status }}</p>
"""
@app.route("/")
def index():
return render_template_string(HTML, status="OK")
def generate_stream():
from camera import picam
while True:
frame = picam.capture_array()
_, jpg = cv2.imencode(".jpg", frame)
yield (b"--framernContent-Type: image/jpegrnrn" +
jpg.tobytes() + b"rn")
@app.route("/stream")
def stream():
return Response(generate_stream(),
mimetype="multipart/x-mixed-replace; boundary=frame")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, threaded=True)
Enregistrement vidéo segmenté
Plutôt qu’un seul énorme fichier vidéo, on découpe en segments de 10 minutes via ffmpeg. Stockage sur SD interne ou disque USB.
# Capture continue segmentée avec libcamera + ffmpeg
libcamera-vid -t 0 --inline --width 1280 --height 720
--framerate 25 --codec h264 -o - |
ffmpeg -i - -c copy -f segment -segment_time 600
-reset_timestamps 1 -strftime 1
/mnt/usb/cam/cam-%Y%m%d-%H%M%S.mp4
# Nettoyage automatique des vidéos > 14 jours :
find /mnt/usb/cam/ -name "*.mp4" -mtime +14 -delete
# Ajouter au crontab :
0 3 * * * find /mnt/usb/cam/ -name "*.mp4" -mtime +14 -delete
Accès distant : SSH, ngrok, IPv6
Trois options pour accéder à votre Pi depuis l’extérieur :
1. ngrok (le plus simple)
# Installation :
curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc |
sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null
echo "deb https://ngrok-agent.s3.amazonaws.com buster main" |
sudo tee /etc/apt/sources.list.d/ngrok.list
sudo apt update && sudo apt install ngrok
ngrok config add-authtoken VOTRE_TOKEN
# Exposer le dashboard Flask :
ngrok http 5000
# → https://xxxx.ngrok-free.app accessible depuis n'importe où
2. IPv6 natif (Topnet, Orange Tunisie)
Si votre FAI fournit du IPv6 natif (Topnet le fait depuis 2022), chaque appareil de votre maison a une IP publique unique IPv6 directement joignable. Ouvrez les bons ports dans le firewall :
sudo ufw allow 22/tcp # SSH
sudo ufw allow 5000/tcp # Flask dashboard
sudo ufw enable
# Trouver votre IPv6 :
ip -6 addr show wlan0 | grep global
3. WireGuard VPN (le plus sécurisé)
Monter un VPN WireGuard sur le Pi, et se connecter depuis le téléphone. Aucun port ouvert vers Internet, traffic chiffré point-à-point. Tutoriel complet sur le wiki WireGuard.
Intégration Home Assistant
En bonus, on peut faire remonter tous les événements PIR/porte/sirène dans Home Assistant via MQTT :
# config Home Assistant - configuration.yaml
mqtt:
binary_sensor:
- name: "PIR Entrée"
state_topic: "didactico/pir"
payload_on: "1"
payload_off: "0"
device_class: motion
- name: "Porte Entrée"
state_topic: "didactico/door"
payload_on: "open"
payload_off: "closed"
device_class: door
switch:
- name: "Sirène"
command_topic: "didactico/siren/set"
state_topic: "didactico/siren/state"
payload_on: "on"
payload_off: "off"
# Automation HA : siren + lumière rouge si mouvement la nuit
automation:
- alias: "Intrusion nocturne"
trigger:
platform: state
entity_id: binary_sensor.pir_entree
to: "on"
condition:
condition: time
after: "23:00:00"
before: "06:00:00"
action:
- service: switch.turn_on
entity_id: switch.sirene
- service: light.turn_on
data:
entity_id: light.salon
color_name: red
ML face recognition pour la famille
Pour éviter les fausses alarmes (la famille rentre du travail), on ajoute de la reconnaissance faciale. La librairie face_recognition (basée sur dlib) tourne en CPU sur Pi 4.
# Installation (long, ~30 min sur Pi 4) :
sudo apt install -y cmake libopenblas-dev liblapack-dev
pip3 install face_recognition opencv-python
# Apprentissage des visages :
mkdir -p /home/didactico/faces/{papa,maman,hamza,sara}
# Mettre 3-5 photos par personne dans chaque dossier
# encode_faces.py
import face_recognition
import os
import pickle
known = {"encodings": [], "names": []}
for person in os.listdir("/home/didactico/faces"):
for f in os.listdir(f"/home/didactico/faces/{person}"):
img = face_recognition.load_image_file(
f"/home/didactico/faces/{person}/{f}")
encs = face_recognition.face_encodings(img)
if encs:
known["encodings"].append(encs[0])
known["names"].append(person)
with open("faces.pkl", "wb") as f:
pickle.dump(known, f)
# Reconnaissance live :
import pickle
import face_recognition
import cv2
with open("faces.pkl", "rb") as f:
known = pickle.load(f)
def identify(frame):
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
locations = face_recognition.face_locations(rgb)
encodings = face_recognition.face_encodings(rgb, locations)
names = []
for enc in encodings:
matches = face_recognition.compare_faces(
known["encodings"], enc, tolerance=0.5)
name = "INCONNU"
if True in matches:
idx = matches.index(True)
name = known["names"][idx]
names.append(name)
return names
# Dans la boucle de détection :
names = identify(frame)
if "INCONNU" in names:
notify_telegram(f"⚠️ Visage inconnu détecté !")
send_arduino({"cmd": "siren", "state": "on", "duration": 60})
else:
notify_telegram(f"👋 {', '.join(names)} rentre à la maison")
Sécurité réseau et hardening
Checklist hardening Pi
- Changer le mot de passe par défaut (pi/raspberry est interdit en 2026)
- Authentification SSH par clé uniquement (désactiver password) :
# Sur votre PC : ssh-keygen -t ed25519 -f ~/.ssh/dida_pi ssh-copy-id -i ~/.ssh/dida_pi.pub didactico@pi.local # Sur le Pi, /etc/ssh/sshd_config : PasswordAuthentication no PermitRootLogin no PubkeyAuthentication yes sudo systemctl restart ssh - fail2ban pour bloquer les attaques bruteforce :
sudo apt install -y fail2ban sudo nano /etc/fail2ban/jail.local # Ajouter : # [sshd] # enabled = true # maxretry = 3 # bantime = 3600 sudo systemctl enable --now fail2ban - HTTPS sur le dashboard Flask (Let’s Encrypt via nginx + certbot)
- Updates automatiques (unattended-upgrades) :
sudo apt install -y unattended-upgrades sudo dpkg-reconfigure unattended-upgrades - Backup quotidien de la SD (image disque vers NAS ou cloud chiffré)
Raspberry Pi 4 + Arduino UNO
Le combo parfait pour vos projets de surveillance, domotique et PFE IoT. Tous les composants disponibles chez Didactico Sfax avec livraison 24-48h partout en Tunisie.
FAQ Pi + Arduino surveillance
Pourquoi un Arduino quand un ESP32 ferait les deux à la fois ?
Un ESP32 est plus puissant qu’un UNO, mais il reste limité (520 Ko RAM, pas de Linux). Pour la vision OpenCV, le ML face recognition et Home Assistant, il faut absolument un Pi. L’Arduino sert juste à offloader le temps réel critique (PIR, sirène, watchdog). Vous pouvez remplacer l’Arduino UNO par un ESP32 si vous voulez ajouter du WiFi/MQTT direct côté capteurs.
Quelle distance maximum entre Pi et Arduino en USB ?
Câble USB standard : 5 mètres. Au-delà, prévoyez un répéteur actif USB (~30 DT) ou passez en RS485 différentiel (compatible jusqu’à 100 m, mais demande des modules MAX485 sur les 2 cartes).
Le PIR HC-SR501 a-t-il beaucoup de faux positifs ?
Le HC-SR501 est sensible aux courants d’air chauds, aux insectes, à la lumière directe du soleil. Réglez les 2 potentiomètres (sensibilité et délai), positionnez-le à l’abri du soleil direct, et combinez toujours avec une confirmation vidéo avant de déclencher la sirène. Pour un cas critique (banque, bijouterie), passez sur un capteur PIR dual avec compensation thermique.
Combien de FPS en détection de mouvement OpenCV sur Pi 4 ?
Avec une caméra 1280×720, 15-25 FPS sans accélération GPU, parfait pour la surveillance. Pour de la reconnaissance faciale temps réel, ça tombe à 2-5 FPS — suffisant pour un usage domestique mais pas en streaming live. Si vous voulez plus, passez sur un Pi 5 ou un mini-PC x86 avec Coral USB.
La caméra Pi v3 voit-elle dans le noir ?
Non, c’est une caméra couleur sans IR. Pour la vision nocturne, prenez la Pi NoIR Camera (sans filtre infrarouge) + un projecteur IR 850 nm. Comptez 130-180 DT supplémentaires.
Stocker les vidéos en cloud plutôt que sur SD ?
Oui, plusieurs options : Backblaze B2 (~6 dollars/To/mois), AWS S3 Glacier Deep Archive (~1 dollar/To/mois), ou self-hosted MinIO sur un VPS Hetzner (~5 euros/mois pour 80 Go). Configurez rclone sur le Pi pour synchroniser /mnt/usb/cam toutes les heures.
Le système consomme combien d’électricité ?
Pi 4 (~5W) + Arduino UNO (~0.5W) + Pi Camera (~0.5W) + sirène en veille (négligeable) = environ 6W en moyenne, soit ~0.144 kWh/jour. À 250 millimes le kWh STEG, ça représente moins de 1.1 DT par mois.
Conclusion
L’architecture hybride Raspberry Pi + Arduino n’est pas un compromis bâtard : c’est la séparation idéale des préoccupations en IoT moderne. Le Pi orchestre, le Arduino agit. Cette répartition vous permet d’obtenir un système de surveillance de niveau commercial pour le prix d’une soirée pizza, tout en gardant le contrôle total sur vos données (pas de cloud propriétaire, pas d’abonnement mensuel, pas de fuite vers des serveurs étrangers).
Pour les étudiants de l’INSAT, ENIT, ISBS et ISET, ce projet constitue une base solide de PFE qui peut s’étendre vers l’IA embarquée (TensorFlow Lite sur Pi), la sécurité offensive (pentest WiFi avec le même matériel), ou l’IoT industriel (passerelle LoRaWAN). Tous les composants sont en stock chez Didactico Sfax avec livraison 24-48h partout en Tunisie.
Pour aller plus loin, consultez nos guides Raspberry Pi Zero et 5 projets Raspberry Pi débutants.