mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
- DisplayManager.defer_update()/process_deferred_updates(): one lock around every queue mutation (appends from the update thread were lost to the render thread's filter/slice reassignments); callables run outside it. - FontManager and element_style no longer cache BDF freetype.Face objects process-wide (load_bdf_face caches them per thread); element_style's LRU is locked against get/move_to_end vs eviction races. - limit_refresh_rate_hz default is one constant, DEFAULT_REFRESH_LIMIT_HZ = 100 (the template's), for the library options, refresh_hz, the matrix guard, Vegas and scroll_config. Previously a missing key capped the panel at 90 while pacing assumed 100. - Sync follower: the TCP thread queues the leader's scroll image; the render thread swaps image/array/width in between frames. - update_display() error log rate-limited (traceback first, then once a minute with a count); swallowed DisplayController exceptions log at DEBUG. - Root display_controller.py runs run.py via runpy. - stream_manager: correct the RLock release comments; merge duplicate if. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
213 lines
9.2 KiB
Python
213 lines
9.2 KiB
Python
"""Display settings the pinned rgbmatrix library refuses, on every board.
|
|
|
|
The library does not raise on a setting it can't use. For most it returns no
|
|
matrix -- ``RGBMatrix::CreateFromOptions()`` gives back NULL when
|
|
``Options::Validate()`` (``lib/options-initialize.cc``) or its GPIO slowdown
|
|
check (``lib/led-matrix.cc``) fails -- and the Python binding stores that
|
|
pointer without checking, so the display process crashes on its first call
|
|
into the matrix. For two it calls ``abort()``: an unknown hardware mapping
|
|
name, and more parallel chains than the mapping has outputs
|
|
(``Framebuffer::InitHardwareMapping()`` and the ``Framebuffer`` constructor in
|
|
``lib/framebuffer.cc``). Either way systemd restarts the service into the same
|
|
crash, and no fallback or hardware status is ever reported.
|
|
|
|
``DisplayManager`` runs :func:`library_refusals` before creating the matrix,
|
|
turning those crashes into a logged error and the usual fallback mode. The
|
|
config API uses the same rules to refuse the settings up front, and
|
|
:data:`INT_SETTING_LIMITS` is the one place the numeric ranges live.
|
|
|
|
Pi 5-only limits come from :mod:`src.pi5_matrix_support` and are added when
|
|
``pi5=True``.
|
|
|
|
**Re-check this when the submodule is bumped** (pinned at 1ee4f76): the
|
|
ranges in ``Options::Validate()``, the mapping table in
|
|
``lib/hardware-mapping.c`` and the setter types in
|
|
``bindings/python/rgbmatrix/core.pyx``. A stale rule here blocks settings a
|
|
new library accepts; a missing one lets the display service crash-loop.
|
|
"""
|
|
|
|
from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Tuple
|
|
|
|
from src.pi5_matrix_support import pi5_unsupported_settings
|
|
|
|
#: The binding's setters for rows, chain_length, parallel and the other small
|
|
#: integers are declared ``uint8_t`` (``core.pyx``), cols ``uint32_t``, and
|
|
#: limit_refresh_rate_hz lands in a C ``int``; a larger value raises
|
|
#: ``OverflowError`` before the library sees it.
|
|
UINT8_MAX = 255
|
|
UINT32_MAX = 2 ** 32 - 1
|
|
INT32_MAX = 2 ** 31 - 1
|
|
|
|
#: field -> (config section, lowest, highest, must be even). The library's own
|
|
#: ranges where it has one, otherwise the binding's integer type.
|
|
INT_SETTING_LIMITS: Dict[str, Tuple[str, int, int, bool]] = {
|
|
'rows': ('hardware', 8, 64, True),
|
|
'cols': ('hardware', 16, UINT32_MAX, False),
|
|
'chain_length': ('hardware', 1, UINT8_MAX, False),
|
|
# 3 is the most any mapping in the default build has (see MAPPING_OUTPUTS).
|
|
'parallel': ('hardware', 1, 3, False),
|
|
'brightness': ('hardware', 1, 100, False),
|
|
'scan_mode': ('hardware', 0, 1, False),
|
|
'pwm_bits': ('hardware', 1, 11, False),
|
|
'pwm_dither_bits': ('hardware', 0, 2, False),
|
|
'pwm_lsb_nanoseconds': ('hardware', 50, 3000, False),
|
|
# 0 = no cap; the library has no upper bound.
|
|
'limit_refresh_rate_hz': ('hardware', 0, INT32_MAX, False),
|
|
'row_address_type': ('hardware', 0, 5, False),
|
|
# 22 registered multiplexers (lib/multiplex-mappers.cc).
|
|
'multiplexing': ('hardware', 0, 22, False),
|
|
'gpio_slowdown': ('runtime', 0, 10, False),
|
|
'rp1_rio': ('runtime', 0, 1, False),
|
|
}
|
|
|
|
#: Hardware mapping name -> parallel chains it has outputs for, as
|
|
#: ``lib/hardware-mapping.c`` defines them. ``compute-module`` exists only when
|
|
#: the library is built with ENABLE_WIDE_GPIO_COMPUTE_MODULE, which the pinned
|
|
#: Makefile leaves commented out and the installer does not set, so the library
|
|
#: LEDMatrix installs aborts on it like any other unknown name.
|
|
MAPPING_OUTPUTS: Dict[str, int] = {
|
|
'regular': 3,
|
|
'adafruit-hat': 1,
|
|
'adafruit-hat-pwm': 1,
|
|
'regular-pi1': 1,
|
|
'classic': 3,
|
|
'classic-pi1': 1,
|
|
}
|
|
|
|
#: The refresh cap (``display.hardware.limit_refresh_rate_hz``) when config
|
|
#: omits it -- config/config.template.json's value. DisplayManager passes it to
|
|
#: the library and reports it as ``refresh_hz`` for scroll pacing, so the two
|
|
#: must be the same number: they were 90 and 100, and pacing solved against a
|
|
#: rate the panel was capped below.
|
|
DEFAULT_REFRESH_LIMIT_HZ = 100
|
|
|
|
#: What DisplayManager passes when a key is missing from display.hardware /
|
|
#: display.runtime. Config migration normally fills these from
|
|
#: config/config.template.json first, so they rarely apply.
|
|
DISPLAY_MANAGER_DEFAULTS: Dict[str, Any] = {
|
|
'rows': 32, 'cols': 64, 'chain_length': 2, 'parallel': 1,
|
|
'hardware_mapping': 'adafruit-hat-pwm', 'brightness': 90, 'pwm_bits': 10,
|
|
'pwm_lsb_nanoseconds': 150, 'led_rgb_sequence': 'RGB',
|
|
'row_address_type': 0, 'multiplexing': 0, 'limit_refresh_rate_hz': DEFAULT_REFRESH_LIMIT_HZ,
|
|
'gpio_slowdown': 3,
|
|
}
|
|
|
|
|
|
class Refusal(NamedTuple):
|
|
"""One reason the library can't start with a config.
|
|
|
|
``fields`` are the settings involved; the config API reports a refusal
|
|
only when the request sets one of them, so a problem already stored
|
|
doesn't block an unrelated save.
|
|
"""
|
|
fields: Tuple[str, ...]
|
|
message: str
|
|
#: A Raspberry Pi 5-only limit, whose message is already a full sentence.
|
|
pi5: bool = False
|
|
|
|
|
|
class MatrixSettingsRefused(RuntimeError):
|
|
"""Raised by DisplayManager instead of handing the library such a config."""
|
|
|
|
|
|
def _as_int(value: Any) -> Optional[int]:
|
|
"""The integer the binding would receive, or None if it isn't one.
|
|
|
|
A value that isn't a number is left to the binding, which raises a
|
|
``TypeError`` DisplayManager already turns into fallback mode.
|
|
"""
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError, OverflowError):
|
|
return None
|
|
|
|
|
|
def _setting(hardware: Mapping[str, Any], runtime: Mapping[str, Any], field: str) -> Any:
|
|
section = runtime if INT_SETTING_LIMITS.get(field, ('hardware',))[0] == 'runtime' else hardware
|
|
if field in section:
|
|
return section[field]
|
|
return DISPLAY_MANAGER_DEFAULTS.get(field)
|
|
|
|
|
|
def describe_range(low: int, high: int, even: bool = False) -> str:
|
|
kind = "an even integer" if even else "an integer"
|
|
if high >= INT32_MAX:
|
|
return f"{kind} of at least {low}"
|
|
return f"{kind} from {low} to {high}"
|
|
|
|
|
|
def library_refusals(hardware: Optional[Mapping[str, Any]],
|
|
runtime: Optional[Mapping[str, Any]] = None,
|
|
pi5: bool = False) -> List[Refusal]:
|
|
"""Every reason the pinned library would refuse (NULL, abort or overflow)
|
|
to start with this ``display.hardware`` / ``display.runtime`` config.
|
|
|
|
Missing keys take DisplayManager's defaults. ``pi5`` adds the Raspberry
|
|
Pi 5 limits from :func:`pi5_unsupported_settings`.
|
|
"""
|
|
hardware = hardware if isinstance(hardware, Mapping) else {}
|
|
runtime = runtime if isinstance(runtime, Mapping) else {}
|
|
refusals: List[Refusal] = []
|
|
|
|
for field, (section, low, high, even) in INT_SETTING_LIMITS.items():
|
|
# DisplayManager sets these only when present, and scan_mode /
|
|
# pwm_dither_bits / rp1_rio have no default of its own.
|
|
source = runtime if section == 'runtime' else hardware
|
|
if field not in source and field not in DISPLAY_MANAGER_DEFAULTS:
|
|
continue
|
|
value = _as_int(_setting(hardware, runtime, field))
|
|
if value is None:
|
|
continue
|
|
if not low <= value <= high or (even and value % 2):
|
|
refusals.append(Refusal(
|
|
(field,), f"{field} {value} (must be {describe_range(low, high, even)})"))
|
|
|
|
mapping = _setting(hardware, runtime, 'hardware_mapping')
|
|
outputs = None
|
|
if not isinstance(mapping, str):
|
|
refusals.append(Refusal(
|
|
('hardware_mapping',), f"hardware mapping {mapping!r} (must be a mapping name)"))
|
|
else:
|
|
# The library matches names case-insensitively and reads an empty
|
|
# name as "regular".
|
|
outputs = MAPPING_OUTPUTS.get((mapping or 'regular').lower())
|
|
if outputs is None:
|
|
refusals.append(Refusal(
|
|
('hardware_mapping',),
|
|
f'hardware mapping "{mapping}" (the installed library has '
|
|
+ ", ".join(MAPPING_OUTPUTS) + ")"))
|
|
|
|
parallel = _as_int(_setting(hardware, runtime, 'parallel'))
|
|
if outputs is not None and parallel is not None and 1 <= parallel <= 3 and parallel > outputs:
|
|
refusals.append(Refusal(
|
|
('parallel', 'hardware_mapping'),
|
|
f'parallel {parallel} with hardware mapping "{mapping}", which has '
|
|
f'{outputs} output{"s" if outputs != 1 else ""}'))
|
|
|
|
sequence = _setting(hardware, runtime, 'led_rgb_sequence')
|
|
if isinstance(sequence, str) and not (
|
|
len(sequence) == 3 and set(sequence.upper()) == {'R', 'G', 'B'}):
|
|
refusals.append(Refusal(
|
|
('led_rgb_sequence',),
|
|
f'LED RGB sequence "{sequence}" (must be R, G and B in some order)'))
|
|
|
|
if pi5:
|
|
unsupported = pi5_unsupported_settings(hardware)
|
|
if unsupported:
|
|
refusals.append(Refusal(
|
|
('row_address_type', 'parallel', 'hardware_mapping'), unsupported, pi5=True))
|
|
|
|
return refusals
|
|
|
|
|
|
def refusal_message(refusals: List[Refusal]) -> Optional[str]:
|
|
"""One sentence naming every refusal, or None when there are none."""
|
|
general = [r.message for r in refusals if not r.pi5]
|
|
pi5 = [r.message for r in refusals if r.pi5]
|
|
parts = []
|
|
if general:
|
|
parts.append("The installed rgbmatrix library can't start with these "
|
|
"display settings: " + "; ".join(general) + ".")
|
|
parts.extend(pi5)
|
|
return " ".join(parts) or None
|