feat(server): ingest temps réel WS + GUI live + client PC

Serveur FastAPI reçoit le flux JSONL (sim ou ROV réel) sur /ws/ingest,
SLAM incrémental, rediffuse carte+poses sur /ws/live, GUI live et export PLY.
Déployé Docker sur caddy-net, exposé /moulin-live/. Client PC stream_client.py.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Flag
2026-06-06 20:27:17 +00:00
parent 06e198c7d9
commit 6e83bbd73f
15 changed files with 12675 additions and 0 deletions

46
server/client/README.md Normal file
View File

@@ -0,0 +1,46 @@
# moulin-mapper — client stream
## Prérequis
```bash
pip install -r requirements.txt
```
## Commande (simulation rapide)
```bash
python stream_client.py \
--file ../data/sim/run_L.jsonl \
--url wss://laboratoire.freeboxos.fr/moulin-live/ws/ingest \
--token moulin-2026 \
--speed 0
```
## Commande temps réel
```bash
python stream_client.py \
--file ../data/sim/run_L.jsonl \
--url wss://laboratoire.freeboxos.fr/moulin-live/ws/ingest \
--token moulin-2026 \
--speed 1.0
```
## Bridge ROV (futur)
```bash
rov_bridge | python stream_client.py \
--stdin \
--url wss://laboratoire.freeboxos.fr/moulin-live/ws/ingest \
--token moulin-2026
```
## Options
| Option | Défaut | Description |
|--------|--------|-------------|
| `--file FILE` | — | Fichier JSONL à envoyer |
| `--stdin` | — | Lit depuis stdin (bridge ROV) |
| `--url URL` | `ws://127.0.0.1:8211/ws/ingest` | URL WebSocket du serveur |
| `--token TOKEN` | `moulin-2026` | Token d'authentification |
| `--speed FLOAT` | `1.0` | 0=max, 1=temps réel, 2=2× plus vite |

View File

@@ -0,0 +1 @@
websockets>=12.0

View File

@@ -0,0 +1,102 @@
"""
stream_client.py — Envoie un flux JSONL vers le serveur moulin-mapper.
Usage:
python stream_client.py --file run_L.jsonl --url ws://127.0.0.1:8211/ws/ingest --token moulin-2026 --speed 1.0
--speed 0 : aussi vite que possible (pas de délai)
--speed 1 : temps réel (deltas de t respectés)
--stdin : lit depuis stdin au lieu d'un fichier (bridge ROV)
"""
import argparse
import json
import sys
import time
import asyncio
try:
import websockets
except ImportError:
print("Installe d'abord : pip install websockets")
sys.exit(1)
async def stream(url: str, token: str, lines, speed: float):
# Ajoute le token comme query param
sep = "&" if "?" in url else "?"
full_url = f"{url}{sep}token={token}"
t_prev = None
sent = 0
t_file_prev = None
print(f"[moulin-stream] connexion → {full_url}")
while True:
try:
async with websockets.connect(full_url, ping_interval=20, ping_timeout=30) as ws:
print("[moulin-stream] connecté")
for line in lines:
line = line.strip()
if not line:
continue
try:
rec = json.loads(line)
except json.JSONDecodeError:
continue
t_rec = rec.get("t", 0.0)
# Timing
if speed > 0 and t_file_prev is not None:
dt_file = t_rec - t_file_prev
if dt_file > 0:
await asyncio.sleep(dt_file / speed)
t_file_prev = t_rec
await ws.send(json.dumps(rec))
sent += 1
if sent % 100 == 0:
print(f"\r[moulin-stream] {sent} lignes envoyées, t={t_rec:.1f}s", end="", flush=True)
print(f"\n[moulin-stream] terminé — {sent} enregistrements envoyés")
return
except (websockets.exceptions.ConnectionClosed,
ConnectionRefusedError,
OSError) as e:
print(f"\n[moulin-stream] connexion perdue ({e}), reconnexion dans 3s…")
await asyncio.sleep(3.0)
# Redémarre depuis le début si on a perdu la connexion
# (les données sont déjà lues depuis un itérateur — on ne peut pas rembobiner stdin)
print("[moulin-stream] AVERTISSEMENT: relance depuis le début du fichier")
break
def main():
parser = argparse.ArgumentParser(description="Stream JSONL vers moulin-mapper")
src = parser.add_mutually_exclusive_group(required=True)
src.add_argument("--file", help="Fichier JSONL à envoyer")
src.add_argument("--stdin", action="store_true", help="Lit depuis stdin")
parser.add_argument("--url", default="ws://127.0.0.1:8211/ws/ingest",
help="URL WebSocket du serveur (ex: wss://lab.freeboxos.fr/moulin-live/ws/ingest)")
parser.add_argument("--token", default="moulin-2026", help="Token d'authentification")
parser.add_argument("--speed", type=float, default=1.0,
help="Multiplicateur de vitesse (0=max, 1=temps réel, 2=2×)")
args = parser.parse_args()
if args.file:
with open(args.file, "r") as f:
lines = f.readlines()
print(f"[moulin-stream] fichier: {args.file} ({len(lines)} lignes)")
else:
lines = sys.stdin
asyncio.run(stream(args.url, args.token, lines, args.speed))
if __name__ == "__main__":
main()