

# Best Practices

Patterns for building reliable apps with the igloohome BLE SDK.

---

## Architecture

```
┌──────────────┐
│   Your App   │
└──────┬───────┘
       │  IglooPlugin API (suspend / Flow)
┌──────▼───────┐
│  Igloohome   │──── REST API ────► Igloo Server
│     SDK      │
└──────┬───────┘
       │  BLE
┌──────▼───────┐
│  Smart Lock  │
└──────────────┘
```

The SDK orchestrates BLE and server operations together. For example, `createPin()` writes the PIN to lock hardware via BLE, registers it on the server, and rolls back if the server call fails — all in one call.

---

## Singleton Pattern

Only one `IglooPlugin` instance should exist at a time. Multiple instances hold separate connection state and will cause undefined BLE behavior.

```kotlin
// Application-scoped singleton
object SdkProvider {
    lateinit var sdk: IglooPlugin
        private set

    fun init(context: Context) {
        sdk = IglooPlugin(context.applicationContext)
    }
}
```

```swift
// App-scoped singleton. IglooPlugin is @MainActor and holds no context/config —
// init() takes no parameters.
@MainActor
enum SdkProvider {
    static let sdk = IglooPlugin()
}
```

---

## Token Management

The igloohome SDK uses separate tokens for different operations:

### Sync Tokens

The `sync()` method requires three dedicated tokens:

| Token | Purpose |
|-------|---------|
| `getDeviceToken` | Reading device state from the server. |
| `storeLogsToken` | Uploading activity logs to the server. |
| `updateDeviceToken` | Patching device info (battery, firmware version). |

```kotlin
val result = sdk.sync(
    deviceId = deviceId,
    key = guestKey,
    getDeviceToken = tokens.getDeviceToken,
    storeLogsToken = tokens.storeLogsToken,
    updateDeviceToken = tokens.updateDeviceToken,
)
```

```swift
let result = try await sdk.sync(
    deviceId,
    key: guestKey,
    getDeviceToken: tokens.getDeviceToken,
    storeLogsToken: tokens.storeLogsToken,
    updateDeviceToken: tokens.updateDeviceToken)
```

### Access Token

All other methods (`createPin`, `addKeycard`, `addFingerprint`, `link`, `checkFirmwareUpdate`, `performDfu`, etc.) accept a single `accessToken: String` parameter. This token is always required.

```kotlin
sdk.createPin(
    deviceId = deviceId,
    key = guestKey,
    pin = "123456",
    pinType = PinType.PERMANENT,
    name = "Front Door PIN",
    accessToken = accessToken,
)
```

```swift
try await sdk.createPin(
    deviceId: deviceId,
    key: guestKey,
    name: "Front Door PIN",
    pin: "123456",
    pinType: .permanent,
    startDate: nil,
    endDate: nil,
    accessToken: accessToken)
```

---

## Connection Management

- **One operation at a time.** BLE operations are serialized internally per device — don't fire multiple lock/unlock calls concurrently on the same device.
- **Disconnect when done.** Call `disconnect(deviceId)` after your operation completes to free BLE resources.
- **The SDK connects automatically.** There is no separate `connect()` method — `lock()`, `sync()`, `pair()` etc. all connect internally.

```kotlin
try {
    sdk.lock(deviceId, key)
} finally {
    sdk.disconnect(deviceId)
}
```

```swift
// Swift has no try/finally — use `defer` instead, which runs when the
// enclosing scope exits whether `lock` throws or not.
defer { Task { try? await sdk.disconnect(deviceId) } }
try await sdk.lock(deviceId, key: key)
```

---

## Coroutine Patterns

All BLE operations are `suspend` functions or return `Flow`. Use structured concurrency.

### Suspend Functions

```kotlin
// In a ViewModel
viewModelScope.launch {
    try {
        sdk.lock(deviceId, key)
        _uiState.value = UiState.Success
    } catch (e: IglooHomeException) {
        _uiState.value = UiState.Error(e.message)
    }
}
```

### Flow Collection

```kotlin
// Scanning — cancel when no longer needed
private var scanJob: Job? = null

fun startScan() {
    scanJob = viewModelScope.launch {
        sdk.scanDevice().collect { result ->
            _devices.value += result
        }
    }
}

fun stopScan() {
    scanJob?.cancel()
}
```

### Flow with Lifecycle

```kotlin
// In a Fragment/Activity
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        sdk.addFingerprint(deviceId, key, name, accessToken).collect { event ->
            // Handle scan events
        }
    }
}
```

---

## Swift Concurrency Patterns

All BLE operations are `async throws` functions or return `AsyncStream`/`AsyncThrowingStream`. Use structured concurrency (`Task`, `async let`, task cancellation) in place of Kotlin's coroutine scopes.

### Async Functions

```swift
// In an @Observable / ObservableObject view model
Task {
    do {
        try await sdk.lock(deviceId, key: key)
        uiState = .success
    } catch {
        uiState = .error(error.localizedDescription)
    }
}
```

### Stream Collection

```swift
// Scanning — cancel when no longer needed
private var scanTask: Task<Void, Never>?

func startScan() {
    scanTask = Task {
        do {
            for try await result in sdk.scansLock() {
                devices.append(result)
            }
        } catch {
            // Handle scan error
        }
    }
}

func stopScan() {
    sdk.stopScan()
    scanTask?.cancel()
}
```

### Stream with View Lifecycle

```swift
// In a SwiftUI View, tied to the view's lifetime
.task {
    for await event in await sdk.addFingerprint(
        deviceId: deviceId, key: key, name: name, accessToken: accessToken)
    {
        // Handle scan events
    }
}
```

