ble: rid: 파서 fixed-width 12B 필드로 + mfv? fallback

명세서 (BLE Command Table) 재확인:
  rid: = [tag 4B] [HW 12B] [Serial 12B] [FW 12B] [crc 2B] = 42B
  각 12B 필드는 ASCII 가변 + NULL/공백/제어문자 padding

기존 파서는 whitespace split 후 prefix 매칭. padding 이 NULL
이면 동작하지만 다른 바이트(예: 0xFF, 0x20 외 임의값)면 전체가
한 토큰으로 합쳐져 fw 추출 실패. 새 펌웨어에서 firmware 안
뜨던 증상의 원인.

수정:
- field(offset) 헬퍼로 정확히 12B 씩 슬라이스 후 NULL/제어 제거
- 명세대로 4 / 16 / 28 offset 에서 HW / SN / FW 분리

추가 fallback:
- sendFirmwareVersionQuery() 신규 — mfv? 단독 쿼리
- rfv: 응답 핸들러 추가
- service ready 4초 후 firmwareVersion 여전히 비어있으면
  mfv? 자동 송신 (mid? 미지원 펌웨어 대응)
This commit is contained in:
2026-06-11 15:37:14 +09:00
parent f85b5cfbed
commit 497b08164e
@@ -536,6 +536,11 @@ class BleManager private constructor(private val context: Context) {
sendRaw(CRC16.buildCommandASCII("mid", " ")) sendRaw(CRC16.buildCommandASCII("mid", " "))
} }
/** mfv? — FW Version only. mid? 응답 안 오는 펌웨어용 fallback. */
fun sendFirmwareVersionQuery() {
sendRaw(CRC16.buildCommandASCII("mfv", " "))
}
fun sendLedMode(state: Int) { fun sendLedMode(state: Int) {
sendRaw(CRC16.buildCommandBE("mls", intArrayOf(state))) sendRaw(CRC16.buildCommandBE("mls", intArrayOf(state)))
} }
@@ -995,6 +1000,13 @@ class BleManager private constructor(private val context: Context) {
startBatteryPolling() startBatteryPolling()
startWatchdog() startWatchdog()
sendDeviceInfoQuery() sendDeviceInfoQuery()
// 4초 후에도 펌웨어 미수신이면 mfv? fallback (구 펌웨어는 mid? 미지원 가능성)
handler.postDelayed({
if (firmwareVersion.value.isEmpty() && isConnected.value) {
debugLogger.info("FW fallback: sending mfv?")
sendFirmwareVersionQuery()
}
}, 4_000)
logd { "Service ready! (CCCD written)" } logd { "Service ready! (CCCD written)" }
} }
} else { } else {
@@ -1115,27 +1127,38 @@ class BleManager private constructor(private val context: Context) {
} }
} }
"rid:" -> { "rid:" -> {
// BLE 응답 끝에 NULL byte padding이 붙는 경우가 있음 — PostgreSQL JSONB가 // 명세: [tag 4B] [HW 12B] [Serial 12B] [FW 12B] [crc 2B] = 42B.
// 0x00을 거부하므로 trim()으로 안 빠지는 NULL/제어문자를 명시적으로 제거. // 각 12B 필드는 ASCII 가변 길이 + NULL/공백/제어문자 padding.
val info = if (data.size > 4) // (구) whitespace split 방식은 padding 이 space 가 아닐 때 깨졌음.
String(data, 4, data.size - 4, Charsets.US_ASCII) fun field(offset: Int): String {
.replace(Regex("[\\x00-\\x1F\\x7F]"), " ").trim() val end = (offset + 12).coerceAtMost(data.size - 2) // CRC 2B 앞까지
else "?" if (end <= offset) return ""
return String(data, offset, end - offset, Charsets.US_ASCII)
.replace(Regex("[\\x00-\\x1F\\x7F]"), "").trim()
}
val hw = field(4)
val sn = field(16)
val fw = field(28)
val info = "$hw $sn $fw".trim()
debugLogger.rx("rid", data.size, "device_info=$info") debugLogger.rx("rid", data.size, "device_info=$info")
debugLogger.info("DEVICE_INFO $info") debugLogger.info("DEVICE_INFO hw=$hw sn=$sn fw=$fw")
// 형식: "VBTHW0100 VBT26040001 VBTFW0111" (HW, Serial, FW)
val parts = info.split(Regex("\\s+")).filter { it.isNotBlank() }
val hw = parts.firstOrNull { it.startsWith("VBTHW") } ?: ""
val fw = parts.firstOrNull { it.startsWith("VBTFW") } ?: ""
val sn = parts.firstOrNull { it.startsWith("VBT") && !it.startsWith("VBTHW") && !it.startsWith("VBTFW") } ?: ""
handler.post { handler.post {
if (hw.isNotEmpty()) hardwareVersion.value = hw if (hw.isNotEmpty()) hardwareVersion.value = hw
if (fw.isNotEmpty()) firmwareVersion.value = fw if (fw.isNotEmpty()) firmwareVersion.value = fw
if (sn.isNotEmpty()) serialNumber.value = sn if (sn.isNotEmpty()) serialNumber.value = sn
} }
// ble.log에 헤더 블록 형태로 (텍스트 에디터로 열었을 때 즉시 보임)
debugLogger.deviceInfo(fw, hw, sn) debugLogger.deviceInfo(fw, hw, sn)
} }
"rfv:" -> {
// 명세: [tag 4B] [FW ASCII N B] [crc 2B] — FW 만 별도 쿼리(mfv?) 응답.
// mid? 가 응답 안 하는 펌웨어 fallback 용.
if (data.size > 6) {
val fw = String(data, 4, data.size - 6, Charsets.US_ASCII)
.replace(Regex("[\\x00-\\x1F\\x7F]"), "").trim()
debugLogger.rx("rfv", data.size, "fw=$fw")
if (fw.isNotEmpty()) handler.post { firmwareVersion.value = fw }
}
}
"rls:" -> { "rls:" -> {
val state = if (data.size >= 5) data[4].toInt() and 0xFF else -1 val state = if (data.size >= 5) data[4].toInt() and 0xFF else -1
debugLogger.rx("rls", data.size, "led_state=$state") debugLogger.rx("rls", data.size, "led_state=$state")