电池曲线更新,优化电量获取机制。充电时不上报新电压。
authorsnickersun <snickersun@163.com>
Wed, 20 May 2026 09:43:36 +0000 (17:43 +0800)
committersnickersun <snickersun@163.com>
Wed, 20 May 2026 09:43:36 +0000 (17:43 +0800)
更新文档及调换存放位置。

24 files changed:
1.doc/K8_diaozhui/ZCV放电数据502631-510 4.35V.xlsx [new file with mode: 0644]
1.doc/K8_diaozhui/design/AI吊坠LED状态优先级状态机.md [moved from 1.doc/AI吊坠LED状态优先级状态机.md with 100% similarity]
1.doc/K8_diaozhui/design/AI吊坠(双按键+三色指示灯)交互规范-新旧变更说明.md [moved from 1.doc/AI吊坠(双按键+三色指示灯)交互规范-新旧变更说明.md with 100% similarity]
1.doc/K8_diaozhui/design/AI吊坠(双按键+三色指示灯)交互规范.md [moved from 1.doc/AI吊坠(双按键+三色指示灯)交互规范.md with 100% similarity]
1.doc/K8_diaozhui/design/AI吊坠(双按键+三色指示灯)交互规范_new.md [moved from 1.doc/AI吊坠(双按键+三色指示灯)交互规范_new.md with 100% similarity]
1.doc/K8_diaozhui/design/AI吊坠(双按键+三色指示灯)交互规范_附录-工程与代码审查.md [moved from 1.doc/AI吊坠(双按键+三色指示灯)交互规范_附录-工程与代码审查.md with 100% similarity]
1.doc/K8_diaozhui/design/AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md [moved from 1.doc/AI吊坠(双按键+三色指示灯)详设-场景与协议流程.md with 100% similarity]
1.doc/K8_diaozhui/design/AI吊坠(双按键+三色指示灯)详设-对照与决策.md [moved from 1.doc/AI吊坠(双按键+三色指示灯)详设-对照与决策.md with 100% similarity]
1.doc/K8_diaozhui/design/RVC Service WebSocket V3 API 开发文档.md [moved from 1.doc/RVC Service WebSocket V3 API 开发文档.md with 100% similarity]
1.doc/K8_diaozhui/design/RVC Service WebSocket V3 API 开发文档.pdf [moved from 1.doc/RVC Service WebSocket V3 API 开发文档.pdf with 100% similarity]
1.doc/K8_diaozhui/design/V2.5_to_V3_协议变更对照.md [moved from 1.doc/V2.5_to_V3_协议变更对照.md with 100% similarity]
1.doc/K8_diaozhui/design/VC_SESSION_ARCH.md [moved from 1.doc/VC_SESSION_ARCH.md with 100% similarity]
1.doc/K8_diaozhui/design/VC_state迁移FSM子状态-改动清单.md [moved from 1.doc/VC_state迁移FSM子状态-改动清单.md with 100% similarity]
1.doc/K8_diaozhui/design/金三服务-语音对话V2.5.md [moved from 1.doc/金三服务-语音对话V2.5.md with 100% similarity]
1.doc/K8_diaozhui/design/金三服务-语音对话V2.5.pdf [moved from 1.doc/金三服务-语音对话V2.5.pdf with 100% similarity]
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/default_config.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/7PIN/default_config.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/jswm/default_config.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/k8/default_config.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/k8s/default_config.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/protocol.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_buttons.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_hw.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_io.py

diff --git a/1.doc/K8_diaozhui/ZCV放电数据502631-510 4.35V.xlsx b/1.doc/K8_diaozhui/ZCV放电数据502631-510 4.35V.xlsx
new file mode 100644 (file)
index 0000000..6067183
Binary files /dev/null and "b/1.doc/K8_diaozhui/ZCV\346\224\276\347\224\265\346\225\260\346\215\256502631-510 4.35V.xlsx" differ
index d26ea6f..aefa493 100644 (file)
@@ -81,25 +81,29 @@ VAD_ACTIVE_BLUE_LED = True
 # =============================================================================
 
 HW_STATUS_PERIOD_MS = 10000
+# 电池电压:全机约每 10s 采样 1 次 getVbatt,10 点滑动平均(见 vc_hw._BatteryCfg)
+VBATT_AVG_LEN = 10
+VBATT_PUSH_MIN_MS = 9000   # 同周期内 AI_hw / protocol 去重,避免重复读电压
 SIGNAL_WEAK_CSQ = 10  # 0≤CSQ<此值 → signal=weak(REQ-ERR-02)
 
-# SoC 百分比门限(OCV 模式,对齐 REQ-BAT-02/03)
-BATTERY_LOW_SOC_PERCENT = 30
-BATTERY_EMPTY_SOC_PERCENT = 10
+# SoC 门限:仅低电提示/LED 空电闪(REQ-BAT-02/03);关机见 VBATT_EMPTY_MV
+BATTERY_LOW_SOC_PERCENT = 25       # <25% 低电语音
+BATTERY_EMPTY_SOC_PERCENT = 5      # <5% LED 空电态;不单独触发关机
 BATTERY_LOW_WARN_INTERVAL_MS = 300000  # 低电语音间隔 5min
 BATTERY_EMPTY_FLASH_COUNT = 3
 BATTERY_EMPTY_FLASH_PERIOD_MS = 500    # 空电红快闪约 2 次/秒
 
 # 电压门限(mV):充满 / 低电 / 空电(与 get_battery、simple 模式共用)
 USE_SIMPLE_VBATT_THRESH = False  # True 时忽略 BATTERY_OCV_MAP,仅用三档电压
