From 8aed3070dedba25d85e75841099fb33eaf8f6e1a Mon Sep 17 00:00:00 2001 From: xue <2144439972@qq.com> Date: Thu, 11 Jun 2026 14:29:34 +0800 Subject: [PATCH] =?utf8?q?=E4=BC=98=E5=8C=96ECG=E6=95=B0=E6=8D=AE=E5=AD=98?= =?utf8?q?=E5=82=A8=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- .../nativeui/ecg/include/ECGMainPresenter.h | 12 +- .../nativeui/ecg/include/ECGMeasuringView.h | 14 +- .../nativeapp/nativeui/ecg/include/ECGModel.h | 21 ++- .../nativeui/ecg/src/ECGHistoryView.cpp | 12 +- .../nativeui/ecg/src/ECGMainPresenter.cpp | 137 ++++++++++++-- .../nativeui/ecg/src/ECGMeasureResultView.cpp | 8 +- .../nativeui/ecg/src/ECGMeasuringView.cpp | 104 ++++++++--- .../nativeapp/nativeui/ecg/src/ECGModel.cpp | 167 +++++++++++++++--- 8 files changed, 391 insertions(+), 84 deletions(-) diff --git a/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/include/ECGMainPresenter.h b/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/include/ECGMainPresenter.h index bb4d1521c..7c193ffda 100644 --- a/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/include/ECGMainPresenter.h +++ b/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/include/ECGMainPresenter.h @@ -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 diff --git a/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/include/ECGMeasuringView.h b/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/include/ECGMeasuringView.h index 339cf41ef..934e06888 100644 --- a/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/include/ECGMeasuringView.h +++ b/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/include/ECGMeasuringView.h @@ -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; }; diff --git a/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/include/ECGModel.h b/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/include/ECGModel.h index 3364e8ca6..a8b9af30e 100644 --- a/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/include/ECGModel.h +++ b/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/include/ECGModel.h @@ -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 &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 historyRecords_; uint8_t resultBpm_{0}; - ECGResultType resultType_{ECGResultType::SINUS_RHYTHM}; + uint8_t resultAfib_{ECG_AFIB_RESULT_INVALID}; }; } // namespace OHOS diff --git a/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGHistoryView.cpp b/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGHistoryView.cpp index 6692403fa..7d271bc4d 100644 --- a/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGHistoryView.cpp +++ b/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGHistoryView.cpp @@ -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) + " 次/分") : "-- 次/分"; diff --git a/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGMainPresenter.cpp b/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGMainPresenter.cpp index 57767290b..478a3a9ba 100644 --- a/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGMainPresenter.cpp +++ b/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGMainPresenter.cpp @@ -14,6 +14,11 @@ #include #include +#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(ZliteKeyCode::ZLITE_KEY_FUNC) && keyState == 1 && mainView != nullptr) { + if (keyID == static_cast(ZliteKeyCode::ZLITE_KEY_POWER) && keyState == 1 && + mainView != nullptr && mainView->GetCurrentViewId() == ECG_INSTRUCTION_VIEW) { OnBottomButtonPressed(mainView); return true; } diff --git a/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGMeasureResultView.cpp b/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGMeasureResultView.cpp index db6c9fdf1..baf1c289c 100644 --- a/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGMeasureResultView.cpp +++ b/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGMeasureResultView.cpp @@ -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)); } } diff --git a/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGMeasuringView.cpp b/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGMeasuringView.cpp index 6b533c62d..84748ca88 100644 --- a/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGMeasuringView.cpp +++ b/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGMeasuringView.cpp @@ -3,10 +3,9 @@ #include "ecg/include/ECGViewClickIds.h" #include "uicomponents/UIComponents.h" #include "wearable_log.h" -#include -#ifndef sinf -#define sinf(x) static_cast(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(20 + (static_cast(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(i) * 0.28f; - wavePoints_[i].x = static_cast(i); - int16_t base = 110; - int16_t v = base + static_cast(35.0f * sinf(t)) + - static_cast(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(kWavePointCount)) { + return; + } + + uint32_t start = count - static_cast(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(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(i); + int32_t v = rawBuf[start + static_cast(i)]; + int16_t y = static_cast(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(i); + wavePoints_[i].y = wavePoints_[i + 1].y; + } + wavePoints_[kWavePointCount - 1].x = static_cast(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(i); + wavePoints_[i].y = 110; + } UpdateCountdownLabel(); - UpdateWaveform(); + RefreshChart(); } -bool ECGMeasuringView::OnTimerTick() { - UpdateWaveform(); +bool ECGMeasuringView::OnCountdownSecondTick() { if (countdownSec_ > 0) { countdownSec_--; UpdateCountdownLabel(); diff --git a/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGModel.cpp b/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGModel.cpp index d595a5ee0..1ec356548 100644 --- a/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGModel.cpp +++ b/code/sdk/application/wearable/n9/nativeapp/nativeui/ecg/src/ECGModel.cpp @@ -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 #include +#include +#include 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(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(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(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(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 -- 2.17.1