feat: developer mode, SWEEP placement, gradient guide, spot flicker fix

- Developer mode: 캐릭터 3탭 토글, dev-only settings/graphs/logs
- SWEEP placement (CKLaw): BladderSphereSeek circle fit + RTracker
  + argmax + coronal lateral + R-peak gate GREEN lock
- Gradient placement: weighted center gradient 기반 상하 판정,
  방향 로직 수정 (ch0=위, ch3=아래), 초기 멘트 "치골 위에 배치"
- Spot 측정: isSpotInProgress 플래그로 중간값 화면 깜빡임 제거
- 좌우 애매한 ↔ 멘트 제거 → 구체적 방향 또는 상태 안내

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 11:32:03 +09:00
parent 4b74b88f76
commit 53a07195f8
7 changed files with 709 additions and 230 deletions
@@ -0,0 +1,72 @@
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<Double>()
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
}
}
}