优化拷机记录显示
authorCaisking <3449750381@qq.com>
Mon, 8 Jun 2026 07:13:50 +0000 (15:13 +0800)
committercaisking <caiyqm@qq.com>
Thu, 11 Jun 2026 04:01:46 +0000 (12:01 +0800)
code/sdk/zhihui/UI/producttest/include/testCcCycle.h
code/sdk/zhihui/UI/producttest/include/testView.h
code/sdk/zhihui/UI/producttest/include/testchanging.h
code/sdk/zhihui/UI/producttest/src/testCcCycle.cpp
code/sdk/zhihui/UI/producttest/src/testPresenter.cpp
code/sdk/zhihui/UI/producttest/src/testView.cpp
code/sdk/zhihui/UI/producttest/src/testchanging.cpp
code/sdk/zhihui/services/cc_cycle_service/cc_cycle_service.c
plan/充放电拷机汇总.plan.md
plan/充电ic专项硬件测试.plan.md
plan/拷机菜单位置与UI优化.plan.md

index 0ee4761..1e4aaaa 100644 (file)
@@ -25,8 +25,11 @@ namespace OHOS
         void ShowIcStatusPanel(void);
         void SetIcPanelVisible(bool visible);
         void SetHistoryPanelVisible(bool visible);
+        void ToggleIcPanelVisible(void);
+        void ToggleHistoryPanelVisible(void);
         void RefreshHistoryPanel(void);
         void CloseVisibleOverlay(void);
+        bool IsOverlayVisible(void) const;
 
     private:
         static void StatusTimerCallback(void *arg);
@@ -51,6 +54,7 @@ namespace OHOS
         UILabelButtonExt *labelButton_cc_ic_{nullptr};
         UILabelButtonExt *labelButton_cc_history_{nullptr};
         UILabelButtonExt *labelButton_cc_back_burn_{nullptr};
+        UILabelButtonExt *labelButton_overlay_back_{nullptr};
         UIScrollView *icStatusScrollView_{nullptr};
         UIScrollView *historyScrollView_{nullptr};
         UILabel *icStatusLabel_{nullptr};
index 28ec7be..c9aa08d 100644 (file)
@@ -132,8 +132,9 @@ namespace OHOS
         // 根据viewId实时更新对应界面的按钮颜色\r
         void UpdateButtonColorByViewId(const char* viewId);\r
 \r
-        /* 链式「下一项」:拷机仅主菜单进入,下一项为拷机或当前为拷机时不显示 */\r
+        /* 链式「下一项」:拷机页本身不显示;其余项仍显示(点击时跳过拷机项) */\r
         static bool ShouldShowChainNextButton(int16_t viewId);\r
+        void RefreshMainMenuUi(void);\r
     private:\r
         void InitIView(void);\r
         void initView_ext(int x, int y, int w, int h, int interval, const char *imagsrc);\r
index 2f665f0..e96e095 100644 (file)
@@ -38,6 +38,8 @@ namespace OHOS
         virtual ~testCHANINGView();
         static testCHANINGView *GetInstance(void);
         void ChangeViewcolor(bool);
+        void RefreshChargingUi(void);
+        void EnsureChargingNextButton(uint8_t id);
 
         UIScrollView *testCHANINGInitIView(uint8_t);
         void ClearAll(void);
@@ -56,6 +58,8 @@ namespace OHOS
 
         UILabelButtonExt* labelButton_next = nullptr;
         
+        void LayoutChargingNextButton(void);
+        
         // 添加成功失败按钮
         UILabelButton *oktestBtn{nullptr};
         UILabelButton *failtestBtn{nullptr};
index d0f1087..e3ca614 100644 (file)
@@ -176,6 +176,14 @@ namespace OHOS
         if (OHOS::NativeAbility::GetInstance().GetCurSliceId() != VIEW_TEST) {
             return;
         }
+        testView *tv = testView::GetInstance();
+        if (tv == nullptr) {
+            return;
+        }
+        UIScrollView *ccSv = tv->GetSystemScrollView(CCCYCLE);
+        if (ccSv == nullptr || !ccSv->IsVisible()) {
+            return;
+        }
         ::GraphicService::GetInstance()->PostGraphicEvent(CcCycleStatusRefreshOnGraphic);
     }
 
@@ -224,11 +232,13 @@ namespace OHOS
 
         if (title_ != nullptr) {
             title_->SetText("充放电拷机");
+            title_->Invalidate();
         }
         if (socLabel_ != nullptr) {
             char socBuf[16];
             (void)snprintf(socBuf, sizeof(socBuf), "%u%%", (unsigned int)cc_cycle_get_soc());
             socLabel_->SetText(socBuf);
+            socLabel_->Invalidate();
         }
         if (metricsLabel_ != nullptr) {
             float voltage = 0.0f;
@@ -239,6 +249,7 @@ namespace OHOS
             (void)snprintf(detail, sizeof(detail), "V:%.2fV  T:%.1fC",
                 (double)voltage, (double)temp);
             metricsLabel_->SetText(detail);
+            metricsLabel_->Invalidate();
         }
         if (statusLabel_ != nullptr) {
             char buf[128];
@@ -253,6 +264,7 @@ namespace OHOS
                 statusLabel_->SetText(buf);
                 statusLabel_->SetStyle(STYLE_TEXT_COLOR, Color::White().full);
             }
+            statusLabel_->Invalidate();
         }
         if (hintLabel_ != nullptr && !icPanelVisible_) {
             uint8_t defect = cc_cycle_get_defect_type();
@@ -261,6 +273,10 @@ namespace OHOS
             } else {
                 hintLabel_->SetText("需插线;选首轮充电或放电后自动循环");
             }
+            hintLabel_->Invalidate();
+        }
+        if (phaseLabel_ != nullptr) {
+            phaseLabel_->Invalidate();
         }
     }
 
@@ -305,6 +321,9 @@ namespace OHOS
         if (overlayGroup_ != nullptr) {
             overlayGroup_->SetVisible(overlayVisible);
         }
+        if (labelButton_overlay_back_ != nullptr) {
+            labelButton_overlay_back_->SetVisible(overlayVisible);
+        }
         if (testCcCycleScrollView_ != nullptr) {
             testCcCycleScrollView_->SetTouchable(!overlayVisible);
             testCcCycleScrollView_->SetDraggable(!overlayVisible);
@@ -345,6 +364,21 @@ namespace OHOS
         }
     }
 
+    void testCcCycleView::ToggleIcPanelVisible(void)
+    {
+        SetIcPanelVisible(!icPanelVisible_);
+    }
+
+    void testCcCycleView::ToggleHistoryPanelVisible(void)
+    {
+        SetHistoryPanelVisible(!historyPanelVisible_);
+    }
+
+    bool testCcCycleView::IsOverlayVisible(void) const
+    {
+        return icPanelVisible_ || historyPanelVisible_;
+    }
+
     void testCcCycleView::RefreshHistoryPanel(void)
     {
         if (g_ccCycleViewValid == 0 || historyLabel_ == nullptr) {
@@ -372,8 +406,7 @@ namespace OHOS
             RefreshHistoryPanel();
             InvalidateLocalView();
         } else if (!visible && !icPanelVisible_) {
-            RefreshStatusLabel();
-            InvalidateLocalView();
+            RefreshCcCycleButtons();
         }
     }
 
@@ -405,8 +438,7 @@ namespace OHOS
             ShowIcStatusPanel();
             InvalidateLocalView();
         } else if (!visible && !historyPanelVisible_) {
-            RefreshStatusLabel();
-            InvalidateLocalView();
+            RefreshCcCycleButtons();
         }
     }
 
@@ -502,6 +534,7 @@ namespace OHOS
         labelButton_cc_ic_ = nullptr;
         labelButton_cc_history_ = nullptr;
         labelButton_cc_back_burn_ = nullptr;
+        labelButton_overlay_back_ = nullptr;
         icStatusScrollView_ = nullptr;
         historyScrollView_ = nullptr;
         icStatusLabel_ = nullptr;
@@ -535,6 +568,9 @@ namespace OHOS
         testCcCycleScrollView_->SetStyle(STYLE_BACKGROUND_COLOR, Color::Black().full);
         testCcCycleScrollView_->SetScrollBlankSize(60);
         testCcCycleScrollView_->SetHorizontalScrollState(false);
