-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathKlipschControlApp.swift
More file actions
281 lines (232 loc) · 9.86 KB
/
Copy pathKlipschControlApp.swift
File metadata and controls
281 lines (232 loc) · 9.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
//
// KlipschControlApp.swift
// KlipschControl
//
// Created by William Leese on 17/02/2024.
//
import SwiftUI
import Foundation
import CoreBluetooth
import os.log
let logger = Logger(subsystem: "KlipschControl", category: "Speaker")
let DEVICE_NAME = "Klipsch The Three Plus"
class Speaker: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, ObservableObject {
let VOLUME_UUID = "DA6D0FA2-0D18-442C-BABE-F85B5BAA6F11"
let POWER_UUID = "DA6D0FE7-0D18-442C-BABE-F85B5BAA6F11"
let INPUT_UUID = "DA6D0FD2-0D18-442C-BABE-F85B5BAA6F11"
// Publish so our view is updated
@Published var bluetoothReady = false
@Published var deviceReady = false
@Published var powerOn = false
@Published var volume = Data([0x01])
@Published var activeInput = Data([0x01])
@Published var statusText = "Disconnected"
var UUIDS: [String] = []
// Core Bluetooth properties
var centralManager: CBCentralManager!
var connectedPeripheral: CBPeripheral?
var characteristics: [String: CBCharacteristic] = [:]
var descriptors: [String: [CBDescriptor]] = [:]
override init() {
super.init()
UUIDS = [VOLUME_UUID, POWER_UUID, INPUT_UUID]
centralManager = CBCentralManager(delegate: self, queue: DispatchQueue.main, options: [CBCentralManagerOptionRestoreIdentifierKey: DEVICE_NAME])
}
func triggerScan() {
self.statusText = "Looking for speaker"
centralManager.scanForPeripherals(withServices: nil, options: nil)
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOn:
bluetoothReady = true
self.statusText = "Bluetooth is ready"
centralManager.scanForPeripherals(withServices: nil, options: nil)
default:
deviceReady = false
powerOn = false
bluetoothReady = false
self.statusText = "Bluetooth not ready"
break
}
}
// Restore the connection to the peripherals
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String: Any]) {
self.statusText = "Restoring state"
if bluetoothReady {
if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] {
for peripheral in peripherals {
centralManager.connect(peripheral, options: nil)
}
}
} else {
self.statusText = "Bluetooth not ready for restore"
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
if peripheral.name == DEVICE_NAME {
self.statusText = "Found our speaker"
connectedPeripheral = peripheral
connectedPeripheral!.delegate = self
// Request a connection to the peripheral
centralManager.connect(connectedPeripheral!, options: nil)
// Stop scanning for peripherals
centralManager.stopScan()
}
}
// callback connect
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
connectedPeripheral?.discoverServices(nil)
self.statusText = "Connected to speaker"
}
// callback service
func peripheral( _ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
self.statusText = "Discovering services"
guard let services = peripheral.services, error == nil else {
self.statusText = "An error occurred discovering services"
return
}
for service in services {
self.statusText = "Found service \(peripheral.name as String?)"
peripheral.discoverCharacteristics(nil, for: service)
}
}
// callback found characteristic
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
self.statusText = "Discovered characteristic"
if let error = error {
self.statusText = "An error occurred discovering characteristics: " + error.localizedDescription
}
service.characteristics?.forEach({ characteristic in
if UUIDS.contains(characteristic.uuid.uuidString) {
characteristics[characteristic.uuid.uuidString] = characteristic
peripheral.discoverDescriptors(for: characteristic)
// read the volume value
if characteristic.uuid.uuidString == VOLUME_UUID {
peripheral.readValue(for: characteristic)
}
// read the input value
if characteristic.uuid.uuidString == INPUT_UUID {
peripheral.readValue(for: characteristic)
}
// read the power value
if characteristic.uuid.uuidString == POWER_UUID {
peripheral.readValue(for: characteristic)
}
}
})
}
// callback discovery of characteristic descriptors
func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) {
if UUIDS.contains(characteristic.uuid.uuidString) {
descriptors[characteristic.uuid.uuidString] = characteristic.descriptors
self.statusText = "" // We're good, no need for status text
deviceReady = true
}
}
// callback update characteristic
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
}
// callback characteristic update value
// using the read value also is done here
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
if let e = error {
self.statusText = "Error didUpdateValue \(e.localizedDescription)"
return
}
if characteristic.uuid.uuidString == VOLUME_UUID {
guard let data = characteristic.value else { return }
volume = data
deviceReady = true
}
if characteristic.uuid.uuidString == INPUT_UUID {
guard let data = characteristic.value else { return }
activeInput = data
}
if characteristic.uuid.uuidString == POWER_UUID {
guard let data = characteristic.value else { return }
if data == Data([0x01]) {
powerOn = true
} else {
connectedPeripheral?.setNotifyValue(true, for: characteristic)
connectedPeripheral?.writeValue(Data([0x01]), for: characteristic, type: .withResponse)
}
}
}
// handle fail to connects
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
if let error = error {
self.statusText = "Failed to connect: \(error.localizedDescription)"
}
}
// handle disconnects
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
// no reason to yet..
}
func disconnect() {
if let connectedPeripheral {
centralManager.cancelPeripheralConnection(connectedPeripheral)
}
}
func switchInput(data: Data) {
ensureOn()
self.connectedPeripheral?.setNotifyValue(true, for: self.characteristics[INPUT_UUID]!)
self.connectedPeripheral?.writeValue(data, for: self.characteristics[INPUT_UUID]!, type: .withResponse)
}
func volumeUp() {
let integerValue = self.volume.withUnsafeBytes { $0.load(as: UInt8.self) }
volume(data: Data([integerValue + 1]))
}
func volumeDown() {
let integerValue = self.volume.withUnsafeBytes { $0.load(as: UInt8.self) }
volume(data: Data([integerValue - 1]))
}
func volume(data: Data) {
ensureOn()
self.connectedPeripheral?.setNotifyValue(true, for: self.characteristics[VOLUME_UUID]!)
self.connectedPeripheral?.writeValue(data, for: self.characteristics[VOLUME_UUID]!, type: .withResponse)
}
func ensureOn() {
if !powerOn {
self.connectedPeripheral?.setNotifyValue(true, for: self.characteristics[POWER_UUID]!)
self.connectedPeripheral?.writeValue(Data([0x01]), for: self.characteristics[POWER_UUID]!, type: .withResponse)
}
}
}
@main
struct KlipschControlApp: App {
@Environment(\.scenePhase) var scenePhase
var speaker = Speaker()
var body: some Scene {
WindowGroup {
ContentView(speaker: speaker)
}
.onChange(of: scenePhase) {
if scenePhase == .active {
speaker.triggerScan()
}
if scenePhase == .background {
startBackgroundTask()
}
}
}
// We need to use the annotation here to have a mutatable value in our struct
@State var backgroundTaskId: UIBackgroundTaskIdentifier = .invalid
func startBackgroundTask() {
backgroundTaskId = UIApplication.shared.beginBackgroundTask { [self] in
self.endBackgroundTask()
}
DispatchQueue.global(qos: .background).async { [self] in
Thread.sleep(forTimeInterval: 20)
// Final check
if UIApplication.shared.applicationState == .background {
speaker.disconnect()
}
self.endBackgroundTask()
}
}
func endBackgroundTask() {
UIApplication.shared.endBackgroundTask(backgroundTaskId)
backgroundTaskId = .invalid
}
}