From 8e930ef8d922aab4696a5acdc358489df18ab8a8 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 18 Aug 2026 23:37:44 -0400 Subject: [PATCH] Remove WiFi gate --- src/main.cpp | 342 +++++++++++++++++++----------------- src/provisioning.cpp | 120 ++++++------- src/provisioning.h | 24 +-- tests/test_provisioning.cpp | 46 ++--- 4 files changed, 266 insertions(+), 266 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 0696d3d..d930431 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,206 +4,218 @@ #include "app_state.h" #include "home.h" -#include "provisioning.h" #include "logger/logger.h" +#include "provisioning.h" -namespace -{ - // How long audio fades out before the app tears down on quit. Only applied - // when audio is actually playing; otherwise quitting is immediate. - constexpr float kQuitFadeSeconds = 5.0f; +namespace { +// How long audio fades out before the app tears down on quit. Only applied +// when audio is actually playing; otherwise quitting is immediate. +constexpr float kQuitFadeSeconds = 5.0f; } // anonymous namespace -SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) -{ - // Initialize text logger - radiantwave::logger::Logger::instance().initialize(); - radiantwave::logger::Logger::instance().info("System", "RadiantWave Kiosk starting up"); +SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) { + // Initialize text logger + radiantwave::logger::Logger::instance().initialize(); + radiantwave::logger::Logger::instance().info( + "System", "RadiantWave Kiosk starting up"); - if (!SDL_Init(SDL_INIT_VIDEO)) - { - std::string err = SDL_GetError(); - radiantwave::logger::Logger::instance().fatal("Graphics", - "Screen display initialization failed. Please verify graphics hardware and display connections (SDL_Init error: " + err + ")"); - SDL_Log("Failed to initialize SDL: %s", err.c_str()); - return SDL_APP_FAILURE; - } + if (!SDL_Init(SDL_INIT_VIDEO)) { + std::string err = SDL_GetError(); + radiantwave::logger::Logger::instance().fatal( + "Graphics", + "Screen display initialization failed. Please verify graphics " + "hardware and display connections (SDL_Init error: " + + err + ")"); + SDL_Log("Failed to initialize SDL: %s", err.c_str()); + return SDL_APP_FAILURE; + } - auto *state = new AppState{}; + auto *state = new AppState{}; - // SDL_WINDOW_FULLSCREEN fills the desktop's native resolution, so the - // compositor never has to scale the moving scroller (scaling moving text - // causes flicker). - state->window = SDL_CreateWindow("RadiantWave", 1920, 1080, SDL_WINDOW_FULLSCREEN); - if (!state->window) - { - SDL_Log("Failed to create window: %s", SDL_GetError()); - delete state; - return SDL_APP_FAILURE; - } + // SDL_WINDOW_FULLSCREEN fills the desktop's native resolution, so the + // compositor never has to scale the moving scroller (scaling moving text + // causes flicker). + state->window = + SDL_CreateWindow("RadiantWave", 1920, 1080, SDL_WINDOW_FULLSCREEN); + if (!state->window) { + SDL_Log("Failed to create window: %s", SDL_GetError()); + delete state; + return SDL_APP_FAILURE; + } - state->renderer = SDL_CreateRenderer(state->window, nullptr); - if (!state->renderer) - { - SDL_Log("Failed to create renderer: %s", SDL_GetError()); - delete state; - return SDL_APP_FAILURE; - } + state->renderer = SDL_CreateRenderer(state->window, nullptr); + if (!state->renderer) { + SDL_Log("Failed to create renderer: %s", SDL_GetError()); + delete state; + return SDL_APP_FAILURE; + } - // Present on the display's refresh. Without VSync the kiosk panel tears - // fast-scrolling text into horizontal bands that flicker as the text moves. - if (!SDL_SetRenderVSync(state->renderer, 1)) { - radiantwave::logger::Logger::instance().warning("Graphics", - "VSync could not be enabled; scrolling text may tear (VSync error: " + std::string(SDL_GetError()) + ")"); - } + // Present on the display's refresh. Without VSync the kiosk panel tears + // fast-scrolling text into horizontal bands that flicker as the text moves. + if (!SDL_SetRenderVSync(state->renderer, 1)) { + radiantwave::logger::Logger::instance().warning( + "Graphics", "VSync could not be enabled; scrolling text may tear " + "(VSync error: " + + std::string(SDL_GetError()) + ")"); + } - // Lock the renderer to a 1920x1080 logical coordinate space. All page - // layout code already targets this resolution, so everything renders at - // the correct size on any display — the GPU scales to native pixels. - if (!SDL_SetRenderLogicalPresentation(state->renderer, 1920, 1080, SDL_LOGICAL_PRESENTATION_STRETCH)) { - radiantwave::logger::Logger::instance().warning("Graphics", - "Could not set logical render size; layout may be wrong on HiDPI displays"); - } + // Lock the renderer to a 1920x1080 logical coordinate space. All page + // layout code already targets this resolution, so everything renders at + // the correct size on any display — the GPU scales to native pixels. + // if (!SDL_SetRenderLogicalPresentation(state->renderer, 1920, 1080, + // SDL_LOGICAL_PRESENTATION_STRETCH)) { + // radiantwave::logger::Logger::instance().warning("Graphics", + // "Could not set logical render size; layout may be wrong on HiDPI + // displays"); + // } - SDL_HideCursor(); + SDL_HideCursor(); - // Initialize configuration, database, and network manager - if (!state->config.initialize()) { - radiantwave::logger::Logger::instance().warning("Config", - "Settings file could not be loaded. Default settings will be used (Config initialization failed at ~/.local/radiantwave/config.yaml)"); - } else { - radiantwave::logger::Logger::instance().info("Config", "Configuration initialized successfully"); - } + // Initialize configuration, database, and network manager + if (!state->config.initialize()) { + radiantwave::logger::Logger::instance().warning( + "Config", "Settings file could not be loaded. Default settings " + "will be used (Config initialization failed at " + "~/.local/radiantwave/config.yaml)"); + } else { + radiantwave::logger::Logger::instance().info( + "Config", "Configuration initialized successfully"); + } - if (!state->db.initialize()) { - radiantwave::logger::Logger::instance().error("Database", - "Database storage unavailable. App features may be limited (Database initialization failed at ~/.local/radiantwave/radiantwave.db)"); - } else { - radiantwave::logger::Logger::instance().info("Database", "Database initialized and schema migrated"); - } + if (!state->db.initialize()) { + radiantwave::logger::Logger::instance().error( + "Database", "Database storage unavailable. App features may be " + "limited (Database initialization failed at " + "~/.local/radiantwave/radiantwave.db)"); + } else { + radiantwave::logger::Logger::instance().info( + "Database", "Database initialized and schema migrated"); + } - if (!state->network.initialize()) { - radiantwave::logger::Logger::instance().warning("Network", - "WiFi network management is unavailable. Check if NetworkManager is running (D-Bus connection to org.freedesktop.NetworkManager failed)"); - } else { - radiantwave::logger::Logger::instance().info("Network", "NetworkManager service connected"); - } + if (!state->network.initialize()) { + radiantwave::logger::Logger::instance().warning( + "Network", "WiFi network management is unavailable. Check if " + "NetworkManager is running (D-Bus connection to " + "org.freedesktop.NetworkManager failed)"); + } else { + radiantwave::logger::Logger::instance().info( + "Network", "NetworkManager service connected"); + } - if (!state->audio.initialize(state->config)) { - radiantwave::logger::Logger::instance().warning("Audio", - "Audio playback is unavailable. Check sound hardware connections (SDL3_mixer initialization failed)"); - } else { - radiantwave::logger::Logger::instance().info("Audio", "Audio subsystem initialized"); - } - // Let playback logs report user-facing display names from the database. - state->audio.attachDatabase(state->db); + if (!state->audio.initialize(state->config)) { + radiantwave::logger::Logger::instance().warning( + "Audio", "Audio playback is unavailable. Check sound hardware " + "connections (SDL3_mixer initialization failed)"); + } else { + radiantwave::logger::Logger::instance().info( + "Audio", "Audio subsystem initialized"); + } + // Let playback logs report user-facing display names from the database. + state->audio.attachDatabase(state->db); - // Background download of audio assets. The worker keeps retrying until - // internet is available, so downloads start automatically on connect. - // The downloaded files are reconciled into the database's audio_files table. - state->audioDownloader.start(state->db); + // Background download of audio assets. The worker keeps retrying until + // internet is available, so downloads start automatically on connect. + // The downloaded files are reconciled into the database's audio_files + // table. + state->audioDownloader.start(state->db); - // Background license validation. Home triggers a refresh whenever it is - // displayed; the result gates access to Play and the Proclamation options. - state->licenseTracker.start(state->db); + // Background license validation. Home triggers a refresh whenever it is + // displayed; the result gates access to Play and the Proclamation options. + state->licenseTracker.start(state->db); - state->config.onChanged([state](const radiantwave::config::AppConfig& new_config) { - SDL_Log("Live config update received: volume=%d, dynamic_audio=%d", - new_config.audio.volume, new_config.audio.dynamic_audio ? 1 : 0); - state->db.logEvent("INFO", "Config", "Live configuration hot-reloaded"); - }); + state->config.onChanged( + [state](const radiantwave::config::AppConfig &new_config) { + SDL_Log("Live config update received: volume=%d, dynamic_audio=%d", + new_config.audio.volume, + new_config.audio.dynamic_audio ? 1 : 0); + state->db.logEvent("INFO", "Config", + "Live configuration hot-reloaded"); + }); - state->keybinds.bind(SDLK_UP, "menu_up", [state]() { - state->currentMenuItem = (state->currentMenuItem + 1) % 2; - }); - state->keybinds.bind(SDLK_DOWN, "menu_down", [state]() { - state->currentMenuItem = (state->currentMenuItem + 1) % 2; - }); + state->keybinds.bind(SDLK_UP, "menu_up", [state]() { + state->currentMenuItem = (state->currentMenuItem + 1) % 2; + }); + state->keybinds.bind(SDLK_DOWN, "menu_down", [state]() { + state->currentMenuItem = (state->currentMenuItem + 1) % 2; + }); - // Drive the First-Run walkthrough: shows the WiFi page when the internet - // is unreachable, then the email and license pages when those are missing, - // finishing on Home. The internet check is a real connectivity probe - // (Ethernet, WiFi, or macOS all work), so it never traps a connected - // device on the WiFi page. - radiantwave::provisioning::begin(*state); + // Drive the First-Run walkthrough: shows the WiFi page when the internet + // is unreachable , then the email and license pages when those + // are missing, finishing on Home. The internet check is a real connectivity + // probe (Ethernet, WiFi, or macOS all work), so it never traps a connected + // device on the WiFi page. + radiantwave::provisioning::begin(*state); - *appstate = state; - return SDL_APP_CONTINUE; + *appstate = state; + return SDL_APP_CONTINUE; } -SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) -{ - auto &state = *static_cast(appstate); +SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) { + auto &state = *static_cast(appstate); - if (event->type == SDL_EVENT_QUIT) - { - state.quitRequested = true; - } + if (event->type == SDL_EVENT_QUIT) { + state.quitRequested = true; + } - if (!state.keybinds.handleEvent(*event)) { - state.pages.handleEvent(*event); - } + if (!state.keybinds.handleEvent(*event)) { + state.pages.handleEvent(*event); + } - if (state.quitRequested && state.quitFadeRemaining <= 0.0f) - { - // Graceful shutdown: start the audio fade-out and keep the loop - // running until it completes, then let SDL_AppQuit tear everything - // down. When nothing is playing there is nothing to fade and quitting - // is immediate. - if (state.audio.stopTrackFadeOut(kQuitFadeSeconds)) - { - state.quitFadeRemaining = kQuitFadeSeconds; - return SDL_APP_CONTINUE; - } - return SDL_APP_SUCCESS; - } + if (state.quitRequested && state.quitFadeRemaining <= 0.0f) { + // Graceful shutdown: start the audio fade-out and keep the loop + // running until it completes, then let SDL_AppQuit tear everything + // down. When nothing is playing there is nothing to fade and quitting + // is immediate. + if (state.audio.stopTrackFadeOut(kQuitFadeSeconds)) { + state.quitFadeRemaining = kQuitFadeSeconds; + return SDL_APP_CONTINUE; + } + return SDL_APP_SUCCESS; + } - return SDL_APP_CONTINUE; + return SDL_APP_CONTINUE; } -SDL_AppResult SDL_AppIterate(void *appstate) -{ - auto &state = *static_cast(appstate); +SDL_AppResult SDL_AppIterate(void *appstate) { + auto &state = *static_cast(appstate); - Uint64 nowTicks = SDL_GetTicks(); - static Uint64 lastTicks = nowTicks; - float dt = static_cast(nowTicks - lastTicks) / 1000.0f; - if (dt > 0.1f) dt = 0.1f; - lastTicks = nowTicks; + Uint64 nowTicks = SDL_GetTicks(); + static Uint64 lastTicks = nowTicks; + float dt = static_cast(nowTicks - lastTicks) / 1000.0f; + if (dt > 0.1f) + dt = 0.1f; + lastTicks = nowTicks; - state.pages.update(dt); + state.pages.update(dt); - if (state.quitFadeRemaining > 0.0f) - { - state.quitFadeRemaining -= dt; - if (state.quitFadeRemaining <= 0.0f) - { - return SDL_APP_SUCCESS; - } - } + if (state.quitFadeRemaining > 0.0f) { + state.quitFadeRemaining -= dt; + if (state.quitFadeRemaining <= 0.0f) { + return SDL_APP_SUCCESS; + } + } - SDL_SetRenderDrawColor(state.renderer, 30, 30, 46, 255); - SDL_RenderClear(state.renderer); + SDL_SetRenderDrawColor(state.renderer, 30, 30, 46, 255); + SDL_RenderClear(state.renderer); - state.pages.render(state.renderer); + state.pages.render(state.renderer); - SDL_RenderPresent(state.renderer); - return SDL_APP_CONTINUE; + SDL_RenderPresent(state.renderer); + return SDL_APP_CONTINUE; } -void SDL_AppQuit(void *appstate, SDL_AppResult result) -{ - auto *state = static_cast(appstate); - if (state) { - state->audioDownloader.shutdown(); - state->licenseTracker.shutdown(); - state->audio.shutdown(); - state->network.shutdown(); - state->config.shutdown(); - state->db.close(); - SDL_DestroyRenderer(state->renderer); - SDL_DestroyWindow(state->window); - delete state; - } - SDL_Quit(); +void SDL_AppQuit(void *appstate, SDL_AppResult result) { + auto *state = static_cast(appstate); + if (state) { + state->audioDownloader.shutdown(); + state->licenseTracker.shutdown(); + state->audio.shutdown(); + state->network.shutdown(); + state->config.shutdown(); + state->db.close(); + SDL_DestroyRenderer(state->renderer); + SDL_DestroyWindow(state->window); + delete state; + } + SDL_Quit(); } diff --git a/src/provisioning.cpp b/src/provisioning.cpp index dce86b9..f510621 100644 --- a/src/provisioning.cpp +++ b/src/provisioning.cpp @@ -1,80 +1,82 @@ #include "provisioning.h" -#include "wifi_setup_page.h" #include "email_address_page.h" -#include "license_key_page.h" #include "home.h" +#include "license_key_page.h" #include "logger/logger.h" +#include "wifi_setup_page.h" namespace radiantwave::provisioning { // Push the page for a walkthrough step, or Home when the walkthrough is done. -static void pushStep(AppState& state, Step step) -{ - switch (step) { - case Step::Internet: - state.pages.push(state, std::make_unique()); - break; - case Step::Email: - state.pages.push(state, std::make_unique()); - break; - case Step::License: - state.pages.push(state, std::make_unique()); - break; - case Step::Done: - state.provisioningMode = false; - state.pages.push(state, std::make_unique()); - break; - } +static void pushStep(AppState &state, Step step) { + switch (step) { + // case Step::Internet: + // state.pages.push(state, std::make_unique()); + // break; + case Step::Email: + state.pages.push(state, std::make_unique()); + break; + case Step::License: + state.pages.push(state, std::make_unique()); + break; + case Step::Done: + state.provisioningMode = false; + state.pages.push(state, std::make_unique()); + break; + } } -void begin(AppState& state) -{ - // Without the database there is nowhere to persist email/license, so the - // walkthrough would trap the user on pages that can never be completed. - if (!state.db.isOpen()) { - state.provisioningMode = false; - state.pages.push(state, std::make_unique()); - return; - } +void begin(AppState &state) { + // Without the database there is nowhere to persist email/license, so the + // walkthrough would trap the user on pages that can never be completed. + if (!state.db.isOpen()) { + state.provisioningMode = false; + state.pages.push(state, std::make_unique()); + return; + } - const bool internetUp = state.network.hasInternet(); - const bool hasEmail = !state.db.getKV(radiantwave::db::KV_EMAIL_ADDRESS).empty(); - const bool hasLicense = !state.db.getKV(radiantwave::db::KV_LICENSE_KEY).empty(); + const bool internetUp = state.network.hasInternet(); + const bool hasEmail = + !state.db.getKV(radiantwave::db::KV_EMAIL_ADDRESS).empty(); + const bool hasLicense = + !state.db.getKV(radiantwave::db::KV_LICENSE_KEY).empty(); - if (indicatesFirstRun(internetUp, hasEmail, hasLicense)) { - // Placeholder: a brand-new device. Later this flag will launch the - // first-run welcome walkthrough (TOS, quick user guide, ...). - state.firstRunWalkthrough = true; - radiantwave::logger::Logger::instance().info("Provisioning", - "First-Run detected (no internet, no email, no license); " - "first-run welcome walkthrough is not yet implemented"); - } + if (indicatesFirstRun(internetUp, hasEmail, hasLicense)) { + // Placeholder: a brand-new device. Later this flag will launch the + // first-run welcome walkthrough (TOS, quick user guide, ...). + state.firstRunWalkthrough = true; + radiantwave::logger::Logger::instance().info( + "Provisioning", + "First-Run detected (no internet, no email, no license); " + "first-run welcome walkthrough is not yet implemented"); + } - state.walkthroughInternetUp = internetUp; - state.provisioningMode = true; - pushStep(state, firstMissingStep(internetUp, hasEmail, hasLicense)); + state.walkthroughInternetUp = internetUp; + state.provisioningMode = true; + pushStep(state, firstMissingStep(internetUp, hasEmail, hasLicense)); } -void advance(AppState& state) -{ - if (state.pages.depth() > 0) { - state.pages.pop(state); - } +void advance(AppState &state) { + if (state.pages.depth() > 0) { + state.pages.pop(state); + } - // Internet is the first prerequisite and never gets "un-reached" once - // satisfied, so it is only re-probed when it was still missing (i.e. the - // walkthrough is coming from the WiFi page). - bool internetUp = state.walkthroughInternetUp; - if (!internetUp) { - internetUp = state.network.hasInternet(); - state.walkthroughInternetUp = internetUp; - } + // Internet is the first prerequisite and never gets "un-reached" once + // satisfied, so it is only re-probed when it was still missing (i.e. the + // walkthrough is coming from the WiFi page). + bool internetUp = state.walkthroughInternetUp; + if (!internetUp) { + internetUp = state.network.hasInternet(); + state.walkthroughInternetUp = internetUp; + } - const bool hasEmail = !state.db.getKV(radiantwave::db::KV_EMAIL_ADDRESS).empty(); - const bool hasLicense = !state.db.getKV(radiantwave::db::KV_LICENSE_KEY).empty(); + const bool hasEmail = + !state.db.getKV(radiantwave::db::KV_EMAIL_ADDRESS).empty(); + const bool hasLicense = + !state.db.getKV(radiantwave::db::KV_LICENSE_KEY).empty(); - state.provisioningMode = true; - pushStep(state, firstMissingStep(internetUp, hasEmail, hasLicense)); + state.provisioningMode = true; + pushStep(state, firstMissingStep(internetUp, hasEmail, hasLicense)); } } // namespace radiantwave::provisioning diff --git a/src/provisioning.h b/src/provisioning.h index 1ba083e..1699df5 100644 --- a/src/provisioning.h +++ b/src/provisioning.h @@ -24,33 +24,33 @@ namespace radiantwave::provisioning { // Prerequisite steps in walkthrough priority order. -enum class Step { Done, Internet, Email, License }; +enum class Step { Done, Email, License }; // First unsatisfied step, given the current conditions. -inline Step firstMissingStep(bool internetUp, bool hasEmail, bool hasLicense) -{ - if (!internetUp) return Step::Internet; - if (!hasEmail) return Step::Email; - if (!hasLicense) return Step::License; - return Step::Done; +inline Step firstMissingStep(bool internetUp, bool hasEmail, bool hasLicense) { + // if (!internetUp) return Step::Internet; + if (!hasEmail) + return Step::Email; + if (!hasLicense) + return Step::License; + return Step::Done; } // True when nothing is configured (no internet, no email, no license). // Signals a brand-new, never-provisioned device. -inline bool indicatesFirstRun(bool internetUp, bool hasEmail, bool hasLicense) -{ - return !internetUp && !hasEmail && !hasLicense; +inline bool indicatesFirstRun(bool internetUp, bool hasEmail, bool hasLicense) { + return !internetUp && !hasEmail && !hasLicense; } // Startup walkthrough. Evaluates the prerequisites and pushes the first // missing step's page (with provisioningMode set), or Home when everything // is satisfied. Home is always the end of the walkthrough. -void begin(AppState& state); +void begin(AppState &state); // Called by a walkthrough page when the user finishes or exits it. Pops the // current page, re-evaluates the prerequisites, and pushes the next missing // step's page (or Home). Internet is only re-probed when it was still // missing, so completing a step never blocks on a redundant check. -void advance(AppState& state); +void advance(AppState &state); } // namespace radiantwave::provisioning diff --git a/tests/test_provisioning.cpp b/tests/test_provisioning.cpp index cc5c3b8..471365d 100644 --- a/tests/test_provisioning.cpp +++ b/tests/test_provisioning.cpp @@ -1,51 +1,37 @@ -#include "test_harness.h" #include "../src/provisioning.h" +#include "test_harness.h" using radiantwave::provisioning::firstMissingStep; using radiantwave::provisioning::indicatesFirstRun; using radiantwave::provisioning::Step; -TEST(first_missing_step_all_satisfied) -{ - ASSERT_EQ(firstMissingStep(true, true, true), Step::Done); +TEST(first_missing_step_all_satisfied) { + ASSERT_EQ(firstMissingStep(true, true, true), Step::Done); } -TEST(first_missing_step_no_internet) -{ - // Internet is checked before email/license regardless of the others. - ASSERT_EQ(firstMissingStep(false, false, false), Step::Internet); - ASSERT_EQ(firstMissingStep(false, true, true), Step::Internet); +TEST(first_missing_step_no_email) { + ASSERT_EQ(firstMissingStep(true, false, false), Step::Email); + ASSERT_EQ(firstMissingStep(true, false, true), Step::Email); } -TEST(first_missing_step_no_email) -{ - ASSERT_EQ(firstMissingStep(true, false, false), Step::Email); - ASSERT_EQ(firstMissingStep(true, false, true), Step::Email); +TEST(first_missing_step_no_license) { + ASSERT_EQ(firstMissingStep(true, true, false), Step::License); } -TEST(first_missing_step_no_license) -{ - ASSERT_EQ(firstMissingStep(true, true, false), Step::License); +TEST(first_run_all_missing) { + ASSERT_TRUE(indicatesFirstRun(false, false, false)); } -TEST(first_run_all_missing) -{ - ASSERT_TRUE(indicatesFirstRun(false, false, false)); +TEST(first_run_has_internet) { + ASSERT_FALSE(indicatesFirstRun(true, false, false)); } -TEST(first_run_has_internet) -{ - ASSERT_FALSE(indicatesFirstRun(true, false, false)); +TEST(first_run_has_email) { + ASSERT_FALSE(indicatesFirstRun(false, true, false)); } -TEST(first_run_has_email) -{ - ASSERT_FALSE(indicatesFirstRun(false, true, false)); -} - -TEST(first_run_has_license) -{ - ASSERT_FALSE(indicatesFirstRun(false, false, true)); +TEST(first_run_has_license) { + ASSERT_FALSE(indicatesFirstRun(false, false, true)); } RUN_ALL_TESTS()