Removed Debug logs from user-view

This commit is contained in:
NeilClack
2026-08-15 15:43:49 -05:00
parent f14bf3e29a
commit e401bd4ec5
14 changed files with 198 additions and 39 deletions
+9
View File
@@ -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
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?fileVersion 4.0.0?><cproject storage_type_id="org.eclipse.cdt.core.XmlProjectDescriptionStorage">
<storageModule moduleId="org.eclipse.cdt.core.settings">
<cconfiguration id="org.eclipse.cdt.core.default.config.2139874559">
<storageModule buildSystemId="org.eclipse.cdt.core.defaultConfigDataProvider" id="org.eclipse.cdt.core.default.config.2139874559" moduleId="org.eclipse.cdt.core.settings" name="Configuration">
<externalSettings/>
<extensions/>
</storageModule>
<storageModule moduleId="org.eclipse.cdt.core.externalSettings"/>
</cconfiguration>
</storageModule>
<storageModule moduleId="org.eclipse.cdt.core.pathentry">
<pathentry excluding="**/CMakeFiles/**" kind="out" path="build"/>
</storageModule>
<storageModule moduleId="org.eclipse.cdt.core.LanguageSettingsProviders"/>
</cproject>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>radiantwave</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.cdt.core.cBuilder</name>
<triggers>clean,full,incremental,</triggers>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.cdt.core.cnature</nature>
<nature>org.eclipse.cdt.core.ccnature</nature>
<nature>org.eclipse.cdt.cmake.core.cmakeNature</nature>
</natures>
</projectDescription>
+2 -1
View File
@@ -254,12 +254,13 @@ target_link_libraries(test_logger PRIVATE GTest::gtest_main)
add_executable(test_audio add_executable(test_audio
tests/test_audio.cpp tests/test_audio.cpp
src/audio/audio_manager.cpp src/audio/audio_manager.cpp
src/db/database_manager.cpp
src/config/yaml_parser.cpp src/config/yaml_parser.cpp
src/config/config_manager.cpp src/config/config_manager.cpp
src/logger/logger.cpp src/logger/logger.cpp
) )
target_include_directories(test_audio PRIVATE src) 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 # Audio downloader unit tests
add_executable(test_audio_downloader add_executable(test_audio_downloader
+4 -4
View File
@@ -367,7 +367,7 @@ void AudioDownloader::run() {
if (stop_.load()) break; if (stop_.load()) break;
setStatus("Downloading audio files (" + std::to_string(downloaded + 1) + setStatus("Downloading audio files (" + std::to_string(downloaded + 1) +
" / " + std::to_string(queue.size()) + ")..."); " / " + std::to_string(queue.size()) + ")...");
if (!downloadAndVerify(work.filename, work.md5)) break; if (!downloadAndVerify(work.filename, work.md5, work.display_name)) break;
addAvailable(work); addAvailable(work);
++downloaded; ++downloaded;
// Publish immediately so a file that just finished downloading // Publish immediately so a file that just finished downloading
@@ -388,7 +388,7 @@ void AudioDownloader::run() {
continue; continue;
} }
if (orderByFilename.count(name) == 0) { 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); std::filesystem::remove(it->path(), ec);
} }
} }
@@ -425,7 +425,7 @@ bool AudioDownloader::fetchManifest(std::vector<AudioManifestEntry>& out) {
return true; 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 dir = audioDir();
std::string destPath = dir + filename; std::string destPath = dir + filename;
std::string tmpPath = destPath + ".part"; std::string tmpPath = destPath + ".part";
@@ -454,7 +454,7 @@ bool AudioDownloader::downloadAndVerify(const std::string& filename, const std::
return false; return false;
} }
logger::Logger::instance().info("Audio", "Downloaded and verified " + filename); logger::Logger::instance().info("Audio", "Downloaded and verified " + displayName);
return true; return true;
} }
+1 -1
View File
@@ -72,7 +72,7 @@ public:
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); 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);
+26 -4
View File
@@ -1,4 +1,5 @@
#include "audio_manager.h" #include "audio_manager.h"
#include "db/database_manager.h"
#include "logger/logger.h" #include "logger/logger.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
@@ -104,10 +105,31 @@ namespace radiantwave::audio
float gain = enabled_ ? (volume_ / 100.0f) : 0.0f; float gain = enabled_ ? (volume_ / 100.0f) : 0.0f;
MIX_SetMixerGain(mixer_, gain); 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; return true;
} }
void AudioManager::attachDatabase(db::DatabaseManager& db)
{
std::lock_guard<std::mutex> 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 float AudioManager::effectiveGain() const
{ {
return enabled_ ? (volume_ / 100.0f) : 0.0f; return enabled_ ? (volume_ / 100.0f) : 0.0f;
@@ -233,7 +255,7 @@ namespace radiantwave::audio
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
currentDevice_ = raw_device_name; 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_) if (configManager_)
{ {
@@ -301,7 +323,7 @@ namespace radiantwave::audio
slot.fadeInElapsed = 0.0f; slot.fadeInElapsed = 0.0f;
MIX_SetTrackGain(slot.track, 0.0f); MIX_SetTrackGain(slot.track, 0.0f);
MIX_PlayTrack(slot.track, 0); // Loop playback 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) bool AudioManager::playTrack(const std::string &wav_filename)
@@ -325,7 +347,7 @@ namespace radiantwave::audio
{ cfg.audio.current_track = wav_filename; }); { 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; return true;
} }
+9
View File
@@ -6,6 +6,10 @@
#include <string> #include <string>
#include <vector> #include <vector>
namespace radiantwave::db {
class DatabaseManager;
}
namespace radiantwave::audio { namespace radiantwave::audio {
struct AudioDeviceItem { struct AudioDeviceItem {
@@ -28,6 +32,9 @@ public:
// Initialize SDL3_mixer audio system // Initialize SDL3_mixer audio system
bool initialize(config::ConfigManager& config); bool initialize(config::ConfigManager& config);
// Provide a database handle for user-facing display-name lookups.
void attachDatabase(db::DatabaseManager& db);
// Shutdown audio subsystem // Shutdown audio subsystem
void shutdown(); void shutdown();
@@ -75,8 +82,10 @@ private:
float effectiveGain() const; float effectiveGain() const;
bool loadTrackInto(int slotIndex, const std::string& wav_filename); bool loadTrackInto(int slotIndex, const std::string& wav_filename);
void startFadeIn(int slotIndex, float seconds); void startFadeIn(int slotIndex, float seconds);
std::string displayNameFor(const std::string& wav_filename) const;
config::ConfigManager* configManager_ = nullptr; config::ConfigManager* configManager_ = nullptr;
db::DatabaseManager* db_ = nullptr;
bool initialized_ = false; bool initialized_ = false;
bool enabled_ = true; bool enabled_ = true;
+11 -8
View File
@@ -46,7 +46,6 @@ std::vector<std::string> wrapText(TTF_Font* font, const std::string& text, float
const char* filterLevelToString(LogViewerPage::FilterLevel lvl) { const char* filterLevelToString(LogViewerPage::FilterLevel lvl) {
switch (lvl) { switch (lvl) {
case LogViewerPage::FilterLevel::All: return "ALL"; case LogViewerPage::FilterLevel::All: return "ALL";
case LogViewerPage::FilterLevel::Debug: return "DEBUG";
case LogViewerPage::FilterLevel::Info: return "INFO"; case LogViewerPage::FilterLevel::Info: return "INFO";
case LogViewerPage::FilterLevel::Warning: return "WARNING"; case LogViewerPage::FilterLevel::Warning: return "WARNING";
case LogViewerPage::FilterLevel::Error: return "ERROR"; case LogViewerPage::FilterLevel::Error: return "ERROR";
@@ -87,11 +86,11 @@ void LogViewerPage::refreshLogs() {
using namespace radiantwave::logger; using namespace radiantwave::logger;
if (filterLevel_ == FilterLevel::All) { 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 { } else {
LogLevel targetLvl = LogLevel::Info; LogLevel targetLvl = LogLevel::Info;
if (filterLevel_ == FilterLevel::Debug) targetLvl = LogLevel::Debug; if (filterLevel_ == FilterLevel::Info) targetLvl = LogLevel::Info;
else if (filterLevel_ == FilterLevel::Info) targetLvl = LogLevel::Info;
else if (filterLevel_ == FilterLevel::Warning) targetLvl = LogLevel::Warning; else if (filterLevel_ == FilterLevel::Warning) targetLvl = LogLevel::Warning;
else if (filterLevel_ == FilterLevel::Error) targetLvl = LogLevel::Error; else if (filterLevel_ == FilterLevel::Error) targetLvl = LogLevel::Error;
else if (filterLevel_ == FilterLevel::Fatal) targetLvl = LogLevel::Fatal; else if (filterLevel_ == FilterLevel::Fatal) targetLvl = LogLevel::Fatal;
@@ -99,6 +98,11 @@ void LogViewerPage::refreshLogs() {
rawEntries_ = Logger::instance().getEntriesFiltered(targetLvl, true); 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) { if (sortOrder_ == SortOrder::NewestFirst) {
std::sort(rawEntries_.begin(), rawEntries_.end(), [](const LogEntry& a, const LogEntry& b) { std::sort(rawEntries_.begin(), rawEntries_.end(), [](const LogEntry& a, const LogEntry& b) {
return a.timestamp > b.timestamp; return a.timestamp > b.timestamp;
@@ -119,11 +123,11 @@ void LogViewerPage::handleEvent(const SDL_Event& event) {
SDL_Keycode key = event.key.key; SDL_Keycode key = event.key.key;
if (key == SDLK_L || key == SDLK_F2 || key == SDLK_RIGHT) { if (key == SDLK_L || key == SDLK_F2 || key == SDLK_RIGHT) {
filterLevel_ = static_cast<FilterLevel>((static_cast<int>(filterLevel_) + 1) % 6); filterLevel_ = static_cast<FilterLevel>((static_cast<int>(filterLevel_) + 1) % 5);
scrollOffset_ = 0; scrollOffset_ = 0;
refreshLogs(); refreshLogs();
} else if (key == SDLK_LEFT) { } else if (key == SDLK_LEFT) {
filterLevel_ = static_cast<FilterLevel>((static_cast<int>(filterLevel_) - 1 + 6) % 6); filterLevel_ = static_cast<FilterLevel>((static_cast<int>(filterLevel_) - 1 + 5) % 5);
scrollOffset_ = 0; scrollOffset_ = 0;
refreshLogs(); refreshLogs();
} else if (key == SDLK_S || key == SDLK_F3) { } 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; std::string fullLineStr = "[" + timeStr + "] [" + std::string(logLevelToString(entry.level)) + "] [" + entry.component + "] " + entry.message;
SDL_Color levelColor = {255, 255, 255, 255}; SDL_Color levelColor = {255, 255, 255, 255};
if (entry.level == LogLevel::Debug) levelColor = {160, 160, 160, 255}; if (entry.level == LogLevel::Info) levelColor = {255, 255, 255, 255};
else 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::Warning) levelColor = {255, 200, 0, 255};
else if (entry.level == LogLevel::Error) levelColor = {255, 80, 80, 255}; else if (entry.level == LogLevel::Error) levelColor = {255, 80, 80, 255};
else if (entry.level == LogLevel::Fatal) levelColor = {255, 60, 180, 255}; else if (entry.level == LogLevel::Fatal) levelColor = {255, 60, 180, 255};
-1
View File
@@ -11,7 +11,6 @@ class LogViewerPage : public Page {
public: public:
enum class FilterLevel { enum class FilterLevel {
All, All,
Debug,
Info, Info,
Warning, Warning,
Error, Error,
+2
View File
@@ -87,6 +87,8 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
} 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.
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.
+33 -19
View File
@@ -325,41 +325,41 @@ NetworkManager& NetworkManager::operator=(NetworkManager&&) noexcept = default;
// ─── Lifecycle ───────────────────────────────────────────────────────── // ─── Lifecycle ─────────────────────────────────────────────────────────
bool NetworkManager::initialize() { bool NetworkManager::initialize() {
log("Initializing NetworkManager connection..."); logDebug("Initializing NetworkManager connection...");
#ifdef __linux__ #ifdef __linux__
int r = sd_bus_open_system(&impl_->bus); int r = sd_bus_open_system(&impl_->bus);
if (r < 0) { 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; return false;
} }
// Check if NM is running // Check if NM is running
uint32_t state = 0; uint32_t state = 0;
if (!impl_->getProperty(NM_SERVICE, NM_PATH, NM_SERVICE, "State", "u", &state)) { 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; return false;
} }
log("NetworkManager state: " + std::to_string(state)); logDebug("NetworkManager state: " + std::to_string(state));
// Find WiFi device // Find WiFi device
impl_->device = WifiDevice::findFirstDevice(); impl_->device = WifiDevice::findFirstDevice();
if (!impl_->device) { if (!impl_->device) {
log("No WiFi device found"); logDebug("No WiFi device found");
return false; return false;
} }
log("Found WiFi device: " + impl_->device->getInterfaceName()); logDebug("Found WiFi device: " + impl_->device->getInterfaceName());
return true; return true;
#else #else
log("NetworkManager not available on this platform"); logDebug("NetworkManager not available on this platform");
return false; return false;
#endif #endif
} }
void NetworkManager::shutdown() { void NetworkManager::shutdown() {
log("Shutting down NetworkManager connection..."); logDebug("Shutting down NetworkManager connection...");
impl_->device.reset(); impl_->device.reset();
#ifdef __linux__ #ifdef __linux__
@@ -374,14 +374,14 @@ void NetworkManager::shutdown() {
bool NetworkManager::requestScan() { bool NetworkManager::requestScan() {
if (!impl_->device) { if (!impl_->device) {
log("No WiFi device available"); logDebug("No WiFi device available");
return false; return false;
} }
log("Requesting WiFi scan..."); logDebug("Requesting WiFi scan...");
const bool ok = impl_->device->requestScan(); const bool ok = impl_->device->requestScan();
if (!ok) { 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; return ok;
} }
@@ -446,7 +446,7 @@ ConnectResult NetworkManager::connect(
const std::string& password, const std::string& password,
std::chrono::seconds timeout std::chrono::seconds timeout
) { ) {
log("Connecting to: " + ssid); logDebug("Connecting to: " + ssid);
notifyState(ConnectionState::Preparing, "Preparing connection to " + ssid); notifyState(ConnectionState::Preparing, "Preparing connection to " + ssid);
if (!impl_->device) { if (!impl_->device) {
@@ -517,7 +517,7 @@ bool NetworkManager::disconnect() {
return false; return false;
} }
log("Disconnecting..."); logDebug("Disconnecting...");
#ifdef __linux__ #ifdef __linux__
// Get active connection // Get active connection
@@ -632,7 +632,7 @@ bool NetworkManager::forgetNetwork(const std::string& ssid) {
if (r >= 0) { if (r >= 0) {
found = true; found = true;
log("Deleted connection profile: " + std::string(conn_path)); logDebug("Deleted connection profile: " + std::string(conn_path));
} }
sd_bus_error_free(&del_error); sd_bus_error_free(&del_error);
} }
@@ -775,6 +775,20 @@ void NetworkManager::log(const std::string& message) {
radiantwave::logger::Logger::instance().info("WiFi", 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) { void NetworkManager::notifyState(ConnectionState state, const std::string& message) {
if (state != impl_->last_notified_state && impl_->state_callback) { if (state != impl_->last_notified_state && impl_->state_callback) {
impl_->last_notified_state = state; 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) { if (current_state == nm_device_state::Failed) {
log("Device state changed to Failed"); logWarning("Device state changed to Failed");
return false; return false;
} }
@@ -808,7 +822,7 @@ bool NetworkManager::waitForDeviceState(uint32_t target_state, std::chrono::seco
} }
void NetworkManager::deleteStaleProfiles(const std::string& ssid) { void NetworkManager::deleteStaleProfiles(const std::string& ssid) {
log("Deleting stale profiles for: " + ssid); logDebug("Deleting stale profiles for: " + ssid);
forgetNetwork(ssid); forgetNetwork(ssid);
} }
@@ -902,11 +916,11 @@ void NetworkManager::refreshDnsResolver() const {
} }
bool NetworkManager::waitForFullConnectivity(std::chrono::seconds timeout) { bool NetworkManager::waitForFullConnectivity(std::chrono::seconds timeout) {
log("Waiting for full connectivity..."); logDebug("Waiting for full connectivity...");
// Wait for device to be activated // Wait for device to be activated
if (!waitForDeviceState(nm_device_state::Activated, timeout)) { if (!waitForDeviceState(nm_device_state::Activated, timeout)) {
log("Device did not reach Activated state"); logWarning("Device did not reach Activated state");
return false; return false;
} }
@@ -927,7 +941,7 @@ bool NetworkManager::waitForFullConnectivity(std::chrono::seconds timeout) {
std::this_thread::sleep_for(std::chrono::milliseconds(500)); std::this_thread::sleep_for(std::chrono::milliseconds(500));
} }
log("No internet connectivity after timeout"); logWarning("No internet connectivity after timeout");
return false; return false;
} }
+3 -1
View File
@@ -99,7 +99,9 @@ public:
private: private:
// Internal helpers // 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); void notifyState(ConnectionState state, const std::string& message);
// Wait for device state // Wait for device state
+62
View File
@@ -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