-VBATT_CHARGE_FULL_MV = 4290
-VBATT_LOW_MV = 3699
-VBATT_EMPTY_MV = 3629
+VBATT_CHARGE_FULL_MV = 4320   # 502631 ZCV 100% OCV≈4.322V;CV 4.35V
+VBATT_LOW_MV = 3630           # 约对应 ZCV 20%(4G 带载下不宜再抬高)
+VBATT_EMPTY_MV = 3400         # 未充电:滑动平均电压 < 此值 → 低电关机(唯一关机条件)
 
-# 开路电压(mV)→SoC(%) 插值表;None 则用 ai_VC 内置默认表
+# 502631-510 4.35V ZCV 放电 OCV 表(mV→SoC);0% 锚定 3400 而非实验室 3262
 BATTERY_OCV_MAP = {
-    4290: 100, 4237: 95, 4179: 90, 4123: 85, 4070: 80, 4020: 75, 3963: 70, 3919: 65, 3882: 60, 3847: 55,
-    3818: 50, 3794: 45, 3772: 40, 3752: 35, 3733: 30, 3716: 25, 3699: 20, 3677: 15, 3652: 10, 3629: 5, 3400: 0,
+    4322: 100, 4239: 95, 4177: 90, 4117: 85, 4059: 80, 4006: 75, 3942: 70, 3898: 65,
+    3853: 60, 3814: 55, 3781: 50, 3755: 45, 3738: 40, 3724: 35, 3706: 30, 3678: 25,
+    3634: 20, 3577: 15, 3522: 10, 3468: 5, 3400: 0,
 }
 
 # =============================================================================
index bde4de0..a1f7252 100644 (file)
@@ -49,21 +49,24 @@ HW_STATUS_DEBUG = True
 # --------------------
 # True 时用三档电压门限估算 SoC,忽略 BATTERY_OCV_MAP
 USE_SIMPLE_VBATT_THRESH = False
-VBATT_CHARGE_FULL_MV = 4290   # USB 在位且电压≥此值视为充满(get_battery / AI_hw_status / AI_dev 共用)
-VBATT_LOW_MV = 3699           # 低电电压门限;simple 模式与 AI_dev 低电提示用
-VBATT_EMPTY_MV = 3629         # ç©ºç\94µç\94µå\8e\8bé\97¨é\99\90ï¼\9bAI_dev ä½\8eç\94µå\85³æ\9cºç­\89
-BATTERY_LOW_SOC_PERCENT = 30  # OCV 模式:soc < 此值 → battery=low(REQ-BAT-02:10%~30%)
-BATTERY_EMPTY_SOC_PERCENT = 10  # OCV 模式:soc < 此值 → battery=empty(REQ-BAT-03:<10%)
+VBATT_CHARGE_FULL_MV = 4320   # 502631 满电 OCV≈4.322V
+VBATT_LOW_MV = 3630           # 约 ZCV 20%
+VBATT_EMPTY_MV = 3400         # æ\9cªå\85\85ç\94µï¼\9aå¹³å\9d\87ç\94µå\8e\8b < 3.40V â\86\92 ä½\8eç\94µå\85³æ\9cºï¼\88å\94¯ä¸\80å\85³æ\9cºæ\9d¡ä»¶ï¼\89
+BATTERY_LOW_SOC_PERCENT = 25  # 未充电:soc<25% 低电语音
+BATTERY_EMPTY_SOC_PERCENT = 5   # 未充电:soc<5% LED 空电闪;不触发关机
 BATTERY_LOW_WARN_INTERVAL_MS = 300000  # REQ-BAT-02:低电语音每 5 分钟
 BATTERY_EMPTY_FLASH_COUNT = 3       # REQ-BAT-03:红快闪次数
 BATTERY_EMPTY_FLASH_PERIOD_MS = 500  # REQ-BAT-03:约 2 次/秒
 HW_STATUS_PERIOD_MS = 10000
+VBATT_AVG_LEN = 10
+VBATT_PUSH_MIN_MS = 9000
 SIGNAL_WEAK_CSQ = 10
 
-# 开路电压(mV) -> SoC(%),按电压线性插值;None 则用 ai_VC 内置默认表
+# 502631 ZCV OCV 表;0% 锚定 3400mV(实机 4G 场景)
 BATTERY_OCV_MAP = {
-    4290: 100, 4237: 95, 4179: 90, 4123: 85, 4070: 80, 4020: 75, 3963: 70, 3919: 65, 3882: 60, 3847: 55,
-    3818: 50, 3794: 45, 3772: 40, 3752: 35, 3733: 30, 3716: 25, 3699: 20, 3677: 15, 3652: 10, 3629: 5, 3400: 0,
+    4322: 100, 4239: 95, 4177: 90, 4117: 85, 4059: 80, 4006: 75, 3942: 70, 3898: 65,
+    3853: 60, 3814: 55, 3781: 50, 3755: 45, 3738: 40, 3724: 35, 3706: 30, 3678: 25,
+    3634: 20, 3577: 15, 3522: 10, 3468: 5, 3400: 0,
 }
 
 # 是否需要初始化蓝牙电源/马达/音频IIS配置等(原先仅 7PIN_1_5)
