igloo
Docs / iglooworks / BLE SDK / Best Practices

Best Practices

Patterns for building reliable apps with the IglooWorks BLE SDK.


Architecture

┌──────────────┐
│   Your App   │
└──────┬───────┘
       │  IglooPlugin API (suspend / Flow)
┌──────▼───────┐
│  IglooWorks  │──── 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.

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

    fun init(context: Context, apiKey: String) {
        sdk = IglooPlugin(context.applicationContext, apiKey)
    }
}
// App-scoped singleton. IglooPlugin is @MainActor and holds no context —
// pick whichever initializer matches your auth mode.
@MainActor
enum SdkProvider {
    static let sdk = IglooPlugin(apiKey: "your-api-key")
}

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.
try {
    sdk.lock(deviceId, key)
} finally {
    sdk.disconnect(deviceId)
}
// 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)

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

// In a ViewModel
viewModelScope.launch {
    try {
        sdk.lock(deviceId, key)
        _uiState.value = UiState.Success
    } catch (e: IglooWorksException) {
        _uiState.value = UiState.Error(e.message)
    }
}
// In an @Observable / ObservableObject view model
Task {
    do {
        try await sdk.lock(deviceId, key: key)
        uiState = .success
    } catch {
        uiState = .error(error.localizedDescription)
    }
}

Stream Collection

// 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()
}
// 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

// In a Fragment/Activity
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        sdk.addFingerprint(deviceId, key, name).collect { event ->
            // Handle scan events
        }
    }
}
// In a SwiftUI View, tied to the view's lifetime
.task {
    for await event in await sdk.addFingerprint(deviceId: deviceId, key: key, name: name) {
        // Handle scan events
    }
}

Error Handling

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

viewModelScope.launch {
    try {
        sdk.pair(deviceId, name, propertyIds)
        _uiState.value = UiState.Paired
    } catch (e: IglooWorksException) {
        _uiState.value = when (e) {
            is IglooWorksException.DevicePairedException ->
                UiState.Error("Already paired")
            is IglooWorksException.HasLinkedDeviceException ->
                UiState.Error("Unlink ${e.linkedDeviceIds.size} accessories first")
            is IglooWorksException.ConnectionException ->
                UiState.Retryable("unexpected bluetooth connection issue")
            is IglooWorksException.TimeoutException ->
                UiState.Retryable("Connection lost — try again")
            else ->
                UiState.Error(e.message ?: "Unknown error")
        }
    }
}
Task {
    do {
        try await sdk.pair(deviceId: deviceId, lockName: name, propertyIds: propertyIds)
        uiState = .paired
    } catch IglooworksError.devicePaired {
        uiState = .error("Already paired")
    } catch IglooworksError.hasLinkedDevice(let linkedDeviceIds) {
        uiState = .error("Unlink \(linkedDeviceIds.count) accessories first")
    } catch LockManagerError.deviceDisconnected {
        uiState = .retryable("unexpected bluetooth connection issue")
    } catch IglooworksError.timeout, LockManagerError.LockTimeoutError {
        uiState = .retryable("Connection lost — try again")
    } catch {
        uiState = .error(error.localizedDescription)
    }
}

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


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
  3. syncJob() (Android) / syncJobs() (iOS) — execute queued PIN/key jobs

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
LockManagerError.bluetoothIsTurnedOff on every call Bluetooth disabled, or NSBluetoothAlwaysUsageDescription missing from Info.plist Enable Bluetooth; verify the Info.plist key is present
LockManagerError.deviceDisconnected frequently Device out of BLE range Move within 2–3 meters of the lock
LockManagerError.LockTimeoutError / IglooworksError.timeout on first call Lock in deep sleep Retry — first connection wakes the lock
IglooworksError.devicePaired / LockManagerError.deviceAlreadyPaired during pair Lock already registered Check server if you own it; factory reset if transferring
IglooworksError.hasLinkedDevice during unpair Accessories still linked Call unlink() for each accessory first
IglooworksError.duplicatePin Same PIN exists on lock Choose a different PIN code
IglooworksError.lockStorageFull Lock PIN/card slots exhausted Delete existing access before adding new ones
IglooworksError.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
401 from an OAuth-mode IglooPlugin() call No accessToken/...Token argument passed for that call Pass the relevant token — required per-call under OAuth mode, unlike API Key mode