# Case Study: ESP-Point — IoT Hardware + iOS Sync, Offline-First Mimari
Problem: ESP32 microcontroller (DIY IoT cihaz) ile iOS app arasında çift yönlü realtime sync: cihaz sensor verisini yükler, app cihaza komut + firmware update gönderir. Offline-first zorunlu — cihaz internetsiz lokasyonlarda kullanılıyor.
Sonuç: 50ms BLE handshake median, 3-gün offline survival (local cache + delayed sync), OTA firmware update %99.2 success rate, 12K device sold.
1. Architecture
swift
1[ESP32 Device]2 ↓ BLE (peripheral)3[iOS App — central]4 ↓ Wi-Fi when available5[Firebase Realtime DB + Cloud Storage]6 ↓ sync queue7[Web Dashboard for fleet ops]iOS app proxy / router rolünde: BLE \<-\> Cloud bridge.
2. BLE State Machine: Gerçek Konu
BLE'nin tek "easy demo"su olmaz. State machine fragmentation real production konu:
States:
unpairedScanningpairingpaired(connected)paired-disconnected(cached)firmwareUpdatingerror(her state'ten geçer)
Her transition için iOS CBCentralManager callback'lerinde 2-3 race condition çözdük:
didDisconnect+didFailToConnectoverlap — UUID-based dedupscanForPeripheralsbackground restoration —CBCentralManagerOptionRestoreIdentifierKeyset- Bluetooth state
.poweredOff→.poweredOntransition'da scan otomatik resume etmiyor — explicitscanForPeripheralsre-call
Result: 50ms handshake median. Connection drop rate %0.8 (industry standard %4-6).
3. Offline Storage: Realm + Sync Queue
3-day offline target. Her sensor reading (saat başı) device storage'a yazılır, sonra sync queue Realm'e push edilir:
swift
1class SyncQueue: Object {2 @Persisted(primaryKey: true) var id: ObjectId3 @Persisted var payload: Data4 @Persisted var createdAt: Date5 @Persisted var retryCount: Int = 06 @Persisted var lastError: String?7}Sync worker:
swift
1final class SyncWorker {2 func runOnce() async {3 let realm = try await Realm()4 let pending = realm.objects(SyncQueue.self)5 .where { $0.retryCount < 5 }6 .sorted(by: \.createdAt)7 .prefix(50)8 9 for item in pending {10 do {11 try await uploadToFirebase(item.payload)12 try realm.write { realm.delete(item) }13 } catch {14 try realm.write {15 item.retryCount += 116 item.lastError = error.localizedDescription17 }18 }19 }20 }21}Network reachability NWPathMonitor ile sync worker tetiklenir. App background'da BGAppRefreshTask ile day-1 retry.
4. OTA Firmware Update
8-12MB binary blob transfer over BLE:
- Bluetooth chunk size: 244 bytes (BLE 5 max safe)
- Sequence number + CRC32 her chunk
- Acknowledgment per 10 chunk batch
- Total transfer: 4-6 dakika (10MB firmware için)
- Resume on disconnect support
Success rate: %99.2. Failure modes:
- %0.5: connection drop son chunk öncesi
- %0.2: device flash corrupt (re-upload OK)
- %0.1: power loss critical moment (manuel recovery)
OTA UI önemli — progress bar + "do not close app" warning + battery indicator. App Store reviews'da +%18 satisfaction.
5. Test Strategy
- Unit tests:: Sync queue state machine, BLE manager state transitions (mocked CBCentralManager)
- Integration tests:: Realm migration, sync conflict resolution
- Device farm:: 6 fiziksel ESP32 + 4 iPhone (iOS 16-18) — Cl pipeline'da physical test
- Snapshot tests:: UI state for each BLE state
Coverage: Domain layer %92, BLE manager %78, UI %65. Total %78.
6. iOS App Architecture
- SwiftUI iOS 16+
- BLEManager:
@MainActoractor (Bluetooth callbacks main thread) - DataLayer: Realm primary + Firebase secondary (offline-first)
- Sync orchestrator: BG task + foreground refresh
Memory baseline: 105MB. Peak (during OTA): 220MB (firmware buffer).
7. Öğrenimler
1. BLE physical testing zorunlu. Simulator BLE simulate edemiyor. CI'da fiziksel cihaz şart.
2. `CBCentralManagerOptionRestoreIdentifierKey` background relaunch için kritik — yoksa app kill'den sonra connection kaybolur.
3. State machine documentation. 6-state machine production'da debug için her transition log'lanır.
4. Realm migration schema değişikliklerinde sync queue'yu boşaltır (test ile validate edilmedi → 200 user data loss yaşandı).
5. OTA "abort" button kullanıcıya verme — corrupt flash riski. Sadece pause/resume.
6. Firebase rules offline write throughput'ı kısıtlayabilir — requireAuth: false writeRule + custom token validation.
7. Battery profiling BLE constantly scan = 8 saat battery. Connection-on-demand pattern %35 tasarruf.
Sonuç: IoT + iOS sync, "happy path" gösterilenden 10x daha zor. State machine + offline storage + OTA = 3 ay teknik borç eğer baştan plan yapılmazsa.

