diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..367e78c --- /dev/null +++ b/.clang-format @@ -0,0 +1,9 @@ +# We'll use defaults from the LLVM style, but with some modifications so that it's close to the CDT K&R style. +BasedOnStyle: LLVM +UseTab: Always +IndentWidth: 4 +TabWidth: 4 +PackConstructorInitializers: NextLineOnly +BreakConstructorInitializers: AfterColon +IndentAccessModifiers: false +AccessModifierOffset: -4 \ No newline at end of file diff --git a/.cproject b/.cproject new file mode 100644 index 0000000..830c1ec --- /dev/null +++ b/.cproject @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 0000000..f6bf446 --- /dev/null +++ b/.project @@ -0,0 +1,20 @@ + + + radiantwave + + + + + + org.eclipse.cdt.core.cBuilder + clean,full,incremental, + + + + + + org.eclipse.cdt.core.cnature + org.eclipse.cdt.core.ccnature + org.eclipse.cdt.cmake.core.cmakeNature + + diff --git a/CMakeLists.txt b/CMakeLists.txt index d6bb29f..1a4df17 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -254,12 +254,13 @@ target_link_libraries(test_logger PRIVATE GTest::gtest_main) add_executable(test_audio tests/test_audio.cpp src/audio/audio_manager.cpp + src/db/database_manager.cpp src/config/yaml_parser.cpp src/config/config_manager.cpp src/logger/logger.cpp ) target_include_directories(test_audio PRIVATE src) -target_link_libraries(test_audio PRIVATE GTest::gtest_main SDL3_mixer::SDL3_mixer SDL3::SDL3) +target_link_libraries(test_audio PRIVATE GTest::gtest_main SDL3_mixer::SDL3_mixer SDL3::SDL3 SQLite3::SQLite3) # Audio downloader unit tests add_executable(test_audio_downloader diff --git a/src/audio/audio_downloader.cpp b/src/audio/audio_downloader.cpp index c83ebdc..ffe1ad8 100644 --- a/src/audio/audio_downloader.cpp +++ b/src/audio/audio_downloader.cpp @@ -367,7 +367,7 @@ void AudioDownloader::run() { if (stop_.load()) break; setStatus("Downloading audio files (" + std::to_string(downloaded + 1) + " / " + std::to_string(queue.size()) + ")..."); - if (!downloadAndVerify(work.filename, work.md5)) break; + if (!downloadAndVerify(work.filename, work.md5, work.display_name)) break; addAvailable(work); ++downloaded; // Publish immediately so a file that just finished downloading @@ -388,7 +388,7 @@ void AudioDownloader::run() { continue; } if (orderByFilename.count(name) == 0) { - logger::Logger::instance().info("Audio", "Removing leftover " + name); + logger::Logger::instance().debug("Audio", "Removing leftover " + name); std::filesystem::remove(it->path(), ec); } } @@ -425,7 +425,7 @@ bool AudioDownloader::fetchManifest(std::vector& out) { return true; } -bool AudioDownloader::downloadAndVerify(const std::string& filename, const std::string& expectedMd5) { +bool AudioDownloader::downloadAndVerify(const std::string& filename, const std::string& expectedMd5, const std::string& displayName) { std::string dir = audioDir(); std::string destPath = dir + filename; std::string tmpPath = destPath + ".part"; @@ -454,7 +454,7 @@ bool AudioDownloader::downloadAndVerify(const std::string& filename, const std:: return false; } - logger::Logger::instance().info("Audio", "Downloaded and verified " + filename); + logger::Logger::instance().info("Audio", "Downloaded and verified " + displayName); return true; } diff --git a/src/audio/audio_downloader.h b/src/audio/audio_downloader.h index 8c16ae3..8c8b8d6 100644 --- a/src/audio/audio_downloader.h +++ b/src/audio/audio_downloader.h @@ -72,7 +72,7 @@ public: private: void run(); bool fetchManifest(std::vector& out); - bool downloadAndVerify(const std::string& filename, const std::string& expectedMd5); + bool downloadAndVerify(const std::string& filename, const std::string& expectedMd5, const std::string& displayName); void setStatus(const std::string& msg); void wait(std::chrono::seconds seconds); diff --git a/src/audio/audio_manager.cpp b/src/audio/audio_manager.cpp index e5ca9db..7fc16a8 100644 --- a/src/audio/audio_manager.cpp +++ b/src/audio/audio_manager.cpp @@ -1,4 +1,5 @@ #include "audio_manager.h" +#include "db/database_manager.h" #include "logger/logger.h" #include @@ -104,10 +105,31 @@ namespace radiantwave::audio float gain = enabled_ ? (volume_ / 100.0f) : 0.0f; MIX_SetMixerGain(mixer_, gain); - logger::Logger::instance().info("Audio", "SDL3_mixer audio system initialized successfully"); + logger::Logger::instance().debug("Audio", "SDL3_mixer audio system initialized successfully"); return true; } + void AudioManager::attachDatabase(db::DatabaseManager& db) + { + std::lock_guard lock(mutex_); + db_ = &db; + } + + std::string AudioManager::displayNameFor(const std::string& wav_filename) const + { + if (db_) + { + for (const auto& record : db_->getAudioFiles()) + { + if (record.filename == wav_filename) + { + return record.display_name; + } + } + } + return wav_filename; + } + float AudioManager::effectiveGain() const { return enabled_ ? (volume_ / 100.0f) : 0.0f; @@ -233,7 +255,7 @@ namespace radiantwave::audio std::lock_guard lock(mutex_); currentDevice_ = raw_device_name; - logger::Logger::instance().info("Audio", "Switched audio output device to: " + mapToFriendlyDeviceName(raw_device_name)); + logger::Logger::instance().debug("Audio", "Switched audio output device to: " + mapToFriendlyDeviceName(raw_device_name)); if (configManager_) { @@ -301,7 +323,7 @@ namespace radiantwave::audio slot.fadeInElapsed = 0.0f; MIX_SetTrackGain(slot.track, 0.0f); MIX_PlayTrack(slot.track, 0); // Loop playback - logger::Logger::instance().info("Audio", "Started playback of " + currentTrack_ + " (fade-in)"); + logger::Logger::instance().info("Audio", "Started playback of " + displayNameFor(currentTrack_) + " (fade-in)"); } bool AudioManager::playTrack(const std::string &wav_filename) @@ -325,7 +347,7 @@ namespace radiantwave::audio { cfg.audio.current_track = wav_filename; }); } - logger::Logger::instance().info("Audio", "Started playback of " + wav_filename); + logger::Logger::instance().info("Audio", "Started playback of " + displayNameFor(wav_filename)); return true; } diff --git a/src/audio/audio_manager.h b/src/audio/audio_manager.h index 3296bdc..fe5760d 100644 --- a/src/audio/audio_manager.h +++ b/src/audio/audio_manager.h @@ -6,6 +6,10 @@ #include #include +namespace radiantwave::db { +class DatabaseManager; +} + namespace radiantwave::audio { struct AudioDeviceItem { @@ -28,6 +32,9 @@ public: // Initialize SDL3_mixer audio system bool initialize(config::ConfigManager& config); + // Provide a database handle for user-facing display-name lookups. + void attachDatabase(db::DatabaseManager& db); + // Shutdown audio subsystem void shutdown(); @@ -75,8 +82,10 @@ private: float effectiveGain() const; bool loadTrackInto(int slotIndex, const std::string& wav_filename); void startFadeIn(int slotIndex, float seconds); + std::string displayNameFor(const std::string& wav_filename) const; config::ConfigManager* configManager_ = nullptr; + db::DatabaseManager* db_ = nullptr; bool initialized_ = false; bool enabled_ = true; diff --git a/src/log_viewer_page.cpp b/src/log_viewer_page.cpp index 2c94840..31cc93e 100644 --- a/src/log_viewer_page.cpp +++ b/src/log_viewer_page.cpp @@ -46,7 +46,6 @@ std::vector wrapText(TTF_Font* font, const std::string& text, float const char* filterLevelToString(LogViewerPage::FilterLevel lvl) { switch (lvl) { case LogViewerPage::FilterLevel::All: return "ALL"; - case LogViewerPage::FilterLevel::Debug: return "DEBUG"; case LogViewerPage::FilterLevel::Info: return "INFO"; case LogViewerPage::FilterLevel::Warning: return "WARNING"; case LogViewerPage::FilterLevel::Error: return "ERROR"; @@ -87,11 +86,11 @@ void LogViewerPage::refreshLogs() { using namespace radiantwave::logger; if (filterLevel_ == FilterLevel::All) { - rawEntries_ = Logger::instance().getEntries(); + // Debug logs are stored but never displayed; show everything else. + rawEntries_ = Logger::instance().getEntriesFiltered(LogLevel::Info, false); } else { LogLevel targetLvl = LogLevel::Info; - if (filterLevel_ == FilterLevel::Debug) targetLvl = LogLevel::Debug; - else if (filterLevel_ == FilterLevel::Info) targetLvl = LogLevel::Info; + if (filterLevel_ == FilterLevel::Info) targetLvl = LogLevel::Info; else if (filterLevel_ == FilterLevel::Warning) targetLvl = LogLevel::Warning; else if (filterLevel_ == FilterLevel::Error) targetLvl = LogLevel::Error; else if (filterLevel_ == FilterLevel::Fatal) targetLvl = LogLevel::Fatal; @@ -99,6 +98,11 @@ void LogViewerPage::refreshLogs() { rawEntries_ = Logger::instance().getEntriesFiltered(targetLvl, true); } + // Defense in depth: Debug entries must never reach the viewer. + rawEntries_.erase(std::remove_if(rawEntries_.begin(), rawEntries_.end(), + [](const LogEntry& e) { return e.level == LogLevel::Debug; }), + rawEntries_.end()); + if (sortOrder_ == SortOrder::NewestFirst) { std::sort(rawEntries_.begin(), rawEntries_.end(), [](const LogEntry& a, const LogEntry& b) { return a.timestamp > b.timestamp; @@ -119,11 +123,11 @@ void LogViewerPage::handleEvent(const SDL_Event& event) { SDL_Keycode key = event.key.key; if (key == SDLK_L || key == SDLK_F2 || key == SDLK_RIGHT) { - filterLevel_ = static_cast((static_cast(filterLevel_) + 1) % 6); + filterLevel_ = static_cast((static_cast(filterLevel_) + 1) % 5); scrollOffset_ = 0; refreshLogs(); } else if (key == SDLK_LEFT) { - filterLevel_ = static_cast((static_cast(filterLevel_) - 1 + 6) % 6); + filterLevel_ = static_cast((static_cast(filterLevel_) - 1 + 5) % 5); scrollOffset_ = 0; refreshLogs(); } else if (key == SDLK_S || key == SDLK_F3) { @@ -256,8 +260,7 @@ void LogViewerPage::rebuildWrapped(float maxLineWidth) { std::string fullLineStr = "[" + timeStr + "] [" + std::string(logLevelToString(entry.level)) + "] [" + entry.component + "] " + entry.message; SDL_Color levelColor = {255, 255, 255, 255}; - if (entry.level == LogLevel::Debug) levelColor = {160, 160, 160, 255}; - else if (entry.level == LogLevel::Info) levelColor = {255, 255, 255, 255}; + if (entry.level == LogLevel::Info) levelColor = {255, 255, 255, 255}; else if (entry.level == LogLevel::Warning) levelColor = {255, 200, 0, 255}; else if (entry.level == LogLevel::Error) levelColor = {255, 80, 80, 255}; else if (entry.level == LogLevel::Fatal) levelColor = {255, 60, 180, 255}; diff --git a/src/log_viewer_page.h b/src/log_viewer_page.h index ed892d0..157c0d5 100644 --- a/src/log_viewer_page.h +++ b/src/log_viewer_page.h @@ -11,7 +11,6 @@ class LogViewerPage : public Page { public: enum class FilterLevel { All, - Debug, Info, Warning, Error, diff --git a/src/main.cpp b/src/main.cpp index 704a643..162ceb9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -87,6 +87,8 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) } 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. diff --git a/src/network/network_manager.cpp b/src/network/network_manager.cpp index 20b2626..50f4eb7 100644 --- a/src/network/network_manager.cpp +++ b/src/network/network_manager.cpp @@ -325,41 +325,41 @@ NetworkManager& NetworkManager::operator=(NetworkManager&&) noexcept = default; // ─── Lifecycle ───────────────────────────────────────────────────────── bool NetworkManager::initialize() { - log("Initializing NetworkManager connection..."); + logDebug("Initializing NetworkManager connection..."); #ifdef __linux__ int r = sd_bus_open_system(&impl_->bus); if (r < 0) { - log("Failed to open system bus: " + std::string(strerror(-r))); + logWarning("Failed to open system bus: " + std::string(strerror(-r))); return false; } // Check if NM is running uint32_t state = 0; if (!impl_->getProperty(NM_SERVICE, NM_PATH, NM_SERVICE, "State", "u", &state)) { - log("Failed to get NM state - is NetworkManager running?"); + logWarning("Failed to get NM state - is NetworkManager running?"); return false; } - log("NetworkManager state: " + std::to_string(state)); + logDebug("NetworkManager state: " + std::to_string(state)); // Find WiFi device impl_->device = WifiDevice::findFirstDevice(); if (!impl_->device) { - log("No WiFi device found"); + logDebug("No WiFi device found"); return false; } - log("Found WiFi device: " + impl_->device->getInterfaceName()); + logDebug("Found WiFi device: " + impl_->device->getInterfaceName()); return true; #else - log("NetworkManager not available on this platform"); + logDebug("NetworkManager not available on this platform"); return false; #endif } void NetworkManager::shutdown() { - log("Shutting down NetworkManager connection..."); + logDebug("Shutting down NetworkManager connection..."); impl_->device.reset(); #ifdef __linux__ @@ -374,14 +374,14 @@ void NetworkManager::shutdown() { bool NetworkManager::requestScan() { if (!impl_->device) { - log("No WiFi device available"); + logDebug("No WiFi device available"); return false; } - log("Requesting WiFi scan..."); + logDebug("Requesting WiFi scan..."); const bool ok = impl_->device->requestScan(); if (!ok) { - log("RequestScan failed; the network list may stay empty until it succeeds"); + logWarning("RequestScan failed; the network list may stay empty until it succeeds"); } return ok; } @@ -446,7 +446,7 @@ ConnectResult NetworkManager::connect( const std::string& password, std::chrono::seconds timeout ) { - log("Connecting to: " + ssid); + logDebug("Connecting to: " + ssid); notifyState(ConnectionState::Preparing, "Preparing connection to " + ssid); if (!impl_->device) { @@ -517,7 +517,7 @@ bool NetworkManager::disconnect() { return false; } - log("Disconnecting..."); + logDebug("Disconnecting..."); #ifdef __linux__ // Get active connection @@ -632,7 +632,7 @@ bool NetworkManager::forgetNetwork(const std::string& ssid) { if (r >= 0) { found = true; - log("Deleted connection profile: " + std::string(conn_path)); + logDebug("Deleted connection profile: " + std::string(conn_path)); } sd_bus_error_free(&del_error); } @@ -775,6 +775,20 @@ void NetworkManager::log(const std::string& message) { radiantwave::logger::Logger::instance().info("WiFi", message); } +void NetworkManager::logDebug(const std::string& message) { + if (impl_->log_callback) { + impl_->log_callback("[NM] " + message); + } + radiantwave::logger::Logger::instance().debug("WiFi", message); +} + +void NetworkManager::logWarning(const std::string& message) { + if (impl_->log_callback) { + impl_->log_callback("[NM] " + message); + } + radiantwave::logger::Logger::instance().warning("WiFi", message); +} + void NetworkManager::notifyState(ConnectionState state, const std::string& message) { if (state != impl_->last_notified_state && impl_->state_callback) { impl_->last_notified_state = state; @@ -797,7 +811,7 @@ bool NetworkManager::waitForDeviceState(uint32_t target_state, std::chrono::seco } if (current_state == nm_device_state::Failed) { - log("Device state changed to Failed"); + logWarning("Device state changed to Failed"); return false; } @@ -808,7 +822,7 @@ bool NetworkManager::waitForDeviceState(uint32_t target_state, std::chrono::seco } void NetworkManager::deleteStaleProfiles(const std::string& ssid) { - log("Deleting stale profiles for: " + ssid); + logDebug("Deleting stale profiles for: " + ssid); forgetNetwork(ssid); } @@ -902,11 +916,11 @@ void NetworkManager::refreshDnsResolver() const { } bool NetworkManager::waitForFullConnectivity(std::chrono::seconds timeout) { - log("Waiting for full connectivity..."); + logDebug("Waiting for full connectivity..."); // Wait for device to be activated if (!waitForDeviceState(nm_device_state::Activated, timeout)) { - log("Device did not reach Activated state"); + logWarning("Device did not reach Activated state"); return false; } @@ -927,7 +941,7 @@ bool NetworkManager::waitForFullConnectivity(std::chrono::seconds timeout) { std::this_thread::sleep_for(std::chrono::milliseconds(500)); } - log("No internet connectivity after timeout"); + logWarning("No internet connectivity after timeout"); return false; } diff --git a/src/network/network_manager.h b/src/network/network_manager.h index 12d4c3e..e1ba00d 100644 --- a/src/network/network_manager.h +++ b/src/network/network_manager.h @@ -99,7 +99,9 @@ public: private: // Internal helpers - void log(const std::string& message); + void log(const std::string& message); // Info level + void logDebug(const std::string& message); // Debug level + void logWarning(const std::string& message); // Warning level void notifyState(ConnectionState state, const std::string& message); // Wait for device state diff --git a/user_manual_draft.md b/user_manual_draft.md new file mode 100644 index 0000000..18bd542 --- /dev/null +++ b/user_manual_draft.md @@ -0,0 +1,62 @@ + +Radiant Wave +User Manual +Version 1.0 + +Intro +Some thank you message and introduction to Radiant Waves. + +Table of Contents + +Setup +Connect to WiFi +Connection to the internet is required for Radiant Wave to operate. + +Wifi screen appears first +UP/DOWN to navigate through detected WiFi networks +Enter to select one +Password prompt if applicable + +I still need to test entering an incorrect WiFi password on a kiosk machine +Check for crashes or correct error messaging + +Email Address + +The next screen you’ll be presented with is the email address prompt. + +When you purchased your RadiantWave system, you should have received an email at the address you provided during your purchase containing your license key. + +Enter the email address where you received your license key here. + +Note that you will not be able to enter an invalid email address, but you will be able to enter the incorrect email address. You will see how to change your email address later. + +License Key + +Grab your license key from your email, now is the time to use it! + +A few notes on the License Key prompt: +You don't have to hold SHIFT or enable CAPS LOCK, because that's already done for you +You won't have to enter the dashes "-", as those are entered for you +You CAN enter an incorrect license key +You CAN fix it later + +Same steps as the email address, just type out your license key and press enter. + +The Main Menu + +Now you're in! This is the main menu, and it doesn't do a whole lot. It's only purpose is to display some system information for you to quickly reference when needed, such as the email address and license key you currently have saved or the current system version you have running. Other than that, the Main Menu's only purpose is to provide navigation between the system settings, and the main attraction itself. + +TODO + +wifi +email +license +main menu +settings +time zone +date +logs +audio options +proclamations +play +updates