feat: réception flux H.264 Pi Zero → MJPEG Jetson

- receive_cam.py : pipeline GStreamer UDP 5600 → MJPEG HTTP 5601
- CLAUDE.md : Pi Zero IP, ports 5600/5601, commandes de session caméra

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nicoboy
2026-06-08 18:50:59 +02:00
parent 9db1897af7
commit 2dda633412
2 changed files with 135 additions and 8 deletions

View File

@ -8,7 +8,7 @@ un LLM les interprète et génère des commandes MAVLink vers ArduPilot.
## Architecture
- **Jetson Orin Nano 8Go** : station sol, héberge WireClaw + SITL
- **ESP32 DevKitV4** : relais WiFi↔UART (firmware PlatformIO sur Windows)
- **Pi Zero 2W** : à venir — flux vidéo caméra + relais série vers Matek H743
- **Pi Zero 2W** : IP 192.168.1.132 — flux vidéo H.264 UDP → Jetson (câblé, arrêté pour l'instant)
- **Matek H743** : à venir — contrôleur de vol ArduPilot réel
## Stack logicielle
@ -25,12 +25,15 @@ un LLM les interprète et génère des commandes MAVLink vers ArduPilot.
- `.env` : secrets — NE JAMAIS MODIFIER NI COMMITTER
- `start.sh` : lance ArduCopter + MAVProxy + WireClaw automatiquement
- `sitl_params.parm` : paramètres SITL (batterie désactivée)
- `receive_cam.py` : réception H.264 UDP port 5600 → MJPEG port 5601 (Pi Zero)
## Ports réseau
- 5760 TCP : ArduCopter ↔ MAVProxy
- 14550 UDP : MAVProxy output standard + ESP32 source
- 14551 UDP : MAVSDK (WireClaw) + heartbeats ESP32
- 14552 UDP : pymavlink (WireClaw)
- 5600 UDP : flux H.264 RTP entrant depuis Pi Zero (caméra OV5647)
- 5601 TCP : MJPEG stream exposé par receive_cam.py (→ YOLOv8, interface web)
## Réseau local
- Jetson Ethernet : 192.168.1.84
@ -59,6 +62,15 @@ nmcli dev status | grep wifi
# Tuer Hermes si actif (occupe port 14550)
pkill -f hermes
# Lancer la réception caméra Pi Zero (quand Pi Zero actif)
python3 ~/wireclaw/receive_cam.py
# Vérifier MJPEG stream caméra
curl -s --max-time 2 http://localhost:5601/ | head -1
# Pipeline GStreamer brute (debug)
gst-launch-1.0 udpsrc port=5600 ! application/x-rtp,payload=96 ! rtph264depay ! avdec_h264 ! videoconvert ! fakesink
# Git commit + push
git add . && git commit -m "message" && git push origin master
```
@ -73,14 +85,15 @@ git add . && git commit -m "message" && git push origin master
- `orbit` → orbite native (fallback waypoints en SITL)
## Prochaines étapes
1. Réception Pi Zero 2W + caméra OV5647 (en attente livraison)
1. ~~Réception Pi Zero 2W + caméra OV5647~~ ✅ Pi Zero IP 192.168.1.132, reçu et testé
2. Câblage UART ESP32 ↔ Pi Zero (GPIO17→GPIO15, GPIO16←GPIO14, GND)
3. Flux vidéo GStreamer CSI → UDP Jetson
4. Interface web FastAPI + HTML (flux vidéo + commandes)
5. Matek H743 + ArduPilot réel (remplace SITL)
6. SkyDroid C12 (caméra thermique + gimbal 3 axes, 117g)
7. YOLOv8 détection anomalies thermiques
8. Whisper local (transcription vocale)
3. ~~Préparation réception flux H.264 UDP Jetson~~ ✅ receive_cam.py prêt, port 5600→5601
4. Activer receive_cam.py quand Pi Zero remis en service + ouvrir port 5600 iptables
5. Intégrer MJPEG 5601 dans l'interface web (flux vidéo + commandes)
6. Matek H743 + ArduPilot réel (remplace SITL)
7. SkyDroid C12 (caméra thermique + gimbal 3 axes, 117g)
8. YOLOv8 détection anomalies thermiques
9. Whisper local (transcription vocale)
## Gitea
- WireClaw Python : git.syoul.fr/nicoboy/wireclaw

114
receive_cam.py Normal file
View File

@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""
Réception flux H.264 UDP depuis Pi Zero (port 5600) via GStreamer.
Exposition MJPEG sur port 5601 pour consommation par YOLOv8 et l'interface web.
"""
import cv2
import threading
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
# Pipeline GStreamer : UDP RTP H.264 → frames décodées
GSTREAMER_PIPELINE = (
"udpsrc port=5600 "
"! application/x-rtp,payload=96 "
"! rtph264depay "
"! avdec_h264 "
"! videoconvert "
"! video/x-raw,format=BGR "
"! appsink drop=true max-buffers=1"
)
MJPEG_PORT = 5601
MJPEG_QUALITY = 70 # Compromis qualité/débit pour le réseau local
# Frame partagée entre le thread GStreamer et le serveur MJPEG
_frame_lock = threading.Lock()
_current_frame = None
_running = True
def capture_loop():
"""Lit les frames depuis GStreamer et met à jour _current_frame."""
global _current_frame, _running
cap = cv2.VideoCapture(GSTREAMER_PIPELINE, cv2.CAP_GSTREAMER)
if not cap.isOpened():
print("[CAM] Impossible d'ouvrir le pipeline GStreamer — Pi Zero connecté ?")
_running = False
return
print("[CAM] Pipeline GStreamer ouvert, réception flux...")
while _running:
ret, frame = cap.read()
if not ret:
print("[CAM] Flux perdu, nouvelle tentative...")
time.sleep(1)
continue
with _frame_lock:
_current_frame = frame
cap.release()
print("[CAM] Pipeline fermé.")
class MJPEGHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
pass # Silencieux pour ne pas polluer les logs WireClaw
def do_GET(self):
if self.path != "/":
self.send_error(404)
return
self.send_response(200)
self.send_header("Content-Type", "multipart/x-mixed-replace; boundary=frame")
self.end_headers()
while _running:
with _frame_lock:
frame = _current_frame
if frame is None:
time.sleep(0.05)
continue
_, jpeg = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, MJPEG_QUALITY])
data = jpeg.tobytes()
try:
self.wfile.write(
f"--frame\r\nContent-Type: image/jpeg\r\nContent-Length: {len(data)}\r\n\r\n".encode()
+ data
+ b"\r\n"
)
self.wfile.flush()
except (BrokenPipeError, ConnectionResetError):
break
time.sleep(1 / 30) # ~30 fps max
def main():
global _running
# Thread de capture GStreamer
t = threading.Thread(target=capture_loop, daemon=True)
t.start()
server = HTTPServer(("0.0.0.0", MJPEG_PORT), MJPEGHandler)
print(f"[CAM] MJPEG stream sur http://0.0.0.0:{MJPEG_PORT}/")
try:
server.serve_forever()
except KeyboardInterrupt:
pass
finally:
_running = False
server.shutdown()
print("[CAM] Arrêt.")
if __name__ == "__main__":
main()