mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-13 02:26:37 +00:00
The ladder ended by telling users to set display_options.scroll_pixels_per_second. scroll_config ranks that key below the scroll_speed + scroll_delay pair, deliberately, and several plugin schemas default the pair into config, so the advised key was silently ignored (a schema-default 1/0.02 pair plus an advised 66 still resolved to 50 px/s). The advice is now the pair that selects the crisp speed exactly (pixels_per_frame every frame_hold/refresh seconds), explains that the pair outranks scroll_pixels_per_second, and gives the scoreboards' per-league scroll_settings.scroll_speed (px/s) form. Tests resolve the printed pair over a schema-default pair and check it lands on the advertised speed and hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
145 lines
5.4 KiB
Python
145 lines
5.4 KiB
Python
"""scripts/scroll_speeds.py must drive the panel the way the service does.
|
|
|
|
--measure and --demo open the matrix themselves. They used to build the
|
|
options from a private copy of DisplayManager's builder that had drifted: it
|
|
read gpio_slowdown from display.hardware (the service reads display.runtime),
|
|
and skipped rp1_rio, panel_type, orientation and more. On a panel that needs a
|
|
high slowdown that measured -- or garbled -- a panel the service never drives.
|
|
"""
|
|
import importlib.util
|
|
import os
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
os.environ.setdefault("EMULATOR", "true")
|
|
|
|
import src.display_manager as display_manager # noqa: E402
|
|
from src.display_manager import DisplayManager # noqa: E402
|
|
|
|
SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "scroll_speeds.py"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def script():
|
|
spec = importlib.util.spec_from_file_location("scroll_speeds_script", SCRIPT)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
class _Options:
|
|
"""Records what is set; declares rp1_rio like a Pi 5-capable binding."""
|
|
|
|
rp1_rio = None
|
|
|
|
|
|
CONFIG = {
|
|
"display": {
|
|
"hardware": {
|
|
"rows": 64, "cols": 64, "chain_length": 2, "parallel": 1,
|
|
"hardware_mapping": "adafruit-hat-pwm",
|
|
"brightness": 70, "pwm_bits": 9, "pwm_lsb_nanoseconds": 130,
|
|
"row_address_type": 5, "panel_type": "FM6126A",
|
|
"limit_refresh_rate_hz": 120, "orientation": "180",
|
|
"pixel_mapper_config": "U-mapper",
|
|
"gpio_slowdown": 2, # a stale key in the wrong block
|
|
},
|
|
"runtime": {"gpio_slowdown": 7, "rp1_rio": 1},
|
|
}
|
|
}
|
|
|
|
|
|
def _script_options(script, config, **kwargs):
|
|
with patch.object(display_manager, "RGBMatrixOptions", _Options):
|
|
return script.build_options(config, **kwargs)
|
|
|
|
|
|
def test_runtime_settings_are_used(script):
|
|
options = _script_options(script, CONFIG)
|
|
assert options.gpio_slowdown == 7
|
|
assert options.rp1_rio == 1
|
|
assert options.panel_type == "FM6126A"
|
|
assert options.row_address_type == 5
|
|
assert options.pixel_mapper_config == "U-mapper;Rotate:180"
|
|
assert options.limit_refresh_rate_hz == 120
|
|
|
|
|
|
def test_the_options_match_the_display_service(script):
|
|
"""Same config in, same options out, attribute for attribute."""
|
|
service = DisplayManager.apply_matrix_options(_Options(), CONFIG)
|
|
assert vars(_script_options(script, CONFIG)) == vars(service)
|
|
|
|
|
|
def test_measure_runs_uncapped(script):
|
|
assert _script_options(script, CONFIG, refresh_override=0).limit_refresh_rate_hz == 0
|
|
|
|
|
|
def test_an_empty_config_gets_the_service_defaults(script):
|
|
options = _script_options(script, {})
|
|
assert vars(options) == vars(DisplayManager.apply_matrix_options(_Options(), {}))
|
|
assert options.gpio_slowdown == 3
|
|
|
|
|
|
def test_the_service_uses_the_shared_builder(test_config):
|
|
"""DisplayManager._setup_matrix fills its options through the same call."""
|
|
with patch.object(display_manager, "RGBMatrix"), \
|
|
patch.object(display_manager, "RGBMatrixOptions", _Options), \
|
|
patch.object(display_manager, "freetype"), \
|
|
patch.object(DisplayManager, "apply_matrix_options",
|
|
wraps=DisplayManager.apply_matrix_options) as shared, \
|
|
patch.dict(os.environ, {"EMULATOR": "false"}):
|
|
DisplayManager._instance = None
|
|
try:
|
|
DisplayManager(test_config)
|
|
finally:
|
|
DisplayManager._instance = None
|
|
shared.assert_called_once()
|
|
|
|
|
|
class TestSpeedAdvice:
|
|
"""The advice must name keys the resolver actually honours."""
|
|
|
|
def _advice(self, script, capsys, hz, want):
|
|
script.print_ladder(hz, want)
|
|
return capsys.readouterr().out
|
|
|
|
def test_advice_is_the_speed_delay_pair(self, script, capsys):
|
|
out = self._advice(script, capsys, 100.0, 60)
|
|
assert '"scroll_speed": 2' in out
|
|
assert '"scroll_delay": 0.03' in out
|
|
|
|
@pytest.mark.parametrize("hz,want", [(100.0, 60), (100.0, 50), (120.0, 45),
|
|
(60.0, 30), (100.0, None)])
|
|
def test_the_advised_pair_resolves_to_the_advised_speed(self, script, capsys,
|
|
hz, want):
|
|
"""Apply the printed pair over a schema-default pair, as a saved config
|
|
would carry it, and the resolver must land on the advertised speed."""
|
|
import json
|
|
import re
|
|
|
|
from src.common import scroll_config
|
|
|
|
out = self._advice(script, capsys, hz, want)
|
|
block = re.search(r'"display_options": (\{[^}]*\})', out)
|
|
assert block, out
|
|
advised = json.loads(block.group(1))
|
|
config = {"display_options": {"scroll_speed": 1.0, "scroll_delay": 0.02,
|
|
"scroll_pixels_per_second": 999,
|
|
**advised}}
|
|
expected = scroll_config.solve_crisp(want if want else hz / 2, hz)
|
|
settings = scroll_config.configure(
|
|
_Helper(), plugin_config=config, refresh_hz=hz)
|
|
assert settings.pixels_per_second == pytest.approx(expected.pixels_per_second)
|
|
assert settings.frame_hold == expected.frame_hold
|
|
|
|
def test_the_deprecated_key_is_not_recommended(self, script, capsys):
|
|
out = self._advice(script, capsys, 100.0, 60)
|
|
assert '"scroll_pixels_per_second":' not in out
|
|
|
|
|
|
class _Helper:
|
|
def set_scroll_speed(self, speed):
|
|
pass
|