fix: retry characteristic discovery when FFE2 missing after disconnect

After 2+ disconnects during provisioning, the beacon's BLE stack can
return incomplete characteristic discovery results. Instead of hard-
failing with "DX-Smart command characteristic (FFE2) not found", we
now re-trigger characteristic discovery (up to 2 attempts) and resume
writing from the saved position once FFE2 is found.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Schwifty 2026-03-22 03:03:08 +00:00
parent d44ca47f36
commit 9319cad2d6

View file

@ -190,6 +190,8 @@ class BeaconProvisioner: NSObject, ObservableObject {
// If a write callback doesn't come back in time, we retry or fail gracefully // If a write callback doesn't come back in time, we retry or fail gracefully
// instead of hanging until the 30s global timeout // instead of hanging until the 30s global timeout
private var writeTimeoutTimer: DispatchWorkItem? private var writeTimeoutTimer: DispatchWorkItem?
private var charRediscoveryCount = 0
private static let MAX_CHAR_REDISCOVERY = 2
private static let WRITE_TIMEOUT_SECONDS: Double = 5.0 private static let WRITE_TIMEOUT_SECONDS: Double = 5.0
private var writeRetryCount = 0 private var writeRetryCount = 0
private static let MAX_WRITE_RETRIES = 1 // Retry once per command before failing private static let MAX_WRITE_RETRIES = 1 // Retry once per command before failing
@ -336,6 +338,7 @@ class BeaconProvisioner: NSObject, ObservableObject {
deviceInfoRetryCount = 0 deviceInfoRetryCount = 0
disconnectRetryCount = 0 disconnectRetryCount = 0
writeRetryCount = 0 writeRetryCount = 0
charRediscoveryCount = 0
currentBeacon = nil currentBeacon = nil
state = .idle state = .idle
progress = "" progress = ""
@ -571,10 +574,29 @@ class BeaconProvisioner: NSObject, ObservableObject {
progress = "Writing config (\(current)/\(total))..." progress = "Writing config (\(current)/\(total))..."
guard let commandChar = characteristics[BeaconProvisioner.DXSMART_COMMAND_CHAR] else { guard let commandChar = characteristics[BeaconProvisioner.DXSMART_COMMAND_CHAR] else {
fail("DX-Smart command characteristic (FFE2) not found", code: .serviceNotFound) // After disconnect+reconnect, characteristic discovery may return incomplete results.
// Re-discover characteristics instead of hard-failing.
if charRediscoveryCount < BeaconProvisioner.MAX_CHAR_REDISCOVERY, let service = configService {
charRediscoveryCount += 1
DebugLog.shared.log("BLE: FFE2 missing — re-discovering characteristics (attempt \(charRediscoveryCount)/\(BeaconProvisioner.MAX_CHAR_REDISCOVERY))")
progress = "Re-discovering characteristics..."
state = .discoveringServices
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
self?.peripheral?.discoverCharacteristics([
BeaconProvisioner.DXSMART_NOTIFY_CHAR,
BeaconProvisioner.DXSMART_COMMAND_CHAR,
BeaconProvisioner.DXSMART_PASSWORD_CHAR
], for: service)
}
} else {
fail("DX-Smart command characteristic (FFE2) not found after \(charRediscoveryCount) rediscovery attempts", code: .serviceNotFound)
}
return return
} }
// Reset rediscovery counter on successful characteristic access
charRediscoveryCount = 0
DebugLog.shared.log("BLE: Writing command \(current)/\(total): \(packet.map { String(format: "%02X", $0) }.joined(separator: " "))") DebugLog.shared.log("BLE: Writing command \(current)/\(total): \(packet.map { String(format: "%02X", $0) }.joined(separator: " "))")
writeRetryCount = 0 writeRetryCount = 0
scheduleWriteTimeout() scheduleWriteTimeout()
@ -1294,6 +1316,13 @@ extension BeaconProvisioner: CBPeripheralDelegate {
if operationMode == .readingConfig { if operationMode == .readingConfig {
// Continue exploring next service // Continue exploring next service
exploreNextService() exploreNextService()
} else if charRediscoveryCount > 0 && dxSmartAuthenticated && !dxSmartCommandQueue.isEmpty {
// Rediscovery during active write resume writing directly (already authenticated)
DebugLog.shared.log("BLE: Characteristics re-discovered after FFE2 miss — resuming write")
state = .writing
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.dxSmartSendNextCommand()
}
} else { } else {
// Provisioning: DX-Smart auth flow // Provisioning: DX-Smart auth flow
dxSmartStartAuth() dxSmartStartAuth()