feat(firmware): 새 DFU zip (1.0.0+5) 번들 + 버전 불일치 시 업데이트 배너

DFU 파일:
  - dfu_application_2+35.zip (구) 삭제
  - dfu_application_1.0.0+5.zip (신규 238KB) 추가

FirmwarePackage:
  - DEFAULT_ASSET = "dfu_application_1.0.0+5.zip"
  - BUNDLED_VERSION = "1.0.0+5" 상수 신규 (manifest.json version_MCUBOOT 와 일치)

BleManager:
  - isFirmwareOutdated: 신규 semver 포맷 ("1.0.0+5") 은 legacy VBTFW build-number
    비교 제외 (false positive 방지 · parseFirmwareBuild 가 tail 숫자 5 를 118 미만
    으로 오판정 안 하도록).
  - isFirmwareUpdateAvailable (신규): 런타임 fw 문자열이 BUNDLED_VERSION 을 포함
    하지 않으면 true. 사용자에게 "새 펌웨어 사용 가능" 배너 노출.

FirmwareWarningBanner:
  - update-available 파랑 배너 신규 (정보성 · 우선순위 outdated > timedOut > update).
  - onUpdateAvailableTap 콜백 · 파랑 배너 탭 시 FIRMWARE_UPDATE 화면 이동.
  - PiezoMonitoringView · PlacementGuideView 호출부에 콜백 전달.

strings.xml (KR/EN):
  - firmware_update_available_title/desc

동작 흐름:
  1. 앱 연결 → mid?/rid: 로 fw 수신 (예: "VBTFW0200")
  2. isFirmwareUpdateAvailable → true (fw !contains "1.0.0+5")
  3. 상단 파랑 배너 "새 펌웨어가 있습니다 · 기기: VBTFW0200 · 신규: 1.0.0+5" 노출
  4. 탭 → FIRMWARE_UPDATE 화면 → DFU 진행 → 성공 시 fw 갱신 후 배너 자동 사라짐

