Newer Older
328 lines | 8.42kb
Bogdan Timofte authored 2 weeks ago
1
# Bluetooth Discovery
2

            
3
Mecanismul de descoperire şi conectare a dispozitivelor Bluetooth.
4

            
5
## Arhitectură
6

            
7
### Components
8

            
9
1. **BluetoothManager**: coordonează CBCentralManager şi descoperirile
10
2. **BluetoothRadio**: interfață spre Core Bluetooth low-level
11
3. **BluetoothSerial**: comunicare pe caracteristici UART
12
4. **MeterCapabilities**: detectează tip meter (UM25C, UM34C, TC66C)
13

            
14
## Scanning
15

            
16
### Lifecycle
17

            
18
```
19
User opens app
20
  ↓
21
SceneDelegate calls appData.activateCloudDeviceSync()
22
  ↓
23
BluetoothManager starts CBCentralManager
24
  ↓
25
CBCentralManager begins scanning
26
  ↓
27
didDiscoverPeripheral → filter by service UUIDs
28
  ↓
29
Check device profile (class, capabilities)
30
  ↓
31
Add to discoveredMeters list (UI refresh)
32
```
33

            
34
### Service UUIDs
35

            
36
```swift
37
let targetServices = [
38
    CBUUID(string: "FFF0"),    // UM25C, UM34C
39
    CBUUID(string: "180D"),    // TC66C (generic service)
40
]
41
```
42

            
43
- **MUST**: Scan cu service UUIDs specifice (nu scan generic)
44
- **SHOULD**: Filter prin manufacturer data dacă posibil
45
- **REASON**: Reduce energy drain, reduce noise
46

            
47
### Advertisement parsing
48

            
49
Meter advertise-ază:
50
- Device name: de ex "UM25C-XXXX" sau "TC66-XXXX"
51
- Services: FFF0 (UM series) sau 180D (TC66)
52
- Manufacturer data: RDTech identifier
53

            
54
```swift
55
if let manufacturerData = advertisement[CBAdvertisementDataManufacturerDataKey] as? Data {
56
    let manufacturerId = manufacturerData.withUnsafeBytes { $0.load(as: UInt16.self) }
57
    if manufacturerId == 0x5449 { // RDTech
58
        // Possible UM meter
59
    }
60
}
61
```
62

            
63
- **SHOULD**: Parse manufacturer data pentru identificare rapidă
64
- **MAY**: Use device name ca fallback (less reliable)
65

            
66
### Device identification
67

            
68
```swift
69
func identifyMeterType(name: String, services: [CBUUID]) -> Model {
70
    if name.contains("UM25C") || services.contains(FFF0) {
71
        return .UM25C
72
    } else if name.contains("UM34C") || services.contains(FFF0) {
73
        return .UM34C
74
    } else if name.contains("TC66") || services.contains(180D) {
75
        return .TC66C
76
    }
77
    return .unknown
78
}
79
```
80

            
81
- **MUST**: Trebuie să identific corect tipul de meter
82
- **SHOULD**: Use name + services (redundant checks)
83
- **MUST**: Fallback la `.unknown` dacă uncertain
84

            
85
## Connection
86

            
87
### Initiation
88

            
89
```
90
User taps meter in list
91
  ↓
92
AppData calls meter.connect()
93
  ↓
94
BluetoothManager calls cbCentralManager.connect(peripheral)
95
  ↓
96
didConnect: state → peripheralConnected
97
  ↓
98
Discover services & characteristics
99
```
100

            
101
### Service/Characteristic discovery
102

            
103
```swift
104
// For UM series (FFF0)
105
let targetService = CBUUID(string: "FFF0")
106
let readCharacteristic = CBUUID(string: "FFF1")  // read measurements
107
let writeCharacteristic = CBUUID(string: "FFF2") // send commands
108

            
109
// For TC66 (180D)
110
let heartRate = CBUUID(string: "2A37")  // uses standard HRM characteristic
111
```
112

            
113
- **MUST**: Discover services → discover characteristics (ordered)
114
- **MUST**: Find expected characteristics, fail if not found
115
- **SHOULD**: Subscribe to notifications pentru updates
116
- **TIMEOUT**: 5s max pentru service discovery
117

            
118
### State transitions
119

            
120
```
121
peripheralConnected
122
  ↓
123
discoveringServices (discovering FFF0 / 180D)
124
  ↓
125
discoveringCharacteristics (discovering FFF1, FFF2 / 2A37)
126
  ↓
127
peripheralReady (services + characteristics found)
128
  ↓
129
comunicating ↔ dataIsAvailable (steady state)
130
```
131

            
132
- **MUST**: Stare monotonă crescătoare (no rollback)
133
- **SHOULD**: Log state transitions
134
- **MUST**: Fail gracefully dacă characteristics nu sunt găsite
135

            
136
## Communication
137

            
138
### Measurement requests
139

            
140
**UM series** (UM25C, UM34C):
141
```swift
142
// Send command to FFF2 (write characteristic)
143
let command = UMProtocol.buildMeasurementRequest()
144
peripheral.writeValue(command, for: writeCharacteristic, type: .withResponse)
145

            
146
// Receive on FFF1 (read characteristic)
147
// Parse payload (voltage, current, power, temperature, etc.)
148
let measurement = UMProtocol.parseMeasurement(data)
149
```
150

            
151
**TC66C**:
152
```swift
153
// Uses standard HRM (Heart Rate Measurement) characteristic
154
// Value format: flags byte + heart_rate_value (2 bytes)
155
// Repurposed for power data: MSB = watts, LSB = amps (approximation)
156
let measurement = TC66Protocol.parseMeasurement(data)
157
```
158

            
159
- **MUST**: Parse protocol-specific payloads
160
- **MUST**: Validate checksum (if applicable)
161
- **SHOULD**: Handle invalid/truncated payloads gracefully
162
- **TIMEOUT**: 3s per measurement request
163

            
164
### Write commands
165

            
166
UM series supports commands:
167
- Request measurement: `0x00 0xF0 0xA0 0x1B` (+ checksum)
168
- Set time: `0x02 ...` (timestamp)
169
- Set calibration: (advanced)
170

            
171
```swift
172
peripheral.writeValue(
173
    command,
174
    for: writeCharacteristic,
175
    type: .withResponse  // MUST: wait for ACK
176
)
177
```
178

            
179
- **MUST**: Use `.withResponse` pentru command-uri critice
180
- **MAY**: Use `.withoutResponse` pentru bulk writes
181
- **SHOULD**: Validate response ACK
182

            
183
## Disconnection handling
184

            
185
### Intentional disconnect
186

            
187
```
188
User taps "Disconnect"
189
  ↓
190
meter.disconnect()
191
  ↓
192
BluetoothManager calls cbCentralManager.cancelPeripheralConnection()
193
  ↓
194
didDisconnect: state → peripheralNotConnected
195
```
196

            
197
- **MUST**: Anulează pending operations
198
- **MUST**: Anulează auto-reconnect logic
199
- **MUST**: Eliberează callbacks
200

            
201
### Unintentional disconnect (BT drop)
202

            
203
```
204
BT device disconnects (out of range / powered off / interference)
205
  ↓
206
didDisconnect event (didDisconnect reason: optional)
207
  ↓
208
state → peripheralNotConnected
209
  ↓
210
Auto-reconnect logic starts (with backoff)
211
```
212

            
213
- **MUST**: Detecta unintentional drops (log reason dacă available)
214
- **SHOULD**: Incepe auto-reconnect cu backoff exponential
215
- **MUST**: Anulează dacă user disconnect manual (flag)
216

            
217
## Auto-reconnect
218

            
219
### Backoff strategy
220

            
221
```
222
Attempt 1: 1s delay
223
Attempt 2: 2s delay
224
Attempt 3: 4s delay
225
Attempt 4: 8s delay
226
Attempt 5: 16s delay
227
Attempt 6: 32s delay
228
Attempt 7+: 60s delay (capped)
229
Max attempts: 3
230
```
231

            
232
- **MUST**: Exponential backoff (2^n, capped at 60s)
233
- **MUST**: Max 3 consecutive retry-uri
234
- **MUST**: Stop retry dacă user disconnect manual
235
- **SHOULD**: Log fiecare retry attempt
236

            
237
### Trigger conditions
238

            
239
- **MUST**: Activate automatic reconnect doar dacă user conectase anterior
240
- **MUST**: Disable dacă user disconnect manual
241
- **MUST**: Disable dacă app goes background > 10 min
242
- **SHOULD**: Resume reconnect dacă app returns foreground
243

            
244
## Testing
245

            
246
### Unit tests
247

            
248
```swift
249
test_scanFiltersByServiceUUIDs()
250
test_deviceIdentification_UM25C()
251
test_deviceIdentification_UM34C()
252
test_deviceIdentification_TC66C()
253
test_connectionStateTransitions()
254
test_serviceDiscovery_UM25C()
255
test_characteristicDiscovery_UM25C()
256
test_measurementParsing_ValidPayload()
257
test_measurementParsing_InvalidPayload()
258
test_disconnectCleansUp()
259
test_unintentionalDropDetected()
260
test_autoReconnectBackoff_Exponential()
261
test_autoReconnectStops_OnManualDisconnect()
262
```
263

            
264
### Integration tests
265

            
266
- [ ] Scan detects available meters
267
- [ ] Device type identified correctly
268
- [ ] Connect → service discover → ready (full flow)
269
- [ ] Measurement received and parsed
270
- [ ] Unintentional drop detected + reconnect
271
- [ ] Auto-reconnect respects backoff timing
272
- [ ] Manual disconnect stops auto-reconnect
273

            
274
## Error handling
275

            
276
### Scan errors
277

            
278
```
279
Error: CBError.unknown
280
Error: CBError.managerStatePoweredOff
281
Error: CBError.invalidParameters
282
```
283

            
284
Handling:
285
- Retry scan periodically
286
- Notify UI: "Bluetooth unavailable"
287

            
288
### Connection errors
289

            
290
```
291
Error: peripheral not found
292
→ Retry with backoff
293

            
294
Error: timeout (no services found)
295
→ Disconnect + retry
296

            
297
Error: security/pairing required
298
→ Notify user: "Pair device in Settings"
299
```
300

            
301
### Communication errors
302

            
303
```
304
Error: write failed
305
→ Retry measurement request
306

            
307
Error: invalid payload
308
→ Log error, skip measurement
309

            
310
Error: characteristic not found
311
→ Disconnect + mark incompatible
312
```
313

            
314
## Dependencies
315

            
316
- `CoreBluetooth`: CBCentralManager, CBPeripheral
317
- `UMProtocol`: payload parsing for UM series
318
- `TC66Protocol`: payload parsing for TC66C
319
- `MeterCapabilities`: device type detection
320
- `AppData`: orchestration
321

            
322
## References
323

            
324
- [UMProtocol.swift](../../USB%20Meter/Model/UMProtocol.swift)
325
- [TC66Protocol.swift](../../USB%20Meter/Model/TC66Protocol.swift)
326
- [BluetoothManager.swift](../../USB%20Meter/Model/BluetoothManager.swift)
327
- [External documentation](https://sigrok.org/wiki/RDTech_UM_series)
328
- [TC66C reverse-engineering notes](../Research%20Resources/Payload%20Notes/TC66C%20Transport%20and%20Payload%20Working%20Note.md)