984fe2c939
feature/cloud-mvp 에서 최근 만들어진 3가지 사용자용 기능을 demo-final 에도 적용.
demo-final 옛 폴더 구조 유지 (folder reorg 안 됨) · 각 파일 위치 수동 매핑.
이식 대상:
1. Firmware DFU (cloud-mvp 672e679):
- firmware/ 폴더 신규 · MEDiDFU 6 파일 이식 (DfuManager 459 line 등)
- assets/dfu_application_2+35.zip (243 KB · signed MCUboot)
- libs.versions.toml: mcumgr = "2.5.0" (Kotlin 2.0 호환)
- build.gradle.kts: mcumgr-ble + lifecycle-runtime-compose 추가
- AppScreen.FIRMWARE_UPDATE enum 추가
- MainActivity: 라우팅 + 뒤로가기 (→ HOME)
- HomeView: "펌웨어 업데이트 (프로브 firmware)" TextButton 추가 (Dev clinical 버튼 밑)
2. BatteryWarningBanner tap-to-dismiss (cloud-mvp aa6cc1f):
- clickable { dismissed = true } + 우측 X 아이콘
- WARNING → CRITICAL 악화 시 자동 재표시
- 재연결 시 dismissed 초기화
3. AppState.isDeviceConnected getter (cloud-mvp 9c82a50):
- mutableStateOf → getter 로 변경 (BleManager.isConnected.value 실시간)
- Compose 자동 재구성 · setter 는 no-op 로 호환 유지
- 홈화면 "연결됨" · Start 후 "연결해제" 불일치 해결
주요 파일 매핑 (feature/cloud-mvp → demo-final):
- core/AppState.kt → AppState.kt (root)
- core/MainActivity.kt → MainActivity.kt (root)
- common/ui/components/BatteryWarningBanner.kt → ui/components/BatteryWarningBanner.kt
- firmware/** → firmware/** (신규 · 동일)
- shell/ui/SettingsTabView.kt (진입점 원본) → HomeView 로 대체 (demo 는 SettingsTab 없음)
검증: BUILD SUCCESSFUL (demoDebug 1m 56s) · APK install PASS
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
78 lines
2.9 KiB
Kotlin
78 lines
2.9 KiB
Kotlin
/*
|
|
* MEDiDFU — VesiSeek firmware updater
|
|
*
|
|
* VesiSeek BLE identifiers and the NUS command framing shared with the firmware.
|
|
*
|
|
* Author: Charles KWON OhJun <charleskwon@medithings.co.kr>
|
|
* MEDiThings Inc.
|
|
*/
|
|
package com.medithings.vesiscan.firmware
|
|
import java.util.UUID
|
|
|
|
/**
|
|
* VesiSeek BLE identifiers.
|
|
*
|
|
* During normal operation the device advertises the Nordic UART Service. After the `mdf?` command
|
|
* it disconnects and re-advertises with the Zephyr SMP service instead, keeping the same
|
|
* advertised name (its serial number).
|
|
*/
|
|
object VesiSeekUuids {
|
|
val NUS_SERVICE: UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
|
|
/** host -> device (write) */
|
|
val NUS_WRITE: UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
|
|
/** device -> host (notify) */
|
|
val NUS_NOTIFY: UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E")
|
|
val CCCD: UUID = UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
|
|
/** Zephyr SMP-over-BLE service, advertised only in DFU mode. */
|
|
val SMP_SERVICE: UUID = UUID.fromString("8D53DC1D-1DB7-4CD3-868B-8A527460AA84")
|
|
}
|
|
|
|
/**
|
|
* CRC-16/CCITT (poly 0x1021, init 0xFFFF) — byte-for-byte identical to the firmware's
|
|
* `cmd_crc16_compute` (Nordic SDK `crc16_compute`).
|
|
*/
|
|
object Crc16 {
|
|
fun compute(data: ByteArray): Int {
|
|
var crc = 0xFFFF
|
|
for (byte in data) {
|
|
crc = crc xor ((byte.toInt() and 0xFF) shl 8)
|
|
repeat(8) {
|
|
crc = if ((crc and 0x8000) != 0) (crc shl 1) xor 0x1021 else crc shl 1
|
|
crc = crc and 0xFFFF
|
|
}
|
|
}
|
|
return crc
|
|
}
|
|
|
|
/** Appends the CRC little-endian, matching the firmware's packet layout. */
|
|
fun appendTo(data: ByteArray): ByteArray {
|
|
val crc = compute(data)
|
|
return data + byteArrayOf((crc and 0xFF).toByte(), ((crc shr 8) and 0xFF).toByte())
|
|
}
|
|
}
|
|
|
|
/** NUS packet framing: `[TAG 4B][DATA N][CRC16 2B little-endian]`. */
|
|
object NusPacket {
|
|
|
|
fun build(tag: String, data: ByteArray = ByteArray(0)): ByteArray {
|
|
require(tag.length == 4) { "tag must be 4 characters, got '$tag'" }
|
|
return Crc16.appendTo(tag.toByteArray(Charsets.US_ASCII) + data)
|
|
}
|
|
|
|
/**
|
|
* Enter-DFU command. The firmware ignores the payload but the command dispatcher needs at
|
|
* least one data byte, so a single 0x00 is sent — the same packet the Go host tool uses.
|
|
*/
|
|
fun enterDfu(): ByteArray = build("mdf?", byteArrayOf(0x00))
|
|
|
|
/** Returns the 4-character response tag, or null if the frame is too short or the CRC fails. */
|
|
fun tagOf(packet: ByteArray): String? {
|
|
if (packet.size < 6) return null
|
|
val body = packet.copyOfRange(0, packet.size - 2)
|
|
val received = (packet[packet.size - 2].toInt() and 0xFF) or
|
|
((packet[packet.size - 1].toInt() and 0xFF) shl 8)
|
|
if (Crc16.compute(body) != received) return null
|
|
return String(packet, 0, 4, Charsets.US_ASCII)
|
|
}
|
|
}
|