From 749e1d4e0e9c8b525ab0712c7e01842074f0d8a1 Mon Sep 17 00:00:00 2001
From: jhchun <jh.chun@medithings.co.kr>
Date: Mon, 29 Jun 2026 16:50:21 +0900
Subject: [PATCH] =?UTF-8?q?=EB=B0=B0=ED=84=B0=EB=A6=AC/=EC=98=A8=EB=8F=84?=
 =?UTF-8?q?=20=EB=B3=B4=ED=98=B8=20=ED=8C=90=EC=A0=95=20=EC=A0=95=EC=B1=85?=
 =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

1. 정렬모드    - 판정하지 않음2. 연속 측정 모드    - 측정 응답에 포함되는 배터리/IMU 온도 값으로 저전압/고온 카운트 및 판정    - 배터리 3500mV 이하 또는 온도 40℃ 이상 30회 연속인 경우 약 5분 후 전원 OFF    - FW 내부 60초 주기 모니터는 skip    - 측정에 실패하는 경우 저전압/고온 카운트는 유지3. 정렬모드도 연속 측정 모드도 아닌 경우    - FW 내부 60초 주기 모니터로 저전압/고온 카운트 및 판정    - 배터리 3500mV 이하 또는 온도 40℃ 이상 5회 연속인 경우 약 5분 후 전원 OFF
---
 src/drivers/battery/battery_adc.c | 218 ++++++++++++++++++++++++------
 src/drivers/battery/battery_adc.h |   7 +
 src/parser.c                      |  90 ++++++++----
 3 files changed, 249 insertions(+), 66 deletions(-)

diff --git a/src/drivers/battery/battery_adc.c b/src/drivers/battery/battery_adc.c
index 43c49d1..4607a32 100644
--- a/src/drivers/battery/battery_adc.c
+++ b/src/drivers/battery/battery_adc.c
@@ -19,6 +19,7 @@
 #include <zephyr/drivers/adc.h>
 #include <zephyr/devicetree.h>
 #include <string.h>
+#include <limits.h>
 
 #include "battery_adc.h"
 #include "debug_print.h"
@@ -43,11 +44,37 @@ volatile uint16_t info_batt = 0;
 
 static struct k_timer battery_timer;
 static struct k_work battery_work;
-static uint8_t low_battery_cnt = 0;
-static uint8_t high_temperature_cnt = 0;
 
-#define BATTERY_MONITOR_LIMIT_COUNT  5U
-#define HIGH_TEMPERATURE_CDEG        4000   /* 40.00 C */
+/* 60초 내부 모니터 전용 카운터
+ * - 앱 측정 명령(mbb?)이 없는 일반 상태에서만 사용
+ * - mbb? 성공 시 연속 측정 판정으로 넘어가므로 0으로 초기화
+ */
+static uint8_t periodic_low_battery_cnt;
+static uint8_t periodic_high_temperature_cnt;
+
+/* 연속 측정(mbb?) 응답값 전용 카운터
+ * - mbb? 응답에 포함된 배터리/IMU 온도 값으로 판정
+ * - 측정 실패 시 호출되지 않으므로 기존 카운트 유지
+ */
+static uint8_t continuous_low_battery_cnt;
+static uint8_t continuous_high_temperature_cnt;
+
+/* 정렬 상태 보호 판정 차단 플래그
+ * - mls?로 ALIGN_SEARCHING/ALIGN_COMPLETE 진입 시 true
+ * - 정렬 중에는 배터리/온도 보호 판정 자체를 진행하지 않음
+ */
+static bool alignment_mode_active;
+
+/* 최근 mbb? 성공 이후 내부 60초 모니터를 잠시 막는 기준 시각
+ * - 연속 측정 중에는 mbb? 응답값으로 보호 판정
+ * - 이 값보다 현재 시간이 작으면 60초 내부 모니터는 skip
+ */
+static int64_t continuous_monitor_active_until_ms;
+
+#define BATTERY_MONITOR_LIMIT_COUNT              5U
+#define BATTERY_CONTINUOUS_MONITOR_LIMIT_COUNT   30U
+#define HIGH_TEMPERATURE_CDEG                    4000   /* 40.00 C */
+#define CONTINUOUS_MONITOR_ACTIVE_MS             10000
 
 #define BATTERY_MONITOR_INTERVAL_MS  60000  /* 60초 주기 */
 
@@ -67,6 +94,77 @@ static int adc_raw_to_mv(int16_t raw)
     return (int)mv;
 }
 
