commit 24dbc7cd6abacb6fde165dc0ba8ac434e798ec81 Author: Debian Date: Sat Mar 14 21:26:06 2026 +0100 Initial commit — Serveur Lucas SmartEye API réception alertes chute (SmartEye/YOLO), analyse IA (Gemini 2.5 Flash), gestion alertes avec escalade (watchdog), notifications Firebase, dashboard web, documentation MkDocs. Co-Authored-By: Claude Opus 4.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bfc47cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# === SECRETS & CREDENTIALS === +admin_key.json +lucas_config.ini +database.json + +# === DONNÉES RUNTIME === +clients/ +uploads/ +alerts/ +photos/ +error_python.log +sms_history.log +last_analysis_report.json + +# === FICHIERS TEMPORAIRES === +__pycache__/ +*.pyc +*.bak + +# === IDE === +.vscode/ +.claude/ +lucas.code-workspace + +# === IMAGES === +*.jpg +!placeholder.jpg + +# === MKDOCS BUILD === +mkdocs-smarteye/site/ + +# === ANCIENS RAPPORTS (historique, pas du code) === +CHANGELOG_v2.0.md +CORRECTIF_TECHNIQUE.md +GEMINI_FIX_README.md +RAPPORT_FINAL_COMPLET.md diff --git a/API_AUTH_DOC.md b/API_AUTH_DOC.md new file mode 100644 index 0000000..4e1926f --- /dev/null +++ b/API_AUTH_DOC.md @@ -0,0 +1,181 @@ +# Documentation Authentification LucasApp + +## Réponses aux questions + +### 1. Quel endpoint doit recevoir/vérifier ce mot de passe ? + +**Endpoint : `https://lucas.unigest.fr/api/auth.php`** + +**Méthode :** POST +**Content-Type :** application/json + +**Corps de la requête :** +```json +{ + "client_id": "Demo_01", + "password": "75a9d2db3919", + "fcm_token": "fheHxyPRQlS7..." (optionnel) +} +``` + +**Réponse succès (200) :** +```json +{ + "status": "success", + "message": "Authentification réussie", + "data": { + "token": "secret123", + "client_name": "Demo_01", + "senior_name": "Mamie Lucas", + "senior_nickname": "mamie", + "senior_photo": "photos/mamie.jpg", + "latitude": "42.1028", + "longitude": "9.5147", + "emergency_number": "15", + "contacts": [...], + "site_status": "active" + } +} +``` + +**Réponses erreur :** +- **400** : Identifiant ou mot de passe manquant +- **403** : Identifiant inconnu / Mot de passe incorrect / Compte non configuré +- **500** : Erreur serveur + +--- + +### 2. Le mot de passe est-il haché côté serveur (bcrypt, SHA256) ou comparé en clair ? + +**Réponse : BCRYPT** + +- Le mot de passe est **haché avec bcrypt** (`password_hash($password, PASSWORD_BCRYPT)`) +- Le hash est stocké dans `database.json` sous le champ `password_hash` +- La vérification utilise `password_verify($password, $password_hash)` +- **Sécurité maximale** : même si database.json est compromis, les mots de passe ne peuvent pas être déchiffrés + +--- + +### 3. Le flux voulu : le serveur génère le mot de passe et le donne au client, ou le client le choisit et le serveur le stocke ? + +**Réponse : SERVEUR GÉNÈRE (recommandé)** + +**Flux principal (production) :** +1. L'admin exécute `php generate_password.php Demo_01` +2. Le serveur génère un mot de passe aléatoire (12 caractères) +3. Le serveur affiche le mot de passe en clair (une seule fois) +4. L'admin le communique au client (QR code, SMS, email) +5. Le client le saisit dans LucasApp +6. L'app l'utilise pour s'authentifier via `/api/auth.php` + +**Flux alternatif (si besoin) :** +- L'admin peut définir un mot de passe personnalisé avec `php set_password.php Demo_01 MonMotDePasse` +- Utile si le client veut choisir son propre mot de passe + +--- + +## Intégration dans LucasApp + +### Étape 1 : Configuration initiale +Quand l'utilisateur ouvre le dialog "Configurer le compte" : +- Demander `client_id` (ID du compte) +- Demander `password` (mot de passe fourni par l'admin) + +### Étape 2 : Authentification +```dart +Future _authenticate() async { + final response = await http.post( + Uri.parse('https://lucas.unigest.fr/api/auth.php'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'client_id': _clientId, + 'password': _clientPassword, + 'fcm_token': await FirebaseMessaging.instance.getToken(), + }), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + final token = data['data']['token']; + + // Stocker le token + infos client dans SharedPreferences + await _prefs.setString('client_token', token); + await _prefs.setString('client_name', data['data']['client_name']); + await _prefs.setString('senior_name', data['data']['senior_name']); + // ... etc + } else { + // Erreur d'authentification + _showError('Identifiant ou mot de passe incorrect'); + } +} +``` + +### Étape 3 : Enregistrement FCM token +Le token FCM est **automatiquement enregistré** lors de l'authentification si vous le passez dans le corps de la requête. + +**Pas besoin d'appel séparé à `/api/register`** — tout se fait en une seule requête. + +--- + +## Architecture complète + +### SmartEye (Jetson) → Lucas +- **Endpoint :** `/api.php` +- **Auth :** `client_id` + `token` (en POST) +- **Usage :** Upload image de détection de chute +- **Inchangé** — SmartEye continue d'utiliser le token directement + +### LucasApp (Flutter) → Lucas +- **Endpoint :** `/api/auth.php` +- **Auth :** `client_id` + `password` (en JSON) +- **Usage :** Authentification + récupération du token +- **Nouveau** — LucasApp utilise le mot de passe pour obtenir le token + +### Sécurité +- **Mot de passe** : stocké haché (bcrypt) dans database.json +- **Token** : stocké en clair (car utilisé par SmartEye en POST) +- **Le mot de passe NE TRANSITE JAMAIS en clair** sur le réseau (HTTPS obligatoire) + +--- + +## Commandes admin + +### Générer un mot de passe aléatoire +```bash +sudo -u www-data php /var/www/lucas/admin/generate_password.php Demo_01 +``` + +### Définir un mot de passe personnalisé +```bash +sudo -u www-data php /var/www/lucas/admin/set_password.php Demo_01 MonMotDePasse123 +``` + +### Vérifier les mots de passe configurés +```bash +grep -A 2 '"name":' /var/www/lucas/database.json | grep -E '"name"|password_hash' +``` + +--- + +## Exemple de test curl + +```bash +curl -X POST https://lucas.unigest.fr/api/auth.php \ + -H "Content-Type: application/json" \ + -d '{ + "client_id": "Demo_01", + "password": "75a9d2db3919" + }' +``` + +**Réponse attendue :** +```json +{ + "status": "success", + "message": "Authentification réussie", + "data": { + "token": "secret123", + ... + } +} +``` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..33ecc94 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,10 @@ +# Instructions Claude Code + +## Autonomie +- Travaille de manière autonome sans interruption. Ne pose jamais de question à l'utilisateur sauf s'il le demande explicitement. +- En cas de doute ou d'ambiguïté, fais le choix le plus raisonnable et note-le dans ta réponse finale. +- Si une commande échoue, tente une alternative plutôt que de demander quoi faire. +- Ne demande jamais confirmation avant d'éditer/créer des fichiers — c'est déjà autorisé dans les permissions. + +## Langue +- Communique toujours en français. diff --git a/acknowledge.php b/acknowledge.php new file mode 100755 index 0000000..6b54c59 --- /dev/null +++ b/acknowledge.php @@ -0,0 +1,31 @@ +"error"])); +$db = json_decode(file_get_contents($json_file), true); + +// 2. Récupérer les infos +$client_in = $_REQUEST['client'] ?? ''; +$token_in = $_REQUEST['token'] ?? ''; +$user = $_REQUEST['user'] ?? 'Inconnu'; + +// 3. Trouver le client +$idx = -1; +foreach ($db['clients'] as $i => $c) { + if (strcasecmp($c['name'], $client_in)==0 && $c['token']===$token_in) { + $idx = $i; break; + } +} +if ($idx === -1) die(json_encode(["status"=>"error"])); + +// 4. JUSTE METTRE À JOUR LE STATUT (On ne touche pas aux images !) +// On garde 'alerte' = true, mais on ajoute 'handled_by' +$db['clients'][$idx]['handled_by'] = $user; +$db['clients'][$idx]['handled_at'] = date("H:i"); + +file_put_contents($json_file, json_encode($db, JSON_PRETTY_PRINT)); + +echo json_encode(["status"=>"success"]); +?> diff --git a/admin.php b/admin.php new file mode 100755 index 0000000..ac0c299 --- /dev/null +++ b/admin.php @@ -0,0 +1,482 @@ +Admin Login + +
+
+

SMARTEYE LUCAS

+
+ + +
+ '.(isset($error)?'

'.$error.'

':'').' +
'; + exit; +} + +// --- CHARGEMENT DES DONNÉES --- +$data = file_exists($DB_FILE) ? json_decode(file_get_contents($DB_FILE), true) : ['clients' => []]; +// Sécurité : structure minimale si fichier vide ou corrompu +if (!isset($data['clients'])) $data['clients'] = []; + +// --- FONCTION TEST ALERTE (SIMULATION SMS) --- +if (isset($_GET['test_alert'])) { + $idTest = $_GET['test_alert']; + if (isset($data['clients'][$idTest])) { + $client = $data['clients'][$idTest]; + $contacts = $client['contacts'] ?? []; + + $count = 0; + $mobiles = []; + + // ICI : Simulation de l'appel API SMS + // Dans le futur api.php, on utilisera ces numéros + foreach($contacts as $c) { + if(!empty($c['phone'])) { + $mobiles[] = $c['phone']; + $count++; + // LOG l'envoi pour preuve + $log_msg = date("Y-m-d H:i:s") . " [TEST ADMIN] SMS vers {$c['phone']} pour {$client['name']}\n"; + file_put_contents("sms_history.log", $log_msg, FILE_APPEND); + } + } + + $successMsg = "✅ Test d'alerte simulé envoyé à $count contacts !
(" . implode(', ', $mobiles) . ")"; + } +} + +// --- SAUVEGARDE CLIENT (AJOUT / EDIT) --- +if (isset($_POST['save_client'])) { + // Nettoyage de l'ID pour éviter les caractères spéciaux + $id = preg_replace('/[^a-z0-9_]/', '', strtolower($_POST['client_id'])); + + // Initialisation si nouveau + if (!isset($data['clients'][$id])) { + $data['clients'][$id]['created_at'] = date('Y-m-d H:i:s'); + $data['clients'][$id]['alerte'] = false; + // Création dossier images + $client_dir = "clients/" . $_POST['name'] . "/"; + if (!is_dir($client_dir)) { mkdir($client_dir, 0775, true); } + } + + // Token : saisi manuellement, ou auto-généré si vide + $token_input = trim($_POST['token'] ?? ''); + if (!empty($token_input)) { + $data['clients'][$id]['token'] = $token_input; + } elseif (!isset($data['clients'][$id]['token'])) { + $data['clients'][$id]['token'] = bin2hex(random_bytes(4)); + } + + // Mot de passe LucasApp (bcrypt) — ignorer si valeur inchangée (bullets) + $new_password = trim($_POST['client_password'] ?? ''); + if (!empty($new_password) && $new_password !== str_repeat("\xE2\x80\xA2", 8) && $new_password !== '••••••••') { + $data['clients'][$id]['password_hash'] = password_hash($new_password, PASSWORD_BCRYPT); + } + + // Mise à jour des champs + $data['clients'][$id]['name'] = $_POST['name']; + $data['clients'][$id]['address'] = $_POST['address']; + $data['clients'][$id]['city'] = $_POST['city']; + $data['clients'][$id]['phone_mobile'] = $_POST['phone_mobile']; + $data['clients'][$id]['phone_fixed'] = $_POST['phone_fixed']; + $data['clients'][$id]['cp'] = $_POST['cp'] ?? ''; + $data['clients'][$id]['date_naissance'] = $_POST['date_naissance'] ?? ''; + $data['clients'][$id]['sex'] = $_POST['sex']; + + // Champs bénéficiaire + $data['clients'][$id]['senior_name'] = $_POST['senior_name'] ?? ''; + $data['clients'][$id]['senior_nickname'] = $_POST['senior_nickname'] ?? ''; + $data['clients'][$id]['senior_photo'] = $_POST['senior_photo'] ?? ''; + if (!isset($data['clients'][$id]['fcm_tokens'])) { + $data['clients'][$id]['fcm_tokens'] = []; + } + + // Champs compte / technique + $data['clients'][$id]['jetson_id'] = $_POST['jetson_id'] ?? ''; + $data['clients'][$id]['jetson_ip'] = $_POST['jetson_ip'] ?? ''; + $data['clients'][$id]['jetson_modele'] = $_POST['jetson_modele'] ?? ''; + $data['clients'][$id]['jetson_ram'] = $_POST['jetson_ram'] ?? ''; + $data['clients'][$id]['jetson_ssd'] = $_POST['jetson_ssd'] ?? ''; + + // Caméras (5 max) + $cameras = []; + if (isset($_POST['cam_marque'])) { + for ($i = 0; $i < count($_POST['cam_marque']); $i++) { + if (!empty($_POST['cam_marque'][$i]) || !empty($_POST['cam_ip'][$i])) { + $cameras[] = [ + 'marque' => $_POST['cam_marque'][$i] ?? '', + 'modele' => $_POST['cam_modele'][$i] ?? '', + 'serie' => $_POST['cam_serie'][$i] ?? '', + 'ip' => $_POST['cam_ip'][$i] ?? '', + 'port_rtsp' => $_POST['cam_port_rtsp'][$i] ?? '', + ]; + } + } + } + $data['clients'][$id]['cameras'] = $cameras; + + // Gestion des contacts multiples + $contacts = []; + if (isset($_POST['contact_name'])) { + for ($i = 0; $i < count($_POST['contact_name']); $i++) { + if (!empty($_POST['contact_name'][$i])) { + $contacts[] = [ + 'name' => $_POST['contact_name'][$i], + 'role' => $_POST['contact_role'][$i], + 'phone' => $_POST['contact_phone'][$i], + 'email' => $_POST['contact_email'][$i], + 'os' => $_POST['contact_os'][$i] ?? '' + ]; + } + } + } + $data['clients'][$id]['contacts'] = $contacts; + + // Écriture DB + file_put_contents($DB_FILE, json_encode($data, JSON_PRETTY_PRINT)); + $tab = $_POST['current_tab'] ?? 'identite'; + header("Location: admin.php?edit=$id&success=1&tab=$tab"); exit; +} + +// --- ARRÊT ALERTE --- +if (isset($_GET['stop_alert'])) { + $stopId = $_GET['stop_alert']; + if (isset($data['clients'][$stopId])) { + $data['clients'][$stopId]['alerte'] = false; + unset($data['clients'][$stopId]['handled_by']); + unset($data['clients'][$stopId]['handled_at']); + unset($data['clients'][$stopId]['message']); + unset($data['clients'][$stopId]['last_update']); + file_put_contents($DB_FILE, json_encode($data, JSON_PRETTY_PRINT)); + // Reload data after save + $data = json_decode(file_get_contents($DB_FILE), true); + } + header("Location: admin.php?alert_stopped=1"); exit; +} + +// --- SUPPRESSION --- +if (isset($_GET['del'])) { + unset($data['clients'][$_GET['del']]); + file_put_contents($DB_FILE, json_encode($data, JSON_PRETTY_PRINT)); + header("Location: admin.php"); exit; +} + +// --- PRÉPARATION VUE --- +$editClient = null; $editId = ""; +if (isset($_GET['edit'])) { + $editId = $_GET['edit']; + $editClient = $data['clients'][$editId] ?? null; +} elseif (isset($_GET['new'])) { + // Mode nouveau client + $editClient = null; +} +?> + + + + + + + SmartEye Admin + + + + + + + + +
+ + +
✅ Modifications enregistrées avec succès !
+ + + +
+ + + +
✅ Alerte arrêtée avec succès — Le client est revenu en surveillance active.
+ + + +
+ +
+ + + + + + + +
+ + + +
+ +
+ + +
+
+
+ + +
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + +
+ Annuler + +
+ +
+
+ +
+ + +
+
+

Parc Clients Surveillés

+ + Nouveau Dossier +
+ + +
+

📭

+

Aucun client enregistré.

+

Cliquez sur "Nouveau Dossier" pour commencer.

