diff --git a/firmware/src/apps/apps.cpp b/firmware/src/apps/apps.cpp index dc759ff0..e0b55b72 100644 --- a/firmware/src/apps/apps.cpp +++ b/firmware/src/apps/apps.cpp @@ -1,6 +1,7 @@ #pragma once #include "apps.h" #include "menu.h" +#include "onboarding.h" #include "settings.h" #include @@ -132,6 +133,44 @@ void Apps::reload(cJSON *apps_) cJSON_Delete(apps_); } +void Apps::createOnboarding() +{ + clear(); + + OnboardingApp *onboarding_app = new OnboardingApp(this->spr_); + uint16_t app_position = 0; + + onboarding_app->add_item( + app_position, + OnboardingItem{ + "SMART KNOB", + "DEV KIT V0.1", + 1, + spr_->color565(255, 255, 255), + nullptr, + nullptr, + "ROTATE TO START", + }); + + app_position++; + + onboarding_app->add_item( + app_position, + OnboardingItem{ + "HOME ASSISTANT", + "INTEGRATION", + 1, + spr_->color565(255, 255, 255), + nullptr, + nullptr, + "PRESS TO CONTINUE", + }); + + add(0, onboarding_app); + + // setActive(0); +} + void Apps::updateMenu() { // re - generate new menu based on loaded apps diff --git a/firmware/src/apps/apps.h b/firmware/src/apps/apps.h index db45db01..278688c6 100644 --- a/firmware/src/apps/apps.h +++ b/firmware/src/apps/apps.h @@ -11,11 +11,14 @@ #include "climate.h" #include "light_dimmer.h" #include "light_switch.h" -#include "menu.h" #include "music.h" #include "settings.h" #include "stopwatch.h" +// include all "menu" apps +#include "menu.h" +#include "onboarding.h" + // TODO: generate menu based on items in the map class Apps { @@ -31,7 +34,9 @@ class Apps void setSprite(TFT_eSprite *spr_); void loadApp(uint8_t position, std::string app_slug, std::string app_id, std::string friendly_name); void updateMenu(); + void reload(cJSON *apps_); + void createOnboarding(); private: QueueHandle_t mutex; diff --git a/firmware/src/apps/onboarding.cpp b/firmware/src/apps/onboarding.cpp new file mode 100644 index 00000000..f79d4cda --- /dev/null +++ b/firmware/src/apps/onboarding.cpp @@ -0,0 +1,144 @@ +#include "onboarding.h" + +OnboardingApp::OnboardingApp(TFT_eSprite *spr_) : App(spr_) +{ + // sprintf(room, "%s", ""); + + motor_config = PB_SmartKnobConfig{ + 0, + 0, + 0, + 0, + -1, // max position < min position indicates no bounds + 25 * PI / 180, + 2, + 1, + 0.55, + "SKDEMO_Menu", // TODO: clean this + 0, + {}, + 0, + 20, + }; +} + +EntityStateUpdate OnboardingApp::updateStateFromKnob(PB_SmartKnobState state) +{ + // TODO: cache menu size + + int32_t position_for_onboarding_calc = state.current_position; + + // needed to next reload of App + motor_config.position_nonce = position_for_onboarding_calc; + motor_config.position = position_for_onboarding_calc; + + if (state.current_position < 0) + { + position_for_onboarding_calc = items.size() * 10000 + state.current_position; + } + + current_onboarding_position = position_for_onboarding_calc % items.size(); + + return EntityStateUpdate{}; +} + +void OnboardingApp::updateStateFromSystem(AppState state) {} + +uint8_t OnboardingApp::navigationNext() +{ + return current_onboarding_position + 1; // +1 to shift from 0 position which is menu itself +} + +TFT_eSprite *OnboardingApp::render() +{ + OnboardingItem current_item = find_item(current_onboarding_position); + OnboardingItem prev_item; + OnboardingItem next_item; + if (current_onboarding_position == 0) + { + // prev_item = find_item(items.size() - 1); + next_item = find_item(current_onboarding_position + 1); + } + else if (current_onboarding_position == items.size() - 1) + { + prev_item = find_item(current_onboarding_position - 1); + // next_item = find_item(0); + } + else + { + prev_item = find_item(current_onboarding_position - 1); + next_item = find_item(current_onboarding_position + 1); + } + render_onboarding_screen(current_item, prev_item, next_item); + return this->spr_; +} + +void OnboardingApp::add_item(uint8_t id, OnboardingItem item) +{ + items[id] = item; + onboarding_items_count++; +} + +// TODO: add protection, could cause panic +OnboardingItem OnboardingApp::find_item(uint8_t id) +{ + return (*items.find(id)).second; +} + +void OnboardingApp::render_onboarding_screen(OnboardingItem current, OnboardingItem prev, OnboardingItem next) +{ + uint32_t color_active = current.color; + uint32_t color_inactive = spr_->color565(150, 150, 150); + uint32_t label_color = color_inactive; + uint32_t background = spr_->color565(0, 0, 0); + + uint16_t center_h = TFT_WIDTH / 2; + uint16_t center_v = TFT_WIDTH / 2; + + int8_t screen_name_label_w = 100; + int8_t screen_name_label_h = spr_->fontHeight(1); + int8_t label_vertical_offset = 25; + + uint8_t icon_size_active = 80; + uint8_t icon_size_inactive = 40; + + spr_->setTextDatum(CC_DATUM); + spr_->setTextSize(1); + spr_->setFreeFont(&NDS1210pt7b); + + // spr_->fillRect(center_h - room_lable_w / 2, label_vertical_offset, room_lable_w, room_lable_h + 1, label_color); // +1 for height to draw circle right + // spr_->fillCircle(center_h - room_lable_w / 2, label_vertical_offset + room_lable_h / 2, room_lable_h / 2, label_color); + // spr_->fillCircle(center_h + room_lable_w / 2, label_vertical_offset + room_lable_h / 2, room_lable_h / 2, label_color); + + if (current.big_icon == nullptr) + { + if (current.small_icon == nullptr) + { + spr_->setTextColor(color_active); + spr_->drawString(current.screen_name, center_v, center_h - screen_name_label_h * 2, 1); + spr_->drawString(current.screen_description, center_v, center_h - screen_name_label_h, 1); + + spr_->setTextColor(spr_->color565(128, 255, 80)); + spr_->drawString(current.call_to_action, center_v, center_v + icon_size_active / 2, 1); + } + else + { + spr_->setTextColor(color_active); + spr_->drawString(current.screen_name, center_v, label_vertical_offset + screen_name_label_h / 2 - 1, 1); + + spr_->setTextColor(spr_->color565(128, 255, 80)); + spr_->drawString(current.call_to_action, center_v, center_v + icon_size_active / 2 + 30, 1); + } + } + + // spr_->drawString(room, center_h, label_vertical_offset + room_lable_h / 2 - 1, 1); + + // spr_->drawBitmap(center_h - icon_size_active / 2, center_v - icon_size_active / 2, current.big_icon, icon_size_active, icon_size_active, color_active, background); + // ESP_LOGD("menu.cpp", "%s", current.screen_name); + + // left one + // spr_->drawBitmap(center_h - icon_size_active / 2 - 20 - icon_size_inactive, center_v - icon_size_inactive / 2, prev.small_icon, icon_size_inactive, icon_size_inactive, color_inactive, background); + + // right one + // spr_->drawBitmap(center_h + icon_size_active / 2 + 20, center_v - icon_size_inactive / 2, next.small_icon, icon_size_inactive, icon_size_inactive, color_inactive, background); +}; \ No newline at end of file diff --git a/firmware/src/apps/onboarding.h b/firmware/src/apps/onboarding.h new file mode 100644 index 00000000..99baaf40 --- /dev/null +++ b/firmware/src/apps/onboarding.h @@ -0,0 +1,36 @@ +#pragma once + +#include "app.h" +#include "font/NDS1210pt7b.h" + +#include + +struct OnboardingItem +{ + const char *screen_name; + const char *screen_description; + uint16_t app_id; + uint32_t color; + const unsigned char *small_icon; + const unsigned char *big_icon; + const char *call_to_action; +}; + +class OnboardingApp : public App +{ +public: + OnboardingApp(TFT_eSprite *spr_); + TFT_eSprite *render(); + EntityStateUpdate updateStateFromKnob(PB_SmartKnobState state); + void updateStateFromSystem(AppState state); + uint8_t navigationNext(); + void add_item(uint8_t id, OnboardingItem item); + OnboardingItem find_item(uint8_t id); + +private: + std::map items; + uint8_t onboarding_items_count = 0; + uint8_t current_onboarding_position = 0; + + void render_onboarding_screen(OnboardingItem current, OnboardingItem prev, OnboardingItem next); +}; diff --git a/firmware/src/display_task.cpp b/firmware/src/display_task.cpp index df0fbdfa..8d0cd0d5 100644 --- a/firmware/src/display_task.cpp +++ b/firmware/src/display_task.cpp @@ -56,43 +56,45 @@ void DisplayTask::run() } spr_.setTextColor(0xFFFF, TFT_BLACK); - std::string apps_config = "[{\"app_slug\":\"stopwatch\",\"app_id\":\"stopwatch.office\",\"friendly_name\":\"Stopwatch\",\"area\":\"office\",\"menu_color\":\"#ffffff\"},{\"app_slug\":\"light_switch\",\"app_id\":\"light.ceiling\",\"friendly_name\":\"Ceiling\",\"area\":\"Kitchen\",\"menu_color\":\"#ffffff\"},{\"app_slug\":\"light_dimmer\",\"app_id\":\"light.workbench\",\"friendly_name\":\"Workbench\",\"area\":\"Kitchen\",\"menu_color\":\"#ffffff\"},{\"app_slug\":\"thermostat\",\"app_id\":\"climate.office\",\"friendly_name\":\"Climate\",\"area\":\"Office\",\"menu_color\":\"#ffffff\"},{\"app_slug\":\"3d_printer\",\"app_id\":\"3d_printer.office\",\"friendly_name\":\"3D Printer\",\"area\":\"Office\",\"menu_color\":\"#ffffff\"},{\"app_slug\":\"blinds\",\"app_id\":\"blinds.office\",\"friendly_name\":\"Shades\",\"area\":\"Office\",\"menu_color\":\"#ffffff\"},{\"app_slug\":\"music\",\"app_id\":\"music.office\",\"friendly_name\":\"Music\",\"area\":\"Office\",\"menu_color\":\"#ffffff\"}]"; - // std::string apps_config = "[{\"app_slug\":\"stopwatch\",\"app_id\":\"stopwatch-office\",\"friendly_name\":\"Stopwatch\",\"area\":\"office\",\"menu_color\":\"#ffffff\"},{\"app_slug\":\"light_switch\",\"app_id\":\"light.ceiling\",\"friendly_name\":\"Ceiling\",\"area\":\"Kitchen\",\"menu_color\":\"#ffffff\"}]"; + // std::string apps_config = "[{\"app_slug\":\"stopwatch\",\"app_id\":\"stopwatch.office\",\"friendly_name\":\"Stopwatch\",\"area\":\"office\",\"menu_color\":\"#ffffff\"},{\"app_slug\":\"light_switch\",\"app_id\":\"light.ceiling\",\"friendly_name\":\"Ceiling\",\"area\":\"Kitchen\",\"menu_color\":\"#ffffff\"},{\"app_slug\":\"light_dimmer\",\"app_id\":\"light.workbench\",\"friendly_name\":\"Workbench\",\"area\":\"Kitchen\",\"menu_color\":\"#ffffff\"},{\"app_slug\":\"thermostat\",\"app_id\":\"climate.office\",\"friendly_name\":\"Climate\",\"area\":\"Office\",\"menu_color\":\"#ffffff\"},{\"app_slug\":\"3d_printer\",\"app_id\":\"3d_printer.office\",\"friendly_name\":\"3D Printer\",\"area\":\"Office\",\"menu_color\":\"#ffffff\"},{\"app_slug\":\"blinds\",\"app_id\":\"blinds.office\",\"friendly_name\":\"Shades\",\"area\":\"Office\",\"menu_color\":\"#ffffff\"},{\"app_slug\":\"music\",\"app_id\":\"music.office\",\"friendly_name\":\"Music\",\"area\":\"Office\",\"menu_color\":\"#ffffff\"}]"; + // // std::string apps_config = "[{\"app_slug\":\"stopwatch\",\"app_id\":\"stopwatch-office\",\"friendly_name\":\"Stopwatch\",\"area\":\"office\",\"menu_color\":\"#ffffff\"},{\"app_slug\":\"light_switch\",\"app_id\":\"light.ceiling\",\"friendly_name\":\"Ceiling\",\"area\":\"Kitchen\",\"menu_color\":\"#ffffff\"}]"; - cJSON *json_root = cJSON_Parse(apps_config.c_str()); + // cJSON *json_root = cJSON_Parse(apps_config.c_str()); - if (json_root == NULL) - { - ESP_LOGE("display_task.cpp", "failed to parse JSON"); - } + // if (json_root == NULL) + // { + // ESP_LOGE("display_task.cpp", "failed to parse JSON"); + // } apps.setSprite(&spr_); - cJSON *json_app = NULL; + // cJSON *json_app = NULL; - uint16_t app_position = 1; + // uint16_t app_position = 1; - cJSON_ArrayForEach(json_app, json_root) - { - cJSON *json_app_slug = cJSON_GetObjectItemCaseSensitive(json_app, "app_slug"); - cJSON *json_app_id = cJSON_GetObjectItemCaseSensitive(json_app, "app_id"); - cJSON *json_friendly_name = cJSON_GetObjectItemCaseSensitive(json_app, "friendly_name"); - snprintf(buf_, sizeof(buf_), "fromJSON > app_slug=%s", json_app_slug->valuestring); - log(buf_); - // ESP_LOGD("display_task.cpp", "%s", buf_); + // cJSON_ArrayForEach(json_app, json_root) + // { + // cJSON *json_app_slug = cJSON_GetObjectItemCaseSensitive(json_app, "app_slug"); + // cJSON *json_app_id = cJSON_GetObjectItemCaseSensitive(json_app, "app_id"); + // cJSON *json_friendly_name = cJSON_GetObjectItemCaseSensitive(json_app, "friendly_name"); + // snprintf(buf_, sizeof(buf_), "fromJSON > app_slug=%s", json_app_slug->valuestring); + // log(buf_); + // // ESP_LOGD("display_task.cpp", "%s", buf_); - apps.loadApp(app_position, std::string(json_app_slug->valuestring), std::string(json_app_id->valuestring), json_friendly_name->valuestring); + // apps.loadApp(app_position, std::string(json_app_slug->valuestring), std::string(json_app_id->valuestring), json_friendly_name->valuestring); - app_position++; - } + // app_position++; + // } - cJSON_Delete(json_root); + // cJSON_Delete(json_root); - SettingsApp *settings_app = new SettingsApp(&spr_); - apps.add(app_position, settings_app); + // SettingsApp *settings_app = new SettingsApp(&spr_); + // apps.add(app_position, settings_app); // generate menu from apps list - apps.updateMenu(); + // apps.updateMenu(); + + apps.createOnboarding(); AppState app_state;