From 2691dbb0142f34f9f0b6433a11e7fc478988c890 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sun, 25 May 2025 22:11:50 +0200 Subject: [PATCH 01/16] Implemented LaunchId and FileSelection, updated Notes (#281) - Implemented `LaunchId` to keep track of the apps that are started - Implemented `FileSelection` app to select existing and/or new files. - Moved some re-usable file functionality to `tt::file::` - Renamed `Files` app to `FileBrowser` - Updated `Notes` app to use new `FileSelection` functionality, and cleaned it up a bit. - General code cleanliness improvements --- App/Source/Boards.h | 1 + Boards/Simulator/Source/Simulator.h | 1 - Tactility/Include/Tactility/app/App.h | 12 +- Tactility/Include/Tactility/app/ElfApp.h | 2 +- .../Tactility/app/filebrowser/FileBrowser.h | 7 + Tactility/Include/Tactility/app/files/Files.h | 15 - .../app/fileselection/FileSelection.h | 23 ++ .../Tactility/hal/sdcard/SdCardDevice.h | 2 +- .../Include/Tactility/service/loader/Loader.h | 3 +- Tactility/Private/Tactility/app/AppInstance.h | 22 +- .../app/{files => filebrowser}/State.h | 2 +- .../app/filebrowser/SupportedFiles.h | 11 + .../app/{files => filebrowser}/View.h | 2 +- .../Private/Tactility/app/files/FileUtils.h | 66 ---- .../app/fileselection/FileSelectionPrivate.h | 14 + .../Tactility/app/fileselection/State.h | 59 ++++ .../Tactility/app/fileselection/View.h | 44 +++ Tactility/Source/Tactility.cpp | 6 +- Tactility/Source/app/App.cpp | 8 +- Tactility/Source/app/ElfApp.cpp | 6 +- .../Files.cpp => filebrowser/FileBrowser.cpp} | 16 +- .../app/{files => filebrowser}/State.cpp | 22 +- .../Source/app/filebrowser/SupportedFiles.cpp | 34 ++ .../app/{files => filebrowser}/View.cpp | 52 +-- Tactility/Source/app/files/FileUtils.cpp | 91 ----- .../app/fileselection/FileSelection.cpp | 78 ++++ Tactility/Source/app/fileselection/State.cpp | 118 +++++++ Tactility/Source/app/fileselection/View.cpp | 215 +++++++++++ Tactility/Source/app/log/Log.cpp | 5 +- Tactility/Source/app/notes/Notes.cpp | 333 ++++++------------ .../app/timedatesettings/TimeDateSettings.cpp | 2 +- .../app/wifiapsettings/WifiApSettings.cpp | 2 +- Tactility/Source/lvgl/LabelUtils.cpp | 8 +- Tactility/Source/service/loader/Loader.cpp | 31 +- TactilityC/Include/tt_app_manifest.h | 4 +- TactilityC/Source/tt_app_manifest.cpp | 16 +- TactilityCore/Include/Tactility/file/File.h | 68 ++++ TactilityCore/Source/file/File.cpp | 78 +++- 38 files changed, 971 insertions(+), 508 deletions(-) create mode 100644 Tactility/Include/Tactility/app/filebrowser/FileBrowser.h delete mode 100644 Tactility/Include/Tactility/app/files/Files.h create mode 100644 Tactility/Include/Tactility/app/fileselection/FileSelection.h rename Tactility/Private/Tactility/app/{files => filebrowser}/State.h (97%) create mode 100644 Tactility/Private/Tactility/app/filebrowser/SupportedFiles.h rename Tactility/Private/Tactility/app/{files => filebrowser}/View.h (96%) delete mode 100644 Tactility/Private/Tactility/app/files/FileUtils.h create mode 100644 Tactility/Private/Tactility/app/fileselection/FileSelectionPrivate.h create mode 100644 Tactility/Private/Tactility/app/fileselection/State.h create mode 100644 Tactility/Private/Tactility/app/fileselection/View.h rename Tactility/Source/app/{files/Files.cpp => filebrowser/FileBrowser.cpp} (66%) rename Tactility/Source/app/{files => filebrowser}/State.cpp (84%) create mode 100644 Tactility/Source/app/filebrowser/SupportedFiles.cpp rename Tactility/Source/app/{files => filebrowser}/View.cpp (87%) delete mode 100644 Tactility/Source/app/files/FileUtils.cpp create mode 100644 Tactility/Source/app/fileselection/FileSelection.cpp create mode 100644 Tactility/Source/app/fileselection/State.cpp create mode 100644 Tactility/Source/app/fileselection/View.cpp diff --git a/App/Source/Boards.h b/App/Source/Boards.h index c5e296ee..982b0cee 100644 --- a/App/Source/Boards.h +++ b/App/Source/Boards.h @@ -1,4 +1,5 @@ #pragma once +#include #ifdef ESP_PLATFORM #include diff --git a/Boards/Simulator/Source/Simulator.h b/Boards/Simulator/Source/Simulator.h index 340e6c2e..34da7869 100644 --- a/Boards/Simulator/Source/Simulator.h +++ b/Boards/Simulator/Source/Simulator.h @@ -1,7 +1,6 @@ #pragma once #include "Main.h" -#include namespace simulator { /** Set the function pointer of the real app_main() */ diff --git a/Tactility/Include/Tactility/app/App.h b/Tactility/Include/Tactility/app/App.h index 5135b485..e9808323 100644 --- a/Tactility/Include/Tactility/app/App.h +++ b/Tactility/Include/Tactility/app/App.h @@ -16,9 +16,9 @@ namespace tt::app { class AppContext; enum class Result; -class App { +typedef unsigned int LaunchId; -private: +class App { Mutex mutex; @@ -44,7 +44,7 @@ public: virtual void onDestroy(AppContext& appContext) {} virtual void onShow(AppContext& appContext, lv_obj_t* parent) {} virtual void onHide(AppContext& appContext) {} - virtual void onResult(AppContext& appContext, Result result, std::unique_ptr _Nullable resultData) {} + virtual void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr _Nullable resultData) {} Mutex& getMutex() { return mutex; } @@ -83,15 +83,15 @@ std::shared_ptr create() { return std::shared_ptr(new T); } * @param[in] id application name or id * @param[in] parameters optional parameters to pass onto the application */ -void start(const std::string& id, std::shared_ptr _Nullable parameters = nullptr); +LaunchId start(const std::string& id, std::shared_ptr _Nullable parameters = nullptr); /** @brief Stop the currently showing app. Show the previous app if any app was still running. */ void stop(); /** @return the currently running app context (it is only ever null before the splash screen is shown) */ -std::shared_ptr _Nullable getCurrentAppContext(); +std::shared_ptr _Nullable getCurrentAppContext(); /** @return the currently running app (it is only ever null before the splash screen is shown) */ -std::shared_ptr _Nullable getCurrentApp(); +std::shared_ptr _Nullable getCurrentApp(); } diff --git a/Tactility/Include/Tactility/app/ElfApp.h b/Tactility/Include/Tactility/app/ElfApp.h index 9821f4eb..c086ab6a 100644 --- a/Tactility/Include/Tactility/app/ElfApp.h +++ b/Tactility/Include/Tactility/app/ElfApp.h @@ -12,7 +12,7 @@ typedef void (*OnCreate)(void* appContext, void* _Nullable data); typedef void (*OnDestroy)(void* appContext, void* _Nullable data); typedef void (*OnShow)(void* appContext, void* _Nullable data, lv_obj_t* parent); typedef void (*OnHide)(void* appContext, void* _Nullable data); -typedef void (*OnResult)(void* appContext, void* _Nullable data, Result result, Bundle* resultData); +typedef void (*OnResult)(void* appContext, void* _Nullable data, LaunchId launchId, Result result, Bundle* resultData); void setElfAppManifest( const char* name, diff --git a/Tactility/Include/Tactility/app/filebrowser/FileBrowser.h b/Tactility/Include/Tactility/app/filebrowser/FileBrowser.h new file mode 100644 index 00000000..86fcf424 --- /dev/null +++ b/Tactility/Include/Tactility/app/filebrowser/FileBrowser.h @@ -0,0 +1,7 @@ +#pragma once + +namespace tt::app::filebrowser { + +void start(); + +} // namespace diff --git a/Tactility/Include/Tactility/app/files/Files.h b/Tactility/Include/Tactility/app/files/Files.h deleted file mode 100644 index 1d77e44a..00000000 --- a/Tactility/Include/Tactility/app/files/Files.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include "app/files/View.h" -#include "app/files/State.h" -#include "app/AppManifest.h" - -#include -#include -#include - -namespace tt::app::files { - -void start(); - -} // namespace diff --git a/Tactility/Include/Tactility/app/fileselection/FileSelection.h b/Tactility/Include/Tactility/app/fileselection/FileSelection.h new file mode 100644 index 00000000..f2c08473 --- /dev/null +++ b/Tactility/Include/Tactility/app/fileselection/FileSelection.h @@ -0,0 +1,23 @@ +#pragma once + +namespace tt::app::fileselection { + +/** + * Show a file selection dialog that allows the user to select an existing file. + * This app returns the absolute file path as a result. + */ +LaunchId startForExistingFile(); + +/** + * Show a file selection dialog that allows the user to select a new or existing file. + * This app returns the absolute file path as a result. + */ +LaunchId startForExistingOrNewFile(); + +/** + * @param bundle the result bundle of an app + * @return the path from the bundle, or empty string if none is present + */ +std::string getResultPath(const Bundle& bundle); + +} // namespace diff --git a/Tactility/Include/Tactility/hal/sdcard/SdCardDevice.h b/Tactility/Include/Tactility/hal/sdcard/SdCardDevice.h index 7ca7bf10..8e753b22 100644 --- a/Tactility/Include/Tactility/hal/sdcard/SdCardDevice.h +++ b/Tactility/Include/Tactility/hal/sdcard/SdCardDevice.h @@ -53,7 +53,7 @@ std::shared_ptr _Nullable find(const std::string& path); * Always calls the function, but doesn't lock if the path is not an SD card path. */ template -inline ReturnType withSdCardLock(const std::string& path, std::function fn) { + ReturnType withSdCardLock(const std::string& path, std::function fn) { auto sdcard = find(path); if (sdcard != nullptr) { auto scoped_lockable = sdcard->getLock().asScopedLock(); diff --git a/Tactility/Include/Tactility/service/loader/Loader.h b/Tactility/Include/Tactility/service/loader/Loader.h index edd02d57..d5d3b651 100644 --- a/Tactility/Include/Tactility/service/loader/Loader.h +++ b/Tactility/Include/Tactility/service/loader/Loader.h @@ -4,7 +4,6 @@ #include #include -#include #include @@ -30,7 +29,7 @@ struct LoaderEvent { * @param[in] id application name or id * @param[in] parameters optional parameters to pass onto the application */ -void startApp(const std::string& id, std::shared_ptr _Nullable parameters = nullptr); +app::LaunchId startApp(const std::string& id, std::shared_ptr _Nullable parameters = nullptr); /** @brief Stop the currently showing app. Show the previous app if any app was still running. */ void stopApp(); diff --git a/Tactility/Private/Tactility/app/AppInstance.h b/Tactility/Private/Tactility/app/AppInstance.h index 3bd76558..f38cfb2b 100644 --- a/Tactility/Private/Tactility/app/AppInstance.h +++ b/Tactility/Private/Tactility/app/AppInstance.h @@ -25,22 +25,21 @@ enum class State { */ class AppInstance : public AppContext { -private: - Mutex mutex = Mutex(Mutex::Type::Normal); const std::shared_ptr manifest; State state = State::Initial; + LaunchId launchId; Flags flags = { .showStatusbar = true }; /** @brief Optional parameters to start the app with * When these are stored in the app struct, the struct takes ownership. * Do not mutate after app creation. */ - std::shared_ptr _Nullable parameters; + std::shared_ptr _Nullable parameters; std::shared_ptr app; - static std::shared_ptr createApp( - const std::shared_ptr& manifest + static std::shared_ptr createApp( + const std::shared_ptr& manifest ) { if (manifest->location.isInternal()) { assert(manifest->createApp != nullptr); @@ -50,7 +49,7 @@ private: TT_LOG_W("", "Manifest specifies createApp, but this is not used with external apps"); } #ifdef ESP_PLATFORM - return app::createElfApp(manifest); + return createElfApp(manifest); #else tt_crash("not supported"); #endif @@ -61,18 +60,23 @@ private: public: - explicit AppInstance(const std::shared_ptr& manifest) : + explicit AppInstance(const std::shared_ptr& manifest, LaunchId launchId) : manifest(manifest), + launchId(launchId), app(createApp(manifest)) {} - AppInstance(const std::shared_ptr& manifest, std::shared_ptr parameters) : + AppInstance(const std::shared_ptr& manifest, LaunchId launchId, std::shared_ptr parameters) : manifest(manifest), + launchId(launchId), parameters(std::move(parameters)), - app(createApp(manifest)) {} + app(createApp(manifest)) + {} ~AppInstance() override = default; + LaunchId getLaunchId() const { return launchId; } + void setState(State state); State getState() const; diff --git a/Tactility/Private/Tactility/app/files/State.h b/Tactility/Private/Tactility/app/filebrowser/State.h similarity index 97% rename from Tactility/Private/Tactility/app/files/State.h rename to Tactility/Private/Tactility/app/filebrowser/State.h index c29e1521..04308e3d 100644 --- a/Tactility/Private/Tactility/app/files/State.h +++ b/Tactility/Private/Tactility/app/filebrowser/State.h @@ -6,7 +6,7 @@ #include #include -namespace tt::app::files { +namespace tt::app::filebrowser { class State { diff --git a/Tactility/Private/Tactility/app/filebrowser/SupportedFiles.h b/Tactility/Private/Tactility/app/filebrowser/SupportedFiles.h new file mode 100644 index 00000000..0ae68fbb --- /dev/null +++ b/Tactility/Private/Tactility/app/filebrowser/SupportedFiles.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace tt::app::filebrowser { + +bool isSupportedExecutableFile(const std::string& filename); +bool isSupportedImageFile(const std::string& filename); +bool isSupportedTextFile(const std::string& filename); + +} // namespace diff --git a/Tactility/Private/Tactility/app/files/View.h b/Tactility/Private/Tactility/app/filebrowser/View.h similarity index 96% rename from Tactility/Private/Tactility/app/files/View.h rename to Tactility/Private/Tactility/app/filebrowser/View.h index 0c616c7c..c6f24026 100644 --- a/Tactility/Private/Tactility/app/files/View.h +++ b/Tactility/Private/Tactility/app/filebrowser/View.h @@ -7,7 +7,7 @@ #include #include -namespace tt::app::files { +namespace tt::app::filebrowser { class View { std::shared_ptr state; diff --git a/Tactility/Private/Tactility/app/files/FileUtils.h b/Tactility/Private/Tactility/app/files/FileUtils.h deleted file mode 100644 index d05cba3c..00000000 --- a/Tactility/Private/Tactility/app/files/FileUtils.h +++ /dev/null @@ -1,66 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -namespace tt::app::files { - -/** File types for `dirent`'s `d_type`. */ -enum { - TT_DT_UNKNOWN = 0, -#define TT_DT_UNKNOWN TT_DT_UNKNOWN // Unknown type - TT_DT_FIFO = 1, -#define TT_DT_FIFO TT_DT_FIFO // Named pipe or FIFO - TT_DT_CHR = 2, -#define TT_DT_CHR TT_DT_CHR // Character device - TT_DT_DIR = 4, -#define TT_DT_DIR TT_DT_DIR // Directory - TT_DT_BLK = 6, -#define TT_DT_BLK TT_DT_BLK // Block device - TT_DT_REG = 8, -#define TT_DT_REG TT_DT_REG // Regular file - TT_DT_LNK = 10, -#define TT_DT_LNK TT_DT_LNK // Symbolic link - TT_DT_SOCK = 12, -#define TT_DT_SOCK TT_DT_SOCK // Local-domain socket - TT_DT_WHT = 14 -#define TT_DT_WHT TT_DT_WHT // Whiteout inodes -}; - - -std::string getChildPath(const std::string& basePath, const std::string& childPath); - -typedef int (*ScandirFilter)(const struct dirent*); - -typedef bool (*ScandirSort)(const struct dirent&, const struct dirent&); - -bool dirent_sort_alpha_and_type(const struct dirent& left, const struct dirent& right); - -int dirent_filter_dot_entries(const struct dirent* entry); - -/** - * A scandir()-like implementation that works on ESP32. - * It does not return "." and ".." items but otherwise functions the same. - * It returns an allocated output array with allocated dirent instances. - * The caller is responsible for free-ing the memory of these. - * - * @param[in] path path the scan for files and directories - * @param[out] outList a pointer to vector of dirent - * @param[in] filter an optional filter to filter out specific items - * @param[in] sort an optional sorting function - * @return the amount of items that were stored in "output" or -1 when an error occurred - */ -int scandir( - const std::string& path, - std::vector& outList, - ScandirFilter _Nullable filter, - ScandirSort _Nullable sort -); - -bool isSupportedExecutableFile(const std::string& filename); -bool isSupportedImageFile(const std::string& filename); -bool isSupportedTextFile(const std::string& filename); - -} // namespace diff --git a/Tactility/Private/Tactility/app/fileselection/FileSelectionPrivate.h b/Tactility/Private/Tactility/app/fileselection/FileSelectionPrivate.h new file mode 100644 index 00000000..479edd12 --- /dev/null +++ b/Tactility/Private/Tactility/app/fileselection/FileSelectionPrivate.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace tt::app::fileselection { + +enum class Mode { + Existing = 0, + ExistingOrNew = 1 +}; + +Mode getMode(const Bundle& bundle); + +} diff --git a/Tactility/Private/Tactility/app/fileselection/State.h b/Tactility/Private/Tactility/app/fileselection/State.h new file mode 100644 index 00000000..912f8466 --- /dev/null +++ b/Tactility/Private/Tactility/app/fileselection/State.h @@ -0,0 +1,59 @@ +#pragma once + +#include + +#include +#include +#include + +namespace tt::app::fileselection { + +class State { + + Mutex mutex = Mutex(Mutex::Type::Recursive); + std::vector dir_entries; + std::string current_path; + std::string selected_child_entry; + +public: + + State(); + + void freeEntries() { + dir_entries.clear(); + } + + ~State() { + freeEntries(); + } + + bool setEntriesForChildPath(const std::string& child_path); + bool setEntriesForPath(const std::string& path); + + template &> Func> + void withEntries(Func&& onEntries) const { + mutex.withLock([&]() { + std::invoke(std::forward(onEntries), dir_entries); + }); + } + + bool getDirent(uint32_t index, dirent& dirent); + + void setSelectedChildEntry(const std::string& newFile) { + selected_child_entry = newFile; + } + + std::string getSelectedChildEntry() const { return selected_child_entry; } + std::string getCurrentPath() const { return current_path; } + std::string getCurrentPathWithTrailingSlash() const { + if (current_path.length() > 1) { + return current_path + "/"; + } else { + return current_path; + } + } + + std::string getSelectedChildPath() const; +}; + +} diff --git a/Tactility/Private/Tactility/app/fileselection/View.h b/Tactility/Private/Tactility/app/fileselection/View.h new file mode 100644 index 00000000..4f8d2085 --- /dev/null +++ b/Tactility/Private/Tactility/app/fileselection/View.h @@ -0,0 +1,44 @@ +#pragma once + +#include "./State.h" +#include "./FileSelectionPrivate.h" + +#include "Tactility/app/AppManifest.h" + +#include +#include + +namespace tt::app::fileselection { + +class View { + std::shared_ptr state; + + lv_obj_t* dir_entry_list = nullptr; + lv_obj_t* navigate_up_button = nullptr; + lv_obj_t* path_textarea = nullptr; + lv_obj_t* select_button = nullptr; + std::function on_file_selected; + + void onTapFile(const std::string&path, const std::string&filename); + static void onSelectButtonPressed(lv_event_t* event); + static void onPathTextChanged(lv_event_t* event); + void createDirEntryWidget(lv_obj_t* parent, dirent& dir_entry); + +public: + + explicit View(const std::shared_ptr& state, std::function onFileSelected) : + state(state), + on_file_selected(std::move(onFileSelected)) + {} + + void init(lv_obj_t* parent, Mode mode); + void update(); + + void onNavigateUpPressed(); + void onDirEntryPressed(uint32_t index); + void onFileSelected(const std::string& path) const { + on_file_selected(path); + } +}; + +} diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 6aa5d1d7..9a7ba056 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -37,7 +37,8 @@ namespace app { namespace chat { extern const AppManifest manifest; } namespace boot { extern const AppManifest manifest; } namespace display { extern const AppManifest manifest; } - namespace files { extern const AppManifest manifest; } + namespace filebrowser { extern const AppManifest manifest; } + namespace fileselection { extern const AppManifest manifest; } namespace gpio { extern const AppManifest manifest; } namespace gpssettings { extern const AppManifest manifest; } namespace i2cscanner { extern const AppManifest manifest; } @@ -78,7 +79,8 @@ static void registerSystemApps() { addApp(app::applist::manifest); addApp(app::calculator::manifest); addApp(app::display::manifest); - addApp(app::files::manifest); + addApp(app::filebrowser::manifest); + addApp(app::fileselection::manifest); addApp(app::gpio::manifest); addApp(app::gpssettings::manifest); addApp(app::i2cscanner::manifest); diff --git a/Tactility/Source/app/App.cpp b/Tactility/Source/app/App.cpp index 2c74ceb8..91ea12e3 100644 --- a/Tactility/Source/app/App.cpp +++ b/Tactility/Source/app/App.cpp @@ -4,19 +4,19 @@ namespace tt::app { -void start(const std::string& id, std::shared_ptr _Nullable parameters) { - service::loader::startApp(id, std::move(parameters)); +LaunchId start(const std::string& id, std::shared_ptr _Nullable parameters) { + return service::loader::startApp(id, std::move(parameters)); } void stop() { service::loader::stopApp(); } -std::shared_ptr _Nullable getCurrentAppContext() { +std::shared_ptr _Nullable getCurrentAppContext() { return service::loader::getCurrentAppContext(); } -std::shared_ptr _Nullable getCurrentApp() { +std::shared_ptr _Nullable getCurrentApp() { return service::loader::getCurrentApp(); } diff --git a/Tactility/Source/app/ElfApp.cpp b/Tactility/Source/app/ElfApp.cpp index 7f75e709..77217194 100644 --- a/Tactility/Source/app/ElfApp.cpp +++ b/Tactility/Source/app/ElfApp.cpp @@ -36,8 +36,6 @@ static ElfManifest elfManifest; class ElfApp : public App { -private: - const std::string filePath; std::unique_ptr elfFileData; esp_elf_t elf; @@ -143,9 +141,9 @@ public: } } - void onResult(AppContext& appContext, Result result, std::unique_ptr resultBundle) override { + void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr resultBundle) override { if (manifest != nullptr && manifest->onResult != nullptr) { - manifest->onResult(&appContext, data, result, resultBundle.get()); + manifest->onResult(&appContext, data, launchId, result, resultBundle.get()); } } }; diff --git a/Tactility/Source/app/files/Files.cpp b/Tactility/Source/app/filebrowser/FileBrowser.cpp similarity index 66% rename from Tactility/Source/app/files/Files.cpp rename to Tactility/Source/app/filebrowser/FileBrowser.cpp index 37b1ceb4..c39edcc2 100644 --- a/Tactility/Source/app/files/Files.cpp +++ b/Tactility/Source/app/filebrowser/FileBrowser.cpp @@ -1,5 +1,5 @@ -#include "Tactility/app/files/View.h" -#include "Tactility/app/files/State.h" +#include "Tactility/app/filebrowser/View.h" +#include "Tactility/app/filebrowser/State.h" #include "Tactility/app/AppContext.h" #include @@ -7,18 +7,18 @@ #include -namespace tt::app::files { +namespace tt::app::filebrowser { -#define TAG "files_app" +#define TAG "filebrowser_app" extern const AppManifest manifest; -class FilesApp : public App { +class FileBrowser : public App { std::unique_ptr view; std::shared_ptr state; public: - FilesApp() { + FileBrowser() { state = std::make_shared(); view = std::make_unique(state); } @@ -27,7 +27,7 @@ public: view->init(parent); } - void onResult(AppContext& appContext, Result result, std::unique_ptr bundle) override { + void onResult(AppContext& appContext, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr bundle) override { view->onResult(result, std::move(bundle)); } }; @@ -37,7 +37,7 @@ extern const AppManifest manifest = { .name = "Files", .icon = TT_ASSETS_APP_ICON_FILES, .type = Type::Hidden, - .createApp = create + .createApp = create }; void start() { diff --git a/Tactility/Source/app/files/State.cpp b/Tactility/Source/app/filebrowser/State.cpp similarity index 84% rename from Tactility/Source/app/files/State.cpp rename to Tactility/Source/app/filebrowser/State.cpp index fae8c301..e2dbe18d 100644 --- a/Tactility/Source/app/files/State.cpp +++ b/Tactility/Source/app/filebrowser/State.cpp @@ -1,6 +1,6 @@ -#include "Tactility/app/files/State.h" -#include "Tactility/app/files/FileUtils.h" +#include "Tactility/app/filebrowser/State.h" +#include #include "Tactility/hal/sdcard/SdCardDevice.h" #include #include @@ -11,9 +11,9 @@ #include #include -#define TAG "files_app" +#define TAG "filebrowser_app" -namespace tt::app::files { +namespace tt::app::filebrowser { State::State() { if (kernel::getPlatform() == kernel::PlatformSimulator) { @@ -30,7 +30,7 @@ State::State() { } std::string State::getSelectedChildPath() const { - return getChildPath(current_path, selected_child_entry); + return file::getChildPath(current_path, selected_child_entry); } bool State::setEntriesForPath(const std::string& path) { @@ -52,12 +52,12 @@ bool State::setEntriesForPath(const std::string& path) { dir_entries.clear(); dir_entries.push_back(dirent{ .d_ino = 0, - .d_type = TT_DT_DIR, + .d_type = file::TT_DT_DIR, .d_name = SYSTEM_PARTITION_NAME }); dir_entries.push_back(dirent{ .d_ino = 1, - .d_type = TT_DT_DIR, + .d_type = file::TT_DT_DIR, .d_name = DATA_PARTITION_NAME }); @@ -68,7 +68,7 @@ bool State::setEntriesForPath(const std::string& path) { auto mount_name = sdcard->getMountPath().substr(1); auto dir_entry = dirent { .d_ino = 2, - .d_type = TT_DT_DIR, + .d_type = file::TT_DT_DIR, .d_name = { 0 } }; assert(mount_name.length() < sizeof(dirent::d_name)); @@ -83,7 +83,7 @@ bool State::setEntriesForPath(const std::string& path) { return true; } else { dir_entries.clear(); - int count = tt::app::files::scandir(path, dir_entries, &dirent_filter_dot_entries, dirent_sort_alpha_and_type); + int count = file::scandir(path, dir_entries, &file::direntFilterDotEntries, file::direntSortAlphaAndType); if (count >= 0) { TT_LOG_I(TAG, "%s has %u entries", path.c_str(), count); current_path = path; @@ -97,8 +97,8 @@ bool State::setEntriesForPath(const std::string& path) { } } -bool State::setEntriesForChildPath(const std::string& child_path) { - auto path = getChildPath(current_path, child_path); +bool State::setEntriesForChildPath(const std::string& childPath) { + auto path = file::getChildPath(current_path, childPath); TT_LOG_I(TAG, "Navigating from %s to %s", current_path.c_str(), path.c_str()); return setEntriesForPath(path); } diff --git a/Tactility/Source/app/filebrowser/SupportedFiles.cpp b/Tactility/Source/app/filebrowser/SupportedFiles.cpp new file mode 100644 index 00000000..3069ad08 --- /dev/null +++ b/Tactility/Source/app/filebrowser/SupportedFiles.cpp @@ -0,0 +1,34 @@ +#include +#include + +namespace tt::app::filebrowser { + +#define TAG "filebrowser_app" + +bool isSupportedExecutableFile(const std::string& filename) { +#ifdef ESP_PLATFORM + // Currently only the PNG library is built into Tactility + return filename.ends_with(".elf"); +#else + return false; +#endif +} + +bool isSupportedImageFile(const std::string& filename) { + // Currently only the PNG library is built into Tactility + return string::lowercase(filename).ends_with(".png"); +} + +bool isSupportedTextFile(const std::string& filename) { + std::string filename_lower = string::lowercase(filename); + return filename_lower.ends_with(".txt") || + filename_lower.ends_with(".ini") || + filename_lower.ends_with(".json") || + filename_lower.ends_with(".yaml") || + filename_lower.ends_with(".yml") || + filename_lower.ends_with(".lua") || + filename_lower.ends_with(".js") || + filename_lower.ends_with(".properties"); +} + +} // namespace tt::app::filebrowser diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/filebrowser/View.cpp similarity index 87% rename from Tactility/Source/app/files/View.cpp rename to Tactility/Source/app/filebrowser/View.cpp index f927bd4c..5da95b85 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/filebrowser/View.cpp @@ -1,5 +1,5 @@ -#include "Tactility/app/files/FileUtils.h" -#include "Tactility/app/files/View.h" +#include "Tactility/app/filebrowser/View.h" +#include "Tactility/app/filebrowser/SupportedFiles.h" #include "Tactility/app/alertdialog/AlertDialog.h" #include "Tactility/app/imageviewer/ImageViewer.h" @@ -10,6 +10,7 @@ #include "Tactility/lvgl/LvglSync.h" #include +#include "Tactility/file/File.h" #include #include @@ -19,43 +20,43 @@ #include "Tactility/service/loader/Loader.h" #endif -#define TAG "files_app" +#define TAG "filebrowser_app" -namespace tt::app::files { +namespace tt::app::filebrowser { // region Callbacks static void dirEntryListScrollBeginCallback(lv_event_t* event) { - auto* view = (View*)lv_event_get_user_data(event); + auto* view = static_cast(lv_event_get_user_data(event)); view->onDirEntryListScrollBegin(); } static void onDirEntryPressedCallback(lv_event_t* event) { - auto* view = (View*)lv_event_get_user_data(event); + auto* view = static_cast(lv_event_get_user_data(event)); auto* button = lv_event_get_target_obj(event); auto index = lv_obj_get_index(button); view->onDirEntryPressed(index); } static void onDirEntryLongPressedCallback(lv_event_t* event) { - auto* view = (View*)lv_event_get_user_data(event); + auto* view = static_cast(lv_event_get_user_data(event)); auto* button = lv_event_get_target_obj(event); auto index = lv_obj_get_index(button); view->onDirEntryLongPressed(index); } static void onRenamePressedCallback(lv_event_t* event) { - auto* view = (View*)lv_event_get_user_data(event); + auto* view = static_cast(lv_event_get_user_data(event)); view->onRenamePressed(); } static void onDeletePressedCallback(lv_event_t* event) { - auto* view = (View*)lv_event_get_user_data(event); + auto* view = static_cast(lv_event_get_user_data(event)); view->onDeletePressed(); } static void onNavigateUpPressedCallback(TT_UNUSED lv_event_t* event) { - auto* view = (View*)lv_event_get_user_data(event); + auto* view = static_cast(lv_event_get_user_data(event)); view->onNavigateUpPressed(); } @@ -86,18 +87,18 @@ void View::viewFile(const std::string& path, const std::string& filename) { if (isSupportedExecutableFile(filename)) { #ifdef ESP_PLATFORM - app::registerElfApp(processed_filepath); - auto app_id = app::getElfAppId(processed_filepath); + registerElfApp(processed_filepath); + auto app_id = getElfAppId(processed_filepath); service::loader::startApp(app_id); #endif } else if (isSupportedImageFile(filename)) { - app::imageviewer::start(processed_filepath); + imageviewer::start(processed_filepath); } else if (isSupportedTextFile(filename)) { if (kernel::getPlatform() == kernel::PlatformEsp) { - app::textviewer::start(processed_filepath); + textviewer::start(processed_filepath); } else { // Remove forward slash, because we need a relative path - app::textviewer::start(processed_filepath.substr(1)); + textviewer::start(processed_filepath.substr(1)); } } else { TT_LOG_W(TAG, "opening files of this type is not supported"); @@ -111,6 +112,7 @@ void View::onDirEntryPressed(uint32_t index) { if (state->getDirent(index, dir_entry)) { TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type); state->setSelectedChildEntry(dir_entry.d_name); + using namespace tt::file; switch (dir_entry.d_type) { case TT_DT_DIR: case TT_DT_CHR: @@ -140,6 +142,7 @@ void View::onDirEntryLongPressed(int32_t index) { if (state->getDirent(index, dir_entry)) { TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type); state->setSelectedChildEntry(dir_entry.d_name); + using namespace file; switch (dir_entry.d_type) { case TT_DT_DIR: case TT_DT_CHR: @@ -161,15 +164,14 @@ void View::onDirEntryLongPressed(int32_t index) { } -void View::createDirEntryWidget(lv_obj_t* parent, struct dirent& dir_entry) { - tt_check(parent); - auto* list = (lv_obj_t*)parent; +void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) { + tt_check(list); const char* symbol; - if (dir_entry.d_type == TT_DT_DIR || dir_entry.d_type == TT_DT_CHR) { + if (dir_entry.d_type == file::TT_DT_DIR || dir_entry.d_type == file::TT_DT_CHR) { symbol = LV_SYMBOL_DIRECTORY; } else if (isSupportedImageFile(dir_entry.d_name)) { symbol = LV_SYMBOL_IMAGE; - } else if (dir_entry.d_type == TT_DT_LNK) { + } else if (dir_entry.d_type == file::TT_DT_LNK) { symbol = LV_SYMBOL_LOOP; } else { symbol = LV_SYMBOL_FILE; @@ -195,7 +197,7 @@ void View::onRenamePressed() { std::string entry_name = state->getSelectedChildEntry(); TT_LOG_I(TAG, "Pending rename %s", entry_name.c_str()); state->setPendingAction(State::ActionRename); - app::inputdialog::start("Rename", "", entry_name); + inputdialog::start("Rename", "", entry_name); } void View::onDeletePressed() { @@ -204,7 +206,7 @@ void View::onDeletePressed() { state->setPendingAction(State::ActionDelete); std::string message = "Do you want to delete this?\n" + file_path; const std::vector choices = { "Yes", "No" }; - app::alertdialog::start("Are you sure?", message, choices); + alertdialog::start("Are you sure?", message, choices); } void View::showActionsForDirectory() { @@ -301,7 +303,7 @@ void View::onResult(Result result, std::unique_ptr bundle) { switch (state->getPendingAction()) { case State::ActionDelete: { if (alertdialog::getResultIndex(*bundle) == 0) { - int delete_count = (int)remove(filepath.c_str()); + int delete_count = remove(filepath.c_str()); if (delete_count > 0) { TT_LOG_I(TAG, "Deleted %d items", delete_count); } else { @@ -313,9 +315,9 @@ void View::onResult(Result result, std::unique_ptr bundle) { break; } case State::ActionRename: { - auto new_name = app::inputdialog::getResult(*bundle); + auto new_name = inputdialog::getResult(*bundle); if (!new_name.empty() && new_name != state->getSelectedChildEntry()) { - std::string rename_to = getChildPath(state->getCurrentPath(), new_name); + std::string rename_to = file::getChildPath(state->getCurrentPath(), new_name); if (rename(filepath.c_str(), rename_to.c_str())) { TT_LOG_I(TAG, "Renamed \"%s\" to \"%s\"", filepath.c_str(), rename_to.c_str()); } else { diff --git a/Tactility/Source/app/files/FileUtils.cpp b/Tactility/Source/app/files/FileUtils.cpp deleted file mode 100644 index a649ea91..00000000 --- a/Tactility/Source/app/files/FileUtils.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#include "Tactility/app/files/FileUtils.h" - -#include -#include - -#include - -namespace tt::app::files { - -#define TAG "file_utils" - -std::string getChildPath(const std::string& basePath, const std::string& childPath) { - // Postfix with "/" when the current path isn't "/" - if (basePath.length() != 1) { - return basePath + "/" + childPath; - } else { - return "/" + childPath; - } -} - -int dirent_filter_dot_entries(const struct dirent* entry) { - return (strcmp(entry->d_name, "..") == 0 || strcmp(entry->d_name, ".") == 0) ? -1 : 0; -} - -bool dirent_sort_alpha_and_type(const struct dirent& left, const struct dirent& right) { - bool left_is_dir = left.d_type == TT_DT_DIR || left.d_type == TT_DT_CHR; - bool right_is_dir = right.d_type == TT_DT_DIR || right.d_type == TT_DT_CHR; - if (left_is_dir == right_is_dir) { - return strcmp(left.d_name, right.d_name) < 0; - } else { - return left_is_dir > right_is_dir; - } -} - -int scandir( - const std::string& path, - std::vector& outList, - ScandirFilter _Nullable filterMethod, - ScandirSort _Nullable sortMethod -) { - TT_LOG_I(TAG, "scandir start"); - DIR* dir = opendir(path.c_str()); - if (dir == nullptr) { - TT_LOG_E(TAG, "Failed to open dir %s", path.c_str()); - return -1; - } - - struct dirent* current_entry; - while ((current_entry = readdir(dir)) != nullptr) { - if (filterMethod(current_entry) == 0) { - outList.push_back(*current_entry); - } - } - - closedir(dir); - - if (sortMethod != nullptr) { - sort(outList.begin(), outList.end(), sortMethod); - } - - TT_LOG_I(TAG, "scandir finish"); - return (int)outList.size(); -}; - -bool isSupportedExecutableFile(const std::string& filename) { -#ifdef ESP_PLATFORM - // Currently only the PNG library is built into Tactility - return filename.ends_with(".elf"); -#else - return false; -#endif -} - -bool isSupportedImageFile(const std::string& filename) { - // Currently only the PNG library is built into Tactility - return string::lowercase(filename).ends_with(".png"); -} - -bool isSupportedTextFile(const std::string& filename) { - std::string filename_lower = string::lowercase(filename); - return filename_lower.ends_with(".txt") || - filename_lower.ends_with(".ini") || - filename_lower.ends_with(".json") || - filename_lower.ends_with(".yaml") || - filename_lower.ends_with(".yml") || - filename_lower.ends_with(".lua") || - filename_lower.ends_with(".js") || - filename_lower.ends_with(".properties"); -} - -} // namespace tt::app::files diff --git a/Tactility/Source/app/fileselection/FileSelection.cpp b/Tactility/Source/app/fileselection/FileSelection.cpp new file mode 100644 index 00000000..0784d001 --- /dev/null +++ b/Tactility/Source/app/fileselection/FileSelection.cpp @@ -0,0 +1,78 @@ +#include "Tactility/app/fileselection/FileSelectionPrivate.h" +#include "Tactility/app/fileselection/View.h" +#include "Tactility/app/fileselection/State.h" +#include "Tactility/app/AppContext.h" + +#include +#include + +#include + +namespace tt::app::fileselection { + +#define TAG "fileselection_app" + +extern const AppManifest manifest; + +std::string getResultPath(const Bundle& bundle) { + std::string result; + if (bundle.optString("path", result)) { + return result; + } else { + return ""; + } +} + +Mode getMode(const Bundle& bundle) { + int32_t mode = static_cast(Mode::ExistingOrNew); + bundle.optInt32("mode", mode); + return static_cast(mode); +} + +void setMode(Bundle& bundle, Mode mode) { + auto mode_int = static_cast(mode); + bundle.putInt32("mode", mode_int); +} + +class FileSelection : public App { + std::unique_ptr view; + std::shared_ptr state; + +public: + FileSelection() { + state = std::make_shared(); + view = std::make_unique(state, [this](const std::string& path) { + auto bundle = std::make_unique(); + bundle->putString("path", path); + setResult(Result::Ok, std::move(bundle)); + service::loader::stopApp(); + }); + } + + void onShow(AppContext& appContext, lv_obj_t* parent) override { + auto mode = getMode(*appContext.getParameters()); + view->init(parent, mode); + } +}; + +extern const AppManifest manifest = { + .id = "FileSelection", + .name = "File Selection", + .icon = TT_ASSETS_APP_ICON_FILES, + .type = Type::Hidden, + .createApp = create +}; + +LaunchId startForExistingFile() { + auto bundle = std::make_shared(); + setMode(*bundle, Mode::Existing); + return service::loader::startApp(manifest.id, bundle); +} + +LaunchId startForExistingOrNewFile() { + auto bundle = std::make_shared(); + setMode(*bundle, Mode::ExistingOrNew); + return service::loader::startApp(manifest.id, bundle); +} + +} // namespace diff --git a/Tactility/Source/app/fileselection/State.cpp b/Tactility/Source/app/fileselection/State.cpp new file mode 100644 index 00000000..c648b6d9 --- /dev/null +++ b/Tactility/Source/app/fileselection/State.cpp @@ -0,0 +1,118 @@ +#include "Tactility/app/fileselection/State.h" + +#include +#include "Tactility/hal/sdcard/SdCardDevice.h" +#include +#include +#include + +#include +#include +#include +#include + +#define TAG "fileselection_app" + +namespace tt::app::fileselection { + +State::State() { + if (kernel::getPlatform() == kernel::PlatformSimulator) { + char cwd[PATH_MAX]; + if (getcwd(cwd, sizeof(cwd)) != nullptr) { + setEntriesForPath(cwd); + } else { + TT_LOG_E(TAG, "Failed to get current work directory files"); + setEntriesForPath("/"); + } + } else { + setEntriesForPath("/"); + } +} + +std::string State::getSelectedChildPath() const { + return file::getChildPath(current_path, selected_child_entry); +} + +bool State::setEntriesForPath(const std::string& path) { + auto lock = mutex.asScopedLock(); + if (!lock.lock(100)) { + TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "setEntriesForPath"); + return false; + } + + TT_LOG_I(TAG, "Changing path: %s -> %s", current_path.c_str(), path.c_str()); + + /** + * ESP32 does not have a root directory, so we have to create it manually. + * We'll add the NVS Flash partitions and the binding for the sdcard. + */ + bool show_custom_root = (kernel::getPlatform() == kernel::PlatformEsp) && (path == "/"); + if (show_custom_root) { + TT_LOG_I(TAG, "Setting custom root"); + dir_entries.clear(); + dir_entries.push_back(dirent{ + .d_ino = 0, + .d_type = file::TT_DT_DIR, + .d_name = SYSTEM_PARTITION_NAME + }); + dir_entries.push_back(dirent{ + .d_ino = 1, + .d_type = file::TT_DT_DIR, + .d_name = DATA_PARTITION_NAME + }); + + auto sdcards = tt::hal::findDevices(hal::Device::Type::SdCard); + for (auto& sdcard : sdcards) { + auto state = sdcard->getState(); + if (state == hal::sdcard::SdCardDevice::State::Mounted) { + auto mount_name = sdcard->getMountPath().substr(1); + auto dir_entry = dirent { + .d_ino = 2, + .d_type = file::TT_DT_DIR, + .d_name = { 0 } + }; + assert(mount_name.length() < sizeof(dirent::d_name)); + strcpy(dir_entry.d_name, mount_name.c_str()); + dir_entries.push_back(dir_entry); + } + } + + current_path = path; + selected_child_entry = ""; + return true; + } else { + dir_entries.clear(); + int count = file::scandir(path, dir_entries, &file::direntFilterDotEntries, file::direntSortAlphaAndType); + if (count >= 0) { + TT_LOG_I(TAG, "%s has %u entries", path.c_str(), count); + current_path = path; + selected_child_entry = ""; + return true; + } else { + TT_LOG_E(TAG, "Failed to fetch entries for %s", path.c_str()); + return false; + } + } +} + +bool State::setEntriesForChildPath(const std::string& childPath) { + auto path = file::getChildPath(current_path, childPath); + TT_LOG_I(TAG, "Navigating from %s to %s", current_path.c_str(), path.c_str()); + return setEntriesForPath(path); +} + +bool State::getDirent(uint32_t index, dirent& dirent) { + auto lock = mutex.asScopedLock(); + if (!lock.lock(50 / portTICK_PERIOD_MS)) { + return false; + } + + if (index < dir_entries.size()) { + dirent = dir_entries[index]; + return true; + } else { + return false; + } +} + +} diff --git a/Tactility/Source/app/fileselection/View.cpp b/Tactility/Source/app/fileselection/View.cpp new file mode 100644 index 00000000..10513923 --- /dev/null +++ b/Tactility/Source/app/fileselection/View.cpp @@ -0,0 +1,215 @@ +#include "Tactility/app/fileselection/View.h" + +#include "Tactility/app/alertdialog/AlertDialog.h" +#include "Tactility/lvgl/Toolbar.h" +#include "Tactility/lvgl/LvglSync.h" + +#include +#include "Tactility/file/File.h" +#include + +#include +#include +#include + +#ifdef ESP_PLATFORM +#include "Tactility/service/loader/Loader.h" +#endif + +#define TAG "fileselection_app" + +namespace tt::app::fileselection { + +// region Callbacks + +static void onDirEntryPressedCallback(lv_event_t* event) { + auto* view = static_cast(lv_event_get_user_data(event)); + auto* button = lv_event_get_target_obj(event); + auto index = lv_obj_get_index(button); + view->onDirEntryPressed(index); +} + +static void onNavigateUpPressedCallback(TT_UNUSED lv_event_t* event) { + auto* view = static_cast(lv_event_get_user_data(event)); + view->onNavigateUpPressed(); +} + +// endregion + +void View::onTapFile(const std::string& path, const std::string& filename) { + std::string file_path = path + "/" + filename; + + // For PC we need to make the path relative to the current work directory, + // because that's how LVGL maps its 'drive letter' to the file system. + std::string processed_filepath; + if (kernel::getPlatform() == kernel::PlatformSimulator) { + char cwd[PATH_MAX]; + if (getcwd(cwd, sizeof(cwd)) == nullptr) { + TT_LOG_E(TAG, "Failed to get current working directory"); + return; + } + if (!file_path.starts_with(cwd)) { + TT_LOG_E(TAG, "Can only work with files in working directory %s", cwd); + return; + } + processed_filepath = file_path.substr(strlen(cwd)); + } else { + processed_filepath = file_path; + } + + TT_LOG_I(TAG, "Clicked %s", processed_filepath.c_str()); + + lv_textarea_set_text(path_textarea, processed_filepath.c_str()); +} + +void View::onDirEntryPressed(uint32_t index) { + dirent dir_entry; + if (state->getDirent(index, dir_entry)) { + TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type); + state->setSelectedChildEntry(dir_entry.d_name); + using namespace tt::file; + switch (dir_entry.d_type) { + case TT_DT_DIR: + case TT_DT_CHR: + state->setEntriesForChildPath(dir_entry.d_name); + lv_textarea_set_text(path_textarea, state->getCurrentPathWithTrailingSlash().c_str()); + update(); + break; + case TT_DT_LNK: + TT_LOG_W(TAG, "opening links is not supported"); + break; + case TT_DT_REG: + onTapFile(state->getCurrentPath(), dir_entry.d_name); + break; + default: + // Assume it's a file + // TODO: Find a better way to identify a file + onTapFile(state->getCurrentPath(), dir_entry.d_name); + break; + } + } +} + +void View::onSelectButtonPressed(lv_event_t* event) { + auto* view = static_cast(lv_event_get_user_data(event)); + const char* path = lv_textarea_get_text(view->path_textarea); + if (path == nullptr || strlen(path) == 0) { + TT_LOG_W(TAG, "Select pressed, but not path found in textarea"); + return; + } + + view->onFileSelected(std::string(path)); +} + +static bool isSelectableFilePath(const char* path) { + if (path == nullptr) { + return false; + } + + auto len = strlen(path); + if (len == 0) { + return false; + } + + return path[len - 1] != '/'; +} + +void View::onPathTextChanged(lv_event_t* event) { + auto* view = static_cast(lv_event_get_user_data(event)); + const char* path = lv_textarea_get_text(view->path_textarea); + if (isSelectableFilePath(path)) { + lv_obj_remove_flag(view->select_button, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(view->select_button, LV_OBJ_FLAG_HIDDEN); + } +} + +void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) { + tt_check(list); + const char* symbol; + if (dir_entry.d_type == file::TT_DT_DIR || dir_entry.d_type == file::TT_DT_CHR) { + symbol = LV_SYMBOL_DIRECTORY; + } else { + symbol = LV_SYMBOL_FILE; + } + lv_obj_t* button = lv_list_add_button(list, symbol, dir_entry.d_name); + lv_obj_add_event_cb(button, &onDirEntryPressedCallback, LV_EVENT_SHORT_CLICKED, this); +} + +void View::onNavigateUpPressed() { + if (state->getCurrentPath() != "/") { + TT_LOG_I(TAG, "Navigating upwards"); + std::string new_absolute_path; + if (string::getPathParent(state->getCurrentPath(), new_absolute_path)) { + state->setEntriesForPath(new_absolute_path); + } + if (new_absolute_path.length() > 1) { + lv_textarea_set_text(path_textarea, (new_absolute_path + "/").c_str()); + } else { + lv_textarea_set_text(path_textarea, new_absolute_path.c_str()); + } + update(); + } +} + +void View::update() { + auto scoped_lockable = lvgl::getSyncLock()->asScopedLock(); + if (scoped_lockable.lock(lvgl::defaultLockTime)) { + lv_obj_clean(dir_entry_list); + + state->withEntries([this](const std::vector& entries) { + for (auto entry : entries) { + TT_LOG_D(TAG, "Entry: %s %d", entry.d_name, entry.d_type); + createDirEntryWidget(dir_entry_list, entry); + } + }); + + if (state->getCurrentPath() == "/") { + lv_obj_add_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_remove_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN); + } + } else { + TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "lvgl"); + } +} + +void View::init(lv_obj_t* parent, Mode mode) { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + + auto* toolbar = lvgl::toolbar_create(parent, "Select File"); + navigate_up_button = lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_UP, &onNavigateUpPressedCallback, this); + + auto* wrapper = lv_obj_create(parent); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_style_border_width(wrapper, 0, 0); + lv_obj_set_style_pad_all(wrapper, 0, 0); + lv_obj_set_flex_grow(wrapper, 1); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW); + + dir_entry_list = lv_list_create(wrapper); + lv_obj_set_height(dir_entry_list, LV_PCT(100)); + lv_obj_set_flex_grow(dir_entry_list, 1); + + auto* bottom_wrapper = lv_obj_create(parent); + lv_obj_set_flex_flow(bottom_wrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_size(bottom_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_border_width(bottom_wrapper, 0, 0); + lv_obj_set_style_pad_all(bottom_wrapper, 0, 0); + + path_textarea = lv_textarea_create(bottom_wrapper); + lv_textarea_set_one_line(path_textarea, true); + lv_obj_set_flex_grow(path_textarea, 1); + service::gui::keyboardAddTextArea(path_textarea); + lv_obj_add_event_cb(path_textarea, onPathTextChanged, LV_EVENT_VALUE_CHANGED, this); + + select_button = lv_button_create(bottom_wrapper); + auto* select_button_label = lv_label_create(select_button); + lv_label_set_text(select_button_label, "Select"); + lv_obj_add_event_cb(select_button, onSelectButtonPressed, LV_EVENT_SHORT_CLICKED, this); + lv_obj_add_flag(select_button, LV_OBJ_FLAG_HIDDEN); + + update(); +} + +} diff --git a/Tactility/Source/app/log/Log.cpp b/Tactility/Source/app/log/Log.cpp index fbd858a2..f2877e05 100644 --- a/Tactility/Source/app/log/Log.cpp +++ b/Tactility/Source/app/log/Log.cpp @@ -91,10 +91,9 @@ public: updateLogEntries(); } - void onResult(AppContext& app, Result result, std::unique_ptr bundle) override { + void onResult(AppContext& app, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr bundle) override { if (result == Result::Ok && bundle != nullptr) { - auto resultIndex = selectiondialog::getResultIndex(*bundle); - switch (resultIndex) { + switch (selectiondialog::getResultIndex(*bundle)) { case 0: filterLevel = LogLevel::Verbose; break; diff --git a/Tactility/Source/app/notes/Notes.cpp b/Tactility/Source/app/notes/Notes.cpp index 301953cb..efb41d46 100644 --- a/Tactility/Source/app/notes/Notes.cpp +++ b/Tactility/Source/app/notes/Notes.cpp @@ -5,35 +5,30 @@ #include #include -#include -#include +#include +#include +#include namespace tt::app::notes { constexpr const char* TAG = "Notes"; class NotesApp : public App { + AppContext* appContext = nullptr; lv_obj_t* uiCurrentFileName; lv_obj_t* uiDropDownMenu; - lv_obj_t* uiFileList; - lv_obj_t* uiFileListCloseBtn; lv_obj_t* uiNoteText; - lv_obj_t* uiSaveDialog; - lv_obj_t* uiSaveDialogFileName; - lv_obj_t* uiSaveDialogSaveBtn; - lv_obj_t* uiSaveDialogCancelBtn; lv_obj_t* uiMessageBox; lv_obj_t* uiMessageBoxButtonOk; lv_obj_t* uiMessageBoxButtonNo; - char menuItem[32]; - uint8_t menuIdx = 0; - std::string fileContents; - std::string fileName; - std::string newFileName; std::string filePath; + std::string saveBuffer; + + LaunchId loadFileLaunchId = 0; + LaunchId saveFileLaunchId = 0; #pragma region Main_Events_Functions @@ -42,81 +37,56 @@ class NotesApp : public App { lv_obj_t* obj = lv_event_get_target_obj(e); if (code == LV_EVENT_CLICKED) { - if (obj == uiFileListCloseBtn) { - lv_obj_add_flag(uiFileList, LV_OBJ_FLAG_HIDDEN); - lv_obj_del(uiFileList); - } else if (obj == uiSaveDialogSaveBtn) { - newFileName = lv_textarea_get_text(uiSaveDialogFileName); - if (newFileName.length() == 0) { - uiMessageBoxShow(menuItem, "Filename is empty.", false); - } else { - std::string noteText = lv_textarea_get_text(uiNoteText); - filePath = appContext->getPaths()->getDataPath(newFileName); - - if (writeFile(filePath, noteText)) { - uiMessageBoxShow(menuItem, "File created successfully!", false); - lv_label_set_text(uiCurrentFileName, newFileName.c_str()); - } else { - uiMessageBoxShow(menuItem, "Something went wrong!\nFile creation failed.", false); - } - } - lv_obj_del(uiSaveDialog); - - } else if (obj == uiMessageBoxButtonOk || obj == uiMessageBoxButtonNo) { + if (obj == uiMessageBoxButtonOk || obj == uiMessageBoxButtonNo) { lv_obj_del(uiMessageBox); - } else if (obj == uiSaveDialogCancelBtn) { - lv_obj_del(uiSaveDialog); } } if (code == LV_EVENT_VALUE_CHANGED) { if (obj == uiDropDownMenu) { - lv_dropdown_get_selected_str(obj, menuItem, sizeof(menuItem)); - menuIdx = lv_dropdown_get_selected(obj); - std::string newContents = lv_textarea_get_text(uiNoteText); - if (menuIdx == 1) { //Save - //Normal Save? + switch (lv_dropdown_get_selected(obj)) { + case 0: // New + resetFileContent(); + break; + case 1: // Save + if (!filePath.empty()) { + lvgl::getSyncLock()->lock(); + saveBuffer = lv_textarea_get_text(uiNoteText); + lvgl::getSyncLock()->unlock(); + saveFile(filePath); + } + break; + case 2: // Save as... + lvgl::getSyncLock()->lock(); + saveBuffer = lv_textarea_get_text(uiNoteText); + lvgl::getSyncLock()->unlock(); + saveFileLaunchId = fileselection::startForExistingOrNewFile(); + TT_LOG_I(TAG, "launched with id %d", loadFileLaunchId); + break; + case 3: // Load + loadFileLaunchId = fileselection::startForExistingFile(); + TT_LOG_I(TAG, "launched with id %d", loadFileLaunchId); + break; } - if (menuIdx == 2) { //Save As... - uiSaveFileDialog(); - return; - } - - //Not working...more investigation needed. - //If note contents has changed in currently open file, save it. - - //bool newToSave = newContents != fileContents && newContents.length() != 0; - //if (newToSave) { - //uiMessageBoxShow(menuItem, "Do you want to save it?", true); - //} else { - menuAction(); - //} } else { - lv_obj_t* cont = lv_event_get_current_target_obj(e); + auto* cont = lv_event_get_current_target_obj(e); if (obj == cont) return; if (lv_obj_get_child(cont, 1)) { - uiSaveFileDialog(); + saveFileLaunchId = fileselection::startForExistingOrNewFile(); + TT_LOG_I(TAG, "launched with id %d", loadFileLaunchId); } else { //Reset - lv_textarea_set_text(uiNoteText, ""); - fileName = ""; - lv_label_set_text(uiCurrentFileName, "Untitled"); + resetFileContent(); } lv_obj_delete(uiMessageBox); } } } - void menuAction() { - switch (menuIdx) { - case 0: //Reset - lv_textarea_set_text(uiNoteText, ""); - fileName = ""; - lv_label_set_text(uiCurrentFileName, "Untitled"); - break; - case 3: - uiOpenFileDialog(); - break; - } + void resetFileContent() { + lv_textarea_set_text(uiNoteText, ""); + filePath = ""; + saveBuffer = ""; + lv_label_set_text(uiCurrentFileName, "Untitled"); } void uiMessageBoxShow(std::string title, std::string message, bool isSelectable) { @@ -166,166 +136,49 @@ class NotesApp : public App { lv_obj_t* buttonLabelOk = lv_label_create(uiMessageBoxButtonOk); lv_obj_align(buttonLabelOk, LV_ALIGN_BOTTOM_MID, 0, 0); lv_label_set_text(buttonLabelOk, "Ok"); - lv_obj_add_event_cb(uiMessageBoxButtonOk, [](lv_event_t* e) { + lv_obj_add_event_cb(uiMessageBoxButtonOk, + [](lv_event_t* e) { auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this); + self->appNotesEventCb(e); + }, + LV_EVENT_CLICKED, + this + ); + } + + if (!filePath.empty()) { + openFile(filePath); } } -#pragma endregion Main_Events_Functions - -#pragma region Save_Events_Functions - - void uiSaveFileDialog() { - uiSaveDialog = lv_obj_create(lv_scr_act()); - if (lv_display_get_horizontal_resolution(nullptr) <= 240 || lv_display_get_vertical_resolution(nullptr) <= 240) { //small screens - lv_obj_set_size(uiSaveDialog, lv_display_get_horizontal_resolution(nullptr), lv_display_get_vertical_resolution(nullptr) - 80); - } else { //large screens - lv_obj_set_size(uiSaveDialog, lv_display_get_horizontal_resolution(nullptr), lv_display_get_vertical_resolution(nullptr) - 230); - } - lv_obj_align(uiSaveDialog, LV_ALIGN_TOP_MID, 0, 0); - lv_obj_remove_flag(uiSaveDialog, LV_OBJ_FLAG_SCROLLABLE); - - lv_obj_t* uiSaveDialogTitle = lv_label_create(uiSaveDialog); - lv_label_set_text(uiSaveDialogTitle, menuItem); - lv_obj_set_size(uiSaveDialogTitle, lv_display_get_horizontal_resolution(nullptr) - 30, 30); - lv_obj_align(uiSaveDialogTitle, LV_ALIGN_TOP_MID, 0, 0); - - uiSaveDialogFileName = lv_textarea_create(uiSaveDialog); - lv_obj_set_size(uiSaveDialogFileName, lv_display_get_horizontal_resolution(nullptr) - 30, 40); - lv_obj_align_to(uiSaveDialogFileName, uiSaveDialogTitle, LV_ALIGN_TOP_MID, 0, 30); - lv_textarea_set_placeholder_text(uiSaveDialogFileName, "Enter file name..."); - lv_textarea_set_one_line(uiSaveDialogFileName, true); - lv_obj_add_state(uiSaveDialogFileName, LV_STATE_FOCUSED); - - //Both hardware and software keyboard not auto attaching here for some reason unless the textarea is touched to focus it... - tt::lvgl::keyboard_add_textarea(uiSaveDialogFileName); - - if (fileName != "" || fileName != "Untitled") { - lv_textarea_set_text(uiSaveDialogFileName, fileName.c_str()); - } else { - lv_textarea_set_placeholder_text(uiSaveDialogFileName, "filename?"); - } - - uiSaveDialogSaveBtn = lv_btn_create(uiSaveDialog); - lv_obj_add_event_cb(uiSaveDialogSaveBtn, [](lv_event_t* e) { - auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this); - lv_obj_align(uiSaveDialogSaveBtn, LV_ALIGN_BOTTOM_LEFT, 0, 0); - lv_obj_t* btnLabel = lv_label_create(uiSaveDialogSaveBtn); - lv_label_set_text(btnLabel, "Save"); - lv_obj_center(btnLabel); - - uiSaveDialogCancelBtn = lv_btn_create(uiSaveDialog); - lv_obj_add_event_cb(uiSaveDialogCancelBtn, [](lv_event_t* e) { - auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this); - lv_obj_align(uiSaveDialogCancelBtn, LV_ALIGN_BOTTOM_RIGHT, 0, 0); - lv_obj_t* btnLabel2 = lv_label_create(uiSaveDialogCancelBtn); - lv_label_set_text(btnLabel2, "Cancel"); - lv_obj_center(btnLabel2); - } - - bool writeFile(std::string path, std::string message) { - std::ofstream fileStream(path); - - if (!fileStream.is_open()) { - TT_LOG_E(TAG, "Failed to write file"); - return false; - } - if (fileStream.is_open()) { - fileStream << message; - TT_LOG_I(TAG, "File written successfully"); - fileStream.close(); - return true; - } - return true; - } - -#pragma endregion Save_Events_Functions - #pragma region Open_Events_Functions - void openFileEventCb(lv_event_t* e) { - lv_event_code_t code = lv_event_get_code(e); - lv_obj_t* obj = lv_event_get_target_obj(e); - - if (code == LV_EVENT_CLICKED) { - std::string selectedFile = lv_list_get_btn_text(uiFileList, obj); - fileName = selectedFile.substr(0, selectedFile.find(" (")); - std::string filePath = appContext->getPaths()->getDataPath(fileName); - fileContents = readFile(filePath.c_str()); - lv_textarea_set_text(uiNoteText, fileContents.c_str()); - lv_obj_add_flag(uiFileList, LV_OBJ_FLAG_HIDDEN); - lv_obj_del(uiFileList); - - lv_label_set_text(uiCurrentFileName, fileName.c_str()); - } + void openFile(const std::string& path) { + // We might be reading from the SD card, which could share a SPI bus with other devices (display) + hal::sdcard::withSdCardLock(path, [this, path]() { + auto data = file::readString(path); + if (data != nullptr) { + auto lock = lvgl::getSyncLock()->asScopedLock(); + lock.lock(); + lv_textarea_set_text(uiNoteText, reinterpret_cast(data.get())); + lv_label_set_text(uiCurrentFileName, path.c_str()); + filePath = path; + TT_LOG_I(TAG, "Loaded from %s", path.c_str()); + } + }); } - void uiOpenFileDialog() { - uiFileList = lv_list_create(lv_scr_act()); - lv_obj_set_size(uiFileList, lv_display_get_horizontal_resolution(nullptr), lv_display_get_vertical_resolution(nullptr)); - lv_obj_align(uiFileList, LV_ALIGN_TOP_MID, 0, 0); - lv_list_add_text(uiFileList, "Notes"); - - uiFileListCloseBtn = lv_btn_create(uiFileList); - lv_obj_set_size(uiFileListCloseBtn, 36, 36); - lv_obj_add_flag(uiFileListCloseBtn, LV_OBJ_FLAG_FLOATING); - lv_obj_align(uiFileListCloseBtn, LV_ALIGN_TOP_RIGHT, 10, 4); - lv_obj_add_event_cb(uiFileListCloseBtn, [](lv_event_t* e) { - auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this); - - lv_obj_t* uiFileListCloseLabel = lv_label_create(uiFileListCloseBtn); - lv_label_set_text(uiFileListCloseLabel, LV_SYMBOL_CLOSE); - lv_obj_center(uiFileListCloseLabel); - - lv_obj_add_flag(uiFileList, LV_OBJ_FLAG_HIDDEN); - - //TODO: Move this to SD Card? - std::vector noteFileList; - const std::string& path = appContext->getPaths()->getDataDirectory(); - DIR* dir = opendir(path.c_str()); - if (dir == nullptr) { - TT_LOG_E(TAG, "Failed to open dir %s", path.c_str()); - return; - } - - struct dirent* current_entry; - while ((current_entry = readdir(dir)) != nullptr) { - noteFileList.push_back(current_entry->d_name); - } - closedir(dir); - - if (noteFileList.size() == 0) return; - - for (std::vector::iterator item = noteFileList.begin(); item != noteFileList.end(); ++item) { - lv_obj_t* btn = lv_list_add_btn(uiFileList, LV_SYMBOL_FILE, (*item).c_str()); - lv_obj_add_event_cb(btn, [](lv_event_t* e) { - auto *self = static_cast(lv_event_get_user_data(e)); - self->openFileEventCb(e); }, LV_EVENT_CLICKED, this); - } - - lv_obj_move_foreground(uiFileListCloseBtn); - lv_obj_remove_flag(uiFileList, LV_OBJ_FLAG_HIDDEN); - } - - std::string readFile(std::string path) { - std::ifstream fileStream(path); - - if (!fileStream.is_open()) { - return ""; - } - - std::string temp = ""; - std::string file_contents; - while (std::getline(fileStream, temp)) { - file_contents += temp; - file_contents.push_back('\n'); - } - fileStream.close(); - return file_contents; + bool saveFile(const std::string& path) { + // We might be writing to SD card, which could share a SPI bus with other devices (display) + return hal::sdcard::withSdCardLock(path, [this, path]() { + if (file::writeString(path, saveBuffer.c_str())) { + TT_LOG_I(TAG, "Saved to %s", path.c_str()); + filePath = path; + return true; + } else { + return false; + } + }); } #pragma endregion Open_Events_Functions @@ -347,9 +200,14 @@ class NotesApp : public App { lv_obj_set_style_border_color(uiDropDownMenu, lv_color_hex(0xFAFAFA), LV_PART_MAIN); lv_obj_set_style_border_width(uiDropDownMenu, 1, LV_PART_MAIN); lv_obj_align(uiDropDownMenu, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_add_event_cb(uiDropDownMenu, [](lv_event_t* e) { + lv_obj_add_event_cb(uiDropDownMenu, + [](lv_event_t* e) { auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_VALUE_CHANGED, this); + self->appNotesEventCb(e); + }, + LV_EVENT_VALUE_CHANGED, + this + ); lv_obj_t* wrapper = lv_obj_create(parent); lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); @@ -387,11 +245,31 @@ class NotesApp : public App { lv_obj_align(uiCurrentFileName, LV_ALIGN_CENTER, 0, 0); //TODO: Move this to SD Card? - if (!tt::file::findOrCreateDirectory(context.getPaths()->getDataDirectory(), 0777)) { + if (!file::findOrCreateDirectory(context.getPaths()->getDataDirectory(), 0777)) { TT_LOG_E(TAG, "Failed to find or create path %s", context.getPaths()->getDataDirectory().c_str()); } - tt::lvgl::keyboard_add_textarea(uiNoteText); + lvgl::keyboard_add_textarea(uiNoteText); + } + + void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr resultData) override { + TT_LOG_I(TAG, "Result for launch id %d", launchId); + if (launchId == loadFileLaunchId) { + loadFileLaunchId = 0; + if (result == Result::Ok && resultData != nullptr) { + auto path = fileselection::getResultPath(*resultData); + openFile(path); + } + } else if (launchId == saveFileLaunchId) { + saveFileLaunchId = 0; + if (result == Result::Ok && resultData != nullptr) { + auto path = fileselection::getResultPath(*resultData); + // Must re-open file, because UI was cleared after opening other app + if (saveFile(path)) { + openFile(path); + } + } + } } }; @@ -401,4 +279,5 @@ extern const AppManifest manifest = { .icon = TT_ASSETS_APP_ICON_NOTES, .createApp = create }; + } // namespace tt::app::notes \ No newline at end of file diff --git a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp index a56ef480..f9290374 100644 --- a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp +++ b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp @@ -88,7 +88,7 @@ public: } } - void onResult(AppContext& app, Result result, std::unique_ptr bundle) override { + void onResult(AppContext& app, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr bundle) override { if (result == Result::Ok && bundle != nullptr) { auto name = timezone::getResultName(*bundle); auto code = timezone::getResultCode(*bundle); diff --git a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp index 8c08f91e..4b18cd01 100644 --- a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp +++ b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp @@ -112,7 +112,7 @@ class WifiApSettings : public App { } } - void onResult(TT_UNUSED AppContext& appContext, TT_UNUSED Result result, std::unique_ptr bundle) override { + void onResult(TT_UNUSED AppContext& appContext, TT_UNUSED LaunchId launchId, TT_UNUSED Result result, std::unique_ptr bundle) override { if (result == Result::Ok && bundle != nullptr) { auto index = alertdialog::getResultIndex(*bundle); if (index == 0) { // Yes diff --git a/Tactility/Source/lvgl/LabelUtils.cpp b/Tactility/Source/lvgl/LabelUtils.cpp index 78ffbb63..90d6a4e7 100644 --- a/Tactility/Source/lvgl/LabelUtils.cpp +++ b/Tactility/Source/lvgl/LabelUtils.cpp @@ -1,14 +1,18 @@ #include #include +#include namespace tt::lvgl { #define TAG "tt_lv_label" bool label_set_text_file(lv_obj_t* label, const char* filepath) { - auto text = file::readString(filepath); + auto text = hal::sdcard::withSdCardLock>(std::string(filepath), [filepath]() { + return file::readString(filepath); + }); + if (text != nullptr) { - lv_label_set_text(label, (const char*)text.get()); + lv_label_set_text(label, reinterpret_cast(text.get())); return true; } else { return false; diff --git a/Tactility/Source/service/loader/Loader.cpp b/Tactility/Source/service/loader/Loader.cpp index e8cd20bf..e7c343ab 100644 --- a/Tactility/Source/service/loader/Loader.cpp +++ b/Tactility/Source/service/loader/Loader.cpp @@ -47,17 +47,17 @@ static const char* appStateToString(app::State state) { class LoaderService final : public Service { -private: - std::shared_ptr pubsubExternal = std::make_shared(); Mutex mutex = Mutex(Mutex::Type::Recursive); std::stack> appStack; + app::LaunchId nextLaunchId = 0; + /** The dispatcher thread needs a callstack large enough to accommodate all the dispatched methods. * This includes full LVGL redraw via Gui::redraw() */ std::unique_ptr dispatcherThread = std::make_unique("loader_dispatcher", 6144); // Files app requires ~5k - void onStartAppMessage(const std::string& id, std::shared_ptr parameters); + void onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr parameters); void onStopAppMessage(const std::string& id); void transitionAppToState(const std::shared_ptr& app, app::State state); @@ -75,7 +75,7 @@ public: }); } - void startApp(const std::string& id, std::shared_ptr parameters); + app::LaunchId startApp(const std::string& id, std::shared_ptr parameters); void stopApp(); std::shared_ptr _Nullable getCurrentAppContext(); @@ -86,8 +86,7 @@ std::shared_ptr _Nullable optScreenshotService() { return service::findServiceById(manifest.id); } -void LoaderService::onStartAppMessage(const std::string& id, std::shared_ptr parameters) { - +void LoaderService::onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr parameters) { TT_LOG_I(TAG, "Start by id %s", id.c_str()); auto app_manifest = app::findAppById(id); @@ -103,7 +102,7 @@ void LoaderService::onStartAppMessage(const std::string& id, std::shared_ptr(app_manifest, parameters); + auto new_app = std::make_shared(app_manifest, launchId, parameters); new_app->mutableFlags().showStatusbar = (app_manifest->type != app::Type::Boot); @@ -123,7 +122,6 @@ void LoaderService::onStartAppMessage(const std::string& id, std::shared_ptrgetLaunchId(); + transitionAppToState(app_to_stop, app::State::Hiding); transitionAppToState(app_to_stop, app::State::Stopped); @@ -196,12 +196,14 @@ void LoaderService::onStopAppMessage(const std::string& id) { if (result_bundle != nullptr) { instance_to_resume->getApp()->onResult( *instance_to_resume, + app_to_stop_launch_id, result, std::move(result_bundle) ); } else { instance_to_resume->getApp()->onResult( *instance_to_resume, + app_to_stop_launch_id, result, nullptr ); @@ -210,6 +212,7 @@ void LoaderService::onStopAppMessage(const std::string& id) { const Bundle empty_bundle; instance_to_resume->getApp()->onResult( *instance_to_resume, + app_to_stop_launch_id, app::Result::Cancelled, nullptr ); @@ -255,10 +258,12 @@ void LoaderService::transitionAppToState(const std::shared_ptr app->setState(state); } -void LoaderService::startApp(const std::string& id, std::shared_ptr parameters) { - dispatcherThread->dispatch([this, id, parameters]() { - onStartAppMessage(id, parameters); +app::LaunchId LoaderService::startApp(const std::string& id, std::shared_ptr parameters) { + auto launch_id = nextLaunchId++; + dispatcherThread->dispatch([this, id, launch_id, parameters]() { + onStartAppMessage(id, launch_id, parameters); }); + return launch_id; } void LoaderService::stopApp() { @@ -278,11 +283,11 @@ std::shared_ptr _Nullable LoaderService::getCurrentAppContext() // region Public API -void startApp(const std::string& id, std::shared_ptr parameters) { +app::LaunchId startApp(const std::string& id, std::shared_ptr parameters) { TT_LOG_I(TAG, "Start app %s", id.c_str()); auto service = optScreenshotService(); assert(service); - service->startApp(id, std::move(parameters)); + return service->startApp(id, std::move(parameters)); } void stopApp() { diff --git a/TactilityC/Include/tt_app_manifest.h b/TactilityC/Include/tt_app_manifest.h index c41b90f7..0640b1d3 100644 --- a/TactilityC/Include/tt_app_manifest.h +++ b/TactilityC/Include/tt_app_manifest.h @@ -16,6 +16,8 @@ typedef enum { typedef void* AppHandle; +typedef unsigned int LaunchId; + /** Important: These function types must map to t::app types exactly */ typedef void* (*AppCreateData)(); typedef void (*AppDestroyData)(void* data); @@ -23,7 +25,7 @@ typedef void (*AppOnCreate)(AppHandle app, void* _Nullable data); typedef void (*AppOnDestroy)(AppHandle app, void* _Nullable data); typedef void (*AppOnShow)(AppHandle app, void* _Nullable data, lv_obj_t* parent); typedef void (*AppOnHide)(AppHandle app, void* _Nullable data); -typedef void (*AppOnResult)(AppHandle app, void* _Nullable data, Result result, BundleHandle resultData); +typedef void (*AppOnResult)(AppHandle app, void* _Nullable data, LaunchId launchId, Result result, BundleHandle resultData); typedef struct { /** The application's human-readable name */ diff --git a/TactilityC/Source/tt_app_manifest.cpp b/TactilityC/Source/tt_app_manifest.cpp index 4d77a2b8..d274cfd8 100644 --- a/TactilityC/Source/tt_app_manifest.cpp +++ b/TactilityC/Source/tt_app_manifest.cpp @@ -12,16 +12,16 @@ void tt_app_register( ) { #ifdef ESP_PLATFORM assert((manifest->createData == nullptr) == (manifest->destroyData == nullptr)); - tt::app::setElfAppManifest( + setElfAppManifest( manifest->name, manifest->icon, - (tt::app::CreateData)manifest->createData, - (tt::app::DestroyData)manifest->destroyData, - (tt::app::OnCreate)manifest->onCreate, - (tt::app::OnDestroy)manifest->onDestroy, - (tt::app::OnShow)manifest->onShow, - (tt::app::OnHide)manifest->onHide, - (tt::app::OnResult)manifest->onResult + manifest->createData, + manifest->destroyData, + manifest->onCreate, + manifest->onDestroy, + manifest->onShow, + manifest->onHide, + reinterpret_cast(manifest->onResult) ); #else tt_crash("TactilityC is not intended for PC/Simulator"); diff --git a/TactilityCore/Include/Tactility/file/File.h b/TactilityCore/Include/Tactility/file/File.h index 5f7d5b38..2aa83f1b 100644 --- a/TactilityCore/Include/Tactility/file/File.h +++ b/TactilityCore/Include/Tactility/file/File.h @@ -3,10 +3,34 @@ #include "Tactility/TactilityCore.h" #include +#include #include +#include namespace tt::file { +/** File types for `dirent`'s `d_type`. */ +enum { + TT_DT_UNKNOWN = 0, +#define TT_DT_UNKNOWN TT_DT_UNKNOWN // Unknown type + TT_DT_FIFO = 1, +#define TT_DT_FIFO TT_DT_FIFO // Named pipe or FIFO + TT_DT_CHR = 2, +#define TT_DT_CHR TT_DT_CHR // Character device + TT_DT_DIR = 4, +#define TT_DT_DIR TT_DT_DIR // Directory + TT_DT_BLK = 6, +#define TT_DT_BLK TT_DT_BLK // Block device + TT_DT_REG = 8, +#define TT_DT_REG TT_DT_REG // Regular file + TT_DT_LNK = 10, +#define TT_DT_LNK TT_DT_LNK // Symbolic link + TT_DT_SOCK = 12, +#define TT_DT_SOCK TT_DT_SOCK // Local-domain socket + TT_DT_WHT = 14 +#define TT_DT_WHT TT_DT_WHT // Whiteout inodes +}; + #ifdef _WIN32 constexpr char SEPARATOR = '\\'; #else @@ -36,6 +60,13 @@ std::unique_ptr readBinary(const std::string& filepath, size_t& outSi */ std::unique_ptr readString(const std::string& filepath); +/** Write text to a file + * @param[in] path file path to write to + * @param[in] content file content to write + * @return true when operation is successful + */ +bool writeString(const std::string& filepath, const std::string& content); + /** Ensure a directory path exists. * @param[in] path the directory path to find, or to create recursively * @param[in] mode the mode to use when creating directories @@ -43,4 +74,41 @@ std::unique_ptr readString(const std::string& filepath); */ bool findOrCreateDirectory(std::string path, mode_t mode); +/** + * Concatenate a child path with a parent path, ensuring proper slash inbetween + * @param basePath an absolute path with or without trailing "/" + * @param childPath the name of the child path (e.g. subfolder or file) + * @return the concatenated path + */ +std::string getChildPath(const std::string& basePath, const std::string& childPath); + +typedef int (*ScandirFilter)(const dirent*); + +typedef bool (*ScandirSort)(const dirent&, const dirent&); + +/** Used for sorting by alphanumeric value and file type */ +bool direntSortAlphaAndType(const dirent& left, const dirent& right); + +/** A filter for filtering out "." and ".." */ +int direntFilterDotEntries(const dirent* entry); + +/** + * A scandir()-like implementation that works on ESP32. + * It does not return "." and ".." items but otherwise functions the same. + * It returns an allocated output array with allocated dirent instances. + * The caller is responsible for free-ing the memory of these. + * + * @param[in] path path the scan for files and directories + * @param[out] outList a pointer to vector of dirent + * @param[in] filter an optional filter to filter out specific items + * @param[in] sort an optional sorting function + * @return the amount of items that were stored in "output" or -1 when an error occurred + */ +int scandir( + const std::string& path, + std::vector& outList, + ScandirFilter _Nullable filter, + ScandirSort _Nullable sort +); + } diff --git a/TactilityCore/Source/file/File.cpp b/TactilityCore/Source/file/File.cpp index 464ec835..8ca16dbc 100644 --- a/TactilityCore/Source/file/File.cpp +++ b/TactilityCore/Source/file/File.cpp @@ -1,9 +1,66 @@ #include "Tactility/file/File.h" +#include +#include + namespace tt::file { #define TAG "file" +std::string getChildPath(const std::string& basePath, const std::string& childPath) { + // Postfix with "/" when the current path isn't "/" + if (basePath.length() != 1) { + return basePath + "/" + childPath; + } else { + return "/" + childPath; + } +} + +int direntFilterDotEntries(const dirent* entry) { + return (strcmp(entry->d_name, "..") == 0 || strcmp(entry->d_name, ".") == 0) ? -1 : 0; +} + +bool direntSortAlphaAndType(const dirent& left, const dirent& right) { + bool left_is_dir = left.d_type == TT_DT_DIR || left.d_type == TT_DT_CHR; + bool right_is_dir = right.d_type == TT_DT_DIR || right.d_type == TT_DT_CHR; + if (left_is_dir == right_is_dir) { + return strcmp(left.d_name, right.d_name) < 0; + } else { + return left_is_dir > right_is_dir; + } +} + +int scandir( + const std::string& path, + std::vector& outList, + ScandirFilter _Nullable filterMethod, + ScandirSort _Nullable sortMethod +) { + TT_LOG_I(TAG, "scandir start"); + DIR* dir = opendir(path.c_str()); + if (dir == nullptr) { + TT_LOG_E(TAG, "Failed to open dir %s", path.c_str()); + return -1; + } + + dirent* current_entry; + while ((current_entry = readdir(dir)) != nullptr) { + if (filterMethod(current_entry) == 0) { + outList.push_back(*current_entry); + } + } + + closedir(dir); + + if (sortMethod != nullptr) { + std::ranges::sort(outList, sortMethod); + } + + TT_LOG_I(TAG, "scandir finish"); + return outList.size(); +} + + long getSize(FILE* file) { long original_offset = ftell(file); @@ -80,12 +137,25 @@ std::unique_ptr readBinary(const std::string& filepath, size_t& outSi std::unique_ptr readString(const std::string& filepath) { size_t size = 0; auto data = readBinaryInternal(filepath, size, 1); - if (data != nullptr) { - data.get()[size] = 0; // Append null terminator - return data; - } else { + if (data == nullptr) { return nullptr; } + + data.get()[size] = 0; // Append null terminator + return data; +} + +bool writeString(const std::string& filepath, const std::string& content) { + std::ofstream fileStream(filepath); + + if (!fileStream.is_open()) { + return false; + } + + fileStream << content; + fileStream.close(); + + return true; } static bool findOrCreateDirectoryInternal(std::string path, mode_t mode) { From ce96474d84f61bfe33715ad0ca2a84b4c7a3b3ab Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Tue, 27 May 2025 16:08:04 +1000 Subject: [PATCH 02/16] Cleanup of Notes.cpp (#282) --- Tactility/Source/app/notes/Notes.cpp | 76 ---------------------------- 1 file changed, 76 deletions(-) diff --git a/Tactility/Source/app/notes/Notes.cpp b/Tactility/Source/app/notes/Notes.cpp index efb41d46..b35e3f12 100644 --- a/Tactility/Source/app/notes/Notes.cpp +++ b/Tactility/Source/app/notes/Notes.cpp @@ -15,14 +15,9 @@ constexpr const char* TAG = "Notes"; class NotesApp : public App { - AppContext* appContext = nullptr; - lv_obj_t* uiCurrentFileName; lv_obj_t* uiDropDownMenu; lv_obj_t* uiNoteText; - lv_obj_t* uiMessageBox; - lv_obj_t* uiMessageBoxButtonOk; - lv_obj_t* uiMessageBoxButtonNo; std::string filePath; std::string saveBuffer; @@ -36,12 +31,6 @@ class NotesApp : public App { lv_event_code_t code = lv_event_get_code(e); lv_obj_t* obj = lv_event_get_target_obj(e); - if (code == LV_EVENT_CLICKED) { - if (obj == uiMessageBoxButtonOk || obj == uiMessageBoxButtonNo) { - lv_obj_del(uiMessageBox); - } - } - if (code == LV_EVENT_VALUE_CHANGED) { if (obj == uiDropDownMenu) { switch (lv_dropdown_get_selected(obj)) { @@ -77,7 +66,6 @@ class NotesApp : public App { } else { //Reset resetFileContent(); } - lv_obj_delete(uiMessageBox); } } } @@ -89,68 +77,6 @@ class NotesApp : public App { lv_label_set_text(uiCurrentFileName, "Untitled"); } - void uiMessageBoxShow(std::string title, std::string message, bool isSelectable) { - uiMessageBox = lv_obj_create(lv_scr_act()); - lv_obj_set_size(uiMessageBox, lv_display_get_horizontal_resolution(nullptr), lv_display_get_vertical_resolution(nullptr)); - lv_obj_align(uiMessageBox, LV_ALIGN_TOP_MID, 0, 0); - lv_obj_remove_flag(uiMessageBox, LV_OBJ_FLAG_SCROLLABLE); - - lv_obj_t* uiMessageBoxTitle = lv_label_create(uiMessageBox); - lv_label_set_text(uiMessageBoxTitle, title.c_str()); - lv_obj_set_size(uiMessageBoxTitle, lv_display_get_horizontal_resolution(nullptr) - 30, 30); - lv_obj_align(uiMessageBoxTitle, LV_ALIGN_TOP_MID, 0, 0); - - lv_obj_t* messageLabel = lv_label_create(uiMessageBox); - lv_obj_align(messageLabel, LV_ALIGN_CENTER, 0, 0); - lv_obj_set_width(messageLabel, LV_PCT(80)); - lv_obj_set_style_text_align(messageLabel, LV_TEXT_ALIGN_CENTER, 0); - lv_label_set_text(messageLabel, message.c_str()); - lv_label_set_long_mode(messageLabel, LV_LABEL_LONG_WRAP); - - lv_obj_t* buttonWrapper = lv_obj_create(uiMessageBox); - lv_obj_set_flex_flow(buttonWrapper, LV_FLEX_FLOW_ROW); - lv_obj_set_size(buttonWrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(buttonWrapper, 0, 0); - lv_obj_set_flex_align(buttonWrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_border_width(buttonWrapper, 0, 0); - lv_obj_align(buttonWrapper, LV_ALIGN_BOTTOM_MID, 0, 5); - - if (isSelectable == true) { - lv_obj_t* buttonYes = lv_button_create(buttonWrapper); - lv_obj_t* buttonLabelYes = lv_label_create(buttonYes); - lv_obj_align(buttonLabelYes, LV_ALIGN_BOTTOM_LEFT, 0, 0); - lv_label_set_text(buttonLabelYes, "Yes"); - lv_obj_add_event_cb(buttonYes, [](lv_event_t* e) { - auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this); - - uiMessageBoxButtonNo = lv_button_create(buttonWrapper); - lv_obj_t* buttonLabelNo = lv_label_create(uiMessageBoxButtonNo); - lv_obj_align(buttonLabelNo, LV_ALIGN_BOTTOM_RIGHT, 0, 0); - lv_label_set_text(buttonLabelNo, "No"); - lv_obj_add_event_cb(uiMessageBoxButtonNo, [](lv_event_t* e) { - auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this); - } else { - uiMessageBoxButtonOk = lv_button_create(buttonWrapper); - lv_obj_t* buttonLabelOk = lv_label_create(uiMessageBoxButtonOk); - lv_obj_align(buttonLabelOk, LV_ALIGN_BOTTOM_MID, 0, 0); - lv_label_set_text(buttonLabelOk, "Ok"); - lv_obj_add_event_cb(uiMessageBoxButtonOk, - [](lv_event_t* e) { - auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); - }, - LV_EVENT_CLICKED, - this - ); - } - - if (!filePath.empty()) { - openFile(filePath); - } - } - #pragma region Open_Events_Functions void openFile(const std::string& path) { @@ -184,8 +110,6 @@ class NotesApp : public App { #pragma endregion Open_Events_Functions void onShow(AppContext& context, lv_obj_t* parent) override { - appContext = &context; - lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); From e4ecec64c92ef4816c6e156fc2b497d1f4b67e8f Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sun, 1 Jun 2025 17:11:29 +0200 Subject: [PATCH 03/16] Reduced desktop icon sizes (#283) This improves loading performance (and uses less memory on ESP32 devices without PSRAM) --- Data/system/app/Launcher/icon_apps.png | Bin 564 -> 1430 bytes Data/system/app/Launcher/icon_files.png | Bin 724 -> 421 bytes Data/system/app/Launcher/icon_settings.png | Bin 2690 -> 1812 bytes Data/system_sources/app/Desktop/apps.svg | 1 - Data/system_sources/app/Desktop/folder.svg | 1 - Data/system_sources/app/Desktop/settings.svg | 1 - Data/system_sources/app/Launcher/apps.svg | 42 ++++++++++++++++++ Data/system_sources/app/Launcher/folder.svg | 42 ++++++++++++++++++ Data/system_sources/app/Launcher/settings.svg | 42 ++++++++++++++++++ Documentation/ideas.md | 1 - Tactility/Source/app/launcher/Launcher.cpp | 16 ++++--- 11 files changed, 136 insertions(+), 10 deletions(-) delete mode 100644 Data/system_sources/app/Desktop/apps.svg delete mode 100644 Data/system_sources/app/Desktop/folder.svg delete mode 100644 Data/system_sources/app/Desktop/settings.svg create mode 100644 Data/system_sources/app/Launcher/apps.svg create mode 100644 Data/system_sources/app/Launcher/folder.svg create mode 100644 Data/system_sources/app/Launcher/settings.svg diff --git a/Data/system/app/Launcher/icon_apps.png b/Data/system/app/Launcher/icon_apps.png index 9381aaa4404943d96a827d38dd66a41498224af7..d750027683a4553f8b222512bb42275f73f52701 100644 GIT binary patch delta 1410 zcmV-|1%3Lo1eOaniBL{Q4GJ0x0000DNk~Le0000e0000e2nGNE0F3^)Z2$lO32;bR za{vGsn*ab9n*m96gOrgrCw~PaNkl~LIxl3#rVI09svnrgsy&&eH)*H(r=_BQ^eMYxFso4rEexcI)mvaaTw z#oRnZHhP8Z*UfafTz~CR;U}OiMp+}!Cp<5IDypqKt0G^;)`Xje`f5`{b|jhuK=)W# zWH7-TK!!FmXw{1BPOi!Fx|lbOsPv?C1m#i76oTmbbgkOPZzp_TQ|hiah^258QY=QF zlr##Z6^Znjnwn|@Olnc2-JndiVDM5`7ynxLbgjrlGu7GI`G1eXAE{c^31g7Mjn$ge zPY&An0Id8Xe;SNgdGt`4JwXl|$I1Q-i7)JD9d7oBt)auoeF0bvgcx<*ymh5|PW~`( zEY6%FL#~?}H;vBOVMI0}oN7E65V)i&y`GcXGRxTurpa@PAYOJ+)5HrE?1vcVJEOAq|S;m;<~x-(zI9o>Lvqy zs;ok&p>h({eV&&)$z0rOM5gy+4*oIE$qmF6NQg`dx_`%W^84bo(_^6hmZmze&2w@` zrCgp~6A+xXhJg^Gy47{_A2svgll#_UZqB5rKIOXk@0%O0`jwXj`5>+)P!l!lj1mka znQK&$U3Q+G3FZJKh|vmmegQHn!GO_LVh?ox4ojjA)3st5zgT1`@K6hN*P67TkW#$` zZS*9#)qmA;O*I0)wa{_mLAYh%Q?-gj`h;U$Xrb;|Bbd?ff)fi7dB@5tBPlbW66lCs zYv&hWMpO=%@M^d{T-r8_x`ph=wuZ(&r+Z>dHg1Go{s+4@|6xK@s{yaNZgyfWER;%> z$7nNy;T<9aG9elqjLUNiQ7%`W3`BMb`Uv5)N`Iguapo3ltIw%OA82=IY+Q{FcjWV@ zncb-GX595>@jZig3Y)?9si~=Dw=-C=$WrTx(*nYpkvsI2h>;~3_=j&xf$2y$+ zdr6lj<`4dzKzB$@it0$Xl_bHG{7N5~e$XyJQGUe)d%Z&8IP-CX3T{h6zw(0+M*I)6Jm>uC$GF}}_i6+O{{!PC~nBhrd?+``SE zoJkWJIAPi}*bm%H=%B12vw z`;Bdc@M^d%CQr^NY@1Cb5pAJx9Aqdas@*_>7hU(hKj$`T-}hJ4#mxR2qMd*VfkT6? zn@gR>a{2nhD#1Xw$pf4bl>@GujeNX}YNsDpv=_r_lo>(Ch2UMsaf$%{0og@*$Gey0 QF8}}l07*qoM6N<$f~o|iY5)KL literal 564 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=oCO|{#S9GUaUjfi{$NBOP*AeO zHKHUqKdq!Zu_%?Hyu4g5GcUV1Ik6yBFTW^#_B$IX1_s81o-U3d6?5LsIP1%7DAMx$ z=E{VpF_So~82XlQ{NfOuXyhPrP}Gst@!|7?T-J|vdTUrttgc|pTU@&B zqE$c6TWn)A6Jxl|AkE#7+tAB;!0f;=rUdB(&?T~eQy;lnyDn9$`d=HnxF$?dxyBi-U{5Q$5+4Xb+=Qg|JD~uIZ zu3;BvXE>VJw0}kD{KXaQHrw7NP5Qv#@U3Un^gKbQidCNWE+6?StP8&B@-b`*u8)~r z*I~Hc;bY>%klkEgf8M-w@X6PG7h0^Y-@U#}evw5(<>j1(MSIHwo@jmlxACiU&oA*0 zdTSMK@aXsTUcOto_Lt0JCJsfL>Jsmcn1_$cLONose3xH-`DJNXZQsTCCR@W}H~s!{ zGWe+6?YmiK`l5QD%z^la_wTdmG5|y28heC%Lal@4o8NoBGJ-fCWNYiTl}xCezuoS@ vFNW95YxsY7*7kp8%wf*aAxi^lI6o6Tp(UA^+2nKp7~>3{u6{1-oD!M1*HQviBL{Q4GJ0x0000DNk~Le0000e0000e2nGNE0F3^)Z2$lO32;bR za{vGsn*ab9n*m96gOrgrCw~DlNkl1+X zQn?D;0I3I{m!NCiDA2+Lw0R6r1Pc{?iPFLUW=nqhCP%TTD!W|NHegc$Xe|bq%@%!R zb*;((q5HoPdf~G4sv#fZ{k*TTya&m)AJZH%QdtdcH~_iyMR{Hq>VIbuFsNr#Gf0{G z@)e=8-f&sB1H5lK=zW(lM2u0*m@! z>m*ZI4bAspvq(I5z%sNf1jy4EvVu=axR%r#W`>blx?%V n3^!D9?p!jF@J4Mc0IhuhrKV!Argf*m00000NkvXXu0mjfd-ADx delta 698 zcmZ3=e1+AlGr-TCmrII^fq{Y7)59eQNIQTq2OE%Fm05TXNO2Z;L>4nJsKp>zNWgT^vIy=DeM~G4FDKNZWs5_0)wyB2QMkIB7Y5D|6z}i51A<;S~;+Z=AR^ z)j8>zpw>dAgtSMDGhVQ12~C`kRddjYmF?24cE+}#*y&4t{L+6?_xS5c+x_;c-!>|B zuS%MixsiDTC({ka8!U`(7;>5z${5TX8LSy*e-klH%B|nGC`xCl(%j2tS(@K^U)tKg zeO{}m-g_u>R&4OZ_6EM!8Oyq=YcJnkle~V{3Xusna(o-y85UVd&okxpWAyxFIxXXq z*nvg;%hElYJ}YUwJ(*i!RmmW5&}~xM?0w(nvuY57XF@hw*J*&*zqw@0WjZTIHmEsHm9{$3&GsPVGWd&j%EmXXgk#@jBo&E6qHXT}rlf zrI1Tl?CHxrhqekG|M>1+TwUm`r>XI_EIXJVoVsY*dTiaTW2_7Z5-!NrGfVuwWxw^N zM?a^4L(JCV|0WVF8?c9$LW69BufPGm18N)z>;J}OKv;S^R=N9?# zD5|`UIpo8sd>Jn5b*|eR4!7zu{EmBUa{BqF4V#}@JrKE2{eJ%TMG@=)6}?w^T34*& zXX}=aW!KhkI`(B=$D=l;{6DFmyBXHIe%)m#J}>N=&&NwsB4+DPPu;jgH*$sfBKaj( yIz7IeoAHjXLD;`Qit)c`u{}FlG=rRdmVfTO)pdW$9V~(AkipZ{&t;ucLK6T0u`A;M diff --git a/Data/system/app/Launcher/icon_settings.png b/Data/system/app/Launcher/icon_settings.png index cdb52d7b433d1e53d2cf0d3013de212eb0ecb2a0..ba33da9f31d361256bc0f14e1dfa5185ad72ea4b 100644 GIT binary patch delta 1795 zcmV+e2mJVg6_gG(iBL{Q4GJ0x0000DNk~Le0000e0000e2nGNE0F3^)Z2$lO32;bR za{vGsn*ab9n*m96gOrgrCw~T^Nkl6~}+)+qDyDntI1OJL9$p2YR7> zh#(LN1W3SxtF#Ii(SSf8B}D=n)W#*yBoA$rwx~!%YFt7dxK@HHNVEktN>Hk*Jhz>7Yp03r-8nq$ZkU}N+q+IB{!jD$&-u>z&o|%Ye19XV%4*<;wZ}p4 z>Cydh$F_E@CXZQ7rzVooo?TUX_s!r61on|s^#37{ev)ojZ4z0{9Gse(9WD+oMk(r& zV_R1NNt+15I94dN+ly<8!`H80ANQLpZv@2;;|V{iZCz@F&6@d+(G&PjS{#5jxyvXl zEG#vO#*b=S{V<+rhJRW?U=YN= z*u!5-bAA{f0Pn9<6LMH#SXI>+-FSw{Gdy@M62FJF>U3chQ*1>g$&N;RYu0-6kYEL2g%=OeFdEwdL|Fy$=dZSF5*}M2-Wa z2ro#YhbxugV?e_XY^7gVPKH~X?GvfVA0jxQTDs@)-~ zPXgEEIw%V&`gPYH{@!wNf*`(4MSlunXH1?k$TnaqVlYh3 znGz7u-I<tb&jH_oC(!+w5Ux6yxv8Of{Ne2d0I2AooAg_}8jD1Qa^^$tk1y$23zk`~WoU zLtkBuJenq&i$qR!iBt~yQSF!KUwf?tAVCm+C_#UoQ=}#!k2-c~f6mrJ5X56D9Pg?r z^D*w&)=H+vhJT{+#Y)9Gxm;E+tlci?lfY1}gR-E=eQu?6 zB-h@;GDqDRBPtJd$CaHci34!$(ut%#a3jdKfR}+cK<9yfgB%tLx2z-%pi;3;DzY=n zi^~4~%B?uQwU{Kea=ARcQqnw3PtTS`8tZZj+`Mw@_l|9S&;SjUWIXMy=q5jmw|}>q zg&?SY(vRZjrNQ4CYJTg*@vVJ}t5)A-ii^LKLUP&swxvDq{4n005tpdZUCqVc zI!U}ReoEleDJKtP0`x06e-SP!azN{szB@89^2V~lrl;R3#kKbcx*vE;#(YMR?+ZGf zUIzkCRBUSp>7&l`W=5ru{4?h&xPKf1;HWmtq3maC#T##3FDre-oHaCVvTfTVy|h~i z4ldiZ8x>%e6q8S74aOvQw(iCKh(lE#X^W$4?@1QFom0Hhy14%Y;XYud%c{J+ixcho z)ge#=#s!{e#mst4&b?mh*e^Q-I*xNeME0bpv#PSEzA$jTV_SDQW$TbO27hnL-oOy) z7Kmg8{%~<&;6}%`CMveIBdHI(4P+n6`Tuok?7ra!<~!fByWA2#j1M95ywkjsBw2^) zD0;6^^QRQ`7ss~VllBS1_+JV)x7&#%?;9zXJL<|73PDi)L}KIvZrS=~mzX!@_nQ(s z6)Rnx7(a|Jq!-+3f4{9tAVkf*>viDj9^Gdh+giVxJXcb%_EQs{U2T#p5U9NiRO!3G lZQ}|An%FpYM4;pZ6VNW`BH?A;#N*@y5pq z7;gv08z1=uBpYoY=enO0EY^-jR+4rM%m;ENPQ+27aK9VBKlnJgFsF?9d{T-TkF%NN$!d4KO91a*PYnJpA%$BAVK zbq#@B-g};as{-Uc0&On~+1ZxK90CB_woMXEKVryl12iGPf5I}f87lpDzHk(v3rAA9 zp9#N8u-s%AMh>Wah=dgD7`zg|h!4QP3$2EBb=;U@C%|_7Yry>H2o?t6MKE;%yb*w| zX2Q#@hPG5`!+-I1fpA5Lb`Qi4fT#nED-bmViN0j%>1QK+{Q$9sz?(o5Ge1~^jlk#t z_&$ULKWOmZ5gLv~v}pisuGpwC|FC@I=urfFUYI|%p*j9-1k8%VTLVQf?F(z9N?UR4 zX6V9moBj2%IBN|7$8jvC;YNVT_9MzimF6$enKiStEHu-2LK$WXC6bY1>pt& zlZTv>DSx`IEuB7I%ZVuDay{)L!Ot-GvO(MZOyVh!{8?)zvkw4zdU{&=i{&E#83A!A zNiJ&B_1DAnMl#w9f!g27elw0NeGy2Id`#rhi(7b{T+j9Wexv za2$UQ5l@puNhaw~sdUh?tW#srY3IE+0L-mwA-ciR)4x**DHCAZ_GA*N!wghyScqw@ zX{LWZ8V?<(cP3N$hhSV=S=L72eE_?NvDq}WsCgwqzrUJ^t9&EiZI;E0RS8y3f=sD9 zRe!-(+T_D_zOWuK&H}?>yWR#y`4E5`6~P0*L;zo8!X7*CZHv2(4wH(U^D@sM{U>t!|~qUbR6ew$9o8jhd^kG!w=x5=BeIaySuw5)?##B zcZ!|&eow@mif#80-WiOk3d#hu87;dZi+}S$PznJ6n3qo|miODX_vt7H9mih{!iFdw zrFvlLq;sZhi`LyUuf*bY0ACGdB6L9(=eHRxyOqr~HbTO2y}OxlH-JP4Pmu`UH+Aiy zY6l&QDR~sYxlu-a=-{FZ|Eebdl7>M5bsTYnNOp|V_!05N)kv{b2N)u}ralF$~%CMv#_>e2n- zxEo~Au*z+{2}nXQ1enkgq!p9N$YW?d5RkbzHVN=i=LRiy0$kUf=H$IyPz(Wps?P1_ zO`YlkI9!i2Az&f+?+TkUs~CZ_llPv>X8S@bQbEK@fa4UFm09d(Kv~Np(0>xOB>yq4 zBN2F2HK`Hkvr3D==S8ykhU0iur&twgB|tc?cMlUi55NrJInAg(fB*nw>0tox#&IOn z0brAgK4|lFh}b8X@RH+t-$RWd5q=!DZ4-&q4ltI52{Uj+uvlT5EeFDQBF^cxLHt`w z&wL|Px6?CEl6)$%UP<()34clY)==>#2xSk*=6jYxr1nPz4`64K^}W&U*t`XX|^0ll3V~_XDAc%wf#l;M%Hx~sG5}tAePGjRPDY7HTE;pI?K?m z*L6L-UN;P_rnV7;4eFbpY6Ad%ak5vtvc0{%YQ+gc*Y!Tj(5`2ub$f(UYiiY1cs7Epj$y$ z7$V+BM0c2adK>CIqyR`du75K#{{-Q;QhSN$F=i|R_`AVIW`8``YHGJf897T=V|0`V z4-7OVp00H}bmBx`CY35($0TpBzACQ^PE(R*sfNpN)Z7Z2rfDvffjS|Jb1Wl$zh!D2 zT*jpp{mO&ZPQyr7y*3NASJy1Tn4PMX|&3W1M8%-=N4%yyN2-}k2ul=((5)&lq>fcMG?Ts%$F{u?)@y59-f zuD1$he#^2lwYnl4$6L%)T`^7yo{`C9P7J4I3x$h?q(jvXVo8jI4a|FI2OlAJA;m)fx!c3r!3w*TEXCh z>PFx!{4g \ No newline at end of file diff --git a/Data/system_sources/app/Desktop/folder.svg b/Data/system_sources/app/Desktop/folder.svg deleted file mode 100644 index 021db149..00000000 --- a/Data/system_sources/app/Desktop/folder.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Data/system_sources/app/Desktop/settings.svg b/Data/system_sources/app/Desktop/settings.svg deleted file mode 100644 index 63cebb1c..00000000 --- a/Data/system_sources/app/Desktop/settings.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Data/system_sources/app/Launcher/apps.svg b/Data/system_sources/app/Launcher/apps.svg new file mode 100644 index 00000000..4ec70006 --- /dev/null +++ b/Data/system_sources/app/Launcher/apps.svg @@ -0,0 +1,42 @@ + + + + + + diff --git a/Data/system_sources/app/Launcher/folder.svg b/Data/system_sources/app/Launcher/folder.svg new file mode 100644 index 00000000..167f0acc --- /dev/null +++ b/Data/system_sources/app/Launcher/folder.svg @@ -0,0 +1,42 @@ + + + + + + diff --git a/Data/system_sources/app/Launcher/settings.svg b/Data/system_sources/app/Launcher/settings.svg new file mode 100644 index 00000000..c442c05f --- /dev/null +++ b/Data/system_sources/app/Launcher/settings.svg @@ -0,0 +1,42 @@ + + + + + + diff --git a/Documentation/ideas.md b/Documentation/ideas.md index 15188b10..b46f8e6b 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -65,6 +65,5 @@ - GPS app - Investigate CSI https://stevenmhernandez.github.io/ESP32-CSI-Tool/ - Compile unix tools to ELF apps? -- Text editor - Todo list - Calendar diff --git a/Tactility/Source/app/launcher/Launcher.cpp b/Tactility/Source/app/launcher/Launcher.cpp index 4fd4be01..a61a89ae 100644 --- a/Tactility/Source/app/launcher/Launcher.cpp +++ b/Tactility/Source/app/launcher/Launcher.cpp @@ -27,33 +27,37 @@ static lv_obj_t* createAppButton(lv_obj_t* parent, const char* title, const char auto* apps_button = lv_button_create(wrapper); lv_obj_set_style_pad_hor(apps_button, 0, 0); lv_obj_set_style_pad_top(apps_button, 0, 0); - lv_obj_set_style_pad_bottom(apps_button, 16, 0); + lv_obj_set_style_pad_bottom(apps_button, 8, 0); lv_obj_set_style_shadow_width(apps_button, 0, 0); lv_obj_set_style_border_width(apps_button, 0, 0); lv_obj_set_style_bg_opa(apps_button, 0, LV_PART_MAIN); auto* button_image = lv_image_create(apps_button); lv_image_set_src(button_image, imageFile); - lv_obj_add_event_cb(apps_button, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)appId); - lv_obj_set_style_image_recolor(button_image, lv_theme_get_color_primary(parent), 0); - lv_obj_set_style_image_recolor_opa(button_image, LV_OPA_COVER, 0); + lv_obj_set_style_image_recolor(button_image, lv_theme_get_color_primary(parent), LV_STATE_DEFAULT); + lv_obj_set_style_image_recolor_opa(button_image, LV_OPA_COVER, LV_STATE_DEFAULT); // Ensure buttons are still tappable when asset fails to load + // Icon images are 40x40, so we get some extra padding too lv_obj_set_size(button_image, 64, 64); auto* label = lv_label_create(wrapper); lv_label_set_text(label, title); lv_obj_align(label, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_obj_add_event_cb(wrapper, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)appId); + lv_obj_add_event_cb(apps_button, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)appId); + lv_obj_add_event_cb(label, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)appId); + return wrapper; } class LauncherApp : public App { void onCreate(TT_UNUSED AppContext& app) override { - auto* config = tt::getConfiguration(); + auto* config = getConfiguration(); if (!config->autoStartAppId.empty()) { TT_LOG_I(TAG, "auto-starting %s", config->autoStartAppId.c_str()); - tt::service::loader::startApp(config->autoStartAppId); + service::loader::startApp(config->autoStartAppId); } } From 3dfc27e93ee75c935ab200d1d346aacf1c815a79 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sun, 1 Jun 2025 17:52:09 +0200 Subject: [PATCH 04/16] Fixes for colours and margins in GPIO app and more (#284) - Fixes for colours and margins in GPIO app - Removed unused imports --- Tactility/Include/Tactility/lvgl/Color.h | 9 ++++++ Tactility/Include/Tactility/lvgl/LabelUtils.h | 2 +- Tactility/Include/Tactility/lvgl/Lvgl.h | 5 +++ Tactility/Include/Tactility/lvgl/Spinner.h | 2 +- Tactility/Include/Tactility/lvgl/Style.h | 2 +- Tactility/Include/Tactility/lvgl/Toolbar.h | 2 +- .../Tactility/app/i2cscanner/I2cScanner.h | 8 ----- Tactility/Source/app/gpio/Gpio.cpp | 32 +++++++++---------- Tactility/Source/lvgl/Color.cpp | 13 ++++++++ 9 files changed, 47 insertions(+), 28 deletions(-) create mode 100644 Tactility/Include/Tactility/lvgl/Color.h create mode 100644 Tactility/Include/Tactility/lvgl/Lvgl.h create mode 100644 Tactility/Source/lvgl/Color.cpp diff --git a/Tactility/Include/Tactility/lvgl/Color.h b/Tactility/Include/Tactility/lvgl/Color.h new file mode 100644 index 00000000..67a58e98 --- /dev/null +++ b/Tactility/Include/Tactility/lvgl/Color.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +lv_color_t lv_color_foreground(); + +lv_color_t lv_color_background(); + +lv_color_t lv_color_background_darkest(); diff --git a/Tactility/Include/Tactility/lvgl/LabelUtils.h b/Tactility/Include/Tactility/lvgl/LabelUtils.h index 04cc892f..2e508e42 100644 --- a/Tactility/Include/Tactility/lvgl/LabelUtils.h +++ b/Tactility/Include/Tactility/lvgl/LabelUtils.h @@ -1,6 +1,6 @@ #pragma once -#include "lvgl.h" +#include namespace tt::lvgl { diff --git a/Tactility/Include/Tactility/lvgl/Lvgl.h b/Tactility/Include/Tactility/lvgl/Lvgl.h new file mode 100644 index 00000000..53117f26 --- /dev/null +++ b/Tactility/Include/Tactility/lvgl/Lvgl.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +#include "./Colors.h" diff --git a/Tactility/Include/Tactility/lvgl/Spinner.h b/Tactility/Include/Tactility/lvgl/Spinner.h index 71bd173c..39b20008 100644 --- a/Tactility/Include/Tactility/lvgl/Spinner.h +++ b/Tactility/Include/Tactility/lvgl/Spinner.h @@ -1,4 +1,4 @@ -#include "lvgl.h" +#include namespace tt::lvgl { diff --git a/Tactility/Include/Tactility/lvgl/Style.h b/Tactility/Include/Tactility/lvgl/Style.h index dd89dd07..cb97c811 100644 --- a/Tactility/Include/Tactility/lvgl/Style.h +++ b/Tactility/Include/Tactility/lvgl/Style.h @@ -1,6 +1,6 @@ #pragma once -#include "lvgl.h" +#include namespace tt::lvgl { diff --git a/Tactility/Include/Tactility/lvgl/Toolbar.h b/Tactility/Include/Tactility/lvgl/Toolbar.h index eac3d747..733f1474 100644 --- a/Tactility/Include/Tactility/lvgl/Toolbar.h +++ b/Tactility/Include/Tactility/lvgl/Toolbar.h @@ -1,7 +1,7 @@ #pragma once -#include "lvgl.h" #include "../app/AppContext.h" +#include namespace tt::lvgl { diff --git a/Tactility/Private/Tactility/app/i2cscanner/I2cScanner.h b/Tactility/Private/Tactility/app/i2cscanner/I2cScanner.h index e83fbd13..0baa9eb3 100644 --- a/Tactility/Private/Tactility/app/i2cscanner/I2cScanner.h +++ b/Tactility/Private/Tactility/app/i2cscanner/I2cScanner.h @@ -1,13 +1,5 @@ #pragma once -#include -#include -#include -#include "lvgl.h" -#include -#include "Timer.h" -#include - namespace tt::app::i2cscanner { void start(); diff --git a/Tactility/Source/app/gpio/Gpio.cpp b/Tactility/Source/app/gpio/Gpio.cpp index 892704ef..9524642f 100644 --- a/Tactility/Source/app/gpio/Gpio.cpp +++ b/Tactility/Source/app/gpio/Gpio.cpp @@ -1,10 +1,10 @@ #include "Tactility/service/loader/Loader.h" -#include "Tactility/lvgl/Toolbar.h" #include #include +#include "Tactility/lvgl/Toolbar.h" #include +#include #include -#include #include namespace tt::app::gpio { @@ -13,10 +13,8 @@ extern const AppManifest manifest; class GpioApp : public App { -private: - - lv_obj_t* lvPins[GPIO_NUM_MAX] = {0 }; - uint8_t pinStates[GPIO_NUM_MAX] = {0 }; + lv_obj_t* lvPins[GPIO_NUM_MAX] = { nullptr }; + uint8_t pinStates[GPIO_NUM_MAX] = { 0 }; std::unique_ptr timer; Mutex mutex; @@ -40,7 +38,7 @@ void GpioApp::updatePinStates() { // Update pin states for (int i = 0; i < GPIO_NUM_MAX; ++i) { #ifdef ESP_PLATFORM - pinStates[i] = gpio_get_level((gpio_num_t)i); + pinStates[i] = gpio_get_level(static_cast(i)); #else pinStates[i] = gpio_get_level(i); #endif @@ -60,9 +58,9 @@ void GpioApp::updatePinWidgets() { if (reinterpret_cast(level) != label_user_data) { lv_obj_set_user_data(label, reinterpret_cast(level)); if (level == 0) { - lv_obj_set_style_text_color(label, lv_color_black(), 0); + lv_obj_set_style_text_color(label, lv_color_background_darkest(), LV_STATE_DEFAULT); } else { - lv_obj_set_style_text_color(label, lv_color_make(0, 200, 0), 0); + lv_obj_set_style_text_color(label, lv_color_make(0, 200, 0), LV_STATE_DEFAULT); } } } @@ -71,8 +69,8 @@ void GpioApp::updatePinWidgets() { lv_obj_t* GpioApp::createGpioRowWrapper(lv_obj_t* parent) { lv_obj_t* wrapper = lv_obj_create(parent); - lv_obj_set_style_pad_all(wrapper, 0, 0); - lv_obj_set_style_border_width(wrapper, 0, 0); + lv_obj_set_style_pad_all(wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_size(wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT); return wrapper; } @@ -87,7 +85,7 @@ void GpioApp::onTimer() { void GpioApp::startTask() { mutex.lock(); assert(timer == nullptr); - timer = std::make_unique(Timer::Type::Periodic, [this]() { + timer = std::make_unique(Timer::Type::Periodic, [this] { onTimer(); }); timer->start(100 / portTICK_PERIOD_MS); @@ -113,7 +111,7 @@ void GpioApp::onShow(AppContext& app, lv_obj_t* parent) { auto* wrapper = lv_obj_create(parent); lv_obj_set_width(wrapper, LV_PCT(100)); lv_obj_set_flex_grow(wrapper, 1); - lv_obj_set_style_border_width(wrapper, 0, 0); + lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); auto* display = lv_obj_get_display(parent); auto horizontal_px = lv_display_get_horizontal_resolution(display); @@ -122,7 +120,8 @@ void GpioApp::onShow(AppContext& app, lv_obj_t* parent) { int32_t x_spacing = 20; uint8_t column = 0; - uint8_t column_limit = is_landscape_display ? 10 : 5; + const uint8_t offset_from_left_label = 4; + const uint8_t column_limit = is_landscape_display ? 10 : 5; auto* row_wrapper = createGpioRowWrapper(wrapper); lv_obj_align(row_wrapper, LV_ALIGN_TOP_MID, 0, 0); @@ -138,8 +137,9 @@ void GpioApp::onShow(AppContext& app, lv_obj_t* parent) { // Add a new GPIO status indicator auto* status_label = lv_label_create(row_wrapper); - lv_obj_set_pos(status_label, (int32_t)((column+1) * x_spacing), 0); + lv_obj_set_pos(status_label, (int32_t)((column+1) * x_spacing + offset_from_left_label), 0); lv_label_set_text_fmt(status_label, "%s", LV_SYMBOL_STOP); + lv_obj_set_style_text_color(status_label, lv_color_background_darkest(), LV_STATE_DEFAULT); lvPins[i] = status_label; column++; @@ -148,7 +148,7 @@ void GpioApp::onShow(AppContext& app, lv_obj_t* parent) { // Add the GPIO number after the last item on a row auto* postfix = lv_label_create(row_wrapper); lv_label_set_text_fmt(postfix, "%02d", i); - lv_obj_set_pos(postfix, (int32_t)((column+1) * x_spacing), 0); + lv_obj_set_pos(postfix, (int32_t)((column+1) * x_spacing + offset_from_left_label), 0); // Add a new row wrapper underneath the last one auto* new_row_wrapper = createGpioRowWrapper(wrapper); diff --git a/Tactility/Source/lvgl/Color.cpp b/Tactility/Source/lvgl/Color.cpp new file mode 100644 index 00000000..a0fdfb35 --- /dev/null +++ b/Tactility/Source/lvgl/Color.cpp @@ -0,0 +1,13 @@ +#include "Tactility/lvgl/Color.h" + +lv_color_t lv_color_foreground() { + return lv_color_make(0xFF, 0xFF, 0xFF); +} + +lv_color_t lv_color_background() { + return lv_color_make(0x28, 0x2B, 0x30); +} + +lv_color_t lv_color_background_darkest() { + return lv_color_make(0x00, 0x00, 0x00); +} From 61165215563805cde65edb44ef3d5eb130af0f21 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sun, 8 Jun 2025 10:37:57 +0200 Subject: [PATCH 05/16] Update launcher icon sizes (#286) This is a missing commit from a previous PR --- Data/system_sources/app/Launcher/apps.svg | 14 +++++++------- Data/system_sources/app/Launcher/folder.svg | 12 ++++++------ Data/system_sources/app/Launcher/settings.svg | 18 +++++++++--------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Data/system_sources/app/Launcher/apps.svg b/Data/system_sources/app/Launcher/apps.svg index 4ec70006..3d4efc50 100644 --- a/Data/system_sources/app/Launcher/apps.svg +++ b/Data/system_sources/app/Launcher/apps.svg @@ -7,9 +7,9 @@ version="1.1" id="svg1" sodipodi:docname="apps.svg" - inkscape:export-filename="apps.png" - inkscape:export-xdpi="192" - inkscape:export-ydpi="192" + inkscape:export-filename="icon_apps.png" + inkscape:export-xdpi="160" + inkscape:export-ydpi="160" inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" @@ -27,10 +27,10 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="17.088414" - inkscape:cx="14.629795" - inkscape:cy="18.901696" - inkscape:window-width="2560" - inkscape:window-height="1371" + inkscape:cx="14.600536" + inkscape:cy="18.960215" + inkscape:window-width="1503" + inkscape:window-height="933" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" diff --git a/Data/system_sources/app/Launcher/folder.svg b/Data/system_sources/app/Launcher/folder.svg index 167f0acc..298363ee 100644 --- a/Data/system_sources/app/Launcher/folder.svg +++ b/Data/system_sources/app/Launcher/folder.svg @@ -7,9 +7,9 @@ version="1.1" id="svg1" sodipodi:docname="folder.svg" - inkscape:export-filename="folder.png" - inkscape:export-xdpi="192" - inkscape:export-ydpi="192" + inkscape:export-filename="icon_files.png" + inkscape:export-xdpi="160" + inkscape:export-ydpi="160" inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" @@ -27,10 +27,10 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="48.333333" - inkscape:cx="12" + inkscape:cx="11.989655" inkscape:cy="12" - inkscape:window-width="2560" - inkscape:window-height="1371" + inkscape:window-width="1503" + inkscape:window-height="933" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" diff --git a/Data/system_sources/app/Launcher/settings.svg b/Data/system_sources/app/Launcher/settings.svg index c442c05f..45925e90 100644 --- a/Data/system_sources/app/Launcher/settings.svg +++ b/Data/system_sources/app/Launcher/settings.svg @@ -7,9 +7,9 @@ version="1.1" id="svg1" sodipodi:docname="settings.svg" - inkscape:export-filename="settings.png" - inkscape:export-xdpi="192" - inkscape:export-ydpi="192" + inkscape:export-filename="icon_settings.png" + inkscape:export-xdpi="160" + inkscape:export-ydpi="160" inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" @@ -27,13 +27,13 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="48.333333" - inkscape:cx="12" + inkscape:cx="11.989655" inkscape:cy="12" - inkscape:window-width="1898" - inkscape:window-height="1269" - inkscape:window-x="26" - inkscape:window-y="23" - inkscape:window-maximized="0" + inkscape:window-width="1503" + inkscape:window-height="933" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" inkscape:current-layer="svg1" /> Date: Sun, 8 Jun 2025 10:38:20 +0200 Subject: [PATCH 06/16] App build system (#285) from https://github.com/ByteWelder/TactilityTool --- ExternalApps/HelloWorld/.gitignore | 2 + ExternalApps/HelloWorld/build.sh | 6 - ExternalApps/HelloWorld/sdkconfig.override | 2 - ExternalApps/HelloWorld/tactility.properties | 2 + ExternalApps/HelloWorld/tactility.py | 392 +++++++++++++++++++ 5 files changed, 396 insertions(+), 8 deletions(-) create mode 100644 ExternalApps/HelloWorld/.gitignore delete mode 100755 ExternalApps/HelloWorld/build.sh delete mode 100644 ExternalApps/HelloWorld/sdkconfig.override create mode 100644 ExternalApps/HelloWorld/tactility.properties create mode 100644 ExternalApps/HelloWorld/tactility.py diff --git a/ExternalApps/HelloWorld/.gitignore b/ExternalApps/HelloWorld/.gitignore new file mode 100644 index 00000000..89baa26e --- /dev/null +++ b/ExternalApps/HelloWorld/.gitignore @@ -0,0 +1,2 @@ +build*/ +.tactility/ diff --git a/ExternalApps/HelloWorld/build.sh b/ExternalApps/HelloWorld/build.sh deleted file mode 100755 index a2f0b9ff..00000000 --- a/ExternalApps/HelloWorld/build.sh +++ /dev/null @@ -1,6 +0,0 @@ -rm sdkconfig -cp ../../sdkconfig sdkconfig -cat sdkconfig.override >> sdkconfig -# First we must run "build" because otherwise "idf.py elf" is not a valid command -idf.py build -idf.py elf diff --git a/ExternalApps/HelloWorld/sdkconfig.override b/ExternalApps/HelloWorld/sdkconfig.override deleted file mode 100644 index b02eb18b..00000000 --- a/ExternalApps/HelloWorld/sdkconfig.override +++ /dev/null @@ -1,2 +0,0 @@ -CONFIG_PARTITION_TABLE_SINGLE_APP=y -CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_LOCK=n diff --git a/ExternalApps/HelloWorld/tactility.properties b/ExternalApps/HelloWorld/tactility.properties new file mode 100644 index 00000000..14c0bdfb --- /dev/null +++ b/ExternalApps/HelloWorld/tactility.properties @@ -0,0 +1,2 @@ +[sdk] +version = 0.4.0-dev diff --git a/ExternalApps/HelloWorld/tactility.py b/ExternalApps/HelloWorld/tactility.py new file mode 100644 index 00000000..8b5e2843 --- /dev/null +++ b/ExternalApps/HelloWorld/tactility.py @@ -0,0 +1,392 @@ +import configparser +import json +import os +import re +import shutil +import sys +import subprocess +import time +import urllib.request +import zipfile + +esp_platforms = ["esp32", "esp32s3"] +ttbuild_path = ".tactility" +ttbuild_version = "0.1.0" +ttbuild_properties_file = "tactility.properties" +ttbuild_cdn = "https://cdn.tactility.one" +ttbuild_sdk_json_validity = 3600 # seconds +verbose = False + +spinner_pattern = [ + "⠋", + "⠙", + "⠹", + "⠸", + "⠼", + "⠴", + "⠦", + "⠧", + "⠇", + "⠏" +] + +if sys.platform == "win32": + shell_color_red = "" + shell_color_orange = "" + shell_color_green = "" + shell_color_purple = "" + shell_color_cyan = "" + shell_color_reset = "" +else: + shell_color_red = "\033[91m" + shell_color_orange = "\033[93m" + shell_color_green = "\033[32m" + shell_color_purple = "\033[35m" + shell_color_cyan = "\033[36m" + shell_color_reset = "\033[m" + +def print_help(): + print("Usage: python tactility.py [action] [options]") + print("") + print("Actions:") + print(" build [esp32,esp32s3,all] Build the app for 1 or more platforms") + print(" clean Clean the build folders") + print(" clearcache Clear the SDK cache") + print(" updateself Update this tool") + print("") + print("Options:") + print(" --help Show this commandline info") + print(" --skip-build Run everything except the idf.py/CMake commands") + print(" --verbose Show extra console output") + +def download_file(url, filepath): + global verbose + if verbose: + print(f"Downloading from {url} to {filepath}") + request = urllib.request.Request( + url, + data=None, + headers={ + "User-Agent": f"Tactility Build Tool {ttbuild_version}" + } + ) + try: + response = urllib.request.urlopen(request) + file = open(filepath, mode="wb") + file.write(response.read()) + file.close() + return True + except OSError as error: + if verbose: + print_error(f"Failed to fetch URL {url}\n{error}") + return False + +def print_warning(message): + print(f"{shell_color_orange}WARNING: {message}{shell_color_reset}") + +def print_error(message): + print(f"{shell_color_red}ERROR: {message}{shell_color_reset}") + +def exit_with_error(message): + print_error(message) + sys.exit(1) + +def is_valid_platform_name(name): + return name == "all" or name == "esp32" or name == "esp32s3" + +def validate_environment(): + global ttbuild_properties_file + if os.environ.get("IDF_PATH") is None: + exit_with_error("IDF is not installed or activated. Ensure you installed the toolset and ran the export command.") + if os.environ.get("TACTILITY_SDK_PATH") is not None: + print_warning("TACTILITY_SDK_PATH is set, but will be ignored by this command") + if not os.path.exists(ttbuild_properties_file): + exit_with_error(f"{ttbuild_properties_file} file not found") + +def setup_environment(): + global ttbuild_path + os.makedirs(ttbuild_path, exist_ok=True) + +def get_sdk_dir(version, platform): + global ttbuild_cdn + return os.path.join(ttbuild_path, f"{version}-{platform}", "TactilitySDK") + +def get_sdk_version(): + global ttbuild_properties_file + parser = configparser.RawConfigParser() + parser.read(ttbuild_properties_file) + sdk_dict = dict(parser.items("sdk")) + if not "version" in sdk_dict: + exit_with_error(f"Could not find 'version' in [sdk] section in {ttbuild_properties_file}") + return sdk_dict["version"] + +def get_sdk_root_dir(version, platform): + global ttbuild_cdn + return os.path.join(ttbuild_path, f"{version}-{platform}") + +def get_sdk_url(version, platform): + global ttbuild_cdn + return f"{ttbuild_cdn}/TactilitySDK-{version}-{platform}.zip" + +def sdk_exists(version, platform): + sdk_dir = get_sdk_dir(version, platform) + return os.path.isdir(sdk_dir) + +def should_update_sdk_json(): + global ttbuild_cdn + json_filepath = os.path.join(ttbuild_path, "sdk.json") + if os.path.exists(json_filepath): + json_modification_time = os.path.getmtime(json_filepath) + now = time.time() + global ttbuild_sdk_json_validity + minimum_seconds_difference = ttbuild_sdk_json_validity + return (now - json_modification_time) > minimum_seconds_difference + else: + return True + +def update_sdk_json(): + global ttbuild_cdn, ttbuild_path + json_url = f"{ttbuild_cdn}/sdk.json" + json_filepath = os.path.join(ttbuild_path, "sdk.json") + return download_file(json_url, json_filepath) + +def should_fetch_sdkconfig_files(): + for platform in esp_platforms: + sdkconfig_filename = f"sdkconfig.app.{platform}" + if not os.path.exists(os.path.join(ttbuild_path, sdkconfig_filename)): + return True + return False + +def fetch_sdkconfig_files(): + for platform in esp_platforms: + sdkconfig_filename = f"sdkconfig.app.{platform}" + target_path = os.path.join(ttbuild_path, sdkconfig_filename) + if not download_file(f"{ttbuild_cdn}/{sdkconfig_filename}", target_path): + exit_with_error(f"Failed to download sdkconfig file for {platform}") + + +def validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build): + version_map = sdk_json["versions"] + if not sdk_version in version_map: + exit_with_error(f"Version not found: {sdk_version}") + version_data = version_map[sdk_version] + available_platforms = version_data["platforms"] + for desired_platform in platforms_to_build: + if not desired_platform in available_platforms: + exit_with_error(f"Platform {desired_platform} is not available. Available ones: {available_platforms}") + +def validate_self(sdk_json): + if not "toolVersion" in sdk_json: + exit_with_error("Server returned invalid SDK data format (toolVersion not found)") + if not "toolCompatibility" in sdk_json: + exit_with_error("Server returned invalid SDK data format (toolCompatibility not found)") + if not "toolDownloadUrl" in sdk_json: + exit_with_error("Server returned invalid SDK data format (toolDownloadUrl not found)") + tool_version = sdk_json["toolVersion"] + tool_compatibility = sdk_json["toolCompatibility"] + if tool_version != ttbuild_version: + print_warning(f"New version available: {tool_version} (currently using {ttbuild_version})") + print_warning(f"Run 'tactility.py updateself' to update.") + if re.search(tool_compatibility, ttbuild_version) is None: + print_error("The tool is not compatible anymore.") + print_error("Run 'tactility.py updateself' to update.") + sys.exit() + +def sdk_download(version, platform): + sdk_root_dir = get_sdk_root_dir(version, platform) + os.makedirs(sdk_root_dir, exist_ok=True) + sdk_url = get_sdk_url(version, platform) + filepath = os.path.join(sdk_root_dir, f"{version}-{platform}.zip") + print(f"Downloading SDK version {version} for {platform}") + if download_file(sdk_url, filepath): + with zipfile.ZipFile(filepath, "r") as zip_ref: + zip_ref.extractall(sdk_root_dir) + return True + else: + return False + +def sdk_download_all(version, platforms): + for platform in platforms: + if not sdk_exists(version, platform): + if not sdk_download(version, platform): + return False + else: + if verbose: + print(f"Using cached download for SDK version {version} and platform {platform}") + return True + +def find_elf_file(platform): + build_dir = f"build-{platform}" + if os.path.exists(build_dir): + for file in os.listdir(build_dir): + if file.endswith(".app.elf"): + return os.path.join(build_dir, file) + return None + +def build_all(version, platforms, skip_build): + for platform in platforms: + # First build command must be "idf.py build", otherwise it fails to execute "idf.py elf" + # We check if the ELF file exists and run the correct command + # This can lead to code caching issues, so sometimes a clean build is required + if find_elf_file(platform) is None: + if not build_first(version, platform, skip_build): + break + else: + if not build_consecutively(version, platform, skip_build): + break + +def wait_for_build(process, platform): + buffer = [] + os.set_blocking(process.stdout.fileno(), False) + while process.poll() is None: + for i in spinner_pattern: + time.sleep(0.1) + progress_text = f"Building for {platform} {shell_color_cyan}" + str(i) + shell_color_reset + sys.stdout.write(progress_text + "\r") + while True: + line = process.stdout.readline() + decoded_line = line.decode("UTF-8") + if decoded_line != "": + buffer.append(decoded_line) + else: + break + return buffer + +# The first build must call "idf.py build" and consecutive builds must call "idf.py elf" as it finishes faster. +# The problem is that the "idf.py build" always results in an error, even though the elf file is created. +# The solution is to suppress the error if we find that the elf file was created. +def build_first(version, platform, skip_build): + sdk_dir = get_sdk_dir(version, platform) + if verbose: + print(f"Using SDK at {sdk_dir}") + os.environ["TACTILITY_SDK_PATH"] = sdk_dir + sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}") + os.system(f"cp {sdkconfig_path} sdkconfig") + elf_path = find_elf_file(platform) + # Remove previous elf file: re-creation of the file is used to measure if the build succeeded, + # as the actual build job will always fail due to technical issues with the elf cmake script + if elf_path is not None: + os.remove(elf_path) + if skip_build: + return True + print("Building first build") + with subprocess.Popen(["idf.py", "-B", f"build-{platform}", "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + build_output = wait_for_build(process, platform) + # The return code is never expected to be 0 due to a bug in the elf cmake script, but we keep it just in case + if process.returncode == 0: + print(f"{shell_color_green}Building for {platform} ✅{shell_color_reset}") + return True + else: + if find_elf_file(platform) is None: + for line in build_output: + print(line, end="") + print(f"{shell_color_red}Building for {platform} failed ❌{shell_color_reset}") + return False + else: + print(f"{shell_color_green}Building for {platform} ✅{shell_color_reset}") + return True + +def build_consecutively(version, platform, skip_build): + sdk_dir = get_sdk_dir(version, platform) + if verbose: + print(f"Using SDK at {sdk_dir}") + os.environ["TACTILITY_SDK_PATH"] = sdk_dir + sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}") + os.system(f"cp {sdkconfig_path} sdkconfig") + if skip_build: + return True + with subprocess.Popen(["idf.py", "-B", f"build-{platform}", "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + build_output = wait_for_build(process, platform) + if process.returncode == 0: + print(f"{shell_color_green}Building for {platform} ✅{shell_color_reset}") + return True + else: + for line in build_output: + print(line, end="") + print(f"{shell_color_red}Building for {platform} failed ❌{shell_color_reset}") + return False + +def read_sdk_json(): + json_file_path = os.path.join(ttbuild_path, "sdk.json") + json_file = open(json_file_path) + return json.load(json_file) + +def build_action(platform_arg): + # Environment validation + validate_environment() + # Environment setup + setup_environment() + if not is_valid_platform_name(platform_arg): + print_help() + exit_with_error("Invalid platform name") + if should_fetch_sdkconfig_files(): + fetch_sdkconfig_files() + # Update SDK cache + if should_update_sdk_json() and not update_sdk_json(): + exit_with_error("Failed to retrieve SDK info") + sdk_json = read_sdk_json() + validate_self(sdk_json) + if not "versions" in sdk_json: + exit_with_error("Version data not found in sdk.json") + # Build + platforms_to_build = esp_platforms if platform_arg == "all" else [platform_arg] + sdk_version = get_sdk_version() + validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build) + if not sdk_download_all(sdk_version, platforms_to_build): + exit_with_error("Failed to download one or more SDKs") + build_all(sdk_version, platforms_to_build, skip_build) # Environment validation + +def clean_action(): + count = 0 + for path in os.listdir("."): + if path.startswith("build-"): + print(f"Removing {path}/") + shutil.rmtree(path) + count = count + 1 + if count == 0: + print("Nothing to clean") + +def clear_cache_action(): + if os.path.exists(ttbuild_path): + print(f"Removing {ttbuild_path}/") + shutil.rmtree(ttbuild_path) + else: + print("Nothing to clear") + +def update_self_action(): + sdk_json = read_sdk_json() + tool_download_url = sdk_json["toolDownloadUrl"] + if download_file(tool_download_url, "tactility.py"): + print("Updated") + else: + exit_with_error("Update failed") + +if __name__ == "__main__": + print(f"Tactility Build System v{ttbuild_version}") + if "--help" in sys.argv: + print_help() + sys.exit() + # Argument validation + if len(sys.argv) == 1: + print_help() + sys.exit() + action_arg = sys.argv[1] + verbose = "--verbose" in sys.argv + skip_build = "--skip-build" in sys.argv + # Actions + if action_arg == "build": + if len(sys.argv) < 3: + print_help() + sys.exit() + else: + platform_arg = sys.argv[2] + build_action(platform_arg) + elif action_arg == "clean": + clean_action() + elif action_arg == "clearcache": + clear_cache_action() + elif action_arg == "updateself": + update_self_action() + else: + print_help() + sys.exit() From 1593eb80ce6b32180f57e5d9ed702d53f72af8c5 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Mon, 9 Jun 2025 13:46:08 +0200 Subject: [PATCH 07/16] TactilityC additions (#287) New TactilityC implementations for: - WiFi - GPS - Preferences - Timezone Also includes: - Some fixes to enums/naming - Cleanup elsewhere --- ExternalApps/HelloWorld/tactility.py | 94 +++++++++++-------- Tactility/Include/Tactility/Preferences.h | 6 +- .../Source/app/gpssettings/GpsSettings.cpp | 4 +- TactilityC/Include/tt_app.h | 2 +- TactilityC/Include/tt_app_manifest.h | 12 +-- TactilityC/Include/tt_gps.h | 21 +++++ TactilityC/Include/tt_preferences.h | 83 ++++++++++++++++ TactilityC/Include/tt_time.h | 42 +++++++++ TactilityC/Include/tt_wifi.h | 74 +++++++++++++++ TactilityC/Source/tt_app.cpp | 8 +- TactilityC/Source/tt_bundle.cpp | 20 ++-- TactilityC/Source/tt_gps.cpp | 45 +++++++++ TactilityC/Source/tt_init.cpp | 29 ++++++ TactilityC/Source/tt_preferences.cpp | 53 +++++++++++ TactilityC/Source/tt_time.cpp | 42 +++++++++ TactilityC/Source/tt_timer.cpp | 2 - TactilityC/Source/tt_wifi.cpp | 55 +++++++++++ 17 files changed, 528 insertions(+), 64 deletions(-) create mode 100644 TactilityC/Include/tt_gps.h create mode 100644 TactilityC/Include/tt_preferences.h create mode 100644 TactilityC/Include/tt_time.h create mode 100644 TactilityC/Include/tt_wifi.h create mode 100644 TactilityC/Source/tt_gps.cpp create mode 100644 TactilityC/Source/tt_preferences.cpp create mode 100644 TactilityC/Source/tt_time.cpp create mode 100644 TactilityC/Source/tt_wifi.cpp diff --git a/ExternalApps/HelloWorld/tactility.py b/ExternalApps/HelloWorld/tactility.py index 8b5e2843..c41b2040 100644 --- a/ExternalApps/HelloWorld/tactility.py +++ b/ExternalApps/HelloWorld/tactility.py @@ -9,13 +9,18 @@ import time import urllib.request import zipfile -esp_platforms = ["esp32", "esp32s3"] +# Targetable platforms that represent a specific hardware target +platform_targets = ["esp32", "esp32s3"] +# All valid platform commandline arguments +platform_arguments = platform_targets.copy() +platform_arguments.append("all") ttbuild_path = ".tactility" -ttbuild_version = "0.1.0" +ttbuild_version = "0.3.0" ttbuild_properties_file = "tactility.properties" ttbuild_cdn = "https://cdn.tactility.one" ttbuild_sdk_json_validity = 3600 # seconds verbose = False +use_local_sdk = False spinner_pattern = [ "⠋", @@ -49,15 +54,19 @@ def print_help(): print("Usage: python tactility.py [action] [options]") print("") print("Actions:") - print(" build [esp32,esp32s3,all] Build the app for 1 or more platforms") - print(" clean Clean the build folders") - print(" clearcache Clear the SDK cache") - print(" updateself Update this tool") + print(" build [esp32,esp32s3,all,local] Build the app for the specified platform") + print(" esp32: ESP32") + print(" esp32s3: ESP32 S3") + print(" all: all supported ESP platforms") + print(" clean Clean the build folders") + print(" clearcache Clear the SDK cache") + print(" updateself Update this tool") print("") print("Options:") - print(" --help Show this commandline info") - print(" --skip-build Run everything except the idf.py/CMake commands") - print(" --verbose Show extra console output") + print(" --help Show this commandline info") + print(" --local-sdk Use SDK specifiedc by environment variable TACTILITY_SDK_PATH") + print(" --skip-build Run everything except the idf.py/CMake commands") + print(" --verbose Show extra console output") def download_file(url, filepath): global verbose @@ -92,24 +101,32 @@ def exit_with_error(message): sys.exit(1) def is_valid_platform_name(name): - return name == "all" or name == "esp32" or name == "esp32s3" + global platform_arguments + return name in platform_arguments def validate_environment(): - global ttbuild_properties_file + global ttbuild_properties_file, use_local_sdk if os.environ.get("IDF_PATH") is None: - exit_with_error("IDF is not installed or activated. Ensure you installed the toolset and ran the export command.") - if os.environ.get("TACTILITY_SDK_PATH") is not None: - print_warning("TACTILITY_SDK_PATH is set, but will be ignored by this command") + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") if not os.path.exists(ttbuild_properties_file): exit_with_error(f"{ttbuild_properties_file} file not found") + if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None: + print_warning("TACTILITY_SDK_PATH is set, but will be ignored by this command.") + print_warning("If you want to use it, use the 'build local' parameters.") + elif use_local_sdk == True and os.environ.get("TACTILITY_SDK_PATH") is None: + exit_with_error("local build was requested, but TACTILITY_SDK_PATH environment variable is not set.") def setup_environment(): global ttbuild_path os.makedirs(ttbuild_path, exist_ok=True) def get_sdk_dir(version, platform): - global ttbuild_cdn - return os.path.join(ttbuild_path, f"{version}-{platform}", "TactilitySDK") + global use_local_sdk + if use_local_sdk: + return os.environ.get("TACTILITY_SDK_PATH") + else: + global ttbuild_cdn + return os.path.join(ttbuild_path, f"{version}-{platform}", "TactilitySDK") def get_sdk_version(): global ttbuild_properties_file @@ -151,14 +168,14 @@ def update_sdk_json(): return download_file(json_url, json_filepath) def should_fetch_sdkconfig_files(): - for platform in esp_platforms: + for platform in platform_targets: sdkconfig_filename = f"sdkconfig.app.{platform}" if not os.path.exists(os.path.join(ttbuild_path, sdkconfig_filename)): return True return False def fetch_sdkconfig_files(): - for platform in esp_platforms: + for platform in platform_targets: sdkconfig_filename = f"sdkconfig.app.{platform}" target_path = os.path.join(ttbuild_path, sdkconfig_filename) if not download_file(f"{ttbuild_cdn}/{sdkconfig_filename}", target_path): @@ -190,7 +207,7 @@ def validate_self(sdk_json): if re.search(tool_compatibility, ttbuild_version) is None: print_error("The tool is not compatible anymore.") print_error("Run 'tactility.py updateself' to update.") - sys.exit() + sys.exit(1) def sdk_download(version, platform): sdk_root_dir = get_sdk_root_dir(version, platform) @@ -316,25 +333,27 @@ def build_action(platform_arg): validate_environment() # Environment setup setup_environment() + platforms_to_build = platform_targets if platform_arg == "all" else [platform_arg] if not is_valid_platform_name(platform_arg): print_help() exit_with_error("Invalid platform name") - if should_fetch_sdkconfig_files(): - fetch_sdkconfig_files() - # Update SDK cache - if should_update_sdk_json() and not update_sdk_json(): - exit_with_error("Failed to retrieve SDK info") - sdk_json = read_sdk_json() - validate_self(sdk_json) - if not "versions" in sdk_json: - exit_with_error("Version data not found in sdk.json") + if not use_local_sdk: + if should_fetch_sdkconfig_files(): + fetch_sdkconfig_files() + # Update SDK cache + if should_update_sdk_json() and not update_sdk_json(): + exit_with_error("Failed to retrieve SDK info") + sdk_json = read_sdk_json() + validate_self(sdk_json) + if not "versions" in sdk_json: + exit_with_error("Version data not found in sdk.json") # Build - platforms_to_build = esp_platforms if platform_arg == "all" else [platform_arg] sdk_version = get_sdk_version() - validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build) - if not sdk_download_all(sdk_version, platforms_to_build): - exit_with_error("Failed to download one or more SDKs") - build_all(sdk_version, platforms_to_build, skip_build) # Environment validation + if not use_local_sdk: + validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build) + if not sdk_download_all(sdk_version, platforms_to_build): + exit_with_error("Failed to download one or more SDKs") + build_all(sdk_version, platforms_to_build, skip_build) # Environment validation def clean_action(): count = 0 @@ -373,14 +392,13 @@ if __name__ == "__main__": action_arg = sys.argv[1] verbose = "--verbose" in sys.argv skip_build = "--skip-build" in sys.argv + use_local_sdk = "--local-sdk" in sys.argv # Actions if action_arg == "build": if len(sys.argv) < 3: print_help() - sys.exit() - else: - platform_arg = sys.argv[2] - build_action(platform_arg) + exit_with_error("Commandline parameter missing") + build_action(sys.argv[2]) elif action_arg == "clean": clean_action() elif action_arg == "clearcache": @@ -389,4 +407,4 @@ if __name__ == "__main__": update_self_action() else: print_help() - sys.exit() + exit_with_error("Unknown commandline parameter") diff --git a/Tactility/Include/Tactility/Preferences.h b/Tactility/Include/Tactility/Preferences.h index d2a34812..b1f778ac 100644 --- a/Tactility/Include/Tactility/Preferences.h +++ b/Tactility/Include/Tactility/Preferences.h @@ -8,9 +8,13 @@ namespace tt { /** * Settings that persist on NVS flash for ESP32. * On simulator, the settings are only in-memory. + * + * Note that on ESP32, there are limitations: + * - namespace name is limited by NVS_NS_NAME_MAX_SIZE (generally 16 characters) + * - key is limited by NVS_KEY_NAME_MAX_SIZE (generally 16 characters) */ class Preferences { -private: + const char* namespace_; public: diff --git a/Tactility/Source/app/gpssettings/GpsSettings.cpp b/Tactility/Source/app/gpssettings/GpsSettings.cpp index 3c1f3e8d..c80de746 100644 --- a/Tactility/Source/app/gpssettings/GpsSettings.cpp +++ b/Tactility/Source/app/gpssettings/GpsSettings.cpp @@ -24,8 +24,6 @@ extern const AppManifest manifest; class GpsSettingsApp final : public App { -private: - std::unique_ptr timer; std::shared_ptr appReference = std::make_shared(this); lv_obj_t* statusWrapper = nullptr; @@ -96,7 +94,7 @@ private: memcpy(&index, &index_as_voidptr, sizeof(int)); std::vector configurations; - auto gps_service = tt::service::gps::findGpsService(); + auto gps_service = service::gps::findGpsService(); if (gps_service && gps_service->getGpsConfigurations(configurations)) { TT_LOG_I(TAG, "Found service and configs %d %d", index, configurations.size()); if (index <= configurations.size()) { diff --git a/TactilityC/Include/tt_app.h b/TactilityC/Include/tt_app.h index b1e97bd2..1e5dbbb0 100644 --- a/TactilityC/Include/tt_app.h +++ b/TactilityC/Include/tt_app.h @@ -18,7 +18,7 @@ BundleHandle _Nullable tt_app_get_parameters(AppHandle handle); * @param[in] result the result state to set * @param[in] bundle the result bundle to set */ -void tt_app_set_result(AppHandle handle, Result result, BundleHandle _Nullable bundle); +void tt_app_set_result(AppHandle handle, AppResult result, BundleHandle _Nullable bundle); /** @return true if a result was set for this app context */ bool tt_app_has_result(AppHandle handle); diff --git a/TactilityC/Include/tt_app_manifest.h b/TactilityC/Include/tt_app_manifest.h index 0640b1d3..7551b4f3 100644 --- a/TactilityC/Include/tt_app_manifest.h +++ b/TactilityC/Include/tt_app_manifest.h @@ -9,14 +9,14 @@ extern "C" { /** Important: These values must map to tt::app::Result values exactly */ typedef enum { - AppResultOk = 0, - AppResultCancelled = 1, - AppResultError = 2 -} Result; + APP_RESULT_OK = 0, + APP_RESULT_CANCELLED = 1, + APP_RESULT_ERROR = 2 +} AppResult; typedef void* AppHandle; -typedef unsigned int LaunchId; +typedef unsigned int AppLaunchId; /** Important: These function types must map to t::app types exactly */ typedef void* (*AppCreateData)(); @@ -25,7 +25,7 @@ typedef void (*AppOnCreate)(AppHandle app, void* _Nullable data); typedef void (*AppOnDestroy)(AppHandle app, void* _Nullable data); typedef void (*AppOnShow)(AppHandle app, void* _Nullable data, lv_obj_t* parent); typedef void (*AppOnHide)(AppHandle app, void* _Nullable data); -typedef void (*AppOnResult)(AppHandle app, void* _Nullable data, LaunchId launchId, Result result, BundleHandle resultData); +typedef void (*AppOnResult)(AppHandle app, void* _Nullable data, AppLaunchId launchId, AppResult result, BundleHandle resultData); typedef struct { /** The application's human-readable name */ diff --git a/TactilityC/Include/tt_gps.h b/TactilityC/Include/tt_gps.h new file mode 100644 index 00000000..8d9bcfae --- /dev/null +++ b/TactilityC/Include/tt_gps.h @@ -0,0 +1,21 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +bool tt_gps_has_coordinates(); + +bool tt_gps_get_coordinates( + float* longitude, + float* latitude, + float* speed, + float* course, + int* day, + int* month, + int* year +); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityC/Include/tt_preferences.h b/TactilityC/Include/tt_preferences.h new file mode 100644 index 00000000..735982b3 --- /dev/null +++ b/TactilityC/Include/tt_preferences.h @@ -0,0 +1,83 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * Note that on ESP32, there are limitations: + * - namespace name is limited by NVS_NS_NAME_MAX_SIZE (generally 16 characters) + * - key is limited by NVS_KEY_NAME_MAX_SIZE (generally 16 characters) + */ + +/** The handle that represents a Preferences instance */ +typedef void* PreferencesHandle; + +/** + * @param[in] identifier the name of the preferences. This determines the NVS namespace on ESP. + * @return a new preferences instance + */ +PreferencesHandle tt_preferences_alloc(const char* identifier); + +/** Dealloc an existing preferences instance */ +void tt_preferences_free(PreferencesHandle handle); + +/** + * Try to get a boolean value + * @param[in] handle the handle that represents the preferences + * @param[in] key the identifier that represents the stored value (~variable name) + * @param[out] out the output value (only set when return value is set to true) + * @return true if "out" was set + */ +bool tt_preferences_opt_bool(PreferencesHandle handle, const char* key, bool* out); + +/** + * Try to get an int32_t value + * @param[in] handle the handle that represents the preferences + * @param[in] key the identifier that represents the stored value (~variable name) + * @param[out] out the output value (only set when return value is set to true) + * @return true if "out" was set + */ +bool tt_preferences_opt_int32(PreferencesHandle handle, const char* key, int32_t* out); + +/** + * Try to get a string + * @warning outSize must be large enough to include null terminator. This means that your string has to be the expected text length + 1 extra character. + * @param[in] handle the handle that represents the preferences + * @param[in] key the identifier that represents the stored value (~variable name) + * @param[out] out the buffer to store the string in + * @param[in] outSize the size of the buffer + * @return true if "out" was set + */ +bool tt_preferences_opt_string(PreferencesHandle handle, const char* key, char* out, uint32_t outSize); + +/** + * Store a boolean value + * @param[in] handle the handle that represents the preferences + * @param[in] key the identifier that represents the stored value (~variable name) + * @param[in] value the value to store + */ +void tt_preferences_put_bool(PreferencesHandle handle, const char* key, bool value); + +/** + * Store an int32_t value + * @param[in] handle the handle that represents the preferences + * @param[in] key the identifier that represents the stored value (~variable name) + * @param[in] value the value to store + */ +void tt_preferences_put_int32(PreferencesHandle handle, const char* key, int32_t value); + +/** + * Store a string value + * @param[in] handle the handle that represents the preferences + * @param[in] key the identifier that represents the stored value (~variable name) + * @param[in] value the value to store + */ +void tt_preferences_put_string(PreferencesHandle handle, const char* key, const char* value); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/TactilityC/Include/tt_time.h b/TactilityC/Include/tt_time.h new file mode 100644 index 00000000..db9f972d --- /dev/null +++ b/TactilityC/Include/tt_time.h @@ -0,0 +1,42 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#define TT_TIMEZONE_NAME_BUFFER_LENGTH 32 +#define TT_TIMEZONE_CODE_BUFFER_LENGTH 48 + +/** + * Set the timezone + * @param[in] name human-readable name + * @param[in] code the technical code (from timezones.csv) + */ +void tt_timezone_set(const char* name, const char* code); + +/** + * Get the name of the timezone + * @param[out] buffer the output buffer which will include a null terminator (should be TT_TIMEZONE_NAME_BUFFER_LENGTH) + * @param[in] bufferSize the size of the output buffer + */ +bool tt_timezone_get_name(char* buffer, size_t bufferSize); + +/** + * Get the code of the timezone (see timezones.csv) + */ +bool tt_timezone_get_code(char* buffer, size_t bufferSize); + +/** @return true when clocks should be shown as a 24 hours one instead of 12 hours */ +bool tt_timezone_is_format_24_hour(); + +/** Set whether clocks should be shown as a 24 hours instead of 12 hours + * @param[in] show24Hour + */ +void tt_timezone_set_format_24_hour(bool show24Hour); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityC/Include/tt_wifi.h b/TactilityC/Include/tt_wifi.h new file mode 100644 index 00000000..659fd750 --- /dev/null +++ b/TactilityC/Include/tt_wifi.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include + +#define TT_WIFI_SSID_LIMIT 32 // 32 characters/octets, according to IEEE 802.11-2020 spec +#define TT_WIFI_CREDENTIALS_PASSWORD_LIMIT 64 // 64 characters/octets, according to IEEE 802.11-2020 spec + +#ifdef __cplusplus +extern "C" { +#endif + +/** Important: These values must map to tt::service::wifi::RadioState values exactly */ +typedef enum { + WIFI_RADIO_STATE_ON_PENDING, + WIFI_RADIO_STATE_ON, + WIFI_RADIO_STATE_CONNECTION_PENDING, + WIFI_RADIO_STATE_CONNECTION_ACTIVE, + WIFI_RADIO_STATE_OFF_PENDING, + WIFI_RADIO_STATE_OFF, +} WifiRadioState; + +/** @return the state of the WiFi radio */ +WifiRadioState tt_wifi_get_radio_state(); + +/** @return a textual representation of the WiFi radio state */ +const char* tt_wifi_radio_state_to_string(WifiRadioState state); + +/** Start scanning */ +void tt_wifi_scan(); + +/** @return true if a scan is active/pending */ +bool tt_wifi_is_scanning(); + +/** + * Return the WiFi SSID that the system tries to connect to, or is connected to. + * @param[out] buffer an allocated string buffer. Its size must be (WIFI_SSID_LIMIT + 1). + */ +void tt_wifi_get_connection_target(char* buffer); + +/** + * @brief Enable/disable the radio. Ignores input if desired state matches current state. + * @param[in] enabled + */ +void tt_wifi_set_enabled(bool enabled); + +/** + * + * @param ssid The access point identifier - maximal 32 characters/octets + * @param password the password - maximum 64 characters/octets + * @param channel 0 means "any" + * @param autoConnect whether we want to automatically reconnect if a disconnect occurs + * @param remember whether the record should be stored permanently on the device (it is only stored if this connection attempt succeeds) + */ +void tt_wifi_connect(const char* ssid, const char* password, int32_t channel, bool autoConnect, bool remember); + +/** + * If WiFi is connected, this disconnects it. + */ +void tt_wifi_disconnect(); + +/** + * @return true if WiFi is active and encrypted + */ +bool tt_wifi_is_connnection_secure(); + +/** + * @return the current radio connection link quality + */ +int tt_wifi_get_rssi(); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityC/Source/tt_app.cpp b/TactilityC/Source/tt_app.cpp index bf770720..2a50ddbf 100644 --- a/TactilityC/Source/tt_app.cpp +++ b/TactilityC/Source/tt_app.cpp @@ -10,9 +10,9 @@ BundleHandle _Nullable tt_app_get_parameters(AppHandle handle) { return (BundleHandle)HANDLE_AS_APP_CONTEXT(handle)->getParameters().get(); } -void tt_app_set_result(AppHandle handle, Result result, BundleHandle _Nullable bundle) { - auto shared_bundle = std::unique_ptr((tt::Bundle*)bundle); - HANDLE_AS_APP_CONTEXT(handle)->getApp()->setResult((tt::app::Result)result, std::move(shared_bundle)); +void tt_app_set_result(AppHandle handle, AppResult result, BundleHandle _Nullable bundle) { + auto shared_bundle = std::unique_ptr(static_cast(bundle)); + HANDLE_AS_APP_CONTEXT(handle)->getApp()->setResult(static_cast(result), std::move(shared_bundle)); } bool tt_app_has_result(AppHandle handle) { @@ -24,7 +24,7 @@ void tt_app_start(const char* appId) { } void tt_app_start_with_bundle(const char* appId, BundleHandle parameters) { - tt::app::start(appId, std::shared_ptr((tt::Bundle*)parameters)); + tt::app::start(appId, std::shared_ptr(static_cast(parameters))); } void tt_app_stop() { diff --git a/TactilityC/Source/tt_bundle.cpp b/TactilityC/Source/tt_bundle.cpp index 14530a32..4172bf2c 100644 --- a/TactilityC/Source/tt_bundle.cpp +++ b/TactilityC/Source/tt_bundle.cpp @@ -23,17 +23,19 @@ bool tt_bundle_opt_int32(BundleHandle handle, const char* key, int32_t* out) { } bool tt_bundle_opt_string(BundleHandle handle, const char* key, char* out, uint32_t outSize) { std::string out_string; - if (HANDLE_AS_BUNDLE(handle)->optString(key, out_string)) { - if (out_string.length() < outSize) { // Need 1 byte to add 0 at the end - memcpy(out, out_string.c_str(), out_string.length()); - out[out_string.length()] = 0x00; - return true; - } else { - return false; - } - } else { + + if (!HANDLE_AS_BUNDLE(handle)->optString(key, out_string)) { return false; } + + if (out_string.length() >= outSize) { + // Need 1 byte to add 0 at the end + return false; + } + + memcpy(out, out_string.c_str(), out_string.length()); + out[out_string.length()] = 0x00; + return true; } void tt_bundle_put_bool(BundleHandle handle, const char* key, bool value) { diff --git a/TactilityC/Source/tt_gps.cpp b/TactilityC/Source/tt_gps.cpp new file mode 100644 index 00000000..5131193c --- /dev/null +++ b/TactilityC/Source/tt_gps.cpp @@ -0,0 +1,45 @@ +#include "tt_gps.h" +#include + +using namespace tt::service; + +extern "C" { + +bool tt_gps_has_coordinates() { + auto service = gps::findGpsService(); + return service != nullptr && service->hasCoordinates(); +} + +bool tt_gps_get_coordinates( + float* longitude, + float* latitude, + float* speed, + float* course, + int* day, + int* month, + int* year +) { + auto service = gps::findGpsService(); + + if (service == nullptr) { + return false; + } + + minmea_sentence_rmc rmc; + + if (!service->getCoordinates(rmc)) { + return false; + } + + *longitude = minmea_tocoord(&rmc.longitude); + *latitude = minmea_tocoord(&rmc.latitude); + *speed = minmea_tocoord(&rmc.speed); + *course = minmea_tocoord(&rmc.course); + *day = rmc.date.day; + *month = rmc.date.month; + *year = rmc.date.year; + + return true; +} + +} \ No newline at end of file diff --git a/TactilityC/Source/tt_init.cpp b/TactilityC/Source/tt_init.cpp index a766a883..42e72bf1 100644 --- a/TactilityC/Source/tt_init.cpp +++ b/TactilityC/Source/tt_init.cpp @@ -5,15 +5,19 @@ #include "tt_app_manifest.h" #include "tt_app_selectiondialog.h" #include "tt_bundle.h" +#include "tt_gps.h" #include "tt_hal_i2c.h" #include "tt_lvgl_keyboard.h" #include "tt_lvgl_spinner.h" #include "tt_lvgl_toolbar.h" #include "tt_message_queue.h" #include "tt_mutex.h" +#include "tt_preferences.h" #include "tt_semaphore.h" #include "tt_thread.h" +#include "tt_time.h" #include "tt_timer.h" +#include "tt_wifi.h" #include @@ -39,6 +43,8 @@ const struct esp_elfsym elf_symbols[] { ESP_ELFSYM_EXPORT(tt_bundle_put_bool), ESP_ELFSYM_EXPORT(tt_bundle_put_int32), ESP_ELFSYM_EXPORT(tt_bundle_put_string), + ESP_ELFSYM_EXPORT(tt_gps_has_coordinates), + ESP_ELFSYM_EXPORT(tt_gps_get_coordinates), ESP_ELFSYM_EXPORT(tt_hal_i2c_start), ESP_ELFSYM_EXPORT(tt_hal_i2c_stop), ESP_ELFSYM_EXPORT(tt_hal_i2c_is_started), @@ -72,6 +78,14 @@ const struct esp_elfsym elf_symbols[] { ESP_ELFSYM_EXPORT(tt_mutex_free), ESP_ELFSYM_EXPORT(tt_mutex_lock), ESP_ELFSYM_EXPORT(tt_mutex_unlock), + ESP_ELFSYM_EXPORT(tt_preferences_alloc), + ESP_ELFSYM_EXPORT(tt_preferences_free), + ESP_ELFSYM_EXPORT(tt_preferences_opt_bool), + ESP_ELFSYM_EXPORT(tt_preferences_opt_int32), + ESP_ELFSYM_EXPORT(tt_preferences_opt_string), + ESP_ELFSYM_EXPORT(tt_preferences_put_bool), + ESP_ELFSYM_EXPORT(tt_preferences_put_int32), + ESP_ELFSYM_EXPORT(tt_preferences_put_string), ESP_ELFSYM_EXPORT(tt_semaphore_alloc), ESP_ELFSYM_EXPORT(tt_semaphore_free), ESP_ELFSYM_EXPORT(tt_semaphore_acquire), @@ -99,6 +113,21 @@ const struct esp_elfsym elf_symbols[] { ESP_ELFSYM_EXPORT(tt_timer_get_expire_time), ESP_ELFSYM_EXPORT(tt_timer_set_pending_callback), ESP_ELFSYM_EXPORT(tt_timer_set_thread_priority), + ESP_ELFSYM_EXPORT(tt_timezone_set), + ESP_ELFSYM_EXPORT(tt_timezone_get_name), + ESP_ELFSYM_EXPORT(tt_timezone_get_code), + ESP_ELFSYM_EXPORT(tt_timezone_is_format_24_hour), + ESP_ELFSYM_EXPORT(tt_timezone_set_format_24_hour), + ESP_ELFSYM_EXPORT(tt_wifi_get_radio_state), + ESP_ELFSYM_EXPORT(tt_wifi_radio_state_to_string), + ESP_ELFSYM_EXPORT(tt_wifi_scan), + ESP_ELFSYM_EXPORT(tt_wifi_is_scanning), + ESP_ELFSYM_EXPORT(tt_wifi_get_connection_target), + ESP_ELFSYM_EXPORT(tt_wifi_set_enabled), + ESP_ELFSYM_EXPORT(tt_wifi_connect), + ESP_ELFSYM_EXPORT(tt_wifi_disconnect), + ESP_ELFSYM_EXPORT(tt_wifi_is_connnection_secure), + ESP_ELFSYM_EXPORT(tt_wifi_get_rssi), // tt::lvgl ESP_ELFSYM_EXPORT(tt_lvgl_spinner_create), // lv_event diff --git a/TactilityC/Source/tt_preferences.cpp b/TactilityC/Source/tt_preferences.cpp new file mode 100644 index 00000000..5aeb6d1d --- /dev/null +++ b/TactilityC/Source/tt_preferences.cpp @@ -0,0 +1,53 @@ +#include "tt_preferences.h" +#include +#include + +#define HANDLE_AS_PREFERENCES(handle) ((tt::Preferences*)(handle)) + +extern "C" { + +PreferencesHandle tt_preferences_alloc(const char* identifier) { + return new tt::Preferences(identifier); +} + +void tt_preferences_free(PreferencesHandle handle) { + delete HANDLE_AS_PREFERENCES(handle); +} + +bool tt_preferences_opt_bool(PreferencesHandle handle, const char* key, bool* out) { + return HANDLE_AS_PREFERENCES(handle)->optBool(key, *out); +} + +bool tt_preferences_opt_int32(PreferencesHandle handle, const char* key, int32_t* out) { + return HANDLE_AS_PREFERENCES(handle)->optInt32(key, *out); +} +bool tt_preferences_opt_string(PreferencesHandle handle, const char* key, char* out, uint32_t outSize) { + std::string out_string; + + if (!HANDLE_AS_PREFERENCES(handle)->optString(key, out_string)) { + return false; + } + + if (out_string.length() >= outSize) { + // Need 1 byte to add 0 at the end + return false; + } + + memcpy(out, out_string.c_str(), out_string.length()); + out[out_string.length()] = 0x00; + return true; +} + +void tt_preferences_put_bool(PreferencesHandle handle, const char* key, bool value) { + HANDLE_AS_PREFERENCES(handle)->putBool(key, value); +} + +void tt_preferences_put_int32(PreferencesHandle handle, const char* key, int32_t value) { + HANDLE_AS_PREFERENCES(handle)->putInt32(key, value); +} + +void tt_preferences_put_string(PreferencesHandle handle, const char* key, const char* value) { + HANDLE_AS_PREFERENCES(handle)->putString(key, value); +} + +} \ No newline at end of file diff --git a/TactilityC/Source/tt_time.cpp b/TactilityC/Source/tt_time.cpp new file mode 100644 index 00000000..9a255e8f --- /dev/null +++ b/TactilityC/Source/tt_time.cpp @@ -0,0 +1,42 @@ +#include "tt_time.h" + +#include +#include + +using namespace tt; + +extern "C" { + +void tt_timezone_set(const char* name, const char* code) { + time::setTimeZone(name, code); +} + +bool tt_timezone_get_name(char* buffer, size_t bufferSize) { + auto name = time::getTimeZoneName(); + if (bufferSize < (name.length() + 1)) { + return false; + } else { + strcpy(buffer, name.c_str()); + return true; + } +} + +bool tt_timezone_get_code(char* buffer, size_t bufferSize) { + auto code = time::getTimeZoneCode(); + if (bufferSize < (code.length() + 1)) { + return false; + } else { + strcpy(buffer, code.c_str()); + return true; + } +} + +bool tt_timezone_is_format_24_hour() { + return time::isTimeFormat24Hour(); +} + +void tt_timezone_set_format_24_hour(bool show24Hour) { + return time::setTimeFormat24Hour(show24Hour); +} + +} diff --git a/TactilityC/Source/tt_timer.cpp b/TactilityC/Source/tt_timer.cpp index 62ad2bbb..42143511 100644 --- a/TactilityC/Source/tt_timer.cpp +++ b/TactilityC/Source/tt_timer.cpp @@ -7,7 +7,6 @@ struct TimerWrapper { extern "C" { - TimerHandle tt_timer_alloc(TimerType type, TimerCallback callback, void* callbackContext) { auto wrapper = std::make_shared(); wrapper->timer = std::make_unique((tt::Timer::Type)type, [callback, callbackContext](){ callback(callbackContext); }); @@ -54,4 +53,3 @@ void tt_timer_set_thread_priority(TimerHandle handle, ThreadPriority priority) { } } - diff --git a/TactilityC/Source/tt_wifi.cpp b/TactilityC/Source/tt_wifi.cpp new file mode 100644 index 00000000..b64441f7 --- /dev/null +++ b/TactilityC/Source/tt_wifi.cpp @@ -0,0 +1,55 @@ +#include "tt_wifi.h" + +#include +#include +#include + +using namespace tt::service; + +extern "C" { + +WifiRadioState tt_wifi_get_radio_state() { + return static_cast(wifi::getRadioState()); +} +const char* tt_wifi_radio_state_to_string(WifiRadioState state) { + return wifi::radioStateToString(static_cast(state)); +} + +void tt_wifi_scan() { + wifi::scan(); +} + +bool tt_wifi_is_scanning() { + return wifi::isScanning(); +} + +void tt_wifi_get_connection_target(char* buffer) { + auto target = wifi::getConnectionTarget(); + strcpy(buffer, target.c_str()); +} + +void tt_wifi_set_enabled(bool enabled) { + wifi::setEnabled(enabled); +} + +void tt_wifi_connect(const char* ssid, const char* password, int32_t channel, bool autoConnect, bool remember) { + wifi::settings::WifiApSettings settings; + strcpy(settings.ssid, ssid); + strcpy(settings.password, password); + settings.channel = channel; + settings.auto_connect = autoConnect; +} + +void tt_wifi_disconnect() { + wifi::disconnect(); +} + +bool tt_wifi_is_connnection_secure() { + return wifi::isConnectionSecure(); +} + +int tt_wifi_get_rssi() { + return wifi::getRssi(); +} + +} From a091923353324f1d974f5672a866be31ffc9a615 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Mon, 9 Jun 2025 21:03:18 +0200 Subject: [PATCH 08/16] Fixed & improved build scripts (#288) * Fixed&improved build scripts * Fixes and updates * Fix for esp-idf-ci-action * Build fixes --- .github/actions/build-firmware/action.yml | 2 +- .github/actions/build-sdk/action.yml | 8 ++++++-- Buildscripts/board.cmake | 4 ++-- Buildscripts/build.sh | 5 +++-- CMakeLists.txt | 2 +- Documentation/releasing.md | 22 ++++++++++++++++++++++ ExternalApps/HelloWorld/tactility.py | 2 +- 7 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 Documentation/releasing.md diff --git a/.github/actions/build-firmware/action.yml b/.github/actions/build-firmware/action.yml index 4c44b347..fbf826c6 100644 --- a/.github/actions/build-firmware/action.yml +++ b/.github/actions/build-firmware/action.yml @@ -18,7 +18,7 @@ runs: shell: bash run: cp sdkconfig.board.${{ inputs.board_id }} sdkconfig - name: 'Build' - uses: espressif/esp-idf-ci-action@main + uses: espressif/esp-idf-ci-action@v1 with: esp_idf_version: v5.4 target: ${{ inputs.arch }} diff --git a/.github/actions/build-sdk/action.yml b/.github/actions/build-sdk/action.yml index 4348680a..96de2367 100644 --- a/.github/actions/build-sdk/action.yml +++ b/.github/actions/build-sdk/action.yml @@ -18,14 +18,18 @@ runs: shell: bash run: cp sdkconfig.board.${{ inputs.board_id }} sdkconfig - name: 'Build' - uses: espressif/esp-idf-ci-action@main + uses: espressif/esp-idf-ci-action@v1 with: + # NOTE: Update with ESP-IDF! esp_idf_version: v5.4 target: ${{ inputs.arch }} path: './' - name: 'Release' shell: bash - run: Buildscripts/release-sdk.sh release/TactilitySDK + env: + # NOTE: Update with ESP-IDF! + ESP_IDF_VERSION: '5.4' + run: Buildscripts/release-sdk.sh release/TactilitySDK - name: 'Upload Artifact' uses: actions/upload-artifact@v4 with: diff --git a/Buildscripts/board.cmake b/Buildscripts/board.cmake index 1dc297da..29afb107 100644 --- a/Buildscripts/board.cmake +++ b/Buildscripts/board.cmake @@ -19,7 +19,7 @@ function(INIT_TACTILITY_GLOBALS SDKCONFIG_FILE) set(id_length 0) math(EXPR id_length "${sdkconfig_board_id_length} - 21") string(SUBSTRING ${sdkconfig_board_id} 20 ${id_length} board_id) - message("Building board: ${Cyan}${board_id}${ColorReset}") + message("Board name: ${Cyan}${board_id}${ColorReset}") if (board_id STREQUAL "cyd-2432s024c") set(TACTILITY_BOARD_PROJECT CYD-2432S024C) @@ -62,7 +62,7 @@ function(INIT_TACTILITY_GLOBALS SDKCONFIG_FILE) if (TACTILITY_BOARD_PROJECT STREQUAL "") message(FATAL_ERROR "No subproject mapped to \"${TACTILITY_BOARD_ID}\" in root Buildscripts/board.cmake") else () - message("Board project: ${Cyan}Boards/${TACTILITY_BOARD_PROJECT}${ColorReset}\n") + message("Board path: ${Cyan}Boards/${TACTILITY_BOARD_PROJECT}${ColorReset}\n") set_property(GLOBAL PROPERTY TACTILITY_BOARD_PROJECT ${TACTILITY_BOARD_PROJECT}) set_property(GLOBAL PROPERTY TACTILITY_BOARD_ID ${board_id}) endif () diff --git a/Buildscripts/build.sh b/Buildscripts/build.sh index 9f7b4af3..b4abca05 100755 --- a/Buildscripts/build.sh +++ b/Buildscripts/build.sh @@ -27,12 +27,13 @@ fi echoNewPhase "Cleaning build folder" -rm -rf build +#rm -rf build echoNewPhase "Building $sdkconfig_file" cp $sdkconfig_file sdkconfig -if not idf.py build; then +idf.py build +if [[ $? != 0 ]]; then fatalError "Failed to build esp32s3 SDK" fi diff --git a/CMakeLists.txt b/CMakeLists.txt index d7907658..6ff5f4c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,7 +21,7 @@ file(READ version.txt TACTILITY_VERSION) add_compile_definitions(TT_VERSION="${TACTILITY_VERSION}") if (DEFINED ENV{ESP_IDF_VERSION}) - message("Building with ESP-IDF ${Cyan}v$ENV{ESP_IDF_VERSION}${ColorReset}") + message("Using ESP-IDF ${Cyan}v$ENV{ESP_IDF_VERSION}${ColorReset}") include($ENV{IDF_PATH}/tools/cmake/project.cmake) include("Buildscripts/board.cmake") diff --git a/Documentation/releasing.md b/Documentation/releasing.md new file mode 100644 index 00000000..d5fb207e --- /dev/null +++ b/Documentation/releasing.md @@ -0,0 +1,22 @@ +# Releasing Tactility + +1. Test the latest version on several devices +2. Build the SDK locally and test it with `ExternalApps/HelloWorld` +3. Test the latest SDK build from GitHub with the CDN: + 1. Download it from the [main branch](https://github.com/ByteWelder/Tactility/actions/workflows/build-sdk.yml) + 2. Upload it to the [CDN](https://dash.cloudflare.com) + 3. Update `sdk.json` from [TactilityTool](https://github.com/ByteWelder/TactilityTool) and upload it to [CDN](https://dash.cloudflare.com) + 4. Test it with `ExternalApps/HelloWorld` (clear all its cache and update the SDK version) +4. Download the latest firmwares [main branch](https://github.com/ByteWelder/Tactility/actions/workflows/build-firmware.yml) +5. Prepare a new version of [TactilityWebInstaller](https://github.com/ByteWelder/TactilityWebInstaller) locally +6. Test the firmwares on all devices with the local web installer +7. If all went well: release the web installer +8. Test web installer in production (use popular devices) +9. Make a new version of the docs available at [TactilityDocs](https://github.com/ByteWelder/TactilityDocs) +10. Make a new [GitHub release](https://github.com/ByteWelder/Tactility/releases/new) + +### Post-release + +1. Mention on Discord +2. Consider notifying vendors/stakeholders +3. Remove dev versions in `sdk.json`from [TactilityTool](https://github.com/ByteWelder/TactilityTool) and upload it to [CDN](https://dash.cloudflare.com) \ No newline at end of file diff --git a/ExternalApps/HelloWorld/tactility.py b/ExternalApps/HelloWorld/tactility.py index c41b2040..e27ed647 100644 --- a/ExternalApps/HelloWorld/tactility.py +++ b/ExternalApps/HelloWorld/tactility.py @@ -217,7 +217,7 @@ def sdk_download(version, platform): print(f"Downloading SDK version {version} for {platform}") if download_file(sdk_url, filepath): with zipfile.ZipFile(filepath, "r") as zip_ref: - zip_ref.extractall(sdk_root_dir) + zip_ref.extractall(os.path.join(sdk_root_dir, "TactilitySDK")) return True else: return False From f9acf04dcbaddeaba924d6b28d6bcfb162012080 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Mon, 9 Jun 2025 21:05:21 +0200 Subject: [PATCH 09/16] Update tactility.py and tactility.properties for new release (#289) --- ExternalApps/HelloWorld/tactility.properties | 2 +- ExternalApps/HelloWorld/tactility.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ExternalApps/HelloWorld/tactility.properties b/ExternalApps/HelloWorld/tactility.properties index 14c0bdfb..aa5aa4a2 100644 --- a/ExternalApps/HelloWorld/tactility.properties +++ b/ExternalApps/HelloWorld/tactility.properties @@ -1,2 +1,2 @@ [sdk] -version = 0.4.0-dev +version = 0.4.0 diff --git a/ExternalApps/HelloWorld/tactility.py b/ExternalApps/HelloWorld/tactility.py index e27ed647..310de9f3 100644 --- a/ExternalApps/HelloWorld/tactility.py +++ b/ExternalApps/HelloWorld/tactility.py @@ -15,7 +15,7 @@ platform_targets = ["esp32", "esp32s3"] platform_arguments = platform_targets.copy() platform_arguments.append("all") ttbuild_path = ".tactility" -ttbuild_version = "0.3.0" +ttbuild_version = "1.0.0" ttbuild_properties_file = "tactility.properties" ttbuild_cdn = "https://cdn.tactility.one" ttbuild_sdk_json_validity = 3600 # seconds From 29e43505172cf6719954f7b3b87e01876ecabc94 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sun, 15 Jun 2025 13:28:09 +0200 Subject: [PATCH 10/16] Update docs and version (#290) --- Documentation/releasing.md | 8 +++++++- version.txt | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Documentation/releasing.md b/Documentation/releasing.md index d5fb207e..be83f7a4 100644 --- a/Documentation/releasing.md +++ b/Documentation/releasing.md @@ -8,7 +8,13 @@ 3. Update `sdk.json` from [TactilityTool](https://github.com/ByteWelder/TactilityTool) and upload it to [CDN](https://dash.cloudflare.com) 4. Test it with `ExternalApps/HelloWorld` (clear all its cache and update the SDK version) 4. Download the latest firmwares [main branch](https://github.com/ByteWelder/Tactility/actions/workflows/build-firmware.yml) -5. Prepare a new version of [TactilityWebInstaller](https://github.com/ByteWelder/TactilityWebInstaller) locally +5. Prepare a new version of [TactilityWebInstaller](https://github.com/ByteWelder/TactilityWebInstaller) locally: + 1. Copy the GitHub firmwares into `scripts/` in the `TactilityWebInstaller` project + 2. Run `python release-all.py` + 3. Copy the unpacked files to `/rom/(device)/(version)/` and copy in `manifest.json` from existing release + 1. **WARNING** If the partitions have changed, update the json! + 4. Update the version in `manifest.json` + 5. Update `version.json` for the device 6. Test the firmwares on all devices with the local web installer 7. If all went well: release the web installer 8. Test web installer in production (use popular devices) diff --git a/version.txt b/version.txt index 60a2d3e9..79a2734b 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.4.0 \ No newline at end of file +0.5.0 \ No newline at end of file From 6de0f442fbe44ed26ace8aadbe4e242178e22186 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sun, 15 Jun 2025 13:49:00 +0200 Subject: [PATCH 11/16] Updated screenshots and .gitignore of HelloWorld (#291) * T-Deck Pro work in progress * Add .gitignore to HelloWorld --- .github/workflows/build-firmware.yml | 18 ++ App/Kconfig | 4 + App/Source/Boards.h | 14 +- Boards/LilygoTdeck/Source/hal/TdeckDisplay.h | 35 --- Boards/LilygoTdeckPro/CMakeLists.txt | 7 + Boards/LilygoTdeckPro/Source/Init.cpp | 63 ++++ Boards/LilygoTdeckPro/Source/LilygoTdeck.cpp | 108 +++++++ Boards/LilygoTdeckPro/Source/LilygoTdeck.h | 5 + Boards/LilygoTdeckPro/Source/hal/GxEPD2.h | 114 +++++++ .../Source/hal/GxEPD2_310_GDEQ031T10.cpp | 297 ++++++++++++++++++ .../Source/hal/GxEPD2_310_GDEQ031T10.h | 100 ++++++ .../LilygoTdeckPro/Source/hal/GxEPD2_EPD.cpp | 161 ++++++++++ Boards/LilygoTdeckPro/Source/hal/GxEPD2_EPD.h | 143 +++++++++ .../Source/hal/TdeckDisplay.cpp | 45 +++ .../LilygoTdeckPro/Source/hal/TdeckDisplay.h | 40 +++ .../Source/hal/TdeckDisplayConstants.h | 8 + .../Source/hal/TdeckKeyboard.cpp | 67 ++++ .../LilygoTdeckPro/Source/hal/TdeckKeyboard.h | 25 ++ .../LilygoTdeckPro/Source/hal/TdeckPower.cpp | 130 ++++++++ Boards/LilygoTdeckPro/Source/hal/TdeckPower.h | 30 ++ .../LilygoTdeckPro/Source/hal/TdeckSdCard.cpp | 33 ++ .../LilygoTdeckPro/Source/hal/TdeckSdCard.h | 7 + Buildscripts/board.cmake | 4 + ExternalApps/HelloWorld/.gitignore | 2 +- sdkconfig.board.lilygo-tdeck-plus | 53 ++++ sdkconfig.board.lilygo-tdeck-pro | 57 ++++ 26 files changed, 1527 insertions(+), 43 deletions(-) create mode 100644 Boards/LilygoTdeckPro/CMakeLists.txt create mode 100644 Boards/LilygoTdeckPro/Source/Init.cpp create mode 100644 Boards/LilygoTdeckPro/Source/LilygoTdeck.cpp create mode 100644 Boards/LilygoTdeckPro/Source/LilygoTdeck.h create mode 100644 Boards/LilygoTdeckPro/Source/hal/GxEPD2.h create mode 100644 Boards/LilygoTdeckPro/Source/hal/GxEPD2_310_GDEQ031T10.cpp create mode 100644 Boards/LilygoTdeckPro/Source/hal/GxEPD2_310_GDEQ031T10.h create mode 100644 Boards/LilygoTdeckPro/Source/hal/GxEPD2_EPD.cpp create mode 100644 Boards/LilygoTdeckPro/Source/hal/GxEPD2_EPD.h create mode 100644 Boards/LilygoTdeckPro/Source/hal/TdeckDisplay.cpp create mode 100644 Boards/LilygoTdeckPro/Source/hal/TdeckDisplay.h create mode 100644 Boards/LilygoTdeckPro/Source/hal/TdeckDisplayConstants.h create mode 100644 Boards/LilygoTdeckPro/Source/hal/TdeckKeyboard.cpp create mode 100644 Boards/LilygoTdeckPro/Source/hal/TdeckKeyboard.h create mode 100644 Boards/LilygoTdeckPro/Source/hal/TdeckPower.cpp create mode 100644 Boards/LilygoTdeckPro/Source/hal/TdeckPower.h create mode 100644 Boards/LilygoTdeckPro/Source/hal/TdeckSdCard.cpp create mode 100644 Boards/LilygoTdeckPro/Source/hal/TdeckSdCard.h create mode 100644 sdkconfig.board.lilygo-tdeck-plus create mode 100644 sdkconfig.board.lilygo-tdeck-pro diff --git a/.github/workflows/build-firmware.yml b/.github/workflows/build-firmware.yml index 2e8f4434..47d58ab5 100644 --- a/.github/workflows/build-firmware.yml +++ b/.github/workflows/build-firmware.yml @@ -126,6 +126,24 @@ jobs: with: board_id: lilygo-tdeck arch: esp32s3 + lilygo-tdeck-plus: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: "Build" + uses: ./.github/actions/build-firmware + with: + board_id: lilygo-tdeck-plus + arch: esp32s3 + lilygo-tdeck-pro: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: "Build" + uses: ./.github/actions/build-firmware + with: + board_id: lilygo-tdeck-pro + arch: esp32s3 m5stack-core2: runs-on: ubuntu-latest steps: diff --git a/App/Kconfig b/App/Kconfig index 302912a1..73f961c2 100644 --- a/App/Kconfig +++ b/App/Kconfig @@ -37,6 +37,10 @@ menu "Tactility App" bool "Elecrow CrowPanel Basic 5.0" config TT_BOARD_LILYGO_TDECK bool "LilyGo T-Deck" + config TT_BOARD_LILYGO_TDECK_PLUS + bool "LilyGo T-Deck Plus" + config TT_BOARD_LILYGO_TDECK_PRO + bool "LilyGo T-Deck Pro" config TT_BOARD_M5STACK_CORE2 bool "M5Stack Core2" config TT_BOARD_M5STACK_CORES3 diff --git a/App/Source/Boards.h b/App/Source/Boards.h index 982b0cee..f42203e1 100644 --- a/App/Source/Boards.h +++ b/App/Source/Boards.h @@ -5,7 +5,7 @@ #include // Supported hardware: -#if defined(CONFIG_TT_BOARD_LILYGO_TDECK) +#if defined(CONFIG_TT_BOARD_LILYGO_TDECK) || defined(CONFIG_TT_BOARD_LILYGO_TDECK_PLUS) || defined(CONFIG_TT_BOARD_LILYGO_TDECK_PRO) #include "LilygoTdeck.h" #define TT_BOARD_HARDWARE &lilygo_tdeck #elif defined(CONFIG_TT_BOARD_CYD_2432S024C) @@ -14,22 +14,22 @@ #elif defined(CONFIG_TT_BOARD_CYD_2432S032C) #include "CYD2432S032C.h" #define TT_BOARD_HARDWARE &cyd_2432S032c_config -#elif (defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_ADVANCE_28)) +#elif defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_ADVANCE_28) #define TT_BOARD_HARDWARE &crowpanel_advance_28 #include "CrowPanelAdvance28.h" -#elif (defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_ADVANCE_35)) +#elif defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_ADVANCE_35) #define TT_BOARD_HARDWARE &crowpanel_advance_35 #include "CrowPanelAdvance35.h" -#elif (defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_ADVANCE_50)) +#elif defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_ADVANCE_50) #define TT_BOARD_HARDWARE &crowpanel_advance_50 #include "CrowPanelAdvance50.h" -#elif (defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_BASIC_28)) +#elif defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_BASIC_28) #define TT_BOARD_HARDWARE &crowpanel_basic_28 #include "CrowPanelBasic28.h" -#elif (defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_BASIC_35)) +#elif defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_BASIC_35) #define TT_BOARD_HARDWARE &crowpanel_basic_35 #include "CrowPanelBasic35.h" -#elif (defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_BASIC_50)) +#elif defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_BASIC_50) #define TT_BOARD_HARDWARE &crowpanel_basic_50 #include "CrowPanelBasic50.h" #elif defined(CONFIG_TT_BOARD_M5STACK_CORE2) diff --git a/Boards/LilygoTdeck/Source/hal/TdeckDisplay.h b/Boards/LilygoTdeck/Source/hal/TdeckDisplay.h index e9a26ec4..5a0d81b3 100644 --- a/Boards/LilygoTdeck/Source/hal/TdeckDisplay.h +++ b/Boards/LilygoTdeck/Source/hal/TdeckDisplay.h @@ -1,40 +1,5 @@ #pragma once #include "Tactility/hal/display/DisplayDevice.h" -#include -#include - -class TdeckDisplay : public tt::hal::display::DisplayDevice { - -private: - - esp_lcd_panel_io_handle_t ioHandle = nullptr; - esp_lcd_panel_handle_t panelHandle = nullptr; - lv_display_t* displayHandle = nullptr; - bool poweredOn = false; - -public: - - std::string getName() const final { return "ST7789"; } - std::string getDescription() const final { return "SPI display"; } - - bool start() override; - - bool stop() override; - - void setPowerOn(bool turnOn) override; - bool isPoweredOn() const override { return poweredOn; }; - bool supportsPowerControl() const override { return true; } - - std::shared_ptr _Nullable createTouch() override; - - void setBacklightDuty(uint8_t backlightDuty) override; - bool supportsBacklightDuty() const override { return true; } - - void setGammaCurve(uint8_t index) override; - uint8_t getGammaCurveCount() const override { return 4; }; - - lv_display_t* _Nullable getLvglDisplay() const override { return displayHandle; } -}; std::shared_ptr createDisplay(); diff --git a/Boards/LilygoTdeckPro/CMakeLists.txt b/Boards/LilygoTdeckPro/CMakeLists.txt new file mode 100644 index 00000000..d1096554 --- /dev/null +++ b/Boards/LilygoTdeckPro/CMakeLists.txt @@ -0,0 +1,7 @@ +file(GLOB_RECURSE SOURCE_FILES Source/*.c*) + +idf_component_register( + SRCS ${SOURCE_FILES} + INCLUDE_DIRS "Source" + REQUIRES Tactility esp_lvgl_port esp_lcd ST7789 GT911 PwmBacklight driver esp_adc +) diff --git a/Boards/LilygoTdeckPro/Source/Init.cpp b/Boards/LilygoTdeckPro/Source/Init.cpp new file mode 100644 index 00000000..c8c4b6df --- /dev/null +++ b/Boards/LilygoTdeckPro/Source/Init.cpp @@ -0,0 +1,63 @@ +#include "PwmBacklight.h" +#include "Tactility/kernel/SystemEvents.h" +#include "Tactility/service/gps/GpsService.h" + +#include +#include + +#define TAG "tdeck" + +// Power on +#define TDECK_POWERON_GPIO GPIO_NUM_10 + +static bool powerOn() { + gpio_config_t device_power_signal_config = { + .pin_bit_mask = BIT64(TDECK_POWERON_GPIO), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + + if (gpio_config(&device_power_signal_config) != ESP_OK) { + return false; + } + + if (gpio_set_level(TDECK_POWERON_GPIO, 1) != ESP_OK) { + return false; + } + + return true; +} + +bool tdeckInit() { + ESP_LOGI(TAG, LOG_MESSAGE_POWER_ON_START); + if (!powerOn()) { + TT_LOG_E(TAG, LOG_MESSAGE_POWER_ON_FAILED); + return false; + } + + /* 32 Khz and higher gives an issue where the screen starts dimming again above 80% brightness + * when moving the brightness slider rapidly from a lower setting to 100%. + * This is not a slider bug (data was debug-traced) */ + if (!driver::pwmbacklight::init(GPIO_NUM_42, 30000)) { + TT_LOG_E(TAG, "Backlight init failed"); + return false; + } + + tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) { + auto gps_service = tt::service::gps::findGpsService(); + if (gps_service != nullptr) { + std::vector gps_configurations; + gps_service->getGpsConfigurations(gps_configurations); + if (gps_configurations.empty()) { + if (gps_service->addGpsConfiguration(tt::hal::gps::GpsConfiguration {.uartName = "Grove", .baudRate = 38400, .model = tt::hal::gps::GpsModel::UBLOX10})) { + TT_LOG_I(TAG, "Configured internal GPS"); + } else { + TT_LOG_E(TAG, "Failed to configure internal GPS"); + } + } + } + }); + return true; +} diff --git a/Boards/LilygoTdeckPro/Source/LilygoTdeck.cpp b/Boards/LilygoTdeckPro/Source/LilygoTdeck.cpp new file mode 100644 index 00000000..d88e4054 --- /dev/null +++ b/Boards/LilygoTdeckPro/Source/LilygoTdeck.cpp @@ -0,0 +1,108 @@ +#include "Tactility/lvgl/LvglSync.h" +#include "hal/TdeckDisplay.h" +#include "hal/TdeckDisplayConstants.h" +#include "hal/TdeckKeyboard.h" +#include "hal/TdeckPower.h" +#include "hal/TdeckSdCard.h" + +#include + +#define TDECK_SPI_TRANSFER_SIZE_LIMIT (TDECK_LCD_HORIZONTAL_RESOLUTION * TDECK_LCD_SPI_TRANSFER_HEIGHT * (LV_COLOR_DEPTH / 8)) + +bool tdeckInit(); + +using namespace tt::hal; + +extern const Configuration lilygo_tdeck = { + .initBoot = tdeckInit, + .createDisplay = createDisplay, + .createKeyboard = createKeyboard, + .sdcard = createTdeckSdCard(), + .power = tdeck_get_power, + .i2c = { + i2c::Configuration { + .name = "Internal", + .port = I2C_NUM_0, + .initMode = i2c::InitMode::ByTactility, + .isMutable = false, + .config = (i2c_config_t) { + .mode = I2C_MODE_MASTER, + .sda_io_num = GPIO_NUM_18, + .scl_io_num = GPIO_NUM_8, + .sda_pullup_en = true, + .scl_pullup_en = true, + .master = { + .clk_speed = 400000 + }, + .clk_flags = 0 + } + }, + i2c::Configuration { + .name = "External", + .port = I2C_NUM_1, + .initMode = i2c::InitMode::Disabled, + .isMutable = true, + .config = (i2c_config_t) { + .mode = I2C_MODE_MASTER, + .sda_io_num = GPIO_NUM_43, + .scl_io_num = GPIO_NUM_44, + .sda_pullup_en = false, + .scl_pullup_en = false, + .master = { + .clk_speed = 400000 + }, + .clk_flags = 0 + } + } + }, + .spi { + spi::Configuration { + .device = SPI2_HOST, + .dma = SPI_DMA_CH_AUTO, + .config = { + .mosi_io_num = GPIO_NUM_41, + .miso_io_num = GPIO_NUM_38, + .sclk_io_num = GPIO_NUM_40, + .quadwp_io_num = GPIO_NUM_NC, // Quad SPI LCD driver is not yet supported + .quadhd_io_num = GPIO_NUM_NC, // Quad SPI LCD driver is not yet supported + .data4_io_num = GPIO_NUM_NC, + .data5_io_num = GPIO_NUM_NC, + .data6_io_num = GPIO_NUM_NC, + .data7_io_num = GPIO_NUM_NC, + .data_io_default_level = false, + .max_transfer_sz = TDECK_SPI_TRANSFER_SIZE_LIMIT, + .flags = 0, + .isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO, + .intr_flags = 0 + }, + .initMode = spi::InitMode::ByTactility, + .isMutable = false, + .lock = tt::lvgl::getSyncLock() // esp_lvgl_port owns the lock for the display + } + }, + .uart { + uart::Configuration { + .name = "Grove", + .port = UART_NUM_1, + .rxPin = GPIO_NUM_44, + .txPin = GPIO_NUM_43, + .rtsPin = GPIO_NUM_NC, + .ctsPin = GPIO_NUM_NC, + .rxBufferSize = 1024, + .txBufferSize = 1024, + .config = { + .baud_rate = 38400, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .rx_flow_ctrl_thresh = 0, + .source_clk = UART_SCLK_DEFAULT, + .flags = { + .allow_pd = 0, + .backup_before_sleep = 0, + } + } + } + } +}; diff --git a/Boards/LilygoTdeckPro/Source/LilygoTdeck.h b/Boards/LilygoTdeckPro/Source/LilygoTdeck.h new file mode 100644 index 00000000..5edc402d --- /dev/null +++ b/Boards/LilygoTdeckPro/Source/LilygoTdeck.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +extern const tt::hal::Configuration lilygo_tdeck; diff --git a/Boards/LilygoTdeckPro/Source/hal/GxEPD2.h b/Boards/LilygoTdeckPro/Source/hal/GxEPD2.h new file mode 100644 index 00000000..3c18dec0 --- /dev/null +++ b/Boards/LilygoTdeckPro/Source/hal/GxEPD2.h @@ -0,0 +1,114 @@ +// Display Library for SPI e-paper panels from Dalian Good Display and boards from Waveshare. +// Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines! +// +// Display Library based on Demo Example from Good Display: https://www.good-display.com/companyfile/32/ +// +// Author: Jean-Marc Zingg +// +// Version: see library.properties +// +// Library: https://github.com/ZinggJM/GxEPD2 + +#pragma once + +// color definitions for GxEPD, GxEPD2 and GxEPD_HD, values correspond to RGB565 values for TFTs +#define GxEPD_BLACK 0x0000 +#define GxEPD_WHITE 0xFFFF +// some controllers for b/w EPDs support grey levels +#define GxEPD_DARKGREY 0x7BEF // 128, 128, 128 +#define GxEPD_LIGHTGREY 0xC618 // 192, 192, 192 +// values for 3-color or 7-color EPDs +#define GxEPD_RED 0xF800 // 255, 0, 0 +#define GxEPD_YELLOW 0xFFE0 // 255, 255, 0 !!no longer same as GxEPD_RED!! +#define GxEPD_COLORED GxEPD_RED +// values for 7-color EPDs only +#define GxEPD_BLUE 0x001F // 0, 0, 255 +#define GxEPD_GREEN 0x07E0 // 0, 255, 0 +#define GxEPD_ORANGE 0xFC00 // 255, 128, 0 + +class GxEPD2 +{ + public: + enum Panel + { + GDEW0102T4, Waveshare_1_02_bw = GDEW0102T4, + GDEP015OC1, Waveshare_1_54_bw = GDEP015OC1, + DEPG0150BN, + GDEH0154D67, Waveshare_1_54_bw_D67 = GDEH0154D67, + GDEW0154T8, + GDEW0154M09, + GDEW0154M10, + GDEY0154D67, + GDE0213B1, Waveshare_2_13_bw = GDE0213B1, + GDEH0213B72, Waveshare_2_13_bw_B72 = GDEH0213B72, + GDEH0213B73, Waveshare_2_13_bw_B73 = GDEH0213B73, + GDEM0213B74, + GDEW0213I5F, Waveshare_2_13_flex = GDEW0213I5F, + GDEW0213M21, + GDEW0213T5D, + DEPG0213BN, + GDEY0213B74, + GDEW026T0, Waveshare_2_6_bw = GDEW026T0, + GDEW026M01, + DEPG0266BN, + GDEY0266T90, + GDEH029A1, Waveshare_2_9_bw = GDEH029A1, + GDEW029T5, Waveshare_2_9_bw_T5 = GDEW029T5, + GDEW029T5D, + GDEW029I6FD, + GDEW029M06, + GDEM029T94, + GDEY029T94, + DEPG0290BS, + GDEW027W3, Waveshare_2_7_bw = GDEW027W3, + GDEY027T91, + GDEQ031T10, + ED037TC1, + GDEW0371W7, Waveshare_3_7_bw = GDEW0371W7, + GDEW042T2, Waveshare_4_2_bw = GDEW042T2, + GDEW042M01, + GDEY042T81, + GDEQ0426T82, + GDEW0583T7, Waveshare_5_83_bw = GDEW0583T7, + GDEW0583T8, + GDEQ0583T31, + GDEW075T8, Waveshare_7_5_bw = GDEW075T8, + GDEW075T7, Waveshare_7_5_bw_T7 = GDEW075T7, + GDEY075T7, + GDEH116T91, + GDEW1248T3, Waveshare_12_24_bw = GDEW1248T3, + ED060SCT, // on Waveshare IT8951 Driver HAT + ED060KC1, // on Waveshare IT8951 Driver HAT 1448x1072 + ED078KC2, // on Waveshare IT8951 Driver HAT 1872x1404 + ES103TC1, // on Waveshare IT8951 Driver HAT 1872x1404 + // 3-color + GDEW0154Z04, Waveshare_1_54_bwr = GDEW0154Z04, + GDEH0154Z90, Waveshare_1_54_bwr_Z90 = GDEH0154Z90, + GDEW0213Z16, Waveshare_2_13_bwr = GDEW0213Z16, + GDEW0213Z19, + GDEY0213Z98, + GDEW029Z10, Waveshare_2_9_bwr = GDEW029Z10, + GDEH029Z13, + GDEM029C90, + GDEY0266Z90, Waveshare_2_66_bwr = GDEY0266Z90, + GDEW027C44, Waveshare_2_7_bwr = GDEW027C44, + GDEW042Z15, Waveshare_4_2_bwr = GDEW042Z15, + GDEQ042Z21, Waveshare_4_2_V2_bwr = GDEQ042Z21, + GDEW0583Z21, Waveshare_5_83_bwr = GDEW0583Z21, + GDEW0583Z83, + GDEW075Z09, Waveshare_7_5_bwr = GDEW075Z09, + GDEW075Z08, Waveshare_7_5_bwr_Z08 = GDEW075Z08, + GDEH075Z90, Waveshare_7_5_bwr_Z90 = GDEH075Z90, + GDEY1248Z51, + // 4-color + GDEY0266F51H, + GDEY029F51H, + Waveshare3inch4color, + GDEY0420F51, + Waveshare437inch4color, + // 7-color + ACeP565, Waveshare_5_65_7c = ACeP565, + GDEY073D46, + ACeP730, Waveshare_7_30_7c = ACeP730 + }; +}; diff --git a/Boards/LilygoTdeckPro/Source/hal/GxEPD2_310_GDEQ031T10.cpp b/Boards/LilygoTdeckPro/Source/hal/GxEPD2_310_GDEQ031T10.cpp new file mode 100644 index 00000000..84d6f60c --- /dev/null +++ b/Boards/LilygoTdeckPro/Source/hal/GxEPD2_310_GDEQ031T10.cpp @@ -0,0 +1,297 @@ +// Display Library for SPI e-paper panels from Dalian Good Display and boards from Waveshare. +// Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines! +// +// based on Demo Example from Good Display: https://www.good-display.com/product/426.html +// Panel: GDEQ031T10 : https://www.good-display.com/product/426.html +// Controller: UC8253 : https://v4.cecdn.yun300.cn/100001_1909185148/UC8253.pdf +// +// Author: Jean-Marc Zingg +// +// Version: see library.properties +// +// Library: https://github.com/ZinggJM/GxEPD2 + +#include "GxEPD2_310_GDEQ031T10.h" +#include + +constexpr uint32_t LOW = 0; +constexpr uint32_t HIGH = 1; + +GxEPD2_310_GDEQ031T10::GxEPD2_310_GDEQ031T10(int16_t cs, int16_t dc, int16_t rst, int16_t busy) : + GxEPD2_EPD(cs, dc, rst, busy, LOW, 10000000, WIDTH, HEIGHT, panel, hasColor, hasPartialUpdate, hasFastPartialUpdate) {} + +void GxEPD2_310_GDEQ031T10::clearScreen(uint8_t value) { + // full refresh needed for all cases (previous != screen) + _writeScreenBuffer(0x10, value); // set previous + _writeScreenBuffer(0x13, value); // set current + refresh(false); // full refresh + _initial_write = false; +} + +void GxEPD2_310_GDEQ031T10::writeScreenBuffer(uint8_t value) { + if (_initial_write) return clearScreen(value); + _writeScreenBuffer(0x13, value); // set current +} + +void GxEPD2_310_GDEQ031T10::writeScreenBufferAgain(uint8_t value) { + _writeScreenBuffer(0x10, value); // set previous + //_writeScreenBuffer(0x13, value); // set current, not needed +} + +void GxEPD2_310_GDEQ031T10::_writeScreenBuffer(uint8_t command, uint8_t value) { + if (!_init_display_done) _InitDisplay(); + _writeCommand(command); + _startTransfer(); + for (uint32_t i = 0; i < uint32_t(WIDTH) * uint32_t(HEIGHT) / 8; i++) { _transfer(value); } + _endTransfer(); +} + +void GxEPD2_310_GDEQ031T10::writeImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { _writeImage(0x13, bitmap, x, y, w, h, invert, mirror_y, pgm); } + +void GxEPD2_310_GDEQ031T10::writeImageForFullRefresh(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { + _writeImage(0x10, bitmap, x, y, w, h, invert, mirror_y, pgm); // set previous + _writeImage(0x13, bitmap, x, y, w, h, invert, mirror_y, pgm); // set current +} + +void GxEPD2_310_GDEQ031T10::writeImageAgain(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { + _writeImage(0x10, bitmap, x, y, w, h, invert, mirror_y, pgm); // set previous + //_writeImage(0x13, bitmap, x, y, w, h, invert, mirror_y, pgm); // set current, not needed +} + +void GxEPD2_310_GDEQ031T10::_writeImage(uint8_t command, const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { + tt::kernel::delayMillis(1); // WDT hack + uint16_t wb = (w + 7) / 8; // width bytes, bitmaps are padded + x -= x % 8; // byte boundary + w = wb * 8; // byte boundary + int16_t x1 = x < 0 ? 0 : x; // limit + int16_t y1 = y < 0 ? 0 : y; // limit + int16_t w1 = x + w < int16_t(WIDTH) ? w : int16_t(WIDTH) - x; // limit + int16_t h1 = y + h < int16_t(HEIGHT) ? h : int16_t(HEIGHT) - y; // limit + int16_t dx = x1 - x; + int16_t dy = y1 - y; + w1 -= dx; + h1 -= dy; + if ((w1 <= 0) || (h1 <= 0)) return; + if (!_init_display_done) _InitDisplay(); + if (_initial_write) writeScreenBuffer(); // initial full screen buffer clean + _writeCommand(0x91); // partial in + _setPartialRamArea(x1, y1, w1, h1); + _writeCommand(command); + _startTransfer(); + for (int16_t i = 0; i < h1; i++) { + for (int16_t j = 0; j < w1 / 8; j++) { + uint8_t data; + // use wb, h of bitmap for index! + uint16_t idx = mirror_y ? j + dx / 8 + uint16_t((h - 1 - (i + dy))) * wb : j + dx / 8 + uint16_t(i + dy) * wb; + if (pgm) { +#if defined(__AVR) || defined(ESP8266) || defined(ESP32) + data = pgm_read_byte(&bitmap[idx]); +#else + data = bitmap[idx]; +#endif + } else { data = bitmap[idx]; } + if (invert) data = ~data; + _transfer(data); + } + } + _endTransfer(); + _writeCommand(0x92); // partial out + tt::kernel::delayMillis(1); // WDT hack +} + +void GxEPD2_310_GDEQ031T10::writeImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, + int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { _writeImagePart(0x13, bitmap, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); } + +void GxEPD2_310_GDEQ031T10::writeImagePartAgain(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, + int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { + _writeImagePart(0x10, bitmap, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); // set previous + //_writeImagePart(0x13, bitmap, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); // set current, not needed +} + +void GxEPD2_310_GDEQ031T10::_writeImagePart(uint8_t command, const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, + int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { + tt::kernel::delayMillis(1); // WDT hack + if ((w_bitmap < 0) || (h_bitmap < 0) || (w < 0) || (h < 0)) return; + if ((x_part < 0) || (x_part >= w_bitmap)) return; + if ((y_part < 0) || (y_part >= h_bitmap)) return; + uint16_t wb_bitmap = (w_bitmap + 7) / 8; // width bytes, bitmaps are padded + x_part -= x_part % 8; // byte boundary + w = w_bitmap - x_part < w ? w_bitmap - x_part : w; // limit + h = h_bitmap - y_part < h ? h_bitmap - y_part : h; // limit + x -= x % 8; // byte boundary + w = 8 * ((w + 7) / 8); // byte boundary, bitmaps are padded + int16_t x1 = x < 0 ? 0 : x; // limit + int16_t y1 = y < 0 ? 0 : y; // limit + int16_t w1 = x + w < int16_t(WIDTH) ? w : int16_t(WIDTH) - x; // limit + int16_t h1 = y + h < int16_t(HEIGHT) ? h : int16_t(HEIGHT) - y; // limit + int16_t dx = x1 - x; + int16_t dy = y1 - y; + w1 -= dx; + h1 -= dy; + if ((w1 <= 0) || (h1 <= 0)) return; + if (!_init_display_done) _InitDisplay(); + if (_initial_write) writeScreenBuffer(); // initial full screen buffer clean + _writeCommand(0x91); // partial in + _setPartialRamArea(x1, y1, w1, h1); + _writeCommand(command); + _startTransfer(); + for (int16_t i = 0; i < h1; i++) { + for (int16_t j = 0; j < w1 / 8; j++) { + uint8_t data; + // use wb_bitmap, h_bitmap of bitmap for index! + uint16_t idx = mirror_y ? x_part / 8 + j + dx / 8 + uint16_t((h_bitmap - 1 - (y_part + i + dy))) * wb_bitmap : x_part / 8 + j + dx / 8 + uint16_t(y_part + i + dy) * wb_bitmap; + if (pgm) { +#if defined(__AVR) || defined(ESP8266) || defined(ESP32) + data = pgm_read_byte(&bitmap[idx]); +#else + data = bitmap[idx]; +#endif + } else { data = bitmap[idx]; } + if (invert) data = ~data; + _transfer(data); + } + } + _endTransfer(); + _writeCommand(0x92); // partial out + tt::kernel::delayMillis(1); // WDT hack +} + +void GxEPD2_310_GDEQ031T10::writeImage(const uint8_t* black, const uint8_t* color, int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { if (black) { writeImage(black, x, y, w, h, invert, mirror_y, pgm); } } + +void GxEPD2_310_GDEQ031T10::writeImagePart(const uint8_t* black, const uint8_t* color, int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, + int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { if (black) { writeImagePart(black, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); } } + +void GxEPD2_310_GDEQ031T10::writeNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { if (data1) { writeImage(data1, x, y, w, h, invert, mirror_y, pgm); } } + +void GxEPD2_310_GDEQ031T10::drawImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { + writeImage(bitmap, x, y, w, h, invert, mirror_y, pgm); + refresh(x, y, w, h); + writeImageAgain(bitmap, x, y, w, h, invert, mirror_y, pgm); +} + +void GxEPD2_310_GDEQ031T10::drawImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, + int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { + writeImagePart(bitmap, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); + refresh(x, y, w, h); + writeImagePartAgain(bitmap, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); +} + +void GxEPD2_310_GDEQ031T10::drawImage(const uint8_t* black, const uint8_t* color, int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { if (black) { drawImage(black, x, y, w, h, invert, mirror_y, pgm); } } + +void GxEPD2_310_GDEQ031T10::drawImagePart(const uint8_t* black, const uint8_t* color, int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, + int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { if (black) { drawImagePart(black, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); } } + +void GxEPD2_310_GDEQ031T10::drawNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { if (data1) { drawImage(data1, x, y, w, h, invert, mirror_y, pgm); } } + +void GxEPD2_310_GDEQ031T10::refresh(bool partial_update_mode) { + if (partial_update_mode) refresh(0, 0, WIDTH, HEIGHT); + else { + _Update_Full(); + _initial_refresh = false; // initial full update done + } +} + +void GxEPD2_310_GDEQ031T10::refresh(int16_t x, int16_t y, int16_t w, int16_t h) { + if (_initial_refresh) return refresh(false); // initial update needs be full update + // intersection with screen + int16_t w1 = x < 0 ? w + x : w; // reduce + int16_t h1 = y < 0 ? h + y : h; // reduce + int16_t x1 = x < 0 ? 0 : x; // limit + int16_t y1 = y < 0 ? 0 : y; // limit + w1 = x1 + w1 < int16_t(WIDTH) ? w1 : int16_t(WIDTH) - x1; // limit + h1 = y1 + h1 < int16_t(HEIGHT) ? h1 : int16_t(HEIGHT) - y1; // limit + if ((w1 <= 0) || (h1 <= 0)) return; + // make x1, w1 multiple of 8 + w1 += x1 % 8; + if (w1 % 8 > 0) w1 += 8 - w1 % 8; + x1 -= x1 % 8; + if (usePartialUpdateWindow) _writeCommand(0x91); // partial in + _setPartialRamArea(x1, y1, w1, h1); + _Update_Part(); + if (usePartialUpdateWindow) _writeCommand(0x92); // partial out +} + +void GxEPD2_310_GDEQ031T10::powerOff(void) { _PowerOff(); } + +void GxEPD2_310_GDEQ031T10::hibernate() { + _PowerOff(); + if (_rst >= 0) { + _writeCommand(0x07); // deep sleep + _writeData(0xA5); // check code + _hibernating = true; + _init_display_done = false; + } +} + +void GxEPD2_310_GDEQ031T10::_setPartialRamArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + uint16_t xe = (x + w - 1) | 0x0007; // byte boundary inclusive (last byte) + uint16_t ye = y + h - 1; + x &= 0xFFF8; // byte boundary + _writeCommand(0x90); // partial window + _writeData(x); + _writeData(xe); + _writeData(y / 256); + _writeData(y % 256); + _writeData(ye / 256); + _writeData(ye % 256); + _writeData(0x01); +} + +void GxEPD2_310_GDEQ031T10::_PowerOn() { + if (!_power_is_on) { + _writeCommand(0x04); + _waitWhileBusy("_PowerOn", power_on_time); + } + _power_is_on = true; +} + +void GxEPD2_310_GDEQ031T10::_PowerOff() { + if (_power_is_on) { + _writeCommand(0x02); // power off + _waitWhileBusy("_PowerOff", power_off_time); + } + _power_is_on = false; +} + +void GxEPD2_310_GDEQ031T10::_InitDisplay() { + _writeCommand(0x00); // PANEL SETTING + _writeData(0x1e); // soft reset + _writeData(0x0d); + tt::kernel::delayMillis(1); + _power_is_on = false; + _writeCommand(0x00); // PANEL SETTING + _writeData(0x1f); // KW: 3f, KWR: 2F, BWROTP: 0f, BWOTP: 1f + _writeData(0x0d); + _init_display_done = true; +} + +void GxEPD2_310_GDEQ031T10::_Update_Full() { + if (useFastFullUpdate) { + _writeCommand(0xE0); // Cascade Setting (CCSET) + _writeData(0x02); // TSFIX + _writeCommand(0xE5); // Force Temperature (TSSET) + _writeData(0x5A); // 90, 1015000us + //_writeData(0x6E); // 110, 1542001 + } + _writeCommand(0x50); + _writeData(0x97); + _PowerOn(); + _writeCommand(0x12); //display refresh + _waitWhileBusy("_Update_Full", full_refresh_time); + _init_display_done = false; // needed, reason unknown +} + +void GxEPD2_310_GDEQ031T10::_Update_Part() { + if (hasFastPartialUpdate) { + _writeCommand(0xE0); // Cascade Setting (CCSET) + _writeData(0x02); // TSFIX + _writeCommand(0xE5); // Force Temperature (TSSET) + _writeData(0x79); // 121 + } + _writeCommand(0x50); + _writeData(0xD7); + _PowerOn(); + _writeCommand(0x12); //display refresh + _waitWhileBusy("_Update_Part", partial_refresh_time); + _init_display_done = false; // needed, reason unknown +} diff --git a/Boards/LilygoTdeckPro/Source/hal/GxEPD2_310_GDEQ031T10.h b/Boards/LilygoTdeckPro/Source/hal/GxEPD2_310_GDEQ031T10.h new file mode 100644 index 00000000..438f9460 --- /dev/null +++ b/Boards/LilygoTdeckPro/Source/hal/GxEPD2_310_GDEQ031T10.h @@ -0,0 +1,100 @@ +// Display Library for SPI e-paper panels from Dalian Good Display and boards from Waveshare. +// Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines! +// +// based on Demo Example from Good Display: https://www.good-display.com/product/426.html +// Panel: GDEQ031T10 : https://www.good-display.com/product/426.html +// Controller: UC8253 : https://v4.cecdn.yun300.cn/100001_1909185148/UC8253.pdf +// +// Author: Jean-Marc Zingg +// +// Version: see library.properties +// +// Library: https://github.com/ZinggJM/GxEPD2 + +#pragma once + +#include "GxEPD2_EPD.h" + +class GxEPD2_310_GDEQ031T10 : public GxEPD2_EPD +{ +public: + // attributes + static const uint16_t WIDTH = 240; + static const uint16_t WIDTH_VISIBLE = WIDTH; + static const uint16_t HEIGHT = 320; + static const GxEPD2::Panel panel = GxEPD2::GDEQ031T10; + static const bool hasColor = false; + static const bool hasPartialUpdate = true; + static const bool usePartialUpdateWindow = true; // set false for better image + static const bool hasFastPartialUpdate = true; // set this false to force full refresh always + static const bool useFastFullUpdate = true; + // set false for extended (low) temperature range, 1015000us vs 3082001us + static const uint16_t power_on_time = 50; // ms, e.g. 45000us + static const uint16_t power_off_time = 50; // ms, e.g. 45000us + static const uint16_t full_refresh_time = 1100; // ms, e.g. 1015000us + static const uint16_t partial_refresh_time = 700; // ms, e.g. 650000us + // constructor + GxEPD2_310_GDEQ031T10(int16_t cs, int16_t dc, int16_t rst, int16_t busy); + // methods (virtual) + // Support for Bitmaps (Sprites) to Controller Buffer and to Screen + void clearScreen(uint8_t value = 0xFF); // init controller memory and screen (default white) + void writeScreenBuffer(uint8_t value = 0xFF); // init controller memory (default white) + void writeScreenBufferAgain(uint8_t value = 0xFF); // init previous buffer controller memory (default white) + // write to controller memory, without screen refresh; x and w should be multiple of 8 + void writeImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, + bool mirror_y = false, bool pgm = false); + void writeImageForFullRefresh(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, + bool invert = false, bool mirror_y = false, bool pgm = false); + void writeImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, + int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, + bool pgm = false); + void writeImage(const uint8_t* black, const uint8_t* color, int16_t x, int16_t y, int16_t w, int16_t h, + bool invert = false, bool mirror_y = false, bool pgm = false); + void writeImagePart(const uint8_t* black, const uint8_t* color, int16_t x_part, int16_t y_part, int16_t w_bitmap, + int16_t h_bitmap, + int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, + bool pgm = false); + // for differential update: set current and previous buffers equal (for fast partial update to work correctly) + void writeImageAgain(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, + bool mirror_y = false, bool pgm = false); + void writeImagePartAgain(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, + int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, + bool pgm = false); + // write sprite of native data to controller memory, without screen refresh; x and w should be multiple of 8 + void writeNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, + bool invert = false, bool mirror_y = false, bool pgm = false); + // write to controller memory, with screen refresh; x and w should be multiple of 8 + void drawImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, + bool mirror_y = false, bool pgm = false); + void drawImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, + int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, + bool pgm = false); + void drawImage(const uint8_t* black, const uint8_t* color, int16_t x, int16_t y, int16_t w, int16_t h, + bool invert = false, bool mirror_y = false, bool pgm = false); + void drawImagePart(const uint8_t* black, const uint8_t* color, int16_t x_part, int16_t y_part, int16_t w_bitmap, + int16_t h_bitmap, + int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, + bool pgm = false); + // write sprite of native data to controller memory, with screen refresh; x and w should be multiple of 8 + void drawNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, + bool invert = false, bool mirror_y = false, bool pgm = false); + void refresh(bool partial_update_mode = false); // screen refresh from controller memory to full screen + void refresh(int16_t x, int16_t y, int16_t w, int16_t h); // screen refresh from controller memory, partial screen + void powerOff(); // turns off generation of panel driving voltages, avoids screen fading over time + void hibernate(); + // turns powerOff() and sets controller to deep sleep for minimum power use, ONLY if wakeable by RST (rst >= 0) +private: + void _writeScreenBuffer(uint8_t command, uint8_t value); + void _writeImage(uint8_t command, const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, + bool invert = false, bool mirror_y = false, bool pgm = false); + void _writeImagePart(uint8_t command, const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, + int16_t h_bitmap, + int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, + bool pgm = false); + void _setPartialRamArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h); + void _PowerOn(); + void _PowerOff(); + void _InitDisplay(); + void _Update_Full(); + void _Update_Part(); +}; diff --git a/Boards/LilygoTdeckPro/Source/hal/GxEPD2_EPD.cpp b/Boards/LilygoTdeckPro/Source/hal/GxEPD2_EPD.cpp new file mode 100644 index 00000000..cf155c4d --- /dev/null +++ b/Boards/LilygoTdeckPro/Source/hal/GxEPD2_EPD.cpp @@ -0,0 +1,161 @@ +// Display Library for SPI e-paper panels from Dalian Good Display and boards from Waveshare. +// Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines! +// +// Display Library based on Demo Example from Good Display: https://www.good-display.com/companyfile/32/ +// +// Author: Jean-Marc Zingg +// +// Version: see library.properties +// +// Library: https://github.com/ZinggJM/GxEPD2 + +#include "GxEPD2_EPD.h" + +#include +#include + +constexpr auto* TAG = "GxEPD2"; + +GxEPD2_EPD::GxEPD2_EPD(int16_t cs, int16_t dc, int16_t rst, int16_t busy, int16_t busy_level, uint32_t busy_timeout, + uint16_t w, uint16_t h, GxEPD2::Panel p, bool c, bool pu, bool fpu) : + WIDTH(w), HEIGHT(h), panel(p), hasColor(c), hasPartialUpdate(pu), hasFastPartialUpdate(fpu), + _cs(cs), _dc(dc), _rst(rst), _busy(busy), _busy_level(busy_level), _busy_timeout(busy_timeout) { + _initial_write = true; + _initial_refresh = true; + _power_is_on = false; + _using_partial_mode = false; + _hibernating = false; + _init_display_done = false; + _busy_callback = 0; + _busy_callback_parameter = 0; +} + +void GxEPD2_EPD::init() { init(true, 10, false); } + +void GxEPD2_EPD::init(bool initial, uint16_t reset_duration, bool pulldown_rst_mode) { + _initial_write = initial; + _initial_refresh = initial; + _pulldown_rst_mode = pulldown_rst_mode; + _power_is_on = false; + _using_partial_mode = false; + _hibernating = false; + _init_display_done = false; +} + +void GxEPD2_EPD::setBusyCallback(void (*busyCallback)(const void*), const void* busy_callback_parameter) { + _busy_callback = busyCallback; + _busy_callback_parameter = busy_callback_parameter; +} + +void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time) { + if (_busy >= 0) { + tt::kernel::delayMillis(1); // add some margin to become active + unsigned long start = tt::kernel::getMicros(); + while (1) { + if (digitalRead(_busy) != _busy_level) break; + if (_busy_callback) _busy_callback(_busy_callback_parameter); + else tt::kernel::delayMillis(1); + if (digitalRead(_busy) != _busy_level) break; + if (tt::kernel::getMicros() - start > _busy_timeout) { + TT_LOG_W(TAG, "Busy timeout"); + break; + } + vPortYield(); // avoid wdt + } + (void)start; + } else tt::kernel::delayMillis(busy_time); +} + +void GxEPD2_EPD::_writeCommand(uint8_t c) { + _pSPIx->beginTransaction(_spi_settings); + if (_dc >= 0) digitalWrite(_dc, LOW); + if (_cs >= 0) digitalWrite(_cs, LOW); + _pSPIx->transfer(c); + if (_cs >= 0) digitalWrite(_cs, HIGH); + if (_dc >= 0) digitalWrite(_dc, HIGH); + _pSPIx->endTransaction(); +} + +void GxEPD2_EPD::_writeData(uint8_t d) { + _pSPIx->beginTransaction(_spi_settings); + if (_cs >= 0) digitalWrite(_cs, LOW); + _pSPIx->transfer(d); + if (_cs >= 0) digitalWrite(_cs, HIGH); + _pSPIx->endTransaction(); +} + +void GxEPD2_EPD::_writeData(const uint8_t* data, uint16_t n) { + _pSPIx->beginTransaction(_spi_settings); + if (_cs >= 0) digitalWrite(_cs, LOW); + for (uint16_t i = 0; i < n; i++) { _pSPIx->transfer(*data++); } + if (_cs >= 0) digitalWrite(_cs, HIGH); + _pSPIx->endTransaction(); +} + +void GxEPD2_EPD::_writeDataPGM(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes) { + _pSPIx->beginTransaction(_spi_settings); + if (_cs >= 0) digitalWrite(_cs, LOW); + for (uint16_t i = 0; i < n; i++) { _pSPIx->transfer(pgm_read_byte(&*data++)); } + while (fill_with_zeroes > 0) { + _pSPIx->transfer(0x00); + fill_with_zeroes--; + } + if (_cs >= 0) digitalWrite(_cs, HIGH); + _pSPIx->endTransaction(); +} + +void GxEPD2_EPD::_writeDataPGM_sCS(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes) { + _pSPIx->beginTransaction(_spi_settings); + for (uint8_t i = 0; i < n; i++) { + if (_cs >= 0) digitalWrite(_cs, LOW); + _pSPIx->transfer(pgm_read_byte(&*data++)); + if (_cs >= 0) digitalWrite(_cs, HIGH); + } + while (fill_with_zeroes > 0) { + if (_cs >= 0) digitalWrite(_cs, LOW); + _pSPIx->transfer(0x00); + fill_with_zeroes--; + if (_cs >= 0) digitalWrite(_cs, HIGH); + } + _pSPIx->endTransaction(); +} + +void GxEPD2_EPD::_writeCommandData(const uint8_t* pCommandData, uint8_t datalen) { + _pSPIx->beginTransaction(_spi_settings); + if (_dc >= 0) digitalWrite(_dc, LOW); + if (_cs >= 0) digitalWrite(_cs, LOW); + _pSPIx->transfer(*pCommandData++); + if (_dc >= 0) digitalWrite(_dc, HIGH); + for (uint8_t i = 0; i < datalen - 1; i++) // sub the command + { + _pSPIx->transfer(*pCommandData++); + } + if (_cs >= 0) digitalWrite(_cs, HIGH); + _pSPIx->endTransaction(); +} + +void GxEPD2_EPD::_writeCommandDataPGM(const uint8_t* pCommandData, uint8_t datalen) { + _pSPIx->beginTransaction(_spi_settings); + if (_dc >= 0) digitalWrite(_dc, LOW); + if (_cs >= 0) digitalWrite(_cs, LOW); + _pSPIx->transfer(pgm_read_byte(&*pCommandData++)); + if (_dc >= 0) digitalWrite(_dc, HIGH); + for (uint8_t i = 0; i < datalen - 1; i++) // sub the command + { + _pSPIx->transfer(pgm_read_byte(&*pCommandData++)); + } + if (_cs >= 0) digitalWrite(_cs, HIGH); + _pSPIx->endTransaction(); +} + +void GxEPD2_EPD::_startTransfer() { + _pSPIx->beginTransaction(_spi_settings); + if (_cs >= 0) digitalWrite(_cs, LOW); +} + +void GxEPD2_EPD::_transfer(uint8_t value) { _pSPIx->transfer(value); } + +void GxEPD2_EPD::_endTransfer() { + if (_cs >= 0) digitalWrite(_cs, HIGH); + _pSPIx->endTransaction(); +} \ No newline at end of file diff --git a/Boards/LilygoTdeckPro/Source/hal/GxEPD2_EPD.h b/Boards/LilygoTdeckPro/Source/hal/GxEPD2_EPD.h new file mode 100644 index 00000000..fc759954 --- /dev/null +++ b/Boards/LilygoTdeckPro/Source/hal/GxEPD2_EPD.h @@ -0,0 +1,143 @@ +// Display Library for SPI e-paper panels from Dalian Good Display and boards from Waveshare. +// Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines! +// +// Display Library based on Demo Example from Good Display: https://www.good-display.com/companyfile/32/ +// +// Author: Jean-Marc Zingg +// +// Version: see library.properties +// +// Library: https://github.com/ZinggJM/GxEPD2 + +#pragma once + +#include "GxEPD2.h" +#include + +#pragma GCC diagnostic ignored "-Wunused-parameter" + +class GxEPD2_EPD +{ +public: + // attributes + const uint16_t WIDTH; + const uint16_t HEIGHT; + const GxEPD2::Panel panel; + const bool hasColor; + const bool hasPartialUpdate; + const bool hasFastPartialUpdate; + // constructor + GxEPD2_EPD(int16_t cs, int16_t dc, int16_t rst, int16_t busy, int16_t busy_level, uint32_t busy_timeout, + uint16_t w, uint16_t h, GxEPD2::Panel p, bool c, bool pu, bool fpu); + virtual void init(); + virtual void init(bool initial, uint16_t reset_duration = 10, bool pulldown_rst_mode = false); + // Support for Bitmaps (Sprites) to Controller Buffer and to Screen + virtual void clearScreen(uint8_t value) = 0; // init controller memory and screen (default white) + virtual void writeScreenBuffer(uint8_t value) = 0; // init controller memory (default white) + // write to controller memory, without screen refresh; x and w should be multiple of 8 + virtual void writeImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, + bool mirror_y = false, bool pgm = false) = 0; + + virtual void writeImageForFullRefresh(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, + bool invert = false, bool mirror_y = false, bool pgm = false) + { + // writeImage is independent from refresh mode for most controllers, exception e.g. SSD1681 + writeImage(bitmap, x, y, w, h, invert, mirror_y, pgm); + } + + virtual void writeImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, + int16_t h_bitmap, + int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, + bool pgm = false) = 0; + // virtual void writeImage(const uint8_t* black, const uint8_t* color, int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0; + // virtual void writeImagePart(const uint8_t* black, const uint8_t* color, int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, + // int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0; + // write sprite of native data to controller memory, without screen refresh; x and w should be multiple of 8 + // virtual void writeNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0; + // for differential update: set current and previous buffers equal (for fast partial update to work correctly) + virtual void writeScreenBufferAgain(uint8_t value = 0xFF) // init controller memory (default white) + { + // most controllers with differential update do switch buffers on refresh, can use: + writeScreenBuffer(value); + } + + virtual void writeImageAgain(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, + bool invert = false, bool mirror_y = false, bool pgm = false) + { + // most controllers with differential update do switch buffers on refresh, can use: + writeImage(bitmap, x, y, w, h, invert, mirror_y, pgm); + } + + virtual void writeImagePartAgain(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, + int16_t h_bitmap, + int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, + bool mirror_y = false, bool pgm = false) + { + // most controllers with differential update do switch buffers on refresh, can use: + writeImagePart(bitmap, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); + } + + // write to controller memory, with screen refresh; x and w should be multiple of 8 + // virtual void drawImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0; + // virtual void drawImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, + // int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0; + // virtual void drawImage(const uint8_t* black, const uint8_t* color, int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0; + // virtual void drawImagePart(const uint8_t* black, const uint8_t* color, int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, + // int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0; + // write sprite of native data to controller memory, with screen refresh; x and w should be multiple of 8 + // virtual void drawNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0; + // a demo bitmap can use yet another bitmap format, e.g. 7-color bitmap from Good Display for GDEY073D46 + virtual void writeDemoBitmap(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, + int16_t mode = 0, bool mirror_y = false, bool pgm = false) + { + }; + + virtual void drawDemoBitmap(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, + int16_t mode = 0, bool mirror_y = false, bool pgm = false) + { + }; + virtual void refresh(bool partial_update_mode = false) = 0; // screen refresh from controller memory to full screen + virtual void refresh(int16_t x, int16_t y, int16_t w, int16_t h) = 0; + // screen refresh from controller memory, partial screen + virtual void powerOff() = 0; // turns off generation of panel driving voltages, avoids screen fading over time + virtual void hibernate() = 0; + // turns powerOff() and sets controller to deep sleep for minimum power use, ONLY if wakeable by RST (rst >= 0) + virtual void setPaged() + { + }; // for GxEPD2_154c paged workaround + // register a callback function to be called during _waitWhileBusy continuously. + void setBusyCallback(void (*busyCallback)(const void*), const void* busy_callback_parameter = 0); + + static inline uint16_t gx_uint16_min(uint16_t a, uint16_t b) + { + return (a < b ? a : b); + }; + + static inline uint16_t gx_uint16_max(uint16_t a, uint16_t b) + { + return (a > b ? a : b); + }; + +protected: + void _waitWhileBusy(const char* comment = 0, uint16_t busy_time = 5000); + void _writeCommand(uint8_t c); + void _writeData(uint8_t d); + void _writeData(const uint8_t* data, uint16_t n); + void _writeDataPGM(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes = 0); + void _writeDataPGM_sCS(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes = 0); + void _writeCommandData(const uint8_t* pCommandData, uint8_t datalen); + void _writeCommandDataPGM(const uint8_t* pCommandData, uint8_t datalen); + void _startTransfer(); + void _transfer(uint8_t value); + void _endTransfer(); + +protected: + int16_t _cs, _dc, _rst, _busy, _busy_level; + uint32_t _busy_timeout; + bool _pulldown_rst_mode; + bool _initial_write, _initial_refresh; + bool _power_is_on, _using_partial_mode, _hibernating; + bool _init_display_done; + void (*_busy_callback)(const void*); + const void* _busy_callback_parameter; +}; diff --git a/Boards/LilygoTdeckPro/Source/hal/TdeckDisplay.cpp b/Boards/LilygoTdeckPro/Source/hal/TdeckDisplay.cpp new file mode 100644 index 00000000..01175a21 --- /dev/null +++ b/Boards/LilygoTdeckPro/Source/hal/TdeckDisplay.cpp @@ -0,0 +1,45 @@ +#include "TdeckDisplay.h" +#include "TdeckDisplayConstants.h" + +#include +#include +#include + +#include + +#define TAG "tdeck_display" + +static std::shared_ptr createTouch() { + // Note for future changes: Reset pin is 48 and interrupt pin is 47 + auto configuration = std::make_unique( + I2C_NUM_0, + 240, + 320, + true, + true, + false + ); + + return std::make_shared(std::move(configuration)); +} + +std::shared_ptr createDisplay() { + auto touch = createTouch(); + + auto configuration = std::make_unique( + TDECK_LCD_SPI_HOST, + TDECK_LCD_PIN_CS, + TDECK_LCD_PIN_DC, + 320, + 240, + touch, + true, + true, + false, + true + ); + + configuration->backlightDutyFunction = driver::pwmbacklight::setBacklightDuty; + + return std::make_shared(std::move(configuration)); +} diff --git a/Boards/LilygoTdeckPro/Source/hal/TdeckDisplay.h b/Boards/LilygoTdeckPro/Source/hal/TdeckDisplay.h new file mode 100644 index 00000000..e9a26ec4 --- /dev/null +++ b/Boards/LilygoTdeckPro/Source/hal/TdeckDisplay.h @@ -0,0 +1,40 @@ +#pragma once + +#include "Tactility/hal/display/DisplayDevice.h" +#include +#include + +class TdeckDisplay : public tt::hal::display::DisplayDevice { + +private: + + esp_lcd_panel_io_handle_t ioHandle = nullptr; + esp_lcd_panel_handle_t panelHandle = nullptr; + lv_display_t* displayHandle = nullptr; + bool poweredOn = false; + +public: + + std::string getName() const final { return "ST7789"; } + std::string getDescription() const final { return "SPI display"; } + + bool start() override; + + bool stop() override; + + void setPowerOn(bool turnOn) override; + bool isPoweredOn() const override { return poweredOn; }; + bool supportsPowerControl() const override { return true; } + + std::shared_ptr _Nullable createTouch() override; + + void setBacklightDuty(uint8_t backlightDuty) override; + bool supportsBacklightDuty() const override { return true; } + + void setGammaCurve(uint8_t index) override; + uint8_t getGammaCurveCount() const override { return 4; }; + + lv_display_t* _Nullable getLvglDisplay() const override { return displayHandle; } +}; + +std::shared_ptr createDisplay(); diff --git a/Boards/LilygoTdeckPro/Source/hal/TdeckDisplayConstants.h b/Boards/LilygoTdeckPro/Source/hal/TdeckDisplayConstants.h new file mode 100644 index 00000000..f0dbb645 --- /dev/null +++ b/Boards/LilygoTdeckPro/Source/hal/TdeckDisplayConstants.h @@ -0,0 +1,8 @@ +#pragma once + +#define TDECK_LCD_SPI_HOST SPI2_HOST +#define TDECK_LCD_PIN_CS GPIO_NUM_12 +#define TDECK_LCD_PIN_DC GPIO_NUM_11 // RS +#define TDECK_LCD_HORIZONTAL_RESOLUTION 320 +#define TDECK_LCD_VERTICAL_RESOLUTION 240 +#define TDECK_LCD_SPI_TRANSFER_HEIGHT (TDECK_LCD_VERTICAL_RESOLUTION / 10) diff --git a/Boards/LilygoTdeckPro/Source/hal/TdeckKeyboard.cpp b/Boards/LilygoTdeckPro/Source/hal/TdeckKeyboard.cpp new file mode 100644 index 00000000..47c8590d --- /dev/null +++ b/Boards/LilygoTdeckPro/Source/hal/TdeckKeyboard.cpp @@ -0,0 +1,67 @@ +#include "TdeckKeyboard.h" +#include +#include + +#define TAG "tdeck_keyboard" + +#define TDECK_KEYBOARD_I2C_BUS_HANDLE I2C_NUM_0 +#define TDECK_KEYBOARD_SLAVE_ADDRESS 0x55 + +static inline bool keyboard_i2c_read(uint8_t* output) { + return tt::hal::i2c::masterRead(TDECK_KEYBOARD_I2C_BUS_HANDLE, TDECK_KEYBOARD_SLAVE_ADDRESS, output, 1, 100 / portTICK_PERIOD_MS); +} + +/** + * The callback simulates press and release events, because the T-Deck + * keyboard only publishes press events on I2C. + * LVGL currently works without those extra release events, but they + * are implemented for correctness and future compatibility. + * + * @param indev_drv + * @param data + */ +static void keyboard_read_callback(TT_UNUSED lv_indev_t* indev, lv_indev_data_t* data) { + static uint8_t last_buffer = 0x00; + uint8_t read_buffer = 0x00; + + // Defaults + data->key = 0; + data->state = LV_INDEV_STATE_RELEASED; + + if (keyboard_i2c_read(&read_buffer)) { + if (read_buffer == 0 && read_buffer != last_buffer) { + TT_LOG_D(TAG, "Released %d", last_buffer); + data->key = last_buffer; + data->state = LV_INDEV_STATE_RELEASED; + } else if (read_buffer != 0) { + TT_LOG_D(TAG, "Pressed %d", read_buffer); + data->key = read_buffer; + data->state = LV_INDEV_STATE_PRESSED; + } + } + + last_buffer = read_buffer; +} + +bool TdeckKeyboard::start(lv_display_t* display) { + deviceHandle = lv_indev_create(); + lv_indev_set_type(deviceHandle, LV_INDEV_TYPE_KEYPAD); + lv_indev_set_read_cb(deviceHandle, &keyboard_read_callback); + lv_indev_set_display(deviceHandle, display); + lv_indev_set_user_data(deviceHandle, this); + return true; +} + +bool TdeckKeyboard::stop() { + lv_indev_delete(deviceHandle); + deviceHandle = nullptr; + return true; +} + +bool TdeckKeyboard::isAttached() const { + return tt::hal::i2c::masterHasDeviceAtAddress(TDECK_KEYBOARD_I2C_BUS_HANDLE, TDECK_KEYBOARD_SLAVE_ADDRESS, 100); +} + +std::shared_ptr createKeyboard() { + return std::make_shared(); +} diff --git a/Boards/LilygoTdeckPro/Source/hal/TdeckKeyboard.h b/Boards/LilygoTdeckPro/Source/hal/TdeckKeyboard.h new file mode 100644 index 00000000..26eea592 --- /dev/null +++ b/Boards/LilygoTdeckPro/Source/hal/TdeckKeyboard.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include +#include + +class TdeckKeyboard : public tt::hal::keyboard::KeyboardDevice { + +private: + + lv_indev_t* _Nullable deviceHandle = nullptr; + +public: + + std::string getName() const final { return "T-Deck Keyboard"; } + std::string getDescription() const final { return "I2C keyboard"; } + + bool start(lv_display_t* display) override; + bool stop() override; + bool isAttached() const override; + lv_indev_t* _Nullable getLvglIndev() override { return deviceHandle; } +}; + +std::shared_ptr createKeyboard(); diff --git a/Boards/LilygoTdeckPro/Source/hal/TdeckPower.cpp b/Boards/LilygoTdeckPro/Source/hal/TdeckPower.cpp new file mode 100644 index 00000000..008198db --- /dev/null +++ b/Boards/LilygoTdeckPro/Source/hal/TdeckPower.cpp @@ -0,0 +1,130 @@ +#include "TdeckPower.h" + +#include + +#define TAG "power" + +/** + * 2.0 ratio, but +.11 added as display voltage sag compensation. + */ +#define ADC_MULTIPLIER 2.11 + +#define ADC_REF_VOLTAGE 3.3f +#define BATTERY_VOLTAGE_MIN 3.2f +#define BATTERY_VOLTAGE_MAX 4.2f + +static adc_oneshot_unit_init_cfg_t adcConfig = { + .unit_id = ADC_UNIT_1, + .clk_src = ADC_RTC_CLK_SRC_DEFAULT, + .ulp_mode = ADC_ULP_MODE_DISABLE, +}; + +static adc_oneshot_chan_cfg_t adcChannelConfig = { + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_DEFAULT, +}; + +static uint8_t estimateChargeLevelFromVoltage(uint32_t milliVolt) { + float volts = std::min((float)milliVolt / 1000.f, BATTERY_VOLTAGE_MAX); + float voltage_percentage = (volts - BATTERY_VOLTAGE_MIN) / (BATTERY_VOLTAGE_MAX - BATTERY_VOLTAGE_MIN); + float voltage_factor = std::min(1.0f, voltage_percentage); + auto charge_level = (uint8_t) (voltage_factor * 100.f); + TT_LOG_V(TAG, "mV = %lu, scaled = %.2f, factor = %.2f, result = %d", milliVolt, volts, voltage_factor, charge_level); + return charge_level; +} + +TdeckPower::TdeckPower() { + if (adc_oneshot_new_unit(&adcConfig, &adcHandle) != ESP_OK) { + TT_LOG_E(TAG, "ADC config failed"); + return; + } + + if (adc_oneshot_config_channel(adcHandle, ADC_CHANNEL_3, &adcChannelConfig) != ESP_OK) { + TT_LOG_E(TAG, "ADC channel config failed"); + + adc_oneshot_del_unit(adcHandle); + return; + } +} + +TdeckPower::~TdeckPower() { + if (adcHandle) { + adc_oneshot_del_unit(adcHandle); + } +} + +bool TdeckPower::supportsMetric(MetricType type) const { + switch (type) { + using enum MetricType; + case BatteryVoltage: + case ChargeLevel: + return true; + default: + return false; + } + + return false; // Safety guard for when new enum values are introduced +} + +bool TdeckPower::getMetric(MetricType type, MetricData& data) { + switch (type) { + using enum MetricType; + case BatteryVoltage: + return readBatteryVoltageSampled(data.valueAsUint32); + case ChargeLevel: + if (readBatteryVoltageSampled(data.valueAsUint32)) { + data.valueAsUint32 = estimateChargeLevelFromVoltage(data.valueAsUint32); + return true; + } else { + return false; + } + default: + return false; + } + + return false; // Safety guard for when new enum values are introduced +} + +bool TdeckPower::readBatteryVoltageOnce(uint32_t& output) { + int raw; + if (adc_oneshot_read(adcHandle, ADC_CHANNEL_3, &raw) == ESP_OK) { + output = ADC_MULTIPLIER * ((1000.f * ADC_REF_VOLTAGE) / 4096.f) * (float)raw; + TT_LOG_V(TAG, "Raw = %d, voltage = %lu", raw, output); + return true; + } else { + TT_LOG_E(TAG, "Read failed"); + return false; + } +} + +#define MAX_VOLTAGE_SAMPLES 15 + +bool TdeckPower::readBatteryVoltageSampled(uint32_t& output) { + size_t samples_read = 0; + uint32_t sample_accumulator = 0; + uint32_t sample_read_buffer; + + for (size_t i = 0; i < MAX_VOLTAGE_SAMPLES; ++i) { + if (readBatteryVoltageOnce(sample_read_buffer)) { + sample_accumulator += sample_read_buffer; + samples_read++; + } + } + + if (samples_read > 0) { + output = sample_accumulator / samples_read; + return true; + } else { + return false; + } +} + +static std::shared_ptr power; + +std::shared_ptr tdeck_get_power() { + if (power == nullptr) { + power = std::make_shared(); + } + return power; +} + diff --git a/Boards/LilygoTdeckPro/Source/hal/TdeckPower.h b/Boards/LilygoTdeckPro/Source/hal/TdeckPower.h new file mode 100644 index 00000000..4dc4d75c --- /dev/null +++ b/Boards/LilygoTdeckPro/Source/hal/TdeckPower.h @@ -0,0 +1,30 @@ +#pragma once + +#include "Tactility/hal/power/PowerDevice.h" +#include +#include + +using tt::hal::power::PowerDevice; + +class TdeckPower : public PowerDevice { + + adc_oneshot_unit_handle_t adcHandle = nullptr; + +public: + + TdeckPower(); + ~TdeckPower(); + + std::string getName() const final { return "ADC Power Measurement"; } + std::string getDescription() const final { return "Power measurement interface via ADC pin"; } + + bool supportsMetric(MetricType type) const override; + bool getMetric(MetricType type, MetricData& data) override; + +private: + + bool readBatteryVoltageSampled(uint32_t& output); + bool readBatteryVoltageOnce(uint32_t& output); +}; + +std::shared_ptr tdeck_get_power(); diff --git a/Boards/LilygoTdeckPro/Source/hal/TdeckSdCard.cpp b/Boards/LilygoTdeckPro/Source/hal/TdeckSdCard.cpp new file mode 100644 index 00000000..486d3d13 --- /dev/null +++ b/Boards/LilygoTdeckPro/Source/hal/TdeckSdCard.cpp @@ -0,0 +1,33 @@ +#include "TdeckSdCard.h" + +#include +#include + +#include + +using tt::hal::sdcard::SpiSdCardDevice; + +#define TDECK_SDCARD_PIN_CS GPIO_NUM_39 +#define TDECK_LCD_PIN_CS GPIO_NUM_12 +#define TDECK_RADIO_PIN_CS GPIO_NUM_9 + +std::shared_ptr createTdeckSdCard() { + auto* configuration = new SpiSdCardDevice::Config( + TDECK_SDCARD_PIN_CS, + GPIO_NUM_NC, + GPIO_NUM_NC, + GPIO_NUM_NC, + SdCardDevice::MountBehaviour::AtBoot, + tt::lvgl::getSyncLock(), + { + TDECK_RADIO_PIN_CS, + TDECK_LCD_PIN_CS + } + ); + + auto* sdcard = (SdCardDevice*) new SpiSdCardDevice( + std::unique_ptr(configuration) + ); + + return std::shared_ptr(sdcard); +} diff --git a/Boards/LilygoTdeckPro/Source/hal/TdeckSdCard.h b/Boards/LilygoTdeckPro/Source/hal/TdeckSdCard.h new file mode 100644 index 00000000..e12eb6d0 --- /dev/null +++ b/Boards/LilygoTdeckPro/Source/hal/TdeckSdCard.h @@ -0,0 +1,7 @@ +#pragma once + +#include "Tactility/hal/sdcard/SdCardDevice.h" + +using tt::hal::sdcard::SdCardDevice; + +std::shared_ptr createTdeckSdCard(); diff --git a/Buildscripts/board.cmake b/Buildscripts/board.cmake index 29afb107..ada9a64f 100644 --- a/Buildscripts/board.cmake +++ b/Buildscripts/board.cmake @@ -47,6 +47,10 @@ function(INIT_TACTILITY_GLOBALS SDKCONFIG_FILE) set(TACTILITY_BOARD_PROJECT ElecrowCrowpanelBasic50) elseif (board_id STREQUAL "lilygo-tdeck") set(TACTILITY_BOARD_PROJECT LilygoTdeck) + elseif (board_id STREQUAL "lilygo-tdeck-plus") + set(TACTILITY_BOARD_PROJECT LilygoTdeck) + elseif (board_id STREQUAL "lilygo-tdeck-pro") + set(TACTILITY_BOARD_PROJECT LilygoTdeckPro) elseif (board_id STREQUAL "m5stack-core2") set(TACTILITY_BOARD_PROJECT M5stackCore2) elseif (board_id STREQUAL "m5stack-cores3") diff --git a/ExternalApps/HelloWorld/.gitignore b/ExternalApps/HelloWorld/.gitignore index 89baa26e..31095bb4 100644 --- a/ExternalApps/HelloWorld/.gitignore +++ b/ExternalApps/HelloWorld/.gitignore @@ -1,2 +1,2 @@ build*/ -.tactility/ +.tactility/ \ No newline at end of file diff --git a/sdkconfig.board.lilygo-tdeck-plus b/sdkconfig.board.lilygo-tdeck-plus new file mode 100644 index 00000000..0b3fc464 --- /dev/null +++ b/sdkconfig.board.lilygo-tdeck-plus @@ -0,0 +1,53 @@ +# Software defaults +# Increase stack size for WiFi (fixes crash after scan) +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072 +CONFIG_LV_FONT_MONTSERRAT_14=y +CONFIG_LV_FONT_MONTSERRAT_18=y +CONFIG_LV_USE_USER_DATA=y +CONFIG_LV_USE_FS_STDIO=y +CONFIG_LV_FS_STDIO_LETTER=65 +CONFIG_LV_FS_STDIO_PATH="" +CONFIG_LV_FS_STDIO_CACHE_SIZE=4096 +CONFIG_LV_USE_LODEPNG=y +CONFIG_LV_USE_BUILTIN_MALLOC=n +CONFIG_LV_USE_CLIB_MALLOC=y +CONFIG_LV_USE_MSGBOX=n +CONFIG_LV_USE_SPINNER=n +CONFIG_LV_USE_WIN=n +CONFIG_LV_USE_SNAPSHOT=y +CONFIG_FREERTOS_HZ=1000 +CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2 +CONFIG_FREERTOS_SMP=n +CONFIG_FREERTOS_UNICORE=n +CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096 +CONFIG_FREERTOS_USE_TRACE_FACILITY=y +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +CONFIG_FATFS_LFN_HEAP=y +CONFIG_FATFS_VOLUME_COUNT=3 + +# Hardware: Main +CONFIG_TT_BOARD_LILYGO_TDECK_PLUS=y +CONFIG_TT_BOARD_NAME="LilyGo T-Deck Plus" +CONFIG_TT_BOARD_ID="lilygo-tdeck-plus" +CONFIG_IDF_EXPERIMENTAL_FEATURES=y +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y +CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y +CONFIG_FLASHMODE_QIO=y +# Hardware: SPI RAM +CONFIG_ESP32S3_SPIRAM_SUPPORT=y +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_SPEED_120M=y +CONFIG_SPIRAM_USE_MALLOC=y +CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y +# SPI Flash (can set back to 80MHz after ESP-IDF bug is resolved) +CONFIG_ESPTOOLPY_FLASHFREQ_120M=y +# LVGL +CONFIG_LV_DPI_DEF=139 +CONFIG_LV_DISP_DEF_REFR_PERIOD=10 +# USB +CONFIG_TINYUSB_MSC_ENABLED=y +CONFIG_TINYUSB_MSC_MOUNT_PATH="/sdcard" \ No newline at end of file diff --git a/sdkconfig.board.lilygo-tdeck-pro b/sdkconfig.board.lilygo-tdeck-pro new file mode 100644 index 00000000..ac0b17c7 --- /dev/null +++ b/sdkconfig.board.lilygo-tdeck-pro @@ -0,0 +1,57 @@ +# Software defaults +# Increase stack size for WiFi (fixes crash after scan) +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072 +CONFIG_LV_FONT_MONTSERRAT_14=y +CONFIG_LV_FONT_MONTSERRAT_18=y +CONFIG_LV_USE_USER_DATA=y +CONFIG_LV_USE_FS_STDIO=y +CONFIG_LV_FS_STDIO_LETTER=65 +CONFIG_LV_FS_STDIO_PATH="" +CONFIG_LV_FS_STDIO_CACHE_SIZE=4096 +CONFIG_LV_USE_LODEPNG=y +CONFIG_LV_USE_BUILTIN_MALLOC=n +CONFIG_LV_USE_CLIB_MALLOC=y +CONFIG_LV_USE_MSGBOX=n +CONFIG_LV_USE_SPINNER=n +CONFIG_LV_USE_WIN=n +CONFIG_LV_USE_SNAPSHOT=y +CONFIG_FREERTOS_HZ=1000 +CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2 +CONFIG_FREERTOS_SMP=n +CONFIG_FREERTOS_UNICORE=n +CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096 +CONFIG_FREERTOS_USE_TRACE_FACILITY=y +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +CONFIG_FATFS_LFN_HEAP=y +CONFIG_FATFS_VOLUME_COUNT=3 + +# Hardware: Main +CONFIG_TT_BOARD_LILYGO_TDECK_PRO=y +CONFIG_TT_BOARD_NAME="LilyGo T-Deck Pro" +CONFIG_TT_BOARD_ID="lilygo-tdeck-pro" +CONFIG_IDF_EXPERIMENTAL_FEATURES=y +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y +CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y +CONFIG_FLASHMODE_QIO=y +# Hardware: SPI RAM +CONFIG_ESP32S3_SPIRAM_SUPPORT=y +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_SPEED_120M=y +CONFIG_SPIRAM_USE_MALLOC=y +CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y +# SPI Flash (can set back to 80MHz after ESP-IDF bug is resolved) +CONFIG_ESPTOOLPY_FLASHFREQ_120M=y +# LVGL +CONFIG_LV_DPI_DEF=139 +CONFIG_LV_DISP_DEF_REFR_PERIOD=10 +CONFIG_LV_USE_THEME_DEFAULT=n +CONFIG_LV_USE_THEME_SIMPLE=n +CONFIG_LV_USE_THEME_MONO=y +CONFIG_LV_COLOR_DEPTH=1 +# USB +CONFIG_TINYUSB_MSC_ENABLED=y +CONFIG_TINYUSB_MSC_MOUNT_PATH="/sdcard" From efd3dc43edd9c4e1af93b57f5762b8871569e3b1 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sun, 15 Jun 2025 13:50:29 +0200 Subject: [PATCH 12/16] Revert "Updated screenshots and .gitignore of HelloWorld (#291)" (#292) This reverts commit 6de0f442fbe44ed26ace8aadbe4e242178e22186. --- .github/workflows/build-firmware.yml | 18 -- App/Kconfig | 4 - App/Source/Boards.h | 14 +- Boards/LilygoTdeck/Source/hal/TdeckDisplay.h | 35 +++ Boards/LilygoTdeckPro/CMakeLists.txt | 7 - Boards/LilygoTdeckPro/Source/Init.cpp | 63 ---- Boards/LilygoTdeckPro/Source/LilygoTdeck.cpp | 108 ------- Boards/LilygoTdeckPro/Source/LilygoTdeck.h | 5 - Boards/LilygoTdeckPro/Source/hal/GxEPD2.h | 114 ------- .../Source/hal/GxEPD2_310_GDEQ031T10.cpp | 297 ------------------ .../Source/hal/GxEPD2_310_GDEQ031T10.h | 100 ------ .../LilygoTdeckPro/Source/hal/GxEPD2_EPD.cpp | 161 ---------- Boards/LilygoTdeckPro/Source/hal/GxEPD2_EPD.h | 143 --------- .../Source/hal/TdeckDisplay.cpp | 45 --- .../LilygoTdeckPro/Source/hal/TdeckDisplay.h | 40 --- .../Source/hal/TdeckDisplayConstants.h | 8 - .../Source/hal/TdeckKeyboard.cpp | 67 ---- .../LilygoTdeckPro/Source/hal/TdeckKeyboard.h | 25 -- .../LilygoTdeckPro/Source/hal/TdeckPower.cpp | 130 -------- Boards/LilygoTdeckPro/Source/hal/TdeckPower.h | 30 -- .../LilygoTdeckPro/Source/hal/TdeckSdCard.cpp | 33 -- .../LilygoTdeckPro/Source/hal/TdeckSdCard.h | 7 - Buildscripts/board.cmake | 4 - ExternalApps/HelloWorld/.gitignore | 2 +- sdkconfig.board.lilygo-tdeck-plus | 53 ---- sdkconfig.board.lilygo-tdeck-pro | 57 ---- 26 files changed, 43 insertions(+), 1527 deletions(-) delete mode 100644 Boards/LilygoTdeckPro/CMakeLists.txt delete mode 100644 Boards/LilygoTdeckPro/Source/Init.cpp delete mode 100644 Boards/LilygoTdeckPro/Source/LilygoTdeck.cpp delete mode 100644 Boards/LilygoTdeckPro/Source/LilygoTdeck.h delete mode 100644 Boards/LilygoTdeckPro/Source/hal/GxEPD2.h delete mode 100644 Boards/LilygoTdeckPro/Source/hal/GxEPD2_310_GDEQ031T10.cpp delete mode 100644 Boards/LilygoTdeckPro/Source/hal/GxEPD2_310_GDEQ031T10.h delete mode 100644 Boards/LilygoTdeckPro/Source/hal/GxEPD2_EPD.cpp delete mode 100644 Boards/LilygoTdeckPro/Source/hal/GxEPD2_EPD.h delete mode 100644 Boards/LilygoTdeckPro/Source/hal/TdeckDisplay.cpp delete mode 100644 Boards/LilygoTdeckPro/Source/hal/TdeckDisplay.h delete mode 100644 Boards/LilygoTdeckPro/Source/hal/TdeckDisplayConstants.h delete mode 100644 Boards/LilygoTdeckPro/Source/hal/TdeckKeyboard.cpp delete mode 100644 Boards/LilygoTdeckPro/Source/hal/TdeckKeyboard.h delete mode 100644 Boards/LilygoTdeckPro/Source/hal/TdeckPower.cpp delete mode 100644 Boards/LilygoTdeckPro/Source/hal/TdeckPower.h delete mode 100644 Boards/LilygoTdeckPro/Source/hal/TdeckSdCard.cpp delete mode 100644 Boards/LilygoTdeckPro/Source/hal/TdeckSdCard.h delete mode 100644 sdkconfig.board.lilygo-tdeck-plus delete mode 100644 sdkconfig.board.lilygo-tdeck-pro diff --git a/.github/workflows/build-firmware.yml b/.github/workflows/build-firmware.yml index 47d58ab5..2e8f4434 100644 --- a/.github/workflows/build-firmware.yml +++ b/.github/workflows/build-firmware.yml @@ -126,24 +126,6 @@ jobs: with: board_id: lilygo-tdeck arch: esp32s3 - lilygo-tdeck-plus: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: "Build" - uses: ./.github/actions/build-firmware - with: - board_id: lilygo-tdeck-plus - arch: esp32s3 - lilygo-tdeck-pro: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: "Build" - uses: ./.github/actions/build-firmware - with: - board_id: lilygo-tdeck-pro - arch: esp32s3 m5stack-core2: runs-on: ubuntu-latest steps: diff --git a/App/Kconfig b/App/Kconfig index 73f961c2..302912a1 100644 --- a/App/Kconfig +++ b/App/Kconfig @@ -37,10 +37,6 @@ menu "Tactility App" bool "Elecrow CrowPanel Basic 5.0" config TT_BOARD_LILYGO_TDECK bool "LilyGo T-Deck" - config TT_BOARD_LILYGO_TDECK_PLUS - bool "LilyGo T-Deck Plus" - config TT_BOARD_LILYGO_TDECK_PRO - bool "LilyGo T-Deck Pro" config TT_BOARD_M5STACK_CORE2 bool "M5Stack Core2" config TT_BOARD_M5STACK_CORES3 diff --git a/App/Source/Boards.h b/App/Source/Boards.h index f42203e1..982b0cee 100644 --- a/App/Source/Boards.h +++ b/App/Source/Boards.h @@ -5,7 +5,7 @@ #include // Supported hardware: -#if defined(CONFIG_TT_BOARD_LILYGO_TDECK) || defined(CONFIG_TT_BOARD_LILYGO_TDECK_PLUS) || defined(CONFIG_TT_BOARD_LILYGO_TDECK_PRO) +#if defined(CONFIG_TT_BOARD_LILYGO_TDECK) #include "LilygoTdeck.h" #define TT_BOARD_HARDWARE &lilygo_tdeck #elif defined(CONFIG_TT_BOARD_CYD_2432S024C) @@ -14,22 +14,22 @@ #elif defined(CONFIG_TT_BOARD_CYD_2432S032C) #include "CYD2432S032C.h" #define TT_BOARD_HARDWARE &cyd_2432S032c_config -#elif defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_ADVANCE_28) +#elif (defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_ADVANCE_28)) #define TT_BOARD_HARDWARE &crowpanel_advance_28 #include "CrowPanelAdvance28.h" -#elif defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_ADVANCE_35) +#elif (defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_ADVANCE_35)) #define TT_BOARD_HARDWARE &crowpanel_advance_35 #include "CrowPanelAdvance35.h" -#elif defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_ADVANCE_50) +#elif (defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_ADVANCE_50)) #define TT_BOARD_HARDWARE &crowpanel_advance_50 #include "CrowPanelAdvance50.h" -#elif defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_BASIC_28) +#elif (defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_BASIC_28)) #define TT_BOARD_HARDWARE &crowpanel_basic_28 #include "CrowPanelBasic28.h" -#elif defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_BASIC_35) +#elif (defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_BASIC_35)) #define TT_BOARD_HARDWARE &crowpanel_basic_35 #include "CrowPanelBasic35.h" -#elif defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_BASIC_50) +#elif (defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_BASIC_50)) #define TT_BOARD_HARDWARE &crowpanel_basic_50 #include "CrowPanelBasic50.h" #elif defined(CONFIG_TT_BOARD_M5STACK_CORE2) diff --git a/Boards/LilygoTdeck/Source/hal/TdeckDisplay.h b/Boards/LilygoTdeck/Source/hal/TdeckDisplay.h index 5a0d81b3..e9a26ec4 100644 --- a/Boards/LilygoTdeck/Source/hal/TdeckDisplay.h +++ b/Boards/LilygoTdeck/Source/hal/TdeckDisplay.h @@ -1,5 +1,40 @@ #pragma once #include "Tactility/hal/display/DisplayDevice.h" +#include +#include + +class TdeckDisplay : public tt::hal::display::DisplayDevice { + +private: + + esp_lcd_panel_io_handle_t ioHandle = nullptr; + esp_lcd_panel_handle_t panelHandle = nullptr; + lv_display_t* displayHandle = nullptr; + bool poweredOn = false; + +public: + + std::string getName() const final { return "ST7789"; } + std::string getDescription() const final { return "SPI display"; } + + bool start() override; + + bool stop() override; + + void setPowerOn(bool turnOn) override; + bool isPoweredOn() const override { return poweredOn; }; + bool supportsPowerControl() const override { return true; } + + std::shared_ptr _Nullable createTouch() override; + + void setBacklightDuty(uint8_t backlightDuty) override; + bool supportsBacklightDuty() const override { return true; } + + void setGammaCurve(uint8_t index) override; + uint8_t getGammaCurveCount() const override { return 4; }; + + lv_display_t* _Nullable getLvglDisplay() const override { return displayHandle; } +}; std::shared_ptr createDisplay(); diff --git a/Boards/LilygoTdeckPro/CMakeLists.txt b/Boards/LilygoTdeckPro/CMakeLists.txt deleted file mode 100644 index d1096554..00000000 --- a/Boards/LilygoTdeckPro/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -file(GLOB_RECURSE SOURCE_FILES Source/*.c*) - -idf_component_register( - SRCS ${SOURCE_FILES} - INCLUDE_DIRS "Source" - REQUIRES Tactility esp_lvgl_port esp_lcd ST7789 GT911 PwmBacklight driver esp_adc -) diff --git a/Boards/LilygoTdeckPro/Source/Init.cpp b/Boards/LilygoTdeckPro/Source/Init.cpp deleted file mode 100644 index c8c4b6df..00000000 --- a/Boards/LilygoTdeckPro/Source/Init.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#include "PwmBacklight.h" -#include "Tactility/kernel/SystemEvents.h" -#include "Tactility/service/gps/GpsService.h" - -#include -#include - -#define TAG "tdeck" - -// Power on -#define TDECK_POWERON_GPIO GPIO_NUM_10 - -static bool powerOn() { - gpio_config_t device_power_signal_config = { - .pin_bit_mask = BIT64(TDECK_POWERON_GPIO), - .mode = GPIO_MODE_OUTPUT, - .pull_up_en = GPIO_PULLUP_DISABLE, - .pull_down_en = GPIO_PULLDOWN_DISABLE, - .intr_type = GPIO_INTR_DISABLE, - }; - - if (gpio_config(&device_power_signal_config) != ESP_OK) { - return false; - } - - if (gpio_set_level(TDECK_POWERON_GPIO, 1) != ESP_OK) { - return false; - } - - return true; -} - -bool tdeckInit() { - ESP_LOGI(TAG, LOG_MESSAGE_POWER_ON_START); - if (!powerOn()) { - TT_LOG_E(TAG, LOG_MESSAGE_POWER_ON_FAILED); - return false; - } - - /* 32 Khz and higher gives an issue where the screen starts dimming again above 80% brightness - * when moving the brightness slider rapidly from a lower setting to 100%. - * This is not a slider bug (data was debug-traced) */ - if (!driver::pwmbacklight::init(GPIO_NUM_42, 30000)) { - TT_LOG_E(TAG, "Backlight init failed"); - return false; - } - - tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) { - auto gps_service = tt::service::gps::findGpsService(); - if (gps_service != nullptr) { - std::vector gps_configurations; - gps_service->getGpsConfigurations(gps_configurations); - if (gps_configurations.empty()) { - if (gps_service->addGpsConfiguration(tt::hal::gps::GpsConfiguration {.uartName = "Grove", .baudRate = 38400, .model = tt::hal::gps::GpsModel::UBLOX10})) { - TT_LOG_I(TAG, "Configured internal GPS"); - } else { - TT_LOG_E(TAG, "Failed to configure internal GPS"); - } - } - } - }); - return true; -} diff --git a/Boards/LilygoTdeckPro/Source/LilygoTdeck.cpp b/Boards/LilygoTdeckPro/Source/LilygoTdeck.cpp deleted file mode 100644 index d88e4054..00000000 --- a/Boards/LilygoTdeckPro/Source/LilygoTdeck.cpp +++ /dev/null @@ -1,108 +0,0 @@ -#include "Tactility/lvgl/LvglSync.h" -#include "hal/TdeckDisplay.h" -#include "hal/TdeckDisplayConstants.h" -#include "hal/TdeckKeyboard.h" -#include "hal/TdeckPower.h" -#include "hal/TdeckSdCard.h" - -#include - -#define TDECK_SPI_TRANSFER_SIZE_LIMIT (TDECK_LCD_HORIZONTAL_RESOLUTION * TDECK_LCD_SPI_TRANSFER_HEIGHT * (LV_COLOR_DEPTH / 8)) - -bool tdeckInit(); - -using namespace tt::hal; - -extern const Configuration lilygo_tdeck = { - .initBoot = tdeckInit, - .createDisplay = createDisplay, - .createKeyboard = createKeyboard, - .sdcard = createTdeckSdCard(), - .power = tdeck_get_power, - .i2c = { - i2c::Configuration { - .name = "Internal", - .port = I2C_NUM_0, - .initMode = i2c::InitMode::ByTactility, - .isMutable = false, - .config = (i2c_config_t) { - .mode = I2C_MODE_MASTER, - .sda_io_num = GPIO_NUM_18, - .scl_io_num = GPIO_NUM_8, - .sda_pullup_en = true, - .scl_pullup_en = true, - .master = { - .clk_speed = 400000 - }, - .clk_flags = 0 - } - }, - i2c::Configuration { - .name = "External", - .port = I2C_NUM_1, - .initMode = i2c::InitMode::Disabled, - .isMutable = true, - .config = (i2c_config_t) { - .mode = I2C_MODE_MASTER, - .sda_io_num = GPIO_NUM_43, - .scl_io_num = GPIO_NUM_44, - .sda_pullup_en = false, - .scl_pullup_en = false, - .master = { - .clk_speed = 400000 - }, - .clk_flags = 0 - } - } - }, - .spi { - spi::Configuration { - .device = SPI2_HOST, - .dma = SPI_DMA_CH_AUTO, - .config = { - .mosi_io_num = GPIO_NUM_41, - .miso_io_num = GPIO_NUM_38, - .sclk_io_num = GPIO_NUM_40, - .quadwp_io_num = GPIO_NUM_NC, // Quad SPI LCD driver is not yet supported - .quadhd_io_num = GPIO_NUM_NC, // Quad SPI LCD driver is not yet supported - .data4_io_num = GPIO_NUM_NC, - .data5_io_num = GPIO_NUM_NC, - .data6_io_num = GPIO_NUM_NC, - .data7_io_num = GPIO_NUM_NC, - .data_io_default_level = false, - .max_transfer_sz = TDECK_SPI_TRANSFER_SIZE_LIMIT, - .flags = 0, - .isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO, - .intr_flags = 0 - }, - .initMode = spi::InitMode::ByTactility, - .isMutable = false, - .lock = tt::lvgl::getSyncLock() // esp_lvgl_port owns the lock for the display - } - }, - .uart { - uart::Configuration { - .name = "Grove", - .port = UART_NUM_1, - .rxPin = GPIO_NUM_44, - .txPin = GPIO_NUM_43, - .rtsPin = GPIO_NUM_NC, - .ctsPin = GPIO_NUM_NC, - .rxBufferSize = 1024, - .txBufferSize = 1024, - .config = { - .baud_rate = 38400, - .data_bits = UART_DATA_8_BITS, - .parity = UART_PARITY_DISABLE, - .stop_bits = UART_STOP_BITS_1, - .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, - .rx_flow_ctrl_thresh = 0, - .source_clk = UART_SCLK_DEFAULT, - .flags = { - .allow_pd = 0, - .backup_before_sleep = 0, - } - } - } - } -}; diff --git a/Boards/LilygoTdeckPro/Source/LilygoTdeck.h b/Boards/LilygoTdeckPro/Source/LilygoTdeck.h deleted file mode 100644 index 5edc402d..00000000 --- a/Boards/LilygoTdeckPro/Source/LilygoTdeck.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include - -extern const tt::hal::Configuration lilygo_tdeck; diff --git a/Boards/LilygoTdeckPro/Source/hal/GxEPD2.h b/Boards/LilygoTdeckPro/Source/hal/GxEPD2.h deleted file mode 100644 index 3c18dec0..00000000 --- a/Boards/LilygoTdeckPro/Source/hal/GxEPD2.h +++ /dev/null @@ -1,114 +0,0 @@ -// Display Library for SPI e-paper panels from Dalian Good Display and boards from Waveshare. -// Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines! -// -// Display Library based on Demo Example from Good Display: https://www.good-display.com/companyfile/32/ -// -// Author: Jean-Marc Zingg -// -// Version: see library.properties -// -// Library: https://github.com/ZinggJM/GxEPD2 - -#pragma once - -// color definitions for GxEPD, GxEPD2 and GxEPD_HD, values correspond to RGB565 values for TFTs -#define GxEPD_BLACK 0x0000 -#define GxEPD_WHITE 0xFFFF -// some controllers for b/w EPDs support grey levels -#define GxEPD_DARKGREY 0x7BEF // 128, 128, 128 -#define GxEPD_LIGHTGREY 0xC618 // 192, 192, 192 -// values for 3-color or 7-color EPDs -#define GxEPD_RED 0xF800 // 255, 0, 0 -#define GxEPD_YELLOW 0xFFE0 // 255, 255, 0 !!no longer same as GxEPD_RED!! -#define GxEPD_COLORED GxEPD_RED -// values for 7-color EPDs only -#define GxEPD_BLUE 0x001F // 0, 0, 255 -#define GxEPD_GREEN 0x07E0 // 0, 255, 0 -#define GxEPD_ORANGE 0xFC00 // 255, 128, 0 - -class GxEPD2 -{ - public: - enum Panel - { - GDEW0102T4, Waveshare_1_02_bw = GDEW0102T4, - GDEP015OC1, Waveshare_1_54_bw = GDEP015OC1, - DEPG0150BN, - GDEH0154D67, Waveshare_1_54_bw_D67 = GDEH0154D67, - GDEW0154T8, - GDEW0154M09, - GDEW0154M10, - GDEY0154D67, - GDE0213B1, Waveshare_2_13_bw = GDE0213B1, - GDEH0213B72, Waveshare_2_13_bw_B72 = GDEH0213B72, - GDEH0213B73, Waveshare_2_13_bw_B73 = GDEH0213B73, - GDEM0213B74, - GDEW0213I5F, Waveshare_2_13_flex = GDEW0213I5F, - GDEW0213M21, - GDEW0213T5D, - DEPG0213BN, - GDEY0213B74, - GDEW026T0, Waveshare_2_6_bw = GDEW026T0, - GDEW026M01, - DEPG0266BN, - GDEY0266T90, - GDEH029A1, Waveshare_2_9_bw = GDEH029A1, - GDEW029T5, Waveshare_2_9_bw_T5 = GDEW029T5, - GDEW029T5D, - GDEW029I6FD, - GDEW029M06, - GDEM029T94, - GDEY029T94, - DEPG0290BS, - GDEW027W3, Waveshare_2_7_bw = GDEW027W3, - GDEY027T91, - GDEQ031T10, - ED037TC1, - GDEW0371W7, Waveshare_3_7_bw = GDEW0371W7, - GDEW042T2, Waveshare_4_2_bw = GDEW042T2, - GDEW042M01, - GDEY042T81, - GDEQ0426T82, - GDEW0583T7, Waveshare_5_83_bw = GDEW0583T7, - GDEW0583T8, - GDEQ0583T31, - GDEW075T8, Waveshare_7_5_bw = GDEW075T8, - GDEW075T7, Waveshare_7_5_bw_T7 = GDEW075T7, - GDEY075T7, - GDEH116T91, - GDEW1248T3, Waveshare_12_24_bw = GDEW1248T3, - ED060SCT, // on Waveshare IT8951 Driver HAT - ED060KC1, // on Waveshare IT8951 Driver HAT 1448x1072 - ED078KC2, // on Waveshare IT8951 Driver HAT 1872x1404 - ES103TC1, // on Waveshare IT8951 Driver HAT 1872x1404 - // 3-color - GDEW0154Z04, Waveshare_1_54_bwr = GDEW0154Z04, - GDEH0154Z90, Waveshare_1_54_bwr_Z90 = GDEH0154Z90, - GDEW0213Z16, Waveshare_2_13_bwr = GDEW0213Z16, - GDEW0213Z19, - GDEY0213Z98, - GDEW029Z10, Waveshare_2_9_bwr = GDEW029Z10, - GDEH029Z13, - GDEM029C90, - GDEY0266Z90, Waveshare_2_66_bwr = GDEY0266Z90, - GDEW027C44, Waveshare_2_7_bwr = GDEW027C44, - GDEW042Z15, Waveshare_4_2_bwr = GDEW042Z15, - GDEQ042Z21, Waveshare_4_2_V2_bwr = GDEQ042Z21, - GDEW0583Z21, Waveshare_5_83_bwr = GDEW0583Z21, - GDEW0583Z83, - GDEW075Z09, Waveshare_7_5_bwr = GDEW075Z09, - GDEW075Z08, Waveshare_7_5_bwr_Z08 = GDEW075Z08, - GDEH075Z90, Waveshare_7_5_bwr_Z90 = GDEH075Z90, - GDEY1248Z51, - // 4-color - GDEY0266F51H, - GDEY029F51H, - Waveshare3inch4color, - GDEY0420F51, - Waveshare437inch4color, - // 7-color - ACeP565, Waveshare_5_65_7c = ACeP565, - GDEY073D46, - ACeP730, Waveshare_7_30_7c = ACeP730 - }; -}; diff --git a/Boards/LilygoTdeckPro/Source/hal/GxEPD2_310_GDEQ031T10.cpp b/Boards/LilygoTdeckPro/Source/hal/GxEPD2_310_GDEQ031T10.cpp deleted file mode 100644 index 84d6f60c..00000000 --- a/Boards/LilygoTdeckPro/Source/hal/GxEPD2_310_GDEQ031T10.cpp +++ /dev/null @@ -1,297 +0,0 @@ -// Display Library for SPI e-paper panels from Dalian Good Display and boards from Waveshare. -// Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines! -// -// based on Demo Example from Good Display: https://www.good-display.com/product/426.html -// Panel: GDEQ031T10 : https://www.good-display.com/product/426.html -// Controller: UC8253 : https://v4.cecdn.yun300.cn/100001_1909185148/UC8253.pdf -// -// Author: Jean-Marc Zingg -// -// Version: see library.properties -// -// Library: https://github.com/ZinggJM/GxEPD2 - -#include "GxEPD2_310_GDEQ031T10.h" -#include - -constexpr uint32_t LOW = 0; -constexpr uint32_t HIGH = 1; - -GxEPD2_310_GDEQ031T10::GxEPD2_310_GDEQ031T10(int16_t cs, int16_t dc, int16_t rst, int16_t busy) : - GxEPD2_EPD(cs, dc, rst, busy, LOW, 10000000, WIDTH, HEIGHT, panel, hasColor, hasPartialUpdate, hasFastPartialUpdate) {} - -void GxEPD2_310_GDEQ031T10::clearScreen(uint8_t value) { - // full refresh needed for all cases (previous != screen) - _writeScreenBuffer(0x10, value); // set previous - _writeScreenBuffer(0x13, value); // set current - refresh(false); // full refresh - _initial_write = false; -} - -void GxEPD2_310_GDEQ031T10::writeScreenBuffer(uint8_t value) { - if (_initial_write) return clearScreen(value); - _writeScreenBuffer(0x13, value); // set current -} - -void GxEPD2_310_GDEQ031T10::writeScreenBufferAgain(uint8_t value) { - _writeScreenBuffer(0x10, value); // set previous - //_writeScreenBuffer(0x13, value); // set current, not needed -} - -void GxEPD2_310_GDEQ031T10::_writeScreenBuffer(uint8_t command, uint8_t value) { - if (!_init_display_done) _InitDisplay(); - _writeCommand(command); - _startTransfer(); - for (uint32_t i = 0; i < uint32_t(WIDTH) * uint32_t(HEIGHT) / 8; i++) { _transfer(value); } - _endTransfer(); -} - -void GxEPD2_310_GDEQ031T10::writeImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { _writeImage(0x13, bitmap, x, y, w, h, invert, mirror_y, pgm); } - -void GxEPD2_310_GDEQ031T10::writeImageForFullRefresh(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { - _writeImage(0x10, bitmap, x, y, w, h, invert, mirror_y, pgm); // set previous - _writeImage(0x13, bitmap, x, y, w, h, invert, mirror_y, pgm); // set current -} - -void GxEPD2_310_GDEQ031T10::writeImageAgain(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { - _writeImage(0x10, bitmap, x, y, w, h, invert, mirror_y, pgm); // set previous - //_writeImage(0x13, bitmap, x, y, w, h, invert, mirror_y, pgm); // set current, not needed -} - -void GxEPD2_310_GDEQ031T10::_writeImage(uint8_t command, const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { - tt::kernel::delayMillis(1); // WDT hack - uint16_t wb = (w + 7) / 8; // width bytes, bitmaps are padded - x -= x % 8; // byte boundary - w = wb * 8; // byte boundary - int16_t x1 = x < 0 ? 0 : x; // limit - int16_t y1 = y < 0 ? 0 : y; // limit - int16_t w1 = x + w < int16_t(WIDTH) ? w : int16_t(WIDTH) - x; // limit - int16_t h1 = y + h < int16_t(HEIGHT) ? h : int16_t(HEIGHT) - y; // limit - int16_t dx = x1 - x; - int16_t dy = y1 - y; - w1 -= dx; - h1 -= dy; - if ((w1 <= 0) || (h1 <= 0)) return; - if (!_init_display_done) _InitDisplay(); - if (_initial_write) writeScreenBuffer(); // initial full screen buffer clean - _writeCommand(0x91); // partial in - _setPartialRamArea(x1, y1, w1, h1); - _writeCommand(command); - _startTransfer(); - for (int16_t i = 0; i < h1; i++) { - for (int16_t j = 0; j < w1 / 8; j++) { - uint8_t data; - // use wb, h of bitmap for index! - uint16_t idx = mirror_y ? j + dx / 8 + uint16_t((h - 1 - (i + dy))) * wb : j + dx / 8 + uint16_t(i + dy) * wb; - if (pgm) { -#if defined(__AVR) || defined(ESP8266) || defined(ESP32) - data = pgm_read_byte(&bitmap[idx]); -#else - data = bitmap[idx]; -#endif - } else { data = bitmap[idx]; } - if (invert) data = ~data; - _transfer(data); - } - } - _endTransfer(); - _writeCommand(0x92); // partial out - tt::kernel::delayMillis(1); // WDT hack -} - -void GxEPD2_310_GDEQ031T10::writeImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, - int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { _writeImagePart(0x13, bitmap, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); } - -void GxEPD2_310_GDEQ031T10::writeImagePartAgain(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, - int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { - _writeImagePart(0x10, bitmap, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); // set previous - //_writeImagePart(0x13, bitmap, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); // set current, not needed -} - -void GxEPD2_310_GDEQ031T10::_writeImagePart(uint8_t command, const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, - int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { - tt::kernel::delayMillis(1); // WDT hack - if ((w_bitmap < 0) || (h_bitmap < 0) || (w < 0) || (h < 0)) return; - if ((x_part < 0) || (x_part >= w_bitmap)) return; - if ((y_part < 0) || (y_part >= h_bitmap)) return; - uint16_t wb_bitmap = (w_bitmap + 7) / 8; // width bytes, bitmaps are padded - x_part -= x_part % 8; // byte boundary - w = w_bitmap - x_part < w ? w_bitmap - x_part : w; // limit - h = h_bitmap - y_part < h ? h_bitmap - y_part : h; // limit - x -= x % 8; // byte boundary - w = 8 * ((w + 7) / 8); // byte boundary, bitmaps are padded - int16_t x1 = x < 0 ? 0 : x; // limit - int16_t y1 = y < 0 ? 0 : y; // limit - int16_t w1 = x + w < int16_t(WIDTH) ? w : int16_t(WIDTH) - x; // limit - int16_t h1 = y + h < int16_t(HEIGHT) ? h : int16_t(HEIGHT) - y; // limit - int16_t dx = x1 - x; - int16_t dy = y1 - y; - w1 -= dx; - h1 -= dy; - if ((w1 <= 0) || (h1 <= 0)) return; - if (!_init_display_done) _InitDisplay(); - if (_initial_write) writeScreenBuffer(); // initial full screen buffer clean - _writeCommand(0x91); // partial in - _setPartialRamArea(x1, y1, w1, h1); - _writeCommand(command); - _startTransfer(); - for (int16_t i = 0; i < h1; i++) { - for (int16_t j = 0; j < w1 / 8; j++) { - uint8_t data; - // use wb_bitmap, h_bitmap of bitmap for index! - uint16_t idx = mirror_y ? x_part / 8 + j + dx / 8 + uint16_t((h_bitmap - 1 - (y_part + i + dy))) * wb_bitmap : x_part / 8 + j + dx / 8 + uint16_t(y_part + i + dy) * wb_bitmap; - if (pgm) { -#if defined(__AVR) || defined(ESP8266) || defined(ESP32) - data = pgm_read_byte(&bitmap[idx]); -#else - data = bitmap[idx]; -#endif - } else { data = bitmap[idx]; } - if (invert) data = ~data; - _transfer(data); - } - } - _endTransfer(); - _writeCommand(0x92); // partial out - tt::kernel::delayMillis(1); // WDT hack -} - -void GxEPD2_310_GDEQ031T10::writeImage(const uint8_t* black, const uint8_t* color, int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { if (black) { writeImage(black, x, y, w, h, invert, mirror_y, pgm); } } - -void GxEPD2_310_GDEQ031T10::writeImagePart(const uint8_t* black, const uint8_t* color, int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, - int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { if (black) { writeImagePart(black, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); } } - -void GxEPD2_310_GDEQ031T10::writeNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { if (data1) { writeImage(data1, x, y, w, h, invert, mirror_y, pgm); } } - -void GxEPD2_310_GDEQ031T10::drawImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { - writeImage(bitmap, x, y, w, h, invert, mirror_y, pgm); - refresh(x, y, w, h); - writeImageAgain(bitmap, x, y, w, h, invert, mirror_y, pgm); -} - -void GxEPD2_310_GDEQ031T10::drawImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, - int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { - writeImagePart(bitmap, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); - refresh(x, y, w, h); - writeImagePartAgain(bitmap, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); -} - -void GxEPD2_310_GDEQ031T10::drawImage(const uint8_t* black, const uint8_t* color, int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { if (black) { drawImage(black, x, y, w, h, invert, mirror_y, pgm); } } - -void GxEPD2_310_GDEQ031T10::drawImagePart(const uint8_t* black, const uint8_t* color, int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, - int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { if (black) { drawImagePart(black, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); } } - -void GxEPD2_310_GDEQ031T10::drawNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { if (data1) { drawImage(data1, x, y, w, h, invert, mirror_y, pgm); } } - -void GxEPD2_310_GDEQ031T10::refresh(bool partial_update_mode) { - if (partial_update_mode) refresh(0, 0, WIDTH, HEIGHT); - else { - _Update_Full(); - _initial_refresh = false; // initial full update done - } -} - -void GxEPD2_310_GDEQ031T10::refresh(int16_t x, int16_t y, int16_t w, int16_t h) { - if (_initial_refresh) return refresh(false); // initial update needs be full update - // intersection with screen - int16_t w1 = x < 0 ? w + x : w; // reduce - int16_t h1 = y < 0 ? h + y : h; // reduce - int16_t x1 = x < 0 ? 0 : x; // limit - int16_t y1 = y < 0 ? 0 : y; // limit - w1 = x1 + w1 < int16_t(WIDTH) ? w1 : int16_t(WIDTH) - x1; // limit - h1 = y1 + h1 < int16_t(HEIGHT) ? h1 : int16_t(HEIGHT) - y1; // limit - if ((w1 <= 0) || (h1 <= 0)) return; - // make x1, w1 multiple of 8 - w1 += x1 % 8; - if (w1 % 8 > 0) w1 += 8 - w1 % 8; - x1 -= x1 % 8; - if (usePartialUpdateWindow) _writeCommand(0x91); // partial in - _setPartialRamArea(x1, y1, w1, h1); - _Update_Part(); - if (usePartialUpdateWindow) _writeCommand(0x92); // partial out -} - -void GxEPD2_310_GDEQ031T10::powerOff(void) { _PowerOff(); } - -void GxEPD2_310_GDEQ031T10::hibernate() { - _PowerOff(); - if (_rst >= 0) { - _writeCommand(0x07); // deep sleep - _writeData(0xA5); // check code - _hibernating = true; - _init_display_done = false; - } -} - -void GxEPD2_310_GDEQ031T10::_setPartialRamArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { - uint16_t xe = (x + w - 1) | 0x0007; // byte boundary inclusive (last byte) - uint16_t ye = y + h - 1; - x &= 0xFFF8; // byte boundary - _writeCommand(0x90); // partial window - _writeData(x); - _writeData(xe); - _writeData(y / 256); - _writeData(y % 256); - _writeData(ye / 256); - _writeData(ye % 256); - _writeData(0x01); -} - -void GxEPD2_310_GDEQ031T10::_PowerOn() { - if (!_power_is_on) { - _writeCommand(0x04); - _waitWhileBusy("_PowerOn", power_on_time); - } - _power_is_on = true; -} - -void GxEPD2_310_GDEQ031T10::_PowerOff() { - if (_power_is_on) { - _writeCommand(0x02); // power off - _waitWhileBusy("_PowerOff", power_off_time); - } - _power_is_on = false; -} - -void GxEPD2_310_GDEQ031T10::_InitDisplay() { - _writeCommand(0x00); // PANEL SETTING - _writeData(0x1e); // soft reset - _writeData(0x0d); - tt::kernel::delayMillis(1); - _power_is_on = false; - _writeCommand(0x00); // PANEL SETTING - _writeData(0x1f); // KW: 3f, KWR: 2F, BWROTP: 0f, BWOTP: 1f - _writeData(0x0d); - _init_display_done = true; -} - -void GxEPD2_310_GDEQ031T10::_Update_Full() { - if (useFastFullUpdate) { - _writeCommand(0xE0); // Cascade Setting (CCSET) - _writeData(0x02); // TSFIX - _writeCommand(0xE5); // Force Temperature (TSSET) - _writeData(0x5A); // 90, 1015000us - //_writeData(0x6E); // 110, 1542001 - } - _writeCommand(0x50); - _writeData(0x97); - _PowerOn(); - _writeCommand(0x12); //display refresh - _waitWhileBusy("_Update_Full", full_refresh_time); - _init_display_done = false; // needed, reason unknown -} - -void GxEPD2_310_GDEQ031T10::_Update_Part() { - if (hasFastPartialUpdate) { - _writeCommand(0xE0); // Cascade Setting (CCSET) - _writeData(0x02); // TSFIX - _writeCommand(0xE5); // Force Temperature (TSSET) - _writeData(0x79); // 121 - } - _writeCommand(0x50); - _writeData(0xD7); - _PowerOn(); - _writeCommand(0x12); //display refresh - _waitWhileBusy("_Update_Part", partial_refresh_time); - _init_display_done = false; // needed, reason unknown -} diff --git a/Boards/LilygoTdeckPro/Source/hal/GxEPD2_310_GDEQ031T10.h b/Boards/LilygoTdeckPro/Source/hal/GxEPD2_310_GDEQ031T10.h deleted file mode 100644 index 438f9460..00000000 --- a/Boards/LilygoTdeckPro/Source/hal/GxEPD2_310_GDEQ031T10.h +++ /dev/null @@ -1,100 +0,0 @@ -// Display Library for SPI e-paper panels from Dalian Good Display and boards from Waveshare. -// Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines! -// -// based on Demo Example from Good Display: https://www.good-display.com/product/426.html -// Panel: GDEQ031T10 : https://www.good-display.com/product/426.html -// Controller: UC8253 : https://v4.cecdn.yun300.cn/100001_1909185148/UC8253.pdf -// -// Author: Jean-Marc Zingg -// -// Version: see library.properties -// -// Library: https://github.com/ZinggJM/GxEPD2 - -#pragma once - -#include "GxEPD2_EPD.h" - -class GxEPD2_310_GDEQ031T10 : public GxEPD2_EPD -{ -public: - // attributes - static const uint16_t WIDTH = 240; - static const uint16_t WIDTH_VISIBLE = WIDTH; - static const uint16_t HEIGHT = 320; - static const GxEPD2::Panel panel = GxEPD2::GDEQ031T10; - static const bool hasColor = false; - static const bool hasPartialUpdate = true; - static const bool usePartialUpdateWindow = true; // set false for better image - static const bool hasFastPartialUpdate = true; // set this false to force full refresh always - static const bool useFastFullUpdate = true; - // set false for extended (low) temperature range, 1015000us vs 3082001us - static const uint16_t power_on_time = 50; // ms, e.g. 45000us - static const uint16_t power_off_time = 50; // ms, e.g. 45000us - static const uint16_t full_refresh_time = 1100; // ms, e.g. 1015000us - static const uint16_t partial_refresh_time = 700; // ms, e.g. 650000us - // constructor - GxEPD2_310_GDEQ031T10(int16_t cs, int16_t dc, int16_t rst, int16_t busy); - // methods (virtual) - // Support for Bitmaps (Sprites) to Controller Buffer and to Screen - void clearScreen(uint8_t value = 0xFF); // init controller memory and screen (default white) - void writeScreenBuffer(uint8_t value = 0xFF); // init controller memory (default white) - void writeScreenBufferAgain(uint8_t value = 0xFF); // init previous buffer controller memory (default white) - // write to controller memory, without screen refresh; x and w should be multiple of 8 - void writeImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, - bool mirror_y = false, bool pgm = false); - void writeImageForFullRefresh(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, - bool invert = false, bool mirror_y = false, bool pgm = false); - void writeImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, - int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, - bool pgm = false); - void writeImage(const uint8_t* black, const uint8_t* color, int16_t x, int16_t y, int16_t w, int16_t h, - bool invert = false, bool mirror_y = false, bool pgm = false); - void writeImagePart(const uint8_t* black, const uint8_t* color, int16_t x_part, int16_t y_part, int16_t w_bitmap, - int16_t h_bitmap, - int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, - bool pgm = false); - // for differential update: set current and previous buffers equal (for fast partial update to work correctly) - void writeImageAgain(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, - bool mirror_y = false, bool pgm = false); - void writeImagePartAgain(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, - int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, - bool pgm = false); - // write sprite of native data to controller memory, without screen refresh; x and w should be multiple of 8 - void writeNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, - bool invert = false, bool mirror_y = false, bool pgm = false); - // write to controller memory, with screen refresh; x and w should be multiple of 8 - void drawImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, - bool mirror_y = false, bool pgm = false); - void drawImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, - int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, - bool pgm = false); - void drawImage(const uint8_t* black, const uint8_t* color, int16_t x, int16_t y, int16_t w, int16_t h, - bool invert = false, bool mirror_y = false, bool pgm = false); - void drawImagePart(const uint8_t* black, const uint8_t* color, int16_t x_part, int16_t y_part, int16_t w_bitmap, - int16_t h_bitmap, - int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, - bool pgm = false); - // write sprite of native data to controller memory, with screen refresh; x and w should be multiple of 8 - void drawNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, - bool invert = false, bool mirror_y = false, bool pgm = false); - void refresh(bool partial_update_mode = false); // screen refresh from controller memory to full screen - void refresh(int16_t x, int16_t y, int16_t w, int16_t h); // screen refresh from controller memory, partial screen - void powerOff(); // turns off generation of panel driving voltages, avoids screen fading over time - void hibernate(); - // turns powerOff() and sets controller to deep sleep for minimum power use, ONLY if wakeable by RST (rst >= 0) -private: - void _writeScreenBuffer(uint8_t command, uint8_t value); - void _writeImage(uint8_t command, const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, - bool invert = false, bool mirror_y = false, bool pgm = false); - void _writeImagePart(uint8_t command, const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, - int16_t h_bitmap, - int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, - bool pgm = false); - void _setPartialRamArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h); - void _PowerOn(); - void _PowerOff(); - void _InitDisplay(); - void _Update_Full(); - void _Update_Part(); -}; diff --git a/Boards/LilygoTdeckPro/Source/hal/GxEPD2_EPD.cpp b/Boards/LilygoTdeckPro/Source/hal/GxEPD2_EPD.cpp deleted file mode 100644 index cf155c4d..00000000 --- a/Boards/LilygoTdeckPro/Source/hal/GxEPD2_EPD.cpp +++ /dev/null @@ -1,161 +0,0 @@ -// Display Library for SPI e-paper panels from Dalian Good Display and boards from Waveshare. -// Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines! -// -// Display Library based on Demo Example from Good Display: https://www.good-display.com/companyfile/32/ -// -// Author: Jean-Marc Zingg -// -// Version: see library.properties -// -// Library: https://github.com/ZinggJM/GxEPD2 - -#include "GxEPD2_EPD.h" - -#include -#include - -constexpr auto* TAG = "GxEPD2"; - -GxEPD2_EPD::GxEPD2_EPD(int16_t cs, int16_t dc, int16_t rst, int16_t busy, int16_t busy_level, uint32_t busy_timeout, - uint16_t w, uint16_t h, GxEPD2::Panel p, bool c, bool pu, bool fpu) : - WIDTH(w), HEIGHT(h), panel(p), hasColor(c), hasPartialUpdate(pu), hasFastPartialUpdate(fpu), - _cs(cs), _dc(dc), _rst(rst), _busy(busy), _busy_level(busy_level), _busy_timeout(busy_timeout) { - _initial_write = true; - _initial_refresh = true; - _power_is_on = false; - _using_partial_mode = false; - _hibernating = false; - _init_display_done = false; - _busy_callback = 0; - _busy_callback_parameter = 0; -} - -void GxEPD2_EPD::init() { init(true, 10, false); } - -void GxEPD2_EPD::init(bool initial, uint16_t reset_duration, bool pulldown_rst_mode) { - _initial_write = initial; - _initial_refresh = initial; - _pulldown_rst_mode = pulldown_rst_mode; - _power_is_on = false; - _using_partial_mode = false; - _hibernating = false; - _init_display_done = false; -} - -void GxEPD2_EPD::setBusyCallback(void (*busyCallback)(const void*), const void* busy_callback_parameter) { - _busy_callback = busyCallback; - _busy_callback_parameter = busy_callback_parameter; -} - -void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time) { - if (_busy >= 0) { - tt::kernel::delayMillis(1); // add some margin to become active - unsigned long start = tt::kernel::getMicros(); - while (1) { - if (digitalRead(_busy) != _busy_level) break; - if (_busy_callback) _busy_callback(_busy_callback_parameter); - else tt::kernel::delayMillis(1); - if (digitalRead(_busy) != _busy_level) break; - if (tt::kernel::getMicros() - start > _busy_timeout) { - TT_LOG_W(TAG, "Busy timeout"); - break; - } - vPortYield(); // avoid wdt - } - (void)start; - } else tt::kernel::delayMillis(busy_time); -} - -void GxEPD2_EPD::_writeCommand(uint8_t c) { - _pSPIx->beginTransaction(_spi_settings); - if (_dc >= 0) digitalWrite(_dc, LOW); - if (_cs >= 0) digitalWrite(_cs, LOW); - _pSPIx->transfer(c); - if (_cs >= 0) digitalWrite(_cs, HIGH); - if (_dc >= 0) digitalWrite(_dc, HIGH); - _pSPIx->endTransaction(); -} - -void GxEPD2_EPD::_writeData(uint8_t d) { - _pSPIx->beginTransaction(_spi_settings); - if (_cs >= 0) digitalWrite(_cs, LOW); - _pSPIx->transfer(d); - if (_cs >= 0) digitalWrite(_cs, HIGH); - _pSPIx->endTransaction(); -} - -void GxEPD2_EPD::_writeData(const uint8_t* data, uint16_t n) { - _pSPIx->beginTransaction(_spi_settings); - if (_cs >= 0) digitalWrite(_cs, LOW); - for (uint16_t i = 0; i < n; i++) { _pSPIx->transfer(*data++); } - if (_cs >= 0) digitalWrite(_cs, HIGH); - _pSPIx->endTransaction(); -} - -void GxEPD2_EPD::_writeDataPGM(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes) { - _pSPIx->beginTransaction(_spi_settings); - if (_cs >= 0) digitalWrite(_cs, LOW); - for (uint16_t i = 0; i < n; i++) { _pSPIx->transfer(pgm_read_byte(&*data++)); } - while (fill_with_zeroes > 0) { - _pSPIx->transfer(0x00); - fill_with_zeroes--; - } - if (_cs >= 0) digitalWrite(_cs, HIGH); - _pSPIx->endTransaction(); -} - -void GxEPD2_EPD::_writeDataPGM_sCS(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes) { - _pSPIx->beginTransaction(_spi_settings); - for (uint8_t i = 0; i < n; i++) { - if (_cs >= 0) digitalWrite(_cs, LOW); - _pSPIx->transfer(pgm_read_byte(&*data++)); - if (_cs >= 0) digitalWrite(_cs, HIGH); - } - while (fill_with_zeroes > 0) { - if (_cs >= 0) digitalWrite(_cs, LOW); - _pSPIx->transfer(0x00); - fill_with_zeroes--; - if (_cs >= 0) digitalWrite(_cs, HIGH); - } - _pSPIx->endTransaction(); -} - -void GxEPD2_EPD::_writeCommandData(const uint8_t* pCommandData, uint8_t datalen) { - _pSPIx->beginTransaction(_spi_settings); - if (_dc >= 0) digitalWrite(_dc, LOW); - if (_cs >= 0) digitalWrite(_cs, LOW); - _pSPIx->transfer(*pCommandData++); - if (_dc >= 0) digitalWrite(_dc, HIGH); - for (uint8_t i = 0; i < datalen - 1; i++) // sub the command - { - _pSPIx->transfer(*pCommandData++); - } - if (_cs >= 0) digitalWrite(_cs, HIGH); - _pSPIx->endTransaction(); -} - -void GxEPD2_EPD::_writeCommandDataPGM(const uint8_t* pCommandData, uint8_t datalen) { - _pSPIx->beginTransaction(_spi_settings); - if (_dc >= 0) digitalWrite(_dc, LOW); - if (_cs >= 0) digitalWrite(_cs, LOW); - _pSPIx->transfer(pgm_read_byte(&*pCommandData++)); - if (_dc >= 0) digitalWrite(_dc, HIGH); - for (uint8_t i = 0; i < datalen - 1; i++) // sub the command - { - _pSPIx->transfer(pgm_read_byte(&*pCommandData++)); - } - if (_cs >= 0) digitalWrite(_cs, HIGH); - _pSPIx->endTransaction(); -} - -void GxEPD2_EPD::_startTransfer() { - _pSPIx->beginTransaction(_spi_settings); - if (_cs >= 0) digitalWrite(_cs, LOW); -} - -void GxEPD2_EPD::_transfer(uint8_t value) { _pSPIx->transfer(value); } - -void GxEPD2_EPD::_endTransfer() { - if (_cs >= 0) digitalWrite(_cs, HIGH); - _pSPIx->endTransaction(); -} \ No newline at end of file diff --git a/Boards/LilygoTdeckPro/Source/hal/GxEPD2_EPD.h b/Boards/LilygoTdeckPro/Source/hal/GxEPD2_EPD.h deleted file mode 100644 index fc759954..00000000 --- a/Boards/LilygoTdeckPro/Source/hal/GxEPD2_EPD.h +++ /dev/null @@ -1,143 +0,0 @@ -// Display Library for SPI e-paper panels from Dalian Good Display and boards from Waveshare. -// Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines! -// -// Display Library based on Demo Example from Good Display: https://www.good-display.com/companyfile/32/ -// -// Author: Jean-Marc Zingg -// -// Version: see library.properties -// -// Library: https://github.com/ZinggJM/GxEPD2 - -#pragma once - -#include "GxEPD2.h" -#include - -#pragma GCC diagnostic ignored "-Wunused-parameter" - -class GxEPD2_EPD -{ -public: - // attributes - const uint16_t WIDTH; - const uint16_t HEIGHT; - const GxEPD2::Panel panel; - const bool hasColor; - const bool hasPartialUpdate; - const bool hasFastPartialUpdate; - // constructor - GxEPD2_EPD(int16_t cs, int16_t dc, int16_t rst, int16_t busy, int16_t busy_level, uint32_t busy_timeout, - uint16_t w, uint16_t h, GxEPD2::Panel p, bool c, bool pu, bool fpu); - virtual void init(); - virtual void init(bool initial, uint16_t reset_duration = 10, bool pulldown_rst_mode = false); - // Support for Bitmaps (Sprites) to Controller Buffer and to Screen - virtual void clearScreen(uint8_t value) = 0; // init controller memory and screen (default white) - virtual void writeScreenBuffer(uint8_t value) = 0; // init controller memory (default white) - // write to controller memory, without screen refresh; x and w should be multiple of 8 - virtual void writeImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, - bool mirror_y = false, bool pgm = false) = 0; - - virtual void writeImageForFullRefresh(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, - bool invert = false, bool mirror_y = false, bool pgm = false) - { - // writeImage is independent from refresh mode for most controllers, exception e.g. SSD1681 - writeImage(bitmap, x, y, w, h, invert, mirror_y, pgm); - } - - virtual void writeImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, - int16_t h_bitmap, - int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, - bool pgm = false) = 0; - // virtual void writeImage(const uint8_t* black, const uint8_t* color, int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0; - // virtual void writeImagePart(const uint8_t* black, const uint8_t* color, int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, - // int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0; - // write sprite of native data to controller memory, without screen refresh; x and w should be multiple of 8 - // virtual void writeNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0; - // for differential update: set current and previous buffers equal (for fast partial update to work correctly) - virtual void writeScreenBufferAgain(uint8_t value = 0xFF) // init controller memory (default white) - { - // most controllers with differential update do switch buffers on refresh, can use: - writeScreenBuffer(value); - } - - virtual void writeImageAgain(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, - bool invert = false, bool mirror_y = false, bool pgm = false) - { - // most controllers with differential update do switch buffers on refresh, can use: - writeImage(bitmap, x, y, w, h, invert, mirror_y, pgm); - } - - virtual void writeImagePartAgain(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, - int16_t h_bitmap, - int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, - bool mirror_y = false, bool pgm = false) - { - // most controllers with differential update do switch buffers on refresh, can use: - writeImagePart(bitmap, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); - } - - // write to controller memory, with screen refresh; x and w should be multiple of 8 - // virtual void drawImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0; - // virtual void drawImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, - // int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0; - // virtual void drawImage(const uint8_t* black, const uint8_t* color, int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0; - // virtual void drawImagePart(const uint8_t* black, const uint8_t* color, int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, - // int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0; - // write sprite of native data to controller memory, with screen refresh; x and w should be multiple of 8 - // virtual void drawNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0; - // a demo bitmap can use yet another bitmap format, e.g. 7-color bitmap from Good Display for GDEY073D46 - virtual void writeDemoBitmap(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, - int16_t mode = 0, bool mirror_y = false, bool pgm = false) - { - }; - - virtual void drawDemoBitmap(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, - int16_t mode = 0, bool mirror_y = false, bool pgm = false) - { - }; - virtual void refresh(bool partial_update_mode = false) = 0; // screen refresh from controller memory to full screen - virtual void refresh(int16_t x, int16_t y, int16_t w, int16_t h) = 0; - // screen refresh from controller memory, partial screen - virtual void powerOff() = 0; // turns off generation of panel driving voltages, avoids screen fading over time - virtual void hibernate() = 0; - // turns powerOff() and sets controller to deep sleep for minimum power use, ONLY if wakeable by RST (rst >= 0) - virtual void setPaged() - { - }; // for GxEPD2_154c paged workaround - // register a callback function to be called during _waitWhileBusy continuously. - void setBusyCallback(void (*busyCallback)(const void*), const void* busy_callback_parameter = 0); - - static inline uint16_t gx_uint16_min(uint16_t a, uint16_t b) - { - return (a < b ? a : b); - }; - - static inline uint16_t gx_uint16_max(uint16_t a, uint16_t b) - { - return (a > b ? a : b); - }; - -protected: - void _waitWhileBusy(const char* comment = 0, uint16_t busy_time = 5000); - void _writeCommand(uint8_t c); - void _writeData(uint8_t d); - void _writeData(const uint8_t* data, uint16_t n); - void _writeDataPGM(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes = 0); - void _writeDataPGM_sCS(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes = 0); - void _writeCommandData(const uint8_t* pCommandData, uint8_t datalen); - void _writeCommandDataPGM(const uint8_t* pCommandData, uint8_t datalen); - void _startTransfer(); - void _transfer(uint8_t value); - void _endTransfer(); - -protected: - int16_t _cs, _dc, _rst, _busy, _busy_level; - uint32_t _busy_timeout; - bool _pulldown_rst_mode; - bool _initial_write, _initial_refresh; - bool _power_is_on, _using_partial_mode, _hibernating; - bool _init_display_done; - void (*_busy_callback)(const void*); - const void* _busy_callback_parameter; -}; diff --git a/Boards/LilygoTdeckPro/Source/hal/TdeckDisplay.cpp b/Boards/LilygoTdeckPro/Source/hal/TdeckDisplay.cpp deleted file mode 100644 index 01175a21..00000000 --- a/Boards/LilygoTdeckPro/Source/hal/TdeckDisplay.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "TdeckDisplay.h" -#include "TdeckDisplayConstants.h" - -#include -#include -#include - -#include - -#define TAG "tdeck_display" - -static std::shared_ptr createTouch() { - // Note for future changes: Reset pin is 48 and interrupt pin is 47 - auto configuration = std::make_unique( - I2C_NUM_0, - 240, - 320, - true, - true, - false - ); - - return std::make_shared(std::move(configuration)); -} - -std::shared_ptr createDisplay() { - auto touch = createTouch(); - - auto configuration = std::make_unique( - TDECK_LCD_SPI_HOST, - TDECK_LCD_PIN_CS, - TDECK_LCD_PIN_DC, - 320, - 240, - touch, - true, - true, - false, - true - ); - - configuration->backlightDutyFunction = driver::pwmbacklight::setBacklightDuty; - - return std::make_shared(std::move(configuration)); -} diff --git a/Boards/LilygoTdeckPro/Source/hal/TdeckDisplay.h b/Boards/LilygoTdeckPro/Source/hal/TdeckDisplay.h deleted file mode 100644 index e9a26ec4..00000000 --- a/Boards/LilygoTdeckPro/Source/hal/TdeckDisplay.h +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#include "Tactility/hal/display/DisplayDevice.h" -#include -#include - -class TdeckDisplay : public tt::hal::display::DisplayDevice { - -private: - - esp_lcd_panel_io_handle_t ioHandle = nullptr; - esp_lcd_panel_handle_t panelHandle = nullptr; - lv_display_t* displayHandle = nullptr; - bool poweredOn = false; - -public: - - std::string getName() const final { return "ST7789"; } - std::string getDescription() const final { return "SPI display"; } - - bool start() override; - - bool stop() override; - - void setPowerOn(bool turnOn) override; - bool isPoweredOn() const override { return poweredOn; }; - bool supportsPowerControl() const override { return true; } - - std::shared_ptr _Nullable createTouch() override; - - void setBacklightDuty(uint8_t backlightDuty) override; - bool supportsBacklightDuty() const override { return true; } - - void setGammaCurve(uint8_t index) override; - uint8_t getGammaCurveCount() const override { return 4; }; - - lv_display_t* _Nullable getLvglDisplay() const override { return displayHandle; } -}; - -std::shared_ptr createDisplay(); diff --git a/Boards/LilygoTdeckPro/Source/hal/TdeckDisplayConstants.h b/Boards/LilygoTdeckPro/Source/hal/TdeckDisplayConstants.h deleted file mode 100644 index f0dbb645..00000000 --- a/Boards/LilygoTdeckPro/Source/hal/TdeckDisplayConstants.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#define TDECK_LCD_SPI_HOST SPI2_HOST -#define TDECK_LCD_PIN_CS GPIO_NUM_12 -#define TDECK_LCD_PIN_DC GPIO_NUM_11 // RS -#define TDECK_LCD_HORIZONTAL_RESOLUTION 320 -#define TDECK_LCD_VERTICAL_RESOLUTION 240 -#define TDECK_LCD_SPI_TRANSFER_HEIGHT (TDECK_LCD_VERTICAL_RESOLUTION / 10) diff --git a/Boards/LilygoTdeckPro/Source/hal/TdeckKeyboard.cpp b/Boards/LilygoTdeckPro/Source/hal/TdeckKeyboard.cpp deleted file mode 100644 index 47c8590d..00000000 --- a/Boards/LilygoTdeckPro/Source/hal/TdeckKeyboard.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include "TdeckKeyboard.h" -#include -#include - -#define TAG "tdeck_keyboard" - -#define TDECK_KEYBOARD_I2C_BUS_HANDLE I2C_NUM_0 -#define TDECK_KEYBOARD_SLAVE_ADDRESS 0x55 - -static inline bool keyboard_i2c_read(uint8_t* output) { - return tt::hal::i2c::masterRead(TDECK_KEYBOARD_I2C_BUS_HANDLE, TDECK_KEYBOARD_SLAVE_ADDRESS, output, 1, 100 / portTICK_PERIOD_MS); -} - -/** - * The callback simulates press and release events, because the T-Deck - * keyboard only publishes press events on I2C. - * LVGL currently works without those extra release events, but they - * are implemented for correctness and future compatibility. - * - * @param indev_drv - * @param data - */ -static void keyboard_read_callback(TT_UNUSED lv_indev_t* indev, lv_indev_data_t* data) { - static uint8_t last_buffer = 0x00; - uint8_t read_buffer = 0x00; - - // Defaults - data->key = 0; - data->state = LV_INDEV_STATE_RELEASED; - - if (keyboard_i2c_read(&read_buffer)) { - if (read_buffer == 0 && read_buffer != last_buffer) { - TT_LOG_D(TAG, "Released %d", last_buffer); - data->key = last_buffer; - data->state = LV_INDEV_STATE_RELEASED; - } else if (read_buffer != 0) { - TT_LOG_D(TAG, "Pressed %d", read_buffer); - data->key = read_buffer; - data->state = LV_INDEV_STATE_PRESSED; - } - } - - last_buffer = read_buffer; -} - -bool TdeckKeyboard::start(lv_display_t* display) { - deviceHandle = lv_indev_create(); - lv_indev_set_type(deviceHandle, LV_INDEV_TYPE_KEYPAD); - lv_indev_set_read_cb(deviceHandle, &keyboard_read_callback); - lv_indev_set_display(deviceHandle, display); - lv_indev_set_user_data(deviceHandle, this); - return true; -} - -bool TdeckKeyboard::stop() { - lv_indev_delete(deviceHandle); - deviceHandle = nullptr; - return true; -} - -bool TdeckKeyboard::isAttached() const { - return tt::hal::i2c::masterHasDeviceAtAddress(TDECK_KEYBOARD_I2C_BUS_HANDLE, TDECK_KEYBOARD_SLAVE_ADDRESS, 100); -} - -std::shared_ptr createKeyboard() { - return std::make_shared(); -} diff --git a/Boards/LilygoTdeckPro/Source/hal/TdeckKeyboard.h b/Boards/LilygoTdeckPro/Source/hal/TdeckKeyboard.h deleted file mode 100644 index 26eea592..00000000 --- a/Boards/LilygoTdeckPro/Source/hal/TdeckKeyboard.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -class TdeckKeyboard : public tt::hal::keyboard::KeyboardDevice { - -private: - - lv_indev_t* _Nullable deviceHandle = nullptr; - -public: - - std::string getName() const final { return "T-Deck Keyboard"; } - std::string getDescription() const final { return "I2C keyboard"; } - - bool start(lv_display_t* display) override; - bool stop() override; - bool isAttached() const override; - lv_indev_t* _Nullable getLvglIndev() override { return deviceHandle; } -}; - -std::shared_ptr createKeyboard(); diff --git a/Boards/LilygoTdeckPro/Source/hal/TdeckPower.cpp b/Boards/LilygoTdeckPro/Source/hal/TdeckPower.cpp deleted file mode 100644 index 008198db..00000000 --- a/Boards/LilygoTdeckPro/Source/hal/TdeckPower.cpp +++ /dev/null @@ -1,130 +0,0 @@ -#include "TdeckPower.h" - -#include - -#define TAG "power" - -/** - * 2.0 ratio, but +.11 added as display voltage sag compensation. - */ -#define ADC_MULTIPLIER 2.11 - -#define ADC_REF_VOLTAGE 3.3f -#define BATTERY_VOLTAGE_MIN 3.2f -#define BATTERY_VOLTAGE_MAX 4.2f - -static adc_oneshot_unit_init_cfg_t adcConfig = { - .unit_id = ADC_UNIT_1, - .clk_src = ADC_RTC_CLK_SRC_DEFAULT, - .ulp_mode = ADC_ULP_MODE_DISABLE, -}; - -static adc_oneshot_chan_cfg_t adcChannelConfig = { - .atten = ADC_ATTEN_DB_12, - .bitwidth = ADC_BITWIDTH_DEFAULT, -}; - -static uint8_t estimateChargeLevelFromVoltage(uint32_t milliVolt) { - float volts = std::min((float)milliVolt / 1000.f, BATTERY_VOLTAGE_MAX); - float voltage_percentage = (volts - BATTERY_VOLTAGE_MIN) / (BATTERY_VOLTAGE_MAX - BATTERY_VOLTAGE_MIN); - float voltage_factor = std::min(1.0f, voltage_percentage); - auto charge_level = (uint8_t) (voltage_factor * 100.f); - TT_LOG_V(TAG, "mV = %lu, scaled = %.2f, factor = %.2f, result = %d", milliVolt, volts, voltage_factor, charge_level); - return charge_level; -} - -TdeckPower::TdeckPower() { - if (adc_oneshot_new_unit(&adcConfig, &adcHandle) != ESP_OK) { - TT_LOG_E(TAG, "ADC config failed"); - return; - } - - if (adc_oneshot_config_channel(adcHandle, ADC_CHANNEL_3, &adcChannelConfig) != ESP_OK) { - TT_LOG_E(TAG, "ADC channel config failed"); - - adc_oneshot_del_unit(adcHandle); - return; - } -} - -TdeckPower::~TdeckPower() { - if (adcHandle) { - adc_oneshot_del_unit(adcHandle); - } -} - -bool TdeckPower::supportsMetric(MetricType type) const { - switch (type) { - using enum MetricType; - case BatteryVoltage: - case ChargeLevel: - return true; - default: - return false; - } - - return false; // Safety guard for when new enum values are introduced -} - -bool TdeckPower::getMetric(MetricType type, MetricData& data) { - switch (type) { - using enum MetricType; - case BatteryVoltage: - return readBatteryVoltageSampled(data.valueAsUint32); - case ChargeLevel: - if (readBatteryVoltageSampled(data.valueAsUint32)) { - data.valueAsUint32 = estimateChargeLevelFromVoltage(data.valueAsUint32); - return true; - } else { - return false; - } - default: - return false; - } - - return false; // Safety guard for when new enum values are introduced -} - -bool TdeckPower::readBatteryVoltageOnce(uint32_t& output) { - int raw; - if (adc_oneshot_read(adcHandle, ADC_CHANNEL_3, &raw) == ESP_OK) { - output = ADC_MULTIPLIER * ((1000.f * ADC_REF_VOLTAGE) / 4096.f) * (float)raw; - TT_LOG_V(TAG, "Raw = %d, voltage = %lu", raw, output); - return true; - } else { - TT_LOG_E(TAG, "Read failed"); - return false; - } -} - -#define MAX_VOLTAGE_SAMPLES 15 - -bool TdeckPower::readBatteryVoltageSampled(uint32_t& output) { - size_t samples_read = 0; - uint32_t sample_accumulator = 0; - uint32_t sample_read_buffer; - - for (size_t i = 0; i < MAX_VOLTAGE_SAMPLES; ++i) { - if (readBatteryVoltageOnce(sample_read_buffer)) { - sample_accumulator += sample_read_buffer; - samples_read++; - } - } - - if (samples_read > 0) { - output = sample_accumulator / samples_read; - return true; - } else { - return false; - } -} - -static std::shared_ptr power; - -std::shared_ptr tdeck_get_power() { - if (power == nullptr) { - power = std::make_shared(); - } - return power; -} - diff --git a/Boards/LilygoTdeckPro/Source/hal/TdeckPower.h b/Boards/LilygoTdeckPro/Source/hal/TdeckPower.h deleted file mode 100644 index 4dc4d75c..00000000 --- a/Boards/LilygoTdeckPro/Source/hal/TdeckPower.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include "Tactility/hal/power/PowerDevice.h" -#include -#include - -using tt::hal::power::PowerDevice; - -class TdeckPower : public PowerDevice { - - adc_oneshot_unit_handle_t adcHandle = nullptr; - -public: - - TdeckPower(); - ~TdeckPower(); - - std::string getName() const final { return "ADC Power Measurement"; } - std::string getDescription() const final { return "Power measurement interface via ADC pin"; } - - bool supportsMetric(MetricType type) const override; - bool getMetric(MetricType type, MetricData& data) override; - -private: - - bool readBatteryVoltageSampled(uint32_t& output); - bool readBatteryVoltageOnce(uint32_t& output); -}; - -std::shared_ptr tdeck_get_power(); diff --git a/Boards/LilygoTdeckPro/Source/hal/TdeckSdCard.cpp b/Boards/LilygoTdeckPro/Source/hal/TdeckSdCard.cpp deleted file mode 100644 index 486d3d13..00000000 --- a/Boards/LilygoTdeckPro/Source/hal/TdeckSdCard.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "TdeckSdCard.h" - -#include -#include - -#include - -using tt::hal::sdcard::SpiSdCardDevice; - -#define TDECK_SDCARD_PIN_CS GPIO_NUM_39 -#define TDECK_LCD_PIN_CS GPIO_NUM_12 -#define TDECK_RADIO_PIN_CS GPIO_NUM_9 - -std::shared_ptr createTdeckSdCard() { - auto* configuration = new SpiSdCardDevice::Config( - TDECK_SDCARD_PIN_CS, - GPIO_NUM_NC, - GPIO_NUM_NC, - GPIO_NUM_NC, - SdCardDevice::MountBehaviour::AtBoot, - tt::lvgl::getSyncLock(), - { - TDECK_RADIO_PIN_CS, - TDECK_LCD_PIN_CS - } - ); - - auto* sdcard = (SdCardDevice*) new SpiSdCardDevice( - std::unique_ptr(configuration) - ); - - return std::shared_ptr(sdcard); -} diff --git a/Boards/LilygoTdeckPro/Source/hal/TdeckSdCard.h b/Boards/LilygoTdeckPro/Source/hal/TdeckSdCard.h deleted file mode 100644 index e12eb6d0..00000000 --- a/Boards/LilygoTdeckPro/Source/hal/TdeckSdCard.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -#include "Tactility/hal/sdcard/SdCardDevice.h" - -using tt::hal::sdcard::SdCardDevice; - -std::shared_ptr createTdeckSdCard(); diff --git a/Buildscripts/board.cmake b/Buildscripts/board.cmake index ada9a64f..29afb107 100644 --- a/Buildscripts/board.cmake +++ b/Buildscripts/board.cmake @@ -47,10 +47,6 @@ function(INIT_TACTILITY_GLOBALS SDKCONFIG_FILE) set(TACTILITY_BOARD_PROJECT ElecrowCrowpanelBasic50) elseif (board_id STREQUAL "lilygo-tdeck") set(TACTILITY_BOARD_PROJECT LilygoTdeck) - elseif (board_id STREQUAL "lilygo-tdeck-plus") - set(TACTILITY_BOARD_PROJECT LilygoTdeck) - elseif (board_id STREQUAL "lilygo-tdeck-pro") - set(TACTILITY_BOARD_PROJECT LilygoTdeckPro) elseif (board_id STREQUAL "m5stack-core2") set(TACTILITY_BOARD_PROJECT M5stackCore2) elseif (board_id STREQUAL "m5stack-cores3") diff --git a/ExternalApps/HelloWorld/.gitignore b/ExternalApps/HelloWorld/.gitignore index 31095bb4..89baa26e 100644 --- a/ExternalApps/HelloWorld/.gitignore +++ b/ExternalApps/HelloWorld/.gitignore @@ -1,2 +1,2 @@ build*/ -.tactility/ \ No newline at end of file +.tactility/ diff --git a/sdkconfig.board.lilygo-tdeck-plus b/sdkconfig.board.lilygo-tdeck-plus deleted file mode 100644 index 0b3fc464..00000000 --- a/sdkconfig.board.lilygo-tdeck-plus +++ /dev/null @@ -1,53 +0,0 @@ -# Software defaults -# Increase stack size for WiFi (fixes crash after scan) -CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072 -CONFIG_LV_FONT_MONTSERRAT_14=y -CONFIG_LV_FONT_MONTSERRAT_18=y -CONFIG_LV_USE_USER_DATA=y -CONFIG_LV_USE_FS_STDIO=y -CONFIG_LV_FS_STDIO_LETTER=65 -CONFIG_LV_FS_STDIO_PATH="" -CONFIG_LV_FS_STDIO_CACHE_SIZE=4096 -CONFIG_LV_USE_LODEPNG=y -CONFIG_LV_USE_BUILTIN_MALLOC=n -CONFIG_LV_USE_CLIB_MALLOC=y -CONFIG_LV_USE_MSGBOX=n -CONFIG_LV_USE_SPINNER=n -CONFIG_LV_USE_WIN=n -CONFIG_LV_USE_SNAPSHOT=y -CONFIG_FREERTOS_HZ=1000 -CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2 -CONFIG_FREERTOS_SMP=n -CONFIG_FREERTOS_UNICORE=n -CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096 -CONFIG_FREERTOS_USE_TRACE_FACILITY=y -CONFIG_PARTITION_TABLE_CUSTOM=y -CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" -CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" -CONFIG_FATFS_LFN_HEAP=y -CONFIG_FATFS_VOLUME_COUNT=3 - -# Hardware: Main -CONFIG_TT_BOARD_LILYGO_TDECK_PLUS=y -CONFIG_TT_BOARD_NAME="LilyGo T-Deck Plus" -CONFIG_TT_BOARD_ID="lilygo-tdeck-plus" -CONFIG_IDF_EXPERIMENTAL_FEATURES=y -CONFIG_IDF_TARGET="esp32s3" -CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y -CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y -CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y -CONFIG_FLASHMODE_QIO=y -# Hardware: SPI RAM -CONFIG_ESP32S3_SPIRAM_SUPPORT=y -CONFIG_SPIRAM_MODE_OCT=y -CONFIG_SPIRAM_SPEED_120M=y -CONFIG_SPIRAM_USE_MALLOC=y -CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y -# SPI Flash (can set back to 80MHz after ESP-IDF bug is resolved) -CONFIG_ESPTOOLPY_FLASHFREQ_120M=y -# LVGL -CONFIG_LV_DPI_DEF=139 -CONFIG_LV_DISP_DEF_REFR_PERIOD=10 -# USB -CONFIG_TINYUSB_MSC_ENABLED=y -CONFIG_TINYUSB_MSC_MOUNT_PATH="/sdcard" \ No newline at end of file diff --git a/sdkconfig.board.lilygo-tdeck-pro b/sdkconfig.board.lilygo-tdeck-pro deleted file mode 100644 index ac0b17c7..00000000 --- a/sdkconfig.board.lilygo-tdeck-pro +++ /dev/null @@ -1,57 +0,0 @@ -# Software defaults -# Increase stack size for WiFi (fixes crash after scan) -CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072 -CONFIG_LV_FONT_MONTSERRAT_14=y -CONFIG_LV_FONT_MONTSERRAT_18=y -CONFIG_LV_USE_USER_DATA=y -CONFIG_LV_USE_FS_STDIO=y -CONFIG_LV_FS_STDIO_LETTER=65 -CONFIG_LV_FS_STDIO_PATH="" -CONFIG_LV_FS_STDIO_CACHE_SIZE=4096 -CONFIG_LV_USE_LODEPNG=y -CONFIG_LV_USE_BUILTIN_MALLOC=n -CONFIG_LV_USE_CLIB_MALLOC=y -CONFIG_LV_USE_MSGBOX=n -CONFIG_LV_USE_SPINNER=n -CONFIG_LV_USE_WIN=n -CONFIG_LV_USE_SNAPSHOT=y -CONFIG_FREERTOS_HZ=1000 -CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2 -CONFIG_FREERTOS_SMP=n -CONFIG_FREERTOS_UNICORE=n -CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096 -CONFIG_FREERTOS_USE_TRACE_FACILITY=y -CONFIG_PARTITION_TABLE_CUSTOM=y -CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" -CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" -CONFIG_FATFS_LFN_HEAP=y -CONFIG_FATFS_VOLUME_COUNT=3 - -# Hardware: Main -CONFIG_TT_BOARD_LILYGO_TDECK_PRO=y -CONFIG_TT_BOARD_NAME="LilyGo T-Deck Pro" -CONFIG_TT_BOARD_ID="lilygo-tdeck-pro" -CONFIG_IDF_EXPERIMENTAL_FEATURES=y -CONFIG_IDF_TARGET="esp32s3" -CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y -CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y -CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y -CONFIG_FLASHMODE_QIO=y -# Hardware: SPI RAM -CONFIG_ESP32S3_SPIRAM_SUPPORT=y -CONFIG_SPIRAM_MODE_OCT=y -CONFIG_SPIRAM_SPEED_120M=y -CONFIG_SPIRAM_USE_MALLOC=y -CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y -# SPI Flash (can set back to 80MHz after ESP-IDF bug is resolved) -CONFIG_ESPTOOLPY_FLASHFREQ_120M=y -# LVGL -CONFIG_LV_DPI_DEF=139 -CONFIG_LV_DISP_DEF_REFR_PERIOD=10 -CONFIG_LV_USE_THEME_DEFAULT=n -CONFIG_LV_USE_THEME_SIMPLE=n -CONFIG_LV_USE_THEME_MONO=y -CONFIG_LV_COLOR_DEPTH=1 -# USB -CONFIG_TINYUSB_MSC_ENABLED=y -CONFIG_TINYUSB_MSC_MOUNT_PATH="/sdcard" From d06197a6aa771cac390f5e6fc5041350482c0674 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sun, 15 Jun 2025 13:55:35 +0200 Subject: [PATCH 13/16] Updated README.md and its screenshots (#293) --- Documentation/pics/screenshot-AppList.png | Bin 4243 -> 4207 bytes Documentation/pics/screenshot-Desktop.png | Bin 3850 -> 0 bytes Documentation/pics/screenshot-Files.png | Bin 2590 -> 0 bytes Documentation/pics/screenshot-HelloWorld.png | Bin 2190 -> 0 bytes Documentation/pics/screenshot-Launcher.png | Bin 0 -> 4055 bytes Documentation/pics/screenshot-Settings.png | Bin 3680 -> 3869 bytes Documentation/pics/screenshot-WifiManage.png | Bin 4974 -> 0 bytes README.md | 10 +++------- 8 files changed, 3 insertions(+), 7 deletions(-) delete mode 100644 Documentation/pics/screenshot-Desktop.png delete mode 100644 Documentation/pics/screenshot-Files.png delete mode 100644 Documentation/pics/screenshot-HelloWorld.png create mode 100644 Documentation/pics/screenshot-Launcher.png delete mode 100644 Documentation/pics/screenshot-WifiManage.png diff --git a/Documentation/pics/screenshot-AppList.png b/Documentation/pics/screenshot-AppList.png index 0dac67e49ddaf52a9b32c030298efc378fe4f68a..c5e8d1947d58d542de72b83f67aef9d535da1a5c 100644 GIT binary patch literal 4207 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 literal 4243 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 Qe-{9TdZxN%D2K5B0dk29$^ZZW diff --git a/Documentation/pics/screenshot-Desktop.png b/Documentation/pics/screenshot-Desktop.png deleted file mode 100644 index c565ac8e739af018d4eef5867bddf8035843cccd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3850 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 Ri;a&SfU&egzd7N3=XV6T-q8R6 diff --git a/Documentation/pics/screenshot-HelloWorld.png b/Documentation/pics/screenshot-HelloWorld.png deleted file mode 100644 index 6acd44fad9539c3e8c2c1199050a1cac2c3d0ebe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2190 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 K_6tp?xc>q=Py~Ab literal 0 HcmV?d00001 diff --git a/Documentation/pics/screenshot-Settings.png b/Documentation/pics/screenshot-Settings.png index 917749a28aeee65b4d150c103a23a0b5e352bc46..3809d80f9620b885e54419cd62c538616fdf3e9b 100644 GIT binary patch literal 3869 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∨nrlcb%4gH)5_NH!vbx&2e5Y;!sF!y03DwfgJ&JXLUGJXoOT# zwZ(9YX<$jmUXg~)2a+kk9#5k-*xg}7dNHeR5Z_&-Wtu*jp^O|*|c z>;J9&*YzXo`k}6qd|j;{r6r^Z_`L1t#trzvNECL{kJ9RXEe^<^PuAFaR%|BzreJl- zwF+228pqD`F2NHXTjc!pGfS7x-Bh_BeeT7-Yqjb3OtTmE0qoSWT{~dC98iFd!TRj2uH%U2j- RC;wc4v4OdMt*%S_e*k;U>j3}& literal 3680 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 diff --git a/Documentation/pics/screenshot-WifiManage.png b/Documentation/pics/screenshot-WifiManage.png deleted file mode 100644 index 0c1478fc787c29cc3bb0e2071a762196b35ee943..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4974 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 diff --git a/README.md b/README.md index b2025e7a..7b40fd5b 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,11 @@ Tactility is an operating system that focuses on the ESP32 microcontroller famil See [https://tactility.one](https://tactility.one) for more information. -![photo of devices running Tactility](Documentation/pics/tactility-devices.webp)  ![screenshot of desktop app](Documentation/pics/screenshot-Desktop.png) +![photo of devices running Tactility](Documentation/pics/tactility-devices.webp)  ![screenshot of launcher app](Documentation/pics/screenshot-Launcher.png) -You can run built-in apps or start them from an SD card: +You can run built-in apps or start them from an SD card. It's easy to manage system settings: -![screenshot off app list app](Documentation/pics/screenshot-AppList.png)  ![screenshot of files app](Documentation/pics/screenshot-Files.png) - -It's easy to manage system settings: - -![screenshot of settings app](Documentation/pics/screenshot-Settings.png)  ![screenshot of wifi management app](Documentation/pics/screenshot-WifiManage.png) +![screenshot off app list app](Documentation/pics/screenshot-AppList.png)  ![screenshot of settings app](Documentation/pics/screenshot-Settings.png) ## License From ab4cf79a478c28964da9ff0e2625dfa449f827c7 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sat, 19 Jul 2025 00:27:49 +0200 Subject: [PATCH 14/16] Merge Develop into Main (#298) Various improvements and new internal APIs including a new Development service+app which allows `tactility.py` to upload and run applications remotely. --- Documentation/ideas.md | 1 + Tactility/CMakeLists.txt | 2 +- Tactility/Include/Tactility/app/ElfApp.h | 5 +- .../Include/Tactility/network/HttpdReq.h | 29 ++ Tactility/Include/Tactility/network/Url.h | 19 ++ .../Include/Tactility/service/wifi/Wifi.h | 5 + .../Tactility/app/development/Development.h | 11 + .../service/development/DevelopmentService.h | 97 ++++++ Tactility/Source/Tactility.cpp | 5 +- Tactility/Source/TactilityHeadless.cpp | 2 + Tactility/Source/app/ElfApp.cpp | 3 +- .../Source/app/development/Development.cpp | 163 ++++++++++ Tactility/Source/lvgl/Statusbar.cpp | 4 +- Tactility/Source/network/HttpdReq.cpp | 160 ++++++++++ Tactility/Source/network/Url.cpp | 82 +++++ .../development/DevelopmentService.cpp | 301 ++++++++++++++++++ Tactility/Source/service/wifi/WifiEsp.cpp | 29 ++ Tactility/Source/service/wifi/WifiMock.cpp | 4 + .../Source/service/wifi/WifiSettingsEsp.cpp | 2 +- TactilityC/Source/tt_init.cpp | 101 ++++++ TactilityCore/Include/Tactility/LogMessages.h | 3 + TactilityCore/Include/Tactility/StringUtils.h | 8 + TactilityCore/Include/Tactility/Timer.h | 1 - TactilityCore/Source/StringUtils.cpp | 10 + Tests/Tactility/UrlTest.cpp | 61 ++++ 25 files changed, 1096 insertions(+), 12 deletions(-) create mode 100644 Tactility/Include/Tactility/network/HttpdReq.h create mode 100644 Tactility/Include/Tactility/network/Url.h create mode 100644 Tactility/Private/Tactility/app/development/Development.h create mode 100644 Tactility/Private/Tactility/service/development/DevelopmentService.h create mode 100644 Tactility/Source/app/development/Development.cpp create mode 100644 Tactility/Source/network/HttpdReq.cpp create mode 100644 Tactility/Source/network/Url.cpp create mode 100644 Tactility/Source/service/development/DevelopmentService.cpp create mode 100644 Tests/Tactility/UrlTest.cpp diff --git a/Documentation/ideas.md b/Documentation/ideas.md index b46f8e6b..34db7bb5 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -1,5 +1,6 @@ # TODOs +- Bug: When a Wi-Fi SSID is too long, then it fails to save the credentials - Add a Keyboard setting app to override the behaviour of soft keyboard hiding (e.g. keyboard hardware is present, but user wants soft keyboard) - HAL for display touch calibration - Start using non_null (either via MS GSL, or custom) diff --git a/Tactility/CMakeLists.txt b/Tactility/CMakeLists.txt index 56ee17ec..db26fe3e 100644 --- a/Tactility/CMakeLists.txt +++ b/Tactility/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) if (DEFINED ENV{ESP_IDF_VERSION}) file(GLOB_RECURSE SOURCE_FILES Source/*.c*) - list(APPEND REQUIRES_LIST TactilityCore lvgl driver elf_loader lv_screenshot QRCode esp_lvgl_port minmea esp_wifi nvs_flash spiffs vfs fatfs lwip) + list(APPEND REQUIRES_LIST TactilityCore lvgl driver elf_loader lv_screenshot QRCode esp_lvgl_port minmea esp_wifi nvs_flash spiffs vfs fatfs lwip esp_http_server) if ("${IDF_TARGET}" STREQUAL "esp32s3") list(APPEND REQUIRES_LIST esp_tinyusb) endif () diff --git a/Tactility/Include/Tactility/app/ElfApp.h b/Tactility/Include/Tactility/app/ElfApp.h index c086ab6a..d9a592d8 100644 --- a/Tactility/Include/Tactility/app/ElfApp.h +++ b/Tactility/Include/Tactility/app/ElfApp.h @@ -31,10 +31,7 @@ void setElfAppManifest( */ std::string getElfAppId(const std::string& filePath); -/** - * @return true when registration was done, false when app was already registered - */ -bool registerElfApp(const std::string& filePath); +void registerElfApp(const std::string& filePath); std::shared_ptr createElfApp(const std::shared_ptr& manifest); diff --git a/Tactility/Include/Tactility/network/HttpdReq.h b/Tactility/Include/Tactility/network/HttpdReq.h new file mode 100644 index 00000000..0fbf9cac --- /dev/null +++ b/Tactility/Include/Tactility/network/HttpdReq.h @@ -0,0 +1,29 @@ +#pragma once + +#ifdef ESP_PLATFORM + +#include +#include +#include +#include +#include + +namespace tt::network { + +bool getHeaderOrSendError(httpd_req_t* request, const std::string& name, std::string& value); + +bool getMultiPartBoundaryOrSendError(httpd_req_t* request, std::string& boundary); + +bool getQueryOrSendError(httpd_req_t* request, std::string& query); + +std::unique_ptr receiveByteArray(httpd_req_t* request, size_t length, size_t& bytesRead); + +std::string receiveTextUntil(httpd_req_t* request, const std::string& terminator); + +std::map parseContentDisposition(const std::vector& input); + +bool readAndDiscardOrSendError(httpd_req_t* request, const std::string& toRead); + +} + +#endif // ESP_PLATFORM \ No newline at end of file diff --git a/Tactility/Include/Tactility/network/Url.h b/Tactility/Include/Tactility/network/Url.h new file mode 100644 index 00000000..6fb6c236 --- /dev/null +++ b/Tactility/Include/Tactility/network/Url.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace tt::network { + +/** + * Parse a query from a URL + * @param[in] query + * @return a map with key-values + */ +std::map parseUrlQuery(std::string query); + +std::string urlEncode(const std::string& input); + +std::string urlDecode(const std::string& input); + +} // namespace \ No newline at end of file diff --git a/Tactility/Include/Tactility/service/wifi/Wifi.h b/Tactility/Include/Tactility/service/wifi/Wifi.h index a43e7799..b41ad0a8 100644 --- a/Tactility/Include/Tactility/service/wifi/Wifi.h +++ b/Tactility/Include/Tactility/service/wifi/Wifi.h @@ -113,6 +113,11 @@ void setScanRecords(uint16_t records); */ void setEnabled(bool enabled); +/** + * @return the IPv4 address or empty string + */ +std::string getIp(); + /** * @brief Connect to a network. Disconnects any existing connection. * Returns immediately but runs in the background. Results are through pubsub. diff --git a/Tactility/Private/Tactility/app/development/Development.h b/Tactility/Private/Tactility/app/development/Development.h new file mode 100644 index 00000000..ca93c52d --- /dev/null +++ b/Tactility/Private/Tactility/app/development/Development.h @@ -0,0 +1,11 @@ +#pragma once + +#ifdef ESP_PLATFORM + +namespace tt::app::development { + +void start(); + +} + +#endif // ESP_PLATFORM \ No newline at end of file diff --git a/Tactility/Private/Tactility/service/development/DevelopmentService.h b/Tactility/Private/Tactility/service/development/DevelopmentService.h new file mode 100644 index 00000000..7fb0d9d4 --- /dev/null +++ b/Tactility/Private/Tactility/service/development/DevelopmentService.h @@ -0,0 +1,97 @@ +#pragma once +#ifdef ESP_PLATFORM + +#include "Tactility/service/Service.h" + +#include + +#include +#include +#include + +namespace tt::service::development { + +class DevelopmentService final : public Service { + + Mutex mutex = Mutex(Mutex::Type::Recursive); + httpd_handle_t server = nullptr; + bool enabled = false; + kernel::SystemEventSubscription networkConnectEventSubscription = 0; + kernel::SystemEventSubscription networkDisconnectEventSubscription = 0; + std::string deviceResponse; + + httpd_uri_t handleGetInfoEndpoint = { + .uri = "/info", + .method = HTTP_GET, + .handler = handleGetInfo, + .user_ctx = this + }; + + httpd_uri_t appRunEndpoint = { + .uri = "/app/run", + .method = HTTP_POST, + .handler = handleAppRun, + .user_ctx = this + }; + + httpd_uri_t appInstallEndpoint = { + .uri = "/app/install", + .method = HTTP_PUT, + .handler = handleAppInstall, + .user_ctx = this + }; + + void onNetworkConnected(); + void onNetworkDisconnected(); + + void startServer(); + void stopServer(); + + static esp_err_t handleGetInfo(httpd_req_t* request); + static esp_err_t handleAppRun(httpd_req_t* request); + static esp_err_t handleAppInstall(httpd_req_t* request); + +public: + + // region Overrides + + void onStart(ServiceContext& service) override; + void onStop(ServiceContext& service) override; + + // endregion Overrides + + // region Internal API + + /** + * Enabling the service means that the user is willing to start the web server. + * @return true when the service is enabled + */ + bool isEnabled() const; + + /** + * Enabling the service means that the user is willing to start the web server. + * @param[in] enabled + */ + void setEnabled(bool enabled); + + /** + * @return true if the service will enable itself when it is started (e.g. on boot, or manual start) + */ + bool isEnabledOnStart() const; + + /** + * Set whether the service should auto-enable when it is started. + * @param enabled + */ + void setEnabledOnStart(bool enabled); + + bool isStarted() const; + + // region Internal API +}; + +std::shared_ptr findService(); + +} + +#endif // ESP_PLATFORM diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 9a7ba056..5cf7e9cf 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -33,9 +33,10 @@ namespace app { namespace addgps { extern const AppManifest manifest; } namespace alertdialog { extern const AppManifest manifest; } namespace applist { extern const AppManifest manifest; } + namespace boot { extern const AppManifest manifest; } namespace calculator { extern const AppManifest manifest; } namespace chat { extern const AppManifest manifest; } - namespace boot { extern const AppManifest manifest; } + namespace development { extern const AppManifest manifest; } namespace display { extern const AppManifest manifest; } namespace filebrowser { extern const AppManifest manifest; } namespace fileselection { extern const AppManifest manifest; } @@ -73,6 +74,7 @@ namespace app { // endregion +// List of all apps excluding Boot app (as Boot app calls this function indirectly) static void registerSystemApps() { addApp(app::addgps::manifest); addApp(app::alertdialog::manifest); @@ -109,6 +111,7 @@ static void registerSystemApps() { #ifdef ESP_PLATFORM addApp(app::chat::manifest); addApp(app::crashdiagnostics::manifest); + addApp(app::development::manifest); #endif if (getConfiguration()->hardware->power != nullptr) { diff --git a/Tactility/Source/TactilityHeadless.cpp b/Tactility/Source/TactilityHeadless.cpp index 7b968f6e..237b110c 100644 --- a/Tactility/Source/TactilityHeadless.cpp +++ b/Tactility/Source/TactilityHeadless.cpp @@ -20,6 +20,7 @@ namespace service::gps { extern const ServiceManifest manifest; } namespace service::wifi { extern const ServiceManifest manifest; } namespace service::sdcard { extern const ServiceManifest manifest; } #ifdef ESP_PLATFORM +namespace service::development { extern const ServiceManifest manifest; } namespace service::espnow { extern const ServiceManifest manifest; } #endif @@ -33,6 +34,7 @@ static void registerAndStartSystemServices() { addService(service::sdcard::manifest); addService(service::wifi::manifest); #ifdef ESP_PLATFORM + addService(service::development::manifest); addService(service::espnow::manifest); #endif } diff --git a/Tactility/Source/app/ElfApp.cpp b/Tactility/Source/app/ElfApp.cpp index 77217194..a868e6cd 100644 --- a/Tactility/Source/app/ElfApp.cpp +++ b/Tactility/Source/app/ElfApp.cpp @@ -177,7 +177,7 @@ std::string getElfAppId(const std::string& filePath) { return filePath; } -bool registerElfApp(const std::string& filePath) { +void registerElfApp(const std::string& filePath) { if (findAppById(filePath) == nullptr) { auto manifest = AppManifest { .id = getElfAppId(filePath), @@ -187,7 +187,6 @@ bool registerElfApp(const std::string& filePath) { }; addApp(manifest); } - return false; } std::shared_ptr createElfApp(const std::shared_ptr& manifest) { diff --git a/Tactility/Source/app/development/Development.cpp b/Tactility/Source/app/development/Development.cpp new file mode 100644 index 00000000..4ea05b93 --- /dev/null +++ b/Tactility/Source/app/development/Development.cpp @@ -0,0 +1,163 @@ +#ifdef ESP_PLATFORM + +#include "Tactility/app/AppManifest.h" +#include "Tactility/lvgl/Style.h" +#include "Tactility/lvgl/Toolbar.h" +#include "Tactility/service/development/DevelopmentService.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace tt::app::development { + +constexpr const char* TAG = "Development"; + +class DevelopmentApp final : public App { + + lv_obj_t* enableSwitch = nullptr; + lv_obj_t* enableOnBootSwitch = nullptr; + lv_obj_t* statusLabel = nullptr; + std::shared_ptr service; + + Timer timer = Timer(Timer::Type::Periodic, [this] { + auto lock = lvgl::getSyncLock()->asScopedLock(); + if (lock.lock(lvgl::defaultLockTime)) { + updateViewState(); + } + }); + + static void onEnableSwitchChanged(lv_event_t* event) { + lv_event_code_t code = lv_event_get_code(event); + auto* widget = static_cast(lv_event_get_target(event)); + if (code == LV_EVENT_VALUE_CHANGED) { + bool is_on = lv_obj_has_state(widget, LV_STATE_CHECKED); + auto* app = static_cast(lv_event_get_user_data(event)); + bool is_changed = is_on != app->service->isEnabled(); + if (is_changed) { + app->service->setEnabled(is_on); + } + } + } + + static void onEnableOnBootSwitchChanged(lv_event_t* event) { + lv_event_code_t code = lv_event_get_code(event); + auto* widget = static_cast(lv_event_get_target(event)); + if (code == LV_EVENT_VALUE_CHANGED) { + bool is_on = lv_obj_has_state(widget, LV_STATE_CHECKED); + auto* app = static_cast(lv_event_get_user_data(event)); + bool is_changed = is_on != app->service->isEnabledOnStart(); + if (is_changed) { + app->service->setEnabledOnStart(is_on); + } + } + } + + void updateViewState() { + if (!service->isEnabled()) { + lv_label_set_text(statusLabel, "Service disabled"); + } else if (!service->isStarted()) { + lv_label_set_text(statusLabel, "Waiting for connection..."); + } else { // enabled and started + auto ip = service::wifi::getIp(); + if (ip.empty()) { + lv_label_set_text(statusLabel, "Waiting for IP..."); + } else { + std::string status = std::string("Available at ") + ip; + lv_label_set_text(statusLabel, status.c_str()); + } + } + } + +public: + + void onCreate(AppContext& appContext) override { + service = service::development::findService(); + if (service == nullptr) { + TT_LOG_E(TAG, "Service not found"); + service::loader::stopApp(); + } + } + + void onShow(AppContext& app, lv_obj_t* parent) override { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + + // Toolbar + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_t* toolbar = lvgl::toolbar_create(parent, app); + + enableSwitch = lvgl::toolbar_add_switch_action(toolbar); + lv_obj_add_event_cb(enableSwitch, onEnableSwitchChanged, LV_EVENT_VALUE_CHANGED, this); + + if (service->isEnabled()) { + lv_obj_add_state(enableSwitch, LV_STATE_CHECKED); + } else { + lv_obj_remove_state(enableSwitch, LV_STATE_CHECKED); + } + + // Wrappers + + lv_obj_t* secondary_flex = lv_obj_create(parent); + lv_obj_set_width(secondary_flex, LV_PCT(100)); + lv_obj_set_flex_grow(secondary_flex, 1); + lv_obj_set_flex_flow(secondary_flex, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_border_width(secondary_flex, 0, 0); + lv_obj_set_style_pad_all(secondary_flex, 0, 0); + lv_obj_set_style_pad_gap(secondary_flex, 0, 0); + lvgl::obj_set_style_bg_invisible(secondary_flex); + + // align() methods don't work on flex, so we need this extra wrapper + lv_obj_t* wrapper = lv_obj_create(secondary_flex); + lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lvgl::obj_set_style_bg_invisible(wrapper); + lv_obj_set_style_border_width(wrapper, 0, 0); + + // Enable on boot + + lv_obj_t* enable_label = lv_label_create(wrapper); + lv_label_set_text(enable_label, "Enable on boot"); + lv_obj_align(enable_label, LV_ALIGN_TOP_LEFT, 0, 6); + + enableOnBootSwitch = lv_switch_create(wrapper); + lv_obj_add_event_cb(enableOnBootSwitch, onEnableOnBootSwitchChanged, LV_EVENT_VALUE_CHANGED, this); + lv_obj_align(enableOnBootSwitch, LV_ALIGN_TOP_RIGHT, 0, 0); + if (service->isEnabledOnStart()) { + lv_obj_add_state(enableOnBootSwitch, LV_STATE_CHECKED); + } else { + lv_obj_remove_state(enableOnBootSwitch, LV_STATE_CHECKED); + } + + statusLabel = lv_label_create(wrapper); + lv_obj_align(statusLabel, LV_ALIGN_TOP_LEFT, 0, 50); + + updateViewState(); + + timer.start(1000); + } + + void onHide(AppContext& appContext) override { + auto lock = lvgl::getSyncLock()->asScopedLock(); + // Ensure that the update isn't already happening + lock.lock(); + timer.stop(); + } +}; + +extern const AppManifest manifest = { + .id = "Development", + .name = "Development", + .type = Type::Settings, + .createApp = create +}; + +void start() { + app::start(manifest.id); +} + +} // namespace + +#endif // ESP_PLATFORM \ No newline at end of file diff --git a/Tactility/Source/lvgl/Statusbar.cpp b/Tactility/Source/lvgl/Statusbar.cpp index b485025f..8bf8d8c2 100644 --- a/Tactility/Source/lvgl/Statusbar.cpp +++ b/Tactility/Source/lvgl/Statusbar.cpp @@ -118,7 +118,7 @@ static void statusbar_pubsub_event(TT_UNUSED const void* message, void* obj) { } } -static void onNetworkConnected(TT_UNUSED kernel::SystemEvent event) { +static void onTimeChanged(TT_UNUSED kernel::SystemEvent event) { if (statusbar_data.mutex.lock(100 / portTICK_PERIOD_MS)) { statusbar_data.time_update_timer->stop(); statusbar_data.time_update_timer->start(5); @@ -139,7 +139,7 @@ static void statusbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) statusbar_data.time_update_timer->start(50 / portTICK_PERIOD_MS); statusbar_data.systemEventSubscription = kernel::subscribeSystemEvent( kernel::SystemEvent::Time, - onNetworkConnected + onTimeChanged ); } } diff --git a/Tactility/Source/network/HttpdReq.cpp b/Tactility/Source/network/HttpdReq.cpp new file mode 100644 index 00000000..8e639909 --- /dev/null +++ b/Tactility/Source/network/HttpdReq.cpp @@ -0,0 +1,160 @@ +#include "Tactility/network/HttpdReq.h" + +#include +#include +#include +#include +#include + +#ifdef ESP_PLATFORM + +#define TAG "network" + +namespace tt::network { + +bool getHeaderOrSendError(httpd_req_t* request, const std::string& name, std::string& value) { + size_t header_size = httpd_req_get_hdr_value_len(request, name.c_str()); + if (header_size == 0) { + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "header missing"); + return false; + } + + auto header_buffer = std::make_unique(header_size + 1); + if (header_buffer == nullptr) { + TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED); + httpd_resp_send_500(request); + return false; + } + + if (httpd_req_get_hdr_value_str(request, name.c_str(), header_buffer.get(), header_size + 1) != ESP_OK) { + httpd_resp_send_500(request); + return false; + } + + value = header_buffer.get(); + return true; +} + +bool getMultiPartBoundaryOrSendError(httpd_req_t* request, std::string& boundary) { + std::string content_type_header; + if (!getHeaderOrSendError(request, "Content-Type", content_type_header)) { + return false; + } + + auto boundary_index = content_type_header.find("boundary="); + if (boundary_index == std::string::npos) { + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "boundary not found in Content-Type"); + return false; + } + + boundary = content_type_header.substr(boundary_index + 9); + return true; +} + +bool getQueryOrSendError(httpd_req_t* request, std::string& query) { + size_t buffer_length = httpd_req_get_url_query_len(request); + if (buffer_length == 0) { + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "id not specified"); + return false; + } + + auto buffer = std::make_unique(buffer_length + 1); + if (buffer.get() == nullptr || httpd_req_get_url_query_str(request, buffer.get(), buffer_length + 1) != ESP_OK) { + httpd_resp_send_500(request); + return false; + } + + query = buffer.get(); + + return true; +} + +std::unique_ptr receiveByteArray(httpd_req_t* request, size_t length, size_t& bytesRead) { + assert(length > 0); + bytesRead = 0; + + auto result = std::make_unique(length); + + while (bytesRead < length) { + size_t read_size = length - bytesRead; + size_t bytes_received = httpd_req_recv(request, result.get() + bytesRead, read_size); + if (bytes_received <= 0) { + TT_LOG_W(TAG, "Received %zu / %zu", bytesRead + bytes_received, length); + return nullptr; + } + + bytesRead += bytes_received; + } + + return result; +} + +std::string receiveTextUntil(httpd_req_t* request, const std::string& terminator) { + size_t read_index = 0; + std::stringstream result; + while (!result.str().ends_with(terminator)) { + char buffer; + size_t bytes_read = httpd_req_recv(request, &buffer, 1); + if (bytes_read <= 0) { + return ""; + } else { + read_index += bytes_read; + } + + result << buffer; + } + + return result.str(); +} + +std::map parseContentDisposition(const std::vector& input) { + std::map result; + static std::string prefix = "Content-Disposition: "; + + // Find header + auto content_disposition_header = std::ranges::find_if(input, [](const std::string& header) { + return header.starts_with(prefix); + }); + + // Header not found + if (content_disposition_header == input.end()) { + return result; + } + + auto parseable = content_disposition_header->substr(prefix.size()); + auto parts = string::split(parseable, "; "); + for (auto part : parts) { + auto key_value = string::split(part, "="); + if (key_value.size() == 2) { + // Trim trailing newlines + auto value = string::trim(key_value[1], "\r\n"); + if (value.size() > 2) { + result[key_value[0]] = value.substr(1, value.size() - 2); + } else { + result[key_value[0]] = ""; + } + } + } + + return result; +} + +bool readAndDiscardOrSendError(httpd_req_t* request, const std::string& toRead) { + size_t bytes_read; + auto buffer = receiveByteArray(request, toRead.length(), bytes_read); + if (bytes_read != toRead.length()) { + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "failed to read discardable data"); + return false; + } + + if (memcmp(buffer.get(), toRead.c_str(), bytes_read) != 0) { + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "discardable data mismatch"); + return false; + } + + return true; +} + +} + +#endif // ESP_PLATFORM \ No newline at end of file diff --git a/Tactility/Source/network/Url.cpp b/Tactility/Source/network/Url.cpp new file mode 100644 index 00000000..7cbdca85 --- /dev/null +++ b/Tactility/Source/network/Url.cpp @@ -0,0 +1,82 @@ +#include "Tactility/network/Url.h" + +#include + +namespace tt::network { + +std::map parseUrlQuery(std::string query) { + std::map result; + + if (query.empty()) { + return result; + } + + size_t current_index = query[0] == '?' ? 1U : 0U; + auto equals_index = query.find_first_of('=', current_index); + while (equals_index != std::string::npos) { + auto index_boundary = query.find_first_of('&', equals_index + 1); + if (index_boundary == std::string::npos) { + index_boundary = query.size(); + } + auto key = query.substr(current_index, (equals_index - current_index)); + auto decodedKey = urlDecode(key); + auto value = query.substr(equals_index + 1, (index_boundary - equals_index - 1)); + auto decodedValue = urlDecode(value); + + result[decodedKey] = decodedValue; + + // Find next token + current_index = index_boundary + 1; + equals_index = query.find_first_of('=', current_index); + } + + return result; +} + +// Adapted from https://stackoverflow.com/a/29962178/3848666 +std::string urlEncode(const std::string& input) { + std::string result = ""; + const char* characters = input.c_str(); + char hex_buffer[10]; + size_t input_length = input.length(); + + for (size_t i = 0;i < input_length;i++) { + unsigned char c = characters[i]; + // uncomment this if you want to encode spaces with + + if (c==' ') { + result += '+'; + } else if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { + result += c; + } else { + sprintf(hex_buffer, "%%%02X", c); //%% means '%' literal, %02X means at least two digits, paddable with a leading zero + result += hex_buffer; + } + } + + return result; +} + +// Adapted from https://stackoverflow.com/a/29962178/3848666 +std::string urlDecode(const std::string& input) { + std::string result; + size_t conversion_buffer, input_length = input.length(); + + for (size_t i = 0; i < input_length; i++) { + if (input[i] != '%') { + if (input[i] == '+') { + result += ' '; + } else { + result += input[i]; + } + } else { + sscanf(input.substr(i + 1, 2).c_str(), "%x", &conversion_buffer); + char c = static_cast(conversion_buffer); + result += c; + i = i + 2; + } + } + + return result; +} + +} // namespace diff --git a/Tactility/Source/service/development/DevelopmentService.cpp b/Tactility/Source/service/development/DevelopmentService.cpp new file mode 100644 index 00000000..27a0f0b4 --- /dev/null +++ b/Tactility/Source/service/development/DevelopmentService.cpp @@ -0,0 +1,301 @@ +#ifdef ESP_PLATFORM + +#include "Tactility/service/development/DevelopmentService.h" + +#include "Tactility/network/HttpdReq.h" +#include "Tactility/network/Url.h" +#include "Tactility/TactilityHeadless.h" +#include "Tactility/service/ServiceManifest.h" +#include "Tactility/service/ServiceRegistry.h" +#include "Tactility/service/wifi/Wifi.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace tt::service::development { + +extern const ServiceManifest manifest; + +constexpr const char* TAG = "DevService"; + +void DevelopmentService::onStart(ServiceContext& service) { + auto lock = mutex.asScopedLock(); + lock.lock(); + + networkConnectEventSubscription = kernel::subscribeSystemEvent( + kernel::SystemEvent::NetworkConnected, + [this](kernel::SystemEvent) { onNetworkConnected(); } + ); + networkConnectEventSubscription = kernel::subscribeSystemEvent( + kernel::SystemEvent::NetworkDisconnected, + [this](kernel::SystemEvent) { onNetworkDisconnected(); } + ); + + setEnabled(isEnabledOnStart()); +} + +void DevelopmentService::onStop(ServiceContext& service) { + auto lock = mutex.asScopedLock(); + lock.lock(); + + kernel::unsubscribeSystemEvent(networkConnectEventSubscription); + kernel::unsubscribeSystemEvent(networkDisconnectEventSubscription); + + if (isEnabled()) { + setEnabled(false); + } +} + +// region Enable/disable + +void DevelopmentService::setEnabled(bool enabled) { + auto lock = mutex.asScopedLock(); + lock.lock(); + this->enabled = enabled; + + // We might already have an IP address, so in case we do, we start the server manually + // Or we started the server while it shouldn't be + if (enabled && !isStarted() && wifi::getRadioState() == wifi::RadioState::ConnectionActive) { + startServer(); + } else if (!enabled && isStarted()) { + stopServer(); + } +} + +bool DevelopmentService::isEnabled() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + return enabled; +} + +bool DevelopmentService::isEnabledOnStart() const { + Preferences preferences = Preferences(manifest.id.c_str()); + bool enabled_on_boot = false; + preferences.optBool("enabledOnBoot", enabled_on_boot); + return enabled_on_boot; +} + +void DevelopmentService::setEnabledOnStart(bool enabled) { + Preferences preferences = Preferences(manifest.id.c_str()); + preferences.putBool("enabledOnBoot", enabled); +} + +// region Enable/disable + +void DevelopmentService::startServer() { + auto lock = mutex.asScopedLock(); + lock.lock(); + + if (isStarted()) { + TT_LOG_W(TAG, "Already started"); + return; + } + + ESP_LOGI(TAG, "Starting server"); + + std::stringstream stream; + stream << "{"; + stream << "\"cpuFamily\":\"" << CONFIG_IDF_TARGET << "\", "; + stream << "\"osVersion\":\"" << TT_VERSION << "\", "; + stream << "\"protocolVersion\":\"1.0.0\""; + stream << "}"; + deviceResponse = stream.str(); + + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + + config.server_port = 6666; + config.uri_match_fn = httpd_uri_match_wildcard; + + if (httpd_start(&server, &config) == ESP_OK) { + httpd_register_uri_handler(server, &handleGetInfoEndpoint); + httpd_register_uri_handler(server, &appRunEndpoint); + httpd_register_uri_handler(server, &appInstallEndpoint); + TT_LOG_I(TAG, "Started on port %d", config.server_port); + } else { + TT_LOG_E(TAG, "Failed to start"); + } +} + +void DevelopmentService::stopServer() { + auto lock = mutex.asScopedLock(); + lock.lock(); + + if (!isStarted()) { + TT_LOG_W(TAG, "Not started"); + return; + } + + TT_LOG_I(TAG, "Stopping server"); + if (httpd_stop(server) != ESP_OK) { + TT_LOG_W(TAG, "Error while stopping"); + } + server = nullptr; +} + +bool DevelopmentService::isStarted() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + return server != nullptr; +} + +void DevelopmentService::onNetworkConnected() { + TT_LOG_I(TAG, "onNetworkConnected"); + mutex.withLock([this] { + if (isEnabled() && !isStarted()) { + startServer(); + } + }); +} + +void DevelopmentService::onNetworkDisconnected() { + TT_LOG_I(TAG, "onNetworkDisconnected"); + mutex.withLock([this] { + if (isStarted()) { + stopServer(); + } + }); +} + +// region endpoints + +esp_err_t DevelopmentService::handleGetInfo(httpd_req_t* request) { + if (httpd_resp_set_type(request, "application/json") != ESP_OK) { + TT_LOG_W(TAG, "Failed to send header"); + return ESP_FAIL; + } + + auto* service = static_cast(request->user_ctx); + + if (httpd_resp_sendstr(request, service->deviceResponse.c_str()) != ESP_OK) { + TT_LOG_W(TAG, "Failed to send response body"); + return ESP_FAIL; + } + + TT_LOG_I(TAG, "[200] /device"); + return ESP_OK; +} + +esp_err_t DevelopmentService::handleAppRun(httpd_req_t* request) { + std::string query; + if (!network::getQueryOrSendError(request, query)) { + return ESP_FAIL; + } + + auto parameters = network::parseUrlQuery(query); + auto id_key_pos = parameters.find("id"); + if (id_key_pos == parameters.end()) { + TT_LOG_W(TAG, "[400] /app/run id not specified"); + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "id not specified"); + return ESP_FAIL; + } + + auto app_id = id_key_pos->second; + if (app_id.ends_with(".app.elf")) { + app::registerElfApp(app_id); + app_id = app::getElfAppId(app_id); + } else if (!app::findAppById(app_id.c_str())) { + TT_LOG_W(TAG, "[400] /app/run app not found"); + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "app not found"); + return ESP_FAIL; + } + + app::start(app_id); + TT_LOG_I(TAG, "[200] /app/run %s", app_id.c_str()); + httpd_resp_send(request, nullptr, 0); + return ESP_OK; +} + +esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) { + std::string boundary; + if (!network::getMultiPartBoundaryOrSendError(request, boundary)) { + return false; + } + + size_t content_left = request->content_len; + + // Skip newline after reading boundary + auto content_headers_data = network::receiveTextUntil(request, "\r\n\r\n"); + content_left -= content_headers_data.length(); + auto content_headers = string::split(content_headers_data, "\r\n") + | std::views::filter([](const std::string& line) { + return line.length() > 0; + }) + | std::ranges::to(); + + auto content_disposition_map = network::parseContentDisposition(content_headers); + if (content_disposition_map.empty()) { + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Multipart form error: invalid content disposition"); + return ESP_FAIL; + } + + auto name_entry = content_disposition_map.find("name"); + auto filename_entry = content_disposition_map.find("filename"); + if ( + name_entry == content_disposition_map.end() || + filename_entry == content_disposition_map.end() || + name_entry->second != "elf" + ) { + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Multipart form error: name or filename parameter missing or mismatching"); + return ESP_FAIL; + } + + // Receive file + size_t content_read; + auto part_after_file = std::format("\r\n--{}--\r\n", boundary); + auto file_size = content_left - part_after_file.length(); + auto buffer = network::receiveByteArray(request, file_size, content_read); + if (content_read != file_size) { + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Multipart form error: file data not received"); + return ESP_FAIL; + } + content_left -= content_read; + + // Write file + auto file_path = std::format("/sdcard/{}", filename_entry->second); + auto* file = fopen(file_path.c_str(), "wb"); + auto file_bytes_written = fwrite(buffer.get(), 1, file_size, file); + fclose(file); + if (file_bytes_written != file_size) { + httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to save file"); + return ESP_FAIL; + } + + // Read and verify part + if (!network::readAndDiscardOrSendError(request, part_after_file)) { + return ESP_FAIL; + } + content_left -= part_after_file.length(); + + if (content_left != 0) { + TT_LOG_W(TAG, "We have more bytes at the end of the request parsing?!"); + } + + TT_LOG_I(TAG, "[200] /app/install -> %s", file_path.c_str()); + + httpd_resp_send(request, nullptr, 0); + return ESP_OK; +} + +// endregion + +std::shared_ptr findService() { + return std::static_pointer_cast( + findServiceById(manifest.id) + ); +} + +extern const ServiceManifest manifest = { + .id = "Development", + .createService = create +}; + +} + +#endif // ESP_PLATFORM diff --git a/Tactility/Source/service/wifi/WifiEsp.cpp b/Tactility/Source/service/wifi/WifiEsp.cpp index 1573b3b0..8fa75838 100644 --- a/Tactility/Source/service/wifi/WifiEsp.cpp +++ b/Tactility/Source/service/wifi/WifiEsp.cpp @@ -1,5 +1,6 @@ #ifdef ESP_PLATFORM +#include #include "Tactility/service/wifi/Wifi.h" #include "Tactility/TactilityHeadless.h" @@ -12,6 +13,7 @@ #include #include +#include #include namespace tt::service::wifi { @@ -71,6 +73,7 @@ public: }; bool pause_auto_connect = false; // Pause when manually disconnecting until manually connecting again bool connection_target_remember = false; // Whether to store the connection_target on successful connection or not + esp_netif_ip_info_t ip_info; RadioState getRadioState() const { auto lock = dataMutex.asScopedLock(); @@ -230,6 +233,19 @@ void disconnect() { getMainDispatcher().dispatch([wifi]() { dispatchDisconnectButKeepActive(wifi); }); } +void clearIp() { + auto wifi = wifi_singleton; + if (wifi == nullptr) { + return; + } + + auto lock = wifi->dataMutex.asScopedLock(); + if (!lock.lock(10 / portTICK_PERIOD_MS)) { + return; + } + + memset(&wifi->ip_info, 0, sizeof(esp_netif_ip_info_t)); +} void setScanRecords(uint16_t records) { TT_LOG_I(TAG, "setScanRecords(%d)", records); auto wifi = wifi_singleton; @@ -463,6 +479,7 @@ static void eventHandler(TT_UNUSED void* arg, esp_event_base_t event_base, int32 } } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { TT_LOG_I(TAG, "eventHandler: disconnected"); + clearIp(); switch (wifi->getRadioState()) { case RadioState::ConnectionPending: wifi->connection_wait_flags.set(WIFI_FAIL_BIT); @@ -476,8 +493,10 @@ static void eventHandler(TT_UNUSED void* arg, esp_event_base_t event_base, int32 } wifi->setRadioState(RadioState::On); publish_event_simple(wifi, EventType::Disconnected); + kernel::publishSystemEvent(kernel::SystemEvent::NetworkDisconnected); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { auto* event = static_cast(event_data); + memcpy(&wifi->ip_info, &event->ip_info, sizeof(esp_netif_ip_info_t)); TT_LOG_I(TAG, "eventHandler: got ip:" IPSTR, IP2STR(&event->ip_info.ip)); if (wifi->getRadioState() == RadioState::ConnectionPending) { wifi->connection_wait_flags.set(WIFI_CONNECTED_BIT); @@ -485,6 +504,7 @@ static void eventHandler(TT_UNUSED void* arg, esp_event_base_t event_base, int32 // TODO: Make thread-safe wifi->pause_auto_connect = false; // Resume auto-connection } + kernel::publishSystemEvent(kernel::SystemEvent::NetworkConnected); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) { auto* event = static_cast(event_data); TT_LOG_I(TAG, "eventHandler: wifi scanning done (scan id %u)", event->scan_id); @@ -874,6 +894,15 @@ void onAutoConnectTimer() { } } +std::string getIp() { + auto wifi = std::static_pointer_cast(wifi_singleton); + + auto lock = wifi->dataMutex.asScopedLock(); + lock.lock(); + + return std::format("{}.{}.{}.{}", IP2STR(&wifi->ip_info.ip)); +} + class WifiService final : public Service { public: diff --git a/Tactility/Source/service/wifi/WifiMock.cpp b/Tactility/Source/service/wifi/WifiMock.cpp index af84a9e2..9828ca92 100644 --- a/Tactility/Source/service/wifi/WifiMock.cpp +++ b/Tactility/Source/service/wifi/WifiMock.cpp @@ -135,6 +135,10 @@ int getRssi() { } } +std::string getIp() { + return "192.168.1.2"; +} + // endregion Public functions class WifiService final : public Service { diff --git a/Tactility/Source/service/wifi/WifiSettingsEsp.cpp b/Tactility/Source/service/wifi/WifiSettingsEsp.cpp index eda470df..51f427dc 100644 --- a/Tactility/Source/service/wifi/WifiSettingsEsp.cpp +++ b/Tactility/Source/service/wifi/WifiSettingsEsp.cpp @@ -36,7 +36,7 @@ bool contains(const char* ssid) { return false; } - bool key_exists = nvs_find_key(handle, ssid, NULL) == ESP_OK; + bool key_exists = nvs_find_key(handle, ssid, nullptr) == ESP_OK; credentials_nvs_close(handle); return key_exists; diff --git a/TactilityC/Source/tt_init.cpp b/TactilityC/Source/tt_init.cpp index 42e72bf1..9c038e7c 100644 --- a/TactilityC/Source/tt_init.cpp +++ b/TactilityC/Source/tt_init.cpp @@ -19,13 +19,76 @@ #include "tt_timer.h" #include "tt_wifi.h" +#include +#include #include +#include +#include #include extern "C" { +// Hidden functions work-around +extern void* _Znwj(uint32_t size); +extern void _ZdlPvj(void* p, uint64_t size); +extern double __adddf3(double a, double b); +extern double __subdf3(double a, double b); +extern double __muldf3 (double a, double b); +extern double __divdf3 (double a, double b); +extern int __nedf2 (double a, double b); + const struct esp_elfsym elf_symbols[] { + // Hidden functions work-around + ESP_ELFSYM_EXPORT(_ZdlPvj), // new? + ESP_ELFSYM_EXPORT(_Znwj), // delete? + ESP_ELFSYM_EXPORT(__adddf3), // Routines for floating point emulation: + ESP_ELFSYM_EXPORT(__subdf3), // See https://gcc.gnu.org/onlinedocs/gccint/Soft-float-library-routines.html + ESP_ELFSYM_EXPORT(__muldf3), + ESP_ELFSYM_EXPORT(__nedf2), + ESP_ELFSYM_EXPORT(__divdf3), + // + ESP_ELFSYM_EXPORT(__assert_func), + // + ESP_ELFSYM_EXPORT(fclose), + ESP_ELFSYM_EXPORT(feof), + ESP_ELFSYM_EXPORT(ferror), + ESP_ELFSYM_EXPORT(fflush), + ESP_ELFSYM_EXPORT(fgetc), + ESP_ELFSYM_EXPORT(fgetpos), + ESP_ELFSYM_EXPORT(fgets), + ESP_ELFSYM_EXPORT(fopen), + ESP_ELFSYM_EXPORT(fputc), + ESP_ELFSYM_EXPORT(fputs), + ESP_ELFSYM_EXPORT(fprintf), + ESP_ELFSYM_EXPORT(fread), + ESP_ELFSYM_EXPORT(fseek), + ESP_ELFSYM_EXPORT(fsetpos), + ESP_ELFSYM_EXPORT(fscanf), + ESP_ELFSYM_EXPORT(ftell), + ESP_ELFSYM_EXPORT(fwrite), + ESP_ELFSYM_EXPORT(getc), + ESP_ELFSYM_EXPORT(putc), + ESP_ELFSYM_EXPORT(puts), + ESP_ELFSYM_EXPORT(printf), + ESP_ELFSYM_EXPORT(sscanf), + ESP_ELFSYM_EXPORT(snprintf), + ESP_ELFSYM_EXPORT(sprintf), + ESP_ELFSYM_EXPORT(vsprintf), + // cstring + ESP_ELFSYM_EXPORT(strlen), + ESP_ELFSYM_EXPORT(strcmp), + ESP_ELFSYM_EXPORT(strncpy), + ESP_ELFSYM_EXPORT(strcpy), + ESP_ELFSYM_EXPORT(strcat), + ESP_ELFSYM_EXPORT(strstr), + ESP_ELFSYM_EXPORT(memset), + ESP_ELFSYM_EXPORT(memcpy), + // ctype + ESP_ELFSYM_EXPORT(isdigit), + // ESP-IDF + ESP_ELFSYM_EXPORT(esp_log_write), + ESP_ELFSYM_EXPORT(esp_log_timestamp), // Tactility ESP_ELFSYM_EXPORT(tt_app_register), ESP_ELFSYM_EXPORT(tt_app_get_parameters), @@ -139,7 +202,10 @@ const struct esp_elfsym elf_symbols[] { ESP_ELFSYM_EXPORT(lv_event_get_user_data), ESP_ELFSYM_EXPORT(lv_event_get_target_obj), ESP_ELFSYM_EXPORT(lv_event_get_target), + ESP_ELFSYM_EXPORT(lv_event_get_current_target_obj), // lv_obj + ESP_ELFSYM_EXPORT(lv_obj_create), + ESP_ELFSYM_EXPORT(lv_obj_delete), ESP_ELFSYM_EXPORT(lv_obj_add_event_cb), ESP_ELFSYM_EXPORT(lv_obj_align), ESP_ELFSYM_EXPORT(lv_obj_align_to), @@ -157,7 +223,13 @@ const struct esp_elfsym elf_symbols[] { ESP_ELFSYM_EXPORT(lv_obj_remove_event_cb), ESP_ELFSYM_EXPORT(lv_obj_get_user_data), ESP_ELFSYM_EXPORT(lv_obj_set_user_data), + ESP_ELFSYM_EXPORT(lv_obj_remove_flag), + ESP_ELFSYM_EXPORT(lv_obj_add_flag), ESP_ELFSYM_EXPORT(lv_obj_set_pos), + ESP_ELFSYM_EXPORT(lv_obj_set_flex_align), + ESP_ELFSYM_EXPORT(lv_obj_set_flex_flow), + ESP_ELFSYM_EXPORT(lv_obj_set_flex_grow), + ESP_ELFSYM_EXPORT(lv_obj_set_style_bg_color), ESP_ELFSYM_EXPORT(lv_obj_set_style_margin_hor), ESP_ELFSYM_EXPORT(lv_obj_set_style_margin_ver), ESP_ELFSYM_EXPORT(lv_obj_set_style_margin_top), @@ -172,19 +244,36 @@ const struct esp_elfsym elf_symbols[] { ESP_ELFSYM_EXPORT(lv_obj_set_style_pad_bottom), ESP_ELFSYM_EXPORT(lv_obj_set_style_pad_left), ESP_ELFSYM_EXPORT(lv_obj_set_style_pad_right), + ESP_ELFSYM_EXPORT(lv_obj_set_style_pad_column), + ESP_ELFSYM_EXPORT(lv_obj_set_style_pad_row), ESP_ELFSYM_EXPORT(lv_obj_set_style_border_width), ESP_ELFSYM_EXPORT(lv_obj_set_style_border_opa), ESP_ELFSYM_EXPORT(lv_obj_set_style_border_post), ESP_ELFSYM_EXPORT(lv_obj_set_style_border_side), ESP_ELFSYM_EXPORT(lv_obj_set_style_border_color), + ESP_ELFSYM_EXPORT(lv_obj_set_align), ESP_ELFSYM_EXPORT(lv_obj_set_x), ESP_ELFSYM_EXPORT(lv_obj_set_y), + ESP_ELFSYM_EXPORT(lv_obj_set_size), ESP_ELFSYM_EXPORT(lv_obj_set_width), ESP_ELFSYM_EXPORT(lv_obj_set_height), ESP_ELFSYM_EXPORT(lv_theme_get_color_primary), ESP_ELFSYM_EXPORT(lv_theme_get_color_secondary), // lv_button ESP_ELFSYM_EXPORT(lv_button_create), + // lv_buttonmatrix + ESP_ELFSYM_EXPORT(lv_buttonmatrix_create), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_get_button_text), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_get_map), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_get_one_checked), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_get_selected_button), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_button_ctrl), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_button_ctrl_all), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_ctrl_map), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_map), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_one_checked), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_button_width), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_selected_button), // lv_label ESP_ELFSYM_EXPORT(lv_label_create), ESP_ELFSYM_EXPORT(lv_label_cut_text), @@ -251,6 +340,18 @@ const struct esp_elfsym elf_symbols[] { ESP_ELFSYM_EXPORT(lv_textarea_set_placeholder_text), ESP_ELFSYM_EXPORT(lv_textarea_set_text), ESP_ELFSYM_EXPORT(lv_textarea_set_text_selection), + // lv_palette + ESP_ELFSYM_EXPORT(lv_palette_main), + ESP_ELFSYM_EXPORT(lv_palette_darken), + ESP_ELFSYM_EXPORT(lv_palette_lighten), + // lv_display + ESP_ELFSYM_EXPORT(lv_display_get_horizontal_resolution), + ESP_ELFSYM_EXPORT(lv_display_get_vertical_resolution), + ESP_ELFSYM_EXPORT(lv_display_get_physical_horizontal_resolution), + ESP_ELFSYM_EXPORT(lv_display_get_physical_vertical_resolution), + // lv_pct + ESP_ELFSYM_EXPORT(lv_pct), + ESP_ELFSYM_EXPORT(lv_pct_to_px), // delimiter ESP_ELFSYM_END }; diff --git a/TactilityCore/Include/Tactility/LogMessages.h b/TactilityCore/Include/Tactility/LogMessages.h index 48178061..e9c5ca10 100644 --- a/TactilityCore/Include/Tactility/LogMessages.h +++ b/TactilityCore/Include/Tactility/LogMessages.h @@ -4,6 +4,9 @@ */ #pragma once +// Alloc +#define LOG_MESSAGE_ALLOC_FAILED "Memory allocation failed" + // Mutex #define LOG_MESSAGE_MUTEX_LOCK_FAILED "Mutex acquisition timeout" #define LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT "Mutex acquisition timeout (%s)" diff --git a/TactilityCore/Include/Tactility/StringUtils.h b/TactilityCore/Include/Tactility/StringUtils.h index 3fefeb38..b92c3196 100644 --- a/TactilityCore/Include/Tactility/StringUtils.h +++ b/TactilityCore/Include/Tactility/StringUtils.h @@ -64,4 +64,12 @@ bool isAsciiHexString(const std::string& input); /** @return the first part of a file name right up (and excluding) the first period character. */ std::string removeFileExtension(const std::string& input); +/** + * Remove the given characters from the start and end of the specified string. + * @param[in] input the text to trim + * @param[in] characters the characters to remove from the input + * @return the input where the specified characters are removed from the start and end of the input string + */ +std::string trim(const std::string& input, const std::string& characters); + } // namespace diff --git a/TactilityCore/Include/Tactility/Timer.h b/TactilityCore/Include/Tactility/Timer.h index 445e23e1..285b5ea3 100644 --- a/TactilityCore/Include/Tactility/Timer.h +++ b/TactilityCore/Include/Tactility/Timer.h @@ -13,7 +13,6 @@ class Timer { public: typedef std::function Callback; -// typedef std::function PendingCallback; typedef void (*PendingCallback)(void* context, uint32_t arg); private: diff --git a/TactilityCore/Source/StringUtils.cpp b/TactilityCore/Source/StringUtils.cpp index 945bef3e..e7f8969f 100644 --- a/TactilityCore/Source/StringUtils.cpp +++ b/TactilityCore/Source/StringUtils.cpp @@ -96,4 +96,14 @@ bool isAsciiHexString(const std::string& input) { }).empty(); } +std::string trim(const std::string& input, const std::string& characters) { + auto index = input.find_first_not_of(characters); + if (index == std::string::npos) { + return ""; + } else { + auto end_index = input.find_last_not_of(characters); + return input.substr(index, end_index - index + 1); + } +} + } // namespace diff --git a/Tests/Tactility/UrlTest.cpp b/Tests/Tactility/UrlTest.cpp new file mode 100644 index 00000000..f2c2fe99 --- /dev/null +++ b/Tests/Tactility/UrlTest.cpp @@ -0,0 +1,61 @@ +#include "doctest.h" +#include + +using namespace tt; + +TEST_CASE("parseUrlQuery can handle a single key-value pair") { + auto map = network::parseUrlQuery("?key=value"); + CHECK_EQ(map.size(), 1); + CHECK_EQ(map["key"], "value"); +} + +TEST_CASE("parseUrlQuery can handle empty value in the middle") { + auto map = network::parseUrlQuery("?a=1&b=&c=3"); + CHECK_EQ(map.size(), 3); + CHECK_EQ(map["a"], "1"); + CHECK_EQ(map["b"], ""); + CHECK_EQ(map["c"], "3"); +} + +TEST_CASE("parseUrlQuery can handle empty value at the end") { + auto map = network::parseUrlQuery("?a=1&b="); + CHECK_EQ(map.size(), 2); + CHECK_EQ(map["a"], "1"); + CHECK_EQ(map["b"], ""); +} + +TEST_CASE("parseUrlQuery returns empty map when query s questionmark with a key without a value") { + auto map = network::parseUrlQuery("?a"); + CHECK_EQ(map.size(), 0); +} + +TEST_CASE("parseUrlQuery returns empty map when query is a questionmark") { + auto map = network::parseUrlQuery("?"); + CHECK_EQ(map.size(), 0); +} + +TEST_CASE("parseUrlQuery should url-decode the value") { + auto map = network::parseUrlQuery("?key=Test%21Test"); + CHECK_EQ(map.size(), 1); + CHECK_EQ(map["key"], "Test!Test"); +} + +TEST_CASE("parseUrlQuery should url-decode the key") { + auto map = network::parseUrlQuery("?Test%21Test=value"); + CHECK_EQ(map.size(), 1); + CHECK_EQ(map["Test!Test"], "value"); +} + +TEST_CASE("urlDecode") { + auto input = std::string("prefix!*'();:@&=+$,/?#[]<>%-.^_`{}|~ \\"); + auto expected = std::string("prefix%21%2A%27%28%29%3B%3A%40%26%3D%2B%24%2C%2F%3F%23%5B%5D%3C%3E%25-.%5E_%60%7B%7D%7C~+%5C"); + auto encoded = network::urlEncode(input); + CHECK_EQ(encoded, expected); +} + +TEST_CASE("urlDecode") { + auto input = std::string("prefix%21%2A%27%28%29%3B%3A%40%26%3D%2B%24%2C%2F%3F%23%5B%5D%3C%3E%25-.%5E_%60%7B%7D%7C~+%5C"); + auto expected = std::string("prefix!*'();:@&=+$,/?#[]<>%-.^_`{}|~ \\"); + auto decoded = network::urlDecode(input); + CHECK_EQ(decoded, expected); +} \ No newline at end of file From 00b62a28318fbc7596f3a2e02b97caa90e4d706b Mon Sep 17 00:00:00 2001 From: flip <110619562+fipfip@users.noreply.github.com> Date: Wed, 23 Jul 2025 20:45:57 +0000 Subject: [PATCH 15/16] Implemented LilyGO T-Lora Pager (#295) --- App/Kconfig | 2 + App/Source/Boards.h | 5 +- App/idf_component.yml | 2 + Boards/LilygoTLoraPager/CMakeLists.txt | 7 + Boards/LilygoTLoraPager/Source/Init.cpp | 55 +++ .../Source/LilygoTloraPager.cpp | 83 ++++ .../Source/LilygoTloraPager.h | 5 + .../Source/hal/TpagerDisplay.cpp | 30 ++ .../Source/hal/TpagerDisplay.h | 40 ++ .../Source/hal/TpagerDisplayConstants.h | 8 + .../Source/hal/TpagerKeyboard.cpp | 359 +++++++++++++++++ .../Source/hal/TpagerKeyboard.h | 54 +++ .../Source/hal/TpagerPower.cpp | 90 +++++ .../LilygoTLoraPager/Source/hal/TpagerPower.h | 26 ++ .../Source/hal/TpagerSdCard.cpp | 31 ++ .../Source/hal/TpagerSdCard.h | 7 + Buildscripts/board.cmake | 2 + COPYRIGHT.md | 6 + Drivers/BQ24295/Source/Bq24295.cpp | 8 +- Drivers/BQ27220/CMakeLists.txt | 5 + Drivers/BQ27220/README.md | 6 + Drivers/BQ27220/Source/Bq27220.cpp | 370 ++++++++++++++++++ Drivers/BQ27220/Source/Bq27220.h | 107 +++++ Drivers/ST7796/CMakeLists.txt | 5 + Drivers/ST7796/README.md | 3 + Drivers/ST7796/Source/St7796Display.cpp | 210 ++++++++++ Drivers/ST7796/Source/St7796Display.h | 103 +++++ Drivers/TCA8418/CMakeLists.txt | 5 + Drivers/TCA8418/COPYRIGHT.md | 18 + Drivers/TCA8418/README.md | 4 + Drivers/TCA8418/Source/Tca8418.cpp | 202 ++++++++++ Drivers/TCA8418/Source/Tca8418.h | 69 ++++ .../Include/Tactility/hal/i2c/I2cDevice.h | 8 +- Tactility/Source/hal/i2c/I2cDevice.cpp | 18 +- Tactility/Source/service/gui/GuiDraw.cpp | 13 +- sdkconfig.board.lilygo-tlora-pager | 56 +++ 36 files changed, 2013 insertions(+), 9 deletions(-) create mode 100644 Boards/LilygoTLoraPager/CMakeLists.txt create mode 100644 Boards/LilygoTLoraPager/Source/Init.cpp create mode 100644 Boards/LilygoTLoraPager/Source/LilygoTloraPager.cpp create mode 100644 Boards/LilygoTLoraPager/Source/LilygoTloraPager.h create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.cpp create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.h create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerDisplayConstants.h create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.cpp create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.h create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerPower.cpp create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerPower.h create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.cpp create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.h create mode 100644 Drivers/BQ27220/CMakeLists.txt create mode 100644 Drivers/BQ27220/README.md create mode 100644 Drivers/BQ27220/Source/Bq27220.cpp create mode 100644 Drivers/BQ27220/Source/Bq27220.h create mode 100644 Drivers/ST7796/CMakeLists.txt create mode 100644 Drivers/ST7796/README.md create mode 100644 Drivers/ST7796/Source/St7796Display.cpp create mode 100644 Drivers/ST7796/Source/St7796Display.h create mode 100644 Drivers/TCA8418/CMakeLists.txt create mode 100644 Drivers/TCA8418/COPYRIGHT.md create mode 100644 Drivers/TCA8418/README.md create mode 100644 Drivers/TCA8418/Source/Tca8418.cpp create mode 100644 Drivers/TCA8418/Source/Tca8418.h create mode 100644 sdkconfig.board.lilygo-tlora-pager diff --git a/App/Kconfig b/App/Kconfig index 302912a1..f6582cd1 100644 --- a/App/Kconfig +++ b/App/Kconfig @@ -37,6 +37,8 @@ menu "Tactility App" bool "Elecrow CrowPanel Basic 5.0" config TT_BOARD_LILYGO_TDECK bool "LilyGo T-Deck" + config TT_BOARD_LILYGO_TLORA_PAGER + bool "LilyGo T-Lora Pager" config TT_BOARD_M5STACK_CORE2 bool "M5Stack Core2" config TT_BOARD_M5STACK_CORES3 diff --git a/App/Source/Boards.h b/App/Source/Boards.h index 982b0cee..650da360 100644 --- a/App/Source/Boards.h +++ b/App/Source/Boards.h @@ -8,6 +8,9 @@ #if defined(CONFIG_TT_BOARD_LILYGO_TDECK) #include "LilygoTdeck.h" #define TT_BOARD_HARDWARE &lilygo_tdeck +#elif defined(CONFIG_TT_BOARD_LILYGO_TLORA_PAGER) +#include "LilygoTloraPager.h" +#define TT_BOARD_HARDWARE &lilygo_tlora_pager #elif defined(CONFIG_TT_BOARD_CYD_2432S024C) #include "CYD2432S024C.h" #define TT_BOARD_HARDWARE &cyd_2432s024c_config @@ -68,4 +71,4 @@ extern tt::hal::Configuration hardware; #define TT_BOARD_HARDWARE &hardware -#endif // ESP_PLATFORM \ No newline at end of file +#endif // ESP_PLATFORM diff --git a/App/idf_component.yml b/App/idf_component.yml index a9f6aa56..4dbbcb52 100644 --- a/App/idf_component.yml +++ b/App/idf_component.yml @@ -12,6 +12,8 @@ dependencies: version: "1.1.1" rules: - if: "target in [esp32s3, esp32p4]" + espressif/esp_lcd_st7796: + version: "1.3.2" espressif/esp_lcd_panel_io_additions: "1.0.1" espressif/esp_tinyusb: version: "1.5.0" diff --git a/Boards/LilygoTLoraPager/CMakeLists.txt b/Boards/LilygoTLoraPager/CMakeLists.txt new file mode 100644 index 00000000..3e3a7c3c --- /dev/null +++ b/Boards/LilygoTLoraPager/CMakeLists.txt @@ -0,0 +1,7 @@ +file(GLOB_RECURSE SOURCE_FILES Source/*.c*) + +idf_component_register( + SRCS ${SOURCE_FILES} + INCLUDE_DIRS "Source" + REQUIRES Tactility esp_lvgl_port esp_lcd ST7796 BQ27220 TCA8418 PwmBacklight driver esp_adc +) diff --git a/Boards/LilygoTLoraPager/Source/Init.cpp b/Boards/LilygoTLoraPager/Source/Init.cpp new file mode 100644 index 00000000..72a70096 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/Init.cpp @@ -0,0 +1,55 @@ +#include "PwmBacklight.h" +#include "Tactility/kernel/SystemEvents.h" +#include "Tactility/service/gps/GpsService.h" + +#include +#include + +#include + +#include +#include + +#define TAG "tpager" + +// Power on +#define TDECK_POWERON_GPIO GPIO_NUM_10 + +std::shared_ptr bq27220; +std::shared_ptr tca8418; + +bool tpagerInit() { + ESP_LOGI(TAG, LOG_MESSAGE_POWER_ON_START); + + /* 32 Khz and higher gives an issue where the screen starts dimming again above 80% brightness + * when moving the brightness slider rapidly from a lower setting to 100%. + * This is not a slider bug (data was debug-traced) */ + if (!driver::pwmbacklight::init(GPIO_NUM_42, 30000)) { + TT_LOG_E(TAG, "Backlight init failed"); + return false; + } + + bq27220 = std::make_shared(I2C_NUM_0); + tt::hal::registerDevice(bq27220); + + tca8418 = std::make_shared(I2C_NUM_0); + tt::hal::registerDevice(tca8418); + + tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) { + bq27220->configureCapacity(1500, 1500); + + auto gps_service = tt::service::gps::findGpsService(); + if (gps_service != nullptr) { + std::vector gps_configurations; + gps_service->getGpsConfigurations(gps_configurations); + if (gps_configurations.empty()) { + if (gps_service->addGpsConfiguration(tt::hal::gps::GpsConfiguration {.uartName = "Grove", .baudRate = 38400, .model = tt::hal::gps::GpsModel::UBLOX10})) { + TT_LOG_I(TAG, "Configured internal GPS"); + } else { + TT_LOG_E(TAG, "Failed to configure internal GPS"); + } + } + } + }); + return true; +} diff --git a/Boards/LilygoTLoraPager/Source/LilygoTloraPager.cpp b/Boards/LilygoTLoraPager/Source/LilygoTloraPager.cpp new file mode 100644 index 00000000..f37e054c --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/LilygoTloraPager.cpp @@ -0,0 +1,83 @@ +#include "Tactility/lvgl/LvglSync.h" +#include "hal/TpagerDisplay.h" +#include "hal/TpagerDisplayConstants.h" +#include "hal/TpagerKeyboard.h" +#include "hal/TpagerPower.h" +#include "hal/TpagerSdCard.h" + +#include + +#define TPAGER_SPI_TRANSFER_SIZE_LIMIT (TPAGER_LCD_HORIZONTAL_RESOLUTION * TPAGER_LCD_SPI_TRANSFER_HEIGHT * (LV_COLOR_DEPTH / 8)) + +bool tpagerInit(); + +using namespace tt::hal; + +extern const Configuration lilygo_tlora_pager = { + .initBoot = tpagerInit, + .createDisplay = createDisplay, + .createKeyboard = createKeyboard, + .sdcard = createTpagerSdCard(), + .power = tpager_get_power, + .i2c = { + i2c::Configuration { + .name = "Shared", + .port = I2C_NUM_0, + .initMode = i2c::InitMode::ByTactility, + .isMutable = true, + .config = (i2c_config_t) { + .mode = I2C_MODE_MASTER, + .sda_io_num = GPIO_NUM_3, + .scl_io_num = GPIO_NUM_2, + .sda_pullup_en = false, + .scl_pullup_en = false, + .master = { + .clk_speed = 100'000 + }, + .clk_flags = 0 + } + } + }, + .spi {spi::Configuration { + .device = SPI2_HOST, + .dma = SPI_DMA_CH_AUTO, + .config = {.mosi_io_num = GPIO_NUM_34, .miso_io_num = GPIO_NUM_33, .sclk_io_num = GPIO_NUM_35, + .quadwp_io_num = GPIO_NUM_NC, // Quad SPI LCD driver is not yet supported + .quadhd_io_num = GPIO_NUM_NC, // Quad SPI LCD driver is not yet supported + .data4_io_num = GPIO_NUM_NC, + .data5_io_num = GPIO_NUM_NC, + .data6_io_num = GPIO_NUM_NC, + .data7_io_num = GPIO_NUM_NC, + .data_io_default_level = false, + .max_transfer_sz = TPAGER_SPI_TRANSFER_SIZE_LIMIT, + .flags = 0, + .isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO, + .intr_flags = 0}, + .initMode = spi::InitMode::ByTactility, + .isMutable = false, + .lock = tt::lvgl::getSyncLock() // esp_lvgl_port owns the lock for the display + }}, + .uart {uart::Configuration { + .name = "Grove", + .port = UART_NUM_1, + .rxPin = GPIO_NUM_4, + .txPin = GPIO_NUM_12, + .rtsPin = GPIO_NUM_NC, + .ctsPin = GPIO_NUM_NC, + .rxBufferSize = 1024, + .txBufferSize = 1024, + .config = { + .baud_rate = 38400, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .rx_flow_ctrl_thresh = 0, + .source_clk = UART_SCLK_DEFAULT, + .flags = { + .allow_pd = 0, + .backup_before_sleep = 0, + } + } + }} +}; diff --git a/Boards/LilygoTLoraPager/Source/LilygoTloraPager.h b/Boards/LilygoTLoraPager/Source/LilygoTloraPager.h new file mode 100644 index 00000000..b3e010fe --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/LilygoTloraPager.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +extern const tt::hal::Configuration lilygo_tlora_pager; diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.cpp b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.cpp new file mode 100644 index 00000000..30dceb9a --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.cpp @@ -0,0 +1,30 @@ +#include "TpagerDisplay.h" +#include "TpagerDisplayConstants.h" + +#include +#include + +#include + +#define TAG "TPAGER_display" + +std::shared_ptr createDisplay() { + auto configuration = std::make_unique( + TPAGER_LCD_SPI_HOST, + TPAGER_LCD_PIN_CS, + TPAGER_LCD_PIN_DC, + 480, // w + 222, // h + nullptr, + true, //swapXY + true, //mirrorX + true, //mirrorY + true, //invertColor + 0, //gapX + 49 //gapY + ); + + configuration->backlightDutyFunction = driver::pwmbacklight::setBacklightDuty; + + return std::make_shared(std::move(configuration)); +} diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.h b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.h new file mode 100644 index 00000000..df09ebc9 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.h @@ -0,0 +1,40 @@ +#pragma once + +#include "Tactility/hal/display/DisplayDevice.h" +#include +#include + +class TpagerDisplay : public tt::hal::display::DisplayDevice { + +private: + + esp_lcd_panel_io_handle_t ioHandle = nullptr; + esp_lcd_panel_handle_t panelHandle = nullptr; + lv_display_t* displayHandle = nullptr; + bool poweredOn = false; + +public: + + std::string getName() const final { return "ST7796"; } + std::string getDescription() const final { return "SPI display"; } + + bool start() override; + + bool stop() override; + + void setPowerOn(bool turnOn) override; + bool isPoweredOn() const override { return poweredOn; }; + bool supportsPowerControl() const override { return true; } + + std::shared_ptr _Nullable createTouch() override; + + void setBacklightDuty(uint8_t backlightDuty) override; + bool supportsBacklightDuty() const override { return true; } + + void setGammaCurve(uint8_t index) override; + uint8_t getGammaCurveCount() const override { return 4; }; + + lv_display_t* _Nullable getLvglDisplay() const override { return displayHandle; } +}; + +std::shared_ptr createDisplay(); diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerDisplayConstants.h b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplayConstants.h new file mode 100644 index 00000000..98847e46 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplayConstants.h @@ -0,0 +1,8 @@ +#pragma once + +#define TPAGER_LCD_SPI_HOST SPI2_HOST +#define TPAGER_LCD_PIN_CS GPIO_NUM_38 +#define TPAGER_LCD_PIN_DC GPIO_NUM_37 // RS +#define TPAGER_LCD_HORIZONTAL_RESOLUTION 222 +#define TPAGER_LCD_VERTICAL_RESOLUTION 480 +#define TPAGER_LCD_SPI_TRANSFER_HEIGHT (TPAGER_LCD_VERTICAL_RESOLUTION / 10) diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.cpp b/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.cpp new file mode 100644 index 00000000..93b31d4f --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.cpp @@ -0,0 +1,359 @@ +#include "TpagerKeyboard.h" +#include +#include + +#include "driver/gpio.h" +#include "freertos/queue.h" + +#include + +#define TAG "tpager_keyboard" + +#define ENCODER_A GPIO_NUM_40 +#define ENCODER_B GPIO_NUM_41 +#define ENCODER_ENTER GPIO_NUM_7 +#define BACKLIGHT GPIO_NUM_46 + +#define KB_ROWS 4 +#define KB_COLS 11 + +// Lowercase Keymap +static constexpr char keymap_lc[KB_ROWS][KB_COLS] = { + {'\0', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'}, + {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '\n', '\0'}, + {'z', 'x', 'c', 'v', 'b', 'n', 'm', '\0', LV_KEY_BACKSPACE, ' ', '\0'}, + {'\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'} +}; + +// Uppercase Keymap +static constexpr char keymap_uc[KB_ROWS][KB_COLS] = { + {'\0', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'}, + {'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', '\n', '\0'}, + {'Z', 'X', 'C', 'V', 'B', 'N', 'M', '\0', LV_KEY_BACKSPACE, ' ', '\0'}, + {'\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'} +}; + +// Symbol Keymap +static constexpr char keymap_sy[KB_ROWS][KB_COLS] = { + {'\0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}, + {'.', '/', '+', '-', '=', ':', '\'', '"', '@', '\t', '\0'}, + {'_', '$', ';', '?', '!', ',', '.', '\0', LV_KEY_BACKSPACE, ' ', '\0'}, + {'\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'} +}; + +static QueueHandle_t keyboardMsg; + +static void keyboard_read_callback(lv_indev_t* indev, lv_indev_data_t* data) { + TpagerKeyboard* kb = (TpagerKeyboard*)lv_indev_get_user_data(indev); + static bool enter_prev = false; + char keypress = 0; + + // Defaults + data->key = 0; + data->state = LV_INDEV_STATE_RELEASED; + + if (xQueueReceive(keyboardMsg, &keypress, pdMS_TO_TICKS(50)) == pdPASS) { + data->key = keypress; + data->state = LV_INDEV_STATE_PRESSED; + } +} + +static void encoder_read_callback(lv_indev_t* indev, lv_indev_data_t* data) { + TpagerKeyboard* kb = (TpagerKeyboard*)lv_indev_get_user_data(indev); + const int enter_filter_threshold = 2; + static int enter_filter = 0; + const int pulses_click = 4; + static int pulses_prev = 0; + bool anyinput = false; + + // Defaults + data->enc_diff = 0; + data->state = LV_INDEV_STATE_RELEASED; + + int pulses = kb->getEncoderPulses(); + int pulse_diff = (pulses - pulses_prev); + if ((pulse_diff > pulses_click) || (pulse_diff < -pulses_click)) { + data->enc_diff = pulse_diff / pulses_click; + pulses_prev = pulses; + anyinput = true; + } + + bool enter = !gpio_get_level(ENCODER_ENTER); + if (enter && (enter_filter < enter_filter_threshold)) { + enter_filter++; + } + if (!enter && (enter_filter > 0)) { + enter_filter--; + } + + if (enter_filter == enter_filter_threshold) { + data->state = LV_INDEV_STATE_PRESSED; + anyinput = true; + } + + if (anyinput) { + kb->makeBacklightImpulse(); + } +} + +void TpagerKeyboard::processKeyboard() { + static bool shift_pressed = false; + static bool sym_pressed = false; + static bool cap_toggle = false; + static bool cap_toggle_armed = true; + bool anykey_pressed = false; + + if (keypad->update()) { + anykey_pressed = (keypad->pressed_key_count > 0); + for (int i = 0; i < keypad->pressed_key_count; i++) { + auto row = keypad->pressed_list[i].row; + auto col = keypad->pressed_list[i].col; + auto hold = keypad->pressed_list[i].hold_time; + + if ((row == 1) && (col == 10)) { + sym_pressed = true; + } + if ((row == 2) && (col == 7)) { + shift_pressed = true; + } + } + + if ((sym_pressed && shift_pressed) && cap_toggle_armed) { + cap_toggle = !cap_toggle; + cap_toggle_armed = false; + } + + for (int i = 0; i < keypad->pressed_key_count; i++) { + auto row = keypad->pressed_list[i].row; + auto col = keypad->pressed_list[i].col; + auto hold = keypad->pressed_list[i].hold_time; + char chr = '\0'; + if (sym_pressed) { + chr = keymap_sy[row][col]; + } else if (shift_pressed || cap_toggle) { + chr = keymap_uc[row][col]; + } else { + chr = keymap_lc[row][col]; + } + + if (chr != '\0') xQueueSend(keyboardMsg, (void*)&chr, portMAX_DELAY); + } + + for (int i = 0; i < keypad->released_key_count; i++) { + auto row = keypad->released_list[i].row; + auto col = keypad->released_list[i].col; + + if ((row == 1) && (col == 10)) { + sym_pressed = false; + } + if ((row == 2) && (col == 7)) { + shift_pressed = false; + } + } + + if ((!sym_pressed && !shift_pressed) && !cap_toggle_armed) { + cap_toggle_armed = true; + } + + if (anykey_pressed) { + makeBacklightImpulse(); + } + } +} + +bool TpagerKeyboard::start(lv_display_t* display) { + backlightOkay = initBacklight(BACKLIGHT, 30000, LEDC_TIMER_0, LEDC_CHANNEL_1); + initEncoder(); + keypad->init(KB_ROWS, KB_COLS); + gpio_input_enable(ENCODER_ENTER); + + assert(inputTimer == nullptr); + inputTimer = std::make_unique(tt::Timer::Type::Periodic, [this] { + processKeyboard(); + }); + + assert(backlightImpulseTimer == nullptr); + backlightImpulseTimer = std::make_unique(tt::Timer::Type::Periodic, [this] { + processBacklightImpuse(); + }); + + kbHandle = lv_indev_create(); + lv_indev_set_type(kbHandle, LV_INDEV_TYPE_KEYPAD); + lv_indev_set_read_cb(kbHandle, &keyboard_read_callback); + lv_indev_set_display(kbHandle, display); + lv_indev_set_user_data(kbHandle, this); + + encHandle = lv_indev_create(); + lv_indev_set_type(encHandle, LV_INDEV_TYPE_ENCODER); + lv_indev_set_read_cb(encHandle, &encoder_read_callback); + lv_indev_set_display(encHandle, display); + lv_indev_set_user_data(encHandle, this); + + inputTimer->start(20 / portTICK_PERIOD_MS); + backlightImpulseTimer->start(50 / portTICK_PERIOD_MS); + + return true; +} + +bool TpagerKeyboard::stop() { + assert(inputTimer); + inputTimer->stop(); + inputTimer = nullptr; + + assert(backlightImpulseTimer); + backlightImpulseTimer->stop(); + backlightImpulseTimer = nullptr; + + lv_indev_delete(kbHandle); + kbHandle = nullptr; + lv_indev_delete(encHandle); + encHandle = nullptr; + return true; +} + +bool TpagerKeyboard::isAttached() const { + return tt::hal::i2c::masterHasDeviceAtAddress(keypad->getPort(), keypad->getAddress(), 100); +} + +void TpagerKeyboard::initEncoder(void) { + const int low_limit = -127; + const int high_limit = 126; + + // Accum. count makes it that over- and underflows are automatically compensated. + // Prerequisite: watchpoints at low and high limit + pcnt_unit_config_t unit_config = { + .low_limit = low_limit, + .high_limit = high_limit, + .flags = {.accum_count = 1}, + }; + + if (pcnt_new_unit(&unit_config, &encPcntUnit) != ESP_OK) { + TT_LOG_E(TAG, "Pulsecounter intialization failed"); + } + + pcnt_glitch_filter_config_t filter_config = { + .max_glitch_ns = 5000, + }; + if (pcnt_unit_set_glitch_filter(encPcntUnit, &filter_config) != ESP_OK) { + TT_LOG_E(TAG, "Pulsecounter glitch filter config failed"); + } + + pcnt_chan_config_t chan_1_config = { + .edge_gpio_num = ENCODER_A, + .level_gpio_num = ENCODER_B, + }; + pcnt_chan_config_t chan_2_config = { + .edge_gpio_num = ENCODER_B, + .level_gpio_num = ENCODER_A, + }; + + pcnt_channel_handle_t pcnt_chan_1 = NULL; + pcnt_channel_handle_t pcnt_chan_2 = NULL; + + if ((pcnt_new_channel(encPcntUnit, &chan_1_config, &pcnt_chan_1) != ESP_OK) || + (pcnt_new_channel(encPcntUnit, &chan_2_config, &pcnt_chan_2) != ESP_OK)) { + TT_LOG_E(TAG, "Pulsecounter channel config failed"); + } + + // second argument is rising edge, third argument is falling edge + if ((pcnt_channel_set_edge_action(pcnt_chan_1, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_INCREASE) != ESP_OK) || + (pcnt_channel_set_edge_action(pcnt_chan_2, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE) != ESP_OK)) { + TT_LOG_E(TAG, "Pulsecounter edge action config failed"); + } + + // second argument is low level, third argument is high level + if ((pcnt_channel_set_level_action(pcnt_chan_1, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE) != ESP_OK) || + (pcnt_channel_set_level_action(pcnt_chan_2, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE) != ESP_OK)) { + TT_LOG_E(TAG, "Pulsecounter level action config failed"); + } + + if ((pcnt_unit_add_watch_point(encPcntUnit, low_limit) != ESP_OK) || + (pcnt_unit_add_watch_point(encPcntUnit, high_limit) != ESP_OK)) { + TT_LOG_E(TAG, "Pulsecounter watch point config failed"); + } + + if (pcnt_unit_enable(encPcntUnit) != ESP_OK) { + TT_LOG_E(TAG, "Pulsecounter could not be enabled"); + } + if (pcnt_unit_clear_count(encPcntUnit) != ESP_OK) { + TT_LOG_E(TAG, "Pulsecounter could not be cleared"); + } + if (pcnt_unit_start(encPcntUnit) != ESP_OK) { + TT_LOG_E(TAG, "Pulsecounter could not be started"); + } +} + +int TpagerKeyboard::getEncoderPulses() { + int pulses = 0; + pcnt_unit_get_count(encPcntUnit, &pulses); + return pulses; +} + + +bool TpagerKeyboard::initBacklight(gpio_num_t pin, uint32_t frequencyHz, ledc_timer_t timer, ledc_channel_t channel) { + backlightPin = pin; + backlightTimer = timer; + backlightChannel = channel; + + ledc_timer_config_t ledc_timer = { + .speed_mode = LEDC_LOW_SPEED_MODE, + .duty_resolution = LEDC_TIMER_8_BIT, + .timer_num = backlightTimer, + .freq_hz = frequencyHz, + .clk_cfg = LEDC_AUTO_CLK, + .deconfigure = false + }; + + if (ledc_timer_config(&ledc_timer) != ESP_OK) { + TT_LOG_E(TAG, "Backlight timer config failed"); + return false; + } + + ledc_channel_config_t ledc_channel = { + .gpio_num = backlightPin, + .speed_mode = LEDC_LOW_SPEED_MODE, + .channel = backlightChannel, + .intr_type = LEDC_INTR_DISABLE, + .timer_sel = backlightTimer, + .duty = 0, + .hpoint = 0, + .sleep_mode = LEDC_SLEEP_MODE_NO_ALIVE_NO_PD, + .flags = { + .output_invert = 0 + } + }; + + if (ledc_channel_config(&ledc_channel) != ESP_OK) { + TT_LOG_E(TAG, "Backlight channel config failed"); + } + + return true; +} + +bool TpagerKeyboard::setBacklightDuty(uint8_t duty) { + if (!backlightOkay) { + TT_LOG_E(TAG, "Backlight not ready"); + return false; + } + return (ledc_set_duty(LEDC_LOW_SPEED_MODE, backlightChannel, duty) == ESP_OK) && + (ledc_update_duty(LEDC_LOW_SPEED_MODE, backlightChannel) == ESP_OK); +} + +void TpagerKeyboard::makeBacklightImpulse() { + backlightImpulseDuty = 255; + setBacklightDuty(backlightImpulseDuty); +} + +void TpagerKeyboard::processBacklightImpuse() { + if (backlightImpulseDuty > 64) { + backlightImpulseDuty--; + setBacklightDuty(backlightImpulseDuty); + } +} + +extern std::shared_ptr tca8418; +std::shared_ptr createKeyboard() { + keyboardMsg = xQueueCreate(20, sizeof(char)); + + return std::make_shared(tca8418); +} diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.h b/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.h new file mode 100644 index 00000000..1aba8c08 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include + + +class TpagerKeyboard : public tt::hal::keyboard::KeyboardDevice { + +private: + + lv_indev_t* _Nullable kbHandle = nullptr; + lv_indev_t* _Nullable encHandle = nullptr; + pcnt_unit_handle_t encPcntUnit = nullptr; + gpio_num_t backlightPin = GPIO_NUM_NC; + ledc_timer_t backlightTimer; + ledc_channel_t backlightChannel; + bool backlightOkay = false; + int backlightImpulseDuty = 0; + + std::shared_ptr keypad; + std::unique_ptr inputTimer; + std::unique_ptr backlightImpulseTimer; + + void initEncoder(void); + bool initBacklight(gpio_num_t pin, uint32_t frequencyHz, ledc_timer_t timer, ledc_channel_t channel); + void processKeyboard(); + void processBacklightImpuse(); + +public: + + TpagerKeyboard(std::shared_ptr tca) : keypad(std::move(tca)) {} + ~TpagerKeyboard() {} + + std::string getName() const final { return "T-Lora Pager Keyboard"; } + std::string getDescription() const final { return "I2C keyboard with encoder"; } + + bool start(lv_display_t* display) override; + bool stop() override; + bool isAttached() const override; + lv_indev_t* _Nullable getLvglIndev() override { return kbHandle; } + + int getEncoderPulses(); + bool setBacklightDuty(uint8_t duty); + void makeBacklightImpulse(); +}; + +std::shared_ptr createKeyboard(); diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerPower.cpp b/Boards/LilygoTLoraPager/Source/hal/TpagerPower.cpp new file mode 100644 index 00000000..d39e3216 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerPower.cpp @@ -0,0 +1,90 @@ +#include "TpagerPower.h" + +#include + +#define TAG "power" + +#define TPAGER_GAUGE_I2C_BUS_HANDLE I2C_NUM_0 + +/* +TpagerPower::TpagerPower() : gauge(TPAGER_GAUGE_I2C_BUS_HANDLE) { + gauge->configureCapacity(1500, 1500); +}*/ + +TpagerPower::~TpagerPower() {} + +bool TpagerPower::supportsMetric(MetricType type) const { + switch (type) { + using enum MetricType; + case IsCharging: + case Current: + case BatteryVoltage: + case ChargeLevel: + return true; + default: + return false; + } + + return false; // Safety guard for when new enum values are introduced +} + +bool TpagerPower::getMetric(MetricType type, MetricData& data) { + /* IsCharging, // bool + Current, // int32_t, mAh - battery current: either during charging (positive value) or discharging (negative value) + BatteryVoltage, // uint32_t, mV + ChargeLevel, // uint8_t [0, 100] +*/ + + uint16_t u16 = 0; + int16_t s16 = 0; + switch (type) { + using enum MetricType; + case IsCharging: + Bq27220::BatteryStatus status; + if (gauge->getBatteryStatus(status)) { + data.valueAsBool = !status.reg.DSG; + return true; + } + return false; + break; + case Current: + if (gauge->getCurrent(s16)) { + data.valueAsInt32 = s16; + return true; + } else { + return false; + } + break; + case BatteryVoltage: + if (gauge->getVoltage(u16)) { + data.valueAsUint32 = u16; + return true; + } else { + return false; + } + break; + case ChargeLevel: + if (gauge->getStateOfCharge(u16)) { + data.valueAsUint8 = u16; + return true; + } else { + return false; + } + break; + default: + return false; + break; + } + + return false; // Safety guard for when new enum values are introduced +} + +static std::shared_ptr power; +extern std::shared_ptr bq27220; + +std::shared_ptr tpager_get_power() { + if (power == nullptr) { + power = std::make_shared(bq27220); + } + return power; +} diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerPower.h b/Boards/LilygoTLoraPager/Source/hal/TpagerPower.h new file mode 100644 index 00000000..a215d3e7 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerPower.h @@ -0,0 +1,26 @@ +#pragma once + +#include "Tactility/hal/power/PowerDevice.h" +#include +#include + +using tt::hal::power::PowerDevice; + +class TpagerPower : public PowerDevice { + std::shared_ptr gauge; + +public: + + TpagerPower(std::shared_ptr bq) : gauge(std::move(bq)) {} + ~TpagerPower(); + + std::string getName() const final { return "T-LoRa Pager Power measument"; } + std::string getDescription() const final { return "Power measurement interface via I2C fuel gauge"; } + + bool supportsMetric(MetricType type) const override; + bool getMetric(MetricType type, MetricData& data) override; + +private: +}; + +std::shared_ptr tpager_get_power(); diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.cpp b/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.cpp new file mode 100644 index 00000000..38340ec2 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.cpp @@ -0,0 +1,31 @@ +#include "TpagerSdCard.h" + +#include +#include + +#include + +using tt::hal::sdcard::SpiSdCardDevice; + +#define TPAGER_SDCARD_PIN_CS GPIO_NUM_21 +#define TPAGER_LCD_PIN_CS GPIO_NUM_38 +#define TPAGER_RADIO_PIN_CS GPIO_NUM_36 + +std::shared_ptr createTpagerSdCard() { + auto* configuration = new SpiSdCardDevice::Config( + TPAGER_SDCARD_PIN_CS, + GPIO_NUM_NC, + GPIO_NUM_NC, + GPIO_NUM_NC, + SdCardDevice::MountBehaviour::AtBoot, + tt::lvgl::getSyncLock(), + {TPAGER_RADIO_PIN_CS, + TPAGER_LCD_PIN_CS} + ); + + auto* sdcard = (SdCardDevice*)new SpiSdCardDevice( + std::unique_ptr(configuration) + ); + + return std::shared_ptr(sdcard); +} diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.h b/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.h new file mode 100644 index 00000000..95da81a6 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.h @@ -0,0 +1,7 @@ +#pragma once + +#include "Tactility/hal/sdcard/SdCardDevice.h" + +using tt::hal::sdcard::SdCardDevice; + +std::shared_ptr createTpagerSdCard(); diff --git a/Buildscripts/board.cmake b/Buildscripts/board.cmake index 29afb107..77018252 100644 --- a/Buildscripts/board.cmake +++ b/Buildscripts/board.cmake @@ -47,6 +47,8 @@ function(INIT_TACTILITY_GLOBALS SDKCONFIG_FILE) set(TACTILITY_BOARD_PROJECT ElecrowCrowpanelBasic50) elseif (board_id STREQUAL "lilygo-tdeck") set(TACTILITY_BOARD_PROJECT LilygoTdeck) + elseif (board_id STREQUAL "lilygo-tlora-pager") + set(TACTILITY_BOARD_PROJECT LilygoTLoraPager) elseif (board_id STREQUAL "m5stack-core2") set(TACTILITY_BOARD_PROJECT M5stackCore2) elseif (board_id STREQUAL "m5stack-cores3") diff --git a/COPYRIGHT.md b/COPYRIGHT.md index 671b0ceb..381a1d5d 100644 --- a/COPYRIGHT.md +++ b/COPYRIGHT.md @@ -53,6 +53,12 @@ Website: https://github.com/meshtastic/firmware License: [GPL v3.0](https://github.com/meshtastic/firmware/blob/master/LICENSE) +### BQ27220 Driver + +Website: https://github.com/Xinyuan-LilyGO/T-Echo/blob/main/LICENSE + +License: [MIT](https://github.com/Xinyuan-LilyGO/T-Echo/blob/main/LICENSE) + ### Other Components See `/components` for the respective projects and their licenses. diff --git a/Drivers/BQ24295/Source/Bq24295.cpp b/Drivers/BQ24295/Source/Bq24295.cpp index 22e94b13..4dfae7bf 100644 --- a/Drivers/BQ24295/Source/Bq24295.cpp +++ b/Drivers/BQ24295/Source/Bq24295.cpp @@ -8,10 +8,10 @@ * https://gitlab.com/hamishcunningham/unphonelibrary/-/blob/main/unPhone.h?ref_type=heads */ namespace registers { - static const uint8_t CHARGE_TERMINATION = 0x05U; // Datasheet page 35: Charge end/timer cntrl - static const uint8_t OPERATION_CONTROL = 0x07U; // Datasheet page 37: Misc operation control - static const uint8_t STATUS = 0x08U; // Datasheet page 38: System status - static const uint8_t VERSION = 0x0AU; // Datasheet page 38: Vendor/part/revision status +static const uint8_t CHARGE_TERMINATION = 0x05U; // Datasheet page 35: Charge end/timer cntrl +static const uint8_t OPERATION_CONTROL = 0x07U; // Datasheet page 37: Misc operation control +static const uint8_t STATUS = 0x08U; // Datasheet page 38: System status +static const uint8_t VERSION = 0x0AU; // Datasheet page 38: Vendor/part/revision status } // namespace registers bool Bq24295::readChargeTermination(uint8_t& out) const { diff --git a/Drivers/BQ27220/CMakeLists.txt b/Drivers/BQ27220/CMakeLists.txt new file mode 100644 index 00000000..8074f3b3 --- /dev/null +++ b/Drivers/BQ27220/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "Source" + INCLUDE_DIRS "Source" + REQUIRES Tactility +) diff --git a/Drivers/BQ27220/README.md b/Drivers/BQ27220/README.md new file mode 100644 index 00000000..6a9240a5 --- /dev/null +++ b/Drivers/BQ27220/README.md @@ -0,0 +1,6 @@ +# BQ27220 + +Power management: Single-Cell CEDV Fuel Gauge + +[Datasheet](https://www.ti.com/lit/gpn/bq27220) +[User Guide](https://www.ti.com/lit/pdf/sluubd4) diff --git a/Drivers/BQ27220/Source/Bq27220.cpp b/Drivers/BQ27220/Source/Bq27220.cpp new file mode 100644 index 00000000..cbf31ebb --- /dev/null +++ b/Drivers/BQ27220/Source/Bq27220.cpp @@ -0,0 +1,370 @@ +#include "Bq27220.h" +#include + +#include "esp_sleep.h" + +#define TAG "bq27220" + +#define ARRAYSIZE(a) (sizeof(a) / sizeof(*(a))) + +static uint8_t highByte(const uint16_t word) { return (word >> 8) & 0xFF; } +static uint8_t lowByte(const uint16_t word) { return word & 0xFF; } +static constexpr void swapEndianess(uint16_t &word) { word = (lowByte(word) << 8) | highByte(word); } + +namespace registers { + static const uint16_t SUBCMD_CTRL_STATUS = 0x0000U; + static const uint16_t SUBCMD_DEVICE_NUMBER = 0x0001U; + static const uint16_t SUBCMD_FW_VERSION = 0x0002U; + static const uint16_t SUBCMD_HW_VERSION = 0x0003U; + static const uint16_t SUBCMD_BOARD_OFFSET = 0x0009U; + static const uint16_t SUBCMD_CC_OFFSET = 0x000AU; + static const uint16_t SUBCMD_CC_OFFSET_SAVE = 0x000BU; + static const uint16_t SUBCMD_OCV_CMD = 0x000CU; + static const uint16_t SUBCMD_BAT_INSERT = 0x000DU; + static const uint16_t SUBCMD_BAT_REMOVE = 0x000EU; + static const uint16_t SUBCMD_SET_SNOOZE = 0x0013U; + static const uint16_t SUBCMD_CLEAR_SNOOZE = 0x0014U; + static const uint16_t SUBCMD_SET_PROFILE_1 = 0x0015U; + static const uint16_t SUBCMD_SET_PROFILE_2 = 0x0016U; + static const uint16_t SUBCMD_SET_PROFILE_3 = 0x0017U; + static const uint16_t SUBCMD_SET_PROFILE_4 = 0x0018U; + static const uint16_t SUBCMD_SET_PROFILE_5 = 0x0019U; + static const uint16_t SUBCMD_SET_PROFILE_6 = 0x001AU; + static const uint16_t SUBCMD_CAL_TOGGLE = 0x002DU; + static const uint16_t SUBCMD_SEALED = 0x0030U; + static const uint16_t SUBCMD_RESET = 0x0041U; + static const uint16_t SUBCMD_EXIT_CAL = 0x0080U; + static const uint16_t SUBCMD_ENTER_CAL = 0x0081U; + static const uint16_t SUBCMD_ENTER_CFG_UPDATE = 0x0090U; + static const uint16_t SUBCMD_EXIT_CFG_UPDATE_REINIT = 0x0091U; + static const uint16_t SUBCMD_EXIT_CFG_UPDATE = 0x0092U; + static const uint16_t SUBCMD_RETURN_TO_ROM = 0x0F00U; + + static const uint8_t CMD_CONTROL = 0x00U; + static const uint8_t CMD_AT_RATE = 0x02U; + static const uint8_t CMD_AT_RATE_TIME_TO_EMPTY = 0x04U; + static const uint8_t CMD_TEMPERATURE = 0x06U; + static const uint8_t CMD_VOLTAGE = 0x08U; + static const uint8_t CMD_BATTERY_STATUS = 0x0AU; + static const uint8_t CMD_CURRENT = 0x0CU; + static const uint8_t CMD_REMAINING_CAPACITY = 0x10U; + static const uint8_t CMD_FULL_CHARGE_CAPACITY = 0x12U; + static const uint8_t CMD_AVG_CURRENT = 0x14U; + static const uint8_t CMD_TIME_TO_EMPTY = 0x16U; + static const uint8_t CMD_TIME_TO_FULL = 0x18U; + static const uint8_t CMD_STANDBY_CURRENT = 0x1AU; + static const uint8_t CMD_STANDBY_TIME_TO_EMPTY = 0x1CU; + static const uint8_t CMD_MAX_LOAD_CURRENT = 0x1EU; + static const uint8_t CMD_MAX_LOAD_TIME_TO_EMPTY = 0x20U; + static const uint8_t CMD_RAW_COULOMB_COUNT = 0x22U; + static const uint8_t CMD_AVG_POWER = 0x24U; + static const uint8_t CMD_INTERNAL_TEMPERATURE = 0x28U; + static const uint8_t CMD_CYCLE_COUNT = 0x2AU; + static const uint8_t CMD_STATE_OF_CHARGE = 0x2CU; + static const uint8_t CMD_STATE_OF_HEALTH = 0x2EU; + static const uint8_t CMD_CHARGE_VOLTAGE = 0x30U; + static const uint8_t CMD_CHARGE_CURRENT = 0x32U; + static const uint8_t CMD_BTP_DISCHARGE_SET = 0x34U; + static const uint8_t CMD_BTP_CHARGE_SET = 0x36U; + static const uint8_t CMD_OPERATION_STATUS = 0x3AU; + static const uint8_t CMD_DESIGN_CAPACITY = 0x3CU; + static const uint8_t CMD_SELECT_SUBCLASS = 0x3EU; + static const uint8_t CMD_MAC_DATA = 0x40U; + static const uint8_t CMD_MAC_DATA_SUM = 0x60U; + static const uint8_t CMD_MAC_DATA_LEN = 0x61U; + static const uint8_t CMD_ANALOG_COUNT = 0x79U; + static const uint8_t CMD_RAW_CURRENT = 0x7AU; + static const uint8_t CMD_RAW_VOLTAGE = 0x7CU; + static const uint8_t CMD_RAW_INTERNAL_TEMPERATURE = 0x7EU; + static const uint8_t MAC_BUFFER_START = 0x40U; + static const uint8_t MAC_BUFFER_END = 0x5FU; + static const uint8_t MAC_DATA_SUM = 0x60U; + static const uint8_t MAC_DATA_LEN = 0x61U; + static const uint8_t ROM_START = 0x3EU; + + static const uint16_t ROM_FULL_CHARGE_CAPACITY = 0x929DU; + static const uint16_t ROM_DESIGN_CAPACITY = 0x929FU; + static const uint16_t ROM_OPERATION_CONFIG_A = 0x9206U; + static const uint16_t ROM_OPERATION_CONFIG_B = 0x9208U; + +} // namespace registers + +bool Bq27220::configureCapacity(uint16_t designCapacity, uint16_t fullChargeCapacity) { + return performConfigUpdate([this, designCapacity, fullChargeCapacity]() { + // Set the design capacity + if (!writeConfig16(registers::ROM_DESIGN_CAPACITY, designCapacity)) { + TT_LOG_E(TAG, "Failed to set design capacity!"); + return false; + } + vTaskDelay(10 / portTICK_PERIOD_MS); + + // Set full charge capacity + if (!writeConfig16(registers::ROM_FULL_CHARGE_CAPACITY, fullChargeCapacity)) { + TT_LOG_E(TAG, "Failed to set full charge capacity!"); + return false; + } + vTaskDelay(10 / portTICK_PERIOD_MS); + + return true; + }); +} + +bool Bq27220::getVoltage(uint16_t &value) { + if (readRegister16(registers::CMD_VOLTAGE, value)) { + swapEndianess(value); + return true; + } + return false; +} + +bool Bq27220::getCurrent(int16_t &value) { + uint16_t u16 = 0; + if (readRegister16(registers::CMD_CURRENT, u16)) { + swapEndianess(u16); + value = (int16_t)u16; + return true; + } + return false; +} + +bool Bq27220::getBatteryStatus(Bq27220::BatteryStatus &batt_sta) { + if (readRegister16(registers::CMD_BATTERY_STATUS, batt_sta.full)) { + swapEndianess(batt_sta.full); + return true; + } + return false; +} + +bool Bq27220::getOperationStatus(OperationStatus &oper_sta) { + if (readRegister16(registers::CMD_OPERATION_STATUS, oper_sta.full)) { + swapEndianess(oper_sta.full); + return true; + } + return false; +} + +bool Bq27220::getTemperature(uint16_t &value) { + if (readRegister16(registers::CMD_TEMPERATURE, value)) { + swapEndianess(value); + return true; + } + return false; +} + +bool Bq27220::getFullChargeCapacity(uint16_t &value) { + if (readRegister16(registers::CMD_FULL_CHARGE_CAPACITY, value)) { + swapEndianess(value); + return true; + } + return false; +} + +bool Bq27220::getDesignCapacity(uint16_t &value) { + if (readRegister16(registers::CMD_DESIGN_CAPACITY, value)) { + swapEndianess(value); + return true; + } + return false; +} + +bool Bq27220::getRemainingCapacity(uint16_t &value) { + if (readRegister16(registers::CMD_REMAINING_CAPACITY, value)) { + swapEndianess(value); + return true; + } + return false; +} + +bool Bq27220::getStateOfCharge(uint16_t &value) { + if (readRegister16(registers::CMD_STATE_OF_CHARGE, value)) { + swapEndianess(value); + return true; + } + return false; +} + +bool Bq27220::getStateOfHealth(uint16_t &value) { + if (readRegister16(registers::CMD_STATE_OF_HEALTH, value)) { + swapEndianess(value); + return true; + } + return false; +} + +bool Bq27220::getChargeVoltageMax(uint16_t &value) { + if (readRegister16(registers::CMD_CHARGE_VOLTAGE, value)) { + swapEndianess(value); + return true; + } + return false; +} + +bool Bq27220::unsealDevice() { + uint8_t cmd1[] = {0x00, 0x14, 0x04}; + if (!write(cmd1, ARRAYSIZE(cmd1))) { + return false; + } + vTaskDelay(50 / portTICK_PERIOD_MS); + uint8_t cmd2[] = {0x00, 0x72, 0x36}; + if (!write(cmd2, ARRAYSIZE(cmd2))) { + return false; + } + vTaskDelay(50 / portTICK_PERIOD_MS); + return true; +} + +bool Bq27220::unsealFullAccess() +{ + uint8_t buffer[3]; + buffer[0] = 0x00; + buffer[1] = lowByte((accessKey >> 24)); + buffer[2] = lowByte((accessKey >> 16)); + if (!write(buffer, ARRAYSIZE(buffer))) { + return false; + } + vTaskDelay(50 / portTICK_PERIOD_MS); + buffer[1] = lowByte((accessKey >> 8)); + buffer[2] = lowByte((accessKey)); + if (!write(buffer, ARRAYSIZE(buffer))) { + return false; + } + vTaskDelay(50 / portTICK_PERIOD_MS); + return true; +} + +bool Bq27220::exitSealMode() { + return sendSubCommand(registers::SUBCMD_SEALED); +} + +bool Bq27220::sendSubCommand(uint16_t subCmd, bool waitConfirm) +{ + uint8_t buffer[3]; + buffer[0] = 0x00; + buffer[1] = lowByte(subCmd); + buffer[2] = highByte(subCmd); + if (!write(buffer, ARRAYSIZE(buffer))) { + return false; + } + if (!waitConfirm) { + vTaskDelay(10 / portTICK_PERIOD_MS); + return true; + } + constexpr uint8_t statusReg = 0x00; + int waitCount = 20; + vTaskDelay(10 / portTICK_PERIOD_MS); + while (waitCount--) { + writeRead(&statusReg, 1, buffer, 2); + uint16_t *value = reinterpret_cast(buffer); + if (*value == 0xFFA5) { + return true; + } + vTaskDelay(100 / portTICK_PERIOD_MS); + } + TT_LOG_E(TAG, "Subcommand x%X failed!", subCmd); + return false; +} + +bool Bq27220::writeConfig16(uint16_t address, uint16_t value) { + constexpr uint8_t fixedDataLength = 0x06; + const uint8_t msbAccessValue = highByte(address); + const uint8_t lsbAccessValue = lowByte(address); + + // Write to access the MSB of Capacity + writeRegister8(registers::ROM_START, msbAccessValue); + + // Write to access the LSB of Capacity + writeRegister8(registers::ROM_START + 1, lsbAccessValue); + + // Write two Capacity bytes starting from 0x40 + uint8_t valueMsb = highByte(value); + uint8_t valueLsb = lowByte(value); + uint8_t configRaw[] = {valueMsb, valueLsb}; + writeRegister(registers::MAC_BUFFER_START, configRaw, 2); + // Calculate new checksum + uint8_t checksum = 0xFF - ((msbAccessValue + lsbAccessValue + valueMsb + valueLsb) & 0xFF); + + // Write new checksum (0x60) + writeRegister8(registers::MAC_DATA_SUM, checksum); + + // Write the block length + writeRegister8(registers::MAC_DATA_LEN, fixedDataLength); + + return true; +} + +bool Bq27220::configPreamble(bool &isSealed) { + int timeout = 0; + OperationStatus status; + + // Check access settings + if(!getOperationStatus(status)) { + TT_LOG_E(TAG, "Cannot read initial operation status!"); + return false; + } + + if (status.reg.SEC == OperationStatusSecSealed) { + isSealed = true; + if (!unsealDevice()) { + TT_LOG_E(TAG, "Unsealing device failure!"); + return false; + } + } + + if (status.reg.SEC != OperationStatusSecFull) { + if (!unsealFullAccess()) { + TT_LOG_E(TAG, "Unsealing full access failure!"); + return false; + } + } + + // Send ENTER_CFG_UPDATE command (0x0090) + if (!sendSubCommand(registers::SUBCMD_ENTER_CFG_UPDATE)) { + TT_LOG_E(TAG, "Config Update Subcommand failure!"); + } + + // Confirm CFUPDATE mode by polling the OperationStatus() register until Bit 2 is set. + bool isConfigUpdate = false; + for (timeout = 30; timeout; --timeout) { + getOperationStatus(status); + if (status.reg.CFGUPDATE) { + isConfigUpdate = true; + break; + } + vTaskDelay(100 / portTICK_PERIOD_MS); + } + if (!isConfigUpdate) { + TT_LOG_E(TAG, "Update Mode timeout, maybe the access key for full permissions is invalid!"); + return false; + } + + return true; +} + +bool Bq27220::configEpilouge(const bool isSealed) { + int timeout = 0; + OperationStatus status; + + // Exit CFUPDATE mode by sending the EXIT_CFG_UPDATE_REINIT (0x0091) or EXIT_CFG_UPDATE (0x0092) command + sendSubCommand(registers::SUBCMD_EXIT_CFG_UPDATE_REINIT); + vTaskDelay(10 / portTICK_PERIOD_MS); + + // Confirm that CFUPDATE mode has been exited by polling the OperationStatus() register until bit 2 is cleared + for (timeout = 60; timeout; --timeout) { + getOperationStatus(status); + if (!status.reg.CFGUPDATE) { + break; + } + vTaskDelay(100 / portTICK_PERIOD_MS); + } + if (timeout == 0) { + TT_LOG_E(TAG, "Timed out waiting to exit update mode."); + return false; + } + + // If the device was previously in SEALED state, return to SEALED mode by sending the Control(0x0030) subcommand + if (isSealed) { + TT_LOG_D(TAG, "Restore Safe Mode!"); + exitSealMode(); + } + return true; +} diff --git a/Drivers/BQ27220/Source/Bq27220.h b/Drivers/BQ27220/Source/Bq27220.h new file mode 100644 index 00000000..be96f551 --- /dev/null +++ b/Drivers/BQ27220/Source/Bq27220.h @@ -0,0 +1,107 @@ +#pragma once + +#include + +#define BQ27220_ADDRESS 0x55 + +class Bq27220 final : public tt::hal::i2c::I2cDevice { + +private: + uint32_t accessKey; + + bool unsealDevice(); + bool unsealFullAccess(); + bool exitSealMode(); + bool sendSubCommand(uint16_t subCmd, bool waitConfirm = false); + bool writeConfig16(uint16_t address, uint16_t value); + bool configPreamble(bool &isSealed); + bool configEpilouge(const bool isSealed); + + template + bool performConfigUpdate(T configUpdateFunc) + { + bool isSealed = false; + + if (!configPreamble(isSealed)) { + return false; + } + bool result = configUpdateFunc(); + configEpilouge(isSealed); + + return result; + } + +public: + // Register structures lifted from + // https://github.com/Xinyuan-LilyGO/T-Deck-Pro/blob/master/lib/BQ27220/bq27220.h + // Copyright (c) 2025 Liygo / Shenzhen Xinyuan Electronic Technology Co., Ltd + + union BatteryStatus { + struct + { + // Low byte, Low bit first + uint16_t DSG : 1; /**< The device is in DISCHARGE */ + uint16_t SYSDWN : 1; /**< System down bit indicating the system should shut down */ + uint16_t TDA : 1; /**< Terminate Discharge Alarm */ + uint16_t BATTPRES : 1; /**< Battery Present detected */ + uint16_t AUTH_GD : 1; /**< Detect inserted battery */ + uint16_t OCVGD : 1; /**< Good OCV measurement taken */ + uint16_t TCA : 1; /**< Terminate Charge Alarm */ + uint16_t RSVD : 1; /**< Reserved */ + // High byte, Low bit first + uint16_t CHGING : 1; /**< Charge inhibit */ + uint16_t FC : 1; /**< Full-charged is detected */ + uint16_t OTD : 1; /**< Overtemperature in discharge condition is detected */ + uint16_t OTC : 1; /**< Overtemperature in charge condition is detected */ + uint16_t SLEEP : 1; /**< Device is operating in SLEEP mode when set */ + uint16_t OCVFALL : 1; /**< Status bit indicating that the OCV reading failed due to current */ + uint16_t OCVCOMP : 1; /**< An OCV measurement update is complete */ + uint16_t FD : 1; /**< Full-discharge is detected */ + } reg; + uint16_t full; + }; + + enum OperationStatusSec { + OperationStatusSecSealed = 0b11, + OperationStatusSecUnsealed = 0b10, + OperationStatusSecFull = 0b01, + }; + + union OperationStatus { + struct + { + // Low byte, Low bit first + bool CALMD : 1; /**< Calibration mode enabled */ + uint8_t SEC : 2; /**< Current security access */ + bool EDV2 : 1; /**< EDV2 threshold exceeded */ + bool VDQ : 1; /**< Indicates if Current discharge cycle is NOT qualified or qualified for an FCC updated */ + bool INITCOMP : 1; /**< gauge initialization is complete */ + bool SMTH : 1; /**< RemainingCapacity is scaled by smooth engine */ + bool BTPINT : 1; /**< BTP threshold has been crossed */ + // High byte, Low bit first + uint8_t RSVD1 : 2; /**< Reserved */ + bool CFGUPDATE : 1; /**< Gauge is in CONFIG UPDATE mode */ + uint8_t RSVD0 : 5; /**< Reserved */ + } reg; + uint16_t full; + }; + + std::string getName() const final { return "BQ27220"; } + + std::string getDescription() const final { return "I2C-controlled CEDV battery fuel gauge"; } + + explicit Bq27220(i2c_port_t port) : I2cDevice(port, BQ27220_ADDRESS), accessKey(0xFFFFFFFF) {} + + bool configureCapacity(uint16_t designCapacity, uint16_t fullChargeCapacity); + bool getVoltage(uint16_t &value); + bool getCurrent(int16_t &value); + bool getBatteryStatus(BatteryStatus &batt_sta); + bool getOperationStatus(OperationStatus &oper_sta); + bool getTemperature(uint16_t &value); + bool getFullChargeCapacity(uint16_t &value); + bool getDesignCapacity(uint16_t &value); + bool getRemainingCapacity(uint16_t &value); + bool getStateOfCharge(uint16_t &value); + bool getStateOfHealth(uint16_t &value); + bool getChargeVoltageMax(uint16_t &value); +}; diff --git a/Drivers/ST7796/CMakeLists.txt b/Drivers/ST7796/CMakeLists.txt new file mode 100644 index 00000000..983cc248 --- /dev/null +++ b/Drivers/ST7796/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "Source" + INCLUDE_DIRS "Source" + REQUIRES Tactility esp_lvgl_port esp_lcd_st7796 driver +) diff --git a/Drivers/ST7796/README.md b/Drivers/ST7796/README.md new file mode 100644 index 00000000..eae8f783 --- /dev/null +++ b/Drivers/ST7796/README.md @@ -0,0 +1,3 @@ +# ST7796 + +A basic ESP32 LVGL driver for ST7796 displays. diff --git a/Drivers/ST7796/Source/St7796Display.cpp b/Drivers/ST7796/Source/St7796Display.cpp new file mode 100644 index 00000000..22ae268f --- /dev/null +++ b/Drivers/ST7796/Source/St7796Display.cpp @@ -0,0 +1,210 @@ +#include "St7796Display.h" + +#include + +#include +#include +#include +#include + +#define TAG "st7796" + +bool St7796Display::start() { + TT_LOG_I(TAG, "Starting"); + + const esp_lcd_panel_io_spi_config_t panel_io_config = { + .cs_gpio_num = configuration->csPin, + .dc_gpio_num = configuration->dcPin, + .spi_mode = 0, + .pclk_hz = configuration->pixelClockFrequency, + .trans_queue_depth = configuration->transactionQueueDepth, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .cs_ena_pretrans = 0, + .cs_ena_posttrans = 0, + .flags = { + .dc_high_on_cmd = 0, + .dc_low_on_data = 0, + .dc_low_on_param = 0, + .octal_mode = 0, + .quad_mode = 0, + .sio_mode = 0, + .lsb_first = 0, + .cs_high_active = 0 + } + }; + + if (esp_lcd_new_panel_io_spi(configuration->spiBusHandle, &panel_io_config, &ioHandle) != ESP_OK) { + TT_LOG_E(TAG, "Failed to create panel"); + return false; + } + + static const st7796_lcd_init_cmd_t lcd_init_cmds[] = { + {0x01, (uint8_t[]) {0x00}, 0, 120}, + {0x11, (uint8_t[]) {0x00}, 0, 120}, + {0xF0, (uint8_t[]) {0xC3}, 1, 0}, + {0xF0, (uint8_t[]) {0xC3}, 1, 0}, + {0xF0, (uint8_t[]) {0x96}, 1, 0}, + {0x36, (uint8_t[]) {0x58}, 1, 0}, + {0x3A, (uint8_t[]) {0x55}, 1, 0}, + {0xB4, (uint8_t[]) {0x01}, 1, 0}, + {0xB6, (uint8_t[]) {0x80, 0x02, 0x3B}, 3, 0}, + {0xE8, (uint8_t[]) {0x40, 0x8A, 0x00, 0x00, 0x29, 0x19, 0xA5, 0x33}, 8, 0}, + {0xC1, (uint8_t[]) {0x06}, 1, 0}, + {0xC2, (uint8_t[]) {0xA7}, 1, 0}, + {0xC5, (uint8_t[]) {0x18}, 1, 0}, + {0xE0, (uint8_t[]) {0xF0, 0x09, 0x0b, 0x06, 0x04, 0x15, 0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14, 0x18, 0x1B}, 15, 0}, + {0xE1, (uint8_t[]) {0xE0, 0x09, 0x0b, 0x06, 0x04, 0x03, 0x2B, 0x43, 0x42, 0x3B, 0x16, 0x14, 0x17, 0x1B}, 15, 120}, + {0xF0, (uint8_t[]) {0x3C}, 1, 0}, + {0xF0, (uint8_t[]) {0x69}, 1, 0}, + {0x21, (uint8_t[]) {0x00}, 1, 0}, + {0x29, (uint8_t[]) {0x00}, 1, 0}, + }; + + st7796_vendor_config_t vendor_config = { + // Uncomment these lines if use custom initialization commands + .init_cmds = lcd_init_cmds, + .init_cmds_size = sizeof(lcd_init_cmds) / sizeof(st7796_lcd_init_cmd_t), + }; + + + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = configuration->resetPin, // Set to -1 if not use +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + .color_space = ESP_LCD_COLOR_SPACE_RGB, +#else + .color_space = LCD_RGB_ELEMENT_ORDER_RGB, + .data_endian = LCD_RGB_DATA_ENDIAN_LITTLE, +#endif + .bits_per_pixel = 16, + .vendor_config = &vendor_config + }; + /* + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = configuration->resetPin, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, + .data_endian = LCD_RGB_DATA_ENDIAN_LITTLE, + .bits_per_pixel = 16, + .flags = { + .reset_active_high = false + }, + .vendor_config = nullptr + }; +*/ + if (esp_lcd_new_panel_st7796(ioHandle, &panel_config, &panelHandle) != ESP_OK) { + TT_LOG_E(TAG, "Failed to create panel"); + return false; + } + + if (esp_lcd_panel_reset(panelHandle) != ESP_OK) { + TT_LOG_E(TAG, "Failed to reset panel"); + return false; + } + + if (esp_lcd_panel_init(panelHandle) != ESP_OK) { + TT_LOG_E(TAG, "Failed to init panel"); + return false; + } + + if (esp_lcd_panel_invert_color(panelHandle, configuration->invertColor) != ESP_OK) { + TT_LOG_E(TAG, "Failed to set panel to invert"); + return false; + } + + if (esp_lcd_panel_swap_xy(panelHandle, configuration->swapXY) != ESP_OK) { + TT_LOG_E(TAG, "Failed to swap XY "); + return false; + } + + if (esp_lcd_panel_mirror(panelHandle, configuration->mirrorX, configuration->mirrorY) != ESP_OK) { + TT_LOG_E(TAG, "Failed to set panel to mirror"); + return false; + } + + if (esp_lcd_panel_set_gap(panelHandle, configuration->gapX, configuration->gapY) != ESP_OK) { + TT_LOG_E(TAG, "Failed to set panel gap"); + return false; + } + + if (esp_lcd_panel_disp_on_off(panelHandle, true) != ESP_OK) { + TT_LOG_E(TAG, "Failed to turn display on"); + return false; + } + + uint32_t buffer_size; + if (configuration->bufferSize == 0) { + buffer_size = configuration->horizontalResolution * configuration->verticalResolution / 10; + } else { + buffer_size = configuration->bufferSize; + } + + const lvgl_port_display_cfg_t disp_cfg = { + .io_handle = ioHandle, + .panel_handle = panelHandle, + .control_handle = nullptr, + .buffer_size = buffer_size, + .double_buffer = false, + .trans_size = 0, + .hres = configuration->horizontalResolution, + .vres = configuration->verticalResolution, + .monochrome = false, + .rotation = { + .swap_xy = configuration->swapXY, + .mirror_x = configuration->mirrorX, + .mirror_y = configuration->mirrorY, + }, + .color_format = LV_COLOR_FORMAT_NATIVE, + .flags = {.buff_dma = true, .buff_spiram = false, .sw_rotate = false, .swap_bytes = true, .full_refresh = false, .direct_mode = false} + }; + + displayHandle = lvgl_port_add_disp(&disp_cfg); + + TT_LOG_I(TAG, "Finished"); + return displayHandle != nullptr; +} + +bool St7796Display::stop() { + assert(displayHandle != nullptr); + + lvgl_port_remove_disp(displayHandle); + + if (esp_lcd_panel_del(panelHandle) != ESP_OK) { + return false; + } + + if (esp_lcd_panel_io_del(ioHandle) != ESP_OK) { + return false; + } + + displayHandle = nullptr; + return true; +} + +void St7796Display::setGammaCurve(uint8_t index) { + uint8_t gamma_curve; + switch (index) { + case 0: + gamma_curve = 0x01; + break; + case 1: + gamma_curve = 0x04; + break; + case 2: + gamma_curve = 0x02; + break; + case 3: + gamma_curve = 0x08; + break; + default: + return; + } + const uint8_t param[] = { + gamma_curve + }; + + /*if (esp_lcd_panel_io_tx_param(ioHandle , LCD_CMD_GAMSET, param, 1) != ESP_OK) { + TT_LOG_E(TAG, "Failed to set gamma"); + }*/ +} diff --git a/Drivers/ST7796/Source/St7796Display.h b/Drivers/ST7796/Source/St7796Display.h new file mode 100644 index 00000000..3b7bf0a8 --- /dev/null +++ b/Drivers/ST7796/Source/St7796Display.h @@ -0,0 +1,103 @@ +#pragma once + +#include "Tactility/hal/display/DisplayDevice.h" + +#include +#include +#include +#include +#include +#include + +class St7796Display final : public tt::hal::display::DisplayDevice { + +public: + + class Configuration { + + public: + + Configuration( + esp_lcd_spi_bus_handle_t spi_bus_handle, + gpio_num_t csPin, + gpio_num_t dcPin, + unsigned int horizontalResolution, + unsigned int verticalResolution, + std::shared_ptr touch, + bool swapXY = false, + bool mirrorX = false, + bool mirrorY = false, + bool invertColor = false, + unsigned int gapX = 0, + unsigned int gapY = 0, + uint32_t bufferSize = 0 // Size in pixel count. 0 means default, which is 1/10 of the screen size + ) : spiBusHandle(spi_bus_handle), + csPin(csPin), + dcPin(dcPin), + horizontalResolution(horizontalResolution), + verticalResolution(verticalResolution), + swapXY(swapXY), + mirrorX(mirrorX), + mirrorY(mirrorY), + invertColor(invertColor), + gapX(gapX), + gapY(gapY), + bufferSize(bufferSize), + touch(std::move(touch)) {} + + esp_lcd_spi_bus_handle_t spiBusHandle; + gpio_num_t csPin; + gpio_num_t dcPin; + gpio_num_t resetPin = GPIO_NUM_NC; + unsigned int pixelClockFrequency = 80'000'000; // Hertz + size_t transactionQueueDepth = 2; + unsigned int horizontalResolution; + unsigned int verticalResolution; + bool swapXY = false; + bool mirrorX = false; + bool mirrorY = false; + bool invertColor = false; + unsigned int gapX = 0; + unsigned int gapY = 0; + uint32_t bufferSize = 0; // Size in pixel count. 0 means default, which is 1/10 of the screen size + std::shared_ptr touch; + std::function _Nullable backlightDutyFunction = nullptr; + }; + +private: + + std::unique_ptr configuration; + esp_lcd_panel_io_handle_t ioHandle = nullptr; + esp_lcd_panel_handle_t panelHandle = nullptr; + lv_display_t* displayHandle = nullptr; + +public: + + explicit St7796Display(std::unique_ptr inConfiguration) : configuration(std::move(inConfiguration)) { + assert(configuration != nullptr); + } + + std::string getName() const final { return "ST7796"; } + std::string getDescription() const final { return "ST7796 display"; } + + bool start() final; + + bool stop() final; + + std::shared_ptr _Nullable createTouch() final { return configuration->touch; } + + void setBacklightDuty(uint8_t backlightDuty) final { + if (configuration->backlightDutyFunction != nullptr) { + configuration->backlightDutyFunction(backlightDuty); + } + } + + void setGammaCurve(uint8_t index) final; + uint8_t getGammaCurveCount() const final { return 4; }; + + bool supportsBacklightDuty() const final { return configuration->backlightDutyFunction != nullptr; } + + lv_display_t* _Nullable getLvglDisplay() const final { return displayHandle; } +}; + +std::shared_ptr createDisplay(); diff --git a/Drivers/TCA8418/CMakeLists.txt b/Drivers/TCA8418/CMakeLists.txt new file mode 100644 index 00000000..8074f3b3 --- /dev/null +++ b/Drivers/TCA8418/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "Source" + INCLUDE_DIRS "Source" + REQUIRES Tactility +) diff --git a/Drivers/TCA8418/COPYRIGHT.md b/Drivers/TCA8418/COPYRIGHT.md new file mode 100644 index 00000000..dff105a9 --- /dev/null +++ b/Drivers/TCA8418/COPYRIGHT.md @@ -0,0 +1,18 @@ +Copyright 2023 Anthony DiGirolamo + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Drivers/TCA8418/README.md b/Drivers/TCA8418/README.md new file mode 100644 index 00000000..e3ef16a1 --- /dev/null +++ b/Drivers/TCA8418/README.md @@ -0,0 +1,4 @@ +# TCA8418 I2C Controlled Keypad Scan IC With Integrated ESD Protection + +[Datasheet](https://www.ti.com/lit/ds/symlink/tca8418.pdf?ts=1751500237439) +[Original implementation](https://github.com/AnthonyDiGirolamo/i2c-thumb-keyboard/tree/master) by Anthony DiGirolamo diff --git a/Drivers/TCA8418/Source/Tca8418.cpp b/Drivers/TCA8418/Source/Tca8418.cpp new file mode 100644 index 00000000..f4bac8b8 --- /dev/null +++ b/Drivers/TCA8418/Source/Tca8418.cpp @@ -0,0 +1,202 @@ +#include "Tca8418.h" +#include + +#define TAG "tca8418" + +namespace registers { +static const uint8_t CFG = 0x01U; +static const uint8_t KP_GPIO1 = 0x1DU; +static const uint8_t KP_GPIO2 = 0x1EU; +static const uint8_t KP_GPIO3 = 0x1FU; + +static const uint8_t KEY_EVENT_A = 0x04U; +static const uint8_t KEY_EVENT_B = 0x05U; +static const uint8_t KEY_EVENT_C = 0x06U; +static const uint8_t KEY_EVENT_D = 0x07U; +static const uint8_t KEY_EVENT_E = 0x08U; +static const uint8_t KEY_EVENT_F = 0x09U; +static const uint8_t KEY_EVENT_G = 0x0AU; +static const uint8_t KEY_EVENT_H = 0x0BU; +static const uint8_t KEY_EVENT_I = 0x0CU; +static const uint8_t KEY_EVENT_J = 0x0DU; +} // namespace registers + + +void Tca8418::init(uint8_t numrows, uint8_t numcols) { + /* + * | ADDRESS | REGISTER NAME | REGISTER DESCRIPTION | BIT7 | BIT6 | BIT5 | BIT4 | BIT3 | BIT2 | BIT1 | BIT0 | + * |---------+---------------+----------------------+------+------+------+------+------+------+------+------| + * | 0x1D | KP_GPIO1 | Keypad/GPIO Select 1 | ROW7 | ROW6 | ROW5 | ROW4 | ROW3 | ROW2 | ROW1 | ROW0 | + * | 0x1E | KP_GPIO2 | Keypad/GPIO Select 2 | COL7 | COL6 | COL5 | COL4 | COL3 | COL2 | COL1 | COL0 | + * | 0x1F | KP_GPIO3 | Keypad/GPIO Select 3 | N/A | N/A | N/A | N/A | N/A | N/A | COL9 | COL8 | + */ + + num_rows = numrows; + num_cols = numcols; + + // everything enabled in key scan mode + uint8_t enabled_rows = 0x3F; + uint16_t enabled_cols = 0x3FF; + + writeRegister8(registers::KP_GPIO1, enabled_rows); + writeRegister8(registers::KP_GPIO2, (uint8_t)(0xFF & enabled_cols)); + writeRegister8(registers::KP_GPIO3, (uint8_t)(0x03 & (enabled_cols >> 8))); + + /* + * BIT: NAME + * + * 7: AI + * Auto-increment for read and write operations; See below table for more information + * 0 = disabled + * 1 = enabled + * + * 6: GPI_E_CFG + * GPI event mode configuration + * 0 = GPI events are tracked when keypad is locked + * 1 = GPI events are not tracked when keypad is locked + * + * 5: OVR_FLOW_M + * Overflow mode + * 0 = disabled; Overflow data is lost + * 1 = enabled; Overflow data shifts with last event pushing first event out + * + * 4: INT_CFG + * Interrupt configuration + * 0 = processor interrupt remains asserted (or low) if host tries to clear interrupt while there is + * still a pending key press, key release or GPI interrupt + * 1 = processor interrupt is deasserted for 50 μs and reassert with pending interrupts + * + * 3: OVR_FLOW_IEN + * Overflow interrupt enable + * 0 = disabled; INT is not asserted if the FIFO overflows + * 1 = enabled; INT becomes asserted if the FIFO overflows + * + * 2: K_LCK_IEN + * Keypad lock interrupt enable + * 0 = disabled; INT is not asserted after a correct unlock key sequence + * 1 = enabled; INT becomes asserted after a correct unlock key sequence + * + * 1: GPI_IEN + * GPI interrupt enable to host processor + * 0 = disabled; INT is not asserted for a change on a GPI + * 1 = enabled; INT becomes asserted for a change on a GPI + * + * 0: KE_IEN + * Key events interrupt enable to host processor + * 0 = disabled; INT is not asserted when a key event occurs + * 1 = enabled; INT becomes asserted when a key event occurs + */ + + // 10111001 xB9 -- fifo overflow enabled + // 10011001 x99 -- fifo overflow disabled + writeRegister8(registers::CFG, 0x99); + + clear_released_list(); + clear_pressed_list(); +} + +bool Tca8418::update() { + last_update_micros = this_update_micros; + uint8_t key_code, key_down, key_event, key_row, key_col; + + key_event = get_key_event(); + // TODO: read gpio R7/R6 status? 0x14 bits 7&6 + // read(0x14, &new_keycode) + + // TODO: use tick function to get an update delta time + this_update_micros = 0; + delta_micros = this_update_micros - last_update_micros; + + if (key_event > 0) { + key_code = key_event & 0x7F; + key_down = (key_event & 0x80) >> 7; + key_row = key_code / num_cols; + key_col = key_code % num_cols; + + // always clear the released list + clear_released_list(); + + if (key_down) { + add_pressed_key(key_row, key_col); + // TODO reject ghosts (assume multiple key presses with the same hold time are ghosts.) + + } else { + add_released_key(key_row, key_col); + remove_pressed_key(key_row, key_col); + } + + return true; + } + + // Increment hold times for pressed keys + for (int i = 0; i < pressed_key_count; i++) { + pressed_list[i].hold_time += delta_micros; + } + + return false; +} + + +void Tca8418::add_pressed_key(uint8_t row, uint8_t col) { + if (pressed_key_count >= KEY_EVENT_LIST_SIZE) + return; + + pressed_list[pressed_key_count].row = row; + pressed_list[pressed_key_count].col = col; + pressed_list[pressed_key_count].hold_time = 0; + pressed_key_count++; +} + +void Tca8418::add_released_key(uint8_t row, uint8_t col) { + if (released_key_count >= KEY_EVENT_LIST_SIZE) + return; + + released_key_count++; + released_list[0].row = row; + released_list[0].col = col; +} + +void Tca8418::remove_pressed_key(uint8_t row, uint8_t col) { + if (pressed_key_count == 0) + return; + + // delete the pressed key + for (int i = 0; i < pressed_key_count; i++) { + if (pressed_list[i].row == row && + pressed_list[i].col == col) { + // shift remaining keys left one index + for (int j = i; i < pressed_key_count; j++) { + if (j == KEY_EVENT_LIST_SIZE - 1) + break; + pressed_list[j].row = pressed_list[j + 1].row; + pressed_list[j].col = pressed_list[j + 1].col; + pressed_list[j].hold_time = pressed_list[j + 1].hold_time; + } + break; + } + } + pressed_key_count--; +} + +void Tca8418::clear_pressed_list() { + for (int i = 0; i < KEY_EVENT_LIST_SIZE; i++) { + pressed_list[i].row = 255; + pressed_list[i].col = 255; + } + pressed_key_count = 0; +} + +void Tca8418::clear_released_list() { + for (int i = 0; i < KEY_EVENT_LIST_SIZE; i++) { + released_list[i].row = 255; + released_list[i].col = 255; + } + released_key_count = 0; +} + +uint8_t Tca8418::get_key_event() { + uint8_t new_keycode = 0; + + readRegister8(registers::KEY_EVENT_A, new_keycode); + return new_keycode; +} diff --git a/Drivers/TCA8418/Source/Tca8418.h b/Drivers/TCA8418/Source/Tca8418.h new file mode 100644 index 00000000..d48d14a3 --- /dev/null +++ b/Drivers/TCA8418/Source/Tca8418.h @@ -0,0 +1,69 @@ +#pragma once + +#include + +#include + +#define TCA8418_ADDRESS 0x34U +#define KEY_EVENT_LIST_SIZE 10 + +class Tca8418 final : public tt::hal::i2c::I2cDevice { + +private: + + uint8_t tca8418_address; + uint32_t last_update_micros; + uint32_t this_update_micros; + + uint8_t new_pressed_keys_count; + + void clear_released_list(); + void clear_pressed_list(); + void add_pressed_key(uint8_t row, uint8_t col); + void add_released_key(uint8_t row, uint8_t col); + void remove_pressed_key(uint8_t row, uint8_t col); + void write(uint8_t register_address, uint8_t data); + bool read(uint8_t register_address, uint8_t* data); + +public: + + struct PressedKey { + uint8_t row; + uint8_t col; + uint32_t hold_time; + }; + + struct ReleasedKey { + uint8_t row; + uint8_t col; + }; + + std::string getName() const final { return "TCA8418"; } + + std::string getDescription() const final { return "I2C-controlled keyboard scan IC"; } + + explicit Tca8418(i2c_port_t port) : I2cDevice(port, TCA8418_ADDRESS) { + delta_micros = 0; + last_update_micros = 0; + this_update_micros = 0; + } + + ~Tca8418() {} + + uint8_t num_rows; + uint8_t num_cols; + + uint32_t delta_micros; + + std::array pressed_list; + std::array released_list; + uint8_t pressed_key_count; + uint8_t released_key_count; + + void init(uint8_t numrows, uint8_t numcols); + bool update(); + uint8_t get_key_event(); + bool button_pressed(uint8_t row, uint8_t button_bit_position); + bool button_released(uint8_t row, uint8_t button_bit_position); + bool button_held(uint8_t row, uint8_t button_bit_position); +}; diff --git a/Tactility/Include/Tactility/hal/i2c/I2cDevice.h b/Tactility/Include/Tactility/hal/i2c/I2cDevice.h index c7754999..0d4c2e7e 100644 --- a/Tactility/Include/Tactility/hal/i2c/I2cDevice.h +++ b/Tactility/Include/Tactility/hal/i2c/I2cDevice.h @@ -1,7 +1,7 @@ #pragma once -#include "I2c.h" #include "../Device.h" +#include "I2c.h" namespace tt::hal::i2c { @@ -20,7 +20,11 @@ protected: static constexpr TickType_t DEFAULT_TIMEOUT = 1000 / portTICK_PERIOD_MS; + bool read(uint8_t* data, size_t dataSize, TickType_t timeout = DEFAULT_TIMEOUT); + bool write(const uint8_t* data, uint16_t dataSize, TickType_t timeout = DEFAULT_TIMEOUT); + bool writeRead(const uint8_t* writeData, size_t writeDataSize, uint8_t* readData, size_t readDataSize, TickType_t timeout = DEFAULT_TIMEOUT); bool readRegister8(uint8_t reg, uint8_t& result) const; + bool writeRegister(uint8_t reg, const uint8_t* data, uint16_t dataSize, TickType_t timeout = DEFAULT_TIMEOUT); bool writeRegister8(uint8_t reg, uint8_t value) const; bool readRegister12(uint8_t reg, float& out) const; bool readRegister14(uint8_t reg, float& out) const; @@ -41,4 +45,4 @@ public: uint8_t getAddress() const { return address; } }; -} +} // namespace tt::hal::i2c diff --git a/Tactility/Source/hal/i2c/I2cDevice.cpp b/Tactility/Source/hal/i2c/I2cDevice.cpp index 4891e471..1e96f8d5 100644 --- a/Tactility/Source/hal/i2c/I2cDevice.cpp +++ b/Tactility/Source/hal/i2c/I2cDevice.cpp @@ -4,6 +4,22 @@ namespace tt::hal::i2c { +bool I2cDevice::read(uint8_t* data, size_t dataSize, TickType_t timeout) { + return tt::hal::i2c::masterRead(port, address, data, dataSize, timeout); +} + +bool I2cDevice::write(const uint8_t* data, uint16_t dataSize, TickType_t timeout) { + return tt::hal::i2c::masterWrite(port, address, data, dataSize, timeout); +} + +bool I2cDevice::writeRead(const uint8_t* writeData, size_t writeDataSize, uint8_t* readData, size_t readDataSize, TickType_t timeout) { + return masterWriteRead(port, address, writeData, writeDataSize, readData, readDataSize, timeout); +} + +bool I2cDevice::writeRegister(uint8_t reg, const uint8_t* data, uint16_t dataSize, TickType_t timeout) { + return masterWriteRegister(port, address, reg, data, dataSize, timeout); +} + bool I2cDevice::readRegister12(uint8_t reg, float& out) const { std::uint8_t data[2] = {0}; if (tt::hal::i2c::masterReadRegister(port, address, reg, data, 2, DEFAULT_TIMEOUT)) { @@ -62,4 +78,4 @@ bool I2cDevice::bitOff(uint8_t reg, uint8_t bitmask) const { } } -} // namespace +} // namespace tt::hal::i2c diff --git a/Tactility/Source/service/gui/GuiDraw.cpp b/Tactility/Source/service/gui/GuiDraw.cpp index ed52c19e..a3ba2333 100644 --- a/Tactility/Source/service/gui/GuiDraw.cpp +++ b/Tactility/Source/service/gui/GuiDraw.cpp @@ -39,6 +39,17 @@ void redraw(Gui* gui) { if (gui->appToRender != nullptr) { + // Create a default group which adds all objects automatically, + // and assign all indevs to it. + // This enables navigation with limited input, such as encoder wheels. + lv_group_t* group = lv_group_create(); + auto* indev = lv_indev_get_next(nullptr); + while (indev) { + lv_indev_set_group(indev, group); + indev = lv_indev_get_next(indev); + } + lv_group_set_default(group); + app::Flags flags = std::static_pointer_cast(gui->appToRender)->getFlags(); if (flags.showStatusbar) { lv_obj_remove_flag(gui->statusbarWidget, LV_OBJ_FLAG_HIDDEN); @@ -61,4 +72,4 @@ void redraw(Gui* gui) { unlock(); } -} // namespace +} // namespace tt::service::gui diff --git a/sdkconfig.board.lilygo-tlora-pager b/sdkconfig.board.lilygo-tlora-pager new file mode 100644 index 00000000..16fb1e30 --- /dev/null +++ b/sdkconfig.board.lilygo-tlora-pager @@ -0,0 +1,56 @@ +# Software defaults +# Increase stack size for WiFi (fixes crash after scan) +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072 +CONFIG_LV_FONT_MONTSERRAT_14=y +CONFIG_LV_FONT_MONTSERRAT_18=y +CONFIG_LV_USE_USER_DATA=y +CONFIG_LV_USE_FS_STDIO=y +CONFIG_LV_FS_STDIO_LETTER=65 +CONFIG_LV_FS_STDIO_PATH="" +CONFIG_LV_FS_STDIO_CACHE_SIZE=4096 +CONFIG_LV_USE_LODEPNG=y +CONFIG_LV_USE_BUILTIN_MALLOC=n +CONFIG_LV_USE_CLIB_MALLOC=y +CONFIG_LV_USE_MSGBOX=n +CONFIG_LV_USE_SPINNER=n +CONFIG_LV_USE_WIN=n +CONFIG_LV_USE_SNAPSHOT=y +CONFIG_FREERTOS_HZ=1000 +CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2 +CONFIG_FREERTOS_SMP=n +CONFIG_FREERTOS_UNICORE=n +CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096 +CONFIG_FREERTOS_USE_TRACE_FACILITY=y +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +CONFIG_FATFS_LFN_HEAP=y +CONFIG_FATFS_VOLUME_COUNT=3 + +# Hardware: Main +CONFIG_TT_BOARD_LILYGO_TLORA_PAGER=y +CONFIG_TT_BOARD_NAME="LilyGo T-Lora Pager" +CONFIG_TT_BOARD_ID="lilygo-tlora-pager" +CONFIG_IDF_EXPERIMENTAL_FEATURES=y +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y +CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y +CONFIG_FLASHMODE_DIO=y +# Hardware: SPI RAM +CONFIG_ESP32S3_SPIRAM_SUPPORT=y +#CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_TYPE_AUTO=y +CONFIG_SPIRAM_SPEED_120M=y +#CONFIG_SPIRAM_BOOT_INIT=y +CONFIG_SPIRAM_USE_MALLOC=y +CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y +# SPI Flash (can set back to 80MHz after ESP-IDF bug is resolved) +CONFIG_ESPTOOLPY_FLASHFREQ_40M=y +# LVGL +CONFIG_LV_DPI_DEF=90 +CONFIG_LV_DISP_DEF_REFR_PERIOD=10 +CONFIG_LV_THEME_DEFAULT_DARK=y +# USB +CONFIG_TINYUSB_MSC_ENABLED=y +CONFIG_TINYUSB_MSC_MOUNT_PATH="/sdcard" From 982fce920791e43a8296354d12b989b1102bb887 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Wed, 23 Jul 2025 23:09:56 +0200 Subject: [PATCH 16/16] Project updates for Lilygo T-Lora Pager (#299) - Updated build scripts for manual release - Updated GitHub Actions - Added reference to [esp32s3-gc9a01-lvgl](https://github.com/UsefulElectronics/esp32s3-gc9a01-lvgl) in `COPYRIGHT.md` --- .github/workflows/build-firmware.yml | 9 +++++++++ Buildscripts/build-and-release-all.sh | 3 +++ COPYRIGHT.md | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/.github/workflows/build-firmware.yml b/.github/workflows/build-firmware.yml index 2e8f4434..b83f578f 100644 --- a/.github/workflows/build-firmware.yml +++ b/.github/workflows/build-firmware.yml @@ -126,6 +126,15 @@ jobs: with: board_id: lilygo-tdeck arch: esp32s3 + lilygo-tlora-pager: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: "Build" + uses: ./.github/actions/build-firmware + with: + board_id: lilygo-tlora-pager + arch: esp32s3 m5stack-core2: runs-on: ubuntu-latest steps: diff --git a/Buildscripts/build-and-release-all.sh b/Buildscripts/build-and-release-all.sh index db6229d3..8472761c 100755 --- a/Buildscripts/build-and-release-all.sh +++ b/Buildscripts/build-and-release-all.sh @@ -35,6 +35,9 @@ release elecrow-crowpanel-basic-50 build lilygo-tdeck release lilygo-tdeck +build lilygo-tlora-pager +release lilygo-tlora-pager + releaseSdk release/TactilitySDK-esp32s3 build cyd-2432s024c diff --git a/COPYRIGHT.md b/COPYRIGHT.md index 381a1d5d..943b3a4e 100644 --- a/COPYRIGHT.md +++ b/COPYRIGHT.md @@ -59,6 +59,12 @@ Website: https://github.com/Xinyuan-LilyGO/T-Echo/blob/main/LICENSE License: [MIT](https://github.com/Xinyuan-LilyGO/T-Echo/blob/main/LICENSE) +### esp32s3-gc9a01-lvgl + +Website: https://github.com/UsefulElectronics/esp32s3-gc9a01-lvgl + +License: [Explicitly granted by author](https://github.com/ByteWelder/Tactility/pull/295#discussion_r2226215423) + ### Other Components See `/components` for the respective projects and their licenses.