package com.example.medilightv2android.managers import kotlin.math.abs /** * R-fit trend tracker — watches per-frame sphere-fit radius and classifies: * RISING — sliding toward bladder centre (R increasing) * PEAK — at / near session maximum R * FALLING — sliding past centre (R decreasing) * NOISE — fluctuating without clear trend * IDLE — no valid fit (too few channels) * * Ported from CKLaw BladderSeekRTracker.kt. */ class BladderSeekRTracker( private val historySize: Int = 8, private val peakToleranceMm: Double = 1.5, private val noiseDeltaMm: Double = 0.4, ) { enum class Trend { RISING, PEAK, FALLING, NOISE, IDLE } data class Status( val currentR: Double?, val bestR: Double, val trend: Trend, val improvementMm: Double, val hintEn: String, ) private val history = ArrayDeque() var bestR: Double = 0.0 private set fun reset() { history.clear() bestR = 0.0 } fun submit(r: Double?): Status { if (r == null || r.isNaN() || r <= 0.0) { return Status(null, bestR, Trend.IDLE, 0.0, "Not enough channels — check signal") } history.addLast(r) while (history.size > historySize) history.removeFirst() if (r > bestR) bestR = r val trend = computeTrend(r) val gap = r - bestR val en = when (trend) { Trend.RISING -> "Getting closer (R=${"%.0f".format(r)}mm)" Trend.PEAK -> "At peak (R=${"%.0f".format(r)}mm)" Trend.FALLING -> "Past peak — reverse (best ${"%.0f".format(bestR)}mm)" Trend.NOISE -> "Searching (R=${"%.0f".format(r)}mm)" Trend.IDLE -> "" } return Status(r, bestR, trend, gap, en) } private fun computeTrend(currentR: Double): Trend { if (history.size < 3) return Trend.NOISE if (bestR - currentR < peakToleranceMm) return Trend.PEAK val prev = history.toList().dropLast(1).takeLast(3) val prevMean = prev.average() val delta = currentR - prevMean return when { abs(delta) < noiseDeltaMm -> Trend.NOISE delta > 0 -> Trend.RISING else -> Trend.FALLING } } }