fix+UX: BLE 중복수신 수정, peak detection 개선, UI 대폭 정리
BLE: - GATT 다중연결 race condition 수정 (reconnectConnecting 플래그) - onCharacteristicChanged API 레벨 분기 (dual callback 방지) - DeviceScan: isConnected → isServiceReady 전환 (서비스 준비 후 화면전환) - Placement: mpa/maa 500ms 딜레이 (power on 대기) - Disconnect & Unbond 기능 추가 (msr? + removeBond + 저장정보 삭제) - autoConnect 삭제 (HomeView) Algorithm: - 후위벽 피크 >= threshold 조건 추가 (false positive 차단) - low-echo 분석범위 80샘플 제한 (뒤쪽 노이즈 무시) - urineLen: post-ant-1 → post-ant 수정 - Placement: UrinAI fallback 제거 (PiezoEchoAnalyzer만 사용) - 기기 프리셋 autoDetect index 7→8 수정 (VBT26040Nxx) - dps 기본값 1.771 → 1.968 UI: - 도넛차트: Home버튼 삭제, Auto/Catheter count/History 삭제 - 3버튼 레이아웃 (Catheterize/Auto/Spot) - 배터리 아이콘 (60+초록/30+주황/빨강) + 설정 톱니바퀴 분리 - SensorSelect/Consent 페이지 스킵 - 온보딩: VesiScan-Basic, NIRS 페이지 삭제 - MaxVolume: Step 표시 삭제, mL 오른쪽 배치 - BackHandler 전체 재정리 (자연스러운 뒤로가기) - BLE 신호 기준 완화 (Good≥-80, Fair≥-90) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -49,7 +49,6 @@ class AppState(private val context: Context) {
|
|||||||
|
|
||||||
currentScreen = when {
|
currentScreen = when {
|
||||||
!storage.hasCompletedOnboarding -> AppScreen.ONBOARDING
|
!storage.hasCompletedOnboarding -> AppScreen.ONBOARDING
|
||||||
!storage.hasConsented -> AppScreen.CONSENT
|
|
||||||
!storage.isRegistered -> AppScreen.REGISTRATION
|
!storage.isRegistered -> AppScreen.REGISTRATION
|
||||||
!storage.hasPin -> AppScreen.PIN_SETUP
|
!storage.hasPin -> AppScreen.PIN_SETUP
|
||||||
else -> AppScreen.PIN_ENTRY
|
else -> AppScreen.PIN_ENTRY
|
||||||
@@ -58,7 +57,8 @@ class AppState(private val context: Context) {
|
|||||||
|
|
||||||
fun completeOnboarding() {
|
fun completeOnboarding() {
|
||||||
storage.hasCompletedOnboarding = true
|
storage.hasCompletedOnboarding = true
|
||||||
currentScreen = AppScreen.CONSENT
|
storage.hasConsented = true
|
||||||
|
currentScreen = AppScreen.REGISTRATION
|
||||||
}
|
}
|
||||||
|
|
||||||
fun completeConsent() {
|
fun completeConsent() {
|
||||||
@@ -93,7 +93,9 @@ class AppState(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun startFromHome() {
|
fun startFromHome() {
|
||||||
currentScreen = AppScreen.SENSOR_SELECT
|
sensorMode = SensorMode.PIEZO
|
||||||
|
storage.sensorMode = SensorMode.PIEZO
|
||||||
|
currentScreen = AppScreen.DEVICE_SCAN
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectSensor(mode: SensorMode) {
|
fun selectSensor(mode: SensorMode) {
|
||||||
@@ -103,7 +105,7 @@ class AppState(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun backToSensorSelect() {
|
fun backToSensorSelect() {
|
||||||
currentScreen = AppScreen.SENSOR_SELECT
|
currentScreen = AppScreen.HOME
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deviceConnected() {
|
fun deviceConnected() {
|
||||||
|
|||||||
@@ -61,19 +61,25 @@ fun MedilightApp(modifier: Modifier = Modifier) {
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val appState = remember { AppState(context) }
|
val appState = remember { AppState(context) }
|
||||||
|
|
||||||
// Android 뒤로가기 = 앱 내 뒤로가기
|
// Android 뒤로가기 = 바로 전 화면
|
||||||
BackHandler(enabled = appState.currentScreen != AppScreen.HOME) {
|
BackHandler(enabled = appState.currentScreen != AppScreen.HOME
|
||||||
|
&& appState.currentScreen != AppScreen.PIN_ENTRY) {
|
||||||
when (appState.currentScreen) {
|
when (appState.currentScreen) {
|
||||||
AppScreen.SENSOR_SELECT -> appState.currentScreen = AppScreen.HOME
|
// 메인 플로우: Home → DeviceScan → Personalization → Placement → Monitoring
|
||||||
AppScreen.DEVICE_SCAN -> appState.currentScreen = AppScreen.SENSOR_SELECT
|
AppScreen.DEVICE_SCAN -> appState.currentScreen = AppScreen.HOME
|
||||||
AppScreen.PIEZO_PERSONALIZATION -> appState.currentScreen = AppScreen.DEVICE_SCAN
|
AppScreen.PIEZO_PERSONALIZATION -> appState.currentScreen = AppScreen.DEVICE_SCAN
|
||||||
AppScreen.PLACEMENT_GUIDE -> appState.goHome()
|
AppScreen.PLACEMENT_GUIDE -> appState.currentScreen = AppScreen.PIEZO_PERSONALIZATION
|
||||||
AppScreen.PIEZO_MONITORING -> appState.currentScreen = AppScreen.PLACEMENT_GUIDE
|
AppScreen.PIEZO_MONITORING -> appState.currentScreen = AppScreen.PLACEMENT_GUIDE
|
||||||
AppScreen.VIVAMYO_MONITORING -> appState.goHome()
|
// 모니터링 하위 화면 → 모니터링
|
||||||
AppScreen.MEASUREMENT_HISTORY -> appState.currentScreen = AppScreen.PIEZO_MONITORING
|
AppScreen.MEASUREMENT_HISTORY -> appState.currentScreen = AppScreen.PIEZO_MONITORING
|
||||||
AppScreen.VOIDING_DIARY -> appState.currentScreen = AppScreen.PIEZO_MONITORING
|
AppScreen.VOIDING_DIARY -> appState.currentScreen = AppScreen.PIEZO_MONITORING
|
||||||
AppScreen.REMINDER_SETTINGS -> appState.currentScreen = AppScreen.PIEZO_MONITORING
|
AppScreen.REMINDER_SETTINGS -> appState.currentScreen = AppScreen.PIEZO_MONITORING
|
||||||
AppScreen.URINE_CAMERA -> appState.currentScreen = AppScreen.PIEZO_MONITORING
|
AppScreen.URINE_CAMERA -> appState.currentScreen = AppScreen.PIEZO_MONITORING
|
||||||
|
// 기타
|
||||||
|
AppScreen.SENSOR_SELECT -> appState.currentScreen = AppScreen.HOME
|
||||||
|
AppScreen.VIVAMYO_MONITORING -> appState.currentScreen = AppScreen.HOME
|
||||||
|
AppScreen.NIRS_CALIBRATION -> appState.currentScreen = AppScreen.DEVICE_SCAN
|
||||||
|
// 온보딩/등록 (뒤로가기 막지 않되 Home으로)
|
||||||
else -> appState.currentScreen = AppScreen.HOME
|
else -> appState.currentScreen = AppScreen.HOME
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,6 +166,12 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
// Connection
|
// Connection
|
||||||
fun connect(bleDevice: BleDevice) {
|
fun connect(bleDevice: BleDevice) {
|
||||||
stopScan()
|
stopScan()
|
||||||
|
cancelAutoReconnect()
|
||||||
|
reconnectScanCallback?.let { cb ->
|
||||||
|
try { bluetoothAdapter?.bluetoothLeScanner?.stopScan(cb) } catch (_: Exception) {}
|
||||||
|
reconnectScanCallback = null
|
||||||
|
}
|
||||||
|
reconnectConnecting = false
|
||||||
logd { "Connecting to ${bleDevice.name} (${bleDevice.address})" }
|
logd { "Connecting to ${bleDevice.name} (${bleDevice.address})" }
|
||||||
|
|
||||||
// Clean up any existing GATT connection
|
// Clean up any existing GATT connection
|
||||||
@@ -241,6 +247,55 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
bluetoothGatt?.disconnect()
|
bluetoothGatt?.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun disconnectAndUnbond() {
|
||||||
|
isUserDisconnect = true
|
||||||
|
cancelAutoReconnect()
|
||||||
|
stopWatchdog()
|
||||||
|
stopBatteryPolling()
|
||||||
|
connectionTimer?.let { handler.removeCallbacks(it) }
|
||||||
|
connectionTimer = null
|
||||||
|
|
||||||
|
// 기기에 본딩 삭제 + 재부팅 명령 (msr?)
|
||||||
|
sendRaw(CRC16.buildCommandASCII("msr", " "))
|
||||||
|
debugLogger.info("TX msr? unbond & reboot device")
|
||||||
|
|
||||||
|
val device = bluetoothGatt?.device
|
||||||
|
|
||||||
|
// 기기가 재부팅되므로 약간 대기 후 GATT 정리
|
||||||
|
handler.postDelayed({
|
||||||
|
bluetoothGatt?.let { gatt ->
|
||||||
|
try { gatt.disconnect(); gatt.close() } catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
bluetoothGatt = null
|
||||||
|
txCharacteristic = null
|
||||||
|
rxCharacteristic = null
|
||||||
|
isConnected.value = false
|
||||||
|
isServiceReady.value = false
|
||||||
|
connectedDeviceName.value = ""
|
||||||
|
batteryLevel.value = 0
|
||||||
|
lastConnectedAddress = null
|
||||||
|
|
||||||
|
// Android 측 본딩 정보도 삭제
|
||||||
|
device?.let { dev ->
|
||||||
|
try {
|
||||||
|
val method = dev.javaClass.getMethod("removeBond")
|
||||||
|
method.invoke(dev)
|
||||||
|
logd { "removeBond called for ${dev.address}" }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logw { "removeBond failed: ${e.message}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 저장된 기기 정보 삭제
|
||||||
|
try {
|
||||||
|
val prefs = context.getSharedPreferences("medilight_prefs", Context.MODE_PRIVATE)
|
||||||
|
prefs.edit().remove("last_device_address").remove("last_device_name").apply()
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
|
||||||
|
debugLogger.info("DEVICE_UNREGISTERED")
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
// Reconnection
|
// Reconnection
|
||||||
fun reconnectToLastDevice(lastDeviceAddress: String?) {
|
fun reconnectToLastDevice(lastDeviceAddress: String?) {
|
||||||
if (lastDeviceAddress == null || bluetoothAdapter?.isEnabled != true) {
|
if (lastDeviceAddress == null || bluetoothAdapter?.isEnabled != true) {
|
||||||
@@ -248,6 +303,13 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop any active reconnect scan first
|
||||||
|
reconnectScanCallback?.let { cb ->
|
||||||
|
try { bluetoothAdapter.bluetoothLeScanner?.stopScan(cb) } catch (_: Exception) {}
|
||||||
|
reconnectScanCallback = null
|
||||||
|
}
|
||||||
|
reconnectConnecting = false
|
||||||
|
|
||||||
// Clean up existing
|
// Clean up existing
|
||||||
bluetoothGatt?.let { gatt ->
|
bluetoothGatt?.let { gatt ->
|
||||||
try { gatt.disconnect(); gatt.close() } catch (_: Exception) {}
|
try { gatt.disconnect(); gatt.close() } catch (_: Exception) {}
|
||||||
@@ -481,6 +543,7 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var reconnectScanCallback: ScanCallback? = null
|
private var reconnectScanCallback: ScanCallback? = null
|
||||||
|
@Volatile private var reconnectConnecting = false
|
||||||
|
|
||||||
private fun attemptAutoReconnect() {
|
private fun attemptAutoReconnect() {
|
||||||
val targetAddress = lastConnectedAddress ?: return
|
val targetAddress = lastConnectedAddress ?: return
|
||||||
@@ -492,11 +555,12 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reconnectAttempt++
|
reconnectAttempt++
|
||||||
|
reconnectConnecting = false
|
||||||
logd { "Auto-reconnect #$reconnectAttempt: scanning for $targetName ($targetAddress)" }
|
logd { "Auto-reconnect #$reconnectAttempt: scanning for $targetName ($targetAddress)" }
|
||||||
debugLogger.info("RECONNECT_SCAN #$reconnectAttempt scanning for $targetName")
|
debugLogger.info("RECONNECT_SCAN #$reconnectAttempt scanning for $targetName")
|
||||||
|
|
||||||
bluetoothGatt?.let { gatt ->
|
bluetoothGatt?.let { gatt ->
|
||||||
try { gatt.close() } catch (_: Exception) {}
|
try { gatt.disconnect(); gatt.close() } catch (_: Exception) {}
|
||||||
bluetoothGatt = null
|
bluetoothGatt = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,17 +578,18 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
|
|
||||||
val callback = object : ScanCallback() {
|
val callback = object : ScanCallback() {
|
||||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||||
|
if (reconnectConnecting) return
|
||||||
val name = try { result.device.name ?: "" } catch (_: Exception) { "" }
|
val name = try { result.device.name ?: "" } catch (_: Exception) { "" }
|
||||||
val address = result.device.address
|
val address = result.device.address
|
||||||
|
|
||||||
if (address == targetAddress || (targetName.isNotEmpty() && name == targetName)) {
|
if (address == targetAddress || (targetName.isNotEmpty() && name == targetName)) {
|
||||||
|
reconnectConnecting = true
|
||||||
logd { "Reconnect scan found: $name ($address)" }
|
logd { "Reconnect scan found: $name ($address)" }
|
||||||
debugLogger.info("RECONNECT_FOUND $name ($address) RSSI=${result.rssi}")
|
debugLogger.info("RECONNECT_FOUND $name ($address) RSSI=${result.rssi}")
|
||||||
|
|
||||||
try { scanner.stopScan(this) } catch (_: Exception) {}
|
try { scanner.stopScan(this) } catch (_: Exception) {}
|
||||||
reconnectScanCallback = null
|
reconnectScanCallback = null
|
||||||
|
|
||||||
// Connect directly (autoConnect=false for fast connection)
|
|
||||||
bluetoothGatt = result.device.connectGatt(
|
bluetoothGatt = result.device.connectGatt(
|
||||||
context, false, gattCallback, BluetoothDevice.TRANSPORT_LE
|
context, false, gattCallback, BluetoothDevice.TRANSPORT_LE
|
||||||
)
|
)
|
||||||
@@ -735,14 +800,17 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) {
|
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
if (characteristic.uuid == RX_CHAR_UUID) {
|
if (characteristic.uuid == RX_CHAR_UUID) {
|
||||||
if (value.size < 4) return
|
if (value.size < 4) return
|
||||||
handler.post { processReceivedData(value) }
|
handler.post { processReceivedData(value) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Deprecated("Deprecated in API 33", ReplaceWith("onCharacteristicChanged(gatt, characteristic, value)"))
|
@Deprecated("Deprecated in API 33", ReplaceWith("onCharacteristicChanged(gatt, characteristic, value)"))
|
||||||
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
if (characteristic.uuid == RX_CHAR_UUID) {
|
if (characteristic.uuid == RX_CHAR_UUID) {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
val data = characteristic.value ?: return
|
val data = characteristic.value ?: return
|
||||||
@@ -750,6 +818,7 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
handler.post { processReceivedData(data) }
|
handler.post { processReceivedData(data) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) {
|
override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) {
|
||||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ object PiezoHW {
|
|||||||
var activePreset: DevicePreset = DevicePreset.V0
|
var activePreset: DevicePreset = DevicePreset.V0
|
||||||
|
|
||||||
fun autoDetectPreset(deviceName: String) {
|
fun autoDetectPreset(deviceName: String) {
|
||||||
if (deviceName.startsWith("VBT2604") && deviceName.length >= 9) {
|
if (deviceName.startsWith("VBT2604") && deviceName.length >= 10) {
|
||||||
when (deviceName[7]) {
|
when (deviceName[8]) {
|
||||||
'0' -> activePreset = DevicePreset.V0
|
'0' -> activePreset = DevicePreset.V0
|
||||||
'2' -> activePreset = DevicePreset.V1
|
'2' -> activePreset = DevicePreset.V1
|
||||||
'3' -> activePreset = DevicePreset.V2
|
'3' -> activePreset = DevicePreset.V2
|
||||||
@@ -91,7 +91,7 @@ object PiezoHW {
|
|||||||
val degreeLR: DoubleArray get() = centerCh.map { degreeLRAll[it] }.toDoubleArray()
|
val degreeLR: DoubleArray get() = centerCh.map { degreeLRAll[it] }.toDoubleArray()
|
||||||
|
|
||||||
/** Acoustic calibration (런타임 조절 가능) */
|
/** Acoustic calibration (런타임 조절 가능) */
|
||||||
var distancePerSample: Double = 1.771 // mm/sample
|
var distancePerSample: Double = 1.968 // mm/sample
|
||||||
const val delayOffsetMm: Double = 6.85
|
const val delayOffsetMm: Double = 6.85
|
||||||
|
|
||||||
/** Volume model */
|
/** Volume model */
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ data class LowEchoResult(
|
|||||||
val lowStart: Int, // low-echo span 시작
|
val lowStart: Int, // low-echo span 시작
|
||||||
val lowEnd: Int, // low-echo span 끝 (inclusive)
|
val lowEnd: Int, // low-echo span 끝 (inclusive)
|
||||||
val lowMean: Double, // low-echo 구간 평균 진폭
|
val lowMean: Double, // low-echo 구간 평균 진폭
|
||||||
val urineLen: Int, // post - ant - 1
|
val urineLen: Int, // post - ant
|
||||||
val score: Double, // low_depth * urine_len
|
val score: Double, // low_depth * urine_len
|
||||||
val innerPeaks: List<Int> // (ant, post) 내부 local peak indices
|
val innerPeaks: List<Int> // (ant, post) 내부 local peak indices
|
||||||
) {
|
) {
|
||||||
@@ -373,8 +373,9 @@ class PiezoEchoAnalyzer private constructor() {
|
|||||||
private fun detectLowEchoCore(sg: DoubleArray, threshold: Double): LowEchoResult? {
|
private fun detectLowEchoCore(sg: DoubleArray, threshold: Double): LowEchoResult? {
|
||||||
if (sg.size < 10) return null
|
if (sg.size < 10) return null
|
||||||
|
|
||||||
// 1) low-echo span 추출
|
// 1) low-echo span 추출 (postMaxIdx까지만 — 이후는 노이즈)
|
||||||
val lowMask = BooleanArray(sg.size) { sg[it] <= threshold }
|
val analysisLimit = min(sg.size, postMaxIdx + 1)
|
||||||
|
val lowMask = BooleanArray(sg.size) { it < analysisLimit && sg[it] <= threshold }
|
||||||
val rawSpans = contiguousTrueSpans(lowMask).filter { it.second - it.first + 1 >= lowMinLen }
|
val rawSpans = contiguousTrueSpans(lowMask).filter { it.second - it.first + 1 >= lowMinLen }
|
||||||
|
|
||||||
// 2) 병합 — gap 내부에 벽 후보(threshold + 5 초과 peak)가 있으면 병합하지 않음
|
// 2) 병합 — gap 내부에 벽 후보(threshold + 5 초과 peak)가 있으면 병합하지 않음
|
||||||
@@ -383,7 +384,7 @@ class PiezoEchoAnalyzer private constructor() {
|
|||||||
val firstSpan = spans.firstOrNull() ?: return null
|
val firstSpan = spans.firstOrNull() ?: return null
|
||||||
|
|
||||||
val s = max(0, firstSpan.first)
|
val s = max(0, firstSpan.first)
|
||||||
val e = min(sg.size - 1, firstSpan.second)
|
val e = min(analysisLimit - 1, firstSpan.second)
|
||||||
if (s > e) return null
|
if (s > e) return null
|
||||||
|
|
||||||
val lowSlice = safeSlice(sg, from = s, to = e) ?: return null
|
val lowSlice = safeSlice(sg, from = s, to = e) ?: return null
|
||||||
@@ -397,20 +398,23 @@ class PiezoEchoAnalyzer private constructor() {
|
|||||||
var post = selectWallByProminence(sg = sg, edge = e, searchWin = peakSearchWin,
|
var post = selectWallByProminence(sg = sg, edge = e, searchWin = peakSearchWin,
|
||||||
peakMin = peakMin, side = WallSide.POST, otherEdge = s) ?: return null
|
peakMin = peakMin, side = WallSide.POST, otherEdge = s) ?: return null
|
||||||
|
|
||||||
// 4) post > POST_MAX_IDX: prominence 기반 재탐색
|
// 4) post가 분석 범위를 넘으면 재탐색
|
||||||
if (post > postMaxIdx) {
|
if (post >= analysisLimit) {
|
||||||
val backHalfEdge = (s + e) / 2
|
val backHalfEdge = (s + e) / 2
|
||||||
post = selectWallByProminence(
|
post = selectWallByProminence(
|
||||||
sg = sg, edge = backHalfEdge,
|
sg = sg, edge = backHalfEdge,
|
||||||
searchWin = postMaxIdx - backHalfEdge,
|
searchWin = analysisLimit - 1 - backHalfEdge,
|
||||||
peakMin = peakMin, side = WallSide.POST, otherEdge = null
|
peakMin = peakMin, side = WallSide.POST, otherEdge = null
|
||||||
) ?: return null
|
) ?: return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val antH = sg[ant]
|
val antH = sg[ant]
|
||||||
val postH = sg[post]
|
val postH = sg[post]
|
||||||
|
|
||||||
|
if (postH < threshold) return null
|
||||||
|
|
||||||
val lowDepth = ((antH + postH) / 2.0) - lowMean
|
val lowDepth = ((antH + postH) / 2.0) - lowMean
|
||||||
val urineLen = post - ant - 1
|
val urineLen = post - ant
|
||||||
|
|
||||||
if (urineLen < minUrineLen) return null
|
if (urineLen < minUrineLen) return null
|
||||||
|
|
||||||
|
|||||||
+8
-7
@@ -49,6 +49,7 @@ fun DeviceScanView(appState: AppState) {
|
|||||||
.sortedByDescending { it.rssi }
|
.sortedByDescending { it.rssi }
|
||||||
|
|
||||||
val isConnected by bleManager.isConnected
|
val isConnected by bleManager.isConnected
|
||||||
|
val isServiceReady by bleManager.isServiceReady
|
||||||
val isScanning by bleManager.isScanning
|
val isScanning by bleManager.isScanning
|
||||||
val bluetoothEnabled by bleManager.bluetoothEnabled
|
val bluetoothEnabled by bleManager.bluetoothEnabled
|
||||||
val connectionError by bleManager.connectionError
|
val connectionError by bleManager.connectionError
|
||||||
@@ -93,8 +94,8 @@ fun DeviceScanView(appState: AppState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(isConnected) {
|
LaunchedEffect(isServiceReady) {
|
||||||
if (isConnected) {
|
if (isServiceReady) {
|
||||||
connectingDeviceAddress = null
|
connectingDeviceAddress = null
|
||||||
appState.deviceConnected()
|
appState.deviceConnected()
|
||||||
}
|
}
|
||||||
@@ -340,9 +341,9 @@ private fun DeviceRow(device: BleDevice, isConnecting: Boolean, enabled: Boolean
|
|||||||
@Composable
|
@Composable
|
||||||
private fun SignalBars(rssi: Int) {
|
private fun SignalBars(rssi: Int) {
|
||||||
val strength = when {
|
val strength = when {
|
||||||
rssi >= -65 -> 4 // Good+Excellent 통합 → Green
|
rssi >= -80 -> 4 // Good
|
||||||
rssi >= -80 -> 2
|
rssi >= -90 -> 2 // Fair
|
||||||
else -> 1
|
else -> 1 // Weak
|
||||||
}
|
}
|
||||||
val color = when {
|
val color = when {
|
||||||
strength >= 4 -> Color.Green
|
strength >= 4 -> Color.Green
|
||||||
@@ -363,8 +364,8 @@ private fun SignalBars(rssi: Int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun signalLabel(rssi: Int): String = when {
|
private fun signalLabel(rssi: Int): String = when {
|
||||||
rssi >= -65 -> "Good"
|
rssi >= -80 -> "Good"
|
||||||
rssi >= -80 -> "Fair"
|
rssi >= -90 -> "Fair"
|
||||||
else -> "Weak"
|
else -> "Weak"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ import androidx.compose.material3.Button
|
|||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import com.example.medilightv2android.ble.BleManager
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -25,12 +22,6 @@ import com.example.medilightv2android.ui.theme.*
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeView(appState: AppState) {
|
fun HomeView(appState: AppState) {
|
||||||
val context = LocalContext.current
|
|
||||||
// 앱 시작 시 마지막 기기에 자동 연결 시도
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
BleManager.getInstance(context).autoConnectLastDevice()
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|||||||
+98
-105
@@ -536,70 +536,42 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
.padding(horizontal = hPad, vertical = if (isCompact) 8.dp else 16.dp),
|
.padding(horizontal = hPad, vertical = if (isCompact) 8.dp else 16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = { appState.goHome() }, modifier = Modifier.size(48.dp)) {
|
val btnSize = if (isCompact) 36.dp else 44.dp
|
||||||
Icon(Icons.Default.Home, "Home", tint = MlPrimary, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp))
|
val iconSize = if (isCompact) 20.dp else 24.dp
|
||||||
}
|
IconButton(onClick = { appState.currentScreen = AppScreen.PLACEMENT_GUIDE }, modifier = Modifier.size(btnSize)) {
|
||||||
IconButton(onClick = { appState.currentScreen = AppScreen.PLACEMENT_GUIDE }, modifier = Modifier.size(48.dp)) {
|
Icon(Icons.Default.MyLocation, "Placement", tint = MlPrimary, modifier = Modifier.size(iconSize))
|
||||||
Icon(Icons.Default.MyLocation, "Placement", tint = MlPrimary, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp))
|
|
||||||
}
|
|
||||||
IconButton(onClick = { appState.currentScreen = AppScreen.VOIDING_DIARY }, modifier = Modifier.size(48.dp)) {
|
|
||||||
Icon(Icons.Default.Description, stringResource(R.string.voiding_diary), tint = MlPrimary, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp))
|
|
||||||
}
|
|
||||||
IconButton(onClick = { appState.currentScreen = AppScreen.REMINDER_SETTINGS }, modifier = Modifier.size(48.dp)) {
|
|
||||||
Icon(Icons.Default.Notifications, contentDescription = "Reminder", tint = MlPrimary, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp))
|
|
||||||
}
|
|
||||||
if (!isCompact) {
|
|
||||||
Text("Bladder Monitor", fontSize = 20.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(start = 8.dp))
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
if (isAutoMeasuring) {
|
// Battery icon (color by level)
|
||||||
|
if (!isDemoMode) {
|
||||||
|
val batteryColor = when {
|
||||||
|
batteryLvl >= 60 -> Color(0xFF4CAF50)
|
||||||
|
batteryLvl >= 30 -> Color(0xFFFF9800)
|
||||||
|
else -> Color.Red
|
||||||
|
}
|
||||||
|
val batteryIcon = when {
|
||||||
|
batteryLvl >= 60 -> Icons.Default.BatteryFull
|
||||||
|
batteryLvl >= 30 -> Icons.Default.Battery4Bar
|
||||||
|
else -> Icons.Default.Battery1Bar
|
||||||
|
}
|
||||||
|
Icon(batteryIcon, "Battery $batteryLvl%", tint = batteryColor, modifier = Modifier.size(iconSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection dot
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.size(8.dp)
|
||||||
.background(Color.Red.copy(alpha = 0.12f))
|
|
||||||
.padding(horizontal = 6.dp, vertical = 3.dp)
|
|
||||||
) {
|
|
||||||
Text(if (isCompact) "A" else "Auto", fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.Red)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Catheter count
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
.background(MlPrimary.copy(alpha = 0.1f))
|
|
||||||
.clickable { showAddCatheterDialog = true }
|
|
||||||
.padding(horizontal = 6.dp, vertical = 3.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.Add, "Add catheter", tint = MlPrimary, modifier = Modifier.size(12.dp))
|
|
||||||
Text("$catheterCount", fontSize = 12.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
|
|
||||||
IconButton(onClick = { appState.showHistory() }, modifier = Modifier.size(48.dp)) {
|
|
||||||
Icon(Icons.Default.History, "History", tint = MlSecondaryText, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp))
|
|
||||||
}
|
|
||||||
IconButton(onClick = { showSettings = !showSettings }, modifier = Modifier.size(48.dp)) {
|
|
||||||
Icon(Icons.Default.Settings, "Settings", tint = if (showSettings) MlPrimary else MlSecondaryText, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Battery / Demo
|
|
||||||
if (isDemoMode) {
|
|
||||||
Text("DEMO", fontSize = 10.sp, fontWeight = FontWeight.Bold, color = Color.White,
|
|
||||||
modifier = Modifier.clip(RoundedCornerShape(4.dp)).background(Color(0xFFFF9500)).padding(horizontal = 4.dp, vertical = 2.dp))
|
|
||||||
} else if (!isCompact) {
|
|
||||||
Text("$batteryLvl%", fontSize = 13.sp, color = MlSecondaryText)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(10.dp)
|
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(if (isDemoMode || isConnected) Color.Green else Color.Red)
|
.background(if (isDemoMode || isConnected) Color(0xFF4CAF50) else Color.Red)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
|
||||||
|
// Settings button
|
||||||
|
IconButton(onClick = { showSettings = !showSettings }, modifier = Modifier.size(btnSize)) {
|
||||||
|
Icon(Icons.Default.Settings, "Settings", tint = if (showSettings) MlPrimary else MlSecondaryText, modifier = Modifier.size(iconSize))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDisconnectionBanner) {
|
if (showDisconnectionBanner) {
|
||||||
@@ -725,6 +697,41 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Text("${appState.piezoSettings.maxVolume} ml", fontSize = 14.sp, color = MlSecondaryText)
|
Text("${appState.piezoSettings.maxVolume} ml", fontSize = 14.sp, color = MlSecondaryText)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
// Disconnect device
|
||||||
|
var showDisconnectConfirm by remember { mutableStateOf(false) }
|
||||||
|
Button(
|
||||||
|
onClick = { showDisconnectConfirm = true },
|
||||||
|
modifier = Modifier.fillMaxWidth().height(44.dp),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color.Red.copy(alpha = 0.1f))
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.LinkOff, null, tint = Color.Red, modifier = Modifier.size(18.dp))
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Disconnect Device", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.Red)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDisconnectConfirm) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showDisconnectConfirm = false },
|
||||||
|
title = { Text("Disconnect Device") },
|
||||||
|
text = { Text("기기 연결을 해제하고 페어링 정보를 삭제합니다. 다시 연결하려면 기기 검색이 필요합니다.") },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
showDisconnectConfirm = false
|
||||||
|
showSettings = false
|
||||||
|
// Disconnect + remove bond + clear saved device
|
||||||
|
bleManager.disconnectAndUnbond()
|
||||||
|
appState.goHome()
|
||||||
|
}) { Text("Disconnect", color = Color.Red) }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showDisconnectConfirm = false }) { Text("Cancel") }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -776,67 +783,53 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
InfoCard("Level", "${appState.bladderLevel.currentLevel}/${BladderLevel.MAX_LEVEL}", "", urgency.accentColor, Modifier.weight(1f))
|
InfoCard("Level", "${appState.bladderLevel.currentLevel}/${BladderLevel.MAX_LEVEL}", "", urgency.accentColor, Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Control buttons
|
// Control buttons: Catheterize / Auto / Spot
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
// Catheterize button → 배뇨 기록 시트
|
// Catheterize
|
||||||
Button(
|
Button(
|
||||||
onClick = { showCatheterizeSheet = true },
|
onClick = { showCatheterizeSheet = true },
|
||||||
modifier = Modifier.weight(1f).height(56.dp),
|
modifier = Modifier.weight(1.2f).height(52.dp),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = MlPrimary.copy(alpha = 0.1f))
|
colors = ButtonDefaults.buttonColors(containerColor = MlPrimary.copy(alpha = 0.1f))
|
||||||
) {
|
) {
|
||||||
Text("Catheterize", fontSize = 16.sp, fontWeight = FontWeight.SemiBold, color = MlPrimary)
|
Text("Catheterize", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = MlPrimary)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spot button (tap = single, long press = auto 30s)
|
// Auto
|
||||||
Box(
|
Button(
|
||||||
modifier = Modifier
|
onClick = {
|
||||||
.weight(1f)
|
if (isAutoMeasuring) stopAutoMeasure() else startAutoMeasure()
|
||||||
.height(56.dp)
|
|
||||||
.clip(RoundedCornerShape(16.dp))
|
|
||||||
.background(
|
|
||||||
Brush.horizontalGradient(
|
|
||||||
if (isAutoMeasuring) listOf(Color.Red, Color(0xFFFF9500))
|
|
||||||
else urgency.gradientColors
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.pointerInput(isAutoMeasuring, isMeasuring) {
|
|
||||||
detectTapGestures(
|
|
||||||
onTap = {
|
|
||||||
if (isAutoMeasuring) stopAutoMeasure()
|
|
||||||
else if (!isMeasuring) measure()
|
|
||||||
},
|
},
|
||||||
onLongPress = {
|
modifier = Modifier.weight(1f).height(52.dp),
|
||||||
if (!isAutoMeasuring && !isMeasuring) startAutoMeasure()
|
shape = RoundedCornerShape(14.dp),
|
||||||
}
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = if (isAutoMeasuring) Color.Red else Color(0xFFFF9800)
|
||||||
)
|
)
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
if (isAutoMeasuring) {
|
||||||
if (isMeasuring && !isAutoMeasuring) {
|
Text("Stop", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
CircularProgressIndicator(color = Color.White, modifier = Modifier.size(20.dp), strokeWidth = 2.dp)
|
} else {
|
||||||
}
|
Text("Auto", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
when {
|
|
||||||
isAutoMeasuring -> "Stop"
|
|
||||||
isMeasuring -> "Measuring..."
|
|
||||||
else -> "Spot"
|
|
||||||
},
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
color = Color.White
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
// Spot (single measurement)
|
||||||
if (isAutoMeasuring) "Auto: IMU 1s + Measure 10s (#$autoTickCount)" else "Long press for auto (IMU 1s + measure 10s)",
|
Button(
|
||||||
fontSize = 11.sp,
|
onClick = { if (!isMeasuring && !isAutoMeasuring) measure() },
|
||||||
color = if (isAutoMeasuring) Color.Red.copy(alpha = 0.7f) else MlSecondaryText
|
enabled = !isMeasuring && !isAutoMeasuring,
|
||||||
|
modifier = Modifier.weight(1f).height(52.dp),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = urgency.gradientColors.first()
|
||||||
)
|
)
|
||||||
|
) {
|
||||||
|
if (isMeasuring && !isAutoMeasuring) {
|
||||||
|
CircularProgressIndicator(color = Color.White, modifier = Modifier.size(18.dp), strokeWidth = 2.dp)
|
||||||
|
} else {
|
||||||
|
Text("Spot", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Developer Tools toggle ──
|
// ── Developer Tools toggle ──
|
||||||
Button(
|
Button(
|
||||||
|
|||||||
+29
-19
@@ -294,14 +294,9 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
|
|
||||||
// 첫 스캔 시 1회만 piezo power on
|
// 첫 스캔 시 1회만 piezo power on
|
||||||
var piezoPowered by remember { mutableStateOf(false) }
|
var piezoPowered by remember { mutableStateOf(false) }
|
||||||
|
val handler = remember { android.os.Handler(android.os.Looper.getMainLooper()) }
|
||||||
|
|
||||||
fun performScan() {
|
fun sendMeasureCommand() {
|
||||||
if (isScanning) return
|
|
||||||
isScanning = true
|
|
||||||
if (!piezoPowered) {
|
|
||||||
bleManager.sendPiezoPowerOn()
|
|
||||||
piezoPowered = true
|
|
||||||
}
|
|
||||||
bleManager.piezoCollector.onMultiChannelComplete = { channels ->
|
bleManager.piezoCollector.onMultiChannelComplete = { channels ->
|
||||||
isScanning = false
|
isScanning = false
|
||||||
scanCount++
|
scanCount++
|
||||||
@@ -319,13 +314,6 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
detected[ch.channel] = true
|
detected[ch.channel] = true
|
||||||
urineLens[ch.channel] = analysis.result.urineLen
|
urineLens[ch.channel] = analysis.result.urineLen
|
||||||
allWalls[ch.channel] = Pair(analysis.result.ant, analysis.result.post)
|
allWalls[ch.channel] = Pair(analysis.result.ant, analysis.result.post)
|
||||||
} else {
|
|
||||||
val gate = UrinAI.detect(ch.buffer)
|
|
||||||
if (gate.detected && (gate.bw - gate.fw) >= MIN_URINE_LEN_PLACEMENT) {
|
|
||||||
detected[ch.channel] = true
|
|
||||||
urineLens[ch.channel] = gate.bw - gate.fw
|
|
||||||
allWalls[ch.channel] = Pair(gate.fw, gate.bw)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,7 +365,19 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
bleManager.sendAllChannels()
|
bleManager.sendAllChannels()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continuous scan loop (3초 간격) — LED는 스캔 콜백에서 동적 제어
|
fun performScan() {
|
||||||
|
if (isScanning) return
|
||||||
|
isScanning = true
|
||||||
|
if (!piezoPowered) {
|
||||||
|
bleManager.sendPiezoPowerOn()
|
||||||
|
piezoPowered = true
|
||||||
|
handler.postDelayed({ sendMeasureCommand() }, 500)
|
||||||
|
} else {
|
||||||
|
sendMeasureCommand()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continuous scan loop (1초 간격) — LED는 스캔 콜백에서 동적 제어
|
||||||
LaunchedEffect(isContinuousMode) {
|
LaunchedEffect(isContinuousMode) {
|
||||||
if (isContinuousMode) {
|
if (isContinuousMode) {
|
||||||
performScan()
|
performScan()
|
||||||
@@ -464,11 +464,21 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Skip button
|
// Skip button
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
TextButton(onClick = { appState.placementComplete() }) {
|
Button(
|
||||||
|
onClick = { appState.placementComplete() },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(48.dp),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MlSecondaryText.copy(alpha = 0.15f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Skip Placement Guide",
|
text = "Skip",
|
||||||
fontSize = 13.sp,
|
fontSize = 15.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = MlSecondaryText
|
color = MlSecondaryText
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -27,8 +27,7 @@ import com.example.medilightv2android.ui.theme.*
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
private val pages = listOf(
|
private val pages = listOf(
|
||||||
"Welcome to MedilightV2" to "Smart bladder monitoring\nfor better quality of life",
|
"Welcome to VesiScan-Basic" to "Smart bladder monitoring\nfor better quality of life",
|
||||||
"NIRS & Piezo Sensors" to "Advanced dual-sensor technology\nfor accurate measurements",
|
|
||||||
"Real-time Monitoring" to "Track your bladder level\nwith instant visual feedback"
|
"Real-time Monitoring" to "Track your bladder level\nwith instant visual feedback"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+5
-23
@@ -62,28 +62,6 @@ fun PiezoPersonalizationView(appState: AppState) {
|
|||||||
IconButton(onClick = { appState.backToSensorSelect() }) {
|
IconButton(onClick = { appState.backToSensorSelect() }) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back", tint = MlPrimary)
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back", tint = MlPrimary)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
Text("Step $currentStep of $totalSteps", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText)
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
Spacer(modifier = Modifier.width(20.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Progress bar
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 24.dp)
|
|
||||||
.height(6.dp)
|
|
||||||
.clip(RoundedCornerShape(4.dp))
|
|
||||||
.background(MlPrimary.copy(alpha = 0.15f))
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(currentStep.toFloat() / totalSteps)
|
|
||||||
.fillMaxHeight()
|
|
||||||
.clip(RoundedCornerShape(4.dp))
|
|
||||||
.background(Brush.horizontalGradient(listOf(MlTeal, MlPrimary)))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
@@ -173,8 +151,12 @@ private fun MaxVolumeStep(appState: AppState) {
|
|||||||
Text("Set your maximum bladder capacity", fontSize = 14.sp, color = MlSecondaryText)
|
Text("Set your maximum bladder capacity", fontSize = 14.sp, color = MlSecondaryText)
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.Bottom) {
|
||||||
Text("${appState.piezoSettings.maxVolume}", fontSize = 56.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
|
Text("${appState.piezoSettings.maxVolume}", fontSize = 56.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
|
||||||
Text("ml", fontSize = 20.sp, color = MlSecondaryText)
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("mL", fontSize = 22.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText,
|
||||||
|
modifier = Modifier.padding(bottom = 10.dp))
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user