+static bool continuous_monitor_is_active(void)
+{
+    /* 마지막 mbb? 성공 이후 CONTINUOUS_MONITOR_ACTIVE_MS 안쪽이면 연속 측정 중으로 간주 */
+    return (continuous_monitor_active_until_ms > 0) &&
+           (k_uptime_get() < continuous_monitor_active_until_ms);
+}
+
+static void record_battery_value(int batt_mv, uint8_t *cnt,
+                                 uint8_t limit, const char *mode)
+{
+    /* mode별 카운터만 전달받아 공통 판정 로직 사용
+     * - periodic: 60초 내부 모니터, limit 5
+     * - continuous: mbb? 응답값 기반, limit 30
+     */
+    if (batt_mv <= LOW_BATTERY_VOLTAGE)
+    {
+        if (*cnt < UINT8_MAX)
+        {
+            (*cnt)++;
+        }
+
+        DBG_PRINTF("[BATT] LOW! mode=%s cnt=%u/%u mv=%d\r\n",
+                   mode, *cnt, limit, batt_mv);
+
+        if (*cnt >= limit)
+        {
+            *cnt = 0;
+            DBG_ERR("[BATT] %ux low (%s) -> Power OFF\r\n", limit, mode);
+            sleep_mode_enter_reason("battery-low");
+        }
+    }
+    else
+    {
+        *cnt = 0;
+    }
+}
+
+static void record_temperature_value(int16_t temp_cdeg, uint8_t *cnt,
+                                     uint8_t limit, const char *mode)
+{
+    /* IMU 온도는 centi-degree 단위
+     * - 4000 = 40.00 C
+     * - 정상 온도 수신 시 기준 이하이면 해당 mode 카운터 초기화
+     */
+    if (temp_cdeg > HIGH_TEMPERATURE_CDEG)
+    {
+        if (*cnt < UINT8_MAX)
+        {
+            (*cnt)++;
+        }
+
+        DBG_PRINTF("[TEMP] HIGH! mode=%s cnt=%u/%u temp=%d.%02d C\r\n",
+                   mode,
+                   *cnt,
+                   limit,
+                   temp_cdeg / 100,
+                   (temp_cdeg < 0 ? -temp_cdeg : temp_cdeg) % 100);
+
+        if (*cnt >= limit)
+        {
+            *cnt = 0;
+            DBG_ERR("[TEMP] %ux high (%s) -> Power OFF\r\n", limit, mode);
+            sleep_mode_enter_reason("temperature-high");
+        }
+    }
+    else
+    {
+        *cnt = 0;
+    }
+}
+
 /*==============================================================================
  * Public functions
  *============================================================================*/
@@ -134,16 +232,72 @@ int battery_read_mv(void)
     return mv;
 }
 
+
+void battery_protection_set_alignment_mode(bool enabled)
+{
+    /* 정렬 상태 진입/해제는 mls? 명령에서 전달
+     * - enabled true: 보호 판정 중지 + 기존 누적값 초기화
+     * - enabled false: 다음 조건부터 다시 판정 가능
+     */
+    alignment_mode_active = enabled;
+    if (enabled)
+    {
+        periodic_low_battery_cnt = 0;
+        periodic_high_temperature_cnt = 0;
+        continuous_low_battery_cnt = 0;
+        continuous_high_temperature_cnt = 0;
+    }
+}
+
+void battery_protection_record_continuous(int batt_mv, int16_t temp_cdeg)
+{
+    /* mbb? 측정 성공 시에만 호출
+     * - 실패한 측정은 호출되지 않아서 continuous 카운트 유지
+     * - 정렬 중에는 응답값이 있어도 보호 판정 제외
+     */
+    if (alignment_mode_active)
+    {
+        return;
+    }
+
+    /* mbb?가 들어온 직후에는 내부 60초 모니터 대신 응답값 기반 판정 사용
+     * - periodic 카운터는 섞이지 않게 초기화
+     * - continuous 카운터는 아래 record_* 함수에서 값에 따라 증가/초기화
+     */
+    continuous_monitor_active_until_ms = k_uptime_get() + CONTINUOUS_MONITOR_ACTIVE_MS;
+    periodic_low_battery_cnt = 0;
+    periodic_high_temperature_cnt = 0;
+
+    record_battery_value(batt_mv,
+                         &continuous_low_battery_cnt,
+                         BATTERY_CONTINUOUS_MONITOR_LIMIT_COUNT,
+                         "continuous");
+    record_temperature_value(temp_cdeg,
+                             &continuous_high_temperature_cnt,
+                             BATTERY_CONTINUOUS_MONITOR_LIMIT_COUNT,
+                             "continuous");
+}
+
 /*==============================================================================
  * 주기 모니터링 타이머
  *============================================================================*/
 
