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:
Chuck
2026-09-15 18:10:29 -04:00
co-authored by Claude Opus 5
parent 768517a658
commit 2d7a687a02
12 changed files with 348 additions and 12 deletions
+10 -2
View File
@@ -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
+1
View File
@@ -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
+5 -1
View File
@@ -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:**
+1 -1
View File
@@ -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 `""` |
+10
View File
@@ -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")
+70
View File
@@ -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) + ".")
+86
View File
@@ -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
+55
View File
@@ -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
+79
View File
@@ -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
+13
View File
@@ -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',
+3 -1
View File
@@ -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>