Photosensitivity guard: no scheme, AI suggestion or remote push can put a seizure-risk strobe on the house #150

Closed
opened 2026-09-26 05:01:07 +00:00 by claude-bot · 2 comments
Collaborator

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.

  • AI generation and refinement: services/ai/suggestion_service.py validates JSON shape only. _SegmentIn and _SchemeIn put no bounds on effect_id or speed. The only guard is prompt text (prompt_builder.py: "Avoid … strobes" for memorial schemes), and FALLBACK_EFFECTS lists safe effects but nothing enforces them. A model can return effect 23 (Strobe) at speed 255 and it will validate.
  • Quick push and /remote (#24): QuickPushRequest.effect_id is ge=0 and nothing more. It isn't even checked against the controller's effect list.
  • The scheme editor: any effect and any speed.

Flash-like WLED effects (IDs checked against the 16.0.0 capture in frontend/src/components/panel/effectSim.ts):

  • The strobe family: 1 Blink, 23 Strobe, 24 Strobe Rainbow, 25 Strobe Mega, 26 Blink Rainbow, 57 Lightning.
  • Also 31 Chase Flash and 32 Chase Flash Rnd, currently unmapped.
  • Several sparkle effects 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.

  • This must be measured, not guessed, the way #21 measured transition wrap on the real controller.
  • Film each flash-family effect at a few sx values (a phone at 240 fps is enough) and record the flash frequency.
  • Store that as a table beside the effect-ID fixture: effect → the maximum safe sx.

Behaviour when a scheme exceeds the limit:

  • Push path: clamp sx to 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.
  • Editor: a warning on the effect picker and speed slider ("will be slowed to ~3 flashes/s on the house"), and the preview (#14) shows what will actually be pushed.
  • AI: reject and repair in the existing one-shot retry, telling the model why. The fallback catalogue offered to the model excludes unsafe effects.
  • Quick push / /remote: unsafe effects are hidden from the picker when the guard is on, and the API clamps them the same way.

Also bound effect_id against the controller's live effect count (fxcount from get_info, #62) in the API, so an out-of-range ID is a 422 and not whatever the firmware does with it.

Acceptance

  • Measured flash-rate table for every flash-family effect on the 16.0.0 controller, with the method written down.
  • Guard on: no path can push a strobe-family effect above the safe rate. There is a translator-level test for each path: scheduled, quick push, /remote, preset sync.
  • AI output with an unsafe effect is repaired or rejected, never saved as-is.
  • Editor and /remote show the effective behaviour.
  • effect_id is bounded by fxcount.
  • Docs explain the guard and what disabling it means. CHANGELOG entry.
## 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`. - **AI generation and refinement:** `services/ai/suggestion_service.py` validates JSON shape only. `_SegmentIn` and `_SchemeIn` put no bounds on `effect_id` or speed. The only guard is prompt text (`prompt_builder.py`: "Avoid … strobes" for memorial schemes), and `FALLBACK_EFFECTS` lists safe effects but nothing enforces them. A model can return effect 23 (Strobe) at speed 255 and it will validate. - **Quick push and `/remote` (#24):** `QuickPushRequest.effect_id` is `ge=0` and nothing more. It isn't even checked against the controller's effect list. - **The scheme editor:** any effect and any speed. **Flash-like WLED effects** (IDs checked against the 16.0.0 capture in `frontend/src/components/panel/effectSim.ts`): - The `strobe` family: 1 Blink, 23 Strobe, 24 Strobe Rainbow, 25 Strobe Mega, 26 Blink Rainbow, 57 Lightning. - Also 31 Chase Flash and 32 Chase Flash Rnd, currently unmapped. - Several `sparkle` effects 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. - **This must be measured, not guessed**, the way #21 measured `transition` wrap on the real controller. - Film each flash-family effect at a few `sx` values (a phone at 240 fps is enough) and record the flash frequency. - Store that as a table beside the effect-ID fixture: effect → the maximum safe `sx`. **Behaviour when a scheme exceeds the limit:** - **Push path:** clamp `sx` to 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. - **Editor:** a warning on the effect picker and speed slider ("will be slowed to ~3 flashes/s on the house"), and the preview (#14) shows what will *actually* be pushed. - **AI:** reject and repair in the existing one-shot retry, telling the model why. The fallback catalogue offered to the model excludes unsafe effects. - **Quick push / `/remote`:** unsafe effects are hidden from the picker when the guard is on, and the API clamps them the same way. **Also bound `effect_id`** against the controller's live effect count (`fxcount` from `get_info`, #62) in the API, so an out-of-range ID is a 422 and not whatever the firmware does with it. ## Acceptance - [ ] Measured flash-rate table for every flash-family effect on the 16.0.0 controller, with the method written down. - [ ] Guard on: no path can push a strobe-family effect above the safe rate. There is a translator-level test for each path: scheduled, quick push, `/remote`, preset sync. - [ ] AI output with an unsafe effect is repaired or rejected, never saved as-is. - [ ] Editor and `/remote` show the effective behaviour. - [ ] `effect_id` is bounded by `fxcount`. - [ ] Docs explain the guard and what disabling it means. CHANGELOG entry.
claude-bot added this to the v1.2.0 milestone 2026-09-26 05:02:36 +00:00
Author
Collaborator

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 sx in FX.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.

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 `sx` in `FX.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.
Author
Collaborator

Done in #167 (squash-merged, CI green).

Delivered:

  • Source-derived flash table: WLED v16.0.0 FX.cpp, commit 4374f01, with every row citing its function and formula.
  • The guard sits at the translator chokepoint and is on by default. It covers the on job (via day_plan, which now carries flash_substitutions), the no-event and unapproved defaults, Push now, quick push and /remote, preview, preset sync and export, and aurora.
  • AI: an unsafe effect fails validation and is repaired if the retry is still unsafe.
  • Editor and /remote: they warn about or hide effects the guard would change.
  • effect_id is bounded by fxcount.
  • A photosensitive_safe setting, defaulting to on.

Limits: Blink, Strobe and their rainbow variants are clamped at sx 238, 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: false until 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.

Done in #167 (squash-merged, CI green). **Delivered:** - **Source-derived flash table:** WLED v16.0.0 `FX.cpp`, commit `4374f01`, with every row citing its function and formula. - **The guard sits at the translator chokepoint and is on by default.** It covers the on job (via `day_plan`, which now carries `flash_substitutions`), the no-event and unapproved defaults, Push now, quick push and `/remote`, preview, preset sync and export, and aurora. - **AI:** an unsafe effect fails validation and is repaired if the retry is still unsafe. - **Editor and `/remote`:** they warn about or hide effects the guard would change. - **`effect_id` is bounded by `fxcount`.** - **A `photosensitive_safe` setting,** defaulting to on. **Limits:** Blink, Strobe and their rainbow variants are clamped at `sx` 238, 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: false` until 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.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Iris-WLED#150
No description provided.