fix: Thread→coroutine, PlacementGuideView 메인스레드 보장

- #2: PiezoMonitoringView raw Thread → measureScope.launch + Dispatchers.Default
  withContext(Main)으로 state 변경 보장, composable 떠나면 자동 취소
- #22: PlacementGuideView onMultiChannelComplete 콜백 → handler.post 감싸기
  BLE Binder 스레드에서 Compose state 직접 변경 → 메인 스레드 보장
- nested handler.post 제거, throttle 시 직접 state 리셋

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 16:30:19 +09:00
parent 758930796f
commit 72ccbdf102
2 changed files with 16 additions and 9 deletions
@@ -106,6 +106,8 @@ fun PiezoMonitoringView(appState: AppState) {
return sorted.subList(1, sorted.size - 1).average() return sorted.subList(1, sorted.size - 1).average()
} }
val measureScope = rememberCoroutineScope()
fun measure() { fun measure() {
if (isMeasuring) { if (isMeasuring) {
Log.w("PiezoMonitor", "measure() called while already measuring — SKIPPED") Log.w("PiezoMonitor", "measure() called while already measuring — SKIPPED")
@@ -129,11 +131,12 @@ fun PiezoMonitoringView(appState: AppState) {
bleManager.piezoCollector.onMultiChannelComplete = { channels -> bleManager.piezoCollector.onMultiChannelComplete = { channels ->
Log.d("PiezoMonitor", "RECEIVED ${channels.size} channels: ${channels.map { "CH${it.channel}(${it.buffer.size})" }}") Log.d("PiezoMonitor", "RECEIVED ${channels.size} channels: ${channels.map { "CH${it.channel}(${it.buffer.size})" }}")
// 분석을 백그라운드에서 수행 후 UI 업데이트는 메인 스레드로 // 분석을 백그라운드에서 수행 후 UI 업데이트는 메인 스레드로
Thread { measureScope.launch {
kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Default) {
val analysisStart = System.currentTimeMillis() val analysisStart = System.currentTimeMillis()
// channels를 로컬 복사 (Thread safety) // channels를 로컬 복사 (Thread safety)
val channelsCopy = channels.toList() val channelsCopy = channels.toList()
handler.post { kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Main) {
lastMultiChannelResult.clear() lastMultiChannelResult.clear()
lastMultiChannelResult.addAll(channelsCopy) lastMultiChannelResult.addAll(channelsCopy)
} }
@@ -303,14 +306,17 @@ fun PiezoMonitoringView(appState: AppState) {
} }
// allWalls를 UI state에 저장 (ChannelPanel에서 사용) // allWalls를 UI state에 저장 (ChannelPanel에서 사용)
handler.post { lastAllWalls = allWalls.toList() } val wallsCopy = allWalls.toList()
kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Main) {
lastAllWalls = wallsCopy
}
val finalRawVol = rawVolumeMl val finalRawVol = rawVolumeMl
val finalTotalWalls = totalWallCount val finalTotalWalls = totalWallCount
val analysisMs = System.currentTimeMillis() - analysisStart val analysisMs = System.currentTimeMillis() - analysisStart
Log.d("PiezoMonitor", "Analysis took ${analysisMs}ms") Log.d("PiezoMonitor", "Analysis took ${analysisMs}ms")
// UI 업데이트는 메인 스레드에서 // UI 업데이트는 메인 스레드에서
handler.post { kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Main) {
if (finalRawVol != null && finalRawVol > 0) { if (finalRawVol != null && finalRawVol > 0) {
if (isAutoMeasuring) { if (isAutoMeasuring) {
if (allChannelsReceived) { if (allChannelsReceived) {
@@ -346,16 +352,14 @@ fun PiezoMonitoringView(appState: AppState) {
measureRetryCount = 0 measureRetryCount = 0
try { measureDoneSignal.value.complete(Unit) } catch (_: Exception) {} try { measureDoneSignal.value.complete(Unit) } catch (_: Exception) {}
} }
}.start() } }
} }
bleManager.piezoCollector.onLog = { msg -> Log.d("PiezoCollector", msg) } bleManager.piezoCollector.onLog = { msg -> Log.d("PiezoCollector", msg) }
if (!bleManager.sendChannelsOnly()) { if (!bleManager.sendChannelsOnly()) {
// Throttle됨 — isMeasuring 리셋 (deadlock 방지) // Throttle됨 — isMeasuring 리셋 (deadlock 방지)
handler.post { isMeasuring = false
isMeasuring = false try { measureDoneSignal.value.complete(Unit) } catch (_: Exception) {}
try { measureDoneSignal.value.complete(Unit) } catch (_: Exception) {}
}
return return
} }
@@ -525,6 +525,8 @@ fun PlacementGuideView(appState: AppState) {
fun sendMeasureCommand() { fun sendMeasureCommand() {
bleManager.piezoCollector.onMultiChannelComplete = { channels -> bleManager.piezoCollector.onMultiChannelComplete = { channels ->
// 메인 스레드에서 처리 (BLE 콜백은 Binder 스레드)
handler.post {
isScanning = false isScanning = false
scanCount++ scanCount++
@@ -682,6 +684,7 @@ fun PlacementGuideView(appState: AppState) {
direction = directionHint direction = directionHint
) )
} }
} // handler.post
bleManager.piezoCollector.onLog = { msg -> Log.d("PlacementGuide", msg) } bleManager.piezoCollector.onLog = { msg -> Log.d("PlacementGuide", msg) }
if (!bleManager.sendAllChannels()) { if (!bleManager.sendAllChannels()) {
// Throttle됨 — isScanning 리셋 (deadlock 방지) // Throttle됨 — isScanning 리셋 (deadlock 방지)