« errorcode 104 » n'était pas un refus : mkdir sur un dossier déjà là
Sylvain a rapporté un errorcode 104 en testant l'envoi. Le code vient du protocole PFTP de Polar, et pftp_error.proto du SDK officiel le nomme : 104 = DIRECTORY_EXISTS. Ce n'est donc pas un refus de la montre — la session BLE s'était parfaitement ouverte, le transport fonctionnait, et c'est notre séquence de mkdir qui n'était pas idempotente. L'hypothèse fautive était écrite noir sur blanc dans parseSteps : « aucun des deux n'existe d'avance pour une date neuve ». Vrai d'une date neuve, faux dès le second envoi vers la même date — exactement ce que Sylvain venait de faire après avoir réussi un premier envoi. Un mkdir qui bute sur 104 est désormais considéré comme satisfait, à la manière d'un mkdir -p : le dossier est là, c'est tout ce qu'on lui demandait. Strictement limité à la création de dossier — un put de fichier n'est jamais avalé, 105 FILE_EXISTS signifierait que l'objectif est déjà écrit et l'appelant doit le savoir. Le renvoi n'est pas passé sous silence pour autant : ecrire() rend un drapeau dossierPreexistant, le plugin le résout en alreadyExisted, et l'UI de coach affiche « Un objectif existait déjà à cette date. Redémarrer la montre pour que le nouveau s'affiche. » C'est la contrepartie de l'index en cache constaté le 31/08 — sans cet avertissement, l'envoi annoncerait un succès que la montre ne montrerait pas. Les codes PFTP sont définis dans PolarPftpStep.swift, donc couverts par les tests Linux : 87 tests verts, dont 3 nouveaux qui vérifient que 104 est satisfaisant pour un mkdir et que 103, 105, 106 et 108 restent des échecs. ⚠️ CoachPolarBLE.swift n'est pas compilable ici — Capacitor et le SDK Polar n'existent pas sur Linux. La modification du plugin est à vérifier au prochain build Xcode ; la logique des codes, elle, est testée. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -125,8 +125,13 @@ public class CoachPolarBLEPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
#if canImport(PolarBleSdk)
|
||||
Task {
|
||||
do {
|
||||
let journal = try await PolarPsFtpWriter().ecrire(etapes, timeout: timeout)
|
||||
call.resolve(["sent": true, "dryRun": false, "log": journal])
|
||||
let issue = try await PolarPsFtpWriter().ecrire(etapes, timeout: timeout)
|
||||
call.resolve(["sent": true, "dryRun": false,
|
||||
"log": issue.journal,
|
||||
// L'UI s'en sert pour prévenir d'un renvoi : la
|
||||
// montre garde son index en cache, et l'objectif
|
||||
// réécrit n'apparaît qu'après un redémarrage.
|
||||
"alreadyExisted": issue.dossierPreexistant])
|
||||
} catch {
|
||||
call.reject("\(error)")
|
||||
}
|
||||
@@ -182,7 +187,12 @@ final class PolarPsFtpWriter {
|
||||
private var publisherAParle = false
|
||||
private var derniereErreurSdk: String?
|
||||
|
||||
func ecrire(_ etapes: [PftpStep], timeout: Double) async throws -> [String] {
|
||||
/// Écrit les étapes et rend le journal, plus un drapeau : le dossier de
|
||||
/// destination existait-il déjà ? Ce drapeau remonte jusqu'à l'UI, qui doit
|
||||
/// avertir — un objectif réécrit n'apparaît qu'après redémarrage de la
|
||||
/// montre.
|
||||
func ecrire(_ etapes: [PftpStep], timeout: Double) async throws
|
||||
-> (journal: [String], dossierPreexistant: Bool) {
|
||||
// ⚠️ PRÉAMBULE INDISPENSABLE — sans lui, rien ne se passe et rien ne le
|
||||
// dit. `CBDeviceListenerImpl.search()` commence par
|
||||
// `monitorBleState().filter { $0 == .poweredOn }` : tant que cet état
|
||||
@@ -270,15 +280,34 @@ final class PolarPsFtpWriter {
|
||||
defer { listener.closeSessionDirect(session) }
|
||||
|
||||
var journal: [String] = []
|
||||
var dossierPreexistant = false
|
||||
for etape in etapes {
|
||||
// Revalidé juste avant l'émission : entre la validation d'entrée et
|
||||
// ce point, rien ne doit avoir introduit un chemin de dossier avec
|
||||
// du contenu.
|
||||
try etape.validate()
|
||||
try await put(client, etape)
|
||||
journal.append("✔ " + etape.describe)
|
||||
do {
|
||||
try await put(client, etape)
|
||||
journal.append("✔ " + etape.describe)
|
||||
} catch BlePsFtpException.responseError(let code)
|
||||
where etape.isDirectory && PftpCode.mkdirEstSatisfait(par: code) {
|
||||
// `mkdir -p` : le dossier est là, c'est tout ce qu'on demandait.
|
||||
//
|
||||
// ⚠️ Rapporté par Sylvain le 01/09/2026 sous la forme
|
||||
// « errorcode 104 » — ce qui donnait à croire à un refus de la
|
||||
// montre alors que la session BLE s'était parfaitement ouverte.
|
||||
// Le commentaire de `parseSteps` disait « aucun des deux
|
||||
// n'existe d'avance pour une date neuve » : c'est vrai d'une
|
||||
// date NEUVE, et faux dès le second envoi vers la même date.
|
||||
//
|
||||
// Le renvoi est signalé plus haut, jamais avalé en silence :
|
||||
// réécrire un objectif déjà présent n'est visible qu'après un
|
||||
// redémarrage de la montre (index en cache, constaté le 31/08).
|
||||
dossierPreexistant = true
|
||||
journal.append("• " + etape.describe + " — existait déjà")
|
||||
}
|
||||
}
|
||||
return journal
|
||||
return (journal, dossierPreexistant)
|
||||
}
|
||||
|
||||
private func trouverClient(_ listener: CBDeviceListenerImpl,
|
||||
|
||||
@@ -25,6 +25,38 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Codes d'erreur du protocole PFTP, tels que Polar les publie.
|
||||
///
|
||||
/// Source : `pftp_error.proto` du SDK officiel — 100 `UNIDENTIFIED_HOST_ERROR`,
|
||||
/// 101 `INVALID_COMMAND`, 102 `INVALID_PARAMETER`, 103 `NO_SUCH_FILE_OR_DIRECTORY`,
|
||||
/// **104 `DIRECTORY_EXISTS`**, 105 `FILE_EXISTS`, 106 `OPERATION_NOT_PERMITTED`,
|
||||
/// 107 `NO_SUCH_USER`, 108 `TIMEOUT`.
|
||||
/// https://github.com/polarofficial/polar-ble-sdk/blob/master/sources/Android/android-communications/library/src/sdk/proto/pftp_error.proto
|
||||
///
|
||||
/// Le SDK iOS les remonte tels quels par
|
||||
/// `BlePsFtpException.responseError(errorCode: Int)`.
|
||||
public enum PftpCode {
|
||||
/// Le dossier visé par un `mkdir` existe déjà.
|
||||
///
|
||||
/// ⚠️ **Ce n'est pas un échec d'envoi.** Rencontré le 01/09/2026 sur un
|
||||
/// second envoi vers la même date : la session BLE s'était bien ouverte, et
|
||||
/// c'est notre séquence de `mkdir` qui n'était pas idempotente. Le message
|
||||
/// « errorcode 104 » donnait donc à croire à un refus de la montre alors
|
||||
/// que le transport fonctionnait.
|
||||
public static let directoryExists = 104
|
||||
|
||||
/// Codes sur lesquels un `mkdir` peut être considéré comme satisfait : le
|
||||
/// dossier est là, c'est tout ce qu'on lui demandait. L'équivalent de
|
||||
/// `mkdir -p`.
|
||||
///
|
||||
/// ⚠️ Strictement limité à la création de dossier. Un `put` de fichier ne
|
||||
/// doit JAMAIS être avalé de la sorte — 105 `FILE_EXISTS` signifierait que
|
||||
/// l'objectif est déjà écrit, ce que l'appelant doit savoir.
|
||||
public static func mkdirEstSatisfait(par code: Int) -> Bool {
|
||||
code == directoryExists
|
||||
}
|
||||
}
|
||||
|
||||
/// Un PUT PFTP : un chemin, un contenu. Un contenu vide crée un dossier.
|
||||
public struct PftpStep: Equatable {
|
||||
public let path: String
|
||||
|
||||
@@ -290,3 +290,32 @@ extension PolarPftpErrorTests {
|
||||
"l'hypothèse du lien exclusif doit être énoncée")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Codes d'erreur PFTP (01/09/2026)
|
||||
|
||||
/// « errorcode 104 » rapporté par Sylvain sur un second envoi vers la même
|
||||
/// date. Ce n'était pas un refus de la montre : la session BLE s'était ouverte,
|
||||
/// et c'est notre séquence de `mkdir` qui n'était pas idempotente.
|
||||
final class PftpCodeTests: XCTestCase {
|
||||
|
||||
func test104EstBienDirectoryExists() {
|
||||
XCTAssertEqual(PftpCode.directoryExists, 104)
|
||||
}
|
||||
|
||||
func testUnMkdirSurDossierExistantEstSatisfait() {
|
||||
XCTAssertTrue(PftpCode.mkdirEstSatisfait(par: 104))
|
||||
}
|
||||
|
||||
func testLesAutresCodesRestentDesEchecs() {
|
||||
// 103 NO_SUCH_FILE_OR_DIRECTORY : le parent manque, l'ordre des mkdir
|
||||
// est en cause — surtout pas à avaler.
|
||||
XCTAssertFalse(PftpCode.mkdirEstSatisfait(par: 103))
|
||||
// 105 FILE_EXISTS : l'objectif est déjà écrit. L'appelant DOIT le
|
||||
// savoir — le remplacer n'est visible qu'après redémarrage de la montre.
|
||||
XCTAssertFalse(PftpCode.mkdirEstSatisfait(par: 105))
|
||||
// 106 OPERATION_NOT_PERMITTED, 108 TIMEOUT : de vrais refus.
|
||||
XCTAssertFalse(PftpCode.mkdirEstSatisfait(par: 106))
|
||||
XCTAssertFalse(PftpCode.mkdirEstSatisfait(par: 108))
|
||||
XCTAssertFalse(PftpCode.mkdirEstSatisfait(par: 0))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user