Skip to content

fix(HDMI): Prevent HDMI CEC from powering on display at boot - #2939

Open
jessica12ryan wants to merge 9 commits into
FalconChristmas:masterfrom
jessica12ryan:fixBootCEC
Open

jessica12ryan wants to merge 9 commits into
FalconChristmas:masterfrom
jessica12ryan:fixBootCEC

Conversation

@jessica12ryan

@jessica12ryan jessica12ryan commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

fix(HDMI): Prevent HDMI CEC from powering on display at boot

Summary

FPP 10 (Trixie, kernel 6.18 vc4-kms-v3d) wakes CEC-capable projectors/TVs ~5s after power-on, before the web UI is up. The Pi's firmware + vc4-hdmi-cec sends a CEC Active Source / Image View On on every HDMI modeset. With hdmi_force_hotplug=1 (SD/FPP_Install.sh:1176) and getty@tty1 enabled at SD/FPP_Install.sh:1296, a display that was in standby (e.g. Epson L210SF) powers on after any power loss — even though FPP 9.x did not.

This PR adds an Advanced-level UI toggle (on by default, opt-out) that suppresses only the initial boot broadcast. The CEC plugin can still power the display on/off on demand. Fresh + upgraded (never-saved) → 1 via upgrade/144 + src/boot/FPPINIT_Config.cpp:1000 migrateHDMICECDefault(); explicit 0 stays off.

Issue

  • Fresh FPP 10 flash wakes projector at ~5s, FPP 9.3 does not (reported on Epson L210SF)
  • User expectation: projector stays in standby until an explicit CEC command; power outage should not leave it running unattended

Root cause

  • Trixie base image + dtoverlay=vc4-kms-v3d,cma-256 (SD/FPP_Install.sh:1111) moves Pi from vc4-fkms-v3d to full KMS. src/framebuffer/KMSFrameBuffer.cpp:183 / src/framebuffer/FrameBuffer.cpp:260 (KMSFrameBuffer::EnableDisplay() at src/framebuffer/KMSFrameBuffer.cpp:767 via drmModeSetCrtc/SetPlane) plus early getty@tty1 drive HDMI and the kernel vc4-hdmi-cec emits Active Source by default.
  • No hdmi_ignore_cec* handling existed (grep 0 hits across src/ SD/ scripts/ www/), and SD/FPP_Install.sh:1112 intentionally kept hdmi_force_hotplug=1 for 6.18 KMS.

Changes