-/** @brief k_work 핸들러 — 스레드 컨텍스트에서 ADC 읽기 (adc_read는 블로킹이라 ISR 불가) */
+/* k_work 핸들러: 스레드 컨텍스트에서 ADC 읽기 (adc_read는 블로킹이라 ISR 불가) */
 
 static void battery_work_handler(struct k_work *work)
 {
     ARG_UNUSED(work);
 
+    /* 보호 판정 제외 구간
+     * - 정렬 중: 정책상 보호 판정 미진행
+     * - 최근 mbb? 성공 직후: mbb? 응답값 기반 continuous 판정이 담당
+     */
+    if (alignment_mode_active || continuous_monitor_is_active())
+    {
+        return;
+    }
+
+    /* 측정 처리 중이거나 IMU FIFO 사용 중(정렬모드)이면 I2C 접근 충돌 방지 */
     if (processing || imu_fifo_is_active())
     {
         return;
@@ -156,24 +310,14 @@ static void battery_work_handler(struct k_work *work)
         return;
     }
 
-    /* 배터리 저전압 */
-    if (batt <= LOW_BATTERY_VOLTAGE)
-    {
-        low_battery_cnt++;
-        DBG_PRINTF("[BATT] LOW! cnt=%d mv=%d\r\n", low_battery_cnt, batt);
-
-        if (low_battery_cnt >= BATTERY_MONITOR_LIMIT_COUNT)
-        {
-            low_battery_cnt = 0;
-            DBG_ERR("[BATT] %ux low -> Power OFF\r\n", BATTERY_MONITOR_LIMIT_COUNT);
-            sleep_mode_enter_reason("battery-low");
-            return;
-        }
-    }
-    else
-    {
-        low_battery_cnt = 0;
-    }
+    /* 일반 상태의 60초 내부 모니터 판정
+     * - 저전압이면 periodic_low_battery_cnt 증가
+     * - 정상 전압이면 periodic_low_battery_cnt 초기화
+     */
+    record_battery_value(batt,
+                         &periodic_low_battery_cnt,
+                         BATTERY_MONITOR_LIMIT_COUNT,
+                         "periodic");
 
     int16_t temp_cdeg = 0;
     int temp_ret = imu_read_temperature_cdeg(&temp_cdeg);
@@ -184,26 +328,14 @@ static void battery_work_handler(struct k_work *work)
         return;
     }
 
-    if (temp_cdeg > HIGH_TEMPERATURE_CDEG)
-    {
-        high_temperature_cnt++;
-        DBG_PRINTF("[TEMP] HIGH! cnt=%d temp=%d.%02d C\r\n",
-                   high_temperature_cnt,
-                   temp_cdeg / 100,
-                   (temp_cdeg < 0 ? -temp_cdeg : temp_cdeg) % 100);
-
-        if (high_temperature_cnt >= BATTERY_MONITOR_LIMIT_COUNT)
-        {
-            high_temperature_cnt = 0;
-            DBG_ERR("[TEMP] %ux high -> Power OFF\r\n", BATTERY_MONITOR_LIMIT_COUNT);
-            sleep_mode_enter_reason("temperature-high");
-            return;
-        }
-    }
-    else
-    {
-        high_temperature_cnt = 0;
-    }
+    /* 일반 상태의 IMU 온도 판정
+     * - 고온이면 periodic_high_temperature_cnt 증가
+     * - 정상 온도이면 periodic_high_temperature_cnt 초기화
+     */
+    record_temperature_value(temp_cdeg,
+                             &periodic_high_temperature_cnt,
+                             BATTERY_MONITOR_LIMIT_COUNT,
+                             "periodic");
 }
 
 /* 타이머 ISR → k_work 예약 */
diff --git a/src/drivers/battery/battery_adc.h b/src/drivers/battery/battery_adc.h
index 400264b..0323420 100644
--- a/src/drivers/battery/battery_adc.h
+++ b/src/drivers/battery/battery_adc.h
@@ -21,6 +21,13 @@ extern volatile uint16_t info_batt;
 
 void battery_adc_init(void);
 int  battery_read_mv(void);
+
+/* 보호 판정 제어 API
+ * - alignment_mode: 정렬 중 배터리/온도 보호 판정 차단
+ * - record_continuous: mbb? 성공 응답값으로 연속 측정 보호 카운트 갱신
+ */
+void battery_protection_set_alignment_mode(bool enabled);
+void battery_protection_record_continuous(int batt_mv, int16_t temp_cdeg);
 void battery_timer_start(void);
 void battery_timer_stop(void);
 void battery_timer_init(void);
