diff options
| author | jacqueline <me@jacqueline.id.au> | 2024-05-29 14:45:49 +1000 |
|---|---|---|
| committer | jacqueline <me@jacqueline.id.au> | 2024-05-29 14:45:49 +1000 |
| commit | 2ff8eac022f397bb1aed28aca376fbe422fc8b3c (patch) | |
| tree | ae80d0d89a212b1badf1d971fc67e701a9e4e962 /src/tangara | |
| parent | ef812a53e5a84665e74be8c46cb983edaa712b3f (diff) | |
| download | tangara-fw-2ff8eac022f397bb1aed28aca376fbe422fc8b3c.tar.gz | |
Start on TTS support by logging the data that will become TTS lines
Includes some misc cleanup of haptic double-triggering (or
non-triggering), since those cases all end up being TTS event
double-reporting, which to me crosses the threshold from "annoying" to
"usability issue"
Diffstat (limited to 'src/tangara')
| -rw-r--r-- | src/tangara/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/tangara/input/device_factory.cpp | 6 | ||||
| -rw-r--r-- | src/tangara/input/feedback_device.hpp | 3 | ||||
| -rw-r--r-- | src/tangara/input/feedback_haptics.cpp | 9 | ||||
| -rw-r--r-- | src/tangara/input/feedback_haptics.hpp | 5 | ||||
| -rw-r--r-- | src/tangara/input/feedback_tts.cpp | 97 | ||||
| -rw-r--r-- | src/tangara/input/feedback_tts.hpp | 36 | ||||
| -rw-r--r-- | src/tangara/input/lvgl_input_driver.cpp | 14 | ||||
| -rw-r--r-- | src/tangara/input/lvgl_input_driver.hpp | 6 | ||||
| -rw-r--r-- | src/tangara/system_fsm/booting.cpp | 2 | ||||
| -rw-r--r-- | src/tangara/system_fsm/service_locator.hpp | 13 | ||||
| -rw-r--r-- | src/tangara/tts/events.hpp | 41 | ||||
| -rw-r--r-- | src/tangara/tts/provider.cpp | 38 | ||||
| -rw-r--r-- | src/tangara/tts/provider.hpp | 23 | ||||
| -rw-r--r-- | src/tangara/ui/lvgl_task.cpp | 4 |
15 files changed, 286 insertions, 13 deletions
diff --git a/src/tangara/CMakeLists.txt b/src/tangara/CMakeLists.txt index fb8d1041..628b7110 100644 --- a/src/tangara/CMakeLists.txt +++ b/src/tangara/CMakeLists.txt @@ -4,7 +4,7 @@ idf_component_register( SRC_DIRS "app_console" "audio" "battery" "database" "dev_console" "events" - "input" "lua" "system_fsm" "ui" + "input" "lua" "system_fsm" "ui" "tts" INCLUDE_DIRS "." REQUIRES "codecs" "drivers" "locale" "memory" "tasks" "util" "graphics" "tinyfsm" "lvgl" "esp_timer" "luavgl" "esp_app_format" "libcppbor" "libtags" diff --git a/src/tangara/input/device_factory.cpp b/src/tangara/input/device_factory.cpp index 8e1c5155..09fd2fd2 100644 --- a/src/tangara/input/device_factory.cpp +++ b/src/tangara/input/device_factory.cpp @@ -9,6 +9,7 @@ #include <memory> #include "input/feedback_haptics.hpp" +#include "input/feedback_tts.hpp" #include "input/input_device.hpp" #include "input/input_nav_buttons.hpp" #include "input/input_touch_dpad.hpp" @@ -52,7 +53,10 @@ auto DeviceFactory::createInputs(drivers::NvsStorage::InputModes mode) auto DeviceFactory::createFeedbacks() -> std::vector<std::shared_ptr<IFeedbackDevice>> { - return {std::make_shared<Haptics>(services_->haptics())}; + return { + std::make_shared<Haptics>(services_->haptics()), + std::make_shared<TextToSpeech>(services_->tts()), + }; } } // namespace input diff --git a/src/tangara/input/feedback_device.hpp b/src/tangara/input/feedback_device.hpp index 4faeeafd..8253642f 100644 --- a/src/tangara/input/feedback_device.hpp +++ b/src/tangara/input/feedback_device.hpp @@ -7,6 +7,7 @@ #pragma once #include <cstdint> +#include "core/lv_group.h" namespace input { @@ -23,7 +24,7 @@ class IFeedbackDevice { public: virtual ~IFeedbackDevice() {} - virtual auto feedback(uint8_t event_type) -> void = 0; + virtual auto feedback(lv_group_t* group, uint8_t event_type) -> void = 0; // TODO: Add configuration; likely the same shape of interface that // IInputDevice uses. diff --git a/src/tangara/input/feedback_haptics.cpp b/src/tangara/input/feedback_haptics.cpp index e690eb9f..c834ca54 100644 --- a/src/tangara/input/feedback_haptics.cpp +++ b/src/tangara/input/feedback_haptics.cpp @@ -8,6 +8,7 @@ #include <cstdint> +#include "core/lv_group.h" #include "lvgl/lvgl.h" #include "core/lv_event.h" @@ -21,7 +22,13 @@ using Effect = drivers::Haptics::Effect; Haptics::Haptics(drivers::Haptics& haptics_) : haptics_(haptics_) {} -auto Haptics::feedback(uint8_t event_type) -> void { +auto Haptics::feedback(lv_group_t* group, uint8_t event_type) -> void { + lv_obj_t* obj = lv_group_get_focused(group); + if (obj == last_selection_) { + return; + } + last_selection_ = obj; + switch (event_type) { case LV_EVENT_FOCUSED: haptics_.PlayWaveformEffect(Effect::kMediumClick1_100Pct); diff --git a/src/tangara/input/feedback_haptics.hpp b/src/tangara/input/feedback_haptics.hpp index bde5f345..91d7ec3a 100644 --- a/src/tangara/input/feedback_haptics.hpp +++ b/src/tangara/input/feedback_haptics.hpp @@ -8,6 +8,8 @@ #include <cstdint> +#include "core/lv_obj.h" + #include "drivers/haptics.hpp" #include "input/feedback_device.hpp" @@ -17,10 +19,11 @@ class Haptics : public IFeedbackDevice { public: Haptics(drivers::Haptics& haptics_); - auto feedback(uint8_t event_type) -> void override; + auto feedback(lv_group_t*, uint8_t event_type) -> void override; private: drivers::Haptics& haptics_; + lv_obj_t* last_selection_; }; } // namespace input diff --git a/src/tangara/input/feedback_tts.cpp b/src/tangara/input/feedback_tts.cpp new file mode 100644 index 00000000..a9267aa8 --- /dev/null +++ b/src/tangara/input/feedback_tts.cpp @@ -0,0 +1,97 @@ +/* + * Copyright 2024 jacqueline <me@jacqueline.id.au> + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "input/feedback_tts.hpp" + +#include <cstdint> +#include <variant> + +#include "lvgl/lvgl.h" + +#include "core/lv_event.h" +#include "core/lv_group.h" +#include "core/lv_obj.h" +#include "core/lv_obj_class.h" +#include "core/lv_obj_tree.h" +#include "extra/widgets/list/lv_list.h" +#include "tts/events.hpp" +#include "widgets/lv_label.h" + +#include "tts/events.hpp" +#include "tts/provider.hpp" + +namespace input { + +TextToSpeech::TextToSpeech(tts::Provider& tts) + : tts_(tts), last_obj_(nullptr) {} + +auto TextToSpeech::feedback(lv_group_t* group, uint8_t event_type) -> void { + if (group != last_group_) { + last_group_ = group; + last_obj_ = nullptr; + if (group) { + tts_.feed(tts::SimpleEvent::kContextChanged); + } + } + + if (group) { + lv_obj_t* focused = lv_group_get_focused(group); + if (focused == last_obj_) { + return; + } + + last_obj_ = focused; + if (focused != nullptr) { + describe(*focused); + } + } +} + +auto TextToSpeech::describe(lv_obj_t& obj) -> void { + if (lv_obj_check_type(&obj, &lv_btn_class) || + lv_obj_check_type(&obj, &lv_list_btn_class)) { + auto desc = findDescription(obj); + tts_.feed(tts::SelectionChanged{ + .new_selection = + tts::SelectionChanged::Selection{ + .description = desc, + .is_interactive = true, + }, + }); + } else { + auto desc = findDescription(obj); + tts_.feed(tts::SelectionChanged{ + .new_selection = + tts::SelectionChanged::Selection{ + .description = desc, + .is_interactive = false, + }, + }); + } +} + +auto TextToSpeech::findDescription(lv_obj_t& obj) + -> std::optional<std::string> { + if (lv_obj_get_child_cnt(&obj) > 0) { + for (size_t i = 0; i < lv_obj_get_child_cnt(&obj); i++) { + auto res = findDescription(*lv_obj_get_child(&obj, i)); + if (res) { + return res; + } + } + } + + if (lv_obj_check_type(&obj, &lv_label_class)) { + std::string text = lv_label_get_text(&obj); + if (!text.empty()) { + return text; + } + } + + return {}; +} + +} // namespace input diff --git a/src/tangara/input/feedback_tts.hpp b/src/tangara/input/feedback_tts.hpp new file mode 100644 index 00000000..ddd83ff0 --- /dev/null +++ b/src/tangara/input/feedback_tts.hpp @@ -0,0 +1,36 @@ +/* + * Copyright 2024 jacqueline <me@jacqueline.id.au> + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include <cstdint> + +#include "core/lv_obj.h" +#include "drivers/haptics.hpp" + +#include "input/feedback_device.hpp" +#include "tts/events.hpp" +#include "tts/provider.hpp" + +namespace input { + +class TextToSpeech : public IFeedbackDevice { + public: + TextToSpeech(tts::Provider&); + + auto feedback(lv_group_t*, uint8_t event_type) -> void override; + + private: + tts::Provider& tts_; + + auto describe(lv_obj_t&) -> void; + auto findDescription(lv_obj_t&) -> std::optional<std::string>; + + lv_group_t* last_group_; + lv_obj_t* last_obj_; +}; + +} // namespace input diff --git a/src/tangara/input/lvgl_input_driver.cpp b/src/tangara/input/lvgl_input_driver.cpp index 8d10bb13..9c1ccff9 100644 --- a/src/tangara/input/lvgl_input_driver.cpp +++ b/src/tangara/input/lvgl_input_driver.cpp @@ -10,6 +10,8 @@ #include <memory> #include <variant> +#include "core/lv_event.h" +#include "core/lv_indev.h" #include "lua.hpp" #include "lvgl.h" @@ -91,6 +93,16 @@ LvglInputDriver::LvglInputDriver(drivers::NvsStorage& nvs, registration_ = lv_indev_drv_register(&driver_); } +auto LvglInputDriver::setGroup(lv_group_t* g) -> void { + if (!g) { + return; + } + lv_indev_set_group(registration_, g); + // Emit a synthetic 'focus' event for the current selection, since otherwise + // our feedback devices won't know that the selection changed. + feedback(LV_EVENT_FOCUSED); +} + auto LvglInputDriver::read(lv_indev_data_t* data) -> void { // TODO: we should pass lock state on to the individual devices, since they // may wish to either ignore the lock state, or power down until unlock. @@ -107,7 +119,7 @@ auto LvglInputDriver::feedback(uint8_t event) -> void { return; } for (auto&& device : feedbacks_) { - device->feedback(event); + device->feedback(registration_->group, event); } } diff --git a/src/tangara/input/lvgl_input_driver.hpp b/src/tangara/input/lvgl_input_driver.hpp index 8ede1855..0b6a7e76 100644 --- a/src/tangara/input/lvgl_input_driver.hpp +++ b/src/tangara/input/lvgl_input_driver.hpp @@ -17,12 +17,12 @@ #include "input/device_factory.hpp" #include "input/feedback_device.hpp" +#include "drivers/nvs.hpp" +#include "drivers/touchwheel.hpp" #include "input/input_device.hpp" #include "input/input_hook.hpp" #include "lua/lua_thread.hpp" #include "lua/property.hpp" -#include "drivers/nvs.hpp" -#include "drivers/touchwheel.hpp" namespace input { @@ -37,10 +37,10 @@ class LvglInputDriver { auto mode() -> lua::Property& { return mode_; } + auto setGroup(lv_group_t*) -> void; auto read(lv_indev_data_t* data) -> void; auto feedback(uint8_t) -> void; - auto registration() -> lv_indev_t* { return registration_; } auto lock(bool l) -> void { is_locked_ = l; } auto pushHooks(lua_State* L) -> int; diff --git a/src/tangara/system_fsm/booting.cpp b/src/tangara/system_fsm/booting.cpp index 22a0fcac..feba0dc0 100644 --- a/src/tangara/system_fsm/booting.cpp +++ b/src/tangara/system_fsm/booting.cpp @@ -38,6 +38,7 @@ #include "system_fsm/service_locator.hpp" #include "system_fsm/system_events.hpp" #include "tasks.hpp" +#include "tts/provider.hpp" #include "ui/ui_fsm.hpp" namespace system_fsm { @@ -99,6 +100,7 @@ auto Booting::entry() -> void { std::make_unique<audio::TrackQueue>(sServices->bg_worker())); sServices->tag_parser(std::make_unique<database::TagParserImpl>()); sServices->collator(locale::CreateCollator()); + sServices->tts(std::make_unique<tts::Provider>()); ESP_LOGI(kTag, "init bluetooth"); sServices->bluetooth(std::make_unique<drivers::Bluetooth>( diff --git a/src/tangara/system_fsm/service_locator.hpp b/src/tangara/system_fsm/service_locator.hpp index 5b2205eb..3d136f3a 100644 --- a/src/tangara/system_fsm/service_locator.hpp +++ b/src/tangara/system_fsm/service_locator.hpp @@ -10,17 +10,18 @@ #include "audio/track_queue.hpp" #include "battery/battery.hpp" -#include "drivers/bluetooth.hpp" #include "collation.hpp" #include "database/database.hpp" #include "database/tag_parser.hpp" +#include "drivers/bluetooth.hpp" #include "drivers/gpios.hpp" #include "drivers/haptics.hpp" #include "drivers/nvs.hpp" #include "drivers/samd.hpp" #include "drivers/storage.hpp" -#include "tasks.hpp" #include "drivers/touchwheel.hpp" +#include "tasks.hpp" +#include "tts/provider.hpp" namespace system_fsm { @@ -69,6 +70,13 @@ class ServiceLocator { auto battery(std::unique_ptr<battery::Battery> i) { battery_ = std::move(i); } + auto tts() -> tts::Provider& { + assert(tts_ != nullptr); + return *tts_; + } + + auto tts(std::unique_ptr<tts::Provider> i) { tts_ = std::move(i); } + auto touchwheel() -> std::optional<drivers::TouchWheel*> { if (!touchwheel_) { return {}; @@ -140,6 +148,7 @@ class ServiceLocator { std::unique_ptr<audio::TrackQueue> queue_; std::unique_ptr<battery::Battery> battery_; + std::unique_ptr<tts::Provider> tts_; std::shared_ptr<database::Database> database_; std::unique_ptr<database::ITagParser> tag_parser_; diff --git a/src/tangara/tts/events.hpp b/src/tangara/tts/events.hpp new file mode 100644 index 00000000..21199db1 --- /dev/null +++ b/src/tangara/tts/events.hpp @@ -0,0 +1,41 @@ +/* + * Copyright 2024 jacqueline <me@jacqueline.id.au> + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include <optional> +#include <string> +#include <variant> + +namespace tts { + +/* + * 'Simple' TTS events are events that do not have any extra event-specific + * details. + */ +enum class SimpleEvent { + /* + * Indicates that the screen's content has substantially changed. e.g. a new + * screen has been pushed. + */ + kContextChanged, +}; + +/* + * Event indicating that the currently selected LVGL object has changed. + */ +struct SelectionChanged { + struct Selection { + std::optional<std::string> description; + bool is_interactive; + }; + + std::optional<Selection> new_selection; +}; + +using Event = std::variant<SimpleEvent, SelectionChanged>; + +} // namespace tts diff --git a/src/tangara/tts/provider.cpp b/src/tangara/tts/provider.cpp new file mode 100644 index 00000000..7d33bae6 --- /dev/null +++ b/src/tangara/tts/provider.cpp @@ -0,0 +1,38 @@ +/* + * Copyright 2024 jacqueline <me@jacqueline.id.au> + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "tts/provider.hpp" + +#include <optional> +#include <string> +#include <variant> + +#include "esp_log.h" + +#include "tts/events.hpp" + +namespace tts { + +[[maybe_unused]] static constexpr char kTag[] = "tts"; + +Provider::Provider() {} + +auto Provider::feed(const Event& e) -> void { + if (std::holds_alternative<SimpleEvent>(e)) { + // ESP_LOGI(kTag, "context changed"); + } else if (std::holds_alternative<SelectionChanged>(e)) { + auto ev = std::get<SelectionChanged>(e); + if (!ev.new_selection) { + // ESP_LOGI(kTag, "no selection"); + } else { + // ESP_LOGI(kTag, "new selection: '%s', interactive? %i", + // ev.new_selection->description.value_or("").c_str(), + // ev.new_selection->is_interactive); + } + } +} + +} // namespace tts diff --git a/src/tangara/tts/provider.hpp b/src/tangara/tts/provider.hpp new file mode 100644 index 00000000..59f61a6c --- /dev/null +++ b/src/tangara/tts/provider.hpp @@ -0,0 +1,23 @@ +/* + * Copyright 2024 jacqueline <me@jacqueline.id.au> + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include <optional> +#include <string> +#include <variant> + +#include "tts/events.hpp" + +namespace tts { + +class Provider { + public: + Provider(); + auto feed(const Event&) -> void; +}; + +} // namespace tts diff --git a/src/tangara/ui/lvgl_task.cpp b/src/tangara/ui/lvgl_task.cpp index 448aa261..4d12b24b 100644 --- a/src/tangara/ui/lvgl_task.cpp +++ b/src/tangara/ui/lvgl_task.cpp @@ -68,14 +68,14 @@ auto UiTask::Main() -> void { if (screen != current_screen_ && screen != nullptr) { lv_scr_load(screen->root()); if (input_) { - lv_indev_set_group(input_->registration(), screen->group()); + input_->setGroup(screen->group()); } current_screen_ = screen; } if (input_ && current_screen_->group() != current_group) { current_group = current_screen_->group(); - lv_indev_set_group(input_->registration(), current_group); + input_->setGroup(current_group); } TickType_t delay = lv_timer_handler(); |
