diff --git a/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt b/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt index 32647bc..47444e7 100644 --- a/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt +++ b/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt @@ -88,6 +88,9 @@ class BleManager private constructor(private val context: Context) { @android.annotation.SuppressLint("MissingPermission") get() = bluetoothGatt?.device?.bondState == android.bluetooth.BluetoothDevice.BOND_BONDED val isReconnecting = mutableStateOf(false) + /** 새 페어링/재연결 진행 중 (connectGatt 호출 ~ STATE_CONNECTED 또는 실패까지). + * ClinicalHome 등에서 "BLE Connecting…" 표시용. */ + val isConnecting = mutableStateOf(false) val connectionError = mutableStateOf(null) // Callbacks @@ -206,12 +209,14 @@ class BleManager private constructor(private val context: Context) { if (idx >= 0) discoveredDevices[idx] = discoveredDevices[idx].copy(isConnecting = true) connectionError.value = null + isConnecting.value = true try { bluetoothGatt = bleDevice.device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) logd { "connectGatt called, gatt=${bluetoothGatt != null}" } } catch (e: Exception) { loge { "connectGatt failed: ${e.message}" } + isConnecting.value = false handler.post { connectionError.value = "Connection failed: ${e.message}" if (idx >= 0) discoveredDevices[idx] = discoveredDevices[idx].copy(isConnecting = false) @@ -786,6 +791,7 @@ class BleManager private constructor(private val context: Context) { BluetoothProfile.STATE_CONNECTED -> { logd { "Connected to ${gatt.device.address}" } isReconnecting.value = false + isConnecting.value = false isUserDisconnect = false connectionError.value = null val idx = discoveredDevices.indexOfFirst { it.address == gatt.device.address } @@ -819,6 +825,7 @@ class BleManager private constructor(private val context: Context) { } BluetoothProfile.STATE_DISCONNECTED -> { logd { "Disconnected from ${gatt.device.address}" } + isConnecting.value = false stopBatteryPolling() bluetoothGatt = null txCharacteristic = null diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalHomeView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalHomeView.kt index bb44ee9..d42412c 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalHomeView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalHomeView.kt @@ -135,6 +135,11 @@ fun ClinicalHomeView(appState: AppState) { else Color(0xFFFF9800).copy(alpha = 0.15f) ) ) { + // 재연결/새 페어링 진행 중 — connectGatt 호출부터 STATE_CONNECTED까지 1~5초 걸리는 동안 + // "BLE Not Connected"가 보이는 깜박임 방지. + val isConnecting by bleManager.isConnecting + val isReconnecting by bleManager.isReconnecting + val showConnecting = !isConnected && (isConnecting || isReconnecting) Row( modifier = Modifier.padding(12.dp).fillMaxWidth(), verticalAlignment = Alignment.CenterVertically @@ -159,6 +164,15 @@ fun ClinicalHomeView(appState: AppState) { onClick = { showUnpairConfirm = true }, colors = ButtonDefaults.outlinedButtonColors(contentColor = Color(0xFFE53935)) ) { Text("Unpair", fontSize = 12.sp) } + } else if (showConnecting) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = MlPrimary, + strokeWidth = 2.dp + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("BLE Connecting…", fontWeight = FontWeight.Bold, fontSize = 14.sp, + color = MlPrimary, modifier = Modifier.weight(1f)) } else { Text("BLE Not Connected", fontWeight = FontWeight.Bold, fontSize = 14.sp, color = Color(0xFFFF9800), modifier = Modifier.weight(1f)) @@ -403,11 +417,20 @@ fun ClinicalHomeView(appState: AppState) { ) Spacer(modifier = Modifier.height(8.dp)) + // Notes — Examiner처럼 키보드 우하단 ✓(Done)으로 완료 가능 + val notesFocus = androidx.compose.ui.platform.LocalSoftwareKeyboardController.current OutlinedTextField( value = notes, onValueChange = { notes = it }, label = { Text("Notes (선택)") }, - modifier = Modifier.fillMaxWidth().height(80.dp) + singleLine = true, + keyboardOptions = androidx.compose.foundation.text.KeyboardOptions( + imeAction = androidx.compose.ui.text.input.ImeAction.Done + ), + keyboardActions = androidx.compose.foundation.text.KeyboardActions( + onDone = { notesFocus?.hide() } + ), + modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(20.dp)) diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalLiveView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalLiveView.kt index 81bc479..8dc4413 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalLiveView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalLiveView.kt @@ -374,34 +374,35 @@ private fun ChannelChart( ) { val antColor = Color(0xFF4CAF50) // green = anterior wall val postColor = Color(0xFFFF9800) // orange = posterior wall + // 사용자 피드백: 태블릿 2x3 grid에서 그래프 작음 → 셀 크기 + 폰트 + 선 굵기 모두 키움 Card( - shape = RoundedCornerShape(6.dp), + shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors(containerColor = Color.White), - modifier = Modifier.fillMaxWidth().height(120.dp) + modifier = Modifier.fillMaxWidth().height(240.dp) ) { - Column(modifier = Modifier.fillMaxSize().padding(4.dp)) { + Column(modifier = Modifier.fillMaxSize().padding(8.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { Box( modifier = Modifier - .size(8.dp) - .clip(RoundedCornerShape(2.dp)) + .size(12.dp) + .clip(RoundedCornerShape(3.dp)) .background(color) ) - Spacer(modifier = Modifier.width(4.dp)) - Text("CH$channelIdx", fontSize = 10.sp, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.width(6.dp)) + Text("CH$channelIdx", fontSize = 16.sp, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.weight(1f)) if (walls != null) { - Text("a${walls.first}", fontSize = 9.sp, color = antColor, fontWeight = FontWeight.SemiBold) - Spacer(modifier = Modifier.width(3.dp)) - Text("p${walls.second}", fontSize = 9.sp, color = postColor, fontWeight = FontWeight.SemiBold) - Spacer(modifier = Modifier.width(4.dp)) + Text("a${walls.first}", fontSize = 14.sp, color = antColor, fontWeight = FontWeight.SemiBold) + Spacer(modifier = Modifier.width(6.dp)) + Text("p${walls.second}", fontSize = 14.sp, color = postColor, fontWeight = FontWeight.SemiBold) + Spacer(modifier = Modifier.width(8.dp)) } if (buffer.isNotEmpty()) { val peak = buffer.maxOrNull()?.toInt() ?: 0 - Text("$peak", fontSize = 9.sp, color = MlSecondaryText) + Text("$peak", fontSize = 14.sp, color = MlSecondaryText, fontWeight = FontWeight.SemiBold) } } - Spacer(modifier = Modifier.height(2.dp)) + Spacer(modifier = Modifier.height(4.dp)) Canvas(modifier = Modifier.fillMaxSize()) { if (buffer.isEmpty()) return@Canvas val w = size.width @@ -429,22 +430,22 @@ private fun ChannelChart( val (antIdx, postIdx) = walls if (antIdx in 0 until n) { val x = antIdx * xScale - drawLine(antColor.copy(alpha = 0.75f), Offset(x, 0f), Offset(x, h), strokeWidth = 1.5f) + drawLine(antColor.copy(alpha = 0.75f), Offset(x, 0f), Offset(x, h), strokeWidth = 2.5f) } if (postIdx in 0 until n) { val x = postIdx * xScale - drawLine(postColor.copy(alpha = 0.75f), Offset(x, 0f), Offset(x, h), strokeWidth = 1.5f) + drawLine(postColor.copy(alpha = 0.75f), Offset(x, 0f), Offset(x, h), strokeWidth = 2.5f) } } - // waveform + // waveform — 굵게 (1.5 → 2.5) val path = Path() for (i in buffer.indices) { val x = i * xScale val y = h - (buffer[i].toInt() - yMin) * yScale if (i == 0) path.moveTo(x, y) else path.lineTo(x, y) } - drawPath(path, color, style = Stroke(width = 1.5f)) + drawPath(path, color, style = Stroke(width = 2.5f)) } } }