Photosensitivity guard: no scheme, AI suggestion or remote push can put a seizure-risk strobe on the house #150
Labels
No labels
area/ai
area/backend
area/frontend
area/infra
area/scheduler
area/wled
good-first-issue
priority/high
priority/low
priority/medium
security
type/bug
type/chore
type/ci-cd
type/docs
type/feature
type/qa
v1.0.0
v1.1.0
v1.2.0
v2.0.0
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/Iris-WLED#150
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
A house's lights are seen by everyone who passes: neighbours, drivers, children. Iris makes no effort to keep flashing within safe limits, and three paths can put a strobe there. All were verified against
main.services/ai/suggestion_service.pyvalidates JSON shape only._SegmentInand_SchemeInput no bounds oneffect_idor speed. The only guard is prompt text (prompt_builder.py: "Avoid … strobes" for memorial schemes), andFALLBACK_EFFECTSlists safe effects but nothing enforces them. A model can return effect 23 (Strobe) at speed 255 and it will validate./remote(#24):QuickPushRequest.effect_idisge=0and nothing more. It isn't even checked against the controller's effect list.Flash-like WLED effects (IDs checked against the 16.0.0 capture in
frontend/src/components/panel/effectSim.ts):strobefamily: 1 Blink, 23 Strobe, 24 Strobe Rainbow, 25 Strobe Mega, 26 Blink Rainbow, 57 Lightning.sparkleeffects are single-frame flashes too, and are rate-dependent.The standard
WCAG 2.3.1, and the ITC/Ofcom broadcast guidance it derives from: no more than three general flashes in any one-second period. Saturated red flashing is the higher-risk case. The guidance was written for screens; a roofline filling a viewer's field of view at night is arguably worse, not better.
Design
One chokepoint: the translator. Enforce in
scheme_translator.py, where every scheme becomes a WLED payload, so that every path is covered by construction: scheduled pushes, quick push,/remote, preview and preset sync. Validation at the individual entry points exists only to give good errors.A setting
photosensitive_safe, on by default. Runtime-settable, with a clear explanation in Settings of what turning it off means.Flash-rate model per effect. WLED effect speed maps to a flash rate differently for each effect.
transitionwrap on the real controller.sxvalues (a phone at 240 fps is enough) and record the flash frequency.sx.Behaviour when a scheme exceeds the limit:
sxto the safe maximum for that effect. If the effect is unsafe at every speed, substitute its non-flashing family neighbour (Strobe → Breathe, Lightning → Twinkle). Log it, and record the substitution in the push result./remote: unsafe effects are hidden from the picker when the guard is on, and the API clamps them the same way.Also bound
effect_idagainst the controller's live effect count (fxcountfromget_info, #62) in the API, so an out-of-range ID is a 422 and not whatever the firmware does with it.Acceptance
/remote, preset sync./remoteshow the effective behaviour.effect_idis bounded byfxcount.Picking this up, in parallel with #149.
Planned deviation from the acceptance list: the flash-rate table can't be filmed on the roofline from here. It will be derived from WLED 16.0.0's firmware source instead: each flash-family effect's timing as a function of
sxinFX.cpp. Every row will cite the formula it came from, and the limits will be set conservatively. The on-hardware filming check stays open as a follow-up acceptance item. It isn't a blocker, because a limit derived from the source is still far safer than no limit.Done in #167 (squash-merged, CI green).
Delivered:
FX.cpp, commit4374f01, with every row citing its function and formula.day_plan, which now carriesflash_substitutions), the no-event and unapproved defaults, Push now, quick push and/remote, preview, preset sync and export, and aurora./remote: they warn about or hide effects the guard would change.effect_idis bounded byfxcount.photosensitive_safesetting, defaulting to on.Limits: Blink, Strobe and their rainbow variants are clamped at
sx238, and Dynamic at 247. Strobe Mega, Lightning, Chase Flash (and its random variant) and Sparkle+ are unsafe at every speed and are replaced.Deviation, as planned: the hardware filming check moved to #168, and the table carries
verified_on_hardware: falseuntil then.Follow-up: #169, the validation gaps. "Push now" doesn't use the day plan, segment fields are unbounded, the import paths don't bound
effect_id, and the AI doesn't get the live effect list.