향후 FW 팀이 버전 스키마 확정 시 BUNDLED_VERSION 만 갱신하면 됨.
This commit is contained in:
2026-08-04 17:24:59 +09:00
parent a992a88b86
commit 4eb67e7450
9 changed files with 74 additions and 15 deletions
@@ -121,13 +121,32 @@ class BleManager private constructor(private val context: Context) {
return m.groupValues[1].toIntOrNull()
}
/** 펌웨어 버전이 MIN_FIRMWARE_BUILD 미만이면 true. 모르거나(아직 미수신) 형식 안 맞으면 false. */
/** 펌웨어 버전이 MIN_FIRMWARE_BUILD 미만이면 true. 모르거나(아직 미수신) 형식 안 맞으면 false.
* legacy "VBTFWxxxx" 포맷 대상. 신규 semver ("1.0.0+5") 는 무시. */
val isFirmwareOutdated: Boolean
get() {
val n = parseFirmwareBuild(firmwareVersion.value) ?: return false
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 (모르는 것과 필요없는 것 구분).
*/
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 batteryLevel = mutableStateOf(0)
/** 현재 연결된 디바이스 RSSI (dBm) · 미측정/미연결 시 null. dev-mode UI 오버레이용. */
val rssi = mutableStateOf<Int?>(null)
@@ -45,7 +45,13 @@ data class FirmwarePackage(
*/
object FirmwareLoader {
const val DEFAULT_ASSET = "dfu_application_2+35.zip"
const val DEFAULT_ASSET = "dfu_application_1.0.0+5.zip"
/**
* 번들된 DFU 버전 (manifest.json 의 version_MCUBOOT 필드와 일치).
* 2026-08-04: BleManager 의 update-available 배너가 이 값 vs 런타임 FW 를 비교.
*/
const val BUNDLED_VERSION = "1.0.0+5"
private const val MANIFEST = "manifest.json"
@@ -1,6 +1,7 @@
package com.medithings.vesiscan.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -39,7 +40,11 @@ import kotlinx.coroutines.delay
* (3) 정상 또는 미연결 — 안 보임
*/
@Composable
fun FirmwareWarningBanner(bleManager: BleManager, modifier: Modifier = Modifier) {
fun FirmwareWarningBanner(
bleManager: BleManager,
modifier: Modifier = Modifier,
onUpdateAvailableTap: (() -> Unit)? = null, // 2026-08-04 · 파랑 배너 탭 시 DFU 이동
) {
val fw by bleManager.firmwareVersion
val isConnected by bleManager.isConnected
val lastRxAt by bleManager.lastBleRxAt
@@ -76,25 +81,42 @@ fun FirmwareWarningBanner(bleManager: BleManager, modifier: Modifier = Modifier)
val deviceAlive = lastRxAt > 0 && (System.currentTimeMillis() - lastRxAt) < 12_000
val outdated = bleManager.isFirmwareOutdated
val updateAvailable = bleManager.isFirmwareUpdateAvailable
val showTimedOut = timedOut && !deviceAlive
if (!outdated && !showTimedOut) return
if (!outdated && !showTimedOut && !updateAvailable) return
val bgColor = if (outdated) Color(0xFFFF9500) else Color(0xFFD32F2F)
val title = if (outdated) stringResource(R.string.firmware_outdated_title) else stringResource(R.string.firmware_no_response_title)
// timedOut(mid? 미수신) 케이스는 사용자에게 mid?/펌웨어 호환 같은 기술 용어가 무의미.
// outdated 일 때만 상세 안내, timedOut 은 타이틀만 노출.
val subtitle: String? = if (outdated) {
stringResource(R.string.firmware_outdated_desc, fw, BleManager.MIN_FIRMWARE_BUILD)
} else {
null
// 우선순위: outdated (빨강) > timedOut (빨강) > updateAvailable (파랑 · 정보성).
val bgColor = when {
outdated -> Color(0xFFFF9500) // 주황 · 강한 경고
showTimedOut -> Color(0xFFD32F2F) // 빨강 · 응답 없음
else -> Color(0xFF1976D2) // 파랑 · 업데이트 사용 가능
}
val title = when {
outdated -> stringResource(R.string.firmware_outdated_title)
showTimedOut -> stringResource(R.string.firmware_no_response_title)
else -> stringResource(R.string.firmware_update_available_title)
}
val subtitle: String? = when {
outdated -> stringResource(R.string.firmware_outdated_desc, fw, BleManager.MIN_FIRMWARE_BUILD)
showTimedOut -> null
else -> stringResource(
R.string.firmware_update_available_desc,
fw,
com.medithings.vesiscan.firmware.FirmwareLoader.BUNDLED_VERSION,
)
}
// 파랑 배너 (update available) 는 탭 가능 · 나머지는 정보 표시만.
val clickableModifier = if (updateAvailable && onUpdateAvailableTap != null) {
Modifier.clickable { onUpdateAvailableTap() }
} else Modifier
Row(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 6.dp)
.clip(RoundedCornerShape(10.dp))
.background(bgColor)
.then(clickableModifier)
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
@@ -1323,7 +1323,10 @@ fun PiezoMonitoringView(appState: AppState) {
// BLE 끊김 banner는 사내 테스트 피드백으로 제거됨 — 백그라운드 자동 재연결만 유지.
// 펌웨어 구버전 경고
com.medithings.vesiscan.ui.components.FirmwareWarningBanner(bleManager)
com.medithings.vesiscan.ui.components.FirmwareWarningBanner(
bleManager,
onUpdateAvailableTap = { appState.currentScreen = com.medithings.vesiscan.AppScreen.FIRMWARE_UPDATE },
)
com.medithings.vesiscan.ui.components.BatteryWarningBanner(bleManager)
val scrollState = rememberScrollState()
@@ -300,7 +300,10 @@ fun PlacementGuideView(appState: AppState) {
}
// 펌웨어 구버전 경고
com.medithings.vesiscan.ui.components.FirmwareWarningBanner(bleManager)
com.medithings.vesiscan.ui.components.FirmwareWarningBanner(
bleManager,
onUpdateAvailableTap = { appState.currentScreen = com.medithings.vesiscan.AppScreen.FIRMWARE_UPDATE },
)
// 배터리 잔량 경고 (<15%)
com.medithings.vesiscan.ui.components.BatteryWarningBanner(bleManager)