summaryrefslogtreecommitdiff
path: root/src/tangara
diff options
context:
space:
mode:
authorjacqueline <me@jacqueline.id.au>2024-05-29 14:45:49 +1000
committerjacqueline <me@jacqueline.id.au>2024-05-29 14:45:49 +1000
commit2ff8eac022f397bb1aed28aca376fbe422fc8b3c (patch)
treeae80d0d89a212b1badf1d971fc67e701a9e4e962 /src/tangara
parentef812a53e5a84665e74be8c46cb983edaa712b3f (diff)
downloadtangara-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.txt2
-rw-r--r--src/tangara/input/device_factory.cpp6
-rw-r--r--src/tangara/input/feedback_device.hpp3
-rw-r--r--src/tangara/input/feedback_haptics.cpp9
-rw-r--r--src/tangara/input/feedback_haptics.hpp5
-rw-r--r--src/tangara/input/feedback_tts.cpp97
-rw-r--r--src/tangara/input/feedback_tts.hpp36
-rw-r--r--src/tangara/input/lvgl_input_driver.cpp14
-rw-r--r--src/tangara/input/lvgl_input_driver.hpp6
-rw-r--r--src/tangara/system_fsm/booting.cpp2
-rw-r--r--src/tangara/system_fsm/service_locator.hpp13
-rw-r--r--src/tangara/tts/events.hpp41
-rw-r--r--src/tangara/tts/provider.cpp38
-rw-r--r--src/tangara/tts/provider.hpp23
-rw-r--r--src/tangara/ui/lvgl_task.cpp4
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();