mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-13 02:26:37 +00:00
fix(web): accept every panel size and row address type the rgbmatrix library does
The Display form capped columns at 128 and chain length at 24, and its submit handler (fixInvalidNumberInputs) rewrote anything larger to the cap, so wide panels and long chains silently saved as the wrong size. The config API checked none of the hardware numbers, so values the library rejects (odd rows, parallel 4, PWM dither bits 3) saved and the matrix then refused to start. - Form limits now match the pinned library: rows even 8-64, cols >= 16 and chain_length >= 1 with no upper bound, parallel 1-3, PWM dither bits 0-2, PWM LSB nanoseconds 50-3000. - save_main_config rejects out-of-range rows, cols, chain_length, parallel, brightness, scan_mode, pwm_bits, pwm_dither_bits, pwm_lsb_nanoseconds and gpio_slowdown with a 400. - A stored gpio_slowdown or pwm_dither_bits of 0 renders as 0 instead of the default, so saving the tab no longer overwrites it. - Row Address Type offers 5 (SM5368 / B707 row shift register). Verified on a Waveshare 96x48 V2 (24S-A1) on a Pi 4 with the Adafruit Triple LED Matrix Bonnet: rows 48, cols 96, row address type 5, BGR, GPIO slowdown 8. - Help text and docs: FM6124-family panels use Panel Type Standard; on a Pi 5 the library supports only row address types 0 and 2. No change to the rpi-rgb-led-matrix submodule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,20 @@ Web interface:
|
||||
JSON API saves are unaffected. Lets plugins keep deprecated or internal keys
|
||||
declared, e.g. countdown's row `id` and weather's `api_key` / `radar_zoom`.
|
||||
See `docs/widget-guide.md`.
|
||||
- Display settings accept every panel size and timing value the rgbmatrix
|
||||
library does. Saving no longer silently cuts columns to 128, chain length to
|
||||
24 or PWM LSB nanoseconds to 500; rows are held to the library's even 8–64,
|
||||
parallel to 1–3 and PWM dither bits to 0–2. A stored GPIO slowdown or PWM
|
||||
dither bits of 0 no longer shows (and re-saves) as 3 or 1. The config API
|
||||
rejects out-of-range `rows`, `cols`, `chain_length`, `parallel`,
|
||||
`brightness`, `scan_mode`, `pwm_bits`, `pwm_dither_bits`,
|
||||
`pwm_lsb_nanoseconds` and `gpio_slowdown` with a 400 instead of saving a
|
||||
config the matrix refuses to start with.
|
||||
- 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.
|
||||
|
||||
## 3.4.0
|
||||
|
||||
|
||||
@@ -494,15 +494,18 @@ These settings control the physical hardware configuration and how the matrix is
|
||||
- **`rows`** (integer, default: 32)
|
||||
- Number of LED rows (vertical pixels) in each panel
|
||||
- Common values: 16, 32, 48, 64
|
||||
- Must be an even number from 8 to 64 (the rgbmatrix library's limit)
|
||||
- Must match your physical panel configuration
|
||||
|
||||
- **`cols`** (integer, default: 64)
|
||||
- Number of LED columns (horizontal pixels) in each panel
|
||||
- Common values: 32, 64, 96, 128
|
||||
- At least 16, with no upper limit
|
||||
- Must match your physical panel configuration
|
||||
|
||||
- **`chain_length`** (integer, default: 2)
|
||||
- Number of LED panels chained together horizontally
|
||||
- At least 1, with no upper limit; longer chains lower the refresh rate
|
||||
- If you have 2 panels side-by-side, set to 2
|
||||
- If you have 4 panels in a row, set to 4
|
||||
- Total display width = `cols × chain_length`
|
||||
@@ -511,13 +514,14 @@ These settings control the physical hardware configuration and how the matrix is
|
||||
- Number of parallel chains (panels stacked vertically)
|
||||
- Use 1 for a single row of panels
|
||||
- Use 2 if you have panels stacked in two rows
|
||||
- 1–3 on a Raspberry Pi, and the HAT needs that many outputs
|
||||
- Total display height = `rows × parallel`
|
||||
|
||||
#### Brightness and Visual Settings
|
||||
|
||||
- **`brightness`** (integer, 0-100, default: 90)
|
||||
- **`brightness`** (integer, 1-100, default: 90)
|
||||
- Display brightness level
|
||||
- Lower values (0-50) are dimmer, higher values (50-100) are brighter
|
||||
- Lower values (1-50) are dimmer, higher values (50-100) are brighter
|
||||
- Recommended: 70-90 for indoor use, 90-100 for bright environments
|
||||
- Very high brightness may cause distortion or require more power
|
||||
|
||||
@@ -612,7 +616,18 @@ These settings are typically only needed for non-standard panels or custom confi
|
||||
- **`row_address_type`** (integer, default: 0)
|
||||
- How rows are addressed on the panel
|
||||
- Most panels use 0 (direct addressing)
|
||||
- Some panels require 1 (AB addressing) or 2 (ABC addressing)
|
||||
- 1 = AB-addressed, 2 = direct row select, 3 = ABC-addressed,
|
||||
4 = ABC shift + DE direct (SM5266), 5 = SM5368 / B707 row shift register
|
||||
- ABC panels (no E line, e.g. many 128x64 FM6124 panels) use 3
|
||||
- Panels with SM5368 row drivers use 5 with `led_rgb_sequence` `"BGR"` —
|
||||
e.g. the Waveshare 96x48 V2 (back silkscreen `24S-A1`; the V1, `24S-A2.1`,
|
||||
uses the defaults). This is what Waveshare's `96X48_1_24_SM5368` panel
|
||||
type sets in their library fork.
|
||||
- SM5368 row drivers are timing-sensitive: if rows jump up and down or the
|
||||
bottom row shows a copy of other rows, raise `gpio_slowdown`. On a Pi 4
|
||||
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
|
||||
- Check your panel datasheet if display appears corrupted
|
||||
|
||||
- **`multiplexing`** (integer, default: 0)
|
||||
@@ -632,6 +647,8 @@ These settings control runtime behavior and GPIO timing:
|
||||
- **Raspberry Pi 4**: Use 4
|
||||
- **Raspberry Pi 5**: Use 1–2 in PIO mode (`rp1_rio: 0`, the default); start with `1` and increase if you see flickering
|
||||
- **Raspberry Pi Zero/1**: Use 1-2
|
||||
- Range: 0-10. Panels on `row_address_type` 5 (SM5368 row drivers) can need
|
||||
6-8 on a Pi 4
|
||||
- Incorrect values can cause display corruption, flickering, or system instability
|
||||
- If you experience issues, try adjusting this value up or down by 1
|
||||
|
||||
|
||||
+11
-11
@@ -51,15 +51,15 @@ in `DisplayManager` (`src/display_manager.py`, ~lines 270–295).
|
||||
|
||||
| Key | Type / default |
|
||||
|---|---|
|
||||
| `rows` / `cols` | int, `32` / `64` |
|
||||
| `chain_length` | int, `2` |
|
||||
| `parallel` | int, `1` |
|
||||
| `brightness` | int, `90` |
|
||||
| `rows` / `cols` | int, `32` / `64` — rows: even, 8–64; cols: at least 16, no upper limit |
|
||||
| `chain_length` | int, `2` — at least 1, no upper limit |
|
||||
| `parallel` | int, `1` — 1–3 |
|
||||
| `brightness` | int, `90` — 1–100 |
|
||||
| `hardware_mapping` | string, `"adafruit-hat"` (code default `"adafruit-hat-pwm"`) |
|
||||
| `scan_mode` | int, `0` |
|
||||
| `pwm_bits` | int, `9` (code default 10) |
|
||||
| `pwm_dither_bits` | int, `1` |
|
||||
| `pwm_lsb_nanoseconds` | int, `130` (code default 150) |
|
||||
| `scan_mode` | int, `0` — `0` progressive, `1` interlaced |
|
||||
| `pwm_bits` | int, `9` (code default 10) — 1–11 |
|
||||
| `pwm_dither_bits` | int, `1` — 0–2 |
|
||||
| `pwm_lsb_nanoseconds` | int, `130` (code default 150) — 50–3000 |
|
||||
| `disable_hardware_pulsing` | bool, `false` |
|
||||
| `inverse_colors` | bool, `false` |
|
||||
| `show_refresh_rate` | bool, `false` |
|
||||
@@ -67,9 +67,9 @@ in `DisplayManager` (`src/display_manager.py`, ~lines 270–295).
|
||||
| `limit_refresh_rate_hz` | int, `100` (code default 90) |
|
||||
| `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 |
|
||||
| `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` |
|
||||
| `multiplexing` | int, `0` — panel multiplexing scheme |
|
||||
| `panel_type` | string, `""` — set to `"FM6126A"` or `"FM6127"` for panels needing init |
|
||||
| `panel_type` | string, `""` — set to `"FM6126A"` or `"FM6127"` for panels needing init; FM6124 / FM6124D / FM6124DJ panels need none, so leave it `""` |
|
||||
|
||||
Where "code default" differs from the template value, the code default only
|
||||
applies if the key is missing entirely from your config.
|
||||
@@ -78,7 +78,7 @@ applies if the key is missing entirely from your config.
|
||||
|
||||
| Key | Type / default | Meaning |
|
||||
|---|---|---|
|
||||
| `gpio_slowdown` | int, `3` | GPIO timing slowdown for faster Pis |
|
||||
| `gpio_slowdown` | int, `3` | GPIO timing slowdown for faster Pis (0–10). Panels on `row_address_type` `5` (SM5368 row drivers) can need 6–8 on a Pi 4 — lower values make rows jump |
|
||||
| `rp1_rio` | int, `0` | RP1 RIO mode on Pi 5 (applied only if the installed matrix library supports it) |
|
||||
|
||||
## `display.double_sided`
|
||||
|
||||
@@ -83,10 +83,10 @@ You should see:
|
||||
|
||||
1. Open the **Display** tab
|
||||
2. Set your matrix configuration:
|
||||
- **Rows**: 32 or 64 (match your hardware)
|
||||
- **Columns**: commonly 64 or 96; the web UI accepts any integer
|
||||
in the 1–128 range, but 64 and 96 are the values the bundled
|
||||
panel hardware ships with
|
||||
- **Rows**: match your panel — commonly 32 or 64; any even number
|
||||
from 8 to 64
|
||||
- **Columns**: match your panel — commonly 64 or 96; at least 16,
|
||||
with no upper limit
|
||||
- **Chain Length**: Number of panels chained horizontally
|
||||
- **Hardware Mapping**: usually `adafruit-hat-pwm` (with the PWM jumper
|
||||
mod) or `adafruit-hat` (without). See the root README for the full list.
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
"""Display hardware settings accept what the rgbmatrix library accepts.
|
||||
|
||||
Held to the ranges in the pinned library (RGBMatrix::Options::Validate in
|
||||
lib/options-initialize.cc, the gpio_slowdown check in lib/led-matrix.cc). Two
|
||||
ways this used to go wrong:
|
||||
|
||||
- The Display form capped cols at 128, chain_length at 24 and
|
||||
pwm_lsb_nanoseconds at 500, and its submit handler (fixInvalidNumberInputs)
|
||||
rewrites anything past an input's min/max to that bound -- so a wide panel or
|
||||
a long chain silently saved as the wrong size.
|
||||
- The API checked none of these, so a value the library rejects (odd rows,
|
||||
parallel 4, pwm_dither_bits 3) saved, and the matrix then refused to start.
|
||||
|
||||
Row address type 5 is the SM5368 / B707 row shift register the Waveshare 96x48
|
||||
V2 needs (Waveshare's own "96X48_1_24_SM5368" panel type in their library fork
|
||||
just sets rows/cols, row_address_type=5 and BGR); the API used to stop at 4.
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
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
|
||||
|
||||
#: 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.
|
||||
WAVESHARE_96X48_V2 = {
|
||||
'rows': 48, 'cols': 96, 'chain_length': 1, 'parallel': 1,
|
||||
'hardware_mapping': 'regular', 'panel_type': '', 'row_address_type': 5,
|
||||
'led_rgb_sequence': 'BGR', 'gpio_slowdown': 8,
|
||||
}
|
||||
|
||||
#: Fields stored under display.runtime; the rest go under display.hardware.
|
||||
RUNTIME_FIELDS = {'gpio_slowdown'}
|
||||
|
||||
|
||||
def _stored(config, field):
|
||||
section = 'runtime' if field in RUNTIME_FIELDS else 'hardware'
|
||||
return config['display'][section][field]
|
||||
|
||||
|
||||
def _post(client, body):
|
||||
return client.post('/api/v3/config/main', data=json.dumps(body),
|
||||
content_type='application/json')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def saved(api_v3_module, monkeypatch):
|
||||
"""Capture what save_main_config would write.
|
||||
|
||||
Asserting on the stored value, not just the status code, is what shows the
|
||||
value passed validation and landed where DisplayManager reads it.
|
||||
"""
|
||||
captured = {}
|
||||
api_v3_module.api_v3.config_manager.load_config.return_value = {}
|
||||
|
||||
def fake_save(_manager, config, **_kwargs):
|
||||
captured['config'] = config
|
||||
return True, ''
|
||||
|
||||
monkeypatch.setattr(api_v3_module, '_save_config_atomic', fake_save)
|
||||
return captured
|
||||
|
||||
|
||||
@pytest.mark.parametrize('as_strings', [False, True], ids=['json-numbers', 'form-strings'])
|
||||
def test_waveshare_96x48_v2_settings_all_save(api_v3_client, saved, as_strings):
|
||||
"""The Display form posts every value as a string (json-enc); API clients send numbers."""
|
||||
body = {k: str(v) if as_strings else v for k, v in WAVESHARE_96X48_V2.items()}
|
||||
response = _post(api_v3_client, body)
|
||||
assert response.status_code == 200, response.get_data(as_text=True)[:200]
|
||||
for field, value in WAVESHARE_96X48_V2.items():
|
||||
assert _stored(saved['config'], field) == value, field
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field,value', [
|
||||
('rows', 8), ('rows', 64),
|
||||
('cols', 16), ('cols', 192), ('cols', 512),
|
||||
('chain_length', 1), ('chain_length', 32),
|
||||
('parallel', 3),
|
||||
('row_address_type', 0), ('row_address_type', 5),
|
||||
('gpio_slowdown', 0), ('gpio_slowdown', 10),
|
||||
('pwm_bits', 1), ('pwm_bits', 11),
|
||||
('pwm_dither_bits', 0), ('pwm_dither_bits', 2),
|
||||
('pwm_lsb_nanoseconds', 50), ('pwm_lsb_nanoseconds', 3000),
|
||||
('scan_mode', 1),
|
||||
('brightness', 1), ('brightness', 100),
|
||||
])
|
||||
def test_values_the_library_accepts_are_saved(api_v3_client, saved, field, value):
|
||||
response = _post(api_v3_client, {field: value})
|
||||
assert response.status_code == 200, response.get_data(as_text=True)[:200]
|
||||
assert _stored(saved['config'], field) == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field,value', [
|
||||
('rows', 6), ('rows', 47), ('rows', 66), ('rows', '48.5'),
|
||||
('cols', 15), ('cols', 96.5), ('cols', True), ('cols', 'wide'),
|
||||
('chain_length', 0),
|
||||
('parallel', 0), ('parallel', 4),
|
||||
('row_address_type', -1), ('row_address_type', 6),
|
||||
('gpio_slowdown', -1), ('gpio_slowdown', 11),
|
||||
('pwm_bits', 0), ('pwm_bits', 12),
|
||||
('pwm_dither_bits', 3),
|
||||
('pwm_lsb_nanoseconds', 49), ('pwm_lsb_nanoseconds', 3001),
|
||||
('scan_mode', 2),
|
||||
('brightness', 0), ('brightness', 101),
|
||||
])
|
||||
def test_values_the_library_rejects_are_refused(api_v3_client, saved, field, value):
|
||||
"""Refused with a message naming the field, and nothing written."""
|
||||
response = _post(api_v3_client, {field: value})
|
||||
assert response.status_code == 400
|
||||
assert field in response.get_json()['message']
|
||||
assert 'config' not in saved
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def display_page(monkeypatch):
|
||||
"""Render the Display settings partial for a given config."""
|
||||
from web_interface.blueprints import pages_v3 as pv
|
||||
|
||||
def render(config):
|
||||
base = PROJECT_ROOT / 'web_interface'
|
||||
app = Flask(__name__, template_folder=str(base / 'templates'),
|
||||
static_folder=str(base / 'static'))
|
||||
app.config['TESTING'] = True
|
||||
config_manager = MagicMock()
|
||||
config_manager.load_config.return_value = config
|
||||
config_manager.get_raw_file_content.return_value = config
|
||||
config_manager.get_config_path.return_value = 'config/config.json'
|
||||
config_manager.get_secrets_path.return_value = 'config/config_secrets.json'
|
||||
monkeypatch.setattr(pv.pages_v3, 'config_manager', config_manager, raising=False)
|
||||
monkeypatch.setattr(pv.pages_v3, 'plugin_manager', MagicMock(plugins={}), raising=False)
|
||||
app.register_blueprint(pv.pages_v3, url_prefix='/v3')
|
||||
response = app.test_client().get('/v3/partials/display')
|
||||
assert response.status_code == 200
|
||||
return response.get_data(as_text=True)
|
||||
|
||||
return render
|
||||
|
||||
|
||||
def _config_with(hardware=None, runtime=None):
|
||||
config = copy.deepcopy(REALISTIC_CONFIG)
|
||||
config['display']['hardware'].update(hardware or {})
|
||||
config['display']['runtime'].update(runtime or {})
|
||||
return config
|
||||
|
||||
|
||||
def _input_tag(body, input_id):
|
||||
match = re.search(r'<input[^>]*\bid="%s"[^>]*>' % re.escape(input_id), body)
|
||||
assert match, f'no <input id="{input_id}">'
|
||||
return match.group(0)
|
||||
|
||||
|
||||
def _attr(tag, name):
|
||||
match = re.search(r'\s%s="([^"]*)"' % name, tag)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def _selected_option(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="([^"]*)"\s+selected\s*>', select.group(0))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('input_id,expected', [
|
||||
('rows', {'min': '8', 'max': '64', 'step': '2'}),
|
||||
('cols', {'min': '16', 'max': None}),
|
||||
('chain_length', {'min': '1', 'max': None}),
|
||||
('parallel', {'min': '1', 'max': '3'}),
|
||||
('gpio_slowdown', {'min': '0', 'max': '10'}),
|
||||
('pwm_bits', {'min': '1', 'max': '11'}),
|
||||
('pwm_dither_bits', {'min': '0', 'max': '2'}),
|
||||
('pwm_lsb_nanoseconds', {'min': '50', 'max': '3000'}),
|
||||
])
|
||||
def test_form_limits_match_the_library(display_page, input_id, expected):
|
||||
"""fixInvalidNumberInputs rewrites a value past min/max on submit, so these
|
||||
attributes are the real limits: a max below the library's clamps panels
|
||||
that would work, and one above it saves a value the matrix rejects."""
|
||||
tag = _input_tag(display_page(_config_with()), input_id)
|
||||
for name, value in expected.items():
|
||||
assert _attr(tag, name) == value, f'{input_id} {name}'
|
||||
|
||||
|
||||
def test_waveshare_96x48_v2_config_renders_back_unchanged(display_page):
|
||||
"""Saving the Display tab posts what it rendered, so each value must render as stored.
|
||||
|
||||
Before row address type 5 was in the dropdown no option was selected, the
|
||||
browser posted the first one (0), and one save scrambled the panel again.
|
||||
"""
|
||||
hardware = {k: v for k, v in WAVESHARE_96X48_V2.items() if k not in RUNTIME_FIELDS}
|
||||
body = display_page(_config_with(hardware=hardware, runtime={'gpio_slowdown': 8}))
|
||||
|
||||
assert _selected_option(body, 'row_address_type') == ['5']
|
||||
assert _selected_option(body, 'led_rgb_sequence') == ['BGR']
|
||||
assert _selected_option(body, 'hardware_mapping') == ['regular']
|
||||
for input_id in ('rows', 'cols', 'chain_length', 'parallel', 'gpio_slowdown'):
|
||||
assert _attr(_input_tag(body, input_id), 'value') == str(WAVESHARE_96X48_V2[input_id]), input_id
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field,section', [
|
||||
('gpio_slowdown', 'runtime'), ('pwm_dither_bits', 'hardware'),
|
||||
])
|
||||
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'
|
||||
@@ -577,10 +577,48 @@ def save_main_config():
|
||||
if 'row_address_type' in data:
|
||||
try:
|
||||
rat_val = int(data['row_address_type'])
|
||||
if rat_val < 0 or rat_val > 4:
|
||||
return jsonify({'status': 'error', 'message': f"Invalid row_address_type '{data['row_address_type']}'. Must be an integer from 0 to 4."}), 400
|
||||
if rat_val < 0 or rat_val > 5:
|
||||
return jsonify({'status': 'error', 'message': f"Invalid row_address_type '{data['row_address_type']}'. Must be an integer from 0 to 5."}), 400
|
||||
except (ValueError, TypeError, OverflowError):
|
||||
return jsonify({'status': 'error', 'message': f"Invalid row_address_type '{data['row_address_type']}'. Must be an integer from 0 to 4."}), 400
|
||||
return jsonify({'status': 'error', 'message': f"Invalid row_address_type '{data['row_address_type']}'. Must be an integer from 0 to 5."}), 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 and chain_length have no
|
||||
# upper bound in the library, so none here.
|
||||
def _hardware_int_error(field, low, high=None, even=False):
|
||||
"""A 400 response if data[field] is not an allowed integer, else None."""
|
||||
raw = data[field]
|
||||
if even:
|
||||
allowed = f"an even integer from {low} to {high}"
|
||||
elif high is None:
|
||||
allowed = f"an integer of at least {low}"
|
||||
else:
|
||||
allowed = f"an integer from {low} to {high}"
|
||||
rejection = (jsonify({'status': 'error', 'message': f"Invalid {field} '{raw}'. Must be {allowed}."}), 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
|
||||
try:
|
||||
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):
|
||||
return rejection
|
||||
return None
|
||||
|
||||
for field, low, high, even in (('rows', 8, 64, 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),
|
||||
('gpio_slowdown', 0, 10, False)):
|
||||
if field in data:
|
||||
error = _hardware_int_error(field, low, high, even)
|
||||
if error:
|
||||
return error
|
||||
|
||||
# Handle hardware settings
|
||||
for field in ['rows', 'cols', 'chain_length', 'parallel', 'brightness', 'hardware_mapping', 'scan_mode',
|
||||
|
||||
@@ -41,46 +41,45 @@
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-4 gap-4 mb-4">
|
||||
<div class="form-group" id="setting-display-rows" data-setting-key="display.hardware.rows">
|
||||
<label for="rows" class="block text-sm font-medium text-gray-700">Rows{{ ui.help_tip('Number of LED rows on a single panel.\nCommon: 16, 32, or 64. Default: 32. Must match your panel.', 'Rows') }}</label>
|
||||
<label for="rows" class="block text-sm font-medium text-gray-700">Rows{{ ui.help_tip('Number of LED rows on a single panel.\nCommon: 16, 32, 48 or 64. Default: 32. Must match your panel. The rgbmatrix library accepts even numbers from 8 to 64.', 'Rows') }}</label>
|
||||
<input type="number"
|
||||
id="rows"
|
||||
name="rows"
|
||||
value="{{ main_config.display.hardware.rows or 32 }}"
|
||||
min="1"
|
||||
max="128"
|
||||
min="8"
|
||||
max="64"
|
||||
step="2"
|
||||
class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="setting-display-cols" data-setting-key="display.hardware.cols">
|
||||
<label for="cols" class="block text-sm font-medium text-gray-700">Columns{{ ui.help_tip('Number of LED columns on a single panel.\nCommon: 32 or 64. Default: 64. Must match your panel.', 'Columns') }}</label>
|
||||
<label for="cols" class="block text-sm font-medium text-gray-700">Columns{{ ui.help_tip('Number of LED columns on a single panel.\nCommon: 32, 64, 96 or 128. Default: 64. Must match your panel. At least 16, with no upper limit.', 'Columns') }}</label>
|
||||
<input type="number"
|
||||
id="cols"
|
||||
name="cols"
|
||||
value="{{ main_config.display.hardware.cols or 64 }}"
|
||||
min="1"
|
||||
max="128"
|
||||
min="16"
|
||||
class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="setting-display-chain_length" data-setting-key="display.hardware.chain_length">
|
||||
<label for="chain_length" class="block text-sm font-medium text-gray-700">Chain Length{{ ui.help_tip('How many panels are wired end-to-end in one chain.\nDefault: 2. Example: two 64×32 panels chained make a 128×32 display.', 'Chain Length') }}</label>
|
||||
<label for="chain_length" class="block text-sm font-medium text-gray-700">Chain Length{{ ui.help_tip('How many panels are wired end-to-end in one chain.\nDefault: 2. Example: two 64×32 panels chained make a 128×32 display. No upper limit, but longer chains lower the refresh rate.', 'Chain Length') }}</label>
|
||||
<input type="number"
|
||||
id="chain_length"
|
||||
name="chain_length"
|
||||
value="{{ main_config.display.hardware.chain_length or 2 }}"
|
||||
min="1"
|
||||
max="24"
|
||||
class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="setting-display-parallel" data-setting-key="display.hardware.parallel">
|
||||
<label for="parallel" class="block text-sm font-medium text-gray-700">Parallel{{ ui.help_tip('Number of separate chains driven in parallel from the HAT.\nDefault: 1. The Raspberry Pi supports up to 3 (some HATs allow more).', 'Parallel') }}</label>
|
||||
<label for="parallel" class="block text-sm font-medium text-gray-700">Parallel{{ ui.help_tip('Number of separate chains driven in parallel from the HAT.\nDefault: 1. The Raspberry Pi supports up to 3, and the HAT needs that many outputs (e.g. the Adafruit Triple LED Matrix Bonnet).', 'Parallel') }}</label>
|
||||
<input type="number"
|
||||
id="parallel"
|
||||
name="parallel"
|
||||
value="{{ main_config.display.hardware.parallel or 1 }}"
|
||||
min="1"
|
||||
max="4"
|
||||
max="3"
|
||||
class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,7 +188,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="setting-display-panel_type" data-setting-key="display.hardware.panel_type">
|
||||
<label for="panel_type" class="block text-sm font-medium text-gray-700">Panel Type{{ ui.help_tip('Special initialization for panels with a specific driver chip (e.g. FM6126A, FM6127).\nLeave on Standard unless your panel stays blank or shows only the first pixel.', 'Panel Type') }}</label>
|
||||
<label for="panel_type" class="block text-sm font-medium text-gray-700">Panel Type{{ ui.help_tip('Special initialization for panels with a specific driver chip (e.g. FM6126A, FM6127).\nLeave on Standard unless your panel stays blank or shows only the first pixel. FM6124 / FM6124D / FM6124DJ panels need no initialization: use Standard.', 'Panel Type') }}</label>
|
||||
<select id="panel_type" name="panel_type" class="form-control">
|
||||
<option value="" {% if not main_config.display.hardware.get('panel_type', '') %}selected{% endif %}>Standard</option>
|
||||
<option value="FM6126A" {% if main_config.display.hardware.get('panel_type', '') == "FM6126A" %}selected{% endif %}>FM6126A</option>
|
||||
@@ -198,24 +197,25 @@
|
||||
</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.', '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.', 'Row Address Type') }}</label>
|
||||
<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>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="form-group" id="setting-display-gpio_slowdown" data-setting-key="display.runtime.gpio_slowdown">
|
||||
<label for="gpio_slowdown" class="block text-sm font-medium text-gray-700">GPIO Slowdown{{ ui.help_tip('Slows the GPIO signal so the panel keeps up.\nGuide: Pi 3 → 1–2, Pi 4 → 2–4, Pi 5 (PIO) → 1–3. Increase if the display shows garbage or flicker; in RIO mode higher values may improve performance.', 'GPIO Slowdown') }}</label>
|
||||
<label for="gpio_slowdown" class="block text-sm font-medium text-gray-700">GPIO Slowdown{{ ui.help_tip('Slows the GPIO signal so the panel keeps up.\nGuide: Pi 3 → 1–2, Pi 4 → 2–4, Pi 5 (PIO) → 1–3. Panels on Row Address Type 5 (SM5368 row drivers) can need 6–8 on a Pi 4. Increase if the display shows garbage, jumping rows or flicker; in RIO mode higher values may improve performance.', 'GPIO Slowdown') }}</label>
|
||||
<input type="number"
|
||||
id="gpio_slowdown"
|
||||
name="gpio_slowdown"
|
||||
value="{{ main_config.display.runtime.gpio_slowdown or 3 }}"
|
||||
value="{{ main_config.display.get('runtime', {}).get('gpio_slowdown', 3) }}"
|
||||
min="0"
|
||||
max="10"
|
||||
class="form-control">
|
||||
@@ -256,26 +256,26 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="setting-display-pwm_dither_bits" data-setting-key="display.hardware.pwm_dither_bits">
|
||||
<label for="pwm_dither_bits" class="block text-sm font-medium text-gray-700">PWM Dither Bits{{ ui.help_tip('Time-dithering to gain apparent color depth (0–4).\nDefault: 0. Raising it can smooth gradients at the cost of a slightly lower refresh rate.', 'PWM Dither Bits') }}</label>
|
||||
<label for="pwm_dither_bits" class="block text-sm font-medium text-gray-700">PWM Dither Bits{{ ui.help_tip('Time-dithering to gain apparent color depth (0–2).\nDefault: 1. Raising it can smooth gradients at the cost of a slightly lower refresh rate.', 'PWM Dither Bits') }}</label>
|
||||
<input type="number"
|
||||
id="pwm_dither_bits"
|
||||
name="pwm_dither_bits"
|
||||
value="{{ main_config.display.hardware.pwm_dither_bits or 1 }}"
|
||||
value="{{ main_config.display.hardware.get('pwm_dither_bits', 1) }}"
|
||||
min="0"
|
||||
max="4"
|
||||
max="2"
|
||||
class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-group" id="setting-display-pwm_lsb_nanoseconds" data-setting-key="display.hardware.pwm_lsb_nanoseconds">
|
||||
<label for="pwm_lsb_nanoseconds" class="block text-sm font-medium text-gray-700">PWM LSB Nanoseconds{{ ui.help_tip('Base time for the least-significant color bit (50–500 ns).\nDefault: 130. Raising it can reduce flicker on some panels but lowers the maximum refresh rate.', 'PWM LSB Nanoseconds') }}</label>
|
||||
<label for="pwm_lsb_nanoseconds" class="block text-sm font-medium text-gray-700">PWM LSB Nanoseconds{{ ui.help_tip('Base time for the least-significant color bit (50–3000 ns).\nDefault: 130. Raising it can reduce flicker on some panels but lowers the maximum refresh rate.', 'PWM LSB Nanoseconds') }}</label>
|
||||
<input type="number"
|
||||
id="pwm_lsb_nanoseconds"
|
||||
name="pwm_lsb_nanoseconds"
|
||||
value="{{ main_config.display.hardware.pwm_lsb_nanoseconds or 130 }}"
|
||||
min="50"
|
||||
max="500"
|
||||
max="3000"
|
||||
class="form-control">
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user