266 lines
7.9 KiB
Python
266 lines
7.9 KiB
Python
import os
|
|
import cv2
|
|
import numpy as np
|
|
import threading
|
|
import socket
|
|
# from flask import Flask, render_template, send_from_directory
|
|
from flask import Flask, render_template, send_from_directory, request, redirect, url_for, session, jsonify
|
|
from collections import deque
|
|
|
|
TX_POWER = -40
|
|
ENV_FACTOR = 2
|
|
WINDOW_SIZE = 5
|
|
|
|
esp_data = {}
|
|
latest_observations = {}
|
|
esp_positions = {}
|
|
position_history = deque(maxlen=WINDOW_SIZE)
|
|
trilat_points = []
|
|
|
|
|
|
# === CONFIG ===
|
|
UDP_IP = "0.0.0.0"
|
|
UDP_PORT = 5000
|
|
BUFFER_SIZE = 65535
|
|
IMAGE_DIR = "static/streams"
|
|
|
|
# === FLASK SECRET KEY ===
|
|
SECRET_KEY = "change_this_for_prod"
|
|
SECRET_TOKEN = b"Sup3rS3cretT0k3n"
|
|
app = Flask(__name__)
|
|
app.secret_key = SECRET_KEY
|
|
|
|
|
|
# === CRÉATION DES DOSSIERS ===
|
|
os.makedirs(IMAGE_DIR, exist_ok=True)
|
|
|
|
udp_thread = None
|
|
flask_thread = None
|
|
udp_sock = None
|
|
flask_server = None
|
|
stop_udp_event = threading.Event()
|
|
|
|
VIDEO_PATH = "static/streams/record.avi"
|
|
video_writer = None
|
|
video_writer_fps = 10 # images/seconde
|
|
video_writer_size = None
|
|
|
|
|
|
@app.route("/login", methods=["GET", "POST"])
|
|
def login():
|
|
error = None
|
|
if request.method == "POST":
|
|
username = request.form.get("username")
|
|
password = request.form.get("password")
|
|
if username == "admin" and password == "admin":
|
|
session["logged_in"] = True
|
|
return redirect(url_for("index"))
|
|
else:
|
|
error = "Identifiants invalides."
|
|
return render_template("login.html", error=error)
|
|
|
|
@app.route("/logout")
|
|
def logout():
|
|
session.pop("logged_in", None)
|
|
return redirect(url_for("login"))
|
|
|
|
@app.route("/")
|
|
def index():
|
|
if not session.get("logged_in"):
|
|
return redirect(url_for("login"))
|
|
# Liste les images disponibles
|
|
image_files = sorted([f for f in os.listdir(IMAGE_DIR) if f.endswith(".jpg")])
|
|
if not image_files:
|
|
image_files = ["waiting_for_camera"]
|
|
return render_template("index.html", image_files=image_files)
|
|
|
|
@app.route("/streams/<filename>")
|
|
def stream_image(filename):
|
|
return send_from_directory(IMAGE_DIR, filename)
|
|
|
|
from flask import request, jsonify
|
|
|
|
@app.route("/api/trilateration", methods=["POST"])
|
|
def trilateration_api():
|
|
# Accepte du texte brut : Content-Type: text/plain
|
|
if request.content_type == "text/plain":
|
|
raw_data = request.data.decode("utf-8").strip()
|
|
print(f"[RAW POST] {raw_data}")
|
|
handle_trilateration_data(raw_data)
|
|
return jsonify({"status": "OK"})
|
|
|
|
# Sinon : rejet explicite
|
|
return "Unsupported Media Type", 415
|
|
|
|
|
|
@app.route("/trilateration")
|
|
def trilateration_view():
|
|
point = get_latest_position()
|
|
return render_template("trilateration.html", point=point, anchors=esp_positions)
|
|
|
|
|
|
|
|
|
|
# === UDP Server pour recevoir les flux ===
|
|
def udp_receiver():
|
|
global udp_sock, video_writer, video_writer_size
|
|
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
udp_sock.bind((UDP_IP, UDP_PORT))
|
|
print(f"[UDP] En écoute sur {UDP_IP}:{UDP_PORT}")
|
|
|
|
while not stop_udp_event.is_set():
|
|
try:
|
|
udp_sock.settimeout(1.0)
|
|
try:
|
|
data, addr = udp_sock.recvfrom(BUFFER_SIZE)
|
|
except socket.timeout:
|
|
continue
|
|
|
|
# Vérifie le token
|
|
if not data.startswith(SECRET_TOKEN):
|
|
print(f"[SECURITE] Token invalide de {addr}")
|
|
continue
|
|
data = data[len(SECRET_TOKEN):] # Retire le token
|
|
|
|
npdata = np.frombuffer(data, np.uint8)
|
|
frame = cv2.imdecode(npdata, cv2.IMREAD_COLOR)
|
|
|
|
if frame is not None:
|
|
print(f"[DEBUG] Frame shape: {frame.shape}")
|
|
filename = f"{addr[0]}_{addr[1]}.jpg"
|
|
filepath = os.path.join(IMAGE_DIR, filename)
|
|
cv2.imwrite(filepath, frame)
|
|
|
|
# --- Ajout pour l'enregistrement vidéo ---
|
|
if video_writer is None:
|
|
video_writer_size = (frame.shape[1], frame.shape[0])
|
|
fourcc = cv2.VideoWriter_fourcc(*'MJPG')
|
|
video_writer = cv2.VideoWriter(VIDEO_PATH, fourcc, video_writer_fps, video_writer_size)
|
|
if not video_writer.isOpened():
|
|
print("[ERREUR] VideoWriter n'a pas pu être ouvert !")
|
|
|
|
if video_writer is not None:
|
|
video_writer.write(frame)
|
|
|
|
except Exception as e:
|
|
print(f"[ERREUR] {e}")
|
|
if video_writer is not None:
|
|
video_writer.release()
|
|
video_writer = None
|
|
udp_sock.close()
|
|
udp_sock = None
|
|
|
|
def rssi_to_distance(rssi):
|
|
return 10 ** ((TX_POWER - rssi) / (10 * ENV_FACTOR))
|
|
|
|
def trilaterate(p1, r1, p2, r2, p3, r3):
|
|
x1, y1 = p1
|
|
x2, y2 = p2
|
|
x3, y3 = p3
|
|
|
|
A = 2 * (x2 - x1)
|
|
B = 2 * (y2 - y1)
|
|
C = r1**2 - r2**2 - x1**2 + x2**2 - y1**2 + y2**2
|
|
|
|
D = 2 * (x3 - x2)
|
|
E = 2 * (y3 - y2)
|
|
F = r2**2 - r3**2 - x2**2 + x3**2 - y2**2 + y3**2
|
|
|
|
denominator = A * E - B * D
|
|
if denominator == 0:
|
|
raise Exception("Trilatération impossible")
|
|
|
|
x = (C * E - B * F) / denominator
|
|
y = (A * F - C * D) / denominator
|
|
|
|
return (x, y)
|
|
|
|
|
|
def handle_trilateration_data(text):
|
|
global latest_observations, trilat_points, position_history, esp_positions
|
|
|
|
try:
|
|
esp_id, coord_str, rssi_str = text.split(";")
|
|
x_str, y_str = coord_str.split(",")
|
|
x, y = float(x_str), float(y_str)
|
|
rssi = float(rssi_str)
|
|
dist = rssi_to_distance(rssi)
|
|
|
|
latest_observations[esp_id] = ((x, y), dist)
|
|
esp_positions[esp_id] = (x, y, dist) # ✅ essentiel pour affichage
|
|
|
|
print(f"[OBS] Reçu de {esp_id} → pos=({x}, {y}), rssi={rssi}, dist={dist:.2f}")
|
|
|
|
if len(latest_observations) >= 3:
|
|
anchors = list(latest_observations.values())[:3]
|
|
pos = trilaterate(*anchors[0], *anchors[1], *anchors[2])
|
|
position_history.append(pos)
|
|
|
|
avg_x = sum(p[0] for p in position_history) / len(position_history)
|
|
avg_y = sum(p[1] for p in position_history) / len(position_history)
|
|
trilat_points = [(avg_x, avg_y)]
|
|
|
|
print(f"[TRILAT] Moyenne position : ({avg_x:.3f}, {avg_y:.3f})")
|
|
except Exception as e:
|
|
print(f"[ERREUR PARSING] {text} → {e}")
|
|
|
|
|
|
def get_latest_position():
|
|
return trilat_points[-1] if trilat_points else None
|
|
|
|
|
|
|
|
|
|
def _run_flask():
|
|
app.run(host="0.0.0.0", port=8000, debug=False, use_reloader=False)
|
|
|
|
def start_cam_server():
|
|
global udp_thread, flask_thread, stop_udp_event
|
|
if udp_thread and udp_thread.is_alive():
|
|
print("[INFO] UDP server déjà démarré.")
|
|
else:
|
|
stop_udp_event.clear()
|
|
udp_thread = threading.Thread(target=udp_receiver, daemon=True)
|
|
udp_thread.start()
|
|
print("[INFO] UDP server démarré.")
|
|
|
|
if flask_thread and flask_thread.is_alive():
|
|
print("[INFO] Flask server déjà démarré.")
|
|
else:
|
|
flask_thread = threading.Thread(target=_run_flask, daemon=True)
|
|
flask_thread.start()
|
|
print("[INFO] Flask server démarré sur http://0.0.0.0:8000.")
|
|
|
|
|
|
def stop_cam_server():
|
|
global stop_udp_event, udp_sock, video_writer
|
|
print("[INFO] Arrêt du serveur UDP...")
|
|
stop_udp_event.set()
|
|
if udp_sock:
|
|
try:
|
|
udp_sock.close()
|
|
except Exception:
|
|
pass
|
|
udp_sock = None
|
|
if video_writer is not None:
|
|
video_writer.release()
|
|
print("[DEBUG] VideoWriter released")
|
|
video_writer = None
|
|
print("[INFO] Serveur UDP arrêté.")
|
|
|
|
# Suppression des fichiers .jpg
|
|
for f in os.listdir(IMAGE_DIR):
|
|
if f.endswith(".jpg"):
|
|
try:
|
|
os.remove(os.path.join(IMAGE_DIR, f))
|
|
except Exception as e:
|
|
print(f"[ERREUR] Impossible de supprimer {f}: {e}")
|
|
|
|
if __name__ == "__main__":
|
|
start_cam_server()
|
|
try:
|
|
while True:
|
|
pass # Le serveur Flask et le thread UDP fonctionnent en arrière-plan
|
|
except KeyboardInterrupt:
|
|
stop_cam_server()
|
|
print("[INFO] Serveur arrêté.") |