From 33e5946895d417d922bd848086314055545bd53f Mon Sep 17 00:00:00 2001 From: jjangddu Date: Mon, 7 Sep 2026 15:49:53 +0900 Subject: [PATCH] =?UTF-8?q?feat(ble):=20=ED=94=84=EB=A1=9C=EB=B8=8C=20?= =?UTF-8?q?=EC=B8=A1=EC=A0=95=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20?= =?UTF-8?q?=EC=9D=BD=EA=B8=B0(mcf=3F)=20=C2=B7=20=EC=9E=84=EC=83=81=20?= =?UTF-8?q?=EC=A0=84=ED=9B=84=20=EB=B3=B4=EC=A1=B4=20=EB=8F=84=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 지금까지 프로브에 무엇이 저장돼 있는지 **알 방법이 없었다.** `mcs?`(쓰기)의 echo `rcs:` 로만 알 수 있었는데, 그건 이미 값을 바꾼 뒤다. · sendPiezoConfigQuery() — `mcf?` [tag][space][crc] 7B. 응답 `rcf:` 는 배치가 `rcs:` 와 같아 파서를 그대로 옮겼다(실패 시 freq=0xFFFF 도 동일). · 응답은 piezoConfigRead 로 받는다. piezoConfigEcho 에 섞으면 안 된다 — 그쪽은 "내가 방금 쓴 게 먹혔나"를 확인하는 자리라 쓰기 직전에 null 로 비우고 기다린다. 조회 응답이 같은 곳에 들어오면 쓰기 검증이 엉뚱한 값을 보고 통과한다. ProbeConfigGuard — 임상 측정 전후로 파라미터를 보존한다. 정렬과 병원 임상 모드는 자기 조건을 프로브 FDS 에 써 넣고 되돌리지 않는다. 일반 측정 화면은 mcs 를 아예 보내지 않고 프로브에 있는 값을 그대로 쓰므로, 임상을 한 번 돌리면 일반 측정의 취득 조건이 조용히 바뀐 채 남는다 — 주파수가 바뀌면 파형이 달라져 BV 에도 영향이 간다. 기본값을 박아 두지 않았다. "일반 측정용 기본값"이 앱 어디에도 없고, 제품이 어떤 조건으로 검증됐는지는 펌웨어·알고리즘 쪽 값이라 여기서 정할 수 없다. 정해서 박으면 그 값이 틀렸을 때 모든 임상 종료 시점에 틀린 값을 심는다. 대신 시작 전 값을 읽어 두었다가 그대로 되돌린다 — 원래 무엇이었든 전후가 같아진다. · 못 읽었으면 되돌리지 않는다. 짐작한 값을 쓰면 원래와 다른 것을 심어 놓고 "복원했다"고 믿게 된다 — 안 되돌리는 것보다 나쁘다. · 쓰기 echo 까지 확인한다. 설정이 거부돼도 rcs: 는 오므로. Co-Authored-By: Claude Opus 5 --- .../com/medithings/vesiscan/ble/BleManager.kt | 39 ++++++++ .../ui/views/clinical/ProbeConfigGuard.kt | 97 +++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 app/src/main/java/com/medithings/vesiscan/ui/views/clinical/ProbeConfigGuard.kt 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 cf7f8e7..a4f0bad 100644 --- a/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt +++ b/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt @@ -806,6 +806,29 @@ class BleManager private constructor(private val context: Context) { /** `rcs:` echo — 프로브에 **실제로 저장된** 측정 파라미터. 아직 못 받았으면 null. */ val piezoConfigEcho = mutableStateOf(null) + /** + * `rcf:` 응답 — 프로브 FDS 에 저장된 현재 측정 파라미터. + * + * [piezoConfigEcho] 와 **따로 둔다.** 그쪽은 "내가 방금 쓴 게 먹혔나"를 확인하는 + * 용도라 쓰기 직전에 null 로 비우고 기다린다. 조회 응답이 같은 자리에 들어오면 + * 쓰기 검증이 엉뚱한 값을 보고 통과해 버린다. + */ + val piezoConfigRead = mutableStateOf(null) + + /** + * `mcf?` — 프로브에 저장된 측정 파라미터를 **읽는다**(쓰지 않는다). + * + * 이게 없던 동안에는 프로브 상태를 알 방법이 아예 없었다. `mcs?` 로 써 봐야 알 수 + * 있었는데 그건 이미 바꾼 것이다. 임상 모드가 조합마다 파라미터를 덮어쓰고 되돌리지 + * 않으므로, 일반 측정 전에 지금 무엇이 들어 있는지 확인할 수단이 필요하다. + * + * 응답은 [piezoConfigRead] 로 온다. + */ + fun sendPiezoConfigQuery() { + piezoConfigRead.value = null + sendRaw(CRC16.buildCommandASCII("mcf", " ")) + } + /** * 측정 파라미터 (`mcs?`). 프로브 NVS 에 저장되어 전원을 껐다 켜도 유지된다. * @@ -1706,6 +1729,22 @@ class BleManager private constructor(private val context: Context) { debugLogger.rx("rcs", data.size, "too short") } } + "rcf:" -> { + // `mcf?` 응답. 배치가 `rcs:` 와 같다 — + // [rcf: 4B][freq][cycles][avg][delay_us][samples] 각 BE 2B + CRC 2B = 16B. + // 실패 시 freq 에 0xFFFF 가 실리는 것도 같다(PiezoConfig.error). + if (data.size >= 14) { + fun be(i: Int) = ((data[i].toInt() and 0xFF) shl 8) or (data[i + 1].toInt() and 0xFF) + val cfg = PiezoConfig(be(4), be(6), be(8), be(10), be(12)) + piezoConfigRead.value = cfg + debugLogger.rx("rcf", data.size, + cfg.error?.let { "조회 실패: $it" } + ?: "freq=${cfg.freq} cyc=${cfg.cycles} avg=${cfg.avg} " + + "delay=${cfg.delayUs} samples=${cfg.samples}") + } else { + debugLogger.rx("rcf", data.size, "too short") + } + } "reb:" -> { // 신구조 reb (210B): tag 4 + ch_info 2 + num_sample 2 + ADC + CRC 2 → ADC = size-10 val samples = ((data.size - 10) / 2).coerceAtLeast(0) diff --git a/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/ProbeConfigGuard.kt b/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/ProbeConfigGuard.kt new file mode 100644 index 0000000..914ad3b --- /dev/null +++ b/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/ProbeConfigGuard.kt @@ -0,0 +1,97 @@ +/* + * ProbeConfigGuard — 임상 측정이 바꾼 프로브 파라미터를 원래대로 되돌린다. + */ +package com.medithings.vesiscan.ui.views.clinical + +import com.medithings.vesiscan.ble.BleManager +import com.medithings.vesiscan.ble.PiezoConfig +import kotlinx.coroutines.delay +import kotlinx.coroutines.withTimeoutOrNull + +/** + * 임상 측정 전후로 프로브 측정 파라미터(`mcs`)를 보존한다. + * + * ## 왜 필요한가 + * 정렬과 병원 임상 모드는 자기 조건을 프로브에 **써 넣고 되돌리지 않는다.** 그 값은 + * FDS(플래시)에 남아 다음 측정에도 적용되는데, 일반 측정 화면은 `mcs` 를 아예 보내지 + * 않고 프로브에 있는 값을 그대로 쓴다. 그래서 임상을 한 번 돌리고 나면 일반 측정의 + * 취득 조건이 조용히 바뀐 채로 남는다 — 주파수가 바뀌면 파형이 달라지므로 BV 에도 영향이 간다. + * + * ## 기본값을 박아 두지 않는 이유 + * "일반 측정용 기본값"이 앱 어디에도 없다. 제품이 어떤 조건으로 검증됐는지는 펌웨어· + * 알고리즘 쪽 값이라 여기서 정할 수 없고, 정해서 박아 두면 그 값이 틀렸을 때 **모든 + * 임상 종료 시점에** 틀린 값을 심게 된다. + * + * 대신 **시작 전 값을 읽어 두었다가 그대로 되돌린다**(`mcf?` → `mcs?`). 원래 무엇이었든 + * 임상 전후가 같아진다. 기기마다 달라도, 나중에 제품 기본값이 바뀌어도 따라간다. + * + * ## 못 읽으면 되돌리지 않는다 + * [capture] 가 실패하면 [restore] 는 아무것도 하지 않는다. 무엇으로 되돌려야 할지 + * 모르는데 짐작한 값을 쓰면, 원래 값과 다른 것을 심어 놓고 "복원했다"고 믿게 된다. + * 그건 안 되돌리는 것보다 나쁘다 — 호출부가 [captured] 로 확인해 사용자에게 알린다. + */ +class ProbeConfigGuard(private val ble: BleManager) { + + /** 시작 전에 읽어 둔 값. null 이면 못 읽었다는 뜻이고, 그때는 되돌리지 않는다. */ + var captured: PiezoConfig? = null + private set + + /** 되돌리기를 실제로 마쳤는가. 화면이 사용자에게 알리는 데 쓴다. */ + var restored: Boolean = false + private set + + /** + * 현재 프로브 설정을 읽어 둔다. 읽었으면 true. + * + * 실패해도 측정은 진행해야 한다 — 복원을 못 하는 것과 측정을 못 하는 것은 다른 문제다. + */ + suspend fun capture(): Boolean { + ble.sendPiezoConfigQuery() + val cfg = withTimeoutOrNull(TIMEOUT_MS) { + while (ble.piezoConfigRead.value == null) delay(POLL_MS) + ble.piezoConfigRead.value + } + // 펌웨어가 실패를 값으로 알려 준다(freq=0xFFFF). 그건 읽은 게 아니다. + captured = cfg?.takeIf { it.error == null } + return captured != null + } + + /** + * 읽어 둔 값으로 되돌린다. 되돌렸으면 true. + * + * 쓰기 echo(`rcs:`)까지 확인한다 — 설정이 거부돼도 응답은 오므로, 확인하지 않으면 + * 되돌리지 못한 것을 되돌렸다고 기록하게 된다. + */ + suspend fun restore(): Boolean { + val c = captured ?: return false + ble.piezoConfigEcho.value = null + ble.sendPiezoConfig( + freq = c.freq, cycles = c.cycles, avg = c.avg, + delayUs = c.delayUs, samples = c.samples, + ) + val echo = withTimeoutOrNull(TIMEOUT_MS) { + while (ble.piezoConfigEcho.value == null) delay(POLL_MS) + ble.piezoConfigEcho.value + } + restored = echo != null && echo.error == null && + echo.freq == c.freq && echo.cycles == c.cycles + return restored + } + + /** 화면에 띄울 한 줄. 복원 결과를 사용자가 알아야 다음 측정을 신뢰할 수 있다. */ + fun summary(): String? = when { + captured == null -> + "프로브 설정을 읽지 못해 측정 전 값으로 되돌리지 못했습니다. " + + "일반 측정 전에 파라미터를 확인하세요." + restored -> captured?.let { + "프로브 설정을 측정 전 값(freq ${it.freq} · cycle ${it.cycles})으로 되돌렸습니다." + } + else -> + "프로브 설정 복원에 실패했습니다. 일반 측정 전에 파라미터를 확인하세요." + } + + private companion object { + const val TIMEOUT_MS = 3_000L + const val POLL_MS = 20L + } +}