mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-13 10:36:35 +00:00
fix(display): refuse settings the rgbmatrix library aborts on, on every board
The library answers several settings with a NULL matrix or abort() rather than an error, so the display service crash-looped (Restart=on-failure) instead of reaching fallback mode: rows above 64, chain_length above 255 (uint8_t binding setter, documented as "no upper limit"), a misspelled hardware_mapping, and parallel 2-3 on a single-output mapping, reachable from the Display form on the default adafruit-hat(-pwm) mapping. #586 only guarded the Pi 5 subset. - src/matrix_support.py holds the rules for every board (Options::Validate ranges, binding integer types, mapping names and outputs from lib/hardware-mapping.c) plus the Pi 5 ones, and is the one source of the API's numeric ranges. - DisplayManager checks them before building options and raises MatrixSettingsRefused, so a hand-edited config falls back with a logged, reported reason. Emulator mode only warns. - The config API refuses them with a 400 naming the setting; combinations are checked against stored values but reported only when the request sets a field involved. - The hardware status file gains "cause" (settings/library/forced). The fallback log and Display banner give the Pi 5 rebuild hint only for a library failure instead of rebuild + gpio_slowdown advice for every failure; one Pi 5 slowdown recommendation (1-3, start at 1). - The Display form offers classic/classic-pi1 and orientation 90/270 and renders any other stored mapping selected with a warning, so an unrelated save no longer rewrites them; the API accepts 90/270. Audit findings F03, F16, F19, F21. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,9 @@ from web_interface.blueprints.api_v3 import (
|
||||
success_response,
|
||||
)
|
||||
from src.common.path_safety import resolve_under
|
||||
from src.pi5_matrix_support import is_raspberry_pi_5, pi5_unsupported_settings
|
||||
from src.display_geometry import ORIENTATION_ROTATE_DEGREES
|
||||
from src.matrix_support import INT_SETTING_LIMITS, describe_range, library_refusals, refusal_message
|
||||
from src.pi5_matrix_support import is_raspberry_pi_5
|
||||
import web_interface.blueprints.api_v3 as _pkg
|
||||
# Read through the module rather than bound by value: tests patch these
|
||||
# as module attributes, and a value binding would not see the patch.
|
||||
@@ -561,28 +563,20 @@ def save_main_config():
|
||||
return jsonify({'status': 'error', 'message': 'pixel_mapper_config must be a string (e.g. "U-mapper;Rotate:90" or empty)'}), 400
|
||||
|
||||
# Validate orientation (physical mounting rotation; composed onto pixel_mapper_config at runtime)
|
||||
ORIENTATION_ALLOWED = {'normal', '180'}
|
||||
ORIENTATION_ALLOWED = set(ORIENTATION_ROTATE_DEGREES)
|
||||
if 'orientation' in data and data['orientation'] not in ORIENTATION_ALLOWED:
|
||||
return jsonify({'status': 'error', 'message': f"Invalid orientation '{data['orientation']}'. Allowed values: {', '.join(sorted(ORIENTATION_ALLOWED))}"}), 400
|
||||
|
||||
# Panel geometry, PWM and GPIO timing, held to what the rgbmatrix library
|
||||
# accepts (RGBMatrix::Options::Validate in lib/options-initialize.cc,
|
||||
# the gpio_slowdown check in lib/led-matrix.cc). Outside those ranges
|
||||
# the config used to save, then the matrix refused to start and the
|
||||
# display dropped to fallback mode. cols, chain_length and
|
||||
# limit_refresh_rate_hz (0 = no cap) have no upper bound in the
|
||||
# library. rows has none here by choice: the library currently
|
||||
# rejects more than 64 per panel, and that limit is left to it so a
|
||||
# library that lifts it needs no change here.
|
||||
def _hardware_int_error(field, low, high=None, even=False):
|
||||
# Panel geometry, PWM and GPIO timing, held to what the rgbmatrix
|
||||
# library and its Python binding accept (src/matrix_support.py:
|
||||
# Options::Validate, the gpio_slowdown check, and the binding's
|
||||
# uint8_t setters, which cap chain_length at 255). Outside those
|
||||
# ranges the library returns no matrix and the display service
|
||||
# crash-loops rather than falling back, so they never save.
|
||||
def _hardware_int_error(field, low, high, even=False):
|
||||
"""A 400 response if data[field] is not an allowed integer, else None."""
|
||||
raw = data[field]
|
||||
kind = "an even integer" if even else "an integer"
|
||||
if high is None:
|
||||
allowed = f"{kind} of at least {low}"
|
||||
else:
|
||||
allowed = f"{kind} from {low} to {high}"
|
||||
rejection = (jsonify({'status': 'error', 'message': f"Invalid {field} '{raw}'. Must be {allowed}."}), 400)
|
||||
rejection = (jsonify({'status': 'error', 'message': f"Invalid {field} '{raw}'. Must be {describe_range(low, high, even)}."}), 400)
|
||||
# int() would quietly turn true into 1 and 48.5 into 48.
|
||||
if isinstance(raw, bool) or (isinstance(raw, float) and not raw.is_integer()):
|
||||
return rejection
|
||||
@@ -590,34 +584,31 @@ def save_main_config():
|
||||
value = int(raw)
|
||||
except (ValueError, TypeError, OverflowError):
|
||||
return rejection
|
||||
if value < low or (high is not None and value > high) or (even and value % 2):
|
||||
if value < low or value > high or (even and value % 2):
|
||||
return rejection
|
||||
return None
|
||||
|
||||
for field, low, high, even in (('rows', 8, None, True), ('cols', 16, None, False),
|
||||
('chain_length', 1, None, False), ('parallel', 1, 3, False),
|
||||
('brightness', 1, 100, False), ('scan_mode', 0, 1, False),
|
||||
('pwm_bits', 1, 11, False), ('pwm_dither_bits', 0, 2, False),
|
||||
('pwm_lsb_nanoseconds', 50, 3000, False),
|
||||
('limit_refresh_rate_hz', 0, None, False),
|
||||
('row_address_type', 0, 5, False), ('multiplexing', 0, 22, False),
|
||||
('gpio_slowdown', 0, 10, False)):
|
||||
if field in data:
|
||||
# rp1_rio has its own check below.
|
||||
for field, (_section, low, high, even) in INT_SETTING_LIMITS.items():
|
||||
if field in data and field != 'rp1_rio':
|
||||
error = _hardware_int_error(field, low, high, even)
|
||||
if error:
|
||||
return error
|
||||
|
||||
# A Pi 5 can't drive every combination (src/pi5_matrix_support.py),
|
||||
# and one it can't crashes the display service instead of falling
|
||||
# back. Checked only when this request sets one of those fields, so
|
||||
# a combination already stored doesn't block unrelated saves.
|
||||
pi5_fields = ('row_address_type', 'parallel', 'hardware_mapping')
|
||||
if any(k in data for k in pi5_fields) and is_raspberry_pi_5():
|
||||
# Combinations the library can't start with: a hardware mapping it
|
||||
# doesn't have, more parallel chains than the mapping has outputs,
|
||||
# and on a Pi 5 its narrower RP1 support. Reported only when this
|
||||
# request sets one of the settings involved, so a problem already
|
||||
# stored doesn't block unrelated saves.
|
||||
combination_fields = ('hardware_mapping', 'parallel', 'row_address_type')
|
||||
if any(k in data for k in combination_fields):
|
||||
effective = dict(current_config['display']['hardware'])
|
||||
effective.update({k: data[k] for k in pi5_fields if k in data})
|
||||
unsupported = pi5_unsupported_settings(effective)
|
||||
if unsupported:
|
||||
return jsonify({'status': 'error', 'message': unsupported}), 400
|
||||
effective.update({k: v for k, v in data.items()
|
||||
if k in combination_fields or k in INT_SETTING_LIMITS})
|
||||
refusals = [r for r in library_refusals(effective, pi5=is_raspberry_pi_5())
|
||||
if any(f in data for f in r.fields)]
|
||||
if refusals:
|
||||
return jsonify({'status': 'error', 'message': refusal_message(refusals)}), 400
|
||||
|
||||
# Handle hardware settings
|
||||
for field in ['rows', 'cols', 'chain_length', 'parallel', 'brightness', 'hardware_mapping', 'scan_mode',
|
||||
|
||||
Reference in New Issue
Block a user