diff --git a/src/parser.c b/src/parser.c
index bb6736e..791166e 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -730,7 +730,7 @@ static int cmd_msr(const uint8_t *data, uint8_t data_len)
 
     /*
      * 현재 연결의 bond/security 정보를 먼저 지우면 같은 연결에서 rsr: 응답을
-     * 보낼 수 없다. 응답 전송 완료 후 bond 삭제와 reboot를 진행한다.
+     * 보낼 수 없음. 응답 전송 완료 후 bond 삭제와 reboot 진행 필요
      */
     k_msleep(100);
 
@@ -842,7 +842,7 @@ static int cmd_mst(const uint8_t *data, uint8_t data_len)
     return 1;
 }
 
-/**
+/*
  * 테스트용
  * mpa?: piezo TX/RX 전원 레일만 켬
  */
@@ -889,7 +889,7 @@ static int cmd_mpb(const uint8_t *data, uint8_t data_len)
     return 1;
 }
 
-/**
+/*
  * 테스트용
  * mpc?: burst만 한 번 발생시키는 테스트 명령
  * echo를 읽지 않고 초음파가 나가는지만 볼 때 사용
@@ -944,6 +944,10 @@ static int cmd_mpc(const uint8_t *data, uint8_t data_len)
     return 1;
 }
 
+/*
+ * 테스트용
+ * mec?: 단일 채널 burst + echo capture
+ */
 static int cmd_mec(const uint8_t *data, uint8_t data_len)
 {
     const piezo_config_t *cfg = piezo_config_get();
@@ -964,10 +968,6 @@ static int cmd_mec(const uint8_t *data, uint8_t data_len)
 
     ARG_UNUSED(freq_option);
 
-    /*
-     * 테스트용
-     * mec?: 단일 채널 burst + echo capture
-     */
     if (num_samples > ECHO_ADC_MAX_SAMPLES)
     {
         num_samples = ECHO_ADC_MAX_SAMPLES;
@@ -1002,12 +1002,11 @@ static int cmd_mec(const uint8_t *data, uint8_t data_len)
     return 1;
 }
 
-/**
- * 테스트용(이전 정렬모드)
- * maa?:
- * - piezo 6채널 burst 순서대로 쏘고
- * - echo 샘플을 채널별로 모은 뒤
- * - reb: 패킷 6개 전송
+/*
+ * 테스트용
+ * mad?: 단일 채널 echo capture만 수행
+ * - piezo burst는 하지 않고 echo ADC만 수행
+ * - echo 샘플을 모은 뒤 reb: 패킷 1개 전송
  * - raa: 상태값은 전체 작업 성공/실패 요약
  */
 static int cmd_mad(const uint8_t *data, uint8_t data_len)
@@ -1058,6 +1057,14 @@ static int cmd_mad(const uint8_t *data, uint8_t data_len)
     return 1;
 }
 
+/*
+ * 테스트용(이전 정렬모드)
+ * maa?:
+ * - piezo 6채널 burst 순서대로 쏘고
+ * - echo 샘플을 채널별로 모은 뒤
+ * - reb: 패킷 6개 전송
+ * - raa: 상태값은 전체 작업 성공/실패 요약
+ */
 static int cmd_maa(const uint8_t *data, uint8_t data_len)
 {
     ARG_UNUSED(data);
@@ -1091,8 +1098,12 @@ static int cmd_maa(const uint8_t *data, uint8_t data_len)
     return 1;
 }
 
