Trois causes opposées disaient la même chose : le premier essai BLE n'a rien appris

Sylvain a lancé l'envoi Bluetooth et obtenu « aucune montre Polar exposant
PsFTP ». Ce message ne permet pas de conclure : il couvre à la fois « aucun
appareil vu » (Bluetooth éteint, autorisation refusée à l'app, montre hors de
portée) et « la montre est là mais ne répond pas » (canal déjà pris par Polar
Flow). Ce sont des causes opposées et des gestes différents. Un essai qui
n'apprend rien est un essai perdu, et celui-là demandait de brancher une montre.

L'erreur porte désormais ce qui a été observé — appareils vus, combien sans le
service FEEE, combien avec mais muets — et rend trois messages distincts, chacun
nommant le geste correspondant. Quatre tests verrouillent la distinction, dont
celui qui interdit d'accuser Polar Flow quand rien n'a été vu : ce serait envoyer
sur une fausse piste.

Le scan devient aussi répétitif, par fenêtres de 4 s jusqu'au timeout, au lieu
d'une passe unique de 10 s. CoreBluetooth peut n'être pas encore poweredOn au
premier appel : le scan ne démarre alors jamais et une fenêtre unique conclut à
tort qu'aucun appareil n'existe. C'est une cause plausible du premier échec, et
elle n'était pas couverte.

73 tests au vert sur tests-linux.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Bettinelli
2026-08-31 12:47:28 +00:00
parent 0110368e17
commit 179528d563
4 changed files with 110 additions and 27 deletions

View File

@@ -99,19 +99,40 @@ public struct PftpStep: Equatable {
public enum PolarPftpError: Error, CustomStringConvertible, Equatable {
case malformed(String)
case watchNotFound(Double)
/// `vues` : appareils BLE aperçus · `sansPsFtp` : sans le service FEEE ·
/// `muettes` : avec FEEE mais qui n'ont pas répondu.
case watchNotFound(Double, vues: Int, sansPsFtp: Int, muettes: Int)
case psftpUnavailable
/// Un échec de connexion a plusieurs causes OPPOSÉES, et un message
/// unique les confond c'est ce qui s'est produit au premier essai du
/// 2026-08-31. Ne jamais fusionner ces branches.
public var description: String {
switch self {
case .malformed(let why): return why
case .watchNotFound(let seconds):
return "aucune montre Polar exposant PsFTP trouvée en \(Int(seconds)) s. "
+ "Vérifier que la montre est allumée, à portée, et que l'app Polar "
+ "Flow n'est pas en train de synchroniser — le canal BLE ne se "
+ "partage pas."
case .malformed(let why):
return why
case .psftpUnavailable:
return "session ouverte mais le service PsFTP (FEEE) n'a pas répondu"
case .watchNotFound(let seconds, 0, _, _):
// Rien du tout : le problème est en amont de la montre.
return "aucun appareil Bluetooth détecté en \(Int(seconds)) s. "
+ "Vérifier, dans l'ordre : le Bluetooth activé sur l'iPhone ; "
+ "l'autorisation Bluetooth accordée à coach (Réglages → coach) ; "
+ "la montre allumée et à portée."
case .watchNotFound(let seconds, let vues, _, let muettes) where muettes > 0:
// Le service est là mais ne répond pas : signature d'un canal déjà pris.
return "\(vues) appareil(s) vu(s), \(muettes) portant PsFTP mais sans "
+ "réponse en \(Int(seconds)) s. Le canal est probablement déjà "
+ "occupé : fermer complètement l'app Polar Flow (elle synchronise "
+ "en arrière-plan), puis réessayer."
case .watchNotFound(let seconds, let vues, _, _):
// Des appareils, mais aucun ne porte le service.
return "\(vues) appareil(s) Bluetooth vu(s) en \(Int(seconds)) s, aucun "
+ "n'expose PsFTP. La Vantage n'annonce peut-être pas ce service "
+ "tant qu'elle est appairée à Flow — c'est l'inconnue restante."
}
}
}