1 contributor
328 lines | 8.42kb

Bluetooth Discovery

Mecanismul de descoperire şi conectare a dispozitivelor Bluetooth.

Arhitectură

Components

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

Scanning

Lifecycle

User opens app
  ↓
SceneDelegate calls appData.activateCloudDeviceSync()
  ↓
BluetoothManager starts CBCentralManager
  ↓
CBCentralManager begins scanning
  ↓
didDiscoverPeripheral → filter by service UUIDs
  ↓
Check device profile (class, capabilities)
  ↓
Add to discoveredMeters list (UI refresh)

Service UUIDs

let targetServices = [
    CBUUID(string: "FFF0"),    // UM25C, UM34C
    CBUUID(string: "180D"),    // TC66C (generic service)
]
  • MUST: Scan cu service UUIDs specifice (nu scan generic)
  • SHOULD: Filter prin manufacturer data dacă posibil
  • REASON: Reduce energy drain, reduce noise

Advertisement parsing

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

if let manufacturerData = advertisement[CBAdvertisementDataManufacturerDataKey] as? Data {
    let manufacturerId = manufacturerData.withUnsafeBytes { $0.load(as: UInt16.self) }
    if manufacturerId == 0x5449 { // RDTech
        // Possible UM meter
    }
}
  • SHOULD: Parse manufacturer data pentru identificare rapidă
  • MAY: Use device name ca fallback (less reliable)

Device identification

func identifyMeterType(name: String, services: [CBUUID]) -> Model {
    if name.contains("UM25C") || services.contains(FFF0) {
        return .UM25C
    } else if name.contains("UM34C") || services.contains(FFF0) {
        return .UM34C
    } else if name.contains("TC66") || services.contains(180D) {
        return .TC66C
    }
    return .unknown
}
  • MUST: Trebuie să identific corect tipul de meter
  • SHOULD: Use name + services (redundant checks)
  • MUST: Fallback la .unknown dacă uncertain

Connection

Initiation

User taps meter in list
  ↓
AppData calls meter.connect()
  ↓
BluetoothManager calls cbCentralManager.connect(peripheral)
  ↓
didConnect: state → peripheralConnected
  ↓
Discover services & characteristics

Service/Characteristic discovery

// For UM series (FFF0)
let targetService = CBUUID(string: "FFF0")
let readCharacteristic = CBUUID(string: "FFF1")  // read measurements
let writeCharacteristic = CBUUID(string: "FFF2") // send commands

// For TC66 (180D)
let heartRate = CBUUID(string: "2A37")  // uses standard HRM characteristic
  • MUST: Discover services → discover characteristics (ordered)
  • MUST: Find expected characteristics, fail if not found
  • SHOULD: Subscribe to notifications pentru updates
  • TIMEOUT: 5s max pentru service discovery

State transitions

peripheralConnected
  ↓
discoveringServices (discovering FFF0 / 180D)
  ↓
discoveringCharacteristics (discovering FFF1, FFF2 / 2A37)
  ↓
peripheralReady (services + characteristics found)
  ↓
comunicating ↔ dataIsAvailable (steady state)
  • MUST: Stare monotonă crescătoare (no rollback)
  • SHOULD: Log state transitions
  • MUST: Fail gracefully dacă characteristics nu sunt găsite

Communication

Measurement requests

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

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

TC66C: swift // Uses standard HRM (Heart Rate Measurement) characteristic // Value format: flags byte + heart_rate_value (2 bytes) // Repurposed for power data: MSB = watts, LSB = amps (approximation) let measurement = TC66Protocol.parseMeasurement(data)

  • MUST: Parse protocol-specific payloads
  • MUST: Validate checksum (if applicable)
  • SHOULD: Handle invalid/truncated payloads gracefully
  • TIMEOUT: 3s per measurement request

Write commands

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

peripheral.writeValue(
    command,
    for: writeCharacteristic,
    type: .withResponse  // MUST: wait for ACK
)
  • MUST: Use .withResponse pentru command-uri critice
  • MAY: Use .withoutResponse pentru bulk writes
  • SHOULD: Validate response ACK

Disconnection handling

Intentional disconnect

User taps "Disconnect"
  ↓
meter.disconnect()
  ↓
BluetoothManager calls cbCentralManager.cancelPeripheralConnection()
  ↓
didDisconnect: state → peripheralNotConnected
  • MUST: Anulează pending operations
  • MUST: Anulează auto-reconnect logic
  • MUST: Eliberează callbacks

Unintentional disconnect (BT drop)

BT device disconnects (out of range / powered off / interference)
  ↓
didDisconnect event (didDisconnect reason: optional)
  ↓
state → peripheralNotConnected
  ↓
Auto-reconnect logic starts (with backoff)
  • MUST: Detecta unintentional drops (log reason dacă available)
  • SHOULD: Incepe auto-reconnect cu backoff exponential
  • MUST: Anulează dacă user disconnect manual (flag)

Auto-reconnect

Backoff strategy

Attempt 1: 1s delay
Attempt 2: 2s delay
Attempt 3: 4s delay
Attempt 4: 8s delay
Attempt 5: 16s delay
Attempt 6: 32s delay
Attempt 7+: 60s delay (capped)
Max attempts: 3
  • MUST: Exponential backoff (2^n, capped at 60s)
  • MUST: Max 3 consecutive retry-uri
  • MUST: Stop retry dacă user disconnect manual
  • SHOULD: Log fiecare retry attempt

Trigger conditions

  • MUST: Activate automatic reconnect doar dacă user conectase anterior
  • MUST: Disable dacă user disconnect manual
  • MUST: Disable dacă app goes background > 10 min
  • SHOULD: Resume reconnect dacă app returns foreground

Testing

Unit tests

test_scanFiltersByServiceUUIDs()
test_deviceIdentification_UM25C()
test_deviceIdentification_UM34C()
test_deviceIdentification_TC66C()
test_connectionStateTransitions()
test_serviceDiscovery_UM25C()
test_characteristicDiscovery_UM25C()
test_measurementParsing_ValidPayload()
test_measurementParsing_InvalidPayload()
test_disconnectCleansUp()
test_unintentionalDropDetected()
test_autoReconnectBackoff_Exponential()
test_autoReconnectStops_OnManualDisconnect()

Integration tests

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

Error handling

Scan errors

Error: CBError.unknown
Error: CBError.managerStatePoweredOff
Error: CBError.invalidParameters

Handling: - Retry scan periodically - Notify UI: "Bluetooth unavailable"

Connection errors

Error: peripheral not found
→ Retry with backoff

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

Error: security/pairing required
→ Notify user: "Pair device in Settings"

Communication errors

Error: write failed
→ Retry measurement request

Error: invalid payload
→ Log error, skip measurement

Error: characteristic not found
→ Disconnect + mark incompatible

Dependencies

  • CoreBluetooth: CBCentralManager, CBPeripheral
  • UMProtocol: payload parsing for UM series
  • TC66Protocol: payload parsing for TC66C
  • MeterCapabilities: device type detection
  • AppData: orchestration

References