USB-Meter / Documentation / API Reference / ChargeCurveIsolation.md
1 contributor
337 lines | 8.774kb

Charge Curve Isolation Operation

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

Responsabilități

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

Problema

Raw data from meter: 00:00 - 0.0W (pre-charge noise) 00:30 - 0.5W (background draw) 01:00 - 15.0W ← REAL START 01:30 - 18.5W 02:00 - 16.2W ... 05:00 - 2.5W (charging optimizer active) 05:30 - 1.0W 06:00 - 0.2W ← REAL END 06:15 - 0.0W (post-charge, device removed)

Sarcini: 1. Find real start (@ 01:00) 2. Find real end (@ 06:00) 3. Extract interval [01:00, 06:00] 4. Ignore pre/post noise

Invarianţi

  • MUST: real_start_time > measurement_start_time (or equal)
  • MUST: real_end_time <= measurement_end_time (or equal)
  • MUST: real_start < real_end
  • MUST: Threshold determination trebuie consistent (nu random)
  • SHOULD: Isolated curve ≥ 90% din total recorded measurements
  • MAY: Isolated curve poate fi < 10% din total (very short charge)

Detection algorithm

Power threshold method

func findChargeStart(measurements: [Measurement]) -> Int? {
    let powerThreshold = 5.0 // Watts
    let minConsecutive = 3 // Measure must sustain > threshold for 3 samples
    
    var sustainCount = 0
    for (i, measurement) in measurements.enumerated() {
        if measurement.power > powerThreshold {
            sustainCount += 1
            if sustainCount >= minConsecutive {
                return i - minConsecutive + 1
            }
        } else {
            sustainCount = 0
        }
    }
    return nil
}

func findChargeEnd(measurements: [Measurement]) -> Int? {
    let powerThreshold = 1.0 // Watts (lower at end, trickle charge)
    let minConsecutive = 5 // Stay below threshold for 5 samples
    
    var sustainCount = 0
    for i in stride(from: measurements.count - 1, through: 0, by: -1) {
        let measurement = measurements[i]
        if measurement.power < powerThreshold {
            sustainCount += 1
            if sustainCount >= minConsecutive {
                return i + 1
            }
        } else {
            sustainCount = 0
        }
    }
    return nil
}

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

Battery level method (fallback)

Dacă dispositiv raportează procent:

func findChargeStartByBattery(measurements: [Measurement]) -> Int? {
    // Find first battery level jump
    for i in 1..<measurements.count {
        let prev = measurements[i-1]
        let curr = measurements[i]
        
        let batteryChange = (curr.batteryPercent ?? 0) - (prev.batteryPercent ?? 0)
        if batteryChange > 1.0 {  // >1% jump = real charge
            return i - 1
        }
    }
    return nil
}

func findChargeEndByBattery(measurements: [Measurement]) -> Int? {
    // Find last battery level change
    for i in stride(from: measurements.count - 1, through: 1, by: -1) {
        let prev = measurements[i-1]
        let curr = measurements[i]
        
        let batteryChange = (curr.batteryPercent ?? 0) - (prev.batteryPercent ?? 0)
        if batteryChange > 0.5 {  // Still charging
            return i
        }
    }
    return nil
}

Combined method (best)

func isolateChargeRange(measurements: [Measurement]) -> (start: Int, end: Int)? {
    // Primary: use power threshold
    guard var start = findChargeStart(measurements: measurements),
          var end = findChargeEnd(measurements: measurements) else {
        // Fallback: use battery level
        guard var start = findChargeStartByBattery(measurements: measurements),
              var end = findChargeEndByBattery(measurements: measurements) else {
            return nil
        }
        return (start, end)
    }
    
    // Validate result
    guard start < end && (end - start) >= 10 else {
        return nil  // Too short (< 10 seconds @ 1Hz)
    }
    
    return (start, end)
}

API Public

Metode

// Detection
func findChargeStart(measurements: [Measurement]) -> Int?
func findChargeEnd(measurements: [Measurement]) -> Int?
func isolateChargeRange(measurements: [Measurement]) -> (start: Int, end: Int)?

// Extraction
func extractIsolatedCurve(measurements: [Measurement]) 
    -> [Measurement]?

// Validation
func isValidChargeRange(start: Int, end: Int, total: Int) -> Bool
func estimateNoisePercentage(measurements: [Measurement]) -> Double

Rezultat

struct IsolatedChargeData {
    let originalMeasurements: [Measurement]
    let isolatedMeasurements: [Measurement]
    let startIndex: Int
    let endIndex: Int
    let realStartTime: Date
    let realEndTime: Date
    let noisePercentagePre: Double
    let noisePercentagePost: Double
    
    var effectiveChargeTime: TimeInterval {
        realEndTime.timeIntervalSince(realStartTime)
    }
}

Comportamente critice

Threshold sensitivity

Problem: threshold prea mare Threshold: 10W Reality: charger 12W on iPhone 15 Result: detectează corect ✓ But: low-power charger 5W → missed (fails)

Problem: threshold prea mic Threshold: 0.5W Reality: background AC noise 1.5W Result: detectează noise, invalid start (fails)

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

Short charges

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

SHOULD: Accept short charges MUST: Still isolate pre/post

Noisy environment

Pre-charge noise: 2W baseline (poor outlet)
Charge power: 18W
Power jump: 16W (obvious)
⟹ Easy to detect

Vs.

Pre-charge noise: 8W (strong interference)
Charge power: 10W
Power jump: 2W (ambiguous!)
⟹ Hard, need fallback (battery level)

SHOULD: Use combined method (power + battery) MUST: Fall back if ambiguous

Charging profile changes

Curve:
- 0-1h: 20W (fast charge phase)
- 1-2h: 10W (medium phase)
- 2-3h: 3W (taper phase)
- 3h+: 0.5W (trickle/done)

Isolated region: 0h-3h (valid)
Threshold: 1W (catches all phases)
⟹ Correct

MUST: Detect end even with tapering

Testare

Unit tests

test_findChargeStart_ValidThreshold()
test_findChargeStart_NoiseIgnored()
test_findChargeEnd_ValidThreshold()
test_findChargeEnd_WithTrickleCharge()
test_isolateChargeRange_ValidData()
test_isolateChargeRange_TooShortIgnored()
test_isolateChargeRange_HighNoise_FallbackToBattery()
test_isValidChargeRange_StartBeforeEnd()
test_isValidChargeRange_MinimumDuration()
test_estimateNoisePercentage()
test_extractIsolatedCurve_PreservesMeasurements()

Integration tests

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

Edge cases

No charge (meter ON, device not charging)

Measurements: all ~0W
Start detection: fails (no power jump)
End detection: fails (no charging detected)
Result: (nil, nil)
⟹ Skip session (valid, no charging occurred)

MUST: Handle gracefully

Meter added after charge started

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

SHOULD: Use what data we have MAY: Mark as "incomplete start"

USB power delivery with negotiation

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

SHOULD: OK, negotiation time negligible

Dependencies

Notes

  • Power threshold: device/charger dependent, may need tuning
  • Battery level fallback: only if power method ambiguous
  • Resolution: 1Hz (1000ms), ~1W precision typical
  • Legat: Charge Session Integrity