-/**
- * 전체 측정 시 사용
+/*
+ * 전체 측정 명령
+ * mbb?: piezo echo sweep + 배터리 + IMU + IMU 온도 측정
+ * - 성공 시 rbb 1개, reb 6개, raa 상태값 전송
+ * - 실패 시 rbb/reb 생략, raa에 실패 원인 코드 전송
+ * - processing=true 구간에서는 내부 모니터의 I2C 접근을 막는 기준으로 사용
  */
 static int cmd_mbb(const uint8_t *data, uint8_t data_len)
 {
@@ -1133,7 +1144,10 @@ static int cmd_mbb(const uint8_t *data, uint8_t data_len)
 
     if (status == ECHO_STATUS_OK)
     {
-        /* info 성격 데이터는 echo sweep이 정상 끝났을 때만 읽음 */
+        /* echo sweep 성공 후에만 부가 센서값 읽기
+         * - rbb에 포함되는 배터리 전압
+         * - 실패 시 ECHO_STATUS_BATT로 종료
+         */
         DBG_PRINTF("[MBB] battery read\r\n");
         batt_mv = battery_read_mv();
         if (batt_mv < 0)
@@ -1144,6 +1158,10 @@ static int cmd_mbb(const uint8_t *data, uint8_t data_len)
 
     if (status == ECHO_STATUS_OK)
     {
+        /* rbb에 포함되는 IMU 6축 값과 IMU 내부 온도 읽기
+         * - direct read 경로 사용
+         * - 실패 시 ECHO_STATUS_IMU로 종료
+         */
         DBG_PRINTF("[MBB] imu/temp read\r\n");
         if (imu_read_with_temperature(accel, gyro, &temp_cdeg) != 0)
         {
@@ -1153,7 +1171,17 @@ static int cmd_mbb(const uint8_t *data, uint8_t data_len)
 
     if (status == ECHO_STATUS_OK)
     {
-        /* rbb: 센서(배터리+IMU+온도) 정보 패킷 */
+        /* mbb? 성공 응답값으로 연속 측정 보호 판정 갱신
+         * - 실패한 측정은 호출하지 않아 기존 저전압/고온 카운트 유지
+         * - rbb에 실리는 배터리/IMU 온도 값과 같은 값을 사용
+         */
+        battery_protection_record_continuous(batt_mv, temp_cdeg);
+
+        /* 측정 성공 응답 순서
+         * - rbb: 배터리 + IMU + IMU 온도 요약
+         * - reb: 채널별 echo raw 데이터
+         * - raa: 마지막 상태 코드
+         */
         DBG_PRINTF("[MBB] response tx start\r\n");
         send_response_bundle((uint16_t)batt_mv, accel, gyro, temp_cdeg);
 
@@ -1175,7 +1203,7 @@ static int cmd_mbb(const uint8_t *data, uint8_t data_len)
     return 1;
 }
 
-/**
+/*
  * mtb?: piezo sweep + IMU FIFO
  */
 static int cmd_mtb(const uint8_t *data, uint8_t data_len)
@@ -1245,7 +1273,7 @@ static int cmd_mtb(const uint8_t *data, uint8_t data_len)
     return 1;
 }
 
-/**
+/*
  * mcf?: piezo 측정 파라미터 읽기
  * 응답 rcf: + 설정값 echo back
  */
@@ -1259,7 +1287,7 @@ static int cmd_mcf(const uint8_t *data, uint8_t data_len)
     return 1;
 }
 
-/**
+/*
  * mcs?: piezo 측정 파라미터 쓰기
  */
 static int cmd_mcs(const uint8_t *data, uint8_t data_len)
@@ -1457,9 +1485,16 @@ static int cmd_mqz(const uint8_t *data, uint8_t data_len)
     return 1;
 }
 
+/*
+ * LED 상태 변경 명령
+ * mls?: 앱에서 전달한 LED 상태값을 적용하고 rls로 echo back
+ * - 파라미터 부족: 0xFFFF
+ * - 범위 초과: 0xFFFE
+ * - LED_STATE_OFF 진입 시 IMU FIFO 정지
+ */
 static int cmd_mls(const uint8_t *data, uint8_t data_len)
 {
-    /* 파라미터 부족 → 에러 코드 0xFFFF 에코 */
+    /* 파라미터 부족: 에러 코드 0xFFFF 에코 */
     if (data_len < 2)
     {
         send_response_u16("rls:", 0xFFFF);
@@ -1467,10 +1502,10 @@ static int cmd_mls(const uint8_t *data, uint8_t data_len)
         return 1;
     }
 
-    /* state 2B BE */
+    /* state 필드: 2바이트 big-endian */
     uint16_t state = ((uint16_t)data[0] << 8) | (uint16_t)data[1];
 
-    /* 범위 초과 → 에러 코드 0xFFFE 에코 */
+    /* 범위 초과: 에러 코드 0xFFFE 에코 */
     if (state > LED_STATE_ERROR)
     {
         send_response_u16("rls:", 0xFFFE);
@@ -1479,6 +1514,15 @@ static int cmd_mls(const uint8_t *data, uint8_t data_len)
     }
 
     led_set_state((led_state_t)state);
+
+    /* 정렬 LED 상태와 보호 판정 정책 연결
+     * - ALIGN_SEARCHING/ALIGN_COMPLETE 동안 보호 판정 차단
+     * - 다른 LED 상태에서는 기존 조건에 따라 보호 판정 재개
+     */
+    battery_protection_set_alignment_mode((state == LED_STATE_ALIGN_SEARCHING) ||
+                                          (state == LED_STATE_ALIGN_COMPLETE));
+
+    /* OFF 상태에서는 정렬/측정용 FIFO를 함께 정리 */
     if (state == LED_STATE_OFF)
     {
         (void)imu_fifo_stop();
