feat: BLE stability fixes, NIRS streaming improvements, catheter count feature

- Fix BLE API deprecation warnings with API 33+ version checks
- Move isServiceReady to onDescriptorWrite for reliable CCCD setup
- Fix battery polling: immediate query on connect, pause during NIRS streaming
- Add mcj retry mechanism for NIRS streaming reliability
- Add debug logging to BLE, NIRS parsing, and processing pipeline
- Add catheter count feature to Piezo monitoring (persistent count, add dialog, auto-decrement on catheterize)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 11:26:36 +09:00
parent 91c5e5e7e0
commit 254381dbaa
7 changed files with 159 additions and 14 deletions
@@ -244,16 +244,28 @@ class BleManager private constructor(private val context: Context) {
// Send
fun sendRaw(data: ByteArray) {
val characteristic = txCharacteristic ?: return
val gatt = bluetoothGatt ?: return
val characteristic = txCharacteristic
val gatt = bluetoothGatt
if (characteristic == null) {
Log.e(TAG, "sendRaw: txCharacteristic is null!")
return
}
if (gatt == null) {
Log.e(TAG, "sendRaw: bluetoothGatt is null!")
return
}
val cmdPreview = if (data.size >= 4) String(data, 0, 4, Charsets.US_ASCII) else "?"
Log.d(TAG, "sendRaw: $cmdPreview (${data.size} bytes)")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
gatt.writeCharacteristic(characteristic, data, BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE)
val result = gatt.writeCharacteristic(characteristic, data, BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE)
Log.d(TAG, "sendRaw result: $result")
} else {
@Suppress("DEPRECATION")
characteristic.value = data
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
@Suppress("DEPRECATION")
gatt.writeCharacteristic(characteristic)
val ok = gatt.writeCharacteristic(characteristic)
Log.d(TAG, "sendRaw success: $ok")
}
}
@@ -293,7 +305,8 @@ class BleManager private constructor(private val context: Context) {
fun startBatteryPolling() {
batteryTimer?.let { handler.removeCallbacks(it) }
handler.postDelayed({ sendBatteryQuery() }, 1000)
// Query immediately, then every 30s
sendBatteryQuery()
val poll = object : Runnable {
override fun run() {
sendBatteryQuery()
@@ -301,10 +314,10 @@ class BleManager private constructor(private val context: Context) {
}
}
batteryTimer = poll
handler.postDelayed(poll, 31000)
handler.postDelayed(poll, 30000)
}
private fun stopBatteryPolling() {
fun stopBatteryPolling() {
batteryTimer?.let { handler.removeCallbacks(it) }
batteryTimer = null
}
@@ -353,7 +366,6 @@ class BleManager private constructor(private val context: Context) {
} catch (_: Exception) {}
gatt.discoverServices()
startBatteryPolling()
onConnectionStateChanged?.invoke(true)
}
BluetoothProfile.STATE_DISCONNECTED -> {
@@ -439,6 +451,7 @@ class BleManager private constructor(private val context: Context) {
if (status == BluetoothGatt.GATT_SUCCESS) {
if (txCharacteristic != null && rxCharacteristic != null) {
isServiceReady.value = true
startBatteryPolling()
Log.d(TAG, "Service ready! (CCCD written)")
}
} else {
@@ -469,7 +482,15 @@ class BleManager private constructor(private val context: Context) {
private fun processReceivedData(data: ByteArray) {
val prefix = String(data, 0, minOf(4, data.size), Charsets.US_ASCII)
Log.d(TAG, "Received: $prefix (${data.size} bytes)")
Log.d(TAG, "Received: $prefix (${data.size} bytes), callback=${
when {
prefix.startsWith("rcj") -> "onNirsMcjReceived=${onNirsMcjReceived != null}"
prefix.startsWith("rta") -> "onNirsPowerOnReceived=${onNirsPowerOnReceived != null}"
prefix.startsWith("rqq") || prefix.startsWith("rsh") -> "onNirsSensorActivated=${onNirsSensorActivated != null}"
prefix.startsWith("rag") -> "onNirsMagReceived=${onNirsMagReceived != null}"
else -> "n/a"
}
}")
when (prefix) {
"rta:", "rta!" -> onNirsPowerOnReceived?.invoke()
"rsh:", "rsh!", "rqq:", "rqq!" -> onNirsSensorActivated?.invoke()
@@ -482,6 +503,7 @@ class BleManager private constructor(private val context: Context) {
batteryLevel.value = ((millivolts - 3700) * 100 / 500).coerceIn(0, 100)
}
}
else -> Log.w(TAG, "Unknown prefix: '$prefix' hex=${data.take(8).joinToString(" ") { "%02X".format(it) }}")
}
}
}