diff --git a/app/src/main/java/com/example/medilightv2android/services/PostureTuningPrefs.kt b/app/src/main/java/com/example/medilightv2android/services/PostureTuningPrefs.kt new file mode 100644 index 0000000..4ffefe7 --- /dev/null +++ b/app/src/main/java/com/example/medilightv2android/services/PostureTuningPrefs.kt @@ -0,0 +1,80 @@ +/* + * PostureTuningPrefs — Posture / Walking detector 임계값 SharedPreferences 영속화. + * + * GreenZoneConstants 의 임계값들은 @Volatile var (메모리) 라 앱 재시작 시 default 리셋. + * 사용자가 dev slider 로 튜닝한 값을 유지하려면 prefs 에 저장 + 시작 시 로드. + * + * 사용: + * loadAll(context) — 앱/화면 시작 시 한 번 호출 → SharedPreferences 값 → GreenZoneConstants + * saveAll(context) — slider 변경 시 호출 → 현재 GreenZoneConstants → SharedPreferences + * resetAll(context) — 사용자가 reset 누름 → defaults 로 복원 + */ +package com.example.medilightv2android.services + +import android.content.Context +import com.example.medilightv2android.managers.GreenZoneConstants + +object PostureTuningPrefs { + + private const val PREF = "posture_tuning_prefs" + + // Defaults — GreenZoneConstants 의 초기값과 일치 유지 + private const val D_PITCH_LYING = 25f + private const val D_ACC_MIN = 0.8f + private const val D_ACC_MAX = 1.2f + private const val D_POLL_MS = 1000L + private const val D_WALK_PITCH = 45f + private const val D_WALK_START_WMEAN = 15f + private const val D_WALK_START_WSTD = 2f + private const val D_WALK_KEEP_WMEAN = 2f + private const val D_WALK_START_HOLD_MS = 6000L + private const val D_WALK_KEEP_HOLD_MS = 2000L + private const val D_WALK_WINDOW_MS = 1500L + + fun loadAll(context: Context) { + val p = context.applicationContext.getSharedPreferences(PREF, Context.MODE_PRIVATE) + GreenZoneConstants.pitchLyingThreshold = p.getFloat("pitchLying", D_PITCH_LYING) + GreenZoneConstants.postureAccMagMin = p.getFloat("accMin", D_ACC_MIN) + GreenZoneConstants.postureAccMagMax = p.getFloat("accMax", D_ACC_MAX) + GreenZoneConstants.posturePollIntervalMs = p.getLong("pollMs", D_POLL_MS) + GreenZoneConstants.walkingMinPitchDeg = p.getFloat("wPitch", D_WALK_PITCH) + GreenZoneConstants.walkingStartWmean = p.getFloat("wStartWmean", D_WALK_START_WMEAN) + GreenZoneConstants.walkingStartWstd = p.getFloat("wStartWstd", D_WALK_START_WSTD) + GreenZoneConstants.walkingKeepWmean = p.getFloat("wKeepWmean", D_WALK_KEEP_WMEAN) + GreenZoneConstants.walkingStartHoldMs = p.getLong("wStartHoldMs", D_WALK_START_HOLD_MS) + GreenZoneConstants.walkingKeepHoldMs = p.getLong("wKeepHoldMs", D_WALK_KEEP_HOLD_MS) + GreenZoneConstants.walkingWindowMs = p.getLong("wWindowMs", D_WALK_WINDOW_MS) + } + + fun saveAll(context: Context) { + context.applicationContext.getSharedPreferences(PREF, Context.MODE_PRIVATE).edit().apply { + putFloat("pitchLying", GreenZoneConstants.pitchLyingThreshold) + putFloat("accMin", GreenZoneConstants.postureAccMagMin) + putFloat("accMax", GreenZoneConstants.postureAccMagMax) + putLong("pollMs", GreenZoneConstants.posturePollIntervalMs) + putFloat("wPitch", GreenZoneConstants.walkingMinPitchDeg) + putFloat("wStartWmean", GreenZoneConstants.walkingStartWmean) + putFloat("wStartWstd", GreenZoneConstants.walkingStartWstd) + putFloat("wKeepWmean", GreenZoneConstants.walkingKeepWmean) + putLong("wStartHoldMs", GreenZoneConstants.walkingStartHoldMs) + putLong("wKeepHoldMs", GreenZoneConstants.walkingKeepHoldMs) + putLong("wWindowMs", GreenZoneConstants.walkingWindowMs) + apply() + } + } + + fun resetAll(context: Context) { + GreenZoneConstants.pitchLyingThreshold = D_PITCH_LYING + GreenZoneConstants.postureAccMagMin = D_ACC_MIN + GreenZoneConstants.postureAccMagMax = D_ACC_MAX + GreenZoneConstants.posturePollIntervalMs = D_POLL_MS + GreenZoneConstants.walkingMinPitchDeg = D_WALK_PITCH + GreenZoneConstants.walkingStartWmean = D_WALK_START_WMEAN + GreenZoneConstants.walkingStartWstd = D_WALK_START_WSTD + GreenZoneConstants.walkingKeepWmean = D_WALK_KEEP_WMEAN + GreenZoneConstants.walkingStartHoldMs = D_WALK_START_HOLD_MS + GreenZoneConstants.walkingKeepHoldMs = D_WALK_KEEP_HOLD_MS + GreenZoneConstants.walkingWindowMs = D_WALK_WINDOW_MS + saveAll(context) + } +} diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt index a98aad3..1b91b34 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt @@ -712,7 +712,9 @@ fun PiezoMonitoringView(appState: AppState) { } // Posture event 로깅 — 검증용 CSV (앱 진입 시 새 파일) + // + 사용자 튜닝 임계값 SharedPreferences 에서 로드 LaunchedEffect(Unit) { + com.example.medilightv2android.services.PostureTuningPrefs.loadAll(context) com.example.medilightv2android.services.PostureEventLogger.startSession(context) } @@ -1892,7 +1894,8 @@ fun PiezoMonitoringView(appState: AppState) { } Slider( value = pitchThrVal, - onValueChange = { pitchThrVal = it; GreenZoneConstants.pitchLyingThreshold = it }, + onValueChange = { pitchThrVal = it; GreenZoneConstants.pitchLyingThreshold = it + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 10f..70f, steps = 11, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary) @@ -1968,6 +1971,7 @@ fun PiezoMonitoringView(appState: AppState) { onValueChange = { pollIntervalVal = it GreenZoneConstants.posturePollIntervalMs = (it * 1000).toLong() + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 1f..15f, // 1초 ~ 15초 steps = 0, @@ -1987,6 +1991,7 @@ fun PiezoMonitoringView(appState: AppState) { } Slider(value = walkPitchVal, onValueChange = { walkPitchVal = it; GreenZoneConstants.walkingMinPitchDeg = it + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 20f..70f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)) var walkStartWmean by remember { mutableStateOf(GreenZoneConstants.walkingStartWmean) } @@ -1997,6 +2002,7 @@ fun PiezoMonitoringView(appState: AppState) { } Slider(value = walkStartWmean, onValueChange = { walkStartWmean = it; GreenZoneConstants.walkingStartWmean = it + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 1f..20f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)) var walkStartWstd by remember { mutableStateOf(GreenZoneConstants.walkingStartWstd) } @@ -2007,6 +2013,7 @@ fun PiezoMonitoringView(appState: AppState) { } Slider(value = walkStartWstd, onValueChange = { walkStartWstd = it; GreenZoneConstants.walkingStartWstd = it + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 0.5f..10f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)) var walkKeepWmean by remember { mutableStateOf(GreenZoneConstants.walkingKeepWmean) } @@ -2017,6 +2024,7 @@ fun PiezoMonitoringView(appState: AppState) { } Slider(value = walkKeepWmean, onValueChange = { walkKeepWmean = it; GreenZoneConstants.walkingKeepWmean = it + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 0.5f..30f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)) // Walking 시간 변수 (sec) @@ -2028,6 +2036,7 @@ fun PiezoMonitoringView(appState: AppState) { } Slider(value = walkStartHold, onValueChange = { walkStartHold = it; GreenZoneConstants.walkingStartHoldMs = (it * 1000).toLong() + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 0.2f..5f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)) var walkKeepHold by remember { mutableStateOf(GreenZoneConstants.walkingKeepHoldMs / 1000f) } @@ -2038,6 +2047,7 @@ fun PiezoMonitoringView(appState: AppState) { } Slider(value = walkKeepHold, onValueChange = { walkKeepHold = it; GreenZoneConstants.walkingKeepHoldMs = (it * 1000).toLong() + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 0.5f..10f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)) var walkWindow by remember { mutableStateOf(GreenZoneConstants.walkingWindowMs / 1000f) } @@ -2048,6 +2058,7 @@ fun PiezoMonitoringView(appState: AppState) { } Slider(value = walkWindow, onValueChange = { walkWindow = it; GreenZoneConstants.walkingWindowMs = (it * 1000).toLong() + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 0.5f..5f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)) HorizontalDivider(modifier = Modifier.padding(vertical = dividerPadding)) @@ -2063,6 +2074,7 @@ fun PiezoMonitoringView(appState: AppState) { } Slider(value = accMagMin, onValueChange = { accMagMin = it; GreenZoneConstants.postureAccMagMin = it + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 0.3f..1.0f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)) var accMagMax by remember { mutableStateOf(GreenZoneConstants.postureAccMagMax) } @@ -2073,10 +2085,27 @@ fun PiezoMonitoringView(appState: AppState) { } Slider(value = accMagMax, onValueChange = { accMagMax = it; GreenZoneConstants.postureAccMagMax = it + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 1.0f..3.0f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)) HorizontalDivider(modifier = Modifier.padding(vertical = dividerPadding)) + // Reset to defaults — 모든 threshold 를 default 로 복원 + prefs 저장. + OutlinedButton( + onClick = { + com.example.medilightv2android.services.PostureTuningPrefs.resetAll(context) + android.widget.Toast.makeText(context, + "Defaults restored — close & reopen Settings to refresh sliders", + android.widget.Toast.LENGTH_LONG).show() + }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.outlinedButtonColors(contentColor = Color(0xFFE53935)) + ) { + Text("Reset Thresholds to Defaults", fontSize = 13.sp, fontWeight = FontWeight.SemiBold) + } + + HorizontalDivider(modifier = Modifier.padding(vertical = dividerPadding)) + // Verification annotation buttons — ground truth marker. // 누르는 순간 timestamp 가 CSV 에 ANNOTATION row 로 기록 → 알고리즘 detection 과 대조. // 파일: Downloads/VesiScan_Posture/yyyy-MM-dd_HHmmss.csv