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
|
continue
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
|
// ⚠️ `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)
|
try await client.waitPsFtpReady(true)
|
||||||
|
}
|
||||||
return (client, session)
|
return (client, session)
|
||||||
} catch {
|
} catch {
|
||||||
muettes += 1
|
muettes += 1
|
||||||
@@ -326,14 +335,40 @@ 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 {
|
private func put(_ client: BlePsFtpClient, _ etape: PftpStep) async throws {
|
||||||
let entree = InputStream(data: etape.data)
|
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,
|
// `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
|
// et l'absence d'erreur vaut acquittement — c'est l'équivalent BLE du
|
||||||
// `05 00 00` observé en USB.
|
// `05 00 00` observé en USB.
|
||||||
|
try await Self.avecEcheance(seconds: 20) {
|
||||||
for try await _ in client.write(etape.header() as NSData, data: entree) {}
|
for try await _ in client.write(etape.header() as NSData, data: entree) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,8 @@ public enum PolarPftpError: Error, CustomStringConvertible, Equatable {
|
|||||||
/// Le SDK n'a remonté aucune session, mais CoreBluetooth, interrogé
|
/// Le SDK n'a remonté aucune session, mais CoreBluetooth, interrogé
|
||||||
/// directement, a vu `vusParCoreBluetooth` appareils. Deux diagnostics
|
/// directement, a vu `vusParCoreBluetooth` appareils. Deux diagnostics
|
||||||
/// opposés selon ce nombre.
|
/// 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,
|
case sdkSilencieux(vusParCoreBluetooth: Int,
|
||||||
publisherAParle: Bool = true,
|
publisherAParle: Bool = true,
|
||||||
erreurSdk: String? = nil)
|
erreurSdk: String? = nil)
|
||||||
@@ -125,6 +127,12 @@ public enum PolarPftpError: Error, CustomStringConvertible, Equatable {
|
|||||||
case .bluetoothUnusable(let why):
|
case .bluetoothUnusable(let why):
|
||||||
return 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, _):
|
case .sdkSilencieux(_, false, _):
|
||||||
// Le publisher n'a JAMAIS rien émis : `search()` s'arrête sur
|
// Le publisher n'a JAMAIS rien émis : `search()` s'arrête sur
|
||||||
// `monitorBleState().filter { $0 == .poweredOn }`. Le scan n'a donc
|
// `monitorBleState().filter { $0 == .poweredOn }`. Le scan n'a donc
|
||||||
|
|||||||
@@ -234,3 +234,27 @@ extension PolarPftpErrorTests {
|
|||||||
XCTAssertTrue(e.description.contains("intégration du SDK"))
|
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