+        testCcCycleScrollView_->SetVerticalScrollState(true);
+        testCcCycleScrollView_->SetYScrollBarVisible(true);
+        testCcCycleScrollView_->SetScrollBlankSize(80, UIAbstractScroll::Direction::BOTTOM);
 
         title_ = testAboutView::GetInstance()->init_title((w - 200) / 2, 40, "充放电拷机");
         if (title_ == nullptr) {
@@ -726,6 +762,15 @@ namespace OHOS
         SetupOverlayScroll(icStatusScrollView_);
         SetupOverlayScroll(historyScrollView_);
 
+        labelButton_overlay_back_ = CreateCcCycleButton(contentX, h - 88, contentW, CC_CYCLE_BTN_H,
+            "cc_cycle_overlay_back", clickListener);
+        if (labelButton_overlay_back_ == nullptr) {
+            return false;
+        }
+        labelButton_overlay_back_->SetText("返回拷机页");
+        labelButton_overlay_back_->SetVisible(false);
+        overlayGroup_->Add(labelButton_overlay_back_);
+
         rootGroup_->Add(testCcCycleScrollView_);
         rootGroup_->Add(overlayGroup_);
         (void)id;
index be17223..c6e90cf 100644 (file)
@@ -330,13 +330,19 @@ namespace OHOS
         }
         if (strcmp(view.GetViewId(), "cc_cycle_ic") == 0) {
             if (testCcCycleView::GetInstance() != nullptr) {
-                testCcCycleView::GetInstance()->SetIcPanelVisible(true);
+                testCcCycleView::GetInstance()->ToggleIcPanelVisible();
             }
             return true;
         }
         if (strcmp(view.GetViewId(), "cc_cycle_history") == 0) {
             if (testCcCycleView::GetInstance() != nullptr) {
-                testCcCycleView::GetInstance()->SetHistoryPanelVisible(true);
+                testCcCycleView::GetInstance()->ToggleHistoryPanelVisible();
+            }
+            return true;
+        }
+        if (strcmp(view.GetViewId(), "cc_cycle_overlay_back") == 0) {
+            if (testCcCycleView::GetInstance() != nullptr) {
+                testCcCycleView::GetInstance()->CloseVisibleOverlay();
             }
             return true;
         }
index 3c7c131..6a5f12a 100644 (file)
@@ -113,16 +113,45 @@ namespace OHOS
             if (Test_ViewMapper_t[i].viewId != viewId) {
                 continue;
             }
-#ifdef ENABLE_HP4570
-            if (Test_ViewMapper_t[i + 1].viewId == CCCYCLE) {
-                return false;
-            }
-#endif
-            return true;
+            return Test_ViewMapper_t[i].enable_falg;
         }
         return false;
     }
 
+    void testView::RefreshMainMenuUi(void)
+    {
+        if (SystemScrollView[TESTVIEW] == nullptr) {
+            return;
+        }
+        if (SystemLabel != nullptr) {
+            SystemLabel->SetText("GNT测试");
+            SystemLabel->Invalidate();
+        }
+        int len = sizeof(Test_ViewMapper_t) / sizeof(Test_ViewMapper_t[0]);
+        for (int b = 1; b < len; b++) {
+            if (button[b] == nullptr) {
+                continue;
+            }
+            const char *vid = button[b]->GetViewId();
+            if (vid == nullptr) {
+                continue;
+            }
+            for (int i = 1; i < len; i++) {
+                if (Test_ViewMapper_t[i].viewid_str == nullptr ||
+                    Test_ViewMapper_t[i].Labe_str == nullptr) {
+                    continue;
+                }
+                if (strcmp(vid, Test_ViewMapper_t[i].viewid_str) == 0) {
+                    button[b]->SetText(Test_ViewMapper_t[i].Labe_str);
+                    button[b]->SetVisible(true);
+                    button[b]->Invalidate();
+                    break;
+                }
+            }
+        }
+        SystemScrollView[TESTVIEW]->Invalidate();
+    }
+
     void testView::ClearAll()
     {
         // 1. 释放group和关联的SystemScrollView[TESTVIEW]
@@ -998,6 +1027,31 @@ namespace OHOS
             WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "SystemView:: SystemScrollView[%d] is nullptr", showView);
         }
 
+#ifdef ENABLE_HP4570
+        testCcCycleView *ccView = testCcCycleView::GetInstance();
+        if (ccView != nullptr) {
+            if (showView == CCCYCLE) {
+                ccView->ResumeStatusTimer();
+            } else {
+                ccView->PauseStatusTimer();
+            }
+        }
+#endif
+        if (showView == TESTVIEW) {
+            RefreshMainMenuUi();
+        } else if (showView == CHARG) {
+            testCHANINGView *chgView = testCHANINGView::GetInstance();
+            if (chgView != nullptr) {
+                chgView->EnsureChargingNextButton((uint8_t)CHARG);
+                chgView->RefreshChargingUi();
+            }
+        } else if (showView == CCCYCLE) {
+            testCcCycleView *ccRefreshView = testCcCycleView::GetInstance();
+            if (ccRefreshView != nullptr) {
+                ccRefreshView->RefreshCcCycleButtons();
+            }
+        }
+
         if (hideView != 0)
         {
             // 2. 修复:核心防死机 - 检查group是否为空(避免group->Remove()空指针崩溃)
index be1dc1a..a6aeca3 100644 (file)
@@ -51,6 +51,124 @@ namespace OHOS
         return g_pSystemAboutTheWatchView;
     }
     static UILabelButtonExt* button{NULL};
+
+    static void FillChargingMetricsText(char *buf, size_t buf_size)
+    {
+        float voltage = 0.0F;
+        float bat_temp = 0.0F;
+        float pcb_temp = 0.0F;
+        uint8_t power = 0;
+        uint8_t charge_id = 0;
+
+        if (buf == nullptr || buf_size == 0) {
+            return;
+        }
+#ifdef SUPPORT_BATTERY_SERVICE
+        power = battery_get_voltage(&voltage);
+        battery_get_pcb_temp(&pcb_temp);
+        battery_get_battery_temp(&bat_temp);
+#endif
+#ifdef ENABLE_HP4570
+        charge_id = (0x2 == hp4570n_get_id()) ? 1 : 0;
+        bool vbus_pg = hp4570n_read_status_vbus_pg();
+        bool chrg_fast = hp4570n_read_chrg_stat_fast_chrg();
+        bool chrg_done = hp4570n_read_chrg_stat_chrg_done();
+        (void)sprintf_s(buf, buf_size,
+            "power:%d\nvoltage:%.2f\nbat_temp:%.2f\npcb_temp:%.2f\ncharge Ic:%d\nvbus_pg:%d chrg:%d/%d",
+            power, voltage, bat_temp, pcb_temp, charge_id, (int)vbus_pg, (int)chrg_fast, (int)chrg_done);
+#else
+        (void)sprintf_s(buf, buf_size,
+            "power:%d\nvoltage:%.2f\nbat_temp:%.2f\npcb_temp:%.2f\ncharge Ic:%d",
+            power, voltage, bat_temp, pcb_temp, charge_id);
+#endif
+    }
+
+    static void ApplyChargingLabelSize(UILabel *label)
+    {
+        if (label == nullptr) {
+            return;
+        }
+        uint16_t textH = label->GetTextHeight();
+        if (textH < 32) {
+            textH = 32;
+        }
+        if (label->GetHeight() != textH) {
+            label->Resize(label->GetWidth(), textH);
+        }
+        label->Invalidate();
+    }
+
+    static int GetChargingNextButtonX(void)
+    {
+        return (Screen::GetInstance().GetWidth() - 135) / 2;
+    }
+
+    void testCHANINGView::LayoutChargingNextButton(void)
+    {
+        if (labelButton_next == nullptr) {
+            return;
+        }
+        int nextY = 320;
+        if (charnge_label != nullptr) {
+            nextY = charnge_label->GetY() + charnge_label->GetHeight() + 16;
+        }
+        labelButton_next->SetPosition(GetChargingNextButtonX(), nextY);
+        labelButton_next->SetVisible(true);
+        labelButton_next->SetTouchable(true);
+        labelButton_next->Invalidate();
+    }
+
+    void testCHANINGView::EnsureChargingNextButton(uint8_t id)
+    {
+        if (labelButton_next != nullptr) {
+            LayoutChargingNextButton();
+            return;
+        }
+        labelButton_next = testAboutView::GetInstance()->init_button_next(
+            GetChargingNextButtonX(), 320, Test_ViewMapper_t[id].viewid_str_next);
+        if (labelButton_next == nullptr) {
+            WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "EnsureChargingNextButton: init fail");
+            return;
+        }
+        if (testCHANINGScrollView != nullptr) {
+            testCHANINGScrollView->Add(labelButton_next);
+        }
+        LayoutChargingNextButton();
+    }
+
+    void testCHANINGView::RefreshChargingUi(void)
+    {
+        char battery_buf[96] = {0};
+
+        if (Title != nullptr) {
+            Title->SetText("充电测试");
+            Title->Invalidate();
+        }
+        if (charnge_label != nullptr) {
+            FillChargingMetricsText(battery_buf, sizeof(battery_buf));
+            charnge_label->SetText(battery_buf);
+            ApplyChargingLabelSize(charnge_label);
+        }
+        LayoutChargingNextButton();
+        if (button != nullptr) {
+            button->SetVisible(true);
+            button->Invalidate();
+        }
+#if ENABLE_TEST_RESULT_BUTTONS
+        if (oktestBtn != nullptr) {
+            oktestBtn->SetVisible(true);
+            oktestBtn->Invalidate();
+        }
+        if (failtestBtn != nullptr) {
+            failtestBtn->SetVisible(true);
+            failtestBtn->Invalidate();
+        }
+#endif
+        if (testCHANINGScrollView != nullptr) {
+            testCHANINGScrollView->Invalidate();
+        }
+    }
+
     void testCHANINGView::ClearAll()
     {
         if(testCHANINGScrollView)
@@ -150,10 +268,10 @@ namespace OHOS
                 button->SetStyle(STYLE_BACKGROUND_COLOR, Color::Blue().full);
                 button->SetText("usb::0");
             }
