4eb67e7450
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 만 갱신하면 됨.
118 lines
4.2 KiB
Kotlin
118 lines
4.2 KiB
Kotlin
/*
|
|
* MEDiDFU — VesiSeek firmware updater
|
|
*
|
|
* Unpacks the DFU archive bundled in assets/ and extracts the signed MCUboot image.
|
|
*
|
|
* Author: Charles KWON OhJun <charleskwon@medithings.co.kr>
|
|
* MEDiThings Inc.
|
|
*/
|
|
package com.medithings.vesiscan.firmware
|
|
import android.content.Context
|
|
import org.json.JSONObject
|
|
import java.io.ByteArrayOutputStream
|
|
import java.util.zip.ZipInputStream
|
|
|
|
/** A signed MCUboot application image unpacked from a `dfu_application_*.zip`. */
|
|
data class FirmwarePackage(
|
|
val assetName: String,
|
|
val imageFileName: String,
|
|
/** `version_MCUBOOT` from the manifest, e.g. "0.0.2+35". Empty when the manifest omits it. */
|
|
val version: String,
|
|
val image: ByteArray,
|
|
) {
|
|
val sizeBytes: Int get() = image.size
|
|
|
|
// Generated equals/hashCode would compare the ByteArray by reference; identify the package by
|
|
// its source instead.
|
|
override fun equals(other: Any?): Boolean =
|
|
this === other || (other is FirmwarePackage &&
|
|
assetName == other.assetName &&
|
|
imageFileName == other.imageFileName &&
|
|
version == other.version &&
|
|
image.size == other.image.size)
|
|
|
|
override fun hashCode(): Int =
|
|
(((assetName.hashCode() * 31 + imageFileName.hashCode()) * 31) + version.hashCode()) * 31 +
|
|
image.size
|
|
}
|
|
|
|
/**
|
|
* Reads a DFU zip bundled in `assets/`.
|
|
*
|
|
* The zip is the nRF Connect SDK / Zephyr layout: a `manifest.json` listing the images plus the
|
|
* signed binaries themselves. The application image (`image_index` 0, or `type` "application") is
|
|
* the one uploaded over SMP.
|
|
*/
|
|
object FirmwareLoader {
|
|
|
|
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"
|
|
|
|
@Throws(IllegalStateException::class)
|
|
fun loadFromAssets(context: Context, assetName: String = DEFAULT_ASSET): FirmwarePackage {
|
|
val entries = readZipEntries(context, assetName)
|
|
|
|
val manifestBytes = entries.entries
|
|
.firstOrNull { it.key.equals(MANIFEST, ignoreCase = true) }
|
|
?.value
|
|
?: error("$MANIFEST not found in $assetName")
|
|
|
|
val files = JSONObject(String(manifestBytes, Charsets.UTF_8)).optJSONArray("files")
|
|
?: error("$MANIFEST has no \"files\" array")
|
|
check(files.length() > 0) { "$MANIFEST lists no images" }
|
|
|
|
var chosen = files.getJSONObject(0)
|
|
for (i in 0 until files.length()) {
|
|
val entry = files.getJSONObject(i)
|
|
if (entry.optString("image_index") == "0" ||
|
|
entry.optString("type").equals("application", ignoreCase = true)
|
|
) {
|
|
chosen = entry
|
|
break
|
|
}
|
|
}
|
|
|
|
val fileName = chosen.optString("file")
|
|
check(fileName.isNotEmpty()) { "$MANIFEST entry has no \"file\" field" }
|
|
val image = entries[fileName] ?: error("$fileName listed in $MANIFEST but missing from the zip")
|
|
check(image.isNotEmpty()) { "$fileName is empty" }
|
|
|
|
return FirmwarePackage(
|
|
assetName = assetName,
|
|
imageFileName = fileName,
|
|
version = chosen.optString("version_MCUBOOT"),
|
|
image = image,
|
|
)
|
|
}
|
|
|
|
private fun readZipEntries(context: Context, assetName: String): Map<String, ByteArray> {
|
|
val entries = LinkedHashMap<String, ByteArray>()
|
|
ZipInputStream(context.assets.open(assetName)).use { zip ->
|
|
val buffer = ByteArray(16 * 1024)
|
|
while (true) {
|
|
val entry = zip.nextEntry ?: break
|
|
if (entry.isDirectory) {
|
|
zip.closeEntry()
|
|
continue
|
|
}
|
|
val out = ByteArrayOutputStream()
|
|
while (true) {
|
|
val read = zip.read(buffer)
|
|
if (read < 0) break
|
|
out.write(buffer, 0, read)
|
|
}
|
|
entries[entry.name] = out.toByteArray()
|
|
zip.closeEntry()
|
|
}
|
|
}
|
|
return entries
|
|
}
|
|
}
|