fix: adaptive inter-command delays to prevent BLE supervision timeouts
Root cause: DX-Smart CP28 beacons were disconnecting during provisioning because the 0.3s inter-command delay was too fast for the beacon's MCU. Frame selection (0x11-0x16) and type commands (0x61, 0x62) trigger internal state changes that need processing time. Rapid writes caused the beacon to miss BLE connection events, triggering link-layer supervision timeouts. Changes: - Base delay: 0.3s → 0.5s for all commands - Heavy delay: 1.0s after frame select/type commands (MCU state change) - Large payload delay: 0.8s after UUID writes (21 bytes) - Resume delay: 0.5s → 1.5s after reconnect (let BLE stack stabilize) - Non-fatal skip delay: 0.15s → 0.5s Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aeab67ea64
commit
bfbc2a5d8c
1 changed files with 50 additions and 5 deletions
|
|
@ -194,6 +194,15 @@ class BeaconProvisioner: NSObject, ObservableObject {
|
||||||
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
|
||||||
|
|
||||||
|
// Adaptive inter-command delays to prevent BLE supervision timeouts.
|
||||||
|
// DX-Smart CP28 beacons have tiny MCU buffers and share radio time between
|
||||||
|
// advertising and GATT — rapid writes cause the beacon to miss connection
|
||||||
|
// events, triggering link-layer supervision timeouts (the "unexpected disconnect"
|
||||||
|
// that was happening 4x during provisioning).
|
||||||
|
private static let BASE_WRITE_DELAY: Double = 0.5 // Default delay between commands (was 0.3)
|
||||||
|
private static let HEAVY_WRITE_DELAY: Double = 1.0 // After frame select/type commands (MCU state change)
|
||||||
|
private static let LARGE_PAYLOAD_DELAY: Double = 0.8 // After UUID/large payload writes
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
centralManager = CBCentralManager(delegate: self, queue: .main)
|
centralManager = CBCentralManager(delegate: self, queue: .main)
|
||||||
|
|
@ -603,7 +612,7 @@ class BeaconProvisioner: NSObject, ObservableObject {
|
||||||
// Non-fatal commands (first 6) — skip and continue
|
// Non-fatal commands (first 6) — skip and continue
|
||||||
DebugLog.shared.log("BLE: Write timeout for non-fatal command \(current)/\(total) — skipping")
|
DebugLog.shared.log("BLE: Write timeout for non-fatal command \(current)/\(total) — skipping")
|
||||||
self.dxSmartWriteIndex += 1
|
self.dxSmartWriteIndex += 1
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||||
self?.dxSmartSendNextCommand()
|
self?.dxSmartSendNextCommand()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -622,6 +631,39 @@ class BeaconProvisioner: NSObject, ObservableObject {
|
||||||
writeTimeoutTimer = nil
|
writeTimeoutTimer = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculate adaptive delay after writing a command.
|
||||||
|
/// Frame selection (0x11-0x16) and type commands (0x61, 0x62) trigger internal
|
||||||
|
/// state changes on the beacon MCU that need extra processing time.
|
||||||
|
/// UUID writes are the largest payload (21 bytes) and also need breathing room.
|
||||||
|
/// Without adaptive delays, the beacon's radio gets overwhelmed and drops the
|
||||||
|
/// BLE connection (supervision timeout).
|
||||||
|
private func delayForCommand(at index: Int) -> Double {
|
||||||
|
guard index < dxSmartCommandQueue.count else { return BeaconProvisioner.BASE_WRITE_DELAY }
|
||||||
|
|
||||||
|
let packet = dxSmartCommandQueue[index]
|
||||||
|
guard packet.count >= 3 else { return BeaconProvisioner.BASE_WRITE_DELAY }
|
||||||
|
|
||||||
|
let cmd = packet[2] // Command byte is at offset 2 (after 4E 4F header)
|
||||||
|
|
||||||
|
switch DXCmd(rawValue: cmd) {
|
||||||
|
case .frameSelectSlot0, .frameSelectSlot1, .frameSelectSlot2,
|
||||||
|
.frameSelectSlot3, .frameSelectSlot4, .frameSelectSlot5:
|
||||||
|
// Frame selection changes internal state — beacon needs time to switch context
|
||||||
|
return BeaconProvisioner.HEAVY_WRITE_DELAY
|
||||||
|
case .deviceInfoType, .iBeaconType:
|
||||||
|
// Frame type assignment — triggers internal config restructuring
|
||||||
|
return BeaconProvisioner.HEAVY_WRITE_DELAY
|
||||||
|
case .uuid:
|
||||||
|
// Largest payload (16 bytes + header = 21 bytes) — give extra time
|
||||||
|
return BeaconProvisioner.LARGE_PAYLOAD_DELAY
|
||||||
|
case .saveConfig:
|
||||||
|
// Save to flash — beacon may reboot, no point waiting long
|
||||||
|
return BeaconProvisioner.BASE_WRITE_DELAY
|
||||||
|
default:
|
||||||
|
return BeaconProvisioner.BASE_WRITE_DELAY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - DX-Smart Packet Builder
|
// MARK: - DX-Smart Packet Builder
|
||||||
|
|
||||||
/// Build a DX-Smart CP28 packet: [4E][4F][CMD][LEN][DATA...][XOR_CHECKSUM]
|
/// Build a DX-Smart CP28 packet: [4E][4F][CMD][LEN][DATA...][XOR_CHECKSUM]
|
||||||
|
|
@ -1298,7 +1340,7 @@ extension BeaconProvisioner: CBPeripheralDelegate {
|
||||||
} else if isNonFatalCommand {
|
} else if isNonFatalCommand {
|
||||||
DebugLog.shared.log("BLE: Non-fatal command failed at step \(dxSmartWriteIndex + 1), continuing...")
|
DebugLog.shared.log("BLE: Non-fatal command failed at step \(dxSmartWriteIndex + 1), continuing...")
|
||||||
dxSmartWriteIndex += 1
|
dxSmartWriteIndex += 1
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||||
self?.dxSmartSendNextCommand()
|
self?.dxSmartSendNextCommand()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1330,8 +1372,9 @@ extension BeaconProvisioner: CBPeripheralDelegate {
|
||||||
state = .writing
|
state = .writing
|
||||||
DebugLog.shared.log("BLE: Resuming write from command \(dxSmartWriteIndex + 1)/\(dxSmartCommandQueue.count) after reconnect")
|
DebugLog.shared.log("BLE: Resuming write from command \(dxSmartWriteIndex + 1)/\(dxSmartCommandQueue.count) after reconnect")
|
||||||
progress = "Resuming config write..."
|
progress = "Resuming config write..."
|
||||||
// Small delay to let the BLE stack settle before resuming writes
|
// Longer delay after reconnect — give the beacon's BLE stack time to stabilize
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
// before resuming writes (prevents immediate re-disconnect)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
|
||||||
self?.dxSmartSendNextCommand()
|
self?.dxSmartSendNextCommand()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1352,8 +1395,10 @@ extension BeaconProvisioner: CBPeripheralDelegate {
|
||||||
// Device info query was sent - wait for response on FFE1, don't process as normal command
|
// Device info query was sent - wait for response on FFE1, don't process as normal command
|
||||||
DebugLog.shared.log("BLE: Device info query sent, waiting for response...")
|
DebugLog.shared.log("BLE: Device info query sent, waiting for response...")
|
||||||
} else {
|
} else {
|
||||||
|
let justWritten = dxSmartWriteIndex
|
||||||
dxSmartWriteIndex += 1
|
dxSmartWriteIndex += 1
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
let delay = delayForCommand(at: justWritten)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||||
self?.dxSmartSendNextCommand()
|
self?.dxSmartSendNextCommand()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue