mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
Scroll speeds are solved against display.hardware.limit_refresh_rate_hz,
which is only a ceiling. A panel that cannot reach it still moves whole
pixels per frame, but every scroll runs slow by the shortfall and the
"smooth" ladder is the cap's, not the panel's. A user rig (Pi 4, 2x128x64,
adafruit-hat-pwm, pwm_bits 9, gpio_slowdown 5) measured 107.6-113.1 Hz under
a 120 Hz cap: 60 px/s ran at 55, and nothing said why.
- scroll_config: refresh_shortfall() (more than 3% under the planned rate),
holdable_cap() (a multiple of 10, 5% under the measurement, since the
measurement is the fast end of an uncapped panel's drift), and
describe_refresh_shortfall().
- FrameTimingRecorder.plan_refresh(): once the measured period has held for
three trusted windows, a shortfall is logged once as a warning naming the
cap to use. DisplayManager calls it only for a real panel, not the
emulator or the fallback canvas. The stats file records
planned_refresh_hz (additive).
- GET /api/v3/config/refresh-rate, plus a hint under the Display tab's
Limit Refresh Rate field with a button that fills in the suggested cap.
- _panel_refresh_hz (behind the Vegas slider's advice) ignores a measurement
written under a different cap, so a changed cap stops being advised from
the old rate before the display restarts.
Verified on ledpi with a temporary 200 Hz cap: the warning logged about a
minute after the restart ("about 132 Hz ... Set Limit Refresh Rate to
120 Hz"), the endpoint returned the same shortfall, and the Display tab
showed the hint; its button filled in 120. ledpi was restored afterwards.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
64 lines
2.1 KiB
Python
64 lines
2.1 KiB
Python
"""GET /api/v3/config/refresh-rate: the cap, the measured rate, a cap to hold."""
|
|
import json
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
from flask import Flask
|
|
|
|
from web_interface.blueprints.api_v3 import api_v3
|
|
|
|
|
|
@pytest.fixture
|
|
def client(monkeypatch, tmp_path):
|
|
stats = tmp_path / "stats.json"
|
|
monkeypatch.setattr("src.common.frame_timing.default_stats_path", lambda: str(stats))
|
|
manager = MagicMock()
|
|
manager.load_config.return_value = {
|
|
"display": {"hardware": {"limit_refresh_rate_hz": 120}}}
|
|
monkeypatch.setattr(api_v3, "config_manager", manager, raising=False)
|
|
app = Flask(__name__)
|
|
app.register_blueprint(api_v3, url_prefix="/api/v3")
|
|
c = app.test_client()
|
|
c.stats_path = stats
|
|
return c
|
|
|
|
|
|
def _get(client):
|
|
body = client.get("/api/v3/config/refresh-rate").get_json()
|
|
assert body["status"] == "success"
|
|
return body["data"]
|
|
|
|
|
|
def test_nothing_measured_yet(client):
|
|
data = _get(client)
|
|
assert data == {"planned_hz": 120.0, "measured_hz": None, "shortfall": None}
|
|
|
|
|
|
def test_a_panel_short_of_its_cap_gets_a_cap_it_can_hold(client):
|
|
client.stats_path.write_text(json.dumps(
|
|
{"measured_refresh_hz": 110.4, "planned_refresh_hz": 120.0}))
|
|
data = _get(client)
|
|
assert data["measured_hz"] == 110.4
|
|
assert data["shortfall"]["suggested_cap_hz"] == 100
|
|
assert data["shortfall"]["slow_percent"] == 8
|
|
|
|
|
|
def test_a_panel_at_its_cap_has_no_shortfall(client):
|
|
client.stats_path.write_text(json.dumps(
|
|
{"measured_refresh_hz": 121.3, "planned_refresh_hz": 120.0}))
|
|
assert _get(client)["shortfall"] is None
|
|
|
|
|
|
def test_a_file_written_under_another_cap_is_stale(client):
|
|
# The cap was changed to 120 but the display still runs under 100 Hz.
|
|
client.stats_path.write_text(json.dumps(
|
|
{"measured_refresh_hz": 99.9, "planned_refresh_hz": 100.0}))
|
|
data = _get(client)
|
|
assert data["measured_hz"] is None
|
|
assert data["shortfall"] is None
|
|
|
|
|
|
def test_a_file_from_a_display_too_old_to_record_its_cap_still_counts(client):
|
|
client.stats_path.write_text(json.dumps({"measured_refresh_hz": 110.4}))
|
|
assert _get(client)["shortfall"]["suggested_cap_hz"] == 100
|