电池统一通过配置进行电压计算,并去掉电池相关硬编码。
authorSnickerSun <snickersun@163.com>
Sun, 17 May 2026 12:07:27 +0000 (20:07 +0800)
committerSnickerSun <snickersun@163.com>
Sun, 17 May 2026 12:07:27 +0000 (20:07 +0800)
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/ai_VC.py
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

index 3623398..8204a6a 100644 (file)
@@ -377,6 +377,115 @@ class AI_led(Abstract):
             return {"mode": getattr(self, "_mode", "?")}
 
 
+def _default_battery_ocv_map():
+    # 与 mgr.py BATTERY_OCV_TABLE nix_coy_mnzo2 默认一致;可被 default_config.BATTERY_OCV_MAP 覆盖
+    return {
+        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,
+    }
+
+
+def _parse_battery_ocv_map(raw):
+    """解析 default_config.BATTERY_OCV_MAP:dict 或 [(mv, soc), ...]。"""
+    if raw is None:
+        return None
+    out = {}
+    try:
+        if isinstance(raw, dict):
+            pairs = raw.items()
+        elif isinstance(raw, (tuple, list)):
+            pairs = ((p[0], p[1]) for p in raw if isinstance(p, (tuple, list)) and len(p) >= 2)
+        else:
+            return None
+        for k, v in pairs:
+            out[int(k)] = int(v)
+        return out if out else None
+    except Exception:
+        return None
+
+
+def _soc_from_vbatt_mv(volt_mv, ocv_map, ocv_volts, vbatt_simple=None):
+    """由电压(mV) 算 SoC(0~100);配置来自 default_config / _BatteryCfg。"""
+    try:
+        v = int(volt_mv)
+    except Exception:
+        return None
+    try:
+        if vbatt_simple:
+            _full, _v20, _v5 = vbatt_simple
+            if v >= int(_full):
+                return 100
+            if v <= int(_v5):
+                return 0
+            if v <= int(_v20):
+                return 10
+            return 60
+    except Exception:
+        pass
+    try:
+        volts = ocv_volts
+        if not volts:
+            return None
+        pre_volt = 0
+        volt_not_under = 0
+        soc1 = 0
+        soc2 = 0
+        for vv in volts:
+            if v > vv:
+                volt_not_under = 1
+                soc1 = ocv_map.get(vv, 0)
+                soc2 = ocv_map.get(pre_volt, 0)
+                break
+            else:
+                pre_volt = vv
+        if pre_volt == 0:
+            return soc1
+        if volt_not_under == 0:
+            return 0
+        return int(soc2 - (soc2 - soc1) * (pre_volt - v) // (pre_volt - vv))
+    except Exception:
+        return None
+
+
+class _BatteryCfg(object):
+    """从 vc_config(default_config) 加载电池参数,供 AI_hw_status / get_battery / AI_dev 共用。"""
+    _inst = None
+
+    def __init__(self):
+        self.ocv_map = _default_battery_ocv_map()
+        self.ocv_volts = sorted(self.ocv_map.keys(), reverse=True)
+        self.charge_full_mv = 4290
+        self.low_mv = 3699
+        self.empty_mv = 3629
+        self.low_soc = 20
+        self.empty_soc = 5
+        self.vbatt_simple = None
+        try:
+            if "vc_config" in globals() and vc_config:
+                self.charge_full_mv = int(vc_config.get_int("VBATT_CHARGE_FULL_MV", 4290))
+                self.low_mv = int(vc_config.get_int("VBATT_LOW_MV", 3699))
+                self.empty_mv = int(vc_config.get_int("VBATT_EMPTY_MV", 3629))
+                self.low_soc = int(vc_config.get_int("BATTERY_LOW_SOC_PERCENT", 20))
+                self.empty_soc = int(vc_config.get_int("BATTERY_EMPTY_SOC_PERCENT", 5))
+                _ocv_cfg = _parse_battery_ocv_map(vc_config.get_attr("BATTERY_OCV_MAP", None))
+                if _ocv_cfg:
+                    self.ocv_map = _ocv_cfg
+                    self.ocv_volts = sorted(self.ocv_map.keys(), reverse=True)
+                if vc_config.get_bool("USE_SIMPLE_VBATT_THRESH", False):
+                    self.vbatt_simple = (self.charge_full_mv, self.low_mv, self.empty_mv)
+        except Exception:
+            pass
+
+    @classmethod
+    def instance(cls):
+        if cls._inst is None:
+            cls._inst = cls()
+        return cls._inst
+
+    def soc_from_vbatt(self, volt_mv):
+        return _soc_from_vbatt_mv(volt_mv, self.ocv_map, self.ocv_volts, self.vbatt_simple)
+
+
 class AI_hw_status(Abstract):
     # 精简硬件状态采集:仅维护 LED 状态机所需的 charging/battery/signal
     def __init__(self):
@@ -385,17 +494,14 @@ class AI_hw_status(Abstract):
             print("[HW] __init__ enter")
         except Exception:
             pass
-        # 参考 mgr.py 的 BATTERY_OCV_TABLE(nix_coy_mnzo2),按电压插值换算 SoC
-        # 仅保留 LED 状态机所需:>=20 正常;<20 低电;<5 极低/空电
-        self._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,
-        }
-        self._ocv_volts = None
-        try:
-            self._ocv_volts = sorted(self._ocv_map.keys(), reverse=True)
-        except Exception:
-            self._ocv_volts = None
+        _bcfg = _BatteryCfg.instance()
+        self._bcfg = _bcfg
+        self._ocv_map = _bcfg.ocv_map
+        self._ocv_volts = _bcfg.ocv_volts
+        self._charge_full_mv = _bcfg.charge_full_mv
+        self._low_soc = _bcfg.low_soc
+        self._empty_soc = _bcfg.empty_soc
+        self._vbatt_simple = _bcfg.vbatt_simple
         # cached state (avoid repeat publish)
         self._chg = None
         self._bat = None
