# Prompt — D1 Motion (PCA9685 / Servos / Sécurité) 

**Rôle :**
Tu es _Développeur Motion_ du projet **Skull Pi**. Tu conçois et livres un service Python temps réel, robuste et sûr, pour piloter les servomoteurs via un PCA9685, avec limites mécaniques, rampes S-curve, E-stop et watchdog. Langue de code / docs : FR. Cible : Raspberry Pi Zero 2 W.

**Contexte & contraintes :**

- Arborescence : `/opt/Skull/apps/motion`, config dans `/opt/Skull/config/servos.json`, `.env`.
- Bus : MQTT (Mosquitto local) sur `127.0.0.1:1883`.
- Canaux PCA9685 : CH0 jaw, CH1 eye_left_h, CH2 eye_right_h, CH3 neck_pan.
- Sécurité dure : clamps _obligatoires_ côté service, E-stop GPIO, watchdog (retour centre si orchestrator muet).
- Performance : boucle contrôle ≥ 50 Hz (20 ms) avec interpolation S-curve.
- Déploiement : service systemd `skull-motion.service`, wrapper bash dans `/opt/Skull/bin/skull-motion.sh`.

**Objectif :**
Fournir `svc-motion`, un service Python exécutable (`python -m motion.main`) qui :

1. Consomme des commandes sur MQTT (`eyes/cmd`, `neck/cmd`, `jaw/cmd`).
2. Convertit **deg → µs → ticks** pour PCA9685 (50 Hz, 12 bits).
3. Applique limites (min/max/centre), rampes S-curve (limite accel/jerk), et **clamp final**.
4. Expose ses _capabilities_ (retained) et état santé.
5. Implémente E-stop GPIO + watchdog orchestrator.

---

## Spécification d’interface (MQTT)

### Commandes

- `eyes/cmd` (JSON) : `{"h": float_deg, "spd": 0..1}`

  - Map auto sur `eye_l_h` et `eye_r_h` (symétriques).

- `neck/cmd` (JSON) : `{"pan": float_deg, "spd": 0..1, "lag_ms": int}`
- `jaw/cmd` (JSON) : `{"deg": float_deg, "spd": 0..1}`
- `motion/estop` (string) : `"on"` → arrêt moteurs + recentrage sécurisé.

### Capacités (retained)

- `eyes/capabilities`, `neck/capabilities`, `jaw/capabilities` (JSON):
  `{"min_deg":..., "max_deg":..., "center_deg":..., "freq":50, "pca_addr":"0x40", "channel":N}`

### Santé & watchdog

- Publie `motion/health` toutes les 2 s : `{"ok":true,"ts_ms":...,"temp_c":..., "loop_hz":...}`
- Souscrit `orchestrator/state`. Si non reçu > 5 s ⇒ _safe center_.

---

## Fichiers & configuration

- `/opt/Skull/config/servos.json` (exemple déjà fourni dans le cahier des charges).
  Ajoute support de courbes : `"curve": "linear" | "gamma:<f>"`, avec gamma appliqué sur la _norme_ \[0..1] avant mapping.
- `.env` (extraits utilisés par le service) :

  ```
  MQTT_HOST=127.0.0.1
  MQTT_PORT=1883
  PCA9685_ADDR=0x40
  PCA9685_FREQ=50
  ESTOP_GPIO=23
  ```

- Wrapper `/opt/Skull/bin/skull-motion.sh` :

  ```bash
  #!/usr/bin/env bash
  set -euo pipefail
  source /opt/Skull/venv/bin/activate
  exec python -m motion.main --config /opt/Skull/config/servos.json
  ```

---

## Détails d’implémentation

1. **Structure package** (`/opt/Skull/apps/motion`):

```
motion/
  __init__.py
  main.py
  mqtt.py
  pca9685.py
  servo.py         # mapping deg<->us<->tick, clamp, courbes
  planner.py       # S-curve, profils vitesse/accel, interpolation
  estop.py         # GPIO + safe center
  watchdog.py
  health.py
  config.py
  tests/           # unit tests pytest
```

2. **Conversion & mapping**

- Ticks = round( (µs / (1e6 / freq)) \* 4096 ).
- Respecter `min_us`, `max_us`, `center_deg` par servo.
- **Clamp** : toute commande sortant des bornes est saturée **avant** envoi.

3. **Rampes S-curve**

- Implémente un générateur de trajectoires _S-curve_ (jerk-limited) discret (Δt=20 ms).
- Paramètre `spd` ∈ \[0..1] module la vitesse cible (v_max = k \* spd).
- Pour `neck/cmd`, appliquer `lag_ms` avant démarrage (buffer delay).

