fix: slim provisioning — skip extra frame disables + full reconnect on FFE2 miss
Two key changes: 1. Remove frames 3-6 disable commands (was steps 16-23, 8 extra BLE writes). We only configure Frame 1 (device info) and Frame 2 (iBeacon), then save. Fewer writes = fewer chances for supervision timeout disconnects. 2. When FFE2 characteristic goes missing after a disconnect, do a full disconnect → reconnect → re-discover services → re-auth → resume cycle instead of trying to rediscover characteristics on the same (stale GATT) connection. CoreBluetooth returns cached results on the same connection, so FFE2 stays missing. Full reconnect forces a fresh GATT discovery. Write sequence is now 16 steps (down from 24). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f0d2b2ae90
commit
d123d2561a
1 changed files with 49 additions and 32 deletions
|
|
@ -470,8 +470,8 @@ class BeaconProvisioner: NSObject, ObservableObject {
|
||||||
dxSmartWriteConfig()
|
dxSmartWriteConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the full command sequence and start writing
|
/// Build the command sequence and start writing
|
||||||
/// New 24-step write sequence for DX-Smart CP28:
|
/// Slim write sequence for DX-Smart CP28 (no extra frame overwrites):
|
||||||
/// 1. DeviceName 0x71 [name bytes] — service point name (max 20 ASCII chars)
|
/// 1. DeviceName 0x71 [name bytes] — service point name (max 20 ASCII chars)
|
||||||
/// 2. Frame1_Select 0x11 — select frame 1
|
/// 2. Frame1_Select 0x11 — select frame 1
|
||||||
/// 3. Frame1_Type 0x61 — enable as device info (broadcasts name)
|
/// 3. Frame1_Type 0x61 — enable as device info (broadcasts name)
|
||||||
|
|
@ -487,8 +487,11 @@ class BeaconProvisioner: NSObject, ObservableObject {
|
||||||
/// 13. AdvInterval 0x78 [advInterval]
|
/// 13. AdvInterval 0x78 [advInterval]
|
||||||
/// 14. TxPower 0x79 [txPower]
|
/// 14. TxPower 0x79 [txPower]
|
||||||
/// 15. TriggerOff 0xA0
|
/// 15. TriggerOff 0xA0
|
||||||
/// 16-23. Frames 3-6 select + 0xFF (disable each)
|
/// 16. SaveConfig 0x60 — persist to flash
|
||||||
/// 24. SaveConfig 0x60 — persist to flash
|
///
|
||||||
|
/// NOTE: Frames 3-6 are intentionally left untouched. Disabling extra frames
|
||||||
|
/// caused additional BLE writes that increased disconnect risk. We only write
|
||||||
|
/// the two frames we care about (device info + iBeacon) and save.
|
||||||
private func dxSmartWriteConfig() {
|
private func dxSmartWriteConfig() {
|
||||||
guard let config = config else {
|
guard let config = config else {
|
||||||
fail("No config provided", code: .noConfig)
|
fail("No config provided", code: .noConfig)
|
||||||
|
|
@ -553,21 +556,9 @@ class BeaconProvisioner: NSObject, ObservableObject {
|
||||||
// 15. TriggerOff (0xA0)
|
// 15. TriggerOff (0xA0)
|
||||||
dxSmartCommandQueue.append(buildDXPacket(cmd: .triggerOff, data: []))
|
dxSmartCommandQueue.append(buildDXPacket(cmd: .triggerOff, data: []))
|
||||||
|
|
||||||
// --- Frames 3-6: Disable each ---
|
// Frames 3-6 intentionally left untouched — fewer writes = fewer disconnects
|
||||||
// 16-17. Frame 3: select (0x13) + disable (0xFF)
|
|
||||||
dxSmartCommandQueue.append(buildDXPacket(cmd: .frameSelectSlot2, data: []))
|
|
||||||
dxSmartCommandQueue.append(buildDXPacket(cmd: .frameDisable, data: []))
|
|
||||||
// 18-19. Frame 4: select (0x14) + disable (0xFF)
|
|
||||||
dxSmartCommandQueue.append(buildDXPacket(cmd: .frameSelectSlot3, data: []))
|
|
||||||
dxSmartCommandQueue.append(buildDXPacket(cmd: .frameDisable, data: []))
|
|
||||||
// 20-21. Frame 5: select (0x15) + disable (0xFF)
|
|
||||||
dxSmartCommandQueue.append(buildDXPacket(cmd: .frameSelectSlot4, data: []))
|
|
||||||
dxSmartCommandQueue.append(buildDXPacket(cmd: .frameDisable, data: []))
|
|
||||||
// 22-23. Frame 6: select (0x16) + disable (0xFF)
|
|
||||||
dxSmartCommandQueue.append(buildDXPacket(cmd: .frameSelectSlot5, data: []))
|
|
||||||
dxSmartCommandQueue.append(buildDXPacket(cmd: .frameDisable, data: []))
|
|
||||||
|
|
||||||
// 24. SaveConfig (0x60) — persist to flash
|
// 16. SaveConfig (0x60) — persist to flash
|
||||||
dxSmartCommandQueue.append(buildDXPacket(cmd: .saveConfig, data: []))
|
dxSmartCommandQueue.append(buildDXPacket(cmd: .saveConfig, data: []))
|
||||||
|
|
||||||
DebugLog.shared.log("BLE: DX-Smart command queue built with \(dxSmartCommandQueue.count) commands")
|
DebugLog.shared.log("BLE: DX-Smart command queue built with \(dxSmartCommandQueue.count) commands")
|
||||||
|
|
@ -590,23 +581,43 @@ 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 {
|
||||||
// After disconnect+reconnect, characteristic discovery may return incomplete results.
|
// FFE2 missing — the GATT cache is stale after a disconnect. Rediscovering
|
||||||
// Re-discover characteristics instead of hard-failing.
|
// characteristics on the same connection doesn't work because CoreBluetooth
|
||||||
if charRediscoveryCount < BeaconProvisioner.MAX_CHAR_REDISCOVERY, let service = configService {
|
// returns cached (stale) results. We need a full disconnect → reconnect →
|
||||||
|
// re-discover services → re-discover characteristics cycle.
|
||||||
|
if charRediscoveryCount < BeaconProvisioner.MAX_CHAR_REDISCOVERY {
|
||||||
charRediscoveryCount += 1
|
charRediscoveryCount += 1
|
||||||
DebugLog.shared.log("BLE: FFE2 missing — re-discovering characteristics (attempt \(charRediscoveryCount)/\(BeaconProvisioner.MAX_CHAR_REDISCOVERY))")
|
DebugLog.shared.log("BLE: FFE2 missing — triggering full disconnect/reconnect (attempt \(charRediscoveryCount)/\(BeaconProvisioner.MAX_CHAR_REDISCOVERY))")
|
||||||
progress = "Re-discovering characteristics..."
|
progress = "FFE2 missing, reconnecting..."
|
||||||
state = .discoveringServices
|
|
||||||
scheduleCharRediscoveryTimeout()
|
// Use the existing disconnect retry path — it handles reconnect + re-auth + resume
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
resumeWriteAfterDisconnect = true
|
||||||
self?.peripheral?.discoverCharacteristics([
|
dxSmartAuthenticated = false
|
||||||
BeaconProvisioner.DXSMART_NOTIFY_CHAR,
|
dxSmartNotifySubscribed = false
|
||||||
BeaconProvisioner.DXSMART_COMMAND_CHAR,
|
passwordIndex = 0
|
||||||
BeaconProvisioner.DXSMART_PASSWORD_CHAR
|
characteristics.removeAll()
|
||||||
], for: service)
|
responseBuffer.removeAll()
|
||||||
|
awaitingCommandResponse = false
|
||||||
|
cancelResponseGateTimeout()
|
||||||
|
state = .connecting
|
||||||
|
|
||||||
|
// Disconnect — didDisconnectPeripheral won't re-enter because we set state = .connecting
|
||||||
|
// and handle reconnect manually here
|
||||||
|
if let peripheral = peripheral, peripheral.state == .connected {
|
||||||
|
centralManager.cancelPeripheralConnection(peripheral)
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
|
||||||
|
guard let self = self, let beacon = self.currentBeacon else { return }
|
||||||
|
guard self.state == .connecting else { return }
|
||||||
|
let resolvedPeripheral = self.resolvePeripheral(beacon)
|
||||||
|
self.peripheral = resolvedPeripheral
|
||||||
|
resolvedPeripheral.delegate = self
|
||||||
|
DebugLog.shared.log("BLE: Reconnecting after FFE2 miss...")
|
||||||
|
self.centralManager.connect(resolvedPeripheral, options: nil)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fail("DX-Smart command characteristic (FFE2) not found after \(charRediscoveryCount) rediscovery attempts", code: .serviceNotFound)
|
fail("DX-Smart command characteristic (FFE2) not found after \(charRediscoveryCount) reconnect attempts", code: .serviceNotFound)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -1227,6 +1238,12 @@ extension BeaconProvisioner: CBCentralManagerDelegate {
|
||||||
if state == .success || state == .idle {
|
if state == .success || state == .idle {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Intentional disconnect for FFE2 reconnect — we're already handling reconnect
|
||||||
|
if state == .connecting && resumeWriteAfterDisconnect {
|
||||||
|
DebugLog.shared.log("BLE: Intentional disconnect for FFE2 reconnect, ignoring")
|
||||||
|
return
|
||||||
|
}
|
||||||
if case .failed = state {
|
if case .failed = state {
|
||||||
DebugLog.shared.log("BLE: Disconnect after failure, ignoring")
|
DebugLog.shared.log("BLE: Disconnect after failure, ignoring")
|
||||||
return
|
return
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue