all screens basically working, but bluetooth is rough

custom
jacqueline 1 year ago
parent 7cdcd44e0c
commit 71b4673039
  1. 5
      lua/licenses.lua
  2. 149
      lua/settings.lua
  3. 25
      src/audio/audio_fsm.cpp
  4. 4
      src/audio/include/audio_events.hpp
  5. 10
      src/drivers/CMakeLists.txt
  6. 55
      src/drivers/bluetooth.cpp
  7. 10
      src/drivers/include/bluetooth.hpp
  8. 2
      src/drivers/include/bluetooth_types.hpp
  9. 2
      src/lua/CMakeLists.txt
  10. 2
      src/lua/bridge.cpp
  11. 15
      src/lua/include/lua_controls.hpp
  12. 13
      src/lua/include/property.hpp
  13. 58
      src/lua/lua_controls.cpp
  14. 76
      src/lua/property.cpp
  15. 6
      src/system_fsm/CMakeLists.txt
  16. 4
      src/system_fsm/booting.cpp
  17. 5
      src/system_fsm/include/system_events.hpp
  18. 53
      src/ui/include/ui_fsm.hpp
  19. 159
      src/ui/ui_fsm.cpp

@ -80,7 +80,10 @@ end
return function() return function()
local menu = widgets.MenuScreen(true, "Licenses") local menu = widgets.MenuScreen({
show_back = true,
title = "Licenses"
})
local container = menu.root:Object { local container = menu.root:Object {
flex = { flex = {

@ -3,6 +3,9 @@ local backstack = require("backstack")
local widgets = require("widgets") local widgets = require("widgets")
local theme = require("theme") local theme = require("theme")
local volume = require("volume") local volume = require("volume")
local display = require("display")
local controls = require("controls")
local bluetooth = require("bluetooth")
local settings = {} local settings = {}
@ -34,16 +37,26 @@ function settings.bluetooth()
flex = { flex = {
flex_direction = "row", flex_direction = "row",
justify_content = "flex-start", justify_content = "flex-start",
align_items = "flex-start", align_items = "content",
align_content = "flex-start", align_content = "flex-start",
}, },
w = lvgl.PCT(100), w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT, h = lvgl.SIZE_CONTENT,
pad_bottom = 1,
} }
enable_container:Label { text = "Enable", flex_grow = 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)
local preferred_container = menu.content:Object { menu.content:Label {
text = "Paired Device",
pad_bottom = 1,
}:add_style(theme.settings_title)
local paired_container = menu.content:Object {
flex = { flex = {
flex_direction = "row", flex_direction = "row",
justify_content = "flex-start", justify_content = "flex-start",
@ -52,25 +65,50 @@ function settings.bluetooth()
}, },
w = lvgl.PCT(100), w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT, 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 { local paired_device = paired_container:Label {
text = "My Cool Speakers", 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 { menu.content:Label {
text = "Available Devices", text = "Nearby Devices",
pad_bottom = 1,
}:add_style(theme.settings_title) }:add_style(theme.settings_title)
local known_devices = menu.content:List { local devices = menu.content:List {
w = lvgl.PCT(100), 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 end
@ -87,8 +125,9 @@ function settings.headphones()
} }
local limits = { -10, 6, 10 } local limits = { -10, 6, 10 }
volume_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function() volume_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function()
local selection = volume_chooser:get('selected') -- luavgl dropdown binding uses 0-based indexing :(
volume.limit_db.set(limits[selection]) local selection = volume_chooser:get('selected') + 1
volume.limit_db:set(limits[selection])
end) end)
menu.content:Label { menu.content:Label {
@ -98,7 +137,7 @@ function settings.headphones()
local balance = menu.content:Slider { local balance = menu.content:Slider {
w = lvgl.PCT(100), w = lvgl.PCT(100),
h = 5, h = 5,
range = { min = -5, max = 5 }, range = { min = -100, max = 100 },
value = 0, value = 0,
} }
balance:onevent(lvgl.EVENT.VALUE_CHANGED, function() balance:onevent(lvgl.EVENT.VALUE_CHANGED, function()
@ -109,10 +148,9 @@ function settings.headphones()
menu.bindings = { menu.bindings = {
volume.limit_db:bind(function(limit) 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 if limits[i] == limit then
volume_chooser:set{ selected = i } volume_chooser:set { selected = i - 1 }
end end
end end
end), end),
@ -122,12 +160,14 @@ function settings.headphones()
} }
if bias < 0 then if bias < 0 then
balance_label:set { 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 { 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
end), end),
} }
@ -138,19 +178,37 @@ end
function settings.display() function settings.display()
local menu = SettingsScreen("Display") local menu = SettingsScreen("Display")
menu.content:Label { local brightness_title = menu.content:Object {
text = "Brightness", flex = {
}:add_style(theme.settings_title) 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 { local brightness = menu.content:Slider {
w = lvgl.PCT(100), w = lvgl.PCT(100),
h = 5, h = 5,
range = { min = 0, max = 100 }, range = { min = 0, max = 100 },
value = 50, value = display.brightness:get(),
} }
brightness:onevent(lvgl.EVENT.VALUE_CHANGED, function() 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) end)
}
return menu
end end
function settings.input() function settings.input()
@ -160,14 +218,41 @@ function settings.input()
text = "Control scheme", text = "Control scheme",
}:add_style(theme.settings_title) }: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 { local controls_chooser = menu.content:Dropdown {
options = "Buttons only\nD-Pad\nTouchwheel", options = options,
selected = 3,
} }
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() controls_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function()
local selection = controls_chooser:get('selected') local option = controls_chooser:get('selected')
print("controls", selection) local scheme = option_to_scheme[option]
controls.scheme:set(scheme)
end) end)
return menu
end end
function settings.database() function settings.database()

@ -87,13 +87,14 @@ void AudioState::react(const SetVolume& ev) {
} }
void AudioState::react(const SetVolumeLimit& ev) { void AudioState::react(const SetVolumeLimit& ev) {
ESP_LOGI(kTag, "new max volume %i db", uint16_t limit_in_dac_units =
(ev.new_limit - drivers::wm8523::kLineLevelReferenceVolume) / 4); drivers::wm8523::kLineLevelReferenceVolume + (ev.limit_db * 4);
sI2SOutput->SetMaxVolume(ev.new_limit);
sServices->nvs().AmpMaxVolume(ev.new_limit); sI2SOutput->SetMaxVolume(limit_in_dac_units);
sServices->nvs().AmpMaxVolume(limit_in_dac_units);
events::Ui().Dispatch(VolumeLimitChanged{ events::Ui().Dispatch(VolumeLimitChanged{
.new_limit = ev.new_limit, .new_limit_db = ev.limit_db,
}); });
events::Ui().Dispatch(VolumeChanged{ events::Ui().Dispatch(VolumeChanged{
.percent = sOutput->GetVolumePct(), .percent = sOutput->GetVolumePct(),
@ -167,6 +168,20 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) {
} }
sOutput->SetMode(IAudioOutput::Modes::kOnPaused); 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.reset(new SampleConverter());
sSampleConverter->SetOutput(sOutput); sSampleConverter->SetOutput(sOutput);

@ -63,11 +63,11 @@ struct VolumeBalanceChanged : tinyfsm::Event {
int left_bias; int left_bias;
}; };
struct VolumeLimitChanged : tinyfsm::Event { struct VolumeLimitChanged : tinyfsm::Event {
uint16_t new_limit; int new_limit_db;
}; };
struct SetVolumeLimit : tinyfsm::Event { struct SetVolumeLimit : tinyfsm::Event {
uint16_t new_limit; int limit_db;
}; };
struct TogglePlayPause : tinyfsm::Event {}; struct TogglePlayPause : tinyfsm::Event {};

@ -3,9 +3,11 @@
# SPDX-License-Identifier: GPL-3.0-only # SPDX-License-Identifier: GPL-3.0-only
idf_component_register( idf_component_register(
SRCS "touchwheel.cpp" "i2s_dac.cpp" "gpios.cpp" "adc.cpp" "storage.cpp" "i2c.cpp" SRCS "touchwheel.cpp" "i2s_dac.cpp" "gpios.cpp" "adc.cpp" "storage.cpp"
"spi.cpp" "display.cpp" "display_init.cpp" "samd.cpp" "relative_wheel.cpp" "wm8523.cpp" "i2c.cpp" "bluetooth.cpp" "spi.cpp" "display.cpp" "display_init.cpp"
"nvs.cpp" "bluetooth.cpp" "haptics.cpp" "spiffs.cpp" "samd.cpp" "relative_wheel.cpp" "wm8523.cpp" "nvs.cpp" "haptics.cpp"
"spiffs.cpp"
INCLUDE_DIRS "include" 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}) target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})

@ -80,6 +80,36 @@ auto Bluetooth::IsEnabled() -> bool {
return !bluetooth::BluetoothState::is_in_state<bluetooth::Disabled>(); 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> { auto Bluetooth::KnownDevices() -> std::vector<bluetooth::Device> {
std::vector<bluetooth::Device> out = bluetooth::BluetoothState::devices(); std::vector<bluetooth::Device> out = bluetooth::BluetoothState::devices();
std::sort(out.begin(), out.end(), [](const auto& a, const auto& b) -> bool { 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{}); bluetooth::events::PreferredDeviceChanged{});
} }
auto Bluetooth::SetDeviceDiscovery(bool allowed) -> void { auto Bluetooth::PreferredDevice() -> std::optional<bluetooth::mac_addr_t> {
if (allowed == bluetooth::BluetoothState::discovery()) { return bluetooth::BluetoothState::preferred_device();
return;
}
bluetooth::BluetoothState::discovery(allowed);
tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch(
bluetooth::events::DiscoveryChanged{});
} }
auto Bluetooth::SetSource(StreamBufferHandle_t src) -> void { 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); bluetooth::BluetoothState::event_handler(cb);
} }
auto DeviceName() -> std::pmr::string { static auto DeviceName() -> std::pmr::string {
uint8_t mac[8]{0}; uint8_t mac[8]{0};
esp_efuse_mac_get_default(mac); esp_efuse_mac_get_default(mac);
std::ostringstream name; std::ostringstream name;
@ -267,7 +292,7 @@ Scanner* BluetoothState::sScanner_;
std::mutex BluetoothState::sDevicesMutex_{}; std::mutex BluetoothState::sDevicesMutex_{};
std::map<mac_addr_t, Device> BluetoothState::sDevices_{}; std::map<mac_addr_t, Device> BluetoothState::sDevices_{};
std::optional<mac_addr_t> BluetoothState::sPreferredDevice_{}; std::optional<mac_addr_t> BluetoothState::sPreferredDevice_{};
mac_addr_t BluetoothState::sCurrentDevice_{0}; std::optional<Device> BluetoothState::sCurrentDevice_{};
bool BluetoothState::sIsDiscoveryAllowed_{false}; bool BluetoothState::sIsDiscoveryAllowed_{false};
std::atomic<StreamBufferHandle_t> BluetoothState::sSource_; std::atomic<StreamBufferHandle_t> BluetoothState::sSource_;
@ -331,7 +356,7 @@ auto BluetoothState::react(const events::DeviceDiscovered& ev) -> void {
sDevices_[ev.device.address] = ev.device; sDevices_[ev.device.address] = ev.device;
if (ev.device.address == sPreferredDevice_) { if (ev.device.address == sPreferredDevice_) {
sCurrentDevice_ = ev.device.address; sCurrentDevice_ = ev.device;
is_preferred = true; is_preferred = true;
} }
@ -466,9 +491,17 @@ void Idle::react(const events::internal::Gap& ev) {
void Connecting::entry() { void Connecting::entry() {
ESP_LOGI(kTag, "connecting to device"); ESP_LOGI(kTag, "connecting to device");
esp_a2d_source_connect(sPreferredDevice_.value().data()); 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) { void Connecting::react(const events::Disable& ev) {
// TODO: disconnect gracefully // TODO: disconnect gracefully

@ -32,14 +32,20 @@ class Bluetooth {
auto Disable() -> void; auto Disable() -> void;
auto IsEnabled() -> bool; 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 * Sets whether or not the bluetooth stack is allowed to actively scan for
* new devices. * new devices.
*/ */
auto SetDeviceDiscovery(bool) -> void; auto SetDeviceDiscovery(bool) -> void;
auto IsDiscovering() -> bool;
auto KnownDevices() -> std::vector<bluetooth::Device>; auto KnownDevices() -> std::vector<bluetooth::Device>;
auto SetPreferredDevice(const bluetooth::mac_addr_t& mac) -> void; auto SetPreferredDevice(const bluetooth::mac_addr_t& mac) -> void;
auto PreferredDevice() -> std::optional<bluetooth::mac_addr_t>;
auto SetSource(StreamBufferHandle_t) -> void; auto SetSource(StreamBufferHandle_t) -> void;
auto SetEventHandler(std::function<void(bluetooth::Event)> cb) -> 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 Init(NvsStorage& storage) -> void;
static auto devices() -> std::vector<Device>; static auto devices() -> std::vector<Device>;
static auto preferred_device() -> std::optional<mac_addr_t>; static auto preferred_device() -> std::optional<mac_addr_t>;
static auto preferred_device(std::optional<mac_addr_t>) -> void; static auto preferred_device(std::optional<mac_addr_t>) -> void;
static auto scanning() -> bool;
static auto discovery() -> bool; static auto discovery() -> bool;
static auto discovery(bool) -> void; static auto discovery(bool) -> void;
@ -135,7 +143,7 @@ class BluetoothState : public tinyfsm::Fsm<BluetoothState> {
static std::mutex sDevicesMutex_; static std::mutex sDevicesMutex_;
static std::map<mac_addr_t, Device> sDevices_; static std::map<mac_addr_t, Device> sDevices_;
static std::optional<mac_addr_t> sPreferredDevice_; static std::optional<mac_addr_t> sPreferredDevice_;
static mac_addr_t sCurrentDevice_; static std::optional<Device> sCurrentDevice_;
static bool sIsDiscoveryAllowed_; static bool sIsDiscoveryAllowed_;
static std::atomic<StreamBufferHandle_t> sSource_; static std::atomic<StreamBufferHandle_t> sSource_;

@ -21,6 +21,8 @@ struct Device {
enum class Event { enum class Event {
kKnownDevicesChanged, kKnownDevicesChanged,
kConnectionStateChanged, kConnectionStateChanged,
kPreferredDeviceChanged,
kDiscoveryChanged,
}; };
} // namespace bluetooth } // namespace bluetooth

@ -4,7 +4,7 @@
idf_component_register( idf_component_register(
SRCS "lua_thread.cpp" "bridge.cpp" "property.cpp" "lua_database.cpp" 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" INCLUDE_DIRS "include"
REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database"
"esp_timer" "battery" "esp-idf-lua" "luavgl" "lua-linenoise" "lua-term" "esp_timer" "battery" "esp-idf-lua" "luavgl" "lua-linenoise" "lua-term"

@ -16,6 +16,7 @@
#include "lauxlib.h" #include "lauxlib.h"
#include "lua.h" #include "lua.h"
#include "lua.hpp" #include "lua.hpp"
#include "lua_controls.hpp"
#include "lua_database.hpp" #include "lua_database.hpp"
#include "lua_queue.hpp" #include "lua_queue.hpp"
#include "lua_version.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); luaL_requiref(&s, "term.core", luaopen_term_core, true);
lua_pop(&s, 1); lua_pop(&s, 1);
RegisterControlsModule(&s);
RegisterDatabaseModule(&s); RegisterDatabaseModule(&s);
RegisterQueueModule(&s); RegisterQueueModule(&s);
RegisterVersionModule(&s); RegisterVersionModule(&s);

@ -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

@ -6,18 +6,27 @@
#pragma once #pragma once
#include <stdint.h>
#include <memory> #include <memory>
#include <string> #include <string>
#include "audio_events.hpp" #include "audio_events.hpp"
#include "bluetooth_types.hpp"
#include "lua.hpp" #include "lua.hpp"
#include "lvgl.h" #include "lvgl.h"
#include "service_locator.hpp" #include "service_locator.hpp"
namespace lua { namespace lua {
using LuaValue = // FIXME: We should use some kind of interface for this instead.
std::variant<std::monostate, int, bool, std::string, audio::Track>; 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*)>; using LuaFunction = std::function<int(lua_State*)>;
class Property { class Property {

@ -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

@ -10,7 +10,9 @@
#include <cmath> #include <cmath>
#include <memory> #include <memory>
#include <string> #include <string>
#include <variant>
#include "bluetooth_types.hpp"
#include "lua.h" #include "lua.h"
#include "lua.hpp" #include "lua.hpp"
#include "lua_thread.hpp" #include "lua_thread.hpp"
@ -185,6 +187,35 @@ static auto pushTagValue(lua_State* L, const database::TagValue& val) -> void {
val); 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 { auto Property::PushValue(lua_State& s) -> int {
std::visit( std::visit(
[&](auto&& arg) { [&](auto&& arg) {
@ -222,6 +253,16 @@ auto Property::PushValue(lua_State& s) -> int {
lua_pushliteral(&s, "encoding"); lua_pushliteral(&s, "encoding");
lua_pushstring(&s, codecs::StreamTypeToString(arg.encoding).c_str()); lua_pushstring(&s, codecs::StreamTypeToString(arg.encoding).c_str());
lua_settable(&s, table); 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 { } else {
static_assert(always_false_v<T>, "PushValue missing type"); static_assert(always_false_v<T>, "PushValue missing type");
} }
@ -230,6 +271,34 @@ auto Property::PushValue(lua_State& s) -> int {
return 1; 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 { auto Property::PopValue(lua_State& s) -> bool {
LuaValue new_val; LuaValue new_val;
switch (lua_type(&s, 2)) { switch (lua_type(&s, 2)) {
@ -250,8 +319,15 @@ auto Property::PopValue(lua_State& s) -> bool {
new_val = lua_tostring(&s, 2); new_val = lua_tostring(&s, 2);
break; break;
default: default:
if (lua_istable(&s, 2)) {
new_val = popRichType(&s);
if (std::holds_alternative<std::monostate>(new_val)) {
return false;
}
} else {
return false; return false;
} }
}
if (cb_ && std::invoke(*cb_, new_val)) { if (cb_ && std::invoke(*cb_, new_val)) {
Update(new_val); Update(new_val);

@ -3,7 +3,9 @@
# SPDX-License-Identifier: GPL-3.0-only # SPDX-License-Identifier: GPL-3.0-only
idf_component_register( 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" 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}) target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})

@ -46,9 +46,7 @@ namespace states {
[[maybe_unused]] static const char kTag[] = "BOOT"; [[maybe_unused]] static const char kTag[] = "BOOT";
static auto bt_event_cb(drivers::bluetooth::Event ev) -> void { static auto bt_event_cb(drivers::bluetooth::Event ev) -> void {
if (ev == drivers::bluetooth::Event::kKnownDevicesChanged) { events::Ui().Dispatch(BluetoothEvent{.event = ev});
events::Ui().Dispatch(BluetoothDevicesChanged{});
}
} }
static const TickType_t kInterruptCheckPeriod = pdMS_TO_TICKS(100); static const TickType_t kInterruptCheckPeriod = pdMS_TO_TICKS(100);

@ -9,6 +9,7 @@
#include <memory> #include <memory>
#include "battery.hpp" #include "battery.hpp"
#include "bluetooth_types.hpp"
#include "database.hpp" #include "database.hpp"
#include "haptics.hpp" #include "haptics.hpp"
#include "service_locator.hpp" #include "service_locator.hpp"
@ -54,7 +55,9 @@ struct BatteryStateChanged : tinyfsm::Event {
battery::Battery::BatteryState new_state; battery::Battery::BatteryState new_state;
}; };
struct BluetoothDevicesChanged : tinyfsm::Event {}; struct BluetoothEvent : tinyfsm::Event {
drivers::bluetooth::Event event;
};
struct HapticTrigger : tinyfsm::Event { struct HapticTrigger : tinyfsm::Event {
drivers::Haptics::Effect effect; drivers::Haptics::Effect effect;

@ -50,39 +50,40 @@ class UiState : public tinyfsm::Fsm<UiState> {
/* Fallback event handler. Does nothing. */ /* Fallback event handler. Does nothing. */
void react(const tinyfsm::Event& ev) {} void react(const tinyfsm::Event& ev) {}
virtual void react(const system_fsm::BatteryStateChanged&); virtual void react(const OnLuaError&) {}
virtual void react(const audio::PlaybackStarted&); virtual void react(const internal::BackPressed&) {}
virtual void react(const audio::PlaybackFinished&); virtual void react(const system_fsm::BootComplete&) {}
virtual void react(const audio::PlaybackUpdate&); virtual void react(const system_fsm::StorageMounted&) {}
virtual void react(const audio::QueueUpdate&);
virtual void react(const audio::VolumeChanged&); void react(const system_fsm::BatteryStateChanged&);
virtual void react(const audio::VolumeBalanceChanged&); void react(const audio::PlaybackStarted&);
virtual void react(const audio::VolumeLimitChanged&); void react(const audio::PlaybackFinished&);
void react(const audio::PlaybackUpdate&);
void react(const audio::QueueUpdate&);
virtual void react(const system_fsm::KeyLockChanged&); void react(const audio::VolumeChanged&);
virtual void react(const OnLuaError&) {} 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&) { virtual void react(const internal::ModalCancelPressed&) {
sCurrentModal.reset(); sCurrentModal.reset();
} }
virtual void react(const internal::ModalConfirmPressed&) { virtual void react(const internal::ModalConfirmPressed&) {
sCurrentModal.reset(); 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&){}; void react(const internal::ReindexDatabase&){};
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&) {}
protected: protected:
void PushScreen(std::shared_ptr<Screen>); void PushScreen(std::shared_ptr<Screen>);
@ -104,6 +105,9 @@ class UiState : public tinyfsm::Fsm<UiState> {
static lua::Property sBluetoothEnabled; static lua::Property sBluetoothEnabled;
static lua::Property sBluetoothConnected; static lua::Property sBluetoothConnected;
static lua::Property sBluetoothPairedDevice;
static lua::Property sBluetoothDevices;
static lua::Property sBluetoothScanning;
static lua::Property sPlaybackPlaying; static lua::Property sPlaybackPlaying;
@ -121,6 +125,9 @@ class UiState : public tinyfsm::Fsm<UiState> {
static lua::Property sVolumeLimit; static lua::Property sVolumeLimit;
static lua::Property sDisplayBrightness; static lua::Property sDisplayBrightness;
static lua::Property sControlsScheme;
static lua::Property sControlSensitivity;
}; };
namespace states { namespace states {

@ -9,6 +9,7 @@
#include <memory> #include <memory>
#include <variant> #include <variant>
#include "bluetooth_types.hpp"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
#include "freertos/projdefs.h" #include "freertos/projdefs.h"
#include "lua.h" #include "lua.h"
@ -73,8 +74,31 @@ lua::Property UiState::sBatteryPct{0};
lua::Property UiState::sBatteryMv{0}; lua::Property UiState::sBatteryMv{0};
lua::Property UiState::sBatteryCharging{false}; 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::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{ lua::Property UiState::sPlaybackPlaying{
false, [](const lua::LuaValue& val) { false, [](const lua::LuaValue& val) {
@ -99,38 +123,92 @@ lua::Property UiState::sQueueRandom{false};
lua::Property UiState::sVolumeCurrentPct{ lua::Property UiState::sVolumeCurrentPct{
0, [](const lua::LuaValue& val) { 0, [](const lua::LuaValue& val) {
if (!std::holds_alternative<int>(val)) {
return false;
}
events::Audio().Dispatch(audio::SetVolume{ events::Audio().Dispatch(audio::SetVolume{
.percent = {}, .percent = std::get<int>(val),
.db = 0, .db = {},
}); });
return false; return true;
}}; }};
lua::Property UiState::sVolumeCurrentDb{ lua::Property UiState::sVolumeCurrentDb{
0, [](const lua::LuaValue& val) { 0, [](const lua::LuaValue& val) {
if (!std::holds_alternative<int>(val)) {
return false;
}
events::Audio().Dispatch(audio::SetVolume{ events::Audio().Dispatch(audio::SetVolume{
.percent = {}, .percent = {},
.db = 0, .db = std::get<int>(val),
}); });
return false; return true;
}}; }};
lua::Property UiState::sVolumeLeftBias{ lua::Property UiState::sVolumeLeftBias{
0, [](const lua::LuaValue& val) { 0, [](const lua::LuaValue& val) {
if (!std::holds_alternative<int>(val)) {
return false;
}
events::Audio().Dispatch(audio::SetVolumeBalance{ events::Audio().Dispatch(audio::SetVolumeBalance{
.left_bias = 0, .left_bias = std::get<int>(val),
}); });
return false; return true;
}}; }};
lua::Property UiState::sVolumeLimit{ lua::Property UiState::sVolumeLimit{
0, [](const lua::LuaValue& val) { 0, [](const lua::LuaValue& val) {
if (!std::holds_alternative<int>(val)) {
return false;
}
int limit = std::get<int>(val);
events::Audio().Dispatch(audio::SetVolumeLimit{ events::Audio().Dispatch(audio::SetVolumeLimit{
.new_limit = 0, .limit_db = limit,
}); });
return true;
}};
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; return false;
}
sDisplay->SetBrightness(*brightness);
sServices->nvs().ScreenBrightness(*brightness);
return true;
}}; }};
lua::Property UiState::sDisplayBrightness{0, [](const lua::LuaValue& val) { lua::Property UiState::sControlsScheme{
sDisplay->SetBrightness(0); 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; return false;
}
sServices->nvs().PrimaryInput(mode);
sInput->mode(mode);
return true;
}}; }};
auto UiState::InitBootSplash(drivers::IGpios& gpios) -> bool { auto UiState::InitBootSplash(drivers::IGpios& gpios) -> bool {
@ -223,7 +301,29 @@ void UiState::react(const audio::VolumeBalanceChanged& ev) {
} }
void UiState::react(const audio::VolumeLimitChanged& 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 { namespace states {
@ -245,12 +345,18 @@ void Splash::react(const system_fsm::BootComplete& ev) {
lv_disp_set_theme(NULL, base_theme); lv_disp_set_theme(NULL, base_theme);
themes::Theme::instance()->Apply(); themes::Theme::instance()->Apply();
sDisplay->SetBrightness(sServices->nvs().ScreenBrightness()); int brightness = sServices->nvs().ScreenBrightness();
sDisplayBrightness.Update(brightness);
sDisplay->SetBrightness(brightness);
auto touchwheel = sServices->touchwheel(); auto touchwheel = sServices->touchwheel();
if (touchwheel) { if (touchwheel) {
sInput = std::make_shared<EncoderInput>(sServices->gpios(), **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); sTask->input(sInput);
} else { } else {
ESP_LOGE(kTag, "no input devices initialised!"); ESP_LOGE(kTag, "no input devices initialised!");
@ -274,10 +380,12 @@ void Lua::entry() {
{"battery_millivolts", &sBatteryMv}, {"battery_millivolts", &sBatteryMv},
{"plugged_in", &sBatteryCharging}, {"plugged_in", &sBatteryCharging},
}); });
sLua->bridge().AddPropertyModule("bluetooth", sLua->bridge().AddPropertyModule(
{ "bluetooth", {
{"enabled", &sBluetoothEnabled}, {"enabled", &sBluetoothEnabled},
{"connected", &sBluetoothConnected}, {"connected", &sBluetoothConnected},
{"paired_device", &sBluetoothPairedDevice},
{"devices", &sBluetoothDevices},
}); });
sLua->bridge().AddPropertyModule("playback", sLua->bridge().AddPropertyModule("playback",
{ {
@ -299,6 +407,16 @@ void Lua::entry() {
{"limit_db", &sVolumeLimit}, {"limit_db", &sVolumeLimit},
}); });
sLua->bridge().AddPropertyModule("display",
{
{"brightness", &sDisplayBrightness},
});
sLua->bridge().AddPropertyModule("controls",
{
{"scheme", &sControlsScheme},
});
sLua->bridge().AddPropertyModule( sLua->bridge().AddPropertyModule(
"backstack", "backstack",
{ {
@ -311,6 +429,15 @@ void Lua::entry() {
{"hide", [&](lua_State* s) { return HideAlert(s); }}, {"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(); sCurrentScreen.reset();
sLua->RunScript("/lua/main.lua"); sLua->RunScript("/lua/main.lua");
} }

Loading…
Cancel
Save