diff --git a/USER_MANUAL.md b/USER_MANUAL.md index c980ba6..c2d38a6 100644 --- a/USER_MANUAL.md +++ b/USER_MANUAL.md @@ -118,7 +118,7 @@ Manager features: - Refresh playlist view. -------------------------------------------------------------------- - + ## Mapping figurines (RFID → what to play) Option A: Map through the web app (recommended) diff --git a/src/config.cpp b/src/config.cpp index 842325a..bbb543c 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -1,27 +1,30 @@ #include "config.h" #include "globals.h" #include +#include +#include +#include +#include // Global config instance Config config; -const String getConfigFilePath() { - static String config_dir; - if (config_dir.isEmpty()) { - config_dir.concat("/"); - config_dir.concat(sys_dir); - config_dir.concat("/"); - config_dir.concat(config_dir); +const char* getConfigFilePath() { + static char path[64]; + static bool init = false; + if (!init) { + // Build "//" once into a static buffer + snprintf(path, sizeof(path), "/%s/%s", sys_dir ? sys_dir : "", config_file ? config_file : ""); + init = true; } - - return config_dir; + return path; } void setDefaultConfig() { config.initialVolume = 7; config.maxVolume = 15; config.sleepTime = 1800000; - config.minVoltage = 3200; + config.minVoltage = 3000; config.voltage100Percent = 4200; config.sleepDelay = 1800000; config.sleepMessageDelay = 1798000; @@ -32,127 +35,169 @@ void setDefaultConfig() { } bool loadConfig() { - String configPath = getConfigFilePath(); - + const char* configPath = getConfigFilePath(); + if (!SD.exists(configPath)) { Serial.println(F("Config file not found, using defaults")); setDefaultConfig(); return saveConfig(); // Create config file with defaults } - + File file = SD.open(configPath, FILE_READ); if (!file) { Serial.println(F("Failed to open config file")); setDefaultConfig(); return false; } - + // Set defaults first setDefaultConfig(); - - // Parse config file line by line - while (file.available()) { - String line = file.readStringUntil('\n'); - line.trim(); - + + // Parse config file line by line using a fixed-size buffer (no dynamic allocation) + char line[160]; + while (true) { + int idx = 0; + int c; + // Read characters until newline or EOF + while (idx < (int)sizeof(line) - 1 && (c = file.read()) >= 0) { + if (c == '\r') continue; + if (c == '\n') break; + line[idx++] = (char)c; + } + if (idx == 0 && c < 0) { + // EOF with no more data + break; + } + line[idx] = '\0'; + + // Trim leading/trailing whitespace + char* s = line; + while (*s && isspace((unsigned char)*s)) ++s; + char* end = s + strlen(s); + while (end > s && isspace((unsigned char)end[-1])) --end; + *end = '\0'; + // Skip empty lines and comments - if (line.length() == 0 || line.startsWith("#")) { + if (*s == '\0' || *s == '#') { + if (c < 0) break; continue; } - - int separatorIndex = line.indexOf('='); - if (separatorIndex == -1) { + + // Split at '=' + char* eq = strchr(s, '='); + if (!eq) { + if (c < 0) break; continue; } - - String key = line.substring(0, separatorIndex); - String value = line.substring(separatorIndex + 1); - key.trim(); - value.trim(); - + *eq = '\0'; + char* key = s; + char* val = eq + 1; + + // Trim key + while (*key && isspace((unsigned char)*key)) ++key; + char* kend = key + strlen(key); + while (kend > key && isspace((unsigned char)kend[-1])) --kend; + *kend = '\0'; + + // Trim val + while (*val && isspace((unsigned char)*val)) ++val; + char* vend = val + strlen(val); + while (vend > val && isspace((unsigned char)vend[-1])) --vend; + *vend = '\0'; + // Parse each configuration value - if (key == "initialVolume") { - config.initialVolume = constrain(value.toInt(), 0, 21); - } else if (key == "maxVolume") { - config.maxVolume = constrain(value.toInt(), 1, 21); - } else if (key == "sleepTime") { - config.sleepTime = value.toInt(); - } else if (key == "minVoltage") { - config.minVoltage = value.toInt(); - } else if (key == "voltage100Percent") { - config.voltage100Percent = value.toInt(); - } else if (key == "sleepDelay") { - config.sleepDelay = value.toInt(); - } else if (key == "sleepMessageDelay") { - config.sleepMessageDelay = value.toInt(); - } else if (key == "rfidLoopInterval") { - config.rfidLoopInterval = constrain(value.toInt(), 1, 255); - } else if (key == "startAtStoredProgress") { - config.startAtStoredProgress = (value == "1" || value.equalsIgnoreCase("true")); - } else if (key == "wifiSSID") { - strncpy(config.wifiSSID, value.c_str(), sizeof(config.wifiSSID) - 1); + if (strcmp(key, "initialVolume") == 0) { + long v = strtol(val, nullptr, 10); + if (v < 0) v = 0; if (v > 21) v = 21; + config.initialVolume = (uint8_t)v; + } else if (strcmp(key, "maxVolume") == 0) { + long v = strtol(val, nullptr, 10); + if (v < 1) v = 1; if (v > 21) v = 21; + config.maxVolume = (uint8_t)v; + } else if (strcmp(key, "sleepTime") == 0) { + config.sleepTime = (uint32_t)strtoul(val, nullptr, 10); + } else if (strcmp(key, "minVoltage") == 0) { + config.minVoltage = (uint16_t)strtoul(val, nullptr, 10); + } else if (strcmp(key, "voltage100Percent") == 0) { + config.voltage100Percent = (uint16_t)strtoul(val, nullptr, 10); + } else if (strcmp(key, "sleepDelay") == 0) { + config.sleepDelay = (uint32_t)strtoul(val, nullptr, 10); + } else if (strcmp(key, "sleepMessageDelay") == 0) { + config.sleepMessageDelay = (uint32_t)strtoul(val, nullptr, 10); + } else if (strcmp(key, "rfidLoopInterval") == 0) { + long v = strtol(val, nullptr, 10); + if (v < 1) v = 1; if (v > 255) v = 255; + config.rfidLoopInterval = (uint8_t)v; + } else if (strcmp(key, "startAtStoredProgress") == 0) { + bool b = (strcmp(val, "1") == 0) || (strcasecmp(val, "true") == 0); + config.startAtStoredProgress = b; + } else if (strcmp(key, "wifiSSID") == 0) { + strncpy(config.wifiSSID, val, sizeof(config.wifiSSID) - 1); config.wifiSSID[sizeof(config.wifiSSID) - 1] = '\0'; - } else if (key == "wifiPassword") { - strncpy(config.wifiPassword, value.c_str(), sizeof(config.wifiPassword) - 1); + } else if (strcmp(key, "wifiPassword") == 0) { + strncpy(config.wifiPassword, val, sizeof(config.wifiPassword) - 1); config.wifiPassword[sizeof(config.wifiPassword) - 1] = '\0'; } + + if (c < 0) break; // EOF after processing last line } - + file.close(); - - Serial.println("Config loaded successfully"); - Serial.print("Initial Volume: "); Serial.println(config.initialVolume); - Serial.print("Max Volume: "); Serial.println(config.maxVolume); - Serial.print("Sleep Delay: "); Serial.println(config.sleepDelay); - Serial.print("RFID Interval: "); Serial.println(config.rfidLoopInterval); - + + Serial.println(F("Config loaded successfully")); + Serial.print(F("Initial Volume: ")); Serial.println(config.initialVolume); + Serial.print(F("Max Volume: ")); Serial.println(config.maxVolume); + Serial.print(F("Sleep Delay: ")); Serial.println(config.sleepDelay); + Serial.print(F("RFID Interval: ")); Serial.println(config.rfidLoopInterval); + Serial.print(F("min Voltage: ")); Serial.println(config.minVoltage); + return true; } bool saveConfig() { - String configPath = getConfigFilePath(); - + const char* configPath = getConfigFilePath(); + File file = SD.open(configPath, FILE_WRITE); if (!file) { Serial.println(F("Failed to create config file")); return false; } - + // Write config file with comments for user reference - file.println("# HannaBox Conf File"); - file.println("# format: key=value"); - file.println(""); - - file.println("# Audio"); - file.print("initialVolume="); file.println(config.initialVolume); - file.print("maxVolume="); file.println(config.maxVolume); - file.println(""); - - file.println("# Power Management (in milliseconds)"); - file.print("sleepTime="); file.println(config.sleepTime); - file.print("sleepDelay="); file.println(config.sleepDelay); - file.print("sleepMessageDelay="); file.println(config.sleepMessageDelay); - file.println(""); - - file.println("# Battery (in millivolts)"); - file.print("minVoltage="); file.println(config.minVoltage); - file.print("voltage100Percent="); file.println(config.voltage100Percent); - file.println(""); - - file.println("# RFID"); - file.print("rfidLoopInterval="); file.println(config.rfidLoopInterval); - file.println(""); - - file.println("# Playback"); - file.print("startAtStoredProgress="); file.println(config.startAtStoredProgress ? "true" : "false"); - file.println(""); - - file.println("# WiFi (leave empty to use current WiFiManager)"); - file.print("wifiSSID="); file.println(config.wifiSSID); - file.print("wifiPassword="); file.println(config.wifiPassword); - + file.println(F("# HannaBox Conf File")); + file.println(F("# format: key=value")); + file.println(); + + file.println(F("# Audio")); + file.print(F("initialVolume=")); file.println(config.initialVolume); + file.print(F("maxVolume=")); file.println(config.maxVolume); + file.println(); + + file.println(F("# Power Management (in milliseconds)")); + file.print(F("sleepTime=")); file.println(config.sleepTime); + file.print(F("sleepDelay=")); file.println(config.sleepDelay); + file.print(F("sleepMessageDelay=")); file.println(config.sleepMessageDelay); + file.println(); + + file.println(F("# Battery (in millivolts)")); + file.print(F("minVoltage=")); file.println(config.minVoltage); + file.print(F("voltage100Percent=")); file.println(config.voltage100Percent); + file.println(); + + file.println(F("# RFID")); + file.print(F("rfidLoopInterval=")); file.println(config.rfidLoopInterval); + file.println(); + + file.println(F("# Playback")); + file.print(F("startAtStoredProgress=")); file.println(config.startAtStoredProgress ? F("true") : F("false")); + file.println(); + + file.println(F("# WiFi (leave empty to use current WiFiManager)")); + file.print(F("wifiSSID=")); file.println(config.wifiSSID); + file.print(F("wifiPassword=")); file.println(config.wifiPassword); + file.close(); - + Serial.println(F("Config saved successfully")); return true; } diff --git a/src/config.h b/src/config.h index 06c2268..b3c3eee 100644 --- a/src/config.h +++ b/src/config.h @@ -3,19 +3,30 @@ #include -// Configuration structure - keep it minimal for memory efficiency +// Keep the structure compact to minimize padding and RAM usage. +// Order fields from wider to narrower types, and avoid inline default initializers. +// Defaults are applied in setDefaultConfig() to keep initialization simple and lightweight. struct Config { - uint8_t initialVolume = 7; - uint8_t maxVolume = 15; - uint32_t sleepTime = 1800000; // 30 minutes in ms - uint16_t minVoltage = 3200; // mV - minimum voltage before sleep - uint16_t voltage100Percent = 4200; // mV - voltage representing 100% battery - uint32_t sleepDelay = 1800000; // 30 minutes in ms - uint32_t sleepMessageDelay = 1798000; // 2 seconds before sleep - uint8_t rfidLoopInterval = 25; // RFID check interval - bool startAtStoredProgress = true; // Resume from last position - char wifiSSID[32] = ""; // WiFi SSID (empty = use current mechanism) - char wifiPassword[64] = ""; // WiFi password (empty = use current mechanism) + // 32-bit values + uint32_t sleepTime; // ms + uint32_t sleepDelay; // ms + uint32_t sleepMessageDelay; // ms + + // 16-bit values + uint16_t minVoltage; // mV + uint16_t voltage100Percent; // mV + + // 8-bit values + uint8_t initialVolume; + uint8_t maxVolume; + uint8_t rfidLoopInterval; + + // flags + bool startAtStoredProgress; + + // WiFi credentials (fixed-size buffers, no heap allocations) + char wifiSSID[32]; // empty => use current mechanism + char wifiPassword[64]; // empty => use current mechanism }; // Global config instance @@ -25,6 +36,9 @@ extern Config config; bool loadConfig(); bool saveConfig(); void setDefaultConfig(); -const String getConfigFilePath(); + +// Returns a pointer to a static buffer with the absolute config file path. +// Avoids dynamic allocation/Arduino String to reduce heap fragmentation. +const char* getConfigFilePath(); #endif diff --git a/src/main.cpp b/src/main.cpp index 3204ab6..8feb4f6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1697,11 +1697,15 @@ void loop() { lastVoltage = getBatteryVoltageMv(); free_heap = xPortGetFreeHeapSize(); - if (lastVoltage < config.minVoltage) + if (lastVoltage < config.minVoltage && config.minVoltage > 0) { if (voltage_threshold_counter > 3) { - Serial.println(F("deep sleep due to low volts..")); + Serial.print("deep sleep due to low volts ("); + Serial.print(lastVoltage); + Serial.print(") min: "); + Serial.println(config.minVoltage); + lastInteraction = millis() - config.sleepMessageDelay; voltage_threshold_counter = 0; }