feat(scroll): report a panel that cannot reach its refresh cap, and suggest one it can hold

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>
This commit is contained in:
Chuck
2026-10-04 17:11:09 -04:00
co-authored by Claude Opus 5.5
parent a74b5a2f0f
commit e39dcb18b4
11 changed files with 390 additions and 4 deletions
+31 -3
View File
@@ -158,11 +158,16 @@ def _panel_refresh_hz(config):
cap = scroll_config.refresh_hz_from_config(config)
try:
with open(frame_timing.default_stats_path(), encoding='utf-8') as fh:
measured = float(json.load(fh).get('measured_refresh_hz') or 0)
stats = json.load(fh)
measured = float(stats.get('measured_refresh_hz') or 0)
planned = float(stats.get('planned_refresh_hz') or 0)
except (OSError, ValueError, TypeError, AttributeError):
measured = planned = 0.0
# Reject a stale file from a previous hardware config: one written under
# another cap (the display has not restarted since it changed), or, from
# a display too old to record its cap, a measurement far off this one.
if planned and abs(planned - cap) > 0.5:
measured = 0.0
# Reject a stale file from a previous hardware config: a measurement far
# off the cap says the config changed since it was written.
if measured > 0 and 0.5 * cap <= measured <= 1.5 * cap:
return measured, 'measured'
return cap, 'configured'
@@ -189,6 +194,29 @@ def get_scroll_speed_advice():
return jsonify({'status': 'success', 'data': advice})
@api_v3.route('/config/refresh-rate', methods=['GET'])
def get_refresh_rate():
"""The refresh cap, what the panel measured, and a cap it can hold.
Backs the hint under the Display tab's Limit Refresh Rate field. Scroll
speeds are solved against the cap, so a panel that cannot reach it runs
every scroll slow; ``shortfall`` (None when the panel keeps up, or nothing
has been measured yet) says by how much and suggests a cap.
"""
from src.common import scroll_config
if not api_v3.config_manager:
return jsonify({'status': 'error', 'message': 'Config manager not initialized'}), 500
config = api_v3.config_manager.load_config()
planned = scroll_config.refresh_hz_from_config(config)
hz, source = _panel_refresh_hz(config)
measured = hz if source == 'measured' else None
return jsonify({'status': 'success', 'data': {
'planned_hz': planned,
'measured_hz': round(measured, 1) if measured else None,
'shortfall': scroll_config.refresh_shortfall(measured, planned),
}})
@api_v3.route('/config/schedule', methods=['GET'])
def get_schedule_config():
"""Get current schedule configuration"""