-            RootView::GetInstance()->Invalidate();
-           
+            if (testCHANINGScrollView != nullptr) {
+                testCHANINGScrollView->Invalidate();
+            }
         }
-        
     }
 
     bool testCHANINGView::testCHANINGScrollViewcrollViewinit(uint8_t id)
@@ -197,12 +315,9 @@ namespace OHOS
         // testCHANINGScrollView->SetOnDragListener(draglistener);
         testCHANINGScrollView->SetTouchable(true);
         testCHANINGScrollView->SetDraggable(true);
-
-        if (testView::ShouldShowChainNextButton(id)) {
-            labelButton_next = testAboutView::GetInstance()->init_button_next(
-                267, 278, Test_ViewMapper_t[id].viewid_str_next);
-            testCHANINGScrollView->Add(labelButton_next);
-        }
+        testCHANINGScrollView->SetVerticalScrollState(true);
+        testCHANINGScrollView->SetScrollBlankSize(48, UIAbstractScroll::Direction::TOP);
+        testCHANINGScrollView->SetScrollBlankSize(72, UIAbstractScroll::Direction::BOTTOM);
 
         UIView::OnDragListener *draglistener = static_cast<UIView::OnDragListener *>(testPresenter::GetInstance());
         if (draglistener == nullptr)
@@ -216,33 +331,29 @@ namespace OHOS
         testCHANINGScrollView->Add(Title);
 
 
-        float voltage = 0.0F;
-        float bat_temp = 0.0F;
-        float pcb_temp = 0.0F;
-        uint8_t power = 0;
-        uint8_t charge_id = 0;
-#ifdef SUPPORT_BATTERY_SERVICE
-        power = battery_get_voltage(&voltage);
-        battery_get_pcb_temp(&pcb_temp);
-        battery_get_battery_temp(&bat_temp);
-
-        charge_id = GetChargeId();
-        bool vbus_pg = hp4570n_read_status_vbus_pg();
-        bool chrg_fast = hp4570n_read_chrg_stat_fast_chrg();
-        bool chrg_done = hp4570n_read_chrg_stat_chrg_done();
-#endif
-        char  battery_buf[96]={0};
-
-#ifdef ENABLE_HP4570
-        sprintf_s(battery_buf,64,"power:%d\nvoltage:%.2f\nbat_temp:%.2f\npcb_temp:%.2f\ncharge Ic:%d\nvbus_pg:%d chrg:%d/%d",
-            power,voltage,bat_temp,pcb_temp,charge_id,(int)vbus_pg,(int)chrg_fast,(int)chrg_done);
-#else
-        sprintf_s(battery_buf,64,"power:%d\nvoltage:%.2f\nbat_temp:%.2f\npcb_temp:%.2f\ncharge Ic:%d",power,voltage,bat_temp,pcb_temp,charge_id);
-#endif
-
-        charnge_label= testAboutView::GetInstance()->init_title(70,240,battery_buf);
+//         float voltage = 0.0F;
+//         float bat_temp = 0.0F;
+//         float pcb_temp = 0.0F;
+//         uint8_t power = 0;
+//         uint8_t charge_id = 0;
+// #ifdef SUPPORT_BATTERY_SERVICE
+//         power = battery_get_voltage(&voltage);
+//         battery_get_pcb_temp(&pcb_temp);
+//         battery_get_battery_temp(&bat_temp);
+        char battery_buf[96] = {0};
+        FillChargingMetricsText(battery_buf, sizeof(battery_buf));
+
+        charnge_label = testAboutView::GetInstance()->init_title(70, 160, battery_buf);
+        if (charnge_label != nullptr) {
+            charnge_label->SetLineBreakMode(UILabel::LINE_BREAK_WRAP);
+            charnge_label->Resize(Screen::GetInstance().GetWidth() - 140, charnge_label->GetHeight());
+            ApplyChargingLabelSize(charnge_label);
+        }
         testCHANINGScrollView->Add(charnge_label);
 
+        /* 下一步必须最后添加,且位于电量文字下方,避免被 label 遮挡 */
+        EnsureChargingNextButton(id);
+
 #if ENABLE_TEST_RESULT_BUTTONS
         // 使用通用的InitLabelButton函数初始化按钮,避免viewId地址过期问题
         // 获取充电测试界面的按钮推荐位置
@@ -285,6 +396,7 @@ namespace OHOS
         //     // WEARABLE_LOGE(WEARABLE_LOG_MODULE_MAIN_SERVICE,"CAT1 SERVICE MCU has got pm_id_cat1 vote\r\n");
         // }
 
+        RefreshChargingUi();
         return true;
     }
 
index 47d1867..d98eb23 100644 (file)
@@ -1058,11 +1058,8 @@ static void cc_finish_discharge_phase(uint32_t now, uint8_t soc)
     }
     cc_save_discharge_duration(now);
     g_store.discharge_end_ts = now;
-    if (!g_phase_restore) {
-        cc_log_event_persist_locked("discharge_end", now, soc);
-    } else {
-        cc_store_mark_persist_locked();
-    }
+    cc_log_event_persist_locked("discharge_end", now, soc);
+    cc_store_mark_persist_locked();
     g_store.discharge_start_ts = 0;
     g_pending_save_time = 1;
 }
@@ -1092,9 +1089,13 @@ static void cc_enter_discharge_phase(void)
         soc = cc_read_soc(NULL);
         g_store.discharge_start_ts = now;
         cc_log_event_persist_locked("discharge_start", now, soc);
-    } else if (g_store.discharge_start_ts == 0) {
-        (void)uapi_get_timestampe_ss(&now);
-        g_store.discharge_start_ts = now;
+    } else {
+        if (g_store.discharge_start_ts == 0) {
+            (void)uapi_get_timestampe_ss(&now);
+            soc = cc_read_soc(NULL);
+            g_store.discharge_start_ts = now;
+            cc_log_event_persist_locked("discharge_start", now, soc);
+        }
     }
     cc_set_charge_enable(0);
     cc_sync_discharge_soc_mode();
@@ -1118,11 +1119,8 @@ static void cc_enter_charging_phase(void)
             soc = cc_read_soc(NULL);
             cc_save_discharge_duration(now);
             g_store.discharge_end_ts = now;
-            if (!g_phase_restore) {
-                cc_log_event_persist_locked("discharge_end", now, soc);
-            } else {
-                cc_store_mark_persist_locked();
-            }
+            cc_log_event_persist_locked("discharge_end", now, soc);
+            cc_store_mark_persist_locked();
             g_store.discharge_start_ts = 0;
         }
         cc_store_mark_persist_locked();
@@ -1135,13 +1133,12 @@ static void cc_enter_charging_phase(void)
     g_store.phase = (uint8_t)CC_PHASE_CHARGING;
     g_store.charge_end_ts = 0;
     (void)uapi_get_timestampe_ss(&chg_now);
