From 2ff8eac022f397bb1aed28aca376fbe422fc8b3c Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 29 May 2024 14:45:49 +1000 Subject: [PATCH] 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" --- lib/luavgl/src/group.c | 9 + lua/playing.lua | 18 ++ lua/widgets.lua | 303 +++++++++++---------- src/tangara/CMakeLists.txt | 2 +- src/tangara/input/device_factory.cpp | 6 +- src/tangara/input/feedback_device.hpp | 3 +- src/tangara/input/feedback_haptics.cpp | 9 +- src/tangara/input/feedback_haptics.hpp | 5 +- src/tangara/input/feedback_tts.cpp | 97 +++++++ src/tangara/input/feedback_tts.hpp | 36 +++ src/tangara/input/lvgl_input_driver.cpp | 14 +- src/tangara/input/lvgl_input_driver.hpp | 6 +- src/tangara/system_fsm/booting.cpp | 2 + src/tangara/system_fsm/service_locator.hpp | 13 +- src/tangara/tts/events.hpp | 41 +++ src/tangara/tts/provider.cpp | 38 +++ src/tangara/tts/provider.hpp | 23 ++ src/tangara/ui/lvgl_task.cpp | 4 +- 18 files changed, 471 insertions(+), 158 deletions(-) create mode 100644 src/tangara/input/feedback_tts.cpp create mode 100644 src/tangara/input/feedback_tts.hpp create mode 100644 src/tangara/tts/events.hpp create mode 100644 src/tangara/tts/provider.cpp create mode 100644 src/tangara/tts/provider.hpp diff --git a/lib/luavgl/src/group.c b/lib/luavgl/src/group.c index 7d88bbe9..9dabbad4 100644 --- a/lib/luavgl/src/group.c +++ b/lib/luavgl/src/group.c @@ -248,6 +248,14 @@ static int luavgl_group_get_focused(lua_State *L) return 1; } +static int luavgl_group_clear_focus(lua_State *L) +{ + luavgl_group_t *g = luavgl_check_group(L, 1); + g->group->obj_focus = NULL; + + return 0; +} + static int luavgl_group_remove_obj(lua_State *L) { lv_obj_t *obj = luavgl_to_obj(L, 1); @@ -319,6 +327,7 @@ static const luaL_Reg group_methods[] = { {"get_wrap", luavgl_group_get_wrap }, {"get_obj_count", luavgl_group_get_obj_count}, {"get_focused", luavgl_group_get_focused }, + {"clear_focus", luavgl_group_clear_focus }, {NULL, NULL }, }; diff --git a/lua/playing.lua b/lua/playing.lua index 8d89ad5f..d3951403 100644 --- a/lua/playing.lua +++ b/lua/playing.lua @@ -126,6 +126,7 @@ return screen:new { range = { min = 0, max = 100 }, value = 0, } + local scrubber_desc = widgets.Description(scrubber, "Scrubber") scrubber:onevent(lvgl.EVENT.RELEASED, function() local track = playback.track:get() @@ -165,6 +166,7 @@ return screen:new { end) local repeat_img = repeat_btn:Image { src = img.repeat_src } theme.set_style(repeat_img, icon_enabled_class) + local repeat_desc = widgets.Description(repeat_btn) local prev_btn = controls:Button {} @@ -177,6 +179,7 @@ return screen:new { end) local prev_img = prev_btn:Image { src = img.prev } theme.set_style(prev_img, icon_enabled_class) + local prev_desc = widgets.Description(prev_btn, "Previous track") local play_pause_btn = controls:Button {} play_pause_btn:onClicked(function() @@ -185,11 +188,13 @@ return screen:new { play_pause_btn:focus() local play_pause_img = play_pause_btn:Image { src = img.pause } theme.set_style(play_pause_img, icon_enabled_class) + local play_pause_desc = widgets.Description(play_pause_btn, "Play") local next_btn = controls:Button {} next_btn:onClicked(queue.next) local next_img = next_btn:Image { src = img.next } theme.set_style(next_img, icon_disabled_class) + local next_desc = widgets.Description(next_btn, "Next track") local shuffle_btn = controls:Button {} shuffle_btn:onClicked(function() @@ -197,6 +202,7 @@ return screen:new { end) local shuffle_img = shuffle_btn:Image { src = img.shuffle } theme.set_style(shuffle_img, icon_enabled_class) + local shuffle_desc = widgets.Description(shuffle_btn) controls:Object({ flex_grow = 1, h = 1 }) -- spacer @@ -205,8 +211,10 @@ return screen:new { playback.playing:bind(function(playing) if playing then play_pause_img:set_src(img.pause) + play_pause_desc:set { text = "Pause" } else play_pause_img:set_src(img.play) + play_pause_desc:set { text = "Play" } end end), playback.position:bind(function(pos) @@ -241,9 +249,19 @@ return screen:new { end), queue.random:bind(function(shuffling) theme.set_style(shuffle_img, shuffling and icon_enabled_class or icon_disabled_class) + if shuffling then + shuffle_desc:set { text = "Disable shuffle" } + else + shuffle_desc:set { text = "Enable shuffle" } + end end), queue.repeat_track:bind(function(en) theme.set_style(repeat_img, en and icon_enabled_class or icon_disabled_class) + if en then + repeat_desc:set { text = "Disable track repeat" } + else + repeat_desc:set { text = "Enable track repeat" } + end end), queue.size:bind(function(num) if not num then return end diff --git a/lua/widgets.lua b/lua/widgets.lua index 0aa3e0b4..96896636 100644 --- a/lua/widgets.lua +++ b/lua/widgets.lua @@ -10,21 +10,30 @@ local theme = require("theme") local screen = require("screen") local img = { - db = lvgl.ImgData("//lua/img/db.png"), - chg = lvgl.ImgData("//lua/img/bat/chg.png"), - bat_100 = lvgl.ImgData("//lua/img/bat/100.png"), - bat_80 = lvgl.ImgData("//lua/img/bat/80.png"), - bat_60 = lvgl.ImgData("//lua/img/bat/60.png"), - bat_40 = lvgl.ImgData("//lua/img/bat/40.png"), - bat_20 = lvgl.ImgData("//lua/img/bat/20.png"), - bat_0 = lvgl.ImgData("//lua/img/bat/0.png"), - bat_0chg = lvgl.ImgData("//lua/img/bat/0chg.png"), - bt_conn = lvgl.ImgData("//lua/assets/bt_conn.png"), - bt = lvgl.ImgData("//lua/assets/bt.png") + db = lvgl.ImgData("//lua/img/db.png"), + chg = lvgl.ImgData("//lua/img/bat/chg.png"), + bat_100 = lvgl.ImgData("//lua/img/bat/100.png"), + bat_80 = lvgl.ImgData("//lua/img/bat/80.png"), + bat_60 = lvgl.ImgData("//lua/img/bat/60.png"), + bat_40 = lvgl.ImgData("//lua/img/bat/40.png"), + bat_20 = lvgl.ImgData("//lua/img/bat/20.png"), + bat_0 = lvgl.ImgData("//lua/img/bat/0.png"), + bat_0chg = lvgl.ImgData("//lua/img/bat/0chg.png"), + bt_conn = lvgl.ImgData("//lua/assets/bt_conn.png"), + bt = lvgl.ImgData("//lua/assets/bt.png") } local widgets = {} +function widgets.Description(obj, text) + local label = obj:Label {} + if text then + label:set { text = text } + end + label:add_flag(lvgl.FLAG.HIDDEN) + return label +end + widgets.MenuScreen = screen:new { show_back = false, title = "", @@ -50,24 +59,24 @@ widgets.MenuScreen = screen:new { } function widgets.Row(parent, left, right) - local container = parent:Object{ - flex = { - flex_direction = "row", - justify_content = "flex-start", - align_items = "flex-start", - align_content = "flex-start" - }, - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT - } - container:add_style(styles.list_item) - container:Label{ - text = left, - flex_grow = 1 - } - container:Label{ - text = right - } + local container = parent:Object { + flex = { + flex_direction = "row", + justify_content = "flex-start", + align_items = "flex-start", + align_content = "flex-start" + }, + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT + } + container:add_style(styles.list_item) + container:Label { + text = left, + flex_grow = 1 + } + container:Label { + text = right + } end local bindings_meta = { @@ -102,7 +111,8 @@ function widgets.StatusBar(parent, opts) w = lvgl.SIZE_CONTENT, h = 12, } - back:Label({ text = "<", align = lvgl.ALIGN.CENTER }) + local label = back:Label({ text = "<", align = lvgl.ALIGN.CENTER }) + widgets.Description(label, "Back") back:onClicked(opts.back_cb) end @@ -125,8 +135,8 @@ function widgets.StatusBar(parent, opts) local charge_icon = battery_icon:Image { src = img.chg } charge_icon:center() - local is_charging = nil - local percent = nil + local is_charging = nil + local percent = nil local function update_battery_icon() if is_charging == nil or percent == nil then return end @@ -191,135 +201,138 @@ function widgets.StatusBar(parent, opts) end function widgets.IconBtn(parent, icon, text) - local btn = parent:Button{ - flex = { - flex_direction = "row", - justify_content = "flex-start", - align_items = "center", - align_content = "center" - }, - w = lvgl.SIZE_CONTENT, - h = lvgl.SIZE_CONTENT, - pad_top = 1, - pad_bottom = 1, - pad_left = 1, - pad_column = 1 - } - btn:Image{ - src = icon - } - btn:Label{ - text = text, - text_font = font.fusion_10 - } - return btn + local btn = parent:Button { + flex = { + flex_direction = "row", + justify_content = "flex-start", + align_items = "center", + align_content = "center" + }, + w = lvgl.SIZE_CONTENT, + h = lvgl.SIZE_CONTENT, + pad_top = 1, + pad_bottom = 1, + pad_left = 1, + pad_column = 1 + } + btn:Image { + src = icon + } + btn:Label { + text = text, + text_font = font.fusion_10 + } + return btn end function widgets.InfiniteList(parent, iterator, opts) - local infinite_list = {} + local infinite_list = {} - infinite_list.root = lvgl.List(parent, { - w = lvgl.PCT(100), - h = lvgl.PCT(100), - flex_grow = 1, - scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF - }) + infinite_list.root = lvgl.List(parent, { + w = lvgl.PCT(100), + h = lvgl.PCT(100), + flex_grow = 1, + scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF + }) - local refreshing = false -- Used so that we can ignore focus events during this phase - local function refresh_group() - refreshing = true - local group = lvgl.group.get_default() - local focused_obj = group:get_focused() - local num_children = infinite_list.root:get_child_cnt() - -- remove all children from the group and re-add them - for i = 0, num_children-1 do - lvgl.group.remove_obj(infinite_list.root:get_child(i)) - end - for i = 0, num_children-1 do - group:add_obj(infinite_list.root:get_child(i)) - end - if (focused_obj) then - lvgl.group.focus_obj(focused_obj) - end - refreshing = false + local refreshing = false -- Used so that we can ignore focus events during this phase + local function refresh_group() + refreshing = true + local group = lvgl.group.get_default() + local focused_obj = group:get_focused() + local num_children = infinite_list.root:get_child_cnt() + if (focused_obj) then + group:clear_focus() + end + -- remove all children from the group and re-add them + for i = 0, num_children - 1 do + lvgl.group.remove_obj(infinite_list.root:get_child(i)) + end + for i = 0, num_children - 1 do + group:add_obj(infinite_list.root:get_child(i)) + end + if (focused_obj) then + lvgl.group.focus_obj(focused_obj) end + refreshing = false + end - local fwd_iterator = iterator:clone() - local bck_iterator = iterator:clone() + local fwd_iterator = iterator:clone() + local bck_iterator = iterator:clone() - local last_selected = 0 - local last_index = 0 - local first_index = 0 + local last_selected = 0 + local last_index = 0 + local first_index = 0 - local function remove_top() - local obj = infinite_list.root:get_child(0) - obj:delete() - first_index = first_index + 1 - bck_iterator:next() - end + local function remove_top() + local obj = infinite_list.root:get_child(0) + obj:delete() + first_index = first_index + 1 + bck_iterator:next() + end - local function remove_last() - local obj = infinite_list.root:get_child(-1) - obj:delete() - last_index = last_index - 1 - fwd_iterator:prev() - end + local function remove_last() + local obj = infinite_list.root:get_child(-1) + obj:delete() + last_index = last_index - 1 + fwd_iterator:prev() + end - local function add_item(item, index) - if not item then - return - end - local this_item = index - local add_to_top = false - if this_item < first_index then - first_index = this_item - add_to_top = true - end - if this_item > last_index then last_index = index end - local btn = infinite_list.root:add_btn(nil, tostring(item)) - if add_to_top then - btn:move_to_index(0) - end - -- opts.callback should take an item and return a function matching the arg of onClicked - if opts.callback then - btn:onClicked(opts.callback(item)) + local function add_item(item, index) + if not item then + return + end + local this_item = index + local add_to_top = false + if this_item < first_index then + first_index = this_item + add_to_top = true + end + if this_item > last_index then last_index = index end + local btn = infinite_list.root:add_btn(nil, tostring(item)) + if add_to_top then + btn:move_to_index(0) + end + -- opts.callback should take an item and return a function matching the arg of onClicked + if opts.callback then + btn:onClicked(opts.callback(item)) + end + btn:onevent(lvgl.EVENT.FOCUSED, function() + if refreshing then return end + if this_item > last_selected and this_item - first_index > 3 then + -- moving forward + local to_add = fwd_iterator:next() + if to_add then + remove_top() + add_item(to_add, last_index + 1) end - btn:onevent(lvgl.EVENT.FOCUSED, function() - if refreshing then return end - if this_item > last_selected and this_item - first_index > 3 then - -- moving forward - local to_add = fwd_iterator:next() - if to_add then - remove_top() - add_item(to_add, last_index+1) - end - end - if this_item < last_selected then - -- moving backward - if (first_index > 0 and last_index - this_item > 3) then - local to_add = bck_iterator:prev(); - if to_add then - remove_last() - add_item(to_add, first_index-1) - refresh_group() - end - end + end + if this_item < last_selected then + -- moving backward + if (first_index > 0 and last_index - this_item > 3) then + local to_add = bck_iterator:prev(); + if to_add then + remove_last() + add_item(to_add, first_index - 1) + refresh_group() end - last_selected = this_item - end) - btn:add_style(styles.list_item) - return btn - end - - for idx = 0, 8 do - local val = fwd_iterator() - if not val then - break end - add_item(val, idx) + end + last_selected = this_item + end) + btn:add_style(styles.list_item) + return btn + end + + for idx = 0, 8 do + local val = fwd_iterator() + if not val then + break end + add_item(val, idx) + end - return infinite_list + return infinite_list end return widgets 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 #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> { - return {std::make_shared(services_->haptics())}; + return { + std::make_shared(services_->haptics()), + std::make_shared(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 +#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 +#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 +#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 + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "input/feedback_tts.hpp" + +#include +#include + +#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 { + 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 + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include + +#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; + + 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 #include +#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(sServices->bg_worker())); sServices->tag_parser(std::make_unique()); sServices->collator(locale::CreateCollator()); + sServices->tts(std::make_unique()); ESP_LOGI(kTag, "init bluetooth"); sServices->bluetooth(std::make_unique( 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 i) { battery_ = std::move(i); } + auto tts() -> tts::Provider& { + assert(tts_ != nullptr); + return *tts_; + } + + auto tts(std::unique_ptr i) { tts_ = std::move(i); } + auto touchwheel() -> std::optional { if (!touchwheel_) { return {}; @@ -140,6 +148,7 @@ class ServiceLocator { std::unique_ptr queue_; std::unique_ptr battery_; + std::unique_ptr tts_; std::shared_ptr database_; std::unique_ptr 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 + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include +#include + +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 description; + bool is_interactive; + }; + + std::optional new_selection; +}; + +using Event = std::variant; + +} // 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 + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "tts/provider.hpp" + +#include +#include +#include + +#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(e)) { + // ESP_LOGI(kTag, "context changed"); + } else if (std::holds_alternative(e)) { + auto ev = std::get(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 + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include +#include + +#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();