---

## Error Handling

Catch at the UI layer, not deep in the call stack.

```kotlin
viewModelScope.launch {
    try {
        sdk.pair(deviceId, name, accessToken)
        _uiState.value = UiState.Paired
    } catch (e: IglooHomeException) {
        _uiState.value = when (e) {
            is IglooHomeException.DevicePairedException ->
                UiState.Error("Already paired")
            is IglooHomeException.HasLinkedDeviceException ->
                UiState.Error("Unlink ${e.linkedDeviceIds.size} accessories first")
            is IglooHomeException.ConnectionException ->
                UiState.Retryable("unexpected bluetooth connection issue")
            is IglooHomeException.TimeoutException ->
                UiState.Retryable("Connection lost — try again")
            else ->
                UiState.Error(e.message ?: "Unknown error")
        }
    }
}
```

```swift
Task {
    do {
        try await sdk.pair(deviceId: deviceId, lockName: name, propertyIds: propertyIds, accessToken: accessToken)
        uiState = .paired
    } catch IgloohomeError.devicePaired {
        uiState = .error("Already paired")
    } catch IgloohomeError.hasLinkedDevice(let linkedDeviceIds) {
        uiState = .error("Unlink \(linkedDeviceIds.count) accessories first")
    } catch IgloohomeError.connection {
        uiState = .retryable("unexpected bluetooth connection issue")
    } catch IgloohomeError.timeout {
        uiState = .retryable("Connection lost — try again")
    } catch {
        uiState = .error(error.localizedDescription)
    }
}
```

**Retry only transient errors:** `TimeoutException`, `ConnectionException`, and `ApiException` with 5xx status (Android); `IgloohomeError.timeout`, `.connection`, and `IgloohomeError.api` with 5xx status (iOS). Terminal errors like `DevicePairedException`/`IgloohomeError.devicePaired` or `LockStorageFullException`/`IgloohomeError.lockStorageFull` require user action.

---

## Recommended Operation Sequences

### First-Time Device Setup

1. `scanDevice()` (Android) / `scansLock()` (iOS) — find the lock
2. `pair()` — register on account
3. `calibrate()` — tune motor direction
4. `sync()` — set clock and read battery
5. `setWifiConfig()` — if bridge, configure WiFi

### Regular Operations

1. `lock()` / `unlock()` — control the lock
2. `sync()` — periodically sync time and logs

### Access Management

1. `createPin()` / `addKeycard()` / `addFingerprint()` — add access
2. `deletePin()` / `deleteKeycard()` / `deleteFingerprint()` — remove access

### Firmware Update

1. `checkFirmwareUpdate()` — check for updates
2. `performDfu()` — apply update (keep screen on)
3. `sync()` — verify device state after update

### Device Removal

1. `unlink()` — remove all accessories first
2. `unpair()` — remove the lock

---

## Troubleshooting

| Problem | Cause | Solution |
|---------|-------|----------|
| `BluetoothException` on every call | Bluetooth disabled or permissions not granted | Check `BluetoothAdapter.isEnabled` and request runtime permissions |
| `ConnectionException` frequently | Device out of BLE range | Move within 2–3 meters of the lock |
| `TimeoutException` on first call | Lock in deep sleep | Retry — first connection wakes the lock |
| `DevicePairedException` during pair | Lock already registered | Check server if you own it; factory reset if transferring |
| `HasLinkedDeviceException` during unpair | Accessories still linked | Call `unlink()` for each accessory first |
| `DuplicatePinException` | Same PIN exists on lock | Choose a different PIN code |
| `LockStorageFullException` | Lock PIN/card slots exhausted | Delete existing access before adding new ones |
| `BatteryLowException` during DFU | Battery below threshold | Charge or replace batteries before DFU |
| Multiple `IglooPlugin` instances | Creating SDK in Activity/Fragment | Use application-scoped singleton |
| BLE operations fail silently | RxJava undeliverable exceptions | SDK handles these internally — upgrade if on old version |

The same table for iOS:

| Problem | Cause | Solution |
|---------|-------|----------|
| `IgloohomeError.bluetoothIsTurnedOff` on every call | Bluetooth disabled, or `NSBluetoothAlwaysUsageDescription` missing from `Info.plist` | Enable Bluetooth; verify the Info.plist keys are present (iOS kills the app on first scan without them) |
| `IgloohomeError.lockNotFound` / `.connection` frequently | Device out of BLE range | Move within 2–3 meters of the lock |
| `IgloohomeError.timeout` on first call | Lock in deep sleep | Retry — first connection wakes the lock |
| `IgloohomeError.devicePaired` during pair | Lock already registered | Check server if you own it; factory reset if transferring |
| `IgloohomeError.hasLinkedDevice` during unpair | Accessories still linked | Call `unlink()` for each accessory first |
| `IgloohomeError.duplicatePin` | Same PIN exists on lock | Choose a different PIN code |
| `IgloohomeError.lockStorageFull` | Lock PIN/card slots exhausted | Delete existing access before adding new ones |
| `IgloohomeError.batteryLow` during DFU | Battery below threshold | Charge or replace batteries before DFU |
| Testing on Simulator does nothing | Bluetooth is unavailable in the iOS Simulator | Test on a physical device |
| Two `CreateKeycardAccessResponse` / `CalibrationLockingDirection` types found | Both `IgloohomeSDK` and `IglooSDKCore` declare a type of that name (different raw values for `CalibrationLockingDirection`) | Import only `IgloohomeSDK` for app code where possible; be explicit about which module's type you mean if you also import `IglooSDKCore` |
