diff --git a/Data/system/app/Boot/logo.png b/Data/system/app/Boot/assets/logo.png similarity index 100% rename from Data/system/app/Boot/logo.png rename to Data/system/app/Boot/assets/logo.png diff --git a/Data/system/app/Boot/assets/logo_small.png b/Data/system/app/Boot/assets/logo_small.png new file mode 100644 index 00000000..bce20678 Binary files /dev/null and b/Data/system/app/Boot/assets/logo_small.png differ diff --git a/Data/system/app/Boot/logo_usb.png b/Data/system/app/Boot/assets/logo_usb.png similarity index 100% rename from Data/system/app/Boot/logo_usb.png rename to Data/system/app/Boot/assets/logo_usb.png diff --git a/Tactility/Private/Tactility/app/serialconsole/ConnectView.h b/Tactility/Private/Tactility/app/serialconsole/ConnectView.h index d3451f92..f8b2ef2e 100644 --- a/Tactility/Private/Tactility/app/serialconsole/ConnectView.h +++ b/Tactility/Private/Tactility/app/serialconsole/ConnectView.h @@ -77,6 +77,14 @@ private: view->onConnect(); } + static lv_obj_t* createRowWrapper(lv_obj_t* parent) { + auto* wrapper = lv_obj_create(parent); + lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_all(wrapper, 0, LV_STATE_DEFAULT); + return wrapper; + } + public: explicit ConnectView(OnConnectedFunction onConnected) : onConnected(std::move(onConnected)) {} @@ -85,46 +93,55 @@ public: uartNames = hal::uart::getNames(); auto* wrapper = lv_obj_create(parent); - lv_obj_set_size(wrapper, LV_PCT(100), LV_PCT(100)); - lv_obj_set_style_pad_ver(wrapper, 0, 0); - lv_obj_set_style_border_width(wrapper, 0, 0); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); lvgl::obj_set_style_bg_invisible(wrapper); - busDropdown = lv_dropdown_create(wrapper); + // Bus selection + + auto* bus_wrapper = createRowWrapper(wrapper); + + busDropdown = lv_dropdown_create(bus_wrapper); auto bus_options = string::join(uartNames, "\n"); lv_dropdown_set_options(busDropdown, bus_options.c_str()); - lv_obj_align(busDropdown, LV_ALIGN_TOP_RIGHT, 0, 0); + lv_obj_align(busDropdown, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_set_width(busDropdown, LV_PCT(50)); - lv_obj_set_style_border_color(busDropdown, lv_color_hex(0xFAFAFA), LV_PART_MAIN); - lv_obj_set_style_border_width(busDropdown, 1, LV_PART_MAIN); + int32_t bus_index = 0; preferences.optInt32("bus", bus_index); if (bus_index < uartNames.size()) { lv_dropdown_set_selected(busDropdown, bus_index); } - auto* bus_label = lv_label_create(wrapper); - lv_obj_align(bus_label, LV_ALIGN_TOP_LEFT, 0, 10); + auto* bus_label = lv_label_create(bus_wrapper); + lv_obj_align(bus_label, LV_ALIGN_LEFT_MID, 0, 0); lv_label_set_text(bus_label, "Bus"); + // Baud rate selection + auto* baud_wrapper = createRowWrapper(wrapper); + int32_t speed = 115200; preferences.optInt32("speed", speed); - speedTextarea = lv_textarea_create(wrapper); + speedTextarea = lv_textarea_create(baud_wrapper); lv_textarea_set_text(speedTextarea, std::to_string(speed).c_str()); lv_textarea_set_one_line(speedTextarea, true); lv_obj_set_width(speedTextarea, LV_PCT(50)); - lv_obj_align(speedTextarea, LV_ALIGN_TOP_RIGHT, 0, 40); + lv_obj_align(speedTextarea, LV_ALIGN_TOP_RIGHT, 0, 0); - auto* baud_rate_label = lv_label_create(wrapper); - lv_obj_align(baud_rate_label, LV_ALIGN_TOP_LEFT, 0, 50); + auto* baud_rate_label = lv_label_create(baud_wrapper); + lv_obj_align(baud_rate_label, LV_ALIGN_TOP_LEFT, 0, 0); lv_label_set_text(baud_rate_label, "Baud"); - auto* connect_button = lv_button_create(wrapper); + // Connect + auto* connect_wrapper = createRowWrapper(wrapper); + + auto* connect_button = lv_button_create(connect_wrapper); + lv_obj_align(connect_button, LV_ALIGN_CENTER, 0, 0); + lv_obj_add_event_cb(connect_button, onConnectCallback, LV_EVENT_SHORT_CLICKED, this); auto* connect_label = lv_label_create(connect_button); lv_label_set_text(connect_label, "Connect"); - lv_obj_align(connect_button, LV_ALIGN_TOP_MID, 0, 90); - lv_obj_add_event_cb(connect_button, onConnectCallback, LV_EVENT_SHORT_CLICKED, this); } void onStop() override { diff --git a/Tactility/Private/Tactility/app/wificonnect/WifiConnect.h b/Tactility/Private/Tactility/app/wificonnect/WifiConnect.h index 656c8f07..1b10fca7 100644 --- a/Tactility/Private/Tactility/app/wificonnect/WifiConnect.h +++ b/Tactility/Private/Tactility/app/wificonnect/WifiConnect.h @@ -20,7 +20,7 @@ class WifiConnect final : public App { }; View view = View(&bindings, &state); PubSub::SubscriptionHandle wifiSubscription; - bool view_enabled = false; + bool viewEnabled = false; void onWifiEvent(service::wifi::WifiEvent event); diff --git a/Tactility/Private/Tactility/app/wifimanage/Bindings.h b/Tactility/Private/Tactility/app/wifimanage/Bindings.h index 1921c903..2cc612d7 100644 --- a/Tactility/Private/Tactility/app/wifimanage/Bindings.h +++ b/Tactility/Private/Tactility/app/wifimanage/Bindings.h @@ -1,11 +1,13 @@ #pragma once +#include + namespace tt::app::wifimanage { typedef void (*OnWifiToggled)(bool enable); -typedef void (*OnConnectSsid)(const char* ssid); +typedef void (*OnConnectSsid)(const std::string& ssid); typedef void (*OnDisconnect)(); -typedef void (*OnShowApSettings)(const char* ssid); +typedef void (*OnShowApSettings)(const std::string& ssid); typedef void (*OnConnectToHidden)(); struct Bindings{ diff --git a/Tactility/Private/Tactility/app/wifimanage/State.h b/Tactility/Private/Tactility/app/wifimanage/State.h index dc41e3e5..5393944e 100644 --- a/Tactility/Private/Tactility/app/wifimanage/State.h +++ b/Tactility/Private/Tactility/app/wifimanage/State.h @@ -37,6 +37,12 @@ public: }); } + std::vector getApRecords() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + return apRecords; + } + void setConnectSsid(const std::string& ssid); std::string getConnectSsid() const; }; diff --git a/Tactility/Private/Tactility/app/wifimanage/View.h b/Tactility/Private/Tactility/app/wifimanage/View.h index 029a4f57..475c4db3 100644 --- a/Tactility/Private/Tactility/app/wifimanage/View.h +++ b/Tactility/Private/Tactility/app/wifimanage/View.h @@ -26,7 +26,10 @@ class View final { void updateScanning(); void updateNetworkList(); void updateConnectToHidden(); - void createSsidListItem(const service::wifi::ApRecord& record, bool isConnecting); + void createSsidListItem(const service::wifi::ApRecord& record, bool isConnecting, size_t index); + + static void showDetails(lv_event_t* event); + static void connect(lv_event_t* event); public: diff --git a/Tactility/Source/app/boot/Boot.cpp b/Tactility/Source/app/boot/Boot.cpp index b664b71c..0736f90b 100644 --- a/Tactility/Source/app/boot/Boot.cpp +++ b/Tactility/Source/app/boot/Boot.cpp @@ -130,6 +130,13 @@ class BootApp : public App { service::loader::startApp(boot_properties.launcherAppId); } + static int getSmallestDimension() { + auto* display = lv_display_get_default(); + int width = lv_display_get_horizontal_resolution(display); + int height = lv_display_get_vertical_resolution(display); + return std::min(width, height); + } + public: void onCreate(AppContext& app) override { @@ -153,7 +160,13 @@ public: lv_obj_align(image, LV_ALIGN_CENTER, 0, 0); const auto paths = app.getPaths(); - const char* logo = hal::usb::isUsbBootMode() ? "logo_usb.png" : "logo.png"; + const char* logo; + // TODO: Replace with automatic asset buckets like on Android + if (getSmallestDimension() < 150) { // e.g. Cardputer + logo = hal::usb::isUsbBootMode() ? "assets/logo_usb.png" : "assets/logo_small.png"; + } else { + logo = hal::usb::isUsbBootMode() ? "assets/logo_usb.png" : "assets/logo.png"; + } const auto logo_path = paths->getSystemPathLvgl(logo); TT_LOG_I(TAG, "%s", logo_path.c_str()); lv_image_set_src(image, logo_path.c_str()); diff --git a/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp b/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp index 949d19eb..30d84169 100644 --- a/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp +++ b/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp @@ -1,5 +1,7 @@ #ifdef ESP_PLATFORM +#include "Tactility/hal/Device.h" + #include #include #include @@ -32,7 +34,11 @@ public: lv_obj_align(top_label, LV_ALIGN_TOP_MID, 0, 2); auto* bottom_label = lv_label_create(parent); - lv_label_set_text(bottom_label, "Tap screen to continue"); + if (hal::hasDevice(hal::Device::Type::Touch)) { + lv_label_set_text(bottom_label, "Tap screen to continue"); + } else { + lv_label_set_text(bottom_label, "Reboot device to continue"); + } lv_obj_align(bottom_label, LV_ALIGN_BOTTOM_MID, 0, -2); std::string url = getUrlFromCrashData(); diff --git a/Tactility/Source/app/development/Development.cpp b/Tactility/Source/app/development/Development.cpp index db8f763e..7914e58c 100644 --- a/Tactility/Source/app/development/Development.cpp +++ b/Tactility/Source/app/development/Development.cpp @@ -93,8 +93,6 @@ public: lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); - // Toolbar - lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_t* toolbar = lvgl::toolbar_create(parent, app); enableSwitch = lvgl::toolbar_add_switch_action(toolbar); diff --git a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp index a40ed0c1..2dfd09c1 100644 --- a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp +++ b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp @@ -1,3 +1,5 @@ +#include "Tactility/lvgl/LvglSync.h" + #include #include #include @@ -22,28 +24,30 @@ void start(const std::string& ssid) { app::start(manifest.id, bundle); } -static void onPressForget(TT_UNUSED lv_event_t* event) { - std::vector choices = { - "Yes", - "No" - }; - alertdialog::start("Confirmation", "Forget the Wi-Fi access point?", choices); -} +class WifiApSettings : public App { -static void onToggleAutoConnect(lv_event_t* event) { - lv_event_code_t code = lv_event_get_code(event); + bool viewEnabled = false; + lv_obj_t* busySpinner = nullptr; + lv_obj_t* connectButton = nullptr; + lv_obj_t* disconnectButton = nullptr; + std::string ssid; + PubSub::SubscriptionHandle wifiSubscription = nullptr; - auto app = getCurrentAppContext(); - auto parameters = app->getParameters(); - tt_check(parameters != nullptr, "Parameters missing"); + static void onPressForget(TT_UNUSED lv_event_t* event) { + std::vector choices = { + "Yes", + "No" + }; + alertdialog::start("Confirmation", "Forget the Wi-Fi access point?", choices); + } - if (code == LV_EVENT_VALUE_CHANGED) { + static void onToggleAutoConnect(lv_event_t* event) { + auto* self = static_cast(lv_event_get_user_data(event)); auto* enable_switch = static_cast(lv_event_get_target(event)); bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED); - std::string ssid = parameters->getString("ssid"); service::wifi::settings::WifiApSettings settings; - if (service::wifi::settings::load(ssid.c_str(), settings)) { + if (service::wifi::settings::load(self->ssid.c_str(), settings)) { settings.autoConnect = is_on; if (!service::wifi::settings::save(settings)) { TT_LOG_E(TAG, "Failed to save settings"); @@ -52,52 +56,134 @@ static void onToggleAutoConnect(lv_event_t* event) { TT_LOG_E(TAG, "Failed to load settings"); } } -} -class WifiApSettings : public App { + static void onPressConnect(lv_event_t* event) { + auto app = getCurrentAppContext(); + auto parameters = app->getParameters(); + tt_check(parameters != nullptr, "Parameters missing"); + + std::string ssid = parameters->getString("ssid"); + service::wifi::settings::WifiApSettings settings; + if (service::wifi::settings::load(ssid.c_str(), settings)) { + auto* button = lv_event_get_target_obj(event); + lv_obj_add_state(button, LV_STATE_DISABLED); + service::wifi::connect(settings, false); + } + } + + static void onPressDisconnect(lv_event_t* event) { + if (service::wifi::getRadioState() == service::wifi::RadioState::ConnectionActive) { + auto* button = lv_event_get_target_obj(event); + lv_obj_add_state(button, LV_STATE_DISABLED); + service::wifi::disconnect(); + } + } + + void onWifiEvent(service::wifi::WifiEvent event) const { + requestViewUpdate(); + } + + void requestViewUpdate() const { + if (viewEnabled) { + if (lvgl::lock(1000)) { + updateViews(); + lvgl::unlock(); + } else { + TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL"); + } + } + } + + void updateConnectButton() const { + if (service::wifi::getConnectionTarget() == ssid && service::wifi::getRadioState() == service::wifi::RadioState::ConnectionActive) { + lv_obj_remove_flag(disconnectButton, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(connectButton, LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_state(disconnectButton, LV_STATE_DISABLED); + } else { + lv_obj_add_flag(disconnectButton, LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_flag(connectButton, LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_state(connectButton, LV_STATE_DISABLED); + } + } + + void updateBusySpinner() const { + if (service::wifi::getRadioState() == service::wifi::RadioState::ConnectionPending) { + lv_obj_remove_flag(busySpinner, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(busySpinner, LV_OBJ_FLAG_HIDDEN); + } + } + + void updateViews() const { + updateConnectButton(); + updateBusySpinner(); + } + +public: + + void onCreate(AppContext& app) override { + const auto parameters = app.getParameters(); + tt_check(parameters != nullptr, "Parameters missing"); + ssid = parameters->getString("ssid"); + } void onShow(AppContext& app, lv_obj_t* parent) override { - auto paremeters = app.getParameters(); - tt_check(paremeters != nullptr, "Parameters missing"); - std::string ssid = paremeters->getString("ssid"); + wifiSubscription = service::wifi::getPubsub()->subscribe([this](auto event) { + requestViewUpdate(); + }); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); - lvgl::toolbar_create(parent, ssid); - - // Wrappers + auto* toolbar = lvgl::toolbar_create(parent, ssid); + busySpinner = lvgl::toolbar_add_spinner_action(toolbar); auto* wrapper = lv_obj_create(parent); lv_obj_set_width(wrapper, LV_PCT(100)); lv_obj_set_flex_grow(wrapper, 1); lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); lvgl::obj_set_style_bg_invisible(wrapper); - // Auto-connect toggle + disconnectButton = lv_button_create(wrapper); + lv_obj_set_width(disconnectButton, LV_PCT(100)); + lv_obj_add_event_cb(disconnectButton, onPressDisconnect, LV_EVENT_SHORT_CLICKED, nullptr); + auto* disconnect_label = lv_label_create(disconnectButton); + lv_obj_align(disconnect_label, LV_ALIGN_CENTER, 0, 0); + lv_label_set_text(disconnect_label, "Disconnect"); - auto* auto_connect_wrapper = lv_obj_create(wrapper); - lv_obj_set_size(auto_connect_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(auto_connect_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_pad_gap(auto_connect_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(auto_connect_wrapper, 0, LV_STATE_DEFAULT); + connectButton = lv_button_create(wrapper); + lv_obj_set_width(connectButton, LV_PCT(100)); + lv_obj_add_event_cb(connectButton, onPressConnect, LV_EVENT_SHORT_CLICKED, nullptr); + auto* connect_label = lv_label_create(connectButton); + lv_obj_align(connect_label, LV_ALIGN_CENTER, 0, 0); + lv_label_set_text(connect_label, "Connect"); - auto* auto_connect_label = lv_label_create(auto_connect_wrapper); - lv_label_set_text(auto_connect_label, "Auto-connect"); - lv_obj_align(auto_connect_label, LV_ALIGN_TOP_LEFT, 0, 6); - - auto* auto_connect_switch = lv_switch_create(auto_connect_wrapper); - lv_obj_add_event_cb(auto_connect_switch, onToggleAutoConnect, LV_EVENT_VALUE_CHANGED, &paremeters); - lv_obj_align(auto_connect_switch, LV_ALIGN_TOP_RIGHT, 0, 0); + // Forget auto* forget_button = lv_button_create(wrapper); lv_obj_set_width(forget_button, LV_PCT(100)); - lv_obj_align_to(forget_button, auto_connect_wrapper, LV_ALIGN_OUT_BOTTOM_MID, 0, 10); lv_obj_add_event_cb(forget_button, onPressForget, LV_EVENT_SHORT_CLICKED, nullptr); auto* forget_button_label = lv_label_create(forget_button); lv_obj_align(forget_button_label, LV_ALIGN_CENTER, 0, 0); lv_label_set_text(forget_button_label, "Forget"); + // Auto-connect + + auto* auto_connect_wrapper = lv_obj_create(wrapper); + lv_obj_set_size(auto_connect_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lvgl::obj_set_style_bg_invisible(auto_connect_wrapper); + lv_obj_set_style_pad_all(auto_connect_wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(auto_connect_wrapper, 0, LV_STATE_DEFAULT); + + auto* auto_connect_label = lv_label_create(auto_connect_wrapper); + lv_label_set_text(auto_connect_label, "Auto-connect"); + lv_obj_align(auto_connect_label, LV_ALIGN_LEFT_MID, 0, 0); + + auto* auto_connect_switch = lv_switch_create(auto_connect_wrapper); + lv_obj_add_event_cb(auto_connect_switch, onToggleAutoConnect, LV_EVENT_VALUE_CHANGED, this); + lv_obj_align(auto_connect_switch, LV_ALIGN_RIGHT_MID, 0, 0); + service::wifi::settings::WifiApSettings settings; if (service::wifi::settings::load(ssid.c_str(), settings)) { if (settings.autoConnect) { @@ -110,33 +196,47 @@ class WifiApSettings : public App { lv_obj_add_flag(forget_button, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(auto_connect_wrapper, LV_OBJ_FLAG_HIDDEN); } + + viewEnabled = true; + + updateViews(); + } + + void onHide(AppContext& app) override { + service::wifi::getPubsub()->unsubscribe(wifiSubscription); + wifiSubscription = nullptr; + viewEnabled = false; } void onResult(TT_UNUSED AppContext& appContext, TT_UNUSED LaunchId launchId, TT_UNUSED Result result, std::unique_ptr bundle) override { - if (result == Result::Ok && bundle != nullptr) { - auto index = alertdialog::getResultIndex(*bundle); - if (index == 0) { // Yes - auto parameters = appContext.getParameters(); - tt_check(parameters != nullptr, "Parameters missing"); - - std::string ssid = parameters->getString("ssid"); - if (service::wifi::settings::remove(ssid.c_str())) { - TT_LOG_I(TAG, "Removed SSID"); - - if ( - service::wifi::getRadioState() == service::wifi::RadioState::ConnectionActive && - service::wifi::getConnectionTarget() == ssid - ) { - service::wifi::disconnect(); - } - - // Stop self - app::stop(); - } else { - TT_LOG_E(TAG, "Failed to remove SSID"); - } - } + if (result != Result::Ok || bundle == nullptr) { + return; } + + auto index = alertdialog::getResultIndex(*bundle); + if (index != 0) { // 0 = Yes + return; + } + + auto parameters = appContext.getParameters(); + tt_check(parameters != nullptr, "Parameters missing"); + + std::string ssid = parameters->getString("ssid"); + if (!service::wifi::settings::remove(ssid.c_str())) { + TT_LOG_E(TAG, "Failed to remove SSID"); + return; + } + + TT_LOG_I(TAG, "Removed SSID"); + if ( + service::wifi::getRadioState() == service::wifi::RadioState::ConnectionActive && + service::wifi::getConnectionTarget() == ssid + ) { + service::wifi::disconnect(); + } + + // Stop app + stop(); } }; diff --git a/Tactility/Source/app/wificonnect/WifiConnect.cpp b/Tactility/Source/app/wificonnect/WifiConnect.cpp index 3cb07abc..4fd8d1bb 100644 --- a/Tactility/Source/app/wificonnect/WifiConnect.cpp +++ b/Tactility/Source/app/wificonnect/WifiConnect.cpp @@ -67,7 +67,7 @@ void WifiConnect::unlock() { void WifiConnect::requestViewUpdate() { lock(); - if (view_enabled) { + if (viewEnabled) { if (lvgl::lock(1000)) { view.update(); lvgl::unlock(); @@ -80,7 +80,7 @@ void WifiConnect::requestViewUpdate() { void WifiConnect::onShow(AppContext& app, lv_obj_t* parent) { lock(); - view_enabled = true; + viewEnabled = true; view.init(app, parent); view.update(); unlock(); @@ -89,7 +89,7 @@ void WifiConnect::onShow(AppContext& app, lv_obj_t* parent) { void WifiConnect::onHide(TT_UNUSED AppContext& app) { // No need to lock view, as this is called from within Gui's LVGL context lock(); - view_enabled = false; + viewEnabled = false; unlock(); } diff --git a/Tactility/Source/app/wifimanage/View.cpp b/Tactility/Source/app/wifimanage/View.cpp index 6548437a..b921db24 100644 --- a/Tactility/Source/app/wifimanage/View.cpp +++ b/Tactility/Source/app/wifimanage/View.cpp @@ -1,3 +1,5 @@ +#include + #include #include @@ -5,7 +7,6 @@ #include #include -#include #include #include @@ -21,7 +22,7 @@ constexpr auto* TAG = "WifiManageView"; std::shared_ptr _Nullable optWifiManage(); -uint8_t mapRssiToPercentage(int rssi) { +static uint8_t mapRssiToPercentage(int rssi) { auto abs_rssi = std::abs(rssi); if (abs_rssi < 30U) { abs_rssi = 30U; @@ -33,28 +34,31 @@ uint8_t mapRssiToPercentage(int rssi) { return (uint8_t)percentage; } -static void on_enable_switch_changed(lv_event_t* event) { - lv_event_code_t code = lv_event_get_code(event); +static void onEnableSwitchChanged(lv_event_t* event) { auto* enable_switch = static_cast(lv_event_get_target(event)); - if (code == LV_EVENT_VALUE_CHANGED) { - bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED); + bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED); - auto wifi = std::static_pointer_cast(getCurrentApp()); - auto bindings = wifi->getBindings(); + auto wifi = std::static_pointer_cast(getCurrentApp()); + auto bindings = wifi->getBindings(); - bindings.onWifiToggled(is_on); - } + bindings.onWifiToggled(is_on); } -static void on_enable_on_boot_switch_changed(lv_event_t* event) { - lv_event_code_t code = lv_event_get_code(event); +static void onEnableOnBootSwitchChanged(lv_event_t* event) { auto* enable_switch = static_cast(lv_event_get_target(event)); - if (code == LV_EVENT_VALUE_CHANGED) { - bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED); - // Dispatch it, so file IO doesn't block the UI - getMainDispatcher().dispatch([is_on] { - service::wifi::settings::setEnableOnBoot(is_on); - }); + bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED); + // Dispatch it, so file IO doesn't block the UI + getMainDispatcher().dispatch([is_on] { + service::wifi::settings::setEnableOnBoot(is_on); + }); +} + +static void onEnableOnBootParentClicked(lv_event_t* event) { + auto* enable_switch = static_cast(lv_event_get_user_data(event)); + if (lv_obj_has_state(enable_switch, LV_STATE_CHECKED)) { + lv_obj_remove_state(enable_switch, LV_STATE_CHECKED); + } else { + lv_obj_add_state(enable_switch, LV_STATE_CHECKED); } } @@ -65,101 +69,67 @@ static void onConnectToHiddenClicked(lv_event_t* event) { // region Secondary updates -static void connect(lv_event_t* event) { - auto* wrapper = lv_event_get_current_target_obj(event); - // Assumes that the second child of the button is a label ... risky - auto* label = lv_obj_get_child(wrapper, 0); - // We get the SSID from the button label because it's safer than alloc'ing - // our own and passing it as the event data - const char* ssid = lv_label_get_text(label); - if (ssid != nullptr) { - TT_LOG_I(TAG, "Clicked AP: %s", ssid); - auto* bindings = (Bindings*)lv_event_get_user_data(event); +void View::connect(lv_event_t* event) { + TT_LOG_D(TAG, "connect()"); + auto* widget = lv_event_get_current_target_obj(event); + auto index = reinterpret_cast(lv_obj_get_user_data(widget)); + auto* self = static_cast(lv_event_get_user_data(event)); + auto ap_records = self->state->getApRecords(); + if (index < ap_records.size()) { + TT_LOG_I(TAG, "Clicked %d/%d", index, ap_records.size() - 1); + auto& ssid = ap_records[index].ssid; + TT_LOG_I(TAG, "Clicked AP: %s", ssid.c_str()); std::string connection_target = service::wifi::getConnectionTarget(); if (connection_target == ssid) { - bindings->onDisconnect(); + self->bindings->onDisconnect(); } else { - bindings->onConnectSsid(ssid); + self->bindings->onConnectSsid(ssid); } - } -} - -static void showDetails(lv_event_t* event) { - auto* wrapper = lv_event_get_current_target_obj(event); - // Hack: Get the hidden label with the ssid - auto* ssid_label = lv_obj_get_child(wrapper, 1); - const char* ssid = lv_label_get_text(ssid_label); - auto* bindings = (Bindings*)lv_event_get_user_data(event); - bindings->onShowApSettings(ssid); -} - -void View::createSsidListItem(const service::wifi::ApRecord& record, bool isConnecting) { - auto ui_scale = hal::getConfiguration()->uiScale; - - auto* wrapper = lv_obj_create(networks_list); - lv_obj_add_event_cb(wrapper, &connect, LV_EVENT_SHORT_CLICKED, bindings); - lv_obj_set_user_data(wrapper, bindings); - lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_pad_gap(wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_margin_all(wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); - - auto* label = lv_label_create(wrapper); - lv_obj_align(label, LV_ALIGN_LEFT_MID, 0, 0); - lv_label_set_text(label, record.ssid.c_str()); - lv_label_set_long_mode(label, LV_LABEL_LONG_SCROLL_CIRCULAR); - lv_obj_set_width(label, LV_PCT(70)); - - auto* info_wrapper = lv_obj_create(wrapper); - lv_obj_set_style_margin_all(info_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_align(info_wrapper, LV_ALIGN_RIGHT_MID); - - if (ui_scale == hal::UiScale::Smallest) { - lv_obj_set_size(info_wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_obj_set_style_pad_hor(info_wrapper, 4, LV_STATE_DEFAULT); } else { - lv_obj_set_size(info_wrapper, 36, 36); - lv_obj_set_style_pad_all(info_wrapper, 0, LV_STATE_DEFAULT); + TT_LOG_W(TAG, "Clicked AP: record %d/%d does not exist", index, ap_records.size() - 1); } +} - lv_obj_set_style_border_color(info_wrapper, lv_theme_get_color_primary(info_wrapper), 0); - lv_obj_add_event_cb(info_wrapper, &showDetails, LV_EVENT_SHORT_CLICKED, bindings); - lv_obj_align(info_wrapper, LV_ALIGN_RIGHT_MID, 0, 0); +void View::showDetails(lv_event_t* event) { + TT_LOG_D(TAG, "showDetails()"); + auto* widget = lv_event_get_current_target_obj(event); + auto index = reinterpret_cast(lv_obj_get_user_data(widget)); + auto* self = static_cast(lv_event_get_user_data(event)); + auto ap_records = self->state->getApRecords(); - auto* info_label = lv_label_create(info_wrapper); - lv_label_set_text(info_label, "i"); - // Hack: Create a hidden label to store data and pass it to the callback - auto* ssid_label = lv_label_create(info_wrapper); - lv_label_set_text(ssid_label, record.ssid.c_str()); - lv_obj_add_flag(ssid_label, LV_OBJ_FLAG_HIDDEN); - lv_obj_set_style_text_color(info_label, lv_theme_get_color_primary(info_wrapper), LV_STATE_DEFAULT); - lv_obj_align(info_label, LV_ALIGN_CENTER, 0, 0); + if (index < ap_records.size()) { + auto& ssid = ap_records[index].ssid; + TT_LOG_I(TAG, "Clicked AP: %s", ssid.c_str()); + self->bindings->onShowApSettings(ssid); + } else { + TT_LOG_W(TAG, "Clicked AP: record %d/%d does not exist", index, ap_records.size() - 1); + } +} +void View::createSsidListItem(const service::wifi::ApRecord& record, bool isConnecting, size_t index) { if (isConnecting) { - auto* connecting_spinner = lvgl::spinner_create(wrapper); - auto spinner_offset_x = (ui_scale == hal::UiScale::Smallest) ? -2 : -8; - lv_obj_align_to(connecting_spinner, info_wrapper, LV_ALIGN_OUT_LEFT_MID, spinner_offset_x, 0); + auto* button = lv_list_add_button(networks_list, LV_SYMBOL_WIFI, record.ssid.c_str()); + lv_obj_add_event_cb(button, showDetails, LV_EVENT_SHORT_CLICKED, this); } else { - auto percentage = mapRssiToPercentage(record.rssi); - - std::string auth_info; - if (record.auth_mode == WIFI_AUTH_OPEN) { - auth_info = "(open)"; + const std::string auth_info = (record.auth_mode == WIFI_AUTH_OPEN) ? "(open) " : " "; + const auto percentage = mapRssiToPercentage(record.rssi); + const auto label = std::format("{} {}{}%", record.ssid, auth_info, percentage); + auto* button = lv_list_add_button(networks_list, nullptr, label.c_str()); + lv_obj_set_user_data(button, reinterpret_cast(index)); + if (service::wifi::settings::contains(record.ssid)) { + lv_obj_add_event_cb(button, showDetails, LV_EVENT_SHORT_CLICKED, this); } else { - auth_info = ""; + lv_obj_add_event_cb(button, connect, LV_EVENT_SHORT_CLICKED, this); } - - auto signal = std::format("{}{}%", auth_info, percentage); - auto* signal_label = lv_label_create(wrapper); - lv_label_set_text(signal_label, signal.c_str()); - auto info_label_offset = (ui_scale == hal::UiScale::Smallest) ? -4 : -16; - lv_obj_align_to(signal_label, info_wrapper, LV_ALIGN_OUT_LEFT_MID, info_label_offset, 0); } } void View::updateConnectToHidden() { + if (connect_to_hidden == nullptr) { + return; + } + using enum service::wifi::RadioState; switch (state->getRadioState()) { case On: @@ -179,6 +149,30 @@ void View::updateConnectToHidden() { void View::updateNetworkList() { lv_obj_clean(networks_list); + // Enable on boot + + auto* enable_on_boot_wrapper = lv_obj_create(networks_list); + lv_obj_set_size(enable_on_boot_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT); + + auto* enable_label = lv_label_create(enable_on_boot_wrapper); + lv_label_set_text(enable_label, "Enable on boot"); + lv_obj_align(enable_label, LV_ALIGN_LEFT_MID, 0, 0); + + enable_on_boot_switch = lv_switch_create(enable_on_boot_wrapper); + lv_obj_align(enable_on_boot_switch, LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_add_event_cb(enable_on_boot_switch, onEnableOnBootSwitchChanged, LV_EVENT_VALUE_CHANGED, bindings); + lv_obj_add_event_cb(enable_on_boot_wrapper, onEnableOnBootParentClicked, LV_EVENT_SHORT_CLICKED, enable_on_boot_switch); + + if (hal::getConfiguration()->uiScale == hal::UiScale::Smallest) { + lv_obj_set_style_pad_ver(enable_on_boot_wrapper, 2, LV_STATE_DEFAULT); + } else { + lv_obj_set_style_pad_ver(enable_on_boot_wrapper, 8, LV_STATE_DEFAULT); + } + + updateEnableOnBootToggle(); + switch (state->getRadioState()) { using enum service::wifi::RadioState; case OnPending: @@ -188,61 +182,71 @@ void View::updateNetworkList() { std::string connection_target = service::wifi::getConnectionTarget(); - state->withApRecords([this, &connection_target](const std::vector& apRecords){ - bool is_connected = !connection_target.empty() && - state->getRadioState() == ConnectionActive; - bool added_connected = false; - if (is_connected && !apRecords.empty()) { - for (auto &record : apRecords) { - if (record.ssid == connection_target) { - lv_list_add_text(networks_list, "Connected"); - createSsidListItem(record, false); - added_connected = true; - break; - } + // Make safe copy + auto ap_records = state->getApRecords(); + + bool is_connected = !connection_target.empty() && + state->getRadioState() == ConnectionActive; + bool added_connected = false; + if (is_connected && !ap_records.empty()) { + for (int i = 0; i < ap_records.size(); ++i) { + auto& record = ap_records[i]; + if (record.ssid == connection_target) { + lv_list_add_text(networks_list, "Connected"); + createSsidListItem(record, false, i); + added_connected = true; + break; } } + } - lv_list_add_text(networks_list, "Other networks"); - std::set used_ssids; - if (!apRecords.empty()) { - for (auto& record : apRecords) { - if (used_ssids.find(record.ssid) == used_ssids.end()) { - bool connection_target_match = (record.ssid == connection_target); - bool is_connecting = connection_target_match - && state->getRadioState() == ConnectionPending && - !connection_target.empty(); - bool skip = connection_target_match && added_connected; - if (!skip) { - createSsidListItem(record, is_connecting); - } - used_ssids.insert(record.ssid); + lv_list_add_text(networks_list, "Other networks"); + std::set used_ssids; + if (!ap_records.empty()) { + for (int i = 0; i < ap_records.size(); ++i) { + auto& record = ap_records[i]; + if (!used_ssids.contains(record.ssid)) { + bool connection_target_match = (record.ssid == connection_target); + bool is_connecting = connection_target_match + && state->getRadioState() == ConnectionPending && + !connection_target.empty(); + bool skip = connection_target_match && added_connected; + if (!skip) { + createSsidListItem(record, is_connecting, i); } + used_ssids.insert(record.ssid); } - lv_obj_clear_flag(networks_list, LV_OBJ_FLAG_HIDDEN); - } else if (!state->hasScannedAfterRadioOn() || state->isScanning()) { - // hasScannedAfterRadioOn() prevents briefly showing "No networks found" when turning radio on. - lv_obj_add_flag(networks_list, LV_OBJ_FLAG_HIDDEN); - } else { - lv_obj_clear_flag(networks_list, LV_OBJ_FLAG_HIDDEN); - lv_obj_t* label = lv_label_create(networks_list); - lv_label_set_text(label, "No networks found."); } - }); + lv_obj_clear_flag(networks_list, LV_OBJ_FLAG_HIDDEN); + } else if (!state->hasScannedAfterRadioOn() || state->isScanning()) { + // hasScannedAfterRadioOn() prevents briefly showing "No networks found" when turning radio on. + lv_obj_add_flag(networks_list, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_clear_flag(networks_list, LV_OBJ_FLAG_HIDDEN); + lv_obj_t* label = lv_label_create(networks_list); + lv_label_set_text(label, "No networks found."); + } + connect_to_hidden = lv_button_create(networks_list); + lv_obj_set_width(connect_to_hidden, LV_PCT(100)); + lv_obj_set_style_margin_ver(connect_to_hidden, 4, LV_STATE_DEFAULT); + auto* connect_to_hidden_label = lv_label_create(connect_to_hidden); + lv_label_set_text(connect_to_hidden_label, "Connect to hidden SSID"); + lv_obj_add_event_cb(connect_to_hidden, onConnectToHiddenClicked, LV_EVENT_SHORT_CLICKED, bindings); break; } - case OffPending: - case Off: { - lv_obj_add_flag(networks_list, LV_OBJ_FLAG_HIDDEN); + + default: + connect_to_hidden = nullptr; + // Nothing to do break; - } } + } void View::updateScanning() { if (state->getRadioState() == service::wifi::RadioState::On && state->isScanning()) { - lv_obj_clear_flag(scanning_spinner, LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_flag(scanning_spinner, LV_OBJ_FLAG_HIDDEN); } else { lv_obj_add_flag(scanning_spinner, LV_OBJ_FLAG_HIDDEN); } @@ -271,11 +275,13 @@ void View::updateWifiToggle() { } void View::updateEnableOnBootToggle() { - lv_obj_clear_state(enable_on_boot_switch, LV_STATE_ANY); - if (service::wifi::settings::shouldEnableOnBoot()) { - lv_obj_add_state(enable_on_boot_switch, LV_STATE_CHECKED); - } else { - lv_obj_remove_state(enable_on_boot_switch, LV_STATE_CHECKED); + if (enable_on_boot_switch != nullptr) { + lv_obj_clear_state(enable_on_boot_switch, LV_STATE_ANY); + if (service::wifi::settings::shouldEnableOnBoot()) { + lv_obj_add_state(enable_on_boot_switch, LV_STATE_CHECKED); + } else { + lv_obj_remove_state(enable_on_boot_switch, LV_STATE_CHECKED); + } } } @@ -284,7 +290,6 @@ void View::updateEnableOnBootToggle() { // region Main void View::init(const AppContext& app, lv_obj_t* parent) { - auto ui_scale = hal::getConfiguration()->uiScale; lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); @@ -300,64 +305,17 @@ void View::init(const AppContext& app, lv_obj_t* parent) { scanning_spinner = lvgl::toolbar_add_spinner_action(toolbar); enable_switch = lvgl::toolbar_add_switch_action(toolbar); - lv_obj_add_event_cb(enable_switch, on_enable_switch_changed, LV_EVENT_VALUE_CHANGED, bindings); + lv_obj_add_event_cb(enable_switch, onEnableSwitchChanged, LV_EVENT_VALUE_CHANGED, bindings); - // Wrappers + // Networks - auto* flex_wrapper = lv_obj_create(parent); - lv_obj_set_width(flex_wrapper, LV_PCT(100)); - lv_obj_set_flex_grow(flex_wrapper, 1); - lv_obj_set_flex_flow(flex_wrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_set_style_border_width(flex_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_pad_all(flex_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_pad_gap(flex_wrapper, 0, LV_STATE_DEFAULT); - lvgl::obj_set_style_bg_invisible(flex_wrapper); - - // Fixed size content wrapper: align() methods don't work on flex, so we need this extra wrapper - - auto* content_wrapper = lv_obj_create(flex_wrapper); - lv_obj_set_size(content_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lvgl::obj_set_style_bg_invisible(content_wrapper); - lv_obj_set_style_border_width(content_wrapper, 0, LV_STATE_DEFAULT); - - // Enable on boot - - auto* enable_on_boot_wrapper = lv_obj_create(content_wrapper); - lv_obj_set_size(enable_on_boot_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT); - - auto* enable_label = lv_label_create(enable_on_boot_wrapper); - lv_label_set_text(enable_label, "Enable on boot"); - lv_obj_align(enable_label, LV_ALIGN_LEFT_MID, 0, 0); - - enable_on_boot_switch = lv_switch_create(enable_on_boot_wrapper); - lv_obj_add_event_cb(enable_on_boot_switch, on_enable_on_boot_switch_changed, LV_EVENT_VALUE_CHANGED, bindings); - lv_obj_align(enable_on_boot_switch, LV_ALIGN_RIGHT_MID, 0, 0); - - // Networks - - networks_list = lv_obj_create(content_wrapper); - lv_obj_set_flex_flow(networks_list, LV_FLEX_FLOW_COLUMN); + networks_list = lv_list_create(parent); + lv_obj_set_flex_grow(networks_list, 1); lv_obj_set_width(networks_list, LV_PCT(100)); - lv_obj_set_height(networks_list, LV_SIZE_CONTENT); - lv_obj_set_style_pad_top(networks_list, 0, LV_STATE_DEFAULT); - lv_obj_set_style_pad_bottom(networks_list, 0, LV_STATE_DEFAULT); - const int network_list_y_offset = ui_scale == hal::UiScale::Smallest ? 22 : 44; - lv_obj_align(networks_list, LV_ALIGN_TOP_LEFT, 0, network_list_y_offset); - - connect_to_hidden = lv_button_create(flex_wrapper); - lv_obj_set_width(connect_to_hidden, LV_PCT(100)); - lv_obj_set_style_margin_bottom(connect_to_hidden, 8, LV_STATE_DEFAULT); - lv_obj_set_style_margin_hor(connect_to_hidden, 12, LV_STATE_DEFAULT); - lv_obj_add_event_cb(connect_to_hidden, onConnectToHiddenClicked, LV_EVENT_SHORT_CLICKED, bindings); - auto* connect_to_hidden_label = lv_label_create(connect_to_hidden); - lv_label_set_text(connect_to_hidden_label, "Connect to hidden SSID"); } void View::update() { updateWifiToggle(); - updateEnableOnBootToggle(); updateScanning(); updateNetworkList(); updateConnectToHidden(); diff --git a/Tactility/Source/app/wifimanage/WifiManage.cpp b/Tactility/Source/app/wifimanage/WifiManage.cpp index 46995b12..a132c5fa 100644 --- a/Tactility/Source/app/wifimanage/WifiManage.cpp +++ b/Tactility/Source/app/wifimanage/WifiManage.cpp @@ -13,7 +13,7 @@ constexpr auto TAG = "WifiManage"; extern const AppManifest manifest; -static void onConnect(const char* ssid) { +static void onConnect(const std::string& ssid) { service::wifi::settings::WifiApSettings settings; if (service::wifi::settings::load(ssid, settings)) { TT_LOG_I(TAG, "Connecting with known credentials"); @@ -24,7 +24,7 @@ static void onConnect(const char* ssid) { } } -static void onShowApSettings(const char* ssid) { +static void onShowApSettings(const std::string& ssid) { wifiapsettings::start(ssid); } diff --git a/Tactility/Source/lvgl/wrappers/button.cpp b/Tactility/Source/lvgl/wrappers/button.cpp index f4f9504d..f30b41e5 100644 --- a/Tactility/Source/lvgl/wrappers/button.cpp +++ b/Tactility/Source/lvgl/wrappers/button.cpp @@ -13,7 +13,7 @@ lv_obj_t* __wrap_lv_button_create(lv_obj_t* parent) { if (tt::hal::getConfiguration()->uiScale == tt::hal::UiScale::Smallest) { lv_obj_set_style_pad_all(button, 2, LV_STATE_DEFAULT); - lv_obj_set_style_radius(button, 2, LV_STATE_DEFAULT); + lv_obj_set_style_radius(button, 3, LV_STATE_DEFAULT); } return button; diff --git a/Tactility/Source/lvgl/wrappers/obj.cpp b/Tactility/Source/lvgl/wrappers/obj.cpp index b82fa118..402a73f2 100644 --- a/Tactility/Source/lvgl/wrappers/obj.cpp +++ b/Tactility/Source/lvgl/wrappers/obj.cpp @@ -13,7 +13,7 @@ void __wrap_lv_obj_set_flex_flow(lv_obj_t* obj, lv_flex_flow_t flow) { __real_lv_obj_set_flex_flow(obj, flow); if (tt::hal::getConfiguration()->uiScale == tt::hal::UiScale::Smallest) { - lv_obj_set_style_pad_gap(obj, 2, LV_STATE_DEFAULT); + lv_obj_set_style_pad_gap(obj, 4, LV_STATE_DEFAULT); } }