Files
radiantwave/src/config/yaml_parser.cpp
T
NeilClack da0b6ffd5e Kiosk: apply schedule engine in playback and add System Settings toggle
ScrollerPage now resolves the active Block from ScheduleManager (reusing
the existing stream-switch path), honoring dynamicAudio true/false and
falling back to local selection for gaps, no schedule, or toggle off.
Adds a 'Schedule' On/Off entry (default On) in System Settings persisted
in config.yaml, and config plumbing (proclamation.schedule_enabled). All
on-device schedule times remain 12h AM/PM.
2026-09-29 10:34:45 -05:00

272 lines
9.5 KiB
C++

#include "yaml_parser.h"
#include <algorithm>
#include <cctype>
#include <sstream>
#include <vector>
namespace radiantwave::config {
namespace {
std::string trim(const std::string& str) {
auto start = str.find_first_not_of(" \t\r\n");
if (start == std::string::npos) return "";
auto end = str.find_last_not_of(" \t\r\n");
return str.substr(start, end - start + 1);
}
std::string unquote(const std::string& str) {
std::string s = trim(str);
if (s.size() >= 2) {
if ((s.front() == '"' && s.back() == '"') ||
(s.front() == '\'' && s.back() == '\'')) {
return s.substr(1, s.size() - 2);
}
}
return s;
}
std::string removeComments(const std::string& line) {
bool in_single_quote = false;
bool in_double_quote = false;
for (size_t i = 0; i < line.size(); ++i) {
char c = line[i];
if (c == '\'' && !in_double_quote) {
in_single_quote = !in_single_quote;
} else if (c == '"' && !in_single_quote) {
in_double_quote = !in_double_quote;
} else if (c == '#' && !in_single_quote && !in_double_quote) {
return line.substr(0, i);
}
}
return line;
}
int countIndent(const std::string& line) {
int indent = 0;
for (char c : line) {
if (c == ' ') indent++;
else if (c == '\t') indent += 2;
else break;
}
return indent;
}
} // anonymous namespace
// ─── YamlNode Implementation ───────────────────────────────────────────
std::string YamlNode::asString(const std::string& defaultValue) const {
if (type == Type::Scalar && !scalar.empty()) {
return scalar;
}
return defaultValue;
}
int YamlNode::asInt(int defaultValue) const {
if (type == Type::Scalar && !scalar.empty()) {
try {
return std::stoi(scalar);
} catch (...) {}
}
return defaultValue;
}
float YamlNode::asFloat(float defaultValue) const {
if (type == Type::Scalar && !scalar.empty()) {
try {
return std::stof(scalar);
} catch (...) {}
}
return defaultValue;
}
bool YamlNode::asBool(bool defaultValue) const {
if (type == Type::Scalar && !scalar.empty()) {
std::string s = scalar;
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return std::tolower(c); });
if (s == "true" || s == "yes" || s == "1" || s == "on") return true;
if (s == "false" || s == "no" || s == "0" || s == "off") return false;
}
return defaultValue;
}
bool YamlNode::hasChild(const std::string& key) const {
if (type != Type::Mapping) return false;
return children.find(key) != children.end();
}
static const YamlNode EMPTY_NODE;
const YamlNode& YamlNode::getChild(const std::string& key) const {
if (type == Type::Mapping) {
auto it = children.find(key);
if (it != children.end()) {
return it->second;
}
}
return EMPTY_NODE;
}
// ─── Parser ─────────────────────────────────────────────────────────────
YamlNode parseYaml(const std::string& content) {
YamlNode root;
root.type = YamlNode::Type::Mapping;
std::stringstream ss(content);
std::string raw_line;
std::string current_section = "";
while (std::getline(ss, raw_line)) {
std::string line_no_comment = removeComments(raw_line);
std::string line = trim(line_no_comment);
if (line.empty()) continue;
int indent = countIndent(line_no_comment);
auto colon_pos = line.find(':');
if (colon_pos == std::string::npos) continue;
std::string key = trim(line.substr(0, colon_pos));
std::string val = trim(line.substr(colon_pos + 1));
if (indent == 0) {
// Top-level key
if (val.empty()) {
// Start a section mapping
current_section = key;
YamlNode section_node;
section_node.type = YamlNode::Type::Mapping;
root.children[key] = section_node;
} else {
current_section = "";
YamlNode scalar_node;
scalar_node.type = YamlNode::Type::Scalar;
scalar_node.scalar = unquote(val);
root.children[key] = scalar_node;
}
} else {
// Indented key (child of current_section)
if (!current_section.empty() && root.children.find(current_section) != root.children.end()) {
YamlNode scalar_node;
scalar_node.type = YamlNode::Type::Scalar;
scalar_node.scalar = unquote(val);
root.children[current_section].children[key] = scalar_node;
}
}
}
return root;
}
std::string serializeYaml(const YamlNode& root) {
std::stringstream ss;
if (root.type == YamlNode::Type::Mapping) {
for (const auto& [key, node] : root.children) {
if (node.isScalar()) {
ss << key << ": \"" << node.scalar << "\"\n";
} else if (node.isMapping()) {
ss << "\n" << key << ":\n";
for (const auto& [child_key, child_node] : node.children) {
ss << " " << child_key << ": \"" << child_node.scalar << "\"\n";
}
}
}
}
return ss.str();
}
// ─── High-Level AppConfig Conversions ──────────────────────────────────
AppConfig parseConfigFromYaml(const std::string& yamlContent) {
AppConfig config = AppConfig::defaults();
YamlNode root = parseYaml(yamlContent);
if (root.hasChild("license_key")) {
config.license_key = root.getChild("license_key").asString(config.license_key);
}
if (root.hasChild("email_address")) {
config.email_address = root.getChild("email_address").asString(config.email_address);
}
if (root.hasChild("provisioning_status")) {
config.provisioning_status = root.getChild("provisioning_status").asString(config.provisioning_status);
}
if (root.hasChild("timezone")) {
config.timezone = root.getChild("timezone").asString(config.timezone);
}
if (root.hasChild("audio")) {
const auto& audio_node = root.getChild("audio");
if (audio_node.hasChild("enabled")) {
config.audio.enabled = audio_node.getChild("enabled").asBool(config.audio.enabled);
}
if (audio_node.hasChild("volume")) {
config.audio.volume = audio_node.getChild("volume").asInt(config.audio.volume);
}
if (audio_node.hasChild("output_device")) {
config.audio.output_device = audio_node.getChild("output_device").asString(config.audio.output_device);
}
if (audio_node.hasChild("current_track")) {
config.audio.current_track = audio_node.getChild("current_track").asString(config.audio.current_track);
}
if (audio_node.hasChild("dynamic_audio")) {
config.audio.dynamic_audio = audio_node.getChild("dynamic_audio").asBool(config.audio.dynamic_audio);
}
}
if (root.hasChild("proclamation")) {
const auto& proc_node = root.getChild("proclamation");
if (proc_node.hasChild("scroll_speed")) {
config.proclamation.scroll_speed = proc_node.getChild("scroll_speed").asFloat(config.proclamation.scroll_speed);
}
if (proc_node.hasChild("font_size")) {
config.proclamation.font_size = proc_node.getChild("font_size").asInt(config.proclamation.font_size);
}
if (proc_node.hasChild("current_id")) {
config.proclamation.current_id = proc_node.getChild("current_id").asString(config.proclamation.current_id);
}
if (proc_node.hasChild("schedule_active")) {
config.proclamation.schedule_active = proc_node.getChild("schedule_active").asBool(config.proclamation.schedule_active);
}
if (proc_node.hasChild("schedule_enabled")) {
config.proclamation.schedule_enabled = proc_node.getChild("schedule_enabled").asBool(config.proclamation.schedule_enabled);
}
}
return config;
}
std::string serializeConfigToYaml(const AppConfig& config) {
std::stringstream ss;
ss << "# RadiantWave Kiosk Configuration\n\n";
ss << "license_key: \"" << config.license_key << "\"\n";
ss << "email_address: \"" << config.email_address << "\"\n";
ss << "provisioning_status: \"" << config.provisioning_status << "\"\n";
ss << "timezone: \"" << config.timezone << "\"\n\n";
ss << "audio:\n";
ss << " enabled: " << (config.audio.enabled ? "true" : "false") << "\n";
ss << " volume: " << config.audio.volume << "\n";
ss << " output_device: \"" << config.audio.output_device << "\"\n";
ss << " current_track: \"" << config.audio.current_track << "\"\n";
ss << " dynamic_audio: " << (config.audio.dynamic_audio ? "true" : "false") << "\n\n";
ss << "proclamation:\n";
ss << " scroll_speed: " << config.proclamation.scroll_speed << "\n";
ss << " font_size: " << config.proclamation.font_size << "\n";
ss << " current_id: \"" << config.proclamation.current_id << "\"\n";
ss << " schedule_active: " << (config.proclamation.schedule_active ? "true" : "false") << "\n";
ss << " schedule_enabled: " << (config.proclamation.schedule_enabled ? "true" : "false") << "\n";
return ss.str();
}
} // namespace radiantwave::config