1 contributor
249 lines | 6.533kb

Core Operations

Operaţii principale care orchestrează aplicaţia.

AppData Lifecycle

AppData este singleton-ul global care orchestrează toate subsistemele.

Inițializare

let appData = AppData()
  • MUST: Se instanțiază în AppDelegate.application(_:didFinishLaunchingWithOptions:)
  • MUST: Se accesează din SceneDelegate.scene(_:willConnectTo:options:)
  • MUST: Inițializează NSPersistentCloudKitContainer cu name "CKModel"
  • MUST: Inițializează CloudDeviceSettingsStore (wraps NSManagedObjectContext)

Startup sequence

  1. AppDelegate.application(_:didFinishLaunchingWithOptions:):

    • Crează AppData()
    • Inițializează Core Data stack
    • Încarcă migrări din cloudStoreRebuildVersion
  2. SceneDelegate.scene(_:willConnectTo:options:):

    • Apelează appData.activateCloudDeviceSync(context: sceneContext)
    • Inițiază BT scanning
    • Inițiază CloudKit sync

Shutdown sequence

  • SHOULD: Finalizează sesiuni active la app terminate
  • MUST: Salvează Core Data context (saveIfNeeded())
  • SHOULD: Deconectează BT graceful

Bluetooth Connection Management

Connection state machine

notPresent
  ↓
peripheralNotConnected ← (user disconnect)
  ↓
peripheralConnectionPending
  ↓
peripheralConnected
  ↓
peripheralReady
  ↓
comunicating ↔ dataIsAvailable

Operations

// Conectare inițială
blutooth.connect(to peripheralUUID: UUID, type: Model)
// MUST: inițiază CBCentralManager scan
// MUST: se conectează la UUID specific
// SHOULD: timeout = 5s
// MUST: pe succes, tranzițe la peripheralConnected

// Deconectare
bluetooth.disconnect(from meter: Meter)
// MUST: anulează reconnect logic
// MUST: eliberează resurse
// MUST: marchez ca manual disconnect

// Auto-reconnect
bluetooth.autoReconnect(meter: Meter, backoff: Backoff)
// SHOULD: exponential backoff: 1s, 2s, 4s, 8s, max 60s
// MUST: anulează dacă utilizator disconnect manual
// MUST: max 3 retry-uri consecutive

Measurement Recording

Session lifecycle

let session = meter.startChargeRecord(for: device)
  1. Start: Crează ChargeRecord(sessionID: UUID(), startTime: now())
  2. Measure: La fiecare 1-2s: swift let measurement = meter.lastDataPoint session.addMeasurement(measurement)
  3. End: swift meter.endChargeRecord(session)
    • Calculează totalEnergy = ∑(V * A * Δt)
    • Marchez ca completed
    • Salvează în Core Data

Invarianţi

  • MUST: O sesiune activă per meter
  • MUST: Măsurătorile sunt cronologice
  • MUST: startTime <= now() <= (endTime || ∞)
  • MUST: Energia ≥ 0

Recording frequency

  • SHOULD: Măsurători la ~1Hz (1000ms interval)
  • MAY: Reduce frequency dacă battery low
  • SHOULD: Drop măsurători dacă queue > 100 items

Cloud Sync

Main concepts

  • DeviceSettings: entitate Core Data persistă MAC, name, temperature unit
  • CloudDeviceSettingsStore: wrapper pe NSManagedObjectContext
  • Rebuild version: cloudStoreRebuildVersion (curent: 3)

Sync flow

  1. Upload: Locale changes → Core Data → CloudKit swift cloudStore.upsertDeviceSettings( macAddress: "AA:BB:CC:DD:EE:FF", meterName: "Kitchen Meter", tc66TemperatureUnit: .celsius, connectionMetadata: ... )

    • MUST: salvează în Core Data sync
    • MUST: CloudKit container replica-ază automat
  2. Download: CloudKit changes → Core Data → UI

    • MUST: NSPersistentCloudKitContainer sincronizează automat
    • SHOULD: Refresh UI după fetch
  3. Conflict resolution: Dacă două modificări simultane

    • MUST: Mergi pe "last write wins" dacă timestamps diferă
    • SHOULD: Loghează conflictul pentru debugging

Discovery throttling

  • MUST: Max 1 discovery per device per 120s
  • SHOULD: Previne CloudKit thrashing din repeat BT advertisements
  • MUST: Reseta timer dacă device disconnect-ează

Device Settings Persistence

MAC address mapping

// CloudDeviceSettingsStore.swift
func upsertDeviceSettings(
    macAddress: String,
    meterName: String,
    tc66TemperatureUnit: TemperatureUnitPreference?,
    connectionMetadata: ConnectionMetadata?,
    discoveryMetadata: DiscoveryMetadata?
)
  • MUST: macAddress optional (datorită migration)
  • MUST: meterName unic pe meter (no duplicates)
  • SHOULD: connectionMetadata.expiresAt = now() + 24h

Legacy KV Store

NSUbiquitousKeyValueStore sincronizează: - MeterNames: {macAddress → meterName} - TC66TemperatureUnits: {macAddress → unit}

  • MUST: Menţinere backward compat
  • SHOULD: Migrate pe Core Data la first sync
  • MAY: Drop la următoarea major version

Error handling

Bluetooth errors

Error: BT peripheral not found
→ Retry connect cu backoff
→ Max 3 retries, apoi fallback offline mode

Error: Characteristic not found
→ Log error
→ Mark meter incompatible (UI warning)

Error: Read timeout
→ Retry measurement request
→ Increment timeout counter

CloudKit errors

Error: Network unavailable
→ Queue pending changes
→ Retry la next network change

Error: Conflict detected
→ Merge data (last write wins)
→ Retry sync

Error: Quota exceeded
→ Log error
→ Notify user (prune old data?)

Testare

Unit tests

test_appDataInitializes_CoreDataAndCloudKit()
test_bluetoothConnectInitiatesProperStateTransition()
test_bluetoothDisconnect_CleansUp()
test_autoReconnectBackoff_ExponentialScaling()
test_sessionStartCreatesValidRecord()
test_sessionEndCalculatesEnergy()
test_cloudSyncUpsert_SavesToCoreData()
test_discoveryThrottling_RespectsTiming()
test_conflictResolution_LastWriteWins()

Integration tests

  • [ ] Full app startup (BT + CloudKit)
  • [ ] Connect meter → start session → record measurements → end session
  • [ ] Disconnect meter → reconnect avec backoff
  • [ ] Device settings sync CloudKit → other device
  • [ ] Offline mode (queue changes, sync later)

Dependenţe

  • AppData: orchestrează tot
  • BluetoothManager: gestionează Core Bluetooth
  • CloudDeviceSettingsStore: Core Data + CloudKit
  • ChargeInsightsStore: sesiuni persistente
  • ConsumptionMonitorStore: monitorizare consum

Notes