diff --git a/CMakeLists.txt b/CMakeLists.txt index 044bca2b..9ebdb742 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,6 +72,8 @@ else () message("Building for sim target") add_compile_definitions(CONFIG_TT_DEVICE_ID="simulator") add_compile_definitions(CONFIG_TT_DEVICE_NAME="Simulator") + add_compile_definitions(CONFIG_TT_DEVICE_VENDOR="") + add_compile_definitions(CONFIG_TT_DEVICE_NAME_SIMPLE="Simulator") add_compile_definitions(CONFIG_TT_LAUNCHER_APP_ID="Launcher") add_compile_definitions(CONFIG_TT_AUTO_START_APP_ID="") endif () diff --git a/Documentation/ideas.md b/Documentation/ideas.md index 8c09f957..1af68702 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -48,6 +48,7 @@ ## Medium Priority +- Make USB host driver disabled by default, so it doesn't consume memory - Filtering for apps in App Hub: - apps that only work on a specific device - Diceware app has large "+" and "-' buttons on Cardputer. It should be smaller. @@ -100,14 +101,12 @@ # App Ideas -- Revisit TinyUSB mouse idea: the bugs related to cleanup seem to be fixed in the library. - Map widget: https://github.com/portapack-mayhem/mayhem-firmware/blob/b66d8b1aa178d8a9cd06436fea788d5d58cb4c8d/firmware/application/ui/ui_geomap.cpp https://github.com/portapack-mayhem/mayhem-firmware/blob/b66d8b1aa178d8a9cd06436fea788d5d58cb4c8d/firmware/tools/generate_world_map.bin.py https://github.com/portapack-mayhem/mayhem-firmware/releases - Weather app: https://lab.flipper.net/apps/flip_weather - wget app: https://lab.flipper.net/apps/web_crawler (add profiles for known public APIs?) -- BlueTooth keyboard app - Chip 8 emulator - BadUSB (in December 2024, TinyUSB has a bug where uninstalling and re-installing the driver fails) - Discord bot diff --git a/Firmware/Kconfig b/Firmware/Kconfig index c35baffd..9ec78134 100644 --- a/Firmware/Kconfig +++ b/Firmware/Kconfig @@ -4,8 +4,18 @@ menu "Tactility App" string "Device Name" default "" help - Human-readable device name, including vendor - config TT_DEVICE_ID + Human-readable device name, including vendor (e.g. "M5Stack Cardputer") + config TT_DEVICE_NAME_SIMPLE + string "Device Name (simple)" + default "" + help + Human-readable simple device name, excluding vendor (e.g. "Cardputer") + config TT_DEVICE_VENDOR + string "Device Vendor Name" + default "" + help + Human-readable device vendor name (e.g. "LilyGO") + config TT_DEVICE_ID string "Device Identifier" default "" help diff --git a/Tactility/Include/Tactility/settings/SystemSettings.h b/Tactility/Include/Tactility/settings/SystemSettings.h index b74b3a79..0339b718 100644 --- a/Tactility/Include/Tactility/settings/SystemSettings.h +++ b/Tactility/Include/Tactility/settings/SystemSettings.h @@ -8,7 +8,6 @@ struct SystemSettings { Language language; bool timeFormat24h; std::string dateFormat; // MM/DD/YYYY, DD/MM/YYYY, YYYY-MM-DD, YYYY/MM/DD - std::string region; // (US, EU, JP, etc.) }; bool loadSystemSettings(SystemSettings& properties); diff --git a/Tactility/Include/Tactility/settings/Time.h b/Tactility/Include/Tactility/settings/Time.h index 40ff450f..2ea05f49 100644 --- a/Tactility/Include/Tactility/settings/Time.h +++ b/Tactility/Include/Tactility/settings/Time.h @@ -16,6 +16,9 @@ void setTimeZone(const std::string& name, const std::string& code); */ std::string getTimeZoneName(); +/** @return true when a timezone has been explicitly set (as opposed to the default) */ +bool hasTimeZone(); + /** * Get the code of the timezone (see timezones.csv) */ diff --git a/Tactility/Private/Tactility/app/setup/Setup.h b/Tactility/Private/Tactility/app/setup/Setup.h new file mode 100644 index 00000000..84751860 --- /dev/null +++ b/Tactility/Private/Tactility/app/setup/Setup.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace tt::app::setup { + +LaunchId start(); + +/** @return true if the setup wizard has already run to completion */ +bool isCompleted(); + +} diff --git a/Tactility/Private/Tactility/app/timezone/TimeZone.h b/Tactility/Private/Tactility/app/timezone/TimeZone.h index 2c673ee1..9e090e8c 100644 --- a/Tactility/Private/Tactility/app/timezone/TimeZone.h +++ b/Tactility/Private/Tactility/app/timezone/TimeZone.h @@ -5,7 +5,7 @@ namespace tt::app::timezone { -LaunchId start(); +LaunchId start(bool saveTimeZone = false); std::string getResultName(const Bundle& bundle); std::string getResultCode(const Bundle& bundle); diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 5f83d446..9b54a02a 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -103,6 +103,7 @@ namespace app { namespace power { extern const AppManifest manifest; } namespace selectiondialog { extern const AppManifest manifest; } namespace settings { extern const AppManifest manifest; } + namespace setup { extern const AppManifest manifest; } namespace systeminfo { extern const AppManifest manifest; } namespace timedatesettings { extern const AppManifest manifest; } namespace touchcalibration { extern const AppManifest manifest; } @@ -156,6 +157,7 @@ static void registerInternalApps() { addAppManifest(app::notes::manifest); addAppManifest(app::settings::manifest); addAppManifest(app::selectiondialog::manifest); + addAppManifest(app::setup::manifest); addAppManifest(app::systeminfo::manifest); addAppManifest(app::timedatesettings::manifest); addAppManifest(app::touchcalibration::manifest); diff --git a/Tactility/Source/app/launcher/Launcher.cpp b/Tactility/Source/app/launcher/Launcher.cpp index 3d99474e..0146d678 100644 --- a/Tactility/Source/app/launcher/Launcher.cpp +++ b/Tactility/Source/app/launcher/Launcher.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -137,22 +138,26 @@ public: void onCreate(AppContext& app) override { settings::BootSettings boot_properties; - if (settings::loadBootSettings(boot_properties)) { - if ( - !boot_properties.autoStartAppId.empty() && - findAppManifestById(boot_properties.autoStartAppId) != nullptr - ) { - LOGGER.info("Starting {}", boot_properties.autoStartAppId); - start(boot_properties.autoStartAppId); - } else { - LOGGER.info("No auto-start app configured. Skipping default auto-start due to boot.properties presence."); - } - } else if ( + if ( + // Auto-start due to built-in requirement strcmp(CONFIG_TT_AUTO_START_APP_ID, "") != 0 && findAppManifestById(CONFIG_TT_AUTO_START_APP_ID) != nullptr ) { LOGGER.info("Starting {}", CONFIG_TT_AUTO_START_APP_ID); start(CONFIG_TT_AUTO_START_APP_ID); + } else if ( + // Auto-start due to user configuration + settings::loadBootSettings(boot_properties) && + !boot_properties.autoStartAppId.empty() && + findAppManifestById(boot_properties.autoStartAppId) != nullptr + ) { + LOGGER.info("Starting {}", boot_properties.autoStartAppId); + start(boot_properties.autoStartAppId); + } else { + // No auto-start, consider running system setup + if (!setup::isCompleted()) { + setup::start(); + } } } diff --git a/Tactility/Source/app/localesettings/LocaleSettings.cpp b/Tactility/Source/app/localesettings/LocaleSettings.cpp index f9523d00..9293b52d 100644 --- a/Tactility/Source/app/localesettings/LocaleSettings.cpp +++ b/Tactility/Source/app/localesettings/LocaleSettings.cpp @@ -28,7 +28,6 @@ extern const AppManifest manifest; class LocaleSettingsApp final : public App { tt::i18n::TextResources textResources = tt::i18n::TextResources(TEXT_RESOURCE_PATH); RecursiveMutex mutex; - lv_obj_t* regionTextArea = nullptr; lv_obj_t* languageDropdown = nullptr; bool settingsUpdated = false; @@ -98,33 +97,6 @@ public: lv_obj_set_width(main_wrapper, LV_PCT(100)); lv_obj_set_flex_grow(main_wrapper, 1); - // Region - - auto* region_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_width(region_wrapper, LV_PCT(100)); - lv_obj_set_height(region_wrapper, LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(region_wrapper, 8, 0); - lv_obj_set_style_border_width(region_wrapper, 0, 0); - - auto* region_label = lv_label_create(region_wrapper); - lv_label_set_text(region_label, textResources[i18n::Text::REGION].c_str()); - lv_obj_align(region_label, LV_ALIGN_LEFT_MID, 4, 0); - - // Region text area for user input (e.g., US, EU, JP) - regionTextArea = lv_textarea_create(region_wrapper); - lv_obj_set_width(regionTextArea, 120); - lv_textarea_set_one_line(regionTextArea, true); - lv_textarea_set_max_length(regionTextArea, 50); - lv_textarea_set_placeholder_text(regionTextArea, "e.g. US, EU"); - - // Load current region from settings - settings::SystemSettings sysSettings; - if (settings::loadSystemSettings(sysSettings)) { - lv_textarea_set_text(regionTextArea, sysSettings.region.c_str()); - } - lv_obj_add_event_cb(regionTextArea, onRegionChanged, LV_EVENT_VALUE_CHANGED, this); - lv_obj_align(regionTextArea, LV_ALIGN_RIGHT_MID, 0, 0); - // Language auto* language_wrapper = lv_obj_create(main_wrapper); @@ -145,16 +117,6 @@ public: lv_dropdown_set_selected(languageDropdown, static_cast(settings::getLanguage())); lv_obj_add_event_cb(languageDropdown, onLanguageSet, LV_EVENT_VALUE_CHANGED, this); } - - void onHide(AppContext& app) override { - if (settingsUpdated && regionTextArea) { - settings::SystemSettings sysSettings; - if (settings::loadSystemSettings(sysSettings)) { - sysSettings.region = lv_textarea_get_text(regionTextArea); - settings::saveSystemSettings(sysSettings); - } - } - } }; extern const AppManifest manifest = { diff --git a/Tactility/Source/app/setup/Setup.cpp b/Tactility/Source/app/setup/Setup.cpp new file mode 100644 index 00000000..0d50b846 --- /dev/null +++ b/Tactility/Source/app/setup/Setup.cpp @@ -0,0 +1,205 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace tt::app::setup { + +extern const AppManifest manifest; + +constexpr auto* PREFERENCES_NAMESPACE = "setup"; +constexpr auto* PREFERENCES_KEY_COMPLETED = "completed"; + +bool isCompleted() { + Preferences preferences(PREFERENCES_NAMESPACE); + bool completed = false; + preferences.optBool(PREFERENCES_KEY_COMPLETED, completed); + return completed; +} + +static void markCompleted() { + Preferences preferences(PREFERENCES_NAMESPACE); + preferences.putBool(PREFERENCES_KEY_COMPLETED, true); +} + +struct StepConfiguration { + std::string title; + std::string description; + std::function run; +}; + +class SetupApp final : public App { + + enum class Phase { + Welcome, + StepIntro, + Done + }; + + Phase phase = Phase::Welcome; + size_t stepIndex = 0; + std::vector steps; + + lv_obj_t* titleLabel = nullptr; + lv_obj_t* descriptionLabel = nullptr; + lv_obj_t* skipButton = nullptr; + lv_obj_t* continueButton = nullptr; + + static void onSkipClickedCallback(lv_event_t* e) { + auto* app = (SetupApp*)lv_event_get_user_data(e); + app->onSkipClicked(); + } + + static void onContinueClickedCallback(lv_event_t* e) { + auto* app = (SetupApp*)lv_event_get_user_data(e); + app->onContinueClicked(); + } + + void renderCurrent() { + switch (phase) { + case Phase::Welcome: { + lv_label_set_text(titleLabel, "Welcome"); + auto device_names = string::split(std::string(CONFIG_TT_DEVICE_NAME_SIMPLE), ","); + lv_label_set_text_fmt(descriptionLabel, "It's time to set up your %s!", device_names.front().c_str()); + lv_obj_add_flag(skipButton, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(lv_obj_get_child(continueButton, 0), "Continue"); + break; + } + case Phase::StepIntro: { + const auto& step = steps[stepIndex]; + lv_label_set_text(titleLabel, step.title.c_str()); + lv_label_set_text(descriptionLabel, step.description.c_str()); + lv_obj_remove_flag(skipButton, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(lv_obj_get_child(skipButton, 0), "Skip"); + lv_label_set_text(lv_obj_get_child(continueButton, 0), "Continue"); + break; + } + case Phase::Done: + lv_label_set_text(titleLabel, "Setup Complete"); + lv_label_set_text(descriptionLabel, "You're all set."); + lv_obj_add_flag(skipButton, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(lv_obj_get_child(continueButton, 0), "Finish"); + break; + } + } + + void advanceTo(size_t index) { + if (index < steps.size()) { + stepIndex = index; + phase = Phase::StepIntro; + } else { + phase = Phase::Done; + } + + renderCurrent(); + } + + void onSkipClicked() { + if (phase == Phase::StepIntro) { + advanceTo(stepIndex + 1); + } + } + + void onContinueClicked() { + switch (phase) { + case Phase::Welcome: + advanceTo(0); + break; + case Phase::StepIntro: + steps[stepIndex].run(); + break; + case Phase::Done: + markCompleted(); + stop(manifest.appId); + break; + } + } + +public: + + void onCreate(AppContext& app) override { + steps = { + { + .title = "Time Zone Setup", + .description = "Let's set the time zone.", + .run = [] { timezone::start(true); } + }, + { + .title = "Wi-Fi Setup", + .description = "Let's connect to a Wi-Fi access point.", + .run = [] { + service::wifi::setEnabled(true); + wifimanage::start(); + } + } + }; + } + + void onShow(AppContext& app, lv_obj_t* parent) override { + titleLabel = lv_label_create(parent); + lv_obj_set_width(titleLabel, LV_PCT(80)); + lv_obj_set_style_text_align(titleLabel, LV_TEXT_ALIGN_CENTER, 0); + lv_label_set_long_mode(titleLabel, LV_LABEL_LONG_WRAP); + auto* font = lvgl_get_text_font(FONT_SIZE_LARGE); + lv_obj_set_style_text_font(titleLabel, font, 0); + + descriptionLabel = lv_label_create(parent); + lv_obj_set_width(descriptionLabel, LV_PCT(80)); + lv_obj_set_style_text_align(descriptionLabel, LV_TEXT_ALIGN_CENTER, 0); + lv_label_set_long_mode(descriptionLabel, LV_LABEL_LONG_WRAP); + lv_obj_align(descriptionLabel, LV_ALIGN_CENTER, 0, 0); + + int title_margin = lvgl_get_text_font_height(FONT_SIZE_LARGE); + lv_obj_align_to(titleLabel, descriptionLabel, LV_ALIGN_OUT_TOP_MID, 0, -title_margin); + + skipButton = lv_button_create(parent); + lv_obj_t* skip_label = lv_label_create(skipButton); + lv_label_set_text(skip_label, "Skip"); + lv_obj_center(skip_label); + lv_obj_align(skipButton, LV_ALIGN_BOTTOM_LEFT, 12, -12); + lv_obj_add_event_cb(skipButton, onSkipClickedCallback, LV_EVENT_SHORT_CLICKED, this); + + continueButton = lv_button_create(parent); + lv_obj_t* continue_label = lv_label_create(continueButton); + lv_label_set_text(continue_label, "Continue"); + lv_obj_center(continue_label); + lv_obj_align(continueButton, LV_ALIGN_BOTTOM_RIGHT, -12, -12); + lv_obj_add_event_cb(continueButton, onContinueClickedCallback, LV_EVENT_SHORT_CLICKED, this); + + renderCurrent(); + } + + void onResult(AppContext& app, LaunchId launchId, Result result, std::unique_ptr bundle) override { + lvgl_lock(); + advanceTo(stepIndex + 1); + lvgl_unlock(); + } +}; + +extern const AppManifest manifest = { + .appId = "Setup", + .appName = "Setup", + .appCategory = Category::System, + .appFlags = AppManifest::Flags::Hidden | AppManifest::Flags::HideStatusBar, + .createApp = create +}; + +LaunchId start() { + return app::start(manifest.appId); +} + +} diff --git a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp index bbf87fb0..ab8b825f 100644 --- a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp +++ b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp @@ -31,7 +31,7 @@ class TimeDateSettingsApp final : public App { } static void onTimeZonePressed(lv_event_t* event) { - timezone::start(); + timezone::start(true); } static void onDateFormatChanged(lv_event_t* event) { @@ -140,7 +140,6 @@ public: const auto name = timezone::getResultName(*bundle); const auto code = timezone::getResultCode(*bundle); LOGGER.info("Result name={} code={}", name, code); - settings::setTimeZone(name, code); if (!name.empty()) { if (lvgl::lock(100 / portTICK_PERIOD_MS)) { diff --git a/Tactility/Source/app/timezone/TimeZone.cpp b/Tactility/Source/app/timezone/TimeZone.cpp index 77a35170..a0f68d4f 100644 --- a/Tactility/Source/app/timezone/TimeZone.cpp +++ b/Tactility/Source/app/timezone/TimeZone.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -23,6 +24,7 @@ static const auto LOGGER = Logger("TimeZone"); constexpr auto* RESULT_BUNDLE_CODE_INDEX = "code"; constexpr auto* RESULT_BUNDLE_NAME_INDEX = "name"; +constexpr auto* PARAM_SAVE_TIME_ZONE = "saveTimeZone"; extern const AppManifest manifest; @@ -74,6 +76,7 @@ class TimeZoneApp final : public App { std::unique_ptr updateTimer; lv_obj_t* listWidget = nullptr; lv_obj_t* filterTextareaWidget = nullptr; + bool saveTimeZone = false; static void onTextareaValueChangedCallback(lv_event_t* e) { auto* app = (TimeZoneApp*)lv_event_get_user_data(e); @@ -104,6 +107,10 @@ class TimeZoneApp final : public App { auto& entry = entries[index]; + if (saveTimeZone) { + settings::setTimeZone(entry.name, entry.code); + } + auto bundle = std::make_unique(); setResultName(*bundle, entry.name); setResultCode(*bundle, entry.code); @@ -222,6 +229,11 @@ public: } void onCreate(AppContext& app) override { + auto parameters = app.getParameters(); + if (parameters != nullptr) { + parameters->optBool(PARAM_SAVE_TIME_ZONE, saveTimeZone); + } + updateTimer = std::make_unique(Timer::Type::Once, 500 / portTICK_PERIOD_MS, [this] { updateList(); }); @@ -230,14 +242,16 @@ public: extern const AppManifest manifest = { .appId = "TimeZone", - .appName = "Select timezone", + .appName = "Select Time zone", .appCategory = Category::System, .appFlags = AppManifest::Flags::Hidden, .createApp = create }; -LaunchId start() { - return app::start(manifest.appId); +LaunchId start(bool saveTimeZone) { + auto bundle = std::make_shared(); + bundle->putBool(PARAM_SAVE_TIME_ZONE, saveTimeZone); + return app::start(manifest.appId, bundle); } } diff --git a/Tactility/Source/settings/SystemSettings.cpp b/Tactility/Source/settings/SystemSettings.cpp index 163a847a..77ec5b35 100644 --- a/Tactility/Source/settings/SystemSettings.cpp +++ b/Tactility/Source/settings/SystemSettings.cpp @@ -14,7 +14,6 @@ static const auto LOGGER = Logger("SystemSettings"); constexpr auto* FILE_PATH_FORMAT = "{}/settings/system.properties"; -static Mutex mutex; static bool cached = false; static SystemSettings cachedSettings; @@ -51,15 +50,6 @@ static bool loadSystemSettingsFromFile(SystemSettings& properties) { properties.dateFormat = "MM/DD/YYYY"; } - // Load region - auto region_entry = map.find("region"); - if (region_entry != map.end() && !region_entry->second.empty()) { - properties.region = region_entry->second; - } else { - LOGGER.info("Region missing or empty, using default EU"); - properties.region = "EU"; - } - LOGGER.info("System settings loaded"); return true; } @@ -82,7 +72,6 @@ bool saveSystemSettings(const SystemSettings& properties) { map["language"] = toString(properties.language); map["timeFormat24h"] = properties.timeFormat24h ? "true" : "false"; map["dateFormat"] = properties.dateFormat; - map["region"] = properties.region; if (!file::savePropertiesFile(file_path, map)) { LOGGER.error("Failed to save {}", file_path); diff --git a/Tactility/Source/settings/time.cpp b/Tactility/Source/settings/time.cpp index 8a9de319..76088601 100644 --- a/Tactility/Source/settings/time.cpp +++ b/Tactility/Source/settings/time.cpp @@ -49,6 +49,15 @@ std::string getTimeZoneName() { } } +bool hasTimeZone() { + Preferences preferences(TIME_SETTINGS_NAMESPACE); + std::string timezone; + if (!preferences.optString(TIMEZONE_PREFERENCES_KEY_NAME, timezone)) { + return false; + } + return !timezone.empty(); +} + std::string getTimeZoneCode() { Preferences preferences(TIME_SETTINGS_NAMESPACE); std::string result; diff --git a/device.py b/device.py index 1bd0bac2..e295a411 100644 --- a/device.py +++ b/device.py @@ -119,6 +119,8 @@ def write_tactility_variables(output_file, device_properties: ConfigParser, devi output_file.write(f"CONFIG_TT_DEVICE_NAME=\"{board_name}\"\n") else: output_file.write(f"CONFIG_TT_DEVICE_NAME=\"{board_vendor} {board_name}\"\n") + output_file.write(f"CONFIG_TT_DEVICE_NAME_SIMPLE=\"{board_name}\"\n") + output_file.write(f"CONFIG_TT_DEVICE_VENDOR=\"{board_vendor}\"\n") output_file.write(f"CONFIG_TT_DEVICE_ID=\"{device_id}\"\n") if device_id == "lilygo-tdeck": output_file.write("CONFIG_TT_TDECK_WORKAROUND=y\n")