#include #include #include #include #include #include #include #include #include #include namespace tt::app::wifimanage { constexpr auto* TAG = "WifiManage"; extern const ::AppManifest manifest; namespace { struct Context { uint32_t appInstanceId; PubSub::SubscriptionHandle wifiSubscription = nullptr; Mutex mutex; Bindings bindings {}; State state; View view = View(&bindings, &state); void lock() { mutex.lock(); } void unlock() { mutex.unlock(); } }; static void onConnect(const std::string& ssid) { service::wifi::settings::WifiApSettings settings; if (service::wifi::settings::load(ssid, settings)) { LOG_I(TAG, "Connecting with known credentials"); service::wifi::connect(settings, false); } else { LOG_I(TAG, "Starting connection dialog"); wificonnect::start(ssid); } } static void onShowApSettings(const std::string& ssid) { wifiapsettings::start(ssid); } static void onDisconnect() { service::wifi::disconnect(); } static void onWifiToggled(bool enabled) { service::wifi::setEnabled(enabled); } static void onConnectToHidden() { wificonnect::start(); } void requestViewUpdate(Context* ctx) { ctx->lock(); lvgl_lock(); // Safe even while buried (e.g. WifiApSettings/WifiConnect opened on top): destroyWidgets() // nulls the view's widget pointers before they're deleted, and update() no-ops on that. ctx->view.update(); lvgl_unlock(); ctx->unlock(); } void onWifiEvent(Context* ctx, service::wifi::WifiEvent event) { auto radio_state = service::wifi::getRadioState(); LOG_I(TAG, "Update with state %s", service::wifi::radioStateToString(radio_state)); ctx->state.setRadioState(radio_state); switch (event.type) { case WIFI_EVENT_TYPE_SCAN_STARTED: ctx->state.setScanning(true); break; case WIFI_EVENT_TYPE_SCAN_FINISHED: ctx->state.setScanning(false); ctx->state.updateApRecords(); break; case WIFI_EVENT_TYPE_RADIO_STATE_CHANGED: if (event.radio_state == WIFI_RADIO_STATE_ON && !service::wifi::isScanning()) { service::wifi::scan(); } break; default: break; } requestViewUpdate(ctx); } void createWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); ctx->lock(); ctx->state.setConnectSsid("Connected"); // TODO update with proper SSID ctx->view.init(ctx->appInstanceId, parent); ctx->view.update(); ctx->unlock(); } // Runs with the LVGL lock already held, possibly on another app's thread - see // WindowDestroyWidgetsFn's warnings. Must stay lock-free: View::reset() only nulls pointers. void destroyWidgets(void* userData) { auto* ctx = static_cast(userData); ctx->view.reset(); } int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { Context ctx; ctx.appInstanceId = appInstanceId; ctx.bindings = (Bindings) { .onWifiToggled = onWifiToggled, .onConnectSsid = onConnect, .onDisconnect = onDisconnect, .onShowApSettings = onShowApSettings, .onConnectToHidden = onConnectToHidden }; ctx.wifiSubscription = service::wifi::getPubsub()->subscribe([&ctx](auto event) { onWifiEvent(&ctx, event); }); // State update (it has its own locking) ctx.state.setRadioState(service::wifi::getRadioState()); ctx.state.setScanning(service::wifi::isScanning()); ctx.state.updateApRecords(); AppEventSubscription sub {}; sub.app_instance_id = appInstanceId; app_event_subscribe(&sub); WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx); service::wifi::RadioState radio_state = service::wifi::getRadioState(); bool can_scan = radio_state == service::wifi::RadioState::On || radio_state == service::wifi::RadioState::ConnectionPending || radio_state == service::wifi::RadioState::ConnectionActive; std::string connection_target = service::wifi::getConnectionTarget(); LOG_I(TAG, "Radio: %s, Scanning: %d, Connected to: %s, Can scan: %d", service::wifi::radioStateToString(radio_state), (int)service::wifi::isScanning(), connection_target.empty() ? "(none)" : connection_target.c_str(), (int)can_scan); if (can_scan && !service::wifi::isScanning()) { service::wifi::scan(); } bool shouldClose = false; while (!shouldClose) { AppEvent event {}; if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { break; } switch (event.type) { case APP_EVENT_CLOSE: app_manager_finish(appInstanceId); shouldClose = true; break; default: break; } } ctx.lock(); service::wifi::getPubsub()->unsubscribe(ctx.wifiSubscription); ctx.wifiSubscription = nullptr; ctx.unlock(); window_manager_remove(window); app_event_unsubscribe(&sub); return 0; } } // namespace uint32_t start(uint32_t callerAppInstanceId) { uint32_t instanceId = 0; app_manager_start_for_result(manifest.id, callerAppInstanceId, 0, nullptr, &instanceId); return instanceId; } extern const ::AppManifest manifest = { .id = "tactility.wifimanage", .name = "Wi-Fi", .category = APP_CATEGORY_SETTINGS, .location = { APP_LOCATION_MEMORY, reinterpret_cast(appMain) } }; } // namespace tt::app::wifimanage