+
+ + + + + $client): ?> + + + + + + + + + +
BénéficiaireLocalisationContactsStatutActions
+
+
ID:
+
+
+ +
+
+ +
+ +
+
+
+ + ALERTE EN COURS + + Surveillance Active + + +
+ + + + 📱 + 🔔 + Éditer + Voir + × +
+
+ +
+ +
+ + diff --git a/admin/generate_password.php b/admin/generate_password.php new file mode 100644 index 0000000..1ba7fce --- /dev/null +++ b/admin/generate_password.php @@ -0,0 +1,58 @@ + + * Génère un mot de passe aléatoire, le hash, et met à jour database.json + */ + +if (php_sapi_name() !== 'cli') { + die("Ce script doit être exécuté en ligne de commande.\n"); +} + +if ($argc < 2) { + echo "Usage : php generate_password.php \n"; + echo "Exemple : php generate_password.php Demo_01\n"; + exit(1); +} + +$client_id = $argv[1]; +$json_file = dirname(__DIR__) . '/database.json'; + +if (!file_exists($json_file)) { + die("Erreur : database.json introuvable\n"); +} + +$db = json_decode(file_get_contents($json_file), true); + +// Recherche du client +$client_index = -1; +if (isset($db['clients'])) { + foreach ($db['clients'] as $index => $c) { + if (strcasecmp($c['name'], $client_id) == 0) { + $client_index = $index; + break; + } + } +} + +if ($client_index === -1) { + die("Erreur : Client '$client_id' introuvable dans la base de données\n"); +} + +// Génération mot de passe aléatoire (12 caractères : lettres + chiffres) +$password = bin2hex(random_bytes(6)); // 12 caractères hexadécimaux +$password_hash = password_hash($password, PASSWORD_BCRYPT); + +// Mise à jour de la base +$db['clients'][$client_index]['password_hash'] = $password_hash; +file_put_contents($json_file, json_encode($db, JSON_PRETTY_PRINT)); + +// Affichage +echo "✅ Mot de passe généré avec succès pour '$client_id'\n"; +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; +echo "📱 Mot de passe : $password\n"; +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; +echo "\n⚠️ Communiquez ce mot de passe au client de manière sécurisée.\n"; +echo " Le hash bcrypt a été enregistré dans database.json\n\n"; +?> diff --git a/admin/set_password.php b/admin/set_password.php new file mode 100644 index 0000000..4468462 --- /dev/null +++ b/admin/set_password.php @@ -0,0 +1,54 @@ + + * Hash un mot de passe fourni et met à jour database.json + */ + +if (php_sapi_name() !== 'cli') { + die("Ce script doit être exécuté en ligne de commande.\n"); +} + +if ($argc < 3) { + echo "Usage : php set_password.php \n"; + echo "Exemple : php set_password.php Demo_01 MonMotDePasse123\n"; + exit(1); +} + +$client_id = $argv[1]; +$password = $argv[2]; +$json_file = dirname(__DIR__) . '/database.json'; + +if (!file_exists($json_file)) { + die("Erreur : database.json introuvable\n"); +} + +$db = json_decode(file_get_contents($json_file), true); + +// Recherche du client +$client_index = -1; +if (isset($db['clients'])) { + foreach ($db['clients'] as $index => $c) { + if (strcasecmp($c['name'], $client_id) == 0) { + $client_index = $index; + break; + } + } +} + +if ($client_index === -1) { + die("Erreur : Client '$client_id' introuvable dans la base de données\n"); +} + +// Hash du mot de passe +$password_hash = password_hash($password, PASSWORD_BCRYPT); + +// Mise à jour de la base +$db['clients'][$client_index]['password_hash'] = $password_hash; +file_put_contents($json_file, json_encode($db, JSON_PRETTY_PRINT)); + +// Affichage +echo "✅ Mot de passe défini avec succès pour '$client_id'\n"; +echo " Le hash bcrypt a été enregistré dans database.json\n\n"; +?> diff --git a/alert_ack.php b/alert_ack.php new file mode 100644 index 0000000..d42255c --- /dev/null +++ b/alert_ack.php @@ -0,0 +1,105 @@ + false, "error" => "Méthode non autorisée"]); + exit; +} + +// Lire le body JSON +$input = json_decode(file_get_contents('php://input'), true); + +if (!$input || !isset($input['alert_id']) || !isset($input['status'])) { + http_response_code(400); + echo json_encode(["success" => false, "error" => "Paramètres manquants : alert_id et status requis"]); + exit; +} + +$alert_id = trim($input['alert_id']); +$status = strtoupper(trim($input['status'])); +$detail = isset($input['detail']) ? trim($input['detail']) : null; + +// Valider le statut +$statuts_valides = ['DELIVERED', 'SEEN', 'ACKNOWLEDGED', 'RESOLVED']; +if (!in_array($status, $statuts_valides)) { + http_response_code(400); + echo json_encode([ + "success" => false, + "error" => "Statut invalide. Valeurs acceptées : " . implode(', ', $statuts_valides) + ]); + exit; +} + +// Valider le format de l'alert_id (UUID) +if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $alert_id)) { + http_response_code(400); + echo json_encode(["success" => false, "error" => "Format alert_id invalide (UUID attendu)"]); + exit; +} + +// Appeler le script Python pour mettre à jour le statut +$python_script = '/var/www/lucas/alert_ack_handler.py'; + +$cmd_args = escapeshellarg($alert_id) . ' ' . escapeshellarg($status); +if ($detail) { + $cmd_args .= ' ' . escapeshellarg($detail); +} + +$command = "/usr/bin/python3 {$python_script} {$cmd_args} 2>&1"; +$output = shell_exec($command); + +if ($output === null) { + http_response_code(500); + echo json_encode(["success" => false, "error" => "Erreur exécution script Python"]); + exit; +} + +// Le script Python retourne du JSON +$result = json_decode(trim($output), true); + +if ($result === null) { + http_response_code(500); + echo json_encode([ + "success" => false, + "error" => "Réponse Python invalide", + "raw" => substr($output, 0, 200) + ]); + exit; +} + +http_response_code($result['success'] ? 200 : 404); +echo json_encode($result); \ No newline at end of file diff --git a/alert_ack_handler.py b/alert_ack_handler.py new file mode 100644 index 0000000..eeccf0f --- /dev/null +++ b/alert_ack_handler.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +""" +SmartEye SENTINEL - Handler d'Acquittement +============================================ +Appelé par alert_ack.php pour mettre à jour le statut d'une alerte. + +Usage : python3 alert_ack_handler.py [detail] + +Auteur : Unigest Solutions / SmartEye V30 +""" + +import sys +import json + +sys.path.insert(0, '/var/www/lucas') +from alert_manager import mettre_a_jour_statut, get_alerte, AlertStatus + +def main(): + if len(sys.argv) < 3: + print(json.dumps({"success": False, "error": "Usage: alert_ack_handler.py [detail]"})) + sys.exit(1) + + alert_id = sys.argv[1] + status = sys.argv[2].upper() + detail = sys.argv[3] if len(sys.argv) > 3 else None + + # Vérifier que l'alerte existe + alerte = get_alerte(alert_id) + if not alerte: + print(json.dumps({ + "success": False, + "error": f"Alerte {alert_id} non trouvée (peut-être déjà résolue ou expirée)" + })) + sys.exit(0) + + # Mettre à jour + result = mettre_a_jour_statut(alert_id, status, detail) + + if result: + print(json.dumps({ + "success": True, + "message": f"Alerte {alert_id[:8]}... mise à jour : {status}", + "alert_id": alert_id, + "new_status": status, + "previous_status": alerte.get("status", "?") + })) + else: + print(json.dumps({ + "success": False, + "error": f"Impossible de passer au statut {status} (statut actuel: {alerte.get('status', '?')})" + })) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/alert_manager.py b/alert_manager.py new file mode 100644 index 0000000..07ff0e0 --- /dev/null +++ b/alert_manager.py @@ -0,0 +1,328 @@ +""" +SmartEye SENTINEL - Gestionnaire d'Alertes avec Acquittement +============================================================= +Gère le cycle de vie complet d'une alerte : + PENDING → DELIVERED → SEEN → ACKNOWLEDGED + +Tant qu'une alerte n'est pas au minimum SEEN, le watchdog la renverra. + +Auteur : Unigest Solutions / SmartEye V30 +""" + +import json +import os +import time +import fcntl +from datetime import datetime, timedelta +from enum import Enum + +# --- CONFIGURATION --- +ALERTS_DIR = "/var/www/lucas/alerts" +ALERTS_DB = os.path.join(ALERTS_DIR, "alerts_active.json") +ALERTS_HISTORY = os.path.join(ALERTS_DIR, "alerts_history.json") +LOCK_FILE = os.path.join(ALERTS_DIR, ".alerts.lock") + +# Stratégie d'escalade (en secondes après la création) +ESCALATION_SCHEDULE = [ + {"delay": 120, "label": "Rappel 1", "priority": "high"}, + {"delay": 300, "label": "Rappel 2", "priority": "critical"}, + {"delay": 600, "label": "Rappel 3", "priority": "critical"}, + {"delay": 1200, "label": "Rappel URGENT", "priority": "emergency"}, + {"delay": 1800, "label": "ESCALADE MAX", "priority": "emergency"}, +] + +# Durée max avant qu'une alerte soit considérée comme expirée (2h) +ALERT_MAX_AGE = 7200 + + +class AlertStatus: + PENDING = "PENDING" # Créée, notification envoyée + DELIVERED = "DELIVERED" # Firebase confirme la livraison + SEEN = "SEEN" # L'app a été ouverte, alerte affichée + ACKNOWLEDGED = "ACKNOWLEDGED" # L'utilisateur a appuyé "J'ai vu" / "J'appelle" + RESOLVED = "RESOLVED" # Situation traitée (fausse alerte ou secours envoyés) + EXPIRED = "EXPIRED" # Timeout sans réponse (après escalade max) + + +def _ensure_dir(): + """Crée le répertoire d'alertes s'il n'existe pas""" + os.makedirs(ALERTS_DIR, exist_ok=True) + + +def _load_alerts(): + """Charge les alertes actives depuis le fichier JSON avec verrou""" + _ensure_dir() + if not os.path.exists(ALERTS_DB): + return {} + try: + with open(ALERTS_DB, 'r') as f: + return json.load(f) + except (json.JSONDecodeError, IOError): + return {} + + +def _save_alerts(alerts): + """Sauvegarde les alertes avec verrou fichier (concurrence safe)""" + _ensure_dir() + lock_fd = None + try: + lock_fd = open(LOCK_FILE, 'w') + fcntl.flock(lock_fd, fcntl.LOCK_EX) + + with open(ALERTS_DB, 'w') as f: + json.dump(alerts, f, indent=2, default=str) + finally: + if lock_fd: + fcntl.flock(lock_fd, fcntl.LOCK_UN) + lock_fd.close() + + +def _archive_alert(alert): + """Archive une alerte terminée dans l'historique""" + _ensure_dir() + history = [] + if os.path.exists(ALERTS_HISTORY): + try: + with open(ALERTS_HISTORY, 'r') as f: + history = json.load(f) + except: + history = [] + + alert["archived_at"] = datetime.now().isoformat() + history.append(alert) + + # Garder max 500 alertes en historique + if len(history) > 500: + history = history[-500:] + + with open(ALERTS_HISTORY, 'w') as f: + json.dump(history, f, indent=2, default=str) + + +# ============================================= +# API PUBLIQUE +# ============================================= + +def creer_alerte(alert_id, image_path, camera_id=None, audio_relay_ws=None, camera_ip_map=None, analyse_data=None): + """ + Crée une nouvelle alerte et l'enregistre comme PENDING. + Appelée par analyze.py après détection d'une chute. + + Returns: dict avec les infos de l'alerte créée + """ + alerts = _load_alerts() + + now = datetime.now() + + alerte = { + "alert_id": alert_id, + "status": AlertStatus.PENDING, + "image_path": image_path, + "camera_id": camera_id or "cam_default", + "audio_relay_ws": audio_relay_ws or "ws://57.128.74.87:8800", + "camera_ip_map": camera_ip_map or "{}", + "created_at": now.isoformat(), + "updated_at": now.isoformat(), + "notification_count": 1, # Première notif déjà envoyée par analyze.py + "last_notification_at": now.isoformat(), + "escalation_level": 0, + "analyse": analyse_data or {}, + "events": [ + { + "timestamp": now.isoformat(), + "event": "CREATED", + "detail": "Alerte créée suite à détection de chute" + }, + { + "timestamp": now.isoformat(), + "event": "NOTIFICATION_SENT", + "detail": "Première notification Firebase envoyée" + } + ] + } + + alerts[alert_id] = alerte + _save_alerts(alerts) + + return alerte + + +def mettre_a_jour_statut(alert_id, new_status, detail=None): + """ + Met à jour le statut d'une alerte. + Appelée par l'endpoint PHP quand l'app envoie un ACK. + + Returns: True si mise à jour OK, False si alerte non trouvée + """ + alerts = _load_alerts() + + if alert_id not in alerts: + return False + + now = datetime.now() + alerte = alerts[alert_id] + old_status = alerte["status"] + + # Empêcher les régressions de statut (ACKNOWLEDGED ne revient pas à SEEN) + status_order = [ + AlertStatus.PENDING, + AlertStatus.DELIVERED, + AlertStatus.SEEN, + AlertStatus.ACKNOWLEDGED, + AlertStatus.RESOLVED + ] + + try: + old_idx = status_order.index(old_status) + new_idx = status_order.index(new_status) + if new_idx < old_idx and new_status != AlertStatus.EXPIRED: + return False # Pas de régression + except ValueError: + pass # Statut inconnu, on laisse passer + + alerte["status"] = new_status + alerte["updated_at"] = now.isoformat() + alerte["events"].append({ + "timestamp": now.isoformat(), + "event": f"STATUS_CHANGED", + "detail": f"{old_status} → {new_status}" + (f" | {detail}" if detail else "") + }) + + # Si l'alerte est résolue ou expirée, l'archiver + if new_status in (AlertStatus.RESOLVED, AlertStatus.EXPIRED): + _archive_alert(alerte) + del alerts[alert_id] + else: + alerts[alert_id] = alerte + + _save_alerts(alerts) + return True + + +def get_alertes_pending(): + """ + Retourne toutes les alertes qui nécessitent un renvoi. + Utilisée par le watchdog (cron). + + Returns: liste des alertes à renvoyer avec leur niveau d'escalade + """ + alerts = _load_alerts() + now = datetime.now() + a_renvoyer = [] + + for alert_id, alerte in alerts.items(): + # Seules les alertes PENDING ou DELIVERED nécessitent un renvoi + if alerte["status"] not in (AlertStatus.PENDING, AlertStatus.DELIVERED): + continue + + created_at = datetime.fromisoformat(alerte["created_at"]) + age_seconds = (now - created_at).total_seconds() + + # Vérifier si l'alerte a expiré + if age_seconds > ALERT_MAX_AGE: + mettre_a_jour_statut(alert_id, AlertStatus.EXPIRED, "Timeout 2h sans réponse") + continue + + # Déterminer le niveau d'escalade actuel + current_level = alerte.get("escalation_level", 0) + + # Chercher le prochain palier d'escalade à déclencher + for i, palier in enumerate(ESCALATION_SCHEDULE): + if i <= current_level: + continue # Déjà passé ce palier + + if age_seconds >= palier["delay"]: + # Vérifier qu'on n'a pas envoyé récemment (anti-spam : 60s min entre 2 envois) + last_notif = datetime.fromisoformat(alerte["last_notification_at"]) + if (now - last_notif).total_seconds() < 60: + continue + + a_renvoyer.append({ + "alert_id": alert_id, + "alerte": alerte, + "escalation_level": i, + "escalation_label": palier["label"], + "priority": palier["priority"], + "age_seconds": int(age_seconds), + "notification_count": alerte["notification_count"] + }) + break # Un seul palier à la fois + + return a_renvoyer + + +def enregistrer_envoi(alert_id, escalation_level): + """ + Enregistre qu'une notification de rappel a été envoyée. + Appelée par le watchdog après renvoi. + """ + alerts = _load_alerts() + + if alert_id not in alerts: + return False + + now = datetime.now() + alerte = alerts[alert_id] + alerte["notification_count"] = alerte.get("notification_count", 0) + 1 + alerte["last_notification_at"] = now.isoformat() + alerte["escalation_level"] = escalation_level + alerte["updated_at"] = now.isoformat() + alerte["events"].append({ + "timestamp": now.isoformat(), + "event": "NOTIFICATION_RESENT", + "detail": f"Rappel niveau {escalation_level} (envoi #{alerte['notification_count']})" + }) + + alerts[alert_id] = alerte + _save_alerts(alerts) + return True + + +def get_alerte(alert_id): + """Récupère une alerte par son ID""" + alerts = _load_alerts() + return alerts.get(alert_id, None) + + +def get_toutes_alertes_actives(): + """Retourne toutes les alertes actives (pour debug/monitoring)""" + return _load_alerts() + + +def compter_alertes_actives(): + """Compte les alertes actives (pour anti-spam dans analyze.py)""" + alerts = _load_alerts() + return len([a for a in alerts.values() + if a["status"] in (AlertStatus.PENDING, AlertStatus.DELIVERED)]) + + +def nettoyer_alertes_obsoletes(max_age_seen=14400, max_age_ack=7200): + """ + Nettoie les alertes SEEN/ACKNOWLEDGED trop vieilles. + Appelée par le watchdog à chaque cycle. + + - SEEN > 4h → EXPIRED (personne n'a réagi) + - ACKNOWLEDGED > 2h → RESOLVED (situation traitée) + + Returns: nombre d'alertes nettoyées + """ + alerts = _load_alerts() + now = datetime.now() + cleaned = 0 + + for alert_id in list(alerts.keys()): + alerte = alerts[alert_id] + created = datetime.fromisoformat(alerte["created_at"]) + age = (now - created).total_seconds() + + if alerte["status"] == AlertStatus.SEEN and age > max_age_seen: + mettre_a_jour_statut(alert_id, AlertStatus.EXPIRED, f"Auto-expiré après {int(age//3600)}h sans acquittement") + cleaned += 1 + elif alerte["status"] == AlertStatus.ACKNOWLEDGED and age > max_age_ack: + mettre_a_jour_statut(alert_id, AlertStatus.RESOLVED, f"Auto-résolu après acquittement ({int(age//3600)}h)") + cleaned += 1 + elif alerte["status"] == AlertStatus.PENDING and age > ALERT_MAX_AGE: + mettre_a_jour_statut(alert_id, AlertStatus.EXPIRED, f"Timeout {int(age//3600)}h sans réponse") + cleaned += 1 + + return cleaned \ No newline at end of file diff --git a/alert_watchdog.py b/alert_watchdog.py new file mode 100644 index 0000000..2f90888 --- /dev/null +++ b/alert_watchdog.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +""" +SmartEye SENTINEL - Watchdog d'Alertes +======================================== +Script cron qui tourne toutes les minutes. +Vérifie les alertes non acquittées et les renvoie avec escalade. + +Crontab : + * * * * * /usr/bin/python3 /var/www/lucas/alert_watchdog.py >> /var/log/smarteye_watchdog.log 2>&1 + +Auteur : Unigest Solutions / SmartEye V30 +""" + +import json +import sys +import os +import uuid +from datetime import datetime, timedelta + +# Ajouter le chemin du projet +sys.path.insert(0, '/var/www/lucas') + +from alert_manager import ( + get_alertes_pending, + enregistrer_envoi, + mettre_a_jour_statut, + nettoyer_alertes_obsoletes, + AlertStatus +) + +# Firebase +import firebase_admin +from firebase_admin import credentials, messaging + +# --- CONFIGURATION --- +TOKEN_TELEPHONE = "fheHxyPRQlS7T0hDnKIpeR:APA91bFPd7v3zO0GqU7pW3X-IfODgnfIUOFUoM4M1_CsCNNE5BSLVSIrCpXqL_xNNNBWdUFE4A4SCNs9_LWUQn8s7mVaw2gnQxhveC1SOc9E0gRiQW1Ct84" +DOMAIN = "https://lucas.unigest.fr" + +# --- INITIALISATION FIREBASE --- +if not firebase_admin._apps: + try: + cred = credentials.Certificate("/var/www/lucas/admin_key.json") + firebase_admin.initialize_app(cred) + except Exception as e: + print(f"[WATCHDOG] Erreur Firebase init: {e}") + sys.exit(1) + + +def envoyer_rappel(alerte_info): + """ + Envoie une notification de rappel avec le niveau d'escalade approprié. + Le message change selon l'urgence croissante. + """ + alert_id = alerte_info["alert_id"] + alerte = alerte_info["alerte"] + level = alerte_info["escalation_level"] + priority = alerte_info["priority"] + count = alerte_info["notification_count"] + age = alerte_info["age_seconds"] + + image_path = alerte.get("image_path", "") + url_image = f"{DOMAIN}/{image_path}" if image_path else "" + + # Messages d'escalade progressifs + messages_escalade = { + 0: {"title": "⚠️ RAPPEL ALERTE CHUTE", + "body": f"Alerte non vue depuis {age // 60} min — Appuyez pour voir"}, + 1: {"title": "🔴 ALERTE CHUTE NON TRAITÉE", + "body": f"URGENT : Personne potentiellement au sol depuis {age // 60} min !"}, + 2: {"title": "🚨 ALERTE CRITIQUE NON TRAITÉE", + "body": f"Aucune réponse depuis {age // 60} min — Intervention nécessaire !"}, + 3: {"title": "🆘 ALERTE MAXIMALE", + "body": f"SANS RÉPONSE DEPUIS {age // 60} MIN — Vérifiez immédiatement !"}, + 4: {"title": "🆘 DERNIÈRE TENTATIVE", + "body": f"Alerte ignorée depuis {age // 60} min — Escalade en cours"}, + } + + msg_data = messages_escalade.get(level, messages_escalade[4]) + + now = datetime.now() + + try: + message = messaging.Message( + notification=messaging.Notification( + title=msg_data["title"], + body=msg_data["body"], + ), + android=messaging.AndroidConfig( + priority='high', + notification=messaging.AndroidNotification( + channel_id='fall_alert_channel', + priority='max' if level >= 2 else 'high', + default_sound=True, + default_vibrate_timings=True, + visibility='public', + ), + # TTL court pour forcer la livraison rapide + ttl=timedelta(seconds=60), + ), + data={ + "urgence": "true", + "type": "chute_rappel", + "alert_id": alert_id, + "escalation_level": str(level), + "escalation_label": alerte_info["escalation_label"], + "notification_count": str(count + 1), + "original_time": alerte.get("created_at", ""), + "age_minutes": str(age // 60), + "senior_name": "Mamie Lucas", + "senior_photo_url": f"{DOMAIN}/photos/mamie.jpg", + "image_url": url_image, + "camera_id": alerte.get("camera_id", ""), + "audio_relay_ws": alerte.get("audio_relay_ws", "ws://57.128.74.87:8800"), + "camera_ip_map": alerte.get("camera_ip_map", "{}"), + "is_reminder": "true", + "click_action": "FLUTTER_NOTIFICATION_CLICK", + }, + token=TOKEN_TELEPHONE, + ) + + response = messaging.send(message) + + # Enregistrer l'envoi + enregistrer_envoi(alert_id, level) + + print(f"[WATCHDOG] ✅ Rappel envoyé — Alerte {alert_id[:8]}... " + f"| Niveau {level} | Envoi #{count + 1} | Âge {age // 60}min " + f"| Firebase: {response}") + + return True + + except messaging.UnregisteredError: + # Token invalide — le téléphone a été réinitialisé ou l'app désinstallée + print(f"[WATCHDOG] ❌ Token Firebase invalide ! L'app est peut-être désinstallée.") + # TODO: Escalader vers SMS ou contact secondaire + return False + + except Exception as e: + print(f"[WATCHDOG] ❌ Erreur envoi rappel {alert_id[:8]}...: {e}") + return False + + +def run_watchdog(): + """Boucle principale du watchdog""" + now = datetime.now() + + # Nettoyage des alertes obsolètes (SEEN > 4h, ACKNOWLEDGED > 2h) + cleaned = nettoyer_alertes_obsoletes() + if cleaned > 0: + print(f"[WATCHDOG] 🧹 {cleaned} alerte(s) obsolète(s) nettoyée(s)") + + # Récupérer les alertes qui nécessitent un renvoi + alertes_a_renvoyer = get_alertes_pending() + + if not alertes_a_renvoyer: + # Silencieux si rien à faire (le cron tourne chaque minute) + return + + print(f"\n[WATCHDOG] === {now.strftime('%Y-%m-%d %H:%M:%S')} === " + f"{len(alertes_a_renvoyer)} alerte(s) à renvoyer") + + for alerte_info in alertes_a_renvoyer: + envoyer_rappel(alerte_info) + + print(f"[WATCHDOG] --- Fin du cycle ---\n") + + +if __name__ == "__main__": + run_watchdog() \ No newline at end of file diff --git a/analyze.py b/analyze.py new file mode 100755 index 0000000..aea5cb9 --- /dev/null +++ b/analyze.py @@ -0,0 +1,450 @@ +import warnings +# ON FAIT TAIRE LES AVERTISSEMENTS POUR QUE LE JSON SOIT PROPRE +warnings.filterwarnings("ignore") +import sys +import json +import os +# Import pour Google AI (Gemini) - NOUVEAU SDK +from google import genai +from google.genai import types +# Import pour Firebase (Notifications App) +import firebase_admin +from firebase_admin import credentials, messaging +import uuid +from datetime import datetime, timedelta + +# Import du gestionnaire d'alertes +from alert_manager import ( + creer_alerte, + compter_alertes_actives, + AlertStatus +) + +# --- CONFIGURATION VIA config_loader --- +from config_loader import get_config, get_client_info, get_client_fcm_tokens + +_cfg = get_config() +API_KEY = _cfg.get("gemini", "api_key") + +# --- INITIALISATION FIREBASE --- +if not firebase_admin._apps: + try: + firebase_cred_path = _cfg.get("firebase", "credentials_path", fallback="/var/www/lucas/admin_key.json") + cred = credentials.Certificate(firebase_cred_path) + firebase_admin.initialize_app(cred) + except Exception as e: + pass + +# --- CONFIGURATION GEMINI (NOUVEAU SDK) --- +client = genai.Client(api_key=API_KEY) + +generation_config = types.GenerateContentConfig( + temperature=0.1, + top_p=0.95, + top_k=40, + max_output_tokens=512, + response_mime_type="application/json", + safety_settings=[ + types.SafetySetting(category="HARM_CATEGORY_HARASSMENT", threshold="OFF"), + types.SafetySetting(category="HARM_CATEGORY_HATE_SPEECH", threshold="OFF"), + types.SafetySetting(category="HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold="OFF"), + types.SafetySetting(category="HARM_CATEGORY_DANGEROUS_CONTENT", threshold="OFF"), + ] +) + +# --- CHARGEMENT INFOS CLIENT DEPUIS DATABASE.JSON --- +def charger_infos_client(): + """Charge les infos du client depuis database.json via LUCAS_CLIENT_ID (name)""" + client_id = os.environ.get("LUCAS_CLIENT_ID", "") + if not client_id: + return {} + return get_client_info(client_id) + +def deriver_camera_urls(): + """Dérive camera_urls (liste RTSP pour grille vidéo) depuis camera_map""" + camera_map_str = os.environ.get("SMARTEYE_CAMERA_MAP", "{}") + rtsp_url = os.environ.get("SMARTEYE_RTSP_URL", "") + try: + camera_map = json.loads(camera_map_str) + # camera_map = {"rtsp://...:8554/12": "192.168.1.143", ...} + # On extrait toutes les URLs RTSP sauf celle de la caméra principale + urls = [url for url in camera_map.keys() if url != rtsp_url] + return json.dumps(urls) + except Exception: + return "[]" + +# --- FONCTIONS UTILITAIRES POUR JSON --- +def nettoyer_json_response(text_response): + """Nettoie agressivement la réponse pour extraire du JSON valide""" + try: + cleaned = text_response.strip() + if cleaned.startswith("```json"): + cleaned = cleaned[7:] + elif cleaned.startswith("```"): + cleaned = cleaned[3:] + if cleaned.endswith("```"): + cleaned = cleaned[:-3] + cleaned = cleaned.strip() + start = cleaned.find('{') + end = cleaned.rfind('}') + 1 + if start != -1 and end > start: + cleaned = cleaned[start:end] + cleaned = ''.join(char for char in cleaned if ord(char) >= 32 or char in '\n\r\t') + return cleaned.strip() + except Exception as e: + return text_response + +def extraire_json_intelligent(text_response): + """Tente plusieurs stratégies pour extraire du JSON valide""" + strategies = [] + strategies.append(nettoyer_json_response(text_response)) + try: + start = text_response.find('{') + end = text_response.rfind('}') + 1 + if start != -1 and end > start: + strategies.append(text_response[start:end]) + except: + pass + try: + lines = text_response.split('\n') + json_lines = [l for l in lines if '{' in l or '}' in l or '"' in l] + if json_lines: + strategies.append(''.join(json_lines)) + except: + pass + for strategy_text in strategies: + try: + result = json.loads(strategy_text) + if isinstance(result, dict): + return result + except: + continue + return None + +def valider_reponse_ia(result): + """Valide que la réponse IA contient les champs requis""" + if not isinstance(result, dict): + return False + if "urgence" not in result: + return False + if not isinstance(result["urgence"], bool): + if str(result["urgence"]).lower() in ["true", "1", "yes", "oui"]: + result["urgence"] = True + elif str(result["urgence"]).lower() in ["false", "0", "no", "non"]: + result["urgence"] = False + else: + return False + if "confiance" not in result: + result["confiance"] = 70 + if "message" not in result: + result["message"] = "Analyse effectuée" + return True + +# --- FONCTION D'ALERTE APP --- +def envoyer_alerte_app(image_path, alert_id): + """ + Envoie une notification à TOUS les tokens FCM du client via Firebase. + Payload dynamique : infos client depuis database.json + camera_urls dérivé. + """ + try: + client_id = os.environ.get("LUCAS_CLIENT_ID", "") + fcm_tokens = get_client_fcm_tokens(client_id) + if not fcm_tokens: + return + + domain = _cfg.get("server", "domain", fallback="https://lucas.unigest.fr") + url_image = f"{domain}/{image_path}" + + # Charger infos client dynamiquement + client_info = charger_infos_client() + senior_name = client_info.get("senior_name", client_info.get("name", "Senior")) + senior_nickname = client_info.get("senior_nickname", senior_name) + senior_photo_rel = client_info.get("senior_photo", "photos/mamie.jpg") + senior_photo = f"{domain}/{senior_photo_rel}" if senior_photo_rel else f"{domain}/photos/mamie.jpg" + senior_lat = str(client_info.get("latitude", "")) + senior_lon = str(client_info.get("longitude", "")) + emergency_number = client_info.get("emergency_number", "15") + + # Dériver camera_urls depuis camera_map + camera_urls = deriver_camera_urls() + + now = datetime.now() + timeline_standing = (now - timedelta(seconds=45)).strftime("%H:%M:%S") + timeline_fall_detected = (now - timedelta(seconds=30)).strftime("%H:%M:%S") + timeline_immobile = (now - timedelta(seconds=15)).strftime("%H:%M:%S") + timeline_alert_sent = now.strftime("%H:%M:%S") + + data_payload = { + "urgence": "true", + "type": "chute", + "alert_id": alert_id, + "is_reminder": "false", + "escalation_level": "0", + "senior_name": senior_name, + "senior_nickname": senior_nickname, + "senior_photo_url": senior_photo, + "emergency_number": emergency_number, + "timeline_standing": timeline_standing, + "timeline_fall_detected": timeline_fall_detected, + "timeline_immobile": timeline_immobile, + "timeline_alert_sent": timeline_alert_sent, + "senior_latitude": senior_lat, + "senior_longitude": senior_lon, + "image_url": url_image, + "rtsp_url": os.environ.get("SMARTEYE_RTSP_URL", ""), + "camera_urls": camera_urls, + "audio_relay_ws": os.environ.get("SMARTEYE_AUDIO_WS", ""), + "camera_ip_map": os.environ.get("SMARTEYE_CAMERA_MAP", "{}"), + "click_action": "FLUTTER_NOTIFICATION_CLICK", + } + + # Envoyer à chaque token FCM enregistré pour ce client + for token in fcm_tokens: + try: + message = messaging.Message( + notification=messaging.Notification( + title="ALERTE CHUTE", + body="Appuyez pour voir la preuve photo", + ), + android=messaging.AndroidConfig( + priority='high', + notification=messaging.AndroidNotification( + channel_id='fall_alert_channel', + priority='high', + default_sound=True, + default_vibrate_timings=True, + visibility='public', + ), + ), + data=data_payload, + token=token, + ) + messaging.send(message) + except Exception: + pass + except Exception as e: + pass + +# --- FONCTION D'ANALYSE AVEC RETRY --- +def appeler_gemini_avec_retry(image_path, prompt, max_retries=3): + """Appelle Gemini avec retry automatique et backoff exponentiel""" + import time + + with open(image_path, "rb") as f: + image_bytes = f.read() + + last_error = None + last_response_text = None + + for attempt in range(max_retries): + try: + response = client.models.generate_content( + model="gemini-2.5-flash", + contents=[ + types.Part(text=prompt), + types.Part(inline_data=types.Blob(mime_type="image/jpeg", data=image_bytes)) + ], + config=generation_config + ) + + if not response or not response.text: + raise ValueError("Réponse vide de Gemini") + + if hasattr(response, 'candidates') and response.candidates: + candidate = response.candidates[0] + finish_reason = getattr(candidate, 'finish_reason', None) + if finish_reason and 'STOP' not in str(finish_reason): + raise ValueError(f"Réponse incomplète: {finish_reason}") + + text_response = response.text.strip() + last_response_text = text_response + + result = extraire_json_intelligent(text_response) + + if result and valider_reponse_ia(result): + return {"success": True, "data": result, "attempt": attempt + 1} + + raise json.JSONDecodeError( + "JSON invalide après extraction intelligente", + text_response, 0 + ) + + except json.JSONDecodeError as e: + last_error = e + try: + with open("/tmp/smarteye_gemini_errors.log", 'a') as f: + f.write(f"\n--- TENTATIVE {attempt + 1}/{max_retries} - {datetime.now()} ---\n") + f.write(f"JSONDecodeError: {str(e)}\n") + if last_response_text: + f.write(f"Réponse brute: {repr(last_response_text[:500])}\n") + except: + pass + + except Exception as e: + last_error = e + try: + with open("/tmp/smarteye_gemini_errors.log", 'a') as f: + f.write(f"\n--- TENTATIVE {attempt + 1}/{max_retries} - {datetime.now()} ---\n") + f.write(f"Erreur {type(e).__name__}: {str(e)}\n") + if last_response_text: + f.write(f"Réponse brute: {repr(last_response_text[:500])}\n") + except: + pass + + if attempt < max_retries - 1: + wait_time = (2 ** attempt) + time.sleep(wait_time) + + return { + "success": False, + "error": last_error, + "error_type": type(last_error).__name__ if last_error else "UnknownError", + "last_response": last_response_text[:200] if last_response_text else None + } + +# --- FONCTION D'ANALYSE --- +def analyze_image(image_path): + if not os.path.exists(image_path): + return {"urgence": False, "message": "Erreur : Image introuvable"} + + prompt = """Caméra de surveillance au domicile d'une personne âgée vivant seule. Le détecteur de chute a déclenché une alerte. Analyse cette image. + +URGENCE (true) : personne au sol, affalée, inerte, en détresse, position anormale. +PAS URGENCE (false) : pièce vide, personne debout, assise sur meuble (chaise/canapé/lit), accroupie volontairement, en train de se pencher. + +En cas de doute réel entre chute et activité normale → true. + +Réponds en JSON : {"urgence": bool, "confiance": int 0-100, "message": "description courte"}""" + + try: + api_result = appeler_gemini_avec_retry(image_path, prompt, max_retries=3) + + if api_result["success"]: + result = api_result["data"] + + # Gemini a répondu → reset du compteur d'erreurs consécutives + try: + error_state_file = "/tmp/smarteye_error_state.json" + if os.path.exists(error_state_file): + os.remove(error_state_file) + except: + pass + + # --- DÉCLENCHEUR AVEC GESTION D'ALERTE --- + if result.get("urgence") is True: + + # Anti-doublon : ne pas créer 10 alertes si la personne est toujours au sol + alertes_actives = compter_alertes_actives() + if alertes_actives >= 3: + # Trop d'alertes actives non traitées → ne pas en rajouter + result["message"] += " [alerte existante en cours]" + return result + + # Générer un ID unique pour cette alerte + alert_id = str(uuid.uuid4()) + + # 1. Envoyer la notification Firebase (avec l'alert_id) + envoyer_alerte_app(image_path, alert_id) + + # 2. Enregistrer l'alerte comme PENDING dans le gestionnaire + creer_alerte( + alert_id=alert_id, + image_path=image_path, + camera_id=os.environ.get("SMARTEYE_CAMERA_ID", "cam_default"), + audio_relay_ws=os.environ.get("SMARTEYE_AUDIO_WS", "ws://57.128.74.87:8800"), + camera_ip_map=os.environ.get("SMARTEYE_CAMERA_MAP", "{}"), + analyse_data=result + ) + + # 3. Ajouter l'alert_id au résultat pour traçabilité + result["alert_id"] = alert_id + + return result + else: + raise Exception(f"{api_result['error_type']}: Échec après 3 tentatives") + + except Exception as e: + import time + import traceback + + error_state_file = "/tmp/smarteye_error_state.json" + now = time.time() + error_type = type(e).__name__ + + # Log vers fichier dédié (pas stderr, car api.php utilise 2>&1) + try: + with open("/tmp/lucas_gemini_errors.log", 'a') as logf: + logf.write(f"\n{'='*50}\n") + logf.write(f"[{datetime.now()}] {error_type}: {e}\n") + logf.write(f"Image: {image_path}\n") + traceback.print_exc(file=logf) + except: + pass + + # --- Charger l'état des erreurs --- + error_state = {"consecutive": 0, "first_error": now, "last_alert_sent": 0} + try: + if os.path.exists(error_state_file): + with open(error_state_file, 'r') as f: + error_state = json.loads(f.read()) + except: + pass + + # Reset si dernière erreur date de plus d'1h (Gemini avait récupéré) + if now - error_state.get("first_error", now) > 3600: + error_state = {"consecutive": 0, "first_error": now, "last_alert_sent": 0} + + error_state["consecutive"] = error_state.get("consecutive", 0) + 1 + error_state["last_error"] = now + if error_state["consecutive"] == 1: + error_state["first_error"] = now + + # --- Sauvegarder l'état --- + try: + with open(error_state_file, 'w') as f: + json.dump(error_state, f) + except: + pass + + consecutive = error_state["consecutive"] + last_alert = error_state.get("last_alert_sent", 0) + cooldown = 1800 # 30 minutes entre deux alertes d'erreur + + # --- SÉCURITÉ : Gemini n'a PAS analysé l'image → TOUJOURS alerter --- + # Principe fondateur : mieux vaut 10 fausses alertes qu'une chute ratée. + # Anti-doublon : on ne spamme pas si des alertes sont déjà actives. + alert_id = str(uuid.uuid4()) + alertes_actives = compter_alertes_actives() + + if alertes_actives < 3: + envoyer_alerte_app(image_path, alert_id) + creer_alerte( + alert_id=alert_id, + image_path=image_path, + camera_id=os.environ.get("SMARTEYE_CAMERA_ID", "cam_default"), + audio_relay_ws=os.environ.get("SMARTEYE_AUDIO_WS", "ws://57.128.74.87:8800"), + camera_ip_map=os.environ.get("SMARTEYE_CAMERA_MAP", "{}"), + analyse_data={"error": True, "error_type": error_type, "consecutive": consecutive} + ) + error_state["last_alert_sent"] = now + try: + with open(error_state_file, 'w') as f: + json.dump(error_state, f) + except: + pass + + return { + "urgence": True, + "confiance": 30, + "message": f"⚠️ Erreur IA ({error_type}), alerte de sécurité par précaution", + "alert_id": alert_id + } + +if __name__ == "__main__": + if len(sys.argv) < 2: + print(json.dumps({"urgence": False, "message": "No image provided"})) + sys.exit(1) + + image_path = sys.argv[1] + result = analyze_image(image_path) + print(json.dumps(result)) diff --git a/analyze_errors_and_learn.py b/analyze_errors_and_learn.py new file mode 100755 index 0000000..d7f48be --- /dev/null +++ b/analyze_errors_and_learn.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +""" +Système d'apprentissage des erreurs +Analyse les logs d'erreurs pour identifier les patterns et proposer des améliorations +""" +import os +import sys +import json +import re +from collections import Counter, defaultdict +from datetime import datetime + +def analyze_error_logs(): + """Analyse les logs d'erreurs Gemini""" + error_log = "/tmp/smarteye_gemini_errors.log" + + if not os.path.exists(error_log): + return { + "status": "no_errors", + "message": "Aucun log d'erreur trouvé - Système sain !" + } + + with open(error_log, 'r') as f: + content = f.read() + + # Patterns à détecter + patterns = { + "JSONDecodeError": r"JSONDecodeError", + "ValueError": r"ValueError", + "TimeoutError": r"TimeoutError", + "ConnectionError": r"ConnectionError", + "API quota": r"quota|rate.?limit", + "Empty response": r"Réponse vide|empty response", + "Malformed JSON": r"JSON invalide|malformed", + } + + # Comptage des erreurs + error_counts = {} + for name, pattern in patterns.items(): + matches = re.findall(pattern, content, re.IGNORECASE) + if matches: + error_counts[name] = len(matches) + + # Extraction des tentatives + tentative_pattern = r"TENTATIVE (\d)/(\d)" + tentatives = re.findall(tentative_pattern, content) + + # Statistiques + total_errors = sum(error_counts.values()) + retry_success = len([t for t in tentatives if t[0] != t[1]]) + + return { + "status": "analyzed", + "total_errors": total_errors, + "error_counts": error_counts, + "retry_attempts": len(tentatives), + "retry_success_rate": (retry_success / len(tentatives) * 100) if tentatives else 0, + "most_common": max(error_counts.items(), key=lambda x: x[1])[0] if error_counts else None + } + +def analyze_image_results(): + """Analyse les résultats d'analyse des images""" + client_folder = "/var/www/lucas/clients/Demo_01" + + if not os.path.exists(client_folder): + return None + + # Statistiques des analyses + total_images = 0 + with_json = 0 + urgence_count = 0 + false_positive_count = 0 + confidence_scores = [] + messages = defaultdict(int) + + for filename in os.listdir(client_folder): + if filename.endswith('.jpg'): + total_images += 1 + json_file = os.path.join(client_folder, filename + '.json') + + if os.path.exists(json_file): + with_json += 1 + try: + with open(json_file, 'r') as f: + data = json.load(f) + + if data.get('urgence') is True: + urgence_count += 1 + elif data.get('urgence') is False: + false_positive_count += 1 + + if 'confiance' in data: + confidence_scores.append(data['confiance']) + + msg = data.get('message', 'Unknown')[:50] + messages[msg] += 1 + + except: + pass + + avg_confidence = sum(confidence_scores) / len(confidence_scores) if confidence_scores else 0 + + return { + "total_images": total_images, + "analyzed": with_json, + "coverage": (with_json / total_images * 100) if total_images else 0, + "urgence_rate": (urgence_count / with_json * 100) if with_json else 0, + "false_positive_rate": (false_positive_count / with_json * 100) if with_json else 0, + "avg_confidence": avg_confidence, + "top_messages": dict(sorted(messages.items(), key=lambda x: x[1], reverse=True)[:5]) + } + +def generate_recommendations(error_analysis, image_analysis): + """Génère des recommandations basées sur l'analyse""" + recommendations = [] + + if not error_analysis or error_analysis["status"] == "no_errors": + recommendations.append({ + "priority": "✅ SUCCESS", + "title": "Système stable", + "description": "Aucune erreur détectée - Le correctif fonctionne parfaitement !", + "action": None + }) + else: + # Analyse des erreurs + if error_analysis["total_errors"] > 10: + recommendations.append({ + "priority": "⚠️ MOYEN", + "title": f"{error_analysis['total_errors']} erreurs détectées", + "description": "Taux d'erreur encore élevé", + "action": "Vérifier la clé API Gemini et les quotas" + }) + + if error_analysis.get("most_common") == "TimeoutError": + recommendations.append({ + "priority": "⚠️ MOYEN", + "title": "Timeouts fréquents", + "description": "L'API Gemini répond lentement", + "action": "Augmenter le timeout à 60s dans analyze.py" + }) + + if error_analysis.get("retry_success_rate", 0) < 50: + recommendations.append({ + "priority": "🔴 URGENT", + "title": "Faible taux de succès des retries", + "description": f"Seulement {error_analysis['retry_success_rate']:.1f}% de succès", + "action": "Augmenter max_retries à 5 ou vérifier la connexion" + }) + + if image_analysis: + # Analyse des images + if image_analysis["coverage"] < 95: + recommendations.append({ + "priority": "ℹ️ INFO", + "title": f"Couverture: {image_analysis['coverage']:.1f}%", + "description": f"{image_analysis['total_images'] - image_analysis['analyzed']} images sans analyse", + "action": "Exécuter repair_missing_analyses.py si besoin" + }) + + if image_analysis["urgence_rate"] > 50: + recommendations.append({ + "priority": "⚠️ MOYEN", + "title": f"Taux d'urgence élevé: {image_analysis['urgence_rate']:.1f}%", + "description": "Beaucoup de vraies alertes détectées", + "action": "Vérifier le positionnement des caméras et le prompt IA" + }) + + if image_analysis["avg_confidence"] < 70: + recommendations.append({ + "priority": "ℹ️ INFO", + "title": f"Confiance moyenne: {image_analysis['avg_confidence']:.0f}%", + "description": "L'IA hésite sur certaines analyses", + "action": "Améliorer le prompt ou utiliser un modèle plus puissant" + }) + + if not recommendations: + recommendations.append({ + "priority": "✅ SUCCESS", + "title": "Système optimal", + "description": "Aucune amélioration nécessaire", + "action": None + }) + + return recommendations + +def main(): + print("="*70) + print("ANALYSE DES ERREURS ET APPRENTISSAGE") + print("="*70) + print() + + # Analyse des erreurs + print("📊 ANALYSE DES LOGS D'ERREURS") + print("-"*70) + error_analysis = analyze_error_logs() + + if error_analysis["status"] == "no_errors": + print("✅ Aucune erreur trouvée - Système parfaitement stable !") + else: + print(f"Total erreurs: {error_analysis['total_errors']}") + print(f"Tentatives de retry: {error_analysis['retry_attempts']}") + print(f"Taux de succès retry: {error_analysis.get('retry_success_rate', 0):.1f}%") + print() + print("Types d'erreurs:") + for error_type, count in sorted(error_analysis['error_counts'].items(), key=lambda x: x[1], reverse=True): + print(f" • {error_type}: {count}") + + print() + print("📸 ANALYSE DES IMAGES") + print("-"*70) + image_analysis = analyze_image_results() + + if image_analysis: + print(f"Total images: {image_analysis['total_images']}") + print(f"Analysées: {image_analysis['analyzed']} ({image_analysis['coverage']:.1f}%)") + print(f"Taux d'urgence: {image_analysis['urgence_rate']:.1f}%") + print(f"Fausses alertes: {image_analysis['false_positive_rate']:.1f}%") + print(f"Confiance moyenne: {image_analysis['avg_confidence']:.0f}%") + print() + print("Messages les plus fréquents:") + for msg, count in list(image_analysis['top_messages'].items())[:3]: + print(f" • {msg}: {count}x") + else: + print("⚠️ Dossier client introuvable") + + print() + print("💡 RECOMMANDATIONS") + print("="*70) + + recommendations = generate_recommendations(error_analysis, image_analysis) + + for i, rec in enumerate(recommendations, 1): + print(f"\n{rec['priority']} {rec['title']}") + print(f" {rec['description']}") + if rec['action']: + print(f" → Action: {rec['action']}") + + print() + print("="*70) + + # Sauvegarde du rapport + report = { + "timestamp": datetime.now().isoformat(), + "error_analysis": error_analysis, + "image_analysis": image_analysis, + "recommendations": recommendations + } + + report_file = "/var/www/lucas/last_analysis_report.json" + with open(report_file, 'w') as f: + json.dump(report, f, indent=2) + + print(f"📄 Rapport sauvegardé: {report_file}") + print() + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\n\n⚠️ Interruption") + sys.exit(1) + except Exception as e: + print(f"\n❌ Erreur: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/api.php b/api.php new file mode 100755 index 0000000..35e2c9d --- /dev/null +++ b/api.php @@ -0,0 +1,140 @@ + "error", "message" => "Base de données introuvable"])); +} +$db = json_decode(file_get_contents($json_file), true); + +// Données reçues +$client_input = $_POST['client_id'] ?? ''; +$token_input = $_POST['token'] ?? ''; + +// --- 2. IDENTIFICATION & SÉCURITÉ --- +$client_index = -1; +$current_client = null; + +if (isset($db['clients'])) { + foreach ($db['clients'] as $index => $c) { + if (strcasecmp($c['name'], $client_input) == 0 && $c['token'] === $token_input) { + $client_index = $index; + $current_client = $c; + break; + } + } +} + +if ($client_index === -1) { + http_response_code(403); + die(json_encode(["status" => "error", "message" => "Authentification échouée"])); +} + +// --- 3. TRAITEMENT IMAGE --- +if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) { + + // Dossier spécifique + $upload_dir = 'clients/' . $current_client['name'] . '/'; + if (!is_dir($upload_dir)) mkdir($upload_dir, 0775, true); + + $filename = "alerte_" . date("Ymd_His") . ".jpg"; + $target_file = $upload_dir . $filename; + + if (move_uploaded_file($_FILES['image']['tmp_name'], $target_file)) { + + // --- 4. APPEL IA & AUTOMATISATION --- + // On reconstruit l'URL publique + $public_url = "https://lucas.unigest.fr/" . $target_file; + + // SÉCURITÉ AJOUTÉE : "cd " . __DIR__ + // On force le serveur à se mettre dans le bon dossier pour trouver admin_key.json + + // Champs caméra envoyés par SmartEye + $env_camera_id = escapeshellarg($_POST['camera_id'] ?? ''); + $env_rtsp_url = escapeshellarg($_POST['rtsp_url'] ?? ''); + $env_audio_ws = escapeshellarg($_POST['audio_ws'] ?? 'ws://57.128.74.87:8800'); + $env_camera_map = escapeshellarg($_POST['camera_map'] ?? '{}'); + + $env_client_id = escapeshellarg($current_client['name']); + + $safe_cmd = "cd " . __DIR__ . " && " + . "LUCAS_CLIENT_ID={$env_client_id} " + . "SMARTEYE_CAMERA_ID={$env_camera_id} " + . "SMARTEYE_RTSP_URL={$env_rtsp_url} " + . "SMARTEYE_AUDIO_WS={$env_audio_ws} " + . "SMARTEYE_CAMERA_MAP={$env_camera_map} " + . "python3 analyze.py " . escapeshellarg($target_file) . " 2>>/tmp/lucas_python_stderr.log"; + + // C'EST ICI QUE TOUT SE JOUE : + // 1. PHP lance Python. + // 2. Python analyse l'image. + // 3. Si Urgence -> Python envoie la notif Firebase au téléphone. + // 4. Python renvoie le résultat JSON à PHP. + $output = shell_exec($safe_cmd); + $resultat_ia = json_decode($output, true); + + // --- GESTION DES RÉSULTATS --- + if (!$resultat_ia) { + // Si le JSON est cassé (crash Python), on force l'urgence par sécurité + $est_urgence = true; + $message_ia = "⚠️ Erreur technique IA. Analyse visuelle requise."; + // Optionnel : On logue l'erreur brute pour déboguer plus tard + file_put_contents("error_python.log", date("Y-m-d H:i:s") . " - " . $output . PHP_EOL, FILE_APPEND); + } else { + $est_urgence = $resultat_ia['urgence'] ?? false; + $message_ia = $resultat_ia['message'] ?? "Analyse terminée"; + } + + // --- 5. MISE A JOUR BASE DE DONNÉES --- + $db['clients'][$client_index]['alerte'] = $est_urgence; + $db['clients'][$client_index]['last_update'] = date("d/m/Y H:i:s"); + $db['clients'][$client_index]['message'] = $message_ia; + + file_put_contents($json_file, json_encode($db, JSON_PRETTY_PRINT)); + + // --- 5b. ARCHIVAGE JSON --- + $history_data = [ + "urgence" => $est_urgence, + "message" => $message_ia, + "timestamp" => date("Y-m-d H:i:s") + ]; + file_put_contents($target_file . ".json", json_encode($history_data)); + + // --- 6. LOG SMS (Optionnel : Backup SMS si urgence) --- + if ($est_urgence) { + $contacts = $current_client['contacts'] ?? []; + $sms_body = "🚨 ALERTE CHUTE : " . $current_client['name']; // Message court pour SMS + + foreach ($contacts as $contact) { + if (!empty($contact['phone'])) { + envoyer_sms_reel($contact['phone'], $sms_body); + } + } + } + + echo json_encode([ + "status" => "success", + "client" => $current_client['name'], + "ia_result" => $resultat_ia + ]); + + } else { + http_response_code(500); + echo json_encode(["status" => "error", "message" => "Erreur écriture fichier"]); + } +} else { + http_response_code(400); + echo json_encode(["status" => "error", "message" => "Pas d'image reçue"]); +} + +function envoyer_sms_reel($numero, $message) { + // Nettoyage sommaire + $numero = str_replace([' ', '.', '-'], '', $numero); + // Appel du gestionnaire SMS (séparé) + $cmd = "python3 sms_manager.py " . escapeshellarg($numero) . " " . escapeshellarg($message) . " > /dev/null 2>&1 &"; + shell_exec($cmd); +} +?> \ No newline at end of file diff --git a/api/auth.php b/api/auth.php new file mode 100644 index 0000000..221e147 --- /dev/null +++ b/api/auth.php @@ -0,0 +1,94 @@ + "error", "message" => "Base de données introuvable"])); +} +$db = json_decode(file_get_contents($json_file), true); + +// --- 2. RÉCEPTION DONNÉES --- +$input = json_decode(file_get_contents('php://input'), true); +$client_id = $input['client_id'] ?? ''; +$password = $input['password'] ?? ''; +$fcm_token = $input['fcm_token'] ?? null; + +if (empty($client_id) || empty($password)) { + http_response_code(400); + die(json_encode(["status" => "error", "message" => "Identifiant et mot de passe requis"])); +} + +// --- 3. RECHERCHE CLIENT --- +$client_index = -1; +$current_client = null; + +if (isset($db['clients'])) { + foreach ($db['clients'] as $index => $c) { + if (strcasecmp($c['name'], $client_id) == 0) { + $client_index = $index; + $current_client = $c; + break; + } + } +} + +if ($client_index === -1) { + http_response_code(403); + die(json_encode(["status" => "error", "message" => "Identifiant inconnu"])); +} + +// --- 4. VÉRIFICATION MOT DE PASSE --- +$password_hash = $current_client['password_hash'] ?? null; + +if ($password_hash === null) { + http_response_code(403); + die(json_encode(["status" => "error", "message" => "Compte non configuré. Contactez l'administrateur."])); +} + +if (!password_verify($password, $password_hash)) { + http_response_code(403); + die(json_encode(["status" => "error", "message" => "Mot de passe incorrect"])); +} + +// --- 5. ENREGISTREMENT FCM TOKEN (si fourni) --- +if ($fcm_token !== null) { + if (!isset($current_client['fcm_tokens'])) { + $current_client['fcm_tokens'] = []; + } + + // Ajouter le token s'il n'existe pas déjà + if (!in_array($fcm_token, $current_client['fcm_tokens'])) { + $current_client['fcm_tokens'][] = $fcm_token; + $db['clients'][$client_index]['fcm_tokens'] = $current_client['fcm_tokens']; + file_put_contents($json_file, json_encode($db, JSON_PRETTY_PRINT)); + } +} + +// --- 6. RÉPONSE SUCCÈS --- +echo json_encode([ + "status" => "success", + "message" => "Authentification réussie", + "data" => [ + "token" => $current_client['token'], + "client_name" => $current_client['name'], + "senior_name" => $current_client['senior_name'] ?? '', + "senior_nickname" => $current_client['senior_nickname'] ?? '', + "senior_photo" => $current_client['senior_photo'] ?? '', + "latitude" => $current_client['latitude'] ?? '', + "longitude" => $current_client['longitude'] ?? '', + "emergency_number" => $current_client['emergency_number'] ?? '15', + "contacts" => $current_client['contacts'] ?? [], + "site_status" => $current_client['site_status'] ?? 'provisioned' + ] +]); +?> diff --git a/check_gemini_health.py b/check_gemini_health.py new file mode 100755 index 0000000..8c19ead --- /dev/null +++ b/check_gemini_health.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +""" +Script de monitoring pour surveiller la santé de l'API Gemini +Affiche les statistiques d'erreurs et les dernières erreurs +""" +import os +import json +from datetime import datetime + +print("="*70) +print("MONITORING SANTÉ API GEMINI") +print("="*70) + +# Fichiers de log +error_log = "/tmp/smarteye_gemini_errors.log" +error_counter = "/tmp/smarteye_gemini_error_count" +error_lock = "/tmp/smarteye_gemini_error_last" + +# 1. Statistiques d'erreurs +print("\n📊 STATISTIQUES D'ERREURS") +print("-"*70) + +if os.path.exists(error_counter): + try: + with open(error_counter, 'r') as f: + data = json.loads(f.read()) + error_count = data.get("count", 0) + first_error = data.get("first_error") + last_error = data.get("last_error") + + print(f"Nombre d'erreurs (dernière heure): {error_count}") + + if first_error: + first_dt = datetime.fromtimestamp(first_error) + print(f"Première erreur: {first_dt.strftime('%Y-%m-%d %H:%M:%S')}") + + if last_error: + last_dt = datetime.fromtimestamp(last_error) + print(f"Dernière erreur: {last_dt.strftime('%Y-%m-%d %H:%M:%S')}") + + # Évaluation de la santé + if error_count == 0: + print("✓ Statut: EXCELLENT - Aucune erreur") + elif error_count < 3: + print("⚠ Statut: BON - Quelques erreurs mineures") + elif error_count < 5: + print("⚠ Statut: ATTENTION - Erreurs modérées") + else: + print("✗ Statut: CRITIQUE - Problème récurrent détecté") + except Exception as e: + print(f"Erreur lecture compteur: {e}") +else: + print("✓ Aucune erreur enregistrée") + +# 2. Cooldown actif +print("\n⏱️ COOLDOWN ANTI-SPAM") +print("-"*70) + +if os.path.exists(error_lock): + try: + with open(error_lock, 'r') as f: + last_alert = float(f.read().strip()) + last_alert_dt = datetime.fromtimestamp(last_alert) + now = datetime.now().timestamp() + remaining = max(0, 300 - (now - last_alert)) + + print(f"Dernière alerte envoyée: {last_alert_dt.strftime('%Y-%m-%d %H:%M:%S')}") + + if remaining > 0: + print(f"⏳ Cooldown actif: {int(remaining)} secondes restantes") + else: + print("✓ Cooldown terminé - Alertes autorisées") + except Exception as e: + print(f"Erreur lecture cooldown: {e}") +else: + print("✓ Pas de cooldown actif") + +# 3. Dernières erreurs du log +print("\n📋 DERNIÈRES ERREURS (10 dernières lignes)") +print("-"*70) + +if os.path.exists(error_log): + try: + with open(error_log, 'r') as f: + lines = f.readlines() + last_lines = lines[-30:] if len(lines) > 30 else lines + + if last_lines: + print(''.join(last_lines)) + else: + print("✓ Aucune erreur dans le log") + except Exception as e: + print(f"Erreur lecture log: {e}") +else: + print("✓ Fichier de log non trouvé (aucune erreur)") + +# 4. Recommandations +print("\n💡 RECOMMANDATIONS") +print("-"*70) + +if os.path.exists(error_counter): + try: + with open(error_counter, 'r') as f: + data = json.loads(f.read()) + error_count = data.get("count", 0) + + if error_count >= 5: + print("⚠ ACTIONS RECOMMANDÉES:") + print(" 1. Vérifier la validité de la clé API Gemini") + print(" 2. Vérifier la connectivité réseau") + print(" 3. Consulter le log détaillé: /tmp/smarteye_gemini_errors.log") + print(" 4. Vérifier les quotas API Google") + print(" 5. Tester manuellement l'API avec check_models.py") + elif error_count >= 3: + print("ℹ️ Surveillance recommandée - Quelques erreurs détectées") + else: + print("✓ Système fonctionnel - Aucune action requise") + except: + pass +else: + print("✓ Système fonctionnel - Aucune action requise") + +print("\n" + "="*70) +print("Fin du rapport") +print("="*70) diff --git a/check_missing_json.sh b/check_missing_json.sh new file mode 100755 index 0000000..0904ade --- /dev/null +++ b/check_missing_json.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Script pour identifier les images sans fichier JSON sidecar + +cd /var/www/lucas/clients/Demo_01 + +echo "===================================================================" +echo "VÉRIFICATION DES IMAGES SANS ANALYSE JSON" +echo "===================================================================" +echo "" + +missing_count=0 +total_count=0 + +for img in $(ls -t *.jpg | head -30); do + total_count=$((total_count + 1)) + if [ ! -f "${img}.json" ]; then + missing_count=$((missing_count + 1)) + timestamp=$(stat -c %y "$img" 2>/dev/null | cut -d'.' -f1) + echo "❌ $img" + echo " Date: $timestamp" + echo "" + fi +done + +echo "===================================================================" +echo "RÉSUMÉ (30 dernières images)" +echo "===================================================================" +echo "Total: $total_count images" +echo "Avec JSON: $((total_count - missing_count)) images" +echo "Sans JSON: $missing_count images" + +if [ $missing_count -gt 0 ]; then + echo "" + echo "⚠️ Il y a des images récentes sans analyse sauvegardée" + echo " Cela peut indiquer des erreurs pendant l'analyse IA" +else + echo "" + echo "✓ Toutes les images récentes ont une analyse" +fi diff --git a/check_models.py b/check_models.py new file mode 100755 index 0000000..c3fa4e5 --- /dev/null +++ b/check_models.py @@ -0,0 +1,13 @@ +import google.generativeai as genai +import os + +API_KEY = "AIzaSyC5wIc9nrhtbVesHqodil6V6-WIxkJFSHA" +genai.configure(api_key=API_KEY) + +print("🔍 Recherche des modèles disponibles...") +try: + for m in genai.list_models(): + if 'generateContent' in m.supported_generation_methods: + print(f"👉 {m.name}") +except Exception as e: + print(f"❌ Erreur : {e}") diff --git a/checklist.php b/checklist.php new file mode 100644 index 0000000..418d56c --- /dev/null +++ b/checklist.php @@ -0,0 +1,494 @@ + + + + + + SmartEye — Checklist Installation + + + + + + + + + +
+
+
+ Progression globale + 0 / 0 +
+
+
+
+
+ Bureau + Sur place + Config App + Tunnels + Tests + Production +
+
+
+ + +
+ + +
+
+ 🏢 +

