Sylvain corrige : il a réinitialisé la montre de son propre chef, la montre n'avait pas planté. Cette section affirmait « les redémarrages n'ont rien changé, il a FALLU une réinitialisation » — une inférence présentée comme une observation, et personne n'avait épuisé les gestes réversibles avant. Ce qui reste établi : après un essai BLE refusé, l'interface USB ne répondait plus dans la foulée — plus aucun /dev/cu.usbmodem*, system_profiler muet, même câble et même port qu'une heure plus tôt. Ce qui ne l'est pas, et qui est retiré : que ce soit durable, qu'une réinitialisation soit le remède, et que « le firmware dégrade son état ». Cette dernière formule extrapolait depuis une observation unique. Le plantage du 17/08 sur requête malformée, lui, reste établi. Si le cas se reproduit, la marche à suivre demande maintenant d'épuiser les gestes réversibles — débrancher, changer de port et de câble, redémarrer la montre, laisser reposer — et de noter lequel a marché. C'est la mesure qui manque au dossier. Conséquence sur une décision prise ce matin : le refus d'ajouter un bouton « Réessayer » s'appuyait sur ce « coût matériel avéré ». La prémisse ne tient plus telle quelle. Le bouton reste absent pour une autre raison — la voie BLE n'est pas viable, outiller la répétition d'un chemin qu'on n'emprunte pas serait à contre-emploi — et la section dit désormais que la décision revient à Sylvain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
480 lines
24 KiB
Markdown
480 lines
24 KiB
Markdown
# É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 dans `Info.plist`, en français.
|
|
|
|
⚠️ **La clé ne suffit pas à déclencher la demande.** iOS ne sollicite
|
|
l'autorisation qu'à la création d'un `CBCentralManager`. Or
|
|
`CBDeviceListenerImpl.search()` commence par
|
|
`monitorBleState().filter { $0 == .poweredOn }` : sans manager, cet état
|
|
n'arrive jamais, le flux n'émet rien, le scan ne démarre pas — et le timeout
|
|
conclut « aucun appareil ». Mesuré le 31/08 : 30 s de scan, aucune alerte, et
|
|
« Bluetooth » absent de Réglages → coach (seuls Position, Caméra, Mouvement,
|
|
Actualisation, Données cellulaires y figuraient).
|
|
|
|
C'est pourquoi `PolarPsFtpWriter` commence par une `SondeBluetooth` : elle crée
|
|
le manager, attend son premier état, et traduit ce qu'il dit — autorisation
|
|
refusée, Bluetooth éteint, matériel absent. **Au premier lancement après ce
|
|
correctif, iOS affichera l'alerte d'autorisation : l'accepter.**
|
|
|
|
## 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 **trois causes opposées** — la distinction
|
|
a été ajoutée le 31/08 après un premier essai où un message unique ne permettait
|
|
pas de conclure :
|
|
|
|
| Message | Cause probable | Geste |
|
|
|---|---|---|
|
|
| « aucun appareil Bluetooth détecté » | Bluetooth éteint, **autorisation refusée à coach**, montre hors de portée | Réglages → coach → Bluetooth |
|
|
| « N appareil(s) vu(s), M portant PsFTP mais sans réponse » | canal déjà occupé | fermer **complètement** Polar Flow |
|
|
| « N appareil(s) vu(s), aucun n'expose PsFTP » | la V3 n'annonce pas le service tant qu'elle est appairée à Flow | c'est l'inconnue de fond |
|
|
|
|
⚠️ Le scan se fait par **fenêtres successives** de 4 s jusqu'au timeout, et non
|
|
en une passe : CoreBluetooth peut n'être pas encore `poweredOn` au premier
|
|
appel, et une fenêtre unique conclurait à tort qu'aucun appareil n'existe.
|
|
|
|
## L'heure de l'objectif — ce qu'on sait, et ce qu'on ne sait pas
|
|
|
|
Un objectif est rangé sous `/U/0/<AAAAMMJJ>/TST/<HHMMSS>/`, et le `start_time`
|
|
écrit dans le fichier doit concorder avec ce dossier : c'est le couple qui situe
|
|
la séance dans la journée. `build_session.py --hour` et le paramètre `hour` des
|
|
routes produisent les deux ensemble, il n'y a donc rien à accorder à la main.
|
|
|
|
⚠️ **En revanche, on ignore si la montre conditionne l'affichage à cette
|
|
heure.** Propose-t-elle l'objectif toute la journée, ou seulement à son
|
|
approche ? Le test du 2026-08-17 ne l'a pas mesuré. À observer au premier envoi
|
|
réussi : écrire un objectif daté 18 h et regarder s'il apparaît immédiatement.
|
|
|
|
- S'il apparaît tout de suite → l'heure n'est qu'une étiquette, en fixer une par
|
|
défaut suffit.
|
|
- S'il n'apparaît qu'à l'approche → il faut écrire à l'heure du départ. Sans
|
|
conséquence pratique : l'envoi se fait de toute façon juste avant de sortir.
|
|
|
|
## 🔴 Où ça bloque au 31/08 : le scan du SDK ne démarre pas
|
|
|
|
Mesuré, pas supposé : **43 appareils vus par un scan CoreBluetooth nu, 0 remonté
|
|
par le SDK Polar.** La radio, l'autorisation et la montre sont donc hors de
|
|
cause — le problème est dans notre usage du SDK.
|
|
|
|
Ce qui a été écarté **sur les sources**, pour ne pas le refaire :
|
|
|
|
- `servicesToScanFor` à `nil` : c'est aussi ce que fait `PolarBleApiImpl`.
|
|
- `scanPreFilter` à `nil` : **accepte** les appareils. La condition est
|
|
`if self.session(peripheral) == nil, let filter = self.scanPreFilter` — sans
|
|
filtre, le `if let` échoue et le rejet est sauté.
|
|
- `addClient()` suffit à lancer le scan : `scanningNeeded()` rend vrai dès que
|
|
`clientCount != 0`, aucune action « admin » n'est requise.
|
|
|
|
**CAUSE RACINE TROUVÉE** — confirmée par l'instrumentation : le publisher de
|
|
`search()` n'émettait **rien du tout**, ni valeur, ni complétion, ni erreur.
|
|
|
|
`CBDeviceListenerImpl.manager` est une **`lazy var`** : le `CBCentralManager`
|
|
n'existe qu'au premier accès.
|
|
|
|
```swift
|
|
fileprivate lazy var manager: SDKCBCentralManager = { … }()
|
|
```
|
|
|
|
Or `search()` commence par `monitorBleState().filter { $0 == .poweredOn }`, et
|
|
`monitorBleState()` se contente de rendre `bleStateSubject` — il ne touche
|
|
jamais `manager`. Les seuls accès du chemin de recherche
|
|
(`retrieveConnectedPeripherals`, `retrievePeripherals`) sont **à l'intérieur du
|
|
`flatMap`**, donc après le filtre. Le manager n'était donc jamais instancié,
|
|
`centralManagerDidUpdateState` jamais appelé, le sujet jamais alimenté : le
|
|
filtre bloquait pour toujours.
|
|
|
|
**Deuxième cause, découverte juste après** : sans `scanPreFilter`, le listener
|
|
remonte **tous** les appareils BLE alentour — 43 mesurés. Le code ouvrait alors
|
|
une session sur chacun à tour de rôle, dont l'Apple Watch
|
|
(`API MISUSE: … name = Apple Watch de Sylvain … can only accept commands while
|
|
in the connected state`), à 12 s d'échéance chacun : l'envoi paraissait ne
|
|
jamais finir. Le SDK officiel pose ce filtre (`deviceFilter` dans
|
|
`PolarBleApiImpl`) ; ne pas le poser était l'omission.
|
|
|
|
**Correctif** : appeler `listener.blePowered()` — `return manager.state ==
|
|
.poweredOn`, le seul accès public exécuté immédiatement — **après** s'être
|
|
abonné à `search()`. L'ordre est critique : si `bleStateSubject` est un
|
|
`PassthroughSubject`, un état émis avant l'abonnement serait perdu et on
|
|
retomberait sur le même silence.
|
|
|
|
## Le démarrage du SDK — deux détails qui décident de tout
|
|
|
|
Lus dans les sources, après plusieurs essais infructueux le 31/08 :
|
|
|
|
**1. `bleStateSubject` est un `CurrentValueSubject<BleState, Never>(.unknown)`**,
|
|
pas un PassthroughSubject. Il **rejoue sa valeur à chaque abonnement** : il est
|
|
donc inutile d'être abonné au moment de l'émission. Un correctif intermédiaire
|
|
réveillait le manager *après* s'être abonné « pour ne pas rater l'événement » —
|
|
raisonnement juste pour un PassthroughSubject, faux ici, et qui exposait au
|
|
piège suivant.
|
|
|
|
**2. `centralManagerDidUpdateState` fait
|
|
`BleState(rawValue: self.manager.state.rawValue)`** — il accède à la `lazy var`
|
|
depuis le délégué. Si CoreBluetooth appelle le délégué avant que l'initialisation
|
|
de la lazy soit terminée, la propriété se réentre.
|
|
|
|
**3. `blePowered()` et `search()` ne lisent pas la même chose.** `blePowered()`
|
|
rend `manager.state == .poweredOn` — l'état de CoreBluetooth. `search()` filtre
|
|
sur `bleStateSubject`, qui n'est alimenté que par `centralManagerDidUpdateState`.
|
|
Le manager peut donc être allumé sans que le délégué ait publié quoi que ce
|
|
soit : le sujet reste à `.unknown` et le filtre bloque. Attendre `blePowered()`
|
|
ne prouve rien sur ce que `search()` verra.
|
|
|
|
⇒ **Séquence correcte** : créer le listener, poser `scanPreFilter`, appeler
|
|
`blePowered()` **une fois** (son seul rôle : instancier la lazy pour que le
|
|
délégué puisse tourner), puis attendre sur **`monitorBleState()`** — public, et
|
|
c'est la source que `search()` lit — jusqu'à `.poweredOn`. Seulement ensuite,
|
|
`search()`, une fois, sur toute la durée. Un abonnement par tranche relancerait le cycle
|
|
`addClient()`/`removeClient()` du scanner et l'empêcherait de découvrir quoi que
|
|
ce soit.
|
|
|
|
**Ce qui a été corrigé et vérifié en chemin** (ne pas refaire) :
|
|
|
|
| Cause | Preuve |
|
|
|---|---|
|
|
| Autorisation Bluetooth jamais demandée | pas de ligne « Bluetooth » dans Réglages → coach |
|
|
| Manager `lazy` jamais réveillé | publisher muet ; `blePowered()` le crée |
|
|
| Aucun filtre de scan | 43 appareils, sessions ouvertes jusque sur l'Apple Watch |
|
|
| Attentes sans limite de temps | envoi figé sans message |
|
|
| Fenêtres de scan successives | `bleStateSubject` ne publie que sur changement |
|
|
|
|
**⚠️ Ce que ça enseigne** : instancier `CBDeviceListenerImpl` soi-même revient à
|
|
réimplémenter ce que `PolarBleApiImpl` fait, sans en voir le détail. On avance
|
|
d'un cran à chaque essai, et chaque essai coûte un cycle Xcode.
|
|
|
|
### Piste pour la reprise : passer par l'API publique
|
|
|
|
Plutôt que de piloter le listener à la main, utiliser
|
|
`PolarBleApiDefaultImpl.polarImplementation(…)` — le chemin que Polar supporte —
|
|
pour la découverte et la connexion (`searchForDevice()`, `connectToDevice(_:)`),
|
|
puis n'atteindre `BlePsFtpClient` qu'une fois la session établie par le SDK.
|
|
|
|
⚠️ À vérifier d'abord, et c'est le point bloquant de cette piste :
|
|
`PolarBleApiImpl` garde son `listener` privé. Il faut donc chercher si une API
|
|
publique donne accès à la session ou au client PsFTP — sinon cette voie est
|
|
fermée elle aussi, et il faudra soit forker le SDK, soit renoncer au Bluetooth
|
|
et garder le câble.
|
|
|
|
**Le câble, lui, fonctionne intégralement** : `tools/polar/README.md` du dépôt
|
|
`coach_sportif`. Rien de ce qui a été livré côté serveur n'est perdu.
|
|
|
|
## 🔑 La montre ne s'annonce pas : elle est déjà connectée
|
|
|
|
Mesuré le 31/08 : **4020 advertisements vus** (HomePod, Furbo…), **aucune
|
|
Polar**. Le scan fonctionne parfaitement — la Vantage ne diffuse simplement pas.
|
|
|
|
C'est le comportement normal du BLE : **un appareil connecté cesse d'émettre**.
|
|
La montre est liée à l'iPhone par l'app Flow, donc elle est invisible au scan,
|
|
par conception. L'attendre était sans espoir, et tous les correctifs de scan qui
|
|
ont précédé ne pouvaient rien y changer.
|
|
|
|
`search()` prévoit ce cas, mais seulement quand on lui passe des UUID :
|
|
|
|
```swift
|
|
foundPeripherals = self.manager.retrieveConnectedPeripherals(withServices: uuids!)
|
|
```
|
|
|
|
⇒ **Appeler `search([BlePsFtpClient.PSFTP_SERVICE], identifiers: nil,
|
|
fetchKnownDevices: true)`**, et non `search(nil, …)` : c'est cette branche qui
|
|
rend les périphériques déjà connectés exposant PsFTP et leur fabrique une
|
|
session.
|
|
|
|
⚠️ Dédupliquer aussi les sessions vues : `AllowDuplicates` est armé côté SDK, le
|
|
même appareil revient à chaque advertisement.
|
|
|
|
## ⛔ RÉPONSE DE FOND (31/08) : la V3 refuse la connexion tierce
|
|
|
|
La question ouverte depuis le 17/08 est tranchée, par la mesure.
|
|
|
|
Une fois la montre cherchée au bon endroit (`retrieveConnectedPeripherals`), le
|
|
plugin **l'atteint** : « 12 appareils vus, **1 portant PsFTP** ». Le service FEEE
|
|
est donc bien exposé et `fetchGattClient` rend un `BlePsFtpClient`.
|
|
|
|
Mais `waitPsFtpReady` n'aboutit pas, et **la montre affiche « connexion
|
|
impossible »** pendant que les réglages iOS la disent toujours connectée à Flow.
|
|
Elle refuse donc la seconde session.
|
|
|
|
⇒ **Le canal PsFTP de la Vantage V3 n'est pas partageable.** Tant qu'elle est
|
|
liée à l'app Flow, aucune app tierce ne peut ouvrir de session — ce qui est
|
|
cohérent avec l'USB, où il fallait déjà fermer Flow.
|
|
|
|
⚠️ **Ce qui n'a PAS été tenté, et qui est la dernière porte** : dissocier la
|
|
montre de Flow (oublier l'appareil côté iOS) pour voir si elle accepte alors
|
|
notre connexion. Non tenté délibérément — cela reviendrait à sacrifier la
|
|
synchronisation quotidienne, qui alimente coach en données, pour un confort
|
|
d'envoi. Le rapport n'y est pas.
|
|
|
|
⚠️ Après un essai de connexion refusé, **redémarrer la montre** (appui long sur
|
|
OK) pour libérer son canal, puis rouvrir Flow.
|
|
|
|
## Ce qui reste inconnu
|
|
|
|
1. ~~La V3 accepte-t-elle une connexion BLE tierce ?~~ **Non** — mesuré le
|
|
31/08, voir ci-dessus.
|
|
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.
|
|
|
|
## 🟠 CE QUE L'ESSAI BLE A COÛTÉ (31/08) — corrigé le 01/09
|
|
|
|
⚠️ **Cette section a d'abord été écrite plus alarmante que les faits.** Sylvain
|
|
l'a corrigée le 01/09 : **la réinitialisation était de son propre chef**, pas une
|
|
nécessité constatée. Distinguer ce qui a été observé de ce qui en a été déduit.
|
|
|
|
**Observé** — après la tentative de connexion Bluetooth refusée par la montre
|
|
(« connexion impossible » à l'écran), **son interface USB ne répondait plus** :
|
|
plus aucun `/dev/cu.usbmodem*`, et `system_profiler` ne listait plus rien —
|
|
alors que la montre chargeait normalement, et que c'était le même câble, le même
|
|
port et la même montre qu'une heure plus tôt.
|
|
|
|
**Déduit à tort** — « les redémarrages n'ont rien changé, il a *fallu* une
|
|
réinitialisation ». La réinitialisation a été **choisie**, pas imposée : aucune
|
|
autre voie n'a été épuisée avant. On ne sait donc pas si l'USB serait revenu
|
|
seul, après un délai, un autre port ou un autre câble.
|
|
|
|
⇒ **Ce qui reste établi** : un essai BLE refusé peut laisser l'USB muet dans la
|
|
foulée. ⇒ **Ce qui ne l'est pas** : que ce soit durable, ni qu'une
|
|
réinitialisation soit le remède. Si le cas se reproduit, épuiser d'abord les
|
|
gestes réversibles — débrancher/rebrancher, changer de port et de câble,
|
|
redémarrer la montre, laisser reposer — et **noter lequel a marché**. C'est
|
|
cette mesure qui manque.
|
|
|
|
⚠️ **Conséquence à assumer, dans sa juste mesure** : une tentative de connexion
|
|
PsFTP refusée peut laisser l'USB muet — observé une fois. Que le firmware
|
|
« dégrade durablement son état » était une extrapolation depuis cette
|
|
observation unique : **retirée**. Le plantage du 17/08 sur requête malformée,
|
|
lui, reste établi et documenté plus haut.
|
|
|
|
⇒ **Ne pas multiplier les essais sans raison**, et prévoir que le câble puisse
|
|
ne pas répondre juste après. Écrire la séance PAR CÂBLE D'ABORD quand c'est
|
|
possible — non parce que la casse est certaine, mais parce qu'un aller-retour de
|
|
diagnostic avant une sortie coûte plus que deux minutes d'anticipation.
|
|
|
|
## 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.
|
|
|
|
## 2026-09-01 — Après réinitialisation, le BLE marche. Et ANCS n'est pas la voie.
|
|
|
|
### Fait neuf : l'écriture BLE a abouti
|
|
|
|
Rapporté par Sylvain : **après la réinitialisation complète de la montre** (celle
|
|
qu'a imposée l'incident USB), l'envoi de l'exercice par Bluetooth **a
|
|
fonctionné**. Il ajoute lui-même : « ce n'est pas viable ».
|
|
|
|
Ça confirme le diagnostic du 31/08 plutôt que de l'infirmer : le refus ne vient
|
|
pas d'une interdiction des apps tierces, mais de **l'occupation du canal PsFTP
|
|
par Flow**. Montre neuve, pas encore liée à Flow ⇒ le canal est libre ⇒ notre
|
|
session s'ouvre. Dès que Flow reprend la main, elle se referme.
|
|
|
|
⇒ **Le prix d'accès au BLE reste le même** : ne pas avoir Flow. Et depuis le
|
|
01/09 ce prix a augmenté — la Polar est devenue la montre de sport, donc Flow
|
|
est le seul canal par lequel les séances entrent dans HealthKit puis dans coach.
|
|
|
|
### La question de Sylvain : « et le protocole des notifications iOS ? »
|
|
|
|
Bonne intuition — les notifications iOS arrivent bien à la montre **pendant que
|
|
Flow est connecté**. Mais le canal ne peut pas porter une séance, et la raison
|
|
est structurelle.
|
|
|
|
**ANCS — Apple Notification Center Service** ([spec Apple][ancs]) :
|
|
|
|
- **Les rôles sont inversés par rapport à PsFTP.** « The publisher of the ANCS
|
|
service (**the iOS device**) shall be referred to as the *Notification
|
|
Provider* » ; « any client of the ANCS service (**an accessory**) shall be
|
|
referred to as a *Notification Consumer* ». C'est l'**iPhone** qui est
|
|
serveur, la montre qui est cliente — d'où l'absence de conflit avec Flow : la
|
|
montre ne cède rien, elle consomme.
|
|
- **Trois caractéristiques, et rien d'autre** : Notification Source
|
|
(`9FBF120D-…`), Control Point (`69D1D8F3-…`), Data Source (`22EAC6E9-…`).
|
|
- **Aucun transport de binaire.** La spec ne décrit aucun mécanisme de transfert
|
|
de fichier ni de payload applicatif : uniquement des métadonnées de
|
|
notification et des commandes d'action prédéfinies.
|
|
|
|
⇒ **On ne fera pas passer un `.BPB` par ANCS.** Ce n'est pas une limite de notre
|
|
implémentation, c'est ce que le protocole est.
|
|
|
|
### Ce qu'ANCS permet quand même, et qui n'est pas rien
|
|
|
|
ANCS transporte du **texte affiché au poignet**, sans câble, sans dissocier, et
|
|
**sans toucher à Flow**. Une notification locale iOS émise par coach est relayée
|
|
par ANCS et s'affiche sur la montre. Le canal existe déjà côté projet :
|
|
`@capacitor/local-notifications` est installé, et `web/static/local-notifications.js`
|
|
est chargé par `_layout.html` — **aucun build Xcode nécessaire**.
|
|
|
|
⚠️ Ce que ça ne fait PAS, et il faut le dire avant de le construire : pas
|
|
d'objectif structuré, **pas d'alerte de zone**, pas de guidage par étape, pas de
|
|
comparaison à l'exécution. C'est un **pense-bête au poignet**, pas un
|
|
remplacement du `.BPB`. La longueur réellement affichable par la V3 n'est pas
|
|
documentée et devra être mesurée.
|
|
|
|
⇒ Le seul chemin qui écrit un vrai objectif **en préservant Flow** reste le
|
|
**câble**.
|
|
|
|
[ancs]: https://developer.apple.com/library/archive/documentation/CoreBluetooth/Reference/AppleNotificationCenterServiceSpecification/Specification/Specification.html
|
|
|
|
### La séquence du 01/09, et ce qu'elle apprend sur la fenêtre
|
|
|
|
Trois envois consécutifs, trois résultats différents — c'est la chronologie qui
|
|
explique tout :
|
|
|
|
| # | résultat | lecture |
|
|
|---|---|---|
|
|
| 1 | **réussi** | montre fraîchement réinitialisée, **pas encore liée à Flow** : canal libre |
|
|
| 2 | **errorcode 104** `DIRECTORY_EXISTS` | canal **toujours** libre, la session s'ouvre — seul le `mkdir` bute sur un dossier déjà créé au 1ᵉʳ envoi |
|
|
| 3 | **« canal probablement occupé »** | Flow a repris la main entre-temps |
|
|
|
|
⇒ Le canal n'est pas ouvert ou fermé « en général » : il y a une **fenêtre**,
|
|
qui commence quand la montre n'est liée à personne et se referme dès que Flow
|
|
se reconnecte. C'est cohérent avec le 31/08 et avec l'USB, où il faut fermer
|
|
Flow.
|
|
|
|
⚠️ **Le message d'erreur du plugin conseillait « fermer complètement l'app Polar
|
|
Flow ». C'est faux, et ça l'était déjà le 31/08** : la montre affichait
|
|
« connexion impossible » pendant que les réglages iOS la disaient **toujours
|
|
connectée** à Flow. iOS maintient le lien d'un accessoire appairé, app fermée ou
|
|
non. Message corrigé le 01/09 — il nomme désormais le geste qui libère
|
|
réellement le canal (redémarrer la montre) et rappelle que le câble est le
|
|
chemin fiable.
|
|
|
|
**Hypothèse ouverte, non vérifiée** : redémarrer la montre puis écrire *dans la
|
|
foulée*, avant que Flow ne se reconnecte, pourrait suffire. Ça expliquerait les
|
|
envois 1 et 2. Ce n'est pas une méthode tant que ça n'a pas été reproduit
|
|
plusieurs fois — et ⛔ chaque essai a un coût matériel avéré (l'USB désactivé le
|
|
31/08). **Écrire la séance par câble AVANT tout essai.**
|
|
|
|
## ⛔ Déconnecter Flow automatiquement : iOS l'interdit (01/09)
|
|
|
|
Question de Sylvain : peut-on déconnecter Polar Flow avant chaque envoi, sans
|
|
geste manuel ? **Non, et ce n'est pas contournable** — c'est une garantie
|
|
d'isolation entre apps, pas une lacune de notre code.
|
|
|
|
Apple, `cancelPeripheralConnection(_:)` :
|
|
|
|
> « Because other apps may still have a connection to the peripheral, canceling
|
|
> a local connection **does not guarantee that the underlying physical link is
|
|
> immediately disconnected**. »
|
|
|
|
Une app n'annule que **sa propre** connexion. Rien dans CoreBluetooth ni dans le
|
|
SDK Polar ne permet de rompre celle d'une autre app, et une app ne peut pas non
|
|
plus couper le Bluetooth du système. Le `disconnectFromDevice` du SDK ne ferme
|
|
que notre session.
|
|
|
|
### Le geste manuel le plus court : le mode avion de la montre
|
|
|
|
Plus rapide qu'un redémarrage, et documenté par Polar
|
|
([Vantage V3, Réglages rapides][qs]) : « Le mode avion coupe toute communication
|
|
sans fil sur votre montre […] vous ne pouvez pas synchroniser vos données avec
|
|
l'application Polar Flow ».
|
|
|
|
**Mode avion ON → OFF**, puis envoyer **immédiatement**. Le ON coupe Flow ; le
|
|
OFF rouvre une fenêtre pendant laquelle le canal PsFTP est libre, avant que Flow
|
|
ne se reconnecte. ⚠️ Pendant le ON, notre app ne peut pas se connecter non plus :
|
|
tout le sans-fil est coupé, c'est bien un ON *puis* OFF.
|
|
|
|
⚠️ **Cette course n'a pas été reproduite plusieurs fois** — c'est une hypothèse
|
|
cohérente avec les envois réussis du 01/09, pas une méthode établie.
|
|
|
|
### Pourquoi il n'y a pas de bouton « Réessayer » dans l'UI
|
|
|
|
⚠️ **Écrit sur une prémisse trop forte, corrigée le 01/09.** L'argument était
|
|
« chaque tentative refusée a un coût matériel avéré — celle du 31/08 a désactivé
|
|
l'USB jusqu'à une réinitialisation ». Or la réinitialisation était **un choix de
|
|
Sylvain**, pas une nécessité constatée. Ce qui reste : l'USB a été muet après un
|
|
essai, une fois, et personne n'a cherché s'il serait revenu seul.
|
|
|
|
Le bouton n'est donc pas refusé sur un risque « avéré ». Il reste absent parce
|
|
que **la voie BLE elle-même n'est pas viable** (course contre Flow, geste manuel
|
|
sur la montre à chaque envoi) : outiller la répétition d'un chemin qu'on
|
|
n'emprunte pas serait du travail à contre-emploi. **C'est à Sylvain de trancher
|
|
s'il veut ce bouton** — la décision lui revient, et l'argument matériel ne doit
|
|
plus peser plus qu'il ne vaut.
|
|
|
|
[qs]: https://support.polar.com/e_manuals/vantage-v3/polar-vantage-v3-user-manual-francais/quick-settings.htm
|