fix: critical code review — thread safety, state management

CRITICAL fixes:
- #1: lastMultiChannelResult 변경을 handler.post로 (메인 스레드 보장)
- #3: measureDoneSignal var=remember → val mutableStateOf (recomposition 안전)
- #34,35: BleManager bluetoothGatt/tx/rxCharacteristic/lastRxTimestamp @Volatile
- #66: GreenZoneConstants 모든 mutable var @Volatile
- #56: PiezoHW.distancePerSample @Volatile
- nested handler.post 제거 (불필요한 프레임 지연)
- maa throttle deadlock 방지 (Boolean 반환 + state 리셋)
- Plot/Log 불일치 수정 (channelLogs를 allWalls 기준으로 통일)
- Placement 센서 위치/화살표 조정, gradient dead zone, CV relaxation
- 개발자 모드 CV 표시, 가이드 상자 항상 표시

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 16:25:06 +09:00
parent 0e0e04b6f5
commit 758930796f
5 changed files with 85 additions and 47 deletions
@@ -48,9 +48,9 @@ class BleManager private constructor(private val context: Context) {
private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
private var bluetoothLeScanner: BluetoothLeScanner? = null
private var bluetoothGatt: BluetoothGatt? = null
private var txCharacteristic: BluetoothGattCharacteristic? = null
private var rxCharacteristic: BluetoothGattCharacteristic? = null
@Volatile private var bluetoothGatt: BluetoothGatt? = null
@Volatile private var txCharacteristic: BluetoothGattCharacteristic? = null
@Volatile private var rxCharacteristic: BluetoothGattCharacteristic? = null
private val handler = Handler(Looper.getMainLooper())
val debugLogger = com.example.medilightv2android.services.BleDebugLogger.getInstance(context)
private var scanTimer: Runnable? = null
@@ -62,7 +62,7 @@ class BleManager private constructor(private val context: Context) {
private var reconnectAttempt = 0
private val reconnectIntervalMs: Long = 3000
private var reconnectTimer: Runnable? = null
private var lastRxTimestamp: Long = 0
@Volatile private var lastRxTimestamp: Long = 0
private var watchdogTimer: Runnable? = null
private val watchdogTimeoutMs: Long = 15000
@@ -238,6 +238,7 @@ class BleManager private constructor(private val context: Context) {
scheduleAutoReconnect()
}
fun disconnect() {
isUserDisconnect = true
cancelAutoReconnect()
@@ -395,16 +396,16 @@ class BleManager private constructor(private val context: Context) {
sendRaw(CRC16.buildCommandBE("mec", intArrayOf(freqOption, delayUs, 140, cycles, 1, piezoCh)))
}
fun sendAllChannels(mode: Int = 0) {
fun sendAllChannels(mode: Int = 0): Boolean {
val now = System.currentTimeMillis()
if (now - lastMaaSentMs < 1000) {
if (now - lastMaaSentMs < 800) {
android.util.Log.w("BleManager", "maa THROTTLED — ${now - lastMaaSentMs}ms since last")
debugLogger.warn("maa THROTTLED ${now - lastMaaSentMs}ms")
return
return false
}
lastMaaSentMs = now
piezoCollector.startMultiChannel(6)
sendRaw(CRC16.buildCommandASCII("maa", " "))
return true
}
// ── NIRS Commands ──
@@ -453,18 +454,18 @@ class BleManager private constructor(private val context: Context) {
sendRaw(CRC16.buildCommandASCII("mbb", " "))
}
/** 6채널 측정만 (maa) — 최소 1초 간격 throttle */
/** 6채널 측정만 (maa) — 최소 800ms 간격 throttle */
private var lastMaaSentMs = 0L
fun sendChannelsOnly() {
fun sendChannelsOnly(): Boolean {
val now = System.currentTimeMillis()
if (now - lastMaaSentMs < 1000) {
if (now - lastMaaSentMs < 800) {
android.util.Log.w("BleManager", "maa THROTTLED — ${now - lastMaaSentMs}ms since last")
debugLogger.warn("maa THROTTLED ${now - lastMaaSentMs}ms")
return
return false
}
lastMaaSentMs = now
piezoCollector.startMultiChannel(6)
sendRaw(CRC16.buildCommandASCII("maa", " "))
return true
}
fun startBatteryPolling() {