优化ECG数据存储逻辑 3458/1 3458/head 3459/target/1
authorxue <2144439972@qq.com>
Thu, 11 Jun 2026 06:29:34 +0000 (14:29 +0800)
committerxue <2144439972@qq.com>
Thu, 11 Jun 2026 06:29:34 +0000 (14:29 +0800)
code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/include/ECGMainPresenter.h
code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/include/ECGMeasuringView.h
code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/include/ECGModel.h
code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGHistoryView.cpp
code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGMainPresenter.cpp
code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGMeasureResultView.cpp
code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGMeasuringView.cpp
code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGModel.cpp

index bb4d152..7c193ff 100644 (file)
@@ -34,6 +34,7 @@ public:
   void StartMeasureTimer();
   void StopMeasureTimer();
   void StopContactGoodTimer();
+  void OnMeasureTimerTick();
   void FinishMeasuringFlow(ECGMainView *mainView);
   void OnBottomButtonPressed(ECGMainView *mainView);
 
@@ -46,8 +47,17 @@ private:
   void ShowContactGood(ECGMainView *mainView);
   bool IsWaitingContact(ECGMainView *mainView) const;
   void SetScreenAlwaysOnForEcg(bool enable);
+  void StartPpgAfibSession();
+  void StopPpgAfibSession();
+  bool PollAfibResult();
+  void OnMeasureTimeout();
 
-  bool screenAlwaysOnForEcg_{false};
+  static constexpr int32_t kMeasureTickMs = 200; // 测量定时器周期
+  static constexpr int32_t kPollIntervalMs = 1000; // 测量定时器间隔
+
+  bool screenAlwaysOnForEcg_{false}; // 屏幕长亮
+  bool measureDataReceived_{false}; // 测量数据是否收到
+  int32_t pollElapsedMs_{0}; // 测量定时器流逝时间
 };
 
 } // namespace OHOS
index 339cf41..934e068 100644 (file)
@@ -14,14 +14,19 @@ public:
   static ECGMeasuringView *GetInstance();
   UIViewGroup *InitView();
   void ResetMeasurement();
-  /** @return true when countdown finished */
-  bool OnTimerTick();
+  void UpdateWaveformFromRaw();
+  void AppendHeartRatePoint(uint8_t hr);
+  /** 每 1s 调用一次,返回 true 表示 30s 倒计时已结束 */
+  bool OnCountdownSecondTick();
   void StopMeasurement();
 
+  static constexpr int16_t kMeasureDurationSec = 30;
+
 private:
   void InitChart();
-  void UpdateWaveform();
+  void RefreshChart();
   void UpdateCountdownLabel();
+  int16_t MapHeartRateToChartY(uint8_t hr) const;
 
   UIViewGroup *group_{nullptr};
   UILabel *titleLabel_{nullptr};
@@ -30,8 +35,7 @@ private:
   UILabel *countdownLabel_{nullptr};
   UILabel *hintLabel_{nullptr};
   Point wavePoints_[48]{};
-  float wavePhase_{0.0f};
-  int16_t countdownSec_{30};
+  int16_t countdownSec_{kMeasureDurationSec};
   static constexpr int16_t kWavePointCount = 48;
 };
 