@@ -410,17 +516,6 @@ class AI_hw_status(Abstract):
                 self._period_ms = vc_config.get_int("HW_STATUS_PERIOD_MS", 10000) or 10000
                 self._weak_csq = vc_config.get_int("SIGNAL_WEAK_CSQ", 10) or 10
                 self._dbg = 1 if vc_config.get_bool("HW_STATUS_DEBUG", False) else 0
-                # 允许覆盖 OCV 表(用电压门限的方式降级兼容)
-                try:
-                    if vc_config.get_bool("USE_SIMPLE_VBATT_THRESH", False):
-                        cf = vc_config.get_int("VBATT_CHARGE_FULL_MV", 4300)
-                        v20 = vc_config.get_int("VBATT_LOW_MV", 3699)
-                        v5 = vc_config.get_int("VBATT_EMPTY_MV", 3629)
-                        self._vbatt_simple = (int(cf), int(v20), int(v5))
-                    else:
-                        self._vbatt_simple = None
-                except Exception:
-                    self._vbatt_simple = None
         except Exception:
             pass
         # 若开启了 LED/按键调试,也一并打开硬件监控打印,便于关联分析
@@ -512,24 +607,17 @@ class AI_hw_status(Abstract):
                     usb_status = 0
             # charging
             if usb_status == 1:
-                # 充满:默认用 4290mV 对齐 mgr OCV 100%,也允许用 simple 门限覆盖
-                full_mv = 4290
-                try:
-                    if getattr(self, "_vbatt_simple", None):
-                        full_mv = int(self._vbatt_simple[0])
-                except Exception:
-                    full_mv = 4290
-                if v is not None and v >= full_mv:
+                if v is not None and v >= int(self._charge_full_mv):
                     chg = "full"
                 else:
                     chg = "charging"
             # battery level for LED table
             if usb_status != 1 and v is not None:
-                soc = self._soc_from_vbatt(v)
+                soc = self._bcfg.soc_from_vbatt(v)
                 if soc is not None:
-                    if soc < 5:
+                    if soc < int(self._empty_soc):
                         bat = "empty"
-                    elif soc < 20:
+                    elif soc < int(self._low_soc):
                         bat = "low"
             self._chg = chg
             self._bat = bat
@@ -572,48 +660,9 @@ class AI_hw_status(Abstract):
             pass
 
     def _soc_from_vbatt(self, volt_mv):
