diff --git a/app/src/main/assets/dfu_application_1.0.0+5.zip b/app/src/main/assets/dfu_application_1.0.0+5.zip new file mode 100644 index 0000000..b9d4790 Binary files /dev/null and b/app/src/main/assets/dfu_application_1.0.0+5.zip differ diff --git a/app/src/main/assets/dfu_application_2+35.zip b/app/src/main/assets/dfu_application_2+35.zip deleted file mode 100644 index 0165e2e..0000000 Binary files a/app/src/main/assets/dfu_application_2+35.zip and /dev/null differ 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 446ff98..731eb60 100644 --- a/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt +++ b/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt @@ -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(null) diff --git a/app/src/main/java/com/medithings/vesiscan/firmware/FirmwarePackage.kt b/app/src/main/java/com/medithings/vesiscan/firmware/FirmwarePackage.kt index f73e31f..0cf7aca 100644 --- a/app/src/main/java/com/medithings/vesiscan/firmware/FirmwarePackage.kt +++ b/app/src/main/java/com/medithings/vesiscan/firmware/FirmwarePackage.kt @@ -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" diff --git a/app/src/main/java/com/medithings/vesiscan/ui/components/FirmwareWarningBanner.kt b/app/src/main/java/com/medithings/vesiscan/ui/components/FirmwareWarningBanner.kt index 2204273..6bc8d60 100644 --- a/app/src/main/java/com/medithings/vesiscan/ui/components/FirmwareWarningBanner.kt +++ b/app/src/main/java/com/medithings/vesiscan/ui/components/FirmwareWarningBanner.kt @@ -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 ) { diff --git a/app/src/main/java/com/medithings/vesiscan/ui/views/monitoring/PiezoMonitoringView.kt b/app/src/main/java/com/medithings/vesiscan/ui/views/monitoring/PiezoMonitoringView.kt index 941bc81..3d27181 100644 --- a/app/src/main/java/com/medithings/vesiscan/ui/views/monitoring/PiezoMonitoringView.kt +++ b/app/src/main/java/com/medithings/vesiscan/ui/views/monitoring/PiezoMonitoringView.kt @@ -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() diff --git a/app/src/main/java/com/medithings/vesiscan/ui/views/monitoring/PlacementGuideView.kt b/app/src/main/java/com/medithings/vesiscan/ui/views/monitoring/PlacementGuideView.kt index 30b3bb0..c824516 100644 --- a/app/src/main/java/com/medithings/vesiscan/ui/views/monitoring/PlacementGuideView.kt +++ b/app/src/main/java/com/medithings/vesiscan/ui/views/monitoring/PlacementGuideView.kt @@ -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) diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index fed94fe..63d35d8 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -177,6 +177,9 @@ 펌웨어 업데이트 필요 기기 응답 없음 현재: %1$s · 필요: VBTFW%2$04d 이상. 측정이 정상 동작하지 않을 수 있습니다. + + 새 펌웨어가 있습니다 + 기기: %1$s · 신규: %2$s. 눌러서 DFU 업데이트를 진행하세요. 잠금 해제됨 — 다시 탭하여 잠금 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7f1edef..bfa7302 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -177,6 +177,9 @@ Firmware update required No response from device Current: %1$s · Required: VBTFW%2$04d or higher. Measurement may not work properly. + + New firmware available + Device: %1$s · New: %2$s. Tap to update via DFU. Unlocked — tap again to lock