index 3364e8c..a8b9af3 100644 (file)
@@ -8,6 +8,9 @@
 
 namespace OHOS {
 
+/** 与 ppg_hal afib_result 一致,0xFF 表示测量未完成 */
+static constexpr uint8_t ECG_AFIB_RESULT_INVALID = 0xFF;
+
 enum class ECGResultType : uint8_t {
   SINUS_RHYTHM = 0,
   ATRIAL_FIBRILLATION = 1,
@@ -17,7 +20,7 @@ enum class ECGResultType : uint8_t {
 struct ECGRecord {
   uint32_t timestamp;
   uint8_t bpm;
-  ECGResultType result;
+  uint8_t afibResult;
 };
 
 class ECGModel {
@@ -34,18 +37,26 @@ public:
   const std::deque<ECGRecord> &GetHistoryRecords() const;
   std::string FormatShortTimestamp(uint32_t timestamp) const;
   std::string GetResultText(ECGResultType type) const;
+  std::string GetAfibResultText(uint8_t afibResult) const;
   ColorType GetResultColor(ECGResultType type) const;
+  ColorType GetAfibResultColor(uint8_t afibResult) const;
+  ECGResultType MapAfibToResultType(uint8_t afibResult) const;
 
-  void SetMeasureResult(uint8_t bpm, ECGResultType result);
+  void SetMeasureResult(uint8_t bpm, uint8_t afibResult);
   uint8_t GetResultBpm() const;
+  uint8_t GetAfibResult() const;
   ECGResultType GetResultType() const;
-  void AddMeasureToHistory();
+  bool SaveMeasureRecord(uint8_t bpm, uint8_t afibResult);
 
 private:
-  ECGRecord lastRecord_{0, 0, ECGResultType::INCONCLUSIVE};
+  bool CreateECGDataDirectory();
+  bool LoadHistoryFromStorage();
+  bool SaveHistoryToStorage();
+
+  ECGRecord lastRecord_{0, 0, ECG_AFIB_RESULT_INVALID};
   std::deque<ECGRecord> historyRecords_;
   uint8_t resultBpm_{0};
-  ECGResultType resultType_{ECGResultType::SINUS_RHYTHM};
+  uint8_t resultAfib_{ECG_AFIB_RESULT_INVALID};
 };
 
 } // namespace OHOS
index 6692403..7d271bc 100644 (file)
@@ -174,9 +174,9 @@ void ECGHistoryView::InitRecentPage() {
 
   InitImageView(recentHeart_, 129, 182+35+12+8, 54, 54, "ecg_recent_heart", ECG_PATH_HEART_RED_ICON);
 
-  InitUILabel(recentBpm_, 129+54+12, 182+35+12, 66, 66, "ecg_recent_bpm", "--",
+  InitUILabel(recentBpm_, 129+54+8, 182+35+12, 74, 66, "ecg_recent_bpm", "--",
               DEFAULT_VECTOR_FONT_FILENAME, 65);
-  recentBpm_->SetAlign(TEXT_ALIGNMENT_CENTER, TEXT_ALIGNMENT_CENTER);
+  recentBpm_->SetAlign(TEXT_ALIGNMENT_RIGHT, TEXT_ALIGNMENT_CENTER);
 
   InitUILabel(recentBpmUnit_, 129+54+12+66+8, 258, 72, 30, "ecg_recent_bpm_unit", "次/分",
               DEFAULT_VECTOR_FONT_FILENAME, 30);
@@ -235,8 +235,8 @@ void ECGHistoryView::UpdateRecentPage() {
   }
   ECGRecord last = model->GetLastRecord();
   recentTime_->SetText(model->FormatShortTimestamp(last.timestamp).c_str());
-  recentResult_->SetText(model->GetResultText(last.result).c_str());
-  recentResult_->SetTextColor(model->GetResultColor(last.result));
+  recentResult_->SetText(model->GetAfibResultText(last.afibResult).c_str());
+  recentResult_->SetTextColor(model->GetAfibResultColor(last.afibResult));
   if (last.bpm > 0) {
     recentBpm_->SetText(std::to_string(last.bpm).c_str());
   } else {
@@ -282,8 +282,8 @@ void ECGHistoryView::RebuildList() {
     InitUILabel(resultLabel, ECG_HISTORY_CARD_MARGIN + 40 + 38 + 8 + 4, y + 36 + 5, 120, 35, "ecg_list_result", "",
                 DEFAULT_VECTOR_FONT_FILENAME, 30);
     resultLabel->SetAlign(TEXT_ALIGNMENT_LEFT, TEXT_ALIGNMENT_CENTER);
-    resultLabel->SetText(model->GetResultText(rec.result).c_str());
-    resultLabel->SetTextColor(model->GetResultColor(rec.result));
+    resultLabel->SetText(model->GetAfibResultText(rec.afibResult).c_str());
+    resultLabel->SetTextColor(model->GetAfibResultColor(rec.afibResult));
 
     UILabel *bpmLabel = nullptr;
     std::string bpmStr = rec.bpm > 0 ? (std::to_string(rec.bpm) + " 次/分") : "-- 次/分";
index 5776729..478a3a9 100644 (file)
 #include <cmsis_os2.h>
 #include <custom_utils.h>
 
+#ifdef SUPPORT_PPG_SERVICE
+#include "ppg_hal.h"
+#include "ppg_service.h"
+#endif
+
 namespace OHOS {
 
 static ECGMainPresenter *g_pECGMainPresenter = nullptr;
@@ -23,21 +28,16 @@ static osTimerId_t g_ecgContactGoodTimer = nullptr;
 REGIST_MENU_PRO(VIEW_ECG, ECGMainView, ECGMainPresenter, PNG_APPLIST_ECG_IMAGE_BIG,
                 PNG_APPLIST_ECG_IMAGE, PNG_APPLIST_ECG_IMAGE, "心电", STR_ECG);
 
+/** 测量定时器回调 */
 static void ECGMeasureTimerCallback(void *arg) {
   UNUSED(arg);
-  ECGMeasuringView *view = ECGMeasuringView::GetInstance();
-  if (view == nullptr) {
-    return;
-  }
-  if (view->OnTimerTick()) {
-    ECGMainPresenter *presenter = ECGMainPresenter::GetInstance();
-    ECGMainView *mainView = ECGMainView::GetInstance();
-    if (presenter != nullptr && mainView != nullptr) {
-      presenter->FinishMeasuringFlow(mainView);
-    }
+  ECGMainPresenter *presenter = ECGMainPresenter::GetInstance();
+  if (presenter != nullptr) {
+    presenter->OnMeasureTimerTick();
   }
 }
 
+/** 完成测量流程定时器回调 */
 static void ECGCompleteTimerCallback(void *arg) {
   UNUSED(arg);
   ECGMainView *mainView = ECGMainView::GetInstance();
@@ -46,6 +46,7 @@ static void ECGCompleteTimerCallback(void *arg) {
   }
 }
 
+/** 接触良好定时器回调 */
 static void ECGContactGoodTimerCallback(void *arg) {
   UNUSED(arg);
   ECGMainPresenter *presenter = ECGMainPresenter::GetInstance();
@@ -78,7 +79,8 @@ void ECGMainPresenter::OnPause(void) {}
 void ECGMainPresenter::OnResume(void) {
   RootView::GetInstance()->SetOnKeyActListener(this);
   ECGMainView *mainView = ECGMainView::GetInstance();
-  if (mainView != nullptr && mainView->GetCurrentViewId() == ECG_MEASURING_VIEW) {
+  if (mainView != nullptr && mainView->GetCurrentViewId() == ECG_MEASURING_VIEW &&
+      !measureDataReceived_) {
     SetScreenAlwaysOnForEcg(true);
   }
 }
@@ -89,6 +91,7 @@ void ECGMainPresenter::OnStop(void) {
   StopContactGoodTimer();
 }
 
+/** 停止接触良好定时器 */
 void ECGMainPresenter::StopContactGoodTimer() {
   if (g_ecgContactGoodTimer != nullptr) {
     osTimerStop(g_ecgContactGoodTimer);
@@ -97,11 +100,13 @@ void ECGMainPresenter::StopContactGoodTimer() {
   }
 }
 
+/** 是否等待接触 */
 bool ECGMainPresenter::IsWaitingContact(ECGMainView *mainView) const {
   return mainView != nullptr && mainView->GetCurrentViewId() == ECG_INSTRUCTION_VIEW &&
          mainView->GetInstructionSubType() == ECG_INSTRUCTION_TYPE_WAIT_CONTACT;
 }
 
+/** 显示等待接触 */
 void ECGMainPresenter::ShowWaitContact(ECGMainView *mainView) {
   int16_t hide = mainView->GetCurrentViewId();
   if (hide == ECG_INSTRUCTION_VIEW) {
@@ -111,6 +116,7 @@ void ECGMainPresenter::ShowWaitContact(ECGMainView *mainView) {
   }
 }
 
+/** 显示接触良好 */
 void ECGMainPresenter::ShowContactGood(ECGMainView *mainView) {
   StopContactGoodTimer();
   mainView->ChangeView(ECG_INSTRUCTION_VIEW, ECG_INSTRUCTION_VIEW, ECG_INSTRUCTION_TYPE_CONTACT_GOOD);
@@ -129,6 +135,7 @@ void ECGMainPresenter::OnBottomButtonPressed(ECGMainView *mainView) {
 }
 
 void ECGMainPresenter::NavigateBack() {
+  StopMeasureTimer();
   NativeAbility::GetInstance().Back(TransitionType::TRANSITION_BACK_TO_HEXAGONS);
 }
 
@@ -144,6 +151,9 @@ bool ECGMainPresenter::HandleSwipeBack() {
     return true;
   }
   int16_t current = mainView->GetCurrentViewId();
+  if (current == ECG_MEASURING_VIEW) {
+    StopMeasureTimer();
+  }
   if (current == ECG_ENTRY_VIEW) {
     NavigateBack();
   } else if (current == ECG_HISTORY_VIEW) {
@@ -161,26 +171,51 @@ bool ECGMainPresenter::HandleSwipeBack() {
   return true;
 }
 
+void ECGMainPresenter::StartPpgAfibSession() {
+#ifdef SUPPORT_PPG_SERVICE
+  ppg_afib_start();
+  WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[ECG] ppg afib start");
+#else
+  (void)0;
+#endif
+}
+
+void ECGMainPresenter::StopPpgAfibSession() {
+#ifdef SUPPORT_PPG_SERVICE
+  ppg_afib_stop();
+  WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[ECG] ppg afib stop");
+#else
+  (void)0;
+#endif
+}
+
 void ECGMainPresenter::StartMeasureTimer() {
   StopMeasureTimer();
+  measureDataReceived_ = false;
+  pollElapsedMs_ = 0;
   uapi_set_screen_on_api();
   SetScreenAlwaysOnForEcg(true);
+  StartPpgAfibSession();
   g_ecgMeasureTimer =
       osTimerNew((osTimerFunc_t)ECGMeasureTimerCallback, osTimerPeriodic, nullptr, nullptr);
   if (g_ecgMeasureTimer != nullptr) {
-    osTimerStart(g_ecgMeasureTimer, 200);
+    osTimerStart(g_ecgMeasureTimer, kMeasureTickMs);
   }
 }
 
 void ECGMainPresenter::StopMeasureTimer() {
   SetScreenAlwaysOnForEcg(false);
+  StopPpgAfibSession();
   if (g_ecgMeasureTimer != nullptr) {
     osTimerStop(g_ecgMeasureTimer);
     osTimerDelete(g_ecgMeasureTimer);
     g_ecgMeasureTimer = nullptr;
   }
+  measureDataReceived_ = false;
+  pollElapsedMs_ = 0;
 }
 
+/** 设置屏幕长亮 */
 void ECGMainPresenter::SetScreenAlwaysOnForEcg(bool enable) {
   if (enable) {
     if (!screenAlwaysOnForEcg_) {
@@ -195,15 +230,79 @@ void ECGMainPresenter::SetScreenAlwaysOnForEcg(bool enable) {
   }
 }
 
+bool ECGMainPresenter::PollAfibResult() {
+#ifdef SUPPORT_PPG_SERVICE
+  uint8_t ecgHr = 0;
+  uint8_t afibResult = PPG_ECG_AFIB_RESULT_INVALID;
+  ppg_get_ecg_afib(&ecgHr, &afibResult);
+  WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[ECG] poll hr=%u afib=%u", ecgHr, afibResult);
+
+  ECGMeasuringView *view = ECGMeasuringView::GetInstance();
+  if (view != nullptr && ecgHr > 0) {
+    view->AppendHeartRatePoint(ecgHr);
+  }
+
+  if (afibResult != PPG_ECG_AFIB_RESULT_INVALID) {
+    ECGModel *model = ECGModel::GetInstance();
+    model->SetMeasureResult(ecgHr, afibResult);
+    model->SaveMeasureRecord(ecgHr, afibResult);
+    measureDataReceived_ = true;
+    return true;
+  }
+#else
+  (void)0;
+#endif
+  return false;
+}
+
+/** 测量超时 */
+void ECGMainPresenter::OnMeasureTimeout() {
+  StopMeasureTimer();
+  ECGMainView *mainView = ECGMainView::GetInstance();
+  if (mainView != nullptr && mainView->GetCurrentViewId() == ECG_MEASURING_VIEW) {
+    mainView->ChangeView(ECG_MEASURING_VIEW, ECG_ABNORMAL_VIEW);
+  }
+}
+
+/** 测量定时器回调 */
+void ECGMainPresenter::OnMeasureTimerTick() {
+  if (measureDataReceived_) {
+    return;
+  }
+
+  ECGMainView *mainView = ECGMainView::GetInstance();
+  ECGMeasuringView *view = ECGMeasuringView::GetInstance();
+  if (mainView == nullptr || view == nullptr ||
+      mainView->GetCurrentViewId() != ECG_MEASURING_VIEW) {
+    return;
+  }
+
+  view->UpdateWaveformFromRaw();
+
+  pollElapsedMs_ += kMeasureTickMs;
+  if (pollElapsedMs_ >= kPollIntervalMs) {
+    pollElapsedMs_ -= kPollIntervalMs;
+    if (PollAfibResult()) {
+      FinishMeasuringFlow(mainView);
+      return;
+    }
+    if (view->OnCountdownSecondTick()) {
+      WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[ECG] measure timeout without afib result");
+      OnMeasureTimeout();
+      return;
+    }
+  }
+}
+
 void ECGMainPresenter::StartMeasureFlow(ECGMainView *mainView) {
   ShowWaitContact(mainView);
 }
 
 void ECGMainPresenter::FinishMeasuringFlow(ECGMainView *mainView) {
+  if (measureDataReceived_ == false) {
+    return;
+  }
   StopMeasureTimer();
-  ECGModel *model = ECGModel::GetInstance();
-  model->SetMeasureResult(76, ECGResultType::SINUS_RHYTHM);
-  model->AddMeasureToHistory();
   mainView->ChangeView(ECG_MEASURING_VIEW, ECG_MEASURE_COMPLETE_VIEW);
   osTimerId_t completeTimer =
       osTimerNew((osTimerFunc_t)ECGCompleteTimerCallback, osTimerOnce, nullptr, nullptr);
@@ -283,12 +382,15 @@ bool ECGMainPresenter::OnClick(UIView &view, const ClickEvent &event) {
     return true;
   }
   if (viewId == ECG_CLICK_EXIT) {
+    if (current == ECG_MEASURING_VIEW) {
+      StopMeasureTimer();
+    }
     mainView->ChangeView(current, ECG_ENTRY_VIEW);
     return true;
   }
   if (viewId == ECG_CLICK_RETRY) {
     if (current == ECG_ABNORMAL_VIEW) {
-      ShowWaitContact(mainView);
+      StartMeasureFlow(mainView);
     }
     return true;
   }
@@ -323,7 +425,8 @@ bool ECGMainPresenter::OnKeyAct(UIView &view, const KeyEvent &event) {
   unsigned short keyState = event.GetState();
   ECGMainView *mainView = ECGMainView::GetInstance();
 
-  if (keyID == static_cast<uint16_t>(ZliteKeyCode::ZLITE_KEY_FUNC) && keyState == 1 && mainView != nullptr) {
+  if (keyID == static_cast<uint16_t>(ZliteKeyCode::ZLITE_KEY_POWER) && keyState == 1 &&
+      mainView != nullptr && mainView->GetCurrentViewId() == ECG_INSTRUCTION_VIEW) {
     OnBottomButtonPressed(mainView);
     return true;
   }
index db6c9fd..baf1c28 100644 (file)
@@ -52,19 +52,19 @@ ECGMeasureResultView *ECGMeasureResultView::GetInstance() { return g_instance; }
 
 void ECGMeasureResultView::UpdateText() {
   ECGModel *model = ECGModel::GetInstance();
-  ECGResultType type = model->GetResultType();
+  uint8_t afibResult = model->GetAfibResult();
   uint8_t bpm = model->GetResultBpm();
 
   if (bpmValueLabel_ != nullptr) {
-    if (type == ECGResultType::INCONCLUSIVE || bpm == 0) {
+    if (bpm == 0) {
       bpmValueLabel_->SetText("--");
     } else {
       bpmValueLabel_->SetText(std::to_string(bpm).c_str());
     }
   }
   if (resultLabel_ != nullptr) {
-    resultLabel_->SetText(model->GetResultText(type).c_str());
-    resultLabel_->SetTextColor(model->GetResultColor(type));
+    resultLabel_->SetText(model->GetAfibResultText(afibResult).c_str());
+    resultLabel_->SetTextColor(model->GetAfibResultColor(afibResult));
   }
 }
 
index 6b533c6..84748ca 100644 (file)
@@ -3,10 +3,9 @@
 #include "ecg/include/ECGViewClickIds.h"
 #include "uicomponents/UIComponents.h"
 #include "wearable_log.h"
-#include <cmath>
 
-#ifndef sinf
-#define sinf(x) static_cast<float>(sin(x))
+#ifdef SUPPORT_PPG_SERVICE
+#include "ppg_hal.h"
 #endif
 
 namespace OHOS {
@@ -69,29 +68,86 @@ void ECGMeasuringView::InitChart() {
   }
 }
 
-void ECGMeasuringView::UpdateWaveform() {
+int16_t ECGMeasuringView::MapHeartRateToChartY(uint8_t hr) const {
+  if (hr < 40) {
+    hr = 40;
+  } else if (hr > 180) {
+    hr = 180;
+  }
+  int16_t y = static_cast<int16_t>(20 + (static_cast<int>(hr) - 40) * 180 / 140);
+  if (y < 20) {
+    y = 20;
+  } else if (y > 200) {
+    y = 200;
+  }
+  return y;
+}
+
+void ECGMeasuringView::RefreshChart() {
   if (dataSerial_ == nullptr || chart_ == nullptr) {
     return;
   }
-  wavePhase_ += 0.35f;
-  for (int i = 0; i < kWavePointCount; i++) {
-    float t = wavePhase_ + static_cast<float>(i) * 0.28f;
-    wavePoints_[i].x = static_cast<int16_t>(i);
-    int16_t base = 110;
-    int16_t v = base + static_cast<int16_t>(35.0f * sinf(t)) +
-                static_cast<int16_t>(18.0f * sinf(t * 4.1f));
-    if (v < 20) {
-      v = 20;
-    } else if (v > 200) {
-      v = 200;
-    }
-    wavePoints_[i].y = v;
-  }
   dataSerial_->ClearData();
   dataSerial_->AddPoints(wavePoints_, kWavePointCount);
   chart_->Invalidate();
 }
 
+void ECGMeasuringView::UpdateWaveformFromRaw() {
+#ifdef SUPPORT_PPG_SERVICE
+  int32_t rawBuf[128] = {0};
+  uint32_t count = ppg_read_ecg_raw(rawBuf, 128);
+  if (count < static_cast<uint32_t>(kWavePointCount)) {
+    return;
+  }
+
+  uint32_t start = count - static_cast<uint32_t>(kWavePointCount);
+  int32_t minVal = rawBuf[start];
+  int32_t maxVal = rawBuf[start];
+  for (int i = 0; i < kWavePointCount; ++i) {
+    int32_t v = rawBuf[start + static_cast<uint32_t>(i)];
+    if (v < minVal) {
+      minVal = v;
+    }
+    if (v > maxVal) {
+      maxVal = v;
+    }
+  }
+  int32_t range = maxVal - minVal;
+  if (range <= 0) {
+    range = 1;
+  }
+
+  for (int i = 0; i < kWavePointCount; ++i) {
+    wavePoints_[i].x = static_cast<int16_t>(i);
+    int32_t v = rawBuf[start + static_cast<uint32_t>(i)];
+    int16_t y = static_cast<int16_t>(20 + (v - minVal) * 180 / range);
+    if (y < 20) {
+      y = 20;
+    } else if (y > 200) {
+      y = 200;
+    }
+    wavePoints_[i].y = y;
+  }
+  RefreshChart();
+#else
+  (void)0;
+#endif
+}
+
+void ECGMeasuringView::AppendHeartRatePoint(uint8_t hr) {
+  if (hr == 0) {
+    return;
+  }
+  int16_t y = MapHeartRateToChartY(hr);
+  for (int i = 0; i < kWavePointCount - 1; ++i) {
+    wavePoints_[i].x = static_cast<int16_t>(i);
+    wavePoints_[i].y = wavePoints_[i + 1].y;
+  }
+  wavePoints_[kWavePointCount - 1].x = static_cast<int16_t>(kWavePointCount - 1);
+  wavePoints_[kWavePointCount - 1].y = y;
+  RefreshChart();
+}
+
 void ECGMeasuringView::UpdateCountdownLabel() {
   if (countdownLabel_ == nullptr) {
     return;
@@ -102,14 +158,16 @@ void ECGMeasuringView::UpdateCountdownLabel() {
 }
 
 void ECGMeasuringView::ResetMeasurement() {
-  countdownSec_ = 30;
-  wavePhase_ = 0.0f;
+  countdownSec_ = kMeasureDurationSec;
+  for (int i = 0; i < kWavePointCount; ++i) {
+    wavePoints_[i].x = static_cast<int16_t>(i);
+    wavePoints_[i].y = 110;
+  }
   UpdateCountdownLabel();
-  UpdateWaveform();
+  RefreshChart();
 }
 
-bool ECGMeasuringView::OnTimerTick() {
-  UpdateWaveform();
+bool ECGMeasuringView::OnCountdownSecondTick() {
   if (countdownSec_ > 0) {
     countdownSec_--;
     UpdateCountdownLabel();
index d595a5e..1ec3565 100644 (file)
@@ -1,13 +1,27 @@
 #include "ecg/include/ECGModel.h"
+#include "custom_utils.h"
 #include "kv_store.h"
 #include "securec.h"
 #include "wearable_log.h"
+#include <cstdio>
 #include <ctime>
+#include <sys/stat.h>
+#include <unistd.h>
 
 namespace OHOS {
 
 namespace {
 static constexpr const char *ECG_KV_DISCLAIMER = "ecg.disclaimer";
+static constexpr const char *ECG_DATA_DIR = "/user/userdata/health/ecg";
+static constexpr const char *ECG_HISTORY_FILE = "/user/userdata/health/ecg/ECGHistory.bin";
+static constexpr uint32_t ECG_HISTORY_MAGIC = 0x45434701U;
+static constexpr uint32_t ECG_HISTORY_MAX_COUNT = 50U;
+
+struct ECGHistoryStorage {
+  uint32_t magic;
+  uint32_t count;
+  ECGRecord records[50];
+};
 } // namespace
 
 ECGModel::ECGModel() { LoadData(); }
@@ -36,18 +50,75 @@ bool ECGModel::MarkDisclaimerAccepted() {
   return true;
 }
 
+bool ECGModel::CreateECGDataDirectory() {
+  if (access(ECG_DATA_DIR, F_OK) == 0) {
+    return true;
+  }
+  if (mkdir(ECG_DATA_DIR, S_IRWXU) != 0) {
+    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ECGModel CreateECGDataDirectory mkdir fail");
+    return false;
+  }
+  return true;
+}
+
+bool ECGModel::LoadHistoryFromStorage() {
+  historyRecords_.clear();
+  lastRecord_ = {0, 0, ECG_AFIB_RESULT_INVALID};
+
+  FILE *fp = fopen(ECG_HISTORY_FILE, "rb");
+  if (fp == nullptr) {
+    return false;
+  }
+
+  ECGHistoryStorage storage = {0};
+  size_t readSize = fread(&storage, sizeof(ECGHistoryStorage), 1, fp);
+  fclose(fp);
+
+  if (readSize != 1 || storage.magic != ECG_HISTORY_MAGIC || storage.count == 0 ||
+      storage.count > ECG_HISTORY_MAX_COUNT) {
+    return false;
+  }
+
+  for (uint32_t i = 0; i < storage.count; ++i) {
+    historyRecords_.push_back(storage.records[i]);
+  }
+  if (!historyRecords_.empty()) {
+    lastRecord_ = historyRecords_.front();
+  }
+  return true;
+}
+
+bool ECGModel::SaveHistoryToStorage() {
+  if (!CreateECGDataDirectory()) {
+    return false;
+  }
+
+  ECGHistoryStorage storage = {0};
+  storage.magic = ECG_HISTORY_MAGIC;
+  storage.count = static_cast<uint32_t>(historyRecords_.size());
+  if (storage.count > ECG_HISTORY_MAX_COUNT) {
+    storage.count = ECG_HISTORY_MAX_COUNT;
+  }
+  for (uint32_t i = 0; i < storage.count; ++i) {
+    storage.records[i] = historyRecords_[i];
+  }
+
+  FILE *fp = fopen(ECG_HISTORY_FILE, "wb");
+  if (fp == nullptr) {
+    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ECGModel SaveHistory fopen fail");
+    return false;
+  }
+  size_t writeSize = fwrite(&storage, sizeof(ECGHistoryStorage), 1, fp);
+  fclose(fp);
+  return writeSize == 1;
+}
+
 void ECGModel::LoadData() {
+  if (LoadHistoryFromStorage()) {
+    return;
+  }
   historyRecords_.clear();
-  time_t now = time(nullptr);
-  uint32_t ts = now > 0 ? static_cast<uint32_t>(now) : 0;
-
-  ECGRecord r1{ts - 3600, 76, ECGResultType::SINUS_RHYTHM};
-  ECGRecord r2{ts - 86400, 82, ECGResultType::ATRIAL_FIBRILLATION};
-  ECGRecord r3{ts - 172800, 0, ECGResultType::INCONCLUSIVE};
-  historyRecords_.push_back(r1);
-  historyRecords_.push_back(r2);
-  historyRecords_.push_back(r3);
-  lastRecord_ = r1;
+  lastRecord_ = {0, 0, ECG_AFIB_RESULT_INVALID};
 }
 
 ECGRecord ECGModel::GetLastRecord() const { return lastRecord_; }
@@ -61,24 +132,58 @@ std::string ECGModel::FormatShortTimestamp(uint32_t timestamp) const {
     return "--";
   }
   time_t t = static_cast<time_t>(timestamp);
-  tm *timeInfo = localtime(&t);
-  if (timeInfo == nullptr) {
+  tm timeInfo = {0};
+  if (localtime_r(&t, &timeInfo) == nullptr) {
     return "--";
   }
   char buf[32] = {0};
-  if (sprintf_s(buf, sizeof(buf), "%02d-%02d %02d:%02d", timeInfo->tm_mon + 1,
-                timeInfo->tm_mday, timeInfo->tm_hour, timeInfo->tm_min) < 0) {
+  if (sprintf_s(buf, sizeof(buf), "%02d-%02d %02d:%02d", timeInfo.tm_mon + 1,
+                timeInfo.tm_mday, timeInfo.tm_hour, timeInfo.tm_min) < 0) {
     return "--";
   }
   return std::string(buf);
 }
 
+ECGResultType ECGModel::MapAfibToResultType(uint8_t afibResult) const {
+  switch (afibResult) {
+    case 0:
+      return ECGResultType::SINUS_RHYTHM;
+    case 2:
+      return ECGResultType::ATRIAL_FIBRILLATION;
+    default:
+      return ECGResultType::INCONCLUSIVE;
+  }
+}
+
+std::string ECGModel::GetAfibResultText(uint8_t afibResult) const {
+  switch (afibResult) {
+    case 0:
+      return "窦性心律";
+    case 1:
+      return "不确定(不规则)";
+    case 2:
+      return "可能房颤";
+    case 3:
+      return "不确定(信号/特征)";
+    case 4:
+      return "心率过低";
+    case 5:
+      return "心率过高";
+    default:
+      return "不确定";
+  }
+}
+
+ColorType ECGModel::GetAfibResultColor(uint8_t afibResult) const {
+  return GetResultColor(MapAfibToResultType(afibResult));
+}
+
 std::string ECGModel::GetResultText(ECGResultType type) const {
   switch (type) {
     case ECGResultType::SINUS_RHYTHM:
       return "窦性心律";
     case ECGResultType::ATRIAL_FIBRILLATION:
-      return "房颤";
+      return "可能房颤";
     case ECGResultType::INCONCLUSIVE:
     default:
       return "不确定";
@@ -102,24 +207,40 @@ ColorType ECGModel::GetResultColor(ECGResultType type) const {
   return color;
 }
 
-void ECGModel::SetMeasureResult(uint8_t bpm, ECGResultType result) {
+void ECGModel::SetMeasureResult(uint8_t bpm, uint8_t afibResult) {
   resultBpm_ = bpm;
-  resultType_ = result;
+  resultAfib_ = afibResult;
 }
 
 uint8_t ECGModel::GetResultBpm() const { return resultBpm_; }
 
-ECGResultType ECGModel::GetResultType() const { return resultType_; }
+uint8_t ECGModel::GetAfibResult() const { return resultAfib_; }
 
-void ECGModel::AddMeasureToHistory() {
-  time_t now = time(nullptr);
-  uint32_t ts = now > 0 ? static_cast<uint32_t>(now) : 0;
-  ECGRecord record{ts, resultBpm_, resultType_};
+ECGResultType ECGModel::GetResultType() const { return MapAfibToResultType(resultAfib_); }
+
+bool ECGModel::SaveMeasureRecord(uint8_t bpm, uint8_t afibResult) {
+  uint32_t now = 0;
+  if (uapi_get_timestampe_ss(&now) != ERRCODE_SUCC) {
+    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ECGModel SaveMeasureRecord get timestamp fail");
+    return false;
+  }
+
+  ECGRecord record = {now, bpm, afibResult};
   historyRecords_.push_front(record);
-  if (historyRecords_.size() > 10) {
+  while (historyRecords_.size() > ECG_HISTORY_MAX_COUNT) {
     historyRecords_.pop_back();
   }
   lastRecord_ = record;
+  resultBpm_ = bpm;
+  resultAfib_ = afibResult;
+
+  if (!SaveHistoryToStorage()) {
+    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ECGModel SaveMeasureRecord storage fail");
+    return false;
+  }
+  WEARABLE_LOGD(WEARABLE_LOG_MODULE_APP, "ECGModel saved bpm=%u afib=%u ts=%u", bpm, afibResult,
+                now);
+  return true;
 }
 
 } // namespace OHOS