-        # 返回 0~100;异常返回 None
-        try:
-            v = int(volt_mv)
-        except Exception:
-            return None
-        # 若启用 simple 门限,直接按门限输出近似 soc(仅用于 LED 分级)
-        try:
-            if getattr(self, "_vbatt_simple", None):
-                _full, _v20, _v5 = self._vbatt_simple
-                if v >= _full:
-                    return 100
-                if v <= _v5:
-                    return 0
-                if v <= _v20:
-                    return 10
-                return 60
-        except Exception:
-            pass
-        try:
-            volts = self._ocv_volts
-            if not volts:
-                return None
-            pre_volt = 0
-            volt_not_under = 0
-            soc1 = 0
-            soc2 = 0
-            for vv in volts:
-                if v > vv:
-                    volt_not_under = 1
-                    soc1 = self._ocv_map.get(vv, 0)
-                    soc2 = self._ocv_map.get(pre_volt, 0)
-                    break
-                else:
-                    pre_volt = vv
-            if pre_volt == 0:
-                return soc1
-            if volt_not_under == 0:
-                return 0
-            # 线性插值(对齐 mgr.__get_soc_from_dict)
-            return int(soc2 - (soc2 - soc1) * (pre_volt - v) // (pre_volt - vv))
-        except Exception:
-            return None
+        return self._bcfg.soc_from_vbatt(volt_mv)
+
+
 def set_ipv4_datacall():
        # 获取第一路的APN,ipType 等信息
        pdpCtx = dataCall.getPDPContext(1)
@@ -1447,7 +1496,6 @@ class AI_dev(Abstract):
                     audio.set_qdai_cfg(*qdai_cfg)  # 用iis做音频输入输出
             except Exception:
                 pass
-        self.VBATT_tab = {"charge_full":4300,"100" :4250 ,  "90":4179 , "80":4070, "60": 3883, "40":3770, "20":3699,  "10":3652,"5":3600,}
         self.KeyAI_status = 0
         self.KeyAI_timer_count = 0
         self.KeyAI_long_press = 0
@@ -2152,7 +2200,8 @@ class AI_dev(Abstract):
                     EventMesh.publish("set_led_state", {"charging": "charging"})#充电红灯常亮
             except Exception:
                 pass
-        if  self.usb_status == 1 and Vbatt>=self.VBATT_tab["charge_full"]and VC_satrt_flag == 0 and voice_record_satrt_flag == 0:#充电充满
+        _bcfg = _BatteryCfg.instance()
+        if self.usb_status == 1 and Vbatt >= _bcfg.charge_full_mv and VC_satrt_flag == 0 and voice_record_satrt_flag == 0:
             try:
                 if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):
                     EventMesh.publish("set_led_state", {"charging": "full"})
@@ -2162,7 +2211,7 @@ class AI_dev(Abstract):
                 EventMesh.publish("audio_montage_play",["R_CHARGE_FULL.mp3"])
                 #self.aud.play(1,0,'U:/static/R_CHARGE_FULL.mp3')#充电已满
                 self.usb_charge_flag = 1
-        if  self.usb_status == 0 and Vbatt<self.VBATT_tab["20"]and VC_satrt_flag == 0 and self.Low_power_warning_flag ==0 and voice_record_satrt_flag == 0:#低电报警
+        if self.usb_status == 0 and Vbatt < _bcfg.low_mv and VC_satrt_flag == 0 and self.Low_power_warning_flag == 0 and voice_record_satrt_flag == 0:
             self.Low_power_warning_flag  = 1
             try:
                 if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):
@@ -2174,14 +2223,14 @@ class AI_dev(Abstract):
             # if self.Low_power_warning_timer >= 2:
             #     self.Low_power_warning_timer = 0
             EventMesh.publish("audio_montage_play",["Low_power_warning.mp3"])
-        if  self.usb_status == 0 and Vbatt>=self.VBATT_tab["20"]and VC_satrt_flag == 0:#没有充电,电量又正常,关灯
+        if self.usb_status == 0 and Vbatt >= _bcfg.low_mv and VC_satrt_flag == 0:
             try:
                 if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):
                     EventMesh.publish("set_led_state", {"charging": "none", "battery": "normal"})
             except Exception:
                 pass
 
-        if  self.usb_status == 0 and Vbatt<self.VBATT_tab["5"]:    
+        if self.usb_status == 0 and Vbatt < _bcfg.empty_mv:
             EventMesh.publish("audio_montage_play",["didianguanji.mp3"])
             utime.sleep_ms(4500)
             self.aud.stopPlayStream()
@@ -2193,21 +2242,19 @@ class AI_dev(Abstract):
             self.pwk_key_Long_Press()
         
     def get_battery(self, event=None, msg=None):
