Showing 4 changed files with 74 additions and 67 deletions
+3 -1
USB Meter/Info.plist
@@ -38,7 +38,9 @@
38 38
 	<key>LSRequiresIPhoneOS</key>
39 39
 	<true/>
40 40
 	<key>NSBluetoothAlwaysUsageDescription</key>
41
-	<string>This app needs to use Bluetooth to connect with USB Meter</string>
41
+	<string>USB Meter uses Bluetooth to discover and connect to your meter devices.</string>
42
+	<key>NSBluetoothPeripheralUsageDescription</key>
43
+	<string>USB Meter uses Bluetooth to discover and connect to your meter devices.</string>
42 44
 	<key>UIApplicationSceneManifest</key>
43 45
 	<dict>
44 46
 		<key>UIApplicationSupportsMultipleScenes</key>
+4 -0
USB Meter/Model/AppData.swift
@@ -16,11 +16,15 @@ final class AppData : ObservableObject {
16 16
     let objectWillChange = ObservableObjectPublisher()
17 17
     private var userDefaultsNotification: AnyCancellable?
18 18
     private var icloudGefaultsNotification: AnyCancellable?
19
+    private var bluetoothManagerNotification: AnyCancellable?
19 20
     init() {
20 21
         userDefaultsNotification = NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification).sink { _ in
21 22
             self.objectWillChange.send()
22 23
         }
23 24
         icloudGefaultsNotification = NotificationCenter.default.publisher(for: NSUbiquitousKeyValueStore.didChangeExternallyNotification).sink(receiveValue: test)
25
+        bluetoothManagerNotification = bluetoothManager.objectWillChange.sink { [weak self] _ in
26
+            self?.objectWillChange.send()
27
+        }
24 28
         //NotificationCenter.default.publisher(for: NSUbiquitousKeyValueStore.didChangeExternallyNotification).sink(receiveValue: { notification in
25 29
         
26 30
     }
+64 -65
USB Meter/Model/BluetoothManager.swift
@@ -9,20 +9,27 @@
9 9
 import CoreBluetooth
10 10
 
