Compare commits
2 Commits
a6abcb7d9d
...
2dda633412
| Author | SHA1 | Date | |
|---|---|---|---|
| 2dda633412 | |||
| 9db1897af7 |
29
CLAUDE.md
29
CLAUDE.md
@ -8,7 +8,7 @@ un LLM les interprète et génère des commandes MAVLink vers ArduPilot.
|
|||||||
## Architecture
|
## Architecture
|
||||||
- **Jetson Orin Nano 8Go** : station sol, héberge WireClaw + SITL
|
- **Jetson Orin Nano 8Go** : station sol, héberge WireClaw + SITL
|
||||||
- **ESP32 DevKitV4** : relais WiFi↔UART (firmware PlatformIO sur Windows)
|
- **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
|
- **Matek H743** : à venir — contrôleur de vol ArduPilot réel
|
||||||
|
|
||||||
## Stack logicielle
|
## 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
|
- `.env` : secrets — NE JAMAIS MODIFIER NI COMMITTER
|
||||||
- `start.sh` : lance ArduCopter + MAVProxy + WireClaw automatiquement
|
- `start.sh` : lance ArduCopter + MAVProxy + WireClaw automatiquement
|
||||||
- `sitl_params.parm` : paramètres SITL (batterie désactivée)
|
- `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
|
## Ports réseau
|
||||||
- 5760 TCP : ArduCopter ↔ MAVProxy
|
- 5760 TCP : ArduCopter ↔ MAVProxy
|
||||||
- 14550 UDP : MAVProxy output standard + ESP32 source
|
- 14550 UDP : MAVProxy output standard + ESP32 source
|
||||||
- 14551 UDP : MAVSDK (WireClaw) + heartbeats ESP32
|
- 14551 UDP : MAVSDK (WireClaw) + heartbeats ESP32
|
||||||
- 14552 UDP : pymavlink (WireClaw)
|
- 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
|
## Réseau local
|
||||||
- Jetson Ethernet : 192.168.1.84
|
- Jetson Ethernet : 192.168.1.84
|
||||||
@ -59,6 +62,15 @@ nmcli dev status | grep wifi
|
|||||||
# Tuer Hermes si actif (occupe port 14550)
|
# Tuer Hermes si actif (occupe port 14550)
|
||||||
pkill -f hermes
|
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 commit + push
|
||||||
git add . && git commit -m "message" && git push origin master
|
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)
|
- `orbit` → orbite native (fallback waypoints en SITL)
|
||||||
|
|
||||||
## Prochaines étapes
|
## 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)
|
2. Câblage UART ESP32 ↔ Pi Zero (GPIO17→GPIO15, GPIO16←GPIO14, GND)
|
||||||
3. Flux vidéo GStreamer CSI → UDP → Jetson
|
3. ~~Préparation réception flux H.264 UDP Jetson~~ ✅ receive_cam.py prêt, port 5600→5601
|
||||||
4. Interface web FastAPI + HTML (flux vidéo + commandes)
|
4. Activer receive_cam.py quand Pi Zero remis en service + ouvrir port 5600 iptables
|
||||||
5. Matek H743 + ArduPilot réel (remplace SITL)
|
5. Intégrer MJPEG 5601 dans l'interface web (flux vidéo + commandes)
|
||||||
6. SkyDroid C12 (caméra thermique + gimbal 3 axes, 117g)
|
6. Matek H743 + ArduPilot réel (remplace SITL)
|
||||||
7. YOLOv8 détection anomalies thermiques
|
7. SkyDroid C12 (caméra thermique + gimbal 3 axes, 117g)
|
||||||
8. Whisper local (transcription vocale)
|
8. YOLOv8 détection anomalies thermiques
|
||||||
|
9. Whisper local (transcription vocale)
|
||||||
|
|
||||||
## Gitea
|
## Gitea
|
||||||
- WireClaw Python : git.syoul.fr/nicoboy/wireclaw
|
- WireClaw Python : git.syoul.fr/nicoboy/wireclaw
|
||||||
|
|||||||
114
receive_cam.py
Normal file
114
receive_cam.py
Normal 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()
|
||||||
Reference in New Issue
Block a user