Regroupe l'état de travail en cours réalisé dans un même worktree sur plusieurs tickets/sprints (#85, #136, #145, #155-160, #162-164), mélangeant des tickets QA et inProgress. Ne constitue pas une feature terminée : commit de sauvegarde avant triage/split par ticket en branches feature/* dédiées. Exclut les dossiers d'environnement de build locaux et le heap dump parasite (.gitignore mis à jour). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
68 lines
8.1 KiB
Markdown
68 lines
8.1 KiB
Markdown
---
|
|
issueRef: "#124"
|
|
version: 3
|
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
|
updatedAt: 1785137390360
|
|
---
|
|
|
|
## #124 — Architecture collecte capteurs montre → séance (Architect, 2026-07-26)
|
|
|
|
Statut : **cadrage technique, ouvre la voie à l'implémentation** mais reste combiné à #123 comme décidé par Main sur #106 — aucun lot d'implémentation ne doit s'ouvrir sans relecture croisée de ce cadrage et du cadrage UX #123 (déjà "cadrage terminé côté UX", en attente de ce document).
|
|
|
|
### Constat sur l'existant (fait, pas une supposition)
|
|
Recherche exhaustive sur `android/`, `watch_app/android/` et tous les Gradle : **aucune infrastructure capteur/santé n'existe aujourd'hui**. Ni `BODY_SENSORS`, ni dépendance Health Services/Health Connect, ni Horologist. La seule dépendance Wear présente (`com.google.android.gms:play-services-wearable:19.0.0`) est l'API Wearable Data Layer classique (MessageClient/DataClient), utilisée uniquement pour le bridge commandes/projection existant — **sans rapport avec les capteurs**. C'est donc une **capacité entièrement nouvelle**, pas une extension d'existant.
|
|
|
|
### Faisabilité et choix d'API
|
|
- API recommandée : **Wear OS Health Services** (`androidx.health:health-services-client`), l'API moderne recommandée par Google pour la fréquence cardiaque sur Wear OS 3+ (l'ancienne lecture directe `Sensor.TYPE_HEART_RATE` est déconseillée/restreinte).
|
|
- **Choix structurant : `MeasureClient` (mesure passive), pas `ExerciseClient`.** `ExerciseClient` déclare un "Exercise" au niveau système (peut entrer en conflit avec d'autres apps fitness, affiche sa propre UI/notification système d'exercice en cours). GameTime a déjà son propre concept de séance ; on ne veut pas d'un second concept de "session" concurrent au niveau OS. `MeasureClient.registerMeasureCallback(DataType.HEART_RATE_BPM, ...)` suffit pour une mesure passive sans ce couplage.
|
|
- Dépendance à ajouter : `watch_app/android/app/build.gradle.kts` uniquement (la fréquence cardiaque se mesure sur la montre, jamais sur le téléphone).
|
|
- Permission `android.permission.BODY_SENSORS` (dangereuse, runtime) dans `watch_app/android/app/src/main/AndroidManifest.xml` ; évaluer `BODY_SENSORS_BACKGROUND` si la mesure doit continuer écran éteint au poignet (probable en usage réel de séance).
|
|
|
|
### Modèle de permission (répond à la question ouverte du carnet UX #123)
|
|
Demande **une seule fois, paresseuse**, déclenchée par le premier passage en "séance active" côté montre après déploiement de la feature — jamais un écran d'onboarding dédié (évite une surface supplémentaire non demandée). Refus ou ignorance → la montre ne mesure jamais, le téléphone ne reçoit jamais de résumé, l'UI reste silencieuse — exactement le contrat UX déjà posé par #123 (silence total, jamais de blocage du parcours principal).
|
|
|
|
### Décision structurante : agrégation locale montre, résumé unique en fin de séance
|
|
Pas de flux temps réel synchronisé en continu (inutile : UX #123 exclut explicitement tout affichage live). La montre :
|
|
1. Accumule localement (min/max/somme/count — arithmétique triviale, pas un "modèle de calcul maison" au sens où l'UX l'exclut pour les calories) pendant toute la séance, **pause-aware** : suspend l'agrégation quand la séance est en pause ou que le repos court sans effort (aligné sur le comportement déjà existant des autres chronos pause-aware, cf. [[gametime-session-execution-timer-refactor]]).
|
|
2. Envoie **un seul message résumé** en fin de séance (terminer/abandonner), pas un flux continu — élimine la complexité de synchronisation/idempotence en continu, cohérent avec le fait que rien ne consomme la donnée en direct.
|
|
3. Le canal Data Layer (`MessageClient`) gère nativement la fiabilité/retry si la montre est temporairement injoignable au moment de l'envoi ; si le message n'arrive jamais (montre débranchée, désinstallée...), la séance reste simplement sans ce bloc — silencieux pour toujours, aucune donnée en attente à représenter, aucune migration rétroactive à inventer (conforme à la demande explicite d'UX #123).
|
|
|
|
### Nouveau contrat (séparé du contrat commandes existant)
|
|
Ne pas réutiliser `WatchCommandEnvelope` (sémantique commande utilisateur + ack) pour de la télémétrie sans accusé de réception. Nouveau type dans `packages/watch_bridge_contract` :
|
|
```dart
|
|
class WatchSensorSummary {
|
|
final int schemaVersion;
|
|
final String sessionId;
|
|
final int sampleCount;
|
|
final double? averageHeartRateBpm;
|
|
final int? maxHeartRateBpm;
|
|
}
|
|
```
|
|
- `sampleCount` permet au téléphone de décider "donnée insuffisante" (UX #123 : "données insuffisantes pour un calcul fiable" → bloc absent). Seuil recommandé : `sampleCount < 3` ⇒ traiter comme absent (pas de moyenne calculée sur un bruit d'une ou deux mesures).
|
|
- Pas d'ack requis (fire-and-forget, perte tolérée par design).
|
|
|
|
### Persistance téléphone
|
|
- Champs nullables **au niveau racine de l'entité d'historique de séance** (celle qui porte déjà la durée totale), pas par série/exercice — UX #123 demande une fréquence cardiaque **de la séance**, une seule paire moyenne/max par séance, pas un état vivant par set :
|
|
```
|
|
averageHeartRateBpm: double?
|
|
maxHeartRateBpm: int?
|
|
```
|
|
(nom exact de l'entité à confirmer par DevBackend selon le modèle d'historique réel — non exploré en détail dans ce cadrage.)
|
|
- **Écriture non bloquante (point important)** : ne jamais faire attendre la finalisation de "Terminer la séance" sur l'arrivée du résumé capteur. La séance se termine et s'enregistre immédiatement, `averageHeartRateBpm`/`maxHeartRateBpm` restent `null`. Si le résumé arrive ensuite (avant ou après la fin de séance, l'ordre n'est pas garanti), un use case dédié `updateWorkoutHistoryHeartRateSummary(historySessionId, ...)` applique un **patch idempotent** a posteriori si les champs sont encore `null` sur l'enregistrement d'historique correspondant, silencieusement si l'enregistrement n'existe plus/a été supprimé.
|
|
- Justification : cohérent par analogie avec [[gametime-online-layer-philosophy]] — ne jamais bloquer une action locale critique (ici, terminer une séance) en attendant une confirmation externe, même si ce n'est pas strictement un flux réseau serveur.
|
|
|
|
### Calories (hors MVP, second temps déjà indiqué par UX)
|
|
Même pipeline (`MeasureClient`, `DataType.CALORIES_TOTAL` si disponible nativement) — champ additif `estimatedCalories: double?` sur le même résumé, à ajouter seulement si demandé explicitement plus tard. Ne pas l'inclure au lot 1.
|
|
|
|
### Découpage en lots proposé (permet d'ouvrir l'implémentation sans ambiguïté)
|
|
1. **Watch (natif)** : dépendance Gradle + permission + `HeartRateSampler` (`MeasureClient`) + accumulateur local pause-aware + envoi résumé fin de séance.
|
|
2. **Contrat** : `WatchSensorSummary` dans `watch_bridge_contract` + tests `dart test`.
|
|
3. **Backend/domaine téléphone** : champs nullables sur l'entité d'historique + migration + `updateWorkoutHistoryHeartRateSummary` + écoute du message natif (extension du service d'écoute bridge déjà existant) + logique de patch tardif idempotent.
|
|
4. **Frontend téléphone** : bloc "Fréquence cardiaque" optionnel sur écran de fin de séance + détail historique (absent si `null`, aucun placeholder — conforme #123).
|
|
5. **QA** : permission refusée, montre sans capteur cardio, déconnexion mi-séance, arrivée tardive du résumé après clôture, seuil `sampleCount` insuffisant.
|
|
|
|
Chaque lot est testable isolément : accumulateur watch (calcul pur), DTO contrat (dart test existant), use case patch (test avec repo fake), rendu UI (widget test null vs présent).
|
|
|
|
### Ce qui reste explicitement hors MVP (repris d'UX #123, confirmé faisable/non-faisable ici)
|
|
VO2 max, zones d'intensité, SpO2, ECG — Health Services les expose en partie mais hors périmètre produit GameTime, pas seulement hors scope technique. Pas d'affichage temps réel (choix d'architecture confirmé ci-dessus, pas seulement une préférence UX : la mesure continue en flux aurait needlessly recompliqué idempotence/sync pour zéro valeur d'usage dans ce MVP).
|