From 71b46730394979ea528d152dbe884cc35c368759 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 17 Jan 2024 11:48:40 +1100 Subject: [PATCH] all screens basically working, but bluetooth is rough --- lua/licenses.lua | 5 +- lua/settings.lua | 149 +++++++++++++++----- src/audio/audio_fsm.cpp | 25 +++- src/audio/include/audio_events.hpp | 4 +- src/drivers/CMakeLists.txt | 10 +- src/drivers/bluetooth.cpp | 55 ++++++-- src/drivers/include/bluetooth.hpp | 10 +- src/drivers/include/bluetooth_types.hpp | 2 + src/drivers/include/nvs.hpp | 2 +- src/lua/CMakeLists.txt | 2 +- src/lua/bridge.cpp | 2 + src/lua/include/lua_controls.hpp | 15 ++ src/lua/include/property.hpp | 13 +- src/lua/lua_controls.cpp | 58 ++++++++ src/lua/property.cpp | 78 ++++++++++- src/system_fsm/CMakeLists.txt | 6 +- src/system_fsm/booting.cpp | 4 +- src/system_fsm/include/system_events.hpp | 5 +- src/ui/include/ui_fsm.hpp | 53 ++++--- src/ui/ui_fsm.cpp | 171 ++++++++++++++++++++--- 20 files changed, 557 insertions(+), 112 deletions(-) create mode 100644 src/lua/include/lua_controls.hpp create mode 100644 src/lua/lua_controls.cpp diff --git a/lua/licenses.lua b/lua/licenses.lua index a86c1c71..0dd29cc9 100644 --- a/lua/licenses.lua +++ b/lua/licenses.lua @@ -80,7 +80,10 @@ end return function() - local menu = widgets.MenuScreen(true, "Licenses") + local menu = widgets.MenuScreen({ + show_back = true, + title = "Licenses" + }) local container = menu.root:Object { flex = { diff --git a/lua/settings.lua b/lua/settings.lua index 291cde6b..bc4cfa1c 100644 --- a/lua/settings.lua +++ b/lua/settings.lua @@ -3,6 +3,9 @@ local backstack = require("backstack") local widgets = require("widgets") local theme = require("theme") local volume = require("volume") +local display = require("display") +local controls = require("controls") +local bluetooth = require("bluetooth") local settings = {} @@ -34,16 +37,26 @@ function settings.bluetooth() flex = { flex_direction = "row", justify_content = "flex-start", - align_items = "flex-start", + align_items = "content", align_content = "flex-start", }, w = lvgl.PCT(100), h = lvgl.SIZE_CONTENT, + pad_bottom = 1, } enable_container:Label { text = "Enable", flex_grow = 1 } - enable_container:Switch {} + local enable_sw = enable_container:Switch {} + enable_sw:onevent(lvgl.EVENT.VALUE_CHANGED, function() + local enabled = enable_sw:enabled() + bluetooth.enabled:set(enabled) + end) + + menu.content:Label { + text = "Paired Device", + pad_bottom = 1, + }:add_style(theme.settings_title) - local preferred_container = menu.content:Object { + local paired_container = menu.content:Object { flex = { flex_direction = "row", justify_content = "flex-start", @@ -52,25 +65,50 @@ function settings.bluetooth() }, w = lvgl.PCT(100), h = lvgl.SIZE_CONTENT, + pad_bottom = 2, } - preferred_container:add_style(theme.settings_title) - preferred_container:Label { - text = "Preferred Device", - flex_grow = 1, - } - preferred_container:Label { text = "x" } - local preferred_device = menu.content:Label { - text = "My Cool Speakers", + local paired_device = paired_container:Label { + flex_grow = 1, } + local clear_paired = paired_container:Button {} + clear_paired:Label { text = "x" } + clear_paired:onClicked(function() + print("clear dev") + bluetooth.paired_device:set() + end) menu.content:Label { - text = "Available Devices", + text = "Nearby Devices", + pad_bottom = 1, }:add_style(theme.settings_title) - local known_devices = menu.content:List { + local devices = menu.content:List { w = lvgl.PCT(100), - flex_grow = 1, + h = lvgl.SIZE_CONTENT, + } + + menu.bindings = { + bluetooth.enabled:bind(function(en) + enable_sw:set { enabled = en } + end), + bluetooth.paired_device:bind(function(device) + if device then + paired_device:set { text = device.name } + clear_paired:clear_flag(lvgl.FLAG.HIDDEN) + else + paired_device:set { text = "None" } + clear_paired:add_flag(lvgl.FLAG.HIDDEN) + end + end), + bluetooth.devices:bind(function(devs) + devices:clean() + for _, dev in pairs(devs) do + devices:add_btn(nil, dev.name):onClicked(function() + bluetooth.paired_device:set(dev) + end) + end + end) } end @@ -87,8 +125,9 @@ function settings.headphones() } local limits = { -10, 6, 10 } volume_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function() - local selection = volume_chooser:get('selected') - volume.limit_db.set(limits[selection]) + -- luavgl dropdown binding uses 0-based indexing :( + local selection = volume_chooser:get('selected') + 1 + volume.limit_db:set(limits[selection]) end) menu.content:Label { @@ -98,7 +137,7 @@ function settings.headphones() local balance = menu.content:Slider { w = lvgl.PCT(100), h = 5, - range = { min = -5, max = 5 }, + range = { min = -100, max = 100 }, value = 0, } balance:onevent(lvgl.EVENT.VALUE_CHANGED, function() @@ -109,10 +148,9 @@ function settings.headphones() menu.bindings = { volume.limit_db:bind(function(limit) - print("new limit", limit) - for i=1,#limits do + for i = 1, #limits do if limits[i] == limit then - volume_chooser:set{ selected = i } + volume_chooser:set { selected = i - 1 } end end end), @@ -122,12 +160,14 @@ function settings.headphones() } if bias < 0 then balance_label:set { - text = string.format("Left -%.2fdB", bias / 4) + text = string.format("Left %.2fdB", bias / 4) } - else + elseif bias > 0 then balance_label:set { - text = string.format("Right -%.2fdB", -bias / 4) + text = string.format("Right %.2fdB", -bias / 4) } + else + balance_label:set { text = "Balanced" } end end), } @@ -138,19 +178,37 @@ end function settings.display() local menu = SettingsScreen("Display") - menu.content:Label { - text = "Brightness", - }:add_style(theme.settings_title) + local brightness_title = menu.content: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, + } + brightness_title:Label { text = "Brightness", flex_grow = 1 } + local brightness_pct = brightness_title:Label {} + brightness_pct:add_style(theme.settings_title) local brightness = menu.content:Slider { w = lvgl.PCT(100), h = 5, range = { min = 0, max = 100 }, - value = 50, + value = display.brightness:get(), } brightness:onevent(lvgl.EVENT.VALUE_CHANGED, function() - print("bright", brightness:value()) + display.brightness:set(brightness:value()) end) + + menu.bindings = { + display.brightness:bind(function(b) + brightness_pct:set { text = tostring(b) .. "%" } + end) + } + + return menu end function settings.input() @@ -160,14 +218,41 @@ function settings.input() text = "Control scheme", }:add_style(theme.settings_title) + local schemes = controls.schemes() + local option_to_scheme = {} + local scheme_to_option = {} + + local option_idx = 0 + local options = "" + + for i, v in pairs(schemes) do + option_to_scheme[option_idx] = i + scheme_to_option[i] = option_idx + if option_idx > 0 then + options = options .. "\n" + end + options = options .. v + option_idx = option_idx + 1 + end + local controls_chooser = menu.content:Dropdown { - options = "Buttons only\nD-Pad\nTouchwheel", - selected = 3, + options = options, + } + + menu.bindings = { + controls.scheme:bind(function(s) + local option = scheme_to_option[s] + controls_chooser:set({ selected = option }) + end) } + controls_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function() - local selection = controls_chooser:get('selected') - print("controls", selection) + local option = controls_chooser:get('selected') + local scheme = option_to_scheme[option] + controls.scheme:set(scheme) end) + + return menu end function settings.database() diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index b903a171..560e655a 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -87,13 +87,14 @@ void AudioState::react(const SetVolume& ev) { } void AudioState::react(const SetVolumeLimit& ev) { - ESP_LOGI(kTag, "new max volume %i db", - (ev.new_limit - drivers::wm8523::kLineLevelReferenceVolume) / 4); - sI2SOutput->SetMaxVolume(ev.new_limit); - sServices->nvs().AmpMaxVolume(ev.new_limit); + uint16_t limit_in_dac_units = + drivers::wm8523::kLineLevelReferenceVolume + (ev.limit_db * 4); + + sI2SOutput->SetMaxVolume(limit_in_dac_units); + sServices->nvs().AmpMaxVolume(limit_in_dac_units); events::Ui().Dispatch(VolumeLimitChanged{ - .new_limit = ev.new_limit, + .new_limit_db = ev.limit_db, }); events::Ui().Dispatch(VolumeChanged{ .percent = sOutput->GetVolumePct(), @@ -167,6 +168,20 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { } sOutput->SetMode(IAudioOutput::Modes::kOnPaused); + events::Ui().Dispatch(VolumeLimitChanged{ + .new_limit_db = + (static_cast(nvs.AmpMaxVolume()) - + static_cast(drivers::wm8523::kLineLevelReferenceVolume)) / + 4, + }); + events::Ui().Dispatch(VolumeChanged{ + .percent = sOutput->GetVolumePct(), + .db = sOutput->GetVolumeDb(), + }); + events::Ui().Dispatch(VolumeBalanceChanged{ + .left_bias = nvs.AmpLeftBias(), + }); + sSampleConverter.reset(new SampleConverter()); sSampleConverter->SetOutput(sOutput); diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp index 0e4d0bc9..b76d8c89 100644 --- a/src/audio/include/audio_events.hpp +++ b/src/audio/include/audio_events.hpp @@ -63,11 +63,11 @@ struct VolumeBalanceChanged : tinyfsm::Event { int left_bias; }; struct VolumeLimitChanged : tinyfsm::Event { - uint16_t new_limit; + int new_limit_db; }; struct SetVolumeLimit : tinyfsm::Event { - uint16_t new_limit; + int limit_db; }; struct TogglePlayPause : tinyfsm::Event {}; diff --git a/src/drivers/CMakeLists.txt b/src/drivers/CMakeLists.txt index 7d1e048d..4155ed66 100644 --- a/src/drivers/CMakeLists.txt +++ b/src/drivers/CMakeLists.txt @@ -3,9 +3,11 @@ # SPDX-License-Identifier: GPL-3.0-only idf_component_register( - SRCS "touchwheel.cpp" "i2s_dac.cpp" "gpios.cpp" "adc.cpp" "storage.cpp" "i2c.cpp" - "spi.cpp" "display.cpp" "display_init.cpp" "samd.cpp" "relative_wheel.cpp" "wm8523.cpp" - "nvs.cpp" "bluetooth.cpp" "haptics.cpp" "spiffs.cpp" + SRCS "touchwheel.cpp" "i2s_dac.cpp" "gpios.cpp" "adc.cpp" "storage.cpp" + "i2c.cpp" "bluetooth.cpp" "spi.cpp" "display.cpp" "display_init.cpp" + "samd.cpp" "relative_wheel.cpp" "wm8523.cpp" "nvs.cpp" "haptics.cpp" + "spiffs.cpp" INCLUDE_DIRS "include" - REQUIRES "esp_adc" "fatfs" "result" "lvgl" "span" "tasks" "nvs_flash" "bt" "tinyfsm" "spiffs") + REQUIRES "esp_adc" "fatfs" "result" "lvgl" "span" "tasks" "nvs_flash" "spiffs" + "bt" "tinyfsm") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/drivers/bluetooth.cpp b/src/drivers/bluetooth.cpp index a962a280..f58c98a3 100644 --- a/src/drivers/bluetooth.cpp +++ b/src/drivers/bluetooth.cpp @@ -80,6 +80,36 @@ auto Bluetooth::IsEnabled() -> bool { return !bluetooth::BluetoothState::is_in_state(); } +auto Bluetooth::IsConnected() -> bool { + return bluetooth::BluetoothState::is_in_state(); +} + +auto Bluetooth::ConnectedDevice() -> std::optional { + if (!IsConnected()) { + return {}; + } + auto looking_for = bluetooth::BluetoothState::preferred_device(); + for (const auto& dev : bluetooth::BluetoothState::devices()) { + if (dev.address == looking_for) { + return dev; + } + } + return {}; +} + +auto Bluetooth::SetDeviceDiscovery(bool allowed) -> void { + if (allowed == bluetooth::BluetoothState::discovery()) { + return; + } + bluetooth::BluetoothState::discovery(allowed); + tinyfsm::FsmList::dispatch( + bluetooth::events::DiscoveryChanged{}); +} + +auto Bluetooth::IsDiscovering() -> bool { + return bluetooth::BluetoothState::discovery(); +} + auto Bluetooth::KnownDevices() -> std::vector { std::vector out = bluetooth::BluetoothState::devices(); std::sort(out.begin(), out.end(), [](const auto& a, const auto& b) -> bool { @@ -97,13 +127,8 @@ auto Bluetooth::SetPreferredDevice(const bluetooth::mac_addr_t& mac) -> void { bluetooth::events::PreferredDeviceChanged{}); } -auto Bluetooth::SetDeviceDiscovery(bool allowed) -> void { - if (allowed == bluetooth::BluetoothState::discovery()) { - return; - } - bluetooth::BluetoothState::discovery(allowed); - tinyfsm::FsmList::dispatch( - bluetooth::events::DiscoveryChanged{}); +auto Bluetooth::PreferredDevice() -> std::optional { + return bluetooth::BluetoothState::preferred_device(); } auto Bluetooth::SetSource(StreamBufferHandle_t src) -> void { @@ -120,7 +145,7 @@ auto Bluetooth::SetEventHandler(std::function cb) bluetooth::BluetoothState::event_handler(cb); } -auto DeviceName() -> std::pmr::string { +static auto DeviceName() -> std::pmr::string { uint8_t mac[8]{0}; esp_efuse_mac_get_default(mac); std::ostringstream name; @@ -267,7 +292,7 @@ Scanner* BluetoothState::sScanner_; std::mutex BluetoothState::sDevicesMutex_{}; std::map BluetoothState::sDevices_{}; std::optional BluetoothState::sPreferredDevice_{}; -mac_addr_t BluetoothState::sCurrentDevice_{0}; +std::optional BluetoothState::sCurrentDevice_{}; bool BluetoothState::sIsDiscoveryAllowed_{false}; std::atomic BluetoothState::sSource_; @@ -331,7 +356,7 @@ auto BluetoothState::react(const events::DeviceDiscovered& ev) -> void { sDevices_[ev.device.address] = ev.device; if (ev.device.address == sPreferredDevice_) { - sCurrentDevice_ = ev.device.address; + sCurrentDevice_ = ev.device; is_preferred = true; } @@ -466,9 +491,17 @@ void Idle::react(const events::internal::Gap& ev) { void Connecting::entry() { ESP_LOGI(kTag, "connecting to device"); esp_a2d_source_connect(sPreferredDevice_.value().data()); + + if (sEventHandler_) { + std::invoke(sEventHandler_, Event::kConnectionStateChanged); + } } -void Connecting::exit() {} +void Connecting::exit() { + if (sEventHandler_) { + std::invoke(sEventHandler_, Event::kConnectionStateChanged); + } +} void Connecting::react(const events::Disable& ev) { // TODO: disconnect gracefully diff --git a/src/drivers/include/bluetooth.hpp b/src/drivers/include/bluetooth.hpp index f3623fb8..4aefbc42 100644 --- a/src/drivers/include/bluetooth.hpp +++ b/src/drivers/include/bluetooth.hpp @@ -32,14 +32,20 @@ class Bluetooth { auto Disable() -> void; auto IsEnabled() -> bool; + auto IsConnected() -> bool; + auto ConnectedDevice() -> std::optional; + /* * Sets whether or not the bluetooth stack is allowed to actively scan for * new devices. */ auto SetDeviceDiscovery(bool) -> void; + auto IsDiscovering() -> bool; auto KnownDevices() -> std::vector; + auto SetPreferredDevice(const bluetooth::mac_addr_t& mac) -> void; + auto PreferredDevice() -> std::optional; auto SetSource(StreamBufferHandle_t) -> void; auto SetEventHandler(std::function cb) -> void; @@ -100,9 +106,11 @@ class BluetoothState : public tinyfsm::Fsm { static auto Init(NvsStorage& storage) -> void; static auto devices() -> std::vector; + static auto preferred_device() -> std::optional; static auto preferred_device(std::optional) -> void; + static auto scanning() -> bool; static auto discovery() -> bool; static auto discovery(bool) -> void; @@ -135,7 +143,7 @@ class BluetoothState : public tinyfsm::Fsm { static std::mutex sDevicesMutex_; static std::map sDevices_; static std::optional sPreferredDevice_; - static mac_addr_t sCurrentDevice_; + static std::optional sCurrentDevice_; static bool sIsDiscoveryAllowed_; static std::atomic sSource_; diff --git a/src/drivers/include/bluetooth_types.hpp b/src/drivers/include/bluetooth_types.hpp index 7e26f0d7..518771e5 100644 --- a/src/drivers/include/bluetooth_types.hpp +++ b/src/drivers/include/bluetooth_types.hpp @@ -21,6 +21,8 @@ struct Device { enum class Event { kKnownDevicesChanged, kConnectionStateChanged, + kPreferredDeviceChanged, + kDiscoveryChanged, }; } // namespace bluetooth diff --git a/src/drivers/include/nvs.hpp b/src/drivers/include/nvs.hpp index b82013b5..bf0bebab 100644 --- a/src/drivers/include/nvs.hpp +++ b/src/drivers/include/nvs.hpp @@ -67,4 +67,4 @@ class NvsStorage { nvs_handle_t handle_; }; -} // namespace drivers \ No newline at end of file +} // namespace drivers diff --git a/src/lua/CMakeLists.txt b/src/lua/CMakeLists.txt index 7174757a..d89b22e8 100644 --- a/src/lua/CMakeLists.txt +++ b/src/lua/CMakeLists.txt @@ -4,7 +4,7 @@ idf_component_register( SRCS "lua_thread.cpp" "bridge.cpp" "property.cpp" "lua_database.cpp" - "lua_queue.cpp" "lua_version.cpp" + "lua_queue.cpp" "lua_version.cpp" "lua_controls.cpp" INCLUDE_DIRS "include" REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery" "esp-idf-lua" "luavgl" "lua-linenoise" "lua-term" diff --git a/src/lua/bridge.cpp b/src/lua/bridge.cpp index 82721d4e..a680a521 100644 --- a/src/lua/bridge.cpp +++ b/src/lua/bridge.cpp @@ -16,6 +16,7 @@ #include "lauxlib.h" #include "lua.h" #include "lua.hpp" +#include "lua_controls.hpp" #include "lua_database.hpp" #include "lua_queue.hpp" #include "lua_version.hpp" @@ -55,6 +56,7 @@ Bridge::Bridge(system_fsm::ServiceLocator& services, lua_State& s) luaL_requiref(&s, "term.core", luaopen_term_core, true); lua_pop(&s, 1); + RegisterControlsModule(&s); RegisterDatabaseModule(&s); RegisterQueueModule(&s); RegisterVersionModule(&s); diff --git a/src/lua/include/lua_controls.hpp b/src/lua/include/lua_controls.hpp new file mode 100644 index 00000000..18ad261d --- /dev/null +++ b/src/lua/include/lua_controls.hpp @@ -0,0 +1,15 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include "lua.hpp" + +namespace lua { + +auto RegisterControlsModule(lua_State*) -> void; + +} // namespace lua diff --git a/src/lua/include/property.hpp b/src/lua/include/property.hpp index a8a88125..9c5129ae 100644 --- a/src/lua/include/property.hpp +++ b/src/lua/include/property.hpp @@ -6,18 +6,27 @@ #pragma once +#include #include #include #include "audio_events.hpp" +#include "bluetooth_types.hpp" #include "lua.hpp" #include "lvgl.h" #include "service_locator.hpp" namespace lua { -using LuaValue = - std::variant; +// FIXME: We should use some kind of interface for this instead. +using LuaValue = std::variant>; + using LuaFunction = std::function; class Property { diff --git a/src/lua/lua_controls.cpp b/src/lua/lua_controls.cpp new file mode 100644 index 00000000..2da0ed11 --- /dev/null +++ b/src/lua/lua_controls.cpp @@ -0,0 +1,58 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "lua_controls.hpp" + +#include +#include + +#include "lua.hpp" + +#include "esp_log.h" +#include "lauxlib.h" +#include "lua.h" +#include "lvgl.h" + +#include "nvs.hpp" +#include "ui_events.hpp" + +namespace lua { + +[[maybe_unused]] static constexpr char kTag[] = "lua_controls"; + +static auto controls_schemes(lua_State* L) -> int { + lua_newtable(L); + + lua_pushliteral(L, "Buttons Only"); + lua_rawseti(L, -2, + static_cast(drivers::NvsStorage::InputModes::kButtonsOnly)); + + lua_pushliteral(L, "D-Pad"); + lua_rawseti( + L, -2, + static_cast(drivers::NvsStorage::InputModes::kDirectionalWheel)); + + lua_pushliteral(L, "Touchwheel"); + lua_rawseti( + L, -2, static_cast(drivers::NvsStorage::InputModes::kRotatingWheel)); + + return 1; +} + +static const struct luaL_Reg kControlsFuncs[] = {{"schemes", controls_schemes}, + {NULL, NULL}}; + +static auto lua_controls(lua_State* state) -> int { + luaL_newlib(state, kControlsFuncs); + return 1; +} + +auto RegisterControlsModule(lua_State* s) -> void { + luaL_requiref(s, "controls", lua_controls, true); + lua_pop(s, 1); +} + +} // namespace lua diff --git a/src/lua/property.cpp b/src/lua/property.cpp index 89351579..2d702447 100644 --- a/src/lua/property.cpp +++ b/src/lua/property.cpp @@ -10,7 +10,9 @@ #include #include #include +#include +#include "bluetooth_types.hpp" #include "lua.h" #include "lua.hpp" #include "lua_thread.hpp" @@ -185,6 +187,35 @@ static auto pushTagValue(lua_State* L, const database::TagValue& val) -> void { val); } +static void pushDevice(lua_State* L, const drivers::bluetooth::Device& dev) { + lua_createtable(L, 0, 4); + + lua_pushliteral(L, "address"); + auto* mac = reinterpret_cast( + lua_newuserdata(L, sizeof(drivers::bluetooth::mac_addr_t))); + *mac = dev.address; + lua_rawset(L, -3); + + // What I just did there was perfectly safe. Look, I can prove it: + static_assert( + std::is_trivially_copy_assignable()); + static_assert( + std::is_trivially_destructible()); + + lua_pushliteral(L, "name"); + lua_pushlstring(L, dev.name.data(), dev.name.size()); + lua_rawset(L, -3); + + // FIXME: This field deserves a little more structure. + lua_pushliteral(L, "class"); + lua_pushinteger(L, dev.class_of_device); + lua_rawset(L, -3); + + lua_pushliteral(L, "signal_strength"); + lua_pushinteger(L, dev.signal_strength); + lua_rawset(L, -3); +} + auto Property::PushValue(lua_State& s) -> int { std::visit( [&](auto&& arg) { @@ -222,6 +253,16 @@ auto Property::PushValue(lua_State& s) -> int { lua_pushliteral(&s, "encoding"); lua_pushstring(&s, codecs::StreamTypeToString(arg.encoding).c_str()); lua_settable(&s, table); + } else if constexpr (std::is_same_v) { + pushDevice(&s, arg); + } else if constexpr (std::is_same_v< + T, std::vector>) { + lua_createtable(&s, arg.size(), 0); + size_t i = 1; + for (const auto& dev : arg) { + pushDevice(&s, dev); + lua_rawseti(&s, -2, i++); + } } else { static_assert(always_false_v, "PushValue missing type"); } @@ -230,6 +271,34 @@ auto Property::PushValue(lua_State& s) -> int { return 1; } +auto popRichType(lua_State* L) -> LuaValue { + lua_pushliteral(L, "address"); + lua_gettable(L, -2); + + if (lua_isuserdata(L, -1)) { + // This must be a bt device! + drivers::bluetooth::mac_addr_t mac = + *reinterpret_cast( + lua_touserdata(L, -1)); + lua_pop(L, 1); + + lua_pushliteral(L, "name"); + lua_gettable(L, -2); + + std::pmr::string name = lua_tostring(L, -1); + lua_pop(L, 1); + + return drivers::bluetooth::Device{ + .address = mac, + .name = name, + .class_of_device = 0, + .signal_strength = 0, + }; + } + + return std::monostate{}; +} + auto Property::PopValue(lua_State& s) -> bool { LuaValue new_val; switch (lua_type(&s, 2)) { @@ -250,7 +319,14 @@ auto Property::PopValue(lua_State& s) -> bool { new_val = lua_tostring(&s, 2); break; default: - return false; + if (lua_istable(&s, 2)) { + new_val = popRichType(&s); + if (std::holds_alternative(new_val)) { + return false; + } + } else { + return false; + } } if (cb_ && std::invoke(*cb_, new_val)) { diff --git a/src/system_fsm/CMakeLists.txt b/src/system_fsm/CMakeLists.txt index 8535a0e7..e98d4653 100644 --- a/src/system_fsm/CMakeLists.txt +++ b/src/system_fsm/CMakeLists.txt @@ -3,7 +3,9 @@ # SPDX-License-Identifier: GPL-3.0-only idf_component_register( - SRCS "system_fsm.cpp" "running.cpp" "booting.cpp" "idle.cpp" "service_locator.cpp" + SRCS "system_fsm.cpp" "running.cpp" "booting.cpp" "idle.cpp" + "service_locator.cpp" INCLUDE_DIRS "include" - REQUIRES "tinyfsm" "drivers" "database" "ui" "result" "events" "audio" "app_console" "battery" "locale") + REQUIRES "tinyfsm" "drivers" "database" "ui" "result" "events" "audio" + "app_console" "battery" "locale") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp index 7bc92a17..898eb6aa 100644 --- a/src/system_fsm/booting.cpp +++ b/src/system_fsm/booting.cpp @@ -46,9 +46,7 @@ namespace states { [[maybe_unused]] static const char kTag[] = "BOOT"; static auto bt_event_cb(drivers::bluetooth::Event ev) -> void { - if (ev == drivers::bluetooth::Event::kKnownDevicesChanged) { - events::Ui().Dispatch(BluetoothDevicesChanged{}); - } + events::Ui().Dispatch(BluetoothEvent{.event = ev}); } static const TickType_t kInterruptCheckPeriod = pdMS_TO_TICKS(100); diff --git a/src/system_fsm/include/system_events.hpp b/src/system_fsm/include/system_events.hpp index 4ead9f2f..54e0aa9c 100644 --- a/src/system_fsm/include/system_events.hpp +++ b/src/system_fsm/include/system_events.hpp @@ -9,6 +9,7 @@ #include #include "battery.hpp" +#include "bluetooth_types.hpp" #include "database.hpp" #include "haptics.hpp" #include "service_locator.hpp" @@ -54,7 +55,9 @@ struct BatteryStateChanged : tinyfsm::Event { battery::Battery::BatteryState new_state; }; -struct BluetoothDevicesChanged : tinyfsm::Event {}; +struct BluetoothEvent : tinyfsm::Event { + drivers::bluetooth::Event event; +}; struct HapticTrigger : tinyfsm::Event { drivers::Haptics::Effect effect; diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index 0b883ee0..583a00a5 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -50,39 +50,40 @@ class UiState : public tinyfsm::Fsm { /* Fallback event handler. Does nothing. */ void react(const tinyfsm::Event& ev) {} - virtual void react(const system_fsm::BatteryStateChanged&); - virtual void react(const audio::PlaybackStarted&); - virtual void react(const audio::PlaybackFinished&); - virtual void react(const audio::PlaybackUpdate&); - virtual void react(const audio::QueueUpdate&); + virtual void react(const OnLuaError&) {} + virtual void react(const internal::BackPressed&) {} + virtual void react(const system_fsm::BootComplete&) {} + virtual void react(const system_fsm::StorageMounted&) {} - virtual void react(const audio::VolumeChanged&); - virtual void react(const audio::VolumeBalanceChanged&); - virtual void react(const audio::VolumeLimitChanged&); + void react(const system_fsm::BatteryStateChanged&); + void react(const audio::PlaybackStarted&); + void react(const audio::PlaybackFinished&); + void react(const audio::PlaybackUpdate&); + void react(const audio::QueueUpdate&); - virtual void react(const system_fsm::KeyLockChanged&); - virtual void react(const OnLuaError&) {} + void react(const audio::VolumeChanged&); + void react(const audio::VolumeBalanceChanged&); + void react(const audio::VolumeLimitChanged&); + + void react(const system_fsm::KeyLockChanged&); + + void react(const internal::DismissAlerts&); + void react(const internal::ControlSchemeChanged&); + + void react(const database::event::UpdateStarted&){}; + void react(const database::event::UpdateProgress&){}; + void react(const database::event::UpdateFinished&){}; + + void react(const system_fsm::BluetoothEvent&); - virtual void react(const internal::BackPressed&) {} virtual void react(const internal::ModalCancelPressed&) { sCurrentModal.reset(); } virtual void react(const internal::ModalConfirmPressed&) { sCurrentModal.reset(); } - void react(const internal::ControlSchemeChanged&); - virtual void react(const internal::ReindexDatabase&){}; - - void react(const internal::DismissAlerts&); - virtual void react(const database::event::UpdateStarted&){}; - virtual void react(const database::event::UpdateProgress&){}; - virtual void react(const database::event::UpdateFinished&){}; - - virtual void react(const system_fsm::DisplayReady&) {} - virtual void react(const system_fsm::BootComplete&) {} - virtual void react(const system_fsm::StorageMounted&) {} - virtual void react(const system_fsm::BluetoothDevicesChanged&) {} + void react(const internal::ReindexDatabase&){}; protected: void PushScreen(std::shared_ptr); @@ -104,6 +105,9 @@ class UiState : public tinyfsm::Fsm { static lua::Property sBluetoothEnabled; static lua::Property sBluetoothConnected; + static lua::Property sBluetoothPairedDevice; + static lua::Property sBluetoothDevices; + static lua::Property sBluetoothScanning; static lua::Property sPlaybackPlaying; @@ -121,6 +125,9 @@ class UiState : public tinyfsm::Fsm { static lua::Property sVolumeLimit; static lua::Property sDisplayBrightness; + + static lua::Property sControlsScheme; + static lua::Property sControlSensitivity; }; namespace states { diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index adda1c18..1c30e822 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -9,6 +9,7 @@ #include #include +#include "bluetooth_types.hpp" #include "freertos/portmacro.h" #include "freertos/projdefs.h" #include "lua.h" @@ -73,8 +74,31 @@ lua::Property UiState::sBatteryPct{0}; lua::Property UiState::sBatteryMv{0}; lua::Property UiState::sBatteryCharging{false}; -lua::Property UiState::sBluetoothEnabled{false}; +lua::Property UiState::sBluetoothEnabled{ + false, [](const lua::LuaValue& val) { + if (!std::holds_alternative(val)) { + return false; + } + if (std::get(val)) { + sServices->bluetooth().Enable(); + sServices->bluetooth().SetDeviceDiscovery(true); + } else { + sServices->bluetooth().Disable(); + } + return true; + }}; lua::Property UiState::sBluetoothConnected{false}; +lua::Property UiState::sBluetoothPairedDevice{ + std::monostate{}, [](const lua::LuaValue& val) { + if (std::holds_alternative(val)) { + auto dev = std::get(val); + sServices->bluetooth().SetPreferredDevice(dev.address); + } + return false; + }}; +lua::Property UiState::sBluetoothDevices{ + std::vector{}}; +lua::Property UiState::sBluetoothScanning{false}; lua::Property UiState::sPlaybackPlaying{ false, [](const lua::LuaValue& val) { @@ -99,39 +123,93 @@ lua::Property UiState::sQueueRandom{false}; lua::Property UiState::sVolumeCurrentPct{ 0, [](const lua::LuaValue& val) { + if (!std::holds_alternative(val)) { + return false; + } events::Audio().Dispatch(audio::SetVolume{ - .percent = {}, - .db = 0, + .percent = std::get(val), + .db = {}, }); - return false; + return true; }}; lua::Property UiState::sVolumeCurrentDb{ 0, [](const lua::LuaValue& val) { + if (!std::holds_alternative(val)) { + return false; + } events::Audio().Dispatch(audio::SetVolume{ .percent = {}, - .db = 0, + .db = std::get(val), }); - return false; + return true; }}; lua::Property UiState::sVolumeLeftBias{ 0, [](const lua::LuaValue& val) { + if (!std::holds_alternative(val)) { + return false; + } events::Audio().Dispatch(audio::SetVolumeBalance{ - .left_bias = 0, + .left_bias = std::get(val), }); - return false; + return true; }}; lua::Property UiState::sVolumeLimit{ 0, [](const lua::LuaValue& val) { + if (!std::holds_alternative(val)) { + return false; + } + int limit = std::get(val); events::Audio().Dispatch(audio::SetVolumeLimit{ - .new_limit = 0, + .limit_db = limit, }); - return false; + return true; }}; -lua::Property UiState::sDisplayBrightness{0, [](const lua::LuaValue& val) { - sDisplay->SetBrightness(0); - return false; - }}; +lua::Property UiState::sDisplayBrightness{ + 0, [](const lua::LuaValue& val) { + std::optional brightness = 0; + std::visit( + [&](auto&& v) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + brightness = v; + } + }, + val); + if (!brightness) { + return false; + } + sDisplay->SetBrightness(*brightness); + sServices->nvs().ScreenBrightness(*brightness); + return true; + }}; + +lua::Property UiState::sControlsScheme{ + 0, [](const lua::LuaValue& val) { + if (!std::holds_alternative(val)) { + return false; + } + drivers::NvsStorage::InputModes mode; + switch (std::get(val)) { + case 0: + mode = drivers::NvsStorage::InputModes::kButtonsOnly; + break; + case 1: + mode = drivers::NvsStorage::InputModes::kButtonsWithWheel; + break; + case 2: + mode = drivers::NvsStorage::InputModes::kDirectionalWheel; + break; + case 3: + mode = drivers::NvsStorage::InputModes::kRotatingWheel; + break; + default: + return false; + } + sServices->nvs().PrimaryInput(mode); + sInput->mode(mode); + return true; + }}; auto UiState::InitBootSplash(drivers::IGpios& gpios) -> bool { // Init LVGL first, since the display driver registers itself with LVGL. @@ -223,7 +301,29 @@ void UiState::react(const audio::VolumeBalanceChanged& ev) { } void UiState::react(const audio::VolumeLimitChanged& ev) { - sVolumeLeftBias.Update(ev.new_limit); + sVolumeLimit.Update(ev.new_limit_db); +} + +void UiState::react(const system_fsm::BluetoothEvent& ev) { + auto bt = sServices->bluetooth(); + switch (ev.event) { + case drivers::bluetooth::Event::kKnownDevicesChanged: + sBluetoothDevices.Update(bt.KnownDevices()); + break; + case drivers::bluetooth::Event::kConnectionStateChanged: + sBluetoothConnected.Update(bt.IsConnected()); + if (bt.ConnectedDevice()) { + sBluetoothPairedDevice.Update(bt.ConnectedDevice().value()); + } else { + sBluetoothPairedDevice.Update(std::monostate{}); + } + break; + case drivers::bluetooth::Event::kDiscoveryChanged: + sBluetoothScanning.Update(bt.IsDiscovering()); + break; + case drivers::bluetooth::Event::kPreferredDeviceChanged: + break; + } } namespace states { @@ -245,12 +345,18 @@ void Splash::react(const system_fsm::BootComplete& ev) { lv_disp_set_theme(NULL, base_theme); themes::Theme::instance()->Apply(); - sDisplay->SetBrightness(sServices->nvs().ScreenBrightness()); + int brightness = sServices->nvs().ScreenBrightness(); + sDisplayBrightness.Update(brightness); + sDisplay->SetBrightness(brightness); auto touchwheel = sServices->touchwheel(); if (touchwheel) { sInput = std::make_shared(sServices->gpios(), **touchwheel); - sInput->mode(sServices->nvs().PrimaryInput()); + + auto mode = sServices->nvs().PrimaryInput(); + sInput->mode(mode); + sControlsScheme.Update(static_cast(mode)); + sTask->input(sInput); } else { ESP_LOGE(kTag, "no input devices initialised!"); @@ -274,11 +380,13 @@ void Lua::entry() { {"battery_millivolts", &sBatteryMv}, {"plugged_in", &sBatteryCharging}, }); - sLua->bridge().AddPropertyModule("bluetooth", - { - {"enabled", &sBluetoothEnabled}, - {"connected", &sBluetoothConnected}, - }); + sLua->bridge().AddPropertyModule( + "bluetooth", { + {"enabled", &sBluetoothEnabled}, + {"connected", &sBluetoothConnected}, + {"paired_device", &sBluetoothPairedDevice}, + {"devices", &sBluetoothDevices}, + }); sLua->bridge().AddPropertyModule("playback", { {"playing", &sPlaybackPlaying}, @@ -299,6 +407,16 @@ void Lua::entry() { {"limit_db", &sVolumeLimit}, }); + sLua->bridge().AddPropertyModule("display", + { + {"brightness", &sDisplayBrightness}, + }); + + sLua->bridge().AddPropertyModule("controls", + { + {"scheme", &sControlsScheme}, + }); + sLua->bridge().AddPropertyModule( "backstack", { @@ -311,6 +429,15 @@ void Lua::entry() { {"hide", [&](lua_State* s) { return HideAlert(s); }}, }); + auto bt = sServices->bluetooth(); + sBluetoothEnabled.Update(bt.IsEnabled()); + sBluetoothConnected.Update(bt.IsConnected()); + if (bt.ConnectedDevice()) { + sBluetoothPairedDevice.Update(bt.ConnectedDevice().value()); + } + sBluetoothDevices.Update(bt.KnownDevices()); + sBluetoothScanning.Update(bt.IsDiscovering()); + sCurrentScreen.reset(); sLua->RunScript("/lua/main.lua"); }