Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 113 additions & 32 deletions Demo/ReliaBLE Demo/ReliaBLE Demo/Central/CentralView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,47 @@ extension ConnectionState {
var description: String {
switch self {
case .connecting: "Connecting"
case .reconnecting(let source, let attempt, _):
switch source {
case .system: "System reconnecting…"
case .library: "Reconnecting (attempt \(attempt ?? 0))"
}
case .connected: "Connected"
case .disconnecting: "Disconnecting"
case .disconnected(let reason): "Disconnected\(reason.map { " (\($0))" } ?? "")"
case .failed(let reason): "Failed\(reason.map { " (\($0))" } ?? "")"
}
}

var nextRetryAt: Date? {
if case .reconnecting(_, _, let date) = self { return date }
return nil
}

var color: Color {
switch self {
case .connected: .green
case .reconnecting: .yellow
case .disconnected(let reason): reason == nil ? .secondary : .orange
case .failed: .red
default: .orange
}
}

/// True when a connection is active or in progress (including reconnect).
var isActiveConnection: Bool {
switch self {
case .connecting, .connected, .disconnecting, .reconnecting:
true
case .disconnected, .failed:
false
}
}

/// Auto-reconnect can only be changed when initiating a fresh connect.
var canEditAutoReconnect: Bool {
!isActiveConnection
}
}

struct CentralView: View {
Expand Down Expand Up @@ -140,37 +175,11 @@ struct CentralView: View {
List {
ForEach(devices, id: \.persistentModelID) { device in
NavigationLink {
Group {
let currentState = viewModel.connectionStates[device.id]
VStack(spacing: 16) {
Text("ID: \(device.id)")
Text("Last seen: \(device.lastSeen?.formatted(date: .numeric, time: .standard) ?? "")")

if let state = currentState {
Text("Connection: \(state.description)")
.foregroundStyle(state == .connected ? Color.green : Color.orange)
} else {
Text("Connection: unknown")
.foregroundStyle(.secondary)
}

Button(action: {
if let state = currentState, state == .connected {
Task { try? await reliaBLE.disconnect(from: Peripheral(id: device.id)) }
} else {
Task { try? await reliaBLE.connect(to: Peripheral(id: device.id)) }
}
}) {
if let state = currentState, state == .connected {
Text("Disconnect")
} else {
Text("Connect")
}
}
.buttonStyle(.bordered)
}
.padding()
}
DeviceDetailView(
device: device,
viewModel: viewModel,
reliaBLE: reliaBLE
)
} label: {
Text("\(device.name ?? "Unknown")")
}
Expand Down Expand Up @@ -201,10 +210,82 @@ struct CentralView: View {
}
}

private struct DeviceDetailView: View {
let device: Device
let viewModel: CentralViewModel
let reliaBLE: ReliaBLEManager

@State private var autoReconnect = true

private var connectionState: ConnectionState? {
viewModel.connectionStates[device.id]
}

private var isActive: Bool {
connectionState?.isActiveConnection == true
}

var body: some View {
VStack(spacing: 16) {
Text("ID: \(device.id)")
Text("Last seen: \(device.lastSeen?.formatted(date: .numeric, time: .standard) ?? "")")

if let state = connectionState {
Text("Connection: \(state.description)")
.foregroundStyle(state.color)

if let retryAt = state.nextRetryAt {
CountdownView(targetDate: retryAt)
}
} else {
Text("Connection: unknown")
.foregroundStyle(.secondary)
}

Button(action: {
if isActive {
Task { try? await reliaBLE.disconnect(from: Peripheral(id: device.id)) }
} else {
Task {
try? await reliaBLE.connect(
to: Peripheral(id: device.id),
autoReconnect: autoReconnect
)
}
}
}) {
Text(isActive ? "Disconnect" : "Connect")
}
.buttonStyle(.bordered)

Toggle("Auto Reconnect:", isOn: $autoReconnect)
.disabled(connectionState.map { !$0.canEditAutoReconnect } ?? false)
}
.padding()
}
}

private struct CountdownView: View {
let targetDate: Date

@State private var now = Date()

var body: some View {
let remaining = max(0, targetDate.timeIntervalSince(now))
Text("Next retry in: \(Int(remaining))s")
.font(.caption)
.foregroundStyle(.secondary)
.monospacedDigit()
.onReceive(Timer.publish(every: 1, on: .main, in: .common).autoconnect()) { _ in
now = Date()
}
}
}

#Preview {
CentralView()
.modelContainer(
for: [Device.self, DiscoveryEvent.self],
inMemory: true
)
}
}
8 changes: 8 additions & 0 deletions Demo/ReliaBLE Demo/ReliaBLE Demo/ReliaBLE_DemoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ struct ReliaBLE_DemoApp: App {
config.logQueue = DispatchQueue(label: "com.five3apps.relia-ble-demo.logging", qos: .utility)
config.loggingEnabled = true

let defaults = UserDefaults.standard
var reconnectPolicy = ReconnectPolicy()
reconnectPolicy.maxAttempts = defaults.object(forKey: "reconnectPolicy.maxAttempts") as? Int ?? 5
reconnectPolicy.initialDelay = defaults.object(forKey: "reconnectPolicy.initialDelay") as? Double ?? 1.0
reconnectPolicy.maxDelay = defaults.object(forKey: "reconnectPolicy.maxDelay") as? Double ?? 30.0
reconnectPolicy.jitter = defaults.object(forKey: "reconnectPolicy.jitter") as? Double ?? 0.2
config.reconnectPolicy = reconnectPolicy

return ReliaBLEManager(config: config)
}()

Expand Down
34 changes: 33 additions & 1 deletion Demo/ReliaBLE Demo/ReliaBLE Demo/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,42 @@ struct SettingsView: View {
@Environment(\.bleManager) private var reliaBLE
@State private var isLoggingEnabled: Bool = false

@AppStorage("reconnectPolicy.maxAttempts") private var maxAttempts = 5
@AppStorage("reconnectPolicy.initialDelay") private var initialDelay = 1.0
@AppStorage("reconnectPolicy.maxDelay") private var maxDelay = 30.0
@AppStorage("reconnectPolicy.jitter") private var jitter = 0.2

var body: some View {
NavigationView {
Form {
Toggle("Enable Logging", isOn: $isLoggingEnabled)
Section("Logging") {
Toggle("Enable Logging", isOn: $isLoggingEnabled)
}

Section {
Stepper("Max Attempts: \(maxAttempts)", value: $maxAttempts, in: 1...20)

VStack(alignment: .leading, spacing: 4) {
Text("Initial Delay: \(String(format: "%.1f", initialDelay))s")
Slider(value: $initialDelay, in: 0.5...10, step: 0.5)
}

VStack(alignment: .leading, spacing: 4) {
Text("Max Delay: \(String(format: "%.0f", maxDelay))s")
Slider(value: $maxDelay, in: 5...120, step: 5)
}

VStack(alignment: .leading, spacing: 4) {
Text("Jitter: \(String(format: "%.2f", jitter))")
Slider(value: $jitter, in: 0...0.5, step: 0.05)
}

Text("Changes take effect on next app launch.")
.font(.caption)
.foregroundStyle(.secondary)
} header: {
Text("Reconnect Policy")
}
}
.navigationTitle("Settings")
}
Expand Down
Loading
Loading