ffee156c17
Cap an addressable strip's estimated current draw to a PSU budget so bright/ white scenes can't brown out an under-spec'd supply (voltage sag -> red/orange shift, flicker, controller resets) — a classic 'it's broken' first impression. - New core/processing/power_limit.py: pure current estimate (full white over N LEDs draws N * mA_per_led) + a (0,1] scale to land a frame on budget. - Applied in WledTargetProcessor._send_to_device (single choke point, every send path; scales into a reusable scratch buffer, never mutates shared frames). - Two per-target fields on LED targets: max_milliamps (0 = unlimited) and milliamps_per_led (default 55), threaded through model/store/manager/processor/ schema/route with hot-update via update_target_settings. Additive with safe defaults (no data migration needed; legacy targets read as unlimited). - Frontend: editor fields + i18n (en/ru/zh) + LedOutputTarget type. - Tests: 10 unit tests for the estimator/scale; full suite green (1911 passed).
71 lines
2.3 KiB
Python
71 lines
2.3 KiB
Python
"""Unit tests for automatic brightness limiting (ABL) current estimation."""
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from ledgrab.core.processing.power_limit import (
|
|
DEFAULT_MILLIAMPS_PER_LED,
|
|
estimate_current_ma,
|
|
power_limit_scale,
|
|
)
|
|
|
|
|
|
def test_default_ma_per_led_constant():
|
|
assert DEFAULT_MILLIAMPS_PER_LED == 55
|
|
|
|
|
|
def test_full_white_draws_ma_per_led_times_count():
|
|
colors = np.full((100, 3), 255, dtype=np.uint8)
|
|
assert estimate_current_ma(colors, 55) == pytest.approx(100 * 55)
|
|
|
|
|
|
def test_black_draws_zero():
|
|
colors = np.zeros((100, 3), dtype=np.uint8)
|
|
assert estimate_current_ma(colors, 55) == 0.0
|
|
|
|
|
|
def test_half_white_is_half_current():
|
|
full = estimate_current_ma(np.full((100, 3), 255, dtype=np.uint8), 55)
|
|
half = estimate_current_ma(np.full((100, 3), 128, dtype=np.uint8), 55)
|
|
assert half == pytest.approx(full * 128 / 255, rel=1e-6)
|
|
|
|
|
|
def test_zero_ma_per_led_draws_zero():
|
|
colors = np.full((100, 3), 255, dtype=np.uint8)
|
|
assert estimate_current_ma(colors, 0) == 0.0
|
|
|
|
|
|
def test_empty_frame_is_safe():
|
|
colors = np.zeros((0, 3), dtype=np.uint8)
|
|
assert estimate_current_ma(colors, 55) == 0.0
|
|
assert power_limit_scale(colors, 1000, 55) == 1.0
|
|
|
|
|
|
def test_scale_is_one_when_disabled():
|
|
colors = np.full((100, 3), 255, dtype=np.uint8)
|
|
assert power_limit_scale(colors, 0, 55) == 1.0
|
|
assert power_limit_scale(colors, -1, 55) == 1.0
|
|
|
|
|
|
def test_scale_is_one_within_budget():
|
|
colors = np.full((100, 3), 255, dtype=np.uint8) # 5500 mA at 55 mA/LED
|
|
assert power_limit_scale(colors, 6000, 55) == 1.0
|
|
assert power_limit_scale(colors, 5500, 55) == 1.0 # exactly on budget
|
|
|
|
|
|
def test_scale_brings_full_white_to_budget():
|
|
colors = np.full((100, 3), 255, dtype=np.uint8) # 5500 mA
|
|
scale = power_limit_scale(colors, 2750, 55) # half budget
|
|
assert scale == pytest.approx(0.5, rel=1e-6)
|
|
|
|
|
|
def test_applying_scale_lands_within_budget():
|
|
colors = np.full((100, 3), 255, dtype=np.uint8) # 5500 mA
|
|
budget = 2750
|
|
scale = power_limit_scale(colors, budget, 55)
|
|
# Mirror the processor's fixed-point application (factor/256).
|
|
factor = int(scale * 256)
|
|
scaled = ((colors.astype(np.uint16) * factor) >> 8).astype(np.uint8)
|
|
# Fixed-point rounding can only ever round DOWN, so we never exceed budget.
|
|
assert estimate_current_ma(scaled, 55) <= budget
|