diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 594a2c5..fc47abc 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -76,7 +76,7 @@ body: label: Issue checklist description: Please double-check that you have done each of the following things before submitting the issue. options: - - label: I searched for previous reports in [the issue tracker](https://github.com/m5stack/M5Stack/issues?q=) + - label: I searched for previous reports in [the issue tracker](https://github.com/m5stack/M5StopWatch-UserDemo/issues?q=) required: true - label: My report contains all necessary details required: true diff --git a/.github/workflows/Arduino-Lint-Check.yml b/.github/workflows/Arduino-Lint-Check.yml deleted file mode 100644 index de06bf0..0000000 --- a/.github/workflows/Arduino-Lint-Check.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Arduino Lint Check -on: - push: - branches: [ master, main ] - pull_request: - branches: [ master, main ] - workflow_dispatch: - -defaults: - run: - shell: bash - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - lint: - name: Lint Check - runs-on: [self-hosted, Linux, X64] - steps: - - uses: actions/checkout@v4 - - uses: arduino/arduino-lint-action@v2 - with: - library-manager: update - compliance: strict - project-type: all diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml index dc9e340..13c1e6a 100644 --- a/.github/workflows/clang-format-check.yml +++ b/.github/workflows/clang-format-check.yml @@ -40,7 +40,7 @@ jobs: matrix: path: - check: './' # path to include - exclude: '' # path to exclude + exclude: '/(assets|hal/drivers)' # path to exclude #- check: 'src' # exclude: '(Fonts)' # Exclude file paths containing "Fonts" #- check: 'examples' @@ -55,7 +55,7 @@ jobs: - name: Run clang-format style check for C/C++/Protobuf programs. uses: jidicula/clang-format-action@v4.10.2 # Using include-regex 10.x or later with: - clang-format-version: '18' + clang-format-version: '22' check-path: ${{ matrix.path['check'] }} exclude-regex: ${{ matrix.path['exclude'] }} include-regex: ${{ env.INCLUDE_REGEX }} diff --git a/.gitignore b/.gitignore index 540f918..6e233f0 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ # Build files build/ cmake-build-*/ +/components/ managed_components/ # PlatformIO @@ -59,3 +60,6 @@ bin/ obj/ *.code-workspace + +sdkconfig +sdkconfig.old diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ff7bc65 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(StopWatch-UserDemo) diff --git a/README.md b/README.md index 2be4654..61dae9b 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,26 @@ -# Product Name +# M5StopWatch-UserDemo +M5Stack StopWatch user demo for hardware evaluation. -## Overview +## Build -### SKU:xxx +### Fetch Dependencies -Description of the product +```bash +python3 ./fetch_repos.py +``` -## Related Link +### Tool Chains -- [Document & Datasheet](https://docs.m5stack.com/en/unit/product_Link) +[ESP-IDF v5.5.4](https://docs.espressif.com/projects/esp-idf/en/v5.5.4/esp32s3/index.html) -## Required Libraries: +### Build -- [Adafruit_BMP280_Library](https://github.com/adafruit/Required_Libraries_Link) +```bash +idf.py build +``` -## License +### Flash -- [Product Name- MIT](LICENSE) - -## Remaining steps(Editorial Staff Look,After following the steps, remember to delete all the content below) - -1. Change [clang format check path](./.github/workflows/clang-format-check.yml#L42-L47). -2. Add License content to [LICENSE](/LICENSE). -3. Change link on line 78 of [bug-report.yml](./.github/ISSUE_TEMPLATE/bug-report.yml#L79). - -```cpp -Example -# M5Unit-ENV - -## Overview - -### SKU:U001 & U001-B & U001-C - -Contains M5Stack-**UNIT ENV** series related case programs.ENV is an environmental sensor with integrated SHT30 and QMP6988 internally to detect temperature, humidity, and atmospheric pressure data. - -## Related Link - -- [Document & Datasheet](https://docs.m5stack.com/en/unit/envIII) - -## Required Libraries: - -- [Adafruit_BMP280_Library](https://github.com/adafruit/Adafruit_BMP280_Library) - -## License - -- [M5Unit-ENV - MIT](LICENSE) -``` \ No newline at end of file +```bash +idf.py flash +``` diff --git a/dependencies.lock b/dependencies.lock new file mode 100644 index 0000000..f023865 --- /dev/null +++ b/dependencies.lock @@ -0,0 +1,58 @@ +dependencies: + espressif/cmake_utilities: + component_hash: 351350613ceafba240b761b4ea991e0f231ac7a9f59a9ee901f751bddc0bb18f + dependencies: + - name: idf + require: private + version: '>=4.1' + source: + registry_url: https://components.espressif.com/ + type: service + version: 0.5.3 + espressif/esp-dsp: + component_hash: 12e44db246517a627bc0fe2b255b8335410c0215c98bfba2df5a8e3edea839ef + dependencies: + - name: idf + require: private + version: '>=4.2' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.8.0 + espressif/esp_codec_dev: + component_hash: 0d9e9bc288156eb55f79338d312e1ebf8c9dfbd5e7d13ef0f20ccb031b4e15cf + dependencies: + - name: idf + require: private + version: '>=4.0' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.5.4 + espressif/i2c_bus: + component_hash: 4e990dc11734316186b489b362c61d41f23f79d58bc169795cec215e528cba14 + dependencies: + - name: espressif/cmake_utilities + registry_url: https://components.espressif.com + require: private + version: '*' + - name: idf + require: private + version: '>=4.0' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.5.0 + idf: + source: + type: idf + version: 5.5.4 +direct_dependencies: +- espressif/cmake_utilities +- espressif/esp-dsp +- espressif/esp_codec_dev +- espressif/i2c_bus +- idf +manifest_hash: 15747d3f6d40317f72fb137333bc3df1641a97ff034c88e9444fca5348997c95 +target: esp32s3 +version: 2.0.0 diff --git a/fetch_repos.py b/fetch_repos.py new file mode 100644 index 0000000..17cc96f --- /dev/null +++ b/fetch_repos.py @@ -0,0 +1,61 @@ +import os +import subprocess +import json + + +def clone_or_update_repo( + repo_url, path, ref=None, with_submodules=False, patch_path=None +): + import os + + if not os.path.exists(path): + subprocess.run(["git", "clone", repo_url, path], check=True) + else: + subprocess.run(["git", "-C", path, "fetch"], check=True) + + if ref: + subprocess.run(["git", "-C", path, "checkout", ref], check=True) + + if with_submodules: + subprocess.run( + ["git", "-C", path, "submodule", "update", "--init", "--recursive"], + check=True, + ) + + # 应用 patch + if patch_path: + patch_full_path = ( + patch_path + if os.path.isabs(patch_path) + else os.path.join(os.getcwd(), patch_path) + ) + # 使用 git apply --check 先检测补丁是否能应用,避免报错 + check_result = subprocess.run( + ["git", "-C", path, "apply", "--check", patch_full_path] + ) + if check_result.returncode == 0: + subprocess.run(["git", "-C", path, "apply", patch_full_path], check=True) + print(f"Applied patch {patch_path} to {path}") + else: + print(f"Patch {patch_path} cannot be applied cleanly to {path}, skipped.") + + +def fetch_dependencies(): + script_dir = os.path.dirname(os.path.abspath(__file__)) + config_path = os.path.join(script_dir, "repos.json") + + with open(config_path) as f: + repos = json.load(f) + + for repo in repos: + repo_path = os.path.join(script_dir, repo["path"]) + branch = repo.get("branch") + with_submodules = repo.get("with_submodules", False) + patch = repo.get("patch") + if patch and not os.path.isabs(patch): + patch = os.path.join(script_dir, patch) + clone_or_update_repo(repo["url"], repo_path, branch, with_submodules, patch) + + +if __name__ == "__main__": + fetch_dependencies() \ No newline at end of file diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..12f461c --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,27 @@ +file(GLOB_RECURSE MY_SRCS + "apps/*.c" + "apps/*.cc" + "apps/*.cpp" + "assets/*.c" + "assets/*.cc" + "assets/*.cpp" + "hal/*.c" + "hal/*.cc" + "hal/*.cpp" +) + +set(MY_INCS + "." +) + +idf_component_register( + SRCS + "main.cpp" + ${MY_SRCS} + INCLUDE_DIRS + ${MY_INCS} + EMBED_FILES + "assets/sfx/boot_sfx.bin" + EMBED_TXTFILES + "hal/utils/config_ap/assets/badge_config_ap.html" +) diff --git a/main/apps/app_alarm_clock/app_alarm_clock.cpp b/main/apps/app_alarm_clock/app_alarm_clock.cpp new file mode 100644 index 0000000..a80acf8 --- /dev/null +++ b/main/apps/app_alarm_clock/app_alarm_clock.cpp @@ -0,0 +1,128 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +#include "app_alarm_clock.h" +#include +#include +#include +#include +#include + +using namespace mooncake; + +AppAlarmClock::AppAlarmClock() +{ + setAppInfo().name = "AlarmClock"; + setAppInfo().icon = (void*)&icon_clock; +} + +void AppAlarmClock::onCreate() +{ + mclog::tagInfo(getAppInfo().name, "on create"); + + _alarm_clock = std::make_unique(); + if (!_alarm_clock->init()) { + mclog::tagError(getAppInfo().name, "failed to init alarm clock model"); + return; + } + + _alarm_clock->onTriggered().connect([this](const model::AlarmClock::AlarmTriggeredEvent& event) { + std::unique_ptr trigger_view; + { + LvglLockGuard lock; + trigger_view = std::make_unique(); + trigger_view->init(event.time); + } + + GetHAL().startAlarm(); + + // Block until confirmed + while (1) { + GetHAL().feedTheDog(); + GetHAL().delay(100); + + LvglLockGuard lock; + if (trigger_view->isConfirmed()) { + trigger_view.reset(); + break; + } + } + + GetHAL().stopAlarm(); + }); +} + +void AppAlarmClock::onOpen() +{ + mclog::tagInfo(getAppInfo().name, "on open"); + + _key_manager = std::make_unique(); + + LvglLockGuard lock; + + if (_alarm_clock) { + _list_view = std::make_unique(*_alarm_clock); + _list_view->init(lv_screen_active()); + } +} + +void AppAlarmClock::onRunning() +{ + if (_key_manager && _key_manager->update() == input::KeyEvent::GoHome) { + close(); + return; + } + + if (_alarm_clock) { + _alarm_clock->update(); + } + + LvglLockGuard lock; + + if (_list_view) { + _list_view->update(); + + if (_list_view->consumeAddRequested()) { + _list_view.reset(); + _add_view = std::make_unique(); + _add_view->init(lv_screen_active()); + } + } + + if (_add_view && _add_view->isConfirmed() && _alarm_clock) { + auto time = _add_view->selectedTime(); + _alarm_clock->addAlarm(time, true); + + _add_view.reset(); + _list_view = std::make_unique(*_alarm_clock); + _list_view->init(lv_screen_active()); + } +} + +void AppAlarmClock::onSleeping() +{ + if (_alarm_clock) { + _alarm_clock->update(); + } +} + +void AppAlarmClock::onClose() +{ + mclog::tagInfo(getAppInfo().name, "on close"); + + _key_manager.reset(); + + LvglLockGuard lock; + + _list_view.reset(); + _add_view.reset(); +} + +void AppAlarmClock::onDestroy() +{ + mclog::tagInfo(getAppInfo().name, "on destroy"); + + _alarm_clock.reset(); +} diff --git a/main/apps/app_alarm_clock/app_alarm_clock.h b/main/apps/app_alarm_clock/app_alarm_clock.h new file mode 100644 index 0000000..9d86204 --- /dev/null +++ b/main/apps/app_alarm_clock/app_alarm_clock.h @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +#pragma once +#include "model/alarm_clock.h" +#include "view/view.h" +#include +#include +#include + +/** + * @brief Derived App + * + */ +class AppAlarmClock : public mooncake::AppAbility { +public: + AppAlarmClock(); + + // Override lifecycle callbacks + void onCreate() override; + void onOpen() override; + void onRunning() override; + void onSleeping() override; + void onClose() override; + void onDestroy() override; + +private: + std::unique_ptr _key_manager; + std::unique_ptr _alarm_clock; + std::unique_ptr _list_view; + std::unique_ptr _add_view; +}; \ No newline at end of file diff --git a/main/apps/app_alarm_clock/model/alarm_clock.cpp b/main/apps/app_alarm_clock/model/alarm_clock.cpp new file mode 100644 index 0000000..7694f88 --- /dev/null +++ b/main/apps/app_alarm_clock/model/alarm_clock.cpp @@ -0,0 +1,239 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +#include "alarm_clock.h" +#include +#include +#include +#include +#include + +using namespace model; + +static const std::string_view _tag = "AlarmClockModel"; + +namespace { + +std::string format_time(const AlarmClock::Time24& time) +{ + return fmt::format("{:02d}:{:02d}", time.hour, time.minute); +} + +} // namespace + +bool AlarmClock::Time24::isValid() const +{ + return hour < 24 && minute < 60; +} + +AlarmClock::Alarm::Alarm(const Time24& time, bool enabled) : _time(time), _enabled(enabled) +{ +} + +void AlarmClock::Alarm::setTime(const Time24& time) +{ + _time = time; +} + +void AlarmClock::Alarm::setEnabled(bool enabled) +{ + _enabled = enabled; +} + +bool AlarmClock::init() +{ + mclog::tagInfo(_tag, "init"); + + _alarms.clear(); + _last_update_ms = 0; + + AlarmStorageSnapshot snapshot; + if (!GetHAL().loadAlarmStorage(snapshot)) { + mclog::tagError(_tag, "load alarm storage failed"); + return false; + } + + mclog::tagInfo(_tag, "loaded storage snapshot, count: {}", snapshot.count); + + return importStorage(snapshot, false); +} + +void AlarmClock::update() +{ + uint32_t now_ms = GetHAL().millis(); + if (_last_update_ms != 0 && now_ms - _last_update_ms < 1000) { + return; + } + _last_update_ms = now_ms; + + std::time_t now = std::time(nullptr); + std::tm* local_time = std::localtime(&now); + if (local_time == nullptr) { + return; + } + + const int date_key = (local_time->tm_year + 1900) * 10000 + (local_time->tm_mon + 1) * 100 + local_time->tm_mday; + + _alarms.forEach([&](Alarm* alarm, int alarm_id) { + if (alarm == nullptr || !alarm->enabled()) { + return; + } + + if (alarm->time().hour != local_time->tm_hour || alarm->time().minute != local_time->tm_min) { + return; + } + + if (alarm->_last_triggered_date_key == date_key) { + return; + } + + alarm->_last_triggered_date_key = date_key; + AlarmTriggeredEvent event; + event.alarmId = alarm_id; + event.time = alarm->time(); + mclog::tagInfo(_tag, "alarm triggered, id: {}, time: {}", alarm_id, format_time(event.time)); + _on_triggered.emit(event); + }); +} + +int AlarmClock::addAlarm(const Time24& time, bool enabled) +{ + if (!time.isValid() || _alarms.activeCount() >= AlarmStorageSnapshot::maxAlarmCount) { + mclog::tagError(_tag, "add alarm failed, time: {}, enabled: {}, active count: {}", format_time(time), enabled, + _alarms.activeCount()); + return -1; + } + + auto alarm_id = _alarms.create(std::make_unique(time, enabled)); + mclog::tagInfo(_tag, "add alarm, id: {}, time: {}, enabled: {}", alarm_id, format_time(time), enabled); + if (!persist()) { + mclog::tagError(_tag, "persist after add failed, id: {}", alarm_id); + } + + return alarm_id; +} + +bool AlarmClock::removeAlarm(int alarmId) +{ + if (!_alarms.destroy(alarmId)) { + mclog::tagError(_tag, "remove alarm failed, invalid id: {}", alarmId); + return false; + } + + mclog::tagInfo(_tag, "remove alarm, id: {}", alarmId); + + return persist(); +} + +bool AlarmClock::setAlarmEnabled(int alarmId, bool enabled) +{ + auto* alarm = getAlarm(alarmId); + if (alarm == nullptr) { + mclog::tagError(_tag, "set enabled failed, invalid id: {}, enabled: {}", alarmId, enabled); + return false; + } + + alarm->setEnabled(enabled); + if (!enabled) { + alarm->_last_triggered_date_key = -1; + } + mclog::tagInfo(_tag, "set alarm enabled, id: {}, time: {}, enabled: {}", alarmId, format_time(alarm->time()), + enabled); + return persist(); +} + +bool AlarmClock::setAlarmTime(int alarmId, const Time24& time) +{ + if (!time.isValid()) { + mclog::tagError(_tag, "set time failed, invalid time: {}, id: {}", format_time(time), alarmId); + return false; + } + + auto* alarm = getAlarm(alarmId); + if (alarm == nullptr) { + mclog::tagError(_tag, "set time failed, invalid id: {}, time: {}", alarmId, format_time(time)); + return false; + } + + alarm->setTime(time); + alarm->_last_triggered_date_key = -1; + mclog::tagInfo(_tag, "set alarm time, id: {}, time: {}", alarmId, format_time(time)); + return persist(); +} + +AlarmClock::Alarm* AlarmClock::getAlarm(int alarmId) +{ + return _alarms.get(alarmId); +} + +void AlarmClock::forEachAlarm(const std::function& visitor) +{ + _alarms.forEach([&](Alarm* alarm, int alarm_id) { + if (alarm != nullptr) { + visitor(alarm_id, *alarm); + } + }); +} + +AlarmStorageSnapshot AlarmClock::exportStorage() +{ + AlarmStorageSnapshot snapshot; + snapshot.count = 0; + + _alarms.forEach([&](Alarm* alarm, int) { + if (alarm == nullptr || snapshot.count >= AlarmStorageSnapshot::maxAlarmCount) { + return; + } + + auto& out = snapshot.alarms[snapshot.count++]; + out.hour = alarm->time().hour; + out.minute = alarm->time().minute; + out.enabled = alarm->enabled() ? 1 : 0; + out.reserved = 0; + }); + + return snapshot; +} + +bool AlarmClock::importStorage(const AlarmStorageSnapshot& snapshot, bool persistAfterImport) +{ + _alarms.clear(); + + mclog::tagInfo(_tag, "import storage, count: {}, persist: {}", snapshot.count, persistAfterImport); + + const std::size_t import_count = std::min(snapshot.count, AlarmStorageSnapshot::maxAlarmCount); + for (std::size_t i = 0; i < import_count; ++i) { + const auto& entry = snapshot.alarms[i]; + if (!entry.isValid()) { + mclog::tagError(_tag, "skip invalid storage entry, index: {}, hour: {}, minute: {}, enabled: {}", i, + entry.hour, entry.minute, entry.enabled); + continue; + } + + Time24 time = { + .hour = entry.hour, + .minute = entry.minute, + }; + _alarms.create(std::make_unique(time, entry.enabled != 0)); + } + + if (!persistAfterImport) { + return true; + } + + return persist(); +} + +bool AlarmClock::persist() +{ + auto snapshot = exportStorage(); + bool ok = GetHAL().saveAlarmStorage(snapshot); + if (ok) { + mclog::tagInfo(_tag, "persist storage, count: {}", snapshot.count); + } else { + mclog::tagError(_tag, "persist storage failed, count: {}", snapshot.count); + } + return ok; +} diff --git a/main/apps/app_alarm_clock/model/alarm_clock.h b/main/apps/app_alarm_clock/model/alarm_clock.h new file mode 100644 index 0000000..a94a658 --- /dev/null +++ b/main/apps/app_alarm_clock/model/alarm_clock.h @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace model { + +class AlarmClock { +public: + struct Time24 { + uint8_t hour = 0; + uint8_t minute = 0; + + bool isValid() const; + }; + + struct AlarmTriggeredEvent { + int alarmId = -1; + Time24 time; + }; + + class Alarm : public uitk::Poolable { + public: + Alarm(const Time24& time, bool enabled); + + const Time24& time() const + { + return _time; + } + + bool enabled() const + { + return _enabled; + } + + void setTime(const Time24& time); + void setEnabled(bool enabled); + + private: + friend class AlarmClock; + + Time24 _time; + bool _enabled = false; + int _last_triggered_date_key = -1; + }; + + bool init(); + void update(); + + int addAlarm(const Time24& time, bool enabled = true); + bool removeAlarm(int alarmId); + bool setAlarmEnabled(int alarmId, bool enabled); + bool setAlarmTime(int alarmId, const Time24& time); + + Alarm* getAlarm(int alarmId); + void forEachAlarm(const std::function& visitor); + + AlarmStorageSnapshot exportStorage(); + bool importStorage(const AlarmStorageSnapshot& snapshot, bool persistAfterImport = false); + + uitk::Signal& onTriggered() + { + return _on_triggered; + } + +private: + bool persist(); + + uitk::ObjectPool _alarms; + uitk::Signal _on_triggered; + uint32_t _last_update_ms = 0; +}; + +} // namespace model diff --git a/main/apps/app_alarm_clock/view/add_alarm.cpp b/main/apps/app_alarm_clock/view/add_alarm.cpp new file mode 100644 index 0000000..fb4eff7 --- /dev/null +++ b/main/apps/app_alarm_clock/view/add_alarm.cpp @@ -0,0 +1,131 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +#include "view.h" +#include +#include + +using namespace view; +using namespace uitk::lvgl_cpp; + +namespace { + +constexpr int _panel_width = 466; +constexpr int _panel_height = 466; +constexpr int _selector_width = 160; +constexpr int _selector_height = 200; +constexpr int _ok_width = 374; +constexpr int _ok_height = 130; + +constexpr uint32_t _bg_color = 0x000000; +constexpr uint32_t _label_color = 0xFFFFFF; +constexpr uint32_t _selector_color = 0x343434; +constexpr uint32_t _selector_selected_color = 0x696969; +constexpr uint32_t _ok_color = 0x4AD78C; +constexpr uint32_t _ok_text_color = 0x0F5831; + +std::string build_number_options(int begin, int end) +{ + std::string options; + for (int value = begin; value <= end; ++value) { + char buffer[4] = {}; + std::snprintf(buffer, sizeof(buffer), "%02d", value); + if (!options.empty()) { + options.push_back('\n'); + } + options += buffer; + } + return options; +} + +} // namespace + +void AlarmAddView::init(lv_obj_t* parent) +{ + _panel = std::make_unique(parent); + _panel->align(LV_ALIGN_CENTER, 0, 0); + _panel->setSize(_panel_width, _panel_height); + _panel->setRadius(0); + _panel->setBorderWidth(0); + _panel->setPaddingAll(0); + _panel->setBgColor(lv_color_hex(_bg_color)); + _panel->setBgOpa(LV_OPA_COVER); + _panel->removeFlag(LV_OBJ_FLAG_SCROLLABLE); + + _title_label = std::make_unique