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);
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};
// 根据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
virtual ~testCHANINGView();
static testCHANINGView *GetInstance(void);
void ChangeViewcolor(bool);
+ void RefreshChargingUi(void);
+ void EnsureChargingNextButton(uint8_t id);
UIScrollView *testCHANINGInitIView(uint8_t);
void ClearAll(void);
UILabelButtonExt* labelButton_next = nullptr;
+ void LayoutChargingNextButton(void);
+
// 添加成功失败按钮
UILabelButton *oktestBtn{nullptr};
UILabelButton *failtestBtn{nullptr};
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);
}
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;
(void)snprintf(detail, sizeof(detail), "V:%.2fV T:%.1fC",
(double)voltage, (double)temp);
metricsLabel_->SetText(detail);
+ metricsLabel_->Invalidate();
}
if (statusLabel_ != nullptr) {
char buf[128];
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();
} else {
hintLabel_->SetText("需插线;选首轮充电或放电后自动循环");
}
+ hintLabel_->Invalidate();
+ }
+ if (phaseLabel_ != nullptr) {
+ phaseLabel_->Invalidate();
}
}
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);
}
}
+ 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) {
RefreshHistoryPanel();
InvalidateLocalView();
} else if (!visible && !icPanelVisible_) {
- RefreshStatusLabel();
- InvalidateLocalView();
+ RefreshCcCycleButtons();
}
}
ShowIcStatusPanel();
InvalidateLocalView();
} else if (!visible && !historyPanelVisible_) {
- RefreshStatusLabel();
- InvalidateLocalView();
+ RefreshCcCycleButtons();
}
}
labelButton_cc_ic_ = nullptr;
labelButton_cc_history_ = nullptr;
labelButton_cc_back_burn_ = nullptr;
+ labelButton_overlay_back_ = nullptr;
icStatusScrollView_ = nullptr;
historyScrollView_ = nullptr;
icStatusLabel_ = nullptr;
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) {
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;
}
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;
}
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]
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()空指针崩溃)
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)
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)
// 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)
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地址过期问题
// 获取充电测试界面的按钮推荐位置
// // WEARABLE_LOGE(WEARABLE_LOG_MODULE_MAIN_SERVICE,"CAT1 SERVICE MCU has got pm_id_cat1 vote\r\n");
// }
+ RefreshChargingUi();
return true;
}
}
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;
}
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();
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();
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);
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;
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) {
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");
}
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) {
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 {
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;
---
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)
### 更新记录
| 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;下一步布局 |
---
掉线(拔充电线)→ 暂停循环、恢复默认充电 IC、退出拷机 UI。
-**充电阶段产测监控**(拷机仍继续):SOC 反降→红;充电超 2h 未满→黄;SOC 卡 80%→蓝;左滑退出后可查 IC 寄存器快照。
+**充电阶段产测监控**(拷机仍继续):SOC 反降→红;充电超 2h 未满→黄;SOC 卡 80%→蓝;ICHG 持续过低→橙;HW 未充→紫;左滑退出后可查 IC 寄存器快照。
---
|----|------|
| 分支 | `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 迁移) |
| 合入主分支 | ⬜ 待确认 |
---
| 事件日志 `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` | 同上 | ✅ |
| 模块 | 文件 | 状态 |
|------|------|------|
| 产测入口「充放电拷机」 | `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 | ✅ |
| | `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 | 内容 |
|----|------|---|------|
| 阶段卡 | `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`(拷机后台不停止)。
| `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 阶段计时(上次 / 本次)✅
| 存储 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 门控) |
---
| 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 — 功能验证(拷机专项)
| 12 | 拷机开关关:表盘/正常充电无影响 | | ⬜ |
| 13 | 放电→充电 ICHG 与插拔一致 | | ⬜ |
| 14 | 「回到拷机界面」+ 汇总 UI(含秒) | | ⬜ |
+| **15** | **拷机记录 overlay:充电/放电成对**(含 reboot 后续跑) | | ⬜ V9.0 |
+| **16** | **拷机运行中切其它产测页,文字完整**(≥10min) | | ⬜ V9.0 |
+| **17** | **电池充电页「下一步」可见且进喇叭** | | ⬜ V9.0 |
### P1 — 已知风险 / 待改进
| 全屏 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) |
| 放电自然掉电 + 关机续跑(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 | | ⬜ | |
| `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 |
左滑退出全屏后拷机**不停止**;点「回到拷机界面」重新进入全屏。
## 十九、下一步建议
-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 验证通过后 **合入主分支**并提交工作区补丁
---
| 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 现场快速区分
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 |
| **温控与保护分池** | 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) |
---
| 方式 | 是否允许 |
|------|----------|
| 主菜单最底部点击「充放电拷机」 | 允许 |
-| 任意常规项「下一项」链式进入 | **禁止**(误导产测) |
+| 任意常规项「下一项」链式 **进入拷机** | **禁止** |
+| 电池充电「下一项」进入 **喇叭**(跳过拷机) | **允许** |
## 实现要点
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)
## 验证