diff --git a/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt b/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt index 731eb60..d6b72ac 100644 --- a/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt +++ b/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt @@ -121,30 +121,75 @@ class BleManager private constructor(private val context: Context) { return m.groupValues[1].toIntOrNull() } - /** 펌웨어 버전이 MIN_FIRMWARE_BUILD 미만이면 true. 모르거나(아직 미수신) 형식 안 맞으면 false. - * legacy "VBTFWxxxx" 포맷 대상. 신규 semver ("1.0.0+5") 는 무시. */ + /** + * 2026-08-05: FW 팀 스펙 (VBTFW 코드표) 반영. 두 포맷 통일 매핑. + * + * A. VBTFW MMNN → (M, 0, N) + * 예: VBTFW0100 = 1.0.0, VBTFW0200 = 2.0.0, VBTFW0201 = 2.0.1 + * B. MCUboot major.minor.patch[+build] → (major, minor, patch) + * 예: 1.0.0+5 = 1.0.0 (build 5 는 major/minor/patch 비교와 무관) + * + * Returns Triple(major, minor, patch) · 파싱 실패 시 null. + */ + private fun parseFwSemver(raw: String): Triple? { + // VBTFW / VB0FW / VBFW MMNN + val vbt = Regex("""VB\d?FW(\d{2})(\d{2})""").find(raw) + if (vbt != null) { + val major = vbt.groupValues[1].toIntOrNull() ?: return null + val patch = vbt.groupValues[2].toIntOrNull() ?: return null + return Triple(major, 0, patch) + } + // MCUboot semver + val mcu = Regex("""(\d+)\.(\d+)\.(\d+)""").find(raw) + if (mcu != null) { + return Triple( + mcu.groupValues[1].toInt(), + mcu.groupValues[2].toInt(), + mcu.groupValues[3].toInt(), + ) + } + return null + } + + /** 튜플 비교: a>b 면 양수, 같으면 0, 작으면 음수. */ + private fun compareSemver(a: Triple, b: Triple): Int { + if (a.first != b.first) return a.first - b.first + if (a.second != b.second) return a.second - b.second + return a.third - b.third + } + + /** 펌웨어 버전이 MIN_FIRMWARE_BUILD 미만이면 true. legacy "VBTFWxxxx" 만 판정. + * 신규 semver 는 별도 update-available 로 처리. */ val isFirmwareOutdated: Boolean get() { val fw = firmwareVersion.value if (fw.isEmpty()) return false - // 신규 semver 포맷 (1.0.0+5 등) 은 이 legacy build-number 비교 대상 아님. if (fw.startsWith("1.") || fw.contains("+")) return false val n = parseFirmwareBuild(fw) ?: return false return n < MIN_FIRMWARE_BUILD } /** - * 앱에 번들된 DFU 펌웨어와 기기 실행 중 펌웨어 버전이 다르면 true (2026-08-04). - * → 사용자에게 "새 펌웨어 사용 가능 · 업데이트 하시겠습니까?" 배너 노출. - * 정확한 match (contains) 로 비교 · 미수신 (empty) 시 false (모르는 것과 필요없는 것 구분). + * 앱 번들 DFU 가 기기 fw 보다 **엄격히 최신** 이면 true (2026-08-05). + * → "새 펌웨어 사용 가능" 배너 노출. + * + * 매핑 규칙 (FW 팀 스펙 · VBTFW 코드표): + * VBTFW MMNN = M.0.N (major=MM, minor=0, patch=NN) + * MCUboot X.Y.Z+B = (X, Y, Z) · +B build 는 무시 (같은 major.minor.patch 는 동일 취급) + * + * 예: + * device VBTFW0200 (2.0.0) · bundled 1.0.0+5 (1.0.0) → bundled < device → **false (배너 X)** + * device VBTFW0100 (1.0.0) · bundled 2.0.0+0 (2.0.0) → bundled > device → **true (배너 O)** + * device VBTFW0200 (2.0.0) · bundled 2.0.1+0 (2.0.1) → bundled > device → true */ val isFirmwareUpdateAvailable: Boolean get() { val fw = firmwareVersion.value if (fw.isEmpty()) return false val bundled = com.medithings.vesiscan.firmware.FirmwareLoader.BUNDLED_VERSION - // fw 가 bundled 버전 문자열을 포함하면 최신 (예: "VesiScan 1.0.0+5" · "1.0.0+5" 등) - return !fw.contains(bundled) + val devTuple = parseFwSemver(fw) ?: return false + val bunTuple = parseFwSemver(bundled) ?: return false + return compareSemver(bunTuple, devTuple) > 0 } val batteryLevel = mutableStateOf(0)