diff --git a/src/boot/FPPINIT.cpp b/src/boot/FPPINIT.cpp index 5b5cc5591..9f6b962b9 100644 --- a/src/boot/FPPINIT.cpp +++ b/src/boot/FPPINIT.cpp @@ -241,7 +241,7 @@ int main(int argc, char* argv[]) { // dup2 replaces that fd before we print anything, so this capture // still works even though the parent shell redirected us away. teeOutput(logFile, "fppinit", "Audio", getpid()); - } else if (action == "configureBBB" || action == "applyThermal" || action == "resetThermal" || action == "installKiosk" || action == "setupPiRTC") { + } else if (action == "configureBBB" || action == "applyThermal" || action == "resetThermal" || action == "installKiosk" || action == "setupPiRTC" || action == "setupHDMICEC") { // Same gap as setupNetwork: invoked directly via PHP exec(), output // otherwise lives only in that request's $output/HTTP response. teeOutput(logFile, "fppinit", "Config", getpid()); @@ -283,6 +283,7 @@ int main(int argc, char* argv[]) { // After DetectCape() above, so a cape shipping DisablePiRTC in its // defaultSettings is honoured on the boot it is first detected on. setupPiRTCConfig(); + setupHDMICECConfig(); int reboot = getRawSettingInt("rebootFlag", 0); if (reboot && !needReboot) { printf("FPP - Clearing reboot flags\n"); @@ -475,6 +476,9 @@ int main(int argc, char* argv[]) { } else if (action == "setupPiRTC") { // The UI's "reboot": 1 on DisablePiRTC prompts the user; don't reboot here. setupPiRTCConfig(false); + } else if (action == "setupHDMICEC") { + // The UI's "reboot": 1 on DisableHDMICECInit prompts the user; don't reboot here. + setupHDMICECConfig(false); } else if (action == "setupNetwork") { PutFileContents(networkSetupMut, "1"); setupNetwork(true); diff --git a/src/boot/FPPINIT.h b/src/boot/FPPINIT.h index 9ee2e6f48..346e1f0d8 100644 --- a/src/boot/FPPINIT.h +++ b/src/boot/FPPINIT.h @@ -94,6 +94,7 @@ void startZRAMSwap(); void startDiskSwap(); void setupChannelOutputs(); void setupPiRTCConfig(bool rebootIfChanged = true); +void setupHDMICECConfig(bool rebootIfChanged = true); void handleRebootActions(); // --------------------------------------------------------------------------- diff --git a/src/boot/FPPINIT_Config.cpp b/src/boot/FPPINIT_Config.cpp index a2f491508..fb029d82c 100644 --- a/src/boot/FPPINIT_Config.cpp +++ b/src/boot/FPPINIT_Config.cpp @@ -1257,6 +1257,83 @@ void setupPiRTCConfig(bool rebootIfChanged) { #endif } +#ifdef PLATFORM_PI +// Suppress the initial CEC Active Source at boot that wakes projectors/TVs +// (e.g. Epson L210SF, issue #2938). When enabled, adds hdmi_ignore_cec_init=1 +// to config.txt. Unlike hdmi_ignore_cec=1 this only suppresses the boot +// broadcast — the CEC plugin can still power the display on/off on demand. +// +// Managed block pattern mirrors applyDisablePiRTCBlock above: strip every +// prior copy (including truncated), remember first insertion point, and +// re-insert ahead of the cape variant block to avoid leapfrog reboot loops. +static const std::string HDMI_CEC_BLOCK_BEGIN = "# FPP HDMI CEC - BEGIN (managed by fppinit, do not edit)"; +static const std::string HDMI_CEC_BLOCK_END = "# FPP HDMI CEC - END"; + +// --- BEGIN applyDisableHDMICECBlock --- +static bool applyDisableHDMICECBlock(std::string& content, bool disable) { + const std::string orig = content; + std::string desired; + if (disable) { + desired = HDMI_CEC_BLOCK_BEGIN + "\n[all]\nhdmi_ignore_cec_init=1\n[all]\n" + HDMI_CEC_BLOCK_END + "\n"; + } + + size_t at = std::string::npos; + size_t begin = content.find(HDMI_CEC_BLOCK_BEGIN); + while (begin != std::string::npos) { + if (at == std::string::npos) { + at = begin; + } + size_t end = content.find(HDMI_CEC_BLOCK_END, begin); + end = (end == std::string::npos) ? content.length() + : end + HDMI_CEC_BLOCK_END.length(); + if (end < content.length() && content[end] == '\n') { + ++end; + } + content.erase(begin, end - begin); + begin = content.find(HDMI_CEC_BLOCK_BEGIN); + } + + if (!desired.empty()) { + if (at == std::string::npos) { + at = content.find(CAPE_VARIANT_BLOCK_BEGIN); + if (at == std::string::npos) { + while (!content.empty() && content.back() == '\n') { + content.pop_back(); + } + content += "\n\n"; + at = content.length(); + } else { + desired += "\n"; + } + } + content.insert(at, desired); + } + return content != orig; +} +// --- END applyDisableHDMICECBlock --- +#endif + +void setupHDMICECConfig(bool rebootIfChanged) { +#ifdef PLATFORM_PI + std::string content = GetFileContents("/boot/firmware/config.txt"); + if (content.empty()) { + return; + } + // Pi 5 has no firmware HDMI path (RP1/KMS), so hdmi_ignore_cec_init is + // ignored. Never write the block on Pi 5; this also cleans up a stale + // block left from when the setting previously defaulted to on. + bool want = !isPi5() && getRawSettingInt("DisableHDMICECInit", 0) != 0; + if (applyDisableHDMICECBlock(content, want)) { + PutFileContents("/boot/firmware/config.txt", content); + printf("FPP - HDMI CEC configuration changed in config.txt\n"); + if (rebootIfChanged) { + printf("\n\nRebooting to load new settings.\n\n"); + exec("/usr/sbin/reboot"); + } + } +#endif +} + void setupChannelOutputs() { #ifdef PLATFORM_PI bool hasDPI = false; diff --git a/www/common/settings.php b/www/common/settings.php index 6fcb86671..24db6f07c 100644 --- a/www/common/settings.php +++ b/www/common/settings.php @@ -462,6 +462,11 @@ function ApplySetting($setting, $value) // "reboot": 1, so the UI is already prompting for that. exec("sudo " . $settings['fppDir'] . "/src/fppinit setupPiRTC", $output); break; + case 'DisableHDMICECInit': + // fppinit owns the config.txt block. The setting is declared + // "reboot": 1, so the UI already prompts for reboot. + exec("sudo " . $settings['fppDir'] . "/src/fppinit setupHDMICEC", $output); + break; case 'screensaver': SetupScreenBlanking($value); break; diff --git a/www/help/settings-system.php b/www/help/settings-system.php index 3fb3ee02c..1b085623c 100644 --- a/www/help/settings-system.php +++ b/www/help/settings-system.php @@ -11,6 +11,7 @@
PrintFanThermalSettings() as FanTrip_<zone sanitized>_<index> inputs, per zone (e.g. CPU, K16-Pro) with trip points sorted low→high (“Fan On Temperature”, “Fan Speed 2/3…”). Changes are written to sysfs immediately and re-applied at every boot by FPPINIT_Config, since trip points revert to their device tree values on boot. Reset to Defaults POSTs api/settings/fanThermal/reset to restore the hardware values. Stored in °C, displayed in °F when Temperature display units = Fahrenheit./dev/i2c-* check) — OLED/LCD on the Pi/BeagleBone I²C bus. Options: Disabled, 128×64 SSD1306 (normal/flipped/h-flipped), 128×64 2-color variants, 128×32 variants, SH1106/SH1107, SSD1327, PCF8574/PCF8574A 16×2/20×4 I²C LCDs, etc. Reboot required.fppinit adds hdmi_ignore_cec_init=1 inside a managed block in /boot/firmware/config.txt. Unlike hdmi_ignore_cec=1 it only suppresses the boot broadcast — the CEC plugin can still power the display on/off on demand. Hidden on Pi 5 (KMS/RP1, no firmware HDMI path) and any stale block is removed there. Reboot required.fppd so slow switches/routers can come up and E1.31 multicast works. Options: 0–10s, 15/20/25/30s, 1/2/3/5 min, Auto (−1, wait until NTP/RTC time is valid or 5 min max). Not shown inside containers.