feat(ble): 프로브 측정 파라미터 읽기(mcf?) · 임상 전후 보존 도구

지금까지 프로브에 무엇이 저장돼 있는지 **알 방법이 없었다.** `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 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 15:49:53 +09:00
parent 49975b7792
commit 33e5946895
2 changed files with 136 additions and 0 deletions
@@ -806,6 +806,29 @@ class BleManager private constructor(private val context: Context) {
/** `rcs:` echo — 프로브에 **실제로 저장된** 측정 파라미터. 아직 못 받았으면 null. */
val piezoConfigEcho = mutableStateOf<PiezoConfig?>(null)
/**
* `rcf:` 응답 — 프로브 FDS 에 저장된 현재 측정 파라미터.
*
* [piezoConfigEcho] 와 **따로 둔다.** 그쪽은 "내가 방금 쓴 게 먹혔나"를 확인하는
* 용도라 쓰기 직전에 null 로 비우고 기다린다. 조회 응답이 같은 자리에 들어오면
* 쓰기 검증이 엉뚱한 값을 보고 통과해 버린다.
*/
val piezoConfigRead = mutableStateOf<PiezoConfig?>(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)