La séance part sur la Polar en Bluetooth, et refuse de partir mal formée
Contrainte posée aujourd'hui : coach → la montre, sans TrainingPeaks, sans Polar Flow, sans Intervals.icu. Toutes les autres voies passent par un tiers ; celle-ci est la seule qui reste, et elle était déjà prouvée en USB le 17/08. Manquait le transport qui la rende utilisable — brancher la montre au Mac cinq minutes avant de sortir n'est pas un usage. `CoachPolarBLE` ne fait que transporter. Les octets viennent du serveur (GET /api/plan/polar-target), produits par un encodeur verrouillé octet pour octet contre des fichiers relus sur la montre. Rien n'est réencodé ici : le faire perdrait la seule garantie que le firmware acceptera le fichier. La chaîne SDK a été vérifiée sur les sources avant d'écrire une ligne, et elle est entièrement publique — CBDeviceListenerImpl, search, openSessionDirect, fetchGattClient(PSFTP_SERVICE), waitPsFtpReady, write(header:data:). Ni fork, ni symbole interne. Le morceau qui compte vraiment est ailleurs. `PolarPftpStep` est séparé du plugin et n'importe que Foundation, pour être exécuté sur Linux : il porte l'en-tête PbPFtpOperation et les deux règles qui décident si une requête part. Le 17/08, un PUT de 174 octets vers un chemin terminé par « / » a bloqué la montre — écran noir, appui long sur OK pour la récupérer. Le firmware ne refuse pas, il s'effondre, et le Bluetooth utilise le même protocole. Ces règles ne peuvent donc pas être vérifiées « à la relecture » : 12 tests les exécutent, dont celui qui interdit de reporter ici le cadrage série [0x05, taille, taille] de la version USB — en BLE c'est le SDK qui cadre, l'ajouter produirait précisément une requête malformée. L'en-tête produit par Swift a été comparé à celui de polar_ftp.py sur trois chemins, dont un de 205 caractères pour le varint sur deux octets : mêmes octets. 69 tests au vert sur tests-linux. Un mode dryRun construit et décrit chaque requête sans rien émettre — le --dry-run qui manquait le jour du plantage. Le runbook Mac dit de s'en servir au premier essai, et liste les trois inconnues qui ne se lèveront que montre en main, la première étant : la V3 accepte-t-elle une connexion BLE tierce pendant qu'elle est appairée à Flow ? Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
108
docs/polar-ble-runbook-mac.md
Normal file
108
docs/polar-ble-runbook-mac.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Écrire la séance sur la Polar en Bluetooth — ce qui reste à faire sur le Mac
|
||||
|
||||
Le code est écrit et poussé. Ce qui suit ne peut se faire que sur le Mac mini,
|
||||
Xcode ouvert, montre à portée. À lire en entier avant le premier envoi : la
|
||||
première tentative est celle qui peut bloquer la montre.
|
||||
|
||||
## Pourquoi ce chemin, et pas un autre
|
||||
|
||||
Contrainte posée le 2026-08-31 : **`coach → la montre`, sans intermédiaire.**
|
||||
Pas de TrainingPeaks, pas de Polar Flow, pas d'Intervals.icu. Ça élimine toutes
|
||||
les autres voies, qui passent toutes par un tiers. Reste l'écriture directe du
|
||||
fichier d'objectif sur la montre par PFTP — prouvée sur la Vantage V3 le
|
||||
2026-08-17, en USB. Le Bluetooth est ce qui la rend utilisable.
|
||||
|
||||
Le détail de ce qui a été mesuré sur la montre est dans le dépôt `coach_sportif`,
|
||||
`tools/polar/README.md`. Il fait foi.
|
||||
|
||||
## 1. Ajouter le SDK Polar au projet
|
||||
|
||||
⚠️ **Pas dans `ios/App/CapApp-SPM/Package.swift`** : ce fichier porte
|
||||
« DO NOT MODIFY — managed by Capacitor CLI » et sera réécrit. Ajouter le paquet
|
||||
au `.xcodeproj` :
|
||||
|
||||
Xcode → File → Add Package Dependencies…
|
||||
https://github.com/polarofficial/polar-ble-sdk
|
||||
→ produit « PolarBleSdk », cible « App »
|
||||
|
||||
Tant que le paquet n'est pas là, `CoachPolarBLE.isAvailable()` répond
|
||||
`{available: false, reason: "PolarBleSdk absent de la cible"}` — le fichier
|
||||
compile quand même, tout le code radio est derrière `#if canImport(PolarBleSdk)`.
|
||||
|
||||
## 2. Target Membership
|
||||
|
||||
Deux fichiers, cible **App** :
|
||||
|
||||
- `ios/App/App/CoachPolarBLE.swift` — le plugin
|
||||
- `ios/App/App/PolarPftpStep.swift` — les garde-fous et l'en-tête PFTP
|
||||
|
||||
Le second n'importe que Foundation : il est aussi lié dans
|
||||
`tests-linux/Sources/CoachModel/` et couvert par 12 tests qui tournent sur
|
||||
Linux (`./tests-linux/run.sh`).
|
||||
|
||||
## 3. Permission Bluetooth
|
||||
|
||||
`NSBluetoothAlwaysUsageDescription` est déjà dans `Info.plist`, en français.
|
||||
iOS demandera l'autorisation au premier scan.
|
||||
|
||||
## 4. Le premier envoi — en `dryRun`, toujours
|
||||
|
||||
Depuis la console Safari attachée à la WebView :
|
||||
|
||||
```js
|
||||
const r = await fetch('/api/plan/polar-target?date=2026-08-31&hour=18')
|
||||
const plan = await r.json()
|
||||
await window.Capacitor.Plugins.CoachPolarBLE.send({ ...plan, dryRun: true })
|
||||
```
|
||||
|
||||
Attendu :
|
||||
|
||||
```
|
||||
{ sent: false, dryRun: true, log: [
|
||||
"mkdir /U/0/20260831/TST/ (0 o)",
|
||||
"mkdir /U/0/20260831/TST/180000/ (0 o)",
|
||||
"put /U/0/20260831/TST/180000/TST.BPB (240 o)",
|
||||
"put /U/0/20260831/TST/180000/ID.BPB (51 o)" ] }
|
||||
```
|
||||
|
||||
**Lire ce journal avant d'ôter `dryRun`.** Un `mkdir` avec des octets, ou un
|
||||
`put` vers un chemin terminé par « / », est la requête exacte qui a bloqué la
|
||||
montre le 2026-08-17 — le plugin la refuse, mais la voir refusée vaut mieux que
|
||||
de la découvrir en radio.
|
||||
|
||||
## 5. L'envoi réel
|
||||
|
||||
Retirer `dryRun`. Fermer l'app Polar Flow d'abord : **le canal BLE ne se
|
||||
partage pas**, et une synchro Flow en cours empêchera la connexion.
|
||||
|
||||
En cas d'échec, le message distingue les cas — montre introuvable, PsFTP
|
||||
muet, requête refusée. Ce ne sont pas les mêmes causes.
|
||||
|
||||
## Ce qui reste inconnu, et ne se lèvera qu'ici
|
||||
|
||||
1. **La V3 accepte-t-elle une connexion BLE tierce** alors qu'elle est appairée
|
||||
à l'app Flow ? Jamais testé. C'est le risque principal.
|
||||
2. **Le filtrage de la montre** se fait sur « expose le service PsFTP (FEEE) »,
|
||||
pas sur le nom : ce que la V3 met dans son advertisement n'a pas été observé,
|
||||
et s'appuyer dessus aurait été une supposition. À resserrer une fois vu. Un
|
||||
capteur H10 s'écarte de lui-même, il ne porte pas PsFTP.
|
||||
3. **Le firmware plante sur requête malformée.** Les garde-fous couvrent les
|
||||
formes connues ; ils ne prouvent pas qu'il n'en existe pas d'autres.
|
||||
|
||||
## Si la montre se bloque
|
||||
|
||||
Logo Polar puis écran noir. **Appui long sur OK, 10-15 s.** À défaut,
|
||||
BACK + DOWN pendant 10 s. En dernier recours, réinitialisation d'usine par
|
||||
FlowSync — les données déjà synchronisées dans Flow sont préservées.
|
||||
|
||||
## Après l'envoi
|
||||
|
||||
L'objectif est **éphémère** : une synchro Flow supprime ce qui est déposé à une
|
||||
date qu'elle ignore (mesuré). C'est pour ça que l'envoi se fait juste avant de
|
||||
sortir, et c'est pour ça que le Bluetooth était la condition — brancher la
|
||||
montre au Mac cinq minutes avant de courir n'était pas un usage.
|
||||
|
||||
⚠️ Les cibles écrites sont des **numéros de zone**, pas des battements : la
|
||||
montre les exécute selon les zones réglées dans Flow. Vérifiées alignées le
|
||||
2026-08-17, à 1 bpm près sur la frontière Z2/Z3. La réponse du serveur porte les
|
||||
bornes de `current_zones()` pour ce contrôle — les afficher avant l'envoi.
|
||||
Reference in New Issue
Block a user