-    if (!g_phase_restore) {
-        soc = cc_read_soc(NULL);
-        g_store.charge_start_ts = chg_now;
-        cc_log_event_persist_locked("charge_start", chg_now, soc);
-    } else if (g_store.charge_start_ts == 0) {
+    soc = cc_read_soc(NULL);
+    if (g_store.charge_start_ts == 0) {
         g_store.charge_start_ts = chg_now;
     }
+    /* 恢复路径也写 charge_start,避免 reboot 后 event_log 缺充电段;同 ts 由历史解析去重 */
+    cc_log_event_persist_locked("charge_start", g_store.charge_start_ts, soc);
     cc_set_white_screen(0);
     if (g_store.auto_running) {
         cc_set_discharge_screen_hold(1);
@@ -2227,10 +2224,25 @@ typedef struct {
     uint8_t  has_soc;
 } cc_hist_evt_t;
 
+typedef struct {
+    uint32_t start;
+    uint32_t end;
+    uint8_t  soc_end;
+    uint16_t round;
+    uint8_t  emitted;
+} cc_hist_chg_round_t;
+
 /* 拷机记录解析/workspace,勿放 GUI 线程栈上 */
 static cc_hist_evt_t g_hist_evts[CC_HIST_EVT_MAX];
+static cc_hist_chg_round_t g_hist_chg_rounds[CC_HIST_ROUND_MAX];
+static int g_hist_n_chg_rounds;
 static char g_hist_display[CC_CYCLE_HISTORY_BUF_RECOMMEND];
 
+static void cc_hist_emit_charge_line(char *buf, unsigned int buf_size, unsigned int *off,
+    uint32_t t0, uint32_t t1, uint8_t in_progress);
+static void cc_hist_emit_discharge_line(char *buf, unsigned int buf_size, unsigned int *off,
+    uint32_t t0, uint32_t t1, uint8_t in_progress);
+
 static int cc_hist_append(char *buf, unsigned int buf_size, unsigned int *off, const char *fmt, ...)
 {
     va_list ap;
@@ -2336,6 +2348,150 @@ static int cc_hist_load_all_events(void)
     return n_evts;
 }
 
+static int cc_hist_load_charge_rounds_from_path(const char *path, int n_rounds)
+{
+    FILE *fp;
+    char line[96];
+    int n = n_rounds;
+
+    if (path == NULL || n >= (int)CC_HIST_ROUND_MAX) {
+        return n_rounds;
+    }
+    fp = fopen(path, "r");
+    if (fp == NULL) {
+        return n;
+    }
+    while (n < (int)CC_HIST_ROUND_MAX && fgets(line, sizeof(line), fp) != NULL) {
+        unsigned int start = 0;
+        unsigned int end = 0;
+        unsigned int soc = 0;
+        unsigned int round = 0;
+
+        if (sscanf_s(line, "start=%u end=%u soc_end=%u round=%u",
+                &start, &end, &soc, &round) >= 2 && end >= start && start != 0) {
+            g_hist_chg_rounds[n].start = start;
+            g_hist_chg_rounds[n].end = end;
+            g_hist_chg_rounds[n].soc_end = (uint8_t)soc;
+            g_hist_chg_rounds[n].round = (uint16_t)round;
+            g_hist_chg_rounds[n].emitted = 0;
+            n++;
+        }
+    }
+    (void)fclose(fp);
+    return n;
+}
+
+static void cc_hist_load_charge_rounds(void)
+{
+    int n;
+
+    g_hist_n_chg_rounds = 0;
+    memset_s(g_hist_chg_rounds, sizeof(g_hist_chg_rounds), 0, sizeof(g_hist_chg_rounds));
+    n = cc_hist_load_charge_rounds_from_path(CC_CYCLE_LEGACY_CHARGE_LOG_PATH, 0);
+    g_hist_n_chg_rounds = cc_hist_load_charge_rounds_from_path(CC_CYCLE_CHARGE_LOG_PATH, n);
+}
+
+static int cc_hist_find_chg_round_by_end(uint32_t end_ts, int *idx_out)
+{
+    int i;
+
+    for (i = 0; i < g_hist_n_chg_rounds; i++) {
+        if (g_hist_chg_rounds[i].end == end_ts && g_hist_chg_rounds[i].start < end_ts) {
+            if (idx_out != NULL) {
+                *idx_out = i;
+            }
+            return 1;
+        }
+    }
+    return 0;
+}
+
+static int cc_hist_find_chg_between(uint32_t t0, uint32_t t1, int *idx_out)
+{
+    int i;
+    int best = -1;
+    uint32_t best_start = 0;
+
+    if (t1 <= t0) {
+        return 0;
+    }
+    for (i = 0; i < g_hist_n_chg_rounds; i++) {
+        const cc_hist_chg_round_t *r = &g_hist_chg_rounds[i];
+
+        if (r->start >= t0 && r->end <= t1 && r->start < r->end) {
+            if (best < 0 || r->start < best_start) {
+                best = i;
+                best_start = r->start;
+            }
+        }
+    }
+    if (best >= 0) {
+        if (idx_out != NULL) {
+            *idx_out = best;
+        }
+        return 1;
+    }
+    return 0;
+}
+
+static void cc_hist_mark_chg_emitted(int idx)
+{
+    if (idx >= 0 && idx < g_hist_n_chg_rounds) {
+        g_hist_chg_rounds[idx].emitted = 1;
+    }
+}
+
+static void cc_hist_emit_chg_round(char *buf, unsigned int buf_size, unsigned int *off,
+    int idx, uint8_t *has_lines)
+{
+    const cc_hist_chg_round_t *r;
+
+    if (idx < 0 || idx >= g_hist_n_chg_rounds || has_lines == NULL) {
+        return;
+    }
+    r = &g_hist_chg_rounds[idx];
+    if (r->emitted) {
+        return;
+    }
+    cc_hist_emit_charge_line(buf, buf_size, off, r->start, r->end, 0);
+    cc_hist_mark_chg_emitted(idx);
+    *has_lines = 1;
+}
+
+static void cc_hist_close_open_discharge(char *buf, unsigned int buf_size, unsigned int *off,
+    uint32_t *open_dis, uint32_t before_ts, uint8_t *has_lines)
+{
+    int chg_idx;
+
+    if (open_dis == NULL || *open_dis == 0 || before_ts <= *open_dis || has_lines == NULL) {
+        return;
+    }
+    if (cc_hist_find_chg_between(*open_dis, before_ts, &chg_idx)) {
+        const cc_hist_chg_round_t *r = &g_hist_chg_rounds[chg_idx];
+
+        cc_hist_emit_discharge_line(buf, buf_size, off, *open_dis, r->start, 0);
+        *has_lines = 1;
+        cc_hist_emit_chg_round(buf, buf_size, off, chg_idx, has_lines);
+        *open_dis = before_ts;
+        return;
+    }
+    cc_hist_emit_discharge_line(buf, buf_size, off, *open_dis, before_ts, 0);
+    *has_lines = 1;
+    *open_dis = before_ts;
+}
+
+static void cc_hist_emit_missing_charge_rounds(char *buf, unsigned int buf_size, unsigned int *off,
+    uint8_t *has_lines)
+{
+    int i;
+
+    for (i = 0; i < g_hist_n_chg_rounds; i++) {
+        if (!g_hist_chg_rounds[i].emitted) {
+            cc_hist_emit_chg_round(buf, buf_size, off, i, has_lines);
+        }
+    }
+}
+
 static int cc_hist_evt_name_eq(const cc_hist_evt_t *e, const char *name)
 {
     if (e == NULL || name == NULL) {
@@ -2459,8 +2615,9 @@ static int cc_hist_format_history_locked(char *buf, unsigned int buf_size)
     int ret;
 
     buf[0] = '\0';
+    cc_hist_load_charge_rounds();
     n_evts = cc_hist_load_all_events();
-    if (n_evts <= 0) {
+    if (n_evts <= 0 && g_hist_n_chg_rounds <= 0) {
         return snprintf_s(buf, buf_size, buf_size - 1, "暂无拷机记录\n");
     }
 
@@ -2486,18 +2643,19 @@ static int cc_hist_format_history_locked(char *buf, unsigned int buf_size)
         if (cc_hist_evt_name_eq(e, "charge_start")) {
             if (open_dis != 0) {
                 if (e->ts >= open_dis) {
-                    cc_hist_emit_discharge_line(buf, buf_size, &off, open_dis, e->ts, 0);
-                    has_lines = 1;
+                    cc_hist_close_open_discharge(buf, buf_size, &off, &open_dis, e->ts, &has_lines);
+                    open_dis = 0;
                 } else if (g_store.discharge_end_ts >= open_dis) {
                     cc_hist_emit_discharge_line(buf, buf_size, &off, open_dis,
                         g_store.discharge_end_ts, 0);
                     has_lines = 1;
+                    open_dis = 0;
                 } else {
                     /* RTC 回拨:重启后 charge_start 早于 discharge_start,仍保留放电段 */
                     cc_hist_emit_discharge_line(buf, buf_size, &off, open_dis, 0, 1);
                     has_lines = 1;
+                    open_dis = 0;
                 }
-                open_dis = 0;
             }
             if (open_chg != 0) {
                 if (e->ts > open_chg) {
@@ -2519,18 +2677,36 @@ static int cc_hist_format_history_locked(char *buf, unsigned int buf_size)
                 if (e->ts == 0 || e->ts >= open_chg) {
                     cc_hist_emit_charge_line(buf, buf_size, &off, open_chg, e->ts, 0);
                     has_lines = 1;
+                    {
+                        int chg_idx = -1;
+                        if (cc_hist_find_chg_round_by_end(e->ts, &chg_idx)) {
+                            cc_hist_mark_chg_emitted(chg_idx);
+                        }
+                    }
                 } else {
                     cc_hist_emit_charge_line(buf, buf_size, &off, open_chg, 0, 1);
                     has_lines = 1;
                 }
                 open_chg = 0;
+            } else if (e->ts != 0) {
+                /* charge_start 因 reboot/restore 未写入 event_log,从 charge_log 还原 */
+                int chg_idx = -1;
+
+                if (cc_hist_find_chg_round_by_end(e->ts, &chg_idx)) {
+                    const cc_hist_chg_round_t *r = &g_hist_chg_rounds[chg_idx];
+
+                    if (open_dis != 0 && r->start >= open_dis) {
+                        cc_hist_emit_discharge_line(buf, buf_size, &off, open_dis, r->start, 0);
+                        has_lines = 1;
+                        open_dis = 0;
+                    }
+                    cc_hist_emit_chg_round(buf, buf_size, &off, chg_idx, &has_lines);
+                }
             }
         } else if (cc_hist_evt_name_eq(e, "discharge_start")) {
             if (open_dis != 0) {
                 if (e->ts > open_dis) {
-                    cc_hist_emit_discharge_line(buf, buf_size, &off, open_dis, e->ts, 0);
-                    has_lines = 1;
-                    open_dis = e->ts;
+                    cc_hist_close_open_discharge(buf, buf_size, &off, &open_dis, e->ts, &has_lines);
                 }
                 /* e->ts <= open_dis:恢复脏数据,忽略 */
             } else {
@@ -2554,6 +2730,13 @@ static int cc_hist_format_history_locked(char *buf, unsigned int buf_size)
         open_dis = 0;
     }
 
+    cc_hist_emit_missing_charge_rounds(buf, buf_size, &off, &has_lines);
+
+    if (open_chg == 0 && g_store.charge_start_ts != 0 &&
+        auto_running && phase == CC_PHASE_CHARGING) {
+        open_chg = g_store.charge_start_ts;
+    }
+
     if (open_chg != 0 && auto_running && phase == CC_PHASE_CHARGING) {
         cc_hist_emit_charge_line(buf, buf_size, &off, open_chg, 0, 1);
         has_lines = 1;
index 11c8ca3..0446272 100644 (file)
@@ -1,22 +1,26 @@
 ---
 name: 充放电拷机汇总
 overview: FD272/HP4570 插线充放电自动拷机(cc_cycle)完整方案汇总:状态机、UI、持久化、充电不良、隔离门控、充电电流与 SOC 显示。
+doc_version: V9.0
 branch: feature/cc-cycle-burn-test
-updated: 2026-06-02
+updated: 2026-06-06
 open_issues:
   - 拷机界面 9% vs 重启表盘 23% SOC 不一致(B1~B4 已合入,待现场确认)
   - 左滑退出拷机页崩溃修复待现场确认
   - 充电不良检测(红/黄/蓝/低电流/HW未充)现场验证
   - 拷机阶段切换 vs 插拔充电线 ICHG 一致性待现场确认
-  - 拷机运行中其他产测页文字消失 / 界面被抢(§二十)
-  - 拷机运行后其它页只显示几个字、拷机页正常、重启可恢复(§20.8)
-  - 拷机页内标题/寄存器 overlay/拷机记录也字不全(§20.8)
+  - V9.0:拷机记录充放成对显示(B10 已合入,待刷机验证)
+  - V9.0:产测页 UI 字不全 / 电池充电下一步(B11/B12 已合入,待刷机验证)
+  - 左滑退出后不自动抢全屏 slice(§20.4 P1 ⬜)
+  - 合入主分支 ⬜
 ---
 
 # 充放电拷机(cc_cycle)方案汇总
 
+> **方案文档版本:V9.0**(2026-06-06)|持久化 **cc_cycle.bin 仍为 v7**(`CC_CYCLE_VERSION=7`,与文档版本无关)  
 > 关联文档:[充电电量显示分析.plan.md](./充电电量显示分析.plan.md)  
-> 关联文档:[拷机放电至关机循环.plan.md](./拷机放电至关机循环.plan.md)
+> 关联文档:[拷机放电至关机循环.plan.md](./拷机放电至关机循环.plan.md)  
+> 关联文档:[拷机菜单位置与UI优化.plan.md](./拷机菜单位置与UI优化.plan.md)
 
 ### 更新记录
 
@@ -29,6 +33,11 @@ open_issues:
 | 2026-06 末 | **充电阶段 ICHG 重配** | 放电→充电时 `battery_request_charge_reconfig()`,避免 0.5C 残留 |
 | 2026-06 末 | **拷机开关隔离** | `test_enabled` 唯一主门控;`cc_cycle_is_active_hw()`;bin 消毒 |
 | 2026-06 末 | **UI 干扰分析** | 产测禁充弹窗;1s RootView 全刷;常亮/GPS 后台占用(§二十) |
+| 2026-06-02 | **§20.9 UI 字不全第一轮** | 局部 Invalidate;overlay 停止 1s 自动刷 |
+| **2026-06-06 V9.0** | **拷机记录充放成对** | `g_phase_restore` 补写事件;`charge_log` 合并;历史解析增强 |
+| **2026-06-06 V9.0** | **不良扩展** | LOW_ICHG(橙)/ HW_NO_CHG(紫) |
+| **2026-06-06 V9.0** | **控制页 UI** | 纵向滚动;overlay「返回拷机页」;IC/记录 toggle |
+| **2026-06-06 V9.0** | **§20.10 产测 UI 加固** | 定时器门控;主菜单/电池充电 Refresh;下一步布局 |
 
 ---
 
@@ -67,7 +76,7 @@ flowchart LR
 
 掉线(拔充电线)→ 暂停循环、恢复默认充电 IC、退出拷机 UI。
 
-**充电阶段产测监控**(拷机仍继续):SOC 反降→红;充电超 2h 未满→黄;SOC 卡 80%→蓝;左滑退出后可查 IC 寄存器快照。
+**充电阶段产测监控**(拷机仍继续):SOC 反降→红;充电超 2h 未满→黄;SOC 卡 80%→蓝;ICHG 持续过低→橙;HW 未充→紫;左滑退出后可查 IC 寄存器快照。
 
 ---
 
@@ -77,7 +86,11 @@ flowchart LR
 |----|------|
 | 分支 | `feature/cc-cycle-burn-test` |
 | 主提交 | `fecd7a21d` — 添加产测充放电拷机功能(FD272/HP4570) |
+| 后续提交 | `b0996bf56` — 增加充放电拷机模式功能与设计 |
+| **V9.0 工作区** | 未提交补丁(约 +471/-73 行),待刷机验证后入库 |
+| V9.0 涉及文件 | `cc_cycle_service.c`;`testCcCycle.cpp/h`;`testPresenter.cpp`;`testView.cpp/h`;`testchanging.cpp/h` |
 | 平台 | **FD272 + ENABLE_HP4570**(产测 UI 与 cc_cycle 均 `#ifdef ENABLE_HP4570`) |
+| 持久化 | **cc_cycle.bin v7**(V9.0 能力在 v7 结构之上,无 v8/v9 迁移) |
 | 合入主分支 | ⬜ 待确认 |
 
 ---
@@ -130,7 +143,8 @@ flowchart LR
 | 事件日志 `user/userdata/cc_cycle/event_log.txt` + 串口 `[cc_cycle]` | 同上 | ✅ |
 | 充电轮次日志 `user/userdata/cc_cycle/charge_log.txt` | 同上 | ✅ |
 | 阶段时长 `cc_cycle_format_duration_line()`(上次/本次 HH:MM:SS) | 同上 | ✅ |
-| 充电不良检测 `cc_apply_charge_defect()`(红/黄/蓝,tick 1s) | 同上 | ✅ |
+| 充电不良检测 `cc_apply_charge_defect()`(红/黄/蓝/低电流/HW未充,tick 1s) | 同上 | ✅ |
+| **拷机记录历史解析 V9.0**(`cc_hist_*`:event_log + charge_log 合并、孤立事件还原、恢复路径补写) | 同上 | ✅ 待验证 |
 | 放电自然掉电 + 5% `pending_charge`(F1) | 同上 | ✅ |
 | 关机保留续跑标志 `cc_release_hw_only_locked`(F2) | 同上 | ✅ |
 | 1s 周期任务 `cc_cycle_svc` | 同上 | ✅ |
@@ -146,7 +160,9 @@ flowchart LR
 | 模块 | 文件 | 状态 |
 |------|------|------|
 | 产测入口「充放电拷机」 | `testView.cpp` | ✅ |
-| 控制页(开关 / 开始·停止 / 充电 IC 状态 / 1s 刷新) | `testCcCycle.cpp` | ✅ |
+| 控制页(开关 / 开始·停止 / 充电 IC 状态 / 拷机记录 / 1s 刷新) | `testCcCycle.cpp` | ✅ |
+| 控制页 **纵向滚动** + overlay「返回拷机页」 | `testCcCycle.cpp` | ✅ V9.0 |
+| 产测子页 **Refresh 加固**(主菜单 / 电池充电) | `testView.cpp`、`testchanging.cpp` | ✅ V9.0 待验证 |
 | 全屏拷机页(多行信息 + 不良底色 + 铃声) | `ccCycleBurnView.cpp` | ✅ |
 | 左滑返回控制页(延迟切 slice,停铃,防崩溃) | `ccCycleBurnPresenter.cpp` | ✅ |
 | 拷机 SOC 统一源 `cc_cycle_get_soc()` | 两页 UI + service | ✅ |
@@ -165,13 +181,13 @@ flowchart LR
 | | `statusLabel_` | 阶段详情 / HiZ / HW:充!(`cc_cycle_format_status_line`) | 1s |
 | 底 | `footerLabel_` | 「左滑返回」 | 静态 |
 
-阶段底色:充电浅绿 `#C8E6C9`、放电白屏、空闲灰;**不良覆盖**:红 `#FFCDD2`、黄 `#FFF9C4`、蓝 `#B3E5FC`。
+é\98¶æ®µåº\95è\89²ï¼\9aå\85\85ç\94µæµ\85绿 `#C8E6C9`ã\80\81æ\94¾ç\94µç\99½å±\8fã\80\81空é\97²ç\81°ï¼\9b**ä¸\8dè\89¯è¦\86ç\9b\96**ï¼\9a红 `#FFCDD2`ã\80\81é»\84 `#FFF9C4`ã\80\81è\93\9d `#B3E5FC`ã\80\81ä½\8eç\94µæµ\81æ©\99 `#FFE0B2`ã\80\81HWæ\9cªå\85\85ç´« `#E1BEE7`ã\80\82
 
 **汇总区**(§十六):标题「充放电拷机汇总」、当前时间(含秒)、上次/本次充放电时长(蓝/红 Label)。
 
-#### 控制页布局(`testCcCycle`)
+#### 控制页布局(`testCcCycle`)— V9.0
 
-466×466 圆形屏:内容区宽 `w-86`(380px)、水平居中 `x=(w-contentW)/2`,与产测主列表一致。
+466×466 圆形屏:内容区宽 `w-86`(380px)、水平居中 `x=(w-contentW)/2`。**主列表可纵向滚动**(`SetVerticalScrollState(true)` + 底部 blank 80px),底部按钮不再被圆屏裁切。
 
 | 行 | 控件 | Y | 内容 |
 |----|------|---|------|
@@ -179,17 +195,17 @@ flowchart LR
 | 阶段卡 | `phaseLabel_` | 78 | 「阶段: 充电中/放电中/空闲」+ 同色块 |
 | SOC | `socLabel_` | 122 | 黄色 40pt `xx%` |
 | 指标 | `metricsLabel_` | 182 | `V:xx.xxV  T:xx.xC` |
-| 时刻 | `timeLabel_` | 218 | 同 `cc_cycle_format_time_line` |
-| æ\97¶é\95¿ | `durationLabel_` | 250 | å\90\8c `cc_cycle_format_duration_line`ï¼\88**æ\9c¬æ¬¡ 1s å®\9eæ\97¶**ï¼\89 |
-| 状态 | `statusLabel_` | 286 | 状态行 + 不良时红色高亮(可多行) |
-| æ\8f\90示 | `hintLabel_` | 314 | é»\98认æ\8f\92线æ\8f\90示ï¼\9bæ\9c\89ä¸\8dè\89¯æ\97¶å¼\95导ç\82¹ IC ç\8a¶æ\80\81 |
-| 按钮1 | `labelButton_cc_sw_` | 360 | **开关:开/关**,380×72 |
-| 按钮2 | `labelButton_cc_start_charge_` / `start_discharge` | 440 | **首轮充电/首轮放电**(运行中变「停止拷机」) |
-| 按钮3 | `labelButton_cc_ic_` | 520 | **充电 IC 状态** |
-| 按钮4 | `labelButton_cc_history_` | — | **拷机记录**(event_log overlay) |
-| 按钮5 | `labelButton_cc_back_burn_` | — | **回到拷机界面**(仅 `cc_cycle_is_running()` 时显示) |
-
-按钮由本地 `CreateCcCycleButton()` 创建。1s 定时器 `RefreshCcCycleButtons` 刷新文案、**上次/本次时长**与状态行。IC 子面板:`icStatusScrollView_` + 「返回拷机页」「刷新实时」。
+| 状态 | `statusLabel_` | 218 | 状态行 + 不良/时长(可多行,红字高亮) |
+| æ\8f\90示 | `hintLabel_` | 278 | é»\98认æ\8f\92线æ\8f\90示ï¼\9bæ\9c\89ä¸\8dè\89¯æ\97¶å¼\95导ç\82¹ IC ç\8a¶æ\80\81 |
+| 按钮1 | `labelButton_cc_sw_` | 326 | **开关:开/关**,380×72 |
+| æ\8c\89é\92®2 | `labelButton_cc_start_charge_` | 406 | **é¦\96è½®å\85\85ç\94µ**ï¼\88è¿\90è¡\8c中å\8f\98ã\80\8cå\81\9cæ­¢æ\8b·æ\9cºã\80\8dï¼\89 |
+| 按钮3 | `labelButton_cc_start_discharge_` | 486 | **首轮放电**(运行中变「停止拷机」) |
+| 按钮4 | `labelButton_cc_ic_` | 566 | **充电 IC 状态**(toggle overlay) |
+| 按钮5 | `labelButton_cc_history_` | 646 | **拷机记录**(toggle overlay) |
+| 按钮6 | `labelButton_cc_back_burn_` | 726 | **回到拷机界面**(仅 `cc_cycle_is_running()` 时显示) |
+| overlay | `labelButton_overlay_back_` | h-88 | **返回拷机页**(overlay 打开时显示) |
+
+按钮由本地 `CreateCcCycleButton()` 创建。1s 定时器 `RefreshCcCycleButtons`:**仅拷机控制页可见时运行**(V9.0 门控);overlay 打开时不 1s 刷 IC/记录。IC/记录子面板:`icStatusScrollView_` / `historyScrollView_` + 左滑关闭 + 「返回拷机页」。
 
 **铃声**:不良首次触发 `AlarmPlayerStart`;左滑退出全屏 `AlarmPlayerStop`(拷机后台不停止)。
 
@@ -222,17 +238,19 @@ flowchart LR
 | `brandy/config.py` 组件列表 | ✅ |
 | `VIEW_CC_CYCLE_BURN` 注册 | ✅ |
 
-### 4.5 充电不良检测(2026-06)✅
+### 4.5 å\85\85ç\94µä¸\8dè\89¯æ£\80æµ\8bï¼\882026-06ï¼\8cV9.0 æ\89©å±\95ï¼\89â\9c\85
 
-仅在 **充电阶段**、拷机 **继续运行**(不自动暂停)。电流不直接采样,用 SOC 变化与时长间接判断。
+仅在 **充电阶段**、拷机 **继续运行**(不自动暂停)。除 SOC/时长间接判断外,V9.0 增加 **HP4570 ICHG 直读** 判定。
 
 | 等级 | UI 底色 | 判定(默认) | 含义 |
 |------|---------|--------------|------|
 | **红** | 浅红 `#FFCDD2` | 充电 SOC 连续 3 次下降,或 5min 净降 ≥1% | 等效无充电电流、电量反降 |
 | **黄** | 浅黄 `#FFF9C4` | 本次充电 >2h(7200s)且 SOC < 99% | 充电过慢 |
 | **蓝** | 浅蓝 `#B3E5FC` | SOC 78–82% 停留 ≥30min 无上升 | 高电量卡在 ~80% |
+| **低电流** | 浅橙 `#FFE0B2` | ICHG ≤20mA 持续 ≥60s(`CC_CYCLE_DEFECT_LOW_ICHG_*`) | HP4570 配置电流过低 |
+| **HW未充** | 浅紫 `#E1BEE7` | 拷机充电阶段 HW 未充且 SOC 长时间不涨 | 硬件未进入充电 |
 
-**优先级**:红 > 黄 > 蓝。首次进入某等级:铃声 + 写 `cc_cycle_defect.bin` + event 日志(`defect_red_soc_drop` / `defect_yellow_slow_charge` / `defect_blue_soc_stuck80`)。
+**优先级**:红 > 黄 > 低电流 > HW未充 > 蓝。首次进入某等级:铃声 + 写 `cc_cycle_defect.bin` + event 日志(`defect_red_soc_drop` / `defect_yellow_slow_charge` / `defect_blue_soc_stuck80` / `defect_low_ichg` / `defect_hw_no_chg`)。
 
 ### 4.6 阶段计时(上次 / 本次)✅
 
@@ -257,7 +275,8 @@ flowchart LR
 | 存储 magic / version | `0x43434643` ("CCFC") / **v7** | `cc_cycle_service.c` |
 | 充电慢判定 | 7200s(2h) | `CC_CYCLE_CHARGE_SLOW_SEC` |
 | 80% 卡住 | SOC 78–82%,1800s(30min) | `CC_CYCLE_DEFECT_*` |
-| 拷机 UI 刷新 | 1s(含本次计时) | `ccCycleBurnPresenter` + `testCcCycle` |
+| 低电流不良 | ICHG ≤20mA,持续 60s | `CC_CYCLE_DEFECT_LOW_ICHG_*` |
+| 拷机 UI 刷新 | 1s(含本次计时);**仅拷机控制页可见时** | `testCcCycle` StatusTimer(V9.0 门控) |
 
 ---
 
@@ -410,6 +429,9 @@ Service 侧另提供 `cc_cycle_format_detail_line()`(SOC+V+T 单行),**当
 | B9 | 汇总 UI + 回到拷机界面 + 当前时间含秒 | ✅ 见 §十六、§十七 |
 | B4 | 开机时序:`cc_cycle_on_boot` 与 battery init 顺序调整 | ✅ early_boot_sync 在 percent_init 前 |
 | B5 | 左滑退出延迟切 slice(防 GuiMainThread 崩溃) | ✅ 待现场确认 |
+| **B10** | **拷机记录充放成对**(恢复补写 + charge_log 合并 + 历史解析) | ✅ V9.0 待刷机验证 |
+| **B11** | **产测页 UI Refresh**(主菜单 / 拷机 overlay 关闭 / 定时器门控) | ✅ V9.0 待刷机验证 |
+| **B12** | **电池充电「下一步」**(跳过拷机进喇叭 + 布局防遮挡) | ✅ V9.0 待刷机验证 |
 
 ### P0 — 功能验证(拷机专项)
 
@@ -428,6 +450,9 @@ Service 侧另提供 `cc_cycle_format_detail_line()`(SOC+V+T 单行),**当
 | 12 | 拷机开关关:表盘/正常充电无影响 | | ⬜ |
 | 13 | 放电→充电 ICHG 与插拔一致 | | ⬜ |
 | 14 | 「回到拷机界面」+ 汇总 UI(含秒) | | ⬜ |
+| **15** | **拷机记录 overlay:充电/放电成对**(含 reboot 后续跑) | | ⬜ V9.0 |
+| **16** | **拷机运行中切其它产测页,文字完整**(≥10min) | | ⬜ V9.0 |
+| **17** | **电池充电页「下一步」可见且进喇叭** | | ⬜ V9.0 |
 
 ### P1 — 已知风险 / 待改进
 
@@ -479,6 +504,7 @@ Service 侧另提供 `cc_cycle_format_detail_line()`(SOC+V+T 单行),**当
 | 全屏 UI | [`ccCycleBurnView.cpp`](../code/sdk/zhihui/UI/producttest/src/ccCycleBurnView.cpp) / [`.h`](../code/sdk/zhihui/UI/producttest/include/ccCycleBurnView.h) |
 | Presenter | [`code/sdk/zhihui/UI/producttest/src/ccCycleBurnPresenter.cpp`](../code/sdk/zhihui/UI/producttest/src/ccCycleBurnPresenter.cpp) |
 | 事件 | [`code/sdk/zhihui/UI/producttest/src/testPresenter.cpp`](../code/sdk/zhihui/UI/producttest/src/testPresenter.cpp) |
+| 产测导航 Refresh | [`testView.cpp`](../code/sdk/zhihui/UI/producttest/src/testView.cpp) / [`testchanging.cpp`](../code/sdk/zhihui/UI/producttest/src/testchanging.cpp)(V9.0) |
 | 开机 | [`code/sdk/zhihui/utils/custom_utils.c`](../code/sdk/zhihui/utils/custom_utils.c) `cc_cycle_on_boot` |
 | SOC 联动 | [`code/sdk/zhihui/services/battery/service/battery_driver.cpp`](../code/sdk/zhihui/services/battery/service/battery_driver.cpp) |
 | 充电管理 | [`code/sdk/zhihui/services/battery/service/battery_service.cpp`](../code/sdk/zhihui/services/battery/service/battery_service.cpp) |
@@ -496,7 +522,8 @@ Service 侧另提供 `cc_cycle_format_detail_line()`(SOC+V+T 单行),**当
 | 放电自然掉电 + 关机续跑(F1/F2) | 2026-06-02 | ✅ | 见 [拷机放电至关机循环.plan.md](./拷机放电至关机循环.plan.md) |
 | 拷机开关隔离 + ICHG 重配 | 2026-06-02 | ✅ | §十三、§十四 |
 | 汇总 UI + 回到拷机界面 | 2026-06-02 | ✅ | §十六、§十七 |
-| 拷机 P0 验证(含隔离/ICHG) | | ⬜ | §八 1~14 |
+| **V9.0 拷机记录 + UI 加固** | 2026-06-06 | 🟡 | §二十一;工作区未提交 |
+| 拷机 P0 验证(含隔离/ICHG) | | ⬜ | §八 1~17 |
 | SOC 低电跳变修复 | | ⬜ | 见充电电量 plan |
 | 合入主分支 | | ⬜ | |
 | 长跑拷机 ≥24h | | ⬜ | |
@@ -639,9 +666,10 @@ API:`cc_cycle_get_burn_summary()`;数据来自 bin `last_*_duration_sec` + 
 | `cc_cycle_sw` | 开关:开/关 | `test_enabled` |
 | `cc_cycle_start_charge` | 首轮充电 / 停止拷机 | `CC_PHASE_CHARGING` |
 | `cc_cycle_start_discharge` | 首轮放电 / 停止拷机 | `CC_PHASE_DISCHARGE` |
-| `cc_cycle_ic` | 充电 IC 状态 | 快照/实时 |
-| `cc_cycle_history` | 拷机记录 | event_log overlay |
+| `cc_cycle_ic` | 充电 IC 状态 | 快照/实时;toggle overlay |
+| `cc_cycle_history` | 拷机记录 | event_log + charge_log 合并展示 |
 | `cc_cycle_back_burn` | 回到拷机界面 | 仅 `is_running` 时显示;`cc_cycle_reopen_burn_ui()` |
+| `cc_cycle_overlay_back` | 返回拷机页 | overlay(IC/记录)打开时显示;关闭并全量 Refresh |
 
 左滑退出全屏后拷机**不停止**;点「回到拷机界面」重新进入全屏。
 
@@ -662,12 +690,13 @@ API:`cc_cycle_get_burn_summary()`;数据来自 bin `last_*_duration_sec` + 
 
 ## 十九、下一步建议
 
-1. **拷机开关隔离**:开关关时表盘/正常充电无影响;开关开+未停时重启/插线续跑;停止后 ICHG/SOC 恢复
-2. **充电电流**:放电→充电阶段切换后 ICHG 与插拔一致(同温度下)
-3. **UI 干扰修复**(§二十):局部 Invalidate、tick 内 screen_hold 去重、拷机运行时不强抢 slice
-4. **现场确认** P0 验证(不良红/黄/蓝、铃声、IC 快照、汇总 UI 含秒、回到拷机界面)
-5. **SOC**:拷机 9% vs 表盘 23%(B1~B4);V 与 % 短期偏差(§十五)
-6. 验证通过后 **合入主分支**
+1. **刷 V9.0 固件验证**:拷机记录充放成对(§八 #15);产测 UI 字不全 / 电池充电下一步(§八 #16/#17)
+2. **拷机开关隔离**:开关关时表盘/正常充电无影响;开关开+未停时重启/插线续跑;停止后 ICHG/SOC 恢复
+3. **充电电流**:放电→充电阶段切换后 ICHG 与插拔一致(同温度下)
+4. **UI 干扰剩余项**(§20.4 P1):左滑退出后不自动抢全屏 slice
+5. **现场确认** P0 验证(不良红/黄/蓝/低电流/HW未充、铃声、IC 快照、汇总 UI)
+6. **SOC**:拷机 9% vs 表盘 23%(B1~B4);V 与 % 短期偏差(§十五)
+7. V9.0 验证通过后 **合入主分支**并提交工作区补丁
 
 ---
 
@@ -740,6 +769,10 @@ flowchart TD
 | P0 | `RefreshStatusLabel` 补 `title_->SetText` | `testCcCycle.cpp` | ✅ |
 | P0 | `cc_set_discharge_screen_hold` 已 hold 早退 | `cc_cycle_service.c` | ✅ |
 | P0 | 删除 `testPresenter::OnResume` 无条件 ResumeStatusTimer | `testPresenter.cpp` | ✅ |
+| P0 | 定时器仅拷机控制页 **IsVisible** 时运行 | `testCcCycle.cpp` | ✅ V9.0 |
+| P0 | 离开拷机页 PauseStatusTimer / 回主菜单 RefreshMainMenuUi | `testView.cpp` | ✅ V9.0 |
+| P0 | 电池充电 RefreshChargingUi + EnsureChargingNextButton | `testchanging.cpp` | ✅ V9.0 |
+| P0 | overlay 关闭走 RefreshCcCycleButtons 全量刷新 | `testCcCycle.cpp` | ✅ V9.0 |
 | P1 | 左滑退出后不自动抢全屏 slice | `cc_cycle_service.c` | ⬜ |
 
 ### 20.5 现场快速区分
@@ -991,3 +1024,86 @@ if (on) {
 1. 拷机运行 10min → 标题/寄存器/拷机记录/其它产测页文字应完整  
 2. 打开「拷机记录」停留 1min → 多行文字不应逐字消失  
 3. 切 LCD 再回来 → 无需重启即可正常显示
+
+### 20.10 UI 字不全优化 — V9.0 第二轮加固(2026-06-06)
+
+在 §20.9 基础上,针对 **拷机后台仍干扰其它产测页**、**overlay 关闭后标题缺字**、**电池充电页控件消失** 追加:
+
+| 改法 | 文件 | 说明 |
+|------|------|------|
+| `StatusTimerCallback` 检查 `CCCYCLE` 页 **IsVisible** | `testCcCycle.cpp` | 拷机页隐藏时不 1s 刷新,避免干扰兄弟 ScrollView |
+| `ChangeView` 离开拷机页 → `PauseStatusTimer` | `testView.cpp` | 拷机对象仍在内存时停定时器 |
+| 回主菜单 → `RefreshMainMenuUi()` | `testView.cpp` | 重设菜单项文字 + Invalidate |
+| 进电池充电 → `EnsureChargingNextButton` + `RefreshChargingUi` | `testchanging.cpp` | 电量 label 动态高度后重排「下一步」 |
+| 关闭 IC/记录 overlay → `RefreshCcCycleButtons()` | `testCcCycle.cpp` | 非仅 `RefreshStatusLabel`;各 label 单独 Invalidate |
+| 「下一步」策略 | `testView.cpp`、`testPresenter.cpp` | 拷机页无下一步;**电池充电有下一步→喇叭**(点击跳过 CCCYCLE) |
+
+**仍待验证**:拷机运行 ≥10min 后切 LCD/GPS/电池充电,文字是否仍完整;左滑退出后是否仍自动抢全屏(§20.4 P1 ⬜)。
+
+---
+
+## 二十一、V9.0 变更明细(2026-06-06)
+
+> **文档版本 V9.0**|**cc_cycle.bin 仍为 v7**(无存储结构变更)
+
+```mermaid
+flowchart TB
+  subgraph v9_service [cc_cycle_service]
+    restore["g_phase_restore 补写 charge_start/discharge_end"]
+    hist["cc_hist 解析 + charge_log 合并"]
+    defect["LOW_ICHG / HW_NO_CHG"]
+  end
+  subgraph v9_ui [产测 UI]
+    overlay["overlay 返回 + 关闭全量 Refresh"]
+    timer["1s 定时器仅拷机页可见"]
+    charg["电池充电 Refresh + 下一步布局"]
+    menu["主菜单 RefreshMainMenuUi"]
+  end
+  v9_service --> histUI["拷机记录 UI 充放成对"]
+  v9_ui --> histUI
+```
+
+### 21.1 拷机记录完整化(B10)
+
+**现象**:拷机记录 overlay 多条「放电」、无「充电」;或仅有孤立 `charge_end`。
+
+| 问题 | 根因 | V9.0 修复 |
+|------|------|-----------|
+| 记录只有放电 | 重启恢复 `g_phase_restore=1` 不写 `charge_start` | `cc_enter_charging_phase` 恢复路径补写 `charge_start`、`discharge_end` |
+| 孤立 charge_end | 解析器丢弃无配对事件 | `cc_hist_emit_missing_charge_rounds` + 从 `charge_log.txt` 还原 |
+| UI 与文件不一致 | legacy `cc_charge_log.txt` 未并入 | 加载/合并 `CC_CYCLE_LEGACY_CHARGE_LOG_PATH` |
+
+**验证**(§八 #15):完成 1 轮充放后打开「拷机记录」,应见:
+
+```
+充电:MM-DD HH:MM-HH:MM
+放电:MM-DD HH:MM-HH:MM
+```
+
+### 21.2 充电不良扩展
+
+见 **§4.5**。新增 `CC_DEFECT_LOW_ICHG`、`CC_DEFECT_HW_NO_CHG` 及对应 UI 色块(控制页 `UpdatePhaseCard`、全屏页阶段底色)。
+
+### 21.3 控制页 / overlay UI
+
+见 **§4.2 控制页布局 — V9.0**、**§十七**(`cc_cycle_overlay_back`)。
+
+### 21.4 产测导航与「下一步」
+
+| 规则 | 实现 |
+|------|------|
+| 拷机 **仅主菜单** 进入 | `initView_ext` 将 CCCYCLE 渲染在菜单最底 |
+| 拷机页 **无**「下一步」 | `ShouldShowChainNextButton(CCCYCLE)==false` |
+| **电池充电** 有「下一步」→ **喇叭** | `testPresenter` 点击时 `nextIdx++` 跳过 CCCYCLE;`EnsureChargingNextButton` 置于电量文字下方防遮挡 |
+
+详见 [拷机菜单位置与UI优化.plan.md](./拷机菜单位置与UI优化.plan.md)。
+
+### 21.5 涉及文件(工作区,待提交)
+
+| 文件 | V9.0 改动摘要 |
+|------|---------------|
+| `cc_cycle_service.c` | 历史解析、charge_log 合并、恢复事件补写、LOW_ICHG/HW_NO_CHG |
+| `testCcCycle.cpp/h` | 滚动、overlay 返回、定时器门控、Refresh 加固 |
+| `testPresenter.cpp` | overlay_back 点击;pending 跳转 |
+| `testView.cpp/h` | RefreshMainMenuUi;ChangeView 门控;ShouldShowChainNextButton |
+| `testchanging.cpp/h` | RefreshChargingUi;EnsureChargingNextButton;Layout |
index a7898f1..49d18e7 100644 (file)
@@ -42,7 +42,7 @@ isProject: false
 | **温控与保护分池** | T1~T3 只做温控,不参与 VBUS16V/反接/短接 |
 | **高损项走 R 组** | A 级项损坏概率高,必须备机 3 台,测完评估是否转 FA |
 | **Excel 列对齐** | 保护/温控结论填 **1#/2#/3#**,与 T1~T3 或 P1~P3 一一对应 |
-| **与 cc_cycle 互补** | 本专项偏边界/安全/参数;长周期可靠性见 [充放电拷机汇总.plan.md](./充放电拷机汇总.plan.md) |
+| **与 cc_cycle 互补** | 本专项偏边界/安全/参数;长周期可靠性见 [充放电拷机汇总.plan.md](./充放电拷机汇总.plan.md)(**V9.0**:拷机记录、不良等级 LOW_ICHG/HW_NO_CHG) |
 
 ---
 
index e9d74bf..86b2582 100644 (file)
@@ -12,15 +12,16 @@ overview: 拷机移产测菜单最底部;仅主菜单点击进入,禁止「
 | 方式 | 是否允许 |
 |------|----------|
 | 主菜单最底部点击「充放电拷机」 | 允许 |
-| 任意常规项「下一项」链式进入 | **禁止**(误导产测) |
+| 任意常规项「下一项」链式 **进入拷机** | **禁止** |
+| 电池充电「下一项」进入 **喇叭**(跳过拷机) | **允许** |
 
 ## 实现要点
 
 1. `Test_ViewMapper_t`:CCCYCLE 移数组末尾,主菜单仍显示
-2. `testPresenter.cpp`:`i+1` 为 CCCYCLE 时不跳转
-3. 倒数第二项测试页:隐藏「下一项」(下一项是拷机时)
-4. `testCcCycle.cpp`:不显示「下一项」
-5. 控制页 + 全屏页 UI 重构(见完整 plan)
+2. `testPresenter.cpp`:链式「下一项」点击时若下一项为 CCCYCLE 则 **跳过**(`nextIdx++`),**不进入拷机**
+3. **拷机页**(CCCYCLE):不显示「下一步」
+4. **电池充电页**(CHARG):**显示**「下一步」,进入喇叭(跳过拷机)
+5. 控制页 + 全屏页 UI 重构(见 [充放电拷机汇总.plan.md](./充放电拷机汇总.plan.md) §4.2 V9.0)
 
 ## 验证