index 4360c21..4f65393 100644 (file)
@@ -84,22 +84,22 @@ HW_STATUS_PERIOD_MS = 10000
 SIGNAL_WEAK_CSQ = 10  # 0≤CSQ<此值 → signal=weak(REQ-ERR-02)\r
 \r
 # SoC 百分比门限(OCV 模式,对齐 REQ-BAT-02/03)\r
-BATTERY_LOW_SOC_PERCENT = 30\r
-BATTERY_EMPTY_SOC_PERCENT = 10\r
+BATTERY_LOW_SOC_PERCENT = 25\r
+BATTERY_EMPTY_SOC_PERCENT = 5\r
 BATTERY_LOW_WARN_INTERVAL_MS = 300000  # 低电语音间隔 5min\r
 BATTERY_EMPTY_FLASH_COUNT = 3\r
 BATTERY_EMPTY_FLASH_PERIOD_MS = 500    # 空电红快闪约 2 次/秒\r
 \r
 # 电压门限(mV):充满 / 低电 / 空电(与 get_battery、simple 模式共用)\r
 USE_SIMPLE_VBATT_THRESH = False  # True 时忽略 BATTERY_OCV_MAP,仅用三档电压\r
-VBATT_CHARGE_FULL_MV = 4290\r
-VBATT_LOW_MV = 3699\r
-VBATT_EMPTY_MV = 3629\r
+VBATT_CHARGE_FULL_MV = 4320\r
+VBATT_LOW_MV = 3630\r
+VBATT_EMPTY_MV = 3400\r
 \r
-# 开路电压(mV)→SoC(%) 插值表;None 则用 ai_VC 内置默认表\r
 BATTERY_OCV_MAP = {\r
-    4290: 100, 4237: 95, 4179: 90, 4123: 85, 4070: 80, 4020: 75, 3963: 70, 3919: 65, 3882: 60, 3847: 55,\r
-    3818: 50, 3794: 45, 3772: 40, 3752: 35, 3733: 30, 3716: 25, 3699: 20, 3677: 15, 3652: 10, 3629: 5, 3400: 0,\r
+    4322: 100, 4239: 95, 4177: 90, 4117: 85, 4059: 80, 4006: 75, 3942: 70, 3898: 65,\r
+    3853: 60, 3814: 55, 3781: 50, 3755: 45, 3738: 40, 3724: 35, 3706: 30, 3678: 25,\r
+    3634: 20, 3577: 15, 3522: 10, 3468: 5, 3400: 0,\r
 }\r
 \r
 # =============================================================================\r
index d26ea6f..aefa493 100644 (file)
@@ -81,25 +81,29 @@ VAD_ACTIVE_BLUE_LED = True
 # =============================================================================
 
 HW_STATUS_PERIOD_MS = 10000
+# 电池电压:全机约每 10s 采样 1 次 getVbatt,10 点滑动平均(见 vc_hw._BatteryCfg)
+VBATT_AVG_LEN = 10
+VBATT_PUSH_MIN_MS = 9000   # 同周期内 AI_hw / protocol 去重,避免重复读电压
 SIGNAL_WEAK_CSQ = 10  # 0≤CSQ<此值 → signal=weak(REQ-ERR-02)
 
-# SoC 百分比门限(OCV 模式,对齐 REQ-BAT-02/03)
-BATTERY_LOW_SOC_PERCENT = 30
-BATTERY_EMPTY_SOC_PERCENT = 10
+# SoC 门限:仅低电提示/LED 空电闪(REQ-BAT-02/03);关机见 VBATT_EMPTY_MV
+BATTERY_LOW_SOC_PERCENT = 25       # <25% 低电语音
+BATTERY_EMPTY_SOC_PERCENT = 5      # <5% LED 空电态;不单独触发关机
 BATTERY_LOW_WARN_INTERVAL_MS = 300000  # 低电语音间隔 5min
 BATTERY_EMPTY_FLASH_COUNT = 3
 BATTERY_EMPTY_FLASH_PERIOD_MS = 500    # 空电红快闪约 2 次/秒
 
 # 电压门限(mV):充满 / 低电 / 空电(与 get_battery、simple 模式共用)
 USE_SIMPLE_VBATT_THRESH = False  # True 时忽略 BATTERY_OCV_MAP,仅用三档电压
-VBATT_CHARGE_FULL_MV = 4290
-VBATT_LOW_MV = 3699
-VBATT_EMPTY_MV = 3629
+VBATT_CHARGE_FULL_MV = 4320   # 502631 ZCV 100% OCV≈4.322V;CV 4.35V
+VBATT_LOW_MV = 3630           # 约对应 ZCV 20%(4G 带载下不宜再抬高)
+VBATT_EMPTY_MV = 3400         # 未充电:滑动平均电压 < 此值 → 低电关机(唯一关机条件)
 