-        Vbatt = Power.getVbatt()
-        if Vbatt>self.VBATT_tab["90"]:
+        # 返回 int 型 SoC(0~100):便于 TTS/上报/比较,且 OCV 插值本身比裸电压稳;不做小数避免频繁跳变
+        try:
+            Vbatt = Power.getVbatt()
+        except Exception:
+            return 0
+        soc = _BatteryCfg.instance().soc_from_vbatt(Vbatt)
+        if soc is None:
+            return 0
+        if soc < 0:
+            return 0
+        if soc > 100:
             return 100
-        if Vbatt>self.VBATT_tab["80"]:
-            return 90
-        if Vbatt>self.VBATT_tab["60"]:
-            return 80
-        if Vbatt>self.VBATT_tab["40"]:
-            return 60
-        if Vbatt>self.VBATT_tab["20"]:
-            return 40
-        if Vbatt>self.VBATT_tab["10"]:
-            return 20
-        if Vbatt<=self.VBATT_tab["10"]:
-            return 10
+        return int(soc)
     def audio_montage_play(self,event,msg):
         self.wakeup_BT()
         # 抢占规则:流播放(TTS)可打断本地;本地提示音不可打断流播放
index 0ba4676..1978dd3 100644 (file)
@@ -43,6 +43,25 @@ LED_DEBUG = True
 KEY_LED_DEBUG = True\r
 HW_STATUS_DEBUG = True\r
 \r
+# --------------------\r
+# 电池 OCV / 硬件状态轮询(AI_hw_status,可在 usr/default_config.py 覆盖)\r
+# --------------------\r
+# True 时用三档电压门限估算 SoC,忽略 BATTERY_OCV_MAP\r
+USE_SIMPLE_VBATT_THRESH = False\r
+VBATT_CHARGE_FULL_MV = 4290   # USB 在位且电压≥此值视为充满(get_battery / AI_hw_status / AI_dev 共用)\r
+VBATT_LOW_MV = 3699           # 低电电压门限;simple 模式与 AI_dev 低电提示用\r
+VBATT_EMPTY_MV = 3629         # 空电电压门限;AI_dev 低电关机等\r
+BATTERY_LOW_SOC_PERCENT = 20  # OCV 模式:soc < 此值 → battery=low\r
+BATTERY_EMPTY_SOC_PERCENT = 5 # OCV 模式:soc < 此值 → battery=empty\r
+HW_STATUS_PERIOD_MS = 10000\r
+SIGNAL_WEAK_CSQ = 10\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
+}\r
+\r
 # 是否需要初始化蓝牙电源/马达/音频IIS配置等(原先仅 7PIN_1_5)\r
 BT_POWER_MOTOR_ENABLED = False\r
 PIN_BT_ON_GPIO = None      # 例如 "GPIO31"\r
index 0ba4676..1978dd3 100644 (file)
@@ -43,6 +43,25 @@ LED_DEBUG = True
 KEY_LED_DEBUG = True\r
 HW_STATUS_DEBUG = True\r
 \r
+# --------------------\r
+# 电池 OCV / 硬件状态轮询(AI_hw_status,可在 usr/default_config.py 覆盖)\r
+# --------------------\r
+# True 时用三档电压门限估算 SoC,忽略 BATTERY_OCV_MAP\r
+USE_SIMPLE_VBATT_THRESH = False\r
+VBATT_CHARGE_FULL_MV = 4290   # USB 在位且电压≥此值视为充满(get_battery / AI_hw_status / AI_dev 共用)\r
+VBATT_LOW_MV = 3699           # 低电电压门限;simple 模式与 AI_dev 低电提示用\r
+VBATT_EMPTY_MV = 3629         # 空电电压门限;AI_dev 低电关机等\r
+BATTERY_LOW_SOC_PERCENT = 20  # OCV 模式:soc < 此值 → battery=low\r
+BATTERY_EMPTY_SOC_PERCENT = 5 # OCV 模式:soc < 此值 → battery=empty\r
+HW_STATUS_PERIOD_MS = 10000\r
+SIGNAL_WEAK_CSQ = 10\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
+}\r
+\r
 # 是否需要初始化蓝牙电源/马达/音频IIS配置等(原先仅 7PIN_1_5)\r
 BT_POWER_MOTOR_ENABLED = False\r
 PIN_BT_ON_GPIO = None      # 例如 "GPIO31"\r