Additive only, no breaking changes, no renames/removals. Toggle defaults on → fresh installs get checked (www/settings.json:1631 default:"1"); existing installs with no key get 1 via upgrade/144 + src/boot/FPPINIT_Config.cpp:1000 migrateHDMICECDefault() on next upgrade_config / /fppos_upgraded boot; explicit 0 stays 0 (opt-out preserved).

  • www/settings.json:181,1620 — New setting DisableHDMICECInit in simplePipeWireVideo group:

    "DisableHDMICECInit": {
      "description": "Prevent HDMI CEC At Boot",
      "tip": "When enabled, adds hdmi_ignore_cec_init=1 to config.txt to suppress the initial CEC Active Source that wakes projectors/TVs at boot (e.g. after a power outage). CEC control via the CEC plugin still works.",
      "platforms": ["Raspberry Pi"], "type": "checkbox", "reboot": 1, "level": 1, "default": "1", "gatherStats": true
    }

    level:1 = Advanced UI. default:"1" = on by default. reboot:1 prompts reboot via existing UI banner. src/boot/FPPINIT_Config.cpp:1333 fallback also 1 so fresh/missing settings default on; upgrade/144 + src/boot/FPPINIT_Config.cpp:1000 migrateHDMICECDefault() pins never-saved → 1 on upgrade/FPPOS, explicit 0 stays 0.

  • www/common/settings.php:437 — New case 'DisableHDMICECInit' delegating to sudo …/src/fppinit setupHDMICEC (same delegation pattern as DisablePiRTC at www/common/settings.php:430).

  • src/boot/FPPINIT.h:97 — Declared setupHDMICECConfig(bool rebootIfChanged=true).

  • src/boot/FPPINIT_Config.cpp:1268 — Managed block HDMI_CEC_BLOCK_BEGIN/END + applyDisableHDMICECBlock() + setupHDMICECConfig(), mirroring applyDisablePiRTCBlock at src/boot/FPPINIT_Config.cpp:1184 (strip every prior copy including truncated, remember first insertion point, insert ahead of CAPE_VARIANT_BLOCK_BEGIN at src/boot/FPPINIT_Config.cpp:1170 to avoid leapfrog with setupChannelOutputs()'s DPI block). Desired block when enabled:

    # FPP HDMI CEC - BEGIN (managed by fppinit, do not edit)
    [all]
    hdmi_ignore_cec_init=1
    [all]
    # FPP HDMI CEC - END
    

    Uses hdmi_ignore_cec_init=1 (boot-only) not hdmi_ignore_cec=1 (would disable all CEC and break plugin).

  • src/boot/FPPINIT.cpp:244,286,479 — Wired setupHDMICEC into teeOutput facility, start boot path (setupHDMICECConfig() after setupPiRTCConfig() at src/boot/FPPINIT.cpp:280 with rebootIfChanged=true), and setupHDMICEC action handler with rebootIfChanged=false (UI path never auto-reboots).

SD/FPP_Install.sh:1112/1176/1296 intentionally untouched — image ships without the line; reconciled on first boot per toggle.

Behavior

  • Toggle on (default): fresh + upgraded (never-saved → 1 via upgrade/144 / src/boot/FPPINIT_Config.cpp:1000) → fppinit setupHDMICEC writes managed block [all]\nhdmi_ignore_cec_init=1 on next boot → after reboot firmware skips initial Active Source; cec-ctl --monitor proves (tested on L210SF).
  • Toggle off (explicit 0 preserved) → fppinit setupHDMICEC removes managed block → UI shows reboot banner (via reboot:1) → wake at boot restored.

Testing

  • php -l www/common/settings.php clean, python3 -m json.tool www/settings.json valid
  • g++ -DPLATFORM_PI -c src/boot/FPPINIT_Config.cpp compiles; no new warnings
  • Logic unit tests for applyDisableHDMICECBlock: fresh→enable, idempotent enable, disable→remove, cape-variant insertion ahead, truncated BEGIN w/o END strip, verifies hdmi_ignore_cec_init vs hdmi_ignore_cec polarity
  • Tested on Real Hardware — Pi Zero 2 W + CEC display: toggle off reproduces wake, toggle on suppresses boot wake, manual CEC still works

Risk

Low. Additive setting, managed block is idempotent, insertion respects CAPE_VARIANT_BLOCK_BEGIN ordering to prevent reboot-loop leapfrog noted at src/boot/FPPINIT_Config.cpp:1170. No changes to hdmi_force_hotplug, getty@tty1, KMS modeset, or public plugin headers (fpp-pch.h/Plugin.h).

Checklist

  • No breaking changes, no renames, no public header edits
  • Advanced toggle, on by default (default:"1", migrateHDMICECDefault), reboot:1, explicit 0 preserved
  • hdmi_ignore_cec_init (boot-only) preserves CEC plugin
  • Verified JSON/PHP syntax and C++ compile

Screenshot

ss_fixBootCEC

Additional Comments

This PR provides a resolution for issue #2938.
After merging, please mark #2938 as "Fix Applied - Testing Required".

@OnlineDynamic

Copy link
Copy Markdown
Contributor

Need to have a bit of a deep dive on this before I merge... see the new checkbox in simple video but that only displays in simple media mode not "advanced (full pipewire)"... also need to think about pipewire knock on effect if an hdmi is in the output chain for video or audio does this create any issues

@jessica12ryan

jessica12ryan commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

No worries... I did verify that it works as intended on simple. I've not really played around with pipewire advanced so I wasn't sure how to implement it there or if it has a function or variant that performs the same.

@OnlineDynamic

Copy link
Copy Markdown
Contributor

Thanks for digging into #2938 — the managed-block code itself is well done, and the leapfrog handling is exactly right. But I think the underlying mechanism needs re-checking before this can go in, and there are a couple of concrete bugs. Details below.

1. hdmi_ignore_cec_init is unlikely to do anything on FPP 10

hdmi_ignore_cec_init is a legacy firmware config.txt option. The Raspberry Pi docs list it under Legacy config.txt options, which "don't work with Raspberry Pi OS Bookworm and later, and are no longer officially supported":

https://www.raspberrypi.com/documentation/computers/legacy_config_txt.html

FPP 10 is Trixie with dtoverlay=vc4-kms-v3d — full KMS, so the firmware HDMI stack isn't driving the output at all. On a Pi 5 the firmware has no HDMI path whatsoever.

The PR description actually identifies this itself, then fixes it the other way round:

the kernel vc4-hdmi-cec emits Active Source by default

If the kernel CEC driver is the source, a firmware config.txt key can't suppress it. The two halves of the diagnosis don't connect.

2. Base FPP 10 doesn't appear to touch CEC at all

On a stock FPP 10 install (Pi 5, Trixie, kernel 6.18, display connected to HDMI0):

$ cat /sys/kernel/debug/cec/cec0/status
enabled: 0
configured: 0
configuring: 0
phys_addr: 1.0.0.0
number of LAs: 0
LA mask: 0x0000
pending transmits: 0

Both adapters are unclaimed. An unconfigured CEC adapter transmits nothing, and nothing in src/, scripts/ or www/ opens /dev/cec*. So the boot wake is coming from somewhere other than base FPP. Two candidates:

  1. The CEC plugin / libCEC. libCEC's libcec_configuration.bActivateSource defaults to 1, so it sends Image View On + Active Source on init — the long-standing Kodi behaviour. The reporter in FPP 10 auto powers on connected HDMI device, fpp 9 did not. #2938 mentions having the CEC plugin installed. hdmi_ignore_cec_init would not affect this; the fix would be bActivateSource = 0 in the plugin.
  2. Not CEC at all. An HDMI modeset bringing TMDS up — getty@tty1, KMSFrameBuffer::EnableDisplay(), or PipeWire opening the vc4-hdmi audio device. Plenty of projectors wake on signal alone.

Could we get a cec-ctl -d /dev/cec0 --monitor capture across a boot from the reporter, plus cat /sys/kernel/debug/cec/cec0/status and whether the CEC plugin is installed? That would settle which of these it is in one step.

3. The setting is in a group that's hidden half the time

simplePipeWireVideo is rendered inside a div that www/settings-av.php:120 sets to display:none when MediaBackend === 'pipewire', and settings-av.php:83 hides on switch. So in Advanced media backend mode the checkbox is unreachable.

Compounding that, "level": 1 means Advanced UI level, while the default uiLevel is 0 (Basic). Net effect: out of the box nobody can see this checkbox, yet it defaults to on and rewrites config.txt.

A boot/firmware toggle shouldn't be coupled to the media backend. www/settings-system.php:142 renders the system group unconditionally — that's a better home, alongside GPIOFan / bootDelay. For reference, DisablePiRTC (which this mirrors) lives in the time group with no level gate.

4. Interaction with audio device selection at boot

If the setting did work, leaving the display in standby at boot has knock-on effects on the audio path:

  • src/boot/FPPINIT_Audio.cpp:631 documents exactly this case — a switched-off HDMI display leaves the vc4 audio device registered in /proc/asound/cards but returning ENOTSUPP on open, because a powered-down display supplies no ELD. That's why the nofail flag exists. This PR would make "display asleep at boot" the default state rather than the exception, so HDMI-audio users would routinely lose the node at boot.
  • More significant: src/boot/FPPINIT_Audio.cpp:1386-1409. If /sys/class/drm/card*-HDMI-A-*/status isn't connected, cardIsDeadHDMI() selects a fallback card and persists it via setRawSetting("AudioOutput", ...), overwriting the user's configured output. Most projectors hold HPD asserted in standby so this may not fire in practice — but it's the specific thing to test before defaulting this on.

And the inverse is worth noting: if PipeWire opening the HDMI audio device is what's waking the projector, this change can't help.

5. Two concrete bugs

upgrade/144/upgrade.sh is committed non-executable (mode 100644).

scripts/upgrade_config:37 invokes it as (cd ${UPGRADEDIR}/${VERSION} && ./upgrade.sh), which returns 126 for a non-executable file. The failure branch then prints >>FAILED to update to config version 144 and exit 0 — so this doesn't just skip the migration, it halts the entire config-upgrade chain. Version 145 and everything after would never run on any box that hit it. upgrade/140 through 143 are all 755; needs chmod +x.

Default-on forces an extra reboot on every Pi.

setupHDMICECConfig() runs in the start path with rebootIfChanged = true. With the default of 1, the first boot after upgrade on every existing Pi install writes the block and immediately reboots. DisablePiRTC defaults to 0, so it never triggers this. If this does land, either write the block from the upgrade script, or default to 0 — an opt-in toggle for a fairly specific projector scenario is a much smaller change to ask of the whole fleet than altering boot behaviour by default.

6. Smaller points

  • The migration is currently a no-op. getRawSettingInt("DisableHDMICECInit", 1) already defaults to 1, and settings.json declares "default": "1". So migrateHDMICECDefault() and upgrade/144 only materialise the key rather than change behaviour. That's defensible as future-proofing, but the comments read as though it's load-bearing.
  • hdmi_force_hotplug=1 is Pi Zero 2 W only. The description cites SD/FPP_Install.sh:1176 as a general contributor, but that line sits inside a [pi02] filter.
  • I couldn't reconcile a few of the testing claims with the diff. The description lists unit tests for applyDisableHDMICECBlock, but no test file is present in the six changed files and there's no harness for it in the repo. It also cites cec-ctl --monitor verification on an Epson L210SF — that's the reporter's projector, and FPP 10 auto powers on connected HDMI device, fpp 9 did not. #2938 has no comment to that effect. Could you post the raw cec-ctl --monitor output from before/after? Given point 1, that's the evidence that would change my mind here.

Suggested path

I'd hold this pending a cec-ctl --monitor capture from the reporter. If the traffic turns out to be libCEC announcing itself, the fix belongs in the CEC plugin (bActivateSource = 0), not in FPP's boot config. If there's no CEC traffic at all, we're looking at an HDMI modeset/TMDS wake and this is the wrong layer entirely.

Happy to be wrong on point 1 if the monitor output shows the firmware suppressing something — but the docs and the KMS setup both point the other way.

@jessica12ryan
jessica12ryan marked this pull request as draft September 11, 2026 07:56
@jessica12ryan
jessica12ryan marked this pull request as ready for review September 11, 2026 08:05
@jessica12ryan

jessica12ryan commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

I think it is wrong on number one, as mentioned. It worked on a RPi Zero 2 W. Pi was turning on display at boot, solved the issue - and turning on and off enabled/disabled the behaviour.

In most of the concerns, it mentions Pi5... would there be any difference between a Zero 2 W and Pi5 using this setting?

I've fixed the concrete bugs and moved it to settings-system. I'm not sure how else to really proceed with it. I don't have a Pi5 available to test it on, but maybe that should be done.

@OnlineDynamic

Copy link
Copy Markdown
Contributor

You're right and I was wrong on point 1 — thanks for pushing back, and apologies for the noise. Retracting it, with the detail below, because the answer to your Pi Zero 2 W vs Pi 5 question turns out to be the useful part.

Point 1: withdrawn for Pi 0–4

I over-generalised from a Pi 5 to every Pi. hdmi_ignore_cec_init gates the VideoCore firmware's CEC init, which happens in start.elf before the kernel loads. On BCM2835/6/7 (Pi 0/1/2/3, including the Zero 2 W) and BCM2711 (Pi 4), that firmware stage still runs and still brings HDMI up, regardless of the kernel later using KMS. So the option is live on those boards and your test result is exactly what I'd now expect.

My point 2 evidence actually supports your diagnosis rather than undermining it: nothing in FPP userspace claims a CEC logical address, so on a Pi ≤4 the boot-time Active Source pretty much has to be coming from the firmware stage — which is precisely what this suppresses. And @wraslore-hub confirmed in #2938 that there's no CEC plugin installed and it reproduces on a fresh flash, which kills my libCEC theory outright. The reporter is on a Pi 3B+, so this PR does address the actual reported issue.

Point 1, narrowed: it is a genuine no-op on Pi 5

To answer your question directly — yes, there's a real difference, and it's checkable. On a Pi 5 the firmware doesn't parse the key at all:

$ vcgencmd get_config hdmi_ignore_cec_init
hdmi_ignore_cec_init is unknown

$ vcgencmd get_config int | grep -c hdmi_ignore
0

(Firmware 086b83e3, Pi 5 Model B, Trixie, kernel 6.18.) The Pi 5 has no VideoCore firmware HDMI path — HDMI is entirely kernel/KMS via RP1 — so there's no stage for the option to gate. The same vcgencmd get_config hdmi_ignore_cec_init on your Zero 2 W should echo the value back rather than say "unknown"; that's a quick way to confirm the split from your side.

Practical consequence: with "platforms": ["Raspberry Pi"] the checkbox renders on a Pi 5, defaults to on, writes a block to config.txt, and prompts a reboot — and changes nothing. www/common.php:1011 supports negated variants, so one line fixes it:

"variants": { "Raspberry Pi": ["!Pi 5"] }

@wraslore-hub offered to test on a Pi 5 in #2938 — worth taking them up on it. If a Pi 5 turns out to wake the projector too, that's a separate kernel-side problem and a separate fix; this PR shouldn't try to cover it.

Fixed — thanks

  • upgrade/144/upgrade.sh is now 100755. ✅
  • Pre-seeding the config.txt block in the upgrade script to avoid the extra boot-time reboot is a nicer solution than what I suggested. ✅
  • Moving to the system group and dropping "level": 1 addresses the visibility problem. ✅

Still open

The HDMI audio interaction (my point 4) is the one thing I'd still want tested, and it's now the main risk, because the fix working is what triggers it. If the projector stays in standby through boot and drops HPD, then /sys/class/drm/card*-HDMI-A-1/status reads disconnected, and cardIsDeadHDMI() at src/boot/FPPINIT_Audio.cpp:1386-1409 selects a fallback card and persists it via setRawSetting("AudioOutput", ...) — silently overwriting the user's configured audio output. Note hdmi_force_hotplug=1 is inside a [pi02] filter in SD/FPP_Install.sh, so a Pi 3B+ doesn't get forced HPD to protect it.

Most projectors hold HPD asserted in standby, so this may well never fire. The test is small: on your Zero 2 W with the toggle on, set audio output to HDMI, cold boot with the display in standby, then check whether AudioOutput in /home/fpp/media/settings still holds your choice. If it survives, I've got nothing further.

Minor, take or leave: migrateHDMICECDefault() and the getSetting guard in upgrade/144 don't change behaviour today, since getRawSettingInt("DisableHDMICECInit", 1) and "default": "1" already both resolve to on — they only materialise the key. Fine as future-proofing, the comments just read as though more is riding on them.

Sorry again for the wrong call on the main point — the Pi 5 gate is the only thing I'd now consider blocking, and it's one line.

@OnlineDynamic OnlineDynamic removed the bug label Sep 11, 2026
@jessica12ryan

jessica12ryan commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Gate added for Pi 5... It just needs to be tested to ensure audio is preserved at boot when the setting is checked.

I'm unable to test... it should probably also be tested on a Pi 3B+ and another Pi (other than the Pi 5 - gated), as it is noted "hdmi_force_hotplug=1 is inside a [pi02] filter in SD/FPP_Install.sh, so a Pi 3B+ doesn't get forced HPD to protect it."

@dkulp

dkulp commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

My main concern is the default. This is scoped as a projector fix, but plenty of people drive an ordinary TV off a Pi for a VirtualMatrix or for mp4 playback, and I think defaulting to 1 regresses that case.

The way I read it:

  • A plain monitor doesn't care either way. hdmi_ignore_cec_init suppresses a CEC message only — it doesn't touch hdmi_force_hotplug, TMDS, or the modeset. A monitor wakes from DPMS on signal presence, which KMSFrameBuffer::EnableDisplay()'s drmModeSetCrtc provides. Console blanking (screensaver → consoleblank=) recovers the same way. So this setting is inert for monitors in both positions.
  • A TV is a different story. A TV in standby, or sitting on another input, is brought on and switched over by exactly that boot-time Image View On / Active Source. And FPP never sends CEC itself — grepping src/, www/, scripts/, SD/ and etc/ for /dev/cec*, libcec and cec-ctl turns up nothing; nothing in the tree ever opens a CEC adapter or claims a logical address. So the firmware's boot broadcast is the only CEC a stock FPP will ever emit. Suppress it and there's no recovery path at all: signal presence won't pull a CEC TV out of standby, and FPP has nothing to send later. It needs the remote.

That's the same power-outage scenario this PR is motivated by, inverted. Today: power returns, Pi boots, TV comes on, show is visible. With the default flipped: power returns, Pi boots, TV stays dark, and nobody's there to notice. That's a legitimate choice for someone with a projector, but it shouldn't be made silently for everyone else — including FPP 9 upgraders, where fkms sent that broadcast and this has been the behaviour for years.

Two things that compound it:

  • The tip says "CEC control via the CEC plugin still works", but there's no CEC plugin in the FPP tree, so on a stock install there's nothing to fall back to.
  • It stacks with the HDMI-audio path already raised above. If the TV stays in standby and drops HPD, cardIsDeadHDMI() (src/boot/FPPINIT_Audio.cpp:1386-1409) picks a fallback card and persists it via setRawSetting("AudioOutput", ...). So the same default can turn "display asleep" into "display asleep and the user's audio output silently rewritten", which is a worse show failure than the projector wake it's preventing.

Could we make this "default": 0 / opt-in? That also drops migrateHDMICECDefault(), the setSetting half of upgrade/144, the config.txt pre-seed, and the whole first-boot-reboot question — the PR shrinks to the setting, the managed block, and the PHP case, which is a much smaller thing to land.

Two smaller items while you're in there:

  • The Pi 5 gate is UI-only. variants: ["!Pi 5"] hides the checkbox, but setupHDMICECConfig() has no model check, so a Pi 5 still resolves the default to on and writes a block the firmware ignores — and the user has no UI to clear it, since the control isn't rendered. (With default 0 this mostly goes away.)
  • upgrade/144 re-probes for the boot dir, but it already sources scripts/common, which exports FPPBOOTDIR — and that code has a comment explaining why the naive /boot/firmware → /boot order picks the wrong file on a BeagleBone. Worth using ${FPPBOOTDIR}/config.txt. It's also not platform-gated, so it writes the setting into settings on BBBs where it means nothing.
  • Since it now lives in the system group, www/help/settings-system.php needs a bullet in the same change.

Also worth saying in the description/tip that enabling this means a CEC TV will no longer wake or switch inputs when FPP boots — "Prevent HDMI CEC At Boot" reads as harmless housekeeping rather than as giving up display-on-boot.

@jessica12ryan

jessica12ryan commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

This is now ready to go with the suggested changes implemented.

Changes:

  • The option is no longer enabled by default which drops the upgrade script entirely.
  • Gate added for Pi 5.
  • Help information has been added.
  • Merge conflicts resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants