mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-13 02:26:37 +00:00
feat: refuse matrix settings a Raspberry Pi 5 can't drive
On a Pi 5 the pinned rgbmatrix library drives the panel through the RP1 chip, and that path supports only row address types 0 and 2, parallel 1-3 and the regular / regular-pi1 / classic / adafruit-hat(-pwm) mappings (Rp1PioConfigSupported in lib/rp1/rp1_pio_backend.cc). For anything else CreateFromOptions returns NULL; the Python binding doesn't check, so the display process crashed on its first call into the matrix and systemd restarted it into the same crash every 10 seconds. - src/pi5_matrix_support.py: the rule and Pi 5 detection, matching the library's /proc/device-tree/model check - DisplayManager raises before creating the matrix, so it is a logged init failure (reported by /api/v3/hardware/status) and fallback mode - the config API rejects those settings on a Pi 5 when a request sets row_address_type, parallel or hardware_mapping - the Display form offers only row address types 0 and 2 on a Pi 5, and warns when a stored value can't be used - CLAUDE.md: re-check the rule whenever the submodule is bumped Review feedback on #586. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+10
-2
@@ -67,8 +67,16 @@ Web interface:
|
||||
- Row Address Type offers 5, the SM5368 / B707 row shift register. The
|
||||
Waveshare 96x48 V2 panel (back silkscreen `24S-A1`) needs it with RGB
|
||||
sequence BGR and, on a Pi 4, a GPIO slowdown of 6–8. Panels with FM6124
|
||||
column drivers need no Panel Type. On a Pi 5 the library supports only row
|
||||
address types 0 and 2.
|
||||
column drivers need no Panel Type.
|
||||
- On a Raspberry Pi 5 the pinned rgbmatrix library can drive only row address
|
||||
types 0 and 2, parallel 1–3 and the standard mappings. For anything else it
|
||||
returns no matrix, which the Python binding doesn't catch, so the display
|
||||
service crashed and restarted every 10 seconds. `DisplayManager` now refuses
|
||||
those settings before creating the matrix (logged, reported by
|
||||
`/api/v3/hardware/status`, fallback mode), the config API rejects them, and
|
||||
the Display form offers only row address types 0 and 2 on a Pi 5. The rule
|
||||
lives in `src/pi5_matrix_support.py` and must be re-checked when the
|
||||
submodule is bumped.
|
||||
|
||||
## 3.4.0
|
||||
|
||||
|
||||
@@ -63,3 +63,4 @@
|
||||
`self.display_manager.image.paste(img, (x, y))` then `update_display()`
|
||||
(use a mask for transparency: `image.paste(rgba, (x, y), rgba)`)
|
||||
- When modifying a plugin in the monorepo, you MUST bump `version` in its `manifest.json` and run `python update_registry.py` — otherwise users won't receive the update
|
||||
- `src/pi5_matrix_support.py` hardcodes what the pinned `rpi-rgb-led-matrix-master` can drive on a Raspberry Pi 5 (`Rp1PioConfigSupported()` in `lib/rp1/rp1_pio_backend.cc`). Re-check it whenever the submodule is bumped: a stale rule blocks Pi 5 settings the new library supports, and a missing one lets the display service crash-loop
|
||||
|
||||
@@ -631,6 +631,10 @@ These settings are typically only needed for non-standard panels or custom confi
|
||||
with an Adafruit Triple LED Matrix Bonnet, 4 left rows jumping; 6–8 gave a
|
||||
stable image.
|
||||
- On a Raspberry Pi 5 the rgbmatrix library currently supports only 0 and 2
|
||||
(and `parallel` 1-3). Anything else would crash the display service, so on
|
||||
a Pi 5 the web UI offers only 0 and 2, the config API refuses the others,
|
||||
and if one is set in `config.json` anyway the display logs why and runs in
|
||||
fallback mode
|
||||
- Check your panel datasheet if display appears corrupted
|
||||
|
||||
- **`multiplexing`** (integer, 0-22, default: 0)
|
||||
@@ -748,7 +752,7 @@ Controls how long each installed plugin stays visible in seconds before switchin
|
||||
- Verify `hardware_mapping` matches your HAT/connection type
|
||||
- Try adjusting `gpio_slowdown`
|
||||
- Ensure your display doesn't need the E-Addressable line
|
||||
- If it went blank right after a settings change, check `sudo journalctl -u ledmatrix` for `Failed to initialize RGB Matrix`: the rgbmatrix library refused a value (for example more than 64 `rows`, or `pwm_dither_bits` above 2) and the display fell back to no output. The library's own message nearby names the setting
|
||||
- If it went blank right after a settings change, check `sudo journalctl -u ledmatrix` for `Failed to initialize RGB Matrix`: the rgbmatrix library refused a value (for example more than 64 `rows`, or `pwm_dither_bits` above 2) and the display fell back to no output. The library's own message nearby names the setting; on a Raspberry Pi 5, LEDMatrix's message names any unsupported `row_address_type`, `parallel` or `hardware_mapping`
|
||||
- A repeating scramble points at `row_address_type` or `multiplexing`; a panel that stays dark, at `panel_type`
|
||||
|
||||
**Rows jump up and down, or the bottom row repeats other rows:**
|
||||
|
||||
@@ -67,7 +67,7 @@ in `DisplayManager` (`src/display_manager.py`, ~lines 270–295).
|
||||
| `limit_refresh_rate_hz` | int, `100` (code default 90) — `0` = no cap; scroll timing assumes 100 Hz when `0` |
|
||||
| `pixel_mapper_config` | string, `""` — e.g. `"U-mapper"` / `"Rotate:90"` |
|
||||
| `orientation` | string, `"normal"` — `"180"` rotates the rendered image 180° for panels physically mounted upside down (e.g. to move the Pi/wiring to a more convenient side); composed onto `pixel_mapper_config` as a trailing `Rotate:180` mapper, so it stays independent of any custom `pixel_mapper_config` value |
|
||||
| `row_address_type` | int, `0` — non-standard panel row addressing: `1` AB, `2` direct row select, `3` ABC, `4` ABC shift + DE direct, `5` SM5368 / B707 row shift register (e.g. Waveshare 96x48 V2, with `led_rgb_sequence` `"BGR"`). On a Pi 5 the library supports only `0` and `2` |
|
||||
| `row_address_type` | int, `0` — non-standard panel row addressing: `1` AB, `2` direct row select, `3` ABC, `4` ABC shift + DE direct, `5` SM5368 / B707 row shift register (e.g. Waveshare 96x48 V2, with `led_rgb_sequence` `"BGR"`). On a Pi 5 the library supports only `0` and `2`, and LEDMatrix enforces that (`src/pi5_matrix_support.py`) |
|
||||
| `multiplexing` | int, `0` — 0–22, pixel wiring scheme for outdoor/specialty panels (names listed in the README) |
|
||||
| `panel_type` | string, `""` — set to `"FM6126A"` or `"FM6127"` for panels needing init; FM6124 / FM6124D / FM6124DJ panels need none, so leave it `""` |
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ from src.display_geometry import (
|
||||
DEFAULT_CHAIN_LENGTH, DEFAULT_COLS, DEFAULT_PARALLEL, DEFAULT_ROWS,
|
||||
physical_size, resolve_double_sided,
|
||||
)
|
||||
from src.pi5_matrix_support import is_raspberry_pi_5, pi5_unsupported_settings
|
||||
import threading
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
@@ -325,6 +326,15 @@ 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")
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Which matrix settings the pinned rgbmatrix library can drive on a Raspberry Pi 5.
|
||||
|
||||
On a Pi 5 the library drives the panel through the RP1 chip, and at the pinned
|
||||
rpi-rgb-led-matrix-master commit (1ee4f76) that path supports only some
|
||||
settings -- ``Rp1PioConfigSupported()`` in ``lib/rp1/rp1_pio_backend.cc``. For
|
||||
anything else ``RGBMatrix::CreateFromOptions()`` returns NULL. The Python
|
||||
binding does not check for that, so instead of raising, the display process
|
||||
crashes on its first call into the matrix, and systemd restarts it into the
|
||||
same crash every 10 seconds.
|
||||
|
||||
``DisplayManager`` checks this rule before creating the matrix, turning that
|
||||
crash into a logged error and the usual fallback mode. The config API and the
|
||||
Display form use the same rule to refuse the settings up front.
|
||||
|
||||
**Re-check this when the submodule is bumped.** Upstream is extending Pi 5
|
||||
support (e.g. 4e326c1b, "Pi5 - Improvements, additional led-row-addr-type
|
||||
support"); a stale rule here would block settings the new library drives.
|
||||
"""
|
||||
|
||||
from typing import Any, Mapping, Optional
|
||||
|
||||
#: Read the way the library's ``Rp1PioPlatformDetected()`` reads it.
|
||||
MODEL_PATH = "/proc/device-tree/model"
|
||||
PI5_MODEL_MARKERS = ("Raspberry Pi 5", "Compute Module 5")
|
||||
|
||||
PI5_ROW_ADDRESS_TYPES = (0, 2)
|
||||
PI5_MAX_PARALLEL = 3
|
||||
PI5_HARDWARE_MAPPINGS = ("regular", "regular-pi1", "classic", "adafruit-hat", "adafruit-hat-pwm")
|
||||
|
||||
|
||||
def is_raspberry_pi_5() -> bool:
|
||||
"""True on a Pi 5-family board: Pi 5, Pi 500 or Compute Module 5."""
|
||||
try:
|
||||
with open(MODEL_PATH, "rb") as f:
|
||||
model = f.read(256).decode("utf-8", "replace")
|
||||
except OSError:
|
||||
return False
|
||||
return any(marker in model for marker in PI5_MODEL_MARKERS)
|
||||
|
||||
|
||||
def _as_int(value: Any, default: int) -> Optional[int]:
|
||||
if value is None or value == "":
|
||||
return default
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
return None
|
||||
|
||||
|
||||
def pi5_unsupported_settings(hardware: Mapping[str, Any]) -> Optional[str]:
|
||||
"""Why a Pi 5 can't drive this ``display.hardware`` config, or None if it can.
|
||||
|
||||
Missing keys take DisplayManager's defaults. A value that isn't a number is
|
||||
left to the caller's own validation rather than reported here.
|
||||
"""
|
||||
problems = []
|
||||
row_address_type = _as_int(hardware.get("row_address_type"), 0)
|
||||
if row_address_type is not None and row_address_type not in PI5_ROW_ADDRESS_TYPES:
|
||||
problems.append(f"row address type {row_address_type} (only 0 and 2 are supported)")
|
||||
parallel = _as_int(hardware.get("parallel"), 1)
|
||||
if parallel is not None and not 1 <= parallel <= PI5_MAX_PARALLEL:
|
||||
problems.append(f"parallel {parallel} (1 to {PI5_MAX_PARALLEL} are supported)")
|
||||
mapping = hardware.get("hardware_mapping", "adafruit-hat-pwm")
|
||||
# The library treats an empty mapping as "regular".
|
||||
if isinstance(mapping, str) and (mapping or "regular") not in PI5_HARDWARE_MAPPINGS:
|
||||
problems.append(f'hardware mapping "{mapping}"')
|
||||
if not problems:
|
||||
return None
|
||||
return ("Not supported on a Raspberry Pi 5 by the installed rgbmatrix library: "
|
||||
+ "; ".join(problems) + ".")
|
||||
@@ -31,6 +31,7 @@ sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from test._api_v3_test_helpers import api_v3_client, api_v3_module # noqa: F401,E402
|
||||
from test.test_web_settings_ui import REALISTIC_CONFIG # noqa: E402
|
||||
from src import pi5_matrix_support # noqa: E402
|
||||
|
||||
#: What a Waveshare RGB-Matrix-P2.5-96x48 V2 (back silkscreen 24S-A1) needed on
|
||||
#: a Pi 4 with an Adafruit Triple LED Matrix Bonnet, checked on the panel.
|
||||
@@ -43,6 +44,24 @@ WAVESHARE_96X48_V2 = {
|
||||
#: Fields stored under display.runtime; the rest go under display.hardware.
|
||||
RUNTIME_FIELDS = {'gpio_slowdown'}
|
||||
|
||||
PI5_MODEL = 'Raspberry Pi 5 Model B Rev 1.0'
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def board(tmp_path, monkeypatch):
|
||||
"""Not a Pi 5 unless a test says so, whatever machine runs the suite.
|
||||
|
||||
Returns a setter: board(PI5_MODEL) makes the API and the form see a Pi 5.
|
||||
"""
|
||||
path = tmp_path / 'device-tree-model'
|
||||
|
||||
def set_model(model):
|
||||
path.write_bytes(model.encode() + b'\x00')
|
||||
|
||||
set_model('Raspberry Pi 4 Model B Rev 1.5')
|
||||
monkeypatch.setattr(pi5_matrix_support, 'MODEL_PATH', str(path))
|
||||
return set_model
|
||||
|
||||
|
||||
def _stored(config, field):
|
||||
section = 'runtime' if field in RUNTIME_FIELDS else 'hardware'
|
||||
@@ -220,3 +239,70 @@ def test_a_stored_zero_renders_as_zero(display_page, field, section):
|
||||
"""`value or default` showed a stored 0 as the default, and the next save wrote it back."""
|
||||
body = display_page(_config_with(**{section: {field: 0}}))
|
||||
assert _attr(_input_tag(body, field), 'value') == '0'
|
||||
|
||||
|
||||
# --- Raspberry Pi 5 -------------------------------------------------------
|
||||
# The pinned library's Pi 5 path drives only row address types 0 and 2,
|
||||
# parallel 1-3 and the standard mappings; anything else crashes the display
|
||||
# service (src/pi5_matrix_support.py), so the API and the form refuse it.
|
||||
|
||||
@pytest.mark.parametrize('body', [
|
||||
{'row_address_type': 5}, {'row_address_type': '1'},
|
||||
{'hardware_mapping': 'compute-module'},
|
||||
])
|
||||
def test_pi5_refuses_what_its_library_cannot_drive(api_v3_client, saved, board, body):
|
||||
board(PI5_MODEL)
|
||||
response = _post(api_v3_client, body)
|
||||
assert response.status_code == 400
|
||||
assert 'Raspberry Pi 5' in response.get_json()['message']
|
||||
assert 'config' not in saved
|
||||
|
||||
|
||||
def test_pi5_saves_what_it_can_drive(api_v3_client, saved, board):
|
||||
board(PI5_MODEL)
|
||||
response = _post(api_v3_client, dict(WAVESHARE_96X48_V2, row_address_type=2))
|
||||
assert response.status_code == 200, response.get_data(as_text=True)[:200]
|
||||
assert saved['config']['display']['hardware']['row_address_type'] == 2
|
||||
|
||||
|
||||
def test_pi5_check_uses_the_stored_value_for_fields_not_sent(api_v3_client, api_v3_module, saved, board):
|
||||
"""Changing only the mapping is still checked against the stored row address type."""
|
||||
board(PI5_MODEL)
|
||||
api_v3_module.api_v3.config_manager.load_config.return_value = {
|
||||
'display': {'hardware': {'row_address_type': 5}}}
|
||||
response = _post(api_v3_client, {'hardware_mapping': 'regular'})
|
||||
assert response.status_code == 400
|
||||
assert 'config' not in saved
|
||||
|
||||
|
||||
def test_pi5_stored_combination_does_not_block_unrelated_saves(api_v3_client, api_v3_module, saved, board):
|
||||
board(PI5_MODEL)
|
||||
api_v3_module.api_v3.config_manager.load_config.return_value = {
|
||||
'display': {'hardware': {'row_address_type': 5}}}
|
||||
response = _post(api_v3_client, {'brightness': 70})
|
||||
assert response.status_code == 200, response.get_data(as_text=True)[:200]
|
||||
|
||||
|
||||
def _option_values(body, select_id):
|
||||
select = re.search(r'<select id="%s".*?</select>' % select_id, body, re.S)
|
||||
assert select, f'no <select id="{select_id}">'
|
||||
return re.findall(r'<option value="([^"]*)"', select.group(0))
|
||||
|
||||
|
||||
def test_pi5_form_offers_only_supported_row_address_types(display_page, board):
|
||||
board(PI5_MODEL)
|
||||
body = display_page(_config_with())
|
||||
assert _option_values(body, 'row_address_type') == ['0', '2']
|
||||
assert "can't be used on this Raspberry Pi 5" not in body
|
||||
|
||||
|
||||
def test_other_boards_offer_every_row_address_type(display_page):
|
||||
body = display_page(_config_with())
|
||||
assert _option_values(body, 'row_address_type') == ['0', '1', '2', '3', '4', '5']
|
||||
|
||||
|
||||
def test_pi5_form_warns_about_a_stored_unsupported_row_address_type(display_page, board):
|
||||
"""The unsupported option isn't offered, so the browser posts 0 -- say so."""
|
||||
board(PI5_MODEL)
|
||||
body = display_page(_config_with(hardware={'row_address_type': 5}))
|
||||
assert "Your saved row address type (5) can't be used on this Raspberry Pi 5" in body
|
||||
|
||||
@@ -279,3 +279,58 @@ class TestDisplayManagerOrientation:
|
||||
suppress_test_pattern=True)
|
||||
options = mock_rgb_matrix['options_class'].return_value
|
||||
assert options.pixel_mapper_config == 'U-mapper;Rotate:180'
|
||||
|
||||
|
||||
class TestDisplayManagerPi5Guard:
|
||||
"""On a Pi 5 the library returns no matrix for settings its RP1 path can't
|
||||
drive, and the binding doesn't check, so the process would crash on its next
|
||||
call. DisplayManager has to refuse before creating the matrix and fall back."""
|
||||
|
||||
def _config(self, **hardware_overrides):
|
||||
config = {
|
||||
'display': {
|
||||
'hardware': {
|
||||
'rows': 48, 'cols': 96, 'chain_length': 1, 'parallel': 1,
|
||||
'hardware_mapping': 'regular', 'brightness': 90,
|
||||
},
|
||||
'runtime': {'gpio_slowdown': 2},
|
||||
},
|
||||
'timezone': 'UTC',
|
||||
'plugin_system': {'plugins_directory': 'plugins'},
|
||||
}
|
||||
config['display']['hardware'].update(hardware_overrides)
|
||||
return config
|
||||
|
||||
@pytest.fixture
|
||||
def board(self, tmp_path, monkeypatch):
|
||||
from src import pi5_matrix_support
|
||||
|
||||
def set_model(model):
|
||||
path = tmp_path / 'model'
|
||||
path.write_bytes(model.encode() + b'\x00')
|
||||
monkeypatch.setattr(pi5_matrix_support, 'MODEL_PATH', str(path))
|
||||
return set_model
|
||||
|
||||
def test_unsupported_setting_on_pi5_never_creates_the_matrix(self, mock_rgb_matrix, board):
|
||||
board('Raspberry Pi 5 Model B Rev 1.0')
|
||||
DisplayManager._instance = None
|
||||
with patch.dict('os.environ', {'EMULATOR': 'false'}):
|
||||
dm = DisplayManager(self._config(row_address_type=5), suppress_test_pattern=True)
|
||||
mock_rgb_matrix['matrix_class'].assert_not_called()
|
||||
assert dm.matrix is None
|
||||
|
||||
def test_supported_setting_on_pi5_creates_the_matrix(self, mock_rgb_matrix, board):
|
||||
board('Raspberry Pi 5 Model B Rev 1.0')
|
||||
DisplayManager._instance = None
|
||||
with patch.dict('os.environ', {'EMULATOR': 'false'}):
|
||||
dm = DisplayManager(self._config(row_address_type=2), suppress_test_pattern=True)
|
||||
mock_rgb_matrix['matrix_class'].assert_called_once()
|
||||
assert dm.matrix is not None
|
||||
|
||||
def test_other_boards_are_left_to_the_library(self, mock_rgb_matrix, board):
|
||||
board('Raspberry Pi 4 Model B Rev 1.5')
|
||||
DisplayManager._instance = None
|
||||
with patch.dict('os.environ', {'EMULATOR': 'false'}):
|
||||
dm = DisplayManager(self._config(row_address_type=5), suppress_test_pattern=True)
|
||||
mock_rgb_matrix['matrix_class'].assert_called_once()
|
||||
assert dm.matrix is not None
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""The Pi 5 rule in src/pi5_matrix_support.py mirrors the pinned library.
|
||||
|
||||
Rp1PioPlatformDetected() and Rp1PioConfigSupported() in
|
||||
rpi-rgb-led-matrix-master/lib/rp1/rp1_pio_backend.cc (commit 1ee4f76). When the
|
||||
submodule is bumped and those change, these are the cases to revisit.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from src import pi5_matrix_support as pi5 # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def model_file(tmp_path, monkeypatch):
|
||||
def write(model):
|
||||
path = tmp_path / 'model'
|
||||
# The device tree string is NUL-terminated.
|
||||
path.write_bytes(model.encode() + b'\x00')
|
||||
monkeypatch.setattr(pi5, 'MODEL_PATH', str(path))
|
||||
return write
|
||||
|
||||
|
||||
@pytest.mark.parametrize('model', [
|
||||
'Raspberry Pi 5 Model B Rev 1.0',
|
||||
'Raspberry Pi 500 Rev 1.0',
|
||||
'Raspberry Pi Compute Module 5 Rev 1.0',
|
||||
])
|
||||
def test_pi5_family_is_detected(model_file, model):
|
||||
model_file(model)
|
||||
assert pi5.is_raspberry_pi_5()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('model', [
|
||||
'Raspberry Pi 4 Model B Rev 1.5',
|
||||
'Raspberry Pi Zero 2 W Rev 1.0',
|
||||
'Raspberry Pi Compute Module 4 Rev 1.0',
|
||||
])
|
||||
def test_other_boards_are_not(model_file, model):
|
||||
model_file(model)
|
||||
assert not pi5.is_raspberry_pi_5()
|
||||
|
||||
|
||||
def test_no_device_tree_is_not_a_pi5(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(pi5, 'MODEL_PATH', str(tmp_path / 'missing'))
|
||||
assert not pi5.is_raspberry_pi_5()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('hardware', [
|
||||
{},
|
||||
{'row_address_type': 0}, {'row_address_type': 2}, {'row_address_type': '2'},
|
||||
{'parallel': 1}, {'parallel': 3},
|
||||
{'hardware_mapping': 'regular'}, {'hardware_mapping': 'regular-pi1'},
|
||||
{'hardware_mapping': 'classic'}, {'hardware_mapping': 'adafruit-hat'},
|
||||
{'hardware_mapping': 'adafruit-hat-pwm'}, {'hardware_mapping': ''},
|
||||
])
|
||||
def test_what_the_pi5_path_supports(hardware):
|
||||
assert pi5.pi5_unsupported_settings(hardware) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('hardware,named', [
|
||||
({'row_address_type': 1}, 'row address type 1'),
|
||||
({'row_address_type': 3}, 'row address type 3'),
|
||||
({'row_address_type': 4}, 'row address type 4'),
|
||||
({'row_address_type': '5'}, 'row address type 5'),
|
||||
({'parallel': 4}, 'parallel 4'),
|
||||
({'hardware_mapping': 'compute-module'}, 'hardware mapping "compute-module"'),
|
||||
])
|
||||
def test_what_it_does_not(hardware, named):
|
||||
message = pi5.pi5_unsupported_settings(hardware)
|
||||
assert message is not None and named in message
|
||||
|
||||
|
||||
def test_every_problem_is_named():
|
||||
message = pi5.pi5_unsupported_settings({'row_address_type': 5, 'parallel': 4})
|
||||
assert 'row address type 5' in message and 'parallel 4' in message
|
||||
@@ -12,6 +12,7 @@ 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
|
||||
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.
|
||||
@@ -606,6 +607,18 @@ def save_main_config():
|
||||
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():
|
||||
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
|
||||
|
||||
# Handle hardware settings
|
||||
for field in ['rows', 'cols', 'chain_length', 'parallel', 'brightness', 'hardware_mapping', 'scan_mode',
|
||||
'pwm_bits', 'pwm_dither_bits', 'pwm_lsb_nanoseconds', 'limit_refresh_rate_hz',
|
||||
|
||||
@@ -14,6 +14,7 @@ _SAFE_WIDGET_NAME_RE = re.compile(r'^[a-zA-Z0-9_-]{1,64}$')
|
||||
_SAFE_WIDGET_SCRIPT_RE = re.compile(r'^[a-zA-Z0-9_-]{1,64}\.js$')
|
||||
from src.web_interface.secret_helpers import mask_secret_fields
|
||||
from src.common.path_safety import resolve_under, safe_path_component
|
||||
from src.pi5_matrix_support import is_raspberry_pi_5
|
||||
from web_interface import widget_bundle
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -544,7 +545,8 @@ def _load_display_partial():
|
||||
if pages_v3.config_manager:
|
||||
main_config = pages_v3.config_manager.load_config()
|
||||
return render_template('v3/partials/display.html',
|
||||
main_config=main_config)
|
||||
main_config=main_config,
|
||||
is_pi5=is_raspberry_pi_5())
|
||||
except Exception as e:
|
||||
logger.error("Error loading partial", exc_info=True)
|
||||
return "Error loading partial", 500
|
||||
|
||||
@@ -196,15 +196,23 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="setting-display-row_address_type" data-setting-key="display.hardware.row_address_type">
|
||||
<label for="row_address_type" class="block text-sm font-medium text-gray-700">Row Address Type{{ ui.help_tip('Row addressing scheme used by the panel.\nLeave at 0 (Default) unless your panel needs AB/ABC addressing — a wrong value shows a garbled or shifted image.\nABC panels (no E line, common on 128x64 FM6124 boards): try 3. Panels with SM5368 row drivers, such as the Waveshare 96x48 V2 (back silkscreen 24S-A1), need 5 with RGB sequence BGR. If rows then jump up and down, or the bottom row shows a copy of other rows, raise GPIO Slowdown (6–8 on a Pi 4). On a Raspberry Pi 5 the rgbmatrix library supports only 0 and 2.', 'Row Address Type') }}</label>
|
||||
<label for="row_address_type" class="block text-sm font-medium text-gray-700">Row Address Type{{ ui.help_tip('Row addressing scheme used by the panel.\nLeave at 0 (Default) unless your panel needs AB/ABC addressing — a wrong value shows a garbled or shifted image.\nABC panels (no E line, common on 128x64 FM6124 boards): try 3. Panels with SM5368 row drivers, such as the Waveshare 96x48 V2 (back silkscreen 24S-A1), need 5 with RGB sequence BGR. If rows then jump up and down, or the bottom row shows a copy of other rows, raise GPIO Slowdown (6–8 on a Pi 4). On a Raspberry Pi 5 the rgbmatrix library supports only 0 and 2, so only those are offered there.', 'Row Address Type') }}</label>
|
||||
{% set stored_row_address_type = main_config.display.hardware.get('row_address_type', 0)|int %}
|
||||
<select id="row_address_type" name="row_address_type" class="form-control">
|
||||
<option value="0" {% if main_config.display.hardware.get('row_address_type', 0)|int == 0 %}selected{% endif %}>0 - Default</option>
|
||||
<option value="1" {% if main_config.display.hardware.get('row_address_type', 0)|int == 1 %}selected{% endif %}>1 - AB-addressed panels</option>
|
||||
<option value="2" {% if main_config.display.hardware.get('row_address_type', 0)|int == 2 %}selected{% endif %}>2 - Row direct</option>
|
||||
<option value="3" {% if main_config.display.hardware.get('row_address_type', 0)|int == 3 %}selected{% endif %}>3 - ABC-addressed panels</option>
|
||||
<option value="4" {% if main_config.display.hardware.get('row_address_type', 0)|int == 4 %}selected{% endif %}>4 - ABC Shift + DE direct</option>
|
||||
<option value="5" {% if main_config.display.hardware.get('row_address_type', 0)|int == 5 %}selected{% endif %}>5 - SM5368 / B707 row shift register</option>
|
||||
<option value="0" {% if stored_row_address_type == 0 %}selected{% endif %}>0 - Default</option>
|
||||
{% if not is_pi5 %}
|
||||
<option value="1" {% if stored_row_address_type == 1 %}selected{% endif %}>1 - AB-addressed panels</option>
|
||||
{% endif %}
|
||||
<option value="2" {% if stored_row_address_type == 2 %}selected{% endif %}>2 - Row direct</option>
|
||||
{% if not is_pi5 %}
|
||||
<option value="3" {% if stored_row_address_type == 3 %}selected{% endif %}>3 - ABC-addressed panels</option>
|
||||
<option value="4" {% if stored_row_address_type == 4 %}selected{% endif %}>4 - ABC Shift + DE direct</option>
|
||||
<option value="5" {% if stored_row_address_type == 5 %}selected{% endif %}>5 - SM5368 / B707 row shift register</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
{% if is_pi5 and stored_row_address_type not in (0, 2) %}
|
||||
<p class="text-sm text-red-600 mt-1">Your saved row address type ({{ stored_row_address_type }}) can't be used on this Raspberry Pi 5, so the display won't start with it. Saving this form sets it to 0.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user