11 11
 class BluetoothManager : NSObject, ObservableObject {
12
-    private var manager: CBCentralManager!
13
-    // MARK: MacOS split advertisementData generating multiple discoveries with partial data: https://stackoverflow.com/questions/41628114/cbperipheral-advertisementdata-is-different-when-discovering-peripherals-on-osx.
14
-    #if targetEnvironment(macCatalyst)
12
+    private var manager: CBCentralManager?
15 13
     private var advertisementDataCache = AdvertisementDataCache()
16
-    #endif
17 14
     @Published var managerState = CBManagerState.unknown
18 15
     
19 16
     override init () {
20 17
         super.init()
21
-        manager = CBCentralManager(delegate: self, queue: nil)
22 18
     }
23 19
     
20
+    func start() {
21
+        guard manager == nil else {
22
+            return
23
+        }
24
+        track("Starting Bluetooth manager and requesting authorization if needed")
25
+        manager = CBCentralManager(delegate: self, queue: nil)
26
+    }
24 27
     
25 28
     private func scanForMeters() {
29
+        guard let manager else {
30
+            track("Scan requested before Bluetooth manager was started")
31
+            return
32
+        }
26 33
         guard manager.state == .poweredOn else {
27 34
             track( "Scan requested but Bluetooth state is \(manager.state)")
28 35
             return
@@ -32,35 +39,57 @@ class BluetoothManager : NSObject, ObservableObject {
32 39
     }
33 40
     
34 41
     func discoveredMeter(peripheral: CBPeripheral, advertising advertismentData: [String : Any], rssi RSSI: NSNumber) {
35
-        //track("discovered new USB Meter: (\(peripheral), advertsing \(advertismentData)")
36
-        if let peripheralName = peripheral.name?.trimmingCharacters(in: .whitespacesAndNewlines) {
37
-            if let kCBAdvDataManufacturerData = advertismentData["kCBAdvDataManufacturerData"] as? Data {
38
-                // MARK: MAC Address
39
-                let macAddress = MACAddress(from: kCBAdvDataManufacturerData.suffix(from: 2))
40
-                // MARK: Model
41
-                if let model = ModelByPeriferalName[peripheralName] {
42
-                    //track("Tetermided model for peripheral name: '\(peripheralName)'")
43
-                    // MARK: Known Meters Lookup
44
-                    if appData.meters[peripheral.identifier] == nil {
45
-                        track("adding new USB Meter named '\(peripheralName)' with MAC Address: '\(macAddress)'")
46
-                        let btSerial = BluetoothSerial(peripheral: peripheral, radio:  modelRadios[model] ?? .UNKNOWN, with: macAddress, managedBy: manager, RSSI: RSSI.intValue)
47
-                        var m = appData.meters
48
-                        m[peripheral.identifier] = Meter(model: model, with: btSerial)
49
-                        appData.meters = m
50
-                    } else {
51
-//                        track("Updating USB Meter: \(peripheral.identifier) ")
52
-                        peripheral.delegate?.peripheral?(peripheral, didReadRSSI: RSSI, error: nil)
53
-                    }
54
-                } else {
55
-                    track("Unable to determine model for peripheral name: '\(peripheralName)'")
56
-                }
57
-            } else {
58
-                track("Insuficient data to use device!")
42
+        guard let peripheralName = resolvedPeripheralName(for: peripheral, advertising: advertismentData) else {
43
+            return
44
+        }
45
+        guard let manufacturerData = resolvedManufacturerData(from: advertismentData), manufacturerData.count > 2 else {
46
+            return
47
+        }
48
+        
49
+        guard let model = ModelByPeriferalName[peripheralName] else {
50
+            return
51
+        }
52
+        
53
+        let macAddress = MACAddress(from: manufacturerData.suffix(from: 2))
54
+        
55
+        if appData.meters[peripheral.identifier] == nil {
56
+            track("adding new USB Meter named '\(peripheralName)' with MAC Address: '\(macAddress)'")
57
+            let btSerial = BluetoothSerial(peripheral: peripheral, radio: modelRadios[model] ?? .UNKNOWN, with: macAddress, managedBy: manager!, RSSI: RSSI.intValue)
58
+            var m = appData.meters
59
+            m[peripheral.identifier] = Meter(model: model, with: btSerial)
60
+            appData.meters = m
61
+        } else if let meter = appData.meters[peripheral.identifier] {
62
+            meter.lastSeen = Date()
63
+            meter.btSerial.RSSI = RSSI.intValue
64
+            if peripheral.delegate == nil {
65
+                peripheral.delegate = meter.btSerial
66
+            }
67
+        }
68
+    }
69
+    
70
+    private func resolvedPeripheralName(for peripheral: CBPeripheral, advertising advertismentData: [String : Any]) -> String? {
71
+        let candidates = [
72
+            (advertismentData[CBAdvertisementDataLocalNameKey] as? String),
73
+            peripheral.name
74
+        ]
75
+        
76
+        for candidate in candidates {
77
+            if let trimmed = candidate?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty {
78
+                return trimmed
59 79
             }
60 80
         }
61
-        else{
62
-            track("Periferal: \(peripheral.identifier) does not have a name")
81
+        
82
+        return nil
83
+    }
84
+    
85
+    private func resolvedManufacturerData(from advertismentData: [String : Any]) -> Data? {
86
+        if let data = advertismentData[CBAdvertisementDataManufacturerDataKey] as? Data {
87
+            return data
63 88
         }
89
+        if let data = advertismentData["kCBAdvDataManufacturerData"] as? Data {
90
+            return data
91
+        }
92
+        return nil
64 93
     }
65 94
 }
66 95
 
@@ -94,39 +123,9 @@ extension BluetoothManager : CBCentralManagerDelegate {
94 123
         }
95 124
     }
96 125
     
97
-    // MARK: MacOS multiple discoveries advertisementData caching
98
-    #if targetEnvironment(macCatalyst)
99
-    private class AdvertisementDataCache {
100
-        
101
-        fileprivate var map = [UUID: [String: Any]]()
102
-        
103
-        func append(peripheral: CBPeripheral, advertisementData: [String: Any]) -> [String: Any] {
104
-            var ad = (map[peripheral.identifier]) ?? [String: Any]()
105
-            for (key, value) in advertisementData {
106
-                ad[key] = value
107
-            }
108
-            map[peripheral.identifier] = ad
109
-            return ad
110
-        }
111
-        
112
-        func clear() {
113
-            map.removeAll()
114
-        }
115
-    }
116
-    #endif
117
-
118 126
     // MARK:  CBCentralManager didDiscover peripheral
119 127
     func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
120
-        #if targetEnvironment(macCatalyst)
121
-// MARK: MacOS probably assumes that if "kCBAdvDataIsConnectable" is not present in parial advertisment data it nust be 0
122
-//        var ad = advertisementData
123
-//        if ( ad["kCBAdvDataManufacturerData"] == nil ) {
124
-//            ad.removeValue(forKey: "kCBAdvDataIsConnectable")
125
-//        }
126 128
         let completeAdvertisementData = self.advertisementDataCache.append(peripheral: peripheral, advertisementData: advertisementData)
127
-        #else
128
-        let completeAdvertisementData = advertisementData
129
-        #endif
130 129
         //track("Device discoverded UUID: '\(peripheral.identifier)' named '\(peripheral.name ?? "Unknown")'); RSSI: \(RSSI) dBm; Advertisment data: \(advertisementData)")
131 130
         discoveredMeter(peripheral: peripheral, advertising: completeAdvertisementData, rssi: RSSI )
132 131
     }
@@ -154,14 +153,15 @@ extension BluetoothManager : CBCentralManagerDelegate {
154 153
     }
155 154
 }
156 155
 
157
-// MARK: MacOS multiple discoveries advertisementData caching
158
-#if targetEnvironment(macCatalyst)
159 156
 private class AdvertisementDataCache {
160 157
     
161
-    fileprivate var map = [UUID: [String: Any]]()
158
+    private var map = [UUID: [String: Any]]()
162 159
     
163 160
     func append(peripheral: CBPeripheral, advertisementData: [String: Any]) -> [String: Any] {
164 161
         var ad = (map[peripheral.identifier]) ?? [String: Any]()
162
+        if let localName = peripheral.name?.trimmingCharacters(in: .whitespacesAndNewlines), !localName.isEmpty {
163
+            ad[CBAdvertisementDataLocalNameKey] = localName
164
+        }
165 165
         for (key, value) in advertisementData {
166 166
             ad[key] = value
167 167
         }
@@ -173,4 +173,3 @@ private class AdvertisementDataCache {
173 173
         map.removeAll()
174 174
     }
175 175
 }
176
-#endif
+3 -1
USB Meter/Views/ContentView.swift
@@ -44,6 +44,8 @@ struct ContentView: View {
44 44
                     print("Help tapped!")
45 45
             })
46 46
         }
47
+        .onAppear {
48
+            appData.bluetoothManager.start()
49
+        }
47 50
     }
48 51
 }
49
-