Une attente sans fin ne dit rien : échéances sur les appels du SDK
L'envoi tournait sans jamais rendre la main, ni message ni erreur. Cause : les `async` du SDK Polar n'ont aucune limite de temps, et `waitPsFtpReady` attend indéfiniment une montre qui ne finit pas sa négociation — canal déjà pris, écran éteint, appairage en cours. Une attente sans fin est pire qu'un échec : elle n'apprend rien, et c'est exactement ce qu'on cherche à éviter depuis ce matin. `waitPsFtpReady` a désormais 12 s, chaque écriture 20 s, et un dépassement compte comme une montre muette — c'est ce qu'elle est. Le message dit quoi faire : réveiller l'écran, rapprocher, fermer Flow. Le fait qu'on soit arrivé jusqu'à ce blocage est en soi une information : avant le réveil du manager lazy, on n'atteignait même pas la phase de connexion. 82 tests au vert sur tests-linux. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -248,7 +248,16 @@ final class PolarPsFtpWriter {
|
||||
continue
|
||||
}
|
||||
do {
|
||||
try await client.waitPsFtpReady(true)
|
||||
// ⚠️ `waitPsFtpReady` n'a AUCUNE limite de temps. Si la
|
||||
// montre ne finit pas la négociation — canal déjà pris,
|
||||
// écran éteint, appairage en cours — l'attente ne rend
|
||||
// jamais la main et l'envoi paraît figé, sans message.
|
||||
// Constaté le 2026-08-31 : « envoi » tournant sans fin.
|
||||
// On lui donne donc une échéance, et un dépassement compte
|
||||
// comme une montre muette : c'est ce qu'il est.
|
||||
try await Self.avecEcheance(seconds: 12) {
|
||||
try await client.waitPsFtpReady(true)
|
||||
}
|
||||
return (client, session)
|
||||
} catch {
|
||||
muettes += 1
|
||||
@@ -326,12 +335,38 @@ final class PolarPsFtpWriter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Court `operation` avec une échéance, et lève si elle est dépassée.
|
||||
///
|
||||
/// Le SDK Polar rend des `async` sans limite de temps : une montre qui ne
|
||||
/// répond pas fige l'appel indéfiniment. Un envoi qui n'aboutit pas doit
|
||||
/// dire pourquoi, pas tourner en silence.
|
||||
static func avecEcheance<T: Sendable>(seconds: Double,
|
||||
_ operation: @escaping @Sendable () async throws -> T)
|
||||
async throws -> T {
|
||||
try await withThrowingTaskGroup(of: T.self) { groupe in
|
||||
groupe.addTask { try await operation() }
|
||||
groupe.addTask {
|
||||
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
|
||||
throw PolarPftpError.echeanceDepassee(seconds)
|
||||
}
|
||||
guard let premier = try await groupe.next() else {
|
||||
throw PolarPftpError.echeanceDepassee(seconds)
|
||||
}
|
||||
groupe.cancelAll()
|
||||
return premier
|
||||
}
|
||||
}
|
||||
|
||||
private func put(_ client: BlePsFtpClient, _ etape: PftpStep) async throws {
|
||||
let entree = InputStream(data: etape.data)
|
||||
// Même raison que pour waitPsFtpReady : une écriture qui n'aboutit pas
|
||||
// doit échouer, pas figer l'app.
|
||||
// `write` rend un flux de progression ; on le consomme jusqu'au bout,
|
||||
// et l'absence d'erreur vaut acquittement — c'est l'équivalent BLE du
|
||||
// `05 00 00` observé en USB.
|
||||
for try await _ in client.write(etape.header() as NSData, data: entree) {}
|
||||
try await Self.avecEcheance(seconds: 20) {
|
||||
for try await _ in client.write(etape.header() as NSData, data: entree) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +109,8 @@ public enum PolarPftpError: Error, CustomStringConvertible, Equatable {
|
||||
/// Le SDK n'a remonté aucune session, mais CoreBluetooth, interrogé
|
||||
/// directement, a vu `vusParCoreBluetooth` appareils. Deux diagnostics
|
||||
/// opposés selon ce nombre.
|
||||
/// Une opération du SDK n'a pas rendu la main dans le délai imparti.
|
||||
case echeanceDepassee(Double)
|
||||
case sdkSilencieux(vusParCoreBluetooth: Int,
|
||||
publisherAParle: Bool = true,
|
||||
erreurSdk: String? = nil)
|
||||
@@ -125,6 +127,12 @@ public enum PolarPftpError: Error, CustomStringConvertible, Equatable {
|
||||
case .bluetoothUnusable(let why):
|
||||
return why
|
||||
|
||||
case .echeanceDepassee(let seconds):
|
||||
return "la montre n'a pas répondu en \(Int(seconds)) s. Réveiller son "
|
||||
+ "écran et la rapprocher de l'iPhone ; vérifier que Polar Flow "
|
||||
+ "est bien fermée."
|
||||
|
||||
|
||||
case .sdkSilencieux(_, false, _):
|
||||
// Le publisher n'a JAMAIS rien émis : `search()` s'arrête sur
|
||||
// `monitorBleState().filter { $0 == .poweredOn }`. Le scan n'a donc
|
||||
|
||||
@@ -234,3 +234,27 @@ extension PolarPftpErrorTests {
|
||||
XCTAssertTrue(e.description.contains("intégration du SDK"))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Un envoi qui n'aboutit pas doit le dire
|
||||
//
|
||||
// Les `async` du SDK Polar n'ont aucune limite de temps. Le 2026-08-31, un envoi
|
||||
// est resté figé sans message : `waitPsFtpReady` attendait une montre qui ne
|
||||
// finissait pas la négociation. Une attente sans fin est pire qu'une erreur —
|
||||
// elle ne dit rien et n'apprend rien.
|
||||
|
||||
extension PolarPftpErrorTests {
|
||||
|
||||
func testLEcheanceDitQuoiFaire() {
|
||||
let e = PolarPftpError.echeanceDepassee(12)
|
||||
XCTAssertTrue(e.description.contains("12"))
|
||||
XCTAssertTrue(e.description.contains("écran"))
|
||||
XCTAssertTrue(e.description.contains("Polar Flow"))
|
||||
}
|
||||
|
||||
func testLEcheanceNeSeConfondPasAvecUnScanVide() {
|
||||
let echeance = PolarPftpError.echeanceDepassee(12).description
|
||||
let vide = PolarPftpError.watchNotFound(30, vues: 0,
|
||||
sansPsFtp: 0, muettes: 0).description
|
||||
XCTAssertNotEqual(echeance, vide)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user