Phase 1 — Au Bureau

+ Avant le déplacement +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FaitTestActionQuiNotes
Créer le dossier client dans admin.phpLucas
Générer le mot de passe LucasApp (admin/generate_password.php)Lucas
Remplir infos senior (nom, adresse, date naissance)Lucas
Ajouter contacts famille (noms, tél, OS)Lucas
Préparer le Jetson (flash SSD, OS, drivers)SmartEye
Pré-configurer WiFi hotspot SmartEye-Setup (connexion sans écran)SmartEye
Préparer clé USB auto-config WiFi (script + wifi.conf du client)SmartEye
Vérifier que SmartEye tourne (service actif, YOLO chargé)SmartEye
Préparer les caméras (reset usine, config 2 flux RTSP) [guide]SmartEye
+
+ + +
+
+ 🏠 +

Phase 2 — Sur Place : Matériel

+ Installation physique +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FaitTestActionQuiNotes
Alimenter le Jetson + connecter au réseau (hotspot OU clé USB OU Ethernet)SmartEye
SSH sur le Jetson → configurer WiFi client nmcli dev wifi connect "SSID" password "***"SmartEye
Installer et positionner les camérasSmartEye
Renseigner IP Jetson + IPs caméras + ports tunnel dans admin.phpLucas
Vérifier que SmartEye détecte les caméras (YOLO actif)SmartEye
+
+ + +
+
+ 📱 +

Phase 3 — Configuration App AVA

