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 = [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 = 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? = 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? = 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? = 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) } }