summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjacqueline <me@jacqueline.id.au>2024-01-17 11:48:40 +1100
committerjacqueline <me@jacqueline.id.au>2024-01-17 11:48:40 +1100
commit71b46730394979ea528d152dbe884cc35c368759 (patch)
tree516b3af32f8822a5f900ea10fd6ffba2e3de1ebb
parent7cdcd44e0ca10ebdc796638190ed1d9b45d99ef0 (diff)
downloadtangara-fw-71b46730394979ea528d152dbe884cc35c368759.tar.gz
all screens basically working, but bluetooth is rough
-rw-r--r--lua/licenses.lua5
-rw-r--r--lua/settings.lua149
-rw-r--r--src/audio/audio_fsm.cpp25
-rw-r--r--src/audio/include/audio_events.hpp4
-rw-r--r--src/drivers/CMakeLists.txt10
-rw-r--r--src/drivers/bluetooth.cpp55
-rw-r--r--src/drivers/include/bluetooth.hpp10
-rw-r--r--src/drivers/include/bluetooth_types.hpp2
-rw-r--r--src/drivers/include/nvs.hpp2
-rw-r--r--src/lua/CMakeLists.txt2
-rw-r--r--src/lua/bridge.cpp2
-rw-r--r--src/lua/include/lua_controls.hpp15
-rw-r--r--src/lua/include/property.hpp13
-rw-r--r--src/lua/lua_controls.cpp58
-rw-r--r--src/lua/property.cpp78
-rw-r--r--src/system_fsm/CMakeLists.txt6
-rw-r--r--src/system_fsm/booting.cpp4
-rw-r--r--src/system_fsm/include/system_events.hpp5
-rw-r--r--src/ui/include/ui_fsm.hpp53
-rw-r--r--src/ui/ui_fsm.cpp171
20 files changed, 557 insertions, 112 deletions
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<int>(nvs.AmpMaxVolume()) -
+ static_cast<int>(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<bluetooth::Disabled>();
}
+auto Bluetooth::IsConnected() -> bool {
+ return bluetooth::BluetoothState::is_in_state<bluetooth::Connected>();
+}
+
+auto Bluetooth::ConnectedDevice() -> std::optional<bluetooth::Device> {
+ 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<bluetooth::BluetoothState>::dispatch(
+ bluetooth::events::DiscoveryChanged{});
+}
+
+auto Bluetooth::IsDiscovering() -> bool {
+ return bluetooth::BluetoothState::discovery();
+}
+
auto Bluetooth::KnownDevices() -> std::vector<bluetooth::Device> {
std::vector<bluetooth::Device> 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<bluetooth::BluetoothState>::dispatch(
- bluetooth::events::DiscoveryChanged{});
+auto Bluetooth::PreferredDevice() -> std::optional<bluetooth::mac_addr_t> {
+ return bluetooth::BluetoothState::preferred_device();
}
auto Bluetooth::SetSource(StreamBufferHandle_t src) -> void {
@@ -120,7 +145,7 @@ auto Bluetooth::SetEventHandler(std::function<void(bluetooth::Event)> 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<mac_addr_t, Device> BluetoothState::sDevices_{};
std::optional<mac_addr_t> BluetoothState::sPreferredDevice_{};
-mac_addr_t BluetoothState::sCurrentDevice_{0};
+std::optional<Device> BluetoothState::sCurrentDevice_{};
bool BluetoothState::sIsDiscoveryAllowed_{false};
std::atomic<StreamBufferHandle_t> 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<bluetooth::Device>;
+
/*
* 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<bluetooth::Device>;
+
auto SetPreferredDevice(const bluetooth::mac_addr_t& mac) -> void;
+ auto PreferredDevice() -> std::optional<bluetooth::mac_addr_t>;
auto SetSource(StreamBufferHandle_t) -> void;
auto SetEventHandler(std::function<void(bluetooth::Event)> cb) -> void;
@@ -100,9 +106,11 @@ class BluetoothState : public tinyfsm::Fsm<BluetoothState> {
static auto Init(NvsStorage& storage) -> void;
static auto devices() -> std::vector<Device>;
+
static auto preferred_device() -> std::optional<mac_addr_t>;
static auto preferred_device(std::optional<mac_addr_t>) -> void;
+ static auto scanning() -> bool;
static auto discovery() -> bool;
static auto discovery(bool) -> void;
@@ -135,7 +143,7 @@ class BluetoothState : public tinyfsm::Fsm<BluetoothState> {
static std::mutex sDevicesMutex_;
static std::map<mac_addr_t, Device> sDevices_;
static std::optional<mac_addr_t> sPreferredDevice_;
- static mac_addr_t sCurrentDevice_;
+ static std::optional<Device> sCurrentDevice_;
static bool sIsDiscoveryAllowed_;
static std::atomic<StreamBufferHandle_t> 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 <me@jacqueline.id.au>
+ *
+ * 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 <stdint.h>
#include <memory>
#include <string>
#include "audio_events.hpp"
+#include "bluetooth_types.hpp"
#include "lua.hpp"
#include "lvgl.h"
#include "service_locator.hpp"
namespace lua {
-using LuaValue =
- std::variant<std::monostate, int, bool, std::string, audio::Track>;
+// FIXME: We should use some kind of interface for this instead.
+using LuaValue = std::variant<std::monostate,
+ int,
+ bool,
+ std::string,
+ audio::Track,
+ drivers::bluetooth::Device,
+ std::vector<drivers::bluetooth::Device>>;
+
using LuaFunction = std::function<int(lua_State*)>;
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 <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "lua_controls.hpp"
+
+#include <memory>
+#include <string>
+
+#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<int>(drivers::NvsStorage::InputModes::kButtonsOnly));
+
+ lua_pushliteral(L, "D-Pad");
+ lua_rawseti(
+ L, -2,
+ static_cast<int>(drivers::NvsStorage::InputModes::kDirectionalWheel));
+
+ lua_pushliteral(L, "Touchwheel");
+ lua_rawseti(
+ L, -2, static_cast<int>(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 <cmath>
#include <memory>
#include <string>
+#include <variant>
+#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<drivers::bluetooth::mac_addr_t*>(
+ 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<drivers::bluetooth::mac_addr_t>());
+ static_assert(
+ std::is_trivially_destructible<drivers::bluetooth::mac_addr_t>());
+
+ 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<T, drivers::bluetooth::Device>) {
+ pushDevice(&s, arg);
+ } else if constexpr (std::is_same_v<
+ T, std::vector<drivers::bluetooth::Device>>) {
+ 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<T>, "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<drivers::bluetooth::mac_addr_t*>(
+ 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<std::monostate>(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 <memory>
#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<UiState> {
/* 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<Screen>);
@@ -104,6 +105,9 @@ class UiState : public tinyfsm::Fsm<UiState> {
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<UiState> {
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 <memory>
#include <variant>
+#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<bool>(val)) {
+ return false;
+ }
+ if (std::get<bool>(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<drivers::bluetooth::Device>(val)) {
+ auto dev = std::get<drivers::bluetooth::Device>(val);
+ sServices->bluetooth().SetPreferredDevice(dev.address);
+ }
+ return false;
+ }};
+lua::Property UiState::sBluetoothDevices{
+ std::vector<drivers::bluetooth::Device>{}};
+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<int>(val)) {
+ return false;
+ }
events::Audio().Dispatch(audio::SetVolume{
- .percent = {},
- .db = 0,
+ .percent = std::get<int>(val),
+ .db = {},
});
- return false;
+ return true;
}};
lua::Property UiState::sVolumeCurrentDb{
0, [](const lua::LuaValue& val) {
+ if (!std::holds_alternative<int>(val)) {
+ return false;
+ }
events::Audio().Dispatch(audio::SetVolume{
.percent = {},
- .db = 0,
+ .db = std::get<int>(val),
});
- return false;
+ return true;
}};
lua::Property UiState::sVolumeLeftBias{
0, [](const lua::LuaValue& val) {
+ if (!std::holds_alternative<int>(val)) {
+ return false;
+ }
events::Audio().Dispatch(audio::SetVolumeBalance{
- .left_bias = 0,
+ .left_bias = std::get<int>(val),
});
- return false;
+ return true;
}};
lua::Property UiState::sVolumeLimit{
0, [](const lua::LuaValue& val) {
+ if (!std::holds_alternative<int>(val)) {
+ return false;
+ }
+ int limit = std::get<int>(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<int> brightness = 0;
+ std::visit(
+ [&](auto&& v) {
+ using T = std::decay_t<decltype(v)>;
+ if constexpr (std::is_same_v<T, int>) {
+ 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<int>(val)) {
+ return false;
+ }
+ drivers::NvsStorage::InputModes mode;
+ switch (std::get<int>(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<EncoderInput>(sServices->gpios(), **touchwheel);
- sInput->mode(sServices->nvs().PrimaryInput());
+
+ auto mode = sServices->nvs().PrimaryInput();
+ sInput->mode(mode);
+ sControlsScheme.Update(static_cast<int>(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");
}