mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-13 10:36:35 +00:00
Merge audit16-core/hardware into the September 16 core audit fixes
# Conflicts: # CHANGELOG.md
This commit is contained in:
+142
-8
@@ -1,17 +1,25 @@
|
||||
"""Display size from config: the one computation every caller shares.
|
||||
"""Display size from config: the one computation the size readers share.
|
||||
|
||||
``DisplayManager`` sizes its canvas from ``display.hardware`` plus
|
||||
``display.double_sided``. The web preview, the Starlark magnify default and
|
||||
the multi-display sync handshake used to re-derive that size themselves,
|
||||
each with its own defaults (``chain_length`` fell back to 2 in one place and
|
||||
1 in three others) and none of them applying double-sided mode. They now all
|
||||
call this module.
|
||||
``display.double_sided``. The web preview endpoints (``/display/current``, the
|
||||
SSE fallback), the Starlark magnify default and ``scripts/dev/vegas_audit.py``
|
||||
used to re-derive that size themselves, each with its own defaults
|
||||
(``chain_length`` fell back to 2 in one place and 1 in others) and none of
|
||||
them applying double-sided mode. They now call this module. (The multi-display
|
||||
sync handshake doesn't compute a size; it shares only
|
||||
``DEFAULT_CHAIN_LENGTH``.)
|
||||
|
||||
The size includes what the rgbmatrix library's pixel mappers do to it --
|
||||
``orientation`` and ``pixel_mapper_config`` (``Rotate:90`` swaps the axes,
|
||||
``U-mapper`` folds the chain) -- because ``RGBMatrix.width``/``height``, which
|
||||
``DisplayManager`` reports on hardware, are measured after them.
|
||||
|
||||
Kept free of hardware imports on purpose: the web interface imports it, and
|
||||
``display_manager`` pulls in ``rgbmatrix``.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Dict, Mapping, Optional, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -22,6 +30,10 @@ DEFAULT_COLS = 64
|
||||
DEFAULT_CHAIN_LENGTH = 2
|
||||
DEFAULT_PARALLEL = 1
|
||||
|
||||
#: ``display.hardware.orientation`` -> degrees of the ``Rotate`` mapper
|
||||
#: DisplayManager appends to ``pixel_mapper_config`` (None: no mapper).
|
||||
ORIENTATION_ROTATE_DEGREES = {'normal': None, '90': 90, '180': 180, '270': 270}
|
||||
|
||||
|
||||
def _display(config: Optional[Mapping[str, Any]]) -> Mapping[str, Any]:
|
||||
# A hand-edited config.json can hold anything here; treat a non-mapping
|
||||
@@ -35,10 +47,128 @@ def _hardware(config: Optional[Mapping[str, Any]]) -> Mapping[str, Any]:
|
||||
return hw if isinstance(hw, Mapping) else {}
|
||||
|
||||
|
||||
def compose_pixel_mapper_config(hardware: Mapping[str, Any]) -> str:
|
||||
"""The ``pixel_mapper_config`` DisplayManager hands the library.
|
||||
|
||||
``pixel_mapper_config`` stays a free-form advanced field (e.g. "U-mapper"
|
||||
for chain layouts); ``orientation`` is the user-facing mounting rotation,
|
||||
appended as a trailing ``Rotate:<deg>`` mapper rather than overwriting it.
|
||||
"""
|
||||
base = hardware.get('pixel_mapper_config') or ''
|
||||
base = base.strip() if isinstance(base, str) else ''
|
||||
degrees = ORIENTATION_ROTATE_DEGREES.get(hardware.get('orientation', 'normal'))
|
||||
if degrees is None:
|
||||
return base
|
||||
rotate = f'Rotate:{degrees}'
|
||||
return f'{base};{rotate}' if base else rotate
|
||||
|
||||
|
||||
def _c_div(a: int, b: int) -> int:
|
||||
"""C integer division (truncates toward zero)."""
|
||||
q = abs(a) // abs(b)
|
||||
return q if (a >= 0) == (b >= 0) else -q
|
||||
|
||||
|
||||
def _c_strtol(text: str) -> Tuple[int, str]:
|
||||
"""strtol(text, &end, 10): the parsed value (0 if none) and the rest."""
|
||||
match = re.match(r'\s*([+-]?\d+)', text)
|
||||
if not match:
|
||||
return 0, text
|
||||
return int(match.group(1)), text[match.end():]
|
||||
|
||||
|
||||
def _remap_size(param: Optional[str], width: int, height: int,
|
||||
chain: int, parallel: int) -> Optional[Tuple[int, int]]:
|
||||
"""RemapMapper::SetParameters and GetSizeMapping (lib/pixel-mapper.cc)."""
|
||||
if not param:
|
||||
return None
|
||||
new_w, rest = _c_strtol(param)
|
||||
if not rest.startswith(','):
|
||||
return None
|
||||
new_h, rest = _c_strtol(rest[1:])
|
||||
if not rest.startswith('|'):
|
||||
return None
|
||||
rest = rest[1:]
|
||||
tiles = []
|
||||
while rest:
|
||||
x, rest = _c_strtol(rest)
|
||||
if not rest.startswith(','):
|
||||
return None
|
||||
y, rest = _c_strtol(rest[1:])
|
||||
if not rest or rest[0].lower() not in 'neswx':
|
||||
return None
|
||||
tiles.append((x, y, rest[0].lower()))
|
||||
rest = rest[1:]
|
||||
if rest.startswith('|'):
|
||||
rest = rest[1:]
|
||||
elif rest:
|
||||
return None
|
||||
if len(tiles) != chain * parallel:
|
||||
return None
|
||||
panel_w, panel_h = _c_div(width, chain), _c_div(height, parallel)
|
||||
for x, y, kind in tiles:
|
||||
if kind == 'x':
|
||||
continue
|
||||
# MapTile::MapToVisible of the panel's (0, 0) and far corner.
|
||||
x0, y0, x1, y1 = {
|
||||
'n': (x, y, x + panel_w - 1, y + panel_h - 1),
|
||||
'w': (x, y + panel_w - 1, x + panel_h - 1, y),
|
||||
's': (x + panel_w - 1, y + panel_h - 1, x, y),
|
||||
'e': (x + panel_h - 1, y, x, y + panel_w - 1),
|
||||
}[kind]
|
||||
if x1 < 0 or x0 >= new_w or y1 < 0 or y0 >= new_h:
|
||||
return None
|
||||
return new_w, new_h
|
||||
|
||||
|
||||
def apply_pixel_mappers(width: int, height: int, mapper_config: str,
|
||||
chain: int, parallel: int) -> Tuple[int, int]:
|
||||
"""The canvas size after the library applies ``mapper_config``.
|
||||
|
||||
Mirrors ``RGBMatrix::Impl::ApplyNamedPixelMappers`` and each built-in
|
||||
mapper's ``SetParameters``/``GetSizeMapping`` in the pinned
|
||||
``lib/pixel-mapper.cc``: mappers apply left to right, and one the library
|
||||
doesn't know or can't configure is skipped and leaves the size alone.
|
||||
``multiplexing`` isn't modelled: its mappers give back the configured
|
||||
size for the panel sizes they are made for.
|
||||
"""
|
||||
for entry in (mapper_config or '').split(';'):
|
||||
name, colon, param = entry.partition(':')
|
||||
name = name.lower()
|
||||
param = param if colon else None
|
||||
if name == 'rotate':
|
||||
if not param:
|
||||
continue
|
||||
angle, rest = _c_strtol(param)
|
||||
if rest or angle % 90:
|
||||
continue
|
||||
if angle % 180:
|
||||
width, height = height, width
|
||||
elif name == 'u-mapper':
|
||||
if chain < 2 or chain % 2 or height % parallel:
|
||||
continue
|
||||
width, height = _c_div(width, 64) * 32, 2 * height
|
||||
elif name == 'v-mapper':
|
||||
width, height = (_c_div(width * parallel, chain),
|
||||
_c_div(height * chain, parallel))
|
||||
elif name == 'stacktorow':
|
||||
if param and any(c not in 'ZzFf, ' for c in param):
|
||||
continue
|
||||
width, height = width * parallel, _c_div(height, parallel)
|
||||
elif name == 'remap':
|
||||
size = _remap_size(param, width, height, chain, parallel)
|
||||
if size is not None:
|
||||
width, height = size
|
||||
# "mirror" keeps the size; the library skips names it doesn't know.
|
||||
return width, height
|
||||
|
||||
|
||||
def physical_size(config: Optional[Mapping[str, Any]]) -> Tuple[int, int]:
|
||||
"""Width and height of the whole panel chain, in pixels.
|
||||
|
||||
``cols * chain_length`` by ``rows * parallel``. Raises ``ValueError`` or
|
||||
``cols * chain_length`` by ``rows * parallel``, then through the pixel
|
||||
mappers ``orientation`` and ``pixel_mapper_config`` set up -- what
|
||||
``RGBMatrix.width``/``height`` report. Raises ``ValueError`` or
|
||||
``TypeError`` on a non-numeric value, as ``DisplayManager`` does; callers
|
||||
decide their own fallback.
|
||||
|
||||
@@ -54,7 +184,11 @@ def physical_size(config: Optional[Mapping[str, Any]]) -> Tuple[int, int]:
|
||||
parallel = int(hw.get('parallel', DEFAULT_PARALLEL))
|
||||
except OverflowError as e:
|
||||
raise ValueError(f"display.hardware size is not finite: {e}") from e
|
||||
return max(1, cols * chain_length), max(1, rows * parallel)
|
||||
width, height = max(1, cols * chain_length), max(1, rows * parallel)
|
||||
if chain_length >= 1 and parallel >= 1:
|
||||
width, height = apply_pixel_mappers(
|
||||
width, height, compose_pixel_mapper_config(hw), chain_length, parallel)
|
||||
return max(1, width), max(1, height)
|
||||
|
||||
|
||||
def resolve_double_sided(physical_width: int, physical_height: int,
|
||||
|
||||
+58
-32
@@ -37,9 +37,11 @@ from PIL import Image, ImageDraw, ImageFont
|
||||
from src.common.font_layout import crisp_size, load_truetype, resolve_asset_path
|
||||
from src.display_geometry import (
|
||||
DEFAULT_CHAIN_LENGTH, DEFAULT_COLS, DEFAULT_PARALLEL, DEFAULT_ROWS,
|
||||
physical_size, resolve_double_sided,
|
||||
ORIENTATION_ROTATE_DEGREES, compose_pixel_mapper_config, physical_size,
|
||||
resolve_double_sided,
|
||||
)
|
||||
from src.pi5_matrix_support import is_raspberry_pi_5, pi5_unsupported_settings
|
||||
from src.matrix_support import MatrixSettingsRefused, library_refusals, refusal_message
|
||||
from src.pi5_matrix_support import is_raspberry_pi_5
|
||||
import threading
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
@@ -247,28 +249,41 @@ class DisplayManager:
|
||||
# Calendar manager is now initialized by DisplayController
|
||||
|
||||
# Orientation setting -> rpi-rgb-led-matrix "Rotate:<deg>" pixel-mapper suffix.
|
||||
# "normal" needs no suffix since 0 degrees is the identity transform.
|
||||
_ORIENTATION_ROTATE_DEGREES = {'normal': None, '90': 90, '180': 180, '270': 270}
|
||||
_ORIENTATION_ROTATE_DEGREES = ORIENTATION_ROTATE_DEGREES
|
||||
|
||||
def _build_pixel_mapper_config(self, hardware_config: dict) -> str:
|
||||
"""Compose the raw pixel_mapper_config string with the orientation setting.
|
||||
"""Compose pixel_mapper_config with the orientation setting.
|
||||
|
||||
`pixel_mapper_config` stays available as a free-form advanced field (e.g.
|
||||
for "U-mapper" chain layouts); `orientation` is the user-facing dropdown
|
||||
for physical mounting (e.g. panels mounted upside down) and is appended as
|
||||
a "Rotate:<deg>" mapper rather than overwriting any existing config.
|
||||
See :func:`src.display_geometry.compose_pixel_mapper_config`, which the
|
||||
web preview shares so it sizes the canvas the same way.
|
||||
"""
|
||||
base_mapper = (hardware_config.get('pixel_mapper_config') or '').strip()
|
||||
orientation = hardware_config.get('orientation', 'normal')
|
||||
degrees = self._ORIENTATION_ROTATE_DEGREES.get(orientation)
|
||||
if degrees is None:
|
||||
return base_mapper
|
||||
rotate_mapper = f'Rotate:{degrees}'
|
||||
return f'{base_mapper};{rotate_mapper}' if base_mapper else rotate_mapper
|
||||
return compose_pixel_mapper_config(hardware_config)
|
||||
|
||||
@staticmethod
|
||||
def _fallback_advice(cause: str, error: Exception) -> str:
|
||||
"""What to do about a failed matrix init, for the log.
|
||||
|
||||
Only a library failure gets the rebuild hint: advice about the build
|
||||
or GPIO timing sends someone whose settings were refused the wrong way.
|
||||
"""
|
||||
if cause == "settings":
|
||||
return (f"{error} Change these in the web interface's Display tab "
|
||||
"(or display.hardware / display.runtime in config.json) "
|
||||
"and restart the display service.")
|
||||
if cause == "forced":
|
||||
return f"Error: {error}."
|
||||
advice = (f"Error: {error}. If the rgbmatrix library printed a message "
|
||||
"just before this, it names the problem.")
|
||||
if is_raspberry_pi_5():
|
||||
advice += (" On a Raspberry Pi 5, an mmap error means the library was "
|
||||
"built without Pi 5 support: sudo RPI_RGB_FORCE_REBUILD=1 "
|
||||
"./first_time_install.sh")
|
||||
return advice
|
||||
|
||||
def _setup_matrix(self):
|
||||
"""Initialize the RGB matrix with configuration settings."""
|
||||
_init_error_str = None
|
||||
_init_cause = None
|
||||
try:
|
||||
# Allow callers (e.g., web UI) to force non-hardware fallback mode
|
||||
if getattr(self, '_force_fallback', False):
|
||||
@@ -278,6 +293,18 @@ class DisplayManager:
|
||||
# Hardware configuration
|
||||
hardware_config = self.config.get('display', {}).get('hardware', {})
|
||||
runtime_config = self.config.get('display', {}).get('runtime', {})
|
||||
|
||||
# The library has no error path for many settings it can't use:
|
||||
# it returns no matrix (which the binding doesn't check, so the
|
||||
# process crashes on its next call) or calls abort(), and systemd
|
||||
# restarts the service into the same crash. Refuse those first so
|
||||
# they become a logged, reported fallback (src/matrix_support.py).
|
||||
refused = refusal_message(library_refusals(
|
||||
hardware_config, runtime_config, pi5=is_raspberry_pi_5()))
|
||||
if refused:
|
||||
if os.getenv("EMULATOR", "false") != "true":
|
||||
raise MatrixSettingsRefused(refused)
|
||||
logger.warning("Emulator mode: continuing, but on a real panel the display would not start. %s", refused)
|
||||
|
||||
# Basic hardware settings
|
||||
options.rows = hardware_config.get('rows', DEFAULT_ROWS)
|
||||
@@ -326,15 +353,6 @@ class DisplayManager:
|
||||
|
||||
logger.info(f"Initializing RGB Matrix with settings: rows={options.rows}, cols={options.cols}, chain_length={options.chain_length}, parallel={options.parallel}, hardware_mapping={options.hardware_mapping}")
|
||||
|
||||
# On a Pi 5 the library hands back no matrix for settings its RP1
|
||||
# path can't drive, and the binding doesn't check -- the process
|
||||
# would crash on its next call instead of reaching the fallback
|
||||
# below. Raise first so it is a logged, reported init failure.
|
||||
if os.getenv("EMULATOR", "false") != "true" and is_raspberry_pi_5():
|
||||
unsupported = pi5_unsupported_settings(hardware_config)
|
||||
if unsupported:
|
||||
raise RuntimeError(unsupported)
|
||||
|
||||
# Initialize the matrix
|
||||
self.matrix = RGBMatrix(options=options)
|
||||
logger.info("RGB Matrix initialized successfully")
|
||||
@@ -379,7 +397,12 @@ class DisplayManager:
|
||||
|
||||
except Exception as e:
|
||||
_init_error_str = str(e)
|
||||
logger.error(f"Failed to initialize RGB Matrix: {e}", exc_info=True)
|
||||
if isinstance(e, MatrixSettingsRefused):
|
||||
_init_cause = "settings"
|
||||
logger.error("Failed to initialize RGB Matrix: %s", e)
|
||||
else:
|
||||
_init_cause = "forced" if getattr(self, '_force_fallback', False) else "library"
|
||||
logger.error(f"Failed to initialize RGB Matrix: {e}", exc_info=True)
|
||||
# Create a fallback image for web preview using configured dimensions when available
|
||||
self.matrix = None
|
||||
try:
|
||||
@@ -406,15 +429,18 @@ class DisplayManager:
|
||||
# Best-effort; ignore drawing errors in fallback
|
||||
pass
|
||||
logger.error(
|
||||
f"Matrix initialization failed — running in fallback/simulation mode "
|
||||
f"(size {fallback_width}x{fallback_height}). Error: {e}. "
|
||||
"On Raspberry Pi 5: ensure rpi-rgb-led-matrix was built from the latest "
|
||||
"submodule (re-run first_time_install.sh). gpio_slowdown of 2–3 is typical for Pi 5 PIO mode."
|
||||
)
|
||||
"Matrix initialization failed — running in fallback/simulation mode "
|
||||
"(size %dx%d). %s",
|
||||
fallback_width, fallback_height, self._fallback_advice(_init_cause, e))
|
||||
# Do not raise here; allow fallback mode so web preview and non-hardware environments work
|
||||
|
||||
# Write hardware status file so the web UI can surface init failures
|
||||
_hw_status = {"ok": self.matrix is not None, "error": _init_error_str}
|
||||
# cause: None when ok; "settings" when LEDMatrix refused the config
|
||||
# (fix the named settings), "library" when the library itself failed,
|
||||
# "forced" for a caller-requested fallback. The Display tab keys its
|
||||
# advice on it.
|
||||
_hw_status = {"ok": self.matrix is not None, "error": _init_error_str,
|
||||
"cause": None if self.matrix is not None else _init_cause}
|
||||
_status_path = "/tmp/led_matrix_hw_status.json" # nosec B108
|
||||
try:
|
||||
if os.path.islink(_status_path):
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
"""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,
|
||||
}
|
||||
|
||||
#: 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': 90,
|
||||
'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
|
||||
Reference in New Issue
Block a user