Sylvain enchaîne un troisième envoi et reçoit « le canal est probablement occupé » — notre propre message, déclenché par watchNotFound avec des montres muettes : le service PsFTP est là, il ne répond pas. La chronologie de ses trois envois explique tout. Le premier réussit, montre fraîchement réinitialisée donc pas encore liée à Flow. Le deuxième renvoie 104 DIRECTORY_EXISTS, ce qui prouve que le canal était ENCORE libre. Le troisième échoue : Flow a repris la main entre-temps. Le canal n'est donc pas ouvert ou fermé en général — il y a une fenêtre, qui se referme dès que Flow se reconnecte. Le message conseillait de fermer complètement l'app Polar Flow. C'était faux, et la mesure du 31/08 le disait déjà : 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. Le message nomme maintenant le geste qui libère réellement le canal — redémarrer la montre — précise que fermer l'app n'y change rien, et rappelle que le câble est le chemin fiable. Corrigé au passage le message des appareils sans PsFTP, qui présentait encore la question comme « l'inconnue restante » alors qu'elle a été tranchée le 31/08 : la montre expose bien PsFTP quand elle est connectée. Un test interdit désormais de la rouvrir dans l'UI. 88 tests Linux verts, dont deux nouveaux qui verrouillent l'absence du conseil inutile et la présence du geste utile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
416 lines
21 KiB
Markdown
416 lines
21 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) — à lire avant d'en retenter un
|
|
|
|
Après la tentative de connexion Bluetooth refusée par la montre
|
|
(« connexion impossible » à l'écran), **son interface USB a cessé d'exister** :
|
|
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.
|
|
|
|
**Les redémarrages n'ont rien changé.** Il a fallu une **réinitialisation** pour
|
|
que l'USB revienne.
|
|
|
|
⚠️ **Conséquence à assumer** : une tentative de connexion PsFTP refusée peut
|
|
désactiver durablement l'interface USB de la V3. Ce n'est pas une hypothèse,
|
|
c'est ce qui s'est produit. Le firmware ne se contente pas de refuser : il
|
|
dégrade son état.
|
|
|
|
⇒ **Ne pas retenter d'essai BLE sans nécessité**, et surtout pas juste avant une
|
|
sortie : le repli par câble n'est pas garanti disponible après coup. Si un essai
|
|
doit avoir lieu, écrire la séance PAR CÂBLE D'ABORD.
|
|
|
|
## 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.**
|