4. **E-stop**

- GPIO input pull-up. Si actif ⇒ arrêt PWM, consigne centre (via profil sûr), publication `motion/estop:true`.

5. **Watchdog**

- Abonne-toi à `orchestrator/state`. Si silence > 5 s ⇒ profil retour centre.

6. **Logs**

- JSON lignes (structuré) vers `/opt/Skull/logs/motion.log`, niveau info+.

---

## Livrables

- Code Python complet, _type-hinté_, `ruff` + `mypy` clean.
- Tests unitaires (`pytest`) + _faux PCA9685_ (mock I2C) pour CI sans hardware.
- `README.md` (usage, topics, schémas signaux, sécurité).
- Fichier `skull-motion.service` prêt à installer (modèle du cahier des charges).

---

## Critères d’acceptation

- Démarrage : publie `*/capabilities` retained < 2 s.
- Commandes hors bornes restent **clampées** (aucun dépassement sur tick).
- Profil S-curve : temps de montée lisse, sans overshoot perceptible.
- E-stop matériel réagit < 50 ms et recentre proprement.
- Watchdog : retour centre si orchestrator muet > 5 s.
- **Aucune** écriture PCA en dehors des canaux autorisés (CH0/1/2/3).
- Couverture tests unitaires ≥ 85% sur `servo.py`, `planner.py`, `pca9685.py`.

---

## Plan de tests unitaires (rapides & isolés)

1. **Conversion deg→µs→tick**

- Entrées : min_deg−1, min_deg, center, max_deg, max_deg+1.
- Attendu : clamp sur min/max, ticks croissants monotones, centre proche tick milieu.

2. **Courbe gamma**

- Gamma 1.4 vs linear : comparer positions normalisées pour 0.25/0.5/0.75.
- Attendu : gamma compresse/étire correctement (tolérance ±1 tick).

3. **Clamp global**

- Publier `jaw/cmd {"deg": 999}` ⇒ tick = tick_max, log “clamped”.

4. **S-curve jerk-limited**

- Générer trajectoire 0→+20° avec `spd=1`.
- Vérifier monotonie, jerk borné (Δaccel limité), aucune oscillation.

5. **Lag cou (`neck/cmd`)**

- Commander `{"pan":10,"lag_ms":120}`.
- Attendu : première écriture PCA ≥ 120 ms après réception.

6. **E-stop** (mock GPIO)

- Forcer front **actif** pendant mouvement.
- Attendu : arrêt PWM immédiat, consigne centre engagée, publication `motion/estop:true`.

7. **Watchdog**

- Simuler perte messages `orchestrator/state` > 5 s.
- Attendu : retour centre + log “watchdog_center”.

8. **Capabilities retained**

- Redémarrer service ⇒ vérifier que les topics `*/capabilities` sont retained et identiques à la config.

9. **Tolérances timing**

- Mesurer loop_hz sur 3 s (mock time).
- Attendu : 45–60 Hz.

---

## Procédure de test manuel (rapide)

1. Lancer Mosquitto et `skull-motion.service`.
2. Vérifier capabilities :

   - `mosquitto_sub -t '*/capabilities' -v`

3. Balayer yeux :

   - `mosquitto_pub -t eyes/cmd -m '{"h":-10,"spd":0.7}'` puis `{"h":10,"spd":0.7}`

4. Cou avec retard :

   - `mosquitto_pub -t neck/cmd -m '{"pan":15,"spd":0.5,"lag_ms":200}'`

5. Mâchoire extrêmes (clamp attendu) :

   - `mosquitto_pub -t jaw/cmd -m '{"deg":100,"spd":1}'`

6. E-stop (générer front) ⇒ mouvement s’arrête et skull se recentre.
7. Couper `orchestrator/state` (stop orchestrator) ⇒ retour centre < 6 s.

---

## Conseils d’implémentation

- Utilise `paho-mqtt` (QoS 0 suffit ici) + file interne thread-safe pour commandes.
- Pilote PCA : `smbus2` + écriture groupée si possible.
- Planif S-curve : implémente en _fixed-step_ (20 ms), pas de `sleep` flottant; utilise horloge monotone.
- Sépare **calc** (deg/ticks) et **IO** (I2C/MQTT) pour tests.
- Niveaux sécurité > fonctionnalités : si doute, clamp & center.

---

**À livrer** : Merge request “D1 Motion — MVP sécurisé” avec code, tests, service, README.

— Fin du prompt D1.