-# 开路电压(mV)→SoC(%) 插值表;None 则用 ai_VC 内置默认表
+# 502631-510 4.35V ZCV 放电 OCV 表(mV→SoC);0% 锚定 3400 而非实验室 3262
 BATTERY_OCV_MAP = {
-    4290: 100, 4237: 95, 4179: 90, 4123: 85, 4070: 80, 4020: 75, 3963: 70, 3919: 65, 3882: 60, 3847: 55,
-    3818: 50, 3794: 45, 3772: 40, 3752: 35, 3733: 30, 3716: 25, 3699: 20, 3677: 15, 3652: 10, 3629: 5, 3400: 0,
+    4322: 100, 4239: 95, 4177: 90, 4117: 85, 4059: 80, 4006: 75, 3942: 70, 3898: 65,
+    3853: 60, 3814: 55, 3781: 50, 3755: 45, 3738: 40, 3724: 35, 3706: 30, 3678: 25,
+    3634: 20, 3577: 15, 3522: 10, 3468: 5, 3400: 0,
 }
 
 # =============================================================================
index 682df73..2567a6e 100644 (file)
@@ -84,22 +84,22 @@ HW_STATUS_PERIOD_MS = 10000
 SIGNAL_WEAK_CSQ = 10  # 0≤CSQ<此值 → signal=weak(REQ-ERR-02)
 
 # SoC 百分比门限(OCV 模式,对齐 REQ-BAT-02/03)
-BATTERY_LOW_SOC_PERCENT = 30
-BATTERY_EMPTY_SOC_PERCENT = 10
+BATTERY_LOW_SOC_PERCENT = 25
+BATTERY_EMPTY_SOC_PERCENT = 5
 BATTERY_LOW_WARN_INTERVAL_MS = 300000  # 低电语音间隔 5min
 BATTERY_EMPTY_FLASH_COUNT = 3
 BATTERY_EMPTY_FLASH_PERIOD_MS = 500    # 空电红快闪约 2 次/秒
 
 # 电压门限(mV):充满 / 低电 / 空电(与 get_battery、simple 模式共用)
 USE_SIMPLE_VBATT_THRESH = False  # True 时忽略 BATTERY_OCV_MAP,仅用三档电压
-VBATT_CHARGE_FULL_MV = 4290
-VBATT_LOW_MV = 3699
-VBATT_EMPTY_MV = 3629
+VBATT_CHARGE_FULL_MV = 4320
+VBATT_LOW_MV = 3630
+VBATT_EMPTY_MV = 3400
 
-# 开路电压(mV)→SoC(%) 插值表;None 则用 ai_VC 内置默认表
 BATTERY_OCV_MAP = {
-    4290: 100, 4237: 95, 4179: 90, 4123: 85, 4070: 80, 4020: 75, 3963: 70, 3919: 65, 3882: 60, 3847: 55,
-    3818: 50, 3794: 45, 3772: 40, 3752: 35, 3733: 30, 3716: 25, 3699: 20, 3677: 15, 3652: 10, 3629: 5, 3400: 0,
+    4322: 100, 4239: 95, 4177: 90, 4117: 85, 4059: 80, 4006: 75, 3942: 70, 3898: 65,
+    3853: 60, 3814: 55, 3781: 50, 3755: 45, 3738: 40, 3724: 35, 3706: 30, 3678: 25,
+    3634: 20, 3577: 15, 3522: 10, 3468: 5, 3400: 0,
 }
 
 # =============================================================================
index 499b341..8e8a3e8 100644 (file)
@@ -1648,10 +1648,8 @@ class SockMangager(Abstract):
         else:\r
             charge_state = '0'\r
 \r
-        #temp = Power.getVbatt()\r
-        temp = EventMesh.publish("get_curr_battery_level")\r
-        vbat = EventMesh.publish("get_battery",temp)\r
-        if vbat == None:\r
+        vbat = EventMesh.publish("get_battery")\r
+        if vbat is None:\r
             vbat = 100\r
         # EventMesh.debug_log("vbat",vbat)\r
         # if temp != None :\r
@@ -1694,20 +1692,19 @@ class SockMangager(Abstract):
         self.public_report_event_cb()\r
     #上报心跳 上报设备状态 检查闹钟 日程\r
     def public_report_event_cb(self):\r
-        """电池平均值"""\r
-        temp = Power.getVbatt()\r
-        if self.bat_count < 10:\r
-            self.vbat_buf.append(temp)\r
-            self.bat_count += 1\r
-        else:\r
-            for i in range(9):\r
-                self.vbat_buf[i] = self.vbat_buf[i+1]\r
-            self.vbat_buf[9] = temp\r
-        curr_count = len(self.vbat_buf)\r
+        """电池平均值:与 vc_hw._BatteryCfg 共用 10 点滑动平均,避免重复 getVbatt。"""\r
         bat_total_value = 0\r
-        for i in range(curr_count):\r
-            bat_total_value += self.vbat_buf[i]\r
-        bat_total_value = bat_total_value//curr_count#一分钟更新一次电量 10个平均值\r
+        try:\r
+            from vc_hw import _BatteryCfg\r
+            bcfg = _BatteryCfg.instance()\r
+            try:\r
+                usb_st = EventMesh.publish("GET_USB_STATE")\r
+            except Exception:\r
+                usb_st = 0\r
+            bcfg.push_vbatt_sample(usb_status=usb_st)\r
+            bat_total_value = bcfg.soc_now()\r
+        except Exception:\r
+            bat_total_value = 0\r
         # EventMesh.publish("AI_schedule_open")#读取闹钟数组\r
         # print("AI_schedule_open!!!")\r
         if EventMesh.publish("get_sos_flag") != None:\r