+ Sur le téléphone famille +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FaitTestActionQuiNotes
Saisir identifiant + mot de passe dans AVAAVA
Vérifier réception smarteye_token + jetson_ip + cameras + AVA + Lucas +
Scanner caméras via Jetson (WiFi local : http://jetson_ip:8080) + AVA + SmartEye +
Vérifier affichage caméra en local (WiFi)AVA
+
+ + +
+
+ 🔗 +

Phase 4 — Tunnels SSH (accès distant)

+ Jetson → OVH +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FaitTestActionQuiNotes
Clé SSH du Jetson ajoutée dans authorized_keys sur OVHLucas
Lancer autossh sur le Jetson avec les ports camérasSmartEye
Vérifier ports actifs sur OVH (ss -tlnp)Lucas
Configurer autossh en service (redémarrage auto)SmartEye
+
+ + +
+
+ 🧪 +

Phase 5 — Tests Complets

+ Couper WiFi, tester en 4G +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FaitTestActionQuiNotes
Caméra live à distance (rtsp://57.128.74.87:port)AVA
Simuler une chute (passer devant caméra, se coucher)SmartEye
Vérifier réception image + analyse Gemini (api.php → analyze.py)Lucas
Réception notification push FirebaseAVA
Consulter photos capturées (photos.php)AVA
Acquitter l'alerte (acknowledge.php)AVA
Résoudre / reset l'alerte (reset.php)AVA
+
+ + +
+
+ 🚀 +

Phase 6 — Mise en Production

+ Validation finale +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
FaitTestActionQuiNotes
Passer site_status à "active"Lucas
Former la famille à l'utilisation de AVAAVA
PV de recette signé par la famille + SmartEye + Lucas + AVA +
+
+ + +
+ +
+
+ + + + diff --git a/config_loader.py b/config_loader.py new file mode 100644 index 0000000..5c9393a --- /dev/null +++ b/config_loader.py @@ -0,0 +1,97 @@ +""" +config_loader.py — Chargement centralisé de la configuration Lucas + +Usage: + from config_loader import get_config, get_client_info + + cfg = get_config() + api_key = cfg.get("gemini", "api_key") + + client = get_client_info("Demo_01") + senior_name = client.get("senior_name", "Senior") +""" + +import configparser +import json +import os + +_config = None +_config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lucas_config.ini") +_db_path = None + + +def get_config(): + """Charge et cache la configuration globale depuis lucas_config.ini""" + global _config, _db_path + if _config is None: + _config = configparser.ConfigParser() + _config.read(_config_path) + _db_path = _config.get("database", "clients_db", fallback="/var/www/lucas/database.json") + return _config + + +def get_db(): + """Charge la base de données clients""" + cfg = get_config() + try: + with open(_db_path, 'r') as f: + return json.load(f) + except Exception: + return {"clients": {}} + + +def get_client_info(client_id): + """Charge les infos d'un client par son name (client_id) + + Cherche dans database.json un client dont le champ 'name' correspond. + Retourne le dict du client ou {} si non trouvé. + """ + db = get_db() + clients = db.get("clients", {}) + + # Recherche par clé directe + if client_id in clients: + return clients[client_id] + + # Recherche par champ 'name' (compatibilité avec les clients indexés numériquement) + for key, client in clients.items(): + if client.get("name", "").lower() == client_id.lower(): + return client + + return {} + + +def get_client_fcm_tokens(client_id): + """Retourne la liste des tokens FCM d'un client""" + client = get_client_info(client_id) + return client.get("fcm_tokens", []) + + +def get_site_ports(site_number): + """Calcule les ports pour un numéro de site donné + + Returns: dict avec base_port, rtsp_ports (list), audio_port, webui_port + """ + cfg = get_config() + base_start = cfg.getint("ports", "base_start", fallback=8550) + ports_per_site = cfg.getint("ports", "ports_per_site", fallback=10) + + base_port = base_start + (site_number - 1) * ports_per_site + return { + "base_port": base_port, + "rtsp_ports": list(range(base_port, base_port + 6)), + "audio_port": base_port + 6, + "webui_port": base_port + 7, + } + + +def get_next_site_number(): + """Retourne le prochain numéro de site disponible""" + db = get_db() + clients = db.get("clients", {}) + max_site = 0 + for client in clients.values(): + sn = client.get("site_number", 0) + if isinstance(sn, int) and sn > max_site: + max_site = sn + return max_site + 1 diff --git a/doc_camera.php b/doc_camera.php new file mode 100644 index 0000000..a6657ff --- /dev/null +++ b/doc_camera.php @@ -0,0 +1,282 @@ + + + + + + SmartEye — Configuration Caméra + + + + + + + + +
+ + +
+
+

Comprendre les 2 flux vidéo

+

Chaque caméra IP diffuse 2 flux RTSP simultanés, utilisés pour des besoins différents.

+
+
+ +
+
+ Flux 2 — Détection + /cam/realmonitor?channel=1&subtype=1 +
+
+

Usage : Flux analysé en continu par YOLO sur le Jetson pour détecter les chutes.

+

Pourquoi basse résolution ? YOLO n'a pas besoin de HD pour détecter une personne. Un flux léger (800x448) permet :

+
    +
  • Moins de charge GPU sur le Jetson
  • +
  • Analyse plus rapide (+ de FPS traités)
  • +
  • Plusieurs caméras en parallèle
  • +
+
+
+ CAMFlux 2 (SD)Jetson YOLOChute ? +
+
+ + +
+
+ Flux 1 — Preuve HD + /cam/realmonitor?channel=1&subtype=0 +
+
+

Usage : Quand YOLO détecte une chute, SmartEye capture une image HD depuis ce flux et l'envoie au serveur Lucas.

+

Pourquoi haute résolution ? L'image de preuve doit être nette pour :

+
    +
  • Analyse précise par Gemini (position, visage, contexte)
  • +
  • Preuve visuelle pour la famille dans l'app AVA
  • +
  • Archivage en qualité
  • +
+
+
+ CHUTECapture HDLucas (Gemini)AVA +
+
+
+ + +
+
+
+  CAMÉRA IP (ex: 192.168.1.196)
+  │
+  ├── Flux 2 (subtype=1) — 800x448, 15fps, 300kbps —→ Jetson YOLO (détection continue 24/7)
+  │                                                            │
+  │                                                    Chute détectée !
+  │                                                            │
+  └── Flux 1 (subtype=0) — 2880x1620, 20fps, 6Mbps —→ Capture screenshot HD
+                                                                    │
+                                                           POST image → Lucas api.php
+                                                                    │
+                                                           Gemini analysePush AVA
+
+
+
+ + +
+
+

Paramètres recommandés

+

Configuration optimale pour le système SmartEye. Accessible via l'interface web de la caméra.

+
+ + +
+
+

Paramètres généraux

+
+
+ + + + + + + + + +
Format vidéo50Hz
Codage vidéoH.264 Main Profile
+
+
+

H.264 est indispensable. Le H.265 n'est pas supporté par tous les lecteurs RTSP. Le Main Profile offre le meilleur compromis qualité/compatibilité.

+
+
+
+ + +
+
+

Premier flux — Preuve HD

+ subtype=0 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParamètreValeurExplication
Résolution2880 x 1620Résolution max de la caméra. Image nette pour Gemini et la famille.
Débit binaire6144 kbpsDébit max. Qualité optimale pour la capture de preuve.
Fréquence20 fpsSuffisant pour capturer un screenshot net. Pas besoin de 25fps.
Intervalle image clé201 image clé par seconde (= intervalle / fps). Capture plus rapide.
Contrôle débitCBRDébit constant. Qualité stable même en mouvement.
Qualité image1 (meilleure)Valeur la plus basse = qualité max.
+
+
+ + +
+
+

Deuxième flux — Détection YOLO

+ subtype=1 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParamètreValeurExplication
Résolution800 x 448YOLO détecte très bien en basse résolution. Économise le GPU.
Débit binaire300 kbps20x moins que le flux HD. Bande passante négligeable.
Fréquence15 fpsSuffisant pour détecter un mouvement. Plus = plus de charge GPU inutile.
Intervalle image clé301 image clé toutes les 2 secondes. Acceptable pour la détection.
Contrôle débitVBRDébit variable. Économise la bande passante quand rien ne bouge.
Qualité image1 (meilleure)On garde la meilleure qualité même en SD pour la détection.
+
+
+
+
+ + +
+
+

URLs RTSP

+

Format des URLs pour caméras Dahua. Remplacer l'IP par celle de la caméra.

+
+
+
+
+ Flux 1 — HD + Capture de preuve +
+ rtsp://admin:password@192.168.1.196/cam/realmonitor?channel=1&subtype=0 +
+
+
+ Flux 2 — SD + Détection YOLO 24/7 +
+ rtsp://admin:password@192.168.1.196/cam/realmonitor?channel=1&subtype=1 +
+
+ Note : Le channel correspond au numéro de canal de la caméra (1 pour une caméra mono-objectif). Le subtype sélectionne le flux : 0 = principal (HD), 1 = secondaire (SD). +
+
+
+ + +
+
+

Checklist configuration caméra

+
+
+
    +
  1. 1. Accéder à l'interface web de la caméra (http://IP_CAM)
  2. +
  3. 2. Aller dans Paramètres → Caméra → Vidéo
  4. +
  5. 3. Configurer le Premier flux (HD) selon le tableau ci-dessus
  6. +
  7. 4. Configurer le Deuxième flux (SD) selon le tableau ci-dessus
  8. +
  9. 5. Cliquer Appliquer
  10. +
  11. 6. Tester les 2 flux RTSP avec VLC (vlc rtsp://...)
  12. +
  13. 7. Vérifier que SmartEye reçoit le flux 2 et détecte correctement
  14. +
+
+
+ +
+ + diff --git a/heartbeat.php b/heartbeat.php new file mode 100644 index 0000000..dc7f897 --- /dev/null +++ b/heartbeat.php @@ -0,0 +1,59 @@ + false, "message" => "POST uniquement"])); +} + +$input = json_decode(file_get_contents("php://input"), true); +$client_id = $input['client_id'] ?? ''; +$token = $input['token'] ?? ''; + +if (empty($client_id) || empty($token)) { + http_response_code(400); + die(json_encode(["success" => false, "message" => "client_id et token requis"])); +} + +$DB_FILE = "database.json"; +$db = json_decode(file_get_contents($DB_FILE), true); + +// Authentification +$found_key = null; +foreach ($db['clients'] as $key => $c) { + if (strcasecmp($c['name'] ?? '', $client_id) === 0 && $c['token'] === $token) { + $found_key = $key; + break; + } +} + +if ($found_key === null) { + http_response_code(403); + die(json_encode(["success" => false, "message" => "Authentification échouée"])); +} + +// Mise à jour heartbeat +$db['clients'][$found_key]['last_heartbeat'] = date("Y-m-d H:i:s"); +$db['clients'][$found_key]['site_status'] = "active"; +$db['clients'][$found_key]['uptime'] = $input['uptime'] ?? 0; +$db['clients'][$found_key]['cameras_active'] = $input['cameras_active'] ?? 0; + +file_put_contents($DB_FILE, json_encode($db, JSON_PRETTY_PRINT)); + +echo json_encode([ + "success" => true, + "message" => "Heartbeat reçu", + "server_time" => date("Y-m-d H:i:s") +]); +?> \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..73fd8fc --- /dev/null +++ b/index.html @@ -0,0 +1,328 @@ + + + + + + + LUCAS - Veiller sans surveiller + + + + + + +
+
+

Leur maison est leur royaume.
Qu'elle le reste.

+

Veiller sans surveiller. La première Intelligence Artificielle qui protège vos parents en cas de chute, + sans jamais filmer leur quotidien.

+ Découvrir la révolution LUCAS +
+
+ +
+
+

Vous voulez les savoir en sécurité.
Ils veulent simplement vivre libres.

+

La dignité n'est pas une option. C'est un droit absolu.

Fini le + choix impossible entre l'angoisse silencieuse de la chute et l'humiliation d'une caméra de surveillance + qui les infantilise. Il existe désormais une autre voie.

+
+
+ +
+
+

Une présence invisible.
Une tranquillité absolue.

+
+
+
+
+

Il ressent, il ne regarde pas

+

LUCAS analyse les postures et les mouvements sans aucune lentille photographique. Zéro image, zéro + vidéo. L'intimité est totalement sacrée.

+
+
+
+

Il ne dort jamais

+

Une chute soudaine ? Une immobilité anormale ? L'IA détecte l'urgence en une fraction de seconde et + alerte instantanément les proches.

+
+
+
💬
+

Il recrée le lien

+

Grâce à son intercom audio bidirectionnel de haute qualité, parlez directement à votre proche pour le + rassurer en attendant les secours.

+
+
+
+ +
+
+
+

Pour elle :
La vie, tout simplement.

+

Aucune installation compliquée. Pas de bouton rouge stigmatisant à porter autour du cou. Pas de + regard inquisiteur dans le salon. LUCAS est un objet d'art discret qui se fond dans le décor. Elle + vit sa vie, libre et digne, avec la certitude qu'en cas de pépin, elle ne sera jamais seule.

+
+
+
+
+

Pour vous :
Le souffle retrouvé.

+

Le poids de la culpabilité s'envole. Vous n'avez plus besoin d'appeler trois fois par jour juste pour + "vérifier". Votre smartphone ne s'allume que si c'est strictement nécessaire. Vous protégez ce que + vous avez de plus cher, tout en reprenant le cours de votre propre vie.

+
+
+
+ + + + + + + \ No newline at end of file diff --git a/landing-liberte.html b/landing-liberte.html new file mode 100644 index 0000000..9163c8a --- /dev/null +++ b/landing-liberte.html @@ -0,0 +1,1403 @@ + + + + + + LUCAS — Vivre chez soi. Librement. + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ + Sérénité connectée +
+ +

+ Vivre chez soi.
Librement. +

+

Et si vos parents pouvaient rester chez eux en toute sécurité ?

+ +

+ LUCAS est l'ange gardien invisible de vos proches. + Une intelligence artificielle bienveillante qui veille sans surveiller, + qui protège sans envahir, et qui vous alerte uniquement quand c'est nécessaire. + Eux vivent librement. Vous dormez tranquille. +

+ + +
+ +
+ +
+
+ +
+ Découvrir +
+
+
+ + +
+
+
+
La promesse
+

Trois libérations en une seule solution

+

+ LUCAS ne se contente pas de détecter les dangers. + Il offre à chaque famille quelque chose d'inestimable. +

+
+ +
+
+
+ +
+

La liberté de rester chez soi

+

+ Vos parents veulent vieillir dans leur maison, entourés de leurs souvenirs. + LUCAS leur permet de garder cette autonomie précieuse, + en toute sécurité. +

+
+
+
+ +
+

La sérénité pour les proches

+

+ Fini cette angoisse au fond de l'estomac chaque soir. + LUCAS veille pour vous et vous prévient uniquement si quelque chose + nécessite votre attention. +

+
+
+
+ +
+

La dignité intacte

+

+ Pas de caméra qui filme en continu, pas de bracelet à porter. + Votre parent vit normalement. Il ne se sent ni observé, ni diminué, + ni dépendant. +

+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
« Je fais mon jardin, je reçois mes amies, je vis ma vie. Et mes enfants sont rassurés. »
+
Michèle, 78 ans
+
+
+
+
+
+
« Maman ne voulait pas de caméra. Avec LUCAS, elle a accepté tout de suite. Elle ne le voit même pas. »
+
Nathalie, fille aidante
+
+
+
+
+
+
« C'est la première fois en trois ans que je pars en week-end sans angoisse. »
+
Frédéric, fils aidant
+
+
+
+
+ +
+
Pourquoi LUCAS
+

Parce que vos parents méritent mieux qu'une caméra

+ +
+

+ Vos parents ont bâti une vie. Élevé des enfants. Traversé des décennies. + Ils méritent de vivre chez eux avec la même fierté, la même liberté + qu'ils ont toujours eue. +

+

+ Les caméras de surveillance les font se sentir observés, infantilisés, + diminués. Les bracelets d'alerte finissent dans un tiroir. Les boutons d'urgence + ne sont jamais pressés — par fierté, par oubli, ou parce que la chute + ne laisse pas le temps. +

+

+ LUCAS prend une autre voie : veiller sans surveiller. +

+
+ +
+ +
+ LUCAS ne regarde pas. LUCAS comprend.
+ Son IA n'analyse que les situations anormales — une chute, + un malaise, une immobilité prolongée. Le reste du temps, rien. + Votre parent vit sa vie. LUCAS est là, invisible, bienveillant, prêt. +
+
+
+ +
+
+
+ + +
+
+
+
Comment ça marche
+

Simple comme bonjour

+

+ Aucune installation complexe. Aucune action requise de votre parent. + LUCAS s'installe en quelques minutes et fonctionne tout seul. +

+
+ +
+
+
1
+

Un capteur discret

+

Un petit boîtier élégant se fond dans le décor. Il ne ressemble pas à une caméra. Il veille en silence.

+ 10 min d'installation +
+
+
2
+

L'IA analyse

+

Deux niveaux d'intelligence artificielle détectent les situations anormales et filtrent les faux positifs.

+ Double IA embarquée +
+
+
3
+

L'alerte part

+

Notification instantanée sur votre smartphone avec le rapport d'analyse et un bouton d'appel aux secours.

+ En moins de 10 secondes +
+
+
4
+

Parlez-lui

+

Grâce à l'intercom, rassurez votre proche instantanément ou évaluez la situation à distance.

+ Audio en temps réel +
+
+
+
+ + +
+
+
+
Pour qui
+

Eux vivent. Vous respirez.

+

+ LUCAS crée un lien invisible de sécurité entre deux générations. +

+
+ +
+
+
+ +
+

Pour vos parents

+

+ Ils continuent de vivre exactement comme avant. Leur jardin, + leurs habitudes, leurs amis à la maison. Rien ne change — + sauf qu'un ange gardien invisible veille désormais sur eux. +

+
    +
  • Rien à porter, rien à recharger
  • +
  • Aucune action requise de leur part
  • +
  • Leur intimité 100% respectée
  • +
  • Ils restent chez eux, libres et protégés
  • +
+
+
+
+ +
+

Pour vous, les proches

+

+ Vous avez votre vie, votre travail, vos enfants. + LUCAS prend le relais quand vous n'êtes pas là. + Vous êtes prévenu si besoin — sinon, profitez de votre soirée. +

+
    +
  • Alerte instantanée sur votre téléphone
  • +
  • Intercom pour parler à votre parent
  • +
  • Appel aux secours en un geste
  • +
  • Tranquillité d'esprit, enfin
  • +
+
+
+
+
+ + +
+
+

+ « On ne peut pas être partout.
+ Mais on peut veiller de partout. » +

+

+ LUCAS est cette présence bienveillante que vous aimeriez offrir à ceux qui comptent. +

+ + Découvrir LUCAS pour votre famille + + +
+
+ + +
+
+
+
L'impact
+

LUCAS en quelques chiffres

+

+ Chaque chiffre représente une famille qui dort mieux, un parent qui vit libre. +

+
+ +
+
+
<0s
+
entre la détection et l'alerte sur votre téléphone
+
+
+
0h/24
+
de veille continue, silencieuse et bienveillante
+
+
+
0
+
caméra qui filme votre parent au quotidien
+
+
+
0%
+
de dignité préservée. Zéro compromis.
+
+
+
+
+ + +
+
+
+
Technologie
+

La sécurité derrière la simplicité

+

+ LUCAS est simple à utiliser. Mais sous le capot, c'est de la technologie de pointe. +

+
+ +
+
+
+ +
+

Double IA

+

Détection locale instantanée + confirmation par IA cloud pour zéro faux positifs.

+
+
+
+ +
+

Chiffrement total

+

Données chiffrées de bout en bout. Aucun tiers n'y accède. Jamais.

+
+
+
+ +
+

Ultra-rapide

+

Moins de 10 secondes entre la détection et la notification. Chaque seconde compte.

+
+
+
+ +
+

Toujours actif

+

Infrastructure cloud redondante. LUCAS veille même quand tout le monde dort.

+
+
+
+
+ + +
+
+
Passez à l'action
+

Offrez la liberté à vos parents.
Offrez-vous la sérénité.

+

+ Recevez une présentation personnalisée de LUCAS et découvrez + comment votre famille peut en bénéficier. +

+
+ + +
+ +
+
+ + +
+ +
+ + + + + + diff --git a/landing-royaume.html b/landing-royaume.html new file mode 100644 index 0000000..73fd8fc --- /dev/null +++ b/landing-royaume.html @@ -0,0 +1,328 @@ + + + + + + + LUCAS - Veiller sans surveiller + + + + + + +
+
+

Leur maison est leur royaume.
Qu'elle le reste.

+

Veiller sans surveiller. La première Intelligence Artificielle qui protège vos parents en cas de chute, + sans jamais filmer leur quotidien.

+ Découvrir la révolution LUCAS +
+
+ +
+
+

Vous voulez les savoir en sécurité.
Ils veulent simplement vivre libres.

+

La dignité n'est pas une option. C'est un droit absolu.

Fini le + choix impossible entre l'angoisse silencieuse de la chute et l'humiliation d'une caméra de surveillance + qui les infantilise. Il existe désormais une autre voie.

+
+
+ +
+
+

Une présence invisible.
Une tranquillité absolue.

+
+
+
+
+

Il ressent, il ne regarde pas

+

LUCAS analyse les postures et les mouvements sans aucune lentille photographique. Zéro image, zéro + vidéo. L'intimité est totalement sacrée.

+
+
+
+

Il ne dort jamais

+

Une chute soudaine ? Une immobilité anormale ? L'IA détecte l'urgence en une fraction de seconde et + alerte instantanément les proches.

+
+
+
💬
+

Il recrée le lien

+

Grâce à son intercom audio bidirectionnel de haute qualité, parlez directement à votre proche pour le + rassurer en attendant les secours.

+
+
+
+ +
+
+
+

Pour elle :
La vie, tout simplement.

+

Aucune installation compliquée. Pas de bouton rouge stigmatisant à porter autour du cou. Pas de + regard inquisiteur dans le salon. LUCAS est un objet d'art discret qui se fond dans le décor. Elle + vit sa vie, libre et digne, avec la certitude qu'en cas de pépin, elle ne sera jamais seule.

+
+
+
+
+

Pour vous :
Le souffle retrouvé.

+

Le poids de la culpabilité s'envole. Vous n'avez plus besoin d'appeler trois fois par jour juste pour + "vérifier". Votre smartphone ne s'allume que si c'est strictement nécessaire. Vous protégez ce que + vous avez de plus cher, tout en reprenant le cours de votre propre vie.

+
+
+
+ + + + + + + \ No newline at end of file diff --git a/mkdocs-smarteye/docs/api/endpoints.md b/mkdocs-smarteye/docs/api/endpoints.md new file mode 100644 index 0000000..c777681 --- /dev/null +++ b/mkdocs-smarteye/docs/api/endpoints.md @@ -0,0 +1,3 @@ +# Endpoints API + +Section en cours de rédaction. Cette page décrira les endpoints de l'API REST, leurs paramètres et leurs réponses. diff --git a/mkdocs-smarteye/docs/api/fcm-payload.md b/mkdocs-smarteye/docs/api/fcm-payload.md new file mode 100644 index 0000000..d6ddcb9 --- /dev/null +++ b/mkdocs-smarteye/docs/api/fcm-payload.md @@ -0,0 +1,3 @@ +# Payload FCM + +Section en cours de rédaction. Cette page décrira la structure des payloads Firebase Cloud Messaging utilisés pour les notifications push. diff --git a/mkdocs-smarteye/docs/architecture/data-flow.md b/mkdocs-smarteye/docs/architecture/data-flow.md new file mode 100644 index 0000000..0551ff3 --- /dev/null +++ b/mkdocs-smarteye/docs/architecture/data-flow.md @@ -0,0 +1,3 @@ +# Flux de données + +Section en cours de rédaction. Cette page décrira le flux de données entre les différents composants du système SmartEye (Jetson, serveur, application mobile). diff --git a/mkdocs-smarteye/docs/architecture/overview.md b/mkdocs-smarteye/docs/architecture/overview.md new file mode 100644 index 0000000..8ca9a83 --- /dev/null +++ b/mkdocs-smarteye/docs/architecture/overview.md @@ -0,0 +1,84 @@ +# Architecture du système + +## Vue d'ensemble + +```mermaid +graph TB + subgraph Domicile["🏠 Domicile du bénéficiaire"] + CAM1[Caméra 1 - Salon] + CAM2[Caméra 2 - Chambre] + CAM3[Caméra 3 - Cuisine] + JETSON[Jetson Orin Nano
SmartEye v30] + CAM1 -->|RTSP local| JETSON + CAM2 -->|RTSP local| JETSON + CAM3 -->|RTSP local| JETSON + end + + subgraph OVH["☁️ Serveur OVH (57.128.74.87)"] + API[api.php
Réception images] + ANALYZE[analyze.py
Gemini 2.5 Flash] + ALERT[alert_manager.py
Cycle de vie alertes] + WATCHDOG[alert_watchdog.py
Escalade cron] + DB[(database.json)] + API --> ANALYZE + ANALYZE --> ALERT + ANALYZE --> DB + WATCHDOG --> ALERT + end + + subgraph Mobile["📱 Téléphones aidants"] + APP1[LucasApp - Fille] + APP2[LucasApp - Fils] + end + + JETSON ==>|SSH Tunnel
Image POST| API + ANALYZE ==>|Firebase Push| APP1 + ANALYZE ==>|Firebase Push| APP2 + JETSON -.->|RTSP Tunnel| APP1 + JETSON -.->|Audio WebSocket| APP1 +``` + +## Composants + +### SmartEye (Jetson Orin Nano) + +| Caractéristique | Détail | +|----------------|--------| +| **Modèle IA** | YOLOv8 (détection) + pose estimation | +| **Caméras max** | 6 par site | +| **Flux** | HD (/11) pour détection, SD (/12) pour streaming | +| **Communication** | SSH tunnels inversés vers OVH | + +### Lucas (Serveur OVH) + +| Caractéristique | Détail | +|----------------|--------| +| **Analyse IA** | Google Gemini 2.5 Flash (vision) | +| **Notifications** | Firebase Cloud Messaging | +| **SMS backup** | OVH SMS API | +| **Dashboard** | PHP + Tailwind CSS | + +### LucasApp (Flutter) + +| Caractéristique | Détail | +|----------------|--------| +| **Plateforme** | Android (Flutter) | +| **Flux vidéo** | RTSP via tunnels SSH | +| **Interphone** | WebSocket audio bidirectionnel (PCM A-law) | +| **Acquittement** | POST vers alert_ack.php | + +## Convention de ports + +Chaque site client utilise une plage de 10 ports : + +``` +Site N → base_port = 8550 + (N-1) × 10 + +Offset 0-5 : RTSP caméras (max 6) +Offset 6 : Audio WebSocket relay +Offset 7 : WebUI locale SmartEye +Offset 8-9 : Réservés +``` + +!!! note "Site pilote (Aléria)" + Le site pilote conserve ses ports historiques : 8554/8555/8556 (RTSP) + 8800 (audio). diff --git a/mkdocs-smarteye/docs/assets/images/deploiement/changement-password.png b/mkdocs-smarteye/docs/assets/images/deploiement/changement-password.png new file mode 100644 index 0000000..054f5d9 Binary files /dev/null and b/mkdocs-smarteye/docs/assets/images/deploiement/changement-password.png differ diff --git a/mkdocs-smarteye/docs/assets/images/deploiement/configuration-sans-fil.png b/mkdocs-smarteye/docs/assets/images/deploiement/configuration-sans-fil.png new file mode 100644 index 0000000..eb7c564 Binary files /dev/null and b/mkdocs-smarteye/docs/assets/images/deploiement/configuration-sans-fil.png differ diff --git a/mkdocs-smarteye/docs/assets/images/deploiement/informations-appareil.jpeg b/mkdocs-smarteye/docs/assets/images/deploiement/informations-appareil.jpeg new file mode 100644 index 0000000..87d298e Binary files /dev/null and b/mkdocs-smarteye/docs/assets/images/deploiement/informations-appareil.jpeg differ diff --git a/mkdocs-smarteye/docs/assets/images/deploiement/mes-cameras.png b/mkdocs-smarteye/docs/assets/images/deploiement/mes-cameras.png new file mode 100644 index 0000000..5bf4b5a Binary files /dev/null and b/mkdocs-smarteye/docs/assets/images/deploiement/mes-cameras.png differ diff --git a/mkdocs-smarteye/docs/assets/images/deploiement/mise-en-place-nom.png b/mkdocs-smarteye/docs/assets/images/deploiement/mise-en-place-nom.png new file mode 100644 index 0000000..337a81c Binary files /dev/null and b/mkdocs-smarteye/docs/assets/images/deploiement/mise-en-place-nom.png differ diff --git a/mkdocs-smarteye/docs/assets/images/deploiement/settings-chambre.png b/mkdocs-smarteye/docs/assets/images/deploiement/settings-chambre.png new file mode 100644 index 0000000..93486ea Binary files /dev/null and b/mkdocs-smarteye/docs/assets/images/deploiement/settings-chambre.png differ diff --git a/mkdocs-smarteye/docs/assets/images/deploiement/trouver-camera.png b/mkdocs-smarteye/docs/assets/images/deploiement/trouver-camera.png new file mode 100644 index 0000000..556720b Binary files /dev/null and b/mkdocs-smarteye/docs/assets/images/deploiement/trouver-camera.png differ diff --git a/mkdocs-smarteye/docs/deploiement/carte-gold.md b/mkdocs-smarteye/docs/deploiement/carte-gold.md new file mode 100644 index 0000000..ea5dcbf --- /dev/null +++ b/mkdocs-smarteye/docs/deploiement/carte-gold.md @@ -0,0 +1,330 @@ +# Preparation de la carte Gold (image maitre) + +
+

Document reserve au responsable technique

+

Cette procedure ne se fait qu'une seule fois. Elle produit l'image maitre qui sera clonee pour chaque nouveau client.

+
+ +--- + +## Qu'est-ce que la carte Gold ? + +La carte Gold est une carte SD **de reference** contenant : + +- Le systeme d'exploitation (JetPack / Linux) +- Les drivers GPU (CUDA, cuDNN, TensorRT) +- SmartEye complet (YOLO, modeles IA, services) +- **Aucune configuration client** (pas de cameras, pas de compte, pas de logs) + +C'est l'equivalent de l'image usine d'une box Free ou Orange. Le Jetson qui demarre avec cette carte arrive directement en **mode installation**, pret a etre configure depuis un smartphone. + +--- + +## Principe : on ne touche JAMAIS a la carte de dev + +``` +Carte actuelle (DEV/Aleria) Carte neuve n°1 Carte neuve n°2, 3, 4... +┌─────────────────────┐ ┌─────────────┐ ┌─────────────┐ +│ SmartEye + Demo_01 │──── clone ──→│ Copie brute │ │ │ +│ Cameras configurees │ │ (tout dedans)│ │ │ +│ Logs, images, etc. │ └──────┬──────┘ │ Client X │ +│ │ │ │ │ +│ ON N'Y TOUCHE PAS │ nettoyage config │ │ +│ │ suppression logs └─────────────┘ +└─────────────────────┘ suppression images ▲ + │ │ + ▼ │ + ┌──────────────┐ clone a l'infini + │ CARTE GOLD │────────────────┘ + │ (mode usine)│ + └──────────────┘ +``` + +!!! warning "Regle d'or" + La carte du Jetson de developpement (Aleria/Demo_01) **reste en place, intacte**. On la clone d'abord, puis on nettoie le clone. Comme ca, si quelque chose se passe mal, le Jetson de dev est toujours operationnel. + +--- + +## Pre-requis + +| Element | Detail | +|---------|--------| +| **Jetson de dev** | Le Jetson actuel (Aleria) qui fonctionne | +| **Carte SD neuve** | 64 Go minimum, classe A2/U3 | +| **PC Linux ou Mac** | Pour cloner et nettoyer | +| **Lecteur de carte SD** | USB ou integre | +| **Espace disque** | ~64 Go libres sur le PC | +| **Balena Etcher** | [balena.io/etcher](https://www.balena.io/etcher/) (gratuit) | + +--- + +!!! danger "Carte SD de reference" + La carte originale du Jetson de dev est une **Samsung EVO 128 Go**. + Les cartes clones doivent etre de capacite **egale ou superieure** et idealement de la meme marque. + Deux cartes "128 Go" de marques differentes n'ont pas exactement la meme taille en octets. + Si la carte clone est plus petite (meme de quelques Mo), le clone sera corrompu et ne bootera pas. + +## Etape 1 — Cloner la carte actuelle (telle quelle) + +Eteindre le Jetson de dev proprement : + +```bash +# Sur le Jetson +sudo shutdown -h now +# Attendre extinction complete (LED eteintes) +``` + +Retirer la carte SD et l'inserer dans le lecteur du PC. + +=== "Linux" + + ```bash + # Identifier la carte SD + lsblk + + # Cloner la carte entiere (NE RIEN MODIFIER) + sudo dd if=/dev/sdb of=$HOME/smarteye-dev-backup.img bs=4M status=progress + + # Compresser le backup + gzip $HOME/smarteye-dev-backup.img + ``` + +=== "Mac" + + ```bash + # Identifier la carte SD + diskutil list + + # Demonter sans ejecter + diskutil unmountDisk /dev/disk4 + + # Cloner (~ 1h pour une carte 128 Go) + sudo dd if=/dev/rdisk4 of=/Users/$(whoami)/smarteye-dev-backup.img bs=4m status=progress + + # Compresser + gzip /Users/$(whoami)/smarteye-dev-backup.img + ``` + + !!! tip "Pourquoi `/Users/$(whoami)/` et pas `~/` ?" + Avec `sudo`, le `~` pointe vers `/var/root/` au lieu de votre dossier utilisateur. `$(whoami)` resout ce probleme en inserant automatiquement votre nom d'utilisateur. + +!!! tip "Conserver ce backup" + Le fichier `smarteye-dev-backup.img.gz` est votre **sauvegarde de securite** du Jetson de dev. Gardez-le precieusement. En cas de probleme, vous pouvez toujours reflasher la carte d'origine. + +**Remettre immediatement la carte dans le Jetson de dev** et le redemarrer. Il reprend son fonctionnement normal. + +--- + +## Etape 2 — Flasher le clone sur une carte neuve + +Inserer une **carte SD neuve** (128 Go) dans le lecteur. + +=== "Balena Etcher (recommande)" + + 1. Ouvrir Balena Etcher + 2. **Flash from file** → selectionner `smarteye-dev-backup.img.gz` + 3. **Select target** → selectionner la carte SD neuve + 4. **Flash!** → attendre ~10 minutes + +=== "Linux" + + ```bash + # 1. Verifier que la carte neuve est detectee + lsblk + # Reperer la carte neuve par sa taille (ex: /dev/sdb, 128 Go) + + # 2. Flasher + gunzip -c $HOME/smarteye-dev-backup.img.gz | sudo dd of=/dev/sdb bs=4M status=progress + sync + ``` + +=== "Mac" + + ```bash + # 1. Verifier que la carte neuve est detectee + diskutil list + # Reperer la carte par sa taille (ex: /dev/disk4, 134.2 GB = 128 Go) + + # 2. Demonter la carte sans l'ejecter + diskutil unmountDisk /dev/disk4 + + # 3. Flasher (~ 1h pour 128 Go, adapter /dev/rdisk4 si necessaire) + gunzip -c /Users/$(whoami)/smarteye-dev-backup.img.gz | sudo dd of=/dev/rdisk4 bs=4m status=progress + ``` + + !!! info "Pourquoi 3 commandes separees ?" + - `diskutil list` → **verifier** qu'on cible la bonne carte (pas le disque du Mac !) + - `diskutil unmountDisk` → **liberer** la carte pour que `dd` puisse y ecrire + - `gunzip -c ... | dd` → **decompresser et flasher** en une seule passe + +A ce stade, la carte neuve est une **copie exacte** de la carte de dev (avec Demo_01, les cameras, les logs — tout). + +--- + +## Etape 3 — Nettoyer le clone (le transformer en Gold) + +Inserer la carte neuve (le clone) dans le Jetson **ou** la monter sur le PC pour nettoyer. + +### Option A : Nettoyer depuis le Jetson + +Inserer la carte clone dans un Jetson, le demarrer, puis : + +```bash +# 1. Arreter SmartEye +sudo systemctl stop smarteye + +# 2. Supprimer la configuration client (Demo_01) +rm -f /w/smarteye_config.yaml + +# 3. Supprimer toutes les images capturees +rm -rf /w/runs/* + +# 4. Supprimer les logs +rm -rf /w/logs/* +rm -f /w/*.log + +# 5. Nettoyer les known_hosts SSH (specifiques au site) +rm -f ~/.ssh/known_hosts + +# 6. Vider l'historique bash +history -c +> ~/.bash_history +``` + +### Option B : Nettoyer depuis le PC (sans Jetson) + +Si vous n'avez qu'un seul Jetson, monter la carte sur le PC : + +```bash +# Monter la partition principale de la carte SD +sudo mount /dev/sdb1 /mnt + +# Nettoyer (adapter les chemins selon la structure) +sudo rm -f /mnt/home/*/w/smarteye_config.yaml +sudo rm -rf /mnt/home/*/w/runs/* +sudo rm -rf /mnt/home/*/w/logs/* +sudo rm -f /mnt/home/*/.ssh/known_hosts +sudo rm -f /mnt/home/*/.bash_history + +# Demonter +sudo umount /mnt +``` + +### Verification du nettoyage + +``` +=== Verification === +Config client : ABSENT ✓ +Images : 0 fichiers ✓ +Logs : 0 fichiers ✓ +known_hosts : ABSENT ✓ +``` + +--- + +## Etape 4 — Verifier le mode setup + +Si vous avez nettoye depuis le Jetson (Option A), verifier que SmartEye demarre en mode installation : + +```bash +sudo systemctl start smarteye +curl http://localhost:8080/api/identity +``` + +Reponse attendue : + +```json +{ + "type": "smarteye", + "version": "v30", + "status": "setup", + "message": "En attente de configuration" +} +``` + +Puis eteindre : + +```bash +sudo shutdown -h now +``` + +--- + +## Etape 5 — Creer l'image Gold definitive + +Retirer la carte nettoyee du Jetson et la recloner en tant que **Gold** : + +=== "Linux" + + ```bash + # La carte nettoyee est dans le lecteur + lsblk + sudo dd if=/dev/sdb of=$HOME/smarteye-gold-v30.img bs=4M status=progress + gzip $HOME/smarteye-gold-v30.img + + # Resultat final + ls -lh $HOME/smarteye-gold-v30.img.gz + ``` + +=== "Mac" + + ```bash + # La carte nettoyee est dans le lecteur + diskutil list + diskutil unmountDisk /dev/disk4 + + sudo dd if=/dev/rdisk4 of=/Users/$(whoami)/smarteye-gold-v30.img bs=4m status=progress + gzip /Users/$(whoami)/smarteye-gold-v30.img + + # Resultat final + ls -lh /Users/$(whoami)/smarteye-gold-v30.img.gz + ``` + +!!! success "Votre image Gold est prete" + Le fichier `smarteye-gold-v30.img.gz` est votre image maitre. C'est a partir de ce fichier que vous [clonerez chaque nouveau Jetson](clonage.md). + +--- + +## Resume des cartes + +| Carte | Contenu | Usage | +|-------|---------|-------| +| **Carte de dev** | SmartEye + Demo_01 + cameras | Reste dans le Jetson de dev, **jamais modifiee** | +| **Backup dev** | `smarteye-dev-backup.img.gz` | Sauvegarde de securite, au cas ou | +| **Image Gold** | `smarteye-gold-v30.img.gz` | Image vierge, **source de tous les clones** | +| **Carte client** | Flashee depuis la Gold | Une par client, configuree sur site | + +--- + +## Checklist finale + +- [ ] Carte de dev clonee → `smarteye-dev-backup.img.gz` +- [ ] Carte de dev remise dans le Jetson, fonctionnement verifie +- [ ] Clone flashe sur carte neuve +- [ ] Clone nettoye (pas de config, pas d'images, pas de logs) +- [ ] Mode setup verifie (JSON "status: setup") +- [ ] Clone nettoye reclone → `smarteye-gold-v30.img.gz` +- [ ] Image Gold stockee en lieu sur + +--- + +## Ou stocker les images ? + +| Fichier | Taille estimee | Ou le garder | +|---------|:--------------:|--------------| +| `smarteye-dev-backup.img.gz` | ~8-15 Go | PC + disque externe | +| `smarteye-gold-v30.img.gz` | ~8-15 Go | PC + disque externe + serveur OVH | + +!!! info "Versionnement" + A chaque mise a jour de SmartEye, creer une nouvelle image Gold : + + ``` + smarteye-gold-v30-2026-02-21.img.gz ← actuelle + smarteye-gold-v31-2026-03-15.img.gz ← apres mise a jour + ``` + +--- + +## Etape suivante + +→ [Cloner et deployer un nouveau Jetson](clonage.md) diff --git a/mkdocs-smarteye/docs/deploiement/clonage.md b/mkdocs-smarteye/docs/deploiement/clonage.md new file mode 100644 index 0000000..a920385 --- /dev/null +++ b/mkdocs-smarteye/docs/deploiement/clonage.md @@ -0,0 +1,239 @@ +# Clonage des cartes SD (au bureau) + +
+

Operations internes — production en serie

+

Cloner, flasher, verifier et reparer les cartes SD avant expedition. Tout en ligne de commande depuis un Mac.

+
+ +--- + +## Image de reference + +| Fichier | Taille | Statut | +|---------|:------:|--------| +| `smarteye-dev-backup.img` | ~54 Go | **Fonctionnelle** — validee le 22/02/2026 | + +Localisation Mac : `/Users/rachid/jetson_backup/smarteye-dev-backup.img` + +!!! warning "Sauvegarde obligatoire" + Cette image doit exister en **au moins 2 exemplaires** : + + - Sur le Mac (disque local ou externe) + - Sur le serveur OVH : `scp smarteye-dev-backup.img debian@lucas.unigest.fr:/home/debian/backups/` + +--- + +## 1 — Flasher une carte SD (production) + +C'est l'operation principale. Pour chaque nouveau client, on flashe une carte SD neuve depuis l'image de reference. + +### Materiel + +- Carte SD neuve **SanDisk Extreme Pro** 64 Go minimum +- Lecteur de carte SD (integre ou USB) + +### Procedure + +```bash +# Identifier la carte SD +diskutil list +# Reperer la carte par sa taille et ses partitions +# Le lecteur integre du Mac peut apparaitre comme "internal, physical", c'est normal + +# Demonter sans ejecter +diskutil unmountDisk /dev/diskN + +# Flasher (remplacer N par le bon numero) +sudo dd if=/Users/rachid/jetson_backup/smarteye-dev-backup.img of=/dev/rdiskN bs=4m status=progress + +# Ejecter proprement +diskutil eject /dev/diskN +``` + +!!! danger "Verifier le numero de disque" + Toujours verifier **deux fois** le `/dev/diskN` avant de lancer `dd`. Se tromper de disque = ecraser le disque du Mac. + +Le `r` devant `diskN` (→ `rdiskN`) utilise le mode raw, **5x plus rapide**. + +Duree : ~20-25 min pour 54 Go a ~39 MB/s. + +### Verification rapide + +Apres le flash, avant d'ejecter : + +```bash +diskutil list /dev/diskN +``` + +Resultat attendu : une **GUID_partition_scheme** avec ~14 partitions Linux + 1 partition EFI. C'est la structure standard du Jetson Nano. + +### Production en serie + +Pour flasher plusieurs cartes a la suite : + +1. Flasher la carte → ejecter +2. Etiqueter la carte (numero de serie / nom client) +3. Inserer la carte dans un Jetson +4. Inserer la carte suivante dans le lecteur → recommencer + +La box part chez le client **carte SD deja inseree, prete a brancher**. + +--- + +## 2 — Creer une nouvelle image de reference (backup) + +Quand SmartEye evolue (nouvelle version YOLO, corrections, etc.), il faut creer une nouvelle image de reference depuis le Jetson de dev. + +### Procedure + +Eteindre le Jetson proprement, retirer la carte SD, l'inserer dans le lecteur du Mac. + +```bash +# Identifier la carte SD +diskutil list + +# Demonter +diskutil unmountDisk /dev/diskN + +# Cloner bit-a-bit +sudo dd if=/dev/rdiskN of=/Users/rachid/jetson_backup/smarteye-dev-backup.img bs=4m status=progress + +# Ejecter +diskutil eject /dev/diskN +``` + +Duree : ~20-25 min pour une carte 64 Go. + +### Nommage et archivage + +``` +smarteye-dev-backup-2026-02-22.img ← actuelle (validee) +smarteye-dev-backup-2026-03-XX.img ← future +``` + +Conserver les anciennes images. Copier sur OVH : + +```bash +scp smarteye-dev-backup.img debian@lucas.unigest.fr:/home/debian/backups/ +``` + +--- + +## 3 — Verifier une image (sans carte SD) + +Pour inspecter une image `.img` sans la flasher : + +```bash +hdiutil attach -nomount -readonly /Users/rachid/jetson_backup/smarteye-dev-backup.img +``` + +Resultat attendu : + +``` +/dev/disk6 GUID_partition_scheme +/dev/disk6s1 EFI +/dev/disk6s2 - s15 Linux Filesystem (multiple) +``` + +Detacher apres verification : + +```bash +hdiutil detach /dev/disk6 +``` + +--- + +## 4 — Reparer une image corrompue + +Si une carte SD ne boot plus (erreur `mmcblk0p1 not found` sur le Jetson), la table de partitions est corrompue. Les donnees sont generalement encore intactes. + +### Diagnostic sur le Jetson + +Si le Jetson tombe dans un shell d'urgence (`bash-5.1#`) : + +```bash +ls /dev/mmcblk0* +``` + +- `mmcblk0` present mais pas `mmcblk0p1` → table de partitions corrompue → reparable +- `mmcblk0` absent → probleme hardware (carte morte ou mauvais contact) + +### Etape 1 : testdisk (retrouver les partitions) + +```bash +brew install testdisk +testdisk /Users/rachid/jetson_backup/smarteye-dev-backup.img +``` + +1. **Create** → nouveau log +2. Selectionner l'image → **Proceed** +3. Type de table → **Intel** +4. **Analyse** → **Quick Search** +5. testdisk retrouve les partitions (FAT32 + Linux) +6. **Entree** pour continuer → **Write** → **Y** + +!!! warning "testdisk ne suffit pas" + testdisk ecrit une table MBR. Le Jetson Nano utilise une table **GPT** avec ~14 partitions. Il faut enchainer avec gdisk. + +### Etape 2 : gdisk (restaurer la GPT) + +```bash +brew install gptfdisk +gdisk /Users/rachid/jetson_backup/smarteye-dev-backup.img +``` + +gdisk detecte le conflit : + +``` +Found valid MBR and corrupt GPT. Which do you want to use? + 1 - MBR + 2 - GPT + 3 - Create blank GPT +``` + +1. Taper **`2`** (GPT — le header principal est souvent intact) +2. Taper **`x`** (menu expert) +3. Taper **`e`** (relocate backup structures to end of disk) +4. Taper **`m`** (retour menu principal) +5. Taper **`w`** → **Y** + +Si gdisk refuse avec "Secondary partition table overlaps the last partition by N blocks" : + +```bash +# Dans un AUTRE terminal, agrandir l'image +dd if=/dev/zero bs=512 count=200 >> /Users/rachid/jetson_backup/smarteye-dev-backup.img +``` + +Puis **quitter** gdisk (`q`) et le **relancer** (il doit recharger la nouvelle taille). Recommencer : 2 → x → e → m → w → Y. + +### Verification finale + +```bash +hdiutil attach -nomount -readonly /Users/rachid/jetson_backup/smarteye-dev-backup.img +``` + +GUID_partition_scheme avec ~14 partitions = image reparee. Flasher sur une carte SD et tester le boot. + +--- + +## Fiabilite en production + +!!! danger "Cartes SD : prototypage seulement" + Les cartes SD ont une duree de vie limitee en ecriture. Docker + YOLO = beaucoup d'ecritures. Pour une production a plusieurs centaines de clients : + + - **SSD SATA via USB 3.0** pour le systeme (fiabilite industrielle) + - **Carte SD uniquement pour le boot initial** (lecture seule) + - **Backup automatique** des configs vers le serveur OVH + + | Composant | Cout unitaire | + |-----------|:------------:| + | SD 32 Go (boot) | ~10 EUR | + | SSD 120 Go SATA | ~15 EUR | + | Adaptateur USB 3.0 → SATA | ~8 EUR | + | **Total par box** | **~33 EUR** | + +--- + +## Etape suivante + +→ [Installation chez le client](installation-client.md) diff --git a/mkdocs-smarteye/docs/deploiement/config-video-camera.md b/mkdocs-smarteye/docs/deploiement/config-video-camera.md new file mode 100644 index 0000000..8a2a097 --- /dev/null +++ b/mkdocs-smarteye/docs/deploiement/config-video-camera.md @@ -0,0 +1,110 @@ +# Configuration video des cameras + +
+

Les 2 flux RTSP — Detection & Preuve HD

+

Chaque camera diffuse 2 flux simultanes utilises pour des besoins differents par SmartEye.

+
+ +--- + +## Comprendre les 2 flux + +Chaque camera IP diffuse **2 flux video RTSP** en parallele : + +| | Flux 2 (SD) | Flux 1 (HD) | +|---|---|---| +| **Subtype** | `subtype=1` | `subtype=0` | +| **Resolution** | 800 x 448 | 2880 x 1620 | +| **Usage** | Detection YOLO 24/7 | Capture preuve en cas de chute | +| **Debit** | 300 kbps | 6144 kbps | +| **Quand** | En permanence | Uniquement lors d'une detection | + +```mermaid +graph LR + CAM[Camera IP] -->|Flux 2 - SD| YOLO[Jetson YOLO] + YOLO -->|Chute detectee| CAPTURE[Capture HD] + CAM -->|Flux 1 - HD| CAPTURE + CAPTURE -->|POST image| LUCAS[Lucas api.php] + LUCAS -->|Gemini analyse| AVA[Push AVA] +``` + +!!! question "Pourquoi 2 flux ?" + **YOLO n'a pas besoin de HD** pour detecter une personne. Un flux leger (800x448) permet d'analyser plus de FPS avec moins de charge GPU. Par contre, **l'image de preuve doit etre nette** pour que Gemini puisse analyser finement et pour que la famille voie clairement la situation. + +--- + +## Parametres recommandes + +### Parametres generaux + +| Parametre | Valeur | +|-----------|--------| +| Format video | **50Hz** | +| Codage video | **H.264 Main Profile** | + +!!! warning "H.264 obligatoire" + Le H.265 n'est pas supporte par tous les lecteurs RTSP. Toujours utiliser H.264 Main Profile. + +### Flux 1 — Preuve HD (subtype=0) + +| Parametre | Valeur | Explication | +|-----------|--------|-------------| +| Resolution | **2880 x 1620** | Resolution max. Image nette pour Gemini et la famille | +| Debit binaire | **6144 kbps** | Debit max. Qualite optimale pour la capture | +| Frequence | **20 fps** | Suffisant pour un screenshot net | +| Intervalle image cle | **20** | 1 keyframe/seconde. Capture plus rapide | +| Controle debit | **CBR** | Debit constant. Qualite stable en mouvement | +| Qualite image | **1** (meilleure) | Valeur basse = qualite max | + +### Flux 2 — Detection YOLO (subtype=1) + +| Parametre | Valeur | Explication | +|-----------|--------|-------------| +| Resolution | **800 x 448** | YOLO detecte bien en SD. Economise le GPU | +| Debit binaire | **300 kbps** | 20x moins que le flux HD | +| Frequence | **15 fps** | Suffisant pour detecter un mouvement | +| Intervalle image cle | **30** | 1 keyframe toutes les 2 secondes | +| Controle debit | **VBR** | Debit variable. Economise la bande passante au repos | +| Qualite image | **1** (meilleure) | Meilleure qualite meme en SD | + +--- + +## URLs RTSP + +Format pour cameras Dahua : + +=== "Flux 1 — HD (preuve)" + + ``` + rtsp://admin:password@192.168.1.196/cam/realmonitor?channel=1&subtype=0 + ``` + +=== "Flux 2 — SD (detection)" + + ``` + rtsp://admin:password@192.168.1.196/cam/realmonitor?channel=1&subtype=1 + ``` + +!!! info "Parametres URL" + - `channel` : numero de canal (1 pour camera mono-objectif) + - `subtype` : **0** = flux principal (HD), **1** = flux secondaire (SD) + +--- + +## Procedure de configuration + +1. Acceder a l'interface web de la camera (`http://IP_CAMERA`) +2. Aller dans **Parametres > Camera > Video** +3. Configurer le **Premier flux** (HD) selon le tableau ci-dessus +4. Configurer le **Deuxieme flux** (SD) selon le tableau ci-dessus +5. Cliquer **Appliquer** +6. Tester les 2 flux avec VLC : `vlc rtsp://admin:password@IP/cam/realmonitor?channel=1&subtype=1` +7. Verifier que SmartEye recoit le flux 2 et detecte correctement + +--- + +## Voir aussi + +- [Installation camera IP](installation-camera.md) — Ajouter une camera au reseau +- [Checklist installation](https://lucas.unigest.fr/checklist.php) — Suivi complet d'une installation +- [Configuration video interactive](https://lucas.unigest.fr/doc_camera.php) — Version web avec tableaux detailles diff --git a/mkdocs-smarteye/docs/deploiement/installation-camera.md b/mkdocs-smarteye/docs/deploiement/installation-camera.md new file mode 100644 index 0000000..d07905e --- /dev/null +++ b/mkdocs-smarteye/docs/deploiement/installation-camera.md @@ -0,0 +1,154 @@ +# Installation d'une camera IP + +
+

Guide technicien — Ajout d'une camera

+

Configuration d'une camera IP via l'application mobile. Temps estime : 5 minutes par camera.

+
+ +--- + +## Pre-requis + +- [ ] Smartphone avec l'application **CamHi** (ou equivalent) installee +- [ ] Camera IP allumee et en mode appairage (LED clignotante) +- [ ] Acces au reseau WiFi du client (SSID + mot de passe) +- [ ] Bluetooth active sur le smartphone + +--- + +## Etape 1 — Trouver la camera + +Ouvrir l'application et lancer la recherche. La camera est detectee automatiquement via **Bluetooth**. + +![Recherche de la camera via Bluetooth](../assets/images/deploiement/trouver-camera.png){ width="280" } + +1. Verifier que la camera est **allumee** et en mode appairage +2. La camera apparait avec son identifiant (ex: `SSAT-326256-ECCFB`) +3. Appuyer sur **Ajouter a** + +!!! tip "La camera n'apparait pas ?" + - Verifier que le Bluetooth est active sur le smartphone + - Rapprocher le telephone de la camera (< 3 metres) + - Redemarrer la camera (debrancher/rebrancher) + +--- + +## Etape 2 — Configurer le WiFi + +La camera doit se connecter au **reseau WiFi du client** pour etre accessible par le Jetson. + +![Configuration du reseau sans fil](../assets/images/deploiement/configuration-sans-fil.png){ width="280" } + +1. Le **nom du WiFi** (SSID) est pre-rempli avec le reseau actuel du smartphone +2. Saisir le **mot de passe WiFi** du client +3. Appuyer sur **Configurer le sans fil et ajouter** + +!!! warning "Important" + La camera et le Jetson doivent etre sur le **meme reseau**. Utiliser le WiFi principal du client, pas un reseau invite. + +--- + +## Etape 3 — Nommer la camera + +Donner un nom explicite a la camera pour l'identifier facilement. + +![Attribution du nom a la camera](../assets/images/deploiement/mise-en-place-nom.png){ width="280" } + +1. Saisir le **nom de la piece** ou la camera est installee +2. Utiliser les suggestions rapides si disponibles : `Salon`, `Chambre`, `Corridor`, `Hall`, etc. +3. Valider avec **OK** + +!!! info "Convention de nommage" + Utiliser des noms simples et coherents : **Salon cuisine**, **Chambre**, **Terrasse**, **Camera fenetre**. Ces noms apparaitront dans LucasApp. + +--- + +## Etape 4 — Changer le mot de passe + +A la premiere connexion, l'application demande de **changer le mot de passe par defaut** de la camera. + +![Prompt de changement de mot de passe](../assets/images/deploiement/changement-password.png){ width="280" } + +1. Appuyer sur **Ok** pour accepter +2. Definir un **mot de passe securise** (minimum 8 caracteres) +3. **Noter le mot de passe** — il sera necessaire pour configurer le flux RTSP sur le Jetson + +!!! danger "Ne pas ignorer cette etape" + Une camera avec le mot de passe par defaut est un risque de securite. Toujours changer le mot de passe. + +--- + +## Etape 5 — Verifier l'installation + +La camera apparait maintenant dans la liste des appareils. + +![Liste des cameras installees](../assets/images/deploiement/mes-cameras.png){ width="280" } + +Verifier que : + +- [ ] La camera est **en ligne** (apercu video visible) +- [ ] Le **nom** est correct +- [ ] L'**icone cloud** indique la connectivite + +--- + +## Etape 6 — Parametrer la camera + +Acceder aux reglages de la camera pour ajuster les parametres. + +![Parametres de la camera](../assets/images/deploiement/settings-chambre.png){ width="280" } + +Parametres recommandes : + +| Parametre | Reglage | +|-----------|---------| +| **Parametres video** | Resolution maximale, 15-20 FPS | +| **Parametres audio** | Activer le micro et le haut-parleur | +| **Gestion des alarmes** | Desactiver (c'est SmartEye qui gere la detection) | +| **Enregistrement carte TF** | Optionnel — activer si une carte SD est inseree | +| **Reglage de l'heure** | Verifier le fuseau horaire (Europe/Paris) | + +--- + +## Etape 7 — Recuperer l'adresse IP + +Aller dans **Information sur l'appareil** pour noter l'adresse IP de la camera. + +![Informations reseau de la camera](../assets/images/deploiement/informations-appareil.jpeg){ width="280" } + +| Information | Usage | +|-------------|-------| +| **Adresse IP** | Necessaire pour configurer le Jetson (ex: `192.168.1.196`) | +| **Type de reseau** | Verifier que c'est bien `WIFI` (ou Ethernet) | +| **Version du logiciel** | Utile pour le support technique | + +!!! success "Camera prete" + L'adresse IP est la derniere information necessaire. Elle sera utilisee pour configurer le flux RTSP dans SmartEye. + +--- + +## Recapitulatif + +| Etape | Action | Duree | +|:-----:|--------|:-----:| +| 1 | Trouver la camera (Bluetooth) | 30s | +| 2 | Configurer le WiFi | 30s | +| 3 | Nommer la camera | 15s | +| 4 | Changer le mot de passe | 30s | +| 5 | Verifier la connexion | 15s | +| 6 | Parametrer (video, audio) | 1 min | +| 7 | Recuperer l'adresse IP | 15s | + +--- + +## Prochaine etape + +Une fois toutes les cameras installees et leurs IPs notees, passer a l'[installation du Jetson SmartEye](jetson.md) pour configurer la detection IA. + +!!! info "Flux RTSP" + Le Jetson utilise les flux RTSP des cameras. L'URL standard est : + ``` + rtsp://:@:554/11 (flux HD) + rtsp://:@:554/12 (flux SD) + ``` + SmartEye utilise le flux SD (`/12`) pour la detection afin d'optimiser les performances. diff --git a/mkdocs-smarteye/docs/deploiement/installation-client.md b/mkdocs-smarteye/docs/deploiement/installation-client.md new file mode 100644 index 0000000..3286761 --- /dev/null +++ b/mkdocs-smarteye/docs/deploiement/installation-client.md @@ -0,0 +1,262 @@ +# Installation chez le client + +
+
+:material-shield-home: +

GUIDE TECHNICIEN TERRAIN

+
+

Deploiement du systeme de surveillance intelligent Lucas. Duree estimee sur site : ~30 minutes.

+
+:material-cctv: Surveillance +:material-brain: IA embarquee +:material-shield-check: Protection 24/7 +:material-phone-alert: Alerte instantanee +
+
+ +--- + +## Pre-requis + +!!! info "Le compte client est deja cree" + Le compte est cree automatiquement lors de la souscription. Le technicien recoit par email : + + - **Identifiant client** (`client_id`) + - **Mot de passe** (token d'authentification) + - **Numero de site** attribue + +**Chez le client :** + +- [ ] Acces Internet (fibre ou ADSL stable) +- [ ] Reseau WiFi pour les cameras (SSID + mot de passe) +- [ ] Prise Ethernet pour le Jetson (cable recommande) +- [ ] Prises electriques a proximite des emplacements cameras + +--- + +## Le kit materiel + +Voici ce que le technicien deballera chez le client : + +### Jetson Orin Nano — Le cerveau SmartEye + +![Jetson Orin Nano Developer Kit](../assets/images/jetson-orin-nano.jpg){ width="500" } + +*NVIDIA Jetson Orin Nano — Le boitier d'intelligence artificielle embarquee.* + +| Caracteristique | Valeur | +|----------------|--------| +| **Processeur** | 6-core Arm Cortex-A78AE | +| **GPU** | 1024-core NVIDIA Ampere | +| **Performance IA** | Jusqu'a 67 TOPS | +| **RAM** | 8 Go LPDDR5 | +| **Stockage** | Carte SD pre-flashee (image Gold) | +| **Alimentation** | USB-C (adaptateur fourni) | + +### Cameras Ctronics PTZ — Les yeux du systeme + + + +*Cameras Ctronics PTZ avec vision nocturne et audio bidirectionnel.* + +| Caracteristique | Valeur | +|----------------|--------| +| **Resolution** | 2K / 4MP minimum | +| **PTZ** | 355 pan, 90 tilt | +| **Vision nocturne** | 30m couleur | +| **Audio** | Bidirectionnel (micro + haut-parleur) | +| **Protocole** | ONVIF, RTSP | +| **Connexion** | WiFi 2.4/5GHz ou Ethernet | +| **Flux RTSP** | HD = `/11`, SD = `/12` | + +### Checklist depart + +- [ ] Boitier Jetson SmartEye (carte SD deja inseree, clonee depuis la Gold) +- [ ] Alimentation USB-C +- [ ] Cable Ethernet (RJ45) — 2 metres minimum +- [ ] Cameras IP (Ctronics) — autant que prevu pour ce client +- [ ] Alimentations cameras (12V) +- [ ] **Fiche credentials** (ID + Token + N site — recus par email) +- [ ] Smartphone avec **LucasApp** installe + +!!! tip "Carte SD" + La carte SD doit etre flashee depuis l'image Gold **avant** le deplacement. + Voir [Clonage des cartes SD](clonage.md) pour la procedure. + +--- + +## Sur site (chez le client) + +### Etape 1 — Installer les cameras + +**Avant de toucher au Jetson**, positionner et brancher toutes les cameras. + +**Regles de positionnement** : + +| Regle | Detail | +|-------|--------| +| Hauteur | 2 a 2,5 metres du sol (au-dessus des portes) | +| Angle | Plongee a ~30, couvrir le maximum de sol | +| Zones cibles | Salon, chambre, couloir — la ou le senior passe le plus de temps | +| Eviter | Contre-jour (fenetre dans le champ), reflets de miroirs | +| Branchement | Chaque camera a son alimentation + connectee au **reseau du client** (WiFi ou Ethernet) | + +!!! info "Combien de cameras ?" + Le systeme supporte jusqu'a **6 cameras par site**. Minimum recommande : 2 (salon + chambre). + +**Verifier** que chaque camera est allumee et connectee au reseau du client avant de continuer. + +--- + +### Etape 2 — Brancher et configurer le Jetson + +#### Branchement physique + +``` + Box Internet du client + [ ::: ] + | + Cable Ethernet + | + [ Jetson SmartEye ] ← USB-C (alimentation) +``` + +1. Brancher le **cable Ethernet** entre le Jetson et la box internet +2. Brancher le **cable USB-C** (alimentation) +3. Attendre **1 minute** — la LED passe au vert fixe + +#### Configuration depuis le smartphone + +Se connecter au **meme WiFi** que le boitier, puis ouvrir dans le navigateur : + +
+http://smarteye.local:8080 +
+ +
+ +!!! tip "Ca ne marche pas ?" + **Solution 1** — Ouvrir LucasApp > Parametres > **Installer un SmartEye** + + **Solution 2** — Scanner le **QR code** colle sous le boitier + +#### Ecran 1/4 : Identification + +Saisir les informations recues par email : + +| Champ | Quoi mettre | +|-------|-------------| +| **ID Client** | L'identifiant recu par email | +| **Token** | Le mot de passe recu par email | +| **Nom du site** | Nom libre (ex: `Mme Dupont - Aleria`) | + +#### Ecran 2/4 : Cameras + +Le Jetson detecte les cameras automatiquement sur le reseau. + +Pour chaque camera trouvee : + +1. **Nommer la piece** (Salon, Chambre, Cuisine...) +2. Saisir les **identifiants** si demande (login/mot de passe de la camera) +3. Appuyer sur **Tester** — verifier que l'image s'affiche + +!!! warning "Aucune camera detectee ?" + - Les cameras sont-elles allumees ? + - Sont-elles sur le **meme reseau** que le Jetson ? + - Essayer de redemarrer la camera + +#### Ecran 3/4 : Connexion serveur + +Les valeurs sont **pre-remplies**. Ne rien modifier sauf indication contraire. + +| Parametre | Valeur par defaut | +|-----------|-------------------| +| Serveur | `57.128.74.87` | +| Utilisateur | `debian` | +| N de site | Automatique | + +Appuyer sur **Tester la connexion** — doit afficher un resultat vert. + +#### Ecran 4/4 : Test final + +Le systeme verifie toute la chaine automatiquement : + +| Etape | Verification | Resultat | +|:-----:|-------------|:--------:| +| 1 | Connexion aux cameras | :material-check: | +| 2 | Detection IA (YOLO) | :material-check: | +| 3 | Tunnel vers le serveur | :material-check: | +| 4 | Envoi d'une image test | :material-check: | +| 5 | Notification recue sur le telephone | :material-check: | + +Si tout est vert : appuyer sur **Terminer**. + +Le Jetson passe en mode surveillance active. + +--- + +## Avant de partir + +### Etape 3 — Test de bout en bout + +**Test obligatoire** — ne pas quitter le domicile sans l'avoir fait. + +1. **Simuler une chute** : se coucher au sol devant une camera, rester immobile 30 secondes +2. **Verifier** la notification sur le telephone du technicien (LucasApp) +3. **Verifier** que l'image recue est nette et bien cadree +4. **Verifier** le flux video en direct dans LucasApp (toutes les cameras) +5. **Tester l'interphone** : parler dans l'app, verifier que le son sort de la camera + +--- + +### Etape 4 — Configurer les telephones des aidants + +Sur le telephone de **chaque aidant** (fils, fille, aide-soignant...) : + +1. **Installer LucasApp** (Play Store / APK) +2. **Ouvrir l'app** — elle s'enregistre automatiquement aupres du serveur +3. **Verifier les notifications** : elles doivent etre autorisees dans les reglages du telephone +4. **Montrer le fonctionnement** : + - Comment repondre a une alerte + - Comment voir les cameras en direct + - Comment utiliser l'interphone + - Expliquer que le numero d'urgence (15 par defaut) est accessible depuis l'alerte + +!!! success "Installation terminee" + Le systeme est operationnel. Le Jetson fonctionne 24h/24, redemarre seul en cas de coupure de courant. + Aucune intervention de l'aidant n'est necessaire au quotidien. + +--- + +## Checklist de depart + +Avant de quitter le domicile, cocher chaque point : + +- [ ] Toutes les cameras sont fixees et alimentees +- [ ] Le Jetson est branche (Ethernet + USB-C) +- [ ] La LED du Jetson clignote rapidement (surveillance active) +- [ ] Le test de chute simulee a fonctionne +- [ ] Le flux video est visible dans LucasApp +- [ ] L'interphone fonctionne dans les deux sens +- [ ] **Chaque aidant** a LucasApp installe et les notifications activees +- [ ] Le boitier est place dans un endroit discret et ventile + +--- + +## Depannage rapide + +| Symptome | Cause probable | Solution | +|----------|---------------|----------| +| LED eteinte | Pas d'alimentation | Verifier le cable USB-C et l'adaptateur | +| `smarteye.local` ne repond pas | mDNS non supporte | Utiliser le scan LucasApp ou le QR code | +| Camera non detectee | Reseau different | Verifier que la camera est sur le meme reseau | +| Test serveur echoue | Pas d'internet | Verifier la connexion internet de la box client | +| Pas de notification | Notifications desactivees | Verifier les reglages du telephone de l'aidant | +| Image floue ou sombre | Mauvais positionnement | Repositionner la camera (hauteur, angle, eclairage) | +| Intercom muet | Camera sans audio | Verifier que la camera supporte l'audio bidirectionnel | + +!!! info "Probleme non resolu ?" + Consulter le [Guide expert](jetson-expert.md) pour un diagnostic technique approfondi. diff --git a/mkdocs-smarteye/docs/deploiement/jetson-expert.md b/mkdocs-smarteye/docs/deploiement/jetson-expert.md new file mode 100644 index 0000000..e3945ff --- /dev/null +++ b/mkdocs-smarteye/docs/deploiement/jetson-expert.md @@ -0,0 +1,380 @@ +# Installation du Jetson SmartEye + +## Philosophie : le Jetson est une Box + +Le Jetson SmartEye se deploie comme une box internet (Free, Orange). L'installateur arrive chez le beneficiaire avec le boitier pre-configure, le branche sur le reseau local, et **tout se passe depuis son smartphone**. Aucun ecran, aucun clavier, aucun cable HDMI. + +```mermaid +graph LR + subgraph Domicile["Domicile du beneficiaire"] + BOX["Jetson SmartEye"] + CAM1["Camera 1"] + CAM2["Camera 2"] + CAM3["Camera 3"] + ROUTER["Box Internet"] + BOX ---|Ethernet| ROUTER + CAM1 ---|WiFi/Ethernet| ROUTER + CAM2 ---|WiFi/Ethernet| ROUTER + CAM3 ---|WiFi/Ethernet| ROUTER + end + + subgraph Install["Smartphone installateur"] + APP["LucasApp / Navigateur"] + end + + ROUTER ---|WiFi| APP + APP -.->|"http://smarteye.local:8080"| BOX +``` + +## Materiel necessaire + +| Element | Detail | +|---------|--------| +| **Jetson Orin Nano** | Avec SmartEye pre-installe sur carte SD | +| **Alimentation** | USB-C 5V/3A (fournie) | +| **Cable Ethernet** | RJ45, branchement sur la box internet du client | +| **Cameras IP** | CTronic ou compatible ONVIF, deja installees | +| **Smartphone** | Android ou iOS, connecte au meme reseau WiFi | + +--- + +## Etape 1 : Branchement physique + +1. **Brancher le cable Ethernet** du Jetson a la box internet du client +2. **Brancher l'alimentation USB-C** du Jetson +3. **Attendre 60 secondes** — le Jetson demarre et se connecte au reseau local + +!!! info "LED de statut" + Le Jetson n'a pas d'ecran mais dispose d'une LED verte : + + - **Clignotement lent** : demarrage en cours + - **Fixe** : connecte au reseau, pret pour la configuration + - **Clignotement rapide** : surveillance active, tout fonctionne + +--- + +## Etape 2 : Decouverte sur le reseau local + +Le Jetson annonce automatiquement sa presence sur le reseau local via **mDNS (Avahi/Bonjour)**. Depuis le smartphone connecte au meme WiFi : + +### Option A : Acces direct (recommande) + +Ouvrir un navigateur et aller a : + +``` +http://smarteye.local:8080 +``` + +!!! tip "Si smarteye.local ne repond pas" + Certaines box internet bloquent le mDNS. Dans ce cas, utiliser l'option B. + +### Option B : Scan reseau depuis LucasApp + +1. Ouvrir **LucasApp** sur le smartphone +2. Aller dans **Parametres > Installer un SmartEye** +3. L'application scanne le reseau local (plage 192.168.x.x) +4. Le Jetson apparait dans la liste des appareils detectes +5. Appuyer dessus pour ouvrir l'interface de configuration + +```mermaid +sequenceDiagram + participant Phone as Smartphone + participant Jetson as Jetson SmartEye + participant Router as Box Internet + + Phone->>Router: Scan reseau local (UDP broadcast) + Router-->>Phone: Liste des appareils + Phone->>Jetson: GET /api/identity (port 8080) + Jetson-->>Phone: {"type": "smarteye", "version": "v30", "status": "setup"} + Phone->>Phone: Affiche "SmartEye detecte" +``` + +### Option C : QR Code sur le boitier + +Un QR code colle sur le Jetson contient son adresse MAC. LucasApp peut scanner ce QR code pour identifier le bon appareil sur le reseau. + +--- + +## Etape 3 : Interface de configuration (WebUI) + +L'interface web du Jetson s'affiche sur le smartphone. L'installation se fait en **4 sous-etapes** guidees. + +### 3.1 — Identification du site + +| Champ | Description | Exemple | +|-------|-------------|---------| +| **ID Client** | Identifiant du beneficiaire (fourni par Lucas) | `dupont_marie` | +| **Token** | Jeton d'authentification (fourni par Lucas) | `b5ce5015` | +| **Nom du site** | Nom lisible | `Residence Dupont` | + +!!! warning "Ou trouver l'ID et le Token ?" + L'ID client et le token sont generes lors de la creation du dossier dans l'interface d'administration Lucas ([admin.php](https://lucas.unigest.fr/admin.php)). Voir le [Guide nouveau client](../guide/nouveau-client.md). + +### 3.2 — Detection automatique des cameras (ONVIF) + +Le Jetson scanne le reseau local a la recherche de cameras compatibles ONVIF. + +``` +Scan en cours... + +Camera detectee : 192.168.1.143 (CTronic - Salon) +Camera detectee : 192.168.1.148 (CTronic - Chambre) +Camera detectee : 192.168.1.152 (CTronic - Cuisine) + +3 cameras trouvees. +``` + +Pour chaque camera detectee : + +| Champ | Auto-detecte | Modifiable | +|-------|:------------:|:----------:| +| **Adresse IP** | Oui | Oui | +| **Nom** (piece) | Non | Oui | +| **Login RTSP** | Non | Oui | +| **Mot de passe RTSP** | Non | Oui | +| **Flux HD** (/11) | Oui | Oui | +| **Flux SD** (/12) | Oui | Oui | +| **Active** | Oui | Oui | + +!!! tip "Verifier le flux en direct" + Appuyer sur **Tester** a cote de chaque camera pour afficher un apercu en direct sur le smartphone. Cela confirme que les identifiants RTSP sont corrects. + +### 3.3 — Connexion au serveur Lucas + +Le Jetson doit etablir une connexion securisee (tunnel SSH) vers le serveur OVH. + +| Parametre | Valeur par defaut | Modifiable | +|-----------|-------------------|:----------:| +| **Serveur** | `57.128.74.87` | Oui | +| **Utilisateur SSH** | `debian` | Oui | +| **Domaine** | `lucas.unigest.fr` | Oui | +| **Numero de site** | Auto (prochain disponible) | Oui | + +```mermaid +sequenceDiagram + participant Jetson + participant OVH as Serveur OVH + + Jetson->>OVH: Test connexion SSH + OVH-->>Jetson: OK - Cle autorisee + Jetson->>OVH: Ouverture tunnels RTSP (ports 8560-8565) + Jetson->>OVH: Ouverture tunnel Audio (port 8566) + Jetson->>OVH: POST api.php (test image) + OVH-->>Jetson: 200 OK - Client reconnu + Jetson-->>Jetson: Connexion validee +``` + +!!! note "Attribution automatique des ports" + Le Jetson calcule automatiquement sa plage de ports selon la convention : + + ``` + Site N → base_port = 8550 + (N-1) x 10 + + Ports attribues : + base+0 a base+5 : RTSP cameras (max 6) + base+6 : Audio WebSocket + base+7 : WebUI locale + base+8 a base+9 : Reserves + ``` + + Exemple pour le site n°2 : ports 8560 a 8569. + +### 3.4 — Test de bout en bout + +Le Jetson effectue un test complet de la chaine : + +``` +[1/5] Connexion cameras RTSP............ OK +[2/5] Detection IA (YOLO)............... OK (personne detectee) +[3/5] Tunnel SSH vers OVH............... OK +[4/5] Envoi image test a api.php........ OK (Gemini: "RAS") +[5/5] Notification push Firebase........ OK (recue sur LucasApp) + +=== INSTALLATION TERMINEE === +SmartEye est operationnel. +La surveillance demarre automatiquement. +``` + +!!! success "Installation terminee" + Une fois le test valide, le Jetson passe en mode **surveillance active**. Il demarre automatiquement a chaque mise sous tension, sans aucune intervention. + +--- + +## Etape 4 : Verification depuis LucasApp + +Sur le smartphone de chaque aidant : + +1. Ouvrir **LucasApp** +2. Verifier que le flux video en direct est accessible +3. Verifier que l'interphone fonctionne (audio bidirectionnel) +4. Effectuer un **test de chute simule** : + - Se coucher au sol dans le champ d'une camera + - Attendre ~30 secondes + - Verifier la reception de la notification push + - Verifier l'image et le message Gemini dans l'alerte + +--- + +## Architecture reseau complete + +```mermaid +graph TB + subgraph Domicile["Domicile du beneficiaire"] + CAM1["Camera 1
192.168.1.143"] + CAM2["Camera 2
192.168.1.148"] + CAM3["Camera 3
192.168.1.152"] + JETSON["Jetson SmartEye
192.168.1.xxx"] + ROUTER["Box Internet
192.168.1.1"] + + CAM1 -->|RTSP local| JETSON + CAM2 -->|RTSP local| JETSON + CAM3 -->|RTSP local| JETSON + JETSON ---|Ethernet| ROUTER + end + + subgraph OVH["Serveur OVH (57.128.74.87)"] + SSH["SSH Gateway"] + RTSP_RELAY["Relay RTSP
ports 8560-8565"] + AUDIO["Audio WebSocket
port 8566"] + API["api.php"] + GEMINI["Gemini 2.5 Flash"] + FCM["Firebase Cloud Messaging"] + end + + JETSON ==>|"SSH Tunnel (autossh)
Persistant, auto-reconnect"| SSH + SSH --> RTSP_RELAY + SSH --> AUDIO + JETSON ==>|"POST image
si chute detectee"| API + API --> GEMINI + GEMINI --> FCM + + subgraph Aidants["Smartphones aidants"] + APP1["LucasApp - Fille"] + APP2["LucasApp - Fils"] + end + + FCM ==>|Push notification| APP1 + FCM ==>|Push notification| APP2 + RTSP_RELAY -.->|"Flux video live"| APP1 + AUDIO -.->|"Interphone"| APP1 +``` + +--- + +## Fichier de configuration genere + +A la fin de l'installation, le Jetson cree automatiquement le fichier `smarteye_config.yaml` : + +```yaml +# Genere automatiquement par l'assistant d'installation +# Ne pas modifier manuellement sauf si necessaire + +site: + id: "dupont_marie" + name: "Residence Dupont" + client_token: "b5ce5015" + +server: + api_url: "https://lucas.unigest.fr/api.php" + ovh_ip: "57.128.74.87" + ssh_user: "debian" + +ports: + site_number: 2 + base_port: 8560 + +cameras: + - id: "Cam1" + name: "Salon" + ip: "192.168.1.143" + username: "admin" + password: "********" + stream_hd: "/11" + stream_sd: "/12" + tunnel_port: 8560 + enabled: true + + - id: "Cam2" + name: "Chambre" + ip: "192.168.1.148" + username: "admin" + password: "********" + stream_hd: "/11" + stream_sd: "/12" + tunnel_port: 8561 + enabled: true + + - id: "Cam3" + name: "Cuisine" + ip: "192.168.1.152" + username: "admin" + password: "********" + stream_hd: "/11" + stream_sd: "/12" + tunnel_port: 8562 + enabled: true + +audio: + local_port: 8800 + tunnel_port: 8566 + +detection: + yolo: + std_confidence: 0.15 + pose_confidence: 0.25 + alert_interval: 120 +``` + +--- + +## Maintenance et acces distant + +Une fois installe, le Jetson est accessible a distance depuis le serveur OVH via le tunnel SSH inverse : + +```bash +# Depuis le serveur OVH, acceder au WebUI du Jetson +# (tunnel inverse sur le port base+7) +curl http://localhost:8567/api/status +``` + +### Commandes utiles (pour le support technique) + +| Action | Commande | +|--------|----------| +| **Statut du Jetson** | `curl http://localhost:/api/status` | +| **Redemarrer SmartEye** | `ssh -p jetson "sudo systemctl restart smarteye"` | +| **Voir les logs** | `ssh -p jetson "journalctl -u smarteye -f"` | +| **Mettre a jour** | `ssh -p jetson "cd /w && git pull && sudo systemctl restart smarteye"` | + +!!! warning "Resilience reseau" + Le service `autossh` maintient les tunnels SSH en permanence. En cas de coupure internet : + + - Les tunnels se retablissent automatiquement a la reconnexion + - SmartEye continue la detection localement meme sans internet + - Les alertes en attente sont envoyees des que la connexion revient + +--- + +## Resume du flux d'installation + +```mermaid +graph TD + A["Branchement Ethernet + USB-C"] --> B["Demarrage automatique (60s)"] + B --> C["Annonce mDNS sur le reseau local"] + C --> D{"Smartphone detecte le Jetson"} + D -->|"smarteye.local:8080"| E["WebUI de configuration"] + D -->|"LucasApp scan"| E + D -->|"QR Code"| E + E --> F["Saisie ID client + Token"] + F --> G["Scan ONVIF des cameras"] + G --> H["Configuration des cameras"] + H --> I["Connexion SSH vers OVH"] + I --> J["Test de bout en bout"] + J -->|"Tout OK"| K["Surveillance active"] + J -->|"Echec"| L["Diagnostic et correction"] + L --> J + + style A fill:#455a64,color:#fff + style K fill:#00695c,color:#fff + style L fill:#bf360c,color:#fff +``` diff --git a/mkdocs-smarteye/docs/deploiement/jetson.md b/mkdocs-smarteye/docs/deploiement/jetson.md new file mode 100644 index 0000000..2649b24 --- /dev/null +++ b/mkdocs-smarteye/docs/deploiement/jetson.md @@ -0,0 +1,165 @@ +# Installation SmartEye - Guide simplifie + +
+

Guide d'installation rapide

+

Tout se fait depuis votre smartphone. Temps estime : 10 minutes.

+
+ +--- + +## Ce dont vous avez besoin + +| | Element | Fourni | +|:-:|---------|:------:| +| 1 | **Boitier Jetson SmartEye** (avec carte SD) | Oui | +| 2 | **Alimentation USB-C** | Oui | +| 3 | **Cable Ethernet (RJ45)** | Oui | +| 4 | **Cameras** deja installees chez le client | - | +| 5 | **Votre smartphone** connecte au WiFi du client | - | +| 6 | **Fiche client** (ID + Token, fournis par l'admin) | Oui | + +--- + +## Etape 1 — Brancher le boitier + +``` + Box Internet du client + [ ::: ] + | + Cable Ethernet + | + [ Jetson SmartEye ] ← USB-C (alimentation) +``` + +1. Brancher le **cable Ethernet** entre le Jetson et la box internet +2. Brancher le **cable USB-C** (alimentation) +3. Attendre **1 minute** que la LED passe au vert fixe + +| LED | Signification | +|:---:|---------------| +| Clignotement lent | Demarrage en cours — patientez | +| **Vert fixe** | Pret — passez a l'etape 2 | +| Clignotement rapide | Surveillance active (tout roule) | + +--- + +## Etape 2 — Se connecter au Jetson + +Assurez-vous que votre smartphone est connecte **au meme WiFi** que le boitier. + +Ouvrez votre navigateur et tapez : + +
+http://smarteye.local:8080 +
+ +
+ +!!! tip "Ca ne marche pas ?" + **Solution 1** — Ouvrir LucasApp > Parametres > **Installer un SmartEye** (scan automatique) + + **Solution 2** — Scanner le **QR code** colle sous le boitier avec LucasApp + +--- + +## Etape 3 — Configurer (4 ecrans) + +### Ecran 1/4 : Identification + +Saisir les informations de la **fiche client** : + +| Champ | Quoi mettre | Exemple | +|-------|-------------|---------| +| **ID Client** | Sur la fiche | `dupont_marie` | +| **Token** | Sur la fiche | `b5ce5015` | +| **Nom du site** | Nom libre | `Mme Dupont - Aleria` | + +Appuyer sur **Suivant →** + +--- + +### Ecran 2/4 : Cameras + +Le Jetson scanne et trouve les cameras automatiquement. + +``` + ✓ Camera 1 — 192.168.1.143 + ✓ Camera 2 — 192.168.1.148 + ✓ Camera 3 — 192.168.1.152 +``` + +Pour chaque camera : + +1. **Nommer la piece** (Salon, Chambre, Cuisine...) +2. Saisir les **identifiants RTSP** si demande (login/mot de passe camera) +3. Appuyer sur **Tester** pour verifier l'image en direct + +!!! warning "Aucune camera detectee ?" + Verifier que les cameras sont allumees et connectees au meme reseau (WiFi ou Ethernet). + +Appuyer sur **Suivant →** + +--- + +### Ecran 3/4 : Connexion serveur + +Les valeurs par defaut sont pre-remplies. **Ne rien modifier** sauf indication contraire. + +| Parametre | Valeur par defaut | +|-----------|-------------------| +| Serveur | `57.128.74.87` | +| Utilisateur | `debian` | +| Domaine | `lucas.unigest.fr` | +| N° de site | Automatique | + +Appuyer sur **Tester la connexion**, puis **Suivant →** + +--- + +### Ecran 4/4 : Test final + +Le Jetson verifie toute la chaine automatiquement : + +| Etape | Quoi | Resultat attendu | +|:-----:|------|:-----------------:| +| 1 | Connexion cameras | ✓ | +| 2 | Detection IA | ✓ | +| 3 | Tunnel vers serveur | ✓ | +| 4 | Envoi image test | ✓ | +| 5 | Notification sur telephone | ✓ | + +Si tout est vert : appuyer sur **Terminer**. + +!!! success "C'est fini" + Le Jetson passe en surveillance. Il redemarre seul en cas de coupure de courant. Rien d'autre a faire. + +--- + +## Etape 4 — Verifier sur LucasApp + +Sur le telephone de **chaque aidant** : + +- [ ] Ouvrir LucasApp → verifier le flux video en direct +- [ ] Tester l'interphone (parler / ecouter) +- [ ] **Simuler une chute** : se coucher au sol devant une camera, attendre 30 secondes, verifier que la notification arrive + +--- + +## En cas de probleme + +| Symptome | Solution | +|----------|----------| +| LED ne s'allume pas | Verifier l'alimentation USB-C | +| `smarteye.local` ne repond pas | Utiliser le scan LucasApp ou le QR code | +| Camera non detectee | Verifier qu'elle est sur le meme reseau | +| Test serveur echoue | Verifier la connexion internet du client | +| Pas de notification | Verifier que LucasApp est installe et les notifications activees | + +!!! info "Besoin d'aide ?" + Contacter le support technique ou consulter le [Guide expert](jetson-expert.md) pour un diagnostic approfondi. + +--- + +
+Recapitulatif : Brancher → Se connecter → 4 ecrans de config → Verifier sur LucasApp. C'est tout. +
diff --git a/mkdocs-smarteye/docs/guide/alertes.md b/mkdocs-smarteye/docs/guide/alertes.md new file mode 100644 index 0000000..6fbafc6 --- /dev/null +++ b/mkdocs-smarteye/docs/guide/alertes.md @@ -0,0 +1,3 @@ +# Gestion des alertes + +Section en cours de rédaction. Cette page décrira le cycle de vie des alertes, de leur déclenchement à leur résolution. diff --git a/mkdocs-smarteye/docs/guide/dashboard.md b/mkdocs-smarteye/docs/guide/dashboard.md new file mode 100644 index 0000000..77b781d --- /dev/null +++ b/mkdocs-smarteye/docs/guide/dashboard.md @@ -0,0 +1,3 @@ +# Dashboard de surveillance + +Section en cours de rédaction. Cette page décrira le tableau de bord de surveillance en temps réel et ses différentes fonctionnalités. diff --git a/mkdocs-smarteye/docs/guide/nouveau-client.md b/mkdocs-smarteye/docs/guide/nouveau-client.md new file mode 100644 index 0000000..1e7977d --- /dev/null +++ b/mkdocs-smarteye/docs/guide/nouveau-client.md @@ -0,0 +1,66 @@ +# Ajouter un nouveau client + +## 1. Accéder à l'administration + +Rendez-vous sur **[lucas.unigest.fr/admin.php](https://lucas.unigest.fr/admin.php)** et entrez le code d'accès. + +![Login SmartEye](../assets/admin-login.png){ width="400" } + +## 2. Créer le dossier + +Cliquez sur **+ Nouveau Dossier** en haut à droite. + +### Fiche bénéficiaire + +Remplissez les informations de la personne surveillée : + +| Champ | Description | Obligatoire | Exemple | +|-------|-------------|:-----------:|---------| +| **ID Système** | Identifiant unique (minuscules, sans espaces) | :material-check: | `dupont_marie` | +| **Nom & Prénom** | Nom complet du bénéficiaire | :material-check: | `Marie Dupont` | +| **Age** | Age du bénéficiaire | | `87` | +| **Sexe** | F ou H | | `F` | +| **Adresse** | Adresse d'installation des caméras | | `12 rue des Roses` | +| **Ville** | Ville / Code postal | | `20270 Aléria` | +| **Mobile** | Numéro principal (format international) | :material-check: | `+33612345678` | +| **Fixe** | Numéro fixe du domicile | | `+33495123456` | + +!!! tip "Convention de nommage" + L'ID système sera utilisé partout : dossier images, configuration SmartEye, base de données. + Choisissez un ID court et descriptif : `nom_prenom` ou `nom_ville`. + +## 3. Ajouter les contacts d'urgence + +Cliquez sur **+ Ajouter Contact** pour chaque personne à prévenir en cas de chute. + +| Champ | Description | Exemple | +|-------|-------------|---------| +| **Nom** | Prénom du contact | Jean | +| **Rôle** | Lien avec le bénéficiaire | Fils | +| **Téléphone** | Numéro mobile (format international) | +33698765432 | +| **Email** | Email (optionnel) | jean@email.fr | + +!!! warning "Ordre des contacts" + Les contacts sont alertés **dans l'ordre de la liste**. Placez le contact principal en premier. + +### Exemple de réseau de contacts typique + +``` +1. Fille (proche géographiquement) → Premier appel +2. Fils (disponible en journée) → Deuxième appel +3. Voisin (à côté, peut intervenir) → Troisième appel +4. Médecin traitant → Notification email +``` + +## 4. Enregistrer + +Cliquez sur **:material-content-save: Enregistrer la Fiche**. Le client apparaît dans le tableau principal avec le statut **Surveillance Active**. + +## 5. Étapes suivantes + +Après la création du client dans Lucas, il faut : + +- [ ] **Installer les caméras** chez le bénéficiaire → [Guide déploiement](../deploiement/jetson.md) +- [ ] **Configurer SmartEye** avec l'ID client → [Installation client](../deploiement/installation-client.md) +- [ ] **Installer LucasApp** sur le téléphone des contacts → [Guide LucasApp](dashboard.md) +- [ ] **Tester une chute simulée** pour valider la chaîne complète diff --git a/mkdocs-smarteye/docs/index.md b/mkdocs-smarteye/docs/index.md new file mode 100644 index 0000000..48f9fc6 --- /dev/null +++ b/mkdocs-smarteye/docs/index.md @@ -0,0 +1,60 @@ +# Lucas — Système Intelligent de Protection à Domicile + +**Surveillance IA · Détection de chutes · Alerte instantanée · Protection 24/7** + +--- + +## Comment ça marche ? + +```mermaid +graph LR + A[Caméras IP] -->|RTSP| B[Jetson Orin Nano] + B -->|Détection YOLO| C{Chute ?} + C -->|Image| D[Serveur Lucas] + D -->|Gemini AI| E{Confirmée ?} + E -->|Oui| F[Alerte Firebase] + F -->|Push| G[LucasApp Mobile] + E -->|Non| H[Fausse alerte filtrée] +``` + +## Les 3 composants + +| Composant | Rôle | Technologie | +|-----------|------|-------------| +| :material-cctv: **SmartEye** | Détection de chutes en temps réel | YOLO + Jetson Orin Nano | +| :material-server: **Lucas** | Backend IA, alertes, gestion clients | PHP + Python + Gemini | +| :material-cellphone: **LucasApp** | Réception alertes, flux live, interphone | Flutter (Android) | + +## Démarrage rapide + +
+ +- :material-account-plus:{ .lg .middle } **Ajouter un client** + + --- + + Créer une fiche bénéficiaire et ses contacts d'urgence + + [:octicons-arrow-right-24: Guide nouveau client](guide/nouveau-client.md) + +- :material-server-network:{ .lg .middle } **Déployer un site** + + --- + + Installer les caméras, le Jetson et configurer le réseau + + [:octicons-arrow-right-24: Guide déploiement](deploiement/jetson.md) + +- :material-api:{ .lg .middle } **API & Intégration** + + --- + + Endpoints, payloads FCM et contrats entre composants + + [:octicons-arrow-right-24: Documentation API](api/endpoints.md) + +
+ +!!! warning "Philosophie de sécurité" + **Mieux vaut 10 fausses alertes filtrées par Gemini qu'une seule chute ignorée.** + YOLO ratisse large, Gemini confirme. En cas de doute, l'alerte part toujours. diff --git a/mkdocs-smarteye/docs/outils/checklist.md b/mkdocs-smarteye/docs/outils/checklist.md new file mode 100644 index 0000000..bc28e08 --- /dev/null +++ b/mkdocs-smarteye/docs/outils/checklist.md @@ -0,0 +1,57 @@ +# Checklist Installation + +
+

Suivi d'installation client

+

Outil interactif pour suivre chaque etape du deploiement chez un client — du bureau jusqu'a la mise en production.

+
+ +--- + +## Acceder a la checklist + +:material-open-in-new: **[Ouvrir la checklist interactive](https://lucas.unigest.fr/checklist.php)** + +--- + +## Les 6 phases d'installation + +La checklist couvre le parcours complet du technicien : + +| Phase | Description | Responsable principal | +|:-----:|-------------|:---------------------:| +| 1 | **Au bureau** — Creer le dossier client, preparer le Jetson et les cameras | :material-server: Lucas + :material-eye: SmartEye | +| 2 | **Sur place : Materiel** — Brancher, connecter, positionner | :material-eye: SmartEye | +| 3 | **Configuration AVA** — Enregistrer le compte, scanner les cameras | :material-cellphone: AVA | +| 4 | **Tunnels SSH** — Cle SSH, autossh, verification ports | :material-eye: SmartEye + :material-server: Lucas | +| 5 | **Tests complets** — Camera live, simulation chute, notifications | :material-check-all: Tous | +| 6 | **Mise en production** — Activation, formation famille, PV recette | :material-check-all: Tous | + +--- + +## Fonctionnalites + +- **2 cases par tache** : Fait + Teste +- **Selecteur client** : gerer plusieurs installations en parallele +- **Barre de progression** : vue d'ensemble de l'avancement +- **Notes libres** : documenter les particularites de chaque installation +- **Sauvegarde locale** : les donnees persistent dans le navigateur (localStorage) +- **Lien vers les guides** : chaque etape technique renvoie vers la documentation detaillee + +--- + +## 3 systemes, 3 couleurs + +| Couleur | Systeme | Role | +|---------|---------|------| +| :material-circle:{ style="color: #f59e0b" } Orange | **SmartEye** | Jetson + cameras + detection YOLO | +| :material-circle:{ style="color: #10b981" } Vert | **Lucas** | Serveur OVH + admin + API + Gemini | +| :material-circle:{ style="color: #6366f1" } Violet | **AVA** | Application mobile famille | + +--- + +## Voir aussi + +- [Configuration video camera](../deploiement/config-video-camera.md) +- [Installation camera IP](../deploiement/installation-camera.md) +- [Installation Jetson](../deploiement/jetson.md) +- [Guide nouveau client](../guide/nouveau-client.md) diff --git a/mkdocs-smarteye/mkdocs.yml b/mkdocs-smarteye/mkdocs.yml new file mode 100644 index 0000000..e243adb --- /dev/null +++ b/mkdocs-smarteye/mkdocs.yml @@ -0,0 +1,72 @@ +site_name: Lucas Documentation +site_description: Système intelligent de protection à domicile — Détection de chutes par IA +site_url: https://lucas.unigest.fr/docs + +theme: + name: material + language: fr + palette: + - scheme: slate + primary: deep purple + accent: cyan + toggle: + icon: material/brightness-4 + name: Mode clair + - scheme: default + primary: deep purple + accent: cyan + toggle: + icon: material/brightness-7 + name: Mode sombre + font: + text: Inter + code: JetBrains Mono + icon: + logo: material/shield-home + features: + - navigation.tabs + - navigation.sections + - navigation.expand + - navigation.top + - search.highlight + - content.code.copy + - content.tabs.link + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - tables + - attr_list + +nav: + - Accueil: index.md + - Architecture: + - Vue d'ensemble: architecture/overview.md + - Flux de données: architecture/data-flow.md + - Guide Utilisateur: + - Nouveau client: guide/nouveau-client.md + - Dashboard: guide/dashboard.md + - Gestion des alertes: guide/alertes.md + - Déploiement: + - Installation caméra IP: deploiement/installation-camera.md + - Configuration vidéo caméra: deploiement/config-video-camera.md + - Installation simplifiée: deploiement/jetson.md + - Installation expert: deploiement/jetson-expert.md + - Préparer la carte Gold: deploiement/carte-gold.md + - Clonage des cartes SD: deploiement/clonage.md + - Installation client: deploiement/installation-client.md + - Outils: + - Checklist installation: outils/checklist.md + - API: + - Endpoints: api/endpoints.md + - Payload FCM: api/fcm-payload.md diff --git a/mkdocs-smarteye/upload.php b/mkdocs-smarteye/upload.php new file mode 100644 index 0000000..1ac1c4b --- /dev/null +++ b/mkdocs-smarteye/upload.php @@ -0,0 +1,415 @@ + false, 'error' => 'Erreur upload: ' . $file['error']]); + exit; + } + + if ($file['size'] > $MAX_SIZE) { + echo json_encode(['success' => false, 'error' => 'Fichier trop volumineux (max 10 MB)']); + exit; + } + + // Déterminer l'extension + $originalName = $file['name']; + $ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION)); + + // Si c'est un collage (blob), détecter le type MIME + if (empty($ext) || $ext === 'blob') { + $mime = mime_content_type($file['tmp_name']); + $mimeMap = [ + 'image/png' => 'png', + 'image/jpeg' => 'jpg', + 'image/gif' => 'gif', + 'image/webp' => 'webp', + 'image/svg+xml' => 'svg', + ]; + $ext = $mimeMap[$mime] ?? ''; + } + + if (!in_array($ext, $ALLOWED_EXTENSIONS)) { + echo json_encode(['success' => false, 'error' => "Extension non autorisée: $ext"]); + exit; + } + + // Nom du fichier : soit le nom custom, soit le nom original, soit un timestamp + $customName = isset($_POST['filename']) ? trim($_POST['filename']) : ''; + if ($customName) { + // Nettoyer le nom + $customName = preg_replace('/[^a-zA-Z0-9_\-]/', '-', $customName); + $filename = $customName . '.' . $ext; + } elseif ($originalName && $originalName !== 'blob' && $originalName !== 'image.png') { + $filename = preg_replace('/[^a-zA-Z0-9_\-\.]/', '-', $originalName); + } else { + $filename = date('Y-m-d_H-i-s') . '.' . $ext; + } + + $targetPath = $targetDir . $filename; + + // Éviter l'écrasement + if (file_exists($targetPath)) { + $base = pathinfo($filename, PATHINFO_FILENAME); + $filename = $base . '_' . time() . '.' . $ext; + $targetPath = $targetDir . $filename; + } + + if (move_uploaded_file($file['tmp_name'], $targetPath)) { + $mdPath = "assets/images/$subdir/$filename"; + echo json_encode([ + 'success' => true, + 'filename' => $filename, + 'subdir' => $subdir, + 'markdown' => "![Description]($mdPath)", + 'path' => $mdPath, + ]); + } else { + echo json_encode(['success' => false, 'error' => 'Impossible de déplacer le fichier']); + } + exit; +} + +// Lister les images existantes +$existingImages = []; +foreach ($SUBDIRS as $sd) { + $dir = $UPLOAD_DIR . $sd . '/'; + if (is_dir($dir)) { + $files = scandir($dir); + foreach ($files as $f) { + if ($f === '.' || $f === '..') continue; + $ext = strtolower(pathinfo($f, PATHINFO_EXTENSION)); + if (in_array($ext, $ALLOWED_EXTENSIONS)) { + $existingImages[] = [ + 'name' => $f, + 'subdir' => $sd, + 'path' => "assets/images/$sd/$f", + 'url' => "docs/assets/images/$sd/$f", + 'size' => filesize($dir . $f), + ]; + } + } + } +} +// Image à la racine de images/ +$rootDir = $UPLOAD_DIR; +if (is_dir($rootDir)) { + $files = scandir($rootDir); + foreach ($files as $f) { + if ($f === '.' || $f === '..' || is_dir($rootDir . $f)) continue; + $ext = strtolower(pathinfo($f, PATHINFO_EXTENSION)); + if (in_array($ext, $ALLOWED_EXTENSIONS)) { + $existingImages[] = [ + 'name' => $f, + 'subdir' => '(racine)', + 'path' => "assets/images/$f", + 'url' => "docs/assets/images/$f", + 'size' => filesize($rootDir . $f), + ]; + } + } +} +?> + + + + + + Upload Images — Documentation SmartEye + + + +

Upload Images

+

Documentation SmartEye — Glissez, cliquez ou collez (Cmd+V) une image

+ +
+ + +
+ +
+
📸
+

Glissez une image ici ou cliquez pour sélectionner

+

Cmd+V pour coller depuis le presse-papiers • JPG, PNG, GIF, WebP, SVG • Max 10 MB

+ +
+ +
+ +

Images existantes ()

+ +

Aucune image pour l'instant.

+ + + + +
Copié !
+ + + + diff --git a/photos.php b/photos.php new file mode 100755 index 0000000..ca2c000 --- /dev/null +++ b/photos.php @@ -0,0 +1,355 @@ + $c) { + // Check 1: ID based (New) + if ($url_id !== null && (string)$id === (string)$url_id) { + $authorized_client = $c; + break; + } + // Check 2: Name/Token based (Legacy) + if (strcasecmp($c['name'], $url_client) == 0 && $c['token'] === $url_token) { + $authorized_client = $c; + break; + } + } + } +} + +if (!$authorized_client) { http_response_code(403); die("

⛔ ACCÈS REFUSÉ

"); } + +// --- 2. ANALYSE DE L'ÉTAT --- +$client_folder = 'clients/' . $authorized_client['name']; +$images = is_dir($client_folder) ? glob($client_folder . "/*.jpg") : []; +array_multisort(array_map('filemtime', $images), SORT_DESC, $images); + +// État par défaut (SAFE) +$state = 'SAFE'; +$banner_color = '#10b981'; // Vert +$banner_text = '✅ SURVEILLANCE ACTIVE'; +$status_text = 'R.A.S — Tout va bien'; +$current_image = 'placeholder.jpg'; +$alert_time = "--/--/----"; + +// DÉTECTION INTELLIGENTE +if (!empty($images)) { + $current_image = $images[0]; + $alert_time = date("d/m/Y à H:i:s", filemtime($current_image)); + + // On demande à la Base de Données ce qu'en pense l'IA + $is_real_alert = $authorized_client['alerte'] ?? false; + + // Lire le verdict RÉEL de l'IA depuis le sidecar JSON de l'image la plus récente + $sidecar_file = $current_image . ".json"; + $sidecar_data = null; + $ia_was_urgent = null; // null = pas de données, true = IA a dit urgence, false = IA a dit RAS + if (file_exists($sidecar_file)) { + $sidecar_data = json_decode(file_get_contents($sidecar_file), true); + if ($sidecar_data && isset($sidecar_data['urgence'])) { + $ia_was_urgent = $sidecar_data['urgence']; + } + } + + // SÉCURITÉ CRITIQUE : Détecter si la DB est en retard sur les alertes réelles + // Compare le timestamp de l'image vs la dernière mise à jour de la DB + $image_timestamp = filemtime($current_image); + $db_last_update = strtotime($authorized_client['last_update'] ?? '2000-01-01'); + $db_is_stale = ($image_timestamp > $db_last_update + 60); // +60s de marge + + // Est-ce que quelqu'un s'en occupe déjà ? + $handler = $authorized_client['handled_by'] ?? null; + $handler_time = $authorized_client['handled_at'] ?? ''; + + if ($handler) { + // ÉTAT ORANGE : PRIS EN CHARGE + $state = 'HANDLED'; + $banner_color = '#f59e0b'; + $banner_text = "🟠 PRISE EN CHARGE PAR " . strtoupper($handler); + $status_text = "⚠️ Intervention en cours par $handler depuis $handler_time"; + } + elseif ($is_real_alert === true) { + // ÉTAT ROUGE : DB dit alerte active + $state = 'DANGER'; + $banner_color = '#ef4444'; + $banner_text = '🚨 ALERTE EN COURS'; + $status_text = '⚠️ Chute Détectée — Intervention Requise'; + } + elseif ($ia_was_urgent === true && $db_is_stale) { + // ÉTAT ROUGE : Le sidecar dit URGENCE mais la DB n'a pas été mise à jour + // → L'alerte n'a JAMAIS été traitée, la DB est en retard + $state = 'DANGER'; + $banner_color = '#ef4444'; + $banner_text = '🚨 ALERTE NON TRAITÉE'; + $status_text = '⚠️ Alerte détectée — Aucune prise en charge enregistrée !'; + } + elseif ($is_real_alert === false && $ia_was_urgent === true) { + // ÉTAT VERT : Alerte RÉELLE qui a été traitée et archivée + // La DB a été mise à jour APRÈS l'image → quelqu'un a géré l'incident + $state = 'RESOLVED'; + $banner_color = '#10b981'; + $banner_text = "✅ INCIDENT TRAITÉ & ARCHIVÉ"; + $status_text = "L'alerte a été prise en charge et archivée."; + } + elseif ($is_real_alert === false) { + // FAUSSE ALERTE : L'IA a dit "pas d'urgence" (sidecar urgence=false ou absent) + $state = 'FALSE_POSITIVE'; + $banner_color = '#3b82f6'; + $banner_text = "🛡️ FAUSSE ALERTE ANNULÉE PAR IA"; + $status_text = "ℹ️ Mouvement détecté, mais l'IA confirme que tout va bien."; + } + else { + // ÉTAT ROUGE : Fallback sécurité + $state = 'DANGER'; + $banner_color = '#ef4444'; + $banner_text = '🚨 ALERTE EN COURS'; + $status_text = '⚠️ Chute Détectée — Intervention Requise'; + } +} +?> + + + + + + LUCAS - Supervision + + + + + +
+
LUCAS
+
+
+ +
+
+ +
+ +
+ + +
+
+
HORODATAGE
+
🕒
+
+
+ 🤖 ANALYSE IA +
+
+
+ +
+
+ +
+

Système Connecté

+

Aucune anomalie détectée.

+
+ + + + + + +
Historique des captures ()
+
+ $img): + $timestamp = filemtime($img); + $date_str = date("d/m H:i", $timestamp); + + // DATA ARCHIVED Check + $json_sidecar = $img . ".json"; + $archived_data = null; + if (file_exists($json_sidecar)) { + $archived_data = json_decode(file_get_contents($json_sidecar), true); + } + + // Prepare JS attributes + $safe_message = htmlspecialchars($archived_data['message'] ?? '', ENT_QUOTES); + $safe_urgence = $archived_data['urgence'] ?? null; // null if no archive + + // If no archive, we pass 'null' to indicate "Unknown/Legacy" + $js_urgence = json_encode($safe_urgence); + ?> +
", this, , "")'> + + + +
+ +
+ +
+ + + +
+ + +
+ + + + +
+ 📞 112 + +
+ + +

L'IA a invalidé l'alerte du capteur.

+ + + +

Cet incident a été traité et archivé.

+ + + +

L'alerte est maintenue visible pour les autres.

+ + + +
+ + + + + diff --git a/placeholder.jpg b/placeholder.jpg new file mode 100755 index 0000000..d4d79a9 Binary files /dev/null and b/placeholder.jpg differ diff --git a/provision.php b/provision.php new file mode 100644 index 0000000..d69b920 --- /dev/null +++ b/provision.php @@ -0,0 +1,137 @@ + + * + * { + * "client_id": "dupont_marie", + * "name": "Marie Dupont", + * "senior_name": "Mamie Marie", + * "address": "12 rue des Roses", + * "city": "20270 Aleria", + * "latitude": "42.1028", + * "longitude": "9.5147", + * "emergency_number": "15", + * "contacts": [{"name": "Jean", "role": "Fils", "phone": "+33698765432"}] + * } + */ +header("Content-Type: application/json"); +header("Access-Control-Allow-Origin: *"); +header("Access-Control-Allow-Methods: POST, OPTIONS"); +header("Access-Control-Allow-Headers: Content-Type, Authorization"); + +if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; } +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + die(json_encode(["success" => false, "message" => "POST uniquement"])); +} + +// Authentification admin +$ADMIN_PASS = "smart123"; +$auth_header = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; +$provided_pass = str_replace('Bearer ', '', $auth_header); +if ($provided_pass !== $ADMIN_PASS) { + http_response_code(401); + die(json_encode(["success" => false, "message" => "Authentification admin requise"])); +} + +$input = json_decode(file_get_contents("php://input"), true); +$client_id = preg_replace('/[^a-z0-9_]/', '', strtolower($input['client_id'] ?? '')); + +if (empty($client_id)) { + http_response_code(400); + die(json_encode(["success" => false, "message" => "client_id requis"])); +} + +$DB_FILE = "database.json"; +$db = json_decode(file_get_contents($DB_FILE), true); + +// Vérifier unicité +foreach ($db['clients'] as $c) { + if (strcasecmp($c['name'] ?? '', $client_id) === 0 || strcasecmp($c['name'] ?? '', $input['name'] ?? '') === 0) { + http_response_code(409); + die(json_encode(["success" => false, "message" => "Client déjà existant"])); + } +} + +// Calculer le prochain site_number +$max_site = 0; +foreach ($db['clients'] as $c) { + $sn = $c['site_number'] ?? 0; + if ($sn > $max_site) $max_site = $sn; +} +$site_number = $max_site + 1; + +// Générer le token +$token = bin2hex(random_bytes(4)); + +// Calculer les ports +$base_port = 8550 + ($site_number - 1) * 10; + +// Créer l'entrée client +$db['clients'][$client_id] = [ + "name" => $input['name'] ?? $client_id, + "token" => $token, + "created_at" => date("Y-m-d H:i:s"), + "site_number" => $site_number, + "site_status" => "provisioned", + "senior_name" => $input['senior_name'] ?? $input['name'] ?? '', + "senior_nickname" => $input['senior_nickname'] ?? '', + "senior_photo" => "", + "latitude" => $input['latitude'] ?? '', + "longitude" => $input['longitude'] ?? '', + "emergency_number" => $input['emergency_number'] ?? '15', + "fcm_tokens" => [], + "contacts" => $input['contacts'] ?? [], + "address" => $input['address'] ?? '', + "city" => $input['city'] ?? '', + "phone_mobile" => $input['phone_mobile'] ?? '', + "phone_fixed" => '', + "age" => $input['age'] ?? '', + "sex" => $input['sex'] ?? 'F', + "alerte" => false, +]; + +// Créer le dossier client +$client_dir = "clients/" . ($input['name'] ?? $client_id) . "/"; +if (!is_dir($client_dir)) { + mkdir($client_dir, 0775, true); +} + +// Sauvegarder la DB +file_put_contents($DB_FILE, json_encode($db, JSON_PRETTY_PRINT)); + +// Données de provisioning pour le QR code +$provision_data = [ + "client_id" => $input['name'] ?? $client_id, + "token" => $token, + "site_number" => $site_number, + "server" => "57.128.74.87", +]; + +// Écrire la demande de firewall dans une file d'attente +$fw_queue = "/var/www/lucas/firewall_queue.txt"; +$fw_cmd = "ufw allow " . $base_port . ":" . ($base_port + 9) . "/tcp comment 'Site $site_number - $client_id'"; +file_put_contents($fw_queue, $fw_cmd . "\n", FILE_APPEND); + +// Réponse +echo json_encode([ + "success" => true, + "status" => "provisioned", + "client_id" => $input['name'] ?? $client_id, + "token" => $token, + "site_number" => $site_number, + "base_port" => $base_port, + "port_range" => $base_port . "-" . ($base_port + 9), + "provision_qr_data" => json_encode($provision_data), + "firewall_queued" => true, + "message" => "Client provisionné. Ports $base_port-" . ($base_port + 9) . " en attente d'ouverture firewall.", +], JSON_PRETTY_PRINT); +?> \ No newline at end of file diff --git a/register_token.php b/register_token.php new file mode 100644 index 0000000..1036023 --- /dev/null +++ b/register_token.php @@ -0,0 +1,90 @@ + false, "message" => "Méthode non autorisée"])); +} + +$input = json_decode(file_get_contents("php://input"), true); +$client_id = $input['client_id'] ?? ''; +$fcm_token = $input['fcm_token'] ?? ''; +$password = $input['password'] ?? ''; + +if (empty($client_id) || empty($fcm_token) || empty($password)) { + http_response_code(400); + die(json_encode(["success" => false, "message" => "client_id, fcm_token et password requis"])); +} + +$DB_FILE = "database.json"; +$db = json_decode(file_get_contents($DB_FILE), true); +if (!$db || !isset($db['clients'])) { + http_response_code(500); + die(json_encode(["success" => false, "message" => "Base de données introuvable"])); +} + +// Trouver le client par name +$found_key = null; +foreach ($db['clients'] as $key => $c) { + if (strcasecmp($c['name'] ?? '', $client_id) === 0) { + $found_key = $key; + break; + } +} + +if ($found_key === null) { + http_response_code(404); + die(json_encode(["success" => false, "message" => "Client inconnu"])); +} + +// Vérification mot de passe (bcrypt) +$password_hash = $db['clients'][$found_key]['password_hash'] ?? null; +if ($password_hash === null) { + http_response_code(403); + die(json_encode(["success" => false, "message" => "Compte non configuré"])); +} +if (!password_verify($password, $password_hash)) { + http_response_code(401); + die(json_encode(["success" => false, "message" => "Mot de passe incorrect"])); +} + +// Initialiser fcm_tokens si absent +if (!isset($db['clients'][$found_key]['fcm_tokens'])) { + $db['clients'][$found_key]['fcm_tokens'] = []; +} + +// Dédupliquer +if (!in_array($fcm_token, $db['clients'][$found_key]['fcm_tokens'])) { + $db['clients'][$found_key]['fcm_tokens'][] = $fcm_token; + file_put_contents($DB_FILE, json_encode($db, JSON_PRETTY_PRINT)); + $cameras = []; + foreach ($db['clients'][$found_key]['cameras'] ?? [] as $cam) { + if (!empty($cam['port_rtsp'])) { + $cameras[] = ["name" => $cam['marque'] . ' ' . $cam['modele'], "rtsp_port" => (int)$cam['port_rtsp']]; + } + } + echo json_encode(["success" => true, "message" => "Token enregistré", "smarteye_token" => $db['clients'][$found_key]['token'] ?? '', "jetson_ip" => $db['clients'][$found_key]['jetson_ip'] ?? '', "server_ip" => "57.128.74.87", "cameras" => $cameras]); +} else { + $cameras = []; + foreach ($db['clients'][$found_key]['cameras'] ?? [] as $cam) { + if (!empty($cam['port_rtsp'])) { + $cameras[] = ["name" => $cam['marque'] . ' ' . $cam['modele'], "rtsp_port" => (int)$cam['port_rtsp']]; + } + } + echo json_encode(["success" => true, "message" => "Token déjà enregistré", "smarteye_token" => $db['clients'][$found_key]['token'] ?? '', "jetson_ip" => $db['clients'][$found_key]['jetson_ip'] ?? '', "server_ip" => "57.128.74.87", "cameras" => $cameras]); +} +?> \ No newline at end of file diff --git a/repair_missing_analyses.py b/repair_missing_analyses.py new file mode 100755 index 0000000..a9b26c2 --- /dev/null +++ b/repair_missing_analyses.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +""" +Script de réparation pour analyser les images historiques +qui n'ont pas de fichier JSON sidecar +""" +import os +import sys +import json +import glob +from datetime import datetime + +# Import du module d'analyse +sys.path.insert(0, '/var/www/lucas') +from analyze import analyze_image + +def find_images_without_json(client_folder): + """Trouve toutes les images sans fichier JSON""" + images = glob.glob(f"{client_folder}/*.jpg") + missing = [] + + for img in images: + json_file = img + ".json" + if not os.path.exists(json_file): + missing.append(img) + + return sorted(missing, key=lambda x: os.path.getmtime(x), reverse=True) + +def repair_image_analysis(image_path): + """Analyse une image et crée son fichier JSON sidecar""" + print(f"\n📸 Analyse: {os.path.basename(image_path)}") + + try: + # Analyser l'image + result = analyze_image(image_path) + + # Créer le fichier JSON sidecar + history_data = { + "urgence": result.get("urgence", False), + "message": result.get("message", "Analyse rétrospective"), + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "retrospective": True # Marqueur pour indiquer que c'est une analyse a posteriori + } + + json_file = image_path + ".json" + with open(json_file, 'w') as f: + json.dump(history_data, f, indent=2) + + status = "🚨 URGENCE" if result.get("urgence") else "✅ SÉCURISÉ" + print(f" {status} - {result.get('message', '')[:60]}") + + return True + + except Exception as e: + print(f" ❌ ERREUR: {type(e).__name__}: {str(e)}") + + # Créer quand même un JSON avec l'erreur + history_data = { + "urgence": None, # null = inconnu + "message": f"Erreur d'analyse rétrospective: {type(e).__name__}", + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "retrospective": True, + "error": True + } + + json_file = image_path + ".json" + with open(json_file, 'w') as f: + json.dump(history_data, f, indent=2) + + return False + +def main(): + print("="*70) + print("RÉPARATION DES ANALYSES MANQUANTES") + print("="*70) + + client_folder = "/var/www/lucas/clients/Demo_01" + + if not os.path.exists(client_folder): + print(f"❌ Dossier introuvable: {client_folder}") + return 1 + + # Trouver les images sans JSON + print(f"\n🔍 Recherche des images sans analyse...") + missing_images = find_images_without_json(client_folder) + + if not missing_images: + print("✅ Toutes les images ont déjà une analyse JSON!") + return 0 + + print(f"\n📊 Trouvé: {len(missing_images)} images sans analyse") + + # Demander confirmation si beaucoup d'images + if len(missing_images) > 50: + print(f"\n⚠️ ATTENTION: {len(missing_images)} images à analyser") + print(f" Cela peut prendre ~{len(missing_images) * 5} secondes") + response = input(" Continuer ? (o/n): ") + if response.lower() != 'o': + print("Annulé.") + return 0 + + # Analyser les images + print(f"\n🚀 Démarrage de l'analyse...") + print("-"*70) + + success_count = 0 + error_count = 0 + + for i, img in enumerate(missing_images, 1): + print(f"\n[{i}/{len(missing_images)}]", end=" ") + + if repair_image_analysis(img): + success_count += 1 + else: + error_count += 1 + + # Résumé + print("\n" + "="*70) + print("RÉSUMÉ") + print("="*70) + print(f"Images analysées: {len(missing_images)}") + print(f"✅ Succès: {success_count}") + print(f"❌ Erreurs: {error_count}") + print("\n✓ Réparation terminée!") + print("="*70) + + return 0 + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + print("\n\n⚠️ Interruption par l'utilisateur") + sys.exit(1) + except Exception as e: + print(f"\n❌ Erreur fatale: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/reset.php b/reset.php new file mode 100755 index 0000000..b305e43 --- /dev/null +++ b/reset.php @@ -0,0 +1,66 @@ + "error", "message" => "DB introuvable"])); +} +$db = json_decode(file_get_contents($json_file), true); + +$client_input = $_REQUEST['client'] ?? ''; +$token_input = $_REQUEST['token'] ?? ''; +$user = $_REQUEST['user'] ?? 'Inconnu'; + +$idx = -1; +if (isset($db['clients'])) { + foreach ($db['clients'] as $i => $c) { + if (strcasecmp($c['name'], $client_input) == 0 && $c['token'] === $token_input) { + $idx = $i; + break; + } + } +} + +if ($idx === -1) { + http_response_code(403); + die(json_encode(["status" => "error", "message" => "Client ou Token incorrect"])); +} + +// 1. RESET DB +$db['clients'][$idx]['alerte'] = false; +$db['clients'][$idx]['message'] = "✅ Intervention effectuée par $user. Système réarmé."; +$db['clients'][$idx]['last_update'] = date("d/m/Y H:i:s"); +file_put_contents($json_file, json_encode($db, JSON_PRETTY_PRINT)); + +// 2. ARCHIVAGE ROBUSTE +$client_name = $db['clients'][$idx]['name']; +$source_dir = $base_dir . '/clients/' . $client_name . '/'; +$archive_dir = $source_dir . 'archives/'; + +// Création dossier archives si besoin +if (!is_dir($archive_dir)) { + if (!mkdir($archive_dir, 0777, true)) { + die(json_encode(["status" => "error", "message" => "Impossible de créer le dossier archives (Permissions?)"])); + } +} + +// Déplacement des fichiers +$images = glob($source_dir . "*.jpg"); +$moved = 0; + +foreach($images as $img) { + $filename = basename($img); + if(rename($img, $archive_dir . $filename)) { + $moved++; + } +} + +echo json_encode([ + "status" => "success", + "message" => "Reset OK. $moved images archivées." +]); +?> \ No newline at end of file diff --git a/shared/deployment/setup_shared.sh b/shared/deployment/setup_shared.sh new file mode 100644 index 0000000..878a7e3 --- /dev/null +++ b/shared/deployment/setup_shared.sh @@ -0,0 +1,222 @@ +#!/bin/bash +# ============================================================ +# setup_shared.sh — À lancer depuis le serveur OVH (session Lucas) +# Initialise le dossier partagé pour les 3 agents +# ============================================================ + +SHARED="/var/www/shared/deployment" + +echo "📁 Création de l'arborescence..." +mkdir -p "$SHARED"/{smarteye,lucas,lucasapp,synthesis} + +# ============================================================ +# BRIEF COMMUN +# ============================================================ +cat > "$SHARED/BRIEF.md" << 'BRIEF_EOF' +# 🎯 Mission : Préparer le Déploiement Multi-Client + +## Contexte +Le système SmartEye/Lucas/LucasApp fonctionne en mode pilote (Aléria). +TOUT est en dur : IPs caméras, ports tunnels, nom du senior, coordonnées GPS, +clés API, tokens Firebase, URLs RTSP, etc. + +## Objectif +Rendre le système **déployable chez n'importe quel client** en externalisant +TOUTES les valeurs en dur dans des fichiers de configuration. + +## Architecture +``` +Caméras IP (réseau local client) + ↓ RTSP +Jetson Orin Nano (SmartEye) — détection chutes + ↓ SSH tunnels (autossh) +Serveur OVH 57.128.74.87 (Lucas) — backend, Gemini, Firebase + ↓ FCM push +App mobile (LucasApp) — alertes, flux live, interphone +``` + +## Valeurs actuellement en dur + +### SmartEye (Jetson) +- IPs caméras : 192.168.1.143, 192.168.1.148, 192.168.1.109 +- Ports RTSP : 554, chemins /11 (HD) et /12 (SD) +- Serveur Lucas : 46.105.30.107 (ANCIEN — maintenant 57.128.74.87) +- Tunnels SSH : ports 8554/8555/8556 (RTSP) + 8800 (audio) +- User SSH : rachid (sur ancien serveur) ou debian (nouveau) +- Seuils détection : conf=0.15, pose=0.25, GS=45/30/20, ghost=30s, immobilité=12s +- Port web UI : 8080 + +### Lucas (OVH) +- senior_name : "Mamie Lucas" +- senior_photo_url : https://lucas.unigest.fr/photos/mamie.jpg +- Coordonnées GPS : 42.1028, 9.5147 (Aléria) +- URLs RTSP publiques : rtsp://46.105.30.107:8554/12, :8555/12, :8556/12 +- Clé Gemini : en dur dans analyze.py +- Credentials Firebase : chemin fixe +- Tokens FCM : en dur ou semi-dur +- Config SMS OVH : clés en dur +- Numéro urgence : +33652631829 +- Ports firewall : fixes + +### LucasApp (Flutter) +- URL API : https://lucas.unigest.fr +- WebSocket audio : ws://46.105.30.107:8800 +- Numéro urgence SAMU : 15 + +## Livrables attendus par agent + +Chaque agent doit produire dans son dossier : +1. `plan.md` — Son plan d'action détaillé +2. `config_spec.*` — Le format de fichier config proposé (yaml/ini/dart) +3. `dependencies.md` — Ce dont il a besoin des 2 autres agents +4. `questions.md` — Questions ouvertes pour les autres +5. `status.txt` — "DONE" quand il a terminé son round + +## Convention de ports par client +Chaque site client = plage de 10 ports sur OVH : +- Site 1 (pilote) : 8550-8559 +- Site 2 : 8560-8569 +- Site N : 8550 + N*10 + +## IMPORTANT +- Le user SSH sur le nouveau OVH est `debian` (pas `rachid`) +- L'IP OVH est maintenant `57.128.74.87` +- Domaine : lucas.unigest.fr +- Les caméras sont des CTronic avec RTSP + audio ONVIF +- Flux HD = /11, flux SD = /12 +BRIEF_EOF + +echo "✅ Brief créé : $SHARED/BRIEF.md" + +# ============================================================ +# INSTRUCTIONS SMARTEYE +# ============================================================ +cat > "$SHARED/smarteye/INSTRUCTIONS.md" << 'EOF' +# 📷 Instructions — Agent SmartEye + +## Ton rôle +Tu es responsable du code Python de détection embarqué sur le Jetson Orin Nano. + +## Lis d'abord +- `/var/www/shared/deployment/BRIEF.md` — le brief commun + +## Ce que tu dois faire +1. Identifier TOUTES les valeurs en dur dans le code SmartEye +2. Créer un format `smarteye_config.yaml` qui externalise tout +3. Modifier le code pour charger depuis ce YAML au démarrage +4. Adapter le service systemd autossh pour lire les ports depuis la config +5. Créer un script `setup_site.sh` interactif pour générer la config d'un nouveau site +6. Documenter les dépendances vers Lucas (endpoints, format des alertes) + +## Écris tes résultats dans +- `/var/www/shared/deployment/smarteye/plan.md` +- `/var/www/shared/deployment/smarteye/config_spec.yaml` (exemple de config) +- `/var/www/shared/deployment/smarteye/dependencies.md` +- `/var/www/shared/deployment/smarteye/questions.md` + +## Consulte les travaux des autres +- `/var/www/shared/deployment/lucas/` — ce que Lucas propose +- `/var/www/shared/deployment/lucasapp/` — ce que LucasApp propose +- `/var/www/shared/deployment/synthesis/` — les synthèses croisées + +## Quand tu as fini +Écris "DONE" dans `/var/www/shared/deployment/smarteye/status.txt` +EOF + +echo "✅ Instructions SmartEye créées" + +# ============================================================ +# INSTRUCTIONS LUCAS +# ============================================================ +cat > "$SHARED/lucas/INSTRUCTIONS.md" << 'EOF' +# 🖥️ Instructions — Agent Lucas + +## Ton rôle +Tu es responsable du backend serveur sur OVH (57.128.74.87). + +## Lis d'abord +- `/var/www/shared/deployment/BRIEF.md` — le brief commun + +## Ce que tu dois faire +1. Identifier TOUTES les valeurs en dur dans analyze.py, alert_ack.php, alert_watchdog.py +2. Créer un format `lucas_config.ini` qui externalise tout +3. Modifier les scripts Python/PHP pour charger depuis cette config +4. Gérer le MULTI-SITE : un seul serveur Lucas doit pouvoir servir N clients +5. Créer un script `add_site.py` pour initialiser un nouveau site (BDD + config + firewall) +6. Documenter le format exact du payload FCM (ce que LucasApp attend) +7. Documenter les endpoints HTTP (ce que SmartEye appelle) + +## Écris tes résultats dans +- `/var/www/shared/deployment/lucas/plan.md` +- `/var/www/shared/deployment/lucas/config_spec.ini` (exemple de config) +- `/var/www/shared/deployment/lucas/dependencies.md` +- `/var/www/shared/deployment/lucas/questions.md` +- `/var/www/shared/deployment/lucas/api_contract.md` (endpoints + payloads) + +## Consulte les travaux des autres +- `/var/www/shared/deployment/smarteye/` — ce que SmartEye propose +- `/var/www/shared/deployment/lucasapp/` — ce que LucasApp propose +- `/var/www/shared/deployment/synthesis/` — les synthèses croisées + +## Quand tu as fini +Écris "DONE" dans `/var/www/shared/deployment/lucas/status.txt` +EOF + +echo "✅ Instructions Lucas créées" + +# ============================================================ +# INSTRUCTIONS LUCASAPP +# ============================================================ +cat > "$SHARED/lucasapp/INSTRUCTIONS.md" << 'EOF' +# 📱 Instructions — Agent LucasApp + +## Ton rôle +Tu es responsable de l'application mobile Flutter (Android). + +## Lis d'abord +- `/var/www/shared/deployment/BRIEF.md` — le brief commun + +## Ce que tu dois faire +1. Identifier TOUTES les valeurs en dur dans le code Dart +2. Faire en sorte que l'app soit 100% pilotée par le payload FCM (zéro config statique liée à un site) +3. Vérifier que les URLs RTSP, WebSocket, endpoints API viennent tous du serveur +4. Proposer un `lib/config.dart` minimal (uniquement l'URL de base Lucas) +5. Documenter ce que l'app attend dans le payload FCM (champs requis) +6. S'assurer que l'app fonctionne pour n'importe quel client sans rebuild + +## Écris tes résultats dans +- `/var/www/shared/deployment/lucasapp/plan.md` +- `/var/www/shared/deployment/lucasapp/config_spec.dart` (exemple) +- `/var/www/shared/deployment/lucasapp/dependencies.md` +- `/var/www/shared/deployment/lucasapp/questions.md` +- `/var/www/shared/deployment/lucasapp/fcm_payload_spec.md` (ce que le payload doit contenir) + +## Consulte les travaux des autres +- `/var/www/shared/deployment/smarteye/` — ce que SmartEye propose +- `/var/www/shared/deployment/lucas/` — ce que Lucas propose +- `/var/www/shared/deployment/synthesis/` — les synthèses croisées + +## Quand tu as fini +Écris "DONE" dans `/var/www/shared/deployment/lucasapp/status.txt` +EOF + +echo "✅ Instructions LucasApp créées" + +# ============================================================ +# PERMISSIONS +# ============================================================ +chmod -R 775 "$SHARED" +chown -R debian:debian "$SHARED" 2>/dev/null || true + +echo "" +echo "════════════════════════════════════════════════════════" +echo "✅ Dossier partagé prêt : $SHARED" +echo "════════════════════════════════════════════════════════" +echo "" +ls -la "$SHARED" +echo "" +echo "Contenu :" +find "$SHARED" -type f | sort +echo "" +echo "🚀 Prochaine étape : coller les prompts dans chaque Claude Code" +echo "════════════════════════════════════════════════════════" \ No newline at end of file diff --git a/sms_dashboard.php b/sms_dashboard.php new file mode 100755 index 0000000..3720f72 --- /dev/null +++ b/sms_dashboard.php @@ -0,0 +1,115 @@ +getMessage()); +} + +// Stats +$stats_sql = "SELECT client_name, COUNT(*) as total FROM sms_logs GROUP BY client_name ORDER BY total DESC"; +$stats = $pdo->query($stats_sql)->fetchAll(PDO::FETCH_ASSOC); + +// Logs +$logs_sql = "SELECT * FROM sms_logs ORDER BY sent_at DESC LIMIT 100"; +$logs = $pdo->query($logs_sql)->fetchAll(PDO::FETCH_ASSOC); +?> + + + + + SMS Manager - Lucas + + + + + + +
+
+

+ + SMS MANAGER +

+

Base de données PostgreSQL

+
+
+ derniers logs affichés +
+
+ +
+ + +
+

📊 Volume par Client

+
+ +
+
+ + SMS +
+
+
+
+
+ +
+
+ + +
+
+

📝 Historique des envois

+
+
+ + + + + + + + + + + + + + + + + + + +
DateClientMessageStatut
+ + + +
+
+ + + + OK + + ERR + +
+
+
+
+ + + diff --git a/sms_manager.py b/sms_manager.py new file mode 100755 index 0000000..d34a180 --- /dev/null +++ b/sms_manager.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +import json +import ovh +import os +import sys +import psycopg2 + +# DB Config +DB_PARAMS = { + "host": "localhost", + "database": "smarteye", + "user": "lucas", + "password": "smarteye_db_pass" +} + +# --- CONFIGURATION OVH (A REMPLIR AVEC TES VRAIES CLES) --- +client = ovh.Client( + endpoint='ovh-eu', + application_key='dc809738a0e0ca05', + application_secret='9a0b1723318676047919f397cabbd4d6', + consumer_key='f1cd0f7101fe2f9e57d1e3bc7c230fa0' +) +SERVICE_NAME = "sms-gr31483-1" # Ton ID Service (gardé de ton ancien code) + +# --- MAIN : Réception des ordres de PHP --- +if __name__ == "__main__": + # On attend 2 arguments : le numéro et le message + if len(sys.argv) < 3: + print("Erreur: Arguments manquants") + sys.exit(1) + + destinataire = sys.argv[1] + message_txt = sys.argv[2] + + try: + # ENVOI REEL OVH + result = client.post(f'/sms/{SERVICE_NAME}/jobs', + message=message_txt, + sender="UNIGESTFR", + receivers=[destinataire], + noStopClause=True, + priority="high" + ) + print(f"✅ SUCCÈS OVH: {result}") + + except Exception as e: + print(f"❌ ERREUR OVH: {str(e)}") + + + +def get_contacts(client_id): + """Lit database.json et renvoie le Nom du client et la liste des numéros""" + db_path = '/var/www/lucas/database.json' # Chemin absolu pour être sûr + + if not os.path.exists(db_path): + return "Inconnu", [] + + try: + with open(db_path, 'r') as f: + data = json.load(f) + + # Structure : clients -> client_id -> contacts + client_data = data.get("clients", {}).get(client_id) + if not client_data: + return "Client Inconnu", [] + + nom_client = client_data.get("name", "Client") + contacts_list = client_data.get("contacts", []) + + numeros_a_contacter = [] + for contact in contacts_list: + phone = contact.get("phone") + if phone: + # Nettoyage + clean = phone.replace(" ", "").replace(".", "").replace("-", "") + numeros_a_contacter.append(clean) + + return nom_client, numeros_a_contacter + + except Exception as e: + sys.stderr.write(f"Erreur lecture DB: {e}\n") + return "Erreur", [] + +def send_alert(client_id, image_url=""): + """Envoie le SMS à tous les contacts du client""" + nom_client, numeros = get_contacts(client_id) + + if not numeros: + return False, "Aucun contact trouvé" + + message_txt = f"ALERTE CHUTE ! {nom_client} est au sol. Voir preuve : {image_url}" + + # On tronque si trop long (sécurité) + if len(message_txt) > 160: message_txt = message_txt[:160] + + logs = [] + for num in numeros: + try: + # ENVOI REEL OVH + result = client.post(f'/sms/{SERVICE_NAME}/jobs', + message=message_txt, + sender="UNIGESTFR", # <--- ON AJOUTE TON SENDER ICI + senderForResponse=False, # <--- ON MET FALSE (Impossible de répondre à un nom) + receivers=[num], + noStopClause=True, + priority="high" + ) + ) + logs.append(f"OK vers {num}") + + # --- LOG TO POSTGRES --- + try: + conn = psycopg2.connect(**DB_PARAMS) + cur = conn.cursor() + cur.execute(""" + INSERT INTO sms_logs (to_number, client_name, message, status, provider_id) + VALUES (%s, %s, %s, %s, %s) + """, (num, nom_client, message_txt, "SENT", str(result.get('ids', [''])[0]))) + conn.commit() + cur.close() + conn.close() + except Exception as e: + logs.append(f"DB ERROR: {e}") + + except Exception as e: + logs.append(f"ECHEC vers {num}: {str(e)}") + # Log failure to DB too + try: + conn = psycopg2.connect(**DB_PARAMS) + cur = conn.cursor() + cur.execute(""" + INSERT INTO sms_logs (to_number, client_name, message, status) + VALUES (%s, %s, %s, %s) + """, (num, nom_client, message_txt, "ERROR")) + conn.commit() + cur.close() + conn.close() + except: pass + + return True, ", ".join(logs) diff --git a/test_analyze_full.py b/test_analyze_full.py new file mode 100644 index 0000000..d22491b --- /dev/null +++ b/test_analyze_full.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +""" +Test complet du système d'analyse avec une vraie image +""" +import sys +import json +import os + +# Import du module d'analyse +sys.path.insert(0, '/var/www/lucas') +from analyze import analyze_image + +print("="*70) +print("TEST COMPLET DU SYSTÈME D'ANALYSE") +print("="*70) + +# Image de test +test_image = "/var/www/lucas/test_alerte.jpg" + +if not os.path.exists(test_image): + print(f"✗ Image de test introuvable: {test_image}") + sys.exit(1) + +print(f"\n📸 Image testée: {test_image}") +print(f"📊 Taille: {os.path.getsize(test_image)} octets") + +print("\n🤖 Lancement de l'analyse IA...") +print("-"*70) + +try: + result = analyze_image(test_image) + + print("\n✓ ANALYSE TERMINÉE") + print("-"*70) + print(json.dumps(result, indent=2, ensure_ascii=False)) + + # Analyser le résultat + print("\n📋 INTERPRÉTATION") + print("-"*70) + + if result.get("urgence"): + print("🚨 URGENCE DÉTECTÉE") + print(f" Confiance: {result.get('confiance', 'N/A')}%") + print(f" Message: {result.get('message', 'N/A')}") + + if "error_details" in result: + print("\n⚠️ DÉTAILS DE L'ERREUR:") + for key, value in result["error_details"].items(): + print(f" - {key}: {value}") + else: + print("✓ PAS D'URGENCE") + print(f" Confiance: {result.get('confiance', 'N/A')}%") + print(f" Message: {result.get('message', 'N/A')}") + + print("\n" + "="*70) + print("Test terminé avec succès") + print("="*70) + +except KeyboardInterrupt: + print("\n\n⚠️ Test interrompu par l'utilisateur") + sys.exit(1) + +except Exception as e: + print(f"\n✗ ERREUR LORS DU TEST") + print(f" Type: {type(e).__name__}") + print(f" Message: {str(e)}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/test_annuaire.py b/test_annuaire.py new file mode 100644 index 0000000..4649906 --- /dev/null +++ b/test_annuaire.py @@ -0,0 +1,17 @@ +import google.generativeai as genai +import os + +# Configure ta clé API ici (remplace par la vraie !) +os.environ["GOOGLE_API_KEY"] = "AIzaSyC5wIc9nrhtbVesHqodil6V6-WIxkJFSHA" +genai.configure(api_key=os.environ["GOOGLE_API_KEY"]) + +print("--- DEBUT DU TEST ---") +try: + print("Interrogation de Google...") + for m in genai.list_models(): + # On cherche uniquement les modèles qui voient (vision) + if 'vision' in m.name or 'gemini-1.5' in m.name or 'pro' in m.name: + print(f"✅ MODÈLE DISPO : {m.name}") +except Exception as e: + print(f"❌ ERREUR CRITIQUE : {e}") +print("--- FIN DU TEST ---") \ No newline at end of file diff --git a/test_json_robustness.py b/test_json_robustness.py new file mode 100755 index 0000000..d61df4e --- /dev/null +++ b/test_json_robustness.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +""" +Script de test pour valider la robustesse du parsing JSON +""" +import sys +sys.path.insert(0, '/var/www/lucas') + +from analyze import nettoyer_json_response, extraire_json_intelligent, valider_reponse_ia +import json + +# Tests de différents formats de réponses malformées +test_cases = [ + # Cas 1: JSON propre + { + "input": '{"urgence": true, "confiance": 85, "message": "Personne au sol"}', + "expected_success": True, + "description": "JSON propre" + }, + # Cas 2: JSON avec markdown + { + "input": '```json\n{"urgence": false, "confiance": 90, "message": "Pièce vide"}\n```', + "expected_success": True, + "description": "JSON dans markdown" + }, + # Cas 3: JSON avec texte avant + { + "input": 'Voici mon analyse:\n{"urgence": true, "confiance": 75, "message": "Chute détectée"}', + "expected_success": True, + "description": "JSON avec texte avant" + }, + # Cas 4: JSON avec texte après + { + "input": '{"urgence": false, "confiance": 95, "message": "Tout va bien"}\nVoilà mon analyse.', + "expected_success": True, + "description": "JSON avec texte après" + }, + # Cas 5: JSON avec espaces + { + "input": ' \n\n {"urgence": true, "confiance": 80, "message": "Urgence"} \n\n ', + "expected_success": True, + "description": "JSON avec espaces" + }, + # Cas 6: JSON multiligne + { + "input": '''{ + "urgence": true, + "confiance": 88, + "message": "Personne allongée" +}''', + "expected_success": True, + "description": "JSON multiligne" + }, + # Cas 7: Réponse complètement invalide + { + "input": 'Ceci n\'est pas du JSON du tout, juste du texte', + "expected_success": False, + "description": "Texte non-JSON" + }, + # Cas 8: JSON incomplet + { + "input": '{"urgence": true, "confiance": ', + "expected_success": False, + "description": "JSON incomplet" + }, +] + +print("="*70) +print("TEST DE ROBUSTESSE DU PARSING JSON") +print("="*70) + +passed = 0 +failed = 0 + +for i, test in enumerate(test_cases, 1): + print(f"\n[TEST {i}] {test['description']}") + print(f"Input: {repr(test['input'][:60])}...") + + try: + # Tenter l'extraction + result = extraire_json_intelligent(test['input']) + + if result: + # Valider le résultat + is_valid = valider_reponse_ia(result) + + if is_valid and test['expected_success']: + print(f"✓ SUCCÈS - JSON extrait: {result}") + passed += 1 + elif not is_valid and test['expected_success']: + print(f"✗ ÉCHEC - JSON invalide: {result}") + failed += 1 + elif is_valid and not test['expected_success']: + print(f"✗ ÉCHEC INATTENDU - JSON trouvé alors qu'on attendait un échec") + failed += 1 + else: + print(f"✓ SUCCÈS - Échec attendu correctement géré") + passed += 1 + else: + if test['expected_success']: + print(f"✗ ÉCHEC - Aucun JSON extrait") + failed += 1 + else: + print(f"✓ SUCCÈS - Échec correctement détecté") + passed += 1 + + except Exception as e: + if test['expected_success']: + print(f"✗ ÉCHEC - Exception: {type(e).__name__}: {e}") + failed += 1 + else: + print(f"✓ SUCCÈS - Exception attendue: {type(e).__name__}") + passed += 1 + +print("\n" + "="*70) +print(f"RÉSULTATS: {passed} réussis, {failed} échoués sur {len(test_cases)} tests") +print("="*70) + +if failed == 0: + print("\n✓ TOUS LES TESTS SONT PASSÉS ! Le système est robuste.") + sys.exit(0) +else: + print(f"\n✗ {failed} test(s) ont échoué. Révision nécessaire.") + sys.exit(1) diff --git a/unregister_token.php b/unregister_token.php new file mode 100644 index 0000000..72cec38 --- /dev/null +++ b/unregister_token.php @@ -0,0 +1,75 @@ + false, "message" => "Méthode non autorisée"])); +} + +$input = json_decode(file_get_contents("php://input"), true); +$client_id = $input['client_id'] ?? ''; +$fcm_token = $input['fcm_token'] ?? ''; +$password = $input['password'] ?? ''; + +if (empty($client_id) || empty($fcm_token) || empty($password)) { + http_response_code(400); + die(json_encode(["success" => false, "message" => "client_id, fcm_token et password requis"])); +} + +$DB_FILE = "database.json"; +$db = json_decode(file_get_contents($DB_FILE), true); +if (!$db || !isset($db['clients'])) { + http_response_code(500); + die(json_encode(["success" => false, "message" => "Base de données introuvable"])); +} + +$found_key = null; +foreach ($db['clients'] as $key => $c) { + if (strcasecmp($c['name'] ?? '', $client_id) === 0) { + $found_key = $key; + break; + } +} + +if ($found_key === null) { + http_response_code(404); + die(json_encode(["success" => false, "message" => "Client inconnu"])); +} + +// Vérification mot de passe (bcrypt) +$password_hash = $db['clients'][$found_key]['password_hash'] ?? null; +if ($password_hash === null) { + http_response_code(403); + die(json_encode(["success" => false, "message" => "Compte non configuré"])); +} +if (!password_verify($password, $password_hash)) { + http_response_code(401); + die(json_encode(["success" => false, "message" => "Mot de passe incorrect"])); +} + +if (isset($db['clients'][$found_key]['fcm_tokens'])) { + $tokens = $db['clients'][$found_key]['fcm_tokens']; + $new_tokens = array_values(array_filter($tokens, function($t) use ($fcm_token) { + return $t !== $fcm_token; + })); + $db['clients'][$found_key]['fcm_tokens'] = $new_tokens; + file_put_contents($DB_FILE, json_encode($db, JSON_PRETTY_PRINT)); + echo json_encode(["success" => true, "message" => "Token retiré"]); +} else { + echo json_encode(["success" => true, "message" => "Aucun token à retirer"]); +} +?> diff --git a/validate_fix.sh b/validate_fix.sh new file mode 100755 index 0000000..1f58358 --- /dev/null +++ b/validate_fix.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +echo "╔════════════════════════════════════════════════════════════════════╗" +echo "║ VALIDATION COMPLÈTE DU CORRECTIF ║" +echo "╚════════════════════════════════════════════════════════════════════╝" +echo "" + +ERRORS=0 + +# Test 1: Syntaxe Python +echo "🔍 Test 1: Validation syntaxe Python..." +if python3 -m py_compile /var/www/lucas/analyze.py 2>/dev/null; then + echo " ✅ Syntaxe valide" +else + echo " ❌ Erreur de syntaxe" + ERRORS=$((ERRORS+1)) +fi +echo "" + +# Test 2: Import du module +echo "🔍 Test 2: Import du module analyze..." +if python3 -c "import sys; sys.path.insert(0, '/var/www/lucas'); import analyze" 2>/dev/null; then + echo " ✅ Import réussi" +else + echo " ❌ Échec d'import" + ERRORS=$((ERRORS+1)) +fi +echo "" + +# Test 3: Tests de robustesse JSON +echo "🔍 Test 3: Tests de robustesse JSON..." +if python3 /var/www/lucas/test_json_robustness.py 2>&1 | grep -q "8 réussis, 0 échoués"; then + echo " ✅ 8/8 tests passés" +else + echo " ❌ Certains tests ont échoué" + ERRORS=$((ERRORS+1)) +fi +echo "" + +# Test 4: Vérification des fichiers créés +echo "🔍 Test 4: Vérification des fichiers..." +FILES=( + "/var/www/lucas/analyze.py" + "/var/www/lucas/check_gemini_health.py" + "/var/www/lucas/test_json_robustness.py" + "/var/www/lucas/test_analyze_full.py" + "/var/www/lucas/GEMINI_FIX_README.md" + "/var/www/lucas/CORRECTIF_TECHNIQUE.md" + "/var/www/lucas/CHANGELOG_v2.0.md" +) + +for FILE in "${FILES[@]}"; do + if [ -f "$FILE" ]; then + echo " ✅ $(basename $FILE)" + else + echo " ❌ $(basename $FILE) manquant" + ERRORS=$((ERRORS+1)) + fi +done +echo "" + +# Test 5: Vérification des permissions +echo "🔍 Test 5: Vérification des permissions..." +if [ -x "/var/www/lucas/analyze.py" ]; then + echo " ✅ analyze.py est exécutable" +else + echo " ⚠️ analyze.py n'est pas exécutable" +fi +echo "" + +# Test 6: Taille du fichier principal +echo "🔍 Test 6: Vérification taille analyze.py..." +SIZE=$(stat -f%z /var/www/lucas/analyze.py 2>/dev/null || stat -c%s /var/www/lucas/analyze.py 2>/dev/null) +if [ "$SIZE" -gt "10000" ]; then + echo " ✅ Taille: $SIZE octets (contenu significatif)" +else + echo " ❌ Taille: $SIZE octets (trop petit)" + ERRORS=$((ERRORS+1)) +fi +echo "" + +# Résumé +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +if [ $ERRORS -eq 0 ]; then + echo "✅ VALIDATION RÉUSSIE - Tous les tests sont passés" + echo "" + echo "Le correctif est prêt pour la production." + echo "Vous pouvez monitorer avec: python3 /var/www/lucas/check_gemini_health.py" + exit 0 +else + echo "❌ VALIDATION ÉCHOUÉE - $ERRORS erreur(s) détectée(s)" + echo "" + echo "Veuillez corriger les erreurs avant de déployer." + exit 1 +fi