declare -A PROJECT_VERSIONS=(
# 基础项目版本
["fd230"]="1.3.107"
- ["fd272"]="1.3.152"
+ ["fd272"]="1.3.154"
["user"]="1.3.107"
["fd230_user"]="1.3.107"
#define CC_CYCLE_BURN_VIEW_H
#include "View.h"
+#include "components/ui_scroll_view.h"
#include "components/ui_view_group.h"
#include "components/ui_label.h"
void OnStart() override;
void OnStop() override;
void RefreshStatus(void);
- void ApplyScreenAlwaysOn(void);
+ bool IsViewReady(void) const;
+ void DetachDragListener(void);
+ void AttachDragListener(void);
private:
void DestroyBurnWidgets(void);
bool CreateBurnView(void);
- UIViewGroup *burnGroup_{nullptr};
+ UIScrollView *burnScrollView_{nullptr};
+ UIViewGroup *burnContentGroup_{nullptr};
UILabel *phaseLabel_{nullptr};
+ UILabel *socLabel_{nullptr};
+ UILabel *metricsLabel_{nullptr};
+ UILabel *summaryTitleLabel_{nullptr};
+ UILabel *clockLabel_{nullptr};
+ UILabel *lastChargeLabel_{nullptr};
+ UILabel *curChargeLabel_{nullptr};
+ UILabel *lastDisLabel_{nullptr};
+ UILabel *curDisLabel_{nullptr};
+ UILabel *defectLabel_{nullptr};
UILabel *statusLabel_{nullptr};
+ UILabel *footerLabel_{nullptr};
bool viewInit_{false};
- void UpdatePhaseBackground(cc_cycle_phase_e phase);
+ void UpdatePhaseBackground(cc_cycle_phase_e phase, uint8_t defectType);
};
}
#define TEST_CC_CYCLE_VIEW_H
#include "components/ui_scroll_view.h"
+#include "components/ui_view_group.h"
#include "components/ui_label.h"
#include "components/ui_label_button_ext.h"
testCcCycleView();
virtual ~testCcCycleView();
static testCcCycleView *GetInstance(void);
+ bool IsUiReady(void) const;
- UIScrollView *testCcCycleInitIView(uint8_t id);
+ UIViewGroup *testCcCycleInitIView(uint8_t id);
void ClearAll(void);
void RefreshCcCycleButtons(void);
void RefreshStatusLabel(void);
+ void PauseStatusTimer(void);
+ void ResumeStatusTimer(void);
+ void ShowIcStatusPanel(void);
+ void SetIcPanelVisible(bool visible);
+ void SetHistoryPanelVisible(bool visible);
+ void RefreshHistoryPanel(void);
+ void CloseVisibleOverlay(void);
private:
static void StatusTimerCallback(void *arg);
+ void UpdatePhaseCard(void);
bool testCcCycleScrollViewInit(uint8_t id);
+ void ApplyOverlayChrome(bool overlayVisible);
+ void SetupOverlayScroll(UIScrollView *scroll);
+ void InvalidateLocalView(void);
+
+ UIViewGroup *rootGroup_{nullptr};
+ UIViewGroup *overlayGroup_{nullptr};
UIScrollView *testCcCycleScrollView_{nullptr};
UILabel *title_{nullptr};
+ UILabel *phaseLabel_{nullptr};
+ UILabel *socLabel_{nullptr};
+ UILabel *metricsLabel_{nullptr};
UILabel *statusLabel_{nullptr};
UILabel *hintLabel_{nullptr};
UILabelButtonExt *labelButton_cc_sw_{nullptr};
- UILabelButtonExt *labelButton_cc_enter_{nullptr};
- UILabelButtonExt *labelButton_next_{nullptr};
+ UILabelButtonExt *labelButton_cc_start_charge_{nullptr};
+ UILabelButtonExt *labelButton_cc_start_discharge_{nullptr};
+ UILabelButtonExt *labelButton_cc_ic_{nullptr};
+ UILabelButtonExt *labelButton_cc_history_{nullptr};
+ UILabelButtonExt *labelButton_cc_back_burn_{nullptr};
+ UIScrollView *icStatusScrollView_{nullptr};
+ UIScrollView *historyScrollView_{nullptr};
+ UILabel *icStatusLabel_{nullptr};
+ UILabel *historyLabel_{nullptr};
+ bool icPanelVisible_{false};
+ bool historyPanelVisible_{false};
};
}
\r
void OnStart() override;\r
void OnResume() override;\r
+ void OnPause() override;\r
\r
static testPresenter *GetInstance();\r
\r
*
* Copyright (c) 2024 by ${git_name_email}, All Rights Reserved.
*/
-#ifndef TEST_PRESENTER_H
-#define TEST_PRESENTER_H
+#ifndef TEST_PRESENTER_C_H
+#define TEST_PRESENTER_C_H
#ifdef __cplusplus
#if __cplusplus
void factorytest_enter_test_gps_view(void);
void factorytest_jump_to_subview(const char *item);
void factorytest_jump_to_subview_id(int16_t target);
+ void factorytest_set_pending_subview(int16_t target);/* 仅登记待跳转子页,等 VIEW_TEST slice OnResume 后再 ChangeView(拷机页退出等场景) */
void factorytest_set_gps_button_state(int on);
void factorytest_trigger_leaklight_ui(void);
void factorytest_set_bt_discovery_active(int active);
\r
// 根据viewId实时更新对应界面的按钮颜色\r
void UpdateButtonColorByViewId(const char* viewId);\r
+\r
+ /* 链式「下一项」:拷机仅主菜单进入,下一项为拷机或当前为拷机时不显示 */\r
+ static bool ShouldShowChainNextButton(int16_t viewId);\r
private:\r
void InitIView(void);\r
void initView_ext(int x, int y, int w, int h, int interval, const char *imagsrc);\r
#include "producttest/include/testPresenter.h"
#include "producttest/include/testView.h"
#include "producttest/include/testCcCycle.h"
+#include "producttest/include/testPresenterC.h"
#include "wearable_log.h"
#include "common/screen.h"
+#include "graphic_service.h"
#include "cmsis_os2.h"
#include <cstdlib>
#include <cstring>
#ifdef ENABLE_HP4570
#include "cc_cycle_service.h"
#endif
+#include "alarm/include/AlarmClockModel.h"
static osTimerId_t g_ccBurnRefreshTimer = nullptr;
+static bool g_ccBurnAlarmPlaying = false;
static osTimerId_t g_ccBurnShowUiTimer = nullptr;
static volatile uint8_t g_burnUiValid = 0;
+static volatile uint8_t g_hideBurnUiPending = 0;
-static void cc_burn_refresh_timer_cb(void *arg)
+// #region agent log
+static void dbg_c3c5cc_ui(const char *hypothesisId, const char *msg, int v1)
{
- (void)arg;
- if (!g_burnUiValid) {
+ PRINT("[DBG:c3c5cc] {\"sessionId\":\"c3c5cc\",\"hypothesisId\":\"%s\",\"location\":\"ccCycleBurnPresenter\","
+ "\"message\":\"%s\",\"data\":{\"v1\":%d}}\n", hypothesisId, msg, v1);
+}
+// #endregion
+
+static void cc_burn_stop_show_ui_timer(void)
+{
+ if (g_ccBurnShowUiTimer != nullptr) {
+ (void)osTimerStop(g_ccBurnShowUiTimer);
+ }
+}
+
+static void cc_burn_stop_alarm(void)
+{
+ if (g_ccBurnAlarmPlaying) {
+ OHOS::AlarmClockModel::GetInstance()->AlarmPlayerStop();
+ g_ccBurnAlarmPlaying = false;
+ }
+}
+
+static uint8_t cc_burn_ui_may_update(void)
+{
+ if (!g_burnUiValid || g_hideBurnUiPending) {
+ return 0;
+ }
+ if (OHOS::NativeAbility::GetInstance().GetCurSliceId() != VIEW_CC_CYCLE_BURN) {
+ return 0;
+ }
+ return 1;
+}
+
+static void cc_burn_alarm_start_on_graphic(void)
+{
+ if (!cc_burn_ui_may_update()) {
+ return;
+ }
+ if (g_ccBurnAlarmPlaying) {
+ return;
+ }
+ OHOS::AlarmClockModel *alarm = OHOS::AlarmClockModel::GetInstance();
+ if (alarm != nullptr) {
+ alarm->AlarmPlayerStart();
+ g_ccBurnAlarmPlaying = true;
+ }
+}
+
+static void cc_burn_check_alarm(void)
+{
+#ifdef ENABLE_HP4570
+ if (cc_cycle_take_defect_alarm() != 0) {
+ if (!g_ccBurnAlarmPlaying) {
+ ::GraphicService::GetInstance()->PostGraphicEvent(cc_burn_alarm_start_on_graphic);
+ }
+ }
+#else
+ (void)0;
+#endif
+}
+
+static void cc_burn_refresh_on_graphic(void)
+{
+ if (!cc_burn_ui_may_update()) {
return;
}
#ifdef ENABLE_HP4570
if (!cc_cycle_is_running()) {
return;
}
+ cc_burn_check_alarm();
#endif
OHOS::ccCycleBurnView *v = OHOS::ccCycleBurnView::GetInstance();
- if (v != nullptr) {
- v->RefreshStatus();
+ if (v == nullptr || !v->IsViewReady()) {
+ return;
}
+ v->RefreshStatus();
}
-static void cc_burn_show_ui_timer_cb(void *arg)
+static void cc_burn_refresh_timer_cb(void *arg)
{
(void)arg;
+ ::GraphicService::GetInstance()->PostGraphicEvent(cc_burn_refresh_on_graphic);
+}
+
+static void cc_burn_show_ui_on_graphic(void)
+{
+ if (g_hideBurnUiPending) {
+ return;
+ }
#ifdef ENABLE_HP4570
if (!cc_cycle_is_running()) {
return;
}
#endif
+ if (OHOS::NativeAbility::GetInstance().GetCurSliceId() == VIEW_CC_CYCLE_BURN) {
+ return;
+ }
OHOS::NativeAbility::GetInstance().ChangeSlice(VIEW_CC_CYCLE_BURN);
}
+static void cc_burn_show_ui_timer_cb(void *arg)
+{
+ (void)arg;
+ ::GraphicService::GetInstance()->PostGraphicEvent(cc_burn_show_ui_on_graphic);
+}
+
+static void cc_burn_hide_ui_deferred(void)
+{
+ // #region agent log
+ dbg_c3c5cc_ui("F", "hide_deferred_run", 0);
+ // #endregion
+ if (!g_hideBurnUiPending) {
+ return;
+ }
+ cc_burn_stop_alarm();
+ if (OHOS::NativeAbility::GetInstance().GetCurSliceId() == VIEW_TEST) {
+ g_hideBurnUiPending = 0;
+ return;
+ }
+#ifdef ENABLE_HP4570
+ factorytest_set_pending_subview(OHOS::CCCYCLE);
+#else
+ factorytest_set_pending_subview(OHOS::TESTVIEW);
+#endif
+ /* 仅切 slice;子页跳转由 testPresenter::OnResume + g_pendingJumpSubView 完成 */
+ OHOS::NativeAbility::GetInstance().ChangeSlice(VIEW_TEST);
+ g_hideBurnUiPending = 0;
+}
+
namespace OHOS
{
REGIST_SLICE(VIEW_CC_CYCLE_BURN, ccCycleBurnView, ccCycleBurnPresenter);
ccCycleBurnPresenter::~ccCycleBurnPresenter()
{
+ cc_burn_stop_show_ui_timer();
+ if (g_ccBurnShowUiTimer != nullptr) {
+ (void)osTimerDelete(g_ccBurnShowUiTimer);
+ g_ccBurnShowUiTimer = nullptr;
+ }
if (g_ccBurnRefreshTimer != nullptr) {
(void)osTimerStop(g_ccBurnRefreshTimer);
(void)osTimerDelete(g_ccBurnRefreshTimer);
void ccCycleBurnPresenter::OnStart()
{
Presenter<ccCycleBurnView>::OnStart();
+ g_hideBurnUiPending = 0;
g_burnUiValid = 1;
+ if (view_ != nullptr && view_->IsViewReady()) {
+ view_->AttachDragListener();
+ }
if (g_ccBurnRefreshTimer == nullptr) {
g_ccBurnRefreshTimer = osTimerNew(cc_burn_refresh_timer_cb, osTimerPeriodic, nullptr, nullptr);
}
void ccCycleBurnPresenter::OnResume(void)
{
- if (view_ != nullptr) {
- view_->ApplyScreenAlwaysOn();
- view_->RefreshStatus();
+ if (view_ == nullptr || !view_->IsViewReady()) {
+ return;
}
+ g_hideBurnUiPending = 0;
+ g_burnUiValid = 1;
+ view_->AttachDragListener();
+#ifdef ENABLE_HP4570
+ if (cc_cycle_is_running() && g_ccBurnRefreshTimer != nullptr) {
+ (void)osTimerStart(g_ccBurnRefreshTimer, 1000);
+ }
+#endif
+ /* 放电常亮由 cc_cycle_service 按阶段控制,此处只刷新 UI */
+ view_->RefreshStatus();
}
void ccCycleBurnPresenter::OnPause(void)
{
+ g_burnUiValid = 0;
+ if (g_ccBurnRefreshTimer != nullptr) {
+ (void)osTimerStop(g_ccBurnRefreshTimer);
+ }
Presenter<ccCycleBurnView>::OnPause();
}
void ccCycleBurnPresenter::OnStop(void)
{
g_burnUiValid = 0;
+ OHOS::ccCycleBurnView *v = OHOS::ccCycleBurnView::GetInstance();
+ if (v != nullptr) {
+ v->DetachDragListener();
+ }
+ cc_burn_stop_alarm();
+ cc_burn_stop_show_ui_timer();
if (g_ccBurnRefreshTimer != nullptr) {
(void)osTimerStop(g_ccBurnRefreshTimer);
}
void cc_cycle_show_burn_ui(void)
{
+ if (g_hideBurnUiPending) {
+ return;
+ }
+ /* 插线/恢复时可能与充电全屏通知同时 PauseSlice,必须在 Gui 线程且略延迟切页 */
if (g_ccBurnShowUiTimer == nullptr) {
g_ccBurnShowUiTimer = osTimerNew(cc_burn_show_ui_timer_cb, osTimerOnce, nullptr, nullptr);
}
if (g_ccBurnShowUiTimer != nullptr) {
(void)osTimerStop(g_ccBurnShowUiTimer);
- (void)osTimerStart(g_ccBurnShowUiTimer, 500);
+ (void)osTimerStart(g_ccBurnShowUiTimer, 800);
+ }
+}
+
+void cc_cycle_reopen_burn_ui(void)
+{
+ g_hideBurnUiPending = 0;
+ g_burnUiValid = 1;
+ if (g_ccBurnShowUiTimer == nullptr) {
+ g_ccBurnShowUiTimer = osTimerNew(cc_burn_show_ui_timer_cb, osTimerOnce, nullptr, nullptr);
+ }
+ if (g_ccBurnShowUiTimer != nullptr) {
+ (void)osTimerStop(g_ccBurnShowUiTimer);
+ (void)osTimerStart(g_ccBurnShowUiTimer, 800);
}
}
void cc_cycle_hide_burn_ui(void)
{
- OHOS::NativeAbility::GetInstance().ChangeSlice(VIEW_TEST);
- if (OHOS::testView::GetInstance() != nullptr) {
+ // #region agent log
+ dbg_c3c5cc_ui("F", "hide_burn_ui_enter", (int)g_hideBurnUiPending);
+ // #endregion
+ if (g_hideBurnUiPending) {
+ return;
+ }
+ /* 先于异步任务置位,丢弃队列里 stale 的 refresh/alarm */
+ g_hideBurnUiPending = 1;
+ g_burnUiValid = 0;
+ cc_burn_stop_alarm();
+ cc_burn_stop_show_ui_timer();
+ if (g_ccBurnRefreshTimer != nullptr) {
+ (void)osTimerStop(g_ccBurnRefreshTimer);
+ }
#ifdef ENABLE_HP4570
- OHOS::testView::GetInstance()->ChangeView(OHOS::TESTVIEW, OHOS::CCCYCLE);
-#else
- OHOS::testView::GetInstance()->ChangeView(OHOS::TESTVIEW, OHOS::TESTVIEW);
+ cc_cycle_on_burn_ui_exit();
#endif
+ {
+ OHOS::ccCycleBurnView *v = OHOS::ccCycleBurnView::GetInstance();
+ if (v != nullptr) {
+ v->DetachDragListener();
+ }
}
- if (OHOS::testCcCycleView::GetInstance() != nullptr) {
- OHOS::testCcCycleView::GetInstance()->RefreshCcCycleButtons();
- }
+ /* 避免在 OnDragEnd 内同步 ChangeSlice+ChangeView 与 Transition 重入导致 GuiMainThread 崩溃 */
+ ::GraphicService::GetInstance()->PostGraphicEvent(cc_burn_hide_ui_deferred);
}
#include "wearable_log.h"
#include "common/screen.h"
#include "gfx_utils/color.h"
-#include "power_display_service.h"
-#include "custom_utils.h"
#include <cstdio>
-#ifdef __cplusplus
-extern "C" {
-#endif
-#include "pm_definition.h"
-#include "pm_veto.h"
-#include "pm.h"
-#ifdef __cplusplus
-}
-#endif
-
#ifdef ENABLE_HP4570
#include "cc_cycle_service.h"
+#include "battery_hal.h"
#endif
namespace OHOS
{
static ccCycleBurnView *g_burnView = nullptr;
+ static volatile uint8_t g_burnViewValid = 0;
ccCycleBurnView::ccCycleBurnView()
{
ccCycleBurnView::~ccCycleBurnView()
{
+ g_burnViewValid = 0;
DestroyBurnWidgets();
g_burnView = nullptr;
}
+ bool ccCycleBurnView::IsViewReady(void) const
+ {
+ return (g_burnViewValid != 0 && g_burnView == this && viewInit_ &&
+ burnScrollView_ != nullptr);
+ }
+
ccCycleBurnView *ccCycleBurnView::GetInstance(void)
{
return g_burnView;
}
- void ccCycleBurnView::UpdatePhaseBackground(cc_cycle_phase_e phase)
+ void ccCycleBurnView::UpdatePhaseBackground(cc_cycle_phase_e phase, uint8_t defectType)
{
- if (burnGroup_ == nullptr) {
+ if (burnContentGroup_ == nullptr) {
return;
}
- if (phase == CC_PHASE_CHARGING) {
- burnGroup_->SetStyle(STYLE_BACKGROUND_COLOR, 0xFFC8E6C9); /* 浅绿=充电 */
- if (phaseLabel_ != nullptr) {
- phaseLabel_->SetStyle(STYLE_TEXT_COLOR, 0xFF1B5E20);
- phaseLabel_->SetText("【充电中】");
- }
+ if (defectType == CC_DEFECT_RED) {
+ burnContentGroup_->SetStyle(STYLE_BACKGROUND_COLOR, 0xFFFFCDD2);
+ } else if (defectType == CC_DEFECT_YELLOW) {
+ burnContentGroup_->SetStyle(STYLE_BACKGROUND_COLOR, 0xFFFFF9C4);
+ } else if (defectType == CC_DEFECT_BLUE) {
+ burnContentGroup_->SetStyle(STYLE_BACKGROUND_COLOR, 0xFFB3E5FC);
+ } else if (defectType == CC_DEFECT_LOW_ICHG) {
+ burnContentGroup_->SetStyle(STYLE_BACKGROUND_COLOR, 0xFFFFE0B2);
+ } else if (defectType == CC_DEFECT_HW_NO_CHG) {
+ burnContentGroup_->SetStyle(STYLE_BACKGROUND_COLOR, 0xFFE1BEE7);
+ } else if (phase == CC_PHASE_CHARGING) {
+ burnContentGroup_->SetStyle(STYLE_BACKGROUND_COLOR, 0xFFC8E6C9);
} else if (phase == CC_PHASE_DISCHARGE) {
- burnGroup_->SetStyle(STYLE_BACKGROUND_COLOR, Color::White().full);
- if (phaseLabel_ != nullptr) {
- phaseLabel_->SetStyle(STYLE_TEXT_COLOR, Color::Black().full);
- phaseLabel_->SetText("【放电中】");
- }
+ burnContentGroup_->SetStyle(STYLE_BACKGROUND_COLOR, Color::White().full);
} else {
- burnGroup_->SetStyle(STYLE_BACKGROUND_COLOR, Color::Gray().full);
- if (phaseLabel_ != nullptr) {
- phaseLabel_->SetText("【空闲】");
- }
+ burnContentGroup_->SetStyle(STYLE_BACKGROUND_COLOR, Color::Gray().full);
}
- }
- void ccCycleBurnView::ApplyScreenAlwaysOn(void)
- {
- const power_display_svr_api_t *display_api = power_display_svr_get_api();
- if (display_api == nullptr) {
+ if (phaseLabel_ == nullptr) {
return;
}
- if (display_api->get_screen_state() != SCREEN_ON) {
- display_api->turn_on_screen();
+ if (phase == CC_PHASE_CHARGING) {
+ phaseLabel_->SetStyle(STYLE_TEXT_COLOR, 0xFF1B5E20);
+ phaseLabel_->SetText("充电中");
+ } else if (phase == CC_PHASE_DISCHARGE) {
+ phaseLabel_->SetStyle(STYLE_TEXT_COLOR, Color::Black().full);
+ phaseLabel_->SetText("放电中");
+ } else {
+ phaseLabel_->SetStyle(STYLE_TEXT_COLOR, Color::White().full);
+ phaseLabel_->SetText("空闲");
}
- display_api->set_auto_timeout_function(false);
- uapi_setScreenBrightness(255);
}
void ccCycleBurnView::RefreshStatus(void)
{
+ if (!IsViewReady() || GetInstance() != this) {
+ return;
+ }
#ifdef ENABLE_HP4570
cc_cycle_phase_e p = cc_cycle_get_phase();
- UpdatePhaseBackground(p);
+ uint8_t defect = cc_cycle_get_defect_type();
+ UpdatePhaseBackground(p, defect);
+ if (socLabel_ != nullptr) {
+ char socBuf[16];
+ (void)snprintf(socBuf, sizeof(socBuf), "%u%%", (unsigned int)cc_cycle_get_soc());
+ socLabel_->SetText(socBuf);
+ }
+ if (metricsLabel_ != nullptr) {
+ float voltage = 0.0f;
+ float temp = 0.0f;
+ (void)battery_get_voltage(&voltage);
+ (void)battery_get_battery_temp(&temp);
+ char detail[64];
+ (void)snprintf(detail, sizeof(detail), "V:%.2fV T:%.1fC",
+ (double)voltage, (double)temp);
+ metricsLabel_->SetText(detail);
+ }
+ if (summaryTitleLabel_ != nullptr) {
+ summaryTitleLabel_->SetText("充放电拷机汇总");
+ }
+ {
+ cc_cycle_burn_summary_t sum;
+ char line[48];
+
+ cc_cycle_get_burn_summary(&sum);
+ if (clockLabel_ != nullptr) {
+ (void)snprintf(line, sizeof(line), "当前时间:%s", sum.current_time);
+ clockLabel_->SetText(line);
+ }
+ if (lastChargeLabel_ != nullptr) {
+ (void)snprintf(line, sizeof(line), "上次充电:%s", sum.last_charge);
+ lastChargeLabel_->SetText(line);
+ }
+ if (curChargeLabel_ != nullptr) {
+ (void)snprintf(line, sizeof(line), "本次充电:%s", sum.cur_charge);
+ curChargeLabel_->SetText(line);
+ }
+ if (lastDisLabel_ != nullptr) {
+ (void)snprintf(line, sizeof(line), "上次放电:%s", sum.last_discharge);
+ lastDisLabel_->SetText(line);
+ }
+ if (curDisLabel_ != nullptr) {
+ (void)snprintf(line, sizeof(line), "本次放电:%s", sum.cur_discharge);
+ curDisLabel_->SetText(line);
+ }
+ }
+ if (defectLabel_ != nullptr) {
+ char defectBuf[64];
+ if (cc_cycle_format_defect_line(defectBuf, sizeof(defectBuf)) > 0) {
+ defectLabel_->SetText(defectBuf);
+ defectLabel_->SetVisible(true);
+ } else {
+ defectLabel_->SetText("");
+ defectLabel_->SetVisible(false);
+ }
+ }
if (statusLabel_ != nullptr) {
char buf[96];
(void)cc_cycle_format_status_line(buf, sizeof(buf));
statusLabel_->SetText(buf);
}
- // RootView::GetInstance()->Invalidate();
#else
if (statusLabel_ != nullptr) {
statusLabel_->SetText("拷机");
}
#endif
- ApplyScreenAlwaysOn();
+ if (burnScrollView_ != nullptr) {
+ burnScrollView_->Invalidate();
+ }
}
void ccCycleBurnView::DestroyBurnWidgets(void)
{
- if (burnGroup_ == nullptr) {
+ g_burnViewValid = 0;
+ if (burnScrollView_ == nullptr) {
return;
}
+ burnScrollView_->SetOnDragListener(nullptr);
UIViewGroup *root = GetRootContainer();
if (root != nullptr) {
- root->Remove(burnGroup_);
+ root->Remove(burnScrollView_);
+ }
+ if (burnContentGroup_ != nullptr) {
+ burnContentGroup_->RemoveAll();
}
- burnGroup_->RemoveAll();
delete phaseLabel_;
+ delete socLabel_;
+ delete metricsLabel_;
+ delete summaryTitleLabel_;
+ delete clockLabel_;
+ delete lastChargeLabel_;
+ delete curChargeLabel_;
+ delete lastDisLabel_;
+ delete curDisLabel_;
+ delete defectLabel_;
delete statusLabel_;
- delete burnGroup_;
+ delete footerLabel_;
+ delete burnContentGroup_;
+ delete burnScrollView_;
phaseLabel_ = nullptr;
+ socLabel_ = nullptr;
+ metricsLabel_ = nullptr;
+ summaryTitleLabel_ = nullptr;
+ clockLabel_ = nullptr;
+ lastChargeLabel_ = nullptr;
+ curChargeLabel_ = nullptr;
+ lastDisLabel_ = nullptr;
+ curDisLabel_ = nullptr;
+ defectLabel_ = nullptr;
statusLabel_ = nullptr;
- burnGroup_ = nullptr;
+ footerLabel_ = nullptr;
+ burnContentGroup_ = nullptr;
+ burnScrollView_ = nullptr;
viewInit_ = false;
}
int w = Screen::GetInstance().GetWidth();
int h = Screen::GetInstance().GetHeight();
- burnGroup_ = new UIViewGroup();
- if (burnGroup_ == nullptr) {
+ static constexpr int kContentH = 580;
+ static constexpr int kScrollBlank = 60;
+ static constexpr int kPhaseFont = 48;
+ static constexpr int kPhaseH = 64;
+ static constexpr int kSummaryFont = 24;
+ static constexpr int kSummaryLineH = 32;
+ static constexpr uint32_t kColorSummaryBlue = 0xFF1976D2;
+ static constexpr uint32_t kColorSummaryRed = 0xFFD32F2F;
+ const int contentX = 20;
+ const int contentW = w - 40;
+
+ burnScrollView_ = new UIScrollView();
+ if (burnScrollView_ == nullptr) {
return false;
}
- burnGroup_->SetPosition(0, 0, w, h);
- burnGroup_->SetStyle(STYLE_BACKGROUND_OPA, 0xff);
- burnGroup_->SetStyle(STYLE_BACKGROUND_COLOR, Color::White().full);
- burnGroup_->SetTouchable(true);
+ burnScrollView_->SetPosition(0, 0, w, h);
+ burnScrollView_->SetStyle(STYLE_BACKGROUND_OPA, 0xff);
+ burnScrollView_->SetStyle(STYLE_BACKGROUND_COLOR, Color::White().full);
+ burnScrollView_->SetScrollBlankSize(kScrollBlank);
+ /* 仅纵向滚动;横向留给左滑返回,避免与 ScrollView 横向拖动冲突 */
+ burnScrollView_->SetHorizontalScrollState(false);
+ burnScrollView_->SetTouchable(true);
+ burnScrollView_->SetDraggable(true);
+
+ burnContentGroup_ = new UIViewGroup();
+ if (burnContentGroup_ == nullptr) {
+ delete burnScrollView_;
+ burnScrollView_ = nullptr;
+ return false;
+ }
+ burnContentGroup_->SetPosition(0, 0, w, kContentH);
+ burnContentGroup_->SetStyle(STYLE_BACKGROUND_OPA, 0xff);
+ burnContentGroup_->SetStyle(STYLE_BACKGROUND_COLOR, Color::White().full);
phaseLabel_ = new UILabel();
if (phaseLabel_ == nullptr) {
return false;
}
- phaseLabel_->SetPosition(0, h / 2 - 80, w, 64);
- phaseLabel_->SetStyle(STYLE_TEXT_FONT, 40);
+ phaseLabel_->SetPosition(0, 8, w, kPhaseH);
+ phaseLabel_->SetStyle(STYLE_TEXT_FONT, kPhaseFont);
phaseLabel_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_CENTER);
- phaseLabel_->SetText("【拷机】");
- burnGroup_->Add(phaseLabel_);
+ phaseLabel_->SetText("拷机");
+ burnContentGroup_->Add(phaseLabel_);
+
+ socLabel_ = new UILabel();
+ if (socLabel_ == nullptr) {
+ return false;
+ }
+ socLabel_->SetPosition(0, 76, w, 96);
+ socLabel_->SetStyle(STYLE_TEXT_COLOR, Color::Black().full);
+ socLabel_->SetStyle(STYLE_TEXT_FONT, 72);
+ socLabel_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_CENTER);
+ socLabel_->SetText("--%");
+ burnContentGroup_->Add(socLabel_);
+
+ metricsLabel_ = new UILabel();
+ if (metricsLabel_ == nullptr) {
+ return false;
+ }
+ metricsLabel_->SetPosition(contentX, 184, contentW, 32);
+ metricsLabel_->SetStyle(STYLE_TEXT_COLOR, Color::Black().full);
+ metricsLabel_->SetStyle(STYLE_TEXT_FONT, 26);
+ metricsLabel_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_CENTER);
+ metricsLabel_->SetText("V:-- T:--");
+ burnContentGroup_->Add(metricsLabel_);
+
+ summaryTitleLabel_ = new UILabel();
+ if (summaryTitleLabel_ == nullptr) {
+ return false;
+ }
+ summaryTitleLabel_->SetPosition(0, 220, w, kSummaryLineH);
+ summaryTitleLabel_->SetStyle(STYLE_TEXT_COLOR, Color::Black().full);
+ summaryTitleLabel_->SetStyle(STYLE_TEXT_FONT, kSummaryFont);
+ summaryTitleLabel_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_CENTER);
+ summaryTitleLabel_->SetText("充放电拷机汇总");
+ burnContentGroup_->Add(summaryTitleLabel_);
+
+ clockLabel_ = new UILabel();
+ if (clockLabel_ == nullptr) {
+ return false;
+ }
+ clockLabel_->SetPosition(contentX, 256, contentW, kSummaryLineH);
+ clockLabel_->SetStyle(STYLE_TEXT_COLOR, Color::Black().full);
+ clockLabel_->SetStyle(STYLE_TEXT_FONT, kSummaryFont);
+ clockLabel_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_CENTER);
+ clockLabel_->SetText("当前时间:--:--:--");
+ burnContentGroup_->Add(clockLabel_);
+
+ lastChargeLabel_ = new UILabel();
+ if (lastChargeLabel_ == nullptr) {
+ return false;
+ }
+ lastChargeLabel_->SetPosition(contentX, 288, contentW, kSummaryLineH);
+ lastChargeLabel_->SetStyle(STYLE_TEXT_COLOR, kColorSummaryBlue);
+ lastChargeLabel_->SetStyle(STYLE_TEXT_FONT, kSummaryFont);
+ lastChargeLabel_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_CENTER);
+ lastChargeLabel_->SetText("上次充电:--");
+ burnContentGroup_->Add(lastChargeLabel_);
+
+ curChargeLabel_ = new UILabel();
+ if (curChargeLabel_ == nullptr) {
+ return false;
+ }
+ curChargeLabel_->SetPosition(contentX, 320, contentW, kSummaryLineH);
+ curChargeLabel_->SetStyle(STYLE_TEXT_COLOR, kColorSummaryRed);
+ curChargeLabel_->SetStyle(STYLE_TEXT_FONT, kSummaryFont);
+ curChargeLabel_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_CENTER);
+ curChargeLabel_->SetText("本次充电:待开始");
+ burnContentGroup_->Add(curChargeLabel_);
+
+ lastDisLabel_ = new UILabel();
+ if (lastDisLabel_ == nullptr) {
+ return false;
+ }
+ lastDisLabel_->SetPosition(contentX, 352, contentW, kSummaryLineH);
+ lastDisLabel_->SetStyle(STYLE_TEXT_COLOR, kColorSummaryBlue);
+ lastDisLabel_->SetStyle(STYLE_TEXT_FONT, kSummaryFont);
+ lastDisLabel_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_CENTER);
+ lastDisLabel_->SetText("上次放电:--");
+ burnContentGroup_->Add(lastDisLabel_);
+
+ curDisLabel_ = new UILabel();
+ if (curDisLabel_ == nullptr) {
+ return false;
+ }
+ curDisLabel_->SetPosition(contentX, 384, contentW, kSummaryLineH);
+ curDisLabel_->SetStyle(STYLE_TEXT_COLOR, kColorSummaryRed);
+ curDisLabel_->SetStyle(STYLE_TEXT_FONT, kSummaryFont);
+ curDisLabel_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_CENTER);
+ curDisLabel_->SetText("本次放电:待开始");
+ burnContentGroup_->Add(curDisLabel_);
+
+ defectLabel_ = new UILabel();
+ if (defectLabel_ == nullptr) {
+ return false;
+ }
+ defectLabel_->SetPosition(contentX, 424, contentW, 36);
+ defectLabel_->SetStyle(STYLE_TEXT_COLOR, 0xFFB71C1C);
+ defectLabel_->SetStyle(STYLE_TEXT_FONT, 22);
+ defectLabel_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_CENTER);
+ defectLabel_->SetVisible(false);
+ burnContentGroup_->Add(defectLabel_);
statusLabel_ = new UILabel();
if (statusLabel_ == nullptr) {
return false;
}
- statusLabel_->SetPosition(20, h / 2, w - 40, 72);
+ statusLabel_->SetPosition(contentX, 464, contentW, 72);
statusLabel_->SetStyle(STYLE_TEXT_COLOR, Color::Black().full);
- statusLabel_->SetStyle(STYLE_TEXT_FONT, 24);
+ statusLabel_->SetStyle(STYLE_TEXT_FONT, 22);
statusLabel_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_TOP);
- statusLabel_->SetText("左滑返回");
- burnGroup_->Add(statusLabel_);
+ statusLabel_->SetText("");
+ burnContentGroup_->Add(statusLabel_);
+
+ footerLabel_ = new UILabel();
+ if (footerLabel_ == nullptr) {
+ return false;
+ }
+ footerLabel_->SetPosition(0, 544, w, 40);
+ footerLabel_->SetStyle(STYLE_TEXT_COLOR, Color::Gray().full);
+ footerLabel_->SetStyle(STYLE_TEXT_FONT, 22);
+ footerLabel_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_CENTER);
+ footerLabel_->SetText("左滑返回");
+ burnContentGroup_->Add(footerLabel_);
+
+ burnScrollView_->Add(burnContentGroup_);
- burnGroup_->SetDraggable(true);
UIView::OnDragListener *dragListener =
static_cast<UIView::OnDragListener *>(ccCycleBurnPresenter::GetInstance());
if (dragListener != nullptr) {
- burnGroup_->SetOnDragListener(dragListener);
+ burnScrollView_->SetOnDragListener(dragListener);
}
- AddViewToRootContainer(burnGroup_);
+ AddViewToRootContainer(burnScrollView_);
+ g_burnViewValid = 1;
return true;
}
return;
}
viewInit_ = true;
- }
- ApplyScreenAlwaysOn();
- uint8_t cnt = pm_veto_get_id_cnt(PM_ID_P_TEST);
- if (cnt == 0) {
- uapi_pm_add_sleep_veto(PM_ID_P_TEST);
+ } else {
+ AttachDragListener();
}
RefreshStatus();
}
- void ccCycleBurnView::OnStop()
+ void ccCycleBurnView::DetachDragListener(void)
{
- WEARABLE_LOGD(WEARABLE_LOG_MODULE_APP, "ccCycleBurnView::OnStop");
- const power_display_svr_api_t *display_api = power_display_svr_get_api();
- if (display_api != nullptr) {
- display_api->set_auto_timeout_function(true);
+ if (burnScrollView_ != nullptr) {
+ burnScrollView_->SetOnDragListener(nullptr);
}
- uint8_t cnt = pm_veto_get_id_cnt(PM_ID_P_TEST);
- if (cnt == 1) {
- uapi_pm_remove_sleep_veto(PM_ID_P_TEST);
+ }
+
+ void ccCycleBurnView::AttachDragListener(void)
+ {
+ if (burnScrollView_ == nullptr) {
+ return;
+ }
+ UIView::OnDragListener *dragListener =
+ static_cast<UIView::OnDragListener *>(ccCycleBurnPresenter::GetInstance());
+ if (dragListener != nullptr) {
+ burnScrollView_->SetOnDragListener(dragListener);
}
}
+
+ void ccCycleBurnView::OnStop()
+ {
+ WEARABLE_LOGD(WEARABLE_LOG_MODULE_APP, "ccCycleBurnView::OnStop");
+ }
}
#include "wearable_log.h"
#include "custom_utils.h"
+#include "AppViewIDs.h"
+#include "NativeAbility.h"
+#include "graphic_service.h"
#include "producttest/include/testPresenter.h"
#include "producttest/include/testCcCycle.h"
#include "producttest/include/testView.h"
#include "producttest/include/testabout.h"
#include "common/screen.h"
+#include "dock/focus_manager.h"
#include "gfx_utils/color.h"
+#include "graphic_config.h"
#include "cmsis_os2.h"
#include <cstdio>
+#include <cstring>
+#include <cstdlib>
#ifdef ENABLE_HP4570
#include "cc_cycle_service.h"
+#include "battery_hal.h"
namespace OHOS
{
static testCcCycleView *g_testCcCycleView = nullptr;
static osTimerId_t g_ccCycleStatusTimer = nullptr;
+ static volatile uint8_t g_ccCycleViewValid = 0;
+ static volatile uint8_t g_ccCycleTimerPaused = 0;
- testCcCycleView::testCcCycleView()
+ static constexpr int CC_CYCLE_CONTENT_MARGIN = 86;
+ static constexpr int CC_CYCLE_BTN_H = 72;
+ static constexpr int CC_CYCLE_BTN_FONT = 28;
+ static constexpr int CC_IC_REG_FONT = 28;
+ static constexpr int CC_IC_REG_LABEL_H = 1600;
+ /* 圆屏 overlay:视口避开上下弧区,SetScrollBlankSize 再留滚动余量 */
+ static constexpr int kOverlayScrollTop = 52;
+ static constexpr int kOverlayScrollBottom = 52;
+ static constexpr int kOverlayScrollBlankTop = 56;
+ static constexpr int kOverlayScrollBlankBottom = 88;
+ static constexpr int kOverlayLabelMargin = 12;
+ static constexpr int CC_HISTORY_FONT = 28;
+ static constexpr int CC_HISTORY_LABEL_H = 2400;
+
+ static char g_icPanelBuf[CC_CYCLE_DEFECT_REG_DUMP_SIZE + 128];
+
+ static bool CcCycleIsSwipeBackToClose(const DragEvent &event)
+ {
+ if (event.GetDragDirection() != DragEvent::DIRECTION_LEFT_TO_RIGHT) {
+ return false;
+ }
+ Point startPoint = event.GetStartPoint();
+ Point endPoint = event.GetLastPoint();
+ int dragDistance = endPoint.x - startPoint.x;
+ int dragHeight = abs(endPoint.y - startPoint.y);
+ int screenWidth = Screen::GetInstance().GetWidth();
+
+ return (dragDistance > 80 && dragHeight < 80 &&
+ startPoint.x < screenWidth / 2 && dragDistance > dragHeight * 2);
+ }
+
+ class CcCycleOverlayDragListener : public UIView::OnDragListener {
+ public:
+ bool OnDrag(UIView &view, const DragEvent &event) override
+ {
+ (void)view;
+ (void)event;
+ return true;
+ }
+
+ bool OnDragEnd(UIView &view, const DragEvent &event) override
+ {
+ (void)view;
+ if (!CcCycleIsSwipeBackToClose(event)) {
+ return false;
+ }
+ testCcCycleView *v = testCcCycleView::GetInstance();
+ if (v != nullptr) {
+ v->CloseVisibleOverlay();
+ }
+ return true;
+ }
+ };
+
+ static CcCycleOverlayDragListener g_ccOverlayDragListener;
+
+ void testCcCycleView::InvalidateLocalView(void)
+ {
+ if (overlayGroup_ != nullptr && overlayGroup_->IsVisible()) {
+ overlayGroup_->Invalidate();
+ return;
+ }
+ if (rootGroup_ != nullptr) {
+ rootGroup_->Invalidate();
+ } else if (testCcCycleScrollView_ != nullptr) {
+ testCcCycleScrollView_->Invalidate();
+ }
+ }
+
+ static void ApplyLabelContentSize(UILabel *label, UIScrollView *scroll)
+ {
+ uint16_t textH;
+
+ if (label == nullptr) {
+ return;
+ }
+ textH = label->GetTextHeight();
+ if (textH < 32) {
+ textH = 32;
+ }
+ if (label->GetHeight() != textH) {
+ label->Resize(label->GetWidth(), textH);
+ }
+ label->Invalidate();
+ if (scroll != nullptr) {
+ scroll->Invalidate();
+ }
+ }
+
+ static void FillIcRegisterPanelText(char *buf, size_t buf_size)
+ {
+ if (buf == NULL || buf_size == 0) {
+ return;
+ }
+ /* 与 debug 页 get_hp4570_register_dump 一致:仅 hp4570n_dump_debug_registers */
+ (void)cc_cycle_format_ic_registers(buf, (unsigned int)buf_size);
+ }
+
+ static UILabelButtonExt *CreateCcCycleButton(int x, int y, int btnW, int btnH,
+ const char *viewId, UIView::OnClickListener *clickListener)
+ {
+ UILabelButtonExt *btn = new UILabelButtonExt();
+ if (btn == nullptr) {
+ return nullptr;
+ }
+ btn->SetPosition(x, y, btnW, btnH);
+ btn->SetViewId(viewId);
+ btn->SetStyleForState(STYLE_BACKGROUND_COLOR, 0xff0b1928, UIButton::PRESSED);
+ btn->SetStyle(STYLE_BACKGROUND_OPA, 0x80);
+ btn->SetAlign(TEXT_ALIGNMENT_CENTER);
+ btn->SetLabelPosition(0, 0);
+ btn->SetFont(DEFAULT_VECTOR_FONT_FILENAME_EN, CC_CYCLE_BTN_FONT);
+ btn->SetOnClickListener(clickListener);
+ btn->SetVisible(true);
+ return btn;
+ }
+
+ testCcCycleView::testCcCycleView() : icPanelVisible_(false), historyPanelVisible_(false)
{
g_testCcCycleView = this;
}
testCcCycleView::~testCcCycleView()
{
+ g_ccCycleViewValid = 0;
if (g_ccCycleStatusTimer != nullptr) {
(void)osTimerStop(g_ccCycleStatusTimer);
(void)osTimerDelete(g_ccCycleStatusTimer);
g_testCcCycleView = nullptr;
}
- void testCcCycleView::StatusTimerCallback(void *arg)
+ static void CcCycleStatusRefreshOnGraphic(void)
{
- (void)arg;
testCcCycleView *v = testCcCycleView::GetInstance();
- if (v != nullptr) {
- v->RefreshCcCycleButtons();
+ if (v == nullptr || !v->IsUiReady()) {
+ return;
}
+ v->RefreshCcCycleButtons();
}
- void testCcCycleView::RefreshStatusLabel(void)
+ void testCcCycleView::StatusTimerCallback(void *arg)
{
- if (statusLabel_ == nullptr) {
+ (void)arg;
+ if (OHOS::NativeAbility::GetInstance().GetCurSliceId() != VIEW_TEST) {
return;
}
- char buf[96];
- (void)cc_cycle_format_status_line(buf, sizeof(buf));
- statusLabel_->SetText(buf);
+ ::GraphicService::GetInstance()->PostGraphicEvent(CcCycleStatusRefreshOnGraphic);
}
- testCcCycleView *testCcCycleView::GetInstance(void)
+ void testCcCycleView::UpdatePhaseCard(void)
{
- return g_testCcCycleView;
+ if (phaseLabel_ == nullptr) {
+ return;
+ }
+ cc_cycle_phase_e phase = cc_cycle_get_phase();
+ if (!cc_cycle_is_running() || phase == CC_PHASE_IDLE) {
+ phaseLabel_->SetStyle(STYLE_BACKGROUND_COLOR, Color::Gray().full);
+ phaseLabel_->SetStyle(STYLE_TEXT_COLOR, Color::White().full);
+ phaseLabel_->SetText("阶段: 空闲");
+ return;
+ }
+ if (phase == CC_PHASE_CHARGING) {
+ uint8_t defect = cc_cycle_get_defect_type();
+ if (defect == CC_DEFECT_RED) {
+ phaseLabel_->SetStyle(STYLE_BACKGROUND_COLOR, 0xFFFFCDD2);
+ } else if (defect == CC_DEFECT_YELLOW) {
+ phaseLabel_->SetStyle(STYLE_BACKGROUND_COLOR, 0xFFFFF9C4);
+ } else if (defect == CC_DEFECT_BLUE) {
+ phaseLabel_->SetStyle(STYLE_BACKGROUND_COLOR, 0xFFB3E5FC);
+ } else if (defect == CC_DEFECT_LOW_ICHG) {
+ phaseLabel_->SetStyle(STYLE_BACKGROUND_COLOR, 0xFFFFE0B2);
+ } else if (defect == CC_DEFECT_HW_NO_CHG) {
+ phaseLabel_->SetStyle(STYLE_BACKGROUND_COLOR, 0xFFE1BEE7);
+ } else {
+ phaseLabel_->SetStyle(STYLE_BACKGROUND_COLOR, 0xFFC8E6C9);
+ }
+ phaseLabel_->SetStyle(STYLE_TEXT_COLOR, 0xFF1B5E20);
+ phaseLabel_->SetText("阶段: 充电中");
+ return;
+ }
+ phaseLabel_->SetStyle(STYLE_BACKGROUND_COLOR, Color::White().full);
+ phaseLabel_->SetStyle(STYLE_TEXT_COLOR, Color::Black().full);
+ phaseLabel_->SetText("阶段: 放电中");
}
- void testCcCycleView::RefreshCcCycleButtons(void)
+ void testCcCycleView::RefreshStatusLabel(void)
{
- if (labelButton_cc_sw_ != nullptr) {
- if (cc_cycle_get_enabled()) {
- labelButton_cc_sw_->SetText("拷机开关:开");
+ if (g_ccCycleViewValid == 0 || testCcCycleScrollView_ == nullptr) {
+ return;
+ }
+ UpdatePhaseCard();
+
+ if (title_ != nullptr) {
+ title_->SetText("充放电拷机");
+ }
+ if (socLabel_ != nullptr) {
+ char socBuf[16];
+ (void)snprintf(socBuf, sizeof(socBuf), "%u%%", (unsigned int)cc_cycle_get_soc());
+ socLabel_->SetText(socBuf);
+ }
+ if (metricsLabel_ != nullptr) {
+ float voltage = 0.0f;
+ float temp = 0.0f;
+ (void)battery_get_voltage(&voltage);
+ (void)battery_get_battery_temp(&temp);
+ char detail[64];
+ (void)snprintf(detail, sizeof(detail), "V:%.2fV T:%.1fC",
+ (double)voltage, (double)temp);
+ metricsLabel_->SetText(detail);
+ }
+ if (statusLabel_ != nullptr) {
+ char buf[128];
+ char defectBuf[64];
+ (void)cc_cycle_format_status_line(buf, sizeof(buf));
+ if (cc_cycle_format_defect_line(defectBuf, sizeof(defectBuf)) > 0) {
+ char combined[192];
+ (void)snprintf(combined, sizeof(combined), "%s\n%s", defectBuf, buf);
+ statusLabel_->SetText(combined);
+ statusLabel_->SetStyle(STYLE_TEXT_COLOR, 0xFFFF5252);
} else {
- labelButton_cc_sw_->SetText("拷机开关:关");
+ statusLabel_->SetText(buf);
+ statusLabel_->SetStyle(STYLE_TEXT_COLOR, Color::White().full);
}
}
- if (labelButton_cc_enter_ != nullptr) {
- if (cc_cycle_is_running()) {
- labelButton_cc_enter_->SetText("停止自动拷机");
+ if (hintLabel_ != nullptr && !icPanelVisible_) {
+ uint8_t defect = cc_cycle_get_defect_type();
+ if (defect != CC_DEFECT_NONE) {
+ hintLabel_->SetText("有不良! 点「充电IC状态」查看寄存器");
} else {
- labelButton_cc_enter_->SetText("开始自动拷机(需插线)");
+ hintLabel_->SetText("需插线;选首轮充电或放电后自动循环");
}
}
- RefreshStatusLabel();
}
- void testCcCycleView::ClearAll(void)
+ void testCcCycleView::ApplyOverlayChrome(bool overlayVisible)
{
- if (testCcCycleScrollView_ == nullptr) {
- return;
+ if (labelButton_cc_ic_ != nullptr) {
+ labelButton_cc_ic_->SetVisible(!overlayVisible);
+ }
+ if (labelButton_cc_history_ != nullptr) {
+ labelButton_cc_history_->SetVisible(!overlayVisible);
+ }
+ if (labelButton_cc_back_burn_ != nullptr) {
+ labelButton_cc_back_burn_->SetVisible(!overlayVisible && cc_cycle_is_running() != 0);
}
- testCcCycleScrollView_->RemoveAll();
if (labelButton_cc_sw_ != nullptr) {
- delete labelButton_cc_sw_;
- labelButton_cc_sw_ = nullptr;
+ labelButton_cc_sw_->SetVisible(!overlayVisible);
}
- if (labelButton_cc_enter_ != nullptr) {
- delete labelButton_cc_enter_;
- labelButton_cc_enter_ = nullptr;
+ if (labelButton_cc_start_charge_ != nullptr) {
+ labelButton_cc_start_charge_->SetVisible(!overlayVisible);
}
- if (labelButton_next_ != nullptr) {
- delete labelButton_next_;
- labelButton_next_ = nullptr;
+ if (labelButton_cc_start_discharge_ != nullptr) {
+ labelButton_cc_start_discharge_->SetVisible(!overlayVisible);
}
- if (hintLabel_ != nullptr) {
- delete hintLabel_;
- hintLabel_ = nullptr;
+ if (title_ != nullptr) {
+ title_->SetVisible(!overlayVisible);
+ }
+ if (phaseLabel_ != nullptr) {
+ phaseLabel_->SetVisible(!overlayVisible);
+ }
+ if (socLabel_ != nullptr) {
+ socLabel_->SetVisible(!overlayVisible);
+ }
+ if (metricsLabel_ != nullptr) {
+ metricsLabel_->SetVisible(!overlayVisible);
}
if (statusLabel_ != nullptr) {
- delete statusLabel_;
- statusLabel_ = nullptr;
+ statusLabel_->SetVisible(!overlayVisible);
}
- if (title_ != nullptr) {
- delete title_;
- title_ = nullptr;
+ if (hintLabel_ != nullptr) {
+ hintLabel_->SetVisible(!overlayVisible);
+ }
+ if (overlayGroup_ != nullptr) {
+ overlayGroup_->SetVisible(overlayVisible);
+ }
+ if (testCcCycleScrollView_ != nullptr) {
+ testCcCycleScrollView_->SetTouchable(!overlayVisible);
+ testCcCycleScrollView_->SetDraggable(!overlayVisible);
+ UIView::OnDragListener *dragListener =
+ static_cast<UIView::OnDragListener *>(testPresenter::GetInstance());
+ if (overlayVisible) {
+ testCcCycleScrollView_->SetOnDragListener(nullptr);
+ } else if (dragListener != nullptr) {
+ testCcCycleScrollView_->SetOnDragListener(dragListener);
+ }
+ }
+ }
+
+ void testCcCycleView::SetupOverlayScroll(UIScrollView *scroll)
+ {
+ if (scroll == nullptr) {
+ return;
+ }
+ scroll->SetIntercept(true);
+ scroll->SetDraggable(true);
+ scroll->SetTouchable(true);
+ scroll->SetHorizontalScrollState(false);
+ scroll->SetVerticalScrollState(true);
+ scroll->SetThrowDrag(false);
+ scroll->SetReboundSize(0);
+ scroll->SetScrollBlankSize(kOverlayScrollBlankBottom, UIAbstractScroll::Direction::BOTTOM);
+ scroll->SetScrollBlankSize(kOverlayScrollBlankTop, UIAbstractScroll::Direction::TOP);
+ scroll->SetOnDragListener(&g_ccOverlayDragListener);
+ FocusManager::GetInstance()->RequestFocus(scroll);
+ }
+
+ void testCcCycleView::CloseVisibleOverlay(void)
+ {
+ if (historyPanelVisible_) {
+ SetHistoryPanelVisible(false);
+ } else if (icPanelVisible_) {
+ SetIcPanelVisible(false);
+ }
+ }
+
+ void testCcCycleView::RefreshHistoryPanel(void)
+ {
+ if (g_ccCycleViewValid == 0 || historyLabel_ == nullptr) {
+ return;
+ }
+ historyLabel_->SetText(cc_cycle_get_history_text());
+ ApplyLabelContentSize(historyLabel_, historyScrollView_);
+ }
+
+ void testCcCycleView::SetHistoryPanelVisible(bool visible)
+ {
+ if (visible) {
+ icPanelVisible_ = false;
+ if (icStatusScrollView_ != nullptr) {
+ icStatusScrollView_->SetVisible(false);
+ }
}
- delete testCcCycleScrollView_;
+ historyPanelVisible_ = visible;
+ if (historyScrollView_ != nullptr) {
+ historyScrollView_->SetVisible(visible);
+ }
+ ApplyOverlayChrome(visible || icPanelVisible_);
+ if (historyScrollView_ != nullptr && visible) {
+ SetupOverlayScroll(historyScrollView_);
+ RefreshHistoryPanel();
+ InvalidateLocalView();
+ } else if (!visible && !icPanelVisible_) {
+ RefreshStatusLabel();
+ InvalidateLocalView();
+ }
+ }
+
+ void testCcCycleView::ShowIcStatusPanel(void)
+ {
+ if (g_ccCycleViewValid == 0 || icStatusLabel_ == nullptr) {
+ return;
+ }
+ FillIcRegisterPanelText(g_icPanelBuf, sizeof(g_icPanelBuf));
+ icStatusLabel_->SetText(g_icPanelBuf);
+ ApplyLabelContentSize(icStatusLabel_, icStatusScrollView_);
+ }
+
+ void testCcCycleView::SetIcPanelVisible(bool visible)
+ {
+ if (visible) {
+ historyPanelVisible_ = false;
+ if (historyScrollView_ != nullptr) {
+ historyScrollView_->SetVisible(false);
+ }
+ }
+ icPanelVisible_ = visible;
+ if (icStatusScrollView_ != nullptr) {
+ icStatusScrollView_->SetVisible(visible);
+ }
+ ApplyOverlayChrome(visible || historyPanelVisible_);
+ if (icStatusScrollView_ != nullptr && visible) {
+ SetupOverlayScroll(icStatusScrollView_);
+ ShowIcStatusPanel();
+ InvalidateLocalView();
+ } else if (!visible && !historyPanelVisible_) {
+ RefreshStatusLabel();
+ InvalidateLocalView();
+ }
+ }
+
+ testCcCycleView *testCcCycleView::GetInstance(void)
+ {
+ return g_testCcCycleView;
+ }
+
+ bool testCcCycleView::IsUiReady(void) const
+ {
+ return (g_ccCycleViewValid != 0 && g_testCcCycleView == this &&
+ rootGroup_ != nullptr && testCcCycleScrollView_ != nullptr);
+ }
+
+ void testCcCycleView::PauseStatusTimer(void)
+ {
+ if (g_ccCycleStatusTimer == nullptr) {
+ return;
+ }
+ (void)osTimerStop(g_ccCycleStatusTimer);
+ g_ccCycleTimerPaused = 1;
+ }
+
+ void testCcCycleView::ResumeStatusTimer(void)
+ {
+ if (g_ccCycleViewValid == 0 || g_ccCycleStatusTimer == nullptr) {
+ return;
+ }
+ if (g_ccCycleTimerPaused == 0) {
+ return;
+ }
+ g_ccCycleTimerPaused = 0;
+ (void)osTimerStart(g_ccCycleStatusTimer, 1000);
+ }
+
+ void testCcCycleView::RefreshCcCycleButtons(void)
+ {
+ if (g_ccCycleViewValid == 0 || testCcCycleScrollView_ == nullptr) {
+ return;
+ }
+ if (labelButton_cc_sw_ != nullptr) {
+ if (cc_cycle_get_enabled()) {
+ labelButton_cc_sw_->SetText("开关:开");
+ } else {
+ labelButton_cc_sw_->SetText("开关:关");
+ }
+ }
+ const char *startText = cc_cycle_is_running() ? "停止拷机" : nullptr;
+ if (labelButton_cc_start_charge_ != nullptr) {
+ labelButton_cc_start_charge_->SetText(
+ startText != nullptr ? startText : "首轮充电");
+ }
+ if (labelButton_cc_start_discharge_ != nullptr) {
+ labelButton_cc_start_discharge_->SetText(
+ startText != nullptr ? startText : "首轮放电");
+ }
+ if (labelButton_cc_back_burn_ != nullptr) {
+ const uint8_t running = cc_cycle_is_running();
+ labelButton_cc_back_burn_->SetVisible(running != 0);
+ if (running != 0) {
+ labelButton_cc_back_burn_->SetText("回到拷机界面");
+ }
+ }
+ /* overlay 大文本仅打开时刷新,避免 1s 定时器干扰矢量字渲染 */
+ if (!icPanelVisible_ && !historyPanelVisible_) {
+ RefreshStatusLabel();
+ }
+ InvalidateLocalView();
+ }
+
+ void testCcCycleView::ClearAll(void)
+ {
+ g_ccCycleViewValid = 0;
+ if (g_ccCycleStatusTimer != nullptr) {
+ (void)osTimerStop(g_ccCycleStatusTimer);
+ }
+ if (rootGroup_ != nullptr) {
+ rootGroup_->RemoveAll();
+ delete rootGroup_;
+ rootGroup_ = nullptr;
+ }
+ overlayGroup_ = nullptr;
testCcCycleScrollView_ = nullptr;
+ title_ = nullptr;
+ phaseLabel_ = nullptr;
+ socLabel_ = nullptr;
+ metricsLabel_ = nullptr;
+ statusLabel_ = nullptr;
+ hintLabel_ = nullptr;
+ labelButton_cc_sw_ = nullptr;
+ labelButton_cc_start_charge_ = nullptr;
+ labelButton_cc_start_discharge_ = nullptr;
+ labelButton_cc_ic_ = nullptr;
+ labelButton_cc_history_ = nullptr;
+ labelButton_cc_back_burn_ = nullptr;
+ icStatusScrollView_ = nullptr;
+ historyScrollView_ = nullptr;
+ icStatusLabel_ = nullptr;
+ historyLabel_ = nullptr;
+ icPanelVisible_ = false;
+ historyPanelVisible_ = false;
}
bool testCcCycleView::testCcCycleScrollViewInit(uint8_t id)
{
int w = Screen::GetInstance().GetWidth();
int h = Screen::GetInstance().GetHeight();
+ const int contentW = w - CC_CYCLE_CONTENT_MARGIN;
+ const int contentX = (w - contentW) / 2;
+ const int overlayScrollH = h - kOverlayScrollTop - kOverlayScrollBottom;
+
+ rootGroup_ = new UIViewGroup();
+ if (rootGroup_ == nullptr) {
+ return false;
+ }
+ rootGroup_->SetPosition(0, 0, w, h);
+ rootGroup_->SetStyle(STYLE_BACKGROUND_OPA, 0xff);
+ rootGroup_->SetStyle(STYLE_BACKGROUND_COLOR, Color::Black().full);
testCcCycleScrollView_ = new UIScrollView();
if (testCcCycleScrollView_ == nullptr) {
testCcCycleScrollView_->SetPosition(0, 0, w, h);
testCcCycleScrollView_->SetStyle(STYLE_BACKGROUND_OPA, 0xff);
testCcCycleScrollView_->SetStyle(STYLE_BACKGROUND_COLOR, Color::Black().full);
+ testCcCycleScrollView_->SetScrollBlankSize(60);
+ testCcCycleScrollView_->SetHorizontalScrollState(false);
- title_ = testAboutView::GetInstance()->init_title(165, 45, "充放电拷机");
+ title_ = testAboutView::GetInstance()->init_title((w - 200) / 2, 40, "充放电拷机");
if (title_ == nullptr) {
return false;
}
testCcCycleScrollView_->Add(title_);
+ phaseLabel_ = new UILabel();
+ if (phaseLabel_ == nullptr) {
+ return false;
+ }
+ phaseLabel_->SetPosition(contentX, 78, contentW, 48);
+ phaseLabel_->SetStyle(STYLE_BACKGROUND_OPA, 0xff);
+ phaseLabel_->SetStyle(STYLE_BACKGROUND_COLOR, Color::Gray().full);
+ phaseLabel_->SetStyle(STYLE_TEXT_COLOR, Color::White().full);
+ phaseLabel_->SetStyle(STYLE_TEXT_FONT, 36);
+ phaseLabel_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_CENTER);
+ phaseLabel_->SetText("阶段: 空闲");
+ testCcCycleScrollView_->Add(phaseLabel_);
+
+ socLabel_ = new UILabel();
+ if (socLabel_ == nullptr) {
+ return false;
+ }
+ socLabel_->SetPosition(contentX, 122, contentW, 56);
+ socLabel_->SetStyle(STYLE_TEXT_COLOR, Color::Yellow().full);
+ socLabel_->SetStyle(STYLE_TEXT_FONT, 40);
+ socLabel_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_CENTER);
+ socLabel_->SetText("--%");
+ testCcCycleScrollView_->Add(socLabel_);
+
+ metricsLabel_ = new UILabel();
+ if (metricsLabel_ == nullptr) {
+ return false;
+ }
+ metricsLabel_->SetPosition(contentX, 182, contentW, 32);
+ metricsLabel_->SetStyle(STYLE_TEXT_COLOR, Color::White().full);
+ metricsLabel_->SetStyle(STYLE_TEXT_FONT, 22);
+ metricsLabel_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_CENTER);
+ metricsLabel_->SetText("V:-- T:--");
+ testCcCycleScrollView_->Add(metricsLabel_);
+
statusLabel_ = new UILabel();
if (statusLabel_ == nullptr) {
return false;
}
- statusLabel_->SetPosition(30, 90, w - 60, 56);
- statusLabel_->SetStyle(STYLE_TEXT_COLOR, Color::Yellow().full);
- statusLabel_->SetStyle(STYLE_TEXT_FONT, 26);
- statusLabel_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_CENTER);
+ statusLabel_->SetPosition(contentX, 218, contentW, 56);
+ statusLabel_->SetStyle(STYLE_TEXT_COLOR, Color::White().full);
+ statusLabel_->SetStyle(STYLE_TEXT_FONT, 20);
+ statusLabel_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_TOP);
statusLabel_->SetText("状态:未运行");
testCcCycleScrollView_->Add(statusLabel_);
if (hintLabel_ == nullptr) {
return false;
}
- hintLabel_->SetPosition(40, 120, w - 80, 80);
- hintLabel_->SetStyle(STYLE_TEXT_COLOR, Color::White().full);
- hintLabel_->SetStyle(STYLE_TEXT_FONT, 24);
- hintLabel_->SetText("插线后开开关再开始;亮屏页左滑返回;本页左滑退出");
+ hintLabel_->SetPosition(contentX, 278, contentW, 40);
+ hintLabel_->SetStyle(STYLE_TEXT_COLOR, Color::Gray().full);
+ hintLabel_->SetStyle(STYLE_TEXT_FONT, 18);
+ hintLabel_->SetText("需插线;选首轮充电或放电后自动循环");
hintLabel_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_TOP);
testCcCycleScrollView_->Add(hintLabel_);
(void)cc_cycle_init();
- labelButton_cc_sw_ = testAboutView::GetInstance()->init_button_next(
- 300, 280, Test_ViewMapper_t[id].viewid_str_next);
UIView::OnClickListener *clickListener =
static_cast<UIView::OnClickListener *>(testPresenter::GetInstance());
- labelButton_cc_sw_->SetViewId("cc_cycle_sw");
- labelButton_cc_sw_->SetText("拷机开关:关");
- labelButton_cc_sw_->SetOnClickListener(clickListener);
+ labelButton_cc_sw_ = CreateCcCycleButton(contentX, 326, contentW, CC_CYCLE_BTN_H,
+ "cc_cycle_sw", clickListener);
+ if (labelButton_cc_sw_ == nullptr) {
+ return false;
+ }
+ labelButton_cc_sw_->SetText("开关:关");
testCcCycleScrollView_->Add(labelButton_cc_sw_);
- labelButton_cc_enter_ = testAboutView::GetInstance()->init_button_next(
- 300, 380, Test_ViewMapper_t[id].viewid_str_next);
- labelButton_cc_enter_->SetViewId("cc_cycle_enter");
- labelButton_cc_enter_->SetText("开始自动拷机(需插线)");
- labelButton_cc_enter_->SetOnClickListener(clickListener);
- testCcCycleScrollView_->Add(labelButton_cc_enter_);
+ labelButton_cc_start_charge_ = CreateCcCycleButton(contentX, 406, contentW, CC_CYCLE_BTN_H,
+ "cc_cycle_start_charge", clickListener);
+ if (labelButton_cc_start_charge_ == nullptr) {
+ return false;
+ }
+ labelButton_cc_start_charge_->SetText("首轮充电");
+ testCcCycleScrollView_->Add(labelButton_cc_start_charge_);
+
+ labelButton_cc_start_discharge_ = CreateCcCycleButton(contentX, 486, contentW, CC_CYCLE_BTN_H,
+ "cc_cycle_start_discharge", clickListener);
+ if (labelButton_cc_start_discharge_ == nullptr) {
+ return false;
+ }
+ labelButton_cc_start_discharge_->SetText("首轮放电");
+ testCcCycleScrollView_->Add(labelButton_cc_start_discharge_);
+
+ labelButton_cc_ic_ = CreateCcCycleButton(contentX, 566, contentW, CC_CYCLE_BTN_H,
+ "cc_cycle_ic", clickListener);
+ if (labelButton_cc_ic_ == nullptr) {
+ return false;
+ }
+ labelButton_cc_ic_->SetText("充电IC状态");
+ testCcCycleScrollView_->Add(labelButton_cc_ic_);
+
+ labelButton_cc_history_ = CreateCcCycleButton(contentX, 646, contentW, CC_CYCLE_BTN_H,
+ "cc_cycle_history", clickListener);
+ if (labelButton_cc_history_ == nullptr) {
+ return false;
+ }
+ labelButton_cc_history_->SetText("拷机记录");
+ testCcCycleScrollView_->Add(labelButton_cc_history_);
+
+ labelButton_cc_back_burn_ = CreateCcCycleButton(contentX, 726, contentW, CC_CYCLE_BTN_H,
+ "cc_cycle_back_burn", clickListener);
+ if (labelButton_cc_back_burn_ == nullptr) {
+ return false;
+ }
+ labelButton_cc_back_burn_->SetText("回到拷机界面");
+ labelButton_cc_back_burn_->SetVisible(false);
+ testCcCycleScrollView_->Add(labelButton_cc_back_burn_);
+
+ overlayGroup_ = new UIViewGroup();
+ if (overlayGroup_ == nullptr) {
+ return false;
+ }
+ overlayGroup_->SetPosition(0, 0, w, h);
+ overlayGroup_->SetStyle(STYLE_BACKGROUND_OPA, 0xff);
+ overlayGroup_->SetStyle(STYLE_BACKGROUND_COLOR, Color::Black().full);
+ overlayGroup_->SetVisible(false);
+
+ icStatusScrollView_ = new UIScrollView();
+ if (icStatusScrollView_ == nullptr) {
+ return false;
+ }
+ icStatusScrollView_->SetPosition(contentX, kOverlayScrollTop, contentW, overlayScrollH);
+ icStatusScrollView_->SetStyle(STYLE_BACKGROUND_OPA, 0xff);
+ icStatusScrollView_->SetStyle(STYLE_BACKGROUND_COLOR, Color::Black().full);
+ icStatusScrollView_->SetScrollBlankSize(kOverlayScrollBlankBottom, UIAbstractScroll::Direction::BOTTOM);
+ icStatusScrollView_->SetScrollBlankSize(kOverlayScrollBlankTop, UIAbstractScroll::Direction::TOP);
+ icStatusScrollView_->SetHorizontalScrollState(false);
+ icStatusScrollView_->SetVerticalScrollState(true);
+ icStatusScrollView_->SetYScrollBarVisible(true);
+ icStatusScrollView_->SetTouchable(true);
+ icStatusScrollView_->SetDraggable(true);
+ icStatusScrollView_->SetVisible(false);
+
+ icStatusLabel_ = new UILabel();
+ if (icStatusLabel_ == nullptr) {
+ return false;
+ }
+ icStatusLabel_->SetPosition(kOverlayLabelMargin, 0, contentW - kOverlayLabelMargin * 2, CC_IC_REG_LABEL_H);
+ icStatusLabel_->SetStyle(STYLE_TEXT_COLOR, Color::White().full);
+ icStatusLabel_->SetStyle(STYLE_TEXT_FONT, CC_IC_REG_FONT);
+ icStatusLabel_->SetStyle(STYLE_LINE_HEIGHT, CC_IC_REG_FONT + 8);
+ icStatusLabel_->SetFont(DEFAULT_VECTOR_FONT_FILENAME, CC_IC_REG_FONT);
+ icStatusLabel_->SetLineBreakMode(UILabel::LINE_BREAK_WRAP);
+ icStatusLabel_->SetAlign(TEXT_ALIGNMENT_LEFT, TEXT_ALIGNMENT_TOP);
+ icStatusLabel_->SetText("");
+ icStatusScrollView_->Add(icStatusLabel_);
+ overlayGroup_->Add(icStatusScrollView_);
+
+ historyScrollView_ = new UIScrollView();
+ if (historyScrollView_ == nullptr) {
+ return false;
+ }
+ historyScrollView_->SetPosition(contentX, kOverlayScrollTop, contentW, overlayScrollH);
+ historyScrollView_->SetStyle(STYLE_BACKGROUND_OPA, 0xff);
+ historyScrollView_->SetStyle(STYLE_BACKGROUND_COLOR, Color::Black().full);
+ historyScrollView_->SetScrollBlankSize(kOverlayScrollBlankBottom, UIAbstractScroll::Direction::BOTTOM);
+ historyScrollView_->SetScrollBlankSize(kOverlayScrollBlankTop, UIAbstractScroll::Direction::TOP);
+ historyScrollView_->SetHorizontalScrollState(false);
+ historyScrollView_->SetVerticalScrollState(true);
+ historyScrollView_->SetYScrollBarVisible(true);
+ historyScrollView_->SetTouchable(true);
+ historyScrollView_->SetDraggable(true);
+ historyScrollView_->SetVisible(false);
+
+ historyLabel_ = new UILabel();
+ if (historyLabel_ == nullptr) {
+ return false;
+ }
+ historyLabel_->SetPosition(kOverlayLabelMargin, 0, contentW - kOverlayLabelMargin * 2, CC_HISTORY_LABEL_H);
+ historyLabel_->SetStyle(STYLE_TEXT_COLOR, Color::White().full);
+ historyLabel_->SetStyle(STYLE_TEXT_FONT, CC_HISTORY_FONT);
+ historyLabel_->SetStyle(STYLE_LINE_HEIGHT, CC_HISTORY_FONT + 8);
+ historyLabel_->SetFont(DEFAULT_VECTOR_FONT_FILENAME, CC_HISTORY_FONT);
+ historyLabel_->SetLineBreakMode(UILabel::LINE_BREAK_WRAP);
+ historyLabel_->SetAlign(TEXT_ALIGNMENT_LEFT, TEXT_ALIGNMENT_TOP);
+ historyLabel_->SetText("");
+ historyScrollView_->Add(historyLabel_);
+ overlayGroup_->Add(historyScrollView_);
+
+ SetupOverlayScroll(icStatusScrollView_);
+ SetupOverlayScroll(historyScrollView_);
- labelButton_next_ = testAboutView::GetInstance()->init_button_next(
- 267, 480, Test_ViewMapper_t[id].viewid_str_next);
- labelButton_next_->SetOnClickListener(clickListener);
- testCcCycleScrollView_->Add(labelButton_next_);
+ rootGroup_->Add(testCcCycleScrollView_);
+ rootGroup_->Add(overlayGroup_);
+ (void)id;
+ g_ccCycleViewValid = 1;
RefreshCcCycleButtons();
if (g_ccCycleStatusTimer == nullptr) {
g_ccCycleStatusTimer = osTimerNew(StatusTimerCallback, osTimerPeriodic, nullptr, nullptr);
return true;
}
- UIScrollView *testCcCycleView::testCcCycleInitIView(uint8_t id)
+ UIViewGroup *testCcCycleView::testCcCycleInitIView(uint8_t id)
{
if (!testCcCycleScrollViewInit(id)) {
return nullptr;
}
- return testCcCycleScrollView_;
+ return rootGroup_;
}
}
+
+
#include "custom_runtime.h"
#if defined(SUPPORT_GNSS)
}
void testPresenter::OnResume()
{
- // 如果有待跳转的子界面(来自 AT / 协议),在 slice resume 后再执行,避免 testView 尚未创建导致跳转丢失
+ // 如果有待跳转的子界面(来自 AT / 协议 / 拷机页退出),在 slice resume 后再执行
if (g_pendingJumpSubView >= 0 && testView::GetInstance()) {
- testView::GetInstance()->ChangeView(TESTVIEW, g_pendingJumpSubView);
+ int16_t target = g_pendingJumpSubView;
g_pendingJumpSubView = -1;
+ // #region agent log
+ PRINT("[DBG:c3c5cc] {\"sessionId\":\"c3c5cc\",\"hypothesisId\":\"I\",\"location\":\"testPresenter::OnResume\","
+ "\"message\":\"pending_subview_apply\",\"data\":{\"v1\":%d}}\n", (int)target);
+ // #endregion
+ testView::GetInstance()->ChangeView(TESTVIEW, target);
+#ifdef ENABLE_HP4570
+ if (target == CCCYCLE) {
+ testCcCycleView *ccView = testCcCycleView::GetInstance();
+ if (ccView != nullptr && ccView->IsUiReady()) {
+ ccView->RefreshCcCycleButtons();
+ ccView->ResumeStatusTimer();
+ }
+ }
+#endif
}
- if(view_->GetSystemScrollView(BUTTON) != nullptr)
- {
+ if (view_ != nullptr && view_->GetSystemScrollView(BUTTON) != nullptr) {
+ testbuttonView *btnView = testbuttonView::GetInstance();
RootView::GetInstance()->SetOnKeyActListener(this);
- testbuttonView::GetInstance()->GetTestBUTTONScrollView()->RequestFocus();
+ if (btnView != nullptr && btnView->GetTestBUTTONScrollView() != nullptr) {
+ btnView->GetTestBUTTONScrollView()->RequestFocus();
+ }
}
}
+ void testPresenter::OnPause(void)
+ {
+#ifdef ENABLE_HP4570
+ testCcCycleView *ccView = testCcCycleView::GetInstance();
+ if (ccView != nullptr) {
+ ccView->PauseStatusTimer();
+ }
+#endif
+ Presenter<testView>::OnPause();
+ }
+
bool testPresenter::OnPress(UIView &view, const PressEvent &event)
{
return true;
const power_display_svr_api_t *display_api = power_display_svr_get_api();
- if (display_api->set_brightness(value) != ERRCODE_SUCC)
+ if (display_api == nullptr || display_api->set_brightness(value) != ERRCODE_SUCC)
{
WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ChangeBrightness: set_brightness fail!");
return false;
}
return true;
}
- if (strcmp(view.GetViewId(), "cc_cycle_enter") == 0) {
+ if (strcmp(view.GetViewId(), "cc_cycle_start_charge") == 0) {
if (!cc_cycle_get_enabled()) {
return true;
}
if (cc_cycle_is_running()) {
cc_cycle_stop_automation();
- } else if (cc_cycle_enter_test_ui() != ERRCODE_SUCC) {
+ } else if (cc_cycle_enter_test_ui_with_phase(CC_PHASE_CHARGING) != ERRCODE_SUCC) {
WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "cc_cycle: need charger plugged");
}
if (testCcCycleView::GetInstance() != nullptr) {
}
return true;
}
+ if (strcmp(view.GetViewId(), "cc_cycle_start_discharge") == 0) {
+ if (!cc_cycle_get_enabled()) {
+ return true;
+ }
+ if (cc_cycle_is_running()) {
+ cc_cycle_stop_automation();
+ } else if (cc_cycle_enter_test_ui_with_phase(CC_PHASE_DISCHARGE) != ERRCODE_SUCC) {
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "cc_cycle: need charger plugged");
+ }
+ if (testCcCycleView::GetInstance() != nullptr) {
+ testCcCycleView::GetInstance()->RefreshCcCycleButtons();
+ }
+ return true;
+ }
+ if (strcmp(view.GetViewId(), "cc_cycle_ic") == 0) {
+ if (testCcCycleView::GetInstance() != nullptr) {
+ testCcCycleView::GetInstance()->SetIcPanelVisible(true);
+ }
+ return true;
+ }
+ if (strcmp(view.GetViewId(), "cc_cycle_history") == 0) {
+ if (testCcCycleView::GetInstance() != nullptr) {
+ testCcCycleView::GetInstance()->SetHistoryPanelVisible(true);
+ }
+ return true;
+ }
+ if (strcmp(view.GetViewId(), "cc_cycle_back_burn") == 0) {
+ if (cc_cycle_is_running()) {
+ cc_cycle_reopen_burn_ui();
+ }
+ return true;
+ }
#endif
for(int i=0;i<len;i++)
}
if(strcmp(view.GetViewId(),Test_ViewMapper_t[i].viewid_str_next) == 0)
{
- if(Test_ViewMapper_t[i+1].enable_falg==true) {
- testView::GetInstance()->ChangeView(Test_ViewMapper_t[i].viewId, Test_ViewMapper_t[i+1].viewId);
+ int nextIdx = i + 1;
+#ifdef ENABLE_HP4570
+ if (nextIdx < len && Test_ViewMapper_t[nextIdx].viewId == CCCYCLE) {
+ nextIdx++;
+ }
+#endif
+ if (nextIdx < len && Test_ViewMapper_t[nextIdx].enable_falg == true) {
+ testView::GetInstance()->ChangeView(Test_ViewMapper_t[i].viewId,
+ Test_ViewMapper_t[nextIdx].viewId);
}
return true;
}
void factorytest_jump_to_subview_id(int16_t target);
+ /* 仅登记待跳转子页,等 VIEW_TEST slice OnResume 后再 ChangeView(拷机页退出等场景) */
+ void factorytest_set_pending_subview(int16_t target)
+ {
+ g_pendingJumpSubView = target;
+ }
+
void factorytest_enter_test_tp_view(void)
{
if(testView::GetInstance())
return g_pSystemView;
}
+ bool testView::ShouldShowChainNextButton(int16_t viewId)
+ {
+#ifdef ENABLE_HP4570
+ if (viewId == CCCYCLE) {
+ return false;
+ }
+#endif
+ int len = sizeof(Test_ViewMapper_t) / sizeof(Test_ViewMapper_t[0]);
+ for (int i = 0; i < len - 1; i++) {
+ 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 false;
+ }
+
void testView::ClearAll()
{
// 1. 释放group和关联的SystemScrollView[TESTVIEW]
buttonconfig.h = h;
int len = sizeof(Test_ViewMapper_t) / sizeof(Test_ViewMapper_t[0]);
+ int btnIdx = 1;
for (int i = 1; i < len; i++)
{
+#ifdef ENABLE_HP4570
+ if (Test_ViewMapper_t[i].viewId == CCCYCLE) {
+ continue;
+ }
+#endif
buttonconfig.str = Test_ViewMapper_t[i].Labe_str;
buttonconfig.ViewId = Test_ViewMapper_t[i].viewid_str;
buttonconfig.ImageSrc = imagsrc;
buttonconfig.label_y = 0;
buttonconfig.Fontsize = 30;
buttonconfig.ALIGN = TEXT_ALIGNMENT_LEFT;
- button[i] = SetTheButton_ext(buttonconfig, clickListener);
+ button[btnIdx++] = SetTheButton_ext(buttonconfig, clickListener);
buttonconfig.y += interval;
}
+#ifdef ENABLE_HP4570
+ for (int i = 1; i < len; i++) {
+ if (Test_ViewMapper_t[i].viewId != CCCYCLE) {
+ continue;
+ }
+ buttonconfig.str = Test_ViewMapper_t[i].Labe_str;
+ buttonconfig.ViewId = Test_ViewMapper_t[i].viewid_str;
+ buttonconfig.ImageSrc = imagsrc;
+ buttonconfig.label_x = 40;
+ buttonconfig.label_y = 0;
+ buttonconfig.Fontsize = 30;
+ buttonconfig.ALIGN = TEXT_ALIGNMENT_LEFT;
+ button[btnIdx++] = SetTheButton_ext(buttonconfig, clickListener);
+ break;
+ }
+#endif
/* -------------------------------------------------------------------------- */
}
{
if (SystemScrollView[thisView] == nullptr) {
WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "SystemView::ChangeCcCycleTESTView");
+ if (group == nullptr) {
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ChangeCcCycleTESTView: group null, defer slice not ready");
+ return false;
+ }
test_cc_cycle_view = new testCcCycleView();
if (test_cc_cycle_view == nullptr) {
WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "SystemView:: new scrollView[%d] fail", thisView);
return false;
}
- SystemScrollView[thisView] = test_cc_cycle_view->testCcCycleInitIView((uint8_t)thisView);
- group->Add(SystemScrollView[thisView]);
+ SystemScrollView[thisView] = static_cast<UIScrollView *>(
+ test_cc_cycle_view->testCcCycleInitIView((uint8_t)thisView));
+ if (SystemScrollView[thisView] != nullptr) {
+ group->Add(SystemScrollView[thisView]);
+ }
}
return true;
}
// InitIView();
initView_ext(43, 120, 380, 100, 120, "/user/res/SETTING_NEXT.bin");
const power_display_svr_api_t *display_api = power_display_svr_get_api();
- if (display_api->get_screen_state() != SCREEN_ON)
- {
- display_api->turn_on_screen();
+ if (display_api != nullptr) {
+ if (display_api->get_screen_state() != SCREEN_ON) {
+ display_api->turn_on_screen();
+ }
+ display_api->set_auto_timeout_function(false);
}
- display_api->set_auto_timeout_function(false);
uint8_t cnt = pm_veto_get_id_cnt(PM_ID_P_TEST);
WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "cnt : %d\n", cnt);
int len = sizeof(Test_ViewMapper_t) / sizeof(Test_ViewMapper_t[0]);
for (int i = 0; i < len; i++)
{
- SystemScrollView[TESTVIEW]->Add(button[i]);
+ if (button[i] != nullptr) {
+ SystemScrollView[TESTVIEW]->Add(button[i]);
+ }
}
group->Add(SystemScrollView[TESTVIEW]);
AddViewToRootContainer(group);
{
WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "testbuttonView::~testbuttonView");
ClearAll();
- // g_pSystemAboutTheWatchView = nullptr;
+ g_pSystemAboutTheWatchView = nullptr;
}
testbuttonView *testbuttonView::GetInstance()
testCHANINGScrollView->SetTouchable(true);
testCHANINGScrollView->SetDraggable(true);
- labelButton_next= testAboutView::GetInstance()->init_button_next(267,278,Test_ViewMapper_t[id].viewid_str_next);
- testCHANINGScrollView->Add(labelButton_next);
+ if (testView::ShouldShowChainNextButton(id)) {
+ labelButton_next = testAboutView::GetInstance()->init_button_next(
+ 267, 278, Test_ViewMapper_t[id].viewid_str_next);
+ testCHANINGScrollView->Add(labelButton_next);
+ }
UIView::OnDragListener *draglistener = static_cast<UIView::OnDragListener *>(testPresenter::GetInstance());
if (draglistener == nullptr)
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[64]={0};
+ 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);
testCHANINGScrollView->Add(charnge_label);
return SUCCESS;\r
}\r
\r
+bool hp4570n_read_chrg_ichg_mA(uint16_t *current_mA)\r
+{\r
+ uint8_t value;\r
+ uint8_t ichg_code;\r
+\r
+ if (current_mA == NULL) {\r
+ return false;\r
+ }\r
+ (void)hp4570n_read_reg(HP4570N_REG_SYS_CTRL1, &value);\r
+ ichg_code = (uint8_t)((value & HP4570N_ICHG_MASK) >> HP4570N_ICHG_BIT_SHIFT);\r
+ *current_mA = (uint16_t)(ichg_code * HP4570N_ICHG_STEP_mA + HP4570N_ICHG_OFFSET_mA);\r
+ return true;\r
+}\r
+\r
bool hp4570ni_set_chrg_ichg_mA(uint16_t current){\r
uint8_t value;\r
uint8_t ichg_code;\r
bool hp4570n_set_low_chrg_ipre_mA(uint16_t current);\r
bool hp4570n_set_chrg_ichg_mA(uint16_t current);\r
bool hp4570ni_set_chrg_ichg_mA(uint16_t current);\r
+/* 读 SYS_CTRL1 中配置的 ICHG(mA);失败返回 false */\r
+bool hp4570n_read_chrg_ichg_mA(uint16_t *current_mA);\r
bool hp4570n_set_chrg_iterm_mA(uint8_t current);\r
void hp4570n_set_chrg_term_en(bool en);// Termination is allowed or not\r
void hp4570n_set_chrg_term_tmr(bool en);// Termination timer control\r
${ROOT_DIR}/middleware/services/gui/uikit/utils/interfaces/innerkits
${ROOT_DIR}/middleware/services/gui/uikit/utils/interfaces/kits
${ROOT_DIR}/application/wearable/nativeapp/nativeui/heartrate/include
+ ${ROOT_DIR}/zhihui/services/cc_cycle_service
+ ${ROOT_DIR}/zhihui/utils
)
set(SOURCES
batteryservice_set_force_discharge_soc(on);
}
+void battery_seed_cc_discharge_soc_peak(int peak)
+{
+ batteryservice_seed_cc_discharge_soc_peak(peak);
+}
+
+int battery_get_cc_discharge_soc_peak(void)
+{
+ return batteryservice_get_cc_discharge_soc_peak();
+}
+
/* 拷机放电:仍插线但禁充时,按未插线曲线/缓降规则算 SOC(避免 99% 锁死) */
void battery_set_force_discharge_soc(uint8_t on);
+void battery_seed_cc_discharge_soc_peak(int peak);
+int battery_get_cc_discharge_soc_peak(void);
#ifdef __cplusplus
#if __cplusplus
s_cc_burn_discharge_soc_peak = -1;
}
}
+
+void batteryservice_seed_cc_discharge_soc_peak(int peak)
+{
+ if (peak < 0 || peak > 100) {
+ return;
+ }
+ s_cc_burn_discharge_soc_peak = peak;
+ history_percent = peak;
+ cur_percent = (uint8_t)peak;
+}
+
+int batteryservice_get_cc_discharge_soc_peak(void)
+{
+ return s_cc_burn_discharge_soc_peak;
+}
// static float sum_vol = 0;
// static int vol_num=0;
#include "debug_print.h"
+extern uint8_t is_doing_shutdown_or_reboot(void);
+
#ifdef ENABLE_HP4570
#include "hp4570.h"
+#include "cc_cycle_service.h"
extern uint8_t cc_cycle_is_discharge_phase(void);
#elif defined ENABLE_SGM41562
#include "sgm41562.h"
#define BATTERY_CMD_CHARGE_PLUG 0x13
#define BATTERY_CMD_CHARGE_UNPLUG 0x14
#define BATTERY_CMD_FIRST_CHECK 0x15
+#define BATTERY_CMD_CHARGE_RECONFIG 0x19
#define BATTERY_LOWPOWER_ALARM_POWERLEVEL 10
#ifdef BOARD_LEINIAO
#define WDT_KICK_INTERVAL_SEC 30u
static uint8_t g_charge_wdt_sec_counter = 0;
+// #region agent log
+static void dbg_c3c5cc_battery(const char *hypothesisId, const char *location, const char *message,
+ int v1, int v2, int v3)
+{
+ PRINT("[DBG:c3c5cc] {\"sessionId\":\"c3c5cc\",\"hypothesisId\":\"%s\",\"location\":\"%s\","
+ "\"message\":\"%s\",\"data\":{\"v1\":%d,\"v2\":%d,\"v3\":%d},\"timestamp\":0}\n",
+ hypothesisId, location, message, v1, v2, v3);
+}
+// #endregion
static void battery_check_timer_callback(void* data);
errcode_t charger_plug_event_notify(uint8_t plugged);
*/
static uint8_t low_power_flag_1=1;
static uint8_t low_power_flag_2=1;
-static uint16_t last_charge_mA = 0;
+static uint16_t last_charge_mA = 0;
+
+void battery_invalidate_charge_current_cache(void)
+{
+ last_charge_mA = 0;
+}
+
+void battery_request_charge_reconfig(void)
+{
+ battery_invalidate_charge_current_cache();
+ send_battery_svr_cmd_msg(BATTERY_CMD_CHARGE_RECONFIG);
+}
static void battery_check_chargement(void)
{
float voltage;
float bat_temp;
+ if (is_doing_shutdown_or_reboot()) {
+ return;
+ }
#ifdef ENABLE_HP4570
/* 拷机放电:Hi-Z/禁充由 cc_cycle 管;拷机充电:走正常温控与 ICHG 档位 */
if (cc_cycle_is_discharge_phase()) {
// g_chargestatus = 0;
// uapi_poweroff_device();
printf("battery temp is too high or low, device will power off\n");
+ // #region agent log
+ dbg_c3c5cc_battery("A", "battery_check_chargement", "shutdown_prejobs_temp", (int)bat_temp, 0, 0);
+ // #endregion
shutdown_reboot_prejobs(SHUTDOWN_TYPE);
+ return;
}
#ifdef DISABLE_VBUS_CHECK
#endif
if (mail.comm.event == BATTERY_CMD_CHECK_VOLTAGE) {
-
-
+ if (is_doing_shutdown_or_reboot()) {
+ continue;
+ }
// if (get_charge_stat() == 1)
// {
{
battery_check_chargement();
+ }else if (mail.comm.event == BATTERY_CMD_CHARGE_RECONFIG) {
+ battery_invalidate_charge_current_cache();
+ battery_check_chargement();
+ PRINT("[battery] cc_cycle charge reconfig done, ichg=%umA status=%d\r\n",
+ (unsigned int)last_charge_mA, g_chargestatus);
}else if(mail.comm.event == BATTERY_CMD_SAVE_PERCENT)
{
// battery_save_percent();
errcode_t battery_charger_plug_event_callback(uint8_t plugged)
{
-
- PRINT("[battery][service] battery_charger_plug_event_callback plugged=%u \n",plugged);
-
battery_info_t *batinfo = get_current_battery_info();
+ PRINT("[battery][service] battery_charger_plug_event_callback plugged=%u \n",plugged);
+ if (batinfo == NULL) {
+ return ERRCODE_FAIL;
+ }
+
if(plugged == 1){
- // PRINT("[battery][service] battery_charger_plug plugged \n");
send_battery_svr_cmd_msg(BATTERY_CMD_CHARGE_PLUG);
batinfo->usb_plugged = true;
-
+#ifdef ENABLE_HP4570
+ /* VBUS/插线状态已更新后再通知拷机,且不在本线程做 HP4570/UI */
+ cc_cycle_on_charger_plug(1);
+#endif
}else{
- // PRINT("[battery][service] battery_charger_plug unplugged \n");
send_battery_svr_cmd_msg(BATTERY_CMD_CHARGE_UNPLUG);
batinfo->usb_plugged = false;
- }
+ }
return ERRCODE_SUCC;
}
{
charge_ic_init();
+#ifdef ENABLE_HP4570
+ cc_cycle_early_boot_sync();
+#endif
// battery_restore_vol();
battery_check_voltage();
battery_percent_init(0); //先读历史电量
#define BATTERY_CMD_SAVE_PERCENT 0x16
#define BATTERY_CMD_START_AGING_TEST 0x17
#define BATTERY_CMD_STOP_AGING_TEST 0x18
+#define BATTERY_CMD_CHARGE_RECONFIG 0x19
uint8_t batteryservice_get_battery_temp(float *temp);
uint8_t batteryservice_get_pcb_temp(float *temp);
void battery_save_vol(void);
void battery_restore_vol(void);
void batteryservice_set_force_discharge_soc(uint8_t on);
+/* 拷机重启:恢复放电 SOC 峰值,避免表盘与拷机页跳变 */
+void batteryservice_seed_cc_discharge_soc_peak(int peak);
+int batteryservice_get_cc_discharge_soc_peak(void);
+/* 拷机放电→充电:清 ICHG 缓存并异步重配温控档位 */
+void battery_invalidate_charge_current_cache(void);
+void battery_request_charge_reconfig(void);
#ifdef __cplusplus
#if __cplusplus
${ROOT_DIR}/zhihui/drivers/charge/hp4570
${ROOT_DIR}/zhihui/services/gnss/inc
${ROOT_DIR}/application/brandy/brandy_standard
+ ${ROOT_DIR}/application/wearable/fd272/service/power_manager/include
+ ${ROOT_DIR}/middleware/chips/brandy/pm/include
+ ${ROOT_DIR}/middleware/chips/brandy/pm/service
)
set(COMPONENT_CCFLAGS
#include "battery_hal.h"
#include "cmsis_os2.h"
#include "securec.h"
+#include "chip_system_time.h"
#include <stdio.h>
#include <string.h>
+#include <stdarg.h>
+#include <unistd.h>
+#include <time.h>
#ifdef ENABLE_HP4570
#include "hp4570.h"
#include "battery_service.h"
+#include "power_display_service.h"
+#include "pm_definition.h"
+#include "pm_veto.h"
+#include "pm.h"
#endif
#if defined(SUPPORT_GNSS)
#endif
#define CC_CYCLE_MAGIC 0x43434643u /* "CCFC" */
-#define CC_CYCLE_VERSION 2
+#define CC_CYCLE_VERSION 7
#define CC_CYCLE_SOC_FULL 99
#define CC_CYCLE_SOC_LOW 10
+#define CC_CYCLE_SOC_ARM 5
+#define CC_CYCLE_BATV_FORCE_CHARGE 3.3f
+#define CC_CYCLE_DEFECT_SNAPSHOT_MAGIC 0x43434446u /* "CCDF" */
+#define CC_CYCLE_CHARGE_SLOW_SEC 7200u
+#define CC_CYCLE_DEFECT_SOC_STUCK_LO 78u
+#define CC_CYCLE_DEFECT_SOC_STUCK_HI 82u
+#define CC_CYCLE_DEFECT_STUCK_SEC 1800u
+#define CC_CYCLE_DEFECT_SOC_DROP_SEC 300u
+#define CC_CYCLE_DEFECT_DROP_STREAK 3u
+/* chrg_done 仅在 SOC 已接近满时才结束充电,避免 10mA/假 done 跳过黄不良 */
+#define CC_CYCLE_CHG_DONE_MIN_SOC 95u
+/* 配置 ICHG 持续低于此值(mA) 判低电流不良;10mA 机通常读 8~16mA */
+#define CC_CYCLE_DEFECT_LOW_ICHG_MA 20u
+#define CC_CYCLE_DEFECT_LOW_ICHG_SEC 60u
+/* 拷机充电:HW 未进入充电子状态且 SOC 不涨 */
+#define CC_CYCLE_DEFECT_HW_NOCHG_GRACE_SEC 60u
+#define CC_CYCLE_DEFECT_HW_NOCHG_SEC 300u
+#define CC_CYCLE_BOOT_RETRY_MAX 20u
+#define CC_HIST_EVT_MAX 96u
+#define CC_HIST_ROUND_MAX 32u
+/* 旧版路径:只读兼容;仅恢复出厂时 cc_cycle_clear_storage 删除 */
+#define CC_CYCLE_LEGACY_STORE_PATH "user/userdata/cc_cycle.bin"
+#define CC_CYCLE_LEGACY_CHARGE_LOG_PATH "user/userdata/cc_charge_log.txt"
+#define CC_CYCLE_LEGACY_EVENT_LOG_PATH "user/userdata/cc_cycle_event_log.txt"
+#define CC_CYCLE_LEGACY_DEFECT_PATH "user/userdata/cc_cycle_defect.bin"
typedef struct {
uint32_t magic;
uint8_t test_enabled;
uint8_t phase;
uint8_t auto_running;
- uint8_t reserved;
+ uint8_t pending_charge;
+ uint8_t last_soc;
+ uint8_t soc_peak;
uint32_t charge_start_ts;
uint32_t charge_end_ts;
uint16_t round_count;
+ uint32_t burn_start_ts;
+ uint32_t discharge_start_ts;
+ uint32_t discharge_end_ts;
+ uint32_t last_charge_duration_sec;
+ uint32_t last_discharge_duration_sec;
+ uint8_t defect_type;
+ uint8_t defect_alarm_pending;
+ uint16_t reserved;
+ uint32_t defect_ts;
} cc_cycle_store_t;
+/* v6:无 discharge_end_ts */
+typedef struct {
+ uint32_t magic;
+ uint16_t version;
+ uint8_t test_enabled;
+ uint8_t phase;
+ uint8_t auto_running;
+ uint8_t pending_charge;
+ uint8_t last_soc;
+ uint8_t soc_peak;
+ uint32_t charge_start_ts;
+ uint32_t charge_end_ts;
+ uint16_t round_count;
+ uint32_t burn_start_ts;
+ uint32_t discharge_start_ts;
+ uint32_t last_charge_duration_sec;
+ uint32_t last_discharge_duration_sec;
+ uint8_t defect_type;
+ uint8_t defect_alarm_pending;
+ uint16_t reserved;
+ uint32_t defect_ts;
+} cc_cycle_store_v6_t;
+
+/* v5:无不良/时长字段 */
+typedef struct {
+ uint32_t magic;
+ uint16_t version;
+ uint8_t test_enabled;
+ uint8_t phase;
+ uint8_t auto_running;
+ uint8_t pending_charge;
+ uint8_t last_soc;
+ uint8_t soc_peak;
+ uint32_t charge_start_ts;
+ uint32_t charge_end_ts;
+ uint16_t round_count;
+ uint32_t burn_start_ts;
+ uint32_t discharge_start_ts;
+} cc_cycle_store_v5_t;
+
+/* v4 布局(无 burn/discharge start ts),用于 bin 迁移 */
+typedef struct {
+ uint32_t magic;
+ uint16_t version;
+ uint8_t test_enabled;
+ uint8_t phase;
+ uint8_t auto_running;
+ uint8_t pending_charge;
+ uint8_t last_soc;
+ uint8_t soc_peak;
+ uint32_t charge_start_ts;
+ uint32_t charge_end_ts;
+ uint16_t round_count;
+} cc_cycle_store_v4_t;
+
+/* v3 布局(无 pending_charge),用于 bin 迁移 */
+typedef struct {
+ uint32_t magic;
+ uint16_t version;
+ uint8_t test_enabled;
+ uint8_t phase;
+ uint8_t auto_running;
+ uint8_t last_soc;
+ uint8_t soc_peak;
+ uint32_t charge_start_ts;
+ uint32_t charge_end_ts;
+ uint16_t round_count;
+} cc_cycle_store_v3_t;
+
static cc_cycle_store_t g_store;
static uint8_t g_task_running;
static uint8_t g_lcd_on;
static uint8_t g_gps_on;
static uint8_t g_pending_hide_burn_ui;
+/* 插线回调在 battery 线程,勿在其内 resume/HP4570/UI,改由 cc_cycle_task 处理 */
+static uint8_t g_pending_plug_resume;
+static uint8_t g_store_dirty;
+static uint8_t g_store_snap_pending;
+static cc_cycle_store_t g_store_snap;
+static uint8_t g_discharge_screen_hold;
+static uint8_t g_phase_restore;
static osMutexId_t g_mutex;
+/* 充电不良检测运行时状态 */
+static uint8_t g_defect_prev_soc;
+static uint8_t g_defect_drop_streak;
+static uint32_t g_defect_soc_drop_base_ts;
+static uint8_t g_defect_soc_drop_base_soc;
+static uint32_t g_defect_stuck_since_ts;
+static uint8_t g_defect_stuck_max_soc;
+static uint32_t g_defect_low_ichg_since_ts;
+static uint32_t g_defect_hw_nochg_since_ts;
+static uint8_t g_defect_hw_nochg_base_soc;
+static osTimerId_t g_boot_retry_timer;
+static uint8_t g_boot_retry_count;
+static uint8_t g_burn_ui_boot_armed;
+static uint8_t g_boot_vbus_wait;
+static uint8_t g_pending_save_time;
+static volatile uint8_t g_pending_charge_reconfig;
+
+static void cc_schedule_charge_reconfig(void)
+{
+#ifdef ENABLE_HP4570
+ g_pending_charge_reconfig = 1;
+#endif
+}
+
+static void cc_flush_pending_charge_reconfig(void)
+{
+#ifdef ENABLE_HP4570
+ if (g_pending_charge_reconfig) {
+ g_pending_charge_reconfig = 0;
+ battery_request_charge_reconfig();
+ }
+#endif
+}
+
static void cc_lock(void)
{
if (g_mutex != NULL) {
}
}
+static uint8_t cc_cycle_resume_automation_locked(void);
+
+static uint8_t cc_cycle_is_active_hw_locked(void)
+{
+ return (g_store.test_enabled != 0 && g_store.auto_running != 0) ? 1 : 0;
+}
+
+static void cc_store_sanitize_after_load(void)
+{
+ if (g_store.test_enabled == 0) {
+ g_store.auto_running = 0;
+ g_store.phase = (uint8_t)CC_PHASE_IDLE;
+#ifdef SUPPORT_BATTERY_SERVICE
+ battery_set_force_discharge_soc(0);
+#endif
+ }
+}
+
static void cc_ensure_userdata_dir(void)
{
static uint8_t s_dir_ready;
}
uapi_create_folder((char *)"user");
uapi_create_folder((char *)"user/userdata");
+ uapi_create_folder((char *)CC_CYCLE_USERDATA_DIR);
s_dir_ready = 1;
}
+static void cc_store_mark_dirty(void);
+static void cc_store_snap_locked(void);
+static void cc_store_write_pending(void);
+static void cc_store_flush(void);
+static void cc_store_mark_persist_locked(void);
+static void cc_log_event_persist_locked(const char *event, uint32_t ts, uint8_t soc);
+static void cc_file_append_sync(const char *path, const char *line);
+static void cc_log_event(const char *event, uint32_t ts, uint8_t soc);
+
+static void cc_backfill_burn_start_ts(void)
+{
+ if (g_store.auto_running && g_store.burn_start_ts == 0) {
+ if (g_store.charge_start_ts != 0) {
+ g_store.burn_start_ts = g_store.charge_start_ts;
+ } else {
+ uint32_t now = 0;
+ (void)uapi_get_timestampe_ss(&now);
+ g_store.burn_start_ts = now;
+ }
+ }
+}
+
+static void cc_purge_legacy_storage(void)
+{
+ (void)uapi_delete_files(CC_CYCLE_LEGACY_STORE_PATH);
+ (void)uapi_delete_files(CC_CYCLE_LEGACY_CHARGE_LOG_PATH);
+ (void)uapi_delete_files(CC_CYCLE_LEGACY_EVENT_LOG_PATH);
+ (void)uapi_delete_files(CC_CYCLE_LEGACY_DEFECT_PATH);
+}
+
+static void cc_store_get_file(const char *path, uint8_t *buf, uint16_t *len)
+{
+ if (path == NULL || buf == NULL || len == NULL) {
+ return;
+ }
+ uapi_get_file_data((char *)path, (uint8_t *)buf, len);
+}
+
+static const char *cc_store_pick_load_path(void)
+{
+ cc_cycle_store_v4_t probe = {0};
+ uint16_t len = (uint16_t)sizeof(probe);
+
+ cc_store_get_file(CC_CYCLE_STORE_PATH, (uint8_t *)&probe, &len);
+ if (probe.magic == CC_CYCLE_MAGIC) {
+ return CC_CYCLE_STORE_PATH;
+ }
+ len = (uint16_t)sizeof(probe);
+ cc_store_get_file(CC_CYCLE_LEGACY_STORE_PATH, (uint8_t *)&probe, &len);
+ if (probe.magic == CC_CYCLE_MAGIC) {
+ return CC_CYCLE_LEGACY_STORE_PATH;
+ }
+ return CC_CYCLE_STORE_PATH;
+}
+
static void cc_store_load(void)
{
- uint16_t len = sizeof(g_store);
+ uint16_t len;
+ cc_cycle_store_v4_t v4 = {0};
+ cc_cycle_store_v3_t v3 = {0};
+ const char *load_path = cc_store_pick_load_path();
+
memset(&g_store, 0, sizeof(g_store));
- uapi_get_file_data((char *)CC_CYCLE_STORE_PATH, (uint8_t *)&g_store, &len);
- if (g_store.magic != CC_CYCLE_MAGIC) {
- memset(&g_store, 0, sizeof(g_store));
+ len = (uint16_t)sizeof(v4);
+ cc_store_get_file(load_path, (uint8_t *)&v4, &len);
+ if (v4.magic != CC_CYCLE_MAGIC) {
g_store.magic = CC_CYCLE_MAGIC;
g_store.version = CC_CYCLE_VERSION;
g_store.phase = (uint8_t)CC_PHASE_IDLE;
+ cc_store_sanitize_after_load();
+ return;
+ }
+ if (v4.version >= CC_CYCLE_VERSION) {
+ len = (uint16_t)sizeof(g_store);
+ cc_store_get_file(load_path, (uint8_t *)&g_store, &len);
+ if (g_store.magic != CC_CYCLE_MAGIC) {
+ memset(&g_store, 0, sizeof(g_store));
+ g_store.magic = CC_CYCLE_MAGIC;
+ g_store.version = CC_CYCLE_VERSION;
+ g_store.phase = (uint8_t)CC_PHASE_IDLE;
+ }
+ cc_store_sanitize_after_load();
+ return;
+ }
+ if (v4.version == 6) {
+ cc_cycle_store_v6_t v6 = {0};
+
+ len = (uint16_t)sizeof(v6);
+ cc_store_get_file(load_path, (uint8_t *)&v6, &len);
+ g_store.magic = CC_CYCLE_MAGIC;
+ g_store.version = CC_CYCLE_VERSION;
+ g_store.test_enabled = v6.test_enabled;
+ g_store.phase = v6.phase;
+ g_store.auto_running = v6.auto_running;
+ g_store.pending_charge = v6.pending_charge;
+ g_store.last_soc = v6.last_soc;
+ g_store.soc_peak = v6.soc_peak;
+ g_store.charge_start_ts = v6.charge_start_ts;
+ g_store.charge_end_ts = v6.charge_end_ts;
+ g_store.round_count = v6.round_count;
+ g_store.burn_start_ts = v6.burn_start_ts;
+ g_store.discharge_start_ts = v6.discharge_start_ts;
+ g_store.discharge_end_ts = 0;
+ g_store.last_charge_duration_sec = v6.last_charge_duration_sec;
+ g_store.last_discharge_duration_sec = v6.last_discharge_duration_sec;
+ g_store.defect_type = v6.defect_type;
+ g_store.defect_alarm_pending = v6.defect_alarm_pending;
+ g_store.defect_ts = v6.defect_ts;
+ cc_store_mark_dirty();
+ cc_store_sanitize_after_load();
+ return;
+ }
+ if (v4.version == 5) {
+ cc_cycle_store_v5_t v5 = {0};
+ len = (uint16_t)sizeof(v5);
+ cc_store_get_file(load_path, (uint8_t *)&v5, &len);
+ g_store.magic = CC_CYCLE_MAGIC;
+ g_store.version = CC_CYCLE_VERSION;
+ g_store.test_enabled = v5.test_enabled;
+ g_store.phase = v5.phase;
+ g_store.auto_running = v5.auto_running;
+ g_store.pending_charge = v5.pending_charge;
+ g_store.last_soc = v5.last_soc;
+ g_store.soc_peak = v5.soc_peak;
+ g_store.charge_start_ts = v5.charge_start_ts;
+ g_store.charge_end_ts = v5.charge_end_ts;
+ g_store.round_count = v5.round_count;
+ g_store.burn_start_ts = v5.burn_start_ts;
+ g_store.discharge_start_ts = v5.discharge_start_ts;
+ g_store.discharge_end_ts = 0;
+ g_store.last_charge_duration_sec = 0;
+ g_store.last_discharge_duration_sec = 0;
+ g_store.defect_type = (uint8_t)CC_DEFECT_NONE;
+ g_store.defect_alarm_pending = 0;
+ g_store.defect_ts = 0;
+ cc_store_mark_dirty();
+ cc_store_sanitize_after_load();
+ return;
+ }
+ if (v4.version == 4) {
+ g_store.magic = CC_CYCLE_MAGIC;
+ g_store.version = CC_CYCLE_VERSION;
+ g_store.test_enabled = v4.test_enabled;
+ g_store.phase = v4.phase;
+ g_store.auto_running = v4.auto_running;
+ g_store.pending_charge = v4.pending_charge;
+ g_store.last_soc = v4.last_soc;
+ g_store.soc_peak = v4.soc_peak;
+ g_store.charge_start_ts = v4.charge_start_ts;
+ g_store.charge_end_ts = v4.charge_end_ts;
+ g_store.round_count = v4.round_count;
+ g_store.burn_start_ts = 0;
+ g_store.discharge_start_ts = 0;
+ g_store.discharge_end_ts = 0;
+ cc_backfill_burn_start_ts();
+ cc_store_mark_dirty();
+ cc_store_sanitize_after_load();
+ return;
+ }
+ /* v2/v3 → v7 */
+ len = (uint16_t)sizeof(v3);
+ cc_store_get_file(load_path, (uint8_t *)&v3, &len);
+ g_store.magic = CC_CYCLE_MAGIC;
+ g_store.version = CC_CYCLE_VERSION;
+ g_store.test_enabled = v3.test_enabled;
+ g_store.phase = v3.phase;
+ g_store.auto_running = v3.auto_running;
+ g_store.pending_charge = 0;
+ if (v3.version == 2 && len >= 16) {
+ g_store.last_soc = v3.last_soc;
+ g_store.soc_peak = 0;
+ } else {
+ g_store.last_soc = v3.last_soc;
+ g_store.soc_peak = v3.soc_peak;
+ }
+ g_store.charge_start_ts = v3.charge_start_ts;
+ g_store.charge_end_ts = v3.charge_end_ts;
+ g_store.round_count = v3.round_count;
+ g_store.burn_start_ts = 0;
+ g_store.discharge_start_ts = 0;
+ g_store.discharge_end_ts = 0;
+ cc_backfill_burn_start_ts();
+ cc_store_mark_dirty();
+ cc_store_sanitize_after_load();
+}
+
+static void cc_clear_pending_charge(void)
+{
+ g_store.pending_charge = 0;
+}
+
+/* 掉电后开机:低电必须先充电(pending / last_soc / 实时 SOC / 电压) */
+static uint8_t cc_should_force_charge(uint8_t soc, float bat_v)
+{
+ if (g_store.pending_charge != 0) {
+ return 1;
+ }
+ if (g_store.phase != (uint8_t)CC_PHASE_DISCHARGE) {
+ return 0;
+ }
+ if (g_store.last_soc > 0 && g_store.last_soc <= CC_CYCLE_SOC_LOW) {
+ return 1;
+ }
+ if (soc <= CC_CYCLE_SOC_LOW) {
+ return 1;
+ }
+ if (bat_v > 0.0f && bat_v <= CC_CYCLE_BATV_FORCE_CHARGE) {
+ return 1;
}
+ return 0;
}
-static void cc_store_save(void)
+static void cc_store_mark_dirty(void)
{
+ g_store_dirty = 1;
+}
+
+/* 调用方已 cc_lock:把当前 g_store 拷到待写缓冲 */
+static void cc_store_snap_locked(void)
+{
+ if (g_store_dirty == 0) {
+ return;
+ }
g_store.magic = CC_CYCLE_MAGIC;
g_store.version = CC_CYCLE_VERSION;
- uapi_save_file_data((char *)CC_CYCLE_STORE_PATH, (uint8_t *)&g_store,
- (uint16_t)sizeof(g_store), "wb");
+ if (memcpy_s(&g_store_snap, sizeof(g_store_snap), &g_store, sizeof(g_store)) != EOK) {
+ return;
+ }
+ g_store_dirty = 0;
+ g_store_snap_pending = 1;
+}
+
+static void cc_store_mark_persist_locked(void)
+{
+ cc_store_mark_dirty();
+ cc_store_snap_locked();
+}
+
+/* 锁外:把快照 fsync 写入 cc_cycle.bin */
+static void cc_store_write_pending(void)
+{
+ FILE *fp;
+
+ if (g_store_snap_pending == 0) {
+ return;
+ }
+ cc_ensure_userdata_dir();
+ fp = fopen(CC_CYCLE_STORE_PATH, "wb");
+ if (fp == NULL) {
+ return;
+ }
+ if (fwrite(&g_store_snap, 1, sizeof(g_store_snap), fp) != sizeof(g_store_snap)) {
+ (void)fclose(fp);
+ return;
+ }
+ (void)fflush(fp);
+ (void)fsync(fileno(fp));
+ (void)fclose(fp);
+ g_store_snap_pending = 0;
+}
+
+/* 持锁期间仅标脏;快照在锁内、落盘在锁外,避免死锁 */
+static void cc_store_flush(void)
+{
+ cc_lock();
+ cc_store_snap_locked();
+ cc_unlock();
+ cc_store_write_pending();
+}
+
+static void cc_file_append_sync(const char *path, const char *line)
+{
+ FILE *fp;
+ size_t len;
+
+ if (path == NULL || line == NULL) {
+ return;
+ }
+ cc_ensure_userdata_dir();
+ fp = fopen(path, "a");
+ if (fp == NULL) {
+ return;
+ }
+ len = strlen(line);
+ if (len > 0) {
+ (void)fwrite(line, 1, len, fp);
+ }
+ (void)fflush(fp);
+ (void)fsync(fileno(fp));
+ (void)fclose(fp);
+}
+
+static void cc_log_event_persist_locked(const char *event, uint32_t ts, uint8_t soc)
+{
+ cc_log_event(event, ts, soc);
+ cc_store_mark_persist_locked();
}
static void cc_sync_discharge_soc_mode(void)
{
#ifdef SUPPORT_BATTERY_SERVICE
- if (g_store.auto_running && g_store.phase == (uint8_t)CC_PHASE_DISCHARGE) {
+ if (cc_cycle_is_active_hw_locked() && g_store.phase == (uint8_t)CC_PHASE_DISCHARGE) {
battery_set_force_discharge_soc(1);
} else {
battery_set_force_discharge_soc(0);
#endif
}
+static void cc_restore_discharge_soc_peak(void)
+{
+#ifdef SUPPORT_BATTERY_SERVICE
+ int seed = -1;
+
+ if (!cc_cycle_is_active_hw_locked()) {
+ return;
+ }
+
+ if (g_store.soc_peak > 0) {
+ seed = g_store.soc_peak;
+ } else if (g_store.last_soc > 0) {
+ seed = g_store.last_soc;
+ }
+ if (seed > 0) {
+ battery_set_force_discharge_soc(1);
+ battery_seed_cc_discharge_soc_peak(seed);
+ }
+#else
+ (void)0;
+#endif
+}
+
+static void cc_update_stored_soc(uint8_t soc)
+{
+ g_store.last_soc = soc;
+#ifdef SUPPORT_BATTERY_SERVICE
+ if (g_store.phase == (uint8_t)CC_PHASE_DISCHARGE) {
+ int peak = battery_get_cc_discharge_soc_peak();
+ if (peak >= 0 && peak <= 100) {
+ g_store.soc_peak = (uint8_t)peak;
+ }
+ }
+#else
+ (void)0;
+#endif
+}
+
static uint8_t cc_read_soc(float *voltage)
{
float v = 0.0f;
static void cc_set_white_screen(uint8_t on)
{
- /* 白屏与常亮由 VIEW_CC_CYCLE_BURN 界面负责 */
+ /* 白屏标志;放电常亮由 cc_set_discharge_screen_hold 负责 */
if (on) {
g_lcd_on = 1;
} else if (g_lcd_on) {
}
}
+#ifdef ENABLE_HP4570
+/* 拷机运行中:充/放电均常亮 + 禁灭屏超时;停止拷机时恢复 */
+static void cc_set_discharge_screen_hold(uint8_t on)
+{
+ const power_display_svr_api_t *display_api = power_display_svr_get_api();
+
+ if (on) {
+ if (g_discharge_screen_hold) {
+ return;
+ }
+ if (display_api != NULL) {
+ if (display_api->get_screen_state() != SCREEN_ON) {
+ display_api->turn_on_screen();
+ }
+ display_api->set_auto_timeout_function(false);
+ }
+ uapi_setScreenBrightness(255);
+ if (pm_veto_get_id_cnt(PM_ID_P_TEST) == 0) {
+ (void)uapi_pm_add_sleep_veto(PM_ID_P_TEST);
+ }
+ g_discharge_screen_hold = 1;
+ return;
+ }
+ if (!g_discharge_screen_hold) {
+ return;
+ }
+ if (display_api != NULL) {
+ display_api->set_auto_timeout_function(true);
+ }
+ if (pm_veto_get_id_cnt(PM_ID_P_TEST) == 1) {
+ (void)uapi_pm_remove_sleep_veto(PM_ID_P_TEST);
+ }
+ g_discharge_screen_hold = 0;
+}
+#else
+static void cc_set_discharge_screen_hold(uint8_t on)
+{
+ (void)on;
+}
+#endif
+
static void cc_set_gps(uint8_t on)
{
#if defined(SUPPORT_GNSS)
static void cc_append_charge_log(uint32_t t_start, uint32_t t_end, uint8_t soc_end)
{
- FILE *fp;
char line[96];
- cc_ensure_userdata_dir();
+
(void)snprintf_s(line, sizeof(line), sizeof(line) - 1,
"start=%u end=%u soc_end=%u round=%u\n",
(unsigned int)t_start, (unsigned int)t_end, (unsigned int)soc_end,
(unsigned int)g_store.round_count);
- fp = fopen(CC_CYCLE_CHARGE_LOG_PATH, "a");
- if (fp == NULL) {
- return;
- }
- (void)fwrite(line, 1, strlen(line), fp);
- (void)fclose(fp);
+ cc_file_append_sync(CC_CYCLE_CHARGE_LOG_PATH, line);
}
-static void cc_enter_discharge_phase(void)
+static void cc_log_event(const char *event, uint32_t ts, uint8_t soc)
{
- g_store.phase = (uint8_t)CC_PHASE_DISCHARGE;
- g_store.charge_start_ts = 0;
- g_store.charge_end_ts = 0;
- cc_set_charge_enable(0);
- cc_sync_discharge_soc_mode();
- if (g_store.auto_running) {
- cc_set_white_screen(1);
- cc_set_gps(1);
+ char line[128];
+ char time_str[32];
+
+ if (event == NULL) {
+ return;
+ }
+ cc_ensure_userdata_dir();
+ memset_s(time_str, sizeof(time_str), 0, sizeof(time_str));
+ if (ts == 0U) {
+ (void)snprintf_s(time_str, sizeof(time_str), sizeof(time_str) - 1, "未校时");
+ } else if (uapi_get_TIME_STR(ts, time_str, '.', ':', DATE_H_M_S) != ERRCODE_SUCC) {
+ (void)snprintf_s(time_str, sizeof(time_str), sizeof(time_str) - 1, "unknown");
}
+ PRINT("[cc_cycle] %s ts=%u time=%s soc=%u round=%u\n",
+ event, (unsigned int)ts, time_str, (unsigned int)soc,
+ (unsigned int)g_store.round_count);
+ (void)snprintf_s(line, sizeof(line), sizeof(line) - 1,
+ "event=%s ts=%u time=%s soc=%u round=%u\n",
+ event, (unsigned int)ts, time_str, (unsigned int)soc,
+ (unsigned int)g_store.round_count);
+ cc_file_append_sync(CC_CYCLE_EVENT_LOG_PATH, line);
}
-static void cc_enter_charging_phase(void)
+static void cc_clear_start_timestamps(void)
{
- uint32_t now = 0;
- (void)uapi_get_timestampe_ss(&now);
- g_store.phase = (uint8_t)CC_PHASE_CHARGING;
- g_store.charge_start_ts = now;
- g_store.charge_end_ts = 0;
- cc_set_white_screen(0);
- cc_set_charge_enable(1);
- cc_sync_discharge_soc_mode();
- if (g_store.auto_running) {
- cc_set_gps(1);
- }
+ g_store.burn_start_ts = 0;
+ g_store.charge_start_ts = 0;
+ g_store.discharge_start_ts = 0;
+ g_store.discharge_end_ts = 0;
}
-static void cc_finish_charging_phase(uint8_t soc)
+static void cc_reset_defect_detect_state(void)
{
- uint32_t now = 0;
- (void)uapi_get_timestampe_ss(&now);
- g_store.charge_end_ts = now;
- g_store.round_count++;
- cc_append_charge_log(g_store.charge_start_ts, g_store.charge_end_ts, soc);
- cc_enter_discharge_phase();
+ g_defect_prev_soc = 0;
+ g_defect_drop_streak = 0;
+ g_defect_soc_drop_base_ts = 0;
+ g_defect_soc_drop_base_soc = 0;
+ g_defect_stuck_since_ts = 0;
+ g_defect_stuck_max_soc = 0;
+ g_defect_low_ichg_since_ts = 0;
+ g_defect_hw_nochg_since_ts = 0;
+ g_defect_hw_nochg_base_soc = 0;
}
-static void cc_finish_discharge_phase(uint8_t soc)
+static void cc_format_hms(uint32_t sec, char *buf, unsigned int buf_size)
{
- (void)soc;
- if (!cc_is_plugged()) {
- return;
+ uint32_t h;
+ uint32_t m;
+ uint32_t s;
+
+ if (buf == NULL || buf_size == 0) {
+ return;
+ }
+ if (sec == 0) {
+ (void)snprintf_s(buf, buf_size, buf_size - 1, "--:--:--");
+ return;
+ }
+ h = sec / 3600u;
+ m = (sec % 3600u) / 60u;
+ s = sec % 60u;
+ (void)snprintf_s(buf, buf_size, buf_size - 1, "%02u:%02u:%02u",
+ (unsigned int)h, (unsigned int)m, (unsigned int)s);
+}
+
+static uint32_t cc_phase_duration_sec(uint32_t start_ts, uint32_t now)
+{
+ if (start_ts == 0 || now <= start_ts) {
+ return 0;
+ }
+ return now - start_ts;
+}
+
+static uint8_t cc_read_charge_ic_id(void)
+{
+#ifdef ENABLE_HP4570
+ uint8_t id = hp4570n_get_id();
+ return (id == 0x2) ? 1 : 0;
+#else
+ return 0;
+#endif
+}
+
+static void cc_fill_ic_status_fields(float *voltage, float *temp, uint8_t *charge_id,
+ uint8_t *vbus_pg, uint8_t *chrg_fast, uint8_t *chrg_done)
+{
+ float v = 0.0f;
+ float t = 0.0f;
+
+ if (voltage != NULL) {
+ *voltage = 0.0f;
+ }
+ if (temp != NULL) {
+ *temp = 0.0f;
+ }
+#ifdef SUPPORT_BATTERY_SERVICE
+ (void)battery_get_voltage(&v);
+ (void)battery_get_battery_temp(&t);
+#endif
+ if (voltage != NULL) {
+ *voltage = v;
+ }
+ if (temp != NULL) {
+ *temp = t;
+ }
+ if (charge_id != NULL) {
+ *charge_id = cc_read_charge_ic_id();
+ }
+#ifdef ENABLE_HP4570
+ if (vbus_pg != NULL) {
+ *vbus_pg = hp4570n_read_status_vbus_pg() ? 1 : 0;
+ }
+ if (chrg_fast != NULL) {
+ *chrg_fast = hp4570n_read_chrg_stat_fast_chrg() ? 1 : 0;
+ }
+ if (chrg_done != NULL) {
+ *chrg_done = hp4570n_read_chrg_stat_chrg_done() ? 1 : 0;
+ }
+#else
+ if (vbus_pg != NULL) {
+ *vbus_pg = 0;
+ }
+ if (chrg_fast != NULL) {
+ *chrg_fast = 0;
+ }
+ if (chrg_done != NULL) {
+ *chrg_done = 0;
+ }
+#endif
+}
+
+static void cc_capture_defect_snapshot(uint8_t defect_type, uint8_t soc, uint32_t now)
+{
+ cc_cycle_defect_snapshot_t snap = {0};
+ float voltage = 0.0f;
+ float temp = 0.0f;
+ uint8_t charge_id = 0;
+ uint8_t vbus_pg = 0;
+ uint8_t chrg_fast = 0;
+ uint8_t chrg_done = 0;
+ const char *defect_name = "unknown";
+
+ snap.magic = CC_CYCLE_DEFECT_SNAPSHOT_MAGIC;
+ snap.defect_type = defect_type;
+ snap.soc = soc;
+ snap.round = g_store.round_count;
+ snap.ts = now;
+ snap.phase = g_store.phase;
+ if (g_store.charge_start_ts != 0 && now > g_store.charge_start_ts) {
+ snap.charge_elapsed_sec = now - g_store.charge_start_ts;
+ }
+ cc_fill_ic_status_fields(&voltage, &temp, &charge_id, &vbus_pg, &chrg_fast, &chrg_done);
+ snap.voltage = voltage;
+ snap.temp = temp;
+ snap.charge_id = charge_id;
+ snap.vbus_pg = vbus_pg;
+ snap.chrg_fast = chrg_fast;
+ snap.chrg_done = chrg_done;
+#ifdef ENABLE_HP4570
+ (void)hp4570n_dump_debug_registers(snap.reg_dump, sizeof(snap.reg_dump));
+#endif
+ cc_ensure_userdata_dir();
+ uapi_save_file_data((char *)CC_CYCLE_DEFECT_SNAPSHOT_PATH, (uint8_t *)&snap,
+ (uint16_t)sizeof(snap), "wb");
+
+ if (defect_type == (uint8_t)CC_DEFECT_RED) {
+ defect_name = "defect_red_soc_drop";
+ } else if (defect_type == (uint8_t)CC_DEFECT_YELLOW) {
+ defect_name = "defect_yellow_slow_charge";
+ } else if (defect_type == (uint8_t)CC_DEFECT_BLUE) {
+ defect_name = "defect_blue_soc_stuck80";
+ } else if (defect_type == (uint8_t)CC_DEFECT_LOW_ICHG) {
+ defect_name = "defect_low_ichg";
+ } else if (defect_type == (uint8_t)CC_DEFECT_HW_NO_CHG) {
+ defect_name = "defect_hw_no_chg";
+ }
+ cc_log_event_persist_locked(defect_name, now, soc);
+}
+
+#ifdef ENABLE_HP4570
+static uint8_t cc_hw_is_actively_charging(void)
+{
+ if (hp4570n_read_chrg_stat_fast_chrg() || hp4570n_read_chrg_stat_pre_chrg() ||
+ hp4570n_read_chrg_stat_dead_chrg()) {
+ return 1;
+ }
+ return 0;
+}
+
+static uint8_t cc_should_finish_charging(uint8_t soc)
+{
+ if (soc >= CC_CYCLE_SOC_FULL) {
+ return 1;
+ }
+ if (hp4570n_read_chrg_stat_chrg_done() && soc >= CC_CYCLE_CHG_DONE_MIN_SOC) {
+ return 1;
+ }
+ return 0;
+}
+
+static uint16_t cc_read_configured_ichg_mA(void)
+{
+ uint16_t ichg = 0;
+
+ if (hp4570n_read_chrg_ichg_mA(&ichg)) {
+ return ichg;
+ }
+ return 0;
+}
+#else
+static uint8_t cc_should_finish_charging(uint8_t soc)
+{
+ return (soc >= CC_CYCLE_SOC_FULL) ? 1 : 0;
+}
+#endif
+
+static uint8_t cc_eval_charge_defect(uint8_t soc, uint32_t now)
+{
+ uint8_t defect = (uint8_t)CC_DEFECT_NONE;
+
+ if (g_defect_prev_soc > 0 && soc < g_defect_prev_soc) {
+ g_defect_drop_streak++;
+ } else if (soc >= g_defect_prev_soc) {
+ g_defect_drop_streak = 0;
+ }
+ g_defect_prev_soc = soc;
+
+ if (g_defect_drop_streak >= CC_CYCLE_DEFECT_DROP_STREAK) {
+ defect = (uint8_t)CC_DEFECT_RED;
+ }
+
+ if (g_defect_soc_drop_base_ts == 0) {
+ g_defect_soc_drop_base_ts = now;
+ g_defect_soc_drop_base_soc = soc;
+ } else if ((now - g_defect_soc_drop_base_ts) >= CC_CYCLE_DEFECT_SOC_DROP_SEC) {
+ if (g_defect_soc_drop_base_soc > soc &&
+ (g_defect_soc_drop_base_soc - soc) >= 1) {
+ defect = (uint8_t)CC_DEFECT_RED;
+ }
+ g_defect_soc_drop_base_ts = now;
+ g_defect_soc_drop_base_soc = soc;
+ }
+
+#ifdef ENABLE_HP4570
+ if (defect == (uint8_t)CC_DEFECT_NONE && soc < CC_CYCLE_SOC_FULL) {
+ uint8_t hw_chg = cc_hw_is_actively_charging();
+ uint32_t chg_elapsed = 0;
+
+ if (g_store.charge_start_ts != 0 && now > g_store.charge_start_ts) {
+ chg_elapsed = now - g_store.charge_start_ts;
+ }
+ if (hw_chg) {
+ g_defect_hw_nochg_since_ts = 0;
+ g_defect_hw_nochg_base_soc = 0;
+ } else if (chg_elapsed >= CC_CYCLE_DEFECT_HW_NOCHG_GRACE_SEC) {
+ if (g_defect_hw_nochg_since_ts == 0) {
+ g_defect_hw_nochg_since_ts = now;
+ g_defect_hw_nochg_base_soc = soc;
+ } else if (soc > g_defect_hw_nochg_base_soc) {
+ g_defect_hw_nochg_since_ts = now;
+ g_defect_hw_nochg_base_soc = soc;
+ } else if ((now - g_defect_hw_nochg_since_ts) >= CC_CYCLE_DEFECT_HW_NOCHG_SEC) {
+ defect = (uint8_t)CC_DEFECT_HW_NO_CHG;
+ }
+ }
+ } else if (defect == (uint8_t)CC_DEFECT_NONE) {
+ g_defect_hw_nochg_since_ts = 0;
+ g_defect_hw_nochg_base_soc = 0;
+ }
+
+ {
+ uint16_t ichg = cc_read_configured_ichg_mA();
+ uint8_t charging_hw = cc_hw_is_actively_charging();
+
+ if (defect == (uint8_t)CC_DEFECT_NONE && charging_hw && ichg > 0 &&
+ ichg <= CC_CYCLE_DEFECT_LOW_ICHG_MA) {
+ if (g_defect_low_ichg_since_ts == 0) {
+ g_defect_low_ichg_since_ts = now;
+ } else if ((now - g_defect_low_ichg_since_ts) >= CC_CYCLE_DEFECT_LOW_ICHG_SEC) {
+ defect = (uint8_t)CC_DEFECT_LOW_ICHG;
+ }
+ } else if (defect == (uint8_t)CC_DEFECT_NONE) {
+ g_defect_low_ichg_since_ts = 0;
+ }
+ }
+#endif
+
+ if (g_store.charge_start_ts != 0 &&
+ (now - g_store.charge_start_ts) > CC_CYCLE_CHARGE_SLOW_SEC &&
+ soc < CC_CYCLE_SOC_FULL) {
+ if (defect == (uint8_t)CC_DEFECT_NONE) {
+ defect = (uint8_t)CC_DEFECT_YELLOW;
+ }
+ }
+
+ if (soc >= CC_CYCLE_DEFECT_SOC_STUCK_LO && soc <= CC_CYCLE_DEFECT_SOC_STUCK_HI) {
+ if (g_defect_stuck_since_ts == 0) {
+ g_defect_stuck_since_ts = now;
+ g_defect_stuck_max_soc = soc;
+ } else {
+ if (soc > g_defect_stuck_max_soc) {
+ g_defect_stuck_max_soc = soc;
+ }
+ if ((now - g_defect_stuck_since_ts) >= CC_CYCLE_DEFECT_STUCK_SEC &&
+ g_defect_stuck_max_soc <= CC_CYCLE_DEFECT_SOC_STUCK_HI) {
+ if (defect == (uint8_t)CC_DEFECT_NONE) {
+ defect = (uint8_t)CC_DEFECT_BLUE;
+ }
+ }
+ }
+ } else {
+ g_defect_stuck_since_ts = 0;
+ g_defect_stuck_max_soc = 0;
+ }
+
+ return defect;
+}
+
+static void cc_apply_charge_defect(uint8_t soc, uint32_t now)
+{
+ uint8_t defect = cc_eval_charge_defect(soc, now);
+
+ if (defect == (uint8_t)CC_DEFECT_NONE) {
+ g_store.defect_type = (uint8_t)CC_DEFECT_NONE;
+ return;
+ }
+ if (defect == g_store.defect_type) {
+ return;
+ }
+ g_store.defect_type = defect;
+ g_store.defect_ts = now;
+ g_store.defect_alarm_pending = 1;
+ cc_capture_defect_snapshot(defect, soc, now);
+}
+
+static void cc_save_discharge_duration(uint32_t now)
+{
+ if (g_store.discharge_start_ts != 0 && now > g_store.discharge_start_ts) {
+ g_store.last_discharge_duration_sec = now - g_store.discharge_start_ts;
+ }
+}
+
+/* 持锁调用:低电落盘 discharge_end,phase 仍为 DISCHARGE */
+static void cc_finish_discharge_phase(uint32_t now, uint8_t soc)
+{
+ if (g_store.discharge_start_ts == 0) {
+ return;
+ }
+ 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_enter_charging_phase();
+ g_store.discharge_start_ts = 0;
+ g_pending_save_time = 1;
+}
+
+static uint32_t cc_current_discharge_duration_sec_locked(uint32_t now)
+{
+ if (g_store.phase == (uint8_t)CC_PHASE_DISCHARGE &&
+ g_store.discharge_start_ts == 0 &&
+ g_store.last_discharge_duration_sec > 0) {
+ return g_store.last_discharge_duration_sec;
+ }
+ return cc_phase_duration_sec(g_store.discharge_start_ts, now);
+}
+
+static void cc_enter_discharge_phase(void)
+{
+ uint32_t now = 0;
+ uint8_t soc = 0;
+
+ cc_clear_pending_charge();
+ g_store.phase = (uint8_t)CC_PHASE_DISCHARGE;
+ g_store.charge_start_ts = 0;
+ g_store.charge_end_ts = 0;
+ g_store.discharge_end_ts = 0;
+ if (!g_phase_restore) {
+ (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);
+ } else if (g_store.discharge_start_ts == 0) {
+ (void)uapi_get_timestampe_ss(&now);
+ g_store.discharge_start_ts = now;
+ }
+ cc_set_charge_enable(0);
+ cc_sync_discharge_soc_mode();
+ if (g_store.auto_running) {
+ cc_set_white_screen(1);
+ cc_set_gps(1);
+ cc_set_discharge_screen_hold(1);
+ }
+}
+
+static void cc_enter_charging_phase(void)
+{
+ uint32_t now = 0;
+ uint32_t chg_now = 0;
+ uint8_t soc = 0;
+
+ (void)uapi_get_timestampe_ss(&now);
+ /* 低电已落盘则跳过;未落盘(中途强制转充)在此关账 */
+ if (g_store.phase == (uint8_t)CC_PHASE_DISCHARGE) {
+ if (g_store.discharge_start_ts != 0) {
+ 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();
+ }
+ g_store.discharge_start_ts = 0;
+ }
+ cc_store_mark_persist_locked();
+ }
+ cc_reset_defect_detect_state();
+ g_store.defect_type = (uint8_t)CC_DEFECT_NONE;
+ g_store.defect_alarm_pending = 0;
+
+ cc_clear_pending_charge();
+ 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) {
+ g_store.charge_start_ts = chg_now;
+ }
+ cc_set_white_screen(0);
+ if (g_store.auto_running) {
+ cc_set_discharge_screen_hold(1);
+ } else {
+ cc_set_discharge_screen_hold(0);
+ }
+ cc_set_charge_enable(1);
+ cc_sync_discharge_soc_mode();
+#ifdef ENABLE_HP4570
+ /* 延迟到释放 cc_lock 后再重配,避免 UI 线程持锁时与 battery 任务并发访问 HP4570 */
+ cc_schedule_charge_reconfig();
+#endif
+ if (g_store.auto_running) {
+ cc_set_gps(0);
+ }
+}
+
+static void cc_finish_charging_phase(uint8_t soc)
+{
+ uint32_t now = 0;
+ uint32_t chg_start = 0;
+
+ (void)uapi_get_timestampe_ss(&now);
+ chg_start = g_store.charge_start_ts;
+ if (chg_start != 0 && now > chg_start) {
+ g_store.last_charge_duration_sec = now - chg_start;
+ }
+ g_store.charge_start_ts = 0;
+ g_store.charge_end_ts = now;
+ g_store.defect_type = (uint8_t)CC_DEFECT_NONE;
+ g_store.defect_alarm_pending = 0;
+ g_store.round_count++;
+ cc_append_charge_log(chg_start, now, soc);
+ cc_log_event_persist_locked("charge_end", now, soc);
+ cc_enter_discharge_phase();
+ cc_store_mark_persist_locked();
}
static void cc_pick_initial_phase(uint8_t soc)
{
#ifdef ENABLE_HP4570
- if (soc >= CC_CYCLE_SOC_FULL || hp4570n_read_chrg_stat_chrg_done()) {
+ if (soc >= CC_CYCLE_SOC_FULL || cc_should_finish_charging(soc)) {
cc_enter_discharge_phase();
} else if (soc <= CC_CYCLE_SOC_LOW) {
cc_enter_charging_phase();
#endif
}
+static void cc_release_hw_only_locked(void)
+{
+ cc_set_white_screen(0);
+ cc_set_gps(0);
+ cc_set_discharge_screen_hold(0);
+ cc_set_charge_enable(1);
+ battery_set_force_discharge_soc(0);
+#ifdef ENABLE_HP4570
+ cc_schedule_charge_reconfig();
+#endif
+}
+
+static void cc_shutdown_cleanup_locked(uint8_t log_stop)
+{
+ if (!g_store.auto_running && g_store.phase == (uint8_t)CC_PHASE_IDLE &&
+ !g_discharge_screen_hold && !g_gps_on) {
+ return;
+ }
+ if (log_stop && g_store.auto_running) {
+ uint32_t now = 0;
+ uint8_t soc = cc_read_soc(NULL);
+ (void)uapi_get_timestampe_ss(&now);
+ cc_log_event("burn_stop", now, soc);
+ }
+ g_store.auto_running = 0;
+ g_store.phase = (uint8_t)CC_PHASE_IDLE;
+ cc_clear_pending_charge();
+ cc_clear_start_timestamps();
+ cc_release_hw_only_locked();
+ cc_store_mark_persist_locked();
+}
+
+static uint8_t cc_process_pending_plug_resume_locked(void)
+{
+ if (g_pending_plug_resume == 0) {
+ return 0;
+ }
+ g_pending_plug_resume = 0;
+ if (!cc_is_plugged()) {
+ return 0;
+ }
+ return cc_cycle_resume_automation_locked();
+}
+
static void cc_tick(void)
{
uint8_t soc;
return;
}
if (!cc_is_plugged()) {
+ /* 重启/OTA 后 VBUS 可能尚未就绪:boot_retry 等待期间勿清 auto_running */
+ if (g_boot_vbus_wait) {
+ return;
+ }
/* 插线拷机:掉线则暂停自动循环,恢复充电 IC 默认 */
cc_set_white_screen(0);
cc_set_gps(0);
+ cc_set_discharge_screen_hold(0);
cc_set_charge_enable(1);
+ (void)uapi_get_timestampe_ss(&now);
+ soc = cc_read_soc(NULL);
+ cc_log_event("burn_stop", now, soc);
g_store.phase = (uint8_t)CC_PHASE_IDLE;
g_store.auto_running = 0;
+ cc_clear_pending_charge();
+ cc_clear_start_timestamps();
battery_set_force_discharge_soc(0);
+#ifdef ENABLE_HP4570
+ cc_schedule_charge_reconfig();
+#endif
g_pending_hide_burn_ui = 1;
+ cc_store_mark_persist_locked();
return;
}
(void)uapi_get_timestampe_ss(&now);
soc = cc_read_soc(NULL);
+ cc_update_stored_soc(soc);
if (g_store.phase == (uint8_t)CC_PHASE_DISCHARGE) {
cc_set_charge_enable(0);
cc_set_white_screen(1);
cc_set_gps(1);
- if (soc <= CC_CYCLE_SOC_LOW) {
- cc_finish_discharge_phase(soc);
+ cc_set_discharge_screen_hold(1);
+ /* 自然掉电:低 SOC 预置 pending_charge,关机后再开自动转充 */
+ {
+ uint8_t just_pending = 0;
+
+ if (soc <= CC_CYCLE_SOC_LOW && g_store.pending_charge == 0) {
+ g_store.pending_charge = 1;
+ just_pending = 1;
+ if (soc <= CC_CYCLE_SOC_ARM) {
+ cc_log_event_persist_locked("pending_charge_arm", now, soc);
+ } else {
+ cc_store_mark_persist_locked();
+ }
+ }
+ /* 首次≤10% / SOC==0 / ≤5% 时落盘 discharge_end;硬关机无 prepare 路径,须尽早写 */
+ if (g_store.discharge_start_ts != 0 &&
+ (soc == 0 || just_pending ||
+ (soc <= CC_CYCLE_SOC_ARM && g_store.pending_charge != 0))) {
+ cc_finish_discharge_phase(now, soc);
+ }
}
return;
}
if (g_store.phase == (uint8_t)CC_PHASE_CHARGING) {
cc_set_white_screen(0);
- cc_set_gps(1);
+ cc_set_gps(0);
+ cc_set_discharge_screen_hold(1);
cc_set_charge_enable(1);
if (g_store.charge_start_ts == 0) {
g_store.charge_start_ts = now;
+ cc_log_event_persist_locked("charge_start", now, soc);
}
+ cc_apply_charge_defect(soc, now);
#ifdef ENABLE_HP4570
- if (soc >= CC_CYCLE_SOC_FULL || hp4570n_read_chrg_stat_chrg_done()) {
+ if (cc_should_finish_charging(soc)) {
cc_finish_charging_phase(soc);
}
#else
g_task_running = 1;
while (g_task_running) {
uint8_t hide_ui = 0;
+ uint8_t show_ui = 0;
cc_lock();
- cc_tick();
- cc_store_save();
+ if (is_doing_shutdown_or_reboot()) {
+ cc_release_hw_only_locked();
+ cc_store_mark_dirty();
+ } else {
+ show_ui = cc_process_pending_plug_resume_locked();
+ cc_tick();
+ }
hide_ui = g_pending_hide_burn_ui;
g_pending_hide_burn_ui = 0;
cc_unlock();
+ cc_store_flush();
+ cc_flush_pending_charge_reconfig();
+ if (g_pending_save_time) {
+ g_pending_save_time = 0;
+ save_time();
+ }
if (hide_ui) {
cc_cycle_hide_burn_ui();
}
+ if (show_ui) {
+ cc_cycle_show_burn_ui();
+ }
osDelay(1000);
}
}
void cc_cycle_init(void)
{
cc_ensure_userdata_dir();
+ if (g_mutex == NULL) {
+ g_mutex = osMutexNew(NULL);
+ }
cc_start_task();
cc_lock();
cc_store_load();
cc_unlock();
+ cc_store_flush();
}
-void cc_cycle_on_boot(void)
+void cc_cycle_early_boot_sync(void)
{
- cc_cycle_init();
- cc_lock();
- if (!g_store.test_enabled || !g_store.auto_running) {
- cc_unlock();
- return;
- }
- if (!cc_is_plugged()) {
- g_store.auto_running = 0;
- g_store.phase = (uint8_t)CC_PHASE_IDLE;
- cc_store_save();
- cc_unlock();
- return;
+ cc_ensure_userdata_dir();
+ if (g_mutex == NULL) {
+ g_mutex = osMutexNew(NULL);
}
- cc_set_gps(1);
- if (g_store.phase == (uint8_t)CC_PHASE_CHARGING) {
- cc_enter_charging_phase();
- } else if (g_store.phase == (uint8_t)CC_PHASE_DISCHARGE) {
- cc_enter_discharge_phase();
- } else {
- cc_pick_initial_phase(cc_read_soc(NULL));
+ cc_lock();
+ cc_store_load();
+ if (g_store.test_enabled && g_store.auto_running && cc_is_plugged() &&
+ g_store.phase == (uint8_t)CC_PHASE_DISCHARGE) {
+ float bat_v = 0.0f;
+ uint8_t soc = cc_read_soc(&bat_v);
+ if (!cc_should_force_charge(soc, bat_v)) {
+ cc_restore_discharge_soc_peak();
+ }
}
- cc_store_save();
cc_unlock();
- cc_cycle_show_burn_ui();
}
-uint8_t cc_cycle_get_enabled(void)
+static void cc_stop_boot_retry_timer(void)
{
- return g_store.test_enabled;
+ if (g_boot_retry_timer != NULL) {
+ (void)osTimerStop(g_boot_retry_timer);
+ }
+ g_boot_retry_count = 0;
+ g_boot_vbus_wait = 0;
}
-void cc_cycle_set_enabled(uint8_t on)
+static void cc_boot_retry_timer_cb(void *arg);
+
+static void cc_schedule_boot_retry(void)
{
- uint8_t hide_ui = 0;
- cc_lock();
- if (!on && g_store.auto_running) {
- g_store.auto_running = 0;
- g_store.phase = (uint8_t)CC_PHASE_IDLE;
- cc_set_white_screen(0);
- cc_set_gps(0);
- cc_set_charge_enable(1);
- battery_set_force_discharge_soc(0);
+ if (g_boot_retry_timer == NULL) {
+ g_boot_retry_timer = osTimerNew(cc_boot_retry_timer_cb, osTimerPeriodic, NULL, NULL);
+ }
+ if (g_boot_retry_timer == NULL) {
+ return;
+ }
+ g_boot_retry_count = 0;
+ g_boot_vbus_wait = 1;
+ (void)osTimerStop(g_boot_retry_timer);
+ (void)osTimerStart(g_boot_retry_timer, 1000);
+ PRINT("[cc_cycle] boot_retry: wait VBUS (keep auto_running)\n");
+}
+
+/* 恢复阶段;调用方需已 cc_lock。返回 1 表示应弹出拷机全屏 UI */
+static uint8_t cc_cycle_resume_automation_locked(void)
+{
+ float bat_v = 0.0f;
+ uint8_t soc;
+ uint8_t show_ui = 0;
+
+ if (!g_store.test_enabled || !g_store.auto_running) {
+ return 0;
+ }
+ if (!cc_is_plugged()) {
+ return 0;
+ }
+ g_phase_restore = 1;
+ soc = cc_read_soc(&bat_v);
+ PRINT("[cc_cycle] resume: pending=%u phase=%u last_soc=%u soc=%u batV=%.2f\n",
+ (unsigned int)g_store.pending_charge, (unsigned int)g_store.phase,
+ (unsigned int)g_store.last_soc, (unsigned int)soc, (double)bat_v);
+ if (g_store.pending_charge != 0) {
+ cc_enter_charging_phase();
+ } else if (g_store.phase == (uint8_t)CC_PHASE_CHARGING) {
+ cc_enter_charging_phase();
+ } else if (g_store.phase == (uint8_t)CC_PHASE_DISCHARGE) {
+ if (cc_should_force_charge(soc, bat_v)) {
+ cc_enter_charging_phase();
+ } else {
+ cc_enter_discharge_phase();
+ cc_restore_discharge_soc_peak();
+ }
+ } else {
+ cc_pick_initial_phase(soc);
+ }
+ g_phase_restore = 0;
+ cc_store_mark_dirty();
+ cc_stop_boot_retry_timer();
+ if (!g_burn_ui_boot_armed) {
+ g_burn_ui_boot_armed = 1;
+ show_ui = 1;
+ }
+ return show_ui;
+}
+
+static void cc_boot_retry_timer_cb(void *arg)
+{
+ (void)arg;
+ cc_lock();
+ if (!g_store.test_enabled || !g_store.auto_running) {
+ cc_stop_boot_retry_timer();
+ cc_unlock();
+ return;
+ }
+ if (cc_is_plugged()) {
+ uint8_t show_ui = cc_cycle_resume_automation_locked();
+ cc_unlock();
+ cc_store_flush();
+ cc_flush_pending_charge_reconfig();
+ if (show_ui) {
+ cc_cycle_show_burn_ui();
+ }
+ return;
+ }
+ g_boot_retry_count++;
+ if (g_boot_retry_count >= CC_CYCLE_BOOT_RETRY_MAX) {
+ if ((g_boot_retry_count % CC_CYCLE_BOOT_RETRY_MAX) == 0) {
+ PRINT("[cc_cycle] boot_retry: still no VBUS %us, keep auto_running\n",
+ (unsigned int)g_boot_retry_count);
+ }
+ }
+ cc_unlock();
+ cc_store_flush();
+ cc_flush_pending_charge_reconfig();
+}
+
+void cc_cycle_on_boot(void)
+{
+ cc_cycle_init();
+ g_burn_ui_boot_armed = 0;
+ cc_lock();
+ if (!g_store.test_enabled) {
+ cc_unlock();
+ return;
+ }
+ /* 开关仍开但 auto_running 被异常清零(如旧版 boot_retry 超时),且阶段未 idle → 恢复自动拷机 */
+ if (!g_store.auto_running &&
+ (g_store.phase != (uint8_t)CC_PHASE_IDLE || g_store.pending_charge != 0 ||
+ g_store.burn_start_ts != 0)) {
+ g_store.auto_running = 1;
+ cc_store_mark_dirty();
+ PRINT("[cc_cycle] boot: restore auto_running (test_enabled=1 phase=%u)\n",
+ (unsigned int)g_store.phase);
+ }
+ if (!g_store.auto_running) {
+ cc_unlock();
+ return;
+ }
+ if (!cc_is_plugged()) {
+ /* 深欠压/硬关机/OTA 后 VBUS 可能尚未就绪,勿立即清 auto_running */
+ cc_schedule_boot_retry();
+ cc_unlock();
+ return;
+ }
+ {
+ uint8_t show_ui = cc_cycle_resume_automation_locked();
+ cc_unlock();
+ cc_store_flush();
+ cc_flush_pending_charge_reconfig();
+ if (show_ui) {
+ cc_cycle_show_burn_ui();
+ }
+ }
+}
+
+void cc_cycle_on_charger_plug(uint8_t plugged)
+{
+ if (plugged == 0) {
+ return;
+ }
+ cc_cycle_init();
+ cc_lock();
+ if (!g_store.test_enabled) {
+ cc_unlock();
+ return;
+ }
+ if (!g_store.auto_running &&
+ (g_store.phase != (uint8_t)CC_PHASE_IDLE || g_store.pending_charge != 0 ||
+ g_store.burn_start_ts != 0)) {
+ g_store.auto_running = 1;
+ cc_store_mark_dirty();
+ }
+ if (!g_store.auto_running) {
+ cc_unlock();
+ return;
+ }
+ /* 仅置位,由 cc_cycle_task 在持锁且与 cc_tick 同线程序列化后 resume */
+ g_pending_plug_resume = 1;
+ cc_unlock();
+}
+
+uint8_t cc_cycle_get_enabled(void)
+{
+ return g_store.test_enabled;
+}
+
+void cc_cycle_set_enabled(uint8_t on)
+{
+ uint8_t hide_ui = 0;
+ cc_lock();
+ if (!on && g_store.auto_running) {
+ cc_shutdown_cleanup_locked(1);
hide_ui = 1;
}
g_store.test_enabled = on ? 1 : 0;
- cc_store_save();
+ cc_store_mark_dirty();
cc_unlock();
+ cc_store_flush();
if (hide_ui) {
cc_cycle_hide_burn_ui();
}
{
uint8_t on = 0;
cc_lock();
- if (g_store.auto_running && g_store.phase == (uint8_t)CC_PHASE_CHARGING) {
+ if (cc_cycle_is_active_hw_locked() && g_store.phase == (uint8_t)CC_PHASE_CHARGING) {
on = 1;
}
cc_unlock();
(unsigned int)soc, (unsigned int)round, intent ? "开" : "关");
}
+int cc_cycle_format_detail_line(char *buf, unsigned int buf_size)
+{
+ float voltage = 0.0f;
+ float temp = 0.0f;
+ uint8_t soc;
+
+ if (buf == NULL || buf_size == 0) {
+ return 0;
+ }
+ cc_lock();
+ soc = cc_read_soc(&voltage);
+ cc_unlock();
+ battery_get_battery_temp(&temp);
+ return snprintf_s(buf, buf_size, buf_size - 1,
+ "SOC:%u%% V:%.2fV T:%.1fC",
+ (unsigned int)soc, (double)voltage, (double)temp);
+}
+
+uint8_t cc_cycle_is_active_hw(void)
+{
+ uint8_t active;
+
+ cc_lock();
+ active = cc_cycle_is_active_hw_locked();
+ cc_unlock();
+ return active;
+}
+
uint8_t cc_cycle_is_active(void)
{
- return (g_store.test_enabled && g_store.auto_running &&
+ uint8_t active;
+
+ cc_lock();
+ active = (cc_cycle_is_active_hw_locked() &&
g_store.phase != (uint8_t)CC_PHASE_IDLE) ? 1 : 0;
+ cc_unlock();
+ return active;
}
uint8_t cc_cycle_is_discharge_phase(void)
{
- return (g_store.test_enabled && g_store.auto_running &&
+ uint8_t dis;
+
+ cc_lock();
+ dis = (cc_cycle_is_active_hw_locked() &&
g_store.phase == (uint8_t)CC_PHASE_DISCHARGE) ? 1 : 0;
+ cc_unlock();
+ return dis;
}
uint8_t cc_cycle_is_running(void)
{
- return g_store.auto_running;
+ uint8_t running;
+
+ cc_lock();
+ running = cc_cycle_is_active_hw_locked();
+ cc_unlock();
+ return running;
+}
+
+void cc_cycle_prepare_for_shutdown(void)
+{
+ cc_lock();
+ if (g_store.phase == (uint8_t)CC_PHASE_DISCHARGE &&
+ g_store.discharge_start_ts != 0 &&
+ g_store.last_soc <= CC_CYCLE_SOC_LOW) {
+ uint32_t now = 0;
+ uint8_t soc = cc_read_soc(NULL);
+
+ (void)uapi_get_timestampe_ss(&now);
+ cc_finish_discharge_phase(now, soc);
+ }
+ cc_release_hw_only_locked();
+ cc_store_mark_dirty();
+ cc_unlock();
+ cc_store_flush();
+ if (g_pending_save_time) {
+ g_pending_save_time = 0;
+ save_time();
+ }
+}
+
+void cc_cycle_on_burn_ui_exit(void)
+{
+ cc_lock();
+ cc_set_discharge_screen_hold(0);
+ cc_unlock();
}
void cc_cycle_stop_automation(void)
{
cc_lock();
- g_store.auto_running = 0;
+ cc_shutdown_cleanup_locked(1);
+ cc_store_mark_dirty();
+ cc_unlock();
+ cc_store_flush();
+ cc_cycle_hide_burn_ui();
+}
+
+void cc_cycle_clear_storage(void)
+{
+ cc_lock();
+ cc_shutdown_cleanup_locked(0);
+ g_store.test_enabled = 0;
+ memset(&g_store, 0, sizeof(g_store));
+ g_store.magic = CC_CYCLE_MAGIC;
+ g_store.version = CC_CYCLE_VERSION;
g_store.phase = (uint8_t)CC_PHASE_IDLE;
- cc_set_white_screen(0);
- cc_set_gps(0);
- cc_set_charge_enable(1);
- battery_set_force_discharge_soc(0);
- cc_store_save();
+ g_store_dirty = 0;
cc_unlock();
+
cc_cycle_hide_burn_ui();
+
+ (void)uapi_delete_files(CC_CYCLE_USERDATA_DIR);
+ cc_purge_legacy_storage();
}
-errcode_t cc_cycle_enter_test_ui(void)
+static void cc_format_ts_short(uint32_t ts, char *buf, unsigned int buf_size)
{
- uint8_t soc;
+ if (buf == NULL || buf_size == 0) {
+ return;
+ }
+ if (ts == 0) {
+ (void)snprintf_s(buf, buf_size, buf_size - 1, "--:--");
+ return;
+ }
+ if (uapi_get_TIME_STR(ts, buf, '.', ':', DATE_M_D_H_M) != ERRCODE_SUCC) {
+ (void)snprintf_s(buf, buf_size, buf_size - 1, "--:--");
+ }
+}
+
+static void cc_format_ts_history(uint32_t ts, char *buf, unsigned int buf_size)
+{
+ if (buf == NULL || buf_size == 0) {
+ return;
+ }
+ if (ts == 0U) {
+ (void)snprintf_s(buf, buf_size, buf_size - 1, "--");
+ return;
+ }
+ /* 月.日 时:分,不显示年、秒 */
+ if (uapi_get_TIME_STR(ts, buf, '.', ':', DATE_M_D_H_M) != ERRCODE_SUCC) {
+ (void)snprintf_s(buf, buf_size, buf_size - 1, "????");
+ }
+}
+
+uint32_t cc_cycle_get_burn_start_ts(void)
+{
+ uint32_t ts;
cc_lock();
- if (!g_store.test_enabled) {
- cc_unlock();
- return ERRCODE_FAIL;
+ ts = g_store.burn_start_ts;
+ cc_unlock();
+ return ts;
+}
+
+uint32_t cc_cycle_get_charge_start_ts(void)
+{
+ uint32_t ts;
+ cc_lock();
+ ts = g_store.charge_start_ts;
+ cc_unlock();
+ return ts;
+}
+
+uint32_t cc_cycle_get_discharge_start_ts(void)
+{
+ uint32_t ts;
+ cc_lock();
+ ts = g_store.discharge_start_ts;
+ cc_unlock();
+ return ts;
+}
+
+int cc_cycle_format_time_line(char *buf, unsigned int buf_size)
+{
+ char burn[20];
+ char chg[20];
+ char dis[20];
+ uint32_t t_burn;
+ uint32_t t_chg;
+ uint32_t t_dis;
+ uint8_t running;
+
+ if (buf == NULL || buf_size == 0) {
+ return 0;
}
- if (!cc_is_plugged()) {
- cc_unlock();
- return ERRCODE_FAIL;
+ cc_lock();
+ running = g_store.auto_running;
+ t_burn = g_store.burn_start_ts;
+ t_chg = g_store.charge_start_ts;
+ t_dis = g_store.discharge_start_ts;
+ cc_unlock();
+
+ cc_format_ts_short(t_burn, burn, sizeof(burn));
+ cc_format_ts_short(t_chg, chg, sizeof(chg));
+ cc_format_ts_short(t_dis, dis, sizeof(dis));
+ (void)running;
+ return snprintf_s(buf, buf_size, buf_size - 1,
+ "开始:%s 充:%s 放:%s", burn, chg, dis);
+}
+
+int cc_cycle_format_duration_line(char *buf, unsigned int buf_size)
+{
+ char last_dur[16];
+ char cur_dur[16];
+ char last_chg[16];
+ char last_dis[16];
+ cc_cycle_phase_e phase;
+ uint32_t last_sec;
+ uint32_t cur_sec;
+ uint32_t last_charge_sec;
+ uint32_t last_discharge_sec;
+ uint32_t now = 0;
+ uint8_t running;
+ const char *last_prefix = "上次";
+ const char *cur_prefix = "本次";
+
+ if (buf == NULL || buf_size == 0) {
+ return 0;
+ }
+ cc_lock();
+ running = g_store.auto_running;
+ phase = (cc_cycle_phase_e)g_store.phase;
+ last_charge_sec = g_store.last_charge_duration_sec;
+ last_discharge_sec = g_store.last_discharge_duration_sec;
+ last_sec = 0;
+ cur_sec = 0;
+ if (running) {
+ (void)uapi_get_timestampe_ss(&now);
+ if (phase == CC_PHASE_CHARGING) {
+ last_prefix = "上次充电";
+ cur_prefix = "本次充电";
+ last_sec = g_store.last_charge_duration_sec;
+ cur_sec = cc_phase_duration_sec(g_store.charge_start_ts, now);
+ } else if (phase == CC_PHASE_DISCHARGE) {
+ last_prefix = "上次放电";
+ cur_prefix = "本次放电";
+ last_sec = g_store.last_discharge_duration_sec;
+ cur_sec = cc_current_discharge_duration_sec_locked(now);
+ } else {
+ last_prefix = "上次";
+ cur_prefix = "本次";
+ }
+ }
+ cc_unlock();
+
+ if (!running) {
+ cc_format_hms(last_charge_sec, last_chg, sizeof(last_chg));
+ cc_format_hms(last_discharge_sec, last_dis, sizeof(last_dis));
+ return snprintf_s(buf, buf_size, buf_size - 1,
+ "上次充:%s 上次放:%s", last_chg, last_dis);
+ }
+ cc_format_hms(last_sec, last_dur, sizeof(last_dur));
+ cc_format_hms(cur_sec, cur_dur, sizeof(cur_dur));
+ return snprintf_s(buf, buf_size, buf_size - 1,
+ "%s:%s %s:%s", last_prefix, last_dur, cur_prefix, cur_dur);
+}
+
+static void cc_format_duration_hmin(uint32_t sec, char *buf, unsigned int buf_size)
+{
+ uint32_t h;
+ uint32_t m;
+
+ if (buf == NULL || buf_size == 0) {
+ return;
+ }
+ if (sec == 0) {
+ (void)snprintf_s(buf, buf_size, buf_size - 1, "--");
+ return;
+ }
+ h = sec / 3600u;
+ m = (sec % 3600u) / 60u;
+ if (h > 0) {
+ (void)snprintf_s(buf, buf_size, buf_size - 1, "%uh%umin",
+ (unsigned int)h, (unsigned int)m);
+ } else {
+ (void)snprintf_s(buf, buf_size, buf_size - 1, "%umin", (unsigned int)m);
+ }
+}
+
+static void cc_format_current_time_mdhm(char *buf, unsigned int buf_size)
+{
+ uint32_t now = 0;
+ time_t raw;
+ struct tm tm_info;
+
+ if (buf == NULL || buf_size == 0) {
+ return;
+ }
+ (void)uapi_get_timestampe_ss(&now);
+ if (now == 0U) {
+ (void)snprintf_s(buf, buf_size, buf_size - 1, "--");
+ return;
+ }
+ raw = (time_t)now;
+ if (localtime_r(&raw, &tm_info) == NULL) {
+ (void)snprintf_s(buf, buf_size, buf_size - 1, "--");
+ return;
+ }
+ (void)snprintf_s(buf, buf_size, buf_size - 1, "%02d.%02d %02d:%02d:%02d",
+ tm_info.tm_mon + 1, tm_info.tm_mday,
+ tm_info.tm_hour, tm_info.tm_min, tm_info.tm_sec);
+}
+
+void cc_cycle_get_burn_summary(cc_cycle_burn_summary_t *out)
+{
+ cc_cycle_phase_e phase;
+ uint32_t now = 0;
+ uint32_t cur_charge_sec = 0;
+ uint32_t cur_discharge_sec = 0;
+ uint8_t running;
+
+ if (out == NULL) {
+ return;
+ }
+ memset(out, 0, sizeof(*out));
+ cc_format_current_time_mdhm(out->current_time, sizeof(out->current_time));
+
+ cc_lock();
+ running = g_store.auto_running;
+ phase = (cc_cycle_phase_e)g_store.phase;
+ cc_format_duration_hmin(g_store.last_charge_duration_sec, out->last_charge, sizeof(out->last_charge));
+ cc_format_duration_hmin(g_store.last_discharge_duration_sec, out->last_discharge, sizeof(out->last_discharge));
+
+ if (running) {
+ (void)uapi_get_timestampe_ss(&now);
}
+
+ if (running && phase == (uint8_t)CC_PHASE_CHARGING && g_store.charge_start_ts != 0) {
+ cur_charge_sec = cc_phase_duration_sec(g_store.charge_start_ts, now);
+ cc_format_hms(cur_charge_sec, out->cur_charge, sizeof(out->cur_charge));
+ } else {
+ (void)snprintf_s(out->cur_charge, sizeof(out->cur_charge), sizeof(out->cur_charge) - 1, "待开始");
+ }
+
+ if (g_store.discharge_start_ts != 0) {
+ cur_discharge_sec = cc_phase_duration_sec(g_store.discharge_start_ts, now);
+ cc_format_hms(cur_discharge_sec, out->cur_discharge, sizeof(out->cur_discharge));
+ } else if (running && phase == (uint8_t)CC_PHASE_DISCHARGE) {
+ cur_discharge_sec = cc_current_discharge_duration_sec_locked(now);
+ if (cur_discharge_sec > 0) {
+ cc_format_duration_hmin(cur_discharge_sec, out->cur_discharge, sizeof(out->cur_discharge));
+ } else {
+ (void)snprintf_s(out->cur_discharge, sizeof(out->cur_discharge),
+ sizeof(out->cur_discharge) - 1, "待开始");
+ }
+ } else {
+ (void)snprintf_s(out->cur_discharge, sizeof(out->cur_discharge),
+ sizeof(out->cur_discharge) - 1, "待开始");
+ }
+ cc_unlock();
+}
+
+int cc_cycle_format_defect_line(char *buf, unsigned int buf_size)
+{
+ uint8_t defect;
+
+ if (buf == NULL || buf_size == 0) {
+ return 0;
+ }
+ defect = cc_cycle_get_defect_type();
+ if (defect == (uint8_t)CC_DEFECT_NONE) {
+ buf[0] = '\0';
+ return 0;
+ }
+ if (defect == (uint8_t)CC_DEFECT_RED) {
+ return snprintf_s(buf, buf_size, buf_size - 1,
+ "【不良】充电无电流 SOC下降");
+ }
+ if (defect == (uint8_t)CC_DEFECT_YELLOW) {
+ return snprintf_s(buf, buf_size, buf_size - 1,
+ "【不良】充电超时>2h");
+ }
+ if (defect == (uint8_t)CC_DEFECT_LOW_ICHG) {
+ return snprintf_s(buf, buf_size, buf_size - 1,
+ "【不良】充电电流过低");
+ }
+ if (defect == (uint8_t)CC_DEFECT_HW_NO_CHG) {
+ return snprintf_s(buf, buf_size, buf_size - 1,
+ "【不良】HW未充电量不涨");
+ }
+ if (defect == (uint8_t)CC_DEFECT_BLUE) {
+ return snprintf_s(buf, buf_size, buf_size - 1,
+ "【不良】SOC卡在80%");
+ }
+ return 0;
+}
+
+uint8_t cc_cycle_get_defect_type(void)
+{
+ uint8_t t;
+ cc_lock();
+ t = g_store.defect_type;
+ cc_unlock();
+ return t;
+}
+
+uint32_t cc_cycle_get_defect_ts(void)
+{
+ uint32_t ts;
+ cc_lock();
+ ts = g_store.defect_ts;
+ cc_unlock();
+ return ts;
+}
+
+uint32_t cc_cycle_get_last_charge_duration_sec(void)
+{
+ uint32_t s;
+ cc_lock();
+ s = g_store.last_charge_duration_sec;
+ cc_unlock();
+ return s;
+}
+
+uint32_t cc_cycle_get_last_discharge_duration_sec(void)
+{
+ uint32_t s;
+ cc_lock();
+ s = g_store.last_discharge_duration_sec;
+ cc_unlock();
+ return s;
+}
+
+uint32_t cc_cycle_get_current_phase_duration_sec(void)
+{
+ uint32_t now = 0;
+ uint32_t sec = 0;
+ cc_lock();
+ (void)uapi_get_timestampe_ss(&now);
+ if (g_store.phase == (uint8_t)CC_PHASE_CHARGING) {
+ sec = cc_phase_duration_sec(g_store.charge_start_ts, now);
+ } else if (g_store.phase == (uint8_t)CC_PHASE_DISCHARGE) {
+ sec = cc_current_discharge_duration_sec_locked(now);
+ }
+ cc_unlock();
+ return sec;
+}
+
+uint8_t cc_cycle_take_defect_alarm(void)
+{
+ uint8_t pending = 0;
+ cc_lock();
+ if (g_store.defect_alarm_pending != 0) {
+ pending = 1;
+ g_store.defect_alarm_pending = 0;
+ }
+ cc_unlock();
+ return pending;
+}
+
+int cc_cycle_read_defect_snapshot(cc_cycle_defect_snapshot_t *out)
+{
+ uint16_t len;
+
+ if (out == NULL) {
+ return -1;
+ }
+ memset(out, 0, sizeof(*out));
+ len = (uint16_t)sizeof(*out);
+ uapi_get_file_data((char *)CC_CYCLE_DEFECT_SNAPSHOT_PATH, (uint8_t *)out, &len);
+ if (out->magic != CC_CYCLE_DEFECT_SNAPSHOT_MAGIC) {
+ len = (uint16_t)sizeof(*out);
+ uapi_get_file_data((char *)CC_CYCLE_LEGACY_DEFECT_PATH, (uint8_t *)out, &len);
+ }
+ if (out->magic != CC_CYCLE_DEFECT_SNAPSHOT_MAGIC) {
+ return -1;
+ }
+ return 0;
+}
+
+int cc_cycle_format_ic_status_live(char *buf, unsigned int buf_size)
+{
+ uint8_t power = 0;
+ float voltage = 0.0f;
+ float temp = 0.0f;
+ uint8_t charge_id = 0;
+ uint8_t vbus_pg = 0;
+ uint8_t chrg_fast = 0;
+ uint8_t chrg_done = 0;
+ int n;
+
+ if (buf == NULL || buf_size == 0) {
+ return 0;
+ }
+#ifdef SUPPORT_BATTERY_SERVICE
+ power = battery_get_voltage(&voltage);
+#else
+ (void)power;
+#endif
+ cc_fill_ic_status_fields(&voltage, &temp, &charge_id, &vbus_pg, &chrg_fast, &chrg_done);
+ n = snprintf_s(buf, buf_size, buf_size - 1,
+ "power:%u\nvoltage:%.2f\nbat_temp:%.1f\ncharge_ic:%u\nvbus_pg:%u\nchrg:%u/%u\n",
+ (unsigned int)power, (double)voltage, (double)temp,
+ (unsigned int)charge_id, (unsigned int)vbus_pg,
+ (unsigned int)chrg_fast, (unsigned int)chrg_done);
+ if (n < 0) {
+ return 0;
+ }
+#ifdef ENABLE_HP4570
+ if ((unsigned int)n < buf_size - 1) {
+ size_t off = (size_t)n;
+ off += hp4570n_dump_debug_registers(buf + off, buf_size - off);
+ (void)off;
+ }
+#endif
+ return (int)strlen(buf);
+}
+
+int cc_cycle_format_ic_registers(char *buf, unsigned int buf_size)
+{
+ if (buf == NULL || buf_size == 0) {
+ return 0;
+ }
+ buf[0] = '\0';
+#ifdef ENABLE_HP4570
+ return (int)hp4570n_dump_debug_registers(buf, buf_size);
+#else
+ return snprintf_s(buf, buf_size, buf_size - 1, "HP4570 未启用");
+#endif
+}
+
+typedef struct {
+ char name[32];
+ uint32_t ts;
+ uint16_t round;
+ uint8_t soc;
+ uint8_t has_soc;
+} cc_hist_evt_t;
+
+/* 拷机记录解析/workspace,勿放 GUI 线程栈上 */
+static cc_hist_evt_t g_hist_evts[CC_HIST_EVT_MAX];
+static char g_hist_display[CC_CYCLE_HISTORY_BUF_RECOMMEND];
+
+static int cc_hist_append(char *buf, unsigned int buf_size, unsigned int *off, const char *fmt, ...)
+{
+ va_list ap;
+ int n;
+
+ if (buf == NULL || off == NULL || *off >= buf_size) {
+ return 0;
+ }
+ va_start(ap, fmt);
+ n = vsnprintf_s(buf + *off, buf_size - *off, buf_size - *off - 1, fmt, ap);
+ va_end(ap);
+ if (n <= 0) {
+ return 0;
+ }
+ *off += (unsigned int)n;
+ if (*off >= buf_size - 1) {
+ *off = buf_size - 1;
+ buf[*off] = '\0';
+ return 0;
+ }
+ return 1;
+}
+
+static int cc_hist_parse_event_line(const char *line, cc_hist_evt_t *out)
+{
+ char ev[32];
+ unsigned int ts;
+ unsigned int round;
+ unsigned int soc;
+ int n;
+
+ if (line == NULL || out == NULL) {
+ return 0;
+ }
+ memset(out, 0, sizeof(*out));
+ n = sscanf_s(line, "event=%31s ts=%u", ev, sizeof(ev), &ts);
+ if (n < 2) {
+ return 0;
+ }
+ round = 0;
+ soc = 0;
+ {
+ const char *rp = strstr(line, "round=");
+ if (rp != NULL) {
+ (void)sscanf_s(rp, "round=%u", &round);
+ }
+ const char *sp = strstr(line, " soc=");
+ if (sp != NULL) {
+ if (sscanf_s(sp, " soc=%u", &soc) >= 1) {
+ out->has_soc = 1;
+ out->soc = (uint8_t)soc;
+ }
+ }
+ }
+ if (strncpy_s(out->name, sizeof(out->name), ev, sizeof(out->name) - 1) != EOK) {
+ return 0;
+ }
+ out->ts = (uint32_t)ts;
+ out->round = (uint16_t)round;
+ return 1;
+}
+
+static int cc_hist_load_events_from_path(const char *path, cc_hist_evt_t *evts, int max_evts, int n_evts)
+{
+ FILE *fp;
+ char line[160];
+ int n;
+
+ if (path == NULL || evts == NULL || max_evts <= 0 || n_evts >= max_evts) {
+ return n_evts;
+ }
+ fp = fopen(path, "r");
+ if (fp == NULL) {
+ return n_evts;
+ }
+ n = n_evts;
+ while (n < max_evts && fgets(line, sizeof(line), fp) != NULL) {
+ size_t len = strlen(line);
+ while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
+ line[len - 1] = '\0';
+ len--;
+ }
+ if (len == 0) {
+ continue;
+ }
+ if (cc_hist_parse_event_line(line, &evts[n])) {
+ n++;
+ }
+ }
+ (void)fclose(fp);
+ return n;
+}
+
+static int cc_hist_load_all_events(void)
+{
+ int n_evts;
+
+ cc_ensure_userdata_dir();
+ n_evts = cc_hist_load_events_from_path(CC_CYCLE_LEGACY_EVENT_LOG_PATH, g_hist_evts,
+ CC_HIST_EVT_MAX, 0);
+ n_evts = cc_hist_load_events_from_path(CC_CYCLE_EVENT_LOG_PATH, g_hist_evts,
+ CC_HIST_EVT_MAX, n_evts);
+ return n_evts;
+}
+
+static int cc_hist_evt_name_eq(const cc_hist_evt_t *e, const char *name)
+{
+ if (e == NULL || name == NULL) {
+ return 0;
+ }
+ return (strcmp(e->name, name) == 0) ? 1 : 0;
+}
+
+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)
+{
+ char s0[32];
+ char s1[32];
+
+ cc_format_ts_history(t0, s0, sizeof(s0));
+ if (in_progress) {
+ (void)cc_hist_append(buf, buf_size, off, "充电:%s-进行中\n", s0);
+ return;
+ }
+ if (t0 != 0 && t1 != 0 && t1 < t0) {
+ return;
+ }
+ cc_format_ts_history(t1, s1, sizeof(s1));
+ (void)cc_hist_append(buf, buf_size, off, "充电:%s-%s\n", s0, s1);
+}
+
+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)
+{
+ char s0[32];
+ char s1[32];
+
+ cc_format_ts_history(t0, s0, sizeof(s0));
+ if (in_progress) {
+ (void)cc_hist_append(buf, buf_size, off, "放电:%s-进行中\n", s0);
+ return;
+ }
+ if (t0 != 0 && t1 != 0 && t1 < t0) {
+ return;
+ }
+ cc_format_ts_history(t1, s1, sizeof(s1));
+ (void)cc_hist_append(buf, buf_size, off, "放电:%s-%s\n", s0, s1);
+}
+
+static uint8_t cc_hist_is_phase_evt(const cc_hist_evt_t *e)
+{
+ if (e == NULL) {
+ return 0;
+ }
+ return (cc_hist_evt_name_eq(e, "charge_start") || cc_hist_evt_name_eq(e, "charge_end") ||
+ cc_hist_evt_name_eq(e, "discharge_start") || cc_hist_evt_name_eq(e, "discharge_end")) ? 1 : 0;
+}
+
+static int cc_hist_name_prefix(const cc_hist_evt_t *e, const char *prefix)
+{
+ if (e == NULL || prefix == NULL) {
+ return 0;
+ }
+ return (strncmp(e->name, prefix, strlen(prefix)) == 0) ? 1 : 0;
+}
+
+static const char *cc_hist_defect_label(const cc_hist_evt_t *e)
+{
+ if (e == NULL) {
+ return NULL;
+ }
+ if (cc_hist_name_prefix(e, "defect_red_soc_drop")) {
+ return "红-无电流SOC降";
+ }
+ if (cc_hist_name_prefix(e, "defect_yellow_slow_ch")) {
+ return "黄-充电超2h";
+ }
+ if (cc_hist_name_prefix(e, "defect_blue_soc_stuck")) {
+ return "蓝-SOC卡80%";
+ }
+ if (cc_hist_name_prefix(e, "defect_low_ichg")) {
+ return "橙-充电电流过低";
+ }
+ if (cc_hist_name_prefix(e, "defect_hw_no_chg")) {
+ return "紫-HW未充电量不涨";
+ }
+ if (strncmp(e->name, "defect_", 7) == 0) {
+ return e->name;
+ }
+ return NULL;
+}
+
+static void cc_hist_emit_defect_line(char *buf, unsigned int buf_size, unsigned int *off,
+ const cc_hist_evt_t *e)
+{
+ char ts[32];
+ const char *label;
+
+ if (e == NULL) {
+ return;
+ }
+ label = cc_hist_defect_label(e);
+ if (label == NULL) {
+ return;
+ }
+ cc_format_ts_history(e->ts, ts, sizeof(ts));
+ if (e->has_soc) {
+ (void)cc_hist_append(buf, buf_size, off, "【%s】%s soc=%u%% round=%u\n",
+ label, ts, (unsigned int)e->soc, (unsigned int)e->round);
+ } else {
+ (void)cc_hist_append(buf, buf_size, off, "【%s】%s round=%u\n",
+ label, ts, (unsigned int)e->round);
+ }
+}
+
+static int cc_hist_format_history_locked(char *buf, unsigned int buf_size)
+{
+ int n_evts = 0;
+ int i;
+ unsigned int off = 0;
+ uint8_t auto_running;
+ cc_cycle_phase_e phase;
+ uint32_t open_chg = 0;
+ uint32_t open_dis = 0;
+ uint8_t has_lines = 0;
+ int ret;
+
+ buf[0] = '\0';
+ n_evts = cc_hist_load_all_events();
+ if (n_evts <= 0) {
+ return snprintf_s(buf, buf_size, buf_size - 1, "暂无拷机记录\n");
+ }
+
+ auto_running = g_store.auto_running;
+ phase = (cc_cycle_phase_e)g_store.phase;
+
+ for (i = 0; i < n_evts; i++) {
+ const cc_hist_evt_t *e = &g_hist_evts[i];
+
+ if (cc_hist_evt_name_eq(e, "burn_start") || cc_hist_evt_name_eq(e, "burn_stop") ||
+ cc_hist_evt_name_eq(e, "pending_charge_arm")) {
+ continue;
+ }
+ if (cc_hist_defect_label(e) != NULL) {
+ cc_hist_emit_defect_line(buf, buf_size, &off, e);
+ has_lines = 1;
+ continue;
+ }
+ if (!cc_hist_is_phase_evt(e)) {
+ continue;
+ }
+
+ 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;
+ } 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;
+ } 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;
+ }
+ if (open_chg != 0) {
+ if (e->ts > open_chg) {
+ cc_hist_emit_charge_line(buf, buf_size, &off, open_chg, e->ts, 0);
+ has_lines = 1;
+ open_chg = e->ts;
+ } else if (e->ts == open_chg) {
+ /* 重复 charge_start,忽略 */
+ } else {
+ cc_hist_emit_charge_line(buf, buf_size, &off, open_chg, 0, 1);
+ has_lines = 1;
+ open_chg = e->ts;
+ }
+ } else {
+ open_chg = e->ts;
+ }
+ } else if (cc_hist_evt_name_eq(e, "charge_end")) {
+ if (open_chg != 0) {
+ 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;
+ } else {
+ cc_hist_emit_charge_line(buf, buf_size, &off, open_chg, 0, 1);
+ has_lines = 1;
+ }
+ open_chg = 0;
+ }
+ } 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;
+ }
+ /* e->ts <= open_dis:恢复脏数据,忽略 */
+ } else {
+ open_dis = e->ts;
+ }
+ } else if (cc_hist_evt_name_eq(e, "discharge_end")) {
+ if (open_dis != 0) {
+ if (e->ts == 0 || e->ts >= open_dis) {
+ cc_hist_emit_discharge_line(buf, buf_size, &off, open_dis, e->ts, 0);
+ has_lines = 1;
+ open_dis = 0;
+ }
+ /* ts < open_dis:脏 event,保留 open_dis 待后续关账 */
+ }
+ }
+ }
+
+ if (open_dis != 0 && 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;
+ }
+
+ 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;
+ } else if (open_dis != 0 && auto_running && phase == CC_PHASE_DISCHARGE) {
+ cc_hist_emit_discharge_line(buf, buf_size, &off, open_dis, 0, 1);
+ has_lines = 1;
+ } else if (open_dis != 0) {
+ cc_hist_emit_discharge_line(buf, buf_size, &off, open_dis, 0, 1);
+ has_lines = 1;
+ }
+
+ if (!has_lines) {
+ return snprintf_s(buf, buf_size, buf_size - 1, "暂无拷机记录\n");
+ }
+ if (off >= buf_size - 16) {
+ (void)cc_hist_append(buf, buf_size, &off, "...(截断)\n");
+ }
+ ret = (int)strlen(buf);
+ return ret;
+}
+
+int cc_cycle_format_history(char *buf, unsigned int buf_size)
+{
+ int ret;
+
+ if (buf == NULL || buf_size == 0) {
+ return 0;
+ }
+ cc_lock();
+ ret = cc_hist_format_history_locked(buf, buf_size);
+ cc_unlock();
+ return ret;
+}
+
+const char *cc_cycle_get_history_text(void)
+{
+ cc_lock();
+ (void)cc_hist_format_history_locked(g_hist_display, sizeof(g_hist_display));
+ cc_unlock();
+ return g_hist_display;
+}
+
+errcode_t cc_cycle_enter_test_ui_with_phase(cc_cycle_phase_e initial_phase)
+{
+ uint8_t soc;
+ uint32_t now = 0;
+
+ if (initial_phase != CC_PHASE_CHARGING && initial_phase != CC_PHASE_DISCHARGE) {
+ return ERRCODE_FAIL;
+ }
+
+ cc_lock();
+ if (!g_store.test_enabled) {
+ cc_unlock();
+ return ERRCODE_FAIL;
+ }
+ if (!cc_is_plugged()) {
+ cc_unlock();
+ return ERRCODE_FAIL;
+ }
+ (void)uapi_get_timestampe_ss(&now);
+ g_store.auto_running = 1;
+ g_store.burn_start_ts = now;
+ soc = cc_read_soc(NULL);
+ cc_log_event_persist_locked("burn_start", now, soc);
+ if (initial_phase == CC_PHASE_CHARGING) {
+ cc_enter_charging_phase();
+ } else {
+ cc_enter_discharge_phase();
+ }
+ cc_store_mark_persist_locked();
+ cc_unlock();
+ cc_store_write_pending();
+ cc_flush_pending_charge_reconfig();
+ cc_cycle_show_burn_ui();
+ return ERRCODE_SUCC;
+}
+
+errcode_t cc_cycle_enter_test_ui(void)
+{
+ uint8_t soc;
+ uint32_t now = 0;
+
+ cc_lock();
+ if (!g_store.test_enabled) {
+ cc_unlock();
+ return ERRCODE_FAIL;
+ }
+ if (!cc_is_plugged()) {
+ cc_unlock();
+ return ERRCODE_FAIL;
+ }
+ (void)uapi_get_timestampe_ss(&now);
g_store.auto_running = 1;
+ g_store.burn_start_ts = now;
soc = cc_read_soc(NULL);
+ cc_log_event_persist_locked("burn_start", now, soc);
cc_pick_initial_phase(soc);
- cc_store_save();
+ cc_store_mark_persist_locked();
cc_unlock();
+ cc_store_write_pending();
+ cc_flush_pending_charge_reconfig();
cc_cycle_show_burn_ui();
return ERRCODE_SUCC;
}
extern "C" {
#endif
-#define CC_CYCLE_STORE_PATH "user/userdata/cc_cycle.bin"
-#define CC_CYCLE_CHARGE_LOG_PATH "user/userdata/cc_charge_log.txt"
+#define CC_CYCLE_USERDATA_DIR "user/userdata/cc_cycle"
+#define CC_CYCLE_STORE_PATH CC_CYCLE_USERDATA_DIR "/cc_cycle.bin"
+#define CC_CYCLE_CHARGE_LOG_PATH CC_CYCLE_USERDATA_DIR "/charge_log.txt"
+#define CC_CYCLE_EVENT_LOG_PATH CC_CYCLE_USERDATA_DIR "/event_log.txt"
+#define CC_CYCLE_DEFECT_SNAPSHOT_PATH CC_CYCLE_USERDATA_DIR "/defect.bin"
+
+#define CC_CYCLE_DEFECT_REG_DUMP_SIZE 2048
typedef enum {
CC_PHASE_IDLE = 0,
CC_PHASE_CHARGING = 2, /* 插线拷机:允许充电,记录起止时间 */
} cc_cycle_phase_e;
+typedef enum {
+ CC_DEFECT_NONE = 0,
+ CC_DEFECT_RED = 1, /* 充电 SOC 下降 */
+ CC_DEFECT_YELLOW = 2, /* 充电超 2h 未满 */
+ CC_DEFECT_BLUE = 3, /* SOC 卡在 ~80% */
+ CC_DEFECT_LOW_ICHG = 4, /* HP4570 配置 ICHG 持续过低(如10mA级) */
+ CC_DEFECT_HW_NO_CHG = 5, /* 拷机充电阶段 HW 未充且 SOC 长时间不涨 */
+} cc_cycle_defect_type_e;
+
+typedef struct {
+ uint32_t magic;
+ uint8_t defect_type;
+ uint8_t soc;
+ uint16_t round;
+ uint32_t ts;
+ float voltage;
+ float temp;
+ uint8_t charge_id;
+ uint8_t vbus_pg;
+ uint8_t chrg_fast;
+ uint8_t chrg_done;
+ uint8_t phase;
+ uint32_t charge_elapsed_sec;
+ char reg_dump[CC_CYCLE_DEFECT_REG_DUMP_SIZE];
+} cc_cycle_defect_snapshot_t;
+
void cc_cycle_init(void);
void cc_cycle_on_boot(void);
+/* 插线事件:深欠压开机 VBUS 晚到时尝试恢复拷机 UI */
+void cc_cycle_on_charger_plug(uint8_t plugged);
+/* 在 battery_percent_init 之前调用,恢复拷机放电 SOC 峰值 */
+void cc_cycle_early_boot_sync(void);
uint8_t cc_cycle_get_enabled(void);
void cc_cycle_set_enabled(uint8_t on);
/* HP4570 硬件充电状态:1=预充/快充中 */
uint8_t cc_cycle_get_hw_is_charging(void);
uint8_t cc_cycle_is_active(void);
+/* 拷机开关开且已运行:允许控制 HP4570/GPS/常亮/SOC 特殊模式 */
+uint8_t cc_cycle_is_active_hw(void);
/* 仅放电阶段:拷机需接管 Hi-Z/禁充;充电阶段交还 battery_service 管电流 */
uint8_t cc_cycle_is_discharge_phase(void);
uint8_t cc_cycle_is_running(void);
+uint32_t cc_cycle_get_burn_start_ts(void);
+uint32_t cc_cycle_get_charge_start_ts(void);
+uint32_t cc_cycle_get_discharge_start_ts(void);
+
/* 写入状态文案,buf 建议>=96 字节,返回 snprintf 结果 */
int cc_cycle_format_status_line(char *buf, unsigned int buf_size);
+/* 拷机/充/放开始时间,buf 建议>=128 字节 */
+int cc_cycle_format_time_line(char *buf, unsigned int buf_size);
+/* 上次/本次阶段时长,buf 建议>=128 字节 */
+int cc_cycle_format_duration_line(char *buf, unsigned int buf_size);
+
+/* 全屏拷机页「充放电拷机汇总」:当前时间 + 上次/本次充放电时长文案 */
+typedef struct {
+ char current_time[32];
+ char last_charge[24];
+ char cur_charge[24];
+ char last_discharge[24];
+ char cur_discharge[24];
+} cc_cycle_burn_summary_t;
+
+void cc_cycle_get_burn_summary(cc_cycle_burn_summary_t *out);
+
+/* 不良文案,无不良返回 0 */
+int cc_cycle_format_defect_line(char *buf, unsigned int buf_size);
+/* 写入 SOC/电压/温度,buf 建议>=128 字节 */
+int cc_cycle_format_detail_line(char *buf, unsigned int buf_size);
+
+uint8_t cc_cycle_get_defect_type(void);
+uint32_t cc_cycle_get_defect_ts(void);
+uint32_t cc_cycle_get_last_charge_duration_sec(void);
+uint32_t cc_cycle_get_last_discharge_duration_sec(void);
+uint32_t cc_cycle_get_current_phase_duration_sec(void);
+/* 读取并清除待播放铃声标志,1=应播放 */
+uint8_t cc_cycle_take_defect_alarm(void);
+/* 读取最近一次不良快照,0=无快照 */
+int cc_cycle_read_defect_snapshot(cc_cycle_defect_snapshot_t *out);
+/* 实时 IC 状态文本,buf 建议>=512 字节 */
+int cc_cycle_format_ic_status_live(char *buf, unsigned int buf_size);
+/* 仅 HP4570 寄存器 dump,与 Debug 页一致,buf 建议>=2048 字节 */
+int cc_cycle_format_ic_registers(char *buf, unsigned int buf_size);
+
+#define CC_CYCLE_HISTORY_BUF_RECOMMEND 4096u
+/* 拷机历史(全 log 时间序),月.日 时:分 */
+int cc_cycle_format_history(char *buf, unsigned int buf_size);
+/* 拷机历史文本(静态缓冲,GUI 线程勿在栈上开大数组) */
+const char *cc_cycle_get_history_text(void);
-/* 需已插充电线;启动后自动循环充放电 */
+/* 需已插充电线;启动后自动循环充放电(按 SOC/上次阶段自动选首轮) */
errcode_t cc_cycle_enter_test_ui(void);
+/* 指定首轮为充电或放电,之后仍按满电/低电自动循环 */
+errcode_t cc_cycle_enter_test_ui_with_phase(cc_cycle_phase_e initial_phase);
void cc_cycle_stop_automation(void);
+/* 关机/重启前释放 GPS、常亮、Hi-Z,交还充电 IC 给 battery_service */
+void cc_cycle_prepare_for_shutdown(void);
+/* 清除拷机持久化目录及内存状态(恢复出厂等场景) */
+void cc_cycle_clear_storage(void);
/* 全屏白底拷机界面(producttest 实现) */
void cc_cycle_show_burn_ui(void);
+/** 产测页手动回到全屏拷机(取消进行中的左滑退出) */
+void cc_cycle_reopen_burn_ui(void);
void cc_cycle_hide_burn_ui(void);
+/* 仅退出拷机全屏 UI:释放放电常亮/PM,不停止自动拷机 */
+void cc_cycle_on_burn_ui_exit(void);
+/* 仅退出拷机全屏 UI:释放放电常亮,不停止自动循环 */
+void cc_cycle_on_burn_ui_hidden(void);
#ifdef __cplusplus
}
\r
static int aging_time = 0;\r
\r
+#ifdef ENABLE_HP4570\r
+/**\r
+ * HP4570 产线筛片(对应根因计划:I2C ID + VBUS_PG + 10s 充电路径)\r
+ * out[0] result: 0=pass, 1=no_vbus, 2=i2c_id, 3=vbus_pg, 4=no_charge_path\r
+ * out[1]=charge_id, out[2]=vbus_pg, out[3]=charge_stat\r
+ * out[4-5]=vbat_before_mV, out[6-7]=vbat_after_mV\r
+ */\r
+static void factorytest_hp4570_screen(uint8_t *out, uint16_t out_len)\r
+{\r
+ const uint16_t wait_ms = 10000;\r
+\r
+ if (out == NULL || out_len < 8) {\r
+ return;\r
+ }\r
+ (void)memset_s(out, out_len, 0, out_len);\r
+\r
+ if (!uapi_get_charger_plug_status()) {\r
+ out[0] = 1;\r
+ return;\r
+ }\r
+\r
+ out[1] = hp4570n_get_id();\r
+ if (out[1] != 0x2) {\r
+ out[0] = 2;\r
+ return;\r
+ }\r
+\r
+ out[2] = hp4570n_read_status_vbus_pg() ? 1 : 0;\r
+ if (out[2] == 0) {\r
+ out[0] = 3;\r
+ return;\r
+ }\r
+\r
+ float v_before = 0.0f;\r
+ float v_after = 0.0f;\r
+ uint16_t mv = 0;\r
+\r
+ (void)batteryservice_get_voltage(&v_before);\r
+ mv = (uint16_t)(v_before * 1000.0f);\r
+ out[4] = (uint8_t)(mv >> 8);\r
+ out[5] = (uint8_t)(mv & 0xff);\r
+\r
+ LOS_Msleep(wait_ms);\r
+\r
+ out[3] = get_charge_stat();\r
+ bool chrg_path_ok = hp4570n_read_chrg_stat_fast_chrg() ||\r
+ hp4570n_read_chrg_stat_pre_chrg() ||\r
+ hp4570n_read_chrg_stat_dead_chrg() ||\r
+ hp4570n_read_chrg_stat_chrg_done();\r
+ if (out[3] == 0 || !chrg_path_ok) {\r
+ out[0] = 4;\r
+ }\r
+\r
+ (void)batteryservice_get_voltage(&v_after);\r
+ mv = (uint16_t)(v_after * 1000.0f);\r
+ out[6] = (uint8_t)(mv >> 8);\r
+ out[7] = (uint8_t)(mv & 0xff);\r
+}\r
+#endif\r
+\r
static uint8_t pwr_button_result = 0;\r
static uint8_t func_button_result = 1;\r
\r
aging_t.charge_error++;\r
aging_t.fail_num++;\r
}\r
+ else if (get_charge_stat() && !hp4570n_read_status_vbus_pg())\r
+ {\r
+ aging_t.charge_error++;\r
+ aging_t.fail_num++;\r
+ }\r
\r
PRINT("充电 失败次数 : %d \r\n",aging_t.charge_error);\r
/********************* 马达 老化测试*****************************/\r
// uapi_poweroff_device();\r
shutdown_reboot_prejobs(SHUTDOWN_TYPE);\r
}\r
+#ifdef ENABLE_HP4570\r
+ else if (testitem == 0x06)\r
+ {\r
+ /* HP4570 产线筛片:插 5V → I2C ID + VBUS_PG + 10s 充电路径(约 10s) */\r
+ uint8_t screen[8] = {0};\r
+\r
+ factorytest_hp4570_screen(screen, sizeof(screen));\r
+ factorytest_protocol_replymsg(BLE_TEST_CMD_MODE_SOFT_TEST,\r
+ BLE_TEST_CMD_SOFT_TEST_SUB_MODE_PM,\r
+ 0x06,\r
+ screen,\r
+ sizeof(screen));\r
+ }\r
+#endif\r
\r
break;\r
\r
void start_shutdown_or_reboot(uint8_t type)
{
PRINT("[PWR_FLOW] start_shutdown_or_reboot: type=%d (prev=%d)\r\n", type, doing_shutdown_type);
+#ifdef ENABLE_HP4570
+ cc_cycle_prepare_for_shutdown();
+#endif
doing_shutdown_type = type;
}
#endif
#ifdef ENABLE_HP4570
- if (custom_get_factory_test_flag() != 0) {
- cc_cycle_on_boot();
- }
+ cc_cycle_on_boot();
#endif
}
//恢复存储
resume_report_log();
+
+#ifdef ENABLE_HP4570
+ cc_cycle_clear_storage();
+#endif
#ifdef SINGLE_USER_PARTITION
char *filebuf[] = {
//恢复存储
resume_report_log();
+
+#ifdef ENABLE_HP4570
+ cc_cycle_clear_storage();
+#endif
#ifdef SINGLE_USER_PARTITION
char *filebuf[] = {
--- /dev/null
+# HP4570 LOT 追溯交叉表
+> 客退 SN 来源:`FD272-0528客退机记录.xlsx`(72 台)
+> 场不良 ~200 台 SN **待质量部导入** — 下方预留列
+> U900 date code / 4570XYSm LOT **待拆机读丝印或来料台账对齐**
+> **闳康 FA 执行 SOP:** [`HP4570_闳康FA测试流程.md`](./HP4570_闳康FA测试流程.md)
+
+---
+
+## 客退 SN 全表(72 台)
+
+| 序号 | 镭雕 SN | 生产批次 | 异常类型 | 状态确认 | 分类 | U900 丝印 | SYS(V) | LOT/date code | 场不良匹配 |
+|------|---------|----------|----------|----------|------|-----------|--------|---------------|------------|
+| 1 | 202604008238 | 202604 | 充电异常 | 无充电电流,充电27%,充电0mA | 硬失效 | 待拆机 | — | 待填 | 待填 |
+| 2 | 202604008923 | 202604 | 充电异常 | 无充电电流,充电不开机,充电1mA | 硬失效 | 4570XYSm | | 待填 | 待填 |
+| 3 | 202604001246 | 202604 | 蓝牙断连 | 其它(复测) | 非HP4570 | 待拆机 | — | 待填 | 待填 |
+| 4 | 202604002638 | 202604 | 无法切换表盘、恢复出产失效 | 其它(复测) | 非HP4570 | 待拆机 | — | 待填 | 待填 |
+| 5 | 202604010452 | 202604 | 充电异常 | 无充电电流,充电0%,充电0mA | 硬失效 | 待拆机 | — | 待填 | 待填 |
+| 6 | 202605000073 | 202605 | 充电异常 | 无充电电流,充电5%,充电20mA | 硬失效 | 待拆机 | — | 待填 | 待填 |
+| 7 | 202605000562 | 202605 | 充电异常 | 无充电电流,充电32%,充电0mA | 硬失效 | 待拆机 | — | 待填 | 待填 |
+| 8 | 202604004969 | 202604 | 充电异常 | 充电100%,充电90mA(复测) | 复测有电流 | 待拆机 | — | 待填 | 待填 |
+| 9 | 202604008937 | 202604 | 充电异常 | 无充电电流,充电28%,充电0mA | 硬失效 | 待拆机 | — | 待填 | 待填 |
+| 10 | 202604005054 | 202604 | 没有久坐提醒、表带脏 | 其它(复测) | 非HP4570 | 待拆机 | — | 待填 | 待填 |
+| 11 | 202604008863 | 202604 | 充电异常 | 无充电电流,充电17%,充电0mA | 硬失效 | 待拆机 | — | 待填 | 待填 |
+| 12 | 202604005461 | 202604 | 打开设置收不到消息提醒 | 其它(复测) | 非HP4570 | 待拆机 | — | 待填 | 待填 |
+| 13 | 202604000633 | 202604 | 充电异常,耗电异常、一天一充 | 充电100%,充电100mA(复测) | 复测有电流 | 待拆机 | — | 待填 | 待填 |
+| 14 | 202604009633 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | 4570XYDn | 3,87 | 待填 | 待填 |
+| 15 | 202604003521 | 202604 | 触摸屏幕不灵敏 | 其它(复测) | 非HP4570 | 待拆机 | — | 待填 | 待填 |
+| 16 | 202604010304 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | 4570XYSm | | 待填 | 待填 |
+| 17 | 202604004336 | 202604 | 连接异常 经常断开 | 其它(复测) | 非HP4570 | 待拆机 | — | 待填 | 待填 |
+| 18 | 202604001281 | 202604 | 充电异常 | 充电96%,充电160mA(复测) | 复测有电流 | 待拆机 | — | 待填 | 待填 |
+| 19 | 202604010818 | 202604 | 充电异常 | 无充电电流,充电14%,充电0mA | 硬失效 | 待拆机 | — | 待填 | 待填 |
+| 20 | 202605000571 | 202605 | 充电异常 | 无充电电流,充电不开机,充电0 | 硬失效 | 4570XYSm | | 待填 | 待填 |
+| 21 | 202604010531 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | | | 待填 | 待填 |
+| 22 | 202604010029 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | | | 待填 | 待填 |
+| 23 | 202605006701 | 202605 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | 4570XYSm | 0.03 | 待填 | 待填 |
+| 24 | 202604010352 | 202604 | 充电异常 | 无充电电流,充电19%,充电10mA | 硬失效 | 待拆机 | — | 待填 | 待填 |
+| 25 | 202604009493 | 202604 | 充电异常 | 无充电电流,充电0%,充电0mA | 硬失效 | 待拆机 | — | 待填 | 待填 |
+| 26 | 202604009391 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | 4570XYSm | 0.08 | 待填 | 待填 |
+| 27 | 202604002502 | 202604 | 充电异常 | 充电100%,充电90mA(复测) | 复测有电流 | 待拆机 | — | 待填 | 待填 |
+| 28 | 202604004429 | 202604 | 充电异常 | 充电99%,充电60mA(复测) | 复测有电流 | 待拆机 | — | 待填 | 待填 |
+| 29 | 202605002701 | 202605 | 充电异常 | 充电100%,充电50mA(复测) | 复测有电流 | 待拆机 | — | 待填 | 待填 |
+| 30 | 202604005109 | 202604 | 充电异常 | 充电100%,充电90mA(复测) | 复测有电流 | 待拆机 | — | 待填 | 待填 |
+| 31 | 202605004429 | 202605 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | | | 待填 | 待填 |
+| 32 | 202604008789 | 202604 | 充电异常 | 无充电电流,充电6%,充电6mA | 硬失效 | 待拆机 | — | 待填 | 待填 |
+| 33 | 202604009949 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | 4570XYSm | | 待填 | 待填 |
+| 34 | 202604000064 | 202604 | 自动黑屏然后重启 | 其它(复测) | 非HP4570 | 待拆机 | — | 待填 | 待填 |
+| 35 | 202604008442 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | 4570XYDn | 0.09 | 待填 | 待填 |
+| 36 | 202604002402 | 202604 | 充电异常,多次自动开机关机、表带过敏、睡 | 无充电电流,充电100%,充电80mA(正常充电) | 矛盾/复测正常 | 待拆机 | — | 待填 | 待填 |
+| 37 | 202604008455 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | 4570XYSm | 0.63 | 待填 | 待填 |
+| 38 | 202604008960 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | 4570XYSm | 0.63 | 待填 | 待填 |
+| 39 | 202604009839 | 202604 | 充电异常 | 无充电电流,充电100%,充电50mA(正常充电) | 矛盾/复测正常 | 待拆机 | — | 待填 | 待填 |
+| 40 | 202604009181 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | 4570XYSm | | 待填 | 待填 |
+| 41 | 202604009728 | 202604 | 无法开机 | 无法开机,其它(复测) | 非充电客诉 | 待拆机 | — | 待填 | 待填 |
+| 42 | 202604009032 | 202604 | 充电异常 | 无充电电流,能开机,电量 12%,充电电流 0mA | 硬失效 | 待拆机 | — | 待填 | 待填 |
+| 43 | 202604008842 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | 待拆机 | — | 待填 | 待填 |
+| 44 | 202604009714 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | 4570XYSm | | 待填 | 待填 |
+| 45 | 202604009150 | 202604 | 充电异常 | 无充电电流,能开机,电量 47%,充电电流 1mA | 硬失效 | 4570XYSm | 4.66 | 待填 | 待填 |
+| 46 | 202604010358 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | | | 待填 | 待填 |
+| 47 | 202604008708 | 202604 | 充电异常 | 充电56%,充电130mA(复测) | 复测有电流 | 待拆机 | — | 待填 | 待填 |
+| 48 | 202604004093 | 202604 | 充电异常 | 充电100%,充电20mA(复测) | 复测有电流 | 待拆机 | — | 待填 | 待填 |
+| 49 | 202605002372 | 202605 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | 待拆机 | — | 待填 | 待填 |
+| 50 | 202605008626 | 202605 | 触摸屏不灵敏 | 充电20%,充电10mA(复测) | 复测有电流 | 待拆机 | — | 待填 | 待填 |
+| 51 | 202604010227 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | 4570XYSm | 0.15 | 待填 | 待填 |
+| 52 | 202604008325 | 202604 | 充电异常 | 无充电电流,能开机,电量 9%,充电电流 0.031A | 硬失效 | 待拆机 | — | 待填 | 待填 |
+| 53 | 202603000429 | 202603 | 耗电异常、蓝牙打不开、抬腕亮屏不灵敏 | 其它(复测) | 非HP4570 | 待拆机 | — | 待填 | 待填 |
+| 54 | 202604010697 | 202604 | 充电异常 | 无充电电流,充电不开机,充电1mA | 硬失效 | | | 待填 | 待填 |
+| 55 | 202604009567 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | 4570XYSm | 0.3 | 待填 | 待填 |
+| 56 | 202604009345 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | | | 待填 | 待填 |
+| 57 | 202604009845 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | | | 待填 | 待填 |
+| 58 | 202604003653 | 202604 | 充电异常 | 充电到100%(复测) | 充电异常 | 待拆机 | — | 待填 | 待填 |
+| 59 | 202604009809 | 202604 | 充电异常 | 无充电电流,充电不开机,充电2mA | 硬失效 | | | 待填 | 待填 |
+| 60 | 202604009990 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | 4570XYSm | | 待填 | 待填 |
+| 61 | 202604008855 | 202604 | 充电异常 | 无充电电流,能开机,电量 5%,充电电流 0mA | 硬失效 | 待拆机 | — | 待填 | 待填 |
+| 62 | 202604008969 | 202604 | 充电异常 | 无充电电流,充电不开机,充电1mA | 硬失效 | | | 待填 | 待填 |
+| 63 | 202603001903 | 202603 | 充电异常、多次自动关机、屏幕亮度异常 | 充电100%,屏幕显示存在问题(复测) | 充电异常 | 待拆机 | — | 待填 | 待填 |
+| 64 | 202604008725 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | | | 待填 | 待填 |
+| 65 | 202604009044 | 202604 | 充电异常 | 无充电电流,能开机,电量25%,充电0mA | 硬失效 | 待拆机 | — | 待填 | 待填 |
+| 66 | 202604001488 | 202604 | 定位不准、自动关机、触摸屏不灵敏 | 其它(复测) | 非HP4570 | 待拆机 | — | 待填 | 待填 |
+| 67 | 202604008880 | 202604 | 充电异常 | 无充电电流,充电不开机,充电0mA | 硬失效 | 4570XYDn | | 待填 | 待填 |
+| 68 | 202605004535 | 202605 | 充电异常 | 无充电电流,充电不开机,充电2mA | 硬失效 | 4570XYDn | | 待填 | 待填 |
+| 69 | 202604009211 | 202604 | 充电异常 | 无充电电流,能开机,电量 1%,充电电流 0mA | 硬失效 | 待拆机 | — | 待填 | 待填 |
+| 70 | 202605001567 | 202605 | 充电异常,关机重启、 | 无充电电流,能开机,电量 33%,充电电流 0mA | 硬失效 | 4570XYSm | 0.06 | 待填 | 待填 |
+| 71 | 202604010689 | 202604 | 充电异常 | 无充电电流,充电不开机,充电1mA | 硬失效 | 4570XYSm | | 待填 | 待填 |
+| 72 | 202604000262 | 202604 | 充电异常、没有心率血氧显示、查不到运动记 | 充电100%,充电40mA(复测) | 复测有电流 | 待拆机 | — | 待填 | 待填 |
+
+---
+
+## 拆机丝印统计(5/28 批次,有记录 20 台)
+
+| 丝印 | 数量 | 备注 |
+|------|------|------|
+| 4570XYSm | 16 | suspect LOT,与 FA 不良片一致 |
+| 4570XYDn | 4 | 5/30 新板同标记,260mA 正常 |
+| (未录) | 10 | 需补读丝印 |
+
+---
+
+## DVT vs MP 物料 diff 清单(待质量填写)
+
+| 对比项 | DVT/EVT | MP(客退不良) | 是否变更 |
+|--------|---------|----------------|----------|
+| HP4570 丝印/date code | 待填 | 4570XYSm 为主 | 待确认 |
+| PCB 版本 | V1.0/V1.1? | V1.2 | 待确认 |
+| SMT 工厂/线体 | 待填 | 待填 | 待确认 |
+| 产测充电筛片 | 待填 | 仅 ID 检查 | **需加 SYS/电流筛** |
+
+---
+
+## 闳康 FA 送样追踪(阶段 6 归档)
+
+| 闳康编号 | 原 SN | 丝印 | 路径 | IV | OBIRCH | SEM | 报告结论 |
+|----------|-------|------|------|-----|--------|-----|----------|
+| Good-1 | 待选 | 4570XYSm/XYDn | 对照 | 待 | 待 | 待 | 待 |
+| Fail-1 | 待选 | 4570XYSm | A decap | 待 | 待 | 待 | 待 |
+| Fail-2 | 待选 | 4570XYSm | A decap | 待 | 待 | 待 | 待 |
+| Fail-3 | 待选(早期片若有) | — | B 背面+COB | 待 | 待 | 待 | 待 |
+
+送样前 checklist 见 [HP4570_闳康FA测试流程.md §5 阶段 0](./HP4570_闳康FA测试流程.md#5-分阶段-sop)。
+
+---
+
+## 场不良 ~200 台(待导入)
+
+```
+# 格式:SN, U900_marking, date_code, 失效天数, 是否202604批次
+# 待质量部从售后系统导出后与上表「场不良匹配」列 join
+```
+
+---
+
+## 下一步
+
+1. 从 72 台客退 + ~200 场不良提取 SN → 按 `202604` 等批次分组
+2. 拆机/来料台账读取 U900 date code → 与 FA 四颗 XYsm 对比
+3. 若 suspect LOT 确认 → 停用 + 产线加严筛片(已实现 PM testitem 0x06)
+4. 按 [HP4570_闳康FA测试流程.md](./HP4570_闳康FA测试流程.md) 送样闳康,结果回填上表「闳康 FA 送样追踪」
--- /dev/null
+# ATE 产测 — HP4570 筛片项(PM submode 0x06)
+
+> 对应根因计划产线拦截:I2C ID + VIN=5V 时 VBUS_PG + 10s 充电路径有效
+> 实现:[`factorytest_protocol.c`](../code/sdk/zhihui/services/factorytest_service/factorytest_protocol.c)
+
+## 命令
+
+| 字段 | 值 |
+|------|-----|
+| 模式 | `BLE_TEST_CMD_MODE_SOFT_TEST` (0x02) |
+| 子模式 | `BLE_TEST_CMD_SOFT_TEST_SUB_MODE_PM` (0x17) |
+| 测试项 | **0x06** |
+
+**前置条件**:整机插 5V 适配器,电池 SOC 不宜为 0%(建议 >20% 以便观察充电态)。
+
+**耗时**:约 **10 秒**(固件内 `LOS_Msleep(10000)` 后返回)。
+
+## 响应 payload(8 字节)
+
+| 偏移 | 含义 |
+|------|------|
+| 0 | **result**:0=PASS,1=未插 VBUS,2=I2C/ID 失败,3=VBUS_PG=0(SYS 路径异常),4=10s 后无有效充电路径 |
+| 1 | charge_id(正常应为 0x02) |
+| 2 | vbus_pg(1=Power Good) |
+| 3 | charge_stat(`get_charge_stat()`) |
+| 4-5 | 等待前 VBAT mV(大端 uint16) |
+| 6-7 | 等待后 VBAT mV(大端 uint16) |
+
+## 判定建议(产线 PC)
+
+```
+PASS: byte0 == 0
+FAIL: byte0 != 0 → 记录 SN,不良品换 U900 或退 SMT
+```
+
+| result | 含义 | 与 FA 关联 |
+|--------|------|------------|
+| 2 | I2C 不通 / ID 错 | 终态 I2C 全挂 |
+| 3 | VBUS_PG=0 | SYS≈0.6V 硬损坏 |
+| 4 | 10s 无充电路径 | 0mA 类失效 |
+
+## 老化测试增强
+
+老化循环中除 `charge_id==0x2` 外,增加:**插线充电时 VBUS_PG 必须为 1**。
+
+## 产测 UI
+
+[`testchanging.cpp`](../code/sdk/zhihui/UI/producttest/src/testchanging.cpp) 充电测试页显示 `vbus_pg` 与充电状态寄存器,便于人工复核。
--- /dev/null
+# HP4570 客退复测子集分析
+
+> 数据源:`Desktop/充电/FD272-0528客退机记录.xlsx` → `汇总表`(72 台有效 SN)
+> 目的:区分 **IC 硬损坏** vs **复测正常/非充电客诉/误报**
+
+---
+
+## 分类汇总
+
+| 类别 | 数量 | 与 HP4570 关系 |
+|------|------|----------------|
+| 状态确认含「无充电电流」(硬失效模式) | 49 | **高度相关** — 与 FA SYS≈0.6V 一致 |
+| 复测有充电电流(50–160mA) | 11 | **低相关** — 可能线束/接触/间歇/误报 |
+| 「其它(复测)」 | 9 | **无关** — 非充电类客诉 |
+| 客诉无电流但复测正常充电 | 2 | **误报或间歇** — 非终态硬损坏 |
+
+---
+
+## A. 复测有充电电流(11 台)— 非 IC 终态硬损坏
+
+| 序号 | 镭雕 SN | 状态确认 | 判定 |
+|------|---------|----------|------|
+| 8 | 202604004969 | 充电100%,充电90mA(复测) | 复测正常,客诉可能误报或间歇 |
+| 13 | 202604000633 | 充电100%,充电100mA(复测) | 同上 |
+| 18 | 202604001281 | 充电96%,充电160mA(复测) | 同上 |
+| 27 | 202604002502 | 充电100%,充电90mA(复测) | 同上 |
+| 28 | 202604004429 | 充电99%,充电60mA(复测) | 同上 |
+| 29 | 202605002701 | 充电100%,充电50mA(复测) | 同上 |
+| 30 | 202604005109 | 充电100%,充电90mA(复测) | 同上 |
+| 47 | 202604008708 | 充电56%,充电130mA(复测) | 同上 |
+| 48 | 202604004093 | 充电100%,充电20mA(复测) | 满电涓流,属正常 |
+| 50 | 202605008626 | 充电20%,充电10mA(复测) | 低 SOC 小电流,需观察是否预充 |
+| 72 | 202604000262 | 充电100%,充电40mA(复测) | 满电涓流,属正常 |
+
+**建议**:这 11 台 **不纳入 HP4570 硬失效统计**;若再次客退且 SYS<1V,再归入硬损坏。
+
+---
+
+## B. 「其它(复测)」(9 台)— 与 HP4570 无关
+
+| 序号 | 镭雕 SN | 异常类型 | 判定 |
+|------|---------|----------|------|
+| 3 | 202604001246 | 蓝牙断连 | 非充电问题 |
+| 4 | 202604002638 | 无法切换表盘、恢复出厂失效 | 软件/UI |
+| 10 | 202604005054 | 没有久坐提醒、表带脏 | 功能/外观 |
+| 12 | 202604005461 | 打开设置收不到消息提醒 | 通知/蓝牙 |
+| 15 | 202604003521 | 触摸屏幕不灵敏 | TP |
+| 17 | 202604004336 | 连接异常 经常断开 | 蓝牙 |
+| 34 | 202604000064 | 自动黑屏然后重启 | 系统稳定性 |
+| 53 | 202603000429 | 耗电异常、蓝牙打不开、抬腕亮屏不灵敏 | 综合/PM |
+| 66 | 202604001488 | 定位不准、自动关机、触摸屏不灵敏 | GPS/TP |
+
+**结论**:这 9 台 **不应计入「无充电电流 / HP4570 损坏」比例**。
+
+---
+
+## C. 描述矛盾(2 台)— 客诉 vs 复测不一致
+
+| 序号 | 镭雕 SN | 状态确认 | 解读 |
+|------|---------|----------|------|
+| 36 | 202604002402 | 无充电电流,充电100%,充电80mA(正常充电) | 用户侧报无电流,复测 80mA — **线束/座子/间歇** 或客诉描述不准 |
+| 39 | 202604009839 | 无充电电流,充电100%,充电50mA(正常充电) | 同上,满电涓流 50mA 属正常 |
+
+**建议**:返修 SOP 先测 **I2C ID + VBUS_PG + 10s 充电状态**,再决定是否拆机换 IC。
+
+---
+
+## D. 硬失效主集(49 台)
+
+状态确认字段含「无充电电流」且充电 0/1/2mA 或不开机 — 与 FA 四颗、拆机 SYS<1V 模式一致。
+完整 SN 列表见 [`plan/HP4570_LOT追溯交叉表.md`](plan/HP4570_LOT追溯交叉表.md)。
+
+---
+
+## 对「98% 良品后续风险」的含义
+
+- **49/72(68%)** 客退为确认硬失效模式 → 与 ~2% 场不良率方向一致
+- **11/72(15%)** 复测有电流 → **不能** 当作 IC 已死
+- **9/72(13%)** 非充电客诉 → 与 HP4570 无关
+- 当前 98% 良品若 **LOT 非 4570XYSm suspect 批**,后续全挂概率 **低于** 已暴露 2% 子集;需 LOT 对齐才能定量
--- /dev/null
+# HP4570 闳康 FA — 测试流程(按商务报价项)
+
+> 关联:[HP4570无电流根因分析.plan.md](./HP4570无电流根因分析.plan.md) · [HP4570_LOT追溯交叉表.md](./HP4570_LOT追溯交叉表.md) · [HP4570_产测筛片项说明.md](./HP4570_产测筛片项说明.md)
+> 供应商:闳康 MA-tek · 报价约 ¥8391(含 6% 税,2026-05-29)
+> 状态:**执行用 SOP**(样品到齐后按本文推进)
+
+---
+
+## 1. 目标(测什么、不测什么)
+
+| 要闭合的问题 | 不测什么 |
+|-------------|----------|
+| die 内 FET/IO 损坏 vs bump/键合 vs 封装应力 | 不再验证软件 CEB/Hi-Z(根因 plan 已排除) |
+| 与良品、同 LOT **4570XYSm** 对比 | 不对「复测仍有电流」的 11 台做全套 decap |
+| 渐进失效:早期片 vs 终态片 IV/OBIRCH 是否不同 | |
+
+---
+
+## 2. 商务报价项与阶段映射
+
+| 报价序号 | 项目 | 工时/数量 | 对应阶段 |
+|---------|------|-----------|----------|
+| 01 | I-V curve 量测 | 4h | 阶段 2 电性 IV |
+| 02 | 超高解析 3D 光学显微镜 | 2h | 阶段 1 外观 |
+| 03 | SEM/EDX | 1h | 阶段 5 形貌确认 |
+| 04 | 去锡球 (FCBGA/WLCSP…) | 2 颗 | 阶段 4A decap 链 |
+| 05 | 取晶粒 | 2 ea | 阶段 4A |
+| 06 | IC 解焊 | 2 颗 | 阶段 4A |
+| 07 | 晶背研磨-Package | 3 ea | 阶段 4B 背面链 |
+| 08 | 开盖后补线 | 3 颗 | 阶段 4B + COB |
+| 09 | OBIRCH | 0.75h | 阶段 3 缺陷定位(替代 EMMI) |
+| 10 | COB 公板-256L | 3 ea | 阶段 4B 可测载体 |
+
+**说明:** 本单用 **OBIRCH** 做漏电/高阻定位,未单列 EMMI;引脚对地阻抗(自然光好/坏对比)若未单列,在阶段 1 向闳康确认是否含在 IV/电性项内。
+
+---
+
+## 3. 样品分工(与报价数量对齐)
+
+| 编号 | 类型 | 数量 | 建议 SN/特征 | 走哪条路径 |
+|------|------|------|-------------|-----------|
+| **Good-1** | 良品对照 | 1 | 同 LOT XYSm 或 XYDn 260mA 正常 | 全程对照(IV/OBIRCH/SEM) |
+| **Fail-1** | 终态不良 | 1 | SYS<1V、I2C 不通、0~10mA | 路径 A:解焊→decap→SEM |
+| **Fail-2** | 终态不良 | 1 | 同上,不同 SN | 路径 A |
+| **Fail-3** | 早期/可偏置(若有) | 1 | 尚能 I2C 或拷机 HW:未充 寄存器 dump | 路径 B:背面研磨→补线→COB→OBIRCH |
+| (可选) | 不良主板 | 1 | 解焊前测板级 IV/阻抗 | 阶段 2 解焊前 |
+
+**不送样:** 客退复测仍有电流的 11 台(见 [HP4570_客退复测子集分析.md](./HP4570_客退复测子集分析.md))。
+
+---
+
+## 4. 总流程
+
+```mermaid
+flowchart TD
+ P0[阶段0: 收样与台账] --> P1[阶段1: 非破坏外观]
+ P1 --> P2[阶段2: 板级/裸片电性 IV]
+ P2 --> P3[阶段3: OBIRCH 定位]
+ P3 --> P4[阶段4: 制样 decap/背面/补线]
+ P4 --> P5[阶段5: SEM/EDX 确认]
+ P5 --> P6[阶段6: 报告与 LOT 结论]
+```
+
+---
+
+## 5. 分阶段 SOP
+
+### 阶段 0 — 收样与台账(开工前)
+
+**智汇视讯提供(送样清单):**
+
+- [ ] SN、镭雕批次(202603/202604/202605)
+- [ ] U900 丝印(4570XYSm / 4570XYDn)
+- [ ] 失效天数、首周是否高强度充放/频繁插拔
+- [ ] 现场:**SYS、VBAT、I2C 通断、充电 mA**
+- [ ] 若有:拷机 `cc_cycle_defect.bin` / `cc_cycle_event_log.txt` / 寄存器 dump
+- [ ] 与 [HP4570_LOT追溯交叉表.md](./HP4570_LOT追溯交叉表.md) 行号对应
+
+**闳康输出:** 样品编号表(Good-1、Fail-1/2/3…),全报告统一引用。
+
+---
+
+### 阶段 1 — 非破坏外观(**02** 3D 显微镜,2h)
+
+| 项 | 内容 |
+|----|------|
+| 对象 | 不良主板 U900 区 + **解焊前** 不良/良品 IC 封装 |
+| 目的 | 封装机械损伤、锡球虚焊、偏位、裂纹(对照根因 plan #1~#3 外观) |
+| 同步确认 | 自然光下 **好/坏引脚对地阻抗** 是否在本阶段或 IV 项内完成 |
+
+**交付:** 3D 照片 + 外观结论(SMT/封装可疑 / 无明显封装异常)。
+
+---
+
+### 阶段 2 — 电性 IV(**01** I-V,4h)
+
+| 项 | 内容 |
+|----|------|
+| 顺序 | 解焊前能测则测主板关键脚;**06 解焊后** 裸片/封装脚完整 IV |
+| 对象 | Fail-1/2/3 + Good-1;**终态 vs 早期片对比(若有 Fail-3)** |
+| 关注脚 | VIN、SYS、BAT、SDA/SCL、INT(与 plan 二极管/OL 表一致) |
+| 目的 | 印证 Q1/Q2 路径异常、IQ 极低、与良品 IV 差异 |
+
+**判定:** IV 已与良品严重偏离 → 进入 OBIRCH,不再讨论软件禁充。
+
+---
+
+### 阶段 3 — OBIRCH 定位(**09**,0.75h)
+
+| 项 | 内容 |
+|----|------|
+| 时机 | **IV 异常之后、开盖/decap 之前** |
+| 对象 | 可加电偏置的 Fail 片;I2C 全挂时先做 **07+08+10** 再做 OBIRCH |
+| 目的 | 非破坏定位 die 内漏电/高阻坐标,供 SEM 开盖位置 |
+| 与 EMMI | 本单 **OBIRCH 替代 EMMI**;报告需写清定位坐标 |
+
+**交付:** OBIRCH 异常点坐标图。
+
+---
+
+### 阶段 4 — 制样(破坏性)
+
+#### 路径 A — Fail-1/2:decap + SEM(主路径)
+
+| 顺序 | 报价项 | 说明 |
+|------|--------|------|
+| 1 | **06** IC 解焊 ×2 | 从主板取下 U900 |
+| 2 | **04** 去锡球 ×2 | CSP 去球(按需要) |
+| 3 | **05** 取晶粒 ×2 | 开盖/decap 准备 |
+| 4 | **03** SEM/EDX | 在 OBIRCH 指示区 + 功率管区拍摄 |
+
+#### 路径 B — Fail-3:背面 + COB + OBIRCH
+
+| 顺序 | 报价项 | 说明 |
+|------|--------|------|
+| 1 | **07** 晶背研磨 ×3 | 背面减薄 |
+| 2 | **08** 开盖后补线 ×3 | 打线至 **10 COB 公板** |
+| 3 | **09** OBIRCH | COB 上定位(若阶段 3 未做或未明) |
+| 4 | **03** SEM/EDX | 按 OBIRCH 点确认形貌 |
+
+**分工:** A 与 B **不互斥**;报价数量(2+3)支持 Fail-1/2 走 A、Fail-3 走 B。
+
+---
+
+### 阶段 5 — SEM/EDX 确认(**03**,1h)
+
+| 项 | 内容 |
+|----|------|
+| 输入 | OBIRCH 坐标 + IV 异常脚 + 芯德 #3 已有 die 裂纹对照 |
+| 对比 | **Fail vs Good-1** 同 LOT |
+| 判定 | bump/键合疲劳 vs 瞬间烧毁;FET 区形貌;是否与已知裂纹一致 |
+
+**注意:** SEM 仅 1h,开工前约定 **优先 OBIRCH 指向区 + 功率管区**,不全 die 扫描。
+
+---
+
+### 阶段 6 — 报告与内部闭环
+
+闳康备注含 **失效分析报告 + 电子数据**。内部需完成:
+
+1. **LOT 结论**:XYSm vs XYDn、202604 是否停批/加严
+2. **责任倾向**:die 潜伏 vs SMT/ESD vs 使用应力加速
+3. **产线**:继续 ATE **PM 0x06** + 拷机筛片(FA 不替代出货拦截)
+4. **归档**:IV/OBIRCH/SEM/3D → 更新 [HP4570_LOT追溯交叉表.md](./HP4570_LOT追溯交叉表.md) FA 列
+
+---
+
+## 6. 时间线(整单约 1 周)
+
+| 阶段 | 预估 | 并行 |
+|------|------|------|
+| 0 收样 | 0.5d | — |
+| 1 3D | 0.5d | 可与 2 部分并行 |
+| 2 IV | 1~2d | 不良+良品 |
+| 3 OBIRCH | 0.5~1d | 依赖 COB 是否就绪 |
+| 4 制样 | 2~3d | 解焊/研磨/补线瓶颈 |
+| 5 SEM | 0.5~1d | 依赖 4 |
+| 6 报告 | 1~2d | — |
+
+**条件:** Day1 样品到齐且 Fail-1/2 与 Fail-3 分工明确 → 可在一周内出初步结论。
+
+---
+
+## 7. 发给闳康的一句话 SOP(可转发)
+
+> 请按:**3D 外观 →(确认引脚阻抗好/坏)→ IV(含早期片若有对比)→ OBIRCH 定位 → 解焊/背面制样+COB 补线 → SEM/EDX 在 OBIRCH 点确认**;Fail-1/2 主做 decap+SEM,Fail-3 主做背面+COB+OBIRCH;良品同 LOT 全程对照。报告需明确 OBIRCH 定位结论及 **die vs 焊点** 责任倾向。
+
+---
+
+## 8. 与软件/拷机的关系
+
+| 手段 | 作用 |
+|------|------|
+| **拷机**(HW:未充、紫/黄不良等) | 产测抓退化,保存寄存器快照供 FA 参考 |
+| **闳康 FA** | 物理根因与批次决策,**不能修复已终态坏片** |
+
+两者并行、不互相替代。
--- /dev/null
+# HP4570 充电无电流 — 根因分析(已确认:芯片物理损坏)
+
+> 状态:**已确认** — FA + 批次特征指向 IC 本体失效,非软件可修复。
+> 数据:`HP4570XYsm不良4pcs测试_20260527.xlsx`、`3#芯德分析.xlsx`
+> 规模:~200/10000(约 2%),**出货后**发现,**不良集中在某几个批次**。
+> **重要现象(用户反馈)**:用户收到后**前期可正常使用**,使用一段时间后才会出现**充电无电流** — 属于**潜伏/渐进失效**,非「一开机就坏」。
+> **失效窗口(当前统计倾向)**:收到后约 **几天~2 周内** 暴露 — 偏快,更符合 **严重潜伏缺陷 + 初期高强度充放/插拔**,而非数月缓慢老化。
+
+> **阶段对比(用户反馈)**:**EVT / DVT 从未出现**此类无充电电流 / HP4570 硬损坏 — **问题为 MP 量产阶段特有**,设计验证阶段电路与早期物料链路基本可信。
+> **客退数据(已导入)**:`Desktop/充电/FD272-0528客退机记录.xlsx`(扶鹰 20260528 批次,72 台有效 SN)
+
+---
+
+## 客退数据摘要(FD272-0528,72 台)
+
+| 指标 | 数量 | 说明 |
+|------|------|------|
+| 客退总台数 | 72 | 均有 `202603/202604/202605` 镭雕 SN |
+| 异常类型含「充电异常」 | 56 | 另 16 台为蓝牙/触摸/耗电等非充电客诉 |
+| 状态确认含「无充电电流」 | **49** | 硬失效主模式(68%) |
+| 状态确认含「不开机」 | 30 | 与无电流高度重叠 |
+| 充电 0mA / 近 0(状态确认) | 51 | 含 0/1/2mA |
+| 标记拆机 | 30 | 与拆机 sheet 对应 |
+| 镭雕 `202604*` | **60** (83%) | MP 四月批次占绝对多数 |
+| 镭雕 `202605*` | 10 (14%) | |
+| 镭雕 `202603*` | 2 | |
+
+**复测子集(见 [`plan/HP4570_客退复测子集分析.md`](plan/HP4570_客退复测子集分析.md))**:
+
+- 复测有正常充电电流(50–160mA):**11 台** — 可能非 IC 硬损坏
+- 「其它(复测)」:**9 台** — **均为非充电客诉**(蓝牙/触摸/GPS 等),与 HP4570 无关
+- 客诉「无充电电流」但复测「正常充电」:**2 台** — 误报或间歇接触
+
+**解读**:约 **49 台** 符合 FA 硬损坏模式;**11 台** 复测有电流需单独归类;**9 台「其它(复测)」不计入 HP4570 失效**。
+
+---
+
+## 拆机验证(客退 xlsx,与 FA 四颗一致)
+
+### 5月28日批次(30 台)
+
+- **丝印**:4570XYSm **16 台**,4570XYDn **4 台**(10 台未录丝印)
+- **SYS 实测**(10 台有数值):**8 台 SYS < 1V**(0.03–0.63V),1 台正常 4.66V
+- **典型不良**:不开机不充电、无充电电流 0mA、升级后 I2C 通讯失败
+- **处置**:更换新 IC 后多数恢复正常;部分送原厂/第三方磨片
+
+### 5月27日之前批次
+
+- 表内总结:**「初步结论为充电 IC 损坏」**
+- **#6**:SYS **0.06V**,VBAT 3.91V — 与 FA 一致
+- **#7**:换 IC 后 SYS **4.63V** / VBAT **3.93V** 恢复
+- **跳过充电 IC 直接给主板供电可正常开机** → 主控 OK,问题在 U900
+- 原厂已带走 **3pcs** 坏片切片(衔接 `3#芯德分析.xlsx`)
+
+### 二极管测量
+
+- 26 号 / 7 号坏板:5V/VSYS/VBAT 等脚 0.39–0.58V 正常导通
+- **6 号主板(无充电 IC)**:5V/INT/VDD/NTC 等多脚 **OL**
+
+### 5月30日 SMT 新主板(对照组)
+
+- 丝印 **XYDn**,充电 **260mA** 正常 — 新料/新批次表现正常
+
+### 更换 IC 后测试
+
+- 多数单元充放电正常;**#2 换 IC 后仍 490mA 异常** — 需区分纯 IC 坏 vs 其它故障
+
+### 客退数据结论
+
+1. **硬损坏占比高**:49 台无充电电流 + 拆机 SYS 异常 + 换 IC 能好 → 与 FA 物理损坏一致,**非刷机能修**
+2. **4570XYSm 与不良强相关** — 83% 客退为 202604 镭雕;XYDn 新板 260mA 正常
+3. **拷机定位**:筛 49 类硬失效;对 11 类复测正常帮助有限
+
+**LOT 追溯**:72 台 SN 清单与交叉表见 [`plan/HP4570_LOT追溯交叉表.md`](plan/HP4570_LOT追溯交叉表.md)
+
+---
+
+## EVT/DVT 无、MP 有:说明什么
+
+| 推断 | 理由 |
+|------|------|
+| **大概率不是原理图/选型根本错误** | EVT/DVT 已覆盖充放、温升、长期试用;若 IN/SYS/BAT 拓扑或 HP4570 用法错误,验证机应早暴露 |
+| **大概率不是当前软件逻辑「写错就永久无充」** | DVT 与 MP 同套 firmware 主线时,纯软件 bug 应在 DVT 复现 |
+| **高度怀疑 MP 环节变化** | 量产才出现的 2%、批次集中、后期硬失效 → 指向 **物料 LOT / SMT / 板厂 / 产测筛片强度** 在 DVT→MP 切换时发生变化 |
+
+```mermaid
+flowchart TB
+ subgraph ok [EVT/DVT 无问题]
+ Design["原理图 U900/HP4570\nTVS/VBUS 设计"]
+ EarlyLot["工程样机料卷/手工或小批 SMT"]
+ SmallN["样本少 潜伏片未统计显著"]
+ end
+ subgraph mp [MP 出现问题]
+ MpLot["量产物料 LOT 切换\nXYsm 某几批"]
+ MpSmt["量产线回流/贴装/ESD"]
+ MpPcb["PCB 版本 V1.1/V1.2\n钢网/工艺变更"]
+ MpTest["ICT 短时测 漏筛潜伏片"]
+ end
+ ok -->|"DVT→MP 变更点"| mp
+ mp --> Field["用户 数天~2周 硬失效"]
+```
+
+### DVT → MP 必须逐项 diff 的清单(给硬件/质量)
+
+1. **HP4570 货源**:DVT 用料 date code / 代理商 vs MP 不良 LOT — **最优先**
+2. **PCB 版本**:EVT/DVT 用 V1.0/V1.1?量产是否 V1.2?充电区铜皮/TVS 位号/过孔有无变更
+3. **SMT 工厂与程序**:是否换线、换钢网、换回流曲线;CSP 4570 的 voiding 标准是否执行
+4. **TVS / 输入保护器件**:4.8V / 18V / 24V 型号或贴装是否 second source
+5. **产测/老化**:DVT 是否每台长时间充放;MP 是否只做短测 — 解释 EVT 无、MP 漏出潜伏片
+6. **电池/结构**:量产电池供应商、连接器压接是否引入额外应力(间接,次要)
+
+**拷机 v4**:若在 **MP 产测** 才大规模启用,而 DVT 未跑 — 拷机是 **MP 新增应力**,但仍需 **suspect LOT** 才解释 2%;对健康 DVT 同料应安全。
+
+---
+
+## 失效时间线:前期能用 → 后期硬损坏
+
+这与 FA「最终态 I2C 全挂 + Q1/Q2 路径 OL」**不矛盾**:
+
+```mermaid
+flowchart LR
+ subgraph factory [出厂/早期]
+ Latent["潜伏缺陷\n微裂纹/弱 ESD/边际 FET"]
+ Pass["产测/ICT 可能通过"]
+ Normal["用户前期充电正常"]
+ end
+ subgraph field [使用过程]
+ Stress["热循环/插拔/深放满充\n大负载/跌落"]
+ Degrade["裂纹扩展 / FET 退化\nI2C 间歇→永久失效"]
+ end
+ subgraph endstate [失效终态 FA]
+ Dead["SYS≈0.6V BAT≈0\nI2C 不通 无充电电流"]
+ end
+ Latent --> Pass --> Normal --> Stress --> Degrade --> Dead
+```
+
+| 阶段 | 表现 | 为何产线难抓 |
+|------|------|----------------|
+| **潜伏** | 功能正常,I2C/充电 OK | ICT 只做短时静态测,看不到累积损伤 |
+| **退化** | 偶发充不进、重启好、温高时更明显 | 用户未必立即返修 |
+| **终态** | 永久无电流,与 FA 四颗一致 | 返修拆机 FA |
+
+**含义**:不是单纯「SMT 贴错一片死一片」,更像是 **某几批料带先天薄弱点 + 用户使用应力把问题跑成硬失效**。
+
+---
+
+## 最符合「前期能用」的机制(按可能性)
+
+1. **die / bond 微裂纹(与 #3 切片一致)**
+ 出厂未断,热循环(充电发热、季节温差)、跌落、挤压后裂纹扩展 → Q1/Q2 最终开路等。
+
+2. **ESD 或 IN/VBUS 瞬态造成的潜伏损伤**
+ 早期 I2C/功率管「还能工作」;多次热插拔、劣质适配器后 IO 或 FET 永久失效(SDA 压降偏高符合方向)。
+
+3. **边际批次 FET / 工艺漂移**
+ 同 LOT 部分 chip 裕量小,在 **反复 SCP hiccup、高温充电、大放电电流** 下先坏;良品同场景仍正常 → 表现为 2% 且批次集中。
+
+4. **焊点 / CSP 互连疲劳(需与纯 die 失效区分)**
+ 早期接触电阻正常,振动/热疲劳后 IN/SYS/BAT 等效开路;**终态测起来也像 IC 死**。FA 若只做板外裸片测试需确认是 die 还是 bump 问题。
+
+**相对次要**:纯软件配置错误(不会「用一阵才永久无电流」)。
+
+---
+
+## 使用场景里可能「加速」的因素(待与用户数据对齐)
+
+| 因素 | 说明 |
+|------|------|
+| 插线次数多 | 每次插线 [`hp4570n_reset()`](code/sdk/zhihui/services/battery/service/battery_service.cpp) + WDT kick,边际 chip 多一次寄存器/状态冲击 |
+| 深放再快充 | 用户用到自动关机再充;HP4570 预充/死电→大电流 CC 阶段热应力大 |
+| 高温环境充电 | 触发 THERM 降流/反复开关充电路径 |
+| 大负载边充边用 | GPS、亮屏、4G 等(与拷机放电类似但用户侧通常较轻) |
+| 产测/老化/拷机 | 若出厂已跑过充放循环,**可能缩短潜伏时间**;需统计不良机是否经老化/拷机工位 |
+
+**拷机 v4**:对**健康 chip** 属手册内工况;对**潜伏裂纹 chip** 可能**缩短从「能用」到「硬损坏」的时间**,适合作为**加严筛选**,不宜单独当根因。
+
+---
+
+## 修订后的首因排序(含 EVT/DVT 对比)
+
+1. **MP 阶段 HP4570 某几批 LOT 潜伏质量问题**(DVT 未用或未覆盖该 LOT)
+2. **MP SMT / 板厂工艺变更**(相对 DVT 小批贴装)— CSP 裂纹、X-ray 漏检
+3. **MP 产测筛片弱于 DVT**(DVT 长测 + MP 短 ICT → 潜伏片出厂)
+4. **用户首周使用应力** — 数天~2 周内触发潜伏缺陷
+5. **软件/拷机** — MP 新增或加严的循环应力,**加速剂**;不能解释 EVT/DVT 无、MP 有
+
+**可基本排除(作为主因)**:原理图 HP4570 用法错误、FD272 充电拓扑设计缺陷 —— 与 **EVT/DVT 从未发生** 不符。
+
+---
+
+## 建议行动(按「后期失效」修订)
+
+### 质量 / FA
+
+1. **向用户/售后收集**:收到至失效的**具体天数、初期是否频繁插拔/是否用到自动关机/首周充电时长** — 与 LOT 交叉(当前倾向:**数天~2 周**)。
+2. **重点问首周行为**:新机首日是否长时间充电+亮屏+定位;是否用过非原装头 — 与「快速进入终态」强相关。
+2. **LOT 追溯**不变;增加:**失效机是否同一生产周、是否同一老化批次**。
+3. **对比 FA**:早期返修机(尚能 I2C)vs 终态机 — 是否存在**退化曲线**(I2C 时好时坏 → 永久挂)。
+4. **decap/SEM**:重点看 **bond wire / bump / FET 区域** 是瞬间烧毁还是疲劳开裂。
+
+### 产线拦截(修订)
+
+- 仅 ICT **不够**;对 suspect LOT 建议加:
+ - **高温充放循环 24~48h** 或 **插拔 500 次 + 每 N 次测 I2C/SYS/充电电流**
+ - 与拷机 v4 类似的**有限循环老化**作为**筛片手段**(在 suspect 批次上),而非全员长期拷机。
+
+### 软件(辅助)
+
+- 无充时 **寄存器 dump + 日志**(是否曾有过 WDT fault / THERM / I2C fail 计数)— 便于区分「突然硬死」vs「渐进异常」。
+- 可选:插线 reset 改为仅 WDT fault 时 reset,降低对边际 chip 的重复冲击。
+
+---
+
+## 已确认失效模式(终态,FA 四颗)
+
+**不是**「CEB 没开 / Hi-Z 没关 / 温控停充」这类软件无电流,而是:
+
+| 证据 | 说明 |
+|------|------|
+| I2C 全挂 | 5V、3.7V 下均无法通讯 → 数字控制面失效 |
+| SYS ≈ 0.6V(VIN=5V) | IN→SYS 功率管 Q1 未建立正常供电 |
+| BAT ≈ 0V(VIN=5V) | 充电路径不存在 → **用户看到「完全无充电电流」** |
+| BAT↔SYS 二极管 OL | Q2(BAT FET)路径损坏;良品约 1.39V |
+| IQ 仅 9~18µA | 非正常充电态(良品 314~343µA) |
+| #3 切片 | die 层裂纹(芯德 FA) |
+| 外观 | #1~#3 封装机械损伤 |
+
+**结论:终态为 U900 HP4570 内部功率 FET + 部分数字/IO 物理损坏;但用户侧往往经历「先正常、后失效」的退化过程,根因是潜伏缺陷 + 使用/循环应力。**
+
+---
+
+## 首因排序(旧版简述,见上文「修订后」)
+
+1. **来料/封测批次潜伏缺陷** — 最高优先级
+2. **SMT / CSP 应力**
+3. **ESD / VBUS 瞬态**
+4. **使用与循环应力加速** — 解释「前期能用」的关键
+
+**拷机 Hi-Z 放电**:手册内模式;对潜伏损伤 chip 可能**缩短寿命**,可作 suspect LOT 加严筛选。
+
+---
+
+## 现场快速判定(返修/售后)
+
+插 5V 适配器,万用表测(或产测读数):
+
+- **SYS < 1V 且 I2C 不通** → 判定 **HP4570 硬损坏**,换 U900
+- **SYS ≈ 4.6V、I2C 正常、仍无电流** → 才查软件/NTC/线束
+
+---
+
+## 建议行动(硬件/质量优先)
+
+1. **LOT 追溯**:200 台不良 date code → 停用/加严该批来料;**72 台客退 SN 交叉表**见 [`plan/HP4570_LOT追溯交叉表.md`](plan/HP4570_LOT追溯交叉表.md)
+2. **产线拦截**(SMT 后或整机首测):I2C 读 ID + VIN=5V 时 VBUS_PG + 10s 充电路径有效 — **已实现** ATE `PM submode testitem 0x06`([`factorytest_protocol.c`](code/sdk/zhihui/services/factorytest_service/factorytest_protocol.c))
+3. **SMT/ESD 审计**:回流、pick 压力、CSP X-ray、腕带/治具
+4. **FA 加深**:decap/SEM 对比良品与同 LOT 不良 — **执行 SOP 见** [`plan/HP4570_闳康FA测试流程.md`](plan/HP4570_闳康FA测试流程.md)(闳康报价 IV/OBIRCH/SEM 等项)
+
+## 软件侧(辅助,不能替代换片)
+
+- 产测增加 HP4570 快速筛片项 — **ATE PM 0x06**(I2C ID + VBUS_PG + 10s 充电态)
+- 无充时寄存器 dump,区分「禁充」vs「I2C/SYS 异常」
+- 可选:HP4570 I2C 互斥、减少插线即 `hp4570n_reset()`
+
+---
+
+## 与拷机 v4 的关系
+
+拷机用于**循环应力与开机恢复验证**;对**已物理损坏**的 IC 无法修复,但可在产测/SMT 后用「首充 + I2C」把坏片挡在出货前。
--- /dev/null
+---
+name: 充放电拷机汇总
+overview: FD272/HP4570 插线充放电自动拷机(cc_cycle)完整方案汇总:状态机、UI、持久化、充电不良、隔离门控、充电电流与 SOC 显示。
+branch: feature/cc-cycle-burn-test
+updated: 2026-06-02
+open_issues:
+ - 拷机界面 9% vs 重启表盘 23% SOC 不一致(B1~B4 已合入,待现场确认)
+ - 左滑退出拷机页崩溃修复待现场确认
+ - 充电不良检测(红/黄/蓝/低电流/HW未充)现场验证
+ - 拷机阶段切换 vs 插拔充电线 ICHG 一致性待现场确认
+ - 拷机运行中其他产测页文字消失 / 界面被抢(§二十)
+ - 拷机运行后其它页只显示几个字、拷机页正常、重启可恢复(§20.8)
+ - 拷机页内标题/寄存器 overlay/拷机记录也字不全(§20.8)
+---
+
+# 充放电拷机(cc_cycle)方案汇总
+
+> 关联文档:[充电电量显示分析.plan.md](./充电电量显示分析.plan.md)
+> 关联文档:[拷机放电至关机循环.plan.md](./拷机放电至关机循环.plan.md)
+
+### 更新记录
+
+| 日期 | 能力 | 说明 |
+|------|------|------|
+| 2026-06 初 | 阶段计时 / 充电不良 / IC 状态 | bin v7;上次/本次 HH:MM:SS |
+| 2026-06 中 | 拷机记录 overlay / 低电落盘 / pending_charge | event_log 配对;F1/F2 自然掉电续跑 |
+| 2026-06 末 | **充放电拷机汇总 UI** | 全屏页:当前时间(含秒)、上次/本次充放电时长(蓝/红) |
+| 2026-06 末 | **回到拷机界面** | 左滑退出后可回全屏;`cc_cycle_reopen_burn_ui()` |
+| 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 后台占用(§二十) |
+
+---
+
+## 〇、设计原则(必读)
+
+| 规则 | 说明 |
+|------|------|
+| **拷机开关关**(`test_enabled=0`) | 对正常用户模式、工厂其它测试项 **零影响**(不 Hi-Z、不改 SOC 算法、不弹拷机 UI) |
+| **开关开 + 已开始 + 未停止** | **必须一直拷机**(离开产测页、重启、插线恢复均继续) |
+| **显式停止** | 点「停止拷机」或关拷机开关 → 释放 HW,恢复正常充电 |
+
+**统一门控**:`cc_cycle_is_active_hw()` = `test_enabled && auto_running`
+
+```mermaid
+flowchart LR
+ sw["拷机开关 test_enabled"]
+ run["已开始 auto_running"]
+ hw["HP4570/GPS/常亮/SOC特殊"]
+ sw -->|0| ok["正常充放电/表盘"]
+ sw -->|1| run
+ run -->|1| hw
+ run -->|0| ok
+```
+
+---
+
+## 一、功能目标
+
+**插线自动循环拷机**:充电线保持插入,设备在产测模式下自动交替执行:
+
+| 阶段 | 行为 | 切换条件 |
+|------|------|----------|
+| **放电** | HP4570 Hi-Z + 禁充,电池供电;白屏常亮 + GPS | **自然掉电**;SOC ≤ **5%** 写 `pending_charge`;**不再** 10% 强制转充 |
+| **充电** | 恢复 VBUS/允充;ICHG 由 `battery_service` 按温控/SOC 管理 | SOC ≥ **99%** 或 CHG_DONE |
+| **循环** | 完成一轮充电记 `round_count++`,写日志,再进入放电 | 自动 |
+
+掉线(拔充电线)→ 暂停循环、恢复默认充电 IC、退出拷机 UI。
+
+**充电阶段产测监控**(拷机仍继续):SOC 反降→红;充电超 2h 未满→黄;SOC 卡 80%→蓝;左滑退出后可查 IC 寄存器快照。
+
+---
+
+## 二、当前分支与提交
+
+| 项 | 状态 |
+|----|------|
+| 分支 | `feature/cc-cycle-burn-test` |
+| 主提交 | `fecd7a21d` — 添加产测充放电拷机功能(FD272/HP4570) |
+| 平台 | **FD272 + ENABLE_HP4570**(产测 UI 与 cc_cycle 均 `#ifdef ENABLE_HP4570`) |
+| 合入主分支 | ⬜ 待确认 |
+
+---
+
+## 三、架构概览
+
+```mermaid
+flowchart LR
+ subgraph ui [产测 UI]
+ testView["testView 入口"]
+ testCcCycle["testCcCycle 控制页"]
+ burnView["ccCycleBurnView 全屏拷机页"]
+ end
+ subgraph svc [cc_cycle_service]
+ task["cc_cycle_task 1s tick"]
+ store["cc_cycle.bin v7"]
+ defectSnap["cc_cycle_defect.bin"]
+ log["cc_charge_log.txt"]
+ end
+ subgraph hw [硬件/充电]
+ hp4570["HP4570 Hi-Z/CEB/ICHG"]
+ battery["battery_service 温控/电流"]
+ end
+ subgraph soc [SOC 算法]
+ forceDischarge["force_discharge_soc 放电阶段"]
+ end
+
+ testView --> testCcCycle
+ testCcCycle --> task
+ task --> hp4570
+ task --> battery
+ task --> forceDischarge
+ task --> store
+ task --> log
+ task --> burnView
+ custom_utils["custom_utils 开机 cc_cycle_on_boot"] --> task
+```
+
+---
+
+## 四、已完成 ✅
+
+### 4.1 核心服务 `cc_cycle_service`
+
+| 模块 | 文件 | 状态 |
+|------|------|------|
+| 状态机(IDLE / DISCHARGE / CHARGING) | `cc_cycle_service.c` | ✅ |
+| 持久化 `user/userdata/cc_cycle/cc_cycle.bin`(**v7**:时间戳 + 上次阶段时长 + 不良状态) | 同上 | ✅ |
+| 不良快照 `user/userdata/cc_cycle/defect.bin`(寄存器 dump + IC 状态) | 同上 | ✅ |
+| 事件日志 `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) | 同上 | ✅ |
+| 放电自然掉电 + 5% `pending_charge`(F1) | 同上 | ✅ |
+| 关机保留续跑标志 `cc_release_hw_only_locked`(F2) | 同上 | ✅ |
+| 1s 周期任务 `cc_cycle_svc` | 同上 | ✅ |
+| 开机恢复 `cc_cycle_on_boot()` | 同上 + `custom_utils.c` | ✅ |
+| 掉线保护(停循环、恢复 IC + reconfig) | 同上 | ✅ |
+| 拷机开关隔离 `cc_cycle_is_active_hw()` + bin 消毒 | 同上 | ✅ |
+| 阶段切换 ICHG 重配 `battery_request_charge_reconfig()` | 同上 + `battery_service.cpp` | ✅ |
+| 汇总 UI `cc_cycle_get_burn_summary()` + 回到拷机界面 | UI 层 | ✅ |
+| 状态行文案 `cc_cycle_format_status_line()` | 同上 | ✅ |
+
+### 4.2 产测 UI
+
+| 模块 | 文件 | 状态 |
+|------|------|------|
+| 产测入口「充放电拷机」 | `testView.cpp` | ✅ |
+| 控制页(开关 / 开始·停止 / 充电 IC 状态 / 1s 刷新) | `testCcCycle.cpp` | ✅ |
+| 全屏拷机页(多行信息 + 不良底色 + 铃声) | `ccCycleBurnView.cpp` | ✅ |
+| 左滑返回控制页(延迟切 slice,停铃,防崩溃) | `ccCycleBurnPresenter.cpp` | ✅ |
+| 拷机 SOC 统一源 `cc_cycle_get_soc()` | 两页 UI + service | ✅ |
+| 屏幕常亮 | 放电由 `cc_cycle_service` 控;充电可灭屏 | ✅ |
+
+#### 全屏拷机页布局(`ccCycleBurnView`)
+
+| 行 | 控件 | 内容 | 刷新 |
+|----|------|------|------|
+| 顶 | `phaseLabel_` | 「充电中」/「放电中」/「空闲」 | 1s |
+| 中 | `socLabel_` | 大号 `xx%`(`cc_cycle_get_soc()`) | 1s |
+| | `metricsLabel_` | `V:3.62V T:28.5C`(`battery_get_voltage` + `battery_get_battery_temp`) | 1s |
+| | `durationLabel_` | `上次充电:HH:MM:SS 本次充电:HH:MM:SS`(放电同理) | **1s,本次实时递增** |
+| | `timeLabel_` | `开始:MM-DD HH:MM 充:… 放:…`(`cc_cycle_format_time_line`) | 1s |
+| | `defectLabel_` | 【不良】文案(有则显示) | 1s |
+| | `statusLabel_` | 阶段详情 / HiZ / HW:充!(`cc_cycle_format_status_line`) | 1s |
+| 底 | `footerLabel_` | 「左滑返回」 | 静态 |
+
+阶段底色:充电浅绿 `#C8E6C9`、放电白屏、空闲灰;**不良覆盖**:红 `#FFCDD2`、黄 `#FFF9C4`、蓝 `#B3E5FC`。
+
+**汇总区**(§十六):标题「充放电拷机汇总」、当前时间(含秒)、上次/本次充放电时长(蓝/红 Label)。
+
+#### 控制页布局(`testCcCycle`)
+
+466×466 圆形屏:内容区宽 `w-86`(380px)、水平居中 `x=(w-contentW)/2`,与产测主列表一致。
+
+| 行 | 控件 | Y | 内容 |
+|----|------|---|------|
+| 标题 | `title_` | 40 | 「充放电拷机」居中 |
+| 阶段卡 | `phaseLabel_` | 78 | 「阶段: 充电中/放电中/空闲」+ 同色块 |
+| SOC | `socLabel_` | 122 | 黄色 40pt `xx%` |
+| 指标 | `metricsLabel_` | 182 | `V:xx.xxV T:xx.xC` |
+| 时刻 | `timeLabel_` | 218 | 同 `cc_cycle_format_time_line` |
+| 时长 | `durationLabel_` | 250 | 同 `cc_cycle_format_duration_line`(**本次 1s 实时**) |
+| 状态 | `statusLabel_` | 286 | 状态行 + 不良时红色高亮(可多行) |
+| 提示 | `hintLabel_` | 314 | 默认插线提示;有不良时引导点 IC 状态 |
+| 按钮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_` + 「返回拷机页」「刷新实时」。
+
+**铃声**:不良首次触发 `AlarmPlayerStart`;左滑退出全屏 `AlarmPlayerStop`(拷机后台不停止)。
+
+#### 左滑退出(防 GuiMainThread 崩溃)
+
+`OnDragEnd` 内不再同步 `ChangeSlice` + `ChangeView`,改为:
+
+1. `cc_cycle_hide_burn_ui()` → `GraphicService::PostGraphicEvent(cc_burn_hide_ui_deferred)`
+2. `factorytest_set_pending_subview(CCCYCLE)` + 仅 `ChangeSlice(VIEW_TEST)`
+3. `testPresenter::OnResume` 应用 `g_pendingJumpSubView` 再 `ChangeView`
+
+拷机后台循环不受影响(除非用户点「停止」)。
+
+### 4.3 与 battery 联动
+
+| 模块 | 文件 | 状态 |
+|------|------|------|
+| 放电阶段跳过 `battery_check_chargement` | `battery_service.cpp` | ✅ |
+| `battery_request_charge_reconfig()` 阶段切换/停止重配 ICHG | 同上 | ✅ |
+| `battery_invalidate_charge_current_cache()` | 同上 | ✅ |
+| `battery_set_force_discharge_soc()` 拷机放电 SOC | `battery_driver.cpp` | ✅ |
+| 放电阶段 SOC 峰值锁定(防插线虚高回升) | `s_cc_burn_discharge_soc_peak` | ✅ |
+| 拷机热路径调试日志默认关闭 | `BATTERY_SOC_VERBOSE_LOG` | ✅ |
+
+### 4.4 构建集成
+
+| 项 | 状态 |
+|----|------|
+| `cc_cycle_service` 加入 services / utils / producttest CMake | ✅ |
+| `brandy/config.py` 组件列表 | ✅ |
+| `VIEW_CC_CYCLE_BURN` 注册 | ✅ |
+
+### 4.5 充电不良检测(2026-06)✅
+
+仅在 **充电阶段**、拷机 **继续运行**(不自动暂停)。电流不直接采样,用 SOC 变化与时长间接判断。
+
+| 等级 | UI 底色 | 判定(默认) | 含义 |
+|------|---------|--------------|------|
+| **红** | 浅红 `#FFCDD2` | 充电 SOC 连续 3 次下降,或 5min 净降 ≥1% | 等效无充电电流、电量反降 |
+| **黄** | 浅黄 `#FFF9C4` | 本次充电 >2h(7200s)且 SOC < 99% | 充电过慢 |
+| **蓝** | 浅蓝 `#B3E5FC` | SOC 78–82% 停留 ≥30min 无上升 | 高电量卡在 ~80% |
+
+**优先级**:红 > 黄 > 蓝。首次进入某等级:铃声 + 写 `cc_cycle_defect.bin` + event 日志(`defect_red_soc_drop` / `defect_yellow_slow_charge` / `defect_blue_soc_stuck80`)。
+
+### 4.6 阶段计时(上次 / 本次)✅
+
+| 项 | 行为 |
+|----|------|
+| **本次** | `cc_cycle_format_duration_line()` 每次用 `now - charge_start_ts` 或 `discharge_start_ts` 算秒数,格式 `HH:MM:SS` |
+| **刷新** | 全屏 `g_ccBurnRefreshTimer`、控制页 `g_ccCycleStatusTimer` 均为 **1000ms** → **本次每秒递增** |
+| **上次** | bin 字段 `last_charge_duration_sec` / `last_discharge_duration_sec`;**仅阶段切换时写入**,界面为固定值 |
+| **切换** | 充电结束 → 写入上次充电时长;进入充电 → 写入上次放电时长,本次充电从 0 计 |
+
+退出全屏后控制页仍 1s 刷新本次;拷机停止时显示 `--:--:--`。
+
+---
+
+## 五、关键参数(当前代码)
+
+| 参数 | 值 | 位置 |
+|------|-----|------|
+| 满电切换阈值 | SOC ≥ 99% 或 CHG_DONE | `CC_CYCLE_SOC_FULL` |
+| 低电切换阈值 | 放电自然掉电;开机兜底 `last_soc≤10%` / `pending_charge` | `CC_CYCLE_SOC_ARM=5` / `CC_CYCLE_SOC_LOW=10` |
+| tick 周期 | 1s | `cc_cycle_task` |
+| 存储 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` |
+
+---
+
+## 六、操作路径(测试用)
+
+1. 进入 **产测** → **充放电拷机**
+2. **插充电线** → 打开 **拷机开关**
+3. 点 **开始自动拷机** → 进入全屏拷机页(阶段底色 + SOC + V/T + 时间 + 状态行),自动选初始阶段
+4. **左滑** 退出全屏 → 回到控制页(同样多行信息;循环仍在后台,除非点停止)
+5. 重启设备:若开关+运行中+仍插线 → `cc_cycle_on_boot()` 恢复并重新进拷机 UI
+6. 充电不良:全屏变色 + 铃声 → **左滑** 回控制页(停铃)→ 点 **充电 IC 状态** 查看快照寄存器(可「刷新实时」)
+
+---
+
+## 七、已知问题:拷机 9% vs 表盘 23% 🔴
+
+### 现象
+
+- **拷机全屏页**显示 SOC 约 **9%**(接近 10% 放电切换阈值,合理)
+- **重启后表盘**显示约 **23%**(同一块电池、仍插着充电线)
+
+### 根因分析(代码层面)
+
+**不是 UI 算错两个数,而是两套 SOC 规则 + 重启时序导致。**
+
+```mermaid
+flowchart TD
+ burnUI["拷机页 cc_cycle_format_status_line"]
+ dialUI["表盘 MainClockView battery_get_voltage"]
+ forceMode["force_discharge_soc=1\n放电阶段"]
+ normalMode["force_discharge_soc=0\n普通/充电阶段"]
+ disMap["battery_percent_map 放电表"]
+ chgMap["battery_charge_percent_map 充电表"]
+ peakLock["s_cc_burn_discharge_soc_peak\n峰值锁定缓降"]
+
+ burnUI --> forceMode
+ forceMode --> disMap --> peakLock
+ peakLock --> soc9["显示约 9%"]
+
+ dialUI --> normalMode
+ normalMode --> chgMap
+ chgMap --> soc23["重启插线查充电表\n端电压虚高约 23%"]
+```
+
+| 因素 | 拷机放电页 | 重启后表盘 |
+|------|------------|------------|
+| `force_discharge_soc` | **开**(Hi-Z 放电阶段) | **关**(重启初期尚未恢复拷机状态) |
+| 电压映射表 | **放电表**(插线也按未插线算) | **充电表**(检测到 VBUS 插入) |
+| 峰值锁定 | **有** `s_cc_burn_discharge_soc_peak` | **无**(RAM 变量重启清零) |
+| 开机时序 | 已在放电态 | `battery_init` 先于 `cc_cycle_on_boot`,表盘先画 23% |
+| history | 拷机内缓降 | `history_percent=0`(restore 被注释) |
+
+关键代码:
+
+```857:858:code/sdk/zhihui/services/battery/service/battery_driver.cpp
+ uint8_t treat_as_charging = (get_charge_stat() != 0) && (g_force_discharge_soc_mode == 0);
+ const float (*battery_map)[2] = treat_as_charging ? battery_charge_percent_map : battery_percent_map;
+```
+
+```999:1021:code/sdk/zhihui/services/battery/service/battery_driver.cpp
+ if (g_force_discharge_soc_mode) {
+ /* s_cc_burn_discharge_soc_peak 仅 RAM,重启丢失 */
+ ...
+ history_percent = percent;
+ }
+```
+
+```898:901:code/sdk/zhihui/utils/custom_utils.c
+ if (custom_get_factory_test_flag() != 0) {
+ cc_cycle_on_boot(); /* 晚于 battery_init / 表盘首次刷电量 */
+ }
+```
+
+**结论:9% 更接近拷机放电真实消耗;23% 是重启后插线 + 充电表 + 无峰值锁定的虚高,属于已知缺陷,不是用户误操作。**
+
+### 修复方向
+
+| 优先级 | 方案 | 说明 | 状态 |
+|--------|------|------|------|
+| **P0** | **拷机激活时全局统一 SOC 源** | `cc_cycle_get_soc()` + `cc_cycle_is_active()` | ✅ B1 |
+| **P0** | **持久化放电 SOC 峰值** | `cc_cycle.bin` `last_soc` / `soc_peak` | ✅ B2 v3 |
+| **P0** | **调整开机时序** | `cc_cycle_early_boot_sync` 在 percent_init 前 | ✅ B4 |
+| P1 | 表盘在拷机运行中显示「拷机 SOC」标识 | 可选,便于测试区分 | ⬜ |
+| P1 | 与 [充电电量显示分析](./充电电量显示分析.plan.md) 的 history restore 联动 | 减少非拷机场景跳变 | ⬜ |
+
+> 以上 P0 已合入,**待重启现场确认** 9% vs 23% 是否消除。
+
+---
+
+## 七点五、拷机 UI 信息行(电压 / 温度 / 时间)— **已实现 ✅**
+
+### 实现方式
+
+全屏页与控制页 **`RefreshStatus` / `RefreshStatusLabel` 分多行展示**,不再挤在一行 status 里:
+
+| 行 | 全屏页控件 | 控制页控件 | 数据来源 |
+|----|------------|------------|----------|
+| 阶段标题 | `phaseLabel_` | `phaseLabel_`(阶段卡) | `cc_cycle_get_phase()` + 底色 |
+| SOC | `socLabel_`(72pt) | `socLabel_`(52pt 黄) | `cc_cycle_get_soc()` |
+| 电压+温度 | `metricsLabel_` | `metricsLabel_` | `battery_get_voltage` + `battery_get_battery_temp` |
+| 时长 | `durationLabel_` | `durationLabel_` | `cc_cycle_format_duration_line()`(**本次 1s 实时**) |
+| 时刻 | `timeLabel_` | `timeLabel_` | `cc_cycle_format_time_line()` |
+| 不良 | `defectLabel_` | (合入 status 红字 + hint) | `cc_cycle_format_defect_line()` |
+| 状态详情 | `statusLabel_` | `statusLabel_` | `cc_cycle_format_status_line()` |
+
+Service 侧另提供 `cc_cycle_format_detail_line()`(SOC+V+T 单行),**当前 UI 直接在 View 里读 HAL**,与控制页格式一致。
+
+### 时长行(上次 / 本次)
+
+- **本次**:每次刷新取当前时间戳减 `charge_start_ts` / `discharge_start_ts`,**每秒 +1**,不存 bin
+- **上次**:`last_charge_duration_sec` / `last_discharge_duration_sec`,阶段结束时写入 v7 bin
+- 详见 **§4.6**
+
+### 时间行与日志对齐
+
+`cc_cycle_format_time_line` 读 `cc_cycle.bin` v7 字段:
+
+- `burn_start_ts` — 本轮拷机开始
+- `charge_start_ts` — 当前/最近充电开始
+- `discharge_start_ts` — 当前/最近放电开始
+
+与串口 `[cc_cycle] burn_start/charge_start/discharge_start` 及 `cc_cycle_event_log.txt` 同一套时间戳。
+
+### 刷新周期
+
+| 页面 | 定时器 | 周期 |
+|------|--------|------|
+| 全屏拷机页 | `ccCycleBurnPresenter` → `g_ccBurnRefreshTimer` | 1s |
+| 控制页 | `testCcCycle` → `g_ccCycleStatusTimer` | 1s |
+
+### 仍可选增强(P2)
+
+- 拷机页显示 PCB 温度(`batteryservice_get_pcb_temp`)
+- 状态行增加目标充电电流 `ichg_target`(见 27°C 140mA 分析 plan)
+
+---
+
+## 八、待办 / 未验证 ⬜
+
+### P0 — Bug 修复(新增,优先于验证)
+
+| # | 项 | 状态 |
+|---|-----|------|
+| B1 | 拷机激活时表盘与拷机页 SOC 统一 | ✅ early_boot_sync + seed peak |
+| B2 | `cc_cycle.bin` 持久化 `last_soc` / 放电峰值 / 阶段时长 / 不良 | ✅ v7 store |
+| B3 | 拷机页 + 控制页多行 UI(SOC / V+T / 时长 / 时刻 / 不良 / 状态) | ✅ 见 §4.2、§7.5 |
+| B6 | 充电不良检测 + IC 快照 + 阶段计时 v7 | ✅ 见 §4.5、§4.6 |
+| B7 | 阶段切换 ICHG 重配(0.5C→1C 修复) | ✅ 见 §十四 |
+| B8 | 拷机开关隔离 + 恢复出厂清存储 | ✅ 见 §十三 |
+| B9 | 汇总 UI + 回到拷机界面 + 当前时间含秒 | ✅ 见 §十六、§十七 |
+| B4 | 开机时序:`cc_cycle_on_boot` 与 battery init 顺序调整 | ✅ early_boot_sync 在 percent_init 前 |
+| B5 | 左滑退出延迟切 slice(防 GuiMainThread 崩溃) | ✅ 待现场确认 |
+
+### P0 — 功能验证(拷机专项)
+
+| # | 项 | 负责人 | 状态 |
+|---|-----|--------|------|
+| 1 | 满电 → 放电:Hi-Z 生效、白屏、SOC 缓降 | | ⬜ |
+| 2 | 放电自然掉电 → 关机 → 再开自动进拷机充电 UI(pending_charge/last_soc 兜底) | | ⬜ |
+| 3 | 99%/CHG_DONE → 下一轮放电,`round_count` +1 | | ⬜ |
+| 4 | 日志 `cc_charge_log.txt` 内容正确 | | ⬜ |
+| 5 | 重启后续跑(`cc_cycle.bin` 恢复) | | ⬜ |
+| 6 | 拔线暂停、再插线需手动重新开始 | | ⬜ |
+| 7 | 拷机 UI 左滑返回后后台仍循环、不崩溃 | | ⬜ |
+| 8 | 长时间拷机(≥24h)稳定性 / 温升 | | ⬜ |
+| 9 | 充电不良红/黄/蓝 + 铃声 + IC 快照查看 | | ⬜ |
+| 11 | 上次/本次阶段时长切换与 1s 刷新 | | ⬜ |
+| 12 | 拷机开关关:表盘/正常充电无影响 | | ⬜ |
+| 13 | 放电→充电 ICHG 与插拔一致 | | ⬜ |
+| 14 | 「回到拷机界面」+ 汇总 UI(含秒) | | ⬜ |
+
+### P1 — 已知风险 / 待改进
+
+| # | 项 | 说明 | 状态 |
+|---|-----|------|------|
+| 0 | **拷机 vs 表盘 SOC 不一致** | B1~B4 已合入,见第七章;待重启现场确认 | 🟡 待验证 |
+| 1 | **SOC 精度** | 仍依赖 OCV 查表;低电/充电切换时可能跳变(见 [充电电量显示分析](./充电电量显示分析.plan.md)) | ⬜ 分析中 |
+| 2 | **电量持久化** | `battery_save_vol` 关机保存仍被注释,与拷机/低电场景均有影响 | ⬜ |
+| 3 | **仅 HP4570** | 无 HP4570 的平台整个 cc_cycle UI 不编译 | 设计如此 |
+| 4 | **无 AT 指令** | 仅产测 UI 入口,暂无 `AT^CCCYCLE` 类命令 | ⬜ 可选 |
+| 5 | **GPS 常开** | 拷机全程 GPS on,功耗/发热需关注 | ⬜ 待评估 |
+| 6 | **合入主分支** | `feature/cc-cycle-burn-test` 待 review/merge | ⬜ |
+
+### P2 — 可选增强
+
+| # | 项 | 状态 |
+|---|-----|------|
+| 0 | 拷机页显示 PCB 温度(已有 API `batteryservice_get_pcb_temp`) | ⬜ |
+| 1 | 拷机页显示当前轮次 / 本轮充电耗时 | ✅ 已有上次/本次时长行 |
+| 2 | 导出拷机报告(轮次、每轮 charge log 汇总) | ⬜ |
+| 3 | 可配置 SOC 阈值(10%/99%) | ⬜ |
+| 4 | 异常告警(放电阶段 HW:充!、温度超限) | 🟡 充电不良红/黄/蓝已实现;放电 HW:充! 仍仅状态行 |
+
+---
+
+## 九、与「充电电量显示 26%」的交叉影响
+
+| 场景 | 拷机相关 | 电量 plan 相关 |
+|------|----------|----------------|
+| 拷机放电阶段 SOC | `force_discharge_soc` + 峰值锁定 | 插线但按放电表算 |
+| 拷机充电阶段 SOC | 走正常 battery_service | 充电表 + history 平滑 |
+| 低电关机再充电开机 | 非拷机路径 | **26% 跳变**,需分场景 restore |
+| 关机 save/restore | cc_cycle.bin 独立持久化 | battery.bin 保存被注释 |
+
+**建议对齐顺序:**
+1. 拷机 P0 八项功能验证先跑通
+2. 并行推进电量 plan 的「低电+充电钳位」(不影响拷机主流程,但改善 SOC 可信度)
+3. 拷机长跑时重点看 SOC 曲线是否单调、切换点是否过早/过晚
+
+---
+
+## 十、核心文件索引
+
+| 类型 | 路径 |
+|------|------|
+| 服务 | [`code/sdk/zhihui/services/cc_cycle_service/cc_cycle_service.c`](../code/sdk/zhihui/services/cc_cycle_service/cc_cycle_service.c) |
+| 头文件 | [`code/sdk/zhihui/services/cc_cycle_service/cc_cycle_service.h`](../code/sdk/zhihui/services/cc_cycle_service/cc_cycle_service.h) |
+| 控制 UI | [`testCcCycle.cpp`](../code/sdk/zhihui/UI/producttest/src/testCcCycle.cpp) / [`.h`](../code/sdk/zhihui/UI/producttest/include/testCcCycle.h) |
+| 全屏 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) |
+| 开机 | [`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) |
+
+---
+
+## 十一、进度对齐表(可填)
+
+| 里程碑 | 目标日期 | 实际 | 备注 |
+|--------|----------|------|------|
+| 代码合入 feature 分支 | 2026-06-02 | ✅ | fecd7a21d |
+| 拷机 SOC 9% vs 23% 修复 | | 🟡 | B1~B4 已合入 |
+| 拷机 UI 多行(SOC/V+T/时间/状态) | 2026-06-02 | ✅ | §4.2、§7.5 |
+| 充电不良检测 + IC 状态 + 阶段时长 | 2026-06-02 | ✅ | §4.5、§4.6 |
+| 放电自然掉电 + 关机续跑(F1/F2) | 2026-06-02 | ✅ | 见 [拷机放电至关机循环.plan.md](./拷机放电至关机循环.plan.md) |
+| 拷机开关隔离 + ICHG 重配 | 2026-06-02 | ✅ | §十三、§十四 |
+| 汇总 UI + 回到拷机界面 | 2026-06-02 | ✅ | §十六、§十七 |
+| 拷机 P0 验证(含隔离/ICHG) | | ⬜ | §八 1~14 |
+| SOC 低电跳变修复 | | ⬜ | 见充电电量 plan |
+| 合入主分支 | | ⬜ | |
+| 长跑拷机 ≥24h | | ⬜ | |
+
+---
+
+## 十二、日志与调试
+
+| 日志/文件 | 用途 |
+|-----------|------|
+| 串口 `[cc_cycle]` 事件 PRINT | 拷机/充/放阶段切换:`burn_start` `charge_start` `discharge_start` `charge_end` `burn_stop` |
+| 串口 `[battery]` / HP4570 相关 PRINT | SOC、充电 IC 状态 |
+| `user/userdata/cc_cycle/cc_cycle.bin` | 开关/阶段/轮次/时间戳/上次阶段时长/不良类型(**v7**) |
+| `user/userdata/cc_cycle/defect.bin` | 最近一次不良快照(HP4570 寄存器 dump + IC 状态) |
+| `user/userdata/cc_cycle/event_log.txt` | 阶段事件 + `defect_*` |
+| `user/userdata/cc_cycle/charge_log.txt` | 每轮充电起止时间、结束 SOC |
+| 拷机 UI 状态行 | `cc_cycle_format_status_line` — 阶段/轮次/HiZ/HW:充! |
+| 拷机 UI 指标行 | `metricsLabel_` — `V:xx.xxV T:xx.xC`(View 直读 HAL) |
+| 拷机 UI 时长行 | `cc_cycle_format_duration_line` — `上次充电:… 本次充电:…`(**本次 1s 实时**) |
+| 拷机 UI 时刻行 | `cc_cycle_format_time_line` — `开始:MM-DD HH:MM 充:… 放:…` |
+| 拷机 UI SOC 行 | `cc_cycle_get_soc()` — 大号百分比,与表盘统一源(拷机激活时) |
+
+**UI 示例(全屏页 / 控制页同步):**
+
+```
+ 充电中 ← phaseLabel_(浅绿/不良色)
+ 85% ← socLabel_
+ V:4.12V T:32.5C ← metricsLabel_
+上次充电:01:23:45 本次充电:00:15:30 ← durationLabel_(本次每秒+1)
+开始:06-02 14:30 充:14:45 放:-- ← timeLabel_
+【不良】充电超时>2h ← defectLabel_(有则显示)
+【充电中】SOC:85% 轮次:3 ... ← statusLabel_
+ 左滑返回 ← footerLabel_(仅全屏页)
+```
+
+**状态行示例:**
+- `【放电中】电池供电 SOC:xx% 轮次:n HiZ:开`
+- `【充电中】SOC:xx% 轮次:n IC:开 HW:充`
+- 异常:`HW:充!`(放电阶段硬件仍在充,需排查 Hi-Z/CEB)
+
+**时间行示例:**
+- `开始:06-02 14:30 充:14:45 放:15:20`
+- 未运行:`开始:--:-- 充:--:-- 放:--:--`
+
+**串口事件示例:**
+- `[cc_cycle] burn_start ts=1717320000 time=2026.06.02.14:30:05 soc=85 round=0`
+
+---
+
+
+## 十三、拷机开关与正常充放电隔离 ✅
+
+### 13.1 开关关时(不应影响正常用户/产测)
+
+| 模块 | 行为 |
+|------|------|
+| HP4570 | `battery_check_chargement()` 正常跑;`cc_cycle_is_discharge_phase()==0` |
+| Hi-Z/GPS/常亮 | `cc_tick` 早退,不执行 |
+| SOC | `force_discharge_soc=0`;表盘与拷机无关 |
+| 恢复出厂 | `cc_cycle_clear_storage()` 已启用(双保险删 userdata) |
+
+### 13.2 已实现加固(2026-06 末)
+
+| 项 | 实现 |
+|----|------|
+| `cc_cycle_is_active_hw()` | `test_enabled && auto_running` |
+| `cc_store_sanitize_after_load()` | 开关关时强制 `auto_running=0`、`phase=IDLE`、清 `force_discharge_soc` |
+| `cc_sync_discharge_soc_mode()` | 经 `is_active_hw` 门控 |
+| `cc_cycle_is_running()` | 改为 `test_enabled && auto_running` |
+| 停止/关开关 | `cc_release_hw_only_locked()` + `battery_request_charge_reconfig()` |
+
+### 13.3 开关开且运行中(故意改变充放电,属拷机设计)
+
+| 阶段 | 充电 IC | battery_service | SOC |
+|------|---------|-----------------|-----|
+| **拷机充电** | CEB=1,ICHG 交 battery | 正常温控 0.2C/0.5C/1C | 充电曲线+慢涨 |
+| **拷机放电** | Hi-Z+禁充,cc 直控 | **跳过** ICHG 管理 | 放电曲线+峰值封顶 |
+
+---
+
+## 十四、充电电流:阶段切换 vs 插拔(0.5C / 1C)
+
+### 现象
+
+拷机放电→充电后充电电流仅 0.5C;重新插拔充电线恢复 1C。
+
+### 根因
+
+| 路径 | 行为 |
+|------|------|
+| 阶段切换 | 仅 `cc_set_charge_enable(1)`,ICHG 依赖 battery;`last_charge_mA` 缓存可能不重写 |
+| 重新插线 | `BATTERY_CMD_CHARGE_PLUG` → `hp4570n_reset()` 强制 260mA |
+
+放电期 `battery_check_chargement()` 被跳过,ICHG 与缓存冻结。
+
+### 修复 ✅
+
+- `battery_request_charge_reconfig()`:清 `last_charge_mA` 缓存 + 按当前温度重算 ICHG
+- `cc_enter_charging_phase()` 延迟触发(`cc_schedule_charge_reconfig`,释放锁后执行)
+- 停止拷机 / `cc_release_hw_only_locked()` 同样触发 reconfig
+
+---
+
+## 十五、SOC 显示:4.47V 与 85% 不一致
+
+### 原因(非 ADC 故障)
+
+| 显示项 | 数据源 |
+|--------|--------|
+| **V:4.47V** | 瞬时 ADC `battery_get_voltage()` |
+| **85%** | `cur_percent`:滑动平均 `avg_vol` + 充电慢涨(每次最多 +1%) |
+
+FD272 充电曲线:4.263V≈85%,4.4V=100%。瞬时值已到 4.47V 时,平均电压与 `history_percent` 可能仍对应 85%。
+
+拷机**放电阶段**另走放电曲线 + `force_discharge_soc` 峰值封顶,与 V 偏差更大。
+
+### 结论
+
+属算法设计(抗纹波/慢涨),非单纯查表错误。拷机页 V 与 % 本就可能短期不一致。
+
+---
+
+## 十六、全屏页「充放电拷机汇总」UI ✅
+
+| 控件 | 内容 |
+|------|------|
+| `summaryTitleLabel_` | 「充放电拷机汇总」 |
+| `clockLabel_` | 当前时间 `月.日 时:分:秒`(1s 刷新) |
+| `lastChargeLabel_` / `curChargeLabel_` | 上次(蓝 XhYmin) / 本次(红 HH:MM:SS 或「待开始」) |
+| `lastDisLabel_` / `curDisLabel_` | 同上,放电 |
+
+API:`cc_cycle_get_burn_summary()`;数据来自 bin `last_*_duration_sec` + 实时 `phase_start_ts`。
+
+---
+
+## 十七、控制页按钮(现行)
+
+| 按钮 ID | 文案 | 说明 |
+|---------|------|------|
+| `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_back_burn` | 回到拷机界面 | 仅 `is_running` 时显示;`cc_cycle_reopen_burn_ui()` |
+
+左滑退出全屏后拷机**不停止**;点「回到拷机界面」重新进入全屏。
+
+---
+
+## 十八、持久化路径(v7)
+
+| 文件 | 路径 |
+|------|------|
+| 状态 bin | `user/userdata/cc_cycle/cc_cycle.bin`(**v7**) |
+| 事件 log | `user/userdata/cc_cycle/event_log.txt` |
+| 充电 log | `user/userdata/cc_cycle/charge_log.txt` |
+| 不良快照 | `user/userdata/cc_cycle/defect.bin` |
+
+旧路径 `user/userdata/cc_cycle.bin` 等只读兼容;恢复出厂清除。
+
+---
+
+## 十九、下一步建议
+
+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. 验证通过后 **合入主分支**
+
+---
+
+## 二十、拷机对其他 UI 的影响(文字消失 / 充弹窗 / 资源)
+
+### 20.1 充电弹窗不显示 — **不是资源不够,是产测模式故意屏蔽**
+
+产测入口进入时即设置 `factory_test_status=1`,并在 `MsgCenterNotifyChargePlugProc` 中:
+
+```630:634:code/sdk/application/wearable/fd272/service/msg_center/src/msg_center_customer.cpp
+ if (CheckPriority(topic) && !uapi_dev_runtime_get_factory_test_status_flag()) //产测模式不弹充电弹窗
+ {
+ NotificationManager::GetInstance()->SetNotifyPriority(PRIORITY_TWO);
+ NotificationManager::GetInstance()->ShowNotify(ShowChargeingNotify, (uint16_t)topic, true);
+ }
+```
+
+进入 BLE 产测模式时还会主动 `ChargerEventPublish(TOPIC_EVENT_CHARGE_UNPLUG)` 关掉已有充弹窗。
+
+**结论**:只要处于产测模式(不论是否开拷机开关),正常用户态充电弹窗**本来就不会弹**。这与拷机内存占用无关;拷机只是产测子功能之一。
+
+若需在产测/拷机充电阶段看充电动画,应走产测内「充电 IC 状态」或单独加拷机专用充电动效,不能指望用户态 `ShowChargeingNotify`。
+
+### 20.2 其他界面「字消失 / 界面被重置」— 多重 UI 干扰叠加
+
+```mermaid
+flowchart TD
+ tick["cc_cycle_task 1s tick"]
+ hold["cc_set_discharge_screen_hold\n灭屏关+亮度255+PM veto"]
+ inv["RootView::Invalidate 全屏重绘"]
+ grab["cc_cycle_show_burn_ui\nChangeSlice 抢全屏"]
+ gps["gnss_start 放电阶段"]
+
+ tick --> hold
+ tick --> gps
+ uiTimer["拷机页 1s 定时器"] --> inv
+ boot["开机/插线恢复"] --> grab
+```
+
+| 原因 | 机制 | 影响 |
+|------|------|------|
+| **① 全屏 RootView 每秒 Invalidate** | `testCcCycle.cpp` / `ccCycleBurnView.cpp` 刷新时调用 `RootView::GetInstance()->Invalidate()` | 整个 GUI 根视图重绘,产测其他子页可能出现闪烁、文字短暂空白、滚动区错位 |
+| **② 每秒重复 screen_hold** | `cc_tick()` 每 1s 调用 `cc_set_discharge_screen_hold(1)`:关自动灭屏、强制亮屏、亮度 255、PM veto | 即使用户切到 LCD/GPS 等其它产测项,拷机后台仍改电源策略,干扰正常 UI 生命周期 |
+| **③ 开机/插线抢全屏** | `cc_cycle_on_boot` / `cc_cycle_on_charger_plug` → `cc_cycle_show_burn_ui()` → 800ms 后 `ChangeSlice(VIEW_CC_CYCLE_BURN)` | 拷机运行中重启或 VBUS 恢复后,会**自动跳回拷机全屏**,其它产测页被顶掉 |
+| **④ 放电阶段 GPS 常开** | `cc_set_gps(1)` 整个放电阶段 | 额外 CPU/功耗/任务,嵌入式 GUI 线程可能被饿死,表现为渲染卡顿或字不显示 |
+| **⑤ IC/记录 overlay 动态 Resize** | `ApplyLabelContentSize` 每秒改 Label 高度 + 全 Root Invalidate | 拷机控制页打开 IC/记录面板时更容易出现文字抖动或消失 |
+
+**不是典型的「内存耗尽 OOM」**,而是:**后台 1s 任务 + 全屏重绘 + 电源/GPS 抢占** 导致 GUI 渲染不稳定。
+
+### 20.3 资源占用概况(参考)
+
+| 项 | 量级 | 说明 |
+|----|------|------|
+| `cc_cycle_svc` 任务栈 | 6KB | 固定,不大 |
+| 1s tick + 条件 fsync | 低~中 | 仅 `g_store_dirty` 时写 bin;但 tick 本身持锁跑 HP4570/SOC |
+| 拷机 UI 定时器 | 2 路 × 1s | 全屏页 + 控制页(仅在对应页时) |
+| GPS | **高** | 放电阶段全程 `gnss_start()` |
+| 屏幕常亮 + PM veto | **中** | `PM_ID_P_TEST`,与产测模式共用 |
+| 不良铃声 | 偶发 | `AlarmPlayerStart` |
+
+拷机**设计目标就是高负载拷机**(常亮 + GPS + 循环充放),对系统资源占用偏高是预期行为,但**不应**因此破坏产测其它页面的文字渲染。
+
+### 20.4 建议修复方向
+
+| 优先级 | 改法 | 文件 | 状态 |
+|--------|------|------|------|
+| P0 | 局部 Invalidate,禁止 RootView | `testCcCycle.cpp`、`ccCycleBurnView.cpp` | ✅ |
+| P0 | overlay 打开时停止 1s 刷 IC/记录 | `testCcCycle.cpp` | ✅ |
+| P0 | `ApplyLabelContentSize` 高度未变不 Resize | `testCcCycle.cpp` | ✅ |
+| P0 | `RefreshStatusLabel` 补 `title_->SetText` | `testCcCycle.cpp` | ✅ |
+| P0 | `cc_set_discharge_screen_hold` 已 hold 早退 | `cc_cycle_service.c` | ✅ |
+| P0 | 删除 `testPresenter::OnResume` 无条件 ResumeStatusTimer | `testPresenter.cpp` | ✅ |
+| P1 | 左滑退出后不自动抢全屏 slice | `cc_cycle_service.c` | ⬜ |
+
+### 20.5 现场快速区分
+
+| 现象 | 更可能原因 |
+|------|------------|
+| 产测任意页插线都不弹充电窗 | **产测模式屏蔽**(§20.1),正常 |
+| 拷机运行中切到 LCD/GPS 等页文字闪/消失 | **RootView 全刷 + screen_hold**(§20.2 ①②) |
+| 重启或插线后自动跳拷机全屏 | **show_burn_ui 抢 slice**(§20.2 ③) |
+| 仅放电阶段卡顿严重 | **GPS 常开**(§20.2 ④) |
+
+### 20.6 现场 heap 快照解读(2026-06-02 16:03)
+
+> 命令来源:`mcu_os_heap_info`(`LOSCFG_MEM_TASK_STAT`);表中 **used/peak size 是任务上下文堆分配**,不是栈深度。
+
+#### 快照 A — 内部 RAM `0x200319f0`(SYS_MEM,约 255KB)
+
+| 指标 | 值 | 说明 |
+|------|-----|------|
+| 总容量 | 0x3e610(~255KB) | 片上紧内存 |
+| 已用 | 0x292f0(~169KB,**66%**) | 水位线 0x2984c,余量仅 ~86KB |
+| **cc_cycle_svc** | used **0x1ef64**(~127KB),peak 0x1efa0 | **占已用 ~75%** |
+| GuiMainThread | 0x101c(~4KB) | 当时 UI 线程几乎无堆分配 |
+| app | 0x23cc | 正常 |
+
+**解读**:拷机 tick 内 `fopen/fwrite/fsync` 写 bin/log 时,VFS/stdio 缓冲在 **cc_cycle_svc 任务上下文** 里从 **内部 RAM** 分配,瞬时顶到 ~127KB。这是内部池吃紧的主因,**不是 cc_cycle 栈爆了**(栈在 PSRAM,见快照 B)。
+
+#### 快照 B — 外部 PSRAM `0x6c22a800`(EXTEND_MEM,约 5.9MB)
+
+| 指标 | 值 | 说明 |
+|------|-----|------|
+| 总容量 | 0x5d5800(~5.9MB) | 大池,仍有余量 |
+| 已用 | 0x300ae0(~3.1MB,**52%**) | 未 OOM |
+| **abilityms** | used **0x21fcec**(~2.2MB),peak 0x2218ac | **UI 框架主占用** |
+| **GuiMainThread** | 0x1a880(~105KB),peak 0x1b090 | 界面渲染线程 |
+| **cc_cycle_svc** | 堆 peak **0x8f8**(~2.3KB) | 此处是 **PSRAM 堆**统计,非栈;栈见 §20.7 |
+| cywee_service | peak 0x5f08 | 算法服务 |
+| Bootstrap | peak 0x9204 | 启动阶段 |
+
+**解读**:UI 活跃时 **abilityms ~2.2MB** 才是 PSRAM 大头;拷机任务栈本身很小。两次快照对比说明 cc_cycle **间歇性** 在写盘时冲击内部 RAM,平时几乎不占外部堆。
+
+#### 与 UI 问题的关系
+
+| 结论 | 依据 |
+|------|------|
+| **整体未 OOM** | PSRAM 仍剩 ~2.9MB;内部池还有 ~86KB free |
+| **拷机写盘会瞬时吃紧内部 RAM** | 快照 A cc_cycle_svc 127KB;可能拖慢系统/GUI,但不是唯一原因 |
+| **UI 字消失主因仍偏 GUI 逻辑** | 快照 B abilityms 2.2MB + §20.2 全屏 Invalidate;堆够但渲染被干扰 |
+| **cc_cycle 栈无溢出迹象** | `AT^STACKINFO` peak 0x1124 / total 0x1800(§20.7) |
+
+#### 堆相关优化(补充 §20.4)
+
+| 优先级 | 改法 |
+|--------|------|
+| P1 | 拷机落盘降频:bin 仅在阶段切换/停止时 fsync;tick 内只 mark dirty |
+| P1 | 写 log 用固定静态缓冲 + 小 chunk append,避免 stdio 大缓冲走内部 RAM |
+| P2 | 评估 `cc_cycle_svc` 文件 I/O 改到 `custom_fileoperate_service` 异步写,脱离拷机任务上下文 |
+
+### 20.7 现场栈快照解读(2026-06-02 16:06 `AT^STACKINFO`)
+
+> **与 §20.6 区别**:本表 **used/peak size 是栈水位**,不是堆分配。
+
+#### 全系统汇总
+
+| 指标 | 值 |
+|------|-----|
+| 全部任务栈合计 | total **218880 B(~214KB)** |
+| 当前栈占用 | used **20672 B(~20KB)** |
+| 历史峰值 | peak **75016 B(~73KB)** |
+
+#### 拷机任务 `cc_cycle_svc`(task 5)
+
+| 字段 | 值 | 说明 |
+|------|-----|------|
+| stack addr | **0x6c511cb0** | 在 **PSRAM**(`USE_EXTEND_MEM`),不占片上 DTCM |
+| total size | **0x1800(6144 B = 6KB)** | 与代码 `1024*6` 一致 |
+| used size | 0xa00(2560 B) | 当前栈深 ~42% |
+| **peak size** | **0x1124(4388 B)** | 历史峰值 **~71%**,余量 ~1.7KB |
+| status | 32(blocked) | 正常:`osDelay(1000)` 睡眠中 |
+
+**结论**:拷机栈**健康**,无溢出风险;峰值出现在 HP4570/写盘/持锁路径,仍低于 6KB 上限。
+
+#### 需关注的其它任务(对比)
+
+| 任务 | total | peak | peak/total | 备注 |
+|------|-------|------|------------|------|
+| **GuiMainThread** | 0x6000(24KB) | **0x3c88(~15KB)** | **~63%** | UI 渲染,深调用链时栈比拷机更紧张 |
+| battery_service | 0x1c00(7KB) | 0xef4(~3.8KB) | ~55% | 充电/SOC 路径 |
+| cywee_service | 0x2000(8KB) | 0xe90(~3.6KB) | ~45% | 算法 |
+| Bootstrap | 0x1200 | 0xa94 | ~56% | 启动 |
+| **cc_cycle_svc** | 0x1800(6KB) | 0x1124(~4.3KB) | **~71%** | 拷机,余量尚可 |
+
+#### 与 §20.6 heap 数据对照
+
+| 指标 | `mcu_os_heap_info`(堆) | `AT^STACKINFO`(栈) |
+|------|--------------------------|----------------------|
+| cc_cycle_svc | 内部堆瞬时 ~127KB(写盘) | 栈 peak **4.3KB / 6KB** |
+| GuiMainThread | PSRAM 堆 peak ~105KB | 栈 peak **15KB / 24KB** |
+| 含义 | 任务上下文 **malloc/stdio** | 任务 **调用栈深度** |
+
+**综合判断**:UI 异常与资源关系 = **内部 RAM 写盘瞬时挤占(堆)** + **GUI 全屏 Invalidate(逻辑)**;**不是 cc_cycle 栈溢出**。若以后要加深拷机调用链(如大 buffer 栈上分配),需关注 cc_cycle 71% 峰值,必要时栈扩到 8KB。
+
+### 20.8 现场现象:字只显示几个,重启可恢复
+
+#### 现象(用户反馈,逐步补充)
+
+| 受影响区域 | 表现 |
+|------------|------|
+| 产测其它子页(LCD/GPS/列表等) | 只出前几个字 |
+| **拷机控制页标题**「充放电拷机」 | **字不全** |
+| **充电 IC 状态 / 寄存器 overlay** | **大段文字只显示一部分** |
+| **拷机记录 overlay**(event_log) | **同上** |
+| 拷机全屏页短行(SOC/阶段/汇总) | 相对完整(每秒 SetText 的短字符串) |
+| 重启后 | 暂时全部正常,拷机跑一段时间后再现 |
+
+> 说明:并非「拷机页完全免疫」,而是 **短 Label + 每秒 SetText** 的控件相对正常;**静态标题 + 多行长文本 overlay** 同样中招,且更严重。
+
+#### 根因推断(与 §20.6/20.7 堆栈数据一致)
+
+**不是 OOM,是 GUI 脏区/矢量字库渲染状态被拷机 1s 刷新搞乱。**
+
+```mermaid
+flowchart LR
+ timer["拷机 1s 定时器"]
+ setText["短 Label SetText"]
+ overlay["IC/记录 overlay\nSetText + Resize"]
+ rootInv["RootView::Invalidate 全树"]
+ fontCache["矢量字库/Glyph 绘制"]
+ staticLbl["title_ 等:只 init 一次"]
+ longLbl["icStatusLabel_ / historyLabel_\n多行 wrap 2048B+"]
+
+ timer --> setText
+ timer --> overlay --> rootInv
+ timer --> rootInv --> fontCache
+ fontCache --> staticLbl
+ fontCache --> longLbl
+ setText -->|"短串相对正常"| ok["SOC/阶段等"]
+```
+
+| 机制 | 说明 |
+|------|------|
+| **短行相对正常** | `socLabel_`/`phaseLabel_` 等 **每秒 SetText 短串**,强制重绘,不易暴露 bug |
+| **标题字不全** | `title_` 仅 `init_title()` **设置一次**,`RefreshStatusLabel` **从不刷新**;全树 Invalidate 后静态 Label 不重绘 |
+| **寄存器/记录更严重** | 打开 overlay 后每秒:`SetText(大缓冲)` → `ApplyLabelContentSize()` **Resize 1600~2400 高** → `RootView::Invalidate()`;多行 wrap + 动态高度 + 全树脏区 = **最易只画前几字** |
+| **其它产测页** | Label init 一次,无 SetText 自愈 |
+| **重启为何好** | 清空脏区队列与 Glyph 缓存;直到拷机再次 1s 全树 Invalidate 后复现 |
+
+#### 代码触发点(控制页 overlay 为重点)
+
+**静态标题(不刷新):**
+
+```530:534:code/sdk/zhihui/UI/producttest/src/testCcCycle.cpp
+ title_ = testAboutView::GetInstance()->init_title((w - 200) / 2, 40, "充放电拷机");
+ ...
+ testCcCycleScrollView_->Add(title_);
+```
+
+**overlay 每秒重绘 + 全树 Invalidate:**
+
+```338:345:code/sdk/zhihui/UI/producttest/src/testCcCycle.cpp
+ void testCcCycleView::RefreshHistoryPanel(void)
+ {
+ ...
+ historyLabel_->SetText(cc_cycle_get_history_text());
+ ApplyLabelContentSize(historyLabel_, historyScrollView_);
+```
+
+```87:108:code/sdk/zhihui/UI/producttest/src/testCcCycle.cpp
+ static void CcCycleRootInvalidate(void)
+ {
+ RootView::GetInstance()->Invalidate(); /* 每次都打全树 */
+ }
+ static void ApplyLabelContentSize(...)
+ {
+ ...
+ label->Resize(...);
+ ...
+ CcCycleRootInvalidate(); /* overlay 每次 Resize 都触发 */
+ }
+```
+
+**1s 定时器在 overlay 打开时仍跑:**
+
+```461:468:code/sdk/zhihui/UI/producttest/src/testCcCycle.cpp
+ if (icPanelVisible_) {
+ ShowIcStatusPanel(); /* 含 ApplyLabelContentSize */
+ } else if (historyPanelVisible_) {
+ RefreshHistoryPanel();
+ } else {
+ RefreshStatusLabel();
+ }
+ CcCycleRootInvalidate();
+```
+
+`testPresenter::OnResume` 在 VIEW_TEST 恢复时无条件 `ResumeStatusTimer()`,切换页过渡期也可能多刷几帧。
+
+#### 修复方向(§20.4 P0,针对 overlay 加强)
+
+| 优先级 | 改法 | 针对 |
+|--------|------|------|
+| P0 | `CcCycleRootInvalidate` → 仅 `rootGroup_`/`overlayGroup_`/`burnScrollView_` Invalidate | 全树 |
+| P0 | overlay 打开时 **不要 1s 自动刷新**;仅「打开时 + 点刷新」更新 IC/记录 | 寄存器/拷机记录 |
+| P0 | `ApplyLabelContentSize`:**文本高度未变则 Skip Resize**;Invalidate 仅 `historyScrollView_`/`icStatusScrollView_` | 多行长文本 |
+| P0 | `RefreshStatusLabel` 内对 `title_` **补一次 SetText** 或 `title_->Invalidate()` | 标题 |
+| P1 | 定时器门控:仅拷机页前台可见时运行 | 后台干扰其它页 |
+| P1 | 产测子页 OnShow 时对 Label 重新 Invalidate/SetText | 其它子页 |
+
+#### 现场快速确认
+
+| 操作 | 预期 |
+|------|------|
+| 拷机运行 → 点「拷机记录」 | 若多行只出前几字 → overlay + ApplyLabelContentSize 路径 |
+| 关闭 overlay 看标题「充放电拷机」 | 若标题也缺字 → 静态 Label + RootView Invalidate |
+| 停拷机后再开 overlay | 若字恢复 → 1s 定时器是触发源 |
+| 全屏拷机页 SOC/汇总 | 若仍完整 → 短串 SetText 与长文 overlay 差异 |
+
+### 20.9 UI 字不全优化 — 已合入(2026-06-02)
+
+#### 1. `testCcCycle.cpp`
+
+- 删除 `CcCycleRootInvalidate()`(`RootView::Invalidate`)
+- 新增 `testCcCycleView::InvalidateLocalView()`:overlay 可见时刷 `overlayGroup_`,否则刷 `rootGroup_`
+- `ApplyLabelContentSize`:`GetHeight() != textH` 才 `Resize`;**不再**调 RootView
+- `RefreshStatusLabel`:补 `title_->SetText("充放电拷机")`;末尾**不** Invalidate
+- `RefreshCcCycleButtons`:`icPanelVisible_ || historyPanelVisible_` 时**跳过** `ShowIcStatusPanel`/`RefreshHistoryPanel`;末尾只调 `InvalidateLocalView()`
+- 打开 overlay 时:`RefreshHistoryPanel`/`ShowIcStatusPanel` 后调 `InvalidateLocalView()` 替代 RootView
+
+#### 2. `ccCycleBurnView.cpp`
+
+- `RefreshStatus` 末尾:`burnScrollView_->Invalidate()` 替代 `RootView::Invalidate()`
+
+#### 3. `testPresenter.cpp`
+
+- 删除 `OnResume` 中无条件 `ResumeStatusTimer()` 块(约 135–141 行);保留 `pendingJumpSubView == CCCYCLE` 分支即可
+
+#### 4. `cc_cycle_service.c` — `cc_set_discharge_screen_hold`
+
+```c
+if (on) {
+ if (g_discharge_screen_hold) {
+ return; /* 已常亮,tick 不重复调 display API */
+ }
+ ...
+}
+```
+
+#### 验证
+
+1. 拷机运行 10min → 标题/寄存器/拷机记录/其它产测页文字应完整
+2. 打开「拷机记录」停留 1min → 多行文字不应逐字消失
+3. 切 LCD 再回来 → 无需重启即可正常显示
--- /dev/null
+---
+name: 充电IC专项硬件测试
+overview: FD272/HP4570 充电 IC 专项硬件测试排程:主测 11 台 + 备机 3 台(共 14 台),温控 T1~T3 独立并行,保护项分 P/R 池并预留损耗样机;目标 6~8 工作日完成 Excel 7 张工作表。
+source: 充电ic专项硬件测试.xlsx(272 平台,2025.11.25)
+updated: 2026-06-04
+branch: —
+related:
+ - plan/充放电拷机汇总.plan.md
+ - plan/HP4570_闳康FA测试流程.md
+ - tools/update_ic_test_xlsx.py
+todos:
+ - id: prepare-samples
+ content: 准备主测 11 台 + 备机 3 台,贴 SN 标签并登记台账(见 §二)
+ status: pending
+ - id: prepare-equip
+ content: 齐套静电台、浪涌仪、程控电源×3、可调NTC假电池×3、数据采集仪、充电底座
+ status: pending
+ - id: flash-firmware
+ content: E 组刷用户版;T/P/C/Q 刷测试版并焊串口;ESD 完成后刷工厂版点检
+ status: pending
+ - id: run-parallel-tracks
+ content: 按 §五 五产线并行执行,高损项仅 R 组,T 组全程锁定
+ status: pending
+ - id: fill-excel-report
+ content: 回填 Excel 7 张工作表 1#/2#/3# 结论列,归档曲线数据与异常 dump
+ status: pending
+isProject: false
+---
+
+# FD272 充电 IC 专项硬件测试计划
+
+> **依据**:[充电ic专项硬件测试.xlsx](/mnt/c/Users/ken'n'y/Desktop/充电/充电ic专项硬件测试.xlsx)
+> **补充**:[充电ic专项硬件测试(奋达补充).xlsx](/mnt/c/Users/ken'n'y/Desktop/充电/充电ic专项硬件测试(奋达补充).xlsx)(Phase 2 功耗/续航,不在本计划主工期)
+> **Excel 工具**:[`tools/update_ic_test_xlsx.py`](../tools/update_ic_test_xlsx.py)(可生成 IC 应力测试 sheet)
+
+---
+
+## 〇、设计原则
+
+| 规则 | 说明 |
+|------|------|
+| **温控与保护分池** | T1~T3 只做温控,不参与 VBUS16V/反接/短接 |
+| **高损项走 R 组** | A 级项损坏概率高,必须备机 3 台,测完评估是否转 FA |
+| **Excel 列对齐** | 保护/温控结论填 **1#/2#/3#**,与 T1~T3 或 P1~P3 一一对应 |
+| **与 cc_cycle 互补** | 本专项偏边界/安全/参数;长周期可靠性见 [充放电拷机汇总.plan.md](./充放电拷机汇总.plan.md) |
+
+---
+
+## 一、测试范围(Excel 7 张表)
+
+| 序号 | 工作表 | 内容 | Excel 样机数 | 本计划分配 |
+|------|--------|------|--------------|------------|
+| 1 | 静电测试 + 静电测试用例 | ESD-001/002/003 | 2 pcs | **E1、E2** |
+| 2 | 浪涌测试 | 6V~14V + 50V~120V | 2 pcs | **E1、E2**(高挡可转 R2) |
+| 3 | 充电基本参数 | 低电压/涓流/恒流/停复充 | 1 pcs | **C1** |
+| 4 | 充放电保护 | 11 项 | 1~3 pcs | **T1~T3**(温控)+ **P1~P3**(主测)+ **R1~R3**(高损) |
+| 5 | 充电曲线 | Time/Vbat/Ibat/Iin | 2 pcs | **Q1、Q2** |
+| 6 | 功耗基本参数 | 关机/待机/亮屏/连 App | 3 pcs | P 或 Q 组通过后复用 |
+| 7 | (补充 xlsx)IC 应力测试 | PM 筛片等 | 见脚本 | Phase 2 / 产测筛片 |
+
+---
+
+## 二、样机台账(开工前必填)
+
+### 2.1 分组与数量
+
+| 代号 | 数量 | 用途 | 损坏风险 | 固件 |
+|------|------|------|----------|------|
+| **E1~E2** | 2 | ESD + 浪涌 | 高 | 用户版 → 工厂版(ESD 后) |
+| **T1~T3** | 3 | 放电温控 + 充电限流 | 低 | 测试版 + 串口 |
+| **P1~P3** | 3 | 保护 C/B 级(4h 长充、OVP/UVP、过放等) | 中 | 测试版 + 串口 |
+| **C1** | 1 | 充电基本参数 | 低 | 测试版 |
+| **Q1~Q2** | 2 | 充电曲线 | 低 | 测试版 |
+| **R1~R3** | 3 | **备机池**(VBUS16V/反接/短接/浪涌替补) | 消耗品 | 测试版 |
+| **合计** | **14** | 11 主测 + 3 备机 | — | — |
+
+### 2.2 台账模板(复制到 Excel 或飞书)
+
+| 代号 | SN | 批次/LOT | 分配产线 | 当前状态 | 备注 |
+|------|-----|----------|----------|----------|------|
+| E1 | | | A-ESD | 待测 | |
+| E2 | | | A-ESD | 待测 | |
+| T1 | | | B2-温控 | 待测 | 高温放电 >50℃ |
+| T2 | | | B2-温控 | 待测 | 低温放电 <-10℃ |
+| T3 | | | B2-温控 | 待测 | 充电限流全温区 |
+| P1 | | | C-保护 | 待测 | Excel 1# |
+| P2 | | | C-保护 | 待测 | Excel 2# |
+| P3 | | | C-保护 | 待测 | Excel 3# |
+| C1 | | | B1-参数 | 待测 | |
+| Q1 | | | C-曲线 | 待测 | |
+| Q2 | | | C-曲线 | 待测 | |
+| R1 | | | R-高损 | 待用 | 计划 VBUS16V+反接 |
+| R2 | | | R-高损 | 待用 | 计划短接+浪涌高挡 |
+| R3 | | | R-浮动 | 待用 | ESD/P 组替补 |
+
+### 2.3 保护项损坏风险分级
+
+| 等级 | 测试项 | 样机 | 损坏后动作 |
+|------|--------|------|------------|
+| **A 高损** | VBUS **16V/0.5h**、反接 30s、Pogo **短接 10s**、浪涌 **50V~120V** | **仅 R 组** | 贴「损坏」→ 转 FA(见 [HP4570_闳康FA测试流程.md](./HP4570_闳康FA测试流程.md)) |
+| **B 中损** | VBUS 8V/0.5h、USB OVP/UVP 扫描、过放拉电压 | P 组;异常换 R | 记录寄存器 dump |
+| **C 低损** | 4h 欠压/高压长充、机械振动、端子漏电、充电曲线 | P/Q 组 | 一般不损坏 |
+
+**备机触发条件**:充电电流持续为 0、I2C 不通、SYS 异常、冒烟/发热 → **立即停测,换 R 组**。
+
+---
+
+## 三、设备清单
+
+| 设备 | 数量 | 使用 |
+|------|------|------|
+| 标准静电实验台 | 1 | ESD(独占) |
+| 浪涌发生器 | 1~2 | E1/E2 并行 |
+| 程控直流电源 | **3** | T1~T3 并行 + P 组 4h 长充 |
+| 可调 NTC 假电池 | **3 套** | T1/T2/T3 各 1 套 |
+| 3.8V 固定假电池 | 1 套 | C1 充电参数 |
+| 数据采集仪 / 电流监测 | 1 | 浪涌、曲线、漏电 |
+| 标准充电底座 + 5V 适配器 | 若干 | ESD 充电场景、反接验证 |
+| 环境温箱(可选) | 1 | 替代 NTC 假电池时 T 组改串行 +2 天 |
+
+---
+
+## 四、分项工时
+
+| 模块 | 样机 | 工时 | 并行 |
+|------|------|------|------|
+| ESD 三场景 | E1、E2 | **1 天**(验收模式,见 §4.1) | 2 台交替打;静电台 1 套 |
+| 浪涌 | E1、E2 | **0.5 天** | 2 台可并行 |
+| 充电基本参数 | C1 | **1~1.5 天** | — |
+| 放电温控 | T1、T2、T3 | **1 天** | **3 台并行** |
+| 充电限流 | T1、T2、T3 | **1 天** | **3 台并行** |
+| 保护 C 级 + 4h 长测 | P1~P3 | **2 天** | 3 台并行 |
+| 高损 A 级 | R1~R3 | **1 天** | 分日单工位 |
+| 充电曲线 | Q1、Q2 | **0.5 天** | 2 台并行 |
+| 功耗四项 | P 或 Q | **1 天** | 3 台分工 |
+| 报告整理 | — | **1 天** | — |
+| **墙钟合计(14 台并行)** | — | **5~7 工作日** | — |
+
+### 4.1 ESD 工期说明(为何 plan 曾写多天 vs 实际 1 天)
+
+原 Excel 用例(ESD-001/002/003)**纸面工作量**很大:
+
+| 维度 | 全量按字面执行 |
+|------|----------------|
+| 场景 | 待机 + 充电 + GPS(PPG 亮灯)共 **3 个** |
+| 点位 | 每场景约 **9~10 个**测试点 |
+| 电压 | 接触 ±6/±8kV;空气 ±10/±12/±16kV |
+| 次数 | 每点每电压 **20 次**(加严 50 次) |
+| 附加 | 每电压后功能检查;每电压等级后 GPS 室外定位;结束后刷工厂版点检 |
+
+粗算:单场景单台 **29 点 × 200 次放电 + 多次功能/GPS 检查** → 熟练工也要 **1.5~2 天/场景**。三场景全量串行 ≈ **4~5 天**——这是之前 plan 写「分开多天」的依据。
+
+**实际实验室 1 天能测完**,通常满足以下 **验收模式**(与你们 Excel D1 排法一致):
+
+| 做法 | 说明 |
+|------|------|
+| **正常验收 20 次** | 不做加严 50 次 |
+| **2 台 + 2 人** | E1/E2 同场景轮流,减少等待 |
+| **电压抽测** | 接触以 ±8kV 为主;空气以 ±10/±16kV 为主,不必每档全扫 |
+| **功能检查合并** | 按「电压等级」或「场景结束」统一检查,而非每点每电压都查一遍 |
+| **GPS 合并** | 每场景结束查 1 次室外定位,而非每个电压档都出去 |
+| **三场景同日串行** | 上午待机 → 下午充电 → 傍晚 GPS;或两班倒 |
+
+> **结论**:**对外验收若按 Excel 全文逐条执行 → 需 3~5 天**;**内部专项/二供筛片按验收模式 → 1 天合理**。本 plan **默认按 1 天排程**;若客户/认证要求全量,ESD 单独 +2~3 天,总工期改为 7~10 天。
+
+**D1 ESD 建议节奏(1 天)**:
+
+| 时段 | 内容 | 样机 |
+|------|------|------|
+| 09:00~11:00 | ESD-001 待机场景(接触 ±8kV + 空气 ±10/±16kV) | E1、E2 |
+| 11:00~14:00 | ESD-002 充电场景(电量 <20%,插底座) | E1、E2 |
+| 14:00~17:00 | ESD-003 GPS 运动场景(PPG 亮灯,室外定位) | E1、E2 |
+| 17:00~18:00 | 刷工厂版 + 传感器/地磁点检 | E1、E2 |
+
+D1 其余产线(C/T/P)与 ESD **并行**——ESD 只占 E1/E2,不占静电台的时段外 T/P/C 照常测。
+
+---
+
+## 五、并行排程(执行用日程)
+
+### 5.1 产线分工
+
+```mermaid
+gantt
+ title 充电IC专项测试 6~8工作日
+ dateFormat YYYY-MM-DD
+ axisFormat %m/%d
+
+ section LineA_ESD
+ ESD三场景1天_E1E2 :a1, 2026-06-09, 1d
+ 浪涌_E1E2 :a2, after a1, 0.5d
+
+ section LineB_Param
+ 充电基本参数_C1 :b1, 2026-06-09, 1.5d
+
+ section LineB_Temp
+ 放电温控_T1T2T3 :t1, 2026-06-09, 1d
+ 充电限流_T1T2T3 :t2, after t1, 1d
+
+ section LineC_Protect
+ 保护短测_P1P2P3 :c1, 2026-06-09, 1d
+ 4h欠压充_P1P2P3 :c2, after c1, 0.5d
+ 4h高压充_P1P2P3 :c3, after c2, 0.5d
+ OVP_UVP_P1 :c4, after c3, 0.5d
+ 充电曲线_Q1Q2 :c5, after c4, 0.5d
+
+ section LineR_Reserve
+ VBUS16V_反接_短接_R组 :r1, 2026-06-11, 1d
+
+ section LineD_Power
+ 功耗 :d1, 2026-06-12, 1d
+```
+
+### 5.2 每日任务清单
+
+| 日 | 并行任务 | 样机 | 填表 |
+|----|----------|------|------|
+| **D1** | **ESD 三场景 1 天收尾**(待机→充电→GPS);C1 充电参数;T1~T3 放电温控;P1~P3 机械振动/漏电/过放 | C1,T1-3,P1-3,E1-2 | 静电测试表 + 充电基本参数 + 保护 1#/2#/3# |
+| **D2** | C1 参数收尾;T1~T3 充电限流;P1~P3 启动 4h 欠压充 | C1,T1-3,P1-3 | 充放电保护温控区 |
+| **D3** | P1~P3 4h 高压充;T 组收尾 | P1-3,T1-3 | 保护长测结论 |
+| **D4** | R1 VBUS16V → R1/R2 反接 → R2 短接;P1 OVP/UVP;Q1/Q2 充电曲线 | R1-3,P1,Q1-2 | 保护 + 充电曲线 |
+| **D5** | E1/E2 浪涌 6V~120V(高挡可用 R2);功耗四项 | E1-2,P/Q | 浪涌、功耗 |
+| **D6** | E1/E2 刷工厂版点检(若 D1 未做);补测 | E1-2 | 静电结论 |
+| **D7** | 补测;报告整理;异常 dump | 按需 | 全部结论列 |
+| **D8** | 评审 / PASS-FAIL | — | 汇总 |
+
+### 5.3 温控三台分工(T1~T3)
+
+| 样机 | Excel 列 | D1~D2 任务 | 判据要点 |
+|------|----------|------------|----------|
+| **T1** | 1# | 放电 >50℃ 自动关机 + 25℃ 恢复 | >50℃ 关机 |
+| **T2** | 2# | 放电 <-10℃ 自动关机 + 恢复 | <-10℃ 关机 |
+| **T3** | 3# | 充电限流:<0℃ 停充;0~15℃ 0.2C;15~45℃ 1C;45~60℃ 0.5C | 各温区电流符合规格 |
+
+### 5.4 R 组高损执行顺序
+
+| 顺序 | 项目 | 样机 | 时长 | 注意 |
+|------|------|------|------|------|
+| 1 | VBUS 16V / 0.5h | R1 | 0.5h + 验证 | 测完大概率报废 |
+| 2 | 反接 30s | R1 或 R2 | 0.5h | 电流 ≤1mA |
+| 3 | Pogo 短接 10s | R2 | 0.5h | 需有人值守 |
+| 4 | 浪涌 50V~120V(若 E 组已损) | R2 | 视档位 | 与 E 组二选一 |
+
+---
+
+## 六、人员与交付物
+
+| 角色 | 人数 | 职责 |
+|------|------|------|
+| 测试工程师 | 2 | 操作、填 Excel、日报 |
+| 硬件支持 | 1 | 飞线、假电池、NTC、VBUS16V 安全 |
+| 软件支持 | 0.5 | 版本确认、串口读 HP4570、`[battery]` 日志 |
+
+**交付物:**
+
+1. 填完整的 Excel 7 张工作表(Date、版本号、1#/2#/3#、结论 PASS/FAIL)
+2. 样机台账(§2.2)含 SN 与损坏记录
+3. 充电曲线原始数据(Time/Vbat/Ibat/Iin)
+4. ESD 失败点照片 + 功能检查记录
+5. 异常样机 HP4570 寄存器 dump(可对齐 `cc_cycle_defect.bin` 格式)
+
+---
+
+## 七、资源汇总
+
+| 维度 | 最少 | **推荐** |
+|------|------|----------|
+| 主测样机 | 8 | **11**(E×2+T×3+P×3+C×1+Q×2) |
+| 备机 | 2 | **3**(R1~R3) |
+| **样机总计** | 10 | **14** |
+| 可调 NTC 假电池 | 1 套 | **3 套** |
+| 程控电源 | 2 | **3** |
+| 日历工期 | 8~10 天(ESD 全量) | **5~7 天**(ESD 1 天验收模式) |
+| 预计损坏/FA | 1~2 台 | **2~3 台**(R 组为主) |
+
+---
+
+## 八、风险与禁止事项
+
+1. **禁止** T1~T3 参与 VBUS16V、反接、短接——温控数据一旦因损坏缺失需整项重测
+2. **禁止** 无备机直接跑 A 级高损项——连续失败会阻塞全计划
+3. **建议顺序**:D1 ESD(E 组)‖ C/T/P 并行 → R 组 A 级 → E 组浪涌 → 工厂版点检
+4. **温箱替代 NTC**:T 组由并行改串行,工期 **+2 天**
+5. 损坏样机 SN 写入 [HP4570_LOT追溯交叉表.md](./HP4570_LOT追溯交叉表.md) 备查
+
+---
+
+## 九、验收标准
+
+| 项 | 标准 |
+|----|------|
+| 样机覆盖 | 14 台台账齐全;T/P/R 分组无交叉违规 |
+| Excel | 7 张表结论列全部填写;1#/2#/3# 与 T1~T3 或 P1~P3 对应 |
+| 温控 | 3 台均完成放电温控 + 充电限流,判据符合规格 |
+| 高损 | R 组执行完毕或有书面说明(备机用尽/项目豁免) |
+| 异常 | 损坏样机有 dump + FA 流转记录 |
+| 工期 | 5~7 工作日(ESD 验收 1 天;全量 ESD 则 8~10 天) |
--- /dev/null
+---
+name: 充电电量显示分析
+overview: 低电量关机后充电开机显示 26% 的根因是 history 丢失 + 纯电压查表。关机保存不能直接恢复——需区分充电/非充电场景,停充后再存、开机分情况恢复。
+todos:
+ - id: save-with-context
+ content: 关机保存前先停充1s读OCV,并记录 percent + was_charging + low_bat_flag
+ - id: smart-restore
+ content: 开机三分支恢复:电压接近用saved / 关机充久了从saved慢涨 / 低电+充电防虚高钳位
+ - id: fix-charge-map-select
+ content: 停充采样窗口内强制用放电表,treat_as_charging 不只看 VBUS
+ - id: verify-logs
+ content: 覆盖三种场景抓日志验证
+isProject: false
+---
+
+# 低电量关机充电开机显示 26% 问题分析
+
+## 一般是否需要算法?
+
+**需要。** 项目已有 **OCV 双表 + history 平滑 + 停充采样** 算法([`battery_driver.cpp`](code/sdk/zhihui/services/battery/service/battery_driver.cpp)),不是库仑计,也没有独立 fuel gauge 芯片。
+
+---
+
+## 核心问题
+
+**26% ≈ 电压查表结果**(FD272 约 3.62~3.82V),空电电池静置/轻充后端电压虚高,而 `history_percent=0`(持久化被注释)导致平滑失效,第一次采样直接跳到查表值。
+
+---
+
+## 关于「关机保存」的风险(用户反馈)
+
+你的顾虑**完全成立**,不能简单取消注释 `battery_save_vol()`:
+
+### 风险场景
+
+| 场景 | 问题 |
+|------|------|
+| **低电关机 + 插着充电器** | 关机瞬间电压被充电抬高,存的 SOC/电压本身偏高 |
+| **关机后继续充电** | 硬件仍可充电,几小时后真实 SOC 远高于保存值 |
+| **立刻充电开机** | 保存值偏低 vs 电压查表偏高,两个都不可信 |
+| **长时间关机充电后开机** | 现有 restore 会因电压差 >0.4V **丢弃** 保存值 → 又回到纯查表 26% |
+
+现有 `battery_restore_vol()` 逻辑:
+
+```653:657:code/sdk/zhihui/services/battery/service/battery_driver.cpp
+ if (bat_vol.percent > 0 && bat_vol.percent <= 100) {
+ float threshold = get_charge_stat() ? 0.4f : 0.25f;
+ if (fabsf(cur_vol - bat_vol.avg_vol) < threshold) {
+ history_percent = bat_vol.percent;
+ }
+ }
+```
+
+- 电压接近 → 用 saved(适合**非充电、短时间**关机)
+- 电压差大 → **丢弃 saved**(长时间关机充电后反而帮不上忙)
+
+所以:**关机保存不是银弹,需要「有条件下保存 + 分场景恢复」。**
+
+---
+
+## 修订方案(替代原 P0 简单恢复)
+
+### 方案 A — 有条件保存(关机时)
+
+保存前增加步骤,避免把「充电中的虚高电压」写进文件:
+
+1. **若插着充电器**:先 `disable_charge()`,等 1s,读一次 OCV 再保存
+2. **扩展 `BAT_VOL_T`**(或 sidecar 字段),多存:
+ - `percent` — 当前显示 SOC(`cur_percent`)
+ - `avg_vol` — 停充后的 OCV
+ - `flags` — bit0: `low_bat_shutdown`,bit1: `was_charging`
+3. **低电关机路径**([`charge_notification.cpp:597`](code/sdk/application/wearable/fd272/service/msg_center/src/notify/charge_notification.cpp))打 `low_bat_shutdown` 标记写入文件(现有 RAM 标志只用于日志,需持久化)
+
+> 不在「正在大电流充电、未停充」时直接存瞬时电压。
+
+### 方案 B — 分场景恢复(开机时)
+
+开机 `battery_restore_vol()` 改为三分支,**不盲目信任 saved 也不盲目信任电压**:
+
+```mermaid
+flowchart TD
+ boot["开机读 saved + 停充1s读 cur_vol"]
+ near["电压接近 saved\n|cur - saved_vol| < 阈值"]
+ chargedWhileOff["cur_vol 明显高于 saved\n关机期间充进去了"]
+ lowBatCharge["low_bat + was_charging\n立刻充电开机"]
+
+ useSaved["history = saved_percent\n正常平滑"]
+ rampUp["history = saved_percent\n向 voltage_soc 慢涨 +1%/tick"]
+ clampDown["history = saved_percent\n不允许超过 saved + 小步长\n防 5%→26% 跳变"]
+
+ boot --> near --> useSaved
+ boot --> chargedWhileOff --> rampUp
+ boot --> lowBatCharge --> clampDown
+```
+
+| 分支 | 条件 | 策略 |
+|------|------|------|
+| **正常恢复** | `\|cur_vol - saved_vol\| < 0.25V` 且非低电关机 | `history = saved`,走现有平滑 |
+| **关机充久了** | `cur_vol > saved_vol + 0.4V` | 以 saved 为**下限**,按 +1%/tick 向 `voltage_soc` 靠拢(承认充进去了,但不瞬间跳) |
+| **低电+充电立刻开机** | `low_bat_flag && was_charging` | 以 saved 为**上限锚点**:显示 `min(voltage_soc, saved + 慢涨)`,**禁止** 5% 瞬间变 26% |
+| **异常** | saved 数据损坏 / vol 不合理 | 丢弃,纯电压 + 慢涨从 0 或 1 开始 |
+
+### 方案 C — 不依赖关机的补充(降低 saved 偏差影响)
+
+- 运行期间定期写 `battery.bin`(如每 10 分钟 / 电量变化 5%),关机保存变成「最后一次快照」而非唯一来源
+- 开机 restore 时优先用「最近一份合理快照」,关机保存只是其中之一
+
+### 方案 D — 查表条件修正(与保存无关,仍建议做)
+
+- 停充 1s 采样窗口内**强制用放电表**
+- `treat_as_charging` 看充电 IC 实际使能,不只看 VBUS 插入
+
+---
+
+## 各场景预期行为(修订后)
+
+| 用户操作 | 修订前 | 修订后 |
+|----------|--------|--------|
+| 低电 5% 关机,立刻插充电开机 | 26% 跳变 | 从 5% 慢涨,不瞬间到 26% |
+| 低电关机,充 2 小时再开机 | 26% 或 0% | 从 5% 慢涨,逐步接近真实值 |
+| 50% 正常关机,未充电,立刻开机 | 可能偏差 | saved ≈ 当前,正常恢复 |
+| 50% 关机,充一夜再开机 | saved 被丢弃 → 查表 | saved 作下限,慢涨到真实 SOC |
+
+---
+
+## 不建议的做法
+
+- ~~直接取消注释 `battery_save_vol()` / `battery_restore_vol()`~~ — 充电关机场景会存错、restore 也会丢弃
+- ~~开机完全信任 saved~~ — 长时间关机充电后 saved 太低
+- ~~开机完全信任电压查表~~ — 低电立刻开机虚高到 26%
+
+---
+
+## 实施优先级
+
+1. **方案 B 低电+充电钳位** — 直接解决 26% 跳变,改动集中在 `battery_restore_vol` + 开机首次 SOC
+2. **方案 A 有条件保存** — 让 saved 值本身更可信
+3. **方案 D 查表修正** — 降低 voltage_soc 基准偏差
+4. **方案 C 定期快照** — 可选,进一步降低对关机瞬间的依赖
+5. **P3 库仑计** — 中长期,若以上仍不满足
+
+---
+
+## 结论
+
+| 问题 | 回答 |
+|------|------|
+| 需要算法吗? | 需要;已有 OCV+平滑,但 history 链路断了 |
+| 关机保存有风险吗? | **有** — 充电中关机、关机后继续充都会让 saved 和真实不一致 |
+| 正确方向? | **不是简单恢复保存**,而是停充后存 + 分场景恢复 + 低电钳位 |
+| 26% 怎么修? | 低电充电开机:以 saved SOC 为锚,禁止电压查表一次性跳变 |
--- /dev/null
+# 拷机「放电至关机 → 开机自动拷机 → 充满再放电」
+
+> 完整 plan 见 Cursor plan;本文档同步关键决策。
+
+## 开机门禁(按你的意见调整)
+
+**只要拷机标志位满足就自动进拷机 UI,不再依赖 `fac.bin` 产测标志。**
+
+| 条件 | 字段 | 说明 |
+|------|------|------|
+| 拷机开关开 | `test_enabled=1` | 产测页「拷机开关」 |
+| 在自动拷机 | `auto_running=1` | 点过「开始自动拷机」且未停止/未拔线 |
+| 插线 | `get_charge_stat()==1` | 未插线:清 `auto_running`,不进 UI |
+
+实现改动:
+- [`custom_utils.c`](code/sdk/zhihui/utils/custom_utils.c):`cc_cycle_on_boot()` **去掉** `custom_get_factory_test_flag()` 判断,改为始终调用(或 `cc_cycle_init` 后由 on_boot 内部读 bin 判断)
+- [`battery_service.cpp`](code/sdk/zhihui/services/battery/service/battery_service.cpp):`cc_cycle_early_boot_sync()` 同样改为只认 `cc_cycle.bin`
+
+满足上述条件 → **`cc_cycle_show_burn_ui()`**(与手动开始拷机同一入口)。
+
+---
+
+## 充电还是放电?—— 开机阶段判断(核心)
+
+**进 UI 和选阶段是两件事:** 先进拷机全屏页,页面内根据 `phase` 显示浅绿(充电)或白屏(放电)。
+阶段由 `cc_cycle.bin` 里持久化字段 + 实时 SOC/电压决定,**优先级如下**:
+
+```mermaid
+flowchart TD
+ Start["cc_cycle_on_boot\n读 cc_cycle.bin"] --> Gate{"test_enabled\n&& auto_running\n&& 插线?"}
+ Gate -->|否| Exit["不进拷机UI"]
+ Gate -->|是| P1{"pending_charge==1?"}
+ P1 -->|是| Charge1["充电 phase\n清 pending_charge"]
+ P1 -->|否| P2{"bin里 phase?"}
+ P2 -->|CHARGING| Charge2["续充电"]
+ P2 -->|DISCHARGE| P3{"pending_charge==1\n或 last_soc<=10%\n或 实时soc<=10%\n或 batV<=3.3V?"}
+ P3 -->|是| Charge3["兜底→充电\n防标志/写盘失败"]
+ P3 -->|否| Discharge["续放电\n恢复 soc_peak"]
+ P2 -->|IDLE/未知| Pick["cc_pick_initial_phase(soc)"]
+ Pick --> PickFull{"soc>=99%\n或 CHG_DONE?"}
+ PickFull -->|是| Discharge2["开始放电"]
+ PickFull -->|否| PickLow{"soc<=10%?"}
+ PickLow -->|是| Charge4["开始充电"]
+ PickLow -->|否| PickMid["中间电量:\n上次CHARGING→充\n否则→放"]
+ Charge1 --> UI["cc_cycle_show_burn_ui"]
+ Charge2 --> UI
+ Charge3 --> UI
+ Charge4 --> UI
+ Discharge --> UI
+ Discharge2 --> UI
+ PickMid --> UI
+```
+
+### 各字段分工(不要混)
+
+| 字段 | 作用 | 何时写入 |
+|------|------|----------|
+| `test_enabled` | 拷机总开关 | 用户 toggling 拷机开关 |
+| `auto_running` | 是否在自动循环 | 点开始=1;停止/拔线/关开关=0 |
+| `phase` | **当前应在充还是放** | tick 状态机切换时写盘 |
+| `pending_charge`(v4 新增) | **掉电后下次必须先充** | 放电 soc≤5% 预置;on_boot 转充后清 0 |
+
+### 典型场景对照
+
+| 场景 | pending | phase | SOC | → 开机阶段 |
+|------|---------|-------|-----|------------|
+| 掉电前已放到 5% 以下 | 1 | DISCHARGE | 低 | **充电** |
+| 掉电前 pending 写盘失败 | 0 | DISCHARGE | 低 | **充电**(见下方三重兜底) |
+| 放电中途 reboot,电量 50% | 0 | DISCHARGE | 中 | **续放电** |
+| 正在充电时 reboot | 0 | CHARGING | 任意 | **续充电** |
+| 首次点「开始」,电量 80% | 0 | IDLE | 中 | `pick` → **放电**(中间偏高默认放) |
+| 首次点「开始」,电量 3% | 0 | IDLE | 低 | `pick` → **充电** |
+| 充满刚要开始下一轮 | 0 | CHARGING→tick切 | ≥99% | tick 会切放电;若 reboot 在切换瞬间看 bin 里 phase |
+
+### 运行中(tick)与开机的区别
+
+- **开机**:读 bin 恢复 `phase` + `pending_charge`,必要时兜底改充电
+- **运行中 tick**(v4):
+ - 放电:**不再** soc≤10% 切充电,放到掉电;soc≤5% 只置 `pending_charge=1`
+ - 充电:soc≥99% 或 CHG_DONE → `cc_finish_charging_phase()` → 进入放电
+
+---
+
+## 当前无法「放电至关机」— 根因(与 plan 目标差距)
+
+> 期望:按上文「放到自然掉电 → 关机 → 再开自动进拷机充电 UI」。
+> 实测:拷机放电阶段 **到 ~10% 就转充电**,或永远走不到自动关机续跑。
+
+### 根因 1:10%×30s 兜底 **主动转充**(与「自然掉电」冲突)
+
+[`cc_cycle_service.c`](code/sdk/zhihui/services/cc_cycle_service/cc_cycle_service.c) `cc_tick()` 放电分支:
+
+```591:603:code/sdk/zhihui/services/cc_cycle_service/cc_cycle_service.c
+ /* v4:放到自然掉电;5% 预置 pending_charge;30s@10% 兜底转充 */
+ if (soc <= CC_CYCLE_SOC_ARM && g_store.pending_charge == 0) {
+ g_store.pending_charge = 1;
+ cc_store_save();
+ }
+ if (soc > CC_CYCLE_SOC_LOW) {
+ g_discharge_low_since_ts = 0;
+ } else {
+ if (g_discharge_low_since_ts == 0) {
+ g_discharge_low_since_ts = now;
+ } else if ((now - g_discharge_low_since_ts) >= CC_CYCLE_DISCHARGE_FAILSAFE_SEC) {
+ cc_finish_discharge_phase(soc); /* → 进入充电,不会关机 */
+ }
+ }
+```
+
+- `CC_CYCLE_SOC_LOW = 10`,`CC_CYCLE_DISCHARGE_FAILSAFE_SEC = 30`
+- **SOC ≤10% 持续 30s 必调 `cc_finish_discharge_phase()` → 切充电**
+- plan 文案写「不再 soc≤10% 切充电」,但 **代码仍保留 30s 兜底**,这是「放不到关机」的**主因**
+
+### 根因 2:关机清理 **清掉拷机续跑标志**
+
+[`cc_cycle_prepare_for_shutdown()`](code/sdk/zhihui/services/cc_cycle_service/cc_cycle_service.c) → `cc_shutdown_cleanup_locked()`:
+
+- `auto_running = 0`
+- `cc_clear_pending_charge()`(**pending_charge 也清 0**)
+- `phase = IDLE`
+
+即使电池真放到关机,下次 [`cc_cycle_on_boot()`](code/sdk/zhihui/services/cc_cycle_service/cc_cycle_service.c) 门禁 `test_enabled && auto_running` **不满足**,**不会自动进拷机 UI/充电**。
+
+与 plan「掉电后再开 → 充电 UI」**矛盾**。
+
+### 根因 3:插线放电时 **不走系统低电关机逻辑**
+
+[`battery_check_chargement()`](code/sdk/zhihui/services/battery/service/battery_service.cpp) 在 `cc_cycle_is_discharge_phase()` 时 **直接 return**;插线分支也 **不对低 SOC 调** `low_power_play_toUI(关机)`。
+
+拷机插线放电依赖 **电池耗尽硬关机** 或 HP4570 VBAT_UVLO,而不是软件 5%/2% 关机流程。
+
+### 根因 4:文档不一致
+
+[`充放电拷机汇总.plan.md`](plan/充放电拷机汇总.plan.md) 仍写「SOC ≤ **10%** → 切充电」(旧逻辑),与 v4「自然掉电 + 5% pending」不一致,易误判为 bug。
+
+---
+
+## 修复方向(对齐 plan「放电关机循环」)— **已实现 2026-06**
+
+| 项 | 改动 | 状态 |
+|----|------|------|
+| **F1** | 去掉 `10%×30s → cc_finish_discharge_phase`;仅 soc≤5% 写 `pending_charge` + 自然掉电 | ✅ `cc_cycle_service.c` |
+| **F2** | `cc_cycle_prepare_for_shutdown()` / 关机 tick 仅 `cc_release_hw_only_locked()`,保留 `auto_running`/`pending_charge`/`phase` | ✅ |
+| **F3** | `cc_cycle_on_boot` 兜底转充(已有) | ✅ 依赖 F2 |
+| **F4** | 低电主动软件关机 | ⬜ 未做,依赖电池自然掉电/UVLO |
+| **F5** | 更新 [`充放电拷机汇总.plan.md`](plan/充放电拷机汇总.plan.md) | ✅ |
+
+### F1 建议(与 plan 一致)
+
+- 删除 `g_discharge_low_since_ts` 30s 转充逻辑,或加宏 `CC_CYCLE_DISCHARGE_FAILSAFE_SEC=0` 产测关闭
+- 保留 soc≤5% → `pending_charge=1` + `cc_store_save()`
+
+### F2 建议(关机后续跑)
+
+```c
+/* 关机/重启:仅释放硬件,不结束拷机会话 */
+void cc_cycle_prepare_for_shutdown(void) {
+ cc_release_hw_only(); /* 新函数:关 GPS/常亮/Hi-Z,允充 */
+ cc_store_save(); /* 保留 auto_running/pending_charge/phase */
+}
+```
+
+`cc_shutdown_cleanup_locked()` 仍用于:用户点停止、拔线、关拷机开关。
+
+---
+
+
+- 置 1:放电 `soc <= 5%`,立即写盘
+- 清 0:on_boot 转充电 / 新放电轮 / 关开关 / 停止 / 拔线
+- ~~tick 兜底:DISCHARGE + soc≤10% 持续 30s → 强制充电~~(**已移除**,改为自然掉电)
+
+### 关机前标志没写上,低电再开会充电吗?
+
+| | 当前代码(未做 v4) | v4 计划 |
+|--|-------------------|---------|
+| bin:`phase=DISCHARGE`,`pending_charge=0` | **不会** — 无条件 `cc_enter_discharge_phase()` | **会** — 开机兜底转充电 |
+| 兜底条件(满足任一即充电) | 无 | ① `pending_charge==1` ② bin 里 **`last_soc<=10%`**(掉电前 tick 已写入,不受插线后 SOC 虚高影响) ③ 实时 `soc<=10%` ④ `batV<=3.3V` |
+
+**为何加 `last_soc`:** 掉电后插线,实时 SOC 可能从 9% 跳到 23%(充电表),单靠实时 soc 兜底会失效;`last_soc` 是掉电前最后一秒写盘的,更可靠。
+
+**仍可能先进系统充电 Logo**(深欠压 `batV<=3.3V`),电压起来 reset 后再跑 `cc_cycle_on_boot` — 与拷机逻辑无关,但最终仍会进充电拷机 UI。
+
+---
+
+
+## cc_cycle.bin v4 字段
+
+```
+test_enabled, auto_running, phase,
+pending_charge, // 新增
+last_soc, soc_peak, round_count, ...
+```
+
+---
+
+## 验证要点
+
+1. **仅拷机标志**:关 fac.bin 或正常产测,只要 `test_enabled+auto_running+插线` → 进拷机 UI
+2. 掉电后再开 → **充电** UI(浅绿)
+3. 放电 50% reboot → **续放电** UI(白屏)
+4. 充电中 reboot → **续充电**
+5. 关拷机开关 reboot → 不进 UI,`auto_running=0`
+
+---
+
+## 硬件风险:全程插线会不会烧充电 IC?
+
+**结论:按现有 HP4570 控制方式,正常不会「烧 IC」;主要压力在电池温升与循环寿命,需长跑监控。**
+
+### 拷机两阶段 IC 在干什么
+
+| 阶段 | HP4570 操作 | 含义 |
+|------|-------------|------|
+| **充电** | `hiz=false` + `ceb=true` | 正常充电路径,ICHG 由 `battery_service` 按温度/SOC 分档 |
+| **放电(线仍插着)** | `ceb=false` + **`hiz=true`** | 禁充 + VBUS 功率路径高阻,**由电池带载**,适配器不往电池硬灌 |
+
+Hi-Z 是芯片寄存器 [`HP4570N_EN_HIZ`](code/sdk/zhihui/drivers/charge/hp4570/hp4570.h) 的设计能力,不是把 VBUS 和电池短接;拷机放电是「插线检测 + 电池供电」,与「边充边用」不同。
+
+### 为何一般不会烧 IC
+
+1. 放电阶段**不打开充电 MOSFET/充电逻辑**(CEB 关),IC 不做降压充电,处于高阻隔离态
+2. 充电阶段走现有产线同一套温控、`ichg` 限流(0.2C/0.5C/1C 分档)
+3. 掉线/停拷机会 `cc_set_charge_enable(1)` 恢复默认,不会长期卡在异常态
+4. 深欠压启动路径里已有 HP4570 WDT 检测/复位([`ohosfwk_startup.c`](code/sdk/application/wearable/ohos_startup/ohosfwk_startup.c))
+
+### 真正要担心的(比烧 IC 更现实)
+
+| 风险 | 说明 | 计划缓解 |
+|------|------|----------|
+| **电池/整机温升** | 放电:白屏常亮 + GPS;充电:GPS 仍开 → 发热叠加 | 长跑看电池/PCB NTC;可选「充电阶段关 GPS」 |
+| **放电阶段不做温控** | `battery_check_chargement` 在放电期 return,拷机 tick 未查温度 | v4 可选:cc_tick 读 `bat_temp`,超阈值停拷机/禁充 |
+| **HW:充! 异常** | 放电期硬件仍在充(Hi-Z/CEB 未生效) | 已有状态行 `HW:充!`,测试需排查 |
+| **满放满充循环** | 伤电池寿命,不直接等于 IC 损坏 | 产测可接受;注明非用户日常使用场景 |
+| **极低电立刻大电流充** | 依赖 HP4570 预充/死电充电状态机 | 观察 pre_chrg/dead_chrg 日志 |
+
+### 与「不插线放电」对比
+
+- 不插线:普通放电,无 VBUS,IC 无额外应力
+- **插线 + Hi-Z 放电**:VBUS 在但路径高阻 — 为拷机自动化服务,属于**有意的产测工况**,需在 HP4570 规格书 Hi-Z + VBUS present 范围内(需硬件同事确认适配器/座子设计)
+
+### 测试建议(写入拷机验证)
+
+1. 每轮记录电池温度、是否出现 `HW:充!`
+2. ≥24h 长跑摸 IC/电池区域温升
+3. 用万用表/日志确认放电阶段 `ceb=0、hiz=1`,充电阶段 `ceb=1、hiz=0`
+4. 异常温升时:停拷机开关即恢复允充,不会锁死在 Hi-Z
+
+---
+
+## 硬件资料评估(HP4570N + PTVSH + FD272 MB)
+
+> 依据:`HP4570N Datasheet_V0.2_20241202.pdf`、`PTVSHC3N18VB` 规格书、[`FD272_MB_V1.1_20251202`](file:///mnt/c/Users/ken'n'y/Desktop/充电/FD272_MB_V1.1_20251202(1).pdf) 充电页(Page 7)
+
+### 1. 拷机放电 = HP4570 手册定义的工作模式(非野路子)
+
+数据手册 **Figure 11 状态机**(Page 22)明确:
+
+- **IN 插入 + `EN_HiZ=1`** → **Battery Discharge Mode**(仅 BAT→SYS)
+- **IN 插入 + `EN_HiZ=0` + 允充** → Battery Charge Mode / Only Power System Mode
+
+与 [`cc_set_charge_enable(0)`](code/sdk/zhihui/services/cc_cycle_service/cc_cycle_service.c) 一致:`ceb=关` + `hiz=开`。
+
+拷机「插线放电」是芯片 **Power Path Management** 设计能力,不是把 VBUS 硬灌进电池。
+
+### 2. FD272 硬件链路
+
+| 器件 | 位号/型号 | 作用 |
+|------|-----------|------|
+| 充电 PMIC | **U900 HP4570C9-4.2V** | VBUS→IN,SYS→系统,BAT→电池;I2C 控制 |
+| VBUS 保护 | **PTVSHC3N18VB** 类 TVS(原理图另有 4.8V/24V 型号) | **ESD/浪涌**,VRWM=18V,非功率 MOSFET |
+| 输入开关 | U901 GLF1121H 等 | VBUS 路径管理 |
+| 温度 | NTC→HP4570 NTC 脚 + BATT_NTC_ADC | 充电温控 |
+
+HP4570 **VIN 绝对最大 28V**,片内 **OVP≈6V**;5V 适配器在安全工作区。PTVSH 18V TVS 用于插拔 ESD/瞬态,与拷机循环无直接冲突。
+
+### 3. 拷机全程插线 — IC 会不会烧?
+
+**结论:在软件正确切换 Hi-Z/CEB 的前提下,HP4570 本身不太会因「插线拷机」而烧毁;主要风险是热、异常充电、电池循环。**
+
+| 阶段 | 硬件行为 | 手册保护 |
+|------|----------|----------|
+| **放电(拷机)** | Q1(IN→SYS)高阻,BAT FET 导通,电池带载 | 放电 OCP 默认 2A(可配至 3.2A);VBAT_UVLO 默认 2.8V 关 BAT FET |
+| **充电(拷机)** | CC/CV,ICHG 由 `battery_service` 分档 | 热调节 80~130°C;TSHUT 关 IN→SYS/SYS→BAT 充电路径;NTC/PCB OTP;IINDPM 默认 ≤1A |
+| **Hi-Z↔充电切换** | 状态机 Figure 11 已定义 | 非短路切换 |
+
+**需警惕(比「烧 IC」更现实):**
+
+1. **`HW:充!`**:放电期若 Hi-Z/CEB 未生效,IN 仍给 SYS/电池补电 → 违背拷机意图,增加发热;需在拷机 UI/日志监控
+2. **拷机负载大**:白屏 + GPS,放电电流可能接近 BAT→SYS OCP;拷机长跑看是否进 hiccup(512ms 间隔)
+3. **充电期 GPS 常开**:整机温升 → 更易触发 HP4570 **THERM_STAT** 降流,属保护而非损坏
+4. **深放到关机**:手册 VBAT_UVLO 会在 ~2.8V 关 BAT FET;拷机「放到掉电」与 UVLO 交互需实测(可能先 UVLO 保护再掉电)
+5. **I2C Watchdog**:HP4570 默认 40s WDT;长跑需确认驱动有 `wdt_rst` 或拷机阶段 WDT 策略(已有 WDT fault 检测)
+
+### 4. 与 v4 拷机计划的硬件配合
+
+- **pending_charge + 低电转充**:符合 HP4570 先 **dead/pre/fast charge** 再 CC/CV 的充电曲线(Page 23-24)
+- **拷机 tick 加温度监控**(计划可选):弥补放电期 `battery_check_chargement` 不跑的空窗
+- **充电阶段可选关 GPS**:降低 PCB/HP4570 结温,减少 THERM 降流打断满充
+- **产测验证**:每轮记录 `THERM_STAT`、`WDT_FAULT`、`HW:充!`、HP4570 寄存器 dump(已有 `hp4570n_dump_debug_registers`)
+
+### 5. 硬件 Sign-off 建议(给硬件同事)
+
+1. 确认 VBUS 上 **PTVSHC3N18VB** 位置与 HP4570 IN 的钳位/布局满足 5V 适配器 + ESD
+2. 确认 **EN_HiZ=1 且 CHAR 禁充** 时 IN→SYS 电流实测 ≈ 0(或仅 quiescent)
+3. 拷机 24h 温升:HP4570 本体、BAT 旁、U901 输入开关
+4. V1.1 / V1.2 位号图对照 U900/D900 是否一致(V1.2 位号图 PDF 为图片,需人工核对)
--- /dev/null
+---
+name: 拷机菜单位置与UI优化
+overview: 拷机移产测菜单最底部;仅主菜单点击进入,禁止「下一项」链式进入;重构控制页与全屏 UI。
+---
+
+# 拷机菜单位置与 UI 优化(摘要)
+
+完整 plan 见 Cursor plan 文件;本文档同步关键决策。
+
+## 进入方式
+
+| 方式 | 是否允许 |
+|------|----------|
+| 主菜单最底部点击「充放电拷机」 | 允许 |
+| 任意常规项「下一项」链式进入 | **禁止**(误导产测) |
+
+## 实现要点
+
+1. `Test_ViewMapper_t`:CCCYCLE 移数组末尾,主菜单仍显示
+2. `testPresenter.cpp`:`i+1` 为 CCCYCLE 时不跳转
+3. 倒数第二项测试页:隐藏「下一项」(下一项是拷机时)
+4. `testCcCycle.cpp`:不显示「下一项」
+5. 控制页 + 全屏页 UI 重构(见完整 plan)
+
+## 验证
+
+- 主菜单点拷机 → OK
+- 恢复出厂 / RF5133 点下一项 → **不进拷机**
+- 电池充电点下一项 → 喇叭,不进拷机