fix(web): reject true and 5.5 for row_address_type and multiplexing

Both still went straight through int(), so a JSON true saved as 1 and 5.5
as 5. They now use the shared hardware range check like the other panel
fields. Review feedback on #586.

Also: the RP1 Backend tooltip said it is ignored on Pi 3/4 (it is ignored
on every model but the Pi 5), and the README gave the dynamic-duration
default cap as 90s; the code default is 180s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-15 17:14:07 -04:00
co-authored by Claude Opus 5
parent 5709036e3c
commit 768517a658
5 changed files with 12 additions and 25 deletions
+5 -4
View File
@@ -54,10 +54,11 @@ Web interface:
parallel is 1–3 and PWM dither bits 0–2, matching the library. A stored GPIO parallel is 1–3 and PWM dither bits 0–2, matching the library. A stored GPIO
slowdown, PWM dither bits or refresh-rate cap of 0 no longer shows (and slowdown, PWM dither bits or refresh-rate cap of 0 no longer shows (and
re-saves) as 3, 1 or 120, and the refresh cap accepts 0 (no cap). The config re-saves) as 3, 1 or 120, and the refresh cap accepts 0 (no cap). The config
API rejects out-of-range `rows`, `cols`, `chain_length`, `parallel`, API rejects out-of-range or non-integer `rows`, `cols`, `chain_length`,
`brightness`, `scan_mode`, `pwm_bits`, `pwm_dither_bits`, `parallel`, `brightness`, `scan_mode`, `pwm_bits`, `pwm_dither_bits`,
`pwm_lsb_nanoseconds`, `limit_refresh_rate_hz` and `gpio_slowdown` with a `pwm_lsb_nanoseconds`, `limit_refresh_rate_hz`, `row_address_type`,
400 instead of saving a config the matrix refuses to start with. `multiplexing` and `gpio_slowdown` with a 400 (JSON `true` or `5.5` used to
save as 1 or 5) instead of saving a config the matrix refuses to start with.
- Display setting help tips and README / config-reference entries corrected - Display setting help tips and README / config-reference entries corrected
and completed: `panel_type` and `rp1_rio` are documented, and completed: `panel_type` and `rp1_rio` are documented,
`show_refresh_rate` prints to the console rather than drawing on the panel, `show_refresh_rate` prints to the console rather than drawing on the panel,
+1 -1
View File
@@ -701,7 +701,7 @@ Controls how long each installed plugin stays visible in seconds before switchin
- Some plugins can automatically adjust their display time based on content - Some plugins can automatically adjust their display time based on content
- This setting limits how long they can extend (prevents one display from dominating) - This setting limits how long they can extend (prevents one display from dominating)
- Example: If set to 60, a plugin can extend up to 60 seconds even if it requests longer - Example: If set to 60, a plugin can extend up to 60 seconds even if it requests longer
- Leave unset to use the default cap (typically 90 seconds) - Leave unset to use the default cap (180 seconds; the web UI accepts 30-1800)
### Example Configuration ### Example Configuration
+4 -1
View File
@@ -87,7 +87,8 @@ def test_waveshare_96x48_v2_settings_all_save(api_v3_client, saved, as_strings):
('cols', 16), ('cols', 192), ('cols', 512), ('cols', 16), ('cols', 192), ('cols', 512),
('chain_length', 1), ('chain_length', 32), ('chain_length', 1), ('chain_length', 32),
('parallel', 3), ('parallel', 3),
('row_address_type', 0), ('row_address_type', 5), ('row_address_type', 0), ('row_address_type', 5), ('row_address_type', 5.0),
('multiplexing', 0), ('multiplexing', 22),
('gpio_slowdown', 0), ('gpio_slowdown', 10), ('gpio_slowdown', 0), ('gpio_slowdown', 10),
('pwm_bits', 1), ('pwm_bits', 11), ('pwm_bits', 1), ('pwm_bits', 11),
('pwm_dither_bits', 0), ('pwm_dither_bits', 2), ('pwm_dither_bits', 0), ('pwm_dither_bits', 2),
@@ -108,6 +109,8 @@ def test_values_in_range_are_saved(api_v3_client, saved, field, value):
('chain_length', 0), ('chain_length', 0),
('parallel', 0), ('parallel', 4), ('parallel', 0), ('parallel', 4),
('row_address_type', -1), ('row_address_type', 6), ('row_address_type', -1), ('row_address_type', 6),
('row_address_type', True), ('row_address_type', 5.5),
('multiplexing', -1), ('multiplexing', 23), ('multiplexing', True),
('gpio_slowdown', -1), ('gpio_slowdown', 11), ('gpio_slowdown', -1), ('gpio_slowdown', 11),
('pwm_bits', 0), ('pwm_bits', 12), ('pwm_bits', 0), ('pwm_bits', 12),
('pwm_dither_bits', 3), ('pwm_dither_bits', 3),
+1 -18
View File
@@ -555,15 +555,6 @@ def save_main_config():
if 'panel_type' in data and data['panel_type'] not in PANEL_TYPE_ALLOWED: if 'panel_type' in data and data['panel_type'] not in PANEL_TYPE_ALLOWED:
return jsonify({'status': 'error', 'message': f"Invalid panel type '{data['panel_type']}'. Allowed values: Standard (empty), FM6126A, FM6127"}), 400 return jsonify({'status': 'error', 'message': f"Invalid panel type '{data['panel_type']}'. Allowed values: Standard (empty), FM6126A, FM6127"}), 400
# Validate multiplexing
if 'multiplexing' in data:
try:
mux_val = int(data['multiplexing'])
if mux_val < 0 or mux_val > 22:
return jsonify({'status': 'error', 'message': f"Invalid multiplexing value '{data['multiplexing']}'. Must be an integer from 0 to 22."}), 400
except (ValueError, TypeError, OverflowError):
return jsonify({'status': 'error', 'message': f"Invalid multiplexing value '{data['multiplexing']}'. Must be an integer from 0 to 22."}), 400
# Validate pixel_mapper_config (free-form mapper string, e.g. "U-mapper;Rotate:90") # Validate pixel_mapper_config (free-form mapper string, e.g. "U-mapper;Rotate:90")
if 'pixel_mapper_config' in data and not isinstance(data['pixel_mapper_config'], str): if 'pixel_mapper_config' in data and not isinstance(data['pixel_mapper_config'], str):
return jsonify({'status': 'error', 'message': 'pixel_mapper_config must be a string (e.g. "U-mapper;Rotate:90" or empty)'}), 400 return jsonify({'status': 'error', 'message': 'pixel_mapper_config must be a string (e.g. "U-mapper;Rotate:90" or empty)'}), 400
@@ -573,15 +564,6 @@ def save_main_config():
if 'orientation' in data and data['orientation'] not in ORIENTATION_ALLOWED: if 'orientation' in data and data['orientation'] not in ORIENTATION_ALLOWED:
return jsonify({'status': 'error', 'message': f"Invalid orientation '{data['orientation']}'. Allowed values: {', '.join(sorted(ORIENTATION_ALLOWED))}"}), 400 return jsonify({'status': 'error', 'message': f"Invalid orientation '{data['orientation']}'. Allowed values: {', '.join(sorted(ORIENTATION_ALLOWED))}"}), 400
# Validate row_address_type
if 'row_address_type' in data:
try:
rat_val = int(data['row_address_type'])
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 5."}), 400
# Panel geometry, PWM and GPIO timing, held to what the rgbmatrix library # Panel geometry, PWM and GPIO timing, held to what the rgbmatrix library
# accepts (RGBMatrix::Options::Validate in lib/options-initialize.cc, # accepts (RGBMatrix::Options::Validate in lib/options-initialize.cc,
# the gpio_slowdown check in lib/led-matrix.cc). Outside those ranges # the gpio_slowdown check in lib/led-matrix.cc). Outside those ranges
@@ -617,6 +599,7 @@ def save_main_config():
('pwm_bits', 1, 11, False), ('pwm_dither_bits', 0, 2, False), ('pwm_bits', 1, 11, False), ('pwm_dither_bits', 0, 2, False),
('pwm_lsb_nanoseconds', 50, 3000, False), ('pwm_lsb_nanoseconds', 50, 3000, False),
('limit_refresh_rate_hz', 0, None, False), ('limit_refresh_rate_hz', 0, None, False),
('row_address_type', 0, 5, False), ('multiplexing', 0, 22, False),
('gpio_slowdown', 0, 10, False)): ('gpio_slowdown', 0, 10, False)):
if field in data: if field in data:
error = _hardware_int_error(field, low, high, even) error = _hardware_int_error(field, low, high, even)
@@ -222,7 +222,7 @@
<div class="form-group" id="setting-display-rp1_rio" data-setting-key="display.runtime.rp1_rio"> <div class="form-group" id="setting-display-rp1_rio" data-setting-key="display.runtime.rp1_rio">
<label for="rp1_rio" class="block text-sm font-medium text-gray-700"> <label for="rp1_rio" class="block text-sm font-medium text-gray-700">
RP1 Backend <span class="text-xs text-gray-400 font-normal">(Pi 5 only)</span>{{ ui.help_tip('Pi 5 RP1 coprocessor driver mode.\nPIO (0) is the default and uses less CPU. RIO (1) can push a higher refresh rate but inverts the GPIO Slowdown behavior. Ignored on Pi 3/4.', 'RP1 Backend') }} RP1 Backend <span class="text-xs text-gray-400 font-normal">(Pi 5 only)</span>{{ ui.help_tip('Pi 5 RP1 coprocessor driver mode.\nPIO (0) is the default and uses less CPU. RIO (1) can push a higher refresh rate but inverts the GPIO Slowdown behavior. Ignored on every other Pi model.', 'RP1 Backend') }}
</label> </label>
<select id="rp1_rio" name="rp1_rio" class="form-control"> <select id="rp1_rio" name="rp1_rio" class="form-control">
<option value="0" {% if main_config.display.get('runtime', {}).get('rp1_rio', 0)|int == 0 %}selected{% endif %}>0 &mdash; PIO (default, low CPU)</option> <option value="0" {% if main_config.display.get('runtime', {}).get('rp1_rio', 0)|int == 0 %}selected{% endif %}>0 &mdash; PIO (default, low CPU)</option>