USB Meter utilizează NSPersistentCloudKitContainer pentru a sincroniza setările dispozitivelor Bluetooth pe mai multe device-uri iOS/iPadOS via iCloud CloudKit.
Container CloudKit: iCloud.ro.xdev.USB-Meter
Stocare centralizată a setărilor și metadatelor de conexiune pentru fiecare contor Bluetooth, cu sincronizare automată pe toate device-urile utilizatorului.
ProcessInfo.processInfo.hostName
| Atribut | Tip | Optional | Descriere |
|---|---|---|---|
| macAddress | String | YES | Adresa MAC unică a contorului BT (cheie candidat pentru deduplicare) |
| meterName | String | YES | Renumire personalizată a contorului (ex: "Kitchen Meter") |
| tc66TemperatureUnit | String | YES | Preferință unitate temperatură pentru TC66C (raw value enum: "celsius"/"fahrenheit") |
| modelType | String | YES | Tip model contor ("UM25C", "UM34C", "TC66C") |
| connectedByDeviceID | String | YES | UUID-ul device-ului iOS care l-a conectat ultima dată |
| connectedByDeviceName | String | YES | Numele device-ului iOS care l-a conectat |
| connectedAt | Date | YES | Timestamp al celei mai recente conexiuni |
| connectedExpiryAt | Date | YES | TTL pentru lock-ul de conexiune (alt device poate prelua dacă a expirat) |
| lastSeenAt | Date | YES | Timestamp al ultimei avertisment BT din device |
| lastSeenByDeviceID | String | YES | UUID device-ul iOS care l-a văzut ultima dată |
| lastSeenByDeviceName | String | YES | Nume device-ul iOS care l-a văzut |
| lastSeenPeripheralName | String | YES | Numele periferalei așa cum raporta dispozitivul |
| updatedAt | Date | YES | Timestamp al ultimei actualizări (audit trail) |
Fiecare atribut Core Data → câmp CloudKit în record-ul DeviceSettings:
- Sincronizare automată bidirecțională
- Lightweight migration pentru schimbări de schemă
- Conflict resolution: NSMergeByPropertyStoreTrumpMergePolicy (CloudKit wins pe changed properties)
uniquenessConstraint pe macAddress — incompatibil cu NSPersistentCloudKitContainer (genereaza erori silentioase)rebuildCanonicalStoreIfNeeded
UserDefaults cu key "cloudStoreRebuildVersion.{n}")┌─────────────────────────────────────────────────────────────────┐
│ Device A (iPhone) │
├─────────────────────────────────────────────────────────────────┤
│ User connects UM25C (MAC: aa:bb:cc:dd:ee:ff) │
│ ↓ │
│ Meter.swift → appData.publishMeterConnection(mac, type) │
│ ↓ │
│ CloudDeviceSettingsStore.setConnection() │
│ - Creates / Updates DeviceSettings record │
│ - Sets connectedByDeviceID = Device A UUID │
│ - connectedAt = now │
│ - connectedExpiryAt = now + 120s │
│ ↓ │
│ NSPersistentCloudKitContainer saves to Core Data │
│ ↓ │
│ CloudKit framework (automatic sync): │
│ - Pushes record modificare → iCloud server │
│ - Records: [CKRecord with RecordID "DeviceSettings/mac"] │
│ ↓ │
│ iCloud Server → Device B (iPad) │
│ ↓ │
│ Device B receives remote change notification │
│ (NSPersistentStoreRemoteChangeNotificationPostOptionKey) │
│ ↓ │
│ AppData.reloadSettingsFromCloudStore() │
│ - Fetches updated record din local Core Data │
│ - Sees connectedByDeviceID = Device A UUID │
│ - Checks connectedExpiryAt (not expired) │
│ - Knows someone else is connected │
│ ↓ │
│ UI updates: shows "Connected on iPhone" │
└─────────────────────────────────────────────────────────────────┘
Device A și Device B conectează același contor ~același timp.
Device A: Device B:
t=0.0s: setConnection() t=0.1s: setConnection()
- connectedByDeviceID = A - connectedByDeviceID = B
- connectedAt = 0.0s - connectedAt = 0.1s
↓ save ↓ save
CloudKit pushes A's record CloudKit pushes B's record
↓ ↓
└──────→ iCloud Server ←──────┘
(concurrent writes)
Merge Strategy:
- NSMergeByPropertyStoreTrumpMergePolicy
- Latest CloudKit record "wins" per property
- Result: B's connectedByDeviceID + connectedAt win
(mai recent timestamp)
Device A conectează la t=0. Expiry la t=120. Device B incearcă conexiune la t=150.
t=0s: Device A connects (connectedAt=0, expiry=120)
t=120s: Connection expires
t=150s: Device B checks:
if (connectedExpiryAt < now):
// Lock expired, can connect
setConnection() → Device B becomes new owner
Rulează la AppDelegate.didFinishLaunchingWithOptions:
rebuildCanonicalStoreIfNeeded(version: 3)
1. Fetch ALL DeviceSettings records
2. Group by normalized macAddress
3. Per group:
- Select "winner" (prefer: more recent updatedAt, more data fields populated)
- Merge other records INTO winner
- Delete duplicates (only delete-uri, no inserts)
4. Save context → CloudKit deletes propagate
5. Mark in UserDefaults as "done for v3"
De ce in-place? Pentru a nu cauza "delete storm": - ❌ Old way: Delete ALL + Insert ALL → alt device primește DELETE-uri înainte de INSERT-uri → pierde date - ✅ New way: Update IN-PLACE + Delete only duplicates → record ID-ul DeviceSettings este păstrat → sincronizare progresivă
| Model | Version | Changes |
|---|---|---|
| USB_Meter.xcdatamodel | 1 | Original schema cu uniquenessConstraint |
| USB_Meter 2.xcdatamodel | 2 | ❌ Removed constraint (but rebuild was destructive) |
| USB_Meter 2.xcdatamodel | 3 | ✅ Rebuilt with in-place merge strategy |
Migration path:
v1 → v2 (lightweight, auto) → v3 (rebuild in-place, UserDefaults gated)