USB-Meter / Documentation / API Reference / ChargeCurveIsolation.md
Newer Older
337 lines | 8.774kb
Bogdan Timofte authored 2 weeks ago
1
# Charge Curve Isolation Operation
2

            
3
Izolarea porţiunii relevante a curbei de încărcare: eliminarea zgomotului pre/post-charging.
4

            
5
## Responsabilități
6

            
7
- Detectarea punctului de start real al încărcării (0 watts baseline)
8
- Detectarea punctului de end real al încărcării (100% sau limit)
9
- Eliminarea zgomotului (ambient AC, meter offset)
10
- Determinarea timpului efectiv de charging
11

            
12
## Problema
13

            
14
**Raw data from meter:**
15
```
16
00:00 - 0.0W (pre-charge noise)
17
00:30 - 0.5W (background draw)
18
01:00 - 15.0W ← REAL START
19
01:30 - 18.5W
20
02:00 - 16.2W
21
...
22
05:00 - 2.5W (charging optimizer active)
23
05:30 - 1.0W
24
06:00 - 0.2W ← REAL END
25
06:15 - 0.0W (post-charge, device removed)
26
```
27

            
28
**Sarcini:**
29
1. Find real start (@ 01:00)
30
2. Find real end (@ 06:00)
31
3. Extract interval [01:00, 06:00]
32
4. Ignore pre/post noise
33

            
34
## Invarianţi
35

            
36
- **MUST**: `real_start_time > measurement_start_time` (or equal)
37
- **MUST**: `real_end_time <= measurement_end_time` (or equal)
38
- **MUST**: `real_start < real_end`
39
- **MUST**: Threshold determination trebuie consistent (nu random)
40
- **SHOULD**: Isolated curve ≥ 90% din total recorded measurements
41
- **MAY**: Isolated curve poate fi < 10% din total (very short charge)
42

            
43
## Detection algorithm
44

            
45
### Power threshold method
46

            
47
```swift
48
func findChargeStart(measurements: [Measurement]) -> Int? {
49
    let powerThreshold = 5.0 // Watts
50
    let minConsecutive = 3 // Measure must sustain > threshold for 3 samples
51

            
52
    var sustainCount = 0
53
    for (i, measurement) in measurements.enumerated() {
54
        if measurement.power > powerThreshold {
55
            sustainCount += 1
56
            if sustainCount >= minConsecutive {
57
                return i - minConsecutive + 1
58
            }
59
        } else {
60
            sustainCount = 0
61
        }
62
    }
63
    return nil
64
}
65

            
66
func findChargeEnd(measurements: [Measurement]) -> Int? {
67
    let powerThreshold = 1.0 // Watts (lower at end, trickle charge)
68
    let minConsecutive = 5 // Stay below threshold for 5 samples
69

            
70
    var sustainCount = 0
71
    for i in stride(from: measurements.count - 1, through: 0, by: -1) {
72
        let measurement = measurements[i]
73
        if measurement.power < powerThreshold {
74
            sustainCount += 1
75
            if sustainCount >= minConsecutive {
76
                return i + 1
77
            }
78
        } else {
79
            sustainCount = 0
80
        }
81
    }
82
    return nil
83
}
84
```
85

            
86
**Thresholds:**
87
- Start: 5W (must exceed noise level)
88
- End: 1W (lower, allows trickle detection)
89
- Sustain: 3 samples @start, 5 @end (hysteresis)
90

            
91
### Battery level method (fallback)
92

            
93
Dacă dispositiv raportează procent:
94

            
95
```swift
96
func findChargeStartByBattery(measurements: [Measurement]) -> Int? {
97
    // Find first battery level jump
98
    for i in 1..<measurements.count {
99
        let prev = measurements[i-1]
100
        let curr = measurements[i]
101

            
102
        let batteryChange = (curr.batteryPercent ?? 0) - (prev.batteryPercent ?? 0)
103
        if batteryChange > 1.0 {  // >1% jump = real charge
104
            return i - 1
105
        }
106
    }
107
    return nil
108
}
109

            
110
func findChargeEndByBattery(measurements: [Measurement]) -> Int? {
111
    // Find last battery level change
112
    for i in stride(from: measurements.count - 1, through: 1, by: -1) {
113
        let prev = measurements[i-1]
114
        let curr = measurements[i]
115

            
116
        let batteryChange = (curr.batteryPercent ?? 0) - (prev.batteryPercent ?? 0)
117
        if batteryChange > 0.5 {  // Still charging
118
            return i
119
        }
120
    }
121
    return nil
122
}
123
```
124

            
125
### Combined method (best)
126

            
127
```swift
128
func isolateChargeRange(measurements: [Measurement]) -> (start: Int, end: Int)? {
129
    // Primary: use power threshold
130
    guard var start = findChargeStart(measurements: measurements),
131
          var end = findChargeEnd(measurements: measurements) else {
132
        // Fallback: use battery level
133
        guard var start = findChargeStartByBattery(measurements: measurements),
134
              var end = findChargeEndByBattery(measurements: measurements) else {
135
            return nil
136
        }
137
        return (start, end)
138
    }
139

            
140
    // Validate result
141
    guard start < end && (end - start) >= 10 else {
142
        return nil  // Too short (< 10 seconds @ 1Hz)
143
    }
144

            
145
    return (start, end)
146
}
147
```
148

            
149
## API Public
150

            
151
### Metode
152

            
153
```swift
154
// Detection
155
func findChargeStart(measurements: [Measurement]) -> Int?
156
func findChargeEnd(measurements: [Measurement]) -> Int?
157
func isolateChargeRange(measurements: [Measurement]) -> (start: Int, end: Int)?
158

            
159
// Extraction
160
func extractIsolatedCurve(measurements: [Measurement])
161
    -> [Measurement]?
162

            
163
// Validation
164
func isValidChargeRange(start: Int, end: Int, total: Int) -> Bool
165
func estimateNoisePercentage(measurements: [Measurement]) -> Double
166
```
167

            
168
### Rezultat
169

            
170
```swift
171
struct IsolatedChargeData {
172
    let originalMeasurements: [Measurement]
173
    let isolatedMeasurements: [Measurement]
174
    let startIndex: Int
175
    let endIndex: Int
176
    let realStartTime: Date
177
    let realEndTime: Date
178
    let noisePercentagePre: Double
179
    let noisePercentagePost: Double
180

            
181
    var effectiveChargeTime: TimeInterval {
182
        realEndTime.timeIntervalSince(realStartTime)
183
    }
184
}
185
```
186

            
187
## Comportamente critice
188

            
189
### Threshold sensitivity
190

            
191
**Problem: threshold prea mare**
192
```
193
Threshold: 10W
194
Reality: charger 12W on iPhone 15
195
Result: detectează corect ✓
196
But: low-power charger 5W → missed (fails)
197
```
198

            
199
**Problem: threshold prea mic**
200
```
201
Threshold: 0.5W
202
Reality: background AC noise 1.5W
203
Result: detectează noise, invalid start (fails)
204
```
205

            
206
**Solution:** Adaptive threshold:
207
```swift
208
let baselineNoise = measurements.prefix(10).map { $0.power }.max() ?? 0
209
let powerThreshold = baselineNoise * 1.5 + 2.0  // 50% margin + 2W
210
```
211

            
212
### Short charges
213

            
214
```
215
Charge duration: 30 seconds (e.g., quick top-up)
216
Measurements: 30 samples @ 1Hz
217
Noise isolation: 3 samples start + 5 samples end = 8 samples
218
Remaining: 22 samples (73% valid)
219
⟹ Valid
220
```
221

            
222
**SHOULD**: Accept short charges
223
**MUST**: Still isolate pre/post
224

            
225
### Noisy environment
226

            
227
```
228
Pre-charge noise: 2W baseline (poor outlet)
229
Charge power: 18W
230
Power jump: 16W (obvious)
231
⟹ Easy to detect
232

            
233
Vs.
234

            
235
Pre-charge noise: 8W (strong interference)
236
Charge power: 10W
237
Power jump: 2W (ambiguous!)
238
⟹ Hard, need fallback (battery level)
239
```
240

            
241
**SHOULD**: Use combined method (power + battery)
242
**MUST**: Fall back if ambiguous
243

            
244
### Charging profile changes
245

            
246
```
247
Curve:
248
- 0-1h: 20W (fast charge phase)
249
- 1-2h: 10W (medium phase)
250
- 2-3h: 3W (taper phase)
251
- 3h+: 0.5W (trickle/done)
252

            
253
Isolated region: 0h-3h (valid)
254
Threshold: 1W (catches all phases)
255
⟹ Correct
256
```
257

            
258
**MUST**: Detect end even with tapering
259

            
260
## Testare
261

            
262
### Unit tests
263

            
264
```swift
265
test_findChargeStart_ValidThreshold()
266
test_findChargeStart_NoiseIgnored()
267
test_findChargeEnd_ValidThreshold()
268
test_findChargeEnd_WithTrickleCharge()
269
test_isolateChargeRange_ValidData()
270
test_isolateChargeRange_TooShortIgnored()
271
test_isolateChargeRange_HighNoise_FallbackToBattery()
272
test_isValidChargeRange_StartBeforeEnd()
273
test_isValidChargeRange_MinimumDuration()
274
test_estimateNoisePercentage()
275
test_extractIsolatedCurve_PreservesMeasurements()
276
```
277

            
278
### Integration tests
279

            
280
- [ ] Raw data: 6h recording, 15 min real charge → correctly isolated
281
- [ ] High pre-charge noise (8W) → still detected
282
- [ ] Low power charger (5W) → detected
283
- [ ] Multiple charge phases → end detected at trickle
284
- [ ] Isolated curve used for energy calculation
285

            
286
## Edge cases
287

            
288
### No charge (meter ON, device not charging)
289

            
290
```
291
Measurements: all ~0W
292
Start detection: fails (no power jump)
293
End detection: fails (no charging detected)
294
Result: (nil, nil)
295
⟹ Skip session (valid, no charging occurred)
296
```
297

            
298
**MUST**: Handle gracefully
299

            
300
### Meter added after charge started
301

            
302
```
303
Recording: 03:00-07:00 (4 hours)
304
Real charge: 01:00-06:00 (5 hours, but missed first 2h)
305
Measurements: 10W constant (already in charge phase)
306
Start: 03:00 (best guess)
307
End: 06:00 (detected from power drop)
308
⟹ Partial isolation OK
309
```
310

            
311
**SHOULD**: Use what data we have
312
**MAY**: Mark as "incomplete start"
313

            
314
### USB power delivery with negotiation
315

            
316
```
317
Time 00:00: Meter ON, no device
318
Time 00:10: Device plugged in, negotiation 100ms
319
Time 00:12: Charging starts (power jump 20W)
320
Samples: ?, ?, [19.5W, 19.8W, ...] ← isolated start
321
⟹ Correct
322
```
323

            
324
**SHOULD**: OK, negotiation time negligible
325

            
326
## Dependencies
327

            
328
- [Charging Monitoring](./ChargingMonitoring.md): input measurements
329
- [Charge Curve Storage](./ChargeCurveStorage.md): store isolated data
330
- Core Data: battery levels (fallback)
331

            
332
## Notes
333

            
334
- Power threshold: device/charger dependent, may need tuning
335
- Battery level fallback: only if power method ambiguous
336
- Resolution: 1Hz (1000ms), ~1W precision typical
337
- Legat: [Charge Session Integrity](../Charge%20Session%20Integrity%20and%20Conflict%20Healing.md)