index bf7ac80..b9f5c38 100644 (file)
@@ -25,7 +25,7 @@ try:
 except Exception:\r
     vc_config = None\r
 from vc_common import dev_type\r
-from vc_hw import set_ipv4_datacall\r
+from vc_hw import set_ipv4_datacall, _BatteryCfg\r
 from vc_uart import AI_UART\r
 \r
 audio_buf = ['chaobingfatis', 'chenggong', 'didianguanji', 'IMEIhao', 'jianquancuowu', 'key_prompt', 'liuliangtishi', 'Low_power_warning', 'R_CHARGE_FULL', 'R_CHARGE_IN', 'R_CHARGE_OUT', 'R_READ_TIME_0', 'R_READ_TIME_1', 'R_READ_TIME_2', 'R_READ_TIME_3', 'R_READ_TIME_4', 'R_READ_TIME_5', 'R_READ_TIME_6', 'R_READ_TIME_7', 'R_READ_TIME_8', 'R_READ_TIME_9', 'R_READ_TIME_A', 'R_READ_TIME_B', 'R_READ_TIME_C', 'R_READ_TIME_D', 'R_READ_TIME_E', 'R_READ_TIME_F', 'R_READ_TIME_G', 'R_READ_TIME_H', 'R_READ_TIME_I', 'R_READ_TIME_J', 'R_READ_TIME_K', 'R_READ_TIME_L', 'R_READ_TIME_M', 'R_READ_TIME_N', 'R_READ_TIME_O', 'R_READ_TIME_P', 'R_READ_TIME_Q', 'R_READ_TIME_R', 'R_READ_TIME_S', 'R_READ_TIME_T', 'R_READ_TIME_U', 'R_READ_TIME_V', 'R_READ_TIME_W', 'R_READ_TIME_X', 'R_READ_TIME_Y', 'R_READ_TIME_Z', 'send_tone', 'shouhou', 'xinhaoruo1', 'zhengzaiguanji']\r
@@ -279,6 +279,10 @@ class AI_dev(Abstract):
             EventMesh.publish("hw_usb_state", state)\r
         except Exception:\r
             pass\r
+        try:\r
+            _BatteryCfg.instance().on_usb_change(1 if state == 1 else 0)\r
+        except Exception:\r
+            pass\r
         EventMesh.publish("publish_AI_btnInfo",{"func":self.usb_event,"data":state})\r
 \r
     def usb_event(self, state):    \r
@@ -914,14 +918,14 @@ class AI_dev(Abstract):
         voice_record_satrt_flag = EventMesh.publish("get_voice_record_flag")#获取静默或者语音留言是否在运行\r
         if voice_record_satrt_flag == None:\r
             voice_record_satrt_flag = 0\r
-        Vbatt = Power.getVbatt()\r
+        _bcfg = _BatteryCfg.instance()\r
+        Vbatt = _bcfg.get_vbatt_mv_charge() if self.usb_status == 1 else _bcfg.get_vbatt_mv()\r
         if self.usb_status == 1 and VC_satrt_flag == 0 and voice_record_satrt_flag == 0:\r
             try:\r
                 if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):\r
                     EventMesh.publish("set_led_state", {"charging": "charging"})#充电红灯常亮\r
             except Exception:\r
                 pass\r
-        _bcfg = _BatteryCfg.instance()\r
         if self.usb_status == 1 and Vbatt >= _bcfg.charge_full_mv and VC_satrt_flag == 0 and voice_record_satrt_flag == 0:\r
             try:\r
                 if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):\r
@@ -934,9 +938,8 @@ class AI_dev(Abstract):
                 self.usb_charge_flag = 1\r
         try:\r
             low_soc = int(_bcfg.low_soc)\r
-            empty_soc = int(_bcfg.empty_soc)\r
         except Exception:\r
-            low_soc, empty_soc = 30, 10\r
+            low_soc = 25\r
         warn_interval_ms = 300000\r
         try:\r
             if "vc_config" in globals() and vc_config:\r
@@ -948,18 +951,17 @@ class AI_dev(Abstract):
             soc = self.get_battery()\r
         except Exception:\r
             soc = None\r
-        if self.usb_status == 0 and soc is not None:\r
-            empty_by_soc = soc < empty_soc\r
-            empty_by_mv = Vbatt is not None and Vbatt < int(_bcfg.empty_mv)\r
-            if empty_by_soc or empty_by_mv:\r
+        if self.usb_status == 0:\r
+            if _bcfg.should_shutdown_empty(Vbatt):\r
                 if getattr(self, "_empty_shutdown_done", 0) == 0:\r
                     self._empty_shutdown_done = 1\r
                     try:\r
-                        print("[BAT] empty shutdown soc={} vbatt={}".format(soc, Vbatt))\r
+                        print("[BAT] empty shutdown vbatt={} soc={} empty_mv={}".format(\r
+                            Vbatt, soc, _bcfg.empty_mv))\r
                     except Exception:\r
                         pass\r
                     EventMesh.publish("publish_AI_btnInfo", {"func": self._battery_empty_shutdown_seq})\r
-            elif soc < low_soc:\r
+            elif soc is not None and soc < low_soc:\r
                 if voice_record_satrt_flag == 0:\r
                     try:\r
                         now = utime.ticks_ms()\r
@@ -991,19 +993,11 @@ class AI_dev(Abstract):
             self.pwk_key_Long_Press()\r
         \r
     def get_battery(self, event=None, msg=None):\r
-        # 返回 int 型 SoC(0~100):便于 TTS/上报/比较,且 OCV 插值本身比裸电压稳;不做小数避免频繁跳变\r
+        # SoC 基于 _BatteryCfg 10 点滑动平均电压,与 protocol 心跳/AI_hw 一致\r
         try:\r
-            Vbatt = Power.getVbatt()\r
+            return _BatteryCfg.instance().soc_now()\r
         except Exception:\r
             return 0\r
-        soc = _BatteryCfg.instance().soc_from_vbatt(Vbatt)\r
-        if soc is None:\r
-            return 0\r
-        if soc < 0:\r
-            return 0\r
-        if soc > 100:\r
-            return 100\r
-        return int(soc)\r
     def _audio_is_battery_priority(self, msg):\r
         try:\r
             for i in (msg or []):\r
index 0c48038..a72b0c2 100644 (file)
@@ -18,10 +18,11 @@ except Exception:
 from vc_common import dev_type\r
 \r
 def _default_battery_ocv_map():\r
-    # 与 mgr.py BATTERY_OCV_TABLE nix_coy_mnzo2 默认一致;可被 default_config.BATTERY_OCV_MAP 覆盖\r
+    # 无 BATTERY_OCV_MAP 配置时的兜底(502631 ZCV;0% 锚定 3400mV,适配 4G 带载)\r
     return {\r
-        4290: 100, 4237: 95, 4179: 90, 4123: 85, 4070: 80, 4020: 75, 3963: 70, 3919: 65, 3882: 60, 3847: 55,\r
-        3818: 50, 3794: 45, 3772: 40, 3752: 35, 3733: 30, 3716: 25, 3699: 20, 3677: 15, 3652: 10, 3629: 5, 3400: 0,\r
+        4322: 100, 4239: 95, 4177: 90, 4117: 85, 4059: 80, 4006: 75, 3942: 70, 3898: 65,\r
+        3853: 60, 3814: 55, 3781: 50, 3755: 45, 3738: 40, 3724: 35, 3706: 30, 3678: 25,\r
+        3634: 20, 3577: 15, 3522: 10, 3468: 5, 3400: 0,\r
     }\r
 \r
 \r
@@ -88,7 +89,7 @@ def _soc_from_vbatt_mv(volt_mv, ocv_map, ocv_volts, vbatt_simple=None):
 \r
 \r
 class _BatteryCfg(object):\r
-    """ä»\8e vc_config(default_config) å\8a è½½ç\94µæ± å\8f\82æ\95°ï¼\8cä¾\9b AI_hw_status / get_battery / AI_dev å\85±ç\94¨。"""\r
+    """ä»\8e vc_config(default_config) å\8a è½½ç\94µæ± å\8f\82æ\95°ï¼\9bå\85¨æ\9cºå\94¯ä¸\80 Power.getVbatt é\87\87æ ·ç\82¹ + 10 ç\82¹æ»\91å\8a¨å¹³å\9d\87。"""\r
     _inst = None\r
 \r
     def __init__(self):\r
@@ -97,16 +98,26 @@ class _BatteryCfg(object):
         self.charge_full_mv = 4290\r
         self.low_mv = 3699\r
         self.empty_mv = 3629\r
-        self.low_soc = 20\r
+        self.low_soc = 25\r
         self.empty_soc = 5\r
         self.vbatt_simple = None\r
+        self._vbat_buf = []\r
+        self._vbat_avg_mv = 0\r
+        self._vbat_chg_last_mv = 0\r
+        self._soc_frozen = None\r
+        self._usb_chg = 0\r
+        self._vbat_last_push_ms = 0\r
+        self._vbat_avg_len = 10\r
+        self._vbat_push_min_ms = 9000\r
         try:\r
             if "vc_config" in globals() and vc_config:\r
-                self.charge_full_mv = int(vc_config.get_int("VBATT_CHARGE_FULL_MV", 4290))\r
-                self.low_mv = int(vc_config.get_int("VBATT_LOW_MV", 3699))\r
-                self.empty_mv = int(vc_config.get_int("VBATT_EMPTY_MV", 3629))\r
-                self.low_soc = int(vc_config.get_int("BATTERY_LOW_SOC_PERCENT", 30))\r
-                self.empty_soc = int(vc_config.get_int("BATTERY_EMPTY_SOC_PERCENT", 10))\r
+                self.charge_full_mv = int(vc_config.get_int("VBATT_CHARGE_FULL_MV", 4320))\r
+                self.low_mv = int(vc_config.get_int("VBATT_LOW_MV", 3630))\r
+                self.empty_mv = int(vc_config.get_int("VBATT_EMPTY_MV", 3400))\r
+                self.low_soc = int(vc_config.get_int("BATTERY_LOW_SOC_PERCENT", 25))\r
+                self.empty_soc = int(vc_config.get_int("BATTERY_EMPTY_SOC_PERCENT", 5))\r
+                self._vbat_avg_len = int(vc_config.get_int("VBATT_AVG_LEN", 10)) or 10\r
+                self._vbat_push_min_ms = int(vc_config.get_int("VBATT_PUSH_MIN_MS", 9000)) or 9000\r
                 _ocv_cfg = _parse_battery_ocv_map(vc_config.get_attr("BATTERY_OCV_MAP", None))\r
                 if _ocv_cfg:\r
                     self.ocv_map = _ocv_cfg\r
@@ -122,9 +133,147 @@ class _BatteryCfg(object):
             cls._inst = cls()\r
         return cls._inst\r
 \r
+    def _push_vbatt_raw(self, raw_mv):\r
+        """与 protocol.py 一致:滑动窗口求算术平均(mV)。"""\r
+        try:\r
+            v = int(raw_mv)\r
+        except Exception:\r
+            return self._vbat_avg_mv\r
+        n = int(self._vbat_avg_len) or 10\r
+        buf = self._vbat_buf\r
+        if len(buf) < n:\r
+            buf.append(v)\r
+        else:\r
+            for i in range(n - 1):\r
+                buf[i] = buf[i + 1]\r
+            buf[n - 1] = v\r
+        total = 0\r
+        for x in buf:\r
+            total += x\r
+        self._vbat_avg_mv = total // len(buf)\r
+        return self._vbat_avg_mv\r
+\r
+    def _usb_is_charging(self):\r
+        try:\r
+            if self._usb_chg:\r
+                return 1\r
+            st = EventMesh.publish("GET_USB_STATE")\r
+            return 1 if st == 1 else 0\r
+        except Exception:\r
+            return 0\r
+\r
+    def on_usb_change(self, plugged_in):\r
+        """插拔 USB:充电电压偏高,与放电 OCV 不可混用;清空滑动窗并冻结/恢复 SoC。"""\r
+        try:\r
+            plug = 1 if plugged_in else 0\r
+            self._usb_chg = plug\r
+            if plug:\r
+                if self._vbat_buf and self._vbat_avg_mv > 0:\r
+                    s = self.soc_from_vbatt(self._vbat_avg_mv)\r
+                    self._soc_frozen = max(0, min(100, int(s) if s is not None else 0))\r
+                elif self._soc_frozen is None:\r
+                    self._soc_frozen = 0\r
+                self._vbat_buf = []\r
+                self._vbat_avg_mv = 0\r
+                self._vbat_last_push_ms = 0\r
+            else:\r
+                self._vbat_buf = []\r
+                self._vbat_avg_mv = 0\r
+                self._vbat_chg_last_mv = 0\r
+                self._vbat_last_push_ms = 0\r
+        except Exception:\r
+            pass\r
+\r
+    def push_vbatt_sample(self, force=0, usb_status=None):\r
+        """采样一次 getVbatt:充电只记瞬时电压(判满);放电才进滑动平均(算 SoC/关机)。"""\r
+        try:\r
+            now = utime.ticks_ms()\r
+            chg = self._usb_chg if usb_status is None else (1 if usb_status == 1 else 0)\r
+            if not force and self._vbat_last_push_ms:\r
+                if utime.ticks_diff(now, self._vbat_last_push_ms) < int(self._vbat_push_min_ms):\r
+                    if chg:\r
+                        return self._vbat_chg_last_mv or self._vbat_avg_mv\r
+                    return self._vbat_avg_mv\r
+            self._vbat_last_push_ms = now\r
+            raw = Power.getVbatt()\r
+        except Exception:\r
+            if self._usb_is_charging():\r
+                return self._vbat_chg_last_mv\r
+            return self._vbat_avg_mv\r
+        try:\r
+            chg = self._usb_chg if usb_status is None else (1 if usb_status == 1 else 0)\r
+            if chg:\r
+                self._vbat_chg_last_mv = int(raw)\r
+                return self._vbat_chg_last_mv\r
+        except Exception:\r
+            pass\r
+        return self._push_vbatt_raw(raw)\r
+\r
+    def get_vbatt_mv(self):\r
+        """放电路径滑动平均电压(mV);充电时不混用充电偏高电压。"""\r
+        if self._usb_is_charging():\r
+            return self._vbat_avg_mv if self._vbat_avg_mv > 0 else 0\r
+        if self._vbat_buf and self._vbat_avg_mv > 0:\r
+            return self._vbat_avg_mv\r
+        return self.push_vbatt_sample(force=1, usb_status=0)\r
+\r
+    def get_vbatt_mv_charge(self):\r
+        """充电时最近一次采样(判充满);未采样则触发一次。"""\r
+        if self._vbat_chg_last_mv > 0:\r
+            return self._vbat_chg_last_mv\r
+        return self.push_vbatt_sample(force=1, usb_status=1)\r
+\r
     def soc_from_vbatt(self, volt_mv):\r
         return _soc_from_vbatt_mv(volt_mv, self.ocv_map, self.ocv_volts, self.vbatt_simple)\r
 \r
+    def soc_now(self):\r
+        """放电:OCV 表 + 滑动平均;充电:冻结插 USB 前的 SoC,避免充电电压算成 100%。"""\r
+        if self._usb_is_charging():\r
+            if self._soc_frozen is not None:\r
+                return int(self._soc_frozen)\r
+            return 100\r
+        v = self.get_vbatt_mv()\r
+        s = self.soc_from_vbatt(v)\r
+        if s is None:\r
+            return 0\r
+        if s < 0:\r
+            return 0\r
+        if s > 100:\r
+            return 100\r
+        return int(s)\r
+\r
+    def should_shutdown_empty(self, vbatt_mv=None):\r
+        """产品定义:仅未充电且平均电压 < VBATT_EMPTY_MV 时低电关机。"""\r
+        if self._usb_is_charging():\r
+            return False\r
+        try:\r
+            v = int(vbatt_mv if vbatt_mv is not None else self.get_vbatt_mv())\r
+        except Exception:\r
+            return False\r
+        return v > 0 and v < int(self.empty_mv)\r
+\r
+    def led_battery_level(self, soc=None, vbatt_mv=None):\r
+        """未充电时 LED 用电位/SoC;充电时由 charging 灯态接管,返回 normal。"""\r
+        if self._usb_is_charging():\r
+            return "normal"\r
+        try:\r
+            s = int(soc if soc is not None else self.soc_now())\r
+        except Exception:\r
+            s = -1\r
+        try:\r
+            v = int(vbatt_mv if vbatt_mv is not None else self.get_vbatt_mv())\r
+        except Exception:\r
+            v = 0\r
+        if v > 0 and v < int(self.empty_mv):\r
+            return "empty"\r
+        if s >= 0 and s < int(self.empty_soc):\r
+            return "empty"\r
+        if s >= 0 and s < int(self.low_soc):\r
+            return "low"\r
+        if v > 0 and v < int(self.low_mv):\r
+            return "low"\r
+        return "normal"\r
+\r
 \r
 class AI_hw_status(Abstract):\r
     # 精简硬件状态采集:仅维护 LED 状态机所需的 charging/battery/signal\r
@@ -209,6 +358,10 @@ class AI_hw_status(Abstract):
             if self._usb == 0:\r
                 self._charge_full_latched = 0\r
                 self._charge_full_streak = 0\r
+            try:\r
+                self._bcfg.on_usb_change(self._usb)\r
+            except Exception:\r
+                pass\r
         except Exception:\r
             pass\r
         try:\r
@@ -234,9 +387,15 @@ class AI_hw_status(Abstract):
     def _update(self):\r
         # 每次刷新都“同步推送一次”当前硬件基础态到 LED(不依赖变化检测)\r
         # 目的:保证 10 秒轮询和 USB callback 都能及时刷新充电/电量/信号的基础态显示\r
-        # charging/battery\r
+        # charging/battery(本周期唯一 getVbatt;充电/放电分流)\r
         try:\r
-            v = Power.getVbatt()\r
+            usb_status = self._usb\r
+            if usb_status is None:\r
+                try:\r
+                    usb_status = EventMesh.publish("GET_USB_STATE")\r
+                except Exception:\r
+                    usb_status = 0\r
+            v = self._bcfg.push_vbatt_sample(usb_status=usb_status)\r
         except Exception:\r
             v = None\r
         soc = None\r
@@ -255,12 +414,17 @@ class AI_hw_status(Abstract):
                         usb_status = 0\r
                 except Exception:\r
                     usb_status = 0\r
-            # charging:插入后先红灯;勿用瞬时 vbatt≥满电阈值直接判满(充电器接入电压即偏高)\r
+            # charging:用充电通道电压判满(勿用放电 OCV 平均)\r
             if usb_status == 1:\r
+                v_chg = v\r
+                try:\r
+                    v_chg = self._bcfg.get_vbatt_mv_charge()\r
+                except Exception:\r
+                    pass\r
                 if getattr(self, "_charge_full_latched", 0) == 1:\r
                     chg = "full"\r
                 else:\r
-                    if v is not None and v >= int(self._charge_full_mv):\r
+                    if v_chg is not None and v_chg >= int(self._charge_full_mv):\r
                         self._charge_full_streak = int(getattr(self, "_charge_full_streak", 0)) + 1\r
                         need = int(getattr(self, "_charge_full_streak_need", 2) or 2)\r
                         if self._charge_full_streak >= need:\r
@@ -271,14 +435,12 @@ class AI_hw_status(Abstract):
                     else:\r
                         self._charge_full_streak = 0\r
                         chg = "charging"\r
-            # battery level for LED table\r
-            if usb_status != 1 and v is not None:\r
-                soc = self._bcfg.soc_from_vbatt(v)\r
-                if soc is not None:\r
-                    if soc < int(self._empty_soc):\r
-                        bat = "empty"\r
-                    elif soc < int(self._low_soc):\r
-                        bat = "low"\r
+            soc = None\r
+            try:\r
+                soc = self._bcfg.soc_now()\r
+            except Exception:\r
+                pass\r
+            bat = self._bcfg.led_battery_level(soc=soc, vbatt_mv=v if usb_status != 1 else None)\r
             self._chg = chg\r
             self._bat = bat\r
         except Exception:\r
index 185997d..b026683 100644 (file)
@@ -1254,7 +1254,11 @@ class VCSessionIoMixin:
             self.AI_TTS_str = "电量剩余百分之{}".format(battery)
             return 0
         if it == "query_voltage":
-            Vbatt = Power.getVbatt()
+            try:
+                from vc_hw import _BatteryCfg
+                Vbatt = _BatteryCfg.instance().get_vbatt_mv()
+            except Exception:
+                Vbatt = Power.getVbatt()
             self.AI_TTS_str = "当前电池电压{}".format(Vbatt)
             return 0
         if it == "query_iccid":