739d52e472
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>
125 lines
4.4 KiB
Kotlin
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)
|
|
}
|
|
}
|