4 Commits
Author SHA1 Message Date
NeilClack b96631c374 Add first Attic push 2026-08-21 14:52:14 -05:00
NeilClack 39eb6d7bf7 Update to new_checkshums.md5
new_checksums.md5 enables audio file naming and other audio related features.
2026-08-21 10:03:31 -05:00
NeilClack 3c9de0bee2 Fix double-scaling by locking page layout to logical presentation
Pages computed layout from SDL_GetWindowSize() (real window
coordinates) while SDL_SetRenderLogicalPresentation() independently
remapped every draw call into a 1920x1080 space. The two coordinate
systems only agreed when the window happened to be exactly 1920x1080;
at any other size, layout coordinates were transformed a second time,
so content shrank/grew quadratically toward the top-left on resize or
on non-1080p monitors. Commenting the call out masked the bug by
making rendering 1:1 with the window, but then nothing scaled: fonts
and margins stayed fixed-pixel while backgrounds cover-fit, and HiDPI
displays drew at point resolution and relied on blurry compositor
upsampling.

Restore SDL_SetRenderLogicalPresentation(1920x1080, LETTERBOX) as the
single source of scaling, and route all twelve layout sites through a
new radiantwave::getLayoutSize() helper (src/logical_size.h) that
returns the renderer's logical size, so pages always lay out against
the design resolution regardless of window or monitor geometry. Add
SDL_WINDOW_HIGH_PIXEL_DENSITY so HiDPI displays render at native pixel
density instead of being upscaled by the OS. Cached per-page textures
become resolution-independent since layout no longer depends on window
size.
2026-08-21 09:29:23 -05:00
NeilClack 8e930ef8d9 Remove WiFi gate 2026-08-18 23:37:44 -04:00
19 changed files with 352 additions and 311 deletions
+8
View File
@@ -65,3 +65,11 @@ jobs:
./build/test_api_client ./build/test_api_client
./build/test_license_service ./build/test_license_service
' '
- name: Push to Attic cache
run: |
nix profile install github:zhaofengli/attic#attic-client \
--extra-experimental-features "nix-command flakes"
attic login radiantwave https://attic.radiantwavetech.com "${{ secrets.ATTIC_TOKEN }}"
attic push radiantwave ./result-kiosk ./result-android ./result-commercial
+18 -14
View File
@@ -37,7 +37,7 @@ struct AudioManifestEntry {
// and keeps the database's audio_files table in sync with what is actually // and keeps the database's audio_files table in sync with what is actually
// on disk. // on disk.
// //
// Fetches checksums.md5, compares each entry against the local file's MD5, // Fetches new_checksums.md5, compares each entry against the local file's MD5,
// and re-downloads anything that is missing or whose checksum differs. // and re-downloads anything that is missing or whose checksum differs.
// Files present on disk but absent from the manifest are deleted, and the // Files present on disk but absent from the manifest are deleted, and the
// audio_files table is reconciled on every pass (and after each individual // audio_files table is reconciled on every pass (and after each individual
@@ -53,12 +53,12 @@ public:
AudioDownloader() = default; AudioDownloader() = default;
~AudioDownloader(); ~AudioDownloader();
AudioDownloader(const AudioDownloader&) = delete; AudioDownloader(const AudioDownloader &) = delete;
AudioDownloader& operator=(const AudioDownloader&) = delete; AudioDownloader &operator=(const AudioDownloader &) = delete;
// Spawns the background thread. Safe to call repeatedly; only one worker // Spawns the background thread. Safe to call repeatedly; only one worker
// runs at a time. The manifest is reconciled against `db` on every pass. // runs at a time. The manifest is reconciled against `db` on every pass.
void start(db::DatabaseManager& db); void start(db::DatabaseManager &db);
// Signals the worker to stop and joins the thread. // Signals the worker to stop and joins the thread.
void shutdown(); void shutdown();
@@ -66,15 +66,18 @@ public:
// Returns the current human-readable status for display (e.g. footer). // Returns the current human-readable status for display (e.g. footer).
std::string statusString() const; std::string statusString() const;
static constexpr const char* AUDIO_BASE_URL = "https://files.radiantwavetech.com/audio/"; static constexpr const char *AUDIO_BASE_URL =
static constexpr const char* MANIFEST_FILENAME = "checksums.md5"; "https://files.radiantwavetech.com/audio/";
static constexpr const char *MANIFEST_FILENAME = "new_checksums.md5";
private: private:
void run(); void run();
bool fetchManifest(std::vector<AudioManifestEntry>& out); bool fetchManifest(std::vector<AudioManifestEntry> &out);
bool downloadAndVerify(const std::string& filename, const std::string& expectedMd5, const std::string& displayName); bool downloadAndVerify(const std::string &filename,
const std::string &expectedMd5,
const std::string &displayName);
void setStatus(const std::string& msg); void setStatus(const std::string &msg);
void wait(std::chrono::seconds seconds); void wait(std::chrono::seconds seconds);
mutable std::mutex mutex_; mutable std::mutex mutex_;
@@ -82,13 +85,14 @@ private:
std::thread worker_; std::thread worker_;
std::atomic<bool> stop_{false}; std::atomic<bool> stop_{false};
std::string status_; std::string status_;
db::DatabaseManager* db_ = nullptr; db::DatabaseManager *db_ = nullptr;
}; };
// Parses checksums.md5 manifest text into entries. Blank lines and lines // Parses new_checksums.md5 manifest text into entries. Blank lines and lines
// starting with '#' are ignored. Lines that don't match a known format are // starting with '#' are ignored. Lines that don't match a known format are
// skipped. Returns false if no valid entries were found. // skipped. Returns false if no valid entries were found.
bool parseManifest(const std::string& text, std::vector<AudioManifestEntry>& out); bool parseManifest(const std::string &text,
std::vector<AudioManifestEntry> &out);
// Maps a frequency value in Hz to its internal solfeggio frequency id using // Maps a frequency value in Hz to its internal solfeggio frequency id using
// the same table the color palettes are seeded from (174/285->1, 396->2, // the same table the color palettes are seeded from (174/285->1, 396->2,
@@ -100,9 +104,9 @@ int frequencyIdForHz(int hz);
// "396Hz.wav" -> 396. Requires the digits to be followed by "Hz" // "396Hz.wav" -> 396. Requires the digits to be followed by "Hz"
// (case-insensitive); returns 0 when the name has no frequency (e.g. // (case-insensitive); returns 0 when the name has no frequency (e.g.
// "AlphaWaves.wav", "OriginalAudio.wav"). // "AlphaWaves.wav", "OriginalAudio.wav").
int hzFromFilename(const std::string& filename); int hzFromFilename(const std::string &filename);
// Computes the lowercase MD5 hex of a file, or an empty string on failure. // Computes the lowercase MD5 hex of a file, or an empty string on failure.
std::string computeFileMd5(const std::string& path); std::string computeFileMd5(const std::string &path);
} // namespace radiantwave::audio } // namespace radiantwave::audio
+2 -1
View File
@@ -1,5 +1,6 @@
#include "audio_settings_page.h" #include "audio_settings_page.h"
#include "app_state.h" #include "app_state.h"
#include "logical_size.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
@@ -477,7 +478,7 @@ void AudioSettingsPage::renderModal(SDL_Renderer* renderer, float fw, float fh,
void AudioSettingsPage::render(SDL_Renderer* renderer) { void AudioSettingsPage::render(SDL_Renderer* renderer) {
int winW, winH; int winW, winH;
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH); radiantwave::getLayoutSize(renderer, &winW, &winH);
float fw = static_cast<float>(winW); float fw = static_cast<float>(winW);
float fh = static_cast<float>(winH); float fh = static_cast<float>(winH);
+2 -1
View File
@@ -1,6 +1,7 @@
#include "date_format_page.h" #include "date_format_page.h"
#include "app_state.h" #include "app_state.h"
#include "date_format.h" #include "date_format.h"
#include "logical_size.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <algorithm> #include <algorithm>
#include <ctime> #include <ctime>
@@ -131,7 +132,7 @@ void DateFormatPage::update(float dt)
void DateFormatPage::render(SDL_Renderer* renderer) void DateFormatPage::render(SDL_Renderer* renderer)
{ {
int winW, winH; int winW, winH;
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH); radiantwave::getLayoutSize(renderer, &winW, &winH);
float fw = static_cast<float>(winW); float fw = static_cast<float>(winW);
float fh = static_cast<float>(winH); float fh = static_cast<float>(winH);
+2 -1
View File
@@ -3,6 +3,7 @@
#include "db/db_keys.h" #include "db/db_keys.h"
#include "provisioning.h" #include "provisioning.h"
#include "validation.h" #include "validation.h"
#include "logical_size.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
namespace { namespace {
@@ -164,7 +165,7 @@ void EmailAddressPage::exitPage(AppState& state)
void EmailAddressPage::render(SDL_Renderer* renderer) void EmailAddressPage::render(SDL_Renderer* renderer)
{ {
int winW, winH; int winW, winH;
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH); radiantwave::getLayoutSize(renderer, &winW, &winH);
float fw = static_cast<float>(winW); float fw = static_cast<float>(winW);
float fh = static_cast<float>(winH); float fh = static_cast<float>(winH);
+2 -1
View File
@@ -8,6 +8,7 @@
#include "audio/audio_downloader.h" #include "audio/audio_downloader.h"
#include "db/db_keys.h" #include "db/db_keys.h"
#include "network/proclamation_sync.h" #include "network/proclamation_sync.h"
#include "logical_size.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h> #include <SDL3_image/SDL_image.h>
#include <ctime> #include <ctime>
@@ -114,7 +115,7 @@ void Home::update(float dt)
void Home::render(SDL_Renderer *renderer) void Home::render(SDL_Renderer *renderer)
{ {
int winW, winH; int winW, winH;
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH); radiantwave::getLayoutSize(renderer, &winW, &winH);
float fw = static_cast<float>(winW); float fw = static_cast<float>(winW);
float fh = static_cast<float>(winH); float fh = static_cast<float>(winH);
+2 -1
View File
@@ -1,6 +1,7 @@
#include "license_blocked_page.h" #include "license_blocked_page.h"
#include "app_state.h" #include "app_state.h"
#include "home.h" #include "home.h"
#include "logical_size.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
void LicenseBlockedPage::onPush(AppState& state) void LicenseBlockedPage::onPush(AppState& state)
@@ -71,7 +72,7 @@ void LicenseBlockedPage::update(float dt)
void LicenseBlockedPage::render(SDL_Renderer* renderer) void LicenseBlockedPage::render(SDL_Renderer* renderer)
{ {
int winW, winH; int winW, winH;
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH); radiantwave::getLayoutSize(renderer, &winW, &winH);
float fw = static_cast<float>(winW); float fw = static_cast<float>(winW);
float fh = static_cast<float>(winH); float fh = static_cast<float>(winH);
+2 -1
View File
@@ -4,6 +4,7 @@
#include "network/license_service.h" #include "network/license_service.h"
#include "provisioning.h" #include "provisioning.h"
#include "validation.h" #include "validation.h"
#include "logical_size.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
void LicenseKeyPage::onPush(AppState& state) void LicenseKeyPage::onPush(AppState& state)
@@ -172,7 +173,7 @@ void LicenseKeyPage::exitPage(AppState& state)
void LicenseKeyPage::render(SDL_Renderer* renderer) void LicenseKeyPage::render(SDL_Renderer* renderer)
{ {
int winW, winH; int winW, winH;
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH); radiantwave::getLayoutSize(renderer, &winW, &winH);
float fw = static_cast<float>(winW); float fw = static_cast<float>(winW);
float fh = static_cast<float>(winH); float fh = static_cast<float>(winH);
+2 -1
View File
@@ -1,6 +1,7 @@
#include "log_viewer_page.h" #include "log_viewer_page.h"
#include "app_state.h" #include "app_state.h"
#include "timezone_helper.h" #include "timezone_helper.h"
#include "logical_size.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <algorithm> #include <algorithm>
#include <sstream> #include <sstream>
@@ -162,7 +163,7 @@ void LogViewerPage::update(float dt) {
void LogViewerPage::render(SDL_Renderer* renderer) { void LogViewerPage::render(SDL_Renderer* renderer) {
int winW, winH; int winW, winH;
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH); radiantwave::getLayoutSize(renderer, &winW, &winH);
float fw = static_cast<float>(winW); float fw = static_cast<float>(winW);
float fh = static_cast<float>(winH); float fh = static_cast<float>(winH);
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <SDL3/SDL.h>
namespace radiantwave {
inline void getLayoutSize(SDL_Renderer* renderer, int* w, int* h)
{
SDL_RendererLogicalPresentation mode = SDL_LOGICAL_PRESENTATION_DISABLED;
if (!SDL_GetRenderLogicalPresentation(renderer, w, h, &mode) ||
mode == SDL_LOGICAL_PRESENTATION_DISABLED || *w <= 0 || *h <= 0)
{
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), w, h);
}
}
} // namespace radiantwave
+71 -59
View File
@@ -4,27 +4,28 @@
#include "app_state.h" #include "app_state.h"
#include "home.h" #include "home.h"
#include "provisioning.h"
#include "logger/logger.h" #include "logger/logger.h"
#include "provisioning.h"
namespace namespace {
{ // How long audio fades out before the app tears down on quit. Only applied
// How long audio fades out before the app tears down on quit. Only applied // when audio is actually playing; otherwise quitting is immediate.
// when audio is actually playing; otherwise quitting is immediate. constexpr float kQuitFadeSeconds = 5.0f;
constexpr float kQuitFadeSeconds = 5.0f;
} // anonymous namespace } // anonymous namespace
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) {
{
// Initialize text logger // Initialize text logger
radiantwave::logger::Logger::instance().initialize(); radiantwave::logger::Logger::instance().initialize();
radiantwave::logger::Logger::instance().info("System", "RadiantWave Kiosk starting up"); radiantwave::logger::Logger::instance().info(
"System", "RadiantWave Kiosk starting up");
if (!SDL_Init(SDL_INIT_VIDEO)) if (!SDL_Init(SDL_INIT_VIDEO)) {
{
std::string err = SDL_GetError(); std::string err = SDL_GetError();
radiantwave::logger::Logger::instance().fatal("Graphics", radiantwave::logger::Logger::instance().fatal(
"Screen display initialization failed. Please verify graphics hardware and display connections (SDL_Init error: " + err + ")"); "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()); SDL_Log("Failed to initialize SDL: %s", err.c_str());
return SDL_APP_FAILURE; return SDL_APP_FAILURE;
} }
@@ -34,17 +35,16 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
// SDL_WINDOW_FULLSCREEN fills the desktop's native resolution, so the // SDL_WINDOW_FULLSCREEN fills the desktop's native resolution, so the
// compositor never has to scale the moving scroller (scaling moving text // compositor never has to scale the moving scroller (scaling moving text
// causes flicker). // causes flicker).
state->window = SDL_CreateWindow("RadiantWave", 1920, 1080, SDL_WINDOW_FULLSCREEN); state->window =
if (!state->window) SDL_CreateWindow("RadiantWave", 1920, 1080, SDL_WINDOW_FULLSCREEN);
{ if (!state->window) {
SDL_Log("Failed to create window: %s", SDL_GetError()); SDL_Log("Failed to create window: %s", SDL_GetError());
delete state; delete state;
return SDL_APP_FAILURE; return SDL_APP_FAILURE;
} }
state->renderer = SDL_CreateRenderer(state->window, nullptr); state->renderer = SDL_CreateRenderer(state->window, nullptr);
if (!state->renderer) if (!state->renderer) {
{
SDL_Log("Failed to create renderer: %s", SDL_GetError()); SDL_Log("Failed to create renderer: %s", SDL_GetError());
delete state; delete state;
return SDL_APP_FAILURE; return SDL_APP_FAILURE;
@@ -53,64 +53,83 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
// Present on the display's refresh. Without VSync the kiosk panel tears // Present on the display's refresh. Without VSync the kiosk panel tears
// fast-scrolling text into horizontal bands that flicker as the text moves. // fast-scrolling text into horizontal bands that flicker as the text moves.
if (!SDL_SetRenderVSync(state->renderer, 1)) { if (!SDL_SetRenderVSync(state->renderer, 1)) {
radiantwave::logger::Logger::instance().warning("Graphics", radiantwave::logger::Logger::instance().warning(
"VSync could not be enabled; scrolling text may tear (VSync error: " + std::string(SDL_GetError()) + ")"); "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 // Lock the renderer to a 1920x1080 logical coordinate space. All page
// layout code already targets this resolution, so everything renders at // layout code already targets this resolution, so everything renders at
// the correct size on any display — the GPU scales to native pixels. // the correct size on any display — the GPU scales to native pixels.
if (!SDL_SetRenderLogicalPresentation(state->renderer, 1920, 1080, SDL_LOGICAL_PRESENTATION_STRETCH)) { if (!SDL_SetRenderLogicalPresentation(state->renderer, 1920, 1080,
radiantwave::logger::Logger::instance().warning("Graphics", SDL_LOGICAL_PRESENTATION_STRETCH)) {
"Could not set logical render size; layout may be wrong on HiDPI displays"); 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 // Initialize configuration, database, and network manager
if (!state->config.initialize()) { if (!state->config.initialize()) {
radiantwave::logger::Logger::instance().warning("Config", radiantwave::logger::Logger::instance().warning(
"Settings file could not be loaded. Default settings will be used (Config initialization failed at ~/.local/radiantwave/config.yaml)"); "Config", "Settings file could not be loaded. Default settings "
"will be used (Config initialization failed at "
"~/.local/radiantwave/config.yaml)");
} else { } else {
radiantwave::logger::Logger::instance().info("Config", "Configuration initialized successfully"); radiantwave::logger::Logger::instance().info(
"Config", "Configuration initialized successfully");
} }
if (!state->db.initialize()) { if (!state->db.initialize()) {
radiantwave::logger::Logger::instance().error("Database", radiantwave::logger::Logger::instance().error(
"Database storage unavailable. App features may be limited (Database initialization failed at ~/.local/radiantwave/radiantwave.db)"); "Database", "Database storage unavailable. App features may be "
"limited (Database initialization failed at "
"~/.local/radiantwave/radiantwave.db)");
} else { } else {
radiantwave::logger::Logger::instance().info("Database", "Database initialized and schema migrated"); radiantwave::logger::Logger::instance().info(
"Database", "Database initialized and schema migrated");
} }
if (!state->network.initialize()) { if (!state->network.initialize()) {
radiantwave::logger::Logger::instance().warning("Network", radiantwave::logger::Logger::instance().warning(
"WiFi network management is unavailable. Check if NetworkManager is running (D-Bus connection to org.freedesktop.NetworkManager failed)"); "Network", "WiFi network management is unavailable. Check if "
"NetworkManager is running (D-Bus connection to "
"org.freedesktop.NetworkManager failed)");
} else { } else {
radiantwave::logger::Logger::instance().info("Network", "NetworkManager service connected"); radiantwave::logger::Logger::instance().info(
"Network", "NetworkManager service connected");
} }
if (!state->audio.initialize(state->config)) { if (!state->audio.initialize(state->config)) {
radiantwave::logger::Logger::instance().warning("Audio", radiantwave::logger::Logger::instance().warning(
"Audio playback is unavailable. Check sound hardware connections (SDL3_mixer initialization failed)"); "Audio", "Audio playback is unavailable. Check sound hardware "
"connections (SDL3_mixer initialization failed)");
} else { } else {
radiantwave::logger::Logger::instance().info("Audio", "Audio subsystem initialized"); radiantwave::logger::Logger::instance().info(
"Audio", "Audio subsystem initialized");
} }
// Let playback logs report user-facing display names from the database. // Let playback logs report user-facing display names from the database.
state->audio.attachDatabase(state->db); state->audio.attachDatabase(state->db);
// Background download of audio assets. The worker keeps retrying until // Background download of audio assets. The worker keeps retrying until
// internet is available, so downloads start automatically on connect. // internet is available, so downloads start automatically on connect.
// The downloaded files are reconciled into the database's audio_files table. // The downloaded files are reconciled into the database's audio_files
// table.
state->audioDownloader.start(state->db); state->audioDownloader.start(state->db);
// Background license validation. Home triggers a refresh whenever it is // Background license validation. Home triggers a refresh whenever it is
// displayed; the result gates access to Play and the Proclamation options. // displayed; the result gates access to Play and the Proclamation options.
state->licenseTracker.start(state->db); state->licenseTracker.start(state->db);
state->config.onChanged([state](const radiantwave::config::AppConfig& new_config) { state->config.onChanged(
[state](const radiantwave::config::AppConfig &new_config) {
SDL_Log("Live config update received: volume=%d, dynamic_audio=%d", SDL_Log("Live config update received: volume=%d, dynamic_audio=%d",
new_config.audio.volume, new_config.audio.dynamic_audio ? 1 : 0); new_config.audio.volume,
state->db.logEvent("INFO", "Config", "Live configuration hot-reloaded"); 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->keybinds.bind(SDLK_UP, "menu_up", [state]() {
@@ -121,9 +140,9 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
}); });
// Drive the First-Run walkthrough: shows the WiFi page when the internet // Drive the First-Run walkthrough: shows the WiFi page when the internet
// is unreachable, then the email and license pages when those are missing, // is unreachable <DEPRECATED>, then the email and license pages when those
// finishing on Home. The internet check is a real connectivity probe // are missing, finishing on Home. The internet check is a real connectivity
// (Ethernet, WiFi, or macOS all work), so it never traps a connected // probe (Ethernet, WiFi, or macOS all work), so it never traps a connected
// device on the WiFi page. // device on the WiFi page.
radiantwave::provisioning::begin(*state); radiantwave::provisioning::begin(*state);
@@ -131,12 +150,10 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
return SDL_APP_CONTINUE; return SDL_APP_CONTINUE;
} }
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) {
{
auto &state = *static_cast<AppState *>(appstate); auto &state = *static_cast<AppState *>(appstate);
if (event->type == SDL_EVENT_QUIT) if (event->type == SDL_EVENT_QUIT) {
{
state.quitRequested = true; state.quitRequested = true;
} }
@@ -144,14 +161,12 @@ SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
state.pages.handleEvent(*event); state.pages.handleEvent(*event);
} }
if (state.quitRequested && state.quitFadeRemaining <= 0.0f) if (state.quitRequested && state.quitFadeRemaining <= 0.0f) {
{
// Graceful shutdown: start the audio fade-out and keep the loop // Graceful shutdown: start the audio fade-out and keep the loop
// running until it completes, then let SDL_AppQuit tear everything // running until it completes, then let SDL_AppQuit tear everything
// down. When nothing is playing there is nothing to fade and quitting // down. When nothing is playing there is nothing to fade and quitting
// is immediate. // is immediate.
if (state.audio.stopTrackFadeOut(kQuitFadeSeconds)) if (state.audio.stopTrackFadeOut(kQuitFadeSeconds)) {
{
state.quitFadeRemaining = kQuitFadeSeconds; state.quitFadeRemaining = kQuitFadeSeconds;
return SDL_APP_CONTINUE; return SDL_APP_CONTINUE;
} }
@@ -161,23 +176,21 @@ SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
return SDL_APP_CONTINUE; return SDL_APP_CONTINUE;
} }
SDL_AppResult SDL_AppIterate(void *appstate) SDL_AppResult SDL_AppIterate(void *appstate) {
{
auto &state = *static_cast<AppState *>(appstate); auto &state = *static_cast<AppState *>(appstate);
Uint64 nowTicks = SDL_GetTicks(); Uint64 nowTicks = SDL_GetTicks();
static Uint64 lastTicks = nowTicks; static Uint64 lastTicks = nowTicks;
float dt = static_cast<float>(nowTicks - lastTicks) / 1000.0f; float dt = static_cast<float>(nowTicks - lastTicks) / 1000.0f;
if (dt > 0.1f) dt = 0.1f; if (dt > 0.1f)
dt = 0.1f;
lastTicks = nowTicks; lastTicks = nowTicks;
state.pages.update(dt); state.pages.update(dt);
if (state.quitFadeRemaining > 0.0f) if (state.quitFadeRemaining > 0.0f) {
{
state.quitFadeRemaining -= dt; state.quitFadeRemaining -= dt;
if (state.quitFadeRemaining <= 0.0f) if (state.quitFadeRemaining <= 0.0f) {
{
return SDL_APP_SUCCESS; return SDL_APP_SUCCESS;
} }
} }
@@ -191,8 +204,7 @@ SDL_AppResult SDL_AppIterate(void *appstate)
return SDL_APP_CONTINUE; return SDL_APP_CONTINUE;
} }
void SDL_AppQuit(void *appstate, SDL_AppResult result) void SDL_AppQuit(void *appstate, SDL_AppResult result) {
{
auto *state = static_cast<AppState *>(appstate); auto *state = static_cast<AppState *>(appstate);
if (state) { if (state) {
state->audioDownloader.shutdown(); state->audioDownloader.shutdown();
+2 -1
View File
@@ -1,6 +1,7 @@
#include "proclamation_options_page.h" #include "proclamation_options_page.h"
#include "app_state.h" #include "app_state.h"
#include "network/proclamation_sync.h" #include "network/proclamation_sync.h"
#include "logical_size.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
@@ -552,7 +553,7 @@ void ProclamationOptionsPage::update(float dt)
void ProclamationOptionsPage::render(SDL_Renderer* renderer) void ProclamationOptionsPage::render(SDL_Renderer* renderer)
{ {
int winW, winH; int winW, winH;
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH); radiantwave::getLayoutSize(renderer, &winW, &winH);
float fw = static_cast<float>(winW); float fw = static_cast<float>(winW);
float fh = static_cast<float>(winH); float fh = static_cast<float>(winH);
+18 -16
View File
@@ -1,19 +1,18 @@
#include "provisioning.h" #include "provisioning.h"
#include "wifi_setup_page.h"
#include "email_address_page.h" #include "email_address_page.h"
#include "license_key_page.h"
#include "home.h" #include "home.h"
#include "license_key_page.h"
#include "logger/logger.h" #include "logger/logger.h"
#include "wifi_setup_page.h"
namespace radiantwave::provisioning { namespace radiantwave::provisioning {
// Push the page for a walkthrough step, or Home when the walkthrough is done. // Push the page for a walkthrough step, or Home when the walkthrough is done.
static void pushStep(AppState& state, Step step) static void pushStep(AppState &state, Step step) {
{
switch (step) { switch (step) {
case Step::Internet: // case Step::Internet:
state.pages.push(state, std::make_unique<WiFiSetupPage>()); // state.pages.push(state, std::make_unique<WiFiSetupPage>());
break; // break;
case Step::Email: case Step::Email:
state.pages.push(state, std::make_unique<EmailAddressPage>()); state.pages.push(state, std::make_unique<EmailAddressPage>());
break; break;
@@ -27,8 +26,7 @@ static void pushStep(AppState& state, Step step)
} }
} }
void begin(AppState& state) void begin(AppState &state) {
{
// Without the database there is nowhere to persist email/license, so the // Without the database there is nowhere to persist email/license, so the
// walkthrough would trap the user on pages that can never be completed. // walkthrough would trap the user on pages that can never be completed.
if (!state.db.isOpen()) { if (!state.db.isOpen()) {
@@ -38,14 +36,17 @@ void begin(AppState& state)
} }
const bool internetUp = state.network.hasInternet(); const bool internetUp = state.network.hasInternet();
const bool hasEmail = !state.db.getKV(radiantwave::db::KV_EMAIL_ADDRESS).empty(); const bool hasEmail =
const bool hasLicense = !state.db.getKV(radiantwave::db::KV_LICENSE_KEY).empty(); !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)) { if (indicatesFirstRun(internetUp, hasEmail, hasLicense)) {
// Placeholder: a brand-new device. Later this flag will launch the // Placeholder: a brand-new device. Later this flag will launch the
// first-run welcome walkthrough (TOS, quick user guide, ...). // first-run welcome walkthrough (TOS, quick user guide, ...).
state.firstRunWalkthrough = true; state.firstRunWalkthrough = true;
radiantwave::logger::Logger::instance().info("Provisioning", radiantwave::logger::Logger::instance().info(
"Provisioning",
"First-Run detected (no internet, no email, no license); " "First-Run detected (no internet, no email, no license); "
"first-run welcome walkthrough is not yet implemented"); "first-run welcome walkthrough is not yet implemented");
} }
@@ -55,8 +56,7 @@ void begin(AppState& state)
pushStep(state, firstMissingStep(internetUp, hasEmail, hasLicense)); pushStep(state, firstMissingStep(internetUp, hasEmail, hasLicense));
} }
void advance(AppState& state) void advance(AppState &state) {
{
if (state.pages.depth() > 0) { if (state.pages.depth() > 0) {
state.pages.pop(state); state.pages.pop(state);
} }
@@ -70,8 +70,10 @@ void advance(AppState& state)
state.walkthroughInternetUp = internetUp; state.walkthroughInternetUp = internetUp;
} }
const bool hasEmail = !state.db.getKV(radiantwave::db::KV_EMAIL_ADDRESS).empty(); const bool hasEmail =
const bool hasLicense = !state.db.getKV(radiantwave::db::KV_LICENSE_KEY).empty(); !state.db.getKV(radiantwave::db::KV_EMAIL_ADDRESS).empty();
const bool hasLicense =
!state.db.getKV(radiantwave::db::KV_LICENSE_KEY).empty();
state.provisioningMode = true; state.provisioningMode = true;
pushStep(state, firstMissingStep(internetUp, hasEmail, hasLicense)); pushStep(state, firstMissingStep(internetUp, hasEmail, hasLicense));
+10 -10
View File
@@ -24,33 +24,33 @@
namespace radiantwave::provisioning { namespace radiantwave::provisioning {
// Prerequisite steps in walkthrough priority order. // 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. // First unsatisfied step, given the current conditions.
inline Step firstMissingStep(bool internetUp, bool hasEmail, bool hasLicense) inline Step firstMissingStep(bool internetUp, bool hasEmail, bool hasLicense) {
{ // if (!internetUp) return Step::Internet;
if (!internetUp) return Step::Internet; if (!hasEmail)
if (!hasEmail) return Step::Email; return Step::Email;
if (!hasLicense) return Step::License; if (!hasLicense)
return Step::License;
return Step::Done; return Step::Done;
} }
// True when nothing is configured (no internet, no email, no license). // True when nothing is configured (no internet, no email, no license).
// Signals a brand-new, never-provisioned device. // Signals a brand-new, never-provisioned device.
inline bool indicatesFirstRun(bool internetUp, bool hasEmail, bool hasLicense) inline bool indicatesFirstRun(bool internetUp, bool hasEmail, bool hasLicense) {
{
return !internetUp && !hasEmail && !hasLicense; return !internetUp && !hasEmail && !hasLicense;
} }
// Startup walkthrough. Evaluates the prerequisites and pushes the first // Startup walkthrough. Evaluates the prerequisites and pushes the first
// missing step's page (with provisioningMode set), or Home when everything // missing step's page (with provisioningMode set), or Home when everything
// is satisfied. Home is always the end of the walkthrough. // 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 // 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 // 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 // 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. // missing, so completing a step never blocks on a redundant check.
void advance(AppState& state); void advance(AppState &state);
} // namespace radiantwave::provisioning } // namespace radiantwave::provisioning
+2 -1
View File
@@ -5,6 +5,7 @@
#include "proclamation_options_page.h" #include "proclamation_options_page.h"
#include "scramble/fibonacci_scrambler.h" #include "scramble/fibonacci_scrambler.h"
#include "utf8_utils.h" #include "utf8_utils.h"
#include "logical_size.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h> #include <SDL3_ttf/SDL_ttf.h>
@@ -35,7 +36,7 @@ void ScrollerPage::onPush(AppState &state)
int winW = 0; int winW = 0;
int winH = 0; int winH = 0;
SDL_GetWindowSize(state.window, &winW, &winH); radiantwave::getLayoutSize(state.renderer, &winW, &winH);
screenW_ = static_cast<float>(winW); screenW_ = static_cast<float>(winW);
screenH_ = static_cast<float>(winH); screenH_ = static_cast<float>(winH);
// Lines are capped at the full screen width; the widest Fibonacci lines are // Lines are capped at the full screen width; the widest Fibonacci lines are
+2 -1
View File
@@ -10,6 +10,7 @@
#include "email_address_page.h" #include "email_address_page.h"
#include "license_key_page.h" #include "license_key_page.h"
#include "date_format_page.h" #include "date_format_page.h"
#include "logical_size.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h> #include <SDL3_image/SDL_image.h>
#include <algorithm> #include <algorithm>
@@ -128,7 +129,7 @@ void Settings::update(float dt)
void Settings::render(SDL_Renderer *renderer) void Settings::render(SDL_Renderer *renderer)
{ {
int winW, winH; int winW, winH;
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH); radiantwave::getLayoutSize(renderer, &winW, &winH);
float fw = static_cast<float>(winW); float fw = static_cast<float>(winW);
float fh = static_cast<float>(winH); float fh = static_cast<float>(winH);
+2 -1
View File
@@ -1,5 +1,6 @@
#include "timezone_page.h" #include "timezone_page.h"
#include "app_state.h" #include "app_state.h"
#include "logical_size.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <algorithm> #include <algorithm>
#include <ctime> #include <ctime>
@@ -199,7 +200,7 @@ void TimeZonePage::update(float dt) {
void TimeZonePage::render(SDL_Renderer* renderer) { void TimeZonePage::render(SDL_Renderer* renderer) {
int winW, winH; int winW, winH;
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH); radiantwave::getLayoutSize(renderer, &winW, &winH);
float fw = static_cast<float>(winW); float fw = static_cast<float>(winW);
float fh = static_cast<float>(winH); float fh = static_cast<float>(winH);
+2 -1
View File
@@ -1,6 +1,7 @@
#include "wifi_setup_page.h" #include "wifi_setup_page.h"
#include "app_state.h" #include "app_state.h"
#include "provisioning.h" #include "provisioning.h"
#include "logical_size.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <algorithm> #include <algorithm>
#include <exception> #include <exception>
@@ -301,7 +302,7 @@ void WiFiSetupPage::update(float dt) {
void WiFiSetupPage::render(SDL_Renderer* renderer) { void WiFiSetupPage::render(SDL_Renderer* renderer) {
int winW, winH; int winW, winH;
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH); radiantwave::getLayoutSize(renderer, &winW, &winH);
float fw = static_cast<float>(winW); float fw = static_cast<float>(winW);
float fh = static_cast<float>(winH); float fh = static_cast<float>(winH);
+8 -22
View File
@@ -1,50 +1,36 @@
#include "test_harness.h"
#include "../src/provisioning.h" #include "../src/provisioning.h"
#include "test_harness.h"
using radiantwave::provisioning::firstMissingStep; using radiantwave::provisioning::firstMissingStep;
using radiantwave::provisioning::indicatesFirstRun; using radiantwave::provisioning::indicatesFirstRun;
using radiantwave::provisioning::Step; using radiantwave::provisioning::Step;
TEST(first_missing_step_all_satisfied) TEST(first_missing_step_all_satisfied) {
{
ASSERT_EQ(firstMissingStep(true, true, true), Step::Done); ASSERT_EQ(firstMissingStep(true, true, true), Step::Done);
} }
TEST(first_missing_step_no_internet) TEST(first_missing_step_no_email) {
{
// 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, false), Step::Email);
ASSERT_EQ(firstMissingStep(true, false, true), Step::Email); ASSERT_EQ(firstMissingStep(true, false, true), Step::Email);
} }
TEST(first_missing_step_no_license) TEST(first_missing_step_no_license) {
{
ASSERT_EQ(firstMissingStep(true, true, false), Step::License); ASSERT_EQ(firstMissingStep(true, true, false), Step::License);
} }
TEST(first_run_all_missing) TEST(first_run_all_missing) {
{
ASSERT_TRUE(indicatesFirstRun(false, false, false)); ASSERT_TRUE(indicatesFirstRun(false, false, false));
} }
TEST(first_run_has_internet) TEST(first_run_has_internet) {
{
ASSERT_FALSE(indicatesFirstRun(true, false, false)); ASSERT_FALSE(indicatesFirstRun(true, false, false));
} }
TEST(first_run_has_email) TEST(first_run_has_email) {
{
ASSERT_FALSE(indicatesFirstRun(false, true, false)); ASSERT_FALSE(indicatesFirstRun(false, true, false));
} }
TEST(first_run_has_license) TEST(first_run_has_license) {
{
ASSERT_FALSE(indicatesFirstRun(false, false, true)); ASSERT_FALSE(indicatesFirstRun(false, false, true));
} }