Files
VesiscanClinicalAndroid/app/src/test/java/com/medithings/vesiscan/ble/ImuRicReassemblyTest.kt
T
dw.jang 739d52e472 feat(ble): rec: / ric: chunk reassembly — 저 MTU 기기 지원
Firmware (2026-07-21+) 는 MTU 협상값이 낮아 reb: 210B / rim: 188B 를
못 보낼 때 다중 chunk (rec: / ric:) 로 쪼개 전송함. 앱은 각 chunk 의
offset/total/chunk_samples 헤더를 읽어 채널별로 재조립해 기존 reb: / rim:
완료 경로와 동일하게 downstream 콜백을 발생시킨다.

- PiezoPacketCollector: rec: chunk → channelResults[chNum] 재조립, session/dup
  규칙은 reb: 와 동일. raa: 도착 시 완료 검사.
- ImuPacketCollector: ric: chunk → onComplete(15 samples), offset=0 재도착 시
  이전 partial 파기.
- BleManager.processReceivedData: rec: / ric: 케이스 dispatch + debug log.
- PiezoRecReassemblyTest / ImuRicReassemblyTest: 실측 84+16 / 14+1 split
  케이스 + 재전송/중복/세션 변경 회귀 방지.

정상 MTU 247 기기의 reb: / rim: 경로에는 변경 없음.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-21 18:01:28 +09:00

125 lines
4.4 KiB
Kotlin

package com.medithings.vesiscan.ble
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
/**
* ric: (MTU 분할 IMU) 재조립 파서 검증.
*
* Firmware 실측 (MTU=185, 2026-07-21 log):
* ric off=0 total=15 chunk=14 len=180
* ric off=14 total=15 chunk=1 len=24
*
* IMU sample 은 12B (Ax Ay Az Gx Gy Gz, 각 2B BE int16, unsigned overflow signed).
* Test 는 15 sample 을 14+1 로 쪼개 주입하고 onComplete 가 정확한 순서/값을 복원하는지 확인.
*/
class ImuRicReassemblyTest {
/**
* ric: 헤더 10B + IMU 12·chunk B + CRC 2B.
* [tag "ric:"] [off BE] [total BE] [chunk BE] [12B·chunk] [CRC 00 00]
*/
private fun makeRicChunk(
offset: Int,
total: Int,
chunkSamples: Int,
rawSamples: Array<IntArray>, // 각 IntArray = [ax, ay, az, gx, gy, gz] (int16 raw)
): ByteArray {
require(rawSamples.size == chunkSamples)
val size = 10 + chunkSamples * 12 + 2
val out = ByteArray(size)
out[0] = 'r'.code.toByte()
out[1] = 'i'.code.toByte()
out[2] = 'c'.code.toByte()
out[3] = ':'.code.toByte()
out[4] = ((offset ushr 8) and 0xFF).toByte()
out[5] = (offset and 0xFF).toByte()
out[6] = ((total ushr 8) and 0xFF).toByte()
out[7] = (total and 0xFF).toByte()
out[8] = ((chunkSamples ushr 8) and 0xFF).toByte()
out[9] = (chunkSamples and 0xFF).toByte()
var p = 10
for (s in rawSamples) {
require(s.size == 6)
for (v in s) {
val vi = v and 0xFFFF
out[p++] = ((vi ushr 8) and 0xFF).toByte()
out[p++] = (vi and 0xFF).toByte()
}
}
out[p] = 0; out[p + 1] = 0
return out
}
/** 예측 가능한 raw 15 sample: Az = 8192 (=1g), 그 외는 sample index 로 구분. */
private fun makeRawSamples(count: Int): Array<IntArray> = Array(count) { i ->
intArrayOf(
/* ax */ i * 10,
/* ay */ i * 10 + 1,
/* az */ 8192, // 정확히 +1g
/* gx */ i,
/* gy */ i * 2,
/* gz */ i * 3,
)
}
@Test
fun `ric 14+1 split reassembles 15 samples in order`() {
val collector = ImuPacketCollector()
var completed: List<ImuSample>? = null
collector.onComplete = { completed = it }
collector.onLog = { /* println(it) */ }
val all = makeRawSamples(15)
val first14 = all.copyOfRange(0, 14)
val last1 = all.copyOfRange(14, 15)
collector.parseRic(makeRicChunk(offset = 0, total = 15, chunkSamples = 14, rawSamples = first14))
collector.parseRic(makeRicChunk(offset = 14, total = 15, chunkSamples = 1, rawSamples = last1))
val result = completed
assertNotNull("onComplete 가 호출되어야 함", result)
assertEquals(15, result!!.size)
// Az = 8192 / 8192 = 1.0 g
for (i in 0 until 15) {
assertEquals("sample[$i].az", 1.0f, result[i].az, 1e-6f)
assertEquals("sample[$i].ax", (i * 10) / 8192f, result[i].ax, 1e-6f)
assertEquals("sample[$i].ay", (i * 10 + 1) / 8192f, result[i].ay, 1e-6f)
assertEquals("sample[$i].gx", i / 65.536f, result[i].gx, 1e-6f)
}
}
@Test
fun `ric single chunk (chunk == total) also completes`() {
val collector = ImuPacketCollector()
var completed: List<ImuSample>? = null
collector.onComplete = { completed = it }
val all = makeRawSamples(15)
collector.parseRic(makeRicChunk(0, 15, 15, all))
assertNotNull(completed)
assertEquals(15, completed!!.size)
}
@Test
fun `ric restart on new offset=0 discards partial`() {
val collector = ImuPacketCollector()
var completed: List<ImuSample>? = null
collector.onComplete = { completed = it }
// 부분 도착
val partial = makeRawSamples(15).copyOfRange(0, 14)
collector.parseRic(makeRicChunk(0, 15, 14, partial))
// 완성 전 새 set 시작 (off=0) → 부분 버림
val newSet = Array(15) { i -> intArrayOf(999 + i, 0, 8192, 0, 0, 0) }
collector.parseRic(makeRicChunk(0, 15, 15, newSet))
assertNotNull(completed)
assertEquals(15, completed!!.size)
// 새 set 값이 반영 (ax=999 시작)
assertEquals(999f / 8192f, completed